orizu 0.4.1 → 0.5.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.
Files changed (76) hide show
  1. package/README.md +22 -13
  2. package/dist/artifact-archive.js +152 -0
  3. package/dist/connectors-cli.js +80 -0
  4. package/dist/credentials.js +4 -1
  5. package/dist/daytona-slice-rehearsal.js +255 -0
  6. package/dist/daytona-slice.js +441 -0
  7. package/dist/egress-allowlist-cli.js +132 -0
  8. package/dist/egress-policy.js +159 -0
  9. package/dist/git-credential.js +131 -0
  10. package/dist/github-setup.js +226 -0
  11. package/dist/help.js +271 -16
  12. package/dist/hosted-bootstrap-rehearsal.js +235 -0
  13. package/dist/hosted-bootstrap.js +365 -0
  14. package/dist/hosted-broker-server.js +220 -0
  15. package/dist/hosted-git-http-server.js +136 -0
  16. package/dist/hosted-harness-claude.js +493 -0
  17. package/dist/hosted-harness-opencode.js +729 -0
  18. package/dist/hosted-harness.js +60 -0
  19. package/dist/hosted-loop.js +477 -0
  20. package/dist/hosted-run-event-sink.js +499 -0
  21. package/dist/hosted-runtime-assets.js +216 -0
  22. package/dist/hosted-session-cli.js +642 -0
  23. package/dist/index.js +855 -237
  24. package/dist/kill-switch-cli.js +89 -0
  25. package/dist/manifests-cli.js +143 -0
  26. package/dist/preview-runtime.js +4 -1
  27. package/dist/sandbox-provider.js +238 -0
  28. package/dist/secret-redaction.js +112 -0
  29. package/dist/session-timeline.js +103 -0
  30. package/dist/setup-prompt.js +4 -2
  31. package/dist/skill-installer.js +9 -1
  32. package/dist/smoke.js +250 -0
  33. package/dist/task-assignment-manifest.js +39 -2
  34. package/dist/vercel-sandbox-provider.js +260 -0
  35. package/dist/workbench-cli.js +493 -0
  36. package/dist/workspace-sync.js +924 -0
  37. package/dist/workspace.js +868 -57
  38. package/package.json +4 -1
  39. package/src/artifact-archive.ts +182 -0
  40. package/src/connectors-cli.ts +115 -0
  41. package/src/credentials.ts +6 -2
  42. package/src/daytona-slice-rehearsal.ts +320 -0
  43. package/src/daytona-slice.ts +598 -0
  44. package/src/egress-allowlist-cli.ts +147 -0
  45. package/src/egress-policy.ts +188 -0
  46. package/src/git-credential.ts +155 -0
  47. package/src/github-setup.ts +366 -0
  48. package/src/help.ts +272 -16
  49. package/src/hosted-bootstrap-rehearsal.ts +312 -0
  50. package/src/hosted-bootstrap.ts +571 -0
  51. package/src/hosted-broker-server.ts +278 -0
  52. package/src/hosted-git-http-server.ts +164 -0
  53. package/src/hosted-harness-claude.ts +621 -0
  54. package/src/hosted-harness-opencode.ts +823 -0
  55. package/src/hosted-harness.ts +124 -0
  56. package/src/hosted-loop.ts +680 -0
  57. package/src/hosted-run-event-sink.ts +632 -0
  58. package/src/hosted-runtime-assets.ts +272 -0
  59. package/src/hosted-session-cli.ts +844 -0
  60. package/src/index.ts +987 -296
  61. package/src/kill-switch-cli.ts +113 -0
  62. package/src/manifests-cli.ts +168 -0
  63. package/src/preview-runtime.ts +14 -9
  64. package/src/sandbox-provider.ts +440 -0
  65. package/src/secret-redaction.ts +124 -0
  66. package/src/session-timeline.ts +152 -0
  67. package/src/setup-prompt.ts +4 -2
  68. package/src/skill-installer.ts +13 -1
  69. package/src/smoke.ts +342 -0
  70. package/src/task-assignment-manifest.ts +70 -2
  71. package/src/vercel-sandbox-provider.ts +388 -0
  72. package/src/workbench-cli.ts +701 -0
  73. package/src/workspace-sync.ts +1148 -0
  74. package/src/workspace.ts +1051 -64
  75. package/vendor/skills/orizu-cli/SKILL.md +3 -1
  76. package/vendor/skills/orizu-cli/references/cli-reference.md +3 -3
