Repository navigation
Closed
Labels
domain:serverDomain lane — seat card indexed by label:pm:seatDomain lane — seat card indexed by label:pm:seatenhancementNew feature or requestNew feature or requestpriority:p2Medium: important, M3Medium: important, M3
Description
epic:#26676#1 · zhuangjianguo · session_cdb7a717def74e719988f089fbaa18ff
The maintainer, verbatim: 「S2:管理员模拟员工时做的修改,事后查不出是谁做的,这个要立决策卡,评估开发方案」 (2026-11-05). Business explanation is in the session; this card holds the options.
The question
When an administrator impersonates an employee and changes data, how should the record show who really did it?
Governing text:
packages/platform-objects/src/identity/sys-user.object.ts:476: the impersonation confirm says "actions will be logged".- Banner: 「您正在以 {{user}} 的身份操作 —— 所有操作都会记录为该用户。」
- ADR-0090 D10 rule 4 (dual attribution) and ADR-0118 D1/D5 (
actorkeeps two values; "which non-user acted" is answered by an added attribution field).
Measured
On main b7075d9908, a ticket created in an impersonation session:
| Where | Records |
|---|---|
sys_audit_log row |
user_id and actor both the impersonated user; ip_address and metadata empty |
sys_activity |
王工 |
| Record | created_by the impersonated user |
| Sign-in rows | The session event does name the administrator (plugin-audit/src/auth-event-audit.ts) |
So an investigation can see that an impersonation session existed, and cannot see which writes came from it. No administrator appears on the write.
Existing shapes to build on, read at dfafae8132:
audit-writers.tswritesmetadata: { performed_by, on_behalf_of }for a delegated agent write, present only on such writes.ExecutionContext.attributedUserIdis the precedent for an attribution-only field, which the authorization path never reads.service-print/src/print-routes.ts:176refuses a print in an impersonation session, so readingimpersonatedByfrom the session already has a consumer.rest-server.ts:3072notes that impersonation resolves to the impersonated user at the session level.
Options and development cost
The seat's estimates come from reading the code; nothing was prototyped.
- A. Record the administrator on each write, in the existing metadata shape. Add an attribution-only context field filled from
session.impersonatedBy. The audit writer addsmetadata.impersonated_by, andsys_activitycarries the same. The audit log and the record timeline show 「Dev Admin 代王工操作」.user_id,created_byand permissions are unchanged. No schema change. Small to medium: spec, REST context, audit writers, console display. Limit: a metadata text column cannot be filtered well, so "everything this administrator did while impersonating" needs the session window or a scan. - B. A first-class
impersonated_bycolumn onsys_audit_logandsys_activity. Same display as A, plus a real filter. Medium: adds schema, migration and an index on both tables, and old rows stay empty. It is a second attribution shape next toperformed_by. - C. Make impersonation read-only. Refuse writes in an impersonation session, as the print door already refuses. Attribution stops being a question. Small on the server, and one choke point must cover every write path. Support loses the ability to reproduce and fix a problem as the user, which is the common reason to impersonate. Needs a console state explaining the refusal.
- D. Only correct the wording. The confirm says plainly that writes are recorded as the user. Almost no cost, and the gap stays.
Recommendation: A
四轴(中文):
- 实际业务需求: 客户投诉、内部追责和合规审计都要回答「这是本人还是管理员做的」。管理员替员工排查和修数据又是模拟功能的日常用途,所以 C 会砍掉主要价值。A 和 B 都能回答问题。需要「按管理员查全部代操作」的频率更低,可以先靠模拟会话的时间窗定位。
- 项目长远合理性: A 沿用代理写入已有的双重归属形状,全平台只有一种写法;B 多一种形状,但查询更好。将来审计查询确实需要时,可以从 A 升到 B,那时再加列和回填。
- 防 AI 写代码犯错: 归属字段只用于记录、不参与授权,沿用
attributedUserId的不变量,AI 不会误把它当主体。C 要覆盖所有写入口,漏一个就是静默缺口。 - 创业阶段不扩散: A 不加列、不加迁移,只是把已有事实写下来。
A's sub-points to confirm:
- Should the timeline show the administrator to everyone who can read the record, or only to administrators? The seat suggests everyone, because the record's owner has a reason to know.
- The empty
ip_addresson record writes is a related gap. The seat suggests filling it in the same change if it is cheap, and not letting it hold up the rest.
Blocks nothing.
Metadata
Metadata
Assignees
Labels
domain:serverDomain lane — seat card indexed by label:pm:seatDomain lane — seat card indexed by label:pm:seatenhancementNew feature or requestNew feature or requestpriority:p2Medium: important, M3Medium: important, M3
Activity
objectstack-fleet commentedon Nov 5, 2026
epic:#26676#1 · zhuangjianguo · session_cdb7a717def74e719988f089fbaa18ff
Ruling recorded: option A. The maintainer, verbatim, in the direct session on 2026-11-05: 「A」
Record the administrator on each write made in an impersonation session, in the additive shape agent writes already use. The subject is unchanged:
user_id,created_by, ownership and permissions stay the impersonated user's.Shape
impersonatedBy. Nothing in the authorization path reads it, the same invariant asattributedUserId.sys_audit_logadds the administrator inmetadata, for exampleimpersonated_by, present only on such a write. Absence means the user acted themselves.sys_activitycarries the same fact.Sub-points, read as the seat's stated recommendation. The maintainer's word did not name these separately, and if they say otherwise, their word replaces these lines:
ip_addresson record writes is filled in the same change if that is cheap. It does not hold up the rest.Verify before landing; report a fork if any of these fails rather than narrowing silently:
performed_byinui/packages/app-shell/srcorui/packages/plugin-detail/src, so a shared rendering that also shows agent writes is welcome if it costs little.Not in this change: a dedicated column and filter by administrator (option B). That can follow if the query proves needed.
Left for triage to route. No lane or claim is set here.
objectstack-fleet commentedon Nov 5, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: first-touch audit · p2 ·
domain:server·pm:queue· 2026-11-05T02:58Z⛔ Not a claim.
session.impersonatedBy. An audit-attribution gap, so p2. The PR states what the audit reader shows.Generated by Claude Code
objectstack-fleet commentedon Nov 5, 2026
domain:server#4 · os-elon-musk · session_016WV4czvZykUqhYxYXSSDt4
Claim: PM loop round 1 · 2026-11-05T21:40Z
Session:
session_016WV4czvZykUqhYxYXSSDt4Account:
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, named in a claim amendment once the thread reports itWorktree:
objectstack-issue-30213Domain:
domain:serverSeat:
domain:server#4File surface: the execution-context assembly that reads the session (
packages/core/src/security/assemble-execution-context.ts,packages/plugins/plugin-auth/src/auth-actor-attribution.ts), the one optional attribution-only field besideattributedUserIdinpackages/spec/src/kernel/execution-context.zod.ts, the audit and activity writers (packages/plugins/plugin-audit/src/**), every write door the ruling's verify list names (data route, actions, flows started by the write, comments), the console audit/timeline display of 「X 代 Y 操作」 underui/packages/**, their tests, one root.changeset/file. ⛔ No schema column, no migration (option B is not ruled). Stop on breach; explain in the reportContainer & model:
M,mode:thread,model: default(dispatch-gates --tier: Clause-② SUSPECT onpackages/spec/src/kernel/execution-context.zod.ts; floor sonnet · default opus · ceiling fable; build at default, review owed atCONTRACT_REVIEW_TIER),effort: highClause-②: yes
Metadata-protocol: none
Responsibility: n/a — not a defect card
Thread-read: 6648465257
Premise: holds per the ruling 6647756733 measured on
mainb7075d9908; the dev re-measures onorigin/mainfirst (an impersonation-session write stamps no administrator anywhere)Serial constraints cleared: none — no open PR's file list touches
audit-writers.ts,execution-context.zod.ts,assemble-execution-context.ts,auth-actor-attribution.tsorengine-execution-plumbing.ts(read over the 36 open PRs at claim time)Clause-② note: the ruling adds one optional attribution field to the published
ExecutionContextschema (a widening). It is built here under the ruling and owes one contract-review-tier review before the queue; if the dev can carry the fact without touchingpackages/spec/**, the claim amends toClause-②: no.objectstack-fleet commentedon Nov 5, 2026
domain:server#4 · os-elon-musk · session_016WV4czvZykUqhYxYXSSDt4
Claim amendment · claim 6670267078 · 2026-11-05T21:48Z
Branch:
claude/project-thread-8i10coDev session:
session_016WV4cxMph1oXW6ujSSVm1G(the card thread this seat opened; worktreeobjectstack-issue-30213, baseorigin/main47c457b2)File surface, added (measured by the dev; accepted by the seat as the doors ruling A's verify list names):
packages/spec/src/data/hook.zod.ts:HookContext.provenance.impersonatedBy(the audit writer reads provenance; the same shape asperformedByClientId).packages/spec/src/contracts/automation-service.ts(AutomationContext.impersonatedBy),packages/triggers/trigger-record-change/src/record-change-trigger.ts,packages/services/service-automation/src/runtime-identity.tsandrun-resume.ts(stripped on the approval decider switch).packages/runtime/src/sandbox/body-runner.tsandscript-runner.ts(sandbox provenance projection, so the key is never declared-but-unpopulated), and the CLI lowered-body enumeration pin.Clause-②: yes (widening; the contract-review-tier review before the queue stands). Metadata-protocol: none.
Fork recorded, not narrowed: comments write no
sys_audit_log/sys_activityrow at all today (sys_commentis inSKIP_OBJECTS), so stamping them needs a column or a new ledger row, both outside ruling A. The dev builds the data route, actions and flows, and reports comments as a fork for the maintainer.Everything else in claim 6670267078 stands.
objectstack-fleet commentedon Nov 6, 2026
domain:server#4 · os-elon-musk · session_016WV4cxMph1oXW6ujSSVm1G
os-dev-report
{ "issue": 30213, "status": "done", "branch": "claude/project-thread-8i10co", "pr": "https://github.com/objectstack-ai/objectstack/pull/30800", "session": "session_016WV4cxMph1oXW6ujSSVm1G", "premise_still_valid": true, "summary": "Ruling A built: ExecutionContext.impersonatedBy and HookContext.provenance.impersonatedBy (attribution only, never in session) set by resolveAuthzContext from the session read; plugin-audit stamps sys_audit_log.metadata.impersonated_by, sys_activity impersonated_by + impersonated_by_name, and the action_invocation row; record-triggered flows carry it on both runAs arms and the approval decider segment drops it; console timeline/history render detail.impersonatedActor in 10 locales. Comments are a reported fork (sys_comment is in SKIP_OBJECTS), not built; ip_address not filled (not cheap). No column, no migration.", "tests": "plugin-audit 3 files 30/30; core 341/341; trigger-record-change 52/52; service-automation 26/26; console 3091 + RecordDetailView 102/102 files 651/651. Ablation: deleting the audit stamp line in audit-writers.ts -> activity-actor-name ledger pin 1 failed / 14 passed; restored, blob == HEAD.", "gates": "turbo build 119/119 on head 4110e4e3; typecheck exit 0 x10 packages; dispatch-gates --ran: 116 derived, 116 run, 0 NOT-MEASURED (check-plugin-teardown-shape --self-test exit 0 after fetching its base commit in the shallow clone; first run exit 3 from shallow history). CI check-runs on 4110e4e3: pending at report time.", "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm", "line_budget": "45 files, +654 -28 = 682 < 3000", "files_changed": 45, "deviations": "Seat-accepted extra surface (comment 6670430008): spec hook.zod.ts provenance, automation-service AutomationContext, trigger-record-change, service-automation runtime-identity + run-resume, runtime sandbox, CLI pin. Agent writes (performed_by) not rendered by the new console line.", "mcp_calls": "4 — mcp__github__issue_read x2, mcp__github__pull_request_read x1 (reads only); subscribe_pr_activity x1 (session subscription, not a GitHub write)", "api_writes": "3 — git push (branch), relay pr_create (#30800, read-back identical 9906/9906 bytes), relay post-stamped comment on #30213", "open_questions": [ { "question": "Comments write no audit/activity row (sys_comment in SKIP_OBJECTS), so an impersonated comment is indistinguishable. Which way?", "options": [ "A — stop skipping sys_comment in the audit mirror: comments get ledger + activity rows with the stamp; every comment writes two more rows", "B — stamp on the comment row: sys_comment has no metadata/JSON field, so it needs a column + migration, excluded by ruling A; needs its own ruling", "C — leave as is, record a known gap in the ruling follow-up" ], "recommendation": "A, because it reuses the exact writer this PR stamps, needs no schema change, and gives comments the same ledger every other write has; cost is two rows per comment (S scope, one follow-up card)" } ], "security_findings": "none", "out_of_scope_findings": [] }Generated by Claude Code
chore(changeset): grade @objectstack/spec minor and declare Metadata-…
objectstack-fleet commentedon Nov 6, 2026
domain:server#4 · os-elon-musk · session_016WV4czvZykUqhYxYXSSDt4
Claim amendment · claim 6670267078 · 2026-11-06T03:55Z
Declarations corrected from the gate's measurement on PR #30800:
Clause-②: yes (widening),Metadata-protocol: added.ExecutionContext.impersonatedByenters thepackages/spec/authorable-surface/kernel.jsonshard, andcheck-changeset-no-majorreportednone-baseline-moved.@objectstack/spectakesminor. The changeset (head465a5099) and the PR body carry both lines. The contract review stays atCONTRACT_REVIEW_TIER(schema face).Everything else in claim 6670267078 (as amended 6670430008) stands.
fix(runtime): the flow action and manual trigger doors carry the impe…
objectstack-fleet commentedon Nov 6, 2026
domain:server#4 · os-elon-musk · session_016WV4czvZykUqhYxYXSSDt4
Landing record · 2026-11-06T06:39Z · PR #30800 merged through the queue as
fd749a45e(ACCEPT on head9d945da8; contract review FAIL 6676544432 on4110e4e3, fixed; delta re-review PASS 6677397495 on9d945da8). Ruling A has landed: a write made while impersonating stamps the administrator as attribution only (sys_audit_log.metadata.impersonated_by,sys_activityimpersonated_by+impersonated_by_name, theaction_invocationrow, and flows the write starts), and the console shows the "on behalf of" line. Follow-ups: #30851 (plugin-auth identity writes; the approval resume decider segment). Thesys_commentfork (A/B/C) stays with the maintainer. Card closed completed byFixes;pm:dispatchedremoved in this stroke.