diff --git a/.github/workflows/branch-checks.yml b/.github/workflows/branch-checks.yml index 3d31f6e9f..0d05c04ea 100644 --- a/.github/workflows/branch-checks.yml +++ b/.github/workflows/branch-checks.yml @@ -100,3 +100,20 @@ jobs: - name: Test run: mise run test:python + + markdown: + name: Markdown + runs-on: build-amd64 + container: + image: ghcr.io/nvidia/openshell/ci:latest + credentials: + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@v4 + + - name: Install tools + run: mise install + + - name: Lint + run: mise run markdown:lint diff --git a/.gitignore b/.gitignore index d6b4fa356..08cb97f60 100644 --- a/.gitignore +++ b/.gitignore @@ -202,3 +202,6 @@ architecture/plans rfc.md .worktrees .z3-trace + +# Markdown/mermaid lint tooling deps +scripts/lint-mermaid/node_modules/ diff --git a/.markdownlint-cli2.jsonc b/.markdownlint-cli2.jsonc new file mode 100644 index 000000000..47b0a6ae6 --- /dev/null +++ b/.markdownlint-cli2.jsonc @@ -0,0 +1,31 @@ +{ + "globs": [ + "**/*.md", + "**/*.mdx" + ], + "gitignore": true, + "ignores": [ + ".agents/**", + ".claude/**", + ".opencode/**", + ".github/**", + "THIRD-PARTY-NOTICES/**", + "CLAUDE.md" + ], + "config": { + "default": true, + // Allow long lines — prose paragraphs are single-line per project style. + "MD013": false, + // Allow GitHub-rendered HTML commonly used in READMEs (centered logos, + // collapsible sections, keyboard hints). Regular prose HTML still flagged. + "MD033": { "allowed_elements": ["p", "img", "br", "a", "div", "details", "summary", "kbd", "sub", "sup"] }, + // Allow duplicate headings in different sections. + "MD024": { "siblings_only": true }, + // Bare URLs are fine in changelogs and tables. + "MD034": false, + // First line does not need to be a heading. + "MD002": false, + // Repo uses padded table pipes (`| foo | bar |`); rule default is "compact". + "MD060": { "style": "padded" } + } +} diff --git a/CLAUDE.md b/CLAUDE.md index eef4bd20c..43c994c2d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1 +1 @@ -@AGENTS.md \ No newline at end of file +@AGENTS.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2852bfa43..c83d32efb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -258,7 +258,7 @@ See [docs/CONTRIBUTING.mdx](docs/CONTRIBUTING.mdx) for the current docs authorin This project uses [Conventional Commits](https://www.conventionalcommits.org/). All commit messages must follow the format: -``` +```text (): [optional body] @@ -279,7 +279,7 @@ This project uses [Conventional Commits](https://www.conventionalcommits.org/). **Examples:** -``` +```text feat(cli): add --verbose flag to openshell run fix(sandbox): handle timeout errors gracefully docs: update installation instructions diff --git a/SECURITY.md b/SECURITY.md index 728cdb4a1..9000efe98 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,4 +1,4 @@ -## Security +# Security NVIDIA is dedicated to the security and trust of our software products and services, including all source code repositories managed through our organization. diff --git a/TESTING.md b/TESTING.md index eba31967e..eab16603b 100644 --- a/TESTING.md +++ b/TESTING.md @@ -10,7 +10,7 @@ mise run ci # Everything: lint, compile checks, and tests ## Test Layout -``` +```text crates/*/src/ # Inline #[cfg(test)] modules crates/*/tests/ # Rust integration tests python/openshell/ # Python unit tests (*_test.py suffix) diff --git a/architecture/README.md b/architecture/README.md index 570fce660..36b0a4978 100644 --- a/architecture/README.md +++ b/architecture/README.md @@ -142,6 +142,7 @@ The connection flow works as follows: 5. The CLI and sandbox exchange SSH traffic bidirectionally through the tunnel. This design provides several benefits: + - Sandbox pods are never directly accessible from outside the cluster. - All access is authenticated and auditable through the gateway. - Session tokens can be revoked to immediately cut off access. @@ -198,7 +199,6 @@ The inference routing system transparently intercepts AI inference API calls fro | Gateway inference service | `crates/openshell-server/src/inference.rs` | Stores cluster inference config, resolves bundles with credentials from provider records | | Proto definitions | `proto/inference.proto` | `ClusterInferenceConfig`, `ResolvedRoute`, bundle RPCs | - ### Container and Build System The platform produces three container images: diff --git a/architecture/custom-vm-runtime.md b/architecture/custom-vm-runtime.md index 4cafe424f..045ee2e9a 100644 --- a/architecture/custom-vm-runtime.md +++ b/architecture/custom-vm-runtime.md @@ -61,7 +61,7 @@ never binds a host-side TCP listener. `openshell-driver-vm` embeds the VM runtime libraries and the sandbox rootfs as zstd-compressed byte arrays, extracting on demand: -``` +```text ~/.local/share/openshell/vm-runtime// # libkrun / libkrunfw / gvproxy ├── libkrun.{dylib,so} ├── libkrunfw.{5.dylib,so.5} diff --git a/architecture/gateway-deploy-connect.md b/architecture/gateway-deploy-connect.md index ec8153302..14bb3e90f 100644 --- a/architecture/gateway-deploy-connect.md +++ b/architecture/gateway-deploy-connect.md @@ -99,7 +99,7 @@ This stores `auth_mode = "plaintext"`, skips mTLS certificate extraction, and by All connection artifacts are stored under `$XDG_CONFIG_HOME/openshell/` (default `~/.config/openshell/`): -``` +```text openshell/ active_gateway # plain text: active gateway name gateways/ diff --git a/architecture/gateway-security.md b/architecture/gateway-security.md index 6baaee88b..a32c3fb52 100644 --- a/architecture/gateway-security.md +++ b/architecture/gateway-security.md @@ -51,7 +51,7 @@ graph TD The PKI is a single-tier CA hierarchy generated by the `openshell-bootstrap` crate using `rcgen`. All certificates are created in a single pass at cluster bootstrap time. -``` +```text openshell-ca (Self-signed Root CA, O=openshell, CN=openshell-ca) ├── openshell-server (Leaf cert, CN=openshell-server) │ SANs: openshell, openshell.openshell.svc, @@ -94,7 +94,7 @@ The Helm StatefulSet (`deploy/helm/openshell/templates/statefulset.yaml`) mounts Environment variables point the gateway binary to these paths: -``` +```text OPENSHELL_TLS_CERT=/etc/openshell-tls/server/tls.crt OPENSHELL_TLS_KEY=/etc/openshell-tls/server/tls.key OPENSHELL_TLS_CLIENT_CA=/etc/openshell-tls/client-ca/ca.crt @@ -108,7 +108,7 @@ When the gateway creates a sandbox pod (`crates/openshell-server/src/sandbox/mod - A read-only mount at `/etc/openshell-tls/client/` on the agent container. - Environment variables for the sandbox gRPC client: -``` +```text OPENSHELL_TLS_CA=/etc/openshell-tls/client/ca.crt OPENSHELL_TLS_CERT=/etc/openshell-tls/client/tls.crt OPENSHELL_TLS_KEY=/etc/openshell-tls/client/tls.key @@ -119,7 +119,7 @@ OPENSHELL_ENDPOINT=https://openshell.openshell.svc.cluster.local:8080 The CLI's copy of the client certificate bundle is written to: -``` +```text $XDG_CONFIG_HOME/openshell/gateways//mtls/ ├── ca.crt ├── tls.crt @@ -183,7 +183,7 @@ The gateway supports three transport modes: ### Connection Flow -``` +```text TCP accept → TLS handshake (mandatory client cert in mTLS mode, optional in dual-auth mode) → hyper auto-negotiates HTTP/1.1 or HTTP/2 via ALPN @@ -225,10 +225,12 @@ Sandbox pods connect back to the gateway at startup to fetch their policy and pr | `OPENSHELL_TLS_KEY` | `/etc/openshell-tls/client/tls.key` | These are used to build a `tonic::transport::ClientTlsConfig` with: + - `ca_certificate()` -- verifies the server's certificate against the cluster CA. - `identity()` -- presents the shared client certificate for mTLS. The sandbox calls two RPCs over this authenticated channel: + - `GetSandboxSettings` -- fetches the YAML policy that governs the sandbox's behavior. - `GetSandboxProviderEnvironment` -- fetches provider credentials as environment variables. diff --git a/architecture/gateway-settings.md b/architecture/gateway-settings.md index ef9538f5b..4ae191de0 100644 --- a/architecture/gateway-settings.md +++ b/architecture/gateway-settings.md @@ -45,6 +45,7 @@ pub const REGISTERED_SETTINGS: &[RegisteredSetting] = &[ The reserved key `policy` is excluded from the registry. It is handled by dedicated policy commands and stored as a hex-encoded protobuf `SandboxPolicy` in the global settings' `Bytes` variant. Attempts to set or delete the `policy` key through settings commands are rejected. Helper functions: + - `setting_for_key(key)` -- look up a `RegisteredSetting` by name, returns `None` for unknown keys - `registered_keys_csv()` -- comma-separated list of valid keys for error messages - `parse_bool_like(raw)` -- flexible bool parsing from CLI string input @@ -83,6 +84,7 @@ The `UpdateSettings` RPC multiplexes policy and setting mutations through a sing | `global` | `bool` | Target gateway-global scope instead of sandbox scope | Validation rules: + - `policy` and `setting_key` cannot both be present - At least one of `policy` or `setting_key` must be present - `delete_setting` cannot be combined with a `policy` payload @@ -266,7 +268,7 @@ This prevents conflicting values at different scopes. An operator must delete a When a global policy is set, sandbox-scoped policy updates via `UpdateSettings` are rejected with `FailedPrecondition`: -``` +```text policy is managed globally; delete global policy before sandbox policy update ``` @@ -442,6 +444,7 @@ openshell policy get --global --full All `--global` mutations require human-in-the-loop confirmation via an interactive prompt. The `--yes` flag bypasses the prompt for scripted/CI usage. In non-interactive mode (no TTY), `--yes` is required -- otherwise the command fails with an error. The confirmation message varies: + - **Global setting set**: warns that this will override sandbox-level values for the key - **Global setting delete**: warns that this re-enables sandbox-level management - **Global policy set**: warns that this overrides all sandbox policies diff --git a/architecture/gateway-single-node.md b/architecture/gateway-single-node.md index 6389c728e..01b69b2f5 100644 --- a/architecture/gateway-single-node.md +++ b/architecture/gateway-single-node.md @@ -168,7 +168,7 @@ For the target daemon (local or remote): - k3s server command: `server --disable=traefik --tls-san=127.0.0.1 --tls-san=localhost --tls-san=host.docker.internal` plus computed extra SANs. - Privileged mode. - Volume bind mount: `openshell-cluster-{name}:/var/lib/rancher/k3s`. - - Network: `openshell-cluster-{name}` (per-gateway bridge network). + - Network: `openshell-cluster-{name}` (per-gateway bridge network). - Extra host: `host.docker.internal:host-gateway`. - The cluster entrypoint prefers the resolved IPv4 for `host.docker.internal` when populating sandbox pod `hostAliases`, then falls back to the container default gateway. This keeps sandbox host aliases working on Docker Desktop, where the host-reachable IP differs from the bridge gateway. - Port mappings: @@ -230,7 +230,7 @@ After deploy, the CLI calls `save_active_gateway(name)`, writing the gateway nam The cluster image is defined by target `cluster` in `deploy/docker/Dockerfile.images`: -``` +```text Base: rancher/k3s:v1.35.2-k3s1 ``` @@ -242,6 +242,7 @@ Layers added: 4. Kubernetes manifests: `deploy/kube/manifests/*.yaml` -> `/opt/openshell/manifests/` Bundled manifests include: + - `openshell-helmchart.yaml` (OpenShell Helm chart auto-deploy) - `envoy-gateway-helmchart.yaml` (Envoy Gateway for Gateway API) - `agent-sandbox.yaml` @@ -363,9 +364,10 @@ flowchart LR 4. Force-remove the per-gateway network via `force_remove_network()`, disconnecting any stale endpoints first. **CLI layer** (`gateway_destroy()` in `run.rs` additionally): - -6. Remove the metadata JSON file via `remove_gateway_metadata()`. -7. Clear the active gateway reference if it matches the destroyed gateway. + +5. Remove the metadata JSON file via `remove_gateway_metadata()`. +6. Clear the active gateway reference if it matches the destroyed gateway. + ## Idempotency and Error Behavior @@ -378,8 +380,8 @@ flowchart LR - Docker API failures from inspect/create/start/remove. - SSH connection failures when creating the remote Docker client. - Health check timeout (6 min) with recent container logs. - - Container exit during any polling phase (health, mTLS) with diagnostic information (exit code, OOM status, recent logs). - - mTLS secret polling timeout (3 min). + - Container exit during any polling phase (health, mTLS) with diagnostic information (exit code, OOM status, recent logs). + - mTLS secret polling timeout (3 min). - Local image ref without registry prefix: clear error with build instructions rather than a failed Docker Hub pull. ## Auto-Bootstrap from `sandbox create` @@ -436,7 +438,7 @@ Environment variables that affect bootstrap behavior when set on the host: Artifacts stored under `$XDG_CONFIG_HOME/openshell/` (default `~/.config/openshell/`): -``` +```text openshell/ active_gateway # plain text: active gateway name gateways/ diff --git a/architecture/gateway.md b/architecture/gateway.md index 9e9da6785..5fb82717a 100644 --- a/architecture/gateway.md +++ b/architecture/gateway.md @@ -510,6 +510,7 @@ graph LR ``` All buses use `tokio::sync::broadcast` channels keyed by sandbox ID. Buffer sizes: + - `SandboxWatchBus`: 128 (signals only, no payload -- just `()`) - `TracingLogBus`: 1024 (full `SandboxStreamEvent` payloads) - `PlatformEventBus`: 1024 (full `SandboxStreamEvent` payloads) diff --git a/architecture/inference-routing.md b/architecture/inference-routing.md index c0c42f4f6..4d7b0f517 100644 --- a/architecture/inference-routing.md +++ b/architecture/inference-routing.md @@ -214,7 +214,7 @@ This eliminates full-body buffering for streaming responses (SSE). Time-to-first When the proxy truncates a streaming response, it injects an SSE error event via `format_sse_error()` (in `crates/openshell-sandbox/src/l7/inference.rs`) before sending the HTTP chunked terminator: -``` +```text data: {"error":{"message":"","type":"proxy_stream_error"}} ``` diff --git a/architecture/policy-advisor.md b/architecture/policy-advisor.md index 6d2728332..c70bfcbd3 100644 --- a/architecture/policy-advisor.md +++ b/architecture/policy-advisor.md @@ -208,6 +208,7 @@ The TUI sandbox screen includes a "Network Rules" panel accessible via `[r]` fro - Expanded detail popup with full binary path, rationale, security notes, and proposed rule Keybindings are state-aware: + - **Pending** → `[a]` approve, `[x]` reject, `[A]` approve all - **Approved** → `[x]` revoke - **Rejected** → `[a]` approve diff --git a/architecture/sandbox-connect.md b/architecture/sandbox-connect.md index 88505c7f1..a86777f6f 100644 --- a/architecture/sandbox-connect.md +++ b/architecture/sandbox-connect.md @@ -178,7 +178,7 @@ sequenceDiagram CLI->>GW: CreateSshSession(sandbox_id) GW-->>CLI: token, gateway_host, gateway_port, scheme, connect_path - Note over CLI: Builds ProxyCommand string; exec()s ssh + Note over CLI: Builds ProxyCommand string: exec()s ssh User->>CLI: ssh spawns ssh-proxy subprocess CLI->>GW: CONNECT /connect/ssh
X-Sandbox-Id, X-Sandbox-Token diff --git a/architecture/sandbox-providers.md b/architecture/sandbox-providers.md index fe5d48a97..088bd7592 100644 --- a/architecture/sandbox-providers.md +++ b/architecture/sandbox-providers.md @@ -333,7 +333,7 @@ the full implementation details, encoding rules, and security properties. ### End-to-End Flow -``` +```text CLI: openshell sandbox create -- claude | +-- detect_provider_from_command(["claude"]) -> "claude" diff --git a/architecture/sandbox.md b/architecture/sandbox.md index 5104a6dcc..2c0b5fe1b 100644 --- a/architecture/sandbox.md +++ b/architecture/sandbox.md @@ -116,18 +116,18 @@ flowchart TD 8. **SSH server** (optional): If `--ssh-socket-path` is provided, spawn an async task running `ssh::run_ssh_server()` with the policy, workdir, netns FD, proxy URL, CA paths, and provider env. The value is a filesystem path to the Unix socket the embedded sshd binds. The supervisor waits on a readiness `oneshot` channel before proceeding so that exec requests arriving immediately after pod-ready cannot race against socket bind. -8a. **Supervisor session** (gRPC mode + SSH socket only): If `--sandbox-id`, `--openshell-endpoint`, and an SSH socket path are all set, spawn `supervisor_session::spawn()`. This task opens a persistent outbound bidirectional gRPC stream to the gateway and bridges inbound relay requests to the local SSH daemon. See [Supervisor Session](#supervisor-session) for the full protocol. +9. **Supervisor session** (gRPC mode + SSH socket only): If `--sandbox-id`, `--openshell-endpoint`, and an SSH socket path are all set, spawn `supervisor_session::spawn()`. This task opens a persistent outbound bidirectional gRPC stream to the gateway and bridges inbound relay requests to the local SSH daemon. See [Supervisor Session](#supervisor-session) for the full protocol. -9. **Child process spawning** (`ProcessHandle::spawn()`): - - Build `tokio::process::Command` with inherited stdio and `kill_on_drop(true)` - - Set environment variables: `OPENSHELL_SANDBOX=1`, provider credentials, proxy URLs, TLS trust store paths - - Pre-exec closure (async-signal-safe): `setpgid` (if non-interactive) -> `setns` (enter netns) -> `drop_privileges` -> `sandbox::apply` (Landlock + seccomp) +10. **Child process spawning** (`ProcessHandle::spawn()`): + - Build `tokio::process::Command` with inherited stdio and `kill_on_drop(true)` + - Set environment variables: `OPENSHELL_SANDBOX=1`, provider credentials, proxy URLs, TLS trust store paths + - Pre-exec closure (async-signal-safe): `setpgid` (if non-interactive) -> `setns` (enter netns) -> `drop_privileges` -> `sandbox::apply` (Landlock + seccomp) -10. **Store entrypoint PID**: `entrypoint_pid.store(pid, Ordering::Release)` so the proxy can resolve TCP peer identity via `/proc`. +11. **Store entrypoint PID**: `entrypoint_pid.store(pid, Ordering::Release)` so the proxy can resolve TCP peer identity via `/proc`. -11. **Spawn policy poll task** (gRPC mode only): If `sandbox_id`, `openshell_endpoint`, and an OPA engine are all present, spawn `run_policy_poll_loop()` as a background tokio task. This task polls the gateway for policy updates and hot-reloads the OPA engine when a new version is detected. See [Policy Reload Lifecycle](#policy-reload-lifecycle) for details. +12. **Spawn policy poll task** (gRPC mode only): If `sandbox_id`, `openshell_endpoint`, and an OPA engine are all present, spawn `run_policy_poll_loop()` as a background tokio task. This task polls the gateway for policy updates and hot-reloads the OPA engine when a new version is detected. See [Policy Reload Lifecycle](#policy-reload-lifecycle) for details. -12. **Wait with timeout**: If `--timeout > 0`, wrap `handle.wait()` in `tokio::time::timeout()`. On timeout, kill the process and return exit code 124. +13. **Wait with timeout**: If `--timeout > 0`, wrap `handle.wait()` in `tokio::time::timeout()`. On timeout, kill the process and return exit code 124. ## Policy Model @@ -244,6 +244,7 @@ Two evaluation methods exist: `evaluate_network()` for the legacy bool-based pat #### `evaluate_network(input: &NetworkInput) -> Result` Input JSON shape: + ```json { "exec": { @@ -259,6 +260,7 @@ Input JSON shape: ``` Evaluates three Rego rules: + 1. `data.openshell.sandbox.allow_network` -> bool 2. `data.openshell.sandbox.deny_reason` -> string 3. `data.openshell.sandbox.matched_network_policy` -> string (or `Undefined`) @@ -273,6 +275,7 @@ Uses the same input JSON shape as `evaluate_network()`. Evaluates the `data.open - `"deny"` -- network connections not allowed by policy The Rego logic: + 1. If `network_policy_for_request` exists (endpoint + binary match), return `"allow"` 2. Default: `"deny"` @@ -394,6 +397,7 @@ pub struct SettingsPollResult { ``` Methods: + - **`connect(endpoint)`**: Establish an mTLS channel and return a new client. - **`poll_settings(sandbox_id)`**: Call `GetSandboxSettings` RPC and return a `SettingsPollResult` containing policy payload (optional), policy metadata, effective config revision, policy source, global policy version, and the effective settings map (for diff logging). - **`report_policy_status(sandbox_id, version, loaded, error_msg)`**: Call `ReportPolicyStatus` RPC with the appropriate `PolicyStatus` enum value (`Loaded` or `Failed`). @@ -404,6 +408,7 @@ Methods: The gateway assigns a monotonically increasing version number to each sandbox policy revision. `GetSandboxSettingsResponse` carries the full effective configuration: policy payload, effective settings map (with per-key scope indicators), a `config_revision` fingerprint that changes when any effective input changes (policy, settings, or source), and a `policy_source` field indicating whether the policy came from the sandbox's own history or from a global override. Proto messages involved: + - `GetSandboxSettingsResponse` (`proto/sandbox.proto`): `policy`, `version`, `policy_hash`, `settings` (map of `EffectiveSetting`), `config_revision`, `policy_source`, `global_policy_version` - `EffectiveSetting` (`proto/sandbox.proto`): `SettingValue value`, `SettingScope scope` - `SettingScope` enum: `UNSPECIFIED`, `SANDBOX`, `GLOBAL` @@ -449,6 +454,7 @@ Landlock restricts the child process's filesystem access to an explicit allowlis 7. Call `ruleset.restrict_self()` -- this applies to the calling process and all descendants Kernel-level error behavior (e.g., Landlock ABI unavailable) depends on `LandlockCompatibility`: + - `BestEffort`: Log a warning and continue without filesystem isolation - `HardRequirement`: Return a fatal error, aborting the sandbox @@ -463,6 +469,7 @@ Seccomp provides three layers of syscall restriction: socket domain blocks, unco **Skipped entirely** in `Allow` mode. Setup: + 1. `prctl(PR_SET_NO_NEW_PRIVS, 1)` -- required before seccomp 2. `seccompiler::apply_filter()` with default action `Allow` and per-rule action `Errno(EPERM)` @@ -512,7 +519,7 @@ The network namespace creates an isolated network stack where the sandboxed proc #### Topology -``` +```text HOST NAMESPACE SANDBOX NAMESPACE ----------------- ----------------- veth-h-{uuid} veth-s-{uuid} @@ -540,6 +547,7 @@ Each step has rollback on failure -- if any `ip` command fails, previously creat #### Cleanup on drop `NetworkNamespace` implements `Drop`: + 1. Close the namespace FD 2. Delete the host-side veth (`ip link delete veth-h-{id}`) -- this automatically removes the peer 3. Delete the namespace (`ip netns delete sandbox-{id}`) @@ -903,6 +911,7 @@ The sandbox is designed to operate both as part of a cluster and as a standalone #### Cluster mode graceful degradation In cluster mode, `fetch_inference_bundle()` failures are handled based on the error type: + - gRPC `PermissionDenied` or `NotFound` (detected via error message string matching): sandbox has no inference policy -- inference routing is silently disabled. - Other errors: logged as a warning, inference routing is disabled. - Empty initial route bundle: inference routing stays enabled with an empty cache and background refresh continues. @@ -1029,12 +1038,14 @@ Expansion happens in `expand_access_presets()` before the Rego engine loads the `validate_l7_policies()` runs at engine load time and returns `(errors, warnings)`: **Errors** (block startup): + - `rules` and `access` both specified on same endpoint - `protocol` specified without `rules` or `access` - `protocol: sql` with `enforcement: enforce` (SQL parsing not available in v1) - Empty `rules` array (would deny all traffic) **Warnings** (logged): + - `tls: terminate` or `tls: passthrough` on any endpoint (deprecated — TLS termination is now automatic; use `tls: skip` to disable) - `tls: skip` with L7 rules on port 443 (L7 inspection cannot work on encrypted traffic) - Unknown HTTP method in rules @@ -1046,23 +1057,27 @@ Expansion happens in `expand_access_presets()` before the Rego engine loads the TLS termination is automatic. The proxy peeks the first bytes of every CONNECT tunnel and terminates TLS whenever a ClientHello is detected. This enables credential injection and L7 inspection on all HTTPS endpoints without requiring explicit `tls: terminate` in the policy. The `tls` field defaults to `Auto`; use `tls: skip` to opt out entirely (e.g., for client-cert mTLS to upstream). **Ephemeral CA lifecycle:** + 1. At sandbox startup, `SandboxCa::generate()` creates a self-signed CA (CN: "OpenShell Sandbox CA") using `rcgen` 2. The CA cert PEM and a combined bundle (system CAs + sandbox CA) are written to `/etc/openshell-tls/` 3. The sandbox CA cert path is set as `NODE_EXTRA_CA_CERTS` (additive for Node.js) 4. The combined bundle is set as `SSL_CERT_FILE`, `REQUESTS_CA_BUNDLE`, `CURL_CA_BUNDLE` (replaces defaults for OpenSSL, Python requests, curl) **TLS auto-detection** (`looks_like_tls()`): + - Peeks up to 8 bytes from the client stream - Checks for TLS ClientHello pattern: byte 0 = `0x16` (ContentType::Handshake), byte 1 = `0x03` (TLS major version), byte 2 ≤ `0x04` (minor version, covering SSL 3.0 through TLS 1.3) - Returns `false` for plaintext HTTP, SSH, or other binary protocols **Per-hostname leaf cert generation:** + - `CertCache` maps hostnames to `CertifiedLeaf` structs (cert chain + private key) - First request for a hostname generates a leaf cert signed by the sandbox CA via `rcgen` - Cache has a hard limit of 256 entries; on overflow, the entire cache is cleared (sufficient for sandbox scale) - Each leaf cert chain contains two certs: the leaf and the CA **Connection flow (when TLS is detected):** + 1. `tls_terminate_client()`: Accept TLS from the sandboxed client using a `ServerConfig` with the hostname-specific leaf cert. ALPN: `http/1.1`. 2. `tls_connect_upstream()`: Connect TLS to the real upstream using a `ClientConfig` with Mozilla root CAs (`webpki_roots`) and system CA certificates. ALPN: `http/1.1`. 3. Proxy now holds plaintext on both sides. If L7 config is present, runs `relay_with_inspection()`. Otherwise, runs `relay_passthrough_with_credentials()` for credential injection without L7 evaluation. @@ -1233,11 +1248,13 @@ each cached entry stores: - File fingerprint (`len`, `mtime`, `ctime`, and on Unix `dev` + `inode`) `verify_or_cache(path)`: + - **First call for a path**: Compute SHA256 via `procfs::file_sha256()`, store as the "golden" hash plus fingerprint, return the hash. - **Subsequent calls, unchanged fingerprint**: Return cached hash without re-hashing the file. - **Subsequent calls, changed fingerprint**: Recompute SHA256 and compare with cached value. Return `Ok(hash)` on match; return `Err` on mismatch (binary tampered/replaced mid-sandbox). The TOFU model means: + - No hashes are specified in policy data -- the first observed binary is trusted - Once trusted, the binary cannot change for the sandbox's lifetime - Both the immediate binary and all ancestor binaries are TOFU-verified @@ -1279,12 +1296,14 @@ Both IPv4 (`/proc/{pid}/net/tcp`) and IPv6 (`/proc/{pid}/net/tcp6`) tables are c Wraps `tokio::process::Child` + PID. Platform-specific `spawn()` methods delegate to `spawn_impl()`. **Environment setup** (both Linux and non-Linux): + - `OPENSHELL_SANDBOX=1` (always set) - Provider credentials (from `GetSandboxProviderEnvironment` RPC) - Proxy URLs: `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY` (uppercase for curl/wget), `NO_PROXY=127.0.0.1,localhost,::1` for localhost bypass, `http_proxy`, `https_proxy`, `grpc_proxy` (lowercase for gRPC C-core), `no_proxy=127.0.0.1,localhost,::1`, `NODE_USE_ENV_PROXY=1` (required for Node.js built-in `fetch`/`http` clients to honor proxy env vars) - TLS trust store: `NODE_EXTRA_CA_CERTS` (standalone CA cert), `SSL_CERT_FILE`, `REQUESTS_CA_BUNDLE`, `CURL_CA_BUNDLE` (combined bundle) **Pre-exec closure** (runs in child after fork, before exec -- async-signal-safe): + 1. `setpgid(0, 0)` if non-interactive (create new process group) 2. `setns(fd, CLONE_NEWNET)` to enter network namespace (Linux only) 3. `drop_privileges(policy)`: `initgroups()` -> `setgid()` -> `setuid()` @@ -1295,6 +1314,7 @@ Wraps `tokio::process::Child` + PID. Platform-specific `spawn()` methods delegat ### `drop_privileges()` Resolves user/group names from policy, then: + 1. `initgroups()` to set supplementary groups (Linux only, not macOS) 2. `setgid()` to target group 3. Verify `getegid()` matches the target GID @@ -1352,6 +1372,7 @@ The `SshHandler` implements `russh::server::Handler`: ### PTY child process `spawn_pty_shell()`: + 1. `openpty()` to create a master/slave PTY pair 2. Build `std::process::Command` (not tokio) with slave FDs for stdin/stdout/stderr 3. Set environment: `OPENSHELL_SANDBOX=1`, `HOME=/sandbox`, `USER=sandbox`, `TERM={negotiated}`, proxy URLs, TLS trust store paths, provider credentials @@ -1570,10 +1591,12 @@ The sandbox uses `miette` for error reporting and `thiserror` for typed errors. ## Logging Dual-output logging is configured in `main.rs`: + - **stdout**: Filtered by `--log-level` (default `warn`), uses ANSI colors - **`/var/log/openshell.log`**: Fixed at `info` level, no ANSI, non-blocking writer Key structured log events: + - `CONNECT`: One per proxy CONNECT request (for non-`inference.local` targets) with full identity context. Inference interception failures produce a separate `info!()` log with `action=deny` and the denial reason. - `BYPASS_DETECT`: One per detected direct connection attempt that bypassed the HTTP CONNECT proxy. Includes destination, protocol, process identity (best-effort), and remediation hint. Emitted at `warn` level. - `L7_REQUEST`: One per L7-inspected request with method, path, and decision @@ -1608,6 +1631,7 @@ flowchart LR ``` Two log sources feed the same `TracingLogBus`: + - **Gateway logs** (`source: "gateway"`): Generated by the server's `SandboxLogLayer` tracing layer when server-side code emits events containing a `sandbox_id` field. These capture reconciliation, provisioning, and management operations. - **Sandbox logs** (`source: "sandbox"`): Pushed from the sandbox supervisor via the `PushSandboxLogs` client-streaming RPC. These capture proxy decisions, policy reloads, process lifecycle, and all other sandbox-internal tracing events. @@ -1626,6 +1650,7 @@ pub struct LogPushLayer { ``` Key behaviors: + - **Level filtering**: Defaults to `INFO`. Configurable via the `OPENSHELL_LOG_PUSH_LEVEL` environment variable (accepts `trace`, `debug`, `info`, `warn`, `error`). Events above the configured level are silently discarded. - **Best-effort delivery**: Uses `try_send()` on the mpsc channel. If the channel is full (1024 lines buffered), the event is dropped. Logging never blocks the sandbox supervisor. - **Structured fields**: Implements a `LogVisitor` that collects all tracing key-value fields (e.g., `dst_host`, `action`, `policy`) into a `HashMap`. The `message` field is extracted separately; all other fields go into `SandboxLogLine.fields`. @@ -1662,6 +1687,7 @@ The background task batches log lines and streams them to the gateway: **File:** `crates/openshell-server/src/grpc.rs` (`push_sandbox_logs`) The `PushSandboxLogs` RPC handler processes each batch: + 1. Validates `sandbox_id` is non-empty (skips empty batches). 2. Iterates over `batch.logs`, capped at 100 lines per batch to prevent abuse. 3. Forces `log.source = "sandbox"` on every line -- the sandbox cannot claim to be the gateway. @@ -1673,6 +1699,7 @@ The `PushSandboxLogs` RPC handler processes each batch: **File:** `crates/openshell-server/src/tracing_bus.rs` `publish_external()` wraps the `SandboxLogLine` in a `SandboxStreamEvent` and calls the internal `publish()` method, which: + 1. Sends the event to the per-sandbox `broadcast::Sender` (capacity 1024). Subscribers (active `WatchSandbox` streams) receive the event immediately. 2. Appends the event to the per-sandbox tail buffer (`VecDeque`), capped at 2000 lines. Overflow evicts the oldest entry. @@ -1746,12 +1773,13 @@ Filtering is implemented server-side. For `WatchSandbox`, filters apply to both `print_log_line()` in `crates/openshell-cli/src/run.rs` formats each log line: -``` +```text [timestamp] [source ] [level] [target] message key=value key=value ``` Example output: -``` + +```text [1708891234.567] [sandbox] [INFO ] [openshell_sandbox::proxy] CONNECT api.example.com:443 dst_host=api.example.com action=allow [1708891234.890] [gateway] [INFO ] [openshell_server::grpc] ReportPolicyStatus: sandbox reported policy load result ``` diff --git a/architecture/security-policy.md b/architecture/security-policy.md index e3ec6abbc..8afef7ae3 100644 --- a/architecture/security-policy.md +++ b/architecture/security-policy.md @@ -283,6 +283,7 @@ The `--global` flag on `policy set`, `policy delete`, `policy list`, and `policy Both `set` and `delete` require interactive confirmation (or `--yes` to bypass). The `--wait` flag is rejected for global policy updates: `"--wait is not supported for global policies; global policies are effective immediately"`. When a global policy is active, sandbox-scoped policy mutations are blocked: + - `policy set ` returns `FailedPrecondition: "policy is managed globally"` - `policy update ` returns `FailedPrecondition: "policy is managed globally"` - `rule approve`, `rule approve-all` return `FailedPrecondition: "cannot approve rules while a global policy is active"` @@ -764,7 +765,7 @@ If any condition fails, the proxy returns `403 Forbidden`. **Logging**: Forward proxy requests are logged distinctly from CONNECT: -``` +```text FORWARD method=GET dst_host=10.86.8.223 dst_port=8000 path=/screenshot/ action=allow policy=computer-control ``` @@ -823,8 +824,8 @@ TLS termination is automatic. The proxy peeks the first bytes of every CONNECT t | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `tls` absent or `""` (default) | **Auto-detect**: The proxy peeks the first bytes of the tunnel. If TLS is detected (ClientHello pattern), the proxy terminates TLS transparently (MITM), enabling credential injection and L7 inspection. If plaintext HTTP is detected, the proxy inspects directly. If neither, traffic is relayed raw. | | `tls: "skip"` | **Explicit opt-out**: No TLS detection, no termination, no credential injection. The tunnel is a raw `copy_bidirectional` relay. Use for client-cert mTLS to upstream or non-standard binary protocols. | -| `tls: "terminate"` *(deprecated)* | Treated as auto-detect. Emits a deprecation warning: "TLS termination is now automatic. Use `tls: skip` to explicitly disable." | -| `tls: "passthrough"` *(deprecated)* | Treated as auto-detect. Emits the same deprecation warning. | +| `tls: "terminate"` _(deprecated)_ | Treated as auto-detect. Emits a deprecation warning: "TLS termination is now automatic. Use `tls: skip` to explicitly disable." | +| `tls: "passthrough"` _(deprecated)_ | Treated as auto-detect. Emits the same deprecation warning. | **Prerequisites for TLS termination (auto-detect path)**: @@ -837,6 +838,7 @@ TLS termination is automatic. The proxy peeks the first bytes of every CONNECT t **Credential injection**: When TLS is auto-terminated but no L7 policy is configured (no `protocol` field), the proxy enters a passthrough relay that rewrites credential placeholders in HTTP headers (via `SecretResolver`) and logs requests for observability, but does not evaluate L7 OPA rules. This means credential injection works on all HTTPS endpoints automatically. **Validation warnings**: + - `tls: terminate` or `tls: passthrough`: deprecated, emits a warning. - `tls: skip` with `protocol: rest` on port 443: emits a warning ("L7 inspection cannot work on encrypted traffic"). @@ -1194,6 +1196,7 @@ With a matching sandbox `/etc/hosts` entry such as `192.168.1.105 searxng.local` #### `allowed_ips` Format Entries can be: + - **CIDR notation**: `10.0.5.0/24`, `172.16.0.0/12`, `192.168.1.0/24` - **Exact IP**: `10.0.5.20` (treated as `/32` for IPv4 or `/128` for IPv6) diff --git a/architecture/tui.md b/architecture/tui.md index 1a83e96d1..00e53a829 100644 --- a/architecture/tui.md +++ b/architecture/tui.md @@ -25,7 +25,7 @@ No separate configuration files or authentication are needed. The TUI divides the terminal into four horizontal regions: -``` +```text ┌─────────────────────────────────────────────────────────────────┐ │ OpenShell ─ my-cluster ─ Dashboard ● Healthy │ ← title bar ├─────────────────────────────────────────────────────────────────┤ @@ -58,10 +58,11 @@ The dashboard is divided into a top info pane and a middle pane with two tabs: - **Global Settings** — gateway-global runtime settings (fetched via `GetGatewaySettings`). **Health status** indicators: - - `●` **Healthy** (green) — everything is running normally. - - `◐` **Degraded** (yellow) — the cluster is up but something needs attention. - - `○` **Unhealthy** (red) — the cluster is not operating correctly. - - `…` — still connecting or status unknown. + +- `●` **Healthy** (green) — everything is running normally. +- `◐` **Degraded** (yellow) — the cluster is up but something needs attention. +- `○` **Unhealthy** (red) — the cluster is not operating correctly. +- `…` — still connecting or status unknown. **Global policy indicator**: When a global policy is active, the gateway row shows `Global Policy Active (vN)` in yellow (the `status_warn` style). The TUI detects this by polling `ListSandboxPolicies` with `global: true, limit: 1` on each tick and checking if the latest revision has `PolicyStatus::Loaded`. See `crates/openshell-tui/src/ui/dashboard.rs`. @@ -174,6 +175,7 @@ The TUI supports creating sandboxes with port forwarding directly from the creat Forwarded ports are displayed in the **NOTES** column of the sandbox table as `fwd:8080,3000` and in the **Forwards** row of the sandbox detail view. Port forwarding lifecycle: + - **On create**: The TUI polls for sandbox readiness (up to 30 attempts at 2-second intervals), then spawns SSH tunnels. - **On delete**: Any active forwards for the sandbox are automatically stopped before deletion. - **PID tracking**: Forward PIDs are stored in `~/.config/openshell/forwards/-.pid`, shared with the CLI. diff --git a/crates/openshell-cli/src/doctor_llm_prompt.md b/crates/openshell-cli/src/doctor_llm_prompt.md index 4d4a6b64c..a30d8981f 100644 --- a/crates/openshell-cli/src/doctor_llm_prompt.md +++ b/crates/openshell-cli/src/doctor_llm_prompt.md @@ -68,11 +68,13 @@ Before running commands, establish: ### Step 0: Quick Connectivity Check Run `openshell status` first. This immediately reveals: + - Which gateway and endpoint the CLI is targeting - Whether the CLI can reach the server (mTLS handshake success/failure) - The server version if connected Common errors at this stage: + - **`tls handshake eof`**: The server isn't running or mTLS credentials are missing/mismatched - **`connection refused`**: The container isn't running or port mapping is broken - **`No gateway configured`**: No gateway has been deployed yet @@ -222,6 +224,7 @@ openshell doctor exec -- kubectl -n openshell get secret openshell-client-tls -o ``` Common mTLS issues: + - **Secrets missing**: The `openshell` namespace may not have been created yet (Helm controller race). Bootstrap waits up to 2 minutes for the namespace. - **mTLS mismatch after manual secret deletion**: Delete all three secrets and redeploy — bootstrap will regenerate and restart the workload. - **CLI can't connect after redeploy**: Check that `~/.config/openshell/gateways//mtls/` contains `ca.crt`, `tls.crt`, `tls.key` and that they were updated at deploy time. diff --git a/crates/openshell-driver-vm/README.md b/crates/openshell-driver-vm/README.md index 8808b25d9..9ee916ef8 100644 --- a/crates/openshell-driver-vm/README.md +++ b/crates/openshell-driver-vm/README.md @@ -31,12 +31,10 @@ Sandbox guests execute `/opt/openshell/bin/openshell-sandbox` as PID 1 inside th ## Quick start (recommended) - ```shell mise run gateway:vm ``` - First run takes a few minutes while `mise run vm:setup` stages libkrun/libkrunfw/gvproxy and `mise run vm:rootfs -- --base` builds the embedded rootfs. Subsequent runs are cached. To keep the Unix socket path under macOS `SUN_LEN`, `mise run gateway:vm` and `start.sh` default the state dir to `/tmp/openshell-vm-driver-dev-$USER-port-$PORT/` (SQLite DB + per-sandbox rootfs + `compute-driver.sock`) unless `OPENSHELL_VM_DRIVER_STATE_DIR` is set. The wrapper also prints the recommended gateway name (`vm-driver-port-$PORT` by default) plus the exact repo-local `scripts/bin/openshell gateway add` and `scripts/bin/openshell gateway select` commands to use from another terminal. This avoids accidentally hitting an older `openshell` binary elsewhere on your `PATH`. It also exports `OPENSHELL_DRIVER_DIR=$PWD/target/debug` before starting the gateway so local dev runs use the freshly built `openshell-driver-vm` instead of an older installed copy from `~/.local/libexec/openshell` or `/usr/local/libexec`. diff --git a/crates/openshell-vm/README.md b/crates/openshell-vm/README.md index fcca20d5b..12edb2af3 100644 --- a/crates/openshell-vm/README.md +++ b/crates/openshell-vm/README.md @@ -118,7 +118,7 @@ Each instance gets its own extracted rootfs under `~/.local/share/openshell/open ## CLI Reference -``` +```text openshell-vm [OPTIONS] [COMMAND] Options: @@ -176,7 +176,7 @@ FROM_SOURCE=1 mise run vm:setup ## Architecture -``` +```text Host (macOS / Linux) openshell-vm binary |-- Embedded runtime (libkrun, libkrunfw, gvproxy, rootfs.tar.zst) diff --git a/crates/openshell-vm/runtime/README.md b/crates/openshell-vm/runtime/README.md index f43981102..76646a5ba 100644 --- a/crates/openshell-vm/runtime/README.md +++ b/crates/openshell-vm/runtime/README.md @@ -19,7 +19,7 @@ that enables these networking and sandboxing features. ## Directory Structure -``` +```text runtime/ kernel/ openshell.kconfig # Kernel config fragment (networking + sandboxing) @@ -29,7 +29,7 @@ runtime/ Each platform builds its own kernel and runtime natively. -``` +```text Linux ARM64: builds aarch64 kernel -> .so (parallel) Linux AMD64: builds x86_64 kernel -> .so (parallel) macOS ARM64: builds aarch64 kernel -> .dylib @@ -67,7 +67,7 @@ FROM_SOURCE=1 mise run vm:setup Build artifacts are placed in `target/libkrun-build/`: -``` +```text target/libkrun-build/ libkrun.so / libkrun.dylib # The VMM library libkrunfw.so* / libkrunfw.dylib # Kernel firmware library @@ -89,7 +89,7 @@ subsystem directly and avoids this entirely. At VM boot, the openshell-vm binary logs provenance information about the loaded runtime: -``` +```text runtime: /path/to/openshell-vm.runtime libkrunfw: libkrunfw.dylib sha256: a1b2c3d4e5f6... @@ -100,7 +100,8 @@ runtime: /path/to/openshell-vm.runtime ``` For stock runtimes: -``` + +```text runtime: /path/to/openshell-vm.runtime libkrunfw: libkrunfw.dylib sha256: f6e5d4c3b2a1... @@ -140,6 +141,7 @@ mise run vm ### "FailedCreatePodSandBox" bridge errors The kernel does not have bridge support. Verify: + ```bash # Inside VM: ip link add test0 type bridge && echo "bridge OK" && ip link del test0 @@ -150,6 +152,7 @@ If this fails, you are running the stock runtime. Build and use the custom one. ### kube-proxy CrashLoopBackOff kube-proxy runs in nftables mode. If it crashes, verify nftables support: + ```bash # Inside VM: nft list ruleset @@ -158,6 +161,7 @@ nft list ruleset If this fails, the kernel may lack `CONFIG_NF_TABLES`. Use the custom runtime. Common errors: + - `unknown option "--xor-mark"`: kube-proxy is running in iptables mode instead of nftables. Verify `--kube-proxy-arg=proxy-mode=nftables` is in the k3s args. @@ -165,12 +169,14 @@ Common errors: If libkrunfw is updated (e.g., via `brew upgrade`), the stock runtime may change. Check provenance: + ```bash # Look for provenance info in VM boot output grep "runtime:" ~/.local/share/openshell/openshell-vm/console.log ``` Re-build the custom runtime if needed: + ```bash FROM_SOURCE=1 mise run vm:setup mise run vm:build diff --git a/docs/.markdownlint-cli2.jsonc b/docs/.markdownlint-cli2.jsonc new file mode 100644 index 000000000..6e86e6286 --- /dev/null +++ b/docs/.markdownlint-cli2.jsonc @@ -0,0 +1,8 @@ +{ + "config": { + // MDX pages get their title from Fern frontmatter, not a top-level H1. + "MD041": false, + // MDX uses JSX components (, , ...) that look like HTML. + "MD033": false + } +} diff --git a/docs/CONTRIBUTING.mdx b/docs/CONTRIBUTING.mdx index 1fcaf322a..f2426873a 100644 --- a/docs/CONTRIBUTING.mdx +++ b/docs/CONTRIBUTING.mdx @@ -56,13 +56,15 @@ PRs that touch `docs/**` or `fern/**` are validated by `.github/workflows/branch - Published docs use Fern MDX under `docs/`. - Every page starts with YAML frontmatter. Use `title` and `description` on every page, then add page-level metadata like `sidebar-title`, `keywords`, and `position` when the page needs them. - Include the SPDX license header as YAML comments inside frontmatter: - ``` + + ```text --- # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 title: "Page Title" --- ``` + - Do not repeat the page title as a body H1. Fern renders the title from frontmatter. ### Frontmatter Template @@ -122,9 +124,11 @@ These patterns are common in LLM-generated text and erode trust with technical r - End every sentence with a period. - Use `code` formatting for CLI commands, file paths, flags, parameter names, and values. - Use `shell` code blocks for copyable CLI examples. Do not prefix commands with `$`: + ```shell openshell gateway start ``` + - Use `text` code blocks for transcripts, log output, and examples that should not be copied verbatim. - Use tables for structured comparisons. Keep tables simple (no nested formatting). - Use Fern components like ``, ``, and `` for callouts, not bold text. @@ -157,13 +161,13 @@ Use these consistently: 5. Run `mise run pre-commit` to catch formatting issues. 6. Open a PR with `docs:` as the conventional commit type. -``` +```text docs: update gateway deployment instructions ``` If your doc change accompanies a code change, include both in the same PR and use the code change's commit type: -``` +```text feat(cli): add --gpu flag to gateway start ``` diff --git a/docs/get-started/tutorials/github-sandbox.mdx b/docs/get-started/tutorials/github-sandbox.mdx index 8492dde71..ee3e16759 100644 --- a/docs/get-started/tutorials/github-sandbox.mdx +++ b/docs/get-started/tutorials/github-sandbox.mdx @@ -152,7 +152,7 @@ The following steps outline the expected process done by the agent: 1. Inspects the deny reasons. 2. Writes an updated policy that adds `github_git` and `github_api` blocks that grant write access to your repository. -3. Saves the policy to `/tmp/sandbox-policy-update.yaml`. +3. Saves the policy to `/tmp/sandbox-policy-update.yaml`. ## Review the Generated Policy diff --git a/docs/get-started/tutorials/inference-ollama.mdx b/docs/get-started/tutorials/inference-ollama.mdx index a123a2b28..4fcc018a7 100644 --- a/docs/get-started/tutorials/inference-ollama.mdx +++ b/docs/get-started/tutorials/inference-ollama.mdx @@ -45,7 +45,7 @@ Chat with a local model ollama run qwen3.5 ``` -Or a cloud model +Or a cloud model ```shell ollama run kimi-k2.5:cloud @@ -77,7 +77,7 @@ ollama launch claude --yes --model qwen3.5 | No local GPU | `qwen3.5:cloud` | Runs on Ollama's cloud infrastructure, no `ollama pull` required | -Cloud models use the `:cloud` tag suffix and do not require local hardware. +Cloud models use the `:cloud` tag suffix and do not require local hardware. ```shell openshell sandbox create --from ollama diff --git a/docs/get-started/tutorials/local-inference-lmstudio.mdx b/docs/get-started/tutorials/local-inference-lmstudio.mdx index 7b87df2e5..976770d1f 100644 --- a/docs/get-started/tutorials/local-inference-lmstudio.mdx +++ b/docs/get-started/tutorials/local-inference-lmstudio.mdx @@ -43,6 +43,7 @@ irm https://lmstudio.ai/install.ps1 | iex And start llmster: + ```shell lms daemon up ``` @@ -64,6 +65,7 @@ If you're using llmster in headless mode, run `lms server start --bind 0.0.0.0`. In the LM Studio app, head to the Model Search tab to download a small model like Qwen3.5 2B. In the terminal, use the following command to download and load the model: + ```shell lms get qwen/qwen3.5-2b lms load qwen/qwen3.5-2b diff --git a/docs/index.mdx b/docs/index.mdx index 05011e555..73b8d1d0b 100644 --- a/docs/index.mdx +++ b/docs/index.mdx @@ -9,7 +9,6 @@ position: 1 import { BadgeLinks } from "./_components/BadgeLinks"; - with { } breaks MDX/acorn */} +{/*Terminal demo styles live in fern/main.css — inline