evolcore 0.0.1 → 0.0.3
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 +64 -0
- package/LICENSE +21 -0
- package/MIGRATION-0.5.0.md +378 -0
- package/README.md +318 -14
- package/ROLE_ACCESS_CONTROL.md +174 -0
- package/assets/.env.template +4 -0
- package/bin/ec-safe-output.js +161 -0
- package/bin/ec.js +29 -0
- package/dist/agents/baseagent.js +163 -0
- package/dist/agents/claude-runner.js +2565 -0
- package/dist/agents/codex-app-server-client.js +448 -0
- package/dist/agents/codex-runner.js +2682 -0
- package/dist/agents/gemini-runner.js +666 -0
- package/dist/agents/runner-types.js +75 -0
- package/dist/aun/aid/agentmd.js +216 -0
- package/dist/aun/aid/client.js +132 -0
- package/dist/aun/aid/control-aid.js +91 -0
- package/dist/aun/aid/identity.js +518 -0
- package/dist/aun/aid/index.js +4 -0
- package/dist/aun/aid/store.js +74 -0
- package/dist/aun/aid/types.js +1 -0
- package/dist/aun/aid/validation.js +21 -0
- package/dist/aun/group-identity.js +10 -0
- package/dist/aun/msg/group-index.js +6 -0
- package/dist/aun/msg/group.js +1231 -0
- package/dist/aun/msg/history.js +123 -0
- package/dist/aun/msg/index.js +5 -0
- package/dist/aun/msg/p2p.js +393 -0
- package/dist/aun/msg/payload-type.js +27 -0
- package/dist/aun/msg/upload.js +137 -0
- package/dist/aun/outbox.js +168 -0
- package/dist/aun/rpc/caller.js +42 -0
- package/dist/aun/rpc/connection.js +25 -0
- package/dist/aun/rpc/index.js +2 -0
- package/dist/aun/service-proxy.js +225 -0
- package/dist/aun/storage/download.js +29 -0
- package/dist/aun/storage/index.js +3 -0
- package/dist/aun/storage/manage.js +10 -0
- package/dist/aun/storage/upload.js +68 -0
- package/dist/channels/aun.js +4164 -0
- package/dist/channels/contact-bind-code.js +134 -0
- package/dist/channels/daemon.js +422 -0
- package/dist/channels/dingtalk.js +1479 -0
- package/dist/channels/feishu.js +1865 -0
- package/dist/channels/qqbot.js +409 -0
- package/dist/channels/wechat.js +817 -0
- package/dist/channels/wecom-card.js +101 -0
- package/dist/channels/wecom-onboarding.js +82 -0
- package/dist/channels/wecom-state.js +191 -0
- package/dist/channels/wecom.js +1157 -0
- package/dist/cli/agent-command.js +642 -0
- package/dist/cli/agent.js +1059 -0
- package/dist/cli/aun-commands.js +2003 -0
- package/dist/cli/bench.js +1228 -0
- package/dist/cli/cli-argv.js +66 -0
- package/dist/cli/code-stats.js +329 -0
- package/dist/cli/command-log.js +82 -0
- package/dist/cli/config-selector.js +69 -0
- package/dist/cli/config.js +261 -0
- package/dist/cli/contact.js +71 -0
- package/dist/cli/ctl-command.js +62 -0
- package/dist/cli/daemon-commands.js +2750 -0
- package/dist/cli/fs-command.js +1447 -0
- package/dist/cli/handoff-command.js +302 -0
- package/dist/cli/help.js +35 -0
- package/dist/cli/index.js +374 -0
- package/dist/cli/init-channel.js +1372 -0
- package/dist/cli/init.js +590 -0
- package/dist/cli/link-rules.js +240 -0
- package/dist/cli/model.js +591 -0
- package/dist/cli/net-check.js +723 -0
- package/dist/cli/queue-command.js +150 -0
- package/dist/cli/raw-key-input.js +25 -0
- package/dist/cli/response.js +344 -0
- package/dist/cli/restart-monitor.js +480 -0
- package/dist/cli/stats.js +609 -0
- package/dist/cli/task-context.js +80 -0
- package/dist/cli/trigger-command.js +545 -0
- package/dist/cli/version.js +93 -0
- package/dist/cli/watch-logs.js +33 -0
- package/dist/cli/watch-msg.js +673 -0
- package/dist/config/boot-log.js +266 -0
- package/dist/config/builtin-role-templates.js +42 -0
- package/dist/config/builtin-roles.js +91 -0
- package/dist/config/config-batch-get.js +11 -0
- package/dist/config/config-field-policy.js +261 -0
- package/dist/config/config-manager.js +1120 -0
- package/dist/config/config-operation-service.js +384 -0
- package/dist/config/contact-alias.js +68 -0
- package/dist/config/contact-book-store.js +454 -0
- package/dist/config/contact-book-v2-startup.js +35 -0
- package/dist/config/contact-book.js +224 -0
- package/dist/config/contact-operation-service.js +110 -0
- package/dist/config/gateway-config.js +858 -0
- package/dist/config/lifecycle.js +17 -0
- package/dist/config/mention-mode.js +27 -0
- package/dist/config/merge.js +161 -0
- package/dist/config/owner-policy.js +4 -0
- package/dist/config/peer-role-resolver.js +218 -0
- package/dist/config/resolved-config-op.js +483 -0
- package/dist/config/role-config-v4-startup.js +32 -0
- package/dist/config/role-config-v5-startup.js +27 -0
- package/dist/config/role-ranks.js +18 -0
- package/dist/config/role-schema.js +105 -0
- package/dist/config/role-service.js +156 -0
- package/dist/config/role-store.js +215 -0
- package/dist/config/roles.js +64 -0
- package/dist/config/schema-registry.js +154 -0
- package/dist/config/snapshot.js +598 -0
- package/dist/config-store.js +501 -0
- package/dist/core/auth/agent-delegation.js +111 -0
- package/dist/core/auth/auth-gateway.js +166 -0
- package/dist/core/auth/authenticated-actor.js +6 -0
- package/dist/core/auth/authorization-audit.js +119 -0
- package/dist/core/auth/operation-authorizer.js +720 -0
- package/dist/core/auth/operation-catalog.js +731 -0
- package/dist/core/baseagent-loader.js +54 -0
- package/dist/core/bootstrap-service.js +175 -0
- package/dist/core/capability/capability-manager.js +316 -0
- package/dist/core/capability/providers/claude-capability-provider.js +176 -0
- package/dist/core/capability/providers/codex-capability-provider.js +148 -0
- package/dist/core/capability/providers/gemini-capability-provider.js +10 -0
- package/dist/core/capability/types.js +27 -0
- package/dist/core/causation/audit.js +103 -0
- package/dist/core/causation/aun-association.js +111 -0
- package/dist/core/causation/context.js +93 -0
- package/dist/core/causation/index.js +4 -0
- package/dist/core/causation/types.js +2 -0
- package/dist/core/channel-loader.js +277 -0
- package/dist/core/command/agent-control.js +616 -0
- package/dist/core/command/cli-intent-parser.js +225 -0
- package/dist/core/command/command-handler.js +1733 -0
- package/dist/core/command/connect-menu.js +374 -0
- package/dist/core/command/menu-handler.js +3452 -0
- package/dist/core/command/menu-protocol.js +247 -0
- package/dist/core/command/role-menu.js +1623 -0
- package/dist/core/command/slash-gate.js +148 -0
- package/dist/core/command/slash-handler.js +3066 -0
- package/dist/core/daemon-file-cache.js +222 -0
- package/dist/core/event-bus.js +32 -0
- package/dist/core/event-catalog.js +810 -0
- package/dist/core/evolagent-registry.js +545 -0
- package/dist/core/evolagent.js +342 -0
- package/dist/core/handoff/dispatcher.js +229 -0
- package/dist/core/handoff/mutex.js +46 -0
- package/dist/core/handoff/runtime.js +324 -0
- package/dist/core/handoff/store.js +537 -0
- package/dist/core/handoff/types.js +12 -0
- package/dist/core/inference/text-inference.js +173 -0
- package/dist/core/interaction-registration.js +10 -0
- package/dist/core/interaction-router.js +278 -0
- package/dist/core/message/create-status.js +67 -0
- package/dist/core/message/im-renderer.js +659 -0
- package/dist/core/message/items-formatter.js +76 -0
- package/dist/core/message/logical-queue-bridge.js +123 -0
- package/dist/core/message/message-bridge.js +874 -0
- package/dist/core/message/message-cache.js +56 -0
- package/dist/core/message/message-log.js +339 -0
- package/dist/core/message/message-processor.js +4 -0
- package/dist/core/message/message-queue.js +1446 -0
- package/dist/core/message/message-utils.js +76 -0
- package/dist/core/message/peer-mode.js +105 -0
- package/dist/core/message/pending-hints.js +232 -0
- package/dist/core/message/response-depth.js +33 -0
- package/dist/core/message/response-engine.js +4020 -0
- package/dist/core/message/response-snapshot.js +83 -0
- package/dist/core/message/send-receipt.js +24 -0
- package/dist/core/message/stream-debouncer.js +139 -0
- package/dist/core/message/stream-idle-monitor.js +124 -0
- package/dist/core/model/config-scope.js +162 -0
- package/dist/core/model/field-scope.js +78 -0
- package/dist/core/model/model-catalog.js +227 -0
- package/dist/core/model/model-diagnostics.js +182 -0
- package/dist/core/model/model-permission.js +90 -0
- package/dist/core/permission/approval-gateway.js +1017 -0
- package/dist/core/permission/ec-command-parser.js +347 -0
- package/dist/core/permission/execution-sandbox.js +16 -0
- package/dist/core/permission/index.js +6 -0
- package/dist/core/permission/mode.js +24 -0
- package/dist/core/permission/sandbox-runtime.js +265 -0
- package/dist/core/permission/tool-policy.js +1019 -0
- package/dist/core/permission/unix-socket-policy.js +99 -0
- package/dist/core/protected-paths.js +332 -0
- package/dist/core/relation/peer-identity.js +222 -0
- package/dist/core/relation/peer-key.js +1 -0
- package/dist/core/role/runtime-policy.js +141 -0
- package/dist/core/session/adapters/claude-session-file-adapter.js +218 -0
- package/dist/core/session/adapters/codex-session-file-adapter.js +333 -0
- package/dist/core/session/adapters/gemini-session-file-adapter.js +181 -0
- package/dist/core/session/session-file-adapter.js +7 -0
- package/dist/core/session/session-file-health.js +45 -0
- package/dist/core/session/session-fs-store.js +273 -0
- package/dist/core/session/session-key.js +24 -0
- package/dist/core/session/session-manager.js +1643 -0
- package/dist/core/session/session-mapper.js +100 -0
- package/dist/core/session/session-renew.js +314 -0
- package/dist/core/session/session-title.js +128 -0
- package/dist/core/session/session-turn-coordinator.js +205 -0
- package/dist/core/session/session-turns.js +67 -0
- package/dist/core/system-channels.js +29 -0
- package/dist/eck/baseagent-caps.js +18 -0
- package/dist/eck/detect.js +47 -0
- package/dist/eck/group-rules-sync.js +345 -0
- package/dist/eck/init.js +77 -0
- package/dist/eck/kit-renderer.js +359 -0
- package/dist/eck/manifest-engine.js +446 -0
- package/dist/eck/message-renderer.js +199 -0
- package/dist/eck/rules-loader.js +28 -0
- package/dist/index.js +2926 -4
- package/dist/ipc.js +777 -0
- package/dist/paths.js +262 -0
- package/dist/product.js +18 -0
- package/dist/response-system/context-builder.js +71 -0
- package/dist/response-system/coordinator.js +117 -0
- package/dist/response-system/decision-executor.js +86 -0
- package/dist/response-system/engines/v1/index.js +21 -0
- package/dist/response-system/engines/v1/interactive-flow.js +27 -0
- package/dist/response-system/engines/v1/proactive-flow.js +137 -0
- package/dist/response-system/engines/v1/types.js +1 -0
- package/dist/response-system/extensions.js +41 -0
- package/dist/response-system/index.js +6 -0
- package/dist/response-system/modes/index.js +7 -0
- package/dist/response-system/modes/single-session/index.js +72 -0
- package/dist/response-system/queues/fifo-queue.js +44 -0
- package/dist/response-system/queues/index.js +6 -0
- package/dist/response-system/queues/lifo-queue.js +42 -0
- package/dist/response-system/queues/priority-queue.js +63 -0
- package/dist/response-system/registry.js +97 -0
- package/dist/response-system/resolver.js +37 -0
- package/dist/response-system/selector.js +23 -0
- package/dist/response-system/types.js +7 -0
- package/dist/stats/billing.js +163 -0
- package/dist/stats/budget.js +93 -0
- package/dist/stats/db.js +403 -0
- package/dist/stats/eck-vars.js +89 -0
- package/dist/stats/index.js +11 -0
- package/dist/stats/normalizer.js +80 -0
- package/dist/stats/price-resolver.js +138 -0
- package/dist/stats/query.js +763 -0
- package/dist/stats/role-budget.js +168 -0
- package/dist/stats/writer.js +151 -0
- package/dist/trigger/anomaly-store.js +258 -0
- package/dist/trigger/audit.js +152 -0
- package/dist/trigger/event-source.js +119 -0
- package/dist/trigger/feedback.js +685 -0
- package/dist/trigger/history.js +290 -0
- package/dist/trigger/manager.js +294 -0
- package/dist/trigger/parser.js +595 -0
- package/dist/trigger/patch.js +155 -0
- package/dist/trigger/scheduler.js +1602 -0
- package/dist/trigger/script-executor.js +155 -0
- package/dist/trigger/state.js +145 -0
- package/dist/trigger/types.js +1 -0
- package/dist/trigger/validation.js +634 -0
- package/dist/types.js +12 -0
- package/dist/utils/aid-bind.js +313 -0
- package/dist/utils/atomic-write.js +95 -0
- package/dist/utils/avatar-upload.js +123 -0
- package/dist/utils/cross-platform.js +297 -0
- package/dist/utils/ecweb-utils.js +73 -0
- package/dist/utils/error-dict.json +153 -0
- package/dist/utils/error-utils.js +349 -0
- package/dist/utils/instance-registry.js +444 -0
- package/dist/utils/locale.js +21 -0
- package/dist/utils/log-writer.js +270 -0
- package/dist/utils/logger.js +89 -0
- package/dist/utils/markdown-to-plain-text.js +20 -0
- package/dist/utils/media-cache.js +274 -0
- package/dist/utils/model-prices.jsonl +20 -0
- package/dist/utils/npm-ops.js +210 -0
- package/dist/utils/process-introspect.js +133 -0
- package/dist/utils/process-tree-stats.js +271 -0
- package/dist/utils/project-path.js +74 -0
- package/dist/utils/restart-safety.js +31 -0
- package/dist/utils/stats.js +410 -0
- package/dist/utils/system-memory.js +62 -0
- package/dist/utils/tool-summary.js +284 -0
- package/dist/utils/welcome.js +268 -0
- package/kits/docs/GUIDE.md +20 -0
- package/kits/docs/INDEX.md +66 -0
- package/kits/docs/aun/CHEATSHEET.md +19 -0
- package/kits/docs/aun/SYNC_PROTOCOL.md +15 -0
- package/kits/docs/channels/aun.md +65 -0
- package/kits/docs/channels/feishu.md +56 -0
- package/kits/docs/context-assembly.md +366 -0
- package/kits/docs/eck_templates/GUIDE.template.md +22 -0
- package/kits/docs/eck_templates/INDEX.template.md +28 -0
- package/kits/docs/eck_templates/path-registry.template.md +33 -0
- package/kits/docs/eck_templates/runtime.template.md +19 -0
- package/kits/docs/evolcore/INDEX.md +68 -0
- package/kits/docs/evolcore/agent.md +77 -0
- package/kits/docs/evolcore/aid.md +52 -0
- package/kits/docs/evolcore/config.md +149 -0
- package/kits/docs/evolcore/contact.md +57 -0
- package/kits/docs/evolcore/ctl.md +46 -0
- package/kits/docs/evolcore/event.md +216 -0
- package/kits/docs/evolcore/fs-architecture.md +1215 -0
- package/kits/docs/evolcore/fs.md +110 -0
- package/kits/docs/evolcore/group-fs.md +17 -0
- package/kits/docs/evolcore/group-rules.md +226 -0
- package/kits/docs/evolcore/group.md +150 -0
- package/kits/docs/evolcore/model.md +50 -0
- package/kits/docs/evolcore/msg.md +136 -0
- package/kits/docs/evolcore/response.md +75 -0
- package/kits/docs/evolcore/rpc.md +37 -0
- package/kits/docs/evolcore/self-summary.md +29 -0
- package/kits/docs/evolcore/stats.md +83 -0
- package/kits/docs/evolcore/storage.md +50 -0
- package/kits/docs/evolcore/trigger.md +539 -0
- package/kits/docs/identity/AID_PROFILE_SPEC.md +26 -0
- package/kits/docs/identity/PATH_OPS.md +16 -0
- package/kits/docs/identity/ROLE_DETAIL.md +23 -0
- package/kits/docs/identity/identity-tools.md +26 -0
- package/kits/docs/path-registry.md +43 -0
- package/kits/docs/prompt-loading-architecture.md +266 -0
- package/kits/docs/venues/aun-group.md +45 -0
- package/kits/docs/venues/aun-private.md +10 -0
- package/kits/docs/venues/client-desktop.md +10 -0
- package/kits/docs/venues/client-mobile.md +10 -0
- package/kits/docs/venues/feishu-group.md +13 -0
- package/kits/docs/venues/feishu-private.md +9 -0
- package/kits/docs/venues/group.md +25 -0
- package/kits/docs/venues/private.md +10 -0
- package/kits/eck_manifest.auxiliary.json +43 -0
- package/kits/eck_manifest.json +203 -0
- package/kits/eck_message_manifest.json +63 -0
- package/kits/migrations/README-role-config-v4.md +32 -0
- package/kits/migrations/migrate-contact-book-v2.mjs +747 -0
- package/kits/migrations/migrate-role-config-v4.mjs +623 -0
- package/kits/migrations/migrate-role-config-v5.mjs +346 -0
- package/kits/migrations/rename-config-file.mjs +99 -0
- package/kits/rules/01-overview.md +142 -0
- package/kits/rules/02-navigation.md +76 -0
- package/kits/rules/03-identity.md +34 -0
- package/kits/rules/04-relation.md +59 -0
- package/kits/rules/05-venue.md +44 -0
- package/kits/rules/06-channel.md +59 -0
- package/kits/schemas/_meta.json +32 -0
- package/kits/schemas/agent-config.schema.1.json +177 -0
- package/kits/schemas/agent-config.schema.2.json +239 -0
- package/kits/schemas/agent-config.schema.3.json +119 -0
- package/kits/schemas/agent-config.schema.4.json +208 -0
- package/kits/schemas/agent-config.schema.5.json +326 -0
- package/kits/schemas/agent-config.schema.6.json +322 -0
- package/kits/schemas/contact-book.schema.1.json +36 -0
- package/kits/schemas/contact-book.schema.2.json +43 -0
- package/kits/schemas/daemon.schema.1.json +90 -0
- package/kits/schemas/defaults.schema.1.json +81 -0
- package/kits/schemas/menu-exec-schema-commands.md +208 -0
- package/kits/schemas/migrations/README.md +28 -0
- package/kits/schemas/relation-config.schema.1.json +158 -0
- package/kits/schemas/relation-config.schema.2.json +73 -0
- package/kits/schemas/relation-config.schema.3.json +50 -0
- package/kits/schemas/relation-config.schema.4.json +47 -0
- package/kits/schemas/relation-config.schema.5.json +47 -0
- package/kits/schemas/role-config.schema.1.json +201 -0
- package/kits/schemas/role-registry.schema.1.json +35 -0
- package/kits/schemas/single-session.schema.1.json +31 -0
- package/kits/templates/bootstrap-welcome.md +15 -0
- package/kits/templates/message-fragments/handoff-request-to-target.md +13 -0
- package/kits/templates/message-fragments/handoff-response-to-origin.md +10 -0
- package/kits/templates/message-fragments/inject-default.md +2 -0
- package/kits/templates/message-fragments/item.md +2 -0
- package/kits/templates/roles/admin.json +9 -0
- package/kits/templates/roles/member.json +41 -0
- package/kits/templates/roles/owner.json +9 -0
- package/kits/templates/roles/visitor.json +40 -0
- package/kits/templates/system-fragments/baseagent.md +14 -0
- package/kits/templates/system-fragments/bootstrap.md +16 -0
- package/kits/templates/system-fragments/channel.md +48 -0
- package/kits/templates/system-fragments/commands.md +28 -0
- package/kits/templates/system-fragments/identity.md +11 -0
- package/kits/templates/system-fragments/relation.md +19 -0
- package/kits/templates/system-fragments/session.md +53 -0
- package/kits/templates/system-fragments/venue.md +31 -0
- package/package.json +50 -15
- package/dist/index.d.ts +0 -7
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
|
@@ -0,0 +1,1019 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { randomUUID } from 'crypto';
|
|
4
|
+
import { logger } from '../../utils/logger.js';
|
|
5
|
+
import { resolveRoot } from '../../paths.js';
|
|
6
|
+
import { containsHClassReference, hClassGrantIncludesProtectedPath, isHClassPath, isSameOrDescendant, isLClassPath, lClassGrantIncludesProtectedPath, resolveProtectedCandidate, } from '../protected-paths.js';
|
|
7
|
+
import { classifyEvolcoreShellCommand, parseBoundedOutputShellCommand, parseLiteralShellArgv, } from './ec-command-parser.js';
|
|
8
|
+
// 绝对禁止命令(所有角色、所有权限模式下都禁止,不可授权)
|
|
9
|
+
// 这些是系统级破坏操作,任何情况下都不应该允许
|
|
10
|
+
const ABSOLUTE_FORBIDDEN = [
|
|
11
|
+
/\bshutdown\b/, // 关机
|
|
12
|
+
/\breboot\b/, // 重启
|
|
13
|
+
/\bmkfs\b/, // mkfs (格式化文件系统)
|
|
14
|
+
/\bformat\s+[a-zA-Z]:/i, // format C: (格式化磁盘)
|
|
15
|
+
/\bdd\s+if=.*of=\/dev/, // dd 写入磁盘设备(读取操作允许)
|
|
16
|
+
];
|
|
17
|
+
const READONLY_SHELL_EXECUTABLES = new Set([
|
|
18
|
+
'ls', '/bin/ls', '/usr/bin/ls',
|
|
19
|
+
'cat', '/bin/cat', '/usr/bin/cat',
|
|
20
|
+
]);
|
|
21
|
+
// 危险操作(需要用户审批才能执行)
|
|
22
|
+
// 这些操作有合理使用场景,但需要用户明确授权
|
|
23
|
+
const DANGEROUS_PATTERNS = [
|
|
24
|
+
{
|
|
25
|
+
kind: 'rm-rf',
|
|
26
|
+
pattern: /\brm\b(?=[^;&|\r\n]*\s(?:-[A-Za-z]*r[A-Za-z]*|--recursive)(?:\s|=|$))(?=[^;&|\r\n]*\s(?:-[A-Za-z]*f[A-Za-z]*|--force)(?:\s|=|$))[^;&|\r\n]*/,
|
|
27
|
+
reason: '递归删除文件/目录,操作不可逆',
|
|
28
|
+
approvalScope: 'request',
|
|
29
|
+
},
|
|
30
|
+
{ kind: 'sudo', pattern: /\bsudo\b/, reason: '以超级用户权限执行命令', approvalScope: 'request' },
|
|
31
|
+
{ kind: 'chmod-777', pattern: /\bchmod\s+777/, reason: '设置文件为完全开放权限(安全风险)', approvalScope: 'request' },
|
|
32
|
+
{ kind: 'device-redirect', pattern: />\s*\/dev\/(?!null\b)/, reason: '写入设备文件(可能影响系统)', approvalScope: 'request' },
|
|
33
|
+
{ kind: 'windows-rd', pattern: /\brd\s+\/s/i, reason: '递归删除目录,操作不可逆', approvalScope: 'request' },
|
|
34
|
+
{ kind: 'windows-del', pattern: /\bdel\s+\/[sfq]/i, reason: '强制/递归删除文件,操作不可逆', approvalScope: 'request' },
|
|
35
|
+
{ kind: 'reg-delete', pattern: /\breg\s+delete/i, reason: '删除注册表项(Windows 系统配置)', approvalScope: 'request' },
|
|
36
|
+
];
|
|
37
|
+
function splitShellCommandSegments(command) {
|
|
38
|
+
const segments = [];
|
|
39
|
+
let segment = '';
|
|
40
|
+
let quote = null;
|
|
41
|
+
let escaped = false;
|
|
42
|
+
let substitutionDepth = 0;
|
|
43
|
+
const finish = () => {
|
|
44
|
+
if (segment.trim())
|
|
45
|
+
segments.push(segment.trim());
|
|
46
|
+
segment = '';
|
|
47
|
+
};
|
|
48
|
+
for (let index = 0; index < command.length; index++) {
|
|
49
|
+
const char = command[index];
|
|
50
|
+
if (escaped) {
|
|
51
|
+
segment += char;
|
|
52
|
+
escaped = false;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (char === '\\' && quote !== 'single') {
|
|
56
|
+
segment += char;
|
|
57
|
+
escaped = true;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (quote === 'single') {
|
|
61
|
+
segment += char;
|
|
62
|
+
if (char === "'")
|
|
63
|
+
quote = null;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (quote === 'double') {
|
|
67
|
+
segment += char;
|
|
68
|
+
if (char === '"')
|
|
69
|
+
quote = null;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (char === "'" || char === '"') {
|
|
73
|
+
quote = char === "'" ? 'single' : 'double';
|
|
74
|
+
segment += char;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (char === '$' && command[index + 1] === '(') {
|
|
78
|
+
substitutionDepth++;
|
|
79
|
+
segment += '$(';
|
|
80
|
+
index++;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (char === ')' && substitutionDepth > 0) {
|
|
84
|
+
substitutionDepth--;
|
|
85
|
+
segment += char;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (substitutionDepth === 0 && (char === ';' || char === '\n' || char === '|' || char === '&')) {
|
|
89
|
+
finish();
|
|
90
|
+
if ((char === '|' || char === '&') && command[index + 1] === char)
|
|
91
|
+
index++;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
segment += char;
|
|
95
|
+
}
|
|
96
|
+
finish();
|
|
97
|
+
return segments;
|
|
98
|
+
}
|
|
99
|
+
function parseShellWords(segment) {
|
|
100
|
+
const words = [];
|
|
101
|
+
let word = '';
|
|
102
|
+
let started = false;
|
|
103
|
+
let quote = null;
|
|
104
|
+
let escaped = false;
|
|
105
|
+
const finish = () => {
|
|
106
|
+
if (started)
|
|
107
|
+
words.push(word);
|
|
108
|
+
word = '';
|
|
109
|
+
started = false;
|
|
110
|
+
};
|
|
111
|
+
for (const char of segment) {
|
|
112
|
+
if (escaped) {
|
|
113
|
+
word += char;
|
|
114
|
+
started = true;
|
|
115
|
+
escaped = false;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (char === '\\' && quote !== 'single') {
|
|
119
|
+
escaped = true;
|
|
120
|
+
started = true;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (quote === 'single') {
|
|
124
|
+
if (char === "'")
|
|
125
|
+
quote = null;
|
|
126
|
+
else
|
|
127
|
+
word += char;
|
|
128
|
+
started = true;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (quote === 'double') {
|
|
132
|
+
if (char === '"')
|
|
133
|
+
quote = null;
|
|
134
|
+
else
|
|
135
|
+
word += char;
|
|
136
|
+
started = true;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (char === "'" || char === '"') {
|
|
140
|
+
quote = char === "'" ? 'single' : 'double';
|
|
141
|
+
started = true;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (/\s/.test(char)) {
|
|
145
|
+
finish();
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
word += char;
|
|
149
|
+
started = true;
|
|
150
|
+
}
|
|
151
|
+
finish();
|
|
152
|
+
return words;
|
|
153
|
+
}
|
|
154
|
+
function commandName(value) {
|
|
155
|
+
return path.basename((value ?? '').replace(/^[({]+|[)}]+$/g, '')).toLowerCase();
|
|
156
|
+
}
|
|
157
|
+
function isOwnerOrAdminRole(role) {
|
|
158
|
+
return role === 'owner' || role === 'admin';
|
|
159
|
+
}
|
|
160
|
+
function hasNodeProcessTarget(command) {
|
|
161
|
+
return /\bnode(?:js|\.exe)?\b/i.test(command);
|
|
162
|
+
}
|
|
163
|
+
function classifyHostProcessArgv(argv) {
|
|
164
|
+
const executable = commandName(argv[0]);
|
|
165
|
+
if (['ps', 'pgrep', 'pidof'].includes(executable)) {
|
|
166
|
+
return { kind: 'process-read' };
|
|
167
|
+
}
|
|
168
|
+
if (['kill', 'pkill', 'killall'].includes(executable)) {
|
|
169
|
+
if (argv.some(hasNodeProcessTarget))
|
|
170
|
+
return undefined;
|
|
171
|
+
return { kind: 'process-signal' };
|
|
172
|
+
}
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
function classifyHostProcessCommand(command) {
|
|
176
|
+
const argv = parseLiteralShellArgv(command);
|
|
177
|
+
return argv ? classifyHostProcessArgv(argv) : undefined;
|
|
178
|
+
}
|
|
179
|
+
function skipCommandOptions(words, index, optionsWithValues) {
|
|
180
|
+
while (index < words.length) {
|
|
181
|
+
const word = words[index];
|
|
182
|
+
if (word === '--')
|
|
183
|
+
return index + 1;
|
|
184
|
+
if (!word.startsWith('-'))
|
|
185
|
+
return index;
|
|
186
|
+
const option = word.split('=', 1)[0];
|
|
187
|
+
index++;
|
|
188
|
+
if (!word.includes('=') && optionsWithValues.has(option))
|
|
189
|
+
index++;
|
|
190
|
+
}
|
|
191
|
+
return index;
|
|
192
|
+
}
|
|
193
|
+
function parseEffectiveCommand(segment) {
|
|
194
|
+
const words = parseShellWords(segment);
|
|
195
|
+
let index = 0;
|
|
196
|
+
while (index < words.length && ['if', 'then', 'do', 'while', 'until', '!'].includes(commandName(words[index])))
|
|
197
|
+
index++;
|
|
198
|
+
while (index < words.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(words[index]))
|
|
199
|
+
index++;
|
|
200
|
+
while (index < words.length) {
|
|
201
|
+
const executable = commandName(words[index]);
|
|
202
|
+
if (executable === 'env') {
|
|
203
|
+
index = skipCommandOptions(words, index + 1, new Set(['-u', '--unset', '-C', '--chdir', '-S', '--split-string']));
|
|
204
|
+
while (index < words.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(words[index]))
|
|
205
|
+
index++;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (executable === 'sudo') {
|
|
209
|
+
index = skipCommandOptions(words, index + 1, new Set([
|
|
210
|
+
'-u', '--user', '-g', '--group', '-h', '--host', '-p', '--prompt', '-C', '--chdir', '-R', '--chroot', '-T', '--command-timeout',
|
|
211
|
+
]));
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (['command', 'builtin', 'exec', 'nohup', 'time'].includes(executable)) {
|
|
215
|
+
index = skipCommandOptions(words, index + 1, new Set());
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
return executable ? { executable, args: words.slice(index + 1), raw: segment } : undefined;
|
|
219
|
+
}
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
222
|
+
function gitSubcommand(args) {
|
|
223
|
+
const optionsWithValues = new Set([
|
|
224
|
+
'-C', '-c', '--git-dir', '--work-tree', '--namespace', '--exec-path', '--config-env', '--super-prefix',
|
|
225
|
+
]);
|
|
226
|
+
let index = 0;
|
|
227
|
+
while (index < args.length) {
|
|
228
|
+
const word = args[index];
|
|
229
|
+
if (word === '--') {
|
|
230
|
+
index++;
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
if (!word.startsWith('-'))
|
|
234
|
+
break;
|
|
235
|
+
const option = word.split('=', 1)[0];
|
|
236
|
+
index++;
|
|
237
|
+
if (!word.includes('=') && optionsWithValues.has(option))
|
|
238
|
+
index++;
|
|
239
|
+
}
|
|
240
|
+
return { name: args[index]?.toLowerCase(), args: args.slice(index + 1) };
|
|
241
|
+
}
|
|
242
|
+
function classifyStructuredCommand(command, depth = 0) {
|
|
243
|
+
const matches = [];
|
|
244
|
+
for (const segment of splitShellCommandSegments(command)) {
|
|
245
|
+
const parsed = parseEffectiveCommand(segment);
|
|
246
|
+
if (!parsed)
|
|
247
|
+
continue;
|
|
248
|
+
if (parsed.executable === 'git') {
|
|
249
|
+
const subcommand = gitSubcommand(parsed.args);
|
|
250
|
+
if (['reset', 'clean', 'restore', 'rm', 'prune'].includes(subcommand.name ?? '')) {
|
|
251
|
+
matches.push({
|
|
252
|
+
kind: 'git-destructive',
|
|
253
|
+
reason: 'Git 破坏性子命令可能不可逆地丢弃本地变更',
|
|
254
|
+
approvalScope: 'all',
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
else if (subcommand.name === 'checkout') {
|
|
258
|
+
matches.push({ kind: 'git-destructive', reason: 'Git checkout 可能覆盖工作区或切换本地状态', approvalScope: 'all' });
|
|
259
|
+
}
|
|
260
|
+
else if (subcommand.name === 'branch' && subcommand.args.includes('-D')) {
|
|
261
|
+
matches.push({ kind: 'git-destructive', reason: 'Git branch -D 会强制删除分支', approvalScope: 'all' });
|
|
262
|
+
}
|
|
263
|
+
else if (subcommand.name === 'update-ref') {
|
|
264
|
+
matches.push({ kind: 'git-destructive', reason: 'Git update-ref 会修改本地引用状态', approvalScope: 'all' });
|
|
265
|
+
}
|
|
266
|
+
else if (subcommand.name === 'reflog') {
|
|
267
|
+
matches.push({ kind: 'git-destructive', reason: 'Git reflog 可能修改或删除本地恢复记录', approvalScope: 'all' });
|
|
268
|
+
}
|
|
269
|
+
else if (subcommand.name === 'push') {
|
|
270
|
+
matches.push({ kind: 'git-push', reason: 'Git push 会修改远端仓库', approvalScope: 'request' });
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (['kill', 'pkill', 'killall'].includes(parsed.executable) && hasNodeProcessTarget(parsed.raw)) {
|
|
274
|
+
matches.push({ kind: 'process-kill', reason: '终止 Node.js 运行时进程', approvalScope: 'request' });
|
|
275
|
+
}
|
|
276
|
+
if (depth < 2 && ['bash', 'sh', 'zsh', 'dash', 'cmd', 'cmd.exe'].includes(parsed.executable)) {
|
|
277
|
+
const commandFlag = parsed.executable.startsWith('cmd') ? '/c' : '-c';
|
|
278
|
+
const commandIndex = parsed.args.findIndex(arg => arg.toLowerCase() === commandFlag);
|
|
279
|
+
if (commandIndex >= 0 && parsed.args[commandIndex + 1]) {
|
|
280
|
+
const nested = classifyStructuredCommand(parsed.args.slice(commandIndex + 1).join(' '), depth + 1);
|
|
281
|
+
if (nested)
|
|
282
|
+
matches.push(nested);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return matches.find(match => match.approvalScope === 'all') ?? matches[0];
|
|
287
|
+
}
|
|
288
|
+
function containsNetStopCommand(command, depth = 0) {
|
|
289
|
+
return splitShellCommandSegments(command).some(segment => {
|
|
290
|
+
const parsed = parseEffectiveCommand(segment);
|
|
291
|
+
if (!parsed)
|
|
292
|
+
return false;
|
|
293
|
+
if ((parsed.executable === 'net' || parsed.executable === 'net.exe')
|
|
294
|
+
&& parsed.args[0]?.toLowerCase() === 'stop') {
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
if (depth >= 2 || !['bash', 'sh', 'zsh', 'dash', 'cmd', 'cmd.exe'].includes(parsed.executable)) {
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
const commandFlag = parsed.executable.startsWith('cmd') ? '/c' : '-c';
|
|
301
|
+
const commandIndex = parsed.args.findIndex(arg => arg.toLowerCase() === commandFlag);
|
|
302
|
+
return commandIndex >= 0
|
|
303
|
+
&& !!parsed.args[commandIndex + 1]
|
|
304
|
+
&& containsNetStopCommand(parsed.args.slice(commandIndex + 1).join(' '), depth + 1);
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* 只读模式检查(用于 PreToolUse hook 和 canUseTool callback)
|
|
309
|
+
* 显式读工具自动允许;写工具和未知工具拒绝。
|
|
310
|
+
* Bash 默认拒绝;只允许由严格字面量解析器证明为 ls/cat 的只读命令。
|
|
311
|
+
* 通过 EC Shell 边界分类的整条字面量调用应由 runner 在调用本函数前放行。
|
|
312
|
+
*/
|
|
313
|
+
export function checkReadonly(toolName, input, projectPath, context) {
|
|
314
|
+
const readOnlyTools = new Set([
|
|
315
|
+
'Read', 'Glob', 'Grep', 'WebSearch', 'WebFetch',
|
|
316
|
+
'TaskList', 'TaskGet', 'ToolSearch',
|
|
317
|
+
]);
|
|
318
|
+
if (readOnlyTools.has(toolName))
|
|
319
|
+
return { behavior: 'allow' };
|
|
320
|
+
if (toolName === 'Write' || toolName === 'Edit' || toolName === 'NotebookEdit') {
|
|
321
|
+
const filePath = (input.file_path || input.notebook_path);
|
|
322
|
+
logger.warn(`[ReadonlyCheck] 🔒 File write blocked: tool=${toolName} path=${filePath} project=${projectPath} session=${context?.sessionId} channel=${context?.channel} peer=${context?.peerId} role=${context?.role}`);
|
|
323
|
+
return { behavior: 'deny', message: '🔒 只读模式:禁止写入或修改文件' };
|
|
324
|
+
}
|
|
325
|
+
if (toolName === 'Bash') {
|
|
326
|
+
const cmd = input.command || '';
|
|
327
|
+
const argv = parseLiteralShellArgv(cmd);
|
|
328
|
+
const executable = argv?.[0];
|
|
329
|
+
const readonlyExecutable = executable && READONLY_SHELL_EXECUTABLES.has(executable);
|
|
330
|
+
if (context?.allowLiteralReadShell === true && argv && readonlyExecutable) {
|
|
331
|
+
const protectedOperand = argv.slice(1).some(argument => {
|
|
332
|
+
if (argument === '-' || argument === '--' || argument.startsWith('-'))
|
|
333
|
+
return false;
|
|
334
|
+
return containsHClassReference(argument)
|
|
335
|
+
|| isHClassPath(argument, { cwd: projectPath });
|
|
336
|
+
});
|
|
337
|
+
if (!containsHClassReference(cmd) && !protectedOperand) {
|
|
338
|
+
return { behavior: 'allow' };
|
|
339
|
+
}
|
|
340
|
+
logger.warn(`[ReadonlyCheck] 🔒 Protected path blocked in readonly shell: cmd="${cmd}" session=${context?.sessionId} channel=${context?.channel} peer=${context?.peerId} role=${context?.role}`);
|
|
341
|
+
return { behavior: 'deny', message: '🔒 只读模式:ls/cat 不允许读取 H 类受保护路径' };
|
|
342
|
+
}
|
|
343
|
+
const cmdPreview = cmd.length > 80 ? cmd.substring(0, 80) + '...' : cmd;
|
|
344
|
+
logger.warn(`[ReadonlyCheck] 🔒 Bash blocked by default: cmd="${cmdPreview}" session=${context?.sessionId} channel=${context?.channel} peer=${context?.peerId} role=${context?.role}`);
|
|
345
|
+
return { behavior: 'deny', message: '🔒 只读模式:Shell 仅允许单个字面量 ls/cat 命令' };
|
|
346
|
+
}
|
|
347
|
+
logger.warn(`[ReadonlyCheck] 🔒 Unknown tool blocked: tool=${toolName} session=${context?.sessionId} channel=${context?.channel} peer=${context?.peerId} role=${context?.role}`);
|
|
348
|
+
return { behavior: 'deny', message: `🔒 只读模式:未声明为只读的工具 ${toolName} 已拒绝执行` };
|
|
349
|
+
}
|
|
350
|
+
const NON_FILESYSTEM_METADATA_TOOLS = new Set([
|
|
351
|
+
'Agent',
|
|
352
|
+
'AskUserQuestion',
|
|
353
|
+
'CronCreate',
|
|
354
|
+
'CronDelete',
|
|
355
|
+
'CronList',
|
|
356
|
+
'EnterPlanMode',
|
|
357
|
+
'ExitPlanMode',
|
|
358
|
+
'SendMessage',
|
|
359
|
+
'Skill',
|
|
360
|
+
'Task',
|
|
361
|
+
'TaskCreate',
|
|
362
|
+
'TaskGet',
|
|
363
|
+
'TaskList',
|
|
364
|
+
'TaskOutput',
|
|
365
|
+
'TaskStop',
|
|
366
|
+
'TaskUpdate',
|
|
367
|
+
'TeamCreate',
|
|
368
|
+
'TeamDelete',
|
|
369
|
+
'TodoWrite',
|
|
370
|
+
'ToolSearch',
|
|
371
|
+
'WebFetch',
|
|
372
|
+
'WebSearch',
|
|
373
|
+
]);
|
|
374
|
+
function collectOpaqueToolStrings(value, output) {
|
|
375
|
+
if (typeof value === 'string') {
|
|
376
|
+
if (value.trim())
|
|
377
|
+
output.push(value);
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
if (Array.isArray(value)) {
|
|
381
|
+
for (const entry of value)
|
|
382
|
+
collectOpaqueToolStrings(entry, output);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
if (!value || typeof value !== 'object')
|
|
386
|
+
return;
|
|
387
|
+
for (const entry of Object.values(value)) {
|
|
388
|
+
collectOpaqueToolStrings(entry, output);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
function collectPermissionPathSpec(value, output, projectRootSubpaths) {
|
|
392
|
+
if (typeof value === 'string') {
|
|
393
|
+
output.push(value);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
397
|
+
return;
|
|
398
|
+
const record = value;
|
|
399
|
+
if (typeof record.path === 'string')
|
|
400
|
+
output.push(record.path);
|
|
401
|
+
if (typeof record.pattern === 'string')
|
|
402
|
+
output.push(record.pattern);
|
|
403
|
+
if (record.type === 'special' && record.value && typeof record.value === 'object') {
|
|
404
|
+
const special = record.value;
|
|
405
|
+
const kind = special.kind;
|
|
406
|
+
if (kind === 'project_roots') {
|
|
407
|
+
const subpath = typeof special.subpath === 'string' && special.subpath ? special.subpath : '.';
|
|
408
|
+
if (projectRootSubpaths)
|
|
409
|
+
projectRootSubpaths.push(subpath);
|
|
410
|
+
else
|
|
411
|
+
output.push(subpath);
|
|
412
|
+
}
|
|
413
|
+
else if (kind === 'unknown' && typeof special.path === 'string' && special.path) {
|
|
414
|
+
const unknownPath = special.path;
|
|
415
|
+
const subpath = typeof special.subpath === 'string' && special.subpath ? special.subpath : undefined;
|
|
416
|
+
output.push(subpath
|
|
417
|
+
? (path.isAbsolute(subpath) ? subpath : path.join(unknownPath, subpath))
|
|
418
|
+
: unknownPath);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
function collectFilesystemGrantPaths(value, output, projectRootSubpaths) {
|
|
423
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
424
|
+
return;
|
|
425
|
+
const profile = value;
|
|
426
|
+
const fileSystem = profile.fileSystem ?? profile.filesystem;
|
|
427
|
+
if (!fileSystem || typeof fileSystem !== 'object' || Array.isArray(fileSystem))
|
|
428
|
+
return;
|
|
429
|
+
const fsProfile = fileSystem;
|
|
430
|
+
for (const key of ['read', 'write']) {
|
|
431
|
+
const paths = fsProfile[key];
|
|
432
|
+
if (Array.isArray(paths)) {
|
|
433
|
+
for (const candidate of paths)
|
|
434
|
+
collectPermissionPathSpec(candidate, output, projectRootSubpaths);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
if (Array.isArray(fsProfile.entries)) {
|
|
438
|
+
for (const entry of fsProfile.entries) {
|
|
439
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry))
|
|
440
|
+
continue;
|
|
441
|
+
const record = entry;
|
|
442
|
+
if (record.access === 'deny')
|
|
443
|
+
continue;
|
|
444
|
+
collectPermissionPathSpec(record.path, output, projectRootSubpaths);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
// Named-profile shaped overlays use path keys with read/write/deny values.
|
|
448
|
+
for (const [candidate, access] of Object.entries(fsProfile)) {
|
|
449
|
+
if (access === 'read' || access === 'write') {
|
|
450
|
+
if (candidate === ':root')
|
|
451
|
+
output.push(path.parse(process.cwd()).root);
|
|
452
|
+
else if (candidate === ':workspace_roots')
|
|
453
|
+
output.push('.');
|
|
454
|
+
else if (![':minimal', ':tmpdir', ':slash_tmp'].includes(candidate))
|
|
455
|
+
output.push(candidate);
|
|
456
|
+
}
|
|
457
|
+
if (!access || typeof access !== 'object' || Array.isArray(access))
|
|
458
|
+
continue;
|
|
459
|
+
for (const [subpath, nestedAccess] of Object.entries(access)) {
|
|
460
|
+
if (nestedAccess !== 'read' && nestedAccess !== 'write')
|
|
461
|
+
continue;
|
|
462
|
+
if (candidate === ':root')
|
|
463
|
+
output.push(path.resolve(path.parse(process.cwd()).root, subpath));
|
|
464
|
+
else if (candidate === ':workspace_roots')
|
|
465
|
+
output.push(subpath === '.' ? '.' : subpath);
|
|
466
|
+
else if (![':minimal', ':tmpdir', ':slash_tmp'].includes(candidate))
|
|
467
|
+
output.push(path.join(candidate, subpath));
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
function collectFilesystemWriteGrantPaths(value, output, projectRootSubpaths) {
|
|
472
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
473
|
+
return;
|
|
474
|
+
const profile = value;
|
|
475
|
+
const fileSystem = profile.fileSystem ?? profile.filesystem;
|
|
476
|
+
if (!fileSystem || typeof fileSystem !== 'object' || Array.isArray(fileSystem))
|
|
477
|
+
return;
|
|
478
|
+
const fsProfile = fileSystem;
|
|
479
|
+
if (Array.isArray(fsProfile.write)) {
|
|
480
|
+
for (const candidate of fsProfile.write)
|
|
481
|
+
collectPermissionPathSpec(candidate, output, projectRootSubpaths);
|
|
482
|
+
}
|
|
483
|
+
if (Array.isArray(fsProfile.entries)) {
|
|
484
|
+
for (const entry of fsProfile.entries) {
|
|
485
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry))
|
|
486
|
+
continue;
|
|
487
|
+
const record = entry;
|
|
488
|
+
if (record.access === 'write')
|
|
489
|
+
collectPermissionPathSpec(record.path, output, projectRootSubpaths);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
for (const [candidate, access] of Object.entries(fsProfile)) {
|
|
493
|
+
if (access === 'write') {
|
|
494
|
+
if (candidate === ':root')
|
|
495
|
+
output.push(path.parse(process.cwd()).root);
|
|
496
|
+
else if (candidate === ':workspace_roots')
|
|
497
|
+
output.push('.');
|
|
498
|
+
else if (![':minimal', ':tmpdir', ':slash_tmp'].includes(candidate))
|
|
499
|
+
output.push(candidate);
|
|
500
|
+
}
|
|
501
|
+
if (!access || typeof access !== 'object' || Array.isArray(access))
|
|
502
|
+
continue;
|
|
503
|
+
for (const [subpath, nestedAccess] of Object.entries(access)) {
|
|
504
|
+
if (nestedAccess !== 'write')
|
|
505
|
+
continue;
|
|
506
|
+
if (candidate === ':root')
|
|
507
|
+
output.push(path.resolve(path.parse(process.cwd()).root, subpath));
|
|
508
|
+
else if (candidate === ':workspace_roots')
|
|
509
|
+
output.push(subpath === '.' ? '.' : subpath);
|
|
510
|
+
else if (![':minimal', ':tmpdir', ':slash_tmp'].includes(candidate))
|
|
511
|
+
output.push(path.join(candidate, subpath));
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
function collectExplicitFileChangePaths(record, output) {
|
|
516
|
+
const explicitPathKeys = ['path', 'filePath', 'file_path', 'movePath', 'move_path', 'destinationPath', 'targetPath'];
|
|
517
|
+
const hasExplicitPath = explicitPathKeys.some(key => typeof record[key] === 'string');
|
|
518
|
+
for (const key of explicitPathKeys) {
|
|
519
|
+
if (typeof record[key] === 'string' && record[key])
|
|
520
|
+
output.push(record[key]);
|
|
521
|
+
}
|
|
522
|
+
if (record.kind && typeof record.kind === 'object') {
|
|
523
|
+
collectExplicitFileChangePaths(record.kind, output);
|
|
524
|
+
}
|
|
525
|
+
return hasExplicitPath;
|
|
526
|
+
}
|
|
527
|
+
function collectFileChangePaths(value, output) {
|
|
528
|
+
if (Array.isArray(value)) {
|
|
529
|
+
for (const entry of value) {
|
|
530
|
+
if (entry && typeof entry === 'object' && !Array.isArray(entry)) {
|
|
531
|
+
collectExplicitFileChangePaths(entry, output);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
if (!value || typeof value !== 'object')
|
|
537
|
+
return;
|
|
538
|
+
const record = value;
|
|
539
|
+
const hasExplicitPath = collectExplicitFileChangePaths(record, output);
|
|
540
|
+
// Claude-style inputs are maps keyed by path; Codex v2 uses an array of
|
|
541
|
+
// records with an explicit path field. Only treat keys as paths for the map
|
|
542
|
+
// shape so diff text and metadata never become path candidates.
|
|
543
|
+
if (!hasExplicitPath) {
|
|
544
|
+
for (const [filePath, change] of Object.entries(record)) {
|
|
545
|
+
if (filePath !== 'kind' && filePath !== 'type')
|
|
546
|
+
output.push(filePath);
|
|
547
|
+
if (change && typeof change === 'object' && !Array.isArray(change)) {
|
|
548
|
+
collectExplicitFileChangePaths(change, output);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
function shellCommandReferencesLClass(command, options) {
|
|
554
|
+
const normalized = command.replace(/\\/g, '/');
|
|
555
|
+
const root = path.resolve(options.root ?? resolveRoot()).replace(/\\/g, '/');
|
|
556
|
+
const protectedRoot = `${root.replace(/\/+$/, '')}/data/triggers`;
|
|
557
|
+
if (normalized.includes(protectedRoot))
|
|
558
|
+
return true;
|
|
559
|
+
const references = normalized.match(/(?:^|[\s"'=<>;&|()])((?:\.\.\/|\.\/)*data\/triggers(?:\/[^\s"';&|<>)]*)?)/g) ?? [];
|
|
560
|
+
return references.some(reference => {
|
|
561
|
+
const candidate = reference.replace(/^[\s"'=<>;&|()]+/, '');
|
|
562
|
+
return isLClassPath(candidate, options);
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
function isLClassMutatingShellCommand(command, options) {
|
|
566
|
+
if (!shellCommandReferencesLClass(command, options))
|
|
567
|
+
return false;
|
|
568
|
+
const mutationPatterns = [
|
|
569
|
+
/(?:^|[;&|]\s*)\s*(?:rm|unlink|rmdir|mv|rename|truncate|touch|chmod|chown|chgrp|mkdir)\b/i,
|
|
570
|
+
/(?:^|[;&|]\s*)\s*(?:sed\s+[^;&|]*\s-i\b|perl\s+[^;&|]*\s-pi\b)/i,
|
|
571
|
+
/(?:^|[;&|]\s*)\s*tee\b/i,
|
|
572
|
+
/\bfind\b[^;&|]*(?:-delete|-exec\s+(?:rm|mv|chmod|chown)\b)/i,
|
|
573
|
+
/\bgit\s+(?:checkout|restore|clean|reset)\b/i,
|
|
574
|
+
/(?:^|[^<])>{1,2}\s*[^;&|\r\n]*data[\\/]triggers(?:[\\/]|$)/i,
|
|
575
|
+
];
|
|
576
|
+
return mutationPatterns.some(pattern => pattern.test(command));
|
|
577
|
+
}
|
|
578
|
+
function requestsFilesystemRoot(value) {
|
|
579
|
+
if (Array.isArray(value))
|
|
580
|
+
return value.some(requestsFilesystemRoot);
|
|
581
|
+
if (!value || typeof value !== 'object')
|
|
582
|
+
return false;
|
|
583
|
+
const record = value;
|
|
584
|
+
if (record.access !== 'deny') {
|
|
585
|
+
const pathSpec = record.path;
|
|
586
|
+
if (pathSpec && typeof pathSpec === 'object' && !Array.isArray(pathSpec)) {
|
|
587
|
+
const pathRecord = pathSpec;
|
|
588
|
+
const special = pathRecord.type === 'special' ? pathRecord.value : undefined;
|
|
589
|
+
if (special && typeof special === 'object' && !Array.isArray(special)) {
|
|
590
|
+
if (special.kind === 'root')
|
|
591
|
+
return true;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return Object.values(record).some(requestsFilesystemRoot);
|
|
596
|
+
}
|
|
597
|
+
export function checkHClassWrite(toolName, input, context) {
|
|
598
|
+
const paths = [];
|
|
599
|
+
const grantPaths = [];
|
|
600
|
+
const projectRootGrantSubpaths = [];
|
|
601
|
+
if (['Read', 'Write', 'Edit', 'NotebookEdit', 'Glob', 'Grep'].includes(toolName)) {
|
|
602
|
+
const filePath = input.file_path ?? input.notebook_path ?? input.path ?? input.pattern;
|
|
603
|
+
if (typeof filePath === 'string' && filePath)
|
|
604
|
+
paths.push(filePath);
|
|
605
|
+
}
|
|
606
|
+
else if (toolName === 'FileChange' || toolName === 'PermissionGrant') {
|
|
607
|
+
const grantRoot = input.grantRoot;
|
|
608
|
+
if (typeof grantRoot === 'string' && grantRoot)
|
|
609
|
+
grantPaths.push(grantRoot);
|
|
610
|
+
const fileChanges = input.fileChanges;
|
|
611
|
+
collectFileChangePaths(fileChanges, paths);
|
|
612
|
+
collectFilesystemGrantPaths(input.permissions, grantPaths, projectRootGrantSubpaths);
|
|
613
|
+
}
|
|
614
|
+
else if (toolName === 'Bash') {
|
|
615
|
+
const command = typeof input.command === 'string' ? input.command.replace(/\\/g, '/') : '';
|
|
616
|
+
if (containsHClassReference(command)) {
|
|
617
|
+
logger.warn(`[H-Class Protection] 🔒 Protected path referenced by shell command: tool=${toolName} ` +
|
|
618
|
+
`session=${context?.sessionId} channel=${context?.channel} peer=${context?.peerId} role=${context?.role}`);
|
|
619
|
+
return {
|
|
620
|
+
behavior: 'deny',
|
|
621
|
+
message: '🔒 Shell 命令涉及受保护的 H 类配置/证书/快照路径,agent 不可直接操作',
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
collectFilesystemGrantPaths(input.additionalPermissions, grantPaths, projectRootGrantSubpaths);
|
|
625
|
+
collectFilesystemGrantPaths(input.permissions, grantPaths, projectRootGrantSubpaths);
|
|
626
|
+
}
|
|
627
|
+
else if (!NON_FILESYSTEM_METADATA_TOOLS.has(toolName)) {
|
|
628
|
+
// Opaque external tools may run outside the Claude/Codex filesystem
|
|
629
|
+
// sandbox. Retain conservative string inspection for those tools, while
|
|
630
|
+
// excluding known orchestration tools whose payload is only metadata.
|
|
631
|
+
collectOpaqueToolStrings(input, paths);
|
|
632
|
+
}
|
|
633
|
+
// Agent/Skill/Task and other orchestration tools carry natural-language
|
|
634
|
+
// metadata, not direct filesystem operands. Treating every string in their
|
|
635
|
+
// payload as a path makes a prompt that merely mentions daemon.json or a
|
|
636
|
+
// *.json_ backup indistinguishable from an actual file access. Any nested
|
|
637
|
+
// filesystem operation is checked again when it reaches Read/Write/Bash,
|
|
638
|
+
// FileChange, or PermissionGrant above.
|
|
639
|
+
const pathOptions = { cwd: context?.projectPath, root: context?.root };
|
|
640
|
+
if (requestsFilesystemRoot(input.permissions) || requestsFilesystemRoot(input.additionalPermissions)) {
|
|
641
|
+
logger.warn(`[H-Class Protection] 🔒 Permission grant covers filesystem root: tool=${toolName} ` +
|
|
642
|
+
`session=${context?.sessionId} channel=${context?.channel} peer=${context?.peerId} role=${context?.role}`);
|
|
643
|
+
return {
|
|
644
|
+
behavior: 'deny',
|
|
645
|
+
message: '🔒 请求的文件系统根权限包含受保护的 H 类路径,已拒绝授权',
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
for (const grantPath of grantPaths) {
|
|
649
|
+
if (!grantPath)
|
|
650
|
+
continue;
|
|
651
|
+
if (containsHClassReference(grantPath) || hClassGrantIncludesProtectedPath(grantPath, pathOptions)) {
|
|
652
|
+
logger.warn(`[H-Class Protection] 🔒 Permission grant covers protected paths: tool=${toolName} path=${grantPath} ` +
|
|
653
|
+
`session=${context?.sessionId} channel=${context?.channel} peer=${context?.peerId} role=${context?.role}`);
|
|
654
|
+
return {
|
|
655
|
+
behavior: 'deny',
|
|
656
|
+
message: '🔒 请求的文件系统权限范围包含受保护的 H 类路径,已拒绝授权',
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
const permissionWorkspace = context?.workspacePath ?? context?.projectPath;
|
|
661
|
+
for (const subpath of projectRootGrantSubpaths) {
|
|
662
|
+
// Codex `project_roots` is relative to the thread workspace, not to the
|
|
663
|
+
// command/permission-request cwd. Keep the two coordinate systems apart.
|
|
664
|
+
if (!permissionWorkspace) {
|
|
665
|
+
return {
|
|
666
|
+
behavior: 'deny',
|
|
667
|
+
message: '🔒 无法确定 project_roots 权限的 thread workspace,已拒绝授权',
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
const candidate = path.isAbsolute(subpath)
|
|
671
|
+
? subpath
|
|
672
|
+
: path.resolve(permissionWorkspace, subpath);
|
|
673
|
+
if (containsHClassReference(subpath) || hClassGrantIncludesProtectedPath(candidate, { root: context?.root })) {
|
|
674
|
+
logger.warn(`[H-Class Protection] 🔒 project_roots grant covers protected paths: tool=${toolName} path=${subpath} ` +
|
|
675
|
+
`workspace=${permissionWorkspace} session=${context?.sessionId} channel=${context?.channel} ` +
|
|
676
|
+
`peer=${context?.peerId} role=${context?.role}`);
|
|
677
|
+
return {
|
|
678
|
+
behavior: 'deny',
|
|
679
|
+
message: '🔒 请求的 project_roots 权限范围包含受保护的 H 类路径,已拒绝授权',
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
for (const filePath of paths) {
|
|
684
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
685
|
+
const matched = containsHClassReference(normalized) || isHClassPath(filePath, pathOptions);
|
|
686
|
+
if (matched) {
|
|
687
|
+
const fileBasename = path.basename(filePath);
|
|
688
|
+
logger.warn(`[H-Class Protection] 🔒 Protected file access blocked: tool=${toolName} path=${filePath} ` +
|
|
689
|
+
`session=${context?.sessionId} channel=${context?.channel} peer=${context?.peerId} role=${context?.role}`);
|
|
690
|
+
// 根据文件类型返回不同的提示信息
|
|
691
|
+
let suggestion = '';
|
|
692
|
+
if (fileBasename === 'config.json' || fileBasename === 'defaults.json' || fileBasename === 'daemon.json') {
|
|
693
|
+
suggestion = '\n\n💡 请使用配置命令操作:\n' +
|
|
694
|
+
' • 查看配置:ec config show --self <aid>\n' +
|
|
695
|
+
' • 读取字段:ec config get <field> --self <aid>\n' +
|
|
696
|
+
' • 修改字段:ec config set <field> <value> --self <aid>\n' +
|
|
697
|
+
' • 帮助文档:ec config --help';
|
|
698
|
+
}
|
|
699
|
+
else if (normalized.includes('/backups/config/') || normalized.includes('/.snapshots/')) {
|
|
700
|
+
suggestion = '\n\n💡 快照由系统自动管理,请使用:\n' +
|
|
701
|
+
' • 创建快照:ec config snapshot --desc "说明"\n' +
|
|
702
|
+
' • 查看历史:ec config history\n' +
|
|
703
|
+
' • 恢复快照:ec config restore <version>';
|
|
704
|
+
}
|
|
705
|
+
else if (normalized.includes('/cert/') || normalized.includes('/keys/') || normalized.includes('/CA/')) {
|
|
706
|
+
suggestion = '\n\n💡 证书和密钥由系统自动管理,请使用:\n' +
|
|
707
|
+
' • 查看证书:ec aid cert show <aid>\n' +
|
|
708
|
+
' • 更新证书:ec aid cert renew <aid>';
|
|
709
|
+
}
|
|
710
|
+
return {
|
|
711
|
+
behavior: 'deny',
|
|
712
|
+
message: `🔒 此文件受保护,agent 不可直接写入\n\n` +
|
|
713
|
+
`文件:${filePath}\n` +
|
|
714
|
+
`类型:配置/快照/证书等系统关键文件${suggestion}`
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
return { behavior: 'allow' };
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
* L-class protection: agents may inspect these paths, but only trusted daemon
|
|
722
|
+
* operations may mutate, delete, move, replace, or grant write access to them.
|
|
723
|
+
*/
|
|
724
|
+
export function checkLClassWrite(toolName, input, context) {
|
|
725
|
+
if (['Read', 'Glob', 'Grep'].includes(toolName))
|
|
726
|
+
return { behavior: 'allow' };
|
|
727
|
+
const paths = [];
|
|
728
|
+
const writeGrantPaths = [];
|
|
729
|
+
const projectRootWriteSubpaths = [];
|
|
730
|
+
if (['Write', 'Edit', 'NotebookEdit'].includes(toolName)) {
|
|
731
|
+
const filePath = input.file_path ?? input.notebook_path ?? input.path;
|
|
732
|
+
if (typeof filePath === 'string' && filePath)
|
|
733
|
+
paths.push(filePath);
|
|
734
|
+
}
|
|
735
|
+
else if (toolName === 'FileChange') {
|
|
736
|
+
const grantRoot = input.grantRoot;
|
|
737
|
+
if (typeof grantRoot === 'string' && grantRoot)
|
|
738
|
+
writeGrantPaths.push(grantRoot);
|
|
739
|
+
collectFileChangePaths(input.fileChanges, paths);
|
|
740
|
+
}
|
|
741
|
+
else if (toolName === 'PermissionGrant') {
|
|
742
|
+
const grantRoot = input.grantRoot;
|
|
743
|
+
if (typeof grantRoot === 'string' && grantRoot)
|
|
744
|
+
writeGrantPaths.push(grantRoot);
|
|
745
|
+
collectFilesystemWriteGrantPaths(input.permissions, writeGrantPaths, projectRootWriteSubpaths);
|
|
746
|
+
}
|
|
747
|
+
else if (toolName === 'Bash') {
|
|
748
|
+
const command = typeof input.command === 'string' ? input.command.replace(/\\/g, '/') : '';
|
|
749
|
+
const pathOptions = { cwd: context?.projectPath, root: context?.root };
|
|
750
|
+
if (isLClassMutatingShellCommand(command, pathOptions)) {
|
|
751
|
+
logger.warn(`[L-Class Protection] Protected path mutation requested by shell command: tool=${toolName} ` +
|
|
752
|
+
`session=${context?.sessionId} channel=${context?.channel} peer=${context?.peerId} role=${context?.role}`);
|
|
753
|
+
return {
|
|
754
|
+
behavior: 'deny',
|
|
755
|
+
message: '🔒 Shell 命令试图修改 L-class 只读路径;读取允许,修改请使用 ec trigger',
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
collectFilesystemWriteGrantPaths(input.additionalPermissions, writeGrantPaths, projectRootWriteSubpaths);
|
|
759
|
+
collectFilesystemWriteGrantPaths(input.permissions, writeGrantPaths, projectRootWriteSubpaths);
|
|
760
|
+
}
|
|
761
|
+
else if (!NON_FILESYSTEM_METADATA_TOOLS.has(toolName)) {
|
|
762
|
+
collectOpaqueToolStrings(input, paths);
|
|
763
|
+
}
|
|
764
|
+
// Do not infer mutation targets from arbitrary orchestration-tool strings.
|
|
765
|
+
// Skills commonly pass args: "" and delegation prompts may discuss
|
|
766
|
+
// protected paths; neither is itself a filesystem mutation.
|
|
767
|
+
const pathOptions = { cwd: context?.projectPath, root: context?.root };
|
|
768
|
+
for (const grantPath of writeGrantPaths) {
|
|
769
|
+
if (grantPath && lClassGrantIncludesProtectedPath(grantPath, pathOptions)) {
|
|
770
|
+
logger.warn(`[L-Class Protection] Write grant covers protected paths: tool=${toolName} path=${grantPath}`);
|
|
771
|
+
return {
|
|
772
|
+
behavior: 'deny',
|
|
773
|
+
message: '🔒 请求的文件系统写权限包含 L-class 只读路径,已拒绝授权',
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
const permissionWorkspace = context?.workspacePath ?? context?.projectPath;
|
|
778
|
+
for (const subpath of projectRootWriteSubpaths) {
|
|
779
|
+
if (!permissionWorkspace) {
|
|
780
|
+
return { behavior: 'deny', message: '🔒 无法确定 L-class 写权限的 thread workspace,已拒绝授权' };
|
|
781
|
+
}
|
|
782
|
+
const candidate = path.isAbsolute(subpath) ? subpath : path.resolve(permissionWorkspace, subpath);
|
|
783
|
+
if (lClassGrantIncludesProtectedPath(candidate, { root: context?.root })) {
|
|
784
|
+
return { behavior: 'deny', message: '🔒 请求的 project_roots 写权限包含 L-class 只读路径,已拒绝授权' };
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
for (const filePath of paths) {
|
|
788
|
+
if (!lClassGrantIncludesProtectedPath(filePath, pathOptions))
|
|
789
|
+
continue;
|
|
790
|
+
logger.warn(`[L-Class Protection] Protected file mutation blocked: tool=${toolName} path=${filePath} ` +
|
|
791
|
+
`session=${context?.sessionId} channel=${context?.channel} peer=${context?.peerId} role=${context?.role}`);
|
|
792
|
+
return {
|
|
793
|
+
behavior: 'deny',
|
|
794
|
+
message: `🔒 L-class 路径允许读取但禁止 Agent 直接修改、删除或移动\n\n文件:${filePath}\n\n💡 请使用 ec trigger 命令修改`,
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
return { behavior: 'allow' };
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* 绝对禁止检查(用于 PreToolUse hook)
|
|
801
|
+
* 检查系统级破坏操作,这些命令任何情况下都不允许执行
|
|
802
|
+
*/
|
|
803
|
+
export function checkBlacklist(toolName, input) {
|
|
804
|
+
// 只检查 Bash 工具,其余工具全部放行
|
|
805
|
+
if (toolName === 'Bash') {
|
|
806
|
+
const cmd = input.command || '';
|
|
807
|
+
// 空命令直接放行
|
|
808
|
+
if (!cmd || cmd.trim() === '') {
|
|
809
|
+
return { behavior: 'allow', updatedInput: input };
|
|
810
|
+
}
|
|
811
|
+
if (containsNetStopCommand(cmd)) {
|
|
812
|
+
const cmdPreview = cmd.length > 60 ? cmd.substring(0, 60) + '...' : cmd;
|
|
813
|
+
return {
|
|
814
|
+
behavior: 'deny',
|
|
815
|
+
message: `🚫 系统级危险操作已拦截\n命令:${cmdPreview}\n\n此类操作可能导致系统损坏,任何权限模式下都不允许执行。`,
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
// 检查绝对禁止命令(系统级破坏操作)
|
|
819
|
+
for (const pattern of ABSOLUTE_FORBIDDEN) {
|
|
820
|
+
if (pattern.test(cmd)) {
|
|
821
|
+
const cmdPreview = cmd.length > 60 ? cmd.substring(0, 60) + '...' : cmd;
|
|
822
|
+
return {
|
|
823
|
+
behavior: 'deny',
|
|
824
|
+
message: `🚫 系统级危险操作已拦截\n命令:${cmdPreview}\n\n此类操作可能导致系统损坏,任何权限模式下都不允许执行。`
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
// 默认允许
|
|
830
|
+
return { behavior: 'allow', updatedInput: input };
|
|
831
|
+
}
|
|
832
|
+
function validateBoundedOutputPath(command, context) {
|
|
833
|
+
const outputPath = command.outputPath;
|
|
834
|
+
if (!outputPath)
|
|
835
|
+
return undefined;
|
|
836
|
+
if (context.permissionMode === 'readonly')
|
|
837
|
+
return '🔒 只读模式:禁止将命令输出写入文件';
|
|
838
|
+
if (path.isAbsolute(outputPath))
|
|
839
|
+
return '🔒 输出文件必须使用项目目录内的相对路径';
|
|
840
|
+
const workspace = resolveProtectedCandidate(context.workspacePath ?? context.projectPath);
|
|
841
|
+
const cwd = resolveProtectedCandidate(context.projectPath);
|
|
842
|
+
if (!isSameOrDescendant(cwd, workspace))
|
|
843
|
+
return '🔒 命令工作目录不在当前项目范围内';
|
|
844
|
+
const target = path.resolve(cwd, outputPath);
|
|
845
|
+
const canonicalTarget = resolveProtectedCandidate(target);
|
|
846
|
+
if (!isSameOrDescendant(canonicalTarget, workspace))
|
|
847
|
+
return '🔒 输出文件不能越出当前项目目录';
|
|
848
|
+
const relative = path.relative(workspace, canonicalTarget);
|
|
849
|
+
if (relative === '.git' || relative.startsWith(`.git${path.sep}`)) {
|
|
850
|
+
return '🔒 命令输出不能直接写入 Git metadata';
|
|
851
|
+
}
|
|
852
|
+
const root = context.root ?? resolveRoot();
|
|
853
|
+
if (isHClassPath(canonicalTarget, { root }) || isLClassPath(canonicalTarget, { root })) {
|
|
854
|
+
return '🔒 命令输出不能写入 EvolCore 受保护路径';
|
|
855
|
+
}
|
|
856
|
+
if (fs.existsSync(canonicalTarget))
|
|
857
|
+
return '🔒 安全输出只允许创建新文件,不能覆盖或追加现有文件';
|
|
858
|
+
try {
|
|
859
|
+
if (!fs.statSync(path.dirname(canonicalTarget)).isDirectory())
|
|
860
|
+
return '🔒 输出文件的父目录无效';
|
|
861
|
+
}
|
|
862
|
+
catch {
|
|
863
|
+
return '🔒 输出文件的父目录必须已经存在';
|
|
864
|
+
}
|
|
865
|
+
return undefined;
|
|
866
|
+
}
|
|
867
|
+
function quotePosixShellArg(value) {
|
|
868
|
+
return `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
869
|
+
}
|
|
870
|
+
function prepareBoundedOutputInput(input, command, kind, context) {
|
|
871
|
+
const validationError = validateBoundedOutputPath(command, context);
|
|
872
|
+
if (validationError)
|
|
873
|
+
return { behavior: 'deny', input, message: validationError };
|
|
874
|
+
const helperPath = context.safeOutputHelperPath;
|
|
875
|
+
if (!helperPath) {
|
|
876
|
+
return {
|
|
877
|
+
behavior: 'allow',
|
|
878
|
+
input,
|
|
879
|
+
reason: kind === 'ec' ? 'ec-command' : 'host-process-command',
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
try {
|
|
883
|
+
if (!fs.statSync(helperPath).isFile())
|
|
884
|
+
throw new Error('not a file');
|
|
885
|
+
}
|
|
886
|
+
catch {
|
|
887
|
+
return { behavior: 'deny', input, message: '🔒 安全输出 helper 不可用,命令已拒绝' };
|
|
888
|
+
}
|
|
889
|
+
const payload = Buffer.from(JSON.stringify({
|
|
890
|
+
v: 1,
|
|
891
|
+
nonce: randomUUID(),
|
|
892
|
+
kind,
|
|
893
|
+
workspace: path.resolve(context.workspacePath ?? context.projectPath),
|
|
894
|
+
cwd: path.resolve(context.projectPath),
|
|
895
|
+
argv: command.argv,
|
|
896
|
+
mergeStderr: command.mergeStderr,
|
|
897
|
+
...(command.headLines ? { headLines: command.headLines } : {}),
|
|
898
|
+
...(command.outputPath ? { outputPath: command.outputPath } : {}),
|
|
899
|
+
}), 'utf8').toString('base64url');
|
|
900
|
+
const rewrittenCommand = `${quotePosixShellArg(process.execPath)} ${quotePosixShellArg(helperPath)} ${quotePosixShellArg(payload)}`;
|
|
901
|
+
return {
|
|
902
|
+
behavior: 'allow',
|
|
903
|
+
input: { ...input, command: rewrittenCommand },
|
|
904
|
+
reason: kind === 'ec' ? 'ec-command' : 'host-process-command',
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
/** Shared fail-closed preflight used before runner-specific mode and approval handling. */
|
|
908
|
+
export function evaluateToolPreflight(toolName, input, context) {
|
|
909
|
+
if (toolName === 'Bash') {
|
|
910
|
+
const command = typeof input.command === 'string' ? input.command : '';
|
|
911
|
+
const ecCommand = classifyEvolcoreShellCommand(command);
|
|
912
|
+
if (ecCommand.kind === 'literal') {
|
|
913
|
+
return { behavior: 'allow', input, reason: 'ec-command' };
|
|
914
|
+
}
|
|
915
|
+
if (ecCommand.kind === 'bounded-output') {
|
|
916
|
+
return prepareBoundedOutputInput(input, ecCommand.command, 'ec', context);
|
|
917
|
+
}
|
|
918
|
+
if (ecCommand.kind === 'composite') {
|
|
919
|
+
return {
|
|
920
|
+
behavior: 'deny',
|
|
921
|
+
input,
|
|
922
|
+
message: '🔒 EC 命令必须作为单个字面量进程调用;请移除管道、重定向、变量展开或命令拼接',
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
const boundedOutput = parseBoundedOutputShellCommand(command);
|
|
926
|
+
const hostProcessCommand = boundedOutput
|
|
927
|
+
? classifyHostProcessArgv(boundedOutput.argv)
|
|
928
|
+
: classifyHostProcessCommand(command);
|
|
929
|
+
if (context.allowHostProcessCommands === true
|
|
930
|
+
&& context.permissionMode !== 'readonly'
|
|
931
|
+
&& isOwnerOrAdminRole(context.role)
|
|
932
|
+
&& hostProcessCommand) {
|
|
933
|
+
if (boundedOutput)
|
|
934
|
+
return prepareBoundedOutputInput(input, boundedOutput, 'host-process', context);
|
|
935
|
+
return { behavior: 'allow', input, reason: 'host-process-command' };
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
const blacklist = checkBlacklist(toolName, input);
|
|
939
|
+
if (blacklist.behavior === 'deny') {
|
|
940
|
+
return { behavior: 'deny', input, message: blacklist.message };
|
|
941
|
+
}
|
|
942
|
+
const checkedInput = blacklist.updatedInput;
|
|
943
|
+
const hClass = checkHClassWrite(toolName, checkedInput, {
|
|
944
|
+
sessionId: context.sessionId,
|
|
945
|
+
channel: context.channel,
|
|
946
|
+
peerId: context.userId,
|
|
947
|
+
role: context.role,
|
|
948
|
+
projectPath: context.projectPath,
|
|
949
|
+
workspacePath: context.workspacePath,
|
|
950
|
+
root: context.root,
|
|
951
|
+
});
|
|
952
|
+
if (hClass.behavior === 'deny') {
|
|
953
|
+
return { behavior: 'deny', input: checkedInput, message: hClass.message };
|
|
954
|
+
}
|
|
955
|
+
const lClass = checkLClassWrite(toolName, checkedInput, {
|
|
956
|
+
sessionId: context.sessionId,
|
|
957
|
+
channel: context.channel,
|
|
958
|
+
peerId: context.userId,
|
|
959
|
+
role: context.role,
|
|
960
|
+
projectPath: context.projectPath,
|
|
961
|
+
workspacePath: context.workspacePath,
|
|
962
|
+
root: context.root,
|
|
963
|
+
});
|
|
964
|
+
if (lClass.behavior === 'deny') {
|
|
965
|
+
return { behavior: 'deny', input: checkedInput, message: lClass.message };
|
|
966
|
+
}
|
|
967
|
+
const dangerous = checkDangerousCommand(toolName, checkedInput);
|
|
968
|
+
if (dangerous.isDangerous
|
|
969
|
+
&& dangerous.kind === 'git-destructive'
|
|
970
|
+
&& context.role !== undefined
|
|
971
|
+
&& !isOwnerOrAdminRole(context.role)) {
|
|
972
|
+
return {
|
|
973
|
+
behavior: 'deny',
|
|
974
|
+
input: checkedInput,
|
|
975
|
+
message: '🔒 Git 破坏性操作仅允许 owner/admin 调用;当前角色无权执行',
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
return { behavior: 'continue', input: checkedInput };
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* 危险命令检测(用于 canUseTool callback)
|
|
982
|
+
* 检测需要用户审批的危险操作,返回是否匹配及风险说明
|
|
983
|
+
*/
|
|
984
|
+
export function checkDangerousCommand(toolName, input) {
|
|
985
|
+
if (toolName !== 'Bash') {
|
|
986
|
+
return { isDangerous: false };
|
|
987
|
+
}
|
|
988
|
+
const cmd = input.command || '';
|
|
989
|
+
if (!cmd || cmd.trim() === '') {
|
|
990
|
+
return { isDangerous: false };
|
|
991
|
+
}
|
|
992
|
+
const structured = classifyStructuredCommand(cmd);
|
|
993
|
+
if (structured) {
|
|
994
|
+
const cmdPreview = cmd.length > 80 ? cmd.substring(0, 80) + '...' : cmd;
|
|
995
|
+
return {
|
|
996
|
+
isDangerous: true,
|
|
997
|
+
kind: structured.kind,
|
|
998
|
+
cacheKey: `dangerous:${toolName}:${structured.kind}`,
|
|
999
|
+
command: cmdPreview,
|
|
1000
|
+
reason: structured.reason,
|
|
1001
|
+
approvalScope: structured.approvalScope,
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
// 检查危险操作模式
|
|
1005
|
+
for (const rule of DANGEROUS_PATTERNS) {
|
|
1006
|
+
if (rule.pattern.test(cmd)) {
|
|
1007
|
+
const cmdPreview = cmd.length > 80 ? cmd.substring(0, 80) + '...' : cmd;
|
|
1008
|
+
return {
|
|
1009
|
+
isDangerous: true,
|
|
1010
|
+
kind: rule.kind,
|
|
1011
|
+
cacheKey: `dangerous:${toolName}:${rule.kind}`,
|
|
1012
|
+
command: cmdPreview,
|
|
1013
|
+
reason: rule.reason,
|
|
1014
|
+
approvalScope: rule.approvalScope,
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
return { isDangerous: false };
|
|
1019
|
+
}
|