You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Tailscale Remote SSH Transport Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.
Goal: Allow each configured Server to use either direct TCP or the host's Tailscale daemon for every Vulseek SSH operation, while preserving the existing execAsyncRemote(serverId, command, onData?) API.
Architecture: Add a direct | tailscale connection setting to Server. A shared SSH transport factory creates either a normal TCP-backed ssh2.Client or a tailscale nc-backed client using the host tailscaled.sock. Ordinary remote commands reuse a process-local SSH connection pool; interactive terminals and log streams use dedicated connections through the same transport factory.
Write failing tests for direct configuration, Tailscale spawn arguments, missing socket, proxy early exit, SSH authentication failure, and cleanup.
For direct, connect ssh2.Client with the existing host, port, username, private key, and timeout behavior.
For tailscale, verify VULSEEK_TAILSCALE_SOCKET (default /var/run/tailscale/tailscaled.sock) is a Unix socket, then call spawn("tailscale", ["--socket=...", "nc", host, String(port)]) without a shell.
Wrap child stdout/stdin as a Node Duplex and pass it as ssh2's sock; retain stderr for diagnostics without treating normal output as SSH data.
Make close() idempotently end the SSH client, destroy the stream, and terminate/reap tailscale nc.
Reject if the proxy exits before SSH becomes ready; never retry through direct TCP.
Verify tests pass and commit as feat(server): add tailscale ssh transport.
Write failing tests proving concurrent first calls share one connection, commands open separate channels, idle connections close, config changes invalidate the entry, and transport errors evict it.
Store ready connections in a module-level Map<serverId, entry> and in-flight creation in a second single-flight map.
Compute a non-logged SHA-256 fingerprint from connection type, address, port, username, SSH Key ID, and private-key content; replace entries whose fingerprint changes.
Increment active channels on acquire, cancel the idle timer, and start a 60-second timer after the last release.
On SSH/proxy error or close, evict and close the entry. Do not replay commands whose channel may already have been accepted.
Refactor execAsyncRemote to acquire the pooled client, call client.exec, preserve stdout/stderr/onData behavior, and release exactly once on every completion path.
Register backend shutdown cleanup to call closeAllRemoteSshConnections.
Verify pool and execAsyncRemote tests pass and commit as refactor(server): pool remote ssh commands.
Task 4: Route every direct SSH entry through the factory
Files:
Modify: packages/server/src/setup/server-validate.ts, server-setup.ts, and server-audit.ts
Install the pinned Tailscale CLI 1.96.4 in development and release backend images and verify tailscale version during image build.
At service start, detect whether the host socket exists. If present, add a read-only bind mount and set VULSEEK_TAILSCALE_SOCKET; if absent, start normally so Direct servers remain usable.
Extend status output to show whether the socket is mounted and reachable.
Do not mount the socket into PostgreSQL, Redis, Traefik, tools, checkout, scan, or Agent containers.
Verify both scripts still start without Tailscale installed and commit as feat(runtime): expose host tailscale socket.
Task 6: Validate behavior and document operations
Files:
Modify: repository deployment/operations documentation near the existing remote Server setup instructions.
Document prerequisites: host tailscaled online, backend socket mount, remote node reachable in the same tailnet, tailnet policy permitting TCP/22, remote OpenSSH and Docker installed, and an existing Vulseek SSH Key.
Document that socket access is privileged and limited to the backend container.
Test a Direct Server and confirm execAsyncRemote(serverId, "docker info") remains unchanged.
Test a Tailscale IPv4 Server and a MagicDNS Server with docker info, setup/validate, terminal, and container logs.
Test missing socket, daemon offline, ACL denial, unknown MagicDNS name, SSH rejection, and backend restart.
Run pnpm --filter @vulseek/server typecheck, relevant Vitest suites, and pnpm check on touched files.
Commit as docs(server): document tailscale remote access.
Acceptance Criteria
Existing Server records remain Direct after migration.
Selecting Tailscale causes all SSH-based Server operations to use the mounted host daemon.
execAsyncRemote callers remain unchanged and reuse one SSH connection per Server for sequential commands within 60 seconds.
Interactive and streaming operations use the same transport but dedicated SSH connections.
Tailscale failures never bypass the configured transport.
Backend shutdown and idle timeout leave no orphan tailscale nc processes.
Tailscale Remote SSH Transport Implementation Plan
Goal: Allow each configured Server to use either direct TCP or the host's Tailscale daemon for every Vulseek SSH operation, while preserving the existing
execAsyncRemote(serverId, command, onData?)API.Architecture: Add a
direct | tailscaleconnection setting to Server. A shared SSH transport factory creates either a normal TCP-backedssh2.Clientor atailscale nc-backed client using the hosttailscaled.sock. Ordinary remote commands reuse a process-local SSH connection pool; interactive terminals and log streams use dedicated connections through the same transport factory.Tech Stack: TypeScript, Node.js 20 streams/child processes,
ssh2, Drizzle/PostgreSQL, React Hook Form/Zod, Docker Swarm, Tailscale CLI 1.96.4.Global Constraints
execAsyncRemote.directand require no manual migration action.tailscaleServer never falls back to direct TCP after a Tailscale failure.tailscaled.sockinto managed, scan, or Agent containers.Task 1: Persist the Server connection type
Files:
packages/server/src/db/schema/server.tsapps/vulseek/drizzle/0214_server_connection_type.sqlapps/vulseek/drizzle/meta/_journal.jsonapps/vulseek/components/dashboard/settings/servers/handle-servers.tsxapps/vulseek/components/dashboard/settings/servers/show-servers.tsxInterfaces:
Add
serverConnectionTypeEnum = pgEnum("serverConnectionType", ["direct", "tailscale"]).Add
server.connectionType, non-null with defaultdirect.Include
connectionTypeinapiCreateServerandapiUpdateServer.Add schema tests or type assertions proving create/update inputs accept only
directandtailscale.Add the enum and column migration with
directas the database default.Add a Connection Type select to create/edit Server forms; keep address, port, username, and SSH Key unchanged.
Show a Direct/Tailscale badge in the Server list.
Run the relevant schema/UI tests and
pnpm --filter @vulseek/server typecheck.Commit as
feat(server): add remote connection type.Task 2: Add the shared SSH transport factory
Files:
packages/server/src/utils/servers/remote-ssh-transport.tspackages/server/src/utils/servers/remote-ssh-transport.test.tsInterfaces:
direct, connectssh2.Clientwith the existing host, port, username, private key, and timeout behavior.tailscale, verifyVULSEEK_TAILSCALE_SOCKET(default/var/run/tailscale/tailscaled.sock) is a Unix socket, then callspawn("tailscale", ["--socket=...", "nc", host, String(port)])without a shell.Duplexand pass it asssh2'ssock; retain stderr for diagnostics without treating normal output as SSH data.close()idempotently end the SSH client, destroy the stream, and terminate/reaptailscale nc.feat(server): add tailscale ssh transport.Task 3: Add the process-local SSH command pool
Files:
packages/server/src/utils/servers/remote-ssh-pool.tspackages/server/src/utils/servers/remote-ssh-pool.test.tspackages/server/src/utils/process/execAsync.tsInterfaces:
Map<serverId, entry>and in-flight creation in a second single-flight map.errororclose, evict and close the entry. Do not replay commands whose channel may already have been accepted.execAsyncRemoteto acquire the pooled client, callclient.exec, preserve stdout/stderr/onData behavior, and release exactly once on every completion path.closeAllRemoteSshConnections.execAsyncRemotetests pass and commit asrefactor(server): pool remote ssh commands.Task 4: Route every direct SSH entry through the factory
Files:
packages/server/src/setup/server-validate.ts,server-setup.ts, andserver-audit.tspackages/server/src/utils/builders/drop.tsapps/vulseek/server/wss/terminal.ts,listen-deployment.ts,docker-container-terminal.ts, anddocker-container-logs.tsInterfaces:
Short commands continue through pooled
execAsyncRemote.Interactive shells, SFTP, deployment listeners, and container log streams call
connectRemoteSshdirectly and own a dedicatedRemoteSshHandle.Add tests/mocks proving each feature requests the shared transport rather than constructing
ssh2.Clientdirectly.Replace direct
.connect({ host, ... })blocks while preserving existing authorization, stream handling, and user-facing errors.Close dedicated handles on WebSocket close, stream close, setup completion, SFTP completion, and every error path.
Run
rg 'new Client\('and confirm only the transport implementation or explicitly local-only code remains.Run relevant server and WebSocket tests and commit as
refactor(server): unify remote ssh connections.Task 5: Mount the host Tailscale management socket
Files:
DockerfileDockerfile.devdev.shrun.shInterfaces:
VULSEEK_TAILSCALE_SOCKET_HOST_PATH, default/var/run/tailscale/tailscaled.sock.VULSEEK_TAILSCALE_SOCKET, container path/var/run/tailscale/tailscaled.sock.Install the pinned Tailscale CLI 1.96.4 in development and release backend images and verify
tailscale versionduring image build.At service start, detect whether the host socket exists. If present, add a read-only bind mount and set
VULSEEK_TAILSCALE_SOCKET; if absent, start normally so Direct servers remain usable.Extend
statusoutput to show whether the socket is mounted and reachable.Do not mount the socket into PostgreSQL, Redis, Traefik, tools, checkout, scan, or Agent containers.
Verify both scripts still start without Tailscale installed and commit as
feat(runtime): expose host tailscale socket.Task 6: Validate behavior and document operations
Files:
Modify: repository deployment/operations documentation near the existing remote Server setup instructions.
Document prerequisites: host
tailscaledonline, backend socket mount, remote node reachable in the same tailnet, tailnet policy permitting TCP/22, remote OpenSSH and Docker installed, and an existing Vulseek SSH Key.Document that socket access is privileged and limited to the backend container.
Test a Direct Server and confirm
execAsyncRemote(serverId, "docker info")remains unchanged.Test a Tailscale IPv4 Server and a MagicDNS Server with
docker info, setup/validate, terminal, and container logs.Test missing socket, daemon offline, ACL denial, unknown MagicDNS name, SSH rejection, and backend restart.
Run
pnpm --filter @vulseek/server typecheck, relevant Vitest suites, andpnpm checkon touched files.Commit as
docs(server): document tailscale remote access.Acceptance Criteria
execAsyncRemotecallers remain unchanged and reuse one SSH connection per Server for sequential commands within 60 seconds.tailscale ncprocesses.