Repository navigation
fix(plugin-approvals, plugin-audit)!: a lookup title is served only for a target whose declared exposure serves get - #22766
Conversation
…approvals inbox and the activity summary Claude-Session: https://claude.ai/code/session_01JfJfBUC3cQ6hhgm9MQK76T Co-authored-by: Claude <noreply@anthropic.com>
…target's declared exposure serves get ApprovalService.enrichRows (payload_display) and resolveLookupTitles (the activity summary) ask the spec's one exposure decision of the referenced object before reading its title; a refused or unreadable declaration withholds the title and the stored id stands. Claude-Session: https://claude.ai/code/session_01JfJfBUC3cQ6hhgm9MQK76T Co-authored-by: Claude <noreply@anthropic.com>
…us rows 5 and 6 decided Claude-Session: https://claude.ai/code/session_01JfJfBUC3cQ6hhgm9MQK76T Co-authored-by: Claude <noreply@anthropic.com>
…arrowing of two served lookup titles Claude-Session: https://claude.ai/code/session_01JfJfBUC3cQ6hhgm9MQK76T Co-authored-by: Claude <noreply@anthropic.com>
…okup-title-exposure
…le (check:objectql-double-limit) Claude-Session: https://claude.ai/code/session_01JfJfBUC3cQ6hhgm9MQK76T Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 12 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 12 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 fc94295a46ee89e0844b3d0791a94e8ea2ea17e0 && git checkout fc94295a46ee89e0844b3d0791a94e8ea2ea17e0
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a18c514965f90e09fa9432c3e72e34426ebc03d6 4437a3219feeef4f52a06d69c9912a14aa2ad7e3 && git checkout -B drift-repro a18c514965f90e09fa9432c3e72e34426ebc03d6 && git merge --no-ff 4437a3219feeef4f52a06d69c9912a14aa2ad7e3
node scripts/docs-audit/affected-docs.mjs --json a18c514965f90e09fa9432c3e72e34426ebc03d6
|
Contract reviewServed-tier: Inputs, and nothing else: card #22738 (body; comments 6104517855 claim, 6105465711 dev report), parent card #22661 (body; comments 6096329009 triage, 6101260211 claim, 6102815147 dev report, 6102873261 census split, 6102951996 seat ACCEPT, 6104471901 landing record) and its contract review 6102920614 on PR #22735 as the family's precedent, PR #22766 (body, its 7-file list, the net diff from merge-base ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
Fixes #22738
Clause-②: no (narrowing)
Census rows 5 and 6 of #22661. Classes, positions and functions only.
What changed
Two reads followed a lookup to the TARGET object's title under a system context and never asked the target's declared exposure. Each now asks the spec's one decision,
canServeApiOperation(@objectstack/spec/data, ADR-0049), forgetbefore it reads: the operation the data door's$expandand the dataset door's labels already ask of the same target (#22735). No second rule, no new error code, and no new package dependency (both packages already depend on@objectstack/spec).plugin-approvals,approval-service.ts).ApprovalService.servesPayloadDisplayTargetis asked inApprovalService.enrichRowsbefore the referenced-title read. A refused target is not read. Its key gets nopayload_displayentry, and the snapshot's stored id stands, which is what a deleted target already answers.plugin-audit,audit-writers.ts).servesSummaryTitleTargetis asked inresolveLookupTitles, where both the tracked-change branch and the milestone branch end. A refused target is not read, and the summary names the record by its stored id, which is what an unresolvable reference already answers. Only rows written from now on change. No stored row is rewritten, and no other activity column changes. The audit lane only tightens what it serves here and widens nothing.enableblock throughengine.getSchema(target), which isObjectQL.getSchema, the schema registry'sgetObject. That is the registry the data door's exposure gate and the analytics door's declaration provider read. A throwing read withholds the title and logs onewarnline (fail-closed, asservesLabelTargetdoes). An engine withoutgetSchemaresolves no lookup field in either package (resolveLookupFieldsand the summary's read plan come back empty), so nothing is served on that branch either.packages/core/src/security/second-object-read-exposure.pin.test.ts). Rows 5 and 6 move fromopentodecided. Each row is held to its decision function, its call site and its behaviour pin.Cross-domain path
The fix lands in two
domain:servicespackages,plugin-approvalsandplugin-audit. The path was declared in the claim and posted to the services seat (#6021) before any edit. Nothing in either package outside the title resolution is touched.Deviation from the suggested route
For a refused key, the inbox sends NO
payload_displayentry rather than writing the stored id intopayload_display.payload_displaymaps a key to a resolved display value, and the console's inbox card drops an unresolved reference rather than render its id. Writing the id there would make the card render an opaque id as if it were a resolved title. The stored id still stands inpayload, which is the answer a deleted target gets.Measurement (real stack, fixture objects, administrator and member)
bf515e724d): both positions served the title of every unexposed target shape (the off switch, a whitelist withoutget, and the deny-all whitelist) to both personas. The exposed control served its title.getas the decision does (404, 405, 405, 200), and the request and the activity row exist at rest.bf515e724d). Ten in-repo objects refusegetby declaration. Exactly one lookup points into any of them, from an object that is itselfapiEnabled: false(control: 88 lookups intosys_user). No shipped object's served title changes.Pins and ablation
plugin-approvals/src/payload-display-target-exposure.test.ts: 2 cases. A refused target is not read, and the served ones keep their titles. An unreadable declaration withholds, with awarnline.plugin-audit/src/audit-lookup-summary.test.ts: 2 new cases, the same two properties, through a realObjectQL(19/19 in the file).packages/qa/dogfood/test/lookup-title-exposure.dogfood.test.ts: 4 cases (inbox list and item, activity summary; administrator and member). 4/4 pass atfb7da543d0after a post-merge rebuild of the closure.scripts/ablation-replace.mjs(anchor hit 1 to 0, blob changed; every restore proved blob equal to HEAD with an emptygit diff HEAD):4437a3219f).ablation-dist-preflightfound the audit marker in 2 built files: dogfood 4 red of 4.--absent, tree clean, dogfood 4/4 green.dist.@objectstack/plugin-approvalsalso resolves to source in the dogfood project (alias).Verification (final head
4437a3219funless noted)dispatch-gates --commands --repo objectstack-ai/objectstackderived 73 families (they include all 53 named in the dispatch). All 73 exit 0, each exit captured before any pipe.--ranreports 73 derived, 73 run, 0 NOT-MEASURED, 0 UNRUN, a derived zero.check:dual-build-cjs-loadsanswered PREREQUISITE NOT MET until a full build (72 tasks). On the first run,check:objectql-double-limitflagged the approvals pin's engine double as unjudged. The double now answers a table whole, because what the pin holds is which objects are read.plugin-approvals72 files / 1025 tests andplugin-audit45 / 716 atc4f5cfa94a, before mergingorigin/main, which touches neither package.corerepo project 5 files / 55 tests.plugin-approvals,plugin-audit,coreanddogfood, each new test present in its program (--listFiles).eslint --no-inline-config --format jsonover the 6 changed.tsfiles: 6 files, 0 errors, 0 warnings (5 to 6 active rules each, read from--print-config).eslint.config.mjsenables no type-aware linting, so no untouched file's verdict can move. The repo-widepnpm lintis CI's.Acceptance notes
sys_userdisplay names (submitter, approvers, the activity actor) are fixed-target reads. The enumeration pin names them and does not hold them.sys_userservesget, so auserreference field is unchanged.Generated by Claude Code