@ai-hero/sandcastle 0.6.4 → 0.6.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +125 -54
- package/dist/{MountConfig.d.ts → MountConfig-CmXclHA5.d.ts} +3 -2
- package/dist/{SandboxProvider.d.ts → SandboxProvider-EkSMuBp8.d.ts} +25 -39
- package/dist/chunk-72UVAC7B.js +99 -0
- package/dist/chunk-72UVAC7B.js.map +1 -0
- package/dist/chunk-BIWNFKGV.js +22 -0
- package/dist/chunk-BIWNFKGV.js.map +1 -0
- package/dist/chunk-NGBM7T3E.js +76 -0
- package/dist/chunk-NGBM7T3E.js.map +1 -0
- package/dist/chunk-Q5W3WQVU.js +25569 -0
- package/dist/chunk-Q5W3WQVU.js.map +1 -0
- package/dist/chunk-UPDEQ2U7.js +362 -0
- package/dist/chunk-UPDEQ2U7.js.map +1 -0
- package/dist/chunk-Z7O2WNRU.js +26934 -0
- package/dist/chunk-Z7O2WNRU.js.map +1 -0
- package/dist/index.d.ts +920 -22
- package/dist/index.js +3212 -9
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts +0 -2
- package/dist/main.js +19256 -13
- package/dist/main.js.map +1 -1
- package/dist/mountUtils-CCA-bbpK.d.ts +25 -0
- package/dist/sandboxes/daytona.d.ts +8 -5
- package/dist/sandboxes/daytona.js +118 -124
- package/dist/sandboxes/daytona.js.map +1 -1
- package/dist/sandboxes/docker.d.ts +10 -8
- package/dist/sandboxes/docker.js +8 -255
- package/dist/sandboxes/docker.js.map +1 -1
- package/dist/sandboxes/no-sandbox.d.ts +7 -4
- package/dist/sandboxes/no-sandbox.js +6 -114
- package/dist/sandboxes/no-sandbox.js.map +1 -1
- package/dist/sandboxes/podman.d.ts +10 -8
- package/dist/sandboxes/podman.js +287 -297
- package/dist/sandboxes/podman.js.map +1 -1
- package/dist/sandboxes/vercel.d.ts +7 -4
- package/dist/sandboxes/vercel.js +144 -165
- package/dist/sandboxes/vercel.js.map +1 -1
- package/package.json +15 -14
- package/dist/AgentProvider.d.ts +0 -134
- package/dist/AgentProvider.d.ts.map +0 -1
- package/dist/AgentProvider.js +0 -647
- package/dist/AgentProvider.js.map +0 -1
- package/dist/AgentStreamEmitter.d.ts +0 -36
- package/dist/AgentStreamEmitter.d.ts.map +0 -1
- package/dist/AgentStreamEmitter.js +0 -21
- package/dist/AgentStreamEmitter.js.map +0 -1
- package/dist/CopyToWorktree.d.ts +0 -15
- package/dist/CopyToWorktree.d.ts.map +0 -1
- package/dist/CopyToWorktree.js +0 -60
- package/dist/CopyToWorktree.js.map +0 -1
- package/dist/Display.d.ts +0 -58
- package/dist/Display.d.ts.map +0 -1
- package/dist/Display.js +0 -142
- package/dist/Display.js.map +0 -1
- package/dist/DockerLifecycle.d.ts +0 -54
- package/dist/DockerLifecycle.d.ts.map +0 -1
- package/dist/DockerLifecycle.js +0 -123
- package/dist/DockerLifecycle.js.map +0 -1
- package/dist/EnvResolver.d.ts +0 -11
- package/dist/EnvResolver.d.ts.map +0 -1
- package/dist/EnvResolver.js +0 -63
- package/dist/EnvResolver.js.map +0 -1
- package/dist/ErrorHandler.d.ts +0 -15
- package/dist/ErrorHandler.d.ts.map +0 -1
- package/dist/ErrorHandler.js +0 -85
- package/dist/ErrorHandler.js.map +0 -1
- package/dist/InitService.d.ts +0 -63
- package/dist/InitService.d.ts.map +0 -1
- package/dist/InitService.js +0 -733
- package/dist/InitService.js.map +0 -1
- package/dist/MountConfig.d.ts.map +0 -1
- package/dist/MountConfig.js +0 -7
- package/dist/MountConfig.js.map +0 -1
- package/dist/Orchestrator.d.ts +0 -56
- package/dist/Orchestrator.d.ts.map +0 -1
- package/dist/Orchestrator.js +0 -293
- package/dist/Orchestrator.js.map +0 -1
- package/dist/Output.d.ts +0 -107
- package/dist/Output.d.ts.map +0 -1
- package/dist/Output.js +0 -95
- package/dist/Output.js.map +0 -1
- package/dist/PodmanLifecycle.d.ts +0 -17
- package/dist/PodmanLifecycle.d.ts.map +0 -1
- package/dist/PodmanLifecycle.js +0 -45
- package/dist/PodmanLifecycle.js.map +0 -1
- package/dist/PromptArgumentSubstitution.d.ts +0 -32
- package/dist/PromptArgumentSubstitution.d.ts.map +0 -1
- package/dist/PromptArgumentSubstitution.js +0 -104
- package/dist/PromptArgumentSubstitution.js.map +0 -1
- package/dist/PromptPreprocessor.d.ts +0 -15
- package/dist/PromptPreprocessor.d.ts.map +0 -1
- package/dist/PromptPreprocessor.js +0 -55
- package/dist/PromptPreprocessor.js.map +0 -1
- package/dist/PromptResolver.d.ts +0 -21
- package/dist/PromptResolver.d.ts.map +0 -1
- package/dist/PromptResolver.js +0 -27
- package/dist/PromptResolver.js.map +0 -1
- package/dist/RecoveryMessage.d.ts +0 -15
- package/dist/RecoveryMessage.d.ts.map +0 -1
- package/dist/RecoveryMessage.js +0 -81
- package/dist/RecoveryMessage.js.map +0 -1
- package/dist/SandboxFactory.d.ts +0 -90
- package/dist/SandboxFactory.d.ts.map +0 -1
- package/dist/SandboxFactory.js +0 -324
- package/dist/SandboxFactory.js.map +0 -1
- package/dist/SandboxLifecycle.d.ts +0 -65
- package/dist/SandboxLifecycle.d.ts.map +0 -1
- package/dist/SandboxLifecycle.js +0 -296
- package/dist/SandboxLifecycle.js.map +0 -1
- package/dist/SandboxProvider.d.ts.map +0 -1
- package/dist/SandboxProvider.js +0 -28
- package/dist/SandboxProvider.js.map +0 -1
- package/dist/SessionStore.d.ts +0 -110
- package/dist/SessionStore.d.ts.map +0 -1
- package/dist/SessionStore.js +0 -330
- package/dist/SessionStore.js.map +0 -1
- package/dist/TextDeltaBuffer.d.ts +0 -24
- package/dist/TextDeltaBuffer.d.ts.map +0 -1
- package/dist/TextDeltaBuffer.js +0 -68
- package/dist/TextDeltaBuffer.js.map +0 -1
- package/dist/WorktreeManager.d.ts +0 -79
- package/dist/WorktreeManager.d.ts.map +0 -1
- package/dist/WorktreeManager.js +0 -283
- package/dist/WorktreeManager.js.map +0 -1
- package/dist/boundedTail.d.ts +0 -48
- package/dist/boundedTail.d.ts.map +0 -1
- package/dist/boundedTail.js +0 -64
- package/dist/boundedTail.js.map +0 -1
- package/dist/cli.d.ts +0 -30
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -309
- package/dist/cli.js.map +0 -1
- package/dist/createSandbox.d.ts +0 -154
- package/dist/createSandbox.d.ts.map +0 -1
- package/dist/createSandbox.js +0 -476
- package/dist/createSandbox.js.map +0 -1
- package/dist/createWorktree.d.ts +0 -154
- package/dist/createWorktree.d.ts.map +0 -1
- package/dist/createWorktree.js +0 -391
- package/dist/createWorktree.js.map +0 -1
- package/dist/errors.d.ts +0 -227
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js +0 -81
- package/dist/errors.js.map +0 -1
- package/dist/extractStructuredOutput.d.ts +0 -23
- package/dist/extractStructuredOutput.d.ts.map +0 -1
- package/dist/extractStructuredOutput.js +0 -102
- package/dist/extractStructuredOutput.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/interactive.d.ts +0 -74
- package/dist/interactive.d.ts.map +0 -1
- package/dist/interactive.js +0 -279
- package/dist/interactive.js.map +0 -1
- package/dist/main.d.ts.map +0 -1
- package/dist/mergeProviderEnv.d.ts +0 -13
- package/dist/mergeProviderEnv.d.ts.map +0 -1
- package/dist/mergeProviderEnv.js +0 -23
- package/dist/mergeProviderEnv.js.map +0 -1
- package/dist/mountUtils.d.ts +0 -146
- package/dist/mountUtils.d.ts.map +0 -1
- package/dist/mountUtils.js +0 -301
- package/dist/mountUtils.js.map +0 -1
- package/dist/raceAbortSignal.d.ts +0 -18
- package/dist/raceAbortSignal.d.ts.map +0 -1
- package/dist/raceAbortSignal.js +0 -32
- package/dist/raceAbortSignal.js.map +0 -1
- package/dist/resolveCwd.d.ts +0 -24
- package/dist/resolveCwd.d.ts.map +0 -1
- package/dist/resolveCwd.js +0 -32
- package/dist/resolveCwd.js.map +0 -1
- package/dist/resumePrecheck.d.ts +0 -26
- package/dist/resumePrecheck.d.ts.map +0 -1
- package/dist/resumePrecheck.js +0 -40
- package/dist/resumePrecheck.js.map +0 -1
- package/dist/run.d.ts +0 -216
- package/dist/run.d.ts.map +0 -1
- package/dist/run.js +0 -313
- package/dist/run.js.map +0 -1
- package/dist/sandboxExec.d.ts +0 -12
- package/dist/sandboxExec.d.ts.map +0 -1
- package/dist/sandboxExec.js +0 -26
- package/dist/sandboxExec.js.map +0 -1
- package/dist/sandboxes/daytona.d.ts.map +0 -1
- package/dist/sandboxes/docker.d.ts.map +0 -1
- package/dist/sandboxes/no-sandbox.d.ts.map +0 -1
- package/dist/sandboxes/podman.d.ts.map +0 -1
- package/dist/sandboxes/test-bind-mount.d.ts +0 -17
- package/dist/sandboxes/test-bind-mount.d.ts.map +0 -1
- package/dist/sandboxes/test-bind-mount.js +0 -92
- package/dist/sandboxes/test-bind-mount.js.map +0 -1
- package/dist/sandboxes/test-isolated.d.ts +0 -17
- package/dist/sandboxes/test-isolated.d.ts.map +0 -1
- package/dist/sandboxes/test-isolated.js +0 -98
- package/dist/sandboxes/test-isolated.js.map +0 -1
- package/dist/sandboxes/vercel.d.ts.map +0 -1
- package/dist/shutdownRegistry.d.ts +0 -30
- package/dist/shutdownRegistry.d.ts.map +0 -1
- package/dist/shutdownRegistry.js +0 -73
- package/dist/shutdownRegistry.js.map +0 -1
- package/dist/startSandbox.d.ts +0 -50
- package/dist/startSandbox.d.ts.map +0 -1
- package/dist/startSandbox.js +0 -117
- package/dist/startSandbox.js.map +0 -1
- package/dist/syncIn.d.ts +0 -24
- package/dist/syncIn.d.ts.map +0 -1
- package/dist/syncIn.js +0 -107
- package/dist/syncIn.js.map +0 -1
- package/dist/syncOut.d.ts +0 -27
- package/dist/syncOut.d.ts.map +0 -1
- package/dist/syncOut.js +0 -271
- package/dist/syncOut.js.map +0 -1
- package/dist/templates.d.ts +0 -2
- package/dist/templates.d.ts.map +0 -1
- package/dist/templates.js +0 -26
- package/dist/templates.js.map +0 -1
- package/dist/terminalCleanup.d.ts +0 -30
- package/dist/terminalCleanup.d.ts.map +0 -1
- package/dist/terminalCleanup.js +0 -37
- package/dist/terminalCleanup.js.map +0 -1
- package/dist/testSandbox.d.ts +0 -8
- package/dist/testSandbox.d.ts.map +0 -1
- package/dist/testSandbox.js +0 -109
- package/dist/testSandbox.js.map +0 -1
- package/dist/testSetup.d.ts +0 -2
- package/dist/testSetup.d.ts.map +0 -1
- package/dist/testSetup.js +0 -29
- package/dist/testSetup.js.map +0 -1
package/README.md
CHANGED
|
@@ -234,6 +234,12 @@ const result = await run({
|
|
|
234
234
|
// Idle timeout in seconds — resets whenever the agent produces output. Default: 600 (10 minutes)
|
|
235
235
|
idleTimeoutSeconds: 600,
|
|
236
236
|
|
|
237
|
+
// Grace window in seconds after the agent emits a completion signal but
|
|
238
|
+
// before its process has exited (a "hanging process" — typically a spawned
|
|
239
|
+
// `gh`/git child or MCP server keeping stdout open). Resets on every
|
|
240
|
+
// subsequent output line so trailing data is still captured. Default: 60
|
|
241
|
+
completionTimeoutSeconds: 60,
|
|
242
|
+
|
|
237
243
|
// Structured output — extract a typed payload from the agent's stdout.
|
|
238
244
|
// Requires maxIterations === 1 and the tag must appear in the prompt.
|
|
239
245
|
// output: Output.object({ tag: "result", schema: z.object({ answer: z.number() }) }),
|
|
@@ -341,18 +347,19 @@ if (closeResult.preservedWorktreePath) {
|
|
|
341
347
|
|
|
342
348
|
#### `SandboxRunOptions`
|
|
343
349
|
|
|
344
|
-
| Option
|
|
345
|
-
|
|
|
346
|
-
| `agent`
|
|
347
|
-
| `prompt`
|
|
348
|
-
| `promptFile`
|
|
349
|
-
| `promptArgs`
|
|
350
|
-
| `maxIterations`
|
|
351
|
-
| `completionSignal`
|
|
352
|
-
| `idleTimeoutSeconds`
|
|
353
|
-
| `
|
|
354
|
-
| `
|
|
355
|
-
| `
|
|
350
|
+
| Option | Type | Default | Description |
|
|
351
|
+
| -------------------------- | ------------------ | ----------------------------- | ------------------------------------------------------------------------------------ |
|
|
352
|
+
| `agent` | AgentProvider | — | **Required.** Agent provider (e.g. `claudeCode("claude-opus-4-7")`) |
|
|
353
|
+
| `prompt` | string | — | Inline prompt (mutually exclusive with `promptFile`) |
|
|
354
|
+
| `promptFile` | string | — | Path to prompt file (mutually exclusive with `prompt`) |
|
|
355
|
+
| `promptArgs` | PromptArgs | — | Key-value map for `{{KEY}}` placeholder substitution |
|
|
356
|
+
| `maxIterations` | number | `1` | Maximum iterations to run |
|
|
357
|
+
| `completionSignal` | string \| string[] | `<promise>COMPLETE</promise>` | String(s) the agent emits to stop the iteration loop early |
|
|
358
|
+
| `idleTimeoutSeconds` | number | `600` | Idle timeout in seconds — resets on each agent output event |
|
|
359
|
+
| `completionTimeoutSeconds` | number | `60` | Grace window after the completion signal is seen but the agent process hasn't exited |
|
|
360
|
+
| `name` | string | — | Display name for the run |
|
|
361
|
+
| `logging` | object | file (auto-generated) | `{ type: 'file', path }` or `{ type: 'stdout' }` |
|
|
362
|
+
| `signal` | AbortSignal | — | Cancels the run when aborted; handle stays usable afterward |
|
|
356
363
|
|
|
357
364
|
#### `SandboxRunResult`
|
|
358
365
|
|
|
@@ -459,22 +466,23 @@ await sandbox.close();
|
|
|
459
466
|
|
|
460
467
|
#### `WorktreeRunOptions`
|
|
461
468
|
|
|
462
|
-
| Option
|
|
463
|
-
|
|
|
464
|
-
| `agent`
|
|
465
|
-
| `sandbox`
|
|
466
|
-
| `prompt`
|
|
467
|
-
| `promptFile`
|
|
468
|
-
| `maxIterations`
|
|
469
|
-
| `completionSignal`
|
|
470
|
-
| `idleTimeoutSeconds`
|
|
471
|
-
| `
|
|
472
|
-
| `
|
|
473
|
-
| `
|
|
474
|
-
| `
|
|
475
|
-
| `
|
|
476
|
-
| `
|
|
477
|
-
| `
|
|
469
|
+
| Option | Type | Default | Description |
|
|
470
|
+
| -------------------------- | ---------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
|
471
|
+
| `agent` | AgentProvider | — | **Required.** Agent provider |
|
|
472
|
+
| `sandbox` | SandboxProvider | — | **Required.** Sandbox provider (AFK agents must be sandboxed) |
|
|
473
|
+
| `prompt` | string | — | Inline prompt (mutually exclusive with `promptFile`) |
|
|
474
|
+
| `promptFile` | string | — | Path to prompt file |
|
|
475
|
+
| `maxIterations` | number | 1 | Maximum iterations to run |
|
|
476
|
+
| `completionSignal` | string \| string[] | — | Substring(s) to stop the iteration loop early |
|
|
477
|
+
| `idleTimeoutSeconds` | number | 600 | Idle timeout in seconds |
|
|
478
|
+
| `completionTimeoutSeconds` | number | 60 | Grace window after completion signal is seen but agent process hasn't exited |
|
|
479
|
+
| `name` | string | — | Optional run name |
|
|
480
|
+
| `logging` | LoggingOption | file | Logging mode |
|
|
481
|
+
| `hooks` | SandboxHooks | — | Lifecycle hooks (`host.*`, `sandbox.*`) |
|
|
482
|
+
| `promptArgs` | PromptArgs | — | Key-value map for `{{KEY}}` placeholder substitution |
|
|
483
|
+
| `env` | Record<string, string> | — | Environment variables to inject into the sandbox |
|
|
484
|
+
| `resumeSession` | string | — | Resume a prior session by ID for agents that support resume. Incompatible with `maxIterations > 1`. Session file must exist on host. |
|
|
485
|
+
| `signal` | AbortSignal | — | Cancel the run when aborted. Kills the in-flight agent subprocess; the worktree is preserved on disk. Rejects with `signal.reason`. |
|
|
478
486
|
|
|
479
487
|
#### `WorktreeRunResult`
|
|
480
488
|
|
|
@@ -614,6 +622,25 @@ await run({
|
|
|
614
622
|
|
|
615
623
|
Tell the agent to output your chosen string(s) in the prompt, and the orchestrator will stop when it detects any of them. The matched signal is returned as `result.completionSignal`.
|
|
616
624
|
|
|
625
|
+
#### Hanging processes after the completion signal
|
|
626
|
+
|
|
627
|
+
The agent process is expected to exit shortly after emitting the completion signal. When a child it spawned — a `gh`/git subprocess, a long-lived MCP server, etc. — inherits the agent's stdout pipe and keeps it open, the parent process can linger long past its logical end. Sandcastle would otherwise wait for the full `idleTimeoutSeconds` and fail with `AgentIdleTimeoutError`, throwing away the commits the agent already made.
|
|
628
|
+
|
|
629
|
+
Instead, once the completion signal is observed in the output buffer, Sandcastle swaps in a short **completion timeout** (default 60 s). When it expires, the run resolves successfully with a warning that the process was hanging; `result.commits` and `result.completionSignal` are populated as if the process had exited cleanly. The timer resets on every subsequent output line, so trailing data emitted after the signal — token-usage events, terminal `result` events, a structured-output `<tag>` — is still captured.
|
|
630
|
+
|
|
631
|
+
A clean process exit always wins the race, so healthy runs gain zero added latency. The completion timeout only matters when the process hangs.
|
|
632
|
+
|
|
633
|
+
Tune the window with `completionTimeoutSeconds`:
|
|
634
|
+
|
|
635
|
+
```ts
|
|
636
|
+
await run({
|
|
637
|
+
// ...
|
|
638
|
+
completionTimeoutSeconds: 30, // shorter grace window
|
|
639
|
+
});
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
This is independent of `idleTimeoutSeconds`. They cover different phases: `idleTimeoutSeconds` runs **before** any signal is seen (genuinely stuck agent → fail); `completionTimeoutSeconds` runs **after** the signal is seen (hanging process → succeed with warning). See [ADR 0019](docs/adr/0019-completion-timeout-for-hanging-process.md).
|
|
643
|
+
|
|
617
644
|
### Structured output
|
|
618
645
|
|
|
619
646
|
Use `Output.object()` to extract a typed, schema-validated JSON payload from the agent's stdout. The agent emits its answer inside an XML tag you specify, and Sandcastle parses, validates, and returns it on `result.output`. The schema can be any [Standard Schema](https://standardschema.dev) validator — the examples below use [Zod](https://zod.dev), but Valibot, ArkType, and others work identically. See [ADR 0010](docs/adr/0010-structured-output.md) for design rationale.
|
|
@@ -682,6 +709,8 @@ Select a template during `sandcastle init` when prompted, or re-run init in a fr
|
|
|
682
709
|
|
|
683
710
|
Scaffolds the `.sandcastle/` config directory and builds the container image. This is the first command you run in a new repo. You choose a sandbox provider (Docker or Podman) during init — selecting Podman writes a `Containerfile` instead of `Dockerfile` and uses `sandcastle podman build-image` for the build step.
|
|
684
711
|
|
|
712
|
+
Init detects your host package manager (npm, pnpm, yarn, or bun) from a `packageManager` field or lockfile, defaulting to npm. Templates whose `main` file imports a host dependency — the planner templates import [Zod](https://zod.dev) for their `<plan>` output schema — prompt you to install it with that package manager when it isn't already in your `package.json`, so the first `npx tsx .sandcastle/main.ts` doesn't fail with `ERR_MODULE_NOT_FOUND`.
|
|
713
|
+
|
|
685
714
|
| Option | Required | Default | Description |
|
|
686
715
|
| -------------- | -------- | ---------------------------- | ---------------------------------------------------------------------------- |
|
|
687
716
|
| `--image-name` | No | `sandcastle:<repo-dir-name>` | Docker image name |
|
|
@@ -738,26 +767,27 @@ Removes the Podman image.
|
|
|
738
767
|
|
|
739
768
|
### `RunOptions`
|
|
740
769
|
|
|
741
|
-
| Option
|
|
742
|
-
|
|
|
743
|
-
| `agent`
|
|
744
|
-
| `sandbox`
|
|
745
|
-
| `cwd`
|
|
746
|
-
| `prompt`
|
|
747
|
-
| `promptFile`
|
|
748
|
-
| `maxIterations`
|
|
749
|
-
| `hooks`
|
|
750
|
-
| `name`
|
|
751
|
-
| `promptArgs`
|
|
752
|
-
| `branchStrategy`
|
|
753
|
-
| `copyToWorktree`
|
|
754
|
-
| `logging`
|
|
755
|
-
| `completionSignal`
|
|
756
|
-
| `idleTimeoutSeconds`
|
|
757
|
-
| `
|
|
758
|
-
| `
|
|
759
|
-
| `
|
|
760
|
-
| `
|
|
770
|
+
| Option | Type | Default | Description |
|
|
771
|
+
| -------------------------- | ------------------ | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
772
|
+
| `agent` | AgentProvider | — | **Required.** Agent provider (e.g. `claudeCode("claude-opus-4-7")`, `pi("claude-sonnet-4-6")`, `codex("gpt-5.4-mini")`, `cursor("composer-2")`, `opencode("opencode/big-pickle")`, `copilot("claude-sonnet-4.5")`) |
|
|
773
|
+
| `sandbox` | SandboxProvider | — | **Required.** Sandbox provider (e.g. `docker()`, `podman()`, `docker({ imageName: "sandcastle:local" })`) |
|
|
774
|
+
| `cwd` | string | `process.cwd()` | Host repo directory — anchor for `.sandcastle/` artifacts and git operations. Relative paths resolve against `process.cwd()`. |
|
|
775
|
+
| `prompt` | string | — | Inline prompt (mutually exclusive with `promptFile`) |
|
|
776
|
+
| `promptFile` | string | — | Path to prompt file (mutually exclusive with `prompt`). Resolves against `process.cwd()`, **not** `cwd`. |
|
|
777
|
+
| `maxIterations` | number | `1` | Maximum iterations to run |
|
|
778
|
+
| `hooks` | SandboxHooks | — | Lifecycle hooks (`host.*`, `sandbox.*`) |
|
|
779
|
+
| `name` | string | — | Display name for the run, shown as a prefix in log output |
|
|
780
|
+
| `promptArgs` | PromptArgs | — | Key-value map for `{{KEY}}` placeholder substitution |
|
|
781
|
+
| `branchStrategy` | BranchStrategy | per-provider default | Branch strategy: `{ type: 'head' }`, `{ type: 'merge-to-head' }`, or `{ type: 'branch', branch: '…' }` |
|
|
782
|
+
| `copyToWorktree` | string[] | — | Host-relative file paths to copy into the sandbox before start (not supported with `branchStrategy: { type: 'head' }`) |
|
|
783
|
+
| `logging` | object | file (auto-generated) | `{ type: 'file', path }` or `{ type: 'stdout' }` |
|
|
784
|
+
| `completionSignal` | string \| string[] | `<promise>COMPLETE</promise>` | String or array of strings the agent emits to stop the iteration loop early |
|
|
785
|
+
| `idleTimeoutSeconds` | number | `600` | Idle timeout in seconds — resets on each agent output event |
|
|
786
|
+
| `completionTimeoutSeconds` | number | `60` | Grace window in seconds after the completion signal is observed but the agent process has not exited (hanging process). See [Hanging processes after the completion signal](#hanging-processes-after-the-completion-signal). |
|
|
787
|
+
| `resumeSession` | string | — | Resume a prior session by ID for agents that support resume. Incompatible with `maxIterations > 1`. Session file must exist on host. |
|
|
788
|
+
| `signal` | AbortSignal | — | Cancel the run when aborted. Kills the in-flight agent subprocess and cancels lifecycle hooks; the worktree is preserved on disk. Rejects with `signal.reason`. |
|
|
789
|
+
| `timeouts` | Timeouts | — | Override default timeouts for built-in lifecycle steps: `copyToWorktreeMs` (60 000), `gitSetupMs` (10 000), `commitCollectionMs` (30 000), `mergeToHostMs` (30 000). |
|
|
790
|
+
| `output` | OutputDefinition | — | Structured output definition (`Output.object(…)` or `Output.string(…)`). Requires `maxIterations === 1`. See [Structured output](#structured-output). |
|
|
761
791
|
|
|
762
792
|
### `RunResult`
|
|
763
793
|
|
|
@@ -790,13 +820,13 @@ Removes the Podman image.
|
|
|
790
820
|
|
|
791
821
|
### Session capture
|
|
792
822
|
|
|
793
|
-
After each resumable provider iteration, Sandcastle automatically captures the agent's session file from the sandbox to the host. Claude Code sessions are stored under `~/.claude/projects/<encoded-path>/<session-id>.jsonl`; Codex sessions are stored under `~/.codex/sessions/YYYY/MM/DD/rollout-*-<session-id>.jsonl`. Any provider-specific `cwd` fields are rewritten to match the host repo root, so the provider's native resume command works.
|
|
823
|
+
After each resumable provider iteration, Sandcastle automatically captures the agent's session file from the sandbox to the host. Claude Code sessions are stored under `~/.claude/projects/<encoded-path>/<session-id>.jsonl`; Codex sessions are stored under `~/.codex/sessions/YYYY/MM/DD/rollout-*-<session-id>.jsonl`; Pi sessions are stored under `~/.pi/agent/sessions/--<encoded-cwd>--/<timestamp>_<session-id>.jsonl`. Any provider-specific `cwd` fields are rewritten to match the host repo root, so the provider's native resume command works.
|
|
794
824
|
|
|
795
|
-
Session capture is enabled by default for `claudeCode()` and `
|
|
825
|
+
Session capture is enabled by default for `claudeCode()`, `codex()`, and `pi()` and can be opted out via `captureSessions: false`. Providers without `sessionStorage` do not attempt capture. Capture failure fails the run.
|
|
796
826
|
|
|
797
827
|
### Session resume
|
|
798
828
|
|
|
799
|
-
Pass `resumeSession` to `run()` to continue a prior Claude Code or
|
|
829
|
+
Pass `resumeSession` to `run()` to continue a prior Claude Code, Codex, or Pi conversation inside a new sandbox:
|
|
800
830
|
|
|
801
831
|
```typescript
|
|
802
832
|
const result = await run({
|
|
@@ -819,9 +849,9 @@ const first = await run({
|
|
|
819
849
|
const second = await first.resume?.("Now implement the plan");
|
|
820
850
|
```
|
|
821
851
|
|
|
822
|
-
`resume` is present only on results from resumable providers (Claude Code, Codex) — hence the optional-chaining call.
|
|
852
|
+
`resume` is present only on results from resumable providers (Claude Code, Codex, Pi) — hence the optional-chaining call.
|
|
823
853
|
|
|
824
|
-
Before the sandbox starts, Sandcastle validates that the session file exists on the host and transfers it into the sandbox with `cwd` fields rewritten to match the sandbox-side path. Claude Code receives `--resume <id>`; Codex receives `codex exec resume <id>` with the prompt piped over stdin
|
|
854
|
+
Before the sandbox starts, Sandcastle validates that the session file exists on the host and transfers it into the sandbox with `cwd` fields rewritten to match the sandbox-side path. Claude Code receives `--resume <id>`; Codex receives `codex exec resume <id>` with the prompt piped over stdin; Pi receives `--session <id>`.
|
|
825
855
|
|
|
826
856
|
Constraints:
|
|
827
857
|
|
|
@@ -830,6 +860,33 @@ Constraints:
|
|
|
830
860
|
- Only iteration 1 receives the resume flag; subsequent iterations (if any) start fresh.
|
|
831
861
|
- Providers without resume support reject `resumeSession`.
|
|
832
862
|
|
|
863
|
+
### Session fork
|
|
864
|
+
|
|
865
|
+
`RunResult.fork(prompt, options?)` is the sibling of `.resume()`: it continues from the last captured session but leaves the parent session JSONL untouched and writes the child under a new session id. The mechanism is the agent's native fork flag — `claude --resume <id> --fork-session` for Claude Code, `codex exec fork <id>` for Codex.
|
|
866
|
+
|
|
867
|
+
Fork enables fan-out workflows where a single parent run is the starting point for several independent children:
|
|
868
|
+
|
|
869
|
+
```typescript
|
|
870
|
+
const parent = await run({
|
|
871
|
+
agent: claudeCode("claude-opus-4-7"),
|
|
872
|
+
sandbox: docker(),
|
|
873
|
+
prompt: "Read the codebase and summarise the data model",
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
const [reviewA, reviewB] = await Promise.all([
|
|
877
|
+
parent.fork?.("Review the migration plan", {
|
|
878
|
+
branchStrategy: { type: "branch", branch: "review-a" },
|
|
879
|
+
}),
|
|
880
|
+
parent.fork?.("Audit the auth layer", {
|
|
881
|
+
branchStrategy: { type: "branch", branch: "review-b" },
|
|
882
|
+
}),
|
|
883
|
+
]);
|
|
884
|
+
```
|
|
885
|
+
|
|
886
|
+
**Fork is session-only.** `--fork-session` and `codex exec fork` isolate the agent session JSONL — they do **not** isolate the branch, worktree, or sandbox. Safe concurrent fan-out (`Promise.all([r.fork(a), r.fork(b)])`) requires the caller to give each child a distinct `branch` via `branchStrategy: { type: "branch", branch: "..." }`. The default `head` and `merge-to-head` strategies are **not** safe for concurrent forks: `head` shares the host working directory across all children, and `merge-to-head` races `git merge` against the same HEAD. See [ADR 0018](docs/adr/0018-fork-is-session-only.md).
|
|
887
|
+
|
|
888
|
+
`fork` is present only on results from providers with `sessionStorage` (Claude Code, Codex) — hence the optional-chaining call. The same single-iteration and session-file constraints as `.resume()` apply.
|
|
889
|
+
|
|
833
890
|
### `ClaudeCodeOptions`
|
|
834
891
|
|
|
835
892
|
The `claudeCode()` factory accepts an optional second argument for provider-specific options:
|
|
@@ -858,6 +915,20 @@ agent: codex("gpt-5.4", { effort: "high" });
|
|
|
858
915
|
| `env` | `Record<string, string>` | `{}` | Environment variables injected by this agent provider |
|
|
859
916
|
| `captureSessions` | `boolean` | `true` | Capture Codex rollout JSONL to host for resume |
|
|
860
917
|
|
|
918
|
+
### `PiOptions`
|
|
919
|
+
|
|
920
|
+
The `pi()` factory accepts an optional second argument for provider-specific options:
|
|
921
|
+
|
|
922
|
+
```typescript
|
|
923
|
+
agent: pi("claude-sonnet-4-6", { thinking: "high" });
|
|
924
|
+
```
|
|
925
|
+
|
|
926
|
+
| Option | Type | Default | Description |
|
|
927
|
+
| ----------------- | ------------------------------------------------------------------------ | ------- | -------------------------------------------------------- |
|
|
928
|
+
| `thinking` | `"off"` \| `"minimal"` \| `"low"` \| `"medium"` \| `"high"` \| `"xhigh"` | — | Pi reasoning effort level via the `--thinking` flag |
|
|
929
|
+
| `env` | `Record<string, string>` | `{}` | Environment variables injected by this agent provider |
|
|
930
|
+
| `captureSessions` | `boolean` | `true` | Capture pi session JSONL to host for `pi --session <id>` |
|
|
931
|
+
|
|
861
932
|
### Provider `env`
|
|
862
933
|
|
|
863
934
|
Both **agent providers** and **sandbox providers** accept an optional `env: Record<string, string>` in their options. These environment variables are merged with the `.sandcastle/.env` resolver output at launch time:
|
|
@@ -1250,7 +1321,7 @@ hooks: {
|
|
|
1250
1321
|
|
|
1251
1322
|
```bash
|
|
1252
1323
|
npm install
|
|
1253
|
-
npm run build #
|
|
1324
|
+
npm run build # Bundle with tsup
|
|
1254
1325
|
npm test # Run tests with vitest
|
|
1255
1326
|
npm run typecheck # Type-check
|
|
1256
1327
|
```
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Each entry describes a host directory to mount into the sandbox container.
|
|
5
5
|
*/
|
|
6
6
|
/** A single bind-mount descriptor for docker()/podman() providers. */
|
|
7
|
-
|
|
7
|
+
interface MountConfig {
|
|
8
8
|
/**
|
|
9
9
|
* Path on the host. Supports:
|
|
10
10
|
* - Absolute paths (`/data/cache`)
|
|
@@ -22,4 +22,5 @@ export interface MountConfig {
|
|
|
22
22
|
/** Mount as read-only. Defaults to `false`. */
|
|
23
23
|
readonly readonly?: boolean;
|
|
24
24
|
}
|
|
25
|
-
|
|
25
|
+
|
|
26
|
+
export type { MountConfig as M };
|
|
@@ -5,20 +5,20 @@
|
|
|
5
5
|
* handles worktree creation, git mount resolution, and commit extraction.
|
|
6
6
|
*/
|
|
7
7
|
/** Result of executing a command inside a sandbox. */
|
|
8
|
-
|
|
8
|
+
interface ExecResult {
|
|
9
9
|
readonly stdout: string;
|
|
10
10
|
readonly stderr: string;
|
|
11
11
|
readonly exitCode: number;
|
|
12
12
|
}
|
|
13
13
|
/** Options for interactiveExec — the streams the provider should wire to the spawned process. */
|
|
14
|
-
|
|
14
|
+
interface InteractiveExecOptions {
|
|
15
15
|
readonly stdin: NodeJS.ReadableStream;
|
|
16
16
|
readonly stdout: NodeJS.WritableStream;
|
|
17
17
|
readonly stderr: NodeJS.WritableStream;
|
|
18
18
|
readonly cwd?: string;
|
|
19
19
|
}
|
|
20
20
|
/** Handle to a running bind-mount sandbox. */
|
|
21
|
-
|
|
21
|
+
interface BindMountSandboxHandle {
|
|
22
22
|
/** Absolute path to the worktree inside the sandbox. */
|
|
23
23
|
readonly worktreePath: string;
|
|
24
24
|
/**
|
|
@@ -56,7 +56,7 @@ export interface BindMountSandboxHandle {
|
|
|
56
56
|
close(): Promise<void>;
|
|
57
57
|
}
|
|
58
58
|
/** Options passed to a bind-mount provider's `create` function. */
|
|
59
|
-
|
|
59
|
+
interface BindMountCreateOptions {
|
|
60
60
|
/** Host-side path to the worktree directory. */
|
|
61
61
|
readonly worktreePath: string;
|
|
62
62
|
/** Host-side path to the original repo root. */
|
|
@@ -71,7 +71,7 @@ export interface BindMountCreateOptions {
|
|
|
71
71
|
readonly env: Record<string, string>;
|
|
72
72
|
}
|
|
73
73
|
/** Configuration for createBindMountSandboxProvider. */
|
|
74
|
-
|
|
74
|
+
interface BindMountSandboxProviderConfig {
|
|
75
75
|
/** Human-readable name for this provider (e.g. "docker", "podman"). */
|
|
76
76
|
readonly name: string;
|
|
77
77
|
/** Environment variables injected by this provider. Merged at launch time. */
|
|
@@ -86,7 +86,7 @@ export interface BindMountSandboxProviderConfig {
|
|
|
86
86
|
readonly create: (options: BindMountCreateOptions) => Promise<BindMountSandboxHandle>;
|
|
87
87
|
}
|
|
88
88
|
/** Handle to a running isolated sandbox (extends bind-mount with file transfer). */
|
|
89
|
-
|
|
89
|
+
interface IsolatedSandboxHandle {
|
|
90
90
|
/** Absolute path to the worktree inside the sandbox. */
|
|
91
91
|
readonly worktreePath: string;
|
|
92
92
|
/**
|
|
@@ -124,12 +124,12 @@ export interface IsolatedSandboxHandle {
|
|
|
124
124
|
close(): Promise<void>;
|
|
125
125
|
}
|
|
126
126
|
/** Options passed to an isolated provider's `create` function. */
|
|
127
|
-
|
|
127
|
+
interface IsolatedCreateOptions {
|
|
128
128
|
/** Environment variables to inject into the sandbox. */
|
|
129
129
|
readonly env: Record<string, string>;
|
|
130
130
|
}
|
|
131
131
|
/** Configuration for createIsolatedSandboxProvider. */
|
|
132
|
-
|
|
132
|
+
interface IsolatedSandboxProviderConfig {
|
|
133
133
|
/** Human-readable name for this provider (e.g. "daytona", "e2b"). */
|
|
134
134
|
readonly name: string;
|
|
135
135
|
/** Environment variables injected by this provider. Merged at launch time. */
|
|
@@ -138,9 +138,7 @@ export interface IsolatedSandboxProviderConfig {
|
|
|
138
138
|
readonly create: (options: IsolatedCreateOptions) => Promise<IsolatedSandboxHandle>;
|
|
139
139
|
}
|
|
140
140
|
/** A bind-mount sandbox provider. */
|
|
141
|
-
|
|
142
|
-
/** @internal Discriminator for internal dispatch. */
|
|
143
|
-
readonly tag: "bind-mount";
|
|
141
|
+
interface BindMountSandboxProvider {
|
|
144
142
|
/** Human-readable provider name. */
|
|
145
143
|
readonly name: string;
|
|
146
144
|
/** Environment variables injected by this provider. */
|
|
@@ -150,22 +148,16 @@ export interface BindMountSandboxProvider {
|
|
|
150
148
|
* `undefined` when the provider does not declare a sandbox home directory.
|
|
151
149
|
*/
|
|
152
150
|
readonly sandboxHomedir: string | undefined;
|
|
153
|
-
/** @internal Create a sandbox handle. */
|
|
154
|
-
readonly create: (options: BindMountCreateOptions) => Promise<BindMountSandboxHandle>;
|
|
155
151
|
}
|
|
156
152
|
/** An isolated sandbox provider. */
|
|
157
|
-
|
|
158
|
-
/** @internal Discriminator for internal dispatch. */
|
|
159
|
-
readonly tag: "isolated";
|
|
153
|
+
interface IsolatedSandboxProvider {
|
|
160
154
|
/** Human-readable provider name. */
|
|
161
155
|
readonly name: string;
|
|
162
156
|
/** Environment variables injected by this provider. */
|
|
163
157
|
readonly env: Record<string, string>;
|
|
164
|
-
/** @internal Create an isolated sandbox handle. */
|
|
165
|
-
readonly create: (options: IsolatedCreateOptions) => Promise<IsolatedSandboxHandle>;
|
|
166
158
|
}
|
|
167
159
|
/** Handle to a no-sandbox session — runs commands directly on the host. */
|
|
168
|
-
|
|
160
|
+
interface NoSandboxHandle {
|
|
169
161
|
/** Absolute path to the worktree on the host. */
|
|
170
162
|
readonly worktreePath: string;
|
|
171
163
|
/**
|
|
@@ -194,29 +186,22 @@ export interface NoSandboxHandle {
|
|
|
194
186
|
close(): Promise<void>;
|
|
195
187
|
}
|
|
196
188
|
/** A no-sandbox provider — runs the agent directly on the host with no container isolation. */
|
|
197
|
-
|
|
198
|
-
/** @internal Discriminator for internal dispatch. */
|
|
199
|
-
readonly tag: "none";
|
|
189
|
+
interface NoSandboxProvider {
|
|
200
190
|
/** Human-readable provider name. */
|
|
201
191
|
readonly name: string;
|
|
202
192
|
/** Environment variables injected by this provider. */
|
|
203
193
|
readonly env: Record<string, string>;
|
|
204
|
-
/** @internal Create a no-sandbox handle. */
|
|
205
|
-
readonly create: (options: {
|
|
206
|
-
readonly worktreePath: string;
|
|
207
|
-
readonly env: Record<string, string>;
|
|
208
|
-
}) => Promise<NoSandboxHandle>;
|
|
209
194
|
}
|
|
210
195
|
/** Head strategy: agent writes directly to host working directory. Bind-mount only. */
|
|
211
|
-
|
|
196
|
+
interface HeadBranchStrategy {
|
|
212
197
|
readonly type: "head";
|
|
213
198
|
}
|
|
214
199
|
/** Merge-to-head strategy: temp branch, merge back to HEAD, delete temp branch. */
|
|
215
|
-
|
|
200
|
+
interface MergeToHeadBranchStrategy {
|
|
216
201
|
readonly type: "merge-to-head";
|
|
217
202
|
}
|
|
218
203
|
/** Branch strategy: commits land on an explicit named branch. */
|
|
219
|
-
|
|
204
|
+
interface NamedBranchStrategy {
|
|
220
205
|
readonly type: "branch";
|
|
221
206
|
readonly branch: string;
|
|
222
207
|
/**
|
|
@@ -228,30 +213,31 @@ export interface NamedBranchStrategy {
|
|
|
228
213
|
readonly baseBranch?: string;
|
|
229
214
|
}
|
|
230
215
|
/** Branch strategy for bind-mount providers (all three variants). */
|
|
231
|
-
|
|
216
|
+
type BindMountBranchStrategy = HeadBranchStrategy | MergeToHeadBranchStrategy | NamedBranchStrategy;
|
|
232
217
|
/** Branch strategy for isolated providers (no head — can't write to host). */
|
|
233
|
-
|
|
218
|
+
type IsolatedBranchStrategy = MergeToHeadBranchStrategy | NamedBranchStrategy;
|
|
234
219
|
/** Branch strategy for no-sandbox providers (all three — same as bind-mount). */
|
|
235
|
-
|
|
220
|
+
type NoSandboxBranchStrategy = HeadBranchStrategy | MergeToHeadBranchStrategy | NamedBranchStrategy;
|
|
236
221
|
/** Union of all branch strategy variants. */
|
|
237
|
-
|
|
222
|
+
type BranchStrategy = BindMountBranchStrategy | IsolatedBranchStrategy | NoSandboxBranchStrategy;
|
|
238
223
|
/**
|
|
239
224
|
* A sandbox provider — the pluggable unit that `run()`, `interactive()`, and
|
|
240
225
|
* `createSandbox()` accept. Tagged for internal dispatch: "bind-mount",
|
|
241
226
|
* "isolated", or "none". When `NoSandboxProvider` is used, the agent runs
|
|
242
227
|
* directly on the host with no container isolation — opt in at your own risk.
|
|
243
228
|
*/
|
|
244
|
-
|
|
229
|
+
type SandboxProvider = BindMountSandboxProvider | IsolatedSandboxProvider | NoSandboxProvider;
|
|
245
230
|
/** @deprecated Use `SandboxProvider` — it now includes `NoSandboxProvider`. */
|
|
246
|
-
|
|
231
|
+
type AnySandboxProvider = SandboxProvider;
|
|
247
232
|
/**
|
|
248
233
|
* Create a bind-mount sandbox provider from a config object.
|
|
249
234
|
* The returned provider can be passed to `run()` or `createSandbox()`.
|
|
250
235
|
*/
|
|
251
|
-
|
|
236
|
+
declare const createBindMountSandboxProvider: (config: BindMountSandboxProviderConfig) => BindMountSandboxProvider;
|
|
252
237
|
/**
|
|
253
238
|
* Create an isolated sandbox provider from a config object.
|
|
254
239
|
* The returned provider can be passed to `run()` or `createSandbox()`.
|
|
255
240
|
*/
|
|
256
|
-
|
|
257
|
-
|
|
241
|
+
declare const createIsolatedSandboxProvider: (config: IsolatedSandboxProviderConfig) => IsolatedSandboxProvider;
|
|
242
|
+
|
|
243
|
+
export { type AnySandboxProvider as A, type BindMountSandboxHandle as B, type ExecResult as E, type HeadBranchStrategy as H, type IsolatedSandboxProvider as I, type MergeToHeadBranchStrategy as M, type NoSandboxProvider as N, type SandboxProvider as S, type BranchStrategy as a, type NamedBranchStrategy as b, type BindMountBranchStrategy as c, type BindMountCreateOptions as d, type BindMountSandboxProvider as e, type BindMountSandboxProviderConfig as f, type InteractiveExecOptions as g, type IsolatedBranchStrategy as h, type IsolatedCreateOptions as i, type IsolatedSandboxHandle as j, type IsolatedSandboxProviderConfig as k, type NoSandboxBranchStrategy as l, type NoSandboxHandle as m, createBindMountSandboxProvider as n, createIsolatedSandboxProvider as o };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { MAX_TAIL_CHARS, BoundedTail } from './chunk-NGBM7T3E.js';
|
|
3
|
+
import { spawn } from 'child_process';
|
|
4
|
+
import { createInterface } from 'readline';
|
|
5
|
+
|
|
6
|
+
createRequire(import.meta.url);
|
|
7
|
+
var noSandbox = (options) => ({
|
|
8
|
+
tag: "none",
|
|
9
|
+
name: "no-sandbox",
|
|
10
|
+
env: options?.env ?? {},
|
|
11
|
+
create: async (createOptions) => {
|
|
12
|
+
const worktreePath = createOptions.worktreePath;
|
|
13
|
+
const processEnv = { ...process.env, ...createOptions.env };
|
|
14
|
+
const maxOutputTailChars = options?.maxOutputTailChars ?? MAX_TAIL_CHARS;
|
|
15
|
+
const handle = {
|
|
16
|
+
worktreePath,
|
|
17
|
+
exec: (command, opts) => {
|
|
18
|
+
const cwd = opts?.cwd ?? worktreePath;
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
const proc = spawn("sh", ["-c", command], {
|
|
21
|
+
cwd,
|
|
22
|
+
env: processEnv,
|
|
23
|
+
stdio: [
|
|
24
|
+
opts?.stdin !== void 0 ? "pipe" : "ignore",
|
|
25
|
+
"pipe",
|
|
26
|
+
"pipe"
|
|
27
|
+
]
|
|
28
|
+
});
|
|
29
|
+
if (opts?.stdin !== void 0) {
|
|
30
|
+
proc.stdin.write(opts.stdin);
|
|
31
|
+
proc.stdin.end();
|
|
32
|
+
}
|
|
33
|
+
proc.on("error", (error) => {
|
|
34
|
+
reject(new Error(`exec failed: ${error.message}`));
|
|
35
|
+
});
|
|
36
|
+
if (opts?.onLine) {
|
|
37
|
+
const onLine = opts.onLine;
|
|
38
|
+
const stdoutTail = new BoundedTail(maxOutputTailChars, "\n");
|
|
39
|
+
const stderrTail = new BoundedTail(maxOutputTailChars, "");
|
|
40
|
+
const rl = createInterface({ input: proc.stdout });
|
|
41
|
+
rl.on("line", (line) => {
|
|
42
|
+
stdoutTail.push(line);
|
|
43
|
+
onLine(line);
|
|
44
|
+
});
|
|
45
|
+
proc.stderr.on("data", (chunk) => {
|
|
46
|
+
stderrTail.push(chunk.toString());
|
|
47
|
+
});
|
|
48
|
+
proc.on("close", (code) => {
|
|
49
|
+
resolve({
|
|
50
|
+
stdout: stdoutTail.toString(),
|
|
51
|
+
stderr: stderrTail.toString(),
|
|
52
|
+
exitCode: code ?? 0
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
} else {
|
|
56
|
+
const stdoutChunks = [];
|
|
57
|
+
const stderrChunks = [];
|
|
58
|
+
proc.stdout.on("data", (chunk) => {
|
|
59
|
+
stdoutChunks.push(chunk.toString());
|
|
60
|
+
});
|
|
61
|
+
proc.stderr.on("data", (chunk) => {
|
|
62
|
+
stderrChunks.push(chunk.toString());
|
|
63
|
+
});
|
|
64
|
+
proc.on("close", (code) => {
|
|
65
|
+
resolve({
|
|
66
|
+
stdout: stdoutChunks.join(""),
|
|
67
|
+
stderr: stderrChunks.join(""),
|
|
68
|
+
exitCode: code ?? 0
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
},
|
|
74
|
+
interactiveExec: (args, opts) => {
|
|
75
|
+
return new Promise((resolve, reject) => {
|
|
76
|
+
const [cmd, ...rest] = args;
|
|
77
|
+
const proc = spawn(cmd, rest, {
|
|
78
|
+
cwd: opts.cwd ?? worktreePath,
|
|
79
|
+
env: processEnv,
|
|
80
|
+
stdio: [opts.stdin, opts.stdout, opts.stderr]
|
|
81
|
+
});
|
|
82
|
+
proc.on("error", (error) => {
|
|
83
|
+
reject(new Error(`exec failed: ${error.message}`));
|
|
84
|
+
});
|
|
85
|
+
proc.on("close", (code) => {
|
|
86
|
+
resolve({ exitCode: code ?? 0 });
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
},
|
|
90
|
+
close: async () => {
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
return handle;
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
export { noSandbox };
|
|
98
|
+
//# sourceMappingURL=chunk-72UVAC7B.js.map
|
|
99
|
+
//# sourceMappingURL=chunk-72UVAC7B.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/sandboxes/no-sandbox.ts"],"names":[],"mappings":"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","file":"chunk-72UVAC7B.js","sourcesContent":["/**\n * No-sandbox provider — runs the agent directly on the host with no container isolation.\n *\n * Usage:\n * import { noSandbox } from \"sandcastle/sandboxes/no-sandbox\";\n * await interactive({ agent: claudeCode(\"claude-opus-4-7\"), sandbox: noSandbox() });\n *\n * Accepted by `run()`, `interactive()`, and `createSandbox()`. Skips\n * container isolation entirely — the agent executes on the host. Does not\n * pass `--dangerously-skip-permissions` to the agent — the user manages\n * permissions themselves.\n */\n\nimport { spawn, type StdioOptions } from \"node:child_process\";\nimport { createInterface } from \"node:readline\";\nimport type {\n NoSandboxProvider,\n NoSandboxHandle,\n ExecResult,\n InteractiveExecOptions,\n} from \"../SandboxProvider.js\";\nimport { BoundedTail, MAX_TAIL_CHARS } from \"../boundedTail.js\";\n\nexport interface NoSandboxOptions {\n /** Environment variables injected by this provider. Merged at launch time. */\n readonly env?: Record<string, string>;\n /**\n * Maximum number of characters of streamed `exec` output retained per stream\n * (stdout and stderr) when an `onLine` callback is supplied (default: 64KiB).\n *\n * Output is delivered live to `onLine` regardless; this only bounds the tail\n * returned in `ExecResult`, preventing a long-running agent's output from\n * overflowing V8's max string length and crashing the run.\n */\n readonly maxOutputTailChars?: number;\n}\n\n/**\n * Create a no-sandbox provider.\n *\n * The returned provider runs the agent directly on the host. All three\n * branch strategies are supported (head, merge-to-head, branch),\n * defaulting to head.\n */\nexport const noSandbox = (options?: NoSandboxOptions): NoSandboxProvider => ({\n tag: \"none\",\n name: \"no-sandbox\",\n env: options?.env ?? {},\n create: async (createOptions): Promise<NoSandboxHandle> => {\n const worktreePath = createOptions.worktreePath;\n const processEnv = { ...process.env, ...createOptions.env };\n const maxOutputTailChars = options?.maxOutputTailChars ?? MAX_TAIL_CHARS;\n\n const handle: NoSandboxHandle = {\n worktreePath,\n\n exec: (\n command: string,\n opts?: {\n onLine?: (line: string) => void;\n cwd?: string;\n sudo?: boolean;\n stdin?: string;\n },\n ): Promise<ExecResult> => {\n // sudo is a no-op for no-sandbox — the user is already on the host\n const cwd = opts?.cwd ?? worktreePath;\n\n return new Promise((resolve, reject) => {\n const proc = spawn(\"sh\", [\"-c\", command], {\n cwd,\n env: processEnv,\n stdio: [\n opts?.stdin !== undefined ? \"pipe\" : \"ignore\",\n \"pipe\",\n \"pipe\",\n ],\n });\n\n if (opts?.stdin !== undefined) {\n proc.stdin!.write(opts.stdin);\n proc.stdin!.end();\n }\n\n proc.on(\"error\", (error) => {\n reject(new Error(`exec failed: ${error.message}`));\n });\n\n if (opts?.onLine) {\n const onLine = opts.onLine;\n const stdoutTail = new BoundedTail(maxOutputTailChars, \"\\n\");\n const stderrTail = new BoundedTail(maxOutputTailChars, \"\");\n const rl = createInterface({ input: proc.stdout! });\n rl.on(\"line\", (line) => {\n stdoutTail.push(line);\n onLine(line);\n });\n proc.stderr!.on(\"data\", (chunk: Buffer) => {\n stderrTail.push(chunk.toString());\n });\n proc.on(\"close\", (code) => {\n resolve({\n stdout: stdoutTail.toString(),\n stderr: stderrTail.toString(),\n exitCode: code ?? 0,\n });\n });\n } else {\n const stdoutChunks: string[] = [];\n const stderrChunks: string[] = [];\n proc.stdout!.on(\"data\", (chunk: Buffer) => {\n stdoutChunks.push(chunk.toString());\n });\n proc.stderr!.on(\"data\", (chunk: Buffer) => {\n stderrChunks.push(chunk.toString());\n });\n proc.on(\"close\", (code) => {\n resolve({\n stdout: stdoutChunks.join(\"\"),\n stderr: stderrChunks.join(\"\"),\n exitCode: code ?? 0,\n });\n });\n }\n });\n },\n\n interactiveExec: (\n args: string[],\n opts: InteractiveExecOptions,\n ): Promise<{ exitCode: number }> => {\n return new Promise((resolve, reject) => {\n const [cmd, ...rest] = args;\n const proc = spawn(cmd!, rest, {\n cwd: opts.cwd ?? worktreePath,\n env: processEnv,\n stdio: [opts.stdin, opts.stdout, opts.stderr] as StdioOptions,\n });\n\n proc.on(\"error\", (error: Error) => {\n reject(new Error(`exec failed: ${error.message}`));\n });\n\n proc.on(\"close\", (code: number | null) => {\n resolve({ exitCode: code ?? 0 });\n });\n });\n },\n\n close: async (): Promise<void> => {\n // No-op — no container to tear down\n },\n };\n\n return handle;\n },\n});\n"]}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
|
|
3
|
+
createRequire(import.meta.url);
|
|
4
|
+
|
|
5
|
+
// src/SandboxProvider.ts
|
|
6
|
+
var createBindMountSandboxProvider = (config) => ({
|
|
7
|
+
tag: "bind-mount",
|
|
8
|
+
name: config.name,
|
|
9
|
+
env: config.env ?? {},
|
|
10
|
+
sandboxHomedir: config.sandboxHomedir,
|
|
11
|
+
create: config.create
|
|
12
|
+
});
|
|
13
|
+
var createIsolatedSandboxProvider = (config) => ({
|
|
14
|
+
tag: "isolated",
|
|
15
|
+
name: config.name,
|
|
16
|
+
env: config.env ?? {},
|
|
17
|
+
create: config.create
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export { createBindMountSandboxProvider, createIsolatedSandboxProvider };
|
|
21
|
+
//# sourceMappingURL=chunk-BIWNFKGV.js.map
|
|
22
|
+
//# sourceMappingURL=chunk-BIWNFKGV.js.map
|