Repository navigation
bug: reserve _provider_* policy key prefix and enforce at gateway boundaries #1982
Description
Activity
- addedarea:policyPolicy engine and policy lifecycle workPolicy engine and policy lifecycle workarea:gatewayGateway server and control-plane workGateway server and control-plane work
on Jun 23, 2026 🏗️ build-plan
Implementation Plan
Issue type:
fix
Complexity: Medium
Confidence: High — clear pathSummary
Reserve
_provider_*as a gateway-owned network policy key namespace at policy ingress. User-authored full policy writes should reject reserved keys with a targeted error, while sandbox-originated sync should strip provider-derived rules before backfilling or persisting source policy.policy get --fullshould continue showing effective provider rules.Scope
crates/openshell-policy/src/compose.rs: add/export a shared reserved-provider-rule predicate or constant; keep composition semantics unchanged.crates/openshell-policy/src/merge.rs: replace raw_provider_prefix checks with the shared predicate without changing merge behavior.crates/openshell-server/src/grpc/validation.rs: add reserved network-policy key validation and unit coverage with an error that points users to round-trippable policy output.crates/openshell-server/src/grpc/sandbox.rs: enforce reserved-key rejection forCreateSandboxpolicy payloads.crates/openshell-server/src/grpc/policy.rs: reject reserved keys for user-authored full sandbox policy updates; strip reserved keys from sandbox-principal policy sync before CAS backfill and revision persistence; keep JIT effective-policy composition unchanged.crates/openshell-sandbox/src/lib.rs: strip provider-derived network policy entries before supervisor discovery/enrichment write-back.docs/sandboxes/providers-v2.mdx: document the reserved namespace and rewrite the stale collision-suffix wording.docs/sandboxes/policies.mdx: clarify that replayingpolicy get --fullrequires removing generated_provider_*entries beforepolicy set.
Implementation Steps
- Add the shared reserved-prefix helper in
openshell-policy, then refactor existing provider-prefix checks in compose/merge and server policy code to use it. - Add gateway validation for user-authored full policy payloads on
CreateSandboxand sandbox-scopedUpdateConfig/policy set, returningINVALID_ARGUMENTfor_provider_*keys. - Add gateway sanitization for sandbox-principal
UpdateConfigso incoming_provider_*entries are removed beforespec.policybackfill and policy revision persistence. - Add sandbox-side sanitization before
discover_and_sync_policy/sync_policywrite-back so current supervisors stop sending provider-derived rules. - Update docs and adjust existing provider-composition tests that currently assert suffixing for user-authored
_provider_*collisions.
Test Plan
- Unit tests:
crates/openshell-policy/src/compose.rs: reserved-prefix helper accepts_provider_fooand rejects non-reserved keys.crates/openshell-server/src/grpc/validation.rs: reserved-key validator rejects_provider_*keys and accepts ordinary network policy keys.crates/openshell-sandbox/src/lib.rs: strip helper removes only reserved provider entries and preserves user-authored entries.
- Integration-style handler tests:
crates/openshell-server/src/grpc/sandbox.rs:CreateSandboxrejects a policy containing_provider_*network policy keys.crates/openshell-server/src/grpc/policy.rs: userUpdateConfigrejects reserved keys; sandbox-principal sync strips reserved keys before backfillingspec.policyand persisting the revision; effective provider composition still returns provider rules while stored source policy does not.
- E2E tests: N/A — this is validation/persistence hygiene in existing handler paths; no e2e files are expected to change.
Risks & Open Questions
- Global policy writes use
validate_policy_safetytoday. The implementation should avoid accidentally changing global policy semantics unless maintainers explicitly want the reserved namespace rejected there as well. - Gateway-side sanitization is required even with sandbox-side stripping, because older supervisors and alternate sandbox clients may still send provider-derived rules.
- Existing docs/tests that describe collision suffixing need to be updated so they do not preserve the old behavior as expected behavior.
Documentation Impact
- Update
docs/sandboxes/providers-v2.mdxanddocs/sandboxes/policies.mdx. docs/sandboxes/manage-providers.mdxcan get a brief cross-reference if the final implementation touches provider command docs.docs/reference/gateway-config.mdxis not affected because this does not add, remove, rename, or change defaults for gateway TOML or driver config fields.
LSM Compatibility
No SELinux/AppArmor-specific impact expected. The change is policy validation and persistence hygiene only; it does not alter process identity,
/procaccess, binary execution, inter-process visibility, or LSM enforcement behavior.
Revision 1 — initial plan
- addedstate:review-readyReady for human reviewReady for human reviewstate:agent-readyApproved for agent implementationApproved for agent implementationstate:in-progressWork is currently in progressWork is currently in progress
on Jun 24, 2026 🏗️ build-from-issue-agent
Implementation Complete
PR: #1991
What was built
Reserved
_provider_*as a provider-derived network policy namespace. User-authored sandbox policy writes now reject reserved keys, and sandbox-originated sync strips provider-derived rules before persistence.Tests
- Unit/focused: provider namespace helpers, gateway validation, CreateSandbox/UpdateConfig handlers, sandbox sync stripping
- Pre-commit:
RUSTC_WRAPPER= mise run pre-commitpassed - E2E: N/A — no
e2e/files changed
Docs updated
docs/sandboxes/providers-v2.mdxdocs/sandboxes/policies.mdx
The issue will auto-close when the PR is merged.
- addedstate:pr-openedPR has been opened for this issuePR has been opened for this issueand removedstate:in-progressWork is currently in progressWork is currently in progressstate:review-readyReady for human reviewReady for human review
on Jun 24, 2026 - added 5 commits that reference this issue
on Jun 24, 2026 - added a commit that references this issue
on Jun 26, 2026
Agent Diagnostic
Investigation reviewing PR #1914 and the discussion in #1914 (comment).
Skills/tools used: code search across
crates/openshell-policy,crates/openshell-server/src/grpc, andcrates/openshell-sandbox; git history on the original composition PR (#1037).Findings:
crates/openshell-policy/src/compose.rs:15-34definesprovider_rule_name()which prefixes provider-derived rules with_provider_. The doc comment atcompose.rs:41-42declares this key namespace reserved for derived data.crates/openshell-policy/src/compose.rs:62-75(unique_provider_rule_key) collision-renames provider rules to_provider_<name>_2,_3, ... when a key already exists in the source policy. This is the mechanism by which a stale baked-in_provider_*rule shadows the live one.crates/openshell-policy/src/merge.rs:400-416andmerge.rs:642-656already filter_provider_*keys out of the endpoint-overlap fallback, confirming the prefix is treated as reserved internally.crates/openshell-server/src/grpc/validation.rs:619-628(validate_policy_safety) does not reject user-authored policy with_provider_*network policy keys. There is no enforcement at the gateway boundary for eitherSetSandboxPolicy,CreateSandbox, or the supervisor-originated enrichment write-back path.Net: the prefix is reserved by convention and by the composition layer, but not by any input validation. That asymmetry produces the bug.
Description
Actual behavior: A user-authored or sandbox-enriched policy can contain
network_policieskeys with the_provider_*prefix. When provider profile composition runs, the composer detects the collision and re-keys the provider rule as_provider_<name>_2. The original (now stale)_provider_<name>from the user policy remains in the effective policy. Non-additive provider profile updates (rename / remove endpoint) silently no-op because the live_2rule sits behind the stale baseline rule.This breaks the core update flow that PR #1914 introduces:
openshell provider profile updatesucceeds, but for any sandbox whose persisted policy already contains the prior_provider_*key, the change does not take effect.Expected behavior: The
_provider_*key namespace is reserved for derived data. Any user-supplied policy or sandbox-originated write that attempts to set keys under that namespace is rejected (or filtered) at the gateway boundary, so provider composition always produces a single authoritative_provider_<name>rule per profile.Reproduction Steps
Path 1 —
policy get --fullround-trip (user-driven)Path 2 — supervisor enrichment write-back (no user action)
Reproducer from pimlock — happens automatically as part of normal sandbox startup:
Full script with pauses for live-state exploration: https://gist.github.com/pimlock/d10de2a4ca5611a9a9ae78fce7e0a489?permalink_comment_id=6212304#gistcomment-6212304
Root cause of path 2: the supervisor enriches the effective policy (including provider-injected rules) and syncs it back to the gateway. The gateway treats the synced policy as authoritative user policy, baking the
_provider_*keys into the source. On the next provider poll, composition collision-renames the new provider rule to_provider_test_2.Proposed Fix
validate_policy_safety(or add a sibling validator) to rejectnetwork_policieskeys matching^_provider_on every user-facing write path — at minimumSetSandboxPolicyandCreateSandbox. Error message should explain that the prefix is reserved for provider composition and direct users topolicy get(without--full) for round-trippable output._provider_*keys before the supervisor pushes enriched policy back to the gateway. Provider-derived rules are gateway-authoritative; the sandbox should never be the source of truth for them. Belt-and-braces: the gateway's enrichment ingestion path should also drop any_provider_*keys it receives from a sandbox.policy get --full:--fullcontinues to render the effective policy including provider contributions — that's the purpose of the flag.policy getwithout--fullalready excludes_provider_*(merge.rs:416), so the documented round-trip workflow stays intact.docs/sandboxes/manage-providers.mdxabout the reservation.Beta-stage, hard reject is fine.
Environment
main/ PR feat(providers): support profile updates #1914 branchopenshell-server), sandbox supervisor (openshell-sandbox), policy engine (openshell-policy).Related
_provider_*composition design.