@event4u/agent-config 14.19.0 → 14.20.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 (101) 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/CHANGELOG.md +108 -156
  5. package/dist/agent-src/commands/agent-handoff.md +42 -41
  6. package/dist/agent-src/contexts/execution/host-capability-manifest.md +7 -1
  7. package/dist/agent-src/rules/untrusted-input-defense.md +0 -2
  8. package/dist/agent-src/skills/evaluate-llm-feature/SKILL.md +1 -1
  9. package/dist/agent-src/skills/markitdown/SKILL.md +1 -1
  10. package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +28 -6
  11. package/dist/agent-src/skills/test-driven-development/SKILL.md +1 -3
  12. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  13. package/dist/agent-src/templates/roadmaps.md +14 -1
  14. package/dist/agent-src/templates/scripts/telemetry/mcp_lite.ts +213 -0
  15. package/dist/agent-src/templates/scripts/telemetry/report_renderer.ts +16 -0
  16. package/dist/agent-src/templates/scripts/telemetry_report.ts +22 -2
  17. package/dist/cli/mcp/dispatch.js +10 -0
  18. package/dist/cli/mcp/stdio.js +33 -2
  19. package/dist/cli/mcp/telemetry.js +227 -0
  20. package/dist/cli-delegate/{chunk-GZTMRYCN.js → chunk-C6IPZRDA.js} +79 -12
  21. package/dist/cli-delegate/{chunk-CYH5SP6M.js → chunk-I6RHFQEG.js} +151 -16
  22. package/dist/cli-delegate/{chunk-IDHUQIXP.js → chunk-NUMQHLS5.js} +21 -4
  23. package/dist/cli-delegate/{chunk-PMEKGDED.js → chunk-PI2GU3DP.js} +163 -1
  24. package/dist/cli-delegate/{chunk-7PRE6ZV2.js → chunk-YS6XTLEK.js} +199 -4
  25. package/dist/cli-delegate/chunk-ZVBOHSVF.js +1109 -0
  26. package/dist/cli-delegate/cmd_conformance.js +73 -109
  27. package/dist/cli-delegate/cmd_doctor.js +2 -2
  28. package/dist/cli-delegate/cmd_export.js +2 -2
  29. package/dist/cli-delegate/cmd_handoff.js +41 -11
  30. package/dist/cli-delegate/cmd_memory_get.js +1 -1
  31. package/dist/cli-delegate/cmd_prune.js +2 -2
  32. package/dist/cli-delegate/cmd_route_audit.js +1 -1
  33. package/dist/cli-delegate/cmd_session_recycle.js +59 -902
  34. package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
  35. package/dist/cli-delegate/cmd_sync.js +2 -2
  36. package/dist/cli-delegate/cmd_uninstall.js +2 -2
  37. package/dist/cli-delegate/cmd_validate.js +2 -2
  38. package/dist/discovery/deprecation-report.md +1 -1
  39. package/dist/discovery/discovery-manifest.json +9 -9
  40. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  41. package/dist/discovery/discovery-manifest.summary.md +1 -1
  42. package/dist/discovery/dormancy-report.md +1 -1
  43. package/dist/discovery/orphan-report.md +1 -1
  44. package/dist/discovery/packs.json +2 -2
  45. package/dist/discovery/trust-report.md +1 -1
  46. package/dist/discovery/workspaces.json +2 -2
  47. package/dist/hooks/dispatch.js +7257 -6497
  48. package/dist/install/install.mjs +393 -168
  49. package/dist/mcp/registry-manifest.json +2 -2
  50. package/dist/mcp/server.json +2 -2
  51. package/dist/mcp/server.mjs +497 -118
  52. package/dist/scripts/mcp_server/telemetry.js +148 -0
  53. package/docs/guidelines/agent-infra/naming.md +1 -1
  54. package/docs/guidelines/agent-infra/security-lint-containment.md +46 -6
  55. package/docs/guidelines/agent-infra/self-improvement-pipeline.md +4 -4
  56. package/docs/guidelines/agent-infra/untrusted-input-spotlighting.md +1 -1
  57. package/docs/guidelines/redundancy-taxonomy.md +28 -0
  58. package/package.json +1 -1
  59. package/src/agent-src/templates/scripts/telemetry/mcp_lite.ts +213 -0
  60. package/src/agent-src/templates/scripts/telemetry/report_renderer.ts +16 -0
  61. package/src/agent-src/templates/scripts/telemetry_report.ts +22 -2
  62. package/src/config/assurance-capability-registry.json +9 -0
  63. package/src/config/gate-coverage.yml +55 -0
  64. package/src/config/gate-violation-baselines.json +15 -3
  65. package/src/scripts/_cli/cmd_session_recycle.ts +51 -3
  66. package/src/scripts/_lib/audit_field_provenance.ts +44 -3
  67. package/src/scripts/_lib/host_capability.ts +38 -0
  68. package/src/scripts/_lib/install_reach_checks.ts +33 -1
  69. package/src/scripts/_lib/mcp_bridge.ts +222 -10
  70. package/src/scripts/_lib/mcp_consent_residual.ts +173 -0
  71. package/src/scripts/_lib/orchestration_record.ts +23 -1
  72. package/src/scripts/_lib/recycle_envelope_paths.ts +190 -0
  73. package/src/scripts/_lib/release_position.ts +96 -0
  74. package/src/scripts/_lib/review_skipped_record.ts +17 -1
  75. package/src/scripts/_lib/security_lint.ts +179 -7
  76. package/src/scripts/_lib/self_repair.ts +141 -6
  77. package/src/scripts/_lib/self_repair_store.ts +273 -7
  78. package/src/scripts/_lib/subagent_capsule.ts +155 -3
  79. package/src/scripts/build_rule_trigger_matrix.ts +5 -0
  80. package/src/scripts/check_mcp_doc_drift.ts +217 -0
  81. package/src/scripts/check_pack_size.ts +321 -3
  82. package/src/scripts/handoff_context_hook.ts +54 -7
  83. package/src/scripts/hooks/session_eol_hook.ts +18 -7
  84. package/src/scripts/install.ts +4 -4
  85. package/src/scripts/lint_agent_security.ts +316 -25
  86. package/src/scripts/lint_confusables.ts +42 -6
  87. package/src/scripts/lint_handoffs.ts +0 -132
  88. package/src/scripts/lint_hidden_unicode.ts +49 -7
  89. package/src/scripts/lint_instruction_smuggling.ts +42 -6
  90. package/src/scripts/lint_mcp_config_security.ts +42 -6
  91. package/src/scripts/lint_roadmap_later_disposition.ts +217 -27
  92. package/src/scripts/lint_skill_frontmatter_safety.ts +43 -7
  93. package/src/scripts/mcp_server/resources.ts +41 -0
  94. package/src/scripts/mcp_server/telemetry.ts +19 -4
  95. package/src/scripts/orchestration_record.ts +12 -1
  96. package/src/scripts/release.ts +24 -24
  97. package/src/scripts/release_publication.ts +6 -2
  98. package/src/scripts/self_repair_cli.ts +31 -1
  99. package/src/scripts/skill_scout.ts +69 -0
  100. package/dist/cli-delegate/chunk-2GNWGMAL.js +0 -172
  101. package/dist/cli-delegate/chunk-LPPWO5CE.js +0 -161
