Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
893be80
feat(proxy): bind static credentials to provider endpoints
johntmyers Jul 28, 2026
ea56878
test(e2e): verify static credential endpoint isolation
johntmyers Jul 28, 2026
30b8736
docs(provider): explain static credential endpoint binding
johntmyers Jul 28, 2026
6d36f25
fix(e2e): use valid endpoint isolation fixtures
johntmyers Jul 28, 2026
e2b271a
test(proxy): stabilize OCSF compatibility capture
johntmyers Jul 28, 2026
6231d64
test(proxy): avoid global tracing capture race
johntmyers Jul 28, 2026
94491fe
docs(provider): explain static credential endpoint binding
johntmyers Jul 28, 2026
131b261
fix(credentials): preserve binding identity across rotations
johntmyers Jul 28, 2026
59b7c13
fix(proxy): enforce bindings across request lifecycle
johntmyers Jul 28, 2026
62d3c21
fix(proxy): close credential relay gaps
johntmyers Jul 28, 2026
44ba683
docs(credentials): clarify binding failure behavior
johntmyers Jul 28, 2026
b92f769
fix(credentials): hash selected provider profile scope
johntmyers Jul 28, 2026
348859f
fix(proxy): resolve credentials after request admission
johntmyers Jul 28, 2026
42a454b
docs(credentials): clarify binding failure diagnostics
johntmyers Jul 28, 2026
9c56260
fix(proxy): align single-route credential denials
johntmyers Jul 29, 2026
a06b272
fix(credentials): harden endpoint-bound rotation
johntmyers Jul 29, 2026
3f5be52
fix(credentials): enforce identity and authority binding
johntmyers Jul 29, 2026
a5b493f
fix(credentials): snapshot provider environment atomically
johntmyers Jul 29, 2026
e897bcf
test(e2e): include authority port in query proxy requests
johntmyers Jul 29, 2026
c6b5e67
fix(credentials): close credential revocation gaps
johntmyers Jul 30, 2026
feec81d
docs(proxy): explain authority mismatch diagnostics
johntmyers Jul 30, 2026
0a719e5
fix(credentials): enforce binding lifecycle invariants
johntmyers Jul 30, 2026
8f656f4
fix(provider): reject credential config collisions
johntmyers Jul 30, 2026
146dc1b
fix(network): capture credential scope atomically
johntmyers Jul 30, 2026
dc7d26a
fix(network): distinguish origin and absolute targets
johntmyers Jul 30, 2026
18fbf27
fix(provider): isolate endpointless profile credentials
johntmyers Jul 30, 2026
da06ea5
fix(network): normalize IPv6 request authorities
johntmyers Jul 30, 2026
1387eac
docs(credentials): clarify endpointless profile isolation
johntmyers Jul 30, 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
22 changes: 22 additions & 0 deletions .agents/skills/debug-inference/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,26 @@ Check instead:
3. The sandbox has that provider attached (`openshell sandbox provider list [name]`)
4. `network_policies` allow that host, port, and HTTP rules

If the response reports `credential_endpoint_mismatch`, the provider is attached
but its credential profile does not authorize that request recipient. Run
`openshell provider get <provider-name>` to identify the provider type, then
inspect its profile endpoints with
`openshell provider profile export <type> -o yaml`. That export uses the current
workspace scope; add `--global` when the provider was created with
`--global-profile`. Compare the profile's endpoint host, port, and path with the
direct request. Correct the provider selection or profile endpoint when that
recipient is intentional. Do not widen the sandbox network policy to work around
the mismatch: policy admission and credential endpoint authorization are
separate checks, and the provider profile should authorize only intended
credential recipients.

If the response reports `request_authority_mismatch`, compare the HTTP request
authority with the CONNECT tunnel endpoint. The host and effective port must
match. For a tunnel to `api.example.com:8443`, send
`Host: api.example.com:8443`; omitting the non-default port makes the request
authority use the transport default and OpenShell rejects it. An absolute-form
request target must use the same authority.

Attach or detach a provider on an existing sandbox with `openshell sandbox provider attach <sandbox> <provider>` and `openshell sandbox provider detach <sandbox> <provider>`.

Use the `generate-sandbox-policy` skill when the user needs help authoring policy YAML.
Expand Down Expand Up @@ -348,6 +368,8 @@ Both commands should return the upstream model list.
| `no compatible route` | Provider type does not match request shape | Create or select a provider of the matching type, or change the client API |
| `inference.local` works but a platform function fails | User route is configured but `sandbox-system` is missing or wrong | `openshell inference get --system`; configure or update with `--system`; inspect supervisor logs |
| Direct call to external host is denied | Missing policy or provider attachment | Update `network_policies` and launch sandbox with the right provider |
| Direct call returns `credential_endpoint_mismatch` | Attached provider profile does not authorize the request host, port, or path | Inspect the provider profile endpoints; select or update the profile only if it intentionally authorizes that recipient |
| Direct call returns `request_authority_mismatch` | HTTP authority does not match the CONNECT host and effective port | Include the explicit non-default port in `Host` and use the same authority in absolute-form targets |
| SDK fails on empty auth token | Client requires a non-empty API key even though OpenShell injects the real one | Use any placeholder token such as `test` |
| Upstream timeout from container to host-local backend | Host firewall or network config blocks container-to-host traffic | Allow the Docker bridge subnet to reach the inference port on the host gateway IP (see firewall fix section above) |

Expand Down
6 changes: 6 additions & 0 deletions .agents/skills/generate-sandbox-policy/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,9 @@ Before presenting the policy to the user, verify correctness **and** flag breadt

- [ ] `protocol: rest` on port 443 should have `tls: terminate`
- [ ] HTTP methods are standard: GET, HEAD, POST, PUT, DELETE, PATCH, OPTIONS, or `*`
- [ ] Credentialed destinations are also covered by the attached provider
profile endpoint; policy admission alone does not authorize credential
resolution

### Structural Checks

Expand Down Expand Up @@ -443,6 +446,9 @@ The policy needs to go somewhere. Determine which mode applies:
2. **Check for conflicts**:
- 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.
- 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.
- If the sandbox uses an attached provider credential, confirm the provider
profile also declares the intended host, port, and path. A sandbox policy
allow cannot expand the profile's static credential binding.

3. **Apply the change**:
- **Adding a new policy**: Insert the new policy block under `network_policies`, maintaining the file's existing indentation and style.
Expand Down
17 changes: 16 additions & 1 deletion .agents/skills/openshell-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,22 @@ Bare `KEY` reads the value from the environment variable of that name and avoids

Other credential sources are `--from-gcloud-adc` for compatible profiles and `--runtime-credentials` when the gateway or sandbox resolves the required credentials at runtime.

Profile-backed provider policy and composition are controlled by the gateway-global `providers_v2_enabled` setting:
Static provider credentials resolve only for hosts, ports, and paths declared by
the provider profile. Use `provider profile export` to inspect that boundary
when a placeholder is present but requests receive
`credential_endpoint_mismatch`. A profileless static provider fails closed
because the gateway cannot construct a binding.

When an inspected request receives `request_authority_mismatch`, compare its
HTTP authority with the CONNECT tunnel endpoint. The host and effective port
must match. For a tunnel to `api.example.com:8443`, send
`Host: api.example.com:8443`; `Host: api.example.com` omits the non-default
port and is rejected. An absolute-form request target must use the same
authority.

Profile-backed provider policy composition is controlled by the gateway-global
`providers_v2_enabled` setting. Static credential endpoint binding remains
active even when policy composition is disabled:

