@event4u/agent-config 17.2.1 → 17.4.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 (225) hide show
  1. package/.augment-plugin/marketplace.json +3 -3
  2. package/.augment-plugin/plugin.json +1 -1
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/.claude-plugin/skills/install-agent-config/SKILL.md +9 -9
  5. package/CHANGELOG.md +167 -193
  6. package/CREDITS.md +2 -2
  7. package/README.md +1 -1
  8. package/dist/agent-src/skills/code-intelligence/SKILL.md +16 -12
  9. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  10. package/dist/agent-src/templates/scripts/check_memory.ts +1 -1
  11. package/dist/cli/commands/recordTriggerEval.js +1 -1
  12. package/dist/cli/registry.js +2 -2
  13. package/dist/cli-delegate/{chunk-KR5ZIFPO.js → chunk-5AYJGUA3.js} +1 -3
  14. package/dist/cli-delegate/chunk-DIFZBP4G.js +1336 -0
  15. package/dist/cli-delegate/chunk-E7ACTJTJ.js +6792 -0
  16. package/dist/cli-delegate/{chunk-VYZGJLCA.js → chunk-RKNT4LIK.js} +22 -1286
  17. package/dist/cli-delegate/{chunk-BZJ4Y3CI.js → chunk-WY5ZZPGL.js} +2 -2
  18. package/dist/cli-delegate/{chunk-CMZEKSNN.js → chunk-X6LZ7W5A.js} +73 -10
  19. package/dist/cli-delegate/cmd_conformance.js +190 -192
  20. package/dist/cli-delegate/cmd_doctor.js +5 -5
  21. package/dist/cli-delegate/cmd_doctor_neighbours.js +3 -3
  22. package/dist/cli-delegate/cmd_explain.js +1 -1
  23. package/dist/cli-delegate/cmd_export.js +2 -2
  24. package/dist/cli-delegate/cmd_handoff.js +2 -2
  25. package/dist/cli-delegate/cmd_memory_get.js +1 -1
  26. package/dist/cli-delegate/cmd_prune.js +2 -2
  27. package/dist/cli-delegate/cmd_route_audit.js +1 -1
  28. package/dist/cli-delegate/cmd_session_recycle.js +2 -2
  29. package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
  30. package/dist/cli-delegate/cmd_settings_set.js +8 -4
  31. package/dist/cli-delegate/cmd_sync.js +2 -2
  32. package/dist/cli-delegate/cmd_uninstall.js +2 -2
  33. package/dist/cli-delegate/cmd_update.js +24 -13
  34. package/dist/cli-delegate/cmd_upgrade.js +2 -2
  35. package/dist/cli-delegate/cmd_validate.js +2 -2
  36. package/dist/cli-delegate/dist-25AZAHOP.js +3240 -0
  37. package/dist/cli-delegate/dist-P7MFPMF3.js +411 -0
  38. package/dist/cli-delegate/dist-XAS5ZLQB.js +6 -0
  39. package/dist/cli-delegate/{upgrade_unattended-VVL6JLSX.js → upgrade_unattended-QCWOKTUN.js} +178 -163
  40. package/dist/discovery/deprecation-report.md +1 -1
  41. package/dist/discovery/discovery-manifest.json +3 -3
  42. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  43. package/dist/discovery/discovery-manifest.summary.md +1 -1
  44. package/dist/discovery/dormancy-report.md +1 -1
  45. package/dist/discovery/orphan-report.md +1 -1
  46. package/dist/discovery/packs.json +2 -2
  47. package/dist/discovery/trust-report.md +1 -1
  48. package/dist/discovery/workspaces.json +2 -2
  49. package/dist/hooks/code_graph_refresh.chunk.cjs +443 -0
  50. package/dist/hooks/code_graph_refresh.chunk.cjs.sha256 +1 -0
  51. package/dist/hooks/dispatch.js +1001 -938
  52. package/dist/hooks/dispatch.sha256 +1 -1
  53. package/dist/hooks/egress_engine.chunk.cjs +57 -7
  54. package/dist/hooks/egress_engine.chunk.cjs.sha256 +1 -1
  55. package/dist/hooks/update_notice.chunk.cjs +914 -0
  56. package/dist/hooks/update_notice.chunk.cjs.sha256 +1 -0
  57. package/dist/install/install.mjs +6974 -6037
  58. package/dist/mcp/mcp-cloudflare-catalogue.json +1 -0
  59. package/dist/mcp/registry-manifest.json +3 -2
  60. package/dist/mcp/server.json +13 -4
  61. package/dist/mcp/server.mjs +71202 -63282
  62. package/dist/scripts/_lib/egress_rules.js +8 -2
  63. package/dist/scripts/condense.js +18 -15
  64. package/dist/server/routes/discovery.js +1 -1
  65. package/dist/server/routes/install.js +1 -1
  66. package/dist/server/routes/ping.js +1 -1
  67. package/dist/server/routes/schema.js +4 -4
  68. package/dist/server/routes/settings.js +2 -6
  69. package/dist/server/routes/wizard.js +6 -6
  70. package/dist/server/schemas/settings.js +42 -42
  71. package/dist/shared/settingsSurface.js +1 -1
  72. package/dist/shared/userMd/schema.js +1 -1
  73. package/dist/shared/zodJsonSchema.js +47 -0
  74. package/dist/ui/assets/index-B0bcIgi0.js +77 -0
  75. package/dist/ui/index.html +1 -1
  76. package/package.json +6 -3
  77. package/src/agent-src/templates/scripts/check_memory.ts +1 -1
  78. package/src/config/ci-local-parity.yml +11 -0
  79. package/src/config/continuity-surface.json +9 -2
  80. package/src/config/evaluator-budgets.json +9 -8
  81. package/src/config/gate-violation-baselines.json +3 -3
  82. package/src/config/gitignore-block.txt +6 -5
  83. package/src/config/hook-bundle-budget.json +46 -6
  84. package/src/config/pack-size-budget.json +12 -1
  85. package/src/config/packed-binary-manifest.json +18 -0
  86. package/src/config/sigstore/npm_keys.json +50 -0
  87. package/src/config/sigstore/trust-material.json +9 -0
  88. package/src/config/sigstore/trusted_root.json +126 -0
  89. package/src/config/skill-body-budget.json +7 -1
  90. package/src/scripts/_cli/cmd_settings_set.ts +3 -2
  91. package/src/scripts/_cli/cmd_update.ts +48 -20
  92. package/src/scripts/_cli/cmd_upgrade.ts +7 -1
  93. package/src/scripts/_cli/upgrade_unattended.ts +90 -76
  94. package/src/scripts/_dispatch.bash +4 -3
  95. package/src/scripts/_lib/confusables.ts +0 -19
  96. package/src/scripts/_lib/egress_normalize.ts +53 -23
  97. package/src/scripts/_lib/egress_rules.ts +8 -2
  98. package/src/scripts/_lib/egress_text.ts +5 -3
  99. package/src/scripts/_lib/egress_token.ts +7 -5
  100. package/src/scripts/_lib/gate_collector_experimental.ts +633 -0
  101. package/src/scripts/_lib/git_common_dir.ts +10 -0
  102. package/src/scripts/_lib/graph_feeder_record.ts +3 -2
  103. package/src/scripts/_lib/install_reach_checks.ts +32 -2
  104. package/src/scripts/_lib/lineage_backfill.ts +136 -0
  105. package/src/scripts/_lib/lineage_ledger.ts +211 -0
  106. package/src/scripts/_lib/lineage_map.ts +55 -0
  107. package/src/scripts/_lib/lineage_query.ts +140 -0
  108. package/src/scripts/_lib/lineage_seed.ts +116 -0
  109. package/src/scripts/_lib/lineage_watch.ts +249 -0
  110. package/src/scripts/_lib/link_crypto.ts +8 -4
  111. package/src/scripts/_lib/listing_manifests.ts +85 -0
  112. package/src/scripts/_lib/neighbour_census.ts +1 -0
  113. package/src/scripts/_lib/production_reach.ts +3 -3
  114. package/src/scripts/_lib/project_registry.ts +227 -0
  115. package/src/scripts/_lib/provenance_verify.ts +14 -242
  116. package/src/scripts/_lib/release_settings_surface.ts +2 -6
  117. package/src/scripts/_lib/sigstore_provenance.ts +177 -0
  118. package/src/scripts/_lib/source_id.ts +48 -0
  119. package/src/scripts/_lib/source_shape.ts +4 -0
  120. package/src/scripts/_lib/unattended_gates.ts +6 -1
  121. package/src/scripts/adoption_status.ts +231 -27
  122. package/src/scripts/advisories.ts +26 -10
  123. package/src/scripts/ai_council/council_egress.ts +5 -6
  124. package/src/scripts/bench_graph_feeder_latency.ts +10 -3
  125. package/src/scripts/build_mcp_registry_manifest.ts +23 -12
  126. package/src/scripts/check_dependency_floors.ts +14 -0
  127. package/src/scripts/check_egress_mutations.ts +1 -0
  128. package/src/scripts/check_release_manifest_content.ts +17 -2
  129. package/src/scripts/check_release_pr_shape.ts +5 -0
  130. package/src/scripts/code_graph/build.ts +145 -45
  131. package/src/scripts/code_graph/cli.ts +382 -35
  132. package/src/scripts/code_graph/conventions_default.ts +247 -0
  133. package/src/scripts/code_graph/detect.ts +5 -5
  134. package/src/scripts/code_graph/estate_cli.ts +135 -0
  135. package/src/scripts/code_graph/estate_index.ts +394 -0
  136. package/src/scripts/code_graph/estate_manifests.ts +223 -0
  137. package/src/scripts/code_graph/estate_map.ts +141 -0
  138. package/src/scripts/code_graph/extract.ts +8 -2
  139. package/src/scripts/code_graph/extract_more.ts +241 -2
  140. package/src/scripts/code_graph/graph_budget.ts +158 -0
  141. package/src/scripts/code_graph/graph_home.ts +80 -0
  142. package/src/scripts/code_graph/lineage_cli.ts +315 -0
  143. package/src/scripts/code_graph/loader.ts +10 -2
  144. package/src/scripts/code_graph/map.ts +195 -53
  145. package/src/scripts/code_graph/map_model.ts +113 -20
  146. package/src/scripts/code_graph/map_verify.ts +80 -5
  147. package/src/scripts/code_graph/map_viewer/viewer.css +18 -0
  148. package/src/scripts/code_graph/map_viewer/viewer.js +146 -15
  149. package/src/scripts/code_graph/passes.ts +173 -0
  150. package/src/scripts/code_graph/project_ref.ts +25 -0
  151. package/src/scripts/code_graph/projects_cli.ts +254 -0
  152. package/src/scripts/code_graph/providers/angular.ts +55 -0
  153. package/src/scripts/code_graph/providers/aspnetcore.ts +182 -0
  154. package/src/scripts/code_graph/providers/catalog.ts +299 -0
  155. package/src/scripts/code_graph/providers/django.ts +206 -0
  156. package/src/scripts/code_graph/providers/express.ts +25 -0
  157. package/src/scripts/code_graph/providers/fastapi.ts +49 -0
  158. package/src/scripts/code_graph/providers/flask.ts +99 -0
  159. package/src/scripts/code_graph/providers/go_http.ts +121 -0
  160. package/src/scripts/code_graph/providers/index.ts +103 -0
  161. package/src/scripts/code_graph/{laravel_overlay.ts → providers/laravel.ts} +113 -7
  162. package/src/scripts/code_graph/providers/nestjs.ts +34 -0
  163. package/src/scripts/code_graph/providers/nextjs.ts +91 -0
  164. package/src/scripts/code_graph/providers/nuxt.ts +55 -0
  165. package/src/scripts/code_graph/providers/rails.ts +204 -0
  166. package/src/scripts/code_graph/providers/react-router.ts +58 -0
  167. package/src/scripts/code_graph/providers/shared/detect.ts +78 -0
  168. package/src/scripts/code_graph/providers/shared/extractors.ts +249 -0
  169. package/src/scripts/code_graph/providers/shared/js.ts +252 -0
  170. package/src/scripts/code_graph/providers/shared/kit.ts +210 -0
  171. package/src/scripts/code_graph/providers/shared/php.ts +64 -0
  172. package/src/scripts/code_graph/providers/shared/python.ts +223 -0
  173. package/src/scripts/code_graph/providers/spring.ts +175 -0
  174. package/src/scripts/code_graph/providers/sqlalchemy.ts +62 -0
  175. package/src/scripts/code_graph/providers/sveltekit.ts +59 -0
  176. package/src/scripts/code_graph/providers/symfony.ts +142 -0
  177. package/src/scripts/code_graph/providers/typeorm.ts +58 -0
  178. package/src/scripts/code_graph/providers/types.ts +56 -0
  179. package/src/scripts/code_graph/providers/vue-router.ts +21 -0
  180. package/src/scripts/code_graph/providers/wordpress.ts +128 -0
  181. package/src/scripts/code_graph/query.ts +9 -9
  182. package/src/scripts/code_graph/refresh_cli.ts +81 -0
  183. package/src/scripts/code_graph/refresh_lock.ts +197 -0
  184. package/src/scripts/code_graph/repo_identity.ts +169 -0
  185. package/src/scripts/code_graph/resolution_tiers.ts +19 -7
  186. package/src/scripts/code_graph/scip_reader.ts +2 -2
  187. package/src/scripts/code_graph/sqlite_store.ts +31 -28
  188. package/src/scripts/code_graph/types.ts +79 -41
  189. package/src/scripts/code_graph/validate.ts +6 -8
  190. package/src/scripts/condense.ts +15 -15
  191. package/src/scripts/generate_settings_reference.ts +2 -6
  192. package/src/scripts/hook_chunks.mjs +11 -0
  193. package/src/scripts/hook_manifest.json +1 -1
  194. package/src/scripts/hook_manifest.yaml +43 -23
  195. package/src/scripts/hooks/code_graph_refresh_hook.ts +163 -0
  196. package/src/scripts/hooks/dispatch_hook.ts +6 -7
  197. package/src/scripts/hooks/host_semantics.ts +72 -0
  198. package/src/scripts/hooks/lazy_concern.ts +8 -0
  199. package/src/scripts/hooks/user_notice.ts +105 -0
  200. package/src/scripts/install-hooks.sh +54 -33
  201. package/src/scripts/install.ts +2 -6
  202. package/src/scripts/lint_harvest_provenance.ts +137 -11
  203. package/src/scripts/lint_hook_manifest.ts +2 -0
  204. package/src/scripts/lint_marketplace.ts +33 -0
  205. package/src/scripts/lint_mcp_registry_manifest.ts +36 -0
  206. package/src/scripts/mcp_server/consumer_tool_catalog.json +70 -15
  207. package/src/scripts/mcp_server/graph_tools.ts +165 -41
  208. package/src/scripts/network_sources.ts +24 -6
  209. package/src/scripts/release.ts +2 -5
  210. package/src/scripts/release_drill.ts +4 -1
  211. package/src/scripts/release_env.ts +17 -0
  212. package/src/scripts/release_mix_taxonomy.json +3 -0
  213. package/src/scripts/schemas/provenance-mechanism.schema.json +46 -0
  214. package/src/scripts/update_notice_hook.ts +75 -7
  215. package/src/server/schemas/settings.ts +42 -42
  216. package/src/shared/settingsSurface.ts +1 -1
  217. package/src/shared/userMd/schema.ts +1 -1
  218. package/src/shared/zodJsonSchema.ts +41 -0
  219. package/src/vendor/grammars/README.md +8 -6
  220. package/src/vendor/grammars/tree-sitter-c_sharp.wasm +0 -0
  221. package/src/vendor/grammars/tree-sitter-ruby.wasm +0 -0
  222. package/dist/cli-delegate/chunk-XIAPY44N.js +0 -4587
  223. package/dist/ui/assets/index-w70Ado09.js +0 -23
  224. /package/dist/cli-delegate/{chunk-FKYZOWVD.js → chunk-QBAFP67O.js} +0 -0
  225. /package/dist/cli-delegate/{chunk-BMGUGSY6.js → chunk-TQG6UMPC.js} +0 -0
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Verify a release's two attestations against pinned sigstore trust material,
3
+ * and bind both to the exact tarball bytes that will be installed.
4
+ *
5
+ * WHY THIS REPLACES THE CERTIFICATE-BOUND CHECK. `provenance_verify.ts` checked
6
+ * the DSSE signature against the certificate a bundle carries and that
7
+ * certificate's SAN — and nothing about who issued it, so anyone who could
8
+ * serve the registry answer could mint a self-signed certificate with the
9
+ * expected SAN and pass. Pinned roots close that; a root fetched at run time
10
+ * would let the same attacker choose what the bundle is checked against.
11
+ *
12
+ * WHAT IS VERIFIED, both attestations required:
13
+ * - the SLSA provenance bundle: Fulcio chain to the pinned root, the signing
14
+ * certificate's validity at the time the transparency log recorded it, a
15
+ * Rekor entry and an SCT checked against the pinned logs, the DSSE signature,
16
+ * and an identity policy — issuer `token.actions.githubusercontent.com` and a
17
+ * SAN that is exactly this repository's `publish-npm.yml` at the tag of the
18
+ * version being installed;
19
+ * - the npm publish attestation: its signature against npm's pinned
20
+ * `npm:attestations` keys, and its Rekor entry — a second trust domain,
21
+ * verified separately;
22
+ * - in both payloads, exactly one subject naming this package at this version,
23
+ * whose sha512 equals the sha512 of the bytes about to be installed.
24
+ *
25
+ * WHAT IT NEVER DOES: fetch trust material (the files are read from the
26
+ * installed package and their digests checked against the recorded ones), fall
27
+ * back to the old check or to `npm audit signatures`, or pass anything it could
28
+ * not verify. Every failure is a reason string, because the caller prints it.
29
+ *
30
+ * The sigstore modules are imported lazily: this runs on the unattended upgrade
31
+ * path only, and nothing else should pay for loading them.
32
+ */
33
+ import { createHash } from 'node:crypto';
34
+ import * as fs from 'node:fs';
35
+ import * as path from 'node:path';
36
+
37
+ import { resolvePackageRoot } from './package_root.js';
38
+ import { EXPECTED_REPOSITORY, EXPECTED_WORKFLOW_REF, payload_names_publish } from './provenance_verify.js';
39
+
40
+ export const SLSA_PROVENANCE_V1 = 'https://slsa.dev/provenance/v1';
41
+ export const NPM_PUBLISH_V01 = 'https://github.com/npm/attestation/tree/main/specs/publish/v0.1';
42
+ export const GITHUB_OIDC_ISSUER = 'https://token.actions.githubusercontent.com';
43
+ export const PACKAGE_PURL_BASE = 'pkg:npm/%40event4u/agent-config';
44
+
45
+ /** Why verification refused, or `null` when everything held. */
46
+ export type ProvenanceVerdict = string | null;
47
+
48
+ function asRecord(v: unknown): Record<string, unknown> | null {
49
+ return v !== null && typeof v === 'object' && !Array.isArray(v) ? (v as Record<string, unknown>) : null;
50
+ }
51
+
52
+ function escapeRegExp(s: string): string {
53
+ return s.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
54
+ }
55
+
56
+ /**
57
+ * The only SAN accepted: this repository's publish workflow at the release tag
58
+ * of exactly the version being installed. Anchored at both ends — the library
59
+ * treats the policy as a pattern, and an unanchored one would accept any SAN
60
+ * that merely contains it.
61
+ */
62
+ export function expectedSan(version: string): RegExp {
63
+ return new RegExp(`^${escapeRegExp(`${EXPECTED_REPOSITORY}/${EXPECTED_WORKFLOW_REF}@refs/tags/${version}`)}$`);
64
+ }
65
+
66
+ export interface TrustFiles {
67
+ readonly trustedRoot: unknown;
68
+ readonly npmKeys: unknown;
69
+ }
70
+
71
+ /**
72
+ * Read the pinned trust material and refuse when a file's digest differs from
73
+ * the recorded one — a release can only be checked against the material it
74
+ * shipped with, as it shipped.
75
+ */
76
+ export function readTrustFiles(packageRoot: string = resolvePackageRoot(import.meta.url)): TrustFiles | string {
77
+ const dir = path.join(packageRoot, 'src', 'config', 'sigstore');
78
+ try {
79
+ const manifest = JSON.parse(fs.readFileSync(path.join(dir, 'trust-material.json'), 'utf-8')) as {
80
+ files?: Record<string, string>;
81
+ };
82
+ const read = (name: string): unknown => {
83
+ const bytes = fs.readFileSync(path.join(dir, name));
84
+ const want = manifest.files?.[name];
85
+ const got = createHash('sha256').update(bytes).digest('hex');
86
+ if (want !== got) throw new Error(`${name} has sha256 ${got}, the recorded one is ${String(want)}`);
87
+ return JSON.parse(bytes.toString('utf-8')) as unknown;
88
+ };
89
+ return { trustedRoot: read('trusted_root.json'), npmKeys: read('npm_keys.json') };
90
+ } catch (err) {
91
+ return `the pinned sigstore trust material is unusable: ${err instanceof Error ? err.message : String(err)}`;
92
+ }
93
+ }
94
+
95
+ /** Exactly one subject, naming this package at this version, with this sha512. */
96
+ function subjectMatches(payload: Record<string, unknown> | null, version: string, sha512Hex: string): string | null {
97
+ const subjects = payload?.['subject'];
98
+ if (!Array.isArray(subjects) || subjects.length !== 1) return 'it does not name exactly one subject';
99
+ const subject = asRecord(subjects[0]);
100
+ if (subject?.['name'] !== `${PACKAGE_PURL_BASE}@${version}`) return `its subject is ${String(subject?.['name'])}, not ${version}`;
101
+ const digest = asRecord(subject['digest'])?.['sha512'];
102
+ if (typeof digest !== 'string' || digest.toLowerCase() !== sha512Hex.toLowerCase()) {
103
+ return 'its subject digest does not match the tarball about to be installed';
104
+ }
105
+ return null;
106
+ }
107
+
108
+ export interface ReleaseToVerify {
109
+ readonly version: string;
110
+ /** sha512 of the exact tarball bytes that will be installed, lower-case hex. */
111
+ readonly tarballSha512Hex: string;
112
+ /** The registry's attestation document for this version. */
113
+ readonly attestations: unknown;
114
+ }
115
+
116
+ export async function verify_release_provenance(
117
+ release: ReleaseToVerify,
118
+ trust: TrustFiles | string = readTrustFiles(),
119
+ ): Promise<ProvenanceVerdict> {
120
+ if (typeof trust === 'string') return trust;
121
+ const list = asRecord(release.attestations)?.['attestations'];
122
+ if (!Array.isArray(list)) return 'the attestation document carries no attestations';
123
+ const byType = new Map<string, unknown>();
124
+ for (const entry of list) {
125
+ const type = asRecord(entry)?.['predicateType'];
126
+ if (typeof type === 'string' && !byType.has(type)) byType.set(type, asRecord(entry)?.['bundle']);
127
+ }
128
+ for (const type of [SLSA_PROVENANCE_V1, NPM_PUBLISH_V01]) {
129
+ if (!byType.has(type)) return `the release carries no ${type} attestation`;
130
+ }
131
+
132
+ const { Verifier, toSignedEntity, toTrustMaterial } = await import('@sigstore/verify');
133
+ const { bundleFromJSON } = await import('@sigstore/bundle');
134
+ const { PublicKey, TrustedRoot } = await import('@sigstore/protobuf-specs');
135
+
136
+ let material: ReturnType<typeof toTrustMaterial>;
137
+ try {
138
+ const keys: Record<string, ReturnType<typeof PublicKey.fromJSON>> = {};
139
+ const listed = asRecord(trust.npmKeys)?.['keys'];
140
+ for (const k of Array.isArray(listed) ? listed : []) {
141
+ const key = asRecord(k);
142
+ if (key?.['keyUsage'] === 'npm:attestations' && typeof key['keyId'] === 'string') {
143
+ keys[key['keyId']] = PublicKey.fromJSON(key['publicKey']);
144
+ }
145
+ }
146
+ material = toTrustMaterial(TrustedRoot.fromJSON(trust.trustedRoot), keys);
147
+ } catch (err) {
148
+ return `the pinned sigstore trust material could not be loaded: ${err instanceof Error ? err.message : String(err)}`;
149
+ }
150
+
151
+ for (const type of [SLSA_PROVENANCE_V1, NPM_PUBLISH_V01]) {
152
+ let payload: Record<string, unknown> | null;
153
+ try {
154
+ const bundle = bundleFromJSON(byType.get(type));
155
+ const verifier = new Verifier(material, { tlogThreshold: 1, ctlogThreshold: 1, timestampThreshold: 1 });
156
+ const policy =
157
+ type === SLSA_PROVENANCE_V1
158
+ ? { subjectAlternativeName: expectedSan(release.version), extensions: { issuer: GITHUB_OIDC_ISSUER } }
159
+ : undefined;
160
+ verifier.verify(toSignedEntity(bundle), policy);
161
+ if (bundle.content.$case !== 'dsseEnvelope') return `the ${type} attestation is not a DSSE envelope`;
162
+ payload = asRecord(JSON.parse(bundle.content.dsseEnvelope.payload.toString('utf-8')));
163
+ } catch (err) {
164
+ const code = asRecord(err)?.['code'];
165
+ return `the ${type} attestation does not verify${typeof code === 'string' ? ` (${code})` : ''}: ${
166
+ err instanceof Error ? err.message : String(err)
167
+ }`;
168
+ }
169
+ if (payload?.['predicateType'] !== type) return `the ${type} attestation's statement names another predicate type`;
170
+ const subject = subjectMatches(payload, release.version, release.tarballSha512Hex);
171
+ if (subject !== null) return `the ${type} attestation is not for this tarball: ${subject}`;
172
+ if (type === SLSA_PROVENANCE_V1 && !payload_names_publish(payload)) {
173
+ return `the signed provenance does not name ${EXPECTED_WORKFLOW_REF} in ${EXPECTED_REPOSITORY}`;
174
+ }
175
+ }
176
+ return null;
177
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Stable, keyed source ids — `src_` plus 10 hex of an HMAC over the canonical identity.
3
+ *
4
+ * `ENC1` tokens are salted per call, so two tokens for one source never compare
5
+ * equal; a round-scoped `S1…` code is renumbered every round. Neither can key a
6
+ * ledger. This id can: it is stable across rounds and URL spellings, and keyed
7
+ * with `secrets.link_encryption_key`, so nobody without the key can confirm a
8
+ * guessed source by hashing its URL. Without a key there is no id — callers
9
+ * refuse rather than mint an unkeyed one.
10
+ */
11
+
12
+ import * as crypto from 'node:crypto';
13
+
14
+ import { canonicalize } from '../harvest_reference_tokens.js';
15
+
16
+ export const SOURCE_ID_PREFIX = 'src_';
17
+ export const SOURCE_ID_RE = /^src_[0-9a-f]{10}$/;
18
+ // Domain-separates this HMAC from any other use of the same key.
19
+ const ID_CONTEXT = 'agent-config/source-id/v1\0';
20
+
21
+ /**
22
+ * Repository URLs collapse through `canonicalize`; anything else (an article,
23
+ * a video, a paper) keeps host, path and query with scheme, credentials,
24
+ * fragment and a trailing slash dropped. A string that is not a URL is used
25
+ * trimmed, as written — an opaque reference names itself.
26
+ */
27
+ export function canonicalSourceIdentity(raw: string): string {
28
+ const repo = canonicalize(raw);
29
+ if (repo !== null) return repo;
30
+ const s = raw.trim();
31
+ if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(s)) return s;
32
+ let u: URL;
33
+ try {
34
+ u = new URL(s);
35
+ } catch {
36
+ return s;
37
+ }
38
+ const host = u.hostname.toLowerCase().replace(/^www\./, '');
39
+ const port = u.port === '' ? '' : `:${u.port}`;
40
+ const pathname = u.pathname.replace(/\/+$/, '');
41
+ return `${host}${port}${pathname}${u.search}`;
42
+ }
43
+
44
+ export function sourceId(identity: string, key: string): string {
45
+ if (!key) throw new Error('no link_encryption_key — a source id is never minted without the key');
46
+ const mac = crypto.createHmac('sha256', key).update(ID_CONTEXT + canonicalSourceIdentity(identity)).digest('hex');
47
+ return SOURCE_ID_PREFIX + mac.slice(0, 10);
48
+ }
@@ -155,6 +155,10 @@ const ALLOWED_OWNERS: readonly string[] = [
155
155
  // looked; this is the answer. Fixing them here rather than by widening the
156
156
  // dedup rule is what keeps that rule strict.
157
157
  'someone', 'o',
158
+ // `someone-else/<repo>` is the wrong-repository fixture of the provenance
159
+ // tests; a committed council prompt quoting those tests verbatim was the
160
+ // measured false positive (2026-10-24). Same shape as the two above.
161
+ 'someone-else',
158
162
  // Vendors whose tools this suite integrates or recommends (rule carve-out).
159
163
  'anthropics', 'anthropic', 'microsoft', 'github', 'modelcontextprotocol',
160
164
  'cloudflare', 'openai', 'nodejs', 'vitest-dev', 'vercel', 'tailwindlabs',
@@ -30,9 +30,12 @@ export interface RegistryCandidate {
30
30
  * workflow produced the document. Tying it to this package's publish
31
31
  * workflow needs the bundle itself, which the packument only points at —
32
32
  * so the pointer travels with the candidate and the caller fetches it
33
- * (`provenance_verify.provenance_matches_publish`).
33
+ * (`sigstore_provenance.verify_release_provenance`).
34
34
  */
35
35
  readonly attestationsUrl: string | null;
36
+ /** Where npm serves this version's tarball, and the integrity it records for it. */
37
+ readonly tarballUrl: string | null;
38
+ readonly integrity: string | null;
36
39
  }
37
40
 
38
41
  function asRecord(v: unknown): Record<string, unknown> | null {
@@ -58,6 +61,8 @@ export function read_candidate(packument: unknown): RegistryCandidate | null {
58
61
  provenance: typeof predicate === 'string' && predicate.startsWith('https://slsa.dev/provenance/'),
59
62
  impactMap: asRecord(manifest['agentConfig'])?.['impact'] ?? null,
60
63
  attestationsUrl: typeof attestations?.['url'] === 'string' ? (attestations['url'] as string) : null,
64
+ tarballUrl: typeof asRecord(manifest['dist'])?.['tarball'] === 'string' ? (asRecord(manifest['dist'])?.['tarball'] as string) : null,
65
+ integrity: typeof asRecord(manifest['dist'])?.['integrity'] === 'string' ? (asRecord(manifest['dist'])?.['integrity'] as string) : null,
61
66
  };
62
67
  }
63
68
 
@@ -22,7 +22,7 @@
22
22
  *
23
23
  * CLI:
24
24
  *
25
- * ./scripts-run src/scripts/adoption_status [--json] [--branch main] [--check-upstream]
25
+ * ./scripts-run src/scripts/adoption_status [--json] [--branch main] [--check-upstream] [--fail-on-action]
26
26
  *
27
27
  * `--check-upstream` reads each ledger row's upstream (`gh pr view` for a pull
28
28
  * request, one `curl` for a listing page or registry query) and appends what it
@@ -30,10 +30,19 @@
30
30
  * report exactly as before, and stderr names that mode plus every row whose
31
31
  * `Last read` date is missing or older than the sheet's re-read threshold.
32
32
  *
33
+ * With `--check-upstream`, a row whose sheet and upstream disagree in a way that
34
+ * needs a person also carries an action (`sheet-behind`, `needs-response`,
35
+ * `listing-lost`, `listing-stale`) next to its unchanged verdict, and the report
36
+ * names the next act. A submission is done when its entry is live, not when it
37
+ * was sent, so `--fail-on-action` turns any action into a non-zero exit for the
38
+ * daily watcher. Without `--check-upstream` there is nothing to judge and the
39
+ * flag is reported on stderr and ignored.
40
+ *
33
41
  * Exit codes:
34
42
  *
35
43
  * 0 — printed successfully (status itself does not gate the exit).
36
44
  * 1 — IO error reading the registry-submissions sheet.
45
+ * 2 — `--check-upstream --fail-on-action` and at least one row needs an action.
37
46
  */
38
47
 
39
48
  import { spawnSync } from 'node:child_process';
@@ -243,25 +252,29 @@ interface Args {
243
252
  json: boolean;
244
253
  branch: string;
245
254
  checkUpstream: boolean;
255
+ failOnAction: boolean;
246
256
  }
