Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
## v1.0.1 (2026-05-05)

## What's Changed

- ci: drop develop-FF gate from release dispatches; require main by @rado0x54 in https://github.com/rado0x54/ShellWatch/pull/200
- Preprod Release by @rado0x54 in https://github.com/rado0x54/ShellWatch/pull/201
- chore: update agent CHANGELOG.md for agent/v1.0.0 by @github-actions[bot] in https://github.com/rado0x54/ShellWatch/pull/202
- docs: align README and docs/ with current code by @rado0x54 in https://github.com/rado0x54/ShellWatch/pull/203
- docs: refactor README — logo, tagline, requirements, dev/prod flow by @rado0x54 in https://github.com/rado0x54/ShellWatch/pull/204
- chore: self-host Geist fonts; drop Google Fonts dependency by @rado0x54 in https://github.com/rado0x54/ShellWatch/pull/206
- ci: bump homebrew-tap formula on agent release by @rado0x54 in https://github.com/rado0x54/ShellWatch/pull/205
- Preprod Release by @rado0x54 in https://github.com/rado0x54/ShellWatch/pull/207

**Full Changelog**: https://github.com/rado0x54/ShellWatch/compare/v1.0.0...v1.0.1

## v1.0.0 (2026-05-03)

## What's Changed
Expand Down
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ ShellWatch is a Human-in-the-Loop platform for agent-driven SSH. Passkey-first a

- **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
- **Agent forwarding into sessions** — your passkey-backed SSH agent is forwarded into ShellWatch sessions (per-endpoint toggle), 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
Expand All @@ -38,6 +38,14 @@ ShellWatch is a Human-in-the-Loop platform for agent-driven SSH. Passkey-first a
PubkeyAcceptedAlgorithms=+webauthn-sk-ecdsa-sha2-nistp256@openssh.com
```

One-liner to append it and reload `sshd`:

```bash
echo 'PubkeyAcceptedAlgorithms=+webauthn-sk-ecdsa-sha2-nistp256@openssh.com' \
| sudo tee -a /etc/ssh/sshd_config
sudo systemctl reload ssh # or: sudo systemctl reload sshd
```

- **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
Expand Down
7 changes: 7 additions & 0 deletions client/src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,13 @@ pre,
background: var(--warning, var(--secondary));
}

.badge-demo {
color: var(--accent);
}
.badge-demo::before {
background: var(--accent);
}

/* ------------------------------------------------------------------
* Status dots — small 6px squares, colored, glow when live
* ------------------------------------------------------------------ */
Expand Down
187 changes: 187 additions & 0 deletions client/src/lib/components/ServerSetupGuide.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
<!-- SPDX-License-Identifier: LicenseRef-FSL-1.1-Apache-2.0 -->
<!--
Walks a user through the two-line server-side setup for ShellWatch passkey
auth. Reused by:
- the "Add Your Own Endpoint" wizard (passes the freshly-registered passkey
so the authorized_keys command is fully copy-pastable)
- the /settings/setup help tab (no passkey context; renders an instructional
placeholder + a link to /settings/passkeys)
-->
<script lang="ts">
import Wordmark from "./Wordmark.svelte";

interface Props {
/**
* Optional passkey to inline into the second copy-block. When omitted, the
* second block shows a placeholder and a hint to grab the entry from
* Settings → Passkeys.
*/
passkey?: { authorizedKeysEntry: string | null; label: string } | null;
/** Account name used to scope the authorized_keys comment. Defaults to "user". */
accountName?: string;
}

let { passkey = null, accountName = "user" }: Props = $props();

// Single source for the sshd line — referenced from both the visible code
// block and the clipboard handler so they can't drift. Mirrors the value
// computed server-side in src/webauthn/ssh-key-format.ts; if that changes
// (additional algorithms, multi-line config), surface it through the
// register response and consume it here instead.
const SSHD_CONFIG_LINE = "PubkeyAcceptedAlgorithms=+webauthn-sk-ecdsa-sha2-nistp256@openssh.com";
const SSHD_CONFIG_ONE_LINER = `echo '${SSHD_CONFIG_LINE}' | sudo tee -a /etc/ssh/sshd_config`;

function sshComment(label: string): string {
const sanitize = (s: string) =>
s
.toLowerCase()
.replace(/[^a-z0-9]/g, "_")
.replace(/_+/g, "_")
.replace(/^_|_$/g, "");
const host = sanitize(window.location.hostname);
const name = sanitize(accountName.trim() || "user");
const key = sanitize(label);
return `${host}-${name}-${key}`;
}

const sshLine = $derived(
passkey?.authorizedKeysEntry
? `${passkey.authorizedKeysEntry} ${sshComment(passkey.label)}`
: null,
);

const sshOneLiner = $derived(sshLine ? `echo '${sshLine}' >> ~/.ssh/authorized_keys` : null);

async function copyToClipboard(text: string, btn: HTMLButtonElement) {
const original = btn.innerHTML;
try {
await navigator.clipboard.writeText(text);
btn.innerHTML = "&#10003; Copied";
} catch {
// Insecure context (HTTP non-localhost) or denied permission — surface
// the failure instead of flashing a false-positive "Copied" state.
btn.innerHTML = "Copy failed";
}
setTimeout(() => {
btn.innerHTML = original;
}, 1500);
}
</script>

<p class="description">
Two one-time steps on each server you want to reach. Requires
<strong>OpenSSH 8.4+</strong>.
</p>

<div class="code-block">
<span class="code-label"
>1. Enable WebAuthn keys in <code>/etc/ssh/sshd_config</code> (reload sshd after)</span
>
<code class="code-content">{SSHD_CONFIG_ONE_LINER}</code>
<button
type="button"
class="btn-copy"
onclick={(e) => copyToClipboard(SSHD_CONFIG_ONE_LINER, e.currentTarget as HTMLButtonElement)}
>Copy</button
>
</div>

{#if sshOneLiner && sshLine}
<div class="code-block">
<span class="code-label">2. Add this passkey to <code>~/.ssh/authorized_keys</code></span>
<code class="code-content">{sshOneLiner}</code>
<button
type="button"
class="btn-copy"
onclick={(e) => copyToClipboard(sshOneLiner!, e.currentTarget as HTMLButtonElement)}
>Copy</button
>
</div>
{:else if passkey}
<p class="hint">
This authenticator does not expose an SSH-compatible public key. You can still use it for <Wordmark
/> login. To enable SSH, register a different passkey from Settings.
</p>
{:else}
<div class="code-block">
<span class="code-label">2. Add a passkey to <code>~/.ssh/authorized_keys</code> (example)</span
>
<code class="code-content placeholder">
echo 'webauthn-sk-ecdsa-sha2-nistp256@openssh.com
AAAAK3dlYmF1dGhuLXNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBExample…=
example_com-alice-yubikey5' &gt;&gt; ~/.ssh/authorized_keys
</code>
</div>
<p class="hint">
Replace the key body and comment with your own — copy the exact one-liner for a specific passkey
from <strong>Settings → Passkeys</strong>.
</p>
{/if}

<style>
.description,
.hint {
color: var(--text-muted);
font-size: 0.85rem;
margin-bottom: 0.75rem;
line-height: 1.55;
}

.hint {
font-size: 0.78rem;
}

.code-block {
position: relative;
background: var(--bg-primary);
border: 1px solid var(--border);
border-radius: 6px;
padding: 0.6rem 0.75rem;
margin-bottom: 0.75rem;
text-align: left;
}

.code-label {
display: block;
font-size: 0.65rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-muted);
margin-bottom: 0.35rem;
}

.code-content {
display: block;
font-size: 0.75rem;
word-break: break-all;
padding-right: 3.5rem; /* leave room for the absolute Copy button */
}

.code-content.placeholder {
color: var(--text-muted);
font-style: italic;
}

.btn-copy {
position: absolute;
top: 0.4rem;
right: 0.4rem;
padding: 0.25rem 0.5rem;
background: var(--bg-secondary);
color: var(--text-primary);
border: 1px solid var(--border);
border-radius: 4px;
font-size: 0.7rem;
cursor: pointer;
}

.btn-copy:hover {
background: var(--border);
}

@media (max-width: 640px) {
.code-content {
font-size: 0.7rem;
}
}
</style>
Loading
Loading