Sign inSign up

olegselajev241/herdr-crew-kit:latest

Manifest digest

sha256:487a53acd68b87671415c69a283db6c7015bbb94933f90ff3053604835ceb895

Last pushed

about 18 hours by olegselajev241

Type

Sandbox Kit

Manifest digest

sha256:487a53acd68b87671415c69a283db6c7015bbb94933f90ff3053604835ceb895

yaml
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