@aws/agentcore 1.0.0-preview.11 → 1.0.0-preview.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/dist/agent-inspector/index.css +1 -1
  2. package/dist/agent-inspector/index.js +83 -78
  3. package/dist/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap +860 -158
  4. package/dist/assets/cdk/bin/cdk.ts +53 -5
  5. package/dist/assets/cdk/lib/cdk-stack.ts +164 -1
  6. package/dist/assets/cdk/package.json +1 -1
  7. package/dist/assets/cdk/test/cdk.test.ts +2 -0
  8. package/dist/assets/python/a2a/googleadk/base/main.py +24 -16
  9. package/dist/assets/python/a2a/langchain_langgraph/base/main.py +24 -16
  10. package/dist/assets/python/a2a/langchain_langgraph/base/pyproject.toml +1 -1
  11. package/dist/assets/python/a2a/strands/base/main.py +23 -17
  12. package/dist/assets/python/agui/googleadk/base/main.py +62 -2
  13. package/dist/assets/python/agui/googleadk/base/pyproject.toml +1 -1
  14. package/dist/assets/python/agui/langchain_langgraph/base/main.py +56 -0
  15. package/dist/assets/python/agui/langchain_langgraph/base/pyproject.toml +1 -1
  16. package/dist/assets/python/agui/strands/base/main.py +63 -1
  17. package/dist/assets/python/http/autogen/base/main.py +27 -19
  18. package/dist/assets/python/http/autogen/base/pyproject.toml +1 -1
  19. package/dist/assets/python/http/googleadk/base/main.py +24 -16
  20. package/dist/assets/python/http/langchain_langgraph/base/main.py +24 -16
  21. package/dist/assets/python/http/langchain_langgraph/base/pyproject.toml +1 -1
  22. package/dist/assets/python/http/openaiagents/base/main.py +24 -16
  23. package/dist/assets/python/http/openaiagents/base/pyproject.toml +1 -1
  24. package/dist/assets/python/http/strands/base/main.py +62 -19
  25. package/dist/assets/python/http/strands/capabilities/payments/__init__.py +4 -0
  26. package/dist/assets/python/http/strands/capabilities/payments/payments.py +142 -0
  27. package/dist/assets/python/mcp/standalone/base/main.py +58 -0
  28. package/dist/cli/index.mjs +772 -660
  29. package/dist/lib/errors/types.d.ts +3 -0
  30. package/dist/lib/errors/types.d.ts.map +1 -1
  31. package/dist/lib/errors/types.js +10 -1
  32. package/dist/lib/errors/types.js.map +1 -1
  33. package/dist/lib/errors/zod.js +1 -1
  34. package/dist/lib/errors/zod.js.map +1 -1
  35. package/dist/lib/packaging/helpers.d.ts.map +1 -1
  36. package/dist/lib/packaging/helpers.js +50 -10
  37. package/dist/lib/packaging/helpers.js.map +1 -1
  38. package/dist/lib/result.d.ts +6 -4
  39. package/dist/lib/result.d.ts.map +1 -1
  40. package/dist/lib/result.js +7 -0
  41. package/dist/lib/result.js.map +1 -1
  42. package/dist/lib/utils/env.d.ts +4 -0
  43. package/dist/lib/utils/env.d.ts.map +1 -1
  44. package/dist/lib/utils/env.js +18 -0
  45. package/dist/lib/utils/env.js.map +1 -1
  46. package/dist/lib/utils/index.d.ts +1 -1
  47. package/dist/lib/utils/index.d.ts.map +1 -1
  48. package/dist/lib/utils/index.js +2 -1
  49. package/dist/lib/utils/index.js.map +1 -1
  50. package/dist/schema/schemas/agent-env.d.ts +50 -4
  51. package/dist/schema/schemas/agent-env.d.ts.map +1 -1
  52. package/dist/schema/schemas/agent-env.js +102 -4
  53. package/dist/schema/schemas/agent-env.js.map +1 -1
  54. package/dist/schema/schemas/agentcore-project.d.ts +69 -4
  55. package/dist/schema/schemas/agentcore-project.d.ts.map +1 -1
  56. package/dist/schema/schemas/agentcore-project.js +61 -5
  57. package/dist/schema/schemas/agentcore-project.js.map +1 -1
  58. package/dist/schema/schemas/deployed-state.d.ts +97 -0
  59. package/dist/schema/schemas/deployed-state.d.ts.map +1 -1
  60. package/dist/schema/schemas/deployed-state.js +24 -4
  61. package/dist/schema/schemas/deployed-state.js.map +1 -1
  62. package/dist/schema/schemas/primitives/harness.d.ts +41 -0
  63. package/dist/schema/schemas/primitives/harness.d.ts.map +1 -1
  64. package/dist/schema/schemas/primitives/harness.js +65 -1
  65. package/dist/schema/schemas/primitives/harness.js.map +1 -1
  66. package/dist/schema/schemas/primitives/index.d.ts +4 -2
  67. package/dist/schema/schemas/primitives/index.d.ts.map +1 -1
  68. package/dist/schema/schemas/primitives/index.js +14 -2
  69. package/dist/schema/schemas/primitives/index.js.map +1 -1
  70. package/dist/schema/schemas/primitives/payment.d.ts +54 -0
  71. package/dist/schema/schemas/primitives/payment.d.ts.map +1 -0
  72. package/dist/schema/schemas/primitives/payment.js +79 -0
  73. package/dist/schema/schemas/primitives/payment.js.map +1 -0
  74. package/npm-shrinkwrap.json +1027 -706
  75. package/package.json +13 -7
  76. package/scripts/run-e2e-local.sh +9 -3
