Skip to content

Commit d556748

Browse files
authored
feat(supervisor-middleware): add network egress middleware (#2027)
Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
1 parent d70adaf commit d556748

61 files changed

Lines changed: 14446 additions & 785 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,15 @@ These pipelines connect skills into end-to-end workflows. Individual skill files
4242
| `crates/openshell-tui/` | Terminal UI | Ratatui-based dashboard for monitoring |
4343
| `crates/openshell-driver-kubernetes/` | Kubernetes compute driver | In-process `ComputeDriver` backend for K8s sandbox pods |
4444
| `crates/openshell-driver-docker/` | Docker compute driver | In-process `ComputeDriver` backend for local Docker sandbox containers |
45+
| `crates/openshell-driver-podman/` | Podman compute driver | In-process `ComputeDriver` backend for local Podman sandbox containers |
4546
| `crates/openshell-driver-vm/` | VM compute driver | Standalone libkrun-backed `ComputeDriver` subprocess (embeds its own rootfs + runtime) |
47+
| `crates/openshell-prover/` | Policy prover | Policy verification and proof generation |
48+
| `crates/openshell-server-macros/` | Server macros | Compile-time helpers for gateway RPC authorization |
49+
| `crates/openshell-supervisor-middleware/` | Middleware runtime | Generic middleware registry, remote service integration, and chain execution |
50+
| `crates/openshell-supervisor-middleware-builtins/` | Built-in middleware | First-party in-process middleware implementations |
51+
| `crates/openshell-supervisor-network/` | Network supervisor | Proxying, L7 enforcement, policy evaluation, and inference routing |
52+
| `crates/openshell-supervisor-process/` | Process supervisor | Process lifecycle, namespace, and bypass monitoring |
53+
| `crates/openshell-vfio/` | VFIO support | PCI and GPU passthrough preparation and lifecycle |
4654
| `python/openshell/` | Python SDK | Python bindings and CLI packaging |
4755
| `proto/` | Protobuf definitions | gRPC service contracts |
4856
| `deploy/` | Docker, Helm, K8s | Dockerfiles, Helm chart, manifests |

CONTRIBUTING.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ Skills live in `.agents/skills/`. Your agent's harness can discover and load the
7575
| Contributing | `create-github-pr` | Create pull requests with proper conventions |
7676
| Reviewing | `review-github-pr` | Summarize PR diffs and key design decisions |
7777
| Reviewing | `review-security-issue` | Assess security issues for severity and remediation |
78+
| Reviewing | `fix-security-issue` | Implement an approved security remediation plan |
7879
| Reviewing | `watch-github-actions` | Monitor CI pipeline status and logs |
7980
| Reviewing | `launch-openshell-gator` | Launch and supervise OpenShell gator agents for issue and PR monitoring |
8081
| Reviewing | `test-release-canary` | Dispatch and iterate on the Release Canary workflow that smoke-tests published artifacts |

Cargo.lock

Lines changed: 40 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ serde_yml = "0.0.12"
7575
toml = "0.8"
7676
apollo-parser = "0.8.5"
7777
tower-mcp-types = "0.12.0"
78+
regex = "1"
7879

7980
# HTTP client
8081
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls-native-roots"] }

architecture/gateway.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,17 @@ config path. A gateway-global policy can override sandbox-scoped policy. The
369369
sandbox supervisor polls for config revisions and hot-reloads dynamic policy
370370
when the policy engine accepts the update.
371371

372+
External supervisor middleware registration is operator-owned configuration
373+
under `[[openshell.supervisor.middleware]]`. At startup the gateway connects to
374+
each service and validates its described bindings and operator body limit.
375+
Policies attach a complete external middleware by its operator-owned registration
376+
name. Manifest bindings are identified by operation and phase, and each manifest
377+
may declare at most one binding for an operation and phase pair.
378+
Before persisting a policy, the gateway asks each selected implementation to
379+
validate its config. The effective sandbox config contains only the registered
380+
services required by that policy; supervisors invoke those services directly on
381+
the request path.
382+
372383
Provider credential expiry is enforced during gateway-to-sandbox credential
373384
resolution and again by the sandbox placeholder resolver. This keeps expired
374385
credentials from resolving even when a running sandbox still has retained

architecture/sandbox.md

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,31 @@ matchers; generic JSON-RPC rules match only the method.
6666
JSON-RPC responses and server-to-client MCP messages on response or SSE streams
6767
are relayed but are not currently parsed for policy enforcement.
6868

69+
For admitted HTTP requests, the proxy can run an ordered supervisor middleware
70+
chain after L7 policy evaluation and before credential injection. Destination
71+
host selectors choose the chain independently of the network rule that admitted
72+
the request. Policy-local map keys identify configs, while built-in names or
73+
operator-owned registration names identify implementations.
74+
75+
Built-ins run in-process; operator services use the same bounded gRPC contract.
76+
`openshell-policy` validates policy-owned structure, and the active middleware
77+
registry validates implementation-owned config. The generic registry and chain
78+
runner live in `openshell-supervisor-middleware`; first-party implementations
79+
live in `openshell-supervisor-middleware-builtins`.
80+
81+
The supervisor installs policy and middleware registry changes as one runtime
82+
generation and preserves the last-known-good generation if preparation fails.
83+
Policy-only updates reuse the connected registry, so an external middleware
84+
outage cannot block unrelated policy changes.
85+
86+
Middleware cannot observe injected credentials or mutate supervisor-owned
87+
credential, routing, or framing headers. Body transformations are re-evaluated
88+
against body-aware L7 policy before later stages or the upstream can observe
89+
them. Requests, results, chain length, execution time, and diagnostics are
90+
bounded; external free-form diagnostic text is not exposed in responses or
91+
security logs. See [Supervisor Middleware](../docs/extensibility/supervisor-middleware.mdx)
92+
for configuration and protocol details.
93+
6994
`https://inference.local` is special. It bypasses OPA network policy and is
7095
handled by the inference interception path:
7196

@@ -206,8 +231,10 @@ engine with a gateway policy revision.
206231
## Failure Behavior
207232

208233
- If gateway config polling fails, the sandbox keeps its last-known-good policy.
209-
- If a live policy update is invalid, the supervisor rejects it and keeps the
210-
current policy.
234+
- If a live policy or middleware-registry update is invalid, the supervisor
235+
rejects the combined update and keeps the current runtime pair.
236+
- If an operator-run middleware call fails, the selected config's `on_error`
237+
behavior decides whether to deny the request or continue without that stage.
211238
- Existing raw byte streams are connection scoped. Dynamic policy changes apply
212239
to new connections or the next parsed HTTP request where the proxy can safely
213240
re-evaluate.

architecture/security-policy.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,10 @@ Sandbox events that represent observable behavior use OCSF structured logs:
194194

195195
Use plain tracing for internal plumbing such as retries, debug state, and
196196
intermediate steps where the final observable event is logged separately.
197+
Forward-proxy success is a final observable event: emit it only after
198+
middleware, token grants, credential rewriting, policy-generation checks, and
199+
the HTTP relay have succeeded so a later denial cannot coexist with an allowed
200+
record for the same request.
197201

198202
Never log secrets, credentials, bearer tokens, or query parameters in OCSF
199203
messages. OCSF JSONL output may be shipped to external systems.

crates/openshell-cli/src/run.rs

Lines changed: 3 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1818,39 +1818,9 @@ fn parse_driver_config_json(value: &str) -> Result<prost_types::Struct> {
18181818
));
18191819
};
18201820

1821-
Ok(prost_types::Struct {
1822-
fields: fields
1823-
.into_iter()
1824-
.map(|(key, value)| json_to_protobuf_value(value).map(|value| (key, value)))
1825-
.collect::<Result<_>>()?,
1826-
})
1827-
}
1828-
1829-
fn json_to_protobuf_value(value: serde_json::Value) -> Result<prost_types::Value> {
1830-
use prost_types::{ListValue, Struct, Value, value::Kind};
1831-
1832-
let kind = match value {
1833-
serde_json::Value::Null => Kind::NullValue(0),
1834-
serde_json::Value::Bool(value) => Kind::BoolValue(value),
1835-
serde_json::Value::Number(value) => Kind::NumberValue(value.as_f64().ok_or_else(|| {
1836-
miette!("--driver-config-json contains a number that cannot be represented")
1837-
})?),
1838-
serde_json::Value::String(value) => Kind::StringValue(value),
1839-
serde_json::Value::Array(values) => Kind::ListValue(ListValue {
1840-
values: values
1841-
.into_iter()
1842-
.map(json_to_protobuf_value)
1843-
.collect::<Result<_>>()?,
1844-
}),
1845-
serde_json::Value::Object(fields) => Kind::StructValue(Struct {
1846-
fields: fields
1847-
.into_iter()
1848-
.map(|(key, value)| json_to_protobuf_value(value).map(|value| (key, value)))
1849-
.collect::<Result<_>>()?,
1850-
}),
1851-
};
1852-
1853-
Ok(Value { kind: Some(kind) })
1821+
openshell_core::proto_struct::json_object_to_struct(fields)
1822+
.into_diagnostic()
1823+
.wrap_err("--driver-config-json contains a value that cannot be represented")
18541824
}
18551825

18561826
fn validate_cpu_quantity(value: &str) -> Result<String> {

crates/openshell-core/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ license.workspace = true
1111
repository.workspace = true
1212

1313
[dependencies]
14+
glob = { workspace = true }
1415
prost = { workspace = true }
1516
prost-types = { workspace = true }
1617
tonic = { workspace = true, features = ["channel", "tls-native-roots"] }

crates/openshell-core/README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,17 @@ router agree on provider defaults. Profiles define:
5050

5151
Do not duplicate provider-specific inference behavior in callers. Add shared
5252
behavior here, then consume it from the gateway, sandbox, and router.
53+
54+
## Middleware Contracts
55+
56+
Built-in supervisor middleware identifiers, host-selector matching, and pure
57+
configuration validation live in `openshell_core::middleware`. Policy admission
58+
and the supervisor runtime consume the same contract without introducing a
59+
dependency from the policy crate to the supervisor implementation.
60+
61+
## Protobuf Struct Conversion
62+
63+
Use `openshell_core::proto_struct` when crossing between `serde_json` values and
64+
`prost_types::{Struct, Value}`. Both conversion directions live in this module;
65+
JSON-to-protobuf conversion is fallible so callers cannot silently replace an
66+
unrepresentable number.

0 commit comments

Comments
 (0)