Allow approved escalation with environment-owned network policies (#46499)

## Why

Environment-owned network policies rejected explicit sandbox escalation before command approval, and retained terminals that bypassed or no longer matched those policies required a new terminal.

## What changed

- Allow `require_escalated` commands through the normal approval flow and bypass managed network proxies when full escalation is permitted.
- Preserve denied-read restrictions, including the sandbox and network proxy needed to enforce them.
- Track the network restrictions bypassed at launch and require escalation review for terminal input when launch permissions or network settings warrant it, instead of rejecting input outright.

## Testing

Extend network approval coverage for approved and denied escalation, unproxied remote execution, and preserved denied-read restrictions. Add retained-terminal coverage verifying command and `write_stdin` approvals with restricted and unrestricted filesystems, and update the unit test for changed environment network policies to expect escalation review.

GitOrigin-RevId: 50524b1bc4e3df58447c3c92fb9e50e69ed50cf8
This commit is contained in:
sayan-oai
2026-09-18 23:06:48 +00:00
committed by copyberry
parent e497393552
commit e0f05de6e0
5 changed files with 226 additions and 47 deletions
+11 -16
View File
@@ -153,16 +153,6 @@ impl ToolOrchestrator {
);
let sandbox_config = environment.config();
let owner_network_policy = sandbox_config.network_policy.is_some();
if owner_network_policy
&& tool
.sandbox_permissions(req)
.requires_escalated_permissions()
{
return Err(ToolError::Rejected(
"attachment-owned network policy cannot be bypassed by sandbox escalation"
.to_string(),
));
}
let workspace_roots = environment.workspace_roots();
let executor_managed_process_sandbox = tool.uses_executor_managed_process_sandbox(req);
let permission_profile = environment.permission_profile();
@@ -236,8 +226,7 @@ impl ToolOrchestrator {
}
// 2) First attempt under the selected sandbox.
let unsandboxed_allowed =
!owner_network_policy && unsandboxed_execution_allowed(&file_system_sandbox_policy);
let unsandboxed_allowed = unsandboxed_execution_allowed(&file_system_sandbox_policy);
let sandbox_override = if unsandboxed_allowed {
sandbox_override_for_first_attempt(
tool.sandbox_permissions(req),
@@ -250,6 +239,8 @@ impl ToolOrchestrator {
let network_approval_spec = tool.network_approval_spec(req, tool_ctx);
// Offline owner attachments stay offline unless approved command permissions grant
// networking. Existing enabled controller proxies remain independently authoritative.
// Preserve this baseline even when escalation skips the execution proxy, so retained
// terminals still record that their launch bypassed network restrictions.
let managed_network_active = if owner_network_policy {
turn_ctx
.config
@@ -257,13 +248,17 @@ impl ToolOrchestrator {
.network
.as_ref()
.is_some_and(NetworkProxySpec::enabled)
|| network_approval_spec.as_ref().is_some_and(|spec| {
effective_network_sandbox_policy(
|| (network_approval_spec.is_some()
|| tool
.sandbox_permissions(req)
.requires_escalated_permissions())
&& effective_network_sandbox_policy(
permission_profile.network_sandbox_policy(),
spec.trigger.additional_permissions.as_ref(),
network_approval_spec
.as_ref()
.and_then(|spec| spec.trigger.additional_permissions.as_ref()),
)
.is_enabled()
})
} else {
turn_ctx.network.is_some()
};
@@ -229,9 +229,12 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecAttempt> for UnifiedExecRunt
req.sandbox_permissions,
&file_system_sandbox_policy,
);
let network =
managed_network_for_sandbox_permissions(req.network.as_ref(), sandbox_permissions)
.cloned();
// Explicit full escalation bypasses controller and attachment-owned network proxies.
// Denied-read restrictions above can still require a sandboxed launch.
if sandbox_permissions.requires_escalated_permissions() {
return None;
}
let network = req.network.clone();
// No-proxy fast path; owners still need a spec for execution-only proxies.
if network.is_none() && req.turn_environment.config().network_policy.is_none() {
return None;
@@ -115,13 +115,6 @@ impl TerminalPermissions {
baseline: &PermissionProfile,
) -> Result<SandboxPermissions, &'static str> {
let bypassed = self.launch_permissions.requires_escalated_permissions();
if current.environment_network.is_some()
&& (bypassed || self.policy.environment_network != current.environment_network)
{
return Err(
"this terminal cannot enforce the current environment-owned network restrictions; start a new terminal",
);
}
// Approval cannot retrofit denied reads onto a running process. Unless
// its sandbox still matches, start a new terminal under the current policy.
if baseline
@@ -96,7 +96,7 @@ fn denied_reads_reject_file_system_changes_but_only_review_network_changes() {
}
#[tokio::test]
async fn captured_network_changes_require_review_or_a_new_terminal() -> anyhow::Result<()> {
async fn captured_network_changes_require_review() -> anyhow::Result<()> {
let (_session, mut turn) = make_session_and_context().await;
let mut environment = turn.environments.primary().expect("environment").clone();
environment.config_mut().permission_profile =
@@ -151,9 +151,7 @@ async fn captured_network_changes_require_review_or_a_new_terminal() -> anyhow::
);
assert_eq!(
permissions.review_requirement(&current, environment.permission_profile()),
Err(
"this terminal cannot enforce the current environment-owned network restrictions; start a new terminal"
)
Ok(SandboxPermissions::RequireEscalated)
);
Ok(())
}