mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
### Why / What / How
**Why:** The Claude Agent SDK CLI renamed the sub-agent tool from
`"Task"` to `"Agent"` in v2.x. Our security hooks only checked for
`"Task"`, so all sub-agent security controls were silently bypassed on
production: concurrency limiting didn't apply, and slot tracking was
broken. This was discovered via Langfuse trace analysis of session
`62b1b2b9` where background sub-agents ran unchecked.
Additionally, the CLI writes sub-agent output to `/tmp/claude-<uid>/`
and project state to `$HOME/.claude/` — both outside the per-session
workspace (`/tmp/copilot-<session>/`). This caused `PermissionError` in
E2B sandboxes and silently lost sub-agent results.
The frontend also had no rendering for the `Agent` / `TaskOutput` SDK
built-in tools — they fell through to the generic "other" category with
no context-aware display.
**What:**
1. Fix the sub-agent tool name recognition (`"Task"` → `{"Task",
"Agent"}`)
2. Allow `run_in_background` — the SDK handles async lifecycle cleanly
(returns `isAsync:true`, model polls via `TaskOutput`)
3. Route CLI state into the workspace via `CLAUDE_CODE_TMPDIR` and
`HOME` env vars
4. Add lifecycle hooks (`SubagentStart`/`SubagentStop`) for
observability
5. Add frontend `"agent"` tool category with proper UI rendering
**How:**
- Security hooks check `tool_name in _SUBAGENT_TOOLS` (frozenset of
`"Task"` and `"Agent"`)
- Background agents are allowed but still count against `max_subtasks`
concurrency limit
- Frontend detects `isAsync: true` output → shows "Agent started
(background)" not "Agent completed"
- `TaskOutput` tool shows retrieval status and collected results
- Robot icon and agent-specific accordion rendering for both foreground
and background agents
### Changes 🏗️
**Backend:**
- **`security_hooks.py`**: Replace `tool_name == "Task"` with `tool_name
in _SUBAGENT_TOOLS`. Remove `run_in_background` deny block (SDK handles
async lifecycle). Add `SubagentStart`/`SubagentStop` hooks.
- **`tool_adapter.py`**: Add `"Agent"` to `_SDK_BUILTIN_ALWAYS` list
alongside `"Task"`.
- **`service.py`**: Set `CLAUDE_CODE_TMPDIR=sdk_cwd` and `HOME=sdk_cwd`
in SDK subprocess env.
- **`security_hooks_test.py`**: Update background tests (allowed, not
blocked). Add test for background agents counting against concurrency
limit.
**Frontend:**
- **`GenericTool/helpers.ts`**: Add `"agent"` tool category for `Agent`,
`Task`, `TaskOutput`. Agent-specific animation text detecting `isAsync`
output. Input summaries from description/prompt fields.
- **`GenericTool/GenericTool.tsx`**: Add `RobotIcon` for agent category.
Add `getAgentAccordionData()` with async-aware title/content.
`TaskOutput` shows retrieval status.
- **`useChatSession.ts`**: Fix pre-existing TS error (void mutation
body).
### Checklist 📋
#### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
- [x] All security hooks tests pass (background allowed + limit
enforced)
- [x] Pre-commit hooks (ruff, black, isort, pyright, tsc) all pass
- [x] E2E test: copilot agent create+run scenario PASS
- [ ] Deploy to dev and test copilot sub-agent spawning with background
mode
#### For configuration changes:
- [x] `.env.default` is updated or already compatible
- [x] `docker-compose.yml` is updated or already compatible
83 lines
2.8 KiB
Python
83 lines
2.8 KiB
Python
"""SDK environment variable builder — importable without circular deps.
|
|
|
|
Extracted from ``service.py`` so that ``backend.blocks.orchestrator``
|
|
can reuse the same subscription / OpenRouter / direct-Anthropic logic
|
|
without pulling in the full copilot service module (which would create a
|
|
circular import through ``executor`` → ``credit`` → ``block_cost_config``).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from backend.copilot.config import ChatConfig
|
|
from backend.copilot.sdk.subscription import validate_subscription
|
|
|
|
# ChatConfig is stateless (reads env vars) — a separate instance is fine.
|
|
# A singleton would require importing service.py which causes the circular dep
|
|
# this module was created to avoid.
|
|
config = ChatConfig()
|
|
|
|
|
|
def build_sdk_env(
|
|
session_id: str | None = None,
|
|
user_id: str | None = None,
|
|
sdk_cwd: str | None = None,
|
|
) -> dict[str, str]:
|
|
"""Build env vars for the SDK CLI subprocess.
|
|
|
|
Three modes (checked in order):
|
|
1. **Subscription** — clears all keys; CLI uses ``claude login`` auth.
|
|
2. **Direct Anthropic** — returns ``{}``; subprocess inherits
|
|
``ANTHROPIC_API_KEY`` from the parent environment.
|
|
3. **OpenRouter** (default) — overrides base URL and auth token to
|
|
route through the proxy, with Langfuse trace headers.
|
|
|
|
When *sdk_cwd* is provided, ``CLAUDE_CODE_TMPDIR`` is set so that
|
|
the CLI writes temp/sub-agent output inside the per-session workspace
|
|
directory rather than an inaccessible system temp path.
|
|
"""
|
|
# --- Mode 1: Claude Code subscription auth ---
|
|
if config.use_claude_code_subscription:
|
|
validate_subscription()
|
|
env: dict[str, str] = {
|
|
"ANTHROPIC_API_KEY": "",
|
|
"ANTHROPIC_AUTH_TOKEN": "",
|
|
"ANTHROPIC_BASE_URL": "",
|
|
}
|
|
if sdk_cwd:
|
|
env["CLAUDE_CODE_TMPDIR"] = sdk_cwd
|
|
return env
|
|
|
|
# --- Mode 2: Direct Anthropic (no proxy hop) ---
|
|
if not config.openrouter_active:
|
|
env = {}
|
|
if sdk_cwd:
|
|
env["CLAUDE_CODE_TMPDIR"] = sdk_cwd
|
|
return env
|
|
|
|
# --- Mode 3: OpenRouter proxy ---
|
|
base = (config.base_url or "").rstrip("/")
|
|
if base.endswith("/v1"):
|
|
base = base[:-3]
|
|
env = {
|
|
"ANTHROPIC_BASE_URL": base,
|
|
"ANTHROPIC_AUTH_TOKEN": config.api_key or "",
|
|
"ANTHROPIC_API_KEY": "", # force CLI to use AUTH_TOKEN
|
|
}
|
|
|
|
# Inject broadcast headers so OpenRouter forwards traces to Langfuse.
|
|
def _safe(v: str) -> str:
|
|
return v.replace("\r", "").replace("\n", "").strip()[:128]
|
|
|
|
parts = []
|
|
if session_id:
|
|
parts.append(f"x-session-id: {_safe(session_id)}")
|
|
if user_id:
|
|
parts.append(f"x-user-id: {_safe(user_id)}")
|
|
if parts:
|
|
env["ANTHROPIC_CUSTOM_HEADERS"] = "\n".join(parts)
|
|
|
|
if sdk_cwd:
|
|
env["CLAUDE_CODE_TMPDIR"] = sdk_cwd
|
|
|
|
return env
|