orizu 0.5.15 → 0.5.17
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/archive-artifacts-cli.js +215 -0
- package/dist/artifact-archive.js +20 -4
- package/dist/dataset-edit-rows.js +90 -0
- package/dist/dataset-upload-snapshot.js +22 -0
- package/dist/diff-comments-cli.js +139 -0
- package/dist/egress-allowlist-cli.js +72 -10
- package/dist/egress-policy.js +12 -0
- package/dist/error-response.js +8 -2
- package/dist/git-credential.js +316 -10
- package/dist/git-recovery-environment.js +156 -0
- package/dist/help.js +125 -9
- package/dist/hosted-boot.js +36 -1
- package/dist/hosted-bootstrap-rehearsal.js +8 -0
- package/dist/hosted-broker-server.js +141 -129
- package/dist/hosted-git-http-server.js +70 -71
- package/dist/hosted-runtime-assets.js +144 -11
- package/dist/hosted-session-cli.js +486 -57
- package/dist/index.js +224 -190
- package/dist/kill-switch-cli.js +10 -0
- package/dist/loopback-serve.js +38 -0
- package/dist/optimizations-list-cli.js +97 -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/repository-backup-core.js +1518 -0
- package/dist/repository-recovery-manifest.js +326 -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/start-intent.js +25 -0
- package/dist/workbench-cli.js +168 -27
- package/dist/workspace-sync.js +5 -0
- package/dist/zip-draft-push.js +3 -2
- package/package.json +1 -1
- package/src/archive-artifacts-cli.ts +382 -0
- package/src/artifact-archive.ts +29 -4
- package/src/dataset-edit-rows.ts +124 -0
- package/src/dataset-upload-snapshot.ts +41 -0
- package/src/diff-comments-cli.ts +250 -0
- package/src/egress-allowlist-cli.ts +87 -13
- package/src/egress-policy.ts +24 -1
- package/src/error-response.ts +13 -2
- package/src/git-credential.ts +424 -9
- package/src/git-recovery-environment.ts +212 -0
- package/src/help.ts +125 -9
- package/src/hosted-boot.ts +47 -1
- package/src/hosted-bootstrap-rehearsal.ts +7 -0
- package/src/hosted-broker-server.ts +152 -134
- package/src/hosted-git-http-server.ts +64 -65
- package/src/hosted-runtime-assets.ts +155 -11
- package/src/hosted-session-cli.ts +618 -28
- package/src/index.ts +278 -244
- package/src/kill-switch-cli.ts +10 -0
- package/src/loopback-serve.ts +55 -0
- package/src/optimizations-list-cli.ts +137 -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/repository-backup-core.ts +2537 -0
- package/src/repository-recovery-manifest.ts +521 -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/start-intent.ts +41 -0
- package/src/workbench-cli.ts +223 -23
- package/src/workspace-sync.ts +4 -0
- package/src/zip-draft-push.ts +5 -2
- package/vendor/orizu-gepa-python/src/orizu_gepa/cli.py +81 -4
- package/vendor/orizu-gepa-python/src/orizu_gepa/client.py +4 -1
- 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 +41 -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
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ALI-1199 archive/restore commands and assignment inventory.
|
|
3
|
+
*
|
|
4
|
+
* Kept outside index.ts per the CLI line ratchet. The server owns
|
|
5
|
+
* authorization and idempotency; this module validates the CLI grammar and
|
|
6
|
+
* renders the canonical response without inventing client-side state.
|
|
7
|
+
*/
|
|
8
|
+
import { authedFetch } from './http.js';
|
|
9
|
+
import { extractErrorMessage } from './error-response.js';
|
|
10
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
11
|
+
const ARTIFACT_TYPES = {
|
|
12
|
+
app: 'app',
|
|
13
|
+
apps: 'app',
|
|
14
|
+
dataset: 'dataset',
|
|
15
|
+
datasets: 'dataset',
|
|
16
|
+
task: 'task',
|
|
17
|
+
tasks: 'task',
|
|
18
|
+
assignment: 'assignment',
|
|
19
|
+
assignments: 'assignment',
|
|
20
|
+
scorer: 'scorer',
|
|
21
|
+
scorers: 'scorer',
|
|
22
|
+
optimization: 'optimization',
|
|
23
|
+
optimizations: 'optimization',
|
|
24
|
+
};
|
|
25
|
+
function argValue(args, flag) {
|
|
26
|
+
const index = args.indexOf(flag);
|
|
27
|
+
if (index === -1 ||
|
|
28
|
+
index + 1 >= args.length ||
|
|
29
|
+
args[index + 1].startsWith('--')) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
return args[index + 1];
|
|
33
|
+
}
|
|
34
|
+
function sanitizeTerminalText(value) {
|
|
35
|
+
return String(value).replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/g, '');
|
|
36
|
+
}
|
|
37
|
+
function columnWidths(headers, rows) {
|
|
38
|
+
return headers.map((header, index) => Math.max(header.length, ...rows.map(row => row[index]?.length || 0)));
|
|
39
|
+
}
|
|
40
|
+
function printTable(headers, rows, print) {
|
|
41
|
+
const widths = columnWidths(headers, rows);
|
|
42
|
+
print(headers.map((header, index) => header.padEnd(widths[index])).join(' '));
|
|
43
|
+
print(widths.map(width => '-'.repeat(width)).join(' '));
|
|
44
|
+
rows.forEach(row => {
|
|
45
|
+
print(row.map((cell, index) => cell.padEnd(widths[index])).join(' '));
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
export function printTaskSummaries(items, print) {
|
|
49
|
+
if (items.length === 0)
|
|
50
|
+
return print('No tasks found.');
|
|
51
|
+
printTable(['TASK ID', 'TASK NAME', 'STATUS', 'ARCHIVE', 'TEAM/PROJECT'], items.map(item => [
|
|
52
|
+
sanitizeTerminalText(item.id),
|
|
53
|
+
sanitizeTerminalText(item.title || '-'),
|
|
54
|
+
sanitizeTerminalText(item.status || '-'),
|
|
55
|
+
sanitizeTerminalText(item.archiveStatus || 'active'),
|
|
56
|
+
item.teamSlug && item.projectSlug
|
|
57
|
+
? sanitizeTerminalText(`${item.teamSlug}/${item.projectSlug}`)
|
|
58
|
+
: 'unknown-project',
|
|
59
|
+
]), print);
|
|
60
|
+
}
|
|
61
|
+
export function printAppSummaries(items, print) {
|
|
62
|
+
if (items.length === 0)
|
|
63
|
+
return print('No apps found.');
|
|
64
|
+
printTable(['APP ID', 'APP NAME', 'VERSION', 'ARCHIVE'], items.map(item => [
|
|
65
|
+
sanitizeTerminalText(item.id),
|
|
66
|
+
sanitizeTerminalText(item.name || '-'),
|
|
67
|
+
`v${item.currentVersionNum || 1}`,
|
|
68
|
+
sanitizeTerminalText(item.status || 'active'),
|
|
69
|
+
]), print);
|
|
70
|
+
}
|
|
71
|
+
export function printDatasetSummaries(items, print) {
|
|
72
|
+
if (items.length === 0)
|
|
73
|
+
return print('No datasets found.');
|
|
74
|
+
printTable(['DATASET ID', 'DATASET', 'ROWS', 'ARCHIVE'], items.map(item => [
|
|
75
|
+
sanitizeTerminalText(item.id),
|
|
76
|
+
sanitizeTerminalText(item.name || '-'),
|
|
77
|
+
sanitizeTerminalText(item.rowCount),
|
|
78
|
+
sanitizeTerminalText(item.status || 'active'),
|
|
79
|
+
]), print);
|
|
80
|
+
}
|
|
81
|
+
export function printScorerSummaries(items, print) {
|
|
82
|
+
if (items.length === 0)
|
|
83
|
+
return print('No scorers found.');
|
|
84
|
+
printTable(['ID', 'NAME', 'MODE', 'METRIC', 'ARCHIVE', 'IMPLEMENTATION'], items.map(item => [
|
|
85
|
+
sanitizeTerminalText(item.id),
|
|
86
|
+
sanitizeTerminalText(item.name),
|
|
87
|
+
sanitizeTerminalText(item.mode),
|
|
88
|
+
sanitizeTerminalText(item.metricLabel),
|
|
89
|
+
sanitizeTerminalText(item.status || 'active'),
|
|
90
|
+
sanitizeTerminalText(item.implementationKind),
|
|
91
|
+
]), print);
|
|
92
|
+
}
|
|
93
|
+
async function parseJsonPayload(response, context) {
|
|
94
|
+
const contentType = response.headers.get('content-type') || '';
|
|
95
|
+
const rawBody = await response.text();
|
|
96
|
+
if (!contentType.includes('application/json')) {
|
|
97
|
+
throw new Error(`${context} returned non-JSON response (status ${response.status}). ` +
|
|
98
|
+
`Body preview: ${sanitizeTerminalText(rawBody.slice(0, 180))}`);
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
return JSON.parse(rawBody);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
throw new Error(`${context} returned invalid JSON (status ${response.status}). ` +
|
|
105
|
+
`Body preview: ${sanitizeTerminalText(rawBody.slice(0, 180))}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function parseStatus(args) {
|
|
109
|
+
const value = argValue(args, '--status') || 'active';
|
|
110
|
+
if (value !== 'active' && value !== 'archived' && value !== 'all') {
|
|
111
|
+
throw new Error('--status must be active, archived, or all');
|
|
112
|
+
}
|
|
113
|
+
return value;
|
|
114
|
+
}
|
|
115
|
+
function artifactUsage() {
|
|
116
|
+
return ('Usage: orizu <apps|datasets|tasks|assignments|scorers|optimizations> ' +
|
|
117
|
+
'<archive|restore> <id> [--project <team/project>] ' +
|
|
118
|
+
'[--assignee <user-id>] [--json]');
|
|
119
|
+
}
|
|
120
|
+
export async function archiveArtifactCommand(args, io) {
|
|
121
|
+
const artifactType = ARTIFACT_TYPES[args[0] || ''];
|
|
122
|
+
const action = args[1];
|
|
123
|
+
const artifactId = args[2];
|
|
124
|
+
if (!artifactType ||
|
|
125
|
+
(action !== 'archive' && action !== 'restore') ||
|
|
126
|
+
!artifactId ||
|
|
127
|
+
!UUID_PATTERN.test(artifactId)) {
|
|
128
|
+
throw new Error(artifactUsage());
|
|
129
|
+
}
|
|
130
|
+
const assigneeId = argValue(args, '--assignee');
|
|
131
|
+
if (assigneeId && !UUID_PATTERN.test(assigneeId)) {
|
|
132
|
+
throw new Error('--assignee must be a UUID');
|
|
133
|
+
}
|
|
134
|
+
if (artifactType !== 'assignment' && assigneeId) {
|
|
135
|
+
throw new Error('--assignee is only valid for assignment archive commands');
|
|
136
|
+
}
|
|
137
|
+
const projectArg = argValue(args, '--project');
|
|
138
|
+
const project = projectArg || await io.resolveProjectSlug(null);
|
|
139
|
+
const params = new URLSearchParams({ project });
|
|
140
|
+
const body = {
|
|
141
|
+
archived: action === 'archive',
|
|
142
|
+
...(assigneeId ? { assigneeId } : {}),
|
|
143
|
+
};
|
|
144
|
+
const fetcher = io.fetcher || authedFetch;
|
|
145
|
+
const response = await fetcher(`/api/cli/archive-artifacts/${artifactType}/${encodeURIComponent(artifactId)}` +
|
|
146
|
+
`?${params.toString()}`, {
|
|
147
|
+
method: 'PATCH',
|
|
148
|
+
headers: { 'content-type': 'application/json' },
|
|
149
|
+
body: JSON.stringify(body),
|
|
150
|
+
});
|
|
151
|
+
if (!response.ok) {
|
|
152
|
+
throw new Error(`Failed to ${action} ${artifactType}: ` +
|
|
153
|
+
await extractErrorMessage(response));
|
|
154
|
+
}
|
|
155
|
+
const data = await parseJsonPayload(response, `${action === 'archive' ? 'Archive' : 'Restore'} ${artifactType}`);
|
|
156
|
+
if (io.json) {
|
|
157
|
+
io.print(JSON.stringify(data));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const verb = action === 'archive' ? 'Archived' : 'Restored';
|
|
161
|
+
const assigneeSuffix = data.artifact.assigneeId
|
|
162
|
+
? ` for ${sanitizeTerminalText(data.artifact.assigneeId)}`
|
|
163
|
+
: '';
|
|
164
|
+
io.print(`${verb} ${sanitizeTerminalText(data.artifact.type)} ` +
|
|
165
|
+
`${sanitizeTerminalText(data.artifact.id)}${assigneeSuffix}.`);
|
|
166
|
+
}
|
|
167
|
+
export async function listAssignmentsCommand(args, io) {
|
|
168
|
+
const status = parseStatus(args);
|
|
169
|
+
const assigneeId = argValue(args, '--assignee');
|
|
170
|
+
if (assigneeId && !UUID_PATTERN.test(assigneeId)) {
|
|
171
|
+
throw new Error('--assignee must be a UUID');
|
|
172
|
+
}
|
|
173
|
+
const projectArg = argValue(args, '--project');
|
|
174
|
+
const project = projectArg || await io.resolveProjectSlug(null);
|
|
175
|
+
const params = new URLSearchParams({ project, status });
|
|
176
|
+
if (assigneeId)
|
|
177
|
+
params.set('assignee', assigneeId);
|
|
178
|
+
const fetcher = io.fetcher || authedFetch;
|
|
179
|
+
const response = await fetcher(`/api/cli/assignments?${params.toString()}`);
|
|
180
|
+
if (!response.ok) {
|
|
181
|
+
throw new Error(`Failed to list assignments: ${await extractErrorMessage(response)}`);
|
|
182
|
+
}
|
|
183
|
+
const data = await parseJsonPayload(response, 'Assignments list');
|
|
184
|
+
if (io.json) {
|
|
185
|
+
io.print(JSON.stringify(data));
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const rows = data.assignments || [];
|
|
189
|
+
if (rows.length === 0) {
|
|
190
|
+
io.print('No assignment task-groups found.');
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const printable = rows.map(row => ({
|
|
194
|
+
id: sanitizeTerminalText(row.taskId),
|
|
195
|
+
title: sanitizeTerminalText(row.taskTitle || '-'),
|
|
196
|
+
lifecycle: sanitizeTerminalText(row.taskStatus || '-'),
|
|
197
|
+
archive: sanitizeTerminalText(row.status || 'active'),
|
|
198
|
+
progress: `${row.completedAssignments}/${row.totalAssignments}`,
|
|
199
|
+
}));
|
|
200
|
+
const idWidth = Math.max('TASK ID'.length, ...printable.map(row => row.id.length));
|
|
201
|
+
const titleWidth = Math.max('TASK'.length, ...printable.map(row => row.title.length));
|
|
202
|
+
const lifecycleWidth = Math.max('LIFECYCLE'.length, ...printable.map(row => row.lifecycle.length));
|
|
203
|
+
const archiveWidth = Math.max('ARCHIVE'.length, ...printable.map(row => row.archive.length));
|
|
204
|
+
io.print(`${'TASK ID'.padEnd(idWidth)} ${'TASK'.padEnd(titleWidth)} ` +
|
|
205
|
+
`${'LIFECYCLE'.padEnd(lifecycleWidth)} ` +
|
|
206
|
+
`${'ARCHIVE'.padEnd(archiveWidth)} PROGRESS`);
|
|
207
|
+
io.print(`${'-'.repeat(idWidth)} ${'-'.repeat(titleWidth)} ` +
|
|
208
|
+
`${'-'.repeat(lifecycleWidth)} ${'-'.repeat(archiveWidth)} ` +
|
|
209
|
+
`${'-'.repeat('PROGRESS'.length)}`);
|
|
210
|
+
printable.forEach(row => {
|
|
211
|
+
io.print(`${row.id.padEnd(idWidth)} ${row.title.padEnd(titleWidth)} ` +
|
|
212
|
+
`${row.lifecycle.padEnd(lifecycleWidth)} ` +
|
|
213
|
+
`${row.archive.padEnd(archiveWidth)} ${row.progress}`);
|
|
214
|
+
});
|
|
215
|
+
}
|
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
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { extractErrorMessage } from './error-response.js';
|
|
2
|
+
import { parseDatasetFile } from './file-parser.js';
|
|
3
|
+
import { authedFetch } from './http.js';
|
|
4
|
+
const MAX_REQUEST_BYTES = 3 * 1024 * 1024;
|
|
5
|
+
const MAX_CHUNK_ROWS = 10;
|
|
6
|
+
const EMPTY_REQUEST_BYTES = Buffer.byteLength('{"rows":[]}', 'utf8');
|
|
7
|
+
function normalizeRows(rows) {
|
|
8
|
+
if (rows.length === 0) {
|
|
9
|
+
throw new Error('Dataset edit file must contain at least one row');
|
|
10
|
+
}
|
|
11
|
+
const seenRowIds = new Set();
|
|
12
|
+
return rows.map((row, index) => {
|
|
13
|
+
const rowId = typeof row.id === 'string' ? row.id.trim() : '';
|
|
14
|
+
if (!rowId) {
|
|
15
|
+
throw new Error(`Dataset edit file rows[${index}] must include a non-empty string id`);
|
|
16
|
+
}
|
|
17
|
+
if (seenRowIds.has(rowId)) {
|
|
18
|
+
throw new Error(`Dataset edit file rows[${index}].id duplicates a previous row id`);
|
|
19
|
+
}
|
|
20
|
+
seenRowIds.add(rowId);
|
|
21
|
+
return { ...row, id: rowId };
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
function chunkRows(rows, sanitize) {
|
|
25
|
+
const chunks = [];
|
|
26
|
+
let currentChunk = [];
|
|
27
|
+
let currentBytes = EMPTY_REQUEST_BYTES;
|
|
28
|
+
for (const row of rows) {
|
|
29
|
+
const rowBytes = Buffer.byteLength(JSON.stringify(row), 'utf8');
|
|
30
|
+
const framedRowBytes = rowBytes + (currentChunk.length > 0 ? 1 : 0);
|
|
31
|
+
if (currentChunk.length > 0 &&
|
|
32
|
+
(currentChunk.length >= MAX_CHUNK_ROWS || currentBytes + framedRowBytes > MAX_REQUEST_BYTES)) {
|
|
33
|
+
chunks.push(currentChunk);
|
|
34
|
+
currentChunk = [];
|
|
35
|
+
currentBytes = EMPTY_REQUEST_BYTES;
|
|
36
|
+
}
|
|
37
|
+
const singleRowRequestBytes = EMPTY_REQUEST_BYTES + rowBytes;
|
|
38
|
+
if (singleRowRequestBytes > MAX_REQUEST_BYTES) {
|
|
39
|
+
throw new Error(`Dataset edit row ${sanitize(row.id)} is ${singleRowRequestBytes} bytes, ` +
|
|
40
|
+
`which exceeds the ${MAX_REQUEST_BYTES}-byte edit request limit`);
|
|
41
|
+
}
|
|
42
|
+
currentChunk.push(row);
|
|
43
|
+
currentBytes += rowBytes + (currentChunk.length > 1 ? 1 : 0);
|
|
44
|
+
}
|
|
45
|
+
if (currentChunk.length > 0)
|
|
46
|
+
chunks.push(currentChunk);
|
|
47
|
+
return chunks;
|
|
48
|
+
}
|
|
49
|
+
export async function editDatasetRows(options) {
|
|
50
|
+
const { rows } = parseDatasetFile(options.file);
|
|
51
|
+
const chunks = chunkRows(normalizeRows(rows), options.sanitize);
|
|
52
|
+
let totalUpdated = 0;
|
|
53
|
+
let lastResult = null;
|
|
54
|
+
for (let index = 0; index < chunks.length; index += 1) {
|
|
55
|
+
const chunk = chunks[index];
|
|
56
|
+
if (!options.json && chunks.length > 1) {
|
|
57
|
+
options.printLine(`Updating chunk ${index + 1}/${chunks.length} (${chunk.length} rows)...`);
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
const response = await authedFetch(`/api/cli/datasets/${encodeURIComponent(options.datasetId)}/rows`, {
|
|
61
|
+
method: 'PATCH',
|
|
62
|
+
headers: { 'Content-Type': 'application/json' },
|
|
63
|
+
body: JSON.stringify({ rows: chunk }),
|
|
64
|
+
});
|
|
65
|
+
if (!response.ok) {
|
|
66
|
+
throw new Error(`HTTP ${response.status}: ${await extractErrorMessage(response)}`);
|
|
67
|
+
}
|
|
68
|
+
const data = await response.json();
|
|
69
|
+
if (!Number.isInteger(data.updatedCount) || data.updatedCount !== chunk.length) {
|
|
70
|
+
throw new Error(`Server reported ${String(data.updatedCount)} updated rows for a ${chunk.length}-row chunk`);
|
|
71
|
+
}
|
|
72
|
+
totalUpdated += data.updatedCount;
|
|
73
|
+
lastResult = data;
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
77
|
+
throw new Error(`Chunk ${index + 1}/${chunks.length} failed: ${message}\n` +
|
|
78
|
+
`${totalUpdated} rows from ${index} chunk(s) were already updated. ` +
|
|
79
|
+
'Retry with a file containing only the remaining row edits.');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (!lastResult)
|
|
83
|
+
throw new Error('Dataset edit file must contain at least one row');
|
|
84
|
+
if (options.json) {
|
|
85
|
+
options.printJson({ dataset: lastResult.dataset, updatedCount: totalUpdated });
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
options.printLine(`Updated ${totalUpdated} rows in dataset ${options.sanitize(lastResult.dataset.name)} ` +
|
|
89
|
+
`(${options.sanitize(lastResult.dataset.id)}). Current row count: ${lastResult.dataset.rowCount}`);
|
|
90
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export async function ensureDatasetUploadSnapshot(dataset, jsonOutput, createSnapshot) {
|
|
2
|
+
try {
|
|
3
|
+
return await createSnapshot(dataset.id);
|
|
4
|
+
}
|
|
5
|
+
catch (error) {
|
|
6
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
7
|
+
const retryCommand = `orizu datasets versions create --dataset ${dataset.id}`;
|
|
8
|
+
if (jsonOutput) {
|
|
9
|
+
throw new Error(JSON.stringify({
|
|
10
|
+
error: 'Dataset upload completed but dataset version creation failed',
|
|
11
|
+
detail,
|
|
12
|
+
upload_completed: true,
|
|
13
|
+
dataset_id: dataset.id,
|
|
14
|
+
dataset_version_id: null,
|
|
15
|
+
retry_command: retryCommand,
|
|
16
|
+
}));
|
|
17
|
+
}
|
|
18
|
+
throw new Error(`Dataset upload completed but dataset version creation failed: ${detail}\n` +
|
|
19
|
+
`Dataset ${dataset.name} (${dataset.id}) was created with ${dataset.rowCount} rows. ` +
|
|
20
|
+
`Retry only the snapshot step with: ${retryCommand}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
const DETAIL_LEVELS = ['hunk', 'diff', 'full'];
|
|
2
|
+
function isCandidatePair(pair) {
|
|
3
|
+
return 'candidateId' in pair.from;
|
|
4
|
+
}
|
|
5
|
+
function usage() {
|
|
6
|
+
return 'Usage: orizu comments diff (--run <run-id> | --prompt <prompt-id>) [--from <revision> --to <revision>] [--detail hunk|diff|full] [--json]';
|
|
7
|
+
}
|
|
8
|
+
function sanitizeHumanText(ctx, value) {
|
|
9
|
+
return ctx.sanitizeTerminalText(value).replaceAll('\r', '');
|
|
10
|
+
}
|
|
11
|
+
function sanitizeHumanInlineText(ctx, value) {
|
|
12
|
+
return sanitizeHumanText(ctx, value).replaceAll('\n', '\\n');
|
|
13
|
+
}
|
|
14
|
+
function humanLineNumber(ctx, line) {
|
|
15
|
+
return line === null ? '-' : sanitizeHumanText(ctx, String(line + 1));
|
|
16
|
+
}
|
|
17
|
+
function buildParams(ctx) {
|
|
18
|
+
const runId = ctx.getArg('--run');
|
|
19
|
+
const promptId = ctx.getArg('--prompt');
|
|
20
|
+
const from = ctx.getArg('--from');
|
|
21
|
+
const to = ctx.getArg('--to');
|
|
22
|
+
const detail = ctx.getArg('--detail') ?? 'diff';
|
|
23
|
+
for (const [name, value] of [
|
|
24
|
+
['--run', runId],
|
|
25
|
+
['--prompt', promptId],
|
|
26
|
+
['--from', from],
|
|
27
|
+
['--to', to],
|
|
28
|
+
['--detail', detail],
|
|
29
|
+
]) {
|
|
30
|
+
ctx.rejectDashPrefixedOptionValue(name, value);
|
|
31
|
+
}
|
|
32
|
+
if (Number(Boolean(runId)) + Number(Boolean(promptId)) !== 1) {
|
|
33
|
+
throw new Error(usage());
|
|
34
|
+
}
|
|
35
|
+
if (Boolean(from) !== Boolean(to)) {
|
|
36
|
+
throw new Error('--from and --to must be provided together');
|
|
37
|
+
}
|
|
38
|
+
if (!DETAIL_LEVELS.includes(detail)) {
|
|
39
|
+
throw new Error('--detail must be hunk, diff, or full');
|
|
40
|
+
}
|
|
41
|
+
if (promptId && from && to) {
|
|
42
|
+
if (!Number.isInteger(Number(from)) || !Number.isInteger(Number(to))) {
|
|
43
|
+
throw new Error('--from and --to must be integer prompt versions');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const params = new URLSearchParams();
|
|
47
|
+
params.set(runId ? 'run' : 'prompt', (runId ?? promptId));
|
|
48
|
+
if (from && to) {
|
|
49
|
+
params.set('from', from);
|
|
50
|
+
params.set('to', to);
|
|
51
|
+
}
|
|
52
|
+
params.set('detail', detail);
|
|
53
|
+
return params;
|
|
54
|
+
}
|
|
55
|
+
function printDiffComments(ctx, payload) {
|
|
56
|
+
ctx.printLine(`Diff comments: ${payload.pairs.length} pair${payload.pairs.length === 1 ? '' : 's'}`);
|
|
57
|
+
if (payload.pairs.length === 0) {
|
|
58
|
+
ctx.printLine('');
|
|
59
|
+
ctx.printLine('No diff comments found.');
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
for (const pair of payload.pairs) {
|
|
63
|
+
const hasCandidateIds = isCandidatePair(pair);
|
|
64
|
+
const from = hasCandidateIds ? pair.from.candidateId : String(pair.from.version);
|
|
65
|
+
const to = hasCandidateIds ? pair.to.candidateId : String(pair.to.version);
|
|
66
|
+
ctx.printLine('');
|
|
67
|
+
ctx.printLine(`${hasCandidateIds ? 'Candidates' : 'Versions'} ` +
|
|
68
|
+
`${sanitizeHumanInlineText(ctx, from)} → ${sanitizeHumanInlineText(ctx, to)}`);
|
|
69
|
+
if (pair.degraded) {
|
|
70
|
+
ctx.printLine(` Context unavailable: ${sanitizeHumanText(ctx, pair.degraded.reason)}`);
|
|
71
|
+
}
|
|
72
|
+
if (pair.diff !== null) {
|
|
73
|
+
ctx.printLine(' Diff:');
|
|
74
|
+
for (const line of sanitizeHumanText(ctx, pair.diff).split('\n')) {
|
|
75
|
+
ctx.printLine(` ${line}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (pair.bodies !== null) {
|
|
79
|
+
for (const [label, body] of [
|
|
80
|
+
['From body', pair.bodies.from],
|
|
81
|
+
['To body', pair.bodies.to],
|
|
82
|
+
]) {
|
|
83
|
+
ctx.printLine(` ${label}:`);
|
|
84
|
+
for (const line of sanitizeHumanText(ctx, body).split('\n')) {
|
|
85
|
+
ctx.printLine(` ${line}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
for (const comment of pair.comments) {
|
|
90
|
+
const authorName = comment.author?.person?.name || comment.author?.name || 'Unknown';
|
|
91
|
+
ctx.printLine(` ${sanitizeHumanInlineText(ctx, comment.id)} · ` +
|
|
92
|
+
`${sanitizeHumanInlineText(ctx, authorName)} · ` +
|
|
93
|
+
`${sanitizeHumanInlineText(ctx, comment.createdAt)} · ` +
|
|
94
|
+
`${sanitizeHumanInlineText(ctx, comment.anchor.side)} line ` +
|
|
95
|
+
`${sanitizeHumanInlineText(ctx, String(comment.anchor.line + 1))}`);
|
|
96
|
+
for (const line of sanitizeHumanText(ctx, comment.body).split('\n')) {
|
|
97
|
+
ctx.printLine(` ${line}`);
|
|
98
|
+
}
|
|
99
|
+
if (!comment.context) {
|
|
100
|
+
ctx.printLine(` Context unavailable: ` +
|
|
101
|
+
`${sanitizeHumanText(ctx, comment.contextUnavailableReason ?? 'body_unresolvable')}`);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
for (const line of comment.context.hunk.lines) {
|
|
105
|
+
const prefix = line.op === 'add' ? '+' : line.op === 'del' ? '-' : ' ';
|
|
106
|
+
const isAnchored = comment.anchor.side === 'old'
|
|
107
|
+
? line.oldLine === comment.anchor.line
|
|
108
|
+
: line.newLine === comment.anchor.line;
|
|
109
|
+
const marker = isAnchored ? '>' : ' ';
|
|
110
|
+
ctx.printLine(` ${marker} old:${humanLineNumber(ctx, line.oldLine)} ` +
|
|
111
|
+
`new:${humanLineNumber(ctx, line.newLine)} | ` +
|
|
112
|
+
`${prefix}${sanitizeHumanText(ctx, line.text)}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
export async function diffCommentsCommand(ctx) {
|
|
118
|
+
const params = buildParams(ctx);
|
|
119
|
+
const response = await ctx.authedFetch(`/api/cli/diff-comments?${params.toString()}`);
|
|
120
|
+
if (!response.ok) {
|
|
121
|
+
let message = `HTTP ${response.status}`;
|
|
122
|
+
try {
|
|
123
|
+
const payload = await response.clone().json();
|
|
124
|
+
if (typeof payload.error === 'string' && payload.error.trim()) {
|
|
125
|
+
message = sanitizeHumanInlineText(ctx, payload.error);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// Keep the status-only error for non-JSON responses.
|
|
130
|
+
}
|
|
131
|
+
throw new Error(`Failed to fetch diff comments: ${message}`);
|
|
132
|
+
}
|
|
133
|
+
const payload = await ctx.parseJsonResponse(response, 'Diff comments');
|
|
134
|
+
if (ctx.hasJsonFlag()) {
|
|
135
|
+
ctx.printJson(payload);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
printDiffComments(ctx, payload);
|
|
139
|
+
}
|
|
@@ -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/egress-policy.js
CHANGED
|
@@ -117,6 +117,17 @@ export function baseEgressDomains(opts = {}) {
|
|
|
117
117
|
}
|
|
118
118
|
for (const d of GIT_HOST_DOMAINS)
|
|
119
119
|
domains.add(d);
|
|
120
|
+
for (const d of opts.repositoryHosts ?? []) {
|
|
121
|
+
const normalized = normalizeEgressDomain(d);
|
|
122
|
+
// Repository hosts come from a validated remote and must remain exact:
|
|
123
|
+
// no URL normalization, leading wildcard, path, port, or case drift is
|
|
124
|
+
// accepted at this final firewall seam.
|
|
125
|
+
if (normalized &&
|
|
126
|
+
!normalized.startsWith('*.') &&
|
|
127
|
+
normalized === d.trim().toLowerCase()) {
|
|
128
|
+
domains.add(normalized);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
120
131
|
return [...domains];
|
|
121
132
|
}
|
|
122
133
|
/**
|
|
@@ -136,6 +147,7 @@ export function buildEgressPolicy(opts = {}) {
|
|
|
136
147
|
orizuBaseUrl: opts.orizuBaseUrl,
|
|
137
148
|
modelHost: brokerHost,
|
|
138
149
|
modelHosts: opts.modelHosts,
|
|
150
|
+
repositoryHosts: opts.repositoryHosts,
|
|
139
151
|
});
|
|
140
152
|
const allow = {};
|
|
141
153
|
for (const domain of base)
|