```bash
openshell settings get --global
Expand Down
12 changes: 12 additions & 0 deletions architecture/gateway.md
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,18 @@ resolution and again by the sandbox placeholder resolver. This keeps expired
credentials from resolving even when a running sandbox still has retained
placeholder generations from an earlier provider credential snapshot.

Static credential delivery is capability-negotiated and endpoint-bound. The
gateway classifies each returned environment entry as either a credential or
non-secret provider configuration and associates every credential key with the
host, port, and path selectors from its effective provider profile. It withholds
static credential material from supervisors that do not advertise binding
support. If a selected provider profile has no usable endpoint, the gateway
withholds only that profile's static credential keys and their expiry and
binding metadata. It continues to return provider-generated non-secret
configuration, valid endpoint-bound static credentials from other attached
providers, and the dynamic credential snapshot. Provider environment revisions
include profile endpoint and binding changes.

## Inference Resolution

Cluster inference routes store only `provider_name`, `model_id`, and optional
Expand Down
18 changes: 18 additions & 0 deletions architecture/sandbox.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,24 @@ its guarded single-request relay while sharing authorization, request context,
policy-pinning, and destination boundaries.
Adapter-specific response and OCSF event shapes remain at the protocol boundary.

Provider credential placeholders are resolved through the live provider state
for each HTTP request, after destination and L7 policy admission. A static
credential resolves only when the request host, port, and path match an endpoint
in that provider's effective profile. CONNECT, absolute-form forward HTTP,
request targets, headers, supported request bodies, SigV4 signing, and opted-in
WebSocket text rewriting use the same scoped resolver. Provider refresh swaps
credential values and endpoint bindings atomically. An invalid or unavailable
refresh revokes the previous static credential state instead of leaving a
partially active or last-known-good static set. Invalid metadata preserves the
supplied dynamic snapshot, while a fetch failure preserves the currently active
dynamic snapshot.

Route selection and policy evaluation use a syntax-only redacted request target;
they do not materialize real credentials. Cross-endpoint placeholder use returns
HTTP 403. After a WebSocket upgrade it closes the connection with policy
violation code 1008. Both paths emit a denied activity event and a detection
finding without logging the placeholder, environment key, secret, or query.

For inspected HTTP traffic, the proxy can enforce REST method/path rules,
WebSocket upgrade and text-message rules, GraphQL operation rules, and
MCP method, tool, and supported params rules or generic JSON-RPC method rules
Expand Down
39 changes: 39 additions & 0 deletions crates/openshell-core/src/endpoint_path.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

//! Canonical provider endpoint path matching shared by policy and runtime code.

/// Return whether `path` is selected by a provider endpoint path pattern.
///
/// Empty paths and `**` match every request path. A trailing `/**` matches the
/// named path itself and every descendant. Other patterns use glob semantics.
#[must_use]
pub fn matches(pattern: &str, path: &str) -> bool {
if pattern.is_empty() || pattern == "**" || pattern == "/**" {
return true;
}
if pattern == path {
return true;
}
if let Some(prefix) = pattern.strip_suffix("/**") {
return path == prefix || path.starts_with(&format!("{prefix}/"));
}
glob::Pattern::new(pattern).is_ok_and(|glob| glob.matches(path))
}

#[cfg(test)]
mod tests {
use super::matches;

#[test]
fn matches_canonical_endpoint_patterns() {
assert!(matches("", "/v1/messages"));
assert!(matches("/**", "/v1/messages"));
assert!(matches("/v1/**", "/v1"));
assert!(matches("/v1/**", "/v1/messages"));
assert!(matches("/v*/messages", "/v1/messages"));
assert!(matches("/v1/*", "/v1/chat/messages"));
assert!(!matches("/v1/**", "/v2/messages"));
assert!(!matches("/v1/*/messages", "/v1/chat/completions"));
}
}
5 changes: 5 additions & 0 deletions crates/openshell-core/src/grpc_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -741,6 +741,7 @@ pub async fn fetch_provider_environment(
let response = client
.get_sandbox_provider_environment(GetSandboxProviderEnvironmentRequest {
sandbox_id: sandbox_id.to_string(),
supports_static_credential_bindings: true,
})
.await
.into_diagnostic()?;
Expand All @@ -751,6 +752,8 @@ pub async fn fetch_provider_environment(
provider_env_revision: inner.provider_env_revision,
credential_expires_at_ms: inner.credential_expires_at_ms,
dynamic_credentials: inner.dynamic_credentials,
static_credential_bindings: inner.static_credential_bindings,
non_secret_environment_keys: inner.non_secret_environment_keys,
})
}

Expand Down Expand Up @@ -840,6 +843,8 @@ pub struct ProviderEnvironmentResult {
pub provider_env_revision: u64,
pub credential_expires_at_ms: HashMap<String, i64>,
pub dynamic_credentials: HashMap<String, crate::proto::ProviderProfileCredential>,
pub static_credential_bindings: HashMap<String, crate::proto::StaticCredentialBinding>,
pub non_secret_environment_keys: Vec<String>,
}

impl CachedOpenShellClient {
Expand Down
1 change: 1 addition & 0 deletions crates/openshell-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ pub mod config;
pub mod denial;
pub mod driver_mounts;
pub mod driver_utils;
pub mod endpoint_path;
pub mod error;
pub mod forward;
pub mod google_cloud;
Expand Down
Loading
Loading