devcontour-harness 0.1.0-alpha.6 → 0.1.0-alpha.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -2
- package/docs/quickstart.md +1 -1
- package/docs/release-notes/0.1.0-alpha.7.md +71 -0
- package/docs/review-probes.md +79 -0
- package/docs/upgrade.md +42 -25
- package/lib/agents/cli-adapter.js +8 -1
- package/lib/agents/inspect.d.ts +56 -2
- package/lib/agents/inspect.js +264 -14
- package/lib/agents/review-probe.d.ts +50 -0
- package/lib/agents/review-probe.js +211 -46
- package/lib/contracts/execution.d.ts +213 -7
- package/lib/contracts/execution.js +67 -3
- package/lib/contracts/published.d.ts +49 -1
- package/lib/control/review.js +9 -5
- package/lib/engine/budget.d.ts +3 -3
- package/lib/engine/engine.d.ts +28 -2
- package/lib/engine/runner.js +20 -12
- package/lib/environment/process.d.ts +8 -0
- package/lib/environment/process.js +8 -2
- package/lib/storage/document.d.ts +56 -4
- package/lib/verification/gates.d.ts +22 -0
- package/lib/verification/gates.js +64 -23
- package/package.json +1 -1
- package/schemas/execution-document.schema.json +190 -4
|
@@ -136,12 +136,76 @@ export const propertyEvidenceSchema = z.strictObject({
|
|
|
136
136
|
.optional(),
|
|
137
137
|
coverage: z.array(z.string().max(1000)).optional(),
|
|
138
138
|
});
|
|
139
|
-
/**
|
|
139
|
+
/** Происхождение подтверждённой команды: событие runtime или запуск probe controller. */
|
|
140
|
+
export const inspectionOriginSchema = z.discriminatedUnion('type', [
|
|
141
|
+
z.strictObject({
|
|
142
|
+
type: z.literal('runtime'),
|
|
143
|
+
/** Имя runtime reviewer (`codex`, `claude`, …). */
|
|
144
|
+
runtime: z.string().min(1).max(100),
|
|
145
|
+
/** ID item в потоке событий runtime; `null` — runtime его не дал. */
|
|
146
|
+
item: z.string().min(1).max(200).nullable(),
|
|
147
|
+
}),
|
|
148
|
+
z.strictObject({
|
|
149
|
+
type: z.literal('controller'),
|
|
150
|
+
/** runId запуска probe в журнале controller (не runId попытки). */
|
|
151
|
+
runId: z.string().min(1).max(200),
|
|
152
|
+
}),
|
|
153
|
+
]);
|
|
154
|
+
/**
|
|
155
|
+
* Что reviewer действительно выполнил — по событиям runtime и журналу
|
|
156
|
+
* проверок controller одного вызова reviewer, не по словам модели
|
|
157
|
+
* (docs/review-probes.md). Поля после `reason` появились в alpha.7 и
|
|
158
|
+
* необязательны: записи прежних версий читаются как есть.
|
|
159
|
+
*/
|
|
140
160
|
export const reviewInspectionSchema = z.strictObject({
|
|
141
161
|
mode: z.enum(['commands', 'diff-only']),
|
|
142
|
-
source: z.enum(['codex-events', 'unavailable']),
|
|
143
|
-
commands: z.array(z.strictObject({
|
|
162
|
+
source: z.enum(['codex-events', 'unavailable', 'controller-probes', 'runtime-and-controller']),
|
|
163
|
+
commands: z.array(z.strictObject({
|
|
164
|
+
command: z.string(),
|
|
165
|
+
/** `null` — код неизвестен: таймаут/отмена после старта, старая проекция без кода. */
|
|
166
|
+
exitCode: z.number().int().nullable(),
|
|
167
|
+
output: z.string(),
|
|
168
|
+
origins: z.array(inspectionOriginSchema).min(1).max(8).optional(),
|
|
169
|
+
/** Исход: `completed`/`failed` — runtime; остальные — статус probe controller. */
|
|
170
|
+
status: z.enum(['completed', 'passed', 'failed', 'timeout', 'cancelled', 'error']).optional(),
|
|
171
|
+
/** Неуспех не в проверяемом коде: среда или отчёт (как у probe). */
|
|
172
|
+
failure: z.enum(['environment', 'report']).optional(),
|
|
173
|
+
/** ID объявленного gate, запущенного через probe. */
|
|
174
|
+
gate: z.string().min(1).max(200).optional(),
|
|
175
|
+
note: z.string().max(2000).optional(),
|
|
176
|
+
})),
|
|
144
177
|
reason: z.string().nullable(),
|
|
178
|
+
/** Вызов reviewer, которому принадлежит итог. */
|
|
179
|
+
context: z
|
|
180
|
+
.strictObject({
|
|
181
|
+
/** ID попытки harness (runId evidence). */
|
|
182
|
+
attempt: z.string().min(1).max(200),
|
|
183
|
+
phase: z.enum(['candidate', 'integration']),
|
|
184
|
+
sha: gitShaSchema,
|
|
185
|
+
call: z.number().int().positive(),
|
|
186
|
+
})
|
|
187
|
+
.optional(),
|
|
188
|
+
/** Число всех подтверждённых операций, в том числе не вошедших в `commands`. */
|
|
189
|
+
total: z.number().int().min(0).optional(),
|
|
190
|
+
/** Записи журнала, не засчитанные выполнением: почему (не более 100). */
|
|
191
|
+
excluded: z
|
|
192
|
+
.array(z.strictObject({
|
|
193
|
+
runId: z.string().min(1).max(200),
|
|
194
|
+
status: z.string().max(40).nullable(),
|
|
195
|
+
reason: z.enum([
|
|
196
|
+
'not-started',
|
|
197
|
+
'legacy-unconfirmed',
|
|
198
|
+
'foreign-scope',
|
|
199
|
+
'unknown-status',
|
|
200
|
+
'unfinished',
|
|
201
|
+
'abandoned',
|
|
202
|
+
]),
|
|
203
|
+
}))
|
|
204
|
+
.max(100)
|
|
205
|
+
.optional(),
|
|
206
|
+
excludedTotal: z.number().int().min(0).optional(),
|
|
207
|
+
/** Строки журнала без формы исхода (битый JSON, `{}`): не запуски. */
|
|
208
|
+
invalid: z.number().int().min(0).optional(),
|
|
145
209
|
});
|
|
146
210
|
export const evidenceFindingSchema = z.strictObject({
|
|
147
211
|
severity: z.string().min(1).max(40),
|
|
@@ -2812,13 +2812,61 @@ export declare const publishedSchemas: {
|
|
|
2812
2812
|
source: z.ZodEnum<{
|
|
2813
2813
|
"codex-events": "codex-events";
|
|
2814
2814
|
unavailable: "unavailable";
|
|
2815
|
+
"controller-probes": "controller-probes";
|
|
2816
|
+
"runtime-and-controller": "runtime-and-controller";
|
|
2815
2817
|
}>;
|
|
2816
2818
|
commands: z.ZodArray<z.ZodObject<{
|
|
2817
2819
|
command: z.ZodString;
|
|
2818
|
-
exitCode: z.ZodNumber
|
|
2820
|
+
exitCode: z.ZodNullable<z.ZodNumber>;
|
|
2819
2821
|
output: z.ZodString;
|
|
2822
|
+
origins: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
2823
|
+
type: z.ZodLiteral<"runtime">;
|
|
2824
|
+
runtime: z.ZodString;
|
|
2825
|
+
item: z.ZodNullable<z.ZodString>;
|
|
2826
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
2827
|
+
type: z.ZodLiteral<"controller">;
|
|
2828
|
+
runId: z.ZodString;
|
|
2829
|
+
}, z.core.$strict>], "type">>>;
|
|
2830
|
+
status: z.ZodOptional<z.ZodEnum<{
|
|
2831
|
+
error: "error";
|
|
2832
|
+
passed: "passed";
|
|
2833
|
+
failed: "failed";
|
|
2834
|
+
cancelled: "cancelled";
|
|
2835
|
+
completed: "completed";
|
|
2836
|
+
timeout: "timeout";
|
|
2837
|
+
}>>;
|
|
2838
|
+
failure: z.ZodOptional<z.ZodEnum<{
|
|
2839
|
+
report: "report";
|
|
2840
|
+
environment: "environment";
|
|
2841
|
+
}>>;
|
|
2842
|
+
gate: z.ZodOptional<z.ZodString>;
|
|
2843
|
+
note: z.ZodOptional<z.ZodString>;
|
|
2820
2844
|
}, z.core.$strict>>;
|
|
2821
2845
|
reason: z.ZodNullable<z.ZodString>;
|
|
2846
|
+
context: z.ZodOptional<z.ZodObject<{
|
|
2847
|
+
attempt: z.ZodString;
|
|
2848
|
+
phase: z.ZodEnum<{
|
|
2849
|
+
candidate: "candidate";
|
|
2850
|
+
integration: "integration";
|
|
2851
|
+
}>;
|
|
2852
|
+
sha: z.ZodString;
|
|
2853
|
+
call: z.ZodNumber;
|
|
2854
|
+
}, z.core.$strict>>;
|
|
2855
|
+
total: z.ZodOptional<z.ZodNumber>;
|
|
2856
|
+
excluded: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
2857
|
+
runId: z.ZodString;
|
|
2858
|
+
status: z.ZodNullable<z.ZodString>;
|
|
2859
|
+
reason: z.ZodEnum<{
|
|
2860
|
+
"not-started": "not-started";
|
|
2861
|
+
"legacy-unconfirmed": "legacy-unconfirmed";
|
|
2862
|
+
"foreign-scope": "foreign-scope";
|
|
2863
|
+
"unknown-status": "unknown-status";
|
|
2864
|
+
unfinished: "unfinished";
|
|
2865
|
+
abandoned: "abandoned";
|
|
2866
|
+
}>;
|
|
2867
|
+
}, z.core.$strict>>>;
|
|
2868
|
+
excludedTotal: z.ZodOptional<z.ZodNumber>;
|
|
2869
|
+
invalid: z.ZodOptional<z.ZodNumber>;
|
|
2822
2870
|
}, z.core.$strict>>;
|
|
2823
2871
|
findings: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
2824
2872
|
severity: z.ZodString;
|
package/lib/control/review.js
CHANGED
|
@@ -3,6 +3,7 @@ import { mkdir, writeFile } from 'node:fs/promises';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { HarnessError, WORKSPACE_OWNER, contextDigest, findingId, mcpEnvironment, reviewIssues, reviewResultSchema, selectReviewer, } from "../contracts/index.js";
|
|
5
5
|
import { reviewOutputSchema } from "../agents/schemas.js";
|
|
6
|
+
import { redactInspection } from "../agents/inspect.js";
|
|
6
7
|
import { executionEnvironment } from "../environment/lifecycle.js";
|
|
7
8
|
import { redactDeep } from "../environment/redaction.js";
|
|
8
9
|
import { TaskFailure } from "../engine/failure.js";
|
|
@@ -165,11 +166,14 @@ export async function technicalReview(engine, policy, adapters, input) {
|
|
|
165
166
|
};
|
|
166
167
|
// Сохраняемые представления маскируются полным окружением вызова: сырой
|
|
167
168
|
// структурированный ответ адаптера может нести значение секрета профиля.
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
169
|
+
const artifact = redactDeep({ response: parsed.success ? parsed.data : null, issues, inspection: null }, execution.redact);
|
|
170
|
+
// Inspection маскируется по полям: служебные значения не текст reviewer.
|
|
171
|
+
await writeFile(record.artifact, JSON.stringify({
|
|
172
|
+
...artifact,
|
|
173
|
+
inspection: response.inspection
|
|
174
|
+
? redactInspection(response.inspection, execution.redact)
|
|
175
|
+
: null,
|
|
176
|
+
}, null, 2));
|
|
173
177
|
// Снимок, который видел reviewer, хранится по digest: согласование этого
|
|
174
178
|
// раунда делает его действующим контекстом графа.
|
|
175
179
|
if (context && contextKey)
|
package/lib/engine/budget.d.ts
CHANGED
|
@@ -47,7 +47,7 @@ export declare function scopeRecords(c: BudgetContext, view: ExecutionView, reco
|
|
|
47
47
|
model: string | null;
|
|
48
48
|
runtimeVersion: string | null;
|
|
49
49
|
startedAt: string;
|
|
50
|
-
outcome: "error" | "active" | "
|
|
50
|
+
outcome: "error" | "active" | "abandoned" | "returned";
|
|
51
51
|
promptBytes: number;
|
|
52
52
|
promptDigest: string;
|
|
53
53
|
usage: {
|
|
@@ -133,7 +133,7 @@ export declare function workBudgets(c: BudgetContext, view: ExecutionView, workI
|
|
|
133
133
|
model: string | null;
|
|
134
134
|
runtimeVersion: string | null;
|
|
135
135
|
startedAt: string;
|
|
136
|
-
outcome: "error" | "active" | "
|
|
136
|
+
outcome: "error" | "active" | "abandoned" | "returned";
|
|
137
137
|
promptBytes: number;
|
|
138
138
|
promptDigest: string;
|
|
139
139
|
usage: {
|
|
@@ -227,7 +227,7 @@ export declare function callBudgets(c: BudgetContext, view: ExecutionView, meta:
|
|
|
227
227
|
model: string | null;
|
|
228
228
|
runtimeVersion: string | null;
|
|
229
229
|
startedAt: string;
|
|
230
|
-
outcome: "error" | "active" | "
|
|
230
|
+
outcome: "error" | "active" | "abandoned" | "returned";
|
|
231
231
|
promptBytes: number;
|
|
232
232
|
promptDigest: string;
|
|
233
233
|
usage: {
|
package/lib/engine/engine.d.ts
CHANGED
|
@@ -492,13 +492,39 @@ export declare class Engine {
|
|
|
492
492
|
consumer?: string | undefined;
|
|
493
493
|
inspection?: {
|
|
494
494
|
mode: "commands" | "diff-only";
|
|
495
|
-
source: "codex-events" | "unavailable";
|
|
495
|
+
source: "codex-events" | "unavailable" | "controller-probes" | "runtime-and-controller";
|
|
496
496
|
commands: {
|
|
497
497
|
command: string;
|
|
498
|
-
exitCode: number;
|
|
498
|
+
exitCode: number | null;
|
|
499
499
|
output: string;
|
|
500
|
+
origins?: ({
|
|
501
|
+
type: "runtime";
|
|
502
|
+
runtime: string;
|
|
503
|
+
item: string | null;
|
|
504
|
+
} | {
|
|
505
|
+
type: "controller";
|
|
506
|
+
runId: string;
|
|
507
|
+
})[] | undefined;
|
|
508
|
+
status?: "error" | "passed" | "failed" | "cancelled" | "completed" | "timeout" | undefined;
|
|
509
|
+
failure?: "report" | "environment" | undefined;
|
|
510
|
+
gate?: string | undefined;
|
|
511
|
+
note?: string | undefined;
|
|
500
512
|
}[];
|
|
501
513
|
reason: string | null;
|
|
514
|
+
context?: {
|
|
515
|
+
attempt: string;
|
|
516
|
+
phase: "candidate" | "integration";
|
|
517
|
+
sha: string;
|
|
518
|
+
call: number;
|
|
519
|
+
} | undefined;
|
|
520
|
+
total?: number | undefined;
|
|
521
|
+
excluded?: {
|
|
522
|
+
runId: string;
|
|
523
|
+
status: string | null;
|
|
524
|
+
reason: "not-started" | "legacy-unconfirmed" | "foreign-scope" | "unknown-status" | "unfinished" | "abandoned";
|
|
525
|
+
}[] | undefined;
|
|
526
|
+
excludedTotal?: number | undefined;
|
|
527
|
+
invalid?: number | undefined;
|
|
502
528
|
} | undefined;
|
|
503
529
|
findings?: {
|
|
504
530
|
severity: string;
|
package/lib/engine/runner.js
CHANGED
|
@@ -8,9 +8,9 @@ import { git } from "../environment/process.js";
|
|
|
8
8
|
import { executionEnvironment, runSteps, withEnvironment } from "../environment/lifecycle.js";
|
|
9
9
|
import { redactDeep } from "../environment/redaction.js";
|
|
10
10
|
import { implementationOutputSchema, reviewOutputSchema } from "../agents/schemas.js";
|
|
11
|
-
import {
|
|
11
|
+
import { reviewInspection } from "../agents/inspect.js";
|
|
12
12
|
import { reviewerRunsChecks } from "../agents/cli-adapter.js";
|
|
13
|
-
import { gateProbePrompt, observeReproductions, probeCommand, probeGates, probeLimits,
|
|
13
|
+
import { gateProbePrompt, observeReproductions, probeCommand, probeGates, probeLimits, probeRecord, readProbeJournal, recoverProbeLog, probeTool, } from "../agents/review-probe.js";
|
|
14
14
|
import { runGate } from "../verification/gates.js";
|
|
15
15
|
import { assertDependencies, dependencyFreshness, removeSnapshots, snapshotDependencies, } from "../verification/dependencies.js";
|
|
16
16
|
import { CleanupFailure, cleanupEnvironment } from "../environment/lifecycle.js";
|
|
@@ -1148,6 +1148,8 @@ export class Runner {
|
|
|
1148
1148
|
* переменных окружения CLI передаёт серверу по имени, на диск они не пишутся.
|
|
1149
1149
|
*/
|
|
1150
1150
|
async reviewProbe(attempt, work, cwd, dir, phase, sha, gates,
|
|
1151
|
+
/** Номер вызова reviewer логического ревью: записывается в каждый запуск probe. */
|
|
1152
|
+
call,
|
|
1151
1153
|
/**
|
|
1152
1154
|
* Повтор reviewer того же логического ревью: срок и остаток бюджета
|
|
1153
1155
|
* проверок переходят от первого вызова, а не выдаются заново.
|
|
@@ -1186,6 +1188,7 @@ export class Runner {
|
|
|
1186
1188
|
gates: probeGates(gates),
|
|
1187
1189
|
phase,
|
|
1188
1190
|
sha,
|
|
1191
|
+
scope: { attempt: attempt.id, phase, sha, call },
|
|
1189
1192
|
artifacts: join(dir, 'probe-runs'),
|
|
1190
1193
|
};
|
|
1191
1194
|
const specPath = join(dir, 'probe.json');
|
|
@@ -1316,7 +1319,7 @@ export class Runner {
|
|
|
1316
1319
|
}
|
|
1317
1320
|
const callDir = call === 1 ? dir : join(dir, `call-${call}`);
|
|
1318
1321
|
await mkdir(callDir, { recursive: true });
|
|
1319
|
-
const probe = await this.reviewProbe(attempt, work, cwd, callDir, phase, sha, gates, shared);
|
|
1322
|
+
const probe = await this.reviewProbe(attempt, work, cwd, callDir, phase, sha, gates, call, shared);
|
|
1320
1323
|
const response = await this.call(this.adapter(attempt.reviewer), {
|
|
1321
1324
|
task: 'review-code',
|
|
1322
1325
|
attempt,
|
|
@@ -1357,9 +1360,17 @@ export class Runner {
|
|
|
1357
1360
|
}
|
|
1358
1361
|
: checkCodeReview(response.output, history, { runId: attempt.id, phase, sha }, { legacy });
|
|
1359
1362
|
const result = verdict.result;
|
|
1360
|
-
const inspection = response.inspection ?? unobservedReview();
|
|
1361
1363
|
// Проверки reviewer — по журналу controller, а не по самоотчёту модели.
|
|
1362
|
-
const
|
|
1364
|
+
const journal = probe ? await readProbeJournal(probe.log) : undefined;
|
|
1365
|
+
const probes = journal?.records.map(probeRecord);
|
|
1366
|
+
// Один итог вызова: события runtime и журнал controller этого вызова,
|
|
1367
|
+
// уже замаскированный; он же — в review.json и во всех ветках evidence.
|
|
1368
|
+
const inspection = reviewInspection({
|
|
1369
|
+
runtime: response.inspection,
|
|
1370
|
+
journal,
|
|
1371
|
+
context: { attempt: attempt.id, phase, sha, call },
|
|
1372
|
+
redact,
|
|
1373
|
+
});
|
|
1363
1374
|
const protocol = !clean
|
|
1364
1375
|
? 'unsafe'
|
|
1365
1376
|
: verdict.valid
|
|
@@ -1388,13 +1399,10 @@ export class Runner {
|
|
|
1388
1399
|
...(result ? {} : { raw: response.output ?? null }),
|
|
1389
1400
|
...(probes ? { probes } : {}),
|
|
1390
1401
|
}, redact);
|
|
1391
|
-
//
|
|
1392
|
-
// короткий секрет, совпавший с их
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
mode: inspection.mode,
|
|
1396
|
-
source: inspection.source,
|
|
1397
|
-
};
|
|
1402
|
+
// Итог уже замаскирован до обрезки; его перечисления и идентификаторы
|
|
1403
|
+
// controller — не текст reviewer: короткий секрет, совпавший с их
|
|
1404
|
+
// частью, их не маскирует.
|
|
1405
|
+
record.inspection = inspection;
|
|
1398
1406
|
if (result)
|
|
1399
1407
|
record.findings = record.findings.map((f, i) => ({
|
|
1400
1408
|
...f,
|
|
@@ -14,6 +14,14 @@ export declare function command(argv: string[], cwd: string, options?: {
|
|
|
14
14
|
env?: NodeJS.ProcessEnv;
|
|
15
15
|
redact?: Redactor;
|
|
16
16
|
onOutput?: (stream: 'stdout' | 'stderr', text: string) => void;
|
|
17
|
+
/**
|
|
18
|
+
* Немаскированный stdout — только в памяти вызывающего, для разбора
|
|
19
|
+
* структуры (события runtime): маска по всему потоку портила бы и
|
|
20
|
+
* служебные строки JSON. Сохранять его нельзя.
|
|
21
|
+
*/
|
|
22
|
+
onRawStdout?: (text: string) => void;
|
|
23
|
+
/** Процесс запущен (событие `spawn`): до него команда не исполнялась. */
|
|
24
|
+
onSpawn?: () => void;
|
|
17
25
|
/**
|
|
18
26
|
* Владеть всеми потомками, включая сменивших группу или сессию: после
|
|
19
27
|
* завершения они останавливаются, и результат не возвращается, пока
|
|
@@ -28,6 +28,8 @@ export async function command(argv, cwd, options = {}) {
|
|
|
28
28
|
detached: process.platform !== 'win32',
|
|
29
29
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
30
30
|
});
|
|
31
|
+
if (options.onSpawn)
|
|
32
|
+
child.once('spawn', options.onSpawn);
|
|
31
33
|
let stdout = '', stderr = '', timedOut = false, killTimer;
|
|
32
34
|
const stdoutDecoder = new StringDecoder('utf8'), stderrDecoder = new StringDecoder('utf8');
|
|
33
35
|
const maskOut = outputRedactor(options.redact), maskErr = outputRedactor(options.redact);
|
|
@@ -49,7 +51,9 @@ export async function command(argv, cwd, options = {}) {
|
|
|
49
51
|
};
|
|
50
52
|
child.stdout.on('data', (d) => {
|
|
51
53
|
lastOutputAt = new Date().toISOString();
|
|
52
|
-
|
|
54
|
+
const text = stdoutDecoder.write(d);
|
|
55
|
+
options.onRawStdout?.(text);
|
|
56
|
+
append('stdout', maskOut(text));
|
|
53
57
|
});
|
|
54
58
|
child.stderr.on('data', (d) => {
|
|
55
59
|
lastOutputAt = new Date().toISOString();
|
|
@@ -152,7 +156,9 @@ export async function command(argv, cwd, options = {}) {
|
|
|
152
156
|
child.stderr.destroy();
|
|
153
157
|
}
|
|
154
158
|
}
|
|
155
|
-
|
|
159
|
+
const rest = stdoutDecoder.end();
|
|
160
|
+
options.onRawStdout?.(rest);
|
|
161
|
+
append('stdout', maskOut(rest, true));
|
|
156
162
|
append('stderr', maskErr(stderrDecoder.end(), true));
|
|
157
163
|
if (outputError) {
|
|
158
164
|
reject(outputError);
|
|
@@ -490,13 +490,39 @@ export declare class DocumentView {
|
|
|
490
490
|
consumer?: string | undefined;
|
|
491
491
|
inspection?: {
|
|
492
492
|
mode: "commands" | "diff-only";
|
|
493
|
-
source: "codex-events" | "unavailable";
|
|
493
|
+
source: "codex-events" | "unavailable" | "controller-probes" | "runtime-and-controller";
|
|
494
494
|
commands: {
|
|
495
495
|
command: string;
|
|
496
|
-
exitCode: number;
|
|
496
|
+
exitCode: number | null;
|
|
497
497
|
output: string;
|
|
498
|
+
origins?: ({
|
|
499
|
+
type: "runtime";
|
|
500
|
+
runtime: string;
|
|
501
|
+
item: string | null;
|
|
502
|
+
} | {
|
|
503
|
+
type: "controller";
|
|
504
|
+
runId: string;
|
|
505
|
+
})[] | undefined;
|
|
506
|
+
status?: "error" | "passed" | "failed" | "cancelled" | "completed" | "timeout" | undefined;
|
|
507
|
+
failure?: "report" | "environment" | undefined;
|
|
508
|
+
gate?: string | undefined;
|
|
509
|
+
note?: string | undefined;
|
|
498
510
|
}[];
|
|
499
511
|
reason: string | null;
|
|
512
|
+
context?: {
|
|
513
|
+
attempt: string;
|
|
514
|
+
phase: "candidate" | "integration";
|
|
515
|
+
sha: string;
|
|
516
|
+
call: number;
|
|
517
|
+
} | undefined;
|
|
518
|
+
total?: number | undefined;
|
|
519
|
+
excluded?: {
|
|
520
|
+
runId: string;
|
|
521
|
+
status: string | null;
|
|
522
|
+
reason: "not-started" | "legacy-unconfirmed" | "foreign-scope" | "unknown-status" | "unfinished" | "abandoned";
|
|
523
|
+
}[] | undefined;
|
|
524
|
+
excludedTotal?: number | undefined;
|
|
525
|
+
invalid?: number | undefined;
|
|
500
526
|
} | undefined;
|
|
501
527
|
findings?: {
|
|
502
528
|
severity: string;
|
|
@@ -755,13 +781,39 @@ export declare class DocumentView {
|
|
|
755
781
|
consumer?: string | undefined;
|
|
756
782
|
inspection?: {
|
|
757
783
|
mode: "commands" | "diff-only";
|
|
758
|
-
source: "codex-events" | "unavailable";
|
|
784
|
+
source: "codex-events" | "unavailable" | "controller-probes" | "runtime-and-controller";
|
|
759
785
|
commands: {
|
|
760
786
|
command: string;
|
|
761
|
-
exitCode: number;
|
|
787
|
+
exitCode: number | null;
|
|
762
788
|
output: string;
|
|
789
|
+
origins?: ({
|
|
790
|
+
type: "runtime";
|
|
791
|
+
runtime: string;
|
|
792
|
+
item: string | null;
|
|
793
|
+
} | {
|
|
794
|
+
type: "controller";
|
|
795
|
+
runId: string;
|
|
796
|
+
})[] | undefined;
|
|
797
|
+
status?: "error" | "passed" | "failed" | "cancelled" | "completed" | "timeout" | undefined;
|
|
798
|
+
failure?: "report" | "environment" | undefined;
|
|
799
|
+
gate?: string | undefined;
|
|
800
|
+
note?: string | undefined;
|
|
763
801
|
}[];
|
|
764
802
|
reason: string | null;
|
|
803
|
+
context?: {
|
|
804
|
+
attempt: string;
|
|
805
|
+
phase: "candidate" | "integration";
|
|
806
|
+
sha: string;
|
|
807
|
+
call: number;
|
|
808
|
+
} | undefined;
|
|
809
|
+
total?: number | undefined;
|
|
810
|
+
excluded?: {
|
|
811
|
+
runId: string;
|
|
812
|
+
status: string | null;
|
|
813
|
+
reason: "not-started" | "legacy-unconfirmed" | "foreign-scope" | "unknown-status" | "unfinished" | "abandoned";
|
|
814
|
+
}[] | undefined;
|
|
815
|
+
excludedTotal?: number | undefined;
|
|
816
|
+
invalid?: number | undefined;
|
|
765
817
|
} | undefined;
|
|
766
818
|
findings?: {
|
|
767
819
|
severity: string;
|
|
@@ -79,14 +79,36 @@ export declare function runCheck(h: {
|
|
|
79
79
|
readable: string[];
|
|
80
80
|
controller: string[];
|
|
81
81
|
settingsDir: string;
|
|
82
|
+
/**
|
|
83
|
+
* Запуск проверки подтверждён: без песочницы — порождён её процесс, в
|
|
84
|
+
* песочнице — обёртка создала метку старта перед exec. Запуск самой
|
|
85
|
+
* обёртки песочницы запуском проверки не считается. Вызывается не более
|
|
86
|
+
* одного раза, в том числе когда проверка затем завершилась исключением.
|
|
87
|
+
*/
|
|
88
|
+
onStart?: () => void;
|
|
82
89
|
}): Promise<{
|
|
83
90
|
attempts: number;
|
|
91
|
+
started: boolean;
|
|
84
92
|
code: number;
|
|
85
93
|
stdout: string;
|
|
86
94
|
stderr: string;
|
|
87
95
|
timedOut: boolean;
|
|
88
96
|
diagnostics: import("../contracts/execution.ts").RuntimeDiagnostics;
|
|
89
97
|
}>;
|
|
98
|
+
/** Исход проверки, завершившейся до исключения (например, отказа уборки), если он есть. */
|
|
99
|
+
export declare function completedCheck(error: unknown): {
|
|
100
|
+
attempts: number;
|
|
101
|
+
started: boolean;
|
|
102
|
+
code: number;
|
|
103
|
+
stdout: string;
|
|
104
|
+
stderr: string;
|
|
105
|
+
timedOut: boolean;
|
|
106
|
+
diagnostics: import("../contracts/execution.ts").RuntimeDiagnostics;
|
|
107
|
+
} | undefined;
|
|
108
|
+
/** Уборка каталогов проверки; тест подменяет её, чтобы проверить отказ после исхода. */
|
|
109
|
+
export declare const checkCleanup: {
|
|
110
|
+
remove: (dir: string) => Promise<void>;
|
|
111
|
+
};
|
|
90
112
|
/** Проверка механизма песочницы; тест подменяет её, чтобы проверить отказ. */
|
|
91
113
|
export declare const isolationBackend: {
|
|
92
114
|
check: typeof isolationSupport;
|
|
@@ -251,7 +251,14 @@ export async function prepareReportPath(root, path, escape) {
|
|
|
251
251
|
export async function runCheck(
|
|
252
252
|
// Нужна только политика изоляции: проверки ревьюера исполняются вне сервера.
|
|
253
253
|
h, options) {
|
|
254
|
-
let
|
|
254
|
+
let launched = false;
|
|
255
|
+
const confirm = () => {
|
|
256
|
+
if (launched)
|
|
257
|
+
return;
|
|
258
|
+
launched = true;
|
|
259
|
+
options.onStart?.();
|
|
260
|
+
};
|
|
261
|
+
let argv = options.argv, env = options.env, started, result, attempts = 1;
|
|
255
262
|
const scratch = await realpath(await mkdtemp(join(tmpdir(), 'dc-gate-')));
|
|
256
263
|
// Метка старта — вне TMPDIR команды: проверка, чистящая свой временный
|
|
257
264
|
// каталог, не должна стирать признак того, что она уже начиналась.
|
|
@@ -275,39 +282,73 @@ h, options) {
|
|
|
275
282
|
argv = wrapped.argv;
|
|
276
283
|
env = { ...env, ...wrapped.env };
|
|
277
284
|
}
|
|
278
|
-
const execute = () =>
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
285
|
+
const execute = () => {
|
|
286
|
+
// Уже отменённый вызов не запускается: слушатель отмены на отменённом
|
|
287
|
+
// сигнале не срабатывает, и команда шла бы до таймаута.
|
|
288
|
+
if (options.signal.aborted)
|
|
289
|
+
throw Object.assign(new Error('Проверка отменена до запуска'), { name: 'AbortError' });
|
|
290
|
+
return command(argv, options.cwd, {
|
|
291
|
+
signal: options.signal,
|
|
292
|
+
...(started === undefined ? { onSpawn: confirm } : {}),
|
|
293
|
+
timeoutMs: options.timeoutMs,
|
|
294
|
+
env,
|
|
295
|
+
redact: options.redact,
|
|
296
|
+
// Отсоединившийся потомок проверки не переживает её: и при успехе,
|
|
297
|
+
// и по таймауту, отмене или падению.
|
|
298
|
+
// Каталоги проверки принадлежат только ей: процесс, запущенный в них
|
|
299
|
+
// во время проверки, — её потомок, даже без метки и родства.
|
|
300
|
+
contain: { dirs: [...options.write, scratch, marks] },
|
|
301
|
+
});
|
|
302
|
+
};
|
|
303
|
+
try {
|
|
295
304
|
result = await execute();
|
|
305
|
+
// sandbox-runtime ищет shell через `which` с таймаутом в секунду и под
|
|
306
|
+
// нагрузкой отказывает до запуска команды. Повтор допустим, только если
|
|
307
|
+
// команда точно не начиналась: метки старта нет.
|
|
308
|
+
for (; attempts < 3 && sandboxNotStarted(result, started); attempts++) {
|
|
309
|
+
await new Promise((r) => setTimeout(r, 500 * attempts));
|
|
310
|
+
result = await execute();
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
finally {
|
|
314
|
+
// Факт старта читается до удаления метки — и при исключении после него.
|
|
315
|
+
if (started !== undefined && existsSync(started))
|
|
316
|
+
confirm();
|
|
296
317
|
}
|
|
297
|
-
|
|
318
|
+
// Без песочницы завершившийся процесс точно порождался.
|
|
319
|
+
if (started === undefined)
|
|
320
|
+
confirm();
|
|
321
|
+
return { ...result, attempts, started: launched };
|
|
298
322
|
}
|
|
299
323
|
finally {
|
|
300
324
|
// Каждый каталог убирается независимо: отказ уборки временного каталога
|
|
301
325
|
// проверки (она могла снять с него права) не должен оставлять и метки.
|
|
302
326
|
const removed = await Promise.allSettled([
|
|
303
|
-
|
|
304
|
-
|
|
327
|
+
checkCleanup.remove(scratch),
|
|
328
|
+
checkCleanup.remove(marks),
|
|
305
329
|
]);
|
|
306
330
|
const failed = removed.find((r) => r.status === 'rejected');
|
|
307
|
-
if (failed)
|
|
308
|
-
|
|
331
|
+
if (failed) {
|
|
332
|
+
// Отказ уборки после завершения не стирает исход проверки: код и
|
|
333
|
+
// вывод доступны вызывающему через ошибку (`completedCheck`).
|
|
334
|
+
const error = failed.reason instanceof Error ? failed.reason : new Error(String(failed.reason));
|
|
335
|
+
throw result
|
|
336
|
+
? Object.assign(error, { [checkResult]: { ...result, attempts, started: launched } })
|
|
337
|
+
: error;
|
|
338
|
+
}
|
|
309
339
|
}
|
|
310
340
|
}
|
|
341
|
+
const checkResult = Symbol('runCheck.result');
|
|
342
|
+
/** Исход проверки, завершившейся до исключения (например, отказа уборки), если он есть. */
|
|
343
|
+
export function completedCheck(error) {
|
|
344
|
+
return error && typeof error === 'object' && checkResult in error
|
|
345
|
+
? error[checkResult]
|
|
346
|
+
: undefined;
|
|
347
|
+
}
|
|
348
|
+
/** Уборка каталогов проверки; тест подменяет её, чтобы проверить отказ после исхода. */
|
|
349
|
+
export const checkCleanup = {
|
|
350
|
+
remove: (dir) => rm(dir, { recursive: true, force: true }),
|
|
351
|
+
};
|
|
311
352
|
/** Проверка механизма песочницы; тест подменяет её, чтобы проверить отказ. */
|
|
312
353
|
export const isolationBackend = { check: isolationSupport };
|
|
313
354
|
/**
|
package/package.json
CHANGED