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
20 changes: 17 additions & 3 deletions docs/payments.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,14 +197,28 @@ AGENTCORE_CREDENTIAL_{CREDENTIAL_NAME}_AUTHORIZATION_ID=...
`{CREDENTIAL_NAME}` is the connector's credential name uppercased with hyphens replaced by underscores. For example, a
credential named `my-cdp-creds` becomes `AGENTCORE_CREDENTIAL_MY_CDP_CREDS_API_KEY_ID`.

### Secret encryption at rest

Connector secret values in `agentcore/.env.local` (API key secrets, wallet secrets, private keys) are encrypted at rest
with a machine-local key stored outside the project directory (your OS keychain, or `~/.agentcore/secrets.key` on
machines without a keychain). Copying or syncing the project directory does **not** expose the secrets — only the holder
of the machine key can decrypt them. Reference IDs (API key ID, app ID) remain readable.

To rotate a credential, re-run `agentcore add payment-connector` with the new values (this re-encrypts immediately).
Editing `agentcore/.env.local` by hand still works: paste the new plaintext value and run `agentcore deploy` — the CLI
re-encrypts it on the next write.

### Credential Rotation

To rotate credentials:
The preferred rotation path is to re-run `agentcore add payment-connector` with the new values — the CLI re-encrypts the
secrets immediately and there is no plaintext window. Hand-editing `.env.local` also works: paste the new plaintext
value and run `agentcore deploy -y` — the CLI re-encrypts on the next write.

1. Update the values in `agentcore/.env.local`
1. Re-run `agentcore add payment-connector` with the new values (preferred), **or** open `agentcore/.env.local`, paste
the new plaintext secret value, and save.
2. Run `agentcore deploy -y`

Deploy automatically updates the PaymentCredentialProvider on AWS with the new secret values.
Deploy automatically updates the PaymentCredentialProvider on AWS with the new (re-encrypted) secret values.

## Deploying with Payments

Expand Down
2 changes: 1 addition & 1 deletion esbuild.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ await esbuild.build({
banner: {
js: `import { createRequire } from 'module'; import { fileURLToPath as __ef } from 'url'; import { dirname as __ed } from 'path'; const require = createRequire(import.meta.url); const __filename = __ef(import.meta.url); const __dirname = __ed(__filename);`,
},
external: ['fsevents', '@aws-cdk/toolkit-lib'],
external: ['fsevents', '@aws-cdk/toolkit-lib', '@napi-rs/keyring'],
plugins: [optionalDepsPlugin, textLoaderPlugin],
});

Expand Down
7 changes: 5 additions & 2 deletions integ-tests/add-agent-auth.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { readEnvFile } from '../src/lib/utils/env.js';
import { createTestProject, readProjectConfig, runCLI } from '../src/test-utils/index.js';
import type { TestProject } from '../src/test-utils/index.js';
import { readFile } from 'node:fs/promises';
Expand Down Expand Up @@ -119,11 +120,13 @@ describe('integration: add BYO agent with CUSTOM_JWT auth', () => {
expect(oauthCred!.authorizerType).toBe('OAuthCredentialProvider');
expect((oauthCred as { managed?: boolean }).managed).toBe(true);

// Verify .env.local has client secrets (namespaced per credential)
// Client ID is a reference (plaintext); the client SECRET is encrypted at rest.
const envPath = join(project.projectPath, 'agentcore', '.env.local');
const envContent = await readFile(envPath, 'utf-8');
expect(envContent).toContain('my-client-id');
expect(envContent).toContain('my-client-secret');
expect(envContent).not.toContain('my-client-secret'); // encrypted at rest
const env = await readEnvFile(join(project.projectPath, 'agentcore'));
expect(env.AGENTCORE_CREDENTIAL_AUTHAGENT2_OAUTH_CLIENT_SECRET).toBe('my-client-secret');
});

