fix(objectql,runtime,metadata-protocol): execute authored (Studio) hook bodies — default bodyRunner + live rebind (#2588)#2596
Merged
Conversation
…ok bodies — wire default bodyRunner + live rebind (#2588) A hook authored at runtime (protocol.saveMetaItem / publish-drafts) loaded into the registry but its L1/L2 body never executed — on publish AND after a full restart. Two gaps compounded: 1. No bodyRunner on the metadata-service bind path: bindHooksToEngine skips body-hooks without a runner, and engine.setDefaultBodyRunner was never called anywhere. AppPlugin.init now installs the QuickJS-sandboxed hook body runner as the engine default (same runner defineStack hooks already use), so runner-less bind paths execute bodies. Opt-out: OS_DISABLE_AUTHORED_HOOKS=1 keeps runtime-authored bodies inert. 2. The bind set never saw authored rows: env-scoped kernels have no DatabaseLoader, so metadataService.loadMany('hook') misses sys_metadata rows entirely — the hook was never even bound-and-skipped. ObjectQLPlugin now re-binds runtime-authored hooks from the rows themselves at kernel:ready (cold boot), on metadata:reloaded (dev artifact reload, package publish), and on every hook mutation via the new server-side protocol.onMetadataMutation listener (saveMetaItem / publishMetaItem / deleteMetaItem notify after persistence) — so direct saves, publishes, edits, and deletes all take effect without a restart. Double-execution guard: package-artifact hooks (bound by AppPlugin under app:<id> with an explicit runner) are excluded from the metadata-service bind via registry.getArtifactItem — previously their string-handler variants were latently double-bound, and a default runner would have made body hooks run twice per event. Deleting the last authored hook explicitly unregisters the package set (bindHooksToEngine early-returns on an empty list before its unregister step). Resyncs are serialized so overlapping mutations cannot land an older snapshot last. Verified end-to-end on a fresh showcase instance: authored beforeUpdate hook mutates input after live save, after draft+publish-drafts, and after a full restart; deletes stop it firing; the defineStack control hook still runs. Closes #2588 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PWLRAafXUQzfi2UNW9CcVp
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
📓 Docs Drift CheckThis PR changes 3 package(s): 24 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
os-zhuang
marked this pull request as ready for review
July 4, 2026 17:17
os-zhuang
added a commit
that referenced
this pull request
Jul 5, 2026
…uthored actions, translations, and runtime sharing rules (#2605, #2591, #2592) (#2608) Three more members of the "declared but never wired" family (#2605), each following the #2596 authored-hooks template and runtime-verified against a live showcase server (author → invoke/read → delete → restart). Authored actions (#2605 item 1): engine.executeAction's map was only ever populated from the app bundle at boot, so a published `action` row was stored and listed but never executable. AppPlugin now installs a QuickJS default action runner (engine.setDefaultActionRunner, opt-out via OS_DISABLE_AUTHORED_ACTIONS=1) and ObjectQLPlugin re-registers authored actions (standalone rows AND object-embedded actions[]) under packageId 'metadata-service' at kernel:ready, on metadata:reloaded, and on action/object protocol mutations. Package-artifact actions are excluded so AppPlugin's handlers are never clobbered. Authored translations (#2591): only static bundles ever reached the i18n runtime. Both adapters (FileI18nAdapter and the kernel's in-memory fallback) now carry a separate authored layer replaced wholesale on each sync (clear-then-reload), overlaying static bundles. The shared sync lives in @objectstack/core (wireAuthoredTranslationSync) and is wired by AppPlugin and I18nServicePlugin with single-owner semantics. Sharing rules (#2592): bindRuleHooks was boot-only and rule authoring is a data insert, so the first runtime rule for an object never evaluated until restart. The sharing plugin now binds afterInsert/afterUpdate/afterDelete triggers on sys_sharing_rule that unbind + re-bind the rule-hook package from a fresh listRules(), serialized and fail-safe. Claude-Session: https://claude.ai/code/session_01Ag7HEv9FABsCQnVNNcPweH Co-authored-by: Claude <noreply@anthropic.com>
This was referenced Jul 5, 2026
68 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
修复 #2588:Studio 运行时创作的 hook 的
body从不执行(发布后不执行,重启后也不执行)。根因(比 issue 描述的更深一层,共两个缺口)
bindHooksToEngine对带body但无 runner 的 hook 直接跳过,而engine.setDefaultBodyRunner()在整个仓库中从未被调用,兜底_defaultBodyRunner永远为空。standalone-stack的proj_local,即pnpm dev复现场景)没有 DatabaseLoader,metadataService.loadMany('hook')完全不返回 sys_metadata 行 —— 所以重启后该 hook 甚至不是"被绑定但跳过",而是从未进入绑定流程。GET /meta/hook能列出它只是因为getMetaItems直接查 sys_metadata。仅修 (1) 无法修复 issue 中的重启场景。修复内容
init()(Phase 1,先于所有start())安装引擎级默认 hook body runner —— 与 defineStack hook 使用同一个 QuickJS 沙箱 runner(capability-gated + 超时)。空环境(empty env)也安装。逃生阀:OS_DISABLE_AUTHORED_HOOKS=1让运行时创作的 body 保持不执行。resyncAuthoredHooks(串行化,防并发交错落下旧快照):metadataService.loadMany('hook')∪ sys_metadata 活跃hook行(直接读表,跨所有 org —— 引擎 hook 本就进程级触发,与 flow trigger 语义一致);同名时 DB 行优先;kernel:ready(冷启动)、metadata:reloaded(dev artifact 重载 / 包发布,镜像 fix(automation): bind a flow published while the server runs, without a restart #2576 的 flow re-sync)、以及新的 protocol 变更监听(见下);'metadata-service'包集合,幂等;删到 0 条时显式unregisterHooksByPackage(bindHooksToEngine对空列表在 unregister 之前就 early-return,否则删除的 hook 会永远继续触发)。onMetadataMutation(listener)(与loadMetaFromDb同级的 server-side 扩展,不进 wire contract)。saveMetaItem/publishMetaItem/deleteMetaItem持久化成功后通知订阅者 —— 这是所有创作入口(rest-server、dispatcher、AI builder)的唯一汇聚点,直接保存(默认mode='publish',不经 publish-drafts、不发metadata:reloaded)也能即时生效。registry.getArtifactItem判定)—— 它们已由 AppPlugin 以app:<id>+ 显式 runner 绑定。此前 string-handler 的 artifact hook 在两条路径下已经潜在双绑;装上默认 runner 后 body hook 也会每事件跑两次,该过滤同时消除两者。运行时验证(全新 showcase 实例,复刻 issue 步骤)
showcase_taskassignee='HOOKED'即时生效,无需重启?mode=draft)→ PATCHpublish-drafts(issue 原始流程)→ PATCHHOOKED-V2)生效showcase_normalize_task_title测试:
pnpm test全绿(129 tasks);objectql 762 / runtime 453 / metadata-protocol 11 通过,新增 binder 默认 runner 回退、resync 各分支、mutation-listener 契约、AppPlugin 安装逻辑(含 opt-out)等用例。是否允许运行时创作(DB 存储、未经代码评审)的 hook 执行沙箱 JS? 本 PR 采取 issue 中倾向的答案(是):QuickJS capability-gated、有超时、
hook.form.ts与完整HookSchema表明产品意图;同时提供OS_DISABLE_AUTHORED_HOOKS=1关闭开关。若评审结论是"hook 仅限代码定义",应改为让 Studio 拒绝创作此类 hook,请在合并前定夺。已知边界(记录,不在本 PR 范围)
function(string handler 目标)仍是死角,issue 中已列为伴生问题。Closes #2588
🤖 Generated with Claude Code
https://claude.ai/code/session_01PWLRAafXUQzfi2UNW9CcVp
Generated by Claude Code