chore(v1.1.3-modules): version bumps + CHANGELOG + blueprint touch-up

- Workspace `1.1.2` → `1.1.3` (`Cargo.toml`).
- Dashboard `0.8.0` → `0.9.0` (`package.json`).
- CHANGELOG: full v1.1.3 entry covering ScriptKind, ModuleSource,
  PicloudModuleResolver, the two caches, dep-graph table, route +
  trigger module rejection, the latent cross-app trigger gap that
  this release closes, migrations 0015/0016, and downgrade caveats.
- Blueprint: mark the "Can scripts `import` Rhai modules?" question
  as resolved; one-line pointer to the v1.1.3 semantics.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-02 22:28:02 +02:00
parent 610fd4ffa2
commit 10f76d29ca
5 changed files with 115 additions and 12 deletions

View File

@@ -1,5 +1,108 @@
# PiCloud Changelog # PiCloud Changelog
## v1.1.3 — Modules (unreleased)
Real per-app Rhai module system. Scripts can `import "<name>" as
<alias>;` other scripts in the same app as reusable libraries. The
v1.0 placeholder `DummyModuleResolver` is replaced by a per-call
`PicloudModuleResolver` that loads `kind = 'module'` scripts via a
new `ModuleSource` trait, compiles them into Rhai modules, caches
the compiled output, and enforces cross-app isolation, circular-
import detection, and an import-depth limit. Two LRU AST caches
(top-level script + per-module compiled module) eliminate the
per-invocation compile cost; both invalidate on `updated_at` change.
### Added
- **`scripts.kind` column** — `'endpoint' | 'module'`, default
`'endpoint'`. Endpoints handle HTTP routes / trigger events;
modules are libraries imported by other scripts. The dashboard
scripts list + script detail page surface the distinction as a
colored badge.
- **`script_imports` dep-graph table** — populated at script save-
time from the literal-path `import "<name>"` declarations in the
source. FK-CASCADE on both columns. No admin surface in v1.1.3
(drives a v1.2+ "Used by" dashboard panel and v1.3+ cluster-mode
eager invalidation).
- **`ModuleSource` trait** — `lookup(&SdkCallCx, name)`. Postgres
impl `PostgresModuleSource` in manager-core. `app_id` derived from
`cx.app_id` (cross-app isolation boundary, mirrors KV / docs).
- **`PicloudModuleResolver`** — implements `rhai::ModuleResolver`.
Per-call instance owns `Arc<SdkCallCx>`, the in-progress imports
stack, the depth counter. Bridges sync `resolve()` to async
`lookup()` via `Handle::block_on` (safe under the executor's
`spawn_blocking` wrap). Replaces `DummyModuleResolver` at line 139
of `executor-core::engine::build_engine`.
- **Module-shape validation** — `kind = 'module'` source must contain
only `fn` declarations, `const` declarations, and `import`
statements at top level (no executable expressions). Walks
`ast.statements()` via `rhai/internals`. Admin endpoint is the
primary gate; the resolver re-runs the check at load time for
defense in depth against DB-direct inserts.
- **Per-module compiled-Module cache** — `LruCache<(AppId, name),
(updated_at, Arc<rhai::Module>)>` owned by `Engine`. Invalidated
lazily on `updated_at` mismatch. Size via
`PICLOUD_MODULE_CACHE_SIZE` (default 512).
- **Top-level script AST cache** — `LruCache<ScriptId, (updated_at,
Arc<rhai::AST>)>` owned by `LocalExecutorClient`. Same staleness
semantics. Size via `PICLOUD_SCRIPT_CACHE_SIZE` (default 256).
- **`ScriptIdentity` + `ExecutorClient::execute_with_identity`** —
new method on the trait; default impl forwards to `execute` so
`RemoteExecutorClient` (and future transports) keep working.
`LocalExecutorClient` overrides it to consult the script cache and
pass the resulting `Arc<rhai::AST>` to `Engine::execute_ast`.
- **`Engine::execute_ast`** — companion to `execute` that takes a
pre-compiled AST so callers (the orchestrator) can reuse one
compile across many invocations.
- **Import depth limit** — `Limits::module_import_depth_max`
(default 8). Not script-overridable.
- **Reserved module names** — module-kind scripts cannot be named
`log`, `regex`, `random`, `time`, `json`, `base64`, `hex`, `url`,
`kv`, `docs`, `dead_letters`, `http`, `files`, `pubsub`, `secrets`,
`email`, `users`, `queue`. Defense against author confusion with
stdlib namespaces.
### Changed
- **Workspace version**: `1.1.2` → `1.1.3`.
- **Rhai SDK version**: `1.3` → `1.4` (additive — every v1.3 script
still runs unchanged; new surface: `import "<name>" as <alias>;`
for endpoint scripts that consume modules in the same app).
- **Dashboard version**: `0.8.0` → `0.9.0`. Adds kind dropdown on
script create + kind badges on the scripts list and detail page.
- **`Services` bundle** — grows a `modules: Arc<dyn ModuleSource>`
field. Constructor signature becomes
`Services::new(kv, docs, dead_letters, events, modules)`.
- **`ScriptValidator` trait** — `validate` now returns
`ValidatedScript { imports: Vec<String> }` so the repo can write
dep-graph edges in the same transaction as the script row. New
`validate_module` method enforces module-shape rules.
- **Trigger creation tightening** — `POST /api/v1/admin/apps/{id}/triggers/{kv,docs,dead_letter}`
now load the target script and reject when (1) it doesn't exist,
(2) it belongs to a different app (latent v1.1.1/v1.1.2 gap —
closed in v1.1.3), or (3) it is `kind = 'module'`.
- **Route creation** — `POST /api/v1/admin/scripts/{id}/routes`
returns 400 when the target script is `kind = 'module'`.
### Migrations
- `0015_scripts_kind.sql` — adds `scripts.kind` with CHECK
`IN ('endpoint','module')`, composite index `(app_id, kind)`, and
a module-name shape CHECK (`^[a-zA-Z_][a-zA-Z0-9_]{0,63}$`).
- `0016_script_imports.sql` — adds the dep-graph table with FK
CASCADE on both columns, PK `(importer, imported)`, and a
reverse-edge index on `imported_script_id`.
### Downgrade caveats
Rolling back v1.1.3 → v1.1.2 with module-kind scripts present
strands them (no `kind` column means everything looks like an
endpoint; modules will then succeed as route targets and immediately
fail to execute meaningfully). Migration `0016_script_imports.sql`
is safe to drop (the table is auxiliary). `0015_scripts_kind.sql`
must be reversed by `DROP COLUMN kind` only after manually re-homing
or deleting module-kind rows.
## v1.1.2 — Documents (unreleased) ## v1.1.2 — Documents (unreleased)
`docs::*` SDK — schemaless JSONB document storage with a first-cut `docs::*` SDK — schemaless JSONB document storage with a first-cut

