Skip to content

Commit b97e337

Browse files
padmak30claude
andauthored
feat: add session filesystem storage support (#893)
Adds --session-storage-mount-path to agentcore create and agentcore add agent, wiring the mount path through schema, CLI flags, TUI wizard, template rendering, and CDK mapping. File tools (file_read, file_write, list_files) with path traversal protection are scaffolded into all 8 framework templates when storage is configured. Fixes A2A sessionId not being forwarded to InvokeAgentRuntimeCommand. Validation is centralised in SessionStorageSchema with no regex duplication across validators or TUI. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent e966cb6 commit b97e337

33 files changed

Lines changed: 1441 additions & 53 deletions

File tree

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

Lines changed: 511 additions & 21 deletions
Large diffs are not rendered by default.

src/assets/python/a2a/googleadk/base/main.py

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import os
12
from google.adk.agents import Agent
23
from google.adk.a2a.executor.a2a_agent_executor import A2aAgentExecutor
34
from google.adk.runners import Runner
@@ -12,12 +13,71 @@ def add_numbers(a: int, b: int) -> int:
1213
return a + b
1314

1415

16+
tools = [add_numbers]
17+
18+
{{#if sessionStorageMountPath}}
19+
SESSION_STORAGE_PATH = "{{sessionStorageMountPath}}"
20+
21+
def _safe_resolve(path: str) -> str:
22+
"""Resolve path safely within the storage boundary."""
23+
resolved = os.path.realpath(os.path.join(SESSION_STORAGE_PATH, path.lstrip("/")))
24+
if not resolved.startswith(os.path.realpath(SESSION_STORAGE_PATH)):
25+
raise ValueError(f"Path '{path}' is outside the storage boundary")
26+
return resolved
27+
28+
def file_read(path: str) -> str:
29+
"""Read a file from persistent storage. The path is relative to the storage root."""
30+
try:
31+
full_path = _safe_resolve(path)
32+
with open(full_path) as f:
33+
return f.read()
34+
except ValueError as e:
35+
return str(e)
36+
except OSError as e:
37+
return f"Error reading '{path}': {e.strerror}"
38+
39+
def file_write(path: str, content: str) -> str:
40+
"""Write content to a file in persistent storage. The path is relative to the storage root."""
41+
try:
42+
full_path = _safe_resolve(path)
43+
parent = os.path.dirname(full_path)
44+
if parent:
45+
os.makedirs(parent, exist_ok=True)
46+
with open(full_path, "w") as f:
47+
f.write(content)
48+
return f"Written to {path}"
49+
except ValueError as e:
50+
return str(e)
51+
except OSError as e:
52+
return f"Error writing '{path}': {e.strerror}"
53+
54+
def list_files(directory: str = "") -> str:
55+
"""List files in persistent storage. The directory is relative to the storage root."""
56+
try:
57+
target = _safe_resolve(directory)
58+
entries = os.listdir(target)
59+
return "\n".join(entries) if entries else "(empty directory)"
60+
except ValueError as e:
61+
return str(e)
62+
except OSError as e:
63+
return f"Error listing '{directory}': {e.strerror}"
64+
65+
tools.extend([file_read, file_write, list_files])
66+
{{/if}}
67+
68+
AGENT_INSTRUCTION = """
69+
You are a helpful assistant. Use tools when appropriate.
70+
{{#if sessionStorageMountPath}}
71+
You have persistent storage at {{sessionStorageMountPath}}. Use file tools to read and write files. Data persists across sessions.
72+
{{/if}}
73+
"""
74+
1575
agent = Agent(
1676
model=load_model(),
1777
name="{{ name }}",
1878
description="A helpful assistant that can use tools.",
19-
instruction="You are a helpful assistant. Use tools when appropriate.",
20-
tools=[add_numbers],
79+
instruction=AGENT_INSTRUCTION,
80+
tools=tools,
2181
)
2282

2383
runner = Runner(

src/assets/python/a2a/langchain_langgraph/base/main.py

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import os
12
from langchain_core.tools import tool
23
from langgraph.prebuilt import create_react_agent
34
from opentelemetry.instrumentation.langchain import LangchainInstrumentor
@@ -18,8 +19,70 @@ def add_numbers(a: int, b: int) -> int:
1819
return a + b
1920

2021

22+
tools = [add_numbers]
23+
24+
{{#if sessionStorageMountPath}}
25+
SESSION_STORAGE_PATH = "{{sessionStorageMountPath}}"
26+
27+
def _safe_resolve(path: str) -> str:
28+
"""Resolve path safely within the storage boundary."""
29+
resolved = os.path.realpath(os.path.join(SESSION_STORAGE_PATH, path.lstrip("/")))
30+
if not resolved.startswith(os.path.realpath(SESSION_STORAGE_PATH)):
31+
raise ValueError(f"Path '{path}' is outside the storage boundary")
32+
return resolved
33+
34+
@tool
35+
def file_read(path: str) -> str:
36+
"""Read a file from persistent storage. The path is relative to the storage root."""
37+
try:
38+
full_path = _safe_resolve(path)
39+
with open(full_path) as f:
40+
return f.read()
41+
except ValueError as e:
42+
return str(e)
43+
except OSError as e:
44+
return f"Error reading '{path}': {e.strerror}"
45+
46+
@tool
47+
def file_write(path: str, content: str) -> str:
48+
"""Write content to a file in persistent storage. The path is relative to the storage root."""
49+
try:
50+
full_path = _safe_resolve(path)
51+
parent = os.path.dirname(full_path)
52+
if parent:
53+
os.makedirs(parent, exist_ok=True)
54+
with open(full_path, "w") as f:
55+
f.write(content)
56+
return f"Written to {path}"
57+
except ValueError as e:
58+
return str(e)
59+
except OSError as e:
60+
return f"Error writing '{path}': {e.strerror}"
61+
62+
@tool
63+
def list_files(directory: str = "") -> str:
64+
"""List files in persistent storage. The directory is relative to the storage root."""
65+
try:
66+
target = _safe_resolve(directory)
67+
entries = os.listdir(target)
68+
return "\n".join(entries) if entries else "(empty directory)"
69+
except ValueError as e:
70+
return str(e)
71+
except OSError as e:
72+
return f"Error listing '{directory}': {e.strerror}"
73+
74+
tools.extend([file_read, file_write, list_files])
75+
{{/if}}
76+
77+
SYSTEM_PROMPT = """
78+
You are a helpful assistant. Use tools when appropriate.
79+
{{#if sessionStorageMountPath}}
80+
You have persistent storage at {{sessionStorageMountPath}}. Use file tools to read and write files. Data persists across sessions.
81+
{{/if}}
82+
"""
83+
2184
model = load_model()
22-
graph = create_react_agent(model, tools=[add_numbers])
85+
graph = create_react_agent(model, tools=tools, prompt=SYSTEM_PROMPT)
2386

2487

2588
class LangGraphA2AExecutor(AgentExecutor):

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

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
{{#if hasMemory}}
66
from memory.session import get_memory_session_manager
77
{{/if}}
8+
{{#if sessionStorageMountPath}}
9+
import os
10+
{{/if}}
811

912

1013
@tool
@@ -15,6 +18,66 @@ def add_numbers(a: int, b: int) -> int:
1518

1619
tools = [add_numbers]
1720

21+
{{#if sessionStorageMountPath}}
22+
SESSION_STORAGE_PATH = "{{sessionStorageMountPath}}"
23+
24+
def _safe_resolve(path: str) -> str:
25+
"""Resolve path safely within the storage boundary."""
26+
resolved = os.path.realpath(os.path.join(SESSION_STORAGE_PATH, path.lstrip("/")))
27+
if not resolved.startswith(os.path.realpath(SESSION_STORAGE_PATH)):
28+
raise ValueError(f"Path '{path}' is outside the storage boundary")
29+
return resolved
30+
31+
@tool
32+
def file_read(path: str) -> str:
33+
"""Read a file from persistent storage. The path is relative to the storage root."""
34+
try:
35+
full_path = _safe_resolve(path)
36+
with open(full_path) as f:
37+
return f.read()
38+
except ValueError as e:
39+
return str(e)
40+
except OSError as e:
41+
return f"Error reading '{path}': {e.strerror}"
42+
43+
@tool
44+
def file_write(path: str, content: str) -> str:
45+
"""Write content to a file in persistent storage. The path is relative to the storage root."""
46+
try:
47+
full_path = _safe_resolve(path)
48+
parent = os.path.dirname(full_path)
49+
if parent:
50+
os.makedirs(parent, exist_ok=True)
51+
with open(full_path, "w") as f:
52+
f.write(content)
53+
return f"Written to {path}"
54+
except ValueError as e:
55+
return str(e)
56+
except OSError as e:
57+
return f"Error writing '{path}': {e.strerror}"
58+
59+
@tool
60+
def list_files(directory: str = "") -> str:
61+
"""List files in persistent storage. The directory is relative to the storage root."""
62+
try:
63+
target = _safe_resolve(directory)
64+
entries = os.listdir(target)
65+
return "\n".join(entries) if entries else "(empty directory)"
66+
except ValueError as e:
67+
return str(e)
68+
except OSError as e:
69+
return f"Error listing '{directory}': {e.strerror}"
70+
71+
tools.extend([file_read, file_write, list_files])
72+
{{/if}}
73+
74+
SYSTEM_PROMPT = """
75+
You are a helpful assistant. Use tools when appropriate.
76+
{{#if sessionStorageMountPath}}
77+
You have persistent storage at {{sessionStorageMountPath}}. Use file tools to read and write files. Data persists across sessions.
78+
{{/if}}
79+
"""
80+
1881
{{#if hasMemory}}
1982
def agent_factory():
2083
cache = {}
@@ -24,7 +87,7 @@ def get_or_create_agent(session_id, user_id):
2487
cache[key] = Agent(
2588
model=load_model(),
2689
session_manager=get_memory_session_manager(session_id, user_id),
27-
system_prompt="You are a helpful assistant. Use tools when appropriate.",
90+
system_prompt=SYSTEM_PROMPT,
2891
tools=tools,
2992
)
3093
return cache[key]
@@ -35,7 +98,7 @@ def get_or_create_agent(session_id, user_id):
3598
{{else}}
3699
agent = Agent(
37100
model=load_model(),
38-
system_prompt="You are a helpful assistant. Use tools when appropriate.",
101+
system_prompt=SYSTEM_PROMPT,
39102
tools=tools,
40103
)
41104
{{/if}}

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

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,66 @@ def add_numbers(a: int, b: int) -> int:
2222
# Define a collection of tools used by the model
2323
tools = [add_numbers_tool]
2424

25+
{{#if sessionStorageMountPath}}
26+
SESSION_STORAGE_PATH = "{{sessionStorageMountPath}}"
27+
28+
def _safe_resolve(path: str) -> str:
29+
"""Resolve path safely within the storage boundary."""
30+
resolved = os.path.realpath(os.path.join(SESSION_STORAGE_PATH, path.lstrip("/")))
31+
if not resolved.startswith(os.path.realpath(SESSION_STORAGE_PATH)):
32+
raise ValueError(f"Path '{path}' is outside the storage boundary")
33+
return resolved
34+
35+
def file_read(path: str) -> str:
36+
"""Read a file from persistent storage. The path is relative to the storage root."""
37+
try:
38+
full_path = _safe_resolve(path)
39+
with open(full_path) as f:
40+
return f.read()
41+
except ValueError as e:
42+
return str(e)
43+
except OSError as e:
44+
return f"Error reading '{path}': {e.strerror}"
45+
46+
def file_write(path: str, content: str) -> str:
47+
"""Write content to a file in persistent storage. The path is relative to the storage root."""
48+
try:
49+
full_path = _safe_resolve(path)
50+
parent = os.path.dirname(full_path)
51+
if parent:
52+
os.makedirs(parent, exist_ok=True)
53+
with open(full_path, "w") as f:
54+
f.write(content)
55+
return f"Written to {path}"
56+
except ValueError as e:
57+
return str(e)
58+
except OSError as e:
59+
return f"Error writing '{path}': {e.strerror}"
60+
61+
def list_files(directory: str = "") -> str:
62+
"""List files in persistent storage. The directory is relative to the storage root."""
63+
try:
64+
target = _safe_resolve(directory)
65+
entries = os.listdir(target)
66+
return "\n".join(entries) if entries else "(empty directory)"
67+
except ValueError as e:
68+
return str(e)
69+
except OSError as e:
70+
return f"Error listing '{directory}': {e.strerror}"
71+
72+
tools.extend([
73+
FunctionTool(file_read, description="Read a file from persistent storage. The path is relative to the storage root."),
74+
FunctionTool(file_write, description="Write content to a file in persistent storage. The path is relative to the storage root."),
75+
FunctionTool(list_files, description="List files in persistent storage. The directory is relative to the storage root."),
76+
])
77+
{{/if}}
78+
79+
SYSTEM_MESSAGE = """
80+
You are a helpful assistant. Use tools when appropriate.
81+
{{#if sessionStorageMountPath}}
82+
You have persistent storage at {{sessionStorageMountPath}}. Use file tools to read and write files. Data persists across sessions.
83+
{{/if}}
84+
"""
2585

2686
@app.entrypoint
2787
async def invoke(payload, context):
@@ -35,7 +95,7 @@ async def invoke(payload, context):
3595
name="{{ name }}",
3696
model_client=load_model(),
3797
tools=tools + mcp_tools,
38-
system_message="You are a helpful assistant. Use tools when appropriate.",
98+
system_message=SYSTEM_MESSAGE,
3999
)
40100

41101
# Process the user prompt

0 commit comments

Comments
 (0)