feat(v1.1.9): InvokeServiceImpl + RouteTable wiring in picloud binary

manager-core/invoke_service.rs implements InvokeService over:
  - ScriptRepository (for Id + Name resolution)
  - RouteTable (for Path resolution via the orchestrator's matcher)
  - OutboxRepo (for invoke_async)

Same-app guard runs at the service entry point: resolve_id() always
verifies resolved.app_id == cx.app_id and returns InvokeError::CrossApp
otherwise. resolve_name() reads cx.app_id when querying (so a wrong
app_id from somewhere else couldn't slip through), but defense-in-depth
won't hurt — future hardening if it surfaces.

resolve_path() runs the matcher against this app's routes only
(RouteTable::snapshot_for_app). Method assumed POST→GET fallback for
v1.1.9 — surfacing method via the SDK is a future addition.

enqueue_async() resolves the target, then writes one outbox row with
source_kind = 'invoke'. The payload carries script_id, script_name,
args, fresh execution_id, root_execution_id (inherited from caller),
trigger_depth + 1. caller principal recorded as origin_principal
(forensic only — the executing script runs with no principal). One
shot — no retry policy on the row; users wrap in retry::with() if
they want retries.

picloud/lib.rs:
  - route_table construction moved BEFORE Services::new so the invoke
    service can hold it. populates from route_repo as before.
  - InvokeServiceImpl wired into Services::new in place of the
    NoopInvokeService placeholder.

Unit tests cover: resolve by Id same-app, cross-app rejected, resolve
by Name finds + not-found, enqueue_async writes Invoke outbox row with
trigger_depth=1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-06 19:48:53 +02:00
parent 36bf046791
commit 563c44ad95
3 changed files with 434 additions and 12 deletions

View File

@@ -274,11 +274,27 @@ pub async fn build_app(
authz.clone(),
),
);
// v1.1.9 function composition. Real InvokeService wired in commit 8;
// the noop here keeps the binary compiling — without it, scripts
// calling `invoke()` will throw a clear error.
let invoke: Arc<dyn picloud_shared::InvokeService> =
Arc::new(picloud_shared::NoopInvokeService);
// Route table created early (before Services) so InvokeServiceImpl
// can use it for path resolution. It's populated below from the
// route_repo, then re-populated whenever the admin layer writes
// routes.
let route_table = Arc::new(RouteTable::new());
let initial = route_repo.list_all().await?;
let compiled = compile_routes(&initial)
.map_err(|e| anyhow::anyhow!("failed to compile stored routes: {e}"))?;
route_table.replace_all(compiled);
// v1.1.9 function composition. InvokeServiceImpl resolves targets
// by Id, Name (via get_by_name), or Path (via the orchestrator's
// RouteTable). invoke_async writes an OutboxSourceKind::Invoke row
// that the dispatcher fires through the standard executor path.
let invoke: Arc<dyn picloud_shared::InvokeService> = Arc::new(
picloud_manager_core::invoke_service::InvokeServiceImpl::new(
Arc::new(PostgresScriptRepoHandle(script_repo.clone())),
route_table.clone(),
outbox_repo.clone(),
),
);
let services = Services::new(
kv,
docs,
@@ -296,13 +312,6 @@ pub async fn build_app(
);
let engine = Arc::new(Engine::new(Limits::default(), services));
// Compile the routes table once at startup; admin writes refresh it.
let route_table = Arc::new(RouteTable::new());
let initial = route_repo.list_all().await?;
let compiled = compile_routes(&initial)
.map_err(|e| anyhow::anyhow!("failed to compile stored routes: {e}"))?;
route_table.replace_all(compiled);
// Same shape for app domains (Host → app_id cache).
let app_domain_table = Arc::new(AppDomainTable::new());
let initial_domains = domains_repo.list_all().await?;