Subchapter 21.2
references/operator-guide.mdMarkdown15 KBView on GitHub
Operator-facing setup and role guidance. Read the IAM section before deploying.
(Kept in references/ rather than as a skill-level README: in this repo READMEs live
at plugin level, and every skill’s operator material goes in references/.)
Every payment control is enforced in code, from an operator-owned config file, before any signing. Instructions to a model are not access controls.
This is the most important section. Read it first.
AgentCore Payments defines a four-role model. Follow the official guide: https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/payments-iam-roles.html (opens in a new tab)
| Role | Purpose | Who uses it |
|---|---|---|
| ControlPlaneRole | Payment managers, connectors, credential providers | Administrator |
| ManagementRole | Payment instruments and sessions. Carries an explicit Deny on ProcessPayment | A human, running this skill’s admin CLI |
| ProcessPaymentRole | Executes payments against an existing session. No session writes | The agent runtime |
| ResourceRetrievalRole | Service role AgentCore assumes to retrieve credentials | bedrock-agentcore.amazonaws.com |
The agent must have neither the ManagementRole nor the ability to run
scripts/agents_pay_admin.py.
A human uses the ManagementRole to create the payment instrument and to authorize each spending session. The agent runs with the ProcessPaymentRole, which can spend an already-approved session but cannot create one.
If the agent gets both, it can mint itself a fresh budget whenever it exhausts one, and the per-session limit stops bounding anything. The AWS guide is explicit:
“Do not include PaymentSession write permissions (for example,
CreatePaymentSession) andProcessPaymentin the same role, or the caller can bypass payment limits by creating new sessions with elevated budgets.”
and on why the split exists at all:
“Separating payment management from payment execution prevents a single compromised identity from both creating sessions with unlimited budgets and executing payments against those sessions.”
IAM is the real boundary. The runtime role must exclude
CreatePaymentSession and every Create* setup action. Verify:
aws iam get-role-policy --role-name <ProcessPaymentRole> --policy-name <Policy> \
| grep -c CreatePaymentSession # must be 0A runtime AccessDeniedException on CreatePaymentSession is the control
working. Do not “fix” it by granting the permission.
Keep the admin CLI out of the agent’s reach. Put it on a workstation or a separate host, not in the runtime image.
The TTY gate is defence in depth, not the boundary. new-session refuses
to run without an interactive terminal and has no --yes flag, so a headless
agent cannot drive it. But an agent running as your user in an interactive
terminal could. IAM is what actually stops that.
| Step | Command | Role |
|---|---|---|
| 1 | agentcore add payment-manager / payment-connector / deploy | ControlPlaneRole |
| 2 | agents_pay_admin.py init-config | none — writes a local file |
| 3 | agents_pay_admin.py create-instrument | ManagementRole |
| 4 | agents_pay_admin.py new-session | ManagementRole |
| 5 | agent calls x402_fetch | ProcessPaymentRole |
# 0. Prerequisites
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements.txt
npm install -g @aws/agentcore
# 1. Provision (ControlPlaneRole; only step that touches provider secrets)
agentcore add payment-manager # NO FLAGS — interactive wizard
agentcore add payment-connector # NO FLAGS — interactive wizard
agentcore deploy
# 2. Write the config with at least one approved payee
python scripts/agents_pay_admin.py init-config \
--max-per-payment-usd 0.05 \
--recipient 0xMerchantWalletAddress
# 3. Create the wallet (ManagementRole) — payer identity comes from the config
python scripts/agents_pay_admin.py create-instrument --email you@example.com
# 3a/3b. THE END USER now delegates in a browser and funds the wallet with USDC.
# Both belong to the wallet, and both must happen BEFORE step 4 —
# sessions are time-bounded, so minting one first just burns the clock.
# Step 3 prints the delegation URL and the wallet address.
# 4. Authorize a budget (ManagementRole; interactive — you type "approve")
python scripts/agents_pay_admin.py new-session --budget 1.00
# 5. Verify, then register the tool in your agent
python scripts/agents_pay_admin.py preflightStep 4 is an ordinary command that prompts. It is not a web console or a callback — you run it in your shell and type one word:
$ python3 scripts/agents_pay_admin.py new-session --budget 1.00
About to create a payment session:
manager : arn:aws:bedrock-agentcore:us-west-2:1234:payment-manager/pm-abc
payer : agents-pay-0e2ed33d1100
budget : 1.00 USD (hard cap for the whole session)
expires in: 60 minutes
Type 'approve' to continue: approve <-- you type this
Payment session created: ps-7f3a9c2e
Recorded in : ~/.agents-pay/config.json (nothing to copy by hand)Anything other than approve aborts and no session is created. If stdin is not a
terminal — an agent shelling out, CI, or a pipe — the command refuses outright
rather than prompting:
Refusing to create a payment session without an interactive terminal.
Session creation requires a human typing 'approve' at a TTY. There is no
non-interactive mode: that would let an automated caller mint spending budget.Piping approve in does not work either.
If the agent runs on OpenClaw, install the published plugin instead of wiring the Python functions by hand:
openclaw plugins install clawhub:@aws/aws-agents-pay
openclaw plugins inspect aws-agents-pay # confirm what it registersOpenClaw executes payments through the TypeScript plugin’s get_paid_content
tool. Other hosts use the equivalent Python x402_fetch tool. Choose exactly one
runtime path; enabling both would make policy enforcement path-dependent.
Two things to confirm before you trust the OpenClaw runtime:
agentcore wizard, never in a model-visible
schema — otherwise they land in transcripts, traces, and logs.If either is wrong, disable the plugin before switching to the Python
x402_fetch path.
Those integrations settle payments inside the framework, so none of this skill’s
controls apply — no per-payment ceiling, no origin vetting, no derived idempotency
token, and auto_session=True requires CreatePaymentSession on the runtime role,
which is the arrangement the IAM guide warns against.
They are a reasonable choice for a prototype or an agent whose whole tool surface you
own. For an agent spending your money against the open web, register x402_fetch
instead. Either way, do not enable both — the model would choose which one settles
a given 402, and the gate becomes advisory.
See “The gate only covers what routes through it” in
security-model.md for the full comparison.
from strands import Agent, tool
from x402_fetch import x402_fetch, payment_session_status
agent = Agent(model=..., tools=[tool(x402_fetch), tool(payment_session_status)])| Capability | Function | Causes spending? | Can raise a limit? |
|---|---|---|---|
| Pay and fetch | x402_fetch(url) | Yes, within both ceilings | No |
| Check remaining budget | payment_session_status() | No — read-only | No |
| Pay for a browser navigation | prepare_browser_payment(url) | Yes, within both ceilings | No |
Out of reach entirely: session creation, infrastructure provisioning, provider credentials, and signed payment proofs.
One file, ~/.agents-pay/config.json — mode 0600, in a 0700 directory, written
atomically. It holds no secrets: only resource identifiers and limits.
{
"resources": {
"payment_manager_arn": "arn:aws:bedrock-agentcore:...:payment-manager/pm-1",
"payment_instrument_id": "pi-...",
"payment_session_id": "ps-...",
"user_id": "agents-pay-0e2ed33d1100",
"region": "us-west-2"
},
"policy": {
"max_per_payment_usd": "0.05",
"allowed_networks": ["eip155:84532"],
"allowed_assets": { "eip155:84532": ["0x036CbD53842c5426634e7929541eC2318f3dCF7e"] },
"allowed_schemes": ["exact"]
}
}create-instrument and new-session write their results here, so nothing needs
copying by hand. The sanctioned runtime reads this file and exposes no config-write
operation.
Single tenant. One installation is one payer. init-config generates the
user_id once and every later step reads it from the config, so you never retype it
— and cannot accidentally mismatch it between create-instrument and new-session,
which would produce a session unable to spend the instrument. It is random rather
than derived from your login or hostname, since it reaches the payments API and the
wallet’s linked accounts. Pass --user-id to override if you really need to. For
multi-tenant runtime isolation, use a separate OS account or isolated runtime for
each tenant. AGENTS_PAY_CONFIG selects paths for human-run admin commands only.
The runtime config path is fixed. It resolves .agents-pay/config.json from
the operating-system account and ignores HOME, AGENTS_PAY_CONFIG, and
X402_POLICY_FILE. Resource identifiers in that file take precedence over their
environment fallbacks. Containers and Lambda may still inject identifiers by
environment when the file omits them, but the policy itself must come from the
fixed file.
File modes protect against other OS principals, not arbitrary code already running as the owner. A harness that grants unrestricted same-user shell access must isolate the signer and config with a separate OS identity, container, or AWS role boundary.
By default the agent may fetch any public HTTPS site. allowed_origins is
optional; set it only when you want to pin a known merchant set. Payment normally
requires an approved recipient: the challenge payTo must appear in
allowed_recipients, or trusted code refuses before signing.
For a new merchant, inspect the unpaid 402 challenge, review payTo, amount,
asset, and network, then re-run init-config --force --recipient <payTo> with the
intended allowlist. Keep the approval in Python policy, not in a prompt.
For an explicit open-recipient policy, run init-config with
--allow-any-recipient instead of any --recipient flags. This delegates
beneficiary choice to the publisher. Scheme, network, exact asset, origin/resource,
per-payment ceiling, and cumulative session budget remain enforced. Never set
allow_any_recipient and allowed_recipients together; trusted code rejects that
ambiguous policy.
| Bound | Scope | Set by |
|---|---|---|
| Session budget | Cumulative — total before a human re-approves | new-session |
max_per_payment_usd | Per transaction | the policy section |
Both are needed. With only the session budget, one hostile challenge for the entire remaining balance drains it in a single payment. (A per-payment bound arguably belongs in the service-side session budget; until AgentCore Payments offers one, the skill enforces it locally.)
The model is treated as inside the threat model — the design must hold when prompt injection succeeds. In summary:
resource object is included only after
URL-binding validation (origin+path must match the requested URL); arbitrary
publisher-injected fields are stripped.return_body: true
in the config file, body content is returned bounded (10 KiB cap) and marked
"untrusted": true. Instructions inside it are data, never commands.Full threat model and enforcement table:
security-model.md.
| Path | Role |
|---|---|
SKILL.md | The skill the agent loads |
scripts/x402_policy.py | The trusted decision point |
scripts/x402_fetch.py | Hardened fetch, settle, session status, browser handles |
scripts/agents_pay_admin.py | Human-run admin CLI — keep away from the agent |
scripts/test_x402_policy.py | Security regression tests |
scripts/test_portability.py | Cross-runtime portability check |
security-model.md | Threat model and enforcement table |
setup.md | Full provisioning walkthrough and IAM policies |
troubleshooting.md | Refusal and failure diagnosis |
python3 scripts/test_x402_policy.py # security regression tests
python3 scripts/test_portability.py # cross-runtime checksCreatePaymentSession by the runtime principal. It should be
impossible, so any occurrence means the IAM split has regressed.bedrock-agentcore calls, especially
ProcessPayment.