Enable user verification for local Codex Desktop sessions (#44613)

## What changed

Allow app-server to advertise `openai/elicitation.userVerification` and route verification requests to local stdio clients named `Codex Desktop` when `experimentalApi` is enabled and the device supports verification. Preserve support for the in-process `codex-tui` client.

Document the experimental verification API, local credential lifecycle, transport restrictions, and GUI requirements for cancellation and late proofs.

## Testing

Extend activation tests to cover desktop capability advertisement and request eligibility, including rejection for other transports, unrecognized client names, missing opt-in, and unsupported devices.

GitOrigin-RevId: ba4fd4b83b9c24541df3a490396d6c715e7b7433
This commit is contained in:
riley-oai
2026-09-10 18:18:31 +00:00
committed by copyberry
parent 196964ef10
commit 3715bf4100
3 changed files with 93 additions and 5 deletions

View File

@@ -44,6 +44,67 @@ after a client tries to archive or delete it.
After the owner releases the worker, its saved conversation can be archived or
deleted normally. Ordinary client-controlled threads keep their existing behavior.
## User verification (experimental)
Codex app-server advertises `openai/elicitation.userVerification` to the
host-owned plugin service for bundled, in-process TUI sessions (`codex-tui`) and
local stdio desktop sessions (`Codex Desktop`) on devices with supported biometric
hardware and the `experimentalApi` opt-in. This is an app-server decision,
independent of whether a key exists; TUI/Desktop/mobile do not advertise this MCP
capability. Mobile integration requires a separate rollout. Other clients and
network connections do not receive this mode, even with a recognized client name.
Before sending verification requests to desktop sessions, deploy a GUI that
handles the typed verification request, cancellation, and late proofs. The general
`experimentalApi` opt-in does not identify a compatible GUI version.
Local UI clients use five methods. They require the existing
`experimentalApi` opt-in. The local provider reports
`unavailable/providerUnavailable` on unsupported platforms or without the required
ChatGPT account identity.
| Method | Params | Result |
| --- | --- | --- |
| `userVerification/status` | `{}` | `{credentialId, unavailableReason, unavailableMessage}` |
| `userVerification/enroll` | `{}` | `{credentialId}` |
| `userVerification/delete` | `{}` | `{}` |
| `userVerification/verify` | `{challenge, title, description}` | `{proof: {credentialId, signature}}` |
| `userVerification/cancel` | `{requestId}` | `{}` |
Status reads local readiness without prompting or contacting a backend. A null
`unavailableReason` means local checks passed, not that registration is valid.
Unsupported platforms and missing account identity are reported in the status
response's `unavailableReason` field.
The initial enrollment creates or reuses the local key only. Backend
registration and revocation are integration TODOs; local success is not server
enrollment. Deletion currently removes that local key synchronously.
Enrollment and deletion coordinate credential lifecycle; callers do not issue
separate generate or rotate commands. Identity comes from the authenticated
account; this API exposes no caller-selected scope.
Verify signs 14096 decoded challenge bytes using P-256 ECDSA with SHA-256. The
challenge and DER signature use unpadded base64url. Title is 1256 UTF-8 bytes;
description is at most 4096 bytes. The UI obtains approval for that display
context before calling. Verify does not require a pending elicitation; a UI with
its own authenticator can return proof directly in elicitation response content.
The calling flow owns pending-request checks and discards late proofs.
Native enroll, delete, and verify accept local stdio and in-process connections.
WebSocket and remote-control peers must use their own device authenticator;
status remains available for local readiness. Dropping an embedded RPC, disconnecting,
or changing authentication cancels its native operation. Responses recheck the
captured identity after waiting for outbound queue capacity.
Canceling or resolving an elicitation does not itself stop a separate
`userVerification/verify` RPC. The GUI must use `userVerification/cancel` to
cancel that RPC and discard late proofs when an approval is canceled or resolved.
See [User verification cancellation](#user-verification-cancellation-experimental)
for request ID and acknowledgment semantics.
Only one native worker runs per app-server. If an OS call remains active after
cancellation or timeout, subsequent local operations return `failed/providerError`
until that worker exits.
Failures use the normal JSON-RPC error envelope with closed `{type, reason}` data:
`invalidRequest`, `unavailable`, `cancelled`, or `failed`. UI clients branch on
these values rather than message text. Native diagnostic payloads stay private.
# Amazon Bedrock authentication
If `model_providers.amazon-bedrock.aws.credential_export` is configured, Bedrock setup and

View File

@@ -14,6 +14,7 @@ use codex_protocol::mcp::OPENAI_ELICITATION_EXTENSION_ID;
use super::*;
use crate::message_processor::ConnectionSessionState;
use crate::message_processor::InitializedConnectionSessionState;
use crate::transport::ConnectionOrigin;
const NON_ORIGINATING_CLIENT_NAMES: &[&str] = &["codex_app_server_daemon", "codex-backend"];
@@ -95,14 +96,14 @@ impl InitializeRequestProcessor {
"Invalid clientInfo.name: '{name}'. Must be a valid HTTP header value."
)));
}
// The bundled TUI shares this build and implements the typed verification UI.
// Independently deployed UIs need their own rollout before receiving this mode.
// Activate only the embedded TUI and local desktop host. Client-supplied
// extensions cannot opt other hosts into verification.
let user_verification_enabled = experimental_api_enabled
&& matches!(
session.origin,
crate::transport::ConnectionOrigin::InProcess
(session.origin, name.as_str()),
(ConnectionOrigin::InProcess, "codex-tui")
| (ConnectionOrigin::Stdio, "Codex Desktop")
)
&& name == "codex-tui"
&& tokio::task::spawn_blocking(self.user_verification.device_supported)
.await
.unwrap_or(false);

View File

@@ -26,6 +26,32 @@ async fn user_verification_initialize_owns_advertisement_and_eligibility() -> Re
(ConnectionOrigin::InProcess, "other-ui", true, true, false),
(ConnectionOrigin::InProcess, "codex-tui", false, true, false),
(ConnectionOrigin::InProcess, "codex-tui", true, false, false),
(ConnectionOrigin::Stdio, "Codex Desktop", true, true, true),
(
ConnectionOrigin::InProcess,
"Codex Desktop",
true,
true,
false,
),
(
ConnectionOrigin::WebSocket,
"Codex Desktop",
true,
true,
false,
),
(
ConnectionOrigin::RemoteControl,
"Codex Desktop",
true,
true,
false,
),
(ConnectionOrigin::Stdio, "other-ui", true, true, false),
(ConnectionOrigin::Stdio, "codex_desktop", true, true, false),
(ConnectionOrigin::Stdio, "Codex Desktop", false, true, false),
(ConnectionOrigin::Stdio, "Codex Desktop", true, false, false),
] {
let probe: fn() -> bool = if supported { || true } else { || false };
let mut h = Harness::new(origin, probe).await?;