Skip to content

feat: Introduce Actions and apply it to Checkout#553

Closed
raginpirate wants to merge 3 commits into
mainfrom
raginpirate/actions
Closed

feat: Introduce Actions and apply it to Checkout#553
raginpirate wants to merge 3 commits into
mainfrom
raginpirate/actions

Conversation

@raginpirate

@raginpirate raginpirate commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Description

Introduce the base Actions primitive for UCP. Actions are scoped, in-band instructions emitted during the lifecycle of a UCP entity when the platform needs to perform an additional step before the entity can progress. The base primitive is intentionally small:

  • id — stable correlation identifier for an action occurrence
  • code — reverse-domain action code
  • severityoptional, required, or blocking
  • config — owner-defined action configuration

This PR also wires actions into Checkout responses through a top-level actions[] field and documents how actions interact with the checkout lifecycle.

The design invariants for this solution are well defined in #517

What this intentionally does not take from #517

Issue #517 proposes a richer Option A execution contract for step-up actions. This PR deliberately does not include those fields in the base primitive.

Specifically, this PR does not add:

  • base state
  • base expires_at
  • base mode
  • base execution
  • base result
  • a standardized recovery directive enum
  • a top-level action capability negotiation model
  • a PSP/handler trust manifest with allowed_origins and supported modes
  • common embedded action transport messages
  • standard 3DS or device data collection action specs
  • common execution.url / visible / request_method fields

Those are all valid concerns for concrete step-up specs, especially 3DS, but they are intentionally deferred out of the base primitive. The tradeoff is explicit: this PR chooses the lower-risk “base envelope first” path. Standardized execution fields can still be introduced later once concrete actions prove the abstraction across multiple domains. Adding owner-defined config fields later is non-breaking; prematurely standardizing base envelope fields would be much harder to unwind.

Known concern / likely follow-up

The main concern we may continue to push on is that opaque config means a generic platform cannot execute every action with one universal renderer. This PR does not claim to fully solve generic step-up execution. It establishes the shared action envelope, checkout lifecycle integration, action identity, and owner obligations. A stacked 3DS/action-specific PR should continue to iterate on the concrete execution, trust, negotiation, lifecycle, and recovery contract needed. For now, all of those validations, including trust, is expected to be uniquely defined by a given action.

Category (Required)

Please select one or more categories that apply to this change.

  • Core Protocol: Changes to the base communication layer, global context, or breaking refactors. (Requires Technical Council approval)

Related Issues

#517
#458

Checklist

  • I have followed the Contributing Guide (including Conventional Commits title requirements and ! for breaking changes).
  • I have updated the documentation (if applicable).
  • My changes pass all local linting and formatting checks.
  • I have added tests that prove my fix is effective or that my feature works.
  • New and existing unit tests pass locally with my changes.
  • (For Core/Capability) I have included/updated the relevant JSON schemas.
  • I have regenerated Python Pydantic models by running generate_models.sh under python_sdk.

@aneeshali aneeshali left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot @raginpirate for introducing the base changes separately. Left a few minor comments!

Comment thread docs/specification/checkout.md Outdated
Comment thread docs/specification/payment-handler-template.md

@drewolson-google drewolson-google left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall looks good, just one comment about ID uniqueness.

Comment thread docs/specification/actions.md Outdated
Businesses **MUST** follow these rules:

* `id` values **MUST** be unique among outstanding actions for the containing
entity.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do IDs have to be globally unique or just unique within a given action code? Said another way, are IDs automatically namespaced by code?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good clarifying question. I propose having them be unique to the entity they are surfaced under, not unique within a given action code. You should not risk collisions between your 3ds action and your ddc action in id.

@jamesandersen jamesandersen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall this is looking pretty solid

Comment thread docs/specification/payment-handler-guide.md Outdated
Comment thread source/schemas/common/types/action.json
@igrigorik igrigorik added this to the Working Draft milestone Jul 3, 2026

@aneeshali aneeshali left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot Daniel, I think the doc update covering order of resolution is good to begin with. The changes look good overall.

@amitpaz1

amitpaz1 commented Jul 8, 2026

Copy link
Copy Markdown

