From cf2817fe5ca3d4475fea1ff9e6924bb5c4e94afc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 3 May 2026 19:13:43 +0200 Subject: [PATCH 1/5] chore: update agent CHANGELOG.md for agent/v1.0.0 (#202) * chore: update agent CHANGELOG.md for agent/v1.0.0 * Change license from FSL-1.1-Apache-2.0 to MIT --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Martin Riedel <1713643+rado0x54@users.noreply.github.com> --- agent-client/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/agent-client/CHANGELOG.md b/agent-client/CHANGELOG.md index 5753fd2..9cd2304 100644 --- a/agent-client/CHANGELOG.md +++ b/agent-client/CHANGELOG.md @@ -1,3 +1,7 @@ +## v1.0.0 (2026-05-03) + +- chore: adopt MIT license ([#192](https://github.com/rado0x54/ShellWatch/pull/192)) + ## v0.1.0 (2026-05-01) - feat(agent): Windows support — named-pipe listener + windows build matrix ([#175](https://github.com/rado0x54/ShellWatch/pull/175)) ([#177](https://github.com/rado0x54/ShellWatch/pull/177)) From a155ebf77dab7620a618d44f3b4f62ce52257b7e Mon Sep 17 00:00:00 2001 From: Martin Riedel <1713643+rado0x54@users.noreply.github.com> Date: Mon, 4 May 2026 12:18:13 +0200 Subject: [PATCH 2/5] docs: align README and docs/ with current code (#203) --- README.md | 4 +-- docs/architecture.md | 58 +++++++++++++++++++++++++++++++---------- docs/ssh2-fork-guide.md | 23 +++++++++------- 3 files changed, 60 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 5340496..b2b6262 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ For detailed architecture docs see [docs/architecture.md](./docs/architecture.md ## Prerequisites -- Node.js 20+ +- Node.js 22+ - pnpm ## Setup @@ -220,7 +220,7 @@ agentSocket: Then run the [`shellwatch-agent`](./agent-client/) thin client on your workstation: ```bash -# Install (Homebrew tap; blocked anonymously while this repo is private — #147): +# Install (Homebrew tap): brew install rado0x54/tap/shellwatch-agent # Or build from source: diff --git a/docs/architecture.md b/docs/architecture.md index 141d59f..fe524a5 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -194,7 +194,7 @@ Actions expire after 60s if no response; denied/expired actions surface back to Bearer token authentication for MCP and agent proxy endpoints. - Keys stored as SHA-256 hashes in the database -- Scoped access: `mcp`, `agent`, `api` +- Scoped access: `mcp`, `agent` - Key prefix stored for identification in logs - `seedAdminApiKey` config option for bootstrapping @@ -216,6 +216,11 @@ SvelteKit SPA (adapter-static) served as static files by Fastify. SvelteKit prov | `/settings/passkeys` | WebAuthn passkey management | | `/settings/api-keys` | API key management | | `/settings/notifications` | Web Push subscription management | +| `/audit/sessions` | Session-lifecycle audit log | +| `/audit/signings` | Signing-request audit log | +| `/admin/accounts` | Admin: account management | +| `/admin/general` | Admin: general settings | +| `/passkey-invite/[token]` | Cross-device passkey enrollment (token-gated) | | `/login` | WebAuthn passkey login (supports `?redirect=` bounce-back) | | `/register` | WebAuthn passkey registration | @@ -224,15 +229,17 @@ SvelteKit SPA (adapter-static) served as static files by Fastify. SvelteKit prov - **REST API** (`/api/*`) — endpoint/session/key/WebAuthn CRUD, action resolve/deny (`/api/actions/:id/resolve` & `/deny`), session tail (`/api/sessions/:id/tail`) - **WebSocket** (`/ws`) — real-time terminal I/O and session events; also carries `sign:request` / `sign:resolved` notifications from the PendingAction system -**WebSocket protocol:** +**WebSocket protocol (`src/server/ws-protocol.ts`):** ``` Client → Server: terminal:attach, terminal:input, terminal:resize, terminal:close, terminal:take-control, terminal:release-control Server → Client: terminal:output, terminal:status, terminal:closed, terminal:mode, - sessions:changed, sign:request, sign:resolved, error + sessions:changed, error ``` +`sign:request` and `sign:resolved` also flow over the same WebSocket but are sent by `WebSocketChannel` in the PendingAction layer (`src/pending-action/ws-channel.ts`), not by the core terminal protocol — that's why they don't appear in `ws-protocol.ts`. + Sign approval itself (resolve/deny with WebAuthn assertion payload) goes over REST, not the WebSocket — see the [PendingAction section](#pendingaction--sign-requests-srcpending-action). **WebSocket extensions (`src/server/ws-extension.ts`):** Pluggable interface for sending server-initiated messages to account-scoped browser connections. `WebSocketChannel` (the notification-dispatcher channel) implements it to broadcast `sign:request` / `sign:resolved` to tabs owned by the target account. @@ -336,14 +343,17 @@ Single-file database (`data/shellwatch.db`) via better-sqlite3 with Drizzle ORM. **Schema (`src/db/schema.ts`):** -| Table | Purpose | -| ---------------------- | ---------------------------------------------------------------------------------- | -| `accounts` | User/agent accounts (admin flag, session limits, last used) | -| `webauthn_credentials` | Passkey credentials (COSE public key, OpenSSH public key, label) | -| `ssh_keys` | File-based SSH key metadata (fingerprint, public key — private keys on filesystem) | -| `endpoints` | SSH target configuration (host, port, username, key/passkey assignment) | -| `api_keys` | API key hashes, scopes, labels | -| `session_history` | Session audit log (endpoint, account, source, timestamps) | +| Table | Purpose | +| ------------------------- | ---------------------------------------------------------------------------------- | +| `accounts` | User/agent accounts (admin flag, session limits, last used) | +| `admin_account` | Single-row pointer table identifying the admin account | +| `webauthn_credentials` | Passkey credentials (COSE public key, OpenSSH public key, label) | +| `ssh_keys` | File-based SSH key metadata (fingerprint, public key — private keys on filesystem) | +| `endpoints` | SSH target configuration (host, port, username, key/passkey assignment) | +| `api_keys` | API key hashes, scopes, labels | +| `audit_session_lifecycle` | Tamper-evident session audit log (open/close events, source, timestamps) | +| `audit_signing_requests` | Signing-request audit log (passkey signs, key approvals — outcomes + metadata) | +| `push_subscriptions` | Web Push subscriptions per account (endpoint, auth/p256dh keys) | **Repositories (`src/db/repositories/`):** @@ -357,6 +367,19 @@ Single-file database (`data/shellwatch.db`) via better-sqlite3 with Drizzle ORM. **Migrations:** Auto-run at startup from `drizzle/` directory. +## Audit Log (`src/audit/`) + +Tamper-evident, append-only audit of session lifecycle and signing-request outcomes. The audit module subscribes to `TerminalManager` events and `PendingActionStore` resolutions; it never joins to live tables at read time, so a passkey rename or endpoint relabel never rewrites history. + +| File | Purpose | +| ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | +| `session-lifecycle-writer.ts` | Subscribes to TerminalManager open/close events, persists to `audit_session_lifecycle` | +| `session-lifecycle-repo.ts` | Keyset-paged read API powering `/audit/sessions` | +| `signing-requests-writer.ts` | Persists every PendingAction outcome (`approved` / `denied` / `expired` / `cancelled`) to `audit_signing_requests` with full trigger metadata | +| `signing-requests-repo.ts` | Read API powering `/audit/signings` | + +All audit reads are account-scoped — admin is an admin role, not a global view across accounts. + ## Security ### IP Allowlist (`src/server/auth/ip-allowlist.ts`) @@ -371,7 +394,7 @@ Session-based authentication for the web UI. Protects REST API and WebSocket rou ### API Key Auth (`src/server/auth/api-key-auth.ts`) -Bearer token authentication for MCP and agent proxy. Keys are stored as SHA-256 hashes. Each key has scopes (`mcp`, `agent`, `api`) that control which interfaces it can access. +Bearer token authentication for MCP and agent proxy. Keys are stored as SHA-256 hashes. Each key has scopes (`mcp`, `agent`) that control which interfaces it can access. ## Configuration @@ -506,6 +529,11 @@ src/ agent-socket/ agent-proxy-route.ts # WebSocket endpoint for SSH agent proxy socket-agent-handler.ts # AgentProtocol wiring to CompositeSshAgent + audit/ + session-lifecycle-writer.ts # Subscribes to TerminalManager, persists open/close events + session-lifecycle-repo.ts # Read API for /audit/sessions + signing-requests-writer.ts # Persists PendingAction outcomes (approve/deny/expire/cancel) + signing-requests-repo.ts # Read API for /audit/signings cli/ keys.ts # CLI for API key management config/ @@ -571,6 +599,9 @@ client/ # SvelteKit frontend (adapter-static) login/ # WebAuthn login page (supports ?redirect= bounce-back) register/ # WebAuthn passkey registration observer/ # Multi-session grid view + audit/ # Audit log views (sessions / signings) + admin/ # Admin views (accounts, general settings) + passkey-invite/[token]/ # Cross-device passkey enrollment settings/ # Settings with tab sub-routes (incl. notifications / Web Push) svelte.config.js # SvelteKit config (adapter-static) agent-client/ # Go thin client for SSH agent proxy @@ -586,6 +617,5 @@ See individual tickets for details: - **Guardrails (#13)** — input filtering layer in TerminalManager, before `sendInput()` - **SSH Server (#12)** — ssh2 Server for agent SSH access, username-based routing, uses AgentSession -- **Audit Log (#16)** — subscribes to TerminalManager events, persists to database - **Telegram notification channel** — new `NotificationChannel` implementation alongside WS/Push -- **Agent client distribution (#35)** — Homebrew tap exists at [rado0x54/homebrew-tap](https://github.com/rado0x54/homebrew-tap) (blocked on upstream-public release per #147); still pending: install script, deb/rpm packaging, install-service CLI; Windows support in #175 +- **Agent client distribution (#35)** — Homebrew tap published at [rado0x54/homebrew-tap](https://github.com/rado0x54/homebrew-tap); still pending: install script, deb/rpm packaging, install-service CLI; Windows support in #175 diff --git a/docs/ssh2-fork-guide.md b/docs/ssh2-fork-guide.md index 3f1cdcb..68ffdda 100644 --- a/docs/ssh2-fork-guide.md +++ b/docs/ssh2-fork-guide.md @@ -1,5 +1,10 @@ # ssh2 Fork: WebAuthn SK Key Support +> **Status:** Implemented. ShellWatch consumes the fork at +> [`github:rado0x54/ssh2#shellwatch`](https://github.com/rado0x54/ssh2/tree/shellwatch) +> (see `package.json`). This document is a reference for what the fork adds +> and why — not a forward-looking plan. + ## Goal Add support for the custom `webauthn-sk-ecdsa-sha2-nistp256@openssh.com` key algorithm to ssh2, enabling ShellWatch to authenticate to SSH servers using WebAuthn credentials via a custom agent that delegates signing to the browser. @@ -26,7 +31,7 @@ PubkeyAcceptedAlgorithms=+webauthn-sk-ecdsa-sha2-nistp256@openssh.com --- -## Changes Required (4 files) +## Changes in the fork (4 files) ### 1. `lib/protocol/constants.js` — Add algorithm to supported list @@ -278,14 +283,14 @@ The ECDSA signature (R, S) comes from the WebAuthn response's `signature` field, ## Testing the Fork -1. Apply the changes above -2. In ShellWatch, point to the fork: `pnpm add ../ssh2` -3. Create a test that: - - Constructs a `WebAuthnSKECDSAKey` from a known EC point - - Calls `parseKey()` with the binary wire format - - Verifies `key.type` is correct - - Verifies `key.getPublicSSH()` produces the correct blob -4. Integration test: connect to an SSH server with `PubkeyAcceptedAlgorithms` configured +The fork ships with the algorithm wired in. Local validation, when iterating on the fork itself: + +1. In ShellWatch, point to a local checkout: `pnpm add ../ssh2` +2. Cover the parser: + - Construct a `WebAuthnSKECDSAKey` from a known EC point + - Call `parseKey()` with the binary wire format + - Verify `key.type` is correct and `key.getPublicSSH()` produces the expected blob +3. Integration: connect to an SSH server with `PubkeyAcceptedAlgorithms=+webauthn-sk-ecdsa-sha2-nistp256@openssh.com` configured. --- From 707d1fb2277eae493135f9aac284752354bf4dd4 Mon Sep 17 00:00:00 2001 From: Martin Riedel <1713643+rado0x54@users.noreply.github.com> Date: Mon, 4 May 2026 14:18:49 +0200 Subject: [PATCH 3/5] =?UTF-8?q?docs:=20refactor=20README=20=E2=80=94=20log?= =?UTF-8?q?o,=20tagline,=20requirements,=20dev/prod=20flow=20(#204)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: trim README, add logo + tagline, align Design.md with current UI * docs: rework README header, requirements, and dev/prod flow --- README.md | 338 +++++++-------------------- design/Design.md | 11 +- design/shellwatch_wordmark-dark.svg | 13 ++ design/shellwatch_wordmark-light.svg | 13 ++ 4 files changed, 112 insertions(+), 263 deletions(-) create mode 100644 design/shellwatch_wordmark-dark.svg create mode 100644 design/shellwatch_wordmark-light.svg diff --git a/README.md b/README.md index b2b6262..5e94bcc 100644 --- a/README.md +++ b/README.md @@ -1,150 +1,113 @@ -# ShellWatch +

+ +
+ + + ShellWatch + +

-ShellWatch is a Human-in-the-Loop platform for agent-driven SSH. It's passkey-first and passkey-only — no passwords anywhere — with an SSH-agent proxy that forwards signing requests end-to-end to a user's WebAuthn passkey. Every agent action surfaces in realtime notifications, persists in a tamper-evident audit log, and can be gated behind explicit human approval before it touches the remote host. +

+ Passkey-Backed SSH for Humans and Agents +

-- **Passkey-only auth** — WebAuthn for UI login, agent enrollment, and SSH key approval; no passwords are stored or exchanged -- **End-to-end SSH-agent proxy** — your local `ssh`/`scp`/`git` reach a WebAuthn passkey via ShellWatch, with explicit browser approval on every signature (OpenSSH 10.3+ on the client; `verify-required` on the server enforces UV) -- **Human-in-the-loop for agents** — MCP agents request, humans approve; sensitive actions can require per-action consent before they hit the remote host -- **Realtime notifications** — sign requests arrive as Web Push and in-UI toasts so an approver can react without watching a tab -- **Tamper-evident audit log** — every signing request and session event persists to SQLite and is surfaced in the UI -- **Two interfaces, one core** — browser terminal (xterm.js) and MCP (streamable HTTP) share the same TerminalManager and stay in sync in real time; sessions created via MCP appear instantly in the UI (and vice versa) +

+ Website · + App · + Docs +

-For detailed architecture docs see [docs/architecture.md](./docs/architecture.md) and the [architecture diagram](./docs/architecture-diagram.md). +ShellWatch is a Human-in-the-Loop platform for agent-driven SSH. Passkey-first and passkey-only — no passwords anywhere — with an SSH-agent proxy that delivers end-to-end secure SSH authentication to your local client. Every agent action surfaces in realtime notifications, persists in a tamper-evident audit log, and can be gated behind explicit human approval before it touches the remote host. -## Prerequisites +- **Passkey-only auth** — WebAuthn for UI login, agent enrollment, and SSH authentication via OpenSSH's [`webauthn-sk-ecdsa-sha2-nistp256@openssh.com`](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.u2f) signature algorithm +- **End-to-end SSH-agent proxy** — local `ssh`/`scp`/`git` reach a passkey via ShellWatch with explicit browser approval per signature +- **Agent forwarding into sessions** — your passkey-backed SSH agent is forwarded into every ShellWatch session, so you can hop to additional hosts and enable SSH-agent-based PAM integration +- **PAM integration** — pair with [`pam-ssh-agent-webauthn`](https://github.com/rado0x54/pam-ssh-agent-webauthn) to gate `sudo` (or any PAM-aware step) behind a passkey approval surfaced through ShellWatch +- **Human-in-the-loop for agents** — MCP agents request, humans approve; sensitive actions can require per-action consent +- **Realtime notifications** — sign requests arrive as Web Push and in-UI toasts +- **Tamper-evident audit log** — every signing request and session event is recorded for later review +- **Three ways in** — web UI for humans, MCP for AI agents, and native `ssh`/`scp`/`git` from your workstation (via the `shellwatch-agent` daemon) -- Node.js 22+ -- pnpm +## Requirements -## Setup +`webauthn-sk-ecdsa-sha2-nistp256@openssh.com` support requires: + +- **Server (`sshd`):** OpenSSH **8.4+**, with the algorithm explicitly enabled in `/etc/ssh/sshd_config`: + + ``` + PubkeyAcceptedAlgorithms=+webauthn-sk-ecdsa-sha2-nistp256@openssh.com + ``` + +- **Client (`ssh`):** OpenSSH **10.3+** — only when using the [SSH agent proxy](#ssh-agent-proxy). The PAM-from-inside-a-session path uses our [PAM module](https://github.com/rado0x54/pam-ssh-agent-webauthn) talking to `$SSH_AUTH_SOCK` directly, and plain ShellWatch sessions opened from the UI or MCP have no client-side OpenSSH requirement. + +## Quick start ```bash git clone https://github.com/rado0x54/ShellWatch.git cd ShellWatch pnpm install +cp config.sample.yaml config.yaml +pnpm dev ``` -### Configuration +`pnpm dev` runs Fastify on `:3000` (API, WebSocket, MCP, agent-proxy) and a Vite dev server on `:3001` for the SvelteKit UI with hot reload — open in dev. Vite proxies WS/API/MCP traffic to Fastify, so everything works on the one URL. -```bash -cp config.sample.yaml config.yaml -``` +See `config.sample.yaml` for all options. Endpoints, keys, and passkeys are managed in the web UI; the config file only handles initial seeding and security settings. -Edit `config.yaml`. See `config.sample.yaml` for all options. Minimal example: +Minimal `config.yaml` for local dev (UI at `:3001`): ```yaml -keyDirectory: ./keys - server: - externalUrl: http://localhost:3000 + externalUrl: http://localhost:3001 security: rpId: localhost trustedWebauthnOrigins: + - http://localhost:3001 - http://localhost:3000 allowedNetworks: - 127.0.0.1/32 - "::1/128" - -# Optional: seed endpoints for the admin account on first run -# seedAdminEndpoints: -# - label: Dev Box -# address: ubuntu@dev.example.com - -# Optional: seed a known API key for MCP / agent proxy -# seedAdminApiKey: sw_000000000000000000000000000000000000000000000000 ``` -Endpoints, keys, and passkeys are managed dynamically via the web UI or REST API — changes are persisted in SQLite. The config file is only for initial seeding and security settings. - -### SSH key setup - -Place key files in the `keys/` directory. They are auto-discovered on startup and watched for changes. +## Production ```bash -ssh-keygen -t ed25519 -f ./keys/dev-box.pem -C "shellwatch" -ssh-copy-id -i ./keys/dev-box.pem.pub ubuntu@dev.example.com +pnpm build # tsc + SvelteKit +pnpm start # serves the pre-built client from dist/client/ ``` -- Keys are auto-discovered by scanning the key directory — no config needed -- Keys are assigned to endpoints via the web UI after discovery -- Key files must be readable by the current user (`chmod 600`) -- The `keys/` directory is gitignored +Then open — Fastify auto-detects `dist/client/` and serves the built UI off the same port as the API, WebSocket, MCP, and agent-proxy. -## Running +### Endpoints -### Development +| Path | Interface | +| -------------- | ----------------------------------------- | +| `/` | Web UI | +| `/observer` | Multi-session grid | +| `/settings/*` | Endpoints, keys, passkeys, API keys | +| `/api/*` | REST API | +| `/ws` | WebSocket (terminal I/O + events) | +| `/mcp` | MCP (streamable HTTP) | +| `/agent-proxy` | SSH agent proxy (WebSocket, API key auth) | +| `/health` | Health check | -```bash -pnpm dev -``` - -Builds the SvelteKit client and starts the server on `http://localhost:3000`. - -### Production - -```bash -pnpm build # compile server (tsc) + build client (SvelteKit) -pnpm start # run production server -``` +## Reverse proxy -The production server auto-detects the built client in `dist/client/` and serves it as static files. No Vite dependency at runtime. - -### All endpoints on a single port - -| Path | Interface | -| -------------- | ------------------------------------------------------- | -| `/` | Web UI — Terminal view | -| `/observer` | Web UI — Multi-session grid | -| `/settings/*` | Web UI — Settings (endpoints, keys, passkeys, API keys) | -| `/login` | Web UI — WebAuthn login | -| `/api/*` | REST API | -| `/ws` | WebSocket (terminal I/O + events) | -| `/mcp` | MCP (streamable HTTP) | -| `/agent-proxy` | SSH agent proxy (WebSocket, API key auth) | -| `/health` | Health check | - -### Deploying behind a reverse proxy - -When ShellWatch sits behind nginx, Caddy, an ALB, Cloudflare, etc., the TCP peer is the proxy — not the real client. Without configuration, every request looks like it came from the proxy, which breaks the sign-request "Source IP" display and the `security.allowedNetworks` allowlist. - -Configure `server.trustProxy` to the CIDR(s) of the proxy you control: +When ShellWatch runs behind nginx/Caddy/an ALB/Cloudflare, set `server.trustProxy` to the CIDR(s) of the proxy you control so real client IPs reach the allowlist and audit log: ```yaml server: externalUrl: https://shellwatch.example.com trustProxy: - - 10.0.0.0/8 # internal proxy CIDR(s) only - - 172.16.0.0/12 - -security: - # Real client IPs are now visible to the allowlist. Either narrow it to your - # known clients, or open it up explicitly: - allowedNetworks: - - 0.0.0.0/0 # all IPv4 - - "::/0" # all IPv6 + - 10.0.0.0/8 ``` -> **Do not set `trustProxy: true` in production.** That trusts `X-Forwarded-For` from any source, letting clients spoof their own IP. Always pin to the CIDR(s) of the proxy you actually run. Make sure the proxy itself sets `X-Forwarded-For` (e.g. nginx `proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;`). - -`trustProxy` also accepts a number (hops to trust) or a single CIDR string. See [Fastify's docs](https://fastify.dev/docs/latest/Reference/Server/#trustproxy) for the full grammar. - -## Web UI - -Open `http://localhost:3000` in your browser. - -- **Sidebar** shows configured endpoints, SSH keys, and active sessions -- Click **Connect** on an endpoint to open a terminal session -- Click a session in the sidebar to switch between terminals -- Sessions show their source — `(ui)` or `(mcp)` -- Terminal auto-resizes with the browser window -- Sessions created via MCP appear automatically — no refresh needed -- **Observer mode** — grid view to monitor multiple sessions at once -- **Sign-request approval** — when a sign is needed (passkey ceremony, SSH key approval), a toast and (optionally) a push notification link to a `/sign/:id` page where you approve or deny +> **Do not set `trustProxy: true` in production.** That trusts `X-Forwarded-For` from any source, letting clients spoof their IP. Pin to the CIDR of the proxy you actually run. Make sure the proxy itself sets `X-Forwarded-For`. See [Fastify's docs](https://fastify.dev/docs/latest/Reference/Server/#trustproxy) for the full grammar. ## MCP -ShellWatch exposes an MCP server over streamable HTTP at `/mcp`: +ShellWatch exposes an MCP server over streamable HTTP at `/mcp`. | Tool | Description | | ----------------------------- | --------------------------------------------- | @@ -156,38 +119,21 @@ ShellWatch exposes an MCP server over streamable HTTP at `/mcp`: | `shellwatch_manage_endpoints` | List, create, update, or delete SSH endpoints | | `shellwatch_manage_keys` | List available SSH keys | -Each MCP client gets an isolated `AgentSession` — agents can only see and control their own sessions. The web UI (admin view) sees all sessions regardless of source. - -**Notifications (server -> client):** +Each MCP client gets an isolated `AgentSession` — agents only see their own sessions. -- `output_available` — new output ready (debounced) -- `session_status` — session state changed +### Connecting an MCP client -### Claude Desktop / Claude Code configuration +Point your client (Claude Desktop, Claude Code, any MCP-aware tool) at the `/mcp` URL — the integrated OAuth flow handles credentials, no manual API key paste needed: -```json -{ - "mcpServers": { - "shellwatch": { - "type": "streamable-http", - "url": "http://localhost:3000/mcp", - "headers": { - "Authorization": "Bearer sw_your_api_key_here" - } - } - } -} +``` +https://your-shellwatch-host/mcp ``` -The API key must have `mcp` scope. Use `seedAdminApiKey` in config to seed a known key, or create one via the web UI under Settings → API Keys. - -## Push Notifications (PWA) - -ShellWatch is a Progressive Web App — it can be installed on mobile and desktop and supports Web Push notifications for sign requests (passkey signing, SSH key approval). This means users don't need the browser tab open to approve signing requests. +OAuth mints an `mcp`-scoped API key after browser approval. For headless setups you can still seed a static key via `seedAdminApiKey` in `config.yaml`, or create one under **Settings → API Keys**. -### Setup +## Push notifications (PWA) -Generate VAPID keys and add them to `config.yaml`: +ShellWatch is an installable PWA with Web Push for sign requests, so approvers don't need the tab open. Generate VAPID keys and add them to `config.yaml`: ```bash npx web-push generate-vapid-keys @@ -200,151 +146,25 @@ vapid: privateKey: "UGo..." ``` -Then enable push notifications in the web UI under Settings → Notifications. The browser will prompt for notification permission. +Enable push under **Settings → Notifications**. When `vapid` is unset, the feature is hidden. -When a sign request arrives (from an MCP agent, SSH agent proxy, or another UI session), a native push notification appears. Tapping it opens the sign request directly. +## SSH agent proxy -Push notifications are optional — when `vapid` is not configured, the feature is simply hidden. - -## SSH Agent Proxy - -ShellWatch can act as an SSH agent for system SSH clients (`ssh`, `scp`, `git`). This allows your local `ssh` command to authenticate using keys managed by ShellWatch — including WebAuthn passkeys — even when ShellWatch runs on a remote server. - -Enable in `config.yaml`: +ShellWatch can act as an SSH agent for system clients (`ssh`, `scp`, `git`), so your local commands authenticate via passkeys managed by ShellWatch. ```yaml agentSocket: proxyEnabled: true ``` -Then run the [`shellwatch-agent`](./agent-client/) thin client on your workstation: +Run [`shellwatch-agent`](./agent-client/) on your workstation: ```bash -# Install (Homebrew tap): brew install rado0x54/tap/shellwatch-agent - -# Or build from source: -cd agent-client && make build # or: go build -o shellwatch-agent ./cmd/shellwatch-agent/ - -# One-time browser-based login. Token persists in your OS keyring. -shellwatch-agent login --server https://shellwatch.example.com - -# Run as a service via Homebrew, or use the manual launchd / systemd setup -# in agent-client/README.md for self-hosted servers. +# Defaults to app.shellwatch.ai; pass `--server https://your-host` to point at a self-hosted instance. +shellwatch-agent login brew services start shellwatch-agent - -# In your shell profile, export the socket path: eval "$(shellwatch-agent --print-env)" ``` -`make build` injects the agent version via `-ldflags` (pulled from `git describe`) so it's surfaced to the approver on `/sign/:id`. Override with `make build VERSION=x.y.z`. - -`login` uses the OAuth shim at `/oauth/authorize` to mint an `agent`-scoped API key without you ever pasting one — see [agent-client/README.md](./agent-client/README.md) for the full flow, the static-key fallback for CI/headless setups, and the credential-store layout. - -Both WebAuthn passkeys and file-based SSH keys are supported for agent-proxy signing — both require browser approval (no silent auto-sign for the agent-proxy path). Passkeys require **OpenSSH 10.3+** on the client. Approval happens on the `/sign/:id` page, which also shows the agent client's self-reported hostname/OS/version when available. See the [agent-client README](./agent-client/README.md) for full usage, configuration, and troubleshooting. - -### Enforcing user verification on the OpenSSH server - -By default, ShellWatch performs the WebAuthn signing ceremony with `userVerification: "required"`, so every signature sent over the agent proxy carries the UV flag. (The setting is configurable per endpoint in Settings → Endpoints if you need to relax it for a specific host.) To make the UV guarantee load-bearing on the server side, configure the remote `sshd` to reject signatures whose UV flag is not set. - -OpenSSH enforces UV on `sk-ecdsa-sha2-nistp256@openssh.com` and `sk-ssh-ed25519@openssh.com` keys via `verify-required` (UV flag bit `0x04`), settable globally in `sshd_config` or per-key in `authorized_keys`. - -Per-key in `authorized_keys` (see `sshd(8)` AUTHORIZED_KEYS FILE FORMAT): - -``` -verify-required sk-ecdsa-sha2-nistp256@openssh.com AAAA... user@host -``` - -Global equivalent in `sshd_config`: - -``` -PubkeyAuthOptions verify-required -``` - -At authentication time, `sshd` ORs the global option with the per-key option — either source sets the requirement. With UV enforced, `sshd` parses `sk_flags` from the signature and rejects when `SSH_SK_USER_VERIFICATION_REQD` (`0x04` — the same bit as WebAuthn's UV flag) is not set, logging `user verification requirement not met`. - -For a hardened deployment, prefer global `PubkeyAuthOptions verify-required` so the policy is enforced uniformly and can't be bypassed by a stale `authorized_keys` entry. - -## Scripts - -| Script | Description | -| -------------------- | ------------------------------------------------------ | -| `pnpm dev` | Build client + start server with hot reload | -| `pnpm build` | Build server (tsc) + client (SvelteKit) for production | -| `pnpm start` | Run production server | -| `pnpm build:server` | Compile server TypeScript only | -| `pnpm build:client` | Build SvelteKit client | -| `pnpm typecheck` | Type check without emitting | -| `pnpm lint` | Lint with ESLint | -| `pnpm lint:fix` | Auto-fix lint issues | -| `pnpm format` | Format with Prettier | -| `pnpm test` | Run all tests | -| `pnpm test:coverage` | Run tests with coverage report | - -## Testing - -Tests cover unit and integration scenarios. No external services needed — everything runs in-process with an embedded ssh2 server. - -```bash -pnpm test # run all tests -pnpm test:coverage # run with coverage report -``` - -## Tech stack - -- **Backend:** Fastify (API, WebSocket, MCP, SSH — all server logic), ssh2, @modelcontextprotocol/sdk -- **Frontend:** SvelteKit (Svelte 5, adapter-static — client-side routing and build only, no SSR), xterm.js -- **Database:** SQLite via Drizzle ORM -- **Auth:** WebAuthn/passkeys (via @simplewebauthn) -- **Testing:** Vitest, ssh2 Server (in-process) -- **Config:** YAML + zod validation -- **Linting:** ESLint (typescript-eslint + eslint-plugin-svelte) -- **Formatting:** Prettier - -## Troubleshooting - -**"Private key not readable"** — Check file permissions: `chmod 600 ./keys/your-key.pem` - -**"Connection timed out"** — Verify the host is reachable and the port is correct. Connection timeout is 10 seconds. - -**"Auth failure"** — Ensure the private key matches the server's authorized keys and the username is correct. - -**Port already in use** — Kill the existing process: `lsof -ti:3000 | xargs kill` - -## License - -The ShellWatch server and client at the repository root are released under the -[**Functional Source License, Version 1.1, Apache 2.0 Future License**](./LICENSE) -(`FSL-1.1-Apache-2.0`, also published upstream as `FSL-1.1-ALv2`). - -In plain English: - -- **Self-hosting allowed** — run ShellWatch on your own infrastructure for any internal purpose. -- **Modify it freely** — fork, patch, change anything you want for your own use. -- **Use it in your business** — internal use is unrestricted, including by enterprises. -- **No competing commercial use** — for two years, you may not offer ShellWatch (or anything substantially similar) as a hosted/commercial service to third parties. -- **Becomes Apache 2.0 after 2 years** — every release auto-relicenses to permissive Apache 2.0 on its second anniversary. - -Sub-components ship under more permissive terms: - -| Path | License | Why | -| --------------- | ------------------------------- | -------------------------------------------------------------------- | -| repo root | [FSL-1.1-Apache-2.0](./LICENSE) | The commercial product. Source-available now, Apache 2.0 in 2 years. | -| `agent-client/` | [MIT](./agent-client/LICENSE) | End-user-machine binary; keeping it MIT removes adoption friction. | - -Third-party dependency license texts ship per release artifact: - -- **Node deps:** `/app/THIRD_PARTY_LICENSES` inside the Docker image — generated at image build by [`scripts/bundle-licenses.mjs`](./scripts/bundle-licenses.mjs). Run locally with `pnpm run licenses:bundle`. -- **Go deps:** `AGENT_THIRD_PARTY_LICENSES` uploaded alongside each agent binary on the [`agent/v*` GitHub releases](https://github.com/rado0x54/ShellWatch/releases) — generated at release time by [`agent-client/scripts/bundle-licenses.sh`](./agent-client/scripts/bundle-licenses.sh). Run locally with `cd agent-client && make licenses`. - -> Note: GitHub's license sidebar will show "Other" — FSL is not yet in -> [licensee](https://github.com/licensee/licensee), so the auto-detection -> can't classify it. The LICENSE file is canonical. - -### Trademark - -"ShellWatch" and the ShellWatch logo are trademarks of Martin Riedel and are -**not** licensed under FSL. The license grants you the right to use, modify, -and redistribute the source code — it does **not** grant the right to use the -ShellWatch name or logo to identify your fork or any derivative product or -service. If you ship a fork, please pick a different name and logo. +Every signing request requires explicit browser approval. To make user-verification load-bearing on the server, set `PubkeyAuthOptions verify-required` in `sshd_config`. Full usage, OAuth/static-key flows, and troubleshooting in the [agent-client README](./agent-client/README.md). diff --git a/design/Design.md b/design/Design.md index 7c50245..2132775 100644 --- a/design/Design.md +++ b/design/Design.md @@ -214,11 +214,12 @@ All buttons use Geist with 0.02em positive tracking and weight 600. ### Signal chips (badges) -A 6px colored dot + lowercase label, no background, no border, no pill. Three variants: +A 6px colored dot + lowercase label, no background, no border, no pill. Four variants: - `.badge-observer` — amber dot, amber label - `.badge-available` — emerald dot, emerald label - `.badge-unavailable` — crimson dot, crimson label +- `.badge-pending` — amber dot, amber label (e.g. "pending confirmation" on a key awaiting approval) Example usage: session list entries, settings rows ("required", "active", "admin"). @@ -226,9 +227,11 @@ Example usage: session list entries, settings rows ("required", "active", "admin 6px colored square (not a circle — no radius). `.open` glows emerald (live signal), `.error` glows crimson, `.opening` flat amber, `.closed` faint grey. -### Inputs — ghost underline +### Inputs — filled fields -Modals and forms use a single `1px` bottom edge of `--outline-variant`. Focus flips the underline to full `--primary` opacity with a subtle `2px` glow below. No background, no box, no focus ring. +Text-like ``, `