Skip to content

Give deployment default model providers one home and close the self-hosted model gap - #140

Merged
SaladDay merged 10 commits into
mainfrom
codex/deployment-model-providers
Sep 25, 2026
Merged

SaladDay merged 10 commits into
mainfrom
codex/deployment-model-providers

Conversation

@SaladDay

@SaladDay SaladDay commented Sep 25, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Phase 3 PR A of the new-user install work. Model credentials get one visible home, and the self-hosted model gap found in the boundary release acceptance is closed.

  • Deployment default model providers become a runtime setting, managed with the Core key:
    • GET /core/v1/harnesses lists the harnesses with enabled, default and each harness's deployment provider (safe view).
    • GET/PUT/DELETE /core/v1/harnesses/{harness}/model-provider: the PUT body is exactly x_agents_core.model_provider.
    • Keys are write-only, never returned, and encrypted with a new credential purpose bound to the harness.
    • Writes are audited with a deployment-level scope (project_id: null).
    • Migration 000076_deployment_model_providers adds the table and the nullable audit scope.
  • Precedence: Session request provider, then saved Agent provider, then deployment default.
    • The default applies to openai_hosted and to Sessions without an environment, never to self_hosted.
    • The resolved provider is frozen into the Session's encrypted snapshot at creation, so changing or removing the default never affects existing Sessions.
  • AGENTS_API_EXECUTION_OPTIONS_FILE is retired. Setting it, even empty, stops Core at startup and points to Web (System) or PUT /core/v1/harnesses/{harness}/model-provider.
  • self_hosted Sessions may carry caller model credentials, in the request or on a saved Agent. They go only to that Session's bound executor, over the existing daemon channel.
  • Fail fast: creating an openai_hosted or self_hosted Session with no resolvable provider returns 400 model_provider_required, param x_agents_core.model_provider. Before, it silently fell back to Codex's built-in provider and hung. Pending input from before the upgrade settles as failed with model_provider_required instead of looping.
  • Idempotency:
    • Retries of hosted Sessions record caller intent.
    • Sessions without an environment keep the documented default-retry equivalence; the deployment default is left out of the request hash.
    • Caller-supplied keys enter stored retry hashes only as a fingerprint keyed by the credential key.
  • Client: listHarnesses, retrieveHarnessModelProvider, setHarnessModelProvider, deleteHarnessModelProvider, CoreHarness and HarnessModelProvider, with strict projections.
  • Pre-upgrade check: deploy/install/model_provider_sessions.py is a read-only count of legacy hosted and self-hosted Sessions that have no frozen provider.

Review

  • Independent blind review of the full diff: no P0 or P1. The P2 (idempotency of Sessions without an environment) and the P3s were fixed.
  • The fix introduced a keyed fingerprint, so it got its own focused review. That found no P0 or P1. Its P2 (default-retry equivalence) and P3s were fixed in e72acdd2, verified by tests, the native verifier and the gate.

Verification

  • Full local server gate on 2cc35bed (the exact head, including main 31b64e38): passed. Playwright browser cases were skipped on the server (Chrome unavailable; approved skip).
  • Native verifier: three native-default retries return the original Session after the default is rotated and after it is removed. The test needs one local edit because of an unrelated breakage on main since Use the colocated daemon as the V1 user-managed executor #8; that is tracked separately.
  • Real check on an isolated mx1 install (Kimi only):
    • route security holds;
    • a hosted Session with no provider runs on the deployment default;
    • freezing holds when the default changes;
    • with no model configured anywhere, creation fails fast;
    • self-hosted runs work with a request provider and with a saved-Agent provider;
    • the model key is not visible inside the sandbox;
    • audit rows have a null Project;
    • the pre-upgrade count is correct.

Compatibility and ops

  • Operators using AGENTS_API_EXECUTION_OPTIONS_FILE must remove it and set deployment defaults in Web or with the new route. A one-time import through parsar apply follows with phase 1b.
  • Retry conflicts: retries of key-bearing requests first sent before this release conflict once. Key-bearing retries also conflict after a credential key change, which is not supported anyway.
  • Legacy hosted and self-hosted Sessions with no frozen provider must be recreated; run the pre-upgrade check first.
  • A dev or preview database that ran this branch while the migration was numbered 000075 must be reset.

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith with what you need. Autofix is disabled.

…osted and self-hosted Sessions

Model credentials get one visible home and the self-hosted model gap from the
fc19bd5 acceptance (D2, D3) is closed.

- Deployment default model providers are a runtime setting: one complete
  bundle per harness in PostgreSQL (migration 000075), sealed with a new
  harness-bound credential purpose, managed only with the Core key through
  GET /core/v1/harnesses and GET/PUT/DELETE
  /core/v1/harnesses/{harness}/model-provider. Reads return the safe view
  (api_key_configured, updated_at), never the key. Writes are audited as
  deployment-wide admin_audit_log entries with a null tenant/project scope.
- Provider precedence stays Session, saved Agent, deployment default. The
  default applies to openai_hosted and none Sessions and is frozen through the
  existing encrypted Session snapshot; later edits never reach existing
  Sessions. self_hosted Sessions now accept Session and Agent bundles, delivered
  over the existing daemon connection, and never use the deployment default.
- openai_hosted and self_hosted Sessions without a bundle are rejected at
  creation with 400 model_provider_required. Legacy rows without a snapshot are
  rejected at message admission and at dispatch instead of falling back to a
  built-in endpoint. deploy/install/model_provider_sessions.py counts them
  read-only before an upgrade.
- AGENTS_API_EXECUTION_OPTIONS_FILE is retired and stops startup with the
  replacement named. Historical snapshots keep their frozen native options.
  The execution-configuration read shows the safe view of new deployment
  selections.
- packages/agents-client: listHarnesses, retrieveHarnessModelProvider,
  setHarnessModelProvider, deleteHarnessModelProvider, CoreHarness,
  HarnessModelProvider; audit entries may have project_id null.
- Contracts, docs and the Core OpenAPI are updated. Tests that create hosted
  or self-hosted Sessions use shared provider fixtures instead of relaxing the
  check.
The error for AGENTS_API_EXECUTION_OPTIONS_FILE now says to remove the setting
and set deployment default model providers in Web (System) or with
PUT /core/v1/harnesses/{harness}/model-provider. The import command it named
does not exist yet; the import work adds it back.
…ess review

P2-1: A same-key retry of a none Session conflicted after the deployment
default changed, because the resolved provider fed the retry hash while caller
intent was recorded only for openai_hosted. Caller intent is now recorded
whenever the deployment default may apply (openai_hosted and none), and a
provider key enters any stored hash only as a fingerprint keyed by the
credential key (new Cipher.Fingerprint), never directly. Without the key, an
intent lookup cannot match and creation reports the missing key. none Sessions
now compare caller intent on retry, like hosted ones; rows without recorded
intent keep the resolved-equivalence rule.

Review follow-ups:
- Input reserved before providers were required fails with
  model_provider_required (new environment_input_reservations.failure_code,
  migration 000075) instead of waiting for its deadline, and the Session
  reports that reason.
- The 000075 down migration also refuses while deployment model providers
  exist.
- Scope wording: the deployment default applies to openai_hosted and none,
  never to self_hosted, in the public API guide, the self_hosted 400 message,
  the client JSDoc and related docs.
- Tests: none retry after the default is rotated and removed; a self-hosted
  preparation carries the frozen provider only to the bound executor; legacy
  reservations settle as failed.
- Docs: a saved Agent's provider key reaches every self_hosted executor
  created with that Agent in the Project; legacy none Sessions run with the
  device's own environment and are not counted by the pre-upgrade check.
The vault intent unit test assumed a none Session without Vaults records no
caller intent. none and openai_hosted now always record it; the unrelated
inline identity case moves to self_hosted, where the default never applies.
…l-providers

# Conflicts:
#	contracts/agents-api/v1/model_provider_admission.go
#	packages/agents-client/src/execution-configuration-projection.ts
Main's public URL binding (#138) took 000075.
…l-providers

# Conflicts:
#	contracts/agents-api/core.openapi.yaml
#	docs/api/README.md
#	packages/agents-client/src/admin-client.ts
#	packages/agents-client/src/admin-projection.ts
#	services/agents-api/HOSTED-RELEASE.md
#	services/agents-api/cmd/server/process_configuration.go
P2-1: Recording raw caller intent for none Sessions broke the documented
retry equivalence (omitted, null and explicit medium verbosity). none keeps
the resolved-request rule again; caller intent stays hosted-only as before.
The resolved hash now leaves out a deployment-sourced provider bundle and its
configuration marker, so setting, rotating or removing the default does not
change a none Session's retry identity. The earlier test edits for the
intent-for-none rule are reverted.

Review follow-ups:
- withoutProviderKey fails closed: an intent whose extension or provider
  cannot be read is rejected, and the provider is re-encoded from its typed
  value with the key replaced by its keyed fingerprint.
- Cipher.Fingerprint rejects a NUL in the purpose.
- New test: the same request under two credential keys gets different
  request_hash and creation_request_hash; an unreadable provider intent is
  rejected.
- The legacy test again asserts that a rejected submit queues nothing.
- The none retry test also retries with a spelled-out default verbosity.
- model-execution.md notes that key-bearing retries first sent before this
  release, or after a credential key change, conflict once.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant