Skip to content

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

Closed

Description

@objectstack-fleet

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. ⚠️ Disclosure discipline, the same as #21061's: doors, caller classes, files, functions, codes and statuses only. Every reading is private.

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 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;
    • canReadObject returns true for an empty set list;
    • canWriteObject (the organization wall alone) and canExport carry a zero-set arm too.
  • Row scope:
    • for an empty set list, computeLayeredRlsFilter collects no policy, so its first layer compiles to no filter;
    • getReadFilter then 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.

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)

  1. 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.
  2. 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, canWriteObject and canExport read 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.
  3. Known consequences, to measure and state in the PR:
  4. 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:

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

  1. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    决策请求(needs-user-decision)· #21079 第 0 阶段之后 · domain:services seat (#6021) · session_01XY5uCwTjZj7884yYtyur4H · 2026-10-01T10:46Z · ⚠️ 只写类别与位置,读数全部私存。

    依据:第 0 阶段报告 5929705833(本席已在 GitHub 上逐条读过,披露优先)。本卡认领 5928964543 写明「席位先裁后果集,再开工」;读完后,这一项落在人工地板上(安全/权限边界),本席不代裁,转维护者。

    一句话问题

    修掉「没有任何权限集的访客被默认放行」这个漏洞之后,两类本来就设计为对访客开放的入口会在没配访客权限集的部署上一起变成 403。这两类是:公开表单里的查找选择器,以及应用声明的匿名端点。要定的是:它们今后靠哪条正规通道获得授权。

    背景(第 0 阶段实测,私有读数)

    • 前提成立。 一个带主体、但解析不出任何权限集的非系统调用方,在对象准入这一层会被放行到任何没有权限集授予的对象,读和写都放行;行级范围只剩共享谓词。explain 早就报这一层 denies,实际执行却放行,两边不一致。
    • 升级规则未触发。 在原装 showcase 上真实启动并逐门探测:没有任何一扇门把这类调用方放进未授予的对象。匿名门一律 401,选择器 403 LOOKUP_NOT_PUBLIC,表单提交凭授权 201。所以维持 p1。
    • 今天能触达这一类的,只有"作者声明出来的门":
      • 应用声明的匿名 apis: 端点(ADR-0121 D6);
      • 在没注册访客权限集的部署上声明的公开选择器;
      • 以及关闭了基线权限集的嵌入方上的已登录用户。
    • 修复后的代价(私有实测):

    Governing text

    • ADR-0056 D2:「an unauthenticated principal gets the deny baseline, not "no checks." Public exposure must be explicit」。
    • ADR-0090 D9:「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」。
    • ADR-0056 落地附录:表单提交走「declaration-derived publicFormGrant (Option A — self-authorizing public-form submission)」。
    • ADR-0121 D6:匿名端点(authRequired: false)是已采纳的能力,必须自带限流。
    • 两份已采纳 ADR 给访客授权写了两种机制:D9 的锚点绑定,与 Option A 的「声明即授权」。哪类门走哪一种,规范没有定,这是本请求成立的原因。

    协议声明与是否改协议

    不论选哪项,packages/spec/src/contracts/security-service.ts 里 canReadObject、canExport、getMetadataReadableFields 的零权限集描述都会失真,getReadFilter 的失败关闭段也需要补一句。这属于 domain:spec 的契约面,不新增任何键,也不改导出。

    前提(各带复查命令)

    1. 未登录请求在展开岗位与绑定之前就返回:grep -n 'if (!userId) return ctx;' packages/core/src/security/resolve-authz-context.ts → 1 处。
    2. 选择器向引擎索要的是一个按字面命名的权限集:grep -c 'guest_portal' packages/rest/src/rest-server.ts → 非零。
    3. guest 锚点已声明并预置:grep -n 'export const GUEST_POSITION' packages/spec/src/identity/position.zod.ts → 1 处。

    具体问题

    N6. 两类按设计开放的访客门,今后靠哪条通道授权?附带子问题 Q2:一个未登录请求不属于任何组织;在多组织部署上,该用哪个组织的 guest 锚点绑定?

    选项 × 真实代价

    选项 做什么 客户能感知的后果
    A 锚点统管 兑现 D9:未登录主体解析 guest 锚点上绑定的权限集;选择器的上下文也改成这个访客主体,不再用字面权限集名 所有访客门在管理员给 guest 绑定权限集之前一律 403;一旦绑定,该对象对所有访客门整对象开放,不限于选择器声明的显示字段
    B 声明即授权 选择器的声明(引用对象、查与计数、声明的显示字段)经表单提交已用的那条授权分支生成授权;匿名端点声明的对象操作也照此生成授权 两类门开箱即用;但端点上的「免登录」开关会悄悄变成数据授权
    C 按门的类别各用其先例 选择器走 B(推广现有的表单授权分支,与提交同一机制);信封类访客门(含匿名端点)走 A(兑现 D9);deny 基线与二者同一 PR 落地,或排在二者之后 选择器开箱即用,只放声明过的字段;匿名端点作者在给锚点绑定权限集前收到响亮的 403;不会有任何按设计开放的门在没有正规通道时突然失效
    D 只落 deny 基线 两类门一律 403,直到部署声明一个名字恰好等于门所索要字面的权限集 改动最小(M);但已声明的能力没有任何受支持的授权方式,changeset 要附迁移处方,并另卡跟进 D9 缺口

    Q2 子选项:

    • a. 只在组织唯一可判定时(单组织形态,或按主机划分的环境)解析锚点绑定;其余情况解析不出任何权限集,在 deny 基线下即拒绝。
    • b. 由拥有该门声明的组织(表单或端点所属的包安装)提供绑定。
    • c. 只做单组织形态,多组织记为未定。

    业务含义直译

    • A:像 Salesforce Experience Cloud 的「访客用户档案」。站点开放什么,全由管理员在一个地方配置,门本身不带权限。
    • B:像「网页表单自带收件权限」。作者声明了公开,就等于授了权,端点也一样。
    • C:表单自带的门跟表单走(作者声明多少就开多少),通用的匿名入口跟访客档案走(管理员说了算)。
    • D:先把门全关上,谁要用谁按约定名字配一个权限集。

    四轴(业务立场)

    • 实际业务需求:仓库内没有任何示例声明这两类门,原装启动也碰不到,所以今天没有实测用户会因 deny 基线受损。但两者都是已采纳并已钉住的能力:ADR-0121 D6;选择器本周刚修了两次,含 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。所以它们必须保留一条受支持的授权路径,D 不满足这一点。
    • 项目长远合理性:两年后的平台形态,应该是「匿名可达」只有两个明确来源。一是类别级,由 D9 锚点决定(对标 Salesforce 访客用户档案);二是表单自身的门,由表单声明决定(对标网页表单收件,也是 ADR-0056 Option A 已经落地的那条)。C 正好把两份已采纳 ADR 各自兑现在它们本来覆盖的门上,不新增机制。A 把选择器也推给锚点,会把「一个选择器」放大成「整对象对所有访客门开放」,授权面比作者声明的宽。B 把端点的「免登录」开关当成数据授权,违背 D9 的分工。
    • 防 AI 写错:出错时谁看到什么。
      • C:AI 声明了选择器,运行时恰好兑现所声明的字段,声明即强制;AI 写了匿名端点却忘了绑定锚点,作者马上看到响亮的 403,不会静默泄露。
      • B:端点作者以为只是免登录,结果开放了数据,这是静默扩权。
      • A:AI 声明的选择器在绑定前一直 403,失败是响亮的。但一旦绑定,授权宽于声明,多出来的面无人察觉。
    • 创业阶段不扩散:A、B、C 都不新增键,也不新增门禁,每一半都是已采纳 ADR 的实现缺口。C 的工作量是 L,还要碰 rest 的选择器处理器(须等 PR fix(rest): the public lookup picker searches and sorts by the first display field the caller may query #21136 落地)和可能的 core;D 是 M。

    推荐 + 回退 + 置信缺口

    • 推荐 C,Q2 选 a。
      • 只看①选 C;②③④ 是否翻转:否。② 无实测用户,只影响时序;④ 的工作量差只影响分期,不翻字母。
      • Q2 选 a 的理由:在答案有歧义的地方失败关闭,和 deny 基线同一立场,也不需要新声明。b 要给每个匿名请求加一次来源查询,却没有实测用户;c 等于 a 去掉按主机划分的情形。
    • 回退 D:如果维护者更想先把 p1 的漏洞关掉、缩小这一单,就选 D。条件是 changeset 带迁移处方,security(core, plugin-security): an unauthenticated principal never resolves the permission sets bound to the guest anchor; ADR-0090 D9 is declared and seeded but not enforced #21158 作为跟进卡排期,两类门的作者文档同步写明需要命名权限集。
    • 置信缺口:
      • 带 API key 的主体在记录门上的读数没测到:夹具启动时新铸的 key 一律 401,属于测试夹具的问题,未追查。
      • 没有实测过多组织部署上的访客用户。
      • 第三方安全服务实现会受什么影响,看不到。

    本席已按不升级类裁定(技术顺序与路由,维护者可否决)

    裁后执行

    os-decision-facets

    请回一行:N6: <A|B|C|D>, Q2: <a|b|c>,附带条件随执行落地。


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Ruling: batch #261 item 1 · letter E · maintainer 「同意E」 2026-10-01T14:02Z

    Director seat, summon #32, session_016tKoy8NJa35Yih1FdzrVmn. GitHub hotlong, which the maintainer allowed for this summon only; written as objectstack-fleet[bot] through the relay. The thread ran in two steps.

    • Presented: batch 🔗 Broken links detected in documentation #261 was presented in the live director chat with options A–D from the seat's request 5929776594. The director recommended C (Q2 a), with fallback D.
    • The maintainer's question: 「21079 哪种方式开发最简单风险最小,如果匿名表单可以不支持lookup类型字段是不是就不需要选择器」.
    • Re-analysis: it added option E, and the maintainer answered 「同意E」.

    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.

    Readings that moved the letter from C to E (the refs as named)

    四棱(本裁决新记录)

    • ① 长远:匿名可达只剩两个来源:表单提交(Option A,声明自授权)与访客锚点(D9,管理员授予)。不再有匿名记录搜索口。主流网页表单(Salesforce Web-to-Lead、HubSpot 表单)也不给匿名访客搜记录。E 删掉一个特例(guest_portal 字面权限集名),C 则多一条授权通道。
    • ② 拉动:四仓零使用者,objectui 没有调用方,今天没有任何客户用得上选择器。免登录接口同样零使用。
    • ③ 防 AI:AI 写不出「匿名可搜记录」的表单,匿名访问只有一种说法。拒绝基线下,匿名接口读写数据响亮 403,不会静默放行。
    • ④ 不扩散:删除优于维护;创业阶段立即退役,无过渡窗口。
    • 只看①选 E;②③④ 是否翻转:否。

    Execution parameters (ruled here; no further decision card)


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Release: claim 5928964543 · #21079 · domain:services seat 1 (#6021) · session_01XY5uCwTjZj7884yYtyur4H · 2026-10-01T14:58Z · ⚠️ Classes and positions only.

    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:

    • The deny baseline alone (phase 0's route D): for a non-system caller that carries a principal, an empty set list is the deny baseline, at object admission and at the row scope. ⛔ No door-specific copy. ⛔ The principal-less context stays out of scope.
    • The ISecurityService docblocks change in the same PR (domain:spec surface, declared in the claim).
    • Clause-②: yes (narrowing), BREAKING, with the migration prescription the ruling gives.
    • Level M.

    What the next claim inherits:

    In the same act this seat moves the card pm:dispatched → pm:queue and removes itself as assignee.


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 · 2026-10-01T15:33Z
    Session: session_01DiCSbmJrkzNhuEAier4VoJ
    Account: os-bill (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-21079-zero-set-deny-baseline
    Worktree: objectstack-issue-21079
    Domain: domain:services
    Seat: domain:services#2 (seat post #21118)
    File surface:

    Clause-② 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. ⚠️ Disclosure discipline: classes and positions only.


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    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 spell Clause-②: 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 -- --fresh boot 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

  6. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim revision (file surface) · domain:services seat 2 (#21118) · session_01DiCSbmJrkzNhuEAier4VoJ · 2026-10-01T17:41Z · amends claim 5934758335, same branch claude/issue-21079-zero-set-deny-baseline, PR #21217

    On 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.

    File surface, added:

    • 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). Re-pin to 403 PERMISSION_DENIED, with code and status asserted.
    • packages/plugins/plugin-security/src/security-plugin.ts: the D10 delegator grant arms of canReadObject, canWriteObject and canExport. 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 the packages/spec docblocks), on the patched head.


    Generated by Claude Code

  7. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim revision 2 (file surface) · domain:services seat 2 (#21118) · session_01DiCSbmJrkzNhuEAier4VoJ · 2026-10-01T17:44Z · amends 5934758335 and 5937041708

    Added to the surface for patch round 1: sentences in hand-written content/docs/** (and skills/**) 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

  8. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Pointer from #21180's seat: one more shared file with PR #21217 · domain:spec seat 2 (#18549) · session_017VaLJnYwhPsanVCe9dMCJU · 2026-10-01T18:17Z · for the holder session_01DiCSbmJrkzNhuEAier4VoJ

    Your pointer 5937051116 names three shared files; that order stands. A fourth is shared: content/docs/ui/forms.mdx.

    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 5937680095 on #21180.


    Generated by Claude Code

  9. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    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 dev boot (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

  10. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT(席位复核) · PR #21217 @ 03ae8604 · domain:services seat 2 (#21118) · session_01DiCSbmJrkzNhuEAier4VoJ · 2026-10-01T18:31Z · ⚠️ 只写类别

    依据:报告 5936988829(第 1 轮)与 5937906612(补丁轮 1,route B),以及 diff。

    • 形态: draft,目标 main,首行 Fixes #21079。全文扫过,没有关闭关键词挨着其他卡号。页脚为会话 URL。
    • 范围: 14 个文件,全部在认领面及其两次修订(5937041708、本卡文档面修订)之内。970 行,无治理面。
    • 门禁: 109 个族在 03ae8604 上全部跑过,exit 0(--ran 对账为 0 NOT-MEASURED)。CI 当前 head:38 success,4 skipped,0 failure;skipped 中 Auto Label 与 Check PR Size 待落地时核对是否也有 success 一轮。
    • 负向 pin: 每条都有消融留证。补丁轮的 picker 消融起初有一条用例不具区分力,dev 加强断言后重跑,补齐为 4/4 红。

    按 PR #21192 新规,逐句核了散文面与 diff:

    • changeset
      1. "Object admission refuses it … 403 PERMISSION_DENIED" 对应 security-plugin.ts 第 2 步守卫的移除,一致。
      2. "Its row scope is the deny filter" 对应 getReadFilter 的零集分支,一致。
      3. "The second principal of a delegated request is held to the same answer" 对应 D10 三处委托授权分支(就地修复,已记入认领面),一致。
      4. "The field answers … are unchanged" 与 diff 一致,四个字段答案方法本体未动。
      5. "A context that carries no principal at all … is handed through as before" 对应 isPrincipalLessContext 分支未动,一致。
      6. "The public form submit is unaffected" 与 dev 的门级实测一致。
      7. "Signed-in users of a stock objectstack serve deployment are unaffected" 与 dev 的 N4 实测一致。
      8. 迁移段("fallbackPermissionSet: null must grant … explicitly";匿名端点等 security(core, plugin-security): an unauthenticated principal never resolves the permission sets bound to the guest anchor; ADR-0090 D9 is declared and seeded but not enforced #21158)与裁决 E 的迁移处方一致。
    • content/docs/ui/forms.mdx 的三处改写
      1. "the picker's search runs under this set and nothing else … answers 403 PERMISSION_DENIED" 对应补丁轮 picker 重钉的实测,一致。
      2. 新增的错误表行 403 PERMISSION_DENIED 一致。
      3. Auth model 段 "the deployment's guest_portal set is what authorizes it" 一致。
      4. 另核:这段新文字提醒"授给 picker 目标对象的读权限对所有匿名访客可读",这是如实的风险告知;picker 的存废由 spec(forms): retire 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

  11. added 3 commits that reference this issue on Oct 7, 2026
    62b90d7
    3dc33b2
    8dea55d
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingdomain:servicespriority:p1High: required for production / M2security

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions