Commit 33681ea
* feat(spec,objectql): Hook.runAs — system | user | inherit, default inherit
WIP: schema + engine derivation. Tests, sweep and generated artifacts follow.
A hook's ctx.api is a ScopedContext over the triggering write's context, so a
column protected for a persona by field-level `editable: false` also refuses the
hook that maintains it — the guard and the legitimate writer were the same door.
`runAs` is the declared knob:
- 'system' elevates the hook's ctx.api data operations (the security
middleware short-circuits on isSystem before the field-level
write check), carrying userId through — elevation is not
anonymity, so updated_by still names the operator;
- 'user' pins them to the triggering user, and REFUSES them when no
trigger user resolves (HOOK_UNSCOPED_DATA_ACCESS, the hook-side
twin of the flow engine's #3760 refusal);
- 'inherit' (default) hands the engine-built api through by reference — the
pre-runAs behaviour, so no existing hook changes.
Scope is ctx.api data operations only: condition evaluation, the readonly strip
on ctx.input, ctx.session and async semantics all keep reading the triggering
operation's context.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MCHHMCjeFFjGBm7QQ37CWF
* test(objectql): pin Hook.runAs on the in-process surface
Two layers: wrapDeclarativeHook over a real ScopedContext whose engine records
the context every data operation carries, and a real ObjectQL dispatch read at
the middleware seam plugin-security reads. Covers the three values, the absent
key, the ADR-0112 refusal (with 'the engine was never called'), the restore
after return and after a throw, the fire-and-forget detachment, the loud
refusal of a non-member value, and the ruling's scope fence.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MCHHMCjeFFjGBm7QQ37CWF
* test(runtime): pin Hook.runAs parity on the sandboxed (L2 body) surface
The surface that had no elevation at all: sudo() is not marshalled into the VM,
so the same source passed a native handler test and threw TypeError once the
build lowered it into a body. No runtime source change is needed — the sandbox
reads ctx.api from the engine context at call time, so the wrapper's swap
covers it — which is exactly the composition fact a unit test cannot state.
Includes a direct handler-vs-body parity assertion at the same seam.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MCHHMCjeFFjGBm7QQ37CWF
* test(dogfood): the #14010 symptom, end to end through the real security stack
A computed column protected by field-level editable:false and maintained by a
hook. Four legs on one boot: the persona's own PATCH is still refused 403; the
persona can still write an ordinary column; a runAs:'system' hook BODY writes
the protected column and the value lands, stamping updated_by with the operator;
and an undeclared hook is still refused with the card's own error text, which is
the zero-migration claim measured rather than asserted.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MCHHMCjeFFjGBm7QQ37CWF
* docs,lint: teach runAs where the old prose said a hook cannot elevate
The gating readonly-hook-writes rule now SKIPS a hook declaring runAs:'system'
(the static strip skips a system context, so the write it exists to catch does
not happen) and its hints name the knob. The hook-bodies table rows that called
the absence a gap now point at it, and the canonical semantics live on the hooks
page. The conditional-lock refusal is unchanged and still says why: elevation
does not waive readonlyWhen, which is what the pre-existing pin predicted would
outlive the sudo half.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MCHHMCjeFFjGBm7QQ37CWF
* chore(spec): regenerate references + liveness counts; add the changeset
Regenerated exactly the two artifacts check:generated proved stale (the api
contract and error-code-ledger pages, both carrying the new
HOOK_UNSCOPED_DATA_ACCESS registration) plus the liveness state counts for the
hook.runAs row. HookSchema's own keys are not walked by authorable-surface/ —
only HookContext is — so no surface or defaults shard moves, and
check:authorable-surface is green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MCHHMCjeFFjGBm7QQ37CWF
* fix(tests,census): satisfy the derived gate family
- the refusal message carries ADR-0049 only; tracker ids move to the source
header (check:doc-authoring), and the dogfood fixture's authored strings lose
theirs too;
- the recording engine double routes update/delete through the shared dispatch
predicates and is registered in the pinned ledger, so it can never be looser
than ObjectQL (check:engine-double-contract);
- the sandbox stub driver honours the caller's limit by presence
(check:objectql-double-limit);
- system-context.mdx anchors re-fixed for the one import line this change adds
to engine.ts (15 anchors, pure line rot; check:system-context-census OK).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MCHHMCjeFFjGBm7QQ37CWF
* chore(i18n): regenerate the metadata-form bundles for the hook runAs field
The new Execution-section select carries a label and helpText, so all four
locale bundles gain the row; zh-CN / ja-JP / es-ES are hand-translated in the
same pass (merge mode preserves them, and the refreshed source hashes are what
makes check:i18n read them as translations rather than drift).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MCHHMCjeFFjGBm7QQ37CWF
* test(objectql): address the recorder's updates and await through try/catch
Two defects the newly-registered dispatch predicate and the test typecheck
found in my own fixtures, not in the change under test: three layer-1 calls
issued an update with no address (a shape the real engine refuses, and now the
double does too — the assert takes BOTH the payload and the options bag, since
the address may ride either), and a HookHandler returns void | Promise<void>,
so the refusal pins await inside try/catch rather than reaching for .catch.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MCHHMCjeFFjGBm7QQ37CWF
* chore: merge main and regenerate the two os-regen artifacts
Discharges the merge commit's os-regen deferral.
- liveness/state-counts.md: regenerated from the merged tree
(pnpm --filter @objectstack/spec gen:liveness-counts).
- system-context.mdx: the merge's only real conflict was one table row whose
prose is byte-identical on both sides and differs only in anchor numbers.
Resolved per the driver's own notice (take either side, then re-derive), then
corrected to the MEASURED lines: the ledger anchors that row by needle, and on
the merged tree the needle sits at 10008 — main's 10007 was stale by exactly
the one import line this branch adds to engine.ts. field.zod.ts:1537 is main's
and stays. check-system-context-census: OK, 140 anchors resolve.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MCHHMCjeFFjGBm7QQ37CWF
* chore: regenerate generated artifacts after merging main
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0174WZTU6XcFcS7g2kykC53i
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 23c72be commit 33681ea
28 files changed
Lines changed: 1665 additions & 42 deletions
File tree
- .changeset
- content/docs
- automation
- permissions
- references/api
- packages
- lint/src
- objectql/src
- platform-objects/src/apps/translations
- qa/dogfood/test
- fixtures
- runtime/src/sandbox
- spec
- liveness
- src
- api
- contracts
- data
- scripts
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
259 | 259 | | |
260 | 260 | | |
261 | 261 | | |
262 | | - | |
263 | | - | |
| 262 | + | |
| 263 | + | |
264 | 264 | | |
265 | 265 | | |
266 | 266 | | |
267 | 267 | | |
268 | 268 | | |
269 | | - | |
| 269 | + | |
270 | 270 | | |
271 | 271 | | |
272 | 272 | | |
273 | 273 | | |
274 | 274 | | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
275 | 295 | | |
276 | 296 | | |
277 | 297 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
141 | 141 | | |
142 | 142 | | |
143 | 143 | | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
144 | 177 | | |
145 | 178 | | |
146 | 179 | | |
| |||
0 commit comments