Wire app-server user verification RPCs to the native provider (#43568)

## Why

The existing `userVerification/*` handlers always returned unavailability, leaving local clients unable to use the native verification provider.

## What changed

- Dispatch status, enrollment, deletion, and challenge signing through an account-scoped provider. Enrollment creates or reuses a local key; it does not register credentials with a server.
- Restrict enrollment, deletion, and signing to stdio and in-process connections, rejecting WebSocket and remote-control callers before parameter validation.
- Bound native work to one operation per service with a 120-second timeout, and propagate in-process caller cancellation. Recheck connection, cancellation, and authentication state before delivering results, including after waiting for response queue capacity.
- Add device-gated verification capability wiring for the experimental in-process `codex-tui` client, with automatic activation still disabled by default.

## Testing

Add tests for local key reuse and deletion, decoded challenge signing, cancellation, authentication changes during verification and response queuing, concurrent worker rejection, and stdio/WebSocket RPC behavior.

GitOrigin-RevId: 7eac15a5ffb049d27bf8fedcfbae19a49a3f6a3d
This commit is contained in:
riley-oai
2026-09-07 19:34:23 +00:00
committed by copyberry
parent daca1fab84
commit ca6fb194b6
14 changed files with 1185 additions and 23 deletions
+46 -11
View File
@@ -135,6 +135,7 @@ fn reject_removed_permission_profile(request: &JSONRPCRequest) -> Result<(), JSO
}
pub(crate) struct MessageProcessor {
user_verification: Arc<crate::user_verification::Service>,
outgoing: Arc<OutgoingMessageSender>,
models_refresh_worker: ModelsRefreshWorker,
turn_cost_worker: Option<TurnCostWorker>,
@@ -168,6 +169,7 @@ pub(crate) struct MessageProcessor {
#[derive(Debug)]
pub(crate) struct ConnectionSessionState {
pub(crate) rpc_gate: Arc<ConnectionRpcGate>,
pub(crate) origin: crate::transport::ConnectionOrigin,
pub(crate) mcp_event_streams: McpEventStreams,
initialized: OnceLock<InitializedConnectionSessionState>,
}
@@ -182,15 +184,10 @@ pub(crate) struct InitializedConnectionSessionState {
pub(crate) client_mcp_extensions: ClientMcpExtensions,
}
impl Default for ConnectionSessionState {
fn default() -> Self {
Self::new()
}
}
impl ConnectionSessionState {
pub(crate) fn new() -> Self {
pub(crate) fn new(origin: crate::transport::ConnectionOrigin) -> Self {
Self {
origin,
rpc_gate: Arc::new(ConnectionRpcGate::new()),
mcp_event_streams: McpEventStreams::default(),
initialized: OnceLock::new(),
@@ -256,6 +253,7 @@ pub(crate) struct MessageProcessorArgs {
pub(crate) config_warnings: Vec<ConfigWarningNotification>,
pub(crate) session_source: SessionSource,
pub(crate) auth_manager: Arc<AuthManager>,
pub(crate) user_verification: Arc<crate::user_verification::Service>,
pub(crate) installation_id: String,
pub(crate) code_mode_session_provider: Option<Arc<dyn CodeModeSessionProvider>>,
pub(crate) rpc_transport: AppServerRpcTransport,
@@ -281,6 +279,7 @@ impl MessageProcessor {
config_warnings,
session_source,
auth_manager,
user_verification,
installation_id,
code_mode_session_provider,
rpc_transport,
@@ -448,6 +447,7 @@ impl MessageProcessor {
Arc::clone(&config),
config_warnings.clone(),
rpc_transport,
Arc::clone(&user_verification),
);
let marketplace_processor = MarketplaceRequestProcessor::new(
Arc::clone(&config),
@@ -569,6 +569,7 @@ impl MessageProcessor {
);
Self {
user_verification,
outgoing,
models_refresh_worker,
turn_cost_worker,
@@ -671,6 +672,7 @@ impl MessageProcessor {
request: ClientRequest,
session: Arc<ConnectionSessionState>,
outbound_initialized: &AtomicBool,
cancellation: tokio_util::sync::CancellationToken,
) {
let request_id = ConnectionRequestId {
connection_id,
@@ -678,8 +680,9 @@ impl MessageProcessor {
};
let request_span =
crate::app_server_tracing::typed_request_span(&request, connection_id, &session);
let request_context =
let mut request_context =
RequestContext::new(request_id.clone(), request_span, /*parent_trace*/ None);
request_context.cancellation = cancellation;
tracing::trace!(
?connection_id,
request_id = ?request_id.request_id,
@@ -992,11 +995,43 @@ impl MessageProcessor {
ClientRequest::Initialize { .. } => {
panic!("Initialize should be handled before initialized request dispatch");
}
ClientRequest::UserVerificationStatus { .. }
request @ (ClientRequest::UserVerificationStatus { .. }
| ClientRequest::UserVerificationEnroll { .. }
| ClientRequest::UserVerificationDelete { .. }
| ClientRequest::UserVerificationVerify { .. } => {
Err(crate::user_verification::unavailable())
| ClientRequest::UserVerificationVerify { .. }) => {
return Box::pin(async {
// Keep verification temporaries out of unrelated RPCs' stack frames.
let operation = match request {
ClientRequest::UserVerificationStatus { .. } => {
crate::user_verification::Operation::Status
}
ClientRequest::UserVerificationEnroll { .. } => {
crate::user_verification::Operation::Enroll
}
ClientRequest::UserVerificationDelete { .. } => {
crate::user_verification::Operation::Delete
}
ClientRequest::UserVerificationVerify { params, .. } => {
crate::user_verification::Operation::Verify(params)
}
_ => unreachable!("matched a user-verification request"),
};
let response = self
.user_verification
.handle(
operation,
Arc::clone(&session.rpc_gate),
request_context.cancellation.clone(),
session.origin,
)
.await?;
let (payload, check) = response.into_parts();
self.outgoing
.send_response_as_checked(request_id.clone(), payload, check)
.await;
Ok(())
})
.await;
}
ClientRequest::ServerDiagnostics { .. } => Ok(Some(read_server_diagnostics().into())),
ClientRequest::ConfigRead { params, .. } => self