This repository has been archived on 2026-06-28. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
agent-builder/agent_builder/skills/workspace-artifact-safety/SKILL.md
robert f6c612a071
All checks were successful
build / build (push) Successful in 26s
Teach builder to author DeepAgents skills
2026-05-19 09:48:24 -03:00

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.