@agent-delivery-harness/cli 0.7.4 → 0.8.1

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.
@@ -1,10 +1,10 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import path from "node:path";
3
- import { captureCheckBindings, captureCheckOutputSnapshots, digestCanonical, classifyCandidateDrift, computeCheckWiringFingerprint, createBlocker, createExecPort, sha256Hex, submitManifest, type Blocker, type CandidateBinding, type ProviderRegistration } from "@agent-delivery-harness/kernel";
3
+ import { captureCheckBindings, captureCheckOutputSnapshots, digestCanonical, classifyCandidateDrift, computeCheckWiringFingerprint, createBlocker, createExecPort, sha256Hex, submitManifest, type Blocker, type CandidateBinding, type CheckBindingOptions, type ProviderRegistration, type SubmissionOptions } from "@agent-delivery-harness/kernel";
4
4
  import type { CommandContext } from "./boundary.ts";
5
5
 
6
6
  /** Runs one declared argv check and submits its product-constructed terminal evidence. */
7
- export async function runDeclaredCheck(context: CommandContext, provider: ProviderRegistration, obligationIds: readonly string[], before: CandidateBinding): Promise<readonly Blocker[]> {
7
+ export async function runDeclaredCheck(context: CommandContext, provider: ProviderRegistration, obligationIds: readonly string[], before: CandidateBinding, scoped: Pick<CheckBindingOptions, "scopedPlan" | "readOutput"> & Pick<SubmissionOptions, "resourceBindings"> = {}): Promise<readonly Blocker[]> {
8
8
  const check = provider.check!;
9
9
  const wiring = await context.wire();
10
10
  const fail = (code: string, summary: string, details?: string): readonly Blocker[] => [createBlocker({ code, source: { kind: "command", id: "delivery-harness.cli.gate" }, summary,
@@ -21,7 +21,7 @@ export async function runDeclaredCheck(context: CommandContext, provider: Provid
21
21
  const captured = await wiring.captureCandidate();
22
22
  if (!captured.ok) return captured.blockers;
23
23
  if (classifyCandidateDrift(before, captured.candidate).length || start.candidate.headSha !== captured.candidate.headSha || fingerprint !== await computeCheckWiringFingerprint(context.rootDir, context.config, wiring.storageOptions)) return fail("check_candidate_changed", "The candidate, base or wiring changed while the check ran.");
24
- const binding = (await captureCheckBindings(context.rootDir, context.config, captured.candidate, wiring.storageOptions))[provider.id];
24
+ const binding = (await captureCheckBindings(context.rootDir, context.config, captured.candidate, { ...wiring.storageOptions, ...scoped }))[provider.id];
25
25
  if (binding === undefined) return fail("check_output_missing", `Declared check ${provider.id} has a missing, unreadable, oversized or escaped output.`);
26
26
  const runId = randomUUID(), finalPassId = "pass-1";
27
27
  const allocation = await context.artifacts.allocateRunRoot({ providerId: provider.id, runId });
@@ -45,6 +45,6 @@ export async function runDeclaredCheck(context: CommandContext, provider: Provid
45
45
  attestation: { level: "self", signatures: [] }, recordedAt: new Date().toISOString(), claims: obligationIds.map(obligation => ({ obligation, payloadSpec: "checks.passed/1", payload })) };
46
46
  const manifestPath = path.join(allocation.runRoot.path, "manifest.json");
47
47
  await context.artifacts.writeTextFile(manifestPath, JSON.stringify(manifest));
48
- const outcome = await submitManifest({ rootDir: context.rootDir, config: context.config, manifestPath }, { captureCandidate: wiring.captureCandidate, artifacts: context.artifacts, ...wiring.storageOptions });
48
+ const outcome = await submitManifest({ rootDir: context.rootDir, config: context.config, manifestPath }, { captureCandidate: wiring.captureCandidate, artifacts: context.artifacts, ...wiring.storageOptions, ...scoped });
49
49
  return outcome.status === "accepted" ? [] : outcome.blockers;
50
50
  }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * What `verify`, `gate`, `record` and `resume` do with the declared posture.
3
+ *
4
+ * Each calls this once its arguments have parsed and before anything else. Under
5
+ * `focused` the command returns the one outcome this builds, having read no
6
+ * delivery record and run no obligation, and that outcome is the whole record.
7
+ * It journals no completion: `gate ok` in a run would read as a gate that passed.
8
+ * Under `gate` the command carries on, with output as it would be had nothing
9
+ * been declared. The one exception is a malformed declaration: a repository said
10
+ * something and it didn't take, so its code and reason go to stderr, leaving
11
+ * what a command writes to stdout as it is.
12
+ */
13
+ import { resolveDeliveryPosture } from "@agent-delivery-harness/kernel";
14
+ import { CliInterruption, type CommandContext, type CommandResult } from "./boundary.ts";
15
+ import { oneLine } from "./run-surface.ts";
16
+
17
+ export async function focusedOutcome(context: CommandContext): Promise<Extract<CommandResult, { kind: "ok" }> | undefined> {
18
+ const resolved = await resolveDeliveryPosture({
19
+ rootDir: context.rootDir,
20
+ baseRef: context.config.baseRef,
21
+ ...(context.signal === undefined ? {} : { signal: context.signal }),
22
+ });
23
+ if (context.signal?.aborted) throw new CliInterruption();
24
+ if (resolved.posture === "focused") {
25
+ return {
26
+ kind: "ok",
27
+ unobserved: true,
28
+ summary: [
29
+ "outcome: focused",
30
+ `posture: ${resolved.posture}`,
31
+ `reason: ${oneLine(resolved.reason, 512)}`,
32
+ `source: ${oneLine(resolved.source, 256)}`,
33
+ `base tip: ${resolved.baseTipSha ?? "(none)"}`,
34
+ ].join("\n"),
35
+ };
36
+ }
37
+ if (resolved.code === "posture_malformed") context.writeError(`${resolved.code}: ${oneLine(resolved.reason, 512)}`);
38
+ return undefined;
39
+ }
@@ -0,0 +1,57 @@
1
+ import { createExecPort, captureInstalledDataResources, normalizeInstalledDataResourceRequests, InstalledDataResourceError, INSTALLED_DATA_LIMITS,
2
+ type InstalledDataResourceContext, type InstalledDataResourceRequest, type InstalledDataResourcePorts } from "@agent-delivery-harness/kernel";
3
+ import type { CheckSnapshot } from "./check-snapshot.ts";
4
+ import { installedDataWorker } from "./installed-data-worker.ts";
5
+
6
+ /** Only ScopedChecks may turn these observations into native applicability.
7
+ * This adapter's return alone is not an attestation or passing check result.
8
+ */
9
+ export async function captureOwnedInstalledDataResources(
10
+ snapshot: Pick<CheckSnapshot, "rootDir" | "verify">,
11
+ requests: readonly InstalledDataResourceRequest[],
12
+ context: InstalledDataResourceContext,
13
+ options: { readonly signal?: AbortSignal; readonly timeoutMs?: number } = {},
14
+ ) {
15
+ const normalized = normalizeInstalledDataResourceRequests(requests);
16
+ const timeoutMs = options.timeoutMs ?? 60_000;
17
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 60_000)
18
+ throw new InstalledDataResourceError("installed_data_descriptor_invalid", "Resource observer requires a bounded positive deadline.");
19
+ if (options.signal?.aborted)
20
+ throw new InstalledDataResourceError("installed_data_resource_unsafe", "Resource observation was cancelled before it started.");
21
+ const request = JSON.stringify({ requests: normalized, limits: INSTALLED_DATA_LIMITS });
22
+ if (Buffer.byteLength(request) > 64 * 1024) throw new InstalledDataResourceError("installed_data_limit_exceeded", "Resource request exceeds the bounded observer input.");
23
+ await snapshot.verify({ outputs: [] });
24
+ try {
25
+ const result = await createExecPort().run({ command: process.execPath, args: ["--input-type=module", "--eval", installedDataWorker], cwd: snapshot.rootDir,
26
+ env: { DELIVERY_INSTALLED_DATA_REQUEST: request }, timeoutMs, maxBuffer: INSTALLED_DATA_LIMITS.serializedBytes,
27
+ ...(options.signal ? { signal: options.signal } : {}) });
28
+ if (result.code !== 0) {
29
+ const code = result.stderr.trim();
30
+ if (code === "installed_data_resolution_unsafe" || code === "installed_data_resource_unsafe" || code === "installed_data_limit_exceeded")
31
+ throw new InstalledDataResourceError(code, "Owned installed resource observation refused.");
32
+ throw new InstalledDataResourceError("installed_data_resource_unsafe", "Owned installed resource observation did not complete.");
33
+ }
34
+ let observed: { entries: Record<string, { kind: string; physicalPath: string; linkTarget: string | null; bytesBase64?: string; members?: string[] }>; resolutions: Record<string, unknown> };
35
+ try { observed = JSON.parse(result.stdout); }
36
+ catch { throw new InstalledDataResourceError("installed_data_capture_invalid", "Owned resource observation returned malformed facts."); }
37
+ const ports: InstalledDataResourcePorts = {
38
+ resolve: async request => observed.resolutions[request.id] as Awaited<ReturnType<InstalledDataResourcePorts["resolve"]>>,
39
+ stat: async file => {
40
+ const entry = observed.entries[file];
41
+ if (!entry) throw new InstalledDataResourceError("installed_data_capture_invalid", "Owned resource metadata is incomplete.");
42
+ return { kind: entry.kind, physicalPath: entry.physicalPath, linkTarget: entry.linkTarget } as Awaited<ReturnType<InstalledDataResourcePorts["stat"]>>;
43
+ },
44
+ readFile: async file => {
45
+ const entry = observed.entries[file];
46
+ if (typeof entry?.bytesBase64 !== "string") throw new InstalledDataResourceError("installed_data_capture_invalid", "Owned resource bytes are incomplete.");
47
+ return Buffer.from(entry.bytesBase64, "base64");
48
+ },
49
+ listDirectory: async file => {
50
+ const entry = observed.entries[file];
51
+ if (!Array.isArray(entry?.members)) throw new InstalledDataResourceError("installed_data_capture_invalid", "Owned directory membership is incomplete.");
52
+ return entry.members;
53
+ },
54
+ };
55
+ return await captureInstalledDataResources(normalized, context, ports);
56
+ } finally { await snapshot.verify({ outputs: [] }); }
57
+ }
@@ -0,0 +1,83 @@
1
+ /** Runs only in a bounded, killable child over one invocation-owned snapshot.
2
+ * The parent validates descriptors first and validates/hashes these facts again.
3
+ * No dependency package code or host preload is executed by observation.
4
+ */
5
+ export const installedDataWorker = String.raw`
6
+ import { lstat, realpath, readdir, open } from 'node:fs/promises';
7
+ import { constants } from 'node:fs';
8
+ import { createRequire } from 'node:module';
9
+ import path from 'node:path';
10
+ const fail=(code)=>{const e=new Error(code);e.code=code;throw e;};
11
+ const input=JSON.parse(process.env.DELIVERY_INSTALLED_DATA_REQUEST);
12
+ const root=await realpath(process.cwd());
13
+ const entries=Object.create(null), resolutions=Object.create(null);
14
+ let rawBytes=0;
15
+ const safe=(value)=>typeof value==='string'&&value.length>0&&!value.includes('\\')&&!value.startsWith('/')&&value.split('/').every(p=>p&&p!=='.'&&p!=='..');
16
+ const absolute=(value)=>{if(!safe(value))fail('installed_data_resource_unsafe');return path.join(root,value);};
17
+ async function statOne(relative){
18
+ if(entries[relative])return entries[relative];
19
+ if(Object.keys(entries).length>=input.limits.entries)fail('installed_data_limit_exceeded');
20
+ let s;try{s=await lstat(absolute(relative));}catch(e){if(e.code!=='ENOENT')throw e;return entries[relative]={kind:'absent',physicalPath:relative,linkTarget:null};}
21
+ if(s.isSymbolicLink())fail('installed_data_resource_unsafe');
22
+ const physical=path.relative(root,await realpath(absolute(relative))).split(path.sep).join('/');
23
+ if(physical!==relative)fail('installed_data_resource_unsafe');
24
+ const kind=s.isFile()?'file':s.isDirectory()?'directory':'unsupported';
25
+ if(kind==='unsupported')fail('installed_data_resource_unsafe');
26
+ return entries[relative]={kind,physicalPath:physical,linkTarget:null};
27
+ }
28
+ async function observe(relative){
29
+ const parts=relative.split('/');
30
+ for(let i=1;i<parts.length;i++){
31
+ const ancestor=await statOne(parts.slice(0,i).join('/'));
32
+ if(ancestor.kind!=='directory'&&ancestor.kind!=='absent')fail('installed_data_resource_unsafe');
33
+ }
34
+ return statOne(relative);
35
+ }
36
+ async function read(relative){
37
+ const entry=await observe(relative);if(entry.kind!=='file')fail('installed_data_resource_unsafe');
38
+ if(entry.bytesBase64!==undefined)return;
39
+ const handle=await open(absolute(relative),constants.O_RDONLY|constants.O_NOFOLLOW);
40
+ try{
41
+ const s=await handle.stat();if(!s.isFile())fail('installed_data_resource_unsafe');
42
+ if(s.size>input.limits.rawBytes-rawBytes)fail('installed_data_limit_exceeded');
43
+ const bytes=Buffer.alloc(s.size+1);const {bytesRead}=await handle.read(bytes,0,bytes.length,0);
44
+ if(bytesRead!==s.size)fail('installed_data_resource_unsafe');
45
+ rawBytes+=bytesRead;entry.bytesBase64=bytes.subarray(0,bytesRead).toString('base64');
46
+ }finally{await handle.close();}
47
+ }
48
+ try{
49
+ for(const r of input.requests){
50
+ const issuer=await observe(r.issuer);if(issuer.kind!=='file')fail('installed_data_resource_unsafe');
51
+ const require=createRequire(absolute(r.issuer));
52
+ const paths=require.resolve.paths(r.package+'/package.json');
53
+ const boundary=paths?.indexOf(path.join(root,'node_modules'))??-1;
54
+ if(boundary<0)fail('installed_data_resolution_unsafe');
55
+ const search=[];
56
+ for(const dir of paths.slice(0,boundary+1)){
57
+ const candidate=path.relative(root,path.join(dir,r.package)).split(path.sep).join('/');
58
+ if(!safe(candidate))fail('installed_data_resolution_unsafe');
59
+ const entry=await observe(candidate);search.push({path:candidate,kind:entry.kind});
60
+ if(search.length<=boundary&&entry.kind!=='absent')fail('installed_data_resolution_unsafe');
61
+ }
62
+ const packageRoot='node_modules/'+r.package, manifestPath=packageRoot+'/package.json';
63
+ await read(manifestPath);
64
+ let resolved;try{resolved=require.resolve(r.package+'/package.json');}catch{fail('installed_data_resolution_unsafe');}
65
+ if(resolved!==absolute(manifestPath))fail('installed_data_resolution_unsafe');
66
+ resolutions[r.id]={mode:'require',packageRoot,manifestPath,search};
67
+ for(const f of r.files)await read(packageRoot+'/'+f);
68
+ for(const d of r.directories){
69
+ const directory=packageRoot+'/'+d.path,entry=await observe(directory);
70
+ if(entry.kind!=='directory')fail('installed_data_resource_unsafe');
71
+ const names=await readdir(absolute(directory));if(names.length>input.limits.directoryMembers)fail('installed_data_limit_exceeded');
72
+ entry.members=names.sort();
73
+ for(const name of names)if(name.endsWith(d.suffix))await read(directory+'/'+name);
74
+ }
75
+ }
76
+ const result=JSON.stringify({entries,resolutions});
77
+ if(Buffer.byteLength(result)>input.limits.serializedBytes)fail('installed_data_limit_exceeded');
78
+ process.stdout.write(result);
79
+ }catch(e){
80
+ const allowed=['installed_data_descriptor_invalid','installed_data_resolution_unsafe','installed_data_resource_unsafe','installed_data_limit_exceeded'];
81
+ process.stderr.write(allowed.includes(e.code)?e.code:'installed_data_resource_unsafe');process.exitCode=1;
82
+ }
83
+ `;
@@ -88,7 +88,7 @@ export async function buildReviewContext(
88
88
  candidate: CapturedCandidate,
89
89
  receipt: PreparationReceipt,
90
90
  ) {
91
- const inputs = await capturePortableEvidenceContext(config, repositoryEvidenceReader(rootDir, createArtifactsPort()), receipt.preparationFingerprint);
91
+ const inputs = await capturePortableEvidenceContext(config, repositoryEvidenceReader(rootDir, createArtifactsPort()), receipt.preparationFingerprint, candidate.deliverable.digest);
92
92
  const charters = inputs.reviewerCharters;
93
93
  if (charters.length === 0 || inputs.release === null) throw new OutcomeError("the compiled policy activates no review lens or installed release");
94
94
  const binding = {
@@ -2,7 +2,7 @@
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { link, mkdir, readFile, readdir, unlink, writeFile } from "node:fs/promises";
4
4
  import path from "node:path";
5
- import { digestCanonical, type ScopedCheckAttempt } from "@agent-delivery-harness/kernel";
5
+ import { digestCanonical, validateResourceApplicabilityProjection, type ResourceApplicabilityProjection, type ScopedCheckAttempt, type ScopedCheckDefinition } from "@agent-delivery-harness/kernel";
6
6
  import { CheckSnapshotError } from "./check-snapshot.ts";
7
7
  import type { RecordedScopedAttemptDiagnostic } from "./scoped-diagnostics.ts";
8
8
  export interface AttemptPayload {
@@ -11,9 +11,29 @@ export interface AttemptPayload {
11
11
  readonly dependencyDigest?: string;
12
12
  readonly log?: string;
13
13
  readonly diagnostic?: RecordedScopedAttemptDiagnostic;
14
+ /** Native successful origin binding is checked before retention and on every load. */
15
+ readonly resourceApplicability?: ResourceApplicabilityProjection;
16
+ /** Original broad selection, authenticated by the native attempt digests. */
17
+ readonly resourceScope?: ScopedCheckDefinition;
14
18
  }
15
- export interface StoredAttempt { readonly attempt: ScopedCheckAttempt; readonly payload?: AttemptPayload }
19
+ export interface StoredAttempt { readonly attempt: ScopedCheckAttempt; readonly payload?: AttemptPayload; readonly executionHeadSha?: string }
16
20
  const corrupt = () => new CheckSnapshotError("check_attempt_corrupt", "Scoped check attempt history is missing, corrupt or inconsistent.");
21
+ const gitSha = (value: unknown): value is string => typeof value === "string" && /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(value);
22
+ /** Shape validation alone is not authority: bind to the native running allocation. */
23
+ function validateResourceOrigin(row: StoredAttempt): void {
24
+ if (row.executionHeadSha !== undefined && !gitSha(row.executionHeadSha)) throw corrupt();
25
+ const resource = row.payload?.resourceApplicability;
26
+ if (resource === undefined) return;
27
+ if (row.attempt.status !== "passed" || !row.executionHeadSha) throw corrupt();
28
+ try {
29
+ const projection = validateResourceApplicabilityProjection(resource);
30
+ const attempt = row.attempt;
31
+ const origin = { providerId: attempt.providerId, runId: attempt.origin.runId, attemptId: attempt.attemptId,
32
+ generation: attempt.generation, inputDigest: attempt.inputDigest, profileDigest: attempt.profileDigest,
33
+ source: { headSha: row.executionHeadSha, treeSha: attempt.origin.candidate.treeSha }, status: "passed", superseded: false };
34
+ if (digestCanonical(projection.origin) !== digestCanonical(origin)) throw corrupt();
35
+ } catch { throw corrupt(); }
36
+ }
17
37
  export class AttemptStore {
18
38
  readonly root: string;
19
39
  constructor(root: string) { this.root = root; }
@@ -24,7 +44,8 @@ export class AttemptStore {
24
44
  if (names.some(n => !/^[1-9][0-9]*$/.test(n) || !Number.isSafeInteger(Number(n)))) throw corrupt();
25
45
  return names.map(Number).sort((a, b) => a - b);
26
46
  }
27
- async allocate(input: Omit<ScopedCheckAttempt, "attemptId" | "generation" | "status">): Promise<ScopedCheckAttempt> {
47
+ async allocate(input: Omit<ScopedCheckAttempt, "attemptId" | "generation" | "status">, options: { readonly executionHeadSha?: string } = {}): Promise<ScopedCheckAttempt> {
48
+ if (options.executionHeadSha !== undefined && !gitSha(options.executionHeadSha)) throw corrupt();
28
49
  await mkdir(this.root, { recursive: true, mode: 0o700 });
29
50
  let generation = Math.max(0, ...await this.generations()) + 1;
30
51
  for (;;) {
@@ -32,7 +53,7 @@ export class AttemptStore {
32
53
  try { await mkdir(dir, { mode: 0o700 }); }
33
54
  catch (error) { if ((error as NodeJS.ErrnoException).code === "EEXIST") { generation++; continue; } throw error; }
34
55
  const attempt: ScopedCheckAttempt = { ...input, generation, attemptId: randomUUID(), status: "running" };
35
- await this.publish(path.join(dir, "running.json"), { attempt });
56
+ await this.publish(path.join(dir, "running.json"), { attempt, ...(options.executionHeadSha !== undefined ? { executionHeadSha: options.executionHeadSha } : {}) });
36
57
  return attempt;
37
58
  }
38
59
  }
@@ -47,10 +68,14 @@ export class AttemptStore {
47
68
  }
48
69
  async finish(attempt: ScopedCheckAttempt, status: "passed" | "failed" | "interrupted", payload: AttemptPayload): Promise<void> {
49
70
  const rows = await this.read();
50
- const running = rows.find(row => row.attempt.generation === attempt.generation)?.attempt;
51
- if (running?.status !== "running" || digestCanonical(running) !== digestCanonical(attempt)) throw corrupt();
52
- await this.publish(path.join(this.root, String(attempt.generation), "terminal.json"), { attempt: { ...attempt, status }, payload });
71
+ const running = rows.find(row => row.attempt.generation === attempt.generation);
72
+ if (running?.attempt.status !== "running" || digestCanonical(running.attempt) !== digestCanonical(attempt)) throw corrupt();
73
+ const terminal: StoredAttempt = { attempt: { ...attempt, status }, payload,
74
+ ...(running.executionHeadSha !== undefined ? { executionHeadSha: running.executionHeadSha } : {}) };
75
+ validateResourceOrigin(terminal);
76
+ await this.publish(this.terminalPath(attempt), terminal);
53
77
  }
78
+ terminalPath(attempt: ScopedCheckAttempt): string { return path.join(this.root, String(attempt.generation), "terminal.json"); }
54
79
  private async load(file: string): Promise<StoredAttempt> {
55
80
  const raw = JSON.parse(await readFile(file, "utf8")) as { digest?: string; entry?: StoredAttempt };
56
81
  const row = raw.entry;
@@ -65,6 +90,7 @@ export class AttemptStore {
65
90
  !candidate || ![candidate.treeSha, candidate.deliverableDigest, candidate.identityToken, candidate.baseRef,
66
91
  candidate.baseTipSha, candidate.mergeBaseSha, candidate.workspaceId].every(nonempty)) throw corrupt();
67
92
  if (row.payload?.durationMs !== undefined && (typeof row.payload.durationMs !== "number" || !Number.isFinite(row.payload.durationMs) || row.payload.durationMs < 0)) throw corrupt();
93
+ validateResourceOrigin(row);
68
94
  return row;
69
95
  }
70
96
  async read(): Promise<StoredAttempt[]> {
@@ -77,7 +103,7 @@ export class AttemptStore {
77
103
  let terminal: StoredAttempt | undefined;
78
104
  try { terminal = await this.load(path.join(dir, "terminal.json")); }
79
105
  catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; }
80
- if (terminal && (terminal.attempt.status === "running" || digestCanonical({ ...terminal.attempt, status: "running" }) !== digestCanonical(start.attempt))) throw corrupt();
106
+ if (terminal && (terminal.executionHeadSha !== start.executionHeadSha || terminal.attempt.status === "running" || digestCanonical({ ...terminal.attempt, status: "running" }) !== digestCanonical(start.attempt))) throw corrupt();
81
107
  rows.push(terminal ?? start);
82
108
  }
83
109
  return rows;
@@ -0,0 +1,20 @@
1
+ /** The declared pre-merge budget, enforced where a selection enters the runner, before any check starts. */
2
+ import { scopedBudgetOffenders, type HarnessConfig } from "@agent-delivery-harness/kernel";
3
+ import { CheckSnapshotError } from "./check-snapshot.ts";
4
+
5
+ /**
6
+ * Refuses a selection whose scoped checks declare more than `scopedExecution.budgetMs` in total, or any one
7
+ * of which declares more than `scopedExecution.checkBudgetMs`. The refusal names only the offending checks:
8
+ * those over the cap, and those that push the running total past the budget in declaration order.
9
+ */
10
+ export function refuseOverBudget(config: Pick<HarnessConfig, "providers" | "scopedExecution">): void {
11
+ const { overCap, overBudget, totalMs } = scopedBudgetOffenders(config);
12
+ if (overCap.length === 0 && overBudget.length === 0) return;
13
+ const { budgetMs, checkBudgetMs } = config.scopedExecution!;
14
+ const timeout = (id: string) => config.providers.find(p => p.id === id)!.check!.timeoutMs;
15
+ const reasons = [
16
+ ...overCap.map(id => `${id} declares timeoutMs ${timeout(id)} over checkBudgetMs ${checkBudgetMs}`),
17
+ ...(overBudget.length ? [`${overBudget.join(", ")} ${overBudget.length === 1 ? "pushes" : "push"} the declared total to ${totalMs}ms over budgetMs ${budgetMs}`] : []),
18
+ ];
19
+ throw new CheckSnapshotError("scoped_budget_exceeded", `The scoped check selection exceeds its declared budget, so no check ran: ${reasons.join("; ")}.`);
20
+ }