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_taskassignee='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

vercelBot commented Jul 4, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
specBuildingBuildingPreview, CommentJul 4, 2026 5:02pm

Request Review

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

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 mainJul 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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

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

@os-zhuang@claude