orizu 0.5.9 → 0.5.11

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 (60) hide show
  1. package/dist/daytona-slice-rehearsal.js +2 -2
  2. package/dist/daytona-slice.js +1 -51
  3. package/dist/git-credential.js +10 -7
  4. package/dist/github-setup.js +18 -2
  5. package/dist/help.js +16 -11
  6. package/dist/hosted-boot.js +713 -0
  7. package/dist/hosted-bootstrap.js +19 -41
  8. package/dist/hosted-harness-opencode.js +4 -2
  9. package/dist/hosted-headless.js +17 -6
  10. package/dist/hosted-loop-lifecycle.js +172 -0
  11. package/dist/hosted-loop.js +57 -148
  12. package/dist/hosted-run-event-sink.js +49 -10
  13. package/dist/hosted-runtime-assets.js +25 -1
  14. package/dist/hosted-session-cli.js +101 -8
  15. package/dist/hosted-skill-staging.js +89 -0
  16. package/dist/index.js +82 -62
  17. package/dist/manifests-cli.js +7 -2
  18. package/dist/preview-runtime.js +21 -4
  19. package/dist/prompt-draft-push.js +95 -0
  20. package/dist/report-comments-cli.js +9 -2
  21. package/dist/sandbox-hygiene.js +60 -0
  22. package/dist/sandbox-provider.js +15 -3
  23. package/dist/scorer-draft-push.js +110 -0
  24. package/dist/team-connectors-cli.js +140 -0
  25. package/dist/vercel-sandbox-provider.js +7 -1
  26. package/dist/workbench-cli.js +234 -6
  27. package/dist/workspace-sync.js +9 -2
  28. package/dist/workspace.js +15 -43
  29. package/dist/zip-draft-push.js +125 -0
  30. package/package.json +6 -6
  31. package/src/daytona-slice-rehearsal.ts +2 -2
  32. package/src/daytona-slice.ts +1 -70
  33. package/src/git-credential.ts +10 -7
  34. package/src/github-setup.ts +24 -2
  35. package/src/help.ts +18 -11
  36. package/src/hosted-boot.ts +946 -0
  37. package/src/hosted-bootstrap.ts +20 -44
  38. package/src/hosted-harness-opencode.ts +4 -2
  39. package/src/hosted-headless.ts +26 -8
  40. package/src/hosted-loop-lifecycle.ts +298 -0
  41. package/src/hosted-loop.ts +91 -269
  42. package/src/hosted-run-event-sink.ts +58 -11
  43. package/src/hosted-runtime-assets.ts +42 -1
  44. package/src/hosted-session-cli.ts +130 -8
  45. package/src/hosted-skill-staging.ts +126 -0
  46. package/src/index.ts +96 -72
  47. package/src/manifests-cli.ts +8 -2
  48. package/src/preview-runtime.ts +27 -4
  49. package/src/prompt-draft-push.ts +143 -0
  50. package/src/report-comments-cli.ts +10 -2
  51. package/src/sandbox-hygiene.ts +79 -0
  52. package/src/sandbox-provider.ts +15 -3
  53. package/src/scorer-draft-push.ts +155 -0
  54. package/src/team-connectors-cli.ts +171 -0
  55. package/src/vercel-sandbox-provider.ts +7 -1
  56. package/src/workbench-cli.ts +326 -6
  57. package/src/workspace-sync.ts +9 -2
  58. package/src/workspace.ts +15 -45
  59. package/src/zip-draft-push.ts +177 -0
  60. package/vendor/skills/orizu-cli/references/building-apps.md +2 -0
