Skip to content

audit: an action run through MCP leaves no audit row naming its surface; ruling 6513869839 parameter 3 (#27681) needs an execution-context surface key, the audit writer and the forward headers #27908

Closed

Description

@objectstack-fleet

domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F

Carries parameter 3 of the maintainer's ruling on #27681 (letter B, record 6513869839): "The audit row of an action run through MCP records the surface, so an administrator can tell an agent's call from a click without a second log." The mechanism PR for #27681 does not carry it, because it needs a packages/spec change (claim amendment 6514100238). Filed by the domain:server#2 seat. ⛔ Not a claim.

What is there today (read from code at origin/main e320e366 by the #27681 card thread)

  • Neither the REST action door nor MCP run_action writes an action-invocation audit row.
  • The script branch of invokeBusinessAction logs console.info [action-audit] and already tells MCP run_action from a REST action. The api, flow and declarative-update branches log nothing.
  • sys_audit_log records the data writes an action's body makes. Its rows have no surface field.
  • ExecutionContext has no key that carries the surface. performedBy is set only by the OAuth /mcp door, and it is lost when an api action is forwarded and re-authenticated.

Wanted (route is triage's)

A durable audit record of each action invocation that names its surface (REST, MCP), on every action branch (script, api, flow, declarative update), and survives the api forward. This likely means an execution-context attribution key in packages/spec (Clause-② per the spec lane), the audit writer in plugin-audit, and the forward headers in packages/runtime. Triage decides the lanes and whether it splits per lane. A log line is not the audit row the ruling asks for.

Links: #27681, ruling 6513869839, decision card #27900 (the declarations).

Filed without domain:*; grading is triage's.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 30, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: 真挡得住的权限 | none | P②

    Triage: p1 · domain:spec · area:automation · Seam · 2026-10-30T03:58Z

    ⛔ Not a claim.


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 30, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:spec#4 · marchtian · session_017ihL2fi9zfJhUTubyJYb6V

    Claim: PM loop round 3 · 2026-10-30T10:58Z
    Session: session_017ihL2fi9zfJhUTubyJYb6V
    Account: marchtian
    Branch: claude/issue-27908-action-audit-surface
    Worktree: objectstack-issue-27908
    Domain: domain:spec
    Seat: domain:spec#4
    Ruling-ref: maintainer ruling 6513869839 on #27681 (letter B, parameter 3: "The audit row of an action run through MCP records the surface, so an administrator can tell an agent's call from a click without a second log."); triage 6514607572 (Seam card: "the spec lane declares the surface key on ExecutionContext. The consumers are the audit writer (plugin-audit, one durable action-invocation row on every branch: script, api, flow, declarative update) and the forward headers (packages/runtime, so the key survives the api re-authentication)." "⛔ A log line is not the audit row.")
    File surface: packages/spec/src/kernel/execution-context.zod.ts (one optional, server-constructed surface key on ExecutionContext, the performedBy docblock shape: never client-supplied, attribution only), the doors that construct it (packages/runtime REST action door and the /mcp bridge's run_action / resume_run), packages/runtime/src/action-execution.ts (invokeBusinessAction on all four branches; the api forward carries the surface across re-authentication beside API_FORWARD_MARK_HEADER, server-to-server only, refused from an outside caller), packages/plugins/plugin-audit/src/** (one durable action-invocation row per invocation naming its surface), tests in those packages, regenerated spec artefacts, changesets (@objectstack/spec minor; runtime / plugin-audit per their published change). ⛔ No change to authorization: the key never becomes a subject, a scope or a gate (stop on breach; explain in the report). ⛔ A console.info line does not count as the row.
    Container & model: M, mode:subagent, model: TIER_DEFAULT (opus) — the quota-exhaustion exemption (claude-fable-5-1 answered HTTP 429; maintainer 「你没有fable了,使用 opus开发完之后等契约复审」), effort: high
    Clause-②: yes (widening)
    Metadata-protocol: none
    Responsibility: an administrator reading the audit trail cannot tell an action an agent ran over MCP from one a person clicked, because no branch of invokeBusinessAction writes an action-invocation row and ExecutionContext carries no surface | the performedBy attribution precedent (ADR-0090 D10 rule 4: server-constructed, attribution only, recorded by the audit writer) is the path | every deployment that serves /mcp actions to agents, on every action branch (script, api, flow, declarative update)
    Thread-read: 6514607572
    Premise: holds — on 3e0fd7a8f7 ExecutionContextSchema has performedBy (execution-context.zod.ts:309) and no surface key; [action-audit] is logged only from packages/runtime/src/action-execution.ts and domains/actions.ts; API_FORWARD_MARK_HEADER is at action-execution.ts:1186; the #27681 mechanism PR #27940 merged (2026-10-30T10:36Z), so there is no in-flight intersection with it
    Serial constraints cleared: draft PR #28047 touches packages/plugins/plugin-audit/src/audit-log-read-visibility.ts and its tests (the read side; this card writes the invocation row), so whichever lands second merges origin/main. Draft PR #27951 touches only packages/runtime/src/mcp-resume-run.test.ts; if this card's pins land in the same file, the second merges. No open PR touches execution-context.zod.ts or action-execution.ts (open PR file lists read just before this claim)

  3. objectstack-fleet commented on Oct 30, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:spec#4 · marchtian · session_017ihL2fi9zfJhUTubyJYb6V
    os-dev-report
    {
    "status": "pr_open_draft",
    "posted_at": "2026-10-30T15:09Z",
    "card": 27908,
    "branch": "claude/issue-27908-action-audit-surface",
    "pr": 28171,
    "head_sha": "c6228a99cd1ef544af6e17b2211b2eb9b2fb9c1a",
    "session": "session_017ihL2fi9zfJhUTubyJYb6V",
    "premise_still_valid": true,
    "summary": "ExecutionContext.surface (rest | mcp) set by the REST action door and the /mcp run_action / resume_run door on a copy of the invocation envelope; plugin-audit writes one sys_audit_log row (action action_invocation, metadata.surface) per invocation on every branch (script, api, flow, update), success or post-dispatch refusal; no new field on sys_audit_log.",
    "seat_decisions": [
    "key name ExecutionContext.surface and its closed values rest, mcp (maintainer approves in the PR, Clause-2 yes)",
    "new sys_audit_log.action enum value action_invocation (a value, not a field)",
    "no surface header on the api forward (deviation 1): accept, or require a trusted in-process carry",
    "Metadata-protocol added with minor bumps instead of the claim none (deviation 2)"
    ],
    "tests": {
    "packages/runtime/src/action-invocation-audit.test.ts": "24 passed (branch x door success and failure, subject 404 row, D4 gate no row, forged forward mark + surface header + upstream surface refused, api forward carries no surface header, envelope not mutated, performedBy carried, resume_run rows)",
    "packages/plugins/plugin-audit/src/action-invocation-audit.test.ts": "7 passed (real ObjectQL read-back)",
    "package_suites": "core 128 files / 2873 passed; plugin-audit 66 / 1058; runtime 484 / 7895 passed 19 skipped; spec 756 / 22510 passed 1 todo",
    "ablation": [
    "runtime: removed await sink.recordActionInvocation(event); gives 20 failed / 4 passed; restored blob d014ccafe5bb",
    "plugin-audit: removed await persistActionInvocationAuditRow(engine, row); gives 3 failed / 4 passed; restored blob 9d2076906454"
    ]
    },
    "gates": {
    "derivation": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack",
    "derived": 131,
    "run": 131,
    "not_measured": 0,
    "unrun": 0,
    "nonzero_final": 0,
    "reconciliation": "dispatch-gates --ran: 131 accounted, 0 NOT-MEASURED, derived zero (every line carries its exit code)",
    "head_measured": "c6228a99cd; 127 commands re-run on this head, pnpm check:pm-dispatch-gates measured exit 0 on 26e3fb2 (the last commit touches no scripts/pm file), check:skill-examples / check:dual-build-cjs-loads / check:type-check-debt measured exit 0 on this head content after pnpm build under the verify lock",
    "first_pass_reds_fixed": [
    "check-tenant-audit-census (+ --self-test): new write site; options cast dropped, page recounted 251 to 252",
    "check:engine-double-contract: ledger --write",
    "check:objectql-double-limit: test double applies limit",
    "check:swallow-census-controls: persistActionInvocationAuditRow copied into WRITE_SHAPED_CALLEES",
    "check:doc-authoring: issue id dropped from the surface .describe()"
    ],
    "first_pass_exit3_then_green": [
    "check:skill-examples",
    "check:dual-build-cjs-loads",
    "check:type-check-debt"
    ],
    "adr_0087": "node scripts/check-adr-0087-registration.mjs --base origin/main: exit 0",
    "check_generated": "14/14 up to date",
    "typecheck": "spec, core, runtime, plugin-audit exit 0",
    "not_run_locally": "test:repo and the full CI farm (left to CI)"
    },
    "dogfood_battery": "n/a, no ! in the title and no (narrowing) arm",
    "line_budget": {
    "declared": "none in the order",
    "files": 29,
    "added": 1312,
    "removed": 56,
    "of_added_tests": 558,
    "of_added_generated_or_census_docs": 54
    },
    "deviations": [
    {
    "id": 1,
    "what": "no surface header on the api forward",
    "why": "measured: the forward mark carries no trust (any caller can send x-objectstack-action-forward), the row is written door-side after the forward from the door envelope, no far-side writer reads a surface, a sound carry needs a new in-process credential honoured by every envelope-resolving face; pinned by a test that the forward carries none and the row says mcp",
    "seat": "decide"
    },
    {
    "id": 2,
    "what": "Metadata-protocol added, changesets minor x4 (spec, core, runtime, plugin-audit)",
    "why": "the key moves packages/spec/authorable-surface/kernel.json and check-changeset-no-major none-baseline-moved refuses none"
    },
    {
    "id": 3,
    "what": "new enum value action_invocation on sys_audit_log.action",
    "why": "a value, not a field; no new column; surface lives in metadata"
    },
    {
    "id": 4,
    "what": "surface stamped on a copy of the envelope, not the request ec",
    "why": "nothing upstream or downstream sees a moved request envelope"
    },
    {
    "id": 5,
    "what": "resume_run also writes a row (branch flow, run_id)",
    "why": "it is an MCP door continuing an admitted flow action"
    },
    {
    "id": 6,
    "what": "tenant_id is the caller active org (ExecutionContext.tenantId), not the subject record org",
    "why": "the row does not read the record"
    },
    {
    "id": 7,
    "what": "test:repo and full farm not run locally",
    "why": "CI owns them per common.md"
    }
    ],
    "files_changed": [
    ".changeset/27908-action-invocation-audit-surface.md",
    "content/docs/permissions/tenant-audit-census.mdx",
    "content/docs/references/data/data-engine.mdx",
    "content/docs/references/kernel/execution-context.mdx",
    "docs/audits/2026-08-tenant-audit-write-call-sites.counts.md",
    "packages/core/src/security/assemble-execution-context.test.ts",
    "packages/core/src/security/assemble-execution-context.ts",
    "packages/plugins/plugin-audit/src/action-invocation-audit.test.ts",
    "packages/plugins/plugin-audit/src/action-invocation-audit.ts",
    "packages/plugins/plugin-audit/src/audit-log-field-redaction.ts",
    "packages/plugins/plugin-audit/src/audit-log-read-visibility.ts",
    "packages/plugins/plugin-audit/src/audit-plugin.ts",
    "packages/plugins/plugin-audit/src/index.ts",
    "packages/plugins/plugin-audit/src/objects/sys-audit-log-retired-actions.test.ts",
    "packages/plugins/plugin-audit/src/objects/sys-audit-log.object.ts",
    "packages/plugins/plugin-audit/src/translations/en.objects.generated.ts",
    "packages/plugins/plugin-audit/src/translations/es-ES.objects.generated.ts",
    "packages/plugins/plugin-audit/src/translations/ja-JP.objects.generated.ts",
    "packages/plugins/plugin-audit/src/translations/zh-CN.objects.generated.ts",
    "packages/runtime/src/action-execution.ts",
    "packages/runtime/src/action-invocation-audit.test.ts",
    "packages/runtime/src/action-invocation-audit.ts",
    "packages/runtime/src/domains/actions.ts",
    "packages/runtime/src/domains/mcp.ts",
    "packages/spec/authorable-surface/kernel.json",
    "packages/spec/src/kernel/execution-context.zod.ts",
    "scripts/check-durability-degradation-log-level.mjs",
    "scripts/engine-double-contract.pinned.json",
    "scripts/measure-durability-swallow-family.mjs"
    ],
    "mcp_calls": 0,
    "api_writes": 2,
    "api_writes_detail": [
    "pr_create via fleet-write dispatch (run 41077866252, read-back identical)",
    "this comment via post-stamped"
    ],
    "open_questions": [
    "Should the forward carry a trusted surface (new in-process credential) so CRUD rows written by an api target also name the door? Not needed for the action_invocation row."
    ],
    "out_of_scope_findings": [
    {
    "class": "a (candidate)",
    "reach": "not measured; read from code",
    "evidence": "packages/runtime/src/http-dispatcher.ts:753 path-matches acceptOAuthAccessToken to /mcp, and forwardApiAction relays the caller bearer to a non-/mcp target, so an OAuth-token agent api action would resolve anonymous at the target (likely 401 on sys
    * identity api actions)",
    "dedupe": "oauth forward api action anonymous acceptOAuthAccessToken"
    },
    {
    "class": "observation",
    "reach": "harmless",
    "evidence": "API_FORWARD_MARK_HEADER x-objectstack-action-forward is forgeable by any caller; its only effect is the loop refusal (pinned in action-invocation-audit.test.ts)"
    }
    ]
    }

  4. objectstack-fleet commented on Oct 31, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:spec#4 · marchtian · session_017ihL2fi9zfJhUTubyJYb6V

    Landed · 2026-10-31T05:47Z · PR #28171 merged by the queue at 2026-10-31T05:46Z as 3a054cbe1f; the card closed by Fixes. This seat strips pm:dispatched in the same act. Contract review: PASS 6537892848 on the landed head. 田騏 approved the names ExecutionContext.surface (rest / mcp) and action_invocation. Every action invocation now leaves one sys_audit_log row naming the door it came through, on both the REST and MCP doors and on every branch. Should-fixes S1 (join the two rows of an api forward to an action route) and S2 (the awaited ledger insert) are filed as #28281; the OAuth-bearer gap on api forwards is #28178.

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions