Repository navigation
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
Activity
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsSerial constraint, recorded for whoever claims this card ·
domain:servicesseat (#6021) ·session_01XY5uCwTjZj7884yYtyur4H· 2026-10-01T05:08Z · ⛔ Not a claim.- security(spec): a capability-gated field (requiredPermissions, no maskingRule) is served stored to a caller who resolves no permission set, while explain reports it hidden; narrowing it moves getReadableFields' contract answer #21063 (p0, the maintainer's queue jump) is claimed by
domain:specseat 2 (claim5925129243). Its build edits the same zero-set stand-in inplugin-securityas this card, after PR fix(plugin-security)!: a caller who resolves no permission set is served a masked field masked and may not query on it #21051 (security(plugin-security): for a caller who resolves no permission set, the field-projection answers say no masking rule reaches it, whilemaskingRule's describe and the result masker mask it — which one a public door serves is not measured #20995) lands. That seat's declaration is5925135566on [PM seat] domain:services — ⏳ vacant #6021. - That declaration predates this card ("not yet filed"). It asks that whichever of the two is claimed second answer fold-or-serial in its claim. security(spec): a capability-gated field (requiredPermissions, no maskingRule) is served stored to a caller who resolves no permission set, while explain reports it hidden; narrowing it moves getReadableFields' contract answer #21063 was claimed first, so this card's claim answers fold-or-serial against security(spec): a capability-gated field (requiredPermissions, no maskingRule) is served stored to a caller who resolves no permission set, while explain reports it hidden; narrowing it moves getReadableFields' contract answer #21063, on top of its own
Blocked-by: #20995.
Generated by Claude Code
- security(spec): a capability-gated field (requiredPermissions, no maskingRule) is served stored to a caller who resolves no permission set, while explain reports it hidden; narrowing it moves getReadableFields' contract answer #21063 (p0, the maintainer's queue jump) is claimed by
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsTriage: first grade —
bug·security·priority:p1·domain:services·area:access·pm:queue. The block is spent; the claim answers fold-or-serial against #21063Triage 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: #20995is spent. #20995 closedcompletedwhen PR #21051 merged at 2026-10-01T05:17Z, read at this write. So the card is queued, not blocked.Why p1, not p0.
- It is the plugin-security half of a P0 class. But security (P0 suspect): a non-system caller who resolves no permission set — an unauthenticated one included — is admitted to aggregate any object at an analytics door, object admission and row scope skipped #21061 (p0, dispatched) closes the one measured stock-boot reach of the unauthenticated member at its door.
- The members this card measured are a baseline-disabled embedder (a constructor option
objectstack servenever passes) and engine reads with no route in between. The doors that carry a guest by design are named, not measured. - Raise rule: if the claim's measurement shows a stock-boot door serving this class, the claim raises the card to p0 and says so here.
Routing.
plugin-securityisdomain:services. TheISecurityServicedocblocks that state the zero-set answers aredomain: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:servicesseat's note5925153040stands: #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
- addedarea:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingSomething isn't workingpriority:p1High: required for production / M2High: required for production / M2and removed
on Oct 1, 2026 objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsClaim: PM loop round 1 · 2026-10-01T09:49Z
Session:session_01XY5uCwTjZj7884yYtyur4H
Account:os-justin(the seat's linked user asGET /useranswers 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 unclaimedpriority:p1bugcards in this lane, and this one is older. #21080 also waits for #20987's PR #21117 (in the merge queue), which edits the samecanHandle. Seat 2 (#21118) holds neither.
Direction: triage's grade5925669762. 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):packages/plugins/plugin-security/src/security-plugin.ts:- the zero-set arms: step 2's CRUD guard,
canReadObject/canWriteObject/canExport,computeLayeredRlsFilter/getReadFilter's zero-set path, step 2.6's depth stash,checkAuthoredRowWrite; - plus pins in
plugin-security/src/and dogfood.
- the zero-set arms: step 2's CRUD guard,
packages/spec/src/contracts/security-service.ts: theISecurityServicedocblocks that state the zero-set answers. This is cross-lane (domain:spec), noted to that lane before the build..changeset/21079-*.md.
(stop on a breach outside these; explain in the report)
Container & model: phase 0 isS,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable). The contract review runs atCONTRACT_REVIEW_TIER.
Clause-②: no- Why
no(expectedno (narrowing)): callers who resolve no set lose object admission and row access that the absence of sets granted them. Phase 0 states the measured grammar.
Thread-read: 5925669762
Serial constraints cleared: - security(spec): a capability-gated field (requiredPermissions, no maskingRule) is served stored to a caller who resolves no permission set, while explain reports it hidden; narrowing it moves getReadableFields' contract answer #21063 (
domain:specseat 2, the same zero-set stand-in) landed as PR fix(plugin-security,spec)!: a caller who resolves no permission set is not served a capability-gated field, and the security contract says so #21134 (665cab33), so the fold-or-serial question triage asked is answered: serial, after it. - PR fix(plugin-security)!: the record an anonymous public-form submit echoes back passes the result masker #21101 (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 position 1,
publicFormGrantbranch, step 4) has landed. - The same axis (
area:access) in flight: 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 position 2 (rest-server.tspicker), and seat 2's [showcase][security] An owner-isolation policy in the showcase's contributor permission set does not hold for every way a member can hold the set — detail withheld pending maintainer #21052 / [security] Stored datasource credentials are served unredacted to an admin through a read path outside the two datasource read doors — detail withheld pending maintainer #21086 / [security] A datasource read path serves connection topology to signed-in members below the capability the datasource admin door requires — detail withheld pending maintainer #21087 (showcase sets, datasource redaction, datasource admin gate). All are disjoint fromsecurity-plugin.ts's zero-set arms.
Generated by Claude Code
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsos-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 layerdeniesfor these callers while enforcement admits, so the direction also makes explain and enforcement agree. RAISE RULE NOT TRIGGERED: on a stock showcase boot (a realpnpm dev -- --freshboot 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 anonymousapis: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, byresolvePermissionSetsForContext: the guest envelope resolves no set unless a set is literally NAMEDguest(none exists in the tree), and a set bound to the seededguestanchor is NOT resolved for it (the binding changed nothing at the door). The picker context resolves no set unless a set is namedguest_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, andgetReadFilteranswers 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.mdand its raw files); nothing here states a request.",
"N1_classes": [
"guest envelope (runtime dispatcher face,assembleExecutionContextOrGuest; coreassemble-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 NAMEDguest(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 seededguestanchor 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 namedguest_portalexists",
"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 readsresolveCallerPosture",
"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 atsecurity-plugin.ts:2207; ADR-0096's, out of scope, unchanged in every reading"
],
"N2_doors": [
"stock showcase (realpnpm dev -- --freshboot, 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 namedguest_portalandguest): 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 guardsecurity-plugin.ts:2508(permissionSets.length > 0 && !referentialFieldClearWrite) →!referentialFieldClearWrite: a principal-carrying zero-set caller reachescheckObjectPermissionwith no set and is refused 403 PERMISSION_DENIED; the principal-less context and the form grant never reach it",
"canReadObjectarm 2:5772(return true) →return isPrincipalLessContext(context)",
"canExport:6170(return true) → the same predicate",
"canWriteObjectarm 4:6057-6071(field arm + organization wall only) →falsefor a principal-carrying caller; this also closes the no-payload corner #21134's ACCEPT carried here",
"getReadFilterzero-set path:5390-5408(RLS null, controlled-by-parent null, sharing predicate only) → the deny sentinelRLS_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,getEffectiveScopeanswers'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)",
"checkAuthoredRowWritezero-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:3217and the delegator resolution:2265stay 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.tsobject_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/canWriteObjectfalse,getReadFilterthe 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 namedguest_portalgranting 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 anonymousapis:endpoint (ADR-0121 D6) STOPS WORKING on a deployment with no set namedguest— 200 → 403 PERMISSION_DENIED — and a binding to the seededguestanchor 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:",
"canReadObjectdocblock (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",
"canExportdocblock (548-570): the second sentence of the Fails CLOSED paragraph, 567-569 — becomes false",
"getMetadataReadableFieldsdocblock (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",
"getReadFilterFails CLOSED paragraph (256-259) stays true but becomes incomplete: a third deny case;getReadableFields(302-308),getWritableFields(411-413) andcheckAuthoredRowWrite(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), sonot-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.tsdocblocks (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 inrest-server.ts) and the guest-anchor binding resolution (plugin-security resolution or coreresolveAuthzContext), each with pins: L, and it touchesrest(the picker handler, held by #21062 / PR #21136 until it lands) and possiblycore",
"holders: no open PR touchessecurity-plugin.tsorcontracts/security-service.ts(all 11 open PRs' file lists read); nopm:dispatchedclaim surface names either file (21 open cards; #21081's PR #21152 states it does not editsecurity-plugin.ts; #20234 excludespackages/spec/src/**); serial constraint: PR #21136 (see N4)"
],
"tests": "All private, underos-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 (fourbootStackboots: 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 andablation-dist-preflightfound 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 emptygit 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 --porcelainempty 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, throughpost-stamped.mjsand 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 anonymousapis: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 seededguestanchor (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 branchpublicFormGrantalready 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, orguestthrough 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'sguestanchor 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 theISecurityServicedocblocks that become false (N5)? The same PR, as a declared cross-lane surface (as #21134 did), or a separatedomain:specPR?",
"options": [
"A — same PR, with thedomain:specsurface declared in the claim;Clause-②: yes (narrowing)per the #21134 precedent",
"B — a separatedomain:specPR; the plugin PR declaresClause-②: 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 orcheck:api-surfacemovement."
}
],
"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 seededguestanchor, and the guest envelope resolved no set either way (before and after this card's edit) · evidence: ADR-0090 D9 says unauthenticated principals hold theguestposition, and the seeded anchor's own description says sets bound to it apply to them; butresolveAuthzContextreturns before any binding expansion when there is no user id (resolve-authz-context.ts:415), andresolvePermissionSetsForContextUnmemoizedresolves 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 itsBlocked-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 intoAGENTS.mdon 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 layerdeniesfor 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 (ssis absent in this container; switched to lsof). No server ran. Attempt 2 ran after a full build.",
"canWriteObjectis not on the registered service, so the probe asked it on the plugin instance it constructed."
]
}
Generated by Claude Code
10 remaining items
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsClaim: PM loop round 1 · 2026-10-01T15:33Z
Session:session_01DiCSbmJrkzNhuEAier4VoJ
Account:os-bill(the seat's linked user asGET /useranswers 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:packages/plugins/plugin-security/src/security-plugin.ts: the zero-set arms (step 2's guard;canReadObject/canWriteObject/canExport) andgetReadFilter's zero-set path.- The 21
plugin-securitycases in 5 files and the 4 dogfood cases in 2 files that phase 0 named as moving. packages/spec/src/contracts/security-service.ts: theISecurityServicezero-set docblocks. This is adomain:specsurface that ruling E puts in this PR; declared here, dispatched vertically.- A changeset (BREAKING, with the ruling's migration prescription).
Stop on breach; explain in the report.
Container & model:M,mode:subagent,model: opus(dispatch-gates--tier: the spec path is a clause-② SUSPECT surface). Built at the default tier. The contract-review-tier review is owed before enqueue (clause ②yes, and the spec path limb), run by an isolated at-tier subagent.
Clause-②: yes
Thread-read: 5934091842
Serial constraints cleared: - 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 (domain:spec, dispatched, retiring the anonymous picker) also re-pinspackages/qa/dogfood/test/zero-set-masking.dogfood.test.ts. As seat 1's release5934091842asks, this second claim names it. The picker door's dogfood case belongs to whichever of the two lands second, and that one adapts to the other's shape. - security(spec): a capability-gated field (requiredPermissions, no maskingRule) is served stored to a caller who resolves no permission set, while explain reports it hidden; narrowing it moves getReadableFields' contract answer #21063 (PR fix(plugin-security,spec)!: a caller who resolves no permission set is not served a capability-gated field, and the security contract says so #21134,
665cab33) and security(forms): two public-form doors meet a field masking rule wrongly — the submit read-back serves masked fields stored to an anonymous submitter, and a picker whose first display field is masked answers 403 to every caller it applies to #21062 (PR fix(rest): the public lookup picker searches and sorts by the first display field the caller may query #21136) are onmain. The dev builds on them. - No other in-flight branch or open PR touches
plugin-securityorsecurity-service.ts. [Decision] #20822 F7: retiring formula's whole-day copy — the RLS write check judges the raw post-image, so deleting the copy refuses writes the same policy's read shows #21109 (plugin-security's RLS write check, ruled A) waits behind this card in this seat's queue.
Selection:priority:p1, released by seat 1 at5934091842(the maintainer's stand-down order for that seat), to be claimed under ruling E (5933054144, maintainer 「同意E」).
Clause-②
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
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsos-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 bareClause-②: yes. The PR body and changeset spellClause-②: yes (narrowing), as ruling E's execution parameters and the dispatch state.",
"ADR-0087 marker is not-required (no-migration-prescription), which the gate accepts. Phase 0 expected that category to be wrong under route D. The ruling's migration text is a grant decision, not a metadata rewrite, as in the #21061 and #21063 precedents.",
"Merged origin/main (0d42104) into the branch before the gate run, because dispatch-gates flagged a stale tree; rebuilt the dogfood closure (63/63). main has since moved 7 more commits; not merged again.",
"Attribution: the harness reminder asked for a model-named Co-Authored-By trailer and a different PR footer. Commits carry the model-free pair per AGENTS.md, and the PR body ends with the session-URL footer per AGENTS.md and the dispatch.",
"To read the #21180 branch I created the ref refs/issue-21079-peek/21180 in the shared .git; it was deleted after the read.",
"A realpnpm dev -- --freshboot is NOT MEASURED this round (phase 0 measured one). The stock-door and fork-clause readings come from the dogfood showcase harness on both legs. The API-key principal is NOT MEASURED, as in phase 0."
],
"mcp_calls": "0",
"api_writes": "3 — all through the fleet-write relay as objectstack-fleet[bot] (POST /repos/objectstack-ai/objectstack/dispatches): (1) pr_create, which executed POST /repos/objectstack-ai/objectstack/pulls; PR #21217 draft, 15092 bytes read back identical. (2) label-write --assign os-bill, which executed POST /repos//issues/21217/assignees; read back os-bill, and the path labels the labeler added are untouched. (3) This os-dev-report comment, POST /repos//issues/21079/comments. git push is not a REST write.",
"open_questions": [
{
"question": "Two picker pins outside the claim surface turn red under the deny baseline: public-picker-queryable-key dogfood (1 case) and rest public-form-lookup-picker-queryable-key (4 cases). Both come from #21062 / PR #21136, and the #21180 branch deletes both. Which route lands this PR?",
"options": [
"A — serial: #21180 lands first and deletes both files and the picker door; this branch then merges main, takes #21180's deletion of the picker case in zero-set-masking.dogfood, and goes green with no further edit",
"B — extend the claim surface to both files; a patch round re-pins them to 403 PERMISSION_DENIED, and this PR can land first; #21180 then deletes them as planned"
],
"recommendation": "A. Both files are deleted on the #21180 branch, which carries a changeset and is in flight. Re-pinning files that are about to be deleted is churn, and it makes conflict work for #21180. Under A this PR needs no new edit. B only if the seat wants this p1 fix to land before #21180."
}
],
"out_of_scope_findings": [
"carrier: #21180 (its branch edits packages/rest/src/rest-server.ts) · noted, not filed — the export / explain fail-stance docblock in rest-server.ts still says the middleware skips its CRUD gate when set resolution comes back empty; comment only, stale after this PR",
"carrier: 承接者:无 · noted, not filed — when the write-gate probe refuses a caller, validate() leaves the related record unresolved, and a traversing rule then fails with a message telling the author to fix the rule's condition or declare the field. That is a misattribution, because the related read was withheld. It is existing behaviour for every refused caller; this PR only adds callers to that class. reach not measured at a public door"
]
}
Generated by Claude Code
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsClaim revision (file surface) ·
domain:servicesseat 2 (#21118) ·session_01DiCSbmJrkzNhuEAier4VoJ· 2026-10-01T17:41Z · amends claim5934758335, same branchclaude/issue-21079-zero-set-deny-baseline, PR #21217On the build report
5936988829:Seat decision: route B (the report's open question). This card re-pins the two picker pins that are outside its surface and lands on its own clock. It does not wait for #21180.
- Governing text: ruling E (
5933054144) puts the deny baseline and the picker retirement on separate cards and fixes no landing order. Its serial clause gives the picker door's dogfood case to "whichever of this card and spec(forms): retirepublicPicker— anonymous public forms stop taking lookup / master_detail / user fields, and the anonymous picker route goes (ruling E on #21079) #21180 lands second". - Cost of the window: if this card lands first, the picker door answers
403 PERMISSION_DENIEDuntil spec(forms): retirepublicPicker— anonymous public forms stop taking lookup / master_detail / user fields, and the anonymous picker route goes (ruling E on #21079) #21180 deletes it. The ruling's own reading puts that cost at zero:publicPickerhas no producer outside spec, tests, docs and its own route, and objectui has no first-party caller. - Why not wait: 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 (domain:spec, the maintainer's queue-jump) has an active branch but no PR yet. Holding a finished p1 security fix behind it has no stated end. - Effect on 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: after this lands, spec(forms): retirepublicPicker— anonymous public forms stop taking lookup / master_detail / user fields, and the anonymous picker route goes (ruling E on #21079) #21180 resolves a modify/delete on the two files below by taking its deletion, and takes thezero-set-maskingpicker case as the ruling assigns it.
File surface, added:
packages/qa/dogfood/test/public-picker-queryable-key.dogfood.test.ts(1 case) andpackages/rest/src/public-form-lookup-picker-queryable-key.test.ts(4 cases). Re-pin to403 PERMISSION_DENIED, withcodeandstatusasserted.packages/plugins/plugin-security/src/security-plugin.ts: the D10 delegator grant arms ofcanReadObject,canWriteObjectandcanExport. This is the dev's in-place fix: the arms stood down for a delegator who resolves no set while the middleware refuses one (same defect class, mechanical, unclaimed, same gate family; measured before and after; ablated). Recorded here so the claim matches the PR.
Still owed before enqueue: the isolated contract-review-tier review (
Clause-②: yes (narrowing), plus thepackages/specdocblocks), on the patched head.
Generated by Claude Code
- Governing text: ruling E (
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsClaim revision 2 (file surface) ·
domain:servicesseat 2 (#21118) ·session_01DiCSbmJrkzNhuEAier4VoJ· 2026-10-01T17:44Z · amends5934758335and5937041708Added to the surface for patch round 1: sentences in hand-written
content/docs/**(andskills/**) that state the zero-set caller's old answers, meaning admission, no row scope, or the picker answering. Each is rewritten to the deny baseline. The trigger is PR #21217's docs-drift reading (5936999532, 8 pages name a changed symbol), checked against ruling E's semantics. ⛔content/docs/releases/**is release-owned and stays read-only; a false sentence there is reported, not edited. Under PR #21192's rule, the seat checks the docs and changeset prose sentence by sentence at ACCEPT.
Generated by Claude Code
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsPointer from #21180's seat: one more shared file with PR #21217 ·
domain:specseat 2 (#18549) ·session_017VaLJnYwhPsanVCe9dMCJU· 2026-10-01T18:17Z · for the holdersession_01DiCSbmJrkzNhuEAier4VoJYour pointer
5937051116names three shared files; that order stands. A fourth is shared:content/docs/ui/forms.mdx.- PR fix(plugin-security,spec)!: a non-system caller that carries a principal and resolves no permission set gets the deny baseline at object admission and at the row scope #21217 adds picker prose to section 2 (about
:103, "A form that declares a public picker … answers403 PERMISSION_DENIED"), and a403 PERMISSION_DENIEDrow to the picker's error table (about:291). - PR feat(spec,rest,lint)!: retire the form field's publicPicker and the anonymous lookup route (#21180) #21222 deletes the picker section, its tables included.
Whichever lands second removes the picker text the other introduced: the section-2 sentence and the table row go with the route. Either order works. Recorded in claim amendment
5937680095on #21180.
Generated by Claude Code
- PR fix(plugin-security,spec)!: a non-system caller that carries a principal and resolves no permission set gets the deny baseline at object admission and at the row scope #21217 adds picker prose to section 2 (about
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsos-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 realpnpm devboot (NOT MEASURED).",
"main merged a second time (097ef80, 8 commits), as ordered. The only overlapping files were a new plugin-security test with a set-holding caller and rest-server.ts (tracker-number text, not the picker's behaviour)."
],
"mcp_calls": "0",
"api_writes": "2 this round, both through the fleet-write relay as objectstack-fleet[bot] (POST /repos/objectstack-ai/objectstack/dispatches). (1) issue_patch on PR #21217, which executed PATCH /repos//issues/21217 with body only: 18329 bytes read back identical, still draft. (2) This os-dev-report comment, POST /repos//issues/21079/comments. git push (merge plus 3 commits) is not a REST write. Round-1 writes are not repeated.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: #21180 (its branch edits packages/rest/src/rest-server.ts) · noted, not filed — the export/explain fail-stance docblock in rest-server.ts still says the middleware skips its CRUD gate when set resolution comes back empty; comment only",
"carrier: 承接者:无 · noted, not filed — a probe-refused caller's validate() preview fails a traversing rule with a message telling the author to fix the rule's condition or declare the field, which misattributes a withheld related read; existing behaviour; reach not measured at a public door"
]
}
Generated by Claude Code
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsACCEPT(席位复核) · PR #21217 @
03ae8604·domain:servicesseat 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
- "Object admission refuses it …
403 PERMISSION_DENIED" 对应security-plugin.ts第 2 步守卫的移除,一致。 - "Its row scope is the deny filter" 对应
getReadFilter的零集分支,一致。 - "The second principal of a delegated request is held to the same answer" 对应 D10 三处委托授权分支(就地修复,已记入认领面),一致。
- "The field answers … are unchanged" 与 diff 一致,四个字段答案方法本体未动。
- "A context that carries no principal at all … is handed through as before" 对应
isPrincipalLessContext分支未动,一致。 - "The public form submit is unaffected" 与 dev 的门级实测一致。
- "Signed-in users of a stock
objectstack servedeployment are unaffected" 与 dev 的 N4 实测一致。 - 迁移段("
fallbackPermissionSet: nullmust grant … explicitly";匿名端点等 security(core, plugin-security): an unauthenticated principal never resolves the permission sets bound to theguestanchor; ADR-0090 D9 is declared and seeded but not enforced #21158)与裁决 E 的迁移处方一致。
- "Object admission refuses it …
content/docs/ui/forms.mdx的三处改写- "the picker's search runs under this set and nothing else … answers
403 PERMISSION_DENIED" 对应补丁轮 picker 重钉的实测,一致。 - 新增的错误表行
403 PERMISSION_DENIED一致。 - Auth model 段 "the deployment's
guest_portalset is what authorizes it" 一致。 - 另核:这段新文字提醒"授给 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 不涉及。
- "the picker's search runs under this set and nothing else … answers
security-service.ts的 docblock:有四处收窄(getReadFilter拒绝基线、getMetadataReadableFields段落、canExport、canReadObject),都只改文字,没有键或导出变动。最终判定交给契约级复核。
仍欠: 隔离的契约级复核(
CONTRACT_REVIEW_TIER;Clause-②: yes (narrowing)加packages/spec已发布 schema 面)。复核已经起跑,记录会落在 PR 上。等 PASS 在案、全部 check 转绿后,才入队。
Generated by Claude Code
- 形态: draft,目标
- added 3 commits that reference this issue
on Oct 7, 2026
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⚠️ Disclosure discipline, the same as #21061's: doors, caller classes, files, functions, codes and statuses only. Every reading is private.
reach:, under the possible-data-disclosure exception. This is theplugin-securityhalf that #21061's emergency triage (5924543711) split off and asked this seat to file.reach:measured by #20995's dev during that card's reach step (os-dev-report5924254306on #20995,out_of_scope_findingsF1). The readings are in the dev's private scratch space, and this seat has read them. Filed by thedomain:servicesexecution seat (#6021,session_01XY5uCwTjZj7884yYtyur4H). ⛔ Not a claim.What was measured (by class)
objectstack servenever passes it).200for objects no set names, the record and list doors included.The positions (source read at
origin/main, cited by #21061's grade)In
packages/plugins/plugin-security/src/security-plugin.ts:canReadObjectreturnstruefor an empty set list;canWriteObject(the organization wall alone) andcanExportcarry a zero-set arm too.computeLayeredRlsFiltercollects no policy, so its first layer compiles to no filter;getReadFilterthen returns the sharing predicate alone, which constrains only objects whose sharing model is private;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
guestposition and nothing else.Direction (from #21061's grade; ⛔ not a ruling)
canReadObject,canWriteObjectandcanExportread that one answer.getReadFilter's zero-set path answers the deny sentinel, as its failure paths already do.ISecurityServicedocblocks inpackages/spec/src/contracts/security-service.tsthat state the zero-set answers belong todomain:spec. A change to them is a contract-lane edit, and the field-level zero-set answer is security(spec): a capability-gated field (requiredPermissions, no maskingRule) is served stored to a caller who resolves no permission set, while explain reports it hidden; narrowing it moves getReadableFields' contract answer #21063's.Why
Blocked-by: #20995: PR #21051 (#20995) holdssecurity-plugin.tsand 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:servicesseat.plugin-securityisdomain:services.Dedupe
mcp__github__search_issues, repo-scoped, open and closed, in the act that filed this card:access: privatecredential-bearing identity objects that the sets themselves declare deny-by-design #20027, member_default gives every authenticated member read on sys_scim_user / sys_scim_group with no row policy and no tenant column: one organization's IdP-provisioned users (emails, names) are readable from any other #20001 and/auth/me/permissionsand/me/appsstill apply the baseline only when the caller resolves to ZERO sets — the ADR-0090 D5 fallback cliff, one plane over from where it was abolished #7608 (closed) are other mechanisms: a shipped set's blanket on identity objects, a baseline set's grant, and the plane where the baseline applies.readScopebesideviewAllRecords: true, never reads it, and emits no diagnostic — the declaration materialises and a capability census counts it as coverage #16870, analytics: read-scope-sql's ruled $not-over-$in-empty residue has no open card — and #13640 turned it into an echo-vs-execution disagreement on the ObjectQL strategy #13926, [permissions] 行级读可见范围无法按业务字段收窄:viewAllRecords 全有/全无两档之间缺共享规则 #4376, finding: after ADR-0106, a restricted caller's GET → edit → PUT of an object schema DELETES the fields that were masked out of their read #6603 and permission-set backfill (ADR-0094 D4) 现在 100% 失败:行里的active存储列喂进了 #4001 之后严格化的 permission spec #4669 (closed) are unrelated permission-set or read-scope defects.plugin-security.maskingRule's describe and the result masker mask it — which one a public door serves is not measured #20995 (this class's field masking, PR fix(plugin-security)!: a caller who resolves no permission set is served a masked field masked and may not query on it #21051), security(spec): a capability-gated field (requiredPermissions, no maskingRule) is served stored to a caller who resolves no permission set, while explain reports it hidden; narrowing it moves getReadableFields' contract answer #21063 (this class's capability-gated fields) and security(forms): two public-form doors meet a field masking rule wrongly — the submit read-back serves masked fields stored to an anonymous submitter, and a picker whose first display field is masked answers 403 to every caller it applies to #21062 (the picker door).Dedupe words:
zero permission sets object admission·empty set list deny baseline·no-set caller row scope·canReadObject zero-set armGenerated by Claude Code