From d9a94f51ba22d0c658e4bce5f9b295b7b0a0c8d0 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Fri, 4 Sep 2026 11:57:40 -0500 Subject: [PATCH] docs(plan): specify harness declaration + centralized auth/provider registry (#49) Design only; implementation blocked pending owner review. - agent.json: one harness identifier (pi first), resolved through a versioned adapter/harness manifest; reusable settingsProfile reference - central data-root registry: providers, accounts (metadata + secret credential split), reusable settings profiles, audited runtime selection - per-seat pi auth.json/models.json mechanically generated and atomically activated; no seat/provider registration ceremony - mixed oauth/api-key accounts supported centrally; one active account per provider per pi materialization - host-side centralized OAuth login/refresh; agents never authenticate - local/remote Ollama modeled as endpoint providers, not accounts - target mosaic auth/provider/agent settings CLI; secrets never on argv - migration, fail-closed acceptance suites, and ten explicit review gates CURRENT.md points only to spec review. Suites 24/15/90/14/17 + verify green; unslop clean. --- BUILD-LOG.md | 33 ++ docs/SESSIONS.md | 1 + ...26-09-03_auth-provider-harness-registry.md | 418 ++++++++++++++++++ docs/plans/CURRENT.md | 3 +- 4 files changed, 454 insertions(+), 1 deletion(-) create mode 100644 docs/plans/2026-09-03_auth-provider-harness-registry.md diff --git a/BUILD-LOG.md b/BUILD-LOG.md index 0bc3a757..f3d70d2f 100644 --- a/BUILD-LOG.md +++ b/BUILD-LOG.md @@ -581,3 +581,36 @@ unified CLI) is owner-gated. 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. + +## Phase 25: harness/provider/auth registry specification (#49) + +- Design-only phase; no implementation authorized. Draft: + `docs/plans/2026-09-03_auth-provider-harness-registry.md`. +- Agent harness is specified in agent.json as one scalar identifier + (`harness: "pi"` initially), resolved through a versioned harness + manifest/adapter rather than a permanent hard-coded enum. Existing v1 + seats migrate with a loud pi default; no CLI harness override in phase 1. +- Central registry separates providers, accounts, reusable settings + profiles, runtime seat selection, and generated harness files. A seat + references one reusable profile; it is not registered with each provider. +- Multiple OAuth/API accounts for one provider may be centrally authorized; + pi materializes exactly one active account per provider. Runtime selection + is audited data-root state; generated auth.json/models.json are disposable + per-seat derivatives. +- OAuth login/refresh is host-side and centralized; agents never enroll or + own refresh tokens. Exact noninteractive pi refresh mechanics remain a + required implementation investigation. +- Local and remote Ollama are endpoint-provider records materialized into + per-seat models.json, independently scoped from authentication accounts. +- Target mosaic CLI covers auth account lifecycle, provider lifecycle, + refresh/ensure, reusable settings profiles, and seat selection. Secret + material is never accepted on argv. +- Spec includes registry paths/schemas, naming/perms, fail-closed launch + materialization, migration from M19, acceptance suites, and ten explicit + owner review gates (including reset/backup semantics and encryption at + rest). + +## Result (spec draft) + +Spec is ready for owner/conductor review; CURRENT.md points only to that +review. Implementation remains blocked until all ten gates are resolved. diff --git a/docs/SESSIONS.md b/docs/SESSIONS.md index c0833af0..aa1b8dd0 100644 --- a/docs/SESSIONS.md +++ b/docs/SESSIONS.md @@ -15,3 +15,4 @@ are never rewritten or removed; corrections are new entries. | 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/.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 | +| 2026-09-03 | owner (requirements) + assistant (conductor/spec author) | Harness/provider/auth registry specification (#49): agent.json harness declaration, centralized providers/accounts/settings profiles, audited runtime selection, per-seat auth/models materialization, centralized OAuth lifecycle, local/remote Ollama, target mosaic CLI | docs/plans/2026-09-03_auth-provider-harness-registry.md; implementation blocked pending ten-gate review; unslop clean | diff --git a/docs/plans/2026-09-03_auth-provider-harness-registry.md b/docs/plans/2026-09-03_auth-provider-harness-registry.md new file mode 100644 index 00000000..5e6fd2b1 --- /dev/null +++ b/docs/plans/2026-09-03_auth-provider-harness-registry.md @@ -0,0 +1,418 @@ +# Harness declaration + centralized auth/provider registry + +Status: **DRAFT FOR OWNER REVIEW** — no implementation is authorized by this +file. Issue: #49. Date: 2026-09-03. + +## Purpose + +Specify how Mosaic Stack supports multiple harnesses, providers, endpoints, +and authentication accounts without modifying a user's default harness +configuration or requiring provider registration on every agent seat. + +Pi is the first implementation checkpoint. The registry and seat contracts +must remain harness-neutral so future adapters (Claude Code, Codex, OpenCode) +materialize their own native files from the same desired state. + +## Non-negotiable decisions already made + +1. Default harness homes (`~/.pi`, and future equivalents) are read-only to + Mosaic. The stack never writes there. +2. Credentials live only under the configured data root, never in the repo, + image, argv, stdout, logs, or generated non-secret manifests. +3. Account registration is centralized. A seat never performs OAuth login and + does not maintain an independent source credential. +4. Seat files are generated artifacts, not user-authored configuration. +5. Missing, stale, invalid, insecure, or ambiguous registry/materialized state + refuses launch. No fallback to a different account. +6. Pi is implemented first; harness-specific behavior stays behind adapters + and materializers. + +## Separate concepts (do not conflate) + +| Concept | Example | Authority | +|---|---|---| +| Harness | `pi`, later `claude-code`, `codex`, `opencode` | `agent.json` + versioned harness manifest | +| Provider | `openai-codex`, `anthropic`, `zai`, `ollama-local` | central provider registry | +| Account | `openai-codex/homelab-openai` | central account registry | +| Endpoint | local/remote Ollama base URL | provider record | +| Settings profile | reusable account/provider/model policy | central settings registry | +| Seat selection | active choice within a profile's allowed accounts | audited runtime state | +| Materialization | harness-native `auth.json` / `models.json` | generated per seat | + +Accounts represent identities. Ollama instances represent endpoints. Harnesses +consume generated configuration. Keeping these axes separate prevents account, +provider, and model-policy conflicts. + +## Harness declaration: `agent.json` + +A defined seat declares exactly one harness: + +```json +{ + "agentVersion": 2, + "name": "researcher", + "role": "researcher", + "harness": "pi", + "settingsProfile": "research-default", + "capabilities": { "tools": ["read", "bash"] } +} +``` + +Rules: + +- `harness` is a scalar identifier, not an array. One process launch runs one + harness. +- It is not permanently hard-coded to an enum in the agent schema. The value + must match a versioned harness manifest/adapter identifier. +- Canonical identifiers proposed for review: `pi`, `claude-code`, `codex`, + `opencode`. Provider `openai-codex` and harness `codex` remain distinct. +- No CLI harness override in phase 1. Harness is part of reviewed seat identity; + an override would bypass that declaration. +- Migration: existing `agentVersion: 1` seats without `harness` resolve to `pi` + with a loud deprecation warning. A later reviewed migration makes version 2 + and `harness` mandatory. +- Plain M13 launches without an agent definition continue to use the system + execution adapter; they have no seat enrollment or generated seat auth. + +### Harness manifests + +Harness support is versioned with its adapter, for example: + +```text +adapters/pi/harness.json +adapters/claude-code/harness.json +``` + +A manifest declares its identifier, adapter, materializers, native config paths, +and supported credential types. Adding a harness requires a reviewed adapter + +manifest + suites; it is not a mutable auth-registry operation. + +## Central registry layout + +Fixed below ``; not configurable independently (config.json remains +the sole system config): + +```text +/auth/ +├── providers/ +│ ├── openai-codex.json +│ ├── anthropic.json +│ ├── zai.json +│ ├── ollama-local.json +│ └── ollama-remote.json +├── accounts/ +│ ├── openai-codex/ +│ │ ├── homelab-openai/ +│ │ │ ├── account.json # non-secret metadata, 0600 +│ │ │ └── credential.json # secret OAuth/API material, 0600 +│ │ └── personal-openai/ +│ │ ├── account.json +│ │ └── credential.json +│ └── zai/ +│ └── main/ +│ ├── account.json +│ └── credential.json +├── settings/ +│ ├── base.json +│ └── research-default.json +└── state/ + ├── refresh.json # non-secret status only + └── activation-log.jsonl # append-only, no secret material +``` + +Naming and security: + +- Provider/account/profile IDs match `^[a-z0-9][a-z0-9._-]{0,63}$`. +- Display names are metadata; path IDs are explicit or deterministically + slugged and confirmed before creation. +- Account path must be under its registered provider. +- Registry dirs are owner-only; account and credential files are regular, + non-symlink, 0600. Loose perms refuse use. +- `credential.json` schema depends on type but is never returned by list/status. +- Provider removal refuses while accounts/profiles/seats reference it. +- Account removal refuses while profiles/selections reference it unless an + explicit reviewed migration removes those references first. + +## Registry records + +### Provider + +Native provider example: + +```json +{ + "providerVersion": 1, + "id": "openai-codex", + "kind": "native", + "harnesses": { "pi": { "providerId": "openai-codex" } }, + "credentialTypes": ["oauth", "api_key"] +} +``` + +Custom endpoint example: + +```json +{ + "providerVersion": 1, + "id": "ollama-local", + "kind": "custom-endpoint", + "harnesses": { + "pi": { + "api": "openai-completions", + "baseUrl": "http://host.docker.internal:11434/v1", + "apiKey": "ollama", + "models": ["qwen2.5-coder:7b", "llama3.1:8b"] + } + }, + "credentialTypes": ["none"] +} +``` + +`ollama-remote` is a second provider record with its remote base URL, optional +credential account, and independently scoped model catalog. Linux compose must +provide host-gateway resolution for local Ollama; container `localhost` is not +the host. + +### Account metadata + +```json +{ + "accountVersion": 1, + "id": "homelab-openai", + "name": "Homelab OpenAI", + "provider": "openai-codex", + "type": "oauth", + "createdAt": "" +} +``` + +`credential.json` holds the pi-compatible OAuth/API payload centrally. API keys +are accepted only from an interactive hidden prompt, stdin, or a validated +0600 file — never an argv value. + +### Reusable settings profile + +Profiles remove per-seat/per-provider registration: + +```json +{ + "settingsVersion": 1, + "id": "research-default", + "allowedAccounts": [ + "openai-codex/homelab-openai", + "openai-codex/personal-openai", + "zai/main" + ], + "defaultAccounts": { + "openai-codex": "openai-codex/homelab-openai", + "zai": "zai/main" + }, + "providers": ["ollama-local", "ollama-remote"], + "models": { + "ollama-local": ["qwen2.5-coder:7b"], + "ollama-remote": ["abliterated-model-id"] + } +} +``` + +A seat references one profile. Editing one central profile updates every seat +that uses it on the next ensure/launch. The seat is not re-registered with each +provider. + +A profile may authorize multiple accounts for the same provider. Pi can place +only one entry per provider in one `auth.json`; therefore exactly one account +per provider is active for a materialization. Defaults provide the initial +selection. + +## Runtime seat selection + +At-will changes are mutable state, not repo edits: + +```text +/agents//settings-selection.json +``` + +Example: + +```json +{ + "selectionVersion": 1, + "profile": "research-default", + "accounts": { + "openai-codex": "openai-codex/personal-openai" + }, + "updatedAt": "" +} +``` + +Rules: + +- Selection can choose only accounts allowed by the seat's referenced profile. +- Omitted provider selections use profile defaults. +- Changing selection is append-only-audited and triggers regeneration. +- No account choice silently falls back to another account. +- In-session identity swapping is deferred. Selection normally applies on + relaunch/new session; changing identity inside a session risks ambiguous + billing, provider state, and audit lineage. + +## Mechanical per-seat materialization + +Generated state: + +```text +/agents//generated/pi/ +├── auth.json # secret, 0600, generated +├── models.json # generated custom providers/model scopes +└── manifest.json # non-secret inputs/hashes/timestamps; no credential hash +``` + +Resolution on `agent.sh `: + +1. Strictly validate agent definition and resolve its harness manifest. +2. Resolve role, settings profile, runtime selection, providers, accounts, and + model scopes. +3. Validate every referenced record, regular-file constraint, ownership, and + 0600 credential perms. +4. Refresh required centralized OAuth records if policy says stale/near expiry; + refusal leaves prior generated files untouched. +5. Generate beside existing files, validate harness-native output, chmod 0600, + then atomically rename into place. +6. Compare/write non-secret manifest state and append an activation receipt. +7. Mount generated files into the harness's native paths read-only and launch. + +No seat authenticates, edits auth.json, or owns an independent OAuth refresh +token. Generated files are disposable derivatives of the registry. + +`mosaic auth ensure []` runs the same materializer explicitly. A periodic +host service refreshes central OAuth records and rematerializes affected seats; +launch-time ensure is the final fail-closed gate. OAuth refresh mechanics must +reuse pi's implementation where possible rather than reimplement provider +protocols; the exact noninteractive refresh trigger is an implementation +investigation and acceptance gate. + +## CLI contract (target `mosaic` surface) + +```text +mosaic auth list [--provider ] [--json] +mosaic auth status [|--seat ] [--json] +mosaic auth new +mosaic auth new --name "Homelab OpenAI" --provider openai-codex --type oauth +mosaic auth rm +mosaic auth login +mosaic auth logout +mosaic auth refresh [|--all] +mosaic auth ensure [|--all] + +mosaic auth provider list [--json] +mosaic auth provider status [--json] +mosaic auth provider create +mosaic auth provider create --id ollama-local --kind custom-endpoint ... +mosaic auth provider rm + +mosaic agent settings use +mosaic agent auth use +mosaic agent auth status +``` + +Behavior: + +- `new` without sufficient flags launches an interactive wizard. +- OAuth `new/login` launches the host-side provider flow once and stores the + resulting centralized profile. Agents never run it. +- API secret input is hidden prompt/stdin/validated file only. +- `rm` reports references and refuses when in use. +- Every mutating command writes an append-only secret-free receipt. +- `list/status --json` never include credential/token/key fields. +- CLI flags name accounts/providers; no secret material is accepted on argv. + +Until M20 provides `mosaic`, `scripts/auth.sh` may prototype the backend, but it +must use the same schemas and must not become a competing implementation. + +## Harness-neutral materializers + +Core resolution produces a secret-bearing internal representation in memory: +selected providers/accounts/models for one seat. A harness materializer maps it +to native files: + +- Pi: `auth.json` + `models.json`. +- Claude Code/Codex/OpenCode: future adapter-defined files/env, without changing + account/provider/profile schemas. + +The harness manifest declares support. Enrollment or launch refuses if a chosen +account/provider cannot materialize for the seat's harness. No best-effort +provider dropping. + +## Policy relationships (future phase, not initial implementation) + +Initial authorization is settings-profile enrollment. Later, the existing +least-privilege doctrine extends naturally: + +```text +role auth ceiling ∩ profile enrollment ∩ mission grant ∩ task grant += effective providers/accounts/models +``` + +A task may narrow a seat's providers/models, never select an account outside +its profile. This is separate from registry and materialization correctness. + +## Migration from M19 prototype + +No named accounts currently exist, so migration is state-free: + +1. Keep `~/.pi/agent/auth.json` as the default harness file, read-only to Mosaic. +2. Replace the prototype `/auth/.json` convention with the + registry tree before users create accounts. +3. Keep `scripts/auth.sh status/accounts` behavior but drive it from registry + schemas. +4. Replace `agent.sh --auth ` direct-file selection with profile + + runtime selection + materialization. A temporary compatibility path may map + `--auth provider/account` to a one-launch selection, but must be explicit and + audited. +5. Add `harness: "pi"` + `settingsProfile` to researcher through an agentVersion + migration after review. + +## Required suites / acceptance + +- Registry schemas: unknown keys, path/name mismatch, symlinks, traversal, + wrong perms, duplicate IDs, missing provider, unsupported credential type. +- Secret non-disclosure: fixture keys/tokens never appear in stdout, stderr, + JSON status, manifests, receipts, or git diff. +- Referential integrity: provider/account/profile removal refuses while used. +- Multiple account types for one provider register successfully. +- Profile authorizes multiple same-provider accounts; exactly one active choice + materializes for pi. +- Changing runtime choice regenerates atomically; failed generation preserves + previous files and records refusal. +- OAuth central refresh rematerializes affected seats; agents never authenticate. +- `harness: pi` resolves; unregistered/unsupported harness refuses. +- Pi materialization generates valid auth.json/models.json with scoped providers + and models; custom local/remote Ollama entries remain distinct. +- Launch refuses missing/stale/invalid materialization; no account fallback. +- Existing generic TUI and default-harness use remain untouched. +- Full existing suites + verify green. + +## Review gates (must resolve before implementation) + +1. Confirm canonical harness IDs: `pi`, `claude-code`, `codex`, `opencode`. +2. Confirm `agentVersion: 2` migration and whether `settingsProfile` becomes + mandatory for defined seats. +3. Confirm registry path/schema split (`account.json` metadata + + `credential.json` secret) versus one encrypted/combined file. +4. Decide encryption-at-rest requirement. Filesystem 0600 is specified now; + external keyring/envelope encryption would change login/refresh design. +5. Confirm selection scope: seat-global only initially, or named launch profiles + (e.g. `work`, `personal`) as a first-class layer. +6. Confirm whether a seat may switch same-provider account on relaunch only, or + whether forked sessions must pin the original account in immutable session + metadata. +7. Determine pi's supported host-side noninteractive OAuth refresh trigger; + implementation must prove refresh without exposing or duplicating tokens. +8. Confirm local Ollama container routing (`host-gateway`) and remote Ollama + transport/auth requirements. +9. Decide whether M20 `packages/mosaic` begins with this auth/provider domain or + whether scripts prototype it first. +10. Define backup/reset semantics for the central registry. Current reset wipes + the data root; OAuth re-login cost may justify a separately protected + registry root, but that would require an explicit canon change. + +Implementation is blocked until these gates are reviewed and owner-approved. diff --git a/docs/plans/CURRENT.md b/docs/plans/CURRENT.md index 5575712b..5564fb83 100644 --- a/docs/plans/CURRENT.md +++ b/docs/plans/CURRENT.md @@ -7,7 +7,7 @@ update this file to the next action). No ambiguity, no re-planning. ## Next action -(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.) +Review `docs/plans/2026-09-03_auth-provider-harness-registry.md` against its ten review gates; revise until owner-approved. No implementation starts before approval. ## Queue (ordered per docs/plans/ROADMAP.md) @@ -54,3 +54,4 @@ git history + Gitea issues. - 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 /auth, 0600 enforced; test-auth 13 → 15 +- 2026-09-03 — harness/provider/auth registry specification drafted (#49): agent.json harness declaration, central provider/account/settings registries, runtime seat selection, mechanical per-harness materialization, centralized OAuth refresh, Ollama endpoints, CLI contract; implementation blocked pending ten-gate review