Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
5cbb47d
feat(supervisor): add managed Pi admission bridge
johnnygreco Aug 11, 2026
30dda21
refactor(supervisor): generalize agent admission bridge
johnnygreco Aug 18, 2026
83ec94f
chore(supervisor): merge upstream main
johnnygreco Aug 26, 2026
2819502
feat(gateway): append local config fragment
johnnygreco Aug 28, 2026
219ecc7
fix(sandbox): synchronize rebuilt policy generation
johnnygreco Aug 28, 2026
b0b9381
fix(middleware): apply payload ceilings per binding
johnnygreco Aug 28, 2026
af3173b
feat(middleware): isolate managed agent attestations
johnnygreco Aug 28, 2026
8332c45
feat(providers): support proxy-delivered static header credentials
johnnygreco Aug 31, 2026
9ac8f8d
refactor(sandbox): remove legacy Pi bridge alias
johnnygreco Sep 2, 2026
db5a3bb
feat(providers): support proxy-delivered static header credentials
johnnygreco Aug 31, 2026
5f542b7
docs(agent): remove legacy bridge alias guidance
johnnygreco Sep 2, 2026
4dc92be
docs(skills): explain proxy credential delivery
johnnygreco Sep 2, 2026
8f208f5
feat(sandbox): allow admission without handle
johnnygreco Sep 2, 2026
bd5d278
feat(providers): drive proxy delivery from credential bindings
johnnygreco Sep 2, 2026
8f7107e
feat(sandbox): encode admission bodies as base64
johnnygreco Sep 2, 2026
f5212b5
fix(sandbox): hide admission metadata
johnnygreco Sep 2, 2026
1e2b164
feat(sandbox): authenticate admission bridge callers
johnnygreco Sep 2, 2026
25fb8a6
chore: merge upstream main
johnnygreco Sep 2, 2026
abf1a62
fix(sdk): round-trip credential delivery in Go
johnnygreco Sep 2, 2026
57cbc0d
feat(providers): merge binding-driven proxy credential delivery
johnnygreco Sep 2, 2026
4d7194d
fix(providers): inject credentials in route-selected relays
johnnygreco Sep 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 20 additions & 8 deletions architecture/gateway.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,8 +177,9 @@ their profile payloads.

Each logical gateway request captures the selected sources into one validated,
immutable effective catalog before deriving provider behavior. Policy layers,
credential scope, injected environment material, dynamic token grants, and
provider-environment revisions use that same catalog. Each configured source is
credential scope, injected environment material, dynamic token grants,
proxy-delivered static credential bindings, and provider-environment revisions
use that same catalog. Each configured source is
therefore fetched at most once per request, and a source revision change becomes
visible on the next request instead of partway through the current request. The
capture emits debug diagnostics with the combined catalog revision, source fetch
Expand Down Expand Up @@ -479,12 +480,23 @@ target so an edited export cannot overwrite a different profile. Database
migrations backfill existing rows with version 1.

Provider profile imports, updates, and deletes hold the sandbox synchronization
guard while checking attached-sandbox dynamic token grant ambiguity or in-use
state and writing the profile record. Sandbox creation with initial providers and
sandbox provider attach/detach use the same guard, so one gateway process cannot
interleave a profile mutation with a sandbox provider-set mutation that would
leave an ambiguous final dynamic-token state or a deleted custom profile that is
still referenced by a sandbox.
guard while checking attached-sandbox runtime-injected credential ambiguity or
in-use state and writing the profile record. Runtime-injected credentials are
dynamic token grants and proxy-delivered static credentials. Token grants on
overlapping selectors are ambiguous only at equal specificity; proxy-delivered
credentials are ambiguous on any overlap of the same port, because the workload
sends no credential the proxy could use to disambiguate. Sandbox creation with
initial providers and sandbox provider attach/detach use the same guard, so one
gateway process cannot interleave a profile mutation with a sandbox provider-set
mutation that would leave an ambiguous final runtime-credential state or a
deleted custom profile that is still referenced by a sandbox.

Proxy-delivered credential values are validated against their declared
placement when a provider is created or updated: `bearer` values must be
`token68` and named `header` values must not contain control characters. The
gateway copies the delivery mode, auth style, and header name onto the static
credential binding it sends to the sandbox so the supervisor never needs profile
metadata to build the header.

Policy and runtime settings are delivered together through the effective sandbox
config path. A gateway-global policy can override sandbox-scoped policy. The
Expand Down
31 changes: 31 additions & 0 deletions architecture/sandbox.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,20 @@ middleware registry validates implementation-owned config. The generic
registry and chain runner live in `openshell-supervisor-middleware`; first-party
implementations live in `openshell-supervisor-middleware-builtins`.

Managed agent admission reuses the operator middleware registry without joining
the HTTP chain. When one configured operator middleware advertises exact
`AGENT_CONVERSATION/AGENT_CONTEXT` hook and schema bindings, the sandbox
supervisor binds a loopback-only bridge inside the workload network namespace.
The bridge stamps sandbox and provider identity, retains an allowed response's
attestation, and returns a bounded opaque handle with the decision and optional
replacement. Provider egress strips and resolves that handle, exposing the
attestation only to the matching middleware stage. Handles are scoped to the
sandbox, middleware, provider target, and runtime generation and remain
retryable only for their bounded lifetime, so partial policy or registry reloads
cannot mix admission and egress state. An allowed admission result without an
attestation returns no handle; append-time checks need only a decision, while an
attested provider-context check supplies the handle used at provider egress.

