diff --git a/docs/oidc.md b/docs/oidc.md index 5d4f1998..4a12e179 100644 --- a/docs/oidc.md +++ b/docs/oidc.md @@ -144,13 +144,43 @@ the provider SDK's native credential option rather than injecting an override header. The grant mode is never inferred: missing user context or a failed OBO mint cannot switch an `entra_obo` definition to client credentials. -`entra_obo` requires `capture_user_credential = true`, the MCP encryption key, -and delegated/admin-consented permission to the audience. `entra_app` requires -`obo_grant_profile = "entra"` and a confidential-client secret; RFC 8693 -client-credentials is not implemented. Configure the permitted resource IDs in -the runtime setting `model.auth_audience_allowlist` before saving dynamic model -definitions. See [Settings](settings.md#model-backend-authentication) for -permissions, failure policy, and lane identity rules. +`entra_obo` needs the MCP encryption key, a credential captured for the driving +user, and delegated/admin-consented permission to the audience. It works under +either grant profile, with one RFC 8693 caveat: the model mint sends **no scope +parameter** (model definitions carry no per-row scopes, unlike MCP servers), so +the IdP must grant the alias's audience to the app client **by default**; on +Keycloak the exchange otherwise fails with "Requested audience not available" +(see issue #955 for the tracked fix). Turning `capture_user_credential` off +later stops *new* captures but does not invalidate credentials already stored, +so existing users keep minting. `entra_app` requires `obo_grant_profile = +"entra"` and a confidential-client secret; RFC 8693 client-credentials is not +implemented. Configure the permitted resource IDs in the runtime setting +`model.auth_audience_allowlist` before saving dynamic model definitions. +De-listing an audience later blocks every write that would arm or re-aim a +definition at it, but does not stop aliases already configured from minting — +disabling the row (the `admin.models` disarm lever) is what stops minting. See +[Settings](settings.md#model-backend-authentication) for permissions, failure +policy, and lane identity rules. + +An unrecognised `obo_grant_profile` is warned about at startup and **rejected +at the write choke points**: configuring an `oauth_obo` MCP server or a dynamic +model alias returns a 400 that echoes the configured value, so the typo is the +diagnosis. At runtime an unknown profile never mints — the mint legs resolve by +exact name; the full cause detail is logged once per audience, and every +affected call still logs its per-turn fallback or refusal naming the alias, +the target audience, and the last recorded cause (`cause=` — for example +`unsupported_grant_profile` or `oidc_not_enabled`) — so a pre-existing row +degrades loudly, with the reason visible mid-incident even after the +once-per-process line has rotated out of retained logs, rather than silently +swapping per-user attribution for the shared static key. + +The `[security]` token encryption key is deployment-wide, not per-host: rows are +encrypted with `MultiFernet` and carry no key id, so every host that reads them +needs the same keyring. That includes the console, which mints for +coordinator-hosted sessions. A node that needs the key and lacks it refuses to +start; the console starts but withholds its coordinator subsystem and shows +the key requirement as the remediation error instead of failing silently at +call time. ### config.toml alternative diff --git a/docs/settings.md b/docs/settings.md index dbc8b1bf..aec3f955 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -67,9 +67,38 @@ Model definitions support three backend credential modes: Dynamic modes require an exact `obo_audience` resource App ID URI. Before an admin can save one, an operator must add that literal audience to `model.auth_audience_allowlist` (comma- or newline-separated). Wildcards and -base-URL host matching are intentionally unsupported. Changing dynamic auth, -its audience, or the gateway `base_url` also requires `admin.mcp`; service -tokens do not bypass this capability-escalation gate. +base-URL host matching are intentionally unsupported, and a row whose +effective mode is `static` refuses to store a new non-empty `obo_audience` on +either create or update — an audience cannot be staged for a later flip +(clearing a stale value, or re-saving it unchanged, stays allowed). On a row +that is (or becomes) dynamic, every change except the tuning fields — context +window, temperature, max tokens, reasoning effort, and the two +reasoning-persistence toggles — also requires `admin.mcp`; service tokens do +not bypass this capability-escalation gate. The one exception is +de-escalation: a save whose only gated change is switching `enabled` off is a +pure disable, needs only `admin.models`, and skips validation — a de-listed +audience must never block disarming its own row. The gate is deny-by-default: +a field counts as auth-relevant unless it is provably neutral, so re-enabling +a disabled dynamic row, re-pointing its `base_url`, or swapping its provider +or alias all escalate. + +Validation runs in two tiers, matching the MCP `oauth_obo` write rules. Row +validity — the audience is allow-listed — applies to every gated write that +touches a dynamic configuration, so a revoked audience can be neither silently +re-pointed at a new `base_url` nor re-armed by an enable flip. Deployment +posture — the token encryption key installed, single sign-on configured, and +the grant profile valid and able to carry the mode — is checked when a write +*chooses* the mode/audience pair and when it re-enables a disabled dynamic +row (arming is the flip that resumes minting, so it must meet what minting +needs); other edits to an existing row stay open if the deployment's posture +changed after it was saved (its mints warn at runtime instead). Refusals name +their cause and echo the configured value. + +One asymmetry to be aware of: the write path counts a transient discovery +outage (`enabled=false`, retryable) as configured, but the mints themselves +require discovery to have completed — a config saved during an outage starts +minting only once any authenticated request heals discovery. Until then calls +warn and follow the fail-open/fail-closed policy above. `entra_app` is supported only with `[oidc] obo_grant_profile = "entra"`. Judge, output-guard, perception, utility, and sub-agent lanes inherit the diff --git a/scripts/livepass.py b/scripts/livepass.py index e3ce135f..9eff7ec3 100755 --- a/scripts/livepass.py +++ b/scripts/livepass.py @@ -405,6 +405,28 @@ CONSOLE_TEMPLATE = """
+ +