@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.
- package/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/skills/install-agent-config/SKILL.md +9 -9
- package/CHANGELOG.md +167 -193
- package/CREDITS.md +2 -2
- package/README.md +1 -1
- package/dist/agent-src/skills/code-intelligence/SKILL.md +16 -12
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/scripts/check_memory.ts +1 -1
- package/dist/cli/commands/recordTriggerEval.js +1 -1
- package/dist/cli/registry.js +2 -2
- package/dist/cli-delegate/{chunk-KR5ZIFPO.js → chunk-5AYJGUA3.js} +1 -3
- package/dist/cli-delegate/chunk-DIFZBP4G.js +1336 -0
- package/dist/cli-delegate/chunk-E7ACTJTJ.js +6792 -0
- package/dist/cli-delegate/{chunk-VYZGJLCA.js → chunk-RKNT4LIK.js} +22 -1286
- package/dist/cli-delegate/{chunk-BZJ4Y3CI.js → chunk-WY5ZZPGL.js} +2 -2
- package/dist/cli-delegate/{chunk-CMZEKSNN.js → chunk-X6LZ7W5A.js} +73 -10
- package/dist/cli-delegate/cmd_conformance.js +190 -192
- package/dist/cli-delegate/cmd_doctor.js +5 -5
- package/dist/cli-delegate/cmd_doctor_neighbours.js +3 -3
- package/dist/cli-delegate/cmd_explain.js +1 -1
- package/dist/cli-delegate/cmd_export.js +2 -2
- package/dist/cli-delegate/cmd_handoff.js +2 -2
- package/dist/cli-delegate/cmd_memory_get.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +2 -2
- package/dist/cli-delegate/cmd_route_audit.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +2 -2
- package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
- package/dist/cli-delegate/cmd_settings_set.js +8 -4
- package/dist/cli-delegate/cmd_sync.js +2 -2
- package/dist/cli-delegate/cmd_uninstall.js +2 -2
- package/dist/cli-delegate/cmd_update.js +24 -13
- package/dist/cli-delegate/cmd_upgrade.js +2 -2
- package/dist/cli-delegate/cmd_validate.js +2 -2
- package/dist/cli-delegate/dist-25AZAHOP.js +3240 -0
- package/dist/cli-delegate/dist-P7MFPMF3.js +411 -0
- package/dist/cli-delegate/dist-XAS5ZLQB.js +6 -0
- package/dist/cli-delegate/{upgrade_unattended-VVL6JLSX.js → upgrade_unattended-QCWOKTUN.js} +178 -163
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +3 -3
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/code_graph_refresh.chunk.cjs +443 -0
- package/dist/hooks/code_graph_refresh.chunk.cjs.sha256 +1 -0
- package/dist/hooks/dispatch.js +1001 -938
- package/dist/hooks/dispatch.sha256 +1 -1
- package/dist/hooks/egress_engine.chunk.cjs +57 -7
- package/dist/hooks/egress_engine.chunk.cjs.sha256 +1 -1
- package/dist/hooks/update_notice.chunk.cjs +914 -0
- package/dist/hooks/update_notice.chunk.cjs.sha256 +1 -0
- package/dist/install/install.mjs +6974 -6037
- package/dist/mcp/mcp-cloudflare-catalogue.json +1 -0
- package/dist/mcp/registry-manifest.json +3 -2
- package/dist/mcp/server.json +13 -4
- package/dist/mcp/server.mjs +71202 -63282
- package/dist/scripts/_lib/egress_rules.js +8 -2
- package/dist/scripts/condense.js +18 -15
- package/dist/server/routes/discovery.js +1 -1
- package/dist/server/routes/install.js +1 -1
- package/dist/server/routes/ping.js +1 -1
- package/dist/server/routes/schema.js +4 -4
- package/dist/server/routes/settings.js +2 -6
- package/dist/server/routes/wizard.js +6 -6
- package/dist/server/schemas/settings.js +42 -42
- package/dist/shared/settingsSurface.js +1 -1
- package/dist/shared/userMd/schema.js +1 -1
- package/dist/shared/zodJsonSchema.js +47 -0
- package/dist/ui/assets/index-B0bcIgi0.js +77 -0
- package/dist/ui/index.html +1 -1
- package/package.json +6 -3
- package/src/agent-src/templates/scripts/check_memory.ts +1 -1
- package/src/config/ci-local-parity.yml +11 -0
- package/src/config/continuity-surface.json +9 -2
- package/src/config/evaluator-budgets.json +9 -8
- package/src/config/gate-violation-baselines.json +3 -3
- package/src/config/gitignore-block.txt +6 -5
- package/src/config/hook-bundle-budget.json +46 -6
- package/src/config/pack-size-budget.json +12 -1
- package/src/config/packed-binary-manifest.json +18 -0
- package/src/config/sigstore/npm_keys.json +50 -0
- package/src/config/sigstore/trust-material.json +9 -0
- package/src/config/sigstore/trusted_root.json +126 -0
- package/src/config/skill-body-budget.json +7 -1
- package/src/scripts/_cli/cmd_settings_set.ts +3 -2
- package/src/scripts/_cli/cmd_update.ts +48 -20
- package/src/scripts/_cli/cmd_upgrade.ts +7 -1
- package/src/scripts/_cli/upgrade_unattended.ts +90 -76
- package/src/scripts/_dispatch.bash +4 -3
- package/src/scripts/_lib/confusables.ts +0 -19
- package/src/scripts/_lib/egress_normalize.ts +53 -23
- package/src/scripts/_lib/egress_rules.ts +8 -2
- package/src/scripts/_lib/egress_text.ts +5 -3
- package/src/scripts/_lib/egress_token.ts +7 -5
- package/src/scripts/_lib/gate_collector_experimental.ts +633 -0
- package/src/scripts/_lib/git_common_dir.ts +10 -0
- package/src/scripts/_lib/graph_feeder_record.ts +3 -2
- package/src/scripts/_lib/install_reach_checks.ts +32 -2
- package/src/scripts/_lib/lineage_backfill.ts +136 -0
- package/src/scripts/_lib/lineage_ledger.ts +211 -0
- package/src/scripts/_lib/lineage_map.ts +55 -0
- package/src/scripts/_lib/lineage_query.ts +140 -0
- package/src/scripts/_lib/lineage_seed.ts +116 -0
- package/src/scripts/_lib/lineage_watch.ts +249 -0
- package/src/scripts/_lib/link_crypto.ts +8 -4
- package/src/scripts/_lib/listing_manifests.ts +85 -0
- package/src/scripts/_lib/neighbour_census.ts +1 -0
- package/src/scripts/_lib/production_reach.ts +3 -3
- package/src/scripts/_lib/project_registry.ts +227 -0
- package/src/scripts/_lib/provenance_verify.ts +14 -242
- package/src/scripts/_lib/release_settings_surface.ts +2 -6
- package/src/scripts/_lib/sigstore_provenance.ts +177 -0
- package/src/scripts/_lib/source_id.ts +48 -0
- package/src/scripts/_lib/source_shape.ts +4 -0
- package/src/scripts/_lib/unattended_gates.ts +6 -1
- package/src/scripts/adoption_status.ts +231 -27
- package/src/scripts/advisories.ts +26 -10
- package/src/scripts/ai_council/council_egress.ts +5 -6
- package/src/scripts/bench_graph_feeder_latency.ts +10 -3
- package/src/scripts/build_mcp_registry_manifest.ts +23 -12
- package/src/scripts/check_dependency_floors.ts +14 -0
- package/src/scripts/check_egress_mutations.ts +1 -0
- package/src/scripts/check_release_manifest_content.ts +17 -2
- package/src/scripts/check_release_pr_shape.ts +5 -0
- package/src/scripts/code_graph/build.ts +145 -45
- package/src/scripts/code_graph/cli.ts +382 -35
- package/src/scripts/code_graph/conventions_default.ts +247 -0
- package/src/scripts/code_graph/detect.ts +5 -5
- package/src/scripts/code_graph/estate_cli.ts +135 -0
- package/src/scripts/code_graph/estate_index.ts +394 -0
- package/src/scripts/code_graph/estate_manifests.ts +223 -0
- package/src/scripts/code_graph/estate_map.ts +141 -0
- package/src/scripts/code_graph/extract.ts +8 -2
- package/src/scripts/code_graph/extract_more.ts +241 -2
- package/src/scripts/code_graph/graph_budget.ts +158 -0
- package/src/scripts/code_graph/graph_home.ts +80 -0
- package/src/scripts/code_graph/lineage_cli.ts +315 -0
- package/src/scripts/code_graph/loader.ts +10 -2
- package/src/scripts/code_graph/map.ts +195 -53
- package/src/scripts/code_graph/map_model.ts +113 -20
- package/src/scripts/code_graph/map_verify.ts +80 -5
- package/src/scripts/code_graph/map_viewer/viewer.css +18 -0
- package/src/scripts/code_graph/map_viewer/viewer.js +146 -15
- package/src/scripts/code_graph/passes.ts +173 -0
- package/src/scripts/code_graph/project_ref.ts +25 -0
- package/src/scripts/code_graph/projects_cli.ts +254 -0
- package/src/scripts/code_graph/providers/angular.ts +55 -0
- package/src/scripts/code_graph/providers/aspnetcore.ts +182 -0
- package/src/scripts/code_graph/providers/catalog.ts +299 -0
- package/src/scripts/code_graph/providers/django.ts +206 -0
- package/src/scripts/code_graph/providers/express.ts +25 -0
- package/src/scripts/code_graph/providers/fastapi.ts +49 -0
- package/src/scripts/code_graph/providers/flask.ts +99 -0
- package/src/scripts/code_graph/providers/go_http.ts +121 -0
- package/src/scripts/code_graph/providers/index.ts +103 -0
- package/src/scripts/code_graph/{laravel_overlay.ts → providers/laravel.ts} +113 -7
- package/src/scripts/code_graph/providers/nestjs.ts +34 -0
- package/src/scripts/code_graph/providers/nextjs.ts +91 -0
- package/src/scripts/code_graph/providers/nuxt.ts +55 -0
- package/src/scripts/code_graph/providers/rails.ts +204 -0
- package/src/scripts/code_graph/providers/react-router.ts +58 -0
- package/src/scripts/code_graph/providers/shared/detect.ts +78 -0
- package/src/scripts/code_graph/providers/shared/extractors.ts +249 -0
- package/src/scripts/code_graph/providers/shared/js.ts +252 -0
- package/src/scripts/code_graph/providers/shared/kit.ts +210 -0
- package/src/scripts/code_graph/providers/shared/php.ts +64 -0
- package/src/scripts/code_graph/providers/shared/python.ts +223 -0
- package/src/scripts/code_graph/providers/spring.ts +175 -0
- package/src/scripts/code_graph/providers/sqlalchemy.ts +62 -0
- package/src/scripts/code_graph/providers/sveltekit.ts +59 -0
- package/src/scripts/code_graph/providers/symfony.ts +142 -0
- package/src/scripts/code_graph/providers/typeorm.ts +58 -0
- package/src/scripts/code_graph/providers/types.ts +56 -0
- package/src/scripts/code_graph/providers/vue-router.ts +21 -0
- package/src/scripts/code_graph/providers/wordpress.ts +128 -0
- package/src/scripts/code_graph/query.ts +9 -9
- package/src/scripts/code_graph/refresh_cli.ts +81 -0
- package/src/scripts/code_graph/refresh_lock.ts +197 -0
- package/src/scripts/code_graph/repo_identity.ts +169 -0
- package/src/scripts/code_graph/resolution_tiers.ts +19 -7
- package/src/scripts/code_graph/scip_reader.ts +2 -2
- package/src/scripts/code_graph/sqlite_store.ts +31 -28
- package/src/scripts/code_graph/types.ts +79 -41
- package/src/scripts/code_graph/validate.ts +6 -8
- package/src/scripts/condense.ts +15 -15
- package/src/scripts/generate_settings_reference.ts +2 -6
- package/src/scripts/hook_chunks.mjs +11 -0
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +43 -23
- package/src/scripts/hooks/code_graph_refresh_hook.ts +163 -0
- package/src/scripts/hooks/dispatch_hook.ts +6 -7
- package/src/scripts/hooks/host_semantics.ts +72 -0
- package/src/scripts/hooks/lazy_concern.ts +8 -0
- package/src/scripts/hooks/user_notice.ts +105 -0
- package/src/scripts/install-hooks.sh +54 -33
- package/src/scripts/install.ts +2 -6
- package/src/scripts/lint_harvest_provenance.ts +137 -11
- package/src/scripts/lint_hook_manifest.ts +2 -0
- package/src/scripts/lint_marketplace.ts +33 -0
- package/src/scripts/lint_mcp_registry_manifest.ts +36 -0
- package/src/scripts/mcp_server/consumer_tool_catalog.json +70 -15
- package/src/scripts/mcp_server/graph_tools.ts +165 -41
- package/src/scripts/network_sources.ts +24 -6
- package/src/scripts/release.ts +2 -5
- package/src/scripts/release_drill.ts +4 -1
- package/src/scripts/release_env.ts +17 -0
- package/src/scripts/release_mix_taxonomy.json +3 -0
- package/src/scripts/schemas/provenance-mechanism.schema.json +46 -0
- package/src/scripts/update_notice_hook.ts +75 -7
- package/src/server/schemas/settings.ts +42 -42
- package/src/shared/settingsSurface.ts +1 -1
- package/src/shared/userMd/schema.ts +1 -1
- package/src/shared/zodJsonSchema.ts +41 -0
- package/src/vendor/grammars/README.md +8 -6
- package/src/vendor/grammars/tree-sitter-c_sharp.wasm +0 -0
- package/src/vendor/grammars/tree-sitter-ruby.wasm +0 -0
- package/dist/cli-delegate/chunk-XIAPY44N.js +0 -4587
- package/dist/ui/assets/index-w70Ado09.js +0 -23
- /package/dist/cli-delegate/{chunk-FKYZOWVD.js → chunk-QBAFP67O.js} +0 -0
- /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
|
-
* (`
|
|
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(
|
|
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
|
-
|
|
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
|
-
| {
|
|
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
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
return {
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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')
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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. */
|