@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/watch.ts
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sandbox file-event hooks — observe create / change / delete of files inside a
|
|
3
|
+
* sandbox (e.g. as an in-sandbox agent edits the workspace).
|
|
4
|
+
*
|
|
5
|
+
* Provider-agnostic: coded against the {@link SandboxHandle} contract only.
|
|
6
|
+
* Two mechanisms, auto-selected:
|
|
7
|
+
*
|
|
8
|
+
* - **Native** — when a provider implements the optional `fs.watch` seam
|
|
9
|
+
* (local-process does, via Node `fs.watch`), OS events drive the feed with low
|
|
10
|
+
* latency.
|
|
11
|
+
* - **Exec-poll** — otherwise (Docker, Cloudflare, any exec-only provider), a
|
|
12
|
+
* single `find … -printf` snapshot of `mtime\tsize\tpath` is taken every
|
|
13
|
+
* `intervalMs` and diffed. Works on any Linux container with GNU findutils
|
|
14
|
+
* (true for `node:*` / debian images) with no extra deps or image changes.
|
|
15
|
+
*
|
|
16
|
+
* The feed intentionally rides only the portable surface, so the same
|
|
17
|
+
* `watchWorkspace` call behaves identically across providers.
|
|
18
|
+
*/
|
|
19
|
+
import { DEFAULT_WORKSPACE_ROOT } from './bootstrap'
|
|
20
|
+
import type { SandboxHandle } from './contracts'
|
|
21
|
+
import type { SandboxFileEvent } from '@tanstack/ai'
|
|
22
|
+
|
|
23
|
+
export type { SandboxFileEvent } from '@tanstack/ai'
|
|
24
|
+
/** @deprecated alias retained for the low-level watch API. */
|
|
25
|
+
export type FileEvent = SandboxFileEvent
|
|
26
|
+
export type FileEventType = SandboxFileEvent['type']
|
|
27
|
+
|
|
28
|
+
export interface WatchOptions {
|
|
29
|
+
/** Called for every observed file event. */
|
|
30
|
+
onEvent: (event: SandboxFileEvent) => void
|
|
31
|
+
/** Workspace root to watch. Defaults to `/workspace`. */
|
|
32
|
+
root?: string
|
|
33
|
+
/** Poll interval for the exec-poll fallback, in ms. Defaults to 700. */
|
|
34
|
+
intervalMs?: number
|
|
35
|
+
/**
|
|
36
|
+
* Directory-name fragments to ignore (a path containing `/<entry>/` is
|
|
37
|
+
* skipped). Defaults to `['.git', 'node_modules']`.
|
|
38
|
+
*/
|
|
39
|
+
ignore?: Array<string>
|
|
40
|
+
/** Stop watching when this signal aborts. */
|
|
41
|
+
signal?: AbortSignal
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface SandboxWatchHandle {
|
|
45
|
+
/** Stop the watcher and release its resources. */
|
|
46
|
+
stop: () => Promise<void>
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const DEFAULT_INTERVAL_MS = 700
|
|
50
|
+
const DEFAULT_IGNORE = ['.git', 'node_modules']
|
|
51
|
+
|
|
52
|
+
/** POSIX single-quote escape for embedding values in a shell command. */
|
|
53
|
+
function q(value: string): string {
|
|
54
|
+
return `'${value.replace(/'/g, `'\\''`)}'`
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Diff two file snapshots (`Map<path, signature>`, signature = `mtime\tsize`).
|
|
59
|
+
* Pure — the heart of the exec-poll path, unit-tested in isolation.
|
|
60
|
+
*/
|
|
61
|
+
export function diffSnapshots(
|
|
62
|
+
prev: Map<string, string>,
|
|
63
|
+
next: Map<string, string>,
|
|
64
|
+
timestamp: number,
|
|
65
|
+
): Array<SandboxFileEvent> {
|
|
66
|
+
const events: Array<SandboxFileEvent> = []
|
|
67
|
+
for (const [path, sig] of next) {
|
|
68
|
+
const before = prev.get(path)
|
|
69
|
+
if (before === undefined) events.push({ type: 'create', path, timestamp })
|
|
70
|
+
else if (before !== sig) events.push({ type: 'change', path, timestamp })
|
|
71
|
+
}
|
|
72
|
+
for (const path of prev.keys()) {
|
|
73
|
+
if (!next.has(path)) events.push({ type: 'delete', path, timestamp })
|
|
74
|
+
}
|
|
75
|
+
return events
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Build the `find` command that prints `mtime\tsize\tpath` for every file. */
|
|
79
|
+
function buildFindCommand(root: string, ignore: Array<string>): string {
|
|
80
|
+
const prunes = ignore
|
|
81
|
+
.map((entry) => `-not -path ${q(`*/${entry}/*`)}`)
|
|
82
|
+
.join(' ')
|
|
83
|
+
return `find ${q(root)} -type f ${prunes} -printf '%T@\\t%s\\t%p\\n'`
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Parse `find -printf` output into a `Map<path, signature>`. */
|
|
87
|
+
function parseFindOutput(stdout: string): Map<string, string> {
|
|
88
|
+
const snapshot = new Map<string, string>()
|
|
89
|
+
for (const line of stdout.split('\n')) {
|
|
90
|
+
if (line === '') continue
|
|
91
|
+
const firstTab = line.indexOf('\t')
|
|
92
|
+
const secondTab = line.indexOf('\t', firstTab + 1)
|
|
93
|
+
if (firstTab === -1 || secondTab === -1) continue
|
|
94
|
+
const mtime = line.slice(0, firstTab)
|
|
95
|
+
const size = line.slice(firstTab + 1, secondTab)
|
|
96
|
+
const path = line.slice(secondTab + 1)
|
|
97
|
+
snapshot.set(path, `${mtime}\t${size}`)
|
|
98
|
+
}
|
|
99
|
+
return snapshot
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Whether a path should be ignored (contains a `/<entry>/` fragment). */
|
|
103
|
+
function isIgnored(path: string, ignore: Array<string>): boolean {
|
|
104
|
+
return ignore.some((entry) => path.includes(`/${entry}/`))
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Start watching a sandbox workspace for file events. Picks the native
|
|
109
|
+
* `fs.watch` fast-path when the provider advertises it, otherwise polls via
|
|
110
|
+
* `find`. Returns a handle whose `stop()` tears everything down.
|
|
111
|
+
*/
|
|
112
|
+
export async function watchWorkspace(
|
|
113
|
+
handle: SandboxHandle,
|
|
114
|
+
options: WatchOptions,
|
|
115
|
+
): Promise<SandboxWatchHandle> {
|
|
116
|
+
const root = options.root ?? DEFAULT_WORKSPACE_ROOT
|
|
117
|
+
const ignore = options.ignore ?? DEFAULT_IGNORE
|
|
118
|
+
const intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS
|
|
119
|
+
|
|
120
|
+
// Already aborted before we start — don't begin any async work.
|
|
121
|
+
if (options.signal?.aborted) return { stop: () => Promise.resolve() }
|
|
122
|
+
|
|
123
|
+
if (handle.fs.watch) {
|
|
124
|
+
return startNativeWatch(handle, { ...options, root, ignore })
|
|
125
|
+
}
|
|
126
|
+
return startPollWatch(handle, { ...options, root, ignore, intervalMs })
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Native fs.watch path: OS events, disambiguated against a known-path set. */
|
|
130
|
+
async function startNativeWatch(
|
|
131
|
+
handle: SandboxHandle,
|
|
132
|
+
options: WatchOptions & { root: string; ignore: Array<string> },
|
|
133
|
+
): Promise<SandboxWatchHandle> {
|
|
134
|
+
const { onEvent, root, ignore } = options
|
|
135
|
+
const watch = handle.fs.watch
|
|
136
|
+
if (!watch) throw new Error('native watch is unavailable on this provider')
|
|
137
|
+
// Seed the set of existing files so the first event per path is classified
|
|
138
|
+
// correctly (create vs change).
|
|
139
|
+
const known = await collectPaths(handle, root, ignore)
|
|
140
|
+
|
|
141
|
+
const subscription = await watch(root, (raw) => {
|
|
142
|
+
const path = raw.path
|
|
143
|
+
if (isIgnored(path, ignore)) return
|
|
144
|
+
void (async () => {
|
|
145
|
+
const exists = await handle.fs.exists(path)
|
|
146
|
+
const timestamp = Date.now()
|
|
147
|
+
if (!exists) {
|
|
148
|
+
if (known.delete(path)) onEvent({ type: 'delete', path, timestamp })
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
if (known.has(path)) onEvent({ type: 'change', path, timestamp })
|
|
152
|
+
else {
|
|
153
|
+
known.add(path)
|
|
154
|
+
onEvent({ type: 'create', path, timestamp })
|
|
155
|
+
}
|
|
156
|
+
})().catch(() => undefined)
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
const onAbort = (): void => void subscription.stop().catch(() => undefined)
|
|
160
|
+
options.signal?.addEventListener('abort', onAbort, { once: true })
|
|
161
|
+
// The signal may have aborted during the awaits above (the once-listener
|
|
162
|
+
// would have missed it) — tear down now if so.
|
|
163
|
+
if (options.signal?.aborted) void subscription.stop().catch(() => undefined)
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
stop: async () => {
|
|
167
|
+
options.signal?.removeEventListener('abort', onAbort)
|
|
168
|
+
await subscription.stop()
|
|
169
|
+
},
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Exec-poll path: snapshot `find -printf` on an interval and diff. */
|
|
174
|
+
async function startPollWatch(
|
|
175
|
+
handle: SandboxHandle,
|
|
176
|
+
options: WatchOptions & {
|
|
177
|
+
root: string
|
|
178
|
+
ignore: Array<string>
|
|
179
|
+
intervalMs: number
|
|
180
|
+
},
|
|
181
|
+
): Promise<SandboxWatchHandle> {
|
|
182
|
+
const { onEvent, root, ignore, intervalMs } = options
|
|
183
|
+
const command = buildFindCommand(root, ignore)
|
|
184
|
+
const controller = new AbortController()
|
|
185
|
+
|
|
186
|
+
const snapshot = async (): Promise<Map<string, string>> => {
|
|
187
|
+
const result = await handle.process.exec(command, {
|
|
188
|
+
cwd: root,
|
|
189
|
+
signal: controller.signal,
|
|
190
|
+
})
|
|
191
|
+
return result.exitCode === 0
|
|
192
|
+
? parseFindOutput(result.stdout)
|
|
193
|
+
: new Map<string, string>()
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
let previous = await snapshot()
|
|
197
|
+
const state = { running: true }
|
|
198
|
+
|
|
199
|
+
const tick = async (): Promise<void> => {
|
|
200
|
+
if (!state.running) return
|
|
201
|
+
try {
|
|
202
|
+
const next = await snapshot()
|
|
203
|
+
for (const event of diffSnapshots(previous, next, Date.now())) {
|
|
204
|
+
onEvent(event)
|
|
205
|
+
}
|
|
206
|
+
previous = next
|
|
207
|
+
} catch {
|
|
208
|
+
// transient exec failure (e.g. mid-teardown) — try again next tick
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const timer = setInterval(() => void tick(), intervalMs)
|
|
213
|
+
// Don't keep the event loop alive on the watcher alone.
|
|
214
|
+
if (typeof timer.unref === 'function') timer.unref()
|
|
215
|
+
|
|
216
|
+
const stop = (): Promise<void> => {
|
|
217
|
+
if (state.running) {
|
|
218
|
+
state.running = false
|
|
219
|
+
clearInterval(timer)
|
|
220
|
+
controller.abort()
|
|
221
|
+
options.signal?.removeEventListener('abort', onAbort)
|
|
222
|
+
}
|
|
223
|
+
return Promise.resolve()
|
|
224
|
+
}
|
|
225
|
+
const onAbort = (): void => void stop()
|
|
226
|
+
options.signal?.addEventListener('abort', onAbort, { once: true })
|
|
227
|
+
// The signal may have aborted during the initial `await snapshot()` above
|
|
228
|
+
// (the once-listener would have missed it) — tear down now if so.
|
|
229
|
+
if (options.signal?.aborted) void stop()
|
|
230
|
+
|
|
231
|
+
return { stop }
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Recursively collect file paths under `root`, honoring `ignore`. */
|
|
235
|
+
async function collectPaths(
|
|
236
|
+
handle: SandboxHandle,
|
|
237
|
+
root: string,
|
|
238
|
+
ignore: Array<string>,
|
|
239
|
+
): Promise<Set<string>> {
|
|
240
|
+
const files = new Set<string>()
|
|
241
|
+
const walk = async (dir: string): Promise<void> => {
|
|
242
|
+
let entries: Awaited<ReturnType<SandboxHandle['fs']['list']>>
|
|
243
|
+
try {
|
|
244
|
+
entries = await handle.fs.list(dir)
|
|
245
|
+
} catch {
|
|
246
|
+
return
|
|
247
|
+
}
|
|
248
|
+
for (const entry of entries) {
|
|
249
|
+
if (ignore.includes(entry.name)) continue
|
|
250
|
+
if (entry.type === 'dir') await walk(entry.path)
|
|
251
|
+
else files.add(entry.path)
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
await walk(root)
|
|
255
|
+
return files
|
|
256
|
+
}
|
package/src/workspace.ts
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import type { SetupInput } from './setup-plan'
|
|
2
|
+
import type { BearerRef, SecretRef, Secrets } from './secrets'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Workspace definition — the portable description of what the agent sees
|
|
6
|
+
* inside the sandbox. Each harness adapter PROJECTS this into its own native
|
|
7
|
+
* format via `projectWorkspace()` (e.g. Claude Code → CLAUDE.md + .claude/skills
|
|
8
|
+
* + --mcp-config). The definition itself is provider- and harness-agnostic.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** Where the working tree comes from. */
|
|
12
|
+
export type WorkspaceSource =
|
|
13
|
+
| {
|
|
14
|
+
type: 'git'
|
|
15
|
+
url: string
|
|
16
|
+
ref?: string
|
|
17
|
+
auth?: { username?: string; token: string }
|
|
18
|
+
/**
|
|
19
|
+
* Clone depth. Defaults to `1` (shallow). Pass a number for a specific
|
|
20
|
+
* depth, or `'full'` to fetch the entire history.
|
|
21
|
+
*/
|
|
22
|
+
depth?: number | 'full'
|
|
23
|
+
}
|
|
24
|
+
| { type: 'local'; path: string }
|
|
25
|
+
| { type: 'none' }
|
|
26
|
+
|
|
27
|
+
/** Clone a git repo into the workspace. `githubRepo` is a convenience wrapper. */
|
|
28
|
+
export function gitSource(input: {
|
|
29
|
+
url: string
|
|
30
|
+
ref?: string
|
|
31
|
+
auth?: { username?: string; token: string }
|
|
32
|
+
depth?: number | 'full'
|
|
33
|
+
}): WorkspaceSource {
|
|
34
|
+
return { type: 'git', ...input }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function githubRepo(input: {
|
|
38
|
+
repo: string
|
|
39
|
+
ref?: string
|
|
40
|
+
auth?: { username?: string; token: string }
|
|
41
|
+
depth?: number | 'full'
|
|
42
|
+
}): WorkspaceSource {
|
|
43
|
+
const url = input.repo.startsWith('http')
|
|
44
|
+
? input.repo
|
|
45
|
+
: `https://github.com/${input.repo}.git`
|
|
46
|
+
return {
|
|
47
|
+
type: 'git',
|
|
48
|
+
url,
|
|
49
|
+
ref: input.ref,
|
|
50
|
+
auth: input.auth,
|
|
51
|
+
depth: input.depth,
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function localSource(path: string): WorkspaceSource {
|
|
56
|
+
return { type: 'local', path }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* An MCP server config where header names/values may be plain strings or
|
|
61
|
+
* unresolved SecretRef values. Secrets are resolved by each harness projector
|
|
62
|
+
* at projection time — never at definition time.
|
|
63
|
+
*/
|
|
64
|
+
export type McpConfig = {
|
|
65
|
+
headers?: Record<string, string | SecretRef | BearerRef>
|
|
66
|
+
[key: string]: unknown
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** A unit of agent guidance/config projected into the harness's native format. */
|
|
70
|
+
export type WorkspaceSkill =
|
|
71
|
+
| { kind: 'file'; path: string; content: string }
|
|
72
|
+
| { kind: 'agent-skill'; name: string }
|
|
73
|
+
| { kind: 'mcp'; name: string; config: McpConfig }
|
|
74
|
+
| {
|
|
75
|
+
kind: 'git'
|
|
76
|
+
/** Short `owner/repo` or a full HTTPS URL. */
|
|
77
|
+
repo: string
|
|
78
|
+
/** Optional SecretRef for private-repo authentication. */
|
|
79
|
+
secret?: SecretRef
|
|
80
|
+
/** Absolute path inside the sandbox to clone into. Defaults to a `.tanstack-skills/<repo>` dir under the workspace root. */
|
|
81
|
+
into?: string
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Write a file (e.g. CLAUDE.md) into the workspace / harness config. */
|
|
85
|
+
export function fileSkill(input: {
|
|
86
|
+
path: string
|
|
87
|
+
content: string
|
|
88
|
+
}): WorkspaceSkill {
|
|
89
|
+
return { kind: 'file', ...input }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Reference a named agent skill the harness should load. */
|
|
93
|
+
export function agentSkill(name: string): WorkspaceSkill {
|
|
94
|
+
return { kind: 'agent-skill', name }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Project an MCP server into the harness. Header values may be SecretRefs. */
|
|
98
|
+
export function mcpSkill(name: string, config: McpConfig): WorkspaceSkill {
|
|
99
|
+
return { kind: 'mcp', name, config }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Clone a git repository as a workspace skill (e.g. a private skill repo).
|
|
104
|
+
* The clone is performed during bootstrap; `secret` is resolved from the
|
|
105
|
+
* workspace `secrets` registry at that time.
|
|
106
|
+
*/
|
|
107
|
+
export function gitSkill(input: {
|
|
108
|
+
repo: string
|
|
109
|
+
secret?: SecretRef
|
|
110
|
+
into?: string
|
|
111
|
+
}): WorkspaceSkill {
|
|
112
|
+
return { kind: 'git', ...input }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export type PackageManager = 'npm' | 'pnpm' | 'yarn' | 'bun' | 'auto'
|
|
116
|
+
|
|
117
|
+
export interface WorkspaceDefinition {
|
|
118
|
+
source: WorkspaceSource
|
|
119
|
+
/** Defaults to `'auto'` — detect from the lockfile after the source lands. */
|
|
120
|
+
packageManager?: PackageManager
|
|
121
|
+
/** Commands run once during bootstrap. Accepts a string array (serial) or a builder function for serial/parallel groups. */
|
|
122
|
+
setup?: SetupInput
|
|
123
|
+
/** Named commands the agent/user can invoke (e.g. { test: 'pnpm test' }). */
|
|
124
|
+
scripts?: Record<string, string>
|
|
125
|
+
/** Guidance/config projected into the harness. */
|
|
126
|
+
skills?: Array<WorkspaceSkill>
|
|
127
|
+
/**
|
|
128
|
+
* Natural-language instructions written to AGENTS.md (and symlinked as
|
|
129
|
+
* CLAUDE.md, GEMINI.md, etc.) inside the sandbox during bootstrap.
|
|
130
|
+
*/
|
|
131
|
+
instructions?: string
|
|
132
|
+
/**
|
|
133
|
+
* Harness plugin identifiers installed idempotently by each harness
|
|
134
|
+
* projector (e.g. `['@anthropic/plugin-foo']` for Claude Code).
|
|
135
|
+
*/
|
|
136
|
+
plugins?: Array<string>
|
|
137
|
+
/**
|
|
138
|
+
* Typed secret references. The underlying values are injected into the
|
|
139
|
+
* sandbox env at create/resume — NEVER written to snapshots, the
|
|
140
|
+
* SandboxStore, or the event log.
|
|
141
|
+
*/
|
|
142
|
+
secrets?: Secrets
|
|
143
|
+
/** Workspace root inside the sandbox. Defaults to `/workspace`. */
|
|
144
|
+
root?: string
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function defineWorkspace(
|
|
148
|
+
definition: WorkspaceDefinition,
|
|
149
|
+
): WorkspaceDefinition {
|
|
150
|
+
return definition
|
|
151
|
+
}
|