feat(fleet): security/trust + headless-worker foundation; unify recursion depth
Lands the Agent Fleet security/trust boundary and the headless-worker bridge on the v0.8.60 line, and collapses the sub-agent and fleet recursion model into a single shared axis (Hunter steer: "not two moving targets"). Security & trust (#3165): - FleetTrustLevel, FleetSecurityPolicy, FleetSecretRef (redacted), FleetWorkerAuth, FleetCapabilityGrant, FleetAlertEndpoint (redacted) in protocol. - secrets: resolve_direct(key, source_hint) — fleet secret resolution, never logged. - Host adapters refuse secret-bearing env keys; SSH uses SendEnv (no argv secrets). Roles & delegation (#3167): - fleet role -> SubAgentType mapping; reviewer/verifier default read-only. Headless worker bridge (#3096/#3154, partial — still simulation, real spawn next): - worker_runtime: FleetTaskSpec -> AgentWorkerSpec, status -> ledger events, exec hardening (mirrors #3027), parallel-safe read-only tool set (#2983). - FleetManager carries an optional SharedSubAgentManager + exec config. Recursion depth — ONE axis: - codewhale_config now owns DEFAULT_SPAWN_DEPTH (3) + MAX_SPAWN_DEPTH_CEILING (3). - sub-agent DEFAULT_MAX_SPAWN_DEPTH and the fleet clamp both source these consts. - fleet default raised 1 -> 3 to match standalone sub-agents; root runs at depth 0, budget gates child delegation. End-to-end test proves a depth-0 fleet worker reaches 3 nested levels (afford >= 3). Dogfood scaffolding (#3166, partial): docs/examples/fleet-dogfood.toml. Tests green: codewhale-config fleet, codewhale-tui fleet (58), subagent max_depth; cargo fmt + git diff --check clean; cargo check --workspace ok. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+244
-95
@@ -19,6 +19,29 @@ Fleet state is stored under the workspace in `.codewhale/fleet.jsonl`. Worker
|
||||
logs and adapter logs are stored under `.codewhale/fleet/` and
|
||||
`.codewhale/fleet-host/`.
|
||||
|
||||
## Naming: Modes, WhaleFlow, Fleet, and Swarm
|
||||
|
||||
These names describe different layers, not competing systems. Agent, Plan, and
|
||||
YOLO stay the permission/work modes. WhaleFlow is an orchestration overlay that
|
||||
can run on top of those modes when the task needs a continuous workflow.
|
||||
|
||||
- **WhaleFlow** is the repeatable workflow plan and user-facing orchestration
|
||||
overlay: a script/IR that decides which phases and agents run next, keeps
|
||||
intermediate results out of the main conversation, and can be inspected or
|
||||
rerun. A WhaleFlow run should have a visible progress view and a clear active
|
||||
header state instead of feeling like a hidden background task.
|
||||
- **Fleet** is the execution substrate: headless workers, local/SSH hosts,
|
||||
trust policy, leases, heartbeats, logs, receipts, and status APIs.
|
||||
- **Swarm** is the high-fanout behavior inside WhaleFlow. It should compile into
|
||||
a WhaleFlow-backed fleet run instead of reviving the old `agent_swarm` tool
|
||||
surface.
|
||||
|
||||
UI guidance: keep the main transcript calm. A WhaleFlow run should appear as a
|
||||
compact progress card plus Work/Agents sidebar rows with phase names, worker
|
||||
counts, receipts, and nested indentation for child workers. Use the whale mark
|
||||
sparingly as an active header/status signal; avoid repeating emoji-heavy rows
|
||||
for every worker.
|
||||
|
||||
## Task Spec
|
||||
|
||||
`codewhale fleet run` accepts JSON or TOML. A minimal JSON spec:
|
||||
@@ -60,118 +83,79 @@ and `json_path`. Specs may also declare `command`,
|
||||
`code_whale_verifier_prompt`, or `manual`; those record a partial receipt until
|
||||
an explicit verifier pass completes.
|
||||
|
||||
### Release Triage Example
|
||||
### Using Role Presets
|
||||
|
||||
Tasks can reference a role name, and the fleet manager fills in defaults
|
||||
from the role registry. Built-in roles (`smoke-runner`, `reviewer`, `builder`,
|
||||
`read-only`) are always available; define your own in `[fleet.roles]`.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "v0.8.60 release triage",
|
||||
"labels": {
|
||||
"milestone": "v0.8.60"
|
||||
},
|
||||
"name": "smoke check",
|
||||
"tasks": [
|
||||
{
|
||||
"id": "release-issue-sweep",
|
||||
"name": "Release issue sweep",
|
||||
"objective": "Find open v0.8.60 blockers and credit-sensitive PRs.",
|
||||
"instructions": "Review the v0.8.60 milestone, linked PRs, changelog entries, and contributor-credit requirements. Write a concise blocker report.",
|
||||
"worker": {
|
||||
"role": "release-triage",
|
||||
"tool_profile": "read-only",
|
||||
"tools": ["gh", "git"],
|
||||
"capabilities": ["github", "release"]
|
||||
},
|
||||
"workspace": {
|
||||
"required_files": ["Cargo.toml", "CHANGELOG.md", ".github/AUTHOR_MAP"],
|
||||
"writable_paths": [".codewhale/fleet"],
|
||||
"environment": {
|
||||
"required": ["PATH"]
|
||||
}
|
||||
},
|
||||
"input_files": ["CHANGELOG.md", ".github/AUTHOR_MAP"],
|
||||
"context": ["Treat community PRs as maintainer evidence."],
|
||||
"budget": {
|
||||
"max_tokens": 12000,
|
||||
"max_tool_calls": 24,
|
||||
"max_seconds": 900
|
||||
},
|
||||
"timeout_seconds": 900,
|
||||
"expected_artifacts": ["log", "report", "receipt"],
|
||||
"scorer": {
|
||||
"kind": "exit_code"
|
||||
},
|
||||
"retry_policy": {
|
||||
"max_attempts": 2,
|
||||
"initial_backoff_seconds": 10,
|
||||
"max_backoff_seconds": 60,
|
||||
"backoff_multiplier": 2
|
||||
},
|
||||
"tags": ["release", "triage"],
|
||||
"metadata": {
|
||||
"class": "release"
|
||||
}
|
||||
"id": "lint",
|
||||
"name": "Lint check",
|
||||
"instructions": "Run lint and report failures.",
|
||||
"worker": { "role": "smoke-runner" },
|
||||
"expected_artifacts": ["log"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Code Review Swarm Example
|
||||
The task inherits the role's tool profile, budget, and timeout. You can
|
||||
override any field in the task spec:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "code review swarm",
|
||||
"id": "deep-review",
|
||||
"name": "Deep review",
|
||||
"instructions": "Review the entire crate for soundness issues.",
|
||||
"worker": {
|
||||
"role": "reviewer",
|
||||
"tools": ["cargo", "rg", "git"],
|
||||
"capabilities": ["rust"]
|
||||
},
|
||||
"input_files": ["crates/**/*.rs"],
|
||||
"budget": { "max_tokens": 32000 },
|
||||
"expected_artifacts": ["log", "report"],
|
||||
"scorer": { "kind": "regex_match", "path": ".codewhale/fleet/report.md", "pattern": "finding|all clear" }
|
||||
}
|
||||
```
|
||||
|
||||
### Multi-Task Run Example
|
||||
|
||||
A single fleet run can dispatch several independent tasks in parallel:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "CI gate",
|
||||
"tasks": [
|
||||
{
|
||||
"id": "protocol-review",
|
||||
"name": "Protocol review",
|
||||
"objective": "Review fleet protocol changes for compatibility and sparse JSON behavior.",
|
||||
"instructions": "Inspect crates/protocol/src/fleet.rs and report behavior regressions, missing serde defaults, or unsafe wire changes.",
|
||||
"worker": {
|
||||
"role": "reviewer",
|
||||
"tool_profile": "read-only",
|
||||
"tools": ["git", "rg", "cargo"],
|
||||
"capabilities": ["rust"]
|
||||
},
|
||||
"input_files": ["crates/protocol/src/fleet.rs"],
|
||||
"budget": {
|
||||
"max_tokens": 8000,
|
||||
"max_tool_calls": 16,
|
||||
"max_seconds": 600
|
||||
},
|
||||
"expected_artifacts": ["log", "report", "receipt"],
|
||||
"scorer": {
|
||||
"kind": "code_whale_verifier_prompt",
|
||||
"prompt": "Verify the review includes at least one concrete file:line finding or explicitly says no issues were found."
|
||||
},
|
||||
"tags": ["review", "protocol"],
|
||||
"metadata": {
|
||||
"class": "code-review"
|
||||
}
|
||||
"id": "check",
|
||||
"name": "Compile check",
|
||||
"instructions": "Run cargo check --workspace and report errors.",
|
||||
"worker": { "role": "builder" },
|
||||
"expected_artifacts": ["log"],
|
||||
"scorer": { "kind": "exit_code" }
|
||||
},
|
||||
{
|
||||
"id": "tui-review",
|
||||
"name": "TUI review",
|
||||
"objective": "Review fleet CLI and manager behavior for operator-visible regressions.",
|
||||
"instructions": "Inspect crates/tui/src/fleet and crates/tui/src/main.rs. Focus on status output, receipt recording, and failure classification.",
|
||||
"worker": {
|
||||
"role": "reviewer",
|
||||
"tool_profile": "read-only",
|
||||
"tools": ["git", "rg", "cargo"],
|
||||
"capabilities": ["rust", "cli"]
|
||||
},
|
||||
"input_files": ["crates/tui/src/fleet", "crates/tui/src/main.rs"],
|
||||
"budget": {
|
||||
"max_tokens": 10000,
|
||||
"max_tool_calls": 20,
|
||||
"max_seconds": 600
|
||||
},
|
||||
"expected_artifacts": ["log", "report", "receipt"],
|
||||
"scorer": {
|
||||
"kind": "manual"
|
||||
},
|
||||
"tags": ["review", "tui"],
|
||||
"metadata": {
|
||||
"class": "code-review"
|
||||
}
|
||||
"id": "clippy",
|
||||
"name": "Clippy lint",
|
||||
"instructions": "Run cargo clippy --workspace and report warnings.",
|
||||
"worker": { "role": "reviewer", "tools": ["cargo", "cargo-clippy"] },
|
||||
"expected_artifacts": ["log"],
|
||||
"scorer": { "kind": "exit_code" }
|
||||
},
|
||||
{
|
||||
"id": "security",
|
||||
"name": "Secret audit",
|
||||
"instructions": "Search for plaintext secrets and report any matches.",
|
||||
"worker": { "role": "read-only", "tools": ["rg"] },
|
||||
"input_files": ["crates/**/*.rs"],
|
||||
"expected_artifacts": ["log", "report"],
|
||||
"retry_policy": { "max_attempts": 1 }
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -373,3 +357,168 @@ Defaults are intentionally conservative:
|
||||
`API_KEY`, and `PRIVATE_KEY` are rejected from adapter allowlists;
|
||||
- secrets should remain in CodeWhale config providers or remote host config,
|
||||
not in task instructions, argv, or fleet logs.
|
||||
|
||||
## Security and Trust Boundaries
|
||||
|
||||
Agent Fleet enforces a trust-level model that separates workers into four tiers.
|
||||
The trust level determines what a worker can access (secrets, network, workspace
|
||||
writes) and how it must prove its identity before being granted those privileges.
|
||||
|
||||
### Trust Levels
|
||||
|
||||
| Level | Access | Requires |
|
||||
|-------|--------|----------|
|
||||
| `sandbox` | No network, no secrets, writes only to `.codewhale/fleet/` | Nothing — default for new workers |
|
||||
| `local` | Workspace reads, gated writes, configured secrets | Local process (same uid) |
|
||||
| `remote-verified` | Network access, bounded capability grants, configured secrets | SSH host-key verification or equivalent attestation |
|
||||
| `operator` | Full access to all secrets, unrestricted writes, any action | Operator-owned machine |
|
||||
|
||||
The default trust level is `sandbox`. Operators must explicitly raise trust for
|
||||
SSH or container workers through the security policy.
|
||||
|
||||
### Security Policy
|
||||
|
||||
A fleet run may carry an optional `security_policy` block that defines the
|
||||
default trust level, which secrets workers may resolve, what capabilities are
|
||||
granted, and a ceiling on the maximum trust level:
|
||||
|
||||
```json
|
||||
{
|
||||
"security_policy": {
|
||||
"default_trust_level": "sandbox",
|
||||
"allowed_secrets": [
|
||||
{"key": "GH_TOKEN", "source": "env"},
|
||||
{"key": "CODEWHALE_API_KEY", "source": "keyring"}
|
||||
],
|
||||
"capability_grants": [
|
||||
{
|
||||
"capability": "network",
|
||||
"scope": "github.com",
|
||||
"reason": "PR review needs GitHub API access"
|
||||
}
|
||||
],
|
||||
"max_trust_level": "remote_verified",
|
||||
"require_identity_verification": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When a run has no explicit `security_policy`, workers inherit conservative
|
||||
defaults: `sandbox` trust, no secrets, no capability grants, and no identity
|
||||
verification requirement.
|
||||
|
||||
### Secret References
|
||||
|
||||
Secrets are never stored as plaintext in task specs, alert configs, or worker
|
||||
definitions. Instead, every secret is a `FleetSecretRef` — a key name plus an
|
||||
optional source hint that tells the fleet manager where to resolve the value:
|
||||
|
||||
```json
|
||||
{"key": "GH_TOKEN", "source": "env"}
|
||||
```
|
||||
|
||||
Supported sources:
|
||||
- `"env"` — resolve from a process environment variable
|
||||
- `"keyring"` — resolve from the OS keyring (macOS Keychain, Windows Credential Manager, Linux Secret Service)
|
||||
- `"file"` — resolve from `~/.codewhale/secrets/`
|
||||
- absent — try all sources in default order (store first, then env)
|
||||
|
||||
Secret refs are redacted in logs and ledger entries: `<secret:env.GH_TOKEN>`.
|
||||
|
||||
### Worker Authentication
|
||||
|
||||
Workers authenticate to the fleet manager using one of three methods:
|
||||
|
||||
- **None** — local workers sharing the same uid (default)
|
||||
- **SSH key** — with optional host-key fingerprint pinning and known-hosts
|
||||
verification. The `host_key_fingerprint` field (SHA256:...) pins the expected
|
||||
server key, preventing MITM attacks on first connection.
|
||||
- **Token** — a bearer token resolved from a `FleetSecretRef`, useful for remote
|
||||
workers behind a fleet proxy.
|
||||
- **mTLS** — mutual TLS with a client certificate and a secret-backed private key.
|
||||
|
||||
SSH workers should always set `host_key_fingerprint` in production:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "builder-1",
|
||||
"name": "Builder 1",
|
||||
"trust_level": "remote_verified",
|
||||
"host": {
|
||||
"kind": "ssh",
|
||||
"host": "builder.example.com",
|
||||
"user": "codewhale",
|
||||
"port": 22,
|
||||
"identity": "~/.ssh/codewhale_fleet",
|
||||
"host_key_fingerprint": "SHA256:aLGqZo1M6c...",
|
||||
"known_hosts": "~/.ssh/known_hosts",
|
||||
"working_directory": "/srv/codewhale/work",
|
||||
"env_allowlist": ["CODEWHALE_PROFILE"],
|
||||
"codewhale_binary": "/usr/local/bin/codewhale"
|
||||
},
|
||||
"capabilities": ["local", "linux", "tests"],
|
||||
"max_concurrent_tasks": 1
|
||||
}
|
||||
```
|
||||
|
||||
### Alert Channel Secrets
|
||||
|
||||
Alert channels (Slack, generic webhook, PagerDuty) use `FleetAlertEndpoint`
|
||||
instead of raw URLs. The webhook URL can be provided inline for non-sensitive
|
||||
endpoints, or as a secret reference:
|
||||
|
||||
```json
|
||||
{
|
||||
"kind": "slack",
|
||||
"webhook": {
|
||||
"url_ref": {"key": "CODEWHALE_FLEET_SLACK_WEBHOOK", "source": "env"},
|
||||
"secret_ref": {"key": "CODEWHALE_FLEET_SLACK_SIGNING_SECRET", "source": "keyring"}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `secret_ref` field provides an optional HMAC secret for webhook payload
|
||||
signing, never stored in plaintext.
|
||||
|
||||
### Config File
|
||||
|
||||
The `[fleet]` table in `config.toml` sets global trust policy defaults:
|
||||
|
||||
```toml
|
||||
[fleet]
|
||||
default_trust_level = "sandbox"
|
||||
require_identity_verification = true
|
||||
max_trust_level = "operator"
|
||||
|
||||
[fleet.exec]
|
||||
# Recursion depth shares ONE axis with standalone sub-agents — a fleet worker
|
||||
# IS a headless sub-agent. 0 blocks child agents (the root worker still runs);
|
||||
# 3 is the default and the ceiling, affording at least three nested levels.
|
||||
max_spawn_depth = 3
|
||||
```
|
||||
|
||||
These defaults apply to fleet runs that don't carry their own `security_policy`.
|
||||
Per-run policies always override the config defaults.
|
||||
|
||||
### Capability Grants
|
||||
|
||||
Capability grants are additive, scoped permissions that authorize specific
|
||||
actions. By default, workers get no grants (least privilege). Common grants:
|
||||
|
||||
- `"network"` with scope `"github.com"` — allow outbound HTTP to GitHub
|
||||
- `"git-push"` — allow `git push` to remotes
|
||||
- `"provider-secrets"` — allow accessing provider API keys
|
||||
- `"release"` — allow release-related operations (tagging, publishing)
|
||||
- `"workspace-write"` with scope `"crates/tui/**"` — allow writes within a path
|
||||
|
||||
### Environment Sanitization
|
||||
|
||||
The host adapter layer enforces environment sanitization at worker start:
|
||||
|
||||
- Only `HOME`, `PATH`, and platform-specific vars (`SYSTEMROOT`, `COMSPEC`) are
|
||||
injected into worker processes by default
|
||||
- Environment allowlists reject any key containing `SECRET`, `TOKEN`, `PASSWORD`,
|
||||
`PASSWD`, `API_KEY`, `CREDENTIAL`, or `PRIVATE_KEY`
|
||||
- SSH workers only send explicitly allowlisted variables via OpenSSH `SendEnv`
|
||||
- Secret values are never embedded in worker argv, task instructions, or fleet
|
||||
logs — only secret refs appear, and they are always redacted
|
||||
|
||||
Reference in New Issue
Block a user