Skip to content

Commit c31e6d7

Browse files
committed
fix(payments): blockers from deep review — gate completeness, secrets, regex, recovery
- F-01-1: gate payment env-var injection + IAM grants by language (Python) and protocol (HTTP). New helper isPaymentEligibleRuntime is used in PaymentManagerPrimitive.add, the vended cdk-stack.ts payment loop, and dev/payment-env.ts so non-Python or non-HTTP runtimes never get env vars they cannot consume. - F-01-2: emit hooks=[ConfigBundleHook()] alongside plugins= in both the template and the regex-emitted Agent block. Prevents existing config-bundle customers from silently losing system-prompt injection when adding payments. - R-13-1: insert the payment import at the top of main.py (after the file docstring and any from __future__ imports). Removes a regex that could splice the new import inside a parenthesised multi-line from x import (...) block and produce a SyntaxError. - R-13-2: tighten the agent-replacement regexes — allow trailing # type: ignore comments, allow typed-annotation _agent: Agent | None = None form, and abort cleanly when the call site is replaced but the singleton has an unrecognised shape rather than shipping corrupted code. - S-02-1: re-add after remove now correctly patches main.py. The previous early-return short-circuited the whole flow when remove() left payments.py behind. - D-12-2: when CFN succeeds but the post-deploy state-write fails, surface the stack name + region + recovery commands so the user can fix the local I/O issue or manually delete the orphan stack. - C-05-3: exclude .env / .env.local / .env.* files from the deploy zip at any depth. Closes a footgun for BYO projects with --code-location . where agentcore/.env.local could otherwise be packaged into S3. Tests: 8 new cases covering paren-aware imports, docstring + future, type-ignore comments, typed annotations, abort path, re-add cycle, and .env exclusion at any depth.
1 parent b192863 commit c31e6d7

14 files changed

Lines changed: 578 additions & 62 deletions

File tree

