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!

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