Skip to content

Commit 8dc2a54

Browse files
committed
docs(policy): document JSON-RPC L7 rules
Document JSON-RPC endpoint configuration, rpc_method and params matchers, batch denial behavior, current directionality limits, matcher scope, and the current policy update CLI limitation. Signed-off-by: Kris Hicks <khicks@nvidia.com>
1 parent 6302ec9 commit 8dc2a54

3 files changed

Lines changed: 128 additions & 10 deletions

File tree

architecture/sandbox.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@ paths, such as proxy support files or GPU device paths when a GPU is present.
4949
All ordinary agent egress is routed through the sandbox proxy. The proxy
5050
identifies the calling binary, checks trust-on-first-use binary identity, rejects
5151
unsafe internal destinations, and evaluates the active policy.
52+
For inspected HTTP traffic, the proxy can enforce REST method/path rules,
53+
WebSocket upgrade and text-message rules, GraphQL operation rules, and
54+
JSON-RPC method and params rules on sandbox-to-server request bodies. JSON-RPC
55+
request inspection buffers up to the endpoint `json_rpc.max_body_bytes` limit.
56+
JSON-RPC responses and server-to-client MCP messages on response or SSE streams
57+
are relayed but are not currently parsed for policy enforcement.
5258

5359
`https://inference.local` is special. It bypasses OPA network policy and is
5460
handled by the inference interception path:

docs/reference/policy-schema.mdx

Lines changed: 72 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ Each endpoint defines a reachable destination and optional inspection rules.
155155
| `host` | string | Yes | Hostname or IP address. Supports a `*` wildcard inside the first DNS label only: `*.example.com`, `**.example.com`, and intra-label patterns like `*-aiplatform.googleapis.com` are accepted; bare `*`/`**`, TLD wildcards (`*.com`), and wildcards outside the first label are rejected at load time. |
156156
| `port` | integer | Yes | TCP port number. |
157157
| `path` | string | No | Optional HTTP path glob used to select between L7 endpoints that share the same host and port. Empty means all paths. Use this when REST and GraphQL live under the same host, such as `/repos/**` and `/graphql`. |
158-
| `protocol` | string | No | Set to `rest` for HTTP method/path inspection, `websocket` for RFC 6455 upgrade and client text-message inspection, or `graphql` for GraphQL-over-HTTP operation inspection. WebSocket endpoints can also use GraphQL operation rules for GraphQL-over-WebSocket traffic. Omit for TCP passthrough. |
158+
| `protocol` | string | No | Set to `rest` for HTTP method/path inspection, `websocket` for RFC 6455 upgrade and client text-message inspection, `graphql` for GraphQL-over-HTTP operation inspection, or `json-rpc` for sandbox-to-server JSON-RPC-over-HTTP method and params inspection. WebSocket endpoints can also use GraphQL operation rules for GraphQL-over-WebSocket traffic. Omit for TCP passthrough. |
159159
| `tls` | string | No | TLS handling mode. The proxy auto-detects TLS by peeking the first bytes of each connection and terminates it for inspected HTTPS traffic, so this field is optional in most cases. Set to `skip` to disable auto-detection for edge cases such as client-certificate mTLS or non-standard protocols. The values `terminate` and `passthrough` are deprecated and log a warning; they are still accepted for backward compatibility but have no effect on behavior. |
160160
| `enforcement` | string | No | `enforce` actively blocks disallowed requests. `audit` logs violations but allows traffic through. |
161161
| `access` | string | No | Access preset. One of `read-only`, `read-write`, or `full`. Mutually exclusive with `rules`. |
@@ -168,18 +168,21 @@ Each endpoint defines a reachable destination and optional inspection rules.
168168
| `persisted_queries` | string | No | GraphQL hash-only behavior for `protocol: graphql` and GraphQL-over-WebSocket operation policy. Default is `deny`; use `allow_registered` only with `graphql_persisted_queries`. |
169169
| `graphql_persisted_queries` | map | No | Trusted GraphQL persisted-query registry keyed by hash or saved-query ID. Values contain `operation_type`, optional `operation_name`, and optional root `fields`. |
170170
| `graphql_max_body_bytes` | integer | No | Maximum GraphQL-over-HTTP request body bytes buffered for inspection. Defaults to `65536`. |
171+
| `json_rpc` | object | No | JSON-RPC endpoint options. For `protocol: json-rpc`, `json_rpc.max_body_bytes` sets the maximum JSON-RPC-over-HTTP request body bytes buffered for inspection. Defaults to `65536`. |
171172

