@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,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one user-global list of projects this machine has worked in (ADR-299).
|
|
3
|
+
*
|
|
4
|
+
* `runtime/projects/registry.json`, keyed by `<repo id>-<checkout>`
|
|
5
|
+
* (`code_graph/repo_identity`): two clones of one origin are one logical
|
|
6
|
+
* project but keep their own root, graph home and commit, so neither build
|
|
7
|
+
* overwrites the other's. A registry written before that, keyed by the bare
|
|
8
|
+
* repo id, still loads; the entry is re-keyed when its checkout next records.
|
|
9
|
+
* A project enters by use — a graph build or refresh, an advisories refresh, or
|
|
10
|
+
* an explicit `code-graph projects add` — never by a crawl. The file names
|
|
11
|
+
* client projects by absolute path, so nothing that leaves the machine may read
|
|
12
|
+
* it (step 6.3 enforces that).
|
|
13
|
+
*
|
|
14
|
+
* Writes are atomic (`write_atomic`), and read-modify-write goes through a
|
|
15
|
+
* short mkdir lock so two processes recording at once do not drop an entry.
|
|
16
|
+
*/
|
|
17
|
+
import * as fs from 'node:fs';
|
|
18
|
+
import * as path from 'node:path';
|
|
19
|
+
|
|
20
|
+
import { canonicalOrigin, projectsDir, remoteUrl, repoIdentity } from '../code_graph/repo_identity.js';
|
|
21
|
+
import { git_common_dir } from './git_common_dir.js';
|
|
22
|
+
import { write_atomic } from './fs_atomic.js';
|
|
23
|
+
import type { EnvMap } from './user_global_paths.js';
|
|
24
|
+
|
|
25
|
+
export const DEFAULT_BUDGET_MB = 2048;
|
|
26
|
+
|
|
27
|
+
export interface ProjectEntry {
|
|
28
|
+
root: string;
|
|
29
|
+
/** The credential-free canonical origin, or null without a remote. */
|
|
30
|
+
remote: string | null;
|
|
31
|
+
fingerprint: string | null;
|
|
32
|
+
last_seen: string;
|
|
33
|
+
/** The graph home directory last used for this project. */
|
|
34
|
+
graph_home: string | null;
|
|
35
|
+
graph_commit: string | null;
|
|
36
|
+
languages: string[];
|
|
37
|
+
providers: string[];
|
|
38
|
+
pinned: boolean;
|
|
39
|
+
/** User-global graph homes of this project (main checkout and worktrees) → last query. */
|
|
40
|
+
homes: Record<string, string>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface Registry {
|
|
44
|
+
schema: 1;
|
|
45
|
+
/** The estate index switch (council O1): off unless the owner turns it on. */
|
|
46
|
+
index: boolean;
|
|
47
|
+
budget_mb: number;
|
|
48
|
+
projects: Record<string, ProjectEntry>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface RegistryOptions {
|
|
52
|
+
env?: EnvMap | null;
|
|
53
|
+
/** Overrides the projects directory (tests, advisories' injected machine dir). */
|
|
54
|
+
dir?: string;
|
|
55
|
+
now?: Date;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function emptyRegistry(): Registry {
|
|
59
|
+
return { schema: 1, index: false, budget_mb: DEFAULT_BUDGET_MB, projects: {} };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function registryDir(opts: RegistryOptions = {}): string {
|
|
63
|
+
return opts.dir ?? projectsDir(opts.env);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function registryPath(env?: EnvMap | null, dir?: string): string {
|
|
67
|
+
return path.join(dir ?? projectsDir(env), 'registry.json');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function isEntry(v: unknown): v is ProjectEntry {
|
|
71
|
+
const e = v as Partial<ProjectEntry> | null;
|
|
72
|
+
return !!e && typeof e.root === 'string' && typeof e.last_seen === 'string';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** A missing, corrupt or foreign file reads as empty, never as an error. */
|
|
76
|
+
export function readRegistry(opts: RegistryOptions = {}): Registry {
|
|
77
|
+
let raw: unknown;
|
|
78
|
+
try {
|
|
79
|
+
raw = JSON.parse(fs.readFileSync(registryPath(opts.env, opts.dir), 'utf-8'));
|
|
80
|
+
} catch {
|
|
81
|
+
return emptyRegistry();
|
|
82
|
+
}
|
|
83
|
+
const r = raw as Partial<Registry> | null;
|
|
84
|
+
if (!r || typeof r !== 'object' || Array.isArray(r) || r.schema !== 1 || typeof r.projects !== 'object' || r.projects === null) {
|
|
85
|
+
return emptyRegistry();
|
|
86
|
+
}
|
|
87
|
+
const projects: Record<string, ProjectEntry> = {};
|
|
88
|
+
for (const [id, e] of Object.entries(r.projects)) {
|
|
89
|
+
if (!isEntry(e)) continue;
|
|
90
|
+
projects[id] = {
|
|
91
|
+
root: e.root,
|
|
92
|
+
remote: e.remote ?? null,
|
|
93
|
+
fingerprint: e.fingerprint ?? null,
|
|
94
|
+
last_seen: e.last_seen,
|
|
95
|
+
graph_home: e.graph_home ?? null,
|
|
96
|
+
graph_commit: e.graph_commit ?? null,
|
|
97
|
+
languages: Array.isArray(e.languages) ? e.languages : [],
|
|
98
|
+
providers: Array.isArray(e.providers) ? e.providers : [],
|
|
99
|
+
pinned: e.pinned === true,
|
|
100
|
+
homes: e.homes && typeof e.homes === 'object' ? e.homes : {},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
schema: 1,
|
|
105
|
+
index: r.index === true,
|
|
106
|
+
budget_mb: typeof r.budget_mb === 'number' && r.budget_mb > 0 ? r.budget_mb : DEFAULT_BUDGET_MB,
|
|
107
|
+
projects,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function writeRegistry(reg: Registry, opts: RegistryOptions = {}): void {
|
|
112
|
+
write_atomic(registryPath(opts.env, opts.dir), `${JSON.stringify(reg, null, 2)}\n`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const LOCK_WAIT_MS = 2_000;
|
|
116
|
+
const LOCK_STALE_MS = 30_000;
|
|
117
|
+
|
|
118
|
+
function sleep(ms: number): void {
|
|
119
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Read, change and write under the registry lock; the change sees the latest file. */
|
|
123
|
+
export function updateRegistry<T>(fn: (reg: Registry) => T, opts: RegistryOptions = {}): T {
|
|
124
|
+
const lock = path.join(registryDir(opts), 'registry.lock');
|
|
125
|
+
fs.mkdirSync(path.dirname(lock), { recursive: true });
|
|
126
|
+
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
127
|
+
for (;;) {
|
|
128
|
+
try {
|
|
129
|
+
fs.mkdirSync(lock);
|
|
130
|
+
break;
|
|
131
|
+
} catch (e) {
|
|
132
|
+
if ((e as NodeJS.ErrnoException).code !== 'EEXIST') throw e;
|
|
133
|
+
try {
|
|
134
|
+
if (Date.now() - fs.statSync(lock).mtimeMs > LOCK_STALE_MS) fs.rmSync(lock, { recursive: true, force: true });
|
|
135
|
+
} catch {
|
|
136
|
+
/* released meanwhile */
|
|
137
|
+
}
|
|
138
|
+
if (Date.now() > deadline) throw new Error(`project registry is locked: ${lock}`);
|
|
139
|
+
sleep(25);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
const reg = readRegistry(opts);
|
|
144
|
+
const out = fn(reg);
|
|
145
|
+
writeRegistry(reg, opts);
|
|
146
|
+
return out;
|
|
147
|
+
} finally {
|
|
148
|
+
fs.rmSync(lock, { recursive: true, force: true });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** The main checkout of a worktree, else the root itself. */
|
|
153
|
+
export function mainRoot(root: string): string {
|
|
154
|
+
let real: string;
|
|
155
|
+
try {
|
|
156
|
+
real = fs.realpathSync(root);
|
|
157
|
+
} catch {
|
|
158
|
+
real = path.resolve(root);
|
|
159
|
+
}
|
|
160
|
+
const common = git_common_dir(real);
|
|
161
|
+
return common !== null && path.basename(common) === '.git' ? path.dirname(common) : real;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export type ProjectFields = Partial<Pick<ProjectEntry, 'fingerprint' | 'graph_home' | 'graph_commit' | 'languages' | 'providers' | 'pinned'>> & {
|
|
165
|
+
/** A user-global graph home that just served or rebuilt this project. */
|
|
166
|
+
usedHome?: string;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/** The logical project an entry key belongs to: the repo id, for a new or a bare legacy key. */
|
|
170
|
+
export function repoIdOf(key: string): string {
|
|
171
|
+
return key.split('-')[0] as string;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Record `root` (by use, never by a crawl) and return its entry key, `<repo id>-<checkout>`. */
|
|
175
|
+
export function recordProject(root: string, fields: ProjectFields = {}, opts: RegistryOptions = {}): string {
|
|
176
|
+
const dir = registryDir(opts);
|
|
177
|
+
const ident = repoIdentity(root, opts.env, dir);
|
|
178
|
+
const id = `${ident.repoId}-${ident.checkout}`;
|
|
179
|
+
const main = mainRoot(root);
|
|
180
|
+
const origin = remoteUrl(main);
|
|
181
|
+
const now = (opts.now ?? new Date()).toISOString();
|
|
182
|
+
updateRegistry((reg) => {
|
|
183
|
+
const legacy = reg.projects[ident.repoId];
|
|
184
|
+
const adopt = reg.projects[id] === undefined && legacy !== undefined && legacy.root === main;
|
|
185
|
+
const prev = reg.projects[id] ?? (adopt ? legacy : undefined);
|
|
186
|
+
if (adopt) delete reg.projects[ident.repoId];
|
|
187
|
+
const { usedHome, ...rest } = fields;
|
|
188
|
+
const next: ProjectEntry = {
|
|
189
|
+
root: main,
|
|
190
|
+
remote: origin === null ? null : canonicalOrigin(origin, main),
|
|
191
|
+
fingerprint: prev?.fingerprint ?? null,
|
|
192
|
+
graph_home: prev?.graph_home ?? null,
|
|
193
|
+
graph_commit: prev?.graph_commit ?? null,
|
|
194
|
+
languages: prev?.languages ?? [],
|
|
195
|
+
providers: prev?.providers ?? [],
|
|
196
|
+
pinned: prev?.pinned ?? false,
|
|
197
|
+
homes: { ...(prev?.homes ?? {}) },
|
|
198
|
+
...Object.fromEntries(Object.entries(rest).filter(([, v]) => v !== undefined)),
|
|
199
|
+
last_seen: now,
|
|
200
|
+
};
|
|
201
|
+
if (usedHome !== undefined) next.homes[usedHome] = now;
|
|
202
|
+
reg.projects[id] = next;
|
|
203
|
+
}, { ...opts, dir });
|
|
204
|
+
return id;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* The entries a reference names: an entry key, a repo id (every checkout of
|
|
209
|
+
* it), a registered name (the root's basename) or a path. A name two projects
|
|
210
|
+
* share returns both, so the caller can refuse to guess.
|
|
211
|
+
*/
|
|
212
|
+
export function findProjects(reg: Registry, ref: string): Array<[string, ProjectEntry]> {
|
|
213
|
+
const all = Object.entries(reg.projects);
|
|
214
|
+
const byKey = all.filter(([id]) => id === ref);
|
|
215
|
+
if (byKey.length) return byKey;
|
|
216
|
+
const byId = all.filter(([id]) => repoIdOf(id) === ref);
|
|
217
|
+
if (byId.length) return byId;
|
|
218
|
+
let real: string | null = null;
|
|
219
|
+
try {
|
|
220
|
+
real = fs.realpathSync(path.resolve(ref));
|
|
221
|
+
} catch {
|
|
222
|
+
real = path.resolve(ref);
|
|
223
|
+
}
|
|
224
|
+
const byPath = all.filter(([, e]) => e.root === real || e.root === ref);
|
|
225
|
+
if (byPath.length) return byPath;
|
|
226
|
+
return all.filter(([, e]) => path.basename(e.root) === ref);
|
|
227
|
+
}
|
|
@@ -1,31 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Which workflow, in which repository, a legitimate release of this package is
|
|
3
|
+
* built by — and the check that a SIGNED provenance payload names them.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* step that opened this file (`road-to-an-update-that-installs-itself-only-when-it-is-safe`
|
|
11
|
-
* 2.1) named exactly that gap — *"does not verify the attestation's signature
|
|
12
|
-
* or tie it to `publish-npm.yml`"* — and this module closes the second half.
|
|
13
|
-
*
|
|
14
|
-
* **The first half is only partly closed here, and the distinction is
|
|
15
|
-
* load-bearing.** `provenance_matches_publish` parses an UNSIGNED reading of
|
|
16
|
-
* the DSSE payload. `verify_attestation_signature` below checks the envelope's
|
|
17
|
-
* signature against the certificate the bundle carries, and requires the
|
|
18
|
-
* payload it verified to name the same workflow — but checks no trust root
|
|
19
|
-
* (see its own note). The registry-key half is `npm audit signatures`, which
|
|
20
|
-
* verifies against npm's own trusted keys and is run by the caller as a
|
|
21
|
-
* separate step; these functions are only meaningful WITH it, and the caller
|
|
22
|
-
* says so where it composes them.
|
|
23
|
-
*
|
|
24
|
-
* Why not verify the signature in-process: it needs a sigstore/cosign
|
|
25
|
-
* verification path, which is a dependency this package does not carry and
|
|
26
|
-
* which the hook and pack byte budgets would both have to absorb. Shelling out
|
|
27
|
-
* to the npm client — already a hard dependency of an upgrade — buys the same
|
|
28
|
-
* property for nothing.
|
|
5
|
+
* Signatures, trust roots and the tie to the tarball are verified in
|
|
6
|
+
* `sigstore_provenance.ts`; this module only reads a payload that has already
|
|
7
|
+
* been verified there. It used to carry a certificate-bound signature check of
|
|
8
|
+
* its own, which checked no issuer and so passed a self-signed certificate
|
|
9
|
+
* carrying the expected SAN; that check is gone rather than kept as a fallback.
|
|
29
10
|
*/
|
|
30
11
|
|
|
31
12
|
/**
|
|
@@ -44,65 +25,16 @@ function asRecord(v: unknown): Record<string, unknown> | null {
|
|
|
44
25
|
return v !== null && typeof v === 'object' && !Array.isArray(v) ? (v as Record<string, unknown>) : null;
|
|
45
26
|
}
|
|
46
27
|
|
|
47
|
-
/** Decode the DSSE payload of the first attestation; `null` when unreadable. */
|
|
48
|
-
function decode_payload(doc: unknown): Record<string, unknown> | null {
|
|
49
|
-
const list = asRecord(doc)?.['attestations'];
|
|
50
|
-
if (!Array.isArray(list) || list.length === 0) return null;
|
|
51
|
-
for (const entry of list) {
|
|
52
|
-
const payload = asRecord(asRecord(asRecord(entry)?.['bundle'])?.['dsseEnvelope'])?.['payload'];
|
|
53
|
-
if (typeof payload !== 'string' || payload === '') continue;
|
|
54
|
-
try {
|
|
55
|
-
const parsed: unknown = JSON.parse(Buffer.from(payload, 'base64').toString('utf8'));
|
|
56
|
-
const rec = asRecord(parsed);
|
|
57
|
-
if (rec !== null) return rec;
|
|
58
|
-
} catch {
|
|
59
|
-
// A payload that is not base64 JSON is not evidence of anything.
|
|
60
|
-
// Keep looking: npm may serve several attestations and only one of
|
|
61
|
-
// them has to be the provenance statement.
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function external_workflow(doc: unknown): Record<string, unknown> | null {
|
|
68
|
-
const payload = decode_payload(doc);
|
|
69
|
-
if (payload === null) return null;
|
|
70
|
-
const predicate = asRecord(payload['predicate']);
|
|
71
|
-
const external = asRecord(asRecord(predicate?.['buildDefinition'])?.['externalParameters']);
|
|
72
|
-
return asRecord(external?.['workflow']);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* The workflow path named by the attestation, or `null` when the document
|
|
77
|
-
* carries none that can be read.
|
|
78
|
-
*/
|
|
79
|
-
export function attestation_workflow_ref(doc: unknown): string | null {
|
|
80
|
-
const path_value = external_workflow(doc)?.['path'];
|
|
81
|
-
return typeof path_value === 'string' && path_value !== '' ? path_value : null;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** The repository named by the attestation, or `null`. */
|
|
85
|
-
export function attestation_repository(doc: unknown): string | null {
|
|
86
|
-
const repo = external_workflow(doc)?.['repository'];
|
|
87
|
-
return typeof repo === 'string' && repo !== '' ? repo : null;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
28
|
/**
|
|
91
|
-
* Does this
|
|
92
|
-
* workflow, in this package's repository?
|
|
29
|
+
* Does this provenance payload say the tarball was built by this package's
|
|
30
|
+
* publish workflow, in this package's repository?
|
|
93
31
|
*
|
|
94
|
-
* Both halves are required and both reject on absence: a
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
* signal.
|
|
32
|
+
* Both halves are required and both reject on absence: a payload that names no
|
|
33
|
+
* workflow is not one that names the right one. Repository is compared with
|
|
34
|
+
* the trailing `.git` and any trailing slash removed, because the same remote
|
|
35
|
+
* is written both ways and neither spelling is an attacker signal.
|
|
99
36
|
*/
|
|
100
|
-
export function
|
|
101
|
-
return payload_names_publish(decode_payload(doc));
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/** The workflow and repository check, over one already-decoded payload. */
|
|
105
|
-
function payload_names_publish(payload: Record<string, unknown> | null): boolean {
|
|
37
|
+
export function payload_names_publish(payload: Record<string, unknown> | null): boolean {
|
|
106
38
|
const predicate = asRecord(payload?.['predicate']);
|
|
107
39
|
const workflow = asRecord(asRecord(asRecord(predicate?.['buildDefinition'])?.['externalParameters'])?.['workflow']);
|
|
108
40
|
if (workflow?.['path'] !== EXPECTED_WORKFLOW_REF) return false;
|
|
@@ -110,163 +42,3 @@ function payload_names_publish(payload: Record<string, unknown> | null): boolean
|
|
|
110
42
|
if (typeof repo !== 'string' || repo === '') return false;
|
|
111
43
|
return repo.replace(/\.git$/, '').replace(/\/$/, '') === EXPECTED_REPOSITORY;
|
|
112
44
|
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Does the certificate's SAN carry the workflow identity as a whole URI entry?
|
|
116
|
-
* Anchored: the entry is the expected workflow followed by `@<ref>`, never a
|
|
117
|
-
* SAN that merely contains the string.
|
|
118
|
-
*/
|
|
119
|
-
function san_names_workflow(san: string): boolean {
|
|
120
|
-
return san.split(/,\s*/).some((e) => e.startsWith(`URI:${SAN_WORKFLOW_MARKER}@`));
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// --- the signature half -------------------------------------------------------
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* What the clauses below do and do not establish.
|
|
127
|
-
*
|
|
128
|
-
* The header above says the signature half is not closed here. It is now
|
|
129
|
-
* HALF-closed, and the half that is missing is stated first so nothing below
|
|
130
|
-
* reads as more than it is.
|
|
131
|
-
*
|
|
132
|
-
* **Closed:** the DSSE envelope's signature is verified against the public key
|
|
133
|
-
* of the certificate the bundle itself carries, over the DSSE
|
|
134
|
-
* pre-authentication encoding — so a payload edited after signing is caught,
|
|
135
|
-
* and a bundle whose signature does not belong to its own certificate is
|
|
136
|
-
* caught. And the certificate's own SAN is required to name the expected
|
|
137
|
-
* workflow, so a bundle whose PAYLOAD claims `publish-npm.yml` while its
|
|
138
|
-
* certificate was issued to some other identity is caught. Before this, both
|
|
139
|
-
* were a base64 decode and a string compare.
|
|
140
|
-
*
|
|
141
|
-
* **Not closed, and this is the residue:** there is no trust root. Nothing
|
|
142
|
-
* here checks that the certificate was issued by Fulcio, and nothing checks a
|
|
143
|
-
* Rekor inclusion proof. An attacker who can serve a registry response can mint
|
|
144
|
-
* a self-signed certificate carrying the expected SAN, sign their own payload
|
|
145
|
-
* with it, and pass every check in this file. Closing that needs a sigstore
|
|
146
|
-
* verification path with pinned roots — a dependency decision, not a line in
|
|
147
|
-
* this function, and it is registered as the `dsse-trust-root` blocker on
|
|
148
|
-
* `road-to-an-update-that-installs-itself-only-when-it-is-safe`.
|
|
149
|
-
*
|
|
150
|
-
* So the honest statement of what the provenance gate is worth today: it
|
|
151
|
-
* defeats a partial compromise — a mis-scoped workflow, an edited payload, a
|
|
152
|
-
* bundle reused from another build — and it does not defeat a forged bundle
|
|
153
|
-
* from someone who already controls what the install reads.
|
|
154
|
-
*/
|
|
155
|
-
|
|
156
|
-
/** Why the signature does not verify, or `null` when it does. */
|
|
157
|
-
export type SignatureVerdict = string | null;
|
|
158
|
-
|
|
159
|
-
/** The SAN extension OID the Fulcio leaf carries the workflow identity in. */
|
|
160
|
-
const SAN_WORKFLOW_MARKER = `${EXPECTED_REPOSITORY}/${EXPECTED_WORKFLOW_REF}`;
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* The DSSE pre-authentication encoding — the exact bytes a DSSE signature is
|
|
164
|
-
* taken over.
|
|
165
|
-
*
|
|
166
|
-
* `DSSEv1 SP len(payloadType) SP payloadType SP len(payload) SP payload`, with
|
|
167
|
-
* both lengths in BYTES rather than characters. Built as a Buffer throughout
|
|
168
|
-
* because the payload is arbitrary bytes and a string round-trip would mangle
|
|
169
|
-
* any that are not valid UTF-8.
|
|
170
|
-
*/
|
|
171
|
-
export function dssePae(payloadType: string, payload: Buffer): Buffer {
|
|
172
|
-
const type = Buffer.from(payloadType, 'utf8');
|
|
173
|
-
return Buffer.concat([
|
|
174
|
-
Buffer.from(`DSSEv1 ${String(type.length)} `, 'utf8'),
|
|
175
|
-
type,
|
|
176
|
-
Buffer.from(` ${String(payload.length)} `, 'utf8'),
|
|
177
|
-
payload,
|
|
178
|
-
]);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
interface Envelope {
|
|
182
|
-
readonly payloadType: string;
|
|
183
|
-
readonly payload: Buffer;
|
|
184
|
-
readonly sig: Buffer;
|
|
185
|
-
readonly certDer: Buffer;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function readEnvelope(entry: unknown): Envelope | null {
|
|
189
|
-
const bundle = asRecord(asRecord(entry)?.['bundle']);
|
|
190
|
-
const dsse = asRecord(bundle?.['dsseEnvelope']);
|
|
191
|
-
if (dsse === null) return null;
|
|
192
|
-
const payloadB64 = dsse['payload'];
|
|
193
|
-
const payloadType = dsse['payloadType'];
|
|
194
|
-
const sigs = dsse['signatures'];
|
|
195
|
-
if (typeof payloadB64 !== 'string' || typeof payloadType !== 'string' || !Array.isArray(sigs) || sigs.length === 0) return null;
|
|
196
|
-
const sigB64 = asRecord(sigs[0])?.['sig'];
|
|
197
|
-
if (typeof sigB64 !== 'string' || sigB64 === '') return null;
|
|
198
|
-
|
|
199
|
-
const material = asRecord(bundle?.['verificationMaterial']);
|
|
200
|
-
// Both shapes sigstore has used: a single certificate, and a chain.
|
|
201
|
-
const single = asRecord(material?.['certificate'])?.['rawBytes'];
|
|
202
|
-
const chain = asRecord(material?.['x509CertificateChain'])?.['certificates'];
|
|
203
|
-
const first = Array.isArray(chain) ? asRecord(chain[0])?.['rawBytes'] : undefined;
|
|
204
|
-
const certB64 = typeof single === 'string' ? single : typeof first === 'string' ? first : null;
|
|
205
|
-
if (certB64 === null || certB64 === '') return null;
|
|
206
|
-
|
|
207
|
-
return {
|
|
208
|
-
payloadType,
|
|
209
|
-
payload: Buffer.from(payloadB64, 'base64'),
|
|
210
|
-
sig: Buffer.from(sigB64, 'base64'),
|
|
211
|
-
certDer: Buffer.from(certB64, 'base64'),
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Verify the first readable attestation's DSSE signature against its own
|
|
217
|
-
* certificate, and require that certificate to name this package's workflow.
|
|
218
|
-
*
|
|
219
|
-
* Returns a reason on every failure rather than a boolean, because the caller
|
|
220
|
-
* prints it: a refusal a user cannot act on is a refusal they route around.
|
|
221
|
-
* `crypto` is imported lazily so a caller that never verifies a signature does
|
|
222
|
-
* not pay for the module — this runs on an install path, never in a hook.
|
|
223
|
-
*/
|
|
224
|
-
export async function verify_attestation_signature(doc: unknown): Promise<SignatureVerdict> {
|
|
225
|
-
const list = asRecord(doc)?.['attestations'];
|
|
226
|
-
if (!Array.isArray(list) || list.length === 0) return 'the attestation bundle carries no attestations';
|
|
227
|
-
const crypto = await import('node:crypto');
|
|
228
|
-
|
|
229
|
-
let lastReason = 'no attestation carried a readable DSSE envelope';
|
|
230
|
-
for (const entry of list) {
|
|
231
|
-
const env = readEnvelope(entry);
|
|
232
|
-
if (env === null) continue;
|
|
233
|
-
let cert: InstanceType<typeof crypto.X509Certificate>;
|
|
234
|
-
try {
|
|
235
|
-
cert = new crypto.X509Certificate(env.certDer);
|
|
236
|
-
} catch {
|
|
237
|
-
lastReason = 'the signing certificate could not be parsed';
|
|
238
|
-
continue;
|
|
239
|
-
}
|
|
240
|
-
// The certificate's own identity, not the payload's claim about it.
|
|
241
|
-
const san = cert.subjectAltName ?? '';
|
|
242
|
-
if (!san_names_workflow(san)) {
|
|
243
|
-
lastReason = `the signing certificate names ${san === '' ? 'no identity' : san}, not ${SAN_WORKFLOW_MARKER}`;
|
|
244
|
-
continue;
|
|
245
|
-
}
|
|
246
|
-
let ok = false;
|
|
247
|
-
try {
|
|
248
|
-
ok = crypto.verify(null, dssePae(env.payloadType, env.payload), cert.publicKey, env.sig);
|
|
249
|
-
} catch {
|
|
250
|
-
ok = false;
|
|
251
|
-
}
|
|
252
|
-
if (!ok) {
|
|
253
|
-
lastReason = 'the DSSE signature does not verify against the certificate in the bundle';
|
|
254
|
-
continue;
|
|
255
|
-
}
|
|
256
|
-
// The payload this signature covers must itself name the workflow:
|
|
257
|
-
// otherwise a valid signature on one entry vouches for a claim made in
|
|
258
|
-
// another, which is all the unsigned check reads.
|
|
259
|
-
let verified: Record<string, unknown> | null = null;
|
|
260
|
-
try {
|
|
261
|
-
verified = asRecord(JSON.parse(env.payload.toString('utf8')));
|
|
262
|
-
} catch {
|
|
263
|
-
verified = null;
|
|
264
|
-
}
|
|
265
|
-
if (!payload_names_publish(verified)) {
|
|
266
|
-
lastReason = 'the signed payload does not name this package\'s publish workflow';
|
|
267
|
-
continue;
|
|
268
|
-
}
|
|
269
|
-
return null;
|
|
270
|
-
}
|
|
271
|
-
return lastReason;
|
|
272
|
-
}
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
import * as fs from 'node:fs';
|
|
34
34
|
import * as path from 'node:path';
|
|
35
35
|
|
|
36
|
-
import {
|
|
36
|
+
import { toJsonSchema7 } from '../../shared/zodJsonSchema.js';
|
|
37
37
|
|
|
38
38
|
import { settingsSchema } from '../../server/schemas/settings.js';
|
|
39
39
|
import { computeSurfaceDelta, flattenSurface, type SettingsSurface, type SurfaceEntry } from '../../shared/settingsSurface.js';
|
|
@@ -50,11 +50,7 @@ import { RENAMED_KEYS, SETTINGS_REMOVAL_RULE } from './settings_renamed_keys.js'
|
|
|
50
50
|
* "keep a preview fast" rule was written about.
|
|
51
51
|
*/
|
|
52
52
|
function _jsonSchema(): Parameters<typeof flattenSurface>[0] {
|
|
53
|
-
return
|
|
54
|
-
name: 'AgentSettings',
|
|
55
|
-
$refStrategy: 'none',
|
|
56
|
-
target: 'jsonSchema7',
|
|
57
|
-
}) as Parameters<typeof flattenSurface>[0];
|
|
53
|
+
return toJsonSchema7(settingsSchema, 'AgentSettings') as Parameters<typeof flattenSurface>[0];
|
|
58
54
|
}
|
|
59
55
|
|
|
60
56
|
/** The one committed copy of the last release's surface. */
|