Skip to content

RFC: typed seller dispositions in the campaign-governance audit record #6757

Description

@lukasz-pubx

Successor to #6656 (closed), alongside #6749 (acceptance-policy discovery) and #6750 (proposal-bound change rights). This RFC proposes records, not authority: it accepts the campaign-governance ("spend approval") model as intentionally asymmetric and accepts that seller-internal review, HITL, and escalation are implementation details. What it adds is the typed record of the seller's decline, which is external by construction: it crosses the wire and lands in the audit trail.

Amended 2026-08-22 per triage and maintainer review (see comments): narrowed to a vocabulary clarification and audit-preservation proposal. The error vocabulary is re-scoped rather than extended (origin on the existing POLICY_VIOLATION details; no new error code); declines map to the existing outcome: "failed" (the originally proposed not_executed value was an error — it is not in outcome-type.json); the relayed record is framed honestly as a buyer-attributed copy; the seller_policy_review hold class and human_involved field are dropped; all disclosure fields are genuinely optional so the opaque form is truly opaque.

Motivation

Grounding: citations from the repo at 17cb0f59776a0ecc68dd912dc43b640442002d74 (main, 2026-08-21); re-verify at implementation time.

The seller's right to decline is normative; its record is not. docs/governance/campaign/specification.mdx § Seller enforcement:

"Seller-side governance (if the seller itself has configured a governance agent on the account) is an independent layer. A buyer's successful check_governance does not obligate the seller to accept the request; the seller's own compliance policies MAY still reject the action via PERMISSION_DENIED."

PERMISSION_DENIED is the same code used for token failures in the preceding paragraph of that section — a buyer cannot distinguish "your token failed verification" from "our standing policy declined this buy". The error-code registry itself divides these concerns already: POLICY_VIOLATION ("Request violates the seller's content or advertising policies"), ACTION_NOT_ALLOWED (mutation not available), PERMISSION_DENIED (caller authorization) — the seller-enforcement text just points at the wrong one.

The audit trail already expects the seller's result — untyped. docs/governance/campaign/responsibilities.mdx: after a seller responds, the orchestrator calls report_plan_outcome, which "lets the governance agent reconcile the approved action with the seller's actual response"; the documented failure mode for omitting it is "Governance budget and audit state drift from what actually happened." A policy-based decline is part of what actually happened — but no structured shape carries it, so the reconciled record can say an approved action failed without preserving why.

Reconciliation is the spec's own defense mechanism — decline reasons are missing from it. specification.mdx § Seller enforcement requires sellers to persist accepted tokens with decision outcomes, because "independent reconciliation between seller records and get_plan_audit_logs is the cross-check that catches a compromised or misbehaving governance agent." The seller-side decline reasons that determine whether approved actions execute have no structured place on the governance-agent side of that reconciliation today.

The typed-detail convention already exists. static/schemas/source/error-details/policy-violation.json is the "Recommended details shape for POLICY_VIOLATION errors" — policy_id, policy_url, violated_rules, all optional, extensible. What it lacks is an origin (whose policy) and explicit semantics for policies the seller does not disclose.

Neighbours and why they don't cover this. #6749 gives sellers structured acceptance-policy discovery and says definitive policy failures "use existing structured rejection/outcome mechanisms and reference the applicable registry policy IDs when they can be disclosed" — leaving the undisclosed case undefined and the audit-record entry unspecified. #6750 binds negotiated change rights and deliberately keeps the seller's process opaque — correctly — but the outcome of that processing has no preserved record either. Both RFCs define what happens before and around the seller's decision; neither defines the record of the decision itself. Pending-processing communication is deliberately out of scope here: the generic async lifecycle and #6750's seller_managed route cover it.

Problem

  1. Buyer's intent check approves an update_media_buy; signed governance_context issued; the seller's execution check passes.
  2. The seller's standing policy (a budget-change threshold, a flight-horizon rule, a regulatory duty such as the political-advertising obligations RFC: structured seller acceptance-policy discovery backed by the policy registry #6749 uses as its first conformance case) declines the action.
  3. On the wire this is PERMISSION_DENIED — indistinguishable from a token failure — or a generic error. In get_plan_audit_logs the plan shows an approved action that failed, with no preserved reason. In a regulated context, the seller's decision — which may need to be demonstrable — exists only in the seller's internal systems.
  4. Reconciliation between seller records and the governance audit log — the spec's stated cross-check — cannot cover the one class of seller decision that changes whether money moves.

Scope

Three mechanisms, all additive and optional. No new error codes, no new outcome values, no seller authority, no workflow exposure, no disclosure obligation.

1. Error vocabulary clarification + origin

The normative seller-enforcement text stops recommending PERMISSION_DENIED for acceptance-policy rejection, and the existing vocabulary is mapped by concern:

