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.
- package/README.md +22 -13
- package/dist/artifact-archive.js +152 -0
- package/dist/connectors-cli.js +80 -0
- package/dist/credentials.js +4 -1
- package/dist/daytona-slice-rehearsal.js +255 -0
- package/dist/daytona-slice.js +441 -0
- package/dist/egress-allowlist-cli.js +132 -0
- package/dist/egress-policy.js +159 -0
- package/dist/git-credential.js +131 -0
- package/dist/github-setup.js +226 -0
- package/dist/help.js +271 -16
- package/dist/hosted-bootstrap-rehearsal.js +235 -0
- package/dist/hosted-bootstrap.js +365 -0
- package/dist/hosted-broker-server.js +220 -0
- package/dist/hosted-git-http-server.js +136 -0
- package/dist/hosted-harness-claude.js +493 -0
- package/dist/hosted-harness-opencode.js +729 -0
- package/dist/hosted-harness.js +60 -0
- package/dist/hosted-loop.js +477 -0
- package/dist/hosted-run-event-sink.js +499 -0
- package/dist/hosted-runtime-assets.js +216 -0
- package/dist/hosted-session-cli.js +642 -0
- package/dist/index.js +855 -237
- package/dist/kill-switch-cli.js +89 -0
- package/dist/manifests-cli.js +143 -0
- package/dist/preview-runtime.js +4 -1
- package/dist/sandbox-provider.js +238 -0
- package/dist/secret-redaction.js +112 -0
- package/dist/session-timeline.js +103 -0
- package/dist/setup-prompt.js +4 -2
- package/dist/skill-installer.js +9 -1
- package/dist/smoke.js +250 -0
- package/dist/task-assignment-manifest.js +39 -2
- package/dist/vercel-sandbox-provider.js +260 -0
- package/dist/workbench-cli.js +493 -0
- package/dist/workspace-sync.js +924 -0
- package/dist/workspace.js +868 -57
- package/package.json +4 -1
- package/src/artifact-archive.ts +182 -0
- package/src/connectors-cli.ts +115 -0
- package/src/credentials.ts +6 -2
- package/src/daytona-slice-rehearsal.ts +320 -0
- package/src/daytona-slice.ts +598 -0
- package/src/egress-allowlist-cli.ts +147 -0
- package/src/egress-policy.ts +188 -0
- package/src/git-credential.ts +155 -0
- package/src/github-setup.ts +366 -0
- package/src/help.ts +272 -16
- package/src/hosted-bootstrap-rehearsal.ts +312 -0
- package/src/hosted-bootstrap.ts +571 -0
- package/src/hosted-broker-server.ts +278 -0
- package/src/hosted-git-http-server.ts +164 -0
- package/src/hosted-harness-claude.ts +621 -0
- package/src/hosted-harness-opencode.ts +823 -0
- package/src/hosted-harness.ts +124 -0
- package/src/hosted-loop.ts +680 -0
- package/src/hosted-run-event-sink.ts +632 -0
- package/src/hosted-runtime-assets.ts +272 -0
- package/src/hosted-session-cli.ts +844 -0
- package/src/index.ts +987 -296
- package/src/kill-switch-cli.ts +113 -0
- package/src/manifests-cli.ts +168 -0
- package/src/preview-runtime.ts +14 -9
- package/src/sandbox-provider.ts +440 -0
- package/src/secret-redaction.ts +124 -0
- package/src/session-timeline.ts +152 -0
- package/src/setup-prompt.ts +4 -2
- package/src/skill-installer.ts +13 -1
- package/src/smoke.ts +342 -0
- package/src/task-assignment-manifest.ts +70 -2
- package/src/vercel-sandbox-provider.ts +388 -0
- package/src/workbench-cli.ts +701 -0
- package/src/workspace-sync.ts +1148 -0
- package/src/workspace.ts +1051 -64
- package/vendor/skills/orizu-cli/SKILL.md +3 -1
- 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.
|