fix(harnesses): stage tokens for agent UID, fix three broken auth paths

Mirrors agent-runtimes audit + fix. All three M22 Phase 9 wrapper-script
auth paths were broken since the cutover: each one ran as the agent UID
trying to `cat` a 0400 root-only ESO mount. Same root cause we hit on
minimax/z-ai earlier today.

Changes:

1. anthropic-cloud-paul-oauth/v1 (NEW in framework)
   - Mirrors agent-runtimes — was previously only present there.
   - init.sh stages oauth_token into ~/.claude/.credentials.json (Claude
     Code's native subscription-OAuth schema). No env, no apiKeyHelper,
     no wrapper. Restores the equivalent of what harness_init.py used to
     do for the legacy SOPS path.
   - The legacy `bin/anthropic-wrapper.sh` was dead code (never wired).

2. gitea-https/v1
   - init.sh stages the token to $HOME/.config/git/gitea-https-token
     (0600 agent:agent) and points the per-host git credential helper at
     the staged copy. Previously the helper `cat`d the ESO mount path
     and silently failed at every git invocation.

3. gitea-admin/v1
   - init.sh stages the token to $HOME/.config/gitea-admin/token
     (0600 agent:agent). Wrapper updated to read from the staged copy.
   - Removes stale `requires: anthropic-cloud/v1` (the only anthropic
     harness in agent-runtimes is anthropic-cloud-paul-oauth/v1).

Pattern matches gitea-ssh / minimax / z-ai: ESO mount stays root-only,
init.sh runs as root and `install -m 0600 -o agent -g agent`s a single
explicit copy. Per-secret enumeration; future ESO Secret keys remain
inaccessible by default.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Paul O'Reilly
2026-05-07 21:05:33 +12:00
parent 12c2172835
commit f8c8805f26
6 changed files with 221 additions and 39 deletions

View File

@@ -1,22 +1,25 @@
#!/bin/bash
# gitea-admin-wrapper.sh — reads the ESO-mounted gitea-admin token at exec time
# and prefixes it as a transient env var to the underlying tool.
# gitea-admin-wrapper.sh — reads the agent-staged gitea-admin token at exec
# time and prefixes it as a transient env var to the underlying tool.
#
# M22 Phase 9: token mounted at /run/agent/secrets/gitea-admin/token (read-only,
# tmpfs, mode 0400). The token NEVER enters the wrapper's parent shell — only
# the exec'd tool's environment via `exec env VAR=value cmd`. The brief
# presence in /proc/<tool_pid>/environ of the tool process is the accepted
# floor (per spec/secrets-runtime.md SR-DISP-7 / H-SECRET-4).
# M22 Phase 9 staging pattern: init.sh runs as root and `install`s a 0600
# agent-owned copy of the ESO-mounted token into $HOME/.config/gitea-admin/
# token. This wrapper reads the staged copy, not the ESO mount path, so
# the `cat` succeeds under the agent UID. The brief presence of the token
# in /proc/<tool_pid>/environ of the exec'd tool is the accepted floor
# (per H-SECRET-4 / SR-DISP-7); the wrapper's parent shell never sees it.
set -euo pipefail
TOKEN_FILE="/run/agent/secrets/gitea-admin/token"
TOKEN_FILE="$HOME/.config/gitea-admin/token"
# git-credential-helper subcommand: read token and emit git credential format.
# Git invokes us as `gitea-admin-wrapper.sh git-credential-helper get` and
# expects key=value lines on stdout terminated by a blank line.
git_credential_helper() {
if [[ ! -r "$TOKEN_FILE" ]]; then
echo "gitea-admin-wrapper: token file not readable at $TOKEN_FILE" >&2
echo "gitea-admin-wrapper: staged token not readable at $TOKEN_FILE" >&2
echo "gitea-admin-wrapper: did init.sh run? (it stages the token from the ESO mount)" >&2
exit 1
fi
local base_url="${GITEA_BASE_URL:-https://gitea.oreillyit.nz}"
@@ -33,7 +36,8 @@ case "${1:-}" in
;;
*)
if [[ ! -r "$TOKEN_FILE" ]]; then
echo "gitea-admin-wrapper: token file not readable at $TOKEN_FILE" >&2
echo "gitea-admin-wrapper: staged token not readable at $TOKEN_FILE" >&2
echo "gitea-admin-wrapper: did init.sh run? (it stages the token from the ESO mount)" >&2
exit 1
fi
# `exec env VAR=...` keeps the secret out of the wrapper's parent shell

View File

@@ -2,8 +2,7 @@ kind: context
name: gitea-admin
version: 1
description: "Gitea admin: SSH, git identity, API token for skynet org"
requires:
- anthropic-cloud/v1
requires: []
provides: [gitea-admin]
git_identity:

View File

@@ -1,19 +1,60 @@
#!/bin/bash
# init.sh for gitea-admin harness (M22 Phase 9)
# Sets up non-secret env vars and configures git credential helper.
# Credential token is NOT exported here — only read at exec time by the wrapper.
# gitea-admin init — stage the API token for the agent user and configure
# the git credential helper.
#
# Threat model: keep the ESO mount root-only (mode 0400). init.sh runs as
# root and `install`s a per-secret 0600 agent-owned copy at
# $AGENT_HOME/.config/gitea-admin/token. The wrapper at
# bin/gitea-admin-wrapper.sh reads the staged copy (not the ESO mount) at
# exec time. Without staging, every wrapper invocation would fail at the
# `cat` step because the wrapper runs under the agent UID and the ESO file
# is root:root mode 0400.
#
# This was previously broken: the wrapper `cat`d the ESO mount path which
# the agent could not read. Discovered during the M22 Phase 9 audit; the
# wrapper-script approach was never re-validated post-cutover. Stage-and-
# wrapper-points-at-stage matches gitea-ssh / gitea-https / minimax / z-ai.
#
# Rotation handling: per-container init. Long-running sessions need a
# future scripts.control_loop hook to re-stage between operations.
set -euo pipefail
# Non-secret configuration
export GITEA_BASE_URL="${GITEA_BASE_URL:-https://gitea.oreillyit.nz}"
ESO_TOKEN="/run/agent/secrets/gitea-admin/token"
# Ensure SSH directory exists with correct permissions
mkdir -p /home/agent/.ssh
chmod 700 /home/agent/.ssh
if [ ! -r "$ESO_TOKEN" ]; then
echo "ERROR: $ESO_TOKEN not readable. Check ESO ExternalSecret acct-<gitea-admin-id>." >&2
exit 1
fi
# Configure git to use the gitea-admin credential helper wrapper
# The wrapper reads the ESO-mounted token at exec time
git config --global credential.helper "!/opt/harness/contexts/gitea-admin/v1/bin/gitea-admin-wrapper.sh git-credential-helper"
AGENT_USER="${AGENT_USER:-agent}"
AGENT_HOME=$(getent passwd "$AGENT_USER" | cut -d: -f6)
if [ -z "$AGENT_HOME" ] || [ ! -d "$AGENT_HOME" ]; then
AGENT_HOME="/home/$AGENT_USER"
fi
echo "[gitea-admin] init complete"
# Stage the token into a per-secret path owned by agent, mode 0600.
# Wrapper reads from this path; ESO mount is never accessed at runtime by
# the agent UID.
STAGED_DIR="$AGENT_HOME/.config/gitea-admin"
STAGED_TOKEN="$STAGED_DIR/token"
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_TOKEN" "$STAGED_TOKEN"
# Ensure SSH directory exists with correct permissions (legacy SSH path).
mkdir -p "$AGENT_HOME/.ssh"
chown "$AGENT_USER:" "$AGENT_HOME/.ssh" 2>/dev/null || true
chmod 0700 "$AGENT_HOME/.ssh"
# Non-secret configuration: GITEA_BASE_URL is set in harness env.
# Configure git to use the gitea-admin credential helper wrapper. Run as
# the agent user so ~/.gitconfig is owned correctly; otherwise root would
# write into /root/.gitconfig and the agent's git wouldn't see the helper.
WRAPPER_PATH="/opt/harness/contexts/gitea-admin/v1/bin/gitea-admin-wrapper.sh"
su - "$AGENT_USER" -c "git config --global credential.helper '!$WRAPPER_PATH git-credential-helper'"
echo "gitea-admin: token staged at $STAGED_TOKEN (0600 $AGENT_USER:$AGENT_USER)"
echo "gitea-admin: git credential helper configured"