Skip to content

Commit b2e79aa

Browse files
committed
docs(policy): explain ambiguity preflight behavior
Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com>
1 parent 5adac68 commit b2e79aa

8 files changed

Lines changed: 82 additions & 13 deletions

File tree

.agents/skills/debug-openshell-cluster/SKILL.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,30 @@ The middleware service must start before the gateway and be reachable from both
106106

107107
At request time, distinguish an explicit `middleware_denied` result from `middleware_failed`. A denial is always enforced. A failure follows the policy-local `on_error`: `fail_closed` blocks the request, while `fail_open` bypasses only that stage and emits a detection finding. If a running supervisor cannot install a new registry, it preserves its last-known-good generation and emits a configuration failure event.
108108

109+
For network policy validation failures, first distinguish a gateway mutation
110+
rejection from a supervisor runtime rejection. Direct policy updates,
111+
incremental merges and approvals, provider attachments, and provider-profile
112+
fanout are validated against the complete effective policy before persistence
113+
when the gateway knows the affected sandbox scope. A `FAILED_PRECONDITION`
114+
ambiguity response means no invalid revision or partial fanout was stored.
115+
Supervisor validation remains defense in depth for startup, races, and policy
116+
sources outside those mutation paths.
117+
118+
Runtime rejection behavior is configured only in `gateway.toml`:
119+
120+
```toml
121+
[openshell.gateway]
122+
policy_validation_failure_mode = "fail_closed"
123+
```
124+
125+
The default `fail_closed` mode deactivates the previous generation, closes
126+
pinned relays, and quarantines new egress until a valid generation loads.
127+
`retain_last_valid` explicitly keeps the previous valid policy active; without
128+
one it still fails closed. Restart the gateway after changing this field.
129+
Inspect sandbox OCSF configuration and finding events for the validation
130+
rationale, configured and effective modes, active generation, and the explicit
131+
`previous_policy_active` state.
132+
109133
### Step 4: Check Docker-Backed Gateways
110134

111135
```bash
@@ -400,6 +424,8 @@ openshell logs <sandbox-name>
400424
| Provider profiles disappear after enabling an interceptor catalog | `provider_profile_sources` selected only an authoritative interceptor or returned invalid/duplicate IDs | Inspect source list and interceptor `Describe`/catalog logs; include `builtin` and `user` when intended |
401425
| Gateway fails after registering supervisor middleware | Service unavailable, invalid manifest, duplicate binding, reserved name, or invalid body/timeout limit | Middleware service and gateway logs; `[[openshell.supervisor.middleware]]`; `Describe` response |
402426
| Policy update rejects `network_middlewares` | Unknown middleware name, implementation-owned config invalid, duplicate order, broad/invalid host selector, or fail-closed coverage of `tls: skip` | Policy error, gateway logs, middleware `ValidateConfig`, selector and order fields |
427+
| Policy mutation returns `FAILED_PRECONDITION` for endpoint ambiguity | Equally specific effective endpoint selectors disagree on connection or request-processing metadata | CLI error, base and provider-composed policy, affected profile attachments; confirm no new revision was stored |
428+
| Supervisor enters policy quarantine | A runtime candidate failed validation while `policy_validation_failure_mode = "fail_closed"` | Sandbox OCSF config/finding events, validation rationale, active generation, `previous_policy_active` |
403429
| HTTP request returns `middleware_failed` or `middleware_denied` | Selected stage failed or explicitly denied the admitted request | Sandbox OCSF logs; policy-local middleware config; service availability; `on_error` |
404430
| Custom compute driver is unavailable | Driver process/socket missing, inaccessible, or configured with a reserved/mismatched name | Socket ownership/mode, driver service logs, gateway `GetCapabilities` logs |
405431
| Image pull failure | Gateway or sandbox image cannot be pulled | Runtime events and image pull credentials |

.agents/skills/generate-sandbox-policy/SKILL.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,10 @@ Only needed for the **Moderate** and **Full** tiers. Translate API path paramete
237237
| `/api/v1/models/{model_id}/versions/{version}` | `/api/v1/models/*/versions/*` |
238238
| All sub-paths under `/api/v1/` | `/api/v1/**` |
239239

240-
Remember: `*` does not cross `/` boundaries. Use `**` for recursive matching across path segments.
240+
Path matching uses the runtime `glob` engine. Both `*` and `**` may cross `/`
241+
boundaries; `?` matches one character, and bracket classes such as `[0-9]` and
242+
`[!0]` are supported. Prefer segment-shaped patterns such as
243+
`/repos/*/issues` for readability, but do not rely on `*` to stop at `/`.
241244

242245
### Building the Explicit Rules List
243246

@@ -439,7 +442,7 @@ The policy needs to go somewhere. Determine which mode applies:
439442

440443
2. **Check for conflicts**:
441444
- Does a policy with the same key already exist? If so, ask the user whether to **replace** it, **merge** new endpoints/binaries into it, or use a different key.
442-
- Does an existing policy already cover the same host:port? Warn the user — overlapping endpoint coverage across policies causes OPA evaluation errors (complete rule conflict).
445+
- Does an existing endpoint selector overlap the new selector? Compatible overlaps are allowed and can intentionally aggregate allow and deny rules. Reject or revise equally specific overlaps that disagree on connection or request-processing metadata, including TLS, destination constraints, protocol/parser behavior, enforcement, or credential handling. A more-specific path selector may override broader request-processing metadata.
443446

444447
3. **Apply the change**:
445448
- **Adding a new policy**: Insert the new policy block under `network_policies`, maintaining the file's existing indentation and style.

.agents/skills/generate-sandbox-policy/examples.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -727,7 +727,9 @@ An exact IP is treated as `/32` — only that specific address is permitted.
727727
**Agent workflow**:
728728

729729
1. Read `sandbox-policy.yaml`
730-
2. Check that no existing policy already covers `api.github.com:443` — if one does, warn about overlap
730+
2. Check existing selectors for `api.github.com:443`. Compatible overlaps may
731+
aggregate request rules; revise equally specific overlaps that disagree on
732+
TLS, destination, protocol/parser, enforcement, or credential behavior.
731733
3. Check that the key `github_readonly` doesn't already exist
732734
4. Insert the new policy under `network_policies`:
733735

.agents/skills/openshell-cli/SKILL.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,13 @@ Edit `current-policy.yaml` to allow the blocked actions. **For policy content au
351351
openshell policy set dev --policy current-policy.yaml --wait
352352
```
353353

