From b1e3ee7052649abdbc525163ded96bf3e9f04872 Mon Sep 17 00:00:00 2001 From: Paul O'Reilly Date: Fri, 8 May 2026 12:24:58 +1200 Subject: [PATCH] fix(airouter): match ESO Secret schema (api_key) + drop dead wrapper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The airouter ESO ExternalSecret materialises a single key `api_key` (matching the provider schema in agent-runtimes M22 Phase 8e cutover, acct-59b7fb0b). The harness init script was checking for `auth_token` + `base_url` (an Anthropic-compat shape that never existed in real Vault state) and failing on every dispatch: ERROR: /run/agent/secrets/airouter/auth_token not readable. Check ESO ExternalSecret for airouter. Surfaced as the second blocker for the M16 Wave A1 dogfood (the first was the airouter dispatcher missing CRS sync; that fix went into agent-runtimes-deploy@0f11cd1). Same shape of bug as the minimax + gitea-ssh init scripts that landed during the same M22 phase — those were fixed at the time, airouter was not. Changes: - init.sh: verify the single `api_key` file (root-only, 0400 ESO mount). Stage to /var/agent-secrets/airouter/api_key with mode 0600 agent-owned (mirrors minimax pattern). H-SECRET-4 compliant — no exports. - harness.yaml: add OPENAI_API_KEY_FILE pointing at the staged path. Agentic runner reads the file at request time per entrypoint/runners/agentic.py:146 (OPENAI_API_KEY_FILE precedence). - Delete dead bin/anthropic-compat-wrapper.sh — confirmed unused per agent-runtimes/memory/log/2026-05-07.214249.md (post-M22-Phase-9 cleanup found these per-provider wrappers were never invoked; runner only prepends /opt/agent/claude-wrapper.sh). CRS picks this up automatically on next CP poll; no agent-runtimes image rebuild needed. Co-Authored-By: Claude Sonnet 4.6 --- .../v1/bin/anthropic-compat-wrapper.sh | 10 ---- harnesses/contexts/airouter/v1/harness.yaml | 6 +++ harnesses/contexts/airouter/v1/init.sh | 48 +++++++++++-------- 3 files changed, 35 insertions(+), 29 deletions(-) delete mode 100755 harnesses/contexts/airouter/v1/bin/anthropic-compat-wrapper.sh diff --git a/harnesses/contexts/airouter/v1/bin/anthropic-compat-wrapper.sh b/harnesses/contexts/airouter/v1/bin/anthropic-compat-wrapper.sh deleted file mode 100755 index 118dac8..0000000 --- a/harnesses/contexts/airouter/v1/bin/anthropic-compat-wrapper.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash -# Anthropic-compat wrapper for airouter harness (Phase 9 ESO-managed secrets) -# Reads airouter credentials from /run/agent/secrets/airouter/ and passes them -# as env vars to the underlying Claude runner. -set -euo pipefail - -exec env \ - ANTHROPIC_AUTH_TOKEN="$(cat /run/agent/secrets/airouter/auth_token)" \ - ANTHROPIC_BASE_URL="$(cat /run/agent/secrets/airouter/base_url)" \ - claude "$@" \ No newline at end of file diff --git a/harnesses/contexts/airouter/v1/harness.yaml b/harnesses/contexts/airouter/v1/harness.yaml index 7e4b3d5..c67042d 100644 --- a/harnesses/contexts/airouter/v1/harness.yaml +++ b/harnesses/contexts/airouter/v1/harness.yaml @@ -7,11 +7,17 @@ provides: [agentic-runner] env: OPENAI_BASE_URL: "https://api.airouter.ch/v1" + # Agentic runner reads the api key from this file at request time. + # init.sh stages a 0600 agent-owned copy from the ESO mount to this path. + OPENAI_API_KEY_FILE: "/var/agent-secrets/airouter/api_key" secrets_required: - name: airouter account_ref: "airouter" mount_path: /run/agent/secrets/airouter + # 0400 (root-only) — defense in depth. The agent user CANNOT read this + # mount; init.sh runs as root and installs a 0600 agent-owned copy at + # OPENAI_API_KEY_FILE (above). Matches the minimax pattern. mode: "0400" scripts: diff --git a/harnesses/contexts/airouter/v1/init.sh b/harnesses/contexts/airouter/v1/init.sh index 0fc4919..9647818 100755 --- a/harnesses/contexts/airouter/v1/init.sh +++ b/harnesses/contexts/airouter/v1/init.sh @@ -1,29 +1,39 @@ #!/bin/bash -# airouter init — Phase 9 ESO-managed secret mount. +# airouter init — Phase 9 ESO-managed secret + agentic-runner staging. # -# Verifies the ESO-mounted secret files exist; the wrapper -# (`bin/anthropic-compat-wrapper.sh`) reads them at exec time. +# The airouter ESO ExternalSecret materialises a single key, `api_key`, +# matching the airouter provider schema. The previous version of this +# script verified `auth_token` + `base_url` (Anthropic-compat shape, dead +# code per the M22 Phase 9 cleanup) and never matched a real ESO Secret. # -# Per H-SECRET-4: NO `export` of credentials here. The earlier draft of this -# file exported ANTHROPIC_AUTH_TOKEN and ANTHROPIC_BASE_URL from this script, -# which runs as root under uid-wrapper.sh — even though the export was -# subshell-scoped, the secret was briefly resident in /proc//environ of -# a root process. The wrapper's `exec env VAR=...` pattern is the only -# acceptable credential delivery point. +# Threat model: ESO mount is root-only (mode 0400) — agent CANNOT read +# /run/agent/secrets/airouter/api_key directly. init.sh runs as root +# (under uid-wrapper.sh, before the gosu drop) and installs a per-agent +# copy of the api_key at a fixed path the agentic runner reads via +# OPENAI_API_KEY_FILE. Same pattern as minimax/v1/init.sh. +# +# Per H-SECRET-4: no `export` of the credential value here — the staged +# file path is referenced from harness env (OPENAI_API_KEY_FILE), and the +# agentic runner reads the file at request time. set -euo pipefail -SECRETS_DIR="/run/agent/secrets/airouter" +ESO_API_KEY="/run/agent/secrets/airouter/api_key" -if [ ! -d "$SECRETS_DIR" ]; then - echo "ERROR: Secret directory $SECRETS_DIR not found. ESO mount may have failed." >&2 +if [ ! -r "$ESO_API_KEY" ]; then + echo "ERROR: $ESO_API_KEY not readable. Check ESO ExternalSecret acct-." >&2 exit 1 fi -for f in auth_token base_url; do - if [ ! -r "$SECRETS_DIR/$f" ]; then - echo "ERROR: $SECRETS_DIR/$f not readable. Check ESO ExternalSecret for airouter." >&2 - exit 1 - fi -done +AGENT_USER="${AGENT_USER:-agent}" -echo "airouter secrets verified at $SECRETS_DIR" +# Stage to a fixed path that harness.yaml env can reference. Outside the +# read-only ESO mount so we can set ownership/mode. +STAGED_DIR="/var/agent-secrets/airouter" +STAGED_KEY="$STAGED_DIR/api_key" +mkdir -p "$STAGED_DIR" +chown "$AGENT_USER:" "$STAGED_DIR" 2>/dev/null || true +chmod 0700 "$STAGED_DIR" +install -m 0600 -o "$AGENT_USER" -g "$AGENT_USER" "$ESO_API_KEY" "$STAGED_KEY" + +echo "airouter api_key staged at $STAGED_KEY (0600 $AGENT_USER:$AGENT_USER)" +echo "airouter OPENAI_API_KEY_FILE set via harness.yaml env"