@agent-delivery-harness/cli 0.2.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 +4 -4
  4. package/src/boundary.ts +77 -61
  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 +80 -12
  10. package/src/commands/gate.ts +33 -8
  11. package/src/commands/maintain.ts +1 -0
  12. package/src/commands/managed.ts +1 -0
  13. package/src/commands/prepare.ts +123 -22
  14. package/src/commands/record.ts +98 -3
  15. package/src/commands/resume.ts +53 -0
  16. package/src/commands/review-context.ts +19 -0
  17. package/src/commands/runs.ts +415 -30
  18. package/src/commands/save-context.ts +51 -0
  19. package/src/commands/submit-evidence.ts +46 -7
  20. package/src/commands/verify.ts +29 -8
  21. package/src/declared-checks.ts +50 -0
  22. package/src/index.ts +14 -1
  23. package/src/main.ts +20 -47
  24. package/src/ordinary-context.ts +99 -0
  25. package/src/provider-rails.ts +23 -704
  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 +129 -11
  35. package/src/run-server.ts +394 -149
  36. package/src/run-surface.ts +81 -8
  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,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
+ }
@@ -0,0 +1,163 @@
1
+ /** Explicit acquisition capture; retained reports remain observations. */
2
+ import {
3
+ captureRunArtifact,
4
+ readRunArtifact,
5
+ validateRunEventInput,
6
+ type RunArtifactMetadata,
7
+ } from "@agent-delivery-harness/kernel";
8
+ import {
9
+ buildRunEvent,
10
+ oneLine,
11
+ resolveRunSurface,
12
+ runSurfaceBlocker,
13
+ } from "./run-surface.ts";
14
+ import type { CommandResult, ConfigFreeCommandContext } from "./boundary.ts";
15
+ const blocked = (reason: string): CommandResult => ({
16
+ kind: "blocked",
17
+ blockers: [
18
+ runSurfaceBlocker({
19
+ code: "run_artifact_unavailable",
20
+ summary: "The run attachment is unavailable.",
21
+ details: oneLine(reason),
22
+ remediation: {
23
+ id: "inspect-selected-report",
24
+ summary:
25
+ "Check the explicit report binding and selected source file, then retry capture.",
26
+ },
27
+ }),
28
+ ],
29
+ });
30
+ const record = (v: unknown): v is Record<string, unknown> =>
31
+ v !== null && typeof v === "object" && !Array.isArray(v);
32
+ export async function runArtifactCommand(
33
+ context: ConfigFreeCommandContext,
34
+ command: "capture" | "artifact",
35
+ args: readonly string[],
36
+ ): Promise<CommandResult> {
37
+ if (
38
+ command === "artifact" &&
39
+ (args.length < 2 ||
40
+ args.length > 3 ||
41
+ (args[2] !== undefined && args[2] !== "--json"))
42
+ )
43
+ return {
44
+ kind: "usage",
45
+ message: "Usage: runs artifact <run-id> <artifact-id> [--json]",
46
+ };
47
+ if (command === "capture" && (args.length !== 3 || args[1] !== "--json"))
48
+ return {
49
+ kind: "usage",
50
+ message: "Usage: runs capture <run-id> --json <request>",
51
+ };
52
+ const resolved = await resolveRunSurface(context.rootDir);
53
+ if (!resolved.ok) return blocked(resolved.reason);
54
+ const { store, commonDir } = resolved.surface;
55
+ const runId = args[0]!;
56
+ if (command === "artifact") {
57
+ const result = await readRunArtifact(store, runId, args[1]!);
58
+ if (!result.ok) return blocked(result.reason);
59
+ context.write(
60
+ args[2] === "--json"
61
+ ? `${JSON.stringify({ spec: "run-artifact/1", labels: "self-attested observation; not admission evidence", runId, ...result })}\n`
62
+ : `${oneLine(Buffer.from(result.base64, "base64").toString("utf8"), 2 * 1024 * 1024)}\n`,
63
+ );
64
+ return { kind: "ok" };
65
+ }
66
+ let request: unknown;
67
+ try {
68
+ if (Buffer.byteLength(args[2]!, "utf8") > 16384)
69
+ return blocked("capture request exceeds 16 KiB");
70
+ request = JSON.parse(args[2]!);
71
+ } catch {
72
+ return { kind: "usage", message: "Capture request must be JSON." };
73
+ }
74
+ if (
75
+ !record(request) ||
76
+ Object.keys(request).sort().join(",") !==
77
+ "artifact,eventId,report,sourcePath,sourceRoot" ||
78
+ typeof request["sourceRoot"] !== "string" ||
79
+ typeof request["sourcePath"] !== "string" ||
80
+ typeof request["eventId"] !== "string" ||
81
+ !record(request["artifact"]) ||
82
+ !record(request["report"])
83
+ )
84
+ return {
85
+ kind: "usage",
86
+ message:
87
+ "Capture requires artifact, report, eventId, sourceRoot and sourcePath.",
88
+ };
89
+ const artifact = request["artifact"];
90
+ const report = request["report"];
91
+ const binding = {
92
+ activityId: artifact["activityId"],
93
+ attemptId: artifact["attemptId"],
94
+ candidateTreeSha: artifact["candidateTreeSha"],
95
+ ...(artifact["roundId"] === undefined
96
+ ? {}
97
+ : { roundId: artifact["roundId"] }),
98
+ ...(artifact["round"] === undefined ? {} : { round: artifact["round"] }),
99
+ ...(artifact["lensId"] === undefined ? {} : { lensId: artifact["lensId"] }),
100
+ };
101
+ const journal = await store.read(runId);
102
+ if (!journal.ok || journal.events[0]?.version !== "run-event/2")
103
+ return blocked(
104
+ "capture requires an existing run-event/2 run; start an explicit successor for a legacy run",
105
+ );
106
+ const make = (suffix: string, kind: string, payload: unknown) => {
107
+ const event = buildRunEvent({
108
+ runId,
109
+ commonDir,
110
+ kind,
111
+ role: "executor",
112
+ payload,
113
+ version: "run-event/2",
114
+ eventId: `${request["eventId"]}-${suffix}`,
115
+ });
116
+ return event;
117
+ };
118
+ const artifactEvent = make("artifact", "artifact.referenced", artifact);
119
+ const reportEvent = make("report", "report.referenced", {
120
+ ...report,
121
+ ...binding,
122
+ availability: "referenced",
123
+ artifactId: artifact["artifactId"],
124
+ });
125
+ // Report input cannot override the capture's binding or carry success fields.
126
+ if (
127
+ Object.keys(report).some(
128
+ (k) =>
129
+ !["reportId", "role", "originatingReportId", "findingId"].includes(k),
130
+ ) ||
131
+ !validateRunEventInput(artifactEvent).ok ||
132
+ !validateRunEventInput(reportEvent).ok
133
+ )
134
+ return blocked("invalid report or artifact event binding");
135
+ const result = await captureRunArtifact({
136
+ store,
137
+ runId,
138
+ metadata: artifact as unknown as RunArtifactMetadata,
139
+ sourceRoot: request["sourceRoot"],
140
+ sourcePath: request["sourcePath"],
141
+ });
142
+ if (!result.ok) {
143
+ const unavailable = make("unavailable", "report.referenced", {
144
+ ...report,
145
+ ...binding,
146
+ availability: "unavailable",
147
+ reason: result.reason,
148
+ });
149
+ await store.append(runId, unavailable, { reuseExistingTimestamp: true });
150
+ return blocked(result.reason);
151
+ }
152
+ for (const event of [artifactEvent, reportEvent]) {
153
+ const appended = await store.append(runId, event, { reuseExistingTimestamp: true });
154
+ if (!appended.ok)
155
+ return blocked(
156
+ `bytes retained but reference append refused: ${appended.rejections.map((r) => r.message).join("; ")}`,
157
+ );
158
+ }
159
+ context.write(
160
+ `${JSON.stringify({ spec: "run-artifact-capture/1", runId, artifactId: result.metadata.artifactId, digest: result.metadata.digest, availability: "referenced", authority: "observation" })}\n`,
161
+ );
162
+ return { kind: "ok" };
163
+ }
@@ -0,0 +1,112 @@
1
+ /** Bounded observational attachment transport; no path is followed by this parser. */
2
+ import {
3
+ canonicalize,
4
+ sha256Hex,
5
+ portableArtifactContents,
6
+ applySecretDiscipline,
7
+ MAX_PORTABLE_ARTIFACTS,
8
+ MAX_PORTABLE_EVIDENCE_BYTES,
9
+ type RunArtifactMetadata,
10
+ type RunEvent,
11
+ } from "@agent-delivery-harness/kernel";
12
+ export interface ArchivedAttachment {
13
+ readonly metadata: RunArtifactMetadata;
14
+ readonly availability: "retained" | "unavailable";
15
+ readonly reason?: string;
16
+ readonly code?: "missing" | "access_refused";
17
+ }
18
+ export interface RunAttachments {
19
+ readonly entries: readonly ArchivedAttachment[];
20
+ readonly blobs: Readonly<Record<string, string>>;
21
+ }
22
+ const record = (v: unknown): v is Record<string, unknown> =>
23
+ v !== null && typeof v === "object" && !Array.isArray(v);
24
+ export function referencedAttachments(
25
+ events: readonly RunEvent[],
26
+ ): Map<string, RunArtifactMetadata> | undefined {
27
+ const entries = new Map<string, RunArtifactMetadata>();
28
+ for (const event of events) {
29
+ if (event.kind !== "artifact.referenced") continue;
30
+ const metadata = event.payload as unknown as RunArtifactMetadata;
31
+ const prior = entries.get(metadata.artifactId);
32
+ if (prior && canonicalize(prior) !== canonicalize(metadata))
33
+ return undefined;
34
+ entries.set(metadata.artifactId, metadata);
35
+ }
36
+ return entries;
37
+ }
38
+ export function validateRunAttachments(
39
+ value: unknown,
40
+ events: readonly RunEvent[],
41
+ ): value is RunAttachments {
42
+ if (
43
+ !record(value) ||
44
+ Object.keys(value).sort().join(",") !== "blobs,entries" ||
45
+ !Array.isArray(value["entries"]) ||
46
+ !record(value["blobs"]) ||
47
+ Buffer.byteLength(JSON.stringify(value), "utf8") >
48
+ MAX_PORTABLE_EVIDENCE_BYTES
49
+ )
50
+ return false;
51
+ const refs = referencedAttachments(events);
52
+ if (
53
+ !refs ||
54
+ refs.size > MAX_PORTABLE_ARTIFACTS ||
55
+ value["entries"].length !== refs.size
56
+ )
57
+ return false;
58
+ const seen = new Set<string>();
59
+ const needed = new Set<string>();
60
+ const read = portableArtifactContents(value["blobs"]);
61
+ if (read.blockers.length) return false;
62
+ for (const entry of value["entries"]) {
63
+ if (!record(entry) || !record(entry["metadata"])) return false;
64
+ const metadata = entry["metadata"] as unknown as RunArtifactMetadata;
65
+ if (
66
+ seen.has(metadata.artifactId) ||
67
+ !refs.has(metadata.artifactId) ||
68
+ canonicalize(metadata) !== canonicalize(refs.get(metadata.artifactId))
69
+ )
70
+ return false;
71
+ seen.add(metadata.artifactId);
72
+ if (entry["availability"] === "unavailable") {
73
+ if (
74
+ Object.keys(entry).sort().join(",") !==
75
+ "availability,code,metadata,reason" ||
76
+ !["missing", "access_refused"].includes(String(entry["code"])) ||
77
+ typeof entry["reason"] !== "string" ||
78
+ !entry["reason"].length ||
79
+ entry["reason"].length > 512
80
+ )
81
+ return false;
82
+ } else if (entry["availability"] === "retained") {
83
+ if (Object.keys(entry).sort().join(",") !== "availability,metadata")
84
+ return false;
85
+ const observed = read.observations.get(metadata.digest);
86
+ if (!observed || observed.base64 === undefined) return false;
87
+ const bytes = Buffer.from(observed.base64, "base64");
88
+ if (
89
+ bytes.length !== metadata.sizeBytes ||
90
+ sha256Hex(bytes) !== metadata.digest
91
+ )
92
+ return false;
93
+ let structured: unknown = null;
94
+ try {
95
+ structured = JSON.parse(bytes.toString("utf8"));
96
+ } catch {}
97
+ if (
98
+ !applySecretDiscipline(
99
+ { text: bytes.toString("utf8"), structured },
100
+ new Set(),
101
+ ).ok
102
+ )
103
+ return false;
104
+ needed.add(metadata.digest);
105
+ } else return false;
106
+ }
107
+ return (
108
+ Object.keys(value["blobs"]).length === needed.size &&
109
+ Object.keys(value["blobs"]).every((key) => needed.has(key)) &&
110
+ applySecretDiscipline(value["entries"], new Set()).ok
111
+ );
112
+ }