Skip to content

Add coder() isolated sandbox provider#495

Open
ThomasK33 wants to merge 5 commits intomattpocock:mainfrom
coder:coder-provider-q30q
Open

Add coder() isolated sandbox provider#495
ThomasK33 wants to merge 5 commits intomattpocock:mainfrom
coder:coder-provider-q30q

Conversation

@ThomasK33
Copy link
Copy Markdown

@ThomasK33 ThomasK33 commented Apr 30, 2026

Hey Matt,

Really cool project, thanks for kicking it off.
I've added a sandbox provider for coder/coder.

Let me know if you're interested in getting this merged in-tree or if this should live outside of Sandcastle and be published separately.

Agent PR body and implementation plan/prompt on clear context below.


Summary

Adds a built-in coder() isolated sandbox provider that integrates Sandcastle with Coder workspaces.

import { run, claudeCode } from "@ai-hero/sandcastle";
import { coder } from "@ai-hero/sandcastle/sandboxes/coder";

await run({
  agent: claudeCode("claude-opus-4-6"),
  sandbox: coder({ template: "coder", preset: "Pittsburgh", onClose: "delete" }),
  prompt: "Fix the failing tests.",
});

The provider can either:

  • Create a new Coder workspace from a template (with optional parameters, parameterFile, preset, templateVersion, organization, and workspaceName); or
  • Attach to an existing Coder workspace by name, owner/name, or UUID (auto-starting it if it is stopped).

What's included

  • Provider: src/sandboxes/coder.ts. CLI-only implementation that shells out to coder for everything (no new npm peer dep). Discriminated CoderOptions union (template xor workspace). Required onClose: "delete" | "stop" | "leave" — Coder workspaces are persistent, so we make the user choose explicitly. Polls coder list -o json for connected Coder workspace agents because they sometimes appear after coder create returns. Streams file copy through OpenSSH ProxyCommand=coder ssh --stdio … so transfer goes via the Coder tunnel without mutating the user's ~/.ssh/config.
  • Tests: src/sandboxes/coder.test.ts — vitest behavior + type-level (@ts-expect-error) tests for required onClose and mutually exclusive template/workspace.
  • Package export: @ai-hero/sandcastle/sandboxes/coder.
  • Docs: README provider table row + Coder subsection; CONTEXT.md defines Coder workspace and Coder workspace agent (qualified to avoid colliding with Sandcastle's own "agent"); ADR 0009 (CLI-only strategy) and ADR 0010 (required onClose).
  • Changeset: patch.
  • Dogfood script: .sandcastle/test-coder.ts (npm run test-coder) launches three workspaces from one template — stop, delete, leave — and writes a JSON report under .sandcastle/logs/.

Validation

Automated:

npm run typecheck
npm test -- src/sandboxes/coder.test.ts   # 10 passed
npm test                                   # 1025 passed (with GIT_AUTHOR_* set per repo convention)

Dogfooded end-to-end against https://dev.coder.com using the coder / "Write Coder on Coder" template with the Pittsburgh preset:

sc-dogfood-05ce81-stop    -> stopped (verified via coder list --all)
sc-dogfood-05ce81-delete  -> missing/deleted
sc-dogfood-05ce81-leave   -> running, agent dev connected

A separate marker run (sc-marker-e33f9a-leave) created /home/coder/sandcastle-marker inside the Coder workspace, verified via coder ssh tk/sc-marker-e33f9a-leave.dev -- sh -c 'ls -l /home/coder':

-rw-r--r-- 1 coder coder 63 Apr 30 13:05 /home/coder/sandcastle-marker

Reports are checked in under .sandcastle/logs/coder-dogfood-*.json for reviewers.

Notes

  • The provider has no new npm peer dependency; the only runtime requirement is the coder CLI on PATH. A single coder whoami -o json preflight on each create() doubles as binary check, auth check, and diagnostic context capture.
  • interactiveExec forwards caller-provided stdio descriptors directly so coder ssh can do its normal TTY auto-detection — there is no --force-tty flag in the verified CLI.
  • v1 does not send a between-call dormancy heartbeat; active SSH sessions already keep last_used_at fresh while commands run.

📋 Implementation Plan

Plan: add a coder() sandbox provider

Context and goal

Add a built-in Sandcastle sandbox provider that integrates with Coder workspaces. Users should be able to either:

  1. Create a new Coder workspace from a template, optionally passing template parameters, a parameter file, a preset, and a template version; or
  2. Attach to an existing Coder workspace by name or ID.

In Sandcastle terminology this is an isolated sandbox provider: the Coder workspace has its own filesystem, so Sandcastle must sync code into it and copy files back out. Be careful with terminology:

  • Say Coder workspace when referring to Coder's external concept.
  • Say Coder workspace agent when referring to Coder's in-workspace daemon.
  • Do not use bare "workspace" as a synonym for Sandcastle sandbox or worktree.

Key design decisions

Provider category

Implement coder() as an IsolatedSandboxProvider, exported from @ai-hero/sandcastle/sandboxes/coder.

Rationale: a Coder workspace is remote/persistent and cannot bind-mount the host worktree.

Implementation strategy: use the coder CLI

Use the coder CLI for all provider operations rather than a REST SDK or custom tunnel implementation.

Reasons:

  • Coder command execution and file transfer go over the Coder SSH tunnel, not plain TCP SSH.
  • coder ssh already implements the tunnel, streams output, supports PTY allocation, and supports multi-agent selection via <workspace>.<agent>.
  • coder create / coder start block until the workspace is running and the workspace agent is connected.
  • coder list -o json gives enough structured state for agent discovery and attach-mode resolution.
  • There is no first-party TypeScript SDK to add as a peer dependency.

Create an ADR for this decision if it is not already present: docs/adr/0009-coder-provider-cli-strategy.md.

Required onClose

onClose is required and has no default:

type CoderOnClose = "delete" | "stop" | "leave";

Reason: Coder workspaces are persistent and potentially user-owned. Any default can surprise users: delete can destroy something valuable; leave can leak resources; mode-dependent defaults hide lifecycle behavior. Make the user choose explicitly.

Create an ADR for this decision if it is not already present: docs/adr/0010-coder-provider-required-onclose.md.

Public options shape

Use a top-level discriminated union based on template vs workspace, with ?: never exclusivity.

interface CoderCommonOptions {
  readonly url?: string;
  readonly token?: string;
  readonly env?: Record<string, string>;
  readonly onClose: "delete" | "stop" | "leave";
  readonly workspaceAgent?: string;
  readonly workdir?: string;
}

interface CoderCreateFromTemplateOptions extends CoderCommonOptions {
  readonly template: string;
  readonly workspace?: never;
  readonly templateVersion?: string;
  readonly parameters?: Record<string, string | number | boolean>;
  readonly parameterFile?: string;
  readonly preset?: string;
  readonly workspaceName?: string;
  readonly organization?: string;
}

interface CoderAttachToWorkspaceOptions extends CoderCommonOptions {
  readonly workspace: string;
  readonly template?: never;
  readonly owner?: string;
}

export type CoderOptions =
  | CoderCreateFromTemplateOptions
  | CoderAttachToWorkspaceOptions;

Details:

  • template and workspace accept either name or ID where the CLI supports it.
  • templateVersion is a CLI-compatible template version value; verify during the CLI spike whether IDs are accepted. If the CLI only accepts version names, document that constraint rather than claiming ID support.
  • parameters values are stringified before becoming --parameter key=value flags.
  • parameterFile passes through as --rich-parameter-file.
  • preset passes through as --preset and is kept separate from parameters.
  • organization is create-mode only; omit the CLI flag when undefined and let the CLI infer from auth context.
  • owner is attach-mode only; default to "me" for name-based lookup.
  • Generate workspaceName as sandcastle-<8-hex-chars> when omitted.

Implementation steps

0. Verify the CLI-only foundation before deep implementation

Before implementing the provider deeply, run a short CLI spike against the target coder CLI version and save representative outputs in notes or test fixtures used by the implementation work:

coder whoami -o json
coder list -o json
coder list -o json --search 'owner:me'
coder create --help
coder ssh --help

Confirm specifically:

  • coder list -o json includes latest_build.resources[].agents[] with at least name, status, and directory or enough information to derive a remote workdir.
  • Workspace lookup by name works through CLI JSON for owner/name or equivalent search semantics.
  • Workspace lookup by ID works through CLI JSON. If ID lookup is not available, narrow the v1 promise or revisit a minimal REST fallback before implementing attach-by-ID.
  • coder ssh <uuid> and coder ssh <uuid>.<agent> work. If SSH does not accept UUID refs, attach-by-ID must resolve UUID → owner/name before constructing sshRef.
  • coder create supports the planned flags: --template, --template-version, --parameter, --rich-parameter-file, --preset, --yes, and the exact organization flag if organization is provided.
  • coder create --template-version accepts the value form promised by CoderOptions; if IDs are not accepted, document templateVersion as a version name / CLI-compatible value only.
  • Multi-agent workspace JSON shape is understood. If no multi-agent test workspace is available, write the code defensively and document that the branch requires manual verification.

If the CLI JSON does not contain the agent/workdir data the provider needs, pause implementation and update this plan rather than silently adding brittle parsing of human-readable output.

1. Add source module

Create src/sandboxes/coder.ts.

Export:

  • CoderOptions
  • CoderCommonOptions
  • CoderCreateFromTemplateOptions
  • CoderAttachToWorkspaceOptions
  • CoderOnClose
  • coder(options: CoderOptions): IsolatedSandboxProvider

Use createIsolatedSandboxProvider({ name: "coder", env: options.env, create }).

2. Build CLI execution helpers

Add small internal helpers in coder.ts only; do not add abstractions unless needed by multiple code paths.

Suggested helpers:

  • coderEnv(options) — merges process.env with CODER_URL / CODER_SESSION_TOKEN when provided.
  • runCoder(args, opts?) — spawn coder, collect stdout/stderr, return exit code; optionally line-stream stdout.
  • runCoderJson<T>(args) — call runCoder, parse stdout as JSON, assert expected shape before use.
  • pipeProcesses(left, right) — for tar pipelines and binary stream copy.
  • shellQuote(value) — quote remote shell paths/commands safely; add focused tests or inline assertions for paths with spaces.
  • assertNonEmptyString, assertArray, etc. — use defensive assertions around parsed CLI JSON.

Preflight every provider create() call with:

coder whoami -o json

This catches missing binary, bad auth, and captures URL, Username, ID, Orgs, Roles for error context. Do not cache preflight results.

3. Create-mode flow

When template is present:

  1. Generate workspaceName if absent: sandcastle-<8 hex>.
  2. Build coder create args:
    • create <workspaceName>
    • --template <template>
    • --template-version <templateVersion> if present
    • --parameter key=value for every parameters entry, after String(value)
    • --rich-parameter-file <parameterFile> if present
    • --preset <preset> if present
    • organization flag if supported by the CLI and organization is present
    • --yes
  3. Let coder create block until ready. Do not pass --no-wait.
  4. Resolve the created workspace from coder list -o json so the handle knows the selected Coder workspace agent and workdir.
  5. If coder create succeeds but any later setup step fails before returning the handle (workspace resolution, Coder workspace agent selection, workdir creation, etc.), attempt cleanup according to onClose for the newly-created Coder workspace before rethrowing. Sandcastle will not call close() if create() never returns a handle.

4. Attach-mode flow

When workspace is present:

  1. Construct and validate the initial workspaceRef:
    • If workspace is a UUID, start with workspaceRef = workspace.
    • Else if owner is provided, use workspaceRef = owner + "/" + workspace.
    • Else use workspaceRef = workspace.
    • If workspace already contains / and owner is also provided, throw a configuration error rather than guessing.
  2. Resolve the workspace using coder list -o json and the provided ID or owner/name query semantics.
  3. If stopped, run coder start <workspaceRef> --yes and wait.
  4. If failed/deleting/deleted, fail with a clear error.
  5. After resolution, normalize workspaceRef to the exact value that lifecycle commands and SSH commands accept. If attach-by-ID required resolving UUID → owner/name, use the resolved owner/name form for workspaceRef and build sshRef from that.
  6. Resolve agents from latest_build.resources[].agents[].

5. Coder workspace agent selection

Use options.workspaceAgent when provided.

If omitted:

  • If exactly one Coder workspace agent exists, use it.
  • If multiple exist, throw an error listing the available agent names and telling the user to set workspaceAgent.
  • If none exist or no connected agent can be found after create/start, throw a clear error with workspace status context.

Use the CLI reference <workspace>.<agent> when a specific agent is needed.

6. Workspace refs and workdir selection

Keep two references distinct throughout the implementation:

  • workspaceRef — the Coder workspace reference used for lifecycle commands (coder start, coder stop, coder delete, coder list matching). Never append the agent name here.
  • sshRef — the reference used for SSH commands. If a Coder workspace agent is selected, this is <workspaceRef>.<workspaceAgent>; otherwise it is workspaceRef.

Compute the provider handle's worktreePath as an absolute path:

  1. options.workdir, if provided. Assert it is absolute; if it is relative, throw a clear configuration error.
  2. <selectedAgent.directory>/.sandcastle/worktree, if the selected Coder workspace agent declares an absolute directory.
  3. Otherwise, query the remote home directory with coder ssh <sshRef> -- sh -c 'printf %s "$HOME"', assert the result is absolute, and use <remoteHome>/.sandcastle/worktree.

Do not use ~/.sandcastle/worktree as worktreePath; ~ is shell syntax, not an absolute path. Ensure the directory exists with coder ssh <sshRef> -- mkdir -p <quoted workdir> before copy/exec use.

7. Implement exec

Use coder ssh <sshRef> -- sh -c <command>.

Requirements:

  • Respect opts.cwd by prefixing/constructing command so it runs from opts.cwd ?? worktreePath.
  • Respect opts.sudo by wrapping appropriately (match existing provider behavior as closely as possible).
  • Respect opts.stdin by piping it to the child process stdin and closing it.
  • Return { stdout, stderr, exitCode }.
  • If opts.onLine is present, call it for each stdout line in real time, not after buffering.

8. Implement interactiveExec

Implement in v1.

Use coder ssh <sshRef> -- ...args with piped stdio from InteractiveExecOptions.

Do not rely only on Coder's automatic TTY detection while stdio is piped through Node: the CLI may not see a TTY even when options.stdin.isTTY is true. If options.stdin.isTTY is true, pass Coder's force-PTY flag (verify exact flag during the CLI spike, expected --force-tty). If the implementation can safely inherit process stdio when the provided streams are exactly process.stdin/stdout/stderr, that is also acceptable, but keep behavior explicit and tested manually.

9. Implement file copy

Use streams through coder ssh, not coder config-ssh or scp. Avoid mutating the user's SSH config.

Be precise about Sandcastle's copy contracts:

  • copyIn(hostDir, sandboxPath) must copy the contents of hostDir into sandboxPath, matching the Vercel provider's directory semantics. It must not create sandboxPath/<hostBase>.
  • copyIn(hostFile, sandboxPath) must write the file to the exact sandboxPath, even when the destination basename differs.
  • copyFileOut(sandboxPath, hostPath) must write the remote file to the exact hostPath, even when the destination basename differs.

Recommended mechanics:

  • Directory copyIn: mkdir -p <sandboxPath>, then tar czf - -C <hostDir> . | coder ssh <sshRef> -- tar xzf - -C <quoted sandboxPath>.
  • File copyIn: mkdir -p <remote dirname>, then pipe a host read stream into coder ssh <sshRef> -- sh -c 'cat > <quoted sandboxPath>'.
  • File copyFileOut: mkdir -p <host dirname>, then pipe coder ssh <sshRef> -- sh -c 'cat < <quoted sandboxPath>' stdout into a host write stream for the exact hostPath.

For every pipeline, fail if either side exits non-zero and include both stderr streams in the thrown error. Use binary streams for file copy; do not route file contents through UTF-8 strings. Quote all remote paths with shellQuote(), including paths that contain spaces or begin with -. Copy exactly what Sandcastle provides; do not add exclusions or special .git handling.

10. Implement close

Use required onClose:

  • "delete"coder delete <workspaceRef> --yes
  • "stop"coder stop <workspaceRef> --yes
  • "leave" → no-op

Use the unqualified workspaceRef for lifecycle commands, never sshRef / <workspace>.<agent>.

If close() fails, surface stderr with enough workspace context. Avoid swallowing lifecycle errors unless matching existing provider behavior requires it.

11. Add package export

Edit package.json exports:

"./sandboxes/coder": {
  "import": "./dist/sandboxes/coder.js",
  "types": "./dist/sandboxes/coder.d.ts"
}

Do not add npm dependencies or peer dependencies.

12. Add tests

Create src/sandboxes/coder.test.ts following the lightweight provider test pattern.

Test at least:

  • coder({ template: "node", onClose: "delete" }) returns tag "isolated", name "coder".
  • coder({ workspace: "my-ws", onClose: "leave" }) returns tag "isolated", name "coder".
  • env is stored and defaults to {}.
  • Options accept parameters, parameterFile, preset, templateVersion, organization, workspaceAgent, workdir.
  • Use expectTypeOf or // @ts-expect-error tests to guard that passing both template and workspace is rejected and omitting onClose is rejected.

Do not run a real Coder workspace in the normal unit test suite.

13. Update docs

Update README.md:

  • Add a Coder row to the built-in providers table.
  • Add a brief ### Coder subsection under "Sandbox Providers" with:
    • CLI prerequisite: coder must be installed and authenticated.
    • Create-mode example.
    • Attach-mode example.
    • Required onClose explanation.
    • workspaceAgent note for multi-agent Coder workspaces.
    • Workdir default note.
    • Dormancy note: SSH exec keeps last_used_at fresh while in flight; no heartbeat between exec calls in v1.

Update CONTEXT.md only if the Coder workspace / Coder workspace agent terms are not already captured.

Ensure the two ADRs above exist and match the final decisions. In particular, verify the CLI-strategy ADR does not incorrectly cross-reference the onClose ADR for dormancy/heartbeat details.

14. Add changeset

Add .changeset/<descriptive-name>.md:

---
"@ai-hero/sandcastle": patch
---

Add a `coder()` isolated sandbox provider for Coder workspaces.

Dogfooding and validation

Automated validation

Run after implementation:

npm run typecheck
npm test -- src/sandboxes/coder.test.ts
npm test

If npm test -- src/sandboxes/coder.test.ts is not the repo's exact test invocation, use the existing Vitest command pattern from package.json.

Manual Coder smoke tests

Use a real Coder deployment or a local coder server instance.

Prerequisites:

coder version
coder whoami -o json

Create-mode smoke test:

  1. Use a simple template with one optional rich parameter, or an existing dev template.
  2. Run Sandcastle with:
sandbox: coder({
  template: "<template-name-or-id>",
  parameters: { example: "value" },
  onClose: "delete",
})
  1. Verify in terminal and Coder UI:
    • Workspace appears.
    • Command output streams.
    • Workspace is deleted at end.

Attach-mode smoke test:

  1. Pre-create a Coder workspace.
  2. Run Sandcastle with:
sandbox: coder({
  workspace: "<workspace-name-or-id>",
  onClose: "leave",
})
  1. Verify:
    • Provider uses existing Coder workspace.
    • Files land under <agent.directory>/.sandcastle/worktree unless workdir overrides.
    • Workspace remains after run.

Interactive smoke test:

  • Use interactive() with the Coder provider and verify input/output works through coder ssh.

File-copy smoke test:

  • Copy a directory with nested files into a differently named target directory; verify only the contents are copied into the target.
  • Copy a local file to a differently named remote file.
  • Copy a remote file back to a differently named host file.
  • Verify binary contents and filenames with spaces survive.

Failure-mode dogfood:

  • Missing coder on PATH → clear preflight error.
  • Bad token → clear preflight/auth error.
  • Multi-agent Coder workspace without workspaceAgent → clear error listing names.
  • Missing required template parameter → Coder stderr surfaces verbatim.

Capture screenshots of:

  • Successful create-mode run in terminal and Coder UI.
  • Successful attach-mode run in terminal and Coder UI.
  • At least one failure-mode error.

If possible, record a short terminal video of create-mode and attach-mode runs for reviewer verification.

Acceptance criteria

  • @ai-hero/sandcastle/sandboxes/coder exports coder() and its option types.
  • coder() returns an isolated sandbox provider and supports run(), createSandbox(), and interactive().
  • TypeScript rejects Coder options that pass both template and workspace.
  • TypeScript rejects Coder options that omit onClose.
  • Create mode can provision a Coder workspace from a template and pass parameters/presets/template version options to the CLI.
  • Attach mode can reuse an existing Coder workspace and start it if stopped.
  • Multi-agent Coder workspaces require explicit workspaceAgent unless exactly one agent exists.
  • exec streams stdout line-by-line via onLine in real time.
  • interactiveExec is implemented using coder ssh.
  • copyIn and copyFileOut work without mutating user SSH config.
  • close() honors delete, stop, and leave exactly.
  • README, changeset, CONTEXT terminology, and ADRs are updated.
  • Typecheck and tests pass, or any blocker is documented with exact failure output.

Risks and mitigations

  • CLI JSON shape changes: keep parsing narrow, assert fields defensively, and surface raw stderr/stdout on failure.
  • Command escaping: avoid shell interpolation where possible; pass args arrays to spawn; use sh -c only where required by Sandcastle's command contract and carefully quote cwd/workdir/remote file paths with a dedicated shellQuote() helper.
  • Tar availability: document that host and Coder workspace need tar; most Coder templates should satisfy this.
  • Long idle gaps between exec calls: no heartbeat in v1. Document that active SSH sessions bump Coder activity, but there is no between-call keepalive.
  • Workspace name collisions: auto-generate names when omitted; if user pins workspaceName, let Coder's existing-name error surface.

Generated with mux • Model: anthropic:claude-opus-4-7 • Thinking: max

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 30, 2026

@ThomasK33 is attempting to deploy a commit to the Matt Pocock's projects Team on Vercel.

A member of the Team first needs to authorize it.

ThomasK33 added 5 commits May 1, 2026 16:36
- `src/sandboxes/coder.ts`: new isolated provider for Coder workspaces
  via the `coder` CLI. Discriminated `CoderOptions` union (template vs
  workspace). Required `onClose: "delete" | "stop" | "leave"`. Polls
  for connected Coder workspace agents after create. Streams file copy
  through OpenSSH `ProxyCommand=coder ssh --stdio` so we never mutate
  the user's SSH config.
- Vitest behavior + type-level tests in `src/sandboxes/coder.test.ts`.
- `@ai-hero/sandcastle/sandboxes/coder` package export.
- `.sandcastle/test-coder.ts` dogfood script and `npm run test-coder`.
- `CONTEXT.md`: defines `Coder workspace` and `Coder workspace agent`.
- `README.md`: provider table row and Coder subsection.
- ADR 0009 (CLI strategy) and ADR 0010 (required `onClose`).
- Changeset (`patch`).

---
_Generated with [`mux`](https://github.com/coder/mux) • Model: `anthropic:claude-opus-4-7` • Thinking: `max`_
Surfaced by downstream dogfood (coder/agent-tty triage flow against
dev.coder.com), both with tests + ADR notes.

1. Coder prebuild claim race
   `coder list -o json` can briefly report a new prebuild claim's agent
   as `connected` while the prior agent is still shutting down. The
   first `coder ssh` lands on the disconnecting agent and fails with
   `error: agent is shutting down` ~3/3 times against a template
   preset that has `desired_prebuild_instances >= 1`. Add a
   `waitForSshReady` probe that runs `coder ssh -- printf ready` until
   the round-trip succeeds (60s budget, 2s interval) before the first
   real `coder ssh`. Use `endsWith("ready")` rather than equality
   because `coder ssh` may prepend release-candidate banner lines.

2. coder ssh does not propagate stdin EOF
   `coder ssh <ws> -- <cmd>` writes the calling shell's stdin into the
   remote process's stdin but never closes it. `claude --print -p -`
   then hangs forever waiting for input the host already sent. The
   same pipeline through OpenSSH with `ProxyCommand=coder ssh --stdio`
   exits cleanly, so route stdin-bearing `exec()` calls through
   `runOpenSsh` (the same transport copyFileIn/copyFileOut already
   use). Inline env vars as a `KEY='value' sh -c '<cmd>'` shell prefix
   because OpenSSH has no `--env` flag. Non-stdin `exec()` keeps the
   existing direct `coder ssh` path so we minimise blast radius.

Tests (vitest):

- `executes non-stdin commands through coder ssh ...` — locks in the
  unchanged path.
- `routes stdin-bearing exec through OpenSSH ProxyCommand to propagate
  EOF` — verifies the spawn target is `ssh` with the expected
  `ProxyCommand` and hostname when `stdin` is set.
- `retries SSH readiness probe when first attempt fails (prebuild
  claim race)` — first `printf ready` fails with `agent is shutting
  down`, second succeeds, `create()` resolves.

Existing ssh mocks updated with a small `isSshReadinessProbe` helper
so they return `"ready"` on the new probe.

Full suite (1027 passed) and typecheck pass.

---
_Generated with [`mux`](https://github.com/coder/mux) • Model: `anthropic:claude-opus-4-7` • Thinking: `max`_
Code-simplifier pass on the recently-added Coder provider helpers. No
behaviour change; 12 coder tests + full suite (1027 tests) green,
typecheck clean.

- runChildProcess(binary, args, options) is now the single source of
  truth for spawning `coder` / `ssh` and collecting stdout/stderr.
  `runCoder` and `runOpenSsh` are thin wrappers. Eliminates a ~60-line
  copy of the spawn/pipe/close lifecycle that landed in b69f536.
- displayCommand(binary, args) replaces the binary-specific
  displayOpenSshCommand helper.
- assertEnvKey(key) replaces the duplicated `assertNonEmptyString +
  includes("=")` validation block in buildSshArgs and buildEnvPrefix.
- exec() hoists `onStdoutLine = opts?.onLine` once and drops the
  `...(opts.onLine === undefined ? {} : { onStdoutLine: opts.onLine })`
  spread on both branches. The project doesn't enable
  exactOptionalPropertyTypes, so a plain `onStdoutLine` field works
  here. Comment near the OpenSSH branch is also tightened to point at
  the canonical EOF/transport explanation in runChildProcess.

Net: -48 lines, identical observable behaviour. The two transports
remain explicit at the call site (runCoder vs runOpenSsh) so the
intent of each branch stays clear.

---
_Generated with [\`mux\`](https://github.com/coder/mux) • Model: \`anthropic:claude-opus-4-7\` • Thinking: \`max\`_
Five comment blocks on the new Coder provider drifted into AI-style
"essay justifying every choice" territory. Trimmed to match the
project's existing comment density (vercel.ts, podman.ts, docker.ts
all keep block comments to ~1-2 lines):

- runChildProcess JSDoc: 9 lines → 5 lines, references upstream issue
  coder/coder#24861 instead of restating the bug.
- waitForSshReady JSDoc: 11 lines → 1 line that points at the
  SSH_READY_POLL_* constants comment which already explains the
  prebuild race.
- exec() stdin-routing comment: 4 lines → 1 line, drops the
  "minimise blast radius" meta-commentary.
- interactiveExec PTY comment: 3 lines → 2 lines.
- coder.test.ts isSshReadinessProbe JSDoc: 5 lines → 1 line.

No code change. 12 coder tests + typecheck still green.

---
_Generated with [\`mux\`](https://github.com/coder/mux) • Model: \`anthropic:claude-opus-4-7\` • Thinking: \`max\`_
Forwards `--use-parameter-defaults` to `coder create` when set, which
auto-accepts template parameter defaults instead of dropping the user
into an interactive prompt. Mirrors the env-var equivalent
`CODER_WORKSPACE_USE_PARAMETER_DEFAULTS=true` that the dogfood script
already uses.

- New `useParameterDefaults?: boolean` field on
  `CoderCreateFromTemplateOptions` (create-mode only — attach mode
  doesn't run `coder create`).
- `createCoderWorkspace` pushes the flag when truthy; absent flag keeps
  default interactive behaviour.
- TDD: vitest now has a focused test that runs the provider twice
  (with and without the option) and asserts the create argv contains
  / does not contain `--use-parameter-defaults`.
- Smoke test gains the field for compile-time coverage.
- README example shows the option.

Full suite: 1028 tests pass; typecheck clean.

---
_Generated with [\`mux\`](https://github.com/coder/mux) • Model: \`anthropic:claude-opus-4-7\` • Thinking: \`max\`_
@ThomasK33 ThomasK33 force-pushed the coder-provider-q30q branch from a9e73cd to 3ad3518 Compare May 1, 2026 16:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant