Skip to content

hook 注册契约只能表达「命中这些对象」,无法表达「全局但排除这些对象」—— #5860 因此在 plugin-audit 内无法落地 #5928

Description

@baozhoutao

发现于 #5860 的实施核对(#5846 (b) 半边)。本单是 #5860 的阻塞项,落点在 packages/objectql(engine-core 车道),与 #5860 的文件面不相交。

背景

#5860 要求把 plugin-audit 的「哪些对象要审计」这条知识,从 handler 内部(SKIP_OBJECTS 早退)搬到注册面上,好让 #5284 的单 id 按对象前置行门、#5038 的批量按对象门对 SKIP_OBJECTS 内的对象判假。

在 packages/plugins/plugin-audit 的文件面内做不到,原因不在插件,而在引擎的 hook 注册契约本身只有一种表达能力。

事实(origin/main 逐处核对)

契约面,packages/objectql/src/engine.ts:

  • HookEntry.object?: string | string[](645/647 行),registerHook 的 options 同形(1096 行);
  • triggerHooks 的按对象匹配(1363 行):targets.includes('*') || targets.includes(context.object);
  • hasHooksFor(event, object)(1396 行)镜像同一条,并且 if (!entry.object) return true;。

即:注册面只能表达允许列表(外加 '*' 全集),没有任何否定 / 排除 / 谓词的表达位。

而 plugin-audit 的知识是一条拒绝列表(packages/plugins/plugin-audit/src/audit-writers.ts:96 的 SKIP_OBJECTS,约 20 张 sys_* 平台表)。允许列表与拒绝列表只在封闭全集上可互换,而对象全集在运行期是开放的:

  • packages/metadata-protocol/src/protocol.ts:7216 applyObjectRegistryMutation 在 /meta PUT 成功后直接 engine.registry.registerObject(...),把新授权的对象注册进引擎;
  • SchemaRegistry.registerObject(packages/objectql/src/registry.ts:1036)不发任何事件(该文件 emit( 出现 0 次),这条路径也不宣告 metadata:reloaded(只有 metadata 插件的 artifact reload 和 packages 域的 publish 会宣告)。

所以插件侧没有任何可订阅的通道,能把一份枚举出来的允许列表保持为最新。

实测(临时探针,测完即删;计数驱动上的 driver.findOne 增量)

场景 findOne 增量 说明
A 今天:audit 形状的全局 afterUpdate + 对 SKIP_OBJECTS 内对象做单 id update 1 #5860 的红态,前提成立
B 同一注册,对普通业务对象 update 1 对照
C 允许列表注册 { object: ['biz_task'] }:对 SKIP_OBJECTS 对象 / 对被枚举对象 0 / 1 门确实会翻 —— 只要表达得出来
D 允许列表注册之后再注册一个新对象,对它 update 0,且审计 handler 一次都没跑 枚举补集的真实代价

D 是决定性的一条:选项 1(枚举 SKIP_OBJECTS 的补集)把「新对象默认被审计」变成「新对象静默不被审计」—— 合规方向的行为倒退,而且无声。

需要的契约形状

在 registerHook 的 options 与 HookEntry 上新增一个声明式的对象排除面,由 triggerHooks 与 hasHooksFor 共同读取:

// packages/objectql/src/engine.ts
export interface HookEntry {
  object?: string | string[];         // 现有:允许列表(缺省 = 全局)
  excludeObjects?: string | string[]; // 新增:从上面的集合里减掉这些名字
  // ...
}

匹配语义:matches(entry, X) = allowMatches(entry, X) && !excludeMatches(entry, X)。两个消费者(派发用的 triggerHooks、需求门用的 hasHooksFor)必须读同一个匹配函数 —— 这两处今天已经是「一份语义两份实现」,再加一维会把 #5038 注释里那条「门比派发更紧就会静默丢 hook」的风险放大。

为什么建议这个形状,而不是谓词回调(objectFilter?: (object: string) => boolean):

  1. 真实业务需求:今天就有两个调用方 —— plugin-audit 的 5 个注册(plugin-audit 的 5 个 hook 全部无 object 注册 ⇒ 引擎「按对象」需求门(#5284 单 id / #5038 批量)在 audit 启用时恒真 #5860),以及 update() 的前置行门是全局的(hooks.get('afterUpdate').length > 0),任一对象注册 afterUpdate 就让所有对象的单 id update 多付一次读 #5284 注释点名的另一个全局注册方 service-storage 的 file-reference reconcile。两者都是「全局,除了这几张平台表」,都是静态名单;谓词回调多出来的表达力当前没有任何调用方需要。
  2. 本项目的长期正确性:excludeObjects 是本仓已有的词汇(packages/spec/src/api/rest-server.zod.ts:374、packages/spec/src/system/disaster-recovery.zod.ts:222 都用它表达「全部对象减去这些」),不新造名词;并且它保持「声明 = 执行」—— 排除名单是引擎读的数据,而不是藏在 handler 里的早退。
  3. 让 AI 写的元数据/代码难以出错:静态名单可打印、可在 logger.debug('Registered hook', ...) 里如实报出、可被诊断面枚举;谓词回调把任意插件代码塞进每次写入的热路径,且无法内省 —— 还会诱导插件从可变状态计算作用域,正好撞上 AGENTS.md「启动期注册表读数不得记录裁决」那一节。

影响面

Refs:#5860、#5846、#5284、#5038、#5272

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions