feat(app-server): enforce managed remote control disable (#27961)

## Why

Managed deployments need a reliable deny gate for remote control.
Persisted enablement and explicit startup requests currently remain able
to start the transport, while the removed `features.remote_control` key
is intentionally only a compatibility no-op.

This adds a dedicated requirement that administrators can use to force
remote control off without deleting the user's persisted preference.
Removing the requirement and restarting restores the prior choice.

## What Changed

- Added top-level `allow_remote_control` requirements parsing, sourced
layer precedence, debug output, and `configRequirements/read` exposure
as `allowRemoteControl`.
- Added a typed transport policy captured from the startup requirements
snapshot. Managed disable forces the initial state to disabled and
prevents enrollment, refresh, connection, and persisted-preference
mutation.
- Rejected every `remoteControl/*` RPC before parameter deserialization
with JSON-RPC `-32600` and `remote control is disabled by managed
requirements`.
- Preserved the existing disabled status notification and the previous
behavior when the requirement is `true` or omitted.
- Regenerated app-server protocol schemas and documented the new
requirement.

## Verification

- Confirmed all remote-control RPCs, including a malformed request,
return the managed-policy error while the initial status notification
remains `disabled`.
- Confirmed explicit ephemeral startup and persisted enablement make no
backend connection and leave the SQLite preference unchanged.
- Confirmed `allow_remote_control = true` does not enable or block
remote control and `configRequirements/read` returns
`allowRemoteControl: false` for the deny policy.

Related issue: N/A (managed-policy hardening).
This commit is contained in:
Anton Panasenko
2026-06-12 20:10:12 -07:00
committed by GitHub
parent 5d7db08b61
commit b9dc3b7a8b
29 changed files with 691 additions and 38 deletions
+23
View File
@@ -196,6 +196,17 @@ fn render_debug_config_lines(
));
}
if let Some(allow_remote_control) = requirements_toml.allow_remote_control {
requirement_lines.push(requirement_line(
"allow_remote_control",
allow_remote_control.to_string(),
requirements
.allow_remote_control
.as_ref()
.map(|sourced| &sourced.source),
));
}
if requirements_toml.guardian_policy_config.is_some() {
requirement_lines.push(requirement_line(
"guardian_policy_config",
@@ -712,6 +723,10 @@ mod tests {
/*value*/ false,
RequirementSource::LegacyManagedConfigTomlFromMdm,
)),
allow_remote_control: Some(Sourced::new(
/*value*/ false,
RequirementSource::LegacyManagedConfigTomlFromMdm,
)),
feature_requirements: Some(Sourced::new(
FeatureRequirementsToml {
entries: BTreeMap::from([("guardian_approval".to_string(), true)]),
@@ -753,6 +768,7 @@ mod tests {
allowed_web_search_modes: Some(vec![WebSearchModeRequirement::Cached]),
allow_managed_hooks_only: Some(true),
allow_appshots: Some(false),
allow_remote_control: Some(false),
computer_use: None,
windows: None,
guardian_policy_config: Some("Use the managed guardian policy.".to_string()),
@@ -795,6 +811,9 @@ mod tests {
.expect("config layer stack");
let rendered = render_stack_to_text(&stack);
#[cfg(not(windows))]
insta::assert_snapshot!("debug_config_requirement_sources", rendered.as_str());
let requirements_source = (RequirementSource::LegacyManagedConfigTomlFromMdm).to_string();
assert!(rendered.contains(&format!(
"allowed_approval_policies: on-request (source: {requirements_source})"
@@ -820,6 +839,9 @@ mod tests {
assert!(rendered.contains(&format!(
"allow_appshots: false (source: {requirements_source})"
)));
assert!(rendered.contains(&format!(
"allow_remote_control: false (source: {requirements_source})"
)));
assert!(rendered.contains(&format!(
"guardian_policy_config: configured (source: {requirements_source})"
)));
@@ -1112,6 +1134,7 @@ approval_policy = "never"
allowed_web_search_modes: Some(Vec::new()),
allow_managed_hooks_only: None,
allow_appshots: None,
allow_remote_control: None,
computer_use: None,
windows: None,
guardian_policy_config: None,
@@ -0,0 +1,23 @@
---
source: tui/src/debug_config.rs
expression: rendered.as_str()
---
/debug-config
Config layer stack (lowest precedence first):
1. user (/home/alice/.codex/config.toml) (enabled)
Requirements:
- allowed_approval_policies: on-request (source: MDM managed_config.toml (legacy))
- allowed_approvals_reviewers: auto_review (source: MDM managed_config.toml (legacy))
- allowed_sandbox_modes: read-only (source: /etc/codex/requirements.toml)
- allowed_web_search_modes: cached, disabled (source: MDM managed_config.toml (legacy))
- allow_managed_hooks_only: true (source: MDM managed_config.toml (legacy))
- allow_appshots: false (source: MDM managed_config.toml (legacy))
- allow_remote_control: false (source: MDM managed_config.toml (legacy))
- guardian_policy_config: configured (source: MDM managed_config.toml (legacy))
- features: guardian_approval=true (source: MDM managed_config.toml (legacy))
- mcp_servers: docs (source: MDM managed_config.toml (legacy))
- enforce_residency: us (source: MDM managed_config.toml (legacy))
- experimental_network: enabled=true, domains={example.com=allow} (source: MDM managed_config.toml (legacy))
- permissions.filesystem.deny_read: /home/alice/.gitconfig (source: /etc/codex/requirements.toml)