src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,22 @@ function toCdkId(name: string): string {
326326
return name.replace(/_/g, '');
327327
}
328328
329+
/**
330+
* Decide whether a deployed runtime should receive payment env vars + IAM grants.
331+
* Payments today only ships a runtime shim for Python HTTP runtimes; injecting
332+
* AGENTCORE_PAYMENT_* env vars into TypeScript / MCP / A2A / AGUI runtimes
333+
* would surface env vars they cannot consume and would dilute least-privilege
334+
* IAM grants for runtimes that never call ProcessPayment.
335+
*/
336+
function isPaymentEligibleAgent(agent: { entrypoint?: string; protocol?: string }): boolean {
337+
if (agent.protocol && agent.protocol !== 'HTTP') {
338+
return false;
339+
}
340+
const entrypoint = typeof agent.entrypoint === 'string' ? agent.entrypoint : '';
341+
const entrypointFile = entrypoint.split(':')[0] ?? '';
342+
return entrypointFile.endsWith('.py');
343+
}
344+
329345
/**
330346
* CDK Stack that deploys AgentCore infrastructure.
331347
*
@@ -372,8 +388,14 @@ export class AgentCoreStack extends Stack {
372388
373389
const prefix = \`AGENTCORE_PAYMENT_\${payment.name.toUpperCase().replace(/-/g, '_')}\`;
374390
375-
// Wire env vars from construct output tokens into all agent environments
391+
// Wire env vars from construct output tokens into eligible agent environments only.
392+
// See isPaymentEligibleAgent — non-Python or non-HTTP runtimes have no shim that
393+
// can consume these env vars, and giving them sts:AssumeRole on the
394+
// ProcessPaymentRole would broaden the privilege surface unnecessarily.
376395
for (const env of this.application.environments.values()) {
396+
if (!isPaymentEligibleAgent(env.agent)) {
397+
continue;
398+
}
377399
env.runtime.addEnvironmentVariable(\`\${prefix}_MANAGER_ARN\`, manager.paymentManagerArn);
378400
env.runtime.addEnvironmentVariable(\`\${prefix}_PROCESS_PAYMENT_ROLE_ARN\`, manager.processPaymentRoleArn);
379401
@@ -412,9 +434,10 @@ export class AgentCoreStack extends Stack {
412434
credentialProviderArn: connector.credentialProviderArn,
413435
});
414436
415-
// Wire first connector's ID as env var
437+
// Wire first connector's ID as env var (eligible agents only)
416438
if (connector === payment.connectors[0]) {
417439
for (const env of this.application.environments.values()) {
440+
if (!isPaymentEligibleAgent(env.agent)) continue;
418441
env.runtime.addEnvironmentVariable(\`\${prefix}_CONNECTOR_ID\`, conn.paymentConnectorId);
419442
}
420443
}
@@ -5003,7 +5026,8 @@ async def invoke(payload, context):
50035026
session_manager=get_memory_session_manager(mem_session_id, mem_user_id),
50045027
system_prompt=DEFAULT_SYSTEM_PROMPT + PAYMENT_SYSTEM_PROMPT,
50055028
tools=tools,
5006-
plugins=plugins,
5029+
plugins=plugins,{{#if hasConfigBundle}}
5030+
hooks=[ConfigBundleHook()],{{/if}}
50075031
)
50085032
{{else}}
50095033
session_id = getattr(context, 'session_id', 'default-session')
@@ -5016,7 +5040,8 @@ async def invoke(payload, context):
50165040
model=load_model(),
50175041
system_prompt=DEFAULT_SYSTEM_PROMPT + PAYMENT_SYSTEM_PROMPT,
50185042
tools=tools,
5019-
plugins=plugins,
5043+
plugins=plugins,{{#if hasConfigBundle}}
5044+
hooks=[ConfigBundleHook()],{{/if}}
50205045
)
50215046
{{else}}
50225047
{{#if hasConfigBundle}}

src/assets/cdk/lib/cdk-stack.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,22 @@ function toCdkId(name: string): string {
5151
return name.replace(/_/g, '');
5252
}
5353

54+
/**
55+
* Decide whether a deployed runtime should receive payment env vars + IAM grants.
56+
* Payments today only ships a runtime shim for Python HTTP runtimes; injecting
57+
* AGENTCORE_PAYMENT_* env vars into TypeScript / MCP / A2A / AGUI runtimes
58+
* would surface env vars they cannot consume and would dilute least-privilege
59+
* IAM grants for runtimes that never call ProcessPayment.
60+
*/
61+
function isPaymentEligibleAgent(agent: { entrypoint?: string; protocol?: string }): boolean {
62+
if (agent.protocol && agent.protocol !== 'HTTP') {
63+
return false;
64+
}
65+
const entrypoint = typeof agent.entrypoint === 'string' ? agent.entrypoint : '';
66+
const entrypointFile = entrypoint.split(':')[0] ?? '';
67+
return entrypointFile.endsWith('.py');
68+
}
69+
5470
/**
5571
* CDK Stack that deploys AgentCore infrastructure.
5672
*
@@ -97,8 +113,14 @@ export class AgentCoreStack extends Stack {
97113

98114
const prefix = `AGENTCORE_PAYMENT_${payment.name.toUpperCase().replace(/-/g, '_')}`;
99115

100-
// Wire env vars from construct output tokens into all agent environments
116+
// Wire env vars from construct output tokens into eligible agent environments only.
117+
// See isPaymentEligibleAgent — non-Python or non-HTTP runtimes have no shim that
118+
// can consume these env vars, and giving them sts:AssumeRole on the
119+
// ProcessPaymentRole would broaden the privilege surface unnecessarily.
101120
for (const env of this.application.environments.values()) {
121+
if (!isPaymentEligibleAgent(env.agent)) {
122+
continue;
123+
}
102124
env.runtime.addEnvironmentVariable(`${prefix}_MANAGER_ARN`, manager.paymentManagerArn);
103125
env.runtime.addEnvironmentVariable(`${prefix}_PROCESS_PAYMENT_ROLE_ARN`, manager.processPaymentRoleArn);
104126

@@ -137,9 +159,10 @@ export class AgentCoreStack extends Stack {
137159
credentialProviderArn: connector.credentialProviderArn,
138160
});
139161

140-
// Wire first connector's ID as env var
162+
// Wire first connector's ID as env var (eligible agents only)
141163
if (connector === payment.connectors[0]) {
142164
for (const env of this.application.environments.values()) {
165+
if (!isPaymentEligibleAgent(env.agent)) continue;
143166
env.runtime.addEnvironmentVariable(`${prefix}_CONNECTOR_ID`, conn.paymentConnectorId);
144167
}
145168
}

src/assets/python/http/strands/base/main.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,8 @@ async def invoke(payload, context):
215215
session_manager=get_memory_session_manager(mem_session_id, mem_user_id),
216216
system_prompt=DEFAULT_SYSTEM_PROMPT + PAYMENT_SYSTEM_PROMPT,
217217
tools=tools,
218-
plugins=plugins,
218+
plugins=plugins,{{#if hasConfigBundle}}
219+
hooks=[ConfigBundleHook()],{{/if}}
219220
)
220221
{{else}}
221222
session_id = getattr(context, 'session_id', 'default-session')
@@ -228,7 +229,8 @@ async def invoke(payload, context):
228229
model=load_model(),
229230
system_prompt=DEFAULT_SYSTEM_PROMPT + PAYMENT_SYSTEM_PROMPT,
230231
tools=tools,
231-
plugins=plugins,
232+
plugins=plugins,{{#if hasConfigBundle}}
233+
hooks=[ConfigBundleHook()],{{/if}}
232234
)
233235
{{else}}
234236
{{#if hasConfigBundle}}

src/cli/commands/deploy/actions.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -558,7 +558,25 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise<Dep
558558
runtimeEndpoints,
559559
payments,
560560
});
561-
await configIO.writeDeployedState(deployedState);
561+
// CFN succeeded by this point — failing to persist deployed-state.json
562+
// would leave AWS resources without a local pointer for teardown. Surface
563+
// a recovery message that names the stack + region so the user can
564+
// either teardown manually or re-run `agentcore status` once the local
565+
// I/O issue is resolved.
566+
try {
567+
await configIO.writeDeployedState(deployedState);
568+
} catch (writeErr) {
569+
const msg = writeErr instanceof Error ? writeErr.message : String(writeErr);
570+
logger.log(
571+
`WARNING: deploy succeeded but writing agentcore/deployed-state.json failed: ${msg}.\n` +
572+
` Stack: ${stackName} (region: ${target.region})\n` +
573+
` AWS resources are running but the CLI cannot track them locally.\n` +
574+
` Recovery options:\n` +
575+
` 1) Fix the local I/O problem (disk space / permissions on agentcore/) and run \`agentcore status\` to refresh.\n` +
576+
` 2) If you want to teardown what was deployed: \`aws cloudformation delete-stack --stack-name ${stackName} --region ${target.region}\``
577+
);
578+
throw writeErr;
579+
}
562580

563581
// Show gateway URLs and target sync status
564582
if (Object.keys(gateways).length > 0) {

src/cli/commands/dev/browser-mode.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,11 @@ export async function runBrowserMode(opts: BrowserModeOptions): Promise<void> {
134134
const { workingDir, project, agentName, otelEnvVars = {}, collector } = opts;
135135

136136
const configRoot = findConfigRoot(workingDir);
137+
// Browser mode serves multiple agents; we don't know which agent will be
138+
// launched until the user picks one in the web UI. Pass no runtime so
139+
// payment env vars are emitted for any deployed manager and the spawned
140+
// agent decides whether to consume them. Single-agent CLI dev correctly
141+
// narrows by runtime via loadDevEnv(workingDir, runtime) elsewhere.
137142
const { envVars } = await loadDevEnv(workingDir);
138143

139144
const supportedAgents = getDevSupportedAgents(project);

src/cli/commands/dev/command.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,8 @@ export const registerDev = (program: Command) => {
341341
}
342342

343343
const agentName = opts.runtime ?? project.runtimes[0]?.name;
344-
const { envVars } = await loadDevEnv(workingDir);
344+
const selectedRuntime = project.runtimes.find(r => r.name === agentName);
345+
const { envVars } = await loadDevEnv(workingDir, selectedRuntime);
345346
const mergedEnvVars = { ...envVars, ...otelEnvVars };
346347
const config = getDevConfig(workingDir, project, configRoot ?? undefined, agentName);
347348

src/cli/operations/dev/load-dev-env.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { findConfigRoot, readEnvFile } from '../../../lib';
2+
import type { AgentEnvSpec } from '../../../schema';
23
import { getGatewayEnvVars } from './gateway-env.js';
34
import { getMemoryEnvVars } from './memory-env.js';
45
import { getPaymentEnvVars } from './payment-env.js';
@@ -13,13 +14,16 @@ export interface DevEnv {
1314
/**
1415
* Load all dev-mode environment variables: deployed-state gateway/memory/payment env vars
1516
* merged with the user's .env file. Deployed-state vars go first so .env can override.
17+
*
18+
* @param runtime The runtime being launched. When provided, payment env vars
19+
* are only injected for runtimes that can consume them (Python HTTP today).
1620
*/
17-
export async function loadDevEnv(workingDir: string): Promise<DevEnv> {
21+
export async function loadDevEnv(workingDir: string, runtime?: AgentEnvSpec): Promise<DevEnv> {
1822
const configRoot = findConfigRoot(workingDir);
1923
const dotEnvVars = configRoot ? await readEnvFile(configRoot) : {};
2024
const gatewayEnvVars = await getGatewayEnvVars();
2125
const memoryEnvVars = await getMemoryEnvVars();
22-
const paymentEnvVars = await getPaymentEnvVars();
26+
const paymentEnvVars = await getPaymentEnvVars(runtime);
2327

2428
return {
2529
envVars: { ...gatewayEnvVars, ...memoryEnvVars, ...paymentEnvVars, ...dotEnvVars },

src/cli/operations/dev/payment-env.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,21 @@
11
import { ConfigIO } from '../../../lib/index.js';
2+
import type { AgentEnvSpec } from '../../../schema';
3+
import { isPaymentEligibleRuntime } from '../../primitives/payment-eligible.js';
4+
5+
/**
6+
* Build payment env vars for a dev runtime. Mirrors the CDK stack's deploy-time
7+
* injection but reads from `deployed-state.json` (so payments only "activate"
8+
* locally once the project has been deployed and state is populated).
9+
*
10+
* @param runtime The agent runtime spec being launched. When provided and the
11+
* runtime is not eligible for payments (non-Python, non-HTTP), an empty map
12+
* is returned — matches the CDK behaviour of skipping ineligible runtimes.
13+
*/
14+
export async function getPaymentEnvVars(runtime?: AgentEnvSpec): Promise<Record<string, string>> {
15+
if (runtime && !isPaymentEligibleRuntime(runtime)) {
16+
return {};
17+
}
218

3-
export async function getPaymentEnvVars(): Promise<Record<string, string>> {
419
const configIO = new ConfigIO();
520
const envVars: Record<string, string> = {};
621

0 commit comments

Comments
 (0)