Skip to content

Let agents work on SSH servers with a password or key (GODM-36) - #36

Merged
danielehrhardt merged 7 commits into
mainfrom
agent/c-pro/cada564c272e
Sep 30, 2026
Merged

danielehrhardt merged 7 commits into
mainfrom
agent/c-pro/cada564c272e

Conversation

@danielehrhardt

Copy link
Copy Markdown
Contributor

What does this change?

Agents can now work on remote machines over SSH (GODM-36).

  • SSH servers page (sidebar → SSH servers): add a server with a password or an SSH key. For a key you can paste it, load a file, pick one from ~/.ssh or generate a new Ed25519 key (with a copy-ready authorized_keys command). Test connection signs in once and pins the server's host key. After that, a server that shows a different key is refused. Each card shows the status, OS, pinned key and who uses the server, and has a quick Run command box.
  • Chats and agents: pick servers for a chat with the new SSH chip in the message box, or assign them to an agent (agent settings → SSH servers, used in every run). A run gets its chat's servers plus its agent's. Only the human assigns servers: agents can't grant them to themselves, and delegated work doesn't inherit them.
  • ssh MCP tools for runs that have servers:
    • list_servers
    • shell: runs in the account's own shell. With sudo: true, Godmode answers sudo's prompt, and the password only goes out when sudo's own randomized prompt appears.
    • read_file / write_file / edit_file: over SFTP, falling back to the shell when a server has no SFTP.
    • upload / download: only within the run's folders. Downloads never follow dangling links and never write into .git or .claude folders.
  • Secrets: the password, private key and passphrase are sealed in the vault. The API never returns them and they are never part of the prompt. Transcripts and every tool result mask them.
  • Connections: uses ssh2, which is pure JavaScript, so it works in the compiled sidecar on every target. Its optional native helpers are never built. There is one pooled connection per server, keepalives are on, and a connection closes after 3 idle minutes or when the server's settings change.
  • Prompt: the system prompt lists the servers with rules for working on real machines (look first, back up configs, no restarts or destructive changes unless asked). Resumed turns restate them.
  • Docs: README (feature row, how-to, gallery), docs/ARCHITECTURE.md (SSH servers) and SECURITY.md.
SSH servers page An agent working on a server

The chat screenshot is a real Claude run against a local test SSH server. The agent read the config and the log, backed up the config and changed it, and didn't restart anything.

How was it tested?

  • pnpm typecheck passes, pnpm --filter @godmode/desktop build passes, and the core suite passes: 761 tests, 0 fail.
  • New packages/core/test/ssh.test.ts (19 tests) runs against an in-process SSH server (test/fixtures/ssh-server.ts: ssh2's server side with password and key sign-in, real sh commands, SFTP and a fake sudo). It covers:
    • sealed secrets and redaction, validation, and keys (public key refused, passphrase required and checked, import from ~/.ssh)
    • host-key pinning, a changed key, a wrong password and a closed port
    • tests of unsaved settings
    • the MCP tools: shell, cwd, stdin, sudo with a saved password, partial NOPASSWD and a rejected password, file tools, and SFTP-less servers
    • forgiving server names, and servers taken away mid-run
    • transfers kept within the run's folders (dangling links, .git and .claude), masking of the password and the key's lines
    • assignments and deletes
    • an end-to-end run with the fake Claude CLI (MCP config, system prompt, resumed turns)
  • ssh2 also checked against a real OpenSSH 10.3 sshd (key auth, exec, SFTP). The compiled sidecar builds and passes its smoke test.
  • Manually in the dashboard, with real Claude runs: adding, testing, assigning, the chip, the agent settings and dark mode.

Checklist

  • pnpm typecheck and pnpm test pass
  • cargo clippy / cargo test pass (if apps/desktop/src-tauri changed) — not changed
  • API changes are reflected in packages/shared and apps/desktop/src/lib/api.ts
  • No secrets, tokens or personal data in code, fixtures, logs or screenshots
  • Docs updated where behaviour changed

SSH servers are saved with their password, private key and passphrase sealed in
the vault and assigned to agents (every run) or chats (the SSH chip). Runs get an
ssh MCP server: shell with sudo answered by Godmode, SFTP file tools with a shell
fallback, and uploads/downloads within the run's folders. Host keys are pinned on
the first connection. The SSH servers page adds, tests and assigns servers and
runs quick commands.
# Conflicts:
#	apps/desktop/src/pages/chat/chat-home.tsx
#	packages/core/src/db/migrations.ts
#	packages/core/src/runner/mcpConfig.ts
#	packages/core/src/runner/prompt.ts
#	packages/core/src/runner/runner.ts
#	packages/core/src/server/routes/chat.ts
#	packages/core/src/services/conversations.ts
#	packages/core/test/scheduler.test.ts
#	packages/shared/src/api.ts
#	packages/shared/src/events.ts
#	packages/shared/src/models.ts
#	pnpm-lock.yaml
… key lines (GODM-36)

- sudo gets the saved password only when its prompt (a random marker) appears, so
  the line can't become input for the command; a second prompt reports a rejected
  password
- downloads refuse dangling links and .git/.claude folders and are renamed into
  place from a new file
- results mask the password, passphrase and every line of the saved key
- a connection made before a server moved no longer pins its host key; the OS is
  probed again after a move
- ids of deleted servers are dropped instead of blocking changes; chats hear
  about a deleted server
- SFTP calls honour run cancellation; honest wording for timed-out commands
- screenshots in the README
# Conflicts:
#	apps/desktop/src/pages/chat/chat-home.tsx
#	packages/core/src/db/migrations.ts
#	packages/core/src/server/app.ts
# Conflicts:
#	README.md
#	apps/desktop/src/components/agents/agent-form.tsx
#	packages/core/src/db/migrations.ts
#	packages/core/src/runner/prompt.ts
#	packages/core/src/runner/runner.ts
#	packages/core/src/vault/vault.ts
@danielehrhardt
danielehrhardt merged commit 9d92e86 into main Sep 30, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant