@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,527 @@
|
|
|
1
|
+
# Vercel Base Snapshot Template Plan
|
|
2
|
+
|
|
3
|
+
Status: draft for review
|
|
4
|
+
Date: 2026-04-30
|
|
5
|
+
Owner package: `@boring/agent`
|
|
6
|
+
Consumers: `apps/full-app`, future `@boring/cloud`
|
|
7
|
+
|
|
8
|
+
## Problem
|
|
9
|
+
|
|
10
|
+
New Vercel sandboxes currently pay cold-start setup cost every time they need
|
|
11
|
+
runtime dependencies, template files, or package installs. We also need a clear
|
|
12
|
+
policy for what happens when the app changes its default sandbox image while
|
|
13
|
+
existing workspaces already have user files.
|
|
14
|
+
|
|
15
|
+
The product invariant stays:
|
|
16
|
+
|
|
17
|
+
> One workspace maps to one durable sandbox identity.
|
|
18
|
+
|
|
19
|
+
A reusable base snapshot can improve first boot, but it must not become the
|
|
20
|
+
source of truth for an existing workspace. Once a workspace has user mutations,
|
|
21
|
+
the workspace's own persistent sandbox state or workspace snapshot wins.
|
|
22
|
+
|
|
23
|
+
## Validated Provider Facts
|
|
24
|
+
|
|
25
|
+
Official Vercel docs checked on 2026-04-30:
|
|
26
|
+
|
|
27
|
+
- `Sandbox.create()` accepts `source: { type: "snapshot", snapshotId }`.
|
|
28
|
+
- `sandbox.snapshot()` captures filesystem and installed packages, then stops
|
|
29
|
+
the source sandbox automatically.
|
|
30
|
+
- Snapshots expire after 30 days by default; `expiration: 0` disables expiry.
|
|
31
|
+
- Non-persistent sandboxes lose filesystem data when stopped unless a snapshot
|
|
32
|
+
exists.
|
|
33
|
+
- Beta persistent sandboxes use a durable `name`; stopping auto-saves state,
|
|
34
|
+
and resuming creates a new compute session from that saved state.
|
|
35
|
+
- Beta persistence is enabled by default and can be disabled with
|
|
36
|
+
`persistent: false`.
|
|
37
|
+
|
|
38
|
+
Implementation note:
|
|
39
|
+
|
|
40
|
+
- Keep a code comment next to `snapshot({ expiration: 0 })` that cites this
|
|
41
|
+
provider behavior. If Vercel changes the zero-expiry contract, bake tooling
|
|
42
|
+
must fail validation instead of creating snapshots that expire unexpectedly.
|
|
43
|
+
|
|
44
|
+
Sources:
|
|
45
|
+
|
|
46
|
+
- https://vercel.com/docs/vercel-sandbox/sdk-reference
|
|
47
|
+
- https://vercel.com/docs/vercel-sandbox/concepts/snapshots
|
|
48
|
+
- https://vercel.com/docs/vercel-sandbox/concepts
|
|
49
|
+
- https://vercel.com/changelog/vercel-sandbox-persistent-sandboxes-beta
|
|
50
|
+
|
|
51
|
+
## Goals
|
|
52
|
+
|
|
53
|
+
- Define an app-level base snapshot that all new Vercel workspaces can start
|
|
54
|
+
from.
|
|
55
|
+
- Keep base snapshot state separate from workspace state.
|
|
56
|
+
- Make invalidation explicit: bake a new base snapshot, rotate config, keep old
|
|
57
|
+
snapshots while any workspace still references them.
|
|
58
|
+
- Support both stable snapshot-based sandboxes and beta persistent named
|
|
59
|
+
sandboxes.
|
|
60
|
+
- Keep `@boring/agent` core-free and app-agnostic.
|
|
61
|
+
- Keep full-app wiring thin: it supplies config and stores, but does not own
|
|
62
|
+
sandbox lifecycle behavior.
|
|
63
|
+
- Keep the DB model provider-neutral so Fly, Modal, local volumes, or future
|
|
64
|
+
providers can use equivalent metadata.
|
|
65
|
+
|
|
66
|
+
## Non-Goals
|
|
67
|
+
|
|
68
|
+
- Do not reset existing workspaces to the new base snapshot automatically.
|
|
69
|
+
- Do not put secrets, user files, provider tokens, or per-user auth material in
|
|
70
|
+
the base snapshot.
|
|
71
|
+
- Do not delete old provider snapshots during rollout.
|
|
72
|
+
- Do not replace the persistent sandbox adapter plan.
|
|
73
|
+
- Do not require `@boring/core` to import `@boring/agent`.
|
|
74
|
+
|
|
75
|
+
## Mental Model
|
|
76
|
+
|
|
77
|
+
There are two snapshot classes:
|
|
78
|
+
|
|
79
|
+
1. Base snapshot
|
|
80
|
+
- App/deployment-level template.
|
|
81
|
+
- Contains shared tools and dependencies.
|
|
82
|
+
- Used only when provisioning a workspace that has no runtime resource yet.
|
|
83
|
+
- Rotated by operators or CI when the manifest changes.
|
|
84
|
+
|
|
85
|
+
2. Workspace snapshot or persistent state
|
|
86
|
+
- Workspace-level durable state.
|
|
87
|
+
- Contains user files and installed packages created inside that workspace.
|
|
88
|
+
- Used to recover a stopped or expired workspace.
|
|
89
|
+
- Always wins over the base snapshot after first provisioning.
|
|
90
|
+
|
|
91
|
+
Restore priority:
|
|
92
|
+
|
|
93
|
+
1. Existing persistent sandbox name, when using Vercel persistent beta.
|
|
94
|
+
2. Existing active stable sandbox id.
|
|
95
|
+
3. Existing workspace snapshot id.
|
|
96
|
+
4. App-level base snapshot id, only for first provisioning.
|
|
97
|
+
5. Empty sandbox or template tarball fallback.
|
|
98
|
+
|
|
99
|
+
## App-Level Configuration
|
|
100
|
+
|
|
101
|
+
Add config fields read by the agent package and provided by the app shell:
|
|
102
|
+
|
|
103
|
+
```text
|
|
104
|
+
BORING_AGENT_VERCEL_BASE_SNAPSHOT_ID=snap_...
|
|
105
|
+
BORING_AGENT_VERCEL_BASE_SNAPSHOT_VERSION=boring-node24-2026-04-30-a1b2c3d4
|
|
106
|
+
BORING_AGENT_VERCEL_BASE_SNAPSHOT_RUNTIME=node24
|
|
107
|
+
BORING_AGENT_VERCEL_BASE_SNAPSHOT_MANIFEST_PATH=./sandbox-template.manifest.json
|
|
108
|
+
BORING_AGENT_VERCEL_BASE_SNAPSHOT_CACHE_PATH=~/.config/boring-agent/base-snapshots.json
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Rules:
|
|
112
|
+
|
|
113
|
+
- `BASE_SNAPSHOT_ID` is optional. Without it, current behavior remains.
|
|
114
|
+
- `BASE_SNAPSHOT_VERSION` is required when `BASE_SNAPSHOT_ID` is set.
|
|
115
|
+
- `BASE_SNAPSHOT_RUNTIME` should default to the adapter runtime, not to the
|
|
116
|
+
current bake helper's `python3.13` default.
|
|
117
|
+
- Version is a human-readable label plus manifest hash.
|
|
118
|
+
- Full app stores these as environment variables or deployment secrets.
|
|
119
|
+
- Future cloud can store the same fields in deployment/app runtime template
|
|
120
|
+
metadata.
|
|
121
|
+
|
|
122
|
+
## Provider-Neutral DB Model
|
|
123
|
+
|
|
124
|
+
The workspace runtime store should track runtime resources generically, not as
|
|
125
|
+
Vercel-only columns.
|
|
126
|
+
|
|
127
|
+
Recommended logical fields for a workspace runtime resource:
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
type WorkspaceRuntimeResource = {
|
|
131
|
+
workspaceId: string
|
|
132
|
+
provider: "vercel" | "local" | "fly" | "modal" | string
|
|
133
|
+
resourceKind: "sandbox" | "volume" | "snapshot" | string
|
|
134
|
+
handleKind: "session" | "persistent-name" | "volume-path" | string
|
|
135
|
+
handle: string
|
|
136
|
+
currentSessionId?: string
|
|
137
|
+
currentSnapshotId?: string
|
|
138
|
+
baseSnapshotId?: string
|
|
139
|
+
baseSnapshotVersion?: string
|
|
140
|
+
templateVersion?: string
|
|
141
|
+
runtime?: string
|
|
142
|
+
state: "pending" | "provisioning" | "ready" | "stopped" | "error"
|
|
143
|
+
status?: string
|
|
144
|
+
expiresAt?: string
|
|
145
|
+
provisionLockToken?: string
|
|
146
|
+
metadata?: Record<string, unknown>
|
|
147
|
+
createdAt: string
|
|
148
|
+
updatedAt: string
|
|
149
|
+
lastUsedAt?: string
|
|
150
|
+
lastSeenAt?: string
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
For Vercel stable:
|
|
155
|
+
|
|
156
|
+
- `handleKind = "session"`
|
|
157
|
+
- `handle = sandboxId`
|
|
158
|
+
- `currentSnapshotId = latest workspace snapshot id`
|
|
159
|
+
- `baseSnapshotId/baseSnapshotVersion = provenance only`
|
|
160
|
+
|
|
161
|
+
For Vercel persistent beta:
|
|
162
|
+
|
|
163
|
+
- `handleKind = "persistent-name"`
|
|
164
|
+
- `handle = deterministic sandbox name`
|
|
165
|
+
- `currentSessionId = latest session id, metadata only`
|
|
166
|
+
- `currentSnapshotId = provider-reported persisted snapshot when available`
|
|
167
|
+
- `baseSnapshotId/baseSnapshotVersion = provenance only`
|
|
168
|
+
|
|
169
|
+
Important rule:
|
|
170
|
+
|
|
171
|
+
- `baseSnapshotId` is never the recovery source after user mutations unless
|
|
172
|
+
there is no workspace state yet.
|
|
173
|
+
- `templateVersion` is first-class because it drives workspace upgrade
|
|
174
|
+
decisions. Do not bury it only in generic metadata.
|
|
175
|
+
- `expiresAt` is first-class because stable workspace snapshots can expire and
|
|
176
|
+
cleanup/resume code must not assume stored snapshot ids are valid forever.
|
|
177
|
+
|
|
178
|
+
## Base Snapshot Registry
|
|
179
|
+
|
|
180
|
+
The app or future cloud layer must keep an app-level registry for base
|
|
181
|
+
snapshots. Environment variables are enough to select the active pointer, but
|
|
182
|
+
not enough for audit, rollback, or cleanup.
|
|
183
|
+
|
|
184
|
+
Recommended logical fields:
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
type WorkspaceBaseSnapshotRegistryEntry = {
|
|
188
|
+
provider: "vercel" | string
|
|
189
|
+
snapshotId: string
|
|
190
|
+
version: string
|
|
191
|
+
manifestHash: string
|
|
192
|
+
runtime: string
|
|
193
|
+
status: "baking" | "ready" | "failed" | "retired"
|
|
194
|
+
active: boolean
|
|
195
|
+
previousSnapshotId?: string
|
|
196
|
+
sourceSandboxId?: string
|
|
197
|
+
manifest: Record<string, unknown>
|
|
198
|
+
createdAt: string
|
|
199
|
+
readyAt?: string
|
|
200
|
+
retiredAt?: string
|
|
201
|
+
expiresAt?: string
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Registry rules:
|
|
206
|
+
|
|
207
|
+
- One active base snapshot per app/runtime/template lane.
|
|
208
|
+
- Old base snapshots remain registered after rotation.
|
|
209
|
+
- Cleanup can delete only snapshots that are not active, not previous rollback
|
|
210
|
+
candidates, and not referenced by any workspace runtime resource.
|
|
211
|
+
- Keep old base snapshots for a minimum retention window, initially 14 days.
|
|
212
|
+
|
|
213
|
+
## Current Code Touchpoints
|
|
214
|
+
|
|
215
|
+
Existing useful pieces:
|
|
216
|
+
|
|
217
|
+
- `createDefaultVercelClient()` in
|
|
218
|
+
`packages/agent/src/server/runtime/modes/vercel-sandbox.ts` already supports
|
|
219
|
+
creating from `source: { type: "snapshot", snapshotId }`.
|
|
220
|
+
- `resolveSandboxHandle()` in
|
|
221
|
+
`packages/agent/src/server/sandbox/vercel-sandbox/resolveSandboxHandle.ts`
|
|
222
|
+
already recreates from a stored snapshot id.
|
|
223
|
+
- `bakeSnapshotIfNeeded()` in
|
|
224
|
+
`packages/agent/src/server/sandbox/vercel-sandbox/bake.ts` already has a
|
|
225
|
+
local cache and package hash, but it is not wired into the mode adapter.
|
|
226
|
+
|
|
227
|
+
Gaps:
|
|
228
|
+
|
|
229
|
+
- `bakeSnapshotIfNeeded()` hashes only package lists. It should hash the full
|
|
230
|
+
base manifest.
|
|
231
|
+
- `bakeSnapshotIfNeeded()` defaults to `python3.13`; full-app likely wants
|
|
232
|
+
`node24` or the adapter runtime.
|
|
233
|
+
- Stable `Sandbox.create()` source is one of snapshot or tarball. If a
|
|
234
|
+
workspace also needs app template files, apply the template overlay after
|
|
235
|
+
creation from the base snapshot.
|
|
236
|
+
- Current handle records do not distinguish base snapshot provenance from
|
|
237
|
+
workspace snapshot recovery.
|
|
238
|
+
|
|
239
|
+
## Base Manifest
|
|
240
|
+
|
|
241
|
+
Create a manifest that describes the base image inputs:
|
|
242
|
+
|
|
243
|
+
```json
|
|
244
|
+
{
|
|
245
|
+
"schemaVersion": 1,
|
|
246
|
+
"name": "boring-full-app-node",
|
|
247
|
+
"runtime": "node24",
|
|
248
|
+
"systemPackages": ["git", "jq", "ripgrep", "tar", "gzip"],
|
|
249
|
+
"commands": [
|
|
250
|
+
"corepack enable",
|
|
251
|
+
"corepack prepare pnpm@latest --activate"
|
|
252
|
+
],
|
|
253
|
+
"markerPath": "/vercel/sandbox/.boring/base-snapshot.json"
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Hash inputs:
|
|
258
|
+
|
|
259
|
+
- manifest schema version
|
|
260
|
+
- runtime
|
|
261
|
+
- system packages
|
|
262
|
+
- package manager versions
|
|
263
|
+
- setup commands
|
|
264
|
+
- copied template files, if any
|
|
265
|
+
- agent package version or template version
|
|
266
|
+
|
|
267
|
+
Marker file written into the seed sandbox:
|
|
268
|
+
|
|
269
|
+
```json
|
|
270
|
+
{
|
|
271
|
+
"kind": "boring-base-snapshot",
|
|
272
|
+
"version": "boring-node24-2026-04-30-a1b2c3d4",
|
|
273
|
+
"runtime": "node24",
|
|
274
|
+
"hash": "a1b2c3d4...",
|
|
275
|
+
"createdAt": "2026-04-30T00:00:00.000Z"
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## Bake Flow
|
|
280
|
+
|
|
281
|
+
1. Load and validate the base manifest.
|
|
282
|
+
2. Compute the manifest hash.
|
|
283
|
+
3. Acquire a distributed bake lock for the manifest hash.
|
|
284
|
+
4. Check registry or cache for an existing snapshot with that hash.
|
|
285
|
+
5. If cache hits and `Snapshot.get()` reports a usable terminal status, reuse
|
|
286
|
+
it.
|
|
287
|
+
6. Create a seed sandbox with the manifest runtime.
|
|
288
|
+
7. Run setup commands with a restricted environment. Pass only explicitly
|
|
289
|
+
allowlisted variables; never inherit the app server environment.
|
|
290
|
+
8. Install system packages and runtime dependencies.
|
|
291
|
+
9. Write the marker file.
|
|
292
|
+
10. Run smoke checks:
|
|
293
|
+
- runtime version
|
|
294
|
+
- `git --version`
|
|
295
|
+
- `rg --version`
|
|
296
|
+
- package manager version
|
|
297
|
+
- marker file content
|
|
298
|
+
11. Snapshot with `expiration: 0` for managed app templates.
|
|
299
|
+
12. Poll `Snapshot.get()` until the snapshot is usable or failed.
|
|
300
|
+
13. Record snapshot id, version, hash, runtime, source sandbox id, expiry, and
|
|
301
|
+
status.
|
|
302
|
+
14. Release the distributed bake lock.
|
|
303
|
+
|
|
304
|
+
Operational note:
|
|
305
|
+
|
|
306
|
+
- `sandbox.snapshot()` stops the seed sandbox. Do not run more setup commands
|
|
307
|
+
after snapshotting.
|
|
308
|
+
- If a second bake process sees the same manifest hash while the first one owns
|
|
309
|
+
the lock, it should wait and reuse the winner's snapshot instead of baking a
|
|
310
|
+
duplicate.
|
|
311
|
+
|
|
312
|
+
## Runtime Provisioning Flow
|
|
313
|
+
|
|
314
|
+
For a new workspace:
|
|
315
|
+
|
|
316
|
+
1. Resolve workspace id and deterministic provider handle.
|
|
317
|
+
2. Attempt to create or claim a `provisioning` runtime resource row with a
|
|
318
|
+
unique `provisionLockToken`.
|
|
319
|
+
3. If another request already owns provisioning, poll the row until it reaches
|
|
320
|
+
`ready` or `error`.
|
|
321
|
+
4. If a ready resource exists, restore from that resource and ignore the base
|
|
322
|
+
snapshot.
|
|
323
|
+
5. If no ready resource exists and `BASE_SNAPSHOT_ID` is configured, create the
|
|
324
|
+
sandbox from that snapshot.
|
|
325
|
+
6. Persist the provider handle and base snapshot provenance on the locked row.
|
|
326
|
+
7. Apply app template overlay if needed.
|
|
327
|
+
8. On overlay failure, mark the runtime resource `error`, keep enough metadata
|
|
328
|
+
to debug the provider handle, and do not mark the workspace `ready`.
|
|
329
|
+
9. For persistent beta, rely on provider persistence. Do not call
|
|
330
|
+
`sandbox.snapshot()` after provisioning.
|
|
331
|
+
10. For stable sandboxes, do not call `sandbox.snapshot()` during first
|
|
332
|
+
provisioning because snapshotting stops the sandbox. Schedule workspace
|
|
333
|
+
snapshots only on explicit save, graceful stop, or idle maintenance with a
|
|
334
|
+
clear reconnect path.
|
|
335
|
+
11. Mark workspace runtime `ready`.
|
|
336
|
+
|
|
337
|
+
For an existing workspace:
|
|
338
|
+
|
|
339
|
+
1. Resume by persistent name or sandbox id.
|
|
340
|
+
2. If stable session is gone and a workspace snapshot exists, recreate from the
|
|
341
|
+
workspace snapshot.
|
|
342
|
+
3. If stable session is gone and no workspace snapshot exists, show a conflict
|
|
343
|
+
error and require explicit reset. Do not fall back to the base snapshot.
|
|
344
|
+
|
|
345
|
+
Persistent beta validation gate:
|
|
346
|
+
|
|
347
|
+
- Before implementation, run a live provider probe for
|
|
348
|
+
`Sandbox.create({ name, persistent: true, source: { type: "snapshot" } })`.
|
|
349
|
+
- If Vercel rejects that combination, first provisioning must either create an
|
|
350
|
+
empty named persistent sandbox and run setup commands, or apply a small
|
|
351
|
+
post-create template overlay. The fallback must be measured before rollout.
|
|
352
|
+
|
|
353
|
+
## Invalidation Policy
|
|
354
|
+
|
|
355
|
+
Snapshots are immutable. Invalidation means rotating the pointer.
|
|
356
|
+
|
|
357
|
+
Process:
|
|
358
|
+
|
|
359
|
+
1. Change the base manifest or dependency versions.
|
|
360
|
+
2. Bake a new snapshot.
|
|
361
|
+
3. Smoke test a new sandbox created from the new snapshot.
|
|
362
|
+
4. Update `BORING_AGENT_VERCEL_BASE_SNAPSHOT_ID`.
|
|
363
|
+
5. Update `BORING_AGENT_VERCEL_BASE_SNAPSHOT_VERSION`.
|
|
364
|
+
6. Deploy the app.
|
|
365
|
+
7. New workspaces use the new base.
|
|
366
|
+
8. Existing workspaces continue using their own workspace resource.
|
|
367
|
+
9. Keep old base snapshots until no workspace records reference them.
|
|
368
|
+
10. Delete old snapshots only through a separate explicit cleanup task.
|
|
369
|
+
|
|
370
|
+
Existing workspace upgrades:
|
|
371
|
+
|
|
372
|
+
- Do not recreate existing workspaces from the new base.
|
|
373
|
+
- Run an explicit workspace migration command inside each workspace.
|
|
374
|
+
- Verify files and dependency state.
|
|
375
|
+
- Save or wait for persistent state to be saved.
|
|
376
|
+
- Update the workspace resource metadata with the applied template version.
|
|
377
|
+
|
|
378
|
+
## Implementation Phases
|
|
379
|
+
|
|
380
|
+
### Phase 1 - Contracts and Config
|
|
381
|
+
|
|
382
|
+
- Add base snapshot config schema.
|
|
383
|
+
- Add provider-neutral runtime resource fields or metadata mapping.
|
|
384
|
+
- Add `provisioning`, `templateVersion`, `expiresAt`, and
|
|
385
|
+
`provisionLockToken` to the runtime resource model.
|
|
386
|
+
- Add app-level base snapshot registry storage.
|
|
387
|
+
- Extend `SandboxHandleRecord` to carry:
|
|
388
|
+
- `currentSnapshotId`
|
|
389
|
+
- `baseSnapshotId`
|
|
390
|
+
- `baseSnapshotVersion`
|
|
391
|
+
- `runtime`
|
|
392
|
+
- `handleKind`
|
|
393
|
+
- Keep backward compatibility for existing records.
|
|
394
|
+
- Confirm and lock down `expiration: 0` semantics in a provider smoke test.
|
|
395
|
+
|
|
396
|
+
### Phase 2 - Resolver Behavior
|
|
397
|
+
|
|
398
|
+
- Teach Vercel stable resolver to use base snapshot only when no workspace
|
|
399
|
+
resource exists.
|
|
400
|
+
- Keep workspace snapshot as the only stable recovery source after first
|
|
401
|
+
provisioning.
|
|
402
|
+
- Preserve the full-app policy where unavailable workspace sandboxes return a
|
|
403
|
+
clear conflict instead of silent replacement.
|
|
404
|
+
- Add the provisioning claim/lock path so concurrent requests cannot create two
|
|
405
|
+
sandboxes for the same workspace.
|
|
406
|
+
- Define the template overlay strategy:
|
|
407
|
+
- preferred: bake template files into the base snapshot when shared by all
|
|
408
|
+
workspaces
|
|
409
|
+
- fallback: keep overlays small and fail provisioning if the overlay exceeds
|
|
410
|
+
a configured size limit
|
|
411
|
+
|
|
412
|
+
### Phase 3 - Bake Command
|
|
413
|
+
|
|
414
|
+
- Replace package-list hash with manifest hash.
|
|
415
|
+
- Add runtime support with a `node24` full-app default.
|
|
416
|
+
- Add `expiration` support.
|
|
417
|
+
- Add `Snapshot.get()` validation before cache reuse.
|
|
418
|
+
- Poll snapshot creation until a usable terminal status before publishing the
|
|
419
|
+
snapshot id.
|
|
420
|
+
- Add a distributed bake lock around manifest hash.
|
|
421
|
+
- Run bake setup commands with an explicitly allowlisted environment.
|
|
422
|
+
- Add smoke checks and marker writing.
|
|
423
|
+
- Define CI or release ownership for bake automation:
|
|
424
|
+
- trigger on manifest change, package/runtime version change, or scheduled
|
|
425
|
+
security refresh
|
|
426
|
+
- run in CI or a controlled release job with Vercel credentials
|
|
427
|
+
- publish the resulting snapshot id/version through deployment config without
|
|
428
|
+
manual copy-paste
|
|
429
|
+
|
|
430
|
+
### Phase 4 - Persistent Beta Alignment
|
|
431
|
+
|
|
432
|
+
- If persistent beta is enabled, create named sandbox from base snapshot on
|
|
433
|
+
first provisioning.
|
|
434
|
+
- Store sandbox name as durable handle.
|
|
435
|
+
- Treat session id as metadata.
|
|
436
|
+
- Verify file operations and `runCommand` auto-resume stopped named sandboxes.
|
|
437
|
+
- Validate that Vercel supports `name` plus `source: snapshot` in the same
|
|
438
|
+
create request.
|
|
439
|
+
- Do not call `sandbox.snapshot()` as part of persistent beta recovery unless
|
|
440
|
+
Vercel exposes it as provider metadata. Provider auto-save is the recovery
|
|
441
|
+
path.
|
|
442
|
+
|
|
443
|
+
### Phase 5 - Tests
|
|
444
|
+
|
|
445
|
+
- Unit test resolver priority:
|
|
446
|
+
- existing workspace snapshot beats base snapshot
|
|
447
|
+
- base snapshot used only for first provisioning
|
|
448
|
+
- missing workspace snapshot returns conflict in full-app policy
|
|
449
|
+
- Unit test manifest hashing and cache reuse.
|
|
450
|
+
- Unit test no secret keys are accepted in base manifest env sections.
|
|
451
|
+
- Unit test bake subprocesses receive only an allowlisted environment.
|
|
452
|
+
- Unit test concurrent provisioning for one workspace creates one provider
|
|
453
|
+
handle.
|
|
454
|
+
- Unit test duplicate bake calls with one manifest hash create one snapshot.
|
|
455
|
+
- Unit test provisioning failure between sandbox create and ready state moves
|
|
456
|
+
the runtime to `error`.
|
|
457
|
+
- Unit test workspace snapshot expiry returns conflict instead of panic or base
|
|
458
|
+
fallback.
|
|
459
|
+
- Unit test template overlay failure does not mark workspace `ready`.
|
|
460
|
+
- E2E test workspace creation starts from base and shows expected tools.
|
|
461
|
+
- E2E test workspace switching preserves file tree and workbench state.
|
|
462
|
+
- E2E test stopped sandbox resumes and user-created files remain visible.
|
|
463
|
+
- E2E test base invalidation affects new workspaces only.
|
|
464
|
+
|
|
465
|
+
### Phase 6 - Rollout
|
|
466
|
+
|
|
467
|
+
- Bake a dev base snapshot.
|
|
468
|
+
- Enable in local full-app environment.
|
|
469
|
+
- Run workspace lifecycle E2E.
|
|
470
|
+
- Deploy to Fly with base snapshot env vars.
|
|
471
|
+
- Create one new workspace and verify first boot.
|
|
472
|
+
- Stop/resume the workspace and verify files persist.
|
|
473
|
+
- Bake a second base snapshot and verify only new workspaces use it.
|
|
474
|
+
- Keep the previous base snapshot id in rollback config until the new snapshot
|
|
475
|
+
is stable.
|
|
476
|
+
- Add an alert/log signal for workspace provisioning failures, initially
|
|
477
|
+
workspace creation failure rate above 5 percent over 5 minutes.
|
|
478
|
+
- Do not retire old base snapshots before the minimum retention window.
|
|
479
|
+
|
|
480
|
+
## Risks
|
|
481
|
+
|
|
482
|
+
- Beta persistent SDK behavior can change before GA.
|
|
483
|
+
- Snapshot storage costs and retention limits need an operator budget.
|
|
484
|
+
- Old workspaces with no workspace snapshot cannot be recovered after stable
|
|
485
|
+
sandbox stop.
|
|
486
|
+
- Using `expiration: 0` avoids surprise expiry but requires cleanup discipline.
|
|
487
|
+
- Template overlay after snapshot creation can reintroduce cold-start cost if it
|
|
488
|
+
grows large.
|
|
489
|
+
- The base snapshot can accidentally capture secrets if bake commands read app
|
|
490
|
+
env. Bake must run with a restricted environment.
|
|
491
|
+
- A broken base snapshot can break all new workspace creation until the pointer
|
|
492
|
+
is rolled back.
|
|
493
|
+
- Concurrent provisioning or concurrent baking can create orphaned provider
|
|
494
|
+
resources unless guarded by locks.
|
|
495
|
+
|
|
496
|
+
## Open Questions
|
|
497
|
+
|
|
498
|
+
- What exact Vercel snapshot statuses should block cache reuse?
|
|
499
|
+
- Should a base snapshot be per app, per tenant, or per workspace template?
|
|
500
|
+
- Should workspace reset create a new deterministic sandbox name generation, or
|
|
501
|
+
delete/reuse the old persistent name?
|
|
502
|
+
- What cleanup command should own old snapshot deletion?
|
|
503
|
+
- What is the maximum acceptable template overlay size before it must be baked
|
|
504
|
+
into the base snapshot?
|
|
505
|
+
- Should bake run in CI, a release job, or an admin-only app command?
|
|
506
|
+
|
|
507
|
+
Questions that must be closed before Phase 2 implementation starts:
|
|
508
|
+
|
|
509
|
+
1. Live validation of persistent `name` plus `source: snapshot`.
|
|
510
|
+
2. Vercel snapshot statuses considered usable for cache and restore.
|
|
511
|
+
3. Template overlay strategy and size limit.
|
|
512
|
+
4. Bake automation owner and credential boundary.
|
|
513
|
+
|
|
514
|
+
## Review Log
|
|
515
|
+
|
|
516
|
+
- Claude Code: `revise`.
|
|
517
|
+
- Main feedback: add provisioning state and lock, poll snapshot status after
|
|
518
|
+
bake, promote expiry/template version into the model, add bake automation
|
|
519
|
+
ownership, add rollback signals, and split persistent beta recovery from
|
|
520
|
+
stable snapshot recovery.
|
|
521
|
+
- Incorporated into this plan.
|
|
522
|
+
- Gemini: `revise`.
|
|
523
|
+
- Main feedback: do not call `sandbox.snapshot()` during initial provisioning
|
|
524
|
+
because it stops the sandbox, validate named persistent sandbox creation
|
|
525
|
+
from a snapshot, add provisioning failure handling, add a registry for
|
|
526
|
+
cleanup, and strip bake environment variables.
|
|
527
|
+
- Incorporated into this plan.
|