micro-models-agent 2.11.0 → 2.13.1
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/CHANGELOG.md +39 -0
- package/dist/assets/app.js +22 -2
- package/dist/assets/index.html +39 -9
- package/dist/assets/js/dom.js +15 -0
- package/dist/assets/js/feed.js +129 -10
- package/dist/assets/js/sidebar.js +512 -0
- package/dist/assets/js/tabs.js +30 -5
- package/dist/assets/setup.js +267 -8
- package/dist/assets/styles.css +168 -3
- package/dist/cli/cache-line.js +1 -0
- package/dist/cli/command-suggest.js +5 -4
- package/dist/cli/commands.js +231 -122
- package/dist/cli/completer.js +132 -31
- package/dist/cli/index.js +1 -0
- package/dist/cli/json-payload.js +4 -4
- package/dist/cli/main.js +166 -61
- package/dist/cli/onboarding-commands.js +126 -0
- package/dist/cli/plugin-commands.js +2 -1
- package/dist/cli/relaunch.js +16 -13
- package/dist/cli/repl-commands-lsp.js +75 -0
- package/dist/cli/repl-commands-skills.js +105 -0
- package/dist/cli/repl-commands.js +589 -266
- package/dist/cli/repl.js +436 -177
- package/dist/cli/run-result.js +5 -6
- package/dist/cli/security-commands.js +37 -28
- package/dist/cli/setup-order.js +4 -55
- package/dist/cli/setup-prompt.js +3 -2
- package/dist/cli/setup.js +27 -107
- package/dist/cli/web-command.js +98 -0
- package/dist/config/budget.js +5 -5
- package/dist/config/config.js +103 -96
- package/dist/config/defaults.js +38 -8
- package/dist/config/domains.js +24 -27
- package/dist/config/env.js +46 -0
- package/dist/config/experts.js +2 -0
- package/dist/config/index.js +1 -0
- package/dist/config/paths.js +56 -0
- package/dist/config/redact.js +44 -0
- package/dist/config/registry-extra.js +334 -0
- package/dist/config/registry.js +584 -0
- package/dist/config/security.js +35 -36
- package/dist/config/timeouts.js +74 -0
- package/dist/config/utils.js +16 -15
- package/dist/core/agent/artifact-progress.js +59 -0
- package/dist/core/agent/artifact-resolve.js +79 -0
- package/dist/core/agent/audit-gate.js +7 -3
- package/dist/core/agent/compaction.js +2 -2
- package/dist/core/agent/constants.js +33 -4
- package/dist/core/agent/context-log.js +454 -0
- package/dist/core/agent/context-renderer.js +5 -0
- package/dist/core/agent/critic-gate.js +132 -0
- package/dist/core/agent/hallucination-gate.js +148 -38
- package/dist/core/agent/loop-state.js +15 -0
- package/dist/core/agent/prefix-monitor.js +1 -1
- package/dist/core/agent/reasoning-resolver.js +10 -1
- package/dist/core/agent/token-tracker.js +7 -4
- package/dist/core/agent/tool-batch.js +93 -18
- package/dist/core/agent/tool-images.js +18 -0
- package/dist/core/agent/tool-output.js +65 -11
- package/dist/core/agent/tool-pairing.js +60 -0
- package/dist/core/agent-moe.js +25 -24
- package/dist/core/agent.js +429 -108
- package/dist/core/bootstrap/agent-phase.js +66 -0
- package/dist/core/bootstrap/config-phase.js +86 -0
- package/dist/core/bootstrap/modules-phase.js +269 -0
- package/dist/core/bootstrap/provider-phase.js +183 -0
- package/dist/core/bootstrap/session-phase.js +204 -0
- package/dist/core/bootstrap/state.js +94 -0
- package/dist/core/bootstrap/workspace-phase.js +115 -0
- package/dist/core/bootstrap.js +92 -660
- package/dist/core/crash-handler.js +17 -7
- package/dist/core/environment.js +37 -12
- package/dist/core/host-plan.js +43 -0
- package/dist/core/host.js +23 -0
- package/dist/core/index.js +1 -0
- package/dist/core/prompt-builder.js +17 -1
- package/dist/core/prompt-overflow.js +42 -43
- package/dist/core/session-logger.js +122 -30
- package/dist/core/system-prompt.js +117 -0
- package/dist/core/version.js +6 -4
- package/dist/core/web-host.js +34 -0
- package/dist/core/workspace.js +19 -15
- package/dist/i18n/en.json +89 -3
- package/dist/i18n/index.js +37 -3
- package/dist/i18n/ru.json +89 -3
- package/dist/index.js +7 -3
- package/dist/llm/image-utils.js +55 -64
- package/dist/llm/llm-errors.js +9 -0
- package/dist/llm/model-loader.js +10 -32
- package/dist/llm/models-dev.js +121 -0
- package/dist/llm/openai-compat.js +146 -73
- package/dist/llm/orchestrator.js +33 -11
- package/dist/llm/provider-budget.js +2 -2
- package/dist/llm/provider-context.js +233 -0
- package/dist/llm/provider.js +1 -1
- package/dist/llm/response.js +5 -0
- package/dist/llm/token-counter.js +27 -11
- package/dist/logger/app-logger.js +48 -22
- package/dist/logger/file-log.js +28 -19
- package/dist/logger/index.js +1 -0
- package/dist/main.js +888 -139
- package/dist/migration/backup.js +14 -3
- package/dist/migration/detect.js +12 -3
- package/dist/migration/index.js +1 -0
- package/dist/modules/artifacts/store.js +9 -8
- package/dist/modules/browser/actions.js +6 -0
- package/dist/modules/browser/bridge-client.js +14 -12
- package/dist/modules/browser/bridge-path.js +4 -4
- package/dist/modules/browser/console-buffer.js +37 -0
- package/dist/modules/browser/cookie-store.js +5 -0
- package/dist/modules/browser/driver-types.js +1 -0
- package/dist/modules/browser/driver.js +19 -11
- package/dist/modules/browser/index.js +3 -1
- package/dist/modules/browser/module.js +2 -1
- package/dist/modules/browser/session.js +10 -30
- package/dist/modules/browser/snapshot.js +19 -13
- package/dist/modules/browser/types.js +1 -0
- package/dist/modules/certification/cli.js +28 -20
- package/dist/modules/certification/fact-checker.js +9 -1
- package/dist/modules/certification/loader.js +9 -2
- package/dist/modules/certification/manifest.js +83 -21
- package/dist/modules/certification/runner.js +23 -13
- package/dist/modules/certification/scenarios.js +20 -20
- package/dist/modules/certification/syntax-scenarios.js +1 -2
- package/dist/modules/context/chunk-query.js +11 -9
- package/dist/modules/context/fact-extractor.js +37 -39
- package/dist/modules/context/index.js +1 -0
- package/dist/modules/context/manager.js +106 -80
- package/dist/modules/critic/client.js +176 -0
- package/dist/modules/critic/evidence.js +81 -0
- package/dist/modules/critic/provider.js +45 -0
- package/dist/modules/critic/tools.js +78 -0
- package/dist/modules/critic/types.js +1 -0
- package/dist/modules/execution/audit-runners.js +28 -34
- package/dist/modules/execution/auditor.js +53 -49
- package/dist/modules/execution/execution-plugin.js +142 -102
- package/dist/modules/execution/index.js +1 -1
- package/dist/modules/execution/input-from.js +37 -13
- package/dist/modules/execution/module.js +368 -191
- package/dist/modules/execution/moe-executor.js +53 -26
- package/dist/modules/execution/plan-actions.js +320 -202
- package/dist/modules/execution/plan-checks.js +35 -0
- package/dist/modules/execution/plan-coverage.js +36 -33
- package/dist/modules/execution/plan-steps.js +11 -3
- package/dist/modules/execution/plan-store.js +83 -17
- package/dist/modules/execution/plan-tool.js +89 -35
- package/dist/modules/execution/plan-validator.js +30 -10
- package/dist/modules/execution/planner.js +22 -13
- package/dist/modules/execution/stuck-detector.js +196 -93
- package/dist/modules/execution/tracker.js +22 -8
- package/dist/modules/execution/verifier.js +69 -48
- package/dist/modules/execution/windows-commands.js +12 -12
- package/dist/modules/hallucination/artifact-claim.js +93 -0
- package/dist/modules/hallucination/confidence.js +13 -11
- package/dist/modules/hallucination/consistency.js +10 -2
- package/dist/modules/hallucination/detector.js +10 -5
- package/dist/modules/hallucination/factual.js +43 -26
- package/dist/modules/hallucination/index.js +1 -0
- package/dist/modules/hallucination/js-identifiers.js +44 -45
- package/dist/modules/hallucination/llm-judge.js +17 -12
- package/dist/modules/hallucination/success-claim.js +59 -0
- package/dist/modules/indexer/cache.js +52 -4
- package/dist/modules/indexer/index.js +1 -0
- package/dist/modules/indexer/map-command.js +2 -4
- package/dist/modules/indexer/map-select.js +16 -16
- package/dist/modules/indexer/module.js +47 -12
- package/dist/modules/indexer/project-profile.js +8 -7
- package/dist/modules/indexer/symbols.js +15 -15
- package/dist/modules/indexer/walker.js +159 -29
- package/dist/modules/lsp/check-tool.js +7 -5
- package/dist/modules/lsp/client.js +39 -39
- package/dist/modules/lsp/command.js +17 -24
- package/dist/modules/lsp/config.js +12 -11
- package/dist/modules/lsp/index.js +1 -0
- package/dist/modules/lsp/module.js +48 -46
- package/dist/modules/lsp/probe.js +13 -14
- package/dist/modules/lsp/project-root.js +14 -17
- package/dist/modules/lsp/startup-check.js +18 -16
- package/dist/modules/mcp/client.js +59 -62
- package/dist/modules/mcp/index.js +1 -0
- package/dist/modules/mcp/module.js +66 -37
- package/dist/modules/mcp/registry.js +5 -0
- package/dist/modules/memory/index.js +1 -0
- package/dist/modules/memory/module.js +12 -8
- package/dist/modules/memory/search.js +4 -1
- package/dist/modules/memory/store.js +40 -2
- package/dist/modules/onboarding/init.js +47 -0
- package/dist/modules/onboarding/meet.js +30 -0
- package/dist/modules/onboarding/mode.js +30 -0
- package/dist/modules/onboarding/prompts.js +43 -0
- package/dist/modules/pipelines/engine.js +10 -0
- package/dist/modules/pipelines/index.js +1 -0
- package/dist/modules/pipelines/parser.js +4 -1
- package/dist/modules/pipelines/template.js +4 -0
- package/dist/modules/plugins/builtin/lint-on-write.js +80 -67
- package/dist/modules/plugins/builtin/notify.js +2 -0
- package/dist/modules/plugins/index.js +1 -0
- package/dist/modules/plugins/loader.js +18 -3
- package/dist/modules/plugins/manager.js +37 -17
- package/dist/modules/pricing/index.js +10 -11
- package/dist/modules/pricing/prices.js +12 -12
- package/dist/modules/processes/index.js +1 -0
- package/dist/modules/processes/kill-tree.js +27 -31
- package/dist/modules/processes/registry.js +44 -25
- package/dist/modules/processes/runner.js +4 -4
- package/dist/modules/providers/capabilities.js +28 -0
- package/dist/modules/providers/create.js +3 -4
- package/dist/modules/providers/factory.js +7 -7
- package/dist/modules/providers/fallback.js +12 -12
- package/dist/modules/providers/health.js +4 -4
- package/dist/modules/providers/manager.js +50 -19
- package/dist/modules/providers/naming.js +15 -0
- package/dist/modules/providers/preflight.js +71 -0
- package/dist/modules/providers/presets.js +39 -8
- package/dist/modules/providers/registry.js +7 -2
- package/dist/modules/reasoning/policy.js +8 -6
- package/dist/modules/reasoning/probe.js +30 -25
- package/dist/modules/security/audit-log.js +19 -36
- package/dist/modules/security/audit-notifier.js +35 -75
- package/dist/modules/security/command-validator.js +99 -51
- package/dist/modules/security/content-scanner.js +8 -8
- package/dist/modules/security/data-sanitizer.js +9 -16
- package/dist/modules/security/encryption.js +32 -52
- package/dist/modules/security/index.js +2 -1
- package/dist/modules/security/network-validator.js +120 -43
- package/dist/modules/security/path-validator.js +61 -46
- package/dist/modules/security/protected-paths.js +73 -0
- package/dist/modules/security/rate-limiter.js +17 -49
- package/dist/modules/security/security-policies.js +25 -57
- package/dist/modules/security/session-encryption.js +32 -71
- package/dist/modules/security/session-isolation.js +15 -28
- package/dist/modules/session/auto-name.js +63 -0
- package/dist/modules/session/export.js +98 -0
- package/dist/modules/session/index.js +1 -0
- package/dist/modules/session/manager.js +79 -28
- package/dist/modules/session/module.js +4 -3
- package/dist/modules/session/store.js +57 -20
- package/dist/modules/setup/answers.js +35 -0
- package/dist/modules/setup/apply.js +79 -0
- package/dist/modules/setup/discovery.js +72 -0
- package/dist/modules/skills/index.js +1 -0
- package/dist/modules/skills/loader.js +9 -1
- package/dist/modules/skills/module.js +11 -0
- package/dist/modules/updater/changelog-reader.js +15 -16
- package/dist/modules/updater/checker.js +9 -4
- package/dist/modules/updater/dev-detect.js +7 -6
- package/dist/modules/updater/index.js +1 -0
- package/dist/modules/updater/module.js +17 -15
- package/dist/modules/user-profile/compressor.js +2 -0
- package/dist/modules/user-profile/index.js +1 -0
- package/dist/modules/user-profile/profile.js +10 -1
- package/dist/modules/webui/answer-api.js +29 -0
- package/dist/modules/webui/assets.js +50 -0
- package/dist/modules/webui/body.js +21 -0
- package/dist/modules/webui/commands-settings.js +209 -0
- package/dist/modules/webui/commands-shared.js +60 -0
- package/dist/modules/webui/commands.js +174 -0
- package/dist/modules/webui/config-api.js +134 -0
- package/dist/modules/webui/file-api.js +202 -0
- package/dist/modules/webui/forward.js +159 -0
- package/dist/modules/webui/history-api.js +112 -0
- package/dist/modules/webui/host-state.js +60 -0
- package/dist/modules/webui/i18n-api.js +9 -0
- package/dist/modules/webui/limits.js +6 -0
- package/dist/modules/webui/log-forward.js +20 -0
- package/dist/modules/webui/memory-commands.js +64 -0
- package/dist/modules/webui/model-cache.js +18 -0
- package/dist/modules/webui/module.js +148 -0
- package/dist/modules/webui/persist.js +24 -0
- package/dist/modules/webui/prompt-io.js +97 -0
- package/dist/modules/webui/providers-api.js +337 -0
- package/dist/modules/webui/providers-view.js +56 -0
- package/dist/modules/webui/reasoning-view.js +15 -0
- package/dist/modules/webui/run-agent.js +81 -0
- package/dist/modules/webui/server.js +284 -0
- package/dist/modules/webui/sessions-api.js +178 -0
- package/dist/modules/webui/settings-api.js +561 -0
- package/dist/modules/webui/setup-api.js +201 -0
- package/dist/modules/webui/state.js +74 -0
- package/dist/modules/webui/util.js +261 -0
- package/dist/output/bus.js +7 -2
- package/dist/output/channel.js +80 -28
- package/dist/output/format.js +2 -1
- package/dist/output/index.js +1 -0
- package/dist/output/json-sink.js +5 -3
- package/dist/output/machine.js +4 -3
- package/dist/output/session-sink.js +4 -4
- package/dist/tools/approve.js +4 -0
- package/dist/tools/attach-image.js +5 -4
- package/dist/tools/auto-fixer.js +26 -44
- package/dist/tools/auto-tags.js +83 -0
- package/dist/tools/bash.js +193 -116
- package/dist/tools/browser.js +76 -31
- package/dist/tools/chunk-query.js +14 -6
- package/dist/tools/create-dir.js +9 -5
- package/dist/tools/delete-file.js +9 -5
- package/dist/tools/download-file.js +32 -7
- package/dist/tools/edit-file.js +52 -14
- package/dist/tools/enable-tools.js +6 -6
- package/dist/tools/executor.js +132 -27
- package/dist/tools/file-info.js +3 -1
- package/dist/tools/filter-tools.js +3 -3
- package/dist/tools/glob-tool.js +4 -1
- package/dist/tools/grep-tool.js +13 -10
- package/dist/tools/hidden-tools-block.js +6 -6
- package/dist/tools/index.js +16 -8
- package/dist/tools/list-dir.js +4 -2
- package/dist/tools/load-skill.js +7 -0
- package/dist/tools/mcp-call.js +15 -4
- package/dist/tools/meet-done.js +14 -0
- package/dist/tools/move-file.js +14 -7
- package/dist/tools/path-utils.js +29 -29
- package/dist/tools/pipeline-run.js +29 -7
- package/dist/tools/preview.js +1 -1
- package/dist/tools/process-ids.js +31 -0
- package/dist/tools/process-kill.js +9 -16
- package/dist/tools/process-list.js +1 -0
- package/dist/tools/process-log.js +11 -16
- package/dist/tools/process-wait.js +63 -0
- package/dist/tools/prompt-io.js +4 -3
- package/dist/tools/question.js +172 -25
- package/dist/tools/read-file.js +55 -34
- package/dist/tools/recall.js +14 -8
- package/dist/tools/registry.js +6 -5
- package/dist/tools/remember.js +8 -4
- package/dist/tools/scope-request.js +11 -6
- package/dist/tools/search-history.js +5 -2
- package/dist/tools/session-info.js +2 -0
- package/dist/tools/set-thinking.js +8 -4
- package/dist/tools/subagent.js +59 -39
- package/dist/tools/syntax-validator.js +292 -90
- package/dist/tools/user-input.js +22 -18
- package/dist/tools/web-browse.js +9 -3
- package/dist/tools/web-fetch.js +12 -23
- package/dist/tools/web-search.js +7 -5
- package/dist/tools/write-file.js +45 -15
- package/dist/ui/action-log.js +13 -0
- package/dist/ui/box.js +4 -3
- package/dist/ui/colors.js +2 -0
- package/dist/ui/diff.js +6 -5
- package/dist/ui/index.js +1 -0
- package/dist/ui/line-editor.js +230 -122
- package/dist/ui/line-math.js +10 -2
- package/dist/ui/md-formatter.js +57 -37
- package/dist/ui/output.js +8 -3
- package/dist/ui/plan-view.js +21 -10
- package/dist/ui/renderer.js +393 -67
- package/dist/ui/spinner.js +16 -5
- package/dist/ui/table.js +15 -17
- package/dist/ui/theme.js +143 -0
- package/dist/utils/child-run.js +20 -0
- package/dist/utils/error.js +1 -1
- package/dist/utils/index.js +2 -0
- package/dist/utils/retry.js +2 -2
- package/dist/utils/truncate.js +1 -1
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { sanitizeLogMessage } from "./data-sanitizer";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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
|
-
/**
|
|
15
|
+
/** Образуют ли четыре октета приватный/резервный IPv4. */
|
|
16
16
|
function isPrivateIpv4(o) {
|
|
17
|
-
const [a, b = 0,
|
|
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
|
-
*
|
|
27
|
-
*
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
75
|
+
const h = hostname.toLowerCase().trim().replace(/^\[|\]$/g, "");
|
|
34
76
|
if (h.includes(":")) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
//
|
|
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; //
|
|
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
|
-
//
|
|
68
|
-
// "127.1"
|
|
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; //
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
*
|
|
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
|
-
//
|
|
225
|
+
// Плюс креды в URL (user:pass@host)
|
|
149
226
|
if (urlObj.username || urlObj.password) {
|
|
150
227
|
result = result.replace(/:\/\/[^:]+:[^@]+@/, "://[REDACTED]:[REDACTED]@");
|
|
151
228
|
}
|
|
152
|
-
//
|
|
229
|
+
// Декодим URL-encoded [REDACTED] обратно в plain text
|
|
153
230
|
result = decodeURIComponent(result);
|
|
154
231
|
return result;
|
|
155
232
|
}
|
|
@@ -1,39 +1,48 @@
|
|
|
1
|
-
|
|
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
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
-
|
|
14
|
-
|
|
17
|
+
realBase = realpathSync(baseResolved);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// baseDir нет (виртуальный путь / фикстура) — каноникализировать нечего.
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
let probe = targetResolved;
|
|
24
|
+
for (;;) {
|
|
15
25
|
try {
|
|
16
|
-
|
|
26
|
+
const real = realpathSync(probe);
|
|
27
|
+
return real !== realBase && !isInsideDir(real, realBase);
|
|
17
28
|
}
|
|
18
29
|
catch {
|
|
19
|
-
|
|
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
|
-
*
|
|
30
|
-
*
|
|
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
|
-
//
|
|
36
|
-
//
|
|
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
|
-
//
|
|
58
|
+
// Выключенная защита пропускает паттерны
|
|
50
59
|
if (!securityConfig?.enabled) {
|
|
51
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
115
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
*
|
|
177
|
-
*
|
|
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
|
-
//
|
|
197
|
+
// Относительный путь для посегментного матчинга
|
|
183
198
|
const relPath = resolvedTargetPath.startsWith(resolvedBaseDir)
|
|
184
199
|
? resolvedTargetPath.slice(resolvedBaseDir.length).replace(/^\//, "")
|
|
185
200
|
: resolvedTargetPath;
|
|
186
|
-
//
|
|
201
|
+
// Паттерны с ** (рекурсивный матчинг каталогов)
|
|
187
202
|
if (pattern.includes("**")) {
|
|
188
|
-
//
|
|
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
|
-
//
|
|
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
|
-
// **
|
|
217
|
+
// ** в конце матчит всё
|
|
203
218
|
if (!suffix)
|
|
204
219
|
return true;
|
|
205
|
-
//
|
|
220
|
+
// Суффикс против оставшихся сегментов
|
|
206
221
|
const suffixParts = suffix.split("/").filter(Boolean);
|
|
207
222
|
const relParts = relPath.split("/");
|
|
208
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
+
}
|