Concern Code
Seller acceptance/content policy (incl. #6749's regulated categories) POLICY_VIOLATION
Buyer change not granted by accepted terms / not currently available (#6750) ACTION_NOT_ALLOWED
Identity, scope, or governance-token failure PERMISSION_DENIED

policy-violation.json gains one optional field — origin (buyer_plan | registry | seller) — so a decline attributes whose policy fired:

{
  "error": "POLICY_VIOLATION",
  "error_details": {
    "origin": "seller",
    "policy_id": "streamhaus_budget_change_approval",
    "policy_url": "https://streamhaus.example/policies/budget-approval",
    "violated_rules": ["budget increase 34% exceeds 20% self-serve limit"]
  }
}

No new error code is added. SELLER_POLICY_REJECTED remains unfiled unless the WG later concludes that POLICY_VIOLATION's recovery classification cannot represent an opaque, terminal-for-this-request refusal.

2. Progressive disclosure — the opaque form is genuinely opaque

All detail fields (policy_id, policy_url, category, violated_rules) are optional, per the existing schema (no required entries). For an undisclosed policy the guaranteed signal is the typed error alone:

{
  "error": "POLICY_VIOLATION",
  "error_details": { "origin": "seller" }
}

A mandatory stable identifier — even an opaque one — is a correlation handle: a buyer could map a seller's confidential control surface across requests. Disclosure is therefore progressive and entirely at the seller's discretion, from bare origin up to full policy reference; disclosed policies (e.g. those published via #6749's acceptance-policy profiles) SHOULD reference their registry policy IDs, per #6749.

3. Audit preservation on the existing failed outcome

report_plan_outcome already supports outcome: "failed" and requires an error. Its error object is extended to carry the structured seller error details (inline or $ref to the policy-violation details shape), and the governance agent retains and exposes that error on the corresponding get_plan_audit_logs outcome entry. get_plan_audit_logs can then answer "why did the approved action fail" with a preserved, typed reason.

Provenance is stated honestly: because report_plan_outcome is buyer-called, this record is a buyer-attributed copy of the seller response — not an authenticated seller disposition. Seller-attested provenance (signed seller evidence or direct authenticated seller reporting) is named future work, not claimed here.

Non-goals

  • No seller authority over spend approval; no change to check_governance, token threading, or the one-agent binding.
  • No exposure of seller-internal review, HITL, or escalation workflow — including no hold-reason class and no human_involved field: the generic async lifecycle communicates pending work, and RFC: bind buyer change rights into accepted proposal terms #6750's seller_managed route carries the externally meaningful processing commitment.
  • No policy synchronization to any governance agent; no discovery semantics (that is RFC: structured seller acceptance-policy discovery backed by the policy registry #6749).
  • No obligation to disclose policy content, identifiers, or categories: the opaque form is first-class.
  • No new error codes or outcome values; no seller-attested audit provenance (future work).

Alternatives considered

  1. Do nothing. Seller declines remain PERMISSION_DENIED-shaped per the current normative text — indistinguishable from token failures — and the audit record keeps losing the reason approved actions fail.
  2. A new SELLER_POLICY_REJECTED error code. Single enum match for agent loops, but adds to the stable error-code vocabulary for a concern the existing POLICY_VIOLATION + origin expresses; deferred unless recovery-classification needs emerge (per maintainer review).
  3. A seller_disposition block on report_plan_outcome with a not_executed outcome (this RFC's originally filed shape). Rejected on review: not_executed is not in outcome-type.json (adding it contradicted the non-breaking claim), and a dedicated seller-attributed block overstated the provenance of a buyer-relayed record. The failed-outcome error extension carries the same information honestly.
  4. A typed hold-reason class (seller_policy_review) (also originally filed). Dropped: pending-processing communication is covered by the generic async lifecycle and RFC: bind buyer change rights into accepted proposal terms #6750's seller_managed commitments, and a reason class begins modeling the internal workflow the protocol deliberately leaves alone. Revisitable with implementer evidence that policy holds are being mistaken for latency in practice.
  5. Free-text reasons on existing errors. No attribution, no machine-actionable structure, nothing aggregatable in audit.

Compatibility impact

Non-breaking. All schema additions are optional fields on additionalProperties: true schemas; no new error codes or outcome values; the specification change removes incorrect guidance (PERMISSION_DENIED for policy rejection) with no behavioral change for conformant implementations. Changeset: patch on experimental surface, minor if the governance/ schemas are classed stable — either way additive (per triage, verify x-status: experimental on governance/ schemas and flag for backfill if missing).

Files affected (as of 17cb0f5 — re-verify at implementation)

  • static/schemas/source/error-details/policy-violation.json — optional origin (buyer_plan | registry | seller); confirm all detail fields remain optional
  • static/schemas/source/governance/report-plan-outcome-request.jsonfailed-outcome error object extended to carry the structured seller details (inline or $ref)
  • docs/governance/campaign/specification.mdx § Seller enforcement — remove PERMISSION_DENIED as the policy-rejection recommendation; add the vocabulary mapping
  • docs/governance/campaign/tasks/report_plan_outcome.mdx, responsibilities.mdx — structured error on failed seller declines; buyer-attributed-copy framing; exposure in get_plan_audit_logs
  • Changeset file, schema index if touched, compliance storyboards as follow-up scope

Reviewer checklist

  • Motivation is clear and not redundant with existing functionality
  • Scope is specific enough to implement without further clarification
  • Alternatives section covers at least one non-obvious alternative
  • Compatibility impact accurately states breaking vs. non-breaking
  • Wire-format or schema snippet included (for schema or task changes)

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    claude-triagedIssue has been triaged by the Claude Code triage routine. Remove to re-triage.governanceIssue concerns the governance protocol domainrfcProtocol change — auto-adds to roadmap boardschemaJSON Schema source-of-truth: definitions, codegen artifacts, validation, hygiene

    Type

    No type

    Projects

    Status
    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions