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:
@@ -67,11 +67,11 @@ green at every step. Not production software — a proven foundation.
|
|||||||
|
|
||||||
`scripts/bootstrap.sh` (idempotent) · `build.sh` · `hello.sh` ·
|
`scripts/bootstrap.sh` (idempotent) · `build.sh` · `hello.sh` ·
|
||||||
`verify.sh` · `run-task.sh run <task.json>` · `release.sh
|
`verify.sh` · `run-task.sh run <task.json>` · `release.sh
|
||||||
package|activate|rollback|status` · `reset.sh` (**danger**: wipes the data
|
package|activate|rollback|status` · `auth.sh status|accounts` · `reset.sh` (**danger**: wipes the data
|
||||||
root; triple-safety-checked) · `mosaic-task.mjs validate|run|show|list|retry|prune|resolve-role` ·
|
root; triple-safety-checked) · `mosaic-task.mjs validate|run|show|list|retry|prune|resolve-role` ·
|
||||||
`agent.sh <name>` (interactive TUI agent) ·
|
`agent.sh <name>` (interactive TUI agent) ·
|
||||||
suites: `test-config.sh`, `test-task.sh`, `test-release.sh`,
|
suites: `test-config.sh`, `test-task.sh`, `test-release.sh`,
|
||||||
`test-conductor.sh`.
|
`test-conductor.sh`, `test-auth.sh`.
|
||||||
|
|
||||||
Full reference — usage, fields, exit codes, safety notes:
|
Full reference — usage, fields, exit codes, safety notes:
|
||||||
`docs/TOOLS.md` (read on demand; do not rely on this summary for detail).
|
`docs/TOOLS.md` (read on demand; do not rely on this summary for detail).
|
||||||
|
|||||||
@@ -522,3 +522,34 @@ verify green.
|
|||||||
Suites 24/90/14/17 + verify green. Seat resolution is now fail closed in
|
Suites 24/90/14/17 + verify green. Seat resolution is now fail closed in
|
||||||
every direction: unknown seat under override, declared role without
|
every direction: unknown seat under override, declared role without
|
||||||
contract, invalid contract, empty ceiling intersection.
|
contract, invalid contract, empty ceiling intersection.
|
||||||
|
|
||||||
|
## Phase 24: M19 — harness auth tooling (pi checkpoint)
|
||||||
|
|
||||||
|
- 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 entries
|
||||||
|
auto-refresh. Multi-account per provider is NOT native (one entry per
|
||||||
|
provider, no namespacing) → named-file design confirmed:
|
||||||
|
auth.<account>.json + per-launch injection.
|
||||||
|
- scripts/auth.sh: `status` (provider names + credential types + perms +
|
||||||
|
env-side credential-like 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
|
||||||
|
(symlink refuses).
|
||||||
|
- scripts/agent.sh --auth <account>: resolves auth.<account>.json and
|
||||||
|
exports PI_AUTH_FILE — the compose read-only mount source, so no new
|
||||||
|
plumbing. Missing/invalid account refuses before any container work.
|
||||||
|
- scripts/test-auth.sh (13 cases, no Docker): the core assertion is the
|
||||||
|
safety property itself — fixture key/token/env VALUES never reach
|
||||||
|
output — plus the exit-code paths and the accounts listing.
|
||||||
|
- Scope note: headless task runs keep the default credential; worker auth
|
||||||
|
selection is a separate policy decision.
|
||||||
|
- Real-host smoke: 3 providers reported (anthropic/openai-codex oauth,
|
||||||
|
zai api_key), perms 600, no named accounts yet.
|
||||||
|
|
||||||
|
## Result (M19)
|
||||||
|
|
||||||
|
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.
|
||||||
|
|||||||
@@ -13,3 +13,4 @@ are never rewritten or removed; corrections are new entries.
|
|||||||
| 2026-09-03 | owner + assistant (conductor) + ms-test collaborator | Skill revisions adjudicated (#44): ms-communications integrated as-authored (owner preamble restructure + collaborator delivery-discipline hunks); ms-conductor collaborator redraft integrated with conductor remediation (step 3 refusal-vs-outage distinction; preserves owner's outage-dispatch intent inside fail-closed canon); TOOLS.md gains release.sh ensure row | skills/ms-communications/SKILL.md, skills/ms-conductor/SKILL.md, docs/TOOLS.md; suites 24/74/14/17 + verify green; unslop clean ×3 |
|
| 2026-09-03 | owner + assistant (conductor) + ms-test collaborator | Skill revisions adjudicated (#44): ms-communications integrated as-authored (owner preamble restructure + collaborator delivery-discipline hunks); ms-conductor collaborator redraft integrated with conductor remediation (step 3 refusal-vs-outage distinction; preserves owner's outage-dispatch intent inside fail-closed canon); TOOLS.md gains release.sh ensure row | skills/ms-communications/SKILL.md, skills/ms-conductor/SKILL.md, docs/TOOLS.md; suites 24/74/14/17 + verify green; unslop clean ×3 |
|
||||||
| 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 | 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 | 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 |
|
||||||
|
|||||||
+20
-1
@@ -54,7 +54,25 @@ contract's tools are a ceiling the seat definition or `--tools` may narrow,
|
|||||||
never escalate past. Missing/invalid contract refuses the launch; empty
|
never escalate past. Missing/invalid contract refuses the launch; empty
|
||||||
intersection = loud tool-free seat. An explicit `MOSAIC_AGENTS_DIR` override
|
intersection = loud tool-free seat. An explicit `MOSAIC_AGENTS_DIR` override
|
||||||
that cannot resolve the named seat also refuses (#46) — unset the override
|
that cannot resolve the named seat also refuses (#46) — unset the override
|
||||||
for the M13 plain governed TUI.
|
for the M13 plain governed TUI. `--auth <account>` injects
|
||||||
|
`auth.<account>.json` (beside the active credential file) as the launch's
|
||||||
|
`PI_AUTH_FILE`; a missing/invalid account refuses (M19).
|
||||||
|
|
||||||
|
## Auth (credentials)
|
||||||
|
|
||||||
|
Credential checkpoint over pi's auth model (one `auth.json` keyed by
|
||||||
|
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.
|
||||||
|
|
||||||
|
| 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 |
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
## Release
|
## Release
|
||||||
|
|
||||||
@@ -86,6 +104,7 @@ Push is never automatic.
|
|||||||
| `scripts/test-task.sh` | Task selftests + live cases | 90 cases |
|
| `scripts/test-task.sh` | Task selftests + live cases | 90 cases |
|
||||||
| `scripts/test-release.sh` | Release selftests | 14 cases |
|
| `scripts/test-release.sh` | Release selftests | 14 cases |
|
||||||
| `scripts/test-conductor.sh` | Auto-apply selftests (sandboxed) | 17 cases |
|
| `scripts/test-conductor.sh` | Auto-apply selftests (sandboxed) | 17 cases |
|
||||||
|
| `scripts/test-auth.sh` | Auth checkpoint selftests (no Docker) | 13 cases |
|
||||||
| `scripts/gitea-api.sh <METHOD> <path> [body]` | Gitea API helper | Token never on argv/stdout |
|
| `scripts/gitea-api.sh <METHOD> <path> [body]` | Gitea API helper | Token never on argv/stdout |
|
||||||
|
|
||||||
## Tools (host-side)
|
## Tools (host-side)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ update this file to the next action). No ambiguity, no re-planning.
|
|||||||
|
|
||||||
## Next action
|
## Next action
|
||||||
|
|
||||||
M19 — harness auth tooling (pi checkpoint: auth status, provider stacking, multi-account investigation) — say "next" to execute.
|
(none queued — M19 completes the agreed ROADMAP sequence M16–M19. M20 — packages/* restructure + unified CLI — is owner-gated per ROADMAP; say "plan M20" to draft its issue, or point at anything else.)
|
||||||
|
|
||||||
## Queue (ordered per docs/plans/ROADMAP.md)
|
## Queue (ordered per docs/plans/ROADMAP.md)
|
||||||
|
|
||||||
@@ -52,3 +52,4 @@ git history + Gitea issues.
|
|||||||
- 2026-09-03 — skill revisions adjudicated (#44): ms-communications integrated as-authored; ms-conductor redraft + conductor remediation (refusal vs outage); TOOLS.md release.sh ensure row
|
- 2026-09-03 — skill revisions adjudicated (#44): ms-communications integrated as-authored; ms-conductor redraft + conductor remediation (refusal vs outage); TOOLS.md release.sh ensure row
|
||||||
- 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 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 — 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
|
||||||
|
|||||||
+18
-2
@@ -3,7 +3,7 @@
|
|||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# scripts/agent.sh <name> [--mission <file>] [--workspace <ws>]
|
# 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,
|
# The agent receives the four immutable contracts (constitution, standards,
|
||||||
# SOUL, USER) plus its own identity and optional mission directives as its
|
# SOUL, USER) plus its own identity and optional mission directives as its
|
||||||
@@ -23,6 +23,7 @@ WORKSPACE=""
|
|||||||
SESSION=""
|
SESSION=""
|
||||||
TOOLS=""
|
TOOLS=""
|
||||||
SKILLS=""
|
SKILLS=""
|
||||||
|
AUTH_ACCOUNT=""
|
||||||
|
|
||||||
while [ $# -gt 0 ]; do
|
while [ $# -gt 0 ]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
@@ -30,15 +31,30 @@ while [ $# -gt 0 ]; do
|
|||||||
--workspace) WORKSPACE="${2:?}"; shift 2 ;;
|
--workspace) WORKSPACE="${2:?}"; shift 2 ;;
|
||||||
--session) SESSION="${2:?}"; shift 2 ;;
|
--session) SESSION="${2:?}"; shift 2 ;;
|
||||||
--tools) TOOLS="${2:?}"; shift 2 ;;
|
--tools) TOOLS="${2:?}"; shift 2 ;;
|
||||||
|
--auth) AUTH_ACCOUNT="${2:?}"; shift 2 ;;
|
||||||
--skills) SKILLS="${2:?}"; shift 2 ;;
|
--skills) SKILLS="${2:?}"; shift 2 ;;
|
||||||
--help|-h) sed -n '2,12p' "$0"; exit 0 ;;
|
--help|-h) sed -n '2,12p' "$0"; exit 0 ;;
|
||||||
*) NAME="$1"; shift ;;
|
*) NAME="$1"; shift ;;
|
||||||
esac
|
esac
|
||||||
done
|
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
|
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_config
|
||||||
load_release
|
load_release
|
||||||
bootstrap_runtime_dir
|
bootstrap_runtime_dir
|
||||||
|
|||||||
Executable
+88
@@ -0,0 +1,88 @@
|
|||||||
|
#!/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.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
#
|
||||||
|
# Exit codes: 0 report produced · 2 credential file unparseable ·
|
||||||
|
# 3 file/dir missing for a read · 4 file/environment problem.
|
||||||
|
set -uo pipefail
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
AUTH_FILE="${PI_AUTH_FILE:-$HOME/.pi/agent/auth.json}"
|
||||||
|
AUTH_DIR="$(dirname "$AUTH_FILE")"
|
||||||
|
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 '
|
||||||
|
const fs = require("fs");
|
||||||
|
const file = process.argv[1];
|
||||||
|
let doc;
|
||||||
|
try { doc = JSON.parse(fs.readFileSync(file, "utf8")); }
|
||||||
|
catch (e) { console.error("auth: credential file is not valid JSON: " + e.message); process.exit(2); }
|
||||||
|
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") + ")");
|
||||||
|
const keys = Object.keys(doc).sort();
|
||||||
|
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("resolution: auth.json entries take priority over environment (pi order: --api-key > auth.json > env > models.json)");
|
||||||
|
' "$AUTH_FILE"; then
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
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
|
||||||
|
shopt -s nullglob
|
||||||
|
FILES=("$AUTH_DIR"/auth.*.json)
|
||||||
|
shopt -u nullglob
|
||||||
|
if [ "${#FILES[@]}" -eq 0 ]; then
|
||||||
|
echo "accounts: (none) — named accounts are auth.<account>.json beside the credential file"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "named accounts in $AUTH_DIR:"
|
||||||
|
for f in "${FILES[@]}"; do
|
||||||
|
name="$(basename "$f" .json)"
|
||||||
|
name="${name#auth.}"
|
||||||
|
marker=""
|
||||||
|
[ "$f" = "$AUTH_FILE" ] && marker=" <- active (PI_AUTH_FILE)"
|
||||||
|
echo " $name$marker"
|
||||||
|
done
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "usage: scripts/auth.sh status | accounts" >&2
|
||||||
|
exit 4
|
||||||
|
;;
|
||||||
|
esac
|
||||||
Executable
+97
@@ -0,0 +1,97 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Fast, sandboxed selftests for the auth checkpoint (M19).
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
set -uo pipefail
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
SANDBOX="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$SANDBOX"' EXIT
|
||||||
|
|
||||||
|
PASS=0
|
||||||
|
FAIL=0
|
||||||
|
# Status colors: terminal-only, NO_COLOR-respecting; plain when piped.
|
||||||
|
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
|
||||||
|
C_OK=$'\033[0;32m'; C_FAIL=$'\033[0;31m'; C_RESET=$'\033[0m'
|
||||||
|
else
|
||||||
|
C_OK=""; C_FAIL=""; C_RESET=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
check() {
|
||||||
|
if [ "$2" = "0" ]; then PASS=$((PASS+1)); echo "${C_OK}OK${C_RESET} $1"; else FAIL=$((FAIL+1)); echo "${C_FAIL}FAIL${C_RESET} $1"; fi
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# status: happy path — providers sorted with credential types
|
||||||
|
OUT="$(PI_AUTH_FILE="$SANDBOX/creds/auth.json" scripts/auth.sh status 2>&1)"; RC=$?
|
||||||
|
[ "$RC" -eq 0 ] \
|
||||||
|
&& 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
|
||||||
|
|
||||||
|
# the core safety property: fixture secret material never reaches output
|
||||||
|
printf '%s\n' "$OUT" | grep -q "$SECRET" \
|
||||||
|
&& check "api key material never reaches output" 1 || check "api key material never reaches output" 0
|
||||||
|
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=$?
|
||||||
|
[ "$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=$?
|
||||||
|
[ "$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)"
|
||||||
|
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: 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$' \
|
||||||
|
&& 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
|
||||||
|
|
||||||
|
# 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" \
|
||||||
|
&& 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: invalid account name refuses
|
||||||
|
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
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "selftest: $PASS passed, $FAIL failed"
|
||||||
|
[ "$FAIL" -eq 0 ]
|
||||||
Reference in New Issue
Block a user