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:
MechaCat02
2026-06-26 07:17:33 +02:00
parent 7fef098b48
commit 1844bef132
25 changed files with 86 additions and 17 deletions

View File

@@ -31,7 +31,7 @@ use picloud_orchestrator_core::{ExecutionGate, ExecutorClient};
use picloud_shared::{
AppId, DeadLetterId, ExecResponseSummary, ExecutionId, ExecutionLogSink, ExecutionSource,
HttpDispatchPayload, InboxDeliveryOutcome, InboxFailureKind, InboxResolver, InboxResult,
RequestId, Script, ScriptId, ScriptSandbox, TriggerEvent,
RequestId, Script, ScriptId, ScriptOwner, ScriptSandbox, TriggerEvent,
};
use rand::Rng;
use uuid::Uuid;
@@ -471,6 +471,7 @@ impl Dispatcher {
rest: String::new(),
sandbox_overrides: script.sandbox,
app_id: claimed.app_id,
script_owner: script.owner(),
principal: Some(principal),
trigger_depth: 0,
root_execution_id: execution_id,
@@ -830,6 +831,7 @@ impl Dispatcher {
is_dead_letter_handler: matches!(trigger.kind, TriggerKind::DeadLetter),
active: trigger.enabled && script.enabled,
script_id: script.id,
script_owner: script.owner(),
script_source: script.source,
script_name: script.name,
script_updated_at: script.updated_at,
@@ -872,6 +874,7 @@ impl Dispatcher {
rest: String::new(),
sandbox_overrides: resolved.sandbox_overrides,
app_id: row.app_id,
script_owner: resolved.script_owner,
principal: Some(principal),
trigger_depth: row.trigger_depth,
root_execution_id: row.root_execution_id.unwrap_or(execution_id),
@@ -935,6 +938,7 @@ impl Dispatcher {
rest: payload.rest,
sandbox_overrides: script.sandbox,
app_id: row.app_id,
script_owner: script.owner(),
// HTTP outbox rows don't run as the trigger registrant —
// they run with no principal (public ingress) or the
// attached one (origin_principal forensic field is not
@@ -953,6 +957,7 @@ impl Dispatcher {
// enqueue is dropped at fire time by the post-match active check.
active: script.enabled,
script_id,
script_owner: script.owner(),
script_source: script.source,
script_name: payload.script_name,
script_updated_at: script.updated_at,
@@ -1035,6 +1040,7 @@ impl Dispatcher {
rest: String::new(),
sandbox_overrides: script.sandbox,
app_id: row.app_id,
script_owner: script.owner(),
// invoke_async runs with no principal — same as HTTP outbox.
principal: None,
trigger_depth,
@@ -1054,6 +1060,7 @@ impl Dispatcher {
// enqueue is dropped at fire time by the post-match active check.
active: script.enabled,
script_id: script.id,
script_owner: script.owner(),
script_source: script.source,
script_name: script.name,
script_updated_at: script.updated_at,
@@ -1356,6 +1363,10 @@ pub struct ResolvedTrigger {
pub script_id: ScriptId,
pub script_source: String,
pub script_name: String,
/// Phase 4b: the resolved script's defining node — the lexical origin
/// for its `import`s (§5.5). `None` only for a corrupt owner row; the
/// executor then falls back to `App(app_id)`.
pub script_owner: Option<ScriptOwner>,
/// v1.1.3: freshness comparator for the orchestrator's top-level
/// script cache. The dispatcher hands `(script_id, updated_at)`
/// in alongside the source so cached ASTs can be reused across

View File

@@ -95,6 +95,7 @@ impl InvokeServiceImpl {
Ok(ResolvedScript {
script_id: script.id,
app_id: cx.app_id,
owner: script.owner(),
source: script.source,
updated_at: script.updated_at,
name: script.name,
@@ -124,6 +125,9 @@ impl InvokeServiceImpl {
// The execution-context app is always the caller's app — a group
// script runs under the inheriting app, never its owner.
app_id: cx.app_id,
// …but its `import`s resolve from its *defining node* (the group
// for an inherited script) — the lexical origin (§5.5).
owner: script.owner(),
source: script.source,
updated_at: script.updated_at,
name: script.name,