diff --git a/.github/workflows/agent-release.yml b/.github/workflows/agent-release.yml index 51e8c31..967858c 100644 --- a/.github/workflows/agent-release.yml +++ b/.github/workflows/agent-release.yml @@ -13,12 +13,20 @@ name: Agent Release # 2. cross-build linux/{amd64,arm64} + darwin/{amd64,arm64} binaries # 3. tag main HEAD as agent/vX.Y.Z, push tag # 4. create GitHub release with binaries + sha256s -# 5. open agent-client/CHANGELOG.md PR to develop +# 5. bump rado0x54/homebrew-tap formula (skipped if HOMEBREW_TAP_TOKEN unset) +# 6. open agent-client/CHANGELOG.md PR to develop # # No source files are bumped. main.Version is injected via -ldflags at build # time. CHANGELOG.md updates flow through normal PR review, so main and # develop never receive bot-authored direct pushes — branch protection # rulesets stay active. +# +# Homebrew tap bump (step 5) writes directly to rado0x54/homebrew-tap's main. +# That repo has no protection ruleset (it's a personal formula tap, not the +# product), so a PR-and-merge round-trip would just be ceremony. Requires a +# fine-grained PAT with `contents: write` on rado0x54/homebrew-tap, stored as +# repo secret HOMEBREW_TAP_TOKEN. If the secret is unset the step is skipped +# so a forked / detached clone of this repo can still cut releases. on: workflow_dispatch: @@ -220,6 +228,69 @@ jobs: LICENSE-shellwatch-agent \ AGENT_THIRD_PARTY_LICENSES + - name: Bump homebrew-tap formula + env: + HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} + TAP_REPO: rado0x54/homebrew-tap + VERSION: ${{ needs.prepare.outputs.version }} + TAG: ${{ needs.prepare.outputs.tag }} + run: | + set -euo pipefail + if [ -z "${HOMEBREW_TAP_TOKEN:-}" ]; then + echo "::notice::HOMEBREW_TAP_TOKEN not set — skipping tap bump" + exit 0 + fi + + # `.sha256` files have format: " ". Pull the hex. + sha_for() { + awk '{print $1}' "artifacts/shellwatch-agent-$1.sha256" + } + DARWIN_ARM64=$(sha_for darwin-arm64) + DARWIN_AMD64=$(sha_for darwin-amd64) + LINUX_ARM64=$(sha_for linux-arm64) + LINUX_AMD64=$(sha_for linux-amd64) + + git clone --depth 1 \ + "https://x-access-token:${HOMEBREW_TAP_TOKEN}@github.com/${TAP_REPO}.git" \ + tap + cd tap + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + # Rewrite version + each per-arch sha256 with a verifying python pass. + # Using python (not sed) so a missed/duplicate match fails the job + # instead of silently shipping a stale formula. + python3 - "$VERSION" "$DARWIN_ARM64" "$DARWIN_AMD64" "$LINUX_ARM64" "$LINUX_AMD64" <<'PY' + import pathlib, re, sys + + version, da_arm, da_amd, lx_arm, lx_amd = sys.argv[1:] + path = pathlib.Path("Formula/shellwatch-agent.rb") + text = path.read_text() + + def sub_once(pattern, repl, label, body): + new, n = re.subn(pattern, repl, body) + if n != 1: + raise SystemExit(f"expected 1 match for {label}, got {n}") + return new + + text = sub_once(r'( version ")[^"]+(")', rf'\g<1>{version}\g<2>', + "version", text) + for arch, sha in [("darwin-arm64", da_arm), ("darwin-amd64", da_amd), + ("linux-arm64", lx_arm), ("linux-amd64", lx_amd)]: + pat = rf'(shellwatch-agent-{arch}"\s*\n\s*sha256 ")[a-f0-9]{{64}}(")' + text = sub_once(pat, rf'\g<1>{sha}\g<2>', f"sha {arch}", text) + + path.write_text(text) + PY + + git add Formula/shellwatch-agent.rb + if git diff --cached --quiet; then + echo "Tap formula already at ${TAG} — nothing to push" + exit 0 + fi + git commit -m "feat: bump shellwatch-agent to ${TAG}" + git push origin HEAD:main + # main/develop are protected by rulesets — bot opens a PR. Admin pushes # any commit to trigger CI (GITHUB_TOKEN-authored PRs don't auto-fire # workflows), squash-merges, then fast-forwards main. diff --git a/README.md b/README.md index 5340496..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 20+ -- 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; blocked anonymously while this repo is private — #147): 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/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)) diff --git a/client/src/app.css b/client/src/app.css index a1f71e5..142caf9 100644 --- a/client/src/app.css +++ b/client/src/app.css @@ -1,6 +1,68 @@ /* SPDX-License-Identifier: LicenseRef-FSL-1.1-Apache-2.0 */ @import "@xterm/xterm/css/xterm.css"; +/* Self-hosted Geist + Geist Mono (variable woff2, weights 100-900). + Files live in client/static/fonts/ and are kept in sync with the + @fontsource-variable/geist{,-mono} packages — re-copy after a bump. */ +@font-face { + font-family: "Geist"; + font-style: normal; + font-display: swap; + font-weight: 100 900; + src: url("/fonts/geist-latin-wght-normal.woff2") format("woff2-variations"); + unicode-range: + U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, + U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +@font-face { + font-family: "Geist"; + font-style: normal; + font-display: swap; + font-weight: 100 900; + src: url("/fonts/geist-latin-ext-wght-normal.woff2") format("woff2-variations"); + unicode-range: + U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, + U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, + U+A720-A7FF; +} +@font-face { + font-family: "Geist"; + font-style: normal; + font-display: swap; + font-weight: 100 900; + src: url("/fonts/geist-cyrillic-wght-normal.woff2") format("woff2-variations"); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +@font-face { + font-family: "Geist Mono"; + font-style: normal; + font-display: swap; + font-weight: 100 900; + src: url("/fonts/geist-mono-latin-wght-normal.woff2") format("woff2-variations"); + unicode-range: + U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, + U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +@font-face { + font-family: "Geist Mono"; + font-style: normal; + font-display: swap; + font-weight: 100 900; + src: url("/fonts/geist-mono-latin-ext-wght-normal.woff2") format("woff2-variations"); + unicode-range: + U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, + U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, + U+A720-A7FF; +} +@font-face { + font-family: "Geist Mono"; + font-style: normal; + font-display: swap; + font-weight: 100 900; + src: url("/fonts/geist-mono-cyrillic-wght-normal.woff2") format("woff2-variations"); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} + *, *::before, *::after { diff --git a/client/src/app.html b/client/src/app.html index a4c5712..742e358 100644 --- a/client/src/app.html +++ b/client/src/app.html @@ -11,12 +11,6 @@ - - - %sveltekit.head% diff --git a/client/static/fonts/OFL.txt b/client/static/fonts/OFL.txt new file mode 100644 index 0000000..98384d6 --- /dev/null +++ b/client/static/fonts/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2024 The Geist Project Authors (https://github.com/vercel/geist-font.git) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/client/static/fonts/geist-cyrillic-wght-normal.woff2 b/client/static/fonts/geist-cyrillic-wght-normal.woff2 new file mode 100644 index 0000000..2000e32 Binary files /dev/null and b/client/static/fonts/geist-cyrillic-wght-normal.woff2 differ diff --git a/client/static/fonts/geist-latin-ext-wght-normal.woff2 b/client/static/fonts/geist-latin-ext-wght-normal.woff2 new file mode 100644 index 0000000..9608c47 Binary files /dev/null and b/client/static/fonts/geist-latin-ext-wght-normal.woff2 differ diff --git a/client/static/fonts/geist-latin-wght-normal.woff2 b/client/static/fonts/geist-latin-wght-normal.woff2 new file mode 100644 index 0000000..93552f5 Binary files /dev/null and b/client/static/fonts/geist-latin-wght-normal.woff2 differ diff --git a/client/static/fonts/geist-mono-cyrillic-wght-normal.woff2 b/client/static/fonts/geist-mono-cyrillic-wght-normal.woff2 new file mode 100644 index 0000000..dbd7964 Binary files /dev/null and b/client/static/fonts/geist-mono-cyrillic-wght-normal.woff2 differ diff --git a/client/static/fonts/geist-mono-latin-ext-wght-normal.woff2 b/client/static/fonts/geist-mono-latin-ext-wght-normal.woff2 new file mode 100644 index 0000000..f638b47 Binary files /dev/null and b/client/static/fonts/geist-mono-latin-ext-wght-normal.woff2 differ diff --git a/client/static/fonts/geist-mono-latin-wght-normal.woff2 b/client/static/fonts/geist-mono-latin-wght-normal.woff2 new file mode 100644 index 0000000..504be86 Binary files /dev/null and b/client/static/fonts/geist-mono-latin-wght-normal.woff2 differ 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 ``, `