@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
|
@@ -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
|
-
/**
|
|
103
|
-
readonly verifyProvenance: (candidate: RegistryCandidate) => Promise<
|
|
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
|
-
|
|
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',
|
|
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
|
|
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(
|
|
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
|
-
//
|
|
296
|
-
//
|
|
297
|
-
//
|
|
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
|
|
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
|
-
*
|
|
306
|
-
*
|
|
307
|
-
*
|
|
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
|
-
*
|
|
310
|
-
*
|
|
311
|
-
*
|
|
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
|
-
*
|
|
317
|
-
*
|
|
318
|
-
*
|
|
319
|
-
*
|
|
320
|
-
*
|
|
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 =
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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 (
|
|
346
|
-
|
|
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
|
-
|
|
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
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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 `
|
|
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(
|
|
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: ${
|
|
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
|
|
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
|
|
23
|
-
readonly
|
|
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
|
-
|
|
31
|
-
const
|
|
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
|
|
42
|
-
if (
|
|
43
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
/**
|
|
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 {
|
|
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
|
-
|
|
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 =
|
|
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 {
|
|
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(
|
|
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 =
|
|
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
|
|
247
|
-
const
|
|
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)));
|