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:
@@ -553,3 +553,31 @@ Auth is checkpointable without exposing credentials, and multi-account has
|
||||
a per-launch path. Suites 24/90/14/17/13 + verify green. The agreed
|
||||
ROADMAP sequence M16–M19 is complete; M20 (packages/* restructure +
|
||||
unified CLI) is owner-gated.
|
||||
|
||||
## Phase 24 follow-up: auth ownership corrected — data root, never ~/.pi (#48)
|
||||
|
||||
- Owner correction after M19 review: mosaic-managed named accounts must
|
||||
not live inside ~/.pi — the stack must never impact default harness
|
||||
usage. Recorded as a ROADMAP standing decision: the stack never writes
|
||||
to default harness config locations; ~/.pi is read-only to the stack.
|
||||
- Correction noted honestly: M19 as shipped placed accounts beside
|
||||
~/.pi/agent/auth.json. Nothing had been created there (accounts reported
|
||||
none), so the move breaks nothing.
|
||||
- auth.sh is now config-driven (data root from config.json, fail closed —
|
||||
consistent with every other tool); status reports both sources labeled:
|
||||
default harness credential (read-only to the stack) + mosaic-managed
|
||||
accounts.
|
||||
- Accounts live at <dataRoot>/auth/<account>.json, perms 0600 enforced:
|
||||
loose perms flagged in listings and refused by agent.sh --auth
|
||||
(mirrors scripts/gitea-api.sh credential hygiene).
|
||||
- Test-authoring correction: the first suite rewrite asserted "(none)" in
|
||||
cases whose fixtures had already created accounts; caught on review
|
||||
before any run, assertions rewritten to match fixture state.
|
||||
- auth.sh suite: 13 → 15 cases (accounts-create-nothing, loose-perms
|
||||
refusal, invalid-config refusal added).
|
||||
|
||||
## Result
|
||||
|
||||
Suites 24/15/90/14/17 + verify green. Default harness usage is untouched
|
||||
by design; all mosaic-managed credentials live inside the governed data
|
||||
root.
|
||||
|
||||
@@ -210,6 +210,8 @@ Pi's documented container authentication (see the package's
|
||||
passes them through. Pi's documented precedence applies.
|
||||
|
||||
Credentials are never committed, never copied into the image, and never printed.
|
||||
Mosaic-managed named accounts (`agent.sh --auth`) live under the data root
|
||||
(`auth/<account>.json`, 0600) — the stack never writes into `~/.pi`.
|
||||
`.env.example` contains non-secret settings only.
|
||||
|
||||
## Boundaries honored
|
||||
|
||||
@@ -14,3 +14,4 @@ are never rewritten or removed; corrections are new entries.
|
||||
| 2026-09-03 | assistant (conductor) | M18 seat-role progressive capability restriction (#45): roles/<role>.json contracts (strict schema, name-filename binding, network declared), mosaic-task.mjs resolve-role, agent.sh ceiling intersection with fail-closed refusals, roles/researcher.json shipped, 14 suite cases (task 74 → 88) | scripts/mosaic-task.mjs, scripts/agent.sh, scripts/test-task.sh, roles/researcher.json, docs; suites 24/88/14/17 + verify green |
|
||||
| 2026-09-03 | owner (decision + live verification) + assistant (conductor) | M18 live verification + follow-up (#46): owner confirmed narrowing/refusal/tool-free live; seatless launch under AGENTS_DIR override discovered and made fail-closed (exit 4); task suite 88 → 90 | scripts/agent.sh, scripts/test-task.sh, docs/TOOLS.md; suites 24/90/14/17 + verify green |
|
||||
| 2026-09-03 | assistant (conductor) | M19 harness auth tooling (#47): pi auth investigation (native provider stacking, no native multi-account), scripts/auth.sh status/accounts (never prints credential material), agent.sh --auth per-launch injection via PI_AUTH_FILE, test-auth.sh suite (13 cases incl. secret-never-printed assertions) | scripts/auth.sh, scripts/agent.sh, scripts/test-auth.sh, docs/TOOLS.md, AGENTS.md; suites 24/90/14/17/13 + verify green |
|
||||
| 2026-09-03 | owner (direction) + assistant (conductor) | M19 correction (#48): mosaic-managed auth moved from ~/.pi to the data root (auth/<account>.json, 0600 enforced); ~/.pi read-only to the stack as a ROADMAP standing decision; auth.sh config-driven; test-auth 15 cases | scripts/auth.sh, scripts/agent.sh, scripts/test-auth.sh, docs/TOOLS.md, docs/plans/ROADMAP.md, README.md; suites 24/15/90/14/17 + verify green |
|
||||
|
||||
+12
-5
@@ -65,14 +65,21 @@ provider; resolution order `--api-key` → `auth.json` → env → models.json).
|
||||
No credential material is ever printed — provider names, credential types,
|
||||
and env var NAMES only.
|
||||
|
||||
Ownership rule (#48): `~/.pi` is read-only to the stack, permanently. The
|
||||
only interaction is the existing read-only container mount of the default
|
||||
credential (`PI_AUTH_FILE`, default `~/.pi/agent/auth.json`). Mosaic-managed
|
||||
accounts live under the data root: `<dataRoot>/auth/<account>.json`, perms
|
||||
0600 (mirroring `scripts/gitea-api.sh` hygiene — loose perms are flagged in
|
||||
listings and refused by `--auth`).
|
||||
|
||||
| Command | Purpose | Notes |
|
||||
|---|---|---|
|
||||
| `scripts/auth.sh status` | Per-provider credential report | Types (`oauth`/`api_key`), perms, env-side names (informational); exit 3 when the file is missing |
|
||||
| `scripts/auth.sh accounts` | List named account files | `auth.<account>.json` beside the credential file; marks the active one |
|
||||
| `scripts/auth.sh status` | Report both credential sources | Default harness credential (read-only) + mosaic-managed accounts; never prints material |
|
||||
| `scripts/auth.sh accounts` | List mosaic-managed accounts | Under the data root; marks the active one; flags non-0600 |
|
||||
|
||||
The file is the compose read-only mount source (`PI_AUTH_FILE`, default
|
||||
`~/.pi/agent/auth.json`); named accounts ride the same mount per launch via
|
||||
`agent.sh --auth`. Headless task runs keep the default credential.
|
||||
`agent.sh --auth <account>` injects `<dataRoot>/auth/<account>.json` as the
|
||||
launch's `PI_AUTH_FILE`; missing/symlinked/non-0600 accounts refuse.
|
||||
Headless task runs keep the default credential.
|
||||
|
||||
## Release
|
||||
|
||||
|
||||
@@ -53,3 +53,4 @@ git history + Gitea issues.
|
||||
- 2026-09-03 — M18 seat-role progressive capability restriction (#45; roles resolve to contracts, ceiling ∩ seat grant, fail-closed refusals, roles/researcher.json); task suite 74 → 88
|
||||
- 2026-09-03 — M18 follow-up: fail-closed seat resolution under MOSAIC_AGENTS_DIR override (#46, owner decision after live verification); task suite 88 → 90; next action M19
|
||||
- 2026-09-03 — M19 harness auth tooling (#47; auth.sh status/accounts, agent.sh --auth per-launch injection via PI_AUTH_FILE, test-auth suite 13 cases with secret-never-printed assertions); agreed sequence M16–M19 complete, M20 owner-gated
|
||||
- 2026-09-03 — M19 correction: auth ownership moved to the data root (#48, owner direction — the stack never writes to default harness config locations; ROADMAP standing decision); auth.sh config-driven, accounts at <dataRoot>/auth, 0600 enforced; test-auth 13 → 15
|
||||
|
||||
@@ -17,6 +17,10 @@ and BUILD-LOG phase.
|
||||
referenced from AGENTS.md.
|
||||
- **Capability restriction becomes progressive and role-based** (M18),
|
||||
porting prior Mosaic Stack concepts where they fit the file-based runtime.
|
||||
- **The stack never writes to default harness config locations** (owner,
|
||||
2026-09-03, #48): `~/.pi` — and any future harness home — is read-only to
|
||||
Mosaic tooling; the only interaction is the existing read-only credential
|
||||
mount. Mosaic-managed auth lives under the data root (`<dataRoot>/auth/`).
|
||||
|
||||
## M16 — Release self-determination
|
||||
|
||||
|
||||
+17
-14
@@ -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
|
||||
|
||||
+72
-42
@@ -1,36 +1,42 @@
|
||||
#!/usr/bin/env bash
|
||||
# auth.sh — pi credential checkpoint (M19): per-provider auth report and
|
||||
# named-account listing. Never prints credential material: provider names,
|
||||
# credential types, permission bits, and env var NAMES only.
|
||||
# auth.sh — pi credential checkpoint (M19, corrected per owner in #48):
|
||||
# per-provider auth report and mosaic-managed account listing. Never prints
|
||||
# credential material: provider names, credential types, permission bits,
|
||||
# and env var NAMES only.
|
||||
#
|
||||
# Ownership rule (#48): ~/.pi is READ-ONLY to the stack — the only
|
||||
# interaction is the existing read-only container mount of the default
|
||||
# credential. Mosaic-managed accounts live under the data root:
|
||||
# <dataRoot>/auth/<account>.json
|
||||
#
|
||||
# Usage:
|
||||
# scripts/auth.sh status per-provider report (auth.json + env-side names)
|
||||
# scripts/auth.sh accounts list named account files (auth.<account>.json)
|
||||
#
|
||||
# The credential file is the compose read-only mount source (PI_AUTH_FILE,
|
||||
# default ~/.pi/agent/auth.json). Resolution follows pi: auth.json entries
|
||||
# take priority over environment variables.
|
||||
# scripts/auth.sh status report default harness credential + accounts
|
||||
# scripts/auth.sh accounts list mosaic-managed accounts (data root)
|
||||
#
|
||||
# Exit codes: 0 report produced · 2 credential file unparseable ·
|
||||
# 3 file/dir missing for a read · 4 file/environment problem.
|
||||
# 3 default credential file missing for a read · 4 file/environment problem.
|
||||
set -uo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
# shellcheck source=common.sh
|
||||
source scripts/common.sh
|
||||
load_config # fail closed: data root comes from the system config
|
||||
|
||||
AUTH_FILE="${PI_AUTH_FILE:-$HOME/.pi/agent/auth.json}"
|
||||
AUTH_DIR="$(dirname "$AUTH_FILE")"
|
||||
HARNESS_FILE="${PI_AUTH_FILE:-$HOME/.pi/agent/auth.json}"
|
||||
ACCOUNTS_DIR="$MOSAIC_DEV_DIR/auth"
|
||||
OP="${1:-}"
|
||||
|
||||
case "$OP" in
|
||||
status)
|
||||
if [ ! -e "$AUTH_FILE" ]; then
|
||||
echo "auth: credential file not found: $AUTH_FILE" >&2
|
||||
exit 3
|
||||
fi
|
||||
if [ ! -f "$AUTH_FILE" ] || [ -L "$AUTH_FILE" ]; then
|
||||
echo "auth: credential file must be a regular, non-symbolic-link file: $AUTH_FILE" >&2
|
||||
exit 4
|
||||
fi
|
||||
if ! node -e '
|
||||
# report_file FILE — providers + types for a pi auth.json; never values.
|
||||
report_file() {
|
||||
local file="$1"
|
||||
if [ ! -e "$file" ]; then
|
||||
echo " (not present)"
|
||||
return 3
|
||||
fi
|
||||
if [ ! -f "$file" ] || [ -L "$file" ]; then
|
||||
echo "auth: credential file must be a regular, non-symbolic-link file: $file" >&2
|
||||
return 4
|
||||
fi
|
||||
if ! node -e '
|
||||
const fs = require("fs");
|
||||
const file = process.argv[1];
|
||||
let doc;
|
||||
@@ -40,45 +46,69 @@ if (typeof doc !== "object" || doc === null || Array.isArray(doc)) {
|
||||
console.error("auth: credential file must be a JSON object keyed by provider"); process.exit(2);
|
||||
}
|
||||
const st = fs.statSync(file);
|
||||
console.log("auth file: " + file + " (perms " + String(st.mode & 0o777).toString(8).padStart(3, "0") + ")");
|
||||
console.log(" perms " + String(st.mode & 0o777).toString(8).padStart(3, "0"));
|
||||
const keys = Object.keys(doc).sort();
|
||||
if (keys.length === 0) console.log("providers: (none in file)");
|
||||
if (keys.length === 0) console.log(" providers: (none in file)");
|
||||
for (const k of keys) {
|
||||
const e = doc[k];
|
||||
let type = "unknown";
|
||||
if (typeof e === "object" && e !== null && typeof e.type === "string" && /^[a-z0-9_-]+$/i.test(e.type)) type = e.type;
|
||||
console.log(" " + k + " type=" + type + " source=auth.json");
|
||||
console.log(" " + k + " type=" + type + " source=auth.json");
|
||||
}
|
||||
console.log("resolution: auth.json entries take priority over environment (pi order: --api-key > auth.json > env > models.json)");
|
||||
' "$AUTH_FILE"; then
|
||||
exit 2
|
||||
fi
|
||||
' "$file"; then
|
||||
return 2
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
case "$OP" in
|
||||
status)
|
||||
echo "default harness credential (read-only to the stack; never written): $HARNESS_FILE"
|
||||
RC=0
|
||||
report_file "$HARNESS_FILE" || RC=$?
|
||||
echo "resolution: auth.json entries take priority over environment (pi order: --api-key > auth.json > env > models.json)"
|
||||
ENV_NAMES="$(env | grep -oE '^[A-Z0-9_]+_API_KEY' | sort -u | paste -sd, -)"
|
||||
if [ -n "$ENV_NAMES" ]; then
|
||||
echo "env-side credential-like names set (informational; file entries take priority): $ENV_NAMES"
|
||||
else
|
||||
echo "env-side credential-like names set: (none)"
|
||||
fi
|
||||
;;
|
||||
accounts)
|
||||
if [ ! -d "$AUTH_DIR" ]; then
|
||||
echo "auth: directory not found: $AUTH_DIR" >&2
|
||||
exit 3
|
||||
fi
|
||||
echo "mosaic-managed accounts ($ACCOUNTS_DIR):"
|
||||
shopt -s nullglob
|
||||
FILES=("$AUTH_DIR"/auth.*.json)
|
||||
FILES=("$ACCOUNTS_DIR"/*.json)
|
||||
shopt -u nullglob
|
||||
if [ "${#FILES[@]}" -eq 0 ]; then
|
||||
echo "accounts: (none) — named accounts are auth.<account>.json beside the credential file"
|
||||
echo " (none)"
|
||||
else
|
||||
for f in "${FILES[@]}"; do
|
||||
name="$(basename "$f" .json)"
|
||||
perms="$(stat -c %a "$f" 2>/dev/null || echo '?')"
|
||||
marker=""
|
||||
[ "$f" = "$HARNESS_FILE" ] && marker=" <- active (PI_AUTH_FILE)"
|
||||
warn=""
|
||||
[ "$perms" != "600" ] && warn=" [not 0600 — agent --auth will refuse]"
|
||||
echo " $name (perms $perms)$marker$warn"
|
||||
done
|
||||
fi
|
||||
exit "$RC"
|
||||
;;
|
||||
accounts)
|
||||
shopt -s nullglob
|
||||
FILES=("$ACCOUNTS_DIR"/*.json)
|
||||
shopt -u nullglob
|
||||
if [ "${#FILES[@]}" -eq 0 ]; then
|
||||
echo "accounts: (none) — mosaic-managed accounts are $ACCOUNTS_DIR/<account>.json (perms 0600)"
|
||||
exit 0
|
||||
fi
|
||||
echo "named accounts in $AUTH_DIR:"
|
||||
echo "mosaic-managed accounts in $ACCOUNTS_DIR:"
|
||||
for f in "${FILES[@]}"; do
|
||||
name="$(basename "$f" .json)"
|
||||
name="${name#auth.}"
|
||||
perms="$(stat -c %a "$f" 2>/dev/null || echo '?')"
|
||||
marker=""
|
||||
[ "$f" = "$AUTH_FILE" ] && marker=" <- active (PI_AUTH_FILE)"
|
||||
echo " $name$marker"
|
||||
[ "$f" = "$HARNESS_FILE" ] && marker=" <- active (PI_AUTH_FILE)"
|
||||
warn=""
|
||||
[ "$perms" != "600" ] && warn=" [not 0600 — agent --auth will refuse]"
|
||||
echo " $name (perms $perms)$marker$warn"
|
||||
done
|
||||
;;
|
||||
*)
|
||||
|
||||
+66
-38
@@ -1,10 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
# Fast, sandboxed selftests for the auth checkpoint (M19).
|
||||
# Fast, sandboxed selftests for the auth checkpoint (M19, corrected in #48).
|
||||
#
|
||||
# No Docker, no network, no real credentials: every case runs against
|
||||
# fixture files via PI_AUTH_FILE. The suite asserts the core safety
|
||||
# property: credential MATERIAL from fixtures never reaches auth.sh or
|
||||
# agent.sh output.
|
||||
# No Docker, no network, no real credentials. Mosaic-managed accounts live
|
||||
# under the data root (config-driven, like every other tool); the default
|
||||
# harness credential appears only as an explicit PI_AUTH_FILE fixture.
|
||||
# The suite asserts the core safety property: credential MATERIAL from
|
||||
# fixtures never reaches auth.sh or agent.sh output.
|
||||
set -uo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
@@ -27,22 +28,36 @@ check() {
|
||||
SECRET="SK-TEST-DO-NOT-PRINT-9f2b"
|
||||
TOKEN="ACCESS-TOKEN-SHOULD-NOT-PRINT"
|
||||
ENVVAL="ENVVAL-SHOULD-NOT-PRINT"
|
||||
mkdir -p "$SANDBOX/creds" "$SANDBOX/empty"
|
||||
printf '{"anthropic":{"type":"oauth","access":"%s"},"zai":{"type":"api_key","key":"%s"}}' "$TOKEN" "$SECRET" > "$SANDBOX/creds/auth.json"
|
||||
printf '{}' > "$SANDBOX/creds/auth.work.json"
|
||||
printf '{}' > "$SANDBOX/creds/auth.personal.json"
|
||||
mkdir -p "$SANDBOX/pi" "$SANDBOX/data/auth" "$SANDBOX/data/state" "$SANDBOX/empty"
|
||||
|
||||
# status: missing credential file -> exit 3 (config missing for a read)
|
||||
OUT="$(PI_AUTH_FILE="$SANDBOX/creds/absent.json" scripts/auth.sh status 2>"$SANDBOX/err.txt")"; RC=$?
|
||||
[ "$RC" -eq 3 ] && grep -q "credential file not found" "$SANDBOX/err.txt" \
|
||||
&& check "status on missing credential file exits 3" 0 || check "status on missing credential file exits 3" 1
|
||||
# mock system config: data root inside the sandbox (never the real one)
|
||||
printf '{"configVersion":1,"environment":"development","dataRoot":"%s","execution":{"backend":"docker","provider":"zai","model":"m","adapter":"mock"}}' "$SANDBOX/data" > "$SANDBOX/mock-config.json"
|
||||
CFG="MOSAIC_CONFIG=$SANDBOX/mock-config.json"
|
||||
|
||||
# status: happy path — providers sorted with credential types
|
||||
OUT="$(PI_AUTH_FILE="$SANDBOX/creds/auth.json" scripts/auth.sh status 2>&1)"; RC=$?
|
||||
# release pointer so agent.sh's ensure stays on the no-op fast path
|
||||
REL="$(tr -d '[:space:]' < RELEASE)"
|
||||
printf '{"pointerVersion":1,"release":"%s","imageTag":"mosaic-poc-agent:0.84.4-r%s","activatedAt":"2026-01-01T00:00:00Z"}\n' "$REL" "$REL" > "$SANDBOX/data/state/active.json"
|
||||
|
||||
# fixtures: a default-harness credential and two mosaic-managed accounts
|
||||
printf '{"anthropic":{"type":"oauth","access":"%s"},"zai":{"type":"api_key","key":"%s"}}' "$TOKEN" "$SECRET" > "$SANDBOX/pi/auth.json"
|
||||
printf '{}' > "$SANDBOX/data/auth/work.json"
|
||||
printf '{}' > "$SANDBOX/data/auth/personal.json"
|
||||
chmod 600 "$SANDBOX/pi/auth.json" "$SANDBOX/data/auth/work.json" "$SANDBOX/data/auth/personal.json"
|
||||
|
||||
# status: missing default harness credential -> exit 3, accounts still listed
|
||||
OUT="$(env PI_AUTH_FILE="$SANDBOX/pi/absent.json" $CFG scripts/auth.sh status 2>"$SANDBOX/err.txt")"; RC=$?
|
||||
[ "$RC" -eq 3 ] \
|
||||
&& printf '%s\n' "$OUT" | grep -q ' work (perms 600)' \
|
||||
&& check "status with missing harness credential exits 3 and still lists accounts" 0 || check "status with missing harness credential exits 3 and still lists accounts" 1
|
||||
|
||||
# status: happy path — harness providers + mosaic accounts, both labeled
|
||||
OUT="$(env PI_AUTH_FILE="$SANDBOX/pi/auth.json" $CFG scripts/auth.sh status 2>&1)"; RC=$?
|
||||
[ "$RC" -eq 0 ] \
|
||||
&& printf '%s\n' "$OUT" | grep -q 'read-only to the stack' \
|
||||
&& printf '%s\n' "$OUT" | grep -q ' anthropic type=oauth source=auth\.json' \
|
||||
&& printf '%s\n' "$OUT" | grep -q ' zai type=api_key source=auth\.json' \
|
||||
&& check "status lists providers with credential types" 0 || check "status lists providers with credential types" 1
|
||||
&& printf '%s\n' "$OUT" | grep -q 'mosaic-managed accounts' \
|
||||
&& check "status reports harness credential (read-only) + mosaic accounts" 0 || check "status reports harness credential (read-only) + mosaic accounts" 1
|
||||
|
||||
# the core safety property: fixture secret material never reaches output
|
||||
printf '%s\n' "$OUT" | grep -q "$SECRET" \
|
||||
@@ -50,48 +65,61 @@ printf '%s\n' "$OUT" | grep -q "$SECRET" \
|
||||
printf '%s\n' "$OUT" | grep -q "$TOKEN" \
|
||||
&& check "oauth token material never reaches output" 1 || check "oauth token material never reaches output" 0
|
||||
|
||||
# status: unparseable file -> exit 2
|
||||
printf 'not json' > "$SANDBOX/creds/broken.json"
|
||||
PI_AUTH_FILE="$SANDBOX/creds/broken.json" scripts/auth.sh status >/dev/null 2>&1; RC=$?
|
||||
# status: unparseable harness file -> exit 2
|
||||
printf 'not json' > "$SANDBOX/pi/broken.json"
|
||||
env PI_AUTH_FILE="$SANDBOX/pi/broken.json" $CFG scripts/auth.sh status >/dev/null 2>&1; RC=$?
|
||||
[ "$RC" -eq 2 ] && check "unparseable credential file exits 2" 0 || check "unparseable credential file exits 2" 1
|
||||
|
||||
# status: symlinked credential file -> exit 4 (must be a regular file)
|
||||
ln -s "$SANDBOX/creds/auth.json" "$SANDBOX/creds/link.json"
|
||||
PI_AUTH_FILE="$SANDBOX/creds/link.json" scripts/auth.sh status >/dev/null 2>&1; RC=$?
|
||||
# status: symlinked harness file -> exit 4
|
||||
ln -s "$SANDBOX/pi/auth.json" "$SANDBOX/pi/link.json"
|
||||
env PI_AUTH_FILE="$SANDBOX/pi/link.json" $CFG scripts/auth.sh status >/dev/null 2>&1; RC=$?
|
||||
[ "$RC" -eq 4 ] && check "symlinked credential file exits 4" 0 || check "symlinked credential file exits 4" 1
|
||||
|
||||
# status: env-side names are informational — name shown, value never
|
||||
OUT="$(FAKE_TEST_API_KEY=$ENVVAL PI_AUTH_FILE="$SANDBOX/creds/auth.json" scripts/auth.sh status 2>&1)"
|
||||
# status: env-side names informational — name shown, value never
|
||||
OUT="$(env FAKE_TEST_API_KEY=$ENVVAL PI_AUTH_FILE="$SANDBOX/pi/auth.json" $CFG scripts/auth.sh status 2>&1)"
|
||||
printf '%s\n' "$OUT" | grep -q "FAKE_TEST_API_KEY" \
|
||||
&& check "env-side credential names reported" 0 || check "env-side credential names reported" 1
|
||||
printf '%s\n' "$OUT" | grep -q "$ENVVAL" \
|
||||
&& check "env var values never reach output" 1 || check "env var values never reach output" 0
|
||||
|
||||
# accounts: empty directory -> (none), exit 0
|
||||
OUT="$(PI_AUTH_FILE="$SANDBOX/empty/auth.json" scripts/auth.sh accounts 2>&1)"; RC=$?
|
||||
[ "$RC" -eq 0 ] && printf '%s\n' "$OUT" | grep -q "(none)" \
|
||||
&& check "accounts with no named files reports none" 0 || check "accounts with no named files reports none" 1
|
||||
# accounts: mosaic-managed, under the data root — a data root with no accounts dir is the default state: (none), exit 0, creates nothing
|
||||
printf '{"configVersion":1,"environment":"development","dataRoot":"%s","execution":{"backend":"docker","provider":"zai","model":"m","adapter":"mock"}}' "$SANDBOX/empty" > "$SANDBOX/mock-config-empty.json"
|
||||
OUT="$(env PI_AUTH_FILE="$SANDBOX/pi/auth.json" MOSAIC_CONFIG="$SANDBOX/mock-config-empty.json" scripts/auth.sh accounts 2>&1)"; RC=$?
|
||||
[ "$RC" -eq 0 ] && printf '%s\n' "$OUT" | grep -q '(none)' && [ ! -d "$SANDBOX/empty/auth" ] \
|
||||
&& check "accounts without an accounts dir reports none and creates nothing" 0 || check "accounts without an accounts dir reports none and creates nothing" 1
|
||||
|
||||
# accounts: listing with active marker
|
||||
OUT="$(PI_AUTH_FILE="$SANDBOX/creds/auth.work.json" scripts/auth.sh accounts 2>&1)"; RC=$?
|
||||
[ "$RC" -eq 0 ] && printf '%s\n' "$OUT" | grep -q '^ work <- active (PI_AUTH_FILE)$' \
|
||||
&& printf '%s\n' "$OUT" | grep -q '^ personal$' \
|
||||
OUT="$(env PI_AUTH_FILE="$SANDBOX/data/auth/work.json" $CFG scripts/auth.sh accounts 2>&1)"; RC=$?
|
||||
printf '%s\n' "$OUT" | grep -q '^ work (perms 600) <- active (PI_AUTH_FILE)$' \
|
||||
&& printf '%s\n' "$OUT" | grep -q '^ personal (perms 600)$' \
|
||||
&& check "accounts lists files and marks the active one" 0 || check "accounts lists files and marks the active one" 1
|
||||
|
||||
# accounts: missing directory -> exit 3
|
||||
PI_AUTH_FILE="$SANDBOX/nodir/auth.json" scripts/auth.sh accounts >/dev/null 2>&1; RC=$?
|
||||
[ "$RC" -eq 3 ] && check "accounts on missing directory exits 3" 0 || check "accounts on missing directory exits 3" 1
|
||||
# accounts: loose perms flagged loudly
|
||||
printf '{}' > "$SANDBOX/data/auth/loose.json"
|
||||
chmod 644 "$SANDBOX/data/auth/loose.json"
|
||||
OUT="$(env $CFG scripts/auth.sh accounts 2>&1)"
|
||||
printf '%s\n' "$OUT" | grep -q 'loose (perms 644) \[not 0600 — agent --auth will refuse\]' \
|
||||
&& check "loose account perms flagged in listing" 0 || check "loose account perms flagged in listing" 1
|
||||
|
||||
# agent.sh --auth: missing account file refuses before any container work
|
||||
scripts/agent.sh researcher --auth nope </dev/null >/dev/null 2>"$SANDBOX/err.txt"; RC=$?
|
||||
[ "$RC" -eq 4 ] && grep -q "no credential file for auth account 'nope'" "$SANDBOX/err.txt" \
|
||||
# agent.sh --auth: missing mosaic-managed account file refuses (pre-container)
|
||||
env $CFG scripts/agent.sh researcher --auth nope </dev/null >/dev/null 2>"$SANDBOX/err.txt"; RC=$?
|
||||
[ "$RC" -eq 4 ] && grep -q "no mosaic-managed credential file for auth account 'nope'" "$SANDBOX/err.txt" \
|
||||
&& check "agent --auth with missing account file refuses (exit 4)" 0 || check "agent --auth with missing account file refuses (exit 4)" 1
|
||||
|
||||
# agent.sh --auth: loose perms refuse (0600 required, mirroring gitea-api.sh)
|
||||
env $CFG scripts/agent.sh researcher --auth loose </dev/null >/dev/null 2>"$SANDBOX/err.txt"; RC=$?
|
||||
[ "$RC" -eq 4 ] && grep -q "must be 0600 (got 644)" "$SANDBOX/err.txt" \
|
||||
&& check "agent --auth with non-0600 account file refuses" 0 || check "agent --auth with non-0600 account file refuses" 1
|
||||
|
||||
# agent.sh --auth: invalid account name refuses
|
||||
scripts/agent.sh researcher --auth "bad~name" </dev/null >/dev/null 2>"$SANDBOX/err.txt"; RC=$?
|
||||
env $CFG scripts/agent.sh researcher --auth "bad~name" </dev/null >/dev/null 2>"$SANDBOX/err.txt"; RC=$?
|
||||
[ "$RC" -eq 4 ] && grep -q "invalid auth account name" "$SANDBOX/err.txt" \
|
||||
&& check "agent --auth with invalid account name refuses" 0 || check "agent --auth with invalid account name refuses" 1
|
||||
|
||||
# auth.sh: invalid config refuses (fail closed, like every tool)
|
||||
MOSAIC_CONFIG="$SANDBOX/no-such-config.json" scripts/auth.sh status >/dev/null 2>&1; RC=$?
|
||||
[ "$RC" -ne 0 ] && check "auth.sh without valid config refuses" 0 || check "auth.sh without valid config refuses" 1
|
||||
|
||||
echo
|
||||
echo "selftest: $PASS passed, $FAIL failed"
|
||||
[ "$FAIL" -eq 0 ]
|
||||
|
||||
Reference in New Issue
Block a user