diff options
| author | Adam Malczewski <[email protected]> | 2026-06-25 12:22:41 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-25 12:22:41 +0900 |
| commit | 54db4583e66134010375a1fa94256f36034ffdff (patch) | |
| tree | ec0bcd395d365741ed18e160f9b5842233051ba2 /packages/wire/src | |
| parent | 0b154bdad4f75a091db3ca46424abd17fbbc23ff (diff) | |
| download | dispatch-54db4583e66134010375a1fa94256f36034ffdff.tar.gz dispatch-54db4583e66134010375a1fa94256f36034ffdff.zip | |
feat(ssh): wave 1 — ExecBackend + computer data model + runtime threading
Wave 1 of transparent SSH support (parallel owner-agents on disjoint packages,
plus the orchestrator-authored kernel contract seam from wave 0):
- packages/wire: + Computer/ComputerEntry (read-only view over ~/.ssh/config
Host aliases) + Workspace.defaultComputerId (string|null, null=local). Types
only; 3 conformance tests.
- packages/exec-backend (NEW core extension): the ExecBackend abstraction
(spawn + minimal fs surface) the bundled tools will program against instead
of node:fs/child_process. LocalExecBackend wraps today's node calls
(behavior-identical; node:fs-style .code errors). execBackendHandle +
ExecBackendResolver (sync; computerId undefined -> local; set -> throws until
the ssh package wires remote resolution in wave 5). 20 tests.
- packages/kernel (runtime only): thread computerId through dispatch.ts +
run-turn.ts exactly as cwd is threaded (opaque, forwarded to
ToolExecuteContext; absent = local = byte-identical to today). +2 tests.
- packages/conversation-store: computer (SSH alias) assignment + resolution
mirroring cwd — WorkspaceRow.defaultComputerId + setWorkspaceDefaultComputerId
+ getComputerId/setComputerId/clearComputerId + getEffectiveComputer
(override -> per-conv -> workspace default -> null/local). Fixes the 3
Workspace literal sites the new required wire field broke. +18 tests.
- orchestrator: root tsconfig.json ref for exec-backend + bun install.
Verified: tsc -b EXIT 0, biome clean, 1592 vitest pass (was 1549, +43).
Refs: notes/ssh-support-plan.md (decisions §0.5/§13). No merge or push.
Diffstat (limited to 'packages/wire/src')
| -rw-r--r-- | packages/wire/src/index.test.ts | 59 | ||||
| -rw-r--r-- | packages/wire/src/index.ts | 49 |
2 files changed, 108 insertions, 0 deletions
diff --git a/packages/wire/src/index.test.ts b/packages/wire/src/index.test.ts new file mode 100644 index 0000000..cd297b7 --- /dev/null +++ b/packages/wire/src/index.test.ts @@ -0,0 +1,59 @@ +/** + * Conformance test for the wire ABI's type-only surface. The wire package ships + * no runtime, so these tests assert that the public shapes COMPILE and round-trip + * — a `Computer` literal satisfies its type, `ComputerEntry` extends `Computer`, + * and a `Workspace` carries the new `defaultComputerId`. The `ComputerEntry → + * Computer` assignment is a genuine compile-time check (it would fail to typecheck + * if the `extends` relationship broke); the runtime assertions are sanity echo. + */ + +import { describe, expect, it } from "vitest"; +import type { Computer, ComputerEntry, Workspace } from "./index.js"; + +describe("@dispatch/wire — Computer / Workspace shapes", () => { + it("a Computer literal satisfies the Computer type", () => { + const c: Computer = { + alias: "myserver", + hostName: "myserver.example.com", + port: 22, + user: "deploy", + identityFile: null, + knownHost: true, + }; + expect(c.alias).toBe("myserver"); + expect(c.port).toBe(22); + expect(c.identityFile).toBeNull(); + expect(c.knownHost).toBe(true); + }); + + it("ComputerEntry extends Computer and carries usageCount", () => { + const entry: ComputerEntry = { + alias: "buildbox", + hostName: "buildbox", + port: 2222, + user: "root", + identityFile: "/home/u/.ssh/id_ed25519", + knownHost: false, + usageCount: 3, + }; + // Compile-time proof that ComputerEntry is assignable to Computer. + const asComputer: Computer = entry; + expect(asComputer.alias).toBe("buildbox"); + expect(entry.usageCount).toBe(3); + }); + + it("a Workspace carries defaultComputerId (null = local)", () => { + const remote: Workspace = { + id: "default", + title: "Default", + defaultCwd: null, + defaultComputerId: "myserver", + createdAt: 0, + lastActivityAt: 0, + }; + expect(remote.defaultComputerId).toBe("myserver"); + + const local: Workspace = { ...remote, defaultComputerId: null }; + expect(local.defaultComputerId).toBeNull(); + }); +}); diff --git a/packages/wire/src/index.ts b/packages/wire/src/index.ts index bade977..f6a95cf 100644 --- a/packages/wire/src/index.ts +++ b/packages/wire/src/index.ts @@ -570,6 +570,14 @@ export interface Workspace { readonly title: string; /** The workspace's default cwd, or `null` (fall through to server default). */ readonly defaultCwd: string | null; + /** + * The workspace's default computer — an SSH config `Host` alias that + * conversations in this workspace inherit when they set no `computerId` of + * their own. `null` means local (no SSH; today's behavior). The computer + * analog of `defaultCwd`. Resolved per-conversation by `getEffectiveComputer` + * (per-conv `computerId` → this → `null`/local). + */ + readonly defaultComputerId: string | null; /** Epoch-ms when the workspace was first created. */ readonly createdAt: number; /** Epoch-ms of the most recent conversation activity in this workspace. */ @@ -584,3 +592,44 @@ export interface WorkspaceEntry extends Workspace { /** Number of conversations assigned to this workspace. */ readonly conversationCount: number; } + +// ─── Computers ─────────────────────────────────────────────────────────────── + +/** + * A read-only view of a remote computer discovered from the system's + * `~/.ssh/config` — a "computer" is a `Host` alias, NOT an editable entity + * (there is no Computer CRUD store). To add a computer, the user adds a `Host` + * block to `~/.ssh/config`; Dispatch discovers it on the next `listComputers()` + * read. Every field below is resolved from the config (first-match-wins for + * `HostName`/`User`/`Port`/`IdentityFile`). + * + * `alias` is the `computerId` users select — the string persisted per + * conversation and per workspace (the computer analog of `cwd`). `knownHost` + * drives the frontend "known/new" indicator and is read-only. + */ +export interface Computer { + /** The SSH config `Host` alias — also the `computerId` users select. */ + readonly alias: string; + /** Resolved `HostName`/IP from the config (falls back to the alias itself). */ + readonly hostName: string; + /** Resolved port (config `Port`, default 22). */ + readonly port: number; + /** Resolved user (config `User`, default the current user). */ + readonly user: string; + /** Resolved `IdentityFile` path (from the config, or `null` = default `~/.ssh/id_*`). */ + readonly identityFile: string | null; + /** + * Whether the host's key is already in `~/.ssh/known_hosts` (i.e. previously + * connected). Drives the frontend "known/new" indicator. Read-only. + */ + readonly knownHost: boolean; +} + +/** + * A computer entry in the list response (`GET /computers`) — a `Computer` plus + * a usage count. Parallel to `WorkspaceEntry`. + */ +export interface ComputerEntry extends Computer { + /** Number of conversations/workspaces whose `computerId` resolves to this alias. */ + readonly usageCount: number; +} |
