@jphutchins/code-review 0.1.0-alpha.36 → 0.1.0-alpha.38
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/index.js +327 -152
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/schema/VERSIONING.md +2 -1
- package/schema/findings.schema.json +2 -2
- package/templates/comment.eta +11 -0
package/dist/index.js
CHANGED
|
@@ -4,14 +4,151 @@ import { readFileSync, writeFileSync, statSync, copyFileSync, readdirSync } from
|
|
|
4
4
|
import { randomBytes } from 'crypto';
|
|
5
5
|
import { resolve as resolve$1, join, dirname, basename, extname } from 'path';
|
|
6
6
|
import { Eta } from 'eta';
|
|
7
|
+
import * as t from 'io-ts';
|
|
7
8
|
import parseDiff from 'parse-diff';
|
|
8
9
|
import { Ajv2020 } from 'ajv/dist/2020.js';
|
|
9
10
|
import _addFormats from 'ajv-formats';
|
|
10
|
-
import * as t from 'io-ts';
|
|
11
11
|
import { execFile } from 'child_process';
|
|
12
12
|
import { performance } from 'perf_hooks';
|
|
13
13
|
import { PathReporter } from 'io-ts/lib/PathReporter.js';
|
|
14
14
|
|
|
15
|
+
var SeverityCodec = t.union([
|
|
16
|
+
t.literal("critical"),
|
|
17
|
+
t.literal("major"),
|
|
18
|
+
t.literal("minor"),
|
|
19
|
+
t.literal("nit")
|
|
20
|
+
]);
|
|
21
|
+
var SideCodec = t.union([t.literal("RIGHT"), t.literal("LEFT")]);
|
|
22
|
+
var VerdictCodec = t.union([
|
|
23
|
+
t.literal("approve"),
|
|
24
|
+
t.literal("comment"),
|
|
25
|
+
t.literal("changes"),
|
|
26
|
+
t.literal("error")
|
|
27
|
+
]);
|
|
28
|
+
var LineNumber = t.refinement(
|
|
29
|
+
t.number,
|
|
30
|
+
(n) => Number.isInteger(n) && n >= 1,
|
|
31
|
+
"LineNumber"
|
|
32
|
+
);
|
|
33
|
+
var Confidence = t.refinement(t.number, (n) => n >= 0 && n <= 1, "Confidence");
|
|
34
|
+
var SCHEMA_VERSION_RE = /^(0|[1-9]\d*)\.(\d+)\.(\d+)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
35
|
+
var SchemaVersion = t.refinement(
|
|
36
|
+
t.string,
|
|
37
|
+
(s) => SCHEMA_VERSION_RE.test(s),
|
|
38
|
+
"SchemaVersion"
|
|
39
|
+
);
|
|
40
|
+
var FindingShape = t.intersection([
|
|
41
|
+
t.type({
|
|
42
|
+
path: t.string,
|
|
43
|
+
start_line: LineNumber,
|
|
44
|
+
end_line: LineNumber,
|
|
45
|
+
severity: SeverityCodec,
|
|
46
|
+
title: t.string,
|
|
47
|
+
description: t.string,
|
|
48
|
+
reasoning: t.string,
|
|
49
|
+
confidence: Confidence
|
|
50
|
+
}),
|
|
51
|
+
t.partial({
|
|
52
|
+
side: SideCodec,
|
|
53
|
+
code: t.string,
|
|
54
|
+
code_url: t.string,
|
|
55
|
+
recommendation: t.string,
|
|
56
|
+
patch: t.string
|
|
57
|
+
})
|
|
58
|
+
]);
|
|
59
|
+
var EndGeStart = t.refinement(
|
|
60
|
+
FindingShape,
|
|
61
|
+
(f) => f.end_line >= f.start_line,
|
|
62
|
+
"EndGeStart"
|
|
63
|
+
);
|
|
64
|
+
var FindingCodec = t.exact(EndGeStart);
|
|
65
|
+
var FindingsCodec = t.exact(
|
|
66
|
+
t.type({
|
|
67
|
+
schema_version: SchemaVersion,
|
|
68
|
+
summary: t.string,
|
|
69
|
+
verdict: VerdictCodec,
|
|
70
|
+
findings: t.array(FindingCodec)
|
|
71
|
+
})
|
|
72
|
+
);
|
|
73
|
+
var TriageCodec = t.type({
|
|
74
|
+
safe: t.boolean,
|
|
75
|
+
reasons: t.string
|
|
76
|
+
});
|
|
77
|
+
var TokenCount = t.refinement(
|
|
78
|
+
t.number,
|
|
79
|
+
(n) => Number.isInteger(n) && n >= 0,
|
|
80
|
+
"TokenCount"
|
|
81
|
+
);
|
|
82
|
+
var ModelUsageEntryCodec = t.intersection([
|
|
83
|
+
t.type({
|
|
84
|
+
model: t.string,
|
|
85
|
+
input_tokens: TokenCount,
|
|
86
|
+
output_tokens: TokenCount
|
|
87
|
+
}),
|
|
88
|
+
t.partial({
|
|
89
|
+
cache_read_tokens: TokenCount,
|
|
90
|
+
cache_write_tokens: TokenCount
|
|
91
|
+
})
|
|
92
|
+
]);
|
|
93
|
+
var ResultEnvelopeCodec = t.intersection([
|
|
94
|
+
t.type({
|
|
95
|
+
schema_version: t.string,
|
|
96
|
+
findings: FindingsCodec,
|
|
97
|
+
models: t.array(ModelUsageEntryCodec),
|
|
98
|
+
turns: TokenCount,
|
|
99
|
+
duration_ms: TokenCount
|
|
100
|
+
}),
|
|
101
|
+
t.partial({
|
|
102
|
+
vendor_cost_usd: t.union([t.number, t.null]),
|
|
103
|
+
route: t.string,
|
|
104
|
+
effort: t.string,
|
|
105
|
+
// The run produced a notice rather than a completed review (security-gate block, agent kill, no
|
|
106
|
+
// recoverable findings). An empty `findings` array alone can't say this — a genuine clean review
|
|
107
|
+
// is also empty — so the render suppresses "clean review" and the sticky precedence guard refuses
|
|
108
|
+
// to bury a completed review under it. Absent ⇒ a completed review.
|
|
109
|
+
incomplete: t.boolean
|
|
110
|
+
})
|
|
111
|
+
]);
|
|
112
|
+
var ModelPricesCodec = t.type({
|
|
113
|
+
in: t.number,
|
|
114
|
+
out: t.number,
|
|
115
|
+
cache_read: t.number,
|
|
116
|
+
cache_write: t.number
|
|
117
|
+
});
|
|
118
|
+
var PriceMapCodec = t.type({
|
|
119
|
+
_updated: t.string,
|
|
120
|
+
_unit: t.string,
|
|
121
|
+
models: t.record(t.string, ModelPricesCodec)
|
|
122
|
+
});
|
|
123
|
+
var TestFailureCodec = t.intersection([
|
|
124
|
+
t.type({ name: t.string }),
|
|
125
|
+
t.partial({ message: t.string })
|
|
126
|
+
]);
|
|
127
|
+
var TestSummaryCodec = t.intersection([
|
|
128
|
+
t.type({
|
|
129
|
+
passed: t.number,
|
|
130
|
+
failed: t.number,
|
|
131
|
+
total: t.number
|
|
132
|
+
}),
|
|
133
|
+
t.partial({
|
|
134
|
+
failures: t.array(TestFailureCodec)
|
|
135
|
+
})
|
|
136
|
+
]);
|
|
137
|
+
var DEFAULT_SCHEMA_VERSION = "0.5.0";
|
|
138
|
+
var emptyFindings = (summary) => ({
|
|
139
|
+
schema_version: DEFAULT_SCHEMA_VERSION,
|
|
140
|
+
summary,
|
|
141
|
+
verdict: "comment",
|
|
142
|
+
findings: []
|
|
143
|
+
});
|
|
144
|
+
var incompleteFindings = (summary) => ({
|
|
145
|
+
schema_version: DEFAULT_SCHEMA_VERSION,
|
|
146
|
+
summary,
|
|
147
|
+
verdict: "error",
|
|
148
|
+
findings: []
|
|
149
|
+
});
|
|
150
|
+
var isIncompleteFindings = (findings) => findings.verdict === "error" && findings.findings.length === 0;
|
|
151
|
+
|
|
15
152
|
// src/cost.ts
|
|
16
153
|
var defaultWarn = (message) => {
|
|
17
154
|
process.stderr.write(`${message}
|
|
@@ -179,6 +316,13 @@ var encodeMarker = (document, jsonUrl, limit) => {
|
|
|
179
316
|
return marker ? `${AGENTS_STOP_DIRECTIVE}
|
|
180
317
|
${marker}` : "";
|
|
181
318
|
};
|
|
319
|
+
var decodeBase64Json = (b64) => {
|
|
320
|
+
try {
|
|
321
|
+
return JSON.parse(Buffer.from(b64, "base64").toString("utf-8"));
|
|
322
|
+
} catch {
|
|
323
|
+
return void 0;
|
|
324
|
+
}
|
|
325
|
+
};
|
|
182
326
|
var findingsPointer = (findings, jsonUrl, limit = EMBED_LIMIT) => encodeMarker(findings, jsonUrl, limit);
|
|
183
327
|
var findingPointer = (finding, schemaVersion, jsonUrl, limit = EMBED_LIMIT) => encodeMarker({ schema_version: schemaVersion, findings: [finding] }, jsonUrl, limit);
|
|
184
328
|
var ZERO_SHA = "0000000000000000000000000000000000000000";
|
|
@@ -189,21 +333,48 @@ var parseReviewedSha = (body) => {
|
|
|
189
333
|
var REVIEW_COMPLETE_MARKER = "<!-- review-complete -->";
|
|
190
334
|
var parseReviewComplete = (body) => body.includes(REVIEW_COMPLETE_MARKER);
|
|
191
335
|
var parseFindingsMarker = (body) => {
|
|
192
|
-
const
|
|
193
|
-
const b64 = match?.[1];
|
|
336
|
+
const b64 = /<!-- code-review:findings-json;base64 ([A-Za-z0-9+/=]+) -->/.exec(body)?.[1];
|
|
194
337
|
if (b64 === void 0) return null;
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
338
|
+
return decodeBase64Json(b64) ?? null;
|
|
339
|
+
};
|
|
340
|
+
var ROUNDS_RE = /<!-- code-review:rounds;base64 ([A-Za-z0-9+/=]+) -->/;
|
|
341
|
+
var SEVERITIES = ["critical", "major", "minor", "nit"];
|
|
342
|
+
var isSeverityCounts = (u) => typeof u === "object" && u !== null && SEVERITIES.every((k) => {
|
|
343
|
+
const v = u[k];
|
|
344
|
+
return typeof v === "number" && Number.isSafeInteger(v) && v >= 0;
|
|
345
|
+
});
|
|
346
|
+
var parseRounds = (body) => {
|
|
347
|
+
const b64 = ROUNDS_RE.exec(body)?.[1];
|
|
348
|
+
if (b64 === void 0) return [];
|
|
349
|
+
const decoded = decodeBase64Json(b64);
|
|
350
|
+
return Array.isArray(decoded) ? decoded.filter(isSeverityCounts) : [];
|
|
351
|
+
};
|
|
352
|
+
var roundsMarker = (rounds) => rounds.length === 0 ? "" : `<!-- code-review:rounds;base64 ${Buffer.from(JSON.stringify(rounds), "utf-8").toString("base64")} -->`;
|
|
353
|
+
var roundChip = (c) => {
|
|
354
|
+
const parts = SEVERITIES.filter((k) => c[k] > 0).map((k) => `${severityEmoji(k)}${String(c[k])}`);
|
|
355
|
+
return parts.length === 0 ? "clean" : parts.join(" ");
|
|
356
|
+
};
|
|
357
|
+
var TRAJECTORY_CHIPS = 8;
|
|
358
|
+
var roundsSummary = (rounds) => {
|
|
359
|
+
if (rounds.length === 0) return "";
|
|
360
|
+
const chips = rounds.slice(-TRAJECTORY_CHIPS).map(roundChip);
|
|
361
|
+
const trajectory = rounds.length > TRAJECTORY_CHIPS ? `\u2026 \u2192 ${chips.join(" \u2192 ")}` : chips.join(" \u2192 ");
|
|
362
|
+
return `**Round ${String(rounds.length)}** \xB7 ${trajectory}`;
|
|
363
|
+
};
|
|
364
|
+
var CONVERGENCE_WEIGHTS = { critical: 4, major: 2, minor: 1, nit: 0 };
|
|
365
|
+
var DEFAULT_CONVERGENCE_THRESHOLD = 1;
|
|
366
|
+
var convergenceScore = (counts) => SEVERITIES.reduce((sum, k) => sum + counts[k] * CONVERGENCE_WEIGHTS[k], 0);
|
|
367
|
+
var convergenceSummary = (counts, threshold = DEFAULT_CONVERGENCE_THRESHOLD) => {
|
|
368
|
+
const score = convergenceScore(counts);
|
|
369
|
+
return score <= threshold ? `**Convergence** \u{1F3C1} ${String(score)} \u2264 ${String(threshold)} \u2014 converged` : `**Convergence** \u{1F504} ${String(score)} > ${String(threshold)} \u2014 iterating`;
|
|
200
370
|
};
|
|
201
371
|
var carryForwardMarkers = (body) => {
|
|
202
372
|
const findings = /<!-- code-review:findings-json[^>]*-->/.exec(body)?.[0];
|
|
203
373
|
const reviewedSha = /<!-- reviewed-sha: [0-9a-fA-F]{40} -->/.exec(body)?.[0];
|
|
374
|
+
const rounds = ROUNDS_RE.exec(body)?.[0];
|
|
204
375
|
const findingsBlock = findings ? `${AGENTS_STOP_DIRECTIVE}
|
|
205
376
|
${findings}` : void 0;
|
|
206
|
-
return [findingsBlock, reviewedSha].filter((m) => m !== void 0).join("\n\n");
|
|
377
|
+
return [findingsBlock, reviewedSha, rounds].filter((m) => m !== void 0).join("\n\n");
|
|
207
378
|
};
|
|
208
379
|
var escapeFence = (text) => text.replace(/```/g, "`` ` ``");
|
|
209
380
|
var projectPatch = (patch) => {
|
|
@@ -239,16 +410,20 @@ var computeSeverityCounts = (findings) => findings.reduce(
|
|
|
239
410
|
(acc, f) => f.severity in acc ? { ...acc, [f.severity]: acc[f.severity] + 1 } : acc,
|
|
240
411
|
emptySeverityCounts()
|
|
241
412
|
);
|
|
413
|
+
var isConvergenceRound = (route, incomplete) => route === "full review" && !incomplete;
|
|
242
414
|
var render = (input) => {
|
|
243
415
|
const eta = new Eta({ autoTrim: false });
|
|
244
416
|
const usageAvailable = input.envelope !== null;
|
|
245
417
|
const hasUsage = input.envelope !== null && input.envelope.models.length > 0;
|
|
246
|
-
const incomplete = input.incomplete ?? input.envelope?.incomplete ?? false;
|
|
418
|
+
const incomplete = (input.incomplete ?? input.envelope?.incomplete ?? false) || isIncompleteFindings(input.findings);
|
|
247
419
|
const costReport = input.envelope ? computeCost(input.envelope.models, input.prices) : null;
|
|
248
420
|
const pricesProvided = input.pricesProvided ?? true;
|
|
249
421
|
const route = input.route ?? input.envelope?.route ?? null;
|
|
250
422
|
const effort = input.effort ?? input.envelope?.effort ?? null;
|
|
251
423
|
const modelNames = input.envelope ? input.envelope.models.map((m) => m.model).join(", ") : "";
|
|
424
|
+
const severityCounts = input.severityCounts ?? computeSeverityCounts(input.findings.findings);
|
|
425
|
+
const rounds = input.rounds ?? [];
|
|
426
|
+
const isFullReviewRound = (input.convergenceRound ?? isConvergenceRound(route, incomplete)) && input.findings.verdict !== "error";
|
|
252
427
|
return eta.renderString(input.template, {
|
|
253
428
|
findings: input.findings,
|
|
254
429
|
envelope: input.envelope,
|
|
@@ -263,13 +438,16 @@ var render = (input) => {
|
|
|
263
438
|
testReport: input.testReport ?? null,
|
|
264
439
|
reviewedSha: input.reviewedSha ?? "0000000000000000000000000000000000000000",
|
|
265
440
|
postedAt: input.postedAt ?? "",
|
|
266
|
-
severityCounts
|
|
441
|
+
severityCounts,
|
|
442
|
+
convergenceSummary: isFullReviewRound ? convergenceSummary(severityCounts, input.convergenceThreshold) : "",
|
|
267
443
|
strays: (input.strays ?? []).map(sanitizeFinding),
|
|
268
444
|
unanchoredCount: input.unanchoredCount ?? 0,
|
|
269
445
|
inlineDisposition: input.inlineDisposition ?? null,
|
|
270
446
|
runUrl: input.runUrl ?? null,
|
|
271
447
|
jsonUrl: input.jsonUrl ?? null,
|
|
272
448
|
findingsPointer: input.findingsPointer ?? findingsPointer(input.findings, input.jsonUrl),
|
|
449
|
+
roundsMarker: roundsMarker(rounds),
|
|
450
|
+
roundsSummary: roundsSummary(rounds),
|
|
273
451
|
reviewUrl: input.reviewUrl ?? null,
|
|
274
452
|
formatTokens: (n) => Number.isFinite(n) && n >= 0 ? n.toLocaleString("en-US") : "\u2014",
|
|
275
453
|
// N/A (never a false $0.00) when no real price map was provided — real tokens, no rates to price them.
|
|
@@ -287,6 +465,8 @@ var render = (input) => {
|
|
|
287
465
|
return "\u{1F4AC} comment";
|
|
288
466
|
case "changes":
|
|
289
467
|
return "\u{1F527} changes requested";
|
|
468
|
+
case "error":
|
|
469
|
+
return "\u{1F6E0}\uFE0F no review verdict";
|
|
290
470
|
default:
|
|
291
471
|
return `\u2753 ${v}`;
|
|
292
472
|
}
|
|
@@ -525,132 +705,6 @@ var readTranscriptTree = (mainPath) => {
|
|
|
525
705
|
missing: mainText === null
|
|
526
706
|
};
|
|
527
707
|
};
|
|
528
|
-
var SeverityCodec = t.union([
|
|
529
|
-
t.literal("critical"),
|
|
530
|
-
t.literal("major"),
|
|
531
|
-
t.literal("minor"),
|
|
532
|
-
t.literal("nit")
|
|
533
|
-
]);
|
|
534
|
-
var SideCodec = t.union([t.literal("RIGHT"), t.literal("LEFT")]);
|
|
535
|
-
var VerdictCodec = t.union([t.literal("approve"), t.literal("comment"), t.literal("changes")]);
|
|
536
|
-
var LineNumber = t.refinement(
|
|
537
|
-
t.number,
|
|
538
|
-
(n) => Number.isInteger(n) && n >= 1,
|
|
539
|
-
"LineNumber"
|
|
540
|
-
);
|
|
541
|
-
var Confidence = t.refinement(t.number, (n) => n >= 0 && n <= 1, "Confidence");
|
|
542
|
-
var SCHEMA_VERSION_RE = /^(0|[1-9]\d*)\.(\d+)\.(\d+)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
543
|
-
var SchemaVersion = t.refinement(
|
|
544
|
-
t.string,
|
|
545
|
-
(s) => SCHEMA_VERSION_RE.test(s),
|
|
546
|
-
"SchemaVersion"
|
|
547
|
-
);
|
|
548
|
-
var FindingShape = t.intersection([
|
|
549
|
-
t.type({
|
|
550
|
-
path: t.string,
|
|
551
|
-
start_line: LineNumber,
|
|
552
|
-
end_line: LineNumber,
|
|
553
|
-
severity: SeverityCodec,
|
|
554
|
-
title: t.string,
|
|
555
|
-
description: t.string,
|
|
556
|
-
reasoning: t.string,
|
|
557
|
-
confidence: Confidence
|
|
558
|
-
}),
|
|
559
|
-
t.partial({
|
|
560
|
-
side: SideCodec,
|
|
561
|
-
code: t.string,
|
|
562
|
-
code_url: t.string,
|
|
563
|
-
recommendation: t.string,
|
|
564
|
-
patch: t.string
|
|
565
|
-
})
|
|
566
|
-
]);
|
|
567
|
-
var EndGeStart = t.refinement(
|
|
568
|
-
FindingShape,
|
|
569
|
-
(f) => f.end_line >= f.start_line,
|
|
570
|
-
"EndGeStart"
|
|
571
|
-
);
|
|
572
|
-
var FindingCodec = t.exact(EndGeStart);
|
|
573
|
-
var FindingsCodec = t.exact(
|
|
574
|
-
t.type({
|
|
575
|
-
schema_version: SchemaVersion,
|
|
576
|
-
summary: t.string,
|
|
577
|
-
verdict: VerdictCodec,
|
|
578
|
-
findings: t.array(FindingCodec)
|
|
579
|
-
})
|
|
580
|
-
);
|
|
581
|
-
var TriageCodec = t.type({
|
|
582
|
-
safe: t.boolean,
|
|
583
|
-
reasons: t.string
|
|
584
|
-
});
|
|
585
|
-
var TokenCount = t.refinement(
|
|
586
|
-
t.number,
|
|
587
|
-
(n) => Number.isInteger(n) && n >= 0,
|
|
588
|
-
"TokenCount"
|
|
589
|
-
);
|
|
590
|
-
var ModelUsageEntryCodec = t.intersection([
|
|
591
|
-
t.type({
|
|
592
|
-
model: t.string,
|
|
593
|
-
input_tokens: TokenCount,
|
|
594
|
-
output_tokens: TokenCount
|
|
595
|
-
}),
|
|
596
|
-
t.partial({
|
|
597
|
-
cache_read_tokens: TokenCount,
|
|
598
|
-
cache_write_tokens: TokenCount
|
|
599
|
-
})
|
|
600
|
-
]);
|
|
601
|
-
var ResultEnvelopeCodec = t.intersection([
|
|
602
|
-
t.type({
|
|
603
|
-
schema_version: t.string,
|
|
604
|
-
findings: FindingsCodec,
|
|
605
|
-
models: t.array(ModelUsageEntryCodec),
|
|
606
|
-
turns: TokenCount,
|
|
607
|
-
duration_ms: TokenCount
|
|
608
|
-
}),
|
|
609
|
-
t.partial({
|
|
610
|
-
vendor_cost_usd: t.union([t.number, t.null]),
|
|
611
|
-
route: t.string,
|
|
612
|
-
effort: t.string,
|
|
613
|
-
// The run produced a notice rather than a completed review (security-gate block, agent kill, no
|
|
614
|
-
// recoverable findings). An empty `findings` array alone can't say this — a genuine clean review
|
|
615
|
-
// is also empty — so the render suppresses "clean review" and the sticky precedence guard refuses
|
|
616
|
-
// to bury a completed review under it. Absent ⇒ a completed review.
|
|
617
|
-
incomplete: t.boolean
|
|
618
|
-
})
|
|
619
|
-
]);
|
|
620
|
-
var ModelPricesCodec = t.type({
|
|
621
|
-
in: t.number,
|
|
622
|
-
out: t.number,
|
|
623
|
-
cache_read: t.number,
|
|
624
|
-
cache_write: t.number
|
|
625
|
-
});
|
|
626
|
-
var PriceMapCodec = t.type({
|
|
627
|
-
_updated: t.string,
|
|
628
|
-
_unit: t.string,
|
|
629
|
-
models: t.record(t.string, ModelPricesCodec)
|
|
630
|
-
});
|
|
631
|
-
var TestFailureCodec = t.intersection([
|
|
632
|
-
t.type({ name: t.string }),
|
|
633
|
-
t.partial({ message: t.string })
|
|
634
|
-
]);
|
|
635
|
-
var TestSummaryCodec = t.intersection([
|
|
636
|
-
t.type({
|
|
637
|
-
passed: t.number,
|
|
638
|
-
failed: t.number,
|
|
639
|
-
total: t.number
|
|
640
|
-
}),
|
|
641
|
-
t.partial({
|
|
642
|
-
failures: t.array(TestFailureCodec)
|
|
643
|
-
})
|
|
644
|
-
]);
|
|
645
|
-
var DEFAULT_SCHEMA_VERSION = "0.4.0";
|
|
646
|
-
var noticeFindings = (summary) => ({
|
|
647
|
-
schema_version: DEFAULT_SCHEMA_VERSION,
|
|
648
|
-
summary,
|
|
649
|
-
verdict: "comment",
|
|
650
|
-
findings: []
|
|
651
|
-
});
|
|
652
|
-
|
|
653
|
-
// src/validate.ts
|
|
654
708
|
var addFormats = _addFormats;
|
|
655
709
|
var validatorCache = /* @__PURE__ */ new Map();
|
|
656
710
|
var compileSchema = (schemaPath) => {
|
|
@@ -990,11 +1044,24 @@ var NOTICE_KINDS = [
|
|
|
990
1044
|
"no-output"
|
|
991
1045
|
];
|
|
992
1046
|
var isNoticeKind = (s) => NOTICE_KINDS.some((k) => k === s);
|
|
1047
|
+
var UNSAFE_IN_SUMMARY = /[\n\r`<>|]/;
|
|
1048
|
+
var MAX_NAMED_HOSTS = 20;
|
|
1049
|
+
var parseAgentAllowlist = (sandboxConfig) => {
|
|
1050
|
+
const domains = sandboxConfig?.network?.allowedDomains;
|
|
1051
|
+
return Array.isArray(domains) ? domains.filter(
|
|
1052
|
+
(d) => typeof d === "string" && d.length <= 256 && !UNSAFE_IN_SUMMARY.test(d)
|
|
1053
|
+
).slice(0, MAX_NAMED_HOSTS) : [];
|
|
1054
|
+
};
|
|
1055
|
+
var egressNote = (agentAllowlist) => agentAllowlist.length === 0 ? "" : `
|
|
1056
|
+
|
|
1057
|
+
If the review failed because the agent could not reach a host it needed, note that its network egress is jailed to only ${agentAllowlist.map((host) => `\`${host}\``).join(
|
|
1058
|
+
", "
|
|
1059
|
+
)}. Add any missing host to the agent's egress allowlist \u2014 the reusable workflow's \`extra_endpoints\` input, or a single-file workflow's \`--extra\` flag on \`sandbox-config\`.`;
|
|
993
1060
|
var blockquote = (text) => text.replaceAll("\n", "\n> ");
|
|
994
1061
|
var reasoned = (lead, noReason, reasons) => typeof reasons === "string" && reasons.trim() !== "" ? `${lead}
|
|
995
1062
|
|
|
996
1063
|
> ${blockquote(reasons)}` : noReason;
|
|
997
|
-
var noticeSummary = (kind, reasons) => {
|
|
1064
|
+
var noticeSummary = (kind, reasons, agentAllowlist) => {
|
|
998
1065
|
switch (kind) {
|
|
999
1066
|
case "security-blocked":
|
|
1000
1067
|
return reasoned(
|
|
@@ -1007,25 +1074,25 @@ var noticeSummary = (kind, reasons) => {
|
|
|
1007
1074
|
"### \u{1F6E0}\uFE0F Security gate could not evaluate\n\nThe security triage could not produce a verdict (operational error), so the review failed closed \u2014 this is an infrastructure failure, not a finding about this diff. Re-run to retry a transient fault; a persistent one is a configuration issue (see the workflow logs). The triage step reported:",
|
|
1008
1075
|
"### \u{1F6E0}\uFE0F Security gate could not evaluate\n\nThe security triage could not produce a verdict (operational error), so the review failed closed \u2014 this is an infrastructure failure, not a finding about this diff. Re-run to retry a transient fault; a persistent one is a configuration issue (see the workflow logs).",
|
|
1009
1076
|
reasons
|
|
1010
|
-
);
|
|
1077
|
+
) + egressNote(agentAllowlist);
|
|
1011
1078
|
case "setup-failed":
|
|
1012
1079
|
return "### \u{1F6E0}\uFE0F Review did not run\n\nThe review job failed before the security triage could run (e.g. dependency install or environment setup). See the workflow logs \u2014 this is an infrastructure failure, not a security verdict.";
|
|
1013
1080
|
case "checkout-failed":
|
|
1014
1081
|
return "### \u26A0\uFE0F Could not check out the PR head\n\nThe PR head commit could not be fetched or checked out (it may have been force-pushed away, or is otherwise unavailable), so the review was skipped rather than run against the wrong tree. See workflow logs.";
|
|
1015
1082
|
case "no-output":
|
|
1016
|
-
return "### \u26A0\uFE0F Review did not complete\n\nThe diff passed triage but the review produced no output. See workflow logs.";
|
|
1083
|
+
return "### \u26A0\uFE0F Review did not complete\n\nThe diff passed triage but the review produced no output. See workflow logs." + egressNote(agentAllowlist);
|
|
1017
1084
|
}
|
|
1018
1085
|
};
|
|
1019
1086
|
var noticeEnvelope = (summary) => ({
|
|
1020
1087
|
schema_version: DEFAULT_SCHEMA_VERSION,
|
|
1021
|
-
findings:
|
|
1088
|
+
findings: incompleteFindings(summary),
|
|
1022
1089
|
models: [],
|
|
1023
1090
|
turns: 0,
|
|
1024
1091
|
duration_ms: 0,
|
|
1025
1092
|
vendor_cost_usd: null,
|
|
1026
1093
|
incomplete: true
|
|
1027
1094
|
});
|
|
1028
|
-
var buildNoticeEnvelope = (kind, reasons) => noticeEnvelope(noticeSummary(kind, reasons));
|
|
1095
|
+
var buildNoticeEnvelope = (kind, reasons, agentAllowlist = []) => noticeEnvelope(noticeSummary(kind, reasons, agentAllowlist));
|
|
1029
1096
|
var buildUnknownNoticeEnvelope = (kind) => noticeEnvelope(
|
|
1030
1097
|
`### \u26A0\uFE0F Review could not be rendered
|
|
1031
1098
|
|
|
@@ -1035,6 +1102,14 @@ var identity = (decoded) => decoded;
|
|
|
1035
1102
|
var findingsTable = [
|
|
1036
1103
|
{
|
|
1037
1104
|
minor: "0.4",
|
|
1105
|
+
defaultVersion: "0.4.0",
|
|
1106
|
+
schemaFile: "findings.schema.json",
|
|
1107
|
+
codec: FindingsCodec,
|
|
1108
|
+
normalize: identity,
|
|
1109
|
+
latest: false
|
|
1110
|
+
},
|
|
1111
|
+
{
|
|
1112
|
+
minor: "0.5",
|
|
1038
1113
|
defaultVersion: DEFAULT_SCHEMA_VERSION,
|
|
1039
1114
|
schemaFile: "findings.schema.json",
|
|
1040
1115
|
codec: FindingsCodec,
|
|
@@ -1533,6 +1608,7 @@ var post = async (input, ghApi = runGhApi) => {
|
|
|
1533
1608
|
);
|
|
1534
1609
|
const existingComplete = existingSticky !== null && parseReviewComplete(existingSticky.body);
|
|
1535
1610
|
const wouldBuryCompleted = (incomplete) => incomplete && existingComplete;
|
|
1611
|
+
const priorRounds = existingSticky !== null ? parseRounds(existingSticky.body) : [];
|
|
1536
1612
|
const leaveInPlace = () => {
|
|
1537
1613
|
process.stderr.write(
|
|
1538
1614
|
`Review did not complete and the sticky already reflects a completed review \u2014 leaving it in place
|
|
@@ -1549,7 +1625,7 @@ var post = async (input, ghApi = runGhApi) => {
|
|
|
1549
1625
|
const inlineTemplate = readFileSync(input.inlineTemplatePath, "utf-8");
|
|
1550
1626
|
const renderNotice = (message) => formatMarkdown(
|
|
1551
1627
|
render({
|
|
1552
|
-
findings:
|
|
1628
|
+
findings: incompleteFindings(`### \u26A0\uFE0F ${message}`),
|
|
1553
1629
|
envelope: null,
|
|
1554
1630
|
incomplete: true,
|
|
1555
1631
|
prices: decodedPrices.right,
|
|
@@ -1558,6 +1634,8 @@ var post = async (input, ghApi = runGhApi) => {
|
|
|
1558
1634
|
route: input.route,
|
|
1559
1635
|
reviewedSha: input.headSha,
|
|
1560
1636
|
effort: input.effort,
|
|
1637
|
+
rounds: priorRounds,
|
|
1638
|
+
convergenceRound: false,
|
|
1561
1639
|
runUrl: input.runUrl,
|
|
1562
1640
|
jsonUrl: input.jsonUrl,
|
|
1563
1641
|
postedAt: input.postedAt
|
|
@@ -1590,16 +1668,21 @@ var post = async (input, ghApi = runGhApi) => {
|
|
|
1590
1668
|
const envelope = loadEnvelope(input.envelopePath);
|
|
1591
1669
|
const testReport = input.testReportPath ? loadTestReport(input.testReportPath) : void 0;
|
|
1592
1670
|
if (envelope === null) {
|
|
1671
|
+
const envelopelessIncomplete = isIncompleteFindings(findings);
|
|
1672
|
+
if (wouldBuryCompleted(envelopelessIncomplete)) leaveInPlace();
|
|
1593
1673
|
const body = formatMarkdown(
|
|
1594
1674
|
render({
|
|
1595
1675
|
findings,
|
|
1596
1676
|
envelope: null,
|
|
1677
|
+
incomplete: envelopelessIncomplete,
|
|
1597
1678
|
prices: decodedPrices.right,
|
|
1598
1679
|
pricesProvided: input.pricesProvided,
|
|
1599
1680
|
template,
|
|
1600
1681
|
route: input.route,
|
|
1601
1682
|
reviewedSha: input.headSha,
|
|
1602
1683
|
effort: input.effort,
|
|
1684
|
+
rounds: priorRounds,
|
|
1685
|
+
convergenceRound: false,
|
|
1603
1686
|
testReport,
|
|
1604
1687
|
inlineDisposition: { kind: "no-envelope" },
|
|
1605
1688
|
runUrl: input.runUrl,
|
|
@@ -1613,7 +1696,7 @@ var post = async (input, ghApi = runGhApi) => {
|
|
|
1613
1696
|
);
|
|
1614
1697
|
process.exit(0);
|
|
1615
1698
|
}
|
|
1616
|
-
const thisIncomplete = envelope.incomplete === true;
|
|
1699
|
+
const thisIncomplete = envelope.incomplete === true || isIncompleteFindings(findings);
|
|
1617
1700
|
if (wouldBuryCompleted(thisIncomplete)) leaveInPlace();
|
|
1618
1701
|
const findingsMarker = findingsPointer(findings, input.jsonUrl);
|
|
1619
1702
|
const {
|
|
@@ -1635,6 +1718,10 @@ var post = async (input, ghApi = runGhApi) => {
|
|
|
1635
1718
|
}
|
|
1636
1719
|
const botReviews = await fetchBotReviews(input.repo, prNumber, input.botLogin, ghApi);
|
|
1637
1720
|
const initialDisposition = comments.length === 0 && strays.length > 0 ? { kind: "none-in-diff" } : void 0;
|
|
1721
|
+
const currentCounts = computeSeverityCounts(findings.findings);
|
|
1722
|
+
const effectiveRoute = input.route ?? envelope.route;
|
|
1723
|
+
const isRound = isConvergenceRound(effectiveRoute, thisIncomplete);
|
|
1724
|
+
const rounds = isRound ? [...priorRounds, currentCounts] : priorRounds;
|
|
1638
1725
|
const commonRenderInput = {
|
|
1639
1726
|
findings,
|
|
1640
1727
|
envelope,
|
|
@@ -1646,7 +1733,10 @@ var post = async (input, ghApi = runGhApi) => {
|
|
|
1646
1733
|
reviewedSha: input.headSha,
|
|
1647
1734
|
effort: input.effort,
|
|
1648
1735
|
testReport,
|
|
1649
|
-
severityCounts:
|
|
1736
|
+
severityCounts: currentCounts,
|
|
1737
|
+
rounds,
|
|
1738
|
+
convergenceThreshold: input.convergenceThreshold,
|
|
1739
|
+
convergenceRound: isRound,
|
|
1650
1740
|
strays,
|
|
1651
1741
|
runUrl: input.runUrl,
|
|
1652
1742
|
jsonUrl: input.jsonUrl,
|
|
@@ -2657,7 +2747,16 @@ var nativeTelemetry = (native, meta) => resolveTelemetry(
|
|
|
2657
2747
|
meta
|
|
2658
2748
|
);
|
|
2659
2749
|
var absentTelemetry = (meta) => resolveTelemetry({ models: [], turns: 0, durationMs: 0, vendorCostUsd: null }, meta);
|
|
2660
|
-
var buildEnvelope = (telemetry, native, agentFilePath, agentFileFallbackPath) => {
|
|
2750
|
+
var buildEnvelope = (telemetry, native, agentFilePath, agentFileFallbackPath, seedUnrevised) => {
|
|
2751
|
+
if (seedUnrevised)
|
|
2752
|
+
return {
|
|
2753
|
+
schema_version: DEFAULT_SCHEMA_VERSION,
|
|
2754
|
+
findings: incompleteFindings(
|
|
2755
|
+
"### \u26A0\uFE0F Review did not complete\n\nThe review agent did not write a review (its draft is the untouched pre-seed). See the workflow logs."
|
|
2756
|
+
),
|
|
2757
|
+
incomplete: true,
|
|
2758
|
+
...telemetry
|
|
2759
|
+
};
|
|
2661
2760
|
const outcome = findingsOutcome(native, agentFilePath, agentFileFallbackPath);
|
|
2662
2761
|
switch (outcome.kind) {
|
|
2663
2762
|
case "ok":
|
|
@@ -2665,7 +2764,7 @@ var buildEnvelope = (telemetry, native, agentFilePath, agentFileFallbackPath) =>
|
|
|
2665
2764
|
case "telemetry-only":
|
|
2666
2765
|
return {
|
|
2667
2766
|
schema_version: DEFAULT_SCHEMA_VERSION,
|
|
2668
|
-
findings:
|
|
2767
|
+
findings: incompleteFindings(`### \u26A0\uFE0F Review did not complete
|
|
2669
2768
|
|
|
2670
2769
|
${outcome.reason}`),
|
|
2671
2770
|
incomplete: true,
|
|
@@ -2683,7 +2782,8 @@ var adapt = (adapterName, native, agentFilePath, meta = {}) => {
|
|
|
2683
2782
|
absentTelemetry(meta),
|
|
2684
2783
|
void 0,
|
|
2685
2784
|
agentFilePath,
|
|
2686
|
-
meta.agentFileFallbackPath
|
|
2785
|
+
meta.agentFileFallbackPath,
|
|
2786
|
+
meta.seedUnrevised === true
|
|
2687
2787
|
)
|
|
2688
2788
|
);
|
|
2689
2789
|
const decoded = ClaudeCodeEnvelopeCodec.decode(native);
|
|
@@ -2694,7 +2794,8 @@ var adapt = (adapterName, native, agentFilePath, meta = {}) => {
|
|
|
2694
2794
|
nativeTelemetry(decoded.right, meta),
|
|
2695
2795
|
native,
|
|
2696
2796
|
agentFilePath,
|
|
2697
|
-
meta.agentFileFallbackPath
|
|
2797
|
+
meta.agentFileFallbackPath,
|
|
2798
|
+
meta.seedUnrevised === true
|
|
2698
2799
|
)
|
|
2699
2800
|
);
|
|
2700
2801
|
}
|
|
@@ -2726,6 +2827,12 @@ var deriveModelHost = (apiBaseUrl) => {
|
|
|
2726
2827
|
}
|
|
2727
2828
|
return host;
|
|
2728
2829
|
};
|
|
2830
|
+
var KNOWN_MODEL_HOST_SUFFIXES = ["anthropic.com", "deepseek.com"];
|
|
2831
|
+
var isKnownModelHost = (host, declared = []) => {
|
|
2832
|
+
const normalize = (h) => h.replace(/\.$/, "").toLowerCase();
|
|
2833
|
+
const target = normalize(host);
|
|
2834
|
+
return declared.some((d) => normalize(d) === target) || KNOWN_MODEL_HOST_SUFFIXES.some((suffix) => target.endsWith(`.${suffix}`));
|
|
2835
|
+
};
|
|
2729
2836
|
var parseExtraEndpoints = (extra) => extra.split(/\s+/).map((token) => token.replace(/:\d+$/, "")).filter((token) => token.length > 0);
|
|
2730
2837
|
var buildSandboxConfig = (opts) => ({
|
|
2731
2838
|
network: {
|
|
@@ -2786,6 +2893,19 @@ var readJSONOrAbsent = (path) => {
|
|
|
2786
2893
|
return void 0;
|
|
2787
2894
|
}
|
|
2788
2895
|
};
|
|
2896
|
+
var readSandboxConfigForNotice = (path) => {
|
|
2897
|
+
const text = readFileOrNull(resolve$1(path));
|
|
2898
|
+
if (text === null) return void 0;
|
|
2899
|
+
const parsed = tryParseJson(text);
|
|
2900
|
+
if (!parsed.ok) {
|
|
2901
|
+
process.stderr.write(
|
|
2902
|
+
`::warning::${annotationSafe(`code-review notice: ${path} is present but not valid JSON \u2014 omitting the agent allowlist from the notice`)}
|
|
2903
|
+
`
|
|
2904
|
+
);
|
|
2905
|
+
return void 0;
|
|
2906
|
+
}
|
|
2907
|
+
return parsed.value;
|
|
2908
|
+
};
|
|
2789
2909
|
var readStdinJSON = () => {
|
|
2790
2910
|
if (process.stdin.isTTY) return null;
|
|
2791
2911
|
const raw = (() => {
|
|
@@ -2836,6 +2956,7 @@ var resolvePrices = (pricesArg) => {
|
|
|
2836
2956
|
return { kind: "absent", path: bundledPath("schema", "prices.example.json") };
|
|
2837
2957
|
};
|
|
2838
2958
|
var TEST_REPORT_DESCRIPTION = 'Path to a JSON test summary: {"passed": number, "failed": number, "total": number, "failures"?: [{"name": string, "message"?: string}]}';
|
|
2959
|
+
var CONVERGENCE_THRESHOLD_DESCRIPTION = "Advisory convergence tolerance: the weighted-severity score (critical 4 \xB7 major 2 \xB7 minor 1 \xB7 nit 0) at or below which the sticky reads as converged (default: 1 \u2014 unlimited nits plus at most one minor)";
|
|
2839
2960
|
var renderCmd = defineCommand({
|
|
2840
2961
|
meta: {
|
|
2841
2962
|
name: "render",
|
|
@@ -2875,6 +2996,10 @@ var renderCmd = defineCommand({
|
|
|
2875
2996
|
"test-report": {
|
|
2876
2997
|
type: "string",
|
|
2877
2998
|
description: TEST_REPORT_DESCRIPTION
|
|
2999
|
+
},
|
|
3000
|
+
"convergence-threshold": {
|
|
3001
|
+
type: "string",
|
|
3002
|
+
description: CONVERGENCE_THRESHOLD_DESCRIPTION
|
|
2878
3003
|
}
|
|
2879
3004
|
},
|
|
2880
3005
|
run: async ({ args }) => {
|
|
@@ -2895,6 +3020,7 @@ var renderCmd = defineCommand({
|
|
|
2895
3020
|
route: args.route,
|
|
2896
3021
|
effort: args.effort,
|
|
2897
3022
|
testReport,
|
|
3023
|
+
convergenceThreshold: parseConvergenceThreshold(args["convergence-threshold"]),
|
|
2898
3024
|
postedAt: formatUtc(/* @__PURE__ */ new Date())
|
|
2899
3025
|
});
|
|
2900
3026
|
process.stdout.write(output2);
|
|
@@ -3015,6 +3141,18 @@ var parseBudgetUsd = (raw) => {
|
|
|
3015
3141
|
const n = Number.parseFloat(raw);
|
|
3016
3142
|
return Number.isFinite(n) && n >= 0 ? n : null;
|
|
3017
3143
|
};
|
|
3144
|
+
var parseConvergenceThreshold = (raw) => {
|
|
3145
|
+
const trimmed = raw?.trim();
|
|
3146
|
+
if (trimmed === void 0 || trimmed === "") return void 0;
|
|
3147
|
+
if (!/^\d+(\.\d+)?$/.test(trimmed)) {
|
|
3148
|
+
fail(`--convergence-threshold must be a non-negative number; got "${trimmed}"`);
|
|
3149
|
+
}
|
|
3150
|
+
const n = Number.parseFloat(trimmed);
|
|
3151
|
+
if (!Number.isFinite(n)) {
|
|
3152
|
+
fail(`--convergence-threshold is too large to be a meaningful tolerance; got "${trimmed}"`);
|
|
3153
|
+
}
|
|
3154
|
+
return n;
|
|
3155
|
+
};
|
|
3018
3156
|
var mtimeMsOf = (path) => {
|
|
3019
3157
|
try {
|
|
3020
3158
|
return statSync(path).mtimeMs;
|
|
@@ -3341,7 +3479,7 @@ var seedDraftCmd = defineCommand({
|
|
|
3341
3479
|
};
|
|
3342
3480
|
const writeScaffold = () => {
|
|
3343
3481
|
try {
|
|
3344
|
-
writeFileSync(outPath, `${JSON.stringify(
|
|
3482
|
+
writeFileSync(outPath, `${JSON.stringify(emptyFindings(""), null, 2)}
|
|
3345
3483
|
`);
|
|
3346
3484
|
writeSeedMarker();
|
|
3347
3485
|
process.stderr.write(
|
|
@@ -3441,11 +3579,14 @@ var adaptCmd = defineCommand({
|
|
|
3441
3579
|
}
|
|
3442
3580
|
},
|
|
3443
3581
|
run: async ({ args }) => {
|
|
3582
|
+
const agentFile = args["agent-file"];
|
|
3583
|
+
const seedUnrevised = agentFile ? !mainHasWrittenDraft(mtimeMsOf(agentFile), mtimeMsOf(seedMarkerPath(agentFile))) : false;
|
|
3444
3584
|
const envelope = unwrapAdapt(
|
|
3445
|
-
adapt(requireAdapterName(args.adapter), readJSONOrAbsent(args.native),
|
|
3585
|
+
adapt(requireAdapterName(args.adapter), readJSONOrAbsent(args.native), agentFile, {
|
|
3446
3586
|
route: args.route,
|
|
3447
3587
|
effort: args.effort,
|
|
3448
3588
|
agentFileFallbackPath: args["agent-file-fallback"],
|
|
3589
|
+
seedUnrevised,
|
|
3449
3590
|
...args.transcript ? {
|
|
3450
3591
|
transcriptFallback: () => transcriptFallbackFrom(args.transcript)
|
|
3451
3592
|
} : {}
|
|
@@ -3469,6 +3610,10 @@ var noticeCmd = defineCommand({
|
|
|
3469
3610
|
reasons: {
|
|
3470
3611
|
type: "string",
|
|
3471
3612
|
description: "security-blocked / triage-error only: the triage's fail-closed reason string (empty/omitted \u21D2 the no-reason wording)"
|
|
3613
|
+
},
|
|
3614
|
+
"sandbox-config": {
|
|
3615
|
+
type: "string",
|
|
3616
|
+
description: "no-output / triage-error only: path to the agent's sandbox-runtime settings (sandbox.json); its network.allowedDomains is named in the notice so an egress-blocked review self-diagnoses. Missing or unreadable \u21D2 the allowlist is omitted (an early failure runs before the jail is set up)."
|
|
3472
3617
|
}
|
|
3473
3618
|
},
|
|
3474
3619
|
// An unrecognized kind degrades to a generic incomplete notice instead of exiting non-zero: a
|
|
@@ -3484,8 +3629,10 @@ var noticeCmd = defineCommand({
|
|
|
3484
3629
|
`);
|
|
3485
3630
|
return;
|
|
3486
3631
|
}
|
|
3632
|
+
const namesAllowlist = args.kind === "no-output" || args.kind === "triage-error";
|
|
3633
|
+
const agentAllowlist = namesAllowlist && args["sandbox-config"] ? parseAgentAllowlist(readSandboxConfigForNotice(args["sandbox-config"])) : [];
|
|
3487
3634
|
process.stdout.write(
|
|
3488
|
-
`${JSON.stringify(buildNoticeEnvelope(args.kind, args.reasons), null, 2)}
|
|
3635
|
+
`${JSON.stringify(buildNoticeEnvelope(args.kind, args.reasons, agentAllowlist), null, 2)}
|
|
3489
3636
|
`
|
|
3490
3637
|
);
|
|
3491
3638
|
}
|
|
@@ -3852,6 +3999,10 @@ var postCmd = defineCommand({
|
|
|
3852
3999
|
"json-url": {
|
|
3853
4000
|
type: "string",
|
|
3854
4001
|
description: "URL to the machine-readable findings JSON artifact, pointed at from the sticky and each inline comment"
|
|
4002
|
+
},
|
|
4003
|
+
"convergence-threshold": {
|
|
4004
|
+
type: "string",
|
|
4005
|
+
description: CONVERGENCE_THRESHOLD_DESCRIPTION
|
|
3855
4006
|
}
|
|
3856
4007
|
},
|
|
3857
4008
|
run: async ({ args }) => {
|
|
@@ -3872,6 +4023,7 @@ var postCmd = defineCommand({
|
|
|
3872
4023
|
testReportPath: args["test-report"],
|
|
3873
4024
|
runUrl: args["run-url"],
|
|
3874
4025
|
jsonUrl: args["json-url"],
|
|
4026
|
+
convergenceThreshold: parseConvergenceThreshold(args["convergence-threshold"]),
|
|
3875
4027
|
postedAt: formatUtc(/* @__PURE__ */ new Date())
|
|
3876
4028
|
});
|
|
3877
4029
|
}
|
|
@@ -4187,9 +4339,32 @@ var sandboxConfigCmd = defineCommand({
|
|
|
4187
4339
|
out: {
|
|
4188
4340
|
type: "string",
|
|
4189
4341
|
description: "Write the settings JSON here instead of stdout"
|
|
4342
|
+
},
|
|
4343
|
+
"known-model-host": {
|
|
4344
|
+
type: "string",
|
|
4345
|
+
description: "Additional model HOST(S) to treat as known (space-separated bare hostnames, not URLs) \u2014 the consumer's declared host(s) for a provider outside the built-in set; a derived host outside the built-ins and this list warns (or fails under --strict-host)"
|
|
4346
|
+
},
|
|
4347
|
+
"strict-host": {
|
|
4348
|
+
type: "boolean",
|
|
4349
|
+
description: "Fail (instead of warning) when the model host derived from api_base_url is not a well-known or declared host \u2014 closes the fail-open-on-typo gap where a mistyped api_base_url would still be allowlisted and sent the key"
|
|
4190
4350
|
}
|
|
4191
4351
|
},
|
|
4192
4352
|
run: ({ args }) => {
|
|
4353
|
+
const modelHost = deriveModelHost(args["api-base-url"]);
|
|
4354
|
+
const declaredHosts = [
|
|
4355
|
+
...args["known-model-host"] ? parseExtraEndpoints(args["known-model-host"]) : [],
|
|
4356
|
+
...args.extra ? parseExtraEndpoints(args.extra) : []
|
|
4357
|
+
];
|
|
4358
|
+
if (!isKnownModelHost(modelHost, declaredHosts)) {
|
|
4359
|
+
const message = `code-review sandbox-config: derived model host "${modelHost}" is not a well-known or declared host \u2014 the jail will allow egress to it and send MODEL_API_KEY there; verify api_base_url is correct`;
|
|
4360
|
+
if (args["strict-host"]) {
|
|
4361
|
+
process.stderr.write(`::error::${annotationSafe(message)}
|
|
4362
|
+
`);
|
|
4363
|
+
process.exit(1);
|
|
4364
|
+
}
|
|
4365
|
+
process.stderr.write(`::warning::${annotationSafe(message)}
|
|
4366
|
+
`);
|
|
4367
|
+
}
|
|
4193
4368
|
const config = buildSandboxConfig({ apiBaseUrl: args["api-base-url"], extra: args.extra });
|
|
4194
4369
|
const json = `${JSON.stringify(config, null, 2)}
|
|
4195
4370
|
`;
|