The supervisor installs policy and middleware registry changes as one runtime
generation and preserves the last-known-good generation if preparation fails.
Policy-only updates reuse the connected registry, so an external middleware
Expand Down Expand Up @@ -394,6 +408,23 @@ subject, and gateway SPIFFE subject, and their cache lifetime is capped by the
intermediate token response, stored subject-token expiry, and supervisor SVID
expiry.

Static credentials may instead opt into proxy delivery (`delivery: proxy` on a
`bearer` or named `header` profile credential). The gateway marks the static
credential binding with the delivery mode and placement metadata, and the
supervisor removes the key from the child environment so the workload holds
neither the secret nor a placeholder. For an inspected REST request whose
endpoint matches a proxy-delivered binding, the proxy resolves the value through
the same request-scoped resolver used for placeholders and replaces the complete
header immediately before the upstream write, after network policy, L7 rules,
and middleware have admitted the request. The binding is the only source of
truth: the proxy does not consult profile metadata at request time. Aliases of
one credential collapse into a single header; distinct matching credentials fail
closed because the gateway already rejects that configuration. Injection
requires an inspected REST endpoint without `tls: skip`, so uninspected traffic
forwards whatever header the application sent. Success and failure both emit an
OCSF HTTP activity event naming the environment key and endpoint but never the
value.

For AWS endpoints that require request-level signing, the proxy supports SigV4
re-signing. When `credential_signing: sigv4` is set on an L7 endpoint, the proxy
strips the client's placeholder-based AWS auth headers, re-signs with real
Expand Down
27 changes: 25 additions & 2 deletions crates/openshell-core/src/middleware.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ use tokio::sync::mpsc;
use tonic::{Request, Response, Status};

use crate::proto::{
HttpHeader, HttpRequestEvaluation, HttpRequestResult, HttpRequestTarget, MiddlewareManifest,
RequestContext, SupervisorMiddlewarePhase, ValidateConfigRequest, ValidateConfigResponse,
AgentConversationEvaluation, AgentConversationResult, HttpHeader, HttpRequestEvaluation,
HttpRequestResult, HttpRequestTarget, MiddlewareManifest, RequestContext,
SupervisorMiddlewarePhase, ValidateConfigRequest, ValidateConfigResponse,
WebSocketSessionEvent, WebSocketSessionEventResult,
};

Expand Down Expand Up @@ -43,6 +44,11 @@ pub trait SupervisorMiddlewareEndpoint: Send + Sync {
request: Request<HttpRequestEvaluation>,
) -> Result<Response<HttpRequestResult>, Status>;

async fn evaluate_agent_conversation(
&self,
request: Request<AgentConversationEvaluation>,
) -> Result<Response<AgentConversationResult>, Status>;

async fn open_websocket_session(
&self,
requests: mpsc::Receiver<WebSocketSessionEvent>,
Expand All @@ -62,6 +68,7 @@ pub struct HttpRequestView<'a> {
headers: &'a [HttpHeader],
body: &'a [u8],
middleware_name: &'a str,
agent_attestation: &'a [u8],
}

impl<'a> HttpRequestView<'a> {
Expand All @@ -84,9 +91,17 @@ impl<'a> HttpRequestView<'a> {
headers,
body,
middleware_name,
agent_attestation: &[],
}
}

/// Attach a supervisor-resolved agent admission attestation to this view.
#[must_use]
pub fn with_agent_attestation(mut self, agent_attestation: &'a [u8]) -> Self {
self.agent_attestation = agent_attestation;
self
}

/// Return the typed middleware phase selected for this invocation.
#[must_use]
pub fn phase(self) -> SupervisorMiddlewarePhase {
Expand Down Expand Up @@ -129,6 +144,13 @@ impl<'a> HttpRequestView<'a> {
pub fn middleware_name(self) -> &'a str {
self.middleware_name
}

/// Return the supervisor-resolved agent admission attestation for this
/// middleware stage. The value is empty for ordinary requests.
#[must_use]
pub fn agent_attestation(self) -> &'a [u8] {
self.agent_attestation
}
}

/// Asynchronous contract for supervisor middleware that runs in-process.
Expand Down Expand Up @@ -176,6 +198,7 @@ impl<'a> HttpRequestView<'a> {
/// phase: SupervisorMiddlewarePhase::PreCredentials as i32,
/// max_payload_bytes: 1024,
/// timeout: String::new(),
/// ..Default::default()
/// }],
/// expected_audience: String::new(),
/// }
Expand Down
20 changes: 1 addition & 19 deletions crates/openshell-core/src/oauth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -239,25 +239,7 @@ pub fn validate_access_token(token: &str) -> Result<()> {
Ok(())
}

fn is_token68(token: &str) -> bool {
let mut padding_started = false;
let mut saw_value = false;
for byte in token.bytes() {
if byte == b'=' {
padding_started = true;
continue;
}
if padding_started || !is_token68_value_byte(byte) {
return false;
}
saw_value = true;
}
saw_value
}

fn is_token68_value_byte(byte: u8) -> bool {
byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~' | b'+' | b'/')
}
pub use crate::provider_credentials::is_token68;

fn failure_message(status: reqwest::StatusCode, body: &str) -> String {
let Ok(error_response) = serde_json::from_str::<OAuthErrorResponse>(body) else {
Expand Down
Loading
Loading