Commit Graph
639 Commits
Author SHA1 Message Date
Max Dubrinsky e07b4a2a7c feat(python-sdk): add provider/policy/upload helpers and demo notebook
Extend python/openshell/ with the verbs needed to express the
multi-agent-notepad flow entirely in Python:

- ProviderClient (create/get/list/delete); credentials_from_env
  resolves env var names to mirror the CLI's --credential flag.
- policy_from_yaml() loads the CLI's YAML format into a SandboxPolicy
  proto. Lazy-imports PyYAML, added to runtime deps.
- SandboxClient.upload() gzip-tars the source and pipes the tarball
  over ExecSandbox(stdin=...) to 'tar xzf -'. No paramiko dependency.
- SandboxClient.create() gains friendly kwargs (name, image, providers,
  policy, environment, gpu, labels) alongside the existing spec= path.

architecture/python-sdk.md documents the design principle (mirror
CLI verbs, not raw protos), the public surface, and what's out of
scope (image build, gateway bootstrap, draft policy, port-forward,
RelayStream).

examples/multi-agent-notepad/notebook.ipynb is a Python equivalent of
demo.sh: env validation, policy render, provider creation, parallel
worker sandboxes via ThreadPoolExecutor, synthesis sandbox, cleanup.

13 new unit tests; all 23 sandbox tests pass; ruff/ty/markdownlint clean.
2026-05-11 20:50:34 -04:00
Taylor MutchandDrew Newberry 3f0a0587c8 docs(rfc): add gateway configuration file RFC (#951)
* docs(rfc): Add gateway configuration RFC

* Update RFC to handle config directories

docs(rfc): address conf.d pattern and remove stale scratch branch reference

Closes out open question #2 with a conf.d-style directory loader as a
planned follow-on to the v1 single-file approach. Documents the three
design decisions needed before implementation. Removes reference to
scratch/server-config-file branch which does not exist.

* docs(rfc): sync gateway config RFC with current repo state

Resync RFC 0002 with the gateway code as it stands today: drop
already-implemented framing, restructure the schema so each compute
driver owns its [openshell.drivers.<name>] table, add the missing
OIDC, metrics-listener, docker, and podman sections, and exclude
database_url and ssh_handshake_secret from the file schema (env/CLI
only).

* docs(rfc): renumber gateway configuration RFC to 0003

RFC 0002 was claimed by the agent-driven policy management RFC that
landed first. Move the gateway configuration proposal to the next
available number.

* Update rfc/0003-gateway-configuration/README.md

Co-authored-by: Drew Newberry <anewberry@nvidia.com>

---------

Co-authored-by: Drew Newberry <anewberry@nvidia.com>
2026-05-11 14:58:11 -07:00
Florent BENOIT b33bbd21cb fix(vm): correct /sandbox ownership when rootfs is built by non-root host (#1176)
When the VM driver extracts OCI image layers on a non-root host (e.g.
macOS UID 501), fs::copy and tar::unpack create files owned by the host
user. The sandbox user inside the VM then cannot write to its own home
directory.

Add a conditional chown in the VM init script that detects the mismatch
and fixes ownership before the supervisor starts. The check is skipped
when ownership is already correct (e.g. Linux-as-root extraction).

Signed-off-by: Florent Benoit <fbenoit@redhat.com>
2026-05-11 15:56:49 -05:00
Florent BENOIT 764d930680 fix(vm): use bash 3.2-safe empty array expansion in supervisor build script (#1311)
macOS ships bash 3.2, which treats empty array expansion ("${arr[@]}")
as an unbound variable error under set -u. Unlike set -e, this error
is not caught by if-constructs — it kills the shell immediately with
no output, causing mise run vm:supervisor to fail silently.

Use the ${arr[@]+"${arr[@]}"} idiom so the script works on both
bash 3.2 (macOS default) and bash 4+.

Signed-off-by: Florent Benoit <fbenoit@redhat.com>
2026-05-11 15:50:30 -05:00
Mesut Oezdil 957daa0a0f docs(helm): document supervisor.sideloadMethod and sandboxNamespace default (#1309)
Add supervisor.sideloadMethod to the Kubernetes setup chart values table
and the compute drivers reference table. The value was added in the
ImageVolumeSource sideload PR and controls whether the supervisor binary
is delivered via an OCI image volume mount or an init container, with
auto-detection based on cluster version when left empty.

Update the server.sandboxNamespace description in both pages to reflect
that the Helm chart now derives it from the release namespace by default
when the value is left empty.
2026-05-11 13:21:56 -07:00
Mesut Oezdil b9b8bc3ff2 fix(driver-kubernetes): propagate log_level as OPENSHELL_LOG_LEVEL env var (#1310)
The sandbox_to_k8s_spec function was inserting logLevel and environment
fields at the Sandbox CR spec level. Neither field exists in the
agents.x-k8s.io/v1alpha1 CRD schema, so Kubernetes silently dropped them
and the log_level value never reached the sandbox pod.

Remove the dead CR-level insertions. Build a merged environment map that
includes OPENSHELL_LOG_LEVEL when log_level is set, and pass it to
sandbox_template_to_k8s so the value flows through build_env_list into
the pod container spec.

Add a unit test verifying OPENSHELL_LOG_LEVEL appears in the container
env and that logLevel is absent from the CR spec.

Fixes #1265
2026-05-11 13:19:09 -07:00
Taylor Mutch 59475aabf4 ci(kubernetes): add kube gateway e2e tests and gated CI workflow (#1251)
* test(helm): Add kube gateway e2e tests

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

* ci(helm): add Branch Helm E2E workflow gated on test:e2e-helm

Adds a label-gated GitHub Actions workflow that exercises the Helm
chart end-to-end against the Rust e2e suite via `mise run e2e:helm`.

Pipeline:
- pr_metadata gates on the `test:e2e-helm` label via the pr-gate action.
- build-gateway / build-supervisor build and push Docker images using
  the reusable docker-build.yml workflow.
- helm-e2e (bare runner): apt-installs z3 build deps so cargo can
  compile the openshell-policy crate's z3-sys backend, creates a kind
  cluster via helm/kind-action, materializes the kind kubeconfig at the
  path mise's [env] block expects, side-loads the freshly built
  gateway/supervisor images, applies
  deploy/kube/manifests/agent-sandbox.yaml so the
  sandboxes.agents.x-k8s.io CRD and reconciling StatefulSet are in
  place, and finally runs `mise run e2e:helm`.

Also expands the `e2e:helm` task to run the full Rust e2e suite
(matching `e2e:podman`) instead of only the smoke test, with
OPENSHELL_E2E_KUBE_TEST as an opt-in single-test override for local
debugging.

Extends the e2e-label-help workflow so applying `test:e2e-helm` posts
the next-step hint pointing at this workflow.

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

* test(helm-e2e): run host-gateway tests on the kube driver

Six tests previously skipped on the kubernetes driver — three in
host_gateway_alias.rs plus the forward_proxy_l7 + graphql_l7 cases —
relied on `host.openshell.internal` reaching either a host process or a
sibling docker container. The Helm chart already supports this via
`server.hostGatewayIP`, but the e2e wrapper never set it.

- with-kube-gateway.sh: auto-detect the host-routable IP (CoreDNS
  `host.k3d.internal` first to handle Docker Desktop, docker network
  gateway as a fallback for kind on Linux CI) and pass it to helm
  install. Also import locally-built images into existing k3d clusters
  and wait for namespace deletion to complete before exit.
- e2e/rust/Cargo.toml + e2e-helm.sh: add an `e2e-host-gateway` feature
  that gates the three test files; docker and podman runs imply it,
  helm runs opt in by default (overridable via
  OPENSHELL_E2E_HELM_FEATURES for remote clusters where the test host
  is unreachable from pods).
- Drop the `skip_if_kube` helper and its callers — feature gating now
  decides whether the tests compile in.

Verified against a local k3d cluster: all six previously-skipped tests
pass, smoke regression intact.

* refactor(e2e): rename e2e:helm to e2e:kubernetes

The runner exercises the gateway on Kubernetes; helm is just the
deployment mechanism. Names now describe the target environment.

- mise task: `e2e:helm` -> `e2e:kubernetes`
- script: e2e/rust/e2e-helm.sh -> e2e/rust/e2e-kubernetes.sh
- env var: OPENSHELL_E2E_HELM_FEATURES -> OPENSHELL_E2E_KUBERNETES_FEATURES
- workflow: branch-helm-e2e.yml -> branch-kubernetes-e2e.yml
  (display name "Branch Kubernetes E2E", job "kubernetes-e2e")
- PR gate label: test:e2e-helm -> test:e2e-kubernetes (e2e-label-help
  hint workflow updated to match)

PR #1251 needs the new label applied; the old label can be removed.

* fix(e2e): prefer IPv4 host-gateway IP from dual-stack docker networks

kind dual-stacks its network; the wrapper's awk picked the first
non-empty gateway, which was the IPv6 entry (fc00:...). Sandbox pods
can't reach the test host's IPv4 listener through that, so the L7 and
host-gateway-alias tests failed in CI even though they passed locally
against k3d (where host.k3d.internal in CoreDNS short-circuits the
docker-network fallback).

Restrict the awk filter to IPv4 octets.

---------

Signed-off-by: Taylor Mutch <taylormutch@gmail.com>
2026-05-11 12:57:23 -07:00
Mrunal Patel 6184d24ea1 feat(k8s): support ImageVolumeSource for supervisor sideload (#1300)
* feat(k8s): support ImageVolumeSource for supervisor sideload

Add a config-driven switch between two supervisor binary delivery
methods: image-volume (default) mounts the supervisor OCI image
directly as a read-only volume (K8s >= v1.33), while init-container
preserves the existing emptyDir + copy-self pattern for older clusters.

Configurable via --supervisor-sideload-method CLI arg,
OPENSHELL_SUPERVISOR_SIDELOAD_METHOD env var, or Helm
supervisor.sideloadMethod value.

* feat(helm): auto-detect supervisor sideload method from cluster version

Use Helm .Capabilities.KubeVersion to choose the supervisor sideload
method automatically: image-volume on K8s >= v1.33, init-container on
older clusters. An explicit supervisor.sideloadMethod value overrides
auto-detection.

* fix(helm): raise auto-detect threshold to K8s v1.35

The ImageVolume feature gate is only enabled by default starting in
K8s v1.35, not v1.33 (where it is beta but off by default). Clusters
on v1.33-v1.34 can still opt in with an explicit sideloadMethod value.
2026-05-11 09:09:14 -07:00
Evan Lezar 5c98604f0f feat(gpu): honor device IDs in Docker and Podman (#1253)
* feat(gpu): honor device IDs in Docker and Podman

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

* test(gpu): add Docker and Podman device selection e2e

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

* ci(gpu): run Docker GPU e2e workflow

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

---------

Signed-off-by: Evan Lezar <elezar@nvidia.com>
2026-05-11 17:09:37 +02:00
Zygmunt Krynicki dfd47683e7 (feat) early snap support (#1238) v0.0.38 2026-05-10 21:12:30 -07:00
Tinson Lai 977be3176f fix(docker): route VM-Docker runtimes through host-gateway (#1301)
Colima, Lima, Rancher Desktop, and OrbStack all run dockerd inside a
host VM. Their bridge gateway IP is reachable from inside containers
but not from the OpenShell server process running on the host, the
same constraint Docker Desktop has — yet the existing
is_docker_desktop check rejects them, leaving callbacks routed at a
bridge IP nothing on the host can listen on.

Detect these runtimes by daemon Name (Lima sets the VM hostname to
colima*, lima-*, rancher-desktop, orbstack) and supplemental labels
(dev.rancherdesktop.*, dev.orbstack.*), and route them through
host-gateway like Docker Desktop.

Signed-off-by: Tinson Lai <tinsonl@nvidia.com>
2026-05-10 21:05:20 -07:00
Derek Carr 24cbaa1149 feat(driver-kubernetes): disable service account token auto-mounting (#1298)
Disables automountServiceAccountToken in sandbox pods for security
hardening. Sandbox pods should not have access to the Kubernetes API
by default.

Adds test case to verify the pod spec includes the disabled setting.

Signed-off-by: Derek Carr <decarr@redhat.com>
2026-05-09 17:44:21 -07:00
Drew Newberry ca63841953 docs(rfc): move policy management RFC to 0002 (#1283) 2026-05-09 11:30:30 -07:00
Drew Newberry 4350482160 docs(readme): add roadmap and RFC issue guidance (#1284)
Signed-off-by: Drew Newberry <anewberry@nvidia.com>
2026-05-09 11:28:09 -07:00
Derek Carr 8d8377625e fix(docker): add SELinux labeling to bind mounts (#1291) 2026-05-09 11:18:34 -07:00
Drew Newberry 072f227248 fix(installer): guard incompatible v0.0.37 upgrades (#1294) 2026-05-09 11:10:57 -07:00
Russell Bryant 57a80ed2ad fix(gateway): update Podman supervisor build task name (#1288)
The Podman path in the dev gateway script references the
build:docker:supervisor-sideload mise task, which was removed in
d8b84773 ("feat(rpm): add RPM packaging with Packit/COPR and GHA
release publishing (#1126)"). That commit consolidated the sideload
and standalone supervisor image builds into a single
build:docker:supervisor task.

Update the reference so `mise run gateway` works with the Podman
compute driver.

Signed-off-by: Russell Bryant <russell.bryant@gmail.com>
2026-05-09 00:17:12 -05:00
Russell Bryant af60d4e466 docs: document OPENSHELL_SSH_HANDSHAKE_SECRET in Getting Started (#1287)
The Podman and Kubernetes compute drivers require
OPENSHELL_SSH_HANDSHAKE_SECRET to be set. This was introduced in
2e0afeab ("feat(vm): derive guest rootfs from sandbox images (#957)"),
which exempted only the Docker and VM drivers from the check.

The Getting Started instructions in CONTRIBUTING.md didn't mention
the variable, so developers using Podman (the default on systems
where it is installed) hit an opaque configuration error on first run.

Add the export as a separate setup step with a comment explaining
which drivers require it.

Signed-off-by: Russell Bryant <russell.bryant@gmail.com>
2026-05-09 00:16:47 -05:00
Alexander WatsonandJohn Myers 1c79b2131f feat: agent-driven policy management MVP (#1151)
* docs(rfc): add agent-driven policy management

* docs(rfc): switch policy MVP to local API

* docs(rfc): clarify policy advisor skill and local logs

* feat(sandbox): add agent-driven policy proposal loop

* test(examples): add codex policy dogfood loop

* refactor(examples): make policy demo agent-agnostic

* refactor(examples): colocate policy validation harness

* docs(examples): add policy demo env sample

* docs(examples): use placeholder env example

* feat(sandbox): wire policy.local denials to OCSF JSONL log

Wires GET /v1/denials?last=N on the sandbox-local policy advisor API to read
recent OCSF JSONL events from /var/log/openshell-ocsf.YYYY-MM-DD.log, filter
to network/L7 denials (action_id=2, class_uid 4001/4002), and return a
compact summary newest-first. Default limit is 10, capped at 100. Ran inside
spawn_blocking so file I/O does not block the policy.local handler.

Other cleanup:

- POST /v1/proposals now uses the typed grpc_client wrapper instead of
  raw_client, so accepted/rejected counts surface to the agent uniformly.
  Wrapper return type extended to the response struct.
- Drop the 'add_rule' snake_case alias in the proposal JSON; canonical form
  is camelCase 'addRule', matching the PolicyMergeOperation convention used
  elsewhere.
- skills/policy_advisor.md updated to match: documents the now-real
  /v1/denials?last=10 endpoint and uses 'addRule' consistently.
- skills.rs test asserts on the canonical 'addRule' phrase rather than the
  removed 'PolicyMergeOperation' substring.

* feat(cli): show L7 protocol/method/path in rule get output

format_endpoint() previously rendered only host:port, dropping protocol,
access, and the L7 rules array. That made openshell rule get text output
unable to distinguish a broad L4 grant from a method/path-scoped L7 REST
rule -- exactly the distinction a developer needs at approval time.

New rendering tags each endpoint with its enforcement layer and surfaces
allow/deny rules:

  bare L4:           api.example:443 [L4]
  L7 read-only:      api.example:443 [L7 rest, access=read-only]
  L7 method/path:    api.example:443 [L7 rest, allow PUT /v1/foo/bar]

Pure display change: no proto, gateway, or behavior changes. Unit test
covers all three rendering cases with synthetic fixtures.

* refactor(examples): rewrite policy demo as Codex-default loop

Re-shape examples/agent-driven-policy-management/ to be a single, clean
end-to-end demonstration of the agent-driven policy loop. A Codex agent
inside an OpenShell sandbox attempts a GitHub Contents API write, hits a
structured 403 from the L7 proxy, reads the policy_advisor skill, drafts a
narrow addRule proposal via http://policy.local/v1/proposals, the host
auto-approves, the sandbox hot-reloads policy, and the agent's retry
succeeds. Whole loop runs in roughly two minutes.

Demo cleanup:

- Drop .env file ceremony. Defaults resolve from gh: owner via
  'gh api user --jq .login', repo defaults to 'openshell-policy-demo',
  token from gh auth token / GITHUB_TOKEN / GH_TOKEN. With gh auth login
  and codex login already done, 'bash demo.sh' Just Works.
- Codex-specific. Bootstraps ~/.codex/auth.json from credentials injected
  by the OpenShell provider, runs codex exec --sandbox danger-full-access
  (OpenShell is the actual security boundary; bwrap nesting cannot create
  user namespaces inside the sandbox container).
- Tighter narrative output: a single 'Preflight' step, a run summary banner
  before launch, an inline narration of what's happening inside the sandbox
  while we poll for the proposal (including the literal structured 403
  body the agent acts on), and an OCSF trace at the end filtered to the
  three events that tell the story (DENY, RELOAD, ALLOW).
- Replace Python heredoc templating with sed; uploads use the single-flag
  pattern (--upload "${PAYLOAD_DIR}:/sandbox") with files referenced at
  the basename-prefixed path that #952 / #1028 established.
- README documents the trust model honestly: structured rule is the
  contract, agent rationale is a hint, prover validation badge in
  progress per RFC 0001.

Move the deterministic no-LLM regression harness out of examples/ into
e2e/policy-advisor/ -- it was a parallel demo, not an example. Same loop
without the LLM, useful for iterating on the proxy and policy.local API.

* style(sandbox,cli): apply rustfmt

Whitespace-only fixups caught by mise run pre-commit. No functional change.

* perf(examples): cap Codex reasoning at 'low' in policy demo

The demo task is mechanical (one HTTP request, parse a structured 403,
post a JSON proposal, retry). Codex's default high-effort reasoning
roughly doubles the demo's wall time without improving outcomes; running
at 'low' lands the same minimal L7 grant in roughly half the time.

Override with DEMO_CODEX_REASONING=medium (or higher) to compare runs.

* fix(sandbox): harden policy.local denials endpoint

Three changes addressing review feedback before merging the agent-driven
policy management MVP:

- Distinguish "OCSF JSONL enabled, no denials" from "OCSF JSONL disabled,
  nothing to read." The endpoint now returns a `log_available` flag and an
  explanatory `note` when the log file is missing, so the in-sandbox agent
  can give the developer an accurate hint instead of a misleading empty
  list.
- Stop echoing the OCSF `message` field in the per-denial summary. The
  proxy's denial messages can include the request path with query string
  (e.g., `?access_token=...`); the structured `host`/`port`/`method`/
  `path`/`binary` fields carry everything the agent needs to draft a
  proposal, and `path` is sourced from `http_request.url.path` which
  already excludes the query string.
- Cap `read_request_body` at a 15s timeout. Bounds slowloris-style stalls
  from a misbehaving in-sandbox process. The proxy listener only accepts
  loopback connections so practical impact is small, but this is cheap
  defense-in-depth.

New tests cover the missing-log signal and the message-redaction guarantee.

* fix(examples): redact tokens in agent log tail and validate DEMO_FILE_DIR

Two small hardening passes on the policy management demo:

- `fail()` now pipes the agent log tail through a redactor that masks the
  GitHub token and Codex credential triple before printing. Codex itself is
  well-behaved about not echoing the token, but a misbehaving tool call
  could leak it; this is a final safety net before the log hits the
  developer's terminal (and any clipboard or chat history that follows).
- `validate_env` now regex-checks DEMO_FILE_DIR with the same allow-list
  the other path-shaped variables use. The value is interpolated through
  sed with `|` as the delimiter when rendering the agent task; rejecting
  unsupported characters keeps the templating predictable and stops a
  user-supplied value from breaking out into a shell context.

* refactor(sandbox): centralize policy.local routes and skill path

Addresses review feedback that the deny body's `next_steps` array and the
route table could drift apart. The route paths and skill location now live
as `pub const`s in `policy_local.rs` and feed both:

- the dispatcher in `route_request` that matches against them
- a new `agent_next_steps()` helper that builds the JSON the L7 deny body
  embeds

`l7/rest.rs::deny_response_body` calls `policy_local::agent_next_steps()`
instead of inlining the array, so adding or renaming a route is a one-line
change in `policy_local.rs` and the agent contract follows automatically.

* feat(sandbox): switch /v1/denials to shorthand log pass-through

Previously /v1/denials parsed `/var/log/openshell-ocsf.*.log` (OCSF JSONL)
and returned structured per-event objects. JSONL is opt-in via
`ocsf_json_enabled`, so the endpoint returned an empty list with a "log
not enabled" hint by default — agents had to navigate a setup step before
the inspect-recent-denials guidance was useful.

Switch to reading the shorthand log at `/var/log/openshell.*.log`, which
is always-on and the same human-readable format `openshell logs` displays.
The endpoint now returns raw shorthand lines (newest first) — the agent
reads them directly, no field parsing.

Tradeoffs:
- Removes the JSONL-on-by-default debate: shorthand is already on, no
  defaults change.
- Updating shorthand is a single-file change in this repo; no schema rev
  needed when we want to add fields.

Implementation:
- `read_recent_denial_lines` walks shorthand log files newest-first,
  filters lines with ` OCSF ` AND ` DENIED ` (the OCSF action label,
  uppercase, space-bounded).
- `collect_shorthand_log_files` matches `openshell.<date>.log`; the
  trailing dot in `SHORTHAND_LOG_PREFIX = "openshell."` excludes
  `openshell-ocsf.<date>.log` so JSONL-on doesn't bleed into responses.
- 4096-byte cap per surfaced line as defense against pathological inputs.
- Skill doc updated to reflect that `/v1/denials` returns raw shorthand
  lines, not structured fields.

Defense-in-depth on query-string secrets:
- `redact_query_strings` strips `?<query>` to `?[redacted]` from each
  surfaced line. The L7 relay path emits OCSF events using
  `redacted_target` (secret-placeholder redaction), but the FORWARD deny
  path in `proxy.rs` populates `OcsfUrl::new("http", host, path, port)`
  and `.message(...)` with the raw request path — query string included.
  Stripping queries at the consumer guards `/v1/denials` regardless of
  whether the upstream emit sites are tightened. The on-disk log is not
  rewritten by this change; that is a separate hardening task tracked
  for the FORWARD path emit sites in proxy.rs.
- `truncate_at_char_boundary` is UTF-8 safe; redaction runs before
  truncation so a cut cannot slice mid-secret.

Tests:
- `recent_denials_returns_newest_first_from_shorthand_lines` covers the
  happy path with mixed allowed/denied/non-OCSF lines.
- `recent_denials_skips_jsonl_log_files` confirms JSONL files don't
  surface even if present.
- `recent_denials_truncates_pathological_lines` covers the cap.
- `is_ocsf_denial_line_filters_correctly` covers the line-level filter.
- `redact_query_strings_removes_query_from_url_token` and
  `redact_query_strings_removes_query_in_reason_tag` cover the redaction
  in both URL token and `[reason:...]` contexts.
- `truncate_at_char_boundary_does_not_panic_on_multibyte` covers the
  UTF-8 safety.

* chore(sandbox): align proto inits with main's L7 GraphQL additions

Post-rebase fixups after #1083 (GraphQL L7 inspection) landed on main and
introduced new fields on the proto types this branch constructs:

- `crates/openshell-sandbox/src/l7/relay.rs`: two `deny_with_redacted_target`
  call sites (REST and GraphQL relay deny paths) now pass the
  `DenyResponseContext` argument that `rest::send_deny_response` expects.
  Both sites pass `host`, `port`, and `binary` from the existing
  `L7EvalContext`, matching the pattern used at the primary deny site.
- `crates/openshell-sandbox/src/policy_local.rs`: `L7Allow`, `L7DenyRule`,
  and `NetworkEndpoint` proto initializers now populate the new GraphQL
  and path-scoping fields with empty defaults. Agent-authored proposals
  via `policy.local` target REST/SQL/L4 today; GraphQL operation matching
  is set on the gateway side or via direct YAML, so empty defaults are
  correct here.

No behavior change. `cargo test -p openshell-sandbox --lib` (650 tests) and
`cargo clippy -p openshell-sandbox --lib --tests -- -D warnings` clean.

* feat(sandbox): gate agent policy proposals behind opt-in feature flag

The agent-driven policy proposal surface delivered by this PR (skill
install, `policy.local` API, `next_steps` array on L7 deny bodies) is
now opt-in via the new `agent_policy_proposals_enabled` setting. Default
false. Same shape as `providers_v2_enabled`: registered in
`openshell-core::settings`, sandbox-level, hot-toggleable via the
existing settings poll loop.

Why: the surface is a novel agent-controlled mutation point in every
sandbox. The per-proposal developer approval gate is a correctness
control, but it doesn't address "should this sandbox have an
agent-authoring API at all" — compliance teams may want that question
closed. The flag is the second gate.

Implementation:
- New registry entry + `AGENT_POLICY_PROPOSALS_ENABLED_KEY` constant in
  `openshell-core::settings`.
- `lib.rs`: process-wide `OnceLock<Arc<AtomicBool>>` mirroring the
  `OCSF_CTX` pattern. `agent_proposals_enabled()` is the single read
  point.
- Initial settings fetch added to `run_sandbox` so skill install honors
  the flag at startup (not just on the poll loop's first tick).
- Skill install in `run_sandbox` is gated on the flag.
- `policy_local::route_request` returns `404 feature_disabled` for all
  routes when the flag is off — including the otherwise-public
  `current_policy` and `denials` routes. When the surface is off it's
  off entirely.
- `policy_local::agent_next_steps` returns an empty array when the flag
  is off so deny bodies don't advertise routes that 404.
- Poll loop updates the atomic on each tick, lazily installs the skill
  on a false→true transition (no claw-back on true→false; stale skill
  on disk is harmless because route + next_steps gate on the live atom).

Tests:
- Shared `test_helpers::ProposalsFlagGuard` mutex+atomic guard for the
  process-wide flag, used across `policy_local::tests` and
  `l7::rest::tests`.
- New: `agent_next_steps_returns_empty_when_flag_off`,
  `agent_next_steps_returns_full_array_when_flag_on`,
  `route_request_returns_feature_disabled_when_flag_off`.
- Updated existing tests that exercise the deny body or the route
  dispatcher to set the flag on first.
- Full sandbox lib test suite: 653 pass, clippy clean.

Demo and e2e:
- `examples/agent-driven-policy-management/demo.sh` and
  `e2e/policy-advisor/test.sh` now snapshot the prior global value of
  the setting, set it to true before sandbox creation (so the
  supervisor's initial poll picks it up), and restore on exit (delete
  if previously unset, otherwise write the prior value back).

Docs:
- RFC 0001 MVP-implementation note documents the flag, default, and
  intended soft-launch posture.

* test(policy-advisor): require proposal opt-in for e2e

* refactor(sandbox): group policy poll loop state

* test(e2e): isolate Kubernetes user namespace test

---------

Co-authored-by: John Myers <9696606+johntmyers@users.noreply.github.com>
2026-05-08 17:14:27 -07:00
Drew Newberry 529be37fd4 chore(installer): promote package install script (#1261) 2026-05-08 17:04:15 -07:00
Saurabh Agarwal 40417981ec fix(helm): derive sandboxNamespace from Release.Namespace instead of hardcoding (#1282)
Signed-off-by: Saurabh Agarwal <sauagarwa@redhat.com>
2026-05-08 16:15:01 -07:00
Drew Newberry 7ad823ea82 fix(install): register local gateway before probing listener (#1280)
Signed-off-by: Drew Newberry <anewberry@nvidia.com>
2026-05-08 15:14:54 -07:00
Drew Newberry b8e87431a5 fix(e2e): isolate kubernetes user namespace test (#1276) v0.0.37 2026-05-08 14:46:57 -07:00
Taylor Mutch 316c788eac fix(helm): derive grpcEndpoint from chart context (#1241)
* fix(helm): derive grpcEndpoint from chart context

The chart hardcoded server.grpcEndpoint to
https://openshell.openshell.svc.cluster.local:8080, which only matched the
in-cluster Service DNS for the standard release name and namespace. A new
helper now builds <scheme>://<fullname>.<namespace>.svc.cluster.local:<port>
from chart context, picking the scheme from server.disableTls. An explicit
server.grpcEndpoint override is passed through verbatim.

* chore(scripts): validate k3d cluster name length early

helm-k3s-local.sh derives the cluster name from the current branch suffix.
Long branch names produced names exceeding k3d's 32-char cap and failed
deep inside k3d cluster create with a confusing validation error. cmd_create
now bails out before invoking docker/k3d with a copy-pasteable
HELM_K3S_CLUSTER_NAME override hint. Status, start, stop, delete, and help
remain unaffected so an over-long derived name does not block diagnostics.
2026-05-08 14:10:19 -07:00
Drew Newberry eec949dd5e fix(installer): stop forcing Homebrew VM driver (#1277) 2026-05-08 13:49:22 -07:00
Drew Newberry daa2a362d5 fix(packaging): enable mTLS for local packages (#1271) 2026-05-08 13:02:12 -07:00
jtoelke2 31f03456a9 ci(os-132): remove obsolete shadow workflows (#1273)
Signed-off-by: Jonas Toelke <jtoelke@nvidia.com>
2026-05-08 14:45:22 -05:00
John T. Myers 1d3b741ee3 feat(providers): support sandbox provider attach lifecycle (#1242)
* feat(providers): support sandbox provider attach lifecycle

Closes #1171

Adds sandbox provider list, attach, and detach API/CLI support while keeping provider policy and credential resolution derived from current sandbox attachments.

* fix(providers): refresh sandbox provider credentials

Adds provider environment revisions and generation-scoped sandbox credential snapshots so future SSH and exec launches pick up provider attach, detach, and credential updates without mutating already-running processes.

Also blocks provider deletion while attached to prevent stale sandbox provider references.

* fix(providers): serialize sandbox object mutations

* test(providers): cover sandbox provider attach lifecycle

* test(providers): accept versioned credential placeholders
2026-05-08 11:14:44 -07:00
jtoelke2 3cfc915bfe ci(os-132): remove stale remote buildx mode (#1267)
Signed-off-by: Jonas Toelke <jtoelke@nvidia.com>
2026-05-08 12:20:42 -05:00
Drew Newberry b74d24bcf6 fix(docs): constrain landing terminal height (#1269)
Signed-off-by: Drew Newberry <anewberry@nvidia.com>
2026-05-08 10:16:04 -07:00
Taylor Mutch a4efc0b738 feat(server): add generate-certs subcommand; replace alpine PKI hook (#1257) 2026-05-08 09:49:07 -07:00
Mrunal Patel 1f35abbefb feat(sandbox): add Kubernetes user namespace isolation (hostUsers: false) (#983)
Add opt-in support for Kubernetes user namespace isolation on sandbox
pods. When enabled, container UID 0 maps to an unprivileged host UID
and capabilities become namespaced, providing defense-in-depth for the
supervisor process.

Configuration is two-layered: a cluster-wide default via
OPENSHELL_ENABLE_USER_NAMESPACES (default false) and a per-sandbox
override via the new `user_namespaces` field on SandboxTemplate.

When user namespaces are active, the pod security context is extended
with SETUID, SETGID, and DAC_READ_SEARCH capabilities to match the
bounding-set requirements inside a user namespace.

Introduces SandboxPodParams struct to replace long argument lists on
sandbox_to_k8s_spec and sandbox_template_to_k8s.

Validated end-to-end on OCP 4.22 (K8s 1.35.3, CRI-O 1.35, RHEL
CoreOS, kernel 5.14) with full SSH tunnel and non-identity UID mapping.
2026-05-08 12:39:48 -04:00
Drew Newberry 645b880513 feat(install): add rpm dev installer support (#1262)
Signed-off-by: Drew Newberry <anewberry@nvidia.com>
2026-05-08 09:45:30 -05:00
Drew Newberry 52097f2d4c ci(release): run package release canaries (#1256) 2026-05-07 21:57:55 -07:00
Drew Newberry 8ab5ee875b fix(vm): harden compute driver socket (#1248) 2026-05-07 20:03:16 -07:00
Drew Newberry 62619eefc7 fix(docker): use supervisor image entrypoint path (#1259)
Signed-off-by: Drew Newberry <anewberry@nvidia.com>
2026-05-07 17:39:44 -07:00
Drew Newberry 084c93b6ad fix(installer): repair dev install package and service setup (#1252) 2026-05-07 16:05:40 -07:00
Taylor Mutch 49cc5a079a docs(helm): replace hard tabs with spaces in README OpenShift block (#1254)
Signed-off-by: Taylor Mutch <taylormutch@gmail.com>
2026-05-07 15:12:16 -07:00
Seth Jennings d2e80d1c94 docs(helm): add agent sandbox prerequisite to Helm README (#1249)
* docs(helm): add agent sandbox prerequisite to Helm README

Add a Prerequisites section requiring the Kubernetes Agent Sandbox
CRDs and controller (v0.4.3) to be installed before deploying OpenShell.

* docs(helm): use latest release URL for agent sandbox manifest
2026-05-07 13:35:29 -07:00
Drew Newberry 310e1a5c14 docs(podman): restore driver architecture details (#1244) 2026-05-07 13:22:24 -07:00
Mesut Oezdil d2321a2c00 docs: fix broken policy-engine anchor in policies page (#1246) 2026-05-07 13:19:42 -07:00
Mesut Oezdil 570600391c docs(helm): fix overlay values paths after ci/ reorganization (#1247) 2026-05-07 12:58:59 -07:00
Taylor Mutch d8f614fa31 docs(kubernetes): add initial reference docs (#1243)
* docs(kubernetes): add initial reference docs

Add a Kubernetes reference section under docs/reference/kubernetes/ covering
gateway setup, OIDC and reverse-proxy authentication, cert-manager PKI, and
Gateway API external access. Update installation.mdx to link to the new setup
page and nest the new folder under the Reference section in index.yml.

* docs(kubernetes): promote Kubernetes to top-level nav and retitle pages

Move docs/reference/kubernetes/ to docs/kubernetes/ so the section appears
as a top-level nav entry rather than nested under Reference. Retitle the
pages to broader, action-oriented names (Managing Certificates, Ingress,
Access Control) and rename the files to match. Update cross-references in
setup.mdx and the installation page.

* docs(nav): revert Reference to folder mode

The Reference section was switched to explicit section/contents to host
the nested Kubernetes subsection. With Kubernetes promoted to a top-level
folder, restore folder-mode auto-detection. Side effect: sandbox-compute-drivers
is picked up again, which was silently absent from the explicit list.

* docs(kubernetes): add OpenShift install page

Mirror the OpenShift install instructions from PR #1240 into the
Kubernetes docs section. Documents the SCC binding for sandbox pods
and the chart overrides required by OpenShift's Security Context
Constraints (disabled PKI init Job, plaintext TLS, cleared fsGroup
and runAsUser).
2026-05-07 12:55:56 -07:00
Seth Jennings 916a9d966f docs(helm): add install instructions for OpenShift (#1240) 2026-05-07 12:36:31 -07:00
Drew Newberry 028763d4db refactor(vm): remove legacy openshell-vm crate (#1239)
Signed-off-by: Drew Newberry <anewberry@nvidia.com>
2026-05-07 10:50:20 -07:00
Taylor Mutch 909e9034aa ci(helm): add helm lint workflow and reorganize chart values under ci/ (#1223)
* test(helm): Reorganize values under /ci, update mise helm:lint task loop

WIP

* ci: Add a helm lint job to validate helm linting passes on changes
2026-05-07 10:36:27 -07:00
Drew Newberry 70a0f6c547 refactor(cli): remove gateway lifecycle management (#1221) 2026-05-07 09:54:13 -07:00
Adam Miller 5bf22fd69a feat(rpm): use :dev image tag for non-release Packit builds (#1218)
* feat(rpm): use :dev image tag for non-release builds

Add a %global image_tag macro to the spec file (default: dev) that
controls the container image tag baked into RPM-built binaries and
the installed systemd unit.

Packit's fix-spec-file action detects tagged stable releases via
git describe --exact-match and overrides the macro to 'latest'.
PR and commit-to-main builds keep the 'dev' default, matching the
:dev images pushed by release-dev.yml.

The init-gateway-env.sh installed copy is also patched at RPM build
time so its commented image defaults stay consistent with the tag
the RPM actually expects from the registry.

Signed-off-by: Adam Miller <admiller@redhat.com>

* fix(rpm): scope image_tag to supervisor only; sandbox base stays latest

The community sandbox base image (ghcr.io/nvidia/openshell-community/
sandboxes/base) does not publish :dev tags -- only :latest and version-
pinned tags are available. Applying %{image_tag} to it caused a
manifest unknown error when pulling the image on non-release builds.

Scope the dynamic tag to the supervisor image only, which is part of
the core OpenShell release pipeline and does receive :dev tags from
release-dev.yml. The sandbox base image unconditionally uses :latest.

Also tighten the sed in %install to target only the supervisor image
line in init-gateway-env.sh rather than blanket-replacing all :latest
occurrences.

Signed-off-by: Adam Miller <admiller@redhat.com>

* fix(rpm): restore %%{openshell_python_version} in Python dist-info METADATA

The Version field inside the METADATA heredoc was hardcoded to 0.0.37
instead of using the %%{openshell_python_version} macro. This caused
the installed Python dist-info to always report the wrong version on
non-release builds.

Signed-off-by: Adam Miller <admiller@redhat.com>

---------

Signed-off-by: Adam Miller <admiller@redhat.com>
2026-05-07 11:45:03 -05:00
John T. Myers cdb1de59ba feat(providers): add custom profile registry (#1170)
Add custom profile registry. Allow attaching custom profiles at sandbox start.
2026-05-07 09:41:25 -07:00
Seth Jennings 8594cb71cf feat(helm): set nameOverride to openshell (#1237) 2026-05-07 09:33:41 -07:00