micro-models-agent 2.10.0 → 2.12.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 (359) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/assets/app.js +22 -4
  3. package/dist/assets/index.html +29 -0
  4. package/dist/assets/js/dom.js +2 -1
  5. package/dist/assets/js/feed.js +38 -26
  6. package/dist/assets/js/format.js +1 -1
  7. package/dist/assets/js/rail.js +114 -0
  8. package/dist/assets/js/settings-controls.js +2 -2
  9. package/dist/assets/js/settings.js +246 -51
  10. package/dist/assets/js/store.js +3 -0
  11. package/dist/assets/js/tabs.js +104 -2
  12. package/dist/assets/styles.css +215 -66
  13. package/dist/cli/cache-line.js +1 -0
  14. package/dist/cli/command-suggest.js +5 -4
  15. package/dist/cli/commands.js +231 -122
  16. package/dist/cli/completer.js +132 -31
  17. package/dist/cli/index.js +1 -0
  18. package/dist/cli/json-payload.js +4 -4
  19. package/dist/cli/main.js +166 -61
  20. package/dist/cli/onboarding-commands.js +126 -0
  21. package/dist/cli/plugin-commands.js +2 -1
  22. package/dist/cli/relaunch.js +16 -13
  23. package/dist/cli/repl-commands-lsp.js +75 -0
  24. package/dist/cli/repl-commands-skills.js +105 -0
  25. package/dist/cli/repl-commands.js +589 -266
  26. package/dist/cli/repl.js +436 -177
  27. package/dist/cli/run-result.js +5 -6
  28. package/dist/cli/security-commands.js +37 -28
  29. package/dist/cli/setup-order.js +4 -55
  30. package/dist/cli/setup-prompt.js +3 -2
  31. package/dist/cli/setup.js +27 -107
  32. package/dist/cli/web-command.js +98 -0
  33. package/dist/config/budget.js +5 -5
  34. package/dist/config/config.js +103 -96
  35. package/dist/config/defaults.js +38 -8
  36. package/dist/config/domains.js +24 -27
  37. package/dist/config/env.js +46 -0
  38. package/dist/config/experts.js +2 -0
  39. package/dist/config/index.js +1 -0
  40. package/dist/config/paths.js +56 -0
  41. package/dist/config/redact.js +44 -0
  42. package/dist/config/registry-extra.js +334 -0
  43. package/dist/config/registry.js +584 -0
  44. package/dist/config/security.js +35 -36
  45. package/dist/config/timeouts.js +74 -0
  46. package/dist/config/utils.js +16 -15
  47. package/dist/core/agent/artifact-progress.js +59 -0
  48. package/dist/core/agent/artifact-resolve.js +79 -0
  49. package/dist/core/agent/audit-gate.js +7 -3
  50. package/dist/core/agent/compaction.js +2 -2
  51. package/dist/core/agent/constants.js +33 -4
  52. package/dist/core/agent/context-log.js +454 -0
  53. package/dist/core/agent/context-renderer.js +5 -0
  54. package/dist/core/agent/critic-gate.js +132 -0
  55. package/dist/core/agent/hallucination-gate.js +148 -38
  56. package/dist/core/agent/loop-state.js +15 -0
  57. package/dist/core/agent/prefix-monitor.js +1 -1
  58. package/dist/core/agent/reasoning-resolver.js +10 -1
  59. package/dist/core/agent/token-tracker.js +7 -4
  60. package/dist/core/agent/tool-batch.js +93 -18
  61. package/dist/core/agent/tool-images.js +18 -0
  62. package/dist/core/agent/tool-output.js +65 -11
  63. package/dist/core/agent/tool-pairing.js +60 -0
  64. package/dist/core/agent-moe.js +25 -24
  65. package/dist/core/agent.js +429 -108
  66. package/dist/core/bootstrap/agent-phase.js +66 -0
  67. package/dist/core/bootstrap/config-phase.js +86 -0
  68. package/dist/core/bootstrap/modules-phase.js +269 -0
  69. package/dist/core/bootstrap/provider-phase.js +183 -0
  70. package/dist/core/bootstrap/session-phase.js +204 -0
  71. package/dist/core/bootstrap/state.js +94 -0
  72. package/dist/core/bootstrap/workspace-phase.js +115 -0
  73. package/dist/core/bootstrap.js +92 -660
  74. package/dist/core/crash-handler.js +17 -7
  75. package/dist/core/environment.js +37 -12
  76. package/dist/core/host-plan.js +43 -0
  77. package/dist/core/host.js +23 -0
  78. package/dist/core/index.js +1 -0
  79. package/dist/core/prompt-builder.js +17 -1
  80. package/dist/core/prompt-overflow.js +42 -43
  81. package/dist/core/session-logger.js +122 -30
  82. package/dist/core/system-prompt.js +117 -0
  83. package/dist/core/version.js +6 -4
  84. package/dist/core/web-host.js +34 -0
  85. package/dist/core/workspace.js +19 -15
  86. package/dist/i18n/en.json +106 -21
  87. package/dist/i18n/index.js +37 -3
  88. package/dist/i18n/ru.json +109 -21
  89. package/dist/index.js +7 -3
  90. package/dist/llm/image-utils.js +55 -64
  91. package/dist/llm/llm-errors.js +9 -0
  92. package/dist/llm/model-loader.js +10 -32
  93. package/dist/llm/models-dev.js +121 -0
  94. package/dist/llm/openai-compat.js +146 -73
  95. package/dist/llm/orchestrator.js +33 -11
  96. package/dist/llm/provider-budget.js +2 -2
  97. package/dist/llm/provider-context.js +233 -0
  98. package/dist/llm/provider.js +1 -1
  99. package/dist/llm/response.js +5 -0
  100. package/dist/llm/token-counter.js +27 -11
  101. package/dist/logger/app-logger.js +48 -22
  102. package/dist/logger/file-log.js +28 -19
  103. package/dist/logger/index.js +1 -0
  104. package/dist/main.js +24285 -22447
  105. package/dist/migration/backup.js +14 -3
  106. package/dist/migration/detect.js +12 -3
  107. package/dist/migration/index.js +1 -0
  108. package/dist/modules/artifacts/store.js +9 -8
  109. package/dist/modules/browser/actions.js +6 -0
  110. package/dist/modules/browser/bridge-client.js +14 -12
  111. package/dist/modules/browser/bridge-path.js +4 -4
  112. package/dist/modules/browser/console-buffer.js +37 -0
  113. package/dist/modules/browser/cookie-store.js +5 -0
  114. package/dist/modules/browser/driver-types.js +1 -0
  115. package/dist/modules/browser/driver.js +19 -11
  116. package/dist/modules/browser/index.js +3 -1
  117. package/dist/modules/browser/module.js +2 -1
  118. package/dist/modules/browser/session.js +10 -30
  119. package/dist/modules/browser/snapshot.js +19 -13
  120. package/dist/modules/browser/types.js +1 -0
  121. package/dist/modules/certification/cli.js +28 -20
  122. package/dist/modules/certification/fact-checker.js +9 -1
  123. package/dist/modules/certification/loader.js +9 -2
  124. package/dist/modules/certification/manifest.js +83 -21
  125. package/dist/modules/certification/runner.js +23 -13
  126. package/dist/modules/certification/scenarios.js +20 -20
  127. package/dist/modules/certification/syntax-scenarios.js +1 -2
  128. package/dist/modules/context/chunk-query.js +11 -9
  129. package/dist/modules/context/fact-extractor.js +37 -39
  130. package/dist/modules/context/index.js +1 -0
  131. package/dist/modules/context/manager.js +106 -80
  132. package/dist/modules/critic/client.js +176 -0
  133. package/dist/modules/critic/evidence.js +81 -0
  134. package/dist/modules/critic/provider.js +45 -0
  135. package/dist/modules/critic/tools.js +78 -0
  136. package/dist/modules/critic/types.js +1 -0
  137. package/dist/modules/execution/audit-runners.js +28 -34
  138. package/dist/modules/execution/auditor.js +53 -49
  139. package/dist/modules/execution/execution-plugin.js +142 -102
  140. package/dist/modules/execution/index.js +1 -1
  141. package/dist/modules/execution/input-from.js +37 -13
  142. package/dist/modules/execution/module.js +368 -191
  143. package/dist/modules/execution/moe-executor.js +53 -26
  144. package/dist/modules/execution/plan-actions.js +320 -202
  145. package/dist/modules/execution/plan-checks.js +35 -0
  146. package/dist/modules/execution/plan-coverage.js +36 -33
  147. package/dist/modules/execution/plan-steps.js +11 -3
  148. package/dist/modules/execution/plan-store.js +83 -17
  149. package/dist/modules/execution/plan-tool.js +89 -35
  150. package/dist/modules/execution/plan-validator.js +30 -10
  151. package/dist/modules/execution/planner.js +22 -13
  152. package/dist/modules/execution/stuck-detector.js +196 -93
  153. package/dist/modules/execution/tracker.js +22 -8
  154. package/dist/modules/execution/verifier.js +69 -48
  155. package/dist/modules/execution/windows-commands.js +12 -12
  156. package/dist/modules/hallucination/artifact-claim.js +93 -0
  157. package/dist/modules/hallucination/confidence.js +13 -11
  158. package/dist/modules/hallucination/consistency.js +10 -2
  159. package/dist/modules/hallucination/detector.js +10 -5
  160. package/dist/modules/hallucination/factual.js +43 -26
  161. package/dist/modules/hallucination/index.js +1 -0
  162. package/dist/modules/hallucination/js-identifiers.js +44 -45
  163. package/dist/modules/hallucination/llm-judge.js +17 -12
  164. package/dist/modules/hallucination/success-claim.js +59 -0
  165. package/dist/modules/indexer/cache.js +52 -4
  166. package/dist/modules/indexer/index.js +1 -0
  167. package/dist/modules/indexer/map-command.js +2 -4
  168. package/dist/modules/indexer/map-select.js +16 -16
  169. package/dist/modules/indexer/module.js +47 -12
  170. package/dist/modules/indexer/project-profile.js +8 -7
  171. package/dist/modules/indexer/symbols.js +15 -15
  172. package/dist/modules/indexer/walker.js +159 -29
  173. package/dist/modules/lsp/check-tool.js +7 -5
  174. package/dist/modules/lsp/client.js +39 -39
  175. package/dist/modules/lsp/command.js +17 -24
  176. package/dist/modules/lsp/config.js +12 -11
  177. package/dist/modules/lsp/index.js +1 -0
  178. package/dist/modules/lsp/module.js +48 -46
  179. package/dist/modules/lsp/probe.js +13 -14
  180. package/dist/modules/lsp/project-root.js +14 -17
  181. package/dist/modules/lsp/startup-check.js +18 -16
  182. package/dist/modules/mcp/client.js +59 -62
  183. package/dist/modules/mcp/index.js +1 -0
  184. package/dist/modules/mcp/module.js +66 -37
  185. package/dist/modules/mcp/registry.js +5 -0
  186. package/dist/modules/memory/index.js +1 -0
  187. package/dist/modules/memory/module.js +12 -8
  188. package/dist/modules/memory/search.js +4 -1
  189. package/dist/modules/memory/store.js +40 -2
  190. package/dist/modules/onboarding/init.js +47 -0
  191. package/dist/modules/onboarding/meet.js +30 -0
  192. package/dist/modules/onboarding/mode.js +30 -0
  193. package/dist/modules/onboarding/prompts.js +43 -0
  194. package/dist/modules/pipelines/engine.js +10 -0
  195. package/dist/modules/pipelines/index.js +1 -0
  196. package/dist/modules/pipelines/parser.js +4 -1
  197. package/dist/modules/pipelines/template.js +4 -0
  198. package/dist/modules/plugins/builtin/lint-on-write.js +80 -67
  199. package/dist/modules/plugins/builtin/notify.js +2 -0
  200. package/dist/modules/plugins/index.js +1 -0
  201. package/dist/modules/plugins/loader.js +18 -3
  202. package/dist/modules/plugins/manager.js +37 -17
  203. package/dist/modules/pricing/index.js +10 -11
  204. package/dist/modules/pricing/prices.js +12 -12
  205. package/dist/modules/processes/index.js +1 -0
  206. package/dist/modules/processes/kill-tree.js +27 -31
  207. package/dist/modules/processes/registry.js +44 -25
  208. package/dist/modules/processes/runner.js +4 -4
  209. package/dist/modules/providers/capabilities.js +28 -0
  210. package/dist/modules/providers/create.js +3 -4
  211. package/dist/modules/providers/factory.js +7 -7
  212. package/dist/modules/providers/fallback.js +12 -12
  213. package/dist/modules/providers/health.js +4 -4
  214. package/dist/modules/providers/manager.js +50 -19
  215. package/dist/modules/providers/naming.js +15 -0
  216. package/dist/modules/providers/preflight.js +71 -0
  217. package/dist/modules/providers/presets.js +39 -8
  218. package/dist/modules/providers/registry.js +7 -2
  219. package/dist/modules/reasoning/policy.js +8 -6
  220. package/dist/modules/reasoning/probe.js +30 -25
  221. package/dist/modules/security/audit-log.js +19 -36
  222. package/dist/modules/security/audit-notifier.js +35 -75
  223. package/dist/modules/security/command-validator.js +99 -51
  224. package/dist/modules/security/content-scanner.js +8 -8
  225. package/dist/modules/security/data-sanitizer.js +9 -16
  226. package/dist/modules/security/encryption.js +32 -52
  227. package/dist/modules/security/index.js +2 -1
  228. package/dist/modules/security/network-validator.js +120 -43
  229. package/dist/modules/security/path-validator.js +61 -46
  230. package/dist/modules/security/protected-paths.js +73 -0
  231. package/dist/modules/security/rate-limiter.js +17 -49
  232. package/dist/modules/security/security-policies.js +25 -57
  233. package/dist/modules/security/session-encryption.js +32 -71
  234. package/dist/modules/security/session-isolation.js +15 -28
  235. package/dist/modules/session/auto-name.js +63 -0
  236. package/dist/modules/session/export.js +98 -0
  237. package/dist/modules/session/index.js +1 -0
  238. package/dist/modules/session/manager.js +79 -28
  239. package/dist/modules/session/module.js +4 -3
  240. package/dist/modules/session/store.js +57 -20
  241. package/dist/modules/setup/answers.js +35 -0
  242. package/dist/modules/setup/apply.js +79 -0
  243. package/dist/modules/setup/discovery.js +72 -0
  244. package/dist/modules/skills/index.js +1 -0
  245. package/dist/modules/skills/loader.js +9 -1
  246. package/dist/modules/skills/module.js +11 -0
  247. package/dist/modules/updater/changelog-reader.js +15 -16
  248. package/dist/modules/updater/checker.js +9 -4
  249. package/dist/modules/updater/dev-detect.js +7 -6
  250. package/dist/modules/updater/index.js +1 -0
  251. package/dist/modules/updater/module.js +17 -15
  252. package/dist/modules/user-profile/compressor.js +2 -0
  253. package/dist/modules/user-profile/index.js +1 -0
  254. package/dist/modules/user-profile/profile.js +10 -1
  255. package/dist/modules/webui/answer-api.js +29 -0
  256. package/dist/modules/webui/assets.js +50 -0
  257. package/dist/modules/webui/body.js +21 -0
  258. package/dist/modules/webui/commands-settings.js +209 -0
  259. package/dist/modules/webui/commands-shared.js +60 -0
  260. package/dist/modules/webui/commands.js +174 -0
  261. package/dist/modules/webui/config-api.js +134 -0
  262. package/dist/modules/webui/file-api.js +202 -0
  263. package/dist/modules/webui/forward.js +159 -0
  264. package/dist/modules/webui/history-api.js +112 -0
  265. package/dist/modules/webui/host-state.js +60 -0
  266. package/dist/modules/webui/i18n-api.js +9 -0
  267. package/dist/modules/webui/limits.js +6 -0
  268. package/dist/modules/webui/log-forward.js +20 -0
  269. package/dist/modules/webui/memory-commands.js +64 -0
  270. package/dist/modules/webui/model-cache.js +18 -0
  271. package/dist/modules/webui/module.js +148 -0
  272. package/dist/modules/webui/persist.js +24 -0
  273. package/dist/modules/webui/prompt-io.js +97 -0
  274. package/dist/modules/webui/providers-api.js +337 -0
  275. package/dist/modules/webui/providers-view.js +56 -0
  276. package/dist/modules/webui/reasoning-view.js +15 -0
  277. package/dist/modules/webui/run-agent.js +81 -0
  278. package/dist/modules/webui/server.js +284 -0
  279. package/dist/modules/webui/sessions-api.js +178 -0
  280. package/dist/modules/webui/settings-api.js +561 -0
  281. package/dist/modules/webui/setup-api.js +201 -0
  282. package/dist/modules/webui/state.js +74 -0
  283. package/dist/modules/webui/util.js +261 -0
  284. package/dist/output/bus.js +7 -2
  285. package/dist/output/channel.js +80 -28
  286. package/dist/output/format.js +2 -1
  287. package/dist/output/index.js +1 -0
  288. package/dist/output/json-sink.js +5 -3
  289. package/dist/output/machine.js +4 -3
  290. package/dist/output/session-sink.js +4 -4
  291. package/dist/tools/approve.js +4 -0
  292. package/dist/tools/attach-image.js +5 -4
  293. package/dist/tools/auto-fixer.js +26 -44
  294. package/dist/tools/auto-tags.js +83 -0
  295. package/dist/tools/bash.js +193 -116
  296. package/dist/tools/browser.js +76 -31
  297. package/dist/tools/chunk-query.js +14 -6
  298. package/dist/tools/create-dir.js +9 -5
  299. package/dist/tools/delete-file.js +9 -5
  300. package/dist/tools/download-file.js +32 -7
  301. package/dist/tools/edit-file.js +52 -14
  302. package/dist/tools/enable-tools.js +6 -6
  303. package/dist/tools/executor.js +132 -27
  304. package/dist/tools/file-info.js +3 -1
  305. package/dist/tools/filter-tools.js +3 -3
  306. package/dist/tools/glob-tool.js +4 -1
  307. package/dist/tools/grep-tool.js +13 -10
  308. package/dist/tools/hidden-tools-block.js +6 -6
  309. package/dist/tools/index.js +16 -8
  310. package/dist/tools/list-dir.js +4 -2
  311. package/dist/tools/load-skill.js +7 -0
  312. package/dist/tools/mcp-call.js +15 -4
  313. package/dist/tools/meet-done.js +14 -0
  314. package/dist/tools/move-file.js +14 -7
  315. package/dist/tools/path-utils.js +29 -29
  316. package/dist/tools/pipeline-run.js +29 -7
  317. package/dist/tools/preview.js +1 -1
  318. package/dist/tools/process-ids.js +31 -0
  319. package/dist/tools/process-kill.js +9 -16
  320. package/dist/tools/process-list.js +1 -0
  321. package/dist/tools/process-log.js +11 -16
  322. package/dist/tools/process-wait.js +63 -0
  323. package/dist/tools/prompt-io.js +4 -3
  324. package/dist/tools/question.js +172 -25
  325. package/dist/tools/read-file.js +55 -34
  326. package/dist/tools/recall.js +14 -8
  327. package/dist/tools/registry.js +6 -5
  328. package/dist/tools/remember.js +8 -4
  329. package/dist/tools/scope-request.js +11 -6
  330. package/dist/tools/search-history.js +5 -2
  331. package/dist/tools/session-info.js +2 -0
  332. package/dist/tools/set-thinking.js +8 -4
  333. package/dist/tools/subagent.js +59 -39
  334. package/dist/tools/syntax-validator.js +292 -90
  335. package/dist/tools/user-input.js +22 -18
  336. package/dist/tools/web-browse.js +9 -3
  337. package/dist/tools/web-fetch.js +12 -23
  338. package/dist/tools/web-search.js +7 -5
  339. package/dist/tools/write-file.js +45 -15
  340. package/dist/ui/action-log.js +13 -0
  341. package/dist/ui/box.js +4 -3
  342. package/dist/ui/colors.js +2 -0
  343. package/dist/ui/diff.js +6 -5
  344. package/dist/ui/index.js +1 -0
  345. package/dist/ui/line-editor.js +230 -122
  346. package/dist/ui/line-math.js +10 -2
  347. package/dist/ui/md-formatter.js +57 -37
  348. package/dist/ui/output.js +8 -3
  349. package/dist/ui/plan-view.js +21 -10
  350. package/dist/ui/renderer.js +393 -67
  351. package/dist/ui/spinner.js +16 -5
  352. package/dist/ui/table.js +15 -17
  353. package/dist/ui/theme.js +143 -0
  354. package/dist/utils/child-run.js +20 -0
  355. package/dist/utils/error.js +1 -1
  356. package/dist/utils/index.js +2 -0
  357. package/dist/utils/retry.js +2 -2
  358. package/dist/utils/truncate.js +1 -1
  359. package/package.json +1 -1
@@ -1,7 +1,7 @@
1
1
  import { sanitizeLogMessage } from "./data-sanitizer";
2
2
  /**
3
- * Parse a numeric IP component honoring decimal, hex (0x…) and octal (0…)
4
- * notations. Returns null when the token is not purely numeric.
3
+ * Разобрать числовой компонент IP с учётом decimal, hex (0x…) и octal (0…).
4
+ * Не числовой токен — null.
5
5
  */
6
6
  function parseIpNumber(s) {
7
7
  if (/^0x[0-9a-f]+$/.test(s))
@@ -12,9 +12,9 @@ function parseIpNumber(s) {
12
12
  return parseInt(s, 10);
13
13
  return null;
14
14
  }
15
- /** Check whether four decoded octets form a private/reserved IPv4 address. */
15
+ /** Образуют ли четыре октета приватный/резервный IPv4. */
16
16
  function isPrivateIpv4(o) {
17
- const [a, b = 0, c = 0, d = 0] = o;
17
+ const [a, b = 0, _c = 0, _d = 0] = o;
18
18
  return (a === 0 ||
19
19
  a === 10 ||
20
20
  a === 127 ||
@@ -23,55 +23,95 @@ function isPrivateIpv4(o) {
23
23
  (a === 192 && b === 168));
24
24
  }
25
25
  /**
26
- * Check if a hostname is a private/link-local address (SSRF protection).
27
- *
28
- * Handles more than just dotted quads: bare integer / hex / octal encodings
29
- * (every HTTP client decodes them as a 32-bit IPv4), shorthand quads
30
- * ("127.1" → 127.0.0.1) and IPv6-mapped IPv4 ("::ffff:7f00:1").
26
+ * Переписать хвостовой dotted-quad (IPv4-mapped литерал, напр. `::ffff:127.0.0.1`)
27
+ * двумя hex-группами, чтобы общий парсер IPv6 его разобрал.
28
+ */
29
+ function normalizeIpv4Tail(input) {
30
+ const idx = input.lastIndexOf(":");
31
+ const tail = idx >= 0 ? input.slice(idx + 1) : input;
32
+ if (!tail.includes("."))
33
+ return input;
34
+ const oct = tail.split(".").map((d) => parseInt(d, 10));
35
+ if (oct.length !== 4 || oct.some((n) => Number.isNaN(n) || n < 0 || n > 255))
36
+ return null;
37
+ const hi = (((oct[0] << 8) | oct[1]) & 0xffff).toString(16);
38
+ const lo = (((oct[2] << 8) | oct[3]) & 0xffff).toString(16);
39
+ return `${idx >= 0 ? input.slice(0, idx + 1) : ""}${hi}:${lo}`;
40
+ }
41
+ /** Развернуть IPv6-литерал (без зоны) в 8 групп; кривой — null. */
42
+ function expandIpv6(input) {
43
+ const normalized = normalizeIpv4Tail(input.split("%")[0]);
44
+ if (normalized === null)
45
+ return null;
46
+ const h = normalized;
47
+ const parse = (s) => s ? s.split(":").filter((x) => x !== "").map((x) => parseInt(x, 16)) : [];
48
+ const [head, tail] = h.split("::");
49
+ let groups;
50
+ if (tail !== undefined) {
51
+ const a = parse(head);
52
+ const b = parse(tail);
53
+ const zeros = 8 - a.length - b.length;
54
+ if (zeros < 0)
55
+ return null;
56
+ groups = [...a, ...new Array(zeros).fill(0), ...b];
57
+ }
58
+ else {
59
+ groups = parse(h);
60
+ }
61
+ if (groups.length !== 8 || groups.some((g) => Number.isNaN(g) || g < 0 || g > 0xffff)) {
62
+ return null;
63
+ }
64
+ return groups;
65
+ }
66
+ /**
67
+ * Приватный/link-local адрес в hostname (SSRF-защита) или нет.
68
+ * Покрывает не только dotted-quad: голые int/hex/octal-кодировки
69
+ * (любой HTTP-клиент декодит их как 32-битный IPv4), короткие квады
70
+ * ("127.1" → 127.0.0.1) и IPv6 — loopback/unspecified/link-local/
71
+ * unique-local и IPv4-mapped в любой форме раскрытия (старые строковые
72
+ * префиксы пропускали `0:0:0:0:0:0:0:1` и `::ffff:0:7f00:1`).
31
73
  */
32
74
  export function isIpPrivate(hostname) {
33
- let h = hostname.toLowerCase().trim().replace(/^\[|\]$/g, "");
75
+ const h = hostname.toLowerCase().trim().replace(/^\[|\]$/g, "");
34
76
  if (h.includes(":")) {
35
- // IPv6-mapped IPv4 (::ffff:127.0.0.1, ::ffff:7f00:1) — decode and fall
36
- // through to the IPv4 checks below.
37
- if (h.startsWith("::ffff:")) {
38
- const mapped = h.slice(7);
39
- if (!mapped.includes(":")) {
40
- h = mapped; // dotted quad — fall through
41
- }
42
- else {
43
- // Hex-colon encoding of the low 32 bits, e.g. "7f00:1".
44
- const segs = mapped.split(":");
45
- const hi = parseInt(segs[0], 16);
46
- const lo = parseInt(segs[1] ?? "", 16);
47
- if (segs.length !== 2 || Number.isNaN(hi) || Number.isNaN(lo))
48
- return false;
49
- return isPrivateIpv4([(hi >> 8) & 255, hi & 255, (lo >> 8) & 255, lo & 255]);
50
- }
51
- }
52
- else {
53
- return h === "::1" || /^(fc|fd)/.test(h) || h.startsWith("fe80");
77
+ const g = expandIpv6(h);
78
+ if (!g)
79
+ return false;
80
+ // IPv4-mapped/translated (::ffff:a.b.c.d) в любой форме с нулями: старшие
81
+ // 96 бит — нули/ffff, младшие 32 — приватный IPv4.
82
+ const high = g.slice(0, 6);
83
+ if (high.some((n) => n === 0xffff) &&
84
+ high.every((n) => n === 0 || n === 0xffff) &&
85
+ isPrivateIpv4([(g[6] >> 8) & 255, g[6] & 255, (g[7] >> 8) & 255, g[7] & 255])) {
86
+ return true;
54
87
  }
88
+ if (g.slice(0, 7).every((n) => n === 0) && g[7] <= 1)
89
+ return true; // :: and ::1
90
+ if ((g[0] & 0xffc0) === 0xfe80)
91
+ return true; // fe80::/10 link-local
92
+ if ((g[0] & 0xfe00) === 0xfc00)
93
+ return true; // fc00::/7 unique-local
94
+ return false;
55
95
  }
56
96
  if (h === "localhost")
57
97
  return true;
58
- // Bare integer/hex/octal: interpreted as one 32-bit IPv4 word.
98
+ // Голый int/hex/octal: интерпретируется как одно 32-битное слово IPv4.
59
99
  if (!h.includes(".")) {
60
100
  const n = parseIpNumber(h);
61
101
  if (n === null)
62
- return false; // a domain name, not an IP
102
+ return false; // доменное имя, не IP
63
103
  if (n < 0 || n > 0xffffffff)
64
104
  return false;
65
105
  return isPrivateIpv4([(n >>> 24) & 255, (n >>> 16) & 255, (n >>> 8) & 255, n & 255]);
66
106
  }
67
- // Dotted form: decode each part (hex/octal aware); shorthand like
68
- // "127.1" pads the missing groups before the last.
107
+ // Точечная форма: декодим каждую часть (hex/octal-aware); короткие вроде
108
+ // "127.1" добивают пропущенные группы нулями перед последней.
69
109
  const parts = h.split(".");
70
110
  const nums = [];
71
111
  for (let i = 0; i < parts.length; i++) {
72
112
  const n = parseIpNumber(parts[i]);
73
113
  if (n === null || n > 255)
74
- return false; // domain or malformed — not our concern
114
+ return false; // домен или мусор — не наша забота
75
115
  nums.push(n);
76
116
  }
77
117
  while (nums.length < 4)
@@ -80,11 +120,9 @@ export function isIpPrivate(hostname) {
80
120
  return false;
81
121
  return isPrivateIpv4(nums);
82
122
  }
83
- /**
84
- * Check if a URL is allowed based on security configuration
85
- */
123
+ /** Разрешён ли URL по конфигу (схема, SSRF, deny/allow-домены). */
86
124
  export function isUrlAllowed(url, securityConfig) {
87
- // If security is disabled, allow everything
125
+ // Выключенная защита разрешает всё
88
126
  if (!securityConfig?.enabled) {
89
127
  return { allowed: true };
90
128
  }
@@ -98,7 +136,7 @@ export function isUrlAllowed(url, securityConfig) {
98
136
  if (isIpPrivate(domain)) {
99
137
  return { allowed: false, reason: `Private address "${domain}" is blocked (SSRF)` };
100
138
  }
101
- // Check denied domains first
139
+ // Сначала denied-домены
102
140
  if (securityConfig.deniedDomains?.length) {
103
141
  for (const deniedDomain of securityConfig.deniedDomains) {
104
142
  const denied = deniedDomain.toLowerCase();
@@ -107,7 +145,7 @@ export function isUrlAllowed(url, securityConfig) {
107
145
  }
108
146
  }
109
147
  }
110
- // Check allowed domains (if non-empty, only these are allowed)
148
+ // Затем allowed-домены (непустой — разрешены только перечисленные)
111
149
  if (securityConfig.allowedDomains?.length) {
112
150
  let isAllowed = false;
113
151
  for (const allowedDomain of securityConfig.allowedDomains) {
@@ -130,9 +168,48 @@ export function isUrlAllowed(url, securityConfig) {
130
168
  return { allowed: false, reason: `Invalid URL: ${url}` };
131
169
  }
132
170
  }
171
+ /** Максимум редиректов, которые проходит (и ревалидирует) {@link fetchValidated}. */
172
+ export const MAX_REDIRECTS = 5;
173
+ /** Бросает {@link fetchValidated}, когда хоп запрещён политикой. */
174
+ export class BlockedRedirectError extends Error {
175
+ reason;
176
+ constructor(reason) {
177
+ super(`redirect blocked: ${reason}`);
178
+ this.reason = reason;
179
+ this.name = "BlockedRedirectError";
180
+ }
181
+ }
133
182
  /**
134
- * Sanitize URL for logging (remove sensitive information)
183
+ * Fetch URL с РУЧНЫМИ редиректами, ревалидируя каждый хоп против SSRF/доменной
184
+ * политики. Обычный `fetch` с `redirect: "follow"` проверяет только стартовый
185
+ * URL, и публичный URL с 302 на `127.0.0.1` или metadata-эндпоинт облака
186
+ * дотягивается до внутреннего сервиса. Запрещённый хоп или превышение лимита —
187
+ * {@link BlockedRedirectError}.
135
188
  */
189
+ export async function fetchValidated(url, securityConfig, init = {}) {
190
+ let current = url;
191
+ for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
192
+ const check = isUrlAllowed(current, securityConfig);
193
+ if (!check.allowed)
194
+ throw new BlockedRedirectError(check.reason || "URL not allowed");
195
+ const response = await fetch(current, { ...init, redirect: "manual" });
196
+ if (response.status >= 300 && response.status < 400) {
197
+ const location = response.headers.get("location");
198
+ if (!location)
199
+ return { response, finalUrl: current };
200
+ try {
201
+ current = new URL(location, current).toString();
202
+ }
203
+ catch {
204
+ throw new BlockedRedirectError("invalid redirect location");
205
+ }
206
+ continue;
207
+ }
208
+ return { response, finalUrl: current };
209
+ }
210
+ throw new BlockedRedirectError("too many redirects");
211
+ }
212
+ /** Санитизация URL для логов (вырезание секретов). */
136
213
  export function sanitizeUrl(url) {
137
214
  try {
138
215
  const urlObj = new URL(url);
@@ -145,11 +222,11 @@ export function sanitizeUrl(url) {
145
222
  }
146
223
  urlObj.search = sanitizedParams.toString();
147
224
  let result = urlObj.toString();
148
- // Also sanitize credentials in URL (user:pass@host)
225
+ // Плюс креды в URL (user:pass@host)
149
226
  if (urlObj.username || urlObj.password) {
150
227
  result = result.replace(/:\/\/[^:]+:[^@]+@/, "://[REDACTED]:[REDACTED]@");
151
228
  }
152
- // Decode URL-encoded [REDACTED] back to plain text
229
+ // Декодим URL-encoded [REDACTED] обратно в plain text
153
230
  result = decodeURIComponent(result);
154
231
  return result;
155
232
  }
@@ -1,39 +1,48 @@
1
- import { resolve, normalize } from "path";
1
+ // Валидация путей: джейл baseDir, симлинки, deny/allow-паттерны, скоуп суб-агента.
2
+ import { resolve, normalize, dirname } from "path";
2
3
  import { realpathSync } from "fs";
3
4
  import { isInsideDir, matchesScopeEntry, toForwardSlash } from "../../tools/path-utils";
4
5
  /**
5
- * Symlink containment: resolve() does NOT follow symlinks, so a symlink
6
- * inside baseDir can point outside it. When the target exists on disk its
7
- * real path must also stay inside the (real) base directory. Returns true if
8
- * the check cannot be performed (target missing) — creation is validated by
9
- * the resolved path itself.
6
+ * Симлинк-контейнмент: resolve() симлинки НЕ резолвит, и симлинк
7
+ * (или Windows-junction) внутри baseDir может указывать наружу.
8
+ * Существующая цель обязана остаться внутри реального base. Для ещё не
9
+ * созданного листа каноникализируется ближайший СУЩЕСТВУЮЩИЙ предок —
10
+ * иначе `base/link/new.txt` проскочил бы, когда `link` смотрит наружу.
11
+ * False (судить нельзя), если нет самого baseDir: чисто лексическая
12
+ * проверка продолжает работать на виртуальных путях.
10
13
  */
11
14
  function escapesViaSymlink(baseResolved, targetResolved) {
15
+ let realBase;
12
16
  try {
13
- const realTarget = realpathSync(targetResolved);
14
- let realBase = baseResolved;
17
+ realBase = realpathSync(baseResolved);
18
+ }
19
+ catch {
20
+ // baseDir нет (виртуальный путь / фикстура) — каноникализировать нечего.
21
+ return false;
22
+ }
23
+ let probe = targetResolved;
24
+ for (;;) {
15
25
  try {
16
- realBase = realpathSync(baseResolved);
26
+ const real = realpathSync(probe);
27
+ return real !== realBase && !isInsideDir(real, realBase);
17
28
  }
18
29
  catch {
19
- // Keep the resolved baseDir when it cannot be canonicalized.
30
+ const parent = dirname(probe);
31
+ if (parent === probe)
32
+ return false; // корень, ничего не существует
33
+ probe = parent;
20
34
  }
21
- return !isInsideDir(realTarget, realBase);
22
- }
23
- catch {
24
- // ENOENT etc — target does not exist yet, nothing to follow.
25
- return false;
26
35
  }
27
36
  }
28
37
  /**
29
- * Check if a path is within the allowed scope
30
- * This combines global security settings with per-tool scope
38
+ * Путь в разрешённом скоупе или нет.
39
+ * Комбинирует глобальные настройки и per-tool скоуп.
31
40
  */
32
41
  export function isPathInScope(baseDir, targetPath, scope, securityConfig) {
33
42
  const baseResolved = resolve(baseDir);
34
43
  const targetResolved = resolve(baseDir, normalize(targetPath));
35
- // First, check that path is within baseDir (separator- and case-safe: a
36
- // sibling directory sharing the prefix must not pass).
44
+ // Сначала — внутри baseDir (сепараторо- и регистро-безопасно: соседний
45
+ // каталог с общим префиксом пройти не должен).
37
46
  if (!isInsideDir(targetResolved, baseResolved)) {
38
47
  return {
39
48
  allowed: false,
@@ -46,9 +55,9 @@ export function isPathInScope(baseDir, targetPath, scope, securityConfig) {
46
55
  reason: "Path resolves outside the base working directory via symlink",
47
56
  };
48
57
  }
49
- // If security is disabled, skip pattern validation
58
+ // Выключенная защита пропускает паттерны
50
59
  if (!securityConfig?.enabled) {
51
- // Still enforce subagent scope if present
60
+ // Скоуп суб-агента действует всё равно
52
61
  if (scope) {
53
62
  const isReadScope = scope.read_only_files.some((f) => matchesScopeEntry(targetResolved, resolve(baseDir, f)));
54
63
  const isWriteScope = scope.allowed_files.some((f) => matchesScopeEntry(targetResolved, resolve(baseDir, f)));
@@ -63,7 +72,7 @@ export function isPathInScope(baseDir, targetPath, scope, securityConfig) {
63
72
  }
64
73
  return { allowed: true };
65
74
  }
66
- // Check global denied patterns
75
+ // Глобальные denied-паттерны
67
76
  if (securityConfig?.denied?.length) {
68
77
  for (const pattern of securityConfig.denied) {
69
78
  if (matchesGlobPattern(targetResolved, baseDir, pattern)) {
@@ -74,7 +83,7 @@ export function isPathInScope(baseDir, targetPath, scope, securityConfig) {
74
83
  }
75
84
  }
76
85
  }
77
- // Check global allowed patterns (if non-empty, only these are allowed)
86
+ // Глобальные allowed-паттерны (непустые — разрешены только совпавшие)
78
87
  if (securityConfig?.allowed?.length) {
79
88
  let isAllowed = false;
80
89
  for (const pattern of securityConfig.allowed) {
@@ -90,7 +99,7 @@ export function isPathInScope(baseDir, targetPath, scope, securityConfig) {
90
99
  };
91
100
  }
92
101
  }
93
- // Check tool-specific scope (for subagents)
102
+ // Per-tool скоуп (для суб-агентов)
94
103
  if (!scope) {
95
104
  return { allowed: true };
96
105
  }
@@ -105,23 +114,29 @@ export function isPathInScope(baseDir, targetPath, scope, securityConfig) {
105
114
  reason: "Path is not within the allowed scope for this sub-agent",
106
115
  };
107
116
  }
108
- /**
109
- * Check if a path is writable (for write/delete operations)
110
- */
117
+ /** Путь доступен для записи (операции write/delete) или нет. */
111
118
  export function isPathWritable(baseDir, targetPath, scope, securityConfig) {
112
119
  const baseResolved = resolve(baseDir);
113
120
  const targetResolved = resolve(baseDir, normalize(targetPath));
114
- // First, check that path is within baseDir (separator- and case-safe: a
115
- // sibling directory sharing the prefix must not pass).
121
+ // Сначала — внутри baseDir (сепараторо- и регистро-безопасно: соседний
122
+ // каталог с общим префиксом пройти не должен).
116
123
  if (!isInsideDir(targetResolved, baseResolved)) {
117
124
  return {
118
125
  allowed: false,
119
126
  reason: "Path is outside the base working directory",
120
127
  };
121
128
  }
122
- // If security is disabled, skip pattern validation
129
+ // Запись не следует за симлинком/junction наружу из baseDir (путь чтения
130
+ // проверял это всегда; путь записи раньше пропускал).
131
+ if (escapesViaSymlink(baseResolved, targetResolved)) {
132
+ return {
133
+ allowed: false,
134
+ reason: "Path resolves outside the base working directory via symlink",
135
+ };
136
+ }
137
+ // Выключенная защита пропускает паттерны
123
138
  if (!securityConfig?.enabled) {
124
- // Still enforce subagent scope if present
139
+ // Скоуп суб-агента действует всё равно
125
140
  if (scope) {
126
141
  const isWriteScope = scope.allowed_files.some((f) => matchesScopeEntry(targetResolved, resolve(baseDir, f)));
127
142
  if (isWriteScope)
@@ -133,7 +148,7 @@ export function isPathWritable(baseDir, targetPath, scope, securityConfig) {
133
148
  }
134
149
  return { allowed: true };
135
150
  }
136
- // Check global denied patterns
151
+ // Глобальные denied-паттерны
137
152
  if (securityConfig?.denied?.length) {
138
153
  for (const pattern of securityConfig.denied) {
139
154
  if (matchesGlobPattern(targetResolved, baseDir, pattern)) {
@@ -144,7 +159,7 @@ export function isPathWritable(baseDir, targetPath, scope, securityConfig) {
144
159
  }
145
160
  }
146
161
  }
147
- // Check global allowed patterns (if non-empty, only these are allowed)
162
+ // Глобальные allowed-паттерны (непустые — разрешены только совпавшие)
148
163
  if (securityConfig?.allowed?.length) {
149
164
  let isAllowed = false;
150
165
  for (const pattern of securityConfig.allowed) {
@@ -160,7 +175,7 @@ export function isPathWritable(baseDir, targetPath, scope, securityConfig) {
160
175
  };
161
176
  }
162
177
  }
163
- // Check tool-specific scope (for subagents)
178
+ // Per-tool скоуп (для суб-агентов)
164
179
  if (!scope) {
165
180
  return { allowed: true };
166
181
  }
@@ -173,24 +188,24 @@ export function isPathWritable(baseDir, targetPath, scope, securityConfig) {
173
188
  };
174
189
  }
175
190
  /**
176
- * Simple glob pattern matching (supports * and **)
177
- * This is a simplified version that handles common cases
191
+ * Простой glob-матчинг (поддерживает * и **).
192
+ * Упрощённая версия для типовых случаев.
178
193
  */
179
194
  function matchesGlobPattern(targetPath, baseDir, pattern) {
180
195
  const resolvedBaseDir = resolve(baseDir);
181
196
  const resolvedTargetPath = resolve(targetPath);
182
- // Relative target path used for segment matching
197
+ // Относительный путь для посегментного матчинга
183
198
  const relPath = resolvedTargetPath.startsWith(resolvedBaseDir)
184
199
  ? resolvedTargetPath.slice(resolvedBaseDir.length).replace(/^\//, "")
185
200
  : resolvedTargetPath;
186
- // Handle patterns with ** (recursive directory matching)
201
+ // Паттерны с ** (рекурсивный матчинг каталогов)
187
202
  if (pattern.includes("**")) {
188
- // Split on ** to get prefix/suffix segments
203
+ // Разбить по ** на префикс/суффикс
189
204
  const cleaned = pattern.replace(/\/$/, "");
190
205
  const [rawPrefix, rawSuffix] = cleaned.split("**");
191
206
  const prefix = rawPrefix.replace(/\/$/, "");
192
207
  const suffix = rawSuffix.replace(/^\//, "");
193
- // Check prefix: everything before ** must match the start of the path
208
+ // Префикс: всё до ** обязано совпасть с началом пути
194
209
  if (prefix) {
195
210
  const prefixParts = prefix.split("/").filter(Boolean);
196
211
  const relParts = relPath.split("/");
@@ -199,13 +214,13 @@ function matchesGlobPattern(targetPath, baseDir, pattern) {
199
214
  return false;
200
215
  }
201
216
  }
202
- // ** at the end matches everything
217
+ // ** в конце матчит всё
203
218
  if (!suffix)
204
219
  return true;
205
- // Check suffix against remaining path segments
220
+ // Суффикс против оставшихся сегментов
206
221
  const suffixParts = suffix.split("/").filter(Boolean);
207
222
  const relParts = relPath.split("/");
208
- // Search for suffix match anywhere in remaining path
223
+ // Ищем совпадение суффикса в любой позиции оставшегося пути
209
224
  for (let start = (prefix ? prefix.split("/").filter(Boolean).length : 0); start <= relParts.length - suffixParts.length; start++) {
210
225
  let match = true;
211
226
  for (let j = 0; j < suffixParts.length; j++) {
@@ -221,13 +236,13 @@ function matchesGlobPattern(targetPath, baseDir, pattern) {
221
236
  }
222
237
  return false;
223
238
  }
224
- // Handle directory patterns (ending with /) without **
239
+ // Паттерны каталогов (с / на конце) без **
225
240
  if (pattern.endsWith("/")) {
226
241
  const dirPattern = pattern.slice(0, -1);
227
242
  const resolvedDirPattern = resolve(baseDir, dirPattern);
228
243
  return isInsideDir(resolvedTargetPath, resolvedDirPattern);
229
244
  }
230
- // Handle * pattern (single level) without **
245
+ // Паттерн * (один уровень) без **
231
246
  if (pattern.includes("*")) {
232
247
  const regexPattern = pattern
233
248
  .replace(/\*/g, "[^/\\\\]*")
@@ -237,7 +252,7 @@ function matchesGlobPattern(targetPath, baseDir, pattern) {
237
252
  const relativePath = toForwardSlash(resolvedTargetPath.slice(resolvedBaseDir.length + 1));
238
253
  return regex.test(relativePath);
239
254
  }
240
- // Exact match (case-insensitive on win32, where the FS is case-insensitive)
255
+ // Точное совпадение (на win32 регистронезависимо — там FS такая)
241
256
  const exactTarget = resolve(baseDir, pattern);
242
257
  if (process.platform === "win32") {
243
258
  return resolvedTargetPath.toLowerCase() === exactTarget.toLowerCase();
@@ -0,0 +1,73 @@
1
+ // Soft-guard чувствительных путей (.git, .env, ключи): всегда включён, требует подтверждения.
2
+ // Не жёсткий блок — тул возвращает директиву «спроси пользователя».
3
+ import { resolve, relative } from "path";
4
+ import { toForwardSlash } from "../../tools/path-utils";
5
+ import { logSecurityBlock } from "./audit-log";
6
+ import { t } from "../../i18n/index";
7
+ /**
8
+ * Чувствительные пути, требующие явного подтверждения (внутренности VCS + секреты/env).
9
+ * Гард ВСЕГДА ВКЛЮЧЁН, независимо от `security.paths.enabled`, — удаление `.git/config`
10
+ * или перезапись `.env`/ключей необратимы. Не хард-блок: тул возвращает директиву
11
+ * «спроси пользователя и жди».
12
+ */
13
+ const DIR_PATTERNS = [".git", ".hg", ".svn"];
14
+ const EXACT_BASENAMES = [
15
+ ".env",
16
+ ".npmrc",
17
+ ".netrc",
18
+ ".git-credentials",
19
+ ".gitconfig",
20
+ ];
21
+ const ENV_PREFIX = ".env.";
22
+ const EXT_PATTERNS = [
23
+ ".key",
24
+ ".pem",
25
+ ".p12",
26
+ ".pfx",
27
+ ".crt",
28
+ ".cer",
29
+ ".secret",
30
+ ".token",
31
+ ];
32
+ /**
33
+ * Совпавший защищённый паттерн для `targetPath` (относительно `baseDir`)
34
+ * или `undefined` для обычного пути. Сравнение регистронезависимо
35
+ * и не зависит от сепаратора (win32-safe).
36
+ */
37
+ export function matchProtectedPath(baseDir, targetPath) {
38
+ const rel = toForwardSlash(relative(resolve(baseDir), resolve(targetPath))).toLowerCase();
39
+ if (!rel)
40
+ return undefined;
41
+ for (const dir of DIR_PATTERNS) {
42
+ if (rel === dir || rel.endsWith(`/${dir}`) || rel.startsWith(`${dir}/`) || rel.includes(`/${dir}/`)) {
43
+ return `${dir}/`;
44
+ }
45
+ }
46
+ const parts = rel.split("/");
47
+ const base = parts[parts.length - 1] ?? "";
48
+ for (const name of EXACT_BASENAMES) {
49
+ if (base === name)
50
+ return name;
51
+ }
52
+ if (base.startsWith(ENV_PREFIX))
53
+ return ".env.*";
54
+ for (const ext of EXT_PATTERNS) {
55
+ if (base.endsWith(ext))
56
+ return `*${ext}`;
57
+ }
58
+ return undefined;
59
+ }
60
+ /**
61
+ * Гард для мутирующих файловых тулов: отказ «спроси пользователя» + запись в аудит,
62
+ * `undefined` — путь обычный. Не хард-блок: пользователь вправе разрешить.
63
+ */
64
+ export function protectedPathBlock(ctx, targetPath, pathLabel, action = "file_write") {
65
+ const pattern = matchProtectedPath(ctx.baseDir, targetPath);
66
+ if (!pattern)
67
+ return undefined;
68
+ logSecurityBlock(ctx.sessionId, action, `Protected path: ${pattern}`, pathLabel);
69
+ return {
70
+ success: false,
71
+ output: `[PROTECTED PATH] ${t("file.protected_path_ask", { path: pathLabel, pattern })}`,
72
+ };
73
+ }