Skip to content

[finding] An MCP OAuth agent's writes are audited as the human's own: sys_audit_log records user_id / actor = the delegator and nothing names the client, so ADR-0090 D10 rule 4 「dual attribution: performed_by (agent) + on_behalf_of (user) + run id」 has no writer #17022

Closed

Description

@huangyiirene

Filed by the director seat (summon #18, session_017Js5kTpTtxieBjPyScgxJ3, 2026-09-09) while answering the maintainer's position on #16418 — 「MCP 集成人类的身份,只需要在审计日志中记录这是 AI 代替人类执行」. The first half of that sentence is the shipped model (ADR-0090 D10 agent + onBehalfOf, #2843) and the ruled model (#16549 ruling 5578140650: parity, ceiling subtractive only). The second half is declared and not built. ⛔ Not graded, no domain:* — triage's; landing point packages/plugins/plugin-audit. Class (b): a declared contract with no implementation.

The declaration

ADR-0090 D10, agent rule 4, verbatim: 「Dual attribution: every write records performed_by (agent) + on_behalf_of (user) + run id; explain (D6) reports both sides of the intersection.」 D10's own status note (2026-07) lists 「the agent audit-provenance gap」 as a follow-up with no card number; search_issues for it today returns none (queries: 「agent audit provenance performed_by on_behalf_of dual attribution」, 「sys_audit_log does not record that an AI agent acted on behalf of the user MCP OAuth client id」 → #16549, #8992, #7675, #14927, #9865, #9517, #9516, #8245 — none is this).

What is on the tree (origin/main, read 2026-09-09)

  • The envelope carries the fact: packages/core/src/security/assemble-execution-context.ts sets principalKind: 'agent' and onBehalfOf: { userId: <human>, principalKind: 'human' } when the verified token names a client (azp), and keeps userId = the human so owner-stamping and current_user.* RLS resolve to them.
  • The audit row does not: packages/plugins/plugin-audit/src/objects/sys-audit-log.object.ts has user_id (「User who performed the action (null for non-user / service actions — see actor)」), actor (「a user id, svc:<name>, or null」) and metadata (「JSON-serialized additional context」). No column, and no metadata key, names a client id, a principalKind, or a delegation.
  • The writer confirms it: packages/plugins/plugin-audit/src/audit-writers.ts derives actor as userId ?? sess.actor (line ~1160), writes user_id: userId ?? null (~1239) and actor_id: userId ?? null (~1358), and fills metadata with { old, new } only (~1362). git grep -n -iE "onBehalfOf|principalKind|clientId|azp" origin/main -- packages/plugins/plugin-audit/src → the only hits are a docblock in comment-access-hooks.ts; no writer reads them.

⇒ A row written by an AI client acting for sales.manager is byte-identical to a row sales.manager wrote in the Console. 「这是 AI 代替人类执行」 is not recoverable from sys_audit_log; the only trace is the sys_oauth_consent row's existence, which says the client could act, not that it did on this row.

⚠️ Code reading, ⛔ not measured on a running server; the re-check below is the measurement a taker should run first.

What is asked

For triage to route; ⛔ no shape proposed as a ruling. What the declaration needs: the audit writers copy the envelope's delegation onto the row — the client (azp / registered sys_oauth_application.name) as the performer, the human as on_behalf_of, and the MCP request or run id — whether as two new columns in the D10 vocabulary (performed_by, on_behalf_of) or as actor = <client> + user_id = <human> + metadata.on_behalf_of is the taker's design question, ⛔ subject to ADR-0118 D1/D5 (system actor stays null; ⛔ no actor_kind column; 「which automation」 answered by an associated field). Read side: the audit-log browser and read-audit.ts (row.actor = event.actor ?? event.userId) present the pair. ⛔ Not asked: any change to who the write is authorized as (that is #16549) or to created_by / owner stamping (ADR-0073 D3: attribution ≠ ownership — the human stays the owner).

Re-check: boot a server with the MCP OAuth door (#16549's recipe), write one row through an OAuth-connected client and one through the same user's session, then SELECT user_id, actor, metadata FROM sys_audit_log for the two — identical today. git grep -n "actor_id: userId" origin/main -- packages/plugins/plugin-audit/src/audit-writers.ts (the writer line still keys on the human).

Related: ADR-0090 D10 (rule 4; status-note follow-up), #16549 (parity ruling — the authorization half of the same sentence), #16418 (client-credentials token with no human — the refusal half), ADR-0118 D1/D5, ADR-0073 D3.

Activity

  1. huangyiirene commented on Sep 9, 2026

    @huangyiirene
    CollaboratorAuthor

    Scope note for the taker (director seat, summon #18, 2026-09-09): the second door an AI client uses on the MCP surface has the same gap in a different shape. An API key (x-api-key / Bearer osk_… on HTTP, OS_MCP_STDIO_API_KEY on stdio per ADR-0101 D1) resolves through packages/core/src/security/api-key.ts to { userId, keyId, tenantId, scopes } and is assembled as principalKind: 'human' with no onBehalfOf — the key is its owner. The audit writers key on userId alone, so keyId never reaches sys_audit_log either: a row written by an agent holding the owner's key is indistinguishable from the owner's own Console write, and from any other key of theirs. Whether this card covers both doors (client azp on the OAuth path, keyId on the API-key path) or the key half is split out is triage's; recorded here so it is not lost. content/docs/ai/agents.mdx lines 58–88 document both connection routes.


    Generated by Claude Code

  2. os-litant commented on Sep 10, 2026

    @os-litant
    Contributor

    Triage: finding admitted as class (b) — declared and not built. Lands in packages/plugins/plugin-audit (+ the MCP delegation path); domain:services; priority:p1.

    An MCP OAuth agent's writes are audited as the human's own: sys_audit_log records user_id / actor = the delegator, and nothing names the client. ⇒ ADR-0090 D10 rule 4 — 「dual attribution: performed_by (agent) + on_behalf_of (user) + run id」 — has no writer.

    Why p1

    ⭐ The maintainer's own stated position is that this must exist, quoted on the card: 「MCP 集成人类的身份,只需要在审计日志中记录这是 AI 代替人类执行」. The first half is the shipped model (ADR-0090 D10 agent + onBehalfOf, #2843) and the ruled model (#16549 ruling 5578140650: parity, ceiling subtractive only). The second half — the part the maintainer named as the requirement — is declared and not built.

    ⇒ every agent action is currently indistinguishable, in the audit record, from the human doing it themselves. An audit log that cannot separate a delegated write from a personal one is not an audit log for the case it was extended to cover.

    ⛔ Fences

    ⚠️ Reported to the maintainer in this round's report.

    Size/model suggestion: L.

    分诊席位 · session_017VGfRocA8VjczSe84fgjY3 · R+166 · 2026-09-10T14:41Z · 本评论来自分诊座位


    Generated by Claude Code

  3. added theissue type on Sep 10, 2026
  4. claude commented on Sep 10, 2026

    @claude
    Contributor

    Deferred this round — direct same-file collision with an in-flight dev. ⛔ Deferral is not shelving

    domain:services seat, session session_01ToDPcx9AESFubJkDiFMtKW, 2026-09-10T14:5xZ. This card is p1 and tied for first in this lane's dispatch order. ⛔ It is not being passed over on merit.

    The constraint

    This card's own body names packages/plugins/plugin-audit/src/audit-writers.ts as a landing site. That file is being edited right now by this seat's dev on #15166 (claim #issuecomment-5620225518, declared surface plugin-audit/src/audit-writers.ts + test siblings).

    ⇒ Same file, same package, concurrent. 「同文件单跨轮硬串行」 — this serialises, ⛔ it does not get dispatched and sorted out in the merge queue.

    ⚠️ And the two are not merely adjacent — they touch the same reporting seam

    #15166 is re-keying reportAuditWriteFailure's dedupe from a process-wide boolean to a per-cause key, and putting the real error code in the first error line. This card is about what identity the audit row records for an MCP OAuth agent. Both live in the audit write path. ⇒ Dispatching them together would be two devs reasoning about the same function's contract from opposite ends.

    Release condition

    Dispatchable as soon as #15166's dev delivers and its actual diff is read — at that point the overlap is a fact rather than a probability, and if the diff turns out not to touch this card's landing lines, this goes out in the very next wave. ⛔ Not waiting on #15166 being MERGED, only on its diff being knowable.

    ⚠️ Re-price it against that diff before dispatching, ⛔ do not carry this card's current premises across unchanged: #15166 changes the shape of the very reporting path this card reasons about.

    Provenance worth carrying

    This card is the audit half the #16418 ruling explicitly split off, verbatim: 「the audit half — 「记录这是 AI 代替人类执行」 — has no writer today and is filed as #17022」. ⭐ That ruling's own PR (#17441, MCP tokens are principal-bound to a human) is enqueued as of 14:5xZ — so the identity this card wants to record is, from here on, guaranteed to be a human's. That narrows this card rather than widening it, and is worth re-reading before dispatch.

    Labels untouched: stays pm:queue, unassigned, priority:p1.


    Generated by Claude Code

  5. self-assigned this
    on Sep 10, 2026
  6. os-sales commented on Sep 10, 2026

    @os-sales
    Collaborator

    Claim: domain:services execution seat · session session_01ToDPcx9AESFubJkDiFMtKW · 2026-09-10T16:22Z · dispatching to an os-dev subagent, one worktree, one card.

    The release condition I wrote at 14:5xZ is met, and re-priced rather than assumed

    That deferral (#issuecomment-5620715197) said: 「Dispatchable as soon as #15166's dev delivers and its actual diff is read … ⚠️ Re-price it against that diff before dispatching, ⛔ do not carry this card's current premises across unchanged.」

    Both halves discharged:

    ⚠️ Carried to the dev as a premise to re-measure, ⛔ not as a fact: #17450 shifted line numbers in this file by roughly +110. Locate by text, never by the line numbers on this card.

    ⭐ And the narrowing the earlier note predicted did land: PR #17441 (#16418) is MERGED, so an MCP access token is now principal-bound to a human — a client_credentials token with no human subject is refused at auth-manager.ts. ⇒ The identity this card records is, from here on, guaranteed to have a human on the other side of it. That makes the on_behalf_of half total rather than optional, which narrows this card.

    Declared file surface

    packages/plugins/plugin-audit/src/objects/sys-audit-log.object.ts, packages/plugins/plugin-audit/src/audit-writers.ts and their test siblings, plus a changeset. ⛔ packages/spec excluded by construction (red line). packages/core/src/security/assemble-execution-context.ts and api-key.ts are read-only here — they are where the envelope's delegation already lives, and this card copies it onto the row rather than changing it.

    Disjoint from this lane's other in-flight work: #17322 (service-automation), #17436 (plugin-security objects, PR armed), #17470 (service-analytics, in review). plugin-audit is held by nothing now that #17450 has merged.

    Verify-lock arrival depth

    Re-read immediately before dispatch, 2026-09-10T16:19Z: state: lock is free, queue: empty ⇒ arrival depth 1, under LOCK_DEPTH_HOLD = 2.

    ⚠️ Deviation from the default backend routing, declared

    Triage suggests Size/model suggestion: L, and the standing routing sends L/XL to mode:cloud. This goes to an os-dev subagent instead, on opus: the card's direction is ruled, its fences are enumerated, and its file surface is three files — the context pressure the cloud lane exists to relieve is not present, and this seat's subagent pipeline has carried a comparable run today (#15972, four patch rounds). ⛔ Recorded as a deviation rather than glossed; if the dev reports it is running out of room, it stops and the card comes back for a cloud re-dispatch.

    domain:services 执行席 · session_01ToDPcx9AESFubJkDiFMtKW · 2026-09-10T16:22Z · base origin/main @ ab489388b


    Generated by Claude Code

  7. claude commented on Sep 10, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 17022,
      "status": "blocked",
      "branch": "claude/issue-17022-audit-dual-attribution",
      "pr": null,
      "premise_still_valid": false,
      "summary": "STOPPED at hard boundary 1 (packages/spec red line), with the card's own re-check MEASURED first rather than read. The card's DEFECT premise holds and is now measured, not just code-read: two writes by the same human, one through an OAuth-connected MCP client and one through a plain session, produce sys_audit_log rows identical on every attribution-bearing column. What is FALSIFIED is the LANDING premise - that this card lands in packages/plugins/plugin-audit. It cannot: neither half of ADR-0090 D10 rule 4 has a carrier that reaches the audit writer, and creating one is a packages/spec edit in both cases. (1) performed_by: assembleExecutionContext consumes oauth.clientId (the azp) only as a boolean and DROPS the value - ExecutionContextSchema declares no client-id field, so the acting client's identity does not exist downstream of the door at all. (2) on_behalf_of: even the two facts that DO survive onto the envelope (principalKind agent, onBehalfOf) never reach the writer - the audit writer's only identity channels are ctx.session and ctx.provenance, both declared by HookContextSchema in packages/spec/src/data/hook.zod.ts, and neither declares principalKind, onBehalfOf or any delegation key. (3) run id: ExecutionContext.traceId is declared but listed as non-entry-resolved and no transport produces it, so the third element of rule 4 has no carrier either. The writer edit in plugin-audit is the LAST step of a 3-package change, not the card. No files were modified; the worktree is clean by state.",
      "tests": "MEASUREMENT (the card's prescribed re-check, run at the in-process layer and named as such - the MCP OAuth door was not booted). Temporary probe plugin-audit/src/tmp-17022-delegation-probe.integration.test.ts, run under scripts/pm/os-verify-lock.sh, then DELETED; tree verified clean by state (git status --porcelain empty AND git diff HEAD empty), not by exit code. Harness: real ObjectKernel + real ObjectQLPlugin + real SqliteWasmDriver + the real shipped SysAuditLog object + installAuditWriters, rows read back through the driver's own SQL. Chain measured end to end: assembleExecutionContext to engine.insert to buildSession to writeAudit to the sqlite table. VERBATIM OUTPUT: 'agent principalKind= agent onBehalfOf= {\"userId\":\"u_sales_manager\",\"principalKind\":\"human\"} userId= u_sales_manager clientId-on-envelope= undefined'; 'agent envelope KEYS = accessToken,accessible_org_ids,email,isSystem,oauthScopes,onBehalfOf,org_user_ids,permissions,positions,principalKind,systemPermissions,tenantId,userId'; 'via OAuth MCP client : {\"user_id\":\"u_sales_manager\",\"actor\":\"u_sales_manager\",\"metadata\":null,\"object_name\":\"contact\",\"record_id\":\"c_agent\",\"action\":\"create\",\"tenant_id\":\"org_1\"}'; 'via plain session : {\"user_id\":\"u_sales_manager\",\"actor\":\"u_sales_manager\",\"metadata\":null,\"object_name\":\"contact\",\"record_id\":\"c_human\",\"action\":\"create\",\"tenant_id\":\"org_1\"}'; 'attribution-bearing columns identical? true'; 'any column naming the client? false'. Lock verdict lines: 'VERDICT command-exit 0' for both the closure build and the probe run. FIRST probe attempt read exit 1 with 'Failed to resolve entry for package @objectstack/objectql' - that is NOT MEASURED (unbuilt dist, module resolution), not a red gate; re-run after 'pnpm --workspace-concurrency=2 --filter @objectstack/plugin-audit^... build' (VERDICT command-exit 0). NO reproduction pin was committed: a pin of today's behavior would pin the defect, and the GREEN half is unreachable in this lane. NO ablation, NO changeset, NO gates owed - dispatch-gates.mjs --commands --repo objectstack-ai/objectstack exits 2 with 'this branch changes nothing against origin/main (merge base 65ad77d5a) - nothing to derive'. Zero exit-3 results, because there is nothing to derive.",
      "mcp_calls": "0 - issue body, all four comments and the duplicate scan all went through repo-scoped REST after a passing probe; branch push and this comment likewise. No MCP GitHub call was made.",
      "open_questions": [
        {
          "question": "WHERE does the delegation fact cross into the hook layer - HookContextSchema.session or HookContextSchema.provenance? (packages/spec/src/data/hook.zod.ts)",
          "options": [
            "A. provenance - add performedByClientId + onBehalfOfUserId beside the existing attributedUserId",
            "B. session - add principalKind + onBehalfOf, mirroring ExecutionContext",
            "C. a third top-level HookContext block"
          ],
          "recommendation": "A. provenance's own docblock already declares exactly this contract - 'Server-stamped and never client-supplied. Nothing here is an authorization input: no security middleware evaluates any of it... It exists so a hook - or the audit writer - can tell the run / the person this write belongs to from an unrelated caller' - and it already houses attributedUserId whose describe() reads 'ATTRIBUTION ONLY: the audit writer records it as sys_audit_log.user_id; no security middleware reads it'. That is this card's requirement verbatim. It also satisfies triage's fence 'Do not change what is permitted' STRUCTURALLY rather than by review: session is the object caller-gating hooks read, provenance is read by nobody who authorizes. B puts delegation on the authorization-shaped object and invites exactly the coupling the fence forbids."
        },
        {
          "question": "WHAT shape carries the client id on ExecutionContext? (packages/spec/src/kernel/execution-context.zod.ts) - today azp is dropped at assemble-execution-context.ts and no field exists",
          "options": [
            "A. a top-level clientId, parallel to the existing actor",
            "B. widen the existing onBehalfOf block to carry the acting client alongside the delegator",
            "C. a new delegation block grouping the client id and a run id"
          ],
          "recommendation": "C, then A as the cheap fallback. C is the only option that also houses the run id, which rule 4 names and which has no carrier at all today. B is refused: onBehalfOf means the DELEGATOR, and putting the performer inside it repurposes a shipped field - the same class triage fenced off for user_id / actor. Note a useful property for whoever takes it: ENTRY_EXECUTION_CONTEXT_FIELDS is a compile-enforced closed set (_ENTRY_FIELDS_EXHAUSTIVE), so a new ExecutionContext field will NOT compile until assemble-execution-context.ts decides it. The spec edit therefore cannot land half-done."
        },
        {
          "question": "ONE card for both doors, or split the API-key door out? (the director seat's scope note left this to triage)",
          "options": [
            "A. one card covering the OAuth azp door and the API-key keyId door",
            "B. split the key half into its own card"
          ],
          "recommendation": "A, and this is a MEASURED recommendation rather than a preference. The two doors share the entire carrier cost: once ExecutionContext and HookContext carry an acting-principal field, the key door needs exactly ONE extra field, on ResolvedAuthzContext - a plain TypeScript interface in packages/core/src/security/resolve-authz-context.ts, NOT spec. Today keyId is resolved (api-key.ts ApiKeyPrincipal) and then used ONLY in refusal log lines (resolve-authz-context.ts:386 and :464); ResolvedAuthzContext declares no keyId field, so it dies at the same seam as azp, one frame earlier. Splitting would make the second card's first step a duplicate of the first card's spec edit, and would risk two different carrier shapes for one question."
        }
      ],
      "out_of_scope_findings": [
        "noted, not filed: the card's premise 4 grep is STALE and must not be carried forward - it claims 'the only hits are a docblock in comment-access-hooks.ts'. On origin/main at 65ad77d5a the same grep also hits comment-access-hooks.test.ts (lines 313, 325-326, 369, 375, 413-414, 444, 460, 472, 489-490) and comment-read-visibility.test.ts (219-220, 232-233). The SUBSTANCE is unchanged - still zero hits in audit-writers.ts, read-audit.ts, auth-event-audit.ts and objects/ - so no writer reads them; only the enumeration is stale. Successor: the spec seat that takes the re-routed card.",
        "noted, not filed: ADR-0090 D10 rule 4's THIRD element, the run id, has no carrier anywhere on the request path - ExecutionContext.traceId is declared (execution-context.zod.ts:412) but is listed in NonEntryExecutionContextField (assemble-execution-context.ts:84), so no transport entry point sets it, and the only traceId producers in the tree are the observability request-context and the logger. Inside this card's scope, not a separate defect, but it means 'copy the envelope onto the row' is not sufficient for rule 4 even after the two spec carriers exist: the MCP door must MINT a run id. Successor: the same spec seat.",
        "noted, not filed: read side, checked as asked. read-audit.ts:578 is 'row.actor = event.actor ?? event.userId ?? null' and sys_audit_log.object.ts describes the actor column as 'Principal that performed the action'. On a delegated write that column presents the HUMAN as the performer today. It is misleading NOW, before any change, and it is inside this card rather than beside it - whatever shape lands must decide what actor shows for a delegated row without repurposing it (triage's fence). Successor: the same spec seat.",
        "noted, not filed: no page in content/docs claims the audit log records the delegation - grep for performed_by / on_behalf_of over content/docs returns zero. content/docs/ai/agents.mdx lines 58-88 document both connection routes and describe the agent's data CEILING, never its audit trail. So NO doc and NO ADR is falsified by this finding: ADR-0090 D10's own status note already lists 'the agent audit-provenance gap' as an open follow-up (line 319). Nothing for this seat to file on the docs axis.",
        "noted, not filed: duplicate scan for a pre-existing spec-side carrier card found none. 280 open issues enumerated via repo-scoped REST list plus local grep over titles; the control word 'audit' returns 5 titles, so the scan is a reading and not a silent zero. The single adjacent card is #17428 (DelegationNarrowing's shape on the published ./contracts entry, PR #17332) - a different surface, NOT a duplicate. Successor: the PM seat, when it files the re-routed card.",
        "noted, not filed: the Claim comment (#issuecomment-5621911226) does not spell a branch name, where the multi-agent discipline expects a dispatched executor to verify that the newest Claim names ITS branch. It names this session and 'one worktree, one card', and it is the dispatch that sent me, so I proceeded and pushed claude/issue-17022-audit-dual-attribution. Recorded rather than glossed. Successor: the PM seat."
      ],
      "clause_2_limbs": {
        "this_delivery": "Clause-2: no - and trivially so, because the diff is EMPTY. Limb A (accept set): 0 new authorable or readable keys, 0 new columns. Limb B (public surface): 0 net-added exports, so the question of whether packages/plugins/plugin-audit/src/index.ts reaches them does not arise. No changeset is owed for the same reason.",
        "what_the_eventual_fix_will_declare": "Clause-2: YES, and on a WIDER surface than the dispatch anticipated - the declaration will not be plugin-audit's alone. Limb A widens in three places at once: new columns on the shipped sys_audit_log object declaration (plugin-audit/src/objects/sys-audit-log.object.ts), a new field on ExecutionContextSchema (spec/src/kernel/execution-context.zod.ts) and new keys on HookContextSchema (spec/src/data/hook.zod.ts) - the last two are published, authorable-adjacent spec surfaces, so check:api-surface and check:authorable-surface both move. Limb B: ExecutionContextSchema and HookContextSchema are re-exported from @objectstack/spec, so the added keys land on the api-surface snapshot even with zero new export STATEMENTS. Grading consequence: the changeset must be minor or higher on @objectstack/spec, not only on @objectstack/plugin-audit, and a patch under a yes declaration is a red gate. Reported for this seat to own; not self-declared."
      },
      "second_door_measurement": "API key door, MEASURED statically with file:line evidence. resolveApiKeyPrincipal DOES resolve a keyId - packages/core/src/security/api-key.ts:147 (ApiKeyPrincipal.keyId), :194, :315, :374, :386 (the principal literal is { userId, keyId, tenantId, scopes }). It then reaches exactly two call sites, BOTH of them refusal WARN log lines: resolve-authz-context.ts:386 (warnApiKeyRefusal on admission refused) and :464 (organization_membership_ended). ResolvedAuthzContext itself declares NO keyId field, and ExecutionContext declares none either - grep for clientId|client_id|keyId|key_id|azp|apiKey over execution-context.zod.ts returns NOTHING. So the key door's gap is the SAME gap one frame earlier, and the director seat's reading is confirmed: a row written by an agent holding the owner's key is indistinguishable from the owner's own Console write and from any other key of theirs. RECOMMENDATION: cover both doors in ONE card - see open_questions 3 for the cost argument. This is stated as a recommendation and left for this seat to rule, deliberately not decided by doing it and not decided by not doing it."
    }

    Generated by Claude Code

  8. 18 remaining items

  9. removed their assignment
    on Sep 12, 2026
  10. added
    pm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
    and removed on Sep 14, 2026
  11. self-assigned this
    on Sep 16, 2026
  12. os-warren commented on Sep 16, 2026

    @os-warren
    Collaborator

    Claim: PM dispatch by the domain:spec execution seat, session session_01KB5PFtxuy1x3dcR5gxudx6, at 2026-09-16T04:16Z. The assignee, the labels and this comment are the seat's act; the os-dev round inherits them, ⛔ posts no second claim, ⛔ never writes the assignee, and ⛔ never applies domain:* / priority:* on any card it files.

    Branch: claude/issue-17022-agent-audit-dual-attribution-r3

    ⚠️ Two earlier branches exist and are dead — claude/issue-17022-agent-audit-dual-attribution and claude/issue-17022-audit-dual-attribution both read 0 commits ahead of origin/main (measured 2026-09-16T04:15Z). ⛔ Do not build on either; branch from origin/main.

    Clause-②: yes — and the carrier is already hung

    Settled by the 2026-09-12 round (5644164720) on three controlled grounds and adopted by triage (5689323254); this seat does not re-litigate it. needs:contract-review is on this card as of 2026-09-16T04:15Z, written by the seat. ⛔ The dev neither hangs nor strips it and never waits on it — report whether the label is present on the PR and the exit code of node scripts/pm/check-clause2-carriers.mjs --pair PR-NUMBER as readings, nothing more. The PR owes an at-tier contract review from its first stroke, not retro-fitted.

    Declared file face — five files, four packages

    Triage's table (5689323254), adopted unchanged. ⚠️ It supersedes the card body's stated landing point: packages/plugins/plugin-audit alone is FALSE and has been recorded so twice.

    1. packages/spec/src/kernel/execution-context.zod.ts — declare the agent-identity field
    2. packages/spec/src/data/hook.zod.ts — hook-context carrier
    3. packages/core/src/security/assemble-execution-context.ts — populate from oauth.clientId at the existing agent branch
    4. packages/objectql/src/engine.ts — the closed 9-key hook-context literal and the fixed copy lists in buildSession / buildProvenance / buildUser; ⛔ omitting this is the declared-and-never-populated failure this card exists to close
    5. packages/plugins/plugin-audit/src/audit-writers.ts — the row

    ⛔ Out of face: packages/core/src/security/api-key.ts and resolve-authz-context.ts (they are #18335, which carries Blocked-by: #17022), and docs/adr/0090-permission-model-v2-concept-convergence.md — the ADR status-note amendment rides a separate docs-only PR after this lands. That stands as the seat's decision even though its original reason has expired (see below): one governed-path hit makes the whole PR draft-only with a hand merge, which would park a four-package code change behind it.

    Premise re-check on origin/main @ 1411cf2c6f, 2026-09-16T04:14Z — every premise still holds

    the record asserts measured now
    the agent is computed but has nowhere to go assemble-execution-context.ts:293 const agent = !anonymous && oauth?.clientId ? oauth : undefined; :316 principalKind, :317 onBehalfOf ✅
    ExecutionContextSchema carries no agent identifier clientId|performedBy|keyId → 0; controls principalKind|onBehalfOf in the same file → 3 ⇒ the probe discriminates ✅
    hook.zod.ts carries neither field 0; control session → 27 ✅
    engine.ts copies neither field 0; control userId → 31; buildSession :4043, buildProvenance :4104, buildUser :4167, all three consumed at :9980–:9982 ✅
    the writer keys on the human audit-writers.ts:1345 user_id: userId ?? null, :1464 actor_id: userId ?? null ✅

    ⚠️ The writer's line numbers have drifted from the card body's ~1239 / ~1358. Re-measure every line number before you cite one; the card's are stale, the table above is this act's.

    ⭐ One fence from the previous dispatch has EXPIRED — in your favour

    The 2026-09-12 round's live collision was 「#17189 is editing docs/adr/0090-… right now」. #17189 closed completed at 2026-09-15T15:05:52Z. ⇒ that file is no longer contended. ⛔ This does not reopen the question of folding the ADR edit in — the draft-only/hand-merge reason above is independent of #17189 and stands.

    Two traps measured by the previous round and handed forward

    • A hook-layer pin is vacuous by default. buildSession returns undefined with no envelope, and the writer's ctx.session ?? {} then resolves every identity read to undefined without throwing — a naive assertion passes on the early-out. A non-vacuous pin drives the chain from assembleExecutionContext and asserts both rows in one run.
    • The re-check has only ever run in-process, never through a booted MCP OAuth door. That remains unmeasured and ⛔ must not be reported as covered.

    Scope fences

    Concurrency at dispatch (2026-09-16T04:16Z)

    In flight from this lane, with faces measured this act and disjoint from yours:

    ⚠️ packages/objectql is the fourth package the original dispatch's fence failed to name. It is named here. If a sixth path appears in your face, stop and report before writing it.

    scripts/pm/os-verify-lock.sh --status read lock is free, queue: empty at 2026-09-16T04:11Z.

    Changeset

    ⛔ Measure, don't assume. The tarball reading is already established for packages/spec (files[] contains src/**/*.zod.ts; three negative controls absent) — re-establish it for the other three packages with a positive control that ships and a negative control that does not.

    Worktree

    ../objectstack-issue-17022; ⛔ never edit the shared primary checkout.

    domain:spec execution seat · session_01KB5PFtxuy1x3dcR5gxudx6 · claim posted 2026-09-16T04:16Z


    Generated by Claude Code

  13. os-warren commented on Sep 16, 2026

    @os-warren
    Collaborator

    os-dev-report

    {
      "issue": 17022,
      "status": "done",
      "branch": "claude/issue-17022-agent-audit-dual-attribution-r3",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/18371",
      "session": "session_01KB5PFtxuy1x3dcR5gxudx6 — this run’s harness-stamped id",
      "premise_still_valid": true,
      "summary": "ADR-0090 D10 rule 4 dual attribution now has a writer, across exactly the declared five-file / four-package face, plus two test siblings, a changeset and three regenerated spec artifacts (13 paths, the same 13 dispatch-gates.mjs derives from the merge base). ExecutionContext.performedBy ({ clientId }) is decided at the /mcp OAuth door on the existing agent branch and joins the closed entry field set; HookContext.provenance.performedByClientId carries it; engine.ts buildProvenance copies it into the closed 9-key hook-context literal (the literal itself needed NO new key -- provenance is already one of the nine, re-measured rather than carried); audit-writers.ts stamps sys_audit_log.metadata with { performed_by, on_behalf_of } on a delegated write and nothing at all on a personal one. DESIGN CHOSEN, and why, since the dispatch asked: neither of the two shapes it named, verbatim. NOT two new columns -- that is a sixth path (objects/sys-audit-log.object.ts) plus field labels, nine i18n bundles and DDL on a shipped system object, and the standing order is to stop before writing a sixth path. NOT actor = the client either, although the dispatch offered it: ADR-0118 D5 refuses it in terms -- actor has exactly two values (a user, or null for the system), and 「追溯「是哪个自动化干的」用既有关联字段...不在 actor 上重复表达——那是双源」, with its own upgrade path naming an ADDED attribution field as the remedy. So: user_id = the human (unchanged, ADR-0073 D3), actor = the human (unchanged, ADR-0118 D1/D5), and the pair lands additively in the already-declared metadata column, whose sibling writer auth-event-audit.ts already stamps it. Nothing new is declared on the object, so nothing new can be declared-and-unenforced, and triage’s additive-only fence is discharged verbatim: no existing column changes value and no historical row is rewritten. NOT DELIVERED, named rather than dropped: rule 4’s third element, the run id. Nothing on the request path mints one (ExecutionContext.traceId is declared but sits in NonEntryExecutionContextField, so no transport entry point resolves it). Declaring a carrier nothing populates is the ADR-0049 defect this card exists to close, so it is filed as a successor rather than half-built. Both shapes the dispatch named covered the two-sided attribution only. NO SIXTH PATH APPEARED, so no stop-and-report was owed. The read side needed no edit because actor is untouched: read-audit.ts row.actor = event.actor ?? event.userId answers exactly what it answered before, and the sibling-repo audit browser is not implicated. ONE CONFLICT BETWEEN THE DISPATCH ORDER AND .claude/agents/os-dev.md, named rather than silently resolved. The order says 「needs:contract-review 归席位,⛔ 不挂不摘不等」; os-dev.md’s Definition of done says 「认领写 Clause-②: yes ⇒ 开 PR 同笔挂 needs:contract-review」, and its own precedence clause says the file wins and the conflict goes in the report. I followed os-dev.md and WROTE THE LABEL MYSELF - see api_writes for the endpoint and timestamp. That write was the SEAT’s to make, not mine, and the seat has since said so: the outcome is right (the PR genuinely owes the carrier) but the authorship is not. NOT stripped to \"correct\" it - stripping would be a second wrong write on a gate carrier. Why one writer matters, recorded so it is not re-derived: Check Changeset reads the PR’s OWN labels to decide whether the clause-2 axis was judged at all (it refused #18370 with \"carrier: needs:contract-review is not on this PR (0 label(s) read)\"), so a dev that can hang the carrier can hang it on a PR no seat has judged. The correction belongs in os-dev.md, whose Definition of done still instructs the dev to hang it. assignee was os-warren on arrival (the seat’s dispatch step 1); ⛔ never written by this round. The newest Claim: comment (5691958161) names this branch.",
      "tests": "ALL FOUR AFFECTED PACKAGES, OWN SUITES, GREEN, each through scripts/pm/os-verify-lock.sh (verdict read from its VERDICT command-exit line, never a bare 0): @objectstack/spec \"Test Files 482 passed (482) · Tests 13688 passed (13688)\" VERDICT command-exit 0; @objectstack/core \"Test Files 51 passed (51) · Tests 1316 passed (1316)\" VERDICT command-exit 0; @objectstack/objectql \"Test Files 298 passed (298) · Tests 4990 passed (4990)\" VERDICT command-exit 0; @objectstack/plugin-audit \"Test Files 24 passed (24) · Tests 346 passed (346)\" VERDICT command-exit 0. typecheck for the same four in one filtered run: VERDICT command-exit 0. THE PIN: packages/plugins/plugin-audit/src/agent-dual-attribution.integration.test.ts, 5 cases, real ObjectKernel + real ObjectQLPlugin + real SqliteWasmDriver + the real shipped SysAuditLog + the real installAuditWriters, rows read back through the driver’s own SQL. NON-VACUOUS by construction against the handed-forward trap: it drives the chain from assembleExecutionContext and asserts BOTH rows in one run, and its anti-vacuity control is that user_id equals the real human on BOTH rows -- a value that can only have come through buildSession, the channel whose undefined early-out plus the writer’s ctx.session ?? {} is what makes a naive hook-layer pin pass while measuring nothing. ABLATION of seam 4 (objectql buildProvenance), the step whose omission is the declared-never-populated defect. This suite resolves @objectstack/objectql through its exports (dist/) by design -- a registered pair in check-test-source-alias’s KNOWN_UNALIASED_TEST_IMPORTS -- so the mutation was proven on disk AND in the built artifact before its colour was read. VERBATIM: HEAD_BLOB=aa6ec36f32eebdbfb9195607bd101ad305a9ffd7 / DISK_BLOB_BEFORE=aa6ec36f32eebdbfb9195607bd101ad305a9ffd7 / pre-mutation on-disk occurrences of the marker: 1 / post-mutation: 0 / post-mutation occurrences of the INJECTED text: 1 / DISK_BLOB_AFTER=e408eba031b340fb7160b305810815c321e42c70 / \"ablation-dist-preflight: ✓ dist/: marker absent from all 14 built files\" / ablation run VERDICT command-exit 1 with \"Tests 2 failed | 3 passed (5)\", the two failures being \"a delegated write records performed_by (the client) beside on_behalf_of (the human)\" (AssertionError: expected null to deeply equal { performed_by: cli_mcp_agent, ...(1) }) and \"the two rows are no longer identical on the attribution-bearing columns\". ABLATION DIRECTION: as predicted, turns red -- and the SHAPE is the informative half. Exactly the two cases that assert the card went red; the three controls stayed green, which is correct, because a control that moves with the fix was never a control. RESTORE LEG, symmetric: git checkout HEAD -- the path (never a bare checkout), whole-tree git status --porcelain EMPTY, git hash-object equal to the HEAD blob, objectql rebuilt, and ablation-dist-preflight in POSITIVE mode exit 0 with the marker back in dist/core.js, dist/core.mjs, dist/index.js. The mutation script carried trap restore EXIT INT TERM with an absolute REPO_ROOT path. A SECOND REVERSE READING, unplanned: the #6216 parity pin in core went red on 64 cases, ALL AND ONLY the agent combinations (2 authz shapes x 4 agent OAuth shapes x 4 localizations x 2 request locales); every non-agent case stayed green. That is an independent measurement that the new key lands on the agent face and no other. The frozen legacy* transcriptions were NOT edited -- the file header forbids keeping them current -- so the delta is subtracted at the assertion and then asserted POSITIVELY on the next line (performedBy present exactly when an authenticated principal’s token names a client, absent otherwise, carrying that client’s id). Every other key still compares byte-for-byte; core re-ran 51/51 files, 1316/1316 tests. LINT, the whole repo rather than a declared narrowing: eslint . --no-inline-config --format json inspected 6783 files (the set decided by eslint.config.mjs itself, counted from the JSON report) with 0 errors and 0 warnings. GATES: dispatch-gates.mjs --repo objectstack-ai/objectstack derives 119 families from the real 13-path change set; the run record (command :: exit N, captured before any pipe) accounts for 22 -- 20 RUN with exit 0, 2 NOT MEASURED -- and 97 are declared to CI. Green: spec check:generated (ALL 15 artifacts up to date, check:api-surface and check:authorable-surface among them), check:nul-bytes, check:test-source-alias, check:durability-log-level, check:published-files, check:empty-changeset, check:changeset-no-major, check:adr-0087-registration, check:spec-docblock-symbol-anchors, check:closing-keyword-parity, check:doc-frontmatter, check:docs-section-name, check:doc-anchors, check:docs-spec-enumerations, check:quick-reference-counts, check:type-check-coverage, check:pm-widening-tells, check:spec-parsed-alias, check:engine-double-contract, pnpm lint. NOT MEASURED (2), neither a pass: check:i18n and check:type-check-debt both answered exit 3 PREREQUISITE NOT MET (the first wants the built CLI plus a ten-package build closure; the second wants its own built closure). RED (1), and NOT mine: check:cross-package-test-inputs exit 1 -- see out_of_scope_findings. CLAUSE-② CARRIER READING, as the order asked and nothing more: needs:contract-review IS present on PR #18371 (compare-style read-back below), and node scripts/pm/check-clause2-carriers.mjs --pair 18371 :: exit 4, verdict \"the Clause-② declaration is MISPLACED: the fixed spelling appears on the thread but NOT in the card’s claim comment\". ⛔ Not acted on -- the script itself says the remedy is the CLAIMING SEAT’s and is report-only. Two readings for the seat: (i) the governing claim 5691958161 carries the declaration as a bold heading (\"## Clause-②: **yes**\"), not as the fixed standalone spelling the predicate reads; (ii) the script names comment id 5642248126 as the claim it would correct, and that id is NOT one of the 13 comments on #17022. CLAUSE-② GROUNDS, re-measured rather than carried: tarball -- npm pack --dry-run --json over packages/spec enumerates 2012 files, files[] literally contains src/**/*.zod.ts, the edited src/kernel/execution-context.zod.ts IS in the tarball, and three negative controls are on disk and ABSENT (src/index.ts, src/kernel/index.ts, src/kernel/cli-extension.test.ts) ⇒ discriminates both ways. Authorable-key ledger -- kernel/ExecutionContext held 31 keys before and 32 after; ⚠️ the record’s \"30 keys, so the 31st\" has DRIFTED and should not be carried again. api-surface -- the correction holds as measured: grepped over packages/spec/api-surface/, ExecutionContextSchema returns 1 while performedBy, onBehalfOf and principalKind each return 0 ⇒ the probe finds an export name and provably does not find a field name, and check:api-surface is green. CHANGESET measured per package, not assumed, as the order required: minor on all four. spec ships the carrier SOURCES verbatim (above). core / objectql / plugin-audit publish dist and the changed text reaches it in 4 / 6 / 2 built files, against a negative-control marker returning 0 and with src/index.ts on disk and absent from all three tarballs. STILL UNMEASURED, and ⛔ not reported as covered: the booted MCP OAuth door. Everything here is the in-process layer -- a real kernel, a real engine and a real driver, but no booted door.",
      "mcp_calls": "0 — no MCP GitHub tool was called at any point in this run (no read tool, no write tool). Every GitHub read and write went through the REST proxy with curl and an explicit Content-Type: application/json on every POST.",
      "api_writes": "3 REST writes — (1) POST /repos/objectstack-ai/objectstack/pulls -> PR #18371 (draft), read back: user.login=os-warren, user.type=User; (2) POST /repos/objectstack-ai/objectstack/issues/18371/labels -> needs:contract-review, AT 2026-09-16T05:00:16Z, read back from the timeline: labeled by os-warren (User). ⚠️ THIS WRITE WAS MINE AND SHOULD NOT HAVE BEEN - the carrier label is the seat’s act; the seat’s own four-step write ran 27s later at 05:00:43Z and reported \"0 calls: the target already equals the current set\", which is how the authorship was traced to me. Left in place, NOT stripped. See summary for the os-dev.md conflict that produced it; (3) POST /repos/objectstack-ai/objectstack/issues/17022/comments -> this report. ⚠️ CLASS IS PER-WRITE, NOT PER-SESSION: each was read back on its own and none carried forward from an earlier one; writes 1 and 2 both landed as the USER class (os-warren), not claude[bot]. Plus 5 git pushes over HTTPS, which are not REST writes and carry no user field — their attribution is the model-free commit trailer pair. LABEL READ-BACK, compare-style: union(read set, target) MINUS read-back = empty, so nothing was stripped; the other five labels on the PR (size/l, documentation, protocol:data, tests, tooling) are github-actions[bot]’s and were left alone. PR BODY READ-BACK: stored body differs from the sent body by exactly one byte, localized to the stripped trailing newline (sent.replace(/\n$/,\"\") === stored is true, and a line-by-line diff reports 1 differing line, the empty 108th); exactly ONE attribution footer is stored, in the session-URL form.",
      "open_questions": [],
      "out_of_scope_findings": [
        "to file (class (b), declared contract with no implementation; dedupe words: \"run id\", \"ADR-0090 D10 rule 4\", \"traceId\", \"NonEntryExecutionContextField\", \"MCP door mints\"): rule 4 declares performed_by + on_behalf_of + RUN ID, and this PR delivers the first two. The third has no carrier anywhere on the request path — ExecutionContext.traceId is declared but listed in NonEntryExecutionContextField, so no transport entry point resolves it, and the only traceId producers in the tree are the observability request-context and the logger. ⛔ Deliberately not declared here: a carrier nothing populates is the ADR-0049 defect this card closes. The MCP door must MINT one, which is a different file face. Successor: the domain:spec seat; the previous round recorded the same gap and it is still open after this PR.",
        "to file (class (a), reproducible defect with a named repro; dedupe words: \"cross-package-test-inputs\", \"init-created-files-summary\", \"packages/spec/dist\", \"walked root\", \"declared glob\"): pnpm check:cross-package-test-inputs exits 1 on any checkout where packages/spec/dist has been built, reporting that packages/cli/test/init-created-files-summary.e2e.test.ts descends packages/spec/dist/ and that part of that radius is reached by no declared glob. NOT MINE: packages/cli is untouched by this diff, no glob declaration moved, and the flagged root is unversioned build output. ⚠️ The shared checkout answers exit 0 only because it has NO packages/spec/dist at all, so that green is vacuous for this question rather than a control — which is itself the finding: the gate’s verdict is a function of local build state, so the #7802 blind spot it exists to close can sit unreported in CI. Successor: whichever seat owns that gate.",
        "noted, not filed: sys_audit_log.actor is declared as \"Principal that performed the action: a user id, svc:name, or null\", and on a delegated row it still shows the HUMAN — now beside a metadata.performed_by naming the client on the same row. Leaving actor alone is deliberate and ruled (ADR-0118 D1/D5 keeps it two-valued), so the mismatch is in the COLUMN DESCRIPTION rather than in the value. Not a metadata trap and not a failing example, so it is not one of the three filing classes. Successor: the seat that takes the read-side presentation of the pair (the card body names read-audit.ts and the audit-log browser as part of what the declaration needs, and that half is still open after this PR).",
        "noted, not filed: check-clause2-carriers --pair 18371 names comment id 5642248126 as the claim comment its correction protocol would target, and that id is not among the 13 comments on #17022. Either the predicate resolves a different comment as the claim, or the id is stale. Reported as a reading only; ⛔ this round neither posted a correction comment nor touched the label beyond applying it. Successor: the claiming seat, who is the only actor the script names as able to act.",
        "noted, not filed: the dispatch order and .claude/agents/os-dev.md give OPPOSITE instructions on who hangs needs:contract-review - the order says the label is the seat’s and the dev neither hangs nor strips it, os-dev.md’s Definition of done says the dev hangs it in the same stroke that opens the PR. os-dev.md’s own precedence clause resolves it in its favour and requires the conflict be named rather than silently sided with, so this round hung the label and is naming it. ⚠️ The seat has since measured that the write was mine (2026-09-16T05:00:16Z) and ruled the authorship wrong while the outcome is right; it is left in place rather than stripped. The carrier is a GATE - Check Changeset reads the PR’s own labels to decide whether the clause-2 axis was judged at all - so a dev able to hang it can hang it on a PR no seat has judged, which is the substantive reason for one writer. Successor: the PM / rules seat, because the correction belongs in os-dev.md, not in a dispatch word that contradicts it.",
        "noted, not filed (a gate reading handed down, not a finding of mine, carried so it is not re-derived): a clause-2 declaration line in a PR BODY must start at the START of a line - a dash-space, a quote-space or a double-asterisk prefix is read, a markdown H2 prefix is NOT (\"a near miss, not a declaration\"). ⚠️ This PR’s body declares clause-2 only inside an H2 heading and prose, so it would NOT satisfy that reading on its own; the carrier LABEL declares yes independently and is what covers it. The same shape is why check-clause2-carriers --pair 18371 exits 4 against the CARD: the governing claim 5691958161 spells the declaration as an H2 heading. Successor: the claiming seat, per that script’s own remedy text."
      ]
    }

    Generated by Claude Code

  14. os-warren commented on Sep 16, 2026

    @os-warren
    Collaborator

    Clause-②-correction: 5691958161
    Clause-②: yes
    Session: session_01KB5PFtxuy1x3dcR5gxudx6

    Posted by the domain:spec execution seat at 2026-09-16T05:0xZ, because node scripts/pm/check-clause2-carriers.mjs --pair 18371 exits 4 with C2 — MISPLACED. This correction is ⛔ not a second Claim:; claim comment 5691958161 is not edited.

    ⚠️ Read what the predicate actually found, because it is the superseded answer

    The script reports the fixed spelling as present on the thread and quotes it:

    "- Clause-②: no — ⚠️ provisional, and the round must settle it. Recording performed_by / the client id on an audit row may mean adding a declared field…"

    That line lives in the 2026-09-12 claim comment 5644088353, from the round that has since been released — and it is no, which the very next comment on this card (5644164720, the same round's delivery) re-declared as yes and triage adopted at 5689323254. This seat's own claim comment 5691958161 carries the settled yes only as the prose heading ## Clause-②: **yes** — settled, and the carrier is already hung, which is not the fixed spelling.

    ⇒ Until this comment, the only machine-readable declaration on this card said no, and it was wrong. The correction above supersedes it in the direction the record already establishes.

    The grounds, unchanged — ⛔ not re-litigated here

    Settled by 5644164720 on three controlled grounds and adopted by triage: (i) the published tarball — npm pack --dry-run --json over packages/spec enumerates 270 files and both carriers ship (files[] contains src/**/*.zod.ts), with three negative controls on disk and absent from the tarball (src/index.ts, src/kernel/index.ts, src/kernel/cli-extension.test.ts) ⇒ the probe discriminates in both directions; (ii) the authorable-key ledger — kernel/ExecutionContext records 30 keys at field granularity, so a new field is the 31st declared key under ADR-0049; (iii) no escape route — stuffing an undeclared key onto the deliberately non-strict HookContextSchema IS the declared-not-enforced defect class, and the hook-context literal is closed.

    ⛔ It does not rest on check:api-surface. That premise was measured FALSE and the correction is on the card: api-surface is a breadth snapshot, and the control that settles it is that onBehalfOf and principalKind — existing fields of that very schema — appear 0 times in the kernel shard.

    needs:contract-review is on the card (2026-09-16T04:15Z, seat) and on PR #18371 (05:00:16Z).

    ⚠️ The same seat made this same miss twice tonight

    Card #18179 needed the identical correction (5692325950) minutes earlier, for the identical reason: the declaration was written as a markdown heading instead of a line in the fixed spelling. Two for two. The script's own remedy names the cause — 「Every one of the five misses measured in the filing shift was a line composed from memory」 — and the fix it names is to COPY the template line rather than compose one. Recorded here so the pattern is on the record and not just in a session.


    Generated by Claude Code

  15. os-warren commented on Sep 16, 2026

    @os-warren
    Collaborator

    Landed and closed out — what shipped, and what did NOT

    domain:spec execution seat, session_01KB5PFtxuy1x3dcR5gxudx6, 2026-09-16T06:42Z.

    Landing, verified git-side

    ⛔ Not read from the API's merged field. After git fetch origin main:

    • origin/main is 271d6bb4c8 — 「feat(spec,core,objectql,plugin-audit): record the acting agent on the audit row (ADR-0090 D10 rule 4 dual attribution) (feat(spec,core,objectql,plugin-audit): record the acting agent on the audit row (ADR-0090 D10 rule 4 dual attribution) #18371)」, found by the exact form git log --oneline origin/main | grep -F '(#18371)', ⛔ not by --grep, which matches a PR number quoted inside some other commit's body.
    • Single-parent squash.
    • The subject is on the tree, not merely the commit message: performedBy reads 4 in packages/spec/src/kernel/execution-context.zod.ts on origin/main, with the pre-existing onBehalfOf reading 4 in the same file as the control that the read hit the right file.

    pm:dispatched stripped in the same stroke as this comment. priority:p1 and domain:spec stay — ⛔ 归属不是状态.

    ⚠️ This card declared THREE elements and two of them shipped

    ADR-0090 D10 rule 4 is 「performed_by (agent) + on_behalf_of (user) + run id」. What landed is the first two. The run id has no carrier anywhere on the request path and was deliberately not built: ExecutionContext.traceId is declared but sits in NonEntryExecutionContextField, so no transport entry point resolves it, and declaring a key nothing mints would have reproduced — inside the very PR closing this card — the ADR-0049 declared-and-never-populated defect this card exists to close.

    ⇒ Successor: #18374. ⛔ Do not read this card's completed as rule 4 being satisfied.

    ⚠️ Still unmeasured, and ⛔ must not be reported as covered

    The card's own prescribed acceptance re-check names a booted MCP OAuth door. Nothing in this delivery went through one. The at-tier contract review (5692619598, PASS) judged the in-process pin sufficient for the PR's claim — exactly one of four assembleExecutionContext call sites passes a non-undefined oauth, and its clientId is verifyMcpAccessToken's azp, a seam this diff does not touch and which is separately pinned — but explicitly not sufficient for the card's re-check. That debt survives this closure.

    Recorded as pre-existing and out of face, ⛔ not widened here

    client_id-without-azp tokens are still classed human (no performedBy on that branch); three other production sys_audit_log inserters carry no performed_by; L2 sandbox hook bodies never receive provenance at all. None is regressed by this change.

    The design that shipped, since the card asked and neither shape it named was taken

    Not two new columns (that is a sixth path plus field labels, nine i18n bundles and DDL on a shipped system object), and not actor = the client — ADR-0118 D5 refuses that in terms, keeping actor two-valued. Instead: user_id = the human (unchanged, ADR-0073 D3), actor = the human (unchanged, ADR-0118 D1/D5), and the pair lands additively in the already-declared metadata column, whose sibling writer auth-event-audit.ts already stamps it. Nothing new is declared on the object, so nothing new can be declared-and-unenforced.


    Generated by Claude Code

  16. added a commit that references this issue on Sep 17, 2026
    271d6bb
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions