Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 6 additions & 4 deletions .agents/skills/debug-openshell-cluster/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,24 +85,24 @@ The custom driver name must not be a reserved built-in name (`docker`, `podman`,
For configured gateway interceptors, inspect `[[openshell.gateway.interceptors]]`, their Unix or network endpoints, and gateway startup logs:

```bash
rg -n 'interceptors|provider_profile_sources|grpc_endpoint|binding_policy|failure_policy' /etc/openshell/gateway.toml
rg -n 'interceptors|provider_profile_sources|grpc_endpoint|tls_ca_cert_path|audience|allow_insecure_transport|binding_policy|failure_policy|gateway_jwt' /etc/openshell/gateway.toml
stat /run/openshell/interceptors/<name>.sock
journalctl -u <interceptor-service> --no-pager --lines=200
journalctl -u openshell-gateway --no-pager --lines=200
```

The gateway calls each interceptor's `Describe` RPC and validates its manifest at startup. Check for unreachable endpoints, invalid RPC/phase bindings, strict `allowlist` or `exact` mismatches, and `post_commit` bindings that resolve to `fail_closed`. If `provider_profile_sources` names an interceptor, that interceptor must advertise provider-profile capability and return a valid, duplicate-free catalog. A selected interceptor-only source is authoritative; include `builtin` or `user` sources explicitly when composition is intended.
The gateway calls each interceptor's `Describe` RPC and validates its manifest at startup. Check for unreachable endpoints, invalid RPC/phase bindings, strict `allowlist` or `exact` mismatches, and `post_commit` bindings that resolve to `fail_closed`. If gateway JWT signing is enabled, authenticated network interceptors require HTTPS and a valid bearer token; check the private CA path, endpoint hostname, expected audience, issuer, `kid`, and interceptor logs for token rejection. `allow_insecure_transport = true` explicitly preserves unauthenticated plaintext behavior. If `provider_profile_sources` names an interceptor, that interceptor must advertise provider-profile capability and return a valid, duplicate-free catalog. A selected interceptor-only source is authoritative; include `builtin` or `user` sources explicitly when composition is intended.

For operator-run supervisor middleware, inspect `[[openshell.supervisor.middleware]]`, service reachability, and both gateway and supervisor logs:

```bash
rg -n 'supervisor|middleware|grpc_endpoint|max_body_bytes|timeout' /etc/openshell/gateway.toml
rg -n 'supervisor|middleware|grpc_endpoint|tls_ca_cert_path|audience|allow_insecure_transport|max_body_bytes|timeout|gateway_jwt' /etc/openshell/gateway.toml
journalctl -u <middleware-service> --no-pager --lines=200
journalctl -u openshell-gateway --no-pager --lines=200
openshell logs <sandbox-name> --tail --source sandbox
```

The middleware service must start before the gateway and be reachable from both the gateway and sandbox supervisors. Gateway startup fails if `Describe` is unavailable, a manifest exposes duplicate `HttpRequest/pre_credentials` bindings, the registration claims the reserved `openshell/` namespace, or body and timeout limits are invalid. Changing a registration requires a gateway restart. A policy update can also fail before persistence if the selected implementation rejects its `network_middlewares` config.
The middleware service must start before the gateway and be reachable from both the gateway and sandbox supervisors. Gateway startup fails if `Describe` is unavailable, a manifest exposes duplicate `HttpRequest/pre_credentials` bindings, the registration claims the reserved `openshell/` namespace, or body and timeout limits are invalid. When gateway JWT signing is disabled, supervisors preserve the legacy unauthenticated connector and do not request extension credentials. When signing is enabled, credential acquisition and verification failures are fail closed: check HTTPS trust and hostname validation, audience and issuer agreement, the token `kid`, gateway `RefreshSandboxToken` errors, and middleware logs. Changing a registration requires a gateway restart. A policy update can also fail before persistence if the selected implementation rejects its `network_middlewares` config.

At request time, distinguish an explicit `middleware_denied` result from `middleware_failed`. A denial is always enforced. A failure follows the policy-local `on_error`: `fail_closed` blocks the request, while `fail_open` bypasses only that stage and emits a detection finding. If a running supervisor cannot install a new registry, it preserves its last-known-good generation and emits a configuration failure event.

Expand Down Expand Up @@ -457,12 +457,14 @@ openshell logs <sandbox-name>
| CLI TLS error | Local mTLS bundle does not match server cert/CA | Check `~/.config/openshell/gateways/<name>/mtls/` |
| Edge or OIDC gateway returns `Unauthenticated` | Stored login expired, audience/scopes mismatch, or gateway auth configuration changed | `openshell gateway info`, `openshell gateway login <name>`, gateway auth logs |
| Gateway fails before serving health after enabling an interceptor | Interceptor endpoint unavailable or manifest/binding validation failed | Gateway and interceptor logs; interceptor socket; `binding_policy`, phases, and failure policy |
| Authenticated interceptor or middleware rejects gateway calls | Private CA or hostname mismatch, expected audience or issuer mismatch, stale/unknown `kid`, or malformed extension token | `tls_ca_cert_path`, registration `audience`, service verifier config and logs; fetch well-known metadata only through the already-trusted gateway TLS endpoint |
| Provider profiles disappear after enabling an interceptor catalog | `provider_profile_sources` selected only an authoritative interceptor or returned invalid/duplicate IDs | Inspect source list and interceptor `Describe`/catalog logs; include `builtin` and `user` when intended |
| Gateway fails after registering supervisor middleware | Service unavailable, invalid manifest, duplicate binding, reserved name, or invalid body/timeout limit | Middleware service and gateway logs; `[[openshell.supervisor.middleware]]`; `Describe` response |
| Policy update rejects `network_middlewares` | Unknown middleware name, implementation-owned config invalid, duplicate order, broad/invalid host selector, or fail-closed coverage of `tls: skip` | Policy error, gateway logs, middleware `ValidateConfig`, selector and order fields |
| Policy mutation returns `FAILED_PRECONDITION` for endpoint ambiguity | Equally specific effective endpoint selectors disagree on connection or request-processing metadata | CLI error, base and provider-composed policy, affected profile attachments; confirm no new revision was stored |
| Supervisor enters policy quarantine | A runtime candidate failed validation while `policy_validation_failure_mode = "fail_closed"` | Sandbox OCSF config/finding events, validation rationale, active generation, `previous_policy_active` |
| HTTP request returns `middleware_failed` or `middleware_denied` | Selected stage failed or explicitly denied the admitted request | Sandbox OCSF logs; policy-local middleware config; service availability; `on_error` |
| Supervisor repeatedly fails to install middleware after enabling gateway JWT signing | Extension credential minting, distribution, or authenticated service connection failed; last-known-good registry remains active | Gateway `RefreshSandboxToken` logs, sandbox configuration events, service token-verification logs, registration TLS/audience settings |
| Custom compute driver is unavailable | Driver process/socket missing, inaccessible, or configured with a reserved/mismatched name | Socket ownership/mode, driver service logs, gateway `GetCapabilities` logs |
| Sandbox remains `Stopping` or `Starting` | Driver stop/start failed, retained resource is missing, or a fresh supervisor has not connected | Gateway and driver logs; `docker inspect`, `podman inspect`, Agent Sandbox status/PVC, or VM state marker and launcher process |
| Image pull failure | Gateway or sandbox image cannot be pulled | Runtime events and image pull credentials |
Expand Down
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,13 @@ These pipelines connect skills into end-to-end workflows. Individual skill files
| `crates/openshell-ocsf/` | OCSF logging | OCSF v1.7.0 event types, builders, shorthand/JSONL formatters, tracing layers |
| `crates/openshell-otel/` | OpenTelemetry support | Shared OTLP trace provider, resource, and tracing-layer construction |
| `crates/openshell-core/` | Shared core | Common types, configuration, error handling |
| `crates/openshell-extension-core/` | Extension core | Shared extension identity, JWT claims, bearer-token rotation, and TLS transport primitives |
| `crates/openshell-sdk/` | Shared client SDK | Async Rust gateway client (gRPC transport, TLS, OIDC refresh, edge tunnel); consumed by CLI, TUI, and `@openshell/sdk` |
| `crates/openshell-providers/` | Provider management | Credential provider backends |
| `crates/openshell-tui/` | Terminal UI | Ratatui-based dashboard for monitoring |
| `crates/openshell-driver-kubernetes-secrets/` | Kubernetes Secrets credential driver | In-process `CredentialDriver` backend for OpenShell-managed K8s Secret storage |
| `crates/openshell-driver-vault/` | Vault credential driver | In-process `CredentialDriver` backend for Vault-compatible KV storage |
| `crates/openshell-driver-db-credstore/` | Database credential driver | In-process `CredentialDriver` backend for gateway database credential storage |
| `crates/openshell-driver-kubernetes/` | Kubernetes compute driver | In-process `ComputeDriver` backend for K8s sandbox pods |
| `crates/openshell-driver-docker/` | Docker compute driver | In-process `ComputeDriver` backend for local Docker sandbox containers |
| `crates/openshell-driver-podman/` | Podman compute driver | In-process `ComputeDriver` backend for local Podman sandbox containers |
Expand Down
1 change: 0 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ Skills live in `.agents/skills/`. Your agent's harness can discover and load the
| Contributing | `create-github-issue` | Create well-structured GitHub issues |
| Contributing | `create-github-pr` | Create pull requests with proper conventions |
| Reviewing | `review-github-pr` | Summarize PR diffs and key design decisions |
| Reviewing | `review-security-changes` | Review code changes for security vulnerabilities and boundary regressions |
| Reviewing | `review-security-issue` | Assess security issues for severity and remediation |
| Reviewing | `fix-security-issue` | Implement an approved security remediation plan |
| Reviewing | `watch-github-actions` | Monitor CI pipeline status and logs |
Expand Down
24 changes: 22 additions & 2 deletions Cargo.lock

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

20 changes: 20 additions & 0 deletions architecture/gateway.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,26 @@ until deliberately added to this allowlist. Interception remains centralized:
allowlisting a unary RPC does not require method-specific gateway
instrumentation.

Remote extension clients share `openshell-extension-core` transport and bearer
primitives. When gateway JWT signing is configured, the gateway mints
short-lived, exact-audience EdDSA credentials for middleware and interceptors,
rotates their in-memory slots without rebuilding clients, and publishes the
public verification key at `/.well-known/jwks.json` alongside OIDC-shaped
discovery metadata at `/.well-known/openid-configuration`. HTTPS extensions can
pin an operator-provided CA while retaining endpoint-hostname verification.

Extension credentials reuse the sandbox signing key and are separated from
sandbox-to-gateway admission tokens by exact audience and by an explicit
`typ` of `openshell-ext+jwt`, so a verifier that checks either one alone
cannot confuse the two. After authenticated `Describe` succeeds, a service may
advertise `expected_audience` as a post-authentication consistency assertion;
a mismatch against operator configuration fails gateway startup. A strict
verifier may reject an incorrect audience before returning the manifest. A
registration may opt out of extension authentication entirely with
`allow_insecure_transport`, which permits a plaintext endpoint, attaches no
credential, and warns at every startup. Credential minting is bounded per
sandbox because it resolves the caller's effective policy.

Each configured interceptor selects a binding policy. `dynamic` accepts valid
manifest declarations and preserves the compatibility behavior. `allowlist`
enables only operator-configured RPCs and phases, while `exact` requires the
Expand Down
14 changes: 14 additions & 0 deletions architecture/sandbox.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,20 @@ generation and preserves the last-known-good generation if preparation fails.
Policy-only updates reuse the connected registry, so an external middleware
outage cannot block unrelated policy changes.

For authenticated operator middleware, the supervisor requests credentials by
registration name through `RefreshSandboxToken`. The gateway resolves names
against the effective policy and mints exact-audience credentials. The
supervisor keeps them in refreshable in-memory slots outside stable middleware
configuration, so rotation neither changes `config_revision` nor reconnects
the registry. Public custom-CA PEM travels with the stable registration.

The slots live in a supervisor-owned `ExtensionCredentialStore` shared by every
gateway connection the supervisor opens, so the registry's clients and the
polling loop that rotates them observe the same credentials. Configuration
polling runs far more frequently than credentials expire, so the loop rotates
only when a credential is missing or has passed four fifths of its lifetime,
and bounds its sleep by the soonest rotation deadline.

Middleware cannot observe injected credentials or mutate supervisor-owned
credential, routing, or framing headers. Body transformations are re-evaluated
against body-aware L7 policy before later stages or the upstream can observe
Expand Down
1 change: 1 addition & 0 deletions crates/openshell-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ repository.workspace = true

[dependencies]
async-trait = "0.1"
openshell-extension-core = { path = "../openshell-extension-core" }
glob = { workspace = true }
prost = { workspace = true }
prost-types = { workspace = true }
Expand Down
40 changes: 40 additions & 0 deletions crates/openshell-core/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
//! Configuration management for `OpenShell` components.

use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::collections::BTreeMap;
use std::fmt;
#[cfg(unix)]
Expand Down Expand Up @@ -624,6 +625,19 @@ pub struct GatewayInterceptorConfig {
/// Interceptor gRPC endpoint. Supports `http://`, `https://`, and
/// `unix://` endpoints.
pub grpc_endpoint: String,
/// Optional PEM trust-root bundle for an HTTPS endpoint. The gateway
/// loads this file during interceptor initialization.
#[serde(default)]
pub tls_ca_cert_path: Option<PathBuf>,
/// Exact JWT audience for this service. When omitted, a kind-scoped value
/// is derived from the configured registration name.
#[serde(default)]
pub audience: Option<String>,
/// Opt out of extension authentication for this interceptor, permitting a
/// plaintext `http://` endpoint with no bearer credential. Development and
/// trusted-network deployments only.
#[serde(default)]
pub allow_insecure_transport: bool,
/// Deterministic service ordering. Lower values run first.
#[serde(default)]
pub order: i32,
Expand All @@ -649,6 +663,19 @@ pub struct GatewayInterceptorConfig {
pub bindings: Vec<GatewayInterceptorBindingOverride>,
}

impl GatewayInterceptorConfig {
/// Resolve the configured JWT audience to its deterministic default.
pub fn resolved_audience(&self) -> Cow<'_, str> {
self.audience
.as_deref()
.filter(|audience| !audience.is_empty())
.map_or_else(
|| Cow::Owned(format!("urn:openshell:extension:interceptor:{}", self.name)),
Cow::Borrowed,
)
}
}

/// Operator policy for authorizing interceptor manifest bindings.
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
Expand Down Expand Up @@ -1199,6 +1226,19 @@ mod tests {
defaulted.binding_policy,
GatewayInterceptorBindingPolicy::Dynamic
);
assert_eq!(
defaulted.resolved_audience(),
"urn:openshell:extension:interceptor:governance"
);
let explicitly_empty = GatewayInterceptorConfig {
name: "governance".to_string(),
audience: Some(String::new()),
..GatewayInterceptorConfig::default()
};
assert_eq!(
explicitly_empty.resolved_audience(),
"urn:openshell:extension:interceptor:governance"
);
assert_eq!(allowlist, GatewayInterceptorBindingPolicy::Allowlist);
assert_eq!(exact, GatewayInterceptorBindingPolicy::Exact);
}
Expand Down
Loading
Loading