@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.
- package/README.md +182 -0
- package/dist/esm/agents-file.d.ts +36 -0
- package/dist/esm/agents-file.js +44 -0
- package/dist/esm/agents-file.js.map +1 -0
- package/dist/esm/approvals.d.ts +38 -0
- package/dist/esm/approvals.js +36 -0
- package/dist/esm/approvals.js.map +1 -0
- package/dist/esm/bootstrap.d.ts +17 -0
- package/dist/esm/bootstrap.js +124 -0
- package/dist/esm/bootstrap.js.map +1 -0
- package/dist/esm/bridge-events.d.ts +21 -0
- package/dist/esm/bridge-events.js +76 -0
- package/dist/esm/bridge-events.js.map +1 -0
- package/dist/esm/capabilities.d.ts +26 -0
- package/dist/esm/capabilities.js +29 -0
- package/dist/esm/capabilities.js.map +1 -0
- package/dist/esm/contracts.d.ts +211 -0
- package/dist/esm/errors.d.ts +16 -0
- package/dist/esm/errors.js +25 -0
- package/dist/esm/errors.js.map +1 -0
- package/dist/esm/git-exec.d.ts +2 -0
- package/dist/esm/git-exec.js +68 -0
- package/dist/esm/git-exec.js.map +1 -0
- package/dist/esm/harness-cwd.d.ts +2 -0
- package/dist/esm/harness-cwd.js +24 -0
- package/dist/esm/harness-cwd.js.map +1 -0
- package/dist/esm/index.d.ts +39 -0
- package/dist/esm/index.js +103 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/key.d.ts +20 -0
- package/dist/esm/key.js +41 -0
- package/dist/esm/key.js.map +1 -0
- package/dist/esm/middleware.d.ts +5 -0
- package/dist/esm/middleware.js +140 -0
- package/dist/esm/middleware.js.map +1 -0
- package/dist/esm/ngrok.d.ts +16 -0
- package/dist/esm/ngrok.js +54 -0
- package/dist/esm/ngrok.js.map +1 -0
- package/dist/esm/policy.d.ts +47 -0
- package/dist/esm/policy.js +44 -0
- package/dist/esm/policy.js.map +1 -0
- package/dist/esm/projection.d.ts +31 -0
- package/dist/esm/projection.js +9 -0
- package/dist/esm/projection.js.map +1 -0
- package/dist/esm/remote-tools.d.ts +48 -0
- package/dist/esm/remote-tools.js +76 -0
- package/dist/esm/remote-tools.js.map +1 -0
- package/dist/esm/run-log.d.ts +81 -0
- package/dist/esm/run-log.js +107 -0
- package/dist/esm/run-log.js.map +1 -0
- package/dist/esm/run.d.ts +58 -0
- package/dist/esm/run.js +89 -0
- package/dist/esm/run.js.map +1 -0
- package/dist/esm/runner.d.ts +21 -0
- package/dist/esm/runner.js +54 -0
- package/dist/esm/runner.js.map +1 -0
- package/dist/esm/sandbox.d.ts +79 -0
- package/dist/esm/sandbox.js +125 -0
- package/dist/esm/sandbox.js.map +1 -0
- package/dist/esm/secrets.d.ts +37 -0
- package/dist/esm/secrets.js +59 -0
- package/dist/esm/secrets.js.map +1 -0
- package/dist/esm/setup-plan.d.ts +13 -0
- package/dist/esm/setup-plan.js +16 -0
- package/dist/esm/setup-plan.js.map +1 -0
- package/dist/esm/shell.d.ts +45 -0
- package/dist/esm/shell.js +164 -0
- package/dist/esm/shell.js.map +1 -0
- package/dist/esm/store.d.ts +53 -0
- package/dist/esm/store.js +34 -0
- package/dist/esm/store.js.map +1 -0
- package/dist/esm/tool-bridge.d.ts +130 -0
- package/dist/esm/tool-bridge.js +197 -0
- package/dist/esm/tool-bridge.js.map +1 -0
- package/dist/esm/watch.d.ts +36 -0
- package/dist/esm/watch.js +144 -0
- package/dist/esm/watch.js.map +1 -0
- package/dist/esm/workspace.d.ts +128 -0
- package/dist/esm/workspace.js +42 -0
- package/dist/esm/workspace.js.map +1 -0
- package/package.json +72 -0
- package/skills/ai-sandbox/SKILL.md +366 -0
- package/src/agents-file.ts +101 -0
- package/src/approvals.ts +96 -0
- package/src/bootstrap.ts +196 -0
- package/src/bridge-events.ts +112 -0
- package/src/capabilities.ts +47 -0
- package/src/contracts.ts +236 -0
- package/src/errors.ts +31 -0
- package/src/git-exec.ts +114 -0
- package/src/harness-cwd.ts +38 -0
- package/src/index.ts +222 -0
- package/src/key.ts +70 -0
- package/src/middleware.ts +233 -0
- package/src/ngrok.ts +85 -0
- package/src/policy.ts +111 -0
- package/src/projection.ts +46 -0
- package/src/remote-tools.ts +180 -0
- package/src/run-log.ts +224 -0
- package/src/run.ts +167 -0
- package/src/runner.ts +99 -0
- package/src/sandbox.ts +259 -0
- package/src/secrets.ts +101 -0
- package/src/setup-plan.ts +25 -0
- package/src/shell.ts +288 -0
- package/src/store.ts +83 -0
- package/src/tool-bridge.ts +399 -0
- package/src/watch.ts +256 -0
- package/src/workspace.ts +151 -0
package/src/contracts.ts
ADDED
|
@@ -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
|
+
}
|
package/src/git-exec.ts
ADDED
|
@@ -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'
|