@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
@@ -28,12 +28,14 @@ async function main() {
28
28
  const spec = await configIO.readProjectSpec();
29
29
  const targets = await configIO.readAWSDeploymentTargets();
30
30
 
31
- // Extract MCP configuration from project spec.
32
- // Gateway fields are stored in agentcore.json but may not yet be on the
33
- // AgentCoreProjectSpec type from @aws/agentcore-cdk, so we read them
34
- // dynamically and cast the resulting object.
31
+ // The vended CDK project compiles against the published @aws/agentcore-cdk
32
+ // schema type, which may lag the CLI's own AgentCoreProjectSpec (e.g. payments,
33
+ // harnesses, gateway fields). Cast once so those fields are reachable.
35
34
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
36
35
  const specAny = spec as any;
36
+
37
+ // Extract MCP configuration from project spec.
38
+ // Gateway fields are stored in agentcore.json but may not yet be on the
37
39
  const mcpSpec = specAny.agentCoreGateways?.length
38
40
  ? {
39
41
  agentCoreGateways: specAny.agentCoreGateways,
@@ -66,6 +68,9 @@ async function main() {
66
68
  codeLocation?: string;
67
69
  tools?: { type: string; name: string }[];
68
70
  apiKeyArn?: string;
71
+ efsAccessPoints?: { accessPointArn: string; mountPath: string }[];
72
+ s3AccessPoints?: { accessPointArn: string; mountPath: string }[];
73
+ apiFormat?: 'converse_stream' | 'responses' | 'chat_completions';
69
74
  }[] = [];
70
75
  for (const entry of specAny.harnesses ?? []) {
71
76
  const harnessDir = path.resolve(projectRoot, entry.path);
@@ -82,6 +87,9 @@ async function main() {
82
87
  codeLocation: harnessSpec.dockerfile ? harnessDir : undefined,
83
88
  tools: harnessSpec.tools,
84
89
  apiKeyArn: harnessSpec.model?.apiKeyArn,
90
+ efsAccessPoints: harnessSpec.efsAccessPoints,
91
+ s3AccessPoints: harnessSpec.s3AccessPoints,
92
+ apiFormat: harnessSpec.model?.apiFormat,
85
93
  });
86
94
  } catch (err) {
87
95
  throw new Error(
@@ -105,11 +113,51 @@ async function main() {
105
113
  | Record<string, { credentialProviderArn: string; clientSecretArn?: string }>
106
114
  | undefined;
107
115
 
116
+ // Payment credential provider ARNs live in the same credentials map as identity credentials
117
+ const paymentCredentials = credentials;
118
+
119
+ const paymentSpec = specAny.payments?.length
120
+ ? specAny.payments.map(
121
+ (p: {
122
+ name: string;
123
+ description?: string;
124
+ authorizerType: 'AWS_IAM' | 'CUSTOM_JWT';
125
+ authorizerConfiguration?: unknown;
126
+ autoPayment?: boolean;
127
+ paymentToolAllowlist?: string[];
128
+ networkPreferences?: string[];
129
+ connectors: { name: string; provider?: string; credentialName: string }[];
130
+ }) => ({
131
+ name: p.name,
132
+ description: p.description,
133
+ authorizerType: p.authorizerType,
134
+ authorizerConfiguration: p.authorizerConfiguration,
135
+ autoPayment: p.autoPayment,
136
+ paymentToolAllowlist: p.paymentToolAllowlist,
137
+ networkPreferences: p.networkPreferences,
138
+ connectors: p.connectors.map(c => {
139
+ const credentialProviderArn = paymentCredentials?.[c.credentialName]?.credentialProviderArn;
140
+ if (!credentialProviderArn) {
141
+ // Fail fast with an actionable message rather than passing an empty
142
+ // ARN that fails opaquely server-side at CreatePaymentConnector.
143
+ throw new Error(
144
+ `Payment connector "${c.name}" on manager "${p.name}" references credential ` +
145
+ `"${c.credentialName}", but no deployed credential provider was found for it. ` +
146
+ `Run \`agentcore deploy\` so the credential provider is created first.`
147
+ );
148
+ }
149
+ return { name: c.name, provider: c.provider, credentialProviderArn };
150
+ }),
151
+ })
152
+ )
153
+ : undefined;
154
+
108
155
  new AgentCoreStack(app, stackName, {
109
156
  spec,
110
157
  mcpSpec,
111
158
  credentials,
112
159
  harnesses: harnessConfigs.length > 0 ? harnessConfigs : undefined,
160
+ paymentSpec,
113
161
  env,
114
162
  description: `AgentCore stack for ${spec.name} deployed to ${target.name} (${target.region})`,
115
163
  tags: {
@@ -124,5 +172,5 @@ async function main() {
124
172
 
125
173
  main().catch((error: unknown) => {
126
174
  console.error('AgentCore CDK synthesis failed:', error instanceof Error ? error.message : error);
127
- process.exitCode = 1;
175
+ process.exit(1);
128
176
  });
@@ -1,10 +1,14 @@
1
1
  import {
2
2
  AgentCoreApplication,
3
3
  AgentCoreMcp,
4
+ AgentCorePaymentManager,
5
+ AgentCorePaymentConnector,
4
6
  type AgentCoreProjectSpec,
5
7
  type AgentCoreMcpSpec,
8
+ type CustomJWTAuthorizerConfig,
6
9
  } from '@aws/agentcore-cdk';
7
10
  import { CfnOutput, Stack, type StackProps } from 'aws-cdk-lib';
11
+ import * as iam from 'aws-cdk-lib/aws-iam';
8
12
  import { Construct } from 'constructs';
9
13
 
10
14
  export interface HarnessConfig {
@@ -17,6 +21,25 @@ export interface HarnessConfig {
17
21
  codeLocation?: string;
18
22
  tools?: { type: string; name: string }[];
19
23
  apiKeyArn?: string;
24
+ efsAccessPoints?: { accessPointArn: string; mountPath: string }[];
25
+ s3AccessPoints?: { accessPointArn: string; mountPath: string }[];
26
+ }
27
+
28
+ export interface PaymentConnectorSpec {
29
+ name: string;
30
+ provider: 'CoinbaseCDP' | 'StripePrivy';
31
+ credentialProviderArn: string;
32
+ }
33
+
34
+ export interface PaymentSpec {
35
+ name: string;
36
+ description?: string;
37
+ authorizerType: 'AWS_IAM' | 'CUSTOM_JWT';
38
+ authorizerConfiguration?: { customJWTAuthorizer: CustomJWTAuthorizerConfig };
39
+ autoPayment?: boolean;
40
+ paymentToolAllowlist?: string[];
41
+ networkPreferences?: string[];
42
+ connectors: PaymentConnectorSpec[];
20
43
  }
21
44
 
22
45
  export interface AgentCoreStackProps extends StackProps {
@@ -36,6 +59,30 @@ export interface AgentCoreStackProps extends StackProps {
36
59
  * Harness role configurations.
37
60
  */
38
61
  harnesses?: HarnessConfig[];
62
+ /**
63
+ * Payment specifications with resolved credential provider ARNs.
64
+ */
65
+ paymentSpec?: PaymentSpec[];
66
+ }
67
+
68
+ function toCdkId(name: string): string {
69
+ return name.replace(/_/g, '');
70
+ }
71
+
72
+ /**
73
+ * Decide whether a deployed runtime should receive payment env vars + IAM grants.
74
+ * Payments today only ships a runtime shim for Python HTTP runtimes; injecting
75
+ * AGENTCORE_PAYMENT_* env vars into TypeScript / MCP / A2A / AGUI runtimes
76
+ * would surface env vars they cannot consume and would dilute least-privilege
77
+ * IAM grants for runtimes that never call ProcessPayment.
78
+ */
79
+ function isPaymentEligibleAgent(agent: { entrypoint?: string; protocol?: string }): boolean {
80
+ if (agent.protocol && agent.protocol !== 'HTTP') {
81
+ return false;
82
+ }
83
+ const entrypoint = typeof agent.entrypoint === 'string' ? agent.entrypoint : '';
84
+ const entrypointFile = entrypoint.split(':')[0] ?? '';
85
+ return entrypointFile.endsWith('.py');
39
86
  }
40
87
 
41
88
  /**
@@ -51,7 +98,7 @@ export class AgentCoreStack extends Stack {
51
98
  constructor(scope: Construct, id: string, props: AgentCoreStackProps) {
52
99
  super(scope, id, props);
53
100
 
54
- const { spec, mcpSpec, credentials, harnesses } = props;
101
+ const { spec, mcpSpec, credentials, harnesses, paymentSpec } = props;
55
102
 
56
103
  // Create AgentCoreApplication with all agents and harness roles
57
104
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -72,6 +119,122 @@ export class AgentCoreStack extends Stack {
72
119
  });
73
120
  }
74
121
 
122
+ // Create payment infrastructure via CFN constructs
123
+ if (paymentSpec && paymentSpec.length > 0) {
124
+ for (const payment of paymentSpec) {
125
+ const mgrId = toCdkId(payment.name);
126
+ const manager = new AgentCorePaymentManager(this, `Payment${mgrId}`, {
127
+ projectName: spec.name,
128
+ name: payment.name,
129
+ authorizerType: payment.authorizerType,
130
+ description: payment.description,
131
+ authorizerConfiguration: payment.authorizerConfiguration,
132
+ tags: spec.tags,
133
+ });
134
+
135
+ const prefix = `AGENTCORE_PAYMENT_${payment.name.toUpperCase().replace(/-/g, '_')}`;
136
+
137
+ // Wire env vars from construct output tokens into eligible agent environments only.
138
+ // See isPaymentEligibleAgent — non-Python or non-HTTP runtimes have no shim that
139
+ // can consume these env vars, and giving them sts:AssumeRole on the
140
+ // ProcessPaymentRole would broaden the privilege surface unnecessarily.
141
+ for (const env of this.application.environments.values()) {
142
+ if (!isPaymentEligibleAgent(env.agent)) {
143
+ continue;
144
+ }
145
+ env.runtime.addEnvironmentVariable(`${prefix}_MANAGER_ARN`, manager.paymentManagerArn);
146
+ env.runtime.addEnvironmentVariable(`${prefix}_PROCESS_PAYMENT_ROLE_ARN`, manager.processPaymentRoleArn);
147
+
148
+ // Grant runtime execution role permission to assume the ProcessPaymentRole.
149
+ // The ProcessPaymentRole's trust policy allows AccountRootPrincipal, but the
150
+ // caller still needs sts:AssumeRole on its own role to perform the assumption.
151
+ env.runtime.role.addToPrincipalPolicy(
152
+ new iam.PolicyStatement({
153
+ actions: ['sts:AssumeRole'],
154
+ resources: [manager.processPaymentRoleArn],
155
+ })
156
+ );
157
+
158
+ // Grant payment data-plane actions directly to the runtime role.
159
+ //
160
+ // NOTE: This deviates from the canonical role model in the AgentCore Payments
161
+ // beta guide, which assigns Get/List/Create instrument+session actions to a
162
+ // separate ManagementRole and limits the agent's role to ProcessPayment only.
163
+ // The current SDK plugin (AgentCorePaymentsPlugin.generate_payment_header)
164
+ // calls GetPaymentInstrument internally during the 402 auto-pay path, so the
165
+ // runtime role needs read access. CreatePaymentSession is included so
166
+ // `agentcore invoke --auto-session` works without a separate ManagementRole
167
+ // call. Tighten this if the SDK is updated to accept pre-fetched instrument
168
+ // details and split create-session into a backend-only flow.
169
+ env.runtime.role.addToPrincipalPolicy(
170
+ new iam.PolicyStatement({
171
+ actions: [
172
+ 'bedrock-agentcore:GetPaymentInstrument',
173
+ 'bedrock-agentcore:ListPaymentInstruments',
174
+ 'bedrock-agentcore:GetPaymentInstrumentBalance',
175
+ 'bedrock-agentcore:GetPaymentSession',
176
+ 'bedrock-agentcore:ListPaymentSessions',
177
+ 'bedrock-agentcore:CreatePaymentSession',
178
+ 'bedrock-agentcore:ProcessPayment',
179
+ ],
180
+ resources: [manager.paymentManagerArn, `${manager.paymentManagerArn}/*`],
181
+ })
182
+ );
183
+
184
+ if (payment.autoPayment !== undefined) {
185
+ env.runtime.addEnvironmentVariable(`${prefix}_AUTO_PAYMENT`, String(payment.autoPayment));
186
+ }
187
+ if (payment.paymentToolAllowlist) {
188
+ env.runtime.addEnvironmentVariable(`${prefix}_TOOL_ALLOWLIST`, payment.paymentToolAllowlist.join(','));
189
+ }
190
+ if (payment.networkPreferences) {
191
+ env.runtime.addEnvironmentVariable(`${prefix}_NETWORK_PREFERENCES`, payment.networkPreferences.join(','));
192
+ }
193
+ if (payment.authorizerType === 'CUSTOM_JWT') {
194
+ env.runtime.addEnvironmentVariable(`${prefix}_AUTH_MODE`, 'bearer');
195
+ }
196
+ }
197
+
198
+ // Create connectors for this manager
199
+ for (const connector of payment.connectors) {
200
+ const connId = toCdkId(connector.name);
201
+ const conn = new AgentCorePaymentConnector(this, `Payment${mgrId}${connId}`, {
202
+ projectName: spec.name,
203
+ paymentManager: manager,
204
+ connectorName: connector.name,
205
+ connectorType: connector.provider,
206
+ credentialProviderArn: connector.credentialProviderArn,
207
+ });
208
+
209
+ // Wire first connector's ID as env var (eligible agents only)
210
+ if (connector === payment.connectors[0]) {
211
+ for (const env of this.application.environments.values()) {
212
+ if (!isPaymentEligibleAgent(env.agent)) continue;
213
+ env.runtime.addEnvironmentVariable(`${prefix}_CONNECTOR_ID`, conn.paymentConnectorId);
214
+ }
215
+ }
216
+
217
+ new CfnOutput(this, `Payment${mgrId}${connId}ConnectorId`, {
218
+ value: conn.paymentConnectorId,
219
+ });
220
+ }
221
+
222
+ // CFN Outputs for post-deploy state parsing
223
+ new CfnOutput(this, `Payment${mgrId}ManagerArn`, {
224
+ value: manager.paymentManagerArn,
225
+ });
226
+ new CfnOutput(this, `Payment${mgrId}ManagerId`, {
227
+ value: manager.paymentManagerId,
228
+ });
229
+ new CfnOutput(this, `Payment${mgrId}ProcessPaymentRoleArn`, {
230
+ value: manager.processPaymentRoleArn,
231
+ });
232
+ new CfnOutput(this, `Payment${mgrId}ResourceRetrievalRoleArn`, {
233
+ value: manager.resourceRetrievalRoleArn,
234
+ });
235
+ }
236
+ }
237
+
75
238
  // Stack-level output
76
239
  new CfnOutput(this, 'StackNameOutput', {
77
240
  description: 'Name of the CloudFormation Stack',
@@ -18,7 +18,7 @@
18
18
  "@types/node": "^24.10.1",
19
19
  "jest": "^29.7.0",
20
20
  "ts-jest": "^29.2.5",
21
- "aws-cdk": "2.1100.1",
21
+ "aws-cdk": "2.1126.0",
22
22
  "prettier": "^3.4.2",
23
23
  "typescript": "~5.9.3"
24
24
  },
@@ -15,6 +15,8 @@ test('AgentCoreStack synthesizes with empty spec', () => {
15
15
  evaluators: [],
16
16
  onlineEvalConfigs: [],
17
17
  policyEngines: [],
18
+ payments: [],
19
+ configBundles: [],
18
20
  agentCoreGateways: [],
19
21
  mcpRuntimeTools: [],
20
22
  unassignedTargets: [],
@@ -1,4 +1,6 @@
1
+ {{#if needsOs}}
1
2
  import os
3
+ {{/if}}
2
4
  from google.adk.agents import Agent
3
5
  from google.adk.a2a.executor.a2a_agent_executor import A2aAgentExecutor
4
6
  from google.adk.runners import Runner
@@ -17,18 +19,21 @@ def add_numbers(a: int, b: int) -> int:
17
19
 
18
20
  tools = [add_numbers]
19
21
 
20
- {{#if sessionStorageMountPath}}
21
- SESSION_STORAGE_PATH = "{{sessionStorageMountPath}}"
22
+ {{#if needsOs}}
23
+ _MOUNT_PATHS = [
24
+ {{#if sessionStorageMountPath}}"{{sessionStorageMountPath}}",{{/if}}
25
+ {{#each efsMounts}}"{{mountPath}}",{{/each}}
26
+ {{#each s3Mounts}}"{{mountPath}}",{{/each}}
27
+ ]
22
28
 
23
29
  def _safe_resolve(path: str) -> str:
24
- """Resolve path safely within the storage boundary."""
25
- resolved = os.path.realpath(os.path.join(SESSION_STORAGE_PATH, path.lstrip("/")))
26
- if not resolved.startswith(os.path.realpath(SESSION_STORAGE_PATH)):
27
- raise ValueError(f"Path '{path}' is outside the storage boundary")
30
+ resolved = os.path.realpath(path)
31
+ if not any(resolved == os.path.realpath(m) or resolved.startswith(os.path.realpath(m) + os.sep) for m in _MOUNT_PATHS):
32
+ raise ValueError(f"Path '{path}' is not within any configured mount ({', '.join(_MOUNT_PATHS)})")
28
33
  return resolved
29
34
 
30
35
  def file_read(path: str) -> str:
31
- """Read a file from persistent storage. The path is relative to the storage root."""
36
+ """Read a file from a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
32
37
  try:
33
38
  full_path = _safe_resolve(path)
34
39
  with open(full_path) as f:
@@ -39,7 +44,7 @@ def file_read(path: str) -> str:
39
44
  return f"Error reading '{path}': {e.strerror}"
40
45
 
41
46
  def file_write(path: str, content: str) -> str:
42
- """Write content to a file in persistent storage. The path is relative to the storage root."""
47
+ """Write a file to a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
43
48
  try:
44
49
  full_path = _safe_resolve(path)
45
50
  parent = os.path.dirname(full_path)
@@ -53,25 +58,28 @@ def file_write(path: str, content: str) -> str:
53
58
  except OSError as e:
54
59
  return f"Error writing '{path}': {e.strerror}"
55
60
 
56
- def list_files(directory: str = "") -> str:
57
- """List files in persistent storage. The directory is relative to the storage root."""
61
+ def list_files(path: str) -> str:
62
+ """List files in a mounted filesystem directory. Use the absolute path (e.g. /mnt/tools)."""
58
63
  try:
59
- target = _safe_resolve(directory)
60
- entries = os.listdir(target)
64
+ full_path = _safe_resolve(path)
65
+ entries = os.listdir(full_path)
61
66
  return "\n".join(entries) if entries else "(empty directory)"
62
67
  except ValueError as e:
63
68
  return str(e)
64
69
  except OSError as e:
65
- return f"Error listing '{directory}': {e.strerror}"
70
+ return f"Error listing '{path}': {e.strerror}"
66
71
 
67
72
  tools.extend([file_read, file_write, list_files])
68
73
  {{/if}}
69
74
 
70
75
  AGENT_INSTRUCTION = """
71
76
  You are a helpful assistant. Use tools when appropriate.
72
- {{#if sessionStorageMountPath}}
73
- You have persistent storage at {{sessionStorageMountPath}}. Use file tools to read and write files. Data persists across sessions.
74
- {{/if}}
77
+ {{#if needsOs}}
78
+ You have access to the following mounted filesystems. Use file_read, file_write, and list_files with full absolute paths:
79
+ {{#if sessionStorageMountPath}}- {{sessionStorageMountPath}}: ephemeral session storage (lost when session ends)
80
+ {{/if}}{{#each efsMounts}}- {{mountPath}}: EFS persistent storage (persists across sessions and agent restarts)
81
+ {{/each}}{{#each s3Mounts}}- {{mountPath}}: S3 Files persistent storage (durable, backed by S3)
82
+ {{/each}}{{/if}}
75
83
  """
76
84
 
77
85
  agent = Agent(
@@ -1,4 +1,6 @@
1
+ {{#if needsOs}}
1
2
  import os
3
+ {{/if}}
2
4
  from langchain_core.tools import tool
3
5
  from langgraph.prebuilt import create_react_agent
4
6
  from opentelemetry.instrumentation.langchain import LangchainInstrumentor
@@ -21,19 +23,22 @@ def add_numbers(a: int, b: int) -> int:
21
23
 
22
24
  tools = [add_numbers]
23
25
 
24
- {{#if sessionStorageMountPath}}
25
- SESSION_STORAGE_PATH = "{{sessionStorageMountPath}}"
26
+ {{#if needsOs}}
27
+ _MOUNT_PATHS = [
28
+ {{#if sessionStorageMountPath}}"{{sessionStorageMountPath}}",{{/if}}
29
+ {{#each efsMounts}}"{{mountPath}}",{{/each}}
30
+ {{#each s3Mounts}}"{{mountPath}}",{{/each}}
31
+ ]
26
32
 
27
33
  def _safe_resolve(path: str) -> str:
28
- """Resolve path safely within the storage boundary."""
29
- resolved = os.path.realpath(os.path.join(SESSION_STORAGE_PATH, path.lstrip("/")))
30
- if not resolved.startswith(os.path.realpath(SESSION_STORAGE_PATH)):
31
- raise ValueError(f"Path '{path}' is outside the storage boundary")
34
+ resolved = os.path.realpath(path)
35
+ if not any(resolved == os.path.realpath(m) or resolved.startswith(os.path.realpath(m) + os.sep) for m in _MOUNT_PATHS):
36
+ raise ValueError(f"Path '{path}' is not within any configured mount ({', '.join(_MOUNT_PATHS)})")
32
37
  return resolved
33
38
 
34
39
  @tool
35
40
  def file_read(path: str) -> str:
36
- """Read a file from persistent storage. The path is relative to the storage root."""
41
+ """Read a file from a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
37
42
  try:
38
43
  full_path = _safe_resolve(path)
39
44
  with open(full_path) as f:
@@ -45,7 +50,7 @@ def file_read(path: str) -> str:
45
50
 
46
51
  @tool
47
52
  def file_write(path: str, content: str) -> str:
48
- """Write content to a file in persistent storage. The path is relative to the storage root."""
53
+ """Write a file to a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
49
54
  try:
50
55
  full_path = _safe_resolve(path)
51
56
  parent = os.path.dirname(full_path)
@@ -60,25 +65,28 @@ def file_write(path: str, content: str) -> str:
60
65
  return f"Error writing '{path}': {e.strerror}"
61
66
 
62
67
  @tool
63
- def list_files(directory: str = "") -> str:
64
- """List files in persistent storage. The directory is relative to the storage root."""
68
+ def list_files(path: str) -> str:
69
+ """List files in a mounted filesystem directory. Use the absolute path (e.g. /mnt/tools)."""
65
70
  try:
66
- target = _safe_resolve(directory)
67
- entries = os.listdir(target)
71
+ full_path = _safe_resolve(path)
72
+ entries = os.listdir(full_path)
68
73
  return "\n".join(entries) if entries else "(empty directory)"
69
74
  except ValueError as e:
70
75
  return str(e)
71
76
  except OSError as e:
72
- return f"Error listing '{directory}': {e.strerror}"
77
+ return f"Error listing '{path}': {e.strerror}"
73
78
 
74
79
  tools.extend([file_read, file_write, list_files])
75
80
  {{/if}}
76
81
 
77
82
  SYSTEM_PROMPT = """
78
83
  You are a helpful assistant. Use tools when appropriate.
79
- {{#if sessionStorageMountPath}}
80
- You have persistent storage at {{sessionStorageMountPath}}. Use file tools to read and write files. Data persists across sessions.
81
- {{/if}}
84
+ {{#if needsOs}}
85
+ You have access to the following mounted filesystems. Use file_read, file_write, and list_files with full absolute paths:
86
+ {{#if sessionStorageMountPath}}- {{sessionStorageMountPath}}: ephemeral session storage (lost when session ends)
87
+ {{/if}}{{#each efsMounts}}- {{mountPath}}: EFS persistent storage (persists across sessions and agent restarts)
88
+ {{/each}}{{#each s3Mounts}}- {{mountPath}}: S3 Files persistent storage (durable, backed by S3)
89
+ {{/each}}{{/if}}
82
90
  """
83
91
 
84
92
  model = load_model()
@@ -17,7 +17,7 @@ dependencies = [
17
17
  {{/if}}{{#if (eq modelProvider "OpenAI")}}"langchain-openai >= 0.2.0",
18
18
  {{/if}}"aws-opentelemetry-distro",
19
19
  "opentelemetry-instrumentation-langchain >= 0.59.0",
20
- "bedrock-agentcore[a2a] >= 1.0.3",
20
+ "bedrock-agentcore[a2a] >= 1.8.0",
21
21
  "botocore[crt] >= 1.35.0",
22
22
  "langgraph >= 0.2.0",
23
23
  ]
@@ -5,7 +5,7 @@ from model.load import load_model
5
5
  {{#if hasMemory}}
6
6
  from memory.session import get_memory_session_manager
7
7
  {{/if}}
8
- {{#if sessionStorageMountPath}}
8
+ {{#if needsOs}}
9
9
  import os
10
10
  {{/if}}
11
11
 
@@ -18,19 +18,22 @@ def add_numbers(a: int, b: int) -> int:
18
18
 
19
19
  tools = [add_numbers]
20
20
 
21
- {{#if sessionStorageMountPath}}
22
- SESSION_STORAGE_PATH = "{{sessionStorageMountPath}}"
21
+ {{#if needsOs}}
22
+ _MOUNT_PATHS = [
23
+ {{#if sessionStorageMountPath}}"{{sessionStorageMountPath}}",{{/if}}
24
+ {{#each efsMounts}}"{{mountPath}}",{{/each}}
25
+ {{#each s3Mounts}}"{{mountPath}}",{{/each}}
26
+ ]
23
27
 
24
28
  def _safe_resolve(path: str) -> str:
25
- """Resolve path safely within the storage boundary."""
26
- resolved = os.path.realpath(os.path.join(SESSION_STORAGE_PATH, path.lstrip("/")))
27
- if not resolved.startswith(os.path.realpath(SESSION_STORAGE_PATH)):
28
- raise ValueError(f"Path '{path}' is outside the storage boundary")
29
+ resolved = os.path.realpath(path)
30
+ if not any(resolved == os.path.realpath(m) or resolved.startswith(os.path.realpath(m) + os.sep) for m in _MOUNT_PATHS):
31
+ raise ValueError(f"Path '{path}' is not within any configured mount ({', '.join(_MOUNT_PATHS)})")
29
32
  return resolved
30
33
 
31
34
  @tool
32
35
  def file_read(path: str) -> str:
33
- """Read a file from persistent storage. The path is relative to the storage root."""
36
+ """Read a file from a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
34
37
  try:
35
38
  full_path = _safe_resolve(path)
36
39
  with open(full_path) as f:
@@ -42,7 +45,7 @@ def file_read(path: str) -> str:
42
45
 
43
46
  @tool
44
47
  def file_write(path: str, content: str) -> str:
45
- """Write content to a file in persistent storage. The path is relative to the storage root."""
48
+ """Write a file to a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
46
49
  try:
47
50
  full_path = _safe_resolve(path)
48
51
  parent = os.path.dirname(full_path)
@@ -57,25 +60,28 @@ def file_write(path: str, content: str) -> str:
57
60
  return f"Error writing '{path}': {e.strerror}"
58
61
 
59
62
  @tool
60
- def list_files(directory: str = "") -> str:
61
- """List files in persistent storage. The directory is relative to the storage root."""
63
+ def list_files(path: str) -> str:
64
+ """List files in a mounted filesystem directory. Use the absolute path (e.g. /mnt/tools)."""
62
65
  try:
63
- target = _safe_resolve(directory)
64
- entries = os.listdir(target)
66
+ full_path = _safe_resolve(path)
67
+ entries = os.listdir(full_path)
65
68
  return "\n".join(entries) if entries else "(empty directory)"
66
69
  except ValueError as e:
67
70
  return str(e)
68
71
  except OSError as e:
69
- return f"Error listing '{directory}': {e.strerror}"
72
+ return f"Error listing '{path}': {e.strerror}"
70
73
 
71
74
  tools.extend([file_read, file_write, list_files])
72
75
  {{/if}}
73
76
 
74
77
  SYSTEM_PROMPT = """
75
78
  You are a helpful assistant. Use tools when appropriate.
76
- {{#if sessionStorageMountPath}}
77
- You have persistent storage at {{sessionStorageMountPath}}. Use file tools to read and write files. Data persists across sessions.
78
- {{/if}}
79
+ {{#if needsOs}}
80
+ You have access to the following mounted filesystems. Use file_read, file_write, and list_files with full absolute paths:
81
+ {{#if sessionStorageMountPath}}- {{sessionStorageMountPath}}: ephemeral session storage (lost when session ends)
82
+ {{/if}}{{#each efsMounts}}- {{mountPath}}: EFS persistent storage (persists across sessions and agent restarts)
83
+ {{/each}}{{#each s3Mounts}}- {{mountPath}}: S3 Files persistent storage (durable, backed by S3)
84
+ {{/each}}{{/if}}
79
85
  """
80
86
 
81
87
  {{#if hasMemory}}
@@ -1,16 +1,76 @@
1
1
  import os
2
2
  import uvicorn
3
3
  from google.adk.agents import LlmAgent
4
+ from google.adk.tools import FunctionTool
4
5
  from ag_ui_adk import ADKAgent, AGUIToolset, create_adk_app
5
6
  from model.load import load_model
6
7
 
7
8
  load_model()
8
9
 
10
+ {{#if needsOs}}
11
+ _MOUNT_PATHS = [
12
+ {{#if sessionStorageMountPath}}"{{sessionStorageMountPath}}",{{/if}}
13
+ {{#each efsMounts}}"{{mountPath}}",{{/each}}
14
+ {{#each s3Mounts}}"{{mountPath}}",{{/each}}
15
+ ]
16
+
17
+ def _safe_resolve(path: str) -> str:
18
+ resolved = os.path.realpath(path)
19
+ if not any(resolved == os.path.realpath(m) or resolved.startswith(os.path.realpath(m) + os.sep) for m in _MOUNT_PATHS):
20
+ raise ValueError(f"Path '{path}' is not within any configured mount ({', '.join(_MOUNT_PATHS)})")
21
+ return resolved
22
+
23
+ def file_read(path: str) -> str:
24
+ """Read a file from a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
25
+ try:
26
+ full_path = _safe_resolve(path)
27
+ with open(full_path) as f:
28
+ return f.read()
29
+ except ValueError as e:
30
+ return str(e)
31
+ except OSError as e:
32
+ return f"Error reading '{path}': {e.strerror}"
33
+
34
+ def file_write(path: str, content: str) -> str:
35
+ """Write a file to a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
36
+ try:
37
+ full_path = _safe_resolve(path)
38
+ parent = os.path.dirname(full_path)
39
+ if parent:
40
+ os.makedirs(parent, exist_ok=True)
41
+ with open(full_path, "w") as f:
42
+ f.write(content)
43
+ return f"Written to {path}"
44
+ except ValueError as e:
45
+ return str(e)
46
+ except OSError as e:
47
+ return f"Error writing '{path}': {e.strerror}"
48
+
49
+ def list_files(path: str) -> str:
50
+ """List files in a mounted filesystem directory. Use the absolute path (e.g. /mnt/tools)."""
51
+ try:
52
+ full_path = _safe_resolve(path)
53
+ entries = os.listdir(full_path)
54
+ return "\n".join(entries) if entries else "(empty directory)"
55
+ except ValueError as e:
56
+ return str(e)
57
+ except OSError as e:
58
+ return f"Error listing '{path}': {e.strerror}"
59
+
60
+ _fs_tools = [FunctionTool(file_read), FunctionTool(file_write), FunctionTool(list_files)]
61
+ {{/if}}
62
+
9
63
  agent = LlmAgent(
10
64
  name="{{ name }}",
11
65
  model="gemini-2.5-flash",
12
- instruction="You are a helpful assistant.",
13
- tools=[AGUIToolset()],
66
+ instruction="""You are a helpful assistant.
67
+ {{#if needsOs}}
68
+ You have access to the following mounted filesystems. Use file_read, file_write, and list_files with full absolute paths:
69
+ {{#if sessionStorageMountPath}}- {{sessionStorageMountPath}}: ephemeral session storage (lost when session ends)
70
+ {{/if}}{{#each efsMounts}}- {{mountPath}}: EFS persistent storage (persists across sessions and agent restarts)
71
+ {{/each}}{{#each s3Mounts}}- {{mountPath}}: S3 Files persistent storage (durable, backed by S3)
72
+ {{/each}}{{/if}}""",
73
+ tools=[AGUIToolset(), {{#if needsOs}}*_fs_tools{{/if}}],
14
74
  )
15
75
 
16
76
  adk_agent = ADKAgent(