Skip to content
Open
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
35 changes: 35 additions & 0 deletions src/bedrock_agentcore/payments/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,39 @@ user interaction, `wait_for_ready=True` cannot be used when creating a Quick Cre

---

### Rotating Connector Credentials

Quick Create connectors have service-managed credentials, so you can replace them without signing in
to the payment provider:

```python
from bedrock_agentcore.payments import CoinbaseCdpSecret, PaymentClient

payment_client = PaymentClient(region_name="us-east-1")
result = payment_client.rotate_payment_connector_credentials(
payment_manager_id="payment-manager-id",
payment_connector_id="payment-connector-id",
secrets=[CoinbaseCdpSecret.API_KEY, CoinbaseCdpSecret.WALLET_SECRET],
)

print(result["status"], result["updatedAt"])
```

The rotation completes before the response returns, so there is no status to poll — on success the
connector stays `READY`, and on failure it is left unchanged and the request can be retried. Only one
rotation runs at a time per connector, so a concurrent call fails with `ConflictException`.

Rotation replaces the credentials on the connector's credential provider, so every connector sharing
that provider is affected. Replace any copy of the previous credentials that you use outside
AgentCore.

This applies only to connectors with a `provisionMode` of `QUICK_CREATE` — check
`get_payment_connector()["provisionMode"]` if you are unsure. For `MANUAL` connectors you own the
credentials: rotate them with the payment provider, then update the credential provider with the new
values.

---

### Creating a Payment Instrument

Create a payment instrument for a user. Below is an example creating an Ethereum-compatible embedded crypto wallet:
Expand Down Expand Up @@ -907,6 +940,7 @@ except PaymentError as e:
| `get_payment_connector()` | Retrieve payment connector details |
| `list_payment_connectors()` | List payment connectors for a manager |
| `update_payment_connector()` | Update a payment connector |
| `rotate_payment_connector_credentials()` | Rotate a connector's service-managed credentials |
| `delete_payment_connector()` | Delete a payment connector |
| `create_payment_manager_with_connector()` | One-step setup with automatic rollback |

Expand All @@ -931,6 +965,7 @@ except PaymentError as e:
| `PaymentConnectorStatus` | Payment connector statuses |
| `PaymentConnectorType` | Supported connector types (CoinbaseCDP, StripePrivy) |
| `PaymentConnectorProvisionMode` | Connector provisioning modes (MANUAL, QUICK_CREATE) |
| `CoinbaseCdpSecret` | Rotatable Coinbase CDP secrets (API_KEY, WALLET_SECRET) |
| `PaymentsAuthorizerType` | Authorizer types (AWS_IAM, CUSTOM_JWT) |
| `NETWORK_PREFERENCES` | Default blockchain network preference order |
| `DEFAULT_MAX_RESULTS` | Default pagination limit (100) |
Expand Down
2 changes: 2 additions & 0 deletions src/bedrock_agentcore/payments/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from .client import PaymentClient
from .constants import (
DEFAULT_MAX_RESULTS,
CoinbaseCdpSecret,
PaymentConnectorProvisionMode,
PaymentConnectorStatus,
PaymentConnectorType,
Expand Down Expand Up @@ -44,6 +45,7 @@
"PaymentConnectorStatus",
"PaymentConnectorType",
"PaymentConnectorProvisionMode",
"CoinbaseCdpSecret",
"PaymentsAuthorizerType",
"PaymentType",
"DEFAULT_MAX_RESULTS",
Expand Down
111 changes: 110 additions & 1 deletion src/bedrock_agentcore/payments/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@
from bedrock_agentcore._utils.user_agent import build_user_agent_suffix
from bedrock_agentcore.services.identity import IdentityClient

from .constants import PaymentConnectorProvisionMode, PaymentConnectorStatus
from .constants import (
CoinbaseCdpSecret,
PaymentConnectorProvisionMode,
PaymentConnectorStatus,
)

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -138,6 +142,7 @@ class PaymentClient:
"list_payment_connectors",
"update_payment_connector",
"delete_payment_connector",
"rotate_payment_connector_credentials",
}

@staticmethod
Expand Down Expand Up @@ -247,6 +252,36 @@ def _build_provider_config_input(
f"Unsupported credential_provider_vendor: '{vendor}'. Supported vendors are: CoinbaseCDP, StripePrivy"
)

@staticmethod
def _build_rotation_config_input(
secrets: List[Union[str, CoinbaseCdpSecret]],
) -> Dict[str, Any]:
"""Build the credentialsToRotate input for a credential rotation request.

CoinbaseCDP is the only provider CredentialRotationConfig currently models, so
there is nothing for the caller to select. The service validates the secret names
(CoinbaseCdpSecret enum), rejects an empty list (@length(min: 1)) and rejects
duplicates (@uniqueItems), so this only normalizes enum members to their values.

Args:
secrets: The service-managed secrets to rotate. Accepts CoinbaseCdpSecret
members or their string values.

Returns:
Dictionary with the single provider entry for the connector type

Example:
For CoinbaseCDP connectors:
{
"coinbaseCDP": {
"secrets": ["API_KEY", "WALLET_SECRET"]
}
}
"""
normalized_secrets = [secret.value if isinstance(secret, CoinbaseCdpSecret) else secret for secret in secrets]

return {"coinbaseCDP": {"secrets": normalized_secrets}}

def __init__(
self,
region_name: Optional[str] = None,
Expand Down Expand Up @@ -794,6 +829,7 @@ def get_payment_connector(self, payment_manager_id: str, payment_connector_id: s
"name": response.get("name"),
"description": response.get("description"),
"providerType": response.get("type"),
"provisionMode": response.get("provisionMode"),
"status": response.get("status"),
"createdAt": response.get("createdAt"),
"updatedAt": response.get("lastUpdatedAt"),
Expand All @@ -803,6 +839,10 @@ def get_payment_connector(self, payment_manager_id: str, payment_connector_id: s
authorization_url
):
result["authorizationUrl"] = authorization_url
# Present only for QUICK_CREATE connectors, whose credentials the service manages
credentials_updated_at = response.get("credentialsUpdatedAt")
if credentials_updated_at is not None:
result["credentialsUpdatedAt"] = credentials_updated_at
return result

except ClientError as e:
Expand Down Expand Up @@ -849,6 +889,7 @@ def list_payment_connectors(
"name": connector.get("name"),
"description": connector.get("description"),
"providerType": connector.get("type"),
"provisionMode": connector.get("provisionMode"),
"status": connector.get("status"),
"createdAt": connector.get("createdAt"),
"updatedAt": connector.get("lastUpdatedAt"),
Expand Down Expand Up @@ -990,6 +1031,74 @@ def get_connector_status(conn_id):
logger.error("Failed to update payment connector: %s", e)
raise

def rotate_payment_connector_credentials(
self,
payment_manager_id: str,
payment_connector_id: str,
secrets: List[Union[str, CoinbaseCdpSecret]],
client_token: Optional[str] = None,
) -> Dict[str, Any]:
"""Replace a payment connector's service-managed credentials with new ones.

Use this only for connectors with a provision mode of QUICK_CREATE, whose
credentials the service issued and stores. For a connector with a provision mode of
MANUAL you own the credentials: rotate them with the payment provider, then call
UpdatePaymentCredentialProvider with the new values.

The rotation completes before the response is returned, so there is no status to
poll. On success the connector stays READY. On failure the connector and its
existing credentials are left unchanged and the request can be retried. Only one
rotation runs at a time for a given connector, so a concurrent call fails with
ConflictException.

Rotation replaces the credentials on the connector's credential provider, so every
connector that uses that provider is affected. Replace any copy of the previous
credentials that you use outside AgentCore.

Args:
payment_manager_id: ID of the payment manager
payment_connector_id: ID of the connector whose credentials to rotate
secrets: The service-managed secrets to rotate (at least one). Accepts
CoinbaseCdpSecret members or their string values.
client_token: Optional idempotency token. If not provided, a UUID will be generated.

Returns:
Dictionary with the connector IDs, its status after the rotation, and the
timestamp the rotation completed

Raises:
ClientError: If the rotation fails, including a ValidationException if secrets
is empty or names a secret the service does not support
"""
credentials_to_rotate = self._build_rotation_config_input(secrets)

if client_token is None:
client_token = str(uuid.uuid4())

try:
logger.info(
"Rotating credentials for payment connector: %s for manager %s",
payment_connector_id,
payment_manager_id,
)
response = self.payments_cp_client.rotate_payment_connector_credentials(
paymentManagerId=payment_manager_id,
paymentConnectorId=payment_connector_id,
credentialsToRotate=credentials_to_rotate,
clientToken=client_token,
)

return {
"paymentConnectorId": response.get("paymentConnectorId"),
"paymentManagerId": response.get("paymentManagerId"),
"status": response.get("status"),
"updatedAt": response.get("lastUpdatedAt"),
}

except ClientError as e:
logger.error("Failed to rotate payment connector credentials: %s", e)
raise

def create_payment_manager_with_connector(
self,
payment_manager_name: str,
Expand Down
11 changes: 11 additions & 0 deletions src/bedrock_agentcore/payments/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,17 @@ class PaymentConnectorProvisionMode(Enum):
QUICK_CREATE = "QUICK_CREATE"


class CoinbaseCdpSecret(Enum):
"""Service-managed Coinbase CDP secrets that can be rotated.

Only credentials the service provisioned (QUICK_CREATE provision mode) are
rotatable. Credentials you supplied yourself remain your responsibility.
"""

API_KEY = "API_KEY"
WALLET_SECRET = "WALLET_SECRET"


class PaymentType(Enum):
"""Payment protocols supported by ProcessPayment."""

Expand Down
Loading
Loading