it('adds a BYO agent with default AWS_IAM auth (no auth flags)', async () => {
Expand Down
223 changes: 223 additions & 0 deletions npm-shrinkwrap.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@
"yaml": "^2.8.3",
"zod": "^4.3.5"
},
"optionalDependencies": {
"@napi-rs/keyring": "^1.3.0"
},
"peerDependencies": {
"aws-cdk-lib": "^2.258.0",
"constructs": "^10.0.0"
Expand Down
28 changes: 19 additions & 9 deletions src/cli/commands/add/__tests__/multi-agent-credentials.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { readEnvFile } from '../../../../lib/utils/env';
import { runCLI } from '../../../../test-utils/index.js';
import { randomUUID } from 'node:crypto';
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
Expand Down Expand Up @@ -47,6 +48,10 @@ describe('multi-agent credential behavior', () => {
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Integ tests pollute the developer's real keychain / ~/.agentcore. runCLI inherits process.env, and these tests don't set AGENTCORE_DISABLE_KEYCHAIN=1 or AGENTCORE_CONFIG_DIR. On a dev/CI machine with a keychain (macOS, Linux with libsecret), the test creates/uses the aws-agentcore / env-local-secret-key entry in the user's keychain; without a keychain, it writes to ~/.agentcore/secrets.key. That's a real side effect from npm run test:integ and (more concerning) means once the developer's keychain has a key from a previous run, these tests will validate using that key rather than a fresh one — cross-run state leakage.

Same issue in integ-tests/add-agent-auth.test.ts (the test added on line 5/120-129 of that file).

Fix: set AGENTCORE_DISABLE_KEYCHAIN=1 and AGENTCORE_CONFIG_DIR=<tmpdir> in the spawned env (via runCLI(..., { env: { ... } }) and createTestProject/beforeAll).

}

async function readEnvDecrypted() {
return readEnvFile(join(projectDir, 'agentcore'));
}

describe('credential reuse with same API key', () => {
it('first agent creates project-scoped credential', async () => {
const result = await runCLI(
Expand Down Expand Up @@ -76,9 +81,11 @@ describe('multi-agent credential behavior', () => {
expect(spec.credentials).toHaveLength(1);
expect(spec.credentials[0].name).toBe(`${projectName}Gemini`);

const env = await readEnvLocal();
expect(env).toContain('AGENTCORE_CREDENTIAL_MULTIAGENTPROJGEMINI=');
expect(env).toContain('KEY1');
const rawEnv = await readEnvLocal();
expect(rawEnv).toContain('AGENTCORE_CREDENTIAL_MULTIAGENTPROJGEMINI=');
expect(rawEnv).not.toContain('KEY1'); // encrypted at rest
const env = await readEnvDecrypted();
expect(env.AGENTCORE_CREDENTIAL_MULTIAGENTPROJGEMINI).toBe('KEY1');
});

it('second agent with same key reuses credential (no duplicate)', async () => {
Expand Down Expand Up @@ -151,12 +158,15 @@ describe('multi-agent credential behavior', () => {
// Should have 3 agents
expect(spec.runtimes).toHaveLength(3);

// .env.local should have both keys
const env = await readEnvLocal();
expect(env).toContain('AGENTCORE_CREDENTIAL_MULTIAGENTPROJGEMINI=');
expect(env).toContain('KEY1');
expect(env).toContain('AGENTCORE_CREDENTIAL_MULTIAGENTPROJAGENT3GEMINI=');
expect(env).toContain('KEY2');
// .env.local should have both keys encrypted at rest, decryptable to original values
const rawEnv = await readEnvLocal();
expect(rawEnv).toContain('AGENTCORE_CREDENTIAL_MULTIAGENTPROJGEMINI=');
expect(rawEnv).toContain('AGENTCORE_CREDENTIAL_MULTIAGENTPROJAGENT3GEMINI=');
expect(rawEnv).not.toContain('KEY1'); // encrypted at rest
expect(rawEnv).not.toContain('KEY2'); // encrypted at rest
const env = await readEnvDecrypted();
expect(env.AGENTCORE_CREDENTIAL_MULTIAGENTPROJGEMINI).toBe('KEY1');
expect(env.AGENTCORE_CREDENTIAL_MULTIAGENTPROJAGENT3GEMINI).toBe('KEY2');

// Generated code should reference correct credentials
const agent1Code = await readFile(join(projectDir, 'app/Agent1/model/load.py'), 'utf-8');
Expand Down
15 changes: 15 additions & 0 deletions src/cli/primitives/PaymentConnectorPrimitive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { findConfigRoot, removeEnvVars, setEnvVar, toError } from '../../lib';
import type { AgentCoreProjectSpec, PaymentProvider } from '../../schema';
import { PaymentConnectorNameSchema, PaymentConnectorSchema, PaymentProviderSchema } from '../../schema';
import type { RemoveResult } from '../commands/remove/types';
import { ANSI } from '../constants';
import { getErrorMessage } from '../errors';
import type { RemovalPreview, SchemaChange } from '../operations/remove/types';
import { requireTTY } from '../tui/guards/tty';
Expand Down Expand Up @@ -447,6 +448,20 @@ export class PaymentConnectorPrimitive extends BasePrimitive<AddPaymentConnector
if (walletSecretResult !== true) failValidation(walletSecretResult);
}

const usedLiteralSecretFlag = [
cliOptions.apiKeySecret,
cliOptions.walletSecret,
cliOptions.appSecret,
cliOptions.authorizationPrivateKey,
].some(v => v !== undefined);
if (usedLiteralSecretFlag && !cliOptions.json) {
process.stderr.write(
`${ANSI.yellow}Warning: passing secrets as CLI flags exposes them to shell history and the ` +
`process table. Prefer interactive mode (run \`agentcore add payment-connector\` with no ` +
`secret flags) for masked entry.${ANSI.reset}\n`
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CLI-flag leak warning only fires for add payment-connector. The same hazard applies to add agent --api-key … and add agent --client-secret … (both also persist secrets to .env.local and both currently accept secret values via positional flags in non-interactive mode). Either:

  1. Extract this warning into a shared helper (e.g., warnOnLiteralSecretFlag(flagNames: string[]) in cli/primitives/) and call it from AgentPrimitive / CredentialPrimitive / auth-utils wherever a secret flag is consumed; or
  2. Document why the warning is payment-only (e.g., "agent api-keys are typed differently" — but I don't see a reason).

Given the PR scope says "Covers the whole CLI uniformly … not just payments," option 1 fits the stated goal.

}

let result: Awaited<ReturnType<typeof this.add>>;
if (provider === 'StripePrivy') {
result = await this.add({
Expand Down
2 changes: 2 additions & 0 deletions src/cli/telemetry/schemas/common-shapes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ export const ErrorName = z.enum([
'PollExhaustedError',
'PollTimeoutError',
'ResourceNotFoundError',
'SecretDecryptionError',
'SecretEncryptionError',
'ServiceError',
'ServiceQuotaError',
'ShellKickedError',
Expand Down
Loading
Loading