Skip to content

Commit ad888cf

Browse files
committed
docs(changeset): action.body binds only for type 'script' (#4352)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012C2cd7tL8QDoZ2QKN3djJ5
1 parent 346c8a6 commit ad888cf

1 file changed

Lines changed: 80 additions & 0 deletions

File tree

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
---
2+
"@objectstack/runtime": minor
3+
"@objectstack/lint": minor
4+
"@objectstack/spec": patch
5+
---
6+
7+
fix(runtime,lint): `action.body` binds a handler only for `type: 'script'` (#4352)
8+
9+
`ActionSchema.body` has always described itself as "Only used when type is
10+
`script`", and its JSDoc went further — "Only meaningful when
11+
`type === 'script'`. When set, the runtime invokes the body inside the sandbox
12+
… and ignores `target`." The runtime read none of it:
13+
`actionBodyRunnerFactory` bound a handler the moment `body` parsed, and
14+
`collectBundleActions` collected any named action. A `type: 'url'` action
15+
carrying a leftover `body` was therefore registered in the action registry and
16+
executed in the sandbox — reachable through
17+
`POST /api/v1/actions/:object/:action` and through
18+
`ql.object(o).execute(name)`, and counted by the governance inventory as a live
19+
handler.
20+
21+
Declared ≠ enforced, in the shape that is hardest to debug: an author flips
22+
`type` from `script` to `url`, reasonably concludes the body is now dead code,
23+
and it keeps running with nothing anywhere saying so.
24+
25+
**Behaviour change.** `body` now runs only under `type: 'script'`:
26+
27+
| Action | Before | After |
28+
|:--|:--|:--|
29+
| `type: 'script'` + `body` | body runs | unchanged — body runs |
30+
| `type` omitted + `body` | body runs | unchanged — body runs (`ActionType.default('script')`) |
31+
| `type: 'url' \| 'modal' \| 'flow' \| 'api' \| 'form'` + `body` | body ran | **no handler is bound**; the refusal is logged |
32+
33+
Only an action that **explicitly** declares a non-`script` type *and* carries a
34+
`body` changes behaviour. An omitted `type` still means `script`, because the
35+
collectors walk raw bundle objects — a `strict: false` `defineStack` or a legacy
36+
`manifest.actions[]` never passes through `ActionSchema`, so the schema's own
37+
default has to be applied at the gate rather than assumed to have been applied
38+
already.
39+
40+
**FROM → TO.** If you have an action whose body you want to keep running, set
41+
`type: 'script'` and move the navigation/dispatch target elsewhere; if you want
42+
the target behaviour, delete the now-inert `body`:
43+
44+
```diff
45+
{
46+
name: 'open_portal',
47+
- type: 'url',
48+
+ type: 'script',
49+
target: '/portal',
50+
body: { language: 'js', source: "await ctx.api.object('lead').update(…)", capabilities: ['api.write'] },
51+
}
52+
```
53+
54+
The refusal is **not** silent — silence would only relocate the invisibility the
55+
issue is about. `actionBodyRunnerFactory` logs a warning naming the action, its
56+
declared `type`, and both fixes.
57+
58+
Authoring-time rejection of the same contradiction already shipped in #4438
59+
(`ActionSchema` rejects `body` alongside a non-`script` `type`), so what remains
60+
reachable here is data at rest published before that gate existed, plus bundles
61+
that never parsed. This release closes that half. New tests also pin that the
62+
**publish gate resolves to the rejecting schema** — through
63+
`getMetadataTypeSchema('action')` and `ObjectSchema.actions` — so a re-point of
64+
either registration cannot silently reopen the hole while the schema's own unit
65+
tests stay green.
66+
67+
`@objectstack/lint`'s `validate-action-body-writes` filters by `type` again.
68+
#4344 deliberately made that rule type-blind on the grounds that "the runtime
69+
binds a handler from `action.body` alone … checking what executes beats checking
70+
what the schema says should" — true then, and the comment predicted its own
71+
revision. Execution and declaration are the same set again, so a non-`script`
72+
body no longer produces write-set advice about writes that provably never
73+
happen; the publish gate names that metadata's real defect (`type`) with its own
74+
prescription.
75+
76+
`collectBundleActions` stays deliberately type-blind: it feeds governance
77+
surfaces that must enumerate every declared action, bound or not, and the other
78+
bind path (`engine.setDefaultActionRunner`, for Studio-authored actions) never
79+
walks it. The gate lives at the single point where a `body` becomes an
80+
executable handler, so there is no second copy of the rule to drift.

0 commit comments

Comments
 (0)