Files
stack/docs/plans/2026-09-03_auth-provider-harness-registry.md
T
jason.woltje 69d1bb3aa4 docs(plan): resolve harness IDs + lifecycle review gate (#50)
Owner adjudication:
- canonical harness IDs match executables: pi, claude, codex, opencode
- agent.json uses one scalar harness ID; registry/manifest resolution, no
  hard-coded schema enum
- target mosaic harness list/detect/install/rm/status lifecycle
- detection recognizes reviewed executables and records compatibility
  without reading/copying harness homes
- installs are exact-version/verified, Mosaic-managed, never global
- detected external harness is available but not container-ready until
  imported/installed, absent a separately reviewed host adapter

Gate 1 resolved. Remaining P0/review gates stay open; no implementation
authorized. Suites 24/15/90/14/17 + verify green; unslop clean.
2026-09-04 12:26:57 -05:00

486 lines
19 KiB
Markdown

# 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 [<id>|--all]
mosaic harness install <id> [--version <exact-compatible-version>]
mosaic harness rm <id>
mosaic harness status [<id>] [--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 <id>` 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 `<dataRoot>/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 `<dataRoot>`; not configurable independently (config.json remains
the sole system config):
```text
<dataRoot>/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": "<UTC timestamp>"
}
```
`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
<dataRoot>/agents/<seat>/settings-selection.json
```
Example:
```json
{
"selectionVersion": 1,
"profile": "research-default",
"accounts": {
"openai-codex": "openai-codex/personal-openai"
},
"updatedAt": "<UTC timestamp>"
}
```
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
<dataRoot>/agents/<seat>/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 <seat>`:
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 [<seat>]` 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 <id>] [--json]
mosaic auth status [<account-ref>|--seat <seat>] [--json]
mosaic auth new
mosaic auth new --name "Homelab OpenAI" --provider openai-codex --type oauth
mosaic auth rm <provider/account>
mosaic auth login <provider/account>
mosaic auth logout <provider/account>
mosaic auth refresh [<provider/account>|--all]
mosaic auth ensure [<seat>|--all]
mosaic auth provider list [--json]
mosaic auth provider status <id> [--json]
mosaic auth provider create
mosaic auth provider create --id ollama-local --kind custom-endpoint ...
mosaic auth provider rm <id>
mosaic harness list|detect|install|rm|status ...
mosaic agent settings use <seat> <profile>
mosaic agent auth use <seat> <provider/account>
mosaic agent auth status <seat>
```
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 `<dataRoot>/auth/<account>.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 <account>` 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.