@aviaratech/ai-delivery 0.3.6 → 0.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -0
- package/dist/agent.d.ts +1 -1
- package/dist/agent.js +1 -1
- package/dist/agent.js.map +1 -1
- package/dist/issue.js +12 -13
- package/dist/issue.js.map +1 -1
- package/dist/lifecycle.test.js +569 -1
- package/dist/lifecycle.test.js.map +1 -1
- package/dist/verification.d.ts +2 -0
- package/dist/verification.js +346 -52
- package/dist/verification.js.map +1 -1
- package/package.json +1 -1
- package/plugins/ai-delivery/.claude-plugin/plugin.json +1 -1
package/dist/verification.d.ts
CHANGED
|
@@ -35,6 +35,8 @@ declare const ResourceSummarySchema: z.ZodObject<{
|
|
|
35
35
|
sampleCount: z.ZodNumber;
|
|
36
36
|
}, z.core.$strict>;
|
|
37
37
|
export type VerificationResourceSummary = z.infer<typeof ResourceSummarySchema>;
|
|
38
|
+
/** Run a real filesystem-negative fixture between strict scans; the callback must clean it up before returning. */
|
|
39
|
+
export declare function withVerificationFilesystemFixture<T>(operation: () => T | Promise<T>): Promise<T>;
|
|
38
40
|
/** @internal Durable transition evidence must precede changes to the existing writer slot. */
|
|
39
41
|
export interface WorktreeTransitionWriterLeaseInput {
|
|
40
42
|
repoRoot: string;
|
package/dist/verification.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn, spawnSync } from 'node:child_process';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
|
-
import { closeSync, existsSync, fsyncSync, openSync, unlinkSync, lstatSync, readFileSync, readdirSync, realpathSync, statfsSync, } from 'node:fs';
|
|
4
|
+
import { closeSync, existsSync, fsyncSync, openSync, unlinkSync, lstatSync, mkdirSync, rmdirSync, renameSync, readFileSync, readdirSync, realpathSync, statfsSync, } from 'node:fs';
|
|
5
5
|
import { basename, dirname, join, resolve, sep } from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import { z } from 'zod';
|
|
@@ -125,6 +125,254 @@ function assertResourceBounds(bounds) {
|
|
|
125
125
|
if (process.platform === 'win32')
|
|
126
126
|
throw new DeliveryError('Resource observation requires POSIX process support.');
|
|
127
127
|
}
|
|
128
|
+
const OUTPUT_GATE_ENV = 'AI_DELIVERY_OUTPUT_OBSERVATION_GATE';
|
|
129
|
+
const GateOwnerSchema = z.strictObject({ pid: z.number().int().positive(), identity: z.string().min(1) });
|
|
130
|
+
const OutputGateSchema = z.strictObject({ directory: z.string().min(1), id: z.uuid(), owner: GateOwnerSchema });
|
|
131
|
+
const GateClaimSchema = z.strictObject({
|
|
132
|
+
gateId: z.uuid(),
|
|
133
|
+
id: z.uuid(),
|
|
134
|
+
kind: z.enum(['scan', 'fixture']),
|
|
135
|
+
owner: GateOwnerSchema,
|
|
136
|
+
});
|
|
137
|
+
function gateOwner() {
|
|
138
|
+
const owner = processSnapshot().get(process.pid);
|
|
139
|
+
if (!owner)
|
|
140
|
+
throw new DeliveryError('Filesystem observation gate owner is unavailable.');
|
|
141
|
+
return { pid: owner.pid, identity: owner.identity };
|
|
142
|
+
}
|
|
143
|
+
function assertGateDirectory(path) {
|
|
144
|
+
const metadata = lstatSync(path);
|
|
145
|
+
if (!metadata.isDirectory() || (metadata.mode & 0o077) !== 0 || realpathSync(path) !== path)
|
|
146
|
+
throw new DeliveryError('Filesystem observation gate directory is invalid.');
|
|
147
|
+
}
|
|
148
|
+
function readOutputGate(value) {
|
|
149
|
+
try {
|
|
150
|
+
const gate = OutputGateSchema.parse(JSON.parse(value));
|
|
151
|
+
assertOutputGate(gate);
|
|
152
|
+
return gate;
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
throw new DeliveryError('Filesystem observation gate capability is invalid or stale: ' +
|
|
156
|
+
(error instanceof Error ? error.message : String(error)));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function gateOwnerAlive(owner, snapshot = processSnapshot()) {
|
|
160
|
+
const current = snapshot.get(owner.pid);
|
|
161
|
+
return current?.identity === owner.identity && !current.status.startsWith('Z');
|
|
162
|
+
}
|
|
163
|
+
function assertOutputGate(gate, abandoned = false) {
|
|
164
|
+
assertGateDirectory(gate.directory);
|
|
165
|
+
const manifest = OutputGateSchema.parse(JSON.parse(assertPrivateFile(join(gate.directory, 'gate.json')).toString('utf8')));
|
|
166
|
+
const snapshot = processSnapshot();
|
|
167
|
+
let actor = snapshot.get(process.pid);
|
|
168
|
+
const ancestors = new Set();
|
|
169
|
+
while (actor && actor.pid !== gate.owner.pid && !ancestors.has(actor.pid)) {
|
|
170
|
+
ancestors.add(actor.pid);
|
|
171
|
+
actor = snapshot.get(actor.ppid);
|
|
172
|
+
}
|
|
173
|
+
if (digestValue(manifest) !== digestValue(gate) ||
|
|
174
|
+
(abandoned
|
|
175
|
+
? gateOwnerAlive(gate.owner, snapshot)
|
|
176
|
+
: !gateOwnerAlive(gate.owner, snapshot) || actor?.identity !== gate.owner.identity))
|
|
177
|
+
throw new DeliveryError('Filesystem observation gate ownership changed, was abandoned or is not an inherited ancestor.');
|
|
178
|
+
}
|
|
179
|
+
function readGateClaim(gate, path) {
|
|
180
|
+
const claim = GateClaimSchema.parse(JSON.parse(assertPrivateFile(path).toString('utf8')));
|
|
181
|
+
if (claim.gateId !== gate.id)
|
|
182
|
+
throw new DeliveryError('Filesystem observation gate claim belongs to another run.');
|
|
183
|
+
return claim;
|
|
184
|
+
}
|
|
185
|
+
function gateScanRequests(gate) {
|
|
186
|
+
const directory = join(gate.directory, 'requests');
|
|
187
|
+
assertGateDirectory(directory);
|
|
188
|
+
const requests = readdirSync(directory);
|
|
189
|
+
if (requests.length > 1024 || requests.some((name) => !/^[a-f0-9-]{36}\.json$/u.test(name)))
|
|
190
|
+
throw new DeliveryError('Filesystem observation gate scan requests are invalid.');
|
|
191
|
+
const live = [];
|
|
192
|
+
const snapshot = processSnapshot();
|
|
193
|
+
for (const name of requests) {
|
|
194
|
+
let claim;
|
|
195
|
+
try {
|
|
196
|
+
claim = readGateClaim(gate, join(directory, name));
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
if (error.code === 'ENOENT')
|
|
200
|
+
continue;
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
if (claim.kind !== 'scan' || name !== claim.id + '.json' || !gateOwnerAlive(claim.owner, snapshot))
|
|
204
|
+
throw new DeliveryError('Filesystem observation gate scan request was abandoned.');
|
|
205
|
+
live.push(name);
|
|
206
|
+
}
|
|
207
|
+
return live;
|
|
208
|
+
}
|
|
209
|
+
function removeGateClaim(gate, path, claim) {
|
|
210
|
+
if (digestValue(readGateClaim(gate, path)) !== digestValue(claim))
|
|
211
|
+
throw new DeliveryError('Filesystem observation gate claim changed before release.');
|
|
212
|
+
unlinkSync(path);
|
|
213
|
+
}
|
|
214
|
+
async function withOutputGateLock(gate, kind, operation, signal) {
|
|
215
|
+
if (!gate)
|
|
216
|
+
return operation();
|
|
217
|
+
assertOutputGate(gate);
|
|
218
|
+
const claim = { gateId: gate.id, id: randomUUID(), kind, owner: gateOwner() };
|
|
219
|
+
const request = kind === 'scan' ? join(gate.directory, 'requests', claim.id + '.json') : undefined;
|
|
220
|
+
const lock = join(gate.directory, 'held');
|
|
221
|
+
const holder = join(lock, 'owner.json');
|
|
222
|
+
if (request) {
|
|
223
|
+
const staging = join(gate.directory, claim.id + '.pending');
|
|
224
|
+
writePrivateJsonFileAtomically(staging, claim);
|
|
225
|
+
renameSync(staging, request);
|
|
226
|
+
}
|
|
227
|
+
let acquired;
|
|
228
|
+
let incompleteSince;
|
|
229
|
+
let fixtureWait;
|
|
230
|
+
const release = () => {
|
|
231
|
+
if (!acquired)
|
|
232
|
+
return;
|
|
233
|
+
const metadata = lstatSync(lock);
|
|
234
|
+
if (!metadata.isDirectory() || metadata.dev !== acquired.dev || metadata.ino !== acquired.ino)
|
|
235
|
+
throw new DeliveryError('Filesystem observation gate lock changed before release.');
|
|
236
|
+
if (readdirSync(lock).join(',') !== 'owner.json')
|
|
237
|
+
throw new DeliveryError('Filesystem observation gate lock has unrelated metadata.');
|
|
238
|
+
removeGateClaim(gate, holder, claim);
|
|
239
|
+
rmdirSync(lock);
|
|
240
|
+
acquired = undefined;
|
|
241
|
+
};
|
|
242
|
+
try {
|
|
243
|
+
for (;;) {
|
|
244
|
+
if (signal?.aborted)
|
|
245
|
+
throw new DeliveryError('Filesystem observation gate wait cancelled.');
|
|
246
|
+
assertOutputGate(gate);
|
|
247
|
+
const requests = gateScanRequests(gate);
|
|
248
|
+
if (kind === 'scan' || requests.length === 0) {
|
|
249
|
+
let created = false;
|
|
250
|
+
try {
|
|
251
|
+
mkdirSync(lock, { mode: 0o700 });
|
|
252
|
+
created = true;
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
if (error.code !== 'EEXIST')
|
|
256
|
+
throw error;
|
|
257
|
+
}
|
|
258
|
+
if (created) {
|
|
259
|
+
const metadata = lstatSync(lock);
|
|
260
|
+
acquired = { dev: metadata.dev, ino: metadata.ino };
|
|
261
|
+
writePrivateJsonFileAtomically(holder, claim);
|
|
262
|
+
// A scan may queue between the fixture's request check and atomic lock acquisition.
|
|
263
|
+
if (kind === 'scan' || gateScanRequests(gate).length === 0)
|
|
264
|
+
return await operation();
|
|
265
|
+
release();
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (existsSync(lock)) {
|
|
269
|
+
try {
|
|
270
|
+
assertGateDirectory(lock);
|
|
271
|
+
const current = readGateClaim(gate, holder);
|
|
272
|
+
incompleteSince = undefined;
|
|
273
|
+
if (!gateOwnerAlive(current.owner))
|
|
274
|
+
throw new DeliveryError('Filesystem observation gate fixture or scanner was abandoned.');
|
|
275
|
+
if (kind === 'scan' && current.kind === 'fixture') {
|
|
276
|
+
if (fixtureWait?.claimId !== current.id)
|
|
277
|
+
fixtureWait = { claimId: current.id, since: Date.now() };
|
|
278
|
+
// A negative reader that blocks prevents strict output observation; terminate this command, never steal its live lease.
|
|
279
|
+
if (Date.now() - fixtureWait.since > 5000)
|
|
280
|
+
throw new DeliveryError('Filesystem-negative fixture did not release a waiting output scan within 5 seconds; reader or teardown is stalled.');
|
|
281
|
+
}
|
|
282
|
+
else
|
|
283
|
+
fixtureWait = undefined;
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
if (error.code !== 'ENOENT')
|
|
287
|
+
throw error;
|
|
288
|
+
incompleteSince ??= Date.now();
|
|
289
|
+
// A concrete publication failure, never an elapsed fixture-work allowance or stale-lock takeover.
|
|
290
|
+
if (Date.now() - incompleteSince > 2000)
|
|
291
|
+
throw new DeliveryError('Filesystem observation gate owner publication is incomplete.');
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
finally {
|
|
298
|
+
try {
|
|
299
|
+
release();
|
|
300
|
+
}
|
|
301
|
+
finally {
|
|
302
|
+
if (request)
|
|
303
|
+
removeGateClaim(gate, request, claim);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
/** Run a real filesystem-negative fixture between strict scans; the callback must clean it up before returning. */
|
|
308
|
+
export async function withVerificationFilesystemFixture(operation) {
|
|
309
|
+
const capability = process.env[OUTPUT_GATE_ENV];
|
|
310
|
+
return withOutputGateLock(capability === undefined ? undefined : readOutputGate(capability), 'fixture', operation);
|
|
311
|
+
}
|
|
312
|
+
function retireOutputGate(gate, abandoned = false) {
|
|
313
|
+
assertOutputGate(gate, abandoned);
|
|
314
|
+
const requests = join(gate.directory, 'requests');
|
|
315
|
+
const held = join(gate.directory, 'held');
|
|
316
|
+
if (readdirSync(gate.directory).some((name) => !['gate.json', 'requests', 'held'].includes(name)))
|
|
317
|
+
throw new DeliveryError('Filesystem observation gate has unrelated or incomplete metadata.');
|
|
318
|
+
if (existsSync(held)) {
|
|
319
|
+
assertGateDirectory(held);
|
|
320
|
+
if (readdirSync(held).join(',') !== 'owner.json')
|
|
321
|
+
throw new DeliveryError('Filesystem observation gate lock has unrelated metadata.');
|
|
322
|
+
const claim = readGateClaim(gate, join(held, 'owner.json'));
|
|
323
|
+
if (gateOwnerAlive(claim.owner))
|
|
324
|
+
throw new DeliveryError('Filesystem observation gate still has a live holder.');
|
|
325
|
+
removeGateClaim(gate, join(held, 'owner.json'), claim);
|
|
326
|
+
rmdirSync(held);
|
|
327
|
+
}
|
|
328
|
+
for (const name of readdirSync(requests)) {
|
|
329
|
+
if (!/^[a-f0-9-]{36}\.json$/u.test(name))
|
|
330
|
+
throw new DeliveryError('Filesystem observation gate has unrelated metadata.');
|
|
331
|
+
const path = join(requests, name);
|
|
332
|
+
const claim = readGateClaim(gate, path);
|
|
333
|
+
if (claim.kind !== 'scan' || name !== claim.id + '.json')
|
|
334
|
+
throw new DeliveryError('Filesystem observation gate has unrelated scan metadata.');
|
|
335
|
+
if (gateOwnerAlive(claim.owner))
|
|
336
|
+
throw new DeliveryError('Filesystem observation gate still has a live scan request.');
|
|
337
|
+
removeGateClaim(gate, path, claim);
|
|
338
|
+
}
|
|
339
|
+
rmdirSync(requests);
|
|
340
|
+
if (digestValue(JSON.parse(assertPrivateFile(join(gate.directory, 'gate.json')).toString('utf8'))) !==
|
|
341
|
+
digestValue(gate))
|
|
342
|
+
throw new DeliveryError('Filesystem observation gate changed before retirement.');
|
|
343
|
+
unlinkSync(join(gate.directory, 'gate.json'));
|
|
344
|
+
rmdirSync(gate.directory);
|
|
345
|
+
}
|
|
346
|
+
async function withOutputObservationGate(root, enabled, operation) {
|
|
347
|
+
const inherited = process.env[OUTPUT_GATE_ENV];
|
|
348
|
+
if (inherited !== undefined)
|
|
349
|
+
return operation(readOutputGate(inherited));
|
|
350
|
+
if (!enabled)
|
|
351
|
+
return operation(undefined);
|
|
352
|
+
const id = randomUUID();
|
|
353
|
+
const namespace = join(gitCommonDir(root), 'ai-delivery', 'output-observation', worktreeDigest(root).slice(7));
|
|
354
|
+
ensurePrivateDirectoryDurably(namespace);
|
|
355
|
+
// The existing writer has recovered and confirmed old command quiescence before this operation.
|
|
356
|
+
for (const priorId of readdirSync(namespace)) {
|
|
357
|
+
z.uuid().parse(priorId);
|
|
358
|
+
const path = join(namespace, priorId);
|
|
359
|
+
assertGateDirectory(path);
|
|
360
|
+
const prior = OutputGateSchema.parse(JSON.parse(assertPrivateFile(join(path, 'gate.json')).toString('utf8')));
|
|
361
|
+
if (prior.id !== priorId || prior.directory !== path || gateOwnerAlive(prior.owner))
|
|
362
|
+
throw new DeliveryError('Prior filesystem observation gate is live or has conflicting ownership.');
|
|
363
|
+
retireOutputGate(prior, true);
|
|
364
|
+
}
|
|
365
|
+
const directory = join(namespace, id);
|
|
366
|
+
ensurePrivateDirectoryDurably(join(directory, 'requests'));
|
|
367
|
+
const gate = { directory, id, owner: gateOwner() };
|
|
368
|
+
writePrivateJsonFileAtomically(join(directory, 'gate.json'), gate);
|
|
369
|
+
try {
|
|
370
|
+
return await operation(gate);
|
|
371
|
+
}
|
|
372
|
+
finally {
|
|
373
|
+
retireOutputGate(gate);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
128
376
|
const OutputBaselineCheckpointSchema = z.strictObject({
|
|
129
377
|
baselineId: z.string().regex(/^sha256:[a-f0-9]{64}$/u),
|
|
130
378
|
files: z.array(z.tuple([z.string().min(1), z.number().int().nonnegative().safe()])),
|
|
@@ -187,7 +435,7 @@ function scanOutputRoots(roots) {
|
|
|
187
435
|
}
|
|
188
436
|
return files;
|
|
189
437
|
}
|
|
190
|
-
function outputBaseline(repoRoot, common, classificationReceiptId, bounds) {
|
|
438
|
+
async function outputBaseline(repoRoot, common, classificationReceiptId, bounds, gate, signal) {
|
|
191
439
|
if (bounds.outputRoots === undefined)
|
|
192
440
|
return undefined;
|
|
193
441
|
const roots = bounds.outputRoots.map((path) => resolve(repoRoot, path)).sort();
|
|
@@ -236,7 +484,7 @@ function outputBaseline(repoRoot, common, classificationReceiptId, bounds) {
|
|
|
236
484
|
let checkpoint;
|
|
237
485
|
if (state?.status !== 'complete') {
|
|
238
486
|
const content = {
|
|
239
|
-
files: [...scanOutputRoots(roots)].sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0)),
|
|
487
|
+
files: [...(await withOutputGateLock(gate, 'scan', () => scanOutputRoots(roots), signal))].sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0)),
|
|
240
488
|
inputId,
|
|
241
489
|
roots,
|
|
242
490
|
schemaVersion: 'ai-delivery.output-baseline@1',
|
|
@@ -261,11 +509,11 @@ function outputBaseline(repoRoot, common, classificationReceiptId, bounds) {
|
|
|
261
509
|
checkpoint.files.some(([file], index) => (index > 0 && file <= checkpoint.files[index - 1][0]) ||
|
|
262
510
|
!roots.some((root) => file === root || file.startsWith(`${root}${sep}`))))
|
|
263
511
|
throw new DeliveryError('Filesystem output baseline checkpoint is corrupt or belongs to another input.');
|
|
264
|
-
return { baselineId, roots, files: new Map(checkpoint.files) };
|
|
512
|
+
return { baselineId, roots, files: new Map(checkpoint.files), ...(gate === undefined ? {} : { gate }) };
|
|
265
513
|
}
|
|
266
|
-
function positiveNewOutputBytes(baseline) {
|
|
514
|
+
async function positiveNewOutputBytes(baseline, signal) {
|
|
267
515
|
let bytes = 0;
|
|
268
|
-
for (const [path, size] of scanOutputRoots(baseline.roots)) {
|
|
516
|
+
for (const [path, size] of await withOutputGateLock(baseline.gate, 'scan', () => scanOutputRoots(baseline.roots), signal)) {
|
|
269
517
|
bytes += Math.max(0, size - (baseline.files.get(path) ?? 0));
|
|
270
518
|
if (!Number.isSafeInteger(bytes))
|
|
271
519
|
throw new DeliveryError('Filesystem output observation overflowed.');
|
|
@@ -382,7 +630,7 @@ function observeOwnedProcesses(state, snapshot) {
|
|
|
382
630
|
state.sampled = true;
|
|
383
631
|
return [...selected.values()];
|
|
384
632
|
}
|
|
385
|
-
function sampleOwnedTree(state, repoRoot, bounds, baseline) {
|
|
633
|
+
function sampleOwnedTree(state, repoRoot, bounds, baseline, newOutputBytes) {
|
|
386
634
|
const owned = observeOwnedProcesses(state, processSnapshot());
|
|
387
635
|
const rssBytes = owned.reduce((total, member) => total + member.rssBytes, 0);
|
|
388
636
|
if (!Number.isSafeInteger(rssBytes))
|
|
@@ -393,7 +641,6 @@ function sampleOwnedTree(state, repoRoot, bounds, baseline) {
|
|
|
393
641
|
const available = freeDiskBytes(repoRoot, baseline);
|
|
394
642
|
if (available < bounds.minFreeDiskBytes)
|
|
395
643
|
throw new DeliveryError(`Free disk fell below limit ${bounds.minFreeDiskBytes}.`);
|
|
396
|
-
const newOutputBytes = baseline === undefined ? undefined : positiveNewOutputBytes(baseline);
|
|
397
644
|
if (newOutputBytes !== undefined && !Number.isSafeInteger(newOutputBytes)) {
|
|
398
645
|
throw new DeliveryError('Filesystem output observation overflowed.');
|
|
399
646
|
}
|
|
@@ -886,6 +1133,9 @@ function runStageCommand(repoRoot, argv, abortSignal, onRunning, resourceBounds,
|
|
|
886
1133
|
cwd: repoRoot,
|
|
887
1134
|
detached: process.platform !== 'win32',
|
|
888
1135
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1136
|
+
...(baseline?.gate === undefined
|
|
1137
|
+
? {}
|
|
1138
|
+
: { env: { ...process.env, [OUTPUT_GATE_ENV]: JSON.stringify(baseline.gate) } }),
|
|
889
1139
|
});
|
|
890
1140
|
const stdout = [];
|
|
891
1141
|
const stderr = [];
|
|
@@ -899,6 +1149,8 @@ function runStageCommand(repoRoot, argv, abortSignal, onRunning, resourceBounds,
|
|
|
899
1149
|
let closed = false;
|
|
900
1150
|
let settled = false;
|
|
901
1151
|
let commandReleased = false;
|
|
1152
|
+
let pendingOutput;
|
|
1153
|
+
const scanCancellation = new AbortController();
|
|
902
1154
|
let closeTimer;
|
|
903
1155
|
let exitPipeTimer;
|
|
904
1156
|
let resourceTimer;
|
|
@@ -955,6 +1207,7 @@ function runStageCommand(repoRoot, argv, abortSignal, onRunning, resourceBounds,
|
|
|
955
1207
|
if (failure !== undefined)
|
|
956
1208
|
return;
|
|
957
1209
|
failure = reason;
|
|
1210
|
+
scanCancellation.abort();
|
|
958
1211
|
killOwned();
|
|
959
1212
|
if (!closed) {
|
|
960
1213
|
closeTimer = setTimeout(() => {
|
|
@@ -1011,10 +1264,26 @@ function runStageCommand(repoRoot, argv, abortSignal, onRunning, resourceBounds,
|
|
|
1011
1264
|
onResourceSample?.(result);
|
|
1012
1265
|
onRunning?.(outputBytes, result);
|
|
1013
1266
|
}
|
|
1014
|
-
if (
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1267
|
+
if (closed || pendingOutput !== undefined)
|
|
1268
|
+
return;
|
|
1269
|
+
pendingOutput = (async () => {
|
|
1270
|
+
if (resourceBounds !== undefined && baseline !== undefined) {
|
|
1271
|
+
const bytes = await positiveNewOutputBytes(baseline, scanCancellation.signal);
|
|
1272
|
+
if (failure !== undefined)
|
|
1273
|
+
return;
|
|
1274
|
+
const result = sampleOwnedTree(owned, repoRoot, resourceBounds, baseline, bytes);
|
|
1275
|
+
onResourceSample?.(result);
|
|
1276
|
+
onRunning?.(outputBytes, result);
|
|
1277
|
+
}
|
|
1278
|
+
if (failure === undefined && !commandReleased) {
|
|
1279
|
+
commandReleased = true;
|
|
1280
|
+
child.stdin.end('run\n');
|
|
1281
|
+
}
|
|
1282
|
+
})()
|
|
1283
|
+
.catch((error) => stop(error instanceof Error ? error.message : String(error)))
|
|
1284
|
+
.finally(() => {
|
|
1285
|
+
pendingOutput = undefined;
|
|
1286
|
+
});
|
|
1018
1287
|
}
|
|
1019
1288
|
catch (error) {
|
|
1020
1289
|
stop(error instanceof Error ? error.message : String(error));
|
|
@@ -1038,11 +1307,11 @@ function runStageCommand(repoRoot, argv, abortSignal, onRunning, resourceBounds,
|
|
|
1038
1307
|
closed = true;
|
|
1039
1308
|
if (settled)
|
|
1040
1309
|
return;
|
|
1041
|
-
|
|
1310
|
+
await pendingOutput;
|
|
1042
1311
|
if (failure === undefined && owned !== undefined) {
|
|
1043
1312
|
try {
|
|
1044
1313
|
if (resourceBounds !== undefined) {
|
|
1045
|
-
const sample = sampleOwnedTree(owned, repoRoot, resourceBounds, baseline);
|
|
1314
|
+
const sample = sampleOwnedTree(owned, repoRoot, resourceBounds, baseline, baseline === undefined ? undefined : await positiveNewOutputBytes(baseline, scanCancellation.signal));
|
|
1046
1315
|
onResourceSample?.(sample);
|
|
1047
1316
|
onRunning?.(outputBytes, sample);
|
|
1048
1317
|
}
|
|
@@ -1085,18 +1354,20 @@ function runStageCommand(repoRoot, argv, abortSignal, onRunning, resourceBounds,
|
|
|
1085
1354
|
resolve(Buffer.concat([...stdout, ...stderr], outputBytes));
|
|
1086
1355
|
};
|
|
1087
1356
|
child.once('close', (code, signal) => {
|
|
1088
|
-
void handleClose(code, signal)
|
|
1357
|
+
void handleClose(code, signal)
|
|
1358
|
+
.catch((error) => {
|
|
1089
1359
|
settled = true;
|
|
1090
1360
|
reject(error);
|
|
1091
|
-
})
|
|
1361
|
+
})
|
|
1362
|
+
.finally(clearWatchers);
|
|
1092
1363
|
});
|
|
1093
1364
|
});
|
|
1094
1365
|
}
|
|
1095
1366
|
export async function verifyIssue(input) {
|
|
1096
1367
|
const root = gitRoot(input.repoRoot);
|
|
1097
|
-
return withVerificationWriter(root, (writer) => verifyIssueOwned(input, writer));
|
|
1368
|
+
return withVerificationWriter(root, (writer) => withOutputObservationGate(root, input.resourceBounds?.outputRoots !== undefined, (gate) => verifyIssueOwned(input, writer, gate)));
|
|
1098
1369
|
}
|
|
1099
|
-
async function verifyIssueOwned(input, writer) {
|
|
1370
|
+
async function verifyIssueOwned(input, writer, gate) {
|
|
1100
1371
|
const assertNotCancelled = () => {
|
|
1101
1372
|
if (input.signal?.aborted)
|
|
1102
1373
|
throw new DeliveryError('Verification cancelled.');
|
|
@@ -1126,7 +1397,7 @@ async function verifyIssueOwned(input, writer) {
|
|
|
1126
1397
|
});
|
|
1127
1398
|
const baseline = input.resourceBounds === undefined
|
|
1128
1399
|
? undefined
|
|
1129
|
-
: outputBaseline(root, common, classification.receiptId, input.resourceBounds);
|
|
1400
|
+
: await outputBaseline(root, common, classification.receiptId, input.resourceBounds, gate, input.signal);
|
|
1130
1401
|
if (input.resourceBounds !== undefined)
|
|
1131
1402
|
assertDiskHeadroom(root, input.resourceBounds, baseline);
|
|
1132
1403
|
const receipts = [];
|
|
@@ -1279,6 +1550,9 @@ async function verifyIssueOwned(input, writer) {
|
|
|
1279
1550
|
sampledFreeDiskBytes: sample.freeDiskBytes,
|
|
1280
1551
|
...(sample.newOutputBytes === undefined ? {} : { sampledNewOutputBytes: sample.newOutputBytes }),
|
|
1281
1552
|
ownedProcessCount: sample.ownedProcessCount,
|
|
1553
|
+
...(baseline !== undefined && sample.newOutputBytes === undefined
|
|
1554
|
+
? { reason: 'waiting for strict filesystem output scan' }
|
|
1555
|
+
: {}),
|
|
1282
1556
|
}),
|
|
1283
1557
|
});
|
|
1284
1558
|
progress(0);
|
|
@@ -1341,6 +1615,19 @@ async function verifyIssueOwned(input, writer) {
|
|
|
1341
1615
|
});
|
|
1342
1616
|
}
|
|
1343
1617
|
assertNotCancelled();
|
|
1618
|
+
if (baseline !== undefined && input.resourceBounds !== undefined) {
|
|
1619
|
+
const bytes = await positiveNewOutputBytes(baseline, input.signal);
|
|
1620
|
+
if (bytes > input.resourceBounds.maxNewOutputBytes)
|
|
1621
|
+
throw new DeliveryError('Positive new filesystem output ' + bytes + ' exceeded limit ' + input.resourceBounds.maxNewOutputBytes + '.');
|
|
1622
|
+
assertDiskHeadroom(root, input.resourceBounds, baseline);
|
|
1623
|
+
recordResourceSample({
|
|
1624
|
+
aggregateRssBytes: 0,
|
|
1625
|
+
freeDiskBytes: freeDiskBytes(root, baseline),
|
|
1626
|
+
newOutputBytes: bytes,
|
|
1627
|
+
ownedProcessCount: 0,
|
|
1628
|
+
});
|
|
1629
|
+
}
|
|
1630
|
+
assertNotCancelled();
|
|
1344
1631
|
assertRepositoryClassificationCurrent({
|
|
1345
1632
|
classification,
|
|
1346
1633
|
configDigest: loaded.configDigest,
|
|
@@ -1429,41 +1716,48 @@ export async function withRuntimeSetupWriter(root, operation) {
|
|
|
1429
1716
|
if (roots[i] === roots[i - 1] || roots[i].startsWith(`${roots[i - 1]}${sep}`))
|
|
1430
1717
|
throw new DeliveryError('Filesystem output roots must not overlap.');
|
|
1431
1718
|
}
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
baselineId: digestValue([...files]),
|
|
1439
|
-
};
|
|
1440
|
-
assertDiskHeadroom(root, bounds, baseline);
|
|
1441
|
-
const started = Date.now();
|
|
1442
|
-
const output = await runStageCommand(root, argv, signal, (capturedOutputBytes, sample) => reportVerificationProgress({
|
|
1443
|
-
state: 'running',
|
|
1444
|
-
stageId: 'runtime-setup',
|
|
1445
|
-
completedStages: 0,
|
|
1446
|
-
reusedStages: 0,
|
|
1447
|
-
remainingStages: 1,
|
|
1448
|
-
elapsedMs: Date.now() - started,
|
|
1449
|
-
capturedOutputBytes,
|
|
1450
|
-
...(sample === undefined
|
|
1451
|
-
? {}
|
|
1719
|
+
return withOutputObservationGate(root, roots !== undefined, async (gate) => {
|
|
1720
|
+
const files = roots === undefined
|
|
1721
|
+
? undefined
|
|
1722
|
+
: await withOutputGateLock(gate, 'scan', () => scanOutputRoots(roots), signal);
|
|
1723
|
+
const baseline = roots === undefined || files === undefined
|
|
1724
|
+
? undefined
|
|
1452
1725
|
: {
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1726
|
+
roots,
|
|
1727
|
+
files,
|
|
1728
|
+
baselineId: digestValue([...files]),
|
|
1729
|
+
...(gate === undefined ? {} : { gate }),
|
|
1730
|
+
};
|
|
1731
|
+
assertDiskHeadroom(root, bounds, baseline);
|
|
1732
|
+
const started = Date.now();
|
|
1733
|
+
const output = await runStageCommand(root, argv, signal, (capturedOutputBytes, sample) => reportVerificationProgress({
|
|
1734
|
+
state: 'running',
|
|
1735
|
+
stageId: 'runtime-setup',
|
|
1736
|
+
completedStages: 0,
|
|
1737
|
+
reusedStages: 0,
|
|
1738
|
+
remainingStages: 1,
|
|
1739
|
+
elapsedMs: Date.now() - started,
|
|
1740
|
+
capturedOutputBytes,
|
|
1741
|
+
...(sample === undefined
|
|
1742
|
+
? {}
|
|
1743
|
+
: {
|
|
1744
|
+
sampledAggregateRssBytes: sample.aggregateRssBytes,
|
|
1745
|
+
sampledFreeDiskBytes: sample.freeDiskBytes,
|
|
1746
|
+
sampledNewOutputBytes: sample.newOutputBytes,
|
|
1747
|
+
}),
|
|
1748
|
+
}), bounds, baseline, undefined, writer);
|
|
1749
|
+
if (signal?.aborted)
|
|
1750
|
+
throw new DeliveryError('Runtime setup cancelled.');
|
|
1751
|
+
reportVerificationProgress({
|
|
1752
|
+
state: 'completed',
|
|
1753
|
+
stageId: 'runtime-setup',
|
|
1754
|
+
completedStages: 1,
|
|
1755
|
+
reusedStages: 0,
|
|
1756
|
+
remainingStages: 0,
|
|
1757
|
+
elapsedMs: Date.now() - started,
|
|
1758
|
+
});
|
|
1759
|
+
return output;
|
|
1465
1760
|
});
|
|
1466
|
-
return output;
|
|
1467
1761
|
},
|
|
1468
1762
|
}));
|
|
1469
1763
|
}
|