Files
codex/codex-rs/state/src/runtime/device_key.rs
T
Ruslan Nigmatullin 19badb0be2 app-server: persist device key bindings in sqlite (#19206)
## Why

Device-key providers should only own platform key material. The
account/client binding used to authorize a signing payload is app-server
state, and keeping that state in provider-specific metadata makes the
same check harder to audit and harder to share across platform
implementations.

Persisting the binding in the shared state database gives the device-key
crate a platform-neutral source of truth before it asks a provider to
sign. It also lets app-server move potentially blocking key operations
off the main message processor path, which matters once providers may
wait for OS authentication prompts.

## What changed

- Add a `device_key_bindings` state migration plus `StateRuntime`
helpers keyed by `key_id`.
- Add an async `DeviceKeyBindingStore` abstraction to `codex-device-key`
and use it from `DeviceKeyStore::create` and `DeviceKeyStore::sign`.
- Keep provider calls behind async store methods and run the synchronous
provider work through `spawn_blocking`.
- Wire app-server device-key RPC handling to the SQLite-backed binding
store and spawn response/error delivery tasks for device-key requests.
- Run the turn-start tracing test on the existing larger current-thread
test harness after the larger async surface made the default test stack
too small locally.

## Validation

- `cargo test -p codex-device-key`
- `cargo test -p codex-state device_key`
- `cargo test -p codex-state`
- `cargo test -p codex-app-server device_key`
- `cargo test -p codex-app-server
message_processor::tracing_tests::turn_start_jsonrpc_span_parents_core_turn_spans`
- `cargo test -p codex-app-server`
- `just fix -p codex-device-key`
- `just fix -p codex-state`
- `just fix -p codex-app-server`
- `just bazel-lock-update`
- `just bazel-lock-check`
- `git diff --check`
2026-04-23 21:55:56 -07:00

67 lines
1.6 KiB
Rust

use super::*;
/// Persisted account/client binding for a generated device key.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeviceKeyBindingRecord {
pub key_id: String,
pub account_user_id: String,
pub client_id: String,
}
impl StateRuntime {
pub async fn get_device_key_binding(
&self,
key_id: &str,
) -> anyhow::Result<Option<DeviceKeyBindingRecord>> {
let row = sqlx::query(
r#"
SELECT key_id, account_user_id, client_id
FROM device_key_bindings
WHERE key_id = ?
"#,
)
.bind(key_id)
.fetch_optional(self.pool.as_ref())
.await?;
row.map(|row| {
Ok(DeviceKeyBindingRecord {
key_id: row.try_get("key_id")?,
account_user_id: row.try_get("account_user_id")?,
client_id: row.try_get("client_id")?,
})
})
.transpose()
}
pub async fn upsert_device_key_binding(
&self,
binding: &DeviceKeyBindingRecord,
) -> anyhow::Result<()> {
let now = Utc::now().timestamp();
sqlx::query(
r#"
INSERT INTO device_key_bindings (
key_id,
account_user_id,
client_id,
created_at,
updated_at
) VALUES (?, ?, ?, ?, ?)
ON CONFLICT(key_id) DO UPDATE SET
account_user_id = excluded.account_user_id,
client_id = excluded.client_id,
updated_at = excluded.updated_at
"#,
)
.bind(&binding.key_id)
.bind(&binding.account_user_id)
.bind(&binding.client_id)
.bind(now)
.bind(now)
.execute(self.pool.as_ref())
.await?;
Ok(())
}
}