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>
83 lines
3.3 KiB
Bash
Executable File
83 lines
3.3 KiB
Bash
Executable File
#!/bin/bash
|
|
# anthropic-cloud-paul-oauth init — write Claude Code's native credentials
|
|
# file from the ESO-mounted oauth_token.
|
|
#
|
|
# Threat model: keep the ESO mount root-only (mode 0400) so the agent user
|
|
# cannot directly `cat` /run/agent/secrets/anthropic-cloud-paul-oauth/*.
|
|
# init.sh runs as root (in uid-wrapper.sh, before the gosu drop) and writes
|
|
# a per-secret artefact at $AGENT_HOME/.claude/.credentials.json with mode
|
|
# 0600 owned by agent. Claude Code reads that file natively for OAuth-based
|
|
# subscription auth — no env vars, no apiKeyHelper, no wrapper script.
|
|
#
|
|
# Why .credentials.json (not apiKeyHelper):
|
|
# - This is the SUBSCRIPTION (Pro/Max) path: ENFORCE_SUBSCRIPTION_PRICING=true
|
|
# on this harness causes the runner to strip ANTHROPIC_API_KEY and
|
|
# ANTHROPIC_AUTH_TOKEN from the subprocess env so an accidental API key
|
|
# can't fall through to per-token billing. Subscription auth flows
|
|
# through CLAUDE_CODE_OAUTH_TOKEN — Claude Code's native storage for
|
|
# that is .credentials.json with the claudeAiOauth shape.
|
|
# - apiKeyHelper would still work, but routes through the api_key path,
|
|
# which the runner's subscription-pricing enforcement is specifically
|
|
# designed to block. .credentials.json is the canonical OAuth path.
|
|
#
|
|
# This mirrors what entrypoint/harness_init.py used to do for the legacy
|
|
# SOPS-decrypted CLAUDE_CODE_OAUTH_TOKEN path. Phase 9 ESO-cutover removed
|
|
# the SOPS files; this init.sh restores the equivalent behaviour for the
|
|
# ESO-mounted token.
|
|
#
|
|
# Rotation handling: per-container init. Ephemeral container agents (one
|
|
# task = one container) always pick up the latest mounted oauth_token.
|
|
# Long-running sessions need a future scripts.control_loop hook to
|
|
# re-stage between agent CLI invocations.
|
|
|
|
set -euo pipefail
|
|
|
|
ESO_OAUTH="/run/agent/secrets/anthropic-cloud-paul-oauth/oauth_token"
|
|
|
|
if [ ! -r "$ESO_OAUTH" ]; then
|
|
echo "ERROR: $ESO_OAUTH not readable. Check ESO ExternalSecret acct-<anthropic-cloud-paul-oauth-id>." >&2
|
|
exit 1
|
|
fi
|
|
|
|
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
|
|
|
|
CONFIG_DIR="${CLAUDE_CONFIG_DIR:-$AGENT_HOME/.claude}"
|
|
mkdir -p "$CONFIG_DIR"
|
|
chown "$AGENT_USER:" "$CONFIG_DIR" 2>/dev/null || true
|
|
chmod 0700 "$CONFIG_DIR"
|
|
|
|
# Build the .credentials.json file. The token is piped via stdin so it
|
|
# never lands in argv (visible in /proc/<pid>/cmdline) or env. The schema
|
|
# matches what `claude setup-token` produces locally and what the legacy
|
|
# SOPS path in harness_init.py wrote.
|
|
CRED_FILE="$CONFIG_DIR/.credentials.json"
|
|
cat "$ESO_OAUTH" | python3 -c "
|
|
import json, sys
|
|
token = sys.stdin.read().strip()
|
|
print(json.dumps({
|
|
'claudeAiOauth': {
|
|
'accessToken': token,
|
|
'refreshToken': None,
|
|
'expiresAt': 4102444800000,
|
|
'scopes': [
|
|
'user:file_upload',
|
|
'user:inference',
|
|
'user:mcp_servers',
|
|
'user:profile',
|
|
'user:sessions:claude_code',
|
|
],
|
|
'subscriptionType': 'max',
|
|
'rateLimitTier': 'default_claude_max_5x',
|
|
}
|
|
}))
|
|
" > "$CRED_FILE"
|
|
|
|
chown "$AGENT_USER:" "$CRED_FILE" 2>/dev/null || true
|
|
chmod 0600 "$CRED_FILE"
|
|
|
|
echo "anthropic-cloud-paul-oauth: oauth_token staged into $CRED_FILE (0600 $AGENT_USER:$AGENT_USER)"
|