Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
1 line
47 KiB
JSON
1 line
47 KiB
JSON
{"repo": "5dive-ai/5dive", "files": {"src/header.sh": {"0dc192c3fb51066597d50d61f36c8d32ea43f2d7": {"t": 1786067513.5742025, "n": 46121, "h": "9534a89df48e6789", "c": "#!/usr/bin/env bash\n# 5dive agent management CLI — runs on user's runtime VM.\n# State: /var/lib/5dive/agents.json (registry) + agents.d/<name>.env (per-agent systemd env).\n# Each agent = Linux user `agent-<name>` in `claude` group (inherits shared\n# /home/claude/.config|.claude|.codex|.aws) + systemd unit 5dive-agent@<name>.service\n# running tmux session `agent-<name>` with the chosen CLI in a restart loop.\n#\n# Output contract:\n# - `--json` is accepted as a GLOBAL flag on any subcommand; stdout is then an\n# envelope `{ok:true,data:...}` on success or `{ok:false,error:{code,class,message}}`\n# on error. Text-mode stderr stays human-readable. Exit code always matches\n# error.code (see E_* below) so shell pipelines can branch without parsing.\n# - Progress `==>` lines always go to stderr so JSON stdout parses cleanly.\nset -euo pipefail\n\n# Some sbin tools (adduser, usermod, userdel) live in /usr/sbin and /sbin. On\n# a normal interactive shell they're on PATH already, but when this script is\n# spawned from a systemd unit that overrides PATH= (or any other restricted\n# parent), /usr/sbin can be missing and the very first agent-create fails\n# with \"adduser: command not found\". Prepend them unconditionally — duplicate\n# entries are harmless.\ncase \":$PATH:\" in\n *\":/usr/sbin:\"*) ;;\n *) export PATH=\"/usr/local/sbin:/usr/sbin:/sbin:$PATH\" ;;\nesac\n\n# NOT A VERSION, AND NOT BUMPED HERE (DIVE-2247). Since assignment moved to tag\n# time, main carries this sentinel and release-cut.yml writes the real version onto\n# the detached release commit it builds. `build.sh` checks the line exists; CI fails\n# the bundle-drift check if it's missing or empty.\n#\n# IF YOU ARE RESOLVING A MERGE CONFLICT ON THIS LINE, read this first: the other\n# side is a hand-assigned version from the era this ticket ended. Keeping the\n# sentinel is correct — but that version was CLAIMED, and possibly installed\n# somewhere, so it must not be re-issued. Check it is <= .release-floor and raise\n# the floor if it is not. Graded by tests/release_cut_assign_unit.sh.\nreadonly FIVE_VERSION=\"0.0.0-dev\"\n\n# GitHub org our repos live under. The org is being renamed\n# 5dive-com -> 5dive-ai (2026-06); fetches must work on either side of the\n# rename, so probe the new org once per process and fall back to the old\n# name. GH_ORG env overrides the probe (CI, forks, air-gapped mirrors).\n# NOTE: install.sh carries a standalone copy of this probe (it runs before\n# the bundle exists) — change the two together.\ngh_org() {\n if [[ -z \"${_GH_ORG_RESOLVED:-}\" ]]; then\n if [[ -n \"${GH_ORG:-}\" ]]; then\n _GH_ORG_RESOLVED=\"$GH_ORG\"\n elif curl -fsI --max-time 8 \"https://raw.githubusercontent.com/5dive-ai/5dive/main/install.sh\" >/dev/null 2>&1; then\n _GH_ORG_RESOLVED=\"5dive-ai\"\n else\n _GH_ORG_RESOLVED=\"5dive-com\"\n fi\n fi\n printf '%s' \"$_GH_ORG_RESOLVED\"\n}\n\n# DIVE-1475: honor an env-set STATE_DIR instead of unconditionally reopening the\n# LIVE store. Lets a test isolate to a temp tree that STICKS through sourcing (and\n# survives into forked subprocesses via `sudo -E`/env_keep) — the isolation-failure\n# class behind the 2026-07-19 /goal-spam AND the board wipe. Prod is unaffected:\n# with the env var unset this is exactly \"/var/lib/5dive\".\nSTATE_DIR=\"${STATE_DIR:-/var/lib/5dive}\"\nREGISTRY=\"${STATE_DIR}/agents.json\"\nENV_DIR=\"${STATE_DIR}/agents.d\"\n\n# DIVE-2336: the payload meaning \"the env-override reporter did not run\". Defined HERE, not\n# in lib/env_overrides.sh, because it is the fallback for that file being unavailable — a\n# fallback must not live inside the thing it is a fallback for. Caught by the full suite:\n# tests/selfcheck_unit.sh sources only header/error_codes/output/cmd_selfcheck, so a\n# call-site fallback that invoked `_env_ov_unavailable` found NO function, produced an\n# empty string, and `jq --argjson eov \"\"` killed the whole --json contract (33/0 -> 26/7).\n# NOT named FIVE_* on purpose: tests/lib/env_isolation.sh (DIVE-2325) blanket-clears that\n# namespace, which would delete this constant inside every harness that needs it.\n_5D_ENV_OV_UNAVAILABLE='{\"process\":[],\"configured\":[],\"configured_state\":\"unavailable\",\"configured_unreadable\":[]}'\nSYSTEMD_UNIT=\"5dive-agent@\"\n\n# Bumped when the on-disk registry shape changes in a way that older CLIs\n# can't read. ensure_state stamps this into agents.json on create + migrates\n# v0 (no version field) registries in place. Keep migrations pure-jq so they\n# run without extra deps.\nreadonly REGISTRY_SCHEMA_VERSION=2\n\n# Exclusive lock for mutating commands. Two dashboard clicks on \"create\" with\n# the same name used to race on adduser + registry_write; now every mutation\n# goes through with_registry_lock so there's exactly one writer at a time.\nREGISTRY_LOCK=\"${STATE_DIR}/registry.lock\"\n\n# Append-only audit trail. Every mutating CLI invocation emits one NDJSON\n# line with {ts,user,cmd,args,result,code}. Sensitive flags (api keys, bot\n# tokens, callback codes) are redacted before write. The HTTP/exec path can\n# pass the Clerk user via FIVEDIVE_AUDIT_USER; otherwise we fall back to\n# SUDO_USER / USER.\nAUDIT_LOG=\"/var/log/5dive/agent-audit.log\"\n\n# Named auth profiles let two agents of the same type authenticate against\n# different accounts/keys. Each profile is a directory of env files (one per\n# type) + any captured CLI config (e.g. a per-profile ~/.claude). The default\n# profile has no name and uses the shared /etc/5dive/connectors/*.env files\n# so existing single-account setups keep working unchanged.\nAUTH_PROFILES_DIR=\"${STATE_DIR}/auth-profiles\"\n\n# Device-code login sessions for the non-TTY auth flow. Each live session is\n# a tmux window owned by the `claude` user, driving `claude setup-token` (or\n# equivalent). State lives under sessions/<id>/ — the dashboard polls it via\n# `5dive agent auth poll` so no PTY bridge is required.\nAUTH_SESSIONS_DIR=\"${STATE_DIR}/auth-sessions\"\n\n# Default tmux cwd for a newly-created agent. Per-agent override goes in the\n# registry as .agents[name].workdir and is written to AGENT_WORKDIR in the\n# systemd env file — 5dive-agent-start.sh reads it and falls back to this\n# path if the configured dir isn't accessible.\nDEFAULT_WORKDIR=\"/home/claude/projects\"\n\n# Per-agent channel secrets live here (readable by the agent user via\n# EnvironmentFile in 5dive-agent@.service). Mode 0640 root:claude is written\n# by the 5dive-write-connector helper — we call it so perms stay consistent.\n# DIVE-1500: FIVEDIVE_CONNECTOR_DIR lets a test harness point channel\n# resolution at fixture configs so it never picks up a real paired token\n# (same env-honor class as STATE_DIR/TASKS_DIR/TASKS_DB). Note _task_agent_channel\n# still falls back to $TELEGRAM_BOT_TOKEN from the process env, so a fixture\n# harness should ALSO set FIVEDIVE_NOTIFY_DRYRUN=1 — the physical send guard.\nCONNECTORS_DIR=\"${FIVEDIVE_CONNECTOR_DIR:-/etc/5dive/connectors}\"\n\n# Known agent types -> (bin path, supports channels yes/no).\n# auth_file is the shared-config path that indicates the type is authenticated.\n# Extend here to add a new agent type.\n#\n# ADDING A TYPE — the absent-vs-zero contract for every TYPE_* map below.\n# These maps are read under `set -uo pipefail`, so whether an omitted key\n# DEGRADES or CRASHES is a per-map decision that has to be written down. Two\n# kinds, and the difference is not guessable from the declaration:\n#\n# REQUIRED — TYPE_BIN. It IS the type registry: is_known_type is literally\n# `[[ -n \"${TYPE_BIN[$1]+x}\" ]]` (lib/validation.sh), and readers\n# loop `${!TYPE_BIN[@]}`. A type absent here does not exist, so\n# bare reads are correct by construction.\n# OPTIONAL — everything else (TYPE_CHANNELS, TYPE_AUTH, TYPE_INSTALL,\n# TYPE_API_FILE, TYPE_API_VAR, TYPE_PROBE, TYPE_SKILLS_DIR).\n# Omission is a MEANINGFUL signal with a documented default, so\n# every reader MUST supply it: `${TYPE_X[$type]:-<default>}`.\n# A bare read turns a supported state into an unbound-variable\n# crash that names the ARRAY and not the type — which is exactly\n# the bug DIVE-2076 fixed in five readers, and DIVE-2076's sweep\n# found again in cmd_auth_set, where the crash pre-empted a\n# graceful `fail` written for that very case one line below.\n#\n# Registering a new type: add TYPE_BIN (mandatory) and then decide each optional\n# map explicitly. Prefer an explicit entry over relying on the default — write\n# `[newtype]=0` in TYPE_CHANNELS rather than omitting it, so the intent is\n# reviewable. tests/type_map_registration_contract_unit.sh enforces both halves:\n# every optional-map reader carries a default, and TYPE_BIN keys are covered.\ndeclare -A TYPE_BIN=(\n [claude]=\"/home/claude/.local/bin/claude\"\n # codex is an npm global under nvm's per-version bin dir. Rather than hardcode\n # a single version path (the `v24` alias lags real node upgrades — a fresh box on\n # v24.18.0 left /home/claude/.nvm/.../v24/bin/codex stale and surfaced as\n # not_installed, DIVE-1329), the TYPE_INSTALL recipe symlinks the freshly\n # installed codex into ~/.local/bin (same dance as opencode/grok/pi) so\n # TYPE_BIN resolves on every box regardless of the active node version.\n [codex]=\"/home/claude/.local/bin/codex\"\n [hermes]=\"/home/claude/.local/bin/hermes\"\n [openclaw]=\"/home/claude/.local/bin/openclaw\"\n [opencode]=\"/home/claude/.local/bin/opencode\"\n # antigravity is Google's native-Go successor to gemini-cli. The installer\n # lands it at ~/.local/bin/agy. State dir is ~/.gemini/antigravity-cli/\n # (the binary identifies as product=antigravity but reuses Google's\n # ~/.gemini parent — see launch log in the antigravity scaffold landed\n # in 5dive@<post-removal>).\n [antigravity]=\"/home/claude/.local/bin/agy\"\n # grok is xAI's CLI. Installer drops the binary at ~/.grok/bin/grok and\n # symlinks ~/.local/bin/grok — we point TYPE_BIN at the symlink to match\n # the convention of the other types.\n [grok]=\"/home/claude/.local/bin/grok\"\n # pi is earendil-works' (Armin Ronacher) TypeScript/Node20 coding agent, the\n # backbone of OpenClaw. `npm i -g @earendil-works/pi-coding-agent` drops the\n # `pi` binary in nvm's per-version bin dir; the TYPE_INSTALL recipe symlinks\n # it into ~/.local/bin so TYPE_BIN resolves on every box (same dance as\n # opencode/openclaw). MIT, ~70.8k stars. Added for the v0.9 pi epic (DIVE-1196).\n [pi]=\"/home/claude/.local/bin/pi\"\n # devin is Cognition's CLI (native binary). The cli.devin.ai installer\n # drops a versioned store at ~/.local/share/devin/cli/_versions/ and\n # symlinks ~/.local/bin/devin — TYPE_BIN points at the symlink, same\n # convention as grok/opencode.\n [devin]=\"/home/claude/.local/bin/devin\"\n)\n# Which types accept --channels=telegram|discord. Each type wires the channel\n# differently (see install_channel_for_<type>_agent below):\n# claude — installs claude-plugins-official's telegram/discord plugin into\n# the agent user's ~/.claude/plugins; the bun server writes\n# ~/.claude/channels/<plugin>/access.json on first launch and\n# cmd_pair pops a pairing code into it.\n# openclaw — `openclaw channels add --channel <ch> --token <token>` writes\n# the credential into the openclaw gateway config; the openclaw\n# `pairing` subcommand handles inbound user approvals separately.\n# hermes — writes TELEGRAM_BOT_TOKEN / DISCORD_BOT_TOKEN to the agent\n# user's ~/.hermes/.env; hermes' gateway picks it up at startup.\n# codex — writes the bot token + access.json into the agent user's\n# ~/.codex/channels/telegram/; 5dive-agent-start wires the\n# telegram-codex MCP server + lifecycle hooks into config.toml\n# and launches codex with --dangerously-bypass-hook-trust.\n# telegram only (no discord build for codex yet).\n# grok — same shape as codex: writes ~/.grok/channels/telegram/{.env,\n# access.json}; 5dive-agent-start writes [mcp_servers.telegram]\n# + [[hooks.*]] into ~/.grok/config.toml. grok runs with\n# --always-approve (set in 5dive-agent-start), which also\n# auto-trusts plugin/MCP commands. telegram only.\n# Only claude needs the pair-code roundtrip — see cmd_pair's dispatch.\n#\n# OPTIONAL map, default 0 (see the absent-vs-zero contract above TYPE_BIN).\n# An omitted key now reads as \"no channel support\" and `--channels` fails with\n# the normal validation error naming the type; before DIVE-2076 it hard-crashed\n# `agent types` with an unbound-variable error naming this array instead.\n# Still write `[newtype]=0` explicitly rather than omitting: the default keeps a\n# customer's box from crashing, it is not a substitute for declaring intent.\ndeclare -A TYPE_CHANNELS=(\n [claude]=1\n [openclaw]=1\n [hermes]=1\n [codex]=1\n [grok]=1\n # opencode ships a telegram bridge too, but as a STANDALONE RELAY (not an MCP\n # server): telegram-opencode/server.ts IS the agent's main process and spawns\n # `opencode serve` over loopback HTTP. 5dive-agent-start launches `bun run\n # --cwd <plugin> start` instead of the opencode TUI; install writes the token\n # + access.json into ~/.opencode/channels/telegram. telegram only.\n [opencode]=1\n # antigravity (agy) ships the same telegram MCP bridge as grok/codex —\n # ~/.gemini/channels/telegram/{.env,access.json} + a shared plugin checkout\n # whose MCP server + lifecycle hooks 5dive-agent-start writes into the\n # GLOBAL ~/.gemini/config/{mcp_config.json,hooks.json} at boot (agy doesn't\n # auto-load a plugin's mcp_config/hooks — only skills/agents). telegram only.\n [antigravity]=1\n # pi ships a telegram bridge as a native pi EXTENSION (fork of benedict2310/\n # TelePi — pi has no MCP-server plugin model like codex/grok; it exposes an\n # in-process extension API). The channel installer (install_channel_for_pi_agent)\n # + telegram-pi plugin land in DIVE-1201/DIVE-1202; until then this flag only\n # marks pi channel-capable — creating a pi agent WITH --channels will fail at\n # install_channel_for_agent's dispatch until 1201 adds the `pi)` case. telegram only.\n [pi]=1\n # devin has no telegram/discord bridge yet — reachable via agent send/ask,\n # the task queue, and sibling agents. Explicit 0 (not omission) to declare\n # the no-channel intent, per the DIVE-2076 note above this map.\n [devin]=0\n)\n# Auth sentinel per type. Agent users run as agent-<name> (in group `claude`)\n# and cannot read /home/claude/.claude/settings.json (mode 0600), so for\n# claude-family types we check /etc/5dive/connectors/anthropic.env (0640\n# root:claude) — that's the file systemd injects via EnvironmentFile.\n# Format: \"<path>\" -> file must exist and be non-empty\n# \"<path>:<KEY>\" -> if path ends in .env, grep ^KEY=; else jq .env[KEY]\n# Omit a type entirely to mark it auth-optional — auth_status_one returns \"ok\"\n# without checking. opencode is the canonical example: it ships with free models\n# and runs out of the box, so the dashboard shouldn't gate `agent create` on a\n# sign-in the user doesn't need.\ndeclare -A TYPE_AUTH=(\n [claude]=\"/etc/5dive/connectors/anthropic.env:CLAUDE_CODE_OAUTH_TOKEN\"\n [codex]=\"/home/claude/.codex/auth.json\"\n # Apr 2026 Anthropic policy change: third-party harnesses can no longer ride\n # the user's Claude Pro/Max subscription token (suspension risk). hermes and\n # openclaw both sign in via OpenAI's /codex/device flow now. hermes writes\n # ~/.hermes/auth.json; openclaw writes its agent-scoped auth-profiles.json\n # under the default agent id \"main\" (resolved by openclaw's resolveAgentDir).\n [hermes]=\"/home/claude/.hermes/auth.json\"\n [openclaw]=\"/home/claude/.openclaw/agents/main/agent/auth-profiles.json\"\n # antigravity tries the OS keyring first (via DBus secret-service) and\n # falls back to a file at ~/.gemini/antigravity-cli/antigravity-oauth-token\n # (mode 0600). Verified empirically against agy 1.0.1: after the device-\n # code flow completes (user pastes the Google OAuth callback code), the\n # binary writes the token-blob file with this exact name — no .json\n # extension, just the bare filename. Agent users run without a DBus\n # session, so the file path is always the live sentinel.\n [antigravity]=\"/home/claude/.gemini/antigravity-cli/antigravity-oauth-token\"\n # grok writes ~/.grok/auth.json on successful `grok login --device-auth`.\n # Verified empirically — auth.json.lock pre-exists the actual auth.json\n # file (created on first device-auth attempt for the locking mechanism).\n [grok]=\"/home/claude/.grok/auth.json\"\n # pi writes credentials to ~/.pi/agent/auth.json on `/login` (API-key provider\n # selection or OAuth for subscription providers; tokens auto-refresh). Verified\n # against @earendil-works/pi-coding-agent docs + benedict2310/TelePi 2026-07-14.\n # Same file-sentinel shape as codex/grok. NOTE: pi ALSO accepts a bare\n # ANTHROPIC_API_KEY/OPENAI_API_KEY env var (no file written) — the api-key\n # injection path (TYPE_API_FILE/VAR + cmd_auth) is finalized in DIVE-1200.\n [pi]=\"/home/claude/.pi/agent/auth.json\"\n # devin writes ~/.local/share/devin/credentials.toml on `devin auth login`\n # (browser OAuth against the user's Devin account — no API key). Verified\n # empirically against devin 3000.2.17.\n [devin]=\"/home/claude/.local/share/devin/credentials.toml\"\n)\n# Installer recipe per type. Run as `claude` user via `sudo -u claude -i bash -lc <recipe>`\n# so $HOME/.nvm and PATH resolve correctly. Empty string => no automated installer\n# (caller must hand-install). Idempotent: each recipe checks first.\ndeclare -A TYPE_INSTALL=(\n # Gate on the EXACT TYPE_BIN path, never `command -v claude` — DIVE-2075,\n # reported by A-MO7SEN (github #196) and hit live during `5dive init` on a box\n # carrying an old npm-global claude. `command -v` answers \"is something named\n # claude on PATH\", which is a different question from \"is the binary I am about\n # to hand to the systemd unit present\": the stray wins, the recipe no-ops in\n # 0s, ~/.local/bin/claude is never created, and cmd_install fails with\n # \"install reported success but $bin still missing\" — permanently, because the\n # gate can never flip no matter how many times you retry. Same defect class as\n # DIVE-2061 (`command -v 5dive` grading the installed CLI instead of the bundle\n # under test). This is the question the codex comment below already prescribes.\n [claude]=\"[[ -x /home/claude/.local/bin/claude ]] || curl -fsSL https://claude.ai/install.sh | bash\"\n # devin: same exact-path guard; the installer self-manages the versioned\n # store + ~/.local/bin symlink, so -x makes the recipe idempotent.\n [devin]=\"[[ -x /home/claude/.local/bin/devin ]] || curl -fsSL https://cli.devin.ai/install.sh | bash\"\n # Verify the EXACT TYPE_BIN path (not `command -v codex`): a stray\n # /usr/bin/codex from apt or a codex left over under a non-v24 nvm major\n # would short-circuit the install and surface as \"install reported success\n # but bin missing\". `nvm install 24` provisions the pinned runtime on a fresh\n # box and selects it, forcing the npm install -g to land in v24's real bin\n # dir even when the `v24` alias has drifted (same drift the nightly\n # soft-updates hit — DIVE-1189). We then one-hop-symlink the just-installed\n # codex into ~/.local/bin (where TYPE_BIN[codex] and the agent unit's PATH\n # look). We resolve its real path deterministically as\n # `dirname $(nvm which 24)/codex` — NOT `command -v codex`, which can land on\n # a stray /usr/bin/codex, and NOT the `/home/.../v24/bin` alias, which lags\n # real node upgrades (a box on v24.18.0 left the alias pointing at v24.16.0\n # and surfaced codex as not_installed — DIVE-1329). `npm install -g` lands the\n # binary in exactly this dir (== `npm prefix -g`/bin), so the symlink is\n # guaranteed to point at the codex we just installed. Mirrors opencode below.\n # DIVE-1189: `5dive agent install codex --upgrade` sets FORCE_INSTALL=1 to skip\n # the -x short-circuit and reinstall @latest in place; without it (the\n # provisioning path) an existing codex is left untouched. \\$-escaped so the\n # var expands when the recipe runs under `bash -lc`, not at array-definition time.\n [codex]=\"{ [[ -z \\\"\\${FORCE_INSTALL:-}\\\" ]] && [[ -x /home/claude/.local/bin/codex ]]; } || { . /home/claude/.nvm/nvm.sh && nvm install 24 >/dev/null && npm install -g @openai/codex@latest && mkdir -p /home/claude/.local/bin && ln -sfn \\\"\\$(dirname \\\"\\$(nvm which 24)\\\")/codex\\\" /home/claude/.local/bin/codex; }\"\n # opencode.ai's installer drops the binary at ~/.opencode/bin/opencode and\n # only adds it to PATH via .bashrc — but bash -lc skips .bashrc on\n # non-interactive shells, so neither the verify check below nor the agent\n # systemd unit (which uses TYPE_BIN's path directly) would find it.\n # Symlink into ~/.local/bin so TYPE_BIN[opencode] resolves on every box.\n [opencode]=\"[[ -x /home/claude/.local/bin/opencode ]] || { curl -fsSL https://opencode.ai/install | bash && mkdir -p /home/claude/.local/bin && ln -sf /home/claude/.opencode/bin/opencode /home/claude/.local/bin/opencode; }\"\n # Both upstreams launch an interactive setup wizard that opens /dev/tty\n # after the binary lands. shelld runs us without a controlling terminal,\n # so the wizard's `exec </dev/tty` blows up with ENXIO and the recipe\n # exits non-zero even though install itself succeeded. Pass the upstream\n # opt-outs (--skip-setup / --no-onboard) to land at the binary and stop.\n # openclaw also defaults to an npm install that drops the binary in\n # nvm's per-version bin dir, not ~/.local/bin — symlink it so TYPE_BIN\n # resolves on every box (same dance as opencode above).\n # hermes' upstream installer recreates /home/claude/.hermes at mode 0700,\n # overriding the 2770 from users.sh and blocking agent-* (claude-group)\n # users from traversing it to exec the venv binary — the unit then\n # crash-loops with `binary not installed`. chmod back to 0775 to match\n # the live perms of /home/claude/.opencode and .local/share/claude.\n [hermes]=\"[[ -x /home/claude/.local/bin/hermes ]] || { curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash -s -- --skip-setup && chmod 0775 /home/claude/.hermes; }\"\n # openclaw's launcher is `#!/usr/bin/env node`, so symlinking only the CLI\n # into ~/.local/bin leaves it unexecutable in systemd/create-time envs where\n # nvm's per-version bin directory is absent from PATH. Install a supported\n # Node 24 and give both executables stable one-hop links in ~/.local/bin.\n # Install the npm package directly under the active Node 24 prefix. The\n # upstream wrapper re-selects nvm's default Node and may attempt a privileged\n # NodeSource upgrade, which fails in our non-interactive `sudo -u claude`\n # installer. FORCE_INSTALL makes --upgrade refresh this exact Node-24 global.\n [openclaw]=\"{ . /home/claude/.nvm/nvm.sh && nvm install 24 >/dev/null && nvm use 24 --silent; } && { [[ \\\"\\${FORCE_INSTALL:-0}\\\" != 1 && -x \\\"\\$(npm prefix -g)/bin/openclaw\\\" ]] || npm --loglevel=error --no-fund --no-audit install -g openclaw@latest; } && mkdir -p /home/claude/.local/bin && ln -sfn \\\"\\$(nvm which 24)\\\" /home/claude/.local/bin/node && ln -sfn \\\"\\$(npm prefix -g)/bin/openclaw\\\" /home/claude/.local/bin/openclaw && [[ -x /home/claude/.local/bin/openclaw ]]\"\n # antigravity's installer drops the native-Go binary at ~/.local/bin/agy\n # and self-updates in the background on each run, so no daily-cron\n # equivalent of @google/gemini-cli's npm update is needed.\n # DIVE-901: gate and verify must agree. `command -v agy` can hit a copy\n # outside TYPE_BIN (PATH drift, image pre-seed) — the recipe then no-ops in\n # 0s and the -x TYPE_BIN guard fails even though agy works. Same class as\n # grok's opportunistic-symlink gap below: ensure the TYPE_BIN symlink\n # ourselves instead of trusting where the binary happened to land.\n # DIVE-2075: the gate itself was still `command -v agy`, so a stray agy on PATH\n # skipped the install and left us hoping the fallback could symlink something.\n # Gate on TYPE_BIN; the trailing `command -v agy` stays, because there the\n # question really is \"where did the installer put it\".\n [antigravity]=\"[ -x /home/claude/.local/bin/agy ] || curl -fsSL https://antigravity.google/cli/install.sh | bash; [ -x /home/claude/.local/bin/agy ] || { mkdir -p /home/claude/.local/bin; p=\\$(command -v agy 2>/dev/null || true); [ -n \\\"\\$p\\\" ] && ln -sf \\\"\\$p\\\" /home/claude/.local/bin/agy; }\"\n # grok's installer drops the binary at ~/.grok/bin/grok but only creates the\n # ~/.local/bin/grok symlink *opportunistically* (its line 328 requires\n # ~/.local/bin already on PATH and ~/.grok/bin not on PATH). On a fresh VM\n # those conditions often don't hold, so it just appends ~/.grok/bin to\n # .bashrc and never makes the symlink TYPE_BIN expects — hence we create the\n # symlink ourselves here rather than trusting the installer. We also drop the\n # installer's ~/.local/bin/agent symlink so it can't shadow future tooling.\n # The binary self-updates on launch; no daily-cron entry needed.\n # DIVE-2075: gate on TYPE_BIN, not `command -v grok` — a stray grok on PATH\n # skipped the install, ~/.grok/bin/grok then did not exist so the symlink\n # branch no-oped too, and the recipe still exited 0 via the trailing `rm -f`.\n [grok]=\"[ -x /home/claude/.local/bin/grok ] || curl -fsSL https://x.ai/cli/install.sh | bash; mkdir -p /home/claude/.local/bin; [ -e /home/claude/.grok/bin/grok ] && ln -sf /home/claude/.grok/bin/grok /home/claude/.local/bin/grok; rm -f /home/claude/.local/bin/agent\"\n # pi is a plain npm package. Install-on-demand like codex (nvm install 24 so the\n # global install lands in v24's bin dir even when the default alias drifted),\n # then symlink into ~/.local/bin like opencode/openclaw so TYPE_BIN[pi]\n # resolves on every box (the systemd unit uses TYPE_BIN's path directly, and\n # bash -lc skips .bashrc so npm's bin dir isn't on PATH). Idempotent via the\n # -x guard. \\$-escaped so npm prefix expands when the recipe runs under bash -lc.\n [pi]=\"[[ -x /home/claude/.local/bin/pi ]] || { . /home/claude/.nvm/nvm.sh && nvm install 24 >/dev/null && npm install -g @earendil-works/pi-coding-agent && mkdir -p /home/claude/.local/bin && ln -sf \\\"\\$(npm prefix -g)/bin/pi\\\" /home/claude/.local/bin/pi; }\"\n)\n\n# vercel-labs/skills CLI agent ID per 5dive type. `npx skills add --agent <id>`\n# uses this to drop SKILL.md into the right per-type dir. openclaw isn't in\n# the upstream registry — passing through its own name makes the CLI fall\n# back to a generic project install at ./skills/<id>, which is what we want.\ndeclare -A SKILLS_AGENT_ID=(\n [claude]=claude-code\n [codex]=codex\n [hermes]=hermes-agent\n [openclaw]=openclaw\n [opencode]=opencode\n # `npx skills add --agent antigravity` is NOT in the upstream registry, but\n # the CLI silently falls back to a generic install path (.agents/skills/) —\n # which is exactly where agy itself reads from (see SKILLS_INSTALL_DIR below).\n # So passing it through works, even though it's an \"unknown\" agent id.\n [antigravity]=antigravity\n # pi, like grok, is a manual-install type (see _skill_needs_manual_install):\n # `npx skills add --agent pi` lands skills in ~/.pi/skills, which pi's resource\n # loader does NOT scan, so pi is git-clone+cp'd into SKILLS_INSTALL_DIR[pi]\n # instead. This id is retained only for the `skill list` annotation, mirroring\n # [grok]=grok (the manual path ignores it for install) (DIVE-1265).\n [pi]=pi\n [grok]=grok\n)\n# Where the skills CLI lands SKILL.md inside the agent user's $HOME, per type.\n# Used for post-install verification, the cmd_skill_list dir-scan fallback,\n# and cmd_skill_rm. Probed empirically against npx skills v0.x — if upstream\n# changes a path, update here. Unknown types fall through to \".claude/skills\"\n# in the lookup sites below.\ndeclare -A SKILLS_INSTALL_DIR=(\n [claude]=\".claude/skills\"\n [codex]=\".agents/skills\"\n [hermes]=\".hermes/skills\"\n [openclaw]=\"skills\"\n [opencode]=\".agents/skills\"\n # agy reads skills from {workspace}/.agents/skills/{name}/SKILL.md — confirmed\n # by grepping the antigravity binary for the path constant. Earlier map said\n # .gemini/antigravity-cli/skills (matching its state dir), which was a guess\n # — wrong. Upstream npx skills fallback already lands at .agents/skills.\n [antigravity]=\".agents/skills\"\n # pi reads user skills from ~/.pi/agent/skills AND ~/.agents/skills (per its\n # resource-loader). We target .agents/skills — the cross-CLI shared dir agy/\n # codex/opencode also use, and where pi's notify-user seed already lands. This\n # is the manual git-clone+cp destination (pi is manual-install; DIVE-1265),\n # and it drives the list/rm dir-scan — add and list now agree here.\n [pi]=\".agents/skills\"\n [grok]=\".grok/skills\"\n)\n\n# api-key target per type: the env file (in /etc/5dive/connectors for the\n# default profile) and the env var inside it. Claude-family is special-cased\n# in cmd_auth_set — `sk-ant-oat01-*` tokens write CLAUDE_CODE_OAUTH_TOKEN,\n# everything else is ANTHROPIC_API_KEY. Non-claude types use a single var\n# that matches what their CLI reads natively.\ndeclare -A TYPE_API_FILE=(\n [claude]=\"anthropic.env\"\n # hermes and openclaw intentionally omitted: both now sign in via OpenAI's\n # /codex/device flow and store credentials in their own files (~/.hermes/\n # auth.json, ~/.openclaw/agents/main/agent/auth-profiles.json). The\n # anthropic.env path no longer feeds either CLI. cmd_auth_set already\n # fails gracefully when a type isn't in this map.\n [codex]=\"openai.env\"\n [opencode]=\"openai.env\"\n [grok]=\"xai.env\"\n # pi is multi-provider (see PI_PROVIDER_VAR): the connector file holds a\n # per-provider *_API_KEY var chosen by `auth set pi --provider`. It's listed\n # here (not in TYPE_API_VAR) so auth_creds_present's default-profile fallback\n # recognizes a pi key written to this file; cmd_auth_set resolves the var\n # itself rather than reading a single TYPE_API_VAR entry. DIVE-1200.\n [pi]=\"pi.env\"\n)\ndeclare -A TYPE_API_VAR=(\n [claude]=\"ANTHROPIC_API_KEY\"\n [codex]=\"OPENAI_API_KEY\"\n [opencode]=\"OPENAI_API_KEY\"\n [grok]=\"XAI_API_KEY\"\n # pi is deliberately absent: it's multi-provider (no single native var).\n # cmd_auth_set resolves pi's target var from --provider via PI_PROVIDER_VAR.\n)\n\n# DIVE-2223: the file each harness ACTUALLY reads for its persona / standing\n# instructions, relative to the agent user's $HOME. Before this map every persona\n# was appended to ~/.claude/CLAUDE.md regardless of type: on a codex / opencode /\n# pi / antigravity seat that write SUCCEEDS into a path with no consumer, so the\n# agent runs bare while every file census counts a persona (same family as\n# DIVE-1930). Rows were measured with a before/after role probe on a live seat,\n# not read from docs — see community/wiki/per-harness-persona-file-paths.md.\n#\n# OPTIONAL map whose default is deliberately NOT the claude path: an unmapped\n# type makes persona_target() REFUSE and warn loudly (src/lib/agent_setup.sh).\n# Silently defaulting to ~/.claude/CLAUDE.md IS the bug this map removes, so an\n# unknown type has to be loud rather than quietly wrong.\ndeclare -A TYPE_PERSONA_FILE=(\n [claude]=\".claude/CLAUDE.md\"\n # grok lands here by DOCUMENTED BEHAVIOUR, not by accident: its\n # docs/user-guide/12-project-rules.md says Claude compatibility is ON by\n # default, so it scans home-level ~/.claude/ for CLAUDE.md / AGENTS.md. One\n # path, two harnesses.\n [grok]=\".claude/CLAUDE.md\"\n # codex ALREADY holds the DIVE-1410 return-channel doc here (preseeded during\n # create by preseed_codex_return_channel), so persona_install_doc PREPENDS —\n # an overwrite would delete operational plumbing to install an identity.\n [codex]=\".codex/AGENTS.md\"\n [opencode]=\".config/opencode/AGENTS.md\"\n # note the /agent segment: pi's resource loader does not read ~/.pi/AGENTS.md\n # (same trap as SKILLS_INSTALL_DIR[pi], DIVE-1265).\n [pi]=\".pi/agent/AGENTS.md\"\n # antigravity reuses Google's ~/.gemini parent (see the TYPE_BIN note).\n [antigravity]=\".gemini/GEMINI.md\"\n # hermes and openclaw are DELIBERATELY UNMAPPED: neither has been probe-verified\n # on a live seat, and a guessed path is exactly the silent no-op this ticket\n # removes. Creating one with a role warns loudly and installs nothing.\n)\n\n# OpenCode reads provider API keys directly from standard environment variables.\n# Keep this catalog deliberately small: these are the providers the 5dive auth\n# path has explicitly verified and can inject without writing OpenCode's native\n# auth.json. OpenRouter is the broad multi-model option; OpenAI remains available\n# for backwards compatibility with the old provider-less auth-set path.\ndeclare -A OPENCODE_PROVIDER_VAR=(\n [openai]=\"OPENAI_API_KEY\"\n [openrouter]=\"OPENROUTER_API_KEY\"\n)\n\n# pi provider -> native env var. pi is API-key multi-provider (NO OAuth): it\n# reads the standard per-provider *_API_KEY var straight from the environment\n# (verified against @earendil-works/pi-coding-agent 0.80.6 dist — it recognizes\n# ANTHROPIC_API_KEY / OPENAI_API_KEY / GEMINI_API_KEY / OPENROUTER_API_KEY /\n# DEEPSEEK_API_KEY / ZAI_API_KEY / MOONSHOT_API_KEY / XAI_API_KEY / ...). 5dive\n# injects the chosen var via the connector env file / profile combined.env the\n# same way codex/opencode/grok do their single native var. DIVE-1200 wired the\n# three core providers; DIVE-1205 adds OpenRouter + the Chinese models\n# (DeepSeek / GLM-Zhipu[=zai] / Kimi-Moonshot / Qwen). IMPORTANT: these are ALL\n# built-in pi providers — pi ships each provider's base_url in its own model\n# registry (docs/providers.md \"For each provider, pi knows all available\n# models\"), so 5dive does NOT need a custom base_url; injecting the right\n# *_API_KEY var is sufficient and pi resolves the endpoint itself. Qwen has no\n# standalone pi provider var, so it routes through OpenRouter. Provider ids\n# below match pi's `--provider` / auth.json-key column (docs/providers.md table)\n# so a `--model` pin resolves to the correct built-in provider. DIVE-1205.\ndeclare -A PI_PROVIDER_VAR=(\n [anthropic]=\"ANTHROPIC_API_KEY\"\n [openai]=\"OPENAI_API_KEY\"\n [google]=\"GEMINI_API_KEY\"\n [openrouter]=\"OPENROUTER_API_KEY\"\n [deepseek]=\"DEEPSEEK_API_KEY\"\n [moonshotai]=\"MOONSHOT_API_KEY\"\n [kimi-coding]=\"KIMI_API_KEY\"\n [zai]=\"ZAI_API_KEY\"\n [minimax]=\"MINIMAX_API_KEY\"\n)\n\n# BYO provider catalog for hermes/openclaw. The dashboard's new-agent\n# wizard collects a canonical id (lowercase, vendor-style) from the user;\n# this table maps it to the provider id each agent CLI's native registry\n# recognizes plus a sensible default model so the agent's first launch\n# doesn't sit at a \"model not configured\" prompt. Empty string in the\n# native column means the type's registry doesn't have that vendor — the\n# wizard hides that tile for that agent type.\n#\n# Native ids were verified empirically:\n# - hermes auth add <p> --type api-key --api-key <k> (writes ~/.hermes/auth.json,\n# auto-resolves base_url from the in-tree provider catalog).\n# - openclaw writes auth-profiles.json with type:\"api_key\" entries; provider\n# ids must match openclaw's built-in provider registry (anthropic, openai,\n# google, deepseek, moonshot, openrouter all present).\n#\n# hermes-moonshot is a special case: its registry has a Kimi provider but no\n# `hermes auth add moonshot` subcommand — the key is read from KIMI_API_KEY in\n# ~/.hermes/.env at gateway startup (see .env.example upstream). _apply_byo_hermes\n# branches on canonical==\"moonshot\" to take the env-var path instead of `auth add`,\n# and cmd_create copies the value into agent-<name>'s own .env before the gateway\n# is started. The HERMES_PROVIDER_ID value for moonshot (\"kimi\") is used as the\n# argument to `hermes config set model.provider`, not as an `auth add` id.\ndeclare -A HERMES_PROVIDER_ID=(\n [openai]=\"\"\n [anthropic]=\"anthropic\"\n [google]=\"gemini\"\n [deepseek]=\"deepseek\"\n [moonshot]=\"kimi\"\n [openrouter]=\"openrouter\"\n [nous]=\"nous\"\n [zai]=\"zai\"\n [minimax]=\"minimax\"\n [qwen]=\"alibaba\"\n [huggingface]=\"huggingface\"\n)\n# Explicit BYO base_url override for hermes, keyed by canonical id. hermes\n# normally auto-resolves model.base_url from its own provider catalog, but for\n# z.ai that catalog resolves an endpoint the GLM Coding-Plan key does NOT auth\n# against, so hermes+zai fails \"Provider authentication failed\" even with a\n# correct key (DIVE-1819). We pin the verified anthropic-wire endpoint z.ai\n# actually serves the coding models on (same one pi and the claude anthropic-skin\n# use — CLAUDE_PROVIDER_BASEURL[zai]). Only providers with a known-good override\n# are listed; everything else falls through to hermes' catalog resolution\n# (base_url left unset, see _apply_byo_hermes).\ndeclare -A HERMES_PROVIDER_URL=(\n [zai]=\"https://api.z.ai/api/anthropic\"\n)\ndeclare -A OPENCLAW_PROVIDER_ID=(\n [openai]=\"openai\"\n [anthropic]=\"anthropic\"\n [google]=\"google\"\n [deepseek]=\"deepseek\"\n [moonshot]=\"moonshot\"\n [openrouter]=\"openrouter\"\n [nous]=\"\"\n [zai]=\"zai\"\n [minimax]=\"minimax\"\n [qwen]=\"qwen\"\n [huggingface]=\"huggingface\"\n)\n# Explicit BYO provider base_url override for openclaw, keyed by canonical id.\n# openclaw's own provider catalog resolves each provider's endpoint, and for most\n# providers that's correct so no entry is listed here. z.ai is the exception\n# (DIVE-1826, the openclaw sibling of the DIVE-1819 hermes fix):\n#\n# 1. openclaw's zai provider speaks z.ai's OpenAI-compatible REST surface\n# (`/paas/v4`), NOT the anthropic-wire endpoint. So — unlike hermes and pi,\n# which pin `api.z.ai/api/anthropic` (HERMES_PROVIDER_URL / CLAUDE_PROVIDER_\n# BASEURL) — openclaw must be pointed at the openai-compat *coding* URL\n# `https://api.z.ai/api/coding/paas/v4`. Pinning the anthropic URL here would\n# break openclaw (wrong wire format). The two override tables are deliberately\n# NOT shared for this reason.\n# 2. z.ai exposes four endpoint families (zai-global / zai-cn / zai-coding-global\n# / zai-coding-cn). openclaw's `zai-api-key` auto-detect probes the GENERAL\n# endpoints (zai-global, zai-cn) BEFORE the Coding Plan ones, and our create\n# path writes a bare `{provider:zai}` auth profile that never runs that probe\n# — so a GLM Coding-Plan key (which authorizes the *coding* surface) lands on\n# the general endpoint and 401s \"authentication failed\" (what lodar hit).\n# Pinning models.providers.zai.baseUrl to the coding-global URL selects the\n# right surface deterministically. Coding-Plan CN users override per agent\n# (`5dive agent <name> tui`) — we default to the global coding endpoint.\ndeclare -A OPENCLAW_PROVIDER_URL=(\n [zai]=\"https://api.z.ai/api/coding/paas/v4\"\n)\n# Optional per-(type, canonical) default model. Missing entry => leave the\n# agent's own default selection logic alone. Conservative defaults: pick\n# the vendor's flagship general-purpose model that's likely to exist in\n# the in-tree catalog. When an entry turns out to be wrong (model id\n# renamed upstream), the user can override via `5dive agent <name> tui`\n# and the agent CLI's own model picker.\ndeclare -A HERMES_PROVIDER_MODEL=(\n [anthropic]=\"claude-sonnet-4-5\"\n [google]=\"gemini-2.0-flash\"\n [deepseek]=\"deepseek-v4-pro\"\n [moonshot]=\"kimi-k2-turbo-preview\"\n [openrouter]=\"openrouter/auto\"\n)\ndeclare -A OPENCLAW_PROVIDER_MODEL=(\n [openai]=\"openai/gpt-4o\"\n [anthropic]=\"anthropic/claude-sonnet-4-5\"\n [google]=\"google/gemini-2.0-flash\"\n [deepseek]=\"deepseek/deepseek-v4-pro\"\n [moonshot]=\"moonshot/kimi-k2-instruct\"\n [openrouter]=\"openrouter/auto\"\n)\ndeclare -A BYO_PROVIDER_LABEL=(\n [openai]=\"OpenAI\"\n [anthropic]=\"Anthropic\"\n [google]=\"Google AI\"\n [deepseek]=\"DeepSeek\"\n [moonshot]=\"Moonshot / Kimi\"\n [openrouter]=\"OpenRouter\"\n [nous]=\"Nous Portal\"\n [zai]=\"Z.ai / GLM\"\n [minimax]=\"MiniMax\"\n [qwen]=\"Alibaba / Qwen\"\n [huggingface]=\"Hugging Face\"\n)\nvalid_byo_provider() {\n [[ -n \"${BYO_PROVIDER_LABEL[$1]:-}\" ]]\n}\n\n# --- Claude (Claude Code) harness BYO custom-provider catalog -----------------\n# The claude harness can be pointed at any third-party provider that ships an\n# Anthropic Messages-API-compatible endpoint by overriding ANTHROPIC_BASE_URL +\n# ANTHROPIC_AUTH_TOKEN and the per-tier model ids (the modern Claude Code knobs\n# ANTHROPIC_DEFAULT_{OPUS,SONNET,HAIKU}_MODEL — so whichever tier the agent\n# selects, and the background haiku tasks, map to a model the provider actually\n# serves instead of 404-ing on \"claude-…\"). Listed endpoints are VERIFIED, not\n# necessarily vendor-DOCUMENTED — and the distinction is load-bearing, so do not\n# \"tidy\" it back. Verified means: the route answers (401 for absent/invalid\n# credentials, NOT 404 — a nonsense sibling path on the same host does return a\n# real url.not_found body, which is what makes the 401 mean something) and a\n# discriminating auth layer runs behind it (absent vs invalid credentials give\n# different messages AND different error types).\n# moonshot is REAL BUT UNDOCUMENTED, measured 2026-07-28 (DIVE-2246). Moonshot's\n# own docs describe an OpenAI-compatible API only — no `anthropic`, no `messages`,\n# no ANTHROPIC_BASE_URL anywhere — yet api.moonshot.ai/anthropic answers exactly as\n# above. A MISSING DOC PAGE IS NOT A MISSING ENDPOINT.\n# This comment previously said \"only providers with a DOCUMENTED anthropic-compat\n# endpoint are listed\", which the table below it violated. That is worse than no\n# comment: an auditor applying the stated rule finds moonshot backed by no vendor\n# doc and DELETES A WORKING PROVIDER. If you add an entry here, verify the route\n# yourself and say so — do not go looking for a doc page to justify it.\n# The rest of BYO_PROVIDER_LABEL is intentionally absent (no compat path at all →\n# would break the harness). Model\n# ids drift upstream — operators can override per agent via the model picker, or\n# we bump these. Values verified against vendor Claude-Code docs 2026-06-03.\n# OpenRouter (DIVE-1100): OpenRouter ships a NATIVE Anthropic-skin endpoint at\n# https://openrouter.ai/api (Claude Code appends /v1/messages), so the harness\n# talks to it directly — no translation proxy. The OpenRouter key rides\n# ANTHROPIC_AUTH_TOKEN (sk-or-…) and ANTHROPIC_API_KEY must be empty; both are\n# already handled by _apply_byo_claude. NOTE: OpenRouter's Anthropic endpoint\n# TRANSLATES — it accepts any OpenRouter model slug (openai/*, google/*, z-ai/*,\n# deepseek/*, meta-llama/*) in Anthropic wire format and converts it, verified\n# 2026-07-10 including a real headless Claude Code turn on z-ai/glm-4.6. The\n# \"openrouter/auto\" alias does NOT resolve here (it's an OpenAI-format router\n# convenience, not a real model), so we pin concrete per-tier defaults to\n# anthropic/* as a SAFE DEFAULT — operators override any tier via\n# `agent create --model=<slug>` or `agent config set model=<slug>` (DIVE-1103).\n# Slugs verified against the LIVE openrouter.ai /api/v1/models list 2026-07-25\n# (DIVE-1897). Verified present: anthropic/claude-opus-5, anthropic/claude-sonnet-5,\n# anthropic/claude-haiku-4.5, openrouter/auto. NOTE haiku is deliberately NOT bumped:\n# there is no claude-haiku-5 on OpenRouter, 4.5 is the current haiku. NOTE the opus\n# slot was the only stale entry — its sibling sonnet was already at 5, so one tier had\n# been bumped and the other had not.\ndeclare -A CLAUDE_PROVIDER_BASEURL=(\n [deepseek]=\"https://api.deepseek.com/anthropic\"\n [moonshot]=\"https://api.moonshot.ai/anthropic\"\n [openrouter]=\"https://openrouter.ai/api\"\n [zai]=\"https://api.z.ai/api/anthropic\"\n)\ndeclare -A CLAUDE_PROVIDER_OPUS_MODEL=(\n [deepseek]=\"deepseek-v4-pro\"\n [moonshot]=\"kimi-k2.5\"\n [openrouter]=\"anthropic/claude-opus-5\"\n [zai]=\"glm-5.2\"\n)\ndeclare -A CLAUDE_PROVIDER_SONNET_MODEL=(\n [deepseek]=\"deepseek-v4-pro\"\n [moonshot]=\"kimi-k2.5\"\n [openrouter]=\"anthropic/claude-sonnet-5\"\n [zai]=\"glm-5-turbo\"\n)\ndeclare -A CLAUDE_PROVIDER_HAIKU_MODEL=(\n [deepseek]=\"deepseek-v4-flash\"\n [moonshot]=\"kimi-k2.5\"\n [openrouter]=\"anthropic/claude-haiku-4.5\"\n [zai]=\"glm-4.5-air\"\n)\n\n# Resolve a canonical UI id to the agent CLI's native provider id. Empty\n# result means the type doesn't support that vendor and the caller should\n# fail with a clear error.\nresolve_native_provider() {\n local type=\"$1\" canonical=\"$2\"\n case \"$type\" in\n hermes) echo \"${HERMES_PROVIDER_ID[$canonical]:-}\" ;;\n openclaw) echo \"${OPENCLAW_PROVIDER_ID[$canonical]:-}\" ;;\n # claude maps a supported provider to itself (the env-var override path in\n # _apply_byo_claude keys off the canonical id, not a renamed native id).\n claude) [[ -n \"${CLAUDE_PROVIDER_BASEURL[$canonical]:-}\" ]] && echo \"$canonical\" ;;\n *) echo \"\" ;;\n esac\n}\n\n# Live auth probe: run \"<cli> <args>\" as user `claude` with a 5s wall-clock\n# cap and see if exit==0. Empty string disables the probe for that type\n# (fall back to sentinel-file presence). Args deliberately keep the prompt\n# short — we care about \"did the API accept our creds\", not the response.\ndeclare -A TYPE_PROBE=(\n [claude]='/home/claude/.local/bin/claude --print ping'\n # hermes/openclaw used to probe via `--print ping` against Anthropic; with the\n # OpenAI OAuth flow that argument shape no longer maps to a quick health check\n # we can rely on, so fall back to file-presence (auth_status_one returns \"ok\"\n # when no probe is configured and the credential file exists).\n [hermes]=''\n [openclaw]=''\n [codex]=''\n [opencode]=''\n # `agy --print ping` triggers a 30s OAuth wait when not authed and can't\n # tell stale-creds from rate-limit from a healthy box. File-presence is\n # the cheaper signal — fall through to TYPE_AUTH's sentinel.\n [antigravity]=''\n # `grok -p ping` would block on stdin via the inline UI; the `agent`\n # subcommand is meant for headless but takes longer to spin up than\n # we want for a 5s probe. Stick with file-presence.\n [grok]=''\n # devin -p spins up a full agent session — too slow for a 5s probe.\n # File-presence via the TYPE_AUTH sentinel.\n [devin]=''\n)\n"}}}} |