summaryrefslogtreecommitdiffhomepage
path: root/packages/wire/src
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-25 12:22:41 +0900
committerAdam Malczewski <[email protected]>2026-06-25 12:22:41 +0900
commit54db4583e66134010375a1fa94256f36034ffdff (patch)
treeec0bcd395d365741ed18e160f9b5842233051ba2 /packages/wire/src
parent0b154bdad4f75a091db3ca46424abd17fbbc23ff (diff)
downloaddispatch-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.ts59
-rw-r--r--packages/wire/src/index.ts49
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;
+}