Repository navigation
fix(plugin-approvals): lookup-target titles and the record-lock refusal title are served only from fields the caller may read - #23629
Conversation
…tles served past the caller's served fields Claude-Session: https://claude.ai/code/session_017WLmd5in9RXu7AjyHR8fKQ Co-authored-by: Claude <noreply@anthropic.com>
…al title are served only from fields the caller may read The payload_display titles are picked from each lookup target as served to the reader, asked through the one served-fields seam once per object per read. The RECORD_LOCKED refusal picks its title from the update pre-image as served to the writer, asked with the writer's own envelope. nameSubjectRecord's inline comment now names the real reason the notification stays label-and-id. Claude-Session: https://claude.ai/code/session_017WLmd5in9RXu7AjyHR8fKQ Co-authored-by: Claude <noreply@anthropic.com>
…cord-lock titles Claude-Session: https://claude.ai/code/session_017WLmd5in9RXu7AjyHR8fKQ Co-authored-by: Claude <noreply@anthropic.com>
…proval-title-family
📓 Docs Drift CheckThis PR changes 1 package(s): 4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 6 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 1623977b65b76b404a3a86d7df7833ece18bafb8 && git checkout 1623977b65b76b404a3a86d7df7833ece18bafb8
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e611e2bdae9c1adb3d28d0e6b8e0cb4cb2f77c2d c5115facf00ce3b6047053b9f5630d32a80d6c70 && git checkout -B drift-repro e611e2bdae9c1adb3d28d0e6b8e0cb4cb2f77c2d && git merge --no-ff c5115facf00ce3b6047053b9f5630d32a80d6c70
node scripts/docs-audit/affected-docs.mjs --json e611e2bdae9c1adb3d28d0e6b8e0cb4cb2f77c2d
|
Contract reviewServed-tier: Read: card #23618 (body and all three comments: triage ① Derived judgmentsAccept-set and public-surface changes the diff implies, each judged.
Check-runs on the head, as read (latest per name, 39 runs): 33 completed success, among them Check Changeset, Governed Surface Queue Guard, Build Core, every Lint family, every Type Check family, Dogfood Regression Gate, Test Core (5/6) and (6/6), the claim and single-writer guards; 6 skipped by design (Build Docs, Console Pin Gate, Dogfood matrix shard, Dogfood Verify CLI, Packed-tarball smoke, Temporal Conformance). In progress at read time: Test Core (1/6), (2/6), (3/6), (4/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 levelChangeset
Governed surfaces: none in the 8-file list (no ③ Boundary flags
Deviation 1 (push order: pins, fix and changeset in one push after the empty-branch probe). Answered: the card's security rule that nothing red lands on the remote before the fix governs; no contract effect. Deviation 2 (file surface beyond the claim's two sources and their tests). Answered: the Deviation 3 (the bounded comment change in Deviation 4 (model-free commit trailers and the repository's session footer). Answered: a process note with no contract effect; the repository's instructions govern the seat. Deviation 5 (a throwaway measurement file in Deviation 6 (merged
Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对
Verdict: ACCEPT at head
Generated by Claude Code |
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 38704103366 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 分类: 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
|
Queue ejection triaged · Signature. The queue build Why it is not this PR's:
Re-check before the re-queue. The head is unchanged ( ⛔ Once only. If the same signature ejects this PR again, this seat stops and hands it over rather than re-queue a second time. Generated by Claude Code |
|
Correction to
Generated by Claude Code |
Fixes #23618
Clause-②: no
The two approval-path titles this card enumerates are now served only from fields the caller may be served, by the served-fields rule PR #23614 applied to the subject's
record_title, asked through one seam. Function level only, per the card.What changed
All in
packages/plugins/plugin-approvals.payload_display).enrichRowsnow titles each record a readable lookup points at from the TARGET record as served to the reader.ApprovalService.servedFieldsis now the one place a row read asksresolveReadableSnapshotFields(readable, and not masked for this caller), as the reading caller, once per object per read.servedSubjectFieldsis that method for the rows' subject objects.enrichRowsasks it for the target objects into the same map, so a target that is also a subject object (a self-lookup) reuses the subject's answer and is not asked twice.titlesAsServedis now the one spelling of how an approval read titles a record: read under the system context,redactSnapshot(record, served), then the existingpickTitle. The subject'srecord_title(resolveSubjectDisplays) and the targets'payload_displayboth call it, so the two cannot drift.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), gives that key no display value, and the snapshot's stored id stands. That is the answer a deleted or exposure-refused target (security(approvals, audit): a lookup target's title is resolved into the approvals inbox payload and the activity summary without asking the target's exposure (census rows 5-6 of #22661) #22738) already gets. A title-eligible field the reader may read on the target still names the record, as for the subject (PR fix(plugin-approvals): a request's subject title is served only from fields its reader may read #23614, pin f). A reader who may read the display field gets the same title as before. The exposure gate is unchanged and still runs first, so a refused target is neither read nor asked.RECORD_LOCKEDrefusal (lifecycle-hooks.ts). The per-record sentence now picks its title from the update pre-image as served to the WRITER.resolveReadableSnapshotFields, thenredactSnapshot), made with the writer's own execution envelope: the one riding on the operation (input.options.context), stripped of its operation-private keys with@objectstack/core'swithoutOperationPrivateKeys. ⛔ It is never rebuilt from the hook session, which dropspermissionsandposture; a question asked with that projection would be a second derivation of the writer's authority.This Opportunity is locked ...). That is the degradation [finding]RECORD_LOCKEDrejection toast surfaces the internal record id and the object API name to end users #18153 already defines; ⛔ never the id. A writer who may read the field is told the title as before.RECORD_LOCKEDand 409 do not move.bindApprovalLockHooktakes a lazy field-visibility getter.approvals-plugin.tspasses the same lazily resolvedsecurityauthority the snapshot redaction middleware reads. With none wired, the title passes through as before, exactly as the payload snapshot does.6185594767on 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, option B).nameSubjectRecord's inline[#23594]comment named a condition PR fix(plugin-approvals): a request's subject title is served only from fields its reader may read #23614 already meets in the inbox. It now gives the real reason theapproval.requestedbody stays label-and-id: it is written once at send time, with no per-recipient reader to narrow for. Two comment lines; the return value is unchanged. No test and no changeset line of its own.The dispatch's mechanism assumptions, measured
objectuiat28fb2a0,payloadSummary). This change does not move that.readUpdatePriorRow(@objectstack/objectql) is a raw driver read shaped to the declared columns. No read middleware runs on it and nothing inplugin-securitytouchesprevious.SecurityPluginand a real engine and driver, in a throwaway rig inplugin-security's tree (deleted, never committed).previouscarried the field.input.options.contextcarriedpermissionsandposture, which the session does not.plugin-security'sby-id-write-unreadable-not-foundandpredicate-write-unreadable-not-matchedpins hold that.Pins
lookup-target-title-reader-fls.integration.test.ts: the realApprovalServiceon a realObjectQLoverSqlDriver(in-memory better-sqlite3), with a field-visibility source standing in forplugin-security's.getRequestandlistRequests, and the stored id stands. A self-lookup is narrowed by the reader's answer for the subject object.record-lock-refusal-writer-fls.integration.test.ts: real engine, real driver, the realbeforeUpdatehook.RECORD_LOCKEDand 409 are asserted on every refusal.payload-display-target-exposure.test.tsandimplicit-reference-target-lookup.test.tspassenrichRows' new required context. No security service is wired there, so nothing is narrowed, which is the posture those pins measure.Red before, ablations after
c0c5a0ba28): 8 failed / 2 passed. The two passes are the controls (b); every other pin was red.d3a9c5f12a, viascripts/ablation-replace.mjsin wrap mode, under an EXIT/INT/TERM restore trap. Each run covers the two new files plussubject-title-reader-fls.integration.test.ts, 18 tests.undefined(the whole-target pick, asks kept). Predicted red: row 1 (a), (a-masked), (c). Observed: 3 failed / 15 passed, exactly those. Restore proven: blob0a8b5fc6121dequals HEAD andgit diff HEADis empty.0a8b5fc6121d.25e7a598c358.diststep: every subject is imported fromsrcby relative path.Tests and gates
pnpm --filter @objectstack/plugin-approvals exec vitest run --maxWorkers=2, the whole package, at the final headc5115facf0(after mergingorigin/main72329942f9, thenpnpm install --frozen-lockfileand a rebuild of the package and its closure, 32 tasks, exit 0): Test Files 77 passed (77), Tests 1072 passed (1072), lockVERDICT command-exit 0. It read the same atd3a9c5f12a, before the merge.pnpm --filter @objectstack/plugin-approvals typecheck: exit 0.check:test-typecheckOK (8 files / 324 errors / 27 signatures held; no new signature).tsc --listFilesontsconfig.test.jsoncompiles all four new or edited test files.c5115facf0:node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 67 commands off this change set. All 67 ran, each exit code captured before any pipe, and all 67 exit 0.check:dual-build-cjs-loadsandcheck:i18nfirst answered 3, PREREQUISITE NOT MET (no workspace dist). After a workspace build (75 tasks, exit 0), both answered 0, and they are recorded at that measured 0, not as failures.--ranreconciliation, with every exit code recorded: 67 derived, 67 run, 0 NOT-MEASURED (derived), 0 UNRUN, exit 0.--no-inline-config) over the 7 changed TS files: exit 0, no findings.check:dev-prereqsandcheck:skill-frame-freshness, because nopackages/specbuild input or frame file is touched. The full farm is CI's.Changeset
.changeset/23618-approval-title-family-served-fields.md:@objectstack/plugin-approvalspatch. It narrows served values only. No schema, route, error code, translation key, export or notification field moves.payload_displayand its values are already optional, andbindApprovalLockHook's new parameter is optional. With no field-visibility source wired, every title is served as before.Clause-②: no, on this body, the changeset and the claim.Deviations
approvals-plugin.ts, one wiring line that hands the lock hook the authority the redaction already reads, and the two pass-through test files named above.6185594767.packages/plugins/plugin-security/srcfor one run and was deleted. The tree was verified clean.Acceptance notes
plugin-securitycomposed. The pins hold the seam to the field-visibility contract's three answers, as the payload and subject-title pins do. Row 2 was measured composed (assumption 2).undefined, passes the title through, logged atwarnas for the snapshot. A source with no masked-for-this-caller answer fails closed ([], no title). A write that carries no execution envelope asks the security service with none, and its answer for no caller decides.Generated by Claude Code