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/exec-backend/src/backend.ts | |
| 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/exec-backend/src/backend.ts')
| -rw-r--r-- | packages/exec-backend/src/backend.ts | 78 |
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>; +} |
