You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Impersonation attribution follow-ups after #30213: plugin-auth identity writes and the approval resume decider segment #30851
Follow-ups from the contract review of PR #30800 (card #30213, ruling A; records 6676544432 and 6677397495). PR #30800 merged as fd749a45e. Neither item is a door ruling A names, and neither blocked that PR. ⛔ Not a claim.
plugin-auth identity-table writes carry no administrator. Writes that plugin-auth makes under authSystemWriteContext / withSystemContext while an administrator is impersonating a user set attributedUserId to the impersonated user, and carry no impersonatedBy. The audit row for such a write is therefore indistinguishable from the user's own. Expected: the same attribution-only impersonated_by stamp PR feat(audit): record the administrator on writes made while impersonating a user #30800 adds on record writes (sys_audit_log.metadata), read from the same session, never from the request body. Precedent for the key: packages/plugins/plugin-auth/src/auth-session-audit.ts.
The approval resume signal carries no impersonation state.deciderSegmentContext (packages/services/service-automation/src/run-resume.ts) drops impersonatedBy for a decider's segment, but has two gaps:
(a) the early return for context.userId === decider keeps the trigger-time stamp on the decider's own segment, even when the decider decided in their own name;
(b) a decider who decides while impersonated gets no stamp at all.
Expected: the decider's segment carries exactly the decision-time impersonatedBy (absent when the decider acted as themselves).
Not included here: the sys_comment fork (comments write no ledger or activity row). That is a separate maintainer decision on #30213, with options A/B/C.
Suggested grade: p3, domain:server. Serial: none (PR #30800 has merged).
Claim: PM loop round 6 · 2026-11-06T06:42Z
Session: session_016WV4cyvF7SP65frVFAYLv7
Account: os-elon-musk (the seat's linked user as GET /user answers it; the card's assignee)
Branch: the card thread's designated claude/project-thread-* branch; this claim is re-posted whole with it once the thread reports it, branched from main
Worktree: objectstack-issue-30851
Domain: domain:server
Seat: domain:server#5 (overflow: this seat's own share of the queue is empty, and the maintainer asked every seat to run at full concurrency; the share seat may say so and this seat yields)
File surface: item 1, packages/plugins/plugin-auth/src/**: the identity-table writes plugin-auth makes under authSystemWriteContext / withSystemContext while an administrator impersonates a user carry the attribution-only impersonated_by stamp PR #30800 adds on record writes (sys_audit_log.metadata), read from the session, never from the request body (precedent auth-session-audit.ts); item 2, packages/services/service-automation/src/run-resume.ts: deciderSegmentContext gives the decider's segment exactly the decision-time impersonatedBy, absent when the decider acted as themselves (the context.userId === decider early return and the impersonated-decider case); their tests; one changeset. The audit write path PR #30800 added (read-only use; an export added there only if none reaches plugin-auth, said in the report). ⛔ packages/spec/**, ⛔ ui/**, ⛔ the sys_comment fork (a separate maintainer decision on #30213)
Container & model: M, mode:thread, model: default, effort: high
Clause-②: no (provisional: attribution-only metadata on audit rows; the dev re-derives it from the diff)
Metadata-protocol: none
Responsibility: an administrator's writes while impersonating read as the user's own in the audit ledger (plugin-auth identity writes), and an approval decided under impersonation carries no stamp or a stale one | none | every administrator who impersonates, and auditors reading sys_audit_log
Thread-read: none
Premise: holds on main677d542: deciderSegmentContext returns the trigger-time context when context.userId === decider (run-resume.ts L328) and sets no decision-time impersonatedBy; PR #30800 merged as fd749a45e
Serial constraints cleared: none named by the card; the dev re-checks open PRs on packages/plugins/plugin-auth/** and run-resume.ts before the first push and merges origin/main after any lands
Claim: PM loop round 6 · 2026-11-06T06:44Z (re-posted whole with the branch, superseding the claim above)
Session: session_016WV4cyvF7SP65frVFAYLv7
Account: os-elon-musk (the seat's linked user as GET /user answers it; the card's assignee)
Branch: claude/project-thread-xv0lyn (the card thread's designated branch, dev session session_016WV4czA9H21XfNGbug1dac), branched from main677d542
Worktree: objectstack-issue-30851
Domain: domain:server
Seat: domain:server#5 (overflow: this seat's own share of the queue is empty, and the maintainer asked every seat to run at full concurrency; the share seat may say so and this seat yields)
File surface: item 1, packages/plugins/plugin-auth/src/**: the identity-table writes plugin-auth makes under authSystemWriteContext / withSystemContext while an administrator impersonates a user carry the attribution-only impersonated_by stamp PR #30800 adds on record writes (sys_audit_log.metadata), read from the session, never from the request body (precedent auth-session-audit.ts); item 2, packages/services/service-automation/src/run-resume.ts: deciderSegmentContext gives the decider's segment exactly the decision-time impersonatedBy, absent when the decider acted as themselves (the context.userId === decider early return and the impersonated-decider case); their tests; one changeset. The audit write path PR #30800 added (read-only use; an export added there only if none reaches plugin-auth, said in the report). ⛔ packages/spec/**, ⛔ ui/**, ⛔ the sys_comment fork (a separate maintainer decision on #30213)
Container & model: M, mode:thread, model: default, effort: high
Clause-②: no (provisional: attribution-only metadata on audit rows; the dev re-derives it from the diff)
Metadata-protocol: none
Responsibility: an administrator's writes while impersonating read as the user's own in the audit ledger (plugin-auth identity writes), and an approval decided under impersonation carries no stamp or a stale one | none | every administrator who impersonates, and auditors reading sys_audit_log
Thread-read: none
Premise: holds on main677d542: deciderSegmentContext returns the trigger-time context when context.userId === decider (run-resume.ts L328) and sets no decision-time impersonatedBy; PR #30800 merged as fd749a45e
Serial constraints cleared: none named by the card; the dev re-checks open PRs on packages/plugins/plugin-auth/** and run-resume.ts before the first push and merges origin/main after any lands
p2, not the filed p3: an audit row an administrator made while impersonating cannot be told from the user's own, and that is the record an audit trail exists to keep. The stamp is attribution-only, read from the session and never from the request body, as PR feat(audit): record the administrator on writes made while impersonating a user #30800 does.
domain:server, already dispatched to domain:server#4.
Claim: PM loop round 6 · 2026-11-06T06:46Z (re-posted whole: the branch, and item 2's surface widened after the dev's measurement, superseding the claims above)
Session: session_016WV4cyvF7SP65frVFAYLv7
Account: os-elon-musk (the seat's linked user as GET /user answers it; the card's assignee)
Branch: claude/project-thread-xv0lyn (the card thread's designated branch, dev session session_016WV4czA9H21XfNGbug1dac), branched from main677d542
Worktree: objectstack-issue-30851
Domain: domain:server
Seat: domain:server#5 (overflow: this seat's own share of the queue is empty, and the maintainer asked every seat to run at full concurrency; the share seat may say so and this seat yields)
File surface: item 1, packages/plugins/plugin-auth/src/**: the identity-table writes plugin-auth makes under authSystemWriteContext / withSystemContext while an administrator impersonates a user carry the attribution-only impersonated_by stamp PR #30800 adds on record writes (sys_audit_log.metadata), read from the session, never from the request body (precedent auth-session-audit.ts); item 2, packages/services/service-automation/src/run-resume.ts: deciderSegmentContext gives the decider's segment exactly the decision-time impersonatedBy, absent when the decider acted as themselves (the context.userId === decider early return and the impersonated-decider case); and, for item 2 (route A, the seat's implementation choice inside the card's Expected line, landing as a second PR from the same thread), packages/spec/src/contracts/automation-service.ts (one optional ResumeSignal.impersonatedBy, honoured only with RESUME_AUTHORITY_SERVICE as resumedBy is) and packages/plugins/plugin-approvals/src/approval-service.ts (the decide path passes it from the decider's session context; the replay record persists it beside resumedBy); their tests; one changeset per PR. The audit write path PR #30800 added (read-only use; an export added there only if none reaches plugin-auth, said in the report). ⛔ any other packages/spec/** file, ⛔ ui/**, ⛔ the sys_comment fork (a separate maintainer decision on #30213)
Container & model: M, mode:thread, model: default, effort: high; PR 2 (the spec contract touch) gets a contract review at the contract-review tier before ACCEPT
Cross-seat declaration: the one packages/spec/src/contracts/automation-service.ts member is a domain:spec file carried by this domain:server card (cross-package means cross-lane, declared here); no domain:spec seat is live to consult
Clause-②: no (provisional: attribution-only metadata on audit rows; the dev re-derives it from the diff)
Metadata-protocol: none
Responsibility: an administrator's writes while impersonating read as the user's own in the audit ledger (plugin-auth identity writes), and an approval decided under impersonation carries no stamp or a stale one | none | every administrator who impersonates, and auditors reading sys_audit_log
Thread-read: none
Premise: holds on main677d542: deciderSegmentContext returns the trigger-time context when context.userId === decider (run-resume.ts L328) and sets no decision-time impersonatedBy; PR #30800 merged as fd749a45e
Serial constraints cleared: none named by the card; the dev re-checks open PRs on packages/plugins/plugin-auth/** and run-resume.ts before the first push and merges origin/main after any lands
{
"issue": 30851,
"status": "done",
"branch": "claude/project-thread-xv0lyn",
"pr": "https://github.com/objectstack-ai/objectstack/pull/30874",
"session": "session_016WV4czA9H21XfNGbug1dac",
"premise_still_valid": true,
"summary": "Item 1: plugin-auth's identity-table writes carry the attribution-only impersonatedBy read from the session. resolveActor answers it, the before-hook resolver passes it, and authSystemWriteContext adds it beside attributedUserId (withSystemContext now builds its write context with that function). The platform-admin routes pass the gated session's impersonatedBy through runAttributedToUser. Item 2 (route A, ruled by the seat after the dev stopped on the spec key): ResumeSignal.impersonatedBy is a new optional member, honoured only beside resumedBy with RESUME_AUTHORITY_SERVICE. The decision door is ApprovalService's decide path in approval-service.ts: it fills the member from the decision request's ExecutionContext, and the stranded-continuation journal and the replay carry it. deciderSegmentContext stamps exactly the decision's value and drops the trigger-time stamp on every human decider's segment, the self-decider path included. Re-derived declarations: Clause-② yes (widening), not the claim's provisional no. The public surface grows: ResumeSignal.impersonatedBy in @objectstack/spec/contracts, plus plugin-auth's new exports resolveAttributedActor and AttributedAuthActor and the optional PlatformAdminActor.impersonatedBy. Metadata-protocol none: no authorable metadata key, no emitted schema or snapshot moved. One changeset, all four packages patch. Surface note: besides the named files, the diff edits the resolveDecisionSegmentContext docblock in service-automation/src/engine.ts (the contract of the run-resume.ts body it changes) and the AutomationContext.impersonatedBy docblock in the allowed spec contract file.",
"tests": "Head b5edf8ea. New pins: service-automation resume-decider-identity.integration.test.ts, 32/32 (new describe: (a) self-decider in own name gives none on both runAs arms; (b) impersonated decider gives the decision's administrator on both arms, plus the trigger user deciding while impersonated; (c) plain decider gives none, and a second plain decision drops the first decision's stamp; CONTROLs: no service marker, and an administrator with no decider). plugin-approvals restored-approval-continuation.test.ts, 24/24 (approve and reject while impersonated; an administrator named in the INPUT stamps nothing; a stranded impersonated decision journals and replays it). plugin-auth auth-actor-attribution.test.ts plus admin-impersonate-endpoint.test.ts, 34/34 (unit seam cases, and on the real better-auth pipeline an impersonated /update-user rename reaching the engine with impersonatedBy = admin, with a body key ignored, and the admin's own rename carrying none). Package suites: plugin-auth 228 files, 4096 passed, 10 skipped; service-automation 264 files, 3429 passed; plugin-approvals 115 files, 1502 passed. Ablations through scripts/ablation-replace.mjs WRAP, each red and restored with git diff HEAD empty: A1 before-hook resolver user-only (main's shape, the reproduction) 1/16 red; A2 authSystemWriteContext spread dropped 4/34; A3 self-decider early return 3/32; A4 switched segment never stamped 3/32; A5 decide door omits it 3/24; A6 journal omits it 1/24. Tests import src, so no dist rebuild was needed.",
"gates": "dispatch-gates --ran: 88 derived, 88 run, 0 NOT-MEASURED, 0 UNRUN (sweep on 8e621657 after a whole-tree turbo build, 119/119). Red in the sweep: check:query-options-erasure, test surface 236 to 237 from an as-any on a find call in the new pin; fixed in b5edf8ea, re-run exit 0 at the 236 ceiling; its 'approval-service.ts fell 10 to 4' line is identical on main. pnpm lint 0, check:cli-command-ids 0, check:adr-symbol-anchors 0. Typecheck of spec, plugin-auth, plugin-approvals and service-automation: exit 0. spec check:generated: 13/14 current; check:test-typecheck failed in the wrapper as 'tsc failed without diagnostics' under the default heap and passed alone at a 6144 MB heap, with no artifact moved. CI check-runs on #30874: in_progress at report time.",
"dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm",
"mcp_calls": "4 — all reads: issue_read get (#30851), issue_read get_comments (#30851) x2, pull_request_read get_files (#30717). No MCP write.",
"api_writes": "2 relay writes — fleet-write pr_create (PR #30874), post-stamped comment (this report). Plus git push of the branch (4 pushes, the first after the seat re-posted the claim).",
"open_questions": [],
"security_findings": "none",
"out_of_scope_findings": [
"carrier: none · noted, not filed — the ADR-0043 action-link decision path authenticates by token with no session, so a decision made through it carries no administrator by construction; the reconstructed (no-journal) replay path in resolveRecordedContinuation carries neither resumedBy nor impersonatedBy, a pre-existing shape this card does not change"
]
}
merged, merge commit 90381874, through the merge queue (seat ACCEPT 6680426963 and contract review PASS 6680400800, both on head b5edf8ea; not governed, +484/-48; every check green or expected-skip before landing)
Shipped: both follow-ups of PR #30800's contract review. First, plugin-auth's identity-table writes made while an administrator impersonates a user now carry impersonatedBy, so sys_audit_log.metadata.impersonated_by names the administrator. Second, a resumed run's decider segment carries exactly the decision-time impersonatedBy, through an optional ResumeSignal.impersonatedBy that is honoured only under RESUME_AUTHORITY_SERVICE. Named for later, no card yet: the RunDataContext.impersonatedBy docblock in service-automation/src/runtime-identity.ts (L102–110) should also name the decider's segment (prose, fix on next touch); the reconstructed no-journal replay carries neither resumedBy nor impersonatedBy (pre-existing). The sys_comment fork stays with the maintainer on #30213.
State change: pm:dispatched removed, assignee cleared.
Activity
objectstack-fleet commentedon Nov 6, 2026
domain:server#5 · os-elon-musk · session_016WV4cyvF7SP65frVFAYLv7
Claim: PM loop round 6 · 2026-11-06T06:42Z
Session:
session_016WV4cyvF7SP65frVFAYLv7Account:
os-elon-musk(the seat's linked user asGET /useranswers it; the card's assignee)Branch: the card thread's designated
claude/project-thread-*branch; this claim is re-posted whole with it once the thread reports it, branched frommainWorktree:
objectstack-issue-30851Domain:
domain:serverSeat:
domain:server#5(overflow: this seat's own share of the queue is empty, and the maintainer asked every seat to run at full concurrency; the share seat may say so and this seat yields)File surface: item 1,
packages/plugins/plugin-auth/src/**: the identity-table writes plugin-auth makes underauthSystemWriteContext/withSystemContextwhile an administrator impersonates a user carry the attribution-onlyimpersonated_bystamp PR #30800 adds on record writes (sys_audit_log.metadata), read from the session, never from the request body (precedentauth-session-audit.ts); item 2,packages/services/service-automation/src/run-resume.ts:deciderSegmentContextgives the decider's segment exactly the decision-timeimpersonatedBy, absent when the decider acted as themselves (thecontext.userId === deciderearly return and the impersonated-decider case); their tests; one changeset. The audit write path PR #30800 added (read-only use; an export added there only if none reaches plugin-auth, said in the report). ⛔packages/spec/**, ⛔ui/**, ⛔ thesys_commentfork (a separate maintainer decision on #30213)Container & model:
M,mode:thread,model: default,effort: highClause-②: no (provisional: attribution-only metadata on audit rows; the dev re-derives it from the diff)
Metadata-protocol: none
Responsibility: an administrator's writes while impersonating read as the user's own in the audit ledger (plugin-auth identity writes), and an approval decided under impersonation carries no stamp or a stale one | none | every administrator who impersonates, and auditors reading
sys_audit_logThread-read: none
Premise: holds on
main677d542:deciderSegmentContextreturns the trigger-time context whencontext.userId === decider(run-resume.tsL328) and sets no decision-timeimpersonatedBy; PR #30800 merged asfd749a45eSerial constraints cleared: none named by the card; the dev re-checks open PRs on
packages/plugins/plugin-auth/**andrun-resume.tsbefore the first push and mergesorigin/mainafter any landsobjectstack-fleet commentedon Nov 6, 2026
domain:server#5 · os-elon-musk · session_016WV4cyvF7SP65frVFAYLv7
Claim: PM loop round 6 · 2026-11-06T06:44Z (re-posted whole with the branch, superseding the claim above)
Session:
session_016WV4cyvF7SP65frVFAYLv7Account:
os-elon-musk(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/project-thread-xv0lyn(the card thread's designated branch, dev sessionsession_016WV4czA9H21XfNGbug1dac), branched frommain677d542Worktree:
objectstack-issue-30851Domain:
domain:serverSeat:
domain:server#5(overflow: this seat's own share of the queue is empty, and the maintainer asked every seat to run at full concurrency; the share seat may say so and this seat yields)File surface: item 1,
packages/plugins/plugin-auth/src/**: the identity-table writes plugin-auth makes underauthSystemWriteContext/withSystemContextwhile an administrator impersonates a user carry the attribution-onlyimpersonated_bystamp PR #30800 adds on record writes (sys_audit_log.metadata), read from the session, never from the request body (precedentauth-session-audit.ts); item 2,packages/services/service-automation/src/run-resume.ts:deciderSegmentContextgives the decider's segment exactly the decision-timeimpersonatedBy, absent when the decider acted as themselves (thecontext.userId === deciderearly return and the impersonated-decider case); their tests; one changeset. The audit write path PR #30800 added (read-only use; an export added there only if none reaches plugin-auth, said in the report). ⛔packages/spec/**, ⛔ui/**, ⛔ thesys_commentfork (a separate maintainer decision on #30213)Container & model:
M,mode:thread,model: default,effort: highClause-②: no (provisional: attribution-only metadata on audit rows; the dev re-derives it from the diff)
Metadata-protocol: none
Responsibility: an administrator's writes while impersonating read as the user's own in the audit ledger (plugin-auth identity writes), and an approval decided under impersonation carries no stamp or a stale one | none | every administrator who impersonates, and auditors reading
sys_audit_logThread-read: none
Premise: holds on
main677d542:deciderSegmentContextreturns the trigger-time context whencontext.userId === decider(run-resume.tsL328) and sets no decision-timeimpersonatedBy; PR #30800 merged asfd749a45eSerial constraints cleared: none named by the card; the dev re-checks open PRs on
packages/plugins/plugin-auth/**andrun-resume.tsbefore the first push and mergesorigin/mainafter any landsobjectstack-fleet commentedon Nov 6, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: audit · p2 ·
domain:server· claimed · 2026-11-06T06:46Z⛔ Not a claim.
domain:server#4's ask (6678967223). Read fresh: two impersonation-attribution gaps left by PR feat(audit): record the administrator on writes made while impersonating a user #30800 ([Decision] Actions taken while impersonating a user are recorded only under that user #30213, ruling A). First, plugin-auth identity writes made during an impersonation carry noimpersonated_by. Second, the approval resume decider segment carries no impersonation state.domain:server, already dispatched todomain:server#4.Generated by Claude Code
objectstack-fleet commentedon Nov 6, 2026
domain:server#5 · os-elon-musk · session_016WV4cyvF7SP65frVFAYLv7
Claim: PM loop round 6 · 2026-11-06T06:46Z (re-posted whole: the branch, and item 2's surface widened after the dev's measurement, superseding the claims above)
Session:
session_016WV4cyvF7SP65frVFAYLv7Account:
os-elon-musk(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/project-thread-xv0lyn(the card thread's designated branch, dev sessionsession_016WV4czA9H21XfNGbug1dac), branched frommain677d542Worktree:
objectstack-issue-30851Domain:
domain:serverSeat:
domain:server#5(overflow: this seat's own share of the queue is empty, and the maintainer asked every seat to run at full concurrency; the share seat may say so and this seat yields)File surface: item 1,
packages/plugins/plugin-auth/src/**: the identity-table writes plugin-auth makes underauthSystemWriteContext/withSystemContextwhile an administrator impersonates a user carry the attribution-onlyimpersonated_bystamp PR #30800 adds on record writes (sys_audit_log.metadata), read from the session, never from the request body (precedentauth-session-audit.ts); item 2,packages/services/service-automation/src/run-resume.ts:deciderSegmentContextgives the decider's segment exactly the decision-timeimpersonatedBy, absent when the decider acted as themselves (thecontext.userId === deciderearly return and the impersonated-decider case); and, for item 2 (route A, the seat's implementation choice inside the card's Expected line, landing as a second PR from the same thread),packages/spec/src/contracts/automation-service.ts(one optionalResumeSignal.impersonatedBy, honoured only withRESUME_AUTHORITY_SERVICEasresumedByis) andpackages/plugins/plugin-approvals/src/approval-service.ts(the decide path passes it from the decider's session context; the replay record persists it besideresumedBy); their tests; one changeset per PR. The audit write path PR #30800 added (read-only use; an export added there only if none reaches plugin-auth, said in the report). ⛔ any otherpackages/spec/**file, ⛔ui/**, ⛔ thesys_commentfork (a separate maintainer decision on #30213)Container & model:
M,mode:thread,model: default,effort: high; PR 2 (the spec contract touch) gets a contract review at the contract-review tier before ACCEPTCross-seat declaration: the one
packages/spec/src/contracts/automation-service.tsmember is adomain:specfile carried by thisdomain:servercard (cross-package means cross-lane, declared here); nodomain:specseat is live to consultClause-②: no (provisional: attribution-only metadata on audit rows; the dev re-derives it from the diff)
Metadata-protocol: none
Responsibility: an administrator's writes while impersonating read as the user's own in the audit ledger (plugin-auth identity writes), and an approval decided under impersonation carries no stamp or a stale one | none | every administrator who impersonates, and auditors reading
sys_audit_logThread-read: none
Premise: holds on
main677d542:deciderSegmentContextreturns the trigger-time context whencontext.userId === decider(run-resume.tsL328) and sets no decision-timeimpersonatedBy; PR #30800 merged asfd749a45eSerial constraints cleared: none named by the card; the dev re-checks open PRs on
packages/plugins/plugin-auth/**andrun-resume.tsbefore the first push and mergesorigin/mainafter any landsobjectstack-fleet commentedon Nov 6, 2026
domain:server#5 · os-elon-musk · session_016WV4czA9H21XfNGbug1dac
os-dev-report
{ "issue": 30851, "status": "done", "branch": "claude/project-thread-xv0lyn", "pr": "https://github.com/objectstack-ai/objectstack/pull/30874", "session": "session_016WV4czA9H21XfNGbug1dac", "premise_still_valid": true, "summary": "Item 1: plugin-auth's identity-table writes carry the attribution-only impersonatedBy read from the session. resolveActor answers it, the before-hook resolver passes it, and authSystemWriteContext adds it beside attributedUserId (withSystemContext now builds its write context with that function). The platform-admin routes pass the gated session's impersonatedBy through runAttributedToUser. Item 2 (route A, ruled by the seat after the dev stopped on the spec key): ResumeSignal.impersonatedBy is a new optional member, honoured only beside resumedBy with RESUME_AUTHORITY_SERVICE. The decision door is ApprovalService's decide path in approval-service.ts: it fills the member from the decision request's ExecutionContext, and the stranded-continuation journal and the replay carry it. deciderSegmentContext stamps exactly the decision's value and drops the trigger-time stamp on every human decider's segment, the self-decider path included. Re-derived declarations: Clause-② yes (widening), not the claim's provisional no. The public surface grows: ResumeSignal.impersonatedBy in @objectstack/spec/contracts, plus plugin-auth's new exports resolveAttributedActor and AttributedAuthActor and the optional PlatformAdminActor.impersonatedBy. Metadata-protocol none: no authorable metadata key, no emitted schema or snapshot moved. One changeset, all four packages patch. Surface note: besides the named files, the diff edits the resolveDecisionSegmentContext docblock in service-automation/src/engine.ts (the contract of the run-resume.ts body it changes) and the AutomationContext.impersonatedBy docblock in the allowed spec contract file.", "tests": "Head b5edf8ea. New pins: service-automation resume-decider-identity.integration.test.ts, 32/32 (new describe: (a) self-decider in own name gives none on both runAs arms; (b) impersonated decider gives the decision's administrator on both arms, plus the trigger user deciding while impersonated; (c) plain decider gives none, and a second plain decision drops the first decision's stamp; CONTROLs: no service marker, and an administrator with no decider). plugin-approvals restored-approval-continuation.test.ts, 24/24 (approve and reject while impersonated; an administrator named in the INPUT stamps nothing; a stranded impersonated decision journals and replays it). plugin-auth auth-actor-attribution.test.ts plus admin-impersonate-endpoint.test.ts, 34/34 (unit seam cases, and on the real better-auth pipeline an impersonated /update-user rename reaching the engine with impersonatedBy = admin, with a body key ignored, and the admin's own rename carrying none). Package suites: plugin-auth 228 files, 4096 passed, 10 skipped; service-automation 264 files, 3429 passed; plugin-approvals 115 files, 1502 passed. Ablations through scripts/ablation-replace.mjs WRAP, each red and restored with git diff HEAD empty: A1 before-hook resolver user-only (main's shape, the reproduction) 1/16 red; A2 authSystemWriteContext spread dropped 4/34; A3 self-decider early return 3/32; A4 switched segment never stamped 3/32; A5 decide door omits it 3/24; A6 journal omits it 1/24. Tests import src, so no dist rebuild was needed.", "gates": "dispatch-gates --ran: 88 derived, 88 run, 0 NOT-MEASURED, 0 UNRUN (sweep on 8e621657 after a whole-tree turbo build, 119/119). Red in the sweep: check:query-options-erasure, test surface 236 to 237 from an as-any on a find call in the new pin; fixed in b5edf8ea, re-run exit 0 at the 236 ceiling; its 'approval-service.ts fell 10 to 4' line is identical on main. pnpm lint 0, check:cli-command-ids 0, check:adr-symbol-anchors 0. Typecheck of spec, plugin-auth, plugin-approvals and service-automation: exit 0. spec check:generated: 13/14 current; check:test-typecheck failed in the wrapper as 'tsc failed without diagnostics' under the default heap and passed alone at a 6144 MB heap, with no artifact moved. CI check-runs on #30874: in_progress at report time.", "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm", "mcp_calls": "4 — all reads: issue_read get (#30851), issue_read get_comments (#30851) x2, pull_request_read get_files (#30717). No MCP write.", "api_writes": "2 relay writes — fleet-write pr_create (PR #30874), post-stamped comment (this report). Plus git push of the branch (4 pushes, the first after the seat re-posted the claim).", "open_questions": [], "security_findings": "none", "out_of_scope_findings": [ "carrier: none · noted, not filed — the ADR-0043 action-link decision path authenticates by token with no session, so a decision made through it carries no administrator by construction; the reconstructed (no-journal) replay path in resolveRecordedContinuation carries neither resumedBy nor impersonatedBy, a pre-existing shape this card does not change" ] }objectstack-fleet commentedon Nov 6, 2026
domain:server#5 · os-elon-musk · session_016WV4cyvF7SP65frVFAYLv7
Landed · 2026-11-06T09:05Z · PR #30874
merged, merge commit90381874, through the merge queue (seat ACCEPT 6680426963 and contract review PASS 6680400800, both on headb5edf8ea; not governed, +484/-48; every check green or expected-skip before landing)Fixes #30851Shipped: both follow-ups of PR #30800's contract review. First, plugin-auth's identity-table writes made while an administrator impersonates a user now carry
impersonatedBy, sosys_audit_log.metadata.impersonated_bynames the administrator. Second, a resumed run's decider segment carries exactly the decision-timeimpersonatedBy, through an optionalResumeSignal.impersonatedBythat is honoured only underRESUME_AUTHORITY_SERVICE. Named for later, no card yet: theRunDataContext.impersonatedBydocblock inservice-automation/src/runtime-identity.ts(L102–110) should also name the decider's segment (prose, fix on next touch); the reconstructed no-journal replay carries neitherresumedBynorimpersonatedBy(pre-existing). Thesys_commentfork stays with the maintainer on #30213.State change:
pm:dispatchedremoved, assignee cleared.