# 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`, `codex`, `opencode`) materialize their own native files from the same desired state. Canonical harness IDs are the executable names so CLI, manifests, diagnostics, and user expectation stay 1:1. ## 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 hard-coded to an enum in the agent schema. The value must resolve dynamically through the installed/available harness registry. - Canonical identifiers (owner-confirmed during #50 review) are executable names: `pi`, `claude`, `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, executable name, adapter, compatible version range, installer/detector metadata, execution mode, materializers, native config paths, and supported credential types. Adding harness support requires a reviewed adapter + manifest + suites. Detecting/installing a known harness is mutable machine state; changing the manifest/adapter contract is a reviewed repository change. ### Harness detection, installation, and availability Target CLI: ```text mosaic harness list [--available|--installed] [--json] mosaic harness detect [|--all] mosaic harness install [--version ] mosaic harness rm mosaic harness status [] [--json] ``` Lifecycle and rules: 1. `detect` checks canonical executable names (`pi`, `claude`, `codex`, `opencode`) on PATH and approved known locations, resolves real paths, obtains versions using manifest-declared noninteractive commands, validates compatibility, and records compatible findings as available. Unknown executables are never auto-registered. 2. Detection records metadata only: executable path, version, source (`external-detected`), compatibility result, detection timestamp, and manifest identity. It never reads/copies the harness's home, settings, auth, sessions, extensions, or plugins. 3. `install ` resolves the reviewed catalog/manifest, pins an exact compatible version, verifies package identity/checksum where supported, and installs/packages into a Mosaic-managed immutable runtime. It never performs an unversioned global install and never writes the default harness home. 4. Built-in pi is an image-baked managed harness. Future installs should package a harness-specific container image/bundle beside the active release so `reset.sh` does not destroy installed runtimes; dataRoot stores registry state and receipts, not the package payload. 5. Availability and launch-readiness are separate fields. A compatible detected host executable is `available`; under the current container boundary it is not automatically `ready` until imported/installed into a managed runtime, unless a separately reviewed adapter explicitly supports host execution. 6. `agent.json.harness` launch requires a compatible, ready harness + adapter + materializer. Detected-but-not-ready, missing, incompatible, or ambiguous installations refuse loudly; no fallback to pi. 7. Install/detect/remove operations write append-only receipts. Removal refuses while defined seats reference the harness unless those bindings are migrated first. Registry state is derived under `/harnesses/`; the reviewed catalog and adapter manifests remain in the installation/repository. This avoids a hard-coded enum while preventing arbitrary PATH executables from becoming trusted harnesses. ## 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 harness list|detect|install|rm|status ... 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. - Harness detection records recognized compatible executables without reading harness homes; incompatible/ambiguous detections refuse availability. - Harness install is exact-version/verified, Mosaic-managed, and never global; detected-but-not-ready harnesses cannot launch seats. - 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. **RESOLVED (owner, #50):** canonical harness IDs are executable names: `pi`, `claude`, `codex`, `opencode`; validation is registry/manifest-driven, not a schema enum. `mosaic harness detect/install/list/rm/status` owns the lifecycle. Remaining seam to approve: detected host executables are available but not launch-ready under the container boundary until imported/installed into a managed runtime (unless host execution receives a separate reviewed adapter). 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/harness domain or whether scripts prototype it first. Confirm managed harness packaging: harness-specific container image/bundle survives dataRoot reset; registry state and receipts remain under dataRoot. 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.