aegiscode 3.1.8 → 3.1.10
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/README.md +23 -15
- package/dist/main.js +549 -552
- package/package.json +4 -2
- package/src/agent/Agent.ts +903 -0
- package/src/agent/SimpleAgent.ts +48 -0
- package/src/agent/index.ts +54 -0
- package/src/agent/orchestrator/AppBuilder.ts +443 -0
- package/src/agent/orchestrator/CouncilAgent.ts +310 -0
- package/src/agent/orchestrator/DiscussionRoom.ts +462 -0
- package/src/agent/orchestrator/OrchestratorAgent.ts +637 -0
- package/src/agent/orchestrator/index.ts +38 -0
- package/src/agent/orchestrator/utils.ts +397 -0
- package/src/agent/pricing.ts +115 -0
- package/src/agent/router.ts +74 -0
- package/src/agent/routerStats.ts +121 -0
- package/src/agent/types.ts +318 -0
- package/src/auth/login.ts +383 -0
- package/src/cli/config.ts +189 -0
- package/src/cli/index.ts +17 -0
- package/src/cli/middleware.ts +119 -0
- package/src/cli/types.ts +75 -0
- package/src/config/ConfigManager.ts +587 -0
- package/src/config/index.ts +7 -0
- package/src/config/types.ts +584 -0
- package/src/context/CompactionService.ts +300 -0
- package/src/context/ContextManager.ts +450 -0
- package/src/context/FileAnalyzer.ts +267 -0
- package/src/context/TokenCounter.ts +265 -0
- package/src/context/index.ts +27 -0
- package/src/context/storage/CacheStore.ts +176 -0
- package/src/context/storage/JSONLStore.ts +201 -0
- package/src/context/storage/MemoryStore.ts +205 -0
- package/src/context/storage/PersistentStore.ts +327 -0
- package/src/context/storage/index.ts +9 -0
- package/src/context/storage/pathUtils.ts +114 -0
- package/src/context/test.ts +309 -0
- package/src/context/types.ts +268 -0
- package/src/hooks/HookExecutor.ts +434 -0
- package/src/hooks/HookManager.ts +596 -0
- package/src/hooks/HookService.ts +269 -0
- package/src/hooks/Matcher.ts +157 -0
- package/src/hooks/index.ts +63 -0
- package/src/hooks/types.ts +424 -0
- package/src/main.tsx +596 -0
- package/src/mcp/HealthMonitor.ts +150 -0
- package/src/mcp/McpClient.ts +491 -0
- package/src/mcp/McpRegistry.ts +321 -0
- package/src/mcp/createMcpTool.ts +251 -0
- package/src/mcp/index.ts +15 -0
- package/src/mcp/server.ts +334 -0
- package/src/mcp/test-server.ts +88 -0
- package/src/mcp/test.ts +372 -0
- package/src/mcp/types.ts +247 -0
- package/src/memory/AgentMemoryBus.ts +432 -0
- package/src/memory/CloudSync.ts +99 -0
- package/src/memory/DriveSync.ts +106 -0
- package/src/memory/SharedMemory.ts +951 -0
- package/src/memory/index.ts +14 -0
- package/src/memory/machineFingerprint.ts +40 -0
- package/src/orchestrator/SubAgentMetadata.ts +136 -0
- package/src/prompts/builder.ts +213 -0
- package/src/prompts/default.ts +144 -0
- package/src/prompts/index.ts +16 -0
- package/src/prompts/plan.ts +64 -0
- package/src/prompts/test.ts +78 -0
- package/src/services/AnthropicChatService.ts +341 -0
- package/src/services/ChatService.ts +347 -0
- package/src/services/ClaudeCliChatService.ts +256 -0
- package/src/services/CloudSync.ts +168 -0
- package/src/services/CostLedger.ts +211 -0
- package/src/services/Heartbeat.ts +135 -0
- package/src/services/LearningCollector.ts +291 -0
- package/src/services/OllamaInstaller.ts +342 -0
- package/src/services/VersionChecker.ts +445 -0
- package/src/services/index.ts +57 -0
- package/src/services/streaming/OpenAIEventAdapter.ts +126 -0
- package/src/services/streaming/RenderingProfile.ts +90 -0
- package/src/services/streaming/StreamEventParser.ts +181 -0
- package/src/services/streaming/ThrottledRenderer.ts +139 -0
- package/src/services/streaming/TranscriptBuffer.ts +574 -0
- package/src/services/streaming/eventStatusMap.ts +52 -0
- package/src/services/streaming/index.ts +46 -0
- package/src/services/streaming/renderFormatting.ts +79 -0
- package/src/services/streaming/types.ts +234 -0
- package/src/skills/SkillLoader.ts +126 -0
- package/src/skills/SkillRegistry.ts +366 -0
- package/src/skills/index.ts +48 -0
- package/src/skills/types.ts +146 -0
- package/src/slash-commands/billing.ts +70 -0
- package/src/slash-commands/build.ts +413 -0
- package/src/slash-commands/builtinCommands.ts +2733 -0
- package/src/slash-commands/clone.ts +242 -0
- package/src/slash-commands/council.ts +125 -0
- package/src/slash-commands/custom/CustomCommandExecutor.ts +143 -0
- package/src/slash-commands/custom/CustomCommandLoader.ts +251 -0
- package/src/slash-commands/custom/CustomCommandRegistry.ts +144 -0
- package/src/slash-commands/custom/index.ts +7 -0
- package/src/slash-commands/debate.ts +254 -0
- package/src/slash-commands/gmail.ts +105 -0
- package/src/slash-commands/index.ts +388 -0
- package/src/slash-commands/mcpCommand.ts +205 -0
- package/src/slash-commands/types.ts +201 -0
- package/src/store/index.ts +76 -0
- package/src/store/selectors.ts +246 -0
- package/src/store/slices/appSlice.ts +205 -0
- package/src/store/slices/commandSlice.ts +115 -0
- package/src/store/slices/configSlice.ts +46 -0
- package/src/store/slices/focusSlice.ts +64 -0
- package/src/store/slices/index.ts +9 -0
- package/src/store/slices/sessionSlice.ts +424 -0
- package/src/store/streaming-buffer.ts +425 -0
- package/src/store/test.ts +296 -0
- package/src/store/types.ts +274 -0
- package/src/store/vanilla.ts +186 -0
- package/src/tools/builtin/bash.ts +236 -0
- package/src/tools/builtin/council.ts +105 -0
- package/src/tools/builtin/edit.ts +213 -0
- package/src/tools/builtin/glob.ts +136 -0
- package/src/tools/builtin/grep.ts +263 -0
- package/src/tools/builtin/index.ts +61 -0
- package/src/tools/builtin/memory.ts +66 -0
- package/src/tools/builtin/read.ts +168 -0
- package/src/tools/builtin/skill.ts +97 -0
- package/src/tools/builtin/snapshot.ts +40 -0
- package/src/tools/builtin/task.ts +106 -0
- package/src/tools/builtin/write.ts +134 -0
- package/src/tools/createTool.ts +221 -0
- package/src/tools/execution/ExecutionPipeline.ts +263 -0
- package/src/tools/execution/index.ts +40 -0
- package/src/tools/execution/stages/CacheStage.ts +131 -0
- package/src/tools/execution/stages/ConfirmationStage.ts +203 -0
- package/src/tools/execution/stages/DiscoveryStage.ts +46 -0
- package/src/tools/execution/stages/ExecutionStage.ts +48 -0
- package/src/tools/execution/stages/FormattingStage.ts +44 -0
- package/src/tools/execution/stages/HookStage.ts +72 -0
- package/src/tools/execution/stages/PermissionStage.ts +287 -0
- package/src/tools/execution/stages/PostHookStage.ts +71 -0
- package/src/tools/execution/stages/index.ts +12 -0
- package/src/tools/execution/test.ts +266 -0
- package/src/tools/execution/types.ts +273 -0
- package/src/tools/index.ts +81 -0
- package/src/tools/registry.ts +304 -0
- package/src/tools/schemas.ts +109 -0
- package/src/tools/test.ts +220 -0
- package/src/tools/types.ts +175 -0
- package/src/tools/validation/PermissionChecker.ts +242 -0
- package/src/tools/validation/SensitiveFileDetector.ts +210 -0
- package/src/tools/validation/index.ts +11 -0
- package/src/ui/App.tsx +166 -0
- package/src/ui/components/AegisInterface.tsx +484 -0
- package/src/ui/components/common/ChatSearch.tsx +150 -0
- package/src/ui/components/common/ErrorBoundary.tsx +82 -0
- package/src/ui/components/common/ExitMessage.tsx +120 -0
- package/src/ui/components/common/LoadingIndicator.tsx +49 -0
- package/src/ui/components/common/index.ts +6 -0
- package/src/ui/components/dialog/ConfirmationPrompt.tsx +208 -0
- package/src/ui/components/dialog/InteractiveSelector.tsx +149 -0
- package/src/ui/components/dialog/SetupWizard.tsx +297 -0
- package/src/ui/components/dialog/UpdatePrompt.tsx +155 -0
- package/src/ui/components/dialog/index.ts +8 -0
- package/src/ui/components/index.ts +28 -0
- package/src/ui/components/input/CommandSuggestions.tsx +139 -0
- package/src/ui/components/input/CustomTextInput.tsx +220 -0
- package/src/ui/components/input/InputArea.tsx +361 -0
- package/src/ui/components/input/PromptSuggestions.tsx +66 -0
- package/src/ui/components/input/index.ts +6 -0
- package/src/ui/components/layout/ChatStatusBar.tsx +90 -0
- package/src/ui/components/layout/ContextBar.tsx +79 -0
- package/src/ui/components/layout/MessageArea.tsx +96 -0
- package/src/ui/components/layout/MessageList.tsx +647 -0
- package/src/ui/components/layout/MessageSeparator.tsx +26 -0
- package/src/ui/components/layout/WelcomeMessage.tsx +93 -0
- package/src/ui/components/layout/index.ts +7 -0
- package/src/ui/components/markdown/CodeHighlighter.tsx +292 -0
- package/src/ui/components/markdown/MessageRenderer.tsx +1211 -0
- package/src/ui/components/markdown/index.ts +8 -0
- package/src/ui/components/markdown/parser.ts +336 -0
- package/src/ui/components/markdown/types.ts +66 -0
- package/src/ui/focus/FocusManager.ts +137 -0
- package/src/ui/focus/index.ts +13 -0
- package/src/ui/focus/types.ts +54 -0
- package/src/ui/focus/useFocus.ts +75 -0
- package/src/ui/hooks/index.ts +11 -0
- package/src/ui/hooks/useAgent.ts +284 -0
- package/src/ui/hooks/useCommandHistory.ts +87 -0
- package/src/ui/hooks/useCommandProcessor.ts +443 -0
- package/src/ui/hooks/useConfirmation.ts +99 -0
- package/src/ui/hooks/useCtrlCHandler.ts +100 -0
- package/src/ui/hooks/useInputBuffer.ts +122 -0
- package/src/ui/hooks/useTerminalSize.ts +68 -0
- package/src/ui/hooks/useTerminalWidth.ts +5 -0
- package/src/ui/hooks/useWindowedList.ts +118 -0
- package/src/ui/render-debugger.ts +621 -0
- package/src/ui/test.ts +189 -0
- package/src/ui/themes/ThemeManager.ts +332 -0
- package/src/ui/themes/aegisTheme.ts +87 -0
- package/src/ui/themes/darkTheme.ts +87 -0
- package/src/ui/themes/defaultTheme.ts +85 -0
- package/src/ui/themes/index.ts +10 -0
- package/src/ui/themes/lightTheme.ts +89 -0
- package/src/ui/themes/popularThemes.ts +187 -0
- package/src/ui/themes/types.ts +130 -0
- package/src/utils/clipboard.ts +48 -0
- package/src/utils/debug.ts +43 -0
- package/src/utils/environment.ts +68 -0
- package/src/utils/index.ts +10 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permission Stage - 权限检查阶段
|
|
3
|
+
*
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { ToolKind } from '../../types.js';
|
|
7
|
+
import {
|
|
8
|
+
PermissionMode,
|
|
9
|
+
PermissionResult,
|
|
10
|
+
type PipelineStage,
|
|
11
|
+
type ToolExecution,
|
|
12
|
+
type PermissionConfig,
|
|
13
|
+
type PermissionCheckResult,
|
|
14
|
+
} from '../types.js';
|
|
15
|
+
import { PermissionChecker } from '../../validation/PermissionChecker.js';
|
|
16
|
+
import { SensitiveFileDetector, SensitivityLevel } from '../../validation/SensitiveFileDetector.js';
|
|
17
|
+
import { onPermissionRequest } from '../../../hooks/index.js';
|
|
18
|
+
|
|
19
|
+
export class PermissionStage implements PipelineStage {
|
|
20
|
+
readonly name = 'permission';
|
|
21
|
+
private permissionChecker: PermissionChecker;
|
|
22
|
+
private defaultMode: PermissionMode;
|
|
23
|
+
|
|
24
|
+
constructor(
|
|
25
|
+
config?: Partial<PermissionConfig>,
|
|
26
|
+
private sessionApprovals?: Set<string>,
|
|
27
|
+
private sessionDenials?: Set<string>,
|
|
28
|
+
defaultMode: PermissionMode = PermissionMode.DEFAULT
|
|
29
|
+
) {
|
|
30
|
+
this.permissionChecker = new PermissionChecker(config);
|
|
31
|
+
this.defaultMode = defaultMode;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async process(execution: ToolExecution): Promise<void> {
|
|
35
|
+
const tool = execution._internal.tool;
|
|
36
|
+
|
|
37
|
+
if (!tool) {
|
|
38
|
+
execution.abort('Tool not found in execution context');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 1. 创建工具调用实例(含 Zod 验
|
|
43
|
+
try {
|
|
44
|
+
const invocation = tool.build(execution.params);
|
|
45
|
+
execution._internal.invocation = invocation;
|
|
46
|
+
} catch (error) {
|
|
47
|
+
execution.abort(
|
|
48
|
+
`Parameter validation failed: ${error instanceof Error ? error.message : String(error)}`
|
|
49
|
+
);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 2. 构建权限签
|
|
54
|
+
const signature = PermissionChecker.buildSignature({
|
|
55
|
+
toolName: tool.name,
|
|
56
|
+
params: execution.params,
|
|
57
|
+
tool,
|
|
58
|
+
});
|
|
59
|
+
execution._internal.permissionSignature = signature;
|
|
60
|
+
|
|
61
|
+
// 3. 检查会话拒绝列
|
|
62
|
+
if (this.sessionDenials?.has(signature)) {
|
|
63
|
+
// Phrased to avoid "permission/denied" vocabulary: this exact string becomes
|
|
64
|
+
// llmContent in a real tool_result, and the model can later free-associate a
|
|
65
|
+
// hallucinated continuation of it in an unrelated turn with no tool call.
|
|
66
|
+
execution.abort('Not executed: you tried this exact action earlier in this session and the user declined it. Choose a different approach.');
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 4. 检查会话批准列
|
|
71
|
+
if (this.sessionApprovals?.has(signature)) {
|
|
72
|
+
// 已在会话中批准,跳过权限检
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 5. 执行权限检
|
|
77
|
+
let checkResult = this.permissionChecker.check({
|
|
78
|
+
toolName: tool.name,
|
|
79
|
+
params: execution.params,
|
|
80
|
+
tool,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// 6. 应用权限模式覆
|
|
84
|
+
const currentMode = execution.context.permissionMode || this.defaultMode;
|
|
85
|
+
checkResult = this.applyModeOverrides(tool.kind, checkResult, currentMode);
|
|
86
|
+
|
|
87
|
+
// 7. 根据结果采取行
|
|
88
|
+
switch (checkResult.result) {
|
|
89
|
+
case PermissionResult.DENY:
|
|
90
|
+
execution.abort(checkResult.reason || 'Not executed: this action is not allowed under current settings. Choose a different approach.');
|
|
91
|
+
return;
|
|
92
|
+
|
|
93
|
+
case PermissionResult.ASK:
|
|
94
|
+
// 执
|
|
95
|
+
const hookDecision = await this.executePermissionHook(execution, tool.name);
|
|
96
|
+
if (hookDecision === 'approve') {
|
|
97
|
+
// Hook 批准,跳过确
|
|
98
|
+
break;
|
|
99
|
+
} else if (hookDecision === 'deny') {
|
|
100
|
+
// Hook 拒
|
|
101
|
+
execution.abort('Not executed: a configured hook excludes this action. Choose a different approach.');
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
// Hook 返回 ask 或无 hook,继续标记需要确
|
|
105
|
+
execution._internal.needsConfirmation = true;
|
|
106
|
+
execution._internal.confirmationReason =
|
|
107
|
+
checkResult.reason || 'This operation requires confirmation';
|
|
108
|
+
break;
|
|
109
|
+
|
|
110
|
+
case PermissionResult.ALLOW:
|
|
111
|
+
// 继续执
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 7. 额外安全检查:敏感文
|
|
116
|
+
this.checkSensitiveFiles(execution);
|
|
117
|
+
|
|
118
|
+
// 8. 额外安全检查:终端操纵(tmux/screen 注入按键到另一个会话
|
|
119
|
+
this.checkTerminalPiloting(execution);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Detects Bash commands that pilot another terminal session by injecting
|
|
124
|
+
* keystrokes (tmux send-keys, screen -X stuff, etc.). This is the same
|
|
125
|
+
* vector a model used to mutate ~/.aegiscode/config.json via a second
|
|
126
|
+
* live aegis instance while looping unattended — the injected keystrokes
|
|
127
|
+
* are indistinguishable from real user input to the receiving process, so
|
|
128
|
+
* the only place to catch this is here, before the command runs. Forces
|
|
129
|
+
* confirmation regardless of requireConfirmation or session approvals.
|
|
130
|
+
*/
|
|
131
|
+
private checkTerminalPiloting(execution: ToolExecution): void {
|
|
132
|
+
const tool = execution._internal.tool;
|
|
133
|
+
if (!tool || tool.name !== 'Bash') return;
|
|
134
|
+
|
|
135
|
+
const command = execution.params.command;
|
|
136
|
+
if (typeof command !== 'string') return;
|
|
137
|
+
|
|
138
|
+
const PILOTING_PATTERNS = [
|
|
139
|
+
/\btmux\s+send-keys\b/,
|
|
140
|
+
/\btmux\s+(?:run-shell|wait-for)\b/,
|
|
141
|
+
/\bscreen\s+-S\s+\S+\s+-X\s+stuff\b/,
|
|
142
|
+
/\bexpect\s+-c\b/,
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
if (PILOTING_PATTERNS.some((re) => re.test(command))) {
|
|
146
|
+
execution._internal.needsConfirmation = true;
|
|
147
|
+
execution._internal.forceConfirmation = true;
|
|
148
|
+
execution._internal.confirmationReason =
|
|
149
|
+
'This command injects input into another terminal session — it can mutate shared ' +
|
|
150
|
+
'config/files exactly like a real user typing, bypassing normal confirmation settings.';
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
*
|
|
156
|
+
*/
|
|
157
|
+
private applyModeOverrides(
|
|
158
|
+
toolKind: ToolKind,
|
|
159
|
+
checkResult: PermissionCheckResult,
|
|
160
|
+
permissionMode: PermissionMode
|
|
161
|
+
): PermissionCheckResult {
|
|
162
|
+
// 1. YOLO 模式:批准所有(最高优先
|
|
163
|
+
if (permissionMode === PermissionMode.YOLO) {
|
|
164
|
+
return {
|
|
165
|
+
result: PermissionResult.ALLOW,
|
|
166
|
+
matchedRule: 'mode:yolo',
|
|
167
|
+
reason: 'YOLO mode: auto-approve all operations',
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 2. PLAN 模式:拒绝非只读工
|
|
172
|
+
if (permissionMode === PermissionMode.PLAN) {
|
|
173
|
+
if (toolKind !== ToolKind.ReadOnly) {
|
|
174
|
+
return {
|
|
175
|
+
result: PermissionResult.DENY,
|
|
176
|
+
matchedRule: 'mode:plan',
|
|
177
|
+
reason: 'Plan mode: only read-only tools allowed',
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// 3. 已被 deny 规则拒绝,不覆
|
|
183
|
+
if (checkResult.result === PermissionResult.DENY) {
|
|
184
|
+
return checkResult;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// 4. 已被 allow 规则批准,不覆
|
|
188
|
+
if (checkResult.result === PermissionResult.ALLOW) {
|
|
189
|
+
return checkResult;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// 5. 只读工具:所有模式下都批
|
|
193
|
+
if (toolKind === ToolKind.ReadOnly) {
|
|
194
|
+
return {
|
|
195
|
+
result: PermissionResult.ALLOW,
|
|
196
|
+
matchedRule: `mode:${permissionMode}:readonly`,
|
|
197
|
+
reason: 'Read-only tools are auto-approved',
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// 6. AUTO_EDIT 模式:批准 Write 工
|
|
202
|
+
if (permissionMode === PermissionMode.AUTO_EDIT && toolKind === ToolKind.Write) {
|
|
203
|
+
return {
|
|
204
|
+
result: PermissionResult.ALLOW,
|
|
205
|
+
matchedRule: 'mode:autoEdit:write',
|
|
206
|
+
reason: 'AUTO_EDIT mode: auto-approve write tools',
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// 7. 其他情况:保持原检查结果(通常
|
|
211
|
+
return checkResult;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
*
|
|
216
|
+
*/
|
|
217
|
+
private checkSensitiveFiles(execution: ToolExecution): void {
|
|
218
|
+
const tool = execution._internal.tool;
|
|
219
|
+
if (!tool) return;
|
|
220
|
+
|
|
221
|
+
// 只检查写入相关工
|
|
222
|
+
if (tool.kind === ToolKind.ReadOnly) return;
|
|
223
|
+
|
|
224
|
+
// 获取受影响的文件路
|
|
225
|
+
const affectedPaths = this.getAffectedPaths(execution);
|
|
226
|
+
if (affectedPaths.length === 0) return;
|
|
227
|
+
|
|
228
|
+
// 检查敏感文
|
|
229
|
+
const sensitiveFiles = SensitiveFileDetector.filterSensitive(
|
|
230
|
+
affectedPaths,
|
|
231
|
+
SensitivityLevel.MEDIUM
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
if (sensitiveFiles.length === 0) return;
|
|
235
|
+
|
|
236
|
+
// 高敏感文件直接拒
|
|
237
|
+
const highSensitive = sensitiveFiles.filter(
|
|
238
|
+
f => f.result.level === SensitivityLevel.HIGH
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
if (highSensitive.length > 0) {
|
|
242
|
+
const files = highSensitive.map(f => f.path).join(', ');
|
|
243
|
+
execution.abort(`Not executed: these files are excluded for safety — ${files}. Choose a different approach.`);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// 中敏感文件需要确
|
|
248
|
+
execution._internal.needsConfirmation = true;
|
|
249
|
+
const reasons = sensitiveFiles.map(f => `${f.path}: ${f.result.reason}`);
|
|
250
|
+
execution._internal.confirmationReason = `Sensitive file access detected:\n${reasons.join('\n')}`;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
*
|
|
255
|
+
*/
|
|
256
|
+
private getAffectedPaths(execution: ToolExecution): string[] {
|
|
257
|
+
const params = execution.params;
|
|
258
|
+
|
|
259
|
+
// 从常见参数名中提取路
|
|
260
|
+
const pathKeys = ['file_path', 'path', 'target', 'destination'];
|
|
261
|
+
const paths: string[] = [];
|
|
262
|
+
|
|
263
|
+
for (const key of pathKeys) {
|
|
264
|
+
if (typeof params[key] === 'string') {
|
|
265
|
+
paths.push(params[key] as string);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return paths;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
*
|
|
274
|
+
*/
|
|
275
|
+
private async executePermissionHook(
|
|
276
|
+
execution: ToolExecution,
|
|
277
|
+
toolName: string
|
|
278
|
+
): Promise<'approve' | 'deny' | 'ask'> {
|
|
279
|
+
return onPermissionRequest(
|
|
280
|
+
toolName,
|
|
281
|
+
execution.params as Record<string, unknown>,
|
|
282
|
+
execution.context.sessionId || 'unknown',
|
|
283
|
+
execution.context.workspaceRoot || process.cwd(),
|
|
284
|
+
execution.context.permissionMode
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post Hook Stage - PostToolUse Hooks 执行阶段
|
|
3
|
+
*
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { PipelineStage, ToolExecution } from '../types.js';
|
|
7
|
+
import { onPostToolUse, onPostToolUseFailure } from '../../../hooks/index.js';
|
|
8
|
+
|
|
9
|
+
export class PostHookStage implements PipelineStage {
|
|
10
|
+
readonly name = 'postHook';
|
|
11
|
+
|
|
12
|
+
async process(execution: ToolExecution): Promise<void> {
|
|
13
|
+
const result = execution.getResult();
|
|
14
|
+
if (!result) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const tool = execution._internal.tool;
|
|
19
|
+
if (!tool) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// 复用 PreToolUse 阶段生成
|
|
24
|
+
const toolUseId = execution._internal.hookToolUseId || `tool_post_${Date.now()}`;
|
|
25
|
+
const sessionId = execution.context.sessionId || 'unknown';
|
|
26
|
+
const projectDir = execution.context.workspaceRoot || process.cwd();
|
|
27
|
+
const permissionMode = execution.context.permissionMode;
|
|
28
|
+
|
|
29
|
+
// 根据执行成
|
|
30
|
+
if (result.success) {
|
|
31
|
+
// 执
|
|
32
|
+
const hookResult = await onPostToolUse(
|
|
33
|
+
tool.name,
|
|
34
|
+
toolUseId,
|
|
35
|
+
execution.params as Record<string, unknown>,
|
|
36
|
+
result,
|
|
37
|
+
sessionId,
|
|
38
|
+
projectDir,
|
|
39
|
+
permissionMode
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
// 处理 Hook 结果:添加额外上下
|
|
43
|
+
if (hookResult.additionalContext) {
|
|
44
|
+
// 将 Hook 注入的上下文追加
|
|
45
|
+
const currentResult = execution.getResult();
|
|
46
|
+
if (currentResult) {
|
|
47
|
+
currentResult.llmContent += `\n\n[Hook Context]\n${hookResult.additionalContext}`;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 处理 Hook 结果:修改输
|
|
52
|
+
if (hookResult.modifiedOutput !== undefined) {
|
|
53
|
+
const currentResult = execution.getResult();
|
|
54
|
+
if (currentResult) {
|
|
55
|
+
currentResult.llmContent = String(hookResult.modifiedOutput);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
// 执
|
|
60
|
+
await onPostToolUseFailure(
|
|
61
|
+
tool.name,
|
|
62
|
+
toolUseId,
|
|
63
|
+
execution.params as Record<string, unknown>,
|
|
64
|
+
result.error?.message || 'Unknown error',
|
|
65
|
+
sessionId,
|
|
66
|
+
projectDir,
|
|
67
|
+
permissionMode
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { DiscoveryStage } from './DiscoveryStage.js';
|
|
6
|
+
export { CacheStage } from './CacheStage.js';
|
|
7
|
+
export { PermissionStage } from './PermissionStage.js';
|
|
8
|
+
export { HookStage } from './HookStage.js';
|
|
9
|
+
export { ConfirmationStage } from './ConfirmationStage.js';
|
|
10
|
+
export { ExecutionStage } from './ExecutionStage.js';
|
|
11
|
+
export { PostHookStage } from './PostHookStage.js';
|
|
12
|
+
export { FormattingStage } from './FormattingStage.js';
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
*
|
|
4
|
+
*
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { ExecutionPipeline, PermissionMode, type PipelineExecutionContext } from './index.js';
|
|
8
|
+
import { ToolRegistry, createToolRegistry, getBuiltinTools } from '../index.js';
|
|
9
|
+
import { PermissionChecker } from '../validation/PermissionChecker.js';
|
|
10
|
+
import { SensitiveFileDetector, SensitivityLevel } from '../validation/SensitiveFileDetector.js';
|
|
11
|
+
import * as fs from 'fs/promises';
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
import * as os from 'os';
|
|
14
|
+
|
|
15
|
+
// ========== 测试辅
|
|
16
|
+
|
|
17
|
+
let testDir: string;
|
|
18
|
+
|
|
19
|
+
async function setup() {
|
|
20
|
+
testDir = path.join(os.tmpdir(), `pipeline-test-${Date.now()}`);
|
|
21
|
+
await fs.mkdir(testDir, { recursive: true });
|
|
22
|
+
|
|
23
|
+
// 创建测试文
|
|
24
|
+
await fs.writeFile(path.join(testDir, 'test.txt'), 'Hello, World!');
|
|
25
|
+
await fs.writeFile(path.join(testDir, 'config.json'), '{"key": "value"}');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function cleanup() {
|
|
29
|
+
try {
|
|
30
|
+
await fs.rm(testDir, { recursive: true, force: true });
|
|
31
|
+
} catch {
|
|
32
|
+
// Ignore cleanup errors
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function createTestContext(mode: PermissionMode = PermissionMode.DEFAULT): PipelineExecutionContext {
|
|
37
|
+
return {
|
|
38
|
+
sessionId: 'test-session',
|
|
39
|
+
workspaceRoot: testDir,
|
|
40
|
+
permissionMode: mode,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ========== 测
|
|
45
|
+
|
|
46
|
+
async function testPermissionChecker() {
|
|
47
|
+
console.log('\n1. 测试 PermissionChecker');
|
|
48
|
+
|
|
49
|
+
const checker = new PermissionChecker({
|
|
50
|
+
allow: ['Bash(npm:*)'],
|
|
51
|
+
deny: ['Bash(rm -rf:*)'],
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// 测试 allow 规
|
|
55
|
+
const allowResult = checker.check({
|
|
56
|
+
toolName: 'Bash',
|
|
57
|
+
params: { command: 'npm test' },
|
|
58
|
+
});
|
|
59
|
+
console.log(` npm test: ${allowResult.result} (expected: allow)`);
|
|
60
|
+
|
|
61
|
+
// 测试 deny 规
|
|
62
|
+
const denyResult = checker.check({
|
|
63
|
+
toolName: 'Bash',
|
|
64
|
+
params: { command: 'rm -rf /' },
|
|
65
|
+
});
|
|
66
|
+
console.log(` rm -rf: ${denyResult.result} (expected: deny)`);
|
|
67
|
+
|
|
68
|
+
// 测试默
|
|
69
|
+
const askResult = checker.check({
|
|
70
|
+
toolName: 'Write',
|
|
71
|
+
params: { file_path: '/tmp/test.txt' },
|
|
72
|
+
});
|
|
73
|
+
console.log(` Write: ${askResult.result} (expected: ask)`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function testSensitiveFileDetector() {
|
|
77
|
+
console.log('\n2. 测试 SensitiveFileDetector');
|
|
78
|
+
|
|
79
|
+
// 高敏
|
|
80
|
+
const envResult = SensitiveFileDetector.check('.env');
|
|
81
|
+
console.log(` .env: ${envResult.level} (expected: high)`);
|
|
82
|
+
|
|
83
|
+
// 中敏
|
|
84
|
+
const logResult = SensitiveFileDetector.check('app.log');
|
|
85
|
+
console.log(` app.log: ${logResult.level} (expected: medium)`);
|
|
86
|
+
|
|
87
|
+
// 低敏
|
|
88
|
+
const configResult = SensitiveFileDetector.check('config.json');
|
|
89
|
+
console.log(` config.json: ${configResult.level} (expected: low)`);
|
|
90
|
+
|
|
91
|
+
// 非敏
|
|
92
|
+
const normalResult = SensitiveFileDetector.check('main.ts');
|
|
93
|
+
console.log(` main.ts: sensitive=${normalResult.sensitive} (expected: false)`);
|
|
94
|
+
|
|
95
|
+
// 危险路
|
|
96
|
+
const dangerous = SensitiveFileDetector.isDangerousPath('/etc/passwd');
|
|
97
|
+
console.log(` /etc/passwd: dangerous=${dangerous} (expected: true)`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function testPipelineReadOnly() {
|
|
101
|
+
console.log('\n3. 测试只读工具执行');
|
|
102
|
+
|
|
103
|
+
const registry = createToolRegistry();
|
|
104
|
+
const builtinTools = getBuiltinTools();
|
|
105
|
+
for (const tool of builtinTools) {
|
|
106
|
+
registry.register(tool);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const pipeline = new ExecutionPipeline(registry);
|
|
110
|
+
const context = createTestContext();
|
|
111
|
+
|
|
112
|
+
// 测试 Read 工
|
|
113
|
+
const testFile = path.join(testDir, 'test.txt');
|
|
114
|
+
const readResult = await pipeline.execute('Read', { file_path: testFile }, context);
|
|
115
|
+
console.log(` Read: success=${readResult.success}, content includes 'Hello'=${readResult.llmContent.includes('Hello')}`);
|
|
116
|
+
|
|
117
|
+
// 测试 Glob 工
|
|
118
|
+
const globResult = await pipeline.execute('Glob', { pattern: '*.txt', path: testDir }, context);
|
|
119
|
+
console.log(` Glob: success=${globResult.success}, found files=${globResult.llmContent.includes('test.txt')}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function testPermissionModes() {
|
|
123
|
+
console.log('\n4. 测试权限模式');
|
|
124
|
+
|
|
125
|
+
const registry = createToolRegistry();
|
|
126
|
+
const builtinTools = getBuiltinTools();
|
|
127
|
+
for (const tool of builtinTools) {
|
|
128
|
+
registry.register(tool);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const pipeline = new ExecutionPipeline(registry);
|
|
132
|
+
const testFile = path.join(testDir, 'write-test.txt');
|
|
133
|
+
|
|
134
|
+
// PLAN 模式:Write 应该被拒
|
|
135
|
+
const planContext = createTestContext(PermissionMode.PLAN);
|
|
136
|
+
const planResult = await pipeline.execute(
|
|
137
|
+
'Write',
|
|
138
|
+
{ file_path: testFile, contents: 'test' },
|
|
139
|
+
planContext
|
|
140
|
+
);
|
|
141
|
+
console.log(` PLAN + Write: success=${planResult.success} (expected: false)`);
|
|
142
|
+
|
|
143
|
+
// YOLO 模式:Write 应该自动批
|
|
144
|
+
const yoloContext = createTestContext(PermissionMode.YOLO);
|
|
145
|
+
const yoloResult = await pipeline.execute(
|
|
146
|
+
'Write',
|
|
147
|
+
{ file_path: testFile, contents: 'YOLO test' },
|
|
148
|
+
yoloContext
|
|
149
|
+
);
|
|
150
|
+
console.log(` YOLO + Write: success=${yoloResult.success} (expected: true)`);
|
|
151
|
+
|
|
152
|
+
// 验证文件内
|
|
153
|
+
const content = await fs.readFile(testFile, 'utf8');
|
|
154
|
+
console.log(` File content correct: ${content === 'YOLO test'}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function testPipelineStages() {
|
|
158
|
+
console.log('\n5. 测试管道阶段事件');
|
|
159
|
+
|
|
160
|
+
const registry = createToolRegistry();
|
|
161
|
+
const builtinTools = getBuiltinTools();
|
|
162
|
+
for (const tool of builtinTools) {
|
|
163
|
+
registry.register(tool);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const pipeline = new ExecutionPipeline(registry);
|
|
167
|
+
const context = createTestContext(PermissionMode.YOLO);
|
|
168
|
+
|
|
169
|
+
const stagesExecuted = pipeline.getStageNames();
|
|
170
|
+
|
|
171
|
+
const testFile = path.join(testDir, 'test.txt');
|
|
172
|
+
await pipeline.execute('Read', { file_path: testFile }, context);
|
|
173
|
+
|
|
174
|
+
console.log(` Stages: ${stagesExecuted.join(' → ')}`);
|
|
175
|
+
console.log(` All ${stagesExecuted.length} stages executed: ${stagesExecuted.length === 8}`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function testExecutionHistory() {
|
|
179
|
+
console.log('\n6. 测试执行历史');
|
|
180
|
+
|
|
181
|
+
const registry = createToolRegistry();
|
|
182
|
+
const builtinTools = getBuiltinTools();
|
|
183
|
+
for (const tool of builtinTools) {
|
|
184
|
+
registry.register(tool);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const pipeline = new ExecutionPipeline(registry);
|
|
188
|
+
const context = createTestContext(PermissionMode.YOLO);
|
|
189
|
+
|
|
190
|
+
// 执行几个操
|
|
191
|
+
const testFile = path.join(testDir, 'test.txt');
|
|
192
|
+
await pipeline.execute('Read', { file_path: testFile }, context);
|
|
193
|
+
await pipeline.execute('Glob', { pattern: '*', path: testDir }, context);
|
|
194
|
+
|
|
195
|
+
const history = pipeline.getHistory();
|
|
196
|
+
console.log(` History entries: ${history.length} (expected: 2)`);
|
|
197
|
+
console.log(` First entry tool: ${history[0]?.toolName} (expected: Read)`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function testSessionApprovals() {
|
|
201
|
+
console.log('\n7. 测试会话批准');
|
|
202
|
+
|
|
203
|
+
const registry = createToolRegistry();
|
|
204
|
+
const builtinTools = getBuiltinTools();
|
|
205
|
+
for (const tool of builtinTools) {
|
|
206
|
+
registry.register(tool);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const pipeline = new ExecutionPipeline(registry);
|
|
210
|
+
|
|
211
|
+
// 手动添加会话批
|
|
212
|
+
pipeline.addSessionApproval('Write(/tmp/approved.txt)');
|
|
213
|
+
|
|
214
|
+
console.log(` Has approval: ${pipeline.hasSessionApproval('Write(/tmp/approved.txt)')} (expected: true)`);
|
|
215
|
+
console.log(` No approval: ${pipeline.hasSessionApproval('Write(/tmp/other.txt)')} (expected: false)`);
|
|
216
|
+
|
|
217
|
+
// 清
|
|
218
|
+
pipeline.clearSessionApprovals();
|
|
219
|
+
console.log(` After clear: ${pipeline.hasSessionApproval('Write(/tmp/approved.txt)')} (expected: false)`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function testBashTool() {
|
|
223
|
+
console.log('\n8. 测试 Bash 工具');
|
|
224
|
+
|
|
225
|
+
const registry = createToolRegistry();
|
|
226
|
+
const builtinTools = getBuiltinTools();
|
|
227
|
+
for (const tool of builtinTools) {
|
|
228
|
+
registry.register(tool);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const pipeline = new ExecutionPipeline(registry);
|
|
232
|
+
const context = createTestContext(PermissionMode.YOLO);
|
|
233
|
+
|
|
234
|
+
// 简单命
|
|
235
|
+
const echoResult = await pipeline.execute(
|
|
236
|
+
'Bash',
|
|
237
|
+
{ command: 'echo "Pipeline Test"' },
|
|
238
|
+
context
|
|
239
|
+
);
|
|
240
|
+
console.log(` echo: success=${echoResult.success}, output includes 'Pipeline Test'=${echoResult.llmContent.includes('Pipeline Test')}`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ========== 主函
|
|
244
|
+
|
|
245
|
+
async function main() {
|
|
246
|
+
console.log('=== 执行管道测试 ===');
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
await setup();
|
|
250
|
+
|
|
251
|
+
await testPermissionChecker();
|
|
252
|
+
await testSensitiveFileDetector();
|
|
253
|
+
await testPipelineReadOnly();
|
|
254
|
+
await testPermissionModes();
|
|
255
|
+
await testPipelineStages();
|
|
256
|
+
await testExecutionHistory();
|
|
257
|
+
await testSessionApprovals();
|
|
258
|
+
await testBashTool();
|
|
259
|
+
|
|
260
|
+
console.log('\n=== 测试完成 ===');
|
|
261
|
+
} finally {
|
|
262
|
+
await cleanup();
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
main().catch(console.error);
|