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

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

    Serial constraint, recorded for whoever claims this card · domain:services seat (#6021) · session_01XY5uCwTjZj7884yYtyur4H · 2026-10-01T05:08Z · ⛔ Not a claim.


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade — bug · security · priority:p1 · domain:services · area:access · pm:queue. The block is spent; the claim answers fold-or-serial against #21063

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-01T05:59Z. ⛔ Not a claim, ⛔ not a dispatch. ⛔ Classes and positions only (#21061's disclosure discipline).

    The body's Blocked-by: #20995 is spent. #20995 closed completed when PR #21051 merged at 2026-10-01T05:17Z, read at this write. So the card is queued, not blocked.

    Why p1, not p0.

    Routing. plugin-security is domain:services. The ISecurityService docblocks that state the zero-set answers are domain:spec's, so the claim declares that surface, as #21063's claim did.

    Direction. It is the card's own scope, confirmed:

    • one answer per layer: 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, and ⛔ the principal-less context stays out of scope (ADR-0096 stages it);
    • the known consequences are measured and stated in the PR.

    Serial. The domain:services seat's note 5925153040 stands: #21063 (p0) is claimed and edits the same zero-set stand-in, so this card's claim answers fold-or-serial against it.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 · 2026-10-01T09:49Z
    Session: session_01XY5uCwTjZj7884yYtyur4H
    Account: os-justin (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 (seat 1, seat post #6021)
    Selection: the full order, read fresh at this pick. This card and #21080 are the two unclaimed priority:p1 bug cards in this lane, and this one is older. #21080 also waits for #20987's PR #21117 (in the merge queue), which edits the same canHandle. Seat 2 (#21118) holds neither.
    Direction: triage's grade 5925669762. 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 (ADR-0096). The known consequences are measured and stated in the PR. Raise rule: if the measurement shows a stock-boot door serving this class, the claim raises the card to p0 and says so here.
    Phase: phase 0 first (measure and route, no file changed). It is a platform-wide narrowing with named consequences (a picker context on a deployment with no guest set; a signed-in caller on a baseline-disabled embedder). The seat rules on the consequence set before any build.
    File surface (the build phase's, declared now, narrowed by phase 0):


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report (phase 0)

    {
    "issue": 21079,
    "status": "blocked",
    "phase": "0 (measure and route; nothing committed, pushed or opened)",
    "branch": "claude/issue-21079-zero-set-deny-baseline",
    "pr": null,
    "session": "session_01XY5uCwTjZj7884yYtyur4H",
    "premise_still_valid": true,
    "summary": "Phase 0 is complete; the build waits for the seat's ruling on the consequence routes. Measured on origin/main b3917d9 (not shallow), on top of PR #21051, PR #21101 and PR #21134, and with #21061's door half (PR #21098, 2bddb19) confirmed landed. PREMISE HOLDS: a non-system caller that carries a principal and resolves no permission set is admitted at object admission to objects no set grants, for reads AND writes (an engine insert was admitted too), and its read scope is only the sharing predicate (a private-OWD object answered no rows; a public one answered every row). Explain already reports the object_crud layer denies for these callers while enforcement admits, so the direction also makes explain and enforcement agree. RAISE RULE NOT TRIGGERED: on a stock showcase boot (a real pnpm dev -- --fresh boot and the dogfood showcase harness) no door serves this class an object no set grants. Every anonymous door answered 401 UNAUTHENTICATED (record, all analytics faces, meta, automation, actions, ai, packages, mcp, keys, notifications, security, the showcase's declared endpoints) or is non-engine by design (form spec 200, i18n 200, the permissions self-read 200, a share-link token 404 under its system context), the picker answered 403 LOOKUP_NOT_PUBLIC (the showcase declares no picker), and the submit answered 201 under its grant. The class is reached today only through AUTHORED doors: an app-declared anonymous apis: endpoint (ADR-0121 D6) and a declared public-form picker on a deployment with no matching set, plus every door for a signed-in user on a baseline-disabled embedder. N1, the classes, by resolvePermissionSetsForContext: the guest envelope resolves no set unless a set is literally NAMED guest (none exists in the tree), and a set bound to the seeded guest anchor is NOT resolved for it (the binding changed nothing at the door). The picker context resolves no set unless a set is named guest_portal (only app-crm declares one, and it grants no read). The form-grant context resolves the same way but is admitted by the grant branch before any zero-set arm. A signed-in user, and an API-key principal (it carries a user id), resolve no set only on a baseline-disabled embedder. An agent principal always resolves its restricted floor set. A context naming only unresolvable sets or positions with no user id is a further member. The principal-less context is handed through (ADR-0096) and is unchanged by every reading. N4, the after side (uncommitted edit of five arms, restored and proven): the guest envelope, the picker context and the no-baseline signed-in user are refused 403 PERMISSION_DENIED at object admission, and getReadFilter answers the deny sentinel. The public-form submit stays 201 on every deployment. A guest-set deployment is unchanged. The stock showcase is unchanged at every door. The two by-design guest doors go dark on a deployment with no matching set (the anonymous endpoint and the picker both 403 PERMISSION_DENIED), and the ADR-0090 D9 anchor binding does not rescue them. So the build needs a ruling on N6 first. Disclosure: the readings, fixture shapes and private scripts are in the seat's private scratch (issue-21079/phase0-evidence.md and its raw files); nothing here states a request.",
    "N1_classes": [
    "guest envelope (runtime dispatcher face, assembleExecutionContextOrGuest; core assemble-execution-context.ts:302,317) — carries a principal (positions + principal kind) by triage's definition; resolves no set on the stock showcase, on a no-guest-set and on a baseline-disabled deployment; resolves one only when a set is NAMED guest (the position-name fold, security-plugin.ts:6316-6361); anonymous authz carries no set names (resolve-authz-context.ts:415), so a binding to the seeded guest anchor is never resolved for it",
    "public picker context (rest-server.ts:11092-11095) — carries a principal (a named set); resolves no set unless a set named guest_portal exists",
    "public-form grant context (rest-server.ts:10862-10866) — same resolution; admitted by the grant branch (security-plugin.ts:2034-2110) before any zero-set arm; its masker reads resolveCallerPosture",
    "signed-in user on a baseline-disabled embedder (fallbackPermissionSet: null, security-plugin.ts:730, constructor :1193) — carries a user id; resolves no set without explicit grants",
    "API-key principal — carries a user id, so the baseline applies (two sets on a stock-shaped deployment); resolves no set only on a baseline-disabled embedder. Door reading NOT MEASURED: a freshly minted key was answered 401 by the record door on every fixture boot (harness posture, not chased)",
    "agent principal — always resolves its restricted floor set (security-plugin.ts:6332-6333, :6370-6381); never in the class",
    "other: a context naming only unresolvable sets or positions and no user id (the #20995 / #21063 pin classes) — resolves no set; in the class",
    "principal-less context (no position, no named set, no user id) — handed through at security-plugin.ts:2207; ADR-0096's, out of scope, unchanged in every reading"
    ],
    "N2_doors": [
    "stock showcase (real pnpm dev -- --fresh boot, no configuration, plus the dogfood showcase harness): no door serves the class an object no set grants — RAISE RULE NOT TRIGGERED (statuses in the summary)",
    "#21061: LANDED on main — 2bddb19 is an ancestor of b3917d9 (exit 0, self-proving); every anonymous analytics face answered 401 UNAUTHENTICATED on both boots",
    "no-guest-set deployment (fixture): an app-declared anonymous endpoint and a declared picker both answer 200 serving an object no set grants; object admission was skipped and no row filter applied on a public-OWD object (no layer decided); on a private-OWD object the row scope (sharing predicate) decided, with an empty result",
    "guest-set deployment (fixture; sets named guest_portal and guest): the same two doors answer 200 for the object the set grants; the set decides",
    "baseline-disabled embedder (fixture): a signed-in user with no grant gets 200 on record, list and query, 201 on create, 200 on export, the analytics cube read, the dataset query and a session-gated declared endpoint, all for objects no set grants; object admission skipped; on a private-OWD object the row scope decided"
    ],
    "N3_positions": [
    "step 2 CRUD guard security-plugin.ts:2508 (permissionSets.length > 0 && !referentialFieldClearWrite) → !referentialFieldClearWrite: a principal-carrying zero-set caller reaches checkObjectPermission with no set and is refused 403 PERMISSION_DENIED; the principal-less context and the form grant never reach it",
    "canReadObject arm 2 :5772 (return true) → return isPrincipalLessContext(context)",
    "canExport :6170 (return true) → the same predicate",
    "canWriteObject arm 4 :6057-6071 (field arm + organization wall only) → false for a principal-carrying caller; this also closes the no-payload corner #21134's ACCEPT carried here",
    "getReadFilter zero-set path :5390-5408 (RLS null, controlled-by-parent null, sharing predicate only) → the deny sentinel RLS_DENY_FILTER (rls-compiler.ts:376) its failure paths already answer (:5353, :5387, :5417)",
    "computeLayeredRlsFilter :7128 (collection :7207 / :8634) — unchanged: with no set Layer 1 compiles to nothing, but every caller of it for this class is now refused upstream (step 2) or answered the sentinel (getReadFilter); the Layer 0 tenant wall (:7426) is unaffected",
    "step 2.6 depth stash :2588 — unchanged and MUST stay gated: ungated, getEffectiveScope answers 'org' for an unmatched set list (permission-evaluator.ts:294), a widening; after the change it is unreachable on reads for this class, and on the referential FK-clear update it keeps standing down (narrowest)",
    "checkAuthoredRowWrite zero-set 'abstain' :5235 — unchanged; abstain is already the non-widening answer and the write is refused at step 2",
    "any other: step 1.5 capability gate :2399 (the stand-in carries no object capability :5705; step 2 refuses first, same code and status); steps 2.7 :2703, 2.8 :2828, 3.6 :3217 and the delegator resolution :2265 stay gated and become unreachable for this class; the field layers (2.5 :2877, 2.5a :2913, 2.5b :2941, 2.9 :3656, the projections :5591/:5604, resolveCallerPosture :5693) are #20995/#21063's and unchanged — the stand-in is still read by the grant branch's masker; explain (explain-engine.ts object_crud) already denies and needs no change"
    ],
    "N4_consequences": [
    "classes, after: guest envelope, picker context, no-baseline signed-in user, no-baseline API-key context and the unresolvable-names context → object admission refused 403 PERMISSION_DENIED (find and insert), canReadObject/canExport/canWriteObject false, getReadFilter the deny sentinel; signed-in member, agent, principal-less and every guest-set class unchanged",
    "public picker on a deployment with no guest set: STOPS WORKING — 200 → 403 PERMISSION_DENIED (refused at object admission); on a deployment with a set named guest_portal granting the referenced object: unchanged 200",
    "public-form submit: UNAFFECTED — 201 before and after on every deployment; the grant's admission runs before the zero-set arms (the service answers for that context flip, but the submit door never asks them)",
    "signed-in user on a baseline-disabled embedder: LOSES THE EVERYTHING-DEFAULT — every door 403 (PERMISSION_DENIED; export EXPORT_NOT_PERMITTED), including an object a non-baseline set names",
    "anonymous-serving dispatcher domains: the stock ones already answer 401 at the door, so nothing changes there; the app-declared anonymous apis: endpoint (ADR-0121 D6) STOPS WORKING on a deployment with no set named guest — 200 → 403 PERMISSION_DENIED — and a binding to the seeded guest anchor does not rescue it",
    "stock showcase: identical at every door before and after",
    "pins that move (measured): plugin-security unit 21 cases in 5 files (zero-set-masking 9, zero-set-capability-fold 9, can-write-object-admission 1, get-writable-fields 1, predicate-related-read-tenant-scope 1; the same 5 files 121/121 green on HEAD); dogfood 4 cases in 2 files (declarative-endpoint-policy: the anonymous endpoint and its cache header; zero-set-masking: the picker door and the no-baseline record door); unchanged: the shared-showcase files, public-form read-back masking, showcase public form, persona matrix, authz conformance; consumer suites that boot the real plugin through its build (rest 17 files, runtime 3, automation 1) green on both legs",
    "open PR #21136 (the picker key, draft) adds a unit pin and a dogfood pin asserting the picker serves a no-session visitor on a deployment with no guest set; both would turn red under the deny baseline unless N6's route keeps the picker granted"
    ],
    "N5_contract_docblocks": [
    "packages/spec/src/contracts/security-service.ts @ b3917d9, by position:",
    "canReadObject docblock (lines 573-616): the arm-list sentence at 587-591 (it names the no-sets skip at 588) and the second sentence of the Fails CLOSED paragraph, 603-605 — become false",
    "canExport docblock (548-570): the second sentence of the Fails CLOSED paragraph, 567-569 — becomes false",
    "getMetadataReadableFields docblock (312-357): the first sentence of the 'Why the two differ' paragraph, 323-327 (the middleware skips its permission-set grant gates for a zero-set caller), and that paragraph's last sentence, 333-337 (not a restricted caller) — become false for a caller that carries a principal",
    "getReadFilter Fails CLOSED paragraph (256-259) stays true but becomes incomplete: a third deny case; getReadableFields (302-308), getWritableFields (411-413) and checkAuthoredRowWrite (729-735) stay true (field-level answers are #21063's)"
    ],
    "N7_clause2_size_holders": [
    "Clause-② grammar: Clause-②: yes (narrowing) if the same PR edits the contract docblocks (the #21134 precedent, whose at-tier record accepted that spelling for a docblock-only contract narrowing); Clause-②: no (narrowing) for a plugin-only PR. Either way minor with the BREAKING banner; the ADR-0087 marker depends on N6 — under option D a migration prescription exists (bind or name a set), so not-required (no-migration-prescription) would be wrong there",
    "size, deny baseline alone: security-plugin.ts (5 arms, about 10 code lines, plus about 60-90 docblock/comment lines); a new plugin-security pin file (about 250-350) and the 5 re-pinned files (about 80-200); security-service.ts docblocks (about 15-30); 2 dogfood files re-pinned plus one door-level dogfood pin (about 160-260); changesets. About 8-10 files, 600-900 changed lines, 3 packages: M",
    "size with route C: add the picker's declaration-derived grant (the existing grant branch generalized, plus the picker context in rest-server.ts) and the guest-anchor binding resolution (plugin-security resolution or core resolveAuthzContext), each with pins: L, and it touches rest (the picker handler, held by #21062 / PR #21136 until it lands) and possibly core",
    "holders: no open PR touches security-plugin.ts or contracts/security-service.ts (all 11 open PRs' file lists read); no pm:dispatched claim surface names either file (21 open cards; #21081's PR #21152 states it does not edit security-plugin.ts; #20234 excludes packages/spec/src/**); serial constraint: PR #21136 (see N4)"
    ],
    "tests": "All private, under os-verify-lock.sh, at b3917d9. (1) Dogfood dependency closure built (63/63 tasks), then the full workspace (72/72) for the real boot. (2) A private dogfood probe (four bootStack boots: stock showcase with its app default profile; no guest set; guest sets; baseline disabled), run on the base build (4/4) and on the after build (4/4), copied in only to run and deleted by its trap. (3) Real stock showcase boot on a private port (checked free by lsof first), 24 anonymous doors probed by status and code; the server was stopped by its recorded process group and the port proven free. (4) After side: an uncommitted edit of five arms through a literal-anchor applier (each anchor 1 → 0 hits, each marker 0 → 1, blob 970439f663 → f5aea4b746), plugin-security rebuilt and ablation-dist-preflight found the marker in 2 built files (absent on the pristine build). Restore: git checkout HEAD -- on the absolute path inside an EXIT/INT/TERM trap, proven by blob equality with HEAD and an empty git diff HEAD, three times; rebuilt (18/18 cache hits) and the preflight found the marker absent from all 6 built files. (5) plugin-security unit suite with the edit: 154 files, 21 failed / 3300 passed / 23 skipped; control leg on HEAD: the same 5 files 121/121. Dogfood set: base 117/117 isolated + 81/81 shared, after 113/117 + 81/81. Consumers through the build: rest 465/465, runtime 52/52, automation 6/6 on both legs. Final state: git status --porcelain empty before the worktree was removed. turbo 2.11.5 wrote its agent-guidance block into AGENTS.md on each build; it was restored every time (carried by #21146).",
    "mcp_calls": "0",
    "api_writes": "1 — this comment, through post-stamped.mjs and the fleet-write relay (POST /repos/objectstack-ai/objectstack/dispatches executing POST /repos//issues/21079/comments). Reads were REST GETs only. No push, no PR, no label, no assignee write.",
    "open_questions": [
    {
    "question": "N6. Under the deny baseline, two by-design guest doors stop working on a deployment with no matching set: the public-form lookup picker and an app-declared anonymous apis: endpoint (ADR-0121 D6). Which route keeps them working? (No in-tree example declares either; the stock showcase is unaffected.)",
    "options": [
    "A — route (a), the door's context carries a set: the guest principal resolves the sets bound to the seeded guest anchor (delivering ADR-0090 D9, declared and seeded but never resolved for an unauthenticated principal today), and the picker's context becomes that guest principal instead of a literal set name. Business: matches D9's division of labor (the anchor answers which objects anonymous may reach); needs an administrator's binding per deployment, and packages can only suggest it. Long term: delivers a declared mechanism, no workaround. AI-safety: an AI that declares a picker or endpoint and forgets the binding gets a loud 403, never a silent widening; the grant is object-wide for every guest door. Startup: implementation of an accepted ADR, no new key; opens a per-organization question (Q2).",
    "B — route (b), the door's declaration is the grant: the picker's own declaration (its referenced object, find and count, its declared display fields) becomes a declaration-derived grant through the one branch publicFormGrant already uses; an anonymous endpoint's declared object operation likewise. Business: works out of the box, like the submit (ADR-0056 Option A). Long term: one grant branch, no door-specific admission copy; for endpoints it conflates the authentication opt-out with authorization, which D9 assigns to the anchor. AI-safety: declaration equals enforcement for the picker, narrowest grant; for endpoints, an opt-out of authentication would silently become a data grant. Startup: no new key.",
    "C — route (c), measured: per door class, the precedent that already governs it. The picker by (b), through the existing grant branch generalized (ADR-0056 Option A, the submit's own precedent). Guest-envelope doors by (a), the D9 anchor resolution. The deny baseline lands with them, or after them, so no by-design door goes dark without a working grant channel. Four axes: each door keeps its current business use; both halves are existing accepted mechanisms delivered rather than new surface; the picker author's declaration is the whole grant and an endpoint author gets a loud 403 until the anchor grants; no new key and no new gate.",
    "D — land the deny baseline alone. Both doors answer 403 PERMISSION_DENIED until a deployment declares a set NAMED after the literal the door requests (guest_portal, or guest through the position-name fold the #13419 ruling retires). Smallest build (M), but it leaves a declared capability with no supported way to grant it (Prime Directive #10), and it needs a migration prescription in the changeset."
    ],
    "recommendation": "C. Business need: nothing in the tree or on the stock boot uses either door, so the deny baseline costs no measured user. But both doors are accepted, pinned capabilities (ADR-0121 D6; the picker received two fixes this week), so they must keep a supported grant. Long term: (b) for the picker is ADR-0056's own Option A, applied through the same branch, and (a) for the guest envelope delivers ADR-0090 D9 instead of leaving its seeded anchor declared and unenforced. AI-safety: absence stops granting anything, the picker's declaration is the entire grant, and a forgotten anchor binding is loud. Startup: no new key, no new gate, and each half is an implementation gap in an accepted ADR. Sequencing: the D9 resolution and the picker grant first or in the same PR, then the deny baseline; PR #21136 lands before the build either way. If the seat prefers the smaller build, D is acceptable only with the changeset's migration prescription and a follow-up card for the D9 gap."
    },
    {
    "question": "Q2 (product semantics, raised by route A/C). An unauthenticated request carries no organization. Which organization's guest anchor bindings apply to it on a deployment with more than one organization?",
    "options": [
    "A — resolve the anchor's bindings only where one organization is unambiguous (the single posture, or a host-scoped environment); elsewhere the guest principal resolves no set, which under the deny baseline means refused",
    "B — the organization that owns the door's declaration (the form's or endpoint's package installation) supplies the bindings",
    "C — defer: resolve on the single posture only and record the multi-organization case as NOT DECIDED until a named multi-organization guest user exists"
    ],
    "recommendation": "A. It fails closed exactly where the answer is ambiguous, which is the deny baseline's own stance, and it needs no new declaration. B adds a provenance lookup on every anonymous request for no measured user, and C is A without the host-scoped case. Four axes: no measured multi-organization guest deployment (business); no workaround (long term); ambiguity is refused rather than guessed (AI-safety); no new surface (startup)."
    },
    {
    "question": "Q3 (routing). Who edits the ISecurityService docblocks that become false (N5)? The same PR, as a declared cross-lane surface (as #21134 did), or a separate domain:spec PR?",
    "options": [
    "A — same PR, with the domain:spec surface declared in the claim; Clause-②: yes (narrowing) per the #21134 precedent",
    "B — a separate domain:spec PR; the plugin PR declares Clause-②: no (narrowing), and the contract states a false zero-set answer between the two merges"
    ],
    "recommendation": "A. Contract-first: the published contract must not state an answer the implementation stopped giving, even for one merge window. It has a precedent with an at-tier PASS. The edit is docblock-only, with no export or check:api-surface movement."
    }
    ],
    "out_of_scope_findings": [
    "class: b · reach: a public door — an app-declared anonymous endpoint answered the same status with and without a permission set bound to the seeded guest anchor, and the guest envelope resolved no set either way (before and after this card's edit) · evidence: ADR-0090 D9 says unauthenticated principals hold the guest position, and the seeded anchor's own description says sets bound to it apply to them; but resolveAuthzContext returns before any binding expansion when there is no user id (resolve-authz-context.ts:415), and resolvePermissionSetsForContextUnmemoized resolves position NAMES only (security-plugin.ts:6316-6361) · Seam: spec:GUEST_POSITION / AUDIENCE_ANCHOR_POSITIONS (ADR-0090 D9) → runtime:core resolveAuthzContext (anonymous early return) + plugin-security resolvePermissionSetsForContextUnmemoized · it falls inside this card's consequence route (N6 A/C), so the seat files it as a sub-issue of #21079 (or as its Blocked-by: if ruled C) · dedupe words: guest anchor binding unauthenticated · guest position binding not resolved · ADR-0090 D9 guest bindings · anonymous principal sets anchor",
    "carrier: #21146 / PR #21151 · noted, not filed — turbo 2.11.5 wrote its agent-guidance block into AGENTS.md on every build in this worktree; restored each time, and the final tree is clean",
    "carrier: PR #21136 · noted, not filed — its new picker pins assume that a no-session visitor is admitted on a deployment with no guest set; serial constraint for this card's build (N4/N7)",
    "carrier: this card's build · noted, not filed — explain reports the object_crud layer denies for every zero-set class while enforcement admits; the direction closes that disagreement with no change to explain"
    ],
    "deviations": [
    "No empty-branch push: the agent definition's rule 1 makes a push the first act after branch creation, but this phase-0 order forbids any push. I followed the order and report the conflict here. The local branch and the worktree were removed at the end, so the build phase can create both fresh.",
    "Real-boot attempt 1 was refused by the dev-prerequisite check (8 packages without a build) after an empty port check (ss is absent in this container; switched to lsof). No server ran. Attempt 2 ran after a full build.",
    "canWriteObject is not on the registered service, so the probe asked it on the plugin instance it constructed."
    ]
    }


    Generated by Claude Code

  5. 10 remaining items

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

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

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

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

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

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

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

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