sha256:487a53acd68b87671415c69a283db6c7015bbb94933f90ff3053604835ceb895
Last pushed
about 18 hours by olegselajev241
Type
Sandbox Kit
Manifest digest
sha256:487a53acd68b87671415c69a283db6c7015bbb94933f90ff3053604835ceb895
schemaVersion: "2"
kind: sandbox
name: herdr-crew
version: 0.5.6
displayName: Herdr Crew
description: |
Task sandbox running a nested Herdr server and a crew of coding agents
(Claude Code, Codex, pi with Google models) in rotating roles: a sandboxed orchestrator
that holds the goal, plus planner, implementer, and QC. The host only
delivers goals and arbitrates resources.
sourceURL: https://github.com/shelajev/herdr
args:
claude_model:
default: claude-opus-5-5
description: Model passed explicitly to Claude Code
codex_model:
default: gpt-6.1-sol
description: Model passed explicitly to Codex
gemini_model:
default: gemini-3.8-flash
description: Google model the pi role pins with --provider google --model
image:
default: docker.io/olegselajev241/herdr-crew:0.5.1
description: Published linux/arm64 Herdr crew template image (0.5.1, digest sha256:45787cc3a320ee7677a1ca248beff29093d78748a30bbf6b7f7a4bae9eca230f); for local builds pass herdr-crew:local after scripts/sbx/build-crew-template.sh
pi_provider:
default: google
description: Provider paired with the pi model
roles:
default: orchestrator=claude,planner=codex,implementer=claude,qc=pi
description: Role assignment for this task; the host rotates this per task and guarantees qc differs from implementer
sandbox:
image: ${{ kit.args.image }}
entrypoint:
- crew-entry
agentInstructions:
filename: AGENTS.md
content: |
## Herdr crew
This sandbox runs a crew of coding agents: an orchestrator that holds the
task goal and coordinates everyone through the local herdr CLI, plus a
planner, an implementer, and quality control. Your role is named in the
prompt you receive; read your prompt pack in `/home/agent/crew/roles/`
and follow it. Only the orchestrator drives other agents; every other
role works solely on what it is prompted with. The role-to-agent
assignment for this task is recorded in `/home/agent/crew/assignment`.
Shared crew files under `/home/agent/crew/`: `goal.md` (the goal, owned by
the orchestrator), `plan.md` (the plan, owned by the planner), `qc-log.md`
(findings and VERDICT lines, owned by qc), `review-log.md` (optional
nonterminal reviews, owned by the assigned reviewer), `status.md` (progress notes and
the final RESULT line, owned by the orchestrator), and `escalations.md`
(resource requests for the host to approve or deny).
permissions:
network:
allow:
- api.anthropic.com
- statsig.anthropic.com
- claude.ai
- console.anthropic.com
- platform.claude.com
- downloads.claude.ai
- claude.com
- code.claude.com
- api.openai.com
- openai.com
- auth.openai.com
- chatgpt.com
- files.openai.com
- codeload.github.com
- generativelanguage.googleapis.com
- herdr.dev
- github.com
- api.github.com
- raw.githubusercontent.com
- objects.githubusercontent.com
- release-assets.githubusercontent.com
- registry.npmjs.org
- archive.ubuntu.com
- security.ubuntu.com
- ports.ubuntu.com
- download.docker.com
credentials:
- service: anthropic
apiKey:
name: ANTHROPIC_API_KEY
inject:
- domain: api.anthropic.com
header: x-api-key
format: '%s'
- domain: claude.ai
header: x-api-key
format: '%s'
- domain: console.anthropic.com
header: x-api-key
format: '%s'
oauth:
tokenEndpoint:
host: platform.claude.com
path: /v1/oauth/token
sentinels:
accessToken: sk-ant-oat01-proxy-managed
refreshToken: sk-ant-ort01-proxy-managed
credentialFile:
path: ~/.claude/.credentials.json
template: '{"claudeAiOauth":{"accessToken":"{{.AccessToken}}","refreshToken":"{{.RefreshToken}}","expiresAt":{{.ExpiresAt}},"scopes":{{.ScopesJSON}}}{{if .PrimaryApiKey}},"primaryApiKey":"{{.PrimaryApiKey}}"{{end}}}'
skipIfEnv:
- ANTHROPIC_API_KEY
- service: openai
apiKey:
name: OPENAI_API_KEY
inject:
- domain: api.openai.com
header: Authorization
format: Bearer %s
- domain: openai.com
header: Authorization
format: Bearer %s
oauth:
tokenEndpoint:
host: auth.openai.com
path: /oauth/token
resourceHosts:
- chatgpt.com
sentinels:
accessToken: oai-oat01-proxy-managed
refreshToken: oai-ort01-proxy-managed
skipIfEnv:
- OPENAI_API_KEY
- service: gemini
required: true
apiKey:
name: GEMINI_API_KEY
proxyManaged: true
inject:
- domain: generativelanguage.googleapis.com
header: x-goog-api-key
format: '%s'
environment:
variables:
CODEX_HOME: /home/agent/.codex
DISABLE_AUTOUPDATER: "1"
GIT_TERMINAL_PROMPT: "0"
HERDR_CREW: "1"
HERDR_CREW_CLAUDE_MODEL: ${{ kit.args.claude_model }}
HERDR_CREW_CODEX_MODEL: ${{ kit.args.codex_model }}
HERDR_CREW_GEMINI_MODEL: ${{ kit.args.gemini_model }}
HERDR_CREW_PI_PROVIDER: ${{ kit.args.pi_provider }}
IS_SANDBOX: "1"
setup:
install:
- command: command -v python3 >/dev/null
user: "1000"
description: Verify Python is available for crew model and phase helpers
- command: crew-check
user: "1000"
description: Verify herdr and the crew agent CLIs are present
- command: |
set -e
[ ! -L /home/agent/.claude.json ] || exit 1
[ ! -e /home/agent/.claude.json ] || exit 0
ws="${WORKSPACE_DIR:-/}"
esc=$(printf '%s' "$ws" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/\\t/g; s/\r/\\r/g')
projects="\"/\": { \"hasTrustDialogAccepted\": true }"
[ "$ws" = "/" ] || projects="$projects, \"$esc\": { \"hasTrustDialogAccepted\": true }"
printf '%s\n' "{
\"bypassPermissionsModeAccepted\": true,
\"hasCompletedOnboarding\": true,
\"projects\": { $projects }
}" > /home/agent/.claude.json
chmod 600 /home/agent/.claude.json
chown agent:agent /home/agent/.claude.json
user: "0"
description: Seed Claude bypass/trust flags
- command: |
set -e
[ ! -L /home/agent/.claude ] && [ ! -L /home/agent/.claude/settings.json ] || exit 1
mkdir -p /home/agent/.claude
[ ! -e /home/agent/.claude/settings.json ] || exit 0
HELPER=''
if [ "${SBX_CRED_ANTHROPIC_MODE:-none}" != none ]; then
HELPER=' "apiKeyHelper": "echo proxy-managed",
'
fi
printf '%s' "{
\"themeId\": 1,
\"alwaysThinkingEnabled\": true,
\"env\": { \"DISABLE_AUTOUPDATER\": \"1\" },
${HELPER} \"permissions\": { \"defaultMode\": \"bypassPermissions\" },
\"bypassPermissionsModeAccepted\": true,
\"skipDangerousModePermissionPrompt\": true
}
" > /home/agent/.claude/settings.json
user: agent
description: Seed Claude settings.json from SBX_CRED_ANTHROPIC_MODE
- command: |
set -e
[ ! -L /home/agent/.codex ] && [ ! -L /home/agent/.codex/config.toml ] && [ ! -L /home/agent/.codex/auth.json ] || exit 1
mkdir -p /home/agent/.codex
MODE="${SBX_CRED_OPENAI_MODE:-none}"
if [ ! -e /home/agent/.codex/config.toml ]; then
cat > /home/agent/.codex/config.toml << 'EOF'
approval_policy = "never"
sandbox_mode = "danger-full-access"
mcp_oauth_credentials_store = "file"
check_for_update_on_startup = false
EOF
case "$MODE" in
oauth|apikey)
cat >> /home/agent/.codex/config.toml << 'EOF'
forced_login_method = "api"
EOF
;;
esac
if [ "$MODE" = oauth ]; then
cat >> /home/agent/.codex/config.toml << 'EOF'
model_provider = "sandboxd"
[model_providers.sandboxd]
name = "Sandbox Proxy"
base_url = "https://chatgpt.com/backend-api/codex"
experimental_bearer_token = "oai-oat01-proxy-managed"
requires_openai_auth = false
EOF
fi
fi
if [ ! -e /home/agent/.codex/auth.json ]; then
case "$MODE" in
oauth|apikey)
cat > /home/agent/.codex/auth.json << 'EOF'
{
"OPENAI_API_KEY": "proxy-managed"
}
EOF
;;
*)
rm -f /home/agent/.codex/auth.json
;;
esac
fi
user: agent
description: Seed Codex config/auth from SBX_CRED_OPENAI_MODE
- command: |
set -e
fail() {
printf 'herdr-crew: %s; cannot seed Codex trust for the task workspace; the host pair smoke must confirm which variable carries the runtime workspace path during install\n' "$*" >&2
exit 1
}
nl=$(printf '\nx')
nl=${nl%x}
raw="${WORKSPACE_DIR:-}"
[ -n "$raw" ] || fail "WORKSPACE_DIR is unset or empty"
case "$raw" in
*[[:cntrl:]]*) fail "WORKSPACE_DIR contains control characters" ;;
/*) ;;
*) fail "WORKSPACE_DIR is not an absolute path ('$raw')" ;;
esac
# The sentinel keeps command substitution from stripping a trailing
# newline that belongs to the resolved name; that newline is then seen
# by the control-character check instead of silently naming another
# directory.
ws=$(readlink -f -- "$raw" 2>/dev/null || :; printf x)
ws=${ws%x}
ws=${ws%"$nl"}
[ -n "$ws" ] || fail "WORKSPACE_DIR ('$raw') cannot be resolved (readlink -f failed or is missing)"
case "$ws" in
/) fail "WORKSPACE_DIR ('$raw') resolves to the filesystem root" ;;
*[[:cntrl:]]*) fail "WORKSPACE_DIR ('$raw') resolves to a path with control characters" ;;
esac
[ -d "$ws" ] || fail "WORKSPACE_DIR ('$raw') is not a directory"
# The trusted path must be the very directory that was validated.
[ "$ws" -ef "$raw" ] || fail "WORKSPACE_DIR ('$raw') does not resolve to the same directory as its canonical path"
home=$(readlink -f -- "${HOME:-/home/agent}" 2>/dev/null) || home=""
for h in /home/agent "$home"; do
[ -n "$h" ] || continue
case "$h/" in
"$ws"/*) fail "WORKSPACE_DIR ('$raw') is the agent home directory or an ancestor of it" ;;
esac
done
case "$ws" in
/home | /tmp | /usr | /etc | /var | /root) fail "WORKSPACE_DIR ('$raw') is a system directory" ;;
esac
printf '%s\n' "herdr-crew: seeding Codex trust for $ws (from WORKSPACE_DIR)"
esc=$(printf '%s' "$ws" | sed 's/\\/\\\\/g; s/"/\\"/g')
header="[projects.\"$esc\"]"
if ! grep -qxF "$header" /home/agent/.codex/config.toml; then
printf '%s\ntrust_level = "trusted"\n' "$header" >> /home/agent/.codex/config.toml
fi
user: agent
description: Seed Codex trust for the task workspace only
- command: |
set -e
[ -n "${MCP_GATEWAY_URL:-}" ] || exit 0
python3 - <<'PY'
import json
import os
from pathlib import Path
import tempfile
import tomllib
from urllib.parse import urlsplit
def fail():
raise SystemExit("herdr-crew: invalid gateway or conflicting native MCP config; no MCP config written")
url = os.environ["MCP_GATEWAY_URL"]
try:
endpoint = urlsplit(url)
port = endpoint.port
if (endpoint.scheme not in ("http", "https") or not endpoint.hostname
or endpoint.username is not None or endpoint.password is not None
or endpoint.query or endpoint.fragment
or any(character.isspace() or not character.isprintable() for character in url)):
fail()
home = Path("/home/agent")
claude_path = home / ".claude.json"
codex_dir = home / ".codex"
codex_path = codex_dir / "config.toml"
if any(path.is_symlink() for path in (home, claude_path, codex_dir, codex_path)):
fail()
claude = json.loads(claude_path.read_text()) if claude_path.exists() else {}
servers = claude.setdefault("mcpServers", {})
desired = {"type": "http", "url": url}
if "central-beans" in servers and servers["central-beans"] != desired:
fail()
servers["central-beans"] = desired
codex_text = codex_path.read_text() if codex_path.exists() else ""
codex = tomllib.loads(codex_text)
codex_servers = codex.get("mcp_servers", {})
if "central-beans" in codex_servers:
if codex_servers["central-beans"] != {"url": url}:
fail()
else:
codex_text += '\n[mcp_servers.central-beans]\nurl = ' + json.dumps(url) + '\n'
tomllib.loads(codex_text)
except (ValueError, TypeError, AttributeError, OSError):
fail()
def write_private(path, content):
temporary = None
try:
with tempfile.NamedTemporaryFile(mode="w", dir=path.parent, delete=False) as output:
temporary = Path(output.name)
output.write(content)
temporary.replace(path)
finally:
if temporary is not None:
temporary.unlink(missing_ok=True)
codex_dir.mkdir(mode=0o700, exist_ok=True)
write_private(claude_path, json.dumps(claude, indent=2) + "\n")
write_private(codex_path, codex_text)
PY
user: agent
description: Seed native Claude and Codex MCP from the task-scoped gateway only
startup:
- command:
- sh
- -c
- export PATH="/home/agent/.local/bin:$PATH"; while true; do herdr agent list >/dev/null 2>&1 || herdr server >>/tmp/herdr-server.log 2>&1; sleep 5; done
user: agent
background: true
description: Keep the headless herdr server running in detached sandboxes
files:
- path: /home/agent/crew/models
content: claude=${{ kit.args.claude_model }},codex=${{ kit.args.codex_model }},pi=${{ kit.args.pi_provider }}/${{ kit.args.gemini_model }}
description: Authoritative model pins for all crew start paths
- path: /home/agent/crew/assignment
content: ${{ kit.args.roles }}
description: Role-to-agent assignment for this task
- path: /home/agent/crew/workdir
content: ${WORKDIR}
description: Absolute workspace path, read by the host task driver