@@ -73,12 +73,14 @@ async function main() {
73
73
  const spec = await configIO.readProjectSpec();
74
74
  const targets = await configIO.readAWSDeploymentTargets();
75
75
 
76
- // Extract MCP configuration from project spec.
77
- // Gateway fields are stored in agentcore.json but may not yet be on the
78
- // AgentCoreProjectSpec type from @aws/agentcore-cdk, so we read them
79
- // dynamically and cast the resulting object.
76
+ // The vended CDK project compiles against the published @aws/agentcore-cdk
77
+ // schema type, which may lag the CLI's own AgentCoreProjectSpec (e.g. payments,
78
+ // harnesses, gateway fields). Cast once so those fields are reachable.
80
79
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
81
80
  const specAny = spec as any;
81
+
82
+ // Extract MCP configuration from project spec.
83
+ // Gateway fields are stored in agentcore.json but may not yet be on the
82
84
  const mcpSpec = specAny.agentCoreGateways?.length
83
85
  ? {
84
86
  agentCoreGateways: specAny.agentCoreGateways,
@@ -111,6 +113,9 @@ async function main() {
111
113
  codeLocation?: string;
112
114
  tools?: { type: string; name: string }[];
113
115
  apiKeyArn?: string;
116
+ efsAccessPoints?: { accessPointArn: string; mountPath: string }[];
117
+ s3AccessPoints?: { accessPointArn: string; mountPath: string }[];
118
+ apiFormat?: 'converse_stream' | 'responses' | 'chat_completions';
114
119
  }[] = [];
115
120
  for (const entry of specAny.harnesses ?? []) {
116
121
  const harnessDir = path.resolve(projectRoot, entry.path);
@@ -127,6 +132,9 @@ async function main() {
127
132
  codeLocation: harnessSpec.dockerfile ? harnessDir : undefined,
128
133
  tools: harnessSpec.tools,
129
134
  apiKeyArn: harnessSpec.model?.apiKeyArn,
135
+ efsAccessPoints: harnessSpec.efsAccessPoints,
136
+ s3AccessPoints: harnessSpec.s3AccessPoints,
137
+ apiFormat: harnessSpec.model?.apiFormat,
130
138
  });
131
139
  } catch (err) {
132
140
  throw new Error(
@@ -150,11 +158,51 @@ async function main() {
150
158
  | Record<string, { credentialProviderArn: string; clientSecretArn?: string }>
151
159
  | undefined;
152
160
 
161
+ // Payment credential provider ARNs live in the same credentials map as identity credentials
162
+ const paymentCredentials = credentials;
163
+
164
+ const paymentSpec = specAny.payments?.length
165
+ ? specAny.payments.map(
166
+ (p: {
167
+ name: string;
168
+ description?: string;
169
+ authorizerType: 'AWS_IAM' | 'CUSTOM_JWT';
170
+ authorizerConfiguration?: unknown;
171
+ autoPayment?: boolean;
172
+ paymentToolAllowlist?: string[];
173
+ networkPreferences?: string[];
174
+ connectors: { name: string; provider?: string; credentialName: string }[];
175
+ }) => ({
176
+ name: p.name,
177
+ description: p.description,
178
+ authorizerType: p.authorizerType,
179
+ authorizerConfiguration: p.authorizerConfiguration,
180
+ autoPayment: p.autoPayment,
181
+ paymentToolAllowlist: p.paymentToolAllowlist,
182
+ networkPreferences: p.networkPreferences,
183
+ connectors: p.connectors.map(c => {
184
+ const credentialProviderArn = paymentCredentials?.[c.credentialName]?.credentialProviderArn;
185
+ if (!credentialProviderArn) {
186
+ // Fail fast with an actionable message rather than passing an empty
187
+ // ARN that fails opaquely server-side at CreatePaymentConnector.
188
+ throw new Error(
189
+ \`Payment connector "\${c.name}" on manager "\${p.name}" references credential \` +
190
+ \`"\${c.credentialName}", but no deployed credential provider was found for it. \` +
191
+ \`Run \\\`agentcore deploy\\\` so the credential provider is created first.\`
192
+ );
193
+ }
194
+ return { name: c.name, provider: c.provider, credentialProviderArn };
195
+ }),
196
+ })
197
+ )
198
+ : undefined;
199
+
153
200
  new AgentCoreStack(app, stackName, {
154
201
  spec,
155
202
  mcpSpec,
156
203
  credentials,
157
204
  harnesses: harnessConfigs.length > 0 ? harnessConfigs : undefined,
205
+ paymentSpec,
158
206
  env,
159
207
  description: \`AgentCore stack for \${spec.name} deployed to \${target.name} (\${target.region})\`,
160
208
  tags: {
@@ -169,7 +217,7 @@ async function main() {
169
217
 
170
218
  main().catch((error: unknown) => {
171
219
  console.error('AgentCore CDK synthesis failed:', error instanceof Error ? error.message : error);
172
- process.exitCode = 1;
220
+ process.exit(1);
173
221
  });
174
222
  "
175
223
  `;
@@ -296,10 +344,14 @@ exports[`Assets Directory Snapshots > CDK assets > cdk/cdk/lib/cdk-stack.ts shou
296
344
  "import {
297
345
  AgentCoreApplication,
298
346
  AgentCoreMcp,
347
+ AgentCorePaymentManager,
348
+ AgentCorePaymentConnector,
299
349
  type AgentCoreProjectSpec,
300
350
  type AgentCoreMcpSpec,
351
+ type CustomJWTAuthorizerConfig,
301
352
  } from '@aws/agentcore-cdk';
302
353
  import { CfnOutput, Stack, type StackProps } from 'aws-cdk-lib';
354
+ import * as iam from 'aws-cdk-lib/aws-iam';
303
355
  import { Construct } from 'constructs';
304
356
 
305
357
  export interface HarnessConfig {
@@ -312,6 +364,25 @@ export interface HarnessConfig {
312
364
  codeLocation?: string;
313
365
  tools?: { type: string; name: string }[];
314
366
  apiKeyArn?: string;
367
+ efsAccessPoints?: { accessPointArn: string; mountPath: string }[];
368
+ s3AccessPoints?: { accessPointArn: string; mountPath: string }[];
369
+ }
370
+
371
+ export interface PaymentConnectorSpec {
372
+ name: string;
373
+ provider: 'CoinbaseCDP' | 'StripePrivy';
374
+ credentialProviderArn: string;
375
+ }
376
+
377
+ export interface PaymentSpec {
378
+ name: string;
379
+ description?: string;
380
+ authorizerType: 'AWS_IAM' | 'CUSTOM_JWT';
381
+ authorizerConfiguration?: { customJWTAuthorizer: CustomJWTAuthorizerConfig };
382
+ autoPayment?: boolean;
383
+ paymentToolAllowlist?: string[];
384
+ networkPreferences?: string[];
385
+ connectors: PaymentConnectorSpec[];
315
386
  }
316
387
 
317
388
  export interface AgentCoreStackProps extends StackProps {
@@ -331,6 +402,30 @@ export interface AgentCoreStackProps extends StackProps {
331
402
  * Harness role configurations.
332
403
  */
333
404
  harnesses?: HarnessConfig[];
405
+ /**
406
+ * Payment specifications with resolved credential provider ARNs.
407
+ */
408
+ paymentSpec?: PaymentSpec[];
409
+ }
410
+
411
+ function toCdkId(name: string): string {
412
+ return name.replace(/_/g, '');
413
+ }
414
+
415
+ /**
416
+ * Decide whether a deployed runtime should receive payment env vars + IAM grants.
417
+ * Payments today only ships a runtime shim for Python HTTP runtimes; injecting
418
+ * AGENTCORE_PAYMENT_* env vars into TypeScript / MCP / A2A / AGUI runtimes
419
+ * would surface env vars they cannot consume and would dilute least-privilege
420
+ * IAM grants for runtimes that never call ProcessPayment.
421
+ */
422
+ function isPaymentEligibleAgent(agent: { entrypoint?: string; protocol?: string }): boolean {
423
+ if (agent.protocol && agent.protocol !== 'HTTP') {
424
+ return false;
425
+ }
426
+ const entrypoint = typeof agent.entrypoint === 'string' ? agent.entrypoint : '';
427
+ const entrypointFile = entrypoint.split(':')[0] ?? '';
428
+ return entrypointFile.endsWith('.py');
334
429
  }
335
430
 
336
431
  /**
@@ -346,7 +441,7 @@ export class AgentCoreStack extends Stack {
346
441
  constructor(scope: Construct, id: string, props: AgentCoreStackProps) {
347
442
  super(scope, id, props);
348
443
 
349
- const { spec, mcpSpec, credentials, harnesses } = props;
444
+ const { spec, mcpSpec, credentials, harnesses, paymentSpec } = props;
350
445
 
351
446
  // Create AgentCoreApplication with all agents and harness roles
352
447
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -367,6 +462,122 @@ export class AgentCoreStack extends Stack {
367
462
  });
368
463
  }
369
464
 
465
+ // Create payment infrastructure via CFN constructs
466
+ if (paymentSpec && paymentSpec.length > 0) {
467
+ for (const payment of paymentSpec) {
468
+ const mgrId = toCdkId(payment.name);
469
+ const manager = new AgentCorePaymentManager(this, \`Payment\${mgrId}\`, {
470
+ projectName: spec.name,
471
+ name: payment.name,
472
+ authorizerType: payment.authorizerType,
473
+ description: payment.description,
474
+ authorizerConfiguration: payment.authorizerConfiguration,
475
+ tags: spec.tags,
476
+ });
477
+
478
+ const prefix = \`AGENTCORE_PAYMENT_\${payment.name.toUpperCase().replace(/-/g, '_')}\`;
479
+
480
+ // Wire env vars from construct output tokens into eligible agent environments only.
481
+ // See isPaymentEligibleAgent — non-Python or non-HTTP runtimes have no shim that
482
+ // can consume these env vars, and giving them sts:AssumeRole on the
483
+ // ProcessPaymentRole would broaden the privilege surface unnecessarily.
484
+ for (const env of this.application.environments.values()) {
485
+ if (!isPaymentEligibleAgent(env.agent)) {
486
+ continue;
487
+ }
488
+ env.runtime.addEnvironmentVariable(\`\${prefix}_MANAGER_ARN\`, manager.paymentManagerArn);
489
+ env.runtime.addEnvironmentVariable(\`\${prefix}_PROCESS_PAYMENT_ROLE_ARN\`, manager.processPaymentRoleArn);
490
+
491
+ // Grant runtime execution role permission to assume the ProcessPaymentRole.
492
+ // The ProcessPaymentRole's trust policy allows AccountRootPrincipal, but the
493
+ // caller still needs sts:AssumeRole on its own role to perform the assumption.
494
+ env.runtime.role.addToPrincipalPolicy(
495
+ new iam.PolicyStatement({
496
+ actions: ['sts:AssumeRole'],
497
+ resources: [manager.processPaymentRoleArn],
498
+ })
499
+ );
500
+
501
+ // Grant payment data-plane actions directly to the runtime role.
502
+ //
503
+ // NOTE: This deviates from the canonical role model in the AgentCore Payments
504
+ // beta guide, which assigns Get/List/Create instrument+session actions to a
505
+ // separate ManagementRole and limits the agent's role to ProcessPayment only.
506
+ // The current SDK plugin (AgentCorePaymentsPlugin.generate_payment_header)
507
+ // calls GetPaymentInstrument internally during the 402 auto-pay path, so the
508
+ // runtime role needs read access. CreatePaymentSession is included so
509
+ // \`agentcore invoke --auto-session\` works without a separate ManagementRole
510
+ // call. Tighten this if the SDK is updated to accept pre-fetched instrument
511
+ // details and split create-session into a backend-only flow.
512
+ env.runtime.role.addToPrincipalPolicy(
513
+ new iam.PolicyStatement({
514
+ actions: [
515
+ 'bedrock-agentcore:GetPaymentInstrument',
516
+ 'bedrock-agentcore:ListPaymentInstruments',
517
+ 'bedrock-agentcore:GetPaymentInstrumentBalance',
518
+ 'bedrock-agentcore:GetPaymentSession',
519
+ 'bedrock-agentcore:ListPaymentSessions',
520
+ 'bedrock-agentcore:CreatePaymentSession',
521
+ 'bedrock-agentcore:ProcessPayment',
522
+ ],
523
+ resources: [manager.paymentManagerArn, \`\${manager.paymentManagerArn}/*\`],
524
+ })
525
+ );
526
+
527
+ if (payment.autoPayment !== undefined) {
528
+ env.runtime.addEnvironmentVariable(\`\${prefix}_AUTO_PAYMENT\`, String(payment.autoPayment));
529
+ }
530
+ if (payment.paymentToolAllowlist) {
531
+ env.runtime.addEnvironmentVariable(\`\${prefix}_TOOL_ALLOWLIST\`, payment.paymentToolAllowlist.join(','));
532
+ }
533
+ if (payment.networkPreferences) {
534
+ env.runtime.addEnvironmentVariable(\`\${prefix}_NETWORK_PREFERENCES\`, payment.networkPreferences.join(','));
535
+ }
536
+ if (payment.authorizerType === 'CUSTOM_JWT') {
537
+ env.runtime.addEnvironmentVariable(\`\${prefix}_AUTH_MODE\`, 'bearer');
538
+ }
539
+ }
540
+
541
+ // Create connectors for this manager
542
+ for (const connector of payment.connectors) {
543
+ const connId = toCdkId(connector.name);
544
+ const conn = new AgentCorePaymentConnector(this, \`Payment\${mgrId}\${connId}\`, {
545
+ projectName: spec.name,
546
+ paymentManager: manager,
547
+ connectorName: connector.name,
548
+ connectorType: connector.provider,
549
+ credentialProviderArn: connector.credentialProviderArn,
550
+ });
551
+
552
+ // Wire first connector's ID as env var (eligible agents only)
553
+ if (connector === payment.connectors[0]) {
554
+ for (const env of this.application.environments.values()) {
555
+ if (!isPaymentEligibleAgent(env.agent)) continue;
556
+ env.runtime.addEnvironmentVariable(\`\${prefix}_CONNECTOR_ID\`, conn.paymentConnectorId);
557
+ }
558
+ }
559
+
560
+ new CfnOutput(this, \`Payment\${mgrId}\${connId}ConnectorId\`, {
561
+ value: conn.paymentConnectorId,
562
+ });
563
+ }
564
+
565
+ // CFN Outputs for post-deploy state parsing
566
+ new CfnOutput(this, \`Payment\${mgrId}ManagerArn\`, {
567
+ value: manager.paymentManagerArn,
568
+ });
569
+ new CfnOutput(this, \`Payment\${mgrId}ManagerId\`, {
570
+ value: manager.paymentManagerId,
571
+ });
572
+ new CfnOutput(this, \`Payment\${mgrId}ProcessPaymentRoleArn\`, {
573
+ value: manager.processPaymentRoleArn,
574
+ });
575
+ new CfnOutput(this, \`Payment\${mgrId}ResourceRetrievalRoleArn\`, {
576
+ value: manager.resourceRetrievalRoleArn,
577
+ });
578
+ }
579
+ }
580
+
370
581
  // Stack-level output
371
582
  new CfnOutput(this, 'StackNameOutput', {
372
583
  description: 'Name of the CloudFormation Stack',
@@ -408,7 +619,7 @@ exports[`Assets Directory Snapshots > CDK assets > cdk/cdk/package.json should m
408
619
  "@types/node": "^24.10.1",
409
620
  "jest": "^29.7.0",
410
621
  "ts-jest": "^29.2.5",
411
- "aws-cdk": "2.1100.1",
622
+ "aws-cdk": "2.1126.0",
412
623
  "prettier": "^3.4.2",
413
624
  "typescript": "~5.9.3"
414
625
  },
@@ -439,6 +650,8 @@ test('AgentCoreStack synthesizes with empty spec', () => {
439
650
  evaluators: [],
440
651
  onlineEvalConfigs: [],
441
652
  policyEngines: [],
653
+ payments: [],
654
+ configBundles: [],
442
655
  agentCoreGateways: [],
443
656
  mcpRuntimeTools: [],
444
657
  unassignedTargets: [],
@@ -599,6 +812,8 @@ exports[`Assets Directory Snapshots > File listing > should match the expected f
599
812
  "python/http/strands/base/pyproject.toml",
600
813
  "python/http/strands/capabilities/memory/__init__.py",
601
814
  "python/http/strands/capabilities/memory/session.py",
815
+ "python/http/strands/capabilities/payments/__init__.py",
816
+ "python/http/strands/capabilities/payments/payments.py",
602
817
  "python/mcp/standalone/base/README.md",
603
818
  "python/mcp/standalone/base/gitignore.template",
604
819
  "python/mcp/standalone/base/main.py",
@@ -1063,7 +1278,9 @@ Thumbs.db
1063
1278
  `;
1064
1279
 
1065
1280
  exports[`Assets Directory Snapshots > Python framework assets > python/python/a2a/googleadk/base/main.py should match snapshot 1`] = `
1066
- "import os
1281
+ "{{#if needsOs}}
1282
+ import os
1283
+ {{/if}}
1067
1284
  from google.adk.agents import Agent
1068
1285
  from google.adk.a2a.executor.a2a_agent_executor import A2aAgentExecutor
1069
1286
  from google.adk.runners import Runner
@@ -1082,18 +1299,21 @@ def add_numbers(a: int, b: int) -> int:
1082
1299
 
1083
1300
  tools = [add_numbers]
1084
1301
 
1085
- {{#if sessionStorageMountPath}}
1086
- SESSION_STORAGE_PATH = "{{sessionStorageMountPath}}"
1302
+ {{#if needsOs}}
1303
+ _MOUNT_PATHS = [
1304
+ {{#if sessionStorageMountPath}}"{{sessionStorageMountPath}}",{{/if}}
1305
+ {{#each efsMounts}}"{{mountPath}}",{{/each}}
1306
+ {{#each s3Mounts}}"{{mountPath}}",{{/each}}
1307
+ ]
1087
1308
 
1088
1309
  def _safe_resolve(path: str) -> str:
1089
- """Resolve path safely within the storage boundary."""
1090
- resolved = os.path.realpath(os.path.join(SESSION_STORAGE_PATH, path.lstrip("/")))
1091
- if not resolved.startswith(os.path.realpath(SESSION_STORAGE_PATH)):
1092
- raise ValueError(f"Path '{path}' is outside the storage boundary")
1310
+ resolved = os.path.realpath(path)
1311
+ if not any(resolved == os.path.realpath(m) or resolved.startswith(os.path.realpath(m) + os.sep) for m in _MOUNT_PATHS):
1312
+ raise ValueError(f"Path '{path}' is not within any configured mount ({', '.join(_MOUNT_PATHS)})")
1093
1313
  return resolved
1094
1314
 
1095
1315
  def file_read(path: str) -> str:
1096
- """Read a file from persistent storage. The path is relative to the storage root."""
1316
+ """Read a file from a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
1097
1317
  try:
1098
1318
  full_path = _safe_resolve(path)
1099
1319
  with open(full_path) as f:
@@ -1104,7 +1324,7 @@ def file_read(path: str) -> str:
1104
1324
  return f"Error reading '{path}': {e.strerror}"
1105
1325
 
1106
1326
  def file_write(path: str, content: str) -> str:
1107
- """Write content to a file in persistent storage. The path is relative to the storage root."""
1327
+ """Write a file to a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
1108
1328
  try:
1109
1329
  full_path = _safe_resolve(path)
1110
1330
  parent = os.path.dirname(full_path)
@@ -1118,25 +1338,28 @@ def file_write(path: str, content: str) -> str:
1118
1338
  except OSError as e:
1119
1339
  return f"Error writing '{path}': {e.strerror}"
1120
1340
 
1121
- def list_files(directory: str = "") -> str:
1122
- """List files in persistent storage. The directory is relative to the storage root."""
1341
+ def list_files(path: str) -> str:
1342
+ """List files in a mounted filesystem directory. Use the absolute path (e.g. /mnt/tools)."""
1123
1343
  try:
1124
- target = _safe_resolve(directory)
1125
- entries = os.listdir(target)
1344
+ full_path = _safe_resolve(path)
1345
+ entries = os.listdir(full_path)
1126
1346
  return "\\n".join(entries) if entries else "(empty directory)"
1127
1347
  except ValueError as e:
1128
1348
  return str(e)
1129
1349
  except OSError as e:
1130
- return f"Error listing '{directory}': {e.strerror}"
1350
+ return f"Error listing '{path}': {e.strerror}"
1131
1351
 
1132
1352
  tools.extend([file_read, file_write, list_files])
1133
1353
  {{/if}}
1134
1354
 
1135
1355
  AGENT_INSTRUCTION = """
1136
1356
  You are a helpful assistant. Use tools when appropriate.
1137
- {{#if sessionStorageMountPath}}
1138
- You have persistent storage at {{sessionStorageMountPath}}. Use file tools to read and write files. Data persists across sessions.
1139
- {{/if}}
1357
+ {{#if needsOs}}
1358
+ You have access to the following mounted filesystems. Use file_read, file_write, and list_files with full absolute paths:
1359
+ {{#if sessionStorageMountPath}}- {{sessionStorageMountPath}}: ephemeral session storage (lost when session ends)
1360
+ {{/if}}{{#each efsMounts}}- {{mountPath}}: EFS persistent storage (persists across sessions and agent restarts)
1361
+ {{/each}}{{#each s3Mounts}}- {{mountPath}}: S3 Files persistent storage (durable, backed by S3)
1362
+ {{/each}}{{/if}}
1140
1363
  """
1141
1364
 
1142
1365
  agent = Agent(
@@ -1322,7 +1545,9 @@ Thumbs.db
1322
1545
  `;
1323
1546
 
1324
1547
  exports[`Assets Directory Snapshots > Python framework assets > python/python/a2a/langchain_langgraph/base/main.py should match snapshot 1`] = `
1325
- "import os
1548
+ "{{#if needsOs}}
1549
+ import os
1550
+ {{/if}}
1326
1551
  from langchain_core.tools import tool
1327
1552
  from langgraph.prebuilt import create_react_agent
1328
1553
  from opentelemetry.instrumentation.langchain import LangchainInstrumentor
@@ -1345,19 +1570,22 @@ def add_numbers(a: int, b: int) -> int:
1345
1570
 
1346
1571
  tools = [add_numbers]
1347
1572
 
1348
- {{#if sessionStorageMountPath}}
1349
- SESSION_STORAGE_PATH = "{{sessionStorageMountPath}}"
1573
+ {{#if needsOs}}
1574
+ _MOUNT_PATHS = [
1575
+ {{#if sessionStorageMountPath}}"{{sessionStorageMountPath}}",{{/if}}
1576
+ {{#each efsMounts}}"{{mountPath}}",{{/each}}
1577
+ {{#each s3Mounts}}"{{mountPath}}",{{/each}}
1578
+ ]
1350
1579
 
1351
1580
  def _safe_resolve(path: str) -> str:
1352
- """Resolve path safely within the storage boundary."""
1353
- resolved = os.path.realpath(os.path.join(SESSION_STORAGE_PATH, path.lstrip("/")))
1354
- if not resolved.startswith(os.path.realpath(SESSION_STORAGE_PATH)):
1355
- raise ValueError(f"Path '{path}' is outside the storage boundary")
1581
+ resolved = os.path.realpath(path)
1582
+ if not any(resolved == os.path.realpath(m) or resolved.startswith(os.path.realpath(m) + os.sep) for m in _MOUNT_PATHS):
1583
+ raise ValueError(f"Path '{path}' is not within any configured mount ({', '.join(_MOUNT_PATHS)})")
1356
1584
  return resolved
1357
1585
 
1358
1586
  @tool
1359
1587
  def file_read(path: str) -> str:
1360
- """Read a file from persistent storage. The path is relative to the storage root."""
1588
+ """Read a file from a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
1361
1589
  try:
1362
1590
  full_path = _safe_resolve(path)
1363
1591
  with open(full_path) as f:
@@ -1369,7 +1597,7 @@ def file_read(path: str) -> str:
1369
1597
 
1370
1598
  @tool
1371
1599
  def file_write(path: str, content: str) -> str:
1372
- """Write content to a file in persistent storage. The path is relative to the storage root."""
1600
+ """Write a file to a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
1373
1601
  try:
1374
1602
  full_path = _safe_resolve(path)
1375
1603
  parent = os.path.dirname(full_path)
@@ -1384,25 +1612,28 @@ def file_write(path: str, content: str) -> str:
1384
1612
  return f"Error writing '{path}': {e.strerror}"
1385
1613
 
1386
1614
  @tool
1387
- def list_files(directory: str = "") -> str:
1388
- """List files in persistent storage. The directory is relative to the storage root."""
1615
+ def list_files(path: str) -> str:
1616
+ """List files in a mounted filesystem directory. Use the absolute path (e.g. /mnt/tools)."""
1389
1617
  try:
1390
- target = _safe_resolve(directory)
1391
- entries = os.listdir(target)
1618
+ full_path = _safe_resolve(path)
1619
+ entries = os.listdir(full_path)
1392
1620
  return "\\n".join(entries) if entries else "(empty directory)"
1393
1621
  except ValueError as e:
1394
1622
  return str(e)
1395
1623
  except OSError as e:
1396
- return f"Error listing '{directory}': {e.strerror}"
1624
+ return f"Error listing '{path}': {e.strerror}"
1397
1625
 
1398
1626
  tools.extend([file_read, file_write, list_files])
1399
1627
  {{/if}}
1400
1628
 
1401
1629
  SYSTEM_PROMPT = """
1402
1630
  You are a helpful assistant. Use tools when appropriate.
1403
- {{#if sessionStorageMountPath}}
1404
- You have persistent storage at {{sessionStorageMountPath}}. Use file tools to read and write files. Data persists across sessions.
1405
- {{/if}}
1631
+ {{#if needsOs}}
1632
+ You have access to the following mounted filesystems. Use file_read, file_write, and list_files with full absolute paths:
1633
+ {{#if sessionStorageMountPath}}- {{sessionStorageMountPath}}: ephemeral session storage (lost when session ends)
1634
+ {{/if}}{{#each efsMounts}}- {{mountPath}}: EFS persistent storage (persists across sessions and agent restarts)
1635
+ {{/each}}{{#each s3Mounts}}- {{mountPath}}: S3 Files persistent storage (durable, backed by S3)
1636
+ {{/each}}{{/if}}
1406
1637
  """
1407
1638
 
1408
1639
  model = load_model()
@@ -1607,7 +1838,7 @@ dependencies = [
1607
1838
  {{/if}}{{#if (eq modelProvider "OpenAI")}}"langchain-openai >= 0.2.0",
1608
1839
  {{/if}}"aws-opentelemetry-distro",
1609
1840
  "opentelemetry-instrumentation-langchain >= 0.59.0",
1610
- "bedrock-agentcore[a2a] >= 1.0.3",
1841
+ "bedrock-agentcore[a2a] >= 1.8.0",
1611
1842
  "botocore[crt] >= 1.35.0",
1612
1843
  "langgraph >= 0.2.0",
1613
1844
  ]
@@ -1696,7 +1927,7 @@ from model.load import load_model
1696
1927
  {{#if hasMemory}}
1697
1928
  from memory.session import get_memory_session_manager
1698
1929
  {{/if}}
1699
- {{#if sessionStorageMountPath}}
1930
+ {{#if needsOs}}
1700
1931
  import os
1701
1932
  {{/if}}
1702
1933
 
@@ -1709,19 +1940,22 @@ def add_numbers(a: int, b: int) -> int:
1709
1940
 
1710
1941
  tools = [add_numbers]
1711
1942
 
1712
- {{#if sessionStorageMountPath}}
1713
- SESSION_STORAGE_PATH = "{{sessionStorageMountPath}}"
1943
+ {{#if needsOs}}
1944
+ _MOUNT_PATHS = [
1945
+ {{#if sessionStorageMountPath}}"{{sessionStorageMountPath}}",{{/if}}
1946
+ {{#each efsMounts}}"{{mountPath}}",{{/each}}
1947
+ {{#each s3Mounts}}"{{mountPath}}",{{/each}}
1948
+ ]
1714
1949
 
1715
1950
  def _safe_resolve(path: str) -> str:
1716
- """Resolve path safely within the storage boundary."""
1717
- resolved = os.path.realpath(os.path.join(SESSION_STORAGE_PATH, path.lstrip("/")))
1718
- if not resolved.startswith(os.path.realpath(SESSION_STORAGE_PATH)):
1719
- raise ValueError(f"Path '{path}' is outside the storage boundary")
1951
+ resolved = os.path.realpath(path)
1952
+ if not any(resolved == os.path.realpath(m) or resolved.startswith(os.path.realpath(m) + os.sep) for m in _MOUNT_PATHS):
1953
+ raise ValueError(f"Path '{path}' is not within any configured mount ({', '.join(_MOUNT_PATHS)})")
1720
1954
  return resolved
1721
1955
 
1722
1956
  @tool
1723
1957
  def file_read(path: str) -> str:
1724
- """Read a file from persistent storage. The path is relative to the storage root."""
1958
+ """Read a file from a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
1725
1959
  try:
1726
1960
  full_path = _safe_resolve(path)
1727
1961
  with open(full_path) as f:
@@ -1733,7 +1967,7 @@ def file_read(path: str) -> str:
1733
1967
 
1734
1968
  @tool
1735
1969
  def file_write(path: str, content: str) -> str:
1736
- """Write content to a file in persistent storage. The path is relative to the storage root."""
1970
+ """Write a file to a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
1737
1971
  try:
1738
1972
  full_path = _safe_resolve(path)
1739
1973
  parent = os.path.dirname(full_path)
@@ -1748,25 +1982,28 @@ def file_write(path: str, content: str) -> str:
1748
1982
  return f"Error writing '{path}': {e.strerror}"
1749
1983
 
1750
1984
  @tool
1751
- def list_files(directory: str = "") -> str:
1752
- """List files in persistent storage. The directory is relative to the storage root."""
1985
+ def list_files(path: str) -> str:
1986
+ """List files in a mounted filesystem directory. Use the absolute path (e.g. /mnt/tools)."""
1753
1987
  try:
1754
- target = _safe_resolve(directory)
1755
- entries = os.listdir(target)
1988
+ full_path = _safe_resolve(path)
1989
+ entries = os.listdir(full_path)
1756
1990
  return "\\n".join(entries) if entries else "(empty directory)"
1757
1991
  except ValueError as e:
1758
1992
  return str(e)
1759
1993
  except OSError as e:
1760
- return f"Error listing '{directory}': {e.strerror}"
1994
+ return f"Error listing '{path}': {e.strerror}"
1761
1995
 
1762
1996
  tools.extend([file_read, file_write, list_files])
1763
1997
  {{/if}}
1764
1998
 
1765
1999
  SYSTEM_PROMPT = """
1766
2000
  You are a helpful assistant. Use tools when appropriate.
1767
- {{#if sessionStorageMountPath}}
1768
- You have persistent storage at {{sessionStorageMountPath}}. Use file tools to read and write files. Data persists across sessions.
1769
- {{/if}}
2001
+ {{#if needsOs}}
2002
+ You have access to the following mounted filesystems. Use file_read, file_write, and list_files with full absolute paths:
2003
+ {{#if sessionStorageMountPath}}- {{sessionStorageMountPath}}: ephemeral session storage (lost when session ends)
2004
+ {{/if}}{{#each efsMounts}}- {{mountPath}}: EFS persistent storage (persists across sessions and agent restarts)
2005
+ {{/each}}{{#each s3Mounts}}- {{mountPath}}: S3 Files persistent storage (durable, backed by S3)
2006
+ {{/each}}{{/if}}
1770
2007
  """
1771
2008
 
1772
2009
  {{#if hasMemory}}
@@ -2093,16 +2330,76 @@ exports[`Assets Directory Snapshots > Python framework assets > python/python/ag
2093
2330
  "import os
2094
2331
  import uvicorn
2095
2332
  from google.adk.agents import LlmAgent
2333
+ from google.adk.tools import FunctionTool
2096
2334
  from ag_ui_adk import ADKAgent, AGUIToolset, create_adk_app
2097
2335
  from model.load import load_model
2098
2336
 
2099
2337
  load_model()
2100
2338
 
2339
+ {{#if needsOs}}
2340
+ _MOUNT_PATHS = [
2341
+ {{#if sessionStorageMountPath}}"{{sessionStorageMountPath}}",{{/if}}
2342
+ {{#each efsMounts}}"{{mountPath}}",{{/each}}
2343
+ {{#each s3Mounts}}"{{mountPath}}",{{/each}}
2344
+ ]
2345
+
2346
+ def _safe_resolve(path: str) -> str:
2347
+ resolved = os.path.realpath(path)
2348
+ if not any(resolved == os.path.realpath(m) or resolved.startswith(os.path.realpath(m) + os.sep) for m in _MOUNT_PATHS):
2349
+ raise ValueError(f"Path '{path}' is not within any configured mount ({', '.join(_MOUNT_PATHS)})")
2350
+ return resolved
2351
+
2352
+ def file_read(path: str) -> str:
2353
+ """Read a file from a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
2354
+ try:
2355
+ full_path = _safe_resolve(path)
2356
+ with open(full_path) as f:
2357
+ return f.read()
2358
+ except ValueError as e:
2359
+ return str(e)
2360
+ except OSError as e:
2361
+ return f"Error reading '{path}': {e.strerror}"
2362
+
2363
+ def file_write(path: str, content: str) -> str:
2364
+ """Write a file to a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
2365
+ try:
2366
+ full_path = _safe_resolve(path)
2367
+ parent = os.path.dirname(full_path)
2368
+ if parent:
2369
+ os.makedirs(parent, exist_ok=True)
2370
+ with open(full_path, "w") as f:
2371
+ f.write(content)
2372
+ return f"Written to {path}"
2373
+ except ValueError as e:
2374
+ return str(e)
2375
+ except OSError as e:
2376
+ return f"Error writing '{path}': {e.strerror}"
2377
+
2378
+ def list_files(path: str) -> str:
2379
+ """List files in a mounted filesystem directory. Use the absolute path (e.g. /mnt/tools)."""
2380
+ try:
2381
+ full_path = _safe_resolve(path)
2382
+ entries = os.listdir(full_path)
2383
+ return "\\n".join(entries) if entries else "(empty directory)"
2384
+ except ValueError as e:
2385
+ return str(e)
2386
+ except OSError as e:
2387
+ return f"Error listing '{path}': {e.strerror}"
2388
+
2389
+ _fs_tools = [FunctionTool(file_read), FunctionTool(file_write), FunctionTool(list_files)]
2390
+ {{/if}}
2391
+
2101
2392
  agent = LlmAgent(
2102
2393
  name="{{ name }}",
2103
2394
  model="gemini-2.5-flash",
2104
- instruction="You are a helpful assistant.",
2105
- tools=[AGUIToolset()],
2395
+ instruction="""You are a helpful assistant.
2396
+ {{#if needsOs}}
2397
+ You have access to the following mounted filesystems. Use file_read, file_write, and list_files with full absolute paths:
2398
+ {{#if sessionStorageMountPath}}- {{sessionStorageMountPath}}: ephemeral session storage (lost when session ends)
2399
+ {{/if}}{{#each efsMounts}}- {{mountPath}}: EFS persistent storage (persists across sessions and agent restarts)
2400
+ {{/each}}{{#each s3Mounts}}- {{mountPath}}: S3 Files persistent storage (durable, backed by S3)
2401
+ {{/each}}{{/if}}""",
2402
+ tools=[AGUIToolset(), {{#if needsOs}}*_fs_tools{{/if}}],
2106
2403
  )
2107
2404
 
2108
2405
  adk_agent = ADKAgent(
@@ -2188,7 +2485,7 @@ requires-python = ">=3.10"
2188
2485
  dependencies = [
2189
2486
  "ag-ui-adk >= 0.6.0",
2190
2487
  "ag-ui-protocol >= 0.1.10",
2191
- "bedrock-agentcore >= 1.0.3",
2488
+ "bedrock-agentcore >= 1.8.0",
2192
2489
  "fastapi >= 0.115.12",
2193
2490
  "google-adk >= 1.16.0, < 2.0.0",
2194
2491
  "google-genai >= 1.0.0, < 2.0.0",
@@ -2301,6 +2598,62 @@ def add_numbers(a: int, b: int) -> int:
2301
2598
 
2302
2599
 
2303
2600
  backend_tools = [add_numbers]
2601
+
2602
+ {{#if needsOs}}
2603
+ _MOUNT_PATHS = [
2604
+ {{#if sessionStorageMountPath}}"{{sessionStorageMountPath}}",{{/if}}
2605
+ {{#each efsMounts}}"{{mountPath}}",{{/each}}
2606
+ {{#each s3Mounts}}"{{mountPath}}",{{/each}}
2607
+ ]
2608
+
2609
+ def _safe_resolve(path: str) -> str:
2610
+ resolved = os.path.realpath(path)
2611
+ if not any(resolved == os.path.realpath(m) or resolved.startswith(os.path.realpath(m) + os.sep) for m in _MOUNT_PATHS):
2612
+ raise ValueError(f"Path '{path}' is not within any configured mount ({', '.join(_MOUNT_PATHS)})")
2613
+ return resolved
2614
+
2615
+ @tool
2616
+ def file_read(path: str) -> str:
2617
+ """Read a file from a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
2618
+ try:
2619
+ full_path = _safe_resolve(path)
2620
+ with open(full_path) as f:
2621
+ return f.read()
2622
+ except ValueError as e:
2623
+ return str(e)
2624
+ except OSError as e:
2625
+ return f"Error reading '{path}': {e.strerror}"
2626
+
2627
+ @tool
2628
+ def file_write(path: str, content: str) -> str:
2629
+ """Write a file to a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
2630
+ try:
2631
+ full_path = _safe_resolve(path)
2632
+ parent = os.path.dirname(full_path)
2633
+ if parent:
2634
+ os.makedirs(parent, exist_ok=True)
2635
+ with open(full_path, "w") as f:
2636
+ f.write(content)
2637
+ return f"Written to {path}"
2638
+ except ValueError as e:
2639
+ return str(e)
2640
+ except OSError as e:
2641
+ return f"Error writing '{path}': {e.strerror}"
2642
+
2643
+ @tool
2644
+ def list_files(path: str) -> str:
2645
+ """List files in a mounted filesystem directory. Use the absolute path (e.g. /mnt/tools)."""
2646
+ try:
2647
+ full_path = _safe_resolve(path)
2648
+ entries = os.listdir(full_path)
2649
+ return "\\n".join(entries) if entries else "(empty directory)"
2650
+ except ValueError as e:
2651
+ return str(e)
2652
+ except OSError as e:
2653
+ return f"Error listing '{path}': {e.strerror}"
2654
+
2655
+ backend_tools.extend([file_read, file_write, list_files])
2656
+ {{/if}}
2304
2657
  model = load_model()
2305
2658
 
2306
2659
 
@@ -2504,7 +2857,7 @@ dependencies = [
2504
2857
  {{/if}}{{#if (eq modelProvider "OpenAI")}}"langchain-openai >= 0.2.0",
2505
2858
  {{/if}}"aws-opentelemetry-distro",
2506
2859
  "opentelemetry-instrumentation-langchain >= 0.59.0",
2507
- "bedrock-agentcore >= 1.0.3",
2860
+ "bedrock-agentcore >= 1.8.0",
2508
2861
  "botocore[crt] >= 1.35.0",
2509
2862
  "langgraph >= 0.3.25",
2510
2863
  "langchain >= 0.3.0",
@@ -2613,9 +2966,71 @@ def add_numbers(a: int, b: int) -> int:
2613
2966
 
2614
2967
  tools = [add_numbers]
2615
2968
 
2969
+ {{#if needsOs}}
2970
+ _MOUNT_PATHS = [
2971
+ {{#if sessionStorageMountPath}}"{{sessionStorageMountPath}}",{{/if}}
2972
+ {{#each efsMounts}}"{{mountPath}}",{{/each}}
2973
+ {{#each s3Mounts}}"{{mountPath}}",{{/each}}
2974
+ ]
2975
+
2976
+ def _safe_resolve(path: str) -> str:
2977
+ resolved = os.path.realpath(path)
2978
+ if not any(resolved == os.path.realpath(m) or resolved.startswith(os.path.realpath(m) + os.sep) for m in _MOUNT_PATHS):
2979
+ raise ValueError(f"Path '{path}' is not within any configured mount ({', '.join(_MOUNT_PATHS)})")
2980
+ return resolved
2981
+
2982
+ @tool
2983
+ def file_read(path: str) -> str:
2984
+ """Read a file from a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
2985
+ try:
2986
+ full_path = _safe_resolve(path)
2987
+ with open(full_path) as f:
2988
+ return f.read()
2989
+ except ValueError as e:
2990
+ return str(e)
2991
+ except OSError as e:
2992
+ return f"Error reading '{path}': {e.strerror}"
2993
+
2994
+ @tool
2995
+ def file_write(path: str, content: str) -> str:
2996
+ """Write a file to a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
2997
+ try:
2998
+ full_path = _safe_resolve(path)
2999
+ parent = os.path.dirname(full_path)
3000
+ if parent:
3001
+ os.makedirs(parent, exist_ok=True)
3002
+ with open(full_path, "w") as f:
3003
+ f.write(content)
3004
+ return f"Written to {path}"
3005
+ except ValueError as e:
3006
+ return str(e)
3007
+ except OSError as e:
3008
+ return f"Error writing '{path}': {e.strerror}"
3009
+
3010
+ @tool
3011
+ def list_files(path: str) -> str:
3012
+ """List files in a mounted filesystem directory. Use the absolute path (e.g. /mnt/tools)."""
3013
+ try:
3014
+ full_path = _safe_resolve(path)
3015
+ entries = os.listdir(full_path)
3016
+ return "\\n".join(entries) if entries else "(empty directory)"
3017
+ except ValueError as e:
3018
+ return str(e)
3019
+ except OSError as e:
3020
+ return f"Error listing '{path}': {e.strerror}"
3021
+
3022
+ tools.extend([file_read, file_write, list_files])
3023
+ {{/if}}
3024
+
2616
3025
  agent = Agent(
2617
3026
  model=load_model(),
2618
- system_prompt="You are a helpful assistant. Use tools when appropriate.",
3027
+ system_prompt="""You are a helpful assistant. Use tools when appropriate.
3028
+ {{#if needsOs}}
3029
+ You have access to the following mounted filesystems. Use file_read, file_write, and list_files with full absolute paths:
3030
+ {{#if sessionStorageMountPath}}- {{sessionStorageMountPath}}: ephemeral session storage (lost when session ends)
3031
+ {{/if}}{{#each efsMounts}}- {{mountPath}}: EFS persistent storage (persists across sessions and agent restarts)
3032
+ {{/each}}{{#each s3Mounts}}- {{mountPath}}: S3 Files persistent storage (durable, backed by S3)
3033
+ {{/each}}{{/if}}""",
2619
3034
  tools=tools,
2620
3035
  )
2621
3036
 
@@ -2940,7 +3355,9 @@ Thumbs.db
2940
3355
  `;
2941
3356
 
2942
3357
  exports[`Assets Directory Snapshots > Python framework assets > python/python/http/autogen/base/main.py should match snapshot 1`] = `
2943
- "import os
3358
+ "{{#if needsOs}}
3359
+ import os
3360
+ {{/if}}
2944
3361
  from autogen_agentchat.agents import AssistantAgent
2945
3362
  from autogen_core.tools import FunctionTool
2946
3363
  from bedrock_agentcore.runtime import BedrockAgentCoreApp
@@ -2964,18 +3381,21 @@ add_numbers_tool = FunctionTool(
2964
3381
  # Define a collection of tools used by the model
2965
3382
  tools = [add_numbers_tool]
2966
3383
 
2967
- {{#if sessionStorageMountPath}}
2968
- SESSION_STORAGE_PATH = "{{sessionStorageMountPath}}"
3384
+ {{#if needsOs}}
3385
+ _MOUNT_PATHS = [
3386
+ {{#if sessionStorageMountPath}}"{{sessionStorageMountPath}}",{{/if}}
3387
+ {{#each efsMounts}}"{{mountPath}}",{{/each}}
3388
+ {{#each s3Mounts}}"{{mountPath}}",{{/each}}
3389
+ ]
2969
3390
 
2970
3391
  def _safe_resolve(path: str) -> str:
2971
- """Resolve path safely within the storage boundary."""
2972
- resolved = os.path.realpath(os.path.join(SESSION_STORAGE_PATH, path.lstrip("/")))
2973
- if not resolved.startswith(os.path.realpath(SESSION_STORAGE_PATH)):
2974
- raise ValueError(f"Path '{path}' is outside the storage boundary")
3392
+ resolved = os.path.realpath(path)
3393
+ if not any(resolved == os.path.realpath(m) or resolved.startswith(os.path.realpath(m) + os.sep) for m in _MOUNT_PATHS):
3394
+ raise ValueError(f"Path '{path}' is not within any configured mount ({', '.join(_MOUNT_PATHS)})")
2975
3395
  return resolved
2976
3396
 
2977
3397
  def file_read(path: str) -> str:
2978
- """Read a file from persistent storage. The path is relative to the storage root."""
3398
+ """Read a file from a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
2979
3399
  try:
2980
3400
  full_path = _safe_resolve(path)
2981
3401
  with open(full_path) as f:
@@ -2986,7 +3406,7 @@ def file_read(path: str) -> str:
2986
3406
  return f"Error reading '{path}': {e.strerror}"
2987
3407
 
2988
3408
  def file_write(path: str, content: str) -> str:
2989
- """Write content to a file in persistent storage. The path is relative to the storage root."""
3409
+ """Write a file to a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
2990
3410
  try:
2991
3411
  full_path = _safe_resolve(path)
2992
3412
  parent = os.path.dirname(full_path)
@@ -3000,29 +3420,32 @@ def file_write(path: str, content: str) -> str:
3000
3420
  except OSError as e:
3001
3421
  return f"Error writing '{path}': {e.strerror}"
3002
3422
 
3003
- def list_files(directory: str = "") -> str:
3004
- """List files in persistent storage. The directory is relative to the storage root."""
3423
+ def list_files(path: str) -> str:
3424
+ """List files in a mounted filesystem directory. Use the absolute path (e.g. /mnt/tools)."""
3005
3425
  try:
3006
- target = _safe_resolve(directory)
3007
- entries = os.listdir(target)
3426
+ full_path = _safe_resolve(path)
3427
+ entries = os.listdir(full_path)
3008
3428
  return "\\n".join(entries) if entries else "(empty directory)"
3009
3429
  except ValueError as e:
3010
3430
  return str(e)
3011
3431
  except OSError as e:
3012
- return f"Error listing '{directory}': {e.strerror}"
3432
+ return f"Error listing '{path}': {e.strerror}"
3013
3433
 
3014
3434
  tools.extend([
3015
- FunctionTool(file_read, description="Read a file from persistent storage. The path is relative to the storage root."),
3016
- FunctionTool(file_write, description="Write content to a file in persistent storage. The path is relative to the storage root."),
3017
- FunctionTool(list_files, description="List files in persistent storage. The directory is relative to the storage root."),
3435
+ FunctionTool(file_read, description="Read a file from a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."),
3436
+ FunctionTool(file_write, description="Write a file to a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."),
3437
+ FunctionTool(list_files, description="List files in a mounted filesystem directory. Use the absolute path (e.g. /mnt/tools)."),
3018
3438
  ])
3019
3439
  {{/if}}
3020
3440
 
3021
3441
  SYSTEM_MESSAGE = """
3022
3442
  You are a helpful assistant. Use tools when appropriate.
3023
- {{#if sessionStorageMountPath}}
3024
- You have persistent storage at {{sessionStorageMountPath}}. Use file tools to read and write files. Data persists across sessions.
3025
- {{/if}}
3443
+ {{#if needsOs}}
3444
+ You have access to the following mounted filesystems. Use file_read, file_write, and list_files with full absolute paths:
3445
+ {{#if sessionStorageMountPath}}- {{sessionStorageMountPath}}: ephemeral session storage (lost when session ends)
3446
+ {{/if}}{{#each efsMounts}}- {{mountPath}}: EFS persistent storage (persists across sessions and agent restarts)
3447
+ {{/each}}{{#each s3Mounts}}- {{mountPath}}: S3 Files persistent storage (durable, backed by S3)
3448
+ {{/each}}{{/if}}
3026
3449
  """
3027
3450
 
3028
3451
  @app.entrypoint
@@ -3253,7 +3676,7 @@ dependencies = [
3253
3676
  "autogen-ext[mcp] >= 0.7.5",
3254
3677
  "opentelemetry-distro",
3255
3678
  "opentelemetry-exporter-otlp",
3256
- "bedrock-agentcore >= 1.0.3",
3679
+ "bedrock-agentcore >= 1.8.0",
3257
3680
  "botocore[crt] >= 1.35.0",
3258
3681
  "tiktoken",
3259
3682
  {{#if (eq modelProvider "Bedrock")}}
@@ -3364,7 +3787,9 @@ Thumbs.db
3364
3787
  `;
3365
3788
 
3366
3789
  exports[`Assets Directory Snapshots > Python framework assets > python/python/http/googleadk/base/main.py should match snapshot 1`] = `
3367
- "import os
3790
+ "{{#if needsOs}}
3791
+ import os
3792
+ {{/if}}
3368
3793
  from google.adk.agents import Agent
3369
3794
  from google.adk.runners import Runner
3370
3795
  from google.adk.sessions import InMemorySessionService
@@ -3395,18 +3820,21 @@ def add_numbers(a: int, b: int) -> int:
3395
3820
  # Define a collection of tools used by the model
3396
3821
  tools = [add_numbers]
3397
3822
 
3398
- {{#if sessionStorageMountPath}}
3399
- SESSION_STORAGE_PATH = "{{sessionStorageMountPath}}"
3823
+ {{#if needsOs}}
3824
+ _MOUNT_PATHS = [
3825
+ {{#if sessionStorageMountPath}}"{{sessionStorageMountPath}}",{{/if}}
3826
+ {{#each efsMounts}}"{{mountPath}}",{{/each}}
3827
+ {{#each s3Mounts}}"{{mountPath}}",{{/each}}
3828
+ ]
3400
3829
 
3401
3830
  def _safe_resolve(path: str) -> str:
3402
- """Resolve path safely within the storage boundary."""
3403
- resolved = os.path.realpath(os.path.join(SESSION_STORAGE_PATH, path.lstrip("/")))
3404
- if not resolved.startswith(os.path.realpath(SESSION_STORAGE_PATH)):
3405
- raise ValueError(f"Path '{path}' is outside the storage boundary")
3831
+ resolved = os.path.realpath(path)
3832
+ if not any(resolved == os.path.realpath(m) or resolved.startswith(os.path.realpath(m) + os.sep) for m in _MOUNT_PATHS):
3833
+ raise ValueError(f"Path '{path}' is not within any configured mount ({', '.join(_MOUNT_PATHS)})")
3406
3834
  return resolved
3407
3835
 
3408
3836
  def file_read(path: str) -> str:
3409
- """Read a file from persistent storage. The path is relative to the storage root."""
3837
+ """Read a file from a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
3410
3838
  try:
3411
3839
  full_path = _safe_resolve(path)
3412
3840
  with open(full_path) as f:
@@ -3417,7 +3845,7 @@ def file_read(path: str) -> str:
3417
3845
  return f"Error reading '{path}': {e.strerror}"
3418
3846
 
3419
3847
  def file_write(path: str, content: str) -> str:
3420
- """Write content to a file in persistent storage. The path is relative to the storage root."""
3848
+ """Write a file to a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
3421
3849
  try:
3422
3850
  full_path = _safe_resolve(path)
3423
3851
  parent = os.path.dirname(full_path)
@@ -3431,25 +3859,28 @@ def file_write(path: str, content: str) -> str:
3431
3859
  except OSError as e:
3432
3860
  return f"Error writing '{path}': {e.strerror}"
3433
3861
 
3434
- def list_files(directory: str = "") -> str:
3435
- """List files in persistent storage. The directory is relative to the storage root."""
3862
+ def list_files(path: str) -> str:
3863
+ """List files in a mounted filesystem directory. Use the absolute path (e.g. /mnt/tools)."""
3436
3864
  try:
3437
- target = _safe_resolve(directory)
3438
- entries = os.listdir(target)
3865
+ full_path = _safe_resolve(path)
3866
+ entries = os.listdir(full_path)
3439
3867
  return "\\n".join(entries) if entries else "(empty directory)"
3440
3868
  except ValueError as e:
3441
3869
  return str(e)
3442
3870
  except OSError as e:
3443
- return f"Error listing '{directory}': {e.strerror}"
3871
+ return f"Error listing '{path}': {e.strerror}"
3444
3872
 
3445
3873
  tools.extend([file_read, file_write, list_files])
3446
3874
  {{/if}}
3447
3875
 
3448
3876
  AGENT_INSTRUCTION = """
3449
3877
  I can answer your questions using the knowledge I have!
3450
- {{#if sessionStorageMountPath}}
3451
- You have persistent storage at {{sessionStorageMountPath}}. Use file tools to read and write files. Data persists across sessions.
3452
- {{/if}}
3878
+ {{#if needsOs}}
3879
+ You have access to the following mounted filesystems. Use file_read, file_write, and list_files with full absolute paths:
3880
+ {{#if sessionStorageMountPath}}- {{sessionStorageMountPath}}: ephemeral session storage (lost when session ends)
3881
+ {{/if}}{{#each efsMounts}}- {{mountPath}}: EFS persistent storage (persists across sessions and agent restarts)
3882
+ {{/each}}{{#each s3Mounts}}- {{mountPath}}: S3 Files persistent storage (durable, backed by S3)
3883
+ {{/each}}{{/if}}
3453
3884
  """
3454
3885
 
3455
3886
  # Get MCP Toolset
@@ -3777,7 +4208,9 @@ Thumbs.db
3777
4208
  `;
3778
4209
 
3779
4210
  exports[`Assets Directory Snapshots > Python framework assets > python/python/http/langchain_langgraph/base/main.py should match snapshot 1`] = `
3780
- "import os
4211
+ "{{#if needsOs}}
4212
+ import os
4213
+ {{/if}}
3781
4214
  from typing import Any
3782
4215
 
3783
4216
  from langchain_core.messages import HumanMessage{{#if hasConfigBundle}}, SystemMessage{{/if}}
@@ -3812,9 +4245,12 @@ def get_or_create_model():
3812
4245
 
3813
4246
  DEFAULT_SYSTEM_PROMPT = """
3814
4247
  You are a helpful assistant. Use tools when appropriate.
3815
- {{#if sessionStorageMountPath}}
3816
- You have persistent storage at {{sessionStorageMountPath}}. Use file tools to read and write files. Data persists across sessions.
3817
- {{/if}}
4248
+ {{#if needsOs}}
4249
+ You have access to the following mounted filesystems. Use file_read, file_write, and list_files with full absolute paths:
4250
+ {{#if sessionStorageMountPath}}- {{sessionStorageMountPath}}: ephemeral session storage (lost when session ends)
4251
+ {{/if}}{{#each efsMounts}}- {{mountPath}}: EFS persistent storage (persists across sessions and agent restarts)
4252
+ {{/each}}{{#each s3Mounts}}- {{mountPath}}: S3 Files persistent storage (durable, backed by S3)
4253
+ {{/each}}{{/if}}
3818
4254
  """
3819
4255
 
3820
4256
 
@@ -3828,19 +4264,22 @@ def add_numbers(a: int, b: int) -> int:
3828
4264
  # Define a collection of tools used by the model
3829
4265
  tools = [add_numbers]
3830
4266
 
3831
- {{#if sessionStorageMountPath}}
3832
- SESSION_STORAGE_PATH = "{{sessionStorageMountPath}}"
4267
+ {{#if needsOs}}
4268
+ _MOUNT_PATHS = [
4269
+ {{#if sessionStorageMountPath}}"{{sessionStorageMountPath}}",{{/if}}
4270
+ {{#each efsMounts}}"{{mountPath}}",{{/each}}
4271
+ {{#each s3Mounts}}"{{mountPath}}",{{/each}}
4272
+ ]
3833
4273
 
3834
4274
  def _safe_resolve(path: str) -> str:
3835
- """Resolve path safely within the storage boundary."""
3836
- resolved = os.path.realpath(os.path.join(SESSION_STORAGE_PATH, path.lstrip("/")))
3837
- if not resolved.startswith(os.path.realpath(SESSION_STORAGE_PATH)):
3838
- raise ValueError(f"Path '{path}' is outside the storage boundary")
4275
+ resolved = os.path.realpath(path)
4276
+ if not any(resolved == os.path.realpath(m) or resolved.startswith(os.path.realpath(m) + os.sep) for m in _MOUNT_PATHS):
4277
+ raise ValueError(f"Path '{path}' is not within any configured mount ({', '.join(_MOUNT_PATHS)})")
3839
4278
  return resolved
3840
4279
 
3841
4280
  @tool
3842
4281
  def file_read(path: str) -> str:
3843
- """Read a file from persistent storage. The path is relative to the storage root."""
4282
+ """Read a file from a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
3844
4283
  try:
3845
4284
  full_path = _safe_resolve(path)
3846
4285
  with open(full_path) as f:
@@ -3852,7 +4291,7 @@ def file_read(path: str) -> str:
3852
4291
 
3853
4292
  @tool
3854
4293
  def file_write(path: str, content: str) -> str:
3855
- """Write content to a file in persistent storage. The path is relative to the storage root."""
4294
+ """Write a file to a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
3856
4295
  try:
3857
4296
  full_path = _safe_resolve(path)
3858
4297
  parent = os.path.dirname(full_path)
@@ -3867,16 +4306,16 @@ def file_write(path: str, content: str) -> str:
3867
4306
  return f"Error writing '{path}': {e.strerror}"
3868
4307
 
3869
4308
  @tool
3870
- def list_files(directory: str = "") -> str:
3871
- """List files in persistent storage. The directory is relative to the storage root."""
4309
+ def list_files(path: str) -> str:
4310
+ """List files in a mounted filesystem directory. Use the absolute path (e.g. /mnt/tools)."""
3872
4311
  try:
3873
- target = _safe_resolve(directory)
3874
- entries = os.listdir(target)
4312
+ full_path = _safe_resolve(path)
4313
+ entries = os.listdir(full_path)
3875
4314
  return "\\n".join(entries) if entries else "(empty directory)"
3876
4315
  except ValueError as e:
3877
4316
  return str(e)
3878
4317
  except OSError as e:
3879
- return f"Error listing '{directory}': {e.strerror}"
4318
+ return f"Error listing '{path}': {e.strerror}"
3880
4319
 
3881
4320
  tools.extend([file_read, file_write, list_files])
3882
4321
  {{/if}}
@@ -4192,7 +4631,7 @@ dependencies = [
4192
4631
  "mcp >= 1.19.0",
4193
4632
  "langchain-mcp-adapters >= 0.1.11",
4194
4633
  "langchain >= 1.0.3",
4195
- "bedrock-agentcore >= 1.0.3",
4634
+ "bedrock-agentcore >= 1.8.0",
4196
4635
  "botocore[crt] >= 1.35.0",
4197
4636
  {{#if (eq modelProvider "Bedrock")}}
4198
4637
  "langchain-aws >= 1.0.0",
@@ -4305,7 +4744,9 @@ Thumbs.db
4305
4744
  `;
4306
4745
 
4307
4746
  exports[`Assets Directory Snapshots > Python framework assets > python/python/http/openaiagents/base/main.py should match snapshot 1`] = `
4308
- "import os
4747
+ "{{#if needsOs}}
4748
+ import os
4749
+ {{/if}}
4309
4750
  from agents import Agent, Runner, function_tool
4310
4751
  from bedrock_agentcore.runtime import BedrockAgentCoreApp
4311
4752
  from model.load import load_model
@@ -4344,19 +4785,22 @@ def add_numbers(a: int, b: int) -> int:
4344
4785
 
4345
4786
  tools = [add_numbers]
4346
4787
 
4347
- {{#if sessionStorageMountPath}}
4348
- SESSION_STORAGE_PATH = "{{sessionStorageMountPath}}"
4788
+ {{#if needsOs}}
4789
+ _MOUNT_PATHS = [
4790
+ {{#if sessionStorageMountPath}}"{{sessionStorageMountPath}}",{{/if}}
4791
+ {{#each efsMounts}}"{{mountPath}}",{{/each}}
4792
+ {{#each s3Mounts}}"{{mountPath}}",{{/each}}
4793
+ ]
4349
4794
 
4350
4795
  def _safe_resolve(path: str) -> str:
4351
- """Resolve path safely within the storage boundary."""
4352
- resolved = os.path.realpath(os.path.join(SESSION_STORAGE_PATH, path.lstrip("/")))
4353
- if not resolved.startswith(os.path.realpath(SESSION_STORAGE_PATH)):
4354
- raise ValueError(f"Path '{path}' is outside the storage boundary")
4796
+ resolved = os.path.realpath(path)
4797
+ if not any(resolved == os.path.realpath(m) or resolved.startswith(os.path.realpath(m) + os.sep) for m in _MOUNT_PATHS):
4798
+ raise ValueError(f"Path '{path}' is not within any configured mount ({', '.join(_MOUNT_PATHS)})")
4355
4799
  return resolved
4356
4800
 
4357
4801
  @function_tool
4358
4802
  def file_read(path: str) -> str:
4359
- """Read a file from persistent storage. The path is relative to the storage root."""
4803
+ """Read a file from a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
4360
4804
  try:
4361
4805
  full_path = _safe_resolve(path)
4362
4806
  with open(full_path) as f:
@@ -4368,7 +4812,7 @@ def file_read(path: str) -> str:
4368
4812
 
4369
4813
  @function_tool
4370
4814
  def file_write(path: str, content: str) -> str:
4371
- """Write content to a file in persistent storage. The path is relative to the storage root."""
4815
+ """Write a file to a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
4372
4816
  try:
4373
4817
  full_path = _safe_resolve(path)
4374
4818
  parent = os.path.dirname(full_path)
@@ -4383,25 +4827,28 @@ def file_write(path: str, content: str) -> str:
4383
4827
  return f"Error writing '{path}': {e.strerror}"
4384
4828
 
4385
4829
  @function_tool
4386
- def list_files(directory: str = "") -> str:
4387
- """List files in persistent storage. The directory is relative to the storage root."""
4830
+ def list_files(path: str) -> str:
4831
+ """List files in a mounted filesystem directory. Use the absolute path (e.g. /mnt/tools)."""
4388
4832
  try:
4389
- target = _safe_resolve(directory)
4390
- entries = os.listdir(target)
4833
+ full_path = _safe_resolve(path)
4834
+ entries = os.listdir(full_path)
4391
4835
  return "\\n".join(entries) if entries else "(empty directory)"
4392
4836
  except ValueError as e:
4393
4837
  return str(e)
4394
4838
  except OSError as e:
4395
- return f"Error listing '{directory}': {e.strerror}"
4839
+ return f"Error listing '{path}': {e.strerror}"
4396
4840
 
4397
4841
  tools.extend([file_read, file_write, list_files])
4398
4842
  {{/if}}
4399
4843
 
4400
4844
  INSTRUCTIONS = """
4401
4845
  You are a helpful assistant. Use tools when appropriate.
4402
- {{#if sessionStorageMountPath}}
4403
- You have persistent storage at {{sessionStorageMountPath}}. Use file tools to read and write files. Data persists across sessions.
4404
- {{/if}}
4846
+ {{#if needsOs}}
4847
+ You have access to the following mounted filesystems. Use file_read, file_write, and list_files with full absolute paths:
4848
+ {{#if sessionStorageMountPath}}- {{sessionStorageMountPath}}: ephemeral session storage (lost when session ends)
4849
+ {{/if}}{{#each efsMounts}}- {{mountPath}}: EFS persistent storage (persists across sessions and agent restarts)
4850
+ {{/each}}{{#each s3Mounts}}- {{mountPath}}: S3 Files persistent storage (durable, backed by S3)
4851
+ {{/each}}{{/if}}
4405
4852
  """
4406
4853
 
4407
4854
  # Define the agent execution
@@ -4620,7 +5067,7 @@ requires-python = ">=3.10"
4620
5067
  dependencies = [
4621
5068
  "aws-opentelemetry-distro",
4622
5069
  "openai-agents >= 0.4.2",
4623
- "bedrock-agentcore >= 1.0.3",
5070
+ "bedrock-agentcore >= 1.8.0",
4624
5071
  "botocore[crt] >= 1.35.0",
4625
5072
  {{#if hasGateway}}{{#if (includes gatewayAuthTypes "AWS_IAM")}}"mcp-proxy-for-aws >= 1.1.0",
4626
5073
  {{/if}}{{/if}}
@@ -4736,9 +5183,12 @@ from mcp_client.client import get_streamable_http_mcp_client
4736
5183
  {{#if hasMemory}}
4737
5184
  from memory.session import get_memory_session_manager
4738
5185
  {{/if}}
4739
- {{#if sessionStorageMountPath}}
5186
+ {{#if needsOs}}
4740
5187
  import os
4741
5188
  {{/if}}
5189
+ {{#if hasPayment}}
5190
+ from capabilities.payments.payments import create_payments_plugin, PAYMENT_SYSTEM_PROMPT
5191
+ {{/if}}
4742
5192
 
4743
5193
  app = BedrockAgentCoreApp()
4744
5194
  log = app.logger
@@ -4752,9 +5202,12 @@ mcp_clients = [get_streamable_http_mcp_client()]
4752
5202
 
4753
5203
  DEFAULT_SYSTEM_PROMPT = """
4754
5204
  You are a helpful assistant. Use tools when appropriate.
4755
- {{#if sessionStorageMountPath}}
4756
- You have persistent storage at {{sessionStorageMountPath}}. Use file tools to read and write files. Data persists across sessions.
4757
- {{/if}}
5205
+ {{#if needsOs}}
5206
+ You have access to the following mounted filesystems. Use file_read, file_write, and list_files with full absolute paths:
5207
+ {{#if sessionStorageMountPath}}- {{sessionStorageMountPath}}: ephemeral session storage (lost when session ends)
5208
+ {{/if}}{{#each efsMounts}}- {{mountPath}}: EFS persistent storage (persists across sessions and agent restarts)
5209
+ {{/each}}{{#each s3Mounts}}- {{mountPath}}: S3 Files persistent storage (durable, backed by S3)
5210
+ {{/each}}{{/if}}
4758
5211
  """
4759
5212
 
4760
5213
  {{#if hasConfigBundle}}
@@ -4775,19 +5228,22 @@ def add_numbers(a: int, b: int) -> int:
4775
5228
  return a+b
4776
5229
  tools.append(add_numbers)
4777
5230
 
4778
- {{#if sessionStorageMountPath}}
4779
- SESSION_STORAGE_PATH = "{{sessionStorageMountPath}}"
5231
+ {{#if needsOs}}
5232
+ _MOUNT_PATHS = [
5233
+ {{#if sessionStorageMountPath}}"{{sessionStorageMountPath}}",{{/if}}
5234
+ {{#each efsMounts}}"{{mountPath}}",{{/each}}
5235
+ {{#each s3Mounts}}"{{mountPath}}",{{/each}}
5236
+ ]
4780
5237
 
4781
5238
  def _safe_resolve(path: str) -> str:
4782
- """Resolve path safely within the storage boundary."""
4783
- resolved = os.path.realpath(os.path.join(SESSION_STORAGE_PATH, path.lstrip("/")))
4784
- if not resolved.startswith(os.path.realpath(SESSION_STORAGE_PATH)):
4785
- raise ValueError(f"Path '{path}' is outside the storage boundary")
5239
+ resolved = os.path.realpath(path)
5240
+ if not any(resolved == os.path.realpath(m) or resolved.startswith(os.path.realpath(m) + os.sep) for m in _MOUNT_PATHS):
5241
+ raise ValueError(f"Path '{path}' is not within any configured mount ({', '.join(_MOUNT_PATHS)})")
4786
5242
  return resolved
4787
5243
 
4788
5244
  @tool
4789
5245
  def file_read(path: str) -> str:
4790
- """Read a file from persistent storage. The path is relative to the storage root."""
5246
+ """Read a file from a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
4791
5247
  try:
4792
5248
  full_path = _safe_resolve(path)
4793
5249
  with open(full_path) as f:
@@ -4799,7 +5255,7 @@ def file_read(path: str) -> str:
4799
5255
 
4800
5256
  @tool
4801
5257
  def file_write(path: str, content: str) -> str:
4802
- """Write content to a file in persistent storage. The path is relative to the storage root."""
5258
+ """Write a file to a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
4803
5259
  try:
4804
5260
  full_path = _safe_resolve(path)
4805
5261
  parent = os.path.dirname(full_path)
@@ -4814,16 +5270,16 @@ def file_write(path: str, content: str) -> str:
4814
5270
  return f"Error writing '{path}': {e.strerror}"
4815
5271
 
4816
5272
  @tool
4817
- def list_files(directory: str = "") -> str:
4818
- """List files in persistent storage. The directory is relative to the storage root."""
5273
+ def list_files(path: str) -> str:
5274
+ """List files in a mounted filesystem directory. Use the absolute path (e.g. /mnt/tools)."""
4819
5275
  try:
4820
- target = _safe_resolve(directory)
4821
- entries = os.listdir(target)
5276
+ full_path = _safe_resolve(path)
5277
+ entries = os.listdir(full_path)
4822
5278
  return "\\n".join(entries) if entries else "(empty directory)"
4823
5279
  except ValueError as e:
4824
5280
  return str(e)
4825
5281
  except OSError as e:
4826
- return f"Error listing '{directory}': {e.strerror}"
5282
+ return f"Error listing '{path}': {e.strerror}"
4827
5283
 
4828
5284
  tools.extend([file_read, file_write, list_files])
4829
5285
  {{/if}}
@@ -4868,12 +5324,12 @@ class ConfigBundleHook(HookProvider):
4868
5324
  {{/if}}
4869
5325
 
4870
5326
  {{#if hasMemory}}
5327
+ {{#unless hasPayment}}
4871
5328
  def agent_factory():
4872
5329
  cache = {}
4873
5330
  def get_or_create_agent(session_id, user_id):
4874
5331
  key = f"{session_id}/{user_id}"
4875
5332
  if key not in cache:
4876
- # Create an agent for the given session_id and user_id
4877
5333
  cache[key] = Agent(
4878
5334
  model=load_model(),
4879
5335
  session_manager=get_memory_session_manager(session_id, user_id),
@@ -4884,6 +5340,7 @@ def agent_factory():
4884
5340
  return cache[key]
4885
5341
  return get_or_create_agent
4886
5342
  get_or_create_agent = agent_factory()
5343
+ {{/unless}}
4887
5344
  {{else}}
4888
5345
  {{#if hasConfigBundle}}
4889
5346
  def create_agent():
@@ -4894,6 +5351,7 @@ def create_agent():
4894
5351
  hooks=[ConfigBundleHook()],
4895
5352
  )
4896
5353
  {{else}}
5354
+ {{#unless hasPayment}}
4897
5355
  _agent = None
4898
5356
 
4899
5357
  def get_or_create_agent():
@@ -4902,9 +5360,10 @@ def get_or_create_agent():
4902
5360
  _agent = Agent(
4903
5361
  model=load_model(),
4904
5362
  system_prompt=DEFAULT_SYSTEM_PROMPT,
4905
- tools=tools
5363
+ tools=tools,
4906
5364
  )
4907
5365
  return _agent
5366
+ {{/unless}}
4908
5367
  {{/if}}
4909
5368
  {{/if}}
4910
5369
 
@@ -4913,16 +5372,47 @@ def get_or_create_agent():
4913
5372
  async def invoke(payload, context):
4914
5373
  log.info("Invoking Agent.....")
4915
5374
 
5375
+ {{#if hasPayment}}
5376
+ user_id = payload.get("user_id") or getattr(context, "user_id", "default-user")
5377
+ instrument_id = payload.get("payment_instrument_id")
5378
+ session_id = payload.get("payment_session_id")
5379
+ payments_plugin = create_payments_plugin(user_id, instrument_id, session_id)
5380
+ plugins = [payments_plugin] if payments_plugin else []
5381
+ {{/if}}
5382
+
4916
5383
  {{#if hasMemory}}
5384
+ {{#if hasPayment}}
5385
+ mem_session_id = getattr(context, 'session_id', 'default-session')
5386
+ mem_user_id = getattr(context, 'user_id', 'default-user')
5387
+ agent = Agent(
5388
+ model=load_model(),
5389
+ session_manager=get_memory_session_manager(mem_session_id, mem_user_id),
5390
+ system_prompt=DEFAULT_SYSTEM_PROMPT + PAYMENT_SYSTEM_PROMPT,
5391
+ tools=tools,
5392
+ plugins=plugins,{{#if hasConfigBundle}}
5393
+ hooks=[ConfigBundleHook()],{{/if}}
5394
+ )
5395
+ {{else}}
4917
5396
  session_id = getattr(context, 'session_id', 'default-session')
4918
5397
  user_id = getattr(context, 'user_id', 'default-user')
4919
5398
  agent = get_or_create_agent(session_id, user_id)
5399
+ {{/if}}
5400
+ {{else}}
5401
+ {{#if hasPayment}}
5402
+ agent = Agent(
5403
+ model=load_model(),
5404
+ system_prompt=DEFAULT_SYSTEM_PROMPT + PAYMENT_SYSTEM_PROMPT,
5405
+ tools=tools,
5406
+ plugins=plugins,{{#if hasConfigBundle}}
5407
+ hooks=[ConfigBundleHook()],{{/if}}
5408
+ )
4920
5409
  {{else}}
4921
5410
  {{#if hasConfigBundle}}
4922
5411
  agent = create_agent()
4923
5412
  {{else}}
4924
5413
  agent = get_or_create_agent()
4925
5414
  {{/if}}
5415
+ {{/if}}
4926
5416
  {{/if}}
4927
5417
 
4928
5418
  # Execute and format response
@@ -5235,6 +5725,160 @@ def get_memory_session_manager(session_id: Optional[str], actor_id: str) -> Opti
5235
5725
  "
5236
5726
  `;
5237
5727
 
5728
+ exports[`Assets Directory Snapshots > Python framework assets > python/python/http/strands/capabilities/payments/__init__.py should match snapshot 1`] = `
5729
+ """"Payment capabilities for Strands agents."""
5730
+ from .payments import create_payments_plugin
5731
+
5732
+ __all__ = ["create_payments_plugin"]
5733
+ "
5734
+ `;
5735
+
5736
+ exports[`Assets Directory Snapshots > Python framework assets > python/python/http/strands/capabilities/payments/payments.py should match snapshot 1`] = `
5737
+ """"Payment capability -- auto-generated by agentcore CLI.
5738
+
5739
+ Configures AgentCorePaymentsPlugin for Strands agents.
5740
+ Manager config is auto-discovered from AGENTCORE_PAYMENT_*
5741
+ environment variables set at deploy time.
5742
+
5743
+ Uses a per-invocation factory pattern so each request gets its own
5744
+ plugin instance with the correct user_id, instrument_id, and session_id.
5745
+ This prevents concurrency bugs where one user's payment context
5746
+ could leak to another user's request.
5747
+
5748
+ Uses the official SDK plugin which handles:
5749
+ - x402 v1 (body-based) and v2 (header-based) payment detection
5750
+ - Automatic 402 response interception and payment processing
5751
+ - Retry limiting (max 3 payment retries per tool use)
5752
+ - Error management and logging
5753
+ """
5754
+ import os
5755
+ import logging
5756
+
5757
+ import boto3
5758
+ from bedrock_agentcore.payments.integrations.strands import AgentCorePaymentsPlugin
5759
+ from bedrock_agentcore.payments.integrations.config import AgentCorePaymentsPluginConfig
5760
+
5761
+ logger = logging.getLogger(__name__)
5762
+
5763
+ PAYMENT_SYSTEM_PROMPT = """
5764
+ You have payment capabilities via the x402 protocol:
5765
+ - Use http_request to call HTTP endpoints. 402 Payment Required responses are settled automatically by the plugin and the call is retried.
5766
+ - Use get_payment_session to check your remaining budget before expensive operations
5767
+ - Use get_payment_instrument_balance to check wallet USDC balance
5768
+ - Use list_payment_instruments to see available payment instruments
5769
+ - If budget is low, inform the user before proceeding with paid requests
5770
+ """
5771
+
5772
+ _manager_arn = None
5773
+ _connector_id = None
5774
+ _process_payment_role_arn = None
5775
+ _name_segment = None
5776
+ _region = None
5777
+ _auth_mode = None
5778
+ _manager_count = 0
5779
+ for key, value in os.environ.items():
5780
+ if key.startswith("AGENTCORE_PAYMENT_") and key.endswith("_MANAGER_ARN"):
5781
+ if _manager_arn is None:
5782
+ _manager_arn = value
5783
+ _name_segment = key[len("AGENTCORE_PAYMENT_"):-len("_MANAGER_ARN")]
5784
+ _manager_count += 1
5785
+ if _manager_count > 1:
5786
+ logger.warning(
5787
+ "Multiple payment managers detected in environment. Using the first one found. "
5788
+ "Remove extra AGENTCORE_PAYMENT_*_MANAGER_ARN env vars to eliminate ambiguity."
5789
+ )
5790
+ _region = os.getenv("AWS_REGION")
5791
+
5792
+ _prefix = f"AGENTCORE_PAYMENT_{_name_segment}_" if _name_segment else "AGENTCORE_PAYMENT_"
5793
+ _auth_mode = os.getenv(f"{_prefix}AUTH_MODE", "sigv4")
5794
+ _connector_id = os.getenv(f"{_prefix}CONNECTOR_ID")
5795
+ _process_payment_role_arn = os.getenv(f"{_prefix}PROCESS_PAYMENT_ROLE_ARN")
5796
+ _auto_payment = os.getenv(f"{_prefix}AUTO_PAYMENT", "true").lower() == "true"
5797
+ _allowlist_raw = os.getenv(f"{_prefix}TOOL_ALLOWLIST")
5798
+ _allowlist = _allowlist_raw.split(",") if _allowlist_raw else None
5799
+ _network_prefs_raw = os.getenv(f"{_prefix}NETWORK_PREFERENCES")
5800
+ _network_prefs = _network_prefs_raw.split(",") if _network_prefs_raw else None
5801
+
5802
+ if not _manager_arn:
5803
+ logger.warning("No payment manager config found in environment")
5804
+ if not _connector_id:
5805
+ logger.warning("No payment connector config found in environment")
5806
+
5807
+
5808
+ def _assume_role_session(role_arn):
5809
+ """Assume an IAM role and return a boto3 session with temporary credentials."""
5810
+ sts = boto3.client("sts", region_name=_region)
5811
+ creds = sts.assume_role(
5812
+ RoleArn=role_arn,
5813
+ RoleSessionName="agentcore-payment-plugin",
5814
+ )["Credentials"]
5815
+ return boto3.Session(
5816
+ aws_access_key_id=creds["AccessKeyId"],
5817
+ aws_secret_access_key=creds["SecretAccessKey"],
5818
+ aws_session_token=creds["SessionToken"],
5819
+ region_name=_region,
5820
+ )
5821
+
5822
+
5823
+ def create_payments_plugin(user_id, instrument_id=None, session_id=None):
5824
+ """Create a fresh plugin instance per invocation.
5825
+
5826
+ Args:
5827
+ user_id: From invocation context (required for SigV4, derived from JWT for bearer)
5828
+ instrument_id: From invocation payload (created by app backend per user)
5829
+ session_id: From invocation payload (created by app backend per conversation)
5830
+
5831
+ Returns:
5832
+ AgentCorePaymentsPlugin instance, or None if no manager is configured.
5833
+ """
5834
+ if not _manager_arn:
5835
+ return None
5836
+
5837
+ config_kwargs = {
5838
+ "payment_manager_arn": _manager_arn,
5839
+ "region": _region,
5840
+ "payment_instrument_id": instrument_id,
5841
+ "payment_session_id": session_id,
5842
+ "payment_connector_id": _connector_id,
5843
+ }
5844
+
5845
+ config_kwargs["auto_payment"] = _auto_payment
5846
+ if _allowlist:
5847
+ config_kwargs["payment_tool_allowlist"] = _allowlist
5848
+ if _network_prefs:
5849
+ config_kwargs["network_preferences_config"] = _network_prefs
5850
+
5851
+ if _process_payment_role_arn:
5852
+ # Only pass boto3_session if SDK supports it (added in bedrock-agentcore >= 1.11).
5853
+ # Older SDKs use the runtime role's default credentials and can still call ProcessPayment
5854
+ # if the runtime role has been granted permission directly.
5855
+ import inspect
5856
+ if "boto3_session" in inspect.signature(AgentCorePaymentsPluginConfig).parameters:
5857
+ config_kwargs["boto3_session"] = _assume_role_session(_process_payment_role_arn)
5858
+ else:
5859
+ logger.warning(
5860
+ "PROCESS_PAYMENT_ROLE_ARN set but bedrock-agentcore SDK does not support boto3_session. "
5861
+ "Upgrade to bedrock-agentcore>=1.11 to enable cross-role payment processing."
5862
+ )
5863
+
5864
+ if _auth_mode == "bearer":
5865
+ bearer_token = os.getenv("AGENTCORE_BEARER_TOKEN")
5866
+ if bearer_token:
5867
+ config_kwargs["bearer_token"] = bearer_token
5868
+ else:
5869
+ logger.warning(
5870
+ "Bearer auth mode configured but AGENTCORE_BEARER_TOKEN not set. "
5871
+ "Falling back to SigV4. Set AGENTCORE_BEARER_TOKEN or pass bearer_token in invoke context."
5872
+ )
5873
+ config_kwargs["user_id"] = user_id or "default-user"
5874
+ else:
5875
+ config_kwargs["user_id"] = user_id or "default-user"
5876
+
5877
+ config = AgentCorePaymentsPluginConfig(**config_kwargs)
5878
+ return AgentCorePaymentsPlugin(config=config)
5879
+ "
5880
+ `;
5881
+
5238
5882
  exports[`Assets Directory Snapshots > Python framework assets > python/python/mcp/standalone/base/README.md should match snapshot 1`] = `
5239
5883
  "# {{ name }}
5240
5884
 
@@ -5321,10 +5965,68 @@ Thumbs.db
5321
5965
  `;
5322
5966
 
5323
5967
  exports[`Assets Directory Snapshots > Python framework assets > python/python/mcp/standalone/base/main.py should match snapshot 1`] = `
5324
- "from mcp.server.fastmcp import FastMCP
5968
+ "{{#if needsOs}}
5969
+ import os
5970
+ {{/if}}
5971
+ from mcp.server.fastmcp import FastMCP
5325
5972
 
5326
5973
  mcp = FastMCP("{{ name }}", host="0.0.0.0", stateless_http=True)
5327
5974
 
5975
+ {{#if needsOs}}
5976
+ _MOUNT_PATHS = [
5977
+ {{#if sessionStorageMountPath}}"{{sessionStorageMountPath}}",{{/if}}
5978
+ {{#each efsMounts}}"{{mountPath}}",{{/each}}
5979
+ {{#each s3Mounts}}"{{mountPath}}",{{/each}}
5980
+ ]
5981
+
5982
+ def _safe_resolve(path: str) -> str:
5983
+ resolved = os.path.realpath(path)
5984
+ if not any(resolved == os.path.realpath(m) or resolved.startswith(os.path.realpath(m) + os.sep) for m in _MOUNT_PATHS):
5985
+ raise ValueError(f"Path '{path}' is not within any configured mount ({', '.join(_MOUNT_PATHS)})")
5986
+ return resolved
5987
+
5988
+ @mcp.tool()
5989
+ def file_read(path: str) -> str:
5990
+ """Read a file from a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
5991
+ try:
5992
+ full_path = _safe_resolve(path)
5993
+ with open(full_path) as f:
5994
+ return f.read()
5995
+ except ValueError as e:
5996
+ return str(e)
5997
+ except OSError as e:
5998
+ return f"Error reading '{path}': {e.strerror}"
5999
+
6000
+ @mcp.tool()
6001
+ def file_write(path: str, content: str) -> str:
6002
+ """Write a file to a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
6003
+ try:
6004
+ full_path = _safe_resolve(path)
6005
+ parent = os.path.dirname(full_path)
6006
+ if parent:
6007
+ os.makedirs(parent, exist_ok=True)
6008
+ with open(full_path, "w") as f:
6009
+ f.write(content)
6010
+ return f"Written to {path}"
6011
+ except ValueError as e:
6012
+ return str(e)
6013
+ except OSError as e:
6014
+ return f"Error writing '{path}': {e.strerror}"
6015
+
6016
+ @mcp.tool()
6017
+ def list_files(path: str) -> str:
6018
+ """List files in a mounted filesystem directory. Use the absolute path (e.g. /mnt/tools)."""
6019
+ try:
6020
+ full_path = _safe_resolve(path)
6021
+ entries = os.listdir(full_path)
6022
+ return "\\n".join(entries) if entries else "(empty directory)"
6023
+ except ValueError as e:
6024
+ return str(e)
6025
+ except OSError as e:
6026
+ return f"Error listing '{path}': {e.strerror}"
6027
+
6028
+ {{/if}}
6029
+
5328
6030
 
5329
6031
  @mcp.tool()
5330
6032
  def add_numbers(a: int, b: int) -> int: