Skip to content

Commit 4469333

Browse files
authored
feat(payments): add AgentCore Payments as first-class CLI resource (#1261)
* feat(payments): add AgentCore Payments as first-class CLI resource Adds AgentCore Payments as a first-class resource type in the CLI: - `agentcore add/remove payment-manager` and `payment-connector` (CoinbaseCDP + StripePrivy), CLI + TUI wizard - Cascading delete of connectors + credentials + .env.local cleanup - CDK-backed deploy via AgentCorePaymentManager / AgentCorePaymentConnector L3 constructs, including runtime-role payment data-plane IAM grants - Payment credential provider setup (imperative, AgentCore Identity vault) - CFN output parsing into deployed-state - Invoke flags: --payment-instrument-id, --payment-session-id, --auto-session - Strands template wires AgentCorePaymentsPlugin; PAYMENT_SYSTEM_PROMPT references the plugin-provided http_request tool Schema: - payments[] in agentcore.json, .optional() (non-breaking: absent configs are not rewritten with payments: []) - payment manager name regex matches CreatePaymentManager API (no underscore); connector names allow underscores per CreatePaymentConnector - getOrCreatePaymentSession unwraps the CreatePaymentSession `paymentSession` response so --auto-session forwards a real session id Verified end-to-end on Base Sepolia: real on-chain USDC settle via the SDK plugin (bedrock-agentcore >= 1.12.0 / PR #493). * fix(payments): route vended CDK spec access through specAny The vended cdk/bin/cdk.ts compiles against the published @aws/agentcore-cdk schema type, which lags the CLI's AgentCoreProjectSpec (no payments/harnesses fields). Restore `const specAny = spec as any` and route payments/harnesses/ gateway field access through it, fixing TS2304 (specAny undefined) and TS2339 (payments missing) introduced while resolving the rebase conflict in this file. Regenerates the asset snapshot to match. * ci(e2e): wire CoinbaseCDP creds into payment deploy test The payment E2E deploy test (payment-strands-bedrock.test.ts) gates on CDP_API_KEY_ID / CDP_API_KEY_SECRET / CDP_WALLET_SECRET but the workflow never supplied them, so it silently skipped in CI. - Map CDP_* env vars in the GA test step from the E2E secret's CDP_* JSON keys (surfaced as E2E_CDP_* by parse-json-secrets). - Add payment-strands-bedrock.test.ts to the GA run command and exclude it from change-detection GA_EXTRA to avoid double-running. Requires CDP_API_KEY_ID / CDP_API_KEY_SECRET / CDP_WALLET_SECRET added to the E2E Secrets Manager secret. Absent (e.g. forks) -> test self-skips via its hasCdpCreds gate. * feat(payments): scope invoke payments to an end user via --payment-user-id Payment instruments and sessions are scoped per end user (wallet owner), but `agentcore invoke` never sent that identity to the agent: --user-id went only to the runtime/Identity header (dropped by the runtime), and the invoke body carried no user_id. The agent (main.py) already reads payload.get("user_id"), so it always fell back to "default-user" and looked up the wallet under the wrong identity ("Instrument not found"). - Add --payment-user-id; write it into the invoke body as user_id. Falls back to --user-id; when neither is set the agent's own "default-user" fallback applies (we never bake that value into the wire). --user-id stays the Identity-header axis. - Warn (stderr, non-fatal) when a payments-enabled invoke has no resolved payment identity, so spend is not silently commingled under "default-user". - Scope --auto-session's created session to the resolved payment identity so the session, instrument, and body user_id all align. - Interactive TUI: payment flags (without a prompt) now carry the payment context across the whole chat session instead of forcing CLI mode; header shows "Payments: active (wallet owner: <id>)". --auto-session remains CLI-only. - Tests: buildInvokePayload backward-compat + snake_case wire shape; the no-identity warning matrix; --auto-session user scoping; payment-flag mode routing. - Docs: --payment-user-id, the two-user_id model, out-of-band instrument creation, and the one-time WalletHub delegated-signing consent step. No CDK or agent-template changes needed: scoping is data-plane (invoke body), and the template already reads payload.user_id. * docs(payments): document delegated-signing setup and verified end-to-end settlement Refine the payments doc against the CDP delegated-signing docs and a verified on-chain settlement (Base Sepolia), so the WalletHub/grant flow is accurate: - Grant delegated signing now describes BOTH required layers: the project-level CDP "Delegated Signing" toggle (developer, once) AND the per-wallet WalletHub grant (end user, once). Hedge the OTP wording (CDP's primary, not only, method) and note WalletHub fronts CDP's createDelegation — there is no API to grant. - Add "End-to-end: get a transaction through" — the full create -> deploy -> create+fund instrument -> grant -> invoke recipe, for both command and interactive modes. - Explain the incognito requirement (avoid carrying a developer Coinbase session) and document transient on-chain settlement failures (retry succeeds; funds not debited on failure), with matching troubleshooting rows. * fix(payments): persist autoPayment and defaultSpendLimit defaults `add payment-manager` previously dropped autoPayment and defaultSpendLimit when not explicitly passed, so the documented defaults never reached agentcore.json. Materialize them via schema .default() (matching the evaluator precedent) and in PaymentManagerPrimitive.add() using nullish coalescing so an explicit --auto-payment false is preserved. * docs(payments): clarify defaultSpendLimit is auto-session-only defaultSpendLimit reads as a deployed-manager budget, but the service has no manager-level budget concept — it only sizes the session that `invoke --auto-session` mints for local testing. Relabel the CLI flag help, the TUI wizard hint, and the docs tables to say so explicitly, and correct the payment-manager overview that implied manager-level 'budget defaults'. * refactor(payments): remove vestigial 'pattern' field The payment-manager 'pattern' (interceptor | tool-based) was collected via CLI flag and TUI wizard, persisted, then silently dropped before deploy (never mapped in the vended bin/cdk.ts), and 'tool-based' was implemented in no layer — shim, SDK plugin, or config. Interceptor is the only real behavior (settlement is never an agent tool, per the payments design). Remove 'pattern' from the schema, CLI flag, persist path, TUI wizard step, status detail, docs, and tests. Existing agentcore.json files carrying 'pattern' still parse cleanly (the schema is not .strict(), so Zod strips the unknown key); covered by a back-compat assertion in agentcore-project.test.ts. Status now shows auto-pay state instead. * feat(payments): split add TUI into Payment Manager + Connector rows Payments was the only resource hidden behind a single 'Payment' menu entry that opened a second manager/connector picker — a nested menu no other resource has. Replace it with two top-level selectable rows ('Payment Manager', 'Payment Connector'), each routing straight into its wizard via a new AddPaymentFlow initialAction prop that skips the intermediate picker. Also fixes `agentcore add payment-manager` / `add payment-connector` with no args: their interactive fallback now passes initialResource so they land directly in the right wizard instead of the generic resource menu. Esc/back (including the error state) returns to the main Add list when launched directly, never the skipped picker. * fix(payments): show payments in the TUI status view The interactive `status` screen (ResourceGraph) iterated every resource type except payments, so a deployed Payment Manager and its connectors were silently absent — even though the data was already computed by the shared status path and `status --type payment` showed it. Add a Payments section modeled on the Gateways block (manager as parent, connectors indented as children), keyed on `payment:<name>` to pick up live deploy status, and include payments in the empty-state guard. * feat(payments): support --auto-session in the interactive invoke TUI `--auto-session` was CLI-only: it forced non-interactive mode and was never threaded into the TUI. Now `agentcore invoke --auto-session` (no prompt) launches the interactive chat and mints/reuses a payment session ONCE at TUI start — scoped to the resolved payment identity, reused on every turn (held in a ref since it is only read in invoke()'s closure, never rendered). Mirrors the CLI mint in action.ts, including its try/catch so a mint failure surfaces as a TUI error screen instead of a hard exit. The --auto-session + --payment-session-id mutual exclusion is enforced at the command boundary before rendering. With a prompt or --json, --auto-session still runs one-shot CLI as before. * feat(invoke): group --help flags into labelled sections `agentcore invoke --help` listed ~30 flags as one flat block. Apply the same pattern as `add ab-test` (opt.hidden + addHelpText): keep Core flags in the default Options list and group the rest under Payments / Output / MCP & Advanced sections, with Harness and Model-override sections gated behind preview so they only appear when those flags are registered. * test(payments): drop stale 'pattern' from unit-test fixtures Follow-up to 0ec6cac (pattern removal): four unit-test fixtures still passed the removed 'pattern' field to add()/manager literals. They were fixed in the working tree but missed from that commit's `git add`, leaving HEAD's tests referencing a field the production type no longer has (clean-checkout typecheck failed). Commit the fixtures so HEAD typechecks standalone. * fix(payments): fail fast when a connector credential ARN is unresolved The vended bin/cdk.ts mapped an unresolved payment credentialProviderArn to an empty string, which then failed opaquely server-side at CreatePaymentConnector. Throw an actionable error at synth time naming the connector, manager, and missing credential instead. Snapshot updated (vended asset). * fix(payments): use isZipExcludedEntry at the zip-packaging sites The rebase left collectFiles/collectFilesSync calling shouldExcludeEntry with a rootDir arg that isn't in scope there (TS2304). Restore the two-helper split from the original branch: shouldExcludeEntry (copy stage, has rootDir) and isZipExcludedEntry (zip stage, no rootDir) — both drop build artefacts and .env secret files; only the copy stage skips the config dir at root. * test(payments): give cfgMgr a connector before validate in e2e The 'accepts paymentToolAllowlist and networkPreferences' case reused the connector-less cfgMgr and expected `validate` to exit 0, but validate correctly rejects any manager with zero connectors. The two new fields parse fine; the failure was the 'has no connectors' business rule. Attach a connector to cfgMgr before injecting the fields and validating, and hoist the inline fs import to the top per AGENTS.md.
1 parent 23947ad commit 4469333

114 files changed

Lines changed: 12160 additions & 120 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/e2e-tests.yml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ jobs:
123123
if [ -n "$HELPERS_CHANGED" ]; then
124124
GA_EXTRA=$(find e2e-tests -name '*.test.ts' \
125125
| grep -v '^e2e-tests/strands-bedrock\.test\.ts$' \
126+
| grep -v '^e2e-tests/payment-strands-bedrock\.test\.ts$' \
126127
| grep -v '^e2e-tests/harness-' \
127128
| tr '\n' ' ')
128129
HARNESS_EXTRA=$(find e2e-tests -name 'harness-*.test.ts' \
@@ -131,6 +132,7 @@ jobs:
131132
else
132133
GA_EXTRA=$(git diff --name-only "$BASE_SHA"..HEAD -- 'e2e-tests/*.test.ts' \
133134
| grep -v '^e2e-tests/strands-bedrock\.test\.ts$' \
135+
| grep -v '^e2e-tests/payment-strands-bedrock\.test\.ts$' \
134136
| grep -v '^e2e-tests/harness-' \
135137
| tr '\n' ' ')
136138
HARNESS_EXTRA=$(git diff --name-only "$BASE_SHA"..HEAD -- 'e2e-tests/harness-*.test.ts' \
@@ -153,7 +155,16 @@ jobs:
153155
E2E_S3_ACCESS_POINT_ARN: ${{ env.E2E_S3_ACCESS_POINT_ARN }}
154156
E2E_FILESYSTEM_SUBNET_ID: ${{ env.E2E_FILESYSTEM_SUBNET_ID }}
155157
E2E_FILESYSTEM_SECURITY_GROUP_ID: ${{ env.E2E_FILESYSTEM_SECURITY_GROUP_ID }}
156-
run: npx vitest run --project e2e e2e-tests/strands-bedrock.test.ts ${{ steps.changed.outputs.ga_extra }}
158+
# CoinbaseCDP testnet creds for payment-strands-bedrock.test.ts. Sourced from
159+
# the same E2E secret (keys CDP_API_KEY_ID / CDP_API_KEY_SECRET / CDP_WALLET_SECRET),
160+
# which parse-json-secrets surfaces as E2E_CDP_*; remapped here to the unprefixed
161+
# names the test reads. Absent on forks -> test self-skips via its hasCdpCreds gate.
162+
CDP_API_KEY_ID: ${{ env.E2E_CDP_API_KEY_ID }}
163+
CDP_API_KEY_SECRET: ${{ env.E2E_CDP_API_KEY_SECRET }}
164+
CDP_WALLET_SECRET: ${{ env.E2E_CDP_WALLET_SECRET }}
165+
run:
166+
npx vitest run --project e2e e2e-tests/strands-bedrock.test.ts e2e-tests/payment-strands-bedrock.test.ts ${{
167+
steps.changed.outputs.ga_extra }}
157168

158169
- name: Install preview CLI globally
159170
run: npm install -g "$PREVIEW_TARBALL"

AGENTS.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@ These options are available on all commands:
3232

3333
- `create` - Create new AgentCore project
3434
- `add` - Add resources (agent, memory, credential, evaluator, online-eval, gateway, gateway-target, policy-engine,
35-
policy)
35+
policy, payment-manager, payment-connector)
3636
- `remove` - Remove resources (agent, memory, credential, evaluator, online-eval, gateway, gateway-target,
37-
policy-engine, policy, all)
37+
policy-engine, policy, payment-manager, payment-connector, all)
3838
- `deploy` - Deploy infrastructure to AWS
3939
- `status` - Check deployment status
4040
- `dev` - Local development server (CodeZip: uvicorn with hot-reload; Container: Docker build + run with volume mount)
@@ -88,6 +88,8 @@ Current primitives:
8888
- `GatewayTargetPrimitive` — gateway target creation/removal with code generation
8989
- `PolicyEnginePrimitive` — Cedar policy engine creation/removal
9090
- `PolicyPrimitive` — Cedar policy creation/removal within policy engines
91+
- `PaymentManagerPrimitive` — payment manager creation/removal with agent code wiring
92+
- `PaymentConnectorPrimitive` — payment connector creation/removal with credential management
9193

9294
Singletons are created in `registry.ts` and wired into CLI commands via `cli.ts`. See `src/cli/AGENTS.md` for details on
9395
adding new primitives.

docs/commands.md

Lines changed: 117 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -140,14 +140,14 @@ agentcore status --runtime-id abc123
140140
agentcore status --json
141141
```
142142

143-
| Flag | Description |
144-
| ------------------- | -------------------------------------------------------------------------------------------------------------------------- |
145-
| `--runtime-id <id>` | Look up a specific runtime by ID |
146-
| `--target <name>` | Select deployment target |
147-
| `--type <type>` | Filter by resource type: `agent`, `memory`, `credential`, `gateway`, `evaluator`, `online-eval`, `policy-engine`, `policy` |
148-
| `--state <state>` | Filter by deployment state: `deployed`, `local-only`, `pending-removal` |
149-
| `--runtime <name>` | Filter to a specific runtime |
150-
| `--json` | JSON output |
143+
| Flag | Description |
144+
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
145+
| `--runtime-id <id>` | Look up a specific runtime by ID |
146+
| `--target <name>` | Select deployment target |
147+
| `--type <type>` | Filter by resource type: `agent`, `memory`, `credential`, `gateway`, `evaluator`, `online-eval`, `payment`, `policy-engine`, `policy` |
148+
| `--state <state>` | Filter by deployment state: `deployed`, `local-only`, `pending-removal` |
149+
| `--runtime <name>` | Filter to a specific runtime |
150+
| `--json` | JSON output |
151151

152152
### validate
153153

@@ -473,6 +473,84 @@ agentcore add gateway-target \
473473
> `open-api-schema` requires `--outbound-auth` (`oauth` or `api-key`). `api-gateway` supports `api-key` or `none`.
474474
> `mcp-server` supports `oauth` or `none`.
475475
476+
### add payment-manager
477+
478+
Add a payment manager to the project. See [Payments](payments.md) for full usage guide.
479+
480+
```bash
481+
# Minimal (defaults: AWS_IAM, auto-payment enabled)
482+
agentcore add payment-manager --name MyManager
483+
484+
# With CUSTOM_JWT authorization
485+
agentcore add payment-manager \
486+
--name MyManager \
487+
--authorizer-type CUSTOM_JWT \
488+
--discovery-url https://cognito-idp.us-east-1.amazonaws.com/us-east-1_XXXXX/.well-known/openid-configuration \
489+
--allowed-clients "client-id-1,client-id-2"
490+
491+
# With advanced options
492+
agentcore add payment-manager \
493+
--name MyManager \
494+
--auto-payment true \
495+
--default-spend-limit 25.00 \
496+
--tool-allowlist "web_search,fetch_url" \
497+
--network-preferences "eip155:84532"
498+
```
499+
500+
| Flag | Description |
501+
| ---------------------------------- | --------------------------------------------------------------------------------------------------------- |
502+
| `--name <name>` | Manager name (required in non-interactive mode) |
503+
| `--authorizer-type <type>` | `AWS_IAM` (default) or `CUSTOM_JWT` |
504+
| `--discovery-url <url>` | OIDC discovery URL (required for CUSTOM_JWT) |
505+
| `--allowed-clients <clients>` | Comma-separated client IDs (CUSTOM_JWT only) |
506+
| `--allowed-audience <audience>` | Comma-separated allowed audiences (CUSTOM_JWT only) |
507+
| `--allowed-scopes <scopes>` | Comma-separated allowed scopes (CUSTOM_JWT only) |
508+
| `--auto-payment [value]` | Enable automatic payment: `true` (default) or `false` |
509+
| `--default-spend-limit <amount>` | Spend cap (USD) for `invoke --auto-session` sessions ONLY; not a deployed-agent budget (default: `10.00`) |
510+
| `--tool-allowlist <tools>` | Comma-separated tool names eligible for payment |
511+
| `--network-preferences <networks>` | Comma-separated network IDs (e.g., `eip155:84532`) |
512+
| `--description <desc>` | Human-readable description |
513+
| `--json` | JSON output |
514+
515+
### add payment-connector
516+
517+
Add a payment connector to an existing payment manager. See [Payments](payments.md) for credential details.
518+
519+
```bash
520+
# CoinbaseCDP provider
521+
agentcore add payment-connector \
522+
--manager MyManager \
523+
--name MyCDPConnector \
524+
--provider CoinbaseCDP \
525+
--api-key-id your-api-key-id \
526+
--api-key-secret your-api-key-secret \
527+
--wallet-secret your-wallet-secret
528+
529+
# StripePrivy provider
530+
agentcore add payment-connector \
531+
--manager MyManager \
532+
--name MyStripeConnector \
533+
--provider StripePrivy \
534+
--app-id your-app-id \
535+
--app-secret your-app-secret \
536+
--authorization-private-key your-private-key \
537+
--authorization-id your-auth-id
538+
```
539+
540+
| Flag | Description |
541+
| ----------------------------------- | ------------------------------------------ |
542+
| `--manager <name>` | Parent payment manager (required) |
543+
| `--name <name>` | Connector name (required) |
544+
| `--provider <provider>` | `CoinbaseCDP` (default) or `StripePrivy` |
545+
| `--api-key-id <id>` | Coinbase CDP API Key ID |
546+
| `--api-key-secret <secret>` | Coinbase CDP API Key Secret |
547+
| `--wallet-secret <secret>` | Coinbase CDP Wallet Secret |
548+
| `--app-id <id>` | Privy App ID (StripePrivy) |
549+
| `--app-secret <secret>` | Privy App Secret (StripePrivy) |
550+
| `--authorization-private-key <key>` | ECDSA P-256 private key (StripePrivy) |
551+
| `--authorization-id <id>` | Authorization key identifier (StripePrivy) |
552+
| `--json` | JSON output |
553+
476554
### add credential
477555

478556
Add a credential to the project. Supports API key and OAuth credential types.
@@ -739,19 +817,22 @@ agentcore remove runtime-endpoint --name prod
739817
agentcore remove dataset --name MyDataset
740818
agentcore remove config-bundle --name MyBundle
741819
agentcore remove ab-test --name PromptComparison
820+
agentcore remove payment-manager --name MyManager -y
821+
agentcore remove payment-connector --name MyCDPConnector --manager MyManager -y
742822

743823
# Reset everything
744824
agentcore remove all -y
745825
agentcore remove all --dry-run # Preview
746826
```
747827

748-
| Flag | Description |
749-
| ------------------- | ------------------------------------------------- |
750-
| `--name <name>` | Resource name |
751-
| `--engine <engine>` | Policy engine name (required for `remove policy`) |
752-
| `-y, --yes` | Skip confirmation |
753-
| `--dry-run` | Preview (`remove all` only) |
754-
| `--json` | JSON output |
828+
| Flag | Description |
829+
| ------------------- | --------------------------------------------------------- |
830+
| `--name <name>` | Resource name |
831+
| `--engine <engine>` | Policy engine name (required for `remove policy`) |
832+
| `--manager <name>` | Parent payment manager (required for `payment-connector`) |
833+
| `-y, --yes` | Skip confirmation |
834+
| `--dry-run` | Preview (`remove all` only) |
835+
| `--json` | JSON output |
755836

756837
---
757838

@@ -815,23 +896,27 @@ agentcore invoke --exec "cat /etc/os-release" --json
815896
The prompt can come from four sources, resolved in this precedence order: `--prompt` > positional > `--prompt-file` >
816897
piped stdin. `--prompt-file` combined with piped stdin content returns a collision error — pick one.
817898

818-
| Flag | Description |
819-
| ---------------------- | ---------------------------------------------------------------- |
820-
| `[prompt]` | Prompt text (positional argument) |
821-
| `--prompt <text>` | Prompt text (flag, takes precedence over positional) |
822-
| `--prompt-file <path>` | Read the prompt from a file (useful for long / structured input) |
823-
| `--runtime <name>` | Specific runtime |
824-
| `--target <name>` | Deployment target |
825-
| `--session-id <id>` | Continue a specific session |
826-
| `--user-id <id>` | User ID for runtime invocation (default: `default-user`) |
827-
| `--stream` | Stream response in real-time |
828-
| `--tool <name>` | MCP tool name (use with `call-tool` prompt) |
829-
| `--input <json>` | MCP tool arguments as JSON (use with `--tool`) |
830-
| `-H, --header <h>` | Custom header (`"Name: Value"`, repeatable) |
831-
| `--bearer-token <t>` | Bearer token for CUSTOM_JWT auth |
832-
| `--exec` | Execute a shell command in the runtime container |
833-
| `--timeout <seconds>` | Timeout in seconds for `--exec` commands |
834-
| `--json` | JSON output |
899+
| Flag | Description |
900+
| ------------------------------ | --------------------------------------------------------------------- |
901+
| `[prompt]` | Prompt text (positional argument) |
902+
| `--prompt <text>` | Prompt text (flag, takes precedence over positional) |
903+
| `--prompt-file <path>` | Read the prompt from a file (useful for long / structured input) |
904+
| `--runtime <name>` | Specific runtime |
905+
| `--target <name>` | Deployment target |
906+
| `--session-id <id>` | Continue a specific session |
907+
| `--user-id <id>` | User ID for runtime invocation (default: `default-user`) |
908+
| `--stream` | Stream response in real-time |
909+
| `--tool <name>` | MCP tool name (use with `call-tool` prompt) |
910+
| `--input <json>` | MCP tool arguments as JSON (use with `--tool`) |
911+
| `-H, --header <h>` | Custom header (`"Name: Value"`, repeatable) |
912+
| `--bearer-token <t>` | Bearer token for CUSTOM_JWT auth |
913+
| `--payment-instrument-id <id>` | Payment instrument ID for x402 payments |
914+
| `--payment-session-id <id>` | Payment session ID for budget tracking |
915+
| `--auto-session` | Auto-create/reuse a payment session for testing |
916+
| `--payment-user-id <id>` | End-user (wallet owner) to scope payments to; defaults to `--user-id` |
917+
| `--exec` | Execute a shell command in the runtime container |
918+
| `--timeout <seconds>` | Timeout in seconds for `--exec` commands |
919+
| `--json` | JSON output |
835920

836921
Piped stdin is auto-detected: when no prompt is supplied and stdin is not a TTY, the prompt is read from stdin.
837922

0 commit comments

Comments
 (0)