3.7 KiB
name, description
| name | description |
|---|---|
| workspace-artifact-safety | Write A2A agents that use ctx.workspace_backend, workspace grants, artifacts, sandbox commands, and file paths safely. Use for agents that read files, create outputs, run subprocesses, convert media, or return downloadable artifacts. |
Workspace And Artifact Safety
Generated agents run with explicit workspace grants. File work must fit those grants and produce artifacts callers can find.
Workspace Access
If agent.py uses ctx.workspace, ctx.workspace_backend(), or a DeepAgents
graph with file tools, declare workspace access on the class:
from a2a_pack import WorkspaceAccess, WorkspaceMode
workspace_access = WorkspaceAccess.dynamic(
max_files=128,
allowed_modes=(WorkspaceMode.READ_ONLY, WorkspaceMode.READ_WRITE_OVERLAY),
require_reason=False,
)
Pass the backend into DeepAgents:
backend = ctx.workspace_backend()
graph = create_deep_agent(model=model, backend=backend, skills=skill_sources or None)
Write durable model-created files under /workspace/outputs/... in prompts.
When writing through ctx.workspace, use workspace-relative paths such as
outputs/report.md.
Artifact Pattern
For user-visible outputs, return a small JSON object and emit an artifact:
async def save_report(ctx: RunContext[NoAuth], report: str) -> dict[str, str]:
ref = await ctx.write_artifact(
"report.md",
report.encode("utf-8"),
"text/markdown",
)
await ctx.emit_artifact(ref)
return {"artifact_uri": ref.uri, "name": ref.name}
For binary files, use the correct media type:
ref = await ctx.write_artifact("frames.zip", zip_bytes, "application/zip")
await ctx.emit_artifact(ref)
Bounded Subprocesses
Media, rendering, archive, and data conversion agents must not run unbounded commands. Use explicit timeouts and return structured errors:
import asyncio
async def run_checked(cmd: list[str], timeout_s: int = 120) -> dict[str, str | int]:
try:
proc = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout_s)
except asyncio.TimeoutError:
return {"ok": 0, "error": "command timed out", "timeout_s": timeout_s}
return {
"ok": 1 if proc.returncode == 0 else 0,
"returncode": proc.returncode,
"stdout": stdout.decode("utf-8", errors="replace")[-4000:],
"stderr": stderr.decode("utf-8", errors="replace")[-4000:],
}
If a nonzero command still produced a usable output, preserve the output and include the command failure as a warning instead of discarding the file.
Scope Expansion
Only use ctx.request_scope(...) when the public @skill declares
allow_scope_expansion=True. Explain the reason in plain language and keep
the requested read/write patterns narrow.
@skill(
description="Analyze a protected folder after the caller approves access",
allow_scope_expansion=True,
)
async def analyze_folder(ctx: RunContext[NoAuth], folder: str) -> dict[str, str]:
await ctx.request_scope(
reason=f"Need to read {folder} to analyze the requested files",
read=(f"{folder.rstrip('/')}/**",),
mode="read_only",
)
return {"status": "approved"}
Safety Checklist
Do not hard-code absolute local paths. Do not expose arbitrary shell command
strings as public inputs. Do not write secrets to artifacts. Do not use
wants_cp_jwt=True for normal agents. Do not assume a file exists; check and
return a structured error that the caller can act on.