172173
Credential rewrite recognizes the canonical `openshell:resolve:env:KEY` placeholder form and whole-token provider-shaped aliases such as `provider-OPENSHELL-RESOLVE-ENV-API_TOKEN` when the referenced environment key exists in the configured provider credentials.
173174

174175
#### Access Levels
175176

176177
The `access` field accepts one of the following values:
177178

178-
| Value | REST expansion | WebSocket expansion | GraphQL expansion |
179-
|---|---|---|---|
180-
| `full` | All methods and paths. | WebSocket upgrade and all inspected client text-message paths. | All operation types. |
181-
| `read-only` | `GET`, `HEAD`, `OPTIONS`. | WebSocket upgrade handshake only. | `query` operations. |
182-
| `read-write` | `GET`, `HEAD`, `OPTIONS`, `POST`, `PUT`, `PATCH`. | WebSocket upgrade handshake and client text messages. | `query` and `mutation` operations. |
179+
| Value | REST expansion | WebSocket expansion | GraphQL expansion | JSON-RPC behavior |
180+
|---|---|---|---|---|
181+
| `full` | All methods and paths. | WebSocket upgrade and all inspected client text-message paths. | All operation types. | Allows matching HTTP requests without constraining JSON-RPC methods. |
182+
| `read-only` | `GET`, `HEAD`, `OPTIONS`. | WebSocket upgrade handshake only. | `query` operations. | Expands to HTTP read methods and does not allow typical JSON-RPC `POST` calls. |
183+
| `read-write` | `GET`, `HEAD`, `OPTIONS`, `POST`, `PUT`, `PATCH`. | WebSocket upgrade handshake and client text messages. | `query` and `mutation` operations. | Allows matching HTTP write requests without constraining JSON-RPC methods. |
184+
185+
For JSON-RPC endpoints, prefer explicit `rules` with `rpc_method` and optional `params` when you need method-level control.
183186

184187
#### Allow Rule Objects
185188

@@ -274,6 +277,42 @@ rules:
274277

275278
Do not combine `method`, `path`, or `query` with `operation_type`, `operation_name`, or `fields` inside the same WebSocket rule. When a WebSocket endpoint has GraphQL operation policy, use GraphQL rules for client messages instead of a raw `WEBSOCKET_TEXT` allow rule.
276279

280+
##### JSON-RPC Allow Rule (`protocol: json-rpc`)
281+
282+
JSON-RPC allow rules match sandbox-to-server JSON-RPC-over-HTTP request objects by RPC method and optional params. They apply to single JSON-RPC requests and batch requests. For a batch, OpenShell evaluates each call independently. JSON-RPC responses and server-to-client messages on response bodies or MCP SSE streams are relayed but are not currently parsed for policy enforcement.
283+
284+
| Field | Type | Required | Description |
285+
|---|---|---|---|
286+
| `rpc_method` | string | Yes | JSON-RPC method name or glob, such as `initialize`, `tools/list`, or `tools/*`. |
287+
| `params` | map | No | Params matchers keyed by flattened object-param path. Use dot-separated keys for nested object params, such as `arguments.scope`. Matcher value can be a glob string or an object with `any`. Strings, numbers, and booleans are converted to strings; arrays, `null`, and non-object top-level params do not produce matcher keys. |
288+
289+
Example JSON-RPC allow rules:
290+
291+
```yaml showLineNumbers={false}
292+
endpoints:
293+
- host: mcp.example.com
294+
port: 443
295+
path: /mcp
296+
protocol: json-rpc
297+
enforcement: enforce
298+
json_rpc:
299+
max_body_bytes: 131072
300+
rules:
301+
- allow:
302+
rpc_method: initialize
303+
- allow:
304+
rpc_method: tools/list
305+
- allow:
306+
rpc_method: tools/call
307+
params:
308+
name: read_status
309+
- allow:
310+
rpc_method: tools/call
311+
params:
312+
name: submit_report
313+
arguments.scope: workspace/main
314+
```
315+
277316
#### Deny Rule Objects
278317

