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

19 KiB

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:

{
  "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:

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:

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):

<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:

{
  "providerVersion": 1,
  "id": "openai-codex",
  "kind": "native",
  "harnesses": { "pi": { "providerId": "openai-codex" } },
  "credentialTypes": ["oauth", "api_key"]
}

Custom endpoint example:

{
  "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

{
  "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:

{
  "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:

<dataRoot>/agents/<seat>/settings-selection.json

Example:

{
  "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:

<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)

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:

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.