summaryrefslogtreecommitdiffhomepage
path: root/packages/ssh/src/service.ts
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-25 21:45:58 +0900
committerAdam Malczewski <[email protected]>2026-06-25 21:45:58 +0900
commit2cc9ddfb590dc60557bba3ed76a6c4639df5f596 (patch)
treeb0ced1ecb5f899e6a2b835d41603c4040a49bbce /packages/ssh/src/service.ts
parent087ce142247637bb10351ab7815144b720836153 (diff)
downloaddispatch-2cc9ddfb590dc60557bba3ed76a6c4639df5f596.tar.gz
dispatch-2cc9ddfb590dc60557bba3ed76a6c4639df5f596.zip
feat(ssh): discover computers from ~/.ssh/known_hosts + remote system-prompt
Two improvements to the SSH support feature: 1. KNOWN_HOSTS DISCOVERY (packages/ssh): Computers are now auto-discovered from ~/.ssh/known_hosts (every hostname you've ever connected to) in ADDITION to ~/.ssh/config (explicit Host aliases). Config entries take precedence (full params); known_hosts entries get defaulted params (User=defaultUser, IdentityFile=null→pool probes default keys, Port from [host]:port or 22, knownHost=true). Zero-config — no ~/.ssh/config file needed; hosts just appear. Reject list: dispatch.toml [ssh].reject = [...] (glob patterns like github.com, *.ts.net) filters noise from the catalog. Read from both the global ~/.config/dispatch/dispatch.toml and the project dispatch.toml. Parsed with Bun.TOML.parse (zero deps). Only filters discovery (catalog); specific lookups (getComputer/getStatus/test/connect) ignore the reject list (it's a visibility filter, not access control). New pure functions: parseKnownHosts(), isRejected(), globMatch(). +26 tests. tsc EXIT 0, biome clean, 1756 tests pass. 2. REMOTE SYSTEM-PROMPT AWARENESS (packages/system-prompt): When a conversation has a computerId set (remote turn), the system prompt now resolves system:os, system:hostname, git:branch/git:status, and file: reads against the REMOTE machine — not the local host. Previously the prompt always said 'Arch Linux (WSL)' + local hostname even when the agent was connected to a remote Artix Linux machine. The ResolverAdapters' hostname()/platform() are now async (so a remote adapter can run 'hostname'/'uname -s' over SSH). The system-prompt extension builds remote adapters from the ExecBackend (readFile→SFTP, spawn→SSH exec). Cache invalidation now checks computerId (switching computers rebuilds the prompt). The compaction path also threads computerId. @dispatch/system-prompt now depends on @dispatch/exec-backend.
Diffstat (limited to 'packages/ssh/src/service.ts')
-rw-r--r--packages/ssh/src/service.ts23
1 files changed, 19 insertions, 4 deletions
diff --git a/packages/ssh/src/service.ts b/packages/ssh/src/service.ts
index 6a809c6..629c9f8 100644
--- a/packages/ssh/src/service.ts
+++ b/packages/ssh/src/service.ts
@@ -21,7 +21,7 @@ import type { ComputerStatusResponse, TestComputerResponse } from "@dispatch/tra
import type { ComputerService } from "@dispatch/transport-http/dist/seam.js";
import type { Computer, ComputerEntry } from "@dispatch/wire";
import { createSshExecBackend } from "./backend.js";
-import { resolveComputer, resolveComputers } from "./config.js";
+import { resolveComputer, resolveComputers, type SshConfigResolveEnv } from "./config.js";
import { createSshConnectionPool, type SshConnectionPool, type SshPoolDeps } from "./pool.js";
/**
@@ -42,6 +42,12 @@ export interface SshServiceDeps extends SshPoolDeps {
* host-bin wires this from conversation-store; absent → every count is 0.
*/
readonly getUsageCounts?: () => Promise<ReadonlyMap<string, number>>;
+ /**
+ * Optional: glob patterns to exclude from the computer catalog (e.g.
+ * `github.com`, `*.ts.net`). Sourced from `dispatch.toml` `[ssh].reject`.
+ * Absent → no filtering.
+ */
+ readonly readRejectPatterns?: () => Promise<readonly string[]>;
}
/** Build the `ComputerService` + the remote-`ExecBackend` factory. */
@@ -53,12 +59,21 @@ export function createSshService(deps: SshServiceDeps): {
} {
const pool = createSshConnectionPool(deps);
- async function readEnv() {
- const [configText, knownHostsText] = await Promise.all([
+ async function readEnv(): Promise<SshConfigResolveEnv> {
+ const [configText, knownHostsText, rejectPatterns] = await Promise.all([
deps.readConfigText().catch(async () => ""),
deps.readFileText(deps.knownHostsPath).catch(async () => ""),
+ deps.readRejectPatterns !== undefined
+ ? deps.readRejectPatterns().catch(async () => [] as readonly string[])
+ : Promise.resolve([] as readonly string[]),
]);
- return { configText, knownHostsText, defaultUser: deps.defaultUser, homeDir: deps.homeDir };
+ const base: SshConfigResolveEnv = {
+ configText,
+ knownHostsText,
+ defaultUser: deps.defaultUser,
+ homeDir: deps.homeDir,
+ };
+ return rejectPatterns.length > 0 ? { ...base, rejectPatterns } : base;
}
const service: ComputerService = {