Files
codex/codex-rs/state/src/runtime/device_key_tests.rs
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

90 lines
2.7 KiB
Rust

use super::DeviceKeyBindingRecord;
use super::StateRuntime;
use super::test_support::unique_temp_dir;
use pretty_assertions::assert_eq;
#[tokio::test]
async fn device_key_binding_round_trips_by_key_id() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let first = DeviceKeyBindingRecord {
key_id: "dk_tpm_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".to_string(),
account_user_id: "account-user-a".to_string(),
client_id: "cli_a".to_string(),
};
let second = DeviceKeyBindingRecord {
key_id: "dk_tpm_BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB".to_string(),
account_user_id: "account-user-b".to_string(),
client_id: "cli_b".to_string(),
};
runtime
.upsert_device_key_binding(&first)
.await
.expect("insert first binding");
runtime
.upsert_device_key_binding(&second)
.await
.expect("insert second binding");
assert_eq!(
runtime
.get_device_key_binding(&first.key_id)
.await
.expect("load first binding"),
Some(first)
);
assert_eq!(
runtime
.get_device_key_binding("dk_tpm_missing")
.await
.expect("load missing binding"),
None
);
let _ = tokio::fs::remove_dir_all(codex_home).await;
}
#[tokio::test]
async fn device_key_binding_upsert_updates_existing_binding() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let key_id = "dk_tpm_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".to_string();
runtime
.upsert_device_key_binding(&DeviceKeyBindingRecord {
key_id: key_id.clone(),
account_user_id: "account-user-a".to_string(),
client_id: "cli_a".to_string(),
})
.await
.expect("insert binding");
runtime
.upsert_device_key_binding(&DeviceKeyBindingRecord {
key_id: key_id.clone(),
account_user_id: "account-user-b".to_string(),
client_id: "cli_b".to_string(),
})
.await
.expect("update binding");
assert_eq!(
runtime
.get_device_key_binding(&key_id)
.await
.expect("load updated binding"),
Some(DeviceKeyBindingRecord {
key_id,
account_user_id: "account-user-b".to_string(),
client_id: "cli_b".to_string(),
})
);
let _ = tokio::fs::remove_dir_all(codex_home).await;
}