Skip to content

Commit a0ccad9

Browse files
committed
chore(changeset): patch release note for the action-body execution-context fix (#3914)
The Check Changeset gate requires every PR to add one; a runtime behavior fix this visible warrants a real release note rather than an empty declaration. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TufbW1LBcSwpta4gfX7Uso
1 parent 73d231d commit a0ccad9

1 file changed

Lines changed: 41 additions & 0 deletions

File tree

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
---
2+
"@objectstack/runtime": patch
3+
---
4+
5+
fix(runtime): action bodies execute under a real execution context — every owner-scoped write no longer dies FORBIDDEN
6+
7+
An action body's `ctx.api` was never bound. The sandbox's `buildSandboxApi`
8+
walked its whole fallback chain — no `actionCtx.api`, and the raw `ObjectQL`
9+
engine has no `.object()` (that lives on `ScopedContext`, reachable only via
10+
`engine.createContext()`, which the action path never called) — and landed on a
11+
repo facade that proxied every call to the engine with **no `context`**.
12+
`ctx.engine` had the identical hole.
13+
14+
Context-less is not "trusted", it is **identity-less**, and identity-less is
15+
strictly worse than either coherent posture: plugin-sharing's write gate
16+
short-circuits on `!context.userId` (no user to own the record) and its bypass
17+
needs `context.isSystem` (never set). So a `type: 'script'` action whose body
18+
called `ctx.api.object('crm_case').update(...)` failed with
19+
`FORBIDDEN: insufficient privileges to update crm_case` — **as the built-in
20+
admin** — while the `[action-audit]` line on the same request announced
21+
RLS-bypassing TRUSTED execution. Objects with a `public` sharing model, no
22+
owner field, or a bypass listing passed the gate early, so only *some* actions
23+
broke and the defect read as object-dependent flakiness.
24+
25+
Both dispatch paths (REST `/actions/:object/:action` and MCP `run_action`) now
26+
bind `ctx.api` to `engine.createContext(...)` and thread the same envelope
27+
through `ctx.engine`, matching what hook bodies already get from the engine's
28+
`buildHookApi`. The envelope is the caller's `ExecutionContext` elevated with
29+
`isSystem: true` — the posture the action surface already documents and gates
30+
for at invoke time (the ADR-0066 D4 capability gate and the `ai.exposed` gate
31+
are what admit a body to trusted execution). The caller's fields are spread
32+
first, so a body's writes stay attributable (`userId` stamps
33+
`created_by`/`updated_by`), org-scoped (`tenantId` stamps the org column and
34+
drives driver-level tenant isolation), and joined to an open transaction —
35+
rather than the unattributable, org-less rows a bare `{ isSystem: true }` would
36+
write.
37+
38+
No authoring change is required: `ctx.api.object(name)` inside a `body` now
39+
does what the docs always said it does. Bodies that worked before (public /
40+
owner-less objects) are unaffected apart from their writes now being correctly
41+
attributed and org-stamped.

0 commit comments

Comments
 (0)