247
257
 
248
258
  export function parse_args(argv: string[]): Args {
249
259
  let json = false;
250
260
  let branch = 'main';
251
261
  let checkUpstream = false;
262
+ let failOnAction = false;
252
263
  for (let i = 0; i < argv.length; i++) {
253
264
  const a = argv[i];
254
265
  if (a === '--json') {
255
266
  json = true;
256
267
  } else if (a === '--check-upstream') {
257
268
  checkUpstream = true;
269
+ } else if (a === '--fail-on-action') {
270
+ failOnAction = true;
258
271
  } else if (a === '--branch') {
259
272
  branch = argv[++i] ?? '';
260
273
  } else if (a !== undefined && a.startsWith('--branch=')) {
261
274
  branch = a.slice('--branch='.length);
262
275
  }
263
276
  }
264
- return { json, branch, checkUpstream };
277
+ return { json, branch, checkUpstream, failOnAction };
265
278
  }
266
279
 
267
280
  export function main(argv: string[] = process.argv.slice(2)): number {
@@ -293,10 +306,21 @@ export function main(argv: string[] = process.argv.slice(2)): number {
293
306
  for (const u of unread) {
294
307
  process.stderr.write(` row ${u.id} ${u.registry}: ${u.label}\n`);
295
308
  }
309
+ if (args.failOnAction) {
310
+ process.stderr.write(
311
+ '--fail-on-action ignored: actions are judged only with --check-upstream\n',
312
+ );
313
+ }
296
314
  return 0;
297
315
  }
298
316
 
299
- const verdicts = judge_upstream(rows, live_upstream_reader, today);
317
+ const verdicts = judge_upstream(
318
+ rows,
319
+ live_upstream_reader,
320
+ today,
321
+ _releasedServer(),
322
+ parse_reread_threshold(registryText),
323
+ );
300
324
  if (args.json) {
301
325
  const base = JSON.parse(render_json(registryCounts, reports, ci, args.branch)) as Record<
302
326
  string,
@@ -308,7 +332,32 @@ export function main(argv: string[] = process.argv.slice(2)): number {
308
332
  process.stdout.write(render_text(registryCounts, reports, ci, args.branch) + '\n');
309
333
  process.stdout.write(render_upstream_text(verdicts, unread) + '\n');
310
334
  }
311
- return 0;
335
+ const code = upstream_exit_code(verdicts, args.failOnAction);
336
+ if (code !== 0) {
337
+ // On a runner the `::error::` prefix turns each line into an annotation
338
+ // naming the row, so the red job says what to do without opening the log.
339
+ const prefix = process.env.GITHUB_ACTIONS === 'true' ? '::error title=adoption_status::' : 'action: ';
340
+ for (const line of action_lines(verdicts)) {
341
+ process.stderr.write(prefix + line + '\n');
342
+ }
343
+ }
344
+ return code;
345
+ }
346
+
347
+ /** `name` and `version` the registry should carry, from package.json. */
348
+ function _releasedServer(): ReleasedServer | undefined {
349
+ try {
350
+ const pkg = JSON.parse(fs.readFileSync(path.join(REPO_ROOT, 'package.json'), 'utf-8')) as {
351
+ mcpName?: unknown;
352
+ version?: unknown;
353
+ };
354
+ if (typeof pkg.mcpName === 'string' && typeof pkg.version === 'string') {
355
+ return { name: pkg.mcpName, version: pkg.version };
356
+ }
357
+ } catch {
358
+ // Without a readable package.json there is nothing to compare against.
359
+ }
360
+ return undefined;
312
361
  }
313
362
 
314
363
  // --- upstream reading --------------------------------------------------------
@@ -325,7 +374,15 @@ export interface LedgerRow {
325
374
  }
326
375
 
327
376
  export type UpstreamReading =
328
- | { ok: true; state: 'open' | 'closed' | 'merged' | 'listed' | 'not-listed'; at?: string }
377
+ | {
378
+ ok: true;
379
+ state: 'open' | 'closed' | 'merged' | 'listed' | 'not-listed';
380
+ at?: string;
381
+ /** `gh pr view` reviewDecision, e.g. `CHANGES_REQUESTED`; pull requests only. */
382
+ reviewDecision?: string;
383
+ /** Server name → the version the registry marks latest; registry queries only. */
384
+ latestVersions?: Record<string, string>;
385
+ }
329
386
  | { ok: false; reason: string };
330
387
 
331
388
  export type UpstreamReader = (row: LedgerRow) => UpstreamReading;
@@ -340,11 +397,25 @@ export type UpstreamVerdict =
340
397
  | 'unreachable'
341
398
  | 'no-upstream';
342
399
 
400
+ /**
401
+ * Reported next to the verdict, never instead of it: the verdict says what the
402
+ * upstream reads, the action says the sheet and that reading disagree in a way a
403
+ * person has to resolve.
404
+ */
405
+ export type ActionVerdict = 'sheet-behind' | 'needs-response' | 'listing-lost' | 'listing-stale' | 'read-overdue';
406
+
343
407
  export interface RowVerdict {
344
408
  id: string;
345
409
  registry: string;
346
410
  verdict: UpstreamVerdict;
347
411
  detail: string;
412
+ action?: ActionVerdict;
413
+ next?: string;
414
+ }
415
+
416
+ export interface ReleasedServer {
417
+ name: string;
418
+ version: string;
348
419
  }
349
420
 
350
421
  export interface UnreadFinding {
@@ -451,6 +522,8 @@ export function judge_upstream(
451
522
  rows: LedgerRow[],
452
523
  reader: UpstreamReader,
453
524
  today: string,
525
+ released?: ReleasedServer,
526
+ thresholdDays: number = DEFAULT_REREAD_THRESHOLD_DAYS,
454
527
  ): RowVerdict[] {
455
528
  return rows.map((row): RowVerdict => {
456
529
  const base = { id: row.id, registry: row.registry };
@@ -459,27 +532,116 @@ export function judge_upstream(
459
532
  }
460
533
  const r = reader(row);
461
534
  if (!r.ok) {
462
- return { ...base, verdict: 'unreachable', detail: r.reason };
463
- }
464
- switch (r.state) {
465
- case 'closed':
466
- return { ...base, verdict: 'answered-closed', detail: `closed ${r.at ?? 'date unknown'}` };
467
- case 'merged':
468
- return { ...base, verdict: 'answered-merged', detail: `merged ${r.at ?? 'date unknown'}` };
469
- case 'listed':
470
- case 'not-listed':
471
- return { ...base, verdict: r.state, detail: row.ref };
472
- case 'open': {
473
- const age = row.date === '' ? null : _daysBetween(row.date, today);
474
- if (row.status === 'submitted' && age !== null && age >= STALLED_AFTER_DAYS) {
475
- return { ...base, verdict: 'stalled', detail: `open ${age} days, no answer` };
476
- }
477
- return { ...base, verdict: 'open', detail: age === null ? 'open' : `open ${age} days` };
535
+ const out = { ...base, verdict: 'unreachable' as const, detail: r.reason };
536
+ // One outage never reds the watcher; a listing nobody has been able to read
537
+ // for longer than the sheet's own threshold does, or it could vanish unseen.
538
+ const age = row.lastRead === '' ? null : _daysBetween(row.lastRead, today);
539
+ const live = row.status === 'accepted' || row.status === 'submitted';
540
+ if (live && (age === null || age > thresholdDays)) {
541
+ return {
542
+ ...out,
543
+ action: 'read-overdue',
544
+ next: `read the upstream another way (a headless browser) and set Last read; unread ${age === null ? 'ever' : `for ${age} days`}`,
545
+ };
478
546
  }
547
+ return out;
479
548
  }
549
+ const out = { ...base, ..._verdictOf(row, r, today) };
550
+ const action = _actionFor(row, r, released);
551
+ // Spread only when present: the JSON writer prints an undefined value as `null`.
552
+ return action === null ? out : { ...out, ...action };
480
553
  });
481
554
  }
482
555
 
556
+ function _verdictOf(
557
+ row: LedgerRow,
558
+ r: Extract<UpstreamReading, { ok: true }>,
559
+ today: string,
560
+ ): { verdict: UpstreamVerdict; detail: string } {
561
+ switch (r.state) {
562
+ case 'closed':
563
+ return { verdict: 'answered-closed', detail: `closed ${r.at ?? 'date unknown'}` };
564
+ case 'merged':
565
+ return { verdict: 'answered-merged', detail: `merged ${r.at ?? 'date unknown'}` };
566
+ case 'listed':
567
+ case 'not-listed':
568
+ return { verdict: r.state, detail: row.ref };
569
+ case 'open': {
570
+ const age = row.date === '' ? null : _daysBetween(row.date, today);
571
+ if (row.status === 'submitted' && age !== null && age >= STALLED_AFTER_DAYS) {
572
+ return { verdict: 'stalled', detail: `open ${age} days, no answer` };
573
+ }
574
+ return { verdict: 'open', detail: age === null ? 'open' : `open ${age} days` };
575
+ }
576
+ }
577
+ }
578
+
579
+ /**
580
+ * The cases where the sheet and the live upstream disagree in a way that needs a
581
+ * person. `rejected` and `stalled` rows are left alone: the sheet already says
582
+ * the attempt ended, and an unreadable upstream is never an action, so a network
583
+ * outage cannot red the watcher.
584
+ */
585
+ function _actionFor(
586
+ row: LedgerRow,
587
+ r: Extract<UpstreamReading, { ok: true }>,
588
+ released: ReleasedServer | undefined,
589
+ ): { action: ActionVerdict; next: string } | null {
590
+ const awaiting = row.status === 'submitted' || row.status === 'pending';
591
+ if (awaiting && (r.state === 'closed' || r.state === 'merged')) {
592
+ return {
593
+ action: 'sheet-behind',
594
+ next: `update the ledger row: the upstream ${r.state} ${r.at ?? 'on an unknown date'} while the sheet says ${row.status}`,
595
+ };
596
+ }
597
+ // Row 4's path after its first CI publish: the entry went live, the sheet still waits.
598
+ if (awaiting && r.state === 'listed') {
599
+ return {
600
+ action: 'sheet-behind',
601
+ next: `update the ledger row: the listing reads listed while the sheet says ${row.status}`,
602
+ };
603
+ }
604
+ if (r.state === 'open' && r.reviewDecision === 'CHANGES_REQUESTED') {
605
+ return { action: 'needs-response', next: `answer the review on ${row.ref}` };
606
+ }
607
+ if (row.status === 'accepted' && r.state === 'not-listed') {
608
+ return { action: 'listing-lost', next: `re-check or re-submit the listing at ${row.ref}` };
609
+ }
610
+ if (
611
+ row.status === 'accepted' &&
612
+ row.kind === 'registry-query' &&
613
+ r.state === 'listed' &&
614
+ released !== undefined &&
615
+ r.latestVersions !== undefined
616
+ ) {
617
+ const live = r.latestVersions[released.name];
618
+ if (live === undefined) {
619
+ return {
620
+ action: 'listing-lost',
621
+ next: `re-check or re-submit the listing: the registry answer carries no ${released.name}`,
622
+ };
623
+ }
624
+ if (live !== released.version) {
625
+ return {
626
+ action: 'listing-stale',
627
+ next: `publish ${released.version} to the registry: it carries ${live} for ${released.name}`,
628
+ };
629
+ }
630
+ }
631
+ return null;
632
+ }
633
+
634
+ /** The rows `--fail-on-action` reds on, one line each: row, action, next act. */
635
+ export function action_lines(verdicts: RowVerdict[]): string[] {
636
+ return verdicts
637
+ .filter((v) => v.action !== undefined)
638
+ .map((v) => `row ${v.id} ${v.action} (${v.registry}) — ${v.next ?? ''}`);
639
+ }
640
+
641
+ export function upstream_exit_code(verdicts: RowVerdict[], failOnAction: boolean): number {
642
+ return failOnAction && verdicts.some((v) => v.action !== undefined) ? 2 : 0;
643
+ }
644
+
483
645
  export function upstream_mode_note(checked: boolean): string {
484
646
  return checked
485
647
  ? 'upstream: read live (--check-upstream)'
@@ -508,6 +670,14 @@ export function render_upstream_text(verdicts: RowVerdict[], unread: UnreadFindi
508
670
  for (const u of unread) {
509
671
  lines.push(` row ${u.id.padEnd(3)} ${u.label} ${u.registry}`);
510
672
  }
673
+ const actions = action_lines(verdicts);
674
+ if (actions.length > 0) {
675
+ lines.push('');
676
+ lines.push('Actions needed (a submission is done only once it is live):');
677
+ for (const a of actions) {
678
+ lines.push(` ${a}`);
679
+ }
680
+ }
511
681
  return lines.join('\n');
512
682
  }
513
683
 
@@ -521,14 +691,14 @@ function _readPullRequest(ref: string): UpstreamReading {
521
691
  }
522
692
  const proc = spawnSync(
523
693
  'gh',
524
- ['pr', 'view', m[2] as string, '--repo', m[1] as string, '--json', 'state,closedAt,mergedAt'],
694
+ ['pr', 'view', m[2] as string, '--repo', m[1] as string, '--json', 'state,closedAt,mergedAt,reviewDecision'],
525
695
  { encoding: 'utf8', timeout: 20_000 },
526
696
  );
527
697
  if (proc.status !== 0) {
528
698
  const snip = sanitize_text(_pyStrip(proc.stderr ?? '')).slice(0, 80);
529
699
  return { ok: false, reason: `gh exit=${proc.status}: ${snip}` };
530
700
  }
531
- let d: { state?: string; closedAt?: string; mergedAt?: string };
701
+ let d: { state?: string; closedAt?: string; mergedAt?: string; reviewDecision?: string };
532
702
  try {
533
703
  d = JSON.parse(proc.stdout) as typeof d;
534
704
  } catch {
@@ -536,7 +706,10 @@ function _readPullRequest(ref: string): UpstreamReading {
536
706
  }
537
707
  if (d.state === 'MERGED') return { ok: true, state: 'merged', at: sanitize_text(d.mergedAt ?? '').slice(0, 10) };
538
708
  if (d.state === 'CLOSED') return { ok: true, state: 'closed', at: sanitize_text(d.closedAt ?? '').slice(0, 10) };
539
- if (d.state === 'OPEN') return { ok: true, state: 'open' };
709
+ if (d.state === 'OPEN') {
710
+ const review = sanitize_text(d.reviewDecision ?? '').slice(0, 40);
711
+ return review === '' ? { ok: true, state: 'open' } : { ok: true, state: 'open', reviewDecision: review };
712
+ }
540
713
  return { ok: false, reason: `unknown pull-request state ${sanitize_text(String(d.state)).slice(0, 40)}` };
541
714
  }
542
715
 
@@ -574,9 +747,21 @@ export function read_url(
574
747
  if (code === '404') return { ok: true, state: 'not-listed' };
575
748
  return { ok: false, reason: `HTTP ${code} from ${ref}` };
576
749
  }
577
- let body: { metadata?: { count?: unknown } };
750
+ return read_registry_answer(out.slice(0, cut), ref);
751
+ }
752
+
753
+ const _REGISTRY_META_KEY = 'io.modelcontextprotocol.registry/official';
754
+
755
+ /**
756
+ * Shape read live from `GET /v0/servers?search=` on 2026-10-24:
757
+ * `{servers: [{server: {name, version}, _meta: {"<key>": {isLatest}}}], metadata: {count}}`.
758
+ * A search returns every published version, so only the one flagged `isLatest`
759
+ * names what a client installs.
760
+ */
761
+ export function read_registry_answer(text: string, ref: string): UpstreamReading {
762
+ let body: { metadata?: { count?: unknown }; servers?: unknown };
578
763
  try {
579
- body = JSON.parse(out.slice(0, cut)) as typeof body;
764
+ body = JSON.parse(text) as typeof body;
580
765
  } catch {
581
766
  return { ok: false, reason: `registry answer from ${ref} not parseable as JSON` };
582
767
  }
@@ -584,7 +769,26 @@ export function read_url(
584
769
  if (typeof n !== 'number') {
585
770
  return { ok: false, reason: 'registry answer has no metadata.count' };
586
771
  }
587
- return { ok: true, state: n > 0 ? 'listed' : 'not-listed' };
772
+ if (n === 0) {
773
+ return { ok: true, state: 'not-listed' };
774
+ }
775
+ const latestVersions: Record<string, string> = {};
776
+ let flagged = false;
777
+ for (const entry of Array.isArray(body.servers) ? body.servers : []) {
778
+ const e = entry as {
779
+ server?: { name?: unknown; version?: unknown };
780
+ _meta?: Record<string, { isLatest?: unknown } | undefined>;
781
+ };
782
+ const name = e.server?.name;
783
+ const version = e.server?.version;
784
+ if (typeof name !== 'string' || typeof version !== 'string') continue;
785
+ if (e._meta?.[_REGISTRY_META_KEY]?.isLatest !== true) continue;
786
+ flagged = true;
787
+ latestVersions[sanitize_text(name).slice(0, 200)] = sanitize_text(version).slice(0, 40);
788
+ }
789
+ // No `isLatest` flag anywhere means the answer is not the shape read above;
790
+ // comparing against an empty map would report a live listing as lost.
791
+ return flagged ? { ok: true, state: 'listed', latestVersions } : { ok: true, state: 'listed' };
588
792
  }
589
793
 
590
794
  /** Reads the real upstream: `gh` for a pull request, `curl` otherwise. */