Repository navigation
Description
由 #4586 的 W3 路由级测试撞出来的既有缺陷,与该 issue 的归因主题无关,按 Prime Directive #10 单独立案,不在 #4586 的 PR 里顺手改(这是一处安全行为变更,值得自己的 review 与 changeset)。
事实
packages/plugins/plugin-security/src/auto-org-admin-grant.ts:94-101:
async function tryDelete(ql: any, object: string, id: string): Promise<boolean> {
try {
await ql.delete(object, id, { context: SYSTEM_CTX }); // ← 三参数
return true;
} catch {
return false;
}
}引擎的签名是两参数(packages/objectql/src/engine.ts:4562):
async delete(object: string, options?: EngineDeleteOptions): Promise<any>于是 options 收到的是字符串 'ups_xxx'。rejectUnknownEngineOptions(engine.ts:249)对它做 Object.entries(bag),得到 '0'/'1'/'2'… 这些字符下标当作未知选项,抛错;tryDelete 的 catch 把它吞掉并返回 false。第三个参数({ context: SYSTEM_CTX })连同系统上下文一起被丢弃。
本仓其余 12 处 ql.delete(...) 调用点全部是规范形状 ql.delete(object, { where: { id }, context })(permission-set-projection.ts:324、cleanup-package-permissions.ts:49、suggested-audience-bindings.ts:245、objectql-adapter.ts 多处)——这一处是唯一的例外。
后果(全部是安全方向的)
tryDelete 是该模块唯一的删除通道,所以三条路径同时死掉:
- 降级不回收能力。
organization/update-member-role把owner/admin降回member后,reconcileOrgAdminGrant走 revoke 分支、removed恒为 0,返回{action:'skipped', reason:'delete_failed'},sys_user_permission_set(organization_admin…)那行留在原地。该行 → 通配viewAllRecords/modifyAllRecords→isTenantAdmin():被降级的人仍然是 tenant admin。移除成员(delete membership)同理。 - ADR-0105 D4 的 superseded-variant 收敛失效。 换 posture 后旧的
organization_admin/organization_admin_no_bypass行不会被清掉,wall-less 部署上留着的正是无边界那份。 kernel:readybackfill 的孤儿清扫失效(membership 已删、grant 还在)。
为什么单测全绿
auto-org-admin-grant.test.ts 的 in-memory stub 自己实现的就是三参数形状:
async delete(object: string, id: string) { tables[object] = ...filter(r => r.id !== id); return true; }stub 与真引擎的签名不一致,于是「revoke 生效」这件事只在测试替身里成立过。正是 #3106 那一类:case 标签/函数体不是 enforcement,要看调用点——只不过这次跑偏的是调用点的签名。
复现
packages/qa/dogfood/test/membership-actor-attribution.dogfood.test.ts(#4586 落的那个文件)里把 demotion 后的断言改回 expect(grants).toHaveLength(0) 即红:真实 organization/update-member-role 降级 200 之后,grant 行仍在。
建议修法
tryDelete改成ql.delete(object, { where: { id }, context: SYSTEM_CTX });- 把 stub 对齐真引擎签名,否则修完仍然测不出来(这才是本 issue 的真正教训);
- 加一条 dogfood 级断言:真实路由降级后 grant 归零;
- 考虑存量:已经部署的环境里留着一批本该被回收的
organization_admin行,kernel:readybackfill 修好后会自愈,但值得在 changeset 里写明这是行为变更(有人会突然失去 tenant admin —— 那正是本该发生的事)。
Activity
os-zhuang commentedon Aug 2, 2026
认领:PM 循环第 1 轮
会话:
session_015Br2xsJsczFsTR9bvbh2Ny分支:
claude/issue-4640-revoke-delete-signatureWorktree:
objectstack-issue-4640分诊记录(PM 侧核对,供廉价否决):
objectstack(后端),issue 已带repo:objectstack,无需跨仓拆分 —— 三条受影响路径(reconcileOrgAdminGrant的 revoke 分支、ADR-0105 D4 收敛、kernel:readybackfill)全在packages/plugins/plugin-security,前端无消费面。origin/main逐条核对,缺陷仍然成立 ——auto-org-admin-grant.ts:96仍是三参数ql.delete(object, id, { context: SYSTEM_CTX });engine.ts:4660的签名仍是两参数async delete(object: string, options?: EngineDeleteOptions);cleanup-package-permissions.ts:49、permission-set-projection.ts:324、suggested-audience-bindings.ts:245)全部是规范形状ql.delete(object, { where: { id }, context }),本处确为唯一例外。sys_membergrade changes lose the true actor — thread attribution through the better-auth seam (validity windows deliberately NOT added) #4586 的 PR feat(auth): 把 better-auth 的真实操作人接到身份表写入上,作为归因 (#4586) #4656 已合并,其正文明确记了「顺带发现、不在本 PR 修」并指向本 issue;仓内无其它 open issue/PR 触碰该模块,不存在影子任务。kernel:readybackfill 时失去它 —— 那正是本该发生的事,要求在 changeset 里写明。Generated by Claude Code
os-zhuang commentedon Aug 2, 2026
PM 复核:通过 —— PR #4681(
claude/issue-4640-revoke-delete-signature,4 文件 / +391 −47)。交付了什么
tryDelete改为ql.delete(object, { where: { id }, context: SYSTEM_CTX }),与引擎两参数签名及仓内其余调用点一致。三条 revoke 路径(降级/移除成员、ADR-0105 D4 superseded-variant 收敛、kernel:ready孤儿清扫)恢复真实删除。rejectUnknownEngineOptions的「未知键即错误」、无 scope 删除的拒绝、系统上下文必须在场。位置参数落进 bag 槽位会抛错,与真引擎同因同果。另有一条专门的漂移守卫测试,断言替身拒收旧的三参数调用 —— 下次同类签名漂移会红,而不是再次假绿。把生产代码改回旧形状,该文件 8 条测试变红(含 5 条本来就存在、却一直假绿的 revoke 断言)。organization/update-member-role降级 200 之后轮询到 grant 归零(reconcile 在响应后异步落库,单次读会 flaky,故轮询)。tryFind/tryInsert的 context 在第三参、与引擎一致,无需修改;只有delete把 context 收在第二参 —— 这个不对称正是踩坑点,已连同三个签名写成注释钉在调用处上方。[security] org-admin grant revoke FAILED — capability still in force;「找到 grant 行却一行没删掉」再记一条 warn;读失败降级 debug。noop(无事可删)与skipped/delete_failed(删除失败)被测试钉成两种可区分的返回。复核依据
对 GitHub 核验而非采信报告自述:PR base
main、正文以Fixes objectstack-ai/objectstack#4640开头;改动面为源码 + 单测 + dogfood + changeset,无content/docs/releases/改动、无无关文件;patch 逐段读过。远端 CI 19 个 check 全绿(Test Core ×2、Dogfood Regression Gate ×2、Build Core、Dogfood Verify CLI、Temporal Conformance live PG + MySQL、tsc、ESLint、Check Changeset 等),mergeable_state: clean,已进 merge queue,由队列 rebase 到当时的main后重跑决定是否落地。changeset 已写明:升级后,存量环境里那批本该被回收的 grant 会在
kernel:readybackfill 时真的被删掉 —— 历史上被降级过、或 membership 已被删除的人会失去 tenant admin。那正是本该发生的事:让他们够格的 grade 早就被拿走了,只有能力行活了下来。确需保留全域可见性的人应显式授予(admin_full_access或明确编写的 permission set),而不是靠一个 better-auth membership grade 的副作用。启动日志会逐条打印被回收的人。dev agent 未返回
needs_decision,也无 out-of-scope findings 需另行立案。Generated by Claude Code
fix(plugin-security): 让 org-admin 的 revoke 真的删除 —— tryDelete 对齐引擎的 de…