Skip to content

fix(objectql,runtime,metadata-protocol): execute authored (Studio) hook bodies — default bodyRunner + live rebind (#2588)#2596

Merged
os-zhuang merged 1 commit into
mainfrom
claude/loving-newton-9sp185
Jul 4, 2026
Merged

fix(objectql,runtime,metadata-protocol): execute authored (Studio) hook bodies — default bodyRunner + live rebind (#2588)#2596
os-zhuang merged 1 commit into
mainfrom
claude/loving-newton-9sp185

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

修复 #2588:Studio 运行时创作的 hook 的 body 从不执行(发布后不执行,重启后也不执行)。

根因(比 issue 描述的更深一层,共两个缺口)

  1. metadata-service 绑定路径没有 bodyRunner(issue 已定位):bindHooksToEngine 对带 body 但无 runner 的 hook 直接跳过,而 engine.setDefaultBodyRunner() 在整个仓库中从未被调用,兜底 _defaultBodyRunner 永远为空。
  2. 绑定集合根本看不到 authored 行(验证中新发现):env-scoped 内核(standalone-stackproj_local,即 pnpm dev 复现场景)没有 DatabaseLoader,metadataService.loadMany('hook') 完全不返回 sys_metadata 行 —— 所以重启后该 hook 甚至不是"被绑定但跳过",而是从未进入绑定流程。GET /meta/hook 能列出它只是因为 getMetaItems 直接查 sys_metadata。仅修 (1) 无法修复 issue 中的重启场景。

修复内容

  • runtime/AppPlugin:init()(Phase 1,先于所有 start())安装引擎级默认 hook body runner —— 与 defineStack hook 使用同一个 QuickJS 沙箱 runner(capability-gated + 超时)。空环境(empty env)也安装。逃生阀:OS_DISABLE_AUTHORED_HOOKS=1 让运行时创作的 body 保持不执行。
  • objectql/ObjectQLPlugin:新增 authored-hook 重绑定 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 会永远继续触发)。
  • metadata-protocol:新增服务端 onMetadataMutation(listener)(与 loadMetaFromDb 同级的 server-side 扩展,不进 wire contract)。saveMetaItem / publishMetaItem / deleteMetaItem 持久化成功后通知订阅者 —— 这是所有创作入口(rest-server、dispatcher、AI builder)的唯一汇聚点,直接保存(默认 mode='publish',不经 publish-drafts、不发 metadata:reloaded)也能即时生效。
  • 双重执行防护:metadata-service 绑定路径现在排除代码包 artifact 自带的 hook(registry.getArtifactItem 判定)—— 它们已由 AppPlugin 以 app:<id> + 显式 runner 绑定。此前 string-handler 的 artifact hook 在两条路径下已经潜在双绑;装上默认 runner 后 body hook 也会每事件跑两次,该过滤同时消除两者。

运行时验证(全新 showcase 实例,复刻 issue 步骤)

场景 结果
运行中直接保存 authored hook → PATCH showcase_task assignee='HOOKED' 即时生效,无需重启
draft 保存(?mode=draft)→ PATCH ✅ 不执行(draft 不上线)
publish-drafts(issue 原始流程)→ PATCH ✅ 新 body(HOOKED-V2)生效
完全重启 → PATCH ✅ 生效(issue 的重启场景)
DELETE hook → PATCH ✅ 停止触发(teardown)
对照组:defineStack hook showcase_normalize_task_title ✅ 照常 trim,且不再有双绑风险

测试:pnpm test 全绿(129 tasks);objectql 762 / runtime 453 / metadata-protocol 11 通过,新增 binder 默认 runner 回退、resync 各分支、mutation-listener 契约、AppPlugin 安装逻辑(含 opt-out)等用例。

⚠️ 需要平台负责人确认的设计决策(issue 中已标注)

是否允许运行时创作(DB 存储、未经代码评审)的 hook 执行沙箱 JS? 本 PR 采取 issue 中倾向的答案(是):QuickJS capability-gated、有超时、hook.form.ts 与完整 HookSchema 表明产品意图;同时提供 OS_DISABLE_AUTHORED_HOOKS=1 关闭开关。若评审结论是"hook 仅限代码定义",应改为让 Studio 拒绝创作此类 hook,请在合并前定夺。

已知边界(记录,不在本 PR 范围)

  • 对代码包 hook 的 overlay 定制(改包内 hook 的 body)不会生效 —— artifact 版本继续执行(与 ADR-0010 artifact-wins 一致);如需 overlay-wins 语义需另行设计。
  • 运行时创作的 function(string handler 目标)仍是死角,issue 中已列为伴生问题。

Closes #2588

🤖 Generated with Claude Code

https://claude.ai/code/session_01PWLRAafXUQzfi2UNW9CcVp


Generated by Claude Code

…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
@vercel

vercel Bot commented Jul 4, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
spec Building Building Preview, Comment Jul 4, 2026 5:02pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/l labels Jul 4, 2026
@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/metadata-protocol, @objectstack/objectql, @objectstack/runtime.

24 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/chatbot-integration.mdx (via @objectstack/runtime)
  • content/docs/api/index.mdx (via @objectstack/runtime)
  • content/docs/automation/hook-bodies.mdx (via @objectstack/runtime)
  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/objectql)
  • content/docs/concepts/north-star.mdx (via packages/runtime)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/runtime)
  • content/docs/data-modeling/formulas.mdx (via packages/objectql)
  • content/docs/deployment/cloud-artifact-api.mdx (via packages/runtime)
  • content/docs/deployment/index.mdx (via @objectstack/runtime)
  • content/docs/deployment/migration-from-objectql.mdx (via @objectstack/objectql)
  • content/docs/deployment/production-readiness.mdx (via @objectstack/runtime)
  • content/docs/deployment/single-project-mode.mdx (via @objectstack/runtime)
  • content/docs/deployment/vercel.mdx (via @objectstack/objectql, @objectstack/runtime)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/objectql)
  • content/docs/kernel/services.mdx (via @objectstack/objectql)
  • content/docs/permissions/authentication.mdx (via @objectstack/objectql, @objectstack/runtime)
  • content/docs/plugins/index.mdx (via @objectstack/objectql)
  • content/docs/plugins/packages.mdx (via @objectstack/objectql, @objectstack/runtime)
  • content/docs/protocol/objectos/http-protocol.mdx (via @objectstack/runtime)
  • content/docs/protocol/objectos/index.mdx (via @objectstack/objectql, @objectstack/runtime)
  • content/docs/protocol/objectos/lifecycle.mdx (via @objectstack/runtime)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/objectql)
  • content/docs/releases/implementation-status.mdx (via @objectstack/objectql, @objectstack/runtime)
  • content/docs/releases/v9.mdx (via @objectstack/objectql)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@os-zhuang
os-zhuang marked this pull request as ready for review July 4, 2026 17:17
@os-zhuang
os-zhuang merged commit 2d567cb into main Jul 4, 2026
15 of 16 checks passed
@os-zhuang
os-zhuang deleted the claude/loving-newton-9sp185 branch 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Authored (Studio) hooks never execute their body — no bodyRunner on the metadata-service bind path

2 participants