We've been building against exactly the gap this PR names in its "known concern": an out-of-band human approval step for unattended buying agents. We shipped an open-source gateway (@agentkitai/agentgate-ucp, MIT) that today has to express "a human must approve this checkout before complete" through the requires_escalation + message-severity path (requires_buyer_review + continue_url). It works, but it conflates three distinct situations the base envelope would let us separate cleanly:

  1. Spend-policy approval — an org-side policy (amount, category, agent identity) requires a human sign-off before completion. The buyer may not be the approver.
  2. Merchant-side hold — the business needs its own human review (inventory, fraud) before the entity can progress.
  3. Buyer input — a field the agent can't answer; a typed form does.

All three currently collapse into requires_escalation messages, so platforms can't distinguish "poll and wait, a decision is coming" from "send the buyer to this URL." An actions[] entry with a stable id fixes the correlation problem this PR already identifies.

As implementer feedback, here is the concrete action spec we would emit, structured per the owner obligations in actions.md (when emitted / severity / config schema / execution / completion / fallback / security). We're happy to adapt it to whatever the base envelope stabilizes to, and to serve as a second domain proving the abstraction beyond 3DS (#458).

ai.agentkit.checkout.human_approval (namespace illustrative)

When emitted. On complete_checkout (or update_checkout crossing a policy threshold), when an approval authority — merchant-side or org-side gateway — requires a human decision before the checkout may reach completed. Emitted alongside status: "requires_escalation" for backward compatibility during transition.

Severity. blocking (checkout cannot reach a successful terminal state until resolved). required allowed for advisory flows where the business permits completion after a timeout policy of its own.

config schema.

{
  "approval_id": "req_8f3ab...",            // required; stable correlation id for the decision
  "approve_url": "https://gate.example/a/req_8f3ab",  // required; where the HUMAN decides (not the agent)
  "requested_by": "spend_policy | merchant_review",    // required; which situation (1) vs (2) above
  "expires_at": "2026-07-08T18:00:00Z",     // optional; after this the action is abandoned
  "form": {                                  // optional; typed buyer-input companion (situation 3)
    "url": "https://forms.example/intake/xyz",
    "fields": ["$.buyer.tax_id"]             // JSONPaths into the checkout the form will write
  }
}

Platform execution requirements. The platform MUST NOT attempt to complete the action itself (no automated interaction with approve_url — it is a human surface). It SHOULD surface approve_url to the responsible human out of band and poll the entity (get_checkout) for resolution. The id remains stable while the approval is outstanding.

Completion criteria. Approved → the action disappears from actions[] and the checkout progresses (ready_for_complete → the platform may re-issue complete_checkout with the ORIGINAL idempotency key, or the business completes and reflects completed). Denied → action disappears; response carries a message with code policy_denied and the checkout stays incomplete (or canceled per business policy). Expired/abandoned → same as denied with code approval_expired.

Fallback (unsupported platform). The business MUST also emit the existing requires_escalation + requires_buyer_review message + continue_url so a platform that ignores actions[] behaves exactly as today. This makes the action purely additive — that's what we ship now, and the action entry is the upgrade path.

Security and trust. approve_url MUST be HTTPS on an origin the owning spec's registration declared (origin validation per actions.md). Decision delivery back into the emitting gateway MUST be authenticated (we use HMAC-signed webhooks, fail-closed — an unsigned decision cannot resume a parked completion). Approval decisions SHOULD be recorded in an auditable log; regulatory context (EU AI Act Art. 12, California SB-833, in force 2026-07-01) increasingly requires demonstrable human checkpoints for automated transactions, which is much easier to certify against a typed action than free-form messages.

Why this might be useful to #553

  • It's a second live domain for the envelope (beyond 3DS/feat: Add Payment Authentication Actions #458), from an independent implementation that's already public and tested — the "concrete actions prove the abstraction across multiple domains" the PR body asks for.
  • It exercises the id correlation design under retries and webhook redelivery (we park the completion and replay with the original idempotency key; the stable action id/approval_id is what makes at-least-once delivery safe).
  • It offers a worked answer to the graceful-fallback obligation: emit the action AND the legacy escalation message during transition.

Reference implementation: https://github.com/agentkitai/agentgate-ucp (gate → park → webhook-resume → idempotent replay; typed buyer-input via JSON-Schema-derived forms). Happy to iterate on field names/shape to match where the Working Draft lands.

