Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 66 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,15 @@ agentcore # interactive TUI
│ │ ├── list # list API key credential providers
│ │ ├── update # update an API key credential provider
│ │ └── delete # delete an API key credential provider
│ └── oauth2-credential-provider
│ ├── create # create an OAuth2 credential provider
│ ├── get # get an OAuth2 credential provider
│ ├── list # list OAuth2 credential providers
│ ├── update # update an OAuth2 credential provider
│ └── delete # delete an OAuth2 credential provider
│ ├── oauth2-credential-provider
│ │ ├── create # create an OAuth2 credential provider
│ │ ├── get # get an OAuth2 credential provider
│ │ ├── list # list OAuth2 credential providers
│ │ ├── update # update an OAuth2 credential provider
│ │ └── delete # delete an OAuth2 credential provider
│ └── payment-credential-provider
│ ├── get # get a payment credential provider
│ └── list # list payment credential providers
├── runtime # inspect deployed AgentCore Runtimes
│ ├── get # fetch a Runtime by id
│ ├── list # list Runtimes (server-side paginated)
Expand Down Expand Up @@ -107,6 +110,20 @@ agentcore # interactive TUI
│ │ └── list # list Rules under a Gateway
│ └── policy
│ └── generate # generate Cedar for a Gateway from a prompt (TUI when run bare)
├── payment # inspect AgentCore Payments (command line only for now)
│ ├── manager
│ │ ├── get # get a payment manager by id
│ │ └── list # list payment managers (server-side paginated)
│ ├── connector # connectors under a payment manager
│ │ ├── get # get a connector (shows the Quick Create authorization URL while pending)
│ │ └── list # list a manager's connectors
│ ├── session # budget-limited payment contexts (data plane)
│ │ ├── get
│ │ └── list
│ └── instrument # embedded crypto wallets (data plane)
│ ├── get
│ ├── list
│ └── balance # read token balance on an explicit chain (default token: USDC)
├── eval # evaluate and optimize AgentCore agents
│ └── evaluator # manage AgentCore evaluators
│ ├── llm-as-a-judge # LLM-as-a-Judge evaluators
Expand Down Expand Up @@ -186,6 +203,49 @@ agentcore project invoke harness \
Use `--target` to select a deployment target. When a project declares exactly
one resource of the requested type, `--name` may be omitted.

### Inspect AgentCore Payments

The `payment` commands call the Payments control and data planes directly, with
no project involved. This command family currently provides read-only inspection
of existing managers, connectors, sessions, instruments, and payment credential
providers. It does not create IAM roles or change provider credentials.

```bash
# Inspect managers and their connectors.
agentcore payment manager list --json
agentcore payment manager get --id <manager id>
agentcore payment connector list --manager-id <manager id>
agentcore payment connector get --manager-id <manager id> --connector-id <connector id>

# Inspect provider metadata stored in AgentCore Identity.
agentcore identity payment-credential-provider list --json
agentcore identity payment-credential-provider get --name <provider name>

# Session and instrument commands take the parent manager ID and a user id.
agentcore payment session list --manager-id <manager id> --user-id alice
agentcore payment instrument list --manager-id <manager id> --user-id alice

# Check funding on one chain. USDC is the default token.
agentcore payment instrument balance --manager-id <manager id> --user-id alice \
--connector-id <connector id> --instrument-id <instrument id> --chain BASE_SEPOLIA
```

`instrument get` returns instrument metadata without querying balances. `balance`
requires an explicit chain and accepts `--token USDC`; wallet network families
such as ETHEREUM do not identify whether to query mainnet or a testnet. The JSON
response retains the raw atomic amount string and decimals. A service error is
reported as an error, never converted to a zero balance.

Data-plane commands work against managers that use the `AWS_IAM` authorizer.
The CLI resolves `--manager-id` through `GetPaymentManager` in the configured
region, then supplies the returned ARN to the data-plane API. Callers need
`bedrock-agentcore:GetPaymentManager` as well as the relevant data-plane action.
Region resolution follows the other imperative commands: `--region`, environment
variables, the active AWS profile, then the CLI default.
A `CUSTOM_JWT` manager accepts only bearer tokens on its data plane, which
these commands do not send yet; the CLI reports that limitation before calling
the data plane.

