Repository navigation
Authored (Studio) hooks never execute their body — no bodyRunner on the metadata-service bind path #2588
Description
Summary
A hook authored in the Studio (i.e. saved as hook metadata via protocol.saveMetaItem / publish-drafts, not defineStack) never executes its body — on publish, and even after a full restart. The hook loads into the registry (it shows up in GET /meta/hook), but its logic silently never runs. This is a "declared-but-not-wired" runtime-authoring gap, not a stale-cache/rebind bug.
Found while generalizing the flow publish-rebind fix (#2576, follow-up #2560). Only flows was a true rebind bug; hooks is deeper.
Runtime evidence (clean-instance showcase, verified)
- Authored an L2 hook into a writable base package
com.example.ops:{ "name":"rebind_probe_hook", "object":"showcase_task", "events":["beforeUpdate"], "body":{ "language":"js", "source":"ctx.input.assignee = 'HOOKED';" }, "priority":10 } POST /packages/com.example.ops/publish-drafts→ thenPATCHashowcase_taskrecord →assigneewas not set toHOOKED(hook didn't fire).- Restarted the server →
PATCHagain → still notHOOKED.GET /meta/hookDOES listrebind_probe_hook(it's loaded, its body just never runs). - Control: the showcase's own
defineStackhookshowcase_normalize_task_title(trims the title onbeforeUpdate) does fire (" spaced "→"spaced"). So event/targeting works — the failure is isolated to the authored-hook bind path.
Root cause
Two hook-bind paths get different treatment:
- defineStack / bundle hooks —
packages/runtime/src/app-plugin.ts:402binds them with abodyRunner:hookBodyRunnerFactory(new QuickJSScriptRunner(), {...}). Bodies run. - authored / metadata-service hooks —
packages/objectql/src/plugin.ts:978binds them with nobodyRunner:bindHooks(items, { packageId: 'metadata-service' }).
bindHooksToEngine skips any hook whose body is set when it has no runner (packages/objectql/src/hook-binder.ts:144 — "hook body present but no bodyRunner supplied"). There is a fallback engine._defaultBodyRunner (engine.ts:516-517) and a setter engine.setDefaultBodyRunner() (engine.ts:537) — but setDefaultBodyRunner is never called anywhere in the repo, so the fallback is always undefined and authored hook bodies are silently dropped.
Impact
priority:p1. Silent data-integrity failure: a user authors a beforeInsert/beforeUpdate normalization or a gated after* side-effect in the Studio, sees it saved, and it never runs — no error, no log. hook is a first-class authorable type (hook.form.ts Studio form exists; HookSchema full-featured). Part of the "Studio authoring dead-ends" family.
Proposed fix (two parts)
- Wire a default bodyRunner at boot so authored hooks execute at all. The runtime already builds one for defineStack hooks — call
ql.setDefaultBodyRunner(hookBodyRunnerFactory(new QuickJSScriptRunner(), { ql, logger, ... }))once at boot (runtime/AppPlugin), so the metadata-service bind path (which passes no explicit runner) picks it up via the existing_defaultBodyRunnerfallback. Also thread thefunctionsmap for string-handlerhooks. - Rebind on publish — have the objectql plugin subscribe to
metadata:reloaded(fired on publish since fix(automation): bind a flow published while the server runs, without a restart #2576) and re-runbindHooks(freshHooks, { packageId: 'metadata-service' })(idempotent:bindHooksToEnginecallsunregisterHooksByPackagefirst, so removed hooks tear down). Mirrorsservice-automation'sresyncFlowsFromProtocol.
⚠️ Design decision needed first
Should runtime-authored (DB-stored, non-code-reviewed) hooks be allowed to run sandboxed JS at all? The missing setDefaultBodyRunner call may be a deliberate gate rather than an oversight. QuickJS is capability-gated and hook.form.ts exists (suggesting intent = yes), but this is a privilege/security boundary the platform owner should confirm before part (1) lands. If the answer is "no, hooks are code-only", the fix is instead to stop the Studio from letting users author a hook that can never run.
Context: post-#2576 boot-cached-consumer audit. Companion issues: authored translations (i18n) and runtime-created sharing rules have related runtime-authoring gaps.
Activity
fix(objectql,runtime,metadata-protocol): execute authored (Studio) ho…