@jamesandersen

Copy link
Copy Markdown
Contributor

@amitpaz1 interesting and valuable to see a use case that further validates the modeling of this base actions PR

Thanks @raginpirate for the work here! LGTM

@amithanda

Copy link
Copy Markdown
Contributor

Approving this. Scoping it as a base envelope first is the right call, and it matches #517's own risk framing (Decision 1: "adding a config field later is non-breaking; un-hoisting an envelope field is breaking, so deferring is strictly lower risk"). The primitive is clean, forward-compatible, and the id identity/idempotency section is genuinely strong.
Reading it against the #517 design invariants, here is how it lands for me.
Invariants this PR already satisfies:

  • 5 (domain-generic, proven on non-3DS): the student-eligibility and account-login examples demonstrate the envelope on two non-3DS cases with no schema change.
  • 6 (zero overhead on the no-action path): actions is optional and omitted or empty when nothing is outstanding.
  • 8 (idempotency / replay safety): the id occurrence rules (stable while outstanding, new occurrence gets a new id, stale-completion supersession) cover this well.
  • 10 (advisory outcome): "completion is advisory... the business remains authoritative," and completion signals are not authoritative unless independently verifiable.
  • 13 (forward-compatible, vendor-agnostic): open-string code, the one legitimate closed severity discriminator, open config, and additionalProperties intentionally left open.
  • 14 (graceful degradation): checkout maps unresolved required/blocking actions to continue_url.
  • Plus a sensible MUST-level trust floor even while the full contract is deferred: HTTPS for executable URLs, validate origins before loading, never interpolate opaque config.
    Invariants rightly deferred to the stacked follow-up, consistent with the PR description's "does not take from [RFC] Proposal: Modeling 3DS and Step-Up Actions in UCP #517" list and "all of those validations, including trust, is expected to be uniquely defined by a given action":
  • 2 (native delegation / one generic renderer) and 3 (hidden/background) and 4 (headless / out-of-band): these need the standardized execution contract (mode, visible, poll_after_seconds), correctly left out of the base.
  • 7 (explicit separable lifecycle): partially covered by severity and the re-drive prose; base state / expires_at deferred.
  • 9 (standardized recovery): the common recovery-directive set is deferred with result.
  • 11 (input trust anchored independently of the business): the mechanism (PSP trust manifest with allowed_origins) is deferred, which also tracks [RFC] Proposal: Modeling 3DS and Step-Up Actions in UCP #517 Decision 5 treating that manifest as separate, governed work that "neither proposal claims a security advantage from it until it exists."
  • 12 (PCI / regulatory specifics): belongs in the concrete 3DS/payment action spec.

The one thing I would ask the follow-up to be explicit about is the completion of invariant 11: when the stacked 3DS/action spec lands, it should name the PSP/handler (not the business) as the trust anchor for the executable surface and define how the platform validates that origin independently of the business, so the advisory-outcome class does not end up trusting a business-asserted URL. Non-blocking for this PR.
Nice work landing the abstraction cleanly. Looking forward to the stacked execution/trust/lifecycle PR.

@igrigorik igrigorik left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@raginpirate @drewolson-google per the TC chat, ptal at this shape: #582.

I won't repeat what's there, but the key thing I'm looking for is clear integration and interop with other and existing primitive in UCP. To that end, I think negotiation/discovery is integral -- the business can emit an action, but how does it know if the platform can action it? I think the answer is simple: actions are extensions; extensions register actions.

@damaz91 damaz91 added gov:needs-gc-review status:needs-triage Signal that the PR is ready for human triage area:payments and removed status:needs-triage Signal that the PR is ready for human triage payments labels Jul 14, 2026
igrigorik added a commit that referenced this pull request Jul 15, 2026
Negotiating an extension confirms support for its complete Action contract,
but it does not make every future config value or delegate trustworthy.
Without an explicit boundary, open config could be mistaken for executable
instructions and surface completion for a Business result.

Require Platforms to process executable behavior only as defined by the active
Action-type contract, allow stricter runtime policy and instance rejection, and
keep later Business resource state authoritative.

