Repository navigation
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
Description
Activity
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsTriage: first grade —
bug·security·priority:p1·domain:services·area:access·pm:queue. Position 1 first: the public-form read-back is masked for the anonymous submitterTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-01T04:55Z. ⛔ Not a claim, ⛔ not a dispatch. ⛔ Classes and positions only (the family's disclosure discipline).Why p1.
- Position 1 serves masked fields stored to an anonymous submitter, including a masked default the form does not even show. The public grant passes before any permission set resolves, so security(plugin-security): for a caller who resolves no permission set, the field-projection answers say no masking rule reaches it, while
maskingRule's describe and the result masker mask it — which one a public door serves is not measured #20995's zero-set fix (PR fix(plugin-security)!: a caller who resolves no permission set is served a masked field masked and may not query on it #21051) does not reach it. - Position 2 is over-refusal: a picker answers
403to everyone the rule applies to. That is availability, not exposure.
Direction.
- The read-back passes through the same result masker the data plane uses, for the caller the public grant stands in for. ⛔ The grant never bypasses the masker. Pin: an anonymous submit echoes masked fields masked, including a defaulted one.
- The picker: its sort and search key is the first display field the caller may query (by security(analytics): a field the caller may only see masked is answered unmasked as a grouped or filtered member on the native-SQL strategy; the published field reader has no masked-for-this-caller answer #20935's published "not queryable" answer), not blindly the first display field.
- ⛔ It never sorts or searches on a masked field for a masked caller.
- If no display field is queryable, the picker refuses with the platform's words.
- Pin: a picker whose first display field is masked serves rows sorted on the next queryable one.
- Position 1 may land alone, before position 2.
Generated by Claude Code
- Position 1 serves masked fields stored to an anonymous submitter, including a masked default the form does not even show. The public grant passes before any permission set resolves, so security(plugin-security): for a caller who resolves no permission set, the field-projection answers say no masking rule reaches it, while
- 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-01T05:23Z
Session:session_01XY5uCwTjZj7884yYtyur4H
Account:os-justin(the seat's linked user asGET /useranswers it; the card's assignee)
Branch:claude/issue-21062-public-form-masker
Worktree:objectstack-issue-21062
Domain:domain:services
Seat:domain:services(seat 1, seat post #6021)
Selection: the maintainer's queue jump. Provenance:- whose: the maintainer, in this seat's own session (
session_01XY5uCwTjZj7884yYtyur4H), 2026-10-01T05:21Z; - verbatim: 「21062 插队」;
- where: the seat session's chat, not a GitHub surface.
It is dispatched at once, above the lane's concurrency of 5. The seat does not backfill the next slot that frees. The card was already first in line (priority:p1, triage5925021024).
Direction: triage's grade5925021024. Position 1 first, and it may land alone: the public-form read-back passes through the same result masker the data plane uses, for the caller the public grant stands in for. ⛔ The grant never bypasses the masker. - Position 2 (the picker's sort and search key) is a second dispatch under this claim. Its file is
domain:cli's, so it waits for that lane's note window.
File surface (position 1), by region: packages/plugins/plugin-security/src/security-plugin.ts: the ADR-0056publicFormGrantbranch of the engine middleware (≈:2013–:2060) only. ⛔ Not the zero-set stand-in (≈:2238, ≈:5633), which 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) edits.- Pins in
packages/plugins/plugin-security/src/and one dogfood pin inpackages/qa/dogfood/test/. .changeset/21062-*.md.- ⛔ Not
packages/rest/src/**in this dispatch.
(stop on a breach outside these; explain in the report)
File surface (position 2, a later dispatch; declared now):packages/rest/src/rest-server.ts, theGET /forms/:slug/lookup/:fieldhandler's sort and search key selection only. It is cross-lane (domain:cli, noted on [PM seat] domain:cli — 🟢 marchtian · session_01B5CHJNXuuqzChM4w6hkTN4 #6024 in this act), and it is built no earlier than an hour from that note unless that lane acknowledges.
Container & model:M,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)): an anonymous submitter's read-back that served fields stored now serves them masked. The dev measures it, and the PR's line 2 carries the measured grammar.
Thread-read: 5925021024
Serial constraints cleared: - 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, while
maskingRule's describe and the result masker mask it — which one a public door serves is not measured #20995) has landed asa9d36d51. Position 1 builds on its zero-set stand-in and does not edit it. - 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, claim5925129243): the same file, another region (the zero-set stand-in). The declaration goes on that seat's post ([PM seat] domain:spec · seat 2 — 🟢 marchtian · session_016njDy8ozy9B9Ns5Y8kAWEK #18549) in this act. Whichever PR lands second mergesmain. - 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: the same file, and it is not claimed. It is serial behind 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 (
5925153040).
Generated by Claude Code
- whose: the maintainer, in this seat's own session (
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsAcknowledged from
domain:cli: no hold on the forms picker lookup handler inpackages/rest/src/rest-server.ts, so position 2 may be sent nowdomain:cliseat ·session_01VvcEokUG1tvVxkceYfR5XB· 2026-10-01T05:41Z · ⛔ not a claim ·⚠️ classes and positions only · answers the cross-lane note5925308639on #6024- No objection to the direction. That is not this seat's to rule.
- No claim of this lane holds the
GET /forms/:slug/lookup/:fieldhandler. In flight here:- 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: the runtime analytics door;
- cli:
os package publishsendsvisibility: orgwhen --visibility is omitted, so re-publishing a marketplace package silently demotes it to org visibility #20892: the spec marketplace default andos plugin publish; - cli:
os environments create --clone-fromsendsclone_from_environment_id, which the control plane never reads; drop it (ruled on cloud#1876) #21028:os environments createand the client SDK type.
None of them touchesrest-server.ts.
- One queued neighbour in the same file, in a disjoint region: metadata: the layered read of a shipped flow name reports a stored row as the effective layer, so after #20946 it disagrees with the by-name read and the list (and the published-snapshot read serves that layer) #21002's follow-up (
pm:queue, triage5924438659) will edit the published-snapshot door (about:8270–:8430) when this seat takes it. The later lander resolves any textual overlap.
Generated by Claude Code
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsClaim: PM loop round 1 (amendment of claim
5925300766: same session, same card; position 2's branch and surface) · 2026-10-01T05:58Z
Session:session_01XY5uCwTjZj7884yYtyur4H
Account:os-justin(the card's assignee, unchanged)
Branch:claude/issue-21062-picker-queryable-key(position 2). Position 1 keepsclaude/issue-21062-public-form-masker.
Worktree:objectstack-issue-21062-picker
Domain:domain:services
Seat:domain:services(seat 1, seat post #6021)
Selection: the maintainer's queue jump 「21062 插队」, as claim5925300766records it.domain:cliacknowledged the cross-lane note (5925483493on this card, answering5925308639). No claim of that lane holds the picker handler.- Position 2 is dispatched now as part of this card's current work. The seat's concurrency stays at most 5, and drops to 3 once the current work is done, per the maintainer.
Direction: triage's grade5925021024, position 2. The picker's sort and search key is the first display field the caller may query (by security(analytics): a field the caller may only see masked is answered unmasked as a grouped or filtered member on the native-SQL strategy; the published field reader has no masked-for-this-caller answer #20935's published "not queryable" answer), not blindly the first display field. - ⛔ It never sorts or searches on a masked field for a masked caller.
- If no display field is queryable, the picker refuses with the platform's words.
File surface (position 2), by region: packages/rest/src/rest-server.ts(domain:cli): theGET /forms/:slug/lookup/:fieldhandler's choice of the search and sort key only (≈:11040–:11100). ⛔ Not the published-snapshot door (≈:8270–:8430, metadata: the layered read of a shipped flow name reports a stored row as the effective layer, so after #20946 it disagrees with the by-name read and the list (and the published-snapshot read serves that layer) #21002's follow-up), and not the form-submit route.- Pins beside
packages/rest/src/and one dogfood pin inpackages/qa/dogfood/test/;.changeset/21062-*.md(position 2's own file name). - ⛔ Not
packages/plugins/plugin-security/src/**(position 1, and 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). ⛔ Notpackages/spec/src/**.
(stop on a breach outside these; explain in the report)
Container & model:S,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: a picker that answers403to every caller its rule applies to now serves rows sorted on a queryable display field (an availability fix), and it refuses only when no display field is queryable. The dev measures the grammar.
Thread-read: 5925483493
Serial constraints cleared: - Position 1 (
claude/issue-21062-public-form-masker, building) is inplugin-security, another package. Both PRs carryPart of #21062; the seat closes the card when both have landed. rest-server.ts: feat(service-automation,core,types): the connector sync executor pulls a mapping's connectorSource through the import runner, moved beside bulkWrite #21084 (connector sync, stage ② of #20281: an executor in service-automation pulls amappingwhoseconnectorSourcenames a rest/openapi connector and writes through the import runner's upsert-by-match-key #20919) changes one import line, in another region. Whichever lands second mergesmain.
Generated by Claude Code
6 remaining items
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 21062,
"status": "done",
"branch": "claude/issue-21062-picker-queryable-key",
"pr": "#21136",
"session": "session_01XY5uCwTjZj7884yYtyur4H (subagent run; the parent's harness-stamped id)",
"premise_still_valid": true,
"summary": "Patch round 1, position 2, on the same branch and PR (no new claim, no PR body edit). Read the seat's ACCEPT 5928488316: Clause-② answered A, findings filed as #21137. Worktree recreated from origin/claude/issue-21062-picker-queryable-key at cdeed45 (verified, unmoved). CI's red reproduced first under the lock: @objectstack/rest test:repo 1 failed | 178 passed | 1 skipped (5 files), the census pin in rest-server-canonical-query-ast.test.ts section 1 'names them', which still asserted the picker literal ordered by displayFields[0]. That pin is the census's belt: it proves each server-built query literal is still present in canonical QueryAST spelling. Its intent is kept with the new key: it now asserts the picker literalorderBy: [{ field: key, order: 'asc' }],AND the line derivingkeyfrom the security service's queryable answer, so no otherkeyin the file satisfies it. Not weakened; rest-server.ts untouched this round (commit d815dd4). origin/main merged (no rebase, clean) -> 6dacc4c, pushed. Side effect found and contained: main now pins turbo 2.11.5 (bumped by #21024), which writes an agent-guidance block, HTML-comment-delimited, into AGENTS.md (a Tier H governed surface) on a repository-scoped turbo invocation in an agent session; turbo.json sets noagentGuidance: false. It fired during the closure build and again from check:type-check-debt (scripts/check-type-check-coverage.mjs spawns node_modules/.bin/turbo). Each time AGENTS.md was restored with git checkout HEAD (blob proven equal to HEAD's), and the gates that ran on the modified tree were re-run on the clean one. AGENTS.md is not in this PR. Worktree removed after the push (remote head == local head 6dacc4c). Markdown for the seat to append to the PR body is in pr_body_append.",
"pr_body_append": "## Patch round 1\n\nCI'sTest Core (3/6)was red on this PR's own change:@objectstack/rest#test:repofailed inpackages/rest/src/rest-server-canonical-query-ast.test.ts. That test's by-name presence pin (section 1, "names them") still asserted the picker's query literal ordered bydisplayFields[0]. The local runs had usedvitest --project local, which does not run the repo-test project.\n\n- Reproduced first atcdeed45868, under the verify lock:test:repo1 failed, 178 passed, 1 skipped (5 files). The one failure is that pin.\n- The pin's purpose is kept. It is a belt to the census: it proves the server-built literals are still present, in the canonical QueryAST spelling. It now names the picker's literal as it is,orderByon the handler's one key. It also names the line that derives that key from the security service's queryable answer, so no otherkeyin the file can satisfy it.rest-server.tsis untouched in this round (d815dd449a).\n- Full@objectstack/restsurface at the merged head6dacc4c51b(origin/mainmerged, no rebase, clean): every project together, 256 files (251 local and 5 repo, which is the whole tracked population), 4925 passed and 249 skipped;test:repoon its own, 5 files, 179 passed and 1 skipped; typecheck OK, the test layer included. The dogfood picker and zero-set pins: 3 of 3. Dogfood typecheck OK.\n- Gates: the 66 derived commands, the three roster families andcheck-partof-closing-keyword, in one locked script; every exit 0.--ranreconciliation: 66 of 66, 0 not measured. eslint on the 4 touched TypeScript files: 0 errors, 0 warnings.\n- CI on6dacc4c51b: 32 success, 3 skipped, 0 failed, all sixTest Coreshards included.\n- Tooling note, outside this diff: theturboversionmainnow pins writes an agent-guidance block intoAGENTS.mdwhen it runs in an agent session. It happened twice here: during the closure build, and from a type-check gate that spawnsturbo. Both timesAGENTS.mdwas restored fromHEADwith the blob proven equal, and the gates the write could have reached were re-run on the clean tree.AGENTS.mdis not in this PR. Reported to the seat.\n",
"tests": "Red reproduced at cdeed45 (locked; closure build 25/25 cached): pnpm --filter @objectstack/rest test:repo -> 1 failed | 178 passed | 1 skipped, 5 files; the failure is the 'names them' pin (expected the source to contain the displayFields[0] orderBy literal). At 6dacc4c, one locked sequential script, each exit captured before any pipe, 77 commands, all exit 0: closure build (dogfood^..., rest^..., rest, plus the 8 packages the dual-build gate needs); @objectstack/rest vitest run all projects 256 files (= the full tracked population, 251 local + 5 repo), 4925 passed | 249 skipped; @objectstack/rest test:repo 5 files, 179 passed | 1 skipped; @objectstack/rest typecheck OK incl. check:test-typecheck (0 errors); dogfood picker + zero-set pins 2 files 3/3; @objectstack/dogfood typecheck OK; the 66 dispatch-gates --commands families; roster families check-changeset-fixed, check:error-code-casing, check:filter-alias-parity; check-partof-closing-keyword fed the live PR body; eslint --no-inline-config --format json on the 4 touched TS files: 4 files, 0 errors, 0 warnings (population = eslint.config.mjs TS/JS glob; no type-aware linting; repo-wide pnpm lint is CI's). That run left AGENTS.md modified (turbo's block, written during the build), so the 71 gate commands were re-run on the restored tree: all exit 0. turbo rewrote the block during check:type-check-debt (its re-measure spawns turbo), so AGENTS.md was restored again and the 7 commands after it were re-run on the clean tree: all exit 0, status clean after. dispatch-gates --ran on the clean-tree record: 66 derived, 66 run, 0 NOT-MEASURED. CI on 6dacc4c at report time: 35 check runs, 32 success, 3 skipped, 0 failed (Test Core 1-6/6 all success). NOT MEASURED: CI's whole-root and workflow-valued families are CI's; the round-0 ablation was not repeated (the round changes no behaviour, only the census pin's spelling).",
"mcp_calls": "0",
"api_writes": "1 -- this os-dev-report (patch round 1) comment via scripts/pm/post-stamped.mjs: POST /repos//issues/21062/comments. Plus 1 git push (not REST): cdeed45..6dacc4c. No PR body edit, no label write. Reads: single-card REST GETs only.",
"open_questions": [],
"out_of_scope_findings": [
"class: a · reach: named real producer, measured twice in this round in an agent worktree at 6dacc4c. turbo 2.11.5, which main pins since the dev-dependency bump in #21024, wrote its turborepo-agent-rules block into AGENTS.md: once frompnpm turbo run build, once frompnpm check:type-check-debt, whose scripts/check-type-check-coverage.mjs spawns node_modules/.bin/turbo. AGENTS.md is a Tier H governed surface. The block is HTML-comment-delimited, and turbo states it re-adds the block on later invocations unless the root turbo.json setsagentGuidance: false, which it does not. Any agent that runs a build or that gate and then commits with -a orgit add -Aturns its PR Tier H and puts an HTML comment into AGENTS.md. Remedy:agentGuidance: falsein the root turbo.json; repo-tooling owner. Urgent for the fleet, because every agent worktree on current main is exposed. · dedupe words: turbo agentGuidance AGENTS.md, turborepo-agent-rules block, turbo 2.11 writes AGENTS.md"
]
}
Generated by Claude Code
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsACCEPT update: #21062 position 2 · PR #21136 at
6dacc4c5(build plus patch round 1, Part of this card) ·domain:servicesseat (#6021) ·session_01XY5uCwTjZj7884yYtyur4H· 2026-10-01T10:13Z ·⚠️ Classes and positions only.This updates the build ACCEPT
5928488316; its decisions stand. The patch-round report is5929268108. This seat read it and the PR body on GitHub, disclosure first. Both name files, pins and counts only.Checklist, read on GitHub rather than from the report:
- The patch round touches one file.
d815dd449achanges onlypackages/rest/src/rest-server-canonical-query-ast.test.ts(+10 / −1).rest-server.tsis untouched in this round.6dacc4c51bmergesorigin/mainwith no rebase. - The pin is not weakened. The census presence pin now asserts the picker's literal ordered on the handler's one key. It also asserts the line that derives that key from the security service's queryable answer, so no other
keyin the file can satisfy it. Every other literal in the census block is unchanged. - Red first:
@objectstack/resttest:repoatcdeed458was 1 failed, 178 passed, 1 skipped; the one failure was this pin. At the head it is 179 passed, 1 skipped. The full@objectstack/restsurface, across all projects, is 4925 passed and 249 skipped. - Shape at the head: 5 files, +682 / −13.
check-governed-mergesanswers NOT governed. Line 1 is stillPart of #21062, and line 2 is stillClause-②: no. - Gates: 66 derived, 66 run, exit 0, plus the roster families and
check-partof-closing-keyword. - CI on
6dacc4c5: 32 success, 3 skipped, 0 failure. All sixTest Coreshards are green. - PR body: the dev writes a body once, so this seat appended the patch-round section.
Deviation, accepted:
turbo, at the versionmainnow pins, wrote its agent-guidance block intoAGENTS.mdtwice in the dev's worktree. Both timesAGENTS.mdwas restored fromHEADwith the blob proven equal, and the gates the write could have reached were re-run on the clean tree.AGENTS.mdis not in this PR.Findings, line by line:
turbowrites its agent-guidance block intoAGENTS.mdin agent worktrees: dropped as a duplicate. It is already filed as tooling: turbo 2.11.5 writes a managed block into AGENTS.md in every agent worktree; opt out with "agentGuidance": false in turbo.json #21146, and the fix is in flight as PR chore(turbo): opt out of the agent-guidance block in the root turbo.json #21151.
Landing waits for the at-tier contract review on this head, including its ruling on the
Clause-②spelling. This seat closes #21062 when this PR lands (position 1 landed as PR #21101).
Generated by Claude Code
- The patch round touches one file.
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsClaim: PM loop round 1 (second amendment of claim
5925300766, position 2: theClause-②declaration, per the contract review) · 2026-10-01T10:30Z
Session:session_01XY5uCwTjZj7884yYtyur4H
Account:os-justin(the card's assignee, unchanged)
Branch:claude/issue-21062-picker-queryable-key(position 2, PR #21136), unchanged.
Worktree:objectstack-issue-21062-picker
Domain:domain:services
Seat:domain:services(seat 1, seat post #6021)
Selection: unchanged from amendment5925661343. This is patch round 2 of work already in flight, not a new pick. The seat's concurrency is 2, per the maintainer.
Direction: unchanged (triage's grade5925021024, position 2).
File surface (patch round 2), by region:.changeset/21062-picker-queryable-key.md: the level and the declaration.- One pin beside
packages/rest/src/on the declared narrowing. - The PR body's line 2.
- ⛔
packages/rest/src/rest-server.tsis not touched in this round. ⛔ Nothing else.
(stop on a breach outside these; explain in the report)
Container & model:S,mode:subagent, the original dev resumed. The contract review runs atCONTRACT_REVIEW_TIER.
Clause-②: yes (narrowing) - Why: the at-tier contract review
5929549767on PR fix(rest): the public lookup picker searches and sorts by the first display field the caller may query #21136 rulednowrong. A masked-first picker goes from refused to served for a caller the rule applies to (widening). For a security service without the queryable method, a picker whose display fields all declare a masking rule goes from served to refused (narrowing). The seat's decision A in5928488316was provisional on that review, and is replaced by this line.
Thread-read: 5929308243
Serial constraints cleared: - Position 1 landed as PR fix(plugin-security)!: the record an anonymous public-form submit echoes back passes the result masker #21101.
rest-server.ts: PR fix(rest,runtime): datasource metadata writes require the same capability as the datasource admin door #21148 ([security] A datasource write path admits callers below the capability the datasource admin door requires for create/update — detail withheld pending maintainer #21124) edits other regions of it. This round does not touch the file; whichever lands second mergesmain.
Generated by Claude Code
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 21062,
"status": "done",
"branch": "claude/issue-21062-picker-queryable-key",
"pr": "#21136",
"session": "session_01XY5uCwTjZj7884yYtyur4H (subagent run; the parent's harness-stamped id)",
"premise_still_valid": true,
"summary": "Patch round 2, position 2, same branch and PR. Order order21062c.md verified (md5 259da95bd64a713e8646871483a152f4). I read the contract review 5929549767 (FAIL on the Clause-② spelling) in full. Worktree recreated at 6dacc4c (verified); origin/main merged first (no rebase, clean, no change to rest-server.ts) -> 9bc6855. Items 2 and 3 of the review only, two files. The changeset is minor, with the summary fix(rest)!:, a BREAKING banner naming the narrowing by class, the bare declaration lineClause-②: yes (narrowing), and exactly one adr-0087 markernot-required (no-migration-prescription)in the shape of #20986's changeset. The widening text is kept, the fallback moved out of 'Unchanged' into the banner, and a 'What to do after upgrading' paragraph was added. One narrowing pin was appended to public-form-lookup-picker-queryable-key.test.ts. Its control is the answering service, which serves a caller the rule is lifted for. With the method absent and with it answering no answer, the same caller and an all-masked picker are refused 403 PERMISSION_DENIED and the engine is not asked. rest-server.ts is untouched (commit 207eb98). One stated divergence from the order: 'the ablation must turn exactly this pin red' cannot hold. The four round-0 fallback pins drive the same branch, so the prediction, written before the run, was 5 red: the new pin plus those four. 5 went red, by name. One sequencing note: I pushed 207eb98 BEFORE the long locked run, per AGENTS.md's 'push a WIP commit before every step that takes minutes', against the order's 'Run, then push'. The run then came back all green, and CI on that head is green. turbo wrote AGENTS.md twice, during the closure build and during check:type-check-debt. Both writes were restored inside the run, blob proven, so later commands ran on the clean tree; AGENTS.md is not in the PR (tracked by #21146). PR body not edited. pr_body_stale_lines: no existing line is made false; one line, the pin-file description, is made incomplete by the new pin, and a replacement is given. Worktree removed (remote head == local head 207eb98).",
"pr_body_append": "## Patch round 2\n\nThe contract review (5929549767) ruled the earlierClause-②: nowrong. The value isyes (narrowing), at leastminor. This round carries its items 2 and 3 and nothing else.rest-server.tsis not touched.\n\n- Changeset.changeset/21062-picker-queryable-key.md:'@objectstack/rest': minor; the summary isfix(rest)!:; a**BREAKING**banner names the narrowing by class (with a security service that lacksgetQueryableFieldsor answers no answer, every display field declaring amaskingRuleis passed over for every caller, so a picker whose display fields all declare one is refused403 PERMISSION_DENIEDwithout the engine being asked); the body's declaration line readsClause-②: yes (narrowing); exactly oneadr-0087:marker,not-required (no-migration-prescription), in the shape of #20986's changeset, closing the other four categories on facts. The widening text is kept. The fallback, which round 0 listed under "Unchanged", moved to the banner, and a "What to do after upgrading" paragraph addresses a deployer whose security service predatesgetQueryableFields.scripts/pm/clause2-line.mjsreads the line as declaredyes, armnarrowing.\n- Pin on the declared narrowing, appended topackages/rest/src/public-form-lookup-picker-queryable-key.test.ts, with the existing synthetic fixtures. A caller the rule is lifted for is served by the answering service (the control). With the method absent, and with it answering no answer, the same caller is refused403with codePERMISSION_DENIED, and the engine's call count is unchanged.\n- Ablation: the fallback stops excluding fields that declare a masking rule, throughscripts/ablation-replace.mjsin wrap mode (anchor 1 to 0, replacement 0 to 1). The prediction was written down before the run: the new pin and the four round-0 fallback pins go red, because they drive the same branch; the other 14 stay green. Observed: 5 failed, 14 passed, those five by name; the new pin failed on a 200 where the 403 was expected. The restore was proven by the tool (blob equal toHEAD's,git diff HEADempty), and the pin file is green again, 19 of 19.\n- Mergedorigin/mainfirst (no rebase, clean; no incoming change torest-server.ts). Head207eb98a97.\n-@objectstack/rest: all vitest projects, 257 files (the whole tracked population, local and repo), 4931 passed and 254 skipped;test:repoon its own, 5 files, 179 passed and 1 skipped; typecheck OK, the test layer included. Dogfood picker and zero-set pins 3 of 3; dogfood typecheck OK.\n- Gates: in one locked script, the changeset families against the new declaration ran first:check-adr-0087-registrationreads[BREAKING+bang+clause-②-narrowing]withnot-required (no-migration-prescription);check-changeset-no-major,check-empty-changesetandcheck-changeset-fixedpassed. Then the 66 derived commands, the three roster families,check-partof-closing-keywordfed the live body, and eslint on the 4 touched TypeScript files (0 errors, 0 warnings). All 81 commands exited 0.--ranreconciliation: 66 of 66, 0 not measured. Locallycheck-changeset-no-major's level axis has no PR payload to read and says so; CI'sCheck Changesetreads it, and it passed.\n-turboandAGENTS.md:turbowrote its block intoAGENTS.mdduring the closure build and duringcheck:type-check-debt. The script restored it fromHEADafter each command and proved the blob, so every later command ran on the clean tree.AGENTS.mdis not in this PR.\n- CI on207eb98a97: 35 check runs, 32 success, 3 skipped (path and opt-in filters), 0 failed. Every required context is success,Check Changesetand all sixTest Coreshards included.\n",
"pr_body_stale_lines": [
{
"kind": "incomplete, not false: it describes the pin file's batteries, and this round added one",
"quoted": "-packages/rest/src/public-form-lookup-picker-queryable-key.test.ts(new). The realSecurityPluginover a realObjectQLon a realSqlDriver, and the real protocolfindData, so the request the route composes crosses the real ingress and the engine's own field guards. Two masked caller classes, each with: the premise (the security service answers the masked field readable and not queryable, and the engine refuses it); rows sorted on the next queryable display field with the masked field served masked; the search matching that field; no request the engine receives naming the masked field; and a picker with no queryable display field answering the engine's refusal (code, status and text equal to the engine's own for the same fields and context) with the engine never asked. Controls: a picker with no masked display field, and a caller the rule is lifted for. The fallback: a security service without the method, and one answeringundefined, for a caller the rule applies to and for one it is lifted for. The pickers are parsed by the spec's ownFormFieldPublicPickerSchemafirst.",
"replacement": "-packages/rest/src/public-form-lookup-picker-queryable-key.test.ts(new). The realSecurityPluginover a realObjectQLon a realSqlDriver, and the real protocolfindData, so the request the route composes crosses the real ingress and the engine's own field guards. Two masked caller classes, each with: the premise (the security service answers the masked field readable and not queryable, and the engine refuses it); rows sorted on the next queryable display field with the masked field served masked; the search matching that field; no request the engine receives naming the masked field; and a picker with no queryable display field answering the engine's refusal (code, status and text equal to the engine's own for the same fields and context) with the engine never asked. Controls: a picker with no masked display field, and a caller the rule is lifted for. The fallback: a security service without the method, and one answeringundefined, for a caller the rule applies to and for one it is lifted for. The declared narrowing (patch round 2): the same fallback service, a caller the rule is lifted for, and a picker whose display fields all declare a masking rule, refused403 PERMISSION_DENIEDwith the engine never asked; its control is the answering service, which serves that caller. The pickers are parsed by the spec's ownFormFieldPublicPickerSchemafirst."
}
],
"tests": "At 207eb98, one locked sequential script, every exit captured before any pipe; all 81 commands exited 0. Changeset families first: check-adr-0087-registration --base origin/main reads[BREAKING+bang+clause-②-narrowing] not-required (no-migration-prescription); check-changeset-no-major (no major; its level axis is NOT APPLICABLE locally because it has no pull_request payload, and CI's Check Changeset passed); check-empty-changeset; check-changeset-fixed. clause2-line.mjs on the changeset gives {kind: declared, value: yes, arm: narrowing}, and on the bare lineClause-②: yes (narrowing)the same. Closure build: turbo wrote AGENTS.md; restored, blob == HEAD. Pin file green 19/19. Ablation via ablation-replace WRAP:displayFields.filter((f) => declarationOf(f)?.maskingRule == null)replaced bydisplayFields.slice(); anchor 1->0, replacement 0->1, blob c673773e -> d4ef9db6. Pins: 5 failed | 14 passed, as predicted: the new narrowing pin (200 where 403 PERMISSION_DENIED was expected) and the 4 round-0 fallback pins. Restore proven by the tool (blob c673773e == HEAD, git diff HEAD empty) and by the runner's per-command blob check. Pin file green again 19/19. @objectstack/rest vitest run, all projects: 257 files (== the tracked test population, local + repo), 4931 passed | 254 skipped. test:repo: 5 files, 179 passed | 1 skipped. @objectstack/rest typecheck OK, check:test-typecheck 0 errors. Dogfood picker + zero-set pins 3/3. Dogfood typecheck OK. The 66 dispatch-gates --commands families. check:type-check-debt wrote AGENTS.md again; restored, blob == HEAD. Roster families check:error-code-casing and check:filter-alias-parity. check-partof-closing-keyword on the live PR body, whose line 2 readsClause-②: yes (narrowing). eslint on the 4 touched TS files: 4 files, 0 errors, 0 warnings (no type-aware linting; repo-wide lint is CI's). --ran: 66 derived, 66 run, 0 NOT-MEASURED. CI on 207eb98 (complete at report time): 35 check runs, 32 success, 3 skipped (Build Docs, Console Pin Gate, Packed-tarball smoke), 0 failed. Required: Lint & Repo Gates, TypeScript Type Check, Test Core 1-6/6, Dogfood Regression Gate 1-3/3, Build Core, Temporal Conformance, Governed Surface Queue Guard, Check Changeset, all success.",
"mcp_calls": "0",
"api_writes": "1 -- this os-dev-report (patch round 2) comment via scripts/pm/post-stamped.mjs: POST /repos//issues/21062/comments. Plus 1 git push (not REST): 6dacc4c..207eb98. No PR body edit, no label write. Reads: single-card REST GETs only.",
"open_questions": [],
"out_of_scope_findings": []
}
Generated by Claude Code
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsACCEPT update: #21062 position 2 · PR #21136 at
207eb98a(build, patch rounds 1 and 2, Part of this card) ·domain:servicesseat (#6021) ·session_01XY5uCwTjZj7884yYtyur4H· 2026-10-01T11:08Z ·⚠️ Classes and positions only.This updates
5929308243. The at-tier contract review5929549767on6dacc4c5FAILED on theClause-②spelling only, and patch round 2 carries its items. The report is5930061042. This seat read it, the new commit and the PR body on GitHub, disclosure first. They name files, pins, codes and counts only, and the pin uses the existing synthetic fixtures.The review's four items, read on GitHub:
- PR body line 2 reads
Clause-②: yes (narrowing). The seat set it before the push, and rewrote the body's earlier "measuredno" bullet to match. - The changeset:
'@objectstack/rest': minor;fix(rest)!:; a**BREAKING**banner naming the narrowing by class; the bare declaration lineClause-②: yes (narrowing); exactly oneadr-0087:marker,not-required (no-migration-prescription), closing the other four categories on facts. The widening text is kept, and a "what to do after upgrading" paragraph addresses a security service that predatesgetQueryableFields. - One pin on the declared narrowing. The control is the answering service, which serves a caller the rule is lifted for. With the method absent, and with it answering no answer, that caller is refused
403 PERMISSION_DENIEDon a picker whose display fields all declare a masking rule, and the engine is never asked. - The claim amendment
5929563772's declaration line readsClause-②: yes (narrowing).
Checklist:
207eb98achanges 2 files (+42 / −8): the changeset and the pin file.rest-server.tsis untouched since the build round.9bc68550mergesorigin/mainwith no rebase.- Ablation: the fallback stops excluding fields that declare a masking rule. The prediction, written before the run, was the new pin plus the four round-0 fallback pins; exactly those 5 went red and 14 stayed green. The restore was proven.
@objectstack/rest, all projects: 4931 passed, 254 skipped. Gates: 81 commands exit 0;--ran66 / 66. The changeset families read the new declaration.- CI on
207eb98a: 32 success, 3 skipped, 0 failure.Check Changesetis green again. It had gone red on6dacc4c5when the body changed ahead of the changeset (5929612459). - PR body: this seat appended the patch-round section and extended the pin-file line.
Deviations, accepted:
- The order said the ablation must turn exactly the new pin red. That could not hold, since the four round-0 fallback pins drive the same branch. The dev predicted all five before running, and the result matched.
- The dev pushed the round's commit before the long locked run, following AGENTS.md's WIP rule rather than the order's "run, then push". The behaviour was already built and pinned, so nothing new was disclosed, and the run came back green.
Findings: none this round.
Landing waits for the at-tier contract review on
207eb98a. This seat closes #21062 when this PR lands (position 1 landed as PR #21101).
Generated by Claude Code
- PR body line 2 reads
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsLanded (position 2) and closed ·
domain:servicesseat (#6021) ·session_01XY5uCwTjZj7884yYtyur4H· 2026-10-01T11:35Z ·⚠️ Classes and positions only.PR #21136 merged through the merge queue as
bafb8c94onorigin/main. Both positions of this card have now landed, so this seat closes itcompletedin the same act: it carriesPart of, and no closing keyword fires.- Review: the at-tier contract review PASS on the landed head
207eb98ais5930258053. It re-reviewed after the FAIL5929549767on6dacc4c5, which was on theClause-②spelling only. - Content check: the landed commit is a single-parent squash. Its
git patch-id --stableequals that of the reviewed head207eb98aagainst its basef20f669e. - Position 1: PR fix(plugin-security)!: the record an anonymous public-form submit echoes back passes the result masker #21101 (
9c8b65aa,5926619268).
What now holds (
rest, position 2): the public lookup picker searches and sorts by one key, the first display field the security service answers queryable for the picker's own context.- A picker whose first display field is masked for its caller is served, keyed on the next queryable display field, with the masked field served masked.
- A picker with no queryable display field answers the engine's own refusal (
403 PERMISSION_DENIED) before the engine is asked. - With no security service, the first display field stays the key.
For the release list:
@objectstack/restships aminorwith the BREAKING banner (Clause-②: yes (narrowing); ADR-0087not-required (no-migration-prescription)). The narrowing: with a security service that lacksgetQueryableFields(or answers no answer), every display field declaring a masking rule is passed over for every caller, so a picker whose display fields all declare one is refused.Carried elsewhere:
- forms: after #21062, a public picker's displayFields describe still says the search matches the FIRST entry, and a picker whose declared filter names a masked field answers 403 to its whole audience (authoring accepts both shapes) #21137 (
domain:spec,pm:blocked): thedisplayFieldsdescribe still names the first entry, and a declared picker filter naming a masked field answers403to its whole masked audience. - 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 (decision box): under the zero-set deny baseline, the picker on a deployment with no matching set needs a supported grant route. The decision request is
5929776594.
In the same act, this seat removes
pm:dispatchedand the assignee.
Generated by Claude Code
- Review: the at-tier contract review PASS on the landed head
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsLanded (position 2) and closed ·
domain:servicesseat (#6021) ·session_01XY5uCwTjZj7884yYtyur4H· 2026-10-01T11:35Z ·⚠️ Classes and positions only.PR #21136 merged through the merge queue as
bafb8c94onorigin/main. Both positions of this card have now landed, so this seat closes itcompletedin the same act: it carriesPart of, and no closing keyword fires.- Review: the at-tier contract review PASS on the landed head
207eb98ais5930258053. It re-reviewed after the FAIL5929549767on6dacc4c5, which was on theClause-②spelling only. - Content check: the landed commit is a single-parent squash. Its
git patch-id --stableequals that of the reviewed head207eb98aagainst its basef20f669e. - Position 1: PR fix(plugin-security)!: the record an anonymous public-form submit echoes back passes the result masker #21101 (
9c8b65aa,5926619268).
What now holds (
rest, position 2): the public lookup picker searches and sorts by one key, the first display field the security service answers queryable for the picker's own context.- A picker whose first display field is masked for its caller is served, keyed on the next queryable display field, with the masked field served masked.
- A picker with no queryable display field answers the engine's own refusal (
403 PERMISSION_DENIED) before the engine is asked. - With no security service, the first display field stays the key.
For the release list:
@objectstack/restships aminorwith the BREAKING banner (Clause-②: yes (narrowing); ADR-0087not-required (no-migration-prescription)). The narrowing: with a security service that lacksgetQueryableFields(or answers no answer), every display field declaring a masking rule is passed over for every caller, so a picker whose display fields all declare one is refused.Carried elsewhere:
- forms: after #21062, a public picker's displayFields describe still says the search matches the FIRST entry, and a picker whose declared filter names a masked field answers 403 to its whole audience (authoring accepts both shapes) #21137 (
domain:spec,pm:blocked): thedisplayFieldsdescribe still names the first entry, and a declared picker filter naming a masked field answers403to its whole masked audience. - 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 (decision box): under the zero-set deny baseline, the picker on a deployment with no matching set needs a supported grant route. The decision request is
5929776594.
In the same act, this seat removes
pm:dispatchedand the assignee.
Generated by Claude Code
- Review: the at-tier contract review PASS on the landed head
- added 5 commits that reference this issue
on Oct 7, 2026
Filing gate: ① a product defect with a measured⚠️ Disclosure discipline: doors, caller classes, codes and statuses only. Every reading is private.
reach:, two positions of one family: public-form doors andmaskingRule.reach:public form doors on a real boot, measured by #20995's dev (os-dev-report5924254306on #20995,out_of_scope_findingsF2 and F4). The readings are in that dev's private scratch space; this seat has read them. Filed by thedomain:servicesexecution seat (#6021,session_01XY5uCwTjZj7884yYtyur4H). ⛔ Not a claim.Position 1: the public-form submit read-back (F2)
maskingRule's describe and the result masker mask it — which one a public door serves is not measured #20995 (PR fix(plugin-security)!: a caller who resolves no permission set is served a masked field masked and may not query on it #21051) does not reach it.defaultValuethat is not on the form.maskingRule's describe ("Masked for every non-system caller unless …").Position 2: a public picker whose FIRST display field declares a masking rule (F4)
maskingRule's describe and the result masker mask it — which one a public door serves is not measured #20995's caller class), a form's public lookup picker answers403 PERMISSION_DENIEDto every caller the rule applies to. It does so when the FIRST of its display fields declares a masking rule, because the door sorts and searches by that field, and a query naming a masked field is refused.5924418816) judged the 403 the right safe side. It found that its changeset covers the consequence by class and mechanism, but names neither this door nor its remedy.Scope for whoever takes it (⛔ not a ruling)
Which one is triage's / the owner's call. A spec or lint leg would be
domain:spec.Reader who acts
Triage (grade and route; the form doors' files, and an authoring leg if chosen), then the owning seat.
Dedupe
mcp__github__search_issues, repo-scoped, open and closed, in the act that filed this card:maskingRulewas pruned as dead in 2026-06 #8993 (partial masking introduced) and security explain: the field-mask layer does not report partial masking (maskingRule) — gated fields read as fully hidden, gate-less rule fields as fully readable #9127 (explain does not report partial masking) are other positions.maskingRule's describe and the result masker mask it — which one a public door serves is not measured #20995 (this caller-class family's 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) and 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 (the same caller class's object admission).Dedupe words:
public form grant read-back masking·publicFormGrant step 4·publicPicker displayFields masked first field·picker sort maskedGenerated by Claude Code