### Examples

```bash
Expand Down
2 changes: 1 addition & 1 deletion src/components/CliOnlyScreen.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ describe("menus list command-line-only subcommands below a divider", () => {
await waitForText(r.lastFrame, "command line only");
expect(menuEntries(r.lastFrame()!)).toEqual({
screens: ["project", "harness", "identity", "runtime", "memory", "gateway", "eval"],
cliOnly: ["feedback", "config", "update"],
cliOnly: ["payment", "feedback", "config", "update"],
});
r.unmount();
});
Expand Down
12 changes: 12 additions & 0 deletions src/core/identity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
GetPaymentCredentialProviderCommand,
ListApiKeyCredentialProvidersCommand,
ListOauth2CredentialProvidersCommand,
ListPaymentCredentialProvidersCommand,
UpdateApiKeyCredentialProviderCommand,
UpdateOauth2CredentialProviderCommand,
UpdatePaymentCredentialProviderCommand,
Expand All @@ -26,6 +27,7 @@ import {
type CreatePaymentCredentialProviderResponse,
type DeletePaymentCredentialProviderResponse,
type GetPaymentCredentialProviderResponse,
type ListPaymentCredentialProvidersResponse,
type UpdatePaymentCredentialProviderResponse,
} from "@aws-sdk/client-bedrock-agentcore-control";
import type {
Expand Down Expand Up @@ -155,6 +157,16 @@ export class IdentityClient implements CoreIdentityClient {
.send(new GetPaymentCredentialProviderCommand({ name }));
}

async listPaymentCredentialProviders(
nextToken: string | undefined,
maxResults: number | undefined,
options: CoreOptions,
): Promise<ListPaymentCredentialProvidersResponse> {
return this.clients
.control(toClientConfig(options))
.send(new ListPaymentCredentialProvidersCommand({ nextToken, maxResults }));
}

async updatePaymentCredentialProvider(
input: UpdatePaymentCredentialProviderInput,
options: CoreOptions,
Expand Down
3 changes: 3 additions & 0 deletions src/core/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { GatewayClient } from "./gateway";
import { HarnessClient } from "./harness";
import { IdentityClient } from "./identity";
import { MemoryClient } from "./memory";
import { PaymentClient } from "./payment";
import { PolicyClient } from "./policy";
import { ObservabilityClient } from "./observability";
import { CloudWatchClient } from "./observability/index";
Expand Down Expand Up @@ -79,6 +80,7 @@ export class CoreClient implements AwsClients {
readonly eval: EvalClient;
readonly observability: ObservabilityClient;
readonly policy: PolicyClient;
readonly payment: PaymentClient;

readonly projectManager: ProjectManager;
readonly bedrockAgentImporter: CoreBedrockAgentImporter;
Expand All @@ -101,6 +103,7 @@ export class CoreClient implements AwsClients {
);
this.gateway = new GatewayClient(this, fetch, this.logger.child({ module: "gateway" }));
this.policy = new PolicyClient(this, this.logger.child({ module: "policy" }));
this.payment = new PaymentClient(this);
// EvalClient shares the injected fetch: dataset content is served from a
// presigned S3 URL, outside the SDK seam the other operations use. The logger
// is used for batch-evaluation result-log diagnostics.
Expand Down
111 changes: 111 additions & 0 deletions src/core/payment.read.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { expect, mock, test } from "bun:test";
import { GetPaymentManagerCommand } from "@aws-sdk/client-bedrock-agentcore-control";
import { ListPaymentSessionsCommand } from "@aws-sdk/client-bedrock-agentcore";
import { PaymentClient } from "./payment";
import type { AwsClients, ClientConfig } from "./types";

const MANAGER_ID = "checkout-abc1234567";
const MANAGER_ARN = `arn:aws:bedrock-agentcore:us-west-2:123456789012:payment-manager/${MANAGER_ID}`;
const request = { managerId: MANAGER_ID, userId: "alice" };
const options = {
region: "us-east-1",
endpointUrl: "https://payments.example.test",
credentials: { accessKeyId: "test-key", secretAccessKey: "test-secret" },
};

function setup() {
const controlSend = mock(async (_command: unknown) => ({
paymentManagerArn: MANAGER_ARN,
authorizerType: "AWS_IAM",
}));
const response = { paymentSessions: [] };
const dataSend = mock(async (_command: unknown) => response);
const control = mock(
(_config: ClientConfig) =>
({ send: controlSend }) as unknown as ReturnType<AwsClients["control"]>,
);
const data = mock(
(_config: ClientConfig) => ({ send: dataSend }) as unknown as ReturnType<AwsClients["data"]>,
);
return {
client: new PaymentClient({ control, data }),
control,
data,
controlSend,
dataSend,
response,
};
}

test("resolves the manager in the configured region and forwards the returned ARN and context", async () => {
const { client, control, data, controlSend, dataSend, response } = setup();
const input = { ...request, agentName: "agent", nextToken: "page+2/=", maxResults: 5 };
expect(await client.listPaymentSessions(input, options)).toBe(response);
expect(controlSend.mock.calls[0]?.[0]).toBeInstanceOf(GetPaymentManagerCommand);
expect(controlSend.mock.calls[0]?.[0]).toMatchObject({ input: { paymentManagerId: MANAGER_ID } });
expect(dataSend.mock.calls[0]?.[0]).toBeInstanceOf(ListPaymentSessionsCommand);
expect((dataSend.mock.calls[0]![0] as ListPaymentSessionsCommand).input).toEqual({
paymentManagerArn: MANAGER_ARN,
userId: "alice",
agentName: "agent",
nextToken: "page+2/=",
maxResults: 5,
});
const config = {
region: options.region,
endpoint: options.endpointUrl,
credentials: options.credentials,
};
expect(control).toHaveBeenCalledWith(config);
expect(data).toHaveBeenCalledWith(config);

controlSend.mockResolvedValueOnce({
paymentManagerArn: `${MANAGER_ARN}-new`,
authorizerType: "AWS_IAM",
});
await client.listPaymentSessions(request, options);
expect(controlSend).toHaveBeenCalledTimes(2);
expect(dataSend.mock.calls[1]?.[0]).toMatchObject({
input: { paymentManagerArn: `${MANAGER_ARN}-new`, userId: "alice" },
});
expect(input).toEqual({ ...request, agentName: "agent", nextToken: "page+2/=", maxResults: 5 });
});

test("rejects an ARN selector before contacting AWS", async () => {
const { client, control, data } = setup();
await expect(client.listPaymentSessions({ managerId: MANAGER_ARN }, options)).rejects.toThrow(
"use a payment manager ID, not an ARN",
);
expect(control).not.toHaveBeenCalled();
expect(data).not.toHaveBeenCalled();
});

test.each([
[{ paymentManagerArn: MANAGER_ARN, authorizerType: "CUSTOM_JWT" }, "CUSTOM_JWT"],
[{ paymentManagerArn: "", authorizerType: "AWS_IAM" }, "returned no ARN"],
] as const)(
"rejects an unusable manager before data-plane access: %j",
async (manager, message) => {
const { client, controlSend, data } = setup();
controlSend.mockResolvedValueOnce(manager);
await expect(client.listPaymentSessions(request, options)).rejects.toThrow(message);
expect(data).not.toHaveBeenCalled();
},
);

test("preserves a lookup failure without contacting the data plane", async () => {
const { client, controlSend, data } = setup();
const error = new Error("Payment manager not found");
controlSend.mockRejectedValueOnce(error);
await expect(client.listPaymentSessions(request, options)).rejects.toBe(error);
expect(data).not.toHaveBeenCalled();
});

test("preserves a data-plane failure without retrying the lookup", async () => {
const { client, controlSend, dataSend } = setup();
const error = new Error("Access denied");
dataSend.mockRejectedValueOnce(error);
await expect(client.listPaymentSessions(request, options)).rejects.toBe(error);
expect(controlSend).toHaveBeenCalledTimes(1);
expect(dataSend).toHaveBeenCalledTimes(1);
});
Loading
Loading