# Model Access Plans Model access plans are daemon-owned, per-workspace configurations for routing supported agent runtimes through a named model endpoint. A plan owns its wire protocol, endpoint, encrypted credential, model catalog, default model, detection state, and enabled state. This slice owns Plans. `WorkspaceAgent` is the user-facing configuration seam that combines one Harness AgentTarget with an optional Plan/default model and instructions. The older per-AgentTarget binding and `modelpolicy` domains are compatibility-only: they remain readable for historical sessions and deletion protection, but Desktop Settings must not expose them as parallel writable configuration. ## Ownership - `services/tuttid/biz/modelplan` owns the Plan model and validation. - `services/tuttid/data/workspace` owns Plan persistence and retains legacy binding persistence during the compatibility window. - `services/tuttid/service/modelplan` owns CRUD, staged endpoint detection, and deletion protection. - `services/tuttid/service/workspaceagent` owns every new Harness-to-Plan mapping. `services/tuttid/service/modelbinding` is a legacy adapter and reference source; Desktop no longer calls its write routes. - `packages/agent/daemon/providerregistry` declares whether a provider runtime accepts a model-plan endpoint, which protocol it consumes, and whether it requires the Responses-to-Chat gateway adapter. - `packages/agent/runtimeprep` contains the provider-specific endpoint adapters. Codex and Tutti Agent receive session-scoped Responses provider configurations pointed at the daemon's loopback Model Gateway; Claude Code receives its supported environment contract; OpenCode receives a session-scoped `opencode.json` provider block via `OPENCODE_CONFIG` (credential travels only as `TUTTI_MODEL_PLAN_API_KEY` with an `{env:…}` reference in the file). - `services/tuttid/service/modelgateway` owns the local `Responses API ↔ Chat Completions API` transport adapter. It does not define session lifecycle, expose a public daemon API, or act as a general multi-provider protocol IR. - `services/tuttid/service/agent` resolves the WorkspaceAgent and supplies its Plan endpoint to runtime preparation. Unsnapshotted historical sessions may still use the isolated legacy-binding fallback. - Desktop settings and AgentGUI composer surfaces consume the daemon APIs directly; they do not own plan credentials or detection state. Provider support is fail-closed. A provider may advertise `modelPlanBinding` only when its registry descriptor declares a protocol and a matching runtime-preparation adapter exists. OpenCode consumes `openai` plans through that path; Cursor currently keeps its provider-native credential source. Provider catalog identity carries `modelPlanProtocol` so desktop resolves protocols through the catalog instead of provider-identity switches. ## Request And Runtime Flow 1. A client creates a Plan through the OpenAPI-defined daemon route. Saving does not require or trigger detection; the user may explicitly run the connection check on any saved Plan. 2. The user explicitly creates a WorkspaceAgent by choosing one Harness and, optionally, one Plan/default model. Saving a Plan never creates the Harness × Plan Cartesian product. 3. Before a new session starts, tuttid resolves that WorkspaceAgent and validates its requested model against the Plan catalog. 4. Runtime preparation injects the endpoint and credential only into the session-scoped provider environment/configuration. Codex and Tutti Agent receive a temporary gateway token instead of the upstream Plan credential. Credentials are never returned by the API or written into generated instructions and manifests. Disabling a plan prevents new sessions from using it; existing running sessions are not interrupted. Deleting a plan is rejected while any consumer still references it: agent target bindings and model-usage policies both count. The references API returns `agent_target` and `model_policy` entries, each carrying the consumer's role (bindings report `default`; policies report `execution`, `planning`, or `review`). Symmetrically, deleting a model-usage policy is rejected while any agent binding still references it; rebind or clear those bindings first. ## Product Availability Model Plan reads and writes are generally available and do not depend on desktop preferences. Desktop always exposes Plans in an independent top-level Model tab and Custom Agents under Agent. Historical `lab.modelPlans` and `lab.workspaceAgents` preference values are ignored. ## Credential Ownership - Plan credentials are AES-256-GCM encrypted at rest in `model_plans` (`api_key_ciphertext`), sharing the managed-credential key derivation. - API responses expose only `hasApiKey`. For Responses-gateway consumers (currently Codex and Tutti Agent), the upstream credential remains in daemon memory and only a random 256-bit gateway token enters `TUTTI_MODEL_PLAN_API_KEY`. Other supported runtimes continue to receive their provider-specific session environment, and the legacy workspace-app grant broker remains a separate credential egress. - Credentials must never appear in logs, events, timeline payloads, detection results, or generated provider instructions. `runtimeprep` writes the Codex-compatible provider table with `env_key`, never the key value. The gateway also redacts the exact upstream credential from forwarded error bodies. ## Staged Detection `modelplan.Service.Detect` runs four daemon-verifiable stages in order — `network → auth → model_discovery → inference` — each with a machine-readable `failureReason`/`remedy` code. A Plan becomes `ready` when all four stages pass or are explicitly skipped. Saving a new or changed Plan never runs this check and never blocks on its result: a new Plan starts `undetected`, and connection changes reset it to `undetected` until the user runs the saved-row check. Providers without a `/models` catalog (404 on every candidate) keep working: discovery is `skipped` when manual models exist and the inference stage becomes the authoritative credential check. Provider discovery is a candidate catalog, not a user selection. When an endpoint-backed draft has no selected model but discovery returns candidates, detection may use the first candidate only as the ephemeral inference target for that run. The tested id is recorded in the detection snapshot, while the Plan's `models` and `defaultModel` remain unchanged until the user selects one. `official_subscription` uses a different adapter behind the same stages. The provider registry chooses the one native runtime for each protocol (Codex for OpenAI and Claude Code for Anthropic). Detection checks installation/runtime, the provider's existing login, provider-native model discovery, and one hidden minimal inference call. It never accepts a Base URL/API key or returns native credentials. Endpoint-backed templates continue to use the HTTP detection path. ## Runtime Injection Chain ```text WorkspaceAgent primary Plan/default model -> agent.Service.resolveModelPlanEndpoint (Create + prepareRuntime) -> runtimeprep.PrepareInput.ModelEndpoint -> Codex / Tutti Agent: agent.Service registers workspace/session route in loopback Model Gateway -> provider preparer writes local /v1 + temporary env_key + wire_api=responses -> gateway translates POST /v1/responses to upstream /v1/chat/completions ClaudeCodePreparer: ANTHROPIC_BASE_URL + ANTHROPIC_API_KEY|ANTHROPIC_AUTH_TOKEN OpenCodePreparer: session opencode.json provider block via OPENCODE_CONFIG -> provider runtime speaks to the plan endpoint for the whole session ``` Rules: - Protocol compatibility is explicit: `codex`/`tutti-agent`/`opencode` consume `openai` plans, `claude-code` consumes `anthropic` plans. Cursor keeps provider-native credentials (no endpoint injection) — no fake UI entry points. - Anthropic-protocol credential injection follows the endpoint's Claude Code contract, not a blanket “official versus relay” rule. `api.anthropic.com` and Kimi Coding (`api.kimi.com`) require `ANTHROPIC_API_KEY`/`x-api-key`; other relay-style endpoints keep the bearer `ANTHROPIC_AUTH_TOKEN` default. Runtime preparation explicitly blanks the opposite credential variable so inherited shell or Claude settings cannot override the Plan's auth shape. Detection and the launched Agent must use equivalent authentication shapes, or a plan can pass inference detection while every real Agent turn gets 401. - Model addressing is a second registry strategy (`ModelPlanModelAddressing`): OpenCode declares `provider_prefixed`, so its composer/settings values carry the injected `tutti-model-plan/` namespace resolved against the session-scoped provider block. Other providers consume raw plan model ids; validation strips the namespace back to plan-domain ids. - Disabled or protocol-mismatched plans fall back to the provider-native credential source with a structured log, never a broken session. - A plan-bound session validates requested models against the plan's model list (`validateModelAgainstPlan`), not provider catalogs. - Responses-gateway routes are in-memory runtime resources keyed by workspace/session. Create failure, deletion, legacy cleanup paths, and resume replacement revoke the previous token. Before each new ordinary Turn, a plan-bound Session compares its snapshotted revision with the current revision of that exact Plan. A change prepares a replacement runtime from the new immutable revision, atomically swaps the durable snapshot, and only then admits the Turn. Guidance and an already-running Turn keep their existing binding. - The Responses-to-Chat gateway is bound to `127.0.0.1:0` on its own listener and serves only authenticated `POST /v1/responses`. It is not mounted on the public tuttid HTTP router, so no daemon OpenAPI change is involved. - Gateway v1 supports messages, text/image input, function calls and outputs, function tools (including Codex namespace containers, with collision-safe Chat names restored on Responses output), reasoning effort/content extensions, Chat JSON/SSE, usage, cancellation, and bounded timeouts. Responses roles follow the Codex-to-Chat normalization used by cc-switch: `developer` becomes `system`, `latest_reminder` and unknown internal roles become `user`, text-only content-part arrays are joined with newlines, and all textual system instructions are merged in original order into the first Chat message. This preserves instruction precedence while supporting upstreams that only tokenize the portable role set or only allow `system` at message index zero. At the request boundary, the gateway intersects tool registrations with this translatable set: non-translatable hosted or future tool declarations are omitted before forwarding to Chat. If filtering leaves no tools, it also omits `tool_choice` and `parallel_tool_calls`. Explicit selection of a filtered tool, `required` with no remaining tool, and hosted tool call/output history remain hard errors because dropping them would change requested or already-recorded conversation semantics. Background Responses, Conversations, Realtime/WebSocket, `/responses/compact`, and non-empty `previous_response_id` are also rejected instead of dropped. - Composer options for a bound target replace provider-native model options with the plan's models (`applyModelPlanComposerOverlay`); each option carries the source plan name and `runtimeContext.modelPlan = {id, name, protocol}`. ## Model Switch Semantics - Within one plan (same credential/base URL): the model change applies to the next call of the same session — `modelSwitch` capability, existing session settings update path. - Across plans or credential sources: always a new session (the endpoint is fixed at session start). UI must present this as "will apply in a new session" and reuse the existing handoff/draft-prefill launch path; there is no pretend hot switch. - Configuration changes never touch running calls; they affect the next call that has not started. ## Acceptance Ladder And The Fixed Review Rule `modelpolicy` keeps a per-session acceptance ladder: `agent_claimed → auto_checked → user_accepted`. Only an explicit user action reaches `user_accepted` today. Automated review runs and automated ladder advancement remain deferred; do not document or UI-promise those behaviors until the later stack layer lands. Review runs must never close work without the user. Explicit in-composer “model consult” (ask another plan/model for advice from the current session) is not a product surface: do not ship composer entry points, runtime commands, timeline cards, or `@` mentions for it. Model access plans remain configurable through workspace settings and Agent/plan assignment surfaces; the composer does not select models through its `@` panel. ## Migration `model_plans_v1` backfills every legacy `managed_model_provider_credentials` row into a named plan (`mp-migrated-`), copying the ciphertext as-is. The WorkspaceAgent migration deterministically materializes legacy AgentTarget bindings as `source=legacy_binding` Agents. Legacy binding rows stay for rollback and pre-snapshot session recovery; new renderer writes are forbidden and the migration must remain idempotent. The retired `first_use_json` column and `model_plan_first_use_candidates_v1` table migration remain in the SQLite schema for downgrade compatibility. Current code does not expose, write, or reconcile first-use state. ## Reference Protection `DELETE /v1/workspaces/{id}/model-plans/{planId}` is blocked (409 `model_plan_referenced`) while WorkspaceAgents or legacy consumers reference the plan; `GET .../references` lists the consumers so the UI shows impact before edits. The resolver composes per-consumer sources (`modelbinding.Service`, `modelpolicy.Service`, and `workspaceagent.Service`). Symmetrically, `DELETE /v1/workspaces/{id}/model-policies/{policyId}` is blocked (409 `model_policy_referenced`) while any agent binding still references the policy. ## What To Avoid - Do not add a "global current model" or a second AgentTarget binding editor. New model selection is owned by explicit WorkspaceAgents. - Do not surface plan credentials to the renderer, tests, or snapshots. - Do not point Codex or Tutti Agent directly at a Chat-only Plan while declaring `wire_api = "responses"`; the loopback Model Gateway is the protocol boundary. - Do not advertise `modelPlanBinding` for providers without a real injection path. - Do not let automated review close work; the ladder tops out at `auto_checked` without the user.