@tanstack/ai-sandbox 0.1.0

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.
Files changed (109) hide show
  1. package/README.md +182 -0
  2. package/dist/esm/agents-file.d.ts +36 -0
  3. package/dist/esm/agents-file.js +44 -0
  4. package/dist/esm/agents-file.js.map +1 -0
  5. package/dist/esm/approvals.d.ts +38 -0
  6. package/dist/esm/approvals.js +36 -0
  7. package/dist/esm/approvals.js.map +1 -0
  8. package/dist/esm/bootstrap.d.ts +17 -0
  9. package/dist/esm/bootstrap.js +124 -0
  10. package/dist/esm/bootstrap.js.map +1 -0
  11. package/dist/esm/bridge-events.d.ts +21 -0
  12. package/dist/esm/bridge-events.js +76 -0
  13. package/dist/esm/bridge-events.js.map +1 -0
  14. package/dist/esm/capabilities.d.ts +26 -0
  15. package/dist/esm/capabilities.js +29 -0
  16. package/dist/esm/capabilities.js.map +1 -0
  17. package/dist/esm/contracts.d.ts +211 -0
  18. package/dist/esm/errors.d.ts +16 -0
  19. package/dist/esm/errors.js +25 -0
  20. package/dist/esm/errors.js.map +1 -0
  21. package/dist/esm/git-exec.d.ts +2 -0
  22. package/dist/esm/git-exec.js +68 -0
  23. package/dist/esm/git-exec.js.map +1 -0
  24. package/dist/esm/harness-cwd.d.ts +2 -0
  25. package/dist/esm/harness-cwd.js +24 -0
  26. package/dist/esm/harness-cwd.js.map +1 -0
  27. package/dist/esm/index.d.ts +39 -0
  28. package/dist/esm/index.js +103 -0
  29. package/dist/esm/index.js.map +1 -0
  30. package/dist/esm/key.d.ts +20 -0
  31. package/dist/esm/key.js +41 -0
  32. package/dist/esm/key.js.map +1 -0
  33. package/dist/esm/middleware.d.ts +5 -0
  34. package/dist/esm/middleware.js +140 -0
  35. package/dist/esm/middleware.js.map +1 -0
  36. package/dist/esm/ngrok.d.ts +16 -0
  37. package/dist/esm/ngrok.js +54 -0
  38. package/dist/esm/ngrok.js.map +1 -0
  39. package/dist/esm/policy.d.ts +47 -0
  40. package/dist/esm/policy.js +44 -0
  41. package/dist/esm/policy.js.map +1 -0
  42. package/dist/esm/projection.d.ts +31 -0
  43. package/dist/esm/projection.js +9 -0
  44. package/dist/esm/projection.js.map +1 -0
  45. package/dist/esm/remote-tools.d.ts +48 -0
  46. package/dist/esm/remote-tools.js +76 -0
  47. package/dist/esm/remote-tools.js.map +1 -0
  48. package/dist/esm/run-log.d.ts +81 -0
  49. package/dist/esm/run-log.js +107 -0
  50. package/dist/esm/run-log.js.map +1 -0
  51. package/dist/esm/run.d.ts +58 -0
  52. package/dist/esm/run.js +89 -0
  53. package/dist/esm/run.js.map +1 -0
  54. package/dist/esm/runner.d.ts +21 -0
  55. package/dist/esm/runner.js +54 -0
  56. package/dist/esm/runner.js.map +1 -0
  57. package/dist/esm/sandbox.d.ts +79 -0
  58. package/dist/esm/sandbox.js +125 -0
  59. package/dist/esm/sandbox.js.map +1 -0
  60. package/dist/esm/secrets.d.ts +37 -0
  61. package/dist/esm/secrets.js +59 -0
  62. package/dist/esm/secrets.js.map +1 -0
  63. package/dist/esm/setup-plan.d.ts +13 -0
  64. package/dist/esm/setup-plan.js +16 -0
  65. package/dist/esm/setup-plan.js.map +1 -0
  66. package/dist/esm/shell.d.ts +45 -0
  67. package/dist/esm/shell.js +164 -0
  68. package/dist/esm/shell.js.map +1 -0
  69. package/dist/esm/store.d.ts +53 -0
  70. package/dist/esm/store.js +34 -0
  71. package/dist/esm/store.js.map +1 -0
  72. package/dist/esm/tool-bridge.d.ts +130 -0
  73. package/dist/esm/tool-bridge.js +197 -0
  74. package/dist/esm/tool-bridge.js.map +1 -0
  75. package/dist/esm/watch.d.ts +36 -0
  76. package/dist/esm/watch.js +144 -0
  77. package/dist/esm/watch.js.map +1 -0
  78. package/dist/esm/workspace.d.ts +128 -0
  79. package/dist/esm/workspace.js +42 -0
  80. package/dist/esm/workspace.js.map +1 -0
  81. package/package.json +72 -0
  82. package/skills/ai-sandbox/SKILL.md +366 -0
  83. package/src/agents-file.ts +101 -0
  84. package/src/approvals.ts +96 -0
  85. package/src/bootstrap.ts +196 -0
  86. package/src/bridge-events.ts +112 -0
  87. package/src/capabilities.ts +47 -0
  88. package/src/contracts.ts +236 -0
  89. package/src/errors.ts +31 -0
  90. package/src/git-exec.ts +114 -0
  91. package/src/harness-cwd.ts +38 -0
  92. package/src/index.ts +222 -0
  93. package/src/key.ts +70 -0
  94. package/src/middleware.ts +233 -0
  95. package/src/ngrok.ts +85 -0
  96. package/src/policy.ts +111 -0
  97. package/src/projection.ts +46 -0
  98. package/src/remote-tools.ts +180 -0
  99. package/src/run-log.ts +224 -0
  100. package/src/run.ts +167 -0
  101. package/src/runner.ts +99 -0
  102. package/src/sandbox.ts +259 -0
  103. package/src/secrets.ts +101 -0
  104. package/src/setup-plan.ts +25 -0
  105. package/src/shell.ts +288 -0
  106. package/src/store.ts +83 -0
  107. package/src/tool-bridge.ts +399 -0
  108. package/src/watch.ts +256 -0
  109. package/src/workspace.ts +151 -0
