orizu 0.5.6 → 0.5.8
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 +13 -2
- package/dist/credentials.js +44 -0
- package/dist/help.js +77 -7
- package/dist/hosted-bootstrap.js +39 -0
- package/dist/hosted-harness-opencode.js +21 -1
- package/dist/hosted-harness.js +9 -0
- package/dist/hosted-harvest.js +116 -0
- package/dist/hosted-headless.js +163 -0
- package/dist/hosted-loop.js +119 -5
- package/dist/hosted-run-event-sink.js +21 -0
- package/dist/hosted-runtime-assets.js +27 -0
- package/dist/hosted-session-cli.js +51 -9
- package/dist/http.js +16 -1
- package/dist/index.js +54 -86
- package/dist/manifests-cli.js +103 -5
- package/dist/report-comments-cli.js +325 -0
- package/dist/skill-installer.js +16 -1
- package/package.json +1 -1
- package/src/credentials.ts +46 -0
- package/src/help.ts +77 -7
- package/src/hosted-bootstrap.ts +46 -0
- package/src/hosted-harness-opencode.ts +24 -2
- package/src/hosted-harness.ts +9 -0
- package/src/hosted-harvest.ts +159 -0
- package/src/hosted-headless.ts +178 -0
- package/src/hosted-loop.ts +133 -4
- package/src/hosted-run-event-sink.ts +28 -0
- package/src/hosted-runtime-assets.ts +31 -0
- package/src/hosted-session-cli.ts +50 -9
- package/src/http.ts +23 -1
- package/src/index.ts +74 -156
- package/src/manifests-cli.ts +110 -5
- package/src/report-comments-cli.ts +524 -0
- package/src/skill-installer.ts +18 -1
- package/vendor/skills/orizu-cli/SKILL.md +9 -2
- package/vendor/skills/orizu-cli/references/prompt-control-plane.md +1 -1
package/dist/hosted-loop.js
CHANGED
|
@@ -36,10 +36,33 @@ import { spawnSync, spawn as nodeSpawn } from 'child_process';
|
|
|
36
36
|
import { closeSync, existsSync, openSync, readFileSync } from 'fs';
|
|
37
37
|
import { tmpdir } from 'os';
|
|
38
38
|
import { join } from 'path';
|
|
39
|
-
import { PREBAKED_MARKER_PATH, SETUP_HOOK_RELATIVE_PATH, parsePrebakedMarker, } from './hosted-runtime-assets.js';
|
|
39
|
+
import { HOSTED_TASK_PREAMBLE, PREBAKED_MARKER_PATH, SETUP_HOOK_RELATIVE_PATH, composeHostedTaskPrompt, parsePrebakedMarker, } from './hosted-runtime-assets.js';
|
|
40
|
+
import { harvestWorkspace } from './hosted-harvest.js';
|
|
41
|
+
import { annotateHeadlessQuestions, withIdleWatchdog } from './hosted-headless.js';
|
|
40
42
|
import { OPENCODE_PINNED_VERSION, createOpenCodeHarness, spawnOpenCode, } from './hosted-harness-opencode.js';
|
|
41
43
|
import { createClaudeAgentHarness } from './hosted-harness-claude.js';
|
|
42
44
|
import { drainHarnessToSink, resumeRunEventSink, } from './hosted-run-event-sink.js';
|
|
45
|
+
/**
|
|
46
|
+
* Environment injected into the spawned OpenCode server so the AGENT's bash sees a
|
|
47
|
+
* PRE-AUTHENTICATED `orizu` CLI (ALI-1044). ORIZU_TOKEN_FILE points at the 0600
|
|
48
|
+
* rotated bearer file and ORIZU_BASE_URL at the Orizu API — the exact vars the
|
|
49
|
+
* CLI's auth resolution reads (resolveEnvBearerToken + resolveBaseUrl). The dummy
|
|
50
|
+
* ANTHROPIC key (model-key brokering, G3) is preserved. No real secret is carried
|
|
51
|
+
* here: the bearer stays on disk and is read fresh per request from the file.
|
|
52
|
+
*/
|
|
53
|
+
export function buildOpenCodeSpawnEnv(context) {
|
|
54
|
+
const env = {};
|
|
55
|
+
if (context.anthropicDummyKey) {
|
|
56
|
+
env.ANTHROPIC_API_KEY = context.anthropicDummyKey;
|
|
57
|
+
}
|
|
58
|
+
if (context.bearerFile) {
|
|
59
|
+
env.ORIZU_TOKEN_FILE = context.bearerFile;
|
|
60
|
+
}
|
|
61
|
+
if (context.apiBaseUrl) {
|
|
62
|
+
env.ORIZU_BASE_URL = context.apiBaseUrl;
|
|
63
|
+
}
|
|
64
|
+
return Object.keys(env).length > 0 ? env : undefined;
|
|
65
|
+
}
|
|
43
66
|
/** Default egress-canary probe host (a known non-allowlisted destination). */
|
|
44
67
|
export const DEFAULT_EGRESS_CANARY_HOST = 'example.com';
|
|
45
68
|
/** Bound (ms) on the canary probe — a live firewall resets/times out fast; a
|
|
@@ -181,6 +204,25 @@ function defaultRunSetupHook({ workspaceDir }) {
|
|
|
181
204
|
const ok = (res.status ?? 1) === 0;
|
|
182
205
|
return { ran: true, ok, detail: ok ? 'setup.sh ok' : `setup.sh exit ${res.status ?? 'unknown'}` };
|
|
183
206
|
}
|
|
207
|
+
/** Default idle-watchdog window (ALI-1037): abort a prompt that makes no progress
|
|
208
|
+
* for this long. The timer resets on every harness EVENT, and a legitimate long
|
|
209
|
+
* single tool call (a big `npm install`, a build, an optimization step) emits no
|
|
210
|
+
* intermediate events — so this must comfortably exceed the longest expected
|
|
211
|
+
* quiet tool call (review finding #2). Overridable per-run via `idleTimeoutMs` or
|
|
212
|
+
* the `ORIZU_AGENT_IDLE_TIMEOUT_MS` env var. */
|
|
213
|
+
export const DEFAULT_AGENT_IDLE_TIMEOUT_MS = 25 * 60 * 1000;
|
|
214
|
+
/** Resolve the idle window: explicit option wins, then env, then the default. */
|
|
215
|
+
function resolveIdleTimeoutMs(override) {
|
|
216
|
+
if (typeof override === 'number' && Number.isFinite(override))
|
|
217
|
+
return override;
|
|
218
|
+
const raw = process.env.ORIZU_AGENT_IDLE_TIMEOUT_MS;
|
|
219
|
+
if (raw) {
|
|
220
|
+
const parsed = Number.parseInt(raw, 10);
|
|
221
|
+
if (Number.isFinite(parsed))
|
|
222
|
+
return parsed;
|
|
223
|
+
}
|
|
224
|
+
return DEFAULT_AGENT_IDLE_TIMEOUT_MS;
|
|
225
|
+
}
|
|
184
226
|
/** Model-key env vars whose value must be scrubbed from every run event. */
|
|
185
227
|
const MODEL_KEY_ENV_VARS = ['ANTHROPIC_API_KEY', 'OPENAI_API_KEY'];
|
|
186
228
|
function redactionListFromEnv() {
|
|
@@ -365,6 +407,49 @@ export async function runHostedLoop(opts) {
|
|
|
365
407
|
}
|
|
366
408
|
}
|
|
367
409
|
}
|
|
410
|
+
// End-of-run auto-harvest (ALI-1036): commit + push anything the agent left
|
|
411
|
+
// uncommitted/unpushed so a run NEVER loses work — on BOTH the success and
|
|
412
|
+
// failure paths. Runs at most once (guarded), records exactly one of
|
|
413
|
+
// work_persisted / work_none / work_persist_failed, and NEVER throws or changes
|
|
414
|
+
// the run's terminal status: a harvest failure is recorded and the run proceeds.
|
|
415
|
+
let harvested = false;
|
|
416
|
+
const runAutoHarvest = async () => {
|
|
417
|
+
if (harvested)
|
|
418
|
+
return;
|
|
419
|
+
harvested = true;
|
|
420
|
+
let outcome;
|
|
421
|
+
try {
|
|
422
|
+
outcome = harvestWorkspace({
|
|
423
|
+
workspaceDir: context.workspaceDir,
|
|
424
|
+
runId: context.runId,
|
|
425
|
+
author: context.author,
|
|
426
|
+
exec: opts.harvestExec,
|
|
427
|
+
// Real harvest only inside a genuine hosted sandbox (prebaked marker
|
|
428
|
+
// present) — never on a host/test working tree (review finding #1).
|
|
429
|
+
enabled: existsSync(PREBAKED_MARKER_PATH),
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
catch (error) {
|
|
433
|
+
outcome = { kind: 'work_persist_failed', error: error instanceof Error ? error.message : String(error) };
|
|
434
|
+
}
|
|
435
|
+
if (sink.sealed)
|
|
436
|
+
return;
|
|
437
|
+
try {
|
|
438
|
+
if (outcome.kind === 'work_persisted') {
|
|
439
|
+
await sink.append({ kind: 'work_persisted', payload: { sha: outcome.sha, files: outcome.files } });
|
|
440
|
+
}
|
|
441
|
+
else if (outcome.kind === 'work_none') {
|
|
442
|
+
await sink.append({ kind: 'work_none', payload: {} });
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
await sink.append({ kind: 'work_persist_failed', payload: { error: outcome.error } });
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
catch {
|
|
449
|
+
// Recording harvest is best-effort — never fail the run because the harvest
|
|
450
|
+
// event append failed.
|
|
451
|
+
}
|
|
452
|
+
};
|
|
368
453
|
let spawned = null;
|
|
369
454
|
let agentSessionId = null;
|
|
370
455
|
let installOk = false;
|
|
@@ -417,7 +502,7 @@ export async function runHostedLoop(opts) {
|
|
|
417
502
|
model: context.model,
|
|
418
503
|
cwd: context.workspaceDir,
|
|
419
504
|
port: context.opencodePort,
|
|
420
|
-
env: context
|
|
505
|
+
env: buildOpenCodeSpawnEnv(context),
|
|
421
506
|
spawn: nodeChildSpawner,
|
|
422
507
|
logPath: join(tmpdir(), `opencode-serve-${context.runId}.log`),
|
|
423
508
|
signal,
|
|
@@ -437,14 +522,41 @@ export async function runHostedLoop(opts) {
|
|
|
437
522
|
resumeAgentSessionId: context.resumeAgentSessionId,
|
|
438
523
|
});
|
|
439
524
|
agentSessionId = started.agentSessionId;
|
|
525
|
+
// Prompt scaffolding (ALI-1036): wrap the user task with the standing
|
|
526
|
+
// headless preamble, keeping the task verbatim beneath a delimiter.
|
|
440
527
|
const prompt = {
|
|
441
528
|
runId: context.runId,
|
|
442
529
|
messageId: context.messageId,
|
|
443
|
-
content: taskPrompt,
|
|
530
|
+
content: composeHostedTaskPrompt(taskPrompt, opts.taskPreamble ?? HOSTED_TASK_PREAMBLE),
|
|
444
531
|
author: context.author,
|
|
445
532
|
};
|
|
446
|
-
|
|
533
|
+
// Drive the prompt through (a) question auto-handling annotation and (b) the
|
|
534
|
+
// idle watchdog (ALI-1037): no harness event for `idleTimeoutMs` aborts the
|
|
535
|
+
// prompt and throws `AgentStalledError` → the catch below harvests, then
|
|
536
|
+
// finishes the run `failed`. The prompt runs under a CHILD AbortController
|
|
537
|
+
// linked to the loop signal so the watchdog can abort the in-flight prompt
|
|
538
|
+
// (unblocking a signal-respecting harness the same way a cancel would), then
|
|
539
|
+
// best-effort `harness.stop()` the driver.
|
|
540
|
+
const idleTimeoutMs = resolveIdleTimeoutMs(opts.idleTimeoutMs);
|
|
541
|
+
const promptController = new AbortController();
|
|
542
|
+
if (signal.aborted)
|
|
543
|
+
promptController.abort();
|
|
544
|
+
else
|
|
545
|
+
signal.addEventListener('abort', () => promptController.abort(), { once: true });
|
|
546
|
+
const annotated = annotateHeadlessQuestions(harness.runPrompt(prompt, promptController.signal));
|
|
547
|
+
const drivenStream = idleTimeoutMs > 0
|
|
548
|
+
? withIdleWatchdog(annotated, {
|
|
549
|
+
timeoutMs: idleTimeoutMs,
|
|
550
|
+
onTimeout: async () => {
|
|
551
|
+
promptController.abort();
|
|
552
|
+
await harness.stop();
|
|
553
|
+
},
|
|
554
|
+
})
|
|
555
|
+
: annotated;
|
|
556
|
+
const status = await drainHarnessToSink(drivenStream, sink, {
|
|
447
557
|
summary: { agentSessionId },
|
|
558
|
+
// Auto-harvest runs on EVERY terminal path, before the sink seals.
|
|
559
|
+
beforeFinish: runAutoHarvest,
|
|
448
560
|
});
|
|
449
561
|
await harness.shutdown();
|
|
450
562
|
return { status, agentSessionId, installOk, error: null };
|
|
@@ -452,7 +564,9 @@ export async function runHostedLoop(opts) {
|
|
|
452
564
|
catch (error) {
|
|
453
565
|
const message = error instanceof Error ? error.message : String(error);
|
|
454
566
|
// Fail closed: never leave the run non-terminal. finish() is idempotent and
|
|
455
|
-
// no-ops when the drain already sealed the sink.
|
|
567
|
+
// no-ops when the drain already sealed the sink. Harvest FIRST (partial work
|
|
568
|
+
// is valuable, incl. an idle-watchdog abort) — before the terminal seals.
|
|
569
|
+
await runAutoHarvest();
|
|
456
570
|
if (!sink.sealed) {
|
|
457
571
|
try {
|
|
458
572
|
await sink.finish('failed', { summary: { error: message } });
|
|
@@ -71,6 +71,13 @@ const EVENT_TYPE_BY_KIND = {
|
|
|
71
71
|
// through this SAME sink (single writer). See EGRESS_*_EVENT_TYPE below.
|
|
72
72
|
egress_blocked: EGRESS_BLOCKED_EVENT_TYPE,
|
|
73
73
|
egress_allowed: EGRESS_ALLOWED_EVENT_TYPE,
|
|
74
|
+
// Durability + headless (ALI-1036 / ALI-1037), emitted by the in-sandbox loop
|
|
75
|
+
// through this SAME sink (single writer): auto-harvest results + headless
|
|
76
|
+
// question auto-handling record.
|
|
77
|
+
work_persisted: 'work_persisted',
|
|
78
|
+
work_none: 'work_none',
|
|
79
|
+
work_persist_failed: 'work_persist_failed',
|
|
80
|
+
question_auto_answered: 'agent_question_auto_answered',
|
|
74
81
|
};
|
|
75
82
|
/** The RunAPI event type a harness kind maps to, or null if it is not appendable
|
|
76
83
|
* (terminal kinds route through the PATCH; deferred kinds have no mapping yet). */
|
|
@@ -457,6 +464,16 @@ export async function resumeRunEventSink(options) {
|
|
|
457
464
|
* run's final status.
|
|
458
465
|
*/
|
|
459
466
|
export async function drainHarnessToSink(events, sink, opts = {}) {
|
|
467
|
+
// Run the pre-terminal hook (auto-harvest) at most once, right before the first
|
|
468
|
+
// finish() on any terminal path — while the sink is still writable.
|
|
469
|
+
let beforeFinishRan = false;
|
|
470
|
+
const runBeforeFinish = async () => {
|
|
471
|
+
if (beforeFinishRan)
|
|
472
|
+
return;
|
|
473
|
+
beforeFinishRan = true;
|
|
474
|
+
if (opts.beforeFinish)
|
|
475
|
+
await opts.beforeFinish();
|
|
476
|
+
};
|
|
460
477
|
for await (const event of events) {
|
|
461
478
|
if (event.kind === 'execution_complete') {
|
|
462
479
|
// A prompt the caller/agent stopped is a cancellation, not a failure: the
|
|
@@ -464,6 +481,7 @@ export async function drainHarnessToSink(events, sink, opts = {}) {
|
|
|
464
481
|
// and stop()-initiated paths. A genuine failure (success:false, no abort
|
|
465
482
|
// flag) still lands 'failed'.
|
|
466
483
|
if (event.payload.aborted === true) {
|
|
484
|
+
await runBeforeFinish();
|
|
467
485
|
await sink.finish('cancelled', {
|
|
468
486
|
summary: opts.summary,
|
|
469
487
|
evidence: opts.evidence,
|
|
@@ -476,10 +494,12 @@ export async function drainHarnessToSink(events, sink, opts = {}) {
|
|
|
476
494
|
const summary = success
|
|
477
495
|
? opts.summary
|
|
478
496
|
: { ...(opts.summary ?? {}), error: event.payload.error ?? 'execution failed' };
|
|
497
|
+
await runBeforeFinish();
|
|
479
498
|
await sink.finish(status, { summary, evidence: opts.evidence, transcript: opts.transcript });
|
|
480
499
|
return status;
|
|
481
500
|
}
|
|
482
501
|
if (event.kind === 'error') {
|
|
502
|
+
await runBeforeFinish();
|
|
483
503
|
await sink.finish('failed', {
|
|
484
504
|
summary: { ...(opts.summary ?? {}), error: event.payload.error ?? 'agent error' },
|
|
485
505
|
evidence: opts.evidence,
|
|
@@ -491,6 +511,7 @@ export async function drainHarnessToSink(events, sink, opts = {}) {
|
|
|
491
511
|
}
|
|
492
512
|
// The harness contract guarantees a terminal event; reaching here means the
|
|
493
513
|
// stream ended abnormally. Fail closed so the run never dangles non-terminal.
|
|
514
|
+
await runBeforeFinish();
|
|
494
515
|
await sink.finish('failed', {
|
|
495
516
|
summary: { ...(opts.summary ?? {}), error: 'harness stream ended without a terminal event' },
|
|
496
517
|
transcript: opts.transcript,
|
|
@@ -39,6 +39,33 @@ export const SETUP_HOOK_RELATIVE_PATH = '.orizu/setup.sh';
|
|
|
39
39
|
* same identity for run-event attribution; keep them identical.
|
|
40
40
|
*/
|
|
41
41
|
export const AGENT_GIT_IDENTITY = { name: 'Orizu Agent', email: 'agent@orizu.ai' };
|
|
42
|
+
// -- Hosted task prompt scaffolding (ALI-1036) -------------------------------
|
|
43
|
+
/**
|
|
44
|
+
* Standing preamble wrapped around every hosted task prompt so a NATURAL task
|
|
45
|
+
* ("review X and update the readme") runs unattended with zero magic words. The
|
|
46
|
+
* agent is told to work autonomously (never block on the human), commit as it
|
|
47
|
+
* goes, and treat Orizu — not the repo — as the home for production pointers and
|
|
48
|
+
* run metadata. An end-of-run auto-harvest (ALI-1036, `harvestWorkspace`) is the
|
|
49
|
+
* safety net for anything left uncommitted; the preamble names it so the agent
|
|
50
|
+
* knows partial work is preserved.
|
|
51
|
+
*/
|
|
52
|
+
export const HOSTED_TASK_PREAMBLE = 'You are running unattended in a hosted sandbox. Work autonomously: never ask the user ' +
|
|
53
|
+
'questions — when uncertain, choose the most reasonable/reversible option and note the ' +
|
|
54
|
+
'decision. Commit your work to the current branch in logical units as you go (an automatic ' +
|
|
55
|
+
'checkpoint will also save anything uncommitted at the end). Never switch branches. ' +
|
|
56
|
+
'Production/default pointers and run metadata live in Orizu, not this repo.';
|
|
57
|
+
/** Delimiter separating the standing preamble from the verbatim user task. */
|
|
58
|
+
export const HOSTED_TASK_DELIMITER = '--- YOUR TASK ---';
|
|
59
|
+
/**
|
|
60
|
+
* Compose the full hosted prompt: the standing preamble, a clear delimiter, then
|
|
61
|
+
* the user's task VERBATIM beneath it. Passing an empty preamble yields the task
|
|
62
|
+
* unchanged (an explicit opt-out for tests / callers that scaffold elsewhere).
|
|
63
|
+
*/
|
|
64
|
+
export function composeHostedTaskPrompt(task, preamble = HOSTED_TASK_PREAMBLE) {
|
|
65
|
+
if (!preamble)
|
|
66
|
+
return task;
|
|
67
|
+
return `${preamble}\n\n${HOSTED_TASK_DELIMITER}\n${task}`;
|
|
68
|
+
}
|
|
42
69
|
// -- Pre-baked runtime marker (ALI-1017) -------------------------------------
|
|
43
70
|
/**
|
|
44
71
|
* Absolute path of the marker the pre-baked hosted-runtime image writes at build
|
|
@@ -198,6 +198,12 @@ async function defaultResolveRepo(fetcher, workspaceId, sessionId) {
|
|
|
198
198
|
return { repoFullName, cloneUrl: `https://github.com/${repoFullName}.git` };
|
|
199
199
|
}
|
|
200
200
|
export async function startHostedSession(opts) {
|
|
201
|
+
// ALI-1035 fail-fast: a hosted run cannot reach the model provider without a key
|
|
202
|
+
// (the firewall brokers the real key onto egress). Refuse BEFORE creating any
|
|
203
|
+
// session, run row, or sandbox — no orphaned rows, no paid box, instant error.
|
|
204
|
+
if (!opts.modelApiKey) {
|
|
205
|
+
throw new Error('hosted session needs a model API key: set ANTHROPIC_API_KEY (or pass --model-key)');
|
|
206
|
+
}
|
|
201
207
|
const rawFetch = opts.rawFetch ?? globalThis.fetch;
|
|
202
208
|
const log = opts.logLine ?? (() => { });
|
|
203
209
|
const model = opts.model ?? DEFAULT_HOSTED_MODEL;
|
|
@@ -418,6 +424,32 @@ export async function startHostedSession(opts) {
|
|
|
418
424
|
let sandboxBudgetMs = durationMs;
|
|
419
425
|
let tokenExpiresAtMs = now() + ttlMinutes * 60 * 1000;
|
|
420
426
|
let stopped = false;
|
|
427
|
+
// ALI-1037 host-side idle gate: only KEEP a sandbox alive while its run is
|
|
428
|
+
// making progress. Before each keepalive extension we cheaply check whether any
|
|
429
|
+
// new run event landed since the last check; a run with NO new events since the
|
|
430
|
+
// previous rotation cycle is not extended (the in-sandbox watchdog is the
|
|
431
|
+
// primary stall killer — this stops the HOST from paying to keep a dead box
|
|
432
|
+
// alive). "Can't tell" (non-2xx / transport error) never starves a healthy run.
|
|
433
|
+
let lastEventCursor = 0;
|
|
434
|
+
const hasEventProgress = async () => {
|
|
435
|
+
try {
|
|
436
|
+
const res = await opts.fetcher(`/api/cli/workbench-runs/${encodeURIComponent(runId)}/events?after=${lastEventCursor}&limit=500`);
|
|
437
|
+
if (!res.ok)
|
|
438
|
+
return true;
|
|
439
|
+
const body = (await res.json().catch(() => ({})));
|
|
440
|
+
const events = Array.isArray(body.events) ? body.events : [];
|
|
441
|
+
if (events.length === 0)
|
|
442
|
+
return false;
|
|
443
|
+
lastEventCursor = events.reduce((max, event) => {
|
|
444
|
+
const seq = typeof event.sequence === 'number' ? event.sequence : 0;
|
|
445
|
+
return seq > max ? seq : max;
|
|
446
|
+
}, lastEventCursor);
|
|
447
|
+
return true;
|
|
448
|
+
}
|
|
449
|
+
catch {
|
|
450
|
+
return true;
|
|
451
|
+
}
|
|
452
|
+
};
|
|
421
453
|
let cancelSleep = null;
|
|
422
454
|
const stop = () => {
|
|
423
455
|
stopped = true;
|
|
@@ -485,15 +517,23 @@ export async function startHostedSession(opts) {
|
|
|
485
517
|
log('rotated agent bearer (file overwritten)');
|
|
486
518
|
// Keepalive: extend the sandbox by one rotation interval each rotation
|
|
487
519
|
// while attached (SDK supports extendTimeout(ms)), bounded by the 24h cap
|
|
488
|
-
// so a long attached session outlives the initial --duration window
|
|
520
|
+
// so a long attached session outlives the initial --duration window — BUT
|
|
521
|
+
// only while the run is making progress (ALI-1037). A run with no new
|
|
522
|
+
// events since the last rotation is not extended: the box is left to wind
|
|
523
|
+
// down to its current budget instead of the host paying for a stalled run.
|
|
489
524
|
if (sandbox.extendTimeout && sandboxBudgetMs + rotationIntervalMs <= sandboxCapMs) {
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
sandboxBudgetMs += rotationIntervalMs;
|
|
493
|
-
log(`extended sandbox timeout (+${Math.round(rotationIntervalMs / 1000)}s, budget ${Math.round(sandboxBudgetMs / 60000)}m)`);
|
|
525
|
+
if (!(await hasEventProgress())) {
|
|
526
|
+
log('sandbox timeout NOT extended: no run events since the last rotation (agent stalled) — letting the box wind down');
|
|
494
527
|
}
|
|
495
|
-
|
|
496
|
-
|
|
528
|
+
else {
|
|
529
|
+
try {
|
|
530
|
+
await sandbox.extendTimeout(rotationIntervalMs);
|
|
531
|
+
sandboxBudgetMs += rotationIntervalMs;
|
|
532
|
+
log(`extended sandbox timeout (+${Math.round(rotationIntervalMs / 1000)}s, budget ${Math.round(sandboxBudgetMs / 60000)}m)`);
|
|
533
|
+
}
|
|
534
|
+
catch (error) {
|
|
535
|
+
log(`sandbox extendTimeout failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
536
|
+
}
|
|
497
537
|
}
|
|
498
538
|
}
|
|
499
539
|
}
|
|
@@ -562,7 +602,7 @@ const HOSTED_USAGE = 'Usage: orizu session start --hosted --task "<prompt>" [--d
|
|
|
562
602
|
'[--model <provider/model>] [--project <team/project>] [--provider vercel|local-sim] ' +
|
|
563
603
|
'[--image <orizu-hosted-runtime:tag> (pre-baked VCR image; default env ORIZU_HOSTED_IMAGE)] ' +
|
|
564
604
|
'[--snapshot <snapshot-id> (pre-baked Vercel snapshot, zero-Docker; default env ORIZU_HOSTED_SNAPSHOT; ' +
|
|
565
|
-
'mutually exclusive with --image)] [--tail]\n' +
|
|
605
|
+
'mutually exclusive with --image)] [--model-key <key> (else ANTHROPIC_API_KEY; required)] [--tail]\n' +
|
|
566
606
|
'Note (v0): if you disconnect, the agent token expires within its TTL (<=60m) and the run finishes ' +
|
|
567
607
|
'cleanly; reconnect with `orizu run tail --run <id>`. The sandbox timeout is independent (up to 24h).';
|
|
568
608
|
/**
|
|
@@ -618,7 +658,9 @@ export async function hostedCommand(args, io) {
|
|
|
618
658
|
hostedImage: argVal(args, '--image') ?? process.env.ORIZU_HOSTED_IMAGE ?? undefined,
|
|
619
659
|
hostedSnapshot: argVal(args, '--snapshot') ?? process.env.ORIZU_HOSTED_SNAPSHOT ?? undefined,
|
|
620
660
|
tail,
|
|
621
|
-
|
|
661
|
+
// Model key: explicit --model-key wins, else ANTHROPIC_API_KEY. Falsy → the
|
|
662
|
+
// fail-fast in startHostedSession throws before any session/sandbox is made.
|
|
663
|
+
modelApiKey: argVal(args, '--model-key') ?? (process.env.ANTHROPIC_API_KEY || undefined),
|
|
622
664
|
logLine: line => io.print(line),
|
|
623
665
|
onTailEvent: event => io.print(io.json ? JSON.stringify(event) : `#${String(event.sequence ?? '?')} ${String(event.eventType ?? '')}`),
|
|
624
666
|
tailImpl: tail
|
package/dist/http.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getActiveBaseUrl, getServerCredentials, updateServerCredentials } from './credentials.js';
|
|
1
|
+
import { getActiveBaseUrl, getServerCredentials, resolveEnvBearerToken, updateServerCredentials, } from './credentials.js';
|
|
2
2
|
import { getFlagBaseUrl, normalizeBaseUrl } from './global-flags.js';
|
|
3
3
|
let runtimeFlags = { local: false, server: null };
|
|
4
4
|
export function setGlobalFlags(flags) {
|
|
@@ -94,6 +94,21 @@ async function refreshCredentials(baseUrl, credentials) {
|
|
|
94
94
|
export async function authedFetch(path, init = {}) {
|
|
95
95
|
const baseUrl = resolveBaseUrl();
|
|
96
96
|
assertSecureTokenTransport(baseUrl);
|
|
97
|
+
// In-sandbox pre-auth (ALI-1044): a bearer supplied via ORIZU_TOKEN /
|
|
98
|
+
// ORIZU_TOKEN_FILE takes precedence over credentials.json. It is externally
|
|
99
|
+
// managed (the hosted loop rotates the token file), so it is read FRESH here on
|
|
100
|
+
// every request and NEVER refreshed by this client — on a 401 the loop rotates
|
|
101
|
+
// the file and the next request naturally picks up the new bearer.
|
|
102
|
+
const envBearer = resolveEnvBearerToken();
|
|
103
|
+
if (envBearer) {
|
|
104
|
+
return await fetch(`${baseUrl}${path}`, {
|
|
105
|
+
...init,
|
|
106
|
+
headers: {
|
|
107
|
+
...(init.headers || {}),
|
|
108
|
+
Authorization: `Bearer ${envBearer}`,
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
}
|
|
97
112
|
const credentials = getServerCredentials(baseUrl);
|
|
98
113
|
if (!credentials) {
|
|
99
114
|
throw new Error(`Not logged in for ${baseUrl}. Run \`orizu login --server ${baseUrl}\` (or \`--local\`) first.`);
|
package/dist/index.js
CHANGED
|
@@ -36,6 +36,7 @@ import { hostedCommand } from './hosted-session-cli.js';
|
|
|
36
36
|
import { workspaceSyncCommand } from './workspace-sync.js';
|
|
37
37
|
import { runGitCredential } from './git-credential.js';
|
|
38
38
|
import { runGithubLink, runInteractiveHostedSetup } from './github-setup.js';
|
|
39
|
+
import { reportCommentsCommand, throwDeprecatedPromptCommentsCommand, } from './report-comments-cli.js';
|
|
39
40
|
function getErrorMessage(error) {
|
|
40
41
|
return error instanceof Error ? error.message : String(error);
|
|
41
42
|
}
|
|
@@ -609,63 +610,6 @@ function printTaskStatusSummary(data) {
|
|
|
609
610
|
});
|
|
610
611
|
}
|
|
611
612
|
}
|
|
612
|
-
function compactTerminalText(value, maxLength = 160) {
|
|
613
|
-
const compact = sanitizeTerminalText(value).replace(/\s+/g, ' ').trim();
|
|
614
|
-
if (compact.length <= maxLength) {
|
|
615
|
-
return compact;
|
|
616
|
-
}
|
|
617
|
-
return `${compact.slice(0, Math.max(0, maxLength - 1)).trim()}…`;
|
|
618
|
-
}
|
|
619
|
-
function printIndentedBody(value, indent) {
|
|
620
|
-
const lines = sanitizeTerminalText(value || '').split(/\r?\n/);
|
|
621
|
-
for (const line of lines) {
|
|
622
|
-
printLine(`${indent}${line}`);
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
function formatPromptCommentAnchor(anchor) {
|
|
626
|
-
if (!anchor) {
|
|
627
|
-
return null;
|
|
628
|
-
}
|
|
629
|
-
const lineLabel = anchor.startLine && anchor.endLine
|
|
630
|
-
? anchor.startLine === anchor.endLine
|
|
631
|
-
? `line ${anchor.startLine}`
|
|
632
|
-
: `lines ${anchor.startLine}-${anchor.endLine}`
|
|
633
|
-
: null;
|
|
634
|
-
const selectedText = anchor.text ? `"${compactTerminalText(anchor.text, 180)}"` : null;
|
|
635
|
-
if (lineLabel && selectedText) {
|
|
636
|
-
return `${lineLabel}: ${selectedText}`;
|
|
637
|
-
}
|
|
638
|
-
return lineLabel || selectedText;
|
|
639
|
-
}
|
|
640
|
-
function printPromptComments(data) {
|
|
641
|
-
const versionLabel = data.version.versionLabel || (data.version.versionNumber ? `v${data.version.versionNumber}` : data.version.id);
|
|
642
|
-
printLine(`Prompt: ${sanitizeTerminalText(data.prompt.name)} (${sanitizeTerminalText(data.prompt.id)})`);
|
|
643
|
-
printLine(`Version: ${sanitizeTerminalText(String(versionLabel))} (${sanitizeTerminalText(data.version.id)})`);
|
|
644
|
-
printLine(`Comments: ${data.summary.threadCount} thread${data.summary.threadCount === 1 ? '' : 's'} ` +
|
|
645
|
-
`(${data.summary.openThreadCount} open, ${data.summary.resolvedThreadCount} resolved, ${data.summary.replyCount} replies)`);
|
|
646
|
-
if (data.comments.length === 0) {
|
|
647
|
-
printLine('\nNo comments found for this prompt version.');
|
|
648
|
-
return;
|
|
649
|
-
}
|
|
650
|
-
for (const [index, thread] of data.comments.entries()) {
|
|
651
|
-
const authorName = thread.author?.name || 'Unknown';
|
|
652
|
-
const anchor = formatPromptCommentAnchor(thread.anchor);
|
|
653
|
-
printLine('');
|
|
654
|
-
printLine(`${index + 1}. [${thread.status}] ${sanitizeTerminalText(authorName)} · ${sanitizeTerminalText(thread.createdAt)}`);
|
|
655
|
-
if (anchor) {
|
|
656
|
-
printLine(` Selection: ${anchor}`);
|
|
657
|
-
}
|
|
658
|
-
printIndentedBody(thread.body, ' ');
|
|
659
|
-
if (thread.replies.length > 0) {
|
|
660
|
-
printLine(` Replies (${thread.replies.length})`);
|
|
661
|
-
for (const reply of thread.replies) {
|
|
662
|
-
const replyAuthor = reply.author?.name || 'Unknown';
|
|
663
|
-
printLine(` - ${sanitizeTerminalText(replyAuthor)} · ${sanitizeTerminalText(reply.createdAt)}`);
|
|
664
|
-
printIndentedBody(reply.body, ' ');
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
613
|
const DEFAULT_AUTH_CALLBACK_PORT = 43123;
|
|
670
614
|
function resolveAuthCallbackPort() {
|
|
671
615
|
const envPort = process.env.ORIZU_AUTH_PORT;
|
|
@@ -1029,33 +973,6 @@ async function listJudges() {
|
|
|
1029
973
|
}
|
|
1030
974
|
printPromptSummaries(data.judges, 'No judges found.');
|
|
1031
975
|
}
|
|
1032
|
-
async function listPromptComments() {
|
|
1033
|
-
const promptRef = getPositionalArg(2);
|
|
1034
|
-
const project = getArg('--project') || await resolveProjectSlug(null);
|
|
1035
|
-
const label = getArg('--label');
|
|
1036
|
-
const version = getArg('--version');
|
|
1037
|
-
if (!promptRef) {
|
|
1038
|
-
throw new Error('Usage: orizu prompts comments <prompt-id-or-name> --project <team/project> [--label <label> | --version <version-id>] [--json]');
|
|
1039
|
-
}
|
|
1040
|
-
if (label && version) {
|
|
1041
|
-
throw new Error('Use either --label or --version, not both');
|
|
1042
|
-
}
|
|
1043
|
-
const params = new URLSearchParams({ project });
|
|
1044
|
-
if (label)
|
|
1045
|
-
params.set('label', label);
|
|
1046
|
-
if (version)
|
|
1047
|
-
params.set('version', version);
|
|
1048
|
-
const response = await authedFetch(`/api/cli/prompts/${encodeURIComponent(promptRef)}/comments?${params.toString()}`);
|
|
1049
|
-
if (!response.ok) {
|
|
1050
|
-
throw new Error(`Failed to fetch prompt comments: ${await response.text()}`);
|
|
1051
|
-
}
|
|
1052
|
-
const data = await parseJsonResponse(response, 'Prompt comments');
|
|
1053
|
-
if (hasJsonFlag()) {
|
|
1054
|
-
printJson(data);
|
|
1055
|
-
return;
|
|
1056
|
-
}
|
|
1057
|
-
printPromptComments(data);
|
|
1058
|
-
}
|
|
1059
976
|
async function listScorers() {
|
|
1060
977
|
const project = getArg('--project') || await resolveProjectSlug(null);
|
|
1061
978
|
const response = await authedFetch(`/api/cli/scorers?project=${encodeURIComponent(project)}`);
|
|
@@ -2144,6 +2061,21 @@ function hasJsonFlag() {
|
|
|
2144
2061
|
function printJson(value) {
|
|
2145
2062
|
printLine(JSON.stringify(value));
|
|
2146
2063
|
}
|
|
2064
|
+
function createReportCommentsCliContext() {
|
|
2065
|
+
return {
|
|
2066
|
+
getArg,
|
|
2067
|
+
getPositionalArg,
|
|
2068
|
+
rejectDashPrefixedOptionValue,
|
|
2069
|
+
resolveProjectSlug,
|
|
2070
|
+
readSourceFile,
|
|
2071
|
+
authedFetch,
|
|
2072
|
+
parseJsonResponse,
|
|
2073
|
+
hasJsonFlag,
|
|
2074
|
+
printJson,
|
|
2075
|
+
printLine,
|
|
2076
|
+
sanitizeTerminalText,
|
|
2077
|
+
};
|
|
2078
|
+
}
|
|
2147
2079
|
function printCapabilities() {
|
|
2148
2080
|
const capabilities = getCapabilities(getCliVersion());
|
|
2149
2081
|
if (hasJsonFlag()) {
|
|
@@ -3859,6 +3791,35 @@ async function setTaskReport() {
|
|
|
3859
3791
|
printLine(`Uploaded task report for ${sanitizeTerminalText(data.task.id)} ` +
|
|
3860
3792
|
`[${sanitizeTerminalText(data.task.status)}]`);
|
|
3861
3793
|
}
|
|
3794
|
+
async function getTaskReport() {
|
|
3795
|
+
const taskId = getArg('--task');
|
|
3796
|
+
rejectDashPrefixedOptionValue('--task', taskId);
|
|
3797
|
+
if (!taskId) {
|
|
3798
|
+
throw new Error('Usage: orizu tasks report get --task <taskId> [--json]');
|
|
3799
|
+
}
|
|
3800
|
+
const response = await authedFetch(`/api/cli/tasks/${encodeURIComponent(taskId)}/report`);
|
|
3801
|
+
if (!response.ok) {
|
|
3802
|
+
throw new Error(`Failed to read task report: ${await response.text()}`);
|
|
3803
|
+
}
|
|
3804
|
+
const data = await parseJsonResponse(response, 'Task report get');
|
|
3805
|
+
if (hasJsonFlag()) {
|
|
3806
|
+
printJson(data);
|
|
3807
|
+
return;
|
|
3808
|
+
}
|
|
3809
|
+
const title = data.task.title ? `${data.task.title} ` : '';
|
|
3810
|
+
printLine(`Task report: ${sanitizeTerminalText(title)}(${sanitizeTerminalText(data.task.id)})`);
|
|
3811
|
+
if (data.task.status) {
|
|
3812
|
+
printLine(`Status: ${sanitizeTerminalText(data.task.status)}`);
|
|
3813
|
+
}
|
|
3814
|
+
if (data.task.report.sourceName) {
|
|
3815
|
+
printLine(`Source: ${sanitizeTerminalText(data.task.report.sourceName)}`);
|
|
3816
|
+
}
|
|
3817
|
+
if (data.task.report.createdAt) {
|
|
3818
|
+
printLine(`Created: ${sanitizeTerminalText(data.task.report.createdAt)}`);
|
|
3819
|
+
}
|
|
3820
|
+
printLine('');
|
|
3821
|
+
printLine(sanitizeTerminalText(data.task.report.markdown));
|
|
3822
|
+
}
|
|
3862
3823
|
function defaultAppExportFilename(appName, versionNum) {
|
|
3863
3824
|
const safeName = appName
|
|
3864
3825
|
.trim()
|
|
@@ -4896,6 +4857,10 @@ export async function main(rawArgs = process.argv.slice(2)) {
|
|
|
4896
4857
|
await logOptimizationEvent();
|
|
4897
4858
|
return;
|
|
4898
4859
|
}
|
|
4860
|
+
if (command === 'comments') {
|
|
4861
|
+
await reportCommentsCommand(subcommand, createReportCommentsCliContext());
|
|
4862
|
+
return;
|
|
4863
|
+
}
|
|
4899
4864
|
if (command === 'teams' && subcommand === 'list') {
|
|
4900
4865
|
await listTeams();
|
|
4901
4866
|
return;
|
|
@@ -4934,8 +4899,7 @@ export async function main(rawArgs = process.argv.slice(2)) {
|
|
|
4934
4899
|
return;
|
|
4935
4900
|
}
|
|
4936
4901
|
if (command === 'prompts' && subcommand === 'comments') {
|
|
4937
|
-
|
|
4938
|
-
return;
|
|
4902
|
+
throwDeprecatedPromptCommentsCommand();
|
|
4939
4903
|
}
|
|
4940
4904
|
if (command === 'prompts' && subcommand === 'pull') {
|
|
4941
4905
|
await pullPromptArtifact('prompt');
|
|
@@ -5100,6 +5064,10 @@ export async function main(rawArgs = process.argv.slice(2)) {
|
|
|
5100
5064
|
await setTaskReport();
|
|
5101
5065
|
return;
|
|
5102
5066
|
}
|
|
5067
|
+
if (command === 'tasks' && subcommand === 'report' && cliArgs[2] === 'get') {
|
|
5068
|
+
await getTaskReport();
|
|
5069
|
+
return;
|
|
5070
|
+
}
|
|
5103
5071
|
if (command === 'tasks' && subcommand === 'pause') {
|
|
5104
5072
|
await updateTaskStatus('paused');
|
|
5105
5073
|
return;
|