feat(auth): M19 harness auth tooling — auth.sh checkpoint + per-launch account injection (#47)

Investigation (pi 0.84.4 docs + host auth.json metadata, values never
read): provider stacking is native (one auth.json keyed by provider;
resolution --api-key > auth.json > env > models.json; OAuth auto-refresh).
Multi-account per provider is NOT native -> named-file design:
auth.<account>.json + per-launch injection.

- scripts/auth.sh: status (provider names, credential types, perms,
  env-side names informational — never credential material) and accounts
  (named files, active marker). Exit codes per convention: 3 missing for
  a read, 2 unparseable, 4 file/environment (symlinks refuse).
- scripts/agent.sh --auth <account>: resolves auth.<account>.json and
  exports PI_AUTH_FILE (the existing compose read-only mount source — no
  new plumbing); missing/invalid account refuses pre-container.
- scripts/test-auth.sh: 13 no-Docker cases; core assertion is the safety
  property itself — fixture key/token/env VALUES never reach output.
- Docs: TOOLS.md Auth section, AGENTS.md command surface + suites.

Headless task runs keep the default credential (worker auth selection is
a separate policy decision). Real-host smoke: anthropic/openai-codex
oauth + zai api_key reported, perms 600, no named accounts yet.

Suites 24/90/14/17/13 + verify green. Agreed sequence M16-M19 complete;
M20 owner-gated.
This commit is contained in:
2026-09-03 19:58:50 -05:00
parent d1d7b5598d
commit 073bbfdb6a
8 changed files with 259 additions and 6 deletions
+18 -2
View File
@@ -3,7 +3,7 @@
#
# Usage:
# scripts/agent.sh <name> [--mission <file>] [--workspace <ws>]
# [--session <name>] [--tools <comma,list>]
# [--session <name>] [--tools <comma,list>] [--auth <account>]
#
# The agent receives the four immutable contracts (constitution, standards,
# SOUL, USER) plus its own identity and optional mission directives as its
@@ -23,6 +23,7 @@ WORKSPACE=""
SESSION=""
TOOLS=""
SKILLS=""
AUTH_ACCOUNT=""
while [ $# -gt 0 ]; do
case "$1" in
@@ -30,15 +31,30 @@ while [ $# -gt 0 ]; do
--workspace) WORKSPACE="${2:?}"; shift 2 ;;
--session) SESSION="${2:?}"; shift 2 ;;
--tools) TOOLS="${2:?}"; shift 2 ;;
--auth) AUTH_ACCOUNT="${2:?}"; shift 2 ;;
--skills) SKILLS="${2:?}"; shift 2 ;;
--help|-h) sed -n '2,12p' "$0"; exit 0 ;;
*) NAME="$1"; shift ;;
esac
done
[ -n "$NAME" ] || { echo "agent: usage: scripts/agent.sh <name> [--mission f] [--workspace ws] [--session s] [--tools list]" >&2; exit 4; }
[ -n "$NAME" ] || { echo "agent: usage: scripts/agent.sh <name> [--mission f] [--workspace ws] [--session s] [--tools list] [--auth account]" >&2; exit 4; }
case "$NAME" in *[!A-Za-z0-9._-]*|'') echo "agent: invalid agent name" >&2; exit 4;; esac
# Named auth account (M19): per-launch credential injection. Resolves
# auth.<account>.json beside the active credential file and exports
# PI_AUTH_FILE (the compose read-only mount source). A missing or invalid
# account refuses - silently falling back to the default credential would
# put one account's work on another's identity.
if [ -n "$AUTH_ACCOUNT" ]; then
case "$AUTH_ACCOUNT" in *[!A-Za-z0-9._-]*|'') echo "agent: invalid auth account name" >&2; exit 4;; esac
AUTH_DIR="$(dirname "${PI_AUTH_FILE:-$HOME/.pi/agent/auth.json}")"
AUTH_RESOLVED="$AUTH_DIR/auth.$AUTH_ACCOUNT.json"
[ -r "$AUTH_RESOLVED" ] && [ ! -L "$AUTH_RESOLVED" ] || { echo "agent: no credential file for auth account '$AUTH_ACCOUNT': $AUTH_RESOLVED" >&2; exit 4; }
export PI_AUTH_FILE="$AUTH_RESOLVED"
echo "agent: auth: account '$AUTH_ACCOUNT' -> $AUTH_RESOLVED" >&2
fi
load_config
load_release
bootstrap_runtime_dir