Skip to content

Cockpit: model-routing editor — validated gates.json writes, auto-committed, loopback-only #209

Description

@robercano

Blocked by #206

Context

The cockpit is read-only by contract. v0.5.0 makes exactly one surgical exception: editing model-per-role routing from the UI. The write target is the adapter's existing routing knobs — gates.jsonbudget.orchestrator_model, budget.worker_model, budget.explorer_model, budget.reviewer_model, and budget.reviewer_models.<lens>. Everything else in gates.json (gates, lenses, consensus, merge policy, budgets, protected paths, …) stays read-only — this is NOT a general config editor, and the server must enforce that, not the UI.

Because gates.json is versioned and reviewed like code, edits must land as git commits, not silent file mutations.

Proposal

  1. POST /api/config/models in cockpit-serve.sh, accepting only the five routing keys above. Strict server-side allowlist: any other key → 400, file untouched. Values validated against known model aliases/ids (haiku / sonnet / opus / full claude-* ids); anything else → 400. Malformed body → 400.
  2. On accept: read-modify-write gates.json preserving key order and formatting (patch the parsed tree, don't re-serialize wholesale), then git commit that single file on the current branch with a clearly attributed message, e.g. chore(cockpit): route reviewer:security -> opus plus a via-cockpit trailer. No push in this endpoint — the owner's normal flow (or the loop's) carries it. The armed loop picks the change up on its next tick since it reads the local checkout.
  3. Dirty-tree guard: refuse (409) if gates.json already has uncommitted local modifications — never bundle unrelated changes into the cockpit's commit.
  4. UI: the existing "Model / skill routing" table becomes editable selects prefilled from the state JSON, with a per-role save → POST → re-fetch cycle and inline error surfacing. Agent-frontmatter model: defaults render read-only alongside, labeled as plugin-owned.
  5. Security posture: write endpoints are active only while the server is bound to 127.0.0.1 (today's default). When Cloudflare exposure lands (v0.4.0: Cockpit: per-project Cloudflare exposure — cockpit-expose skill (named tunnel, Access-gated DNS, loop-scoped unit) #143), writes stay refused behind a tunnel unless origin JWT validation (Cockpit: defense-in-depth — validate Cf-Access-Jwt-Assertion at the origin when tunneled #144) is verified — enforced server-side per request, not a UI toggle. Localhost read-only mode via an env/flag (COCKPIT_READONLY=1) for cautious setups.

Acceptance criteria

  • A valid POST updates exactly the requested keys in gates.json, commits only that file, and the commit message attributes the cockpit; the routing table reflects the new values after re-fetch.
  • Non-allowlisted key, unknown model value, or malformed body → 400 with a machine-readable error; file and git state untouched.
  • Dirty gates.json → 409, nothing committed.
  • Writes refused when not bound to loopback, and when COCKPIT_READONLY=1.
  • Tests cover accept / reject / dirty-tree / readonly paths against a temp-dir repo seam (never the real repo config), fixtures-only, no network.

Notes

Metadata

Metadata

Assignees

No one assigned

    Labels

    backlogFiled, not yet approved by the owner - the loop must NOT pick it upenhancementNew feature or requestmodule:harnessOrchestrator machinery under .claudepriority:highNext in line

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions