Skip to content

Commit d2a9791

Browse files
committed
refactor(middleware): move shared contracts to core
Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
1 parent d27085c commit d2a9791

11 files changed

Lines changed: 99 additions & 47 deletions

File tree

Cargo.lock

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

architecture/sandbox.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,9 @@ of the network rule that admitted the request. Built-ins run in-process;
7272
operator-registered services are called directly from the supervisor
7373
over the common middleware gRPC contract. The gateway validates external
7474
service capabilities and policy-owned config before delivery. Supervisors keep
75-
the last-known-good service registry when a live config reload fails.
75+
the last-known-good service registry when a live config reload fails. Built-in
76+
middleware identifiers and pure config validation live in `openshell-core` so
77+
policy admission does not depend on the supervisor runtime implementation.
7678

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

crates/openshell-core/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,10 @@ 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 and pure configuration validation
57+
live in `openshell_core::middleware`. Policy admission and the supervisor
58+
runtime consume the same contract without introducing a dependency from the
59+
policy crate to the supervisor implementation.

crates/openshell-core/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ pub mod grpc_client;
2323
pub mod image;
2424
pub mod inference;
2525
pub mod metadata;
26+
pub mod middleware;
2627
pub mod net;
2728
pub mod paths;
2829
pub mod policy;
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
//! Shared supervisor middleware identifiers and policy validation contracts.
5+
6+
use miette::{Result, miette};
7+
8+
/// Binding identifier for the built-in secret redaction middleware.
9+
pub const BUILTIN_SECRETS: &str = "openshell/secrets";
10+
11+
/// Validate policy-owned configuration for a built-in middleware.
12+
pub fn validate_builtin_config(implementation: &str, config: &prost_types::Struct) -> Result<()> {
13+
match implementation {
14+
BUILTIN_SECRETS => validate_secrets_config(config),
15+
other => Err(miette!(
16+
"middleware implementation '{other}' is not available in phase 1"
17+
)),
18+
}
19+
}
20+
21+
fn validate_secrets_config(config: &prost_types::Struct) -> Result<()> {
22+
let mode = config
23+
.fields
24+
.get("secrets")
25+
.and_then(|value| match value.kind.as_ref() {
26+
Some(prost_types::value::Kind::StringValue(value)) => Some(value.as_str()),
27+
_ => None,
28+
})
29+
.unwrap_or("redact");
30+
if mode != "redact" {
31+
return Err(miette!(
32+
"{BUILTIN_SECRETS} only supports config.secrets: redact in phase 1"
33+
));
34+
}
35+
Ok(())
36+
}
37+
38+
#[cfg(test)]
39+
mod tests {
40+
use super::*;
41+
42+
#[test]
43+
fn secrets_config_defaults_to_redact() {
44+
validate_builtin_config(BUILTIN_SECRETS, &prost_types::Struct::default()).unwrap();
45+
}
46+
47+
#[test]
48+
fn secrets_config_rejects_unsupported_mode() {
49+
let config = prost_types::Struct {
50+
fields: std::iter::once((
51+
"secrets".to_string(),
52+
prost_types::Value {
53+
kind: Some(prost_types::value::Kind::StringValue("allow".into())),
54+
},
55+
))
56+
.collect(),
57+
};
58+
59+
let error = validate_builtin_config(BUILTIN_SECRETS, &config).unwrap_err();
60+
assert!(
61+
error
62+
.to_string()
63+
.contains("only supports config.secrets: redact")
64+
);
65+
}
66+
67+
#[test]
68+
fn rejects_unknown_builtin() {
69+
let error = validate_builtin_config("openshell/unknown", &prost_types::Struct::default())
70+
.unwrap_err();
71+
assert!(
72+
error
73+
.to_string()
74+
.contains("implementation 'openshell/unknown' is not available")
75+
);
76+
}
77+
}

crates/openshell-policy/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ repository.workspace = true
1313
[dependencies]
1414
glob = { workspace = true }
1515
openshell-core = { path = "../openshell-core", default-features = false }
16-
openshell-supervisor-middleware = { path = "../openshell-supervisor-middleware" }
1716
prost-types = { workspace = true }
1817
serde = { workspace = true }
1918
serde_json = { workspace = true }

crates/openshell-policy/src/lib.rs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1532,7 +1532,7 @@ pub fn validate_sandbox_policy(
15321532
reason: "implementation must not be empty".to_string(),
15331533
});
15341534
} else if middleware.middleware.starts_with("openshell/")
1535-
&& middleware.middleware != openshell_supervisor_middleware::BUILTIN_SECRETS
1535+
&& middleware.middleware != openshell_core::middleware::BUILTIN_SECRETS
15361536
{
15371537
violations.push(PolicyViolation::InvalidMiddlewareConfig {
15381538
name: middleware.name.clone(),
@@ -1572,12 +1572,11 @@ pub fn validate_sandbox_policy(
15721572
}
15731573
}
15741574

1575-
if middleware.middleware == openshell_supervisor_middleware::BUILTIN_SECRETS {
1575+
if middleware.middleware == openshell_core::middleware::BUILTIN_SECRETS {
15761576
let config = middleware.config.clone().unwrap_or_default();
1577-
if let Err(error) = openshell_supervisor_middleware::validate_builtin_config(
1578-
&middleware.middleware,
1579-
&config,
1580-
) {
1577+
if let Err(error) =
1578+
openshell_core::middleware::validate_builtin_config(&middleware.middleware, &config)
1579+
{
15811580
violations.push(PolicyViolation::InvalidBuiltinMiddlewareConfig {
15821581
name: middleware.name.clone(),
15831582
reason: error.to_string(),

crates/openshell-supervisor-middleware/src/builtins/mod.rs

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,6 @@ pub fn describe() -> Vec<MiddlewareBinding> {
1010
vec![secrets::describe()]
1111
}
1212

13-
pub fn validate_config(binding_id: &str, config: &prost_types::Struct) -> Result<()> {
14-
match binding_id {
15-
secrets::BINDING_ID => secrets::validate_config(config),
16-
other => Err(miette!(
17-
"middleware implementation '{other}' is not available in phase 1"
18-
)),
19-
}
20-
}
21-
2213
pub fn evaluate_http_request(evaluation: &HttpRequestEvaluation) -> Result<HttpRequestResult> {
2314
match evaluation.binding_id.as_str() {
2415
secrets::BINDING_ID => secrets::evaluate_http_request(evaluation),

crates/openshell-supervisor-middleware/src/builtins/secrets.rs

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use openshell_core::proto::{
1010
};
1111
use regex::Regex;
1212

13-
pub const BINDING_ID: &str = "openshell/secrets";
13+
pub const BINDING_ID: &str = openshell_core::middleware::BUILTIN_SECRETS;
1414
const OPERATION: &str = "HttpRequest";
1515
const PHASE: &str = "pre_credentials";
1616
const MAX_BODY_BYTES: u64 = 256 * 1024;
@@ -54,21 +54,7 @@ static SECRET_PATTERNS: LazyLock<[SecretPattern; 2]> = LazyLock::new(|| {
5454
});
5555

5656
pub fn validate_config(config: &prost_types::Struct) -> Result<()> {
57-
let mode = config
58-
.fields
59-
.get("secrets")
60-
.and_then(|value| match value.kind.as_ref() {
61-
Some(prost_types::value::Kind::StringValue(value)) => Some(value.as_str()),
62-
_ => None,
63-
})
64-
.unwrap_or("redact");
65-
if mode != "redact" {
66-
return Err(miette!(
67-
"{} only supports config.secrets: redact in phase 1",
68-
BINDING_ID
69-
));
70-
}
71-
Ok(())
57+
openshell_core::middleware::validate_builtin_config(BINDING_ID, config)
7258
}
7359

7460
pub fn evaluate_http_request(evaluation: &HttpRequestEvaluation) -> Result<HttpRequestResult> {

crates/openshell-supervisor-middleware/src/lib.rs

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use std::collections::{BTreeMap, HashMap, HashSet};
1111
use std::sync::{Arc, LazyLock};
1212

1313
use miette::{Result, miette};
14+
pub use openshell_core::middleware::{BUILTIN_SECRETS, validate_builtin_config};
1415
pub use service::InProcessMiddlewareService;
1516

1617
use openshell_core::proto::middleware::v1::supervisor_middleware_server::SupervisorMiddleware;
@@ -23,18 +24,8 @@ use tokio::sync::OnceCell;
2324
use tonic::Request;
2425

2526
pub const API_VERSION: &str = "openshell.middleware.v1";
26-
pub const BUILTIN_SECRETS: &str = builtins::secrets::BINDING_ID;
2727
const HTTP_REQUEST_OPERATION: &str = "HttpRequest";
2828
const PRE_CREDENTIALS_PHASE: &str = "pre_credentials";
29-
30-
/// Validate the configuration for an in-process middleware implementation.
31-
///
32-
/// Policy admission uses this same implementation-specific validation before a
33-
/// configuration can reach the request path.
34-
pub fn validate_builtin_config(implementation: &str, config: &prost_types::Struct) -> Result<()> {
35-
builtins::validate_config(implementation, config)
36-
}
37-
3829
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3930
pub enum OnError {
4031
FailClosed,

0 commit comments

Comments
 (0)