Skip to content

feat(audit): record the administrator on writes made while impersonating a user - #30800

Merged
objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/project-thread-8i10co
Nov 6, 2026
Merged

objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/project-thread-8i10co

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread

Fixes #30213

Clause-②: yes (widening)
Metadata-protocol: added

维护者速读

  • 改了什么:管理员模拟员工时做的每一次写入,审计日志(sys_audit_log.metadata.impersonated_by)和记录动态(sys_activity)都记下这位管理员;记录详情页的动态与字段历史显示「Dev Admin 代王工操作」。数据归属、created_by、权限仍是员工本人,没有加列、没有迁移。
  • 为什么改:裁决 A(「A」)。之前模拟会话里的写入与员工本人的写入在审计上完全一样,事后查不出是谁做的。
  • 风险与回滚:新字段只用于记录,授权路径不读它;回滚即还原本 PR,老数据不受影响(老行本来就没有这个键)。评论是分岔:评论不写任何审计/动态行,本 PR 没有改,交您定夺(见下方 Acceptance notes)。
  • 先看:packages/core/src/security/resolve-authz-context.ts(从会话读出管理员)、packages/plugins/plugin-audit/src/audit-writers.ts(写入审计与动态的那几行)、packages/services/service-automation/src/run-resume.ts(审批人接手后不再沿用管理员)。
  • 两分钟验证:pnpm --filter @objectstack/plugin-audit exec vitest run src/activity-actor-name.test.ts,预期 15 个用例全绿,其中 5 个是本卡的(审计行带 impersonated_by、动态行带管理员名、员工本人写入不带、钩子里的调用者仍是员工)。
  • 需要你做的:评论那一处的分岔选一个方向(见 Acceptance notes 的 needs_decision 三个选项);其余无,席位落地(本 PR 触 packages/spec,落地前欠一次契约级复核)。

What changes

An administrator who impersonates a user writes as that user. Before this PR nothing on the write said otherwise: the sys_audit_log row (user_id and actor both the user, metadata empty), the sys_activity row and the record were byte-identical to the user's own work. Only the sign-in row named the administrator.

