--- name: workspace-artifact-safety description: 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: ```python 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: ```python 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: ```python 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: ```python 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: ```python 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. ```python @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.