@@ -0,0 +1,388 @@
1
+ /**
2
+ * Vercel Sandbox adapter for the Orizu SandboxProvider seam (ALI-928 / P3.5,
3
+ * per ADR-003 + ADR-005). Thin adapter over `@vercel/sandbox`, mirroring the
4
+ * Daytona adapter's shape (`createDaytonaProvider`):
5
+ *
6
+ * - the SDK is imported LAZILY through a NON-LITERAL specifier so the CLI /
7
+ * app build resolve without the package and tests never load it;
8
+ * - a `loadModule` dep is injectable, so the fake-SDK unit tests exercise the
9
+ * request mapping (timeout, runtime, egress pass-through, extendTimeout)
10
+ * without the real package or a network.
11
+ *
12
+ * FOUNDER REQUIREMENT (locked): the sandbox `timeout` is ALWAYS set from opts
13
+ * (session length) — the adapter NEVER lets Vercel's 5-minute default apply.
14
+ * Default 60 min, hard cap 24 h.
15
+ *
16
+ * G5 (ALI-1006) owns the egress ALLOWLIST CONTENT (`egress-policy.ts` →
17
+ * `buildEgressPolicy`); this adapter only plumbs the `egressPolicy` option
18
+ * through to the Sandbox firewall (`networkPolicy`) at create. The model-key
19
+ * credential broker (G3/ALI-1004) is COMPOSED into that same default-deny policy
20
+ * by `buildEgressPolicy`: a per-domain request transform injects the model key AT
21
+ * THE PROXY, so the raw key never enters the sandbox (see
22
+ * docs/.../sandbox-secrets-policy.md §3).
23
+ */
24
+
25
+ import type {
26
+ ExecResult,
27
+ GitCloneParams,
28
+ GitPushParams,
29
+ SandboxCreateOpts,
30
+ SandboxProvider,
31
+ SandboxSession,
32
+ } from './sandbox-provider.js'
33
+
34
+ // -- Timeout policy (founder requirement) ------------------------------------
35
+
36
+ /** Default sandbox session length when opts omit one (NEVER Vercel's 5 min). */
37
+ export const VERCEL_DEFAULT_TIMEOUT_MS = 60 * 60 * 1000
38
+ /** Hard cap on a configured session length (24 h). */
39
+ export const VERCEL_MAX_TIMEOUT_MS = 24 * 60 * 60 * 1000
40
+ /** Default runtime image when opts/config omit one. */
41
+ export const VERCEL_DEFAULT_RUNTIME = 'node24'
42
+
43
+ // -- Custom-image readiness retry (ALI-1017) ---------------------------------
44
+ // A freshly-pushed VCR image is asynchronously PREPARED; a create against it
45
+ // while it is still 'Preparing' throws `image_not_ready`. We retry that (and
46
+ // only that) a bounded number of times with linear backoff so a create that
47
+ // races the preparation window succeeds instead of failing the whole run.
48
+ /** Max create attempts when the SDK reports the image is not yet ready. */
49
+ export const VERCEL_IMAGE_NOT_READY_MAX_ATTEMPTS = 6
50
+ /** Base backoff (ms) between image-not-ready retries (multiplied by attempt#). */
51
+ export const VERCEL_IMAGE_NOT_READY_BACKOFF_MS = 5000
52
+
53
+ // -- Structural view of the @vercel/sandbox surface we touch -----------------
54
+ // Kept local (not imported) so the seam type-checks and the app build resolves
55
+ // without the package present — same discipline as the Daytona adapter.
56
+
57
+ interface VercelSdkCommandFinished {
58
+ exitCode: number
59
+ stdout(): Promise<string>
60
+ stderr(): Promise<string>
61
+ }
62
+
63
+ interface VercelRunCommandParams {
64
+ cmd: string
65
+ args?: string[]
66
+ cwd?: string
67
+ env?: Record<string, string>
68
+ sudo?: boolean
69
+ }
70
+
71
+ /** The @vercel/sandbox `Snapshot` surface we touch: the id getter is enough to
72
+ * hand the snapshot to a later `create({ source: { type: 'snapshot', … } })`. */
73
+ interface VercelSdkSnapshot {
74
+ readonly snapshotId: string
75
+ }
76
+
77
+ interface VercelSdkSandbox {
78
+ readonly sandboxId: string
79
+ runCommand(params: VercelRunCommandParams): Promise<VercelSdkCommandFinished>
80
+ readFileToBuffer(file: { path: string; cwd?: string }): Promise<Uint8Array | Buffer | null>
81
+ writeFiles(files: { path: string; content: string | Uint8Array; mode?: number }[]): Promise<void>
82
+ stop(opts?: { blocking?: boolean }): Promise<unknown>
83
+ extendTimeout(durationMs: number): Promise<void>
84
+ updateNetworkPolicy(policy: unknown): Promise<unknown>
85
+ /** Snapshot the running sandbox; STOPS it as part of the process (verified
86
+ * against @vercel/sandbox@1.10.2 `Sandbox.snapshot(opts?): Promise<Snapshot>`). */
87
+ snapshot(opts?: { expiration?: number }): Promise<VercelSdkSnapshot>
88
+ }
89
+
90
+ interface VercelCreateParams {
91
+ timeout: number
92
+ runtime?: string
93
+ /** Custom VCR image ref (pre-baked runtime, ALI-1017); omitted → base runtime. */
94
+ image?: string
95
+ /**
96
+ * Boot from a Vercel Sandbox SNAPSHOT (ALI-1017, zero-Docker prebaked path).
97
+ * VERIFIED against @vercel/sandbox@1.10.2: the create-from-snapshot param is the
98
+ * NESTED `source: { type: 'snapshot', snapshotId }` (NOT a flat `snapshot`), and
99
+ * the snapshot variant of `CreateSandboxParams` OMITS `runtime`/`source` from the
100
+ * base — so we never set `runtime` alongside a snapshot `source`.
101
+ */
102
+ source?: { type: 'snapshot'; snapshotId: string }
103
+ resources?: { vcpus: number }
104
+ ports?: number[]
105
+ networkPolicy?: unknown
106
+ env?: Record<string, string>
107
+ token?: string
108
+ projectId?: string
109
+ teamId?: string
110
+ }
111
+
112
+ interface VercelSandboxCtor {
113
+ create(params: VercelCreateParams): Promise<VercelSdkSandbox>
114
+ }
115
+
116
+ export interface VercelSdkModule {
117
+ Sandbox: VercelSandboxCtor
118
+ }
119
+
120
+ // PIN CONTRACT: verified against @vercel/sandbox@1.10.2 — `Sandbox.create`
121
+ // accepts `{ timeout, runtime, resources, ports, networkPolicy, env }` plus the
122
+ // `{ token, projectId, teamId }` credential fields, and a `source` union whose
123
+ // snapshot arm is `{ type: 'snapshot', snapshotId }` (create-from-snapshot). The
124
+ // returned Sandbox exposes `runCommand / readFileToBuffer / writeFiles / stop /
125
+ // extendTimeout / updateNetworkPolicy / snapshot / sandboxId`, and `snapshot()`
126
+ // returns a `Snapshot` with a `snapshotId` getter. packages/cli/package.json pins
127
+ // this version EXACTLY (no caret); re-verify before bumping. The lazy
128
+ // `import(specifier)` keeps the (Node-only) SDK genuinely optional — it loads only
129
+ // when the Vercel provider is actually selected.
130
+ const VERCEL_SDK_SPECIFIER = '@vercel/sandbox'
131
+
132
+ async function loadVercelModule(): Promise<VercelSdkModule> {
133
+ try {
134
+ const specifier: string = VERCEL_SDK_SPECIFIER
135
+ const mod = (await import(specifier)) as unknown as VercelSdkModule
136
+ if (!mod || typeof mod.Sandbox?.create !== 'function') {
137
+ throw new Error('module did not export a Sandbox class with create()')
138
+ }
139
+ return mod
140
+ } catch (error) {
141
+ const detail = error instanceof Error ? error.message : String(error)
142
+ throw new Error(
143
+ `Vercel Sandbox SDK unavailable (${detail}). Install it before the live run: \`bun install --cwd packages/cli\` (declares @vercel/sandbox).`
144
+ )
145
+ }
146
+ }
147
+
148
+ // -- Auth resolution ---------------------------------------------------------
149
+
150
+ export interface VercelProviderConfig {
151
+ /** Vercel API token; default env VERCEL_TOKEN, then VERCEL_OIDC_TOKEN. */
152
+ token?: string
153
+ /** Project id; default env VERCEL_PROJECT_ID. */
154
+ projectId?: string
155
+ /** Team id; default env VERCEL_TEAM_ID. */
156
+ teamId?: string
157
+ /** Default runtime image (default node24). */
158
+ runtime?: string
159
+ /** Default custom VCR image ref (pre-baked runtime); default env
160
+ * ORIZU_HOSTED_IMAGE, else undefined = base runtime (back-compat). */
161
+ image?: string
162
+ /** Default Vercel Sandbox snapshot id (zero-Docker prebaked runtime); default
163
+ * env ORIZU_HOSTED_SNAPSHOT, else undefined. Mutually exclusive with `image`;
164
+ * when both resolve, the snapshot wins (see createSandbox). */
165
+ snapshot?: string
166
+ /** Default session length (ms) when createSandbox opts omit one. */
167
+ defaultTimeoutMs?: number
168
+ /** Hard cap on the configured session length (ms; default 24 h). */
169
+ maxTimeoutMs?: number
170
+ /** Max create attempts while a custom image reports `image_not_ready`. */
171
+ imageNotReadyMaxAttempts?: number
172
+ /** Base backoff (ms) between image-not-ready retries (× attempt number). */
173
+ imageNotReadyBackoffMs?: number
174
+ }
175
+
176
+ export interface VercelProviderDeps {
177
+ loadModule?: () => Promise<VercelSdkModule>
178
+ /** Injectable delay so the image-not-ready retry test runs without real waits. */
179
+ sleep?: (ms: number) => Promise<void>
180
+ }
181
+
182
+ interface ResolvedCredentials {
183
+ token?: string
184
+ projectId?: string
185
+ teamId?: string
186
+ }
187
+
188
+ function resolveCredentials(
189
+ config: VercelProviderConfig,
190
+ env: NodeJS.ProcessEnv
191
+ ): ResolvedCredentials {
192
+ return {
193
+ token: config.token ?? env.VERCEL_TOKEN ?? env.VERCEL_OIDC_TOKEN,
194
+ projectId: config.projectId ?? env.VERCEL_PROJECT_ID,
195
+ teamId: config.teamId ?? env.VERCEL_TEAM_ID,
196
+ }
197
+ }
198
+
199
+ /** True when live credentials are present (used by the env-gated smoke test). */
200
+ export function hasVercelCredentials(env: NodeJS.ProcessEnv = process.env): boolean {
201
+ const creds = resolveCredentials({}, env)
202
+ return Boolean(creds.token && creds.projectId && creds.teamId)
203
+ }
204
+
205
+ // The inline git credential helper: git runs the value as a shell command with
206
+ // the action appended, and it simply echoes the per-call username/password from
207
+ // the command's OWN env. No secret is written to git config or the clone URL —
208
+ // the credential lives only in the single git process's transient env, matching
209
+ // the Daytona adapter's clean per-call contract.
210
+ const GIT_USER_ENV = 'ORIZU_GIT_USER'
211
+ const GIT_PASS_ENV = 'ORIZU_GIT_PASS'
212
+ const INLINE_CRED_HELPER = `!f() { echo "username=$${GIT_USER_ENV}"; echo "password=$${GIT_PASS_ENV}"; }; f`
213
+
214
+ function credEnv(username?: string, password?: string): Record<string, string> {
215
+ return { [GIT_USER_ENV]: username ?? 'x-access-token', [GIT_PASS_ENV]: password ?? '' }
216
+ }
217
+
218
+ function clampTimeout(requested: number | undefined, def: number, cap: number): number {
219
+ const base = typeof requested === 'number' && Number.isFinite(requested) && requested > 0 ? requested : def
220
+ return Math.min(Math.max(1, Math.floor(base)), cap)
221
+ }
222
+
223
+ /**
224
+ * True when an error is the SDK's "custom image is still being prepared" signal.
225
+ * We match on a `code` field OR the message text, so we catch the error whether
226
+ * the SDK surfaces it as `{ code: 'image_not_ready' }` or only in `.message`.
227
+ * ONLY this specific condition is retried — any other create failure propagates.
228
+ */
229
+ export function isImageNotReadyError(error: unknown): boolean {
230
+ if (!error || typeof error !== 'object') return false
231
+ const rec = error as { code?: unknown; message?: unknown }
232
+ if (typeof rec.code === 'string' && rec.code === 'image_not_ready') return true
233
+ return typeof rec.message === 'string' && /image_not_ready/i.test(rec.message)
234
+ }
235
+
236
+ const sleep = (ms: number): Promise<void> => new Promise(resolve => setTimeout(resolve, ms))
237
+
238
+ function wrapVercelSandbox(sandbox: VercelSdkSandbox): SandboxSession {
239
+ const runShell = async (command: string, cwd?: string): Promise<VercelSdkCommandFinished> =>
240
+ sandbox.runCommand({ cmd: 'bash', args: ['-c', command], cwd })
241
+
242
+ return {
243
+ id: sandbox.sandboxId,
244
+ async gitClone(params: GitCloneParams) {
245
+ const args = ['-c', `credential.helper=${INLINE_CRED_HELPER}`, 'clone']
246
+ if (params.branch) args.push('--branch', params.branch)
247
+ args.push(params.url, params.path)
248
+ const res = await sandbox.runCommand({ cmd: 'git', args, env: credEnv(params.username, params.password) })
249
+ if (res.exitCode !== 0) {
250
+ const stderr = (await res.stderr()).trim()
251
+ throw new Error(`git clone failed: ${stderr || `exit ${res.exitCode}`}`)
252
+ }
253
+ },
254
+ async gitPush(params: GitPushParams) {
255
+ const res = await sandbox.runCommand({
256
+ cmd: 'git',
257
+ args: ['-C', params.path, '-c', `credential.helper=${INLINE_CRED_HELPER}`, 'push'],
258
+ env: credEnv(params.username, params.password),
259
+ })
260
+ if (res.exitCode !== 0) {
261
+ const stderr = (await res.stderr()).trim()
262
+ throw new Error(`git push failed: ${stderr || `exit ${res.exitCode}`}`)
263
+ }
264
+ },
265
+ async exec(command: string, opts?: { cwd?: string }): Promise<ExecResult> {
266
+ const res = await runShell(command, opts?.cwd)
267
+ const [stdout, stderr] = await Promise.all([res.stdout(), res.stderr()])
268
+ // A missing/non-numeric exitCode is a FAILURE (1), never masked to success.
269
+ const exitCode = typeof res.exitCode === 'number' ? res.exitCode : 1
270
+ return { stdout, exitCode, stderr }
271
+ },
272
+ async readFile(path: string): Promise<string> {
273
+ const buf = await sandbox.readFileToBuffer({ path })
274
+ if (buf === null || buf === undefined) {
275
+ throw new Error(`readFile: ${path} not found`)
276
+ }
277
+ return Buffer.from(buf).toString('utf8')
278
+ },
279
+ async writeFile(path: string, content: string): Promise<void> {
280
+ await sandbox.writeFiles([{ path, content }])
281
+ },
282
+ async fileExists(path: string): Promise<boolean> {
283
+ const res = await sandbox.runCommand({ cmd: 'test', args: ['-e', path] })
284
+ return res.exitCode === 0
285
+ },
286
+ async destroy(): Promise<void> {
287
+ await sandbox.stop()
288
+ },
289
+ async extendTimeout(durationMs: number): Promise<void> {
290
+ await sandbox.extendTimeout(durationMs)
291
+ },
292
+ // ALI-1017: capture a snapshot of THIS running sandbox and return its id so a
293
+ // provisioner can bake the runtime once and boot future sandboxes from it with
294
+ // no Docker/VCR. Vercel STOPS the sandbox as part of snapshotting, so the
295
+ // caller must treat the session as spent afterwards.
296
+ async snapshot(opts?: { expiration?: number }): Promise<string> {
297
+ const snap = await sandbox.snapshot(opts)
298
+ const id = snap?.snapshotId
299
+ if (typeof id !== 'string' || id.length === 0) {
300
+ throw new Error('sandbox.snapshot() returned no snapshotId')
301
+ }
302
+ return id
303
+ },
304
+ }
305
+ }
306
+
307
+ export function createVercelProvider(
308
+ config: VercelProviderConfig = {},
309
+ deps: VercelProviderDeps = {}
310
+ ): SandboxProvider {
311
+ const load = deps.loadModule ?? loadVercelModule
312
+ const waitFor = deps.sleep ?? sleep
313
+ const defaultTimeoutMs = config.defaultTimeoutMs ?? VERCEL_DEFAULT_TIMEOUT_MS
314
+ const maxTimeoutMs = config.maxTimeoutMs ?? VERCEL_MAX_TIMEOUT_MS
315
+ const imageNotReadyMaxAttempts = config.imageNotReadyMaxAttempts ?? VERCEL_IMAGE_NOT_READY_MAX_ATTEMPTS
316
+ const imageNotReadyBackoffMs = config.imageNotReadyBackoffMs ?? VERCEL_IMAGE_NOT_READY_BACKOFF_MS
317
+ let modulePromise: Promise<VercelSdkModule> | null = null
318
+ const sdk = (): Promise<VercelSdkModule> => {
319
+ if (!modulePromise) modulePromise = load()
320
+ return modulePromise
321
+ }
322
+
323
+ return {
324
+ kind: 'vercel',
325
+ async createSandbox(opts: SandboxCreateOpts = {}): Promise<SandboxSession> {
326
+ const mod = await sdk()
327
+ const creds = resolveCredentials(config, process.env)
328
+ const params: VercelCreateParams = {
329
+ // FOUNDER REQUIREMENT: always set timeout from opts/config; never rely
330
+ // on Vercel's 5-minute default.
331
+ timeout: clampTimeout(opts.timeoutMs, defaultTimeoutMs, maxTimeoutMs),
332
+ }
333
+ // ALI-1017: two mutually-exclusive prebaked-runtime paths, each resolved by
334
+ // the same precedence (per-call opts → provider config → env default):
335
+ // - `snapshot` → boot from a Vercel Sandbox snapshot (zero-Docker path);
336
+ // - `image` → boot from a custom VCR image (Docker/VCR path).
337
+ // When BOTH resolve, the SNAPSHOT wins (documented preference) — never boot
338
+ // an ambiguous mix. A snapshot boot OMITS `runtime`/`image` (the SDK's
339
+ // snapshot create-variant forbids `runtime`); the base runtime is otherwise
340
+ // set so we never fall back to Vercel's default.
341
+ const snapshot = opts.snapshot ?? config.snapshot ?? process.env.ORIZU_HOSTED_SNAPSHOT
342
+ const image = opts.image ?? config.image ?? process.env.ORIZU_HOSTED_IMAGE
343
+ if (snapshot) {
344
+ params.source = { type: 'snapshot', snapshotId: snapshot }
345
+ } else {
346
+ params.runtime = opts.runtime ?? config.runtime ?? VERCEL_DEFAULT_RUNTIME
347
+ if (image) params.image = image
348
+ }
349
+ if (typeof opts.vcpus === 'number' && opts.vcpus > 0) params.resources = { vcpus: opts.vcpus }
350
+ if (opts.ports && opts.ports.length > 0) params.ports = opts.ports
351
+ // G5 supplies the allowlist content; we only pass the policy through.
352
+ if (opts.egressPolicy !== undefined) params.networkPolicy = opts.egressPolicy
353
+ if (opts.envVars) params.env = opts.envVars
354
+ if (creds.token) params.token = creds.token
355
+ if (creds.projectId) params.projectId = creds.projectId
356
+ if (creds.teamId) params.teamId = creds.teamId
357
+
358
+ // A freshly-pushed custom image may still be 'Preparing' in VCR; retry the
359
+ // `image_not_ready` create (and ONLY that) with bounded linear backoff.
360
+ let lastError: unknown
361
+ for (let attempt = 1; attempt <= imageNotReadyMaxAttempts; attempt += 1) {
362
+ try {
363
+ const sandbox = await mod.Sandbox.create(params)
364
+ return wrapVercelSandbox(sandbox)
365
+ } catch (error) {
366
+ if (!isImageNotReadyError(error)) throw error
367
+ lastError = error
368
+ if (attempt < imageNotReadyMaxAttempts) await waitFor(imageNotReadyBackoffMs * attempt)
369
+ }
370
+ }
371
+ const detail = lastError instanceof Error ? lastError.message : String(lastError)
372
+ throw new Error(
373
+ `Vercel Sandbox image ${image ?? snapshot ?? '(unknown)'} not ready after ${imageNotReadyMaxAttempts} attempts (${detail}). ` +
374
+ 'Wait until the image shows status "Ready" for linux/amd64 in VCR before creating a sandbox.'
375
+ )
376
+ },
377
+ }
378
+ }
379
+
380
+ // -- Model-key credential brokering (G3 / ALI-1004) --------------------------
381
+ //
382
+ // The model-key broker is NOT a separate builder anymore: `egress-policy.ts` →
383
+ // `buildEgressPolicy({ modelKeyBroker })` COMPOSES the key-injection transform
384
+ // onto the model host's rule inside the ONE default-deny allowlist, so brokering
385
+ // and allowlisting cannot disagree. The prior permissive `buildModelKeyBrokerPolicy`
386
+ // (which emitted an allow-all `'*': []`) has been DELETED — a default-deny policy
387
+ // must only ever come from `buildEgressPolicy`. See
388
+ // docs/requirements/hosted-customer-workbench/sandbox-egress-policy.md §2.