@agent-delivery-harness/cli 0.1.0 → 0.5.0

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 (40) hide show
  1. package/LICENSE +104 -201
  2. package/NOTICE +1 -1
  3. package/package.json +10 -5
  4. package/src/boundary.ts +302 -9
  5. package/src/command-activity.ts +75 -0
  6. package/src/commands/admit.ts +106 -0
  7. package/src/commands/check.ts +13 -2
  8. package/src/commands/emit-review-evidence.ts +38 -0
  9. package/src/commands/emit.ts +419 -0
  10. package/src/commands/gate.ts +127 -18
  11. package/src/commands/maintain.ts +203 -0
  12. package/src/commands/managed.ts +478 -0
  13. package/src/commands/prepare.ts +123 -15
  14. package/src/commands/record.ts +100 -8
  15. package/src/commands/resume.ts +53 -0
  16. package/src/commands/review-context.ts +19 -0
  17. package/src/commands/runs.ts +640 -0
  18. package/src/commands/save-context.ts +51 -0
  19. package/src/commands/submit-evidence.ts +46 -7
  20. package/src/commands/verify.ts +173 -5
  21. package/src/declared-checks.ts +50 -0
  22. package/src/index.ts +40 -4
  23. package/src/main.ts +38 -42
  24. package/src/ordinary-context.ts +99 -0
  25. package/src/provider-rails.ts +24 -0
  26. package/src/record-retention.ts +325 -0
  27. package/src/review-evidence.ts +284 -0
  28. package/src/run-archive-commands.ts +164 -0
  29. package/src/run-archive.ts +127 -0
  30. package/src/run-artifact-commands.ts +163 -0
  31. package/src/run-attachments.ts +112 -0
  32. package/src/run-export.ts +164 -0
  33. package/src/run-live.ts +102 -0
  34. package/src/run-projection.ts +396 -0
  35. package/src/run-server.ts +905 -0
  36. package/src/run-surface.ts +334 -0
  37. package/src/run-view-command.ts +77 -0
  38. package/src/run-view-html.ts +138 -0
  39. package/src/run-view-record.ts +119 -0
  40. package/src/run-view.ts +343 -0
@@ -0,0 +1,284 @@
1
+ /**
2
+ * The shipped review-outcome evidence writer. It transcribes concluded host
3
+ * results without running reviewers or upgrading their self-attestation.
4
+ *
5
+ * Capture `delivery-harness review-context --json` before review, then pass
6
+ * that original document to `emit-review-evidence --context <path>` with a
7
+ * review-outcome/1 document on stdin naming its contextDigest. Reviewer names
8
+ * come from the resolved activated charters; for example a named result is
9
+ * `{ "id": "outcome-correctness", "result": "rejected" }`.
10
+ *
11
+ * The raw context and outcome remain digest-bound artifacts. Submission is
12
+ * still the authority on whether the resulting evidence satisfies the gate.
13
+ */
14
+ import { randomUUID } from "node:crypto";
15
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
16
+ import path from "node:path";
17
+
18
+ import {
19
+ resolveReviewCharters,
20
+ validateReviewedContext, parseReviewOutcome, deriveTelemetry, reviewerLists,
21
+ capturePortableEvidenceContext, repositoryEvidenceReader, createArtifactsPort,
22
+ ReviewInputError as OutcomeError,
23
+ BlockedError,
24
+ digestCanonical,
25
+ evaluatePreparationReceipt,
26
+ sha256Hex,
27
+ type CapturedCandidate,
28
+ type PreparationReceipt,
29
+ type HarnessConfig,
30
+ REVIEW_GREEN_1,
31
+ REVIEW_GREEN_2,
32
+ } from "@agent-delivery-harness/kernel";
33
+ import type { CommandContext } from "./boundary.ts";
34
+
35
+ // ── The charters the compiled policy activates ───────────────────────────────
36
+
37
+ /**
38
+ * The compiled policy this repository is judged under, and the installed
39
+ * generation whose archive carries the charters its lenses reference by
40
+ * identity. One activated lens is one reviewer, and the basename of the
41
+ * charter path the archive's manifest declares is the reviewer id the evidence
42
+ * carries — the same two documents `policy-projection-check.ts` compiles the
43
+ * projection from.
44
+ */
45
+ export const COMPILED_SNAPSHOT_FILE = ".agents/policy/compiled-snapshot.json";
46
+ export const INSTALLED_ARCHIVE_DIR = ".agent-skills/current";
47
+
48
+ export const CHARTER_EXTENSION = ".md";
49
+
50
+ /** Review payloads this provider can emit, in preference order. */
51
+ export const REVIEW_PAYLOAD_SPECS = [REVIEW_GREEN_2, REVIEW_GREEN_1] as const;
52
+ export const REVIEW_PAYLOAD_SPEC = REVIEW_GREEN_2;
53
+
54
+ /** The envelope spec the manifest declares. */
55
+ export const ENVELOPE_SPEC = "delivery-evidence/1";
56
+
57
+ /** The outcome document this emitter reads. */
58
+ export const OUTCOME_SPEC = "review-outcome/1";
59
+
60
+ /** This emitter's own version, carried in the manifest's provider triple. */
61
+ export const EMITTER_VERSION = "1.0.0";
62
+
63
+ export type { ResolvedCharter } from "@agent-delivery-harness/kernel";
64
+ export async function resolveActivatedCharters(rootDir: string, config?: Pick<HarnessConfig, "additionalReviewLenses">) {
65
+ return resolveReviewCharters(repositoryEvidenceReader(rootDir, createArtifactsPort()), config);
66
+ }
67
+ export async function resolveReviewerCharters(rootDir: string): Promise<string[]> {
68
+ return (await resolveActivatedCharters(rootDir)).map((charter) => charter.reviewerId).sort();
69
+ }
70
+
71
+ /** Exact review input, retained by the host before acquiring any outcomes. */
72
+ export const REVIEW_CONTEXT_SPEC = "review-context/1";
73
+
74
+ function manifestCandidate(captured: CapturedCandidate) {
75
+ return {
76
+ vcs: captured.vcs,
77
+ treeSha: captured.treeSha,
78
+ headSha: captured.headSha,
79
+ deliverable: { digest: captured.deliverable.digest, identity: captured.deliverable.identity },
80
+ base: { ref: captured.base.ref, tipSha: captured.base.tipSha, mergeBaseSha: captured.base.mergeBaseSha },
81
+ workspaceId: captured.workspaceId,
82
+ };
83
+ }
84
+
85
+ export async function buildReviewContext(
86
+ rootDir: string,
87
+ config: HarnessConfig,
88
+ candidate: CapturedCandidate,
89
+ receipt: PreparationReceipt,
90
+ ) {
91
+ const inputs = await capturePortableEvidenceContext(config, repositoryEvidenceReader(rootDir, createArtifactsPort()), receipt.preparationFingerprint);
92
+ const charters = inputs.reviewerCharters;
93
+ if (charters.length === 0 || inputs.release === null) throw new OutcomeError("the compiled policy activates no review lens or installed release");
94
+ const binding = {
95
+ gate: resolveGateBinding(config),
96
+ candidate: manifestCandidate(candidate),
97
+ preparationFingerprint: inputs.preparationFingerprint,
98
+ configurationDigest: inputs.configurationDigest,
99
+ policyDigest: inputs.policyDigest,
100
+ release: inputs.release,
101
+ workflowGraphSha256: inputs.workflowGraphSha256,
102
+ charters,
103
+ };
104
+ return { spec: REVIEW_CONTEXT_SPEC, digest: digestCanonical(binding), binding };
105
+ }
106
+
107
+ export type ReviewContextDocument = Awaited<ReturnType<typeof buildReviewContext>>;
108
+
109
+ export { validateReviewedContext, parseReviewOutcome, deriveTelemetry, reviewerLists, REVIEWER_RESULTS } from "@agent-delivery-harness/kernel";
110
+ export type { ReviewOutcome, ReviewerOutcome, ReviewerResult } from "@agent-delivery-harness/kernel";
111
+ export { OutcomeError };
112
+ // ── The gate this provider serves ────────────────────────────────────────────
113
+
114
+ export interface GateBinding {
115
+ readonly obligationId: string;
116
+ readonly providerId: string;
117
+ }
118
+
119
+ /**
120
+ * The obligation this manifest answers, and the provider it answers as, taken
121
+ * from the config rather than from a constant — a provider id that has drifted
122
+ * from the gate it serves is rejected as `unknown_provider` at evaluation, long
123
+ * after the review it describes has been paid for.
124
+ */
125
+ export function resolveGateBinding(config: HarnessConfig): GateBinding {
126
+ const obligations = config.obligations.filter((obligation) =>
127
+ REVIEW_PAYLOAD_SPECS.some((payloadSpec) => obligation.acceptedPayloadSpecs.includes(payloadSpec)),
128
+ );
129
+ if (obligations.length !== 1) {
130
+ throw new OutcomeError(
131
+ `the gate declares ${obligations.length} obligations accepting a supported review.green payload; this provider serves exactly one`,
132
+ );
133
+ }
134
+ const obligation = obligations[0]!;
135
+ if (obligation.providers.length !== 1) {
136
+ throw new OutcomeError(
137
+ `obligation ${obligation.id} names ${obligation.providers.length} providers; this provider serves exactly one`,
138
+ );
139
+ }
140
+ return { obligationId: obligation.id, providerId: obligation.providers[0]! };
141
+ }
142
+
143
+ function resolveReviewPayloadSpec(
144
+ config: HarnessConfig,
145
+ obligationId: string,
146
+ ): (typeof REVIEW_PAYLOAD_SPECS)[number] {
147
+ const obligation = config.obligations.find((entry) => entry.id === obligationId);
148
+ const payloadSpec = REVIEW_PAYLOAD_SPECS.find((candidate) => obligation?.acceptedPayloadSpecs.includes(candidate));
149
+ if (payloadSpec === undefined) throw new OutcomeError(`obligation ${obligationId} accepts no supported review.green payload`);
150
+ return payloadSpec;
151
+ }
152
+
153
+ // ── The manifest ─────────────────────────────────────────────────────────────
154
+
155
+ export interface EmitResult {
156
+ readonly manifestPath: string;
157
+ readonly runRoot: string;
158
+ }
159
+
160
+ /**
161
+ * Capture the candidate, allocate the run root, stamp one approval per
162
+ * approving reviewer, and write the manifest. The capture is deliberately the
163
+ * same call the recorder makes at submission (same config, same workspace id,
164
+ * same identity computation): anything else describes a tree the recorder will
165
+ * refuse to recognise.
166
+ */
167
+ export async function emitReviewEvidence(context: CommandContext, original: unknown, document: unknown): Promise<EmitResult> {
168
+ const { rootDir, config } = context;
169
+ const wiring = await context.wire();
170
+ const capture = await wiring.captureCandidate();
171
+ if (!capture.ok) throw new OutcomeError(`the candidate could not be captured: ${capture.code}`);
172
+ const captured = capture.candidate;
173
+ const preparation = await evaluatePreparationReceipt(rootDir, { config, candidate: captured }, wiring.storageOptions);
174
+ if (!preparation.prepared) throw new BlockedError([...preparation.blockers]);
175
+ const current = await buildReviewContext(rootDir, config, captured, preparation.receipt);
176
+ validateReviewedContext(original, current, document);
177
+ const charters = current.binding.charters.map((charter) => charter.reviewerId).sort();
178
+ const outcome = parseReviewOutcome(document, charters);
179
+ const binding = current.binding.gate;
180
+ const payloadSpec = resolveReviewPayloadSpec(config, binding.obligationId);
181
+ const candidate = manifestCandidate(captured);
182
+
183
+ const provider = {
184
+ id: binding.providerId,
185
+ version: EMITTER_VERSION,
186
+ // One emitter run is one evaluated pass over this candidate.
187
+ runId: `r-${randomUUID()}`,
188
+ finalPassId: outcome.finalPassId ?? "pass-1",
189
+ };
190
+ const reviewed = original as ReviewContextDocument;
191
+ const originalRunHistory = outcome.runHistory ?? [{
192
+ preparedTreeSha: reviewed.binding.candidate.treeSha,
193
+ evaluatedInPassId: provider.finalPassId,
194
+ }];
195
+ const finalEntry = originalRunHistory.at(-1)!;
196
+ if (finalEntry["preparedTreeSha"] !== reviewed.binding.candidate.treeSha ||
197
+ finalEntry["evaluatedInPassId"] !== provider.finalPassId) {
198
+ throw new OutcomeError("the supplied final review pass does not name the original reviewed candidate");
199
+ }
200
+ // ENV-9's final tree is a preparation coordinate. A proven neutral reuse
201
+ // projects that coordinate without claiming the reviewers saw a new raw
202
+ // tree, adding a round, or overwriting their original history.
203
+ const runHistory = originalRunHistory.map((entry, index) => index === originalRunHistory.length - 1
204
+ ? { ...entry, preparedTreeSha: captured.treeSha } : entry);
205
+
206
+ const allocation = await context.artifacts.allocateRunRoot({ providerId: provider.id, runId: provider.runId });
207
+ if (!allocation.ok) throw new OutcomeError(`the run root was refused: ${allocation.reason}`);
208
+ const runRoot = allocation.runRoot.path;
209
+
210
+ const lists = reviewerLists(charters, outcome);
211
+ await mkdir(path.join(runRoot, "reviewers"), { recursive: true });
212
+ const artifacts: { path: string; sha256: string; role: string }[] = [];
213
+ for (const [name, value] of [["review-context", original], ["review-outcome", document]] as const) {
214
+ const bytes = `${JSON.stringify(value, null, 2)}\n`;
215
+ await writeFile(path.join(runRoot, `${name}.json`), bytes, "utf8");
216
+ artifacts.push({ path: `${name}.json`, sha256: sha256Hex(bytes), role: name });
217
+ }
218
+ if (digestCanonical(reviewed.binding.candidate) !== digestCanonical(candidate)) {
219
+ const bytes = `${JSON.stringify({
220
+ spec: "review-context-projection/1",
221
+ basis: "unchanged-deliverable-and-review-inputs",
222
+ originalContextDigest: reviewed.digest,
223
+ reviewedCandidate: reviewed.binding.candidate,
224
+ preparedCandidate: candidate,
225
+ originalRunHistory,
226
+ reviewRoundAdded: false,
227
+ }, null, 2)}\n`;
228
+ await writeFile(path.join(runRoot, "review-context-projection.json"), bytes, "utf8");
229
+ artifacts.push({ path: "review-context-projection.json", sha256: sha256Hex(bytes), role: "review-context-projection" });
230
+ }
231
+ for (const reviewerId of lists.approved) {
232
+ // §9.2: the stamp re-states the whole binding, so each approval is
233
+ // independently interpretable in an audit.
234
+ const stamp = `${JSON.stringify(
235
+ {
236
+ schemaVersion: 1,
237
+ reviewerId,
238
+ result: "approved",
239
+ provider: { id: provider.id, runId: provider.runId, finalPassId: provider.finalPassId },
240
+ workspaceId: candidate.workspaceId,
241
+ candidate,
242
+ },
243
+ null,
244
+ 2,
245
+ )}\n`;
246
+ const relativePath = `reviewers/${reviewerId}.json`;
247
+ await writeFile(path.join(runRoot, relativePath), stamp, "utf8");
248
+ artifacts.push({ path: relativePath, sha256: sha256Hex(stamp), role: "reviewer-approval" });
249
+ }
250
+
251
+ const manifest = {
252
+ spec: ENVELOPE_SPEC,
253
+ provider,
254
+ candidate,
255
+ repository: null,
256
+ runHistory,
257
+ artifacts,
258
+ attestation: { level: "self", signatures: [] },
259
+ recordedAt: new Date().toISOString(),
260
+ claims: [
261
+ {
262
+ obligation: binding.obligationId,
263
+ payloadSpec,
264
+ payload: {
265
+ verdict: outcome.verdict,
266
+ finalized: true,
267
+ editedAfterFinalPass: false,
268
+ reviewers: {
269
+ selected: lists.selected,
270
+ completed: lists.completed,
271
+ failed: lists.failed,
272
+ timedOut: lists.timedOut,
273
+ },
274
+ findings: outcome.findings,
275
+ telemetry: { ...deriveTelemetry(outcome.findings, runHistory.length), ...(outcome.cost === undefined ? {} : { cost: outcome.cost }) },
276
+ },
277
+ },
278
+ ],
279
+ };
280
+
281
+ const manifestPath = path.join(runRoot, "manifest.json");
282
+ await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
283
+ return { manifestPath, runRoot };
284
+ }
@@ -0,0 +1,164 @@
1
+ import { constants } from "node:fs";
2
+ import { open, realpath } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import {
5
+ createArtifactsPort,
6
+ readRunArtifact,
7
+ MAX_PORTABLE_RECORD_BYTES,
8
+ } from "@agent-delivery-harness/kernel";
9
+ import { buildRunArchive, readArchiveArtifact } from "./run-archive.ts";
10
+ import { parseRunExport } from "./run-export.ts";
11
+ import {
12
+ resolveRunSurface,
13
+ oneLine,
14
+ runSurfaceBlocker,
15
+ } from "./run-surface.ts";
16
+ import type { ConfigFreeCommandContext, CommandResult } from "./boundary.ts";
17
+ const blocked = (reason: string): CommandResult => ({
18
+ kind: "blocked",
19
+ blockers: [
20
+ runSurfaceBlocker({
21
+ code: "run_archive_unavailable",
22
+ summary: "The run archive is unavailable.",
23
+ details: oneLine(reason),
24
+ remediation: {
25
+ id: "inspect-run-archive",
26
+ summary:
27
+ "Check the selected archive or retained run attachments and retry.",
28
+ },
29
+ }),
30
+ ],
31
+ });
32
+ /** Explicit file input only, bounded on the opened descriptor; never a server path. */
33
+ export async function readArchiveFile(file: string): Promise<string> {
34
+ // Inspect the descriptor before reading; a FIFO must not wait for a writer.
35
+ const h = await open(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
36
+ try {
37
+ const stat = await h.stat();
38
+ if (!stat.isFile() || stat.size > MAX_PORTABLE_RECORD_BYTES)
39
+ throw Error("archive must be a regular file no larger than 16 MiB");
40
+ const buffer = Buffer.alloc(
41
+ Math.min(stat.size + 1, MAX_PORTABLE_RECORD_BYTES + 1),
42
+ );
43
+ let offset = 0;
44
+ while (offset < buffer.length) {
45
+ const read = await h.read(buffer, offset, buffer.length - offset, null);
46
+ if (!read.bytesRead) break;
47
+ offset += read.bytesRead;
48
+ }
49
+ if (offset !== stat.size || offset > MAX_PORTABLE_RECORD_BYTES)
50
+ throw Error("archive changed size while reading");
51
+ return buffer.subarray(0, offset).toString("utf8");
52
+ } finally {
53
+ await h.close();
54
+ }
55
+ }
56
+ async function resolvedDestination(file: string): Promise<string> {
57
+ let parent = path.dirname(file);
58
+ const suffix = [path.basename(file)];
59
+ for (;;) {
60
+ try {
61
+ return path.join(await realpath(parent), ...suffix);
62
+ } catch (error) {
63
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
64
+ suffix.unshift(path.basename(parent));
65
+ const next = path.dirname(parent);
66
+ if (next === parent) throw error;
67
+ parent = next;
68
+ }
69
+ }
70
+ }
71
+ const within = (parent: string, child: string) => {
72
+ const relative = path.relative(parent, child);
73
+ return (
74
+ relative === "" ||
75
+ (!relative.startsWith("..") && !path.isAbsolute(relative))
76
+ );
77
+ };
78
+ export async function runArchiveCommand(
79
+ context: ConfigFreeCommandContext,
80
+ command: "export" | "archive",
81
+ args: readonly string[],
82
+ ): Promise<CommandResult> {
83
+ if (command === "export" && (args.length !== 3 || args[1] !== "--output"))
84
+ return {
85
+ kind: "usage",
86
+ message: "Usage: runs export <run-id> --output <file>",
87
+ };
88
+ if (
89
+ command === "archive" &&
90
+ !(args.length === 1 || (args.length === 3 && args[1] === "--artifact"))
91
+ )
92
+ return {
93
+ kind: "usage",
94
+ message: "Usage: runs archive <file> [--artifact <id>]",
95
+ };
96
+ try {
97
+ if (command === "archive") {
98
+ const text = await readArchiveFile(
99
+ path.resolve(context.rootDir, args[0]!),
100
+ );
101
+ const parsed = parseRunExport(text);
102
+ if (!parsed.ok) return blocked("archive invalid or unsupported");
103
+ if (args[1] === "--artifact") {
104
+ const artifact = readArchiveArtifact(text, args[2]!);
105
+ if (!artifact.ok) return blocked(artifact.reason);
106
+ context.write(
107
+ JSON.stringify({
108
+ spec: "run-artifact/1",
109
+ historical: true,
110
+ authority: "observation",
111
+ runId: parsed.value.runId,
112
+ ...artifact,
113
+ }),
114
+ );
115
+ } else
116
+ context.write(
117
+ JSON.stringify({
118
+ historical: true,
119
+ authority: "observation",
120
+ archive: parsed.value,
121
+ }),
122
+ );
123
+ return { kind: "ok" };
124
+ }
125
+ const resolved = await resolveRunSurface(context.rootDir);
126
+ if (!resolved.ok) return blocked(resolved.reason);
127
+ const store = resolved.surface.store;
128
+ const runId = args[0]!;
129
+ const journal = await store.read(runId);
130
+ if (!journal.ok) return blocked("run unavailable");
131
+ const archive = await buildRunArchive({
132
+ runId,
133
+ events: journal.events,
134
+ refusedAppends: await store.readNotes(runId),
135
+ readArtifact: (id) => readRunArtifact(store, runId, id),
136
+ });
137
+ if (!archive.ok) return blocked(archive.reason);
138
+ const destination = await resolvedDestination(
139
+ path.resolve(context.rootDir, args[2]!),
140
+ );
141
+ if (
142
+ within(await realpath(resolved.surface.commonDir), destination) ||
143
+ within(await realpath(resolved.surface.runsDir), destination)
144
+ )
145
+ return blocked(
146
+ "archive output cannot replace repository metadata or the live run store",
147
+ );
148
+ await createArtifactsPort().writeTextFile(destination, archive.text, {
149
+ mode: 0o600,
150
+ });
151
+ context.write(
152
+ JSON.stringify({
153
+ spec: "run-archive-export/1",
154
+ runId,
155
+ path: path.resolve(context.rootDir, args[2]!),
156
+ sizeBytes: Buffer.byteLength(archive.text),
157
+ authority: "observation",
158
+ }),
159
+ );
160
+ return { kind: "ok" };
161
+ } catch {
162
+ return blocked("archive file unreadable, oversized, unsafe, or unwritable");
163
+ }
164
+ }
@@ -0,0 +1,127 @@
1
+ import {
2
+ applySecretDiscipline,
3
+ canonicalize,
4
+ MAX_PORTABLE_RECORD_BYTES,
5
+ MAX_PORTABLE_ARTIFACTS,
6
+ MAX_PORTABLE_EVIDENCE_BYTES,
7
+ type RunArtifactResult,
8
+ type RunEvent,
9
+ } from "@agent-delivery-harness/kernel";
10
+ import { buildRunExport, parseRunExport } from "./run-export.ts";
11
+ import {
12
+ referencedAttachments,
13
+ validateRunAttachments,
14
+ type ArchivedAttachment,
15
+ } from "./run-attachments.ts";
16
+ export type RunArchiveBuildResult =
17
+ | { readonly ok: true; readonly text: string }
18
+ | { readonly ok: false; readonly reason: string };
19
+ /** Capture a snapshot; does not write or modify the source journal. */
20
+ export async function buildRunArchive(input: {
21
+ readonly runId: string;
22
+ readonly events: readonly RunEvent[];
23
+ readonly refusedAppends?: readonly unknown[];
24
+ readonly readArtifact: (artifactId: string) => Promise<RunArtifactResult>;
25
+ }): Promise<RunArchiveBuildResult> {
26
+ const base = buildRunExport(input);
27
+ if (
28
+ Buffer.byteLength(JSON.stringify(base), "utf8") > MAX_PORTABLE_RECORD_BYTES
29
+ )
30
+ return { ok: false, reason: "archive exceeds 16 MiB serialized limit" };
31
+ if (!parseRunExport(JSON.stringify(base)).ok)
32
+ return { ok: false, reason: "invalid run export" };
33
+ if (!applySecretDiscipline(base, new Set()).ok)
34
+ return { ok: false, reason: "archive contains a secret-like value" };
35
+ const refs = referencedAttachments(input.events);
36
+ if (refs && refs.size > MAX_PORTABLE_ARTIFACTS)
37
+ return { ok: false, reason: "archive exceeds 128 attachments" };
38
+ if (!refs)
39
+ return { ok: false, reason: "conflicting attachment reference bindings" };
40
+ const entries: ArchivedAttachment[] = [];
41
+ const blobs: Record<string, string> = Object.create(null) as Record<
42
+ string,
43
+ string
44
+ >;
45
+ if (base.spec === "delivery-run-export/2")
46
+ for (const metadata of refs.values()) {
47
+ const artifact = await input.readArtifact(metadata.artifactId);
48
+ if (!artifact.ok) {
49
+ if (artifact.code !== "missing" && artifact.code !== "access_refused")
50
+ return {
51
+ ok: false,
52
+ reason: `retained attachment ${artifact.code}; export refused`,
53
+ };
54
+ entries.push({
55
+ metadata,
56
+ availability: "unavailable",
57
+ code: artifact.code,
58
+ reason: artifact.reason.slice(0, 512),
59
+ });
60
+ continue;
61
+ }
62
+ if (canonicalize(artifact.metadata) !== canonicalize(metadata))
63
+ return { ok: false, reason: "attachment binding differs from journal" };
64
+ if (
65
+ blobs[metadata.digest] !== undefined &&
66
+ blobs[metadata.digest] !== artifact.base64
67
+ )
68
+ return { ok: false, reason: "conflicting attachment bytes" };
69
+ blobs[metadata.digest] = artifact.base64;
70
+ entries.push({ metadata, availability: "retained" });
71
+ if (
72
+ Buffer.byteLength(JSON.stringify({ entries, blobs }), "utf8") >
73
+ MAX_PORTABLE_EVIDENCE_BYTES
74
+ )
75
+ return {
76
+ ok: false,
77
+ reason: "archive exceeds 8 MiB serialized attachment limit",
78
+ };
79
+ }
80
+ const attachments = { entries, blobs };
81
+ if (
82
+ base.spec === "delivery-run-export/2" &&
83
+ !validateRunAttachments(attachments, input.events)
84
+ )
85
+ return {
86
+ ok: false,
87
+ reason: "attachment bytes, bindings or serialized limits are invalid",
88
+ };
89
+ const text = JSON.stringify({
90
+ ...base,
91
+ ...(base.spec === "delivery-run-export/2" ? { attachments } : {}),
92
+ });
93
+ if (Buffer.byteLength(text, "utf8") > MAX_PORTABLE_RECORD_BYTES)
94
+ return { ok: false, reason: "archive exceeds 16 MiB serialized limit" };
95
+ return { ok: true, text };
96
+ }
97
+ /** Historical read only: never resumes, imports, verifies a candidate, or resolves paths. */
98
+ export function readArchiveArtifact(
99
+ text: string,
100
+ artifactId: string,
101
+ ): RunArtifactResult {
102
+ if (Buffer.byteLength(text, "utf8") > MAX_PORTABLE_RECORD_BYTES)
103
+ return {
104
+ ok: false,
105
+ code: "invalid",
106
+ reason: "archive exceeds 16 MiB serialized limit",
107
+ };
108
+ const parsed = parseRunExport(text);
109
+ if (!parsed.ok)
110
+ return { ok: false, code: "invalid", reason: "archive invalid" };
111
+ const entry = parsed.value.attachments?.entries.find(
112
+ (e) => e.metadata.artifactId === artifactId,
113
+ );
114
+ if (!entry)
115
+ return {
116
+ ok: false,
117
+ code: "invalid",
118
+ reason: "attachment not retained in this archive",
119
+ };
120
+ if (entry.availability === "unavailable")
121
+ return { ok: false, code: entry.code!, reason: entry.reason! };
122
+ return {
123
+ ok: true,
124
+ metadata: entry.metadata,
125
+ base64: parsed.value.attachments!.blobs[entry.metadata.digest]!,
126
+ };
127
+ }