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 +
+
+
+
+
+ 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) + -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