@@ -0,0 +1,236 @@
1
+ /**
2
+ * Provider-agnostic sandbox contracts.
3
+ *
4
+ * A {@link SandboxProvider} owns an isolation primitive (Docker container,
5
+ * Cloudflare DO-backed container, a local OS process tree, …) and knows how to
6
+ * create / resume / restore / destroy a {@link SandboxHandle}. A
7
+ * `SandboxHandle` is the uniform runtime surface every consumer (harness
8
+ * adapters, the workspace bootstrap engine, advanced users) codes against.
9
+ *
10
+ * Providers differ in what they can do — see {@link SandboxCapabilities}. The
11
+ * mandatory `fs` and `exec` capabilities are guaranteed by the contract;
12
+ * everything else is optional and capability-gated. Calling an unsupported
13
+ * optional method throws {@link UnsupportedCapabilityError} rather than
14
+ * silently no-opping.
15
+ */
16
+ import type { WorkspaceDefinition } from './workspace'
17
+ import type { SandboxPolicy } from './policy'
18
+
19
+ /** Static description of what a provider supports. */
20
+ export interface SandboxCapabilities {
21
+ /** Read/write/list/… via {@link SandboxFs}. Always true (mandatory). */
22
+ fs: boolean
23
+ /** Blocking command execution via {@link SandboxProcess.exec}. Always true (mandatory). */
24
+ exec: boolean
25
+ /** Per-create / per-command environment variables. */
26
+ env: boolean
27
+ /** Expose a port and resolve a reachable channel via {@link SandboxPorts}. */
28
+ ports: boolean
29
+ /** Long-running/background processes via {@link SandboxProcess.spawn}. */
30
+ backgroundProcesses: boolean
31
+ /**
32
+ * A spawned process exposes a writable host→process stdin
33
+ * ({@link SpawnHandle.stdin}). `true` for host/Docker; some edge providers
34
+ * (e.g. Cloudflare) run background processes WITHOUT a writable stdin, so
35
+ * harness adapters that feed a prompt over stdin must instead deliver it via a
36
+ * file + shell redirection.
37
+ */
38
+ writableStdin: boolean
39
+ /** Capture/restore filesystem snapshots via {@link SandboxHandle.snapshot}. */
40
+ snapshots: boolean
41
+ /** Declarative network egress allow/deny policy. */
42
+ networkPolicy: boolean
43
+ /** Filesystem persists across sandbox stop/restart without a snapshot. */
44
+ durableFilesystem: boolean
45
+ /** Branch a new sandbox from current state via {@link SandboxHandle.fork}. */
46
+ fork: boolean
47
+ }
48
+
49
+ /** Result of a blocking command. */
50
+ export interface ExecResult {
51
+ stdout: string
52
+ stderr: string
53
+ exitCode: number
54
+ }
55
+
56
+ /** Options for {@link SandboxProcess.exec} / {@link SandboxProcess.spawn}. */
57
+ export interface ProcessOptions {
58
+ /** Working directory inside the sandbox. Defaults to the workspace root. */
59
+ cwd?: string
60
+ /** Per-command environment variables, merged over the sandbox env. */
61
+ env?: Record<string, string>
62
+ /** Abort the command/process when this signal fires. */
63
+ signal?: AbortSignal
64
+ }
65
+
66
+ /**
67
+ * A live background process. `stdout`/`stderr` are async-iterables of decoded
68
+ * chunks; `stdin.write` feeds the process (duplex — required for ACP harness
69
+ * protocols such as Codex / Gemini CLI). There is intentionally NO
70
+ * reconnect-to-a-running-process in v1 — that belongs to the durable-stream /
71
+ * persistence layer.
72
+ */
73
+ export interface SpawnHandle {
74
+ readonly pid: number
75
+ readonly stdout: AsyncIterable<string>
76
+ readonly stderr: AsyncIterable<string>
77
+ readonly stdin: {
78
+ write: (data: string) => Promise<void>
79
+ end: () => Promise<void>
80
+ }
81
+ /** Resolves with the exit code when the process exits. */
82
+ wait: () => Promise<number>
83
+ kill: (signal?: NodeJS.Signals | number) => Promise<void>
84
+ }
85
+
86
+ export interface SandboxProcess {
87
+ /** Run a command to completion and capture stdout/stderr/exit code. */
88
+ exec: (command: string, options?: ProcessOptions) => Promise<ExecResult>
89
+ /** Start a long-running/background process with streamable, duplex IO. */
90
+ spawn: (command: string, options?: ProcessOptions) => Promise<SpawnHandle>
91
+ }
92
+
93
+ /** Common, portable filesystem operations every provider implements. */
94
+ export interface SandboxFs {
95
+ read: (path: string) => Promise<string>
96
+ readBytes: (path: string) => Promise<Uint8Array>
97
+ write: (path: string, data: string | Uint8Array) => Promise<void>
98
+ list: (
99
+ path: string,
100
+ ) => Promise<Array<{ name: string; path: string; type: 'file' | 'dir' }>>
101
+ mkdir: (path: string) => Promise<void>
102
+ remove: (path: string) => Promise<void>
103
+ rename: (from: string, to: string) => Promise<void>
104
+ exists: (path: string) => Promise<boolean>
105
+ /** Optional — present only when `capabilities.fs` providers advertise watch. */
106
+ watch?: (
107
+ path: string,
108
+ onEvent: (event: { type: string; path: string }) => void,
109
+ ) => Promise<{ stop: () => Promise<void> }>
110
+ }
111
+
112
+ /**
113
+ * Uniform git surface. Implementations either delegate to the provider's
114
+ * native git (when advertised) or desugar to `process.exec("git …")`, so the
115
+ * contract is identical across providers.
116
+ */
117
+ export interface SandboxGit {
118
+ clone: (input: {
119
+ url: string
120
+ dir?: string
121
+ ref?: string
122
+ auth?: { username?: string; token: string }
123
+ depth?: number | 'full'
124
+ }) => Promise<void>
125
+ status: (dir?: string) => Promise<string>
126
+ add: (paths: Array<string>, dir?: string) => Promise<void>
127
+ commit: (message: string, dir?: string) => Promise<void>
128
+ push: (dir?: string) => Promise<void>
129
+ pull: (dir?: string) => Promise<void>
130
+ /** Returns the current branch name. */
131
+ branch: (dir?: string) => Promise<string>
132
+ }
133
+
134
+ /** A reachable channel to a port inside the sandbox. */
135
+ export interface SandboxChannel {
136
+ /** URL the host can reach (localhost / host-bound port / authenticated preview URL). */
137
+ url: string
138
+ /** Bearer token gating the channel, when the provider issues one. */
139
+ token?: string
140
+ /**
141
+ * Ready-to-send HTTP headers that authenticate requests to {@link url}, when
142
+ * the provider's auth doesn't fit a plain `Authorization: Bearer <token>`
143
+ * (e.g. Daytona's `x-daytona-preview-token`). Consumers that speak HTTP to the
144
+ * channel should attach these verbatim; the provider owns the header names so
145
+ * consumers stay provider-agnostic.
146
+ */
147
+ headers?: Record<string, string>
148
+ }
149
+
150
+ export interface SandboxPorts {
151
+ /** Expose `port` and resolve the best reachable channel for the host. */
152
+ connect: (port: number) => Promise<SandboxChannel>
153
+ }
154
+
155
+ export interface SandboxEnv {
156
+ set: (vars: Record<string, string>) => Promise<void>
157
+ }
158
+
159
+ /** Opaque reference to a stored snapshot, used to restore later. */
160
+ export interface SnapshotRef {
161
+ id: string
162
+ label?: string
163
+ }
164
+
165
+ /** The uniform runtime surface a sandbox exposes. */
166
+ export interface SandboxHandle {
167
+ /** Provider-assigned id used to reconnect to this sandbox. */
168
+ readonly id: string
169
+ /** Provider name (e.g. "docker", "cloudflare", "local-process"). */
170
+ readonly provider: string
171
+ /**
172
+ * Real filesystem path backing the virtual workspace root (`/workspace`).
173
+ * Harness CLIs and ACP `newSession` interpret cwd literally — use
174
+ * {@link resolveHarnessCwd} rather than the virtual path when the provider
175
+ * maps `/workspace` elsewhere (Daytona, Vercel, local-process).
176
+ */
177
+ readonly workspaceRoot?: string
178
+ /** What this sandbox can do. */
179
+ readonly capabilities: SandboxCapabilities
180
+ readonly fs: SandboxFs
181
+ readonly git: SandboxGit
182
+ readonly process: SandboxProcess
183
+ readonly ports: SandboxPorts
184
+ readonly env: SandboxEnv
185
+ /** Capability-gated: throws UnsupportedCapabilityError if `capabilities.snapshots` is false. */
186
+ snapshot?: (label?: string) => Promise<SnapshotRef>
187
+ /** Capability-gated: throws UnsupportedCapabilityError if `capabilities.fork` is false. */
188
+ fork?: () => Promise<SandboxHandle>
189
+ destroy: () => Promise<void>
190
+ }
191
+
192
+ /** Input passed to {@link SandboxProvider.create}. */
193
+ export interface SandboxCreateInput {
194
+ workspace?: WorkspaceDefinition
195
+ policy?: SandboxPolicy
196
+ env?: Record<string, string>
197
+ signal?: AbortSignal
198
+ }
199
+
200
+ /** Input passed to {@link SandboxProvider.resume}. */
201
+ export interface SandboxResumeInput {
202
+ /** Provider-assigned sandbox id recorded by a prior run. */
203
+ id: string
204
+ signal?: AbortSignal
205
+ }
206
+
207
+ /** Input passed to {@link SandboxProvider.restoreSnapshot}. */
208
+ export interface SandboxRestoreInput {
209
+ snapshotId: string
210
+ workspace?: WorkspaceDefinition
211
+ policy?: SandboxPolicy
212
+ env?: Record<string, string>
213
+ signal?: AbortSignal
214
+ }
215
+
216
+ /** Input passed to {@link SandboxProvider.destroy}. */
217
+ export interface SandboxDestroyInput {
218
+ id: string
219
+ signal?: AbortSignal
220
+ }
221
+
222
+ /**
223
+ * Owns an isolation primitive. Implemented by `@tanstack/ai-sandbox-*`
224
+ * provider packages.
225
+ */
226
+ export interface SandboxProvider {
227
+ readonly name: string
228
+ /** Static capability descriptor. */
229
+ capabilities: () => SandboxCapabilities
230
+ create: (input: SandboxCreateInput) => Promise<SandboxHandle>
231
+ /** Reconnect to an existing sandbox by id; resolves null if it's gone. */
232
+ resume: (input: SandboxResumeInput) => Promise<SandboxHandle | null>
233
+ /** Capability-gated: present only when `capabilities().snapshots` is true. */
234
+ restoreSnapshot?: (input: SandboxRestoreInput) => Promise<SandboxHandle>
235
+ destroy: (input: SandboxDestroyInput) => Promise<void>
236
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Thrown when code invokes an optional sandbox capability that the active
3
+ * provider does not support. Core/middleware should check
4
+ * `handle.capabilities` BEFORE using an optional capability and degrade
5
+ * gracefully; this error exists so that a direct call to an unsupported
6
+ * optional method fails loud instead of silently no-opping.
7
+ */
8
+ export class UnsupportedCapabilityError extends Error {
9
+ readonly provider: string
10
+ readonly capability: string
11
+
12
+ constructor(provider: string, capability: string, hint?: string) {
13
+ super(
14
+ `Sandbox provider "${provider}" does not support the "${capability}" capability.` +
15
+ (hint ? ` ${hint}` : ''),
16
+ )
17
+ this.name = 'UnsupportedCapabilityError'
18
+ this.provider = provider
19
+ this.capability = capability
20
+ }
21
+ }
22
+
23
+ /** Thrown when a harness adapter requires a sandbox but none was provided. */
24
+ export class MissingSandboxError extends Error {
25
+ constructor(adapterName: string) {
26
+ super(
27
+ `Adapter "${adapterName}" requires a sandbox. Add withSandbox(defineSandbox({ ... })) to chat() middleware.`,
28
+ )
29
+ this.name = 'MissingSandboxError'
30
+ }
31
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * An exec-backed {@link SandboxGit} implementation. Providers without a native
3
+ * git API (local-process, Docker) get a uniform `sandbox.git` by desugaring to
4
+ * `process.exec("git …")`. Providers WITH native git (Daytona, Cloudflare) may
5
+ * supply their own implementation instead.
6
+ *
7
+ * Security:
8
+ * - Every interpolated value is single-quote escaped (no shell injection).
9
+ * - A `--` end-of-options separator precedes untrusted positionals and values
10
+ * are rejected if they begin with `-`, so a repo URL / ref / path can't
11
+ * smuggle a git flag (e.g. `--upload-pack=…`).
12
+ * - Auth tokens NEVER appear in argv (they'd leak via `ps` / process logs).
13
+ * Instead a one-shot `credential.helper` reads the token from the child
14
+ * process ENV. The helper string is single-quoted so the OUTER shell never
15
+ * expands the env var — only git's own helper subshell does, at use time.
16
+ *
17
+ * NOTE: `SandboxProcess.exec` takes a command STRING by design (the sandbox
18
+ * runs shell commands), so we mitigate flag smuggling with `--` + validation
19
+ * rather than an argv array.
20
+ */
21
+ import type { SandboxGit, SandboxProcess } from './contracts'
22
+
23
+ /** POSIX single-quote escape: wrap in '…' and escape embedded quotes. */
24
+ function q(value: string): string {
25
+ return `'${value.replace(/'/g, `'\\''`)}'`
26
+ }
27
+
28
+ /** Reject values that could be parsed as a git flag when used as a positional. */
29
+ function assertNoLeadingDash(value: string, name: string): void {
30
+ if (value.startsWith('-')) {
31
+ throw new Error(
32
+ `git-exec: ${name} "${value}" must not begin with "-" (argument-injection guard).`,
33
+ )
34
+ }
35
+ }
36
+
37
+ // Credential helper that prints creds read from the child ENV. Single-quoted at
38
+ // the call site so the outer shell passes it literally; git expands the vars in
39
+ // its own helper subshell, keeping the token out of argv.
40
+ const CREDENTIAL_HELPER =
41
+ '!f() { echo "username=${GIT_ASKPASS_USER}"; echo "password=${GIT_ASKPASS_TOKEN}"; }; f'
42
+
43
+ export function createExecBackedGit(
44
+ process: SandboxProcess,
45
+ defaultRoot: string,
46
+ ): SandboxGit {
47
+ const at = (dir?: string): string => {
48
+ const d = dir ?? defaultRoot
49
+ assertNoLeadingDash(d, 'dir')
50
+ return q(d)
51
+ }
52
+
53
+ return {
54
+ clone: async ({ url, dir, ref, auth, depth }) => {
55
+ assertNoLeadingDash(url, 'url')
56
+ const target = dir ?? defaultRoot
57
+ assertNoLeadingDash(target, 'dir')
58
+ if (ref !== undefined) assertNoLeadingDash(ref, 'ref')
59
+ const refArg = ref ? `--branch ${q(ref)} ` : ''
60
+ const resolvedDepth = depth ?? 1
61
+ // `depth` is interpolated unquoted into the command, so validate it the
62
+ // same way other positionals are guarded — a non-positive-integer (e.g. an
63
+ // untyped caller passing a string) must never reach the shell.
64
+ if (
65
+ resolvedDepth !== 'full' &&
66
+ (!Number.isInteger(resolvedDepth) || resolvedDepth <= 0)
67
+ ) {
68
+ throw new Error('git-exec: depth must be a positive integer or "full".')
69
+ }
70
+ const depthArg =
71
+ resolvedDepth === 'full'
72
+ ? ''
73
+ : `--depth ${resolvedDepth} --single-branch `
74
+
75
+ if (auth?.token) {
76
+ await process.exec(
77
+ `git -c credential.helper=${q(CREDENTIAL_HELPER)} clone ${refArg}${depthArg}-- ${q(url)} ${q(target)}`,
78
+ {
79
+ // Token lives only in the child env, never in argv.
80
+ env: {
81
+ GIT_ASKPASS_USER: auth.username ?? 'x-access-token',
82
+ GIT_ASKPASS_TOKEN: auth.token,
83
+ GIT_TERMINAL_PROMPT: '0',
84
+ },
85
+ },
86
+ )
87
+ return
88
+ }
89
+
90
+ await process.exec(
91
+ `git clone ${refArg}${depthArg}-- ${q(url)} ${q(target)}`,
92
+ )
93
+ },
94
+ status: async (dir) =>
95
+ (await process.exec(`git -C ${at(dir)} status --porcelain`)).stdout,
96
+ add: async (paths, dir) => {
97
+ paths.forEach((p, i) => assertNoLeadingDash(p, `path[${i}]`))
98
+ await process.exec(`git -C ${at(dir)} add -- ${paths.map(q).join(' ')}`)
99
+ },
100
+ commit: async (message, dir) => {
101
+ await process.exec(`git -C ${at(dir)} commit -m ${q(message)}`)
102
+ },
103
+ push: async (dir) => {
104
+ await process.exec(`git -C ${at(dir)} push`)
105
+ },
106
+ pull: async (dir) => {
107
+ await process.exec(`git -C ${at(dir)} pull`)
108
+ },
109
+ branch: async (dir) =>
110
+ (
111
+ await process.exec(`git -C ${at(dir)} rev-parse --abbrev-ref HEAD`)
112
+ ).stdout.trim(),
113
+ }
114
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Resolve a VIRTUAL sandbox cwd (e.g. `/workspace`) to the path a harness CLI
3
+ * or ACP session must use on the real filesystem.
4
+ *
5
+ * Provider handles map virtual paths for spawn/exec/fs; harness-facing APIs
6
+ * interpret cwd literally (`grok --cwd`, ACP `newSession`, opencode HTTP
7
+ * `directory`, …).
8
+ */
9
+ import * as path from 'node:path'
10
+ import { DEFAULT_WORKSPACE_ROOT } from './bootstrap'
11
+ import type { SandboxHandle } from './contracts'
12
+
13
+ function mapVirtualWorkspacePath(virtualCwd: string, realRoot: string): string {
14
+ if (virtualCwd === DEFAULT_WORKSPACE_ROOT) return realRoot
15
+ if (virtualCwd.startsWith(`${DEFAULT_WORKSPACE_ROOT}/`)) {
16
+ const rel = virtualCwd.slice(DEFAULT_WORKSPACE_ROOT.length + 1)
17
+ return realRoot === DEFAULT_WORKSPACE_ROOT
18
+ ? `${DEFAULT_WORKSPACE_ROOT}/${rel}`
19
+ : path.posix.join(realRoot, rel)
20
+ }
21
+ return virtualCwd
22
+ }
23
+
24
+ export function resolveHarnessCwd(
25
+ handle: SandboxHandle,
26
+ virtualCwd: string = DEFAULT_WORKSPACE_ROOT,
27
+ ): string {
28
+ if (handle.provider === 'local-process') {
29
+ return mapVirtualWorkspacePath(virtualCwd, handle.id)
30
+ }
31
+
32
+ const root = handle.workspaceRoot
33
+ if (root !== undefined && root !== DEFAULT_WORKSPACE_ROOT) {
34
+ return mapVirtualWorkspacePath(virtualCwd, root)
35
+ }
36
+
37
+ return virtualCwd
38
+ }
package/src/index.ts ADDED
@@ -0,0 +1,222 @@
1
+ // Capability tokens + accessors
2
+ export {
3
+ SandboxCapability,
4
+ SandboxStoreCapability,
5
+ LocksCapability,
6
+ SandboxPolicyCapability,
7
+ ToolBridgeProvisionerCapability,
8
+ getSandbox,
9
+ provideSandbox,
10
+ getSandboxStore,
11
+ provideSandboxStore,
12
+ getLocks,
13
+ provideLocks,
14
+ getSandboxPolicy,
15
+ provideSandboxPolicy,
16
+ getToolBridgeProvisioner,
17
+ provideToolBridgeProvisioner,
18
+ } from './capabilities'
19
+
20
+ // Workspace projection capability (provided by withSandbox, consumed by harness adapters)
21
+ export {
22
+ ProjectionCapability,
23
+ getWorkspaceProjection,
24
+ provideWorkspaceProjection,
25
+ } from './projection'
26
+ export type { WorkspaceProjection } from './projection'
27
+
28
+ // Middleware
29
+ export { withSandbox } from './middleware'
30
+
31
+ // Sandbox definition + lifecycle
32
+ export { defineSandbox } from './sandbox'
33
+ export type {
34
+ SandboxConfig,
35
+ SandboxDefinition,
36
+ SandboxEnsureContext,
37
+ SandboxLifecycle,
38
+ SandboxHooks,
39
+ ReuseStrategy,
40
+ SnapshotStrategy,
41
+ } from './sandbox'
42
+
43
+ // Workspace
44
+ export {
45
+ defineWorkspace,
46
+ gitSource,
47
+ githubRepo,
48
+ localSource,
49
+ fileSkill,
50
+ agentSkill,
51
+ mcpSkill,
52
+ gitSkill,
53
+ } from './workspace'
54
+ export type {
55
+ WorkspaceDefinition,
56
+ WorkspaceSource,
57
+ WorkspaceSkill,
58
+ PackageManager,
59
+ McpConfig,
60
+ } from './workspace'
61
+
62
+ // Secrets
63
+ export {
64
+ createSecrets,
65
+ bearer,
66
+ isSecretRef,
67
+ resolveSecret,
68
+ resolveBearer,
69
+ resolveAllSecrets,
70
+ } from './secrets'
71
+ export type { SecretRef, Secrets, BearerRef } from './secrets'
72
+
73
+ // Policy
74
+ export { defineSandboxPolicy, evaluateCommand, commandAliases } from './policy'
75
+ export type {
76
+ SandboxPolicy,
77
+ PolicyDecision,
78
+ CommandRules,
79
+ CapabilityRules,
80
+ } from './policy'
81
+
82
+ // Provider + handle contracts
83
+ export type {
84
+ SandboxProvider,
85
+ SandboxHandle,
86
+ SandboxCapabilities,
87
+ SandboxFs,
88
+ SandboxGit,
89
+ SandboxProcess,
90
+ SandboxPorts,
91
+ SandboxEnv,
92
+ SandboxChannel,
93
+ SpawnHandle,
94
+ ExecResult,
95
+ ProcessOptions,
96
+ SnapshotRef,
97
+ SandboxCreateInput,
98
+ SandboxResumeInput,
99
+ SandboxRestoreInput,
100
+ SandboxDestroyInput,
101
+ } from './contracts'
102
+
103
+ // Stores (interfaces + in-memory defaults)
104
+ export { InMemorySandboxStore, InMemoryLockStore } from './store'
105
+ export type { SandboxStore, LockStore, SandboxRecord } from './store'
106
+
107
+ // Bootstrap engine (exported for provider/adapter authors + tests)
108
+ export {
109
+ bootstrapWorkspace,
110
+ detectPackageManager,
111
+ DEFAULT_WORKSPACE_ROOT,
112
+ } from './bootstrap'
113
+ export { resolveHarnessCwd } from './harness-cwd'
114
+ export type { BootstrapResult } from './bootstrap'
115
+
116
+ // AGENTS.md writer + gitSkill path helper (used by bootstrap + harness adapters)
117
+ export {
118
+ writeAgentsFile,
119
+ resolveGitSkillDir,
120
+ formatWorkspaceScriptsSection,
121
+ mergeAgentsContent,
122
+ } from './agents-file'
123
+
124
+ // Exec-backed git helper (for providers without native git)
125
+ export { createExecBackedGit } from './git-exec'
126
+
127
+ // Harness runner: spawn an agent CLI in a sandbox + stream NDJSON stdout
128
+ export { spawnNdjson, toLines } from './runner'
129
+ export type { SpawnNdjsonOptions } from './runner'
130
+
131
+ // MCP tool-proxy bridge (shared by harness adapters): transport-agnostic core
132
+ // + the node:http host transport + a fetch-friendly JSON-RPC dispatcher.
133
+ export {
134
+ startHostToolBridge,
135
+ hostForSandbox,
136
+ createToolBridgeCore,
137
+ handleBridgeJsonRpc,
138
+ timingSafeBearerEqual,
139
+ nodeHttpBridgeProvisioner,
140
+ BRIDGED_MCP_SERVER_NAME,
141
+ } from './tool-bridge'
142
+ export type {
143
+ HostToolBridge,
144
+ StartBridgeOptions,
145
+ ToolBridgeCore,
146
+ ToolBridgeCoreOptions,
147
+ ToolDescriptor,
148
+ ToolCallResult,
149
+ BridgePermission,
150
+ PermissionToolResult,
151
+ ToolBridgeProvisioner,
152
+ ToolBridgeProvisionOptions,
153
+ ProvisionedBridge,
154
+ } from './tool-bridge'
155
+
156
+ // Surface bridged-tool custom events (e.g. code mode console logs) on a harness
157
+ // adapter's live output stream.
158
+ export { createBridgeEventChannel, mergeChunkStreams } from './bridge-events'
159
+ export type { BridgeEventChannel } from './bridge-events'
160
+
161
+ // Host-tool delegation for the co-located ("combined") model: harness + bridge
162
+ // run in-container; only chat()-tool EXECUTION crosses back to the orchestrator.
163
+ export {
164
+ remoteToolStubs,
165
+ toolDescriptors,
166
+ httpRemoteToolExecutor,
167
+ executeHostTool,
168
+ isToolExecRequest,
169
+ } from './remote-tools'
170
+ export type {
171
+ RemoteToolExecutor,
172
+ RemoteToolExecuteOptions,
173
+ ToolExecRequest,
174
+ } from './remote-tools'
175
+
176
+ // Resumable run event-log — the primitive that lets a trigger start a run and
177
+ // return while a durable orchestrator drives it and clients tail from a cursor.
178
+ export { InMemoryRunEventLog, isTerminalRunStatus } from './run-log'
179
+ export type {
180
+ RunEventLog,
181
+ RunRecord,
182
+ RunEvent,
183
+ RunStatus,
184
+ TerminalRunStatus,
185
+ RunError,
186
+ RunEventLogReadOptions,
187
+ } from './run-log'
188
+
189
+ // Run driver — pump a chat() stream into the event-log so a trigger returns
190
+ // immediately while a durable orchestrator drives the run and clients tail it.
191
+ export { pipeToRunLog, RunController } from './run'
192
+ export type {
193
+ PipeToRunLogOptions,
194
+ RunControllerStartInput,
195
+ RunHandle,
196
+ } from './run'
197
+
198
+ // Interactive approvals (shared by harness adapters)
199
+ export {
200
+ resolveApproval,
201
+ approvalId,
202
+ buildApprovalRequestedEvent,
203
+ APPROVAL_REQUESTED_EVENT,
204
+ } from './approvals'
205
+ export type { ResolveApprovalInput, ApprovalOutcome } from './approvals'
206
+
207
+ // File-event watch (low-level workspace observer)
208
+ export { watchWorkspace, diffSnapshots } from './watch'
209
+ export type {
210
+ SandboxFileEvent,
211
+ FileEvent,
212
+ FileEventType,
213
+ WatchOptions,
214
+ SandboxWatchHandle,
215
+ } from './watch'
216
+
217
+ // Keying
218
+ export { computeSandboxKey, computeWorkspaceHash } from './key'
219
+ export type { SandboxKeyInput } from './key'
220
+
221
+ // Errors
222
+ export { UnsupportedCapabilityError, MissingSandboxError } from './errors'