354+
The gateway validates the complete effective candidate—including attached
355+
provider-profile policy—before it stores a direct update, incremental merge,
356+
approved proposal, provider attachment, or profile update that affects attached
357+
sandboxes. An ambiguity failure returns `FAILED_PRECONDITION`; the rejected
358+
candidate does not create a policy revision or partially update affected
359+
sandboxes. Fix the conflicting endpoint selectors and submit again.
360+
354361
The `--wait` flag blocks until the sandbox confirms the policy is loaded (polls every second). Exit codes:
355362
- **0**: Policy loaded successfully
356363
- **1**: Policy load failed
@@ -569,6 +576,13 @@ openshell settings set --global --key providers_v2_enabled --value true
569576
570577
Global mutations prompt for confirmation. Use `--yes` only in reviewed automation.
571578
579+
`policy_validation_failure_mode` is gateway startup configuration, not a
580+
mutable `openshell settings` key. Set it under `[openshell.gateway]` in
581+
`gateway.toml` and restart the gateway. The security-first default is
582+
`fail_closed`; `retain_last_valid` is an explicit availability tradeoff. OCSF
583+
configuration events state whether the previous generation is active after a
584+
runtime validation failure.
585+
572586
## Workflow 10: Service Access
573587
574588
Use `forward` for local access and `service` for a gateway-managed HTTP endpoint:

architecture/security-policy.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,19 @@ connection metadata agrees. When request paths overlap, a path endpoint with a
108108
higher specificity rank deterministically overrides broader request-processing
109109
metadata. Equally specific overlapping endpoints must agree.
110110

111+
Gateway mutation paths validate the complete effective candidate before
112+
persistence when the affected sandbox scope is known. Direct replacements,
113+
incremental merges and approvals, provider attachment, and profile fanout reject
114+
ambiguity atomically, without creating an invalid revision or partially
115+
activating an update. Supervisor validation remains the defense-in-depth
116+
boundary for startup, concurrent changes, and sources outside those mutations.
117+
111118
The `[openshell.gateway] policy_validation_failure_mode` configuration controls
112-
rejected generations. It defaults to `fail_closed`, which publishes a quarantine
113-
generation, denies new egress, invalidates existing relays, and leaves the
114-
previous policy inactive. Operators may explicitly select
119+
candidates rejected by supervisor runtime validation. Gateway preflight
120+
rejections never become generations and leave the active policy unchanged. The
121+
runtime mode defaults to `fail_closed`, which publishes a quarantine generation,
122+
denies new egress, invalidates existing relays, and leaves the previous policy
123+
inactive. Operators may explicitly select
115124
`retain_last_valid`, which keeps the previous generation active. With no
116125
previous valid generation, the effective mode remains `fail_closed` regardless
117126
of the configured mode. The gateway distributes this startup configuration to

docs/reference/gateway-config.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ phases = ["validate"]
175175

176176
Local Docker, Podman, and VM gateways can also set `[openshell.gateway.mtls_auth] enabled = true` to authenticate CLI callers from verified client certificates. Kubernetes deployments must leave this unset and use OIDC or a trusted access proxy; the Helm chart does not render this table.
177177

