@hachej/boring-agent 0.1.16 → 0.1.18
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/dist/{sandbox-handle-store-hK76cTjn.d.ts → agentPluginEvents-zyIvVjsA.d.ts} +28 -32
- package/dist/{chunk-F3CE5CNW.js → chunk-B5JECXMG.js} +5 -7
- package/dist/front/index.d.ts +23 -7
- package/dist/front/index.js +764 -421
- package/dist/front/styles.css +61 -0
- package/dist/{tool-ui-DSmWuqGe.d.ts → harness-DRrTn_5T.d.ts} +43 -24
- package/dist/server/index.d.ts +79 -10
- package/dist/server/index.js +243 -54
- package/dist/shared/index.d.ts +5 -3
- package/dist/shared/index.js +3 -1
- package/dist/tool-ui-DIFNGwYd.d.ts +20 -0
- package/docs/ACCESSIBILITY.md +55 -0
- package/docs/API.md +64 -0
- package/docs/CSP.md +40 -0
- package/docs/ERROR_CODES.md +54 -0
- package/docs/KNOWN_LIMITATIONS.md +100 -0
- package/docs/MIGRATION.md +50 -0
- package/docs/PERFORMANCE.md +68 -0
- package/docs/PLUGINS.md +108 -0
- package/docs/README.md +17 -0
- package/docs/RISKS-MULTI-TAB.md +46 -0
- package/docs/STYLING.md +71 -0
- package/docs/UI-SHADCN.md +56 -0
- package/docs/VERCEL_COSTS.md +103 -0
- package/docs/plans/AGENT_EVAL_FRAMEWORK.md +466 -0
- package/docs/plans/agent-package-spec.md +1777 -0
- package/docs/plans/harness-followup-capabilities.md +440 -0
- package/docs/plans/harness-tool-ui-capabilities.md +144 -0
- package/docs/plans/pi-followup-history-projection.md +229 -0
- package/docs/plans/pi-tools-migration.md +1061 -0
- package/docs/plans/reviews/pi-followup-history-codex-review.md +11 -0
- package/docs/plans/reviews/pi-followup-history-gpt-review.md +64 -0
- package/docs/plans/reviews/pi-followup-history-opus-review.md +43 -0
- package/docs/plans/reviews/pi-followup-history-xai-review.md +43 -0
- package/docs/plans/vercel-base-snapshot-template-plan.md +527 -0
- package/docs/plans/vercel-persistent-sandbox-adapter.md +553 -0
- package/docs/runtime.md +56 -0
- package/docs/tools.md +75 -0
- package/package.json +4 -3
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
# Vercel Persistent Sandbox Adapter Plan
|
|
2
|
+
|
|
3
|
+
Status: draft
|
|
4
|
+
Date: 2026-04-29
|
|
5
|
+
Owner package: `@boring/agent`
|
|
6
|
+
Consumer app: `apps/full-app`
|
|
7
|
+
|
|
8
|
+
## Summary
|
|
9
|
+
|
|
10
|
+
Move full-app workspaces from Vercel Sandbox stable session IDs to Vercel
|
|
11
|
+
Sandbox beta persistent names.
|
|
12
|
+
|
|
13
|
+
The product invariant stays:
|
|
14
|
+
|
|
15
|
+
> One Boring workspace maps to one durable sandbox identity.
|
|
16
|
+
|
|
17
|
+
With Vercel stable `@vercel/sandbox@1.10.0`, the durable thing we stored was a
|
|
18
|
+
session-like `sandboxId`. When that sandbox stopped and there was no snapshot,
|
|
19
|
+
the filesystem was not recoverable. With Vercel beta persistent sandboxes, the
|
|
20
|
+
durable thing is the sandbox `name`; sessions are compute instances that can be
|
|
21
|
+
stopped and resumed from persisted state.
|
|
22
|
+
|
|
23
|
+
This is not a drop-in package bump. Beta removes the old `sandbox.fs.readdir`
|
|
24
|
+
style API and exposes persistent sandbox methods that resume sessions under
|
|
25
|
+
the hood. We need a new adapter path that speaks the beta API cleanly.
|
|
26
|
+
|
|
27
|
+
## Validated Facts
|
|
28
|
+
|
|
29
|
+
Versions checked:
|
|
30
|
+
|
|
31
|
+
- Current repo dependency: `@vercel/sandbox@1.10.0`
|
|
32
|
+
- Current beta: `@vercel/sandbox@2.0.0-beta.14`
|
|
33
|
+
|
|
34
|
+
Official docs say:
|
|
35
|
+
|
|
36
|
+
- Stable `Sandbox.get({ sandboxId })` reconnects to an active sandbox.
|
|
37
|
+
- Stable snapshots recreate by starting a new sandbox from a snapshot.
|
|
38
|
+
- Beta persistent sandboxes use `Sandbox.create({ name })` and
|
|
39
|
+
`Sandbox.get({ name })`.
|
|
40
|
+
- Beta persistence is enabled by default and can resume a stopped sandbox by
|
|
41
|
+
creating a new session from persisted filesystem state.
|
|
42
|
+
|
|
43
|
+
Live validation performed against beta:
|
|
44
|
+
|
|
45
|
+
1. Created named sandbox with `persistent: true`.
|
|
46
|
+
2. Wrote `probe.txt`.
|
|
47
|
+
3. Stopped the sandbox with `blocking: true`.
|
|
48
|
+
4. Fetched with `Sandbox.get({ name, resume: false })`; status was `stopped`.
|
|
49
|
+
5. Called `readFileToBuffer({ path: "probe.txt" })`.
|
|
50
|
+
6. SDK resumed the sandbox and returned the file content.
|
|
51
|
+
7. Session list became `["stopped", "running"]`.
|
|
52
|
+
8. Test sandbox was deleted.
|
|
53
|
+
|
|
54
|
+
Conclusion: beta file operations do resume a stopped persistent sandbox. We do
|
|
55
|
+
not need a noop wake command before reads if the adapter uses beta `Sandbox`
|
|
56
|
+
methods. Command-backed operations also resume through `runCommand`.
|
|
57
|
+
|
|
58
|
+
## Goals
|
|
59
|
+
|
|
60
|
+
- Make `apps/full-app` use persistent Vercel sandboxes by default once the
|
|
61
|
+
adapter is proven.
|
|
62
|
+
- Keep `@boring/agent` standalone and core-free.
|
|
63
|
+
- Preserve the existing stable Vercel adapter for standalone users until beta
|
|
64
|
+
is GA or explicitly adopted.
|
|
65
|
+
- Store durable sandbox identity as a sandbox name, not a session id.
|
|
66
|
+
- Keep session id as metadata only.
|
|
67
|
+
- Make stopped sandbox recovery automatic when Vercel persistence is enabled.
|
|
68
|
+
- Keep no-snapshot data-loss protection for the stable adapter.
|
|
69
|
+
- Keep path validation inside the workspace adapter before any command or file
|
|
70
|
+
operation crosses into the remote sandbox.
|
|
71
|
+
- Keep full-app wiring simple: it injects stores and chooses mode/channel; it
|
|
72
|
+
does not own sandbox lifecycle behavior.
|
|
73
|
+
|
|
74
|
+
## Non-Goals
|
|
75
|
+
|
|
76
|
+
- Do not delete existing Vercel sandboxes or snapshots.
|
|
77
|
+
- Do not migrate old stopped stable sandboxes that have no snapshot. Their
|
|
78
|
+
filesystem data is already unrecoverable.
|
|
79
|
+
- Do not remove manual snapshot support. Keep it as fallback and for stable.
|
|
80
|
+
- Do not make `@boring/core` import `@boring/agent`.
|
|
81
|
+
- Do not hand-roll shell escaping for file operations. Use validated paths plus
|
|
82
|
+
argv-based Node scripts or beta SDK file methods.
|
|
83
|
+
|
|
84
|
+
## Proposed User-Facing Policy
|
|
85
|
+
|
|
86
|
+
Default full-app policy:
|
|
87
|
+
|
|
88
|
+
- `1 workspace = 1 persistent Vercel sandbox name`
|
|
89
|
+
- Name format is deterministic from workspace id and deployment namespace.
|
|
90
|
+
- Current session id can change over time.
|
|
91
|
+
- Stopping a sandbox is allowed; the next file or command operation resumes it.
|
|
92
|
+
- Workspace reset is the explicit destructive action that creates a new durable
|
|
93
|
+
sandbox name or deletes the existing one.
|
|
94
|
+
|
|
95
|
+
Standalone agent policy:
|
|
96
|
+
|
|
97
|
+
- `vercel-sandbox` remains stable SDK behavior by default.
|
|
98
|
+
- Add opt-in persistent beta channel for users who want named persistence.
|
|
99
|
+
|
|
100
|
+
## Package Strategy
|
|
101
|
+
|
|
102
|
+
Add the beta SDK as an npm alias instead of replacing stable immediately:
|
|
103
|
+
|
|
104
|
+
```json
|
|
105
|
+
{
|
|
106
|
+
"dependencies": {
|
|
107
|
+
"@vercel/sandbox": "^1.10.0",
|
|
108
|
+
"@vercel/sandbox-beta": "npm:@vercel/sandbox@2.0.0-beta.14"
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Rationale:
|
|
114
|
+
|
|
115
|
+
- Stable adapter can continue importing `@vercel/sandbox`.
|
|
116
|
+
- Persistent adapter imports `@vercel/sandbox-beta`.
|
|
117
|
+
- Type surfaces can differ without conditional imports or unsafe casts.
|
|
118
|
+
- We can remove the alias after beta reaches GA.
|
|
119
|
+
|
|
120
|
+
## Runtime Selection
|
|
121
|
+
|
|
122
|
+
Prefer a channel flag over exposing beta as the only mode:
|
|
123
|
+
|
|
124
|
+
```text
|
|
125
|
+
BORING_AGENT_MODE=vercel-sandbox
|
|
126
|
+
BORING_AGENT_VERCEL_SANDBOX_CHANNEL=stable | persistent-beta
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Defaults:
|
|
130
|
+
|
|
131
|
+
- Agent package standalone: `stable`
|
|
132
|
+
- Full app: `persistent-beta`
|
|
133
|
+
|
|
134
|
+
Alternative:
|
|
135
|
+
|
|
136
|
+
- Add a new runtime mode id, `vercel-sandbox-persistent`.
|
|
137
|
+
|
|
138
|
+
Recommendation:
|
|
139
|
+
|
|
140
|
+
- Use the channel flag internally first. The mode remains conceptually Vercel
|
|
141
|
+
sandbox; the channel selects provider API generation.
|
|
142
|
+
|
|
143
|
+
## Store Contract
|
|
144
|
+
|
|
145
|
+
Current `SandboxHandleRecord` assumes `sandboxId` is durable. That is true for
|
|
146
|
+
stable only. Add a discriminated union.
|
|
147
|
+
|
|
148
|
+
Target shape:
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
export type SandboxHandleRecord =
|
|
152
|
+
| {
|
|
153
|
+
kind: "vercel-session"
|
|
154
|
+
workspaceId: string
|
|
155
|
+
sandboxId: string
|
|
156
|
+
snapshotId?: string
|
|
157
|
+
createdAt: string
|
|
158
|
+
lastUsedAt: string
|
|
159
|
+
}
|
|
160
|
+
| {
|
|
161
|
+
kind: "vercel-persistent"
|
|
162
|
+
workspaceId: string
|
|
163
|
+
sandboxName: string
|
|
164
|
+
currentSessionId?: string
|
|
165
|
+
currentSnapshotId?: string
|
|
166
|
+
status?: string
|
|
167
|
+
persistent: true
|
|
168
|
+
createdAt: string
|
|
169
|
+
lastUsedAt: string
|
|
170
|
+
lastSeenAt?: string
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Compatibility rules:
|
|
175
|
+
|
|
176
|
+
- Existing records with no `kind` are read as `vercel-session`.
|
|
177
|
+
- Stable resolver only accepts `vercel-session`.
|
|
178
|
+
- Persistent resolver only accepts `vercel-persistent`.
|
|
179
|
+
- If channel changes for a workspace, return a clear configuration error and
|
|
180
|
+
require explicit reset/migration.
|
|
181
|
+
|
|
182
|
+
## Core DB Changes
|
|
183
|
+
|
|
184
|
+
Core owns the DB-backed store, so add fields to `WorkspaceRuntime`.
|
|
185
|
+
|
|
186
|
+
Recommended columns:
|
|
187
|
+
|
|
188
|
+
- `sandbox_handle_kind text null`
|
|
189
|
+
- `sandbox_name text null`
|
|
190
|
+
- `sandbox_id text null`
|
|
191
|
+
- `sandbox_current_session_id text null`
|
|
192
|
+
- `sandbox_status text null`
|
|
193
|
+
- `sandbox_persistent boolean null`
|
|
194
|
+
- `sandbox_sdk_channel text null`
|
|
195
|
+
- `sandbox_snapshot_id text null`
|
|
196
|
+
- `sandbox_created_at timestamptz null`
|
|
197
|
+
- `sandbox_last_seen_at timestamptz null`
|
|
198
|
+
- `sandbox_last_used_at timestamptz null`
|
|
199
|
+
|
|
200
|
+
Mapping:
|
|
201
|
+
|
|
202
|
+
- Stable:
|
|
203
|
+
- `sandbox_handle_kind = "vercel-session"`
|
|
204
|
+
- `sandbox_id = "sbx_..."`
|
|
205
|
+
- `sandbox_snapshot_id = "snap_..." | null`
|
|
206
|
+
- Persistent beta:
|
|
207
|
+
- `sandbox_handle_kind = "vercel-persistent"`
|
|
208
|
+
- `sandbox_name = "boring-<namespace>-<workspace-hash>"`
|
|
209
|
+
- `sandbox_current_session_id = "sbx_..." | null`
|
|
210
|
+
- `sandbox_snapshot_id = current Vercel snapshot id | null`
|
|
211
|
+
- `sandbox_persistent = true`
|
|
212
|
+
- `sandbox_sdk_channel = "persistent-beta"`
|
|
213
|
+
|
|
214
|
+
Name generation:
|
|
215
|
+
|
|
216
|
+
- Add `BORING_AGENT_VERCEL_SANDBOX_NAME_PREFIX`.
|
|
217
|
+
- Default prefix: `boring`.
|
|
218
|
+
- Include deployment namespace, default `dev`.
|
|
219
|
+
- Hash workspace id to avoid provider naming constraints.
|
|
220
|
+
- Example: `boring-dev-ws-6f8d4a9c2b0e`.
|
|
221
|
+
|
|
222
|
+
The exact name validator must be backed by either Vercel docs or live API
|
|
223
|
+
errors. Until documented, keep names lowercase alphanumeric plus hyphens.
|
|
224
|
+
|
|
225
|
+
## Agent Architecture
|
|
226
|
+
|
|
227
|
+
Add persistent beta files under the existing Vercel sandbox area:
|
|
228
|
+
|
|
229
|
+
```text
|
|
230
|
+
packages/agent/src/server/sandbox/vercel-sandbox/
|
|
231
|
+
persistentClient.ts
|
|
232
|
+
resolvePersistentSandboxHandle.ts
|
|
233
|
+
createPersistentVercelSandboxWorkspace.ts
|
|
234
|
+
createPersistentVercelSandboxExec.ts
|
|
235
|
+
persistentFileOps.ts
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Keep stable files in place:
|
|
239
|
+
|
|
240
|
+
```text
|
|
241
|
+
resolveSandboxHandle.ts
|
|
242
|
+
createVercelSandboxWorkspace.ts
|
|
243
|
+
createVercelSandboxExec.ts
|
|
244
|
+
periodicSnapshot.ts
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
`createVercelSandboxModeAdapter()` becomes a small dispatcher:
|
|
248
|
+
|
|
249
|
+
1. Resolve auth.
|
|
250
|
+
2. Read `BORING_AGENT_VERCEL_SANDBOX_CHANNEL`.
|
|
251
|
+
3. If `stable`, use current stable adapter path.
|
|
252
|
+
4. If `persistent-beta`, use new persistent path.
|
|
253
|
+
|
|
254
|
+
Longer-term cleanup:
|
|
255
|
+
|
|
256
|
+
- Extract common auth, timeout, template packaging, logging, and dirty tracking
|
|
257
|
+
helpers after both paths are tested.
|
|
258
|
+
|
|
259
|
+
## Persistent Resolver
|
|
260
|
+
|
|
261
|
+
New resolver behavior:
|
|
262
|
+
|
|
263
|
+
1. Normalize workspace id.
|
|
264
|
+
2. Compute deterministic sandbox name.
|
|
265
|
+
3. Load store record.
|
|
266
|
+
4. If record exists:
|
|
267
|
+
- verify `kind === "vercel-persistent"`
|
|
268
|
+
- prefer stored `sandboxName`
|
|
269
|
+
- call `Sandbox.get({ name })`
|
|
270
|
+
5. If not found:
|
|
271
|
+
- call `Sandbox.create({ name, persistent: true, ...template/source })`
|
|
272
|
+
6. If create conflicts because another process won:
|
|
273
|
+
- call `Sandbox.get({ name })`
|
|
274
|
+
7. Persist metadata:
|
|
275
|
+
- `sandboxName`
|
|
276
|
+
- `currentSessionId`
|
|
277
|
+
- `currentSnapshotId`
|
|
278
|
+
- `status`
|
|
279
|
+
- timestamps
|
|
280
|
+
|
|
281
|
+
Use `Sandbox.get({ name })` with default `resume: true` for normal route
|
|
282
|
+
resolution. Use `resume: false` only for diagnostics or non-mutating status
|
|
283
|
+
checks where waking the sandbox would be surprising.
|
|
284
|
+
|
|
285
|
+
## Workspace Adapter
|
|
286
|
+
|
|
287
|
+
Beta SDK has no `sandbox.fs.readdir`. Implement the `Workspace` interface with
|
|
288
|
+
a mix of beta methods and command-backed Node scripts.
|
|
289
|
+
|
|
290
|
+
Path rules:
|
|
291
|
+
|
|
292
|
+
- Accept only user relative paths.
|
|
293
|
+
- Use existing `validatePath` logic.
|
|
294
|
+
- Convert validated paths to sandbox-relative paths under `/vercel/sandbox`.
|
|
295
|
+
- Never concatenate user paths into shell command strings.
|
|
296
|
+
|
|
297
|
+
Operation mapping:
|
|
298
|
+
|
|
299
|
+
| Workspace method | Persistent implementation |
|
|
300
|
+
|---|---|
|
|
301
|
+
| `readFile` | `sandbox.readFileToBuffer({ path, cwd })` |
|
|
302
|
+
| `writeFile` | `sandbox.writeFiles([{ path, content }])` |
|
|
303
|
+
| `mkdir` | `sandbox.mkDir(path)` for simple mkdir; Node script for recursive if needed |
|
|
304
|
+
| `readdir` | `runCommand(node, ["-e", script, "--", path])` returning JSON |
|
|
305
|
+
| `stat` | `runCommand(node, ["-e", script, "--", path])` returning JSON |
|
|
306
|
+
| `unlink` | Node script with `fs.rm(path, { recursive: false })` |
|
|
307
|
+
| `rename` | Node script with `fs.rename(from, to)` |
|
|
308
|
+
|
|
309
|
+
Node script requirements:
|
|
310
|
+
|
|
311
|
+
- Lives in source as a string constant or small packaged helper.
|
|
312
|
+
- Receives paths through argv, not interpolated source.
|
|
313
|
+
- Emits a single JSON object.
|
|
314
|
+
- Maps common errors to stable Boring error codes.
|
|
315
|
+
- Has unit tests for path traversal, missing path, and bad JSON.
|
|
316
|
+
|
|
317
|
+
Cache:
|
|
318
|
+
|
|
319
|
+
- Keep existing short metadata caches for `readdir` and `stat`.
|
|
320
|
+
- Invalidate on write, unlink, mkdir, rename, and any sandbox command.
|
|
321
|
+
- Since beta can resume and create a new session, cache keys must not depend on
|
|
322
|
+
session id unless we intentionally want session-local caches.
|
|
323
|
+
|
|
324
|
+
## Exec Adapter
|
|
325
|
+
|
|
326
|
+
Beta `sandbox.runCommand()` is compatible enough for the current exec shape.
|
|
327
|
+
Create a beta-specific exec wrapper anyway so types do not depend on stable
|
|
328
|
+
`Sandbox`.
|
|
329
|
+
|
|
330
|
+
Behavior:
|
|
331
|
+
|
|
332
|
+
- Run `sh -c` exactly as the stable exec adapter does for agent commands.
|
|
333
|
+
- Preserve timeout, heartbeat, output cap, stdout/stderr streaming.
|
|
334
|
+
- Invalidate workspace metadata cache after command completion.
|
|
335
|
+
- Persist latest session metadata after resume if available.
|
|
336
|
+
|
|
337
|
+
Risk:
|
|
338
|
+
|
|
339
|
+
- `sh -c` is acceptable for agent-authored commands, but file operations must
|
|
340
|
+
not use shell strings.
|
|
341
|
+
|
|
342
|
+
## Snapshot Policy After Persistent Beta
|
|
343
|
+
|
|
344
|
+
Persistent beta changes the primary policy:
|
|
345
|
+
|
|
346
|
+
- Automatic Vercel persistence is primary.
|
|
347
|
+
- Manual dirty periodic snapshots are fallback only.
|
|
348
|
+
|
|
349
|
+
For stable:
|
|
350
|
+
|
|
351
|
+
- Keep current dirty snapshot scheduler.
|
|
352
|
+
- Keep `SANDBOX_EXPIRED` if no snapshot exists.
|
|
353
|
+
|
|
354
|
+
For persistent beta:
|
|
355
|
+
|
|
356
|
+
- Do not run periodic snapshot scheduler by default because `snapshot()` stops
|
|
357
|
+
the current session.
|
|
358
|
+
- Store `currentSnapshotId` if Vercel reports one.
|
|
359
|
+
- Offer explicit "checkpoint now" later if product needs it.
|
|
360
|
+
- On app shutdown, do not force snapshot; let Vercel persistence handle stop.
|
|
361
|
+
|
|
362
|
+
## Full-App Wiring
|
|
363
|
+
|
|
364
|
+
Full-app should only configure and inject:
|
|
365
|
+
|
|
366
|
+
- `WorkspaceRuntimeSandboxHandleStore`
|
|
367
|
+
- `BORING_AGENT_MODE=vercel-sandbox`
|
|
368
|
+
- `BORING_AGENT_VERCEL_SANDBOX_CHANNEL=persistent-beta`
|
|
369
|
+
- credentials and timeout/prefix env vars
|
|
370
|
+
|
|
371
|
+
Full-app should not:
|
|
372
|
+
|
|
373
|
+
- import Vercel SDK directly
|
|
374
|
+
- know how resume works
|
|
375
|
+
- mutate sandbox handles outside the store implementation
|
|
376
|
+
|
|
377
|
+
## Migration Strategy
|
|
378
|
+
|
|
379
|
+
No silent migration from stable stopped sandboxes.
|
|
380
|
+
|
|
381
|
+
For active stable workspaces:
|
|
382
|
+
|
|
383
|
+
1. Leave current stable record intact.
|
|
384
|
+
2. When full-app switches to persistent beta, create a new persistent sandbox
|
|
385
|
+
name for the workspace.
|
|
386
|
+
3. Seed from template or current workspace source of truth if available.
|
|
387
|
+
4. If old stable sandbox is running and user asks to migrate files, add a
|
|
388
|
+
separate explicit migration command later.
|
|
389
|
+
|
|
390
|
+
For stopped stable workspaces with no snapshot:
|
|
391
|
+
|
|
392
|
+
- Return clear status: old sandbox expired and had no recoverable snapshot.
|
|
393
|
+
- Create new persistent sandbox only after explicit reset or provisioning path.
|
|
394
|
+
|
|
395
|
+
For development local state:
|
|
396
|
+
|
|
397
|
+
- Allow `BORING_AGENT_VERCEL_SANDBOX_CHANNEL=stable` to keep using current
|
|
398
|
+
DB rows during rollout.
|
|
399
|
+
|
|
400
|
+
## Rollout Phases
|
|
401
|
+
|
|
402
|
+
### Phase 1 - Types, Store, Config
|
|
403
|
+
|
|
404
|
+
- Add beta dependency alias.
|
|
405
|
+
- Extend config schema with:
|
|
406
|
+
- `BORING_AGENT_VERCEL_SANDBOX_CHANNEL`
|
|
407
|
+
- `BORING_AGENT_VERCEL_SANDBOX_NAME_PREFIX`
|
|
408
|
+
- `BORING_AGENT_VERCEL_SANDBOX_NAMESPACE`
|
|
409
|
+
- Add discriminated sandbox handle type.
|
|
410
|
+
- Update `FileHandleStore`.
|
|
411
|
+
- Update `WorkspaceRuntimeSandboxHandleStore`.
|
|
412
|
+
- Add DB migration and local store support.
|
|
413
|
+
- Add type tests.
|
|
414
|
+
|
|
415
|
+
Acceptance:
|
|
416
|
+
|
|
417
|
+
- Existing stable tests pass.
|
|
418
|
+
- Old JSON records without `kind` still load as stable records.
|
|
419
|
+
- Core store round-trips stable and persistent records.
|
|
420
|
+
|
|
421
|
+
### Phase 2 - Persistent Resolver
|
|
422
|
+
|
|
423
|
+
- Add beta client wrapper.
|
|
424
|
+
- Add deterministic name generator.
|
|
425
|
+
- Add `resolvePersistentSandboxHandle()`.
|
|
426
|
+
- Add race-safe get/create/get behavior.
|
|
427
|
+
- Add tests for stopped status, missing record, conflict, and wrong kind.
|
|
428
|
+
|
|
429
|
+
Acceptance:
|
|
430
|
+
|
|
431
|
+
- Resolver never stores session id as durable identity.
|
|
432
|
+
- `Sandbox.get({ name })` path resumes by default.
|
|
433
|
+
- Wrong channel for existing record gives a stable explicit error.
|
|
434
|
+
|
|
435
|
+
### Phase 3 - Persistent Workspace And Exec
|
|
436
|
+
|
|
437
|
+
- Add persistent workspace adapter.
|
|
438
|
+
- Add command-backed `readdir`, `stat`, `unlink`, `rename`.
|
|
439
|
+
- Add persistent exec wrapper.
|
|
440
|
+
- Generalize metadata invalidation to support both stable and beta sandbox
|
|
441
|
+
object types.
|
|
442
|
+
- Wire persistent path into runtime mode dispatcher.
|
|
443
|
+
|
|
444
|
+
Acceptance:
|
|
445
|
+
|
|
446
|
+
- File tree loads after a sandbox was stopped.
|
|
447
|
+
- Read/write/mkdir/rename/delete work after stop/resume.
|
|
448
|
+
- Agent command execution resumes stopped sandbox.
|
|
449
|
+
- No file operation interpolates user path into shell source.
|
|
450
|
+
|
|
451
|
+
### Phase 4 - Full-App Default And Smoke
|
|
452
|
+
|
|
453
|
+
- Set full-app env defaults to persistent beta.
|
|
454
|
+
- Add README/env example docs.
|
|
455
|
+
- Add an opt-in live smoke script for beta persistence:
|
|
456
|
+
- create named sandbox
|
|
457
|
+
- write file
|
|
458
|
+
- stop
|
|
459
|
+
- read file
|
|
460
|
+
- list sessions
|
|
461
|
+
- delete test sandbox
|
|
462
|
+
- Run local full-app against Vercel beta channel.
|
|
463
|
+
|
|
464
|
+
Acceptance:
|
|
465
|
+
|
|
466
|
+
- `/api/v1/tree` works for a new workspace.
|
|
467
|
+
- Stop sandbox in Vercel, reload file tree, it resumes and still sees files.
|
|
468
|
+
- DB row stores sandbox name and latest session id.
|
|
469
|
+
- Stable channel still works in package tests.
|
|
470
|
+
|
|
471
|
+
### Phase 5 - Cleanup After Beta GA
|
|
472
|
+
|
|
473
|
+
- Revisit dependency alias.
|
|
474
|
+
- If Vercel stable catches up, remove stable-specific snapshot recreation where
|
|
475
|
+
no longer needed.
|
|
476
|
+
- Decide whether to rename channel from `persistent-beta` to `persistent`.
|
|
477
|
+
- Keep backwards compatibility for old stable records until an explicit cleanup
|
|
478
|
+
bead removes it.
|
|
479
|
+
|
|
480
|
+
## Test Matrix
|
|
481
|
+
|
|
482
|
+
Unit tests:
|
|
483
|
+
|
|
484
|
+
- Config channel parsing.
|
|
485
|
+
- Name generation is deterministic and provider-safe.
|
|
486
|
+
- Store contract discriminates stable vs persistent.
|
|
487
|
+
- Persistent resolver get/create/conflict.
|
|
488
|
+
- Persistent workspace path validation.
|
|
489
|
+
- Persistent `readdir` and `stat` JSON parsing.
|
|
490
|
+
- Persistent mutations invalidate caches and mark dirty.
|
|
491
|
+
- Persistent exec streams stdout/stderr and handles timeout.
|
|
492
|
+
|
|
493
|
+
Integration tests with mocked beta SDK:
|
|
494
|
+
|
|
495
|
+
- Stopped sandbox resumes on `readFileToBuffer`.
|
|
496
|
+
- Stopped sandbox resumes on `runCommand`.
|
|
497
|
+
- Session id changes after resume and store is updated.
|
|
498
|
+
- Wrong channel existing record returns stable error.
|
|
499
|
+
|
|
500
|
+
Opt-in live smoke:
|
|
501
|
+
|
|
502
|
+
- Requires Vercel token/team/project env.
|
|
503
|
+
- Creates a unique sandbox name.
|
|
504
|
+
- Deletes it at the end.
|
|
505
|
+
- Must not run in default CI.
|
|
506
|
+
|
|
507
|
+
Full-app smoke:
|
|
508
|
+
|
|
509
|
+
- Sign up/login.
|
|
510
|
+
- Create workspace.
|
|
511
|
+
- Fetch file tree.
|
|
512
|
+
- Write file.
|
|
513
|
+
- Stop sandbox externally or via test helper.
|
|
514
|
+
- Fetch file tree again.
|
|
515
|
+
- Assert file remains.
|
|
516
|
+
|
|
517
|
+
## Risks
|
|
518
|
+
|
|
519
|
+
- Beta API can change. Mitigation: isolate imports behind
|
|
520
|
+
`persistentClient.ts` and use package alias.
|
|
521
|
+
- Provider naming constraints are not clearly documented. Mitigation: hashed
|
|
522
|
+
lowercase names, live smoke test, explicit error if rejected.
|
|
523
|
+
- `snapshot()` stops sessions. Mitigation: disable periodic snapshot scheduler
|
|
524
|
+
by default in persistent beta.
|
|
525
|
+
- Filetree via `runCommand` may be slower than old `fs.readdir`. Mitigation:
|
|
526
|
+
cache metadata and use one Node script that returns exactly what the UI needs.
|
|
527
|
+
- Race on first workspace open. Mitigation: create conflict falls back to get.
|
|
528
|
+
- Channel switch can strand old stable records. Mitigation: explicit error and
|
|
529
|
+
reset path, no silent replacement.
|
|
530
|
+
|
|
531
|
+
## Open Questions
|
|
532
|
+
|
|
533
|
+
- Should full-app default to `persistent-beta` immediately, or behind an env
|
|
534
|
+
flag until a live smoke passes on the deployment host?
|
|
535
|
+
- Do we want a user-visible "restart sandbox" action, or should resume remain
|
|
536
|
+
invisible?
|
|
537
|
+
- Should workspace reset delete the persistent sandbox remotely, or only detach
|
|
538
|
+
it from the DB row? Deletion is destructive and needs explicit UX.
|
|
539
|
+
- Should we expose session history/snapshots in workspace settings later?
|
|
540
|
+
- Do we seed persistent sandboxes from tarball template only, or also support
|
|
541
|
+
importing from a running stable sandbox as a separate migration action?
|
|
542
|
+
|
|
543
|
+
## Done Criteria
|
|
544
|
+
|
|
545
|
+
- Full-app uses persistent Vercel sandbox names by default.
|
|
546
|
+
- One workspace can survive sandbox stop and reload file tree without losing
|
|
547
|
+
files.
|
|
548
|
+
- DB shows durable sandbox name plus current session id.
|
|
549
|
+
- No silent empty sandbox creation on stable expired handles.
|
|
550
|
+
- Stable `vercel-sandbox` package behavior remains tested.
|
|
551
|
+
- `pnpm --filter @boring/agent typecheck` passes.
|
|
552
|
+
- Relevant agent/core/full-app tests pass.
|
|
553
|
+
- Live smoke script passes against Vercel beta and cleans up after itself.
|
package/docs/runtime.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
> The boring-ui agent supports multiple runtime modes. Ask it to configure the right one for your deployment.
|
|
2
|
+
|
|
3
|
+
# Runtime Modes
|
|
4
|
+
|
|
5
|
+
The agent supports three execution modes controlling how `bash` and filesystem tools run.
|
|
6
|
+
|
|
7
|
+
## Modes
|
|
8
|
+
|
|
9
|
+
| mode | description | use when |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| `direct` | tools run directly in the host process | local dev, trusted environments |
|
|
12
|
+
| `local` | tools run in a `bwrap` sandbox (Linux only) | self-hosted, untrusted input |
|
|
13
|
+
| `vercel-sandbox` | tools run in Vercel Firecracker microVMs | production on Vercel |
|
|
14
|
+
|
|
15
|
+
Set via env var:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
BORING_AGENT_MODE=vercel-sandbox
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Defaults to `direct` when unset.
|
|
22
|
+
|
|
23
|
+
## vercel-sandbox
|
|
24
|
+
|
|
25
|
+
Each workspace session gets its own Firecracker microVM. Files and shell state persist across turns within a session. Snapshots are taken every 10 minutes.
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
BORING_AGENT_MODE=vercel-sandbox
|
|
29
|
+
VERCEL_TEAM_ID=team_...
|
|
30
|
+
VERCEL_PROJECT_ID=prj_...
|
|
31
|
+
# production: uses Vercel OIDC automatically
|
|
32
|
+
# local emulation: set VERCEL_TOKEN
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Sandbox lifetime:
|
|
36
|
+
```bash
|
|
37
|
+
BORING_AGENT_VERCEL_SANDBOX_TIMEOUT_MS=2700000 # max 2700000ms (Vercel limit)
|
|
38
|
+
BORING_AGENT_SNAPSHOT_KEEP=2 # retained snapshots per workspace
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## local (bwrap)
|
|
42
|
+
|
|
43
|
+
Linux only. Wraps tool execution in a `bubblewrap` sandbox. The workspace root is mounted read-write; the rest of the filesystem is read-only.
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
BORING_AGENT_MODE=local
|
|
47
|
+
BORING_AGENT_WORKSPACE_ROOT=/home/ubuntu/projects/my-app
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Workspace root
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
BORING_AGENT_WORKSPACE_ROOT=/absolute/path/to/workspace
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
When unset, defaults to the current working directory at server start.
|
package/docs/tools.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
> The boring-ui agent has a built-in tool catalog. Ask it to use or extend these tools.
|
|
2
|
+
|
|
3
|
+
# Agent Tools
|
|
4
|
+
|
|
5
|
+
The agent runtime ships a catalog of tools for interacting with the workspace filesystem and shell.
|
|
6
|
+
|
|
7
|
+
## Built-in tools
|
|
8
|
+
|
|
9
|
+
| tool | description |
|
|
10
|
+
|---|---|
|
|
11
|
+
| `read` | Read a file's contents |
|
|
12
|
+
| `write` | Write or overwrite a file |
|
|
13
|
+
| `edit` | Apply a targeted string replacement to a file |
|
|
14
|
+
| `bash` | Execute a shell command |
|
|
15
|
+
| `find` | Find files by name pattern |
|
|
16
|
+
| `grep` | Search file contents |
|
|
17
|
+
| `ls` | List directory contents |
|
|
18
|
+
| `exec_ui` | Post a command to the workspace UI (open panels, navigate, etc.) |
|
|
19
|
+
| `get_ui_state` | Read what panels are currently open |
|
|
20
|
+
|
|
21
|
+
## exec_ui
|
|
22
|
+
|
|
23
|
+
The primary tool for interacting with the workspace frontend. See [bridge.md](../../workspace/docs/bridge.md) for the full command reference.
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"kind": "openSurface",
|
|
28
|
+
"params": { "kind": "my-plugin.open", "target": "item-123" }
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Adding custom tools
|
|
33
|
+
|
|
34
|
+
For statically composed app/server integrations, contribute tools from a
|
|
35
|
+
workspace server plugin:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { defineServerPlugin } from "@hachej/boring-workspace/server"
|
|
39
|
+
|
|
40
|
+
export default defineServerPlugin({
|
|
41
|
+
id: "my-plugin",
|
|
42
|
+
systemPrompt: "Use my_tool when the user asks to process an item.",
|
|
43
|
+
agentTools: [
|
|
44
|
+
{
|
|
45
|
+
name: "my_tool",
|
|
46
|
+
description: "Does something useful",
|
|
47
|
+
parameters: {
|
|
48
|
+
type: "object",
|
|
49
|
+
properties: { id: { type: "string" } },
|
|
50
|
+
required: ["id"],
|
|
51
|
+
additionalProperties: false,
|
|
52
|
+
},
|
|
53
|
+
async execute(params) {
|
|
54
|
+
return {
|
|
55
|
+
content: [{ type: "text", text: `processed ${String(params.id)}` }],
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
})
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Expose that entry with `package.json#boring.server` or pass the plugin object to
|
|
64
|
+
`createWorkspaceAgentServer({ plugins: [...] })`. This is static/boot-time
|
|
65
|
+
server composition; restart the host process after changing routes or tools.
|
|
66
|
+
|
|
67
|
+
For hot-reloadable chat behavior in user plugin packages, prefer Pi-native
|
|
68
|
+
resources declared in `package.json#pi` (`extensions`, `skills`, `prompts`, and
|
|
69
|
+
`systemPrompt`). Those participate in the `/reload` path and are the right
|
|
70
|
+
place for tools/skills that should update without restarting the workspace
|
|
71
|
+
server.
|
|
72
|
+
|
|
73
|
+
## Runtime modes
|
|
74
|
+
|
|
75
|
+
See [runtime.md](./runtime.md) for how tool execution is sandboxed.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hachej/boring-agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.18",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Pane-embeddable coding agent. Ships direct/local/vercel-sandbox execution modes behind one interface.",
|
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
"**/*.css"
|
|
38
38
|
],
|
|
39
39
|
"files": [
|
|
40
|
-
"dist"
|
|
40
|
+
"dist",
|
|
41
|
+
"docs"
|
|
41
42
|
],
|
|
42
43
|
"dependencies": {
|
|
43
44
|
"@ai-sdk/react": "^3.0.170",
|
|
@@ -74,7 +75,7 @@
|
|
|
74
75
|
"use-stick-to-bottom": "^1.1.3",
|
|
75
76
|
"yaml": "^2.8.3",
|
|
76
77
|
"zod": "^3.25.76",
|
|
77
|
-
"@hachej/boring-ui-kit": "0.1.
|
|
78
|
+
"@hachej/boring-ui-kit": "0.1.18"
|
|
78
79
|
},
|
|
79
80
|
"devDependencies": {
|
|
80
81
|
"@opentelemetry/api": "^1.9.1",
|