@hachej/boring-agent 0.1.17 → 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.
Files changed (39) hide show
  1. package/dist/{sandbox-handle-store-hK76cTjn.d.ts → agentPluginEvents-zyIvVjsA.d.ts} +28 -32
  2. package/dist/{chunk-F3CE5CNW.js → chunk-B5JECXMG.js} +5 -7
  3. package/dist/front/index.d.ts +23 -7
  4. package/dist/front/index.js +764 -421
  5. package/dist/front/styles.css +61 -0
  6. package/dist/{tool-ui-DSmWuqGe.d.ts → harness-DRrTn_5T.d.ts} +43 -24
  7. package/dist/server/index.d.ts +79 -10
  8. package/dist/server/index.js +243 -54
  9. package/dist/shared/index.d.ts +5 -3
  10. package/dist/shared/index.js +3 -1
  11. package/dist/tool-ui-DIFNGwYd.d.ts +20 -0
  12. package/docs/ACCESSIBILITY.md +55 -0
  13. package/docs/API.md +64 -0
  14. package/docs/CSP.md +40 -0
  15. package/docs/ERROR_CODES.md +54 -0
  16. package/docs/KNOWN_LIMITATIONS.md +100 -0
  17. package/docs/MIGRATION.md +50 -0
  18. package/docs/PERFORMANCE.md +68 -0
  19. package/docs/PLUGINS.md +108 -0
  20. package/docs/README.md +17 -0
  21. package/docs/RISKS-MULTI-TAB.md +46 -0
  22. package/docs/STYLING.md +71 -0
  23. package/docs/UI-SHADCN.md +56 -0
  24. package/docs/VERCEL_COSTS.md +103 -0
  25. package/docs/plans/AGENT_EVAL_FRAMEWORK.md +466 -0
  26. package/docs/plans/agent-package-spec.md +1777 -0
  27. package/docs/plans/harness-followup-capabilities.md +440 -0
  28. package/docs/plans/harness-tool-ui-capabilities.md +144 -0
  29. package/docs/plans/pi-followup-history-projection.md +229 -0
  30. package/docs/plans/pi-tools-migration.md +1061 -0
  31. package/docs/plans/reviews/pi-followup-history-codex-review.md +11 -0
  32. package/docs/plans/reviews/pi-followup-history-gpt-review.md +64 -0
  33. package/docs/plans/reviews/pi-followup-history-opus-review.md +43 -0
  34. package/docs/plans/reviews/pi-followup-history-xai-review.md +43 -0
  35. package/docs/plans/vercel-base-snapshot-template-plan.md +527 -0
  36. package/docs/plans/vercel-persistent-sandbox-adapter.md +553 -0
  37. package/docs/runtime.md +56 -0
  38. package/docs/tools.md +75 -0
  39. 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.
@@ -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.17",
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.17"
78
+ "@hachej/boring-ui-kit": "0.1.18"
78
79
  },
79
80
  "devDependencies": {
80
81
  "@opentelemetry/api": "^1.9.1",