diff options
| author | Adam Malczewski <[email protected]> | 2026-06-25 21:45:58 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-25 21:45:58 +0900 |
| commit | 2cc9ddfb590dc60557bba3ed76a6c4639df5f596 (patch) | |
| tree | b0ced1ecb5f899e6a2b835d41603c4040a49bbce /packages/session-orchestrator/src | |
| parent | 087ce142247637bb10351ab7815144b720836153 (diff) | |
| download | dispatch-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/session-orchestrator/src')
| -rw-r--r-- | packages/session-orchestrator/src/orchestrator.test.ts | 19 | ||||
| -rw-r--r-- | packages/session-orchestrator/src/orchestrator.ts | 13 |
2 files changed, 22 insertions, 10 deletions
diff --git a/packages/session-orchestrator/src/orchestrator.test.ts b/packages/session-orchestrator/src/orchestrator.test.ts index 8ff3f5e..654c0c3 100644 --- a/packages/session-orchestrator/src/orchestrator.test.ts +++ b/packages/session-orchestrator/src/orchestrator.test.ts @@ -3611,12 +3611,13 @@ function createFakeSystemPromptService( constructImpl: ( conversationId: string, cwd: string, - context?: { readonly model?: string }, + context?: { readonly model?: string; readonly computerId?: string }, ) => Promise<string>, - getWithMetaImpl: ( - conversationId: string, - ) => Promise<{ readonly prompt: string | null; readonly cwd: string | null }> = () => - Promise.resolve({ prompt: null, cwd: null }), + getWithMetaImpl: (conversationId: string) => Promise<{ + readonly prompt: string | null; + readonly cwd: string | null; + readonly computerId: string | null; + }> = () => Promise.resolve({ prompt: null, cwd: null, computerId: null }), ): SystemPromptService { return { construct: constructImpl, @@ -3663,7 +3664,7 @@ describe("system prompt: regular turn flow", () => { }, async (conversationId) => { getCalls.push(conversationId); - return null; + return { prompt: null, cwd: null, computerId: null }; }, ), }); @@ -3714,7 +3715,7 @@ describe("system prompt: regular turn flow", () => { }, async (conversationId) => { getWithMetaCalls.push(conversationId); - return { prompt: "PERSISTED_PROMPT", cwd: "/work/dir" }; + return { prompt: "PERSISTED_PROMPT", cwd: "/work/dir", computerId: null }; }, ), }); @@ -3762,7 +3763,7 @@ describe("system prompt: regular turn flow", () => { constructCalls.push({ conversationId, cwd, model: context?.model }); return "RECONSTRUCTED_PROMPT"; }, - async () => ({ prompt: null, cwd: null }), + async () => ({ prompt: null, cwd: null, computerId: null }), ), }); @@ -3815,7 +3816,7 @@ describe("system prompt: regular turn flow", () => { async (conversationId) => { getWithMetaCalls.push(conversationId); // Stored prompt was built against an OLD cwd. - return { prompt: "STALE_PROMPT", cwd: "/old/dir" }; + return { prompt: "STALE_PROMPT", cwd: "/old/dir", computerId: null }; }, ), }); diff --git a/packages/session-orchestrator/src/orchestrator.ts b/packages/session-orchestrator/src/orchestrator.ts index a1401d6..4aa77f7 100644 --- a/packages/session-orchestrator/src/orchestrator.ts +++ b/packages/session-orchestrator/src/orchestrator.ts @@ -619,17 +619,26 @@ export function createSessionOrchestrator( { ...(effectiveModelName !== undefined ? { model: effectiveModelName } : {}), ...(workspaceId !== undefined ? { workspaceId } : {}), + ...(effectiveComputerId !== undefined ? { computerId: effectiveComputerId } : {}), }, ); } else { const meta = await systemPromptService.getWithMeta(conversationId); const currentCwd = effectiveCwd ?? process.cwd(); - if (meta.prompt !== null && meta.cwd === currentCwd) { + const currentComputerId = effectiveComputerId ?? null; + // Invalidate when cwd OR computerId changed (switching computers + // must rebuild the prompt against the remote OS/hostname). + if ( + meta.prompt !== null && + meta.cwd === currentCwd && + meta.computerId === currentComputerId + ) { systemPrompt = meta.prompt; } else { systemPrompt = await systemPromptService.construct(conversationId, currentCwd, { ...(effectiveModelName !== undefined ? { model: effectiveModelName } : {}), ...(workspaceId !== undefined ? { workspaceId } : {}), + ...(effectiveComputerId !== undefined ? { computerId: effectiveComputerId } : {}), }); } } @@ -1122,9 +1131,11 @@ export function createCompactionService( if (systemPromptService !== undefined) { const cwd = (await deps.conversationStore.getEffectiveCwd(conversationId)) ?? process.cwd(); const workspaceId = await deps.conversationStore.getWorkspaceId(conversationId); + const computerId = await deps.conversationStore.getEffectiveComputer(conversationId); const constructed = await systemPromptService.construct(conversationId, cwd, { ...(opts?.modelName !== undefined ? { model: opts.modelName } : {}), workspaceId, + ...(computerId !== null ? { computerId } : {}), }); compactionSystemPrompt = `${constructed}\n\n${COMPACTION_SYSTEM_PROMPT}`; } else { |
