Feat: Preserve network access on read-only sandbox policies (#13409)

## Summary

`PermissionProfile.network` could not be preserved when additional or
compiled permissions resolved to
`SandboxPolicy::ReadOnly`, because `ReadOnly` had no network_access
field. This change makes read-only + network
enabled representable directly and threads that through the protocol,
app-server v2 mirror, and permission-
  merging logic.

## What changed

- Added `network_access: bool` to `SandboxPolicy::ReadOnly` in the core
protocol and app-server v2 protocol.
- Kept backward compatibility by defaulting the new field to false, so
legacy read-only payloads still
    deserialize unchanged.
- Updated `has_full_network_access()` and sandbox summaries to respect
read-only network access.
  - Preserved PermissionProfile.network when:
      - compiling skill permission profiles into sandbox policies
      - normalizing additional permissions
      - merging additional permissions into existing sandbox policies
- Updated the approval overlay to show network in the rendered
permission rule when requested.
  - Regenerated app-server schema fixtures for the new v2 wire shape.
This commit is contained in:
Celia Chen
2026-03-03 18:41:57 -08:00
committed by GitHub
parent 2d8c1575b8
commit e6773f856c
20 changed files with 218 additions and 26 deletions

View File

@@ -641,6 +641,9 @@ fn format_additional_permissions_rule(
additional_permissions: &PermissionProfile,
) -> Option<String> {
let mut parts = Vec::new();
if matches!(additional_permissions.network, Some(true)) {
parts.push("network".to_string());
}
if let Some(file_system) = additional_permissions.file_system.as_ref() {
if let Some(read) = file_system.read.as_ref() {
let reads = read
@@ -1074,6 +1077,7 @@ mod tests {
available_decisions: vec![ReviewDecision::Approved, ReviewDecision::Abort],
network_approval_context: None,
additional_permissions: Some(PermissionProfile {
network: Some(true),
file_system: Some(FileSystemPermissions {
read: Some(vec![absolute_path("/tmp/readme.txt")]),
write: Some(vec![absolute_path("/tmp/out.txt")]),
@@ -1100,6 +1104,10 @@ mod tests {
.any(|line| line.contains("Permission rule:")),
"expected permission-rule line, got {rendered:?}"
);
assert!(
rendered.iter().any(|line| line.contains("network;")),
"expected network permission text, got {rendered:?}"
);
}
#[test]
@@ -1115,6 +1123,7 @@ mod tests {
available_decisions: vec![ReviewDecision::Approved, ReviewDecision::Abort],
network_approval_context: None,
additional_permissions: Some(PermissionProfile {
network: Some(true),
file_system: Some(FileSystemPermissions {
read: Some(vec![absolute_path("/tmp/readme.txt")]),
write: Some(vec![absolute_path("/tmp/out.txt")]),

View File

@@ -7,7 +7,7 @@ expression: "render_overlay_lines(&view, 120)"
Reason: need filesystem access
Permission rule: read `/tmp/readme.txt`; write `/tmp/out.txt`
Permission rule: network; read `/tmp/readme.txt`; write `/tmp/out.txt`
$ cat /tmp/readme.txt