Skip to content

fix(spec,objectql)!: hook 的空目标不再被静默放大成通配符 - #4281

Merged
os-zhuang merged 1 commit into
mainfrom
claude/unknown-key-stripping-strictness-h3a3zt
Jul 31, 2026
Merged

fix(spec,objectql)!: hook 的空目标不再被静默放大成通配符#4281
os-zhuang merged 1 commit into
mainfrom
claude/unknown-key-stripping-strictness-h3a3zt

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

起因

原计划是给 object: '*' 的通配 hook 加 review 提示(#4001 campaign 的收尾项之一)。摸实现时发现了更硬的东西:通配符不需要作者主动写就会发生。

HookSchema.objectz.union([z.string(), z.array(z.string())]),没有任何非空约束;hook-binder.tsnormalizeObjects 则是:

if(Array.isArray(target))returntarget.length>0 ? target : ['*'];if(typeoftarget==='string'&&target.length>0)return[target];return['*'];

'*' 是引擎派发的 match-everything 哨兵(engine.ts:targets.includes('*'))。合起来:

作者写的解析实际注册到
object: ''✅ 通过每一个对象
object: []✅ 通过每一个对象
object: ['']✅ 通过名为 '' 的对象 → 永不触发

实测确认(改动前逐个 safeParse + binder 行为核对)。这是 #4001 的失效模式反向发作:平常的静默剥离是把写下的东西变窄,这一条把「没填」放大成系统里最大的影响面 —— 全对象 × 全事件,且无任何诊断。[''] 则是另一头,ADR-0078 的「静默失效元数据」:注册在一个谁也匹配不上的对象名上,永远不会跑。

两种都不是任何人的本意 —— 这个兜底还把错误记录了两次(见下),本身就说明没人有意为之。

改动

1. HookSchema.object 加 refinement(packages/spec/src/data/hook.zod.ts)
一条 .refine() 覆盖两个分支,'' / ' ' / [] / [''] / ['account',''] 全部拒绝。按 #4001 要求,错误必须可修而非只是「大声」:

A hook object target must name at least one object. An empty target is not "no target": until #4001'' and [] were widened to the wildcard '*', registering the hook on EVERY object … Name the object(s) — object: 'account' or object: ['account', 'contact'] — or, if firing on every object really is the intent, write the wildcard explicitly: object: '*'.

2. binder 独立堵一遍(packages/objectql/src/hook-binder.ts)
bindHooksToEngine 吃的是 Hook[](z.input)且不 parse —— 所以光改 schema 覆盖不到这条路径。normalizeObjects 不再放大,改为过滤空白后返回 [];空结果走已有的 skipped + errors 通道并 warn,而不是静默不注册。

3. 顺带修掉 strict 从来没生效过
BindHooksOptions.strict 文档写的是「treat unresolved hooks as fatal errors … fail fast on misconfiguration」,但每个 hook 的 try/catch 把它自己 strict 分支抛出的异常吞了 —— 结果是同一个失败被记录两次然后继续绑定。探针实测确认(THREW: false | errors: [<skip 记录>, <被吞的 throw>])后才改。现在 strict 下绑定失败真的致命。之所以在本 PR 一并修:我的新分支也用 opts.strict,照抄一个已知失效的模式比修掉它更糟。

4. 文档(content/docs/automation/hooks.mdx)新增 Targeting objects 一节:三种写法、空目标被拒的 Callout、以及 Wildcard hooks earn a higher review bar —— 影响面含未来才创建的对象、成本随对象数×写入量放大(该用 condition 而非 body 里 early return)、通配 + api.write L2 body 是系统里最难验证的形状(链到写集 gap)。

验证

  • @objectstack/spec276 文件 / 7160 用例全过;packages/spec 全部 12 个 check 闸门 OK(docs / skill-refs / skill-docs / api-surface / spec-changes / upgrade-guide / liveness / react-blocks / react-conformance / skill-examples / variant-docs / strictness-ledger),build 后跑的 api-surface 也在内
  • hook-binder.test.ts24 过(新增 9 个:四种空目标形状各自不注册且不触发任何对象、混合列表只丢空成员、显式 '*' 照常绑定、strict 致命、非 strict 仍记录并继续)
  • hook.test.ts56 过(新增 5 种空目标拒绝 + 数组内通配仍通过,并断言错误信息里同时出现「must name at least one object」和 object: '*')
  • tsc --noEmit spec / objectql 均干净
  • 仓库级 check:role-word / check:doc-authoring / check:nul-bytes OK
  • objectql 全量有 75 failed / 4 files(protocol-data、protocol-unknown-query-param、query-expression-conformance、protocol-discovery)—— 已证预存在:stash 掉本 PR 全部改动后重跑,同样 75 failed / 同样 4 个文件(通过数 1245 → 1254,差值正是我新增的 9 个用例)。与 hook 无关,属 REST 列表:显式 filter 在场时,同时传的字段级参数被静默丢弃(#4134 的邻居) #4164/REST 列表:未知查询参数被静默当作字段过滤器 —— ?pageSize=5 返回 200 + 空列表 #4134 一族
  • 第一方资产扫描:处使用空目标;plugin.ts / 默认权限集里的 '*' 是代码注册路径,不经 schema,不受影响

关于原计划里的 lint

原本要加一条「通配 hook → advisory warning」。看过 validate-security-posture.ts 的纪律说明后放弃了 —— 那里明写「Per ADR-0049 discipline these are NOT advisory security: every error rule mirrors a runtime enforcement point」。对故意写的通配 hook 发一条无动作可做的告警,不镜像任何运行时执行点,只会变成每次 os validate 的噪音。意外的通配现在在 parse 期就是硬错误(严格优于 lint 告警),故意的通配交给文档里的 review bar。

破坏性

!:此前能解析并绑定的元数据现在被拒。但被拒的只有两类 —— 作者不知情的意外通配,和拼错的通配。没有合法用法受影响,第一方扫描零命中。

关联:#4001(母 issue)、ADR-0078、#4271(写集 lint)。


Generated by Claude Code

…the wildcard
HookSchema.object had no emptiness constraint, so '', [] and [''] all
parsed. normalizeObjects then mapped the first two to ['*'] — the
engine's match-everything sentinel — so a hook whose target was left
blank registered on EVERY object, on every event it listed, silently.
That is #4001 pointed the wrong way: the usual silent strip narrows what
was written, this one widened blank intent into the broadest possible
blast radius. [''] failed the other way, registering on an object name
nothing matches (ADR-0078 silently-inert).
Both are refused now, in two places: HookSchema gains a refinement whose
error names the spellings that work, and the binder — which accepts
unparsed Hook input, so the schema alone would not cover it — skips and
records an untargetable hook instead of widening it. A wildcard hook
stays legitimate; it has to be spelled '*' so a reviewer sees it.
Also fixes bindHooksToEngine's strict option: documented as fail-fast,
it never threw, because the per-hook try/catch swallowed the throw its
own strict branch raised — the failure was recorded twice and binding
carried on. Proven by probe before changing it.
Docs: hooks.mdx gains a Targeting objects section covering the three
forms, the refusal, and the review bar a wildcard earns.
First-party scan: zero assets use an empty target; the wildcards in
plugin.ts / permission sets are code-registered and unaffected.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0147tNF4Snk7Ry1KGt4a5PY4
@vercel

vercelBot commented Jul 31, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredJul 31, 2026 2:28am

Request Review

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation protocol:data tests tooling labels Jul 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/objectql, @objectstack/spec.

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

  • content/docs/ai/agents.mdx(via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx(via @objectstack/spec)
  • content/docs/ai/skills.mdx(via @objectstack/spec)
  • content/docs/api/client-sdk.mdx(via @objectstack/spec)
  • content/docs/api/environment-routing.mdx(via @objectstack/spec)
  • content/docs/api/error-catalog.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx(via @objectstack/spec)
  • content/docs/api/index.mdx(via @objectstack/spec)
  • content/docs/automation/approvals.mdx(via packages/spec)
  • content/docs/automation/flows.mdx(via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx(via packages/spec)
  • content/docs/automation/hooks.mdx(via @objectstack/spec)
  • content/docs/automation/index.mdx(via @objectstack/spec)
  • content/docs/automation/webhooks.mdx(via @objectstack/spec)
  • content/docs/automation/workflows.mdx(via @objectstack/spec)
  • content/docs/concepts/architecture.mdx(via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx(via packages/spec)
  • content/docs/concepts/index.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx(via @objectstack/objectql, packages/spec)
  • content/docs/concepts/north-star.mdx(via packages/spec)
  • content/docs/data-modeling/analytics.mdx(via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx(via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx(via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx(via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx(via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx(via packages/objectql, @objectstack/spec)
  • content/docs/data-modeling/index.mdx(via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx(via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx(via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx(via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx(via @objectstack/spec)
  • content/docs/deployment/cli.mdx(via @objectstack/spec)
  • content/docs/deployment/migration-from-objectql.mdx(via @objectstack/objectql)
  • content/docs/deployment/troubleshooting.mdx(via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx(via @objectstack/spec)
  • content/docs/deployment/vercel.mdx(via @objectstack/objectql)
  • content/docs/getting-started/build-with-claude-code.mdx(via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx(via @objectstack/spec)
  • content/docs/getting-started/examples.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx(via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx(via @objectstack/spec)
  • content/docs/kernel/cluster.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx(via packages/spec)
  • content/docs/kernel/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/email-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx(via packages/spec)
  • content/docs/kernel/services-checklist.mdx(via @objectstack/objectql, @objectstack/spec)
  • content/docs/kernel/services.mdx(via @objectstack/objectql, @objectstack/spec)
  • content/docs/permissions/authentication.mdx(via @objectstack/objectql)
  • content/docs/permissions/authorization.mdx(via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx(via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx(via @objectstack/spec)
  • content/docs/permissions/positions.mdx(via @objectstack/spec)
  • content/docs/permissions/rls.mdx(via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx(via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx(via @objectstack/spec)
  • content/docs/plugins/development.mdx(via @objectstack/spec)
  • content/docs/plugins/index.mdx(via @objectstack/objectql, @objectstack/spec)
  • content/docs/plugins/packages.mdx(via @objectstack/objectql, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx(via @objectstack/spec)
  • content/docs/protocol/diagram.mdx(via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx(via @objectstack/objectql, @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/runtime-capabilities.mdx(via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx(via packages/objectql, @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx(via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx(via @objectstack/objectql, @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx(via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx(via @objectstack/objectql, @objectstack/spec)
  • content/docs/releases/index.mdx(via @objectstack/spec)
  • content/docs/releases/v12.mdx(via @objectstack/spec)
  • content/docs/releases/v13.mdx(via @objectstack/spec)
  • content/docs/releases/v16.mdx(via @objectstack/spec)
  • content/docs/releases/v17.mdx(via @objectstack/spec)
  • content/docs/releases/v9.mdx(via @objectstack/objectql, @objectstack/spec)
  • content/docs/ui/actions.mdx(via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx(via @objectstack/spec)
  • content/docs/ui/dashboards.mdx(via @objectstack/spec)
  • content/docs/ui/forms.mdx(via @objectstack/spec)
  • content/docs/ui/index.mdx(via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx(via @objectstack/spec)
  • content/docs/ui/setup-app.mdx(via @objectstack/spec)
  • content/docs/ui/translations.mdx(via @objectstack/spec)
  • content/docs/ui/views.mdx(via @objectstack/spec)

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.

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

Labels

documentationImprovements or additions to documentationprotocol:datasize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-zhuang@claude