18
Cargo.lock generated
View File

@@ -1514,7 +1514,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]] [[package]]
name = "picloud" name = "picloud"
version = "1.1.2" version = "1.1.3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@@ -1540,7 +1540,7 @@ dependencies = [
[[package]] [[package]]
name = "picloud-cli" name = "picloud-cli"
version = "1.1.2" version = "1.1.3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"assert_cmd", "assert_cmd",
@@ -1561,7 +1561,7 @@ dependencies = [
[[package]] [[package]]
name = "picloud-executor" name = "picloud-executor"
version = "1.1.2" version = "1.1.3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"picloud-executor-core", "picloud-executor-core",
@@ -1573,7 +1573,7 @@ dependencies = [
[[package]] [[package]]
name = "picloud-executor-core" name = "picloud-executor-core"
version = "1.1.2" version = "1.1.3"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"base64", "base64",
@@ -1595,7 +1595,7 @@ dependencies = [
[[package]] [[package]]
name = "picloud-manager" name = "picloud-manager"
version = "1.1.2" version = "1.1.3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"picloud-manager-core", "picloud-manager-core",
@@ -1607,7 +1607,7 @@ dependencies = [
[[package]] [[package]]
name = "picloud-manager-core" name = "picloud-manager-core"
version = "1.1.2" version = "1.1.3"
dependencies = [ dependencies = [
"argon2", "argon2",
"async-trait", "async-trait",
@@ -1632,7 +1632,7 @@ dependencies = [
[[package]] [[package]]
name = "picloud-orchestrator" name = "picloud-orchestrator"
version = "1.1.2" version = "1.1.3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"picloud-orchestrator-core", "picloud-orchestrator-core",
@@ -1644,7 +1644,7 @@ dependencies = [
[[package]] [[package]]
name = "picloud-orchestrator-core" name = "picloud-orchestrator-core"
version = "1.1.2" version = "1.1.3"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -1665,7 +1665,7 @@ dependencies = [
[[package]] [[package]]
name = "picloud-shared" name = "picloud-shared"
version = "1.1.2" version = "1.1.3"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"chrono", "chrono",

View File

@@ -13,7 +13,7 @@ members = [
] ]
[workspace.package] [workspace.package]
version = "1.1.2" version = "1.1.3"
edition = "2021" edition = "2021"
rust-version = "1.92" rust-version = "1.92"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"

View File

@@ -1,6 +1,6 @@
{ {
"name": "picloud-dashboard", "name": "picloud-dashboard",
"version": "0.8.0", "version": "0.9.0",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {

View File

@@ -1772,7 +1772,7 @@ if allowed {
- [ ] **Debugging**: How to trace interceptor execution in logs/dashboard? - [ ] **Debugging**: How to trace interceptor execution in logs/dashboard?
### Rhai & SDK ### Rhai & SDK
- [ ] **Module loading**: Can scripts `import` external Rhai modules? (probably no for MVP) - [x] **Module loading**: Scripts can `import "<name>" as <alias>;` other scripts in the same app (v1.1.3 — `scripts.kind = 'module'`). Per-app, cross-app isolated, cache-invalidated on `updated_at` change. External (off-platform) modules remain out of scope.
- [ ] **File system access**: Can scripts read/write to local filesystem? (no for MVP) - [ ] **File system access**: Can scripts read/write to local filesystem? (no for MVP)
- [ ] **Request/response sizes**: Max payload size? (set sensible default, e.g., 10MB) - [ ] **Request/response sizes**: Max payload size? (set sensible default, e.g., 10MB)