feat(app-server): persist remote-control desired state (#27445)

## Why

Remote-control runtime enablement and persisted enrollment preference
were represented by separate flags. That made startup rehydration, RPC
persistence, and new-enrollment seeding race with one another, and it
did not cleanly distinguish runtime-only CLI or daemon starts from
durable app-server RPC changes.

## What Changed

- Replace the parallel enablement, seed, and rehydration flags with one
transport-owned `RemoteControlDesiredState`.
- Add nullable enrollment-scoped persistence and preserve existing
preferences during enrollment upserts.
- Rehydrate plain startup only after auth and client scope resolve,
without overwriting a concurrent RPC transition.
- Make ordinary `remoteControl/enable` and `remoteControl/disable`
durable while retaining `ephemeral: true` for runtime-only callers.
- Have the daemon explicitly request ephemeral enablement and regenerate
the app-server schemas.

## Verification

- Covered migration and `NULL`/`0`/`1` persistence round trips.
- Covered plain-start rehydration and runtime-only versus durable
enrollment seeding.
- Covered durable enable, durable disable, and ephemeral enable through
app-server RPC.
- Covered the daemon's exact `{ "ephemeral": true }` request payload.

Related issue: N/A (internal remote-control persistence architecture
change).
This commit is contained in:
Anton Panasenko
2026-06-11 21:28:52 -07:00
committed by GitHub
Unverified
parent be338ee9a2
commit d61dfeb23a
33 changed files with 2157 additions and 412 deletions
+100 -2
View File
@@ -11,6 +11,7 @@ pub struct RemoteControlEnrollmentRecord {
pub server_id: String,
pub environment_id: String,
pub server_name: String,
pub remote_control_enabled: Option<bool>,
}
fn remote_control_app_server_client_name_key(app_server_client_name: Option<&str>) -> &str {
@@ -34,7 +35,8 @@ impl StateRuntime {
) -> anyhow::Result<Option<RemoteControlEnrollmentRecord>> {
let row = sqlx::query(
r#"
SELECT websocket_url, account_id, app_server_client_name, server_id, environment_id, server_name
SELECT websocket_url, account_id, app_server_client_name, server_id, environment_id, server_name,
remote_control_enabled
FROM remote_control_enrollments
WHERE websocket_url = ? AND account_id = ? AND app_server_client_name = ?
"#,
@@ -56,6 +58,7 @@ WHERE websocket_url = ? AND account_id = ? AND app_server_client_name = ?
server_id: row.try_get("server_id")?,
environment_id: row.try_get("environment_id")?,
server_name: row.try_get("server_name")?,
remote_control_enabled: row.try_get("remote_control_enabled")?,
})
})
.transpose()
@@ -74,8 +77,9 @@ INSERT INTO remote_control_enrollments (
server_id,
environment_id,
server_name,
remote_control_enabled,
updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?)
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(websocket_url, account_id, app_server_client_name) DO UPDATE SET
server_id = excluded.server_id,
environment_id = excluded.environment_id,
@@ -91,12 +95,39 @@ ON CONFLICT(websocket_url, account_id, app_server_client_name) DO UPDATE SET
.bind(&enrollment.server_id)
.bind(&enrollment.environment_id)
.bind(&enrollment.server_name)
.bind(enrollment.remote_control_enabled)
.bind(Utc::now().timestamp())
.execute(self.pool.as_ref())
.await?;
Ok(())
}
pub async fn set_remote_control_enabled(
&self,
websocket_url: &str,
account_id: &str,
app_server_client_name: Option<&str>,
remote_control_enabled: bool,
) -> anyhow::Result<u64> {
let result = sqlx::query(
r#"
UPDATE remote_control_enrollments
SET remote_control_enabled = ?, updated_at = ?
WHERE websocket_url = ? AND account_id = ? AND app_server_client_name = ?
"#,
)
.bind(remote_control_enabled)
.bind(Utc::now().timestamp())
.bind(websocket_url)
.bind(account_id)
.bind(remote_control_app_server_client_name_key(
app_server_client_name,
))
.execute(self.pool.as_ref())
.await?;
Ok(result.rows_affected())
}
pub async fn delete_remote_control_enrollment(
&self,
websocket_url: &str,
@@ -125,7 +156,13 @@ mod tests {
use super::RemoteControlEnrollmentRecord;
use super::StateRuntime;
use super::test_support::unique_temp_dir;
use crate::migrations::STATE_MIGRATOR;
use crate::state_db_path;
use pretty_assertions::assert_eq;
use sqlx::SqlitePool;
use sqlx::migrate::Migrator;
use sqlx::sqlite::SqliteConnectOptions;
use std::borrow::Cow;
#[tokio::test]
async fn remote_control_enrollment_round_trips_by_target_and_account() {
@@ -143,6 +180,7 @@ mod tests {
server_id: "srv_e_first".to_string(),
environment_id: "env_first".to_string(),
server_name: "first-server".to_string(),
remote_control_enabled: Some(false),
})
.await
.expect("insert first enrollment");
@@ -155,6 +193,7 @@ mod tests {
server_id: "srv_e_second".to_string(),
environment_id: "env_second".to_string(),
server_name: "second-server".to_string(),
remote_control_enabled: Some(true),
})
.await
.expect("insert second enrollment");
@@ -176,6 +215,7 @@ mod tests {
server_id: "srv_e_first".to_string(),
environment_id: "env_first".to_string(),
server_name: "first-server".to_string(),
remote_control_enabled: Some(false),
})
);
assert_eq!(
@@ -220,6 +260,7 @@ mod tests {
server_id: "srv_e_first".to_string(),
environment_id: "env_first".to_string(),
server_name: "first-server".to_string(),
remote_control_enabled: Some(false),
})
.await
.expect("insert first enrollment");
@@ -232,6 +273,7 @@ mod tests {
server_id: "srv_e_second".to_string(),
environment_id: "env_second".to_string(),
server_name: "second-server".to_string(),
remote_control_enabled: Some(true),
})
.await
.expect("insert second enrollment");
@@ -275,9 +317,65 @@ mod tests {
server_id: "srv_e_second".to_string(),
environment_id: "env_second".to_string(),
server_name: "second-server".to_string(),
remote_control_enabled: Some(true),
})
);
let _ = tokio::fs::remove_dir_all(codex_home).await;
}
#[tokio::test]
async fn migration_preserves_legacy_remote_control_preference_as_null() {
let codex_home = unique_temp_dir();
tokio::fs::create_dir_all(&codex_home)
.await
.expect("create codex home");
let old_state_migrator = Migrator {
migrations: Cow::Owned(STATE_MIGRATOR.migrations[..36].to_vec()),
ignore_missing: false,
locking: true,
no_tx: false,
table_name: STATE_MIGRATOR.table_name.clone(),
create_schemas: STATE_MIGRATOR.create_schemas.clone(),
};
let pool = SqlitePool::connect_with(
SqliteConnectOptions::new()
.filename(state_db_path(codex_home.as_path()))
.create_if_missing(true),
)
.await
.expect("open old state db");
old_state_migrator
.run(&pool)
.await
.expect("apply old state schema");
sqlx::query("INSERT INTO remote_control_enrollments (websocket_url, account_id, app_server_client_name, server_id, environment_id, server_name, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)")
.bind("wss://example.com/backend-api/wham/remote/control/server")
.bind("account-a")
.bind("desktop-client")
.bind("srv_e_first")
.bind("env_first")
.bind("first-server")
.bind(1_i64)
.execute(&pool)
.await
.expect("insert legacy enrollment");
pool.close().await;
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let actual = runtime
.get_remote_control_enrollment(
"wss://example.com/backend-api/wham/remote/control/server",
"account-a",
Some("desktop-client"),
)
.await
.expect("load migrated enrollment")
.expect("legacy enrollment should remain");
assert_eq!(actual.remote_control_enabled, None);
let _ = tokio::fs::remove_dir_all(codex_home).await;
}
}