@aiwg/cli 2026.8.3 → 2026.8.5

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.
@@ -60,6 +60,17 @@ const DEFAULT_REGISTRY = {
60
60
  servers: {},
61
61
  };
62
62
 
63
+ const ENV_REFERENCE_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
64
+
65
+ function validateCredentialReferences(def) {
66
+ for (const [header, envName] of Object.entries(def.headerEnv || {})) {
67
+ if (!header.trim()) throw new Error('MCP header-env header name must not be empty');
68
+ if (!ENV_REFERENCE_NAME.test(envName)) {
69
+ throw new Error(`Invalid MCP header environment variable reference "${envName}"`);
70
+ }
71
+ }
72
+ }
73
+
63
74
  export class McpServerRegistry {
64
75
  #configDir;
65
76
  #cache = null;
@@ -99,6 +110,7 @@ export class McpServerRegistry {
99
110
  }
100
111
 
101
112
  async add(def) {
113
+ validateCredentialReferences(def);
102
114
  const data = await this.load();
103
115
 
104
116
  if (data.servers[def.name]) {
@@ -133,12 +145,14 @@ export class McpServerRegistry {
133
145
  throw new Error(`Server "${name}" not found.`);
134
146
  }
135
147
 
136
- data.servers[name] = {
148
+ const next = {
137
149
  ...data.servers[name],
138
150
  ...updates,
139
151
  name,
140
152
  updatedAt: new Date().toISOString(),
141
153
  };
154
+ validateCredentialReferences(next);
155
+ data.servers[name] = next;
142
156
 
143
157
  await this.save();
144
158
  }
@@ -40,7 +40,7 @@ function usage() {
40
40
  return [
41
41
  "Usage: aiwg research-query <question> [--backend fortemi-core|local] [--graph <name>]",
42
42
  " [--depth quick|thorough] [--sources-only] [--max-sources N]",
43
- " [--json] [--save]",
43
+ " [--include-diagnostics] [--json] [--save]",
44
44
  ].join("\n");
45
45
  }
46
46
  function flagValue(args, flag, errorMessage) {
@@ -73,7 +73,12 @@ function stripFlags(args) {
73
73
  "--depth",
74
74
  "--max-sources",
75
75
  ]);
76
- const bareFlags = new Set(["--sources-only", "--json", "--save"]);
76
+ const bareFlags = new Set([
77
+ "--sources-only",
78
+ "--include-diagnostics",
79
+ "--json",
80
+ "--save",
81
+ ]);
77
82
  const question = [];
78
83
  for (let index = 0; index < args.length; index++) {
79
84
  const arg = args[index];
@@ -96,8 +101,7 @@ function parseArgs(args) {
96
101
  if (backend && backend !== "local" && backend !== "fortemi-core") {
97
102
  throw new Error("--backend must be local or fortemi-core");
98
103
  }
99
- const depth = (flagValue(args, "--depth", "--depth must be quick or thorough") ??
100
- "thorough");
104
+ const depth = (flagValue(args, "--depth", "--depth must be quick or thorough") ?? "thorough");
101
105
  if (depth !== "quick" && depth !== "thorough") {
102
106
  throw new Error("--depth must be quick or thorough");
103
107
  }
@@ -113,6 +117,7 @@ function parseArgs(args) {
113
117
  depth,
114
118
  maxSources,
115
119
  sourcesOnly: hasFlag(args, "--sources-only"),
120
+ includeDiagnostics: hasFlag(args, "--include-diagnostics"),
116
121
  json: hasFlag(args, "--json"),
117
122
  save: hasFlag(args, "--save"),
118
123
  };
@@ -140,18 +145,40 @@ function entryId(entry) {
140
145
  text.match(/\bPROF-[A-Z0-9-]+/i)?.[0]?.toUpperCase() ??
141
146
  path.basename(entry.path).replace(/\.[^.]+$/, ""));
142
147
  }
143
- function gradeFromText(text) {
144
- const normalized = text.toUpperCase();
145
- if (/\bVERY\s+LOW\b/.test(normalized))
148
+ function normalizedGrade(value) {
149
+ const normalized = value.trim().toUpperCase();
150
+ if (/^(VERY\s+LOW|D)$/.test(normalized))
146
151
  return "VERY LOW";
147
- if (/\bHIGH\b/.test(normalized))
152
+ if (/^(HIGH|A(?:-)?)$/.test(normalized))
148
153
  return "HIGH";
149
- if (/\bMODERATE\b/.test(normalized))
154
+ if (/^(MODERATE|B(?:-)?)$/.test(normalized))
150
155
  return "MODERATE";
151
- if (/\bLOW\b/.test(normalized))
156
+ if (/^(LOW|C(?:-)?)$/.test(normalized))
152
157
  return "LOW";
153
158
  return "UNKNOWN";
154
159
  }
160
+ /**
161
+ * Extract only an explicitly declared research grade. Generic severity words
162
+ * in scan reports must never become evidence-quality signals.
163
+ */
164
+ function gradeForEntry(entry, body) {
165
+ for (const tag of entry.tags) {
166
+ const tagged = tag.match(/^grade[-_: ](very[-_ ]low|high|moderate|low|[a-d](?:-)?)$/i);
167
+ if (tagged)
168
+ return normalizedGrade(tagged[1].replace(/[-_]/g, " "));
169
+ }
170
+ const text = [entry.summary, body].join("\n");
171
+ const declarations = [
172
+ /\bGRADE(?:\s+(?:quality|rating|level|assessment))?\s*[:=]\s*\*{0,2}\s*(VERY\s+LOW|HIGH|MODERATE|LOW|[A-D](?:-)?)\b/i,
173
+ /##\s+GRADE[^\n]*\n(?:[^\n]*\n){0,3}?[^\n]*?(?:overall|rating|level)?\s*[:=]\s*\*{0,2}\s*(VERY\s+LOW|HIGH|MODERATE|LOW|[A-D](?:-)?)\b/i,
174
+ ];
175
+ for (const declaration of declarations) {
176
+ const match = declaration.exec(text);
177
+ if (match)
178
+ return normalizedGrade(match[1]);
179
+ }
180
+ return "UNKNOWN";
181
+ }
155
182
  function relevance(score) {
156
183
  if (score >= 0.55)
157
184
  return "direct";
@@ -191,26 +218,58 @@ function isResearchEntry(entry) {
191
218
  entryPath.includes("/research/") ||
192
219
  entryPath.includes("/kb/"));
193
220
  }
221
+ /** Generated diagnostics are searchable only by explicit operator opt-in. */
222
+ function isDiagnosticEntry(entry) {
223
+ const type = entry.type.toLowerCase();
224
+ const entryPath = entry.path.toLowerCase().replaceAll("\\", "/");
225
+ const tags = entry.tags.map((tag) => tag.toLowerCase());
226
+ return (entryPath.includes("/.aiwg/research/quarantine/") ||
227
+ entryPath.includes("/research/quarantine/") ||
228
+ /(?:^|\/)no-ref-[^/]*artifact-scan\.md$/.test(entryPath) ||
229
+ /(?:llm|integrity|artifact)-scan\.md$/.test(entryPath) ||
230
+ /(?:integrity|artifact|llm)[-_ ]scan|quarantine|diagnostic/.test(type) ||
231
+ tags.some((tag) => /^(?:integrity|artifact|llm)[-_ ]scan$|^quarantine$|^diagnostic$/.test(tag)));
232
+ }
194
233
  function localEntries(cwd, graph) {
195
234
  const index = loadGraphIndexFile(cwd, "metadata.json", graph);
196
- return index ? Object.values(index.entries) : [];
235
+ return index ? Object.values(index.entries).map((entry) => ({ entry })) : [];
236
+ }
237
+ function cachedRecordBody(record) {
238
+ return [
239
+ record.search?.body,
240
+ record.text,
241
+ ...(record.chunks ?? []).map((chunk) => chunk.text),
242
+ ]
243
+ .filter((part) => Boolean(part))
244
+ .join("\n");
197
245
  }
198
246
  async function backendEntries(cwd, graph, backend) {
199
247
  if (backend === "fortemi-core") {
200
- const { loadFortemiCoreMetadataEntries } = await import("../artifacts/fortemi-core-query-adapter.js");
248
+ const { loadFortemiCoreExport, loadFortemiCoreMetadataEntries } = await import("../artifacts/fortemi-core-query-adapter.js");
201
249
  const loaded = loadFortemiCoreMetadataEntries(cwd, graph);
202
- return { entries: loaded.entries, hint: loaded.reason };
250
+ if (loaded.reason)
251
+ return { entries: [], hint: loaded.reason };
252
+ const exported = loadFortemiCoreExport(cwd, graph);
253
+ if (!exported.exported)
254
+ return { entries: [], hint: exported.reason };
255
+ const records = new Map(exported.exported.items.map((record) => [record.source.path, record]));
256
+ return {
257
+ entries: loaded.entries.map((entry) => {
258
+ const record = records.get(entry.path);
259
+ return {
260
+ entry,
261
+ cachedBody: record ? cachedRecordBody(record) : "",
262
+ };
263
+ }),
264
+ };
203
265
  }
204
266
  return { entries: localEntries(cwd, graph) };
205
267
  }
206
- function sourceForEntry(cwd, entry, question, depth) {
207
- const body = depth === "thorough" ? readEntryBody(cwd, entry.path) : "";
208
- const sourceText = [
209
- entry.title,
210
- entry.summary,
211
- entry.tags.join(" "),
212
- body,
213
- ].join("\n");
268
+ function sourceForEntry(cwd, candidate, question, depth) {
269
+ const { entry } = candidate;
270
+ const body = depth === "thorough"
271
+ ? (candidate.cachedBody ?? readEntryBody(cwd, entry.path))
272
+ : "";
214
273
  const score = scoreEntry(entry, question, body, depth);
215
274
  if (score <= 0)
216
275
  return null;
@@ -219,7 +278,7 @@ function sourceForEntry(cwd, entry, question, depth) {
219
278
  path: entry.path,
220
279
  title: entry.title,
221
280
  type: entry.type,
222
- grade: gradeFromText(sourceText),
281
+ grade: gradeForEntry(entry, body),
223
282
  relevance: relevance(score),
224
283
  score: Math.round(score * 1000) / 1000,
225
284
  summary: entry.summary,
@@ -236,13 +295,24 @@ export async function runResearchQuery(cwd, options) {
236
295
  throw new Error(loaded.hint);
237
296
  }
238
297
  const sources = loaded.entries
239
- .filter(isResearchEntry)
240
- .map((entry) => sourceForEntry(cwd, entry, options.question, depth))
298
+ .filter(({ entry }) => isResearchEntry(entry))
299
+ .filter(({ entry }) => options.includeDiagnostics || !isDiagnosticEntry(entry))
300
+ .map((candidate) => sourceForEntry(cwd, candidate, options.question, depth))
241
301
  .filter((source) => source !== null)
242
302
  .sort((left, right) => {
243
303
  const scoreCmp = right.score - left.score;
244
304
  if (scoreCmp !== 0)
245
305
  return scoreCmp;
306
+ const gradeOrder = [
307
+ "HIGH",
308
+ "MODERATE",
309
+ "LOW",
310
+ "VERY LOW",
311
+ "UNKNOWN",
312
+ ];
313
+ const gradeCmp = gradeOrder.indexOf(left.grade) - gradeOrder.indexOf(right.grade);
314
+ if (gradeCmp !== 0)
315
+ return gradeCmp;
246
316
  return left.path.localeCompare(right.path);
247
317
  })
248
318
  .slice(0, maxSources);
@@ -49,6 +49,13 @@ export interface WebReleaseOptions {
49
49
  credentialProvider?: () => Promise<string | null>;
50
50
  /** Test/development escape hatch. HTTP remains restricted to loopback. */
51
51
  allowInsecureLoopbackHttp?: boolean;
52
+ /** Structured cache diagnostics; never includes URLs, headers, or credentials. */
53
+ onDiagnostic?: (diagnostic: WebReleaseDiagnostic) => void;
54
+ }
55
+ export interface WebReleaseDiagnostic {
56
+ resource: "channel" | "version-index";
57
+ outcome: "conditional-hit" | "revalidated" | "unconditional";
58
+ validator: "etag" | "last-modified" | "none";
52
59
  }
53
60
  export interface VerifiedReleaseDescriptor {
54
61
  path: string;
@@ -386,6 +386,69 @@ async function fetchBytes(fetcher, url, label, maxBytes, bearerToken) {
386
386
  clearTimeout(timeout);
387
387
  }
388
388
  }
389
+ function validatorFromResponse(response, payloadSha256) {
390
+ // Preserve the origin's ETag octets, including the W/ prefix for weak tags.
391
+ // HTTP validators only suppress transfer; Ed25519 and SHA-256 remain the
392
+ // authority for every cached representation accepted by this module.
393
+ const etag = response.headers.get("etag")?.trim();
394
+ const lastModified = response.headers.get("last-modified")?.trim();
395
+ if (!etag && !lastModified)
396
+ return undefined;
397
+ return {
398
+ schemaVersion: "aiwg.http-validator/v1",
399
+ payloadSha256,
400
+ ...(etag ? { etag } : { lastModified: lastModified }),
401
+ };
402
+ }
403
+ function readMetadataValidator(pathname, payloadSha256) {
404
+ if (!fs.existsSync(pathname))
405
+ return undefined;
406
+ const value = parseJson(readVerifiedRegularFile(pathname, {
407
+ label: "cached HTTP metadata validator",
408
+ maxBytes: MAX_COMPLETION_MARKER_BYTES,
409
+ }), "cached HTTP metadata validator");
410
+ if (!isRecord(value) || value.schemaVersion !== "aiwg.http-validator/v1" || value.payloadSha256 !== payloadSha256) {
411
+ throw new Error("cached HTTP metadata validator does not match the verified signed payload");
412
+ }
413
+ const etag = typeof value.etag === "string" && value.etag.trim() ? value.etag.trim() : undefined;
414
+ const lastModified = typeof value.lastModified === "string" && value.lastModified.trim() ? value.lastModified.trim() : undefined;
415
+ if (!etag && !lastModified)
416
+ throw new Error("cached HTTP metadata validator is empty");
417
+ return { schemaVersion: "aiwg.http-validator/v1", payloadSha256, ...(etag ? { etag } : { lastModified }) };
418
+ }
419
+ async function fetchMetadata(fetcher, url, label, maxBytes, cachedValidator) {
420
+ const headers = { "accept-encoding": "identity" };
421
+ if (cachedValidator?.etag)
422
+ headers["if-none-match"] = cachedValidator.etag;
423
+ else if (cachedValidator?.lastModified)
424
+ headers["if-modified-since"] = cachedValidator.lastModified;
425
+ const controller = new AbortController();
426
+ const timeout = setTimeout(() => controller.abort(), RESOURCE_FETCH_TIMEOUT_MS);
427
+ let response;
428
+ try {
429
+ response = await fetcher(url, { redirect: "error", headers, signal: controller.signal });
430
+ }
431
+ catch (error) {
432
+ if (controller.signal.aborted)
433
+ throw new Error(`${label} request timed out after ${RESOURCE_FETCH_TIMEOUT_MS}ms`);
434
+ throw error;
435
+ }
436
+ finally {
437
+ clearTimeout(timeout);
438
+ }
439
+ if (response.status === 304) {
440
+ if (!cachedValidator)
441
+ throw new Error(`${label} returned 304 without a verified cached representation`);
442
+ return {
443
+ notModified: true,
444
+ validator: validatorFromResponse(response, cachedValidator.payloadSha256) ?? cachedValidator,
445
+ };
446
+ }
447
+ if (!response.ok)
448
+ throw new Error(`${label} fetch failed (${response.status}): ${url}`);
449
+ const bytes = await fetchBytes(async () => response, url, label, maxBytes);
450
+ return { notModified: false, bytes, validator: validatorFromResponse(response, sha256(bytes)) };
451
+ }
389
452
  function verifyDescriptor(bytes, descriptor, label = descriptor.path) {
390
453
  if (bytes.byteLength !== descriptor.size || sha256(bytes) !== descriptor.sha256) {
391
454
  throw new Error(`release descriptor size or digest verification failed: ${label}`);
@@ -623,7 +686,14 @@ function readCachedChannel(cacheRoot, channel, publicKeyPem) {
623
686
  if (candidate.name !== `${manifest.sequence}-${digest}`) {
624
687
  throw new Error(`cached channel ${channel} generation name does not match its signed metadata`);
625
688
  }
626
- valid.push({ manifest, bytes, signatureBytes, digest });
689
+ let validator;
690
+ try {
691
+ validator = readMetadataValidator(path.join(dir, "http-validator.json"), digest);
692
+ }
693
+ catch {
694
+ validator = undefined;
695
+ }
696
+ valid.push({ manifest, bytes, signatureBytes, digest, validator });
627
697
  }
628
698
  catch {
629
699
  corrupt = true;
@@ -658,10 +728,23 @@ function readCachedVersionIndex(cacheRoot, publicKeyPem) {
658
728
  label: "cached resource version index signature",
659
729
  maxBytes: MAX_SIGNATURE_BYTES,
660
730
  });
661
- verifySignedResourceBytes(bytes, signatureBytes, publicKeyPem, "cached resource version index");
662
- return validateVersionIndex(parseJson(bytes, "cached resource version index"));
731
+ const digest = verifySignedResourceBytes(bytes, signatureBytes, publicKeyPem, "cached resource version index");
732
+ return {
733
+ index: validateVersionIndex(parseJson(bytes, "cached resource version index")),
734
+ bytes,
735
+ signatureBytes,
736
+ digest,
737
+ validator: (() => {
738
+ try {
739
+ return readMetadataValidator(path.join(dir, "http-validator.json"), digest);
740
+ }
741
+ catch {
742
+ return undefined;
743
+ }
744
+ })(),
745
+ };
663
746
  }
664
- function cacheVersionIndex(cacheRoot, bytes, signatureBytes) {
747
+ function cacheVersionIndex(cacheRoot, bytes, signatureBytes, validator) {
665
748
  const target = versionIndexCacheDir(cacheRoot);
666
749
  const stagingRoot = path.join(cacheRoot, ".staging", "versions");
667
750
  fs.mkdirSync(stagingRoot, { recursive: true });
@@ -669,6 +752,8 @@ function cacheVersionIndex(cacheRoot, bytes, signatureBytes) {
669
752
  try {
670
753
  fs.writeFileSync(path.join(stage, "versions.json"), bytes, { flag: "wx" });
671
754
  fs.writeFileSync(path.join(stage, "versions.sig"), signatureBytes, { flag: "wx" });
755
+ if (validator)
756
+ fs.writeFileSync(path.join(stage, "http-validator.json"), `${JSON.stringify(validator)}\n`, { flag: "wx" });
672
757
  if (fs.existsSync(target))
673
758
  fs.rmSync(target, { recursive: true, force: true });
674
759
  installGeneration(stage, target);
@@ -678,19 +763,33 @@ function cacheVersionIndex(cacheRoot, bytes, signatureBytes) {
678
763
  throw error;
679
764
  }
680
765
  }
681
- async function fetchAndCacheVersionIndex(base, fetcher, cacheRoot, publicKeyPem) {
682
- const indexBytes = await fetchBytes(fetcher, resourceUrl(base, "resources/versions.json"), "resource version index", MAX_VERSION_INDEX_BYTES);
766
+ async function fetchAndCacheVersionIndex(base, fetcher, cacheRoot, publicKeyPem, diagnostic) {
767
+ let cached = null;
768
+ try {
769
+ cached = readCachedVersionIndex(cacheRoot, publicKeyPem);
770
+ }
771
+ catch {
772
+ cached = null;
773
+ }
774
+ const fetched = await fetchMetadata(fetcher, resourceUrl(base, "resources/versions.json"), "resource version index", MAX_VERSION_INDEX_BYTES, cached?.validator);
775
+ if (fetched.notModified) {
776
+ cacheVersionIndex(cacheRoot, cached.bytes, cached.signatureBytes, fetched.validator);
777
+ diagnostic?.({ resource: "version-index", outcome: "conditional-hit", validator: cached.validator.etag ? "etag" : "last-modified" });
778
+ return cached.index;
779
+ }
780
+ const indexBytes = fetched.bytes;
683
781
  const signatureBytes = await fetchBytes(fetcher, resourceUrl(base, "resources/versions.sig"), "resource version index signature", MAX_SIGNATURE_BYTES);
684
782
  verifySignedResourceBytes(indexBytes, signatureBytes, publicKeyPem, "resource version index");
685
783
  const index = validateVersionIndex(parseJson(indexBytes, "resource version index"));
686
- cacheVersionIndex(cacheRoot, indexBytes, signatureBytes);
784
+ cacheVersionIndex(cacheRoot, indexBytes, signatureBytes, fetched.validator);
785
+ diagnostic?.({ resource: "version-index", outcome: cached?.validator ? "revalidated" : "unconditional", validator: fetched.validator?.etag ? "etag" : fetched.validator?.lastModified ? "last-modified" : "none" });
687
786
  return index;
688
787
  }
689
- async function resolveVersionIndex(base, fetcher, cacheRoot, publicKeyPem, offline) {
788
+ async function resolveVersionIndex(base, fetcher, cacheRoot, publicKeyPem, offline, diagnostic) {
690
789
  if (offline) {
691
790
  const cached = readCachedVersionIndex(cacheRoot, publicKeyPem);
692
791
  if (cached)
693
- return cached;
792
+ return cached.index;
694
793
  const versions = cachedReleaseVersions(cacheRoot).flatMap((version) => cachedDigests(cacheRoot, version).map((digest) => ({
695
794
  version,
696
795
  releaseManifest: `/resources/${version}/manifest.json`,
@@ -703,12 +802,14 @@ async function resolveVersionIndex(base, fetcher, cacheRoot, publicKeyPem, offli
703
802
  if (!fetcher)
704
803
  throw new Error("No fetch implementation is available for AIWG web resources");
705
804
  try {
706
- return await fetchAndCacheVersionIndex(base, fetcher, cacheRoot, publicKeyPem);
805
+ return await fetchAndCacheVersionIndex(base, fetcher, cacheRoot, publicKeyPem, diagnostic);
707
806
  }
708
807
  catch (error) {
808
+ if (error instanceof Error && /fetch failed \((?:401|403|429|5\d\d)\)/.test(error.message))
809
+ throw error;
709
810
  const cached = readCachedVersionIndex(cacheRoot, publicKeyPem);
710
811
  if (cached)
711
- return cached;
812
+ return cached.index;
712
813
  throw error;
713
814
  }
714
815
  }
@@ -776,7 +877,7 @@ function fsyncTree(root) {
776
877
  }
777
878
  fsyncDirectory(root);
778
879
  }
779
- function cacheChannel(cacheRoot, manifest, bytes, signatureBytes, digest) {
880
+ function cacheChannel(cacheRoot, manifest, bytes, signatureBytes, digest, validator) {
780
881
  const root = channelGenerationRoot(cacheRoot, manifest.channel);
781
882
  const stagingRoot = path.join(cacheRoot, ".staging", "channels");
782
883
  fs.mkdirSync(stagingRoot, { recursive: true });
@@ -784,6 +885,8 @@ function cacheChannel(cacheRoot, manifest, bytes, signatureBytes, digest) {
784
885
  try {
785
886
  fs.writeFileSync(path.join(stage, "channel.json"), bytes, { flag: "wx" });
786
887
  fs.writeFileSync(path.join(stage, "channel.sig"), signatureBytes, { flag: "wx" });
888
+ if (validator)
889
+ fs.writeFileSync(path.join(stage, "http-validator.json"), `${JSON.stringify(validator)}\n`, { flag: "wx" });
787
890
  const target = path.join(root, `${manifest.sequence}-${digest}`);
788
891
  if (fs.existsSync(target)) {
789
892
  let matches = false;
@@ -797,7 +900,11 @@ function cacheChannel(cacheRoot, manifest, bytes, signatureBytes, digest) {
797
900
  readVerifiedRegularFile(path.join(target, "channel.sig"), {
798
901
  label: `cached channel ${manifest.channel} signature`,
799
902
  maxBytes: MAX_SIGNATURE_BYTES,
800
- }).equals(Buffer.from(signatureBytes));
903
+ }).equals(Buffer.from(signatureBytes)) &&
904
+ (validator
905
+ ? readMetadataValidator(path.join(target, "http-validator.json"), digest)?.etag === validator.etag &&
906
+ readMetadataValidator(path.join(target, "http-validator.json"), digest)?.lastModified === validator.lastModified
907
+ : !fs.existsSync(path.join(target, "http-validator.json")));
801
908
  }
802
909
  catch {
803
910
  matches = false;
@@ -921,8 +1028,20 @@ export async function resolveWebRelease(options = {}) {
921
1028
  return fetchAndCacheRelease(base, fetcher, cacheRoot, selector, selector.value, publicKeyPem);
922
1029
  }
923
1030
  if (selector.kind === "range" || selector.kind === "digest") {
1031
+ if (!options.offline && selector.kind === "digest") {
1032
+ for (const version of cachedReleaseVersions(cacheRoot)) {
1033
+ if (!cachedDigests(cacheRoot, version).includes(selector.digest))
1034
+ continue;
1035
+ try {
1036
+ return verifyCachedGeneration(cacheRoot, version, selector.digest, selector, publicKeyPem, base, selector.digest);
1037
+ }
1038
+ catch {
1039
+ // A corrupt immutable generation cannot bypass signed index resolution.
1040
+ }
1041
+ }
1042
+ }
924
1043
  const fetcher = authorize;
925
- const index = await resolveVersionIndex(base, fetcher, cacheRoot, publicKeyPem, options.offline);
1044
+ const index = await resolveVersionIndex(base, fetcher, cacheRoot, publicKeyPem, options.offline, options.onDiagnostic);
926
1045
  const selected = selectVersionFromIndex(index, selector);
927
1046
  if (options.offline) {
928
1047
  return resolveOfflineExact(cacheRoot, selector, selected.version, publicKeyPem, base, selected.releaseManifestSha256);
@@ -941,11 +1060,23 @@ export async function resolveWebRelease(options = {}) {
941
1060
  if (!fetcher)
942
1061
  throw new Error("No fetch implementation is available for AIWG web resources");
943
1062
  const channelPrefix = `resources/channels/${selector.value}`;
944
- const channelBytes = await fetchBytes(fetcher, resourceUrl(base, `${channelPrefix}.json`), `channel ${selector.value}`, MAX_SIGNED_METADATA_BYTES);
1063
+ let prior = null;
1064
+ try {
1065
+ prior = readCachedChannel(cacheRoot, selector.value, publicKeyPem);
1066
+ }
1067
+ catch {
1068
+ prior = null;
1069
+ }
1070
+ const fetched = await fetchMetadata(fetcher, resourceUrl(base, `${channelPrefix}.json`), `channel ${selector.value}`, MAX_SIGNED_METADATA_BYTES, prior?.validator);
1071
+ if (fetched.notModified) {
1072
+ cacheChannel(cacheRoot, prior.manifest, prior.bytes, prior.signatureBytes, prior.digest, fetched.validator);
1073
+ options.onDiagnostic?.({ resource: "channel", outcome: "conditional-hit", validator: prior.validator.etag ? "etag" : "last-modified" });
1074
+ return fetchAndCacheRelease(base, fetcher, cacheRoot, selector, prior.manifest.version, publicKeyPem, prior.manifest.releaseManifestSha256, prior.manifest.sequence);
1075
+ }
1076
+ const channelBytes = fetched.bytes;
945
1077
  const channelSignatureBytes = await fetchBytes(fetcher, resourceUrl(base, `${channelPrefix}.sig`), `channel ${selector.value} signature`, MAX_SIGNATURE_BYTES);
946
1078
  const channelDigest = verifySignedResourceBytes(channelBytes, channelSignatureBytes, publicKeyPem, `channel ${selector.value}`);
947
1079
  const channel = validateChannelManifest(parseJson(channelBytes, `channel ${selector.value}`), selector.value);
948
- const prior = readCachedChannel(cacheRoot, selector.value, publicKeyPem);
949
1080
  if (prior && channel.sequence < prior.manifest.sequence) {
950
1081
  throw new Error(`channel ${selector.value} sequence rollback detected (${channel.sequence} < ${prior.manifest.sequence})`);
951
1082
  }
@@ -957,7 +1088,8 @@ export async function resolveWebRelease(options = {}) {
957
1088
  throw new Error(`channel ${selector.value} sequence ${channel.sequence} has conflicting signed metadata`);
958
1089
  }
959
1090
  const release = await fetchAndCacheRelease(base, fetcher, cacheRoot, selector, channel.version, publicKeyPem, channel.releaseManifestSha256, channel.sequence);
960
- cacheChannel(cacheRoot, channel, channelBytes, channelSignatureBytes, channelDigest);
1091
+ cacheChannel(cacheRoot, channel, channelBytes, channelSignatureBytes, channelDigest, fetched.validator);
1092
+ options.onDiagnostic?.({ resource: "channel", outcome: prior?.validator ? "revalidated" : "unconditional", validator: fetched.validator?.etag ? "etag" : fetched.validator?.lastModified ? "last-modified" : "none" });
961
1093
  return release;
962
1094
  }
963
1095
  export async function fetchVerifiedRawResource(release, resourcePath, options = {}) {