Carry #553's concrete security and fallback concerns into conditional
Action-type authoring guidance. Extension specifications define executable
fields, trust anchors, interaction controls, completion observation, failure
behavior, and fallback only where relevant. This preserves the existing wire
shape without introducing a generic executor, callback, or state model.
@raginpirate

Copy link
Copy Markdown
Contributor Author

Closing this PR in favour of #582 which delivers the core of this PR and improves on how lifecycle and negotiation were proposed. Thanks Ilya!

igrigorik added a commit that referenced this pull request Jul 23, 2026
* feat: introduce Actions as a core UCP concept

   Add Actions as the machine-readable representation of outstanding work that a
   Platform can perform while a Cart or Checkout progresses. Actions are
   response-only, keyed by the active extension that defines their behavior, and
   carry a small common instance envelope: a stable ID, whether the instance is
   required, and optional extension-owned configuration.

   For example, a student-verification extension can return an Action alongside a
   provisional discount and an explanatory Message:

   {
     "actions": {
       "com.example.identity.student_verification": [{
         "id": "verify-student-1",
         "required": true,
         "config": {
           "verification_url": "https://business.example.com/verify/abc"
         }
       }]
     },
     "messages": [{
       "type": "info",
       "code": "eligibility_accepted",
       "content": "Student discount applied provisionally. Verify your status."
     }]
   }

   The Action identifies the outstanding work and provides its structured
   configuration. Messages remain orthogonal: they explain resource state and
   provide buyer-facing context, but do not define how the Action is processed or
   determine its outcome.

   Integrate Actions with the core Cart and Checkout models. Cart remains
   statusless and permits unrelated mutations while an Action is outstanding.
   Checkout uses its existing lifecycle: required Actions can keep a checkout
   incomplete, ready-for-complete checkouts have none, and Complete Checkout can
   return complete_in_progress with a required Action and no order. After
   processing it, the Platform polls Get Checkout for the authoritative result
   rather than issuing another Complete request.

   Represent each response as a complete snapshot of outstanding Action instances.
   Keep IDs stable for the same obligation, assign replacements new IDs, and let
   each defining extension own configuration, processing, trust, fallback, and
   outcome semantics.

   Reuse existing capability negotiation and allOf composition so concrete
   extensions register and validate their own Action keys without introducing a
   separate Action registry, execution protocol, or generic state machine.

* docs: define Action keys as extension-declared types

The original Actions contract equated each map key with the extension that
defined it. That framing implied a one-to-one relationship between extensions
and Actions, making it unclear how one negotiated extension could expose
several distinct runtime contracts such as device-data collection and a 3DS
challenge.

Define each reverse-domain map key as a concrete Action type declared by an
active extension. An extension may use its own name for a single Action type or
declare several distinct types within a namespace controlled by its schema
authority. Negotiating the extension activates the complete contract it
declares.

For example, one `com.example.payment.authentication` extension can declare
two independently dispatched Action types:

{
  "actions": {
    "com.example.payment.authentication.device_data_collection": [{
      "id": "collect-device-data-1",
      "required": true
    }],
    "com.example.payment.authentication.three_ds_challenge": [{
      "id": "challenge-cardholder-1",
      "required": true
    }]
  }
}

This preserves direct dispatch and homogeneous instance arrays without adding
a per-instance type discriminator. Existing allOf composition lets the
declaring extension contribute each type key and validate its config, while
existing reverse-domain governance provides namespace ownership without an
Action-specific registry.

Clarify that the map defines no processing order across Action types. JSON
preserves order within each type's array, but the declaring extension decides
whether that order has processing meaning and defines any cross-type
sequencing.

Align Cart and Checkout terminology around Action-defined gated effects and
update the Student Verification example to show that an extension may use its
own name for its single Action type.

This changes the semantic definition, not the wire shape: Action instances
remain response-only objects containing id, required, and optional config;
non-empty arrays, stable identity, parent lifecycle, and idempotency rules are
unchanged.

* docs: correlate operation failures with Actions

A required Action describes a gate on the effect defined by its Action type,
but it does not say whether a particular operation was accepted. Adding a
separate blocking severity would conflate those dimensions: the same Action can
remain required while also explaining why one attempted effect was not applied.

Use the existing Message contract to express that relationship. A Business
returns the current resource with a recoverable error Message whose RFC 9535
path selects the related Action occurrence. Info and warning Messages may use
the same path relationship to explain outstanding work without reporting an
operation failure.

