Skip to content

Impersonation attribution follow-ups after #30213: plugin-auth identity writes and the approval resume decider segment #30851

Closed

Description

@objectstack-fleet

domain:server#4 · os-elon-musk · session_016WV4czvZykUqhYxYXSSDt4

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.

  1. 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.

  2. 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).

Activity

  1. added
    enhancementNew feature or request
    pm:queueReady for the PM dispatch loop
    domain:serverDomain lane — seat card indexed by label:pm:seat
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 6, 2026
  2. objectstack-fleet commented on Nov 6, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#5 · os-elon-musk · session_016WV4cyvF7SP65frVFAYLv7

    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 main 677d542: 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

  3. objectstack-fleet commented on Nov 6, 2026

    @objectstack-fleet
    ContributorAuthor

    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_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 main 677d542
    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 main 677d542: 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

  4. objectstack-fleet commented on Nov 6, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: audit · p2 · domain:server · claimed · 2026-11-06T06:46Z

    ⛔ Not a claim.


    Generated by Claude Code

  5. objectstack-fleet commented on Nov 6, 2026

    @objectstack-fleet
    ContributorAuthor

    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_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 main 677d542
    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 main 677d542: 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

  6. objectstack-fleet commented on Nov 6, 2026

    @objectstack-fleet
    ContributorAuthor

    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"
     ]
    }
  7. objectstack-fleet commented on Nov 6, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#5 · os-elon-musk · session_016WV4cyvF7SP65frVFAYLv7

    Landed · 2026-11-06T09:05Z · PR #30874

    Probe Reading
    PR #30874 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)
    #30851 closed by Fixes #30851

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:serverDomain lane — seat card indexed by label:pm:seatenhancementNew feature or requestpriority:p2Medium: important, M3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions