Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .github/workflows/slack-pr-review-notification.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,7 @@ permissions:

jobs:
call:
# Fork PRs do not receive the AWS credentials required for notifications.
if: github.event.pull_request.head.repo.full_name == github.repository
uses: aws/agentcore-devx-devtools/.github/workflows/reusable-slack-pr-review-notification.yml@626595f508220b53805c86c1b54d089420add4d3
secrets: inherit
48 changes: 46 additions & 2 deletions bun.lock

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions command.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ This reference was generated from `agentcore --help` for version `1.0.0-rc.4`.
- [`agentcore payment connector`](#agentcore-payment-connector)
- [`agentcore payment connector get`](#agentcore-payment-connector-get)
- [`agentcore payment connector list`](#agentcore-payment-connector-list)
- [`agentcore payment connector rotate-credentials`](#agentcore-payment-connector-rotate-credentials)
- [`agentcore payment session`](#agentcore-payment-session)
- [`agentcore payment session get`](#agentcore-payment-session-get)
- [`agentcore payment session list`](#agentcore-payment-session-list)
Expand Down Expand Up @@ -2845,6 +2846,21 @@ list the connectors of a payment manager
- `--next-token <next-token>`: pagination token returned by a previous request
- `--max-results <max-results>`: maximum number of items to return

##### `agentcore payment connector rotate-credentials`

```text
agentcore payment connector rotate-credentials [options]
```

rotate service-managed credentials for a Quick Create Coinbase connector

**Options**

- `--manager-id <manager-id>`: the parent payment manager ID (required)
- `--connector-id <connector-id>`: the payment connector ID (required)
- `--secrets <secrets...>`: credential kinds to rotate: API\_KEY, WALLET\_SECRET, or both (not secret values) (required)
- `--client-token <client-token>`: idempotency token for this request

#### `agentcore payment session`

```text
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
"@aws-sdk/client-application-signals": "^3.1092.0",
"@aws-sdk/client-bedrock-agent": "^3.1092.0",
"@aws-sdk/client-bedrock-agentcore": "^3.1135.0",
"@aws-sdk/client-bedrock-agentcore-control": "^3.1129.0",
"@aws-sdk/client-bedrock-agentcore-control": "^3.1143.0",
"@aws-sdk/client-cloudformation": "^3.1092.0",
"@aws-sdk/client-cloudwatch-logs": "^3.1092.0",
"@aws-sdk/client-iam": "^3.1080.0",
Expand Down
12 changes: 12 additions & 0 deletions src/core/payment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ import {
GetPaymentManagerCommand,
ListPaymentConnectorsCommand,
ListPaymentManagersCommand,
RotatePaymentConnectorCredentialsCommand,
type GetPaymentConnectorResponse,
type GetPaymentManagerResponse,
type ListPaymentConnectorsResponse,
type ListPaymentManagersResponse,
type RotatePaymentConnectorCredentialsRequest,
type RotatePaymentConnectorCredentialsResponse,
} from "@aws-sdk/client-bedrock-agentcore-control";
import {
GetPaymentInstrumentBalanceCommand,
Expand Down Expand Up @@ -85,6 +88,15 @@ export class PaymentClient implements CorePaymentClient {
);
}

async rotatePaymentConnectorCredentials(
request: RotatePaymentConnectorCredentialsRequest,
options: CoreOptions,
): Promise<RotatePaymentConnectorCredentialsResponse> {
return this.clients
.control(toClientConfig(options))
.send(new RotatePaymentConnectorCredentialsCommand(request));
}

// ─── payment sessions (data plane) ──────────────────────────────────────────

async getPaymentSession(
Expand Down
4 changes: 3 additions & 1 deletion src/handlers/payment/connector/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import { renderTui } from "../../../tui";
import type { Core } from "../../types";
import { createGetPaymentConnectorHandler } from "./get";
import { createListPaymentConnectorsHandler } from "./list";
import { createRotatePaymentConnectorCredentialsHandler } from "./rotate-credentials";

export function createPaymentConnectorHandler(core: Core, io: AppIO): Router {
return new Router("connector", "manage connectors under a payment manager")
.default(renderTui(core, io))
.handler(createGetPaymentConnectorHandler(core))
.handler(createListPaymentConnectorsHandler(core));
.handler(createListPaymentConnectorsHandler(core))
.handler(createRotatePaymentConnectorCredentialsHandler(core));
}
39 changes: 39 additions & 0 deletions src/handlers/payment/connector/rotate-credentials/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { CoinbaseCdpSecret } from "@aws-sdk/client-bedrock-agentcore-control";
import z from "zod";
import { createHandler, flag } from "../../../../router";
import { JsonRendererKey } from "../../../../tui";
import type { Core } from "../../../types";
import { coreOptsFromCtx } from "../../../utils";

export const createRotatePaymentConnectorCredentialsHandler = (core: Core) =>
createHandler({
name: "rotate-credentials",
description: "rotate service-managed credentials for a Quick Create Coinbase connector",
flags: [
flag("manager-id", "the parent payment manager ID", z.string().min(1)),
flag("connector-id", "the payment connector ID", z.string().min(1)),
flag(
"secrets",
"credential kinds to rotate: API_KEY, WALLET_SECRET, or both (not secret values)",
z
.array(z.enum(CoinbaseCdpSecret))
.min(1)
.refine((secrets) => new Set(secrets).size === secrets.length, {
message: "credential selections must be unique",
}),
),
flag("client-token", "idempotency token for this request", z.string().optional()),
],
handle: async (ctx, flags) => {
const response = await core.payment.rotatePaymentConnectorCredentials(
{
paymentManagerId: flags["manager-id"],
paymentConnectorId: flags["connector-id"],
credentialsToRotate: { coinbaseCDP: { secrets: flags.secrets } },
clientToken: flags["client-token"],
},
coreOptsFromCtx(ctx),
);
ctx.require(JsonRendererKey).renderJson(response);
},
});
81 changes: 66 additions & 15 deletions src/handlers/payment/payment.read.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ const INSTRUMENT_CONNECTOR_ID = "mycdpconnectoraidandal-okve8guw4y";
const SESSION_ID = "payment-session-nq812U4e1BJIfw1";
const INSTRUMENT_ID = "payment-instrument-CG2Tl7U1HnCGfHW";
const scope = ["--manager-id", MANAGER_ID, "--user-id", "agentcore-cli-e2e"];
const rotationArgs = [
"connector",
"rotate-credentials",
"--manager-id",
MANAGER_ID,
"--connector-id",
CONNECTOR_ID,
];

function setup(resource = "manager", overrides: Partial<ReturnType<typeof fixtureFactories>> = {}) {
const core = new CoreClient({
Expand All @@ -42,7 +50,7 @@ function setup(resource = "manager", overrides: Partial<ReturnType<typeof fixtur
};
}

test("registers the read-only command tree without TUI or mutation leaves", () => {
test("registers the payment command tree without TUI leaves", () => {
const payment = compile(setup().root, ValueContext.EmptyContext()).commands.find(
(c) => c.name() === "payment",
)!;
Expand All @@ -52,7 +60,7 @@ test("registers the read-only command tree without TUI or mutation leaves", () =
),
).toEqual({
manager: ["get", "list"],
connector: ["get", "list"],
connector: ["get", "list", "rotate-credentials"],
session: ["get", "list"],
instrument: ["get", "list", "balance"],
});
Expand All @@ -61,6 +69,45 @@ test("registers the read-only command tree without TUI or mutation leaves", () =
}
});

test.each([
{ secrets: ["API_KEY"], clientToken: undefined },
{ secrets: ["WALLET_SECRET"], clientToken: "rotate-wallet" },
{ secrets: ["API_KEY", "WALLET_SECRET"], clientToken: "rotate-both" },
])("rotates the selected connector credentials: %j", async ({ secrets, clientToken }) => {
const response = {
paymentManagerId: MANAGER_ID,
paymentConnectorId: CONNECTOR_ID,
status: "READY",
lastUpdatedAt: new Date("2026-09-18T00:00:00.000Z"),
};
const send = mock(async (_command: { input: unknown }) => response);
const createControlClient = mock(() => ({ send }) as never);
const { run, io } = setup("connector", { createControlClient });
await run([
...rotationArgs,
"--secrets",
...secrets,
...(clientToken ? ["--client-token", clientToken] : []),
"--json",
]);
expect(createControlClient).toHaveBeenCalledWith({
region: "us-west-2",
});
expect(send).toHaveBeenCalledTimes(1);
const command = send.mock.calls[0]![0];
expect(command.constructor.name).toBe("RotatePaymentConnectorCredentialsCommand");
expect(command.input).toEqual({
paymentManagerId: MANAGER_ID,
paymentConnectorId: CONNECTOR_ID,
credentialsToRotate: { coinbaseCDP: { secrets } },
clientToken,
});
expect(JSON.parse(io.stdout())).toEqual({
...response,
lastUpdatedAt: response.lastUpdatedAt.toISOString(),
});
});

test.each([
["manager", "get", ["--id", "mypaymentmanager-o4ks3qfgtb"]],
["manager", "list", []],
Expand Down Expand Up @@ -180,6 +227,9 @@ test.each([
["instrument", "get", ...scope, "--instrument-id", INSTRUMENT_ID, "--manager-arn", "arn:old"],
"unknown option",
],
[rotationArgs, "--secrets"],
[[...rotationArgs, "--secrets", "INVALID"], "Invalid value for option '--secrets'"],
[[...rotationArgs, "--secrets", "API_KEY", "API_KEY"], "must be unique"],
] as const)("rejects incomplete or obsolete selectors: %j", async (args, message) => {
await expect(setup().run([...args])).rejects.toThrow(message);
});
Expand All @@ -197,16 +247,17 @@ test.each([
await expect(run(["session", "list", ...scope])).rejects.toThrow(message);
});

test.each(["createControlClient", "createDataClient"] as const)(
"preserves a payment service failure from %s",
async (factory) => {
const error = new Error("Payment request denied");
const send = mock(async () => {
throw error;
});
const { run, io } = setup("session", { [factory]: () => ({ send }) as never });
await expect(run(["session", "list", ...scope])).rejects.toBe(error);
expect(send).toHaveBeenCalledTimes(1);
expect(io.stdout()).toBe("");
},
);
test.each([
["createControlClient", ["session", "list", ...scope]],
["createDataClient", ["session", "list", ...scope]],
["createControlClient", [...rotationArgs, "--secrets", "API_KEY"]],
] as const)("preserves a payment service failure from %s for %j", async (factory, args) => {
const error = new Error("Payment request denied");
const send = mock(async () => {
throw error;
});
const { run, io } = setup("session", { [factory]: () => ({ send }) as never });
await expect(run([...args])).rejects.toBe(error);
expect(send).toHaveBeenCalledTimes(1);
expect(io.stdout()).toBe("");
});
6 changes: 6 additions & 0 deletions src/handlers/payment/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import type {
GetPaymentManagerResponse,
ListPaymentConnectorsResponse,
ListPaymentManagersResponse,
RotatePaymentConnectorCredentialsRequest,
RotatePaymentConnectorCredentialsResponse,
} from "@aws-sdk/client-bedrock-agentcore-control";
import type {
GetPaymentInstrumentRequest,
Expand Down Expand Up @@ -45,6 +47,10 @@ export interface CorePaymentClient {
maxResults: number | undefined,
options: CoreOptions,
): Promise<ListPaymentConnectorsResponse>;
rotatePaymentConnectorCredentials(
request: RotatePaymentConnectorCredentialsRequest,
options: CoreOptions,
): Promise<RotatePaymentConnectorCredentialsResponse>;

// Core resolves the selected manager ID to the ARN required by the data plane.
getPaymentSession(
Expand Down
4 changes: 4 additions & 0 deletions src/testing/TestCoreClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import type {
GetPaymentManagerResponse,
ListPaymentConnectorsResponse,
ListPaymentManagersResponse,
RotatePaymentConnectorCredentialsResponse,
ListAgentRuntimeEndpointsResponse,
ListAgentRuntimesResponse,
ListAgentRuntimeVersionsResponse,
Expand Down Expand Up @@ -1579,6 +1580,9 @@ export class TestPaymentClient implements CorePaymentClient {
async listPaymentConnectors(): Promise<ListPaymentConnectorsResponse> {
throw new Error("Unexpected payment call");
}
async rotatePaymentConnectorCredentials(): Promise<RotatePaymentConnectorCredentialsResponse> {
throw new Error("Unexpected payment call");
}
async getPaymentSession(): Promise<GetPaymentSessionResponse> {
throw new Error("Unexpected payment call");
}
Expand Down
Loading