Repository navigation
feat: incremental policy updates via openshell policy update #825
Description
Originally asked for in #768
Problem Statement
The only way to modify a sandbox network policy today is full replacement via openshell policy set --file policy.yaml. Adding a single endpoint means editing a YAML file, re-uploading the entire policy, and hoping you didn't break something. There's no way to make targeted changes.
Meanwhile, the mechanistic mapper generates rich L7 proposals (method, path, protocol, enforcement) from observed traffic, but chunk approval silently drops the L7 fields when merging. Users approve a chunk expecting L7 inspection to be configured -- it isn't.
Proposed Design
A new openshell policy update command for targeted, incremental policy changes. All flags are repeatable and compose into a single atomic operation.
Adding Endpoints
# Basic L4 endpoint (any binary can use it)
openshell policy update my-sandbox \
--add-endpoint "ghcr.io:443"
# L7 endpoint with access preset and binary restriction
openshell policy update my-sandbox \
--add-endpoint "api.github.com:443:read-write:rest:enforce" \
--binary "/usr/bin/gh" \
--binary "/usr/bin/curl"Format: host:port[:access[:protocol[:enforcement]]]
- Defaults: no access preset (L4-only), no protocol, no enforcement
--binaryis optional -- if omitted, any process in the sandbox can use the endpoint--rule-nameoptionally controls which named rule the endpoint is added to (auto-generated if omitted)
Adding Deny Rules
Block specific operations on endpoints that otherwise have broad access. Deny rules take precedence over allow rules (shipped in #822).
openshell policy update my-sandbox \
--add-deny "api.github.com:443:POST:/repos/*/pulls/*/reviews" \
--add-deny "api.github.com:443:PUT:/repos/*/branches/*/protection"Format: host:port:METHOD:path_glob
The endpoint must already have L7 inspection enabled (protocol: rest or sql). If not, the command errors with a suggestion to add the endpoint with L7 first.
Adding Allow Rules
Add fine-grained L7 allow rules to an existing endpoint. When the endpoint uses an access preset (like read-only), the preset is automatically expanded to explicit rules before merging.
# Start with read-only, then selectively allow POST on specific routes
openshell policy update my-sandbox \
--add-allow "api.github.com:443:POST:/repos/*/issues"If the endpoint had access: read-only, this expands to:
GET /**,HEAD /**,OPTIONS /**(from the preset)POST /repos/*/issues(the new rule)
The access field is replaced by explicit rules and the CLI warns about the expansion.
Removing Endpoints
openshell policy update my-sandbox \
--remove-endpoint "old-api.example.com:443"If the rule has no endpoints remaining after removal, the entire rule is removed. Removing a non-existent endpoint is a no-op (idempotent).
Removing Named Rules
openshell policy update my-sandbox \
--remove-rule "allow_old_api_example_com_443"Compound Operations
Multiple flags compose into one atomic update:
openshell policy update my-sandbox \
--add-endpoint "api.github.com:443:read-write:rest:enforce" \
--binary "/usr/bin/gh" \
--add-deny "api.github.com:443:POST:/repos/*/pulls/*/reviews" \
--add-deny "api.github.com:443:PUT:/repos/*/branches/*/protection" \
--remove-endpoint "old-api.example.com:443"Dry Run
Preview what would change without applying:
openshell policy update my-sandbox \
--add-endpoint "ghcr.io:443:read-only" \
--dry-runWait for Sandbox
Block until the sandbox loads the updated policy:
openshell policy update my-sandbox \
--add-endpoint "ghcr.io:443" \
--wait --timeout 60Full Flag Reference
| Flag | Description | Repeatable |
|---|---|---|
--add-endpoint <spec> |
Add a network endpoint. host:port[:access[:protocol[:enforcement]]] |
Yes |
--remove-endpoint <spec> |
Remove an endpoint by host:port |
Yes |
--add-deny <spec> |
Add a deny rule. host:port:METHOD:path |
Yes |
--add-allow <spec> |
Add an allow rule. host:port:METHOD:path |
Yes |
--remove-rule <name> |
Remove an entire named rule | Yes |
--binary <path> |
Binary to associate with --add-endpoint |
Yes |
--rule-name <name> |
Target rule name for --add-endpoint (auto-generated if omitted) |
No |
--dry-run |
Show what would change without applying | No |
--wait |
Wait for sandbox to load the updated policy | No |
--timeout <secs> |
Timeout for --wait (default: 60) |
No |
Alternatives Considered
- YAML patching: Let users submit partial YAML that gets merged. More flexible but harder to validate and harder to express in a CLI.
- Separate RPCs per operation: One RPC for add-endpoint, another for remove, etc. More proto surface area for the same functionality. Rejected in favor of a batched approach.
Agent Investigation
Explored the current merge_chunk_into_policy() implementation in crates/openshell-server/src/grpc/policy.rs. Confirmed that L7 fields (protocol, enforcement, rules, deny_rules) are silently dropped when merging into existing endpoints. The mechanistic mapper correctly populates these fields in proposals, but the merge function only copies binaries and allowed_ips. This is the core gap that this feature addresses.
Also explored the UpdateConfigRequest proto -- extending it with a merge_operations field is additive and backward-compatible.
Activity