orizu 0.5.26 → 0.5.28
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 +19 -0
- package/dist/artifacts-git-runtime.d.ts +34 -0
- package/dist/artifacts-git-runtime.d.ts.map +1 -0
- package/dist/artifacts-git-runtime.js +312 -0
- package/dist/cloudflare-artifacts-git-remote.d.ts +5 -0
- package/dist/cloudflare-artifacts-git-remote.d.ts.map +1 -0
- package/dist/cloudflare-artifacts-git-remote.js +21 -0
- package/dist/dataset-transfer-download.d.ts +2 -0
- package/dist/dataset-transfer-download.d.ts.map +1 -0
- package/dist/dataset-transfer-download.js +66 -0
- package/dist/dataset-transfer-policy.d.ts +2 -0
- package/dist/dataset-transfer-policy.d.ts.map +1 -0
- package/dist/dataset-transfer-policy.js +8 -0
- package/dist/dataset-transfer-upload.d.ts +42 -0
- package/dist/dataset-transfer-upload.d.ts.map +1 -0
- package/dist/dataset-transfer-upload.js +110 -0
- package/dist/git-credential.d.ts.map +1 -1
- package/dist/git-credential.js +1 -1
- package/dist/git-recovery-environment.d.ts +1 -0
- package/dist/git-recovery-environment.d.ts.map +1 -1
- package/dist/git-recovery-environment.js +1 -1
- package/dist/help.d.ts.map +1 -1
- package/dist/help.js +44 -11
- package/dist/hosted-boot.d.ts +75 -2
- package/dist/hosted-boot.d.ts.map +1 -1
- package/dist/hosted-boot.js +585 -102
- package/dist/hosted-bootstrap.d.ts +2 -0
- package/dist/hosted-bootstrap.d.ts.map +1 -1
- package/dist/hosted-bootstrap.js +12 -3
- package/dist/hosted-harness-opencode.d.ts +8 -9
- package/dist/hosted-harness-opencode.d.ts.map +1 -1
- package/dist/hosted-harness-opencode.js +126 -24
- package/dist/hosted-harness.d.ts +3 -0
- package/dist/hosted-harness.d.ts.map +1 -1
- package/dist/hosted-harness.js +4 -1
- package/dist/hosted-harvest.d.ts +30 -9
- package/dist/hosted-harvest.d.ts.map +1 -1
- package/dist/hosted-harvest.js +222 -28
- package/dist/hosted-loop-lifecycle.d.ts +15 -3
- package/dist/hosted-loop-lifecycle.d.ts.map +1 -1
- package/dist/hosted-loop-lifecycle.js +6 -4
- package/dist/hosted-loop.d.ts +47 -7
- package/dist/hosted-loop.d.ts.map +1 -1
- package/dist/hosted-loop.js +310 -64
- package/dist/hosted-question-limits.d.ts +7 -0
- package/dist/hosted-question-limits.d.ts.map +1 -0
- package/dist/hosted-question-limits.js +6 -0
- package/dist/hosted-question.d.ts +27 -0
- package/dist/hosted-question.d.ts.map +1 -0
- package/dist/hosted-question.js +118 -0
- package/dist/hosted-run-event-sink.d.ts +10 -5
- package/dist/hosted-run-event-sink.d.ts.map +1 -1
- package/dist/hosted-run-event-sink.js +12 -8
- package/dist/hosted-runtime-assets.d.ts +16 -7
- package/dist/hosted-runtime-assets.d.ts.map +1 -1
- package/dist/hosted-runtime-assets.js +40 -12
- package/dist/hosted-session-cli.d.ts.map +1 -1
- package/dist/hosted-session-cli.js +10 -19
- package/dist/http.d.ts +1 -1
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +10 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +113 -92
- package/dist/instruction-set-gepa-launch.d.ts +1 -1
- package/dist/instruction-set-gepa-launch.d.ts.map +1 -1
- package/dist/instruction-set-gepa-launch.js +5 -3
- package/dist/instruction-set-loader.d.ts.map +1 -1
- package/dist/instruction-set-loader.js +63 -2
- package/dist/instruction-set-lock/index.d.ts +51 -0
- package/dist/instruction-set-lock/index.d.ts.map +1 -0
- package/dist/instruction-set-lock/index.js +459 -0
- package/dist/instruction-set-lock/schema.v1.d.ts +136 -0
- package/dist/instruction-set-lock/schema.v1.d.ts.map +1 -0
- package/dist/instruction-set-lock/schema.v1.js +155 -0
- package/dist/instruction-set-provenance.d.ts +26 -0
- package/dist/instruction-set-provenance.d.ts.map +1 -0
- package/dist/instruction-set-provenance.js +68 -0
- package/dist/instruction-set-sync/helpers.d.ts +22 -0
- package/dist/instruction-set-sync/helpers.d.ts.map +1 -0
- package/dist/instruction-set-sync/helpers.js +677 -0
- package/dist/instruction-set-sync/index.d.ts +60 -0
- package/dist/instruction-set-sync/index.d.ts.map +1 -0
- package/dist/instruction-set-sync/index.js +689 -0
- package/dist/instruction-set-update/index.d.ts +42 -0
- package/dist/instruction-set-update/index.d.ts.map +1 -0
- package/dist/instruction-set-update/index.js +445 -0
- package/dist/instruction-set-verify/index.d.ts +27 -0
- package/dist/instruction-set-verify/index.d.ts.map +1 -0
- package/dist/instruction-set-verify/index.js +865 -0
- package/dist/instruction-sets-cli.d.ts +1 -4
- package/dist/instruction-sets-cli.d.ts.map +1 -1
- package/dist/instruction-sets-cli.js +220 -109
- package/dist/model-config-fields.d.ts +35 -0
- package/dist/model-config-fields.d.ts.map +1 -0
- package/dist/model-config-fields.js +28 -0
- package/dist/optimizations-list-cli.d.ts.map +1 -1
- package/dist/optimizations-list-cli.js +7 -4
- package/dist/score-format.d.ts +2 -0
- package/dist/score-format.d.ts.map +1 -0
- package/dist/score-format.js +18 -0
- package/dist/workbench-repo-contract.d.ts +65 -0
- package/dist/workbench-repo-contract.d.ts.map +1 -0
- package/dist/workbench-repo-contract.js +250 -0
- package/dist/workspace-bootstrap-job.d.ts +45 -0
- package/dist/workspace-bootstrap-job.d.ts.map +1 -0
- package/dist/workspace-bootstrap-job.js +431 -0
- package/dist/workspace-bootstrap-seed.d.ts +74 -0
- package/dist/workspace-bootstrap-seed.d.ts.map +1 -0
- package/dist/workspace-bootstrap-seed.js +209 -0
- package/package.json +4 -3
- package/src/artifacts-git-runtime.ts +360 -0
- package/src/cloudflare-artifacts-git-remote.ts +24 -0
- package/src/dataset-transfer-download.ts +61 -0
- package/src/dataset-transfer-policy.ts +8 -0
- package/src/dataset-transfer-upload.ts +145 -0
- package/src/git-credential.ts +1 -2
- package/src/git-recovery-environment.ts +1 -1
- package/src/help.ts +44 -11
- package/src/hosted-boot.ts +644 -109
- package/src/hosted-bootstrap.ts +14 -3
- package/src/hosted-harness-opencode.ts +130 -28
- package/src/hosted-harness.ts +4 -1
- package/src/hosted-harvest.ts +258 -33
- package/src/hosted-loop-lifecycle.ts +19 -5
- package/src/hosted-loop.ts +340 -68
- package/src/hosted-question-limits.ts +6 -0
- package/src/hosted-question.ts +145 -0
- package/src/hosted-run-event-sink.ts +24 -12
- package/src/hosted-runtime-assets.ts +51 -11
- package/src/hosted-session-cli.ts +10 -24
- package/src/http.ts +10 -4
- package/src/index.ts +108 -134
- package/src/instruction-set-gepa-launch.ts +4 -4
- package/src/instruction-set-loader.ts +53 -2
- package/src/instruction-set-lock/index.ts +561 -0
- package/src/instruction-set-lock/schema.v1.json +141 -0
- package/src/instruction-set-lock/schema.v1.ts +155 -0
- package/src/instruction-set-provenance.ts +89 -0
- package/src/instruction-set-sync/helpers.ts +707 -0
- package/src/instruction-set-sync/index.ts +791 -0
- package/src/instruction-set-update/index.ts +488 -0
- package/src/instruction-set-verify/index.ts +1067 -0
- package/src/instruction-sets-cli.ts +226 -114
- package/src/model-config-fields.ts +31 -0
- package/src/optimizations-list-cli.ts +7 -4
- package/src/score-format.ts +16 -0
- package/src/workbench-repo-contract.ts +314 -0
- package/src/workspace-bootstrap-job.ts +611 -0
- package/src/workspace-bootstrap-seed.ts +350 -0
- package/vendor/orizu-gepa/src/orizu_gepa_connector/instruction_set_loader.py +46 -2
- package/vendor/skills/orizu/SKILL.md +4 -2
- package/vendor/skills/orizu/flows/promote.md +4 -0
- package/vendor/skills/orizu/references/assess-and-plan.md +1 -1
- package/vendor/skills/orizu/references/authority-map.md +1 -1
- package/vendor/skills/orizu/references/cli-reference.md +151 -35
- package/vendor/skills/orizu/references/instructions-after-prompts.md +3 -3
- package/vendor/skills/orizu/references/migrate-existing-gepa-setup.md +5 -6
- package/vendor/skills/orizu/references/migrate-hand-rolled-instruction-layout.md +252 -0
- package/vendor/skills/orizu/references/optimization-reports.md +5 -4
- package/vendor/skills/orizu/references/optimization-with-gepa.md +1 -1
- package/vendor/skills/orizu/references/orizu-in-your-codebase.md +195 -0
- package/vendor/skills/orizu/references/primer.md +4 -2
- package/vendor/skills/orizu/references/prompt-control-plane.md +33 -29
- package/vendor/skills/orizu/references/vocabulary.md +1 -1
package/dist/hosted-boot.js
CHANGED
|
@@ -30,10 +30,12 @@
|
|
|
30
30
|
* non-secret ANTHROPIC_API_KEY dummy placeholder (firewall brokers the real key).
|
|
31
31
|
*/
|
|
32
32
|
import { spawnSync } from 'child_process';
|
|
33
|
-
import { closeSync, mkdirSync, openSync, readFileSync, renameSync, writeSync } from 'fs';
|
|
33
|
+
import { closeSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, unlinkSync, writeSync } from 'fs';
|
|
34
|
+
import { isValidCloudflareArtifactsGitRemote } from './cloudflare-artifacts-git-remote.js';
|
|
34
35
|
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 { INJECTED_ENV_VARS_ENV, closeHostedLoopSession, runHostedLoop, runHostedLoopTurn, startHostedLoopSession, } from './hosted-loop.js';
|
|
36
37
|
import { DEFAULT_EGRESS_CANARY_HOST, DEFAULT_HOSTED_MODEL } from './hosted-loop-lifecycle.js';
|
|
38
|
+
import { composeHostedAnswerPrompt } from './hosted-question.js';
|
|
37
39
|
import { resumeRunEventSink } from './hosted-run-event-sink.js';
|
|
38
40
|
import { stageOrizuSkill } from './hosted-skill-staging.js';
|
|
39
41
|
/** Non-secret placeholder model key (firewall brokers the real org key on
|
|
@@ -42,8 +44,6 @@ import { stageOrizuSkill } from './hosted-skill-staging.js';
|
|
|
42
44
|
* DO-path `workers/session-coordinator/src/bootstrap.ts`); kept in sync by
|
|
43
45
|
* grep. Inlined (not imported) to avoid a hosted-session-cli import cycle. */
|
|
44
46
|
const ANTHROPIC_DUMMY_KEY = 'sk-ant-orizu-proxy-broker-placeholder';
|
|
45
|
-
const CLOUDFLARE_ARTIFACTS_HOST = /^[0-9a-f]{32}\.artifacts\.cloudflare\.net$/;
|
|
46
|
-
const CLOUDFLARE_ARTIFACTS_GIT_PATH = /^\/git\/[A-Za-z0-9][A-Za-z0-9._-]{0,255}\/[A-Za-z0-9][A-Za-z0-9._-]{0,255}\.git$/;
|
|
47
47
|
// -- Frozen env contract ------------------------------------------------------
|
|
48
48
|
export const REQUIRED_BOOT_ENV_VARS = [
|
|
49
49
|
'ORIZU_BOOT_SECRET',
|
|
@@ -100,6 +100,17 @@ export function deriveBootStatusUrl(agentTokenUrl) {
|
|
|
100
100
|
* is never a place for a secret; this bounds it anyway, and we scrub the boot
|
|
101
101
|
* secret defensively before sending. */
|
|
102
102
|
const MAX_BOOT_REASON_CHARS = 800;
|
|
103
|
+
const TURN_STATUS_ATTEMPT_TIMEOUT_MS = 10_000;
|
|
104
|
+
function resolveTestPositiveInteger(processEnv, name, maximum = Number.MAX_SAFE_INTEGER) {
|
|
105
|
+
const configured = processEnv.NODE_ENV === 'test' ? processEnv[name] : undefined;
|
|
106
|
+
if (!configured || !/^\d+$/u.test(configured))
|
|
107
|
+
return undefined;
|
|
108
|
+
const value = Number(configured);
|
|
109
|
+
return value > 0 && value <= maximum ? value : undefined;
|
|
110
|
+
}
|
|
111
|
+
function resolveTurnStatusAttemptTimeoutMs(processEnv) {
|
|
112
|
+
return resolveTestPositiveInteger(processEnv, 'ORIZU_HOSTED_TEST_TURN_STATUS_TIMEOUT_MS');
|
|
113
|
+
}
|
|
103
114
|
/** Best-effort scrub + truncate for a reported failure reason: never leak the
|
|
104
115
|
* boot secret (the one credential the boot always holds), and keep it short. */
|
|
105
116
|
export function redactBootReason(reason, bootSecret) {
|
|
@@ -132,6 +143,55 @@ export async function postBootStatus(opts) {
|
|
|
132
143
|
opts.log?.(`boot-status ${opts.status} report failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
133
144
|
}
|
|
134
145
|
}
|
|
146
|
+
async function postRequiredIdleReady(opts) {
|
|
147
|
+
let detail = 'not attempted';
|
|
148
|
+
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
149
|
+
try {
|
|
150
|
+
const response = await opts.fetchImpl(opts.bootStatusUrl, {
|
|
151
|
+
method: 'POST',
|
|
152
|
+
headers: { Authorization: `Bearer ${opts.bootSecret}`, 'Content-Type': 'application/json' },
|
|
153
|
+
body: JSON.stringify({ status: 'ready' }),
|
|
154
|
+
signal: AbortSignal.timeout(opts.attemptTimeoutMs ?? TURN_STATUS_ATTEMPT_TIMEOUT_MS),
|
|
155
|
+
});
|
|
156
|
+
if (response.ok)
|
|
157
|
+
return;
|
|
158
|
+
detail = `status ${response.status}`;
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
detail = error instanceof Error ? error.message : String(error);
|
|
162
|
+
}
|
|
163
|
+
if (attempt < 4)
|
|
164
|
+
await opts.sleep(250 * 2 ** attempt);
|
|
165
|
+
}
|
|
166
|
+
throw new Error(`ready acknowledgement failed: ${detail}`);
|
|
167
|
+
}
|
|
168
|
+
async function postTurnStatus(opts) {
|
|
169
|
+
const sleep = opts.sleep ?? ((ms) => new Promise(resolve => setTimeout(resolve, ms)));
|
|
170
|
+
let detail = 'not attempted';
|
|
171
|
+
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
172
|
+
try {
|
|
173
|
+
const response = await opts.fetchImpl(opts.bootStatusUrl, {
|
|
174
|
+
method: 'POST',
|
|
175
|
+
headers: { Authorization: `Bearer ${opts.bootSecret}`, 'Content-Type': 'application/json' },
|
|
176
|
+
body: JSON.stringify({
|
|
177
|
+
status: opts.status,
|
|
178
|
+
runId: opts.runId,
|
|
179
|
+
...(opts.reason ? { reason: opts.reason } : {}),
|
|
180
|
+
}),
|
|
181
|
+
signal: AbortSignal.timeout(opts.attemptTimeoutMs ?? TURN_STATUS_ATTEMPT_TIMEOUT_MS),
|
|
182
|
+
});
|
|
183
|
+
if (response.ok)
|
|
184
|
+
return;
|
|
185
|
+
detail = `status ${response.status}`;
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
detail = error instanceof Error ? error.message : String(error);
|
|
189
|
+
}
|
|
190
|
+
if (attempt < 4)
|
|
191
|
+
await sleep(250 * 2 ** attempt);
|
|
192
|
+
}
|
|
193
|
+
throw new Error(`${opts.status} acknowledgement failed: ${detail}`);
|
|
194
|
+
}
|
|
135
195
|
/**
|
|
136
196
|
* Exchange the boot secret for a fresh Orizu agent bearer at the coordinator's
|
|
137
197
|
* agent-token route (`GET`, `Authorization: Bearer <boot secret>` → 200
|
|
@@ -209,6 +269,9 @@ export async function fetchEnvBundle(opts) {
|
|
|
209
269
|
* values from run events. Returns the merged redaction-var list.
|
|
210
270
|
*/
|
|
211
271
|
export function applyEnvBundle(bundle, env) {
|
|
272
|
+
if (bundle.connectors.some(connector => connector.envVar.startsWith('ORIZU_'))) {
|
|
273
|
+
throw new Error('reserved_connector_env: connector env vars must not use ORIZU_*');
|
|
274
|
+
}
|
|
212
275
|
for (const connector of bundle.connectors) {
|
|
213
276
|
env[connector.envVar] = connector.value;
|
|
214
277
|
}
|
|
@@ -225,6 +288,11 @@ export function applyEnvBundle(bundle, env) {
|
|
|
225
288
|
env[INJECTED_ENV_VARS_ENV] = merged.join(',');
|
|
226
289
|
return merged;
|
|
227
290
|
}
|
|
291
|
+
function promptForPendingHostedTurn(turn) {
|
|
292
|
+
return turn.answerToQuestion
|
|
293
|
+
? composeHostedAnswerPrompt(turn.answerToQuestion, turn.body)
|
|
294
|
+
: turn.body;
|
|
295
|
+
}
|
|
228
296
|
async function bearerJson(fetchImpl, url, bearer, init = {}) {
|
|
229
297
|
const res = await fetchImpl(url, {
|
|
230
298
|
...init,
|
|
@@ -244,7 +312,7 @@ function asPositiveNumber(value) {
|
|
|
244
312
|
/** GET the session (agent-capable, RLS-scoped) to learn its workspace, branch,
|
|
245
313
|
* task, and any existing run — the DO start body carries none of these. */
|
|
246
314
|
export async function resolveSession(opts) {
|
|
247
|
-
const body = await bearerJson(opts.fetchImpl, `${opts.baseUrl}/api/cli/sessions/${encodeURIComponent(opts.sessionId)}`, opts.bearer);
|
|
315
|
+
const body = await bearerJson(opts.fetchImpl, `${opts.baseUrl}/api/cli/sessions/${encodeURIComponent(opts.sessionId)}`, opts.bearer, opts.signal ? { signal: opts.signal } : {});
|
|
248
316
|
const session = (body.session ?? {});
|
|
249
317
|
const workspaceId = asString(session.workspaceId);
|
|
250
318
|
if (!workspaceId)
|
|
@@ -253,21 +321,124 @@ export async function resolveSession(opts) {
|
|
|
253
321
|
if (!repoBranch)
|
|
254
322
|
throw new Error('session response carried no repoBranch (branch not provisioned)');
|
|
255
323
|
const clientInfo = (session.clientInfo ?? {});
|
|
256
|
-
const
|
|
257
|
-
if (!
|
|
324
|
+
const initialTask = asString(clientInfo.task);
|
|
325
|
+
if (!initialTask)
|
|
258
326
|
throw new Error('session client_info carried no task prompt');
|
|
259
327
|
const runs = Array.isArray(session.runs) ? session.runs : [];
|
|
260
|
-
const
|
|
328
|
+
const pending = session.pendingTurn && typeof session.pendingTurn === 'object'
|
|
329
|
+
? session.pendingTurn : null;
|
|
330
|
+
const answer = pending?.answerToQuestion && typeof pending.answerToQuestion === 'object'
|
|
331
|
+
? pending.answerToQuestion
|
|
332
|
+
: null;
|
|
333
|
+
const answerToQuestion = answer && asString(answer.questionId) && asString(answer.question)
|
|
334
|
+
? { questionId: answer.questionId, question: answer.question }
|
|
335
|
+
: null;
|
|
336
|
+
const hasValidTurnId = pending?.turnId === null || asString(pending?.turnId) !== null;
|
|
337
|
+
const pendingTurn = pending && hasValidTurnId &&
|
|
338
|
+
Number.isSafeInteger(pending.ordinal) && pending.ordinal > 0 &&
|
|
339
|
+
asString(pending.runId) && asString(pending.body) && asString(pending.clientMessageId) &&
|
|
340
|
+
(pending.answerToQuestion === null || answerToQuestion)
|
|
341
|
+
? {
|
|
342
|
+
turnId: pending.turnId === null ? null : pending.turnId,
|
|
343
|
+
ordinal: pending.ordinal,
|
|
344
|
+
runId: pending.runId,
|
|
345
|
+
body: pending.body,
|
|
346
|
+
clientMessageId: pending.clientMessageId,
|
|
347
|
+
answerToQuestion,
|
|
348
|
+
}
|
|
349
|
+
: null;
|
|
350
|
+
if (pending && !pendingTurn)
|
|
351
|
+
throw new Error('hosted_pending_turn_invalid');
|
|
352
|
+
const unfinishedRun = pendingTurn
|
|
353
|
+
? runs.find(run => asString(run.id) === pendingTurn.runId &&
|
|
354
|
+
(run.status === 'pending' || run.status === 'running'))
|
|
355
|
+
: null;
|
|
356
|
+
// The route orders runs newest-first. Resume the service-selected lowest
|
|
357
|
+
// unfinished turn; only when none exists do we fall back to the initial
|
|
358
|
+
// (oldest) run and its original task.
|
|
359
|
+
const selectedRun = unfinishedRun ?? runs.at(-1);
|
|
360
|
+
const runId = asString(selectedRun?.id);
|
|
261
361
|
return {
|
|
262
362
|
workspaceId,
|
|
263
363
|
repoBranch,
|
|
264
|
-
task,
|
|
364
|
+
task: unfinishedRun ? promptForPendingHostedTurn(pendingTurn) : initialTask,
|
|
265
365
|
model: asString(clientInfo.model),
|
|
266
366
|
reasoningEffort: asString(clientInfo.reasoningEffort),
|
|
367
|
+
surface: asString(clientInfo.surface),
|
|
267
368
|
durationMinutes: asPositiveNumber(clientInfo.durationMinutes),
|
|
268
369
|
runId,
|
|
370
|
+
runStatus: asString(selectedRun?.status),
|
|
371
|
+
status: asString(session.status),
|
|
372
|
+
pendingTurn: unfinishedRun ? pendingTurn : null,
|
|
373
|
+
interruptRequestedRunId: asString(session.interruptRequestedRunId),
|
|
374
|
+
agentSessionId: asString(session.agentSessionId),
|
|
375
|
+
projectId: asString(session.projectId),
|
|
269
376
|
};
|
|
270
377
|
}
|
|
378
|
+
/** Ride the ALI-1757 session/pending-turn poll while one prompt is active.
|
|
379
|
+
* This uses the same additive session GET contract, but deliberately shares
|
|
380
|
+
* none of the between-turn credential cleanup or mint-on-401 behavior: the
|
|
381
|
+
* session-stable bearer file is live ORIZU_TOKEN_FILE for the prompt. */
|
|
382
|
+
export async function pollHostedSessionDuringTurn(opts) {
|
|
383
|
+
const diagnose = (message) => {
|
|
384
|
+
try {
|
|
385
|
+
opts.onDiagnostic?.(message);
|
|
386
|
+
}
|
|
387
|
+
catch { /* diagnostics are best-effort */ }
|
|
388
|
+
};
|
|
389
|
+
const readFile = opts.readFile ?? ((path) => readFileSync(path, 'utf8'));
|
|
390
|
+
const sleep = opts.sleep ?? ((ms) => new Promise(resolve => {
|
|
391
|
+
if (opts.signal.aborted) {
|
|
392
|
+
resolve();
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
const timer = setTimeout(finish, ms);
|
|
396
|
+
const handleAbort = () => finish();
|
|
397
|
+
function finish() {
|
|
398
|
+
clearTimeout(timer);
|
|
399
|
+
opts.signal.removeEventListener('abort', handleAbort);
|
|
400
|
+
resolve();
|
|
401
|
+
}
|
|
402
|
+
opts.signal.addEventListener('abort', handleAbort, { once: true });
|
|
403
|
+
}));
|
|
404
|
+
while (!opts.signal.aborted) {
|
|
405
|
+
try {
|
|
406
|
+
const latest = await resolveSession({
|
|
407
|
+
baseUrl: opts.baseUrl,
|
|
408
|
+
sessionId: opts.sessionId,
|
|
409
|
+
bearer: readFile(opts.bearerFileAbs).trim(),
|
|
410
|
+
signal: AbortSignal.any([
|
|
411
|
+
opts.signal,
|
|
412
|
+
AbortSignal.timeout(10_000),
|
|
413
|
+
]),
|
|
414
|
+
fetchImpl: (url, init) => {
|
|
415
|
+
const headers = new Headers(init?.headers);
|
|
416
|
+
// Additive, authority-free discriminator: existing session GET
|
|
417
|
+
// remains the one endpoint, while fixtures/observability can separate
|
|
418
|
+
// prompt-time health reads from between-turn queue consumption.
|
|
419
|
+
headers.set('x-orizu-hosted-poll', 'during-turn');
|
|
420
|
+
return opts.fetchImpl(url, { ...init, headers });
|
|
421
|
+
},
|
|
422
|
+
});
|
|
423
|
+
if (latest.interruptRequestedRunId === opts.runId) {
|
|
424
|
+
try {
|
|
425
|
+
opts.onInterrupt();
|
|
426
|
+
}
|
|
427
|
+
catch {
|
|
428
|
+
diagnose('hosted interrupt callback failed; request will not be redelivered');
|
|
429
|
+
}
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
catch {
|
|
434
|
+
// Fixed, value-free diagnostic: bearer values and response bodies never
|
|
435
|
+
// enter logs. A transient read failure cannot fail the running turn.
|
|
436
|
+
diagnose('hosted interrupt poll failed; retrying');
|
|
437
|
+
}
|
|
438
|
+
if (!opts.signal.aborted)
|
|
439
|
+
await sleep(5_000);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
271
442
|
/** Reuse an existing run or start one with the AGENT bearer (actor_type='agent'),
|
|
272
443
|
* exactly like the operator path's run-start step. */
|
|
273
444
|
export async function ensureRun(opts) {
|
|
@@ -287,6 +458,29 @@ export async function ensureRun(opts) {
|
|
|
287
458
|
throw new Error('workbench-run start returned no run id');
|
|
288
459
|
return runId;
|
|
289
460
|
}
|
|
461
|
+
/** Emit the RunAPI-reserved run_started transition for a pre-created turn.
|
|
462
|
+
* Idempotent for the initial run, which may already be running. */
|
|
463
|
+
export async function beginRunExecution(opts) {
|
|
464
|
+
const sleep = opts.sleep ?? ((ms) => new Promise(resolve => setTimeout(resolve, ms)));
|
|
465
|
+
let detail = 'not attempted';
|
|
466
|
+
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
467
|
+
try {
|
|
468
|
+
await bearerJson(opts.fetchImpl, `${opts.baseUrl}/api/cli/workbench-runs/${encodeURIComponent(opts.runId)}`, opts.bearer, {
|
|
469
|
+
method: 'POST',
|
|
470
|
+
headers: { 'Content-Type': 'application/json' },
|
|
471
|
+
body: '{}',
|
|
472
|
+
signal: AbortSignal.timeout(opts.attemptTimeoutMs ?? TURN_STATUS_ATTEMPT_TIMEOUT_MS),
|
|
473
|
+
});
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
catch (error) {
|
|
477
|
+
detail = error instanceof Error ? error.message : String(error);
|
|
478
|
+
}
|
|
479
|
+
if (attempt < 4)
|
|
480
|
+
await sleep(250 * 2 ** attempt);
|
|
481
|
+
}
|
|
482
|
+
throw new Error(`run_started transition failed: ${detail}`);
|
|
483
|
+
}
|
|
290
484
|
/** Mint a session_read repo token to learn the repo full name, then build the
|
|
291
485
|
* GitHub clone URL (mirrors the operator path's `defaultResolveRepo`). */
|
|
292
486
|
export async function resolveRepo(opts) {
|
|
@@ -302,22 +496,7 @@ export async function resolveRepo(opts) {
|
|
|
302
496
|
if (!artifactsRemote) {
|
|
303
497
|
throw new Error('repo-token response carried no Artifacts remote');
|
|
304
498
|
}
|
|
305
|
-
|
|
306
|
-
try {
|
|
307
|
-
parsed = new URL(artifactsRemote);
|
|
308
|
-
}
|
|
309
|
-
catch {
|
|
310
|
-
throw new Error('repo-token response carried an invalid Artifacts remote');
|
|
311
|
-
}
|
|
312
|
-
if (parsed.protocol !== 'https:' ||
|
|
313
|
-
parsed.username ||
|
|
314
|
-
parsed.password ||
|
|
315
|
-
parsed.port ||
|
|
316
|
-
parsed.search ||
|
|
317
|
-
parsed.hash ||
|
|
318
|
-
!CLOUDFLARE_ARTIFACTS_HOST.test(parsed.hostname) ||
|
|
319
|
-
!CLOUDFLARE_ARTIFACTS_GIT_PATH.test(parsed.pathname) ||
|
|
320
|
-
parsed.toString() !== artifactsRemote) {
|
|
499
|
+
if (!isValidCloudflareArtifactsGitRemote(artifactsRemote)) {
|
|
321
500
|
throw new Error('repo-token response carried an invalid Artifacts remote');
|
|
322
501
|
}
|
|
323
502
|
return {
|
|
@@ -371,6 +550,8 @@ export function assertSafeRunId(runId) {
|
|
|
371
550
|
function writeSecretFile(path, contents, write) {
|
|
372
551
|
write(path, contents.endsWith('\n') ? contents : `${contents}\n`);
|
|
373
552
|
}
|
|
553
|
+
class HostedTurnFailure extends Error {
|
|
554
|
+
}
|
|
374
555
|
/**
|
|
375
556
|
* The full in-sandbox boot: validate → pull bearer → pull+apply env bundle →
|
|
376
557
|
* resolve session/run/repo → write the pull-mode credential assets → clone the
|
|
@@ -403,6 +584,16 @@ export async function runHostedBoot(opts) {
|
|
|
403
584
|
const readFile = opts.readFile ?? ((path) => readFileSync(path, 'utf8'));
|
|
404
585
|
const log = opts.log ?? (() => { });
|
|
405
586
|
const now = opts.now ?? (() => Date.now());
|
|
587
|
+
const sleep = opts.sleep ?? ((ms) => new Promise(resolve => setTimeout(resolve, ms)));
|
|
588
|
+
const testRotationMinimum = processEnv.NODE_ENV === 'test'
|
|
589
|
+
? processEnv.ORIZU_HOSTED_TEST_BEARER_ROTATION_MIN_DELAY_MS
|
|
590
|
+
: undefined;
|
|
591
|
+
const bearerRotationMinimumDelayMs = testRotationMinimum && /^\d+$/u.test(testRotationMinimum)
|
|
592
|
+
? Number(testRotationMinimum)
|
|
593
|
+
: undefined;
|
|
594
|
+
const turnStatusAttemptTimeoutMs = resolveTurnStatusAttemptTimeoutMs(processEnv);
|
|
595
|
+
const idlePollMs = resolveTestPositiveInteger(processEnv, 'ORIZU_HOSTED_TEST_IDLE_POLL_MS') ?? 5_000;
|
|
596
|
+
const idleReadBackoffCapMs = resolveTestPositiveInteger(processEnv, 'ORIZU_HOSTED_TEST_IDLE_READ_BACKOFF_CAP_MS') ?? 45_000;
|
|
406
597
|
// 0 — ENV HYGIENE (ALI-1062): the boot secret is the DO path's only durable
|
|
407
598
|
// credential (it mints agent bearers at the internet-reachable agent-token
|
|
408
599
|
// route for up to 24h), and `env.bootSecret` is already captured — so scrub
|
|
@@ -413,8 +604,10 @@ export async function runHostedBoot(opts) {
|
|
|
413
604
|
// boot-status closures hold `env.bootSecret`, and the git credential helper
|
|
414
605
|
// reads the 0600 run-dir boot-secret FILE (written in step 4).
|
|
415
606
|
delete processEnv.ORIZU_BOOT_SECRET;
|
|
607
|
+
if (env.runId)
|
|
608
|
+
assertSafeRunId(env.runId);
|
|
416
609
|
// 1 — Pull the agent bearer (retry/backoff — the DO may still be arming).
|
|
417
|
-
|
|
610
|
+
let bearer = await pullAgentBearer({
|
|
418
611
|
agentTokenUrl: env.agentTokenUrl,
|
|
419
612
|
bootSecret: env.bootSecret,
|
|
420
613
|
fetchImpl,
|
|
@@ -429,14 +622,87 @@ export async function runHostedBoot(opts) {
|
|
|
429
622
|
const redacted = applyEnvBundle(bundle, processEnv);
|
|
430
623
|
log(`env bundle applied (${bundle.connectors.length} connectors, ${redacted.length} redacted vars)`);
|
|
431
624
|
// 3 — Resolve the session (workspace/branch/task) + run + repo from the plane.
|
|
432
|
-
|
|
625
|
+
let session = await resolveSession({ baseUrl: env.baseUrl, sessionId: env.sessionId, bearer: bearer.token, fetchImpl });
|
|
626
|
+
let currentNonTerminalRunId = session.runStatus === 'pending' || session.runStatus === 'running'
|
|
627
|
+
? session.runId
|
|
628
|
+
: null;
|
|
629
|
+
// Production restarts with no live run are between turns. Prove this boot is
|
|
630
|
+
// live before waiting so the coordinator's readiness timeout cannot reap an
|
|
631
|
+
// intentionally idle sandbox before the longer hosted idle policy applies.
|
|
632
|
+
if (!opts.runLoop && !currentNonTerminalRunId) {
|
|
633
|
+
await postRequiredIdleReady({
|
|
634
|
+
bootStatusUrl: env.bootStatusUrl,
|
|
635
|
+
bootSecret: env.bootSecret,
|
|
636
|
+
fetchImpl,
|
|
637
|
+
sleep,
|
|
638
|
+
attemptTimeoutMs: turnStatusAttemptTimeoutMs,
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
let consecutiveIdleSessionReadFailures = 0;
|
|
642
|
+
const recordIdleSessionReadFailure = (detail) => {
|
|
643
|
+
consecutiveIdleSessionReadFailures += 1;
|
|
644
|
+
if (consecutiveIdleSessionReadFailures >= 10) {
|
|
645
|
+
throw new Error(`idle session read failed after 10 attempts: ${detail}`);
|
|
646
|
+
}
|
|
647
|
+
log(`idle session resolution failed; retrying (${detail})`);
|
|
648
|
+
};
|
|
649
|
+
while (!opts.runLoop && !currentNonTerminalRunId) {
|
|
650
|
+
await sleep(Math.min(idlePollMs * 2 ** consecutiveIdleSessionReadFailures, idleReadBackoffCapMs));
|
|
651
|
+
try {
|
|
652
|
+
if (bearer.expiresAtMs !== null && bearer.expiresAtMs <= now() + 60_000) {
|
|
653
|
+
bearer = await pullAgentBearer({
|
|
654
|
+
agentTokenUrl: env.agentTokenUrl, bootSecret: env.bootSecret,
|
|
655
|
+
fetchImpl, attempts: 3, backoffMs: 250, sleep, log,
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
session = await resolveSession({
|
|
659
|
+
baseUrl: env.baseUrl, sessionId: env.sessionId, bearer: bearer.token, fetchImpl,
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
catch (error) {
|
|
663
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
664
|
+
if (detail.startsWith('agent-token pull failed after')) {
|
|
665
|
+
throw new Error(`idle agent-token refresh failed: ${detail}`);
|
|
666
|
+
}
|
|
667
|
+
if (!detail.includes('(401)')) {
|
|
668
|
+
recordIdleSessionReadFailure(detail);
|
|
669
|
+
continue;
|
|
670
|
+
}
|
|
671
|
+
try {
|
|
672
|
+
bearer = await pullAgentBearer({
|
|
673
|
+
agentTokenUrl: env.agentTokenUrl, bootSecret: env.bootSecret,
|
|
674
|
+
fetchImpl, attempts: 3, backoffMs: 250, sleep, log,
|
|
675
|
+
});
|
|
676
|
+
session = await resolveSession({
|
|
677
|
+
baseUrl: env.baseUrl, sessionId: env.sessionId, bearer: bearer.token, fetchImpl,
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
catch (retryError) {
|
|
681
|
+
const detail = retryError instanceof Error ? retryError.message : String(retryError);
|
|
682
|
+
if (detail.startsWith('agent-token pull failed after')) {
|
|
683
|
+
throw new Error(`idle agent-token refresh failed: ${detail}`);
|
|
684
|
+
}
|
|
685
|
+
recordIdleSessionReadFailure(detail);
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
consecutiveIdleSessionReadFailures = 0;
|
|
690
|
+
if (session.status && session.status !== 'active') {
|
|
691
|
+
return { ok: true, runId: null, loopStatus: null, error: null };
|
|
692
|
+
}
|
|
693
|
+
currentNonTerminalRunId = session.runStatus === 'pending' || session.runStatus === 'running'
|
|
694
|
+
? session.runId
|
|
695
|
+
: null;
|
|
696
|
+
}
|
|
433
697
|
const workspaceId = env.workspaceId ?? session.workspaceId;
|
|
434
|
-
|
|
698
|
+
// A non-null production run came from current session authority. Only the
|
|
699
|
+
// injected one-shot test seam may create a missing run.
|
|
700
|
+
const runId = currentNonTerminalRunId ?? await ensureRun({
|
|
435
701
|
baseUrl: env.baseUrl,
|
|
436
702
|
sessionId: env.sessionId,
|
|
437
703
|
bearer: bearer.token,
|
|
438
704
|
fetchImpl,
|
|
439
|
-
existingRunId:
|
|
705
|
+
existingRunId: null,
|
|
440
706
|
});
|
|
441
707
|
// Path-safety BEFORE the run id reaches any `.orizu-run/${runId}` path
|
|
442
708
|
// (ALI-1060): a caller-supplied ORIZU_RUN_ID must not escape the run dir.
|
|
@@ -452,20 +718,25 @@ export async function runHostedBoot(opts) {
|
|
|
452
718
|
// loop's event sink; the boot secret feeds the credential helper's per-op pull.
|
|
453
719
|
const runDirRel = `.orizu-run/${runId}`;
|
|
454
720
|
const runDirAbs = `${root}/${runDirRel}`;
|
|
721
|
+
const sessionDirAbs = `${root}/.orizu-session/${env.sessionId}`;
|
|
455
722
|
const workspaceDir = `${root}/repo`;
|
|
456
723
|
assertSafeGitValue('sessionBranch', session.repoBranch);
|
|
457
724
|
assertSafeGitValue('cloneUrl', repo.cloneUrl);
|
|
458
725
|
mkdirp(runDirAbs);
|
|
459
|
-
|
|
460
|
-
const
|
|
461
|
-
const
|
|
462
|
-
const
|
|
463
|
-
const
|
|
726
|
+
mkdirp(sessionDirAbs);
|
|
727
|
+
const bootSecretFileAbs = `${sessionDirAbs}/boot-secret`;
|
|
728
|
+
const bearerFileAbs = `${sessionDirAbs}/${BEARER_BASENAME}`;
|
|
729
|
+
const helperScriptAbs = `${sessionDirAbs}/${HELPER_SCRIPT_BASENAME}`;
|
|
730
|
+
const bootContextAbs = `${sessionDirAbs}/${BOOT_CONTEXT_BASENAME}`;
|
|
731
|
+
const cacheFileAbs = `${sessionDirAbs}/${REPO_CRED_CACHE_BASENAME}`;
|
|
464
732
|
const taskFileAbs = `${runDirAbs}/task.txt`;
|
|
465
733
|
writeSecretFile(bootSecretFileAbs, env.bootSecret, writeFile);
|
|
466
734
|
writeSecretFile(bearerFileAbs, bearer.token, writeFile);
|
|
467
735
|
writeFile(taskFileAbs, session.task);
|
|
468
736
|
writeFile(helperScriptAbs, renderCredentialHelperScript());
|
|
737
|
+
// Keep the session-stable cache inode available across turns without ever
|
|
738
|
+
// persisting an Artifacts credential (the helper deliberately leaves it empty).
|
|
739
|
+
writeSecretFile(cacheFileAbs, '', writeFile);
|
|
469
740
|
const bootContext = {
|
|
470
741
|
apiBaseUrl: env.baseUrl,
|
|
471
742
|
workspaceId,
|
|
@@ -545,15 +816,26 @@ export async function runHostedBoot(opts) {
|
|
|
545
816
|
// sandbox always runs the pre-baked image under an enforced egress policy, so
|
|
546
817
|
// the loop's startup egress canary + deferred setup hook are both armed.
|
|
547
818
|
const model = session.model ?? DEFAULT_HOSTED_MODEL;
|
|
819
|
+
const configuredOpenCodePort = resolveTestPositiveInteger(processEnv, 'ORIZU_HOSTED_TEST_OPENCODE_PORT', 65_535) ?? (processEnv.ORIZU_OPENCODE_PORT
|
|
820
|
+
? Number(processEnv.ORIZU_OPENCODE_PORT)
|
|
821
|
+
: undefined);
|
|
822
|
+
if (configuredOpenCodePort !== undefined && (!Number.isInteger(configuredOpenCodePort) || configuredOpenCodePort < 1 || configuredOpenCodePort > 65_535)) {
|
|
823
|
+
throw new Error('ORIZU_OPENCODE_PORT must be a valid TCP port');
|
|
824
|
+
}
|
|
548
825
|
const loopContext = {
|
|
549
826
|
apiBaseUrl: env.baseUrl,
|
|
550
827
|
runId,
|
|
551
828
|
bearerFile: bearerFileAbs,
|
|
552
829
|
taskFile: taskFileAbs,
|
|
553
830
|
workspaceDir,
|
|
831
|
+
sessionBranch: session.repoBranch,
|
|
832
|
+
repositoryRemote: repo.cloneUrl,
|
|
833
|
+
repositoryCredentialHelper: helperValue,
|
|
554
834
|
model,
|
|
555
835
|
reasoningEffort: session.reasoningEffort ?? undefined,
|
|
836
|
+
sessionOrigin: session.surface === 'hosted-web' ? 'hosted-web' : 'cli',
|
|
556
837
|
messageId: `${runId}:task`,
|
|
838
|
+
resumeAgentSessionId: session.agentSessionId ?? undefined,
|
|
557
839
|
author: AGENT_GIT_IDENTITY,
|
|
558
840
|
anthropicDummyKey: env.anthropicDummyKey ?? ANTHROPIC_DUMMY_KEY,
|
|
559
841
|
// Derive the per-prompt max-duration cap from the session duration so a long
|
|
@@ -563,66 +845,227 @@ export async function runHostedBoot(opts) {
|
|
|
563
845
|
prebaked: true,
|
|
564
846
|
egressCanaryHost: DEFAULT_EGRESS_CANARY_HOST,
|
|
565
847
|
runSetupHook: true,
|
|
848
|
+
opencodePort: configuredOpenCodePort,
|
|
566
849
|
};
|
|
567
|
-
const
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
redactSecretsList: input.redactSecretsList,
|
|
584
|
-
}));
|
|
585
|
-
const result = await runLoop({
|
|
586
|
-
context: loopContext,
|
|
587
|
-
taskPrompt: session.task,
|
|
588
|
-
// ALI-1062 exact-value redaction: the boot secret is 64 bare hex chars —
|
|
589
|
-
// no TOKEN_SHAPE_PATTERNS rule can catch it, so it MUST ride the loop's
|
|
590
|
-
// verbatim redaction list or an echoed env dump would land it in
|
|
591
|
-
// workbench_run_events.
|
|
592
|
-
redactSecretsList: [env.bootSecret],
|
|
593
|
-
// Read the bearer FILE fresh per request (review F1): `startBearerRotation`
|
|
594
|
-
// rewrites `bearerFileAbs` before the current bearer expires, so a run
|
|
595
|
-
// exceeding the ~60-min TTL (DO cap is 24h) keeps a valid bearer. A frozen
|
|
596
|
-
// closure over the boot-time token would silently stop recording events at
|
|
597
|
-
// ~TTL. The initial token was written to this file at boot (writeSecretFile
|
|
598
|
-
// above); the write is atomic (F2), so this never reads a torn file.
|
|
599
|
-
bearerProvider: () => readFile(bearerFileAbs).trim(),
|
|
600
|
-
});
|
|
601
|
-
log(`hosted-loop finished: ${result.status}`);
|
|
602
|
-
// ALI-1064: the loop is TERMINAL — the auto-harvest (ALI-1036) and the
|
|
603
|
-
// run's terminal seal already ran INSIDE runHostedLoop, so it is safe to
|
|
604
|
-
// hand the session back. Report `complete` so the DO ends the workspace
|
|
605
|
-
// session server-side and stops/destroys the sandbox NOW instead of
|
|
606
|
-
// extending it until the 24h cap (live 2026-07-15: zombie boxes burned
|
|
607
|
-
// 2×CPU/4GiB for hours after their loops finished). This fires for a
|
|
608
|
-
// failed loop too — the run is terminal either way, and the loop already
|
|
609
|
-
// owns its own run-level terminal state (never a `failed` boot-status
|
|
610
|
-
// here; that would misreport a run failure as a bootstrap failure).
|
|
611
|
-
// Best-effort: the DO's duration/24h caps are the backstop.
|
|
612
|
-
await postBootStatus({
|
|
613
|
-
bootStatusUrl: env.bootStatusUrl,
|
|
614
|
-
bootSecret: env.bootSecret,
|
|
615
|
-
status: 'complete',
|
|
616
|
-
runId,
|
|
617
|
-
reason: result.error ? redactBootReason(result.error, env.bootSecret) : null,
|
|
618
|
-
fetchImpl,
|
|
619
|
-
log,
|
|
850
|
+
const runLoop = opts.runLoop ??
|
|
851
|
+
((input) => runHostedLoop({
|
|
852
|
+
context: input.context,
|
|
853
|
+
taskPrompt: input.taskPrompt,
|
|
854
|
+
bearerProvider: input.bearerProvider,
|
|
855
|
+
redactSecretsList: input.redactSecretsList,
|
|
856
|
+
onDiagnostic: log,
|
|
857
|
+
}));
|
|
858
|
+
// Preserve the injected one-shot seam for existing unit callers. Production
|
|
859
|
+
// uses the durable multi-turn branch below (and therefore has no injected
|
|
860
|
+
// loop or harness boundary).
|
|
861
|
+
if (opts.runLoop) {
|
|
862
|
+
const rotation = startBearerRotation({
|
|
863
|
+
agentTokenUrl: env.agentTokenUrl, bootSecret: env.bootSecret, bearerFileAbs,
|
|
864
|
+
fetchImpl, writeFile, initialExpiresAtMs: bearer.expiresAtMs, now, log,
|
|
865
|
+
minimumDelayMs: bearerRotationMinimumDelayMs,
|
|
620
866
|
});
|
|
621
|
-
|
|
867
|
+
try {
|
|
868
|
+
const result = await runLoop({
|
|
869
|
+
context: loopContext, taskPrompt: session.task,
|
|
870
|
+
redactSecretsList: [env.bootSecret],
|
|
871
|
+
bearerProvider: () => readFile(bearerFileAbs).trim(),
|
|
872
|
+
});
|
|
873
|
+
// The one-shot compatibility path predates turn callbacks, but its
|
|
874
|
+
// terminal reason has the same custody rule: scrub the bare boot secret
|
|
875
|
+
// before it crosses the boot-status boundary.
|
|
876
|
+
await postBootStatus({
|
|
877
|
+
bootStatusUrl: env.bootStatusUrl, bootSecret: env.bootSecret,
|
|
878
|
+
status: 'complete', runId,
|
|
879
|
+
reason: result.error ? redactBootReason(result.error, env.bootSecret) : null,
|
|
880
|
+
fetchImpl, log,
|
|
881
|
+
});
|
|
882
|
+
return { ok: !result.error, runId, loopStatus: result.status, error: result.error };
|
|
883
|
+
}
|
|
884
|
+
finally {
|
|
885
|
+
await rotation.stop();
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
const { runId: _runId, bearerFile: _bearerFile, taskFile: _taskFile, messageId: _messageId, ...sessionLoopBase } = loopContext;
|
|
889
|
+
const sessionContext = {
|
|
890
|
+
...sessionLoopBase,
|
|
891
|
+
sessionId: env.sessionId,
|
|
892
|
+
projectId: session.projectId,
|
|
893
|
+
sessionDir: sessionDirAbs,
|
|
894
|
+
};
|
|
895
|
+
const hostedLoopSession = await startHostedLoopSession(sessionContext);
|
|
896
|
+
let current = session.pendingTurn?.runId === runId
|
|
897
|
+
? { runId, ordinal: session.pendingTurn.ordinal, body: promptForPendingHostedTurn(session.pendingTurn) }
|
|
898
|
+
: { runId, ordinal: 1, body: session.task };
|
|
899
|
+
let currentBearer = bearer;
|
|
900
|
+
let lastResult = { status: 'succeeded', agentSessionId: session.agentSessionId, installOk: true, error: null };
|
|
901
|
+
let hostedTurnFailure = null;
|
|
902
|
+
try {
|
|
903
|
+
for (;;) {
|
|
904
|
+
assertSafeRunId(current.runId);
|
|
905
|
+
const currentRunDir = `${root}/.orizu-run/${current.runId}`;
|
|
906
|
+
const currentTaskFile = `${currentRunDir}/task.txt`;
|
|
907
|
+
mkdirp(currentRunDir);
|
|
908
|
+
writeFile(currentTaskFile, current.body);
|
|
909
|
+
writeSecretFile(bearerFileAbs, currentBearer.token, writeFile);
|
|
910
|
+
// Keep the failure reporter pinned to the turn that is about to execute;
|
|
911
|
+
// the boot-time run id becomes stale as soon as a follow-up is admitted.
|
|
912
|
+
opts.onBootContext?.({ runId: current.runId, bearer: currentBearer.token });
|
|
913
|
+
await postTurnStatus({
|
|
914
|
+
bootStatusUrl: env.bootStatusUrl, bootSecret: env.bootSecret,
|
|
915
|
+
status: 'turn_started', runId: current.runId, fetchImpl, sleep,
|
|
916
|
+
attemptTimeoutMs: turnStatusAttemptTimeoutMs,
|
|
917
|
+
});
|
|
918
|
+
await beginRunExecution({
|
|
919
|
+
baseUrl: env.baseUrl,
|
|
920
|
+
runId: current.runId,
|
|
921
|
+
bearer: currentBearer.token,
|
|
922
|
+
fetchImpl,
|
|
923
|
+
sleep,
|
|
924
|
+
attemptTimeoutMs: turnStatusAttemptTimeoutMs,
|
|
925
|
+
});
|
|
926
|
+
const rotation = startBearerRotation({
|
|
927
|
+
agentTokenUrl: env.agentTokenUrl, bootSecret: env.bootSecret, bearerFileAbs,
|
|
928
|
+
fetchImpl, writeFile, initialExpiresAtMs: currentBearer.expiresAtMs, now, log,
|
|
929
|
+
minimumDelayMs: bearerRotationMinimumDelayMs,
|
|
930
|
+
});
|
|
931
|
+
const promptInterruptController = new AbortController();
|
|
932
|
+
const interruptPollController = new AbortController();
|
|
933
|
+
const interruptPoll = pollHostedSessionDuringTurn({
|
|
934
|
+
baseUrl: env.baseUrl,
|
|
935
|
+
sessionId: env.sessionId,
|
|
936
|
+
runId: current.runId,
|
|
937
|
+
bearerFileAbs,
|
|
938
|
+
fetchImpl,
|
|
939
|
+
signal: interruptPollController.signal,
|
|
940
|
+
onInterrupt: () => promptInterruptController.abort(),
|
|
941
|
+
onDiagnostic: log,
|
|
942
|
+
readFile,
|
|
943
|
+
});
|
|
944
|
+
try {
|
|
945
|
+
lastResult = await runHostedLoopTurn(hostedLoopSession, {
|
|
946
|
+
runId: current.runId,
|
|
947
|
+
ordinal: current.ordinal,
|
|
948
|
+
taskFile: currentTaskFile,
|
|
949
|
+
messageId: `${current.runId}:task`,
|
|
950
|
+
bearerFile: bearerFileAbs,
|
|
951
|
+
}, {
|
|
952
|
+
taskPrompt: current.body,
|
|
953
|
+
redactSecretsList: [env.bootSecret],
|
|
954
|
+
bearerProvider: () => readFile(bearerFileAbs).trim(),
|
|
955
|
+
interruptSignal: promptInterruptController.signal,
|
|
956
|
+
onDiagnostic: log,
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
finally {
|
|
960
|
+
// Stop and join the during-prompt reader BEFORE removing the live
|
|
961
|
+
// ORIZU_TOKEN_FILE. Only the existing between-turn path unlinks it.
|
|
962
|
+
interruptPollController.abort();
|
|
963
|
+
await interruptPoll;
|
|
964
|
+
// Remove the active-turn credential before waiting for an in-flight
|
|
965
|
+
// refresh. The post-await stopped check then prevents its recreation.
|
|
966
|
+
try {
|
|
967
|
+
unlinkSync(bearerFileAbs);
|
|
968
|
+
}
|
|
969
|
+
catch { /* already absent */ }
|
|
970
|
+
await rotation.stop();
|
|
971
|
+
}
|
|
972
|
+
log(`hosted-loop turn ${current.ordinal} finished: ${lastResult.status}`);
|
|
973
|
+
await postTurnStatus({
|
|
974
|
+
bootStatusUrl: env.bootStatusUrl, bootSecret: env.bootSecret,
|
|
975
|
+
status: 'turn_completed', runId: current.runId, fetchImpl, sleep,
|
|
976
|
+
attemptTimeoutMs: turnStatusAttemptTimeoutMs,
|
|
977
|
+
});
|
|
978
|
+
if (lastResult.error) {
|
|
979
|
+
throw new Error(`fatal hosted turn exit: ${lastResult.error}`);
|
|
980
|
+
}
|
|
981
|
+
// One boot writer drains the lowest pending ordinal. Idle status reads
|
|
982
|
+
// reuse the current bearer, staying well below the coordinator's mint
|
|
983
|
+
// budget. Refresh proactively near expiry and once on a 401. Exhausting
|
|
984
|
+
// the bounded agent-token pull is fatal and reported for the current run;
|
|
985
|
+
// other transient status-read failures retain the session and back off.
|
|
986
|
+
for (;;) {
|
|
987
|
+
await sleep(5_000);
|
|
988
|
+
let pendingTurn = null;
|
|
989
|
+
try {
|
|
990
|
+
if (currentBearer.expiresAtMs !== null && currentBearer.expiresAtMs <= now() + 60_000) {
|
|
991
|
+
currentBearer = await pullAgentBearer({
|
|
992
|
+
agentTokenUrl: env.agentTokenUrl, bootSecret: env.bootSecret,
|
|
993
|
+
fetchImpl, attempts: 3, backoffMs: 250, sleep, log,
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
let latest;
|
|
997
|
+
try {
|
|
998
|
+
latest = await resolveSession({
|
|
999
|
+
baseUrl: env.baseUrl, sessionId: env.sessionId,
|
|
1000
|
+
bearer: currentBearer.token, fetchImpl,
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
catch (error) {
|
|
1004
|
+
if (!(error instanceof Error) || !error.message.includes('(401)'))
|
|
1005
|
+
throw error;
|
|
1006
|
+
currentBearer = await pullAgentBearer({
|
|
1007
|
+
agentTokenUrl: env.agentTokenUrl, bootSecret: env.bootSecret,
|
|
1008
|
+
fetchImpl, attempts: 3, backoffMs: 250, sleep, log,
|
|
1009
|
+
});
|
|
1010
|
+
latest = await resolveSession({
|
|
1011
|
+
baseUrl: env.baseUrl, sessionId: env.sessionId,
|
|
1012
|
+
bearer: currentBearer.token, fetchImpl,
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
if (latest.status && latest.status !== 'active') {
|
|
1016
|
+
return { ok: true, runId: current.runId, loopStatus: lastResult.status, error: null };
|
|
1017
|
+
}
|
|
1018
|
+
pendingTurn = latest.pendingTurn;
|
|
1019
|
+
}
|
|
1020
|
+
catch (error) {
|
|
1021
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1022
|
+
if (detail.startsWith('agent-token pull failed after')) {
|
|
1023
|
+
throw new Error(`idle agent-token refresh failed: ${detail}`);
|
|
1024
|
+
}
|
|
1025
|
+
log(`turn poll failed; retrying (${detail})`);
|
|
1026
|
+
}
|
|
1027
|
+
finally {
|
|
1028
|
+
try {
|
|
1029
|
+
unlinkSync(bearerFileAbs);
|
|
1030
|
+
}
|
|
1031
|
+
catch { /* idle: no readable bearer */ }
|
|
1032
|
+
}
|
|
1033
|
+
if (pendingTurn) {
|
|
1034
|
+
current = {
|
|
1035
|
+
runId: pendingTurn.runId,
|
|
1036
|
+
ordinal: pendingTurn.ordinal,
|
|
1037
|
+
body: promptForPendingHostedTurn(pendingTurn),
|
|
1038
|
+
};
|
|
1039
|
+
break;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
catch (error) {
|
|
1045
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1046
|
+
hostedTurnFailure = new HostedTurnFailure(detail);
|
|
622
1047
|
}
|
|
623
1048
|
finally {
|
|
624
|
-
|
|
1049
|
+
try {
|
|
1050
|
+
await closeHostedLoopSession(hostedLoopSession);
|
|
1051
|
+
}
|
|
1052
|
+
catch (error) {
|
|
1053
|
+
log(`hosted-loop cleanup failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1054
|
+
}
|
|
1055
|
+
try {
|
|
1056
|
+
unlinkSync(bearerFileAbs);
|
|
1057
|
+
}
|
|
1058
|
+
catch { /* already absent */ }
|
|
1059
|
+
try {
|
|
1060
|
+
rmSync(sessionDirAbs, { recursive: true, force: true });
|
|
1061
|
+
}
|
|
1062
|
+
catch (error) {
|
|
1063
|
+
log(`hosted-loop session-dir cleanup failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1064
|
+
}
|
|
625
1065
|
}
|
|
1066
|
+
if (hostedTurnFailure)
|
|
1067
|
+
throw hostedTurnFailure;
|
|
1068
|
+
throw new HostedTurnFailure('hosted turn drain exited without a terminal result');
|
|
626
1069
|
}
|
|
627
1070
|
/**
|
|
628
1071
|
* Background refresher: before the current bearer expires, pull a fresh one from
|
|
@@ -633,7 +1076,7 @@ export async function runHostedBoot(opts) {
|
|
|
633
1076
|
* reads this file fresh per request (see runHostedBoot), so a rewritten file is
|
|
634
1077
|
* picked up on the next event/request. No-ops when there is no expiry signal.
|
|
635
1078
|
*/
|
|
636
|
-
function startBearerRotation(opts) {
|
|
1079
|
+
export function startBearerRotation(opts) {
|
|
637
1080
|
// Refresh ~5 min before expiry; if the server gives no expiry, fall back to a
|
|
638
1081
|
// conservative fixed cadence just under a typical 60-min TTL.
|
|
639
1082
|
const REFRESH_BUFFER_MS = 5 * 60 * 1000;
|
|
@@ -643,15 +1086,7 @@ function startBearerRotation(opts) {
|
|
|
643
1086
|
const MAX_SCHEDULE_MS = 6 * 60 * 60 * 1000;
|
|
644
1087
|
let stopped = false;
|
|
645
1088
|
let timer = null;
|
|
646
|
-
|
|
647
|
-
if (stopped)
|
|
648
|
-
return;
|
|
649
|
-
const raw = expiresAtMs ? expiresAtMs - opts.now() - REFRESH_BUFFER_MS : FALLBACK_INTERVAL_MS;
|
|
650
|
-
const delay = Math.min(MAX_SCHEDULE_MS, Math.max(30_000, raw));
|
|
651
|
-
timer = setTimeout(refresh, delay);
|
|
652
|
-
if (typeof timer.unref === 'function')
|
|
653
|
-
timer.unref();
|
|
654
|
-
};
|
|
1089
|
+
let refreshInFlight = null;
|
|
655
1090
|
const refresh = async () => {
|
|
656
1091
|
if (stopped)
|
|
657
1092
|
return;
|
|
@@ -662,22 +1097,46 @@ function startBearerRotation(opts) {
|
|
|
662
1097
|
fetchImpl: opts.fetchImpl,
|
|
663
1098
|
attempts: 3,
|
|
664
1099
|
});
|
|
1100
|
+
// stop() may have won while the network pull was in flight. Never
|
|
1101
|
+
// recreate the bearer after the turn has entered its credential-free idle.
|
|
1102
|
+
if (stopped)
|
|
1103
|
+
return;
|
|
665
1104
|
const contents = fresh.token.endsWith('\n') ? fresh.token : `${fresh.token}\n`;
|
|
666
1105
|
opts.writeFile(opts.bearerFileAbs, contents);
|
|
667
1106
|
opts.log('rotated agent bearer (0600 file rewritten)');
|
|
668
1107
|
scheduleFrom(fresh.expiresAtMs);
|
|
669
1108
|
}
|
|
670
1109
|
catch (error) {
|
|
1110
|
+
if (stopped)
|
|
1111
|
+
return;
|
|
671
1112
|
opts.log(`agent bearer rotation failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
672
1113
|
scheduleFrom(opts.now() + REFRESH_BUFFER_MS); // retry soon
|
|
673
1114
|
}
|
|
674
1115
|
};
|
|
1116
|
+
const startRefresh = () => {
|
|
1117
|
+
const operation = refresh();
|
|
1118
|
+
refreshInFlight = operation;
|
|
1119
|
+
void operation.finally(() => {
|
|
1120
|
+
if (refreshInFlight === operation)
|
|
1121
|
+
refreshInFlight = null;
|
|
1122
|
+
});
|
|
1123
|
+
};
|
|
1124
|
+
const scheduleFrom = (expiresAtMs) => {
|
|
1125
|
+
if (stopped)
|
|
1126
|
+
return;
|
|
1127
|
+
const raw = expiresAtMs ? expiresAtMs - opts.now() - REFRESH_BUFFER_MS : FALLBACK_INTERVAL_MS;
|
|
1128
|
+
const delay = Math.min(MAX_SCHEDULE_MS, Math.max(opts.minimumDelayMs ?? 30_000, raw));
|
|
1129
|
+
timer = (opts.schedule ?? setTimeout)(startRefresh, delay);
|
|
1130
|
+
if (typeof timer.unref === 'function')
|
|
1131
|
+
timer.unref();
|
|
1132
|
+
};
|
|
675
1133
|
scheduleFrom(opts.initialExpiresAtMs);
|
|
676
1134
|
return {
|
|
677
|
-
stop: () => {
|
|
1135
|
+
stop: async () => {
|
|
678
1136
|
stopped = true;
|
|
679
1137
|
if (timer)
|
|
680
1138
|
clearTimeout(timer);
|
|
1139
|
+
await refreshInFlight;
|
|
681
1140
|
},
|
|
682
1141
|
};
|
|
683
1142
|
}
|
|
@@ -718,7 +1177,11 @@ export async function hostedBootCommand(io) {
|
|
|
718
1177
|
// Report it so the DO stops + destroys the sandbox NOW instead of waiting
|
|
719
1178
|
// for the readiness timeout, and mark the run failed so it never stays
|
|
720
1179
|
// 'running' forever (the DO also marks it — both are idempotent).
|
|
721
|
-
await reportBootFailure({
|
|
1180
|
+
await reportBootFailure({
|
|
1181
|
+
env, ctx: bootCtx, reason: message, log,
|
|
1182
|
+
status: error instanceof HostedTurnFailure ? 'turn_failed' : 'failed',
|
|
1183
|
+
attemptTimeoutMs: resolveTurnStatusAttemptTimeoutMs(process.env),
|
|
1184
|
+
});
|
|
722
1185
|
return 1;
|
|
723
1186
|
}
|
|
724
1187
|
}
|
|
@@ -773,10 +1236,30 @@ export async function reportBootFailure(opts) {
|
|
|
773
1236
|
opts.log(`run-failed mark failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
774
1237
|
}
|
|
775
1238
|
}
|
|
1239
|
+
const status = opts.status ?? 'failed';
|
|
1240
|
+
if (status === 'turn_failed' && runId) {
|
|
1241
|
+
try {
|
|
1242
|
+
await postTurnStatus({
|
|
1243
|
+
bootStatusUrl: opts.env.bootStatusUrl,
|
|
1244
|
+
bootSecret: opts.env.bootSecret,
|
|
1245
|
+
status,
|
|
1246
|
+
runId,
|
|
1247
|
+
reason,
|
|
1248
|
+
fetchImpl,
|
|
1249
|
+
sleep: opts.sleep,
|
|
1250
|
+
attemptTimeoutMs: opts.attemptTimeoutMs,
|
|
1251
|
+
});
|
|
1252
|
+
opts.log('turn_failed acknowledged');
|
|
1253
|
+
}
|
|
1254
|
+
catch (error) {
|
|
1255
|
+
opts.log(`turn_failed acknowledgement exhausted: ${error instanceof Error ? error.message : String(error)}`);
|
|
1256
|
+
}
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
776
1259
|
await postBootStatus({
|
|
777
1260
|
bootStatusUrl: opts.env.bootStatusUrl,
|
|
778
1261
|
bootSecret: opts.env.bootSecret,
|
|
779
|
-
status
|
|
1262
|
+
status,
|
|
780
1263
|
runId,
|
|
781
1264
|
reason,
|
|
782
1265
|
fetchImpl,
|