@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
@@ -22,17 +22,14 @@
22
22
  * registry, a live session) · `1` a step failed and the previous version was
23
23
  * restored · `4` a step failed and the restore did not reach the previous version.
24
24
  */
25
+ import * as crypto from 'node:crypto';
25
26
  import * as fs from 'node:fs';
27
+ import * as os from 'node:os';
26
28
  import * as path from 'node:path';
27
29
  import { spawnSync } from 'node:child_process';
28
30
  import process from 'node:process';
29
31
 
30
- import {
31
- EXPECTED_WORKFLOW_REF,
32
- attestation_workflow_ref,
33
- provenance_matches_publish,
34
- verify_attestation_signature,
35
- } from '../_lib/provenance_verify.js';
32
+ import { type ReleaseToVerify, verify_release_provenance } from '../_lib/sigstore_provenance.js';
36
33
  import { read_candidate, unattended_decision, type RegistryCandidate } from '../_lib/unattended_gates.js';
37
34
  import type { InstallOrigin } from '../_lib/classify_update.js';
38
35
  import * as installed_lock from '../_lib/installed_lock.js';
@@ -97,10 +94,13 @@ export interface StepResult {
97
94
 
98
95
  export type Packument = { readonly ok: true; readonly doc: unknown } | { readonly ok: false; readonly reason: string };
99
96
 
97
+ /** A verified tarball on disk, the only thing the install step may install — or why there is none. */
98
+ export type Provenance = { readonly ok: true; readonly tarball: string } | { readonly ok: false; readonly reason: string };
99
+
100
100
  export interface UnattendedDeps {
101
101
  readonly fetchPackument: () => Promise<Packument>;
102
- /** Refusal reason when the attestation does not tie to this package's publish workflow, else `null`. */
103
- readonly verifyProvenance: (candidate: RegistryCandidate) => Promise<string | null>;
102
+ /** Download the candidate, verify its attestations against it, and hand back the verified file. */
103
+ readonly verifyProvenance: (candidate: RegistryCandidate) => Promise<Provenance>;
104
104
  readonly run: (cmd: string[]) => StepResult;
105
105
  /** The version recorded in `installed.lock`; `''` when unknown. */
106
106
  readonly readInstalled: () => string;
@@ -243,9 +243,10 @@ function release_lock(deps: Pick<UnattendedDeps, 'lockPath' | 'pid'>): void {
243
243
 
244
244
  // --- the run -----------------------------------------------------------------
245
245
 
246
- function install_steps(version: string, deps: UnattendedDeps): string[][] {
246
+ /** `spec` is the verified tarball for an upgrade, and `<package>@<version>` only for a restore. */
247
+ function install_steps(spec: string, deps: UnattendedDeps): string[][] {
247
248
  return [
248
- ['npm', 'install', '-g', `${PACKAGE_NAME}@${version}`],
249
+ ['npm', 'install', '-g', spec],
249
250
  [deps.agentConfigBin, 'global', '--no-ui'],
250
251
  ...deps.globalSettings.map((target) => [deps.agentConfigBin, 'settings:sync', '--path', target]),
251
252
  ];
@@ -263,7 +264,7 @@ function run_steps(steps: string[][], deps: UnattendedDeps, honourSignal = true)
263
264
 
264
265
  /** Restored means both the binary and `installed.lock` read the previous version again. */
265
266
  function restore(previous: string, deps: UnattendedDeps): number {
266
- const back = run_steps(install_steps(previous, deps).slice(0, 2), deps, false);
267
+ const back = run_steps(install_steps(`${PACKAGE_NAME}@${previous}`, deps).slice(0, 2), deps, false);
267
268
  const lock = deps.readInstalled();
268
269
  const binary = deps.readBinary();
269
270
  if (back.ok && lock === previous && binary === previous) {
@@ -279,7 +280,10 @@ function restore(previous: string, deps: UnattendedDeps): number {
279
280
  return EXIT_RESTORE_FAILED;
280
281
  }
281
282
 
282
- async function decide(installed: string, deps: UnattendedDeps): Promise<{ candidate: RegistryCandidate } | { refused: string[] }> {
283
+ async function decide(
284
+ installed: string,
285
+ deps: UnattendedDeps,
286
+ ): Promise<{ candidate: RegistryCandidate; tarball: string } | { refused: string[] }> {
283
287
  const fetched = await deps.fetchPackument();
284
288
  if (!fetched.ok) return { refused: [`registry unavailable: ${fetched.reason}`] };
285
289
  const candidate = read_candidate(fetched.doc);
@@ -292,85 +296,89 @@ async function decide(installed: string, deps: UnattendedDeps): Promise<{ candid
292
296
  env: { ...deps.env, now: deps.now().toISOString() },
293
297
  });
294
298
  if (!decision.install) return { refused: [...decision.reasons] };
295
- // 2.1's open half. `decision` accepted an attestation that EXISTS and
296
- // carries an SLSA predicate; that document is producible by any workflow
297
- // in any repository with `id-token: write`, so it is checked here against
298
- // the one workflow this package publishes from before anything installs.
299
+ // `decision` accepted an attestation that EXISTS and carries an SLSA
300
+ // predicate, which any workflow with `id-token: write` can produce; the
301
+ // tarball is verified here against pinned roots before anything installs.
299
302
  const provenance = await deps.verifyProvenance(candidate);
300
- if (provenance !== null) return { refused: [provenance] };
301
- return { candidate };
303
+ if (!provenance.ok) return { refused: [provenance.reason] };
304
+ return { candidate, tarball: provenance.tarball };
302
305
  }
303
306
 
307
+ /** Largest tarball the unattended path downloads; a release is about 16 MB. */
308
+ export const MAX_TARBALL_BYTES = 64 * 1024 * 1024;
309
+
304
310
  /**
305
- * Fetch the attestation bundle, require it to name this package's publish
306
- * workflow, and require its DSSE signature to verify. Returns a refusal
307
- * reason, or `null` when both hold.
311
+ * Download the candidate's tarball, check it against the packument's integrity
312
+ * and against both attestations, and write exactly those bytes to a private
313
+ * temp file — the file the install step installs. Returns that file, or why
314
+ * there is none.
308
315
  *
309
- * **This used to say the signature half was `npm audit signatures` "in the
310
- * install step". It was not, and never had been** — `install_steps` runs
311
- * `npm install -g`, `agent-config global` and `settings:sync`, and no audit.
312
- * The sentence described a composition nobody wrote, which is the worst shape
313
- * a security note can have, so it is replaced by what the code does rather
314
- * than corrected in place.
316
+ * The bytes verified and the bytes installed are one buffer: installing by
317
+ * `<package>@<version>` would let the registry serve different bytes to the
318
+ * second request than to the first.
315
319
  *
316
- * What runs now: the payload is tied to the workflow, the envelope's signature
317
- * is verified against the certificate the bundle carries, and that
318
- * certificate's SAN is required to name the same workflow. What does NOT run:
319
- * any check of who ISSUED that certificate. The residue is the
320
- * `dsse-trust-root` blocker on the roadmap, and
321
- * `verify_attestation_signature`'s header states it in full.
322
- *
323
- * A bundle that cannot be fetched REFUSES rather than passing. Unlike the live
324
- * session register, there is no second independent gate behind this one: an
325
- * unreachable attestation is exactly what an attacker serving a tarball
326
- * without one produces, so failing open here would hand back the property the
327
- * check exists to establish.
320
+ * Every URL comes from the packument, so each must be on the registry's own
321
+ * origin; a hostile answer would otherwise choose both the host fetched and the
322
+ * document verified. A fetch that fails REFUSES rather than passing: an
323
+ * unreachable attestation is exactly what an attacker serving a tarball without
324
+ * one produces.
328
325
  */
329
326
  export async function verify_provenance_default(
330
327
  candidate: RegistryCandidate,
331
328
  fetch_impl: typeof fetch = fetch,
332
- timeoutMs = 10_000,
333
- ): Promise<string | null> {
334
- if (candidate.attestationsUrl === null) {
335
- return `${candidate.version} carries no attestation bundle to verify`;
329
+ timeoutMs = 60_000,
330
+ verify: (release: ReleaseToVerify) => Promise<string | null> = (r) => verify_release_provenance(r),
331
+ ): Promise<Provenance> {
332
+ const v = candidate.version;
333
+ const registry = new URL(PACKUMENT_URL).origin;
334
+ const refuse = (reason: string): Provenance => ({ ok: false, reason: `${v}: ${reason}` });
335
+ for (const [what, url] of [
336
+ ['attestation bundle', candidate.attestationsUrl],
337
+ ['tarball', candidate.tarballUrl],
338
+ ] as const) {
339
+ if (url === null) return refuse(`the registry names no ${what} to verify`);
340
+ let origin = '';
341
+ try {
342
+ origin = new URL(url).origin;
343
+ } catch {
344
+ // an unparsable URL is refused below like a foreign one
345
+ }
346
+ if (origin !== registry) return refuse(`the ${what} url is not on ${registry}`);
336
347
  }
337
- // The URL comes from the packument, so a hostile registry answer would
338
- // otherwise choose both the host fetched and the document verified.
339
- let origin: string;
348
+ const integrity = /^sha512-([A-Za-z0-9+/=]+)$/.exec(candidate.integrity ?? '');
349
+ if (integrity === null) return refuse('the registry records no sha512 integrity for the tarball');
350
+
351
+ const get = async (url: string): Promise<Response> => {
352
+ const resp = await fetch_impl(url, { signal: AbortSignal.timeout(timeoutMs), redirect: 'error' });
353
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
354
+ return resp;
355
+ };
356
+ let attestations: unknown;
357
+ let bytes: Buffer;
340
358
  try {
341
- origin = new URL(candidate.attestationsUrl).origin;
342
- } catch {
343
- origin = '';
359
+ attestations = await (await get(candidate.attestationsUrl as string)).json();
360
+ const resp = await get(candidate.tarballUrl as string);
361
+ if (Number(resp.headers.get('content-length') ?? 0) > MAX_TARBALL_BYTES) return refuse('the tarball is larger than allowed');
362
+ bytes = Buffer.from(await resp.arrayBuffer());
363
+ } catch (err) {
364
+ return refuse(`download failed: ${err instanceof Error ? err.message : String(err)}`);
344
365
  }
345
- if (origin !== new URL(PACKUMENT_URL).origin) {
346
- return `${candidate.version}: attestation url is not on ${new URL(PACKUMENT_URL).origin}`;
366
+ if (bytes.length > MAX_TARBALL_BYTES) return refuse('the tarball is larger than allowed');
367
+ const digest = crypto.createHash('sha512').update(bytes).digest();
368
+ if (!digest.equals(Buffer.from(integrity[1] as string, 'base64'))) {
369
+ return refuse('the downloaded tarball does not match the integrity the registry records');
347
370
  }
348
- let doc: unknown;
371
+ const reason = await verify({ version: v, tarballSha512Hex: digest.toString('hex'), attestations });
372
+ if (reason !== null) return refuse(reason);
373
+
349
374
  try {
350
- const resp = await fetch_impl(candidate.attestationsUrl, {
351
- signal: AbortSignal.timeout(timeoutMs),
352
- headers: { accept: 'application/json' },
353
- redirect: 'error',
354
- });
355
- if (!resp.ok) return `attestation fetch failed: HTTP ${resp.status}`;
356
- doc = await resp.json();
375
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-config-upgrade-'));
376
+ const tarball = path.join(dir, `agent-config-${v}.tgz`);
377
+ fs.writeFileSync(tarball, bytes, { mode: 0o600, flag: 'wx' });
378
+ return { ok: true, tarball };
357
379
  } catch (err) {
358
- return `attestation fetch failed: ${err instanceof Error ? err.message : String(err)}`;
359
- }
360
- if (!provenance_matches_publish(doc)) {
361
- const named = attestation_workflow_ref(doc);
362
- return named === null
363
- ? `${candidate.version} has no readable provenance workflow in its attestation`
364
- : `${candidate.version} was built by ${named}, not ${EXPECTED_WORKFLOW_REF}`;
380
+ return refuse(`cannot write the verified tarball: ${err instanceof Error ? err.message : String(err)}`);
365
381
  }
366
- // The payload says the right thing; this asks whether the payload is the
367
- // one that was signed, and whether the certificate that signed it carries
368
- // the same identity. It does NOT establish who issued that certificate —
369
- // see `verify_attestation_signature`'s own note and the `dsse-trust-root`
370
- // blocker, which is where the trust root is decided.
371
- const signature = await verify_attestation_signature(doc);
372
- if (signature !== null) return `${candidate.version}: ${signature}`;
373
- return null;
374
382
  }
375
383
 
376
384
  /** Contexts where even the registry fetch and the lock file are side effects nobody asked for. */
@@ -397,14 +405,16 @@ export async function run_unattended(deps: UnattendedDeps): Promise<number> {
397
405
  say(deps.err, `${PREFIX}: not installing — ${locked}.`);
398
406
  return EXIT_REFUSED;
399
407
  }
408
+ let verified: string | null = null;
400
409
  try {
401
410
  const verdict = await decide(previous, deps);
402
411
  if ('refused' in verdict) {
403
412
  say(deps.out, `ℹ️ ${PREFIX}: not installing (installed: ${previous}) — ${verdict.refused.join('; ')}.`);
404
413
  return EXIT_REFUSED;
405
414
  }
415
+ verified = verdict.tarball;
406
416
  const target = verdict.candidate.version;
407
- const steps = install_steps(target, deps);
417
+ const steps = install_steps(verified, deps);
408
418
  if (deps.activationBlocked !== null) {
409
419
  const how = deps.dryRun ? 'dry run: a real run would not install' : 'not installing';
410
420
  say(deps.out, `ℹ️ ${PREFIX}: every gate passed for ${target}, but ${how} — ${deps.activationBlocked}.`);
@@ -422,10 +432,13 @@ export async function run_unattended(deps: UnattendedDeps): Promise<number> {
422
432
  }
423
433
  const what = `${ran.cmd.join(' ')} (exit ${ran.result.rc})`;
424
434
  if (/\bEACCES\b/.test(ran.result.stderr)) {
435
+ // The verified file is removed when this run ends, so the command a
436
+ // person can still run names the version instead.
437
+ const manual = ran.cmd === steps[0] ? ['npm', 'install', '-g', `${PACKAGE_NAME}@${target}`] : ran.cmd;
425
438
  say(
426
439
  deps.err,
427
440
  `❌ ${PREFIX}: permission denied on ${what}. agent-config never runs sudo; ` +
428
- `run this yourself with the right permissions: ${ran.cmd.join(' ')}`,
441
+ `run this yourself with the right permissions: ${manual.join(' ')}`,
429
442
  );
430
443
  // Denied on the very first write, npm changed nothing; any later step leaves a mixed install.
431
444
  const untouched = ran.cmd === steps[0] && deps.readInstalled() === previous && deps.readBinary() === previous;
@@ -435,6 +448,7 @@ export async function run_unattended(deps: UnattendedDeps): Promise<number> {
435
448
  say(deps.err, `❌ ${PREFIX}: step failed: ${what} — restoring ${previous}.`);
436
449
  return restore(previous, deps);
437
450
  } finally {
451
+ if (verified !== null) fs.rmSync(path.dirname(verified), { recursive: true, force: true });
438
452
  release_lock(deps);
439
453
  }
440
454
  }
@@ -104,7 +104,8 @@ EOF
104
104
  cat <<'EOF'
105
105
 
106
106
  Tier 1 — power-user (release shape, audit, migration):
107
- update Update the agent_config_version pin in .agent-settings.yml
107
+ update Pin agent_config_version in .agent-settings.yml; installs that
108
+ version globally first when the global install is older
108
109
  Flags: --check (read-only) | --to <version> (explicit pin)
109
110
  upgrade Install the latest @event4u/agent-config globally
110
111
  (npm i -g) and refresh the global install + plugin.
@@ -227,7 +228,7 @@ Tier 2 — maintenance / internal (hooks, MCP, memory, telemetry):
227
228
  benchmark Report context-token reduction vs the full always-loaded
228
229
  projection (from the pinned token baseline). Flags: --format
229
230
  code-graph Deterministic code-graph engine (ADR-124, Class A).
230
- Usage: code-graph build|validate|detect|query|explain|affected|path [options]
231
+ Usage: code-graph build|refresh|map|validate|detect|query|explain|affected|path [options]
231
232
  sessions:list List live agent sessions on this repository (shared
232
233
  session register) PLUS the unmerged branches checked
233
234
  out in other worktrees — the axis that needs no
@@ -1400,7 +1401,7 @@ cmd_use() {
1400
1401
  }
1401
1402
 
1402
1403
  # `agent-config update` — flip the agent_config_version pin in
1403
- # .agent-settings.yml. See scripts/_cli/cmd_update.ts (P3.1 of
1404
+ # .agent-settings.yml, upgrading the global install first when it is older. See scripts/_cli/cmd_update.ts (P3.1 of
1404
1405
  # road-to-portable-runtime-and-update-check.md).
1405
1406
  cmd_update() {
1406
1407
  exec_ts "$PACKAGE_ROOT/src/scripts/_cli/cmd_update.ts" "$@"
@@ -49,25 +49,6 @@ export const CONFUSABLE_FOREIGN: ReadonlySet<number> = new Set([
49
49
  0x03a1 /*Ρ→P*/, 0x03a4 /*Τ→T*/, 0x03a5 /*Υ→Y*/, 0x03a7 /*Χ→X*/, 0x0392 /*Β→B*/,
50
50
  ]);
51
51
 
52
- /**
53
- * The basic-Latin twin of each confusable foreign letter that has one, for a
54
- * detector that folds a lookalike back before matching. Every key is in
55
- * `CONFUSABLE_FOREIGN`, which a test holds.
56
- */
57
- export const LATIN_TWIN: ReadonlyMap<number, string> = new Map([
58
- [0x0430, 'a'], [0x0435, 'e'], [0x043e, 'o'], [0x0440, 'p'], [0x0441, 'c'],
59
- [0x0443, 'y'], [0x0445, 'x'], [0x0455, 's'], [0x0456, 'i'], [0x0458, 'j'],
60
- [0x04bb, 'h'], [0x0501, 'd'], [0x051b, 'q'],
61
- [0x0410, 'A'], [0x0412, 'B'], [0x0415, 'E'], [0x041a, 'K'], [0x041c, 'M'],
62
- [0x041d, 'H'], [0x041e, 'O'], [0x0420, 'P'], [0x0421, 'C'], [0x0422, 'T'],
63
- [0x0423, 'Y'], [0x0425, 'X'], [0x0405, 'S'], [0x0406, 'I'], [0x0408, 'J'],
64
- [0x03b1, 'a'], [0x03b5, 'e'], [0x03bf, 'o'], [0x03bd, 'v'], [0x03c1, 'p'],
65
- [0x03c5, 'u'], [0x03c7, 'x'], [0x03ba, 'k'],
66
- [0x0391, 'A'], [0x0392, 'B'], [0x0395, 'E'], [0x0396, 'Z'], [0x0397, 'H'],
67
- [0x0399, 'I'], [0x039a, 'K'], [0x039c, 'M'], [0x039d, 'N'], [0x039f, 'O'],
68
- [0x03a1, 'P'], [0x03a4, 'T'], [0x03a5, 'Y'], [0x03a7, 'X'],
69
- ]);
70
-
71
52
  type ScriptName = 'latin' | 'cyrillic' | 'greek' | 'other';
72
53
 
73
54
  function _script_of(ch: string): ScriptName {
@@ -8,9 +8,20 @@
8
8
  * back to the original offsets, so a replacement removes the disguised bytes
9
9
  * too. Pure: no I/O.
10
10
  */
11
- import { LATIN_TWIN } from './confusables.js';
12
11
  import { detect, EGRESS_RULES, type EgressRule, type EgressSpan } from './egress_rules.js';
13
12
 
13
+ // Aligned: the n-th code point of FOREIGN, three hex digits each, folds to the
14
+ // n-th letter of LATIN. Kept here, not in confusables.ts, so the engine chunk
15
+ // does not carry that module's script regexes.
16
+ const FOREIGN = '43043543e4404414434454554564584bb50151b41041241541a41c41d41e4204214224234254054064083b13b53bf3bd3c13c53c73ba39139239539639739939a39c39d39f3a13a43a53a7';
17
+ const LATIN = 'aeopcyxsijhdqABEKMHOPCTYXSIJaeovpuxkABEZHIKMNOPTYX';
18
+
19
+ /**
20
+ * The basic-Latin twin of each confusable foreign letter that has one. Every
21
+ * key is in `CONFUSABLE_FOREIGN` (confusables.ts), which a test holds.
22
+ */
23
+ export const LATIN_TWIN: ReadonlyMap<number, string> = new Map([...LATIN].map((c, i) => [parseInt(FOREIGN.slice(i * 3, i * 3 + 3), 16), c]));
24
+
14
25
  /**
15
26
  * Invisible and bidirectional-control characters: soft hyphen, zero-width
16
27
  * marks, bidi embeddings and isolates, word joiners, the BOM and the Tag block.
@@ -19,17 +30,15 @@ export const HIDDEN_UNICODE_RE = /[­​-‏‪-‮⁠-⁤⁦-⁩\u{E0000}-\u
19
30
 
20
31
  export interface Normalized {
21
32
  readonly text: string;
22
- /** For each UTF-16 unit of `text`, the original offsets of the character it came from. */
23
- readonly originStart: readonly number[];
24
- readonly originEnd: readonly number[];
33
+ /** For each UTF-16 unit of `text`, the original offset of the character it came from. */
34
+ readonly origin: readonly number[];
25
35
  readonly hidden: number;
26
36
  readonly folded: number;
27
37
  }
28
38
 
29
39
  export function normalizeForDetection(text: string): Normalized {
30
- const out: string[] = [];
31
- const originStart: number[] = [];
32
- const originEnd: number[] = [];
40
+ let out = '';
41
+ const origin: number[] = [];
33
42
  let hidden = 0;
34
43
  let folded = 0;
35
44
  let at = 0;
@@ -38,30 +47,51 @@ export function normalizeForDetection(text: string): Normalized {
38
47
  if (HIDDEN_UNICODE_RE.test(ch)) {
39
48
  hidden += 1;
40
49
  } else {
41
- const twin = LATIN_TWIN.get(ch.codePointAt(0) as number);
42
- if (twin !== undefined) folded += 1;
43
- const emit = twin ?? ch;
44
- for (let k = 0; k < emit.length; k += 1) {
45
- originStart.push(at);
46
- originEnd.push(at + ch.length);
47
- }
48
- out.push(emit);
50
+ const emit = LATIN_TWIN.get(ch.codePointAt(0) as number) ?? ch;
51
+ if (emit !== ch) folded += 1;
52
+ out += emit;
53
+ for (let k = 0; k < emit.length; k += 1) origin.push(at);
49
54
  }
50
55
  at += ch.length;
51
56
  }
52
- return { text: out.join(''), originStart, originEnd, hidden, folded };
57
+ return { text: out, origin, hidden, folded };
58
+ }
59
+
60
+ export interface NormalizedDetection {
61
+ readonly spans: EgressSpan[];
62
+ /**
63
+ * An extent reader such as a rule's `through`, applied to the text the spans
64
+ * were found in, with its end in original offsets. A disguised END marker is
65
+ * then seen by the reader exactly as the disguised BEGIN was by the rule.
66
+ */
67
+ end<S extends EgressSpan>(span: S, read: (text: string, span: S) => number | null): number | null;
53
68
  }
54
69
 
55
70
  /**
56
71
  * `detect`, run on the normalised text with spans in original offsets. With no
57
72
  * invisible or confusable character the result is `detect(text)` exactly.
58
73
  */
59
- export function detectNormalized(text: string, rules: readonly EgressRule[] = EGRESS_RULES): EgressSpan[] {
74
+ export function detectWithExtent(text: string, rules: readonly EgressRule[] = EGRESS_RULES): NormalizedDetection {
60
75
  const n = normalizeForDetection(text);
61
- if (n.hidden === 0 && n.folded === 0) return detect(text, rules);
62
- return detect(n.text, rules).map((s) => ({
63
- ...s,
64
- start: n.originStart[s.start] as number,
65
- end: n.originEnd[s.end - 1] as number,
66
- }));
76
+ if (n.hidden === 0 && n.folded === 0) return { spans: detect(text, rules), end: (s, read) => read(text, s) };
77
+ const back = (i: number): number => {
78
+ const o = n.origin[i - 1] as number;
79
+ return o + ((text.codePointAt(o) as number) > 0xffff ? 2 : 1);
80
+ };
81
+ const fwd = (o: number): number => {
82
+ let i = 0;
83
+ while (i < n.origin.length && (n.origin[i] as number) < o) i += 1;
84
+ return i;
85
+ };
86
+ return {
87
+ spans: detect(n.text, rules).map((s) => ({ ...s, start: n.origin[s.start] as number, end: back(s.end) })),
88
+ end: (s, read) => {
89
+ const e = read(n.text, { ...s, start: fwd(s.start), end: fwd(s.end) });
90
+ return e === null ? null : back(e);
91
+ },
92
+ };
93
+ }
94
+
95
+ export function detectNormalized(text: string, rules: readonly EgressRule[] = EGRESS_RULES): EgressSpan[] {
96
+ return detectWithExtent(text, rules).spans;
67
97
  }
@@ -133,12 +133,18 @@ export interface AssignmentShape {
133
133
  readonly gap?: string;
134
134
  }
135
135
 
136
- /** Source of a keyword-assignment shape: keyword group, operator, then the consumer's value. */
136
+ /**
137
+ * Source of a keyword-assignment shape: keyword group, operator, then the consumer's value.
138
+ * Not an assignment: `===` (strict equality), `==` followed by whitespace (a spaced
139
+ * comparison) and `=>` (an arrow) — read as one, `token === 'x'` yielded the value `==` and
140
+ * the council bundle refused every later `==`. A shell `name==value` stays an assignment,
141
+ * and its second `=` belongs to the operator, so the value read is `value`, never a lone `=`.
142
+ */
137
143
  export function assignmentSource(shape: AssignmentShape): string {
138
144
  const alt = shape.keywords.map((k) => ASSIGNMENT_KEYWORDS[k]).join('|');
139
145
  const group = shape.capture === true ? `(${alt})` : `(?:${alt})`;
140
146
  const gap = shape.gap ?? '\\s*';
141
- const head = `${shape.before ?? ''}${group}${shape.after ?? ''}${gap}[:=]`;
147
+ const head = `${shape.before ?? ''}${group}${shape.after ?? ''}${gap}(?::|=(?!==|=\\s|>)=?)`;
142
148
  return shape.value === undefined ? head : `${head}${gap}${shape.value}`;
143
149
  }
144
150
 
@@ -3,7 +3,8 @@
3
3
  // (`egress_engine.ts`, D9), so the stage keeps the field walk and the chunk
4
4
  // keeps everything that reads a rule.
5
5
 
6
- import { detect, EGRESS_RULES, type EgressClass } from './egress_rules.js';
6
+ import { detectWithExtent } from './egress_normalize.js';
7
+ import { EGRESS_RULES, type EgressClass } from './egress_rules.js';
7
8
  import { EgressRefusedError, replaceValues, type TokenScope } from './egress_token.js';
8
9
 
9
10
  const RULE_BY_ID = new Map(EGRESS_RULES.map((r) => [r.rule, r]));
@@ -19,10 +20,11 @@ export interface TextTally {
19
20
  export function tallyText(text: string): TextTally {
20
21
  const found: Array<{ class: EgressClass; type: string }> = [];
21
22
  const refused: string[] = [];
22
- for (const span of detect(text)) {
23
+ const at = detectWithExtent(text);
24
+ for (const span of at.spans) {
23
25
  const policy = RULE_BY_ID.get(span.rule)?.replacement;
24
26
  if (policy !== undefined && policy !== 'detection') {
25
- const end = policy.through(text, span);
27
+ const end = at.end(span, policy.through);
26
28
  if (end === null || end < span.end) {
27
29
  refused.push(span.rule);
28
30
  continue;
@@ -5,7 +5,8 @@
5
5
 
6
6
  import { createHmac, randomBytes } from 'node:crypto';
7
7
 
8
- import { detect, EGRESS_RULES, type EgressClass, type EgressSpan, replaceSpans } from './egress_rules.js';
8
+ import { detectWithExtent, type NormalizedDetection } from './egress_normalize.js';
9
+ import { EGRESS_RULES, type EgressClass, type EgressSpan, replaceSpans } from './egress_rules.js';
9
10
 
10
11
  export const TOKEN_FP_LENGTH = 6;
11
12
 
@@ -209,12 +210,12 @@ const RULE_BY_ID = new Map(EGRESS_RULES.map((r) => [r.rule, r]));
209
210
  const CLASS_RANK: Readonly<Record<EgressClass, number>> = { credential: 1, identifier: 0 };
210
211
 
211
212
  /** The bytes each detection obliges a replacement to remove; overlapping extents merge. */
212
- function replacementSpans(text: string, detected: readonly EgressSpan[]): EgressSpan[] {
213
+ function replacementSpans(detected: readonly EgressSpan[], at: NormalizedDetection): EgressSpan[] {
213
214
  const extents = detected.map((s) => {
214
215
  const policy = RULE_BY_ID.get(s.rule)?.replacement;
215
216
  if (policy === undefined) throw new EgressRefusedError(s.rule);
216
217
  if (policy === 'detection') return s;
217
- const end = policy.through(text, s);
218
+ const end = at.end(s, policy.through);
218
219
  if (end === null || end < s.end) throw new EgressRefusedError(s.rule);
219
220
  return { ...s, end };
220
221
  });
@@ -243,8 +244,9 @@ function replacementSpans(text: string, detected: readonly EgressSpan[]): Egress
243
244
  */
244
245
  export function replaceValues(text: string, scope: TokenScope): string {
245
246
  const owned = parseTokens(text).filter((t) => scope.owns(text.slice(t.start, t.end)));
246
- const detected = detect(text).filter((s) => !owned.some((t) => t.start <= s.start && s.end <= t.end));
247
- const spans = replacementSpans(text, detected);
247
+ const at = detectWithExtent(text);
248
+ const detected = at.spans.filter((s) => !owned.some((t) => t.start <= s.start && s.end <= t.end));
249
+ const spans = replacementSpans(detected, at);
248
250
  // Mint in text order first: replaceSpans substitutes right to left, and minting
249
251
  // inside its callback would number the last value 1.
250
252
  const tokens = spans.map((s) => scope.mint(s.type, s.class, text.slice(s.start, s.end)));