167 Commits
Author SHA1 Message Date
Evan Lezar f00ad23a26 fix(podman): tolerate shutdown transport closes (#2498)
Signed-off-by: Evan Lezar <elezar@nvidia.com>
2026-07-28 08:00:37 +00:00
Evan Lezar 39bf94e528 fix(server): bind gateway listeners before sandbox resume (#2495)
* refactor(server): extract gateway listener binding

Signed-off-by: Evan Lezar <elezar@nvidia.com>

* fix(server): bind gateway listeners before sandbox resume

Signed-off-by: Evan Lezar <elezar@nvidia.com>

---------

Signed-off-by: Evan Lezar <elezar@nvidia.com>
2026-07-27 15:02:04 +00:00
Andrew White 76a5397e6f fix: assorted arithmetic and indexing robustness fixes (#2451)
* fix(core): avoid i64 underflow on expired GCP token

When a cached GCP access token was already expired (expires_at_ms < now),
the remaining lifetime calculation used plain subtraction:
(expires_at_ms - now) / 1000. This underflows in debug builds and wraps
in release builds.

Use saturating_sub so an expired token simply yields a non-positive
remaining lifetime and is skipped. Add a regression test.

Signed-off-by: Andrew White <andrewh@cdw.com>

* fix(server): avoid i64 underflow in lease age calculation

The lease steal path computed age as now_ms() - record.updated_at_ms.
If the stored updated_at timestamp is in the future (e.g. clock skew),
the subtraction underflows. Use saturating_sub to treat a future
timestamp as age 0, keeping the lease considered fresh.

Signed-off-by: Andrew White <andrewh@cdw.com>

* fix(server): avoid i64 overflow on large credential max lifetime

max_lifetime_seconds is only validated as >= 0. The code multiplied the
raw i64 value by 1000 to compute max_expires, which overflows for
values above i64::MAX / 1000. The capped i32 value used for the STS
request was already computed but not reused.

Compute max_lifetime_ms from the capped value with saturating_mul and
use it for both expires_at fallback and max_expires.

Signed-off-by: Andrew White <andrewh@cdw.com>

* fix(server): avoid hardcoded indexing into base_url_config_keys

resolve_vertex_ai_route assumed every inference provider profile has at
least two base URL config keys by indexing [0] and [1]. Only the Vertex
profile satisfies that today; other profiles would panic here.

Iterate base_url_config_keys with find_map instead, and add a test with
an empty key list to ensure no panic.

Signed-off-by: Andrew White <andrewh@cdw.com>

* fix(server): clamp lease age at zero for future timestamps

Address review feedback: add regression coverage for future-timestamp
lease clock skew. Extract lease_is_expired() and clamp the age at zero —
i64::saturating_sub saturates at i64::MIN, not zero, so a future
updated_at_ms must be clamped explicitly to be treated as age zero and
remain unstealable with a positive TTL.

Signed-off-by: Andrew White <andrewh@cdw.com>

* fix(server): fall back past blank preferred Vertex base URL

Address review feedback: find_map stopped at the first present key even
when its value was blank, and the outer filter discarded it without
considering lower-priority aliases. Filter blank values inside the
closure so a blank preferred key falls back to a valid alias, matching
the documented priority order. Add a regression test with a blank
preferred key and a valid fallback.

Signed-off-by: Andrew White <andrewh@cdw.com>

* fix(server): saturate AWS STS expiry cap on saturated clock

Address review feedback: now_ms + max_lifetime_ms could still overflow
when current_time_ms() saturates near i64::MAX. Compute
max_expires = now_ms.saturating_add(max_lifetime_ms) once and use it for
both the conversion fallback and the expiry cap. Add a boundary unit
test covering a saturated clock.

Signed-off-by: Andrew White <andrewh@cdw.com>

---------

Signed-off-by: Andrew White <andrewh@cdw.com>
2026-07-27 14:39:40 +00:00
Grace Smith deced8716b refactor(policy): extract shared L7 endpoint validation (#2389)
Move L7 endpoint semantic checks into the openshell-policy crate so both
profile lint and the runtime validator share one implementation. This
eliminates drift between the two validation paths.

The shared validator covers 9 checks: unknown protocol, rules/access
mutual exclusivity, JSON-RPC family access rejection, json-rpc requires
rules, non-JSON-RPC protocol requires rules or access, MCP requires
rules when allow_all is false, rules-would-deny-all detection,
deny_rules require protocol, and deny_rules require base allow set.

Changes rules/deny_rules fields to Option<Vec<...>> so absent vs empty
is distinguishable at lint time. Adds is_effectively_empty() to
L7AllowProfile for deny-all detection of allow: {} objects. Makes
rules_would_deny_all MCP-aware by checking tool/params.name selectors
before classifying a rule as deny-all. Adds params field to
L7AllowProfile so MCP tool selectors survive proto round-trip.

Signed-off-by: Grace Smith <gsmith@redhat.com>
Signed-off-by: Grace Smith <grasmith@redhat.com>
2026-07-24 17:48:24 +00:00
Piotr MlocekandEvan Lezar f7cd910829 fix: eliminate parallel Rust test flakes (#2434)
* fix: eliminate parallel Rust test flakes

Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>

* refactor(supervisor-network): name workload proxy TCP connection

Signed-off-by: Evan Lezar <elezar@nvidia.com>

* refactor(supervisor-network): name procfs address family

Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>

---------

Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
Signed-off-by: Evan Lezar <elezar@nvidia.com>
Co-authored-by: Evan Lezar <elezar@nvidia.com>
2026-07-24 15:47:44 +00:00
alangou 1d4ac708f1 fix(policy): keep internal allowed IP proposals pending (#2416)
Signed-off-by: Adrien Langou <alangou@nvidia.com>
2026-07-23 12:17:21 +00:00
Piotr Mlocek cbdeb4d537 fix(server): prevent unrelated sandbox deletes from blocking deletion events (#2340)
* fix(server): unblock sandbox deletion events

Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>

* refactor(server): simplify sandbox settings cleanup

Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>

* refactor(server): make delete lock ordering explicit

Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>

* fix(server): bind delete worker to stable sandbox id

Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>

* fix(server): disarm canceled queued deletes

Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>

* test: harden sandbox lifecycle cleanup coverage

Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>

---------

Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
2026-07-22 12:02:37 +00:00
Mesut Oezdil 744a65d52e fix(driver-podman): avoid panic when HOME is unset on macOS (#2327)
* fix(driver-podman): resolve Podman socket via auto-detection

Align Podman socket selection with the existing Docker model: explicit
config wins, otherwise probe openshell-core for a responsive socket.
This also fixes the original HOME-unset panic, since resolution no
longer hardcodes a per-OS default path.

- add detect_podman_socket() in openshell-core, mirroring
  detect_docker_socket
- PodmanComputeConfig.socket_path is now Option<PathBuf>, no default
- remove default_socket_path() (podman driver) and podman_socket_path()
  (vm driver), both replaced by the shared detector
- update server env override and CLI for the new Option type
- add tests: responsive-candidate detection in openshell-core, and
  config-error (not panic) when no socket is configured or reachable

* fix(driver-podman): address review feedback on socket resolution

Extract socket resolution into resolve_socket_path, taking the
detector as a parameter so tests do not depend on real env vars or
the host's actual Podman state. Replace the flaky env-mutating test
with three deterministic cases: explicit wins, detected is used when
absent, and neither source errors.

Fix the socket_path doc comment to describe it from a config user's
point of view, matching DockerComputeConfig's docstring. Drop a
comment that only made sense next to the Docker driver code.

Update the Podman README and gateway docs: they described a fixed
per-OS default path that no longer exists, replace with the actual
probe-then-fail behavior.

* docs(driver-podman): simplify socket default description

Previous wording was self-contradictory (says auto-detect on unset,
then lists the same var as a probed candidate) and omitted the Linux
/run/user/uid/podman/podman.sock candidate.
2026-07-21 14:01:19 +00:00
Derek Carr 5952a5a23f feat(workspace): add workspace resource model with scoping, membershi… (#2243)
* feat(workspace): implement workspace model (Phase 1 of RFC 0011)

Implements workspace and membership model providing hard isolation
boundaries for multi-player OpenShell deployments.

Workspace CRUD with Kubernetes-style Terminating phase for graceful
deletion. All resources scoped by workspace via ObjectMeta. Membership
RPCs for workspace access control. Persistence migration shifts name
uniqueness to (object_type, workspace, name). Provider profiles support
platform and workspace scoping. Service routing uses workspace-prefixed
DNS labels. Inference routes renamed and workspace-scoped with
DeleteInferenceRoute RPC. Python SDK with WorkspaceClient, two-method
list pattern (workspace-scoped and for_all_workspaces), and workspace
parameter on all methods. CLI workspace flags, TUI workspace cycling.
K8s driver filters unmanaged CRs and uses delete preconditions. Podman
driver uses immutable container IDs. Label serialization fixed across
all put_if call sites.

Signed-off-by: Derek Carr <decarr@redhat.com>

* fix(cli): delegate sandbox upload command to existing upload function

The standalone `sandbox upload` command reimplemented upload logic
inline with two bugs: it used `Path::exists()` which follows symlinks
(rejecting dangling symlinks), and it ran git-aware filtering on
symlink sources. The `run::sandbox_upload()` function already handles
both cases correctly via `sandbox_upload_plan()`. Replace the inline
logic with a call to the existing function.

Signed-off-by: Derek Carr <decarr@redhat.com>

* fix(e2e): shorten sandbox names and fix test compatibility

Shorten the sandbox name in initial_sparse_policy_is_acknowledged_as_loaded
from 'e2e-2159-sparse-enrich' (22 chars) to 'e2e-sparse-enrich' (17 chars)
to comply with MAX_ROUTABLE_NAME_LEN (19 chars).

Also capture stderr in create_keep_with_args so future sandbox creation
failures include the actual CLI error instead of reporting empty output.

Signed-off-by: Derek Carr <decarr@redhat.com>

* test(workspace): add test coverage for workspace CRUD and persistence isolation

Add unit tests for workspace create happy path, get round-trip, get
not-found, get empty-name rejection, already-exists error, and
resolve_workspace not-found. Add persistence test proving cross-workspace
name uniqueness (same name in different workspaces produces separate
records). Add workspace name max-length boundary tests. Fix e2e harness
to include stderr in name-parse-failure error path. Align Python e2e
test_workspace_crud with try/finally pattern. Document provider profile
catalog workspace scoping gap in RFC 0011.

Signed-off-by: Derek Carr <decarr@redhat.com>

* fix(examples): update examples for workspace model compatibility

Shorten sandbox names in demo scripts to fit the 19-character
MAX_ROUTABLE_NAME_LEN limit: policy-demo prefix to pd-, multi-agent
notepad derives a short SANDBOX_TAG from the run ID, governance
interceptor uses gs-PID-RANDOM. Update vscode-remote-sandbox.md SSH
host aliases from openshell-{name} to openshell-{name}.{workspace}
format.

Signed-off-by: Derek Carr <decarr@redhat.com>

* feat(sdk): add workspace-scoped client and workspace CRUD

Add WorkspaceScopedClient modeled after kube::Api::namespaced — captures
workspace once and injects it into every sandbox request. Add workspace
CRUD methods (create, get, list, delete) and list_sandboxes_all_workspaces
on OpenShellClient. Extend SandboxRef with workspace field and add
WorkspaceRef type. Include mock tests for all new operations.

Signed-off-by: Derek Carr <decarr@redhat.com>

* fix(lint): resolve clippy warnings in workspace test assertions

Signed-off-by: Derek Carr <decarr@redhat.com>

* fix(docs): convert indented code blocks to fenced in RFC 0011

Signed-off-by: Derek Carr <decarr@redhat.com>

* fix(lint): resolve clippy warnings and apply cargo fmt across workspace

Auto-format with cargo fmt and fix clippy warnings exposed by the
reformat: unnecessary qualifications, map_unwrap_or, identical match
arms, unused variable prefix, dead code annotations, and let-unit-value
in e2e harness.

Signed-off-by: Derek Carr <decarr@redhat.com>

* fix(workspace): address workspace scoping issues from review

- Add workspace field to settings JSON output (CLI)
- Skip Podman containers missing workspace label instead of defaulting
  to empty string, matching K8s driver behavior
- Add resource_version to list_by_scope SELECT in both SQLite and
  Postgres backends, with regression test
- Gate PolicyLocalContext proposal/lookup routes on workspace readiness,
  returning 503 when workspace is not yet discovered
- Block sandbox and provider creation in TUI all-workspaces mode
- Clear workspace vectors in TUI reset_sandbox_state

Signed-off-by: Derek Carr <decarr@redhat.com>

* fix(workspace): make provider profile catalog workspace-aware

Thread workspace through snapshot_catalog so the
EffectiveProviderProfileCatalog enforces workspace boundaries on both
read and write paths. UserProviderProfileSource now loads platform-scoped
profiles (workspace "") plus the target workspace's profiles, preventing
cross-workspace duplicate profile ID collisions that previously caused
global catalog failures.

Update RFC 0011 to reflect catalog scoping is implemented in Phase 1
rather than deferred to future work.

Signed-off-by: Derek Carr <decarr@redhat.com>

* fix(persistence): include workspace column in atomic policy revision INSERT

put_policy_revision_atomic omitted the workspace column from the INSERT
into the objects table in both SQLite and Postgres backends, causing
atomically-written policy revisions to lose their workspace association.
Add workspace field to AtomicPolicyRevisionWrite and thread it through
both backend INSERT statements, matching the non-atomic put_policy_revision
path which already included it.

Signed-off-by: Derek Carr <decarr@redhat.com>

* fix(proxy): skip ancestor walk when socket owner is the entrypoint

collect_ancestor_identities walked the entire process tree above the
entrypoint when the connecting process was the entrypoint itself,
SHA256-hashing every ancestor binary (IDE, shell, container runtime).
On dev machines with large binaries in the ancestor chain this exceeded
the 30-second test timeout. When start_pid == stop_pid there are no
intermediate ancestors to verify, so return an empty list immediately.

Signed-off-by: Derek Carr <decarr@redhat.com>

* fix(workspace): make provider profile catalog scope-aware

Allow the same profile ID at platform and workspace scopes by
introducing layered catalog entries where workspace profiles shadow
platform profiles. Add source and scope fields to the ProviderProfile
proto and CLI output. Migrate List/Get handlers to the catalog,
fixing divergence with runtime profile resolution.

Signed-off-by: Derek Carr <decarr@redhat.com>

* fix(e2e): align podman e2e labels with centralized driver constants

The podman driver moved its container labels to the centralized
openshell.ai/ prefix, but the e2e test harness and cleanup script
still referenced the old openshell.sandbox-* keys, causing the
local_driver_token_restart test to fail on container lookup.

Signed-off-by: Derek Carr <decarr@redhat.com>

* fix(e2e): align python profile isolation test with scope-aware catalog

Platform profiles are now visible in workspace listings as fallbacks
per the layered catalog design. Update the assertion to match.

Signed-off-by: Derek Carr <decarr@redhat.com>

* fix(workspace): honor profile_workspace in runtime profile resolution

Runtime profile lookups now consult provider.profile_workspace via
get_type_profile_for_scope. Providers created with --global-profile
(profile_workspace="") resolve to the platform profile even when a
workspace profile shadows the same ID. All 6 runtime call sites
updated; type-only call sites remain scope-agnostic.

Signed-off-by: Derek Carr <decarr@redhat.com>

---------

Signed-off-by: Derek Carr <decarr@redhat.com>
2026-07-21 00:59:18 +00:00
Russell Bryant 9377e0d5fe fix(providers): allow git clone/fetch via default GitHub provider (#2317)
* fix(providers): allow git clone/fetch via default GitHub provider

The github.com:443 git-transport endpoint used the read-only access
preset, which expands to GET/HEAD/OPTIONS only. Git smart HTTP requires
a POST to */git-upload-pack for clone and fetch, so the L7 proxy denied
those operations and `gh repo clone` / `git clone https://...` failed.

Replace the preset with explicit rules that permit the read-only methods
plus POST */git-upload-pack, so clone/fetch work while push
(git-receive-pack) stays blocked. Enabling push still requires an
explicit policy proposal.

Why allowing this POST is still read-only: in git's smart HTTP protocol
POST is an RPC transport, not a write. A clone/fetch does GET
*/info/refs (ref discovery) followed by POST */git-upload-pack, whose
body is only the client's want/have negotiation; the server responds
with a packfile and nothing on the server is modified (data flows
server -> client). The service names are from the server's perspective:
git-upload-pack = the server uploads a pack to the client (a read/
download), while git-receive-pack = the server receives a pack from the
client (the actual write/push). The new rule is scoped to
*/git-upload-pack only, so push (git-receive-pack) and arbitrary POSTs
to github.com remain denied.

Add a provider-profile regression test and a rego enforcement test
covering ref discovery, upload-pack (allowed), and receive-pack (denied).

Closes #1769

Signed-off-by: Russell Bryant <rbryant@redhat.com>

* test(providers): strengthen git-transport regression and add clone e2e

Pin the exact allowed rule set for the built-in github git-transport
endpoint in both the provider-profile and composed-policy tests, so a
broader or additional POST rule (e.g. POST **) that could enable push
via git-receive-pack fails the test instead of passing a substring
check. Add an e2e test that attaches the built-in github provider and
clones a public repo over HTTPS, exercising provider attachment,
effective-policy composition, TLS interception, and real git behavior.

Update the Providers V2 docs so the github.com git-transport endpoint
shows explicit clone/fetch rules instead of the stale read-only preset.

Refs #1769

Signed-off-by: Russell Bryant <rbryant@redhat.com>

* test(providers): isolate providers_v2 mutation in clone e2e

The clone e2e enables the gateway-global providers_v2_enabled setting.
Restore its exact prior value (or absence) captured via GetGatewayConfig
instead of unconditionally deleting it, and serialize the mutation
across xdist workers with an exclusive file lock on the run's shared
base temp dir, so a shared or pre-configured gateway is left untouched
and parallel workers cannot race the read-modify-restore.

Refs #1769

Signed-off-by: Russell Bryant <rbryant@redhat.com>

* test(providers): serialize providers_v2 mutation with a suite-wide guard

The clone e2e's per-fixture lock only coordinated fixtures that acquired
it; other xdist workers hit the same gateway without it and could
observe the transiently-enabled providers_v2_enabled global during their
own sandbox creation (CWE-362).

Add an autouse readers-writer guard in conftest: every test holds a
shared lock on the gateway config, and a test marked
exclusive_gateway_config holds an exclusive lock. Mark the clone test
exclusive so no other worker is mid-test while it enables and restores
the gateway-global setting. Exact prior-value restoration is retained.

Refs #1769

Signed-off-by: Russell Bryant <rbryant@redhat.com>

---------

Signed-off-by: Russell Bryant <rbryant@redhat.com>
2026-07-20 21:07:17 +00:00
emonq a2cd5f8eda fix(gateway): honor tty flag for interactive exec (#2315)
* fix(gateway): honor tty flag for interactive exec

Pass the requested TTY mode through the interactive SSH relay.
Skip PTY allocation and resize forwarding when TTY is disabled,
and add regression coverage for both modes.

Signed-off-by: emonq <emonq@outlook.com>

* test(gateway): improve `test_sandbox_interactive_exec_honors_tty` to test streamed stdin and stdout/stderr

Signed-off-by: emonq <emonq@outlook.com>

---------

Signed-off-by: emonq <emonq@outlook.com>
2026-07-20 17:47:37 +00:00
Piotr Mlocek d556748771 feat(supervisor-middleware): add network egress middleware (#2027)
Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
2026-07-16 17:47:49 -07:00
Russell Bryant aa483ecb9a feat(providers): AWS STS AssumeRole refresh strategy and aws-s3 profile (#1782)
Add gateway-managed AWS STS credential refresh (provider-v2, #1576). The
gateway calls sts:AssumeRole and writes three short-lived credentials
(AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN) to the
provider record; the proxy re-signs requests with SigV4. Adds the aws and
aws-s3 provider profiles and a declarative multi-output refresh model
(additional_outputs) so one AssumeRole co-mints all three credentials.

Signed-off-by: Russell Bryant <rbryant@redhat.com>
2026-07-16 13:28:01 -07:00
krishicks cf4deccd37 fix(gateway): probe Docker socket during driver auto-detection (#2303)
Previously, Docker was auto-detected when the CLI was installed or a candidate
Unix socket existed. Neither check verified that the Docker API was responsive.
A similar check was done when auto-detecting Podman in the past, but was
replaced in 1f07bf04 with a probe of candidate Podman sockets instead.

This change applies the functional API probing approach introduced for Podman
in 1f07bf04 to Docker. It also makes Docker driver initialization use the same
socket-selection mechanism as Docker auto-detection instead of Bollard’s local
defaults. This means the previously auto-detectable Docker socket paths
$HOME/.docker/run/docker.sock and $XDG_RUNTIME_DIR/docker.sock will actually be
usable.

When no working compute driver can be auto-detected, the gateway exits early
with a message saying as much:

> configuration error: no compute driver configured and auto-detection found no
> suitable driver; set --drivers or OPENSHELL_DRIVERS to kubernetes, podman,
> docker, or vm

This makes for a better user experience when installing OpenShell without an
available supported compute driver.

Signed-off-by: Kris Hicks <khicks@nvidia.com>
2026-07-16 10:02:24 -07:00
Matthew Grossman 077adb7907 fix(server): persist sandbox labels on create (#2306)
Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>
2026-07-16 09:13:23 -07:00
Evan Lezar 21aaa89522 feat(gateway): add elevated gateway info (#2202)
* feat(cli)!: fold gateway metadata into list

BREAKING CHANGE: openshell gateway info no longer shows local gateway registration metadata. Use openshell gateway list or openshell gateway list -o json for local registration details.

Signed-off-by: Evan Lezar <elezar@nvidia.com>

* feat(gateway): add elevated gateway info

Signed-off-by: Evan Lezar <elezar@nvidia.com>

---------

Signed-off-by: Evan Lezar <elezar@nvidia.com>
2026-07-15 22:04:57 +00:00
Drew Newberry 83003e80fc feat(interceptors): initial gateway interceptor implementation and reference example (#2005)
* feat(gateway): add descriptor-driven interceptors

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(gateway): add service-reflected interceptors

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* wip

* fix(gateway): harden interceptor evaluation

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(interceptors): label metrics and harden governance smoke

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* remove on_error: ignore

* feat(gateway-interceptors): emit log annotations

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(examples): govern provider profiles in interceptor

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(gateway): preserve update config annotations

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(providers): support interceptor profile catalogs

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* wip

* feat(governance-interceptor): sign provider profiles

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(providers): use configured profile sources for refresh updates

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(gateway-interceptors): add phase-specific evaluation payloads

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(providers): compose provider profile sources

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(gateway-interceptors): preserve committed responses

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(gateway-interceptors): reject ambiguous protobuf oneofs

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(gateway-interceptors): validate patch candidates per binding

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(gateway-interceptors): use reflected protobuf codec

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(governance-example): canonicalize signed protobuf hashes

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(gateway): commit policy provenance atomically

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(gateway): close signed governance bypasses

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(gateway): isolate interceptor secrets and authority

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(gateway): snapshot provider profiles per request

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* chore(gateway): resolve server clippy warnings

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(server): satisfy provider source clippy lint

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(server): initialize policy test annotations

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(gateway-interceptors): require explicit route allowlist

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* docs(proto): clarify update annotation semantics

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

---------

Signed-off-by: Drew Newberry <anewberry@nvidia.com>
2026-07-14 20:58:36 -07:00
Tinson Lai e3d26dd3ae fix(policy): keep approved chunk when a mechanistic denial resubmits its endpoint (#2242)
* fix(policy): keep approved chunk when a mechanistic denial resubmits its endpoint

A mechanistic denial flush for an endpoint already covered by an
auto-approved mechanistic chunk flipped that chunk approved -> rejected
with no human action. The dedup upsert in put_draft_chunk returns the
existing row's id, which aliases onto the approved chunk; the self-reject
scan then matched the row against itself and rejected it, while the merged
rule stayed enforced — the governance ledger disagreed with the live policy.

Guard self_reject_mechanistic_if_already_covered to act only on a still
pending effective chunk, and exclude the incoming id from the covering
scan. Add a regression test and document the dedup/self-reject invariant.

Fixes #2165
Refs NVIDIA/NemoClaw#6329

Signed-off-by: Tinson Lai <tinsonl@nvidia.com>

* fix(policy): reject mechanistic chunk via atomic pending compare-and-set

The self-reject-when-covered path read a chunk, confirmed it was pending,
then issued an unconditional status update to rejected. An approval that
committed between the read and the write flipped an already-approved chunk
to rejected while its rule stayed merged, recreating the ledger/enforcement
mismatch through a concurrent path.

Add conditionally_reject_draft_chunk to the policy store: the pending->rejected
transition carries a status = 'pending' predicate on the final write and
reports whether a row changed. Zero changed rows is a benign no-op, meaning
another operation already decided the chunk. Implemented for both SQLite and
PostgreSQL. The pending pre-read and the self-exclusion guard stay as
defense-in-depth. Adds persistence-level regression tests proving an approved
row cannot be conditionally rejected and that an approval racing the reject
wins.

Signed-off-by: Tinson Lai <tinsonl@nvidia.com>

---------

Signed-off-by: Tinson Lai <tinsonl@nvidia.com>
2026-07-13 16:30:32 -07:00
Grace Smith 4e1ffef84b fix(certgen): stage temp dir inside output dir to fix cross-device rename (#2241)
generate-certs stages temp files beside --output-dir using
dir.with_file_name(). When --output-dir is a container or WSL volume
mount, the staging dir lands on a different filesystem and
std::fs::rename fails with EXDEV.

Move staging inside the output dir so the rename always stays on the
same filesystem, preserving atomic replacement.

Fixes #2173

Signed-off-by: Grace Smith <grasmith@redhat.com>
2026-07-13 09:20:25 -07:00
Adel Zaalouk bb72d0123c fix(server): allow newlines in exec command arguments (#1965) 2026-07-12 17:28:57 -07:00
Evan Lezar 45614a3fb6 refactor(api): remove SandboxTemplate.volume_claim_templates (#2088)
The field was added during the Kubernetes driver extraction refactor
(#817) as a pass-through mechanism, but was never wired up to a CLI
flag, Python SDK helper, or any documentation. The only reachable
user path was raw gRPC construction.

The Kubernetes driver now always injects the default workspace PVC,
removing the branching logic that checked for a user-supplied VCT.
Field number 9 is reserved in the proto to prevent reuse.

Signed-off-by: Evan Lezar <elezar@nvidia.com>
2026-07-01 16:04:47 +02:00
Evan Lezar 0a25fdf525 refactor(core): remove unused extra bind addresses (#2059)
Signed-off-by: Evan Lezar <elezar@nvidia.com>
2026-06-30 10:37:10 -07:00
Evan Lezar a5161d0bc3 refactor(server): normalize compute driver config acquisition (#1974)
* refactor(server): remove unused compute runtime constructor parameter

Signed-off-by: Evan Lezar <elezar@nvidia.com>

* refactor(server): normalize compute driver type imports

Signed-off-by: Evan Lezar <elezar@nvidia.com>

* refactor(server): key driver config tables by name

Signed-off-by: Evan Lezar <elezar@nvidia.com>

* refactor(server): normalize compute driver config acquisition

Signed-off-by: Evan Lezar <elezar@nvidia.com>

---------

Signed-off-by: Evan Lezar <elezar@nvidia.com>
2026-06-29 18:59:08 +02:00
krishicksandddurst 7bce1223dc feat(policy): add JSON-RPC and MCP L7 policies (#1865)
Add policy schema, proto, provider profile, OPA, and L7 proxy support for
`protocol: json-rpc` and `protocol: mcp`. Generic JSON-RPC endpoints match
exact method names only, with `method: "*"` as the all-method sentinel;
wildcard/glob methods and params matchers are rejected.

Parse JSON-RPC request bodies and batches in the forward proxy, deny
response-shaped client frames, limit receive-stream GET allowance to MCP
endpoints, and redact params in decision logs. Preserve L7 rule params on the
proto load path so MCP `tools/call` tool filters behave like YAML-loaded
policies.

Add MCP conformance coverage, JSON-RPC L7 e2e coverage, and docs for the new
protocols and current matcher limitations.

Signed-off-by: Kris Hicks <khicks@nvidia.com>
Co-authored-by: ddurst <267424412+ddurst-nvidia@users.noreply.github.com>
2026-06-26 15:52:16 -07:00
Gal Zaidman 45e5a5d128 fix(server): prevent exec relays from hanging on idle connections (#1992)
Add HTTP/2 keepalive on supervisor multiplex connections so half-dead
sessions cannot leave in-flight exec relays parked indefinitely. Configure
SSH keepalive on exec relay clients so long silent commands are not timed
out on stdout idle alone; wedged or orphaned relays fail after missed
keepalives instead.

After a command reports exit status, bound how long the gateway waits for
the trailing channel close. Return UNAVAILABLE when a relay closes before
reporting exit status rather than defaulting to exit code 1.

Signed-off-by: Gal Zaidman <gzaidman@nvidia.com>
2026-06-26 13:33:32 -07:00
Taylor Mutch ba21bb32a2 feat(kubernetes): support agent-sandbox v1beta1 (#2009)
* feat(kubernetes): support agent-sandbox v1beta1

Signed-off-by: Taylor Mutch <taylormutch@gmail.com>

* ci(kubernetes): test agent-sandbox api versions

Signed-off-by: Taylor Mutch <taylormutch@gmail.com>

* fix(kubernetes): retry agent-sandbox raw 404s

Signed-off-by: Taylor Mutch <taylormutch@gmail.com>

* fix(kubernetes): harden agent-sandbox api setup

Signed-off-by: Taylor Mutch <taylormutch@gmail.com>

* fix(kubernetes): cache agent-sandbox api version

Signed-off-by: Taylor Mutch <taylormutch@gmail.com>

* docs(kubernetes): document agent-sandbox upgrade behavior

Signed-off-by: Taylor Mutch <taylormutch@gmail.com>

* refactor(kubernetes): collapse agent-sandbox api selection

Signed-off-by: Taylor Mutch <taylormutch@gmail.com>

---------

Signed-off-by: Taylor Mutch <taylormutch@gmail.com>
2026-06-26 11:44:52 -07:00
John T. Myers b855d8d872 fix(policy): reserve provider rule namespace (#1991)
Closes #1982

Reject user-authored _provider_* network policy keys at gateway boundaries, strip provider-derived rules from sandbox sync, and document the round-trip workflow.

Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com>
2026-06-26 08:28:35 -07:00
Evan Lezar e3382cb447 fix(server): update driver spec test argument (#2022)
* fix(server): update driver spec test argument

Signed-off-by: Evan Lezar <elezar@nvidia.com>

* test(server): stabilize concurrent lease steal test

Signed-off-by: Evan Lezar <elezar@nvidia.com>

---------

Signed-off-by: Evan Lezar <elezar@nvidia.com>
2026-06-26 06:59:15 -07:00
st-grandEvan Lezar 75a317ea45 feat(server): support out-of-tree compute drivers via --compute-driver-socket (#1703)
* feat(server): support remote compute driver endpoints

Add named remote compute driver endpoint support to the gateway.

Remote drivers are selected by a non-reserved compute driver name and either a CLI/env socket endpoint or [openshell.drivers.<name>].socket_path. The VM driver now enters ComputeRuntime through the same acquired remote endpoint path, while Docker, Podman, and Kubernetes retain their in-process drivers.

Require --drivers/OPENSHELL_DRIVERS when pairing an ad-hoc socket endpoint so the socket does not imply a magic driver name, and keep reserved in-tree names unavailable for unmanaged socket endpoints.

Co-authored-by: Evan Lezar <elezar@nvidia.com>

Signed-off-by: st-gr <38470677+st-gr@users.noreply.github.com>
Signed-off-by: Evan Lezar <elezar@nvidia.com>

* test(server): cover remote compute driver UDS lifecycle

Signed-off-by: Evan Lezar <elezar@nvidia.com>

---------

Signed-off-by: st-gr <38470677+st-gr@users.noreply.github.com>
Signed-off-by: Evan Lezar <elezar@nvidia.com>
Co-authored-by: Evan Lezar <elezar@nvidia.com>
2026-06-26 12:32:20 +02:00
Simon Scatton c7879a0a1d test(e2e): stop using custom e2e binary builds (#2000)
Remove the production-crate dev-settings feature so e2e tests build the same gateway, CLI, and supervisor binaries that we ship. Move settings coverage from dummy test-only keys to the production ocsf_json_enabled setting.

Signed-off-by: Simon Scatton <sscatton@nvidia.com>
2026-06-25 13:08:04 +00:00
Evan Lezar 2c545893ed feat(cli): add GPU count requests (#1812)
* feat(gpu)!: add resource requirements

BREAKING CHANGE: SandboxSpec.gpu and DriverSandboxSpec.gpu were replaced with resource_requirements.gpu, changing protobuf field 9 from a bool to a message for both public and driver APIs.

Signed-off-by: Evan Lezar <elezar@nvidia.com>

* refactor(gpu): pass requirements through sandbox create

Pass the coupled GPU requirement object through the CLI sandbox_create boundary instead of splitting presence and count into separate arguments.

Signed-off-by: Evan Lezar <elezar@nvidia.com>

* refactor(gpu): pass requirements to timeout message

Pass ResourceRequirements into the provisioning timeout message helper so GPU hints are derived from the same nested request object used to create the sandbox.

Signed-off-by: Evan Lezar <elezar@nvidia.com>

* refactor(gpu): pass driver requirements through helpers

Thread Option<GpuResourceRequirements> through driver validation and rendering helpers instead of splitting GPU presence and count into separate arguments.

Signed-off-by: Evan Lezar <elezar@nvidia.com>

* fix(gpu): validate exact device requests

Require exact driver GPU device lists to be tied to a GPU request, allow a single exact device to use the default countless request, and require explicit matching counts for multi-device lists.

Signed-off-by: Evan Lezar <elezar@nvidia.com>

---------

Signed-off-by: Evan Lezar <elezar@nvidia.com>
2026-06-23 21:49:02 -07:00
Taylor Mutch 8e831f3a52 fix(ci): fix linting issues (#1985) 2026-06-23 20:05:34 -07:00
st-grandClaude Opus 4.7 4ee27d9955 feat(sandbox,providers): add aws-bedrock as a recognized inference provider (#1704)
* feat(sandbox): allow AWS Bedrock InvokeModel paths through the L7 router

Adds two patterns to `default_patterns()` so the supervisor's L7
inference router recognizes the Bedrock InvokeModel URL shape and
forwards matched requests to the registered upstream:

- `POST /model/{modelId}/invoke`                       → aws_bedrock_invoke
- `POST /model/{modelId}/invoke-with-response-stream`  → aws_bedrock_invoke_stream

The `{modelId}` segment is wildcarded by extending `detect_inference_pattern`
to handle one middle `/*/` segment in addition to the existing trailing
`/*`. The wildcard is constrained to a single non-empty path segment to
avoid path-traversal liabilities — `/model//invoke` and `/model/a/b/invoke`
both no-match.

Without this, sandboxes running Claude Code in its native Bedrock mode
(`CLAUDE_CODE_USE_BEDROCK=1`, `ANTHROPIC_BEDROCK_BASE_URL`, AWS-style
auth) hit the supervisor with `403 connection not allowed by policy`
because their URL doesn't match `/v1/*` shapes. The fix unblocks
operators wanting to register direct AWS Bedrock, an in-cluster
Bedrock-compatible bridge, or a Bedrock-emulating LiteLLM as
`--type aws-bedrock` providers.

Tests cover: positive matches for invoke + invoke-with-response-stream,
query-string handling, GET rejection, empty-segment rejection,
multi-segment rejection, and unknown-action rejection.

Companion changes (provider discovery spec + YAML profile) follow in
the next commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: st-gr <38470677+st-gr@users.noreply.github.com>

* feat(providers): add aws-bedrock provider profile + discovery spec

Adds `aws-bedrock` to the built-in provider catalog so operators can
run `openshell provider create --type aws-bedrock --credential ...`
and have the gateway treat it as a first-class inference provider
alongside `anthropic`, `openai`, etc.

- `providers/aws-bedrock.yaml`: YAML profile declaring four credentials
  (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN, AWS_REGION).
  Default endpoint is `bedrock-runtime.us-east-1.amazonaws.com:443`;
  operators in other regions or running against a Bedrock-compatible
  proxy override via the operator-supplied `BEDROCK_BASE_URL` config-key
  (mirrors `ANTHROPIC_BASE_URL` for the `anthropic` provider).

- `crates/openshell-providers/src/providers/aws_bedrock.rs`: the
  `ProviderDiscoverySpec` so `openshell provider create --auto-providers`
  picks up AWS_* env vars from local credentials.

- `crates/openshell-providers/src/providers/mod.rs`: register the module.

- `crates/openshell-providers/src/lib.rs`: register the SPEC in the
  default registry alongside the other providers.

- `crates/openshell-providers/src/profiles.rs`: include the new YAML in
  `BUILT_IN_PROFILE_YAMLS`.

What this PR explicitly does NOT add (intentionally separated for
review-size reasons; will follow up):

- A SigV4 signer in `openshell-router`. The current change simply
  declares the protocol; a follow-up PR adds outbound SigV4 signing
  using the `aws-sigv4` crate and a new `auth_style: sigv4` validator
  branch in profiles.rs. Operators who don't need SigV4 (e.g. an
  in-cluster bridge that ignores it and authenticates separately to
  the upstream) can use this PR today.

- Body translation between Bedrock InvokeModel shape and other
  inference shapes. The router treats Bedrock requests as opaque
  pass-through; if the operator's upstream is real AWS Bedrock it
  speaks Bedrock natively, if it's a translating bridge the bridge
  does any conversion server-side.

- `BEDROCK_BASE_URL` placeholder substitution in the YAML loader.
  Today the YAML's `host` is a literal default; operators override
  with the config-key the same way `ANTHROPIC_BASE_URL` works.

Tested: `cargo test -p openshell-providers` (35 tests green) and
`cargo test -p openshell-sandbox --lib l7::inference` (40 tests green
including the seven new aws_bedrock cases from the previous commit).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: st-gr <38470677+st-gr@users.noreply.github.com>

* revert(providers): drop legacy aws-bedrock SPEC, rely on v2 YAML profile

Addresses johntmyers's review on NVIDIA/OpenShell#1704: net-new
providers should land via the v2 YAML profile only and should NOT
require changes to the legacy `ProviderDiscoverySpec` registry.

- Delete `crates/openshell-providers/src/providers/aws_bedrock.rs`
  (the legacy SPEC + `test_discovers_env_credential!` invocation).
- Drop `pub mod aws_bedrock;` from `crates/openshell-providers/src/providers/mod.rs`.
- Drop `registry.register(providers::aws_bedrock::SPEC)` from
  `crates/openshell-providers/src/lib.rs`.

Kept:

- `providers/aws-bedrock.yaml` and the `include_str!` in
  `BUILT_IN_PROFILE_YAMLS` (`profiles.rs`) — the v2 path.
  `discover_from_profile()` (`crates/openshell-providers/src/discovery.rs`)
  picks up AWS_* env vars via `discovery.credentials` in the YAML.
- L7 router patterns in `crates/openshell-sandbox/src/l7/inference.rs`
  — orthogonal to the provider registry.

The discovery test in the deleted file goes with it; v2 doesn't have
an established per-provider env-var-pickup unit test pattern, and
other YAML-only registrations (none today, but this is the new
direction) won't carry one either.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: st-gr <38470677+st-gr@users.noreply.github.com>

* fix(providers): include aws_session_token in discovery + update profile assertion

Two fixes from johntmyers's gator-agent re-check on NVIDIA/OpenShell#1704:

1. `providers/aws-bedrock.yaml`: add `aws_session_token` to
   `discovery.credentials`. The credential is declared in the profile
   but was missing from the discovery scan list, so Providers v2
   `--from-existing` would silently drop temporary AWS credentials
   (STS / IRSA scenarios).

2. `crates/openshell-server/src/grpc/provider.rs`: update the static
   `list_provider_profiles_returns_built_in_profile_categories`
   assertion to include `aws-bedrock` at alphabetical position 0.
   Adding `providers/aws-bedrock.yaml` to BUILT_IN_PROFILE_YAMLS made
   the prior `["claude-code", "github", "nvidia"]` expectation stale.

Remaining blockers from the same review (deferred to follow-up
commits): `inference::profile_for` registration for aws-bedrock,
user-facing provider + inference-routing docs, and an
`upsert_cluster_inference_route` integration test.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: st-gr <38470677+st-gr@users.noreply.github.com>

* feat(inference): register aws-bedrock profile (bridge-fronted) + docs

Addresses johntmyers's blocking review feedback on PR #1704:
"aws-bedrock still is not wired into the managed inference.local route
registry. profile_for only registers openai, anthropic, and nvidia, so
inference set --provider <aws-bedrock-provider> will reject this
provider before the new sandbox L7 patterns can be used."

Approach: register aws-bedrock as a *bridge-fronted* upstream — the
router does not inject any auth header on outbound requests; the
configured BEDROCK_BASE_URL is expected to point at a translating
bridge / Bedrock-compatible proxy that handles auth in its own pod.
This is the shape the L7 patterns commit (8b30211a) and the YAML
profile (6b51e1a6) were designed for. SigV4 signing for direct AWS
Bedrock is a separate follow-up; see PR thread.

Changes:

- core::inference::AuthHeader: add `None` variant for upstreams that
  authenticate themselves.
- core::inference: add AWS_BEDROCK_PROFILE static + register in
  profile_for. Default base URL is bedrock-runtime.us-east-1, override
  via BEDROCK_BASE_URL config-key (mirrors ANTHROPIC_BASE_URL pattern).
  Empty credential_key_names + auth: None means no router-side
  credential lookup at route time.
- router::backend: handle AuthHeader::None as a no-op (skip auth
  injection).
- server::inference::resolve_provider_route: gate find_provider_api_key
  on auth != None. aws-bedrock providers with empty credentials now
  resolve cleanly. Updated the unsupported-type error message to
  include aws-bedrock in the supported list.
- server::inference tests: add positive
  upsert_cluster_route_succeeds_for_aws_bedrock_without_api_key test
  covering the new code path end-to-end (provider with empty creds +
  BEDROCK_BASE_URL config → upsert succeeds → resolved route has
  empty api_key + provider_type aws-bedrock + bridge URL).
- core::inference tests: profile_for_known_types covers aws-bedrock,
  case-insensitive lookup, plus three new aws-bedrock-specific tests
  (auth: None, no credential keys, bedrock-specific protocols).
- docs/sandboxes/inference-routing.mdx: header forwarding row
  mentions aws-bedrock has no passthrough headers; new tabs in
  Supported API Patterns (InvokeModel + InvokeModelWithResponseStream)
  and Create a Provider (with the bridge-fronted shape note + SigV4
  deferral).
- docs/sandboxes/manage-providers.mdx: new row in Supported Provider
  Types table; new row in Supported Inference Providers table.

Verification (in dev container):
- cargo check -p openshell-core -p openshell-router -p openshell-server: clean
- cargo test -p openshell-core --lib inference: 14/14 pass (incl. 3 new)
- cargo test -p openshell-server --lib inference::tests::upsert: 6/6 pass
  (incl. new aws-bedrock test)
- cargo fmt --check: clean
- cargo clippy --all-targets -D warnings: clean

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: st-gr <38470677+st-gr@users.noreply.github.com>

* fix(aws-bedrock): bridge-only YAML, doc actual cmd shape, neg test

Addresses four findings from gator-agent's #1704 re-check on 4ab587f1:

- **Item 5** (YAML collects unused AWS creds): mark all four AWS
  credentials `required: false` and clear `discovery.credentials`.
  Bridge-fronted routing intentionally does not consume AWS
  credentials, so `--from-existing` no longer scans for them. The
  credentials remain in the schema (not deleted) so the SigV4
  follow-up can flip them back without a schema migration. Added a
  multi-line description that names the bridge-fronted shape and the
  SigV4 deferral so readers don't have to cross-reference the PR
  thread.

- **Item 3** (docs show command that the CLI rejects): rewrite the
  Create-a-Provider example for AWS Bedrock to use the actual
  required shape — placeholder `--credential AWS_ACCESS_KEY_ID=
  unused-bridge-fronted-shape` plus the `--config BEDROCK_BASE_URL`.
  The placeholder satisfies the gRPC handler's
  `provider.credentials.is_empty()` rejection without expanding
  server-side validation; the router ignores it on the outbound path
  because `auth: AuthHeader::None` skips header injection. Operators
  see a clearly-labeled placeholder in `provider get` output.

- **Item 1** (validator probe): document `--no-verify` as required
  for `openshell inference set --provider <aws-bedrock>` since the
  default validation probe doesn't recognize the
  `aws_bedrock_invoke` / `aws_bedrock_invoke_stream` protocols. Doc
  now shows the full `provider create` + `inference set --no-verify`
  flow with rationale for both decisions inline.

- **Item 6** (docs polish): `inference-routing.mdx` summary row now
  lists AWS Bedrock alongside NVIDIA, Anthropic, Vertex AI, and
  OpenAI-compatible providers, with the bridge-fronted caveat
  inline.

Test additions in `crates/openshell-server/src/inference.rs`:

- Renamed the existing aws-bedrock test from
  `..._without_api_key` to `..._with_bridge_url` and updated it to
  use a placeholder credential (mirroring the doc-recommended
  pattern operators will copy-paste). The `auth: None` path still
  produces an empty `api_key` on the resolved route — the test now
  documents that the credential is *stored* but not *used*.
- Added `upsert_cluster_route_rejects_aws_bedrock_without_bedrock_base_url`:
  the negative half of johntmyers' "successfully used by
  upsert_cluster_inference_route or intentionally rejected with a
  clear documented error" ask. With
  `default_base_url: ""` and no `BEDROCK_BASE_URL` config, route
  resolution returns `InvalidArgument` naming the missing base_url
  rather than silently forwarding prompts to AWS Bedrock with no
  usable auth.

Verification (in dev container):
- cargo test -p openshell-core --lib inference: 18/18 (incl. 3 new)
- cargo test -p openshell-server --lib inference::tests::upsert: 8/8
  (incl. 2 new aws-bedrock cases — positive + negative)
- cargo fmt --check: clean
- cargo clippy --all-targets -D warnings: clean

Item 2 (router-side enforcement of operator-configured Bedrock model
path, replacing the current verbatim path forwarding + body-only
model rewrite) is the remaining blocker and is genuinely separable —
it touches the L7 router with streaming-aware test coverage.
Deferring to its own commit so the security-critical change gets the
review attention it deserves.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: st-gr <38470677+st-gr@users.noreply.github.com>

* fix(router): enforce operator-configured Bedrock model in request path

Closes the security-blocking item from gator-agent's #1704 re-check
on 4ab587f1: "Bedrock carries the model id in /model/{modelId}/invoke,
but the router currently forwards the caller's original path and only
rewrites JSON body model. That lets sandbox code choose a different
upstream model than the operator-configured route model, and may also
mutate native Bedrock request bodies incorrectly."

Two changes in `prepare_backend_request`:

1. **Path rewrite for Bedrock routes.** Before computing the upstream
   URL, parse the inbound path's `/model/<id>/invoke[-with-response-stream]`
   shape and substitute the operator-configured `route.model` for the
   caller-supplied model segment. Sandbox code that hardcodes a
   different model still works (we don't reject on mismatch), but the
   operator's configured model is what reaches the upstream / bridge.
   If the inbound path is somehow not a recognized Bedrock shape on a
   Bedrock route (the L7 pattern detector upstream of the router
   should never produce this combination), reject with
   RouterError::Internal naming the offending path rather than
   forwarding verbatim.

2. **Skip body-model injection for Bedrock routes.** The existing body
   rewriter unconditionally inserts `route.model` into the JSON body
   for non-Vertex routes. AWS Bedrock InvokeModel encodes the model
   in the URL path; the body is the raw provider-specific payload
   (Anthropic Messages for Claude, Mistral payload for Mistral, etc.)
   and must not be mutated. The branch ordering is now:
   needs_vertex_anthropic_version → strip body model + inject
   anthropic_version; route_is_bedrock → leave body alone; else →
   inject route.model (existing default).

New helpers, all in `crates/openshell-router/src/backend.rs`:

- `route_is_bedrock(route)` — true when route.protocols contains
  aws_bedrock_invoke or aws_bedrock_invoke_stream.
- `parse_bedrock_invocation_path(path)` — returns
  Some((model_id, "/invoke" | "/invoke-with-response-stream")) for
  paths matching the recognized Bedrock shapes. Strips query strings.
  Rejects empty model ids and multi-segment ids (defense-in-depth
  matching the L7 pattern detector's existing guards).
- `rewrite_bedrock_path(route, path)` — returns the path with the
  caller's model segment replaced by route.model.

Test coverage in the same file (9 new tests):

- parse_bedrock_invocation_path: positive cases for both invoke
  variants, query-string stripping; negative cases for empty model id,
  multi-segment id, unknown action, wrong prefix, missing slash.
- route_is_bedrock: matches both protocol variants singly and
  combined; rejects openai_chat_completions.
- rewrite_bedrock_path: substitutes operator model on both invoke
  variants; returns None for non-Bedrock paths.
- bedrock_route_rewrites_model_in_path_and_preserves_body
  (wiremock end-to-end): caller sends /model/some-other-model/invoke
  with a body containing model: "caller-supplied-model-name". Mock
  asserts the upstream receives /model/<operator-model>/invoke and the
  body's model field is the caller's value (NOT route.model) — proves
  both the path rewrite and the body preservation.
- bedrock_route_streaming_rewrites_model_in_path: same contract for
  invoke-with-response-stream.
- bedrock_route_rejects_non_bedrock_path: defense-in-depth coverage of
  the Internal-error path when a Bedrock route receives a path that
  doesn't match Bedrock shape.

Verification (in dev container):
- cargo test -p openshell-router --lib: 53/53 (incl. 9 new)
- cargo fmt --check: clean
- cargo clippy -p openshell-core -p openshell-router -p openshell-server
  --all-targets -- -D warnings: clean

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: st-gr <38470677+st-gr@users.noreply.github.com>

* fix(sandbox/l7): declare framing for Bedrock patterns

Closes the buffered-vs-streaming framing warning from gator-agent's
re-check on 4ab587f1: "Bedrock InvokeModel should be buffered while
InvokeModelWithResponseStream is streaming. Please add framing/coverage
so /model/{id}/invoke cannot be corrupted by the streaming proxy's
truncation/error-frame behavior."

The InferenceApiPattern struct gained a `framing: ResponseFraming`
field upstream after the original Bedrock-patterns commit (#22b78cff)
landed; the cherry-pick onto current upstream/main left the two
Bedrock entries without the new field. Fixed here:

- aws_bedrock_invoke (POST /model/{id}/invoke):
    framing = ResponseFraming::Buffered
  InvokeModel returns one JSON object the caller decodes whole. Sending
  it through the streaming proxy would risk a mid-body size-cap
  truncation or idle-timeout failure appending an SSE error event onto
  bytes the caller decodes as one JSON body — the same corruption mode
  that drove the existing embeddings + model-discovery to Buffered.
- aws_bedrock_invoke_stream (POST /model/{id}/invoke-with-response-stream):
    framing = ResponseFraming::Streaming
  InvokeModelWithResponseStream returns an AWS event-stream of binary
  chunks; the caller wants chunks incrementally, so the streaming proxy
  path is correct.

Two new tests in `crates/openshell-sandbox/src/l7/inference.rs` pin
down the contract:

- aws_bedrock_invoke_is_buffered — detect_inference_pattern returns a
  Buffered pattern for /model/<id>/invoke, with explanatory message
  naming the corruption mode being prevented.
- aws_bedrock_invoke_stream_is_streaming — same shape, asserting
  Streaming for /model/<id>/invoke-with-response-stream.

Verification (in dev container):
- cargo check -p openshell-sandbox: clean (was failing on missing
  `framing` field before this commit)
- cargo test -p openshell-sandbox --lib l7::inference::tests::aws_bedrock:
  7/7 (incl. 2 new framing tests)
- cargo fmt --check: clean
- cargo clippy --all-targets -- -D warnings: clean

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: st-gr <38470677+st-gr@users.noreply.github.com>

* fix(inference): drop aws_bedrock_invoke_stream until protocol-aware errors land

Per PR #1704 review (johntmyers): defer Bedrock streaming to a follow-up
that also wires protocol-aware error framing for the AWS event-stream
shape. Until then, surfacing `/model/{id}/invoke-with-response-stream`
risks shipping responses the sandbox cannot interpret on failure.

- AWS_BEDROCK_PROTOCOLS no longer advertises `aws_bedrock_invoke_stream`.
- The L7 inference pattern table drops the streaming entry; only
  `aws_bedrock_invoke` (buffered) is recognized.
- Test `aws_bedrock_invoke_stream_pattern_is_deferred` asserts no
  pattern claims that protocol so the gap is visible.

Signed-off-by: st-gr <38470677+st-gr@users.noreply.github.com>

* fix(inference): validate AWS Bedrock model_id at route + rewrite, preserve query

Per PR #1704 review (johntmyers, gator agent): the operator-configured
Bedrock model_id flows verbatim into the upstream URL path; the previous
plumbing left no enforcement that the value was a single benign path
segment, opening a path-injection vector.

Defense in depth, both layers:

* `openshell-server::inference`: new `validate_aws_bedrock_model_id`
  rejects empty, leading/trailing whitespace, `/`, `\\`, `?`, `#`, `%`,
  `..`, control/whitespace characters. Wired into `resolve_provider_route`
  ahead of base_url resolution so the route store cannot persist a
  malformed model_id. Mirrors `validate_vertex_model_id` exactly.

* `openshell-router::backend`: `rewrite_bedrock_path` now refuses to
  construct the upstream URL unless `route.model` passes
  `is_valid_bedrock_model_id`, so even a stale or hand-edited route
  cannot reach the wire. The parser also drops the
  `/invoke-with-response-stream` arm to match the protocol catalog.

* `parse_bedrock_invocation_path` returns the `?`-prefixed query tail
  as a third element; `rewrite_bedrock_path` re-attaches it so any
  caller-supplied query string is preserved through the model rewrite.

Tests: 5 unit tests for the validator, 1 integration test that
exercises every unsafe-model_id reject path through
`upsert_cluster_inference_route`, plus a router-side rewrite-rejects
test covering 11 unsafe `route.model` values. All 72 server inference
tests + router tests pass.

Signed-off-by: st-gr <38470677+st-gr@users.noreply.github.com>

* fix(aws-bedrock): make built-in profile non-egress-granting; tighten docs

Per PR #1704 review (johntmyers): a single profile that is "usable as
is" — not a profile that auto-grants direct AWS Bedrock egress to any
sandbox that selects it. The bridge IS the egress point and is
operator-managed; the profile must not implicitly punch a hole through
the cluster's network policy on its behalf.

* `providers/aws-bedrock.yaml`: clear `endpoints` and `binaries` to
  empty arrays. Rewrite the description to spell out that the profile
  is intentionally non-egress-granting, that operators are responsible
  for declaring their bridge's egress endpoint and binary attribution,
  and that the SigV4 follow-up will repopulate these fields once
  router-side signing exists.

* `docs/sandboxes/inference-routing.mdx`:
  - Drop the `InvokeModelWithResponseStream` row from the supported
    patterns table (matches the protocol-catalog + L7-pattern drop).
  - Update the `--no-verify` paragraph to reference only
    `aws_bedrock_invoke`.
  - Generalise the placeholder-credential rationale: any
    standalone-router profile registering `AuthHeader::None` will hit
    the same non-empty-credentials structural requirement.

Signed-off-by: st-gr <38470677+st-gr@users.noreply.github.com>

* style(router): satisfy cargo fmt for Bedrock query-tail extraction

CI's rustfmt rejected the one-line `let (path_only, query_tail) = path.find('?').map_or(...)`
shape from commit c51160e4 and required the chained-method layout
instead. Functional behaviour is unchanged; tests still pass.

Signed-off-by: st-gr <38470677+st-gr@users.noreply.github.com>

* style(router): drop unnecessary `crate::` prefix on RouterError matcher

`RouterError` is already imported at the top of the file, so the
two test-side `matches!(result, Err(crate::RouterError::UpstreamProtocol(_)))`
uses trip clippy's `unused_qualifications` under `-D warnings`. Drop
the prefix on both sites; functional behaviour is unchanged.

These warnings predate this PR (originated in 25abc9e3c on 2026-06-07)
but NVIDIA's `rust:lint` re-runs because this PR touches
`backend.rs`, so the lint regression surfaces here.

Signed-off-by: st-gr <38470677+st-gr@users.noreply.github.com>

* style(server): silence unused-variable lint on first shutdown_tx

`crates/openshell-server/src/lib.rs` declares `(shutdown_tx, shutdown_rx)`
twice in `run_server` — the first pair (introduced by upstream #1577
reconciler-lease work) only consumes `shutdown_rx`, leaving the first
`shutdown_tx` unused. Clippy's `-D warnings` flags it on workspace
lint. Rebasing this Bedrock-scoped PR onto current upstream/main
exposes the regression because we re-touch the file and the lint
re-runs.

Prefix the unused half with an underscore so the workspace lint is
clean. Behaviour is unchanged — only the second shutdown_tx (line
~425) ever sends a shutdown signal today.

This is a drive-by upstream fix unrelated to the Bedrock provider
work; keeping it separate so it can be cherry-picked or reverted
independently of the Bedrock commits.

Signed-off-by: st-gr <38470677+st-gr@users.noreply.github.com>

---------

Signed-off-by: st-gr <38470677+st-gr@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-23 13:10:37 -07:00
Robert Sturla 48545cfb59 feat(sandbox): add GCE metadata emulator for Google Cloud (#1763)
* feat(core): add shared GCP constants module

Single source of truth for GCP naming: env var aliases, provider config
keys, token search order, and Vertex-specific env vars. Consumed by
openshell-server, openshell-providers, and openshell-sandbox.

- Add google_cloud.rs with metadata emulator host and loopback address
- Define PROJECT_ID, REGION, and SERVICE_ACCOUNT_EMAIL env var aliases
- Add provider config key constants for gcp provider implementations
- Define TOKEN_ENV_KEYS search order (SA token takes priority over ADC)
- Add Vertex-specific env vars for Goose and Claude Code SDK integration
- Add STATIC_CONFIG_KEYS as union of all alias arrays for env resolution
- Export module via openshell-core lib.rs

Signed-off-by: Robert Sturla <rsturla@redhat.com>

* feat(providers): add google-cloud and vertex provider plugins

Add GoogleCloudProvider and VertexProvider implementing inject_env to
project GCP config (project ID, region, SA email, metadata host) into
sandbox environment variables. Replace the inline Vertex AI env
injection in the server with the registry-based inject_env dispatch.

Also adds the google-cloud.yaml provider profile with SA JWT and ADC
OAuth2 credential refresh flows.

Signed-off-by: Robert Sturla <rsturla@redhat.com>

* feat(sandbox): add GCE metadata emulator for GCP

Add a loopback HTTP server on 127.0.0.1:8174 inside the sandbox
network namespace that emulates the GCE instance metadata API.
GCP client SDKs discover it via GCE_METADATA_HOST and obtain
credential placeholders that the proxy resolves to real tokens
at egress.

Add metadata_server module with MetadataHandler trait and
netns-aware TCP binding via std::thread (not spawn_blocking)
to avoid tokio pool namespace contamination
Add google_cloud_metadata module implementing the GCE metadata
API subset (token, project-id, email, scopes, service-accounts)
Add child_env_resolved() and gcp_token_response() to
ProviderCredentialState for GCP-aware credential projection
Wire metadata server into sandbox lifecycle before SSH handler
Collapse multi-line HTTP response format string into single line

Signed-off-by: Robert Sturla <rsturla@redhat.com>

* docs(sandbox): add GCP credentials documentation

Document the google-cloud provider setup for ADC and service account
flows, injected environment variables, metadata emulator behavior, and
network policy configuration for GCP APIs.

Signed-off-by: Robert Sturla <rsturla@redhat.com>

* feat(cli): support --from-gcloud-adc for google-cloud providers

Widen --from-gcloud-adc to accept google-cloud providers. The ADC
credential key is derived from the provider profile rather than
hardcoded per type, so future GCP provider types get ADC support by
declaring the right refresh metadata in their profile YAML.

Add ProviderTypeProfile::adc_credential() to find the ADC-compatible
credential from a profile's refresh metadata. Remove unused
VERTEX_AI_ADC_TOKEN_KEY and GCP_ADC_TOKEN_KEY constants.

Signed-off-by: Robert Sturla <rsturla@redhat.com>

---------

Signed-off-by: Robert Sturla <rsturla@redhat.com>
2026-06-23 15:05:07 -05:00
John T. Myers 48a7d09e82 feat(providers): support profile updates (#1914)
* feat(providers): support profile updates

Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com>

* fix(providers): require CAS for profile updates

Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com>

* fix(providers): serialize profile update validation

Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com>

* fix(providers): serialize profile import invariants

Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com>

* fix(providers): require profile update target id

Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com>

---------

Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com>
2026-06-23 11:34:46 -07:00
Evan Lezar 4c75b8542b fix(server): share gateway shutdown channel (#1945)
Signed-off-by: Evan Lezar <elezar@nvidia.com>
2026-06-17 10:48:54 +02:00
mmilutinovic371 36bb9e3ec2 feat(providers): add DeepInfra as a built-in inference provider (#1902)
* feat(providers): add DeepInfra as a built-in inference provider (v2 only)

- Adds `deepinfra` as a built-in Providers v2 profile (`providers/deepinfra.yaml`)
  with inference category, Bearer auth, and `DEEPINFRA_API_KEY` discovery
- Adds `DEEPINFRA_PROFILE` to inference routing so `inference.local` works
  with the `deepinfra` provider type
- Fixes `build_backend_url` to strip `/v1` from request paths when the base
  URL contains `/v1/` as an internal segment (e.g. `api.deepinfra.com/v1/openai`),
  preventing double-versioned paths like `.../v1/openai/v1/chat/completions`
- Updates `docs/sandboxes/providers-v2.mdx` and `docs/sandboxes/manage-providers.mdx`
  with DeepInfra entries; removes the old v1 workaround row that used `openai`
  type with `OPENAI_API_KEY`

Signed-off-by: Milos Milutinovic <codemastermilos@gmail.com>

* fix(providers): address gator review findings for DeepInfra provider

- Narrow build_backend_url /v1 dedupe to URLs whose path component is
  exactly /v1 or starts with /v1/ — prevents regression on proxy
  endpoints where /v1 is buried deeper (e.g. /api/v1/openai); add
  regression test for the nested proxy path case
- Add deepinfra provider plugin with DEEPINFRA_API_KEY discovery,
  registered in ProviderRegistry so known_types() and TUI include it
- Add deepinfra to unsupported-inference-provider error message in
  openshell-server for accurate user-facing debugging guidance
- Add deepinfra to openai_compatible_profiles_include_embeddings test
  to lock in the OpenAI-compatible protocol contract

Signed-off-by: Milos Milutinovic <codemastermilos@gmail.com>

* fix(router): handle /v1 as final path segment in build_backend_url dedup

Extends the /v1 deduplication logic to also strip /v1 from request paths
when the base URL's path ends with /v1 (e.g. https://api.groq.com/openai/v1).
The previous fix only matched paths starting with /v1/, which regressed
providers like Groq whose base path has /v1 as the last segment rather than
the first. The nested-proxy exclusion (e.g. /api/v1/openai) is preserved
since /v1 appears in the middle — neither first nor last segment. Adds a
regression test for the Groq-style base URL.

Signed-off-by: Milos Milutinovic <codemastermilos@gmail.com>

* fix(providers): add deepinfra telemetry bucket and update profile list test

- Add DeepInfra variant to ProviderProfile telemetry enum and from_raw()
  mapping so deepinfra providers are tracked in their own bucket rather
  than falling through to Custom
- Map deepinfra in telemetry_provider_profile() in openshell-server
- Add deepinfra to list_provider_profiles_returns_built_in_profile_categories
  test (sorted between cursor and github)
- Update architecture/gateway.md inference provider list to include deepinfra

Signed-off-by: Milos Milutinovic <codemastermilos@gmail.com>

* style(router): apply cargo fmt to backend.rs

Signed-off-by: Milos Milutinovic <codemastermilos@gmail.com>

---------

Signed-off-by: Milos Milutinovic <codemastermilos@gmail.com>
2026-06-16 16:39:03 -07:00
Yuedong Wu ff028ce0df feat(server): support TLS certificate hot-reload (#1870)
* feat(server): support TLS certificate hot-reload

Signed-off-by: Yuedong Wu <dwcn22@outlook.com>

* refactor(server): extract shared TLS test utilities

Signed-off-by: Yuedong Wu <dwcn22@outlook.com>

---------

Signed-off-by: Yuedong Wu <dwcn22@outlook.com>
2026-06-16 16:16:13 -07:00
Taylor Mutch fd6cbf6b6c fix(server): retry sandbox delete phase conflicts (#1905)
Signed-off-by: Taylor Mutch <taylormutch@gmail.com>
2026-06-16 10:42:03 -07:00
Derek Carr e73745f10e feat(gateway): add reconciler lease for HA multi-replica deployments (#1577)
Introduce a database-backed lease that ensures only one gateway replica
runs the watch and reconcile loops. Includes lease primitives with CAS
safety, cooperative cancellation via watch channels, SQLite bypass for
single-replica deployments, and integration tests covering failover,
contention, and CAS chain integrity.

Signed-off-by: Derek Carr <decarr@redhat.com>
2026-06-11 12:25:57 -04:00
alangou f33fd02f4d fix(server): use public tonic body type in gRPC rate limiter (#1872)
Signed-off-by: Adrien Langou <alangou@nvidia.com>
2026-06-11 17:29:57 +02:00
alangou b6c87a76ab feat(server): add grpc rate limiting gateway-wide (#1566)
Signed-off-by: Adrien Langou <alangou@nvidia.com>
2026-06-11 13:49:41 +02:00
Evan Lezar 1dc59853ff feat(gpu): move device selection to driver config (#1815)
* refactor(kubernetes): use shared proto struct helpers

Signed-off-by: Evan Lezar <elezar@nvidia.com>

* refactor(docker): extract sandbox template validation

* feat(gpu)!: move device selection to driver config

BREAKING CHANGE: The openshell sandbox create --gpu-device flag and
corresponding API field were removed. Select specific GPUs through
driver-specific driver_config fields instead.

Signed-off-by: Evan Lezar <elezar@nvidia.com>

* fix(kubernetes): reject unknown driver config fields

Signed-off-by: Evan Lezar <elezar@nvidia.com>

---------

Signed-off-by: Evan Lezar <elezar@nvidia.com>
2026-06-11 12:53:06 +02:00
Drew Newberry 530aaf1360 feat(drivers): support docker and podman config mounts (#1785)
* feat(drivers): support docker and podman config mounts

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* docs(drivers): trim mount docs

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* test(e2e): cover local driver volume mounts

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(podman): satisfy linux clippy lint

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(drivers): gate bind mounts behind gateway config

* docs(sandbox): simplify mount examples

* cleanup

* test(e2e): stabilize branch checks

* fix(drivers): tighten local mount validation

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

---------

Signed-off-by: Drew Newberry <anewberry@nvidia.com>
2026-06-10 11:46:20 -07:00
Taylor Mutch 702cbc4f63 feat(providers): support SPIFFE-backed token grants (#1784)
* feat(providers): support SPIFFE-backed token grants

Add provider profile token_grant metadata and expand endpoint-specific
dynamic credentials so sandbox supervisors can request SPIFFE JWT-SVIDs,
exchange them with an OAuth-style token endpoint, cache returned access
tokens, and inject bearer tokens into matching HTTP requests.

Wire Kubernetes and Helm deployments to mount the provider SPIFFE Workload
API socket into sandbox pods for token grant exchange.

Signed-off-by: Taylor Mutch <taylormutch@gmail.com>
Signed-off-by: Gordon Sim <gsim@redhat.com>

* test(examples): add SPIFFE token grant demo

Add a reusable alpha/beta demo that deploys a SPIFFE-verifying token issuer
and protected services, imports a token-grant provider profile, creates a
sandbox, and verifies endpoint-specific bearer tokens.

The script leaves Kubernetes workloads in place, deletes sandboxes through
openshell unless KEEP_SANDBOX=1, and prints protected service logs as proof
of life.

Signed-off-by: Taylor Mutch <taylormutch@gmail.com>

* fix(providers): harden SPIFFE token grants

* fix(providers): harden dynamic token grants

Signed-off-by: Taylor Mutch <taylormutch@gmail.com>

* fix(providers): harden token grant handling

---------

Signed-off-by: Taylor Mutch <taylormutch@gmail.com>
Signed-off-by: Gordon Sim <gsim@redhat.com>
2026-06-10 10:54:39 -07:00
LateNightHackathonandlatenighthackathon c1d3b43ddb fix(policy): classify advisory private-IP notes with the canonical is_internal_ip (#1777) (#1824)
generate_security_notes() flagged a destination as internal/private using naive
string prefixes. host.starts_with("172.") matched all of 172.0.0.0/8 even
though only 172.16.0.0/12 is private (RFC 1918), so the advisory flagged public
addresses (172.15/172.32) and missed CGNAT (100.64.0.0/10), IPv6 ULA, and the
other reserved ranges; it also flagged non-IP hostnames that merely start with
10./172.. The two copies of the check (openshell-sandbox mechanistic_mapper.rs
and openshell-server grpc/policy.rs) now parse the host as an IP and defer to
openshell-core net::is_internal_ip, the same RFC-accurate, unit-tested classifier
the runtime SSRF enforcement path uses, so the proposed-rule advisory matches
actual enforcement.

Per review, is_internal_v4 now uses the stable Ipv4Addr predicates as far as
they go: is_documentation() (RFC 5737) replaces the two manual TEST-NET-2/3
checks and also closes a gap, since 192.0.2.0/24 (TEST-NET-1) was previously
unflagged, and is_broadcast() (RFC 919) is added. Manual checks remain only for
the ranges without a stable predicate: CGNAT 100.64.0.0/10, IETF protocol
assignments 192.0.0.0/24, and benchmarking 198.18.0.0/15.

Adds regression coverage: both note suites now assert hostname-prefix false
positives (10.example.com, 172.example.com) are not flagged and IPv6 ULA
(fd00::1) is flagged; net.rs covers the newly classified TEST-NET-1 and
broadcast addresses.

Closes #1777

Signed-off-by: latenighthackathon <latenighthackathon@users.noreply.github.com>
Co-authored-by: latenighthackathon <latenighthackathon@users.noreply.github.com>
2026-06-10 09:51:38 -07:00
Russell Bryant 7274a6bea3 feat(cli): add --env flag to sandbox create/exec and fix env var passthrough (#1730)
* feat(cli): add --env flag to sandbox create and exec

Add `--env KEY=VALUE` (repeatable) to `openshell sandbox create` and
`openshell sandbox exec`, wiring user-specified environment variables
into the existing `SandboxSpec.environment` and
`ExecSandboxRequest.environment` protobuf fields which were previously
only accessible via the SDK/gRPC API.

Fix a bug where user-specified environment variables set via
`SandboxSpec.environment` were silently dropped inside sandbox SSH
sessions. The SSH handler's `apply_child_env()` calls `env_clear()` to
strip supervisor-internal secrets before spawning user shells, but this
also wiped container-level env vars including user-specified ones. This
was introduced in 5fd4885a (2026-02-26, "feat(sandbox): VS Code
Remote-SSH support with platform detection fix") which added
`env_clear()` to fix a VS Code platform detection timeout caused by
leaked supervisor env vars.

The fix introduces an `OPENSHELL_USER_ENVIRONMENT` sidecar env var:
compute drivers (Docker, Podman, Kubernetes, VM) serialize the
user-specified environment as JSON into this variable at container
creation time, the sandbox supervisor deserializes it at startup, and
the SSH handler re-injects the values into child processes after
`env_clear()`. User-specified vars are applied at lowest priority so
proxy, TLS, and provider credentials still take precedence.

Signed-off-by: Russell Bryant <russell.bryant@gmail.com>

* fix(sandbox): address PR review feedback for env var injection

- VM driver: restructure build_guest_environment to apply user env
  before required driver vars, preventing user-specified values from
  overriding OPENSHELL_ENDPOINT, PATH, and other critical vars.
  Matches the pattern already used by Docker/Podman/Kubernetes.

- All drivers: insert OPENSHELL_USER_ENVIRONMENT sidecar after
  environment.extend(user_env) so a user-supplied key of the same
  name cannot clobber the JSON payload the supervisor parses.

- Podman driver: fix template/spec env precedence — template env is
  now applied first with spec overwriting, matching the other drivers
  (spec is user-specified and should win over image defaults).

- CLI: add parse_env_pairs() that rejects OPENSHELL_* prefixed keys
  with a clear error message. Both sandbox create and exec call sites
  updated to use it.

- SSH handler: filter out OPENSHELL_* keys from user_environment
  before injecting into child processes.

- Docs: add "Set Environment Variables" section to manage-sandboxes.mdx
  documenting --env for both create and exec, per-command override
  semantics, and the OPENSHELL_* reservation.

- Tests: add sandbox_create_env_rejects_reserved_prefix test.

- Fix rustfmt formatting in lib.rs, container.rs, driver.rs.

* fix(server): reject OPENSHELL_* env keys at API boundary

Add server-side validation to reject environment variable keys starting
with OPENSHELL_ in SandboxSpec.environment, SandboxTemplate.environment,
and ExecSandboxRequest.environment. This closes the gap where SDK/gRPC
callers (bypassing the CLI) could inject reserved keys that affect
supervisor-internal state.

The CLI already rejects these keys via parse_env_pairs, but the API
boundary is the authoritative enforcement point since all clients
(CLI, SDK, direct gRPC) pass through it.

* fix(server): validate env key names and control chars at API boundary

Add POSIX env key format validation (^[A-Za-z_][A-Za-z0-9_]*$) and
control character rejection for SandboxSpec.environment,
SandboxTemplate.environment, and ExecSandboxRequest.environment at the
gRPC validation layer. This catches invalid keys like "1BAD" or
"BAD-NAME" and values containing newlines at the API boundary rather
than letting them surface as driver-specific container/pod failures.

Also add matching CLI-side validation in parse_env_pairs so users get
immediate feedback. Update docs to say "OpenShell rejects" rather than
"the CLI rejects" since enforcement is now at both boundaries.

* fix(server): allow exec helper keys and add env size limit

Split exec env validation so OPENSHELL_PYFUNC_B64 (used by the Python
SDK's exec_python()) passes through while runtime-clobberable keys
like OPENSHELL_SANDBOX_ID are still rejected. Create-time validation
remains strict — all OPENSHELL_* keys are blocked.

Add a 256 KiB total serialized-size limit on create-time environment
maps. The drivers serialize the full map into a single
OPENSHELL_USER_ENVIRONMENT env var; capping the input at the API
boundary prevents driver/runtime-specific startup failures from
oversized env blocks.

Add template environment control-character test and exec helper key
acceptance test.

---------

Signed-off-by: Russell Bryant <russell.bryant@gmail.com>
2026-06-08 14:30:05 -07:00
Eric Curtin 1399f371e0 refactor: deduplicate OCSF builder setters and persistence helpers (#1800)
Eliminate three categories of repeated code:

1. OCSF builder setter methods
   Add six macros to builders/mod.rs (impl_activity_setter!,
   impl_action_disposition_setters!, impl_actor_process_setter!,
   impl_dst_endpoint_setter!, impl_src_endpoint_addr_setter!,
   impl_firewall_rule_setter!) that generate the identical one-liner
   setters duplicated across network, http, ssh, process, finding,
   and lifecycle builders. Removes ~55 hand-written method bodies.

2. Persistence protobuf decode-and-hydrate block
   Extract a private decode_record<T>() helper that decodes an
   ObjectRecord into a protobuf message and hydrates resource_version.
   The identical three-line block was repeated in get_message,
   get_message_by_name, list_messages, and list_messages_with_selector.
   Each method now delegates to decode_record via .map().transpose()
   or .into_iter().map().collect().

3. Store enum dispatch arms
   Add a store_dispatch! macro that expands
   match self { Postgres(s) => s.method(...).await, Sqlite(s) => ... }.
   Applied to all 13 dispatch-only methods on Store, removing ~80
   lines of mechanical match arms with zero logic.
2026-06-08 08:59:29 -07:00
Shiju 25abc9e3c7 feat(inference): allow local embeddings route (#1774)
* feat(inference): allow local embeddings route

Route OpenAI-compatible embeddings through the local inference proxy so
sandboxed vector workloads reach a configured provider via the same
route classification and auth path that chat, completion, and model
discovery already use.

- Add openai_embeddings to the OpenAI-compatible protocol set so
  providers (openai, nvidia) advertise embeddings routing.
- Classify POST /v1/embeddings as the openai_embeddings protocol in the
  sandbox L7 patterns.
- Serve embeddings buffered with an accurate Content-Length, since the
  response is a single JSON object rather than an SSE token stream. The
  streaming path appends an SSE error frame on a size-cap or idle-timeout
  truncation, which would corrupt a one-object body the client parses
  whole. protocol_returns_buffered_body() selects the path.
- Probe an embeddings-only backend against /v1/embeddings during
  validation, after the chat and completion protocols so a multi-protocol
  route still prefers those.
- Extract two shared helpers. http_status_text() backs both response
  formatters and adds 401/422/429/503 for embeddings passthrough and
  router error mapping; write_inference_router_error() backs the streaming
  and buffered routing paths.
- Return an OpenAI-shaped embeddings body from the mock route.

Tests cover profile lookup, L7 pattern detection, the mock body, and
buffered Content-Length framing with no chunked transfer-encoding and no
SSE error frame.

Signed-off-by: Shiju <shiju@nvidia.com>

* fix(inference): cap buffered inference response body

The buffered proxy path read the whole upstream response into memory with
no size bound. The route timeout bounds elapsed time but not memory, so a
misbehaving or oversized upstream could force unbounded allocation in the
sandbox proxy. The streaming path already caps each response at 32 MiB;
the buffered path did not.

Cap the buffered read at the same 32 MiB. An advertised over-cap body is
rejected from its Content-Length before any bytes are read, and chunks
accumulate under the same bound so a chunked or mislabeled body cannot
slip past. An over-cap response fails as an upstream protocol error,
surfaced as HTTP 502 at the proxy boundary, and is never partially
returned.

Tests

- cargo test -p openshell-router \
    proxy_to_backend_rejects_over_cap_response_body

Signed-off-by: Shiju <shiju@nvidia.com>

* fix(inference): validate embeddings models against all advertised protocols

A managed route resolves to its provider profile's full protocol set, so
an embeddings model such as text-embedding-3-small lists
openai_chat_completions alongside openai_embeddings. Route verification
probed only the first writable protocol and stopped on its failure. It
sent a chat probe with the embedding model, the provider rejected it as
wrong-shape, and the route failed validation before the embeddings probe
ran. Embeddings-only configs could not be verified.

Try the advertised protocols in preference order. A request-shape
rejection (HTTP 400, 404, 405, 422) falls through to the next protocol,
so an embeddings model validates against /v1/embeddings even when the
chat probe rejects it. Credential, rate-limit, connectivity, and
upstream-health failures stay terminal and stop validation at the first
probe, so a bad key or a down backend is reported as itself rather than
masked by a later probe.

validation_probe becomes validation_probes, which returns the ordered
list, and the per-probe fallback retry (max_completion_tokens versus
max_tokens) moves into a shared helper.

Tests

- cargo test -p openshell-router \
    verify_embeddings_model_falls_through_chat_probe
- cargo test -p openshell-router verify_stops_on_credentials_failure

Signed-off-by: Shiju <shiju@nvidia.com>

* fix(inference): serve model discovery responses buffered

GET /v1/models returns a single JSON model list, the same response shape
as embeddings. The sandbox inference proxy was routing it through the SSE
streaming path. A streaming size-cap or idle-timeout truncation appends
an SSE error frame to the body, which corrupts a payload the client
parses as one JSON object.

Make response framing a property of the protocol. A new ResponseFraming
field on InferenceApiPattern is set once per pattern in default_patterns.
model_discovery and openai_embeddings are now Buffered, while chat
completions, completions, responses, and Anthropic messages stay
Streaming. The proxy dispatch gates on pattern.is_buffered(), which
replaces the stringly-typed protocol_returns_buffered_body predicate so
the streaming-versus-buffered decision lives in one place and cannot
drift across the sites that read it.

Model discovery now flows through the same buffered path as embeddings,
framed with an accurate Content-Length and bounded by the buffered-read
size cap that path already enforces.

Tests

- cargo test -p openshell-sandbox protocol_framing_classification
- cargo test -p openshell-sandbox \
    inference_model_discovery_served_buffered_with_content_length

Signed-off-by: Shiju <shiju@nvidia.com>

* docs(inference): document embeddings route in supported patterns

Signed-off-by: Shiju <shiju@nvidia.com>

---------

Signed-off-by: Shiju <shiju@nvidia.com>
2026-06-07 10:27:34 -07:00
Calum Murray b392b2eefe feat(providersv2): add path auth_style (#1622)
Signed-off-by: Calum Murray <cmurray@redhat.com>
2026-06-05 17:14:05 -07:00