Commit Graph
50 Commits
Author SHA1 Message Date
Drew Newberry f169084923 fix(supervisor): tailor Landlock rights by inode type (#2380)
Closes #2218

Signed-off-by: Drew Newberry <anewberry@nvidia.com>
2026-07-21 03:59:02 +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
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
Evan Lezar 5102cb9413 fix(sandbox): restore GPU procfs baseline (#1522)
Signed-off-by: Evan Lezar <elezar@nvidia.com>
2026-06-03 11:08:21 +02:00
John T. Myers 62c421b21e feat(providers): add profile-backed policy visibility (#1640)
* chore: wip providers v2 tui and codex profile

* chore: wip effective policy get and codex profile

* chore: wip provider profiles and tui detail views

* feat(tui): annotate policy proposal review status
2026-06-02 17:39:01 -07:00
Alexander Watson e98ea3ee93 feat(policy): add agentic approval loop (#1528) 2026-05-29 21:11:31 -07:00
mjamivandJohn Myers 528fb29147 fix(sandbox): allow first-label L7 host wildcards (#1304)
* fix(sandbox): allow first-label L7 host wildcards

* docs(sandbox): document L7 host wildcard contract + add OPA runtime tests

- Add Host Wildcards section to architecture/security-policy.md
  describing accepted (first-label *, **, intra-label *-X) and
  rejected (bare, TLD, non-first-label, recursive-in-label) forms,
  and noting that wildcards never cross '.' boundaries.
- Expand the policy-schema.mdx 'host' field description to reflect
  the same contract instead of only mentioning '*.example.com'.
- Add OPA runtime tests asserting '*-aiplatform.googleapis.com'
  matches 'us-central1-aiplatform.googleapis.com' and does not match
  'us-central1.aiplatform.googleapis.com' (cross-dot boundary). Locks
  validator/runtime alignment for intra-label wildcards.

* chore: update mise lockfile

* test(server): tolerate serialized inference upserts

---------

Co-authored-by: John Myers <9696606+johntmyers@users.noreply.github.com>
2026-05-21 09:32:37 -07:00
Russell Bryant 9a7c0df00b fix(sandbox): remove DNS resolution from mechanistic mapper to prevent data exfiltration (#1329)
* fix(sandbox): remove DNS resolution from mechanistic mapper to prevent data exfiltration

The mechanistic mapper resolved denied hostnames via DNS to check whether
they mapped to private IPs (for populating allowed_ips in proposals).
This leaked the denied hostname through DNS queries even though the HTTP
connection was blocked by policy — a data exfiltration vector.

Remove resolve_allowed_ips_if_private() entirely. Proposals no longer
include allowed_ips. If a user applies a proposed rule and the host
resolves to a private IP, the proxy's SSRF defense denies the connection.
That denial flows back through the aggregator, and the user can then
explicitly configure allowed_ips in their policy. This two-step flow is
also more explicit about private IP access, which is desirable given #1245.

Closes #1169

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

* fix(sandbox): update SSRF note and document two-step allowed_ips flow

Update the mechanistic mapper SSRF security note to reflect that
proposals no longer populate allowed_ips — approving a proposal does
not bypass SSRF protection. Clarify the two-step approval flow in
architecture/security-policy.md.

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

---------

Signed-off-by: Russell Bryant <russell.bryant@gmail.com>
2026-05-15 09:14:17 -07:00
Aaron Erickson 🦞andJohn Myers 9ea94b645d fix(sandbox): rewrite messaging credential placeholders (#1286)
* fix(sandbox): rewrite credential placeholders in websocket text frames

Signed-off-by: Aaron Erickson <aerickson@nvidia.com>

* fix(sandbox): harden websocket credential rewrite

Signed-off-by: Aaron Erickson <aerickson@nvidia.com>

* feat(sandbox): add websocket l7 inspection and compression

Signed-off-by: Aaron Erickson <aerickson@nvidia.com>

* fix(sandbox): harden websocket upgrade validation

Signed-off-by: Aaron Erickson <aerickson@nvidia.com>

* test(sandbox): cover route-selected websocket upgrades

Signed-off-by: Aaron Erickson <aerickson@nvidia.com>

* fix(sandbox): harden websocket negotiation parsing

Signed-off-by: Aaron Erickson <aerickson@nvidia.com>

* test(sandbox): add websocket conformance relay matrix

Signed-off-by: Aaron Erickson <aerickson@nvidia.com>

* test(e2e): add websocket conformance lane

Signed-off-by: Aaron Erickson <aerickson@nvidia.com>

* fix(policy): support websocket incremental rules

Signed-off-by: Aaron Erickson <aerickson@nvidia.com>

* feat(policy): enable websocket credential rewrite updates

Signed-off-by: Aaron Erickson <aerickson@nvidia.com>

* fix(cli): make websocket rewrite endpoint-local

Signed-off-by: Aaron Erickson <aerickson@nvidia.com>

* feat(sandbox): support graphql websocket policy

Signed-off-by: Aaron Erickson <aerickson@nvidia.com>

* fix(policy): allow private IPs for websocket endpoints

* feat(sandbox): rewrite REST credential placeholders

Signed-off-by: Aaron Erickson <aerickson@nvidia.com>

* refactor(sandbox): generalize credential aliases

Signed-off-by: Aaron Erickson <aerickson@nvidia.com>

* fix(sandbox): rewrite encoded form credentials

Signed-off-by: Aaron Erickson <aerickson@nvidia.com>

* fix(sandbox): close websocket policy and provider alias gaps

Signed-off-by: Aaron Erickson <aerickson@nvidia.com>

* fix(e2e): route websocket probe through host gateway

* fix(e2e): stabilize websocket probe handshake

* fix(e2e): exercise websocket probe through proxy

* ci: remove websocket conformance workflow

---------

Signed-off-by: Aaron Erickson <aerickson@nvidia.com>
Co-authored-by: John Myers <9696606+johntmyers@users.noreply.github.com>
2026-05-11 22:40:44 -07:00
Drew Newberry cc2114e267 docs(architecture): reset subsystem docs (#1184) 2026-05-07 08:59:02 -07:00
Drew Newberry f56c09c7df docs: update gateway deployment architecture (#1108) 2026-05-04 22:52:29 -07:00
John T. Myers 6b21804258 feat(policy): add GraphQL L7 inspection (#1083)
Support GraphQL L7 policies
2026-05-04 11:50:09 -07:00
John T. Myers 5d0a44cbac fix(sandbox): invalidate stale l7 tunnels after reload (#1118)
Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com>
2026-05-01 13:01:30 -07:00
Piotr Mlocek ee2de81bc9 fix(sandbox): preserve encoded slash policy from proto (#1073) 2026-04-29 16:36:49 -07:00
Piotr Mlocek df38d1f66f feat(ci): add Markdown and Mermaid linting (#933) 2026-04-24 11:27:02 -07:00
John T. Myers f954e5927a fix(sandbox): resolve sandbox host aliases in SSRF checks (#912)
Closes #879

Resolve policy hostnames against the sandbox's /etc/hosts before DNS so Kubernetes hostAliases are visible to the proxy while keeping the existing allowed_ips SSRF enforcement semantics intact.

Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com>
2026-04-22 12:37:32 -07:00
John T. Myers e28ca07863 fix(sandbox): preserve explicit read-only baseline paths (#910)
Closes #905

Skip proxy baseline read-write enrichment when a path is already explicitly configured as read-only, add regression coverage for the proto and local policy code paths, and document the precedence rule in the sandbox architecture docs.

Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com>
2026-04-22 12:18:24 -07:00
John T. Myers 40e9bf6feb feat(policy): add incremental sandbox policy updates (#860)
* feat(policy): add incremental sandbox policy updates

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

* docs(policies): expand incremental update guidance

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

* feat(policy): audit incremental updates in gateway logs

* docs(policy): quote glob specs in shell examples

---------

Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com>
2026-04-20 08:00:02 -07:00
John T. MyersandJohn Myers e0db01e9ff fix(sandbox): preserve ownership for existing read_write paths (#827)
Closes #783

Only chown read_write paths that the supervisor creates at startup, and leave pre-existing image paths with their original ownership. Add sandbox tests for creation, symlink rejection, and existing-path ownership preservation, and update architecture docs to match the new behavior.

Co-authored-by: John Myers <johntmyers@users.noreply.github.com>
2026-04-14 21:51:47 -07:00
John T. Myers 2ca553a4a0 fix(sandbox): validate always-blocked IPs at load time, enrich denial logs, and filter un-fixable proposals (#814) (#815)
Policies with allowed_ips entries targeting loopback, link-local, or
unspecified ranges now fail at connection time instead of being silently
blocked at runtime. The shorthand log format for DENIED events includes
a [reason:...] suffix so operators can distinguish 'allowlist miss' from
'structurally un-allowable'. The mechanistic mapper skips proposals for
always-blocked destinations, preventing the infinite TUI notification
loop. The gateway validates proposed rules on approval as defense-in-depth.

- Extract shared IP helpers (is_always_blocked_ip, is_always_blocked_net,
  is_internal_ip) to openshell_core::net
- Reject always-blocked entries in parse_allowed_ips with hard error
- Skip implicit allowed_ips synthesis for always-blocked literal IP hosts
- Add status_detail to HttpActivityBuilder for denial reason propagation
- Enrich NET and HTTP shorthand with [reason:...] for DENIED events
- Add engine: tag to HTTP shorthand (consistency with NET shorthand)
- Filter always-blocked proposals in mechanistic mapper generate_proposals
- Add validate_rule_not_always_blocked server-side defense-in-depth
- Update architecture docs, published docs, and E2E test assertions
2026-04-13 09:47:50 -07:00
John T. Myers 8887d7c66a fix(sandbox): harden seccomp filter to block dangerous syscalls (#740) 2026-04-02 14:30:51 -07:00
John T. MyersandJohn Myers e8950e624c feat(sandbox): add L7 query parameter matchers (#617)
* feat(sandbox): add L7 query parameter matchers

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

* fix(sandbox): decode + as space in query params and validate glob syntax

Three improvements from PR #617 review:

1. Decode + as space in query string values per the
   application/x-www-form-urlencoded convention. This matches Python's
   urllib.parse, JavaScript's URLSearchParams, Go's url.ParseQuery, and
   most HTTP frameworks. Literal + should be sent as %2B.

2. Add glob pattern syntax validation (warnings) for query matchers.
   Checks for unclosed brackets and braces in glob/any patterns. These
   are warnings (not errors) because OPA's glob.match is forgiving,
   but they surface likely typos during policy loading.

3. Add missing test cases: empty query values, keys without values,
   unicode after percent-decoding, empty query strings, and literal +
   via %2B encoding.

* fix(sandbox): add missing query_params field in forward proxy L7 request info

* style(sandbox): fix formatting in proxy L7 query param parsing

---------

Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com>
Co-authored-by: John Myers <johntmyers@users.noreply.github.com>
2026-03-30 13:53:12 -07:00
John T. MyersandJohn Myers 758c62d18d fix(sandbox): handle per-path Landlock errors instead of abandoning entire ruleset (#677)
* fix(sandbox): handle per-path Landlock errors instead of abandoning entire ruleset

A single missing path (e.g., /app in containers without that directory)
caused PathFd::new() to propagate an error out of the entire Landlock
setup closure. Under BestEffort mode, this silently disabled all
filesystem restrictions for the sandbox.

Changes:
- Extract try_open_path() and classify_path_error() helpers that handle
  PathFd failures per-path instead of per-ruleset
- BestEffort mode: skip inaccessible paths with a warning, apply
  remaining rules
- HardRequirement mode: fail immediately on any inaccessible path
- Add zero-rule safety check to prevent applying an empty ruleset that
  would block all filesystem access
- Pre-filter system-injected baseline paths (e.g., /app) in enrichment
  functions so missing paths never reach Landlock
- Add unit tests for try_open_path, classify_path_error, and error
  classification for ENOENT, EACCES, ELOOP, ENAMETOOLONG, ENOTDIR
- Update user-facing docs and architecture docs with Landlock behavior
  tables, baseline path filtering, and compatibility mode semantics
- Fix stale ABI::V1 references in docs (code uses ABI::V2)

Closes #664

* fix(sandbox): use debug log for NotFound in Landlock best-effort mode

NotFound errors for stale baseline paths (e.g. /app persisted in the
server-stored policy but absent in this container) are expected in
best-effort mode. Downgrade from warn! to debug! so the message does
not leak into SSH exec stdout (the pre_exec hook inherits the tracing
subscriber whose writer targets fd 1).

Genuine errors (permission denied, symlink loops, etc.) remain at warn!
for operator visibility.

Also move custom_image e2e marker from /opt to /etc (a Landlock baseline
read-only path) since the security fix now properly enforces filesystem
restrictions.

---------

Co-authored-by: John Myers <johntmyers@users.noreply.github.com>
2026-03-30 13:19:19 -07:00
LateNightHackathonandlatenighthackathon 94fbb643b3 fix(proxy): add L7 inspection to forward proxy path (#666)
* refactor(l7): export evaluate_l7_request for cross-module use

Make evaluate_l7_request() public so the forward proxy path can
evaluate individual requests against L7 policy without going
through the full relay_with_inspection() loop.

Signed-off-by: latenighthackathon <latenighthackathon@users.noreply.github.com>

* fix(proxy): add L7 inspection to forward proxy path

The forward proxy previously rejected all requests to endpoints with
L7 rules (blanket 403), forcing clients through the CONNECT tunnel.
This meant policies like read-only (allow GET, block POST) had no
effect on plain http:// requests through the forward proxy.

Replace the blanket rejection with actual L7 evaluation:
- Query L7 config for the endpoint (same as before)
- Clone the OPA engine and evaluate the request method/path
- Allow if L7 policy permits, deny with 403 if enforcement is enforce
- Audit mode: log but allow (matching CONNECT path behavior)
- Fail-closed: deny on evaluation errors

The forward proxy uses Connection: close (one request per connection),
so a single evaluation suffices — no relay loop needed.

Update e2e tests to validate the new behavior:
- GET /allowed → 200 (L7 policy allows)
- POST /allowed → 403 (L7 policy denies, enforcement: enforce)

Update security-policy.md to reflect the new forward proxy L7 behavior.

Closes #643

Signed-off-by: latenighthackathon <latenighthackathon@users.noreply.github.com>

* style(proxy): apply cargo fmt formatting

Signed-off-by: latenighthackathon <latenighthackathon@users.noreply.github.com>

---------

Signed-off-by: latenighthackathon <latenighthackathon@users.noreply.github.com>
Co-authored-by: latenighthackathon <latenighthackathon@users.noreply.github.com>
2026-03-30 09:24:38 -07:00
John T. Myers f37b69b5e5 feat(sandbox): auto-detect TLS and terminate unconditionally for credential injection (#544)
* feat(sandbox): auto-detect TLS and terminate unconditionally for credential injection

Closes #533

The proxy now auto-detects TLS by peeking the first bytes of each
connection. When TLS is detected, it terminates unconditionally —
enabling credential injection and optional L7 inspection without
requiring explicit 'tls: terminate' in the policy.
2026-03-23 18:45:18 -07:00
John T. Myers a831a8921b feat(settings): gateway-to-sandbox runtime settings channel (#474)
* feat(gateway/sandbox): add global and sandbox runtime settings flow
2026-03-20 14:08:57 -07:00
Piotr Mlocek c0cdd665b6 fix(gateway): allow first live network policy update (#493) 2026-03-19 20:09:52 -07:00
John T. Myersandjohntmyers 241e95dc39 feat(policy): support host wildcards and multi-port endpoints (#366)
* feat(policy): support host wildcards and multi-port endpoints

Add glob-style host wildcards to endpoints[].host using OPA's
glob.match with "." as delimiter — *.example.com matches a single
DNS label, **.example.com matches across labels. Validation rejects
bare * and requires *. prefix; warns on broad patterns like *.com.

Add repeated uint32 ports field to NetworkEndpoint for multi-port
support. Backwards compatible: existing port scalar is normalized to
ports array. Both the proto-to-JSON and YAML-to-JSON conversion paths
emit a ports array; Rego always references endpoint.ports[_].

Fix OpaEngine::reload() to route through the full preprocessing
pipeline instead of bypassing L7 validation and port normalization.

Closes #359

* fix(policy): reject configs with both port and ports set

---------

Co-authored-by: johntmyers <johntmyers@users.noreply.github.com>
2026-03-16 13:36:33 -07:00
Piotr Mlocek 85903b95e0 docs: add debug-inference skill, Ollama tutorial, and remove stale inference policy references (#353) 2026-03-16 07:26:25 -07:00
Drew Newberry fbd93a4632 refactor: rename navigator- crate prefix to openshell- (#277) 2026-03-13 02:02:18 -07:00
Drew Newberry 7b0a243304 ci: remove sandbox docker build from publish and e2e workflows (#275) 2026-03-13 00:56:07 -07:00
John T. Myers 454327d890 feat(policy): add policy recommendation plumbing (#204) (#222)
* feat(policy): add policy recommendation plumbing — denial aggregation, transport, approval pipeline, and mechanistic recommendations

Implement the infrastructure layer for automated policy recommendations (#204):

- Proto: 9 new RPCs and messages for draft policy lifecycle (submit, get, approve, reject, approve-all, edit, undo, clear, history)
- Persistence: SQLite/Postgres migrations and store methods for draft_policy_chunks and denial_summaries tables
- Server: Full gRPC handler implementations with mechanistic mapper that auto-generates NetworkPolicyRule proposals from denial summaries
- Sandbox: DenialAggregator with MPSC channel, deduplication, periodic flush to gateway via SubmitPolicyAnalysis
- CLI: 'openshell draft' subcommand with get/approve/reject/approve-all/undo/clear/history operations
- TUI: Draft recommendations panel accessible from sandbox policy view
- Docs: Architecture documentation in architecture/policy-advisor.md

* feat(policy): add L7-aware mechanistic mapper and policy advisor CTF example

Add L7 rule generation to mechanistic mapper (build_l7_rules,
generalise_path, looks_like_id) with 3 new unit tests. Add
examples/policy-advisor/ with a 7-gate CTF script, restrictive
sandbox policy, and walkthrough README.

* fix(policy): use sandbox name for denial flush and add TUI draft badges

Fix denial aggregator passing sandbox UUID instead of name to
SubmitPolicyAnalysis, which caused 'sandbox not found' errors on
flush. Add notification badges to the TUI sandbox list and detail
header showing pending draft recommendation counts.

* fix(policy): deduplicate draft chunks and tolerate overlapping OPA rules

Skip draft chunk creation when a pending/approved chunk already covers
the same host:port endpoint, preventing duplicate rules across denial
aggregator flush cycles.

Rewrite three OPA complete rules (network_policy_for_request,
matched_network_policy, matched_endpoint_config) to tolerate multiple
matching policies without triggering a "complete rule conflict" error.
network_policy_for_request becomes a boolean, matched_network_policy
uses a set comprehension with min(), and matched_endpoint_config uses
an array comprehension with index-0 selection.

* feat(tui): interactive draft actions, highlight bar, and detail popup

Rework the draft recommendations panel to match the logs UX:
- Highlight bar (green accent + background) instead of arrow marker
- Viewport-aware j/k scrolling with g/G for top/bottom
- Enter opens a full-screen detail popup showing endpoints, binaries,
  rationale, security notes, and action hints

Add approve/reject/approve-all draft actions:
- [a] approve selected chunk, [x] reject, [A] approve all pending
- Actions work from both the list view and the detail popup
- gRPC calls run async; result updates status bar and refreshes data
- Nav bar shows all available keybindings

Fix draft count refresh: sandbox_draft_counts now refreshes on every
tick (not just Dashboard), so the detail header badge updates in
real time.

Improve badge labels: show 'N pending' instead of a bare number in
both the dashboard sandbox list and sandbox detail header.

* refactor(policy): DB-level draft chunk dedup with hit counter and timestamps

Replace the in-memory HashSet dedup in SubmitPolicyAnalysis with a
database-level upsert. New denormalized columns on draft_policy_chunks:
- host, port: extracted from proposed_rule at insert time
- hit_count: incremented on conflict (same sandbox + host + port)
- first_seen_ms, last_seen_ms: track when the endpoint was first and
  most recently proposed

A partial unique index (WHERE status IN ('pending','approved')) ensures
only one active chunk per endpoint per sandbox; rejected/superseded
chunks don't block new proposals.

Surface hit_count and first/last_seen in:
- CLI: 'openshell draft get' shows 'Hits: N (first ..., last ...)'
- TUI: detail popup shows hits row; list view shows 'Nx' suffix

* fix(policy): optimistic retry on policy version conflicts + structured logging

merge_chunk_into_policy and remove_chunk_from_policy now retry up to 5
times on UNIQUE constraint violations (version conflicts from concurrent
approvals). Each attempt re-reads the latest policy, re-merges the rule,
and increments the version. This eliminates the race condition where
rapid successive approvals would fail with a DB error.

Add structured tracing to all draft action handlers:
- ApproveDraftChunk: logs rule_name, host, port, hit_count before merge
  and version + policy_hash after success
- RejectDraftChunk: logs rule_name, host, port, reason
- ApproveAllDraftChunks: logs pending_count at start, per-chunk merge
  progress, and final summary with chunks_approved/skipped
- UndoDraftChunk: logs before/after with rule_name and version
- Retry attempts log as warnings with attempt number and conflicting
  version

* wip: forward proxy fix, mapper allowed_ips, TUI polish, CTF rewrite

* fix(tui): use correct --gateway flag for ssh-proxy ProxyCommand

* chore: add Docker cleanup script for stale images, volumes, and build cache

* feat(tui): approve-all confirmation modal and CTF cleanup

Add [A] confirmation popup that snapshots pending chunks, shows a
scrollable list, and approves each chunk individually on confirm.
This prevents approving chunks that arrived after the modal opened.

Remove transient issue #205 reference from CTF victory banner.

* fix(tui): correct import ordering for rustfmt

* wip: stateful toggle model, rename to network rules

Draft chunks now follow a toggle state machine:
  pending -> approved | rejected (initial decision)
  approved <-> rejected (toggle)

One row per (sandbox_id, host, port) via expanded unique index.
Rejecting an approved rule removes it from the active policy.
Re-approving a rejected rule merges it back.

Rename CLI from 'draft' to 'rule', TUI from 'Draft Recommendations'
to 'Network Rules'. State-aware keybindings: approved shows [x] Revoke,
rejected shows [a] Approve. Fix sandbox detail hiding delete confirmation
behind pending message.

* refactor(policy): move mapper sandbox-side, slim schema, per-binary granularity

Move mechanistic mapper from gateway to sandbox so all analysis runs
sandbox-side (N sandboxes = N independent pipelines). Gateway is now a
thin validate + persist + approval layer.

Architectural changes:
- Move mechanistic_mapper.rs from navigator-server to navigator-sandbox
- Sandbox flush flow: aggregator drains -> mapper runs -> proposals sent
- Gateway SubmitPolicyAnalysis: validate + persist only, no mapper
- Drop denial_summaries table (write-only, zero readers)
- Consolidate migrations 003+004+005 into single 003

Schema slimming:
- Drop 5 unused columns from draft_policy_chunks (stage, denial_refs,
  supersedes_chunk_id, analysis_mode, decided_by)
- Add per-binary granularity: binary column, widen unique index to
  (sandbox_id, host, port, binary)
- Mapper groups by (host, port, binary), one proposal per triple
- Merge appends binary to existing rule; revoke removes just that binary

CTF & UX:
- 7-gate CTF: add Gate 3 (curl -> ifconfig.me:80) for per-binary demo
- TUI shows binary short name in list, full path in detail popup
- CLI output shows binary field
- Idempotent rule names, hit_count accumulates real denial counts
- Rationale text no longer bakes in stale denial count
2026-03-12 08:28:26 -07:00
Drew Newberry 984d1a6e5c chore: rename project from NemoClaw to OpenShell (#198) 2026-03-10 11:49:09 -07:00
Piotr Mlocek 2a3bc18174 fix(sandbox): treat IPv6 ULA addresses as internal (#173) 2026-03-09 13:38:40 -07:00
John T. MyersandJohn Myers b8d873f68f feat(proxy): support plain HTTP forward proxy for private IP endpoints (#158)
* feat(proxy): support plain HTTP forward proxy for private IP endpoints

Add forward proxy mode to the sandbox proxy so that standard HTTP
libraries (httpx, requests, etc.) work with HTTP_PROXY for plain HTTP
calls to private IP endpoints. Previously, non-CONNECT methods were
unconditionally rejected with 403.

The forward proxy path requires all three conditions to be met:
- OPA policy explicitly allows the destination
- The matched endpoint has allowed_ips configured
- All resolved IPs are RFC 1918 private

This ensures plain HTTP never reaches the public internet while enabling
seamless access to internal services without custom CONNECT tunnel code.

Implementation:
- parse_proxy_uri(): parses absolute-form URIs into components
- rewrite_forward_request(): rewrites to origin-form, strips hop-by-hop
  headers, adds Via and Connection: close
- handle_forward_proxy(): full handler with OPA eval, SSRF checks,
  private-IP gate, upstream connect, and bidirectional relay
- Updated dispatch in handle_tcp_connection to route non-CONNECT methods

Includes 14 unit tests and 6 E2E tests (FWD-1 through FWD-6).
CONNECT path remains completely untouched.

Closes #155

* fix(proxy): remove InspectForInference match arm removed by #146

The inference routing simplification in #146 reduced NetworkAction to
Allow/Deny, removing InspectForInference. Drop the dead match arm from
handle_forward_proxy.

* fix(sandbox): restore BestEffort as default Landlock compatibility

The Landlock V2 upgrade in #151 changed the default from BestEffort to
HardRequirement. This causes all proxy-mode sandboxes to crash with
Permission denied when the policy omits the landlock field, because the
child process gets locked to only /etc/navigator-tls and /sandbox.

Restore BestEffort as the default so policies without an explicit
landlock field degrade gracefully.

Fixes #161

* fix(sandbox): inject baseline filesystem paths for proxy-mode sandboxes

Proxy-mode sandboxes need baseline filesystem paths (/usr, /lib, /etc,
/app, /var/log read-only; /sandbox, /tmp read-write) for the child
process to function under Landlock. Without these, the child can't exec
binaries, resolve DNS, or load shared libraries.

The supervisor now enriches the policy with these baseline paths at
startup, covering both standalone (file) and gateway (gRPC) modes. For
gateway mode, the enriched policy is synced back so users see the
effective policy via 'nemoclaw sandbox get'.

The gateway validation is relaxed to allow additive filesystem changes
(new paths can be added, existing paths cannot be removed) to support
the supervisor's enrichment sync-back.

Includes 2 E2E tests: BFS-1 (missing filesystem_policy) and BFS-2
(incomplete filesystem_policy).

Fixes #161

* fix(e2e): update assertion for relaxed filesystem validation message

---------

Co-authored-by: John Myers <johntmyers@users.noreply.github.com>
2026-03-06 19:31:46 -08:00
Drew Newberry 07b9d5d00e feat(cli): restructure CLI commands for simpler UX (#156) 2026-03-06 16:26:39 -08:00
Piotr Mlocek 31d7ca53ff refactor(inference): simplify routing — introduce inference.local, remove implicit catch-all (#146)
Remove multi-route CRUD system and replace with single managed cluster
route (inference.local). Key changes:

- Remove inference route CRUD RPCs and CLI commands
- Remove InspectForInference OPA action; policy is binary allow/deny
- Introduce AuthHeader enum and InferenceProviderProfile in navigator-core
- Router is now provider-agnostic: auth style carried on ResolvedRoute
- Replace InferenceRouteSpec with ClusterInferenceConfig (2 fields vs 8)
- Rename proto: routing_hint->name, SandboxResolvedRoute->ResolvedRoute,
  GetSandboxInferenceBundle->GetInferenceBundle, drop sandbox_id param
- Rename RouteConfig.route -> RouteConfig.name; use inference.local
- Add 'nemoclaw cluster inference update' for partial config changes
- Delete stale navigator.inference.v1.rs checked-in proto file
- Update architecture docs, agent skills, and CLI reference

Closes #133
2026-03-06 13:54:23 -08:00
John T. MyersandJohn Myers 5e7a2f2dd8 fix(sandbox): verify effective UID/GID after privilege drop (#132)
Closes #32

Add post-condition checks in drop_privileges() to verify that setgid()
and setuid() actually changed the effective IDs. Also verify that
setuid(0) fails after dropping privileges, confirming root cannot be
re-acquired. This is a defense-in-depth hardening measure per CWE-250
and CERT POS37-C. All added syscalls (geteuid, getegid, setuid) are
async-signal-safe, so they are safe in the pre_exec context.

Co-authored-by: John Myers <johntmyers@users.noreply.github.com>
2026-03-05 14:27:48 -08:00
Piotr Mlocek 5befdbf307 fix(sandbox): remove control plane bypass from proxy (#128)
The sandbox proxy had a control_plane_endpoints allowlist that let
sandboxed processes reach the gateway without OPA evaluation or SSRF
checks. This is a security issue: if the sandbox can talk to the
control plane, it could modify its own policy, exfiltrate data through
provider environment endpoints, or abuse other gateway APIs.

All connections now go through OPA policy evaluation and SSRF
protection with no exceptions. Removes AllowedEndpoint, parse_endpoint_url,
the is_control_plane bypass in handle_tcp_connection, and the engine field
from ConnectDecision.
2026-03-05 12:50:26 -08:00
Drew Newberry 99bba8028a feat(sandbox): support policy discovery and restrictive defaults on sandbox containers (#84) 2026-03-04 17:17:22 -08:00
John T. MyersandJohn Myers 1d81fb187b feat(skills): create nemoclaw-cli agent skill (#85)
* feat(skills): create nemoclaw-cli agent skill and fix generate-sandbox-policy schema drift

Closes #83

Create a new nemoclaw-cli agent skill with tiered workflow guidance (basic
through advanced) and a supplementary CLI reference. The skill covers provider
management, sandbox lifecycle, policy iteration loops, BYOC pipelines, and
agent-assisted sandbox sessions. It cross-references generate-sandbox-policy
for policy content authoring and includes --help self-teaching as a fallback.

Fix critical schema drift in generate-sandbox-policy: rename
allowed_routing_hints to allowed_routes (matching the actual proto/serde
field name). Update default read_only paths to match dev-sandbox-policy.yaml.
Fix the same field name error in architecture/security-policy.md.

* policy updates

* fix(test): update OPA tests for renamed github_ssh_over_https policy key

---------

Co-authored-by: John Myers <johntmyers@users.noreply.github.com>
2026-03-04 09:52:55 -08:00
Drew Newberry 9099bc3972 chore: rename Navigator to NemoClaw across user facing contracts (#73) 2026-03-03 11:41:19 -08:00
John T. MyersandJohn Myers ff52643237 feat(sandbox): allow egress to private IP space via allowed_ips policy field (#60)
* feat(sandbox): allow egress to private IP space via allowed_ips policy field

Add an allowed_ips field to NetworkEndpoint that accepts CIDR notation and
exact IPs, enabling sandboxes to reach private IP space under policy control
while maintaining SSRF protections for loopback and link-local addresses.

Three modes are supported:
- Default (no allowed_ips): all private IPs blocked (existing behavior)
- Host + allowlist: domain must resolve to an IP in allowed_ips
- Hostless allowlist (no host): any domain allowed if resolved IP matches



Co-authored-by: John Myers <johntmyers@users.noreply.github.com>
2026-03-02 19:29:34 -08:00
John Myers 757217f4bd feat(sandbox): support live policy updates, history, and policy-aware logs (!55)
Closes #78

## What You Can Do

### Live policy updates
Update a running sandbox's network policy without recreating it:
```bash
nav sandbox policy set <sandbox> --policy new-policy.yaml --wait --timeout 60
```
The sandbox hot-reloads the policy within 30s (configurable). On failure, the previous policy stays active (last-known-good).

Idempotent — submitting the same policy twice returns the existing version:
```
✓ Policy version 3 submitted (hash: a1b2c3d4e5f6)
$ nav sandbox policy set test --policy same.yaml
· Policy unchanged (version 3, hash: a1b2c3d4e5f6)
```

### Policy history & inspection
```bash
nav sandbox policy list <sandbox>          # version history with status
nav sandbox policy get <sandbox>           # current policy metadata
nav sandbox policy get <sandbox> --full    # print full policy as YAML
nav sandbox policy get <sandbox> --rev 2 --full  # specific revision as YAML
```

### Sandbox logs
Stream logs from both the gateway and sandbox supervisor in one view:
```bash
nav sandbox logs <sandbox>                      # one-shot, last 2000 lines
nav sandbox logs <sandbox> --tail               # live streaming
nav sandbox logs <sandbox> --source sandbox     # supervisor logs only
nav sandbox logs <sandbox> --source gateway     # gateway logs only
nav sandbox logs <sandbox> --level warn         # warnings and errors only
nav sandbox logs <sandbox> --since 5m           # last 5 minutes
nav sandbox logs <sandbox> --tail --source sandbox --level info
```

Each log line is tagged with its source and includes structured fields:
```
[1772055394.673] [sandbox] [INFO ] [navigator_sandbox::proxy] CONNECT action=allow dst_host=api.anthropic.com dst_port=443 policy=claude_code
[1772055061.005] [gateway] [INFO ] [navigator_server::grpc] GetSandboxPolicy served from policy history
```

---

## Implementation

### Proto changes
- 7 new RPCs: `UpdateSandboxPolicy`, `GetSandboxPolicyStatus`, `ListSandboxPolicies`, `ReportPolicyStatus`, `GetSandboxLogs`, `PushSandboxLogs` (client-streaming)
- `SandboxLogLine`: added `source` (gateway/sandbox), `fields` (structured key-value map)
- `WatchSandboxRequest`: added `log_since_ms`, `log_sources`, `log_min_level`
- `GetSandboxLogsRequest`: added `sources`, `min_level`
- `PolicyStatus` enum, `SandboxPolicyRevision` message
- `Sandbox.current_policy_version` field

### Server
- **Policy persistence**: New `sandbox_policies` table (SQLite + Postgres) with per-sandbox monotonic versions, status tracking, and policy hash
- **UpdateSandboxPolicy**: Validates static field immutability (filesystem/landlock/process), network mode consistency (Block↔Proxy), deterministic hash comparison for idempotent updates
- **Lazy backfill**: First `GetSandboxPolicy` call creates version 1 from `spec.policy` for existing sandboxes
- **Log broker**: `TracingLogBus::publish_external()` injects sandbox-pushed logs into the same broadcast channel + tail buffer (2000 lines). Server forces `source="sandbox"` and `sandbox_id` on all pushed logs
- **Source/level filtering**: Applied server-side in both `GetSandboxLogs` and `WatchSandbox` streams
- **Version supersession**: When a new version is loaded, all older pending+loaded versions are marked superseded

### Sandbox
- **`OpaEngine::reload_from_proto()`**: Full `from_proto()` pipeline (L7 validation, access preset expansion) with atomic engine swap. On failure, previous engine untouched (LKG)
- **Policy poll loop**: Background task polls every 30s (configurable via `NAVIGATOR_POLICY_POLL_INTERVAL_SECS`), reports status via `ReportPolicyStatus` RPC
- **`LogPushLayer`**: Tracing layer captures events at INFO+ (configurable via `NAVIGATOR_LOG_PUSH_LEVEL`), sends structured fields via `PushSandboxLogs` client-streaming RPC. Background task batches 50 lines / flushes every 500ms. Best-effort (drops on full channel, never blocks)
- **`CachedNavigatorClient`**: Persistent mTLS channel for both policy polling and log push

### Database migration
- `002_create_sandbox_policies.sql` (SQLite + Postgres)

## Tests
- **Unit**: 8 policy persistence tests (put/get/list/status/supersede/isolation)
- **Integration**: 4 test files updated with new RPC stubs
- **E2E**: `test_live_policy_update_and_logs` — full lifecycle: create → set same (unchanged) → push new → wait for load → verify connectivity → push same (unchanged) → verify history → fetch logs

## Documentation
- `architecture/sandbox.md`: Log streaming architecture, LogPushLayer, push task, server broker, source tagging, structured fields, CLI filtering, failure modes
- `architecture/security-policy.md`: Live update semantics, deterministic hashing, CLI filter flags, policy inspection
- `architecture/plans/issue-78-sandbox-log-streaming.md`: Design plan for log streaming

## Security
- Trust boundary documented: shared mTLS cert model (per-sandbox auth tracked in #80)
- Server forces `source="sandbox"` and `sandbox_id` on pushed logs (can't impersonate gateway or other sandboxes)
- Per-batch line cap (100) prevents flooding
2026-02-25 17:50:41 -08:00
Drew Newberry 2d85338940 feat(platform): cleanup api surface area and mtls flows (!39)
Closes #48, #52

## Summary
- Replace the envoy-gateway-based TLS setup with inline PKI generation during cluster bootstrap, generating CA, server, and client certificates directly in the `navigator-bootstrap` crate
- Remove all envoy gateway Helm templates (`gateway.yaml`, `gatewayclass.yaml`, `grpcroute.yaml`, PKI job, traffic policies) and the `Dockerfile.pki-job`
- Add native mTLS support to the navigator server with `tokio-rustls`, mounting client TLS certs as volumes into sandbox pods
- Update cluster entrypoint, healthcheck, and deploy scripts to work with the new direct-TLS architecture
- Add TLS security e2e test and fix formatting/clippy warnings

## Test Plan
- All unit tests pass (`cargo test --workspace`)
- Clippy clean (`cargo clippy --workspace --all-targets`)
- Format clean (`cargo fmt --all -- --check`)
- Python tests pass (`uv run pytest python/`)
- Full `mise run pre-commit` passes
2026-02-24 11:33:33 -08:00
Drew Newberry c011a1cfb4 chore: changes for intial openclaw demo 2026-02-23 22:56:27 -08:00
John Myers 82e6b81324 fix(security): reject CONNECT to internal IPs (SSRF defense-in-depth) (!37)
> **🔧 security-fix-agent**

Closes #62

## Security Fix

### Summary
The CONNECT proxy accepted hostnames from clients and connected to whatever IP they resolved to, with no validation against internal address ranges. While the OPA policy is default-deny, a misconfigured or overly permissive policy could allow SSRF to cloud metadata (169.254.169.254), localhost, or RFC1918 services. This fix adds DNS resolution before connecting and rejects any host that resolves to an internal IP.

### Severity Assessment
- **Impact:** Medium — if exploited, could reach cloud metadata (IAM creds), cluster-internal services, or host-local services
- **Exploitability:** Very low — requires OPA policy misconfiguration or DNS rebinding attack
- **Affected components:** `crates/navigator-sandbox/src/proxy.rs` — `handle_tcp_connection`

### Changes Made
- `crates/navigator-sandbox/src/proxy.rs`: Added `is_internal_ip()` helper that checks IPv4 loopback/private/link-local, IPv6 loopback/link-local, and IPv4-mapped IPv6. Added `resolve_and_reject_internal()` that resolves DNS and rejects internal IPs. Inserted check between OPA allow and `TcpStream::connect`, with control plane endpoints exempt.
- `architecture/security-policy.md`: Added SSRF Protection section with blocked ranges table and flow diagram
- `architecture/sandbox.md`: Updated proxy connection flow diagram and added SSRF protection subsection
- `architecture/README.md`: Added internal IP rejection step to proxy description

### Tests Added
- **Unit:** 17 tests in `proxy::tests` — covers IPv4 loopback/private/link-local, IPv6 loopback/link-local, IPv4-mapped IPv6, public IPs, DNS resolution of localhost/127.0.0.1/169.254.169.254, and DNS failure handling
- **Integration/E2E:** N/A — the proxy runs inside a Linux network namespace; unit tests for IP checking and DNS resolution cover the security boundary

### Documentation Updated
- `architecture/security-policy.md`: New SSRF Protection section with blocked IP ranges and Mermaid flowchart
- `architecture/sandbox.md`: Updated proxy flow diagram and added SSRF protection subsection
- `architecture/README.md`: Added step 4 to proxy description

### Verification
All 85 sandbox tests pass including 17 new proxy SSRF tests. Pre-commit (fmt, clippy, full test suite) passes clean with zero warnings.
2026-02-20 10:17:46 -08:00
Drew Newberry 98c0e08423 feat(sandbox): add --policy flag for custom sandbox policy and allow /dev/null in filesystem policy
Add a --policy CLI flag to sandbox create that overrides the built-in
default and NAVIGATOR_SANDBOX_POLICY env var for specifying a custom
sandbox policy YAML file. Also add /dev/null to the default filesystem
read_write list and update security-policy docs with formatting fixes.
2026-02-19 23:12:32 -08:00
John Myers ee989e8c80 Docs(organize docs add sub agents) (!35)
Add two sub-agents that have been useful thus far. The documentation agent has memory that is tracked.

Used the docs sub-agent to create detailed docs for all sub-systems + and overview doc.
2026-02-19 18:04:36 -08:00