178-
`[openshell.gateway] policy_validation_failure_mode` controls what sandbox supervisors do when a complete candidate policy fails validation. The default, `fail_closed`, deactivates the previous network policy, closes relays pinned to it, and denies new egress until a valid generation loads. `retain_last_valid` leaves the previous valid generation active. Both modes reject the candidate atomically; startup always fails closed when no previous valid generation exists. Changing the value requires restarting the gateway so it can reload `gateway.toml` and distribute the new posture to sandbox supervisors.
178+
`[openshell.gateway] policy_validation_failure_mode` controls what sandbox supervisors do when a complete candidate policy fails runtime validation. The default, `fail_closed`, deactivates the previous network policy, closes relays pinned to it, and denies new egress until a valid generation loads. `retain_last_valid` leaves the previous valid generation active. Both modes reject the candidate atomically; startup always fails closed when no previous valid generation exists. Gateway mutation paths that can preflight a known effective scope reject invalid candidates before persistence and leave the active policy unchanged regardless of this setting. Changing the value requires restarting the gateway so it can reload `gateway.toml` and distribute the new posture to sandbox supervisors.
179179

180180
`[openshell.gateway.gateway_jwt] ttl_secs` controls gateway-minted sandbox JWT lifetime. When omitted, it defaults to `0`: the token `exp` claim and `expires_at_ms` response field become `0`, and the sandbox JWT does not expire. Use that default only for local single-player Docker, Podman, or VM gateways. Kubernetes and other shared deployments should set a positive TTL; Helm renders `3600` seconds by default, and the gateway logs a warning when a Kubernetes gateway uses `0`.
181181

docs/reference/policy-schema.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ REST allow rules match HTTP requests by method, path, and optional query paramet
216216
| Field | Type | Required | Description |
217217
|---|---|---|---|
218218
| `method` | string | Yes | HTTP method to allow (for example, `GET`, `POST`). `*` matches any method. |
219-
| `path` | string | Yes | URL path pattern. Supports `*` and `**` glob syntax. |
219+
| `path` | string | Yes | URL path glob. `*` and `**` match zero or more characters and may cross `/`; `?` matches one character; bracket classes such as `[0-9]` and `[!0]` are supported. |
220220
| `query` | map | No | Query parameter matchers keyed by decoded param name. Matcher value can be a glob string (`tag: "foo-*"`) or an object with `any` (`tag: { any: ["foo-*", "bar-*"] }`). |
221221

222222
Example REST allow rules:

docs/sandboxes/policies.mdx

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,21 @@ The following steps outline the hot-reload policy update workflow.
205205

206206
OpenShell validates a complete candidate policy before activating any part of it. Endpoints may overlap when their connection and request-processing metadata agree. For example, two `api.example.com:443` REST entries can contribute different allow and deny rules when they use the same TLS, destination, credential, parser, and enforcement settings. A plain L4 endpoint may overlap an L7 endpoint because it authorizes the destination without contributing request-processing metadata. A more-specific path endpoint may override request-processing metadata from a broader endpoint, such as a `/graphql` GraphQL endpoint alongside a general REST endpoint for the same host. OpenShell rejects the candidate when overlapping exact or wildcard host selectors can both contribute equally specific endpoint configuration and disagree on those fields.
207207

208-
The gateway's `policy_validation_failure_mode` configuration determines what happens after rejection. Set it under `[openshell.gateway]` in `gateway.toml`. Its default is `fail_closed`:
208+
When the gateway knows the affected sandbox scope, it validates the complete
209+
effective candidate before persistence. This covers direct policy replacement,
210+
incremental merges and proposal approvals, provider attachment, and
211+
provider-profile updates that fan out to attached sandboxes. An ambiguity
212+
failure returns `FAILED_PRECONDITION`; OpenShell stores no invalid policy
213+
revision and does not partially apply a profile update. Supervisor validation
214+
remains a defense-in-depth boundary for startup, concurrent changes, and policy
215+
sources outside those mutation paths.
216+
217+
A gateway preflight rejection leaves the currently active policy unchanged
218+
regardless of failure mode because the candidate is never persisted or
219+
distributed. If a candidate reaches a supervisor and fails runtime validation,
220+
the gateway's `policy_validation_failure_mode` configuration determines the
221+
supervisor posture. Set it under `[openshell.gateway]` in `gateway.toml`. Its
222+
default is `fail_closed`:
209223

210224
```toml
211225
[openshell.gateway]
@@ -354,13 +368,14 @@ means:
354368
- match the endpoint `api.github.com:443`.
355369
- match HTTP method `POST`.
356370
- match paths like `/repos/acme/issues`.
357-
- do not match deeper paths like `/repos/acme/project/issues/123` because `*` matches one path segment.
371+
- also match deeper paths when the surrounding literals align, because `*` may include `/`.
358372

359373
Path globs follow the same semantics as YAML allow and deny rules:
360374

361-
- `*` matches one path segment.
362-
- `**` matches any number of segments.
363-
- `/repos/*/issues` matches one repository owner or name segment in the middle.
375+
- `*` and `**` match zero or more characters and may cross `/` boundaries.
376+
- `?` matches exactly one character.
377+
- bracket classes such as `[0-9]` and negated classes such as `[!0]` are supported.
378+
- `/repos/*/issues` matches any intervening text, including multiple path segments.
364379
- `/repos/**` matches everything under `/repos/`.
365380

366381
The rule-level commands only modify method and path constraints. They do not change binaries, hostnames, ports, protocol settings, or WebSocket message payload matching.

0 commit comments

Comments
 (0)