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
Unverified
parent 5d7db08b61
commit b9dc3b7a8b
29 changed files with 691 additions and 38 deletions
@@ -149,6 +149,7 @@ pub struct ConfigRequirements {
pub web_search_mode: ConstrainedWithSource<WebSearchMode>,
pub allow_managed_hooks_only: Option<Sourced<bool>>,
pub allow_appshots: Option<Sourced<bool>>,
pub allow_remote_control: Option<Sourced<bool>>,
pub computer_use: Option<Sourced<ComputerUseRequirementsToml>>,
pub feature_requirements: Option<Sourced<FeatureRequirementsToml>>,
pub managed_hooks: Option<ConstrainedWithSource<ManagedHooksRequirementsToml>>,
@@ -189,6 +190,7 @@ impl Default for ConfigRequirements {
),
allow_managed_hooks_only: None,
allow_appshots: None,
allow_remote_control: None,
computer_use: None,
feature_requirements: None,
managed_hooks: None,
@@ -828,6 +830,7 @@ pub struct ConfigRequirementsToml {
pub allowed_web_search_modes: Option<Vec<WebSearchModeRequirement>>,
pub allow_managed_hooks_only: Option<bool>,
pub allow_appshots: Option<bool>,
pub allow_remote_control: Option<bool>,
pub computer_use: Option<ComputerUseRequirementsToml>,
pub windows: Option<WindowsRequirementsToml>,
#[serde(rename = "features", alias = "feature_requirements")]
@@ -882,6 +885,7 @@ pub struct ConfigRequirementsWithSources {
pub allowed_web_search_modes: Option<Sourced<Vec<WebSearchModeRequirement>>>,
pub allow_managed_hooks_only: Option<Sourced<bool>>,
pub allow_appshots: Option<Sourced<bool>>,
pub allow_remote_control: Option<Sourced<bool>>,
pub computer_use: Option<Sourced<ComputerUseRequirementsToml>>,
pub windows: Option<Sourced<WindowsRequirementsToml>>,
pub feature_requirements: Option<Sourced<FeatureRequirementsToml>>,
@@ -924,6 +928,7 @@ impl ConfigRequirementsWithSources {
allowed_web_search_modes: _,
allow_managed_hooks_only: _,
allow_appshots: _,
allow_remote_control: _,
computer_use: _,
windows: _,
feature_requirements: _,
@@ -959,6 +964,7 @@ impl ConfigRequirementsWithSources {
allowed_web_search_modes,
allow_managed_hooks_only,
allow_appshots,
allow_remote_control,
computer_use,
windows,
feature_requirements,
@@ -992,6 +998,7 @@ impl ConfigRequirementsWithSources {
allowed_web_search_modes,
allow_managed_hooks_only,
allow_appshots,
allow_remote_control,
computer_use,
windows,
feature_requirements,
@@ -1015,6 +1022,7 @@ impl ConfigRequirementsWithSources {
allowed_web_search_modes: allowed_web_search_modes.map(|sourced| sourced.value),
allow_managed_hooks_only: allow_managed_hooks_only.map(|sourced| sourced.value),
allow_appshots: allow_appshots.map(|sourced| sourced.value),
allow_remote_control: allow_remote_control.map(|sourced| sourced.value),
computer_use: computer_use.map(|sourced| sourced.value),
windows: windows.map(|sourced| sourced.value),
feature_requirements: feature_requirements.map(|sourced| sourced.value),
@@ -1104,6 +1112,7 @@ impl ConfigRequirementsToml {
&& self.allowed_web_search_modes.is_none()
&& self.allow_managed_hooks_only.is_none()
&& self.allow_appshots.is_none()
&& self.allow_remote_control.is_none()
&& self
.computer_use
.as_ref()
@@ -1156,6 +1165,7 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
allowed_web_search_modes,
allow_managed_hooks_only,
allow_appshots,
allow_remote_control,
computer_use,
windows,
feature_requirements,
@@ -1434,6 +1444,7 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
web_search_mode,
allow_managed_hooks_only,
allow_appshots,
allow_remote_control,
computer_use,
feature_requirements,
managed_hooks,
@@ -1525,6 +1536,7 @@ mod tests {
allowed_web_search_modes,
allow_managed_hooks_only,
allow_appshots,
allow_remote_control,
computer_use,
windows,
feature_requirements,
@@ -1555,6 +1567,8 @@ mod tests {
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
allow_appshots: allow_appshots
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
allow_remote_control: allow_remote_control
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
computer_use: computer_use.map(|value| Sourced::new(value, RequirementSource::Unknown)),
windows: windows.map(|value| Sourced::new(value, RequirementSource::Unknown)),
feature_requirements: feature_requirements
@@ -1688,6 +1702,19 @@ mod tests {
Ok(())
}
#[test]
fn allow_remote_control_false_is_still_configured() -> Result<()> {
let requirements: ConfigRequirementsToml = from_str(
r#"
allow_remote_control = false
"#,
)?;
assert_eq!(requirements.allow_remote_control, Some(false));
assert!(!requirements.is_empty());
Ok(())
}
#[test]
fn deserialize_computer_use_requirements() -> Result<()> {
let requirements: ConfigRequirementsToml = from_str(
@@ -1745,6 +1772,7 @@ mod tests {
allowed_web_search_modes: Some(allowed_web_search_modes.clone()),
allow_managed_hooks_only: Some(true),
allow_appshots: Some(false),
allow_remote_control: Some(false),
computer_use: Some(computer_use.clone()),
windows: None,
feature_requirements: Some(feature_requirements.clone()),
@@ -1787,6 +1815,10 @@ mod tests {
enforce_source.clone(),
)),
allow_appshots: Some(Sourced::new(/*value*/ false, enforce_source.clone(),)),
allow_remote_control: Some(Sourced::new(
/*value*/ false,
enforce_source.clone(),
)),
computer_use: Some(Sourced::new(computer_use, enforce_source.clone())),
windows: None,
feature_requirements: Some(Sourced::new(
@@ -1835,6 +1867,7 @@ mod tests {
allowed_web_search_modes: None,
allow_managed_hooks_only: None,
allow_appshots: None,
allow_remote_control: None,
computer_use: None,
windows: None,
feature_requirements: None,
@@ -1888,6 +1921,7 @@ mod tests {
allowed_web_search_modes: None,
allow_managed_hooks_only: None,
allow_appshots: None,
allow_remote_control: None,
computer_use: None,
windows: None,
feature_requirements: None,
@@ -182,6 +182,7 @@ fn populate_merged_regular_fields_with_sources(
allowed_web_search_modes,
allow_managed_hooks_only,
allow_appshots,
allow_remote_control,
computer_use,
windows,
feature_requirements,
@@ -210,6 +211,7 @@ fn populate_merged_regular_fields_with_sources(
set_sourced!(allowed_web_search_modes, &["allowed_web_search_modes"]);
set_sourced!(allow_managed_hooks_only, &["allow_managed_hooks_only"]);
set_sourced!(allow_appshots, &["allow_appshots"]);
set_sourced!(allow_remote_control, &["allow_remote_control"]);
set_sourced!(computer_use, &["computer_use"]);
set_sourced!(windows, &["windows"]);
set_sourced!(feature_requirements, &["features", "feature_requirements"]);
@@ -63,6 +63,7 @@ fn top_level_values_use_toml_priority() {
allowed_approval_policies = ["on-request"]
allowed_sandbox_modes = ["workspace-write"]
default_permissions = ":workspace"
allow_remote_control = true
[allowed_permission_profiles]
":read-only" = true
@@ -76,6 +77,7 @@ default_permissions = ":workspace"
allowed_approval_policies = ["never"]
allowed_sandbox_modes = ["read-only"]
default_permissions = ":read-only"
allow_remote_control = false
[allowed_permission_profiles]
":danger-full-access" = false
@@ -93,6 +95,7 @@ default_permissions = ":read-only"
allowed_approval_policies = ["never"]
allowed_sandbox_modes = ["read-only"]
default_permissions = ":read-only"
allow_remote_control = false
[allowed_permission_profiles]
":danger-full-access" = false
@@ -135,6 +138,7 @@ fn composition_strategy_applies_to_non_cloud_layers() {
format!(
r#"
allowed_approval_policies = ["on-request"]
allow_remote_control = true
[features]
shared = false
@@ -154,6 +158,7 @@ deny_read = [{low_path:?}]
format!(
r#"
allowed_approval_policies = ["never"]
allow_remote_control = false
[features]
shared = true
@@ -178,6 +183,7 @@ deny_read = [{high_path:?}]
expected_requirements(format!(
r#"
allowed_approval_policies = ["never"]
allow_remote_control = false
[features]
shared = true
@@ -198,7 +204,14 @@ deny_read = [{high_path:?}, {low_path:?}]
);
assert_eq!(
composed.allowed_approval_policies,
Some(Sourced::new(vec![AskForApproval::Never], mdm_source))
Some(Sourced::new(
vec![AskForApproval::Never],
mdm_source.clone()
))
);
assert_eq!(
composed.allow_remote_control,
Some(Sourced::new(/*value*/ false, mdm_source))
);
}
+5 -1
View File
@@ -73,14 +73,18 @@ impl LoaderOverrides {
}
}
/// Returns overrides with host MDM disabled and managed config loaded from `managed_config_path`.
/// Returns overrides with host MDM disabled and managed config loaded from
/// `managed_config_path`. System requirements are loaded from a sibling
/// `requirements.toml` fixture.
///
/// This is intended for tests that supply an explicit managed config fixture.
pub fn with_managed_config_path_for_tests(managed_config_path: PathBuf) -> Self {
let system_requirements_path = managed_config_path.with_file_name("requirements.toml");
Self {
user_config_path: None,
user_config_profile: None,
managed_config_path: Some(managed_config_path),
system_requirements_path: Some(system_requirements_path),
..Self::without_managed_config_for_tests()
}
}