require a2a pack write prefix support
All checks were successful
build / build (push) Successful in 20s
All checks were successful
build / build (push) Successful in 20s
This commit is contained in:
@@ -218,7 +218,8 @@ Your tools:
|
||||
for ``@skill`` semantics,
|
||||
``context.py`` for ``RunContext``
|
||||
(workspace, sandbox, emit_progress,
|
||||
ask, collect, request_scope),
|
||||
ask, collect, request_scope,
|
||||
ensure_read/ensure_write),
|
||||
``runtime.py`` for ``AgentRuntime``
|
||||
fields (apt_packages, pricing,
|
||||
egress, etc.).
|
||||
@@ -350,6 +351,7 @@ def _build_project_backend(ctx: BuilderContext, *, settings: Settings) -> Any |
|
||||
allow_patterns=(f"{prefix}/**", "outputs/**"),
|
||||
deny_patterns=(),
|
||||
outputs_prefix=None,
|
||||
write_prefixes=(f"{prefix}/", "outputs/"),
|
||||
)
|
||||
)
|
||||
return _with_builder_skills(WorkspaceBackend(workspace))
|
||||
|
||||
@@ -17,7 +17,8 @@ Use this shape for non-trivial generated agents:
|
||||
DeepAgent with `create_deep_agent`.
|
||||
3. Pass `backend=ctx.workspace_backend()` so DeepAgents file tools write to the
|
||||
caller's durable workspace instead of LangGraph state.
|
||||
4. Pass `skills=[RUNTIME_SKILLS_ROOT]` when the generated project includes
|
||||
4. Seed project skills into the current grant's write prefix and pass
|
||||
`skills=skill_sources or None` when the generated project includes
|
||||
`skills/<skill-name>/SKILL.md`.
|
||||
5. For custom subagents, include a `skills` field on each subagent definition.
|
||||
The general-purpose subagent inherits main skills, but custom subagents do
|
||||
@@ -47,32 +48,46 @@ before `create_deep_agent`:
|
||||
|
||||
```python
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
RUNTIME_SKILLS_ROOT = "/outputs/{{ agent_name }}/.deepagents/skills/"
|
||||
RUNTIME_SKILLS_DIR = "{{ agent_name }}/.deepagents/skills/"
|
||||
|
||||
|
||||
def _seed_runtime_skills(backend: Any) -> None:
|
||||
def _runtime_skills_root(ctx: Any) -> str:
|
||||
workspace = getattr(ctx, "_workspace", None)
|
||||
prefixes = tuple(getattr(workspace, "write_prefixes", ()) or ())
|
||||
if not prefixes:
|
||||
outputs_prefix = getattr(workspace, "outputs_prefix", None)
|
||||
prefixes = (outputs_prefix or "outputs/",)
|
||||
prefix = str(prefixes[0]).strip("/")
|
||||
return f"/{prefix}/{RUNTIME_SKILLS_DIR}" if prefix else f"/{RUNTIME_SKILLS_DIR}"
|
||||
|
||||
|
||||
def _seed_runtime_skills(backend: Any, ctx: Any) -> list[str]:
|
||||
root = Path(__file__).parent / "skills"
|
||||
if not root.exists():
|
||||
return
|
||||
return []
|
||||
runtime_skills_root = _runtime_skills_root(ctx)
|
||||
uploads: list[tuple[str, bytes]] = []
|
||||
for path in root.rglob("*"):
|
||||
if path.is_file():
|
||||
rel = path.relative_to(root).as_posix()
|
||||
uploads.append((RUNTIME_SKILLS_ROOT + rel, path.read_bytes()))
|
||||
uploads.append((runtime_skills_root + rel, path.read_bytes()))
|
||||
if uploads:
|
||||
backend.upload_files(uploads)
|
||||
return [runtime_skills_root]
|
||||
return []
|
||||
```
|
||||
|
||||
Then:
|
||||
|
||||
```python
|
||||
backend = ctx.workspace_backend()
|
||||
_seed_runtime_skills(backend)
|
||||
skill_sources = _seed_runtime_skills(backend, ctx)
|
||||
return create_deep_agent(
|
||||
model=model,
|
||||
backend=backend,
|
||||
skills=[RUNTIME_SKILLS_ROOT],
|
||||
skills=skill_sources or None,
|
||||
tools=[small_deterministic_tool],
|
||||
subagents=[...],
|
||||
system_prompt=SYSTEM_PROMPT,
|
||||
|
||||
@@ -21,12 +21,12 @@ from langchain_core.tools import tool
|
||||
from langchain_openai import ChatOpenAI
|
||||
from deepagents import create_deep_agent
|
||||
|
||||
RUNTIME_SKILLS_ROOT = "/outputs/research-agent/.deepagents/skills/"
|
||||
RUNTIME_SKILLS_DIR = "research-agent/.deepagents/skills/"
|
||||
|
||||
SYSTEM_PROMPT = """\
|
||||
You are a careful research agent. Use project skills for workflow rules and
|
||||
use workspace-backed tools for durable files. Prefer /workspace/outputs/ for
|
||||
stable output paths.
|
||||
stable output paths only when that is one of the grant write prefixes.
|
||||
"""
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ def build_graph(ctx: Any) -> Any:
|
||||
return json.dumps({"ok": not missing, "missing_indexes": missing})
|
||||
|
||||
backend = ctx.workspace_backend()
|
||||
skill_sources = seed_runtime_skills(backend)
|
||||
skill_sources = seed_runtime_skills(backend, ctx)
|
||||
return create_deep_agent(
|
||||
model=model,
|
||||
backend=backend,
|
||||
@@ -71,18 +71,29 @@ def build_graph(ctx: Any) -> Any:
|
||||
)
|
||||
|
||||
|
||||
def seed_runtime_skills(backend: Any) -> list[str]:
|
||||
def runtime_skills_root(ctx: Any) -> str:
|
||||
workspace = getattr(ctx, "_workspace", None)
|
||||
prefixes = tuple(getattr(workspace, "write_prefixes", ()) or ())
|
||||
if not prefixes:
|
||||
outputs_prefix = getattr(workspace, "outputs_prefix", None)
|
||||
prefixes = (outputs_prefix or "outputs/",)
|
||||
prefix = str(prefixes[0]).strip("/")
|
||||
return f"/{prefix}/{RUNTIME_SKILLS_DIR}" if prefix else f"/{RUNTIME_SKILLS_DIR}"
|
||||
|
||||
|
||||
def seed_runtime_skills(backend: Any, ctx: Any) -> list[str]:
|
||||
root = Path(__file__).parent / "skills"
|
||||
if not root.exists():
|
||||
return []
|
||||
runtime_root = runtime_skills_root(ctx)
|
||||
uploads: list[tuple[str, bytes]] = []
|
||||
for path in root.rglob("*"):
|
||||
if path.is_file():
|
||||
rel = path.relative_to(root).as_posix()
|
||||
uploads.append((RUNTIME_SKILLS_ROOT + rel, path.read_bytes()))
|
||||
uploads.append((runtime_root + rel, path.read_bytes()))
|
||||
if uploads:
|
||||
backend.upload_files(uploads)
|
||||
return [RUNTIME_SKILLS_ROOT]
|
||||
return [runtime_root]
|
||||
return []
|
||||
```
|
||||
|
||||
|
||||
@@ -77,21 +77,23 @@ async def run_checked(script: str, timeout_s: int = 120) -> dict[str, str | int]
|
||||
}
|
||||
```
|
||||
|
||||
Commands may write to `/workspace/outputs/...` for stable paths, or to normal
|
||||
tool defaults such as `/tmp`, `/root`, or `/app`. The platform sandbox mirrors
|
||||
changed files outside `/workspace` into `outputs/rootfs-captures/...`. Do not
|
||||
use `asyncio.create_subprocess_exec(...)` for durable outputs. Plain subprocesses
|
||||
run inside the agent container, which is not mounted to the caller workspace and
|
||||
is not rootfs-captured.
|
||||
Commands may write under the grant's `write_prefixes` for stable paths, or to
|
||||
normal tool defaults such as `/tmp`, `/root`, or `/app`. The platform sandbox
|
||||
mirrors changed files outside `/workspace` into `outputs/rootfs-captures/...`.
|
||||
Do not use `asyncio.create_subprocess_exec(...)` for durable outputs. Plain
|
||||
subprocesses run inside the agent container, which is not mounted to the caller
|
||||
workspace and is not rootfs-captured.
|
||||
|
||||
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
|
||||
Only use `ctx.request_scope(...)`, `ctx.ensure_read(...)`, or
|
||||
`ctx.ensure_write(...)` when the public `@skill` declares
|
||||
`allow_scope_expansion=True`. Explain the reason in plain language and keep
|
||||
the requested read/write patterns narrow.
|
||||
the requested read/write patterns narrow. New write prefixes always require
|
||||
caller approval.
|
||||
|
||||
```python
|
||||
@skill(
|
||||
@@ -99,10 +101,9 @@ the requested read/write patterns narrow.
|
||||
allow_scope_expansion=True,
|
||||
)
|
||||
async def analyze_folder(ctx: RunContext[NoAuth], folder: str) -> dict[str, str]:
|
||||
await ctx.request_scope(
|
||||
await ctx.ensure_read(
|
||||
reason=f"Need to read {folder} to analyze the requested files",
|
||||
read=(f"{folder.rstrip('/')}/**",),
|
||||
mode="read_only",
|
||||
patterns=(f"{folder.rstrip('/')}/**",),
|
||||
)
|
||||
return {"status": "approved"}
|
||||
```
|
||||
|
||||
@@ -28,7 +28,7 @@ if TYPE_CHECKING:
|
||||
from .config import Settings
|
||||
|
||||
|
||||
A2A_PACK_MIN_VERSION = "0.1.28"
|
||||
A2A_PACK_MIN_VERSION = "0.1.29"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
||||
Reference in New Issue
Block a user