Skip to content

fix(sharing): 停用规则时撤回已物化授权,并修复 DELETE 500 (#4433, #4434) - #4495

Merged
os-zhuang merged 3 commits into
mainfrom
claude/v17-verification-defects-gnf9e6-sharing
Aug 1, 2026
Merged

fix(sharing): 停用规则时撤回已物化授权,并修复 DELETE 500 (#4433, #4434)#4495
os-zhuang merged 3 commits into
mainfrom
claude/v17-verification-defects-gnf9e6-sharing

Conversation

@os-zhuang

@os-zhuang os-zhuang commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

v17 验收缺陷清单 #4482 的第一批修复。这一对缺陷合在一起,会让一条过度授权的共享规则在 API 层面无法挽回

修了什么

#4433 —— 停用规则从不撤回已物化的授权

共享规则的授权是物化的:评估规则会真的往 sys_record_share 写入 source:'rule' 行。这些行的存活期超过产生它们的那次评估,所以"撤回"必须是一个明确的动作。原先三条路径全部失效,各自的成因互相独立:

1. 停用动作本身(主因)。 sys_sharing_rule 的 reconcile trigger 对所有 isSystem 写入跳过,理由是"那些是启动期播种"。但 SharingRuleService.defineRule —— POST /sharing/rules 背后唯一的实现,也是文档规定的停用规则的方式 —— 无条件使用 SYSTEM_CTX 写入,因为它必须触达一张 sharing 中间件本来会拦截的平台表。于是这个跳过条件命中了 100% 的 REST 授权写入#3821 建好的撤销通道存在、有测试(测的是真实路径根本不会发送的 mock session),且在生产中不可达。现改为按启动阶段判断 —— 这才是那个跳过条件真正想问的问题。

2. 记录触碰。 evaluateAllForRecord 只列 active 规则,停用的规则根本不在循环里,它物化的授权连看都没被看一眼。现在遍历全部规则;停用的规则"期望授权集合为空",直接走 reconcileForRecord 已有的 revoke 分支。

3. 启动回填。 backfillRuleGrants 拿到的是 activeOnly 列表,结构上不可能撤销任何东西。现在遍历全部规则,并新增 sweepOrphanedRuleGrants 清理规则行已彻底消失的授权 —— 靠遍历规则永远够不到它们,所以需要独立的清扫。

#4434 —— DELETE 两种寻址形式都 500

deleteRulewhere:{source,source_id} 谓词删 sys_record_share,既无标量 id 也无 multi:true,正好是 engine dispatch 唯一拒绝的形状,在触达规则行之前就抛了。

修复方式是走 purgeRuleGrants(而不是补一个 multi:true):让规则授权只有一条回收路径 —— SharingService.revoke,与其它所有撤销路径(evaluateRule 遇到 inactive、数据 API 删除后的 revokeRuleGrants)完全一致。补 multi:true 能消掉 500,却会让 delete 成为唯一绕过 sharing service 的撤销路径(AGENTS.md PD #5)。

测试假引擎的保真度

makeEngine().delete 原先接受任意 where —— 这正是 #4434 能带着绿灯发布的原因:已有的 deleteRule drops rule + all its grants 测试一直在对着一个真实服务器必然拒绝的删除断言成功。现已对齐真实 engine 的 dispatch 守卫,该测试在修复前确实失败

验证证据

单元 / 集成测试

pnpm --filter @objectstack/plugin-sharing test243 passed (11 files)
pnpm typecheck(全仓库 turbo)→ 122/122 successful

新增回归测试均已确认撤掉修复后会失败(8 条):#4434 的两种寻址形式、按服务路径撤销、触碰撤销、孤儿清扫、跨启动撤销,以及把旧的 activeOnly 调用点作为缺陷钉死的用例。rule-rebind.test.ts 中原本 skips system-context writes 那条测试钉死的正是缺陷行为,已替换为正确契约。

真实启动验证(持久化 SQLite,端口 38102)

用持久化数据库而非 --fresh,以便做真正的重启。showcase 的 showcase_private_note(sharingModel: 'private'),admin 持有笔记,member B 为收件人。

步骤 B 读取 记录上的 share 数
基线(无规则) 404 0
建规则 active:true 200 1
停用 active:false 404 0
再触碰记录(PATCH) 404 0
完整重启后 404 0

规则在重启后仍为 active: false,授权保持已撤回 —— #4433 的核心断言(issue 里"下次记录改动时不撤,完整重启后也不撤"两条路径)均已实测通过。

启动清扫的修复能力也单独验证过:直接往 SQLite 注入一条 source:'rule'source_id 指向不存在规则的孤儿授权(模拟从缺陷版本升级上来的数据库),重启后日志出现 [sharing-rule] revoked rule grants whose rule row no longer exists {"grants":1},孤儿行消失,同时合法的种子规则授权原样保留

#4434 两种寻址形式:

寻址方式 DELETE 规则 GET B 读取 share 数
按 name(rc1_del_byname) 204 404 404
按 id(srule_2719031d-…) 204 404 404 1 → 0

两者均从原先的无条件 500 RULE_DELETE_FAILED 变为 204,且授权同步撤回。验证结束后已 kill $(lsof -ti tcp:38102) 并清理临时数据库。

文档

CI 的 docs-drift advisory 点了 6 篇手写文档。逐篇核对后:没有一篇存在失真断言 —— 撤回时机本来就没有被写进文档,这也正是该行为得以悄悄漂移的原因。

  • permissions/sharing-rules.mdx —— 补上缺失的章节:三个 reconcile 时机、delete 支持 id 与 name、启动清扫。
  • permissions/permissions-matrix.mdx —— 原文只说规则在记录 insert/update 时重新评估,属实但不完整(现在规则写入与启动也会 reconcile),已补正。
  • 其余四篇(protocol/objectql/security.mdxkernel/services-checklist.mdxplugins/packages.mdxreleases/implementation-status.mdx)仅列出该插件或其所属层次,无需改动。

content/docs/references/ 为自动生成,未触碰。

Changeset

@objectstack/plugin-sharing: minor。定 minor 而非 patch 的理由:这改变了可观察的运行时行为 —— 规则处于 inactive 或已消失的 source:'rule' 授权会在升级后消失(包括为绕开旧行为而手工修补出来的数据),且 DELETE 从 500 变为真正删除,原先把该 500 当作"不支持"的调用方现在会真的删掉规则。

边界

未触及 packages/spec、公开导出或任何协议层;无可作者化 spec key 的移除或重命名。无需决策拍板。

分支:claude/v17-verification-defects-gnf9e6-sharing

Fixes #4433
Fixes #4434

claude added 2 commits August 1, 2026 10:49
An over-granting sharing rule had no withdrawal path on the API surface:
deactivating it never withdrew its materialized grants, and the DELETE
route answered 500 for both address forms. RC-exit blockers, since v17
advertises "switching a rule off actually withdraws access".

#4434 — deleteRule purged sys_record_share with a predicate-shaped
engine.delete carrying neither a scalar id nor multi:true, the one shape
the engine's dispatch refuses. It threw before ever reaching the rule
row, so every DELETE /sharing/rules/:idOrName 500'd. Routed through
purgeRuleGrants instead of adding multi:true, so a rule's grants retire
exactly one way — SharingService.revoke, as every other withdrawal path
already does (AGENTS.md PD #5).

#4433 — three independent gaps, one per path the issue walked:
  - deactivation: the rule-write trigger skipped reconcile on every
    isSystem write. defineRule (the only implementation behind
    POST /sharing/rules) writes with SYSTEM_CTX unconditionally, so the
    skip caught 100% of REST authoring. Now gated on boot phase, the
    question the skip actually meant to ask.
  - record touch: evaluateAllForRecord listed activeOnly rules, so a
    deactivated rule was absent from the loop and its grants were never
    examined. Lists every rule; inactive ones desire nothing and take
    reconcileForRecord's existing revoke branch.
  - boot: the backfill was handed activeOnly rules, making it
    structurally incapable of revoking. Walks every rule, plus a new
    sweepOrphanedRuleGrants for grants whose rule row is gone entirely
    (unreachable by rule iteration).

Test fakes: makeEngine().delete now mirrors the real engine's dispatch
guard. The looser fake is why #4434 shipped green — the pre-existing
"deleteRule drops rule + all its grants" test passed against a delete
the running server always rejected.

Progress: service + plugin fixes done, regression tests added for both
issues. Remaining: rule-rebind.test.ts still pins the old isSystem skip
and needs updating; full gates + live-server verification pending.

Refs #4433, #4434

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017gEHJN2NFpS9VMeURvakgD
rule-rebind.test.ts asserted the defect: 'skips system-context writes'
passed a mocked session the real REST path never sends, then asserted
the reconcile did NOT happen. Replaced with the corrected contract —
deferral is about WHEN a write happens (before the kernel:bootstrapped
backfill), not WHO made it — plus cases for both session kinds after
boot and the seeding deferral during it.

pnpm --filter @objectstack/plugin-sharing test: 243 passed (11 files).

Refs #4433

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017gEHJN2NFpS9VMeURvakgD
@vercel

vercel Bot commented Aug 1, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 1, 2026 12:16pm

Request Review

@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-sharing.

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

  • content/docs/kernel/services-checklist.mdx (via @objectstack/plugin-sharing)
  • content/docs/permissions/authorization.mdx (via packages/plugins/plugin-sharing)
  • content/docs/permissions/permissions-matrix.mdx (via packages/plugins/plugin-sharing)
  • content/docs/plugins/packages.mdx (via @objectstack/plugin-sharing)
  • content/docs/protocol/objectql/security.mdx (via packages/plugins/plugin-sharing)
  • content/docs/releases/implementation-status.mdx (via @objectstack/plugin-sharing)

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.

Live verification on a persistent SQLite datastore confirmed all three
withdrawal moments and both DELETE address forms, so the guarantee is now
worth stating: an over-granting sharing rule is always recoverable from
the API surface.

The CI docs-drift advisory flagged six hand-written pages against this
change. None asserted anything false — the withdrawal timing was simply
undocumented, which is how the behaviour drifted unnoticed in the first
place. Added the missing section to permissions/sharing-rules.mdx (the
three reconcile moments, delete-by-id-or-name, the boot sweep for grants
whose rule row is gone) and sharpened permissions-matrix.mdx, which said
rules re-evaluate on record insert/update and stopped there — true but
partial now that rule writes and boot also reconcile. The other four
flagged pages only list the plugin or its layer and needed no change.
content/docs/references/ is generated and was not touched.

Changeset: minor on @objectstack/plugin-sharing. Not patch — a
`source: 'rule'` grant whose rule is inactive or gone now disappears on
upgrade, and DELETE starts succeeding where it used to 500, so callers
that treated that 500 as "unsupported" will now really delete.

Refs #4433, #4434

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017gEHJN2NFpS9VMeURvakgD
@github-actions github-actions Bot added documentation Improvements or additions to documentation tooling labels Aug 1, 2026

Copy link
Copy Markdown
Contributor Author

CI 全绿,验证完成

db2e901 上 17 项检查:16 success / 1 skipped(Console Pin Gate 跳过属预期,本 PR 未动 console)。

关键几项:Test Core ✅、TypeScript Type Check ✅、Build Core ✅、Build Docs ✅、ESLint ✅、Dogfood Regression Gate (1/2, 2/2) ✅、Temporal Conformance (live PG + MySQL) ✅、Spec property liveness ✅。

之前挂掉的 Check Changeset 现已通过 —— 已补 .changeset/sharing-rule-withdrawal-and-delete.md,@objectstack/plugin-sharing: minor(理由见 PR 正文的 Changeset 一节:授权撤回是可观察行为变更,且 DELETE 从 500 变为真正删除)。

Flag docs affected by code changes 也已通过。 该 advisory 点的 6 篇文档已逐篇核对:没有失真断言,真正的问题是撤回时机从未被写进文档——这也正是该行为得以悄悄漂移的原因。已补 permissions/sharing-rules.mdx 的撤回生命周期章节,并修正 permissions-matrix.mdx 中"仅在记录 insert/update 时重新评估"这一不完整表述。content/docs/references/ 为生成产物,未触碰。

跨重启撤回与 DELETE 两种寻址形式均已在真实服务器上实测通过,证据表格见 PR 正文「验证证据」一节。端口 38102 已释放,临时数据库已清理。

PR 仍为 draft,等待人工 review 后再决定是否转 ready。


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review August 1, 2026 12:56
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 1, 2026
Merged via the queue into main with commit ba5ff2f Aug 1, 2026
18 checks passed
@os-zhuang
os-zhuang deleted the claude/v17-verification-defects-gnf9e6-sharing branch August 1, 2026 13:10
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

2 participants