|
| 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