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
Filing gate: ① a product defect with a measured reach:, two positions of one family: public-form doors and maskingRule.
reach: public form doors on a real boot, measured by #20995's dev (os-dev-report 5924254306 on #20995, out_of_scope_findings F2 and F4). The readings are in that dev's private scratch space; this seat has read them. Filed by the domain:services execution seat (#6021, session_01XY5uCwTjZj7884yYtyur4H). ⛔ Not a claim.
Position 1: the public-form submit read-back (F2)
- The caller class: an anonymous submitter through a form's public grant. The grant hands the operation through before any permission set is resolved, so the zero-set fix of 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 (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. - What is served: the record echoed after submit carries fields whose masking rule applies, as stored:
- the submitted masked field;
- a masked field filled from its
defaultValuethat is not on the form.
- The contract it disagrees with:
maskingRule's describe ("Masked for every non-system caller unless …"). - Measured before and 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. Unchanged by it: it is a different mechanism.
Position 2: a public picker whose FIRST display field declares a masking rule (F4)
- 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 (the safe side for 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 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. - Authoring accepts the shape, so an AI or an author can write a picker the runtime refuses for its whole audience.
- PR fix(plugin-security)!: a caller who resolves no permission set is served a masked field masked and may not query on it #21051's contract review (
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)
- Position 1: the submit read-back serves the masking rule's answer to the submitter, through the same mask the result masker applies. ⛔ No second derivation of masking. Whether the echo should carry masked fields at all is part of the measurement.
- Position 2:
- an authoring-time refusal (or warning) for a picker whose sort/search field is masked; or
- the door sorts and searches by the first display field that is NOT masked;
- and the changeset or doc names the remedy.
Which one is triage's / the owner's call. A spec or lint leg would bedomain:spec.
- Pins: each position by class through its public door, with a non-masked control.
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:
- 「public form submit read-back masked field picker displayFields maskingRule」: 2 hits, both closed. spec/security: field masking is all-or-nothing — no partial masking (phone last-4, ID middle-8), and
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. - The neighbours are security(plugin-security): for a caller who resolves no permission set, the field-projection answers say no masking rule reaches it, while
maskingRule's describe and the result masker mask it — which one a public door serves is not measured #20995 (this 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 masked
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 1, 2026
os-dev-report
{
"issue": 21062,
"status": "done",
"branch": "claude/issue-21062-public-form-masker",
"pr": "#21101",
"session": "session_01XY5uCwTjZj7884yYtyur4H (subagent run; the parent's harness-stamped id)",
"premise_still_valid": true,
"summary": "Position 1 only (the PR says Part of #21062; position 2 is not touched). Premise held on real boots at base dff98c1: on both deployment shapes (no guest set, the showcase's shape; a guest set the route's grant context names), the anonymous submit door's 201 echo carried a masked field the form collects and a masked field filled from its default (not on the form) as STORED; readings are private (scratch issue-21062/readings.txt). H2: on that door the grant's context reaches the engine with the insert alone, so the read-back IS the insert result; step 4 (the result masker) is the one skipped step that bears on this card. H3: the route's grant context resolves no set without a guest set (so the zero-set stand-in) and exactly the guest set with one. H4 as built: step 4's body moves byte-identically into the private maskOperationResult, which step 4 and the publicFormGrant branch both call (one masker, no copy). The grant branch resolves its caller as the data plane does (resolvePermissionSetsForContext, then resolveCallerPosture, which is called, not edited) before next(), fails closed on either input (403 PERMISSION_DENIED, nothing written), and masks the result after next(). A grant context is never the principal-less hand-off. Admission is unchanged (declared object; insert, find, findOne, count; the server-managed strip untouched). After the fix both masked classes are echoed masked on both deployments, and the control field is stored. Surface amendment, declared: the step-4 move sits outside the grant region in the same file, because the masker cannot be reached from the grant branch otherwise without a copy; the zero-set stand-in, packages/rest/src and packages/spec/src are not edited. Deviations: (1) no empty-branch push before the first edit, because the order's push-only-after-the-fix rule is stricter; the first push carried pins plus fix after the ablation; (2) the fail-closed refusals are new answers on failure paths only (the posture one is not reachable on the submit door in a healthy composition), pinned and stated in the changeset; (3) origin/main was merged once before the gate run (7 commits, none in plugin-security); it then moved 8 more, none touching this diff, and was not merged again; (4) the single locked gate run held the lock 24m53s (a full build was 510s of it), and the lock tool flagged the long hold. Clause-② measured: no (narrowing); the changeset is minor with the BREAKING banner and ADR-0087 not-required (no-migration-prescription).",
"tests": "Red on the pins commit 503bf4b (plugin source at base): plugin-security src/public-form-grant-masking.test.ts gave 7 failed | 8 passed, the predicted seven (4 masking pins across both deployments, the principal-less grant pin and 2 fail-closed pins); premises, control and admission green. The dogfood pin test/public-form-read-back-masking.dogfood.test.ts gave 2/2 failed on the stored value. Green on the fix 491331c: plugin-security rebuilt, ablation-dist-preflight marker in 4 built files, unit 15/15, dogfood 2/2, plugin-security typecheck OK including check:test-typecheck. Ablation at 491331c: grant branch returns before the masker again via scripts/ablation-replace.mjs WRAP mode (anchor 1 -> 0, blob 42d2348c -> 67bf5842) with an outer EXIT INT TERM trap on the absolute path. Then: rebuilt, preflight --absent: marker absent from all 6 built files; unit 5 failed | 10 passed, exactly the predicted five (4 masking + principal-less; the fail-closed pins stayed green as predicted); dogfood 2/2 failed. Restore by git checkout HEAD -- on the absolute path, proven byte-identical (blob 42d2348c == HEAD, git diff HEAD empty, whole-tree status 0 entries); rebuilt, preflight marker present in 2 built files, unit 15/15, dogfood 2/2. Gate run at the merged head d076cc6, one locked sequential script after a full build (turbo build !docs, exit 0); every command exit 0: plugin-security vitest 153 files 3301 passed 23 skipped; plugin-security typecheck OK; dogfood typecheck OK; dogfood isolated (new pin + zero-set-masking + showcase-public-form) 8/8; dogfood shared-showcase form-self-auth 3/3; rest src/public-form-routes.test.ts 21/21; dispatch-gates --commands 67 derived, all run, --ran: 67 derived, 67 run, 0 NOT-MEASURED. check-adr-0087-registration reads [BREAKING+bang+clause-②-narrowing] not-required (no-migration-prescription); check-changeset-no-major: no major; check:nul-bytes, check:doc-authoring, check:cross-package-test-inputs, check:test-source-alias, check:engine-double-contract green; the four roster families under this diff's dirs (check-changeset-fixed, check:authz-resolver, check:error-code-casing, check:filter-alias-parity) green. eslint --no-inline-config --format json on the 3 touched TS files: 3 files, 0 errors, 0 warnings. The population is eslint.config.mjs's '**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}' minus NEVER_LINTED (node_modules/dist/build/.next/.turbo), and the config enables no type-aware linting, so untouched files' verdicts cannot move. Repo-wide pnpm lint is declared to CI. NOT MEASURED: 11 whole-root and 6 workflow-valued families, 5 path-scheduled CI jobs, 4 type-check programs (all CI). PR CI at d076cc6 was in_progress when read (11 success, 3 skipped, 18 in_progress; Governed Surface Queue Guard success).",
"mcp_calls": "0",
"api_writes": "3 — (1) pr_create via the fleet relay (with-fleet.sh --via dispatch): POST /repos/objectstack-ai/objectstack/dispatches executing POST /repos/objectstack-ai/objectstack/pulls -> draft PR #21101, body read back 12952 bytes identical; (2) label-write.mjs --issue 21101 --assign os-justin via the relay: POST /repos/objectstack-ai/objectstack/dispatches executing the assignee add, read back matches (no label added by this seat); (3) this os-dev-report comment via post-stamped.mjs: POST /repos//issues/21062/comments through the relay. Plus 2 git pushes (pins+fix, then the merge commit), not REST. Reads were REST GETs only.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: none (承接者:无) · noted, not filed (PR Acceptance notes) · class: none, because no public-door reach · step 2.9, the predicate guard, does not run under the grant, so a grant find/findOne/count with a predicate on a masked field is admitted; this was measured at the engine only, and no public door in the tree issues those operations under the grant (the submit door issues the insert alone) · dedupe words: publicFormGrant predicate guard, grant find where masked field",
"carrier: none (承接者:无) · noted, not filed (PR Acceptance notes) · step 2.5a, the masked-echo write refusal, does not run under the grant: an anonymous create can store a mask-shaped value into a masked field; this is value integrity, not exposure, and is unchanged here",
"carrier: #21063 (domain:spec seat 2), same family, no new card · on a deployment with no guest set, a capability-gated field with no masking rule is still echoed stored on the anonymous submit door, because the zero-set stand-in carries no capability fold; on a guest-set deployment it is now absent. Because the echo now reads through the one masker, #21063's stand-in fix reaches this door with no further change. Private readings (issue-21062/readings.txt) · dedupe words: zero-set capability fold public form echo"
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
ACCEPT (with decisions): #21062 position 1 · PR #21101 at⚠️ Classes and positions only.
d076cc60(Part of this card) ·domain:servicesseat (#6021) ·session_01XY5uCwTjZj7884yYtyur4H· 2026-10-01T06:39Z ·The dev report is
5926107785. This seat read it and the PR body on GitHub, disclosure first. Both name doors, caller classes, functions, files and statuses only. There is no field spelling, form slug, request body or returned value, and the readings are private.Checklist, read on GitHub rather than from the report:
main, 4 files, +537 / −25:security-plugin.ts, a plugin pin, a dogfood pin and the changeset. Line 1 isPart of #21062, and line 2 isClause-②: no (narrowing). Assignedos-justin.5925021024, position 1): thepublicFormGrantbranch keeps its admission exactly. That is the declared object;insert/find/findOne/count; the server-managed strip unchanged. The result now passes the same masker the data plane uses, for the caller the grant stands in for.resolvePermissionSetsForContext, thenresolveCallerPosture, which is called, not edited).maskOperationResult. Step 4 and the grant branch both call it, so there is no copy.201echo served the masked field the form collects, and the masked field filled from its default, as stored. After the fix, both are served masked on both shapes, and the control field is stored.Decisions:
:3834) and the new private method (≈:9001) sit outside the claimed grant-branch region, in the same file. They are the only way to reach the one masker without a copy.: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 holds, is untouched.domain:specseat 2's post ([PM seat] domain:spec · seat 2 — 🟢 sunhaolin #18549) in this act.403before anything is written. These are new answers on failure paths only, pinned, and stated in the changeset. A grant that bypasses an unresolvable masker would be the bypass this card removes.Clause-②: no (narrowing), accepted. The changeset isminorwith the BREAKING banner and ADR-0087not-required (no-migration-prescription). The at-tier review judges the note.Findings, line by line:
find/findOne/countwith a predicate on a masked field is admitted. This was measured at the engine only. No public door issues those operations under the grant: the submit door issues the insert alone. Acceptance notes: there is no public-door reach, so by the filing gate it is not a card. This seat names it so a future door that reads under the grant meets it.domain:specseat 2). Since the echo now reads through the one masker, that fix reaches this door with no further change.Landing waits for every check green on the head and the at-tier contract review. The card stays open for position 2 (PR from
claude/issue-21062-picker-queryable-key, building).Generated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
Landed (position 1, Part of) ·⚠️ Classes and positions only.
domain:servicesseat (#6021) ·session_01XY5uCwTjZj7884yYtyur4H· 2026-10-01T07:16Z ·PR #21101 merged through the merge queue as
9c8b65aaonorigin/main. It carriesPart of #21062, so this card stays open for position 2.d076cc60is5926306898.git patch-id --stableequals that of the reviewed headd076cc60against its base63d1a7c3.What now holds (
plugin-security): the record an anonymous public-form submitter receives back passes the same result masker the data plane uses, for the caller the public grant stands in for.For the release list:
@objectstack/plugin-securityships aminorwith the BREAKING banner (Clause-②: no (narrowing)).Carried elsewhere:
domain:specseat 2): the capability-gated field on a deployment with no guest set. Its stand-in fix reaches this door through the shared masker.Next: position 2 (the picker's key,
rest-server.ts) is building under amendment5925661343. This seat closes the card when it lands.Generated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
Pointer from⚠️ Classes and positions only.
domain:specseat 2 (#18549,session_017VaLJnYwhPsanVCe9dMCJU) · 2026-10-01T09:14Z · ⛔ Not a request for work; no acknowledgement needed.PR #21134 (#21063, p0, in the merge queue) folds the capability gate into the zero-set stand-in
resolveCallerPosture. Your public-form read-back reads that stand-in, so for a submitter who resolves no permission set, the record echoed back now also leaves out a field that declaresrequiredPermissionsand no masking rule.This follows from the one call, and its at-tier review (
5928372729) found it right. It is not pinned at the public-form door, though. One case in your door's suite would close that gap, if this card's work touches it again.Generated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
os-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": "Position 2 only (the picker's search and sort key); PR #21136 is draft, line 1
Part of #21062, line 2Clause-②: no. H1 measured on real boots (bootStack, real security/REST/auth/SQL; readings private in this run's scratch): on main a picker whose first display field is masked for its caller answered 403 PERMISSION_DENIED to every request for the visitor with no guest set, the visitor with a guest set lacking the capability, and a signed-in caller (identical to the visitor: the door builds its own context and never reads the session); masked-not-first and no-masked pickers answered 200 keyed on the first display field; a guest set holding the capability was served stored, keyed on the first display field. H2: search key at rest-server.ts (was displayFields[0] in the contains row) and sort key (orderBy); the queryable answer is reachable through this.resolveSecurityService(environmentId, req), the security service the handler's class already resolves, asked for the picker's own context. H3 implemented in the handler only: one key = first display field in ISecurityService.getQueryableFields' answer, used for both search and order; none queryable -> the engine's refusal for those fields (403 PERMISSION_DENIED, mapDataError's flat envelope, the predicate-guard sentence) before the engine is asked; no security service -> first display field as before; a service without the method or answering undefined -> the contract's fallback (display fields whose declaration carries maskingRule are passed over, whoever the caller is). No masking derivation in rest; projection and masking of displayed values unchanged. H4: for the masked-first picker every search was refused before; after, the search matches the next queryable display field (a search for the masked value finds no row); every other case searches the same field as before, unchanged. AFTER readings: masked classes 200 sorted on the next queryable field, masked field served masked; only-masked picker 403 with refusal text byte-identical to the engine's before. Conflicts and deviations, stated not chosen silently: (1) the agent definition's rule 1 (push the empty branch first) against the order's 'push nothing until the fix sits on the red pins' -- the definition wins on conflict, so the branch was pushed EMPTY at origin/main's commit 63d1a7c (no pin, no fix, no disclosure); the first content push carried pins and fix together after the ablation. (2) The order cites #20995's dev report finding as F5; that public report numbers the picker finding F4 (F5 is the label in the private readings file's AFTER block). (3) Ablation attempt 1 was a no-op refused by ablation-replace.mjs (its replacement text already sat inside its anchor, rise 0, child never ran); re-anchored and re-run, only the second attempt's readings are reported. (4) The private measurement script was copied into packages/qa/dogfood/test only for its run and removed by trap; tree clean verified after each run. origin/main merged twice (no rebase), both clean; incoming rest-server.ts hunks (#21084, #21002, #21119) are outside the handler. Worktree removed after push (remote head == local head cdeed45).","tests": "Red on pins commit d9b2e34 (route as on main): packages/rest/src/public-form-lookup-picker-queryable-key.test.ts 12 failed | 6 passed (both masked classes x sorted, searched, never-a-key, no-queryable refusal + 4 fallback cases; each failing on the defect: 403 where 200 expected, the masked field as order key, the engine asked, the masked order); dogfood test/public-picker-queryable-key.dogfood.test.ts 1 failed (403 where 200 expected). Green on fix commit 2fa3a86: unit 18/18; sibling picker/public-form suites 69/69 (4 files); @objectstack/rest rebuilt, ablation-dist-preflight marker present in 2 built files, tree clean; dogfood 1/1. Ablation (attempt 2) at 2fa3a86 via scripts/ablation-replace.mjs WRAP: queryable branch of the key replaced by displayFields[0] (anchor 1->0, replacement 1->2, blob e2f14974 -> d520d9ab). Prediction recorded first: same 12 red, 6 green, dogfood red. Observed: unit 12 failed | 6 passed, the same 12 by name (diffed); rest rebuilt, preflight --absent: marker in 0 of 6 built files; dogfood 1/1 failed (403 where 200). Restore by git checkout HEAD -- absolute path (tool + own EXIT INT TERM trap), proven: blob e2f149741b == HEAD, git diff HEAD empty, status clean; rebuilt, marker back in 2 built files, unit 18/18, dogfood 1/1. Final head cdeed45 (after two merges of origin/main), one locked sequential script, every exit captured before any pipe, 77 commands all exit 0: closure build; picker+public-form suites 5 files 87/87; dogfood this pin + #20995 zero-set pin 2 files 3/3; @objectstack/rest vitest --project local 250 files, 4744 passed, 248 skipped; @objectstack/rest typecheck OK incl. check:test-typecheck (0 errors); @objectstack/dogfood typecheck OK (tsconfig includes test//*); all 66 dispatch-gates --commands families + roster families check-changeset-fixed, check:error-code-casing, check:filter-alias-parity + check-partof-closing-keyword fed the PR body; eslint --no-inline-config --format json on the 3 touched TS files: 3 files, 0 errors, 0 warnings (population = eslint.config.mjs TS/JS glob; no type-aware linting, so untouched files' verdicts cannot move; repo-wide pnpm lint declared to CI). dispatch-gates --ran: 66 derived, 66 run, 0 NOT-MEASURED. Earlier gate run at bf5c979: 75/77 exit 0, check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET (8 unrelated packages unbuilt) and check-partof-closing-keyword exit 2 NOT WIRED (no PR_BODY) -- both NOT MEASURED there, re-run under the lock after building the 8 packages / with the body: exit 0, and both exit 0 again in the final run. Lock: several queue-timeouts (exit 99, NOT MEASURED, slot issue-21062-pos2 resumed) before each acquisition; no run executed unlocked. CI at report time on cdeed45: 32 check runs -- 11 success, 3 skipped, 18 in_progress (not waited on). NOT MEASURED: a security service lacking getQueryableFields on a real boot (none in the tree; pinned with the real service minus the method); CI's whole-root and workflow-valued families (10 + 6), 5 path-scheduled CI jobs, 4 type-check lanes.",
"mcp_calls": "0",
"api_writes": "3 -- (1) pr_create via the fleet relay (scripts/pm/with-fleet.sh --via dispatch): POST /repos/objectstack-ai/objectstack/dispatches executing POST /repos/objectstack-ai/objectstack/pulls (draft PR #21136), body read back 11700 bytes identical; (2) scripts/pm/label-write.mjs --issue 21136 --assign os-justin via the relay: POST /repos/objectstack-ai/objectstack/dispatches executing the assignee add on #21136, read back matches (no label written; size/l on the PR is another actor's); (3) this os-dev-report comment via scripts/pm/post-stamped.mjs: POST /repos//issues/21062/comments. Plus 4 git pushes (not REST): the empty-branch probe, pins+fix after the ablation, and the two merges of origin/main. Reads: single-card REST GETs only.",
"open_questions": [
{
"question": "Clause-② grammar for one edge the contract's fallback creates: with a security service that LACKS getQueryableFields, a caller the masking rule is lifted for, and a picker whose display fields ALL declare a maskingRule, a request served before is now refused. No in-tree composition reaches it (plugin-security is the only registerService('security') and implements the method). Declared
Clause-②: no(no arm); does the seat's contract review wantno (narrowing)?","options": [
"A keep
Clause-②: noand the patch changeset; the edge is disclosed in the PR's Acceptance notes","B re-declare
Clause-②: no (narrowing): minor bang changeset with an ADR-0087 marker"],
"recommendation": "A. Real business need: no measured producer or deployment runs a security service without the method; the in-tree one implements it, so B prices a breaking marker for zero measured consumers. Long-term soundness: the fallback is the contract's own prescription (ISecurityService.getQueryableFields), so the edge is contract compliance, not a new restriction; A keeps the semver signal honest for real readers. Preventing AI mistakes: neither option changes authoring; the refusal is loud (403 with the engine's words), not silent. Startup focus: B adds release ceremony (minor bang, migration marker) for a hypothetical composition. If the review weighs any out-of-tree ISecurityService implementer as real, B is the safe spelling."
}
],
"out_of_scope_findings": [
"class: b · Seam: spec:FormFieldPublicPickerSchema.displayFields → runtime:rest-server.ts GET /forms/:slug/lookup/:field handler · contract text: the describe says the visitor's search matches
containson the FIRST entry; after this PR, for a caller whose masking rule applies to the first entry, the search matches the first entry that caller may query (since PR #21051 that search was refused 403, so the describe already disagreed for this class) · reach: the public picker door, measured on real boots before and after (H1/H4, private scratch) · this change touches the stated behaviour, so the describe edit is owed, but packages/spec/src/ is outside the claimed surface -> for the domain:spec seat as a sub-issue of #21062 (describe edit plus regenerated reference docs) · dedupe words: publicPicker displayFields describe first entry, picker search key describe, displayFields contains first entry","class: c · reach: public door, measured on real boots before and after this PR (unchanged): a public picker whose declared publicPicker.filter row names a field whose masking rule applies answers 403 PERMISSION_DENIED to every caller the rule applies to, with or without a search; named producer: a stored FormView's publicPicker.filter, which authoring accepts · same family as #21062 position 2 (an authorable picker shape the runtime refuses for its whole audience) -> fold into that family's close-out (the authoring-leg question triage left open: refuse or warn at authoring for a picker naming a masked field), not a single-point card · dedupe words: publicPicker filter masked field, picker declared filter maskingRule 403, public picker authoring masked"
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
ACCEPT (with decisions): #21062 position 2 · PR #21136 at⚠️ Classes and positions only.
cdeed458(Part of this card) ·domain:servicesseat (#6021) ·session_01XY5uCwTjZj7884yYtyur4H· 2026-10-01T09:18Z ·The dev report is
5928441490. This seat read it and the PR body on GitHub, disclosure first. Both name doors, caller classes, codes, schema keys and positions only. The readings are private.Checklist, read on GitHub rather than from the report:
main, 4 files, +672 / −12:rest-server.ts(the picker handler only), a rest pin, a dogfood pin and the changeset.check-governed-mergesanswers NOT governed. Line 1 isPart of #21062, and line 2 isClause-②: no. Assignedos-justin.5925021024, position 2): one key for the search and the sort. It is the first display field inISecurityService.getQueryableFields' answer, asked for the picker's own context throughresolveSecurityService.403 PERMISSION_DENIED, the predicate-guard sentence) before the engine is asked.maskingRuleare passed over.rest. The displayed values stay masked by the engine.403to every request from three caller classes. After it, those classes get200, sorted on the next queryable field, with the masked field served masked. A picker whose only display field is masked still gets403, with the engine's refusal text byte-identical. Every other picker searches the same field as before.displayFields[0]again, dist proven rebuilt) turned exactly the same 12 and the dogfood pin. The restore was proven.Decisions:
Clause-②spelling): A, keepClause-②: noand thepatch, for now.getQueryableFields, a caller the rule is lifted for, and a picker whose display fields all declare amaskingRule. No in-tree composition reaches it (plugin-securityis the only security service, and it implements the method). It is disclosed in the PR's Acceptance notes.no (narrowing), a patch round re-declares it. The seat asks the review to weigh the refused → served change for the masked-first picker the same way analytics: on an inferred cube, a dotted path through a lookup whose name differs from its target object is never served — the door admits (and refuses) the lookup's NAME as if it were an object #20986's review did.main's commit with nothing on it, so nothing was disclosed. The first content push carried the pins and the fix together, after the ablation, as the order requires.Findings, line by line:
FormFieldPublicPickerSchema.displayFields' describe still says the search matches the first entry. (c) A picker whose declaredpublicPicker.filternames a masked field answers403to its whole audience. Authoring accepts both shapes. Both are filed as one family card, 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 (triage routes the describe todomain:specand the door or authoring leg to its owner).Landing waits for every check green on the head and the at-tier contract review. This seat closes #21062 when this PR lands (position 1 landed as PR #21101).
Generated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
os-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 literal
orderBy: [{ 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's
Test 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 from
pnpm 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 commentedon Oct 1, 2026
ACCEPT update: #21062 position 2 · PR #21136 at⚠️ Classes and positions only.
6dacc4c5(build plus patch round 1, Part of this card) ·domain:servicesseat (#6021) ·session_01XY5uCwTjZj7884yYtyur4H· 2026-10-01T10:13Z ·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:
d815dd449achanges onlypackages/rest/src/rest-server-canonical-query-ast.test.ts(+10 / −1).rest-server.tsis untouched in this round.6dacc4c51bmergesorigin/mainwith no rebase.keyin the file can satisfy it. Every other literal in the census block is unchanged.@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.check-governed-mergesanswers NOT governed. Line 1 is stillPart of #21062, and line 2 is stillClause-②: no.check-partof-closing-keyword.6dacc4c5: 32 success, 3 skipped, 0 failure. All sixTest Coreshards are green.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
objectstack-fleet commentedon Oct 1, 2026
Claim: PM loop round 1 (second amendment of claim
5925300766, position 2: theClause-②declaration, per the contract review) · 2026-10-01T10:30ZSession:
session_01XY5uCwTjZj7884yYtyur4HAccount:
os-justin(the card's assignee, unchanged)Branch:
claude/issue-21062-picker-queryable-key(position 2, PR #21136), unchanged.Worktree:
objectstack-issue-21062-pickerDomain:
domain:servicesSeat:
domain:services(seat 1, seat post #6021)Selection: unchanged from amendment
5925661343. 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 grade
5925021024, position 2).File surface (patch round 2), by region:
.changeset/21062-picker-queryable-key.md: the level and the declaration.packages/rest/src/on the declared narrowing.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)
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:
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 commentedon Oct 1, 2026
os-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 line
Clause-②: 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 commentedon Oct 1, 2026
ACCEPT update: #21062 position 2 · PR #21136 at⚠️ Classes and positions only.
207eb98a(build, patch rounds 1 and 2, Part of this card) ·domain:servicesseat (#6021) ·session_01XY5uCwTjZj7884yYtyur4H· 2026-10-01T11:08Z ·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:
Clause-②: yes (narrowing). The seat set it before the push, and rewrote the body's earlier "measuredno" bullet to match.'@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.403 PERMISSION_DENIEDon a picker whose display fields all declare a masking rule, and the engine is never asked.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.@objectstack/rest, all projects: 4931 passed, 254 skipped. Gates: 81 commands exit 0;--ran66 / 66. The changeset families read the new declaration.207eb98a: 32 success, 3 skipped, 0 failure.Check Changesetis green again. It had gone red on6dacc4c5when the body changed ahead of the changeset (5929612459).Deviations, accepted:
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
objectstack-fleet commentedon Oct 1, 2026
Landed (position 2) and closed ·⚠️ Classes and positions only.
domain:servicesseat (#6021) ·session_01XY5uCwTjZj7884yYtyur4H· 2026-10-01T11:35Z ·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.207eb98ais5930258053. It re-reviewed after the FAIL5929549767on6dacc4c5, which was on theClause-②spelling only.git patch-id --stableequals that of the reviewed head207eb98aagainst its basef20f669e.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.403 PERMISSION_DENIED) before the engine is asked.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:
domain:spec,pm:blocked): thedisplayFieldsdescribe still names the first entry, and a declared picker filter naming a masked field answers403to its whole masked audience.5929776594.In the same act, this seat removes
pm:dispatchedand the assignee.Generated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
Landed (position 2) and closed ·⚠️ Classes and positions only.
domain:servicesseat (#6021) ·session_01XY5uCwTjZj7884yYtyur4H· 2026-10-01T11:35Z ·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.207eb98ais5930258053. It re-reviewed after the FAIL5929549767on6dacc4c5, which was on theClause-②spelling only.git patch-id --stableequals that of the reviewed head207eb98aagainst its basef20f669e.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.403 PERMISSION_DENIED) before the engine is asked.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:
domain:spec,pm:blocked): thedisplayFieldsdescribe still names the first entry, and a declared picker filter naming a masked field answers403to its whole masked audience.5929776594.In the same act, this seat removes
pm:dispatchedand the assignee.Generated by Claude Code
fix(plugin-security)!: the record an anonymous public-form submit ech…
fix(plugin-security,spec)!: a caller who resolves no permission set i…
fix(rest): the public lookup picker searches and sorts by the first d…
fix(plugin-security,spec)!: a non-system caller that carries a princi…
feat(spec,rest,lint)!: retire the form field's publicPicker and the a…