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
parent be338ee9a2
commit d61dfeb23a
33 changed files with 2157 additions and 412 deletions
@@ -8,6 +8,8 @@ use std::time::Duration;
use anyhow::Context;
use anyhow::Result;
use anyhow::bail;
#[cfg(unix)]
use codex_app_server_transport::REMOTE_CONTROL_DISABLED_ENV_VAR;
use serde::Deserialize;
use serde::Serialize;
use tokio::fs;
@@ -164,6 +166,9 @@ impl PidBackend {
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::from(stderr_log.into_std().await));
if let Some((key, value)) = self.command_env() {
command.env(key, value);
}
#[cfg(unix)]
{
@@ -407,6 +412,19 @@ impl PidBackend {
}
}
#[cfg(unix)]
fn command_env(&self) -> Option<(&'static str, &'static str)> {
match self.command_kind {
PidCommandKind::AppServer {
remote_control_enabled: false,
} => Some((REMOTE_CONTROL_DISABLED_ENV_VAR, "1")),
PidCommandKind::AppServer {
remote_control_enabled: true,
}
| PidCommandKind::UpdateLoop => None,
}
}
fn terminate_process(&self, pid: u32) -> Result<()> {
match self.command_kind {
PidCommandKind::AppServer { .. } => terminate_process(pid),
@@ -3,6 +3,8 @@ use std::time::Duration;
use pretty_assertions::assert_eq;
use tempfile::TempDir;
use codex_app_server_transport::REMOTE_CONTROL_DISABLED_ENV_VAR;
use super::PidBackend;
use super::PidCommandKind;
use super::PidFileState;
@@ -174,6 +176,24 @@ fn app_server_remote_control_uses_runtime_flag() {
);
}
#[test]
fn app_server_disabled_remote_control_uses_compatible_args_and_runtime_env() {
let backend = PidBackend::new(
"codex".into(),
"app-server.pid".into(),
/*remote_control_enabled*/ false,
);
assert_eq!(
backend.command_args(),
vec!["app-server", "--listen", "unix://"]
);
assert_eq!(
backend.command_env(),
Some((REMOTE_CONTROL_DISABLED_ENV_VAR, "1"))
);
}
#[tokio::test]
async fn read_stderr_log_tail_returns_recent_complete_lines() {
let temp_dir = TempDir::new().expect("temp dir");