Teach builder to author DeepAgents skills
All checks were successful
build / build (push) Successful in 26s
All checks were successful
build / build (push) Successful in 26s
This commit is contained in:
113
agent_builder/skills/workspace-artifact-safety/SKILL.md
Normal file
113
agent_builder/skills/workspace-artifact-safety/SKILL.md
Normal file
@@ -0,0 +1,113 @@
|
||||
---
|
||||
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.
|
||||
Reference in New Issue
Block a user