feat(scripts): polymorphic script owner (app XOR group) — Phase 4 foundation
Phase 4-lite C1. Make script ownership polymorphic so a script can be owned
by a GROUP (a template inherited by descendant apps) instead of an app —
mirroring vars/secrets (0048/0049), but ON DELETE RESTRICT (code is not data).
Schema (0050): `scripts.group_id` (nullable FK→groups RESTRICT), `app_id` made
nullable, `scripts_owner_exactly_one` CHECK, and the per-app name index split
into two per-owner partial-unique indexes. Existing app-owned rows keep their
exact `(app_id, lower(name))` uniqueness.
Type: `Script.app_id` becomes `Option<AppId>`; add `Script.group_id` +
`ScriptOwner` / `is_owned_by_app()`. `NewScript` gains the same polymorphic
owner. The execution-context app (what a script runs *under*) is supplied by
the invoking route/trigger/caller, never read off the script — group scripts
have no single app.
Behavior is fully preserved for app-owned scripts (the only kind creatable
today): every isolation backstop and authz site now uses `is_owned_by_app`,
which is byte-identical for app owners and **fails closed** for group owners.
A group script therefore can't yet be run, route/trigger-bound, invoked, or
managed via the app-script API — those land in C2 (group-script creation) and
C3 (chain-membership resolution + binding). The `/execute/{id}` bypass 404s a
group script (no app context to run under).
Re-blesses expected_schema.txt; note the golden was last blessed at migration
0044, so this also captures the already-committed 0045–0049 schema.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -126,8 +126,14 @@ where
|
||||
if !script.enabled {
|
||||
return Err(ApiError::NotFound(id));
|
||||
}
|
||||
// The direct-execute bypass runs a script under its *owning app's*
|
||||
// context. A group-owned script (Phase 4) is a template with no single
|
||||
// app, so it cannot be invoked through this id-addressed bypass — it
|
||||
// runs only via a descendant app's route/trigger, which supplies the
|
||||
// execution-context app. Fail closed (404) rather than guess an app.
|
||||
let app_id = script.app_id.ok_or(ApiError::NotFound(id))?;
|
||||
|
||||
let mut req = build_exec_request(id, &script.name, &headers, &body, script.app_id, principal)?;
|
||||
let mut req = build_exec_request(id, &script.name, &headers, &body, app_id, principal)?;
|
||||
req.sandbox_overrides = script.sandbox;
|
||||
let request_id = req.request_id;
|
||||
let request_path = req.path.clone();
|
||||
@@ -151,7 +157,7 @@ where
|
||||
// audit-visible platform — but a sink failure must not mask the
|
||||
// user-facing result, so we only log a warning if it fails.
|
||||
let log = build_execution_log(
|
||||
script.app_id,
|
||||
app_id,
|
||||
id,
|
||||
request_id,
|
||||
request_path,
|
||||
|
||||
Reference in New Issue
Block a user