@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
|
@@ -29,18 +29,22 @@ def _get_bearer_token_{{snakeCase name}}(*, access_token: str):
|
|
|
29
29
|
{{#each gatewayProviders}}
|
|
30
30
|
def get_{{snakeCase name}}_mcp_client() -> MCPClient | None:
|
|
31
31
|
"""Returns an MCP Client connected to the {{name}} gateway."""
|
|
32
|
+
{{#if hardcodedUrl}}
|
|
33
|
+
url = {{safeJson hardcodedUrl}}
|
|
34
|
+
{{else}}
|
|
32
35
|
url = os.environ.get("{{envVarName}}")
|
|
33
36
|
if not url:
|
|
34
37
|
logger.warning("{{envVarName}} not set — {{name}} gateway tools unavailable")
|
|
35
38
|
return None
|
|
39
|
+
{{/if}}
|
|
36
40
|
{{#if (eq authType "AWS_IAM")}}
|
|
37
|
-
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"))))
|
|
41
|
+
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}}")
|
|
38
42
|
{{else if (eq authType "CUSTOM_JWT")}}
|
|
39
43
|
token = _get_bearer_token_{{snakeCase name}}()
|
|
40
44
|
headers = {"Authorization": f"Bearer {token}"} if token else {}
|
|
41
|
-
return MCPClient(lambda: streamablehttp_client(url, headers=headers))
|
|
45
|
+
return MCPClient(lambda: streamablehttp_client(url, headers=headers), prefix="{{snakeCase name}}")
|
|
42
46
|
{{else}}
|
|
43
|
-
return MCPClient(lambda: streamablehttp_client(url))
|
|
47
|
+
return MCPClient(lambda: streamablehttp_client(url), prefix="{{snakeCase name}}")
|
|
44
48
|
{{/if}}
|
|
45
49
|
|
|
46
50
|
{{/each}}
|
|
@@ -53,7 +57,41 @@ def get_all_gateway_mcp_clients() -> list[MCPClient]:
|
|
|
53
57
|
clients.append(client)
|
|
54
58
|
{{/each}}
|
|
55
59
|
return clients
|
|
56
|
-
{{
|
|
60
|
+
{{/if}}
|
|
61
|
+
{{#if remoteMcpTools}}
|
|
62
|
+
{{#if (some remoteMcpTools "headerCredentials")}}
|
|
63
|
+
from bedrock_agentcore.identity.auth import requires_api_key
|
|
64
|
+
{{/if}}
|
|
65
|
+
{{#each remoteMcpTools}}
|
|
66
|
+
{{#if headerCredentials}}
|
|
67
|
+
{{#each headerCredentials}}
|
|
68
|
+
@requires_api_key(provider_name="{{credentialName}}")
|
|
69
|
+
def _get_{{snakeCase ../name}}_{{snakeCase headerKey}}_key(api_key: str) -> str:
|
|
70
|
+
"""Fetch {{headerKey}} credential for {{../name}} from AgentCore Identity."""
|
|
71
|
+
return api_key
|
|
72
|
+
|
|
73
|
+
{{/each}}
|
|
74
|
+
{{/if}}
|
|
75
|
+
def get_{{snakeCase name}}_mcp_client() -> MCPClient | None:
|
|
76
|
+
"""Returns an MCP Client for the {{name}} remote MCP server."""
|
|
77
|
+
url = {{safeJson url}}
|
|
78
|
+
{{#if headerCredentials}}
|
|
79
|
+
if os.getenv("LOCAL_DEV") == "1":
|
|
80
|
+
headers = { {{#each headerCredentials}}{{safeJson headerKey}}: os.environ.get("{{envVarName}}", ""){{#unless @last}}, {{/unless}}{{/each}} }
|
|
81
|
+
else:
|
|
82
|
+
headers = { {{#each headerCredentials}}{{safeJson headerKey}}: _get_{{snakeCase ../name}}_{{snakeCase headerKey}}_key(){{#unless @last}}, {{/unless}}{{/each}} }
|
|
83
|
+
return MCPClient(lambda: streamablehttp_client(url, headers=headers))
|
|
84
|
+
{{else}}
|
|
85
|
+
return MCPClient(lambda: streamablehttp_client(url))
|
|
86
|
+
{{/if}}
|
|
87
|
+
|
|
88
|
+
{{/each}}
|
|
89
|
+
def get_all_remote_mcp_clients() -> list[MCPClient]:
|
|
90
|
+
"""Returns all configured remote MCP clients."""
|
|
91
|
+
clients = [{{#each remoteMcpTools}}get_{{snakeCase name}}_mcp_client(){{#unless @last}}, {{/unless}}{{/each}}]
|
|
92
|
+
return [c for c in clients if c is not None]
|
|
93
|
+
{{/if}}
|
|
94
|
+
{{#unless (or hasGateway remoteMcpTools)}}
|
|
57
95
|
{{#if isVpc}}
|
|
58
96
|
# VPC mode: external MCP endpoints are not reachable without a NAT gateway.
|
|
59
97
|
# Add an AgentCore Gateway with `agentcore add gateway`, or configure your own endpoint below.
|
|
@@ -62,6 +100,7 @@ def get_streamable_http_mcp_client() -> MCPClient | None:
|
|
|
62
100
|
"""No MCP server configured. Add a gateway with `agentcore add gateway`."""
|
|
63
101
|
return None
|
|
64
102
|
{{else}}
|
|
103
|
+
{{#unless isExportHarness}}
|
|
65
104
|
# ExaAI provides information about code through web searches, crawling and code context searches through their platform. Requires no authentication
|
|
66
105
|
EXAMPLE_MCP_ENDPOINT = "https://mcp.exa.ai/mcp"
|
|
67
106
|
|
|
@@ -69,5 +108,6 @@ def get_streamable_http_mcp_client() -> MCPClient:
|
|
|
69
108
|
"""Returns an MCP Client compatible with Strands"""
|
|
70
109
|
# to use an MCP server that supports bearer authentication, add headers={"Authorization": f"Bearer {access_token}"}
|
|
71
110
|
return MCPClient(lambda: streamablehttp_client(EXAMPLE_MCP_ENDPOINT))
|
|
111
|
+
{{/unless}}
|
|
72
112
|
{{/if}}
|
|
73
|
-
{{/
|
|
113
|
+
{{/unless}}
|
|
@@ -4,7 +4,7 @@ from strands.models.bedrock import BedrockModel
|
|
|
4
4
|
|
|
5
5
|
def load_model() -> BedrockModel:
|
|
6
6
|
"""Get Bedrock model client using IAM credentials."""
|
|
7
|
-
return BedrockModel(model_id="global.anthropic.claude-sonnet-4-5-20250929-v1:0")
|
|
7
|
+
return BedrockModel(model_id="{{#if modelId}}{{modelId}}{{else}}global.anthropic.claude-sonnet-4-5-20250929-v1:0{{/if}}")
|
|
8
8
|
{{/if}}
|
|
9
9
|
{{#if (eq modelProvider "Anthropic")}}
|
|
10
10
|
import os
|
|
@@ -80,7 +80,7 @@ def load_model() -> OpenAIModel:
|
|
|
80
80
|
"""Get authenticated OpenAI model client."""
|
|
81
81
|
return OpenAIModel(
|
|
82
82
|
client_args={"api_key": _get_api_key()},
|
|
83
|
-
model_id="gpt-4.1",
|
|
83
|
+
model_id="{{#if modelId}}{{modelId}}{{else}}gpt-4.1{{/if}}",
|
|
84
84
|
)
|
|
85
85
|
{{/if}}
|
|
86
86
|
{{#if (eq modelProvider "Gemini")}}
|
|
@@ -118,6 +118,6 @@ def load_model() -> GeminiModel:
|
|
|
118
118
|
"""Get authenticated Gemini model client."""
|
|
119
119
|
return GeminiModel(
|
|
120
120
|
client_args={"api_key": _get_api_key()},
|
|
121
|
-
model_id="gemini-2.5-flash",
|
|
121
|
+
model_id="{{#if modelId}}{{modelId}}{{else}}gemini-2.5-flash{{/if}}",
|
|
122
122
|
)
|
|
123
123
|
{{/if}}
|
|
@@ -16,8 +16,11 @@ dependencies = [
|
|
|
16
16
|
{{#if (eq modelProvider "Gemini")}}"google-genai >= 1.0.0",
|
|
17
17
|
{{/if}}"mcp >= 1.19.0",
|
|
18
18
|
{{#if (eq modelProvider "OpenAI")}}"openai >= 1.0.0",
|
|
19
|
-
{{/if}}"strands-agents >= 1.
|
|
20
|
-
{{#if
|
|
19
|
+
{{/if}}"strands-agents >= 1.15.0",
|
|
20
|
+
{{#if (or hasBrowser hasCodeInterpreter)}}"strands-agents-tools >= 0.1.0",
|
|
21
|
+
{{/if}}{{#if hasBrowser}}"nest-asyncio >= 1.5.0",
|
|
22
|
+
"playwright >= 1.42.0",
|
|
23
|
+
{{/if}}{{#if hasGateway}}{{#if (includes gatewayAuthTypes "AWS_IAM")}}"mcp-proxy-for-aws >= 1.1.0",
|
|
21
24
|
{{/if}}{{/if}}
|
|
22
25
|
]
|
|
23
26
|
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""Skill fetcher — downloads s3/git skills to local filesystem on first use.
|
|
2
|
+
|
|
3
|
+
Resolved paths are passed to AgentSkills(skills=...) in main.py.
|
|
4
|
+
Cache directory: <tmpdir>/.agents/skills/ — an absolute path under the system temp
|
|
5
|
+
directory (honors $TMPDIR, defaults to /tmp). The runtime working directory (e.g.
|
|
6
|
+
/var/task in a CodeZip runtime) is read-only, so the cache must live somewhere
|
|
7
|
+
guaranteed-writable.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import base64
|
|
11
|
+
import hashlib
|
|
12
|
+
import json
|
|
13
|
+
import logging
|
|
14
|
+
import os
|
|
15
|
+
import shutil
|
|
16
|
+
import subprocess
|
|
17
|
+
import tempfile
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Optional
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
_SKILLS_BASE = Path(tempfile.gettempdir()) / ".agents" / "skills"
|
|
24
|
+
_GIT_TIMEOUT = 60
|
|
25
|
+
_S3_MAX_SIZE_BYTES = 1 * 1024 * 1024 * 1024 # 1 GB
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _stable_hash(value: str) -> str:
|
|
29
|
+
return hashlib.sha256(value.encode()).hexdigest()[:12]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _cleanup(path: Path) -> None:
|
|
33
|
+
"""Remove a partially-created skill directory so retries don't see stale state."""
|
|
34
|
+
shutil.rmtree(path, ignore_errors=True)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _read_map(type_dir: Path) -> dict:
|
|
38
|
+
map_file = type_dir / ".map.json"
|
|
39
|
+
return json.loads(map_file.read_text()) if map_file.exists() else {}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _write_map(type_dir: Path, mapping: dict) -> None:
|
|
43
|
+
type_dir.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
(type_dir / ".map.json").write_text(json.dumps(mapping))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _resolve_cached(type_dir: Path, source_hash: str) -> Optional[str]:
|
|
48
|
+
"""Return the cached skill directory for a source hash, or None if not on disk."""
|
|
49
|
+
mapping = _read_map(type_dir)
|
|
50
|
+
dir_name = mapping.get(source_hash)
|
|
51
|
+
if dir_name and (type_dir / dir_name).exists():
|
|
52
|
+
return str(type_dir / dir_name)
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _read_skill_name(skill_dir: Path) -> str:
|
|
57
|
+
"""Extract the skill name from SKILL.md YAML frontmatter."""
|
|
58
|
+
content = (skill_dir / "SKILL.md").read_text()
|
|
59
|
+
if not content.startswith("---"):
|
|
60
|
+
raise ValueError(f"SKILL.md in {skill_dir} has no YAML frontmatter (must start with ---)")
|
|
61
|
+
parts = content.split("---", 2)
|
|
62
|
+
if len(parts) < 3:
|
|
63
|
+
raise ValueError(f"SKILL.md in {skill_dir} has malformed frontmatter (missing closing ---)")
|
|
64
|
+
for line in parts[1].strip().splitlines():
|
|
65
|
+
if line.startswith("name:"):
|
|
66
|
+
name = line[len("name:"):].strip().strip("\"'")
|
|
67
|
+
if name:
|
|
68
|
+
return name
|
|
69
|
+
raise ValueError(f"SKILL.md in {skill_dir} is missing a 'name' field in frontmatter")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _pick_dir_name(type_dir: Path, name: str, source_hash: str) -> str:
|
|
73
|
+
"""Pick a unique directory name, appending a hash suffix on collision."""
|
|
74
|
+
if not (type_dir / name).exists():
|
|
75
|
+
return name
|
|
76
|
+
return f"{name}-{source_hash[:8]}"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _rename_and_cache_skill(type_dir: Path, temp_dir: Path, source_hash: str, skill_root: Path,
|
|
80
|
+
source_label: str = "") -> Path:
|
|
81
|
+
"""Validate SKILL.md, rename the temp dir to the skill's declared name, and update the map.
|
|
82
|
+
|
|
83
|
+
Raises ValueError if SKILL.md is missing or has invalid frontmatter.
|
|
84
|
+
"""
|
|
85
|
+
if not (skill_root / "SKILL.md").exists():
|
|
86
|
+
_cleanup(temp_dir)
|
|
87
|
+
hint = f" (source: {source_label})" if source_label else ""
|
|
88
|
+
raise ValueError(f"No SKILL.md found in fetched skill{hint}")
|
|
89
|
+
|
|
90
|
+
name = _read_skill_name(skill_root)
|
|
91
|
+
dir_name = _pick_dir_name(type_dir, name, source_hash)
|
|
92
|
+
final_dir = type_dir / dir_name
|
|
93
|
+
if final_dir != temp_dir:
|
|
94
|
+
temp_dir.rename(final_dir)
|
|
95
|
+
|
|
96
|
+
mapping = _read_map(type_dir)
|
|
97
|
+
mapping[source_hash] = dir_name
|
|
98
|
+
_write_map(type_dir, mapping)
|
|
99
|
+
return final_dir
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _fetch_s3_skill(source: str, s3_client=None) -> Path:
|
|
103
|
+
"""Download an s3:// skill prefix and return the local directory."""
|
|
104
|
+
uri = source if source.endswith("/") else source + "/"
|
|
105
|
+
source_hash = _stable_hash(uri)
|
|
106
|
+
type_dir = _SKILLS_BASE / "s3"
|
|
107
|
+
|
|
108
|
+
cached = _resolve_cached(type_dir, source_hash)
|
|
109
|
+
if cached:
|
|
110
|
+
return Path(cached)
|
|
111
|
+
|
|
112
|
+
import boto3
|
|
113
|
+
client = s3_client or boto3.client("s3")
|
|
114
|
+
bucket, _, prefix = uri[len("s3://"):].partition("/")
|
|
115
|
+
if not bucket:
|
|
116
|
+
raise ValueError(f"Invalid S3 URI (no bucket): {uri}")
|
|
117
|
+
|
|
118
|
+
temp_dir = type_dir / source_hash
|
|
119
|
+
_cleanup(temp_dir)
|
|
120
|
+
temp_dir.mkdir(parents=True, exist_ok=True)
|
|
121
|
+
temp_root = temp_dir.resolve()
|
|
122
|
+
|
|
123
|
+
paginator = client.get_paginator("list_objects_v2")
|
|
124
|
+
total = 0
|
|
125
|
+
for page in paginator.paginate(Bucket=bucket, Prefix=prefix):
|
|
126
|
+
for obj in page.get("Contents", []):
|
|
127
|
+
total += obj["Size"]
|
|
128
|
+
if total > _S3_MAX_SIZE_BYTES:
|
|
129
|
+
_cleanup(temp_dir)
|
|
130
|
+
raise ValueError(f"S3 skill {uri} exceeds 1 GB size limit")
|
|
131
|
+
rel = obj["Key"][len(prefix):].lstrip("/")
|
|
132
|
+
if not rel:
|
|
133
|
+
continue
|
|
134
|
+
dest = (temp_dir / rel).resolve()
|
|
135
|
+
if dest != temp_root and not str(dest).startswith(str(temp_root) + os.sep):
|
|
136
|
+
_cleanup(temp_dir)
|
|
137
|
+
raise ValueError(f"Path traversal detected in S3 key: {obj['Key']}")
|
|
138
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
139
|
+
client.download_file(bucket, obj["Key"], str(dest))
|
|
140
|
+
|
|
141
|
+
if total == 0:
|
|
142
|
+
_cleanup(temp_dir)
|
|
143
|
+
raise ValueError(f"No files found at S3 URI: {uri}")
|
|
144
|
+
|
|
145
|
+
return _rename_and_cache_skill(type_dir, temp_dir, source_hash, temp_dir, source_label=uri)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _resolve_credential_arn(credential_arn: str, identity_client) -> str:
|
|
149
|
+
"""Resolve a Token Vault API-key credential ARN to its secret value via AgentCore Identity.
|
|
150
|
+
|
|
151
|
+
ARN format: arn:<p>:bedrock-agentcore:<region>:<account>:token-vault/<vault>/apikeycredentialprovider/<name>
|
|
152
|
+
"""
|
|
153
|
+
from bedrock_agentcore.runtime.context import BedrockAgentCoreContext # noqa: PLC0415
|
|
154
|
+
|
|
155
|
+
provider_name = credential_arn.rsplit("/", 1)[-1]
|
|
156
|
+
if not provider_name:
|
|
157
|
+
raise ValueError(f"Invalid credential ARN: {credential_arn}")
|
|
158
|
+
workload_token = BedrockAgentCoreContext.get_workload_access_token()
|
|
159
|
+
if not workload_token:
|
|
160
|
+
raise ValueError("Credential ARN resolution requires a workload access token")
|
|
161
|
+
api_key = identity_client.dp_client.get_resource_api_key(
|
|
162
|
+
resourceCredentialProviderName=provider_name,
|
|
163
|
+
workloadIdentityToken=workload_token,
|
|
164
|
+
)["apiKey"]
|
|
165
|
+
if not api_key:
|
|
166
|
+
raise ValueError(f"Identity returned empty API key for provider: {provider_name}")
|
|
167
|
+
return api_key
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _build_git_auth_env(credential_arn: Optional[str], username: Optional[str], identity_client=None) -> dict:
|
|
171
|
+
"""Build GIT_CONFIG_* env vars for HTTP Basic auth using a Token Vault credential ARN.
|
|
172
|
+
|
|
173
|
+
Uses env vars instead of -c args to avoid leaking credentials in /proc/*/cmdline,
|
|
174
|
+
and so auth propagates to sub-commands (e.g. sparse-checkout triggering a fetch).
|
|
175
|
+
"""
|
|
176
|
+
if not credential_arn or not identity_client:
|
|
177
|
+
return {}
|
|
178
|
+
password = _resolve_credential_arn(credential_arn, identity_client)
|
|
179
|
+
user = username or "oauth2"
|
|
180
|
+
encoded = base64.b64encode(f"{user}:{password}".encode()).decode()
|
|
181
|
+
return {
|
|
182
|
+
"GIT_CONFIG_COUNT": "1",
|
|
183
|
+
"GIT_CONFIG_KEY_0": "http.extraHeader",
|
|
184
|
+
"GIT_CONFIG_VALUE_0": f"Authorization: Basic {encoded}",
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _fetch_git_skill(url: str, skill_path: str = "", credential_arn: Optional[str] = None,
|
|
189
|
+
username: Optional[str] = None, identity_client=None) -> Path:
|
|
190
|
+
"""Shallow-clone a git skill repository and return the local skill directory.
|
|
191
|
+
|
|
192
|
+
Returns the directory containing SKILL.md (the subdir itself for sparse checkouts).
|
|
193
|
+
"""
|
|
194
|
+
if skill_path and (os.path.isabs(skill_path) or ".." in Path(skill_path).parts):
|
|
195
|
+
raise ValueError(f"Path traversal detected in skill path: {skill_path}")
|
|
196
|
+
|
|
197
|
+
source_hash = _stable_hash(f"{url}:{skill_path}")
|
|
198
|
+
type_dir = _SKILLS_BASE / "git"
|
|
199
|
+
|
|
200
|
+
cached = _resolve_cached(type_dir, source_hash)
|
|
201
|
+
if cached:
|
|
202
|
+
return Path(cached) / skill_path if skill_path else Path(cached)
|
|
203
|
+
|
|
204
|
+
temp_dir = type_dir / source_hash
|
|
205
|
+
_cleanup(temp_dir)
|
|
206
|
+
temp_dir.mkdir(parents=True, exist_ok=True)
|
|
207
|
+
|
|
208
|
+
extra_env = _build_git_auth_env(credential_arn, username, identity_client)
|
|
209
|
+
git_env = {**os.environ, **extra_env} if extra_env else None
|
|
210
|
+
|
|
211
|
+
try:
|
|
212
|
+
if skill_path:
|
|
213
|
+
subprocess.run(
|
|
214
|
+
["git", "clone", "--depth", "1", "--filter=blob:none", "--sparse", url, str(temp_dir)],
|
|
215
|
+
check=True, timeout=_GIT_TIMEOUT, capture_output=True, env=git_env,
|
|
216
|
+
)
|
|
217
|
+
subprocess.run(
|
|
218
|
+
["git", "sparse-checkout", "set", skill_path],
|
|
219
|
+
check=True, timeout=_GIT_TIMEOUT, capture_output=True, cwd=str(temp_dir), env=git_env,
|
|
220
|
+
)
|
|
221
|
+
else:
|
|
222
|
+
subprocess.run(
|
|
223
|
+
["git", "clone", "--depth", "1", url, str(temp_dir)],
|
|
224
|
+
check=True, timeout=_GIT_TIMEOUT, capture_output=True, env=git_env,
|
|
225
|
+
)
|
|
226
|
+
except Exception:
|
|
227
|
+
_cleanup(temp_dir)
|
|
228
|
+
raise
|
|
229
|
+
|
|
230
|
+
if skill_path and not (temp_dir / skill_path).exists():
|
|
231
|
+
_cleanup(temp_dir)
|
|
232
|
+
raise ValueError(f"Skill path '{skill_path}' not found in repository '{url}'")
|
|
233
|
+
|
|
234
|
+
# SKILL.md lives inside the subdir for sparse checkouts.
|
|
235
|
+
skill_root = temp_dir / skill_path if skill_path else temp_dir
|
|
236
|
+
label = f"{url}:{skill_path}" if skill_path else url
|
|
237
|
+
final_dir = _rename_and_cache_skill(type_dir, temp_dir, source_hash, skill_root, source_label=label)
|
|
238
|
+
return final_dir / skill_path if skill_path else final_dir
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def resolve_s3_skills(sources: list, s3_client=None) -> list:
|
|
242
|
+
"""Resolve s3:// skill URIs to local filesystem paths.
|
|
243
|
+
|
|
244
|
+
Any fetch failure raises and fails the invocation — a partial skill set
|
|
245
|
+
would silently run the agent without capabilities the harness declared.
|
|
246
|
+
"""
|
|
247
|
+
paths = []
|
|
248
|
+
for uri in sources:
|
|
249
|
+
try:
|
|
250
|
+
skill_dir = _fetch_s3_skill(uri, s3_client)
|
|
251
|
+
except Exception as e:
|
|
252
|
+
raise ValueError(f"Failed to resolve S3 skill '{uri}': {e}") from e
|
|
253
|
+
paths.append(str(skill_dir.resolve()))
|
|
254
|
+
return paths
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def resolve_git_skills(sources: list, identity_client=None) -> list:
|
|
258
|
+
"""Resolve git skill dicts to local filesystem paths.
|
|
259
|
+
|
|
260
|
+
Each source is a dict with keys: url (required), path (optional),
|
|
261
|
+
credentialArn (optional), username (optional).
|
|
262
|
+
|
|
263
|
+
Any fetch failure raises and fails the invocation — a partial skill set
|
|
264
|
+
would silently run the agent without capabilities the harness declared.
|
|
265
|
+
"""
|
|
266
|
+
paths = []
|
|
267
|
+
for source in sources:
|
|
268
|
+
try:
|
|
269
|
+
skill_dir = _fetch_git_skill(
|
|
270
|
+
url=source["url"],
|
|
271
|
+
skill_path=source.get("path") or "",
|
|
272
|
+
credential_arn=source.get("credentialArn"),
|
|
273
|
+
username=source.get("username"),
|
|
274
|
+
identity_client=identity_client,
|
|
275
|
+
)
|
|
276
|
+
except Exception as e:
|
|
277
|
+
raise ValueError(f"Failed to resolve git skill '{source.get('url', source)}': {e}") from e
|
|
278
|
+
paths.append(str(skill_dir.resolve()))
|
|
279
|
+
return paths
|
package/dist/assets/python/http/strands/capabilities/execution-limits/hooks/execution_limits.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from strands.hooks import BeforeModelCallEvent
|
|
5
|
+
from strands.hooks.registry import HookProvider, HookRegistry
|
|
6
|
+
from strands.types.exceptions import EventLoopException
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ExecutionLimitExceeded(Exception):
|
|
10
|
+
def __init__(self, message: str) -> None:
|
|
11
|
+
super().__init__(message)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ExecutionLimitsHook(HookProvider):
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
max_iterations: Optional[int] = None,
|
|
18
|
+
max_tokens: Optional[int] = None,
|
|
19
|
+
timeout_seconds: Optional[float] = None,
|
|
20
|
+
) -> None:
|
|
21
|
+
self._max_iterations = max_iterations
|
|
22
|
+
self._max_tokens = max_tokens
|
|
23
|
+
self._timeout_seconds = timeout_seconds
|
|
24
|
+
self._iteration_count = 0
|
|
25
|
+
self._start_time = time.monotonic()
|
|
26
|
+
|
|
27
|
+
def register_hooks(self, registry: HookRegistry, **kwargs) -> None:
|
|
28
|
+
registry.add_callback(BeforeModelCallEvent, self._check_limits)
|
|
29
|
+
|
|
30
|
+
def _check_limits(self, event: BeforeModelCallEvent) -> None:
|
|
31
|
+
self._iteration_count += 1
|
|
32
|
+
|
|
33
|
+
if self._max_iterations is not None and self._iteration_count > self._max_iterations:
|
|
34
|
+
raise EventLoopException(
|
|
35
|
+
ExecutionLimitExceeded(f"Max iterations exceeded: {self._max_iterations}")
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
if self._timeout_seconds is not None:
|
|
39
|
+
elapsed = time.monotonic() - self._start_time
|
|
40
|
+
if elapsed > self._timeout_seconds:
|
|
41
|
+
raise EventLoopException(
|
|
42
|
+
ExecutionLimitExceeded(
|
|
43
|
+
f"Timeout exceeded: {self._timeout_seconds}s (elapsed {elapsed:.1f}s)"
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
if self._max_tokens is not None:
|
|
48
|
+
used = event.agent.event_loop_metrics.accumulated_usage.get("outputTokens", 0)
|
|
49
|
+
if used >= self._max_tokens:
|
|
50
|
+
raise EventLoopException(
|
|
51
|
+
ExecutionLimitExceeded(
|
|
52
|
+
f"Max output tokens exceeded: {used}/{self._max_tokens}"
|
|
53
|
+
)
|
|
54
|
+
)
|
|
@@ -24,8 +24,11 @@ def get_memory_session_manager(session_id: Optional[str], actor_id: str) -> Opti
|
|
|
24
24
|
{{#if (includes memoryProviders.[0].strategies "USER_PREFERENCE")}}
|
|
25
25
|
f"/users/{actor_id}/preferences": RetrievalConfig(top_k=3, relevance_score=0.5),
|
|
26
26
|
{{/if}}
|
|
27
|
+
{{#if (includes memoryProviders.[0].strategies "EPISODIC")}}
|
|
28
|
+
f"/episodes/{actor_id}/{session_id}": RetrievalConfig(top_k=5, relevance_score=0.5),
|
|
29
|
+
{{/if}}
|
|
27
30
|
{{#if (includes memoryProviders.[0].strategies "SUMMARIZATION")}}
|
|
28
|
-
f"/summaries/{actor_id}": RetrievalConfig(top_k=3, relevance_score=0.5),
|
|
31
|
+
f"/summaries/{actor_id}/{session_id}": RetrievalConfig(top_k=3, relevance_score=0.5),
|
|
29
32
|
{{/if}}
|
|
30
33
|
}
|
|
31
34
|
{{/if}}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Payment capability -- auto-generated by agentcore CLI.
|
|
2
|
+
|
|
3
|
+
Configures AgentCorePaymentsPlugin for Strands agents.
|
|
4
|
+
Manager config is auto-discovered from AGENTCORE_PAYMENT_*
|
|
5
|
+
environment variables set at deploy time.
|
|
6
|
+
|
|
7
|
+
Uses a per-invocation factory pattern so each request gets its own
|
|
8
|
+
plugin instance with the correct user_id, instrument_id, and session_id.
|
|
9
|
+
This prevents concurrency bugs where one user's payment context
|
|
10
|
+
could leak to another user's request.
|
|
11
|
+
|
|
12
|
+
Uses the official SDK plugin which handles:
|
|
13
|
+
- x402 v1 (body-based) and v2 (header-based) payment detection
|
|
14
|
+
- Automatic 402 response interception and payment processing
|
|
15
|
+
- Retry limiting (max 3 payment retries per tool use)
|
|
16
|
+
- Error management and logging
|
|
17
|
+
"""
|
|
18
|
+
import os
|
|
19
|
+
import logging
|
|
20
|
+
|
|
21
|
+
import boto3
|
|
22
|
+
from bedrock_agentcore.payments.integrations.strands import AgentCorePaymentsPlugin
|
|
23
|
+
from bedrock_agentcore.payments.integrations.config import AgentCorePaymentsPluginConfig
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
PAYMENT_SYSTEM_PROMPT = """
|
|
28
|
+
You have payment capabilities via the x402 protocol:
|
|
29
|
+
- Use http_request to call HTTP endpoints. 402 Payment Required responses are settled automatically by the plugin and the call is retried.
|
|
30
|
+
- Use get_payment_session to check your remaining budget before expensive operations
|
|
31
|
+
- Use get_payment_instrument_balance to check wallet USDC balance
|
|
32
|
+
- Use list_payment_instruments to see available payment instruments
|
|
33
|
+
- If budget is low, inform the user before proceeding with paid requests
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
_manager_arn = None
|
|
37
|
+
_connector_id = None
|
|
38
|
+
_process_payment_role_arn = None
|
|
39
|
+
_name_segment = None
|
|
40
|
+
_region = None
|
|
41
|
+
_auth_mode = None
|
|
42
|
+
_manager_count = 0
|
|
43
|
+
for key, value in os.environ.items():
|
|
44
|
+
if key.startswith("AGENTCORE_PAYMENT_") and key.endswith("_MANAGER_ARN"):
|
|
45
|
+
if _manager_arn is None:
|
|
46
|
+
_manager_arn = value
|
|
47
|
+
_name_segment = key[len("AGENTCORE_PAYMENT_"):-len("_MANAGER_ARN")]
|
|
48
|
+
_manager_count += 1
|
|
49
|
+
if _manager_count > 1:
|
|
50
|
+
logger.warning(
|
|
51
|
+
"Multiple payment managers detected in environment. Using the first one found. "
|
|
52
|
+
"Remove extra AGENTCORE_PAYMENT_*_MANAGER_ARN env vars to eliminate ambiguity."
|
|
53
|
+
)
|
|
54
|
+
_region = os.getenv("AWS_REGION")
|
|
55
|
+
|
|
56
|
+
_prefix = f"AGENTCORE_PAYMENT_{_name_segment}_" if _name_segment else "AGENTCORE_PAYMENT_"
|
|
57
|
+
_auth_mode = os.getenv(f"{_prefix}AUTH_MODE", "sigv4")
|
|
58
|
+
_connector_id = os.getenv(f"{_prefix}CONNECTOR_ID")
|
|
59
|
+
_process_payment_role_arn = os.getenv(f"{_prefix}PROCESS_PAYMENT_ROLE_ARN")
|
|
60
|
+
_auto_payment = os.getenv(f"{_prefix}AUTO_PAYMENT", "true").lower() == "true"
|
|
61
|
+
_allowlist_raw = os.getenv(f"{_prefix}TOOL_ALLOWLIST")
|
|
62
|
+
_allowlist = _allowlist_raw.split(",") if _allowlist_raw else None
|
|
63
|
+
_network_prefs_raw = os.getenv(f"{_prefix}NETWORK_PREFERENCES")
|
|
64
|
+
_network_prefs = _network_prefs_raw.split(",") if _network_prefs_raw else None
|
|
65
|
+
|
|
66
|
+
if not _manager_arn:
|
|
67
|
+
logger.warning("No payment manager config found in environment")
|
|
68
|
+
if not _connector_id:
|
|
69
|
+
logger.warning("No payment connector config found in environment")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _assume_role_session(role_arn):
|
|
73
|
+
"""Assume an IAM role and return a boto3 session with temporary credentials."""
|
|
74
|
+
sts = boto3.client("sts", region_name=_region)
|
|
75
|
+
creds = sts.assume_role(
|
|
76
|
+
RoleArn=role_arn,
|
|
77
|
+
RoleSessionName="agentcore-payment-plugin",
|
|
78
|
+
)["Credentials"]
|
|
79
|
+
return boto3.Session(
|
|
80
|
+
aws_access_key_id=creds["AccessKeyId"],
|
|
81
|
+
aws_secret_access_key=creds["SecretAccessKey"],
|
|
82
|
+
aws_session_token=creds["SessionToken"],
|
|
83
|
+
region_name=_region,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def create_payments_plugin(user_id, instrument_id=None, session_id=None):
|
|
88
|
+
"""Create a fresh plugin instance per invocation.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
user_id: From invocation context (required for SigV4, derived from JWT for bearer)
|
|
92
|
+
instrument_id: From invocation payload (created by app backend per user)
|
|
93
|
+
session_id: From invocation payload (created by app backend per conversation)
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
AgentCorePaymentsPlugin instance, or None if no manager is configured.
|
|
97
|
+
"""
|
|
98
|
+
if not _manager_arn:
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
config_kwargs = {
|
|
102
|
+
"payment_manager_arn": _manager_arn,
|
|
103
|
+
"region": _region,
|
|
104
|
+
"payment_instrument_id": instrument_id,
|
|
105
|
+
"payment_session_id": session_id,
|
|
106
|
+
"payment_connector_id": _connector_id,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
config_kwargs["auto_payment"] = _auto_payment
|
|
110
|
+
if _allowlist:
|
|
111
|
+
config_kwargs["payment_tool_allowlist"] = _allowlist
|
|
112
|
+
if _network_prefs:
|
|
113
|
+
config_kwargs["network_preferences_config"] = _network_prefs
|
|
114
|
+
|
|
115
|
+
if _process_payment_role_arn:
|
|
116
|
+
# Only pass boto3_session if SDK supports it (added in bedrock-agentcore >= 1.11).
|
|
117
|
+
# Older SDKs use the runtime role's default credentials and can still call ProcessPayment
|
|
118
|
+
# if the runtime role has been granted permission directly.
|
|
119
|
+
import inspect
|
|
120
|
+
if "boto3_session" in inspect.signature(AgentCorePaymentsPluginConfig).parameters:
|
|
121
|
+
config_kwargs["boto3_session"] = _assume_role_session(_process_payment_role_arn)
|
|
122
|
+
else:
|
|
123
|
+
logger.warning(
|
|
124
|
+
"PROCESS_PAYMENT_ROLE_ARN set but bedrock-agentcore SDK does not support boto3_session. "
|
|
125
|
+
"Upgrade to bedrock-agentcore>=1.11 to enable cross-role payment processing."
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
if _auth_mode == "bearer":
|
|
129
|
+
bearer_token = os.getenv("AGENTCORE_BEARER_TOKEN")
|
|
130
|
+
if bearer_token:
|
|
131
|
+
config_kwargs["bearer_token"] = bearer_token
|
|
132
|
+
else:
|
|
133
|
+
logger.warning(
|
|
134
|
+
"Bearer auth mode configured but AGENTCORE_BEARER_TOKEN not set. "
|
|
135
|
+
"Falling back to SigV4. Set AGENTCORE_BEARER_TOKEN or pass bearer_token in invoke context."
|
|
136
|
+
)
|
|
137
|
+
config_kwargs["user_id"] = user_id or "default-user"
|
|
138
|
+
else:
|
|
139
|
+
config_kwargs["user_id"] = user_id or "default-user"
|
|
140
|
+
|
|
141
|
+
config = AgentCorePaymentsPluginConfig(**config_kwargs)
|
|
142
|
+
return AgentCorePaymentsPlugin(config=config)
|