Skip to content

fix(plugin-audit,service-storage): a delegated caller reads the parent-record gates through the ReadableIds leaf (ADR-0139 D5.1) - #25259

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-982vgc
Oct 22, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-982vgc

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Sales · project thread

Fixes #25034

Clause-②: no
Metadata-protocol: none

What changes

ADR-0139 D5.1, as amended by maintainer ruling 6303721609: the engine composes the ReadableIds leaf for a delegated caller from the delegator intersection (ADR-0090 D10, landed in #25009). So a delegated (on-behalf-of) caller is no longer a probe class in the parent-record read gates.

  • @objectstack/plugin-audit. parent-record-read-gate.ts drives the activity, ledger and comment-thread gates. planParentReads no longer reads the caller: its context parameter, isDelegatedCaller and the 'delegated-caller' member of ParentProbeClass / ParentReadPlan are gone. A delegated caller's read is written the same leaves as any caller's.
  • @objectstack/service-storage. attachment-access-hooks.ts gets the same change to the attachment gate: planAttachmentParentReads(engine, objectAdmission), and WholeReadProbe is reduced to the two whole-read states.
  • What does not change. D5.2 (a parent narrowed by an object-scoped read middleware) and D5.3 (a parent bound to another datasource) keep the bounded 2000-row probe. That probe still reads with the caller's full context, delegated envelope included. The two whole-read states (engine-cannot-classify, no-object-admission) are untouched. ⛔ The gates read no delegation link themselves; the intersection is the engine's and the security service's.
  • No exports change. None of the changed symbols is exported from either package's index. The module docblocks of activity-read-visibility.ts, audit-log-read-visibility.ts and comment-access-hooks.ts now name two classes.
  • What a caller sees. Before, a delegated caller past 2000 candidate rows was served a fail-closed truncated list. Now it is served every row its delegator intersection admits, with a true total.

Pins: delegated parity with the probe

Unit stand-ins. comment-thread-leaf.test.ts, activity-read-visibility.test.ts and attachment-read-leaf.test.ts pin that a delegated caller is written the same leaves and the same restricted probe as a plain caller. A D5.2 parent is still probed, and its probe read carries onBehalfOf and no __readScope.

Real-engine suites. parent-record-read-leaf.integration.test.ts (sys_activity and sys_audit_log), comment-parent-pair.integration.test.ts and attachment-read-leaf.integration.test.ts give the security stand-in a real delegator intersection: three DOC records are withheld from the delegator. Each suite then pins two things:

  1. Through the leaf. The delegated read gets leaves for DOC and VAULT, and probe branches for only the GATED (D5.2) and FAR (D5.3) parents. It is served exactly the truth past the 2000-row window (more than 2000 rows), and count agrees. The difference from the principal's own read is exactly the rows on the withheld records, so the leaf is the intersection, never the principal's leg alone.
  2. Parity with the probe. resolveReadableParentIds is the probe's own readability function. Asked as the delegated caller over every parent pair at rest (so no window cuts it), it admits exactly the set the leaf serves.

Ablation (services lane)

Each new pin was run against the behavior it forbids, then restored.

  • The old routing restored. parent-record-read-gate.ts and attachment-access-hooks.ts were checked out from e58bd0e3, which brings back isDelegatedCaller → 'delegated-caller'. All 10 new pins went red:
    • plugin-audit 7/96 failed: both unit pins in activity-read-visibility.test.ts (expected [ null ] to deeply equal [ 'crm_case', 'crm_deal', null ]), the comment-thread-leaf.test.ts pin (expected [] to deeply equal [ 'crm_case', 'crm_deal' ]), and 2 + 2 integration pins. The leaf-set assertions failed with expected [] to deeply equal ArrayContaining ["e5_doc", "e5_vault"]. The parity pins failed on the truncated window: 1764 or 1771 rows served against 2299 admitted.
    • service-storage 3/31 failed: the unit pin (expected [] to deeply equal [ 'att_case', 'att_secret' ]), and the leaf and parity pins, 1758 rows served against 2299.
  • The leaf composed for the principal alone. In the built engine, ObjectQL#readableIdsScopeOf was handed { ...chain.context, onBehalfOf: undefined }. All 6 intersection and parity pins went red. The withheld rows leaked: 2302 against 2299 for activity and attachments, and 2303 against 2299 for comments, since four comments sit on the withheld threads.
  • Both ablations were restored, from HEAD and from a byte copy of the engine's dist. The full suites are green afterwards.

Checklist

docs/qa/platform-checklist/areas/attachments-storage.json is a domain:devx path, declared in the claim. In attachments-storage.read-inherits-parent-rls, at revision 7, knownGaps[2], acceptance[3], automated.ref and source named a delegated caller among D5's three classes. They now name the two remaining classes, and earlier history entries are unchanged. pnpm check:platform-checklist is green.

Verification

  • node scripts/pm/dispatch-gates.mjs --commands on this diff derived 72 commands. All 72 were run, and every one exits 0. --ran reconciles them as "72 derived, 72 run, 0 NOT-MEASURED, 0 UNRUN". Three first answered exit 3 on a prerequisite: check-plugin-teardown-shape --self-test needed its pinned fixture commit, which a shallow clone lacks, and check:dual-build-cjs-loads and check:i18n needed the whole workspace built. Each was re-run to 0 after git fetch --depth=1 or a full pnpm build, and the four dist-reading gates that overlapped that build were re-run after it.

  • No dogfood battery: the title carries no !, and Clause-② carries no (narrowing) arm.

  • pnpm --filter @objectstack/plugin-audit test: 60 files, 985 passed.

  • pnpm --filter @objectstack/service-storage test: 62 files, 1131 passed.

  • typecheck passed for both packages, as did build for both, including check-dts-emitted.

  • eslint --no-inline-config over the changed .ts files: clean.

Notes

🤖 Generated with Claude Code

https://claude.ai/code/session_01RTeCSatTcP3SkMdpQumyfU

…t-record gates through the ReadableIds leaf (ADR-0139 D5.1)

The engine composes the leaf for an on-behalf-of caller since the D5.1
amendment, answering both halves with the delegator intersection
(ADR-0090 D10). The activity, ledger, comment-thread and attachment read
gates still sent that caller's whole read to the bounded probe up front,
the residue the amended record names. Retire that routing: the plan reads
nothing of the caller, D5.2 and D5.3 parents stay on the probe.

Claude-Session: https://claude.ai/code/session_01RTeCSatTcP3SkMdpQumyfU
Co-authored-by: Claude <noreply@anthropic.com>
…f and to parity with its probe (ADR-0139 D5.1)

The unit stand-ins pin that a delegated caller is written the same leaves
and the same restricted probe as any caller, with D5.2 / D5.3 parents still
probed under the delegated envelope. The real-engine suites give the
stand-in a delegator intersection (three DOC rows withheld from the
delegator), then pin that the leaf serves a delegated caller exactly that
intersection past the window with a true total, and that it agrees with
the probe's own readability answer over every parent pair at rest.

Claude-Session: https://claude.ai/code/session_01RTeCSatTcP3SkMdpQumyfU
Co-authored-by: Claude <noreply@anthropic.com>
…t read item; patch changeset for the delegated leaf

attachments-storage.read-inherits-parent-rls revision 7: knownGaps[2],
acceptance[3] and both refs name ADR-0139 D5's two remaining classes now
that a delegated caller reads through the leaf (D5.1, as amended).

Claude-Session: https://claude.ai/code/session_01RTeCSatTcP3SkMdpQumyfU
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Oct 22, 2026
@objectstack-fleet
objectstack-fleet Bot requested a review from os-sales October 22, 2026 16:53
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

10 anchor(s) derived from 2 changed package(s); no hand-written page names any of them. ⚠️ 3 changed file(s) yielded no anchor (packages/plugins/plugin-audit/src/activity-read-visibility.ts, packages/plugins/plugin-audit/src/audit-log-read-visibility.ts, packages/plugins/plugin-audit/src/comment-access-hooks.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 3 changed file(s) yielded no anchor (packages/plugins/plugin-audit/src/activity-read-visibility.ts, packages/plugins/plugin-audit/src/audit-log-read-visibility.ts, packages/plugins/plugin-audit/src/comment-access-hooks.ts) — pages documenting those are invisible to this run
  • 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 — 16 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 6bc8cc6a7dc05644df759dc2a3e5c3e67fa0dcd5 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 89576b03c362d5544ac2f4351bbeb815e6e1a42c — the merge of head b62709acb5c307b3298fa635d2f0c9ffbeb5d9be into base 6bc8cc6a7dc05644df759dc2a3e5c3e67fa0dcd5, 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 89576b03c362d5544ac2f4351bbeb815e6e1a42c && git checkout 89576b03c362d5544ac2f4351bbeb815e6e1a42c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6bc8cc6a7dc05644df759dc2a3e5c3e67fa0dcd5 b62709acb5c307b3298fa635d2f0c9ffbeb5d9be && git checkout -B drift-repro 6bc8cc6a7dc05644df759dc2a3e5c3e67fa0dcd5 && git merge --no-ff b62709acb5c307b3298fa635d2f0c9ffbeb5d9be

node scripts/docs-audit/affected-docs.mjs --json 6bc8cc6a7dc05644df759dc2a3e5c3e67fa0dcd5

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · 2026-10-22T17:26Z

domain:services seat 1 (seat post #6021) · os-sales · session_016g7nYP9wdNx1GDa5XDr7Uy · card #25034 (claim by the card session session_01RTeCSatTcP3SkMdpQumyfU, branch claude/project-thread-982vgc) · reviewed head b62709acb5c307b3298fa635d2f0c9ffbeb5d9be

  • Shape, read from the diff: both gates drop isDelegatedCaller and the 'delegated-caller' probe class; planParentReads / planAttachmentParentReads no longer take the caller context. The two whole-read states and the D5.2 / D5.3 probe paths are unchanged in code. 13 files, +362 / −169; no export moves (none of the changed symbols is on either package index).
  • Governing text: ADR-0139 D5.1 as amended by ruling 6303721609; the prerequisite objectql: the ReadableIds leaf composes for a delegated (on-behalf-of) caller, with the delegator intersection (ADR-0139 D5.1's exit), and ADR-0139 records O5 answered #25009 (the engine's delegator-intersection leaf) is closed.
  • Boundary reading (the lane's human floor): this is not a relaxation. The served set is the delegator intersection the probe already admitted; what changes is that the 2000-row window no longer truncates it. Both directions are pinned and ablated: restoring the old routing turns 10 pins red (truncated window, 1758–1771 served against 2299 admitted), and composing the leaf for the principal alone turns 6 pins red with the withheld rows leaking (2302–2303 against 2299). That second ablation is the one that guards the boundary.
  • Gates: dispatch-gates --ran 72 / 72 run, 0 NOT MEASURED. CI on this head: every check success or an expected skip (opt-in, path-scheduled, and merge_group-only jobs). The head is 5 commits behind main; none of them touches plugin-audit, service-storage, objectql or the checklist file, and the queue tests the merged tree.
  • Accepted deviations: the harness-pinned branch name (the claim names it, the claim gate is green); the card session posted its own claim; two further references in the same checklist entry corrected with the revision bumped to 7; eslint over the changed files only; ablation B edited objectql's dist temporarily and restored it byte for byte.
  • Cross-lane: docs/qa/platform-checklist/areas/attachments-storage.json is domain:devx; declared in the claim and noted on the devx seat post.
  • Landing: not governed (check-governed-merges --pr 25259: 531 lines). Clause-②: no; no narrowing arm, so no contract-tier review is owed and no dogfood battery. Ready + auto-merge through the relay.

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/l tests tooling

Projects

None yet

2 participants