@aws/agentcore 1.0.0-preview.12 → 1.0.0-preview.14
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.
- package/README.md +31 -17
- package/dist/agent-inspector/index.css +1 -1
- package/dist/agent-inspector/index.js +90 -85
- package/dist/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap +1373 -113
- package/dist/assets/agents/AGENTS.md +14 -0
- package/dist/assets/cdk/bin/cdk.ts +90 -24
- package/dist/assets/cdk/lib/cdk-stack.ts +178 -12
- package/dist/assets/cdk/test/cdk.test.ts +3 -0
- package/dist/assets/container/python/Dockerfile +4 -0
- package/dist/assets/python/a2a/langchain_langgraph/base/pyproject.toml +1 -1
- package/dist/assets/python/a2a/strands/capabilities/memory/session.py +4 -1
- package/dist/assets/python/agui/googleadk/base/pyproject.toml +1 -1
- package/dist/assets/python/agui/langchain_langgraph/base/pyproject.toml +1 -1
- package/dist/assets/python/agui/strands/capabilities/memory/session.py +4 -1
- package/dist/assets/python/http/autogen/base/pyproject.toml +1 -1
- package/dist/assets/python/http/googleadk/base/mcp_client/client.py +7 -7
- package/dist/assets/python/http/googleadk/base/pyproject.toml +1 -1
- package/dist/assets/python/http/langchain_langgraph/base/mcp_client/client.py +10 -8
- package/dist/assets/python/http/langchain_langgraph/base/pyproject.toml +2 -2
- package/dist/assets/python/http/openaiagents/base/main.py +19 -9
- package/dist/assets/python/http/openaiagents/base/mcp_client/client.py +4 -4
- package/dist/assets/python/http/openaiagents/base/pyproject.toml +2 -2
- package/dist/assets/python/http/strands/base/main.py +475 -27
- package/dist/assets/python/http/strands/base/mcp_client/client.py +45 -5
- package/dist/assets/python/http/strands/base/model/load.py +3 -3
- package/dist/assets/python/http/strands/base/pyproject.toml +5 -2
- package/dist/assets/python/http/strands/base/skills/fetcher.py +279 -0
- package/dist/assets/python/http/strands/capabilities/execution-limits/hooks/execution_limits.py +54 -0
- package/dist/assets/python/http/strands/capabilities/memory/session.py +4 -1
- package/dist/assets/python/http/strands/capabilities/payments/__init__.py +4 -0
- package/dist/assets/python/http/strands/capabilities/payments/payments.py +142 -0
- package/dist/cli/index.mjs +1235 -983
- package/dist/lib/errors/types.d.ts +18 -0
- package/dist/lib/errors/types.d.ts.map +1 -1
- package/dist/lib/errors/types.js +28 -1
- package/dist/lib/errors/types.js.map +1 -1
- package/dist/lib/packaging/helpers.d.ts.map +1 -1
- package/dist/lib/packaging/helpers.js +50 -10
- package/dist/lib/packaging/helpers.js.map +1 -1
- package/dist/lib/schemas/io/config-io.d.ts.map +1 -1
- package/dist/lib/schemas/io/config-io.js +0 -2
- package/dist/lib/schemas/io/config-io.js.map +1 -1
- package/dist/lib/utils/env.d.ts +4 -0
- package/dist/lib/utils/env.d.ts.map +1 -1
- package/dist/lib/utils/env.js +18 -0
- package/dist/lib/utils/env.js.map +1 -1
- package/dist/lib/utils/index.d.ts +1 -1
- package/dist/lib/utils/index.d.ts.map +1 -1
- package/dist/lib/utils/index.js +2 -1
- package/dist/lib/utils/index.js.map +1 -1
- package/dist/schema/constants.d.ts +20 -0
- package/dist/schema/constants.d.ts.map +1 -1
- package/dist/schema/constants.js +25 -1
- package/dist/schema/constants.js.map +1 -1
- package/dist/schema/schemas/agent-env.d.ts +35 -0
- package/dist/schema/schemas/agent-env.d.ts.map +1 -1
- package/dist/schema/schemas/agentcore-project.d.ts +239 -25
- package/dist/schema/schemas/agentcore-project.d.ts.map +1 -1
- package/dist/schema/schemas/agentcore-project.js +140 -34
- package/dist/schema/schemas/agentcore-project.js.map +1 -1
- package/dist/schema/schemas/auth.d.ts +175 -0
- package/dist/schema/schemas/auth.d.ts.map +1 -1
- package/dist/schema/schemas/auth.js +126 -3
- package/dist/schema/schemas/auth.js.map +1 -1
- package/dist/schema/schemas/deployed-state.d.ts +237 -46
- package/dist/schema/schemas/deployed-state.d.ts.map +1 -1
- package/dist/schema/schemas/deployed-state.js +62 -17
- package/dist/schema/schemas/deployed-state.js.map +1 -1
- package/dist/schema/schemas/mcp-defs.d.ts +2 -1
- package/dist/schema/schemas/mcp-defs.d.ts.map +1 -1
- package/dist/schema/schemas/mcp-defs.js +3 -2
- package/dist/schema/schemas/mcp-defs.js.map +1 -1
- package/dist/schema/schemas/mcp.d.ts +186 -0
- package/dist/schema/schemas/mcp.d.ts.map +1 -1
- package/dist/schema/schemas/mcp.js +479 -4
- package/dist/schema/schemas/mcp.js.map +1 -1
- package/dist/schema/schemas/primitives/ab-test.d.ts +0 -6
- package/dist/schema/schemas/primitives/ab-test.d.ts.map +1 -1
- package/dist/schema/schemas/primitives/ab-test.js +0 -2
- package/dist/schema/schemas/primitives/ab-test.js.map +1 -1
- package/dist/schema/schemas/primitives/harness.d.ts +242 -11
- package/dist/schema/schemas/primitives/harness.d.ts.map +1 -1
- package/dist/schema/schemas/primitives/harness.js +309 -21
- package/dist/schema/schemas/primitives/harness.js.map +1 -1
- package/dist/schema/schemas/primitives/index.d.ts +8 -8
- package/dist/schema/schemas/primitives/index.d.ts.map +1 -1
- package/dist/schema/schemas/primitives/index.js +18 -5
- package/dist/schema/schemas/primitives/index.js.map +1 -1
- package/dist/schema/schemas/primitives/knowledge-base.d.ts +104 -0
- package/dist/schema/schemas/primitives/knowledge-base.d.ts.map +1 -0
- package/dist/schema/schemas/primitives/knowledge-base.js +99 -0
- package/dist/schema/schemas/primitives/knowledge-base.js.map +1 -0
- package/dist/schema/schemas/primitives/online-eval-config.d.ts +20 -2
- package/dist/schema/schemas/primitives/online-eval-config.d.ts.map +1 -1
- package/dist/schema/schemas/primitives/online-eval-config.js +43 -5
- package/dist/schema/schemas/primitives/online-eval-config.js.map +1 -1
- package/dist/schema/schemas/primitives/payment.d.ts +54 -0
- package/dist/schema/schemas/primitives/payment.d.ts.map +1 -0
- package/dist/schema/schemas/primitives/payment.js +79 -0
- package/dist/schema/schemas/primitives/payment.js.map +1 -0
- package/dist/schema/schemas/primitives/policy.d.ts +26 -0
- package/dist/schema/schemas/primitives/policy.d.ts.map +1 -1
- package/dist/schema/schemas/primitives/policy.js +5 -1
- package/dist/schema/schemas/primitives/policy.js.map +1 -1
- package/npm-shrinkwrap.json +941 -670
- package/package.json +11 -8
- package/scripts/bundle.mjs +51 -0
- package/scripts/run-e2e-local.sh +48 -10
- package/dist/schema/schemas/primitives/http-gateway.d.ts +0 -21
- package/dist/schema/schemas/primitives/http-gateway.d.ts.map +0 -1
- package/dist/schema/schemas/primitives/http-gateway.js +0 -34
- package/dist/schema/schemas/primitives/http-gateway.js.map +0 -1
|
@@ -44,8 +44,8 @@ agentcore status # checks deployment status
|
|
|
44
44
|
|
|
45
45
|
exports[`Assets Directory Snapshots > CDK assets > cdk/cdk/bin/cdk.ts should match snapshot 1`] = `
|
|
46
46
|
"#!/usr/bin/env node
|
|
47
|
-
import { AgentCoreStack } from '../lib/cdk-stack';
|
|
48
|
-
import { ConfigIO, type AwsDeploymentTarget } from '@aws/agentcore-cdk';
|
|
47
|
+
import { AgentCoreStack, type HarnessConfig } from '../lib/cdk-stack';
|
|
48
|
+
import { ConfigIO, HarnessSpecSchema, type AwsDeploymentTarget } from '@aws/agentcore-cdk';
|
|
49
49
|
import { App, type Environment } from 'aws-cdk-lib';
|
|
50
50
|
import * as path from 'path';
|
|
51
51
|
import * as fs from 'fs';
|
|
@@ -73,12 +73,14 @@ async function main() {
|
|
|
73
73
|
const spec = await configIO.readProjectSpec();
|
|
74
74
|
const targets = await configIO.readAWSDeploymentTargets();
|
|
75
75
|
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
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,
|
|
@@ -99,38 +101,61 @@ async function main() {
|
|
|
99
101
|
throw new Error('No deployment targets configured. Please define targets in agentcore/aws-targets.json');
|
|
100
102
|
}
|
|
101
103
|
|
|
102
|
-
// Read harness configs
|
|
104
|
+
// Read harness configs: the full validated spec drives the CFN resource; the
|
|
105
|
+
// role-scoped fields drive the IAM role + container build.
|
|
103
106
|
const projectRoot = path.resolve(configRoot, '..');
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
107
|
+
|
|
108
|
+
// Read non-S3 KB connector-config files and pass their parsed contents to the
|
|
109
|
+
// L3 verbatim. The L3 does not read files; it expects the parsed
|
|
110
|
+
// connectorParameters keyed by the data source's connectorConfigFile path.
|
|
111
|
+
const connectorParametersByFile: Record<string, Record<string, unknown>> = {};
|
|
112
|
+
for (const kb of specAny.knowledgeBases ?? []) {
|
|
113
|
+
for (const ds of kb.dataSources ?? []) {
|
|
114
|
+
if (ds.type !== 'S3' && ds.connectorConfigFile) {
|
|
115
|
+
const abs = path.resolve(projectRoot, ds.connectorConfigFile);
|
|
116
|
+
try {
|
|
117
|
+
connectorParametersByFile[ds.connectorConfigFile] = JSON.parse(fs.readFileSync(abs, 'utf-8'));
|
|
118
|
+
} catch (err) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
\`Could not read connector config '\${ds.connectorConfigFile}' for knowledge base '\${kb.name}' at \${abs}: \${err instanceof Error ? err.message : err}\`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Harness is preview-gated. The CLI bundle bakes the preview flag at build time and
|
|
128
|
+
// forwards it to this child process via AGENTCORE_PREVIEW (see toolkit-lib/wrapper.ts).
|
|
129
|
+
// This app is built separately and cannot see that build-time define, so it gates on the
|
|
130
|
+
// env var. Absent/anything-but-'1' defaults to off so a stale harnesses[] entry in a
|
|
131
|
+
// non-preview build never synthesizes an AWS::BedrockAgentCore::Harness resource.
|
|
132
|
+
const previewEnabled = process.env.AGENTCORE_PREVIEW === '1';
|
|
133
|
+
|
|
134
|
+
const harnessConfigs: HarnessConfig[] = [];
|
|
135
|
+
for (const entry of previewEnabled ? (specAny.harnesses ?? []) : []) {
|
|
118
136
|
const harnessDir = path.resolve(projectRoot, entry.path);
|
|
119
137
|
const harnessPath = path.resolve(harnessDir, 'harness.json');
|
|
120
138
|
try {
|
|
121
|
-
const harnessSpec = JSON.parse(fs.readFileSync(harnessPath, 'utf-8'));
|
|
139
|
+
const harnessSpec = HarnessSpecSchema.parse(JSON.parse(fs.readFileSync(harnessPath, 'utf-8')));
|
|
122
140
|
harnessConfigs.push({
|
|
123
141
|
name: entry.name,
|
|
124
142
|
executionRoleArn: harnessSpec.executionRoleArn,
|
|
125
|
-
|
|
143
|
+
// Only an \`existing\` memory ref carries a name to wire IAM against; managed memory is
|
|
144
|
+
// owned by the harness (no sibling) and disabled has none — both resolve to undefined.
|
|
145
|
+
memoryName: harnessSpec.memory?.mode === 'existing' ? harnessSpec.memory.name : undefined,
|
|
126
146
|
containerUri: harnessSpec.containerUri,
|
|
127
147
|
hasDockerfile: !!harnessSpec.dockerfile,
|
|
128
148
|
dockerfile: harnessSpec.dockerfile,
|
|
129
149
|
codeLocation: harnessSpec.dockerfile ? harnessDir : undefined,
|
|
130
150
|
tools: harnessSpec.tools,
|
|
151
|
+
skills: harnessSpec.skills,
|
|
131
152
|
apiKeyArn: harnessSpec.model?.apiKeyArn,
|
|
132
153
|
efsAccessPoints: harnessSpec.efsAccessPoints,
|
|
133
154
|
s3AccessPoints: harnessSpec.s3AccessPoints,
|
|
155
|
+
apiFormat: harnessSpec.model?.apiFormat,
|
|
156
|
+
// Full spec + dir drive the AWS::BedrockAgentCore::Harness CFN resource.
|
|
157
|
+
spec: harnessSpec,
|
|
158
|
+
harnessDir,
|
|
134
159
|
});
|
|
135
160
|
} catch (err) {
|
|
136
161
|
throw new Error(
|
|
@@ -154,11 +179,52 @@ async function main() {
|
|
|
154
179
|
| Record<string, { credentialProviderArn: string; clientSecretArn?: string }>
|
|
155
180
|
| undefined;
|
|
156
181
|
|
|
182
|
+
// Payment credential provider ARNs live in the same credentials map as identity credentials
|
|
183
|
+
const paymentCredentials = credentials;
|
|
184
|
+
|
|
185
|
+
const paymentSpec = specAny.payments?.length
|
|
186
|
+
? specAny.payments.map(
|
|
187
|
+
(p: {
|
|
188
|
+
name: string;
|
|
189
|
+
description?: string;
|
|
190
|
+
authorizerType: 'AWS_IAM' | 'CUSTOM_JWT';
|
|
191
|
+
authorizerConfiguration?: unknown;
|
|
192
|
+
autoPayment?: boolean;
|
|
193
|
+
paymentToolAllowlist?: string[];
|
|
194
|
+
networkPreferences?: string[];
|
|
195
|
+
connectors: { name: string; provider?: string; credentialName: string }[];
|
|
196
|
+
}) => ({
|
|
197
|
+
name: p.name,
|
|
198
|
+
description: p.description,
|
|
199
|
+
authorizerType: p.authorizerType,
|
|
200
|
+
authorizerConfiguration: p.authorizerConfiguration,
|
|
201
|
+
autoPayment: p.autoPayment,
|
|
202
|
+
paymentToolAllowlist: p.paymentToolAllowlist,
|
|
203
|
+
networkPreferences: p.networkPreferences,
|
|
204
|
+
connectors: p.connectors.map(c => {
|
|
205
|
+
const credentialProviderArn = paymentCredentials?.[c.credentialName]?.credentialProviderArn;
|
|
206
|
+
if (!credentialProviderArn) {
|
|
207
|
+
// Fail fast with an actionable message rather than passing an empty
|
|
208
|
+
// ARN that fails opaquely server-side at CreatePaymentConnector.
|
|
209
|
+
throw new Error(
|
|
210
|
+
\`Payment connector "\${c.name}" on manager "\${p.name}" references credential \` +
|
|
211
|
+
\`"\${c.credentialName}", but no deployed credential provider was found for it. \` +
|
|
212
|
+
\`Run \\\`agentcore deploy\\\` so the credential provider is created first.\`
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
return { name: c.name, provider: c.provider, credentialProviderArn };
|
|
216
|
+
}),
|
|
217
|
+
})
|
|
218
|
+
)
|
|
219
|
+
: undefined;
|
|
220
|
+
|
|
157
221
|
new AgentCoreStack(app, stackName, {
|
|
158
222
|
spec,
|
|
159
223
|
mcpSpec,
|
|
160
224
|
credentials,
|
|
225
|
+
connectorParametersByFile,
|
|
161
226
|
harnesses: harnessConfigs.length > 0 ? harnessConfigs : undefined,
|
|
227
|
+
paymentSpec,
|
|
162
228
|
env,
|
|
163
229
|
description: \`AgentCore stack for \${spec.name} deployed to \${target.name} (\${target.region})\`,
|
|
164
230
|
tags: {
|
|
@@ -173,7 +239,7 @@ async function main() {
|
|
|
173
239
|
|
|
174
240
|
main().catch((error: unknown) => {
|
|
175
241
|
console.error('AgentCore CDK synthesis failed:', error instanceof Error ? error.message : error);
|
|
176
|
-
process.
|
|
242
|
+
process.exit(1);
|
|
177
243
|
});
|
|
178
244
|
"
|
|
179
245
|
`;
|
|
@@ -300,24 +366,39 @@ exports[`Assets Directory Snapshots > CDK assets > cdk/cdk/lib/cdk-stack.ts shou
|
|
|
300
366
|
"import {
|
|
301
367
|
AgentCoreApplication,
|
|
302
368
|
AgentCoreMcp,
|
|
369
|
+
AgentCorePaymentManager,
|
|
370
|
+
AgentCorePaymentConnector,
|
|
303
371
|
type AgentCoreProjectSpec,
|
|
304
372
|
type AgentCoreMcpSpec,
|
|
373
|
+
type CustomJWTAuthorizerConfig,
|
|
374
|
+
type HarnessDeploymentConfig,
|
|
305
375
|
} from '@aws/agentcore-cdk';
|
|
306
376
|
import { CfnOutput, Stack, type StackProps } from 'aws-cdk-lib';
|
|
377
|
+
import * as iam from 'aws-cdk-lib/aws-iam';
|
|
307
378
|
import { Construct } from 'constructs';
|
|
308
379
|
|
|
309
|
-
|
|
380
|
+
/**
|
|
381
|
+
* Harness deployment config: role-scoped fields (for IAM role + container build)
|
|
382
|
+
* plus the full validated spec + its config directory so the L3 construct can
|
|
383
|
+
* synthesize the AWS::BedrockAgentCore::Harness resource.
|
|
384
|
+
*/
|
|
385
|
+
export type HarnessConfig = HarnessDeploymentConfig;
|
|
386
|
+
|
|
387
|
+
export interface PaymentConnectorSpec {
|
|
310
388
|
name: string;
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
389
|
+
provider: 'CoinbaseCDP' | 'StripePrivy';
|
|
390
|
+
credentialProviderArn: string;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export interface PaymentSpec {
|
|
394
|
+
name: string;
|
|
395
|
+
description?: string;
|
|
396
|
+
authorizerType: 'AWS_IAM' | 'CUSTOM_JWT';
|
|
397
|
+
authorizerConfiguration?: { customJWTAuthorizer: CustomJWTAuthorizerConfig };
|
|
398
|
+
autoPayment?: boolean;
|
|
399
|
+
paymentToolAllowlist?: string[];
|
|
400
|
+
networkPreferences?: string[];
|
|
401
|
+
connectors: PaymentConnectorSpec[];
|
|
321
402
|
}
|
|
322
403
|
|
|
323
404
|
export interface AgentCoreStackProps extends StackProps {
|
|
@@ -337,6 +418,35 @@ export interface AgentCoreStackProps extends StackProps {
|
|
|
337
418
|
* Harness role configurations.
|
|
338
419
|
*/
|
|
339
420
|
harnesses?: HarnessConfig[];
|
|
421
|
+
/**
|
|
422
|
+
* Parsed connectorParameters for non-S3 KB data sources, keyed by
|
|
423
|
+
* connectorConfigFile path. Forwarded to AgentCoreApplication.
|
|
424
|
+
*/
|
|
425
|
+
connectorParametersByFile?: Record<string, Record<string, unknown>>;
|
|
426
|
+
/**
|
|
427
|
+
* Payment specifications with resolved credential provider ARNs.
|
|
428
|
+
*/
|
|
429
|
+
paymentSpec?: PaymentSpec[];
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function toCdkId(name: string): string {
|
|
433
|
+
return name.replace(/_/g, '');
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Decide whether a deployed runtime should receive payment env vars + IAM grants.
|
|
438
|
+
* Payments today only ships a runtime shim for Python HTTP runtimes; injecting
|
|
439
|
+
* AGENTCORE_PAYMENT_* env vars into TypeScript / MCP / A2A / AGUI runtimes
|
|
440
|
+
* would surface env vars they cannot consume and would dilute least-privilege
|
|
441
|
+
* IAM grants for runtimes that never call ProcessPayment.
|
|
442
|
+
*/
|
|
443
|
+
function isPaymentEligibleAgent(agent: { entrypoint?: string; protocol?: string }): boolean {
|
|
444
|
+
if (agent.protocol && agent.protocol !== 'HTTP') {
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
const entrypoint = typeof agent.entrypoint === 'string' ? agent.entrypoint : '';
|
|
448
|
+
const entrypointFile = entrypoint.split(':')[0] ?? '';
|
|
449
|
+
return entrypointFile.endsWith('.py');
|
|
340
450
|
}
|
|
341
451
|
|
|
342
452
|
/**
|
|
@@ -352,7 +462,7 @@ export class AgentCoreStack extends Stack {
|
|
|
352
462
|
constructor(scope: Construct, id: string, props: AgentCoreStackProps) {
|
|
353
463
|
super(scope, id, props);
|
|
354
464
|
|
|
355
|
-
const { spec, mcpSpec, credentials, harnesses } = props;
|
|
465
|
+
const { spec, mcpSpec, credentials, harnesses, connectorParametersByFile, paymentSpec } = props;
|
|
356
466
|
|
|
357
467
|
// Create AgentCoreApplication with all agents and harness roles
|
|
358
468
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -360,6 +470,12 @@ export class AgentCoreStack extends Stack {
|
|
|
360
470
|
if (harnesses?.length) {
|
|
361
471
|
appProps.harnesses = harnesses;
|
|
362
472
|
}
|
|
473
|
+
if (connectorParametersByFile && Object.keys(connectorParametersByFile).length > 0) {
|
|
474
|
+
appProps.connectorParametersByFile = connectorParametersByFile;
|
|
475
|
+
}
|
|
476
|
+
if (credentials) {
|
|
477
|
+
appProps.credentials = credentials;
|
|
478
|
+
}
|
|
363
479
|
this.application = new AgentCoreApplication(this, 'Application', appProps as any);
|
|
364
480
|
|
|
365
481
|
// Create AgentCoreMcp if there are gateways configured
|
|
@@ -373,6 +489,122 @@ export class AgentCoreStack extends Stack {
|
|
|
373
489
|
});
|
|
374
490
|
}
|
|
375
491
|
|
|
492
|
+
// Create payment infrastructure via CFN constructs
|
|
493
|
+
if (paymentSpec && paymentSpec.length > 0) {
|
|
494
|
+
for (const payment of paymentSpec) {
|
|
495
|
+
const mgrId = toCdkId(payment.name);
|
|
496
|
+
const manager = new AgentCorePaymentManager(this, \`Payment\${mgrId}\`, {
|
|
497
|
+
projectName: spec.name,
|
|
498
|
+
name: payment.name,
|
|
499
|
+
authorizerType: payment.authorizerType,
|
|
500
|
+
description: payment.description,
|
|
501
|
+
authorizerConfiguration: payment.authorizerConfiguration,
|
|
502
|
+
tags: spec.tags,
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
const prefix = \`AGENTCORE_PAYMENT_\${payment.name.toUpperCase().replace(/-/g, '_')}\`;
|
|
506
|
+
|
|
507
|
+
// Wire env vars from construct output tokens into eligible agent environments only.
|
|
508
|
+
// See isPaymentEligibleAgent — non-Python or non-HTTP runtimes have no shim that
|
|
509
|
+
// can consume these env vars, and giving them sts:AssumeRole on the
|
|
510
|
+
// ProcessPaymentRole would broaden the privilege surface unnecessarily.
|
|
511
|
+
for (const env of this.application.environments.values()) {
|
|
512
|
+
if (!isPaymentEligibleAgent(env.agent)) {
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
env.runtime.addEnvironmentVariable(\`\${prefix}_MANAGER_ARN\`, manager.paymentManagerArn);
|
|
516
|
+
env.runtime.addEnvironmentVariable(\`\${prefix}_PROCESS_PAYMENT_ROLE_ARN\`, manager.processPaymentRoleArn);
|
|
517
|
+
|
|
518
|
+
// Grant runtime execution role permission to assume the ProcessPaymentRole.
|
|
519
|
+
// The ProcessPaymentRole's trust policy allows AccountRootPrincipal, but the
|
|
520
|
+
// caller still needs sts:AssumeRole on its own role to perform the assumption.
|
|
521
|
+
env.runtime.role.addToPrincipalPolicy(
|
|
522
|
+
new iam.PolicyStatement({
|
|
523
|
+
actions: ['sts:AssumeRole'],
|
|
524
|
+
resources: [manager.processPaymentRoleArn],
|
|
525
|
+
})
|
|
526
|
+
);
|
|
527
|
+
|
|
528
|
+
// Grant payment data-plane actions directly to the runtime role.
|
|
529
|
+
//
|
|
530
|
+
// NOTE: This deviates from the canonical role model in the AgentCore Payments
|
|
531
|
+
// beta guide, which assigns Get/List/Create instrument+session actions to a
|
|
532
|
+
// separate ManagementRole and limits the agent's role to ProcessPayment only.
|
|
533
|
+
// The current SDK plugin (AgentCorePaymentsPlugin.generate_payment_header)
|
|
534
|
+
// calls GetPaymentInstrument internally during the 402 auto-pay path, so the
|
|
535
|
+
// runtime role needs read access. CreatePaymentSession is included so
|
|
536
|
+
// \`agentcore invoke --auto-session\` works without a separate ManagementRole
|
|
537
|
+
// call. Tighten this if the SDK is updated to accept pre-fetched instrument
|
|
538
|
+
// details and split create-session into a backend-only flow.
|
|
539
|
+
env.runtime.role.addToPrincipalPolicy(
|
|
540
|
+
new iam.PolicyStatement({
|
|
541
|
+
actions: [
|
|
542
|
+
'bedrock-agentcore:GetPaymentInstrument',
|
|
543
|
+
'bedrock-agentcore:ListPaymentInstruments',
|
|
544
|
+
'bedrock-agentcore:GetPaymentInstrumentBalance',
|
|
545
|
+
'bedrock-agentcore:GetPaymentSession',
|
|
546
|
+
'bedrock-agentcore:ListPaymentSessions',
|
|
547
|
+
'bedrock-agentcore:CreatePaymentSession',
|
|
548
|
+
'bedrock-agentcore:ProcessPayment',
|
|
549
|
+
],
|
|
550
|
+
resources: [manager.paymentManagerArn, \`\${manager.paymentManagerArn}/*\`],
|
|
551
|
+
})
|
|
552
|
+
);
|
|
553
|
+
|
|
554
|
+
if (payment.autoPayment !== undefined) {
|
|
555
|
+
env.runtime.addEnvironmentVariable(\`\${prefix}_AUTO_PAYMENT\`, String(payment.autoPayment));
|
|
556
|
+
}
|
|
557
|
+
if (payment.paymentToolAllowlist) {
|
|
558
|
+
env.runtime.addEnvironmentVariable(\`\${prefix}_TOOL_ALLOWLIST\`, payment.paymentToolAllowlist.join(','));
|
|
559
|
+
}
|
|
560
|
+
if (payment.networkPreferences) {
|
|
561
|
+
env.runtime.addEnvironmentVariable(\`\${prefix}_NETWORK_PREFERENCES\`, payment.networkPreferences.join(','));
|
|
562
|
+
}
|
|
563
|
+
if (payment.authorizerType === 'CUSTOM_JWT') {
|
|
564
|
+
env.runtime.addEnvironmentVariable(\`\${prefix}_AUTH_MODE\`, 'bearer');
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Create connectors for this manager
|
|
569
|
+
for (const connector of payment.connectors) {
|
|
570
|
+
const connId = toCdkId(connector.name);
|
|
571
|
+
const conn = new AgentCorePaymentConnector(this, \`Payment\${mgrId}\${connId}\`, {
|
|
572
|
+
projectName: spec.name,
|
|
573
|
+
paymentManager: manager,
|
|
574
|
+
connectorName: connector.name,
|
|
575
|
+
connectorType: connector.provider,
|
|
576
|
+
credentialProviderArn: connector.credentialProviderArn,
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
// Wire first connector's ID as env var (eligible agents only)
|
|
580
|
+
if (connector === payment.connectors[0]) {
|
|
581
|
+
for (const env of this.application.environments.values()) {
|
|
582
|
+
if (!isPaymentEligibleAgent(env.agent)) continue;
|
|
583
|
+
env.runtime.addEnvironmentVariable(\`\${prefix}_CONNECTOR_ID\`, conn.paymentConnectorId);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
new CfnOutput(this, \`Payment\${mgrId}\${connId}ConnectorId\`, {
|
|
588
|
+
value: conn.paymentConnectorId,
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// CFN Outputs for post-deploy state parsing
|
|
593
|
+
new CfnOutput(this, \`Payment\${mgrId}ManagerArn\`, {
|
|
594
|
+
value: manager.paymentManagerArn,
|
|
595
|
+
});
|
|
596
|
+
new CfnOutput(this, \`Payment\${mgrId}ManagerId\`, {
|
|
597
|
+
value: manager.paymentManagerId,
|
|
598
|
+
});
|
|
599
|
+
new CfnOutput(this, \`Payment\${mgrId}ProcessPaymentRoleArn\`, {
|
|
600
|
+
value: manager.processPaymentRoleArn,
|
|
601
|
+
});
|
|
602
|
+
new CfnOutput(this, \`Payment\${mgrId}ResourceRetrievalRoleArn\`, {
|
|
603
|
+
value: manager.resourceRetrievalRoleArn,
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
376
608
|
// Stack-level output
|
|
377
609
|
new CfnOutput(this, 'StackNameOutput', {
|
|
378
610
|
description: 'Name of the CloudFormation Stack',
|
|
@@ -444,11 +676,14 @@ test('AgentCoreStack synthesizes with empty spec', () => {
|
|
|
444
676
|
credentials: [],
|
|
445
677
|
evaluators: [],
|
|
446
678
|
onlineEvalConfigs: [],
|
|
679
|
+
configBundles: [],
|
|
447
680
|
policyEngines: [],
|
|
681
|
+
payments: [],
|
|
448
682
|
agentCoreGateways: [],
|
|
449
683
|
mcpRuntimeTools: [],
|
|
450
684
|
unassignedTargets: [],
|
|
451
685
|
datasets: [],
|
|
686
|
+
knowledgeBases: [],
|
|
452
687
|
},
|
|
453
688
|
});
|
|
454
689
|
const template = Template.fromStack(stack);
|
|
@@ -603,8 +838,12 @@ exports[`Assets Directory Snapshots > File listing > should match the expected f
|
|
|
603
838
|
"python/http/strands/base/model/__init__.py",
|
|
604
839
|
"python/http/strands/base/model/load.py",
|
|
605
840
|
"python/http/strands/base/pyproject.toml",
|
|
841
|
+
"python/http/strands/base/skills/fetcher.py",
|
|
842
|
+
"python/http/strands/capabilities/execution-limits/hooks/execution_limits.py",
|
|
606
843
|
"python/http/strands/capabilities/memory/__init__.py",
|
|
607
844
|
"python/http/strands/capabilities/memory/session.py",
|
|
845
|
+
"python/http/strands/capabilities/payments/__init__.py",
|
|
846
|
+
"python/http/strands/capabilities/payments/payments.py",
|
|
608
847
|
"python/mcp/standalone/base/README.md",
|
|
609
848
|
"python/mcp/standalone/base/gitignore.template",
|
|
610
849
|
"python/mcp/standalone/base/main.py",
|
|
@@ -1629,7 +1868,7 @@ dependencies = [
|
|
|
1629
1868
|
{{/if}}{{#if (eq modelProvider "OpenAI")}}"langchain-openai >= 0.2.0",
|
|
1630
1869
|
{{/if}}"aws-opentelemetry-distro",
|
|
1631
1870
|
"opentelemetry-instrumentation-langchain >= 0.59.0",
|
|
1632
|
-
"bedrock-agentcore[a2a] >= 1.0
|
|
1871
|
+
"bedrock-agentcore[a2a] >= 1.8.0",
|
|
1633
1872
|
"botocore[crt] >= 1.35.0",
|
|
1634
1873
|
"langgraph >= 0.2.0",
|
|
1635
1874
|
]
|
|
@@ -2018,8 +2257,11 @@ def get_memory_session_manager(session_id: Optional[str], actor_id: str) -> Opti
|
|
|
2018
2257
|
{{#if (includes memoryProviders.[0].strategies "USER_PREFERENCE")}}
|
|
2019
2258
|
f"/users/{actor_id}/preferences": RetrievalConfig(top_k=3, relevance_score=0.5),
|
|
2020
2259
|
{{/if}}
|
|
2260
|
+
{{#if (includes memoryProviders.[0].strategies "EPISODIC")}}
|
|
2261
|
+
f"/episodes/{actor_id}/{session_id}": RetrievalConfig(top_k=5, relevance_score=0.5),
|
|
2262
|
+
{{/if}}
|
|
2021
2263
|
{{#if (includes memoryProviders.[0].strategies "SUMMARIZATION")}}
|
|
2022
|
-
f"/summaries/{actor_id}": RetrievalConfig(top_k=3, relevance_score=0.5),
|
|
2264
|
+
f"/summaries/{actor_id}/{session_id}": RetrievalConfig(top_k=3, relevance_score=0.5),
|
|
2023
2265
|
{{/if}}
|
|
2024
2266
|
}
|
|
2025
2267
|
{{/if}}
|
|
@@ -2276,7 +2518,7 @@ requires-python = ">=3.10"
|
|
|
2276
2518
|
dependencies = [
|
|
2277
2519
|
"ag-ui-adk >= 0.6.0",
|
|
2278
2520
|
"ag-ui-protocol >= 0.1.10",
|
|
2279
|
-
"bedrock-agentcore >= 1.0
|
|
2521
|
+
"bedrock-agentcore >= 1.8.0",
|
|
2280
2522
|
"fastapi >= 0.115.12",
|
|
2281
2523
|
"google-adk >= 1.16.0, < 2.0.0",
|
|
2282
2524
|
"google-genai >= 1.0.0, < 2.0.0",
|
|
@@ -2648,7 +2890,7 @@ dependencies = [
|
|
|
2648
2890
|
{{/if}}{{#if (eq modelProvider "OpenAI")}}"langchain-openai >= 0.2.0",
|
|
2649
2891
|
{{/if}}"aws-opentelemetry-distro",
|
|
2650
2892
|
"opentelemetry-instrumentation-langchain >= 0.59.0",
|
|
2651
|
-
"bedrock-agentcore >= 1.0
|
|
2893
|
+
"bedrock-agentcore >= 1.8.0",
|
|
2652
2894
|
"botocore[crt] >= 1.35.0",
|
|
2653
2895
|
"langgraph >= 0.3.25",
|
|
2654
2896
|
"langchain >= 0.3.0",
|
|
@@ -3037,8 +3279,11 @@ def get_memory_session_manager(session_id: Optional[str], actor_id: str) -> Opti
|
|
|
3037
3279
|
{{#if (includes memoryProviders.[0].strategies "USER_PREFERENCE")}}
|
|
3038
3280
|
f"/users/{actor_id}/preferences": RetrievalConfig(top_k=3, relevance_score=0.5),
|
|
3039
3281
|
{{/if}}
|
|
3282
|
+
{{#if (includes memoryProviders.[0].strategies "EPISODIC")}}
|
|
3283
|
+
f"/episodes/{actor_id}/{session_id}": RetrievalConfig(top_k=5, relevance_score=0.5),
|
|
3284
|
+
{{/if}}
|
|
3040
3285
|
{{#if (includes memoryProviders.[0].strategies "SUMMARIZATION")}}
|
|
3041
|
-
f"/summaries/{actor_id}": RetrievalConfig(top_k=3, relevance_score=0.5),
|
|
3286
|
+
f"/summaries/{actor_id}/{session_id}": RetrievalConfig(top_k=3, relevance_score=0.5),
|
|
3042
3287
|
{{/if}}
|
|
3043
3288
|
}
|
|
3044
3289
|
{{/if}}
|
|
@@ -3467,7 +3712,7 @@ dependencies = [
|
|
|
3467
3712
|
"autogen-ext[mcp] >= 0.7.5",
|
|
3468
3713
|
"opentelemetry-distro",
|
|
3469
3714
|
"opentelemetry-exporter-otlp",
|
|
3470
|
-
"bedrock-agentcore >= 1.0
|
|
3715
|
+
"bedrock-agentcore >= 1.8.0",
|
|
3471
3716
|
"botocore[crt] >= 1.35.0",
|
|
3472
3717
|
"tiktoken",
|
|
3473
3718
|
{{#if (eq modelProvider "Bedrock")}}
|
|
@@ -3791,20 +4036,20 @@ def get_all_gateway_mcp_toolsets() -> list[MCPToolset]:
|
|
|
3791
4036
|
{{#each gatewayProviders}}
|
|
3792
4037
|
url = os.environ.get("{{envVarName}}")
|
|
3793
4038
|
if url:
|
|
3794
|
-
|
|
4039
|
+
{{#if (eq authType "AWS_IAM")}}
|
|
3795
4040
|
session = create_aws_session()
|
|
3796
4041
|
auth = SigV4HTTPXAuth(session.get_credentials(), "bedrock-agentcore", session.region_name)
|
|
3797
|
-
toolsets.append(MCPToolset(connection_params=StreamableHTTPConnectionParams(
|
|
4042
|
+
toolsets.append(MCPToolset(tool_name_prefix="{{snakeCase name}}", connection_params=StreamableHTTPConnectionParams(
|
|
3798
4043
|
url=url,
|
|
3799
4044
|
httpx_client_factory=lambda **kwargs: httpx.AsyncClient(auth=auth, **kwargs)
|
|
3800
4045
|
)))
|
|
3801
|
-
|
|
4046
|
+
{{else if (eq authType "CUSTOM_JWT")}}
|
|
3802
4047
|
token = _get_bearer_token_{{snakeCase name}}()
|
|
3803
4048
|
headers = {"Authorization": f"Bearer {token}"} if token else None
|
|
3804
|
-
toolsets.append(MCPToolset(connection_params=StreamableHTTPConnectionParams(url=url, headers=headers)))
|
|
3805
|
-
|
|
3806
|
-
toolsets.append(MCPToolset(connection_params=StreamableHTTPConnectionParams(url=url)))
|
|
3807
|
-
|
|
4049
|
+
toolsets.append(MCPToolset(tool_name_prefix="{{snakeCase name}}", connection_params=StreamableHTTPConnectionParams(url=url, headers=headers)))
|
|
4050
|
+
{{else}}
|
|
4051
|
+
toolsets.append(MCPToolset(tool_name_prefix="{{snakeCase name}}", connection_params=StreamableHTTPConnectionParams(url=url)))
|
|
4052
|
+
{{/if}}
|
|
3808
4053
|
else:
|
|
3809
4054
|
logger.warning("{{envVarName}} not set — {{name}} gateway tools unavailable")
|
|
3810
4055
|
{{/each}}
|
|
@@ -3897,7 +4142,7 @@ requires-python = ">=3.10"
|
|
|
3897
4142
|
dependencies = [
|
|
3898
4143
|
"opentelemetry-distro",
|
|
3899
4144
|
"opentelemetry-exporter-otlp",
|
|
3900
|
-
"google-adk >= 1.
|
|
4145
|
+
"google-adk >= 1.35.0, < 2.0.0",
|
|
3901
4146
|
"google-genai >= 1.0.0, < 2.0.0",
|
|
3902
4147
|
"bedrock-agentcore >= 1.0.3",
|
|
3903
4148
|
"botocore[crt] >= 1.35.0",
|
|
@@ -4226,23 +4471,25 @@ def get_all_gateway_mcp_client() -> MultiServerMCPClient | None:
|
|
|
4226
4471
|
{{#each gatewayProviders}}
|
|
4227
4472
|
url = os.environ.get("{{envVarName}}")
|
|
4228
4473
|
if url:
|
|
4229
|
-
|
|
4474
|
+
{{#if (eq authType "AWS_IAM")}}
|
|
4230
4475
|
session = create_aws_session()
|
|
4231
4476
|
auth = SigV4HTTPXAuth(session.get_credentials(), "bedrock-agentcore", session.region_name)
|
|
4232
|
-
servers["{{name}}"] = {"transport": "streamable_http", "url": url, "auth": auth}
|
|
4233
|
-
|
|
4477
|
+
servers["{{snakeCase name}}"] = {"transport": "streamable_http", "url": url, "auth": auth}
|
|
4478
|
+
{{else if (eq authType "CUSTOM_JWT")}}
|
|
4234
4479
|
token = _get_bearer_token_{{snakeCase name}}()
|
|
4235
4480
|
headers = {"Authorization": f"Bearer {token}"} if token else None
|
|
4236
|
-
servers["{{name}}"] = {"transport": "streamable_http", "url": url, "headers": headers}
|
|
4237
|
-
|
|
4238
|
-
servers["{{name}}"] = {"transport": "streamable_http", "url": url}
|
|
4239
|
-
|
|
4481
|
+
servers["{{snakeCase name}}"] = {"transport": "streamable_http", "url": url, "headers": headers}
|
|
4482
|
+
{{else}}
|
|
4483
|
+
servers["{{snakeCase name}}"] = {"transport": "streamable_http", "url": url}
|
|
4484
|
+
{{/if}}
|
|
4240
4485
|
else:
|
|
4241
4486
|
logger.warning("{{envVarName}} not set — {{name}} gateway tools unavailable")
|
|
4242
4487
|
{{/each}}
|
|
4243
4488
|
if not servers:
|
|
4244
4489
|
return None
|
|
4245
|
-
|
|
4490
|
+
# tool_name_prefix namespaces each gateway's tools by server key so multiple
|
|
4491
|
+
# gateways exposing the same tool (e.g. x_amz_bedrock_agentcore_search) don't collide.
|
|
4492
|
+
return MultiServerMCPClient(servers, tool_name_prefix=True)
|
|
4246
4493
|
{{else}}
|
|
4247
4494
|
{{#if isVpc}}
|
|
4248
4495
|
# VPC mode: external MCP endpoints are not reachable without a NAT gateway.
|
|
@@ -4420,9 +4667,9 @@ dependencies = [
|
|
|
4420
4667
|
"opentelemetry-instrumentation-langchain >= 0.59.0",
|
|
4421
4668
|
"langgraph >= 1.0.2",
|
|
4422
4669
|
"mcp >= 1.19.0",
|
|
4423
|
-
"langchain-mcp-adapters >= 0.
|
|
4670
|
+
"langchain-mcp-adapters >= 0.2.0",
|
|
4424
4671
|
"langchain >= 1.0.3",
|
|
4425
|
-
"bedrock-agentcore >= 1.0
|
|
4672
|
+
"bedrock-agentcore >= 1.8.0",
|
|
4426
4673
|
"botocore[crt] >= 1.35.0",
|
|
4427
4674
|
{{#if (eq modelProvider "Bedrock")}}
|
|
4428
4675
|
"langchain-aws >= 1.0.0",
|
|
@@ -4538,6 +4785,9 @@ exports[`Assets Directory Snapshots > Python framework assets > python/python/ht
|
|
|
4538
4785
|
"{{#if needsOs}}
|
|
4539
4786
|
import os
|
|
4540
4787
|
{{/if}}
|
|
4788
|
+
{{#if hasGateway}}
|
|
4789
|
+
from contextlib import AsyncExitStack
|
|
4790
|
+
{{/if}}
|
|
4541
4791
|
from agents import Agent, Runner, function_tool
|
|
4542
4792
|
from bedrock_agentcore.runtime import BedrockAgentCoreApp
|
|
4543
4793
|
from model.load import load_model
|
|
@@ -4648,15 +4898,22 @@ async def main(query):
|
|
|
4648
4898
|
try:
|
|
4649
4899
|
{{#if hasGateway}}
|
|
4650
4900
|
if mcp_servers:
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
mcp_servers
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4901
|
+
# Connect every gateway server before the run; include_server_in_tool_names
|
|
4902
|
+
# namespaces each server's tools so multiple gateways exposing the same tool
|
|
4903
|
+
# (e.g. x_amz_bedrock_agentcore_search) don't collide.
|
|
4904
|
+
async with AsyncExitStack() as stack:
|
|
4905
|
+
for server in mcp_servers:
|
|
4906
|
+
await stack.enter_async_context(server)
|
|
4907
|
+
agent = Agent(
|
|
4908
|
+
name="{{ name }}",
|
|
4909
|
+
model="gpt-4.1",
|
|
4910
|
+
instructions=INSTRUCTIONS,
|
|
4911
|
+
mcp_servers=mcp_servers,
|
|
4912
|
+
tools=tools,
|
|
4913
|
+
mcp_config={"include_server_in_tool_names": True},
|
|
4914
|
+
)
|
|
4915
|
+
result = await Runner.run(agent, query)
|
|
4916
|
+
return result
|
|
4660
4917
|
else:
|
|
4661
4918
|
agent = Agent(
|
|
4662
4919
|
name="{{ name }}",
|
|
@@ -4756,20 +5013,20 @@ def get_all_gateway_mcp_servers() -> list[MCPServerStreamableHttp]:
|
|
|
4756
5013
|
{{#each gatewayProviders}}
|
|
4757
5014
|
url = os.environ.get("{{envVarName}}")
|
|
4758
5015
|
if url:
|
|
4759
|
-
|
|
5016
|
+
{{#if (eq authType "AWS_IAM")}}
|
|
4760
5017
|
session = create_aws_session()
|
|
4761
5018
|
auth = SigV4HTTPXAuth(session.get_credentials(), "bedrock-agentcore", session.region_name)
|
|
4762
5019
|
servers.append(MCPServerStreamableHttp(
|
|
4763
5020
|
name="{{name}}",
|
|
4764
5021
|
params={"url": url, "httpx_client_factory": lambda **kwargs: httpx.AsyncClient(auth=auth, **kwargs)}
|
|
4765
5022
|
))
|
|
4766
|
-
|
|
5023
|
+
{{else if (eq authType "CUSTOM_JWT")}}
|
|
4767
5024
|
token = _get_bearer_token_{{snakeCase name}}()
|
|
4768
5025
|
headers = {"Authorization": f"Bearer {token}"} if token else {}
|
|
4769
5026
|
servers.append(MCPServerStreamableHttp(name="{{name}}", params={"url": url, "headers": headers}))
|
|
4770
|
-
|
|
5027
|
+
{{else}}
|
|
4771
5028
|
servers.append(MCPServerStreamableHttp(name="{{name}}", params={"url": url}))
|
|
4772
|
-
|
|
5029
|
+
{{/if}}
|
|
4773
5030
|
else:
|
|
4774
5031
|
logger.warning("{{envVarName}} not set — {{name}} gateway tools unavailable")
|
|
4775
5032
|
{{/each}}
|
|
@@ -4857,8 +5114,8 @@ readme = "README.md"
|
|
|
4857
5114
|
requires-python = ">=3.10"
|
|
4858
5115
|
dependencies = [
|
|
4859
5116
|
"aws-opentelemetry-distro",
|
|
4860
|
-
"openai-agents >= 0.
|
|
4861
|
-
"bedrock-agentcore >= 1.0
|
|
5117
|
+
"openai-agents >= 0.16.0",
|
|
5118
|
+
"bedrock-agentcore >= 1.8.0",
|
|
4862
5119
|
"botocore[crt] >= 1.35.0",
|
|
4863
5120
|
{{#if hasGateway}}{{#if (includes gatewayAuthTypes "AWS_IAM")}}"mcp-proxy-for-aws >= 1.1.0",
|
|
4864
5121
|
{{/if}}{{/if}}
|
|
@@ -4958,45 +5215,113 @@ Thumbs.db"
|
|
|
4958
5215
|
|
|
4959
5216
|
exports[`Assets Directory Snapshots > Python framework assets > python/python/http/strands/base/main.py should match snapshot 1`] = `
|
|
4960
5217
|
"from typing import Any
|
|
5218
|
+
{{#if inlineFunctionTools}}
|
|
5219
|
+
import json
|
|
4961
5220
|
|
|
5221
|
+
from strands.tools.tools import PythonAgentTool
|
|
5222
|
+
from strands.types.tools import ToolResult, ToolUse
|
|
5223
|
+
{{/if}}
|
|
4962
5224
|
from strands import Agent, tool
|
|
5225
|
+
{{#if hasSkillsFetcher}}
|
|
5226
|
+
from strands import AgentSkills
|
|
5227
|
+
{{#if hasFetchedSkills}}
|
|
5228
|
+
from skills.fetcher import resolve_s3_skills, resolve_git_skills
|
|
5229
|
+
{{/if}}
|
|
5230
|
+
{{#if (some gitSkills "credentialArn")}}
|
|
5231
|
+
from bedrock_agentcore.services.identity import IdentityClient
|
|
5232
|
+
{{/if}}
|
|
5233
|
+
{{/if}}
|
|
5234
|
+
import asyncio
|
|
5235
|
+
{{#if hasShell}}
|
|
5236
|
+
import subprocess
|
|
5237
|
+
{{/if}}
|
|
5238
|
+
{{#if hasFileOperations}}
|
|
5239
|
+
import os
|
|
5240
|
+
{{/if}}
|
|
5241
|
+
{{#if hasExecutionLimits}}
|
|
5242
|
+
from strands.tools.executors import SequentialToolExecutor
|
|
5243
|
+
from strands.types.exceptions import EventLoopException
|
|
5244
|
+
from hooks.execution_limits import ExecutionLimitExceeded, ExecutionLimitsHook
|
|
5245
|
+
{{/if}}
|
|
4963
5246
|
{{#if hasConfigBundle}}
|
|
4964
5247
|
from strands.hooks import HookProvider, HookRegistry, BeforeInvocationEvent, BeforeToolCallEvent
|
|
5248
|
+
{{/if}}
|
|
5249
|
+
{{#if truncationStrategy}}
|
|
5250
|
+
{{#if (eq truncationStrategy "sliding_window")}}
|
|
5251
|
+
from strands.agent.conversation_manager.sliding_window_conversation_manager import SlidingWindowConversationManager
|
|
5252
|
+
{{/if}}
|
|
5253
|
+
{{#if (eq truncationStrategy "summarization")}}
|
|
5254
|
+
from strands.agent.conversation_manager.summarizing_conversation_manager import SummarizingConversationManager
|
|
5255
|
+
{{/if}}
|
|
5256
|
+
{{else}}
|
|
5257
|
+
from strands.agent.conversation_manager.null_conversation_manager import NullConversationManager
|
|
5258
|
+
{{/if}}
|
|
5259
|
+
{{#if hasConfigBundle}}
|
|
4965
5260
|
from bedrock_agentcore.runtime.context import BedrockAgentCoreContext
|
|
4966
5261
|
{{/if}}
|
|
5262
|
+
{{#if hasBrowser}}
|
|
5263
|
+
from strands_tools.browser import AgentCoreBrowser
|
|
5264
|
+
{{/if}}
|
|
5265
|
+
{{#if hasCodeInterpreter}}
|
|
5266
|
+
from strands_tools.code_interpreter import AgentCoreCodeInterpreter
|
|
5267
|
+
{{/if}}
|
|
4967
5268
|
from bedrock_agentcore.runtime import BedrockAgentCoreApp
|
|
4968
5269
|
from model.load import load_model
|
|
4969
5270
|
{{#if hasGateway}}
|
|
4970
5271
|
from mcp_client.client import get_all_gateway_mcp_clients
|
|
4971
|
-
{{else}}
|
|
4972
|
-
from mcp_client.client import get_streamable_http_mcp_client
|
|
4973
5272
|
{{/if}}
|
|
5273
|
+
{{#if remoteMcpTools}}
|
|
5274
|
+
from mcp_client.client import get_all_remote_mcp_clients
|
|
5275
|
+
{{/if}}
|
|
5276
|
+
{{#unless (or hasGateway remoteMcpTools)}}
|
|
5277
|
+
{{#unless isExportHarness}}
|
|
5278
|
+
from mcp_client.client import get_streamable_http_mcp_client
|
|
5279
|
+
{{/unless}}
|
|
5280
|
+
{{/unless}}
|
|
4974
5281
|
{{#if hasMemory}}
|
|
4975
5282
|
from memory.session import get_memory_session_manager
|
|
4976
5283
|
{{/if}}
|
|
4977
|
-
{{#
|
|
5284
|
+
{{#unless hasFileOperations}}
|
|
5285
|
+
{{#if (or needsOs (some gitSkills "credentialArn"))}}
|
|
4978
5286
|
import os
|
|
4979
5287
|
{{/if}}
|
|
5288
|
+
{{/unless}}
|
|
5289
|
+
{{#if hasPayment}}
|
|
5290
|
+
from capabilities.payments.payments import create_payments_plugin, PAYMENT_SYSTEM_PROMPT
|
|
5291
|
+
{{/if}}
|
|
4980
5292
|
|
|
4981
5293
|
app = BedrockAgentCoreApp()
|
|
4982
5294
|
log = app.logger
|
|
4983
5295
|
|
|
4984
|
-
#
|
|
5296
|
+
{{#if (or hasGateway remoteMcpTools)}}
|
|
5297
|
+
# Define MCP clients for all configured MCP servers (gateways and/or remote MCP)
|
|
5298
|
+
mcp_clients = []
|
|
4985
5299
|
{{#if hasGateway}}
|
|
4986
|
-
mcp_clients
|
|
5300
|
+
mcp_clients += get_all_gateway_mcp_clients()
|
|
5301
|
+
{{/if}}
|
|
5302
|
+
{{#if remoteMcpTools}}
|
|
5303
|
+
mcp_clients += get_all_remote_mcp_clients()
|
|
5304
|
+
{{/if}}
|
|
4987
5305
|
{{else}}
|
|
5306
|
+
{{#unless isExportHarness}}
|
|
5307
|
+
# Define a Streamable HTTP MCP Client
|
|
4988
5308
|
mcp_clients = [get_streamable_http_mcp_client()]
|
|
5309
|
+
{{/unless}}
|
|
4989
5310
|
{{/if}}
|
|
4990
5311
|
|
|
5312
|
+
{{#if systemPromptText}}
|
|
5313
|
+
DEFAULT_SYSTEM_PROMPT = """{{escapePyStr systemPromptText}}"""
|
|
5314
|
+
{{else}}
|
|
4991
5315
|
DEFAULT_SYSTEM_PROMPT = """
|
|
4992
5316
|
You are a helpful assistant. Use tools when appropriate.
|
|
4993
|
-
{{#if needsOs}}
|
|
5317
|
+
{{#if needsOs}}{{#unless isExportHarness}}
|
|
4994
5318
|
You have access to the following mounted filesystems. Use file_read, file_write, and list_files with full absolute paths:
|
|
4995
5319
|
{{#if sessionStorageMountPath}}- {{sessionStorageMountPath}}: ephemeral session storage (lost when session ends)
|
|
4996
5320
|
{{/if}}{{#each efsMounts}}- {{mountPath}}: EFS persistent storage (persists across sessions and agent restarts)
|
|
4997
5321
|
{{/each}}{{#each s3Mounts}}- {{mountPath}}: S3 Files persistent storage (durable, backed by S3)
|
|
4998
|
-
{{/each}}{{/if}}
|
|
5322
|
+
{{/each}}{{/unless}}{{/if}}
|
|
4999
5323
|
"""
|
|
5324
|
+
{{/if}}
|
|
5000
5325
|
|
|
5001
5326
|
{{#if hasConfigBundle}}
|
|
5002
5327
|
DEFAULT_TOOL_DESC = "Return the sum of two numbers"
|
|
@@ -5005,6 +5330,30 @@ DEFAULT_TOOL_DESC = "Return the sum of two numbers"
|
|
|
5005
5330
|
# Define a collection of tools used by the model
|
|
5006
5331
|
tools = []
|
|
5007
5332
|
|
|
5333
|
+
{{#if inlineFunctionTools}}
|
|
5334
|
+
# Inline function tools — stop the agent loop so the tool call streams back to the caller
|
|
5335
|
+
def _make_inline_tool(name: str, spec: dict) -> PythonAgentTool:
|
|
5336
|
+
def _handler(tool: ToolUse, **kwargs: Any) -> ToolResult:
|
|
5337
|
+
kwargs.get("request_state", {})["stop_event_loop"] = True
|
|
5338
|
+
return {"toolUseId": tool["toolUseId"], "status": "success", "content": [{"text": " "}]}
|
|
5339
|
+
_handler.__name__ = name
|
|
5340
|
+
return PythonAgentTool(tool_name=name, tool_spec=spec, tool_func=_handler)
|
|
5341
|
+
|
|
5342
|
+
{{#each inlineFunctionTools}}
|
|
5343
|
+
_INLINE_SPEC_{{snakeCase name}} = {
|
|
5344
|
+
"name": "{{name}}",
|
|
5345
|
+
"description": {{safeJson description}},
|
|
5346
|
+
"inputSchema": {"json": json.loads({{pyJsonStr inputSchema}}) },
|
|
5347
|
+
}
|
|
5348
|
+
tools.append(_make_inline_tool("{{name}}", _INLINE_SPEC_{{snakeCase name}}))
|
|
5349
|
+
{{/each}}
|
|
5350
|
+
|
|
5351
|
+
_INLINE_FUNCTION_NAMES = { {{#each inlineFunctionTools}}"{{name}}"{{#unless @last}}, {{/unless}}{{/each}} }
|
|
5352
|
+
|
|
5353
|
+
{{else}}
|
|
5354
|
+
_INLINE_FUNCTION_NAMES = set()
|
|
5355
|
+
|
|
5356
|
+
{{#unless isExportHarness}}
|
|
5008
5357
|
# Define a simple function tool
|
|
5009
5358
|
{{#if hasConfigBundle}}
|
|
5010
5359
|
@tool(description=DEFAULT_TOOL_DESC)
|
|
@@ -5016,7 +5365,116 @@ def add_numbers(a: int, b: int) -> int:
|
|
|
5016
5365
|
return a+b
|
|
5017
5366
|
tools.append(add_numbers)
|
|
5018
5367
|
|
|
5019
|
-
{{
|
|
5368
|
+
{{/unless}}
|
|
5369
|
+
{{/if}}
|
|
5370
|
+
{{#if hasBrowser}}
|
|
5371
|
+
tools.append(AgentCoreBrowser({{#if browserIdentifier}}identifier="{{browserIdentifier}}"{{/if}}).browser)
|
|
5372
|
+
{{/if}}
|
|
5373
|
+
{{#if hasCodeInterpreter}}
|
|
5374
|
+
tools.append(AgentCoreCodeInterpreter({{#if codeInterpreterIdentifier}}identifier="{{codeInterpreterIdentifier}}"{{/if}}).code_interpreter)
|
|
5375
|
+
{{/if}}
|
|
5376
|
+
{{#if hasShell}}
|
|
5377
|
+
@tool
|
|
5378
|
+
def shell(command: str, timeout: int = 300) -> dict:
|
|
5379
|
+
"""Execute a bash command and return the results.
|
|
5380
|
+
|
|
5381
|
+
Args:
|
|
5382
|
+
command: The bash command to execute
|
|
5383
|
+
timeout: Timeout in seconds (default: 300)
|
|
5384
|
+
|
|
5385
|
+
Returns:
|
|
5386
|
+
Dict with stdout, stderr, and exit_code
|
|
5387
|
+
"""
|
|
5388
|
+
result = subprocess.run(
|
|
5389
|
+
command, shell=True, capture_output=True, text=True, timeout=timeout
|
|
5390
|
+
)
|
|
5391
|
+
return {"stdout": result.stdout, "stderr": result.stderr, "exit_code": result.returncode}
|
|
5392
|
+
|
|
5393
|
+
tools.append(shell)
|
|
5394
|
+
{{/if}}
|
|
5395
|
+
{{#if hasFileOperations}}
|
|
5396
|
+
@tool
|
|
5397
|
+
def file_operations(
|
|
5398
|
+
command: str,
|
|
5399
|
+
path: str,
|
|
5400
|
+
old_str: str = None,
|
|
5401
|
+
new_str: str = None,
|
|
5402
|
+
file_text: str = None,
|
|
5403
|
+
insert_line: int = None,
|
|
5404
|
+
view_range: list = None,
|
|
5405
|
+
) -> str:
|
|
5406
|
+
"""Text editor tool for viewing and modifying files.
|
|
5407
|
+
|
|
5408
|
+
Args:
|
|
5409
|
+
command: The command to execute ("view", "str_replace", "create", "insert")
|
|
5410
|
+
path: Path to the file or directory
|
|
5411
|
+
old_str: Text to replace (for str_replace command)
|
|
5412
|
+
new_str: Replacement text (for str_replace and insert commands)
|
|
5413
|
+
file_text: Content for new file (for create command)
|
|
5414
|
+
insert_line: Line number to insert after (for insert command)
|
|
5415
|
+
view_range: [start_line, end_line] for viewing specific lines (for view command)
|
|
5416
|
+
|
|
5417
|
+
Returns:
|
|
5418
|
+
Result of the operation
|
|
5419
|
+
"""
|
|
5420
|
+
try:
|
|
5421
|
+
if command == "view":
|
|
5422
|
+
if not os.path.exists(path):
|
|
5423
|
+
return f"Error: Path '{path}' does not exist"
|
|
5424
|
+
if os.path.isdir(path):
|
|
5425
|
+
return "\\n".join(os.listdir(path))
|
|
5426
|
+
with open(path) as f:
|
|
5427
|
+
lines = f.read().splitlines()
|
|
5428
|
+
if view_range:
|
|
5429
|
+
start, end = view_range
|
|
5430
|
+
start_idx = max(0, start - 1)
|
|
5431
|
+
end_idx = len(lines) if end == -1 else min(len(lines), end)
|
|
5432
|
+
lines = lines[start_idx:end_idx]
|
|
5433
|
+
start_num = start_idx + 1
|
|
5434
|
+
else:
|
|
5435
|
+
start_num = 1
|
|
5436
|
+
return "\\n".join(f"{start_num + i}: {line}" for i, line in enumerate(lines))
|
|
5437
|
+
elif command == "str_replace":
|
|
5438
|
+
if old_str is None or new_str is None:
|
|
5439
|
+
return "Error: str_replace requires both old_str and new_str parameters"
|
|
5440
|
+
if not os.path.exists(path):
|
|
5441
|
+
return f"Error: File '{path}' does not exist"
|
|
5442
|
+
content = open(path).read()
|
|
5443
|
+
if old_str not in content:
|
|
5444
|
+
return "Error: Text not found in file"
|
|
5445
|
+
count = content.count(old_str)
|
|
5446
|
+
if count > 1:
|
|
5447
|
+
return f"Error: Text appears {count} times in file. Please be more specific."
|
|
5448
|
+
open(path, "w").write(content.replace(old_str, new_str, 1))
|
|
5449
|
+
return f"Successfully replaced text in '{path}'"
|
|
5450
|
+
elif command == "create":
|
|
5451
|
+
if file_text is None:
|
|
5452
|
+
return "Error: create requires file_text parameter"
|
|
5453
|
+
os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True)
|
|
5454
|
+
open(path, "w").write(file_text)
|
|
5455
|
+
return f"Successfully created file '{path}'"
|
|
5456
|
+
elif command == "insert":
|
|
5457
|
+
if new_str is None or insert_line is None:
|
|
5458
|
+
return "Error: insert requires both new_str and insert_line parameters"
|
|
5459
|
+
if not os.path.exists(path):
|
|
5460
|
+
return f"Error: File '{path}' does not exist"
|
|
5461
|
+
lines = open(path).read().splitlines(True)
|
|
5462
|
+
if insert_line == 0:
|
|
5463
|
+
lines.insert(0, new_str + "\\n")
|
|
5464
|
+
elif insert_line >= len(lines):
|
|
5465
|
+
lines.append(new_str + "\\n")
|
|
5466
|
+
else:
|
|
5467
|
+
lines.insert(insert_line, new_str + "\\n")
|
|
5468
|
+
open(path, "w").write("".join(lines))
|
|
5469
|
+
return f"Successfully inserted text in '{path}' at line {insert_line + 1}"
|
|
5470
|
+
else:
|
|
5471
|
+
return f"Error: Unknown command '{command}'"
|
|
5472
|
+
except Exception as e:
|
|
5473
|
+
return f"Error: {e}"
|
|
5474
|
+
|
|
5475
|
+
tools.append(file_operations)
|
|
5476
|
+
{{/if}}
|
|
5477
|
+
{{#if needsOs}}{{#unless isExportHarness}}
|
|
5020
5478
|
_MOUNT_PATHS = [
|
|
5021
5479
|
{{#if sessionStorageMountPath}}"{{sessionStorageMountPath}}",{{/if}}
|
|
5022
5480
|
{{#each efsMounts}}"{{mountPath}}",{{/each}}
|
|
@@ -5070,12 +5528,21 @@ def list_files(path: str) -> str:
|
|
|
5070
5528
|
return f"Error listing '{path}': {e.strerror}"
|
|
5071
5529
|
|
|
5072
5530
|
tools.extend([file_read, file_write, list_files])
|
|
5073
|
-
{{/if}}
|
|
5531
|
+
{{/unless}}{{/if}}
|
|
5074
5532
|
|
|
5533
|
+
{{#if (or hasGateway remoteMcpTools)}}
|
|
5534
|
+
# Add MCP clients to tools
|
|
5535
|
+
for mcp_client in mcp_clients:
|
|
5536
|
+
if mcp_client:
|
|
5537
|
+
tools.append(mcp_client)
|
|
5538
|
+
{{else}}
|
|
5539
|
+
{{#unless isExportHarness}}
|
|
5075
5540
|
# Add MCP client to tools if available
|
|
5076
5541
|
for mcp_client in mcp_clients:
|
|
5077
5542
|
if mcp_client:
|
|
5078
5543
|
tools.append(mcp_client)
|
|
5544
|
+
{{/unless}}
|
|
5545
|
+
{{/if}}
|
|
5079
5546
|
|
|
5080
5547
|
{{#if hasConfigBundle}}
|
|
5081
5548
|
|
|
@@ -5111,71 +5578,309 @@ class ConfigBundleHook(HookProvider):
|
|
|
5111
5578
|
|
|
5112
5579
|
{{/if}}
|
|
5113
5580
|
|
|
5581
|
+
def _make_conversation_manager():
|
|
5582
|
+
{{#if truncationStrategy}}
|
|
5583
|
+
{{#if (eq truncationStrategy "sliding_window")}}
|
|
5584
|
+
{{#if truncationConfig}}
|
|
5585
|
+
return SlidingWindowConversationManager(**{{safeJson truncationConfig}}, per_turn=True)
|
|
5586
|
+
{{else}}
|
|
5587
|
+
return SlidingWindowConversationManager(per_turn=True)
|
|
5588
|
+
{{/if}}
|
|
5589
|
+
{{else}}
|
|
5590
|
+
{{#if truncationConfig}}
|
|
5591
|
+
return SummarizingConversationManager(**{{safeJson truncationConfig}})
|
|
5592
|
+
{{else}}
|
|
5593
|
+
return SummarizingConversationManager()
|
|
5594
|
+
{{/if}}
|
|
5595
|
+
{{/if}}
|
|
5596
|
+
{{else}}
|
|
5597
|
+
return NullConversationManager()
|
|
5598
|
+
{{/if}}
|
|
5599
|
+
|
|
5114
5600
|
{{#if hasMemory}}
|
|
5601
|
+
{{#unless hasPayment}}
|
|
5115
5602
|
def agent_factory():
|
|
5116
5603
|
cache = {}
|
|
5117
|
-
def get_or_create_agent(session_id, user_id):
|
|
5118
|
-
|
|
5604
|
+
def get_or_create_agent(session_id, user_id{{#if hasSkillsFetcher}}, skill_plugins=None{{/if}}):
|
|
5605
|
+
{{#if actorId}}
|
|
5606
|
+
_actor_id = "{{actorId}}"
|
|
5607
|
+
{{else}}
|
|
5608
|
+
_actor_id = user_id
|
|
5609
|
+
{{/if}}
|
|
5610
|
+
key = f"{session_id}/{_actor_id}"
|
|
5119
5611
|
if key not in cache:
|
|
5120
|
-
# Create an agent for the given session_id and user_id
|
|
5121
5612
|
cache[key] = Agent(
|
|
5122
5613
|
model=load_model(),
|
|
5123
|
-
session_manager=get_memory_session_manager(session_id,
|
|
5614
|
+
session_manager=get_memory_session_manager(session_id, _actor_id),
|
|
5615
|
+
conversation_manager=_make_conversation_manager(),
|
|
5124
5616
|
system_prompt=DEFAULT_SYSTEM_PROMPT,
|
|
5125
|
-
tools=tools
|
|
5126
|
-
|
|
5617
|
+
tools=tools,
|
|
5618
|
+
{{#if hasSkillsFetcher}}
|
|
5619
|
+
plugins=skill_plugins or None,
|
|
5620
|
+
{{/if}}
|
|
5621
|
+
{{#if hasExecutionLimits}}
|
|
5622
|
+
tool_executor=SequentialToolExecutor(),
|
|
5623
|
+
callback_handler=None,
|
|
5624
|
+
{{/if}}
|
|
5625
|
+
hooks=[
|
|
5626
|
+
{{#if hasExecutionLimits}}
|
|
5627
|
+
ExecutionLimitsHook(
|
|
5628
|
+
{{#if maxIterations}}max_iterations={{maxIterations}},{{/if}}
|
|
5629
|
+
{{#if maxTokens}}max_tokens={{maxTokens}},{{/if}}
|
|
5630
|
+
{{#if timeoutSeconds}}timeout_seconds={{timeoutSeconds}},{{/if}}
|
|
5631
|
+
),
|
|
5632
|
+
{{/if}}
|
|
5633
|
+
{{#if hasConfigBundle}}
|
|
5634
|
+
ConfigBundleHook(),
|
|
5635
|
+
{{/if}}
|
|
5636
|
+
],
|
|
5127
5637
|
)
|
|
5128
5638
|
return cache[key]
|
|
5129
5639
|
return get_or_create_agent
|
|
5130
5640
|
get_or_create_agent = agent_factory()
|
|
5641
|
+
{{/unless}}
|
|
5131
5642
|
{{else}}
|
|
5132
5643
|
{{#if hasConfigBundle}}
|
|
5133
|
-
def create_agent():
|
|
5644
|
+
def create_agent({{#if hasSkillsFetcher}}skill_plugins=None{{/if}}):
|
|
5134
5645
|
return Agent(
|
|
5135
5646
|
model=load_model(),
|
|
5136
5647
|
system_prompt=DEFAULT_SYSTEM_PROMPT,
|
|
5137
5648
|
tools=tools,
|
|
5649
|
+
conversation_manager=_make_conversation_manager(),
|
|
5650
|
+
{{#if hasSkillsFetcher}}
|
|
5651
|
+
plugins=skill_plugins or None,
|
|
5652
|
+
{{/if}}
|
|
5138
5653
|
hooks=[ConfigBundleHook()],
|
|
5139
5654
|
)
|
|
5140
5655
|
{{else}}
|
|
5656
|
+
{{#unless hasPayment}}
|
|
5141
5657
|
_agent = None
|
|
5142
5658
|
|
|
5143
|
-
def get_or_create_agent():
|
|
5659
|
+
def get_or_create_agent({{#if hasSkillsFetcher}}skill_plugins=None{{/if}}):
|
|
5144
5660
|
global _agent
|
|
5145
5661
|
if _agent is None:
|
|
5146
5662
|
_agent = Agent(
|
|
5147
5663
|
model=load_model(),
|
|
5148
5664
|
system_prompt=DEFAULT_SYSTEM_PROMPT,
|
|
5149
|
-
tools=tools
|
|
5665
|
+
tools=tools,
|
|
5666
|
+
conversation_manager=_make_conversation_manager(),
|
|
5667
|
+
{{#if hasSkillsFetcher}}
|
|
5668
|
+
plugins=skill_plugins or None,
|
|
5669
|
+
{{/if}}
|
|
5670
|
+
{{#if hasExecutionLimits}}
|
|
5671
|
+
tool_executor=SequentialToolExecutor(),
|
|
5672
|
+
callback_handler=None,
|
|
5673
|
+
{{/if}}
|
|
5674
|
+
hooks=[
|
|
5675
|
+
{{#if hasExecutionLimits}}
|
|
5676
|
+
ExecutionLimitsHook(
|
|
5677
|
+
{{#if maxIterations}}max_iterations={{maxIterations}},{{/if}}
|
|
5678
|
+
{{#if maxTokens}}max_tokens={{maxTokens}},{{/if}}
|
|
5679
|
+
{{#if timeoutSeconds}}timeout_seconds={{timeoutSeconds}},{{/if}}
|
|
5680
|
+
),
|
|
5681
|
+
{{/if}}
|
|
5682
|
+
],
|
|
5150
5683
|
)
|
|
5151
5684
|
return _agent
|
|
5685
|
+
{{/unless}}
|
|
5152
5686
|
{{/if}}
|
|
5153
5687
|
{{/if}}
|
|
5154
5688
|
|
|
5155
5689
|
|
|
5690
|
+
def _extract_prompt(payload: dict):
|
|
5691
|
+
"""Accept harness-style messages[], tool_results[], or plain prompt string payloads."""
|
|
5692
|
+
if "messages" in payload:
|
|
5693
|
+
return payload["messages"]
|
|
5694
|
+
if "tool_results" in payload:
|
|
5695
|
+
return [{"role": "user", "content": [{"toolResult": {
|
|
5696
|
+
"toolUseId": tr["toolUseId"],
|
|
5697
|
+
"status": tr.get("status", "success"),
|
|
5698
|
+
"content": tr.get("content", []),
|
|
5699
|
+
}} for tr in payload["tool_results"]]}]
|
|
5700
|
+
return payload.get("prompt", "")
|
|
5701
|
+
|
|
5702
|
+
|
|
5703
|
+
def _has_inline_function_call(messages) -> bool:
|
|
5704
|
+
"""Return True if messages contains an assistant toolUse for an inline function tool."""
|
|
5705
|
+
if not _INLINE_FUNCTION_NAMES or not isinstance(messages, list):
|
|
5706
|
+
return False
|
|
5707
|
+
for msg in messages:
|
|
5708
|
+
if msg.get("role") == "assistant":
|
|
5709
|
+
for block in msg.get("content", []):
|
|
5710
|
+
if isinstance(block, dict) and block.get("toolUse", {}).get("name") in _INLINE_FUNCTION_NAMES:
|
|
5711
|
+
return True
|
|
5712
|
+
return False
|
|
5713
|
+
|
|
5714
|
+
|
|
5715
|
+
def _is_inline_function_call(event: dict) -> bool:
|
|
5716
|
+
"""Check if a contentBlockStart event is for an inline function tool."""
|
|
5717
|
+
if not _INLINE_FUNCTION_NAMES:
|
|
5718
|
+
return False
|
|
5719
|
+
cbs = event.get("contentBlockStart", {})
|
|
5720
|
+
start = cbs.get("start", {})
|
|
5721
|
+
tool_use = start.get("toolUse") if isinstance(start, dict) else None
|
|
5722
|
+
return tool_use is not None and tool_use.get("name") in _INLINE_FUNCTION_NAMES
|
|
5723
|
+
|
|
5724
|
+
|
|
5725
|
+
|
|
5156
5726
|
@app.entrypoint
|
|
5157
5727
|
async def invoke(payload, context):
|
|
5158
5728
|
log.info("Invoking Agent.....")
|
|
5159
5729
|
|
|
5730
|
+
{{#if hasPayment}}
|
|
5731
|
+
user_id = payload.get("user_id") or getattr(context, "user_id", "default-user")
|
|
5732
|
+
instrument_id = payload.get("payment_instrument_id")
|
|
5733
|
+
session_id = payload.get("payment_session_id")
|
|
5734
|
+
payments_plugin = create_payments_plugin(user_id, instrument_id, session_id)
|
|
5735
|
+
plugins = [payments_plugin] if payments_plugin else []
|
|
5736
|
+
{{/if}}
|
|
5737
|
+
{{#if hasSkillsFetcher}}
|
|
5738
|
+
skill_paths = [{{#each pathSkills}}{{safeJson this}}{{#unless @last}}, {{/unless}}{{/each}}]
|
|
5739
|
+
{{#if s3Skills}}
|
|
5740
|
+
s3_skill_sources = [{{#each s3Skills}}{{safeJson this}}{{#unless @last}}, {{/unless}}{{/each}}]
|
|
5741
|
+
skill_paths.extend(await asyncio.to_thread(resolve_s3_skills, s3_skill_sources, None))
|
|
5742
|
+
{{/if}}
|
|
5743
|
+
{{#if gitSkills}}
|
|
5744
|
+
git_skill_sources = [
|
|
5745
|
+
{{#each gitSkills}}
|
|
5746
|
+
dict(url={{safeJson this.url}}{{#if this.path}}, path={{safeJson this.path}}{{/if}}{{#if this.credentialArn}}, credentialArn={{safeJson this.credentialArn}}{{#if this.username}}, username={{safeJson this.username}}{{/if}}{{/if}}),
|
|
5747
|
+
{{/each}}
|
|
5748
|
+
]
|
|
5749
|
+
{{#if (some gitSkills "credentialArn")}}
|
|
5750
|
+
_git_identity_client = IdentityClient(os.environ.get("AWS_REGION", os.environ.get("AWS_DEFAULT_REGION", "us-east-1")))
|
|
5751
|
+
{{else}}
|
|
5752
|
+
_git_identity_client = None
|
|
5753
|
+
{{/if}}
|
|
5754
|
+
skill_paths.extend(await asyncio.to_thread(resolve_git_skills, git_skill_sources, _git_identity_client))
|
|
5755
|
+
{{/if}}
|
|
5756
|
+
_skill_plugins = [AgentSkills(skills=skill_paths)] if skill_paths else []
|
|
5757
|
+
{{/if}}
|
|
5758
|
+
|
|
5160
5759
|
{{#if hasMemory}}
|
|
5760
|
+
{{#if hasPayment}}
|
|
5761
|
+
mem_session_id = getattr(context, 'session_id', 'default-session')
|
|
5762
|
+
{{#if actorId}}
|
|
5763
|
+
mem_user_id = "{{actorId}}"
|
|
5764
|
+
{{else}}
|
|
5765
|
+
mem_user_id = getattr(context, 'user_id', 'default-user')
|
|
5766
|
+
{{/if}}
|
|
5767
|
+
agent = Agent(
|
|
5768
|
+
model=load_model(),
|
|
5769
|
+
session_manager=get_memory_session_manager(mem_session_id, mem_user_id),
|
|
5770
|
+
system_prompt=DEFAULT_SYSTEM_PROMPT + PAYMENT_SYSTEM_PROMPT,
|
|
5771
|
+
tools=tools,
|
|
5772
|
+
plugins=plugins{{#if hasSkillsFetcher}} + _skill_plugins{{/if}},{{#if hasConfigBundle}}
|
|
5773
|
+
hooks=[ConfigBundleHook()],{{/if}}
|
|
5774
|
+
)
|
|
5775
|
+
{{else}}
|
|
5161
5776
|
session_id = getattr(context, 'session_id', 'default-session')
|
|
5777
|
+
{{#if actorId}}
|
|
5778
|
+
user_id = "{{actorId}}"
|
|
5779
|
+
{{else}}
|
|
5162
5780
|
user_id = getattr(context, 'user_id', 'default-user')
|
|
5163
|
-
|
|
5781
|
+
{{/if}}
|
|
5782
|
+
agent = get_or_create_agent(session_id, user_id{{#if hasSkillsFetcher}}, _skill_plugins{{/if}})
|
|
5783
|
+
{{/if}}
|
|
5784
|
+
{{else}}
|
|
5785
|
+
{{#if hasPayment}}
|
|
5786
|
+
agent = Agent(
|
|
5787
|
+
model=load_model(),
|
|
5788
|
+
system_prompt=DEFAULT_SYSTEM_PROMPT + PAYMENT_SYSTEM_PROMPT,
|
|
5789
|
+
tools=tools,
|
|
5790
|
+
plugins=plugins{{#if hasSkillsFetcher}} + _skill_plugins{{/if}},{{#if hasConfigBundle}}
|
|
5791
|
+
hooks=[ConfigBundleHook()],{{/if}}
|
|
5792
|
+
)
|
|
5164
5793
|
{{else}}
|
|
5165
5794
|
{{#if hasConfigBundle}}
|
|
5166
|
-
agent = create_agent()
|
|
5795
|
+
agent = create_agent({{#if hasSkillsFetcher}}_skill_plugins{{/if}})
|
|
5167
5796
|
{{else}}
|
|
5168
|
-
agent = get_or_create_agent()
|
|
5797
|
+
agent = get_or_create_agent({{#if hasSkillsFetcher}}_skill_plugins{{/if}})
|
|
5798
|
+
{{/if}}
|
|
5169
5799
|
{{/if}}
|
|
5170
5800
|
{{/if}}
|
|
5171
5801
|
|
|
5172
|
-
|
|
5173
|
-
stream = agent.stream_async(payload.get("prompt"))
|
|
5802
|
+
prompt = _extract_prompt(payload)
|
|
5174
5803
|
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5804
|
+
{{#if inlineFunctionTools}}
|
|
5805
|
+
# If Turn 2 carries the harness-style assistant(toolUse)+user(toolResult) pair,
|
|
5806
|
+
# strip the placeholder turn Strands stored during Turn 1 so the real toolResult
|
|
5807
|
+
# is injected cleanly — same protocol as the harness runtime.
|
|
5808
|
+
if _has_inline_function_call(prompt):
|
|
5809
|
+
msgs = agent.messages
|
|
5810
|
+
if len(msgs) >= 2 and any("toolResult" in b for b in msgs[-1].get("content", [])):
|
|
5811
|
+
del msgs[-2:]
|
|
5812
|
+
{{/if}}
|
|
5813
|
+
|
|
5814
|
+
{{#if hasExecutionLimits}}
|
|
5815
|
+
timeout_seconds = {{#if timeoutSeconds}}{{timeoutSeconds}}{{else}}None{{/if}}
|
|
5816
|
+
timeout_fired = False
|
|
5817
|
+
watchdog_task = None
|
|
5818
|
+
if timeout_seconds is not None:
|
|
5819
|
+
async def _timeout_watchdog():
|
|
5820
|
+
nonlocal timeout_fired
|
|
5821
|
+
await asyncio.sleep(timeout_seconds)
|
|
5822
|
+
timeout_fired = True
|
|
5823
|
+
agent.cancel()
|
|
5824
|
+
watchdog_task = asyncio.create_task(_timeout_watchdog())
|
|
5825
|
+
|
|
5826
|
+
try:
|
|
5827
|
+
{{#if inlineFunctionTools}}
|
|
5828
|
+
hit_inline_function = False
|
|
5829
|
+
{{/if}}
|
|
5830
|
+
async for event in agent.stream_async(
|
|
5831
|
+
prompt,
|
|
5832
|
+
):
|
|
5833
|
+
if not isinstance(event, dict) or "event" not in event:
|
|
5834
|
+
continue
|
|
5835
|
+
cbs = event["event"].get("contentBlockStart")
|
|
5836
|
+
if cbs is not None and not cbs.get("start"):
|
|
5837
|
+
continue
|
|
5838
|
+
{{#if inlineFunctionTools}}
|
|
5839
|
+
if not hit_inline_function:
|
|
5840
|
+
hit_inline_function = _is_inline_function_call(event["event"])
|
|
5841
|
+
{{/if}}
|
|
5842
|
+
yield event
|
|
5843
|
+
{{#if inlineFunctionTools}}
|
|
5844
|
+
if hit_inline_function and "messageStop" in event["event"]:
|
|
5845
|
+
return
|
|
5846
|
+
{{/if}}
|
|
5847
|
+
|
|
5848
|
+
if timeout_fired:
|
|
5849
|
+
yield {"event": {"messageStop": {"stopReason": "timeout_exceeded"}}}
|
|
5850
|
+
except EventLoopException as e:
|
|
5851
|
+
if isinstance(e.original_exception, ExecutionLimitExceeded):
|
|
5852
|
+
yield {"event": {"messageStop": {"stopReason": str(e.original_exception)}}}
|
|
5853
|
+
return
|
|
5854
|
+
raise
|
|
5855
|
+
finally:
|
|
5856
|
+
if watchdog_task is not None:
|
|
5857
|
+
watchdog_task.cancel()
|
|
5858
|
+
try:
|
|
5859
|
+
await watchdog_task
|
|
5860
|
+
except asyncio.CancelledError:
|
|
5861
|
+
pass
|
|
5862
|
+
{{else}}
|
|
5863
|
+
{{#if inlineFunctionTools}}
|
|
5864
|
+
hit_inline_function = False
|
|
5865
|
+
{{/if}}
|
|
5866
|
+
async for event in agent.stream_async(
|
|
5867
|
+
prompt,
|
|
5868
|
+
):
|
|
5869
|
+
if not isinstance(event, dict) or "event" not in event:
|
|
5870
|
+
continue
|
|
5871
|
+
cbs = event["event"].get("contentBlockStart")
|
|
5872
|
+
if cbs is not None and not cbs.get("start"):
|
|
5873
|
+
continue
|
|
5874
|
+
{{#if inlineFunctionTools}}
|
|
5875
|
+
if not hit_inline_function:
|
|
5876
|
+
hit_inline_function = _is_inline_function_call(event["event"])
|
|
5877
|
+
{{/if}}
|
|
5878
|
+
yield event
|
|
5879
|
+
{{#if inlineFunctionTools}}
|
|
5880
|
+
if hit_inline_function and "messageStop" in event["event"]:
|
|
5881
|
+
return
|
|
5882
|
+
{{/if}}
|
|
5883
|
+
{{/if}}
|
|
5179
5884
|
|
|
5180
5885
|
|
|
5181
5886
|
if __name__ == "__main__":
|
|
@@ -5220,18 +5925,22 @@ def _get_bearer_token_{{snakeCase name}}(*, access_token: str):
|
|
|
5220
5925
|
{{#each gatewayProviders}}
|
|
5221
5926
|
def get_{{snakeCase name}}_mcp_client() -> MCPClient | None:
|
|
5222
5927
|
"""Returns an MCP Client connected to the {{name}} gateway."""
|
|
5928
|
+
{{#if hardcodedUrl}}
|
|
5929
|
+
url = {{safeJson hardcodedUrl}}
|
|
5930
|
+
{{else}}
|
|
5223
5931
|
url = os.environ.get("{{envVarName}}")
|
|
5224
5932
|
if not url:
|
|
5225
5933
|
logger.warning("{{envVarName}} not set — {{name}} gateway tools unavailable")
|
|
5226
5934
|
return None
|
|
5935
|
+
{{/if}}
|
|
5227
5936
|
{{#if (eq authType "AWS_IAM")}}
|
|
5228
|
-
return MCPClient(lambda: aws_iam_streamablehttp_client(url, aws_service="bedrock-agentcore", aws_region=os.environ.get("AWS_REGION", os.environ.get("AWS_DEFAULT_REGION"))))
|
|
5937
|
+
return MCPClient(lambda: aws_iam_streamablehttp_client(url, aws_service="bedrock-agentcore", aws_region=os.environ.get("AWS_REGION", os.environ.get("AWS_DEFAULT_REGION"))), prefix="{{snakeCase name}}")
|
|
5229
5938
|
{{else if (eq authType "CUSTOM_JWT")}}
|
|
5230
5939
|
token = _get_bearer_token_{{snakeCase name}}()
|
|
5231
5940
|
headers = {"Authorization": f"Bearer {token}"} if token else {}
|
|
5232
|
-
return MCPClient(lambda: streamablehttp_client(url, headers=headers))
|
|
5941
|
+
return MCPClient(lambda: streamablehttp_client(url, headers=headers), prefix="{{snakeCase name}}")
|
|
5233
5942
|
{{else}}
|
|
5234
|
-
return MCPClient(lambda: streamablehttp_client(url))
|
|
5943
|
+
return MCPClient(lambda: streamablehttp_client(url), prefix="{{snakeCase name}}")
|
|
5235
5944
|
{{/if}}
|
|
5236
5945
|
|
|
5237
5946
|
{{/each}}
|
|
@@ -5244,7 +5953,41 @@ def get_all_gateway_mcp_clients() -> list[MCPClient]:
|
|
|
5244
5953
|
clients.append(client)
|
|
5245
5954
|
{{/each}}
|
|
5246
5955
|
return clients
|
|
5247
|
-
{{
|
|
5956
|
+
{{/if}}
|
|
5957
|
+
{{#if remoteMcpTools}}
|
|
5958
|
+
{{#if (some remoteMcpTools "headerCredentials")}}
|
|
5959
|
+
from bedrock_agentcore.identity.auth import requires_api_key
|
|
5960
|
+
{{/if}}
|
|
5961
|
+
{{#each remoteMcpTools}}
|
|
5962
|
+
{{#if headerCredentials}}
|
|
5963
|
+
{{#each headerCredentials}}
|
|
5964
|
+
@requires_api_key(provider_name="{{credentialName}}")
|
|
5965
|
+
def _get_{{snakeCase ../name}}_{{snakeCase headerKey}}_key(api_key: str) -> str:
|
|
5966
|
+
"""Fetch {{headerKey}} credential for {{../name}} from AgentCore Identity."""
|
|
5967
|
+
return api_key
|
|
5968
|
+
|
|
5969
|
+
{{/each}}
|
|
5970
|
+
{{/if}}
|
|
5971
|
+
def get_{{snakeCase name}}_mcp_client() -> MCPClient | None:
|
|
5972
|
+
"""Returns an MCP Client for the {{name}} remote MCP server."""
|
|
5973
|
+
url = {{safeJson url}}
|
|
5974
|
+
{{#if headerCredentials}}
|
|
5975
|
+
if os.getenv("LOCAL_DEV") == "1":
|
|
5976
|
+
headers = { {{#each headerCredentials}}{{safeJson headerKey}}: os.environ.get("{{envVarName}}", ""){{#unless @last}}, {{/unless}}{{/each}} }
|
|
5977
|
+
else:
|
|
5978
|
+
headers = { {{#each headerCredentials}}{{safeJson headerKey}}: _get_{{snakeCase ../name}}_{{snakeCase headerKey}}_key(){{#unless @last}}, {{/unless}}{{/each}} }
|
|
5979
|
+
return MCPClient(lambda: streamablehttp_client(url, headers=headers))
|
|
5980
|
+
{{else}}
|
|
5981
|
+
return MCPClient(lambda: streamablehttp_client(url))
|
|
5982
|
+
{{/if}}
|
|
5983
|
+
|
|
5984
|
+
{{/each}}
|
|
5985
|
+
def get_all_remote_mcp_clients() -> list[MCPClient]:
|
|
5986
|
+
"""Returns all configured remote MCP clients."""
|
|
5987
|
+
clients = [{{#each remoteMcpTools}}get_{{snakeCase name}}_mcp_client(){{#unless @last}}, {{/unless}}{{/each}}]
|
|
5988
|
+
return [c for c in clients if c is not None]
|
|
5989
|
+
{{/if}}
|
|
5990
|
+
{{#unless (or hasGateway remoteMcpTools)}}
|
|
5248
5991
|
{{#if isVpc}}
|
|
5249
5992
|
# VPC mode: external MCP endpoints are not reachable without a NAT gateway.
|
|
5250
5993
|
# Add an AgentCore Gateway with \`agentcore add gateway\`, or configure your own endpoint below.
|
|
@@ -5253,6 +5996,7 @@ def get_streamable_http_mcp_client() -> MCPClient | None:
|
|
|
5253
5996
|
"""No MCP server configured. Add a gateway with \`agentcore add gateway\`."""
|
|
5254
5997
|
return None
|
|
5255
5998
|
{{else}}
|
|
5999
|
+
{{#unless isExportHarness}}
|
|
5256
6000
|
# ExaAI provides information about code through web searches, crawling and code context searches through their platform. Requires no authentication
|
|
5257
6001
|
EXAMPLE_MCP_ENDPOINT = "https://mcp.exa.ai/mcp"
|
|
5258
6002
|
|
|
@@ -5260,8 +6004,9 @@ def get_streamable_http_mcp_client() -> MCPClient:
|
|
|
5260
6004
|
"""Returns an MCP Client compatible with Strands"""
|
|
5261
6005
|
# to use an MCP server that supports bearer authentication, add headers={"Authorization": f"Bearer {access_token}"}
|
|
5262
6006
|
return MCPClient(lambda: streamablehttp_client(EXAMPLE_MCP_ENDPOINT))
|
|
6007
|
+
{{/unless}}
|
|
5263
6008
|
{{/if}}
|
|
5264
|
-
{{/
|
|
6009
|
+
{{/unless}}
|
|
5265
6010
|
"
|
|
5266
6011
|
`;
|
|
5267
6012
|
|
|
@@ -5277,7 +6022,7 @@ from strands.models.bedrock import BedrockModel
|
|
|
5277
6022
|
|
|
5278
6023
|
def load_model() -> BedrockModel:
|
|
5279
6024
|
"""Get Bedrock model client using IAM credentials."""
|
|
5280
|
-
return BedrockModel(model_id="global.anthropic.claude-sonnet-4-5-20250929-v1:0")
|
|
6025
|
+
return BedrockModel(model_id="{{#if modelId}}{{modelId}}{{else}}global.anthropic.claude-sonnet-4-5-20250929-v1:0{{/if}}")
|
|
5281
6026
|
{{/if}}
|
|
5282
6027
|
{{#if (eq modelProvider "Anthropic")}}
|
|
5283
6028
|
import os
|
|
@@ -5353,7 +6098,7 @@ def load_model() -> OpenAIModel:
|
|
|
5353
6098
|
"""Get authenticated OpenAI model client."""
|
|
5354
6099
|
return OpenAIModel(
|
|
5355
6100
|
client_args={"api_key": _get_api_key()},
|
|
5356
|
-
model_id="gpt-4.1",
|
|
6101
|
+
model_id="{{#if modelId}}{{modelId}}{{else}}gpt-4.1{{/if}}",
|
|
5357
6102
|
)
|
|
5358
6103
|
{{/if}}
|
|
5359
6104
|
{{#if (eq modelProvider "Gemini")}}
|
|
@@ -5391,7 +6136,7 @@ def load_model() -> GeminiModel:
|
|
|
5391
6136
|
"""Get authenticated Gemini model client."""
|
|
5392
6137
|
return GeminiModel(
|
|
5393
6138
|
client_args={"api_key": _get_api_key()},
|
|
5394
|
-
model_id="gemini-2.5-flash",
|
|
6139
|
+
model_id="{{#if modelId}}{{modelId}}{{else}}gemini-2.5-flash{{/if}}",
|
|
5395
6140
|
)
|
|
5396
6141
|
{{/if}}
|
|
5397
6142
|
"
|
|
@@ -5416,8 +6161,11 @@ dependencies = [
|
|
|
5416
6161
|
{{#if (eq modelProvider "Gemini")}}"google-genai >= 1.0.0",
|
|
5417
6162
|
{{/if}}"mcp >= 1.19.0",
|
|
5418
6163
|
{{#if (eq modelProvider "OpenAI")}}"openai >= 1.0.0",
|
|
5419
|
-
{{/if}}"strands-agents >= 1.
|
|
5420
|
-
{{#if
|
|
6164
|
+
{{/if}}"strands-agents >= 1.15.0",
|
|
6165
|
+
{{#if (or hasBrowser hasCodeInterpreter)}}"strands-agents-tools >= 0.1.0",
|
|
6166
|
+
{{/if}}{{#if hasBrowser}}"nest-asyncio >= 1.5.0",
|
|
6167
|
+
"playwright >= 1.42.0",
|
|
6168
|
+
{{/if}}{{#if hasGateway}}{{#if (includes gatewayAuthTypes "AWS_IAM")}}"mcp-proxy-for-aws >= 1.1.0",
|
|
5421
6169
|
{{/if}}{{/if}}
|
|
5422
6170
|
]
|
|
5423
6171
|
|
|
@@ -5426,6 +6174,347 @@ packages = ["."]
|
|
|
5426
6174
|
"
|
|
5427
6175
|
`;
|
|
5428
6176
|
|
|
6177
|
+
exports[`Assets Directory Snapshots > Python framework assets > python/python/http/strands/base/skills/fetcher.py should match snapshot 1`] = `
|
|
6178
|
+
""""Skill fetcher — downloads s3/git skills to local filesystem on first use.
|
|
6179
|
+
|
|
6180
|
+
Resolved paths are passed to AgentSkills(skills=...) in main.py.
|
|
6181
|
+
Cache directory: <tmpdir>/.agents/skills/ — an absolute path under the system temp
|
|
6182
|
+
directory (honors $TMPDIR, defaults to /tmp). The runtime working directory (e.g.
|
|
6183
|
+
/var/task in a CodeZip runtime) is read-only, so the cache must live somewhere
|
|
6184
|
+
guaranteed-writable.
|
|
6185
|
+
"""
|
|
6186
|
+
|
|
6187
|
+
import base64
|
|
6188
|
+
import hashlib
|
|
6189
|
+
import json
|
|
6190
|
+
import logging
|
|
6191
|
+
import os
|
|
6192
|
+
import shutil
|
|
6193
|
+
import subprocess
|
|
6194
|
+
import tempfile
|
|
6195
|
+
from pathlib import Path
|
|
6196
|
+
from typing import Optional
|
|
6197
|
+
|
|
6198
|
+
logger = logging.getLogger(__name__)
|
|
6199
|
+
|
|
6200
|
+
_SKILLS_BASE = Path(tempfile.gettempdir()) / ".agents" / "skills"
|
|
6201
|
+
_GIT_TIMEOUT = 60
|
|
6202
|
+
_S3_MAX_SIZE_BYTES = 1 * 1024 * 1024 * 1024 # 1 GB
|
|
6203
|
+
|
|
6204
|
+
|
|
6205
|
+
def _stable_hash(value: str) -> str:
|
|
6206
|
+
return hashlib.sha256(value.encode()).hexdigest()[:12]
|
|
6207
|
+
|
|
6208
|
+
|
|
6209
|
+
def _cleanup(path: Path) -> None:
|
|
6210
|
+
"""Remove a partially-created skill directory so retries don't see stale state."""
|
|
6211
|
+
shutil.rmtree(path, ignore_errors=True)
|
|
6212
|
+
|
|
6213
|
+
|
|
6214
|
+
def _read_map(type_dir: Path) -> dict:
|
|
6215
|
+
map_file = type_dir / ".map.json"
|
|
6216
|
+
return json.loads(map_file.read_text()) if map_file.exists() else {}
|
|
6217
|
+
|
|
6218
|
+
|
|
6219
|
+
def _write_map(type_dir: Path, mapping: dict) -> None:
|
|
6220
|
+
type_dir.mkdir(parents=True, exist_ok=True)
|
|
6221
|
+
(type_dir / ".map.json").write_text(json.dumps(mapping))
|
|
6222
|
+
|
|
6223
|
+
|
|
6224
|
+
def _resolve_cached(type_dir: Path, source_hash: str) -> Optional[str]:
|
|
6225
|
+
"""Return the cached skill directory for a source hash, or None if not on disk."""
|
|
6226
|
+
mapping = _read_map(type_dir)
|
|
6227
|
+
dir_name = mapping.get(source_hash)
|
|
6228
|
+
if dir_name and (type_dir / dir_name).exists():
|
|
6229
|
+
return str(type_dir / dir_name)
|
|
6230
|
+
return None
|
|
6231
|
+
|
|
6232
|
+
|
|
6233
|
+
def _read_skill_name(skill_dir: Path) -> str:
|
|
6234
|
+
"""Extract the skill name from SKILL.md YAML frontmatter."""
|
|
6235
|
+
content = (skill_dir / "SKILL.md").read_text()
|
|
6236
|
+
if not content.startswith("---"):
|
|
6237
|
+
raise ValueError(f"SKILL.md in {skill_dir} has no YAML frontmatter (must start with ---)")
|
|
6238
|
+
parts = content.split("---", 2)
|
|
6239
|
+
if len(parts) < 3:
|
|
6240
|
+
raise ValueError(f"SKILL.md in {skill_dir} has malformed frontmatter (missing closing ---)")
|
|
6241
|
+
for line in parts[1].strip().splitlines():
|
|
6242
|
+
if line.startswith("name:"):
|
|
6243
|
+
name = line[len("name:"):].strip().strip("\\"'")
|
|
6244
|
+
if name:
|
|
6245
|
+
return name
|
|
6246
|
+
raise ValueError(f"SKILL.md in {skill_dir} is missing a 'name' field in frontmatter")
|
|
6247
|
+
|
|
6248
|
+
|
|
6249
|
+
def _pick_dir_name(type_dir: Path, name: str, source_hash: str) -> str:
|
|
6250
|
+
"""Pick a unique directory name, appending a hash suffix on collision."""
|
|
6251
|
+
if not (type_dir / name).exists():
|
|
6252
|
+
return name
|
|
6253
|
+
return f"{name}-{source_hash[:8]}"
|
|
6254
|
+
|
|
6255
|
+
|
|
6256
|
+
def _rename_and_cache_skill(type_dir: Path, temp_dir: Path, source_hash: str, skill_root: Path,
|
|
6257
|
+
source_label: str = "") -> Path:
|
|
6258
|
+
"""Validate SKILL.md, rename the temp dir to the skill's declared name, and update the map.
|
|
6259
|
+
|
|
6260
|
+
Raises ValueError if SKILL.md is missing or has invalid frontmatter.
|
|
6261
|
+
"""
|
|
6262
|
+
if not (skill_root / "SKILL.md").exists():
|
|
6263
|
+
_cleanup(temp_dir)
|
|
6264
|
+
hint = f" (source: {source_label})" if source_label else ""
|
|
6265
|
+
raise ValueError(f"No SKILL.md found in fetched skill{hint}")
|
|
6266
|
+
|
|
6267
|
+
name = _read_skill_name(skill_root)
|
|
6268
|
+
dir_name = _pick_dir_name(type_dir, name, source_hash)
|
|
6269
|
+
final_dir = type_dir / dir_name
|
|
6270
|
+
if final_dir != temp_dir:
|
|
6271
|
+
temp_dir.rename(final_dir)
|
|
6272
|
+
|
|
6273
|
+
mapping = _read_map(type_dir)
|
|
6274
|
+
mapping[source_hash] = dir_name
|
|
6275
|
+
_write_map(type_dir, mapping)
|
|
6276
|
+
return final_dir
|
|
6277
|
+
|
|
6278
|
+
|
|
6279
|
+
def _fetch_s3_skill(source: str, s3_client=None) -> Path:
|
|
6280
|
+
"""Download an s3:// skill prefix and return the local directory."""
|
|
6281
|
+
uri = source if source.endswith("/") else source + "/"
|
|
6282
|
+
source_hash = _stable_hash(uri)
|
|
6283
|
+
type_dir = _SKILLS_BASE / "s3"
|
|
6284
|
+
|
|
6285
|
+
cached = _resolve_cached(type_dir, source_hash)
|
|
6286
|
+
if cached:
|
|
6287
|
+
return Path(cached)
|
|
6288
|
+
|
|
6289
|
+
import boto3
|
|
6290
|
+
client = s3_client or boto3.client("s3")
|
|
6291
|
+
bucket, _, prefix = uri[len("s3://"):].partition("/")
|
|
6292
|
+
if not bucket:
|
|
6293
|
+
raise ValueError(f"Invalid S3 URI (no bucket): {uri}")
|
|
6294
|
+
|
|
6295
|
+
temp_dir = type_dir / source_hash
|
|
6296
|
+
_cleanup(temp_dir)
|
|
6297
|
+
temp_dir.mkdir(parents=True, exist_ok=True)
|
|
6298
|
+
temp_root = temp_dir.resolve()
|
|
6299
|
+
|
|
6300
|
+
paginator = client.get_paginator("list_objects_v2")
|
|
6301
|
+
total = 0
|
|
6302
|
+
for page in paginator.paginate(Bucket=bucket, Prefix=prefix):
|
|
6303
|
+
for obj in page.get("Contents", []):
|
|
6304
|
+
total += obj["Size"]
|
|
6305
|
+
if total > _S3_MAX_SIZE_BYTES:
|
|
6306
|
+
_cleanup(temp_dir)
|
|
6307
|
+
raise ValueError(f"S3 skill {uri} exceeds 1 GB size limit")
|
|
6308
|
+
rel = obj["Key"][len(prefix):].lstrip("/")
|
|
6309
|
+
if not rel:
|
|
6310
|
+
continue
|
|
6311
|
+
dest = (temp_dir / rel).resolve()
|
|
6312
|
+
if dest != temp_root and not str(dest).startswith(str(temp_root) + os.sep):
|
|
6313
|
+
_cleanup(temp_dir)
|
|
6314
|
+
raise ValueError(f"Path traversal detected in S3 key: {obj['Key']}")
|
|
6315
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
6316
|
+
client.download_file(bucket, obj["Key"], str(dest))
|
|
6317
|
+
|
|
6318
|
+
if total == 0:
|
|
6319
|
+
_cleanup(temp_dir)
|
|
6320
|
+
raise ValueError(f"No files found at S3 URI: {uri}")
|
|
6321
|
+
|
|
6322
|
+
return _rename_and_cache_skill(type_dir, temp_dir, source_hash, temp_dir, source_label=uri)
|
|
6323
|
+
|
|
6324
|
+
|
|
6325
|
+
def _resolve_credential_arn(credential_arn: str, identity_client) -> str:
|
|
6326
|
+
"""Resolve a Token Vault API-key credential ARN to its secret value via AgentCore Identity.
|
|
6327
|
+
|
|
6328
|
+
ARN format: arn:<p>:bedrock-agentcore:<region>:<account>:token-vault/<vault>/apikeycredentialprovider/<name>
|
|
6329
|
+
"""
|
|
6330
|
+
from bedrock_agentcore.runtime.context import BedrockAgentCoreContext # noqa: PLC0415
|
|
6331
|
+
|
|
6332
|
+
provider_name = credential_arn.rsplit("/", 1)[-1]
|
|
6333
|
+
if not provider_name:
|
|
6334
|
+
raise ValueError(f"Invalid credential ARN: {credential_arn}")
|
|
6335
|
+
workload_token = BedrockAgentCoreContext.get_workload_access_token()
|
|
6336
|
+
if not workload_token:
|
|
6337
|
+
raise ValueError("Credential ARN resolution requires a workload access token")
|
|
6338
|
+
api_key = identity_client.dp_client.get_resource_api_key(
|
|
6339
|
+
resourceCredentialProviderName=provider_name,
|
|
6340
|
+
workloadIdentityToken=workload_token,
|
|
6341
|
+
)["apiKey"]
|
|
6342
|
+
if not api_key:
|
|
6343
|
+
raise ValueError(f"Identity returned empty API key for provider: {provider_name}")
|
|
6344
|
+
return api_key
|
|
6345
|
+
|
|
6346
|
+
|
|
6347
|
+
def _build_git_auth_env(credential_arn: Optional[str], username: Optional[str], identity_client=None) -> dict:
|
|
6348
|
+
"""Build GIT_CONFIG_* env vars for HTTP Basic auth using a Token Vault credential ARN.
|
|
6349
|
+
|
|
6350
|
+
Uses env vars instead of -c args to avoid leaking credentials in /proc/*/cmdline,
|
|
6351
|
+
and so auth propagates to sub-commands (e.g. sparse-checkout triggering a fetch).
|
|
6352
|
+
"""
|
|
6353
|
+
if not credential_arn or not identity_client:
|
|
6354
|
+
return {}
|
|
6355
|
+
password = _resolve_credential_arn(credential_arn, identity_client)
|
|
6356
|
+
user = username or "oauth2"
|
|
6357
|
+
encoded = base64.b64encode(f"{user}:{password}".encode()).decode()
|
|
6358
|
+
return {
|
|
6359
|
+
"GIT_CONFIG_COUNT": "1",
|
|
6360
|
+
"GIT_CONFIG_KEY_0": "http.extraHeader",
|
|
6361
|
+
"GIT_CONFIG_VALUE_0": f"Authorization: Basic {encoded}",
|
|
6362
|
+
}
|
|
6363
|
+
|
|
6364
|
+
|
|
6365
|
+
def _fetch_git_skill(url: str, skill_path: str = "", credential_arn: Optional[str] = None,
|
|
6366
|
+
username: Optional[str] = None, identity_client=None) -> Path:
|
|
6367
|
+
"""Shallow-clone a git skill repository and return the local skill directory.
|
|
6368
|
+
|
|
6369
|
+
Returns the directory containing SKILL.md (the subdir itself for sparse checkouts).
|
|
6370
|
+
"""
|
|
6371
|
+
if skill_path and (os.path.isabs(skill_path) or ".." in Path(skill_path).parts):
|
|
6372
|
+
raise ValueError(f"Path traversal detected in skill path: {skill_path}")
|
|
6373
|
+
|
|
6374
|
+
source_hash = _stable_hash(f"{url}:{skill_path}")
|
|
6375
|
+
type_dir = _SKILLS_BASE / "git"
|
|
6376
|
+
|
|
6377
|
+
cached = _resolve_cached(type_dir, source_hash)
|
|
6378
|
+
if cached:
|
|
6379
|
+
return Path(cached) / skill_path if skill_path else Path(cached)
|
|
6380
|
+
|
|
6381
|
+
temp_dir = type_dir / source_hash
|
|
6382
|
+
_cleanup(temp_dir)
|
|
6383
|
+
temp_dir.mkdir(parents=True, exist_ok=True)
|
|
6384
|
+
|
|
6385
|
+
extra_env = _build_git_auth_env(credential_arn, username, identity_client)
|
|
6386
|
+
git_env = {**os.environ, **extra_env} if extra_env else None
|
|
6387
|
+
|
|
6388
|
+
try:
|
|
6389
|
+
if skill_path:
|
|
6390
|
+
subprocess.run(
|
|
6391
|
+
["git", "clone", "--depth", "1", "--filter=blob:none", "--sparse", url, str(temp_dir)],
|
|
6392
|
+
check=True, timeout=_GIT_TIMEOUT, capture_output=True, env=git_env,
|
|
6393
|
+
)
|
|
6394
|
+
subprocess.run(
|
|
6395
|
+
["git", "sparse-checkout", "set", skill_path],
|
|
6396
|
+
check=True, timeout=_GIT_TIMEOUT, capture_output=True, cwd=str(temp_dir), env=git_env,
|
|
6397
|
+
)
|
|
6398
|
+
else:
|
|
6399
|
+
subprocess.run(
|
|
6400
|
+
["git", "clone", "--depth", "1", url, str(temp_dir)],
|
|
6401
|
+
check=True, timeout=_GIT_TIMEOUT, capture_output=True, env=git_env,
|
|
6402
|
+
)
|
|
6403
|
+
except Exception:
|
|
6404
|
+
_cleanup(temp_dir)
|
|
6405
|
+
raise
|
|
6406
|
+
|
|
6407
|
+
if skill_path and not (temp_dir / skill_path).exists():
|
|
6408
|
+
_cleanup(temp_dir)
|
|
6409
|
+
raise ValueError(f"Skill path '{skill_path}' not found in repository '{url}'")
|
|
6410
|
+
|
|
6411
|
+
# SKILL.md lives inside the subdir for sparse checkouts.
|
|
6412
|
+
skill_root = temp_dir / skill_path if skill_path else temp_dir
|
|
6413
|
+
label = f"{url}:{skill_path}" if skill_path else url
|
|
6414
|
+
final_dir = _rename_and_cache_skill(type_dir, temp_dir, source_hash, skill_root, source_label=label)
|
|
6415
|
+
return final_dir / skill_path if skill_path else final_dir
|
|
6416
|
+
|
|
6417
|
+
|
|
6418
|
+
def resolve_s3_skills(sources: list, s3_client=None) -> list:
|
|
6419
|
+
"""Resolve s3:// skill URIs to local filesystem paths.
|
|
6420
|
+
|
|
6421
|
+
Any fetch failure raises and fails the invocation — a partial skill set
|
|
6422
|
+
would silently run the agent without capabilities the harness declared.
|
|
6423
|
+
"""
|
|
6424
|
+
paths = []
|
|
6425
|
+
for uri in sources:
|
|
6426
|
+
try:
|
|
6427
|
+
skill_dir = _fetch_s3_skill(uri, s3_client)
|
|
6428
|
+
except Exception as e:
|
|
6429
|
+
raise ValueError(f"Failed to resolve S3 skill '{uri}': {e}") from e
|
|
6430
|
+
paths.append(str(skill_dir.resolve()))
|
|
6431
|
+
return paths
|
|
6432
|
+
|
|
6433
|
+
|
|
6434
|
+
def resolve_git_skills(sources: list, identity_client=None) -> list:
|
|
6435
|
+
"""Resolve git skill dicts to local filesystem paths.
|
|
6436
|
+
|
|
6437
|
+
Each source is a dict with keys: url (required), path (optional),
|
|
6438
|
+
credentialArn (optional), username (optional).
|
|
6439
|
+
|
|
6440
|
+
Any fetch failure raises and fails the invocation — a partial skill set
|
|
6441
|
+
would silently run the agent without capabilities the harness declared.
|
|
6442
|
+
"""
|
|
6443
|
+
paths = []
|
|
6444
|
+
for source in sources:
|
|
6445
|
+
try:
|
|
6446
|
+
skill_dir = _fetch_git_skill(
|
|
6447
|
+
url=source["url"],
|
|
6448
|
+
skill_path=source.get("path") or "",
|
|
6449
|
+
credential_arn=source.get("credentialArn"),
|
|
6450
|
+
username=source.get("username"),
|
|
6451
|
+
identity_client=identity_client,
|
|
6452
|
+
)
|
|
6453
|
+
except Exception as e:
|
|
6454
|
+
raise ValueError(f"Failed to resolve git skill '{source.get('url', source)}': {e}") from e
|
|
6455
|
+
paths.append(str(skill_dir.resolve()))
|
|
6456
|
+
return paths
|
|
6457
|
+
"
|
|
6458
|
+
`;
|
|
6459
|
+
|
|
6460
|
+
exports[`Assets Directory Snapshots > Python framework assets > python/python/http/strands/capabilities/execution-limits/hooks/execution_limits.py should match snapshot 1`] = `
|
|
6461
|
+
"import time
|
|
6462
|
+
from typing import Optional
|
|
6463
|
+
|
|
6464
|
+
from strands.hooks import BeforeModelCallEvent
|
|
6465
|
+
from strands.hooks.registry import HookProvider, HookRegistry
|
|
6466
|
+
from strands.types.exceptions import EventLoopException
|
|
6467
|
+
|
|
6468
|
+
|
|
6469
|
+
class ExecutionLimitExceeded(Exception):
|
|
6470
|
+
def __init__(self, message: str) -> None:
|
|
6471
|
+
super().__init__(message)
|
|
6472
|
+
|
|
6473
|
+
|
|
6474
|
+
class ExecutionLimitsHook(HookProvider):
|
|
6475
|
+
def __init__(
|
|
6476
|
+
self,
|
|
6477
|
+
max_iterations: Optional[int] = None,
|
|
6478
|
+
max_tokens: Optional[int] = None,
|
|
6479
|
+
timeout_seconds: Optional[float] = None,
|
|
6480
|
+
) -> None:
|
|
6481
|
+
self._max_iterations = max_iterations
|
|
6482
|
+
self._max_tokens = max_tokens
|
|
6483
|
+
self._timeout_seconds = timeout_seconds
|
|
6484
|
+
self._iteration_count = 0
|
|
6485
|
+
self._start_time = time.monotonic()
|
|
6486
|
+
|
|
6487
|
+
def register_hooks(self, registry: HookRegistry, **kwargs) -> None:
|
|
6488
|
+
registry.add_callback(BeforeModelCallEvent, self._check_limits)
|
|
6489
|
+
|
|
6490
|
+
def _check_limits(self, event: BeforeModelCallEvent) -> None:
|
|
6491
|
+
self._iteration_count += 1
|
|
6492
|
+
|
|
6493
|
+
if self._max_iterations is not None and self._iteration_count > self._max_iterations:
|
|
6494
|
+
raise EventLoopException(
|
|
6495
|
+
ExecutionLimitExceeded(f"Max iterations exceeded: {self._max_iterations}")
|
|
6496
|
+
)
|
|
6497
|
+
|
|
6498
|
+
if self._timeout_seconds is not None:
|
|
6499
|
+
elapsed = time.monotonic() - self._start_time
|
|
6500
|
+
if elapsed > self._timeout_seconds:
|
|
6501
|
+
raise EventLoopException(
|
|
6502
|
+
ExecutionLimitExceeded(
|
|
6503
|
+
f"Timeout exceeded: {self._timeout_seconds}s (elapsed {elapsed:.1f}s)"
|
|
6504
|
+
)
|
|
6505
|
+
)
|
|
6506
|
+
|
|
6507
|
+
if self._max_tokens is not None:
|
|
6508
|
+
used = event.agent.event_loop_metrics.accumulated_usage.get("outputTokens", 0)
|
|
6509
|
+
if used >= self._max_tokens:
|
|
6510
|
+
raise EventLoopException(
|
|
6511
|
+
ExecutionLimitExceeded(
|
|
6512
|
+
f"Max output tokens exceeded: {used}/{self._max_tokens}"
|
|
6513
|
+
)
|
|
6514
|
+
)
|
|
6515
|
+
"
|
|
6516
|
+
`;
|
|
6517
|
+
|
|
5429
6518
|
exports[`Assets Directory Snapshots > Python framework assets > python/python/http/strands/capabilities/memory/__init__.py should match snapshot 1`] = `
|
|
5430
6519
|
"# Package marker
|
|
5431
6520
|
"
|
|
@@ -5458,8 +6547,11 @@ def get_memory_session_manager(session_id: Optional[str], actor_id: str) -> Opti
|
|
|
5458
6547
|
{{#if (includes memoryProviders.[0].strategies "USER_PREFERENCE")}}
|
|
5459
6548
|
f"/users/{actor_id}/preferences": RetrievalConfig(top_k=3, relevance_score=0.5),
|
|
5460
6549
|
{{/if}}
|
|
6550
|
+
{{#if (includes memoryProviders.[0].strategies "EPISODIC")}}
|
|
6551
|
+
f"/episodes/{actor_id}/{session_id}": RetrievalConfig(top_k=5, relevance_score=0.5),
|
|
6552
|
+
{{/if}}
|
|
5461
6553
|
{{#if (includes memoryProviders.[0].strategies "SUMMARIZATION")}}
|
|
5462
|
-
f"/summaries/{actor_id}": RetrievalConfig(top_k=3, relevance_score=0.5),
|
|
6554
|
+
f"/summaries/{actor_id}/{session_id}": RetrievalConfig(top_k=3, relevance_score=0.5),
|
|
5463
6555
|
{{/if}}
|
|
5464
6556
|
}
|
|
5465
6557
|
{{/if}}
|
|
@@ -5479,6 +6571,160 @@ def get_memory_session_manager(session_id: Optional[str], actor_id: str) -> Opti
|
|
|
5479
6571
|
"
|
|
5480
6572
|
`;
|
|
5481
6573
|
|
|
6574
|
+
exports[`Assets Directory Snapshots > Python framework assets > python/python/http/strands/capabilities/payments/__init__.py should match snapshot 1`] = `
|
|
6575
|
+
""""Payment capabilities for Strands agents."""
|
|
6576
|
+
from .payments import create_payments_plugin
|
|
6577
|
+
|
|
6578
|
+
__all__ = ["create_payments_plugin"]
|
|
6579
|
+
"
|
|
6580
|
+
`;
|
|
6581
|
+
|
|
6582
|
+
exports[`Assets Directory Snapshots > Python framework assets > python/python/http/strands/capabilities/payments/payments.py should match snapshot 1`] = `
|
|
6583
|
+
""""Payment capability -- auto-generated by agentcore CLI.
|
|
6584
|
+
|
|
6585
|
+
Configures AgentCorePaymentsPlugin for Strands agents.
|
|
6586
|
+
Manager config is auto-discovered from AGENTCORE_PAYMENT_*
|
|
6587
|
+
environment variables set at deploy time.
|
|
6588
|
+
|
|
6589
|
+
Uses a per-invocation factory pattern so each request gets its own
|
|
6590
|
+
plugin instance with the correct user_id, instrument_id, and session_id.
|
|
6591
|
+
This prevents concurrency bugs where one user's payment context
|
|
6592
|
+
could leak to another user's request.
|
|
6593
|
+
|
|
6594
|
+
Uses the official SDK plugin which handles:
|
|
6595
|
+
- x402 v1 (body-based) and v2 (header-based) payment detection
|
|
6596
|
+
- Automatic 402 response interception and payment processing
|
|
6597
|
+
- Retry limiting (max 3 payment retries per tool use)
|
|
6598
|
+
- Error management and logging
|
|
6599
|
+
"""
|
|
6600
|
+
import os
|
|
6601
|
+
import logging
|
|
6602
|
+
|
|
6603
|
+
import boto3
|
|
6604
|
+
from bedrock_agentcore.payments.integrations.strands import AgentCorePaymentsPlugin
|
|
6605
|
+
from bedrock_agentcore.payments.integrations.config import AgentCorePaymentsPluginConfig
|
|
6606
|
+
|
|
6607
|
+
logger = logging.getLogger(__name__)
|
|
6608
|
+
|
|
6609
|
+
PAYMENT_SYSTEM_PROMPT = """
|
|
6610
|
+
You have payment capabilities via the x402 protocol:
|
|
6611
|
+
- Use http_request to call HTTP endpoints. 402 Payment Required responses are settled automatically by the plugin and the call is retried.
|
|
6612
|
+
- Use get_payment_session to check your remaining budget before expensive operations
|
|
6613
|
+
- Use get_payment_instrument_balance to check wallet USDC balance
|
|
6614
|
+
- Use list_payment_instruments to see available payment instruments
|
|
6615
|
+
- If budget is low, inform the user before proceeding with paid requests
|
|
6616
|
+
"""
|
|
6617
|
+
|
|
6618
|
+
_manager_arn = None
|
|
6619
|
+
_connector_id = None
|
|
6620
|
+
_process_payment_role_arn = None
|
|
6621
|
+
_name_segment = None
|
|
6622
|
+
_region = None
|
|
6623
|
+
_auth_mode = None
|
|
6624
|
+
_manager_count = 0
|
|
6625
|
+
for key, value in os.environ.items():
|
|
6626
|
+
if key.startswith("AGENTCORE_PAYMENT_") and key.endswith("_MANAGER_ARN"):
|
|
6627
|
+
if _manager_arn is None:
|
|
6628
|
+
_manager_arn = value
|
|
6629
|
+
_name_segment = key[len("AGENTCORE_PAYMENT_"):-len("_MANAGER_ARN")]
|
|
6630
|
+
_manager_count += 1
|
|
6631
|
+
if _manager_count > 1:
|
|
6632
|
+
logger.warning(
|
|
6633
|
+
"Multiple payment managers detected in environment. Using the first one found. "
|
|
6634
|
+
"Remove extra AGENTCORE_PAYMENT_*_MANAGER_ARN env vars to eliminate ambiguity."
|
|
6635
|
+
)
|
|
6636
|
+
_region = os.getenv("AWS_REGION")
|
|
6637
|
+
|
|
6638
|
+
_prefix = f"AGENTCORE_PAYMENT_{_name_segment}_" if _name_segment else "AGENTCORE_PAYMENT_"
|
|
6639
|
+
_auth_mode = os.getenv(f"{_prefix}AUTH_MODE", "sigv4")
|
|
6640
|
+
_connector_id = os.getenv(f"{_prefix}CONNECTOR_ID")
|
|
6641
|
+
_process_payment_role_arn = os.getenv(f"{_prefix}PROCESS_PAYMENT_ROLE_ARN")
|
|
6642
|
+
_auto_payment = os.getenv(f"{_prefix}AUTO_PAYMENT", "true").lower() == "true"
|
|
6643
|
+
_allowlist_raw = os.getenv(f"{_prefix}TOOL_ALLOWLIST")
|
|
6644
|
+
_allowlist = _allowlist_raw.split(",") if _allowlist_raw else None
|
|
6645
|
+
_network_prefs_raw = os.getenv(f"{_prefix}NETWORK_PREFERENCES")
|
|
6646
|
+
_network_prefs = _network_prefs_raw.split(",") if _network_prefs_raw else None
|
|
6647
|
+
|
|
6648
|
+
if not _manager_arn:
|
|
6649
|
+
logger.warning("No payment manager config found in environment")
|
|
6650
|
+
if not _connector_id:
|
|
6651
|
+
logger.warning("No payment connector config found in environment")
|
|
6652
|
+
|
|
6653
|
+
|
|
6654
|
+
def _assume_role_session(role_arn):
|
|
6655
|
+
"""Assume an IAM role and return a boto3 session with temporary credentials."""
|
|
6656
|
+
sts = boto3.client("sts", region_name=_region)
|
|
6657
|
+
creds = sts.assume_role(
|
|
6658
|
+
RoleArn=role_arn,
|
|
6659
|
+
RoleSessionName="agentcore-payment-plugin",
|
|
6660
|
+
)["Credentials"]
|
|
6661
|
+
return boto3.Session(
|
|
6662
|
+
aws_access_key_id=creds["AccessKeyId"],
|
|
6663
|
+
aws_secret_access_key=creds["SecretAccessKey"],
|
|
6664
|
+
aws_session_token=creds["SessionToken"],
|
|
6665
|
+
region_name=_region,
|
|
6666
|
+
)
|
|
6667
|
+
|
|
6668
|
+
|
|
6669
|
+
def create_payments_plugin(user_id, instrument_id=None, session_id=None):
|
|
6670
|
+
"""Create a fresh plugin instance per invocation.
|
|
6671
|
+
|
|
6672
|
+
Args:
|
|
6673
|
+
user_id: From invocation context (required for SigV4, derived from JWT for bearer)
|
|
6674
|
+
instrument_id: From invocation payload (created by app backend per user)
|
|
6675
|
+
session_id: From invocation payload (created by app backend per conversation)
|
|
6676
|
+
|
|
6677
|
+
Returns:
|
|
6678
|
+
AgentCorePaymentsPlugin instance, or None if no manager is configured.
|
|
6679
|
+
"""
|
|
6680
|
+
if not _manager_arn:
|
|
6681
|
+
return None
|
|
6682
|
+
|
|
6683
|
+
config_kwargs = {
|
|
6684
|
+
"payment_manager_arn": _manager_arn,
|
|
6685
|
+
"region": _region,
|
|
6686
|
+
"payment_instrument_id": instrument_id,
|
|
6687
|
+
"payment_session_id": session_id,
|
|
6688
|
+
"payment_connector_id": _connector_id,
|
|
6689
|
+
}
|
|
6690
|
+
|
|
6691
|
+
config_kwargs["auto_payment"] = _auto_payment
|
|
6692
|
+
if _allowlist:
|
|
6693
|
+
config_kwargs["payment_tool_allowlist"] = _allowlist
|
|
6694
|
+
if _network_prefs:
|
|
6695
|
+
config_kwargs["network_preferences_config"] = _network_prefs
|
|
6696
|
+
|
|
6697
|
+
if _process_payment_role_arn:
|
|
6698
|
+
# Only pass boto3_session if SDK supports it (added in bedrock-agentcore >= 1.11).
|
|
6699
|
+
# Older SDKs use the runtime role's default credentials and can still call ProcessPayment
|
|
6700
|
+
# if the runtime role has been granted permission directly.
|
|
6701
|
+
import inspect
|
|
6702
|
+
if "boto3_session" in inspect.signature(AgentCorePaymentsPluginConfig).parameters:
|
|
6703
|
+
config_kwargs["boto3_session"] = _assume_role_session(_process_payment_role_arn)
|
|
6704
|
+
else:
|
|
6705
|
+
logger.warning(
|
|
6706
|
+
"PROCESS_PAYMENT_ROLE_ARN set but bedrock-agentcore SDK does not support boto3_session. "
|
|
6707
|
+
"Upgrade to bedrock-agentcore>=1.11 to enable cross-role payment processing."
|
|
6708
|
+
)
|
|
6709
|
+
|
|
6710
|
+
if _auth_mode == "bearer":
|
|
6711
|
+
bearer_token = os.getenv("AGENTCORE_BEARER_TOKEN")
|
|
6712
|
+
if bearer_token:
|
|
6713
|
+
config_kwargs["bearer_token"] = bearer_token
|
|
6714
|
+
else:
|
|
6715
|
+
logger.warning(
|
|
6716
|
+
"Bearer auth mode configured but AGENTCORE_BEARER_TOKEN not set. "
|
|
6717
|
+
"Falling back to SigV4. Set AGENTCORE_BEARER_TOKEN or pass bearer_token in invoke context."
|
|
6718
|
+
)
|
|
6719
|
+
config_kwargs["user_id"] = user_id or "default-user"
|
|
6720
|
+
else:
|
|
6721
|
+
config_kwargs["user_id"] = user_id or "default-user"
|
|
6722
|
+
|
|
6723
|
+
config = AgentCorePaymentsPluginConfig(**config_kwargs)
|
|
6724
|
+
return AgentCorePaymentsPlugin(config=config)
|
|
6725
|
+
"
|
|
6726
|
+
`;
|
|
6727
|
+
|
|
5482
6728
|
exports[`Assets Directory Snapshots > Python framework assets > python/python/mcp/standalone/base/README.md should match snapshot 1`] = `
|
|
5483
6729
|
"# {{ name }}
|
|
5484
6730
|
|
|
@@ -5968,6 +7214,19 @@ When modifying JSON config files:
|
|
|
5968
7214
|
4. Use CloudFormation-safe names (alphanumeric, start with letter)
|
|
5969
7215
|
5. Run \`agentcore validate\` to verify changes
|
|
5970
7216
|
|
|
7217
|
+
## Harness Export
|
|
7218
|
+
|
|
7219
|
+
\`agentcore export harness\` converts a harness configuration into a deployable Strands Python agent under \`app/<agentName>/\`.
|
|
7220
|
+
|
|
7221
|
+
**After every export, you MUST read \`app/<agentName>/EXPORT_NOTES.md\` before proceeding.**
|
|
7222
|
+
|
|
7223
|
+
This file lists any manual follow-up items required before the agent will deploy or run correctly — missing files to create, IAM policies to add, or configuration steps the exporter could not automate. A clean export produces "No manual steps required." Complete every item in the file before running \`agentcore deploy\`.
|
|
7224
|
+
|
|
7225
|
+
\`\`\`bash
|
|
7226
|
+
agentcore export harness --name <harnessName> # generates app/<agentName>/EXPORT_NOTES.md
|
|
7227
|
+
cat app/<agentName>/EXPORT_NOTES.md # read this before touching anything else
|
|
7228
|
+
\`\`\`
|
|
7229
|
+
|
|
5971
7230
|
## CLI Commands
|
|
5972
7231
|
|
|
5973
7232
|
| Command | Description |
|
|
@@ -5975,6 +7234,7 @@ When modifying JSON config files:
|
|
|
5975
7234
|
| \`agentcore create\` | Create a new project |
|
|
5976
7235
|
| \`agentcore add <resource>\` | Add agent, memory, credential, gateway, evaluator, policy |
|
|
5977
7236
|
| \`agentcore remove <resource>\` | Remove a resource |
|
|
7237
|
+
| \`agentcore export harness\` | Export a harness to a Strands runtime agent |
|
|
5978
7238
|
| \`agentcore dev\` | Run agent locally with hot-reload |
|
|
5979
7239
|
| \`agentcore deploy\` | Deploy to AWS |
|
|
5980
7240
|
| \`agentcore status\` | Show deployment status |
|