Files
stack/docs/plans/2026-09-03_auth-provider-harness-registry.md
T
jason.woltje d9a94f51ba 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.
2026-09-04 11:57:40 -05:00

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

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

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 <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 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.
  • 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.