summaryrefslogtreecommitdiffhomepage
path: root/packages/exec-backend/src/backend.ts
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/exec-backend/src/backend.ts
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/exec-backend/src/backend.ts')
-rw-r--r--packages/exec-backend/src/backend.ts78
1 files changed, 78 insertions, 0 deletions
diff --git a/packages/exec-backend/src/backend.ts b/packages/exec-backend/src/backend.ts
new file mode 100644
index 0000000..f6a807f
--- /dev/null
+++ b/packages/exec-backend/src/backend.ts
@@ -0,0 +1,78 @@
+/**
+ * ExecBackend — the transport-agnostic spawn + minimal filesystem surface.
+ *
+ * Tools (tool-shell, tool-read-file, tool-write-file, tool-edit-file) program
+ * against THIS abstraction instead of `node:fs` / `node:child_process` directly.
+ * Two implementations exist:
+ *
+ * - `LocalExecBackend` — wraps today's node calls (behavior-identical).
+ * - `SshExecBackend` — wraps ssh2 `exec` + `sftp` (added later by the `ssh`
+ * package; not this package's concern — but THIS interface is the seam it
+ * implements).
+ *
+ * The surface is deliberately SMALL (only what the bundled tools use) so a
+ * remote implementation is tractable. New operations are added here, not ad hoc.
+ *
+ * Resolved per-call from `ToolExecuteContext.computerId` via the injected
+ * `ExecBackendResolver` (see `./service.js`). `computerId` undefined → local.
+ *
+ * Error contract: `readFile`/`stat`/`readdir`/`writeFile` throw node:fs-style
+ * errors carrying a `.code` property (e.g. `"ENOENT"`) so the tools' existing
+ * error branches work unchanged. `exists` never throws (returns `false` on
+ * missing). The SshExecBackend maps ssh2 errors onto these same shapes.
+ */
+
+/** A spawned process's result. Mirrors tool-shell's `SpawnResult` exactly. */
+export interface ExecResult {
+ readonly exitCode: number | null;
+ readonly timedOut: boolean;
+ readonly aborted: boolean;
+}
+
+/** Parameters for spawning a shell command. Mirrors tool-shell's `SpawnShell` params. */
+export interface SpawnParams {
+ readonly command: string;
+ readonly cwd: string;
+ readonly signal: AbortSignal;
+ readonly timeout: number;
+ readonly onOutput: (data: string, stream: "stdout" | "stderr") => void;
+}
+
+/** Stat result — the subset read_file / write_file / edit_file need. */
+export interface StatResult {
+ readonly isFile: boolean;
+ readonly isDirectory: boolean;
+}
+
+/** A directory entry — the subset read_file lists. */
+export interface DirEntry {
+ readonly name: string;
+ readonly isDirectory: boolean;
+}
+
+/**
+ * The execution backend: spawn + a minimal filesystem surface.
+ * Tools program against THIS, never against `node:fs`. Resolved per-call from
+ * `ToolExecuteContext.computerId` via the injected resolver.
+ */
+export interface ExecBackend {
+ /** Run a shell command, streaming stdout/stderr. The shell-tool seam. */
+ readonly spawn: (params: SpawnParams) => Promise<ExecResult>;
+
+ // --- filesystem (the read_file / write_file / edit_file surface) ---
+
+ /** Read a file as utf8 text. Throws node:fs-style errors with `.code`. */
+ readonly readFile: (path: string) => Promise<string>;
+
+ /** Write utf8 text to a file. Throws on failure (e.g. missing parent dir). */
+ readonly writeFile: (path: string, content: string) => Promise<void>;
+
+ /** Stat a path. Throws node:fs-style errors with `.code` (e.g. `"ENOENT"`). */
+ readonly stat: (path: string) => Promise<StatResult>;
+
+ /** List directory entries. Throws node:fs-style errors with `.code`. */
+ readonly readdir: (path: string) => Promise<readonly DirEntry[]>;
+
+ /** Check existence without throwing (returns `false` when the path is missing). */
+ readonly exists: (path: string) => Promise<boolean>;
+}