@@ -0,0 +1,713 @@
1
+ /**
2
+ * `orizu internal hosted-boot` — the in-sandbox BOOT ENTRYPOINT for the DO-path
3
+ * hosted agent (ALI-1057). The session coordinator's Durable Object launches
4
+ * this DETACHED (`ORIZU_SANDBOX_ENTRYPOINT`, via nohup) once it has verified the
5
+ * pre-baked runtime marker; from here the sandbox bootstraps itself under the
6
+ * PULL model (ALI-1055): no operator writes its credentials, and no bearer or
7
+ * connector secret ever rests in Cloudflare.
8
+ *
9
+ * WHAT DIFFERS FROM THE OPERATOR PATH (packages/cli/src/hosted-session-cli.ts +
10
+ * hosted-bootstrap.ts) — and ONLY this:
11
+ * (a) credentials are PULLED over HTTP at boot instead of the operator writing
12
+ * a 0600 bearer file: the boot secret is exchanged for a fresh Orizu agent
13
+ * bearer at ORIZU_AGENT_TOKEN_URL, and the connector env comes from
14
+ * ORIZU_ENV_BUNDLE_URL (the 5A contract — connectors only, NO model key);
15
+ * (b) the git credential helper runs in PULL MODE (hosted-runtime-assets.ts):
16
+ * it GETs a fresh bearer per git op from the agent-token URL (boot-secret
17
+ * auth) rather than reading a host-rotated 0600 file.
18
+ * Everything else — the clone via the credential helper, the loop, the event
19
+ * sink, redaction — is REUSED, not reimplemented.
20
+ *
21
+ * FROZEN ENV CONTRACT (workers/session-coordinator/src/bootstrap.ts
22
+ * `planSandboxEnv`). Required (fail-fast if any is missing):
23
+ * ORIZU_BOOT_SECRET the per-sandbox durable bootstrap secret;
24
+ * ORIZU_AGENT_TOKEN_URL {coordinator}/sessions/:id/agent-token (bearer pull);
25
+ * ORIZU_ENV_BUNDLE_URL {orizu}/api/coordinator/sessions/:id/env-bundle;
26
+ * ORIZU_BASE_URL the Orizu control-plane origin;
27
+ * ORIZU_SESSION_ID this session id.
28
+ * Present-but-optional in the contract, resolved from the control plane when
29
+ * absent: ORIZU_RUN_ID, ORIZU_WORKSPACE_ID (read back from the session), and the
30
+ * non-secret ANTHROPIC_API_KEY dummy placeholder (firewall brokers the real key).
31
+ */
32
+ import { spawnSync } from 'child_process';
33
+ import { closeSync, mkdirSync, openSync, readFileSync, renameSync, writeSync } from 'fs';
34
+ import { AGENT_GIT_IDENTITY, BEARER_BASENAME, BOOT_CONTEXT_BASENAME, DEFAULT_CACHE_REFRESH_BUFFER_MS, HELPER_SCRIPT_BASENAME, REPO_CRED_CACHE_BASENAME, renderCredentialHelperScript, serializeBootContext, } from './hosted-runtime-assets.js';
35
+ import { INJECTED_ENV_VARS_ENV, runHostedLoop, } from './hosted-loop.js';
36
+ import { DEFAULT_EGRESS_CANARY_HOST } from './hosted-loop-lifecycle.js';
37
+ import { resumeRunEventSink } from './hosted-run-event-sink.js';
38
+ import { stageOrizuSkill } from './hosted-skill-staging.js';
39
+ /** Non-secret placeholder model key (firewall brokers the real org key on
40
+ * egress). Byte-for-byte the operator path's constant
41
+ * (packages/cli/src/hosted-session-cli.ts `ANTHROPIC_DUMMY_KEY` and the
42
+ * DO-path `workers/session-coordinator/src/bootstrap.ts`); kept in sync by
43
+ * grep. Inlined (not imported) to avoid a hosted-session-cli import cycle. */
44
+ const ANTHROPIC_DUMMY_KEY = 'sk-ant-orizu-proxy-broker-placeholder';
45
+ /** Default hosted model — mirrors hosted-session-cli.ts `DEFAULT_HOSTED_MODEL`. */
46
+ const DEFAULT_HOSTED_MODEL = 'anthropic/claude-opus-4-8';
47
+ // -- Frozen env contract ------------------------------------------------------
48
+ export const REQUIRED_BOOT_ENV_VARS = [
49
+ 'ORIZU_BOOT_SECRET',
50
+ 'ORIZU_AGENT_TOKEN_URL',
51
+ 'ORIZU_ENV_BUNDLE_URL',
52
+ 'ORIZU_BASE_URL',
53
+ 'ORIZU_SESSION_ID',
54
+ ];
55
+ /** Validate the frozen env contract. A list of missing REQUIRED names means the
56
+ * boot must FAIL FAST with a clear error (the DO records the non-zero exit). */
57
+ export function resolveHostedBootEnv(env) {
58
+ const missing = [];
59
+ const req = (name) => {
60
+ const value = env[name]?.trim();
61
+ if (!value)
62
+ missing.push(name);
63
+ return value ?? '';
64
+ };
65
+ const bootSecret = req('ORIZU_BOOT_SECRET');
66
+ const agentTokenUrl = req('ORIZU_AGENT_TOKEN_URL');
67
+ const envBundleUrl = req('ORIZU_ENV_BUNDLE_URL');
68
+ const baseUrl = req('ORIZU_BASE_URL');
69
+ const sessionId = req('ORIZU_SESSION_ID');
70
+ if (missing.length > 0)
71
+ return { ok: false, missing };
72
+ return {
73
+ ok: true,
74
+ value: {
75
+ bootSecret,
76
+ agentTokenUrl,
77
+ // The boot-status route is the agent-token route's sibling on the same
78
+ // coordinator, same boot-secret auth (ALI-1060). Deriving it here means no
79
+ // new env var in the frozen contract / planSandboxEnv.
80
+ bootStatusUrl: deriveBootStatusUrl(agentTokenUrl),
81
+ envBundleUrl,
82
+ baseUrl: baseUrl.replace(/\/+$/, ''),
83
+ sessionId,
84
+ runId: env.ORIZU_RUN_ID?.trim() || null,
85
+ workspaceId: env.ORIZU_WORKSPACE_ID?.trim() || null,
86
+ anthropicDummyKey: env.ANTHROPIC_API_KEY?.trim() || null,
87
+ },
88
+ };
89
+ }
90
+ /** The boot-status route is the agent-token route's sibling; swap the trailing
91
+ * path segment. Falls back to appending when the shape is unexpected. */
92
+ export function deriveBootStatusUrl(agentTokenUrl) {
93
+ if (/\/agent-token\/?$/.test(agentTokenUrl)) {
94
+ return agentTokenUrl.replace(/\/agent-token\/?$/, '/boot-status');
95
+ }
96
+ return `${agentTokenUrl.replace(/\/+$/, '')}/boot-status`;
97
+ }
98
+ // -- Boot-status callback (ALI-1060) ------------------------------------------
99
+ /** Max chars of the failure reason reported to the coordinator. A boot reason
100
+ * is never a place for a secret; this bounds it anyway, and we scrub the boot
101
+ * secret defensively before sending. */
102
+ const MAX_BOOT_REASON_CHARS = 800;
103
+ /** Best-effort scrub + truncate for a reported failure reason: never leak the
104
+ * boot secret (the one credential the boot always holds), and keep it short. */
105
+ export function redactBootReason(reason, bootSecret) {
106
+ let out = reason;
107
+ if (bootSecret && out.includes(bootSecret)) {
108
+ out = out.split(bootSecret).join('[redacted]');
109
+ }
110
+ return out.slice(0, MAX_BOOT_REASON_CHARS);
111
+ }
112
+ /**
113
+ * Report the boot outcome to the coordinator's boot-status route (ALI-1060),
114
+ * authed with the boot secret (same as the agent-token pull). Best-effort: the
115
+ * DO's own readiness-timeout is the backstop if this never lands, so a failed
116
+ * report must never mask the boot result. NEVER logs the secret.
117
+ */
118
+ export async function postBootStatus(opts) {
119
+ try {
120
+ const res = await opts.fetchImpl(opts.bootStatusUrl, {
121
+ method: 'POST',
122
+ headers: { Authorization: `Bearer ${opts.bootSecret}`, 'Content-Type': 'application/json' },
123
+ body: JSON.stringify({
124
+ status: opts.status,
125
+ ...(opts.runId ? { runId: opts.runId } : {}),
126
+ ...(opts.reason ? { reason: opts.reason } : {}),
127
+ }),
128
+ });
129
+ opts.log?.(`boot-status ${opts.status} reported (${res.status})`);
130
+ }
131
+ catch (error) {
132
+ opts.log?.(`boot-status ${opts.status} report failed: ${error instanceof Error ? error.message : String(error)}`);
133
+ }
134
+ }
135
+ /**
136
+ * Exchange the boot secret for a fresh Orizu agent bearer at the coordinator's
137
+ * agent-token route (`GET`, `Authorization: Bearer <boot secret>` → 200
138
+ * `{token, tokenId, expiresAt}`). Retries with exponential backoff: the DO
139
+ * persists the boot-secret digest and serves this route DURING bootstrap, but a
140
+ * transient mint failure (control-plane blip) should not abort the whole boot.
141
+ * The secret is NEVER logged.
142
+ */
143
+ export async function pullAgentBearer(opts) {
144
+ const attempts = Math.max(1, opts.attempts ?? 5);
145
+ const baseBackoff = opts.backoffMs ?? 500;
146
+ const sleep = opts.sleep ?? ((ms) => new Promise(resolve => setTimeout(resolve, ms)));
147
+ let lastDetail = 'no attempt made';
148
+ for (let attempt = 0; attempt < attempts; attempt++) {
149
+ try {
150
+ const res = await opts.fetchImpl(opts.agentTokenUrl, {
151
+ method: 'GET',
152
+ headers: { Authorization: `Bearer ${opts.bootSecret}` },
153
+ });
154
+ if (res.ok) {
155
+ const data = (await res.json().catch(() => null));
156
+ const token = data && typeof data.token === 'string' ? data.token : '';
157
+ if (token) {
158
+ const expiry = data && typeof data.expiresAt === 'string' ? Date.parse(data.expiresAt) : NaN;
159
+ return { token, expiresAtMs: Number.isFinite(expiry) ? expiry : null };
160
+ }
161
+ lastDetail = 'agent-token response carried no token';
162
+ }
163
+ else {
164
+ lastDetail = `agent-token pull returned ${res.status}`;
165
+ }
166
+ }
167
+ catch (error) {
168
+ lastDetail = error instanceof Error ? error.message : String(error);
169
+ }
170
+ if (attempt < attempts - 1) {
171
+ const delay = baseBackoff * 2 ** attempt;
172
+ opts.log?.(`agent-token pull attempt ${attempt + 1}/${attempts} failed (${lastDetail}); retrying in ${delay}ms`);
173
+ await sleep(delay);
174
+ }
175
+ }
176
+ throw new Error(`agent-token pull failed after ${attempts} attempts: ${lastDetail}`);
177
+ }
178
+ /**
179
+ * Pull the connector env bundle (5A contract): `GET` with the agent bearer → 200
180
+ * `{sessionId, connectors:[{ref,envVar,value}], redactEnvVars:[...]}`. The model
181
+ * key is DELIBERATELY absent (brokered at the firewall). Never logs a value.
182
+ */
183
+ export async function fetchEnvBundle(opts) {
184
+ const res = await opts.fetchImpl(opts.envBundleUrl, {
185
+ method: 'GET',
186
+ headers: { Authorization: `Bearer ${opts.bearer}` },
187
+ });
188
+ if (!res.ok)
189
+ throw new Error(`env-bundle pull returned ${res.status}`);
190
+ const data = (await res.json().catch(() => null));
191
+ if (!data || typeof data !== 'object')
192
+ throw new Error('env-bundle response was not a JSON object');
193
+ const connectors = Array.isArray(data.connectors)
194
+ ? data.connectors.filter((c) => !!c && typeof c === 'object' && typeof c.envVar === 'string' && typeof c.value === 'string')
195
+ : [];
196
+ const redactEnvVars = Array.isArray(data.redactEnvVars)
197
+ ? data.redactEnvVars.filter((v) => typeof v === 'string')
198
+ : connectors.map(c => c.envVar);
199
+ return {
200
+ sessionId: typeof data.sessionId === 'string' ? data.sessionId : null,
201
+ connectors,
202
+ redactEnvVars,
203
+ };
204
+ }
205
+ /**
206
+ * Export each connector `{envVar: value}` into the environment the loop/agent
207
+ * inherits, and REGISTER every redacted var on `ORIZU_INJECTED_ENV_VARS` — the
208
+ * exact hook the in-sandbox loop reads (`redactionListFromEnv`) to scrub these
209
+ * values from run events. Returns the merged redaction-var list.
210
+ */
211
+ export function applyEnvBundle(bundle, env) {
212
+ for (const connector of bundle.connectors) {
213
+ env[connector.envVar] = connector.value;
214
+ }
215
+ const existing = (env[INJECTED_ENV_VARS_ENV] ?? '')
216
+ .split(',')
217
+ .map(name => name.trim())
218
+ .filter(name => name.length > 0);
219
+ // Redact EVERY exported connector var, not only the server's redactEnvVars
220
+ // list (review F4 defense-in-depth): if a compromised/misbehaving bundle
221
+ // returned connectors with a partial/empty redactEnvVars, their values would
222
+ // otherwise reach run events unscrubbed. We export the value, so we redact it.
223
+ const connectorVars = bundle.connectors.map(c => c.envVar);
224
+ const merged = Array.from(new Set([...existing, ...bundle.redactEnvVars, ...connectorVars]));
225
+ env[INJECTED_ENV_VARS_ENV] = merged.join(',');
226
+ return merged;
227
+ }
228
+ async function bearerJson(fetchImpl, url, bearer, init = {}) {
229
+ const res = await fetchImpl(url, {
230
+ ...init,
231
+ headers: { ...(init.headers || {}), Authorization: `Bearer ${bearer}` },
232
+ });
233
+ if (!res.ok) {
234
+ throw new Error(`${init.method ?? 'GET'} ${new URL(url).pathname} failed (${res.status})`);
235
+ }
236
+ return (await res.json().catch(() => ({})));
237
+ }
238
+ function asString(value) {
239
+ return typeof value === 'string' && value.length > 0 ? value : null;
240
+ }
241
+ function asPositiveNumber(value) {
242
+ return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : null;
243
+ }
244
+ /** GET the session (agent-capable, RLS-scoped) to learn its workspace, branch,
245
+ * task, and any existing run — the DO start body carries none of these. */
246
+ export async function resolveSession(opts) {
247
+ const body = await bearerJson(opts.fetchImpl, `${opts.baseUrl}/api/cli/sessions/${encodeURIComponent(opts.sessionId)}`, opts.bearer);
248
+ const session = (body.session ?? {});
249
+ const workspaceId = asString(session.workspaceId);
250
+ if (!workspaceId)
251
+ throw new Error('session response carried no workspaceId');
252
+ const repoBranch = asString(session.repoBranch);
253
+ if (!repoBranch)
254
+ throw new Error('session response carried no repoBranch (branch not provisioned)');
255
+ const clientInfo = (session.clientInfo ?? {});
256
+ const task = asString(clientInfo.task);
257
+ if (!task)
258
+ throw new Error('session client_info carried no task prompt');
259
+ const runs = Array.isArray(session.runs) ? session.runs : [];
260
+ const runId = runs.length > 0 ? asString(runs[0].id) : null;
261
+ return {
262
+ workspaceId,
263
+ repoBranch,
264
+ task,
265
+ model: asString(clientInfo.model),
266
+ reasoningEffort: asString(clientInfo.reasoningEffort),
267
+ durationMinutes: asPositiveNumber(clientInfo.durationMinutes),
268
+ runId,
269
+ };
270
+ }
271
+ /** Reuse an existing run or start one with the AGENT bearer (actor_type='agent'),
272
+ * exactly like the operator path's run-start step. */
273
+ export async function ensureRun(opts) {
274
+ if (opts.existingRunId)
275
+ return opts.existingRunId;
276
+ const body = await bearerJson(opts.fetchImpl, `${opts.baseUrl}/api/cli/workbench-runs`, opts.bearer, {
277
+ method: 'POST',
278
+ headers: { 'Content-Type': 'application/json' },
279
+ body: JSON.stringify({
280
+ workspaceSessionId: opts.sessionId,
281
+ title: opts.title ?? `Hosted session ${opts.sessionId}`,
282
+ }),
283
+ });
284
+ const run = (body.run ?? {});
285
+ const runId = asString(run.id);
286
+ if (!runId)
287
+ throw new Error('workbench-run start returned no run id');
288
+ return runId;
289
+ }
290
+ /** Mint a session_read repo token to learn the repo full name, then build the
291
+ * GitHub clone URL (mirrors the operator path's `defaultResolveRepo`). */
292
+ export async function resolveRepo(opts) {
293
+ const url = `${opts.baseUrl}/api/cli/workspaces/${encodeURIComponent(opts.workspaceId)}/repo-token`;
294
+ const minted = await bearerJson(opts.fetchImpl, url, opts.bearer, {
295
+ method: 'POST',
296
+ headers: { 'Content-Type': 'application/json' },
297
+ body: JSON.stringify({ purpose: 'session_read', sessionId: opts.sessionId }),
298
+ });
299
+ const repoFullName = asString(minted.repo);
300
+ if (!repoFullName)
301
+ throw new Error('repo-token response carried no repo');
302
+ // We deliberately do NOT early-revoke this probe token (ALI-1069): the
303
+ // repo-token DELETE route is HUMAN-ONLY (`requireCliSupabase` default-denies
304
+ // agent bearers), so an agent-bearer revoke would always 401 — a guaranteed
305
+ // failed request per boot, not a real revoke. Let the short (~60-min)
306
+ // session_read TTL expire on its own; the credential helper mints its own
307
+ // per-op tokens for the actual clone/fetch/push, so this probe token is never
308
+ // reused after we read `repo` from it.
309
+ return { repoFullName, cloneUrl: `https://github.com/${repoFullName}.git` };
310
+ }
311
+ const defaultExec = (cmd, args, opts) => {
312
+ const res = spawnSync(cmd, args, { cwd: opts?.cwd, encoding: 'utf8' });
313
+ return { status: res.status ?? 1, stdout: res.stdout ?? '', stderr: res.stderr ?? '' };
314
+ };
315
+ /** Values interpolated into git's credential.helper string / clone args must be
316
+ * benign. We pass everything via execFile-style arg arrays (no shell), but
317
+ * still reject anything with newlines/control chars as defense in depth. */
318
+ const SAFE_GIT_VALUE = /^[A-Za-z0-9._/:@+ -]+$/;
319
+ function assertSafeGitValue(name, value) {
320
+ if (!SAFE_GIT_VALUE.test(value)) {
321
+ throw new Error(`unsafe characters in ${name}; refusing to pass to git`);
322
+ }
323
+ }
324
+ /**
325
+ * Path-safety for the run id BEFORE it is interpolated into `.orizu-run/${runId}`
326
+ * (ALI-1060). ORIZU_RUN_ID can be CALLER-SUPPLIED (resume flows), and unlike the
327
+ * boot-created uuid it is untrusted — a `../` value would escape the run dir and
328
+ * let boot assets (0600 bearer/boot-secret files) land outside it. A run id is a
329
+ * single opaque segment: allow only `[A-Za-z0-9._-]` and reject any `..`, so no
330
+ * separators or traversal survive. Stricter than the operator path's shared
331
+ * shell-value assertion (which allows `/`), applied to the same value.
332
+ */
333
+ const SAFE_RUN_ID = /^[A-Za-z0-9._-]+$/;
334
+ export function assertSafeRunId(runId) {
335
+ if (!SAFE_RUN_ID.test(runId) || runId.includes('..')) {
336
+ throw new Error('unsafe run id; refusing to build run-dir paths');
337
+ }
338
+ }
339
+ function writeSecretFile(path, contents, write) {
340
+ write(path, contents.endsWith('\n') ? contents : `${contents}\n`);
341
+ }
342
+ /**
343
+ * The full in-sandbox boot: validate → pull bearer → pull+apply env bundle →
344
+ * resolve session/run/repo → write the pull-mode credential assets → clone the
345
+ * session branch → launch the hosted loop (reused, in-process) with a bearer
346
+ * provider fed by the same pull-mode source. Returns the loop's terminal status.
347
+ */
348
+ export async function runHostedBoot(opts) {
349
+ const env = opts.env;
350
+ const processEnv = opts.processEnv ?? process.env;
351
+ const root = opts.root ?? process.cwd();
352
+ const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
353
+ const exec = opts.exec ?? defaultExec;
354
+ const mkdirp = opts.mkdirp ?? ((path) => void mkdirSync(path, { recursive: true }));
355
+ const writeFile = opts.writeFile ??
356
+ ((path, contents) => {
357
+ // Atomic write (review F2): write a 0600 temp then rename, so a reader
358
+ // (the loop reading the bearer file per request, or the credential
359
+ // helper) never observes a truncated file mid-rotation. Single writer
360
+ // per path (the rotation timer), so a fixed .tmp suffix is safe.
361
+ const tmp = `${path}.tmp`;
362
+ const fd = openSync(tmp, 'w', 0o600);
363
+ try {
364
+ writeSync(fd, contents);
365
+ }
366
+ finally {
367
+ closeSync(fd);
368
+ }
369
+ renameSync(tmp, path);
370
+ });
371
+ const readFile = opts.readFile ?? ((path) => readFileSync(path, 'utf8'));
372
+ const log = opts.log ?? (() => { });
373
+ const now = opts.now ?? (() => Date.now());
374
+ // 1 — Pull the agent bearer (retry/backoff — the DO may still be arming).
375
+ const bearer = await pullAgentBearer({
376
+ agentTokenUrl: env.agentTokenUrl,
377
+ bootSecret: env.bootSecret,
378
+ fetchImpl,
379
+ attempts: opts.bearerAttempts,
380
+ backoffMs: opts.bearerBackoffMs,
381
+ sleep: opts.sleep,
382
+ log,
383
+ });
384
+ log('agent bearer pulled');
385
+ // 2 — Pull the connector env bundle and export it (+ register redaction).
386
+ const bundle = await fetchEnvBundle({ envBundleUrl: env.envBundleUrl, bearer: bearer.token, fetchImpl });
387
+ const redacted = applyEnvBundle(bundle, processEnv);
388
+ log(`env bundle applied (${bundle.connectors.length} connectors, ${redacted.length} redacted vars)`);
389
+ // 3 — Resolve the session (workspace/branch/task) + run + repo from the plane.
390
+ const session = await resolveSession({ baseUrl: env.baseUrl, sessionId: env.sessionId, bearer: bearer.token, fetchImpl });
391
+ const workspaceId = env.workspaceId ?? session.workspaceId;
392
+ const runId = await ensureRun({
393
+ baseUrl: env.baseUrl,
394
+ sessionId: env.sessionId,
395
+ bearer: bearer.token,
396
+ fetchImpl,
397
+ existingRunId: env.runId ?? session.runId,
398
+ });
399
+ // Path-safety BEFORE the run id reaches any `.orizu-run/${runId}` path
400
+ // (ALI-1060): a caller-supplied ORIZU_RUN_ID must not escape the run dir.
401
+ assertSafeRunId(runId);
402
+ // Hand the run id + bearer to the caller so a throw AFTER this point can mark
403
+ // the run failed + report the boot failure (the DO is the backstop otherwise).
404
+ opts.onBootContext?.({ runId, bearer: bearer.token });
405
+ const repo = await resolveRepo({ baseUrl: env.baseUrl, workspaceId, sessionId: env.sessionId, bearer: bearer.token, fetchImpl });
406
+ log(`resolved run ${runId} on ${repo.repoFullName}@${session.repoBranch}`);
407
+ // 4 — Write the run-scoped assets: boot secret + initial bearer (0600), the
408
+ // PULL-MODE credential helper + its boot context. The bearer file feeds the
409
+ // loop's event sink; the boot secret feeds the credential helper's per-op pull.
410
+ const runDirRel = `.orizu-run/${runId}`;
411
+ const runDirAbs = `${root}/${runDirRel}`;
412
+ const workspaceDir = `${root}/repo`;
413
+ assertSafeGitValue('sessionBranch', session.repoBranch);
414
+ assertSafeGitValue('cloneUrl', repo.cloneUrl);
415
+ mkdirp(runDirAbs);
416
+ const bootSecretFileAbs = `${runDirAbs}/boot-secret`;
417
+ const bearerFileAbs = `${runDirAbs}/${BEARER_BASENAME}`;
418
+ const helperScriptAbs = `${runDirAbs}/${HELPER_SCRIPT_BASENAME}`;
419
+ const bootContextAbs = `${runDirAbs}/${BOOT_CONTEXT_BASENAME}`;
420
+ const cacheFileAbs = `${runDirAbs}/${REPO_CRED_CACHE_BASENAME}`;
421
+ const taskFileAbs = `${runDirAbs}/task.txt`;
422
+ writeSecretFile(bootSecretFileAbs, env.bootSecret, writeFile);
423
+ writeSecretFile(bearerFileAbs, bearer.token, writeFile);
424
+ writeFile(taskFileAbs, session.task);
425
+ writeFile(helperScriptAbs, renderCredentialHelperScript());
426
+ const bootContext = {
427
+ apiBaseUrl: env.baseUrl,
428
+ workspaceId,
429
+ sessionId: env.sessionId,
430
+ runId,
431
+ sessionBranch: session.repoBranch,
432
+ repoFullName: repo.repoFullName,
433
+ host: 'github.com',
434
+ bearerFile: bearerFileAbs,
435
+ cacheFile: cacheFileAbs,
436
+ // PULL MODE: source the bearer over HTTP from the coordinator per git op.
437
+ agentTokenUrl: env.agentTokenUrl,
438
+ bootSecretFile: bootSecretFileAbs,
439
+ tokenPurposes: { primary: 'session_write', fallback: 'session_read' },
440
+ cacheBufferMs: DEFAULT_CACHE_REFRESH_BUFFER_MS,
441
+ };
442
+ writeFile(bootContextAbs, serializeBootContext(bootContext));
443
+ // 5 — Clone the session branch VIA the pull-mode credential helper (same
444
+ // invocation the operator path makes; no token in the URL/config).
445
+ const helperValue = `!node ${helperScriptAbs} ${bootContextAbs}`;
446
+ const clone = exec('git', [
447
+ 'clone',
448
+ '--depth',
449
+ '1',
450
+ '--branch',
451
+ session.repoBranch,
452
+ '-c',
453
+ `credential.helper=${helperValue}`,
454
+ '-c',
455
+ 'credential.useHttpPath=true',
456
+ repo.cloneUrl,
457
+ workspaceDir,
458
+ ]);
459
+ if (clone.status !== 0) {
460
+ const detail = (clone.stderr || clone.stdout || `exit ${clone.status}`).trim();
461
+ throw new Error(`git clone failed: ${detail}`);
462
+ }
463
+ // Persist the helper + agent identity repo-LOCAL for subsequent fetch/push.
464
+ exec('git', ['-C', workspaceDir, 'config', 'credential.helper', helperValue]);
465
+ exec('git', ['-C', workspaceDir, 'config', 'credential.useHttpPath', 'true']);
466
+ exec('git', ['-C', workspaceDir, 'config', 'user.name', AGENT_GIT_IDENTITY.name]);
467
+ exec('git', ['-C', workspaceDir, 'config', 'user.email', AGENT_GIT_IDENTITY.email]);
468
+ log(`cloned ${session.repoBranch}`);
469
+ // 5b — Stage the orizu-cli skill into the cloned repo so the agent discovers the
470
+ // Orizu workflows (ALI-1059). SHARED with the operator path via `stageOrizuSkill`
471
+ // (one resolution chain + the harvest-safe .git/info/exclude append). Non-fatal:
472
+ // a staging failure is logged, never aborts the boot. The DO `BootExec` is
473
+ // shell-less, so the staging script runs under `sh -c`.
474
+ try {
475
+ const skillStage = await stageOrizuSkill({
476
+ workspaceDir,
477
+ exec: async (command) => {
478
+ const res = exec('sh', ['-c', command]);
479
+ return { exitCode: res.status, stdout: res.stdout, stderr: res.stderr ?? '' };
480
+ },
481
+ });
482
+ log(`orizu-cli skill staged: ${skillStage.ok ? skillStage.method : 'unresolved'}`);
483
+ }
484
+ catch (error) {
485
+ log(`orizu-cli skill staging failed: ${error instanceof Error ? error.message : String(error)}`);
486
+ }
487
+ // ALI-1060: the agent is now genuinely LIVE — bearer minted, connectors
488
+ // applied, session/run resolved, repo cloned, skill staged. Signal the
489
+ // coordinator's readiness gate BEFORE the loop so the DO stops waiting (and
490
+ // never tears down a healthy box on timeout). Best-effort; the DO's timeout is
491
+ // the backstop.
492
+ await postBootStatus({
493
+ bootStatusUrl: env.bootStatusUrl,
494
+ bootSecret: env.bootSecret,
495
+ status: 'ready',
496
+ runId,
497
+ fetchImpl,
498
+ log,
499
+ });
500
+ // 6 — Launch the hosted loop, reused in-process. The bearer PROVIDER keeps the
501
+ // event sink's bearer fresh from the same pull-mode source; the DO-provisioned
502
+ // sandbox always runs the pre-baked image under an enforced egress policy, so
503
+ // the loop's startup egress canary + deferred setup hook are both armed.
504
+ const model = session.model ?? DEFAULT_HOSTED_MODEL;
505
+ const loopContext = {
506
+ apiBaseUrl: env.baseUrl,
507
+ runId,
508
+ bearerFile: bearerFileAbs,
509
+ taskFile: taskFileAbs,
510
+ workspaceDir,
511
+ model,
512
+ reasoningEffort: session.reasoningEffort ?? undefined,
513
+ messageId: `${runId}:task`,
514
+ author: AGENT_GIT_IDENTITY,
515
+ anthropicDummyKey: env.anthropicDummyKey ?? ANTHROPIC_DUMMY_KEY,
516
+ // Derive the per-prompt max-duration cap from the session duration so a long
517
+ // run is not killed at the hard-coded 90-min prompt cap (ALI-1061). Unset →
518
+ // the harness default (5400s) floor holds.
519
+ sandboxBudgetMs: session.durationMinutes != null ? session.durationMinutes * 60 * 1000 : undefined,
520
+ prebaked: true,
521
+ egressCanaryHost: DEFAULT_EGRESS_CANARY_HOST,
522
+ runSetupHook: true,
523
+ };
524
+ const rotation = startBearerRotation({
525
+ agentTokenUrl: env.agentTokenUrl,
526
+ bootSecret: env.bootSecret,
527
+ bearerFileAbs,
528
+ fetchImpl,
529
+ writeFile,
530
+ initialExpiresAtMs: bearer.expiresAtMs,
531
+ now,
532
+ log,
533
+ });
534
+ try {
535
+ const runLoop = opts.runLoop ??
536
+ ((input) => runHostedLoop({ context: input.context, taskPrompt: input.taskPrompt, bearerProvider: input.bearerProvider }));
537
+ const result = await runLoop({
538
+ context: loopContext,
539
+ taskPrompt: session.task,
540
+ // Read the bearer FILE fresh per request (review F1): `startBearerRotation`
541
+ // rewrites `bearerFileAbs` before the current bearer expires, so a run
542
+ // exceeding the ~60-min TTL (DO cap is 24h) keeps a valid bearer. A frozen
543
+ // closure over the boot-time token would silently stop recording events at
544
+ // ~TTL. The initial token was written to this file at boot (writeSecretFile
545
+ // above); the write is atomic (F2), so this never reads a torn file.
546
+ bearerProvider: () => readFile(bearerFileAbs).trim(),
547
+ });
548
+ log(`hosted-loop finished: ${result.status}`);
549
+ return { ok: !result.error, runId, loopStatus: result.status, error: result.error };
550
+ }
551
+ finally {
552
+ rotation.stop();
553
+ }
554
+ }
555
+ /**
556
+ * Background refresher: before the current bearer expires, pull a fresh one from
557
+ * the coordinator (boot-secret auth) and atomically rewrite the 0600 bearer file
558
+ * the loop reads per request. This is the DO-path analogue of the operator's
559
+ * host-side rotation loop — moved in-sandbox because no operator is attached.
560
+ * Best-effort: a failed refresh is logged and retried. The loop's bearerProvider
561
+ * reads this file fresh per request (see runHostedBoot), so a rewritten file is
562
+ * picked up on the next event/request. No-ops when there is no expiry signal.
563
+ */
564
+ function startBearerRotation(opts) {
565
+ // Refresh ~5 min before expiry; if the server gives no expiry, fall back to a
566
+ // conservative fixed cadence just under a typical 60-min TTL.
567
+ const REFRESH_BUFFER_MS = 5 * 60 * 1000;
568
+ const FALLBACK_INTERVAL_MS = 50 * 60 * 1000;
569
+ // Cap the delay well under setTimeout's 32-bit ms limit: a far-future (or
570
+ // implausible) expiry just means we re-verify the bearer every few hours.
571
+ const MAX_SCHEDULE_MS = 6 * 60 * 60 * 1000;
572
+ let stopped = false;
573
+ let timer = null;
574
+ const scheduleFrom = (expiresAtMs) => {
575
+ if (stopped)
576
+ return;
577
+ const raw = expiresAtMs ? expiresAtMs - opts.now() - REFRESH_BUFFER_MS : FALLBACK_INTERVAL_MS;
578
+ const delay = Math.min(MAX_SCHEDULE_MS, Math.max(30_000, raw));
579
+ timer = setTimeout(refresh, delay);
580
+ if (typeof timer.unref === 'function')
581
+ timer.unref();
582
+ };
583
+ const refresh = async () => {
584
+ if (stopped)
585
+ return;
586
+ try {
587
+ const fresh = await pullAgentBearer({
588
+ agentTokenUrl: opts.agentTokenUrl,
589
+ bootSecret: opts.bootSecret,
590
+ fetchImpl: opts.fetchImpl,
591
+ attempts: 3,
592
+ });
593
+ const contents = fresh.token.endsWith('\n') ? fresh.token : `${fresh.token}\n`;
594
+ opts.writeFile(opts.bearerFileAbs, contents);
595
+ opts.log('rotated agent bearer (0600 file rewritten)');
596
+ scheduleFrom(fresh.expiresAtMs);
597
+ }
598
+ catch (error) {
599
+ opts.log(`agent bearer rotation failed: ${error instanceof Error ? error.message : String(error)}`);
600
+ scheduleFrom(opts.now() + REFRESH_BUFFER_MS); // retry soon
601
+ }
602
+ };
603
+ scheduleFrom(opts.initialExpiresAtMs);
604
+ return {
605
+ stop: () => {
606
+ stopped = true;
607
+ if (timer)
608
+ clearTimeout(timer);
609
+ },
610
+ };
611
+ }
612
+ /**
613
+ * Read the frozen env contract from the process env and run the boot. Fails fast
614
+ * (exit 1) with a clear message naming every missing required var. The DO records
615
+ * the exit code; run failures beyond the boot itself are recorded server-side.
616
+ */
617
+ export async function hostedBootCommand(io) {
618
+ const resolved = resolveHostedBootEnv(process.env);
619
+ if (!resolved.ok) {
620
+ io.printErr?.(`hosted-boot: missing required env: ${resolved.missing.join(', ')} ` +
621
+ '(the DO sandbox env contract — see planSandboxEnv)');
622
+ return 1;
623
+ }
624
+ const env = resolved.value;
625
+ const log = (line) => io.printErr?.(`[hosted-boot] ${line}`);
626
+ // Captured once the run id + bearer are known, so a LATER throw can mark the
627
+ // run failed with an authenticated bearer (ALI-1060).
628
+ let bootCtx = null;
629
+ try {
630
+ const result = await runHostedBoot({
631
+ env,
632
+ log,
633
+ onBootContext: ctx => {
634
+ bootCtx = ctx;
635
+ },
636
+ });
637
+ io.print(io.json
638
+ ? JSON.stringify({ ok: result.ok, runId: result.runId, status: result.loopStatus, error: result.error })
639
+ : `hosted-boot finished: ${result.loopStatus ?? 'unknown'}${result.error ? ` (${result.error})` : ''}`);
640
+ return result.ok ? 0 : 1;
641
+ }
642
+ catch (error) {
643
+ const message = error instanceof Error ? error.message : String(error);
644
+ io.printErr?.(`hosted-boot failed: ${message}`);
645
+ // A boot THROW (before/around ensureRun) means the agent never came alive.
646
+ // Report it so the DO stops + destroys the sandbox NOW instead of waiting
647
+ // for the readiness timeout, and mark the run failed so it never stays
648
+ // 'running' forever (the DO also marks it — both are idempotent).
649
+ await reportBootFailure({ env, ctx: bootCtx, reason: message, log });
650
+ return 1;
651
+ }
652
+ }
653
+ /**
654
+ * Failure fan-out on a boot throw (ALI-1060): mark the run terminally `failed`
655
+ * (the operator path's agent-bearer terminal PATCH) and always signal the
656
+ * coordinator's boot-status route so the DO tears the sandbox down promptly.
657
+ * Best-effort throughout — the boot secret is scrubbed from the reported reason.
658
+ *
659
+ * PRE-BEARER failures (a throw before `onBootContext` — `pullAgentBearer` /
660
+ * `fetchEnvBundle` blew up, so `ctx` is null) still have a run id: the server
661
+ * PRE-CREATES it and injects ORIZU_RUN_ID. We attempt a SHORT bearer pull here
662
+ * so we can self-mark that run failed rather than leaving it `running` until the
663
+ * DO's readiness timeout. If even that pull fails, the DO backstop (boot-status
664
+ * failure now, readiness timeout otherwise) is the last line.
665
+ */
666
+ export async function reportBootFailure(opts) {
667
+ const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
668
+ const reason = redactBootReason(opts.reason, opts.env.bootSecret);
669
+ // The run id is known from the boot context, or (pre-created / resume flows)
670
+ // straight from the env contract.
671
+ const runId = opts.ctx?.runId ?? opts.env.runId;
672
+ // Prefer the bearer we already hold; otherwise pull a fresh one (bounded — the
673
+ // DO is the backstop) so a pre-bearer failure can still self-mark the run.
674
+ let bearer = opts.ctx?.bearer ?? null;
675
+ if (!bearer && runId) {
676
+ try {
677
+ bearer = (await pullAgentBearer({
678
+ agentTokenUrl: opts.env.agentTokenUrl,
679
+ bootSecret: opts.env.bootSecret,
680
+ fetchImpl,
681
+ attempts: 2,
682
+ log: opts.log,
683
+ })).token;
684
+ }
685
+ catch (error) {
686
+ opts.log(`failure-path bearer pull failed: ${error instanceof Error ? error.message : String(error)}`);
687
+ }
688
+ }
689
+ if (bearer && runId) {
690
+ try {
691
+ const sink = await resumeRunEventSink({
692
+ apiBaseUrl: opts.env.baseUrl,
693
+ runId,
694
+ bearer,
695
+ fetchImpl,
696
+ });
697
+ await sink.finish('failed', { summary: { error: reason } });
698
+ opts.log('marked run failed');
699
+ }
700
+ catch (error) {
701
+ opts.log(`run-failed mark failed: ${error instanceof Error ? error.message : String(error)}`);
702
+ }
703
+ }
704
+ await postBootStatus({
705
+ bootStatusUrl: opts.env.bootStatusUrl,
706
+ bootSecret: opts.env.bootSecret,
707
+ status: 'failed',
708
+ runId,
709
+ reason,
710
+ fetchImpl,
711
+ log: opts.log,
712
+ });
713
+ }