Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
6726086
feat(proxy): bind static credentials to provider endpoints
johntmyers Jul 28, 2026
8d51ff3
test(e2e): verify static credential endpoint isolation
johntmyers Jul 28, 2026
c77dd21
docs(provider): explain static credential endpoint binding
johntmyers Jul 28, 2026
56ec811
fix(e2e): use valid endpoint isolation fixtures
johntmyers Jul 28, 2026
315ac52
docs(provider): explain static credential endpoint binding
johntmyers Jul 28, 2026
c960bce
fix(credentials): preserve binding identity across rotations
johntmyers Jul 28, 2026
70f1313
fix(proxy): enforce bindings across request lifecycle
johntmyers Jul 28, 2026
d97149a
fix(proxy): close credential relay gaps
johntmyers Jul 28, 2026
d49930a
docs(credentials): clarify binding failure behavior
johntmyers Jul 28, 2026
f8a4f69
fix(credentials): hash selected provider profile scope
johntmyers Jul 28, 2026
d8c6cf9
fix(proxy): resolve credentials after request admission
johntmyers Jul 28, 2026
866a668
docs(credentials): clarify binding failure diagnostics
johntmyers Jul 28, 2026
781518c
fix(proxy): align single-route credential denials
johntmyers Jul 29, 2026
afcead4
fix(credentials): harden endpoint-bound rotation
johntmyers Jul 29, 2026
03a786f
fix(credentials): enforce identity and authority binding
johntmyers Jul 29, 2026
dbf240e
fix(credentials): snapshot provider environment atomically
johntmyers Jul 29, 2026
1c0e678
test(e2e): include authority port in query proxy requests
johntmyers Jul 29, 2026
3cddf32
fix(credentials): close credential revocation gaps
johntmyers Jul 30, 2026
360ce5f
docs(proxy): explain authority mismatch diagnostics
johntmyers Jul 30, 2026
dff4da9
fix(credentials): enforce binding lifecycle invariants
johntmyers Jul 30, 2026
51eb100
fix(provider): reject credential config collisions
johntmyers Jul 30, 2026
8325664
fix(network): capture credential scope atomically
johntmyers Jul 30, 2026
c7b369f
fix(network): distinguish origin and absolute targets
johntmyers Jul 30, 2026
ee4b82a
fix(provider): isolate endpointless profile credentials
johntmyers Jul 30, 2026
18ee780
fix(network): normalize IPv6 request authorities
johntmyers Jul 30, 2026
756f195
docs(credentials): clarify endpointless profile isolation
johntmyers Jul 30, 2026
0874137
feat(policy): bind endpointless provider credentials
johntmyers Jul 30, 2026
e22e710
fix(credentials): use current GCP placeholder revision
johntmyers Jul 30, 2026
ce23b55
docs(providers): explain policy credential bindings
johntmyers Jul 30, 2026
27b1ce8
test(credentials): cover endpointless fail-closed invariant
johntmyers Jul 30, 2026
4500514
test(policy): expect ambiguity rejection at creation
johntmyers Jul 30, 2026
2035963
test(server): authenticate rebased policy requests
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 @@ -122,7 +122,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 @@ -407,6 +407,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
8 changes: 8 additions & 0 deletions architecture/security-policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,14 @@ with the sandbox's ephemeral CA and inspect method/path or protocol-specific
metadata before forwarding. The proxy also supports credential injection on
terminated HTTP streams when policy allows the endpoint.

Static provider credentials have an independent endpoint-binding boundary.
Provider profile endpoints define that boundary by default. An endpointless
profile can delegate binding authority to sandbox policy through an endpoint
that names the concrete attached provider instance. The gateway rejects
unattached, profileless, endpointful, and gateway-global uses of that policy
binding. Policy endpoint changes rotate the provider-environment revision so
the supervisor installs policy and credential binding snapshots atomically.

Raw streams and long-lived response bodies are connection scoped. Policy
generation changes close relays pinned to the previous generation instead of
allowing them to continue under stale authorization. HTTP upgrades switch to
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