@agent-delivery-harness/cli 0.2.0 → 0.7.3

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.
Files changed (47) hide show
  1. package/LICENSE +104 -201
  2. package/NOTICE +1 -1
  3. package/package.json +5 -5
  4. package/src/boundary.ts +84 -63
  5. package/src/check-snapshot.ts +167 -0
  6. package/src/command-activity.ts +75 -0
  7. package/src/commands/admit.ts +106 -0
  8. package/src/commands/check.ts +13 -2
  9. package/src/commands/emit-review-evidence.ts +38 -0
  10. package/src/commands/emit.ts +80 -12
  11. package/src/commands/gate.ts +64 -13
  12. package/src/commands/maintain.ts +1 -0
  13. package/src/commands/managed.ts +105 -34
  14. package/src/commands/prepare.ts +148 -22
  15. package/src/commands/record.ts +107 -3
  16. package/src/commands/resume.ts +53 -0
  17. package/src/commands/review-context.ts +19 -0
  18. package/src/commands/runs.ts +461 -30
  19. package/src/commands/save-context.ts +51 -0
  20. package/src/commands/submit-evidence.ts +46 -7
  21. package/src/commands/verify.ts +156 -10
  22. package/src/declared-checks.ts +50 -0
  23. package/src/index.ts +17 -1
  24. package/src/main.ts +56 -65
  25. package/src/ordinary-context.ts +83 -0
  26. package/src/provider-rails.ts +23 -704
  27. package/src/record-retention.ts +325 -0
  28. package/src/review-evidence.ts +284 -0
  29. package/src/run-archive-commands.ts +164 -0
  30. package/src/run-archive.ts +127 -0
  31. package/src/run-artifact-commands.ts +163 -0
  32. package/src/run-attachments.ts +112 -0
  33. package/src/run-export.ts +202 -0
  34. package/src/run-live.ts +102 -0
  35. package/src/run-projection.ts +327 -12
  36. package/src/run-server.ts +403 -149
  37. package/src/run-surface.ts +196 -9
  38. package/src/run-view-command.ts +77 -0
  39. package/src/run-view-html.ts +138 -0
  40. package/src/run-view-record.ts +119 -0
  41. package/src/run-view.ts +343 -0
  42. package/src/scoped-attempts.ts +86 -0
  43. package/src/scoped-candidate.ts +28 -0
  44. package/src/scoped-checks.ts +196 -0
  45. package/src/scoped-diagnostics.ts +124 -0
  46. package/src/scoped-observations.ts +51 -0
  47. package/src/snapshot-inventory-worker.ts +40 -0
@@ -0,0 +1,124 @@
1
+ import path from "node:path";
2
+ import { digestCanonical, resolveRecordStorage, type ExecOutcome, type HarnessConfig, type ScopedCheckAttempt } from "@agent-delivery-harness/kernel";
3
+ import { AttemptStore } from "./scoped-attempts.ts";
4
+ import { CheckSnapshotError } from "./check-snapshot.ts";
5
+ import { projectScopedAttempt } from "./scoped-observations.ts";
6
+
7
+ const phases = ["snapshot-setup", "pre-command-verification", "command", "post-command-verification", "output-capture", "complete"] as const;
8
+ const failureCodes = ["check_snapshot_interrupted", "check_snapshot_timeout", "check_snapshot_unavailable", "check_snapshot_escape", "check_snapshot_drift", "check_snapshot_cleanup_failed", "check_dependency_source_overlap", "check_dependency_failed", "check_command_failed", "check_output_missing", "check_attempt_superseded", "check_artifact_unavailable", "check_evidence_rejected"] as const;
9
+ const executionCodes = ["ENOENT", "EACCES", "EPERM", "ABORT_ERR", "ERR_CHILD_PROCESS_STDIO_MAXBUFFER", "SIGKILL", "SIGTERM", "execution_failed"] as const;
10
+ export type ScopedDiagnosticPhase = typeof phases[number];
11
+ export type ScopedDiagnosticFailureCode = typeof failureCodes[number];
12
+ export type ScopedDiagnosticExecutionCode = typeof executionCodes[number];
13
+ export type ScopedDiagnosticCommand =
14
+ | { readonly exitCode: number | null; readonly outputTail: string; readonly truncated: boolean }
15
+ | { readonly unavailable: "not-started" | "not-completed" };
16
+ export interface ScopedDependencyDiagnostic {
17
+ readonly durationMs: number;
18
+ readonly executionErrorCode?: ScopedDiagnosticExecutionCode;
19
+ readonly command: ScopedDiagnosticCommand;
20
+ }
21
+ export interface RecordedScopedAttemptDiagnostic {
22
+ readonly availability: "available";
23
+ readonly phase: ScopedDiagnosticPhase;
24
+ readonly failure: { readonly code: ScopedDiagnosticFailureCode; readonly executionErrorCode?: ScopedDiagnosticExecutionCode }
25
+ | { readonly unavailable: "not-failed" | "unclassified" };
26
+ readonly command: ScopedDiagnosticCommand;
27
+ /** Present only when this attempt actually ran dependency setup; never borrowed from a reused snapshot. */
28
+ readonly dependency?: ScopedDependencyDiagnostic;
29
+ }
30
+ export type ScopedAttemptDiagnostic = RecordedScopedAttemptDiagnostic
31
+ | { readonly availability: "unavailable"; readonly reason: "legacy" | "running" };
32
+ export interface ScopedCheckDiagnostics {
33
+ readonly version: "scoped-check-diagnostics/1";
34
+ readonly providers: readonly {
35
+ readonly providerId: string;
36
+ readonly attempts: readonly (ScopedCheckAttempt & { readonly durationMs?: number; readonly diagnostic: ScopedAttemptDiagnostic })[];
37
+ }[];
38
+ readonly unavailableAttemptIds: readonly string[];
39
+ }
40
+
41
+ export function scopedDiagnosticFailure(error: unknown, executionErrorCode?: string): RecordedScopedAttemptDiagnostic["failure"] {
42
+ if (!(error instanceof CheckSnapshotError) || !(failureCodes as readonly string[]).includes(error.code)) return { unavailable: "unclassified" };
43
+ return { code: error.code as ScopedDiagnosticFailureCode,
44
+ ...((executionCodes as readonly string[]).includes(executionErrorCode ?? "") ? { executionErrorCode: executionErrorCode as ScopedDiagnosticExecutionCode } : {}) };
45
+ }
46
+ export function redactScopedOutput(text: string, secrets: readonly string[]): string {
47
+ return [...secrets].filter(Boolean).sort((a, b) => b.length - a.length).reduce((value, secret) => value.split(secret).join("[REDACTED]"), text);
48
+ }
49
+ export function scopedCommandDiagnostic(result: ExecOutcome, secrets: readonly string[]): ScopedDiagnosticCommand {
50
+ // execFile can clip a fully emitted credential before full-value redaction.
51
+ // The typed execution failure remains available; this partial capture is not safe to export.
52
+ if (result.errorCode === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER") return { unavailable: "not-completed" };
53
+ const numericExit = result.errorCode === undefined || result.errorCode === String(result.code);
54
+ if (!numericExit && result.stdout.length + result.stderr.length === 0) return { unavailable: ["ENOENT", "EACCES", "EPERM"].includes(result.errorCode!) ? "not-started" : "not-completed" };
55
+ const redacted = redactScopedOutput(`${result.stdout}\n${result.stderr}`, secrets);
56
+ return { exitCode: numericExit ? result.code : null, outputTail: redacted.slice(-4000), truncated: redacted.length > 4000 };
57
+ }
58
+
59
+ export function scopedDependencyDiagnostic(result: ExecOutcome, durationMs: number, secrets: readonly string[]): ScopedDependencyDiagnostic {
60
+ return { durationMs, command: scopedCommandDiagnostic(result, secrets),
61
+ ...((executionCodes as readonly string[]).includes(result.errorCode ?? "") ? { executionErrorCode: result.errorCode as ScopedDiagnosticExecutionCode } : {}) };
62
+ }
63
+
64
+ function projectDiagnostic(value: unknown): RecordedScopedAttemptDiagnostic {
65
+ const corrupt = (): never => { throw new CheckSnapshotError("check_attempt_corrupt", "Selected scoped diagnostics are malformed."); };
66
+ const object = (v: unknown): Record<string, unknown> => v !== null && typeof v === "object" && !Array.isArray(v) ? v as Record<string, unknown> : corrupt();
67
+ const row = object(value), failure = object(row["failure"]);
68
+ if (row["availability"] !== "available" || !(phases as readonly unknown[]).includes(row["phase"])) return corrupt();
69
+ let safeFailure: RecordedScopedAttemptDiagnostic["failure"];
70
+ if ("unavailable" in failure) {
71
+ if (typeof failure["unavailable"] !== "string" || !["not-failed", "unclassified"].includes(failure["unavailable"]) || "code" in failure || "executionErrorCode" in failure) return corrupt();
72
+ safeFailure = { unavailable: failure["unavailable"] as "not-failed" | "unclassified" };
73
+ } else {
74
+ if (!(failureCodes as readonly unknown[]).includes(failure["code"]) || ("executionErrorCode" in failure && !(executionCodes as readonly unknown[]).includes(failure["executionErrorCode"]))) return corrupt();
75
+ safeFailure = { code: failure["code"] as ScopedDiagnosticFailureCode, ...("executionErrorCode" in failure ? { executionErrorCode: failure["executionErrorCode"] as ScopedDiagnosticExecutionCode } : {}) };
76
+ }
77
+ const commandDiagnostic = (value: unknown): ScopedDiagnosticCommand => {
78
+ const command = object(value);
79
+ if ("unavailable" in command) {
80
+ if (typeof command["unavailable"] !== "string" || !["not-started", "not-completed"].includes(command["unavailable"]) || "exitCode" in command || "outputTail" in command || "truncated" in command) return corrupt();
81
+ return { unavailable: command["unavailable"] as "not-started" | "not-completed" };
82
+ }
83
+ if (!(command["exitCode"] === null || typeof command["exitCode"] === "number" && Number.isSafeInteger(command["exitCode"]) && command["exitCode"] >= 0) || typeof command["outputTail"] !== "string" || command["outputTail"].length > 4000 || typeof command["truncated"] !== "boolean") return corrupt();
84
+ return { exitCode: command["exitCode"] as number | null, outputTail: command["outputTail"], truncated: command["truncated"] };
85
+ };
86
+ let dependency: ScopedDependencyDiagnostic | undefined;
87
+ if ("dependency" in row) {
88
+ const value = object(row["dependency"]);
89
+ if (typeof value["durationMs"] !== "number" || !Number.isFinite(value["durationMs"]) || value["durationMs"] < 0 ||
90
+ ("executionErrorCode" in value && !(executionCodes as readonly unknown[]).includes(value["executionErrorCode"]))) return corrupt();
91
+ dependency = { durationMs: value["durationMs"], command: commandDiagnostic(value["command"]),
92
+ ...("executionErrorCode" in value ? { executionErrorCode: value["executionErrorCode"] as ScopedDiagnosticExecutionCode } : {}) };
93
+ }
94
+ return { availability: "available", phase: row["phase"] as ScopedDiagnosticPhase, failure: safeFailure, command: commandDiagnostic(row["command"]),
95
+ ...(dependency === undefined ? {} : { dependency }) };
96
+ }
97
+
98
+ /** Read at most 100 explicit attempts. No logs from legacy payloads, no writes,
99
+ * and no claim of applicability/admission. Unselected provider history is ignored. */
100
+ export async function readScopedCheckDiagnostics(input: {
101
+ readonly rootDir: string;
102
+ readonly config: Pick<HarnessConfig, "gateId" | "storageNamespace" | "providers">;
103
+ readonly attemptIds: readonly string[];
104
+ }): Promise<ScopedCheckDiagnostics> {
105
+ const { rootDir, config, attemptIds } = input;
106
+ if (!Array.isArray(attemptIds) || attemptIds.length > 100 || attemptIds.some(id => typeof id !== "string" || !id.length || id.length > 128) || new Set(attemptIds).size !== attemptIds.length) throw new CheckSnapshotError("check_diagnostics_request_invalid", "Diagnostics require at most 100 unique nonempty attempt identifiers.");
107
+ const requested = new Set(attemptIds), seen = new Set<string>();
108
+ const providers: ScopedCheckDiagnostics["providers"][number][] = [];
109
+ const storage = await resolveRecordStorage(rootDir, { storageNamespace: config.storageNamespace, leaf: "scoped-attempts" });
110
+ for (const provider of config.providers.filter(p => p.check?.scope !== undefined)) {
111
+ const rows = requested.size ? await new AttemptStore(path.join(storage.storageDir, digestCanonical({ gate: config.gateId, provider: provider.id }))).read() : [];
112
+ if (rows.some(row => row.attempt.providerId !== provider.id)) throw new CheckSnapshotError("check_attempt_corrupt", "Scoped history belongs to a different provider.");
113
+ const attempts: ScopedCheckDiagnostics["providers"][number]["attempts"][number][] = [];
114
+ for (const { attempt, payload } of rows.filter(row => requested.has(row.attempt.attemptId))) {
115
+ if (seen.has(attempt.attemptId)) throw new CheckSnapshotError("check_attempt_corrupt", "A requested attempt identifier is ambiguous.");
116
+ seen.add(attempt.attemptId);
117
+ const diagnostic: ScopedAttemptDiagnostic = attempt.status === "running" ? { availability: "unavailable", reason: "running" }
118
+ : payload?.diagnostic === undefined ? { availability: "unavailable", reason: "legacy" } : projectDiagnostic(payload.diagnostic);
119
+ attempts.push({ ...projectScopedAttempt(attempt, payload?.durationMs), diagnostic });
120
+ }
121
+ providers.push({ providerId: provider.id, attempts });
122
+ }
123
+ return { version: "scoped-check-diagnostics/1", providers, unavailableAttemptIds: attemptIds.filter(id => !seen.has(id)) };
124
+ }
@@ -0,0 +1,51 @@
1
+ import path from "node:path";
2
+ import { digestCanonical, resolveRecordStorage, type HarnessConfig, type ScopedCheckAttempt } from "@agent-delivery-harness/kernel";
3
+ import { AttemptStore } from "./scoped-attempts.ts";
4
+ import { CheckSnapshotError } from "./check-snapshot.ts";
5
+
6
+ export interface ScopedCheckObservations {
7
+ readonly version: "scoped-check-observations/1";
8
+ readonly providers: readonly {
9
+ readonly providerId: string;
10
+ readonly attempts: readonly (ScopedCheckAttempt & { readonly durationMs?: number })[];
11
+ }[];
12
+ }
13
+
14
+ export function projectScopedAttempt(attempt: ScopedCheckAttempt, durationMs?: number): ScopedCheckAttempt & { readonly durationMs?: number } {
15
+ return {
16
+ version: attempt.version, providerId: attempt.providerId, attemptId: attempt.attemptId,
17
+ generation: attempt.generation, inputDigest: attempt.inputDigest, profileDigest: attempt.profileDigest,
18
+ status: attempt.status, origin: { runId: attempt.origin.runId, candidate: {
19
+ treeSha: attempt.origin.candidate.treeSha, deliverableDigest: attempt.origin.candidate.deliverableDigest,
20
+ identityToken: attempt.origin.candidate.identityToken, baseRef: attempt.origin.candidate.baseRef,
21
+ baseTipSha: attempt.origin.candidate.baseTipSha, mergeBaseSha: attempt.origin.candidate.mergeBaseSha,
22
+ workspaceId: attempt.origin.candidate.workspaceId,
23
+ } }, ...(durationMs === undefined ? {} : { durationMs }),
24
+ };
25
+ }
26
+
27
+ /**
28
+ * Read retained attempts for the supplied scoped providers, in configuration
29
+ * order and ascending generation order. This creates no directories or evidence.
30
+ * Providers removed from this supplied configuration are not inventoried.
31
+ * Absent history is empty; corrupt selected history throws. Durations are reported only
32
+ * when recorded by the executor. These observations make no statement about
33
+ * reuse, current applicability or admission; use the gate/verify for those.
34
+ */
35
+ export async function readScopedCheckObservations(input: {
36
+ readonly rootDir: string;
37
+ readonly config: Pick<HarnessConfig, "gateId" | "storageNamespace" | "providers">;
38
+ }): Promise<ScopedCheckObservations> {
39
+ const { rootDir, config } = input;
40
+ const storage = await resolveRecordStorage(rootDir, { storageNamespace: config.storageNamespace, leaf: "scoped-attempts" });
41
+ const providers: ScopedCheckObservations["providers"][number][] = [];
42
+ for (const provider of config.providers.filter(provider => provider.check?.scope !== undefined)) {
43
+ const store = new AttemptStore(path.join(storage.storageDir, digestCanonical({ gate: config.gateId, provider: provider.id })));
44
+ const retained = await store.read();
45
+ if (retained.some(row => row.attempt.providerId !== provider.id)) {
46
+ throw new CheckSnapshotError("check_attempt_corrupt", "Scoped check attempt history belongs to a different provider.");
47
+ }
48
+ providers.push({ providerId: provider.id, attempts: retained.map(({ attempt, payload }) => projectScopedAttempt(attempt, payload?.durationMs)) });
49
+ }
50
+ return { version: "scoped-check-observations/1", providers };
51
+ }
@@ -0,0 +1,40 @@
1
+ /** Inline so the distributed single-file runtime owns the worker bytes too.
2
+ * Only Node builtins are available; no author code or credentials are loaded. */
3
+ export const snapshotInventoryWorker = String.raw`
4
+ import { lstat, readFile, readdir, readlink, realpath } from 'node:fs/promises';
5
+ import { createHash } from 'node:crypto';
6
+ import path from 'node:path';
7
+ const { root, outputs, excludeDependencies, forbidGit } = JSON.parse(process.env.DELIVERY_SNAPSHOT_REQUEST);
8
+ const entries = [];
9
+ const inside = target => {
10
+ const relative = path.relative(root, target);
11
+ return relative !== '..' && !relative.startsWith('..' + path.sep) && !path.isAbsolute(relative);
12
+ };
13
+ const escape = () => { throw { code: 'check_snapshot_escape' }; };
14
+ const walk = async relative => {
15
+ if (relative === '.git' || (excludeDependencies && relative.split('/').includes('node_modules')) ||
16
+ outputs.some(o => o.endsWith('/') ? relative === o.slice(0, -1) || relative.startsWith(o) : relative === o)) return;
17
+ const absolute = path.join(root, relative), stat = await lstat(absolute);
18
+ if (stat.isSymbolicLink()) {
19
+ const target = await readlink(absolute);
20
+ if (path.isAbsolute(target) || !inside(path.resolve(path.dirname(absolute), target))) escape();
21
+ try { if (!inside(await realpath(absolute))) escape(); }
22
+ catch (error) { if (error.code !== 'ENOENT') throw error; }
23
+ entries.push([relative, 'link', target]);
24
+ } else if (stat.isDirectory()) {
25
+ for (const name of (await readdir(absolute)).sort()) await walk(relative ? relative + '/' + name : name);
26
+ } else if (stat.isFile()) {
27
+ entries.push([relative, stat.mode & 0o111, createHash('sha256').update(await readFile(absolute)).digest('hex')]);
28
+ } else escape();
29
+ };
30
+ try {
31
+ if (forbidGit && await lstat(path.join(root, '.git')).then(() => true, error => { if (error.code === 'ENOENT') return false; throw error; })) throw { code: 'check_snapshot_drift' };
32
+ await walk('');
33
+ // Tuples contain only arrays, strings and permission integers: JSON.stringify
34
+ // has exactly the same bytes as canonical JSON for this restricted shape.
35
+ process.stdout.write(JSON.stringify({ digest: createHash('sha256').update(JSON.stringify(entries)).digest('hex') }));
36
+ } catch (error) {
37
+ process.stdout.write(JSON.stringify({ code: ['check_snapshot_escape', 'check_snapshot_drift'].includes(error.code) ? error.code : 'check_snapshot_unavailable' }));
38
+ process.exitCode = 1;
39
+ }
40
+ `;