279318
Blocks specific operations on endpoints that otherwise have broad access. Deny rules are evaluated after allow rules and take precedence: if a request matches any deny rule, it is blocked regardless of what the allow rules or access preset permit.
@@ -356,6 +395,33 @@ endpoints:
356395
operation_name: Admin*
357396
```
358397

398+
##### JSON-RPC Deny Rule (`protocol: json-rpc`)
399+
400+
JSON-RPC deny rules use the same field names as JSON-RPC allow rules, but they appear directly under each `deny_rules` entry instead of under an `allow` wrapper. Deny rules take precedence over allow rules. In a batch request, one denied call denies the full batch.
401+
402+
| Field | Type | Required | Description |
403+
|---|---|---|---|
404+
| `rpc_method` | string | Yes | JSON-RPC method name or glob to deny. |
405+
| `params` | map | No | Params matchers keyed by flattened object-param path. Omit to deny every call matching `rpc_method`. Strings, numbers, and booleans are converted to strings; arrays, `null`, and non-object top-level params do not produce matcher keys. |
406+
407+
Example JSON-RPC deny rules:
408+
409+
```yaml showLineNumbers={false}
410+
endpoints:
411+
- host: mcp.example.com
412+
port: 443
413+
path: /mcp
414+
protocol: json-rpc
415+
enforcement: enforce
416+
rules:
417+
- allow:
418+
rpc_method: tools/*
419+
deny_rules:
420+
- rpc_method: tools/call
421+
params:
422+
name: delete_resource
423+
```
424+
359425
### Binary Object
360426

361427
Identifies an executable that is permitted to use the associated endpoints.

docs/sandboxes/policies.mdx

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ The following steps outline the hot-reload policy update workflow.
148148

149149
To inspect a stored sandbox-authored revision instead of the current effective policy, pass `--rev <version>`.
150150

151-
5. Edit the YAML: add or adjust `network_policies` entries, binaries, `access`, or `rules`.
151+
5. Edit the YAML: add or adjust `network_policies` entries, binaries, `access`, `rules`, or protocol-specific matchers such as GraphQL operation fields and JSON-RPC `rpc_method` / `params` rules.
152152

153153
6. Push the updated policy when you need a full replacement. Exit codes: 0 = loaded, 1 = validation failed, 124 = timeout.
154154

@@ -173,7 +173,7 @@ Use `openshell policy update` when you want to merge network policy changes into
173173
- remove one endpoint or one named rule without rewriting the rest of the file.
174174
- preview a merged result locally with `--dry-run` before you send it to the gateway.
175175

176-
Use `openshell policy set` instead when you want to replace the full policy, update static sections, or make broader edits that are easier to express in YAML.
176+
Use `openshell policy set` instead when you want to replace the full policy, update static sections, or make broader edits that are easier to express in YAML. Use full YAML for GraphQL and JSON-RPC rule shapes.
177177

178178
### Update Commands
179179

@@ -210,6 +210,7 @@ This is the practical difference:
210210
Current constraints:
211211

212212
- `--add-allow` and `--add-deny` work on `protocol: rest` and `protocol: websocket` endpoints.
213+
- GraphQL and JSON-RPC fine-grained rules require full policy YAML applied with `openshell policy set`.
213214
- `--add-deny` requires the endpoint to already have an allow base, either an `access` preset or explicit allow `rules`.
214215
- `protocol: sql` is not a practical incremental workflow today. OpenShell does not do full SQL parsing, and SQL enforcement is not meaningfully supported yet.
215216

@@ -228,7 +229,7 @@ Each segment has a fixed meaning:
228229
| `host` | Yes | Destination hostname. |
229230
| `port` | Yes | Destination port, `1` through `65535`. |
230231
| `access` | No | Access preset for L7 endpoints: `read-only`, `read-write`, or `full`. Incremental updates expand presets into protocol-specific method/path rules for REST and WebSocket endpoints. |
231-
| `protocol` | No | L7 inspection mode: `rest`, `websocket`, or `sql`. `sql` is audit-only and not a recommended workflow today. |
232+
| `protocol` | No | L7 inspection mode accepted by `openshell policy update`: `rest`, `websocket`, or `sql`. `sql` is audit-only and not a recommended workflow today. Full policy YAML also supports `graphql` and `json-rpc`. |
232233
| `enforcement` | No | Enforcement mode for inspected traffic: `enforce` or `audit`. |
233234
| `options` | No | Comma-separated endpoint options. Use `websocket-credential-rewrite` with `protocol: websocket` or REST compatibility endpoints that perform a WebSocket upgrade. Use `request-body-credential-rewrite` only with `protocol: rest`. |
234235

@@ -548,7 +549,7 @@ For an end-to-end walkthrough that combines this policy with a GitHub credential
548549
- { path: /usr/bin/gh }
549550
```
550551

551-
Endpoints with `protocol: rest` enable HTTP request inspection and can opt in to supported text request body credential rewrite. Endpoints with `protocol: websocket` validate WebSocket upgrades and inspect client text messages on the upgraded request path. WebSocket endpoints can also classify GraphQL-over-WebSocket operation messages with the same operation rules used by GraphQL-over-HTTP. Endpoints with `protocol: graphql` parse GraphQL-over-HTTP payloads before evaluating rules. The endpoint-level `path` field lets these protocols share `api.github.com:443` without treating GraphQL payloads as plain REST `POST /graphql` requests.
552+
Endpoints with `protocol: rest` enable HTTP request inspection and can opt in to supported text request body credential rewrite. Endpoints with `protocol: websocket` validate WebSocket upgrades and inspect client text messages on the upgraded request path. WebSocket endpoints can also classify GraphQL-over-WebSocket operation messages with the same operation rules used by GraphQL-over-HTTP. Endpoints with `protocol: graphql` parse GraphQL-over-HTTP payloads before evaluating rules. Endpoints with `protocol: json-rpc` parse JSON-RPC-over-HTTP request bodies and evaluate `rpc_method` and optional params rules. The endpoint-level `path` field lets these protocols share `api.github.com:443` without treating GraphQL payloads as plain REST `POST /graphql` requests.
552553
</Tab>
553554

554555
</Tabs>
@@ -579,6 +580,51 @@ REST rules can also constrain query parameter values:
579580

580581
`query` matchers are case-sensitive and run on decoded values. If a request has duplicate keys (for example, `tag=a&tag=b`), every value for that key must match the configured glob(s).
581582

583+
### JSON-RPC matching
584+
585+
JSON-RPC endpoints use `protocol: json-rpc`. The proxy parses sandbox-to-server JSON-RPC-over-HTTP request bodies, evaluates the `method` field against `rpc_method`, and can match object params through dot-separated `params` keys.
586+
587+
JSON-RPC policy enforcement is directional. It applies to HTTP request bodies sent by the sandboxed process to the configured endpoint. JSON-RPC responses and server-to-client messages carried on response bodies or MCP SSE streams are relayed but are not currently parsed for policy enforcement.
588+
589+
JSON-RPC endpoint policies currently require full policy YAML applied with `openshell policy set`; the incremental `openshell policy update --add-endpoint` parser does not accept `json-rpc` as a protocol.
590+
591+
```yaml showLineNumbers={false}
592+
mcp_server:
593+
name: mcp_server
594+
endpoints:
595+
- host: mcp.example.com
596+
port: 443
597+
path: /mcp
598+
protocol: json-rpc
599+
enforcement: enforce
600+
json_rpc:
601+
max_body_bytes: 131072
602+
rules:
603+
- allow:
604+
rpc_method: initialize
605+
- allow:
606+
rpc_method: tools/list
607+
- allow:
608+
rpc_method: tools/call
609+
params:
610+
name: read_status
611+
- allow:
612+
rpc_method: tools/call
613+
params:
614+
name: submit_report
615+
arguments.scope: workspace/main
616+
deny_rules:
617+
- rpc_method: tools/call
618+
params:
619+
name: delete_resource
620+
binaries:
621+
- { path: /usr/bin/python3 }
622+
```
623+
624+
`json_rpc.max_body_bytes` controls how many JSON-RPC-over-HTTP request body bytes OpenShell buffers for inspection. It defaults to `65536`.
625+
626+
`params` matchers are case-sensitive and use the same string glob or `{ any: [...] }` matcher syntax as REST query parameters. They match scalar leaf values from object params: strings, numbers, and booleans are converted to strings, and nested JSON object params are flattened with dot-separated keys before matching. Arrays, `null`, and non-object top-level params do not produce matcher keys. This is useful for controls such as matching MCP `tools/call` by `params.name`, but it is not a complete MCP payload policy for rich nested content. For batch requests, OpenShell evaluates each JSON-RPC call independently and denies the whole batch if any call is denied.
627+
582628
### GraphQL matching
583629

584630
GraphQL endpoints use `protocol: graphql`. The proxy parses GraphQL-over-HTTP `GET` and `POST` requests, classifies each operation, and evaluates rules against the operation type, optional operation name, and selected root fields.

0 commit comments

Comments
 (0)