@@ -0,0 +1,227 @@
1
+ /**
2
+ * Call telemetry for the stdio-lite MCP server (roadmap step 4.1).
3
+ *
4
+ * WHY THIS EXISTS. Before it, `grep -rn 'telemetry|collector|record'
5
+ * src/cli/mcp/*.ts` was empty: the two lite tools were invisible, so every
6
+ * statement about whether the lite surface is used at all was an assumption.
7
+ * One row per `tools/call` turns that into a reading — including a zero, which
8
+ * is a result.
9
+ *
10
+ * THREE CONSTRAINTS THE ROADMAP SET, AND HOW EACH IS MET.
11
+ *
12
+ * - **No second sink.** Rows go to the sink that already collects MCP
13
+ * `tools/call` records, `agents/runtime/mcp-telemetry/calls.jsonl`, through
14
+ * the same `record_call` the kernel server uses. Nothing new is created.
15
+ * - **No new consent gate.** The switch is the EXISTING
16
+ * `telemetry.artifact_engagement.enabled` key, default `false`. No key is
17
+ * introduced, so a consumer who has not opted into telemetry does not
18
+ * acquire a new thing to opt out of.
19
+ * - **One row per call, none when off.** `recordLiteCall` is the only writer
20
+ * and returns `null` without touching the filesystem when the gate is off.
21
+ *
22
+ * PRIVACY BY CONSTRUCTION. Two fields are caller-influenced and both are
23
+ * clamped at the boundary. `host` is resolved against a CLOSED vocabulary; an
24
+ * unrecognised client name becomes `other`, never the string the client sent.
25
+ * `tool_name` is caller-supplied on the wire — a client may call
26
+ * `tools/call` with ANY name — so a name outside a conservative identifier
27
+ * shape is recorded as `other_tool` rather than stored verbatim. Without that
28
+ * clamp the sink would accept a path or a secret as a tool name and this
29
+ * report would print it. The record therefore has no field capable of carrying
30
+ * free-form text, and there is no scrubbing pass that could fail because there
31
+ * is nothing to scrub. The client-install hash comes from the kernel collector
32
+ * unchanged.
33
+ *
34
+ * NEVER THROWS. Telemetry must not break the wire surface; every path here
35
+ * either records or returns `null`.
36
+ */
37
+ import * as fs from 'node:fs';
38
+ import * as path from 'node:path';
39
+ import { record_call } from '../../scripts/mcp_server/telemetry.js';
40
+ /** Transport label for this surface — distinguishes it from the kernel server. */
41
+ export const LITE_TRANSPORT = 'stdio-lite';
42
+ /** The settings key that gates every row. Deliberately an EXISTING key. */
43
+ export const LITE_TELEMETRY_SETTING = 'telemetry.artifact_engagement.enabled';
44
+ /**
45
+ * The closed host vocabulary.
46
+ *
47
+ * Membership is what makes the field non-identifying: a value is either one of
48
+ * these literals, `other`, or `unknown`. It is matched against the MCP
49
+ * `clientInfo.name` a client sends at `initialize`, lower-cased, with `_` and
50
+ * spaces folded to `-`; a name that merely CONTAINS one of these is resolved to
51
+ * it, because clients ship names like `claude-code-cli` and `Cursor (0.42)`.
52
+ */
53
+ export const KNOWN_HOSTS = [
54
+ 'claude-code',
55
+ 'claude-desktop',
56
+ 'cursor',
57
+ 'windsurf',
58
+ 'cline',
59
+ 'gemini-cli',
60
+ 'copilot',
61
+ 'augment',
62
+ 'aider',
63
+ 'codex',
64
+ 'roocode',
65
+ 'continue',
66
+ 'kilocode',
67
+ 'zed',
68
+ 'jetbrains',
69
+ 'kiro',
70
+ 'opencode',
71
+ 'trae',
72
+ 'warp',
73
+ ];
74
+ /** No `initialize` seen yet, or it carried no client name. */
75
+ export const HOST_UNKNOWN = 'unknown';
76
+ /** A client name that is not in the closed vocabulary. Never the raw string. */
77
+ export const HOST_OTHER = 'other';
78
+ /**
79
+ * Resolve a client-supplied name onto the closed vocabulary.
80
+ *
81
+ * Longest match first, so `claude-desktop` is not swallowed by a shorter
82
+ * sibling and `claude-code` wins over a bare `claude` substring.
83
+ */
84
+ export function normalizeHost(clientName) {
85
+ if (typeof clientName !== 'string')
86
+ return HOST_UNKNOWN;
87
+ const folded = clientName.trim().toLowerCase().replace(/[_\s]+/g, '-');
88
+ if (folded === '')
89
+ return HOST_UNKNOWN;
90
+ const byLength = [...KNOWN_HOSTS].sort((a, b) => b.length - a.length);
91
+ for (const known of byLength) {
92
+ if (folded === known || folded.includes(known))
93
+ return known;
94
+ }
95
+ return HOST_OTHER;
96
+ }
97
+ /**
98
+ * The shape a tool name must have to be stored verbatim.
99
+ *
100
+ * Conservative on purpose: MCP tool names are identifiers, so anything with a
101
+ * separator, whitespace, or unusual length is not a tool name that this surface
102
+ * could ever serve — it is caller-supplied text, and text is what must not
103
+ * reach the store.
104
+ */
105
+ const TOOL_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,63}$/;
106
+ /** A caller-supplied name that is not identifier-shaped. Never the raw string. */
107
+ export const TOOL_OTHER = 'other_tool';
108
+ /**
109
+ * Clamp a wire tool name onto a storable one.
110
+ *
111
+ * A name this surface actually serves always passes; an unknown but
112
+ * identifier-shaped name passes too, because that is the latent-demand signal
113
+ * the store exists to carry. Everything else becomes `other_tool`.
114
+ */
115
+ export function normalizeToolName(name, isLiteTool) {
116
+ if (typeof name !== 'string' || name === '')
117
+ return '';
118
+ if (isLiteTool)
119
+ return name;
120
+ return TOOL_NAME_RE.test(name) ? name : TOOL_OTHER;
121
+ }
122
+ /** `str.strip()` on both ends, matching the sibling settings peeks in `src/cli`. */
123
+ function _strip(s) {
124
+ return s.replace(/^[ \t\r\f\v]+/, '').replace(/[ \t\r\f\v]+$/, '');
125
+ }
126
+ /**
127
+ * Read `telemetry.artifact_engagement.enabled` from a project settings file.
128
+ *
129
+ * A deliberately small nested-key peek over the YAML subset this package's own
130
+ * settings files are written in — the same shape `workspace_crypto.isEnabled`
131
+ * and `workspace_analytics` already use in `src/cli`, and for the same reason:
132
+ * the lite server must stay pure-Node with no YAML dependency.
133
+ *
134
+ * Every failure resolves to `false`. Missing file, unreadable file, missing
135
+ * section, missing key and an unparseable value are all indistinguishable and
136
+ * all mean OFF — the default-off doctrine the engagement collector states for
137
+ * itself.
138
+ */
139
+ export function liteTelemetryEnabled(settingsPath) {
140
+ const p = settingsPath ?? path.join(process.cwd(), '.agent-settings.yml');
141
+ let text;
142
+ try {
143
+ text = fs.readFileSync(p, 'utf-8');
144
+ }
145
+ catch {
146
+ return false;
147
+ }
148
+ // Track the two nesting levels the key needs: `telemetry:` at column 0,
149
+ // then `artifact_engagement:` beneath it, then `enabled:` beneath that.
150
+ let inTelemetry = false;
151
+ let inSection = false;
152
+ let sectionIndent = -1;
153
+ for (const raw of text.split(/\r\n|\r|\n/)) {
154
+ const line = raw.replace(/[ \t\r\f\v]+$/, '');
155
+ if (line === '' || _strip(line).startsWith('#'))
156
+ continue;
157
+ const indent = line.length - line.replace(/^ +/, '').length;
158
+ const body = _strip(line);
159
+ if (indent === 0) {
160
+ inTelemetry = body === 'telemetry:';
161
+ inSection = false;
162
+ sectionIndent = -1;
163
+ continue;
164
+ }
165
+ if (!inTelemetry)
166
+ continue;
167
+ if (!inSection) {
168
+ if (body === 'artifact_engagement:') {
169
+ inSection = true;
170
+ sectionIndent = indent;
171
+ }
172
+ continue;
173
+ }
174
+ // Left the section — a sibling key at or above its indent closes it.
175
+ if (indent <= sectionIndent) {
176
+ inSection = body === 'artifact_engagement:';
177
+ if (inSection)
178
+ sectionIndent = indent;
179
+ continue;
180
+ }
181
+ if (body.startsWith('enabled:')) {
182
+ const value = _strip(body.slice('enabled:'.length)).toLowerCase();
183
+ return value === 'true' || value === 'yes' || value === 'on';
184
+ }
185
+ }
186
+ return false;
187
+ }
188
+ /**
189
+ * Classify one lite `tools/call`.
190
+ *
191
+ * `implemented` — a lite tool ran. `stub` — the name is not on this surface, so
192
+ * the `not_implemented` envelope was returned, which is exactly the condition
193
+ * the kernel collector defines that outcome for. The lite server cannot
194
+ * distinguish "in the kernel catalogue but absent here" from "in no catalogue
195
+ * at all", so it never claims `latent_demand`: the kernel server owns that
196
+ * distinction, and guessing it here would put an unearned claim in the store.
197
+ */
198
+ export function classifyLiteOutcome(isLiteTool) {
199
+ return isLiteTool ? 'implemented' : 'stub';
200
+ }
201
+ /**
202
+ * Append exactly one row for one `tools/call`, or nothing.
203
+ *
204
+ * Returns the record written, or `null` when the gate is off or the write
205
+ * failed. Never throws.
206
+ */
207
+ export function recordLiteCall(options) {
208
+ const { toolName, isLiteTool, host, consumerRoot, settingsPath } = options;
209
+ const stored = normalizeToolName(toolName, isLiteTool);
210
+ if (stored === '')
211
+ return null;
212
+ try {
213
+ if (!liteTelemetryEnabled(settingsPath))
214
+ return null;
215
+ return record_call({
216
+ tool_name: stored,
217
+ outcome: classifyLiteOutcome(isLiteTool),
218
+ transport: LITE_TRANSPORT,
219
+ consumer_root: consumerRoot,
220
+ host,
221
+ });
222
+ }
223
+ catch {
224
+ return null;
225
+ }
226
+ }
227
+ //# sourceMappingURL=telemetry.js.map
@@ -21,6 +21,8 @@ import {
21
21
  is_ancestor,
22
22
  load_inventory,
23
23
  manifest_path,
24
+ printResiduals,
25
+ probeHostCapabilities,
24
26
  read_manifest,
25
27
  reap_stale,
26
28
  reap_tagged_orphans,
@@ -28,7 +30,7 @@ import {
28
30
  save_inventory,
29
31
  upsert_tool,
30
32
  write_manifest
31
- } from "./chunk-PMEKGDED.js";
33
+ } from "./chunk-PI2GU3DP.js";
32
34
  import {
33
35
  check_version,
34
36
  classify_mismatch,
@@ -398,16 +400,82 @@ function is_claude_builtin_name(slug) {
398
400
  import * as fs from "node:fs";
399
401
  import * as path from "node:path";
400
402
  var MCP_SERVER_KEY = "agent-config";
401
- var MCP_BRIDGE_ENTRY = {
402
- mcpServers: {
403
- [MCP_SERVER_KEY]: {
404
- command: "npx",
405
- args: ["-y", "@event4u/agent-config", "mcp-server"]
403
+ var MCP_PACKAGE_NAME = "@event4u/agent-config";
404
+ function mcpBridgeEntry(packageRoot) {
405
+ const version = readPackageVersion(packageRoot);
406
+ const spec = version === null ? MCP_PACKAGE_NAME : `${MCP_PACKAGE_NAME}@${version}`;
407
+ return {
408
+ mcpServers: {
409
+ [MCP_SERVER_KEY]: {
410
+ command: "npx",
411
+ args: ["-y", spec, "mcp-server"]
412
+ }
406
413
  }
414
+ };
415
+ }
416
+ function readPackageVersion(packageRoot) {
417
+ try {
418
+ const raw = fs.readFileSync(path.join(packageRoot, "package.json"), "utf-8");
419
+ const v = JSON.parse(raw).version;
420
+ return typeof v === "string" && v.trim() !== "" ? v : null;
421
+ } catch {
422
+ return null;
407
423
  }
408
- };
424
+ }
409
425
  function makeEnsureMcpBridge(mergeJsonFile) {
410
- return (projectRoot, force) => mergeJsonFile(path.join(projectRoot, ".mcp.json"), MCP_BRIDGE_ENTRY, force, ".mcp.json");
426
+ return (projectRoot, force, packageRoot = projectRoot) => mergeJsonFile(
427
+ path.join(projectRoot, ".mcp.json"),
428
+ mcpBridgeEntry(packageRoot),
429
+ force,
430
+ ".mcp.json"
431
+ );
432
+ }
433
+ var MCP_PROJECT_CONFIG = {
434
+ "claude-code": { hostId: "claude", relPath: ".mcp.json" },
435
+ cursor: { hostId: "cursor", relPath: ".cursor/mcp.json" },
436
+ "gemini-cli": { hostId: "gemini", relPath: ".gemini/settings.json" }
437
+ };
438
+ function mcpRegistrationTargets(tools, readsConfig) {
439
+ const out = [];
440
+ for (const [toolId, spec] of Object.entries(MCP_PROJECT_CONFIG)) {
441
+ if (toolId === "claude-code") continue;
442
+ if (!tools.has(toolId)) continue;
443
+ if (!readsConfig(spec.hostId)) continue;
444
+ out.push({ toolId, hostId: spec.hostId, relPath: spec.relPath });
445
+ }
446
+ return out;
447
+ }
448
+ function makeEnsureMcpRegistrations(mergeJsonFile) {
449
+ return (projectRoot, force, packageRoot, targets) => {
450
+ const merged = {};
451
+ for (const t of targets) {
452
+ merged[t.toolId] = mergeJsonFile(
453
+ path.join(projectRoot, ...t.relPath.split("/")),
454
+ mcpBridgeEntry(packageRoot),
455
+ force,
456
+ t.relPath
457
+ );
458
+ }
459
+ return merged;
460
+ };
461
+ }
462
+ function readsProjectMcpConfig(hostId) {
463
+ return probeHostCapabilities(hostId).reads_project_mcp_config;
464
+ }
465
+ function registerMcpHosts(mergeJsonFile, projectRoot, force, packageRoot, tools, into, readsConfig = readsProjectMcpConfig) {
466
+ const ensure = makeEnsureMcpRegistrations(mergeJsonFile);
467
+ const claude = MCP_PROJECT_CONFIG["claude-code"];
468
+ if (tools.has("claude-code") && claude !== void 0) {
469
+ const merged = ensure(projectRoot, force, packageRoot, [
470
+ { toolId: "claude-code", hostId: claude.hostId, relPath: claude.relPath }
471
+ ]);
472
+ (into["claude-code"] ??= []).push(...merged["claude-code"] ?? []);
473
+ }
474
+ for (const [toolId, keys] of Object.entries(
475
+ ensure(projectRoot, force, packageRoot, mcpRegistrationTargets(tools, readsConfig))
476
+ )) {
477
+ (into[toolId] ??= []).push(...keys);
478
+ }
411
479
  }
412
480
  function projectionModeOf(raw) {
413
481
  return raw === "scoped" ? "scoped" : raw === "tiered" ? "tiered" : "legacy-all";
@@ -6804,11 +6872,9 @@ function _main_project_install(opts, project_root, parsed_tools, is_first_run) {
6804
6872
  ...ensure_augment_bridge(project_root, opts.force)
6805
6873
  ];
6806
6874
  if (_is_tool_enabled(tools, "claude-code")) {
6807
- merged_keys_by_tool["claude-code"] = [
6808
- ...ensure_claude_bridge(project_root, opts.force),
6809
- ...ensure_mcp_bridge(project_root, opts.force)
6810
- ];
6875
+ merged_keys_by_tool["claude-code"] = ensure_claude_bridge(project_root, opts.force);
6811
6876
  }
6877
+ registerMcpHosts(merge_json_file, project_root, opts.force, package_root, tools, merged_keys_by_tool);
6812
6878
  if (_is_tool_enabled(tools, "cursor")) {
6813
6879
  merged_keys_by_tool["cursor"] = ensure_cursor_bridge(project_root, opts.force);
6814
6880
  }
@@ -6902,6 +6968,7 @@ function _main_project_install(opts, project_root, parsed_tools, is_first_run) {
6902
6968
  );
6903
6969
  process2.stdout.write("\n");
6904
6970
  }
6971
+ printResiduals(tools, state.QUIET);
6905
6972
  if (_is_tool_enabled(tools, "claude-code")) {
6906
6973
  const team_hint = _team_setup_hint_line(project_root);
6907
6974
  if (team_hint !== null) {
@@ -25,6 +25,7 @@ import { spawnSync } from "node:child_process";
25
25
  import { fileURLToPath as fileURLToPath2, pathToFileURL } from "node:url";
26
26
 
27
27
  // src/scripts/_lib/security_lint.ts
28
+ import { createHash } from "node:crypto";
28
29
  import * as fs from "node:fs";
29
30
  import * as path from "node:path";
30
31
  import { fileURLToPath } from "node:url";
@@ -41,7 +42,7 @@ var DEFAULT_SCAN_ROOTS = [
41
42
  "dist/agent-src"
42
43
  ];
43
44
  var _EXAMPLE_PATH = /(^|\/)(docs|evals|tests?|fixtures?)(\/|$)|example|template|sample|\/_template/i;
44
- var _PRAGMA = /<!--\s*security-lint:\s*allow\s+([A-Za-z0-9_-]+)\s+"([^"]+)"\s*-->/g;
45
+ var _PRAGMA = /<!--\s*security-lint:\s*allow\s+([A-Za-z0-9_-]+)\s+"([^"]+)"((?:\s+sha256:[0-9a-f]{64})*)\s*-->/g;
45
46
  var _FENCE = /^(\s*)(`{3,}|~{3,})\s*([A-Za-z0-9_-]*)\s*$/;
46
47
  var SEVERITY_RANK = { LOW: 1, MED: 2, HIGH: 3 };
47
48
  var Finding = class {
@@ -87,6 +88,10 @@ function is_example_path(rel_path) {
87
88
  function path_weight(rel_path) {
88
89
  return is_example_path(rel_path) ? 0.25 : 1;
89
90
  }
91
+ var PROJECTION_PREFIX = "dist/agent-src/";
92
+ function source_identity(rel) {
93
+ return rel.startsWith(PROJECTION_PREFIX) ? `src/${rel.slice(PROJECTION_PREFIX.length)}` : rel;
94
+ }
90
95
  var ScannedFile = class {
91
96
  path;
92
97
  // absolute or as-given path
@@ -97,8 +102,10 @@ var ScannedFile = class {
97
102
  in_any_fence;
98
103
  pragmas;
99
104
  // check id → reason
105
+ /** check id → the sha256 fingerprints that pragma accepts. Empty = unbound. */
106
+ pragma_hashes;
100
107
  weight;
101
- constructor(pathArg, rel, lines, in_example_fence, in_any_fence, pragmas, weight) {
108
+ constructor(pathArg, rel, lines, in_example_fence, in_any_fence, pragmas, weight, pragma_hashes = {}) {
102
109
  this.path = pathArg;
103
110
  this.rel = rel;
104
111
  this.lines = lines;
@@ -106,10 +113,74 @@ var ScannedFile = class {
106
113
  this.in_any_fence = in_any_fence;
107
114
  this.pragmas = pragmas;
108
115
  this.weight = weight;
116
+ this.pragma_hashes = pragma_hashes;
109
117
  }
110
118
  pragma_allows(check) {
111
119
  return Object.prototype.hasOwnProperty.call(this.pragmas, check);
112
120
  }
121
+ /**
122
+ * Which form of pragma this file carries for `check`.
123
+ *
124
+ * `absent` — no pragma; scan normally.
125
+ * `legacy` — a pragma with no fingerprint. Suppresses the whole file, as it
126
+ * always did, and says so: a key lookup accepts whatever the
127
+ * line is later changed to say, which is the defect being paid
128
+ * down, not a mode anybody should choose.
129
+ * `bound` — a pragma listing fingerprints. Suppresses exactly the matches
130
+ * whose evidence hashes to one of them; anything else survives.
131
+ */
132
+ pragma_form(check) {
133
+ if (!this.pragma_allows(check)) return "absent";
134
+ return (this.pragma_hashes[check] ?? []).length > 0 ? "bound" : "legacy";
135
+ }
136
+ /**
137
+ * The fingerprint one finding's evidence hashes to.
138
+ *
139
+ * WHAT IS HASHED, and each part earns its place:
140
+ * check id — the same line may legitimately be accepted for one check
141
+ * and not another.
142
+ * rel path — the location IDENTITY. Not the line NUMBER: an unrelated
143
+ * insert above would otherwise break every pragma in the
144
+ * file, and a suppression that breaks on unrelated edits
145
+ * gets deleted rather than re-derived. A `dist/agent-src/`
146
+ * path folds to its `src/` original first, because the
147
+ * projection is byte-exact by contract (ADR-201) and
148
+ * carries the pragma verbatim — without the fold, every
149
+ * migrated pragma would suppress the source and leave its
150
+ * own copy flagged, which is a gate that reds on a file
151
+ * nobody may edit.
152
+ * matched line, — the evidence. ASCII whitespace runs collapse so a reflow
153
+ * normalized does not invalidate the binding, `sha256:<hex>` tokens are
154
+ * removed, and NOTHING else is stripped: the zero-width
155
+ * characters this suite hunts are exactly what a wider
156
+ * normalizer would erase.
157
+ *
158
+ * WHY THE HASH TOKENS COME OUT. A pragma's own reason string can be the
159
+ * matched evidence — two in this tree are, because they quote the very
160
+ * phrases their check detects in order to explain themselves. Without this
161
+ * the fingerprint would have to be computed over a line that already
162
+ * contains it, which has no fixed point: writing the hash changes the line,
163
+ * which changes the hash. Stripping the tokens makes the fingerprint stable
164
+ * under the act of recording it, and costs nothing elsewhere — a `sha256:`
165
+ * token in ordinary prose is not evidence of anything a linter here checks.
166
+ *
167
+ * Deliberately NOT the whole file: incidental content elsewhere would make
168
+ * every unrelated edit re-fire the pragma, which is the noise that produces
169
+ * blanket suppressions.
170
+ */
171
+ evidence_fingerprint(finding) {
172
+ const text = finding.line > 0 ? this.lines[finding.line - 1] ?? "" : "";
173
+ const normalized = text.replace(/\s*sha256:[0-9a-f]{64}/g, "").replace(/[ \t]+/g, " ").trim();
174
+ return createHash("sha256").update(`${finding.check}
175
+ ${source_identity(this.rel)}
176
+ ${normalized}`, "utf-8").digest("hex");
177
+ }
178
+ /** Drop the findings this file's BOUND pragma for `check` accepts. */
179
+ filter_bound_pragma(check, findings) {
180
+ const accepted = new Set(this.pragma_hashes[check] ?? []);
181
+ if (accepted.size === 0) return [...findings];
182
+ return findings.filter((f) => !accepted.has(this.evidence_fingerprint(f)));
183
+ }
113
184
  /** Yield [lineno, text] honouring the fence masks (mirrors iter_lines). */
114
185
  *iter_lines(opts = {}) {
115
186
  const skip_example_fence = opts.skip_example_fence ?? true;
@@ -200,11 +271,18 @@ function scan_file(filePath) {
200
271
  }
201
272
  }
202
273
  const pragmas = {};
274
+ const pragmaHashes = {};
203
275
  for (const text of lines) {
204
276
  _PRAGMA.lastIndex = 0;
205
277
  let m;
206
278
  while ((m = _PRAGMA.exec(text)) !== null) {
207
- pragmas[m[1]] = m[2];
279
+ const check = m[1];
280
+ pragmas[check] = m[2];
281
+ const hashes = (m[3] ?? "").match(/sha256:([0-9a-f]{64})/g) ?? [];
282
+ pragmaHashes[check] = [
283
+ ...pragmaHashes[check] ?? [],
284
+ ...hashes.map((h) => h.slice("sha256:".length))
285
+ ];
208
286
  if (m.index === _PRAGMA.lastIndex) {
209
287
  _PRAGMA.lastIndex += 1;
210
288
  }
@@ -217,7 +295,29 @@ function scan_file(filePath) {
217
295
  in_example,
218
296
  in_any,
219
297
  pragmas,
220
- path_weight(rel)
298
+ path_weight(rel),
299
+ pragmaHashes
300
+ );
301
+ }
302
+ function scan_path(filePath, base) {
303
+ const sf = scan_file(filePath);
304
+ let rel;
305
+ const baseResolved = path.resolve(base);
306
+ const fileResolved = path.resolve(filePath);
307
+ if (fileResolved === baseResolved || fileResolved.startsWith(baseResolved + path.sep)) {
308
+ rel = path.relative(baseResolved, fileResolved).split(path.sep).join("/");
309
+ } else {
310
+ rel = path.basename(filePath);
311
+ }
312
+ return new ScannedFile(
313
+ sf.path,
314
+ rel,
315
+ sf.lines,
316
+ sf.in_example_fence,
317
+ sf.in_any_fence,
318
+ sf.pragmas,
319
+ path_weight(rel),
320
+ sf.pragma_hashes
221
321
  );
222
322
  }
223
323
  function _rglobSorted(base) {
@@ -253,9 +353,9 @@ function _pathCmp(a, b) {
253
353
  if (pa.length > pb.length) return 1;
254
354
  return 0;
255
355
  }
256
- function* iter_corpus(roots = DEFAULT_SCAN_ROOTS, exts = [".md"]) {
356
+ function* iter_corpus(roots = DEFAULT_SCAN_ROOTS, exts = [".md"], scanBase = ROOT) {
257
357
  for (const root of roots) {
258
- const base = path.join(ROOT, root);
358
+ const base = path.join(scanBase, root);
259
359
  if (!fs.existsSync(base)) {
260
360
  continue;
261
361
  }
@@ -267,11 +367,27 @@ function* iter_corpus(roots = DEFAULT_SCAN_ROOTS, exts = [".md"]) {
267
367
  continue;
268
368
  }
269
369
  if (st.isFile() && exts.includes(path.extname(p))) {
270
- yield scan_file(p);
370
+ yield scan_path(p, scanBase);
271
371
  }
272
372
  }
273
373
  }
274
374
  }
375
+ var LEGACY_PRAGMA_CHECK = "legacy-pragma";
376
+ function legacy_pragma_finding(sf, check) {
377
+ return new Finding(
378
+ sf.rel,
379
+ 0,
380
+ LEGACY_PRAGMA_CHECK,
381
+ "LOW",
382
+ `unbound \`security-lint: allow ${check}\` pragma suppresses this whole file \u2014 it accepts whatever the matched text is later changed to say. Bind it with \`sha256:<hex>\` per accepted match.`,
383
+ sf.weight
384
+ );
385
+ }
386
+ var CHILD_SCANNED_PREFIX = "scanned: ";
387
+ function report_child_scanned(count) {
388
+ process.stderr.write(`${CHILD_SCANNED_PREFIX}${String(count)}
389
+ `);
390
+ }
275
391
  function report(findings, opts) {
276
392
  const check_label = opts.check_label;
277
393
  if (findings.length === 0) {
@@ -647,9 +763,11 @@ function _cpName(cp) {
647
763
  return n !== void 0 ? n : "<unnamed>";
648
764
  }
649
765
  function _scan(sf) {
650
- if (sf.pragma_allows(CHECK)) {
651
- return [];
766
+ const pragmaForm = sf.pragma_form(CHECK);
767
+ if (pragmaForm === "legacy") {
768
+ return [legacy_pragma_finding(sf, CHECK)];
652
769
  }
770
+ const finish = (hits) => pragmaForm === "bound" ? sf.filter_bound_pragma(CHECK, hits) : hits;
653
771
  const out = [];
654
772
  for (const [lineno, text] of sf.iter_lines({ skip_example_fence: true })) {
655
773
  let vs_run = 0;
@@ -687,7 +805,7 @@ function _scan(sf) {
687
805
  );
688
806
  }
689
807
  }
690
- return out;
808
+ return finish(out);
691
809
  }
692
810
  var _BINARY_EXT = /* @__PURE__ */ new Set([
693
811
  "png",
@@ -848,7 +966,7 @@ function _sanitize(filePath) {
848
966
  return out;
849
967
  }
850
968
  function _argError(msg) {
851
- process.stderr.write("usage: lint_hidden_unicode [-h] [--json] [--fix]\n");
969
+ process.stderr.write("usage: lint_hidden_unicode [-h] [--json] [--fix] [--root DIR]\n");
852
970
  process.stderr.write(`lint_hidden_unicode: error: ${msg}
853
971
  `);
854
972
  process.exit(2);
@@ -856,14 +974,27 @@ function _argError(msg) {
856
974
  function parse_args(argv) {
857
975
  const out = { json: false, fix: false };
858
976
  const extra = [];
977
+ let rootPending = false;
859
978
  for (const a of argv) {
979
+ if (rootPending) {
980
+ out.root = a;
981
+ rootPending = false;
982
+ continue;
983
+ }
860
984
  if (a === "-h" || a === "--help") {
861
- process.stdout.write("usage: lint_hidden_unicode [-h] [--json] [--fix]\n");
985
+ process.stdout.write("usage: lint_hidden_unicode [-h] [--json] [--fix] [--root DIR]\n");
862
986
  process.exit(0);
863
987
  } else if (a === "--json") {
864
988
  out.json = true;
865
989
  } else if (a === "--fix") {
866
990
  out.fix = true;
991
+ } else if (a === "--root" || a.startsWith("--root=")) {
992
+ const eq = a.indexOf("=");
993
+ if (eq !== -1) {
994
+ out.root = a.slice(eq + 1);
995
+ } else {
996
+ rootPending = true;
997
+ }
867
998
  } else {
868
999
  extra.push(a);
869
1000
  }
@@ -875,10 +1006,11 @@ function parse_args(argv) {
875
1006
  }
876
1007
  function main(argv = null) {
877
1008
  const args = parse_args(argv ?? process.argv.slice(2));
1009
+ const scanBase = args.root === void 0 ? ROOT : path2.resolve(args.root);
878
1010
  const findings = [];
879
1011
  const flagged = /* @__PURE__ */ new Set();
880
1012
  let corpusFiles = 0;
881
- for (const sf of iter_corpus()) {
1013
+ for (const sf of iter_corpus(DEFAULT_SCAN_ROOTS, [".md"], scanBase)) {
882
1014
  corpusFiles += 1;
883
1015
  const hits = _scan(sf);
884
1016
  for (const h of hits) {
@@ -903,7 +1035,7 @@ function main(argv = null) {
903
1035
  }
904
1036
  throw exc;
905
1037
  }
906
- const sourceFiles = _eligibleSourceFiles();
1038
+ const sourceFiles = args.root === void 0 ? _eligibleSourceFiles() : [];
907
1039
  for (const h of _scanSourceControlBytes(sourceFiles)) {
908
1040
  findings.push(h);
909
1041
  }
@@ -922,6 +1054,9 @@ function main(argv = null) {
922
1054
  `);
923
1055
  }
924
1056
  }
1057
+ if (args.json) {
1058
+ report_child_scanned(corpusFiles + (sourceFiles === null ? 0 : sourceFiles.length));
1059
+ }
925
1060
  if (args.json) {
926
1061
  const payload = py_json_dumps_indent2(findings.map((f) => f.toDict()));
927
1062
  process.stdout.write(payload + "\n");
@@ -2298,7 +2433,7 @@ function queryIndex(indexPath, keys, types) {
2298
2433
  }
2299
2434
 
2300
2435
  // src/scripts/learning_sidecar.ts
2301
- import { createHash } from "node:crypto";
2436
+ import { createHash as createHash2 } from "node:crypto";
2302
2437
  import * as fs6 from "node:fs";
2303
2438
  import * as path6 from "node:path";
2304
2439
  import process4 from "node:process";
@@ -2314,7 +2449,7 @@ function _parseTsMs(ts) {
2314
2449
  return Number.isNaN(t) ? null : t;
2315
2450
  }
2316
2451
  function _bodyHash(body) {
2317
- return createHash("sha256").update(body.trim().toLowerCase()).digest("hex").slice(0, 12);
2452
+ return createHash2("sha256").update(body.trim().toLowerCase()).digest("hex").slice(0, 12);
2318
2453
  }
2319
2454
  function _globSignalFiles(dir) {
2320
2455
  let names;