orizu 0.5.15 → 0.5.16
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/dist/artifact-archive.js +20 -4
- package/dist/egress-allowlist-cli.js +72 -10
- package/dist/help.js +13 -6
- package/dist/hosted-runtime-assets.js +8 -1
- package/dist/hosted-session-cli.js +70 -15
- package/dist/index.js +88 -61
- package/dist/kill-switch-cli.js +10 -0
- package/dist/optimizations-list-cli.js +85 -0
- package/dist/preview-runtime.js +93 -0
- package/dist/prompt-draft-push.js +3 -2
- package/dist/registration-tag-warning.js +49 -0
- package/dist/run-event-digest.js +319 -0
- package/dist/runner-dir-verify.js +848 -0
- package/dist/scorer-draft-push.js +3 -2
- package/dist/scores-accept-cli.js +87 -0
- package/dist/workbench-cli.js +4 -2
- package/dist/zip-draft-push.js +3 -2
- package/package.json +1 -1
- package/src/artifact-archive.ts +29 -4
- package/src/egress-allowlist-cli.ts +87 -13
- package/src/help.ts +13 -6
- package/src/hosted-runtime-assets.ts +14 -1
- package/src/hosted-session-cli.ts +76 -14
- package/src/index.ts +92 -74
- package/src/kill-switch-cli.ts +10 -0
- package/src/optimizations-list-cli.ts +117 -0
- package/src/preview-runtime.ts +94 -0
- package/src/prompt-draft-push.ts +5 -2
- package/src/registration-tag-warning.ts +58 -0
- package/src/run-event-digest.ts +333 -0
- package/src/runner-dir-verify.ts +1029 -0
- package/src/scorer-draft-push.ts +5 -2
- package/src/scores-accept-cli.ts +124 -0
- package/src/workbench-cli.ts +4 -2
- package/src/zip-draft-push.ts +5 -2
- package/vendor/orizu-gepa-python/src/orizu_gepa/cli.py +80 -3
- package/vendor/orizu-gepa-python/src/orizu_gepa/optimizer.py +288 -15
- package/vendor/orizu-gepa-python/src/orizu_gepa/runner.py +133 -8
- package/vendor/skills/orizu-cli/SKILL.md +2 -0
- package/vendor/skills/orizu-cli/references/building-apps.md +131 -1
- package/vendor/skills/orizu-cli/references/building-judges.md +18 -0
- package/vendor/skills/orizu-cli/references/cli-reference.md +2 -1
- package/vendor/skills/orizu-cli/references/optimization-with-gepa.md +3 -1
- package/vendor/skills/orizu-cli/references/prompt-control-plane.md +75 -0
- package/vendor/skills/orizu-cli/scripts/test-app.mjs +3 -0
package/dist/artifact-archive.js
CHANGED
|
@@ -11,7 +11,9 @@ const ARTIFACT_EXCLUDED_PATH_NAMES = new Set([
|
|
|
11
11
|
function isNodeError(error) {
|
|
12
12
|
return error instanceof Error;
|
|
13
13
|
}
|
|
14
|
-
|
|
14
|
+
/** Exported for runner-dir-verify (ALI-1159): the pre-exec scan must resolve
|
|
15
|
+
* the same directory this module hashes and the runner later executes. */
|
|
16
|
+
export function expandHomePath(path) {
|
|
15
17
|
if (path.startsWith('~/')) {
|
|
16
18
|
const home = process.env.HOME || '';
|
|
17
19
|
return `${home}/${path.slice(2)}`;
|
|
@@ -123,7 +125,13 @@ export function createStoredZip(entries) {
|
|
|
123
125
|
endOfCentralDirectory.writeUInt16LE(0, 20);
|
|
124
126
|
return Buffer.concat([...localParts, centralDirectory, endOfCentralDirectory]);
|
|
125
127
|
}
|
|
126
|
-
|
|
128
|
+
/**
|
|
129
|
+
* Read the exact entry set the deterministic archive covers (sorted relative
|
|
130
|
+
* paths + bytes). Exported for runner-dir-verify (ALI-1159): the verified
|
|
131
|
+
* snapshot must be materialized from the SAME in-memory bytes that were
|
|
132
|
+
* hashed, so a post-hash mutation of the directory cannot change what runs.
|
|
133
|
+
*/
|
|
134
|
+
export function readArtifactEntries(dirArg) {
|
|
127
135
|
const sourceDir = expandHomePath(dirArg);
|
|
128
136
|
try {
|
|
129
137
|
const stats = statSync(sourceDir);
|
|
@@ -141,12 +149,20 @@ export function zipDirectoryToBase64(dirArg) {
|
|
|
141
149
|
if (files.length === 0) {
|
|
142
150
|
throw new Error(`Directory contains no artifact files: ${sourceDir}`);
|
|
143
151
|
}
|
|
144
|
-
|
|
152
|
+
return files.map(relativePath => ({
|
|
145
153
|
path: relativePath,
|
|
146
154
|
data: readFileSync(join(sourceDir, relativePath)),
|
|
147
|
-
|
|
155
|
+
mode: statSync(join(sourceDir, relativePath)).mode,
|
|
156
|
+
}));
|
|
157
|
+
}
|
|
158
|
+
/** Deterministic archive of an already-read entry set. */
|
|
159
|
+
export function archiveArtifactEntries(entries) {
|
|
160
|
+
const bytes = createStoredZip(entries);
|
|
148
161
|
return {
|
|
149
162
|
zipBase64: bytes.toString('base64'),
|
|
150
163
|
contentSha256: createHash('sha256').update(bytes).digest('hex'),
|
|
151
164
|
};
|
|
152
165
|
}
|
|
166
|
+
export function zipDirectoryToBase64(dirArg) {
|
|
167
|
+
return archiveArtifactEntries(readArtifactEntries(dirArg));
|
|
168
|
+
}
|
|
@@ -13,6 +13,12 @@
|
|
|
13
13
|
* with an injected fetcher.
|
|
14
14
|
*/
|
|
15
15
|
import { authedFetch } from './http.js';
|
|
16
|
+
/** Mirrors MAX_TEAM_EGRESS_DOMAINS in the shared read-side validator
|
|
17
|
+
* (workers/session-coordinator/src/egress-domains.ts). The coordinator only
|
|
18
|
+
* applies the first this-many UNIQUE valid domains at policy compose; the
|
|
19
|
+
* CLI is a standalone package and cannot import the worker module, so the
|
|
20
|
+
* cap constant is duplicated here (kept in sync via that file's tests). */
|
|
21
|
+
const MAX_ADDITIVE_EGRESS_DOMAINS = 100;
|
|
16
22
|
function fetcherFrom(io) {
|
|
17
23
|
return io.fetcher ?? authedFetch;
|
|
18
24
|
}
|
|
@@ -55,22 +61,72 @@ function splitDomains(value) {
|
|
|
55
61
|
.map(d => d.trim())
|
|
56
62
|
.filter(d => d.length > 0);
|
|
57
63
|
}
|
|
64
|
+
function stringArray(value) {
|
|
65
|
+
return Array.isArray(value) ? value.filter((d) => typeof d === 'string') : [];
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Re-derive the "beyond the 100-domain cap" entries from a stored set the
|
|
69
|
+
* SAME way the GET route does (positional over UNIQUE VALID domains): the
|
|
70
|
+
* coordinator applies only the first MAX_ADDITIVE_EGRESS_DOMAINS valid
|
|
71
|
+
* domains, so every valid entry past that index is configured-but-inert.
|
|
72
|
+
* Stored entries are already canonical + deduped by the write RPC, and the
|
|
73
|
+
* invalid (grandfathered) ones — flagged by the server — are not "valid"
|
|
74
|
+
* and so never consume a cap slot. Used for the PUT response, where a
|
|
75
|
+
* shrink-by-one on an oversized legacy row leaves the returned set still
|
|
76
|
+
* over-cap and the server does not send cappedDomains back.
|
|
77
|
+
*/
|
|
78
|
+
function deriveCapped(stored, invalid) {
|
|
79
|
+
const invalidSet = new Set(invalid);
|
|
80
|
+
const capped = [];
|
|
81
|
+
let validCount = 0;
|
|
82
|
+
for (const d of stored) {
|
|
83
|
+
if (invalidSet.has(d))
|
|
84
|
+
continue;
|
|
85
|
+
validCount += 1;
|
|
86
|
+
if (validCount > MAX_ADDITIVE_EGRESS_DOMAINS)
|
|
87
|
+
capped.push(d);
|
|
88
|
+
}
|
|
89
|
+
return capped;
|
|
90
|
+
}
|
|
58
91
|
async function fetchCurrent(fetcher, path) {
|
|
59
92
|
const response = await fetcher(path, { method: 'GET' });
|
|
60
93
|
if (!response.ok) {
|
|
61
94
|
throw new Error(`show failed (${response.status}): ${await responseMessage(response)}`);
|
|
62
95
|
}
|
|
63
96
|
const data = (await response.json());
|
|
64
|
-
return
|
|
97
|
+
return {
|
|
98
|
+
domains: stringArray(data.extraDomains),
|
|
99
|
+
invalid: stringArray(data.invalidDomains),
|
|
100
|
+
capped: stringArray(data.cappedDomains),
|
|
101
|
+
};
|
|
65
102
|
}
|
|
66
|
-
function emit(io, teamSlug,
|
|
103
|
+
function emit(io, teamSlug, current) {
|
|
67
104
|
if (io.json) {
|
|
68
|
-
io.print(JSON.stringify({
|
|
105
|
+
io.print(JSON.stringify({
|
|
106
|
+
teamSlug,
|
|
107
|
+
extraDomains: current.domains,
|
|
108
|
+
invalidDomains: current.invalid,
|
|
109
|
+
cappedDomains: current.capped,
|
|
110
|
+
}));
|
|
69
111
|
return;
|
|
70
112
|
}
|
|
71
|
-
|
|
113
|
+
const invalid = new Set(current.invalid);
|
|
114
|
+
const capped = new Set(current.capped);
|
|
115
|
+
const lines = current.domains.map((d) => {
|
|
116
|
+
// Round 4 (P3): re-adding a non-grandfathered invalid entry is
|
|
117
|
+
// guaranteed to fail the write RPC's tightened validator (grandfathering
|
|
118
|
+
// is retention-only — see set_team_egress_allowlist), so "remove and
|
|
119
|
+
// re-add" was actively misleading. The only viable remediation is
|
|
120
|
+
// removal.
|
|
121
|
+
if (invalid.has(d))
|
|
122
|
+
return `${d} (invalid — will not take effect; remove it (this value is no longer valid and cannot be re-added))`;
|
|
123
|
+
if (capped.has(d))
|
|
124
|
+
return `${d} (beyond the 100-domain cap — will not take effect)`;
|
|
125
|
+
return d;
|
|
126
|
+
});
|
|
127
|
+
io.print(lines.length === 0
|
|
72
128
|
? `${teamSlug}: no additive egress domains (base allowlist only)`
|
|
73
|
-
: `${teamSlug} additive egress domains:\n ${
|
|
129
|
+
: `${teamSlug} additive egress domains:\n ${lines.join('\n ')}`);
|
|
74
130
|
}
|
|
75
131
|
export async function egressAllowlistCommand(args, io) {
|
|
76
132
|
const teamSlug = firstPositional(args);
|
|
@@ -100,7 +156,7 @@ export async function egressAllowlistCommand(args, io) {
|
|
|
100
156
|
}
|
|
101
157
|
else {
|
|
102
158
|
const current = await fetchCurrent(fetcher, path);
|
|
103
|
-
const set = new Set(current);
|
|
159
|
+
const set = new Set(current.domains);
|
|
104
160
|
if (addValue)
|
|
105
161
|
for (const d of splitDomains(addValue))
|
|
106
162
|
set.add(d);
|
|
@@ -119,10 +175,16 @@ export async function egressAllowlistCommand(args, io) {
|
|
|
119
175
|
return 1;
|
|
120
176
|
}
|
|
121
177
|
const data = (await response.json());
|
|
122
|
-
const stored = Array.isArray(data.extraDomains)
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
178
|
+
const stored = Array.isArray(data.extraDomains) ? stringArray(data.extraDomains) : next;
|
|
179
|
+
const invalid = stringArray(data.invalidDomains);
|
|
180
|
+
// Grandfathered retention: a successful PUT may still STORE legacy
|
|
181
|
+
// invalid entries the admin retained — the server flags them and we show
|
|
182
|
+
// it. Round 5 (P2): a >100-entry legacy row can only shrink one entry per
|
|
183
|
+
// PUT (the write RPC's GREATEST(100, stored-1) bound), so the returned
|
|
184
|
+
// set can still be OVER the cap. Re-derive the cap annotations from the
|
|
185
|
+
// returned set — same positional logic GET uses — instead of assuming
|
|
186
|
+
// the RPC left it at/under 100.
|
|
187
|
+
emit(io, teamSlug, { domains: stored, invalid, capped: deriveCapped(stored, invalid) });
|
|
126
188
|
return 0;
|
|
127
189
|
}
|
|
128
190
|
catch (error) {
|
package/dist/help.js
CHANGED
|
@@ -394,6 +394,12 @@ export const COMMAND_DOCS = [
|
|
|
394
394
|
summary: 'Submit score results for a scorer against a prompt version or optimization candidate.',
|
|
395
395
|
group: 'Scorers and runners',
|
|
396
396
|
},
|
|
397
|
+
{
|
|
398
|
+
path: ['scores', 'accept'],
|
|
399
|
+
usage: 'orizu scores accept <score-run-id> [--project <team/project>] [--json]',
|
|
400
|
+
summary: 'Record a human curator acceptance of an agent-reported score as trusted evidence (human-only).',
|
|
401
|
+
group: 'Scorers and runners',
|
|
402
|
+
},
|
|
397
403
|
{
|
|
398
404
|
path: ['runners', 'push'],
|
|
399
405
|
usage: 'orizu runners push <dir> [--project <team/project>] [--name <name>] [--label <label>] [--json]',
|
|
@@ -415,7 +421,7 @@ export const COMMAND_DOCS = [
|
|
|
415
421
|
{
|
|
416
422
|
path: ['runners', 'exec'],
|
|
417
423
|
usage: 'orizu runners exec (--prompt <prompt-version-id> | --prompt-version <id> --runner-version <id> | --scorer-version <id>) --dataset-version <id> --split-set <id-or-name> --split <name> [--runner-dir <dir>] --out <results.jsonl|results.jsonl.gz>',
|
|
418
|
-
summary: 'Execute a runner locally against a dataset split and write row results.',
|
|
424
|
+
summary: 'Execute a runner locally against a dataset split and write row results. --runner-dir bytes must match the registered runner version (ADR-007) — register with `orizu runners push` first.',
|
|
419
425
|
group: 'Scorers and runners',
|
|
420
426
|
},
|
|
421
427
|
{
|
|
@@ -450,8 +456,8 @@ export const COMMAND_DOCS = [
|
|
|
450
456
|
},
|
|
451
457
|
{
|
|
452
458
|
path: ['optimizations', 'run-gepa'],
|
|
453
|
-
usage: 'orizu optimizations run-gepa --project <team/project> --optimizer-version-id <id> --candidate-version-id <id> --runner-version-id <id> --candidate-runner-dir <dir> --scorer-version-id <id> --scorer-runner-version-id <id> --scorer-runner-dir <dir> --dataset-version-id <id> --split-set-id <id> [--train-split train] [--val-split validation] [--budget auto|light|medium|heavy | --max-metric-calls <n> | --max-full-evals <n> | --max-iterations <n>] [--num-threads auto|N] [--reflection-retry-attempts N] [--reflection-http-timeout-seconds N] [--log-dir logs]',
|
|
454
|
-
summary: 'Run the bundled GEPA-style optimizer locally and stream events.',
|
|
459
|
+
usage: 'orizu optimizations run-gepa --project <team/project> --optimizer-version-id <id> --candidate-version-id <id> --runner-version-id <id> --candidate-runner-dir <dir> --scorer-version-id <id> --scorer-runner-version-id <id> --scorer-runner-dir <dir> [--scorer-input-contract gepa|flat_row] [--scorer-candidate-field <row-field>] [--allow-degenerate-seed] --dataset-version-id <id> --split-set-id <id> [--train-split train] [--val-split validation] [--budget auto|light|medium|heavy | --max-metric-calls <n> | --max-full-evals <n> | --max-iterations <n>] [--num-threads auto|N] [--reflection-retry-attempts N] [--reflection-http-timeout-seconds N] [--log-dir logs]',
|
|
460
|
+
summary: 'Run the bundled GEPA-style optimizer locally and stream events. Validates the scorer contract on the seed before iterating; judge runners built for flat-row score runs need --scorer-input-contract flat_row (ALI-1158).',
|
|
455
461
|
group: 'Optimizations',
|
|
456
462
|
},
|
|
457
463
|
{
|
|
@@ -747,7 +753,7 @@ export const COMMAND_DOCS = [
|
|
|
747
753
|
},
|
|
748
754
|
{
|
|
749
755
|
path: ['session', 'start'],
|
|
750
|
-
usage: 'orizu session start [--project <team/project>] [--workspace <dir>] [--json] | orizu session start --hosted (--task <prompt> | --task-file <path>) [--duration <min>] [--project <team/project>] [--tail]',
|
|
756
|
+
usage: 'orizu session start [--project <team/project>] [--workspace <dir>] [--json] | orizu session start --hosted (--task <prompt> | --task-file <path>) [--duration <min>] [--project <team/project>] [--tail [--quiet]]',
|
|
751
757
|
summary: 'Start a durable workspace session, or add --hosted with a task prompt to start a coordinator-managed hosted agent. Plain sessions check out their remote session branch locally when run inside the workbench clone (or with --workspace).',
|
|
752
758
|
group: 'Sessions',
|
|
753
759
|
options: [
|
|
@@ -760,7 +766,8 @@ export const COMMAND_DOCS = [
|
|
|
760
766
|
{ name: '--model <provider/model>', help: 'Optional hosted model override.' },
|
|
761
767
|
{ name: '--reasoning-effort <level>', help: 'Optional hosted model reasoning-effort override.' },
|
|
762
768
|
{ name: '--title <title>', help: 'Optional hosted run title.' },
|
|
763
|
-
{ name: '--tail', help: 'Follow the hosted run until it reaches a terminal state.' },
|
|
769
|
+
{ name: '--tail', help: 'Follow the hosted run until it reaches a terminal state, streaming compact per-event digests (agent text, tool calls with args, results).' },
|
|
770
|
+
{ name: '--quiet', help: 'With --tail: stream event types only (the pre-digest output).' },
|
|
764
771
|
{ name: '--json', help: 'Emit the machine-readable session payload.' },
|
|
765
772
|
],
|
|
766
773
|
examples: [
|
|
@@ -835,7 +842,7 @@ export const COMMAND_DOCS = [
|
|
|
835
842
|
{
|
|
836
843
|
path: ['run', 'tail'],
|
|
837
844
|
usage: 'orizu run tail --run <id> [--after <seq>] [--interval <seconds>] [--once] [--json]',
|
|
838
|
-
summary: 'Cursor-poll a workbench run event log; JSON mode emits one event object per line.',
|
|
845
|
+
summary: 'Cursor-poll a workbench run event log as compact per-event digest lines; JSON mode emits one full event object per line.',
|
|
839
846
|
group: 'Workbench runs',
|
|
840
847
|
options: [
|
|
841
848
|
{ name: '--run <id>', help: 'Workbench run id.', required: true },
|
|
@@ -101,7 +101,14 @@ export function parsePrebakedMarker(raw) {
|
|
|
101
101
|
typeof claudeSdkVersion !== 'string') {
|
|
102
102
|
return null;
|
|
103
103
|
}
|
|
104
|
-
|
|
104
|
+
const marker = { cliVersion, opencodeVersion, claudeSdkVersion, builtFor: 'vercel-sandbox' };
|
|
105
|
+
// Optional Braintrust pins (ALI-1048): carried through when present as strings;
|
|
106
|
+
// absent/odd-typed values are simply omitted (never invalidate the marker).
|
|
107
|
+
if (typeof rec.braintrustPyVersion === 'string')
|
|
108
|
+
marker.braintrustPyVersion = rec.braintrustPyVersion;
|
|
109
|
+
if (typeof rec.braintrustNpmVersion === 'string')
|
|
110
|
+
marker.braintrustNpmVersion = rec.braintrustNpmVersion;
|
|
111
|
+
return marker;
|
|
105
112
|
}
|
|
106
113
|
export function serializeBootContext(ctx) {
|
|
107
114
|
return `${JSON.stringify(ctx, null, 2)}\n`;
|
|
@@ -50,6 +50,7 @@ import { findUnknownOption } from './option-validation.js';
|
|
|
50
50
|
import { getWorkspaceRoot } from './workspace.js';
|
|
51
51
|
import { attachedWorkspaceId } from './workspace-sync.js';
|
|
52
52
|
import { tailWorkbenchRun } from './workbench-cli.js';
|
|
53
|
+
import { formatRunEventDigest, formatRunEventTypeLine } from './run-event-digest.js';
|
|
53
54
|
// Default hosted model — SINGLE source of truth in hosted-loop-lifecycle.ts
|
|
54
55
|
// (ALI-1086); re-exported here so existing importers keep working.
|
|
55
56
|
export { DEFAULT_HOSTED_MODEL };
|
|
@@ -621,7 +622,9 @@ function numFlag(args, flag) {
|
|
|
621
622
|
}
|
|
622
623
|
const HOSTED_USAGE = 'Usage: orizu session start --hosted (--task "<prompt>" | --task-file <path>) [--duration <min> (default 60, max 1440)] ' +
|
|
623
624
|
'[--model <provider/model>] [--reasoning-effort <level>] [--project <team/project>] ' +
|
|
624
|
-
'[--title <title>] [--tail]\n' +
|
|
625
|
+
'[--title <title>] [--tail [--quiet]]\n' +
|
|
626
|
+
'--tail streams compact per-event digests (agent text, tool calls + args, results); ' +
|
|
627
|
+
'--quiet restores the types-only stream; --json keeps full event payloads.\n' +
|
|
625
628
|
'Default: the Orizu server provisions the sandbox (no VERCEL_* or model key needed on your machine); ' +
|
|
626
629
|
'the session coordinator owns its lifetime (ALI-1055).\n' +
|
|
627
630
|
'DEPRECATED escape hatch: --operator provisions from THIS machine (requires VERCEL_* + ANTHROPIC_API_KEY ' +
|
|
@@ -640,6 +643,7 @@ const HOSTED_START_OPTIONS = new Set([
|
|
|
640
643
|
'--project',
|
|
641
644
|
'--title',
|
|
642
645
|
'--tail',
|
|
646
|
+
'--quiet',
|
|
643
647
|
'--operator',
|
|
644
648
|
'--provider',
|
|
645
649
|
'--runtime',
|
|
@@ -673,7 +677,24 @@ export const OPERATOR_DEPRECATION_WARNING = '! DEPRECATED: --operator (operator-
|
|
|
673
677
|
* route and let the Durable Object coordinator own the sandbox lifetime. The
|
|
674
678
|
* CLI never reads VERCEL_* or a model key on this path — the server holds them.
|
|
675
679
|
*/
|
|
676
|
-
|
|
680
|
+
/** Bounded reconcile of a start that the coordinator reported still-in-flight
|
|
681
|
+
* (R7 P4). Each retry re-pokes the SAME session by its returned sessionId, so
|
|
682
|
+
* a race that momentarily answers start_pending never spawns a duplicate.
|
|
683
|
+
*
|
|
684
|
+
* R8 P2 — the retry budget must OUTLIVE a stale start lease. If a start-lease
|
|
685
|
+
* owner crashed WITHOUT running its release, the lease lingers until its TTL
|
|
686
|
+
* (the coordinator's START_LEASE_TTL_MS = 3 × CALLBACK_TIMEOUT_MS = 30s), and
|
|
687
|
+
* every duplicate poke sees it and answers start_pending. So the CLI's OWN
|
|
688
|
+
* cumulative backoff (independent of server-side watch time) must exceed that
|
|
689
|
+
* TTL plus margin, so the CLI keeps polling until the lease expires and a
|
|
690
|
+
* retry then wins — rather than giving up while the crashed owner's lease
|
|
691
|
+
* still lingers. COORDINATOR_START_LEASE_TTL_MS mirrors the worker constant
|
|
692
|
+
* and is pinned to it by a contract test in the CLI suite. */
|
|
693
|
+
export const COORDINATOR_START_LEASE_TTL_MS = 30_000;
|
|
694
|
+
export const HOSTED_START_PENDING_RETRY_MS = 2_000;
|
|
695
|
+
const HOSTED_START_PENDING_TTL_MARGIN_MS = 6_000;
|
|
696
|
+
export const HOSTED_START_PENDING_MAX_RETRIES = Math.ceil((COORDINATOR_START_LEASE_TTL_MS + HOSTED_START_PENDING_TTL_MARGIN_MS) / HOSTED_START_PENDING_RETRY_MS);
|
|
697
|
+
export async function startHostedViaServer(args, workspaceId, task, tail, io, fetcher = (path, init) => authedFetch(path, init), sleep = ms => new Promise(resolve => setTimeout(resolve, ms))) {
|
|
677
698
|
const durationMinutes = numFlag(args, '--duration');
|
|
678
699
|
const model = argVal(args, '--model') ?? undefined;
|
|
679
700
|
const reasoningEffort = argVal(args, '--reasoning-effort') ?? undefined;
|
|
@@ -727,19 +748,39 @@ export async function startHostedViaServer(args, workspaceId, task, tail, io, fe
|
|
|
727
748
|
io.printErr?.(message);
|
|
728
749
|
return 1;
|
|
729
750
|
}
|
|
751
|
+
// R7 P4: the coordinator may momentarily answer start_pending (a start for
|
|
752
|
+
// this session is racing). It returns the sessionId so we RECONCILE the SAME
|
|
753
|
+
// session — re-poking by that id rather than blindly re-initing a duplicate.
|
|
730
754
|
let response;
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
755
|
+
let body = {};
|
|
756
|
+
let reconcileSessionId;
|
|
757
|
+
for (let attempt = 0;; attempt++) {
|
|
758
|
+
const attemptBody = reconcileSessionId
|
|
759
|
+
? JSON.stringify({ ...payload, sessionId: reconcileSessionId })
|
|
760
|
+
: serializedPayload;
|
|
761
|
+
try {
|
|
762
|
+
response = await fetcher('/api/cli/hosted-sessions', {
|
|
763
|
+
method: 'POST',
|
|
764
|
+
headers: { 'Content-Type': 'application/json' },
|
|
765
|
+
body: attemptBody,
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
catch (error) {
|
|
769
|
+
io.printErr?.(`hosted session request failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
770
|
+
return 1;
|
|
771
|
+
}
|
|
772
|
+
body = (await response.json().catch(() => ({})));
|
|
773
|
+
const stillPending = response.status === 409 &&
|
|
774
|
+
body.coordinator === 'start_pending' &&
|
|
775
|
+
typeof body.sessionId === 'string' &&
|
|
776
|
+
body.sessionId.length > 0;
|
|
777
|
+
if (stillPending && attempt < HOSTED_START_PENDING_MAX_RETRIES) {
|
|
778
|
+
reconcileSessionId = body.sessionId;
|
|
779
|
+
await sleep(HOSTED_START_PENDING_RETRY_MS);
|
|
780
|
+
continue;
|
|
781
|
+
}
|
|
782
|
+
break;
|
|
741
783
|
}
|
|
742
|
-
const body = (await response.json().catch(() => ({})));
|
|
743
784
|
const errorMessage = typeof body.error === 'string' ? body.error : `${response.status}`;
|
|
744
785
|
if (response.status === 503) {
|
|
745
786
|
io.printErr?.(errorMessage);
|
|
@@ -764,12 +805,20 @@ export async function startHostedViaServer(args, workspaceId, task, tail, io, fe
|
|
|
764
805
|
io.print(`stream it: orizu run tail --run ${runId}`);
|
|
765
806
|
}
|
|
766
807
|
if (tail && runId) {
|
|
808
|
+
// ALI-1045: the attached tail renders the SAME compact per-event digests
|
|
809
|
+
// `run tail` uses (agent text, tool + args, result + first output line) so
|
|
810
|
+
// the operator watching live sees what is happening without a second
|
|
811
|
+
// terminal. `--quiet` keeps the old types-only stream; `--json` keeps full
|
|
812
|
+
// fidelity.
|
|
813
|
+
const quiet = hasFlag(args, '--quiet');
|
|
767
814
|
await tailWorkbenchRun({
|
|
768
815
|
fetcher,
|
|
769
816
|
runId,
|
|
770
817
|
onEvent: event => io.print(io.json
|
|
771
818
|
? JSON.stringify(event)
|
|
772
|
-
:
|
|
819
|
+
: quiet
|
|
820
|
+
? formatRunEventTypeLine(event)
|
|
821
|
+
: formatRunEventDigest(event)),
|
|
773
822
|
});
|
|
774
823
|
}
|
|
775
824
|
return 0;
|
|
@@ -882,7 +931,13 @@ export async function hostedCommand(args, io, deps = {}) {
|
|
|
882
931
|
// fail-fast in startHostedSession throws before any session/sandbox is made.
|
|
883
932
|
modelApiKey: argVal(args, '--model-key') ?? (process.env.ANTHROPIC_API_KEY || undefined),
|
|
884
933
|
logLine: line => io.print(line),
|
|
885
|
-
|
|
934
|
+
// ALI-1045: compact per-event digests (shared with `run tail`); `--quiet`
|
|
935
|
+
// keeps the old types-only stream, `--json` keeps full fidelity.
|
|
936
|
+
onTailEvent: event => io.print(io.json
|
|
937
|
+
? JSON.stringify(event)
|
|
938
|
+
: hasFlag(args, '--quiet')
|
|
939
|
+
? formatRunEventTypeLine(event)
|
|
940
|
+
: formatRunEventDigest(event)),
|
|
886
941
|
tailImpl: tail
|
|
887
942
|
? async ({ fetcher, runId, onEvent }) => {
|
|
888
943
|
await tailWorkbenchRun({ fetcher, runId, onEvent: event => onEvent?.(event) });
|
package/dist/index.js
CHANGED
|
@@ -29,6 +29,8 @@ import { connectorsCommand } from './connectors-cli.js';
|
|
|
29
29
|
import { ARTIFACT_MAX_BYTES, runnerOptimizerCommand } from './artifact-pull.js';
|
|
30
30
|
import { killSwitchCommand } from './kill-switch-cli.js';
|
|
31
31
|
import { egressAllowlistCommand } from './egress-allowlist-cli.js';
|
|
32
|
+
import { acceptScoreRunCommand } from './scores-accept-cli.js';
|
|
33
|
+
import { listOptimizationRunsCommand } from './optimizations-list-cli.js';
|
|
32
34
|
import { teamConnectorsCommand } from './team-connectors-cli.js';
|
|
33
35
|
import { manifestsCommand } from './manifests-cli.js';
|
|
34
36
|
import { workbenchCommand } from './workbench-cli.js';
|
|
@@ -36,6 +38,7 @@ import { hostedCommand } from './hosted-session-cli.js';
|
|
|
36
38
|
import { workspaceSyncCommand } from './workspace-sync.js';
|
|
37
39
|
import { runGitCredential } from './git-credential.js';
|
|
38
40
|
import { pushPromptDraft } from './prompt-draft-push.js';
|
|
41
|
+
import { assertSnapshotManifestConfined, verifyGepaRunnerDirsFromArgs, verifyRunnerDirRegistered } from './runner-dir-verify.js';
|
|
39
42
|
import { runScorersRegister } from './scorer-draft-push.js';
|
|
40
43
|
import { runZipArtifactPush } from './zip-draft-push.js';
|
|
41
44
|
import { runGithubLink, runInteractiveHostedSetup } from './github-setup.js';
|
|
@@ -1504,8 +1507,10 @@ async function submitScoreResults() {
|
|
|
1504
1507
|
printJson(data);
|
|
1505
1508
|
return;
|
|
1506
1509
|
}
|
|
1510
|
+
// ALI-1175: label the stored provenance — a submitted score is a client-reported claim.
|
|
1507
1511
|
printLine(`Submitted score ${sanitizeTerminalText(data.scoreRun.id)} ` +
|
|
1508
|
-
`(${formatPercent(data.scoreRun.scoreValue)})`
|
|
1512
|
+
`(${formatPercent(data.scoreRun.scoreValue)})` +
|
|
1513
|
+
(data.scoreRun.provenance ? ` [${sanitizeTerminalText(String(data.scoreRun.provenance).replace(/_/g, '-'))}]` : ''));
|
|
1509
1514
|
}
|
|
1510
1515
|
function looksLikeAggregateScoreObject(value) {
|
|
1511
1516
|
return (value.scoreValue !== undefined ||
|
|
@@ -1640,11 +1645,23 @@ async function execScorer() {
|
|
|
1640
1645
|
: scoreResult
|
|
1641
1646
|
? scoreResult.scoreValue
|
|
1642
1647
|
: null;
|
|
1648
|
+
// ALI-1175 (round 5 thread 6 + round 6 thread 2): a scorers-exec score is an
|
|
1649
|
+
// agent-reported claim whether or not it was submitted — label both the
|
|
1650
|
+
// submitted (scoreRun.provenance) and the --no-submit (top-level provenance)
|
|
1651
|
+
// outputs, so a displayed value is never presented as unlabeled evidence.
|
|
1652
|
+
const execProvenance = scoreRun && typeof scoreRun.provenance === 'string'
|
|
1653
|
+
? scoreRun.provenance
|
|
1654
|
+
: typeof data.provenance === 'string'
|
|
1655
|
+
? data.provenance
|
|
1656
|
+
: null;
|
|
1657
|
+
const provenanceSuffix = execProvenance
|
|
1658
|
+
? ` [${sanitizeTerminalText(execProvenance.replace(/_/g, '-'))}]`
|
|
1659
|
+
: '';
|
|
1643
1660
|
if (scoreRun && typeof scoreRun.id === 'string') {
|
|
1644
|
-
printLine(`Executed scorer ${sanitizeTerminalText(scorerVersionId)} -> score ${sanitizeTerminalText(scoreRun.id)} (${formatPercent(numberFromUnknown(scoreValue))})`);
|
|
1661
|
+
printLine(`Executed scorer ${sanitizeTerminalText(scorerVersionId)} -> score ${sanitizeTerminalText(scoreRun.id)} (${formatPercent(numberFromUnknown(scoreValue))})${provenanceSuffix}`);
|
|
1645
1662
|
}
|
|
1646
1663
|
else {
|
|
1647
|
-
printLine(`Executed scorer ${sanitizeTerminalText(scorerVersionId)} (${formatPercent(numberFromUnknown(scoreValue))})`);
|
|
1664
|
+
printLine(`Executed scorer ${sanitizeTerminalText(scorerVersionId)} (${formatPercent(numberFromUnknown(scoreValue))})${provenanceSuffix}`);
|
|
1648
1665
|
}
|
|
1649
1666
|
if (outPath) {
|
|
1650
1667
|
printLine(`Wrote scorer result to ${sanitizeTerminalText(outPath)}`);
|
|
@@ -1866,34 +1883,8 @@ async function exportOptimizationRun() {
|
|
|
1866
1883
|
writeTextFileEnsuringDir(filename, `${JSON.stringify(data, null, 2)}\n`);
|
|
1867
1884
|
printLine(`Saved optimization export to ${sanitizeTerminalText(filename)}`);
|
|
1868
1885
|
}
|
|
1869
|
-
// ALI-1073
|
|
1870
|
-
//
|
|
1871
|
-
async function listOptimizationRuns() {
|
|
1872
|
-
const project = getArg('--project') || await resolveProjectSlug(null);
|
|
1873
|
-
const response = await authedFetch(`/api/cli/optimization-runs?project=${encodeURIComponent(project)}`);
|
|
1874
|
-
if (!response.ok) {
|
|
1875
|
-
throw new Error(`Failed to list optimization runs: ${await response.text()}`);
|
|
1876
|
-
}
|
|
1877
|
-
const data = await parseJsonResponse(response, 'Optimization runs list');
|
|
1878
|
-
if (hasJsonFlag()) {
|
|
1879
|
-
printJson(data);
|
|
1880
|
-
return;
|
|
1881
|
-
}
|
|
1882
|
-
const rows = data.optimizationRuns || [];
|
|
1883
|
-
if (rows.length === 0) {
|
|
1884
|
-
printLine('No optimization runs found for this project.');
|
|
1885
|
-
return;
|
|
1886
|
-
}
|
|
1887
|
-
const idWidth = Math.max(2, ...rows.map(row => String(row.id).length));
|
|
1888
|
-
const statusWidth = Math.max(6, ...rows.map(row => String(row.status).length));
|
|
1889
|
-
printLine(`${'ID'.padEnd(idWidth)} ${'STATUS'.padEnd(statusWidth)} BEST PROMOTED_VERSION`);
|
|
1890
|
-
printLine(`${'-'.repeat(idWidth)} ${'-'.repeat(statusWidth)} ${'-'.repeat(5)} ${'-'.repeat('PROMOTED_VERSION'.length)}`);
|
|
1891
|
-
for (const row of rows) {
|
|
1892
|
-
const best = row.bestScore === null || row.bestScore === undefined ? '-' : String(row.bestScore);
|
|
1893
|
-
const promoted = row.resultPromptVersionId || '-';
|
|
1894
|
-
printLine(`${String(row.id).padEnd(idWidth)} ${String(row.status).padEnd(statusWidth)} ${best.padEnd(5)} ${promoted}`);
|
|
1895
|
-
}
|
|
1896
|
-
}
|
|
1886
|
+
// ALI-1073 list moved to optimizations-list-cli.ts (line ratchet, ALI-976);
|
|
1887
|
+
// ALI-1175 labels its best scores as agent-reported evidence there.
|
|
1897
1888
|
function removeFlagWithValue(args, flag) {
|
|
1898
1889
|
const filtered = [];
|
|
1899
1890
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -1915,31 +1906,50 @@ function bundledOrizuGepaPythonPath() {
|
|
|
1915
1906
|
async function runGepaOptimization() {
|
|
1916
1907
|
const project = getArg('--project') || await resolveProjectSlug(null);
|
|
1917
1908
|
const baseUrl = getBaseUrl();
|
|
1918
|
-
// Uniform resolution (ALI-1090): honors ORIZU_TOKEN / ORIZU_TOKEN_FILE before
|
|
1919
|
-
// credentials.json so hosted sandboxes can run optimizations. Read at spawn
|
|
1920
|
-
// time — the child gets the freshest bearer available at launch.
|
|
1921
|
-
const token = resolveAuthTokenForBaseUrl(baseUrl);
|
|
1922
1909
|
const python = getArg('--python') || process.env.PYTHON || 'python3';
|
|
1923
1910
|
const bundledPythonPath = bundledOrizuGepaPythonPath();
|
|
1924
1911
|
let forwardedArgs = removeFlagWithValue(cliArgs.slice(2), '--python');
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1912
|
+
// ALI-1159 (ADR-007): GEPA executes ad-hoc local runner dirs while
|
|
1913
|
+
// attributing records to registered runner version ids — verify the bytes
|
|
1914
|
+
// ARE those versions before the optimizer spawns. Scans the forwarded argv
|
|
1915
|
+
// itself (both argparse flag forms), fail-closed on half-populated pairs,
|
|
1916
|
+
// and REWRITES the dir flags to verified snapshots (the exact hashed bytes)
|
|
1917
|
+
// so a post-hash mutation of the original dirs cannot change what runs.
|
|
1918
|
+
const verified = await verifyGepaRunnerDirsFromArgs(forwardedArgs);
|
|
1919
|
+
forwardedArgs = verified.args;
|
|
1920
|
+
let result;
|
|
1921
|
+
try {
|
|
1922
|
+
// Uniform resolution (ALI-1090): honors ORIZU_TOKEN / ORIZU_TOKEN_FILE before
|
|
1923
|
+
// credentials.json so hosted sandboxes can run optimizations. Read at spawn
|
|
1924
|
+
// time — AFTER the verification awaits above, so a hosted token-file
|
|
1925
|
+
// rotation crossing the hash + lookup round-trips cannot hand the child a
|
|
1926
|
+
// stale bearer.
|
|
1927
|
+
const token = resolveAuthTokenForBaseUrl(baseUrl);
|
|
1928
|
+
if (!forwardedArgs.includes('--project')) {
|
|
1929
|
+
forwardedArgs = ['--project', project, ...forwardedArgs];
|
|
1930
|
+
}
|
|
1931
|
+
const pythonPathEntries = [
|
|
1932
|
+
bundledPythonPath,
|
|
1933
|
+
process.env.PYTHONPATH,
|
|
1934
|
+
].filter((entry) => Boolean(entry));
|
|
1935
|
+
result = spawnSync(python, ['-m', 'orizu_gepa.cli', ...forwardedArgs], {
|
|
1936
|
+
stdio: 'inherit',
|
|
1937
|
+
env: {
|
|
1938
|
+
...process.env,
|
|
1939
|
+
ORIZU_API_URL: baseUrl,
|
|
1940
|
+
ORIZU_TOKEN: token,
|
|
1941
|
+
ORIZU_PROJECT: project,
|
|
1942
|
+
// Integrity handshake (ALI-1159): the Python entrypoint refuses
|
|
1943
|
+
// runner dirs that are not in this wrapper-verified list.
|
|
1944
|
+
ORIZU_VERIFIED_RUNNER_DIRS: JSON.stringify(verified.verifiedDirs),
|
|
1945
|
+
PYTHONPATH: pythonPathEntries.join(delimiter),
|
|
1946
|
+
PYTHONUNBUFFERED: process.env.PYTHONUNBUFFERED || '1',
|
|
1947
|
+
},
|
|
1948
|
+
});
|
|
1949
|
+
}
|
|
1950
|
+
finally {
|
|
1951
|
+
verified.cleanup();
|
|
1952
|
+
}
|
|
1943
1953
|
if (result.error) {
|
|
1944
1954
|
throw result.error;
|
|
1945
1955
|
}
|
|
@@ -4635,10 +4645,7 @@ async function materializeRunnerVersion(runnerVersionId) {
|
|
|
4635
4645
|
rmSync(tempDir, { recursive: true, force: true });
|
|
4636
4646
|
throw new Error(`unzip failed: ${sanitizeTerminalText(result.stderr || result.stdout || '')}`);
|
|
4637
4647
|
}
|
|
4638
|
-
return {
|
|
4639
|
-
runnerDir,
|
|
4640
|
-
cleanup: () => rmSync(tempDir, { recursive: true, force: true }),
|
|
4641
|
-
};
|
|
4648
|
+
return { runnerDir, cleanup: () => rmSync(tempDir, { recursive: true, force: true }) };
|
|
4642
4649
|
}
|
|
4643
4650
|
async function runnersExec() {
|
|
4644
4651
|
const prompt = getArg('--prompt');
|
|
@@ -4680,12 +4687,28 @@ async function runnersExec() {
|
|
|
4680
4687
|
throw new Error(`Failed to fetch runner execution context: ${await contextResponse.text()}`);
|
|
4681
4688
|
}
|
|
4682
4689
|
const context = await parseJsonResponse(contextResponse, 'Runner exec context');
|
|
4690
|
+
const resolvedRunnerVersionId = runnerVersion || context.prompt.runnerVersionId;
|
|
4691
|
+
if (!resolvedRunnerVersionId) {
|
|
4692
|
+
throw new Error('Runner exec context did not resolve a runner version id; pass --runner-version explicitly');
|
|
4693
|
+
}
|
|
4694
|
+
// ALI-1159 (ADR-007): results stamp the registered runner_version_id, so
|
|
4695
|
+
// ad-hoc --runner-dir bytes must BE that registered version — and execution
|
|
4696
|
+
// runs the verified SNAPSHOT (exact hashed bytes), never the mutable dir.
|
|
4683
4697
|
const materializedRunner = runnerDirArg
|
|
4684
|
-
?
|
|
4685
|
-
|
|
4698
|
+
? await (async () => {
|
|
4699
|
+
const verified = await verifyRunnerDirRegistered({
|
|
4700
|
+
runnerVersionId: resolvedRunnerVersionId,
|
|
4701
|
+
dir: runnerDirArg,
|
|
4702
|
+
flag: '--runner-dir',
|
|
4703
|
+
});
|
|
4704
|
+
return { runnerDir: verified.snapshotDir, cleanup: verified.cleanup };
|
|
4705
|
+
})()
|
|
4706
|
+
: await materializeRunnerVersion(resolvedRunnerVersionId);
|
|
4686
4707
|
const runnerDir = materializedRunner.runnerDir;
|
|
4687
|
-
const manifest = readRunnerManifest(runnerDir);
|
|
4688
4708
|
try {
|
|
4709
|
+
assertSnapshotManifestConfined(runnerDir, runnerDirArg ? '--runner-dir' : '--runner-version');
|
|
4710
|
+
// Inside the try (codex round-6 P3): a bad manifest must not leak the dir.
|
|
4711
|
+
const manifest = readRunnerManifest(runnerDir);
|
|
4689
4712
|
if (manifest.supports_body_kind && !manifest.supports_body_kind.includes(context.prompt.bodyKind)) {
|
|
4690
4713
|
throw new Error(`Runner does not support prompt body kind '${context.prompt.bodyKind}'. Supported kinds: ${manifest.supports_body_kind.join(', ')}`);
|
|
4691
4714
|
}
|
|
@@ -4996,6 +5019,10 @@ export async function main(rawArgs = process.argv.slice(2)) {
|
|
|
4996
5019
|
await submitScoreResults();
|
|
4997
5020
|
return;
|
|
4998
5021
|
}
|
|
5022
|
+
if (command === 'scores' && subcommand === 'accept') {
|
|
5023
|
+
await acceptScoreRunCommand(cliArgs.slice(2), { json: hasJsonFlag(), print: printLine, resolveProjectSlug });
|
|
5024
|
+
return;
|
|
5025
|
+
}
|
|
4999
5026
|
if (command === 'optimizations' && subcommand === 'start') {
|
|
5000
5027
|
await startOptimizationRun();
|
|
5001
5028
|
return;
|
|
@@ -5009,7 +5036,7 @@ export async function main(rawArgs = process.argv.slice(2)) {
|
|
|
5009
5036
|
return;
|
|
5010
5037
|
}
|
|
5011
5038
|
if (command === 'optimizations' && subcommand === 'list') {
|
|
5012
|
-
await
|
|
5039
|
+
await listOptimizationRunsCommand(cliArgs.slice(2), { json: hasJsonFlag(), print: printLine, resolveProjectSlug });
|
|
5013
5040
|
return;
|
|
5014
5041
|
}
|
|
5015
5042
|
if (command === 'optimizations' &&
|
package/dist/kill-switch-cli.js
CHANGED
|
@@ -8,6 +8,16 @@
|
|
|
8
8
|
* (killed sessions stay ended). Human ADMIN only: the server rejects agent
|
|
9
9
|
* bearers and non-admins. Pure command logic with an injected fetcher so the
|
|
10
10
|
* entry point owns argument parsing and human/JSON output.
|
|
11
|
+
*
|
|
12
|
+
* Containment (ALI-1166): the guarantee is session-end + per-request
|
|
13
|
+
* verification + token-row revocation at engage — a bearer bound to an ended
|
|
14
|
+
* session can never verify again, and ended sessions are terminal
|
|
15
|
+
* (DB-enforced; no post-kill resurrection).
|
|
16
|
+
* `tokensRevoked` in the summary counts the persisted agent token rows the
|
|
17
|
+
* engage revoked (trigger cascade + belt, one transaction); it may include
|
|
18
|
+
* expired or already-ended-session stragglers that were never live bearers at
|
|
19
|
+
* kill time. `--json` output is the server's summary passed through, including
|
|
20
|
+
* `containment: "session-end"`.
|
|
11
21
|
*/
|
|
12
22
|
import { authedFetch } from './http.js';
|
|
13
23
|
async function responseMessage(response) {
|