For example:

{
  "status": "incomplete",
  "actions": {
    "com.example.identity.student_verification": [{
      "id": "verify-student-1",
      "required": true,
      "config": {
        "verification_url": "https://business.example.com/verify/abc"
      }
    }]
  },
  "messages": [{
    "type": "error",
    "code": "eligibility_verification_required",
    "severity": "recoverable",
    "path": "$.actions['com.example.identity.student_verification'][0]",
    "content": "Verification is required before this effect can be applied."
  }]
}

This keeps the signals orthogonal: the Action defines outstanding work and
requiredness, the Message reports the response-scoped operation outcome, and
the parent resource status remains authoritative for lifecycle.

Clarify the Checkout consequences. If a required Action prevents Complete
Checkout from being accepted, the Business returns the current Checkout with
status incomplete and a recoverable error pointing to the Action. A
complete_in_progress response instead means Complete Checkout was accepted for
asynchronous processing, including when that accepted flow waits for a required
Action.

Processing an Action does not automatically replay the earlier request. The
Platform obtains the latest Checkout through Get Checkout or a subsequent
Update response, and any later re-drive is a new operation governed by the
existing idempotency rules.

Broaden recoverable errors to include conditions resolved through a related
Action, not only input modification. Keep Message paths as ordinary positional
RFC 9535 locations in the returned response without adding naming rules,
stable-index guarantees, or an Action-specific Message type.

The wire shape remains unchanged: Actions still contain id, required, and
optional config, and Messages retain their existing type, code, severity,
content, and optional path fields.

* docs: define Action trust and execution boundaries

Negotiating an extension confirms support for its complete Action contract,
but it does not make every future config value or delegate trustworthy.
Without an explicit boundary, open config could be mistaken for executable
instructions and surface completion for a Business result.

Require Platforms to process executable behavior only as defined by the active
Action-type contract, allow stricter runtime policy and instance rejection, and
keep later Business resource state authoritative.

Carry #553's concrete security and fallback concerns into conditional
Action-type authoring guidance. Extension specifications define executable
fields, trust anchors, interaction controls, completion observation, failure
behavior, and fallback only where relevant. This preserves the existing wire
shape without introducing a generic executor, callback, or state model.

* adopt actions in Catalog responses

   Expose response-only Actions on Catalog Search, batch Lookup, and
   successful Get Product responses so negotiated extensions can gate or
   explain catalog effects without introducing a Catalog workflow.

   Keep existing product payload requirements intact, allow Businesses to
   return zero, some, or all otherwise relevant results, and require a fresh
   Catalog operation after Action processing. Clarify the shared Action,
   Message, identity, sequencing, and trust rules while preserving Checkout
   as the lifecycle authority.

* define Checkout Action recovery

   An Action can report completion before the Business reflects its result,
   leaving the Platform at risk of repeating external work, resubmitting
   Complete Checkout, or checking state indefinitely.

   Make Business-reported Checkout state authoritative: use bounded Get
   Checkout requests through expires_at, prevent unsafe Action retries, and
   order fallback, continue_url handoff, and cancellation. Extend the
   authoring guide so concrete Action contracts define timeout, backoff,
   safe-retry, and failure behavior without adding generic polling fields.

* clarify Action identity scope

   Catalog operations do not share a resource lifetime, making the common
   cross-response ID rule ambiguous.

   Define uniqueness within each response, retain stable IDs across
   representations of the same parent resource, and allow independent Catalog
   calls to reuse IDs without implying correlation.

* simplify Action gating and freeze Checkout updates

   The per-instance required flag did not represent a distinction used by any
   current Action and made gating easy to conflate with Message severity.
   Accepted asynchronous completion also left Update availability ambiguous,
   risking a full replacement racing Business order processing.

   Make every emitted Action gate its Action-type-defined effect, leave Messages
   to explain responses and operation outcomes, and remove required from the
   common schema and adopting capability examples.

   Treat complete_in_progress as frozen for new Update and Complete operations.
   Keep Get Checkout authoritative, preserve replay-protected duplicate responses,
   and align the core, REST, MCP, OpenAPI, and OpenRPC contracts.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants