Skip to content

fix(plugin-approvals): a request's subject title is served only from fields its reader may read - #23614

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-23594-subject-title-fls
Oct 15, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-23594-subject-title-fls

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #23594
Clause-②: no

A request's subject title (record_title) is now served only from fields its reader may be served. The one-tap action-link pages, which have no reader to narrow for, name the record by its id. Function level only, per the card.

What changed

All in packages/plugins/plugin-approvals.

The dispatch's mechanism assumptions, measured on origin/main a6d0715535

  1. Held. listRequests and loadRequest already redacted the payload by the caller (redactPayloads), then resolveSubjectDisplays read the subject under SYSTEM_CTX and picked the title from the whole record, once per object, for every reader. The narrowing now sits where the payload's does, with the same answer.
  2. Held already, measured. The title's snapshot fallback (pickTitle(r.payload) in enrichRows) reads the payload after redactPayloads has redacted it for the reader, so a deleted subject never leaked a withheld snapshot field. Pin (b) is green on the old code and on the new.
  3. Toasts: every request action's read-back row (readBackRequest to loadRequest) is enriched under the acting caller's context, so the success copy's result.request.record_title token is narrowed by this change. Action-link pages: the token chain reads the request with getRequest(..., SYSTEM_CTX) (peek) and decides under the system context with the slot's person attached (redeem). Field-level security does not narrow a system context (plugin-security returns the full set for isSystem), so the page cannot be narrowed for its bearer. Per the dispatch, the title is withheld there, not granted, for every bearer.
  4. Not built; reported. The subject's own declared API exposure does not gate the payload snapshot: the [finding] approvals: sys_approval_request.payload_json snapshots the full raw row and serves it to approvers unfiltered — object-level hidden: true and FLS declarations cannot reach inside the JSON blob #10749 header states the seam "does not gate on OBJECT-level access". Gating the title by exposure while the same field ships in the snapshot would be a second rule beside the redaction. 896a434 gated a SECOND object's titles (payload_display lookup targets), which no approval routes the reader to. Whether exposure should gate the approval path at all is a separate design question.

Pins

New subject-title-reader-fls.integration.test.ts: the real ApprovalService on a real ObjectQL over SqlDriver (in-memory better-sqlite3), with a field-visibility source standing in for plugin-security's.

  • (a) A reader who may not read the display field gets no title on getRequest and listRequests. The payload is narrowed by the same answer. A display field served masked is withheld the same way.
  • (b) The same reader, subject deleted, snapshot carrying the title: no title. Control: the reader who may read it gets the snapshot's title.
  • (c) The decision result row, which the toast token reads, carries no title for that reader.
  • (d) The action-link confirm and result pages name the record by its id and carry no title, for a restricted bearer and for a reader alike.
  • (e) Control: a reader who may read the field gets the title on the single read, the list and the decision result.
  • (f) A withheld display field lends nothing: with the display field withheld and another title-eligible field readable, the title is the readable field's value, and the withheld value appears nowhere on the row.
  • (g) One rule, asked once: one field-visibility ask per object per read, on the single read and on a multi-row list.

Updated: action-page-member.integration.test.ts, the POST parity twin case (a token is single-use, so twins now differ by their record ids alone; byte-equal once that difference is named, and each page equals the renderer's page for its own request). payload-display-target-exposure.test.ts and implicit-reference-target-lookup.test.ts pass the new required argument (an empty map: no answer for any object). One comment in approval-requested-notification.integration.test.ts no longer says the inbox title is unnarrowed.

Red before, ablations after

  • Before the fix, the new file on a6d0715535 plus the pins: 5 failed / 3 passed. The failures were (a) twice, (c), (d) and (f); (b), (e) and (g) were green.
  • Ablation 1, at commit 67fd2c6bee, via scripts/ablation-replace.mjs under an EXIT/INT/TERM trap. The served-view line in resolveSubjectDisplays was replaced by the old whole-record pick. Predicted: red on (a), (a masked), (c) and (f). Observed: 4 failed / 4 passed, exactly those. Restore proven: blob daeac5d639c7 equals HEAD, and git diff HEAD is empty.
  • Ablation 2, same tool. The old record_title || record_id row was put back in summaryRows. Predicted: red on (d). Observed: 1 failed / 7 passed, that one. Restore proven: blob 4123cfb893a1 equals HEAD, and git diff HEAD is empty.
  • No dist step: every subject is imported from src by relative path.

Tests and gates

  • pnpm --filter @objectstack/plugin-approvals exec vitest run --maxWorkers=2 (whole package, at 67fd2c6bee, before the merge of origin/main 8d05456cbe): Test Files 75 passed (75), Tests 1062 passed (1062), lock VERDICT command-exit 0. The merge brought only plugin-auth, plugin-email, AGENTS.md and a scripts/pm file, none in this package or its dependency closure.
  • pnpm --filter @objectstack/plugin-approvals typecheck: exit 0. check:test-typecheck OK (8 files / 324 errors / 27 signatures held). tsc --listFiles on tsconfig.test.json compiles all five edited or new test files.
  • Gates: at d3ca88e8d3 (the final head), node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 67 commands. All 67 ran, each exit code captured before any pipe, and all 67 exit 0. check:dual-build-cjs-loads and check:i18n first answered 3, PREREQUISITE NOT MET (no workspace dist). After a workspace build (75 tasks, exit 0), both answered 0; they are recorded at that measured 0, not as failures. --ran reconciliation: 67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN (exit 0). eslint over the 7 changed TS files: exit 0, no findings. The local-only families (check:dev-prereqs, check:skill-frame-freshness) are not owed: no packages/spec build input or frame file is touched. The full farm is CI's.

Acceptance notes

  • The toast for a withheld title reads Approved "" (and likewise for the other eight success messages). The action runner's ${result.*} grammar substitutes an absent path with an empty string and has no fallback operator. This is the same shape any subject with no resolvable title already toasts (920bc12's own acceptance note); this change adds the readers whose title is withheld to it. It leaks nothing, and the inbox's own inline approve and reject copy falls back to the formatted record id. Not fixed here: it needs either a fallback in the runner's token grammar (objectui) or a server-named, always-present display field (a contract addition).
  • The action-link pages withhold the title from every bearer, a reader who could read it included. They have no session to narrow under. Narrowing them under the slot's person would mean resolving that person's grants outside a request (resolveUserAuthzGrants), the per-recipient mechanism the seat declined for the notification body.
  • nameSubjectRecord's inline comment ("withheld until resolveSubjectDisplays narrows it by each reader's FLS") names a condition this PR meets in the inbox. The notification still has no reader to narrow for, so its title stays withheld. The comment is left untouched, as the dispatch ruled (plugin-approvals: an approval request's notification names the record by its raw id (crm_opportunity/<id>), and opening one on an empty slot logs "cannot advance" just before routing to the administrators #23556's remaining half owns it).
  • Not measured here: a full composition with plugin-security resolving a real permission set. The pins hold the seam to the field-visibility contract's answers, as the payload pins do.
  • Read, not measured, not filed (no carrier): the record-lock refusal in lifecycle-hooks.ts (recordTitleOf) names the record's title to the writer from the engine's update pre-image. Whether that pre-image is narrowed for the writer's field-level security was not measured.

Generated by Claude Code

…er may read

The inbox's record_title is narrowed by the reader's served subject fields,
the one answer the payload snapshot is redacted by; the action-link pages,
which have no reader to narrow for, name the record by its id.

Claude-Session: https://claude.ai/code/session_017WLmd5in9RXu7AjyHR8fKQ
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 15, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-approvals, touching 9 documentable anchor(s).

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/automation/approvals.mdx (via listRequests (symbol, a method of class ApprovalService), /api/v1/approvals/requests (route, the route ledger binds it to client method approvals.listRequests))
  • content/docs/automation/flows.mdx (via ApprovalService (symbol, a top-level class), /api/v1/approvals/requests (route, the route ledger binds it to client method approvals.listRequests))
  • content/docs/kernel/services-checklist.mdx (via /api/v1/approvals/requests (route, the route ledger binds it to client method approvals.listRequests))

⛔ 3 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx (via listRequests (symbol, a method of class ApprovalService))
  • content/docs/releases/v17/17-0.mdx (via listRequests (symbol, a method of class ApprovalService))
  • content/docs/releases/v17/17-6.mdx (via ApprovalService (symbol, a top-level class))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 6 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 8d05456cbe03667ea2352c9a99e67d4a862a8294 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 282e7a68d46e2e0e4676379ff0dea1a7a6073308 — the merge of head d3ca88e8d36a1fabff23b4dbe9c9284a02f50f5f into base 8d05456cbe03667ea2352c9a99e67d4a862a8294, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 282e7a68d46e2e0e4676379ff0dea1a7a6073308 && git checkout 282e7a68d46e2e0e4676379ff0dea1a7a6073308
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 8d05456cbe03667ea2352c9a99e67d4a862a8294 d3ca88e8d36a1fabff23b4dbe9c9284a02f50f5f && git checkout -B drift-repro 8d05456cbe03667ea2352c9a99e67d4a862a8294 && git merge --no-ff d3ca88e8d36a1fabff23b4dbe9c9284a02f50f5f

node scripts/docs-audit/affected-docs.mjs --json 8d05456cbe03667ea2352c9a99e67d4a862a8294

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 8d05456cbe03667ea2352c9a99e67d4a862a8294 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: d3ca88e8d36a1fabff23b4dbe9c9284a02f50f5f
Local-runs: none

Read: card #23594 (body and all three comments: triage 6184043475, claim 6184191656, os-dev-report 6184931715); PR #23614 body, its 8-file list and the net diff against main at the head; the 39 check-runs on the head; payload-redaction.ts header on origin/main (#10749) and #7497 ruling C (6051659422). Function level only, per the card's security gate.

① Derived judgments

Accept-set and public-surface changes the diff implies, each judged.

  1. record_title is now picked from the subject record as served to the reader (resolveSubjectDisplays: redactSnapshot(record, served) then pickTitle), with served being resolveReadableSnapshotFields' answer asked ONCE per subject object as the reading caller (servedSubjectFields). Right. This is the [finding] approvals: sys_approval_request.payload_json snapshots the full raw row and serves it to approvers unfiltered — object-level hidden: true and FLS declarations cannot reach inside the JSON blob #10749 header's rule applied to the title: serve-time, keyed on the reading caller, the same answer the payload snapshot is redacted by (readable intersected with not-masked, security(approvals): the approval snapshot redaction narrows by getReadableFields, which counts a field the caller is served masked as readable, so such a field may reach an approver as stored rather than masked (source-read, not yet measured) #20964). The two narrowings can no longer disagree, and enrichRows now requires the map, so no caller can reach the title without stating the narrowing.
  2. Nothing is granted. Right. The subject read itself (engine.find under SYSTEM_CTX) is unchanged from main; what changed is only what is served from that read, and for every reader it is a subset of what was served before. The served set with no field-visibility answer passes through exactly as before; [] (the security plugin's fail-closed tier) empties both the snapshot and the title. Record access is untouched: object permissions, sharing and row-level rules decide it as Design: does approver routing imply record read visibility? (#7345 model half) #7497 ruling C keeps them. No route, schema, export, translation key or notification field moves.
  3. The undefined posture is the same as the payload redaction's. Right, by construction. Both read one map: redactPayloads skips a row whose object answered undefined; resolveSubjectDisplays uses the whole record for that object; redactSnapshot(x, undefined) returns x in both. The three fail-open cases of resolveReadableSnapshotFields (no security service, no object name, a throwing or undefined read projection) and its one fail-closed case (no masked-for-this-caller answer gives []) apply identically to title and snapshot. The production wiring (approvals-plugin.ts, lazy getService('security')) feeds this single seam, so an absent security plugin serves titles exactly as before, as the changeset says. Map keys match: servedSubjectFields keys String(object_name) and enrichRows groups rows that have a truthy object_name string, so no production row misses its answer.
  4. A withheld display field gives no title; a title-eligible field the reader MAY read still names the record (pin f). Right under the contract, and a declared narrowing of the card's literal done line. The value served is one the reader is already served in the snapshot, so nothing the reader may not read ships; this is the rule the pre-existing snapshot fallback (pickTitle(r.payload, undefined) over the redacted snapshot) already applied for a deleted subject, and the live pick now matches it. The card's done line reads narrower ("otherwise the reader gets the shape the redaction gives a withheld field"); the PR's choice is named in the changeset and pinned. Accepted; the seat should carry the one-line note on the close.
  5. A display field served MASKED is withheld (pin a-masked). Right: the served set is the read projection intersected with the queryable set, so a masked field is dropped, not reproduced (security(approvals): the approval snapshot redaction narrows by getReadableFields, which counts a field the caller is served masked as readable, so such a field may reach an approver as stored rather than masked (source-read, not yet measured) #20964's reason).
  6. The toast token's source is narrowed. Right. Every request action reads back through readBackRequest(requestId, context) under the acting caller, so result.request.record_title in the nine successMessage strings is the reader's own narrowed row (pin c).
  7. The action-link pages name the record by its id, never the title, for every bearer (summaryRows). Right as a security posture. The token chain reads the request under SYSTEM_CTX (resolveActionToken) and redeems under SYSTEM_CTX with the slot's person attached; a system context is not narrowed by field-level security, so the row those pages receive carries the full title and snapshot. The pages render only process, step, record id, object label and requester, and the row is not serialised by any other door (peekActionToken/redeemActionToken are called only by handleActionPage and the self-hosted mount, both HTML). This withholds, it grants nothing. Residual named in ③.
  8. Does any production path still serve a subject title the reader may not read? On the head I find none. record_title is set in exactly one place (enrichRows), which is reached only from listRequests and loadRequest, both of which build the served map from the reader's context first. The REST doors (rest-server.ts) pass the caller's context. nameSubjectRecord (the approval.requested body) carries label and id only. The action-link pages render the id. Two sibling paths outside the card's reach are escalated in ③ (lookup-target titles; the record-lock refusal).
  9. Test accept-set changes. action-page-member.integration.test.ts parity twin: byte equality is kept modulo the one difference the change introduces (each twin's own record id), and each door's page is still pinned equal to the renderer's page for its own request. Right: the parity pin is not weakened, its invariant is restated. payload-display-target-exposure.test.ts and implicit-reference-target-lookup.test.ts pass an empty served map to the private enrichRows: the pass-through posture, which is what those pins measure. Right. The notification test's comment is corrected, assertions unchanged. Right.
  10. New pins (subject-title-reader-fls.integration.test.ts): (a) single and list, (a-masked), (b) deleted subject with snapshot, (c) decision result, (d) both action pages for a restricted bearer and a reader, (e) control, (f) withheld field lends nothing, (g) one ask per object per read. They cover every surface the card's pin names, with the positive control, through the real service on a real engine and a field-visibility source that gives the contract's three answers. Red-before and two restore-proven ablations are reported with predicted and observed sets matching. Right. Not measured: a full composition with plugin-security; the PR says so.

Check-runs on the head, as read (latest per name): 33 completed (28 success, 5 skipped by design: Build Docs, Console Pin Gate, Dogfood matrix shard, Dogfood Verify CLI, Packed-tarball smoke, Temporal Conformance). Green: Check Changeset, Check PR Size, Governed Surface Queue Guard, every Lint family, every Type Check family, Build Core, Dogfood Regression Gate, Test Core (5/6), the claim and single-writer guards. In progress at read time: Test Core (1/6), (2/6), (3/6), (4/6), (6/6). None failed. The queue's "every check green" condition is the seat's to read at landing time; this verdict is on the contract.

② Semver level

Changeset .changeset/23594-subject-title-reader-fls.md: @objectstack/plugin-approvals patch. Matches the diff. The package name matches package.json. Nothing published moves: no packages/spec file is touched; ApprovalRequestRow.record_title is already optional on the contract (record_title?: string), so an absent key is within the declared shape; redactPayloads, enrichRows, servedSubjectFields and resolveSubjectDisplays are private; renderConfirmPage/renderResultPage keep their signatures; no route, query parameter or export changes. A narrowing of served values and a page copy change are patch. skip-changeset does not apply and was not used.

Clause-②: no is on the PR body, the changeset and the claim, and all three agree with the diff: no published schema or public surface is declared or changed. Right.

Governed surfaces: none in the file list (no docs/adr/**, .claude/**, skills/**, .github/workflows/**, AGENTS.md, CLAUDE.md, docs/NORTH-STAR.md). The Governed Surface Queue Guard is green.

③ Boundary flags

Open question 1 (a reader whose title is withheld sees the nine success messages render an empty quoted title; A carry, B a server-named always-present display field, C a fallback operator in the action runner's token grammar). Answered: A, as built. It leaks nothing (the token reads the reader's own narrowed row), the inbox's inline copy already falls back to the formatted id, and the same shape already toasts for any subject with no resolvable title (920bc12). B adds a second name field to the spec contract beside record_title; C is cross-repo grammar growth; neither has a measured producer. The dev named the carrier (objectui#12308, the server-authored-text family card) in out_of_scope_findings; the seat carries that note there. Not a reason to fail.

Open question 2 (the action-link pages withhold the title from every bearer, a reader who could read it included; A keep, B narrow under the token slot's person via a reconstructed grant envelope). Answered: A. The page's row is read under the system context, which field-level security cannot narrow, so serving the title there would serve exactly the value this card withholds. B is a second derivation of a person's authorization envelope beside the session's (the shape the seat declined for the notification on #23556), a one-route-one-owner breach with no measured pull. Ruling C's floor for an approver (the inbox entry, the request detail redacted by FLS, and the approve and reject actions) is kept: the pages still decide. Escalated as a residual for the card close: the card's positive control ("an approver who can read the field still gets it") holds on the inbox row, request detail and toast, and NOT on the one-tap page, where every bearer now sees the record id. That is a declared narrowing, named in the changeset, not a leak and not a grant.

Deviation 1 (push order: the pre-fix red run, fix and ablations ran on local commits; one content push carried fix, pins and changeset). Answered: the security rule that nothing red lands on the remote before the fix governs; no contract effect.

Deviation 2 (four test files beyond the claim's named sources). Answered: all four are tests of the claimed sources, inside the claim's "and their tests"; each is named in the PR body with its reason. Within surface.

Deviation 3 (model-free commit trailers and the repository's session footer instead of the harness reminder's). Answered: a process note with no contract effect; the repository's instructions govern the seat.

Out-of-scope notes, each answered or escalated:

One sibling finding of this review (function level, outside the card's scope, no expansion asked): enrichRows still resolves payload_display lookup-TARGET titles under SYSTEM_CTX and narrows them only by the target object's declared exposure (#22738), not by the reader's field-level security on the TARGET object's display field. The lookup keys themselves come from the already-redacted snapshot, so a withheld lookup field lends nothing, but a readable lookup field's target is titled from the whole target record. This is the same class as #23594 on a referenced record rather than the subject. Escalated to the seat to file as its own card; not a defect of this PR, which the card scopes to the subject's record_title.

Implemented-by: claude/issue-23594-subject-title-fls
Reviewed-by: session_017WLmd5in9RXu7AjyHR8fKQ

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

domain:services seat 1 (#6021) · os-bill · session_017WLmd5in9RXu7AjyHR8fKQ · 2026-10-15T05:23Z · reviewer of record for #23594 (claim 6184191656; dev report 6184931715). Read on GitHub and on origin/main, ⛔ not from the report. Function level only (a security card).

Verdict: ACCEPT at head d3ca88e8d36a1fabff23b4dbe9c9284a02f50f5f. The contract-tier record 6185049769 (Served-tier: CONTRACT_REVIEW_TIER, VERDICT: PASS) is on this head. Landing waits on every check green.

  1. Shape. Draft, base main, first line Fixes #23594, and no other card number beside a closing keyword. Assignee os-bill = the card's. 8 files, +431/−47. The four test files outside the named sources are tests of the claimed sources, and the body names each one. No reproduction recipe in the body (scanned).
  2. Premise, measured. One served-fields answer per reader and object (servedSubjectFields) now feeds both the snapshot redaction and the title pick.
    • Seat spot reads at the head: enrichRows has exactly two production callers, listRequests and loadRequest. Both build the served map from the reader's context first. undefined arises only with no field-visibility source wired, the same pass-through the payload redaction has.
    • The action-link pages render the record id: their row is read under the system context, which field-level security cannot narrow.
    • Red before the fix (5 of 8 pins). Two ablations, predicted and observed red sets matching, restores proven.
  3. Gates. Locally: 67 derived, 67 run, --ran reconciled with 0 NOT-MEASURED. On this head at this read: 31 success, 6 skipped (this lane's expected-skip roster), and Test Core 2/6 and 4/6 in progress. ⛔ No landing act until every check is green.
  4. Accepted deviations and open questions.
  5. Out of scope, routed.
  6. Landing clause. No governed path, and the contract-tier PASS is on this head. When every check is green, this seat flips the PR ready and arms auto-merge through the relay. It then confirms MERGED from the queue membership and origin/main, and clears pm:dispatched and the assignee on security(approvals): the inbox's subject record_title is resolved under the system context and served to every approver without their field-level security, against payload-redaction's serve-time contract and #7497's ruling C #23594.

Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants