Repository navigation
fix(plugin-approvals): an out-of-office delegation applies only in its own organization - #25439
Conversation
…s own organization Claude-Session: https://claude.ai/code/session_012KEX9o2giH3Wzt4jgeNTAK Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012KEX9o2giH3Wzt4jgeNTAK Co-authored-by: Claude <noreply@anthropic.com>
…stamp fixtures The en bundle is regenerated from the corrected organization_id help; the es-ES and ja-JP leaves, which were byte copies of the old English text, are translated, so their source-hash entries drop. Fixtures that wrote an organization-less delegation and expected it to reroute now write it in the request's organization. Claude-Session: https://claude.ai/code/session_012KEX9o2giH3Wzt4jgeNTAK Co-authored-by: Claude <noreply@anthropic.com>
…ganization's approvals Claude-Session: https://claude.ai/code/session_012KEX9o2giH3Wzt4jgeNTAK Co-authored-by: Claude <noreply@anthropic.com>
…tes through the engine predicates The fake engine's update and delete open with assertEngineUpdateDispatch / assertEngineDeleteDispatch, and its find holds the caller's limit, so it is no looser than ObjectQL. The pinned ledger records the new double. Claude-Session: https://claude.ai/code/session_012KEX9o2giH3Wzt4jgeNTAK Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 3 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 — 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 9fa607a49ef26f55ac0873a8d0a0266ec7107144 && git checkout 9fa607a49ef26f55ac0873a8d0a0266ec7107144
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 77392e901cce58316e803144971f2741ef049fc4 c7cf6654b99ab2d9da5e75445bdbcb8b57c9bc0d && git checkout -B drift-repro 77392e901cce58316e803144971f2741ef049fc4 && git merge --no-ff c7cf6654b99ab2d9da5e75445bdbcb8b57c9bc0d
node scripts/docs-audit/affected-docs.mjs --json 77392e901cce58316e803144971f2741ef049fc4
|
Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
席位核对 · ACCEPT · 2026-10-23T14:27Z
Generated by Claude Code |
Requested by Sales · project thread
Fixes #25280
Clause-②: no (narrowing)
Metadata-protocol: none
What changed
ApprovalService.lookupActiveDelegationnow reads asys_approval_delegationrow the waygrantAppliesInTenant(@objectstack/core,resolve-authz-context.ts) reads a stored position or permission-set grant: the row reroutes an approver only when itsorganization_idis the request's organization, and both must name one.NULL, absent, blank)warnnames the roworganizationNamed()normalizesNULL, absent and''to "none", as core's module-privategrantOrganizationOfdoes. It is applied to both sides of the comparison.warn. The structured fields are{ delegationId, delegatorId, delegateId, requestOrganizationId }, and the message gives the remedy. Its author still sees the row in Setup, and nothing else says it has stopped applying.sys_approval_delegation.organization_idhelp, which said "null = applies across tenants for this delegator", now says what the code does:enis regenerated withnode scripts/check-i18n-bundles.mjs --write.zh-CN,es-ESandja-JPare translated by hand. Thees-ESandja-JPleaves were byte copies of the old English, so their source-hash entries drop on extract.content/docs/automation/approvals.mdx(Out-of-office delegation) state the organization scope.Who is affected
Under a wall (
group/isolated), only stored state is affected. No current write produces an organization-less delegation:injectTenantOnInsertinsql-driver.ts).resolveSystemInsertOrganization, D9).denyOrganizationRemoval,security-plugin-write-tenant-wall.ts).What remains are rows stored before 18.0. The D10 ceremony has no anchor for this table under a wall (inventory fate 3), so it reports such a row and leaves it
NULL. Before this change, that row rerouted every organization's approvals for its delegator, onto a delegate who may hold no membership there.Under
single, a delegation written on 18.x applies exactly as before. D9 stamps the Default Organization on both the delegation and the record, and the request takes its record's organization. Only a pre-ceremony organization-less row stops applying, andos migrate organization-ownership --applyattributes it.The changeset declares the break (
**BREAKING**, ADR-0087not-required (no-migration-prescription)marker) and gives the operator remedy for each posture. Precedent:.changeset/24583-7a2-organization-less-grant-readers-retired.md.Tests
New file
ooo-delegation-organization.test.ts(18 cases). Each walled case runs under bothisolatedandgroup:single, a Default-Organization delegation reroutes, and an organization-less row does not and is reported.approval-service.test.ts: the pin "a null-org rule applies across tenants" is replaced by its inverse.Fixtures that wrote an organization-less delegation and expected a reroute now write it in the request's organization:
admin-exemption-retired.test.ts:seedDelegationwrites't1'.admin-rescue-named-slate.integration.test.ts:dlg_adminwritesORG, andsys_approval_delegationleaves therelaxTenantColumnslist.approval-notification-body.integration.test.ts:dlg_1anddlg_2writeorgOfDeal(...), andsys_approval_delegationleaves therelaxTenantColumnslist.The three
installDefaultOrganizationsuites that insert a delegation (actor-lookups-hold-ids,approval-notification-keyed-text,approval-requested-notification) pass unchanged. D9 stamps the Default Organization on both sides there.scripts/engine-double-contract.pinned.jsonrecords the new test file's double (check-engine-double-contract --write). Itsupdate/deleteopen withassertEngineUpdateDispatch/assertEngineDeleteDispatch, and itsfindholds the caller'slimit.Verification
pnpm --filter @objectstack/plugin-approvals exec vitest run: 95 files, 1232 tests, all passed.pnpm --filter @objectstack/plugin-approvals typecheckpassed, includingcheck:test-typecheck.scripts/ablation-replace.mjs): the old filter was restored inapproval-service.ts, then the new file,approval-service.test.tsand the three re-stamped fixtures were run.singleorganization-less cases, plus the replaced pin.git diff HEADwas empty.node scripts/check-i18n-bundles.mjs: OK, all bundles in sync.node scripts/check-adr-0087-registration.mjs --base origin/main: exit 0.dispatch-gates --commands): on the first pass 95 of 99 exited 0. The other four were handled as follows:check:engine-double-contractandcheck:objectql-double-limit: fixed (see above).check:dual-build-cjs-loadsandcheck:skill-examples: exit 3 because their prerequisite dists were missing. Those packages are now built.(narrowing)arm) are running now. Their counts go in theos-dev-reporton plugin-approvals: an approval delegation with no organization still applies across every tenant (the retired NULL-means-global reading, ADR-0131) #25280.Acceptance notes
claude/project-thread-4a6wrl, notclaude/issue-25280-….content/docs/automation/approvals.mdxgains one paragraph on the organization scope, so the guide states the behaviour this PR changes.delegator_iddefault and help. Whichever lands second merges main and regenerates the bundles.Generated by Claude Code
🤖 Generated with Claude Code
https://claude.ai/code/session_012KEX9o2giH3Wzt4jgeNTAK