Skip to content

docs(skills): document the sandboxed hook body ctx/capability contract - #3314

Merged
os-zhuang merged 1 commit into
mainfrom
docs/skill-hook-sandbox-body
Jul 19, 2026
Merged

docs(skills): document the sandboxed hook body ctx/capability contract#3314
os-zhuang merged 1 commit into
mainfrom
docs/skill-hook-sandbox-body

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

What & why

The objectstack-data hooks docs explained lifecycle events and registration, but never the sandboxed body form — the metadata-native shape ({ language, source, capabilities }) that a metadata-only runtime actually executes and that ships inside every build artifact. A third-party evaluator on 15.1.x got hooks working only by reverse-engineering @objectstack/runtime's dist (memory project_thirdparty_eval_15_1_findings). This fills that gap.

Adds a "Sandboxed Hook Bodies (body)" section to references/data-hooks.md

Verified against runtime source and a live create-objectstack@15.1.1 repro:

  • body vs the deprecated inline handler — where each runs (QuickJS VM vs full Node), how each ships, that body wins when both are present.
  • The sandbox ctxinput / previous (undefined on insert → !ctx.previous = create) / result / user / session / event / object / api / log / crypto, marshalled as a JSON snapshot.
  • ctx.api.object(n) repofind / findOne / count / insert / update({ id, ...fields }) / upsert / delete. Query key is where (object + $-operators) — notfilter: [['id','=',x]] (that array-of-triples value matches nothing).
  • The six legal HookBodyCapability tokens (api.read, api.write, api.transaction, crypto.uuid, crypto.hash, log) + call-time gating/throw behaviour.
  • Sandbox globals — verified available (Date/Math/JSON/Promise/Map/…) vs absent (console/fetch/setTimeout/URL/…), and build-rejected identifiers (import/process/eval/free identifiers).
  • Gotcha 1 — afterUpdate ctx.result is a partial patch → re-query for a lookup FK it doesn't carry.
  • Gotcha 2 — cross-object writes obey the target's sharing model (public_readFORBIDDEN: insufficient privileges, admin not exempt).
  • A copy-paste afterUpdate example (gated by the <!-- os:check --> tsc gate) + [BodyRunner] troubleshooting (incl. ctx.log emission is best-effort).

Plus: a quick-ref body block in rules/hooks.md, a SKILL.md pointer, a stale "14 events" → 8 fix, and the handler-form Cross-Object API example switched to canonical where.

Correction vs the source brief

The brief specified findOne({ filter: [['id','=',id]] }). Source + the #1867 real-engine/real-sandbox integration test + the objectstack-query skill all show the canonical key is where with an object value; filter is only a tolerated object-valued alias, and the triple-array value isn't supported. The docs teach where accordingly.

Verification

  • Live repro: scaffolded npx create-objectstack@latest (15.1.1), added candidate/position objects + the doc's hook verbatim, os dev, drove it over REST → hook fired, re-query + cross-object update landed (position.status = filled), zero [BodyRunner] threw. A beforeInsert probe confirmed the globals table.
  • Gates green:check:skill-docs, check:skill-refs, check:skill-examples (new example type-checks against built @objectstack/spec), check:doc-authoring.

🤖 Generated with Claude Code

The objectstack-data hooks docs covered events + registration but never the
sandbox `body` form — the metadata-native shape a metadata-only runtime
actually executes. A third-party evaluator on 15.1.x got hooks working only by
reverse-engineering `@objectstack/runtime`'s dist (memory:
project_thirdparty_eval_15_1_findings).
Add a "Sandboxed Hook Bodies (`body`)" section to references/data-hooks.md,
verified against runtime source AND a live `create-objectstack@15.1.1` repro:
- `body` vs the deprecated inline `handler`; the `{ language, source,
capabilities }` shape; `source` is the function body wrapped as
`(async (ctx) => { … })(ctx)`.
- the sandbox `ctx` (input/previous/result/user/session/event/object/api/log/
crypto); `!ctx.previous` distinguishes create.
- `ctx.api.object(n)` repo — find/findOne/count/insert/update({id,...})/upsert/
delete; query key is `where` (object + $-operators), NOT `filter:[['id','=',x]]`.
- the six legal `HookBodyCapability` tokens + call-time gating/throw behaviour.
- sandbox globals (verified available vs absent) and build-rejected identifiers.
- gotcha: afterUpdate `ctx.result` is a PARTIAL patch → re-query for lookup FKs.
- gotcha: cross-object writes obey the TARGET's sharing model (public_read →
`FORBIDDEN`, admin not exempt).
- a copy-paste afterUpdate example (os:check-gated) + `[BodyRunner]` troubleshooting,
including that `ctx.log` emission is best-effort.
Also: a quick-ref `body` block in rules/hooks.md, a SKILL.md pointer, fix the
stale "14 events" → 8, and switch the handler-form Cross-Object API example to
the canonical `where`.
Verified end-to-end: scaffolded `create-objectstack@15.1.1`, wrote the doc's
hook verbatim, drove it over REST — hook fired, the cross-object update landed
(`position.status = filled`), zero `[BodyRunner] threw`. Skill gates
(skill-docs / skill-refs / skill-examples / doc-authoring) all green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@os-zhuangos-zhuang added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Jul 19, 2026
@vercel

vercelBot commented Jul 19, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
specBuildingBuildingPreview, CommentJul 19, 2026 5:38pm

Request Review

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@os-zhuang