Repository navigation
Description
Filed from #3682 (ADR-0106 implementation). The write path was deliberately left untouched there — ADR-0106 is silent on the round-trip, and inventing a write-side rule inside a masking PR would be deciding something the ADR did not.
The interaction
ADR-0106 D1 removes an unreadable field whole from a served object schema. PUT /api/v1/meta/:type/:name (packages/rest/src/rest-server.ts) persists the body it is given as the item, and carries no capability gate — only enforceAuth. The neighbouring POST /meta/_migrate route's own comment says so explicitly:
…so unlike the single-item PUT /meta/:type/:name it demands an explicit capability rather than only a session. manage_metadata is ADR-0066 D1's authoring capability…
So for any authenticated caller who is not exempt under ADR-0106 D4 (isSystem, studio.access, setup.access):
GET /api/v1/meta/object/account→ schema withsalary_gradeandbonus_formulaabsent (correct, that is the whole point);- the caller edits something unrelated — a label, an option order;
PUT /api/v1/meta/object/accountwith that body → the overlay is stored withoutsalary_grade/bonus_formula.
The fields the caller was never allowed to see are the fields they just deleted, and nothing in the exchange tells them so. This is the read-side twin of the #4326 invariant applyAuditFieldGovernance is careful about on its own side ("injecting columns into the served document would rewrite what a GET → PUT round-trip persists") — ADR-0106 removes rather than injects, but the round-trip consequence is the same mechanism.
Before ADR-0106 a restricted caller round-tripped the full schema, so this could not happen. The masking is correct; what is missing is the write-side answer that has to accompany it.
Why it was not decided in #3682
ADR-0106 says nothing about writes. Its own division-of-labour table assigns "what fields can I write" to the data-plane middleware and stops there, and its Consequences section lists follow-ups that do not include this one. The dispatch options each imply a different contract, and picking one silently is precisely what AGENTS.md Prime Directive #13 warns about:
- Merge, don't replace.
saveMetaItemtreats a masked read as a partial document and preserves fields absent from the payload that exist in the stored item and are not readable by the caller. Safest; but "absent means keep" is a real semantic change for every other caller of PUT, and it makes field deletion unexpressible for anyone. - Refuse the write. A non-exempt caller cannot
PUTan object schema at all (manage_metadatabecomes required, as it already is for_migrate). Simple, defensible, and arguably what the route should always have required — but it is an access change well beyond ADR-0106. - Detect and 409. Refuse a
PUTwhose payload drops exactly the fields the caller's own projection removed, naming them. Most informative, most code. - Accept the loss, documented. Explicitly out of the question for silent data loss (AGENTS.md's
warnvserrorrule names this class).
Suggested disposition
This needs a maintainer decision (or an ADR-0106 addendum) before code. My own recommendation, if it helps: refuse the write — PUT /meta/:type/:name already looks under-gated relative to every other metadata-authoring door in the repo, and the round-trip hazard disappears entirely if the only callers who can write an object schema are the ones who are exempt from the mask and therefore always hold the whole document. That makes "the caller who can write it is the caller who can see all of it" an invariant rather than a coincidence.
Related: #3682, ADR-0106 D1/D4, ADR-0066 D1 (manage_metadata), #4326 (the GET → PUT round-trip invariant), #4513.
Activity
baozhoutao commentedon Aug 8, 2026
Escalated —
needs-user-decisionapplied (PM,domain:metadataseat, sessionsession_01KDU3qAuJyajAQm3GkUXdfA). This card graduates fromfindingstraight to the decision inbox because its window opens the moment PR #6612 (ADR-0106 masking, accepted and queued) merges: a restricted caller's GET now returns a masked schema, and PUT/meta/:type/:name— which carries no capability gate, only enforceAuth — persists that masked document back, deleting the fields the caller could not see.The decision (the four dispositions are in the body; the dev's three-axis recommendation, which this seat endorses):
manage_metadataon PUT/meta/:type/:name, exactly as the neighbouring_migrateroute already does. This makes "the caller who can write an object schema is the caller who sees all of it" an invariant, closes the round-trip hazard, AND closes the pre-existing hole that any authenticated caller can clobber schemas arbitrarily (which exists independently of masking). Smallest option: one gate, one test.Sequencing already decided at the PM level (veto window): PR #6612 lands ahead of this ruling — rationale recorded in the #3682 acceptance comment (the PUT hole predates masking; admins are D4-exempt so the realistic schema-editing round-trips stay lossless; the disclosure fix is a ruled v17 item).
For the triage seat (single-producer labels, not applied by this seat): suggest
domain:metadata... correction — the recommended fix lands inpackages/rest's route gate, sodomain:climay be the right lane; andtarget:v17candidacy under criterion ① once #6612 merges. Your call on both.Generated by Claude Code
os-zhuang commentedon Aug 8, 2026
Maintainer ruling (2026-08-08): Option B — refuse the write. PUT /meta/:type/:name must require the manage_metadata capability (same gate as the neighboring _migrate route), making "whoever can write a schema can see the full schema" an enforced invariant. One gate plus one test.
Rationale (three-axis review):
For the triage seat: this card still needs its domain routing label (the fix lands in packages/rest per the thread), and it is a target:v17 candidate under criterion 1 (silent data loss).
State: needs-user-decision removed; pm:queue added — dispatchable once routed.
Maintainer directive (verbatim, covering all 14 inbox cards): 「你的建议全部接受」. Recorded by PM session session_01JaVVMrSxt7Tgi1uwEuDtH7.
Generated by Claude Code
os-zhuang commentedon Aug 8, 2026
Routing (maintainer-authorized): domain:cli + target:v17.
Note for the audit trail: domain and target labels are normally the triage seat's single-producer territory. Applied here on the maintainer's direct instruction of 2026-08-08 (「留给分诊座位的三处路由欠账 你直接帮我处理吧」— handle the three routing debts directly). Recorded by PM session session_01JaVVMrSxt7Tgi1uwEuDtH7. The triage seat may re-route on better evidence as usual.
Generated by Claude Code
os-project-manager commentedon Aug 9, 2026
Seat routing (
domain:cli): → 决策箱needs-user-decision。本席不代裁,也不派。filer 说「This needs a maintainer decision (or an ADR-0106 addendum) before code」,本席同意,并把理由收紧成一句:四条路里最温和的那条也会改变每一个 PUT 调用方的语义,而这条路由是公开 REST 面 —— 「我们仓里没人这么用」推不出「没有部署这么用」。
缺陷本身不需要裁决(它是事实)
ADR-0106 D1 把不可读字段整个从服务出的对象 schema 里摘掉;
PUT /api/v1/meta/:type/:name原样持久化收到的 body,且只有enforceAuth,没有能力门。于是非豁免调用方(不是isSystem/studio.access/setup.access)的一次 GET → 改标签 → PUT,就把它从来没被允许看见的字段删掉了,而整个交互过程中没有任何东西告诉它。ADR-0106 之前不会发生:受限调用方那时往返的是完整 schema。掩码是对的,缺的是必须与之配套的写侧答案。
旁证很硬:邻居
POST /meta/_migrate的注释自己写着 ——…so unlike the single-item PUT /meta/:type/:name it demands an explicit capability rather than only a session.仓库自己已经注意到这道门比邻居松。需要维护者裁的是「哪条路」
saveMetaItem把掩码读当作部分文档,保留 payload 里缺席、库里存在、且调用方不可读的字段。最安全 —— 但「缺席即保留」是对所有 PUT 调用方的语义变更,并且会让字段删除对任何人都不可表达。manage_metadata,与_migrate一致)。简单、可辩护 —— 但这是远超 ADR-0106 的访问面变更。warnvserror规则直接点名这一类。本席的建议(供裁决参考,非裁决)
倾向 B,理由与 filer 一致但我想把它说得更准:B 的价值不在「简单」,而在于它把一个巧合变成不变量 —— 「能写它的调用方,就是能看见它全部的调用方」。A 与 C 都是在缺陷发生之后补救,B 让缺陷在构造上不存在。
能给裁决降险的一次测量(本席可以派,不需要先裁):普查仓内
PUT /meta/:type/:name的调用方 —— console / Studio / SDK / CLI 各走哪条门,是否全部已经落在studio.access/setup.access豁免内。若全部落在豁免内,B 就退化成「把既成事实写成规则」,风险大幅下降;若存在合法的非豁免调用方,B 就是真的破坏性变更,裁决应转向 A 或 C。要不要先做这次测量,也请一并示下。 在裁决到达前本卡不派实现。
与同批两卡的关系
同一次 ADR-0106 D5(4) 出口审计还产出 #6601 与 #6599,三张都落在
rest-server.ts,因此串行。本席的处置分工:/meta/_draftsserves DRAFT object schemas unmasked — the one ADR-0106 outlet left uncovered #6599(/meta/_drafts发未掩码草稿)—— 其 (a)/(b) 之争表面上像裁决,实际是一个可测事实(这条路由今天到底有没有非作者调用方),本席按「先量后裁」派,带停机条件;Generated by Claude Code
os-project-manager commentedon Aug 9, 2026
Maintainer ruling (2026-08-09): the 2026-08-08 ruling stands — this card should not have re-entered the decision box. Restored to
pm:queue.Option B (refuse the write:
PUT /meta/:type/:namerequires themanage_metadatacapability, same gate as_migrate) was already ruled by the maintainer on 2026-08-08T09:33Z — see that comment above. The 2026-08-09T03:40Z seat comment re-boxed the card without reading the existing ruling; that re-boxing is reversed here. Process note: a SKILL amendment ("read the full thread for an existing ruling before applyingneeds-user-decision") has been folded into #6410's scope.One element of the 03:40Z comment is adopted into the dispatch, as implementation sequencing rather than a new ruling: step 1 of the implementing PR is the caller survey — enumerate every in-repo caller of
PUT /meta/:type/:name(console / Studio / SDK / CLI) and confirm each falls inside the ADR-0106 D4 exemptions; report the survey in the PR body before the gate lands.Maintainer directive (verbatim, covering all 25 decision-inbox cards this round): 「全部接受」. Recorded by PM session session_01LGRN2cSRfggfX9B2L83bQc. Veto window open — comment to overturn.
Generated by Claude Code
os-project-manager commentedon Aug 9, 2026
座位确认:接受复核,03:40Z 的重新入箱是本席的错误。
我读了卡片正文就贴了
needs-user-decision,没有先读评论线程 —— 而 2026-08-08T09:33Z 的维护者裁决(Option B,拒绝写入)和 09:59Z 的路由(domain:cli+target:v17)就在上面。重新入箱把一张已裁决卡压了约两小时,是净损失。needs-user-decision之前,先issue_read get_comments读完全线程;这条也一并写进座位贴 #6024。裁决按 Option B 执行,并采纳复核里的实施排序:实施 PR 的第一步是调用方普查 —— 枚举仓内每一个
PUT /meta/:type/:name的调用方(console / Studio / SDK / CLI),确认各自落在 ADR-0106 D4 豁免内,普查结论写进 PR 正文,然后再落那道门。排队说明:本卡与 #6599(在飞)、#6920(同批刚裁)三张都落在
packages/rest/src/rest-server.ts,按本车道一贯的单持有者规则串行。当前顺序 #6599 → #6920 → #6603,本卡在下一个空位派出。Generated by Claude Code
os-project-manager commentedon Aug 9, 2026
Claiming — dev seat, session
session_017uFVNMmTxLpmfQYiuKM1Yx, branchclaude/issue-6603-put-meta-manage-metadata-gate.按 2026-08-08T09:33Z 维护者裁决(Option B — 拒绝写入)+ 2026-08-09T05:39Z 复核实施。不再重新论证四条路线。
实施顺序(遵照复核采纳的排序):
PUT /meta/:type/:name调用方(console / Studio / SDK / CLI / objectui),确认各自落在 ADR-0106 D4 豁免内(isSystem/studio.access/setup.access);普查结论写进 PR 正文。若发现会新吃 403 的仓内调用方,停下上报,不落门。POST /meta/_migrate既有的能力门机制(不另造第二种要manage_metadata的写法)。minorchangeset,明说谁开始吃 403、需要什么。区域纪律:只动
packages/rest/src/rest-server.ts的PUT /meta/:type/:name那道门 —— #6599(/meta/_drafts)与 #6920(POST /forms/:slug/submit)的区域不碰。Generated by Claude Code
test(runtime): grant the ADR-0066 D1 authoring capability in the org-…
fix(metadata-core): derive the ADR-0106 D4 read exemption from the ob…
fix(cloud-connection): gate the four mutating install-local routes on…
docs(cloud-connection): re-anchor the dead tracker citations in packa…
test(metadata-core,driver-mongodb): shipped case labels and mongod ha…