This PR carries the administrator as an attribution-only fact, in the additive shape an agent's write already uses (performed_by / on_behalf_of):

  • Spec. ExecutionContext.impersonatedBy (optional string, a sys_user id) and HookContext.provenance.impersonatedBy, both documented as attribution only. AutomationContext.impersonatedBy lets a flow started by such a write carry it.
  • Request identity. resolveAuthzContext's session arm reads session.impersonatedBy off the same session read that admitted the caller (session credential only; an API key carries none). assembleExecutionContext emits it on both entries (never on a guest envelope). Every transport that uses the shared resolver (REST, the runtime dispatcher, MCP) gets it from that one place.
  • Engine. buildProvenance copies it into the hook context's provenance, never into session. The sandbox body runner projects it like the other provenance members.
  • Audit writers. The CRUD mirror stamps sys_audit_log.metadata.impersonated_by and adds impersonated_by plus impersonated_by_name (the administrator's name, from the same memoized sys_user read as actor_name) to the sys_activity row's metadata. The action_invocation row stamps impersonated_by too. Each key appears only on such a write, so its absence means the user acted themselves. user_id, actor and actor_id do not move (ADR-0118 D1/D5).
  • Flows started by the write. The record-change trigger reads provenance.impersonatedBy into the run's AutomationContext; resolveRunDataContext puts it on every data node's context on both runAs arms; it survives suspension with the persisted context. When an approval decision hands the run to a decider, the decider's segment drops it (they act in their own name).
  • Console. The record timeline (record:activity, the console record page's merged feed), record:history and the record page's field history (from sys_audit_log) read 「Dev Admin 代王工操作」: new key detail.impersonatedActor in all ten console languages. An administrator whose name the writer could not read shows as the unknown-user text, never as "System" or a raw id.

What the audit reader shows now, for a ticket created by Dev Admin impersonating 王工: sys_audit_log row user_id = 王工, actor = 王工, metadata = {"impersonated_by": "<Dev Admin's id>"}; sys_activity row actor_id = 王工, actor_name = 王工, metadata carrying impersonated_by and impersonated_by_name: "Dev Admin"; the record's created_by stays 王工; the timeline line reads 「Dev Admin 代王工操作」.

The ruling's verify list (comment 6647756733), item by item

  • Every write door stamps it.
    • Generic data route: yes, through the shared resolver every transport uses.
    • Actions: yes. The action body's context spreads the caller's envelope (buildActionExecutionContext), so its writes carry it; the action_invocation row carries it.
    • Flows started by the write: yes, on both runAs arms, until a decider takes over.
    • Comments: no. This is a fork, reported, not narrowed (see Acceptance notes).
  • The fact does not reach a caller-gating hook as the caller. Yes, pinned: a beforeInsert hook in an impersonated write sees session.userId = the user, nothing naming the administrator in session, and provenance = { impersonatedBy }.
  • Console display is new work. Built for the activity rows and the audit-log field history. The shared rendering does not also show agent writes (performed_by); left out to keep this PR to the ruling.
  • Sub-point: everyone who can read the record sees the administrator on the timeline. Yes. sys_activity metadata is served to every reader of the row; sys_audit_log.metadata is not narrowed (audit-log-field-redaction.ts's note updated to say so).
  • Sub-point: fill the empty ip_address if cheap. Not done. The hook context carries no request IP, so filling it means a second envelope field set at every door. That is not cheap, and the ruling says it must not hold the rest.

Tests

  • plugin-audit activity-actor-name.test.ts: five new cases. An impersonated insert stamps sys_audit_log.metadata.impersonated_by with user_id/actor still the user. The sys_activity row carries impersonated_by and impersonated_by_name. The user's own write carries neither key (control). A beforeInsert hook sees the user in session and the administrator only in provenance. action-invocation-audit.test.ts: the invocation row stamps it.
  • core: resolve-authz-context.test.ts (an impersonation session yields it with the user's grants; the user's own session does not) and assemble-execution-context.test.ts (both entries carry it, a guest envelope never does).
  • trigger-record-change: the run's AutomationContext carries the write's provenance.impersonatedBy.
  • service-automation resume-decider-identity.integration.test.ts: data nodes carry it on both runAs arms, and the decider's segment after an approval drops it (plus a control).
  • cli lowered-body-face-enumeration.test.ts: the provenance member set now includes impersonatedBy.
  • Console: recordActivityFeed.actorId-12067.test.tsx (composed actor line, unknown administrator falls back to the unknown-user text, never "System") and auditHistoryDisplay.test.ts (auditImpersonatorId).
  • Ablation: deleting the audit stamp line in audit-writers.ts turns the ledger pin red (1 failed / 14 passed); restored, blob equal to HEAD.

Acceptance notes

  • needs_decision: comments. sys_comment is in plugin-audit's SKIP_OBJECTS, so a comment writes no sys_audit_log and no sys_activity row at all, impersonated or not, and its author_id is the impersonated user. The ruling's verify list names comments as a write door, so this is reported, not narrowed. Options:
    • (a) Stop skipping sys_comment in the audit mirror, so a comment gets the same ledger and activity rows (and the stamp) as any write. Widest change: every comment starts writing two rows.
    • (b) Stamp the fact on the comment row itself. sys_comment has no metadata or JSON field today, so this needs a new column and a migration, which ruling A excludes; it would need its own ruling.
    • (c) Leave comments as they are and record it as a known gap in the ruling's follow-up.
      ⛔ No column and no ledger row was added for comments in this PR.
  • Agent writes (performed_by) are not shown by the new console line; out of the ruling's scope.
  • ip_address is not filled (see the verify list).
  • Not re-measured against a live deployment; the evidence is the unit and integration pins above.
  • Touches packages/spec (widening, optional members only): a contract-review-tier review is owed before the queue.

Verification

  • Full turbo build 119/119 on the merge head; typecheck exit 0 for spec, core, objectql, runtime, plugin-audit, trigger-record-change, service-automation, @object-ui/plugin-detail, @object-ui/i18n, @object-ui/app-shell.
  • Package suites: plugin-audit 30, core 341, trigger-record-change 52, service-automation 26, console 3091 + record-detail view 651 (102/102 files), all green.
  • dispatch-gates.mjs --ran: 116 derived families, 116 run, 0 NOT-MEASURED. check-plugin-teardown-shape --self-test needed its base commit fetched in the shallow clone; exit 0 after that (48 cases).
  • spec gen:docs regenerated content/docs/references/{data/data-engine,data/hook,kernel/execution-context}.mdx.

Changed lines: 45 files, 654 insertions, 28 deletions (682, under 3000).

🤖 Generated with Claude Code

https://claude.ai/code/session_016WV4cxMph1oXW6ujSSVm1G

…ersonation session

Carry the session's impersonatedBy as an attribution-only
ExecutionContext.impersonatedBy, hand it to hooks as
provenance.impersonatedBy, and stamp sys_audit_log.metadata.impersonated_by
and the sys_activity row metadata. user_id, created_by and permissions stay
the impersonated user's. Flows started by such a write carry it onto their
own writes; action invocation rows carry it too.

Claude-Session: https://claude.ai/code/session_016WV4cxMph1oXW6ujSSVm1G
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation protocol:data tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 6, 2026
@github-actions

github-actions Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 7 package(s): @objectstack/core, @objectstack/objectql, @objectstack/plugin-audit, @objectstack/runtime, @objectstack/service-automation, @objectstack/spec, @objectstack/trigger-record-change, touching 29 documentable anchor(s). ⚠️ 2 changed file(s) yielded no anchor (packages/plugins/plugin-audit/src/audit-log-field-redaction.ts, packages/spec/authorable-surface/kernel.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

29 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 3da9a4c132db7bf5ae1ca673930c1034e50d0195.

⛔ 12 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/plugins/plugin-audit/src/audit-log-field-redaction.ts, packages/spec/authorable-surface/kernel.json) — pages documenting those are invisible to this run
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 63 of 221 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 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; 103 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 — 184 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 3da9a4c132db7bf5ae1ca673930c1034e50d0195 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 1eb476bdca9f357364fa62453212f092dc66a963 — the merge of head 9d945da817688a41fb0fcd36c9ead6a606f4bffe into base 3da9a4c132db7bf5ae1ca673930c1034e50d0195, 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 1eb476bdca9f357364fa62453212f092dc66a963 && git checkout 1eb476bdca9f357364fa62453212f092dc66a963
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 3da9a4c132db7bf5ae1ca673930c1034e50d0195 9d945da817688a41fb0fcd36c9ead6a606f4bffe && git checkout -B drift-repro 3da9a4c132db7bf5ae1ca673930c1034e50d0195 && git merge --no-ff 9d945da817688a41fb0fcd36c9ead6a606f4bffe

node scripts/docs-audit/affected-docs.mjs --json 3da9a4c132db7bf5ae1ca673930c1034e50d0195

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 3da9a4c132db7bf5ae1ca673930c1034e50d0195 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…protocol: added (#30213)

ExecutionContext.impersonatedBy enters the kernel authorable-surface shard,
so the change is a metadata-protocol widening: Clause-②: yes (widening),
Metadata-protocol: added, @objectstack/spec at minor.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cxMph1oXW6ujSSVm1G
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

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

Contract review for card #30213 · schema face (packages/spec/src/** hits) · read-only reviewer at the contract-review tier, brief per record-recognisers --brief-template --paths … · 2026-11-06T04:02Z

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 465a509963ba3cfb92610af0ecb08426799f7369
Local-runs: none
Scope: packages/spec/src/contracts/automation-service.ts, packages/spec/src/data/hook.zod.ts, packages/spec/src/kernel/execution-context.zod.ts + packages/spec/authorable-surface/kernel.json, packages/core/src/security/resolve-authz-context.ts, packages/core/src/security/assemble-execution-context.ts, packages/objectql/src/engine-execution-plumbing.ts, packages/plugins/plugin-audit/src/audit-writers.ts, packages/plugins/plugin-audit/src/action-invocation-audit.ts, packages/plugins/plugin-audit/src/impersonation-attribution.ts, packages/triggers/trigger-record-change/src/record-change-trigger.ts, packages/services/service-automation/src/runtime-identity.ts, packages/services/service-automation/src/run-resume.ts, packages/runtime/src/sandbox/body-runner.ts, packages/runtime/src/sandbox/script-runner.ts, packages/runtime/src/action-execution.ts (flow-target arm, read), packages/runtime/src/domains/automation.ts (buildAutomationContext, read), packages/plugins/plugin-auth/src/auth-session-audit.ts (key precedent, read), .changeset/30213-impersonation-write-attribution.md

The code was reviewed at 4110e4e3. 465a5099 is a changeset-only commit over it.

① Derived judgments

  • ExecutionContext.impersonatedBy — right, and so is its producer. The only writer is the session arm of resolveAuthzContextInScope. It sets the key only from sessionData.session.impersonatedBy, only for a non-blank string and only under the session credential. It goes out through the closed ENTRY_EXECUTION_CONTEXT_FIELDS set, and the guest envelope drops it. No caller can set it: no door spreads a request body into the envelope.
  • HookContext.provenance.impersonatedBy — right. buildProvenance copies it. buildSession is untouched, so no hook that gates on the caller sees the administrator as the caller. The sandbox projects the key, and the CLI enumeration pin moves with it.
  • AutomationContext.impersonatedBy — right where it is populated. The record-change trigger reads it from provenance. resolveRunDataContext carries it on both runAs arms, subflows inherit it, and deciderSegmentContext drops it when someone else decides.
  • Row keys — right, and spelled as the platform already spells them: sys_audit_log.metadata.impersonated_by, sys_activity.metadata.impersonated_by + impersonated_by_name. The sign-in row's context.impersonated_by and sys_session.impersonated_by use the same name. user_id and actor do not move (ADR-0118 D1/D5).
  • Where the fact is lost (derived): two doors build the run's AutomationContext by picking identity keys off the envelope by name, and neither picks impersonatedBy. One is the type: 'flow' action arm (action-execution.ts ~L2698). The other is the manual trigger mapper buildAutomationContext (domains/automation.ts ~L146). See ④.

② Semver level

'@objectstack/spec': minor, every other package patch. Both carriers read Clause-②: yes (widening) and Metadata-protocol: added. added is right: kernel/ExecutionContext is an emitted JSON-Schema def, so its new top-level key moves the kernel.json shard. The nested provenance key moves nothing. Generated artifacts and reference pages are regenerated. One Check Changeset run on this head failed because it read the body before the re-patch; the later run is green.

③ Boundary flags

  • The comments fork is escalated to the maintainer, not narrowed. sys_comment is in SKIP_OBJECTS. The options are A (un-skip it in the audit mirror), B (a column) and C (a known gap).
  • ip_address is not filled, as the ruling allows.
  • Follow-up material, outside ruling A:
    • plugin-auth identity-table writes carry no administrator.
    • The resume signal carries no impersonation state for the decider's own segment.
    • The new console line does not render agent writes.

④ Design judgment

  • execution-context.zod.ts — holds.
  • hook.zod.ts — holds.
  • automation-service.ts — must-fix. The contract says the key is absent only on runs that a user's own write started. Two first consumers start runs from an envelope that carries the administrator, and they drop it: the flow-target action arm and buildAutomationContext. A flow that an impersonating administrator starts through a flow-target action, or through POST /:name/trigger, therefore writes unstamped rows, while the same click's action_invocation row is stamped. The fix is three parts:
    1. One guarded spread per site.
    2. A pin for each.
    3. The docblock widened from "the record-change trigger" to every door that starts a run from an envelope carrying the key.

Implemented-by: claude/project-thread-8i10co
Reviewed-by: session_016WV4czvZykUqhYxYXSSDt4

VERDICT: FAIL

…rsonating administrator (#30213)

Both doors build a run's AutomationContext by picking identity keys off
the caller's ExecutionContext by name and dropped impersonatedBy, so a
flow an impersonating administrator started through them wrote rows that
read as the user's own. Forward it (non-blank, attribution only) and pin
both doors with a no-impersonation control.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cxMph1oXW6ujSSVm1G
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

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

Delta re-review for card #30213 over record 6676544432 (FAIL on 465a5099) · schema face · read-only reviewer at the contract-review tier · 2026-11-06T05:03Z

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 9d945da817688a41fb0fcd36c9ead6a606f4bffe
Local-runs: none
Scope: packages/spec/src/contracts/automation-service.ts, packages/spec/src/data/hook.zod.ts, packages/spec/src/kernel/execution-context.zod.ts + packages/runtime/src/action-execution.ts (dispatchFlowAction, flow arm), packages/runtime/src/domains/automation.ts (buildAutomationContext, handleAutomation trigger routes), packages/runtime/src/flow-run-impersonated-by.test.ts (new), packages/runtime/src/endpoint-executor.ts (declared type: 'flow' endpoint, read), packages/runtime/src/domains/actions.ts (read), packages/triggers/trigger-record-change/src/record-change-trigger.ts (read), packages/triggers/trigger-api/src/api-trigger.ts (read), packages/triggers/trigger-schedule/src/schedule-trigger.ts + time-relative-trigger.ts (read), packages/services/service-automation/src/engine.ts, runtime-identity.ts, run-resume.ts (read), packages/core/src/security/resolve-authz-context.ts (read), packages/runtime/src/flow-run-posture.test.ts + flow-run-locale.test.ts (sibling pins, read), .changeset/30213-impersonation-write-attribution.md

9d945da8 is one commit over 465a5099 and touches 5 files: the two door sites, a new pin file, the AutomationContext.impersonatedBy docblock and one changeset bullet. Everything else is byte-identical to the tree reviewed in 6676544432.

① Derived judgments

Unchanged from 6676544432, except for the following.

  • Both doors now carry the key. dispatchFlowAction and buildAutomationContext set impersonatedBy only when the execution context holds a non-blank string, trimmed. That is the same guard as the producer (resolve-authz-context.ts) and the trigger's impersonatedByOf. Both read the execution context only: a body key impersonatedBy lands in params, and that is pinned.

  • No other named door drops it (from a grep for execute( at 9d945da8).

    • The trigger routes and the declared type: 'flow' endpoint all go through buildAutomationContext.
    • REST /actions and MCP run_action go through dispatchFlowAction.
    • The record-change trigger reads provenance.
    • trigger-api, the schedule trigger and the time-relative trigger carry no user identity.
  • Pins cover both doors and both trigger routes:

    • The key is forwarded, with userId still the impersonated user.
    • CONTROL: the user's own request, a blank value and an anonymous request each add no key (absent, not undefined).
    • A body cannot forge the key.

    The file follows the one-file-per-key form of its flow-run-posture / flow-run-locale siblings.

② Semver level

Unchanged: '@objectstack/spec': minor, every other package patch, Clause-②: yes (widening), Metadata-protocol: added. The delta's spec change is a docblock on a TS interface member, which moves no emitted def or shard.

③ Boundary flags

Unchanged: the sys_comment fork stays escalated to the maintainer, and the follow-ups stay outside ruling A. The delta adds nothing.

④ Design judgment

  • execution-context.zod.ts — holds (unchanged).
  • hook.zod.ts — holds (unchanged).
  • automation-service.ts — the must-fix is closed. The docblock now says "Every door that starts a run from an envelope carrying it sets it", and names the three doors. The code agrees, and no envelope-carrying starter is left that drops the key. The ATTRIBUTION ONLY paragraph is untouched. Nit, no action owed: the docblock's list doesn't name the declared type: 'flow' endpoint, though the "every door" sentence covers it and holds as read.
  • No new finding in the delta.

Implemented-by: claude/project-thread-8i10co
Reviewed-by: session_016WV4czvZykUqhYxYXSSDt4

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

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

维护者速读

  1. 改了什么:管理员模拟员工期间的每一次写入,现在都会在审计日志 sys_audit_log.metadata.impersonated_by 和记录动态 sys_activity 里记下这位管理员,记录详情页显示「某管理员代某员工操作」。数据归属、created_by 和权限仍按员工本人算。没有加列,也没有迁移。
  2. 为什么改:[Decision] Actions taken while impersonating a user are recorded only under that user #30213 裁定 A(记录 6647756733)。以前模拟会话里的写入和员工本人的写入在审计上完全一样,事后查不出是谁做的。
  3. 风险:低。新字段只用于记录,授权路径不读它。spec 是增量放宽(ExecutionContext.impersonatedBy 等),不是破坏性改动。回滚就是还原本 PR,老数据不受影响。
  4. 先看:packages/core/src/security/resolve-authz-context.ts(从会话读出管理员);packages/plugins/plugin-audit/src/audit-writers.ts(审计与动态的写入);packages/services/service-automation/src/run-resume.ts(审批人接手后不再沿用管理员)。
  5. 两分钟验证:pnpm --filter @objectstack/plugin-audit exec vitest run src/activity-actor-name.test.ts。
  6. 需要你做的:本 PR 不需要。评论分叉(A/B/C,推荐 A)的决定卡在席位线程,三个选项都在本 PR 之外处理,不挡落地。

席位意见:ACCEPT(#30213,裁定 A)。

席位核对

Head-sha: 9d945da817688a41fb0fcd36c9ead6a606f4bffe · read 2026-11-06T05:29Z

Item Reading
Shape Draft → main, Fixes #30213, title feat(audit):. 48 files, +802/−28. Branch claude/project-thread-8i10co (claim 6670267078, amendments 6670430008 and 6676443469).
Seat diff read The session arm of resolveAuthzContext reads session.impersonatedBy from the same session read that admitted the caller. The value is never set on an API key or a guest envelope. buildProvenance puts it in provenance, never in session. The audit writers stamp impersonated_by (plus impersonated_by_name on sys_activity) only on an impersonated write, and leave user_id, actor and actor_id untouched. Flows started by the write carry it on both runAs arms, and a decider's segment drops it. The console shows detail.impersonatedActor in ten languages.
Evidence Dev report 6676374251: plugin-audit 30/30, core 341/341, trigger-record-change 52/52, service-automation 26/26, console 651 files; ablation of the audit stamp reds the ledger pin; dispatch-gates 116/116. Contract review FAIL 6676544432 on 4110e4e3 (flow-target action arm, buildAutomationContext dropping the key) was fixed at this head. Delta re-review PASS 6677397495 on this head.
Gates (check-runs on this head) 65 completed: 58 success, 7 skipped, 0 failed. mergeable_state clean.
Accepted deviations None.
Out of scope The sys_comment fork (comments are in SKIP_OBJECTS, so they write no ledger or activity row) is escalated to the maintainer. A means a new S card, B a column plus migration (outside ruling A), C a known gap. All three are outside this PR. Plugin-auth identity writes and the resume decider segment get follow-up cards from the seat.
Semver @objectstack/spec: minor; every other package: patch. Clause-②: yes (widening), Metadata-protocol: added. Not BREAKING.
Landing Not governed (check-governed-merges --pr 30800: 0 of 48 paths), 830 lines ≤ 3000, not BREAKING. The seat lands it now: pr_ready + automerge_enable on this head.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 6, 2026 05:30
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 6, 2026 05:30
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 6, 2026
Merged via the queue into main with commit fd749a4 Nov 6, 2026
66 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-8i10co branch November 6, 2026 05:59
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 protocol:data size/l tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Decision] Actions taken while impersonating a user are recorded only under that user

2 participants