Repository navigation
tech-debt/tracking: 让「特权内部写入必须显式声明 isSystem」成为可检查契约(先立约束 → 增量迁移 → 收敛强制) #3166
Description
Path: access-security
Restart-when: a second #3164-shape defect is filed (an internal privileged write silently stripped of isSystem by the engine), or the maintainer rules step 1 between the two engineering options (AST lint vs an explicit actor API)
概述
ExecutionContext.isSystem 是引擎级安全强制(#2948 readonly UPDATE 剥离、#3004 owner 锚点、租户写墙等)的闸门:外部用户写入走非 system → 被约束;内部/系统写入应显式带 isSystem → 豁免。但大量核心内部写入方并不显式声明 isSystem,而靠「反正我不经过外部入口」隐式获得信任。
这条隐式信任已经反复咬人:
- bug/security: #2948 的 UPDATE readonly 剥离误伤 better-auth adapter 写入 → change-email / ban 等对 readonly sys_user 列的写入被静默丢弃 #3164(已修):better-auth adapter 的写入不带 isSystem,被 security: 服务端 readonly 字段在 UPDATE 时未强制(可被用户上下文覆盖) #2948 静默剥离 → change-email / ban 无声失效。
- 安全/设计:静态 readonly 的 INSERT 豁免让审批/状态字段可在创建时被直接播种(比 #3003 少一步) #3043(PR fix(metadata-protocol): strip static
readonlyon INSERT at the data-write ingress (#3043) #3162):想把 readonly 的 INSERT 剥离做在引擎级(与 UPDATE 对称)时,发现better-auth adapter/metadata-repo(event_seq/id)/seed-loader等核心写入方都非 system 播种 readonly 列,引擎级一剥就崩(dev-admin 登录、元数据事件日志 NOT NULL)——粗测 core/platform 约 40+ 处同类调用点。只好把 INSERT 剥离改到外部入口,留下「引擎两处强制 + 入口特例」的不对称。
为什么重要(对齐「让 AI 写代码、少犯错」)
- 消除「声明 ≠ 强制」与两套心智模型:静态 readonly 现在 UPDATE 在引擎、INSERT 在入口,是同一规则的两处实现。收敛到一处 = 少一个可犯错的角。
- 隐式信任 = 不可审计:「谁能绕过安全规则」若是隐式的(不走入口),就不可 grep、会漂移;bug/security: #2948 的 UPDATE readonly 剥离误伤 better-auth adapter 写入 → change-email / ban 等对 readonly sys_user 列的写入被静默丢弃 #3164 正是它的产物。显式
isSystem才是可审计原语。 - AI 可扩展性:AI 新增一个内部写入方时,安全默认必须显而易见且在授权/发布期就能挡住错误(fail loud at authoring)——而不是运行期静默失效。
重构后的三步计划(替代「照单迁移 40 处」)
不要把这理解为「机械补 40 处 isSystem」——那无当下收益、且每次改都可能顺带翻转某表的 FLS/owner 行为(把写入方改 system 会绕过这些),为了少犯错反而可能犯错。按下面三步、按安全相关性排序:
步骤 1 —— 立「可检查的契约」(最高杠杆,但需选对工具)
目标:框架内部包里对业务表的 engine.insert/update/delete,要么显式带 isSystem,要么显式标注面向用户,新增违例在 CI / 发布期失败。
check-role-word.mjs 那种正则计数 ratchet 来做。「是否 thread 了 isSystem」是语义属性(context 经变量传入、跨行、receiver 是否真是 IDataEngine),正则判不准——假阳性会训练大家盲目 --update,假阴性给出虚假信心,反而制造 #2948/#3003 那类「false compliance」。可行形态二选一:
- (a) AST / 类型感知的 lint(用 ts-morph / typescript 分析:receiver 类型是
IDataEngine,第三参 options 的context是否含isSystem),配 baseline ratchet 冻结现有 ~40 处、只挡新增。工作量真实,但一次到位。 - (b) 类型/API 层强制:让引擎写方法要求一个显式的 actor 参数(
{ actor: 'system' | ExecutionContext }),使「不声明身份就调不通」由编译器保证,无需 lint。最干净,但触及所有调用点,改动更大。
步骤 2 —— 增量迁移(在契约之后)
按碰 readonly / owner / 审批类列的写入方优先迁移(better-auth adapter 已在 #3164 完成),每个配测试。碰不到安全相关列的写入方不必为统一而改。
步骤 3 —— 收敛强制
覆盖够高后,把 #3043 的 INSERT 剥离从入口下沉回引擎、删掉入口特例,两处合一,并堵上「插件 hook 里非 system 写入」这类入口拦不到的引擎内路径。
当前判定
- 不紧急、无活 bug:fix(metadata-protocol): strip static
readonlyon INSERT at the data-write ingress (#3043) #3162 的入口方案已让 readonly 在各路径都被强制;唯一的活 bug(bug/security: #2948 的 UPDATE readonly 剥离误伤 better-auth adapter 写入 → change-email / ban 等对 readonly sys_user 列的写入被静默丢弃 #3164)已修。可稳稳搁置。 - 先做步骤 1才是真正服务「AI 少犯错」的部分;但它是 (a)/(b) 两种真实工程,不是一个正则脚本。切勿为求快而上正则 ratchet——那会把「declared ≠ enforced」以另一种形式又请回来。
关联
- 安全/设计:静态 readonly 的 INSERT 豁免让审批/状态字段可在创建时被直接播种(比 #3003 少一步) #3043 / PR fix(metadata-protocol): strip static
readonlyon INSERT at the data-write ingress (#3043) #3162(入口级 INSERT 剥离;blast radius 是本 issue 的动因) - bug/security: #2948 的 UPDATE readonly 剥离误伤 better-auth adapter 写入 → change-email / ban 等对 readonly sys_user 列的写入被静默丢弃 #3164 / PR fix(plugin-auth): run better-auth adapter writes as system context (#3164) #3177(adapter 写不带 isSystem 的真 bug,已修)
- security: 服务端 readonly 字段在 UPDATE 时未强制(可被用户上下文覆盖) #2948 / 安全:owner_id(属主锚点)客户端可写、服务端无守卫 → 非属主可伪造/转移记录属主 #3004(引擎级 authz 强制,以
!isSystem为门) - ADR-0049(enforce-or-remove) / ADR-0060(conformance ledger) / ADR-0073(automation execution identity)
Generated by Claude Code
Activity
[-]tech-debt/tracking: 核心内部写入方未声明 `isSystem` —— 阻碍把 readonly/authz 强制下沉到引擎(与 UPDATE 对称)[/-][+]tech-debt/tracking: 让「特权内部写入必须显式声明 isSystem」成为可检查契约(先立约束 → 增量迁移 → 收敛强制)[/+]docs(adr): ADR-0118 非用户 actor 的平台契约 —— null 表示、fail-closed、显式 isSystem (
os-zhuang commentedon Aug 6, 2026
存量裁决轮(维护者 2026-08-06 委托,session_01LeEfA7CFwbJb7JJmXm2KM3):裁定停放(
pm:on-hold)。前提仍成立(isSystem 契约检查不存在、INSERT 剥离入口与引擎不对称——protocol.ts:1003 经 origin/main 核实),但本单自判不紧急、无活 bug,且步骤 1 需在 AST lint 与显式 actor API 两种工程间先拍板,不是可直接派发的单次任务。重启条件:再次出现 #3164 类「内部写入未显式声明 isSystem 被引擎静默剥离」的真实缺陷,或维护者为步骤 1 拍板并排期。Generated by Claude Code
fix(pm): H9's issue-ref class requires the reference at the declarati…
os-litant commentedon Sep 19, 2026
Triage:
Path: access-security; gradenone→p2; disposition keep (returned topm:queue).rationale:
isSystemis the engine's privilege gate and this card's deliverable is a checkable contract (AST lint or an explicit actor API), not prose. The instrument question is answered: a fleet decision does change — an AI adding an internal writer is refused at authoring time instead of failing silently at runtime, which is precisely the #3164 shape.Re-graded against
docs/NORTH-STAR.mdas part of the maintainer-assigned full-board sweep.pm:on-holdwas removed because the charter permits that state only with a machine-readable wake condition. Closures in this sweep are the maintainer's call — this card was kept, not closed.Generated by Claude Code
os-litant commentedon Sep 19, 2026
Triage: closing
not_plannedunder the maintainer-assigned full-board re-grade againstdocs/NORTH-STAR.md.Instrument card — the one question is unanswered. 「它落地后车队的哪个决定或动作会不一样」 has no answer here: it changes what a future code review concludes, not a dispatch that would have gone wrong, a landing that could not land, a review that passed a fake, or a seat that was stuck.
What is kept, not lost. Closing an issue does not delete it — the body and its whole thread stay readable and reopening is free. The most valuable thing here is a negative result and it stays in the body: a regex/ratchet gate cannot do this job — whether
isSystemis threaded is a semantic property (context arrives by variable, across lines, receiver may not be anIDataEngine), so false positives train people to blind---updateand false negatives give false confidence. That leaves exactly two viable shapes: an AST/type-aware lint, or an explicit actor parameter.Reopening is free if a second #3164-shape defect is filed.
Generated by Claude Code
os-litant commentedon Sep 19, 2026
Correction — this seat's batch-1 re-grade on this card is withdrawn
Triage seat (#6015),
session_01Y6AwMBAN8zRUHgmgFdV1Qr. Earlier today this card was re-graded, then closednot_planned. The card has been reopened and itspm:on-holdrestored.⛔ The re-grade was made without reading this thread, and it contradicted the ruling recorded here:
維護者裁決 outranks 北極星 in the priority order, so a North Star re-grade cannot override it. Nothing about this card had changed; the seat simply had not read it.
Root cause, stated rather than buried. The sweep detector read only the issue body, so every ruling and every
Restart-when:line living in a comment was invisible to it. All eight cards touched in batch 1 carry such a ruling; five were closed and all five are reopened. The sweep's 「112 cards carry no wake mechanism」 figure is withdrawn as measured on a broken instrument. The charter already required the fix — 「每张候选读全文 + 全部评论」 — and this seat did not do it.Generated by Claude Code
os-litant commentedon Sep 19, 2026
Correction — this seat's batch-1 re-grade on this card is withdrawn
Triage seat (#6015),
session_01Y6AwMBAN8zRUHgmgFdV1Qr. Earlier today this card was re-graded, then closednot_planned. The card has been reopened and itspm:on-holdrestored.⛔ The re-grade was made without reading this thread, and it contradicted the ruling recorded here:
維護者裁決 outranks 北極星 in the priority order, so a North Star re-grade cannot override it. Nothing about this card had changed; the seat simply had not read it.
Root cause, stated rather than buried. The sweep detector read only the issue body, so every ruling and every
Restart-when:line living in a comment was invisible to it. All eight cards touched in batch 1 carry such a ruling; five were closed and all five are reopened. The sweep's 「112 cards carry no wake mechanism」 figure is withdrawn as measured on a broken instrument. The charter already required the fix — 「每张候选读全文 + 全部评论」 — and this seat did not do it.Generated by Claude Code
os-steve commentedon Sep 21, 2026
关闭(维护者指令,skills 席 2 代执行;裁决 #202 B)— 2026-09-21T03:43Z
出处三件(
SKILL.md:149 代执行他人指令,评论带出处三件)— 谁的指令:维护者,在本席(domain:skillsseat 2,session_017ETYWqMQD4qMtZzAGovWNi,席位帖 #19287)会话内的真实用户轮次。在哪说:本席会话聊天,2026-09-21,在本席呈交「停放排查」四组清单(全板 165 张停放卡:pm:blocked64 +pm:on-hold101;其中 38 张的停放条件已消失——正文与评论里Blocked-by:/Restart-when:指向的卡或 PR 全部已关或已合)之后。原话(逐字,⛔ 未翻译、未润色):「还有哪些应该解除停放的你一起排查一下」;对四组清单:「同意」。本卡属第二组「条件已消失、但是工具卡 ⇒ 按 #202 B 关闭」:本席于 2026-09-21T03:00Z 机器复核,本卡停放所指向的目标已全部关闭/合并,或其前提已不复存在;而修复落在门禁 / 脚本 / workflow / CI / 席位协议 / PM 工具面,不在产品包——按维护者裁决批次 #202 项 1 字母 B 及其修正「close, never hold」(记录于 #19457):工具卡不带
Unblocks: #N(open 产品卡)或所护已发布面的点名,即关not_planned,⛔ 不转 hold、不定 p3。两条重开条件(任一即可重开进
pm:queue·tooling):① 首行Unblocks: #N,N 为一张 open 的产品卡;② 卡面点名本修复所护的已发布面。卡上已有的测量与分析原样保留,供重开时续用。Generated by Claude Code