feat(executor): thread the defining node into ExecRequest (Phase 4b C2)
Carry the resolved script's owner (its defining node) from dispatch into the executor so `import` resolution can be lexical (§5.5). The executing app (`app_id`) stays the SDK isolation boundary; `script_owner` is a separate axis — the lexical origin for imports. - `ExecRequest.script_owner: Option<ScriptOwner>` (serde default; `None` falls back to `App(app_id)` in the engine for old payloads / cluster wire). - `ScriptOwner` gains `Serialize`/`Deserialize` for the wire. - The engine computes `default_origin` and hands it to the resolver (used fully in C3; the resolve() lookup already uses it). - Every dispatch site sets it from the resolved `Script.owner()`: the 4 dispatcher arms (queue/trigger/http/invoke_async, via a new `ResolvedTrigger.script_owner`), the orchestrator id-bypass, and the `invoke()` SDK path (new `ResolvedScript.owner`, so a group script invoked by an app resolves imports from the group). Behaviour-preserving: app scripts still resolve `App(app_id)`; group scripts can't yet carry imports (lifted in C4). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -300,11 +300,18 @@ impl Engine {
|
||||
// installed with the real per-call resolver. The resolver owns
|
||||
// `cx.clone()` so cross-app isolation derives from this exact
|
||||
// call's context, not from any script-passed argument.
|
||||
// The lexical origin for `import` resolution (§5.5): the top-level
|
||||
// script's defining node. Falls back to the executing app when the
|
||||
// request predates Phase 4b (old payloads / cluster wire).
|
||||
let default_origin = req
|
||||
.script_owner
|
||||
.unwrap_or(picloud_shared::ScriptOwner::App(req.app_id));
|
||||
let resolver = PicloudModuleResolver::new(
|
||||
self.services.modules.clone(),
|
||||
cx.clone(),
|
||||
self.module_cache.clone(),
|
||||
effective_limits.module_import_depth_max,
|
||||
default_origin,
|
||||
);
|
||||
engine.set_module_resolver(resolver);
|
||||
let self_engine = self.self_arc();
|
||||
|
||||
@@ -26,7 +26,9 @@ use std::sync::{Arc, Mutex};
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use lru::LruCache;
|
||||
use picloud_shared::{AppId, ModuleSource, ModuleSourceError, SdkCallCx, ValidatedScript};
|
||||
use picloud_shared::{
|
||||
AppId, ModuleSource, ModuleSourceError, ScriptOwner, SdkCallCx, ValidatedScript,
|
||||
};
|
||||
use rhai::module_resolvers::ModuleResolver;
|
||||
use rhai::{Engine as RhaiEngine, EvalAltResult, Module, Position, Shared, AST};
|
||||
|
||||
@@ -96,6 +98,12 @@ pub struct PicloudModuleResolver {
|
||||
/// via `PICLOUD_MODULE_IMPORT_DEPTH_MAX`. Read from `Limits` at
|
||||
/// resolver construction.
|
||||
depth_limit: u32,
|
||||
|
||||
/// Lexical origin for a top-level `import` — the defining node of the
|
||||
/// script being executed (Phase 4b, §5.5). Used when Rhai gives no
|
||||
/// `_source` (the entry AST). Nested imports inside a resolved module
|
||||
/// override this via the module AST's source (C3).
|
||||
default_origin: ScriptOwner,
|
||||
}
|
||||
|
||||
impl PicloudModuleResolver {
|
||||
@@ -105,6 +113,7 @@ impl PicloudModuleResolver {
|
||||
cx: Arc<SdkCallCx>,
|
||||
cache: Arc<ModuleCache>,
|
||||
depth_limit: u32,
|
||||
default_origin: ScriptOwner,
|
||||
) -> Self {
|
||||
Self {
|
||||
source,
|
||||
@@ -113,6 +122,7 @@ impl PicloudModuleResolver {
|
||||
in_progress: Mutex::new(Vec::new()),
|
||||
depth: Mutex::new(0),
|
||||
depth_limit,
|
||||
default_origin,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,19 +329,14 @@ impl ModuleResolver for PicloudModuleResolver {
|
||||
))
|
||||
})?;
|
||||
|
||||
// C1 (Phase 4b): resolve via the owner-aware `resolve`. Until C3
|
||||
// threads the importing script's true defining node through, the
|
||||
// origin is the calling app — behaviour-preserving for app-owned
|
||||
// scripts (group scripts can't yet carry imports). The app-rooted
|
||||
// chain walk additionally lets an app import an ancestor group's
|
||||
// modules, which is the intended Phase 4b capability.
|
||||
// Lexical resolution (§5.5): resolve `path` against the importing
|
||||
// node's chain. For the entry script that's `default_origin` (its
|
||||
// defining node); nested-import origin via `_source` lands in C3.
|
||||
// An app-rooted walk reaches ancestor group modules; a group-rooted
|
||||
// walk is sealed from app modules below it (the trust boundary).
|
||||
let origin = self.default_origin;
|
||||
let lookup_result: Result<Option<picloud_shared::ModuleScript>, ModuleSourceError> =
|
||||
tokio::task::block_in_place(|| {
|
||||
handle.block_on(
|
||||
self.source
|
||||
.resolve(picloud_shared::ScriptOwner::App(self.cx.app_id), path),
|
||||
)
|
||||
});
|
||||
tokio::task::block_in_place(|| handle.block_on(self.source.resolve(origin, path)));
|
||||
|
||||
let module_row = match lookup_result {
|
||||
Ok(Some(m)) => m,
|
||||
|
||||
@@ -179,6 +179,10 @@ fn invoke_blocking(
|
||||
rest: String::new(),
|
||||
sandbox_overrides: picloud_shared::ScriptSandbox::default(),
|
||||
app_id: cx.app_id,
|
||||
// Lexical origin (§5.5): the callee's defining node — a group
|
||||
// script's imports resolve from the group even when invoked by an
|
||||
// app. `None` falls back to `App(cx.app_id)` in the engine.
|
||||
script_owner: resolved.owner,
|
||||
// Same-app invoke is a function call, not a re-auth boundary —
|
||||
// inherit the caller's principal.
|
||||
principal: cx.principal.clone(),
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::collections::BTreeMap;
|
||||
use chrono::{DateTime, Utc};
|
||||
use picloud_shared::{
|
||||
AppId, ExecutionId, ExecutionLog, ExecutionSource, ExecutionStatus, Principal, RequestId,
|
||||
ScriptId, ScriptSandbox, TriggerEvent,
|
||||
ScriptId, ScriptOwner, ScriptSandbox, TriggerEvent,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
@@ -69,6 +69,16 @@ pub struct ExecRequest {
|
||||
/// Internal-only; not surfaced via `ctx` (which the script sees).
|
||||
pub app_id: AppId,
|
||||
|
||||
/// The **defining node** of the top-level script being executed —
|
||||
/// the resolved `Script`'s owner (Phase 4b). This is the lexical
|
||||
/// origin its `import` statements resolve against (§5.5): an inherited
|
||||
/// group endpoint's imports seal to the **group**, not the inheriting
|
||||
/// app, so a leaf can't shadow them. Distinct from `app_id`, which is
|
||||
/// always the execution boundary (where SDK calls scope). `None` (old
|
||||
/// payloads / cluster wire) falls back to `App(app_id)` in the engine.
|
||||
#[serde(default)]
|
||||
pub script_owner: Option<ScriptOwner>,
|
||||
|
||||
/// Caller identity, when authenticated. `None` for unauthenticated
|
||||
/// data-plane HTTP requests (the common case for public scripts);
|
||||
/// `Some` when a bearer token or session cookie was resolved.
|
||||
|
||||
Reference in New Issue
Block a user