@f2a/openclaw-f2a 0.2.20
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 +510 -0
- package/dist/agent-manager.d.ts +78 -0
- package/dist/agent-manager.d.ts.map +1 -0
- package/dist/agent-manager.js +206 -0
- package/dist/agent-manager.js.map +1 -0
- package/dist/announcement-queue.d.ts +152 -0
- package/dist/announcement-queue.d.ts.map +1 -0
- package/dist/announcement-queue.js +307 -0
- package/dist/announcement-queue.js.map +1 -0
- package/dist/capability-detector.d.ts +21 -0
- package/dist/capability-detector.d.ts.map +1 -0
- package/dist/capability-detector.js +178 -0
- package/dist/capability-detector.js.map +1 -0
- package/dist/claim-handlers.d.ts +75 -0
- package/dist/claim-handlers.d.ts.map +1 -0
- package/dist/claim-handlers.js +368 -0
- package/dist/claim-handlers.js.map +1 -0
- package/dist/connector.d.ts +174 -0
- package/dist/connector.d.ts.map +1 -0
- package/dist/connector.js +1284 -0
- package/dist/connector.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +45 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +28 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +44 -0
- package/dist/logger.js.map +1 -0
- package/dist/network-client.d.ts +73 -0
- package/dist/network-client.d.ts.map +1 -0
- package/dist/network-client.js +202 -0
- package/dist/network-client.js.map +1 -0
- package/dist/node-manager.d.ts +79 -0
- package/dist/node-manager.d.ts.map +1 -0
- package/dist/node-manager.js +374 -0
- package/dist/node-manager.js.map +1 -0
- package/dist/plugin.d.ts +22 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +148 -0
- package/dist/plugin.js.map +1 -0
- package/dist/reputation.d.ts +156 -0
- package/dist/reputation.d.ts.map +1 -0
- package/dist/reputation.js +432 -0
- package/dist/reputation.js.map +1 -0
- package/dist/task-guard.d.ts +159 -0
- package/dist/task-guard.d.ts.map +1 -0
- package/dist/task-guard.js +763 -0
- package/dist/task-guard.js.map +1 -0
- package/dist/task-queue.d.ts +130 -0
- package/dist/task-queue.d.ts.map +1 -0
- package/dist/task-queue.js +592 -0
- package/dist/task-queue.js.map +1 -0
- package/dist/tool-handlers.d.ts +158 -0
- package/dist/tool-handlers.d.ts.map +1 -0
- package/dist/tool-handlers.js +727 -0
- package/dist/tool-handlers.js.map +1 -0
- package/dist/types.d.ts +417 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +29 -0
- package/dist/types.js.map +1 -0
- package/dist/webhook-pusher.d.ts +71 -0
- package/dist/webhook-pusher.d.ts.map +1 -0
- package/dist/webhook-pusher.js +175 -0
- package/dist/webhook-pusher.js.map +1 -0
- package/dist/webhook-server.d.ts +70 -0
- package/dist/webhook-server.d.ts.map +1 -0
- package/dist/webhook-server.js +191 -0
- package/dist/webhook-server.js.map +1 -0
- package/openclaw.plugin.json +107 -0
- package/package.json +53 -0
|
@@ -0,0 +1,763 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* F2A Task Guard
|
|
4
|
+
* 轻量级任务安全检查和评审
|
|
5
|
+
*/
|
|
6
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
7
|
+
if (k2 === undefined) k2 = k;
|
|
8
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
9
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
10
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
11
|
+
}
|
|
12
|
+
Object.defineProperty(o, k2, desc);
|
|
13
|
+
}) : (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
o[k2] = m[k];
|
|
16
|
+
}));
|
|
17
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
18
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
19
|
+
}) : function(o, v) {
|
|
20
|
+
o["default"] = v;
|
|
21
|
+
});
|
|
22
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
23
|
+
var ownKeys = function(o) {
|
|
24
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
25
|
+
var ar = [];
|
|
26
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
27
|
+
return ar;
|
|
28
|
+
};
|
|
29
|
+
return ownKeys(o);
|
|
30
|
+
};
|
|
31
|
+
return function (mod) {
|
|
32
|
+
if (mod && mod.__esModule) return mod;
|
|
33
|
+
var result = {};
|
|
34
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
35
|
+
__setModuleDefault(result, mod);
|
|
36
|
+
return result;
|
|
37
|
+
};
|
|
38
|
+
})();
|
|
39
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
|
+
exports.taskGuard = exports.TaskGuard = exports.DEFAULT_TASK_GUARD_CONFIG = void 0;
|
|
41
|
+
const logger_js_1 = require("./logger.js");
|
|
42
|
+
const fs = __importStar(require("fs"));
|
|
43
|
+
const path = __importStar(require("path"));
|
|
44
|
+
// 默认配置
|
|
45
|
+
exports.DEFAULT_TASK_GUARD_CONFIG = {
|
|
46
|
+
enabled: true,
|
|
47
|
+
requireConfirmationForDangerous: true,
|
|
48
|
+
maxTasksPerMinute: 10,
|
|
49
|
+
blockedKeywords: [
|
|
50
|
+
'rm -rf /',
|
|
51
|
+
'rm -rf /*',
|
|
52
|
+
'format',
|
|
53
|
+
'delete all',
|
|
54
|
+
'destroy',
|
|
55
|
+
'wipe'
|
|
56
|
+
],
|
|
57
|
+
dangerousPatterns: [
|
|
58
|
+
/rm\s+-rf\s+\/\s*$/i,
|
|
59
|
+
/format\s+/i,
|
|
60
|
+
/delete\s+all/i,
|
|
61
|
+
/drop\s+database/i,
|
|
62
|
+
/shutdown\s+-h/i
|
|
63
|
+
],
|
|
64
|
+
minReputationForDangerous: 70,
|
|
65
|
+
persistDir: undefined,
|
|
66
|
+
persistIntervalMs: 30000 // 30秒
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* 路径规范化 - 移除 .. 和多余的斜杠
|
|
70
|
+
* 用于检测路径遍历绕过
|
|
71
|
+
*/
|
|
72
|
+
function normalizePath(path) {
|
|
73
|
+
// 解码 URL 编码
|
|
74
|
+
let normalized = path;
|
|
75
|
+
try {
|
|
76
|
+
normalized = decodeURIComponent(normalized);
|
|
77
|
+
}
|
|
78
|
+
catch { /* ignore */ }
|
|
79
|
+
// 替换多个斜杠为单个
|
|
80
|
+
normalized = normalized.replace(/\/+/g, '/');
|
|
81
|
+
// 解析 .. 和 .
|
|
82
|
+
const parts = normalized.split('/');
|
|
83
|
+
const result = [];
|
|
84
|
+
for (const part of parts) {
|
|
85
|
+
if (part === '..') {
|
|
86
|
+
result.pop();
|
|
87
|
+
}
|
|
88
|
+
else if (part !== '.' && part !== '') {
|
|
89
|
+
result.push(part);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return '/' + result.join('/');
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* 检测变量替换绕过
|
|
96
|
+
* P1 修复:增加更多危险模式检测
|
|
97
|
+
*/
|
|
98
|
+
function detectVariableSubstitution(text) {
|
|
99
|
+
const detected = [];
|
|
100
|
+
// 环境变量模式: $VAR, ${VAR}, %VAR%
|
|
101
|
+
const envPatterns = [
|
|
102
|
+
/\$([A-Za-z_][A-Za-z0-9_]*)/g, // $VAR
|
|
103
|
+
/\$\{([^}]+)\}/g, // ${VAR}
|
|
104
|
+
/%([A-Za-z_][A-Za-z0-9_]*)%/g, // %VAR%
|
|
105
|
+
];
|
|
106
|
+
for (const pattern of envPatterns) {
|
|
107
|
+
let match;
|
|
108
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
109
|
+
detected.push(`变量替换: ${match[0]}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// P1 修复:检测算术表达式 $((expression))
|
|
113
|
+
const arithmeticPattern = /\$\(\(([^)]+)\)\)/g;
|
|
114
|
+
let arithmeticMatch;
|
|
115
|
+
while ((arithmeticMatch = arithmeticPattern.exec(text)) !== null) {
|
|
116
|
+
detected.push(`算术表达式: ${arithmeticMatch[0]}`);
|
|
117
|
+
}
|
|
118
|
+
// P1 修复:检测数组引用 ${array[@]}, ${array[*]}
|
|
119
|
+
const arrayPattern = /\$\{[A-Za-z_][A-Za-z0-9_]*\[@?\*?\]\}/g;
|
|
120
|
+
let arrayMatch;
|
|
121
|
+
while ((arrayMatch = arrayPattern.exec(text)) !== null) {
|
|
122
|
+
detected.push(`数组引用: ${arrayMatch[0]}`);
|
|
123
|
+
}
|
|
124
|
+
// P1 修复:检测特殊变量: $?, $$, $!, $0, $1-$9, $#, $@, $*, $-
|
|
125
|
+
// 注意:使用 text.match(pattern) 而非 pattern.test(text) 避免 lastIndex bug
|
|
126
|
+
const specialVars = [
|
|
127
|
+
{ pattern: /\$\?/g, name: '退出状态($?)' },
|
|
128
|
+
{ pattern: /\$\$/g, name: '进程ID($$)' },
|
|
129
|
+
{ pattern: /\$!/g, name: '后台进程ID($!)' },
|
|
130
|
+
{ pattern: /\$[0-9]/g, name: '位置参数($0-$9)' },
|
|
131
|
+
{ pattern: /\$#/g, name: '参数个数($#)' },
|
|
132
|
+
{ pattern: /\$@/g, name: '所有参数($@)' },
|
|
133
|
+
{ pattern: /\$\*/g, name: '所有参数($*)' },
|
|
134
|
+
{ pattern: /\$-/g, name: 'Shell选项($-)' },
|
|
135
|
+
];
|
|
136
|
+
for (const { pattern, name } of specialVars) {
|
|
137
|
+
// P0 修复:使用 text.match(pattern) 避免 lastIndex bug
|
|
138
|
+
// 全局正则的 test() 会更新 lastIndex,导致后续调用结果不一致
|
|
139
|
+
if (text.match(pattern)) {
|
|
140
|
+
detected.push(`特殊变量: ${name}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// P1 修复:检测命令替换的变体
|
|
144
|
+
// ${command} - bash 命令替换
|
|
145
|
+
const bashCommandPattern = /\$\(([^)]+)\)/g;
|
|
146
|
+
let bashMatch;
|
|
147
|
+
while ((bashMatch = bashCommandPattern.exec(text)) !== null) {
|
|
148
|
+
// 排除已检测的算术表达式
|
|
149
|
+
if (!bashMatch[0].startsWith('$((')) {
|
|
150
|
+
detected.push(`命令替换: ${bashMatch[0]}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return detected;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* 检测编码绕过
|
|
157
|
+
*/
|
|
158
|
+
function detectEncodingBypass(text) {
|
|
159
|
+
const detected = [];
|
|
160
|
+
// 八进制编码: \177, \027
|
|
161
|
+
if (/\\[0-7]{1,3}/.test(text)) {
|
|
162
|
+
detected.push('八进制编码');
|
|
163
|
+
}
|
|
164
|
+
// 十六进制编码: \x7f, \x1b
|
|
165
|
+
if (/\\x[0-9a-fA-F]{2}/.test(text)) {
|
|
166
|
+
detected.push('十六进制编码');
|
|
167
|
+
}
|
|
168
|
+
// Unicode 编码: \u007f, \u007F, \u{7f} (ES6)
|
|
169
|
+
if (/\\u[0-9a-fA-F]{4}/.test(text) || /\\u\{[0-9a-fA-F]+\}/.test(text)) {
|
|
170
|
+
detected.push('Unicode编码');
|
|
171
|
+
}
|
|
172
|
+
// HTML 实体编码:  ,  , <, >, &
|
|
173
|
+
// 十六进制格式: &#xHH;
|
|
174
|
+
if (/&#x[0-9a-fA-F]+;?/i.test(text)) {
|
|
175
|
+
detected.push('HTML实体编码(十六进制)');
|
|
176
|
+
}
|
|
177
|
+
// 十进制格式: &#DDD;
|
|
178
|
+
if (/&#\d+;?/.test(text)) {
|
|
179
|
+
detected.push('HTML实体编码(十进制)');
|
|
180
|
+
}
|
|
181
|
+
// 命名实体: < > & " '
|
|
182
|
+
if (/&(lt|gt|amp|quot|apos|#x?\d+);/i.test(text)) {
|
|
183
|
+
detected.push('HTML实体编码(命名)');
|
|
184
|
+
}
|
|
185
|
+
// URL 编码: %20, %2f
|
|
186
|
+
if (/%[0-9a-fA-F]{2}/.test(text)) {
|
|
187
|
+
detected.push('URL编码');
|
|
188
|
+
}
|
|
189
|
+
return detected;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* 检测命令注入绕过
|
|
193
|
+
*/
|
|
194
|
+
function detectCommandInjectionBypass(text) {
|
|
195
|
+
const detected = [];
|
|
196
|
+
const lowerText = text.toLowerCase();
|
|
197
|
+
// 反引号命令替换
|
|
198
|
+
if (/`[^`]+`/.test(text)) {
|
|
199
|
+
detected.push('反引号命令替换');
|
|
200
|
+
}
|
|
201
|
+
// $() 命令替换
|
|
202
|
+
if (/\$\([^)]+\)/.test(text)) {
|
|
203
|
+
detected.push('$()命令替换');
|
|
204
|
+
}
|
|
205
|
+
// 分号命令链接
|
|
206
|
+
if (/;\s*(rm|dd|mkfs|shutdown|reboot|halt|init)\b/i.test(text)) {
|
|
207
|
+
detected.push('分号命令链接');
|
|
208
|
+
}
|
|
209
|
+
// 管道命令注入
|
|
210
|
+
if (/\|\s*(rm|dd|mkfs|shutdown|reboot|halt)\b/i.test(text)) {
|
|
211
|
+
detected.push('管道命令注入');
|
|
212
|
+
}
|
|
213
|
+
// &&/|| 命令链接
|
|
214
|
+
if (/(&&|\|\|)\s*(rm|dd|mkfs|shutdown|reboot|halt)\b/i.test(text)) {
|
|
215
|
+
detected.push('逻辑运算符命令链接');
|
|
216
|
+
}
|
|
217
|
+
return detected;
|
|
218
|
+
}
|
|
219
|
+
class TaskGuard {
|
|
220
|
+
config;
|
|
221
|
+
rules;
|
|
222
|
+
recentTasks = new Map();
|
|
223
|
+
/** 清理阈值:当条目数超过此值时触发清理 */
|
|
224
|
+
cleanupThreshold = 100;
|
|
225
|
+
/** 上次清理时间戳 */
|
|
226
|
+
lastCleanupTime = 0;
|
|
227
|
+
/** 定时清理间隔(毫秒) */
|
|
228
|
+
cleanupIntervalMs = 60000; // 1分钟
|
|
229
|
+
/** 持久化文件路径 */
|
|
230
|
+
persistFilePath = null;
|
|
231
|
+
/** 持久化定时器 */
|
|
232
|
+
persistTimer = null;
|
|
233
|
+
/** 是否有未保存的更改 */
|
|
234
|
+
hasUnsavedChanges = false;
|
|
235
|
+
constructor(config = {}) {
|
|
236
|
+
this.config = { ...exports.DEFAULT_TASK_GUARD_CONFIG, ...config };
|
|
237
|
+
this.rules = this.createDefaultRules();
|
|
238
|
+
// 初始化持久化
|
|
239
|
+
if (this.config.persistDir) {
|
|
240
|
+
this.initPersistence(this.config.persistDir, this.config.persistIntervalMs);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* 初始化持久化
|
|
245
|
+
*/
|
|
246
|
+
initPersistence(persistDir, persistIntervalMs) {
|
|
247
|
+
try {
|
|
248
|
+
// 确保目录存在
|
|
249
|
+
if (!fs.existsSync(persistDir)) {
|
|
250
|
+
fs.mkdirSync(persistDir, { recursive: true });
|
|
251
|
+
}
|
|
252
|
+
this.persistFilePath = path.join(persistDir, 'task-guard-state.json');
|
|
253
|
+
// 加载已保存的状态
|
|
254
|
+
this.loadPersistedState();
|
|
255
|
+
// 设置定期保存
|
|
256
|
+
const interval = persistIntervalMs ?? exports.DEFAULT_TASK_GUARD_CONFIG.persistIntervalMs ?? 30000;
|
|
257
|
+
this.persistTimer = setInterval(() => {
|
|
258
|
+
this.saveStateIfNeeded();
|
|
259
|
+
}, interval);
|
|
260
|
+
// 防止定时器阻止进程退出
|
|
261
|
+
if (this.persistTimer.unref) {
|
|
262
|
+
this.persistTimer.unref();
|
|
263
|
+
}
|
|
264
|
+
// 注册进程退出处理,确保状态持久化(仅在真正需要持久化时注册)
|
|
265
|
+
registerShutdownHandlers();
|
|
266
|
+
logger_js_1.taskGuardLogger.info('persistence-initialized: persistDir=%s, intervalMs=%d', persistDir, interval);
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
logger_js_1.taskGuardLogger.error('persistence-init-failed: error=%s', error);
|
|
270
|
+
this.persistFilePath = null;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* 加载已保存的状态
|
|
275
|
+
*/
|
|
276
|
+
loadPersistedState() {
|
|
277
|
+
if (!this.persistFilePath || !fs.existsSync(this.persistFilePath)) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
try {
|
|
281
|
+
const data = fs.readFileSync(this.persistFilePath, 'utf-8');
|
|
282
|
+
const state = JSON.parse(data);
|
|
283
|
+
if (state.recentTasks && typeof state.recentTasks === 'object') {
|
|
284
|
+
const now = Date.now();
|
|
285
|
+
const windowMs = 60000; // 1分钟窗口
|
|
286
|
+
// 过滤掉过期的时间戳,只保留有效的
|
|
287
|
+
let loadedCount = 0;
|
|
288
|
+
for (const [peerId, timestamps] of Object.entries(state.recentTasks)) {
|
|
289
|
+
if (Array.isArray(timestamps)) {
|
|
290
|
+
const validTimestamps = timestamps.filter(t => typeof t === 'number' && now - t < windowMs);
|
|
291
|
+
if (validTimestamps.length > 0) {
|
|
292
|
+
this.recentTasks.set(peerId, validTimestamps);
|
|
293
|
+
loadedCount += validTimestamps.length;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
logger_js_1.taskGuardLogger.info('persistence-loaded: entries=%d, timestamps=%d, savedAt=%s', this.recentTasks.size, loadedCount, new Date(state.savedAt).toISOString());
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
catch (error) {
|
|
301
|
+
logger_js_1.taskGuardLogger.warn('persistence-load-failed: error=%s', error);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* 保存状态到文件
|
|
306
|
+
*/
|
|
307
|
+
saveState() {
|
|
308
|
+
if (!this.persistFilePath) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
try {
|
|
312
|
+
const state = {
|
|
313
|
+
recentTasks: Object.fromEntries(this.recentTasks),
|
|
314
|
+
savedAt: Date.now()
|
|
315
|
+
};
|
|
316
|
+
// 写入临时文件,然后原子性重命名
|
|
317
|
+
const tempPath = this.persistFilePath + '.tmp';
|
|
318
|
+
fs.writeFileSync(tempPath, JSON.stringify(state), 'utf-8');
|
|
319
|
+
fs.renameSync(tempPath, this.persistFilePath);
|
|
320
|
+
this.hasUnsavedChanges = false;
|
|
321
|
+
logger_js_1.taskGuardLogger.debug('persistence-saved: entries=%d', this.recentTasks.size);
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
logger_js_1.taskGuardLogger.error('persistence-save-failed: error=%s', error);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* 仅在有未保存更改时保存
|
|
329
|
+
*/
|
|
330
|
+
saveStateIfNeeded() {
|
|
331
|
+
if (this.hasUnsavedChanges) {
|
|
332
|
+
this.saveState();
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* 手动保存当前状态
|
|
337
|
+
*/
|
|
338
|
+
forceSave() {
|
|
339
|
+
this.saveState();
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* 关闭持久化(停止定时器并保存最后状态)
|
|
343
|
+
*/
|
|
344
|
+
shutdown() {
|
|
345
|
+
if (this.persistTimer) {
|
|
346
|
+
clearInterval(this.persistTimer);
|
|
347
|
+
this.persistTimer = null;
|
|
348
|
+
}
|
|
349
|
+
// 保存最终状态
|
|
350
|
+
if (this.hasUnsavedChanges) {
|
|
351
|
+
this.saveState();
|
|
352
|
+
}
|
|
353
|
+
logger_js_1.taskGuardLogger.info('task-guard-shutdown: persisted=%s', !!this.persistFilePath);
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* 检查任务
|
|
357
|
+
*/
|
|
358
|
+
check(task, context = {}) {
|
|
359
|
+
const fullContext = {
|
|
360
|
+
requesterReputation: context.requesterReputation,
|
|
361
|
+
isWhitelisted: context.isWhitelisted ?? false,
|
|
362
|
+
isBlacklisted: context.isBlacklisted ?? false,
|
|
363
|
+
recentTaskCount: this.getRecentTaskCount(task.from),
|
|
364
|
+
config: this.config
|
|
365
|
+
};
|
|
366
|
+
const taskId = 'taskId' in task ? task.taskId : task.announcementId;
|
|
367
|
+
logger_js_1.taskGuardLogger.debug('check: taskId=%s, from=%s, rules=%d', taskId, task.from, this.rules.filter(r => r.enabled).length);
|
|
368
|
+
const results = [];
|
|
369
|
+
// 运行所有规则
|
|
370
|
+
for (const rule of this.rules) {
|
|
371
|
+
if (!rule.enabled)
|
|
372
|
+
continue;
|
|
373
|
+
try {
|
|
374
|
+
const result = rule.check(task, fullContext);
|
|
375
|
+
results.push(result);
|
|
376
|
+
// 记录规则执行结果
|
|
377
|
+
if (!result.passed) {
|
|
378
|
+
if (result.severity === 'block') {
|
|
379
|
+
logger_js_1.taskGuardLogger.warn('rule-blocked: taskId=%s, ruleId=%s, message=%s', taskId, rule.id, result.message);
|
|
380
|
+
}
|
|
381
|
+
else if (result.severity === 'warn') {
|
|
382
|
+
logger_js_1.taskGuardLogger.info('rule-warning: taskId=%s, ruleId=%s, message=%s', taskId, rule.id, result.message);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
catch (error) {
|
|
387
|
+
logger_js_1.taskGuardLogger.error('rule-error: ruleId=%s, taskId=%s, error=%s', rule.id, taskId, error);
|
|
388
|
+
results.push({
|
|
389
|
+
passed: false,
|
|
390
|
+
severity: 'warn',
|
|
391
|
+
ruleId: rule.id,
|
|
392
|
+
message: `规则执行错误: ${rule.name}`
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
// 记录任务
|
|
397
|
+
this.recordTask(task.from);
|
|
398
|
+
const blocks = results.filter(r => r.severity === 'block' && !r.passed);
|
|
399
|
+
const warnings = results.filter(r => r.severity === 'warn' && !r.passed);
|
|
400
|
+
const requiresConfirmation = results.some(r => r.severity === 'warn' &&
|
|
401
|
+
!r.passed &&
|
|
402
|
+
this.config.requireConfirmationForDangerous);
|
|
403
|
+
const passed = blocks.length === 0;
|
|
404
|
+
logger_js_1.taskGuardLogger.debug('check-result: taskId=%s, passed=%s, blocks=%d, warnings=%d, requiresConfirmation=%s', taskId, passed, blocks.length, warnings.length, requiresConfirmation);
|
|
405
|
+
return {
|
|
406
|
+
taskId,
|
|
407
|
+
passed,
|
|
408
|
+
results,
|
|
409
|
+
warnings,
|
|
410
|
+
blocks,
|
|
411
|
+
requiresConfirmation,
|
|
412
|
+
timestamp: Date.now()
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* 快速检查(只返回是否通过)
|
|
417
|
+
*/
|
|
418
|
+
quickCheck(task, context) {
|
|
419
|
+
const report = this.check(task, context);
|
|
420
|
+
const taskId = 'taskId' in task ? task.taskId : task.announcementId;
|
|
421
|
+
logger_js_1.taskGuardLogger.debug('quickCheck: taskId=%s, passed=%s', taskId, report.passed);
|
|
422
|
+
return report.passed;
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* 添加自定义规则
|
|
426
|
+
*/
|
|
427
|
+
addRule(rule) {
|
|
428
|
+
this.rules.push(rule);
|
|
429
|
+
logger_js_1.taskGuardLogger.info('addRule: ruleId=%s, name=%s, severity=%s, enabled=%s', rule.id, rule.name, rule.severity, rule.enabled);
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* 启用/禁用规则
|
|
433
|
+
*/
|
|
434
|
+
setRuleEnabled(ruleId, enabled) {
|
|
435
|
+
const rule = this.rules.find(r => r.id === ruleId);
|
|
436
|
+
if (rule) {
|
|
437
|
+
rule.enabled = enabled;
|
|
438
|
+
logger_js_1.taskGuardLogger.info('setRuleEnabled: ruleId=%s, enabled=%s', ruleId, enabled);
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
logger_js_1.taskGuardLogger.warn('setRuleEnabled: rule not found, ruleId=%s', ruleId);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* 更新配置
|
|
446
|
+
*/
|
|
447
|
+
updateConfig(config) {
|
|
448
|
+
this.config = { ...this.config, ...config };
|
|
449
|
+
}
|
|
450
|
+
// ========== 私有方法 ==========
|
|
451
|
+
createDefaultRules() {
|
|
452
|
+
return [
|
|
453
|
+
// 规则 1: 黑名单检查
|
|
454
|
+
{
|
|
455
|
+
id: 'blacklist',
|
|
456
|
+
name: '黑名单检查',
|
|
457
|
+
description: '检查请求者是否在黑名单中',
|
|
458
|
+
enabled: true,
|
|
459
|
+
severity: 'block',
|
|
460
|
+
check: (task, context) => ({
|
|
461
|
+
passed: !context.isBlacklisted,
|
|
462
|
+
severity: 'block',
|
|
463
|
+
ruleId: 'blacklist',
|
|
464
|
+
message: context.isBlacklisted
|
|
465
|
+
? '请求者在黑名单中,任务被拒绝'
|
|
466
|
+
: '通过黑名单检查'
|
|
467
|
+
})
|
|
468
|
+
},
|
|
469
|
+
// 规则 2: 频率限制
|
|
470
|
+
{
|
|
471
|
+
id: 'rate-limit',
|
|
472
|
+
name: '频率限制',
|
|
473
|
+
description: '检查请求频率是否过高',
|
|
474
|
+
enabled: true,
|
|
475
|
+
severity: 'block',
|
|
476
|
+
check: (task, context) => {
|
|
477
|
+
const exceeded = context.recentTaskCount > context.config.maxTasksPerMinute;
|
|
478
|
+
return {
|
|
479
|
+
passed: !exceeded,
|
|
480
|
+
severity: 'block',
|
|
481
|
+
ruleId: 'rate-limit',
|
|
482
|
+
message: exceeded
|
|
483
|
+
? `请求频率过高: ${context.recentTaskCount}/${context.config.maxTasksPerMinute} 每分钟`
|
|
484
|
+
: '频率检查通过',
|
|
485
|
+
details: { current: context.recentTaskCount, limit: context.config.maxTasksPerMinute }
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
},
|
|
489
|
+
// 规则 3: 危险关键词检查
|
|
490
|
+
{
|
|
491
|
+
id: 'dangerous-keywords',
|
|
492
|
+
name: '危险关键词检查',
|
|
493
|
+
description: '检查任务描述中是否包含危险关键词',
|
|
494
|
+
enabled: true,
|
|
495
|
+
severity: 'block',
|
|
496
|
+
check: (task, context) => {
|
|
497
|
+
const description = task.description.toLowerCase();
|
|
498
|
+
const found = context.config.blockedKeywords.filter(kw => description.includes(kw.toLowerCase()));
|
|
499
|
+
return {
|
|
500
|
+
passed: found.length === 0,
|
|
501
|
+
severity: 'block',
|
|
502
|
+
ruleId: 'dangerous-keywords',
|
|
503
|
+
message: found.length > 0
|
|
504
|
+
? `发现危险关键词: ${found.join(', ')}`
|
|
505
|
+
: '未检测到危险关键词',
|
|
506
|
+
details: { found }
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
},
|
|
510
|
+
// 规则 4: 危险模式检查(正则)
|
|
511
|
+
{
|
|
512
|
+
id: 'dangerous-patterns',
|
|
513
|
+
name: '危险模式检查',
|
|
514
|
+
description: '使用正则表达式检测危险命令模式',
|
|
515
|
+
enabled: true,
|
|
516
|
+
severity: 'block',
|
|
517
|
+
check: (task, context) => {
|
|
518
|
+
const description = task.description;
|
|
519
|
+
const found = [];
|
|
520
|
+
for (const pattern of context.config.dangerousPatterns) {
|
|
521
|
+
if (pattern.test(description)) {
|
|
522
|
+
found.push(pattern.source);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
// P1 修复:集成变量替换和编码绕过检测
|
|
526
|
+
const variableSubstitutions = detectVariableSubstitution(description);
|
|
527
|
+
if (variableSubstitutions.length > 0) {
|
|
528
|
+
found.push(...variableSubstitutions);
|
|
529
|
+
}
|
|
530
|
+
const encodingBypasses = detectEncodingBypass(description);
|
|
531
|
+
if (encodingBypasses.length > 0) {
|
|
532
|
+
found.push(...encodingBypasses);
|
|
533
|
+
}
|
|
534
|
+
// P0 修复:集成命令注入绕过检测(包括反引号命令替换)
|
|
535
|
+
const commandInjectionBypasses = detectCommandInjectionBypass(description);
|
|
536
|
+
if (commandInjectionBypasses.length > 0) {
|
|
537
|
+
found.push(...commandInjectionBypasses);
|
|
538
|
+
}
|
|
539
|
+
return {
|
|
540
|
+
passed: found.length === 0,
|
|
541
|
+
severity: 'block',
|
|
542
|
+
ruleId: 'dangerous-patterns',
|
|
543
|
+
message: found.length > 0
|
|
544
|
+
? `发现危险命令模式`
|
|
545
|
+
: '未检测到危险模式',
|
|
546
|
+
details: { patternsFound: found.length, details: found }
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
},
|
|
550
|
+
// 规则 5: 信誉检查
|
|
551
|
+
{
|
|
552
|
+
id: 'reputation',
|
|
553
|
+
name: '信誉检查',
|
|
554
|
+
description: '检查请求者信誉是否足够',
|
|
555
|
+
enabled: true,
|
|
556
|
+
severity: 'warn',
|
|
557
|
+
check: (task, context) => {
|
|
558
|
+
if (!context.requesterReputation) {
|
|
559
|
+
// 无信誉记录时返回 warn,而非直接通过
|
|
560
|
+
// 新 peer 首次任务需要谨慎处理
|
|
561
|
+
return {
|
|
562
|
+
passed: true,
|
|
563
|
+
severity: 'warn',
|
|
564
|
+
ruleId: 'reputation',
|
|
565
|
+
message: '无信誉记录,首次任务建议谨慎处理'
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
const rep = context.requesterReputation.score;
|
|
569
|
+
const isDangerous = this.isDangerousTask(task);
|
|
570
|
+
if (isDangerous && rep < context.config.minReputationForDangerous) {
|
|
571
|
+
return {
|
|
572
|
+
passed: false,
|
|
573
|
+
severity: 'warn',
|
|
574
|
+
ruleId: 'reputation',
|
|
575
|
+
message: `信誉不足执行危险任务: ${rep} < ${context.config.minReputationForDangerous}`,
|
|
576
|
+
details: { reputation: rep, required: context.config.minReputationForDangerous }
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
return {
|
|
580
|
+
passed: true,
|
|
581
|
+
severity: 'info',
|
|
582
|
+
ruleId: 'reputation',
|
|
583
|
+
message: `信誉检查通过: ${rep}`,
|
|
584
|
+
details: { reputation: rep }
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
},
|
|
588
|
+
// 规则 6: 文件操作检查
|
|
589
|
+
{
|
|
590
|
+
id: 'file-operation',
|
|
591
|
+
name: '文件操作检查',
|
|
592
|
+
description: '检查文件操作是否在允许范围内',
|
|
593
|
+
enabled: true,
|
|
594
|
+
severity: 'warn',
|
|
595
|
+
check: (task, context) => {
|
|
596
|
+
const description = task.description.toLowerCase();
|
|
597
|
+
const isFileOp = /\b(read|write|edit|delete|remove)\b/.test(description);
|
|
598
|
+
const hasPath = /[\/~]\w+/.test(description);
|
|
599
|
+
if (isFileOp && hasPath) {
|
|
600
|
+
// 检查是否是系统路径(包括 macOS 路径)
|
|
601
|
+
const systemPaths = [
|
|
602
|
+
// Linux/Unix 系统路径
|
|
603
|
+
'/etc/', '/sys/', '/proc/', '/dev/', '/root/', '/boot/',
|
|
604
|
+
// macOS 系统路径
|
|
605
|
+
'/System/', '/Library/', '/Applications/', '/usr/', '/bin/', '/sbin/',
|
|
606
|
+
// Windows 系统路径
|
|
607
|
+
'c:\\windows', 'c:\\program files', 'c:\\program files (x86)',
|
|
608
|
+
'c:\\users\\public', 'c:\\users\\default'
|
|
609
|
+
];
|
|
610
|
+
const hasSystemPath = systemPaths.some(p => description.includes(p.toLowerCase()));
|
|
611
|
+
if (hasSystemPath) {
|
|
612
|
+
return {
|
|
613
|
+
passed: false,
|
|
614
|
+
severity: 'warn',
|
|
615
|
+
ruleId: 'file-operation',
|
|
616
|
+
message: '检测到系统路径文件操作,需要确认',
|
|
617
|
+
details: { systemPath: true }
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
return {
|
|
622
|
+
passed: true,
|
|
623
|
+
severity: 'info',
|
|
624
|
+
ruleId: 'file-operation',
|
|
625
|
+
message: '文件操作检查通过'
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
},
|
|
629
|
+
// 规则 7: 网络操作检查
|
|
630
|
+
{
|
|
631
|
+
id: 'network-operation',
|
|
632
|
+
name: '网络操作检查',
|
|
633
|
+
description: '检查网络操作是否可疑',
|
|
634
|
+
enabled: true,
|
|
635
|
+
severity: 'warn',
|
|
636
|
+
check: (task, context) => {
|
|
637
|
+
const description = task.description.toLowerCase();
|
|
638
|
+
const isNetworkOp = /\b(fetch|download|curl|wget|http|api)\b/.test(description);
|
|
639
|
+
if (isNetworkOp) {
|
|
640
|
+
// 更精确的可疑文件扩展名检测(移除 python/script 等宽泛词)
|
|
641
|
+
const suspiciousExtensions = ['exe', 'dll', 'app', 'deb', 'rpm', 'dmg', 'msi', 'bat', 'ps1'];
|
|
642
|
+
const hasSuspicious = suspiciousExtensions.some(ext => {
|
|
643
|
+
// 匹配 .ext 后跟空格、引号、斜杠、大于号,或字符串结尾
|
|
644
|
+
const pattern = new RegExp(`\\.${ext}([\\s"'\\/>]|$)`, 'i');
|
|
645
|
+
return pattern.test(description);
|
|
646
|
+
});
|
|
647
|
+
if (hasSuspicious) {
|
|
648
|
+
return {
|
|
649
|
+
passed: false,
|
|
650
|
+
severity: 'warn',
|
|
651
|
+
ruleId: 'network-operation',
|
|
652
|
+
message: '检测到可疑的网络下载操作(可执行文件)',
|
|
653
|
+
details: { suspicious: true }
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
return {
|
|
658
|
+
passed: true,
|
|
659
|
+
severity: 'info',
|
|
660
|
+
ruleId: 'network-operation',
|
|
661
|
+
message: '网络操作检查通过'
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
];
|
|
666
|
+
}
|
|
667
|
+
isDangerousTask(task) {
|
|
668
|
+
const description = task.description.toLowerCase();
|
|
669
|
+
// 检查关键词
|
|
670
|
+
if (this.config.blockedKeywords.some(kw => description.includes(kw.toLowerCase()))) {
|
|
671
|
+
return true;
|
|
672
|
+
}
|
|
673
|
+
// 检查模式
|
|
674
|
+
if (this.config.dangerousPatterns.some(p => p.test(description))) {
|
|
675
|
+
return true;
|
|
676
|
+
}
|
|
677
|
+
return false;
|
|
678
|
+
}
|
|
679
|
+
getRecentTaskCount(peerId) {
|
|
680
|
+
const now = Date.now();
|
|
681
|
+
const windowMs = this.config.maxTasksPerMinute ? 60000 : 60000;
|
|
682
|
+
const windowStart = now - windowMs;
|
|
683
|
+
const timestamps = this.recentTasks.get(peerId) || [];
|
|
684
|
+
const recent = timestamps.filter(t => t > windowStart);
|
|
685
|
+
// 更新存储
|
|
686
|
+
this.recentTasks.set(peerId, recent);
|
|
687
|
+
return recent.length;
|
|
688
|
+
}
|
|
689
|
+
recordTask(peerId) {
|
|
690
|
+
const timestamps = this.recentTasks.get(peerId) || [];
|
|
691
|
+
timestamps.push(Date.now());
|
|
692
|
+
this.recentTasks.set(peerId, timestamps);
|
|
693
|
+
// 标记有未保存的更改
|
|
694
|
+
this.hasUnsavedChanges = true;
|
|
695
|
+
// 优化:仅在超过阈值或定时触发时清理,而非每次都清理
|
|
696
|
+
this.maybeCleanup();
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* 条件触发清理:当条目数超过阈值或距上次清理超过间隔时执行
|
|
700
|
+
*/
|
|
701
|
+
maybeCleanup() {
|
|
702
|
+
const now = Date.now();
|
|
703
|
+
const shouldCleanup = this.recentTasks.size > this.cleanupThreshold ||
|
|
704
|
+
(now - this.lastCleanupTime) > this.cleanupIntervalMs;
|
|
705
|
+
if (shouldCleanup) {
|
|
706
|
+
this.cleanupRecentTasks();
|
|
707
|
+
this.lastCleanupTime = now;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* 清理过期的 recentTasks 条目,防止内存泄漏
|
|
712
|
+
*/
|
|
713
|
+
cleanupRecentTasks() {
|
|
714
|
+
const now = Date.now();
|
|
715
|
+
const windowMs = 60000; // 1分钟窗口
|
|
716
|
+
let hadChanges = false;
|
|
717
|
+
for (const [key, timestamps] of this.recentTasks.entries()) {
|
|
718
|
+
// 过滤掉过期的时间戳
|
|
719
|
+
const validTimestamps = timestamps.filter(t => now - t < windowMs);
|
|
720
|
+
if (validTimestamps.length === 0) {
|
|
721
|
+
// 没有有效时间戳,删除整个条目
|
|
722
|
+
this.recentTasks.delete(key);
|
|
723
|
+
hadChanges = true;
|
|
724
|
+
}
|
|
725
|
+
else if (validTimestamps.length !== timestamps.length) {
|
|
726
|
+
// 更新为有效的时间戳
|
|
727
|
+
this.recentTasks.set(key, validTimestamps);
|
|
728
|
+
hadChanges = true;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
if (hadChanges) {
|
|
732
|
+
this.hasUnsavedChanges = true;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
exports.TaskGuard = TaskGuard;
|
|
737
|
+
// 导出单例
|
|
738
|
+
const globalTaskGuard = new TaskGuard();
|
|
739
|
+
// 注册进程退出处理,确保状态持久化
|
|
740
|
+
let shutdownHandlersRegistered = false;
|
|
741
|
+
const registerShutdownHandlers = () => {
|
|
742
|
+
if (shutdownHandlersRegistered)
|
|
743
|
+
return;
|
|
744
|
+
shutdownHandlersRegistered = true;
|
|
745
|
+
// 处理正常退出
|
|
746
|
+
process.on('beforeExit', () => {
|
|
747
|
+
globalTaskGuard.shutdown();
|
|
748
|
+
});
|
|
749
|
+
// 处理 SIGINT (Ctrl+C)
|
|
750
|
+
process.on('SIGINT', () => {
|
|
751
|
+
globalTaskGuard.shutdown();
|
|
752
|
+
process.exit(0);
|
|
753
|
+
});
|
|
754
|
+
// 处理 SIGTERM
|
|
755
|
+
process.on('SIGTERM', () => {
|
|
756
|
+
globalTaskGuard.shutdown();
|
|
757
|
+
process.exit(0);
|
|
758
|
+
});
|
|
759
|
+
};
|
|
760
|
+
// 不在模块加载时注册,改为在 initPersistence 中注册
|
|
761
|
+
// 避免阻止 openclaw gateway status 等一次性命令退出
|
|
762
|
+
exports.taskGuard = globalTaskGuard;
|
|
763
|
+
//# sourceMappingURL=task-guard.js.map
|