security(plugin-security): a non-system caller who resolves no permission set is admitted to every object and read with no row scope; an empty set list grants by absence instead of answering the deny baseline #21079
Description
Ruled: 5933054144 · letter E · 2026-10-01T14:02Z
Was blocked by #20995. That block is spent: #20995 closed when PR #21051 merged, as triage recorded in 5925669762. Under ruling E this card has no blocker; its one serial constraint is #21180, on a shared dogfood file.
Filing gate: ① a product defect with a measured reach:, under the possible-data-disclosure exception. This is the plugin-security half that #21061's emergency triage (5924543711) split off and asked this seat to file.
reach: measured by #20995's dev during that card's reach step (os-dev-report 5924254306 on #20995, out_of_scope_findings F1). The readings are in the dev's private scratch space, and this seat has read them. Filed by the domain:services execution seat (#6021, session_01XY5uCwTjZj7884yYtyur4H). ⛔ Not a claim.
What was measured (by class)
- The caller class: a non-system caller that carries a principal (the guest envelope, a picker context, or a user id) and whose permission-set resolution returns no set. Measured members:
- an unauthenticated request carried as the guest envelope;
- a picker context on a deployment with no guest set registered;
- a signed-in user on an embedder that disables the baseline (a constructor option;
objectstack servenever passes it).
- What it gets: on the measured boots, object-level admission to objects no set grants. The row-scope skip is read from source (below), ⛔ not measured.
- The unauthenticated member: aggregates over any object at the analytics cube-read door. security (P0 suspect): a non-system caller who resolves no permission set — an unauthenticated one included — is admitted to aggregate any object at an analytics door, object admission and row scope skipped #21061 closes that door with the authentication its siblings apply.
- The signed-in member on the baseline-disabled embedder: the record doors answer
200for objects no set names, the record and list doors included. - The engine itself, with no route in between: a read as the picker context or the guest envelope is admitted.
- Why security (P0 suspect): a non-system caller who resolves no permission set — an unauthenticated one included — is admitted to aggregate any object at an analytics door, object admission and row scope skipped #21061's door gate does not close the class: the door gate refuses the unauthenticated caller at one door. Every door that serves a principal with no set still reaches the same early returns, the doors that carry a guest by design included.
The positions (source read at origin/main, cited by #21061's grade)
In packages/plugins/plugin-security/src/security-plugin.ts:
- Object admission:
- the engine middleware's step 2 CRUD gate runs only under a non-empty set list;
canReadObjectreturnstruefor an empty set list;canWriteObject(the organization wall alone) andcanExportcarry a zero-set arm too.
- Row scope:
- for an empty set list,
computeLayeredRlsFiltercollects no policy, so its first layer compiles to no filter; getReadFilterthen returns the sharing predicate alone, which constrains only objects whose sharing model is private;- step 2.6's depth stash and
checkAuthoredRowWrite('abstain') also stand down for an empty set list.
- for an empty set list,
ADR-0056 D2 says an unauthenticated principal gets the deny baseline, not "no checks". ADR-0090 D9 says a guest holds the guest position and nothing else.
Direction (from #21061's grade; ⛔ not a ruling)
- Measure first, privately: which doors and callers this class reaches on a stock boot and on a baseline-disabled embedder, and what each known consequence below costs.
- One answer per layer: for a non-system caller that carries a principal, an empty set list is the deny baseline.
- At object admission, step 2's guard and the zero-set arms of
canReadObject,canWriteObjectandcanExportread that one answer. - At the row scope,
getReadFilter's zero-set path answers the deny sentinel, as its failure paths already do. - ⛔ No door-specific copy of the rule. ⛔ The principal-less context (no positions, no sets, no user id) is out of scope; ADR-0096 stages it separately.
- At object admission, step 2's guard and the zero-set arms of
- Known consequences, to measure and state in the PR:
- a picker context on a deployment with no guest set is admitted today only by the absence of sets, and would be refused (the neighbour security(forms): two public-form doors meet a field masking rule wrongly — the submit read-back serves masked fields stored to an anonymous submitter, and a picker whose first display field is masked answers 403 to every caller it applies to #21062 owns that door);
- a signed-in caller on an embedder that disables the baseline loses the everything-default;
- the
ISecurityServicedocblocks inpackages/spec/src/contracts/security-service.tsthat state the zero-set answers belong todomain:spec. A change to them is a contract-lane edit, and the field-level zero-set answer is security(spec): a capability-gated field (requiredPermissions, no maskingRule) is served stored to a caller who resolves no permission set, while explain reports it hidden; narrowing it moves getReadableFields' contract answer #21063's.
- Pins: a zero-set caller of each measured member is refused object admission and gets the deny scope, with a signed-in member resolving a set as the control. No pin title states a request.
Why Blocked-by: #20995: PR #21051 (#20995) holds security-plugin.ts and is in the merge queue. This card also changes every door's zero-set answer, so it measures on top of #20995's merge.
Reader who acts
Triage (grade and route), then the domain:services seat. plugin-security is domain:services.
Dedupe
mcp__github__search_issues, repo-scoped, open and closed, in the act that filed this card:
- 「zero permission sets caller admitted object admission canReadObject empty set deny baseline row scope」: 9 hits.
- security (P0 suspect): a non-system caller who resolves no permission set — an unauthenticated one included — is admitted to aggregate any object at an analytics door, object admission and row scope skipped #21061 is the parent: the door half, which split this half off.
- security (P0 suspect): the managed-object blanket gives non-admin shipped sets an object-level read on two
access: privatecredential-bearing identity objects that the sets themselves declare deny-by-design #20027, member_default gives every authenticated member read on sys_scim_user / sys_scim_group with no row policy and no tenant column: one organization's IdP-provisioned users (emails, names) are readable from any other #20001 and/auth/me/permissionsand/me/appsstill apply the baseline only when the caller resolves to ZERO sets — the ADR-0090 D5 fallback cliff, one plane over from where it was abolished #7608 (closed) are other mechanisms: a shipped set's blanket on identity objects, a baseline set's grant, and the plane where the baseline applies. - A permission set accepts a hierarchy
readScopebesideviewAllRecords: true, never reads it, and emits no diagnostic — the declaration materialises and a capability census counts it as coverage #16870, analytics: read-scope-sql's ruled $not-over-$in-empty residue has no open card — and #13640 turned it into an echo-vs-execution disagreement on the ObjectQL strategy #13926, [permissions] 行级读可见范围无法按业务字段收窄:viewAllRecords 全有/全无两档之间缺共享规则 #4376, finding: after ADR-0106, a restricted caller's GET → edit → PUT of an object schema DELETES the fields that were masked out of their read #6603 and permission-set backfill (ADR-0094 D4) 现在 100% 失败:行里的active存储列喂进了 #4001 之后严格化的 permission spec #4669 (closed) are unrelated permission-set or read-scope defects.
- None is this class's object admission or row scope in
plugin-security. - Neighbours: security(plugin-security): for a caller who resolves no permission set, the field-projection answers say no masking rule reaches it, while
maskingRule's describe and the result masker mask it — which one a public door serves is not measured #20995 (this class's field masking, PR fix(plugin-security)!: a caller who resolves no permission set is served a masked field masked and may not query on it #21051), security(spec): a capability-gated field (requiredPermissions, no maskingRule) is served stored to a caller who resolves no permission set, while explain reports it hidden; narrowing it moves getReadableFields' contract answer #21063 (this class's capability-gated fields) and security(forms): two public-form doors meet a field masking rule wrongly — the submit read-back serves masked fields stored to an anonymous submitter, and a picker whose first display field is masked answers 403 to every caller it applies to #21062 (the picker door).
Dedupe words: zero permission sets object admission · empty set list deny baseline · no-set caller row scope · canReadObject zero-set arm
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 1, 2026
决策请求(needs-user-decision)· #21079 第 0 阶段之后 ·⚠️ 只写类别与位置,读数全部私存。
domain:servicesseat (#6021) ·session_01XY5uCwTjZj7884yYtyur4H· 2026-10-01T10:46Z ·依据:第 0 阶段报告
5929705833(本席已在 GitHub 上逐条读过,披露优先)。本卡认领5928964543写明「席位先裁后果集,再开工」;读完后,这一项落在人工地板上(安全/权限边界),本席不代裁,转维护者。一句话问题
修掉「没有任何权限集的访客被默认放行」这个漏洞之后,两类本来就设计为对访客开放的入口会在没配访客权限集的部署上一起变成 403。这两类是:公开表单里的查找选择器,以及应用声明的匿名端点。要定的是:它们今后靠哪条正规通道获得授权。
背景(第 0 阶段实测,私有读数)
denies,实际执行却放行,两边不一致。401,选择器403 LOOKUP_NOT_PUBLIC,表单提交凭授权201。所以维持 p1。apis:端点(ADR-0121 D6);200变403 PERMISSION_DENIED。并且把权限集绑定到系统预置的guest锚点也救不回来:该绑定今天从未对未登录主体生效,已另立 security(core, plugin-security): an unauthenticated principal never resolves the permission sets bound to theguestanchor; ADR-0090 D9 is declared and seeded but not enforced #21158。Governing text
guest— NEW builtin, undeletable; unauthenticated principals hold it implicitly and nothing else.」;分工是「the guest position answers "which object classes are anonymously reachable at all"」;P2 证明「a guest sees exactly the guest bindings and nothing else」。publicFormGrant(Option A — self-authorizing public-form submission)」。authRequired: false)是已采纳的能力,必须自带限流。协议声明与是否改协议
不论选哪项,
packages/spec/src/contracts/security-service.ts里canReadObject、canExport、getMetadataReadableFields的零权限集描述都会失真,getReadFilter的失败关闭段也需要补一句。这属于domain:spec的契约面,不新增任何键,也不改导出。前提(各带复查命令)
grep -n 'if (!userId) return ctx;' packages/core/src/security/resolve-authz-context.ts→ 1 处。grep -c 'guest_portal' packages/rest/src/rest-server.ts→ 非零。guest锚点已声明并预置:grep -n 'export const GUEST_POSITION' packages/spec/src/identity/position.zod.ts→ 1 处。具体问题
N6. 两类按设计开放的访客门,今后靠哪条通道授权?附带子问题 Q2:一个未登录请求不属于任何组织;在多组织部署上,该用哪个组织的
guest锚点绑定?选项 × 真实代价
guest锚点上绑定的权限集;选择器的上下文也改成这个访客主体,不再用字面权限集名guest绑定权限集之前一律403;一旦绑定,该对象对所有访客门整对象开放,不限于选择器声明的显示字段403;不会有任何按设计开放的门在没有正规通道时突然失效403,直到部署声明一个名字恰好等于门所索要字面的权限集Q2 子选项:
业务含义直译
四轴(业务立场)
403,不会静默泄露。403,失败是响亮的。但一旦绑定,授权宽于声明,多出来的面无人察觉。rest的选择器处理器(须等 PR fix(rest): the public lookup picker searches and sorts by the first display field the caller may query #21136 落地)和可能的core;D 是 M。推荐 + 回退 + 置信缺口
guestanchor; ADR-0090 D9 is declared and seeded but not enforced #21158 作为跟进卡排期,两类门的作者文档同步写明需要命名权限集。401,属于测试夹具的问题,未追查。本席已按不升级类裁定(技术顺序与路由,维护者可否决)
ISecurityService描述由同一 PR 改,domain:spec契约面在认领里申报,开工前给domain:spec发跨车道通知。依据是契约优先:已发布契约不得陈述实现已不再给出的答案,哪怕只隔一个合并窗口。fix(plugin-security,spec)!: a caller who resolves no permission set is not served a capability-gated field, and the security contract says so #21134 有同类先例,且在契约复审档 PASS。Clause-②拼法到时按选定路线逐面判。裁后执行
guestanchor; ADR-0090 D9 is declared and seeded but not enforced #21158 作为本卡的Blocked-by:或同一 PR 的一半,由本席在同一张认领里申报core与rest的跨车道面。顺序是先做 D9 解析与选择器授权,deny 基线同 PR 或随后落地,并改钉上述用例。os-decision-facets
403;B 是静默扩权。Prior rulings read:
guest anchor binding·zero permission sets object admission→ 14 + 3 hits; ADR-0056 D2 / Option A, ADR-0090 D9, ADR-0121 D6; Permission-set resolution binds by POSITION NAME while sys_position_permission_set sits near-empty — confirm name-based resolution is the intended mechanism, not an accident the empty junction hides #13419 (position-name fold retired); thread: security (P0 suspect): a non-system caller who resolves no permission set — an unauthenticated one included — is admitted to aggregate any object at an analytics door, object admission and row scope skipped #21061 grade5924543711, triage5925669762.推荐:C(Q2:a)。只看①选 C;②③④ 是否翻转:否。
置信缺口:API key 主体的门读数、多组织访客、第三方安全服务实现。
请回一行:
N6: <A|B|C|D>, Q2: <a|b|c>,附带条件随执行落地。Generated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
Ruling: batch #261 item 1 · letter E · maintainer 「同意E」 2026-10-01T14:02Z
Director seat, summon #32,
session_016tKoy8NJa35Yih1FdzrVmn. GitHubhotlong, which the maintainer allowed for this summon only; written asobjectstack-fleet[bot]through the relay. The thread ran in two steps.The ruling
E = this card lands the deny baseline ALONE (route D), and the anonymous public-form picker is retired on its own card, #21180.
canReadObject/canWriteObject/canExport) and at the row scope (getReadFilteranswers the deny sentinel). ⛔ No door-specific copy. ⛔ The principal-less context stays out of scope (ADR-0096).publicPicker— anonymous public forms stop taking lookup / master_detail / user fields, and the anonymous picker route goes (ruling E on #21079) #21180: anonymous public forms no longer take lookup /master_detail/userfields.publicPickeris retired, andGET /forms/:slug/lookup/:fieldis deleted. This reverses thepublicPickeris enforced by the REST lookup route but declared nowhere inpackages/spec— no saved form can ever enable it #7467 ruling (option 1, declarepublicPicker).guestanchor bindings apply on a multi-organization deployment travels with security(core, plugin-security): an unauthenticated principal never resolves the permission sets bound to theguestanchor; ADR-0090 D9 is declared and seeded but not enforced #21158, which stays on hold. Its restart is rewritten in the same act as this ruling.Readings that moved the letter from C to E (the refs as named)
publicPickerhas no producer outside spec, tests, docs and its own REST route (objectstackfbcc05f400), and none in hotcrmfb408a7304or cloud.^\s*authRequired:\s*falsehas 0 hits inexamples(objectstack); the controlauthRequired: truehitsexamples/app-showcase/src/system/apis/index.ts:82.main6c3da53aeehas no source requesting the picker route:lookup/|/lookupoverpackagesandappshits only comments and a/dev/lookupdev route. Control: the same tree hits the anonymous form's/forms/routes.maind34aa58a2a, re-run 12:47Z: the unauthenticated early return (resolve-authz-context.ts, 1), the literalguest_portalpicker context (rest-server.ts, 7),GUEST_POSITION(position.zod.ts:150), and the zero-set guardpermissionSets.length > 0 && !referentialFieldClearWrite(1). The defect stands.:95) and the flip's Option A (:224-:229); ADR-0090 D9 (:346-:350) and:496(the anchors are tenant-level only); ADR-0121 D6; ADR-0061:54(anonymous search "keeps the existingpublicPickermodel", which spec(forms): retirepublicPicker— anonymous public forms stop taking lookup / master_detail / user fields, and the anonymous picker route goes (ruling E on #21079) #21180 annotates in a separate Tier H PR);publicPickeris enforced by the REST lookup route but declared nowhere inpackages/spec— no saved form can ever enable it #7467 (reversed here); Permission-set resolution binds by POSITION NAME while sys_position_permission_set sits near-empty — confirm name-based resolution is the intended mechanism, not an accident the empty junction hides #13419 (the position-name fold is retired). Thread: triage 5925669762, phase 0 5929705833, seat request 5929776594.四棱(本裁决新记录)
guest_portal字面权限集名),C 则多一条授权通道。Execution parameters (ruled here; no further decision card)
needs-user-decision→pm:dispatchedin the act that records this ruling. The claim 5928964543 (domain:servicesseat 1,os-justin) stands, and the build phase follows phase 0's measured route D.ISecurityServicedocblocks change in the same PR (the seat's in-seat Q3, which stands).Clause-②: yes (narrowing), with the ADR-0087 marker the gate asks for. The migration prescription: a caller that resolves no permission set is refused object admission and reads nothing. An app-declared anonymous endpoint (authRequired: false) can no longer read or write objects until theguestanchor's bindings are resolved for anonymous callers (security(core, plugin-security): an unauthenticated principal never resolves the permission sets bound to theguestanchor; ADR-0090 D9 is declared and seeded but not enforced #21158). An embedder that setsfallbackPermissionSet: nullmust grant its signed-in users a set explicitly.publicPicker— anonymous public forms stop taking lookup / master_detail / user fields, and the anonymous picker route goes (ruling E on #21079) #21180 lands second.publicPicker— anonymous public forms stop taking lookup / master_detail / user fields, and the anonymous picker route goes (ruling E on #21079) #21180 re-pinspackages/qa/dogfood/test/zero-set-masking.dogfood.test.tstoo. The second to claim names the other.main.publicPickerdefects) is superseded by spec(forms): retirepublicPicker— anonymous public forms stop taking lookup / master_detail / user fields, and the anonymous picker route goes (ruling E on #21079) #21180 and closednot_plannedin this act.guestanchor; ADR-0090 D9 is declared and seeded but not enforced #21158 stayspm:on-hold. Its condition (this ruling) is spent, so its restart is rewritten in this act: it restarts when a first-party anonymous data door exists. Q2 goes with it.Generated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
Release: claim⚠️ Classes and positions only.
5928964543· #21079 ·domain:servicesseat 1 (#6021) ·session_01XY5uCwTjZj7884yYtyur4H· 2026-10-01T14:58Z ·Why this seat lets go: the maintainer ordered this seat to stand down once its current work lands, and to start nothing new: 「当前任务处理完,合并后就下班」. That was said in this seat's session and recorded on #6021 (
5932495429). This card's build had not started (phase 0 only), so it goes back to the lane queue now, instead of waiting hours behind this seat's landings. Seat 2 (#21118) or the next incumbent claims it under the ruling below. This is not a change of route, and the premise is not falsified.The ruling to build: E, record
5933054144(maintainer 「同意E」, director summon #32), with its execution parameters as written there:ISecurityServicedocblocks change in the same PR (domain:specsurface, declared in the claim).Clause-②: yes (narrowing), BREAKING, with the migration prescription the ruling gives.What the next claim inherits:
5929705833(public, classes and positions): the caller classes (N1), the positions to change (N3, five arms) and what must stay gated, the consequences (N4), the docblock positions (N5), and the pins that move (21plugin-securitycases in 5 files, 4 dogfood cases in 2 files).publicPicker— anonymous public forms stop taking lookup / master_detail / user fields, and the anonymous picker route goes (ruling E on #21079) #21180 (domain:spec, the picker's retirement) also re-pinspackages/qa/dogfood/test/zero-set-masking.dogfood.test.ts. The second of the two to claim names the other, and the picker door's dogfood case belongs to whichever lands second. security(spec): a capability-gated field (requiredPermissions, no maskingRule) is served stored to a caller who resolves no permission set, while explain reports it hidden; narrowing it moves getReadableFields' contract answer #21063 (PR fix(plugin-security,spec)!: a caller who resolves no permission set is not served a capability-gated field, and the security contract says so #21134) and security(forms): two public-form doors meet a field masking rule wrongly — the submit read-back serves masked fields stored to an anonymous submitter, and a picker whose first display field is masked answers 403 to every caller it applies to #21062 (PR fix(rest): the public lookup picker searches and sorts by the first display field the caller may query #21136) are onmain.guestanchor; ADR-0090 D9 is declared and seeded but not enforced #21158 stays on hold under the restart the ruling rewrote.In the same act this seat moves the card
pm:dispatched→pm:queueand removes itself as assignee.Generated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
Claim: PM loop round 1 · 2026-10-01T15:33Z
Session:
session_01DiCSbmJrkzNhuEAier4VoJAccount:
os-bill(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-21079-zero-set-deny-baselineWorktree:
objectstack-issue-21079Domain:
domain:servicesSeat:
domain:services#2(seat post #21118)File surface:
packages/plugins/plugin-security/src/security-plugin.ts: the zero-set arms (step 2's guard;canReadObject/canWriteObject/canExport) andgetReadFilter's zero-set path.plugin-securitycases in 5 files and the 4 dogfood cases in 2 files that phase 0 named as moving.packages/spec/src/contracts/security-service.ts: theISecurityServicezero-set docblocks. This is adomain:specsurface that ruling E puts in this PR; declared here, dispatched vertically.Stop on breach; explain in the report.
Container & model:
M,mode:subagent,model: opus(dispatch-gates--tier: the spec path is a clause-② SUSPECT surface). Built at the default tier. The contract-review-tier review is owed before enqueue (clause ②yes, and the spec path limb), run by an isolated at-tier subagent.Clause-②: yes
Thread-read: 5934091842
Serial constraints cleared:
publicPicker— anonymous public forms stop taking lookup / master_detail / user fields, and the anonymous picker route goes (ruling E on #21079) #21180 (domain:spec, dispatched, retiring the anonymous picker) also re-pinspackages/qa/dogfood/test/zero-set-masking.dogfood.test.ts. As seat 1's release5934091842asks, this second claim names it. The picker door's dogfood case belongs to whichever of the two lands second, and that one adapts to the other's shape.665cab33) and security(forms): two public-form doors meet a field masking rule wrongly — the submit read-back serves masked fields stored to an anonymous submitter, and a picker whose first display field is masked answers 403 to every caller it applies to #21062 (PR fix(rest): the public lookup picker searches and sorts by the first display field the caller may query #21136) are onmain. The dev builds on them.plugin-securityorsecurity-service.ts. [Decision] #20822 F7: retiring formula's whole-day copy — the RLS write check judges the raw post-image, so deleting the copy refuses writes the same policy's read shows #21109 (plugin-security's RLS write check, ruled A) waits behind this card in this seat's queue.Selection:
priority:p1, released by seat 1 at5934091842(the maintainer's stand-down order for that seat), to be claimed under ruling E (5933054144, maintainer 「同意E」).Clause-②⚠️ Disclosure discipline: classes and positions only.
yes(narrowing), as ruling E's execution parameters state. The zero-set caller's answers narrow to the deny baseline at object admission and at the row scope, and the published contract's docblocks narrow to match.Generated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
os-dev-report
{
"issue": 21079,
"status": "blocked",
"branch": "claude/issue-21079-zero-set-deny-baseline",
"pr": "#21217",
"session": "session_01DiCSbmJrkzNhuEAier4VoJ",
"premise_still_valid": true,
"summary": "Ruling E has landed as route D in draft PR #21217 at 3e49b1a. A non-system caller that carries a principal and resolves no permission set is now refused at object admission. The engine middleware step 2 CRUD gate no longer has the resolved-set guard, so it answers PERMISSION_DENIED/403 for every engine operation. canReadObject, canExport and canWriteObject answer false for that caller, and getReadFilter answers the deny sentinel. The principal-less context is the one exception (isPrincipalLessContext, ADR-0096), and it is unchanged. The ISecurityService docblocks narrow in the same PR: canReadObject, canExport, getMetadataReadableFields and the getReadFilter deny cases; no key or export moves. The changeset is BREAKING with the ruling's migration text verbatim, carries Clause-②: yes (narrowing), and its ADR-0087 marker is accepted by the gate. Premise re-measured privately on base 0b12b9e, engine level. The guest envelope, a picker-shaped context, a signed-in user on a baseline-disabled embedder, and an unresolvable-names context were each admitted (find and insert), probes true, no row scope. After the change each is refused 403 PERMISSION_DENIED, probes false, deny sentinel. The principal-less context and the set-holding controls are unchanged. N4, measured at door level on bootStack with the dist checked by preflight. 26 stock-showcase and security dogfood files are green on both legs. The anonymous authRequired:false endpoint, the picker and the baseline-disabled record door each go from 200 to 403 PERMISSION_DENIED; the endpoint still passes authentication. The public form submit is unaffected. Fork clause: not triggered. No stock-boot door was found serving the class on the dogfood showcase harness; a real pnpm dev boot was NOT MEASURED. BLOCKED ON ONE SEAT DECISION. Re-derived on current main, the moving set is phase 0's 21 plugin-security cases in 5 files and 4 dogfood cases in 2 files, all re-pinned, plus 5 more cases in 2 files outside the claim surface. Those are packages/qa/dogfood/test/public-picker-queryable-key.dogfood.test.ts (1 case) and packages/rest/src/public-form-lookup-picker-queryable-key.test.ts (4 cases), both picker pins from #21062 (PR #21136, landed after phase 0). The #21180 branch deletes both. Per the claim's stop-on-breach rule they are not edited, so the PR is red on those 5 cases. Route A: serialize behind #21180 (it deletes both files), then merge main. Route B: add both files to the claim surface and run a patch round that re-pins them to 403. Everything else in the PR is complete and verified locally.",
"tests": "All runs under os-verify-lock. (1) plugin-security full suite at 3e49b1a: 155 files, 3339 passed, 23 skipped. Base 0b12b9e: 154 files, 3321 passed. Right after the change, before re-pinning: 21 failed in 5 files, the same 21 phase 0 named. plugin-security typecheck exit 0; tsconfig.test.json --listFiles includes all 6 edited test files. (2) New zero-set-deny-baseline.test.ts: 18/18. It pins, per class, refusal of all 7 engine operations before they run (code+status), the three probes false and the deny sentinel. Controls: a set-holding member, a registered named set and a set-holding delegator. Boundary: the principal-less context. (3) Ablations: one leg per negative pin via scripts/ablation-replace.mjs (wrap mode), each with an EXIT/INT/TERM trap restoring from HEAD on an absolute path. Every leg landed (anchor 1 to 0, blob changed) and went red on the final test files: step2 guard 23/139; canReadObject arm 3; canWriteObject arm4 9; canExport arm 3; getReadFilter line 6; delegator grant arms 1 each (read, write, export). The first canExport-delegator attempt was a no-op: the replacement contained the anchor, the tool refused, nothing ran. It was re-run with a new anchor. Final state: blob a4a95715 == HEAD, git diff HEAD empty, status clean. (4) Dist-level leg: base security-plugin.ts restored into the tree (blob 970439f6, marker 3 to 0), plugin-security rebuilt, dogfood set run: the 4 re-pinned dogfood cases red, 27 other files green; the rest picker file 19/19 green on base. Then restored, rebuilt, and the preflight found the marker in 2 built files. (5) Dogfood set at the final head: 30 files, 313 passed, 1 failed (the out-of-surface picker file). Consumers that boot the real plugin through its build: rest 13 files/224 passed (plus the out-of-surface picker file, 4 failed), runtime 7/93, objectql 41, verify 6, plugin-auth 76, service-automation 21, plugin-sharing 20, plugin-approvals 2. (6) Spec contract tests (security-service, core-service-contracts) 31/31; spec check:generated: all 15 artifacts up to date. (7) Lint, narrowed: 10 changed .ts files, eslint --format json counts 10 files with 0 errors and 0 warnings. The population is eslint.config.mjs files **/*.{ts,...} minus NEVER_LINTED. The config has no type-aware linting (no parserOptions.project), so untouched files cannot move.",
"gates": "dispatch-gates --repo objectstack-ai/objectstack --commands derived 87 families at 3e49b1a. All 87 run at 3e49b1a, exit 0. --ran with recorded exit codes: 87 derived, 87 run, 0 NOT-MEASURED (a derived zero). Two needed earlier fixes. check:engine-double-contract was red on the new suite's fake engine; its findOne now opens with assertEngineFindOnePredicate, and the pinned ledger is untouched. check:dual-build-cjs-loads answered PREREQUISITE NOT MET until 8 packages were built (turbo, under lock). Spec gates named by the dispatch are all exit 0: check:authorable-surface, check:liveness, check:docs, check:api-surface. The derivation tree is 7 commits behind origin/main now; the only overlapping file is a new plugin-security test whose caller resolves a set. CI on the PR: in_progress at report time (10 success, 3 skipped, 19 in progress); it is expected red on the 5 out-of-surface picker cases.",
"line_budget": "11 files, +591/-200 vs merge base 0d42104 (dispatch-gates: 789 changed lines, under the 5000 human-merge threshold). No governed surface and no line-ratcheted ledger is touched. Level M, as ruled.",
"files_changed": [
".changeset/21079-zero-set-deny-baseline.md",
"packages/plugins/plugin-security/src/security-plugin.ts",
"packages/plugins/plugin-security/src/zero-set-deny-baseline.test.ts",
"packages/plugins/plugin-security/src/zero-set-masking.test.ts",
"packages/plugins/plugin-security/src/zero-set-capability-fold.test.ts",
"packages/plugins/plugin-security/src/can-write-object-admission.test.ts",
"packages/plugins/plugin-security/src/get-writable-fields.test.ts",
"packages/plugins/plugin-security/src/predicate-related-read-tenant-scope.test.ts",
"packages/qa/dogfood/test/zero-set-masking.dogfood.test.ts",
"packages/qa/dogfood/test/declarative-endpoint-policy.dogfood.test.ts",
"packages/spec/src/contracts/security-service.ts"
],
"deviations": [
"Breach stop: 2 more moving-pin files are outside the claim surface: the picker queryable-key dogfood file and the rest unit file. They are not edited, and the PR is red on 5 cases until the seat picks route A (serial behind #21180) or route B (extend the surface, then a patch round). Phase 0 predated PR #21136; it named PR #21136 as a serial constraint.",
"Zone 2 assumption 4 falsified: an untouched picker-door case in zero-set-masking.dogfood turns red under the deny baseline; phase 0 N4 listed it as moving. It is re-pinned to 403 PERMISSION_DENIED with its fixture untouched, not restructured. If #21180 lands first, its deletion of the case wins at merge.",
"Bounded in-place fix (all four conditions hold): the D10 delegator grant arm in canReadObject (arm 6), canWriteObject (arm 8) and canExport stood down on a delegator who resolves no set, while the middleware refuses one. Measured privately before the change: middleware 403, all three probes true. The fix drops the length guard on those three arms; the capability arms are left as they were, because the grant arm dominates them. The claim file surface should add these three arms; I cannot write the claim.",
"Clause-② spelling: the claim line is bare
Clause-②: yes. The PR body and changeset spellClause-②: yes (narrowing), as ruling E's execution parameters and the dispatch state.","ADR-0087 marker is not-required (no-migration-prescription), which the gate accepts. Phase 0 expected that category to be wrong under route D. The ruling's migration text is a grant decision, not a metadata rewrite, as in the #21061 and #21063 precedents.",
"Merged origin/main (0d42104) into the branch before the gate run, because dispatch-gates flagged a stale tree; rebuilt the dogfood closure (63/63). main has since moved 7 more commits; not merged again.",
"Attribution: the harness reminder asked for a model-named Co-Authored-By trailer and a different PR footer. Commits carry the model-free pair per AGENTS.md, and the PR body ends with the session-URL footer per AGENTS.md and the dispatch.",
"To read the #21180 branch I created the ref refs/issue-21079-peek/21180 in the shared .git; it was deleted after the read.",
"A real
pnpm dev -- --freshboot is NOT MEASURED this round (phase 0 measured one). The stock-door and fork-clause readings come from the dogfood showcase harness on both legs. The API-key principal is NOT MEASURED, as in phase 0."],
"mcp_calls": "0",
"api_writes": "3 — all through the fleet-write relay as objectstack-fleet[bot] (POST /repos/objectstack-ai/objectstack/dispatches): (1) pr_create, which executed POST /repos/objectstack-ai/objectstack/pulls; PR #21217 draft, 15092 bytes read back identical. (2) label-write --assign os-bill, which executed POST /repos//issues/21217/assignees; read back os-bill, and the path labels the labeler added are untouched. (3) This os-dev-report comment, POST /repos//issues/21079/comments. git push is not a REST write.",
"open_questions": [
{
"question": "Two picker pins outside the claim surface turn red under the deny baseline: public-picker-queryable-key dogfood (1 case) and rest public-form-lookup-picker-queryable-key (4 cases). Both come from #21062 / PR #21136, and the #21180 branch deletes both. Which route lands this PR?",
"options": [
"A — serial: #21180 lands first and deletes both files and the picker door; this branch then merges main, takes #21180's deletion of the picker case in zero-set-masking.dogfood, and goes green with no further edit",
"B — extend the claim surface to both files; a patch round re-pins them to 403 PERMISSION_DENIED, and this PR can land first; #21180 then deletes them as planned"
],
"recommendation": "A. Both files are deleted on the #21180 branch, which carries a changeset and is in flight. Re-pinning files that are about to be deleted is churn, and it makes conflict work for #21180. Under A this PR needs no new edit. B only if the seat wants this p1 fix to land before #21180."
}
],
"out_of_scope_findings": [
"carrier: #21180 (its branch edits packages/rest/src/rest-server.ts) · noted, not filed — the export / explain fail-stance docblock in rest-server.ts still says the middleware skips its CRUD gate when set resolution comes back empty; comment only, stale after this PR",
"carrier: 承接者:无 · noted, not filed — when the write-gate probe refuses a caller, validate() leaves the related record unresolved, and a traversing rule then fails with a message telling the author to fix the rule's condition or declare the field. That is a misattribution, because the related read was withheld. It is existing behaviour for every refused caller; this PR only adds callers to that class. reach not measured at a public door"
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
Claim revision (file surface) ·
domain:servicesseat 2 (#21118) ·session_01DiCSbmJrkzNhuEAier4VoJ· 2026-10-01T17:41Z · amends claim5934758335, same branchclaude/issue-21079-zero-set-deny-baseline, PR #21217On the build report
5936988829:Seat decision: route B (the report's open question). This card re-pins the two picker pins that are outside its surface and lands on its own clock. It does not wait for #21180.
5933054144) puts the deny baseline and the picker retirement on separate cards and fixes no landing order. Its serial clause gives the picker door's dogfood case to "whichever of this card and spec(forms): retirepublicPicker— anonymous public forms stop taking lookup / master_detail / user fields, and the anonymous picker route goes (ruling E on #21079) #21180 lands second".403 PERMISSION_DENIEDuntil spec(forms): retirepublicPicker— anonymous public forms stop taking lookup / master_detail / user fields, and the anonymous picker route goes (ruling E on #21079) #21180 deletes it. The ruling's own reading puts that cost at zero:publicPickerhas no producer outside spec, tests, docs and its own route, and objectui has no first-party caller.publicPicker— anonymous public forms stop taking lookup / master_detail / user fields, and the anonymous picker route goes (ruling E on #21079) #21180 (domain:spec, the maintainer's queue-jump) has an active branch but no PR yet. Holding a finished p1 security fix behind it has no stated end.publicPicker— anonymous public forms stop taking lookup / master_detail / user fields, and the anonymous picker route goes (ruling E on #21079) #21180: after this lands, spec(forms): retirepublicPicker— anonymous public forms stop taking lookup / master_detail / user fields, and the anonymous picker route goes (ruling E on #21079) #21180 resolves a modify/delete on the two files below by taking its deletion, and takes thezero-set-maskingpicker case as the ruling assigns it.File surface, added:
packages/qa/dogfood/test/public-picker-queryable-key.dogfood.test.ts(1 case) andpackages/rest/src/public-form-lookup-picker-queryable-key.test.ts(4 cases). Re-pin to403 PERMISSION_DENIED, withcodeandstatusasserted.packages/plugins/plugin-security/src/security-plugin.ts: the D10 delegator grant arms ofcanReadObject,canWriteObjectandcanExport. This is the dev's in-place fix: the arms stood down for a delegator who resolves no set while the middleware refuses one (same defect class, mechanical, unclaimed, same gate family; measured before and after; ablated). Recorded here so the claim matches the PR.Still owed before enqueue: the isolated contract-review-tier review (
Clause-②: yes (narrowing), plus thepackages/specdocblocks), on the patched head.Generated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
Claim revision 2 (file surface) ·
domain:servicesseat 2 (#21118) ·session_01DiCSbmJrkzNhuEAier4VoJ· 2026-10-01T17:44Z · amends5934758335and5937041708Added to the surface for patch round 1: sentences in hand-written
content/docs/**(andskills/**) that state the zero-set caller's old answers, meaning admission, no row scope, or the picker answering. Each is rewritten to the deny baseline. The trigger is PR #21217's docs-drift reading (5936999532, 8 pages name a changed symbol), checked against ruling E's semantics. ⛔content/docs/releases/**is release-owned and stays read-only; a false sentence there is reported, not edited. Under PR #21192's rule, the seat checks the docs and changeset prose sentence by sentence at ACCEPT.Generated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
Pointer from #21180's seat: one more shared file with PR #21217 ·
domain:specseat 2 (#18549) ·session_017VaLJnYwhPsanVCe9dMCJU· 2026-10-01T18:17Z · for the holdersession_01DiCSbmJrkzNhuEAier4VoJYour pointer
5937051116names three shared files; that order stands. A fourth is shared:content/docs/ui/forms.mdx.:103, "A form that declares a public picker … answers403 PERMISSION_DENIED"), and a403 PERMISSION_DENIEDrow to the picker's error table (about:291).Whichever lands second removes the picker text the other introduced: the section-2 sentence and the table row go with the route. Either order works. Recorded in claim amendment
5937680095on #21180.Generated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
os-dev-report
{
"pr": "#21217",
"issue": 21079,
"status": "done",
"round": "patch round 1 (route B, claim revision 5937041708)",
"branch": "claude/issue-21079-zero-set-deny-baseline",
"head": "03ae8604",
"session": "session_01DiCSbmJrkzNhuEAier4VoJ",
"premise_still_valid": true,
"summary": "Patch round 1 implements route B (claim revision 5937041708) on PR #21217; the new head is 03ae860. origin/main 097ef80 was merged as merge commit 2e1564f (no rebase, no force), followed by pnpm install --frozen-lockfile and a rebuild of the dogfood and rest closures (63/63). The 5 out-of-surface picker cases are re-pinned to the deny baseline, each asserting 403 with code PERMISSION_DENIED. In packages/rest/src/public-form-lookup-picker-queryable-key.test.ts (4 cases), the no-guest-set class moves into its own describe and no case is deleted. The listing, the search, the masked-only picker and the no-masked-field control all answer 403 PERMISSION_DENIED with no row served and no stored value in the body. The key the picker composes is still asserted. The masked-only case also proves the engine refuses even a query keyed on a field the caller may query on, which only object admission refuses. In packages/qa/dogfood/test/public-picker-queryable-key.dogfood.test.ts (1 case), a visitor with no session and a signed-in caller each get 403 PERMISSION_DENIED on the listing and the search, and the masked-only picker gets the same; nothing is served. An ablation leg put back the base step 2 guard, the only arm the picker door reads. It turned exactly the 4 rest cases and the 1 dogfood case red, and both files were green again after a proven restore. The docs addendum rewrote three passages of content/docs/ui/forms.mdx that this PR made false; no other page in content/docs (outside releases and the generated references) or in skills states the old answers. The delegator-arm in-place fix is unchanged and is now recorded in the claim. The PR body was updated through the relay (route B and why, the delegator-arm evidence, the picker re-pins and their leg, the docs rewrites, and gates at 03ae860). The closing line for this card, the Clause-② line and the session-URL footer are kept, and no closing keyword sits next to any other card number. The body read back identical. The worktree is removed right after this report (node_modules first, no --force).",
"tests": "All runs under os-verify-lock at 03ae860 unless stated. (1) plugin-security full suite: 156 files, 3345 passed, 23 skipped (main added one file). typecheck exit 0, check:test-typecheck OK. (2) rest full local project: 256 files, 4844 passed, 310 skipped. typecheck exit 0; tsconfig.test.json --listFiles includes the re-pinned file. (3) dogfood typecheck exit 0 (tsconfig include test/*). The dogfood set from round 1 (30 files, including the picker file, the two zero-set dogfood files and the anonymous-endpoint file): 314 passed, 0 failed. The plugin-security dist preflight found the deny-baseline marker present in 2 built files with a clean tree. (4) Picker ablation leg via scripts/ablation-replace.mjs in wrap mode, under an EXIT/INT/TERM trap restoring HEAD on an absolute path. The anchor was the step 2 guard: 1 to 0 hits, blob a4a95715 to b3abe51e. The wrapped child rebuilt plugin-security, and the preflight found the injected guard in 2 built files. The rest file went red on 4 of 19 (exactly the 4 re-pinned cases) and the dogfood file on 1 of 1. Restore proven: blob a4a95715 == HEAD and git diff HEAD empty. After the rebuild, the --absent preflight read the marker absent from all 6 built files and the tree clean. Both files then ran green (19/19, 1/1). The first run of this leg turned only 3 rest cases red: the masked-only case stayed green, because the field guard alone answers the same code and status. The pin was strengthened (commit 335cae7) and the leg re-run, which gave the 4/4 above. (5) Round-1 evidence stands for the unchanged plugin-security pins: 8 ablation legs, the dist-level leg, the consumer suites and the spec tests. (6) Lint, narrowed: the 12 changed .ts files vs merge base 097ef80, eslint --format json counts 12 files with 0 errors and 0 warnings. The population is eslint.config.mjs files /*.{ts,...} minus NEVER_LINTED. There is no type-aware linting, and the lint config is untouched.",
"gates": "At 03ae860: dispatch-gates --repo objectstack-ai/objectstack --commands derived 109 families (the change set is 14 paths vs merge base 097ef80; the forms.mdx edit added 22 docs families). All 109 run at 03ae860, exit 0. --ran with recorded exit codes: 109 derived, 109 run, 0 NOT-MEASURED (a derived zero). No stale-tree warning at derivation. check:dual-build-cjs-loads read its 8 prerequisite packages after they were built (turbo, 41 tasks, under lock). check:spec entry-nameability prints its standing NOT MEASURED note for two subpaths with no callable export; that note is not about this diff. CI read once at 03ae860, not awaited: 31 success, 4 skipped, 5 in progress, 0 failure.",
"line_budget": "14 files, +722/-248 vs merge base 097ef80 (dispatch-gates: 970 changed lines, under the 5000 human-merge threshold). No governed surface: skills/ was read, not edited. Level M.",
"files_changed": [
".changeset/21079-zero-set-deny-baseline.md",
"content/docs/ui/forms.mdx",
"packages/plugins/plugin-security/src/security-plugin.ts",
"packages/plugins/plugin-security/src/zero-set-deny-baseline.test.ts",
"packages/plugins/plugin-security/src/zero-set-masking.test.ts",
"packages/plugins/plugin-security/src/zero-set-capability-fold.test.ts",
"packages/plugins/plugin-security/src/can-write-object-admission.test.ts",
"packages/plugins/plugin-security/src/get-writable-fields.test.ts",
"packages/plugins/plugin-security/src/predicate-related-read-tenant-scope.test.ts",
"packages/qa/dogfood/test/zero-set-masking.dogfood.test.ts",
"packages/qa/dogfood/test/declarative-endpoint-policy.dogfood.test.ts",
"packages/qa/dogfood/test/public-picker-queryable-key.dogfood.test.ts",
"packages/rest/src/public-form-lookup-picker-queryable-key.test.ts",
"packages/spec/src/contracts/security-service.ts"
],
"docs_rewrites": [
"content/docs/ui/forms.mdx:106-111 (now 106-118). OLD: authorization no longer depends on the guest set; you only need one for the legacy back-compat path; keep it INSERT-only on the target object. NEW: that holds for the submit. A form with a public picker needs a guest set granting read on the picker's target object, because without one the picker answers 403 PERMISSION_DENIED (the ADR-0056 D2 deny baseline). Keep the set INSERT-only on the form's target object, and know that any read granted a picker's target object is readable by every anonymous visitor through that picker.",
"content/docs/ui/forms.mdx:294 (new row after the 403 LOOKUP_NOT_PUBLIC row; previously absent). NEW: 403 PERMISSION_DENIED when no guest set grants read on the picker's target object; the search context resolves no permission set and is refused at object admission; the declaration bounds what is searched and never grants the read.",
"content/docs/ui/forms.mdx:294 (now 302). OLD: the lookup context carries no publicFormGrant, which is why its result set is bounded by the picker declaration instead. NEW: the declaration bounds what the search matches and projects, and the guest set authorizes it; with no such set granting read on the target object, the search context resolves no permission set and is refused at object admission (403 PERMISSION_DENIED, the deny baseline)."
],
"docs_audit": "Read all 8 drift-check pages: kernel/contracts/index, kernel/runtime-services/index, permissions/field-level-security, permissions/index, permissions/sharing-rules, permissions/system-context, plugins/packages and ui/forms. Only ui/forms carried false sentences. system-context names the four probes only in its isSystem bypass table, which is unchanged. sharing-rules' getReadFilter sentence (resolution failures fail closed) stays true. Greps over content/docs (excluding releases/ and the generated references/) and skills (*.md, .mdx). The broad pattern (no permission set | zero permission set | empty set | no sets resolved | resolves no set | falls through | fall-open | everything-default | fallbackPermissionSet | publicPicker | guest_portal | lookup picker | authRequired: false) hit 20 docs files and 3 skills files; each hit was read. The narrow pattern for a set-less caller being admitted, unrestricted or skipped hit 0. Control: the term "permission set" hits 8 times in permissions/index.mdx. skills/objectstack-api states authRequired:false as an authentication opt-out only, which is still true, so no skills edit was needed and skills/ stays untouched (governed). The generated references/ui/view.mdx describes the picker opt-in and its 403 LOOKUP_NOT_PUBLIC, which is still true. Release pages implementation-status, v15, v17/17-0 and v17/17-5 were read and not edited; none states the old zero-set admission, so there is no release finding.",
"deviations": [
"Picker pin strengthened beyond a re-pin. The masked-only rest case additionally asserts that the engine refuses a query keyed on a queryable display field. Without that assertion the case was not discriminating: the first ablation run left it green, so this is recorded as a no-op leg for that case. It was re-run red after the change.",
"The picker ablation leg puts back the base step 2 guard rather than restoring the whole base security-plugin.ts, because ablation-replace mutates by anchor. That guard is the only arm the picker door reads (getQueryableFields is field-level and unchanged), and round 1 already measured the whole-file dist-level leg.",
"docs surface: content/docs/ui/forms.mdx edited under the PM addendum; the seat records the surface addition on #21079.",
"The round-1 deviations stand: the Clause-② spelling, the ADR-0087 marker, the attribution form, and no real
pnpm devboot (NOT MEASURED).","main merged a second time (097ef80, 8 commits), as ordered. The only overlapping files were a new plugin-security test with a set-holding caller and rest-server.ts (tracker-number text, not the picker's behaviour)."
],
"mcp_calls": "0",
"api_writes": "2 this round, both through the fleet-write relay as objectstack-fleet[bot] (POST /repos/objectstack-ai/objectstack/dispatches). (1) issue_patch on PR #21217, which executed PATCH /repos//issues/21217 with body only: 18329 bytes read back identical, still draft. (2) This os-dev-report comment, POST /repos//issues/21079/comments. git push (merge plus 3 commits) is not a REST write. Round-1 writes are not repeated.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: #21180 (its branch edits packages/rest/src/rest-server.ts) · noted, not filed — the export/explain fail-stance docblock in rest-server.ts still says the middleware skips its CRUD gate when set resolution comes back empty; comment only",
"carrier: 承接者:无 · noted, not filed — a probe-refused caller's validate() preview fails a traversing rule with a message telling the author to fix the rule's condition or declare the field, which misattributes a withheld related read; existing behaviour; reach not measured at a public door"
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
ACCEPT(席位复核) · PR #21217 @⚠️ 只写类别
03ae8604·domain:servicesseat 2 (#21118) ·session_01DiCSbmJrkzNhuEAier4VoJ· 2026-10-01T18:31Z ·依据:报告
5936988829(第 1 轮)与5937906612(补丁轮 1,route B),以及 diff。main,首行Fixes #21079。全文扫过,没有关闭关键词挨着其他卡号。页脚为会话 URL。5937041708、本卡文档面修订)之内。970 行,无治理面。03ae8604上全部跑过,exit 0(--ran对账为 0 NOT-MEASURED)。CI 当前 head:38 success,4 skipped,0 failure;skipped 中 Auto Label 与 Check PR Size 待落地时核对是否也有 success 一轮。按 PR #21192 新规,逐句核了散文面与 diff:
403 PERMISSION_DENIED" 对应security-plugin.ts第 2 步守卫的移除,一致。getReadFilter的零集分支,一致。isPrincipalLessContext分支未动,一致。objectstack servedeployment are unaffected" 与 dev 的 N4 实测一致。fallbackPermissionSet: nullmust grant … explicitly";匿名端点等 security(core, plugin-security): an unauthenticated principal never resolves the permission sets bound to theguestanchor; ADR-0090 D9 is declared and seeded but not enforced #21158)与裁决 E 的迁移处方一致。content/docs/ui/forms.mdx的三处改写403 PERMISSION_DENIED" 对应补丁轮 picker 重钉的实测,一致。403 PERMISSION_DENIED一致。guest_portalset is what authorizes it" 一致。publicPicker— anonymous public forms stop taking lookup / master_detail / user fields, and the anonymous picker route goes (ruling E on #21079) #21180 处理,本 PR 不涉及。security-service.ts的 docblock:有四处收窄(getReadFilter拒绝基线、getMetadataReadableFields段落、canExport、canReadObject),都只改文字,没有键或导出变动。最终判定交给契约级复核。仍欠: 隔离的契约级复核(
CONTRACT_REVIEW_TIER;Clause-②: yes (narrowing)加packages/spec已发布 schema 面)。复核已经起跑,记录会落在 PR 上。等 PASS 在案、全部 check 转绿后,才入队。Generated by Claude Code
fix(plugin-security,spec)!: a non-system caller that carries a princi…
feat(spec,rest,lint)!: retire the form field's publicPicker and the a…
docs(adr): ADR-0061 D5 — dated note: the anonymous publicPicker searc…