fix(auth): mosaic-managed auth lives under the data root, never ~/.pi (#48)

Owner direction: the stack must never impact default harness usage.
Correction to M19 as shipped (nothing had been created in ~/.pi — the
move breaks nothing).

- Mosaic-managed accounts: <dataRoot>/auth/<account>.json, perms 0600
  enforced (loose perms flagged in listings, refused by --auth — mirrors
  gitea-api.sh credential hygiene).
- ~/.pi is read-only to the stack, permanently; the only interaction
  remains the existing read-only container mount of the default
  credential. Recorded as a ROADMAP standing decision.
- auth.sh is now config-driven (data root from config.json, fail closed,
  consistent with every other tool); status reports both sources labeled.
- agent.sh --auth resolution moved after load_config (needs the data
  root); missing/symlinked/non-0600 accounts refuse.
- test-auth.sh: 15 no-Docker cases (accounts-create-nothing, loose-perms
  refusal, invalid-config refusal added). Test-authoring correction
  recorded in BUILD-LOG (fixture-state mismatch caught before running).

Suites 24/15/90/14/17 + verify green.
This commit is contained in:
2026-09-03 22:53:33 -05:00
parent 073bbfdb6a
commit 975084abe2
9 changed files with 203 additions and 99 deletions
+17 -14
View File
@@ -41,25 +41,28 @@ done
[ -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
ensure_release_aligned
# Named auth account (M19, corrected per owner in #48): per-launch
# credential injection from MOSAIC-MANAGED storage under the data root —
# never from inside ~/.pi, which is read-only to the stack. Resolves
# <dataRoot>/auth/<account>.json and exports PI_AUTH_FILE (the compose
# read-only mount source). Missing, symlinked, or non-0600 files refuse:
# silently falling back to another 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_RESOLVED="$MOSAIC_DEV_DIR/auth/$AUTH_ACCOUNT.json"
[ -r "$AUTH_RESOLVED" ] && [ ! -L "$AUTH_RESOLVED" ] || { echo "agent: no mosaic-managed credential file for auth account '$AUTH_ACCOUNT': $AUTH_RESOLVED (see scripts/auth.sh accounts)" >&2; exit 4; }
AUTH_PERMS="$(stat -c %a "$AUTH_RESOLVED")"
[ "$AUTH_PERMS" = "600" ] || { echo "agent: auth account '$AUTH_ACCOUNT' file must be 0600 (got $AUTH_PERMS): $AUTH_RESOLVED" >&2; exit 4; }
export PI_AUTH_FILE="$AUTH_RESOLVED"
echo "agent: auth: account '$AUTH_ACCOUNT' -> $AUTH_RESOLVED" >&2
fi
# Onboarding gate (M16): a TUI agent cannot launch without a user profile.
# The onboarding wizard runs automatically here - the TTY is already yours.
if [ ! -f "$MOSAIC_DEV_DIR/user/USER.md" ]; then