safeclaw 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/safeclaw.js +3 -0
- package/dist/main.js +4257 -0
- package/package.json +69 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/assets/index-8apm98QE.js +126 -0
- package/public/assets/index-Ck6Q-RL6.css +1 -0
- package/public/favicon-16.png +0 -0
- package/public/favicon-32.png +0 -0
- package/public/index.html +16 -0
- package/public/safeclaw_icon.png +0 -0
package/dist/main.js
ADDED
|
@@ -0,0 +1,4257 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __export = (target, all) => {
|
|
3
|
+
for (var name in all)
|
|
4
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
// src/main.ts
|
|
8
|
+
import { Command } from "commander";
|
|
9
|
+
|
|
10
|
+
// src/lib/version.ts
|
|
11
|
+
import { createRequire } from "module";
|
|
12
|
+
var require2 = createRequire(import.meta.url);
|
|
13
|
+
var pkg = require2("../../package.json");
|
|
14
|
+
var VERSION = pkg.version;
|
|
15
|
+
|
|
16
|
+
// src/server/index.ts
|
|
17
|
+
import Fastify from "fastify";
|
|
18
|
+
import fastifyStatic from "@fastify/static";
|
|
19
|
+
import fastifyCors from "@fastify/cors";
|
|
20
|
+
import fs7 from "fs";
|
|
21
|
+
|
|
22
|
+
// src/lib/paths.ts
|
|
23
|
+
import path from "path";
|
|
24
|
+
import os from "os";
|
|
25
|
+
var HOME = os.homedir();
|
|
26
|
+
var SAFECLAW_DIR = path.join(HOME, ".safeclaw");
|
|
27
|
+
var DB_PATH = path.join(SAFECLAW_DIR, "safeclaw.db");
|
|
28
|
+
var CONFIG_PATH = path.join(SAFECLAW_DIR, "config.json");
|
|
29
|
+
var LOGS_DIR = path.join(SAFECLAW_DIR, "logs");
|
|
30
|
+
var DEBUG_LOG_PATH = path.join(LOGS_DIR, "debug.log");
|
|
31
|
+
var OPENCLAW_DIR = path.join(HOME, ".openclaw");
|
|
32
|
+
var OPENCLAW_CONFIG_PATH = path.join(OPENCLAW_DIR, "openclaw.json");
|
|
33
|
+
var OPENCLAW_IDENTITY_DIR = path.join(OPENCLAW_DIR, "identity");
|
|
34
|
+
var OPENCLAW_DEVICE_JSON = path.join(OPENCLAW_IDENTITY_DIR, "device.json");
|
|
35
|
+
var OPENCLAW_DEVICE_AUTH_JSON = path.join(OPENCLAW_IDENTITY_DIR, "device-auth.json");
|
|
36
|
+
var OPENCLAW_EXEC_APPROVALS_PATH = path.join(OPENCLAW_DIR, "exec-approvals.json");
|
|
37
|
+
function getPublicDir() {
|
|
38
|
+
const currentDir = path.dirname(new URL(import.meta.url).pathname);
|
|
39
|
+
return path.resolve(currentDir, "..", "..", "public");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/server/socket.ts
|
|
43
|
+
import { Server as SocketIOServer } from "socket.io";
|
|
44
|
+
|
|
45
|
+
// src/db/index.ts
|
|
46
|
+
import Database from "better-sqlite3";
|
|
47
|
+
import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
48
|
+
|
|
49
|
+
// src/lib/config.ts
|
|
50
|
+
import fs from "fs";
|
|
51
|
+
|
|
52
|
+
// ../../packages/shared/dist/schemas.js
|
|
53
|
+
import { z } from "zod";
|
|
54
|
+
var commandStatusSchema = z.enum(["ALLOWED", "BLOCKED", "PENDING"]);
|
|
55
|
+
var threatLevelSchema = z.enum([
|
|
56
|
+
"NONE",
|
|
57
|
+
"LOW",
|
|
58
|
+
"MEDIUM",
|
|
59
|
+
"HIGH",
|
|
60
|
+
"CRITICAL"
|
|
61
|
+
]);
|
|
62
|
+
var sessionStatusSchema = z.enum(["ACTIVE", "ENDED"]);
|
|
63
|
+
var commandLogSchema = z.object({
|
|
64
|
+
id: z.number(),
|
|
65
|
+
command: z.string().min(1),
|
|
66
|
+
status: commandStatusSchema,
|
|
67
|
+
threatLevel: threatLevelSchema,
|
|
68
|
+
timestamp: z.string(),
|
|
69
|
+
sessionId: z.string().nullable(),
|
|
70
|
+
decisionBy: z.string().nullable()
|
|
71
|
+
});
|
|
72
|
+
var sessionSchema = z.object({
|
|
73
|
+
id: z.string(),
|
|
74
|
+
startedAt: z.string(),
|
|
75
|
+
endedAt: z.string().nullable(),
|
|
76
|
+
status: sessionStatusSchema
|
|
77
|
+
});
|
|
78
|
+
var decisionPayloadSchema = z.object({
|
|
79
|
+
commandId: z.number().int().positive(),
|
|
80
|
+
action: z.enum(["ALLOW", "DENY"])
|
|
81
|
+
});
|
|
82
|
+
var safeClawConfigSchema = z.object({
|
|
83
|
+
version: z.string(),
|
|
84
|
+
port: z.number().int().min(1024).max(65535).default(54335),
|
|
85
|
+
autoOpenBrowser: z.boolean().default(true),
|
|
86
|
+
premium: z.boolean().default(false),
|
|
87
|
+
userId: z.string().nullable().default(null)
|
|
88
|
+
});
|
|
89
|
+
var activityTypeSchema = z.enum([
|
|
90
|
+
"file_read",
|
|
91
|
+
"file_write",
|
|
92
|
+
"shell_command",
|
|
93
|
+
"web_browse",
|
|
94
|
+
"tool_call",
|
|
95
|
+
"message",
|
|
96
|
+
"unknown"
|
|
97
|
+
]);
|
|
98
|
+
var openClawConnectionStatusSchema = z.enum([
|
|
99
|
+
"connected",
|
|
100
|
+
"disconnected",
|
|
101
|
+
"connecting",
|
|
102
|
+
"not_configured"
|
|
103
|
+
]);
|
|
104
|
+
var threatCategoryIdSchema = z.enum([
|
|
105
|
+
"TC-SEC",
|
|
106
|
+
"TC-EXF",
|
|
107
|
+
"TC-INJ",
|
|
108
|
+
"TC-DES",
|
|
109
|
+
"TC-ESC",
|
|
110
|
+
"TC-SUP",
|
|
111
|
+
"TC-SFA",
|
|
112
|
+
"TC-SYS",
|
|
113
|
+
"TC-NET",
|
|
114
|
+
"TC-MCP"
|
|
115
|
+
]);
|
|
116
|
+
var threatFindingSchema = z.object({
|
|
117
|
+
categoryId: threatCategoryIdSchema,
|
|
118
|
+
categoryName: z.string(),
|
|
119
|
+
severity: threatLevelSchema,
|
|
120
|
+
reason: z.string(),
|
|
121
|
+
evidence: z.string().optional(),
|
|
122
|
+
owaspRef: z.string().optional()
|
|
123
|
+
});
|
|
124
|
+
var agentActivitySchema = z.object({
|
|
125
|
+
id: z.number(),
|
|
126
|
+
openclawSessionId: z.string(),
|
|
127
|
+
activityType: activityTypeSchema,
|
|
128
|
+
detail: z.string(),
|
|
129
|
+
rawPayload: z.string(),
|
|
130
|
+
threatLevel: threatLevelSchema,
|
|
131
|
+
timestamp: z.string(),
|
|
132
|
+
toolName: z.string().nullable(),
|
|
133
|
+
targetPath: z.string().nullable(),
|
|
134
|
+
runId: z.string().nullable(),
|
|
135
|
+
contentPreview: z.string().nullable(),
|
|
136
|
+
readContentPreview: z.string().nullable(),
|
|
137
|
+
secretsDetected: z.array(z.string()).nullable(),
|
|
138
|
+
threatFindings: z.array(threatFindingSchema).nullable(),
|
|
139
|
+
resolved: z.boolean(),
|
|
140
|
+
resolvedAt: z.string().nullable()
|
|
141
|
+
});
|
|
142
|
+
var openClawSessionSchema = z.object({
|
|
143
|
+
id: z.string(),
|
|
144
|
+
startedAt: z.string(),
|
|
145
|
+
endedAt: z.string().nullable(),
|
|
146
|
+
status: sessionStatusSchema,
|
|
147
|
+
model: z.string().nullable(),
|
|
148
|
+
activityCount: z.number(),
|
|
149
|
+
threatSummary: z.record(threatLevelSchema, z.number())
|
|
150
|
+
});
|
|
151
|
+
var openClawToolsExecSchema = z.object({
|
|
152
|
+
host: z.string().optional(),
|
|
153
|
+
security: z.enum(["deny", "allowlist", "full"]).optional(),
|
|
154
|
+
ask: z.enum(["off", "on-miss", "always"]).optional()
|
|
155
|
+
}).passthrough();
|
|
156
|
+
var openClawToolsConfigSchema = z.object({
|
|
157
|
+
allow: z.array(z.string()).optional(),
|
|
158
|
+
deny: z.array(z.string()).optional(),
|
|
159
|
+
profile: z.string().optional(),
|
|
160
|
+
exec: openClawToolsExecSchema.optional()
|
|
161
|
+
}).passthrough();
|
|
162
|
+
var openClawBrowserConfigSchema = z.object({
|
|
163
|
+
enabled: z.boolean().optional()
|
|
164
|
+
}).passthrough();
|
|
165
|
+
var accessCategorySchema = z.enum([
|
|
166
|
+
"filesystem",
|
|
167
|
+
"mcp_servers",
|
|
168
|
+
"network",
|
|
169
|
+
"system_commands"
|
|
170
|
+
]);
|
|
171
|
+
var accessToggleStateSchema = z.object({
|
|
172
|
+
category: accessCategorySchema,
|
|
173
|
+
enabled: z.boolean()
|
|
174
|
+
});
|
|
175
|
+
var mcpServerStateSchema = z.object({
|
|
176
|
+
name: z.string(),
|
|
177
|
+
pluginEnabled: z.boolean(),
|
|
178
|
+
toolsDenyBlocked: z.boolean(),
|
|
179
|
+
effectivelyEnabled: z.boolean()
|
|
180
|
+
});
|
|
181
|
+
var openClawSandboxDockerConfigSchema = z.object({
|
|
182
|
+
binds: z.array(z.string()).optional(),
|
|
183
|
+
network: z.string().optional()
|
|
184
|
+
}).passthrough();
|
|
185
|
+
var openClawSandboxConfigSchema = z.object({
|
|
186
|
+
mode: z.enum(["off", "non-main", "all"]).optional(),
|
|
187
|
+
workspaceAccess: z.enum(["none", "ro", "rw"]).optional(),
|
|
188
|
+
docker: openClawSandboxDockerConfigSchema.optional()
|
|
189
|
+
}).passthrough();
|
|
190
|
+
var openClawConfigSchema = z.object({
|
|
191
|
+
messages: z.object({
|
|
192
|
+
ackReactionScope: z.string().optional()
|
|
193
|
+
}).passthrough().optional(),
|
|
194
|
+
agents: z.object({
|
|
195
|
+
defaults: z.object({
|
|
196
|
+
maxConcurrent: z.number().optional(),
|
|
197
|
+
subagents: z.object({
|
|
198
|
+
maxConcurrent: z.number().optional()
|
|
199
|
+
}).passthrough().optional(),
|
|
200
|
+
compaction: z.object({
|
|
201
|
+
mode: z.string().optional()
|
|
202
|
+
}).passthrough().optional(),
|
|
203
|
+
workspace: z.string().optional(),
|
|
204
|
+
sandbox: openClawSandboxConfigSchema.optional(),
|
|
205
|
+
model: z.object({
|
|
206
|
+
primary: z.string().optional()
|
|
207
|
+
}).passthrough().optional(),
|
|
208
|
+
models: z.record(z.record(z.unknown())).optional()
|
|
209
|
+
}).passthrough().optional()
|
|
210
|
+
}).passthrough().optional(),
|
|
211
|
+
gateway: z.object({
|
|
212
|
+
mode: z.string().optional(),
|
|
213
|
+
auth: z.object({
|
|
214
|
+
mode: z.string().optional(),
|
|
215
|
+
token: z.string().optional()
|
|
216
|
+
}).passthrough().optional(),
|
|
217
|
+
port: z.number().optional(),
|
|
218
|
+
bind: z.string().optional(),
|
|
219
|
+
trustedProxies: z.array(z.string()).optional(),
|
|
220
|
+
tailscale: z.object({
|
|
221
|
+
mode: z.string().optional(),
|
|
222
|
+
resetOnExit: z.boolean().optional()
|
|
223
|
+
}).passthrough().optional()
|
|
224
|
+
}).passthrough().optional(),
|
|
225
|
+
auth: z.object({
|
|
226
|
+
profiles: z.record(z.object({
|
|
227
|
+
provider: z.string().optional(),
|
|
228
|
+
mode: z.string().optional()
|
|
229
|
+
}).passthrough()).optional()
|
|
230
|
+
}).passthrough().optional(),
|
|
231
|
+
plugins: z.object({
|
|
232
|
+
entries: z.record(z.object({
|
|
233
|
+
enabled: z.boolean().optional()
|
|
234
|
+
}).passthrough()).optional()
|
|
235
|
+
}).passthrough().optional(),
|
|
236
|
+
tools: openClawToolsConfigSchema.optional(),
|
|
237
|
+
browser: openClawBrowserConfigSchema.optional(),
|
|
238
|
+
channels: z.object({
|
|
239
|
+
whatsapp: z.object({
|
|
240
|
+
selfChatMode: z.boolean().optional(),
|
|
241
|
+
dmPolicy: z.string().optional(),
|
|
242
|
+
allowFrom: z.array(z.string()).optional()
|
|
243
|
+
}).passthrough().optional()
|
|
244
|
+
}).passthrough().optional(),
|
|
245
|
+
wizard: z.object({
|
|
246
|
+
lastRunAt: z.string().optional(),
|
|
247
|
+
lastRunVersion: z.string().optional(),
|
|
248
|
+
lastRunCommand: z.string().optional(),
|
|
249
|
+
lastRunMode: z.string().optional()
|
|
250
|
+
}).passthrough().optional(),
|
|
251
|
+
meta: z.object({
|
|
252
|
+
lastTouchedVersion: z.string().optional(),
|
|
253
|
+
lastTouchedAt: z.string().optional()
|
|
254
|
+
}).passthrough().optional()
|
|
255
|
+
}).passthrough();
|
|
256
|
+
var execDecisionSchema = z.enum(["allow-once", "allow-always", "deny"]);
|
|
257
|
+
var execApprovalEntrySchema = z.object({
|
|
258
|
+
id: z.string(),
|
|
259
|
+
command: z.string(),
|
|
260
|
+
cwd: z.string(),
|
|
261
|
+
security: z.string(),
|
|
262
|
+
sessionKey: z.string(),
|
|
263
|
+
requestedAt: z.string(),
|
|
264
|
+
expiresAt: z.string(),
|
|
265
|
+
decision: execDecisionSchema.nullable(),
|
|
266
|
+
decidedBy: z.enum(["user", "auto-deny"]).nullable(),
|
|
267
|
+
decidedAt: z.string().nullable()
|
|
268
|
+
});
|
|
269
|
+
var allowlistPatternSchema = z.object({
|
|
270
|
+
pattern: z.string().min(1)
|
|
271
|
+
});
|
|
272
|
+
var allowlistStateSchema = z.object({
|
|
273
|
+
patterns: z.array(allowlistPatternSchema)
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// src/lib/config.ts
|
|
277
|
+
var DEFAULT_CONFIG = {
|
|
278
|
+
version: VERSION,
|
|
279
|
+
port: 54335,
|
|
280
|
+
autoOpenBrowser: true,
|
|
281
|
+
premium: false,
|
|
282
|
+
userId: null
|
|
283
|
+
};
|
|
284
|
+
function ensureDataDir() {
|
|
285
|
+
if (!fs.existsSync(SAFECLAW_DIR)) {
|
|
286
|
+
fs.mkdirSync(SAFECLAW_DIR, { recursive: true });
|
|
287
|
+
}
|
|
288
|
+
if (!fs.existsSync(LOGS_DIR)) {
|
|
289
|
+
fs.mkdirSync(LOGS_DIR, { recursive: true });
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
function readConfig() {
|
|
293
|
+
ensureDataDir();
|
|
294
|
+
if (!fs.existsSync(CONFIG_PATH)) {
|
|
295
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(DEFAULT_CONFIG, null, 2));
|
|
296
|
+
return DEFAULT_CONFIG;
|
|
297
|
+
}
|
|
298
|
+
const raw = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
|
|
299
|
+
return safeClawConfigSchema.parse(raw);
|
|
300
|
+
}
|
|
301
|
+
function writeConfig(config) {
|
|
302
|
+
ensureDataDir();
|
|
303
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
304
|
+
}
|
|
305
|
+
function resetConfig() {
|
|
306
|
+
writeConfig(DEFAULT_CONFIG);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// src/db/schema.ts
|
|
310
|
+
var schema_exports = {};
|
|
311
|
+
__export(schema_exports, {
|
|
312
|
+
accessConfig: () => accessConfig,
|
|
313
|
+
agentActivities: () => agentActivities,
|
|
314
|
+
commandLogs: () => commandLogs,
|
|
315
|
+
execApprovals: () => execApprovals,
|
|
316
|
+
openclawSessions: () => openclawSessions,
|
|
317
|
+
restrictedPatterns: () => restrictedPatterns,
|
|
318
|
+
sessions: () => sessions
|
|
319
|
+
});
|
|
320
|
+
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
|
|
321
|
+
import { sql } from "drizzle-orm";
|
|
322
|
+
var commandLogs = sqliteTable("command_logs", {
|
|
323
|
+
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
324
|
+
command: text("command").notNull(),
|
|
325
|
+
status: text("status", {
|
|
326
|
+
enum: ["ALLOWED", "BLOCKED", "PENDING"]
|
|
327
|
+
}).notNull().default("PENDING"),
|
|
328
|
+
threatLevel: text("threat_level", {
|
|
329
|
+
enum: ["NONE", "LOW", "MEDIUM", "HIGH", "CRITICAL"]
|
|
330
|
+
}).notNull().default("NONE"),
|
|
331
|
+
timestamp: text("timestamp").notNull().default(sql`(datetime('now'))`),
|
|
332
|
+
sessionId: text("session_id"),
|
|
333
|
+
decisionBy: text("decision_by")
|
|
334
|
+
});
|
|
335
|
+
var sessions = sqliteTable("sessions", {
|
|
336
|
+
id: text("id").primaryKey(),
|
|
337
|
+
startedAt: text("started_at").notNull().default(sql`(datetime('now'))`),
|
|
338
|
+
endedAt: text("ended_at"),
|
|
339
|
+
status: text("status", {
|
|
340
|
+
enum: ["ACTIVE", "ENDED"]
|
|
341
|
+
}).notNull().default("ACTIVE")
|
|
342
|
+
});
|
|
343
|
+
var accessConfig = sqliteTable("access_config", {
|
|
344
|
+
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
345
|
+
category: text("category").notNull(),
|
|
346
|
+
key: text("key").notNull(),
|
|
347
|
+
value: text("value").notNull(),
|
|
348
|
+
updatedAt: text("updated_at").notNull().default(sql`(datetime('now'))`)
|
|
349
|
+
});
|
|
350
|
+
var openclawSessions = sqliteTable("openclaw_sessions", {
|
|
351
|
+
id: text("id").primaryKey(),
|
|
352
|
+
startedAt: text("started_at").notNull().default(sql`(datetime('now'))`),
|
|
353
|
+
endedAt: text("ended_at"),
|
|
354
|
+
status: text("status", {
|
|
355
|
+
enum: ["ACTIVE", "ENDED"]
|
|
356
|
+
}).notNull().default("ACTIVE"),
|
|
357
|
+
model: text("model")
|
|
358
|
+
});
|
|
359
|
+
var agentActivities = sqliteTable("agent_activities", {
|
|
360
|
+
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
361
|
+
openclawSessionId: text("openclaw_session_id").notNull(),
|
|
362
|
+
activityType: text("activity_type", {
|
|
363
|
+
enum: [
|
|
364
|
+
"file_read",
|
|
365
|
+
"file_write",
|
|
366
|
+
"shell_command",
|
|
367
|
+
"web_browse",
|
|
368
|
+
"tool_call",
|
|
369
|
+
"message",
|
|
370
|
+
"unknown"
|
|
371
|
+
]
|
|
372
|
+
}).notNull(),
|
|
373
|
+
detail: text("detail").notNull(),
|
|
374
|
+
rawPayload: text("raw_payload").notNull().default("{}"),
|
|
375
|
+
threatLevel: text("threat_level", {
|
|
376
|
+
enum: ["NONE", "LOW", "MEDIUM", "HIGH", "CRITICAL"]
|
|
377
|
+
}).notNull().default("NONE"),
|
|
378
|
+
timestamp: text("timestamp").notNull().default(sql`(datetime('now'))`),
|
|
379
|
+
toolName: text("tool_name"),
|
|
380
|
+
targetPath: text("target_path"),
|
|
381
|
+
runId: text("run_id"),
|
|
382
|
+
contentPreview: text("content_preview"),
|
|
383
|
+
readContentPreview: text("read_content_preview"),
|
|
384
|
+
secretsDetected: text("secrets_detected"),
|
|
385
|
+
threatFindings: text("threat_findings"),
|
|
386
|
+
resolved: integer("resolved").notNull().default(0),
|
|
387
|
+
resolvedAt: text("resolved_at")
|
|
388
|
+
});
|
|
389
|
+
var restrictedPatterns = sqliteTable("restricted_patterns", {
|
|
390
|
+
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
391
|
+
pattern: text("pattern").notNull().unique(),
|
|
392
|
+
createdAt: text("created_at").notNull().default(sql`(datetime('now'))`)
|
|
393
|
+
});
|
|
394
|
+
var execApprovals = sqliteTable("exec_approvals", {
|
|
395
|
+
id: text("id").primaryKey(),
|
|
396
|
+
command: text("command").notNull(),
|
|
397
|
+
cwd: text("cwd").notNull(),
|
|
398
|
+
security: text("security").notNull(),
|
|
399
|
+
sessionKey: text("session_key").notNull(),
|
|
400
|
+
requestedAt: text("requested_at").notNull(),
|
|
401
|
+
expiresAt: text("expires_at").notNull(),
|
|
402
|
+
decision: text("decision", {
|
|
403
|
+
enum: ["allow-once", "allow-always", "deny"]
|
|
404
|
+
}),
|
|
405
|
+
decidedBy: text("decided_by", {
|
|
406
|
+
enum: ["user", "auto-deny"]
|
|
407
|
+
}),
|
|
408
|
+
decidedAt: text("decided_at"),
|
|
409
|
+
matchedPattern: text("matched_pattern")
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
// src/db/index.ts
|
|
413
|
+
var _db = null;
|
|
414
|
+
function getDb() {
|
|
415
|
+
if (!_db) {
|
|
416
|
+
ensureDataDir();
|
|
417
|
+
const sqlite = new Database(DB_PATH);
|
|
418
|
+
sqlite.pragma("journal_mode = WAL");
|
|
419
|
+
_db = drizzle(sqlite, { schema: schema_exports });
|
|
420
|
+
}
|
|
421
|
+
return _db;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// src/server/socket.ts
|
|
425
|
+
import { eq as eq4, desc as desc3 } from "drizzle-orm";
|
|
426
|
+
|
|
427
|
+
// src/lib/logger.ts
|
|
428
|
+
import pino from "pino";
|
|
429
|
+
import fs2 from "fs";
|
|
430
|
+
if (!fs2.existsSync(LOGS_DIR)) {
|
|
431
|
+
fs2.mkdirSync(LOGS_DIR, { recursive: true });
|
|
432
|
+
}
|
|
433
|
+
function createLogger(consoleLevel = "info") {
|
|
434
|
+
return pino({
|
|
435
|
+
level: "debug",
|
|
436
|
+
transport: {
|
|
437
|
+
targets: [
|
|
438
|
+
{
|
|
439
|
+
target: "pino-pretty",
|
|
440
|
+
options: { colorize: true },
|
|
441
|
+
level: consoleLevel
|
|
442
|
+
},
|
|
443
|
+
{
|
|
444
|
+
target: "pino/file",
|
|
445
|
+
options: { destination: DEBUG_LOG_PATH },
|
|
446
|
+
level: "debug"
|
|
447
|
+
}
|
|
448
|
+
]
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
var logger = createLogger("info");
|
|
453
|
+
function setVerbose(verbose) {
|
|
454
|
+
if (verbose) {
|
|
455
|
+
logger = createLogger("debug");
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// src/lib/openclaw-config.ts
|
|
460
|
+
import fs3 from "fs";
|
|
461
|
+
function readOpenClawConfig() {
|
|
462
|
+
if (!fs3.existsSync(OPENCLAW_CONFIG_PATH)) {
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
const raw = JSON.parse(fs3.readFileSync(OPENCLAW_CONFIG_PATH, "utf-8"));
|
|
466
|
+
return openClawConfigSchema.parse(raw);
|
|
467
|
+
}
|
|
468
|
+
function writeOpenClawConfig(updates) {
|
|
469
|
+
const current = readOpenClawConfig();
|
|
470
|
+
if (!current) return null;
|
|
471
|
+
const merged = deepMerge(current, updates);
|
|
472
|
+
fs3.writeFileSync(OPENCLAW_CONFIG_PATH, JSON.stringify(merged, null, 2));
|
|
473
|
+
return merged;
|
|
474
|
+
}
|
|
475
|
+
function deepMerge(target, source) {
|
|
476
|
+
const result = { ...target };
|
|
477
|
+
for (const key of Object.keys(source)) {
|
|
478
|
+
const srcVal = source[key];
|
|
479
|
+
const tgtVal = target[key];
|
|
480
|
+
if (srcVal !== null && typeof srcVal === "object" && !Array.isArray(srcVal) && tgtVal !== null && typeof tgtVal === "object" && !Array.isArray(tgtVal)) {
|
|
481
|
+
result[key] = deepMerge(tgtVal, srcVal);
|
|
482
|
+
} else {
|
|
483
|
+
result[key] = srcVal;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return result;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// src/lib/openclaw-client.ts
|
|
490
|
+
import { EventEmitter } from "events";
|
|
491
|
+
import crypto from "crypto";
|
|
492
|
+
import fs4 from "fs";
|
|
493
|
+
import { WebSocket } from "ws";
|
|
494
|
+
function readDeviceIdentity() {
|
|
495
|
+
try {
|
|
496
|
+
const raw = fs4.readFileSync(OPENCLAW_DEVICE_JSON, "utf-8");
|
|
497
|
+
const data = JSON.parse(raw);
|
|
498
|
+
if (!data.deviceId || !data.publicKeyPem || !data.privateKeyPem) return null;
|
|
499
|
+
return {
|
|
500
|
+
deviceId: data.deviceId,
|
|
501
|
+
publicKeyPem: data.publicKeyPem,
|
|
502
|
+
privateKeyPem: data.privateKeyPem
|
|
503
|
+
};
|
|
504
|
+
} catch {
|
|
505
|
+
return null;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
function base64UrlEncode(buf) {
|
|
509
|
+
return buf.toString("base64url");
|
|
510
|
+
}
|
|
511
|
+
function publicKeyPemToRawBase64Url(pem) {
|
|
512
|
+
const key = crypto.createPublicKey(pem);
|
|
513
|
+
const jwk = key.export({ format: "jwk" });
|
|
514
|
+
if (!jwk.x) throw new Error("Failed to extract raw public key");
|
|
515
|
+
return jwk.x;
|
|
516
|
+
}
|
|
517
|
+
function buildDeviceAuthPayload(params) {
|
|
518
|
+
return [
|
|
519
|
+
"v1",
|
|
520
|
+
params.deviceId,
|
|
521
|
+
params.clientId,
|
|
522
|
+
params.clientMode,
|
|
523
|
+
params.role,
|
|
524
|
+
params.scopes.join(","),
|
|
525
|
+
String(params.signedAtMs),
|
|
526
|
+
params.token ?? ""
|
|
527
|
+
].join("|");
|
|
528
|
+
}
|
|
529
|
+
function signDevicePayload(privateKeyPem, payload) {
|
|
530
|
+
const key = crypto.createPrivateKey(privateKeyPem);
|
|
531
|
+
const sig = crypto.sign(null, Buffer.from(payload, "utf-8"), key);
|
|
532
|
+
return base64UrlEncode(sig);
|
|
533
|
+
}
|
|
534
|
+
var OpenClawClient = class extends EventEmitter {
|
|
535
|
+
ws = null;
|
|
536
|
+
status = "disconnected";
|
|
537
|
+
reconnectTimer = null;
|
|
538
|
+
reconnectAttempts = 0;
|
|
539
|
+
maxReconnectAttempts = 20;
|
|
540
|
+
baseReconnectDelay = 2e3;
|
|
541
|
+
destroyed = false;
|
|
542
|
+
pendingRequests = /* @__PURE__ */ new Map();
|
|
543
|
+
activeSessions = /* @__PURE__ */ new Set();
|
|
544
|
+
connect() {
|
|
545
|
+
if (this.destroyed) return;
|
|
546
|
+
const config = readOpenClawConfig();
|
|
547
|
+
if (!config?.gateway) {
|
|
548
|
+
this.setStatus("not_configured");
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
const port = config.gateway.port ?? 18789;
|
|
552
|
+
const identity = readDeviceIdentity();
|
|
553
|
+
if (!identity) {
|
|
554
|
+
logger.warn("OpenClaw device identity not found at ~/.openclaw/identity/device.json");
|
|
555
|
+
this.setStatus("not_configured");
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
const gatewayToken = config.gateway.auth?.token ?? null;
|
|
559
|
+
if (!gatewayToken) {
|
|
560
|
+
logger.warn("OpenClaw gateway auth token not found in config");
|
|
561
|
+
this.setStatus("not_configured");
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
this.setStatus("connecting");
|
|
565
|
+
try {
|
|
566
|
+
const url = `ws://127.0.0.1:${port}`;
|
|
567
|
+
this.ws = new WebSocket(url);
|
|
568
|
+
this.ws.on("open", () => {
|
|
569
|
+
logger.info(`WebSocket open to OpenClaw gateway on port ${port}`);
|
|
570
|
+
});
|
|
571
|
+
this.ws.on("message", (data) => {
|
|
572
|
+
try {
|
|
573
|
+
const msg = JSON.parse(String(data));
|
|
574
|
+
this.handleMessage(msg, identity, gatewayToken);
|
|
575
|
+
} catch (err) {
|
|
576
|
+
logger.warn({ err }, "Failed to parse OpenClaw gateway message");
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
this.ws.on("close", (code, reason) => {
|
|
580
|
+
logger.info(
|
|
581
|
+
{ code, reason: reason.toString() },
|
|
582
|
+
"OpenClaw gateway connection closed"
|
|
583
|
+
);
|
|
584
|
+
this.cleanupPendingRequests();
|
|
585
|
+
this.setStatus("disconnected");
|
|
586
|
+
this.scheduleReconnect();
|
|
587
|
+
});
|
|
588
|
+
this.ws.on("error", (err) => {
|
|
589
|
+
logger.debug({ err: err.message }, "OpenClaw WebSocket error");
|
|
590
|
+
});
|
|
591
|
+
} catch (err) {
|
|
592
|
+
logger.error({ err }, "Failed to create OpenClaw WebSocket connection");
|
|
593
|
+
this.setStatus("disconnected");
|
|
594
|
+
this.scheduleReconnect();
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
// --- Message dispatcher ---
|
|
598
|
+
handleMessage(msg, identity, gatewayToken) {
|
|
599
|
+
switch (msg.type) {
|
|
600
|
+
case "event":
|
|
601
|
+
this.handleEvent(msg, identity, gatewayToken);
|
|
602
|
+
break;
|
|
603
|
+
case "res":
|
|
604
|
+
this.handleResponse(msg);
|
|
605
|
+
break;
|
|
606
|
+
case "req":
|
|
607
|
+
break;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
// --- Event handler ---
|
|
611
|
+
handleEvent(evt, identity, gatewayToken) {
|
|
612
|
+
switch (evt.event) {
|
|
613
|
+
case "connect.challenge":
|
|
614
|
+
this.sendConnectRequest(identity, gatewayToken, evt.payload);
|
|
615
|
+
break;
|
|
616
|
+
case "tick":
|
|
617
|
+
break;
|
|
618
|
+
case "agent":
|
|
619
|
+
this.handleAgentEvent(evt);
|
|
620
|
+
break;
|
|
621
|
+
case "chat":
|
|
622
|
+
this.handleChatEvent(evt);
|
|
623
|
+
break;
|
|
624
|
+
case "presence":
|
|
625
|
+
break;
|
|
626
|
+
case "exec.approval.requested":
|
|
627
|
+
this.handleExecApproval(evt);
|
|
628
|
+
break;
|
|
629
|
+
case "shutdown":
|
|
630
|
+
logger.info("OpenClaw gateway shutting down");
|
|
631
|
+
break;
|
|
632
|
+
default:
|
|
633
|
+
logger.debug({ event: evt.event }, "Unhandled OpenClaw event type");
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
// --- Connect handshake ---
|
|
637
|
+
async sendConnectRequest(identity, gatewayToken, _challengePayload) {
|
|
638
|
+
try {
|
|
639
|
+
const clientId = "gateway-client";
|
|
640
|
+
const clientMode = "backend";
|
|
641
|
+
const role = "operator";
|
|
642
|
+
const scopes = ["operator.read", "operator.approvals"];
|
|
643
|
+
const signedAtMs = Date.now();
|
|
644
|
+
const payload = buildDeviceAuthPayload({
|
|
645
|
+
deviceId: identity.deviceId,
|
|
646
|
+
clientId,
|
|
647
|
+
clientMode,
|
|
648
|
+
role,
|
|
649
|
+
scopes,
|
|
650
|
+
signedAtMs,
|
|
651
|
+
token: gatewayToken
|
|
652
|
+
});
|
|
653
|
+
const signature = signDevicePayload(identity.privateKeyPem, payload);
|
|
654
|
+
const publicKey = publicKeyPemToRawBase64Url(identity.publicKeyPem);
|
|
655
|
+
const res = await this.sendRequest("connect", {
|
|
656
|
+
minProtocol: 3,
|
|
657
|
+
maxProtocol: 3,
|
|
658
|
+
client: {
|
|
659
|
+
id: clientId,
|
|
660
|
+
version: "1.0.0",
|
|
661
|
+
platform: process.platform,
|
|
662
|
+
mode: clientMode
|
|
663
|
+
},
|
|
664
|
+
role,
|
|
665
|
+
scopes,
|
|
666
|
+
caps: [],
|
|
667
|
+
device: {
|
|
668
|
+
id: identity.deviceId,
|
|
669
|
+
publicKey,
|
|
670
|
+
signature,
|
|
671
|
+
signedAt: signedAtMs
|
|
672
|
+
},
|
|
673
|
+
auth: { token: gatewayToken },
|
|
674
|
+
locale: "en-US",
|
|
675
|
+
userAgent: "safeclaw-monitor/1.0.0"
|
|
676
|
+
});
|
|
677
|
+
if (res.ok && res.payload?.type === "hello-ok") {
|
|
678
|
+
this.reconnectAttempts = 0;
|
|
679
|
+
this.setStatus("connected");
|
|
680
|
+
logger.info("Successfully connected to OpenClaw gateway (hello-ok)");
|
|
681
|
+
} else {
|
|
682
|
+
logger.error(
|
|
683
|
+
{ error: res.error },
|
|
684
|
+
"OpenClaw connect handshake rejected"
|
|
685
|
+
);
|
|
686
|
+
this.ws?.close();
|
|
687
|
+
}
|
|
688
|
+
} catch (err) {
|
|
689
|
+
logger.error({ err }, "OpenClaw connect handshake failed");
|
|
690
|
+
this.ws?.close();
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
// --- Agent event parsing ---
|
|
694
|
+
handleAgentEvent(evt) {
|
|
695
|
+
const payload = evt.payload ?? {};
|
|
696
|
+
const sessionKey = payload.sessionKey ?? "unknown";
|
|
697
|
+
const stream = payload.stream;
|
|
698
|
+
const data = payload.data ?? {};
|
|
699
|
+
const ts = payload.ts;
|
|
700
|
+
const timestamp = ts ? new Date(ts).toISOString() : (/* @__PURE__ */ new Date()).toISOString();
|
|
701
|
+
if (stream === "lifecycle") {
|
|
702
|
+
const phase = data.phase;
|
|
703
|
+
if (phase === "start") {
|
|
704
|
+
if (!this.activeSessions.has(sessionKey)) {
|
|
705
|
+
this.activeSessions.add(sessionKey);
|
|
706
|
+
this.emit("sessionStart", sessionKey);
|
|
707
|
+
}
|
|
708
|
+
} else if (phase === "end" || phase === "error") {
|
|
709
|
+
if (this.activeSessions.has(sessionKey)) {
|
|
710
|
+
this.activeSessions.delete(sessionKey);
|
|
711
|
+
this.emit("sessionEnd", sessionKey);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
if (stream === "tool") {
|
|
717
|
+
const toolName = data.name ?? data.toolName ?? "unknown";
|
|
718
|
+
const phase = data.phase;
|
|
719
|
+
const args = data.args ?? {};
|
|
720
|
+
const metaRaw = data.meta;
|
|
721
|
+
const meta = typeof metaRaw === "object" && metaRaw !== null ? metaRaw : typeof metaRaw === "string" ? { description: metaRaw } : {};
|
|
722
|
+
const parsed = this.parseToolActivity(
|
|
723
|
+
sessionKey,
|
|
724
|
+
toolName,
|
|
725
|
+
phase ?? "start",
|
|
726
|
+
args,
|
|
727
|
+
meta,
|
|
728
|
+
JSON.stringify(evt),
|
|
729
|
+
timestamp
|
|
730
|
+
);
|
|
731
|
+
if (parsed) {
|
|
732
|
+
this.emit("activity", parsed);
|
|
733
|
+
}
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
if (stream === "assistant") {
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
if (stream === "compaction") {
|
|
740
|
+
logger.debug({ sessionKey }, "OpenClaw context compaction event");
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
// --- Chat event parsing (WhatsApp, etc.) ---
|
|
744
|
+
handleChatEvent(evt) {
|
|
745
|
+
const payload = evt.payload ?? {};
|
|
746
|
+
const sessionKey = payload.sessionKey ?? "unknown";
|
|
747
|
+
const state = payload.state;
|
|
748
|
+
const status = payload.status;
|
|
749
|
+
const message = payload.message;
|
|
750
|
+
const ts = payload.ts;
|
|
751
|
+
const timestamp = ts ? new Date(ts).toISOString() : (/* @__PURE__ */ new Date()).toISOString();
|
|
752
|
+
const runId = payload.runId ?? null;
|
|
753
|
+
let channel = "unknown";
|
|
754
|
+
const parts = sessionKey.split(":");
|
|
755
|
+
if (parts.length >= 1) {
|
|
756
|
+
channel = parts[0];
|
|
757
|
+
}
|
|
758
|
+
let targetPath = null;
|
|
759
|
+
if (parts.length >= 3) {
|
|
760
|
+
targetPath = parts.slice(2).join(":");
|
|
761
|
+
}
|
|
762
|
+
const eventState = state ?? status;
|
|
763
|
+
if (eventState === "delta") {
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
const role = message?.role;
|
|
767
|
+
const content = message?.content;
|
|
768
|
+
let messageText = "";
|
|
769
|
+
if (Array.isArray(content)) {
|
|
770
|
+
messageText = content.filter((item) => item?.type === "text").map((item) => item?.text).join(" ").slice(0, 100);
|
|
771
|
+
}
|
|
772
|
+
const detail = eventState === "final" ? `${channel} message: ${messageText || "(sent)"}` : eventState === "started" ? `${channel} message sent` : eventState === "ok" ? `${channel} message delivered` : eventState === "error" ? `${channel} message failed` : `${channel} message (${eventState ?? "unknown"})`;
|
|
773
|
+
const activity = {
|
|
774
|
+
openclawSessionId: sessionKey,
|
|
775
|
+
activityType: "message",
|
|
776
|
+
detail,
|
|
777
|
+
rawPayload: JSON.stringify(evt),
|
|
778
|
+
toolName: channel,
|
|
779
|
+
targetPath,
|
|
780
|
+
timestamp,
|
|
781
|
+
runId
|
|
782
|
+
};
|
|
783
|
+
this.emit("activity", activity);
|
|
784
|
+
if (channel !== "agent" && eventState === "started" && !this.activeSessions.has(sessionKey)) {
|
|
785
|
+
this.activeSessions.add(sessionKey);
|
|
786
|
+
this.emit("sessionStart", sessionKey);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
// --- Exec approval event handling ---
|
|
790
|
+
handleExecApproval(evt) {
|
|
791
|
+
const payload = evt.payload ?? {};
|
|
792
|
+
const request = payload.request ?? {};
|
|
793
|
+
const sessionKey = request.sessionKey ?? "unknown";
|
|
794
|
+
const approval = {
|
|
795
|
+
id: payload.id ?? "unknown",
|
|
796
|
+
command: request.command ?? "",
|
|
797
|
+
cwd: request.cwd ?? "",
|
|
798
|
+
security: request.security ?? "normal",
|
|
799
|
+
sessionKey
|
|
800
|
+
};
|
|
801
|
+
logger.info(
|
|
802
|
+
{ command: approval.command, security: approval.security },
|
|
803
|
+
"Exec approval requested by OpenClaw agent"
|
|
804
|
+
);
|
|
805
|
+
this.emit("execApproval", approval);
|
|
806
|
+
const activity = {
|
|
807
|
+
openclawSessionId: sessionKey,
|
|
808
|
+
activityType: "shell_command",
|
|
809
|
+
detail: `[APPROVAL] ${approval.command}`,
|
|
810
|
+
rawPayload: JSON.stringify(evt),
|
|
811
|
+
toolName: "exec",
|
|
812
|
+
targetPath: null,
|
|
813
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
814
|
+
runId: null
|
|
815
|
+
};
|
|
816
|
+
this.emit("activity", activity);
|
|
817
|
+
}
|
|
818
|
+
// --- Tool → Activity type mapping ---
|
|
819
|
+
parseToolActivity(sessionId, toolName, phase, args, meta, rawPayload, timestamp) {
|
|
820
|
+
let activityType = "tool_call";
|
|
821
|
+
let detail = `Tool: ${toolName} (${phase})`;
|
|
822
|
+
let targetPath = null;
|
|
823
|
+
const lowerTool = toolName.toLowerCase();
|
|
824
|
+
if (lowerTool === "read" || lowerTool === "attach") {
|
|
825
|
+
activityType = "file_read";
|
|
826
|
+
targetPath = args.path ?? args.file_path ?? args.url;
|
|
827
|
+
detail = `Read ${targetPath ?? "unknown file"}`;
|
|
828
|
+
} else if (lowerTool === "write" || lowerTool === "edit" || lowerTool === "create" || lowerTool === "patch" || lowerTool === "apply_patch") {
|
|
829
|
+
activityType = "file_write";
|
|
830
|
+
targetPath = args.path ?? args.file_path ?? args.file;
|
|
831
|
+
detail = `${lowerTool === "edit" ? "Edit" : lowerTool === "apply_patch" ? "Patch" : "Write"} ${targetPath ?? "unknown file"}`;
|
|
832
|
+
} else if (lowerTool === "exec" || lowerTool === "bash" || lowerTool === "shell" || lowerTool === "command" || lowerTool === "terminal") {
|
|
833
|
+
activityType = "shell_command";
|
|
834
|
+
const cmd = args.command ?? args.cmd ?? "";
|
|
835
|
+
detail = cmd || `Shell: ${toolName}`;
|
|
836
|
+
targetPath = null;
|
|
837
|
+
} else if (lowerTool === "browser" || lowerTool === "browse" || lowerTool === "fetch" || lowerTool === "web" || lowerTool === "http" || lowerTool === "url") {
|
|
838
|
+
activityType = "web_browse";
|
|
839
|
+
targetPath = args.url ?? args.uri;
|
|
840
|
+
const action = args.action ?? "";
|
|
841
|
+
detail = action ? `Browse: ${action}${targetPath ? ` ${targetPath}` : ""}` : `Browse ${targetPath ?? "unknown URL"}`;
|
|
842
|
+
} else if (lowerTool === "message" || lowerTool === "send" || lowerTool === "whatsapp" || lowerTool === "sms" || lowerTool === "notify") {
|
|
843
|
+
activityType = "message";
|
|
844
|
+
const to = args.to ?? args.target ?? args.recipient;
|
|
845
|
+
const provider = args.provider ?? "";
|
|
846
|
+
const action = args.action ?? "send";
|
|
847
|
+
detail = `${provider || toolName} ${action}${to ? ` to ${to}` : ""}`;
|
|
848
|
+
targetPath = to;
|
|
849
|
+
} else {
|
|
850
|
+
const desc5 = meta.description ?? "";
|
|
851
|
+
const action = args.action ?? "";
|
|
852
|
+
detail = desc5 ? `${toolName}: ${desc5}` : action ? `${toolName}: ${action}` : `Tool: ${toolName}`;
|
|
853
|
+
}
|
|
854
|
+
return {
|
|
855
|
+
openclawSessionId: sessionId,
|
|
856
|
+
activityType,
|
|
857
|
+
detail,
|
|
858
|
+
rawPayload,
|
|
859
|
+
toolName,
|
|
860
|
+
targetPath,
|
|
861
|
+
timestamp,
|
|
862
|
+
runId: null
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
// --- Request/response ---
|
|
866
|
+
sendRequest(method, params) {
|
|
867
|
+
return new Promise((resolve, reject) => {
|
|
868
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
869
|
+
return reject(new Error("WebSocket not open"));
|
|
870
|
+
}
|
|
871
|
+
const id = crypto.randomUUID();
|
|
872
|
+
const msg = { type: "req", id, method, params };
|
|
873
|
+
const timer = setTimeout(() => {
|
|
874
|
+
this.pendingRequests.delete(id);
|
|
875
|
+
reject(new Error(`Request ${method} timed out`));
|
|
876
|
+
}, 1e4);
|
|
877
|
+
this.pendingRequests.set(id, { resolve, timer });
|
|
878
|
+
this.ws.send(JSON.stringify(msg));
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
handleResponse(res) {
|
|
882
|
+
const pending = this.pendingRequests.get(res.id);
|
|
883
|
+
if (pending) {
|
|
884
|
+
clearTimeout(pending.timer);
|
|
885
|
+
this.pendingRequests.delete(res.id);
|
|
886
|
+
pending.resolve(res);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
cleanupPendingRequests() {
|
|
890
|
+
for (const [id, pending] of this.pendingRequests) {
|
|
891
|
+
clearTimeout(pending.timer);
|
|
892
|
+
this.pendingRequests.delete(id);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
// --- Reconnection ---
|
|
896
|
+
scheduleReconnect() {
|
|
897
|
+
if (this.destroyed) return;
|
|
898
|
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
899
|
+
logger.warn("Max reconnect attempts reached for OpenClaw gateway");
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
const delay = Math.min(
|
|
903
|
+
this.baseReconnectDelay * Math.pow(1.5, this.reconnectAttempts),
|
|
904
|
+
3e4
|
|
905
|
+
);
|
|
906
|
+
this.reconnectAttempts++;
|
|
907
|
+
logger.info(
|
|
908
|
+
`Reconnecting to OpenClaw in ${Math.round(delay)}ms (attempt ${this.reconnectAttempts})`
|
|
909
|
+
);
|
|
910
|
+
this.reconnectTimer = setTimeout(() => this.connect(), delay);
|
|
911
|
+
}
|
|
912
|
+
// --- Status ---
|
|
913
|
+
setStatus(status) {
|
|
914
|
+
if (this.status !== status) {
|
|
915
|
+
this.status = status;
|
|
916
|
+
this.emit("statusChange", status);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
getStatus() {
|
|
920
|
+
return this.status;
|
|
921
|
+
}
|
|
922
|
+
// --- Exec approval resolution ---
|
|
923
|
+
async resolveExecApproval(approvalId, decision) {
|
|
924
|
+
try {
|
|
925
|
+
const res = await this.sendRequest("exec.approval.resolve", {
|
|
926
|
+
id: approvalId,
|
|
927
|
+
decision
|
|
928
|
+
});
|
|
929
|
+
if (res.ok) {
|
|
930
|
+
logger.info({ approvalId, decision }, "Exec approval resolved");
|
|
931
|
+
return true;
|
|
932
|
+
}
|
|
933
|
+
logger.error(
|
|
934
|
+
{ approvalId, decision, error: res.error },
|
|
935
|
+
"Exec approval resolution rejected"
|
|
936
|
+
);
|
|
937
|
+
return false;
|
|
938
|
+
} catch (err) {
|
|
939
|
+
logger.error(
|
|
940
|
+
{ err, approvalId, decision },
|
|
941
|
+
"Failed to resolve exec approval"
|
|
942
|
+
);
|
|
943
|
+
return false;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
// --- Exec approvals file management (via gateway) ---
|
|
947
|
+
async getExecApprovals() {
|
|
948
|
+
try {
|
|
949
|
+
const res = await this.sendRequest("exec.approvals.get", {});
|
|
950
|
+
if (res.ok && res.payload) {
|
|
951
|
+
return {
|
|
952
|
+
file: res.payload.file,
|
|
953
|
+
hash: res.payload.hash
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
logger.error(
|
|
957
|
+
{ error: res.error },
|
|
958
|
+
"Failed to get exec approvals from gateway"
|
|
959
|
+
);
|
|
960
|
+
return null;
|
|
961
|
+
} catch (err) {
|
|
962
|
+
logger.error({ err }, "Failed to get exec approvals from gateway");
|
|
963
|
+
return null;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
async setExecApprovals(file, baseHash) {
|
|
967
|
+
try {
|
|
968
|
+
const res = await this.sendRequest("exec.approvals.set", {
|
|
969
|
+
file,
|
|
970
|
+
baseHash
|
|
971
|
+
});
|
|
972
|
+
if (res.ok) {
|
|
973
|
+
logger.info("Successfully updated exec approvals via gateway");
|
|
974
|
+
return true;
|
|
975
|
+
}
|
|
976
|
+
logger.error(
|
|
977
|
+
{ error: res.error },
|
|
978
|
+
"Failed to set exec approvals on gateway"
|
|
979
|
+
);
|
|
980
|
+
return false;
|
|
981
|
+
} catch (err) {
|
|
982
|
+
logger.error({ err }, "Failed to set exec approvals on gateway");
|
|
983
|
+
return false;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
// --- Lifecycle ---
|
|
987
|
+
reconnect() {
|
|
988
|
+
this.reconnectAttempts = 0;
|
|
989
|
+
this.disconnect();
|
|
990
|
+
this.connect();
|
|
991
|
+
}
|
|
992
|
+
disconnect() {
|
|
993
|
+
if (this.reconnectTimer) {
|
|
994
|
+
clearTimeout(this.reconnectTimer);
|
|
995
|
+
this.reconnectTimer = null;
|
|
996
|
+
}
|
|
997
|
+
this.cleanupPendingRequests();
|
|
998
|
+
if (this.ws) {
|
|
999
|
+
this.ws.removeAllListeners();
|
|
1000
|
+
this.ws.close();
|
|
1001
|
+
this.ws = null;
|
|
1002
|
+
}
|
|
1003
|
+
this.setStatus("disconnected");
|
|
1004
|
+
}
|
|
1005
|
+
destroy() {
|
|
1006
|
+
this.destroyed = true;
|
|
1007
|
+
this.disconnect();
|
|
1008
|
+
this.removeAllListeners();
|
|
1009
|
+
}
|
|
1010
|
+
};
|
|
1011
|
+
|
|
1012
|
+
// src/services/session-watcher.ts
|
|
1013
|
+
import { EventEmitter as EventEmitter2 } from "events";
|
|
1014
|
+
import fs5 from "fs";
|
|
1015
|
+
import path2 from "path";
|
|
1016
|
+
var MAX_CONTENT_PREVIEW = 10 * 1024;
|
|
1017
|
+
var SessionWatcher = class extends EventEmitter2 {
|
|
1018
|
+
watchedFiles = /* @__PURE__ */ new Map();
|
|
1019
|
+
agentsDirWatcher = null;
|
|
1020
|
+
scanInterval = null;
|
|
1021
|
+
destroyed = false;
|
|
1022
|
+
// Track current runId (user message id) per session file
|
|
1023
|
+
currentRunIds = /* @__PURE__ */ new Map();
|
|
1024
|
+
// Track processed tool call IDs to avoid duplicates
|
|
1025
|
+
processedToolCalls = /* @__PURE__ */ new Set();
|
|
1026
|
+
// Map toolCallId → pending tool call info for matching with results
|
|
1027
|
+
pendingToolCalls = /* @__PURE__ */ new Map();
|
|
1028
|
+
// Cache read content by "runId:targetPath" so writes can reference what was read
|
|
1029
|
+
recentReadContent = /* @__PURE__ */ new Map();
|
|
1030
|
+
start() {
|
|
1031
|
+
if (this.destroyed) return;
|
|
1032
|
+
const agentsDir = path2.join(OPENCLAW_DIR, "agents");
|
|
1033
|
+
if (!fs5.existsSync(agentsDir)) {
|
|
1034
|
+
logger.debug("OpenClaw agents directory not found, will retry");
|
|
1035
|
+
}
|
|
1036
|
+
this.scanForSessions();
|
|
1037
|
+
this.scanInterval = setInterval(() => {
|
|
1038
|
+
if (!this.destroyed) this.scanForSessions();
|
|
1039
|
+
}, 1e4);
|
|
1040
|
+
try {
|
|
1041
|
+
if (fs5.existsSync(agentsDir)) {
|
|
1042
|
+
this.agentsDirWatcher = fs5.watch(agentsDir, { recursive: true }, () => {
|
|
1043
|
+
if (!this.destroyed) this.scanForSessions();
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
} catch {
|
|
1047
|
+
}
|
|
1048
|
+
logger.info("Session file watcher started");
|
|
1049
|
+
}
|
|
1050
|
+
stop() {
|
|
1051
|
+
this.destroyed = true;
|
|
1052
|
+
if (this.scanInterval) {
|
|
1053
|
+
clearInterval(this.scanInterval);
|
|
1054
|
+
this.scanInterval = null;
|
|
1055
|
+
}
|
|
1056
|
+
if (this.agentsDirWatcher) {
|
|
1057
|
+
this.agentsDirWatcher.close();
|
|
1058
|
+
this.agentsDirWatcher = null;
|
|
1059
|
+
}
|
|
1060
|
+
for (const watched of this.watchedFiles.values()) {
|
|
1061
|
+
if (watched.watcher) {
|
|
1062
|
+
watched.watcher.close();
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
this.watchedFiles.clear();
|
|
1066
|
+
logger.info("Session file watcher stopped");
|
|
1067
|
+
}
|
|
1068
|
+
scanForSessions() {
|
|
1069
|
+
const agentsDir = path2.join(OPENCLAW_DIR, "agents");
|
|
1070
|
+
if (!fs5.existsSync(agentsDir)) return;
|
|
1071
|
+
try {
|
|
1072
|
+
const agentNames = fs5.readdirSync(agentsDir).filter((name) => {
|
|
1073
|
+
const stat = fs5.statSync(path2.join(agentsDir, name));
|
|
1074
|
+
return stat.isDirectory();
|
|
1075
|
+
});
|
|
1076
|
+
for (const agentName of agentNames) {
|
|
1077
|
+
this.discoverSessionFiles(agentName);
|
|
1078
|
+
}
|
|
1079
|
+
} catch (err) {
|
|
1080
|
+
logger.debug({ err }, "Failed to scan agents directory");
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
discoverSessionFiles(agentName) {
|
|
1084
|
+
const sessionsDir = path2.join(OPENCLAW_DIR, "agents", agentName, "sessions");
|
|
1085
|
+
if (!fs5.existsSync(sessionsDir)) return;
|
|
1086
|
+
const sessionsJsonPath = path2.join(sessionsDir, "sessions.json");
|
|
1087
|
+
if (fs5.existsSync(sessionsJsonPath)) {
|
|
1088
|
+
try {
|
|
1089
|
+
const raw = fs5.readFileSync(sessionsJsonPath, "utf-8");
|
|
1090
|
+
const sessionsData = JSON.parse(raw);
|
|
1091
|
+
const sessions2 = sessionsData.sessions ?? [];
|
|
1092
|
+
for (const session of sessions2) {
|
|
1093
|
+
const jsonlFile = session.file ? path2.join(sessionsDir, session.file) : path2.join(sessionsDir, `${session.id}.jsonl`);
|
|
1094
|
+
this.watchSessionFile(jsonlFile, session.id, agentName);
|
|
1095
|
+
}
|
|
1096
|
+
} catch (err) {
|
|
1097
|
+
logger.debug({ err, agentName }, "Failed to read sessions.json");
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
try {
|
|
1101
|
+
const files = fs5.readdirSync(sessionsDir).filter((f) => f.endsWith(".jsonl"));
|
|
1102
|
+
for (const file of files) {
|
|
1103
|
+
const filePath = path2.join(sessionsDir, file);
|
|
1104
|
+
const sessionId = path2.basename(file, ".jsonl");
|
|
1105
|
+
this.watchSessionFile(filePath, sessionId, agentName);
|
|
1106
|
+
}
|
|
1107
|
+
} catch {
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
watchSessionFile(filePath, sessionId, agentName) {
|
|
1111
|
+
if (this.watchedFiles.has(filePath)) return;
|
|
1112
|
+
if (!fs5.existsSync(filePath)) return;
|
|
1113
|
+
const stat = fs5.statSync(filePath);
|
|
1114
|
+
const watched = {
|
|
1115
|
+
path: filePath,
|
|
1116
|
+
position: stat.size,
|
|
1117
|
+
// Start from current end (don't replay history)
|
|
1118
|
+
watcher: null,
|
|
1119
|
+
sessionId,
|
|
1120
|
+
agentName
|
|
1121
|
+
};
|
|
1122
|
+
try {
|
|
1123
|
+
watched.watcher = fs5.watch(filePath, () => {
|
|
1124
|
+
if (!this.destroyed) {
|
|
1125
|
+
this.readNewEntries(watched);
|
|
1126
|
+
}
|
|
1127
|
+
});
|
|
1128
|
+
} catch (err) {
|
|
1129
|
+
logger.debug({ err, filePath }, "Failed to watch session file");
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
this.watchedFiles.set(filePath, watched);
|
|
1133
|
+
logger.info({ sessionId, agentName }, "Watching session JSONL file");
|
|
1134
|
+
}
|
|
1135
|
+
readNewEntries(watched) {
|
|
1136
|
+
try {
|
|
1137
|
+
const stat = fs5.statSync(watched.path);
|
|
1138
|
+
if (stat.size <= watched.position) return;
|
|
1139
|
+
const fd = fs5.openSync(watched.path, "r");
|
|
1140
|
+
const bufferSize = stat.size - watched.position;
|
|
1141
|
+
const buffer = Buffer.alloc(bufferSize);
|
|
1142
|
+
fs5.readSync(fd, buffer, 0, bufferSize, watched.position);
|
|
1143
|
+
fs5.closeSync(fd);
|
|
1144
|
+
watched.position = stat.size;
|
|
1145
|
+
const text2 = buffer.toString("utf-8");
|
|
1146
|
+
const lines = text2.split("\n").filter((l) => l.trim());
|
|
1147
|
+
for (const line of lines) {
|
|
1148
|
+
try {
|
|
1149
|
+
const entry = JSON.parse(line);
|
|
1150
|
+
this.processEntry(entry, watched.sessionId);
|
|
1151
|
+
} catch {
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
} catch (err) {
|
|
1155
|
+
logger.debug({ err, path: watched.path }, "Failed to read new JSONL entries");
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
processEntry(entry, openclawSessionId) {
|
|
1159
|
+
if (entry.type !== "message" || !entry.message) return;
|
|
1160
|
+
const { role, content } = entry.message;
|
|
1161
|
+
const fileKey = openclawSessionId;
|
|
1162
|
+
if (role === "user") {
|
|
1163
|
+
const previousRunId = this.currentRunIds.get(fileKey);
|
|
1164
|
+
if (previousRunId) {
|
|
1165
|
+
for (const key of this.recentReadContent.keys()) {
|
|
1166
|
+
if (key.startsWith(`${previousRunId}:`)) {
|
|
1167
|
+
this.recentReadContent.delete(key);
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
this.currentRunIds.set(fileKey, entry.id);
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1174
|
+
const currentRunId = this.currentRunIds.get(fileKey) ?? null;
|
|
1175
|
+
if (role === "assistant" && Array.isArray(content)) {
|
|
1176
|
+
for (const item of content) {
|
|
1177
|
+
if (item.type === "toolCall" && item.id && item.name) {
|
|
1178
|
+
if (this.processedToolCalls.has(item.id)) continue;
|
|
1179
|
+
this.processedToolCalls.add(item.id);
|
|
1180
|
+
const args = item.arguments ?? {};
|
|
1181
|
+
const { activityType, detail, targetPath } = this.mapToolToActivity(
|
|
1182
|
+
item.name,
|
|
1183
|
+
args
|
|
1184
|
+
);
|
|
1185
|
+
const timestamp = entry.timestamp || (/* @__PURE__ */ new Date()).toISOString();
|
|
1186
|
+
this.pendingToolCalls.set(item.id, {
|
|
1187
|
+
activityType,
|
|
1188
|
+
toolName: item.name,
|
|
1189
|
+
targetPath,
|
|
1190
|
+
detail,
|
|
1191
|
+
sessionId: openclawSessionId,
|
|
1192
|
+
runId: currentRunId,
|
|
1193
|
+
timestamp
|
|
1194
|
+
});
|
|
1195
|
+
const activity = {
|
|
1196
|
+
openclawSessionId,
|
|
1197
|
+
activityType,
|
|
1198
|
+
detail,
|
|
1199
|
+
rawPayload: JSON.stringify(entry),
|
|
1200
|
+
toolName: item.name,
|
|
1201
|
+
targetPath,
|
|
1202
|
+
timestamp,
|
|
1203
|
+
runId: currentRunId,
|
|
1204
|
+
contentPreview: null,
|
|
1205
|
+
readContentPreview: null
|
|
1206
|
+
};
|
|
1207
|
+
this.emit("activity", activity);
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1212
|
+
if (role === "toolResult" && Array.isArray(content)) {
|
|
1213
|
+
const toolCallId = entry.message.toolCallId;
|
|
1214
|
+
if (!toolCallId) return;
|
|
1215
|
+
const pending = this.pendingToolCalls.get(toolCallId);
|
|
1216
|
+
if (!pending) return;
|
|
1217
|
+
this.pendingToolCalls.delete(toolCallId);
|
|
1218
|
+
let contentPreview = null;
|
|
1219
|
+
const textParts = [];
|
|
1220
|
+
for (const item of content) {
|
|
1221
|
+
if (item.type === "text" && item.text) {
|
|
1222
|
+
textParts.push(item.text);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
if (textParts.length > 0) {
|
|
1226
|
+
const fullContent = textParts.join("\n");
|
|
1227
|
+
contentPreview = fullContent.length > MAX_CONTENT_PREVIEW ? fullContent.slice(0, MAX_CONTENT_PREVIEW) + "...[truncated]" : fullContent;
|
|
1228
|
+
}
|
|
1229
|
+
if (contentPreview) {
|
|
1230
|
+
if (pending.activityType === "file_read" && pending.targetPath && pending.runId) {
|
|
1231
|
+
this.recentReadContent.set(`${pending.runId}:${pending.targetPath}`, contentPreview);
|
|
1232
|
+
}
|
|
1233
|
+
let readContentPreview = null;
|
|
1234
|
+
if (pending.activityType === "file_write" && pending.targetPath && pending.runId) {
|
|
1235
|
+
readContentPreview = this.recentReadContent.get(`${pending.runId}:${pending.targetPath}`) ?? null;
|
|
1236
|
+
}
|
|
1237
|
+
const activity = {
|
|
1238
|
+
openclawSessionId: pending.sessionId,
|
|
1239
|
+
activityType: pending.activityType,
|
|
1240
|
+
detail: `${pending.detail} [result]`,
|
|
1241
|
+
rawPayload: JSON.stringify(entry),
|
|
1242
|
+
toolName: pending.toolName,
|
|
1243
|
+
targetPath: pending.targetPath,
|
|
1244
|
+
timestamp: entry.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
|
|
1245
|
+
runId: pending.runId,
|
|
1246
|
+
contentPreview,
|
|
1247
|
+
readContentPreview
|
|
1248
|
+
};
|
|
1249
|
+
this.emit("activity", activity);
|
|
1250
|
+
}
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
mapToolToActivity(toolName, args) {
|
|
1255
|
+
const lower = toolName.toLowerCase();
|
|
1256
|
+
if (lower === "read" || lower === "attach") {
|
|
1257
|
+
const targetPath = args.path ?? args.file_path ?? args.url;
|
|
1258
|
+
return {
|
|
1259
|
+
activityType: "file_read",
|
|
1260
|
+
detail: `Read ${targetPath ?? "unknown file"}`,
|
|
1261
|
+
targetPath
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
if (lower === "write" || lower === "edit" || lower === "create" || lower === "patch" || lower === "apply_patch" || lower === "notebook_edit") {
|
|
1265
|
+
const targetPath = args.path ?? args.file_path ?? args.file ?? args.notebook_path;
|
|
1266
|
+
const verb = lower === "edit" ? "Edit" : lower === "apply_patch" ? "Patch" : lower === "notebook_edit" ? "Edit notebook" : "Write";
|
|
1267
|
+
return {
|
|
1268
|
+
activityType: "file_write",
|
|
1269
|
+
detail: `${verb} ${targetPath ?? "unknown file"}`,
|
|
1270
|
+
targetPath
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1273
|
+
if (lower === "exec" || lower === "bash" || lower === "shell" || lower === "command" || lower === "terminal") {
|
|
1274
|
+
const cmd = args.command ?? args.cmd ?? "";
|
|
1275
|
+
return {
|
|
1276
|
+
activityType: "shell_command",
|
|
1277
|
+
detail: cmd || `Shell: ${toolName}`,
|
|
1278
|
+
targetPath: null
|
|
1279
|
+
};
|
|
1280
|
+
}
|
|
1281
|
+
if (lower === "browser" || lower === "browse" || lower === "fetch" || lower === "web" || lower === "http" || lower === "url") {
|
|
1282
|
+
const targetPath = args.url ?? args.uri;
|
|
1283
|
+
return {
|
|
1284
|
+
activityType: "web_browse",
|
|
1285
|
+
detail: `Browse ${targetPath ?? "unknown URL"}`,
|
|
1286
|
+
targetPath
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
if (lower === "message" || lower === "send" || lower === "whatsapp" || lower === "sms" || lower === "notify") {
|
|
1290
|
+
const to = args.to ?? args.target ?? args.recipient;
|
|
1291
|
+
return {
|
|
1292
|
+
activityType: "message",
|
|
1293
|
+
detail: `${toolName} send${to ? ` to ${to}` : ""}`,
|
|
1294
|
+
targetPath: to
|
|
1295
|
+
};
|
|
1296
|
+
}
|
|
1297
|
+
if (lower === "glob" || lower === "grep" || lower === "search" || lower === "find") {
|
|
1298
|
+
const pattern = args.pattern ?? args.query ?? "";
|
|
1299
|
+
return {
|
|
1300
|
+
activityType: "file_read",
|
|
1301
|
+
detail: `${toolName}: ${pattern}`,
|
|
1302
|
+
targetPath: null
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
return {
|
|
1306
|
+
activityType: "tool_call",
|
|
1307
|
+
detail: `Tool: ${toolName}`,
|
|
1308
|
+
targetPath: null
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
};
|
|
1312
|
+
|
|
1313
|
+
// src/services/exec-approval-service.ts
|
|
1314
|
+
import { eq, desc, sql as sql2 } from "drizzle-orm";
|
|
1315
|
+
import path3 from "path";
|
|
1316
|
+
var DEFAULT_TIMEOUT_MS = 6e5;
|
|
1317
|
+
var instance = null;
|
|
1318
|
+
function matchesPattern(command, pattern) {
|
|
1319
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
1320
|
+
const regexStr = "^" + escaped.replace(/\*/g, ".*") + "$";
|
|
1321
|
+
return new RegExp(regexStr, "i").test(command);
|
|
1322
|
+
}
|
|
1323
|
+
var ExecApprovalService = class {
|
|
1324
|
+
client;
|
|
1325
|
+
io;
|
|
1326
|
+
pending = /* @__PURE__ */ new Map();
|
|
1327
|
+
timeoutMs;
|
|
1328
|
+
/** In-memory cache of restricted patterns, loaded from DB on init */
|
|
1329
|
+
restrictedPatterns = [];
|
|
1330
|
+
constructor(client, io2, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
1331
|
+
this.client = client;
|
|
1332
|
+
this.io = io2;
|
|
1333
|
+
this.timeoutMs = timeoutMs;
|
|
1334
|
+
this.loadPatternsFromDb();
|
|
1335
|
+
}
|
|
1336
|
+
/**
|
|
1337
|
+
* Load restricted patterns from the database into the in-memory cache.
|
|
1338
|
+
* Called once on construction so patterns survive restarts.
|
|
1339
|
+
*/
|
|
1340
|
+
loadPatternsFromDb() {
|
|
1341
|
+
try {
|
|
1342
|
+
const db = getDb();
|
|
1343
|
+
const rows = db.select({ pattern: schema_exports.restrictedPatterns.pattern }).from(schema_exports.restrictedPatterns).all();
|
|
1344
|
+
this.restrictedPatterns = rows.map((r) => r.pattern);
|
|
1345
|
+
logger.info(
|
|
1346
|
+
{ count: this.restrictedPatterns.length },
|
|
1347
|
+
"Loaded restricted patterns from database"
|
|
1348
|
+
);
|
|
1349
|
+
} catch (err) {
|
|
1350
|
+
logger.error({ err }, "Failed to load restricted patterns from database");
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
/**
|
|
1354
|
+
* Handle an incoming exec approval request from the OpenClaw gateway.
|
|
1355
|
+
*
|
|
1356
|
+
* Blocklist model:
|
|
1357
|
+
* - If the command matches a restricted pattern → queue for user approval
|
|
1358
|
+
* - If it doesn't match → auto-approve immediately with "allow-once"
|
|
1359
|
+
*/
|
|
1360
|
+
handleRequest(request) {
|
|
1361
|
+
const matchedPattern = this.findMatchingPattern(request.command);
|
|
1362
|
+
if (!matchedPattern) {
|
|
1363
|
+
this.resolveToGateway(request.id, "allow-once");
|
|
1364
|
+
logger.debug(
|
|
1365
|
+
{ command: request.command },
|
|
1366
|
+
"Command auto-approved (not restricted)"
|
|
1367
|
+
);
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
const now = /* @__PURE__ */ new Date();
|
|
1371
|
+
const expiresAt = new Date(now.getTime() + this.timeoutMs);
|
|
1372
|
+
const entry = {
|
|
1373
|
+
id: request.id,
|
|
1374
|
+
command: request.command,
|
|
1375
|
+
cwd: request.cwd,
|
|
1376
|
+
security: request.security,
|
|
1377
|
+
sessionKey: request.sessionKey,
|
|
1378
|
+
requestedAt: now.toISOString(),
|
|
1379
|
+
expiresAt: expiresAt.toISOString(),
|
|
1380
|
+
decision: null,
|
|
1381
|
+
decidedBy: null,
|
|
1382
|
+
decidedAt: null
|
|
1383
|
+
};
|
|
1384
|
+
const timer = setTimeout(() => {
|
|
1385
|
+
this.handleTimeout(request.id);
|
|
1386
|
+
}, this.timeoutMs);
|
|
1387
|
+
this.pending.set(request.id, { entry, timer, matchedPattern });
|
|
1388
|
+
this.persistApproval(entry, matchedPattern);
|
|
1389
|
+
this.io.emit("safeclaw:execApprovalRequested", entry);
|
|
1390
|
+
logger.info(
|
|
1391
|
+
{
|
|
1392
|
+
command: request.command,
|
|
1393
|
+
matchedPattern,
|
|
1394
|
+
timeoutMs: this.timeoutMs
|
|
1395
|
+
},
|
|
1396
|
+
"Restricted command queued for approval"
|
|
1397
|
+
);
|
|
1398
|
+
}
|
|
1399
|
+
/**
|
|
1400
|
+
* Handle a user decision from the UI.
|
|
1401
|
+
*/
|
|
1402
|
+
handleDecision(approvalId, decision) {
|
|
1403
|
+
const pending = this.pending.get(approvalId);
|
|
1404
|
+
if (!pending) {
|
|
1405
|
+
logger.warn(
|
|
1406
|
+
{ approvalId },
|
|
1407
|
+
"Decision for unknown or already-resolved approval"
|
|
1408
|
+
);
|
|
1409
|
+
return;
|
|
1410
|
+
}
|
|
1411
|
+
clearTimeout(pending.timer);
|
|
1412
|
+
this.pending.delete(approvalId);
|
|
1413
|
+
const entry = pending.entry;
|
|
1414
|
+
entry.decision = decision;
|
|
1415
|
+
entry.decidedBy = "user";
|
|
1416
|
+
entry.decidedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1417
|
+
this.resolveToGateway(approvalId, decision);
|
|
1418
|
+
this.updateApprovalDecision(entry);
|
|
1419
|
+
if (decision === "allow-always" && pending.matchedPattern) {
|
|
1420
|
+
this.removeRestrictedPattern(pending.matchedPattern);
|
|
1421
|
+
}
|
|
1422
|
+
this.io.emit("safeclaw:execApprovalResolved", entry);
|
|
1423
|
+
logger.info(
|
|
1424
|
+
{ command: entry.command, decision, approvalId },
|
|
1425
|
+
"Exec approval decided by user"
|
|
1426
|
+
);
|
|
1427
|
+
}
|
|
1428
|
+
// --- Timeout handling ---
|
|
1429
|
+
handleTimeout(approvalId) {
|
|
1430
|
+
const pending = this.pending.get(approvalId);
|
|
1431
|
+
if (!pending) return;
|
|
1432
|
+
this.pending.delete(approvalId);
|
|
1433
|
+
const entry = pending.entry;
|
|
1434
|
+
entry.decision = "deny";
|
|
1435
|
+
entry.decidedBy = "auto-deny";
|
|
1436
|
+
entry.decidedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1437
|
+
this.resolveToGateway(approvalId, "deny");
|
|
1438
|
+
this.updateApprovalDecision(entry);
|
|
1439
|
+
this.io.emit("safeclaw:execApprovalResolved", entry);
|
|
1440
|
+
logger.info(
|
|
1441
|
+
{ command: entry.command, approvalId },
|
|
1442
|
+
"Exec approval auto-denied (timeout)"
|
|
1443
|
+
);
|
|
1444
|
+
}
|
|
1445
|
+
// --- Gateway communication ---
|
|
1446
|
+
resolveToGateway(approvalId, decision) {
|
|
1447
|
+
this.client.resolveExecApproval(approvalId, decision).catch((err) => {
|
|
1448
|
+
logger.error(
|
|
1449
|
+
{ err, approvalId, decision },
|
|
1450
|
+
"Failed to send decision to gateway"
|
|
1451
|
+
);
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
1454
|
+
// --- Database persistence ---
|
|
1455
|
+
persistApproval(entry, matchedPattern) {
|
|
1456
|
+
try {
|
|
1457
|
+
const db = getDb();
|
|
1458
|
+
db.insert(schema_exports.execApprovals).values({
|
|
1459
|
+
id: entry.id,
|
|
1460
|
+
command: entry.command,
|
|
1461
|
+
cwd: entry.cwd,
|
|
1462
|
+
security: entry.security,
|
|
1463
|
+
sessionKey: entry.sessionKey,
|
|
1464
|
+
requestedAt: entry.requestedAt,
|
|
1465
|
+
expiresAt: entry.expiresAt,
|
|
1466
|
+
decision: entry.decision,
|
|
1467
|
+
decidedBy: entry.decidedBy,
|
|
1468
|
+
decidedAt: entry.decidedAt,
|
|
1469
|
+
matchedPattern
|
|
1470
|
+
}).run();
|
|
1471
|
+
} catch (err) {
|
|
1472
|
+
logger.error({ err, id: entry.id }, "Failed to persist exec approval");
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
updateApprovalDecision(entry) {
|
|
1476
|
+
try {
|
|
1477
|
+
const db = getDb();
|
|
1478
|
+
db.update(schema_exports.execApprovals).set({
|
|
1479
|
+
decision: entry.decision,
|
|
1480
|
+
decidedBy: entry.decidedBy,
|
|
1481
|
+
decidedAt: entry.decidedAt
|
|
1482
|
+
}).where(eq(schema_exports.execApprovals.id, entry.id)).run();
|
|
1483
|
+
} catch (err) {
|
|
1484
|
+
logger.error(
|
|
1485
|
+
{ err, id: entry.id },
|
|
1486
|
+
"Failed to update exec approval decision"
|
|
1487
|
+
);
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
// --- Pattern matching ---
|
|
1491
|
+
findMatchingPattern(command) {
|
|
1492
|
+
for (const pattern of this.restrictedPatterns) {
|
|
1493
|
+
if (matchesPattern(command, pattern)) {
|
|
1494
|
+
return pattern;
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
return null;
|
|
1498
|
+
}
|
|
1499
|
+
// --- Restricted patterns management ---
|
|
1500
|
+
getRestrictedPatterns() {
|
|
1501
|
+
return [...this.restrictedPatterns];
|
|
1502
|
+
}
|
|
1503
|
+
addRestrictedPattern(pattern) {
|
|
1504
|
+
const trimmed = pattern.trim();
|
|
1505
|
+
if (!trimmed) return this.restrictedPatterns;
|
|
1506
|
+
if (!this.restrictedPatterns.includes(trimmed)) {
|
|
1507
|
+
this.restrictedPatterns.push(trimmed);
|
|
1508
|
+
try {
|
|
1509
|
+
const db = getDb();
|
|
1510
|
+
db.insert(schema_exports.restrictedPatterns).values({ pattern: trimmed }).onConflictDoNothing().run();
|
|
1511
|
+
} catch (err) {
|
|
1512
|
+
logger.error({ err, pattern: trimmed }, "Failed to persist restricted pattern");
|
|
1513
|
+
}
|
|
1514
|
+
this.syncRemoveFromOpenClawAllowlist(trimmed).catch((err) => {
|
|
1515
|
+
logger.error(
|
|
1516
|
+
{ err, pattern: trimmed },
|
|
1517
|
+
"Failed to sync pattern to OpenClaw allowlist"
|
|
1518
|
+
);
|
|
1519
|
+
});
|
|
1520
|
+
logger.info({ pattern: trimmed }, "Added restricted command pattern");
|
|
1521
|
+
}
|
|
1522
|
+
this.broadcastPatterns();
|
|
1523
|
+
return [...this.restrictedPatterns];
|
|
1524
|
+
}
|
|
1525
|
+
removeRestrictedPattern(pattern) {
|
|
1526
|
+
this.restrictedPatterns = this.restrictedPatterns.filter(
|
|
1527
|
+
(p) => p !== pattern
|
|
1528
|
+
);
|
|
1529
|
+
try {
|
|
1530
|
+
const db = getDb();
|
|
1531
|
+
db.delete(schema_exports.restrictedPatterns).where(eq(schema_exports.restrictedPatterns.pattern, pattern)).run();
|
|
1532
|
+
} catch (err) {
|
|
1533
|
+
logger.error({ err, pattern }, "Failed to remove restricted pattern from database");
|
|
1534
|
+
}
|
|
1535
|
+
logger.info({ pattern }, "Removed restricted command pattern");
|
|
1536
|
+
this.broadcastPatterns();
|
|
1537
|
+
return [...this.restrictedPatterns];
|
|
1538
|
+
}
|
|
1539
|
+
// --- OpenClaw allowlist sync ---
|
|
1540
|
+
/**
|
|
1541
|
+
* Determine if an OpenClaw allowlist entry matches a SafeClaw restricted pattern.
|
|
1542
|
+
*
|
|
1543
|
+
* Matching strategy:
|
|
1544
|
+
* 1. Extract the binary name from the SafeClaw pattern's first token
|
|
1545
|
+
* and compare against the basename of the allowlist entry's path.
|
|
1546
|
+
* 2. Check entry.lastUsedCommand against the full glob pattern.
|
|
1547
|
+
* 3. Check the entry.pattern itself against the full glob pattern.
|
|
1548
|
+
*/
|
|
1549
|
+
allowlistEntryMatchesRestriction(entry, restrictedPattern) {
|
|
1550
|
+
const firstToken = restrictedPattern.split(/\s+/)[0].replace(/\*+$/, "");
|
|
1551
|
+
if (firstToken) {
|
|
1552
|
+
const entryBasename = path3.basename(entry.pattern);
|
|
1553
|
+
if (entryBasename === firstToken || entryBasename.startsWith(firstToken)) {
|
|
1554
|
+
return true;
|
|
1555
|
+
}
|
|
1556
|
+
if (entry.lastResolvedPath) {
|
|
1557
|
+
const resolvedBasename = path3.basename(entry.lastResolvedPath);
|
|
1558
|
+
if (resolvedBasename === firstToken || resolvedBasename.startsWith(firstToken)) {
|
|
1559
|
+
return true;
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
if (entry.lastUsedCommand && matchesPattern(entry.lastUsedCommand, restrictedPattern)) {
|
|
1564
|
+
return true;
|
|
1565
|
+
}
|
|
1566
|
+
if (matchesPattern(entry.pattern, restrictedPattern)) {
|
|
1567
|
+
return true;
|
|
1568
|
+
}
|
|
1569
|
+
return false;
|
|
1570
|
+
}
|
|
1571
|
+
/**
|
|
1572
|
+
* Remove entries from OpenClaw's exec-approvals allowlist that match
|
|
1573
|
+
* the given restricted pattern. Uses optimistic locking via the gateway.
|
|
1574
|
+
*/
|
|
1575
|
+
async syncRemoveFromOpenClawAllowlist(pattern, maxRetries = 2) {
|
|
1576
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
1577
|
+
try {
|
|
1578
|
+
const result = await this.client.getExecApprovals();
|
|
1579
|
+
if (!result) {
|
|
1580
|
+
logger.warn("Could not read OpenClaw exec approvals for sync");
|
|
1581
|
+
return;
|
|
1582
|
+
}
|
|
1583
|
+
const { file, hash } = result;
|
|
1584
|
+
let modified = false;
|
|
1585
|
+
if (file.agents) {
|
|
1586
|
+
for (const agentKey of Object.keys(file.agents)) {
|
|
1587
|
+
const agent = file.agents[agentKey];
|
|
1588
|
+
if (!agent.allowlist || agent.allowlist.length === 0) continue;
|
|
1589
|
+
const before = agent.allowlist.length;
|
|
1590
|
+
agent.allowlist = agent.allowlist.filter(
|
|
1591
|
+
(entry) => !this.allowlistEntryMatchesRestriction(entry, pattern)
|
|
1592
|
+
);
|
|
1593
|
+
const removed = before - agent.allowlist.length;
|
|
1594
|
+
if (removed > 0) {
|
|
1595
|
+
modified = true;
|
|
1596
|
+
logger.info(
|
|
1597
|
+
{ agentKey, pattern, removed },
|
|
1598
|
+
"Filtered OpenClaw allowlist entries matching new restriction"
|
|
1599
|
+
);
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
if (!modified) return;
|
|
1604
|
+
const success = await this.client.setExecApprovals(file, hash);
|
|
1605
|
+
if (success) return;
|
|
1606
|
+
if (attempt < maxRetries) {
|
|
1607
|
+
logger.warn(
|
|
1608
|
+
{ attempt, pattern },
|
|
1609
|
+
"Optimistic lock conflict, retrying sync"
|
|
1610
|
+
);
|
|
1611
|
+
}
|
|
1612
|
+
} catch (err) {
|
|
1613
|
+
logger.error({ err, pattern, attempt }, "Sync attempt failed");
|
|
1614
|
+
if (attempt >= maxRetries) return;
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
logger.error(
|
|
1618
|
+
{ pattern },
|
|
1619
|
+
"Exhausted retries for OpenClaw allowlist sync"
|
|
1620
|
+
);
|
|
1621
|
+
}
|
|
1622
|
+
broadcastPatterns() {
|
|
1623
|
+
this.io.emit("safeclaw:allowlistState", {
|
|
1624
|
+
patterns: this.restrictedPatterns.map((p) => ({ pattern: p }))
|
|
1625
|
+
});
|
|
1626
|
+
}
|
|
1627
|
+
// --- Query methods ---
|
|
1628
|
+
getPendingApprovals() {
|
|
1629
|
+
return Array.from(this.pending.values()).map((p) => p.entry);
|
|
1630
|
+
}
|
|
1631
|
+
getHistory(limit = 50) {
|
|
1632
|
+
try {
|
|
1633
|
+
const db = getDb();
|
|
1634
|
+
const rows = db.select().from(schema_exports.execApprovals).where(sql2`${schema_exports.execApprovals.decision} IS NOT NULL`).orderBy(desc(schema_exports.execApprovals.decidedAt)).limit(limit).all();
|
|
1635
|
+
return rows.map((r) => ({
|
|
1636
|
+
id: r.id,
|
|
1637
|
+
command: r.command,
|
|
1638
|
+
cwd: r.cwd,
|
|
1639
|
+
security: r.security,
|
|
1640
|
+
sessionKey: r.sessionKey,
|
|
1641
|
+
requestedAt: r.requestedAt,
|
|
1642
|
+
expiresAt: r.expiresAt,
|
|
1643
|
+
decision: r.decision,
|
|
1644
|
+
decidedBy: r.decidedBy,
|
|
1645
|
+
decidedAt: r.decidedAt
|
|
1646
|
+
}));
|
|
1647
|
+
} catch (err) {
|
|
1648
|
+
logger.error({ err }, "Failed to load approval history from database");
|
|
1649
|
+
return [];
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
/**
|
|
1653
|
+
* Get total counts for exec approvals from the database.
|
|
1654
|
+
*/
|
|
1655
|
+
getStats() {
|
|
1656
|
+
try {
|
|
1657
|
+
const db = getDb();
|
|
1658
|
+
const rows = db.select().from(schema_exports.execApprovals).all();
|
|
1659
|
+
const decided = rows.filter((r) => r.decision !== null);
|
|
1660
|
+
const blocked = decided.filter((r) => r.decision === "deny").length;
|
|
1661
|
+
const allowed = decided.filter(
|
|
1662
|
+
(r) => r.decision === "allow-once" || r.decision === "allow-always"
|
|
1663
|
+
).length;
|
|
1664
|
+
const pendingDb = rows.filter((r) => r.decision === null).length;
|
|
1665
|
+
const livePending = this.pending.size;
|
|
1666
|
+
return {
|
|
1667
|
+
total: rows.length + livePending - pendingDb,
|
|
1668
|
+
// avoid double-counting DB pending
|
|
1669
|
+
blocked,
|
|
1670
|
+
allowed,
|
|
1671
|
+
pending: livePending
|
|
1672
|
+
};
|
|
1673
|
+
} catch (err) {
|
|
1674
|
+
logger.error({ err }, "Failed to compute exec approval stats");
|
|
1675
|
+
return { total: 0, blocked: 0, allowed: 0, pending: 0 };
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
/**
|
|
1679
|
+
* Clean up all pending timers on shutdown.
|
|
1680
|
+
*/
|
|
1681
|
+
destroy() {
|
|
1682
|
+
for (const [, pending] of this.pending) {
|
|
1683
|
+
clearTimeout(pending.timer);
|
|
1684
|
+
}
|
|
1685
|
+
this.pending.clear();
|
|
1686
|
+
}
|
|
1687
|
+
};
|
|
1688
|
+
function createExecApprovalService(client, io2, timeoutMs) {
|
|
1689
|
+
if (instance) {
|
|
1690
|
+
instance.destroy();
|
|
1691
|
+
}
|
|
1692
|
+
instance = new ExecApprovalService(client, io2, timeoutMs);
|
|
1693
|
+
return instance;
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
// src/lib/exec-approvals-config.ts
|
|
1697
|
+
import fs6 from "fs";
|
|
1698
|
+
function readExecApprovalsConfig() {
|
|
1699
|
+
try {
|
|
1700
|
+
if (!fs6.existsSync(OPENCLAW_EXEC_APPROVALS_PATH)) return null;
|
|
1701
|
+
const raw = fs6.readFileSync(OPENCLAW_EXEC_APPROVALS_PATH, "utf-8");
|
|
1702
|
+
return JSON.parse(raw);
|
|
1703
|
+
} catch (err) {
|
|
1704
|
+
logger.warn({ err }, "Failed to read exec-approvals.json");
|
|
1705
|
+
return null;
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
function writeExecApprovalsConfig(config) {
|
|
1709
|
+
fs6.writeFileSync(
|
|
1710
|
+
OPENCLAW_EXEC_APPROVALS_PATH,
|
|
1711
|
+
JSON.stringify(config, null, 2)
|
|
1712
|
+
);
|
|
1713
|
+
}
|
|
1714
|
+
function ensureDefaults() {
|
|
1715
|
+
const config = readExecApprovalsConfig();
|
|
1716
|
+
if (!config) {
|
|
1717
|
+
logger.warn("exec-approvals.json not found, cannot configure defaults");
|
|
1718
|
+
return;
|
|
1719
|
+
}
|
|
1720
|
+
let changed = false;
|
|
1721
|
+
if (!config.defaults) {
|
|
1722
|
+
config.defaults = {};
|
|
1723
|
+
}
|
|
1724
|
+
if (config.defaults.security !== "allowlist") {
|
|
1725
|
+
config.defaults.security = "allowlist";
|
|
1726
|
+
changed = true;
|
|
1727
|
+
}
|
|
1728
|
+
if (config.defaults.ask !== "always") {
|
|
1729
|
+
config.defaults.ask = "always";
|
|
1730
|
+
changed = true;
|
|
1731
|
+
}
|
|
1732
|
+
if (config.defaults.askFallback !== "deny") {
|
|
1733
|
+
config.defaults.askFallback = "deny";
|
|
1734
|
+
changed = true;
|
|
1735
|
+
}
|
|
1736
|
+
if (changed) {
|
|
1737
|
+
writeExecApprovalsConfig(config);
|
|
1738
|
+
logger.info(
|
|
1739
|
+
{ defaults: config.defaults },
|
|
1740
|
+
"Configured exec-approvals.json defaults for command interception"
|
|
1741
|
+
);
|
|
1742
|
+
}
|
|
1743
|
+
const openclawConfig = readOpenClawConfig();
|
|
1744
|
+
if (openclawConfig) {
|
|
1745
|
+
const execHost = openclawConfig.tools?.exec?.host;
|
|
1746
|
+
if (execHost !== "gateway") {
|
|
1747
|
+
writeOpenClawConfig({ tools: { exec: { host: "gateway" } } });
|
|
1748
|
+
logger.info(
|
|
1749
|
+
"Set tools.exec.host='gateway' in openclaw.json for exec approval support"
|
|
1750
|
+
);
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
// src/lib/secret-scanner.ts
|
|
1756
|
+
var SECRET_PATTERNS = [
|
|
1757
|
+
// AWS
|
|
1758
|
+
{ pattern: /AKIA[0-9A-Z]{16}/, type: "AWS_ACCESS_KEY", severity: "CRITICAL" },
|
|
1759
|
+
{ pattern: /(?:aws_secret_access_key|AWS_SECRET_ACCESS_KEY)\s*[=:]\s*\S{20,}/, type: "AWS_SECRET_KEY", severity: "CRITICAL" },
|
|
1760
|
+
// OpenAI
|
|
1761
|
+
{ pattern: /sk-[a-zA-Z0-9]{20,}/, type: "OPENAI_API_KEY", severity: "CRITICAL" },
|
|
1762
|
+
// GitHub
|
|
1763
|
+
{ pattern: /ghp_[a-zA-Z0-9]{36}/, type: "GITHUB_TOKEN", severity: "CRITICAL" },
|
|
1764
|
+
{ pattern: /github_pat_[a-zA-Z0-9_]{22,}/, type: "GITHUB_TOKEN", severity: "CRITICAL" },
|
|
1765
|
+
{ pattern: /gho_[a-zA-Z0-9]{36}/, type: "GITHUB_TOKEN", severity: "CRITICAL" },
|
|
1766
|
+
// GitLab
|
|
1767
|
+
{ pattern: /glpat-[a-zA-Z0-9\-_]{20,}/, type: "GITLAB_TOKEN", severity: "CRITICAL" },
|
|
1768
|
+
// Private keys
|
|
1769
|
+
{ pattern: /-----BEGIN\s+(RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/, type: "PEM_PRIVATE_KEY", severity: "CRITICAL" },
|
|
1770
|
+
// Stripe
|
|
1771
|
+
{ pattern: /[rs]k_(live|test)_[a-zA-Z0-9]{20,}/, type: "STRIPE_KEY", severity: "CRITICAL" },
|
|
1772
|
+
// SendGrid
|
|
1773
|
+
{ pattern: /SG\.[a-zA-Z0-9\-_]{22,}\.[a-zA-Z0-9\-_]{22,}/, type: "SENDGRID_KEY", severity: "CRITICAL" },
|
|
1774
|
+
// Twilio
|
|
1775
|
+
{ pattern: /SK[a-f0-9]{32}/, type: "TWILIO_KEY", severity: "CRITICAL" },
|
|
1776
|
+
// Slack
|
|
1777
|
+
{ pattern: /xox[bpars]-[a-zA-Z0-9\-]{10,}/, type: "SLACK_TOKEN", severity: "HIGH" },
|
|
1778
|
+
{ pattern: /https:\/\/hooks\.slack\.com\/services\/T[a-zA-Z0-9_]+\/B[a-zA-Z0-9_]+\/[a-zA-Z0-9_]+/, type: "SLACK_WEBHOOK", severity: "HIGH" },
|
|
1779
|
+
// Database URLs
|
|
1780
|
+
{ pattern: /(?:postgres|mysql|mongodb|redis|amqp):\/\/[^\s'"]+@[^\s'"]+/, type: "DATABASE_URL", severity: "HIGH" },
|
|
1781
|
+
// Basic auth headers
|
|
1782
|
+
{ pattern: /Authorization:\s*Basic\s+[A-Za-z0-9+/=]{10,}/, type: "BASIC_AUTH_HEADER", severity: "HIGH" },
|
|
1783
|
+
// Env-style password/secret assignments
|
|
1784
|
+
{ pattern: /(?:PASSWORD|SECRET|TOKEN|API_KEY|APIKEY|AUTH_TOKEN|ACCESS_TOKEN)\s*[=:]\s*['"]?[^\s'"]{8,}/i, type: "PASSWORD_IN_ENV", severity: "HIGH" },
|
|
1785
|
+
// JWT tokens
|
|
1786
|
+
{ pattern: /eyJ[a-zA-Z0-9_-]{10,}\.eyJ[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}/, type: "JWT_TOKEN", severity: "MEDIUM" },
|
|
1787
|
+
// Generic API key patterns
|
|
1788
|
+
{ pattern: /(?:api[_-]?key|apikey)\s*[=:]\s*['"]?[a-zA-Z0-9\-_]{16,}/i, type: "GENERIC_API_KEY", severity: "MEDIUM" },
|
|
1789
|
+
// Generic secret patterns
|
|
1790
|
+
{ pattern: /(?:secret|private[_-]?key)\s*[=:]\s*['"]?[a-zA-Z0-9\-_]{16,}/i, type: "GENERIC_SECRET", severity: "MEDIUM" }
|
|
1791
|
+
];
|
|
1792
|
+
function scanForSecrets(content) {
|
|
1793
|
+
if (!content) return { types: [], maxSeverity: "NONE" };
|
|
1794
|
+
const found = /* @__PURE__ */ new Set();
|
|
1795
|
+
let maxSeverity = "NONE";
|
|
1796
|
+
const severityOrder = {
|
|
1797
|
+
NONE: 0,
|
|
1798
|
+
LOW: 1,
|
|
1799
|
+
MEDIUM: 2,
|
|
1800
|
+
HIGH: 3,
|
|
1801
|
+
CRITICAL: 4
|
|
1802
|
+
};
|
|
1803
|
+
for (const { pattern, type, severity } of SECRET_PATTERNS) {
|
|
1804
|
+
if (pattern.test(content)) {
|
|
1805
|
+
found.add(type);
|
|
1806
|
+
if (severityOrder[severity] > severityOrder[maxSeverity]) {
|
|
1807
|
+
maxSeverity = severity;
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
return { types: Array.from(found), maxSeverity };
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
// src/lib/threat-patterns.ts
|
|
1815
|
+
var PROMPT_INJECTION_STRONG = [
|
|
1816
|
+
/\b(?:ignore|disregard|forget|override)\s+(?:all\s+)?(?:previous|prior|above|your)\s+(?:instructions?|rules?|prompts?|guidelines?|constraints?)/i,
|
|
1817
|
+
/\b(?:you\s+(?:are|must|should|will)\s+(?:now|henceforth|from\s+now)\s+(?:be|act|behave|respond)\s+(?:as|like))/i,
|
|
1818
|
+
/\bsystem\s*prompt\b/i,
|
|
1819
|
+
/\bnew\s+instructions?\s*:/i,
|
|
1820
|
+
/\b(?:execute|run|perform)\s+(?:the\s+following|these)\s+(?:commands?|actions?|steps?)\s*:/i,
|
|
1821
|
+
/<!--\s*(?:hidden|invisible|system)\s*(?:instruction|prompt|directive)/i,
|
|
1822
|
+
/\[INST\]|\[\/INST\]|<\|im_start\|>|<\|im_end\|>/,
|
|
1823
|
+
/\bdo\s+not\s+(?:follow|obey|listen\s+to)\s+(?:the|your|any)\s+(?:original|previous|prior)/i
|
|
1824
|
+
];
|
|
1825
|
+
var PROMPT_INJECTION_WEAK = [
|
|
1826
|
+
/\b(?:as\s+an?\s+(?:AI|assistant|language\s+model|LLM|agent))/i,
|
|
1827
|
+
/\b(?:your\s+(?:task|job|role|purpose)\s+is\s+(?:to|now))/i,
|
|
1828
|
+
/\b(?:pretend|imagine|suppose)\s+(?:you\s+are|that\s+you)/i,
|
|
1829
|
+
/\b(?:do\s+not|don'?t)\s+(?:tell|reveal|share|disclose|mention)\s+(?:the|this|any)/i,
|
|
1830
|
+
/\bact\s+as\s+(?:a|an|if)\b/i
|
|
1831
|
+
];
|
|
1832
|
+
var DESTRUCTIVE_CRITICAL = [
|
|
1833
|
+
{ pattern: /rm\s+(-[a-z]*r[a-z]*f|--recursive\s+--force|-[a-z]*f[a-z]*r)\s+\/(?:\s|$)/, label: "rm -rf /" },
|
|
1834
|
+
{ pattern: /mkfs\./, label: "filesystem format" },
|
|
1835
|
+
{ pattern: /dd\s+if=\/dev/, label: "raw disk write (dd)" },
|
|
1836
|
+
{ pattern: /:\(\)\s*\{\s*:\|:\s*&\s*\}\s*;/, label: "fork bomb" },
|
|
1837
|
+
{ pattern: /chmod\s+777\s+\/(?:\s|$)/, label: "chmod 777 /" },
|
|
1838
|
+
{ pattern: />\s*\/dev\/sd[a-z]/, label: "overwrite disk device" }
|
|
1839
|
+
];
|
|
1840
|
+
var DESTRUCTIVE_HIGH = [
|
|
1841
|
+
{ pattern: /rm\s+-[a-z]*r[a-z]*f?\b/, label: "recursive delete (rm -rf)" },
|
|
1842
|
+
{ pattern: /DROP\s+(?:TABLE|DATABASE|SCHEMA)\b/i, label: "SQL DROP" },
|
|
1843
|
+
{ pattern: /TRUNCATE\s+TABLE\b/i, label: "SQL TRUNCATE" },
|
|
1844
|
+
{ pattern: /DELETE\s+FROM\s+\w+\s*(?:;|$)/i, label: "SQL DELETE without WHERE" },
|
|
1845
|
+
{ pattern: /git\s+push\s+.*--force/, label: "git force push" },
|
|
1846
|
+
{ pattern: /git\s+reset\s+--hard/, label: "git hard reset" }
|
|
1847
|
+
];
|
|
1848
|
+
var PRIVILEGE_CRITICAL = [
|
|
1849
|
+
{ pattern: /sudo\s+.*(?:rm|dd|mkfs|chmod\s+777)/, label: "sudo + destructive command" }
|
|
1850
|
+
];
|
|
1851
|
+
var PRIVILEGE_HIGH = [
|
|
1852
|
+
{ pattern: /\bsudo\b/, label: "sudo" },
|
|
1853
|
+
{ pattern: /\busermod\b/, label: "usermod" },
|
|
1854
|
+
{ pattern: /\buseradd\b/, label: "useradd" },
|
|
1855
|
+
{ pattern: /\bpasswd\b/, label: "passwd" },
|
|
1856
|
+
{ pattern: /\bsu\s+-?\s*\w/, label: "su (switch user)" },
|
|
1857
|
+
{ pattern: /chmod\s+[0-7]{3,4}\s+\/(?:etc|usr|sys|boot)/, label: "chmod on system path" },
|
|
1858
|
+
{ pattern: /chown\s+\w+[:\s]\w*\s+\/(?:etc|usr|sys|boot)/, label: "chown on system path" }
|
|
1859
|
+
];
|
|
1860
|
+
var PRIVILEGE_MEDIUM = [
|
|
1861
|
+
{ pattern: /\bchmod\b/, label: "chmod" },
|
|
1862
|
+
{ pattern: /\bchown\b/, label: "chown" }
|
|
1863
|
+
];
|
|
1864
|
+
var SUPPLY_CHAIN_HIGH = [
|
|
1865
|
+
{ pattern: /npm\s+install\s+(?:-g\s+)?\S+(?:\s|$)/, label: "npm install package" },
|
|
1866
|
+
{ pattern: /pip\s+install\s+(?!-r\s+requirements)\S+/, label: "pip install package" },
|
|
1867
|
+
{ pattern: /gem\s+install\s+/, label: "gem install" },
|
|
1868
|
+
{ pattern: /cargo\s+install\s+/, label: "cargo install" },
|
|
1869
|
+
{ pattern: /go\s+install\s+/, label: "go install" },
|
|
1870
|
+
{ pattern: /(?:curl|wget)\s+.*\|\s*(?:sh|bash|zsh|node|python)/, label: "download and execute script" },
|
|
1871
|
+
{ pattern: /npx\s+(?!safeclaw)\S+/, label: "npx execute remote package" }
|
|
1872
|
+
];
|
|
1873
|
+
var SUPPLY_CHAIN_MEDIUM = [
|
|
1874
|
+
{ pattern: /npm\s+install\b/, label: "npm install" },
|
|
1875
|
+
{ pattern: /pip\s+install\s+-r/, label: "pip install from requirements" },
|
|
1876
|
+
{ pattern: /brew\s+install\s+/, label: "brew install" },
|
|
1877
|
+
{ pattern: /apt(?:-get)?\s+install\s+/, label: "apt install" },
|
|
1878
|
+
{ pattern: /yum\s+install\s+/, label: "yum install" }
|
|
1879
|
+
];
|
|
1880
|
+
var EXFILTRATION_URLS = [
|
|
1881
|
+
{ pattern: /pastebin\.com/, label: "pastebin.com" },
|
|
1882
|
+
{ pattern: /paste\.ee/, label: "paste.ee" },
|
|
1883
|
+
{ pattern: /transfer\.sh/, label: "transfer.sh" },
|
|
1884
|
+
{ pattern: /ngrok\.io/, label: "ngrok.io" },
|
|
1885
|
+
{ pattern: /requestbin/, label: "requestbin" },
|
|
1886
|
+
{ pattern: /webhook\.site/, label: "webhook.site" },
|
|
1887
|
+
{ pattern: /pipedream\.net/, label: "pipedream.net" },
|
|
1888
|
+
{ pattern: /hookbin\.com/, label: "hookbin.com" },
|
|
1889
|
+
{ pattern: /beeceptor\.com/, label: "beeceptor.com" },
|
|
1890
|
+
{ pattern: /postb\.in/, label: "postb.in" }
|
|
1891
|
+
];
|
|
1892
|
+
var NETWORK_COMMAND_PATTERNS = [
|
|
1893
|
+
{ pattern: /curl\s+.*-X\s*POST\b/i, label: "curl POST" },
|
|
1894
|
+
{ pattern: /curl\s+.*(?:--data|-d)\s+/i, label: "curl with data" },
|
|
1895
|
+
{ pattern: /wget\s+.*--post-data/i, label: "wget POST" },
|
|
1896
|
+
{ pattern: /nc\s+-.*\d+\.\d+\.\d+\.\d+/, label: "netcat to IP" },
|
|
1897
|
+
{ pattern: /\bssh\s+.*@/, label: "SSH connection" },
|
|
1898
|
+
{ pattern: /\bscp\s+/, label: "SCP file transfer" }
|
|
1899
|
+
];
|
|
1900
|
+
var RAW_IP_URL = /^https?:\/\/\d+\.\d+\.\d+\.\d+/;
|
|
1901
|
+
var CODE_EXFILTRATION_PATTERNS = [
|
|
1902
|
+
{ pattern: /(?:fetch|axios|http|request)\s*\(.*process\.env/s, label: "HTTP request with env vars" },
|
|
1903
|
+
{ pattern: /(?:fetch|axios|http|request)\s*\(.*(?:readFileSync|readFile)/s, label: "HTTP request with file content" },
|
|
1904
|
+
{ pattern: /(?:Buffer|btoa|atob).*(?:fetch|http|request|send)/s, label: "encoded data in HTTP request" }
|
|
1905
|
+
];
|
|
1906
|
+
var OBFUSCATION_PATTERNS = [
|
|
1907
|
+
{ pattern: /eval\s*\(\s*(?:atob|Buffer\.from|decodeURIComponent)\s*\(/, label: "eval with decoded content" },
|
|
1908
|
+
{ pattern: /(?:String\.fromCharCode|\\x[0-9a-f]{2}){5,}/i, label: "character code obfuscation" },
|
|
1909
|
+
{ pattern: /new\s+Function\s*\(\s*['"`].*['"`]\s*\)/, label: "dynamic Function constructor" }
|
|
1910
|
+
];
|
|
1911
|
+
var SENSITIVE_PATH_RULES = [
|
|
1912
|
+
// Auth files — write is CRITICAL, read is HIGH
|
|
1913
|
+
{ pattern: /\/etc\/passwd$/, label: "/etc/passwd", readSeverity: "HIGH", writeSeverity: "CRITICAL" },
|
|
1914
|
+
{ pattern: /\/etc\/shadow$/, label: "/etc/shadow", readSeverity: "HIGH", writeSeverity: "CRITICAL" },
|
|
1915
|
+
{ pattern: /\/etc\/sudoers/, label: "/etc/sudoers", readSeverity: "HIGH", writeSeverity: "CRITICAL" },
|
|
1916
|
+
{ pattern: /\.ssh\/authorized_keys$/, label: ".ssh/authorized_keys", readSeverity: "HIGH", writeSeverity: "CRITICAL" },
|
|
1917
|
+
// Credential directories
|
|
1918
|
+
{ pattern: /\.ssh\//, label: ".ssh directory", readSeverity: "HIGH", writeSeverity: "HIGH" },
|
|
1919
|
+
{ pattern: /\.aws\//, label: ".aws directory", readSeverity: "HIGH", writeSeverity: "HIGH" },
|
|
1920
|
+
{ pattern: /\.gnupg\//, label: ".gnupg directory", readSeverity: "HIGH", writeSeverity: "HIGH" },
|
|
1921
|
+
{ pattern: /\.gcloud\//, label: ".gcloud directory", readSeverity: "HIGH", writeSeverity: "HIGH" },
|
|
1922
|
+
// Credential files
|
|
1923
|
+
{ pattern: /\.env$/, label: ".env file", readSeverity: "HIGH", writeSeverity: "CRITICAL" },
|
|
1924
|
+
{ pattern: /\.env\.\w+$/, label: ".env.* file", readSeverity: "HIGH", writeSeverity: "HIGH" },
|
|
1925
|
+
{ pattern: /\.(pem|key|p12|pfx)$/i, label: "key/certificate file", readSeverity: "HIGH", writeSeverity: "CRITICAL" },
|
|
1926
|
+
{ pattern: /id_rsa$|id_ed25519$|id_ecdsa$/, label: "SSH private key", readSeverity: "HIGH", writeSeverity: "CRITICAL" },
|
|
1927
|
+
{ pattern: /\.keychain$/, label: "keychain file", readSeverity: "HIGH", writeSeverity: "HIGH" },
|
|
1928
|
+
{ pattern: /\.(pass|secret|token)$/i, label: "credentials file", readSeverity: "HIGH", writeSeverity: "HIGH" }
|
|
1929
|
+
];
|
|
1930
|
+
var SYSTEM_PATH_RULES = [
|
|
1931
|
+
// Critical system paths (write only — reads are TC-SFA)
|
|
1932
|
+
{ pattern: /^\/etc\//, label: "/etc/ system config", severity: "HIGH" },
|
|
1933
|
+
{ pattern: /^\/usr\/bin\//, label: "/usr/bin/ system binaries", severity: "HIGH" },
|
|
1934
|
+
{ pattern: /^\/usr\/sbin\//, label: "/usr/sbin/ system admin binaries", severity: "HIGH" },
|
|
1935
|
+
{ pattern: /^\/usr\/local\/bin\//, label: "/usr/local/bin/ local binaries", severity: "HIGH" },
|
|
1936
|
+
{ pattern: /^\/boot\//, label: "/boot/ boot configuration", severity: "CRITICAL" },
|
|
1937
|
+
{ pattern: /^\/System\//, label: "/System/ macOS system", severity: "CRITICAL" },
|
|
1938
|
+
{ pattern: /^\/Library\/Launch/, label: "macOS LaunchDaemons/Agents", severity: "CRITICAL" },
|
|
1939
|
+
// Config files
|
|
1940
|
+
{ pattern: /\/\.bashrc$|\/\.bash_profile$/, label: ".bashrc/.bash_profile", severity: "MEDIUM" },
|
|
1941
|
+
{ pattern: /\/\.zshrc$|\/\.zprofile$/, label: ".zshrc/.zprofile", severity: "MEDIUM" },
|
|
1942
|
+
{ pattern: /\/\.npmrc$/, label: ".npmrc", severity: "MEDIUM" },
|
|
1943
|
+
{ pattern: /\/\.gitconfig$/, label: ".gitconfig", severity: "MEDIUM" }
|
|
1944
|
+
];
|
|
1945
|
+
var DEPENDENCY_FILES = [
|
|
1946
|
+
/package\.json$/,
|
|
1947
|
+
/requirements\.txt$/,
|
|
1948
|
+
/Gemfile$/,
|
|
1949
|
+
/go\.mod$/,
|
|
1950
|
+
/Cargo\.toml$/,
|
|
1951
|
+
/pom\.xml$/,
|
|
1952
|
+
/build\.gradle$/,
|
|
1953
|
+
/pyproject\.toml$/,
|
|
1954
|
+
/composer\.json$/
|
|
1955
|
+
];
|
|
1956
|
+
var BUILD_CI_FILES = [
|
|
1957
|
+
/\.github\/workflows\//,
|
|
1958
|
+
/\.gitlab-ci\.yml$/,
|
|
1959
|
+
/Jenkinsfile$/,
|
|
1960
|
+
/Makefile$/,
|
|
1961
|
+
/Dockerfile$/,
|
|
1962
|
+
/docker-compose\.ya?ml$/,
|
|
1963
|
+
/\.circleci\//
|
|
1964
|
+
];
|
|
1965
|
+
|
|
1966
|
+
// src/lib/threat-classifier.ts
|
|
1967
|
+
var SEVERITY_ORDER = {
|
|
1968
|
+
NONE: 0,
|
|
1969
|
+
LOW: 1,
|
|
1970
|
+
MEDIUM: 2,
|
|
1971
|
+
HIGH: 3,
|
|
1972
|
+
CRITICAL: 4
|
|
1973
|
+
};
|
|
1974
|
+
function maxThreat(a, b) {
|
|
1975
|
+
return SEVERITY_ORDER[a] >= SEVERITY_ORDER[b] ? a : b;
|
|
1976
|
+
}
|
|
1977
|
+
function finding(categoryId, categoryName, severity, reason, evidence, owaspRef) {
|
|
1978
|
+
const f = { categoryId, categoryName, severity, reason };
|
|
1979
|
+
if (evidence) f.evidence = evidence;
|
|
1980
|
+
if (owaspRef) f.owaspRef = owaspRef;
|
|
1981
|
+
return f;
|
|
1982
|
+
}
|
|
1983
|
+
function classifyActivity(input) {
|
|
1984
|
+
const findings = [];
|
|
1985
|
+
findings.push(...analyzeSecretExposure(input));
|
|
1986
|
+
findings.push(...analyzeDataExfiltration(input));
|
|
1987
|
+
findings.push(...analyzePromptInjection(input));
|
|
1988
|
+
findings.push(...analyzeDestructiveOps(input));
|
|
1989
|
+
findings.push(...analyzePrivilegeEscalation(input));
|
|
1990
|
+
findings.push(...analyzeSupplyChain(input));
|
|
1991
|
+
findings.push(...analyzeSensitiveFileAccess(input));
|
|
1992
|
+
findings.push(...analyzeSystemModification(input));
|
|
1993
|
+
findings.push(...analyzeNetworkActivity(input));
|
|
1994
|
+
findings.push(...analyzeMcpToolPoisoning(input));
|
|
1995
|
+
const threatLevel = findings.length > 0 ? findings.reduce((max, f) => maxThreat(max, f.severity), "NONE") : "NONE";
|
|
1996
|
+
const secretTypes = findings.filter((f) => f.categoryId === "TC-SEC" && f.evidence).map((f) => f.evidence);
|
|
1997
|
+
const secretsDetected = secretTypes.length > 0 ? secretTypes : null;
|
|
1998
|
+
return { threatLevel, findings, secretsDetected };
|
|
1999
|
+
}
|
|
2000
|
+
function analyzeSecretExposure(input) {
|
|
2001
|
+
const results = [];
|
|
2002
|
+
const consumesContent = ["file_read", "shell_command", "web_browse", "tool_call"].includes(input.activityType);
|
|
2003
|
+
const writesContent = input.activityType === "file_write";
|
|
2004
|
+
if (input.contentPreview) {
|
|
2005
|
+
const scan = scanForSecrets(input.contentPreview);
|
|
2006
|
+
for (const secretType of scan.types) {
|
|
2007
|
+
if (consumesContent) {
|
|
2008
|
+
results.push(finding(
|
|
2009
|
+
"TC-SEC",
|
|
2010
|
+
"Secret Exposure",
|
|
2011
|
+
scan.maxSeverity,
|
|
2012
|
+
`Content contains ${secretType} \u2014 sent to model provider cloud as part of agent context`,
|
|
2013
|
+
secretType,
|
|
2014
|
+
"LLM02"
|
|
2015
|
+
));
|
|
2016
|
+
} else if (writesContent) {
|
|
2017
|
+
results.push(finding(
|
|
2018
|
+
"TC-SEC",
|
|
2019
|
+
"Secret Exposure",
|
|
2020
|
+
SEVERITY_ORDER[scan.maxSeverity] >= SEVERITY_ORDER["HIGH"] ? "HIGH" : scan.maxSeverity,
|
|
2021
|
+
`Writing content containing ${secretType} \u2014 creates credential leak risk`,
|
|
2022
|
+
secretType,
|
|
2023
|
+
"LLM02"
|
|
2024
|
+
));
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
if (writesContent && input.readContentPreview) {
|
|
2029
|
+
const scan = scanForSecrets(input.readContentPreview);
|
|
2030
|
+
for (const secretType of scan.types) {
|
|
2031
|
+
if (!results.some((r) => r.evidence === secretType)) {
|
|
2032
|
+
results.push(finding(
|
|
2033
|
+
"TC-SEC",
|
|
2034
|
+
"Secret Exposure",
|
|
2035
|
+
scan.maxSeverity,
|
|
2036
|
+
`File being edited contains ${secretType} \u2014 original content was in agent context`,
|
|
2037
|
+
secretType,
|
|
2038
|
+
"LLM02"
|
|
2039
|
+
));
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
return results;
|
|
2044
|
+
}
|
|
2045
|
+
function analyzeDataExfiltration(input) {
|
|
2046
|
+
const results = [];
|
|
2047
|
+
if (input.activityType === "shell_command") {
|
|
2048
|
+
const cmd = input.detail;
|
|
2049
|
+
for (const { pattern, label } of EXFILTRATION_URLS) {
|
|
2050
|
+
if (pattern.test(cmd)) {
|
|
2051
|
+
results.push(finding(
|
|
2052
|
+
"TC-EXF",
|
|
2053
|
+
"Data Exfiltration",
|
|
2054
|
+
"HIGH",
|
|
2055
|
+
`Command targets known exfiltration service: ${label}`,
|
|
2056
|
+
label,
|
|
2057
|
+
"LLM02"
|
|
2058
|
+
));
|
|
2059
|
+
break;
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
for (const { pattern, label } of NETWORK_COMMAND_PATTERNS) {
|
|
2063
|
+
if (pattern.test(cmd) && /(?:--data|-d\s|--post-data)/.test(cmd)) {
|
|
2064
|
+
results.push(finding(
|
|
2065
|
+
"TC-EXF",
|
|
2066
|
+
"Data Exfiltration",
|
|
2067
|
+
"MEDIUM",
|
|
2068
|
+
`Outbound data transfer via ${label}`,
|
|
2069
|
+
label,
|
|
2070
|
+
"LLM02"
|
|
2071
|
+
));
|
|
2072
|
+
break;
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
if (input.activityType === "web_browse" && input.targetPath) {
|
|
2077
|
+
for (const { pattern, label } of EXFILTRATION_URLS) {
|
|
2078
|
+
if (pattern.test(input.targetPath)) {
|
|
2079
|
+
results.push(finding(
|
|
2080
|
+
"TC-EXF",
|
|
2081
|
+
"Data Exfiltration",
|
|
2082
|
+
"HIGH",
|
|
2083
|
+
`Browsing known exfiltration service: ${label}`,
|
|
2084
|
+
label,
|
|
2085
|
+
"LLM02"
|
|
2086
|
+
));
|
|
2087
|
+
break;
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
if (input.activityType === "message" && input.contentPreview) {
|
|
2092
|
+
const scan = scanForSecrets(input.contentPreview);
|
|
2093
|
+
if (scan.types.length > 0) {
|
|
2094
|
+
results.push(finding(
|
|
2095
|
+
"TC-EXF",
|
|
2096
|
+
"Data Exfiltration",
|
|
2097
|
+
"CRITICAL",
|
|
2098
|
+
`Message contains secrets (${scan.types.join(", ")}) being sent externally`,
|
|
2099
|
+
scan.types.join(", "),
|
|
2100
|
+
"LLM02"
|
|
2101
|
+
));
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
if (input.activityType === "file_write" && input.contentPreview) {
|
|
2105
|
+
for (const { pattern, label } of CODE_EXFILTRATION_PATTERNS) {
|
|
2106
|
+
if (pattern.test(input.contentPreview)) {
|
|
2107
|
+
results.push(finding(
|
|
2108
|
+
"TC-EXF",
|
|
2109
|
+
"Data Exfiltration",
|
|
2110
|
+
"HIGH",
|
|
2111
|
+
`Written code contains data exfiltration pattern: ${label}`,
|
|
2112
|
+
label,
|
|
2113
|
+
"LLM02"
|
|
2114
|
+
));
|
|
2115
|
+
break;
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
for (const { pattern, label } of OBFUSCATION_PATTERNS) {
|
|
2119
|
+
if (pattern.test(input.contentPreview)) {
|
|
2120
|
+
results.push(finding(
|
|
2121
|
+
"TC-EXF",
|
|
2122
|
+
"Data Exfiltration",
|
|
2123
|
+
"HIGH",
|
|
2124
|
+
`Written code contains obfuscation pattern: ${label}`,
|
|
2125
|
+
label,
|
|
2126
|
+
"LLM05"
|
|
2127
|
+
));
|
|
2128
|
+
break;
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
return results;
|
|
2133
|
+
}
|
|
2134
|
+
function analyzePromptInjection(input) {
|
|
2135
|
+
const results = [];
|
|
2136
|
+
if (!["file_read", "web_browse", "shell_command", "tool_call"].includes(input.activityType)) {
|
|
2137
|
+
return results;
|
|
2138
|
+
}
|
|
2139
|
+
if (!input.contentPreview) return results;
|
|
2140
|
+
for (const pattern of PROMPT_INJECTION_STRONG) {
|
|
2141
|
+
const match = input.contentPreview.match(pattern);
|
|
2142
|
+
if (match) {
|
|
2143
|
+
const source = input.activityType === "web_browse" ? "Web page" : input.activityType === "file_read" ? "File" : input.activityType === "shell_command" ? "Command output" : "Content";
|
|
2144
|
+
results.push(finding(
|
|
2145
|
+
"TC-INJ",
|
|
2146
|
+
"Prompt Injection Risk",
|
|
2147
|
+
"HIGH",
|
|
2148
|
+
`${source} contains potential prompt injection: "${match[0].slice(0, 80)}"`,
|
|
2149
|
+
match[0].slice(0, 100),
|
|
2150
|
+
"LLM01"
|
|
2151
|
+
));
|
|
2152
|
+
break;
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
if (results.length === 0 && (input.activityType === "web_browse" || input.activityType === "tool_call")) {
|
|
2156
|
+
for (const pattern of PROMPT_INJECTION_WEAK) {
|
|
2157
|
+
const match = input.contentPreview.match(pattern);
|
|
2158
|
+
if (match) {
|
|
2159
|
+
results.push(finding(
|
|
2160
|
+
"TC-INJ",
|
|
2161
|
+
"Prompt Injection Risk",
|
|
2162
|
+
"MEDIUM",
|
|
2163
|
+
`External content contains instruction-like pattern: "${match[0].slice(0, 80)}"`,
|
|
2164
|
+
match[0].slice(0, 100),
|
|
2165
|
+
"LLM01"
|
|
2166
|
+
));
|
|
2167
|
+
break;
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
return results;
|
|
2172
|
+
}
|
|
2173
|
+
function analyzeDestructiveOps(input) {
|
|
2174
|
+
const results = [];
|
|
2175
|
+
if (input.activityType === "shell_command") {
|
|
2176
|
+
const cmd = input.detail;
|
|
2177
|
+
for (const { pattern, label } of DESTRUCTIVE_CRITICAL) {
|
|
2178
|
+
if (pattern.test(cmd)) {
|
|
2179
|
+
results.push(finding(
|
|
2180
|
+
"TC-DES",
|
|
2181
|
+
"Destructive Operation",
|
|
2182
|
+
"CRITICAL",
|
|
2183
|
+
`Destructive system command: ${label}`,
|
|
2184
|
+
label,
|
|
2185
|
+
"LLM06"
|
|
2186
|
+
));
|
|
2187
|
+
return results;
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
for (const { pattern, label } of DESTRUCTIVE_HIGH) {
|
|
2191
|
+
if (pattern.test(cmd)) {
|
|
2192
|
+
results.push(finding(
|
|
2193
|
+
"TC-DES",
|
|
2194
|
+
"Destructive Operation",
|
|
2195
|
+
"HIGH",
|
|
2196
|
+
`Potentially destructive command: ${label}`,
|
|
2197
|
+
label,
|
|
2198
|
+
"LLM06"
|
|
2199
|
+
));
|
|
2200
|
+
break;
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
if (input.activityType === "file_write" && input.contentPreview) {
|
|
2205
|
+
if (/DROP\s+(?:TABLE|DATABASE|SCHEMA)\b/i.test(input.contentPreview)) {
|
|
2206
|
+
results.push(finding(
|
|
2207
|
+
"TC-DES",
|
|
2208
|
+
"Destructive Operation",
|
|
2209
|
+
"MEDIUM",
|
|
2210
|
+
"File contains destructive SQL statements (DROP)",
|
|
2211
|
+
"DROP TABLE/DATABASE",
|
|
2212
|
+
"LLM06"
|
|
2213
|
+
));
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
return results;
|
|
2217
|
+
}
|
|
2218
|
+
function analyzePrivilegeEscalation(input) {
|
|
2219
|
+
const results = [];
|
|
2220
|
+
if (input.activityType !== "shell_command") return results;
|
|
2221
|
+
const cmd = input.detail;
|
|
2222
|
+
for (const { pattern, label } of PRIVILEGE_CRITICAL) {
|
|
2223
|
+
if (pattern.test(cmd)) {
|
|
2224
|
+
results.push(finding(
|
|
2225
|
+
"TC-ESC",
|
|
2226
|
+
"Privilege Escalation",
|
|
2227
|
+
"CRITICAL",
|
|
2228
|
+
`Elevated privilege with destructive command: ${label}`,
|
|
2229
|
+
label,
|
|
2230
|
+
"LLM06"
|
|
2231
|
+
));
|
|
2232
|
+
return results;
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
for (const { pattern, label } of PRIVILEGE_HIGH) {
|
|
2236
|
+
if (pattern.test(cmd)) {
|
|
2237
|
+
results.push(finding(
|
|
2238
|
+
"TC-ESC",
|
|
2239
|
+
"Privilege Escalation",
|
|
2240
|
+
"HIGH",
|
|
2241
|
+
`Privilege escalation: ${label}`,
|
|
2242
|
+
label,
|
|
2243
|
+
"LLM06"
|
|
2244
|
+
));
|
|
2245
|
+
break;
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
if (results.length === 0) {
|
|
2249
|
+
for (const { pattern, label } of PRIVILEGE_MEDIUM) {
|
|
2250
|
+
if (pattern.test(cmd)) {
|
|
2251
|
+
results.push(finding(
|
|
2252
|
+
"TC-ESC",
|
|
2253
|
+
"Privilege Escalation",
|
|
2254
|
+
"MEDIUM",
|
|
2255
|
+
`Permission modification: ${label}`,
|
|
2256
|
+
label,
|
|
2257
|
+
"LLM06"
|
|
2258
|
+
));
|
|
2259
|
+
break;
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
return results;
|
|
2264
|
+
}
|
|
2265
|
+
function analyzeSupplyChain(input) {
|
|
2266
|
+
const results = [];
|
|
2267
|
+
if (input.activityType === "shell_command") {
|
|
2268
|
+
const cmd = input.detail;
|
|
2269
|
+
for (const { pattern, label } of SUPPLY_CHAIN_HIGH) {
|
|
2270
|
+
if (pattern.test(cmd)) {
|
|
2271
|
+
results.push(finding(
|
|
2272
|
+
"TC-SUP",
|
|
2273
|
+
"Supply Chain Risk",
|
|
2274
|
+
"HIGH",
|
|
2275
|
+
`Package installation or remote execution: ${label}`,
|
|
2276
|
+
label,
|
|
2277
|
+
"LLM03"
|
|
2278
|
+
));
|
|
2279
|
+
return results;
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
for (const { pattern, label } of SUPPLY_CHAIN_MEDIUM) {
|
|
2283
|
+
if (pattern.test(cmd)) {
|
|
2284
|
+
results.push(finding(
|
|
2285
|
+
"TC-SUP",
|
|
2286
|
+
"Supply Chain Risk",
|
|
2287
|
+
"MEDIUM",
|
|
2288
|
+
`Package management operation: ${label}`,
|
|
2289
|
+
label,
|
|
2290
|
+
"LLM03"
|
|
2291
|
+
));
|
|
2292
|
+
break;
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
if (input.activityType === "file_write" && input.targetPath) {
|
|
2297
|
+
for (const pattern of DEPENDENCY_FILES) {
|
|
2298
|
+
if (pattern.test(input.targetPath)) {
|
|
2299
|
+
results.push(finding(
|
|
2300
|
+
"TC-SUP",
|
|
2301
|
+
"Supply Chain Risk",
|
|
2302
|
+
"MEDIUM",
|
|
2303
|
+
`Modifying dependency file: ${input.targetPath.split("/").pop()}`,
|
|
2304
|
+
input.targetPath,
|
|
2305
|
+
"LLM03"
|
|
2306
|
+
));
|
|
2307
|
+
break;
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
for (const pattern of BUILD_CI_FILES) {
|
|
2311
|
+
if (pattern.test(input.targetPath)) {
|
|
2312
|
+
results.push(finding(
|
|
2313
|
+
"TC-SUP",
|
|
2314
|
+
"Supply Chain Risk",
|
|
2315
|
+
"MEDIUM",
|
|
2316
|
+
`Modifying build/CI configuration: ${input.targetPath.split("/").pop()}`,
|
|
2317
|
+
input.targetPath,
|
|
2318
|
+
"LLM03"
|
|
2319
|
+
));
|
|
2320
|
+
break;
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
return results;
|
|
2325
|
+
}
|
|
2326
|
+
function analyzeSensitiveFileAccess(input) {
|
|
2327
|
+
const results = [];
|
|
2328
|
+
if (!["file_read", "file_write"].includes(input.activityType)) return results;
|
|
2329
|
+
if (!input.targetPath) return results;
|
|
2330
|
+
const isWrite = input.activityType === "file_write";
|
|
2331
|
+
for (const rule of SENSITIVE_PATH_RULES) {
|
|
2332
|
+
if (rule.pattern.test(input.targetPath)) {
|
|
2333
|
+
const severity = isWrite ? rule.writeSeverity : rule.readSeverity;
|
|
2334
|
+
const verb = isWrite ? "Writing to" : "Reading";
|
|
2335
|
+
results.push(finding(
|
|
2336
|
+
"TC-SFA",
|
|
2337
|
+
"Sensitive File Access",
|
|
2338
|
+
severity,
|
|
2339
|
+
`${verb} sensitive path: ${rule.label}`,
|
|
2340
|
+
input.targetPath,
|
|
2341
|
+
"LLM02"
|
|
2342
|
+
));
|
|
2343
|
+
break;
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
return results;
|
|
2347
|
+
}
|
|
2348
|
+
function analyzeSystemModification(input) {
|
|
2349
|
+
const results = [];
|
|
2350
|
+
if (input.activityType !== "file_write") return results;
|
|
2351
|
+
if (!input.targetPath) return results;
|
|
2352
|
+
for (const rule of SYSTEM_PATH_RULES) {
|
|
2353
|
+
if (rule.pattern.test(input.targetPath)) {
|
|
2354
|
+
results.push(finding(
|
|
2355
|
+
"TC-SYS",
|
|
2356
|
+
"System Modification",
|
|
2357
|
+
rule.severity,
|
|
2358
|
+
`Modifying system path: ${rule.label}`,
|
|
2359
|
+
input.targetPath,
|
|
2360
|
+
"LLM06"
|
|
2361
|
+
));
|
|
2362
|
+
break;
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
return results;
|
|
2366
|
+
}
|
|
2367
|
+
function analyzeNetworkActivity(input) {
|
|
2368
|
+
const results = [];
|
|
2369
|
+
if (input.activityType === "web_browse" && input.targetPath) {
|
|
2370
|
+
if (RAW_IP_URL.test(input.targetPath)) {
|
|
2371
|
+
results.push(finding(
|
|
2372
|
+
"TC-NET",
|
|
2373
|
+
"Suspicious Network",
|
|
2374
|
+
"MEDIUM",
|
|
2375
|
+
`Browsing raw IP address instead of domain name`,
|
|
2376
|
+
input.targetPath
|
|
2377
|
+
));
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
if (input.activityType === "shell_command") {
|
|
2381
|
+
const cmd = input.detail;
|
|
2382
|
+
for (const { pattern, label } of NETWORK_COMMAND_PATTERNS) {
|
|
2383
|
+
if (pattern.test(cmd)) {
|
|
2384
|
+
const hasExfilUrl = EXFILTRATION_URLS.some((e) => e.pattern.test(cmd));
|
|
2385
|
+
if (!hasExfilUrl) {
|
|
2386
|
+
results.push(finding(
|
|
2387
|
+
"TC-NET",
|
|
2388
|
+
"Suspicious Network",
|
|
2389
|
+
"MEDIUM",
|
|
2390
|
+
`Network operation: ${label}`,
|
|
2391
|
+
label
|
|
2392
|
+
));
|
|
2393
|
+
break;
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
if (input.activityType === "message") {
|
|
2399
|
+
results.push(finding(
|
|
2400
|
+
"TC-NET",
|
|
2401
|
+
"Suspicious Network",
|
|
2402
|
+
"LOW",
|
|
2403
|
+
"External message sent",
|
|
2404
|
+
input.targetPath ?? void 0
|
|
2405
|
+
));
|
|
2406
|
+
}
|
|
2407
|
+
return results;
|
|
2408
|
+
}
|
|
2409
|
+
function analyzeMcpToolPoisoning(input) {
|
|
2410
|
+
const results = [];
|
|
2411
|
+
if (input.activityType !== "tool_call") return results;
|
|
2412
|
+
if (input.contentPreview) {
|
|
2413
|
+
for (const pattern of PROMPT_INJECTION_STRONG) {
|
|
2414
|
+
if (pattern.test(input.contentPreview)) {
|
|
2415
|
+
results.push(finding(
|
|
2416
|
+
"TC-MCP",
|
|
2417
|
+
"MCP/Tool Poisoning",
|
|
2418
|
+
"HIGH",
|
|
2419
|
+
`Tool response contains agent-directive content that may manipulate behavior`,
|
|
2420
|
+
input.toolName ?? void 0,
|
|
2421
|
+
"LLM01"
|
|
2422
|
+
));
|
|
2423
|
+
break;
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
return results;
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
// src/interceptor.ts
|
|
2431
|
+
function analyzeActivityThreat(activityType, detail, targetPath, contentPreview, readContentPreview, toolName) {
|
|
2432
|
+
const classification = classifyActivity({
|
|
2433
|
+
activityType,
|
|
2434
|
+
detail,
|
|
2435
|
+
targetPath,
|
|
2436
|
+
contentPreview: contentPreview ?? null,
|
|
2437
|
+
readContentPreview: readContentPreview ?? null,
|
|
2438
|
+
toolName: toolName ?? null
|
|
2439
|
+
});
|
|
2440
|
+
return {
|
|
2441
|
+
threatLevel: classification.threatLevel,
|
|
2442
|
+
secretsDetected: classification.secretsDetected,
|
|
2443
|
+
findings: classification.findings
|
|
2444
|
+
};
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
// src/services/openclaw-monitor.ts
|
|
2448
|
+
import { eq as eq2, ne, desc as desc2, and, sql as sql3 } from "drizzle-orm";
|
|
2449
|
+
var instance2 = null;
|
|
2450
|
+
var OpenClawMonitor = class {
|
|
2451
|
+
client;
|
|
2452
|
+
sessionWatcher;
|
|
2453
|
+
execApprovalService;
|
|
2454
|
+
io;
|
|
2455
|
+
lastEventAt = null;
|
|
2456
|
+
constructor(io2) {
|
|
2457
|
+
this.io = io2;
|
|
2458
|
+
this.client = new OpenClawClient();
|
|
2459
|
+
this.sessionWatcher = new SessionWatcher();
|
|
2460
|
+
this.execApprovalService = createExecApprovalService(this.client, io2);
|
|
2461
|
+
this.setupListeners();
|
|
2462
|
+
ensureDefaults();
|
|
2463
|
+
}
|
|
2464
|
+
setupListeners() {
|
|
2465
|
+
this.client.on("activity", (parsed) => {
|
|
2466
|
+
this.handleActivity({
|
|
2467
|
+
openclawSessionId: parsed.openclawSessionId,
|
|
2468
|
+
activityType: parsed.activityType,
|
|
2469
|
+
detail: parsed.detail,
|
|
2470
|
+
rawPayload: parsed.rawPayload,
|
|
2471
|
+
toolName: parsed.toolName,
|
|
2472
|
+
targetPath: parsed.targetPath,
|
|
2473
|
+
timestamp: parsed.timestamp,
|
|
2474
|
+
runId: parsed.runId ?? null,
|
|
2475
|
+
contentPreview: null,
|
|
2476
|
+
readContentPreview: null
|
|
2477
|
+
}).catch(
|
|
2478
|
+
(err) => logger.error({ err }, "Failed to handle OpenClaw activity")
|
|
2479
|
+
);
|
|
2480
|
+
});
|
|
2481
|
+
this.client.on("sessionStart", (sessionId, model) => {
|
|
2482
|
+
this.handleSessionStart(sessionId, model).catch(
|
|
2483
|
+
(err) => logger.error({ err }, "Failed to handle OpenClaw session start")
|
|
2484
|
+
);
|
|
2485
|
+
});
|
|
2486
|
+
this.client.on("sessionEnd", (sessionId) => {
|
|
2487
|
+
this.handleSessionEnd(sessionId).catch(
|
|
2488
|
+
(err) => logger.error({ err }, "Failed to handle OpenClaw session end")
|
|
2489
|
+
);
|
|
2490
|
+
});
|
|
2491
|
+
this.client.on("statusChange", (_status) => {
|
|
2492
|
+
this.broadcastStatus();
|
|
2493
|
+
});
|
|
2494
|
+
this.client.on("execApproval", (request) => {
|
|
2495
|
+
this.execApprovalService.handleRequest(request);
|
|
2496
|
+
});
|
|
2497
|
+
this.sessionWatcher.on("activity", (activity) => {
|
|
2498
|
+
this.handleActivity(activity).catch(
|
|
2499
|
+
(err) => logger.error({ err }, "Failed to handle session file activity")
|
|
2500
|
+
);
|
|
2501
|
+
});
|
|
2502
|
+
}
|
|
2503
|
+
async handleActivity(parsed) {
|
|
2504
|
+
const { threatLevel, secretsDetected, findings } = analyzeActivityThreat(
|
|
2505
|
+
parsed.activityType,
|
|
2506
|
+
parsed.detail,
|
|
2507
|
+
parsed.targetPath,
|
|
2508
|
+
parsed.contentPreview,
|
|
2509
|
+
parsed.readContentPreview,
|
|
2510
|
+
parsed.toolName
|
|
2511
|
+
);
|
|
2512
|
+
const db = getDb();
|
|
2513
|
+
const existingSession = await db.select().from(schema_exports.openclawSessions).where(eq2(schema_exports.openclawSessions.id, parsed.openclawSessionId)).limit(1);
|
|
2514
|
+
if (existingSession.length === 0) {
|
|
2515
|
+
await db.insert(schema_exports.openclawSessions).values({
|
|
2516
|
+
id: parsed.openclawSessionId,
|
|
2517
|
+
status: "ACTIVE"
|
|
2518
|
+
});
|
|
2519
|
+
}
|
|
2520
|
+
const secretsJson = secretsDetected ? JSON.stringify(secretsDetected) : null;
|
|
2521
|
+
const findingsJson = findings.length > 0 ? JSON.stringify(findings) : null;
|
|
2522
|
+
const [inserted] = await db.insert(schema_exports.agentActivities).values({
|
|
2523
|
+
openclawSessionId: parsed.openclawSessionId,
|
|
2524
|
+
activityType: parsed.activityType,
|
|
2525
|
+
detail: parsed.detail,
|
|
2526
|
+
rawPayload: parsed.rawPayload,
|
|
2527
|
+
threatLevel,
|
|
2528
|
+
timestamp: parsed.timestamp,
|
|
2529
|
+
toolName: parsed.toolName,
|
|
2530
|
+
targetPath: parsed.targetPath,
|
|
2531
|
+
runId: parsed.runId,
|
|
2532
|
+
contentPreview: parsed.contentPreview,
|
|
2533
|
+
readContentPreview: parsed.readContentPreview,
|
|
2534
|
+
secretsDetected: secretsJson,
|
|
2535
|
+
threatFindings: findingsJson
|
|
2536
|
+
}).returning();
|
|
2537
|
+
this.lastEventAt = parsed.timestamp;
|
|
2538
|
+
let parsedSecrets = null;
|
|
2539
|
+
if (inserted.secretsDetected) {
|
|
2540
|
+
try {
|
|
2541
|
+
parsedSecrets = JSON.parse(inserted.secretsDetected);
|
|
2542
|
+
} catch {
|
|
2543
|
+
parsedSecrets = null;
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
let parsedFindings = null;
|
|
2547
|
+
if (inserted.threatFindings) {
|
|
2548
|
+
try {
|
|
2549
|
+
parsedFindings = JSON.parse(inserted.threatFindings);
|
|
2550
|
+
} catch {
|
|
2551
|
+
parsedFindings = null;
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
const activity = {
|
|
2555
|
+
id: inserted.id,
|
|
2556
|
+
openclawSessionId: inserted.openclawSessionId,
|
|
2557
|
+
activityType: inserted.activityType,
|
|
2558
|
+
detail: inserted.detail,
|
|
2559
|
+
rawPayload: inserted.rawPayload,
|
|
2560
|
+
threatLevel: inserted.threatLevel,
|
|
2561
|
+
timestamp: inserted.timestamp,
|
|
2562
|
+
toolName: inserted.toolName,
|
|
2563
|
+
targetPath: inserted.targetPath,
|
|
2564
|
+
runId: inserted.runId,
|
|
2565
|
+
contentPreview: inserted.contentPreview,
|
|
2566
|
+
readContentPreview: inserted.readContentPreview,
|
|
2567
|
+
secretsDetected: parsedSecrets,
|
|
2568
|
+
threatFindings: parsedFindings,
|
|
2569
|
+
resolved: false,
|
|
2570
|
+
resolvedAt: null
|
|
2571
|
+
};
|
|
2572
|
+
this.io.emit("safeclaw:openclawActivity", activity);
|
|
2573
|
+
if (threatLevel === "HIGH" || threatLevel === "CRITICAL") {
|
|
2574
|
+
this.io.emit("safeclaw:alert", {
|
|
2575
|
+
command: parsed.detail,
|
|
2576
|
+
threatLevel,
|
|
2577
|
+
id: inserted.id
|
|
2578
|
+
});
|
|
2579
|
+
}
|
|
2580
|
+
if (secretsDetected && secretsDetected.length > 0) {
|
|
2581
|
+
logger.warn(
|
|
2582
|
+
{
|
|
2583
|
+
sessionId: parsed.openclawSessionId,
|
|
2584
|
+
secrets: secretsDetected,
|
|
2585
|
+
threat: threatLevel
|
|
2586
|
+
},
|
|
2587
|
+
`Secrets detected in agent activity: ${secretsDetected.join(", ")}`
|
|
2588
|
+
);
|
|
2589
|
+
}
|
|
2590
|
+
logger.info(
|
|
2591
|
+
{
|
|
2592
|
+
sessionId: parsed.openclawSessionId,
|
|
2593
|
+
type: parsed.activityType,
|
|
2594
|
+
threat: threatLevel,
|
|
2595
|
+
runId: parsed.runId
|
|
2596
|
+
},
|
|
2597
|
+
`OpenClaw activity: ${parsed.detail}`
|
|
2598
|
+
);
|
|
2599
|
+
}
|
|
2600
|
+
async handleSessionStart(sessionId, model) {
|
|
2601
|
+
const db = getDb();
|
|
2602
|
+
const existing = await db.select().from(schema_exports.openclawSessions).where(eq2(schema_exports.openclawSessions.id, sessionId)).limit(1);
|
|
2603
|
+
if (existing.length === 0) {
|
|
2604
|
+
await db.insert(schema_exports.openclawSessions).values({
|
|
2605
|
+
id: sessionId,
|
|
2606
|
+
status: "ACTIVE",
|
|
2607
|
+
model: model ?? null
|
|
2608
|
+
});
|
|
2609
|
+
} else {
|
|
2610
|
+
await db.update(schema_exports.openclawSessions).set({ status: "ACTIVE", model: model ?? null }).where(eq2(schema_exports.openclawSessions.id, sessionId));
|
|
2611
|
+
}
|
|
2612
|
+
const session = await this.buildSessionPayload(sessionId);
|
|
2613
|
+
if (session) {
|
|
2614
|
+
this.io.emit("safeclaw:openclawSessionUpdate", session);
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
async handleSessionEnd(sessionId) {
|
|
2618
|
+
const db = getDb();
|
|
2619
|
+
await db.update(schema_exports.openclawSessions).set({
|
|
2620
|
+
status: "ENDED",
|
|
2621
|
+
endedAt: sql3`datetime('now')`
|
|
2622
|
+
}).where(eq2(schema_exports.openclawSessions.id, sessionId));
|
|
2623
|
+
const session = await this.buildSessionPayload(sessionId);
|
|
2624
|
+
if (session) {
|
|
2625
|
+
this.io.emit("safeclaw:openclawSessionUpdate", session);
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
async buildSessionPayload(sessionId) {
|
|
2629
|
+
const db = getDb();
|
|
2630
|
+
const [row] = await db.select().from(schema_exports.openclawSessions).where(eq2(schema_exports.openclawSessions.id, sessionId));
|
|
2631
|
+
if (!row) return null;
|
|
2632
|
+
const activities = await db.select().from(schema_exports.agentActivities).where(eq2(schema_exports.agentActivities.openclawSessionId, sessionId));
|
|
2633
|
+
const threatSummary = {
|
|
2634
|
+
NONE: 0,
|
|
2635
|
+
LOW: 0,
|
|
2636
|
+
MEDIUM: 0,
|
|
2637
|
+
HIGH: 0,
|
|
2638
|
+
CRITICAL: 0
|
|
2639
|
+
};
|
|
2640
|
+
for (const a of activities) {
|
|
2641
|
+
threatSummary[a.threatLevel]++;
|
|
2642
|
+
}
|
|
2643
|
+
return {
|
|
2644
|
+
id: row.id,
|
|
2645
|
+
startedAt: row.startedAt,
|
|
2646
|
+
endedAt: row.endedAt,
|
|
2647
|
+
status: row.status,
|
|
2648
|
+
model: row.model,
|
|
2649
|
+
activityCount: activities.length,
|
|
2650
|
+
threatSummary
|
|
2651
|
+
};
|
|
2652
|
+
}
|
|
2653
|
+
mapRowToActivity(r) {
|
|
2654
|
+
let parsedSecrets = null;
|
|
2655
|
+
if (r.secretsDetected) {
|
|
2656
|
+
try {
|
|
2657
|
+
parsedSecrets = JSON.parse(r.secretsDetected);
|
|
2658
|
+
} catch {
|
|
2659
|
+
parsedSecrets = null;
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2662
|
+
let parsedFindings = null;
|
|
2663
|
+
if (r.threatFindings) {
|
|
2664
|
+
try {
|
|
2665
|
+
parsedFindings = JSON.parse(r.threatFindings);
|
|
2666
|
+
} catch {
|
|
2667
|
+
parsedFindings = null;
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
return {
|
|
2671
|
+
id: r.id,
|
|
2672
|
+
openclawSessionId: r.openclawSessionId,
|
|
2673
|
+
activityType: r.activityType,
|
|
2674
|
+
detail: r.detail,
|
|
2675
|
+
rawPayload: r.rawPayload,
|
|
2676
|
+
threatLevel: r.threatLevel,
|
|
2677
|
+
timestamp: r.timestamp,
|
|
2678
|
+
toolName: r.toolName,
|
|
2679
|
+
targetPath: r.targetPath,
|
|
2680
|
+
runId: r.runId,
|
|
2681
|
+
contentPreview: r.contentPreview,
|
|
2682
|
+
readContentPreview: r.readContentPreview,
|
|
2683
|
+
secretsDetected: parsedSecrets,
|
|
2684
|
+
threatFindings: parsedFindings,
|
|
2685
|
+
resolved: Boolean(r.resolved),
|
|
2686
|
+
resolvedAt: r.resolvedAt ?? null
|
|
2687
|
+
};
|
|
2688
|
+
}
|
|
2689
|
+
async resolveActivity(activityId, resolved) {
|
|
2690
|
+
const db = getDb();
|
|
2691
|
+
await db.update(schema_exports.agentActivities).set({
|
|
2692
|
+
resolved: resolved ? 1 : 0,
|
|
2693
|
+
resolvedAt: resolved ? (/* @__PURE__ */ new Date()).toISOString() : null
|
|
2694
|
+
}).where(eq2(schema_exports.agentActivities.id, activityId));
|
|
2695
|
+
const [row] = await db.select().from(schema_exports.agentActivities).where(eq2(schema_exports.agentActivities.id, activityId));
|
|
2696
|
+
if (!row) return null;
|
|
2697
|
+
return this.mapRowToActivity(row);
|
|
2698
|
+
}
|
|
2699
|
+
async getThreats(severity, resolved, limit = 100) {
|
|
2700
|
+
const db = getDb();
|
|
2701
|
+
const conditions = [ne(schema_exports.agentActivities.threatLevel, "NONE")];
|
|
2702
|
+
if (severity) {
|
|
2703
|
+
conditions.push(eq2(schema_exports.agentActivities.threatLevel, severity));
|
|
2704
|
+
}
|
|
2705
|
+
if (resolved !== void 0) {
|
|
2706
|
+
conditions.push(eq2(schema_exports.agentActivities.resolved, resolved ? 1 : 0));
|
|
2707
|
+
}
|
|
2708
|
+
const rows = await db.select().from(schema_exports.agentActivities).where(and(...conditions)).orderBy(desc2(schema_exports.agentActivities.id)).limit(limit);
|
|
2709
|
+
return rows.map((r) => this.mapRowToActivity(r));
|
|
2710
|
+
}
|
|
2711
|
+
async getActivities(sessionId, limit = 50) {
|
|
2712
|
+
const db = getDb();
|
|
2713
|
+
let rows;
|
|
2714
|
+
if (sessionId) {
|
|
2715
|
+
rows = await db.select().from(schema_exports.agentActivities).where(eq2(schema_exports.agentActivities.openclawSessionId, sessionId)).orderBy(desc2(schema_exports.agentActivities.id)).limit(limit);
|
|
2716
|
+
} else {
|
|
2717
|
+
rows = await db.select().from(schema_exports.agentActivities).orderBy(desc2(schema_exports.agentActivities.id)).limit(limit);
|
|
2718
|
+
}
|
|
2719
|
+
return rows.map((r) => this.mapRowToActivity(r));
|
|
2720
|
+
}
|
|
2721
|
+
async getAllSessions() {
|
|
2722
|
+
const db = getDb();
|
|
2723
|
+
const rows = await db.select().from(schema_exports.openclawSessions).orderBy(desc2(schema_exports.openclawSessions.startedAt));
|
|
2724
|
+
const results = [];
|
|
2725
|
+
for (const row of rows) {
|
|
2726
|
+
const session = await this.buildSessionPayload(row.id);
|
|
2727
|
+
if (session) results.push(session);
|
|
2728
|
+
}
|
|
2729
|
+
return results;
|
|
2730
|
+
}
|
|
2731
|
+
broadcastStatus() {
|
|
2732
|
+
const config = readOpenClawConfig();
|
|
2733
|
+
this.io.emit("safeclaw:openclawMonitorStatus", {
|
|
2734
|
+
connectionStatus: this.client.getStatus(),
|
|
2735
|
+
gatewayPort: config?.gateway?.port ?? null,
|
|
2736
|
+
lastEventAt: this.lastEventAt,
|
|
2737
|
+
activeSessionCount: 0
|
|
2738
|
+
});
|
|
2739
|
+
}
|
|
2740
|
+
getExecApprovalService() {
|
|
2741
|
+
return this.execApprovalService;
|
|
2742
|
+
}
|
|
2743
|
+
start() {
|
|
2744
|
+
this.client.connect();
|
|
2745
|
+
this.sessionWatcher.start();
|
|
2746
|
+
}
|
|
2747
|
+
reconnect() {
|
|
2748
|
+
this.client.reconnect();
|
|
2749
|
+
}
|
|
2750
|
+
stop() {
|
|
2751
|
+
this.execApprovalService.destroy();
|
|
2752
|
+
this.client.destroy();
|
|
2753
|
+
this.sessionWatcher.stop();
|
|
2754
|
+
}
|
|
2755
|
+
getStatus() {
|
|
2756
|
+
return this.client.getStatus();
|
|
2757
|
+
}
|
|
2758
|
+
};
|
|
2759
|
+
function createOpenClawMonitor(io2) {
|
|
2760
|
+
if (instance2) {
|
|
2761
|
+
instance2.stop();
|
|
2762
|
+
}
|
|
2763
|
+
instance2 = new OpenClawMonitor(io2);
|
|
2764
|
+
return instance2;
|
|
2765
|
+
}
|
|
2766
|
+
function getOpenClawMonitor() {
|
|
2767
|
+
return instance2;
|
|
2768
|
+
}
|
|
2769
|
+
|
|
2770
|
+
// src/services/access-control.ts
|
|
2771
|
+
import { eq as eq3, and as and2, sql as sql4 } from "drizzle-orm";
|
|
2772
|
+
var TOOL_GROUP_MAP = {
|
|
2773
|
+
filesystem: "group:fs",
|
|
2774
|
+
system_commands: "group:runtime",
|
|
2775
|
+
network: "group:web"
|
|
2776
|
+
};
|
|
2777
|
+
function deriveMcpServerStates(config, denyList) {
|
|
2778
|
+
const pluginEntries = config.plugins?.entries ?? {};
|
|
2779
|
+
return Object.keys(pluginEntries).map((name) => {
|
|
2780
|
+
const pluginEnabled = pluginEntries[name].enabled !== false;
|
|
2781
|
+
const toolsDenyBlocked = denyList.includes(`mcp__${name}`);
|
|
2782
|
+
return {
|
|
2783
|
+
name,
|
|
2784
|
+
pluginEnabled,
|
|
2785
|
+
toolsDenyBlocked,
|
|
2786
|
+
effectivelyEnabled: pluginEnabled && !toolsDenyBlocked
|
|
2787
|
+
};
|
|
2788
|
+
});
|
|
2789
|
+
}
|
|
2790
|
+
function deriveAccessState() {
|
|
2791
|
+
const config = readOpenClawConfig();
|
|
2792
|
+
if (!config) {
|
|
2793
|
+
return {
|
|
2794
|
+
toggles: [
|
|
2795
|
+
{ category: "filesystem", enabled: true },
|
|
2796
|
+
{ category: "mcp_servers", enabled: true },
|
|
2797
|
+
{ category: "network", enabled: true },
|
|
2798
|
+
{ category: "system_commands", enabled: true }
|
|
2799
|
+
],
|
|
2800
|
+
mcpServers: [],
|
|
2801
|
+
openclawConfigAvailable: false
|
|
2802
|
+
};
|
|
2803
|
+
}
|
|
2804
|
+
const denyList = config.tools?.deny ?? [];
|
|
2805
|
+
const filesystemEnabled = !denyList.includes("group:fs");
|
|
2806
|
+
const systemCommandsEnabled = !denyList.includes("group:runtime");
|
|
2807
|
+
const networkEnabled = !denyList.includes("group:web") && config.browser?.enabled !== false;
|
|
2808
|
+
const pluginEntries = config.plugins?.entries ?? {};
|
|
2809
|
+
const pluginNames = Object.keys(pluginEntries);
|
|
2810
|
+
const mcpEnabled = pluginNames.length === 0 || pluginNames.some((name) => pluginEntries[name].enabled !== false);
|
|
2811
|
+
const toggles = [
|
|
2812
|
+
{ category: "filesystem", enabled: filesystemEnabled },
|
|
2813
|
+
{ category: "mcp_servers", enabled: mcpEnabled },
|
|
2814
|
+
{ category: "network", enabled: networkEnabled },
|
|
2815
|
+
{ category: "system_commands", enabled: systemCommandsEnabled }
|
|
2816
|
+
];
|
|
2817
|
+
const mcpServers = deriveMcpServerStates(config, denyList);
|
|
2818
|
+
return { toggles, mcpServers, openclawConfigAvailable: true };
|
|
2819
|
+
}
|
|
2820
|
+
async function applyAccessToggle(category, enabled) {
|
|
2821
|
+
const config = readOpenClawConfig();
|
|
2822
|
+
if (!config) {
|
|
2823
|
+
throw new Error("OpenClaw config not found");
|
|
2824
|
+
}
|
|
2825
|
+
if (category === "mcp_servers") {
|
|
2826
|
+
await applyMcpToggle(config, enabled);
|
|
2827
|
+
} else if (category === "network") {
|
|
2828
|
+
applyNetworkToggle(config, enabled);
|
|
2829
|
+
} else {
|
|
2830
|
+
applyToolGroupToggle(config, category, enabled);
|
|
2831
|
+
}
|
|
2832
|
+
await updateAuditDb(category, enabled);
|
|
2833
|
+
return deriveAccessState();
|
|
2834
|
+
}
|
|
2835
|
+
async function applyMcpServerToggle(serverName, enabled) {
|
|
2836
|
+
const config = readOpenClawConfig();
|
|
2837
|
+
if (!config) {
|
|
2838
|
+
throw new Error("OpenClaw config not found");
|
|
2839
|
+
}
|
|
2840
|
+
const denyPattern = `mcp__${serverName}`;
|
|
2841
|
+
const currentDeny = [...config.tools?.deny ?? []];
|
|
2842
|
+
if (enabled) {
|
|
2843
|
+
const filtered = currentDeny.filter((entry) => entry !== denyPattern);
|
|
2844
|
+
writeOpenClawConfig({ tools: { deny: filtered } });
|
|
2845
|
+
} else {
|
|
2846
|
+
if (!currentDeny.includes(denyPattern)) {
|
|
2847
|
+
currentDeny.push(denyPattern);
|
|
2848
|
+
}
|
|
2849
|
+
writeOpenClawConfig({ tools: { deny: currentDeny } });
|
|
2850
|
+
}
|
|
2851
|
+
await updateAuditDb(`mcp_server:${serverName}`, enabled);
|
|
2852
|
+
return deriveAccessState();
|
|
2853
|
+
}
|
|
2854
|
+
function applyToolGroupToggle(config, category, enabled) {
|
|
2855
|
+
const groupName = TOOL_GROUP_MAP[category];
|
|
2856
|
+
if (!groupName) return;
|
|
2857
|
+
const currentDeny = [...config.tools?.deny ?? []];
|
|
2858
|
+
if (enabled) {
|
|
2859
|
+
const filtered = currentDeny.filter((entry) => entry !== groupName);
|
|
2860
|
+
writeOpenClawConfig({ tools: { deny: filtered } });
|
|
2861
|
+
} else {
|
|
2862
|
+
if (!currentDeny.includes(groupName)) {
|
|
2863
|
+
currentDeny.push(groupName);
|
|
2864
|
+
}
|
|
2865
|
+
writeOpenClawConfig({ tools: { deny: currentDeny } });
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
2868
|
+
function applyNetworkToggle(config, enabled) {
|
|
2869
|
+
const currentDeny = [...config.tools?.deny ?? []];
|
|
2870
|
+
const groupName = "group:web";
|
|
2871
|
+
if (enabled) {
|
|
2872
|
+
const filtered = currentDeny.filter((entry) => entry !== groupName);
|
|
2873
|
+
writeOpenClawConfig({
|
|
2874
|
+
tools: { deny: filtered },
|
|
2875
|
+
browser: { enabled: true }
|
|
2876
|
+
});
|
|
2877
|
+
} else {
|
|
2878
|
+
if (!currentDeny.includes(groupName)) {
|
|
2879
|
+
currentDeny.push(groupName);
|
|
2880
|
+
}
|
|
2881
|
+
writeOpenClawConfig({
|
|
2882
|
+
tools: { deny: currentDeny },
|
|
2883
|
+
browser: { enabled: false }
|
|
2884
|
+
});
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2887
|
+
async function applyMcpToggle(config, enabled) {
|
|
2888
|
+
const pluginEntries = config.plugins?.entries ?? {};
|
|
2889
|
+
const pluginNames = Object.keys(pluginEntries);
|
|
2890
|
+
if (pluginNames.length === 0) return;
|
|
2891
|
+
const currentDeny = [...config.tools?.deny ?? []];
|
|
2892
|
+
if (!enabled) {
|
|
2893
|
+
const stateMap = {};
|
|
2894
|
+
for (const name of pluginNames) {
|
|
2895
|
+
stateMap[name] = pluginEntries[name].enabled !== false;
|
|
2896
|
+
}
|
|
2897
|
+
await savePreviousPluginState(stateMap);
|
|
2898
|
+
const disabledEntries = {};
|
|
2899
|
+
for (const name of pluginNames) {
|
|
2900
|
+
disabledEntries[name] = { enabled: false };
|
|
2901
|
+
}
|
|
2902
|
+
for (const name of pluginNames) {
|
|
2903
|
+
const denyPattern = `mcp__${name}`;
|
|
2904
|
+
if (!currentDeny.includes(denyPattern)) {
|
|
2905
|
+
currentDeny.push(denyPattern);
|
|
2906
|
+
}
|
|
2907
|
+
}
|
|
2908
|
+
writeOpenClawConfig({
|
|
2909
|
+
plugins: { entries: disabledEntries },
|
|
2910
|
+
tools: { deny: currentDeny }
|
|
2911
|
+
});
|
|
2912
|
+
} else {
|
|
2913
|
+
const previousState = await loadPreviousPluginState();
|
|
2914
|
+
const restoredEntries = {};
|
|
2915
|
+
for (const name of pluginNames) {
|
|
2916
|
+
restoredEntries[name] = {
|
|
2917
|
+
enabled: previousState?.[name] ?? true
|
|
2918
|
+
};
|
|
2919
|
+
}
|
|
2920
|
+
const mcpDenyPatterns = new Set(pluginNames.map((n) => `mcp__${n}`));
|
|
2921
|
+
const filteredDeny = currentDeny.filter(
|
|
2922
|
+
(entry) => !mcpDenyPatterns.has(entry)
|
|
2923
|
+
);
|
|
2924
|
+
writeOpenClawConfig({
|
|
2925
|
+
plugins: { entries: restoredEntries },
|
|
2926
|
+
tools: { deny: filteredDeny }
|
|
2927
|
+
});
|
|
2928
|
+
}
|
|
2929
|
+
}
|
|
2930
|
+
async function savePreviousPluginState(stateMap) {
|
|
2931
|
+
const db = getDb();
|
|
2932
|
+
const existing = await db.select().from(schema_exports.accessConfig).where(
|
|
2933
|
+
and2(
|
|
2934
|
+
eq3(schema_exports.accessConfig.category, "mcp_servers"),
|
|
2935
|
+
eq3(schema_exports.accessConfig.key, "previous_plugin_state")
|
|
2936
|
+
)
|
|
2937
|
+
);
|
|
2938
|
+
if (existing.length > 0) {
|
|
2939
|
+
await db.update(schema_exports.accessConfig).set({
|
|
2940
|
+
value: JSON.stringify(stateMap),
|
|
2941
|
+
updatedAt: sql4`datetime('now')`
|
|
2942
|
+
}).where(
|
|
2943
|
+
and2(
|
|
2944
|
+
eq3(schema_exports.accessConfig.category, "mcp_servers"),
|
|
2945
|
+
eq3(schema_exports.accessConfig.key, "previous_plugin_state")
|
|
2946
|
+
)
|
|
2947
|
+
);
|
|
2948
|
+
} else {
|
|
2949
|
+
await db.insert(schema_exports.accessConfig).values({
|
|
2950
|
+
category: "mcp_servers",
|
|
2951
|
+
key: "previous_plugin_state",
|
|
2952
|
+
value: JSON.stringify(stateMap)
|
|
2953
|
+
});
|
|
2954
|
+
}
|
|
2955
|
+
}
|
|
2956
|
+
async function loadPreviousPluginState() {
|
|
2957
|
+
const db = getDb();
|
|
2958
|
+
const rows = await db.select().from(schema_exports.accessConfig).where(
|
|
2959
|
+
and2(
|
|
2960
|
+
eq3(schema_exports.accessConfig.category, "mcp_servers"),
|
|
2961
|
+
eq3(schema_exports.accessConfig.key, "previous_plugin_state")
|
|
2962
|
+
)
|
|
2963
|
+
);
|
|
2964
|
+
if (rows.length === 0) return null;
|
|
2965
|
+
try {
|
|
2966
|
+
return JSON.parse(rows[0].value);
|
|
2967
|
+
} catch {
|
|
2968
|
+
return null;
|
|
2969
|
+
}
|
|
2970
|
+
}
|
|
2971
|
+
async function updateAuditDb(category, enabled) {
|
|
2972
|
+
const db = getDb();
|
|
2973
|
+
try {
|
|
2974
|
+
await db.update(schema_exports.accessConfig).set({
|
|
2975
|
+
value: enabled ? "true" : "false",
|
|
2976
|
+
updatedAt: sql4`datetime('now')`
|
|
2977
|
+
}).where(
|
|
2978
|
+
and2(
|
|
2979
|
+
eq3(schema_exports.accessConfig.category, category),
|
|
2980
|
+
eq3(schema_exports.accessConfig.key, "enabled")
|
|
2981
|
+
)
|
|
2982
|
+
);
|
|
2983
|
+
} catch (err) {
|
|
2984
|
+
logger.warn({ err, category, enabled }, "Failed to update audit DB");
|
|
2985
|
+
}
|
|
2986
|
+
}
|
|
2987
|
+
|
|
2988
|
+
// src/server/socket.ts
|
|
2989
|
+
var io = null;
|
|
2990
|
+
function setupSocketIO(httpServer) {
|
|
2991
|
+
io = new SocketIOServer(
|
|
2992
|
+
httpServer,
|
|
2993
|
+
{
|
|
2994
|
+
cors: {
|
|
2995
|
+
origin: "*",
|
|
2996
|
+
methods: ["GET", "POST"]
|
|
2997
|
+
}
|
|
2998
|
+
}
|
|
2999
|
+
);
|
|
3000
|
+
io.on("connection", (socket) => {
|
|
3001
|
+
logger.info(`Client connected: ${socket.id}`);
|
|
3002
|
+
socket.on("safeclaw:getStats", async () => {
|
|
3003
|
+
const db = getDb();
|
|
3004
|
+
const allLogs = await db.select().from(schema_exports.commandLogs);
|
|
3005
|
+
const activeSessions = await db.select().from(schema_exports.sessions).where(eq4(schema_exports.sessions.status, "ACTIVE"));
|
|
3006
|
+
const allActivities = await db.select().from(schema_exports.agentActivities);
|
|
3007
|
+
const openclawActiveSessions = await db.select().from(schema_exports.openclawSessions).where(eq4(schema_exports.openclawSessions.status, "ACTIVE"));
|
|
3008
|
+
let execApprovalTotal = 0;
|
|
3009
|
+
let execApprovalBlocked = 0;
|
|
3010
|
+
let execApprovalAllowed = 0;
|
|
3011
|
+
let execApprovalPending = 0;
|
|
3012
|
+
const monitor = getOpenClawMonitor();
|
|
3013
|
+
if (monitor) {
|
|
3014
|
+
const approvalService = monitor.getExecApprovalService();
|
|
3015
|
+
const approvalStats = approvalService.getStats();
|
|
3016
|
+
execApprovalTotal = approvalStats.total;
|
|
3017
|
+
execApprovalBlocked = approvalStats.blocked;
|
|
3018
|
+
execApprovalAllowed = approvalStats.allowed;
|
|
3019
|
+
execApprovalPending = approvalStats.pending;
|
|
3020
|
+
}
|
|
3021
|
+
const stats = {
|
|
3022
|
+
totalCommands: allLogs.length,
|
|
3023
|
+
blockedCommands: allLogs.filter((l) => l.status === "BLOCKED").length,
|
|
3024
|
+
allowedCommands: allLogs.filter((l) => l.status === "ALLOWED").length,
|
|
3025
|
+
activeSessions: activeSessions.length,
|
|
3026
|
+
threatBreakdown: {
|
|
3027
|
+
NONE: allLogs.filter((l) => l.threatLevel === "NONE").length,
|
|
3028
|
+
LOW: allLogs.filter((l) => l.threatLevel === "LOW").length,
|
|
3029
|
+
MEDIUM: allLogs.filter((l) => l.threatLevel === "MEDIUM").length,
|
|
3030
|
+
HIGH: allLogs.filter((l) => l.threatLevel === "HIGH").length,
|
|
3031
|
+
CRITICAL: allLogs.filter((l) => l.threatLevel === "CRITICAL").length
|
|
3032
|
+
},
|
|
3033
|
+
openclawActivities: allActivities.length,
|
|
3034
|
+
openclawActiveSessions: openclawActiveSessions.length,
|
|
3035
|
+
openclawThreatBreakdown: {
|
|
3036
|
+
NONE: allActivities.filter((a) => a.threatLevel === "NONE").length,
|
|
3037
|
+
LOW: allActivities.filter((a) => a.threatLevel === "LOW").length,
|
|
3038
|
+
MEDIUM: allActivities.filter((a) => a.threatLevel === "MEDIUM").length,
|
|
3039
|
+
HIGH: allActivities.filter((a) => a.threatLevel === "HIGH").length,
|
|
3040
|
+
CRITICAL: allActivities.filter((a) => a.threatLevel === "CRITICAL").length
|
|
3041
|
+
},
|
|
3042
|
+
resolvedThreatBreakdown: {
|
|
3043
|
+
NONE: allActivities.filter((a) => a.threatLevel === "NONE" && a.resolved === 1).length,
|
|
3044
|
+
LOW: allActivities.filter((a) => a.threatLevel === "LOW" && a.resolved === 1).length,
|
|
3045
|
+
MEDIUM: allActivities.filter((a) => a.threatLevel === "MEDIUM" && a.resolved === 1).length,
|
|
3046
|
+
HIGH: allActivities.filter((a) => a.threatLevel === "HIGH" && a.resolved === 1).length,
|
|
3047
|
+
CRITICAL: allActivities.filter((a) => a.threatLevel === "CRITICAL" && a.resolved === 1).length
|
|
3048
|
+
},
|
|
3049
|
+
threatDetectionRate: {
|
|
3050
|
+
activitiesWithThreats: allActivities.filter((a) => a.threatLevel !== "NONE").length,
|
|
3051
|
+
totalActivities: allActivities.length
|
|
3052
|
+
},
|
|
3053
|
+
execApprovalTotal,
|
|
3054
|
+
execApprovalBlocked,
|
|
3055
|
+
execApprovalAllowed,
|
|
3056
|
+
execApprovalPending
|
|
3057
|
+
};
|
|
3058
|
+
socket.emit("safeclaw:stats", stats);
|
|
3059
|
+
});
|
|
3060
|
+
socket.on("safeclaw:decision", async ({ commandId, action }) => {
|
|
3061
|
+
const db = getDb();
|
|
3062
|
+
const newStatus = action === "ALLOW" ? "ALLOWED" : "BLOCKED";
|
|
3063
|
+
await db.update(schema_exports.commandLogs).set({ status: newStatus, decisionBy: "USER" }).where(eq4(schema_exports.commandLogs.id, commandId));
|
|
3064
|
+
logger.info(`Command ${commandId} ${newStatus} by user`);
|
|
3065
|
+
const [updated] = await db.select().from(schema_exports.commandLogs).where(eq4(schema_exports.commandLogs.id, commandId));
|
|
3066
|
+
if (updated) {
|
|
3067
|
+
io.emit("safeclaw:commandLogged", {
|
|
3068
|
+
id: updated.id,
|
|
3069
|
+
command: updated.command,
|
|
3070
|
+
status: updated.status,
|
|
3071
|
+
threatLevel: updated.threatLevel,
|
|
3072
|
+
timestamp: updated.timestamp,
|
|
3073
|
+
sessionId: updated.sessionId,
|
|
3074
|
+
decisionBy: updated.decisionBy
|
|
3075
|
+
});
|
|
3076
|
+
}
|
|
3077
|
+
});
|
|
3078
|
+
socket.on("safeclaw:getRecentCommands", async ({ limit }) => {
|
|
3079
|
+
const db = getDb();
|
|
3080
|
+
const commands = await db.select().from(schema_exports.commandLogs).orderBy(desc3(schema_exports.commandLogs.id)).limit(limit);
|
|
3081
|
+
for (const cmd of commands) {
|
|
3082
|
+
socket.emit("safeclaw:commandLogged", {
|
|
3083
|
+
id: cmd.id,
|
|
3084
|
+
command: cmd.command,
|
|
3085
|
+
status: cmd.status,
|
|
3086
|
+
threatLevel: cmd.threatLevel,
|
|
3087
|
+
timestamp: cmd.timestamp,
|
|
3088
|
+
sessionId: cmd.sessionId,
|
|
3089
|
+
decisionBy: cmd.decisionBy
|
|
3090
|
+
});
|
|
3091
|
+
}
|
|
3092
|
+
});
|
|
3093
|
+
socket.on("safeclaw:getAccessConfig", async () => {
|
|
3094
|
+
const db = getDb();
|
|
3095
|
+
const config = await db.select().from(schema_exports.accessConfig);
|
|
3096
|
+
socket.emit("safeclaw:accessConfig", config);
|
|
3097
|
+
});
|
|
3098
|
+
socket.on("safeclaw:getAccessControlState", () => {
|
|
3099
|
+
const state = deriveAccessState();
|
|
3100
|
+
socket.emit("safeclaw:accessControlState", state);
|
|
3101
|
+
});
|
|
3102
|
+
socket.on("safeclaw:toggleAccess", async ({ category, enabled }) => {
|
|
3103
|
+
try {
|
|
3104
|
+
const newState = await applyAccessToggle(
|
|
3105
|
+
category,
|
|
3106
|
+
enabled
|
|
3107
|
+
);
|
|
3108
|
+
logger.info(`Access toggle: ${category} set to ${enabled}`);
|
|
3109
|
+
io.emit("safeclaw:accessControlState", newState);
|
|
3110
|
+
} catch (err) {
|
|
3111
|
+
logger.error({ err, category, enabled }, "Failed to apply access toggle");
|
|
3112
|
+
const state = deriveAccessState();
|
|
3113
|
+
io.emit("safeclaw:accessControlState", state);
|
|
3114
|
+
}
|
|
3115
|
+
});
|
|
3116
|
+
socket.on("safeclaw:toggleMcpServer", async ({ serverName, enabled }) => {
|
|
3117
|
+
try {
|
|
3118
|
+
const newState = await applyMcpServerToggle(serverName, enabled);
|
|
3119
|
+
logger.info(`MCP server toggle: ${serverName} set to ${enabled}`);
|
|
3120
|
+
io.emit("safeclaw:accessControlState", newState);
|
|
3121
|
+
} catch (err) {
|
|
3122
|
+
logger.error({ err, serverName, enabled }, "Failed to toggle MCP server");
|
|
3123
|
+
const state = deriveAccessState();
|
|
3124
|
+
io.emit("safeclaw:accessControlState", state);
|
|
3125
|
+
}
|
|
3126
|
+
});
|
|
3127
|
+
socket.on("safeclaw:getSettings", async () => {
|
|
3128
|
+
const config = readConfig();
|
|
3129
|
+
socket.emit("safeclaw:settingsData", config);
|
|
3130
|
+
});
|
|
3131
|
+
socket.on("safeclaw:updateSettings", async (updates) => {
|
|
3132
|
+
const current = readConfig();
|
|
3133
|
+
const updated = { ...current, ...updates };
|
|
3134
|
+
writeConfig(updated);
|
|
3135
|
+
logger.info({ updates }, "Settings updated");
|
|
3136
|
+
socket.emit("safeclaw:settingsData", updated);
|
|
3137
|
+
});
|
|
3138
|
+
socket.on("safeclaw:getOpenclawConfig", async () => {
|
|
3139
|
+
const config = readOpenClawConfig();
|
|
3140
|
+
socket.emit("safeclaw:openclawConfig", config);
|
|
3141
|
+
});
|
|
3142
|
+
socket.on("safeclaw:updateOpenclawConfig", async (updates) => {
|
|
3143
|
+
const updated = writeOpenClawConfig(updates);
|
|
3144
|
+
logger.info({ updates }, "OpenClaw config updated");
|
|
3145
|
+
socket.emit("safeclaw:openclawConfig", updated);
|
|
3146
|
+
});
|
|
3147
|
+
socket.on("safeclaw:getOpenclawSessions", async () => {
|
|
3148
|
+
const monitor = getOpenClawMonitor();
|
|
3149
|
+
if (!monitor) return;
|
|
3150
|
+
const sessions2 = await monitor.getAllSessions();
|
|
3151
|
+
for (const session of sessions2) {
|
|
3152
|
+
socket.emit("safeclaw:openclawSessionUpdate", session);
|
|
3153
|
+
}
|
|
3154
|
+
});
|
|
3155
|
+
socket.on("safeclaw:getOpenclawActivities", async ({ sessionId, limit }) => {
|
|
3156
|
+
const monitor = getOpenClawMonitor();
|
|
3157
|
+
if (!monitor) return;
|
|
3158
|
+
const activities = await monitor.getActivities(sessionId, limit);
|
|
3159
|
+
for (const activity of activities) {
|
|
3160
|
+
socket.emit("safeclaw:openclawActivity", activity);
|
|
3161
|
+
}
|
|
3162
|
+
});
|
|
3163
|
+
socket.on("safeclaw:getOpenclawMonitorStatus", async () => {
|
|
3164
|
+
const monitor = getOpenClawMonitor();
|
|
3165
|
+
if (!monitor) return;
|
|
3166
|
+
monitor.broadcastStatus();
|
|
3167
|
+
});
|
|
3168
|
+
socket.on("safeclaw:reconnectOpenclaw", () => {
|
|
3169
|
+
const monitor = getOpenClawMonitor();
|
|
3170
|
+
if (!monitor) return;
|
|
3171
|
+
monitor.reconnect();
|
|
3172
|
+
});
|
|
3173
|
+
socket.on("safeclaw:resolveActivity", async ({ activityId, resolved }) => {
|
|
3174
|
+
const monitor = getOpenClawMonitor();
|
|
3175
|
+
if (!monitor) return;
|
|
3176
|
+
const updated = await monitor.resolveActivity(activityId, resolved);
|
|
3177
|
+
if (updated) {
|
|
3178
|
+
io.emit("safeclaw:threatResolved", updated);
|
|
3179
|
+
}
|
|
3180
|
+
});
|
|
3181
|
+
socket.on("safeclaw:getThreats", async ({ severity, resolved, limit }) => {
|
|
3182
|
+
const monitor = getOpenClawMonitor();
|
|
3183
|
+
if (!monitor) return;
|
|
3184
|
+
const threats = await monitor.getThreats(severity, resolved, limit);
|
|
3185
|
+
for (const threat of threats) {
|
|
3186
|
+
socket.emit("safeclaw:openclawActivity", threat);
|
|
3187
|
+
}
|
|
3188
|
+
});
|
|
3189
|
+
socket.on("safeclaw:execDecision", ({ approvalId, decision }) => {
|
|
3190
|
+
const monitor = getOpenClawMonitor();
|
|
3191
|
+
if (!monitor) return;
|
|
3192
|
+
const service = monitor.getExecApprovalService();
|
|
3193
|
+
service.handleDecision(approvalId, decision);
|
|
3194
|
+
});
|
|
3195
|
+
socket.on("safeclaw:getPendingApprovals", () => {
|
|
3196
|
+
const monitor = getOpenClawMonitor();
|
|
3197
|
+
if (!monitor) return;
|
|
3198
|
+
const service = monitor.getExecApprovalService();
|
|
3199
|
+
const pending = service.getPendingApprovals();
|
|
3200
|
+
for (const entry of pending) {
|
|
3201
|
+
socket.emit("safeclaw:execApprovalRequested", entry);
|
|
3202
|
+
}
|
|
3203
|
+
});
|
|
3204
|
+
socket.on("safeclaw:getApprovalHistory", ({ limit }) => {
|
|
3205
|
+
const monitor = getOpenClawMonitor();
|
|
3206
|
+
if (!monitor) return;
|
|
3207
|
+
const service = monitor.getExecApprovalService();
|
|
3208
|
+
const history = service.getHistory(limit);
|
|
3209
|
+
socket.emit("safeclaw:approvalHistoryBatch", history);
|
|
3210
|
+
});
|
|
3211
|
+
socket.on("safeclaw:getAllowlist", () => {
|
|
3212
|
+
const monitor = getOpenClawMonitor();
|
|
3213
|
+
if (!monitor) return;
|
|
3214
|
+
const service = monitor.getExecApprovalService();
|
|
3215
|
+
const patterns = service.getRestrictedPatterns();
|
|
3216
|
+
socket.emit("safeclaw:allowlistState", {
|
|
3217
|
+
patterns: patterns.map((p) => ({ pattern: p }))
|
|
3218
|
+
});
|
|
3219
|
+
});
|
|
3220
|
+
socket.on("safeclaw:addAllowlistPattern", ({ pattern }) => {
|
|
3221
|
+
const monitor = getOpenClawMonitor();
|
|
3222
|
+
if (!monitor) return;
|
|
3223
|
+
const service = monitor.getExecApprovalService();
|
|
3224
|
+
try {
|
|
3225
|
+
service.addRestrictedPattern(pattern);
|
|
3226
|
+
} catch (err) {
|
|
3227
|
+
logger.error({ err, pattern }, "Failed to add restricted pattern");
|
|
3228
|
+
}
|
|
3229
|
+
});
|
|
3230
|
+
socket.on("safeclaw:removeAllowlistPattern", ({ pattern }) => {
|
|
3231
|
+
const monitor = getOpenClawMonitor();
|
|
3232
|
+
if (!monitor) return;
|
|
3233
|
+
const service = monitor.getExecApprovalService();
|
|
3234
|
+
try {
|
|
3235
|
+
service.removeRestrictedPattern(pattern);
|
|
3236
|
+
} catch (err) {
|
|
3237
|
+
logger.error({ err, pattern }, "Failed to remove restricted pattern");
|
|
3238
|
+
}
|
|
3239
|
+
});
|
|
3240
|
+
socket.on("disconnect", () => {
|
|
3241
|
+
logger.info(`Client disconnected: ${socket.id}`);
|
|
3242
|
+
});
|
|
3243
|
+
});
|
|
3244
|
+
return io;
|
|
3245
|
+
}
|
|
3246
|
+
|
|
3247
|
+
// src/server/routes.ts
|
|
3248
|
+
import { desc as desc4, eq as eq5, and as and4, sql as sql6 } from "drizzle-orm";
|
|
3249
|
+
async function registerRoutes(app) {
|
|
3250
|
+
app.get("/api/health", async () => {
|
|
3251
|
+
return { status: "ok", timestamp: (/* @__PURE__ */ new Date()).toISOString() };
|
|
3252
|
+
});
|
|
3253
|
+
app.get("/api/commands", async (request) => {
|
|
3254
|
+
const db = getDb();
|
|
3255
|
+
const { limit = 50 } = request.query;
|
|
3256
|
+
const commands = await db.select().from(schema_exports.commandLogs).orderBy(desc4(schema_exports.commandLogs.id)).limit(Number(limit));
|
|
3257
|
+
return commands;
|
|
3258
|
+
});
|
|
3259
|
+
app.get("/api/sessions", async () => {
|
|
3260
|
+
const db = getDb();
|
|
3261
|
+
const sessionList = await db.select().from(schema_exports.sessions).orderBy(desc4(schema_exports.sessions.startedAt));
|
|
3262
|
+
return sessionList;
|
|
3263
|
+
});
|
|
3264
|
+
app.get("/api/config", async () => {
|
|
3265
|
+
const db = getDb();
|
|
3266
|
+
const config = await db.select().from(schema_exports.accessConfig);
|
|
3267
|
+
return config;
|
|
3268
|
+
});
|
|
3269
|
+
app.get("/api/access-control/state", async () => {
|
|
3270
|
+
return deriveAccessState();
|
|
3271
|
+
});
|
|
3272
|
+
app.put("/api/config/access", async (request) => {
|
|
3273
|
+
const { category, enabled } = request.body;
|
|
3274
|
+
try {
|
|
3275
|
+
return await applyAccessToggle(
|
|
3276
|
+
category,
|
|
3277
|
+
enabled
|
|
3278
|
+
);
|
|
3279
|
+
} catch {
|
|
3280
|
+
const db = getDb();
|
|
3281
|
+
await db.update(schema_exports.accessConfig).set({
|
|
3282
|
+
value: enabled ? "true" : "false",
|
|
3283
|
+
updatedAt: sql6`datetime('now')`
|
|
3284
|
+
}).where(
|
|
3285
|
+
and4(
|
|
3286
|
+
eq5(schema_exports.accessConfig.category, category),
|
|
3287
|
+
eq5(schema_exports.accessConfig.key, "enabled")
|
|
3288
|
+
)
|
|
3289
|
+
);
|
|
3290
|
+
return deriveAccessState();
|
|
3291
|
+
}
|
|
3292
|
+
});
|
|
3293
|
+
app.put("/api/config/access/mcp-server", async (request) => {
|
|
3294
|
+
const { serverName, enabled } = request.body;
|
|
3295
|
+
try {
|
|
3296
|
+
return await applyMcpServerToggle(serverName, enabled);
|
|
3297
|
+
} catch {
|
|
3298
|
+
return deriveAccessState();
|
|
3299
|
+
}
|
|
3300
|
+
});
|
|
3301
|
+
app.get("/api/settings", async () => {
|
|
3302
|
+
return readConfig();
|
|
3303
|
+
});
|
|
3304
|
+
app.put("/api/settings", async (request) => {
|
|
3305
|
+
const updates = request.body;
|
|
3306
|
+
const current = readConfig();
|
|
3307
|
+
const updated = { ...current, ...updates };
|
|
3308
|
+
writeConfig(updated);
|
|
3309
|
+
return updated;
|
|
3310
|
+
});
|
|
3311
|
+
app.put("/api/commands/:id/decision", async (request) => {
|
|
3312
|
+
const { id } = request.params;
|
|
3313
|
+
const { action } = request.body;
|
|
3314
|
+
const db = getDb();
|
|
3315
|
+
const newStatus = action === "ALLOW" ? "ALLOWED" : "BLOCKED";
|
|
3316
|
+
await db.update(schema_exports.commandLogs).set({ status: newStatus, decisionBy: "USER" }).where(eq5(schema_exports.commandLogs.id, Number(id)));
|
|
3317
|
+
const [updated] = await db.select().from(schema_exports.commandLogs).where(eq5(schema_exports.commandLogs.id, Number(id)));
|
|
3318
|
+
return updated;
|
|
3319
|
+
});
|
|
3320
|
+
app.get("/api/openclaw/config", async () => {
|
|
3321
|
+
const config = readOpenClawConfig();
|
|
3322
|
+
return config ?? { error: "OpenClaw config not found" };
|
|
3323
|
+
});
|
|
3324
|
+
app.put("/api/openclaw/config", async (request) => {
|
|
3325
|
+
const updates = request.body;
|
|
3326
|
+
const updated = writeOpenClawConfig(updates);
|
|
3327
|
+
if (!updated) {
|
|
3328
|
+
return { error: "OpenClaw config not found" };
|
|
3329
|
+
}
|
|
3330
|
+
return updated;
|
|
3331
|
+
});
|
|
3332
|
+
app.get("/api/openclaw/sessions", async () => {
|
|
3333
|
+
const monitor = getOpenClawMonitor();
|
|
3334
|
+
if (!monitor) return [];
|
|
3335
|
+
return monitor.getAllSessions();
|
|
3336
|
+
});
|
|
3337
|
+
app.get("/api/openclaw/activities", async (request) => {
|
|
3338
|
+
const { sessionId, limit = 50 } = request.query;
|
|
3339
|
+
const monitor = getOpenClawMonitor();
|
|
3340
|
+
if (!monitor) return [];
|
|
3341
|
+
return monitor.getActivities(sessionId, Number(limit));
|
|
3342
|
+
});
|
|
3343
|
+
app.get("/api/openclaw/threats", async (request) => {
|
|
3344
|
+
const { severity, resolved, limit = 100 } = request.query;
|
|
3345
|
+
const monitor = getOpenClawMonitor();
|
|
3346
|
+
if (!monitor) return [];
|
|
3347
|
+
return monitor.getThreats(
|
|
3348
|
+
severity,
|
|
3349
|
+
resolved === void 0 ? void 0 : resolved === "true",
|
|
3350
|
+
Number(limit)
|
|
3351
|
+
);
|
|
3352
|
+
});
|
|
3353
|
+
app.put("/api/openclaw/activities/:id/resolve", async (request) => {
|
|
3354
|
+
const { id } = request.params;
|
|
3355
|
+
const { resolved } = request.body;
|
|
3356
|
+
const monitor = getOpenClawMonitor();
|
|
3357
|
+
if (!monitor) return { error: "Monitor not available" };
|
|
3358
|
+
const updated = await monitor.resolveActivity(Number(id), resolved);
|
|
3359
|
+
if (!updated) return { error: "Activity not found" };
|
|
3360
|
+
return updated;
|
|
3361
|
+
});
|
|
3362
|
+
app.get("/api/openclaw/status", async () => {
|
|
3363
|
+
const monitor = getOpenClawMonitor();
|
|
3364
|
+
const config = readOpenClawConfig();
|
|
3365
|
+
if (!monitor) {
|
|
3366
|
+
return {
|
|
3367
|
+
connectionStatus: "not_configured",
|
|
3368
|
+
gatewayPort: config?.gateway?.port ?? null,
|
|
3369
|
+
lastEventAt: null,
|
|
3370
|
+
activeSessionCount: 0
|
|
3371
|
+
};
|
|
3372
|
+
}
|
|
3373
|
+
return {
|
|
3374
|
+
connectionStatus: monitor.getStatus(),
|
|
3375
|
+
gatewayPort: config?.gateway?.port ?? null,
|
|
3376
|
+
lastEventAt: null,
|
|
3377
|
+
activeSessionCount: 0
|
|
3378
|
+
};
|
|
3379
|
+
});
|
|
3380
|
+
app.get("/api/exec-approvals/pending", async () => {
|
|
3381
|
+
const monitor = getOpenClawMonitor();
|
|
3382
|
+
if (!monitor) return [];
|
|
3383
|
+
return monitor.getExecApprovalService().getPendingApprovals();
|
|
3384
|
+
});
|
|
3385
|
+
app.get("/api/exec-approvals/history", async (request) => {
|
|
3386
|
+
const { limit = 50 } = request.query;
|
|
3387
|
+
const monitor = getOpenClawMonitor();
|
|
3388
|
+
if (!monitor) return [];
|
|
3389
|
+
return monitor.getExecApprovalService().getHistory(Number(limit));
|
|
3390
|
+
});
|
|
3391
|
+
app.put("/api/exec-approvals/:id/decision", async (request) => {
|
|
3392
|
+
const { id } = request.params;
|
|
3393
|
+
const { decision } = request.body;
|
|
3394
|
+
const monitor = getOpenClawMonitor();
|
|
3395
|
+
if (!monitor) return { error: "Monitor not available" };
|
|
3396
|
+
monitor.getExecApprovalService().handleDecision(id, decision);
|
|
3397
|
+
return { ok: true };
|
|
3398
|
+
});
|
|
3399
|
+
app.get("/api/allowlist", async () => {
|
|
3400
|
+
const monitor = getOpenClawMonitor();
|
|
3401
|
+
if (!monitor) return { patterns: [] };
|
|
3402
|
+
const patterns = monitor.getExecApprovalService().getRestrictedPatterns();
|
|
3403
|
+
return { patterns: patterns.map((p) => ({ pattern: p })) };
|
|
3404
|
+
});
|
|
3405
|
+
app.post("/api/allowlist", async (request) => {
|
|
3406
|
+
const { pattern } = request.body;
|
|
3407
|
+
const monitor = getOpenClawMonitor();
|
|
3408
|
+
if (!monitor) return { error: "Monitor not available" };
|
|
3409
|
+
const patterns = monitor.getExecApprovalService().addRestrictedPattern(pattern);
|
|
3410
|
+
return { patterns: patterns.map((p) => ({ pattern: p })) };
|
|
3411
|
+
});
|
|
3412
|
+
app.delete("/api/allowlist", async (request) => {
|
|
3413
|
+
const { pattern } = request.body;
|
|
3414
|
+
const monitor = getOpenClawMonitor();
|
|
3415
|
+
if (!monitor) return { error: "Monitor not available" };
|
|
3416
|
+
const patterns = monitor.getExecApprovalService().removeRestrictedPattern(pattern);
|
|
3417
|
+
return { patterns: patterns.map((p) => ({ pattern: p })) };
|
|
3418
|
+
});
|
|
3419
|
+
}
|
|
3420
|
+
|
|
3421
|
+
// src/server/index.ts
|
|
3422
|
+
async function createAppServer(port) {
|
|
3423
|
+
const app = Fastify({
|
|
3424
|
+
logger: false
|
|
3425
|
+
});
|
|
3426
|
+
await app.register(fastifyCors, { origin: "*" });
|
|
3427
|
+
await registerRoutes(app);
|
|
3428
|
+
const publicDir = getPublicDir();
|
|
3429
|
+
if (fs7.existsSync(publicDir) && fs7.readdirSync(publicDir).filter((f) => f !== ".gitkeep").length > 0) {
|
|
3430
|
+
await app.register(fastifyStatic, {
|
|
3431
|
+
root: publicDir,
|
|
3432
|
+
prefix: "/",
|
|
3433
|
+
wildcard: true
|
|
3434
|
+
});
|
|
3435
|
+
app.setNotFoundHandler(async (request, reply) => {
|
|
3436
|
+
if (request.url.startsWith("/api/") || request.url.startsWith("/socket.io/")) {
|
|
3437
|
+
return reply.status(404).send({ error: "Not found" });
|
|
3438
|
+
}
|
|
3439
|
+
return reply.sendFile("index.html");
|
|
3440
|
+
});
|
|
3441
|
+
} else {
|
|
3442
|
+
logger.warn(
|
|
3443
|
+
"No frontend build found in public/. Run 'pnpm build:web' first."
|
|
3444
|
+
);
|
|
3445
|
+
}
|
|
3446
|
+
await app.ready();
|
|
3447
|
+
const httpServer = app.server;
|
|
3448
|
+
const io2 = setupSocketIO(httpServer);
|
|
3449
|
+
const monitor = createOpenClawMonitor(io2);
|
|
3450
|
+
monitor.start();
|
|
3451
|
+
return { app, io: io2, httpServer, monitor };
|
|
3452
|
+
}
|
|
3453
|
+
|
|
3454
|
+
// src/db/migrate.ts
|
|
3455
|
+
import Database2 from "better-sqlite3";
|
|
3456
|
+
var DEFAULT_ACCESS_CONFIG = [
|
|
3457
|
+
{ category: "filesystem", key: "enabled", value: "true" },
|
|
3458
|
+
{ category: "mcp_servers", key: "enabled", value: "true" },
|
|
3459
|
+
{ category: "network", key: "enabled", value: "true" },
|
|
3460
|
+
{ category: "system_commands", key: "enabled", value: "false" }
|
|
3461
|
+
];
|
|
3462
|
+
function pushSchema() {
|
|
3463
|
+
ensureDataDir();
|
|
3464
|
+
const sqlite = new Database2(DB_PATH);
|
|
3465
|
+
sqlite.pragma("journal_mode = WAL");
|
|
3466
|
+
sqlite.exec(`
|
|
3467
|
+
CREATE TABLE IF NOT EXISTS command_logs (
|
|
3468
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
3469
|
+
command TEXT NOT NULL,
|
|
3470
|
+
status TEXT NOT NULL DEFAULT 'PENDING',
|
|
3471
|
+
threat_level TEXT NOT NULL DEFAULT 'NONE',
|
|
3472
|
+
timestamp TEXT NOT NULL DEFAULT (datetime('now')),
|
|
3473
|
+
session_id TEXT,
|
|
3474
|
+
decision_by TEXT
|
|
3475
|
+
);
|
|
3476
|
+
|
|
3477
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
3478
|
+
id TEXT PRIMARY KEY,
|
|
3479
|
+
started_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
3480
|
+
ended_at TEXT,
|
|
3481
|
+
status TEXT NOT NULL DEFAULT 'ACTIVE'
|
|
3482
|
+
);
|
|
3483
|
+
|
|
3484
|
+
CREATE TABLE IF NOT EXISTS access_config (
|
|
3485
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
3486
|
+
category TEXT NOT NULL,
|
|
3487
|
+
key TEXT NOT NULL,
|
|
3488
|
+
value TEXT NOT NULL,
|
|
3489
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
3490
|
+
);
|
|
3491
|
+
|
|
3492
|
+
CREATE TABLE IF NOT EXISTS openclaw_sessions (
|
|
3493
|
+
id TEXT PRIMARY KEY,
|
|
3494
|
+
started_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
3495
|
+
ended_at TEXT,
|
|
3496
|
+
status TEXT NOT NULL DEFAULT 'ACTIVE',
|
|
3497
|
+
model TEXT
|
|
3498
|
+
);
|
|
3499
|
+
|
|
3500
|
+
CREATE TABLE IF NOT EXISTS agent_activities (
|
|
3501
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
3502
|
+
openclaw_session_id TEXT NOT NULL,
|
|
3503
|
+
activity_type TEXT NOT NULL,
|
|
3504
|
+
detail TEXT NOT NULL,
|
|
3505
|
+
raw_payload TEXT NOT NULL DEFAULT '{}',
|
|
3506
|
+
threat_level TEXT NOT NULL DEFAULT 'NONE',
|
|
3507
|
+
timestamp TEXT NOT NULL DEFAULT (datetime('now')),
|
|
3508
|
+
tool_name TEXT,
|
|
3509
|
+
target_path TEXT
|
|
3510
|
+
);
|
|
3511
|
+
|
|
3512
|
+
CREATE INDEX IF NOT EXISTS idx_agent_activities_session
|
|
3513
|
+
ON agent_activities(openclaw_session_id);
|
|
3514
|
+
CREATE INDEX IF NOT EXISTS idx_agent_activities_threat
|
|
3515
|
+
ON agent_activities(threat_level);
|
|
3516
|
+
`);
|
|
3517
|
+
const columns = sqlite.prepare("PRAGMA table_info(agent_activities)").all();
|
|
3518
|
+
const columnNames = new Set(columns.map((c) => c.name));
|
|
3519
|
+
if (!columnNames.has("run_id")) {
|
|
3520
|
+
sqlite.exec("ALTER TABLE agent_activities ADD COLUMN run_id TEXT");
|
|
3521
|
+
}
|
|
3522
|
+
if (!columnNames.has("content_preview")) {
|
|
3523
|
+
sqlite.exec(
|
|
3524
|
+
"ALTER TABLE agent_activities ADD COLUMN content_preview TEXT"
|
|
3525
|
+
);
|
|
3526
|
+
}
|
|
3527
|
+
if (!columnNames.has("secrets_detected")) {
|
|
3528
|
+
sqlite.exec(
|
|
3529
|
+
"ALTER TABLE agent_activities ADD COLUMN secrets_detected TEXT"
|
|
3530
|
+
);
|
|
3531
|
+
}
|
|
3532
|
+
if (!columnNames.has("read_content_preview")) {
|
|
3533
|
+
sqlite.exec(
|
|
3534
|
+
"ALTER TABLE agent_activities ADD COLUMN read_content_preview TEXT"
|
|
3535
|
+
);
|
|
3536
|
+
}
|
|
3537
|
+
if (!columnNames.has("threat_findings")) {
|
|
3538
|
+
sqlite.exec(
|
|
3539
|
+
"ALTER TABLE agent_activities ADD COLUMN threat_findings TEXT"
|
|
3540
|
+
);
|
|
3541
|
+
}
|
|
3542
|
+
if (!columnNames.has("resolved")) {
|
|
3543
|
+
sqlite.exec(
|
|
3544
|
+
"ALTER TABLE agent_activities ADD COLUMN resolved INTEGER NOT NULL DEFAULT 0"
|
|
3545
|
+
);
|
|
3546
|
+
}
|
|
3547
|
+
if (!columnNames.has("resolved_at")) {
|
|
3548
|
+
sqlite.exec(
|
|
3549
|
+
"ALTER TABLE agent_activities ADD COLUMN resolved_at TEXT"
|
|
3550
|
+
);
|
|
3551
|
+
}
|
|
3552
|
+
sqlite.exec(
|
|
3553
|
+
"CREATE INDEX IF NOT EXISTS idx_agent_activities_run_id ON agent_activities(run_id)"
|
|
3554
|
+
);
|
|
3555
|
+
sqlite.exec(`
|
|
3556
|
+
CREATE TABLE IF NOT EXISTS restricted_patterns (
|
|
3557
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
3558
|
+
pattern TEXT NOT NULL UNIQUE,
|
|
3559
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
3560
|
+
);
|
|
3561
|
+
|
|
3562
|
+
CREATE TABLE IF NOT EXISTS exec_approvals (
|
|
3563
|
+
id TEXT PRIMARY KEY,
|
|
3564
|
+
command TEXT NOT NULL,
|
|
3565
|
+
cwd TEXT NOT NULL,
|
|
3566
|
+
security TEXT NOT NULL,
|
|
3567
|
+
session_key TEXT NOT NULL,
|
|
3568
|
+
requested_at TEXT NOT NULL,
|
|
3569
|
+
expires_at TEXT NOT NULL,
|
|
3570
|
+
decision TEXT,
|
|
3571
|
+
decided_by TEXT,
|
|
3572
|
+
decided_at TEXT,
|
|
3573
|
+
matched_pattern TEXT
|
|
3574
|
+
);
|
|
3575
|
+
|
|
3576
|
+
CREATE INDEX IF NOT EXISTS idx_exec_approvals_decision
|
|
3577
|
+
ON exec_approvals(decision);
|
|
3578
|
+
CREATE INDEX IF NOT EXISTS idx_exec_approvals_decided_at
|
|
3579
|
+
ON exec_approvals(decided_at);
|
|
3580
|
+
`);
|
|
3581
|
+
const count = sqlite.prepare("SELECT COUNT(*) as cnt FROM access_config").get();
|
|
3582
|
+
if (count.cnt === 0) {
|
|
3583
|
+
const insert = sqlite.prepare(
|
|
3584
|
+
"INSERT INTO access_config (category, key, value) VALUES (?, ?, ?)"
|
|
3585
|
+
);
|
|
3586
|
+
const seedAll = sqlite.transaction(() => {
|
|
3587
|
+
for (const entry of DEFAULT_ACCESS_CONFIG) {
|
|
3588
|
+
insert.run(entry.category, entry.key, entry.value);
|
|
3589
|
+
}
|
|
3590
|
+
});
|
|
3591
|
+
seedAll();
|
|
3592
|
+
}
|
|
3593
|
+
const unknownSession = sqlite.prepare("SELECT id FROM openclaw_sessions WHERE id = 'unknown'").get();
|
|
3594
|
+
if (unknownSession) {
|
|
3595
|
+
sqlite.exec(`
|
|
3596
|
+
DELETE FROM agent_activities WHERE openclaw_session_id = 'unknown';
|
|
3597
|
+
DELETE FROM openclaw_sessions WHERE id = 'unknown';
|
|
3598
|
+
`);
|
|
3599
|
+
}
|
|
3600
|
+
sqlite.exec("DELETE FROM access_config WHERE category = 'database'");
|
|
3601
|
+
sqlite.close();
|
|
3602
|
+
}
|
|
3603
|
+
|
|
3604
|
+
// src/lib/banner.ts
|
|
3605
|
+
import pc from "picocolors";
|
|
3606
|
+
function printBanner(port) {
|
|
3607
|
+
const lines = [
|
|
3608
|
+
[" _____ ___ ", "________"],
|
|
3609
|
+
[" / ___/____ _/ __/__ ", "/ ____/ /___ __ __"],
|
|
3610
|
+
[" \\__ \\/ __ `/ /_/ _ \\ ", "/ / / / __ `| | /| / /"],
|
|
3611
|
+
[" ___/ / /_/ / __/ __/", "/ /___/ / /_/ /| |/ |/ /"],
|
|
3612
|
+
["/____/\\__,_/_/ \\___/ ", "\\____/_/\\__,_/ |__/|__/"]
|
|
3613
|
+
];
|
|
3614
|
+
console.log();
|
|
3615
|
+
for (const [safe, claw] of lines) {
|
|
3616
|
+
console.log(pc.bold(pc.white(safe)) + pc.red(claw));
|
|
3617
|
+
}
|
|
3618
|
+
console.log();
|
|
3619
|
+
console.log(
|
|
3620
|
+
pc.bold(pc.white("Safe")) + pc.bold(pc.red("Claw")) + pc.dim(` v${VERSION}`) + " - AI Agent Security Dashboard"
|
|
3621
|
+
);
|
|
3622
|
+
console.log(pc.dim("\u2500".repeat(48)));
|
|
3623
|
+
console.log(`${pc.dim("Dashboard:")} ${pc.cyan(`http://localhost:${port}`)}`);
|
|
3624
|
+
console.log(`${pc.dim("Data dir:")} ~/.safeclaw/`);
|
|
3625
|
+
console.log();
|
|
3626
|
+
}
|
|
3627
|
+
|
|
3628
|
+
// src/commands/start.ts
|
|
3629
|
+
import pc2 from "picocolors";
|
|
3630
|
+
import open from "open";
|
|
3631
|
+
async function startCommand(options) {
|
|
3632
|
+
if (options.verbose) {
|
|
3633
|
+
setVerbose(true);
|
|
3634
|
+
logger.debug("Verbose logging enabled");
|
|
3635
|
+
}
|
|
3636
|
+
ensureDataDir();
|
|
3637
|
+
const config = readConfig();
|
|
3638
|
+
const port = options.port ? parseInt(options.port, 10) : config.port;
|
|
3639
|
+
if (isNaN(port) || port < 1024 || port > 65535) {
|
|
3640
|
+
process.stderr.write(
|
|
3641
|
+
pc2.red(`Invalid port: ${options.port}. Must be between 1024 and 65535.
|
|
3642
|
+
`)
|
|
3643
|
+
);
|
|
3644
|
+
process.exit(1);
|
|
3645
|
+
}
|
|
3646
|
+
pushSchema();
|
|
3647
|
+
printBanner(port);
|
|
3648
|
+
try {
|
|
3649
|
+
const { app, monitor } = await createAppServer(port);
|
|
3650
|
+
await app.listen({ port, host: "0.0.0.0" });
|
|
3651
|
+
logger.info(`Server listening on http://localhost:${port}`);
|
|
3652
|
+
const shouldOpen = options.open && config.autoOpenBrowser;
|
|
3653
|
+
if (shouldOpen) {
|
|
3654
|
+
await open(`http://localhost:${port}`);
|
|
3655
|
+
}
|
|
3656
|
+
const shutdown = async () => {
|
|
3657
|
+
logger.info("Shutting down SafeClaw...");
|
|
3658
|
+
monitor.stop();
|
|
3659
|
+
await app.close();
|
|
3660
|
+
process.exit(0);
|
|
3661
|
+
};
|
|
3662
|
+
process.on("SIGINT", shutdown);
|
|
3663
|
+
process.on("SIGTERM", shutdown);
|
|
3664
|
+
} catch (err) {
|
|
3665
|
+
const error = err;
|
|
3666
|
+
if (error.code === "EADDRINUSE") {
|
|
3667
|
+
process.stderr.write(
|
|
3668
|
+
pc2.red(`
|
|
3669
|
+
Port ${port} is already in use.
|
|
3670
|
+
`) + pc2.dim(`Try: safeclaw start --port ${port + 1}
|
|
3671
|
+
`) + pc2.dim(`Or stop the process using port ${port} first.
|
|
3672
|
+
`)
|
|
3673
|
+
);
|
|
3674
|
+
process.exit(1);
|
|
3675
|
+
}
|
|
3676
|
+
if (error.code === "EACCES") {
|
|
3677
|
+
process.stderr.write(
|
|
3678
|
+
pc2.red(`
|
|
3679
|
+
Permission denied for port ${port}.
|
|
3680
|
+
`) + pc2.dim(`Ports below 1024 require elevated privileges.
|
|
3681
|
+
`) + pc2.dim(`Try: safeclaw start --port 54335
|
|
3682
|
+
`)
|
|
3683
|
+
);
|
|
3684
|
+
process.exit(1);
|
|
3685
|
+
}
|
|
3686
|
+
throw err;
|
|
3687
|
+
}
|
|
3688
|
+
}
|
|
3689
|
+
|
|
3690
|
+
// src/commands/reset.ts
|
|
3691
|
+
import fs8 from "fs";
|
|
3692
|
+
import readline from "readline/promises";
|
|
3693
|
+
import pc3 from "picocolors";
|
|
3694
|
+
async function resetCommand(options) {
|
|
3695
|
+
if (!options.force) {
|
|
3696
|
+
const rl = readline.createInterface({
|
|
3697
|
+
input: process.stdin,
|
|
3698
|
+
output: process.stdout
|
|
3699
|
+
});
|
|
3700
|
+
const answer = await rl.question(
|
|
3701
|
+
pc3.yellow("This will delete the database and reset config to defaults.\n") + "Are you sure? (y/N) "
|
|
3702
|
+
);
|
|
3703
|
+
rl.close();
|
|
3704
|
+
if (answer.toLowerCase() !== "y" && answer.toLowerCase() !== "yes") {
|
|
3705
|
+
console.log("Reset cancelled.");
|
|
3706
|
+
return;
|
|
3707
|
+
}
|
|
3708
|
+
}
|
|
3709
|
+
console.log(pc3.bold("Resetting SafeClaw..."));
|
|
3710
|
+
if (fs8.existsSync(DB_PATH)) {
|
|
3711
|
+
fs8.unlinkSync(DB_PATH);
|
|
3712
|
+
const walPath = DB_PATH + "-wal";
|
|
3713
|
+
const shmPath = DB_PATH + "-shm";
|
|
3714
|
+
if (fs8.existsSync(walPath)) fs8.unlinkSync(walPath);
|
|
3715
|
+
if (fs8.existsSync(shmPath)) fs8.unlinkSync(shmPath);
|
|
3716
|
+
console.log(pc3.green(" Database deleted."));
|
|
3717
|
+
} else {
|
|
3718
|
+
console.log(pc3.dim(" No database found, skipping."));
|
|
3719
|
+
}
|
|
3720
|
+
resetConfig();
|
|
3721
|
+
console.log(pc3.green(" Config reset to defaults."));
|
|
3722
|
+
console.log(
|
|
3723
|
+
pc3.bold("\nDone.") + " Run " + pc3.cyan("safeclaw start") + " to start fresh."
|
|
3724
|
+
);
|
|
3725
|
+
}
|
|
3726
|
+
|
|
3727
|
+
// src/commands/status.ts
|
|
3728
|
+
import fs9 from "fs";
|
|
3729
|
+
import Database3 from "better-sqlite3";
|
|
3730
|
+
import pc4 from "picocolors";
|
|
3731
|
+
async function statusCommand(options) {
|
|
3732
|
+
const exists = fs9.existsSync(SAFECLAW_DIR);
|
|
3733
|
+
if (!exists) {
|
|
3734
|
+
if (options.json) {
|
|
3735
|
+
console.log(JSON.stringify({ initialized: false }, null, 2));
|
|
3736
|
+
} else {
|
|
3737
|
+
console.log(
|
|
3738
|
+
pc4.yellow("SafeClaw is not initialized.") + " Run " + pc4.cyan("safeclaw start") + " first."
|
|
3739
|
+
);
|
|
3740
|
+
}
|
|
3741
|
+
return;
|
|
3742
|
+
}
|
|
3743
|
+
const config = readConfig();
|
|
3744
|
+
let logCount = 0;
|
|
3745
|
+
let activityCount = 0;
|
|
3746
|
+
const dbExists = fs9.existsSync(DB_PATH);
|
|
3747
|
+
if (dbExists) {
|
|
3748
|
+
try {
|
|
3749
|
+
const sqlite = new Database3(DB_PATH, { readonly: true });
|
|
3750
|
+
const cmdRow = sqlite.prepare("SELECT COUNT(*) as count FROM command_logs").get();
|
|
3751
|
+
logCount = cmdRow.count;
|
|
3752
|
+
const actRow = sqlite.prepare("SELECT COUNT(*) as count FROM agent_activities").get();
|
|
3753
|
+
activityCount = actRow.count;
|
|
3754
|
+
sqlite.close();
|
|
3755
|
+
} catch {
|
|
3756
|
+
logCount = -1;
|
|
3757
|
+
activityCount = -1;
|
|
3758
|
+
}
|
|
3759
|
+
}
|
|
3760
|
+
const openclawConfigExists = fs9.existsSync(OPENCLAW_CONFIG_PATH);
|
|
3761
|
+
if (options.json) {
|
|
3762
|
+
const data = {
|
|
3763
|
+
version: VERSION,
|
|
3764
|
+
initialized: true,
|
|
3765
|
+
dataDir: SAFECLAW_DIR,
|
|
3766
|
+
database: dbExists ? "exists" : "not_found",
|
|
3767
|
+
config: fs9.existsSync(CONFIG_PATH) ? "exists" : "not_found",
|
|
3768
|
+
port: config.port,
|
|
3769
|
+
autoOpenBrowser: config.autoOpenBrowser,
|
|
3770
|
+
premium: config.premium,
|
|
3771
|
+
commandLogs: logCount,
|
|
3772
|
+
agentActivities: activityCount,
|
|
3773
|
+
openclawConfigured: openclawConfigExists
|
|
3774
|
+
};
|
|
3775
|
+
console.log(JSON.stringify(data, null, 2));
|
|
3776
|
+
return;
|
|
3777
|
+
}
|
|
3778
|
+
console.log(pc4.bold("\nSafeClaw Status") + pc4.dim(` v${VERSION}`));
|
|
3779
|
+
console.log(pc4.dim("\u2500".repeat(40)));
|
|
3780
|
+
console.log(` ${pc4.dim("Data dir:")} ${SAFECLAW_DIR}`);
|
|
3781
|
+
console.log(
|
|
3782
|
+
` ${pc4.dim("Database:")} ${dbExists ? pc4.green("exists") : pc4.red("not found")}`
|
|
3783
|
+
);
|
|
3784
|
+
console.log(
|
|
3785
|
+
` ${pc4.dim("Config:")} ${fs9.existsSync(CONFIG_PATH) ? pc4.green("exists") : pc4.red("not found")}`
|
|
3786
|
+
);
|
|
3787
|
+
console.log(` ${pc4.dim("Port:")} ${config.port}`);
|
|
3788
|
+
console.log(` ${pc4.dim("Auto-open:")} ${config.autoOpenBrowser ? "Yes" : "No"}`);
|
|
3789
|
+
console.log(` ${pc4.dim("Premium:")} ${config.premium ? "Yes" : "No"}`);
|
|
3790
|
+
console.log(
|
|
3791
|
+
` ${pc4.dim("Cmd logs:")} ${logCount >= 0 ? logCount.toLocaleString() : pc4.red("error reading")}`
|
|
3792
|
+
);
|
|
3793
|
+
console.log(
|
|
3794
|
+
` ${pc4.dim("Activities:")} ${activityCount >= 0 ? activityCount.toLocaleString() : pc4.red("error reading")}`
|
|
3795
|
+
);
|
|
3796
|
+
console.log(
|
|
3797
|
+
` ${pc4.dim("OpenClaw:")} ${openclawConfigExists ? pc4.green("configured") : pc4.yellow("not found")}`
|
|
3798
|
+
);
|
|
3799
|
+
console.log();
|
|
3800
|
+
}
|
|
3801
|
+
|
|
3802
|
+
// src/commands/doctor.ts
|
|
3803
|
+
import fs10 from "fs";
|
|
3804
|
+
import net from "net";
|
|
3805
|
+
import Database4 from "better-sqlite3";
|
|
3806
|
+
import pc5 from "picocolors";
|
|
3807
|
+
function checkNodeVersion() {
|
|
3808
|
+
const major = parseInt(process.versions.node.split(".")[0], 10);
|
|
3809
|
+
if (major >= 20) {
|
|
3810
|
+
return {
|
|
3811
|
+
name: "Node.js version",
|
|
3812
|
+
status: "pass",
|
|
3813
|
+
message: `v${process.versions.node}`
|
|
3814
|
+
};
|
|
3815
|
+
}
|
|
3816
|
+
return {
|
|
3817
|
+
name: "Node.js version",
|
|
3818
|
+
status: "fail",
|
|
3819
|
+
message: `v${process.versions.node} (requires >= 20.0.0)`
|
|
3820
|
+
};
|
|
3821
|
+
}
|
|
3822
|
+
function checkDataDir() {
|
|
3823
|
+
try {
|
|
3824
|
+
ensureDataDir();
|
|
3825
|
+
fs10.accessSync(SAFECLAW_DIR, fs10.constants.W_OK);
|
|
3826
|
+
return {
|
|
3827
|
+
name: "Data directory writable",
|
|
3828
|
+
status: "pass",
|
|
3829
|
+
message: SAFECLAW_DIR
|
|
3830
|
+
};
|
|
3831
|
+
} catch {
|
|
3832
|
+
return {
|
|
3833
|
+
name: "Data directory writable",
|
|
3834
|
+
status: "fail",
|
|
3835
|
+
message: `Cannot write to ${SAFECLAW_DIR}`
|
|
3836
|
+
};
|
|
3837
|
+
}
|
|
3838
|
+
}
|
|
3839
|
+
function checkDatabase() {
|
|
3840
|
+
if (!fs10.existsSync(DB_PATH)) {
|
|
3841
|
+
return {
|
|
3842
|
+
name: "Database",
|
|
3843
|
+
status: "warn",
|
|
3844
|
+
message: "Not created yet (run 'safeclaw start' first)"
|
|
3845
|
+
};
|
|
3846
|
+
}
|
|
3847
|
+
try {
|
|
3848
|
+
const sqlite = new Database4(DB_PATH, { readonly: true });
|
|
3849
|
+
const tables = sqlite.prepare("SELECT name FROM sqlite_master WHERE type='table'").all();
|
|
3850
|
+
sqlite.close();
|
|
3851
|
+
const expected = [
|
|
3852
|
+
"command_logs",
|
|
3853
|
+
"sessions",
|
|
3854
|
+
"access_config",
|
|
3855
|
+
"openclaw_sessions",
|
|
3856
|
+
"agent_activities",
|
|
3857
|
+
"restricted_patterns",
|
|
3858
|
+
"exec_approvals"
|
|
3859
|
+
];
|
|
3860
|
+
const tableNames = new Set(tables.map((t) => t.name));
|
|
3861
|
+
const missing = expected.filter((t) => !tableNames.has(t));
|
|
3862
|
+
if (missing.length > 0) {
|
|
3863
|
+
return {
|
|
3864
|
+
name: "Database",
|
|
3865
|
+
status: "warn",
|
|
3866
|
+
message: `Missing tables: ${missing.join(", ")}`
|
|
3867
|
+
};
|
|
3868
|
+
}
|
|
3869
|
+
return {
|
|
3870
|
+
name: "Database",
|
|
3871
|
+
status: "pass",
|
|
3872
|
+
message: `${tables.length} tables`
|
|
3873
|
+
};
|
|
3874
|
+
} catch (err) {
|
|
3875
|
+
return {
|
|
3876
|
+
name: "Database",
|
|
3877
|
+
status: "fail",
|
|
3878
|
+
message: `Cannot open database: ${err.message}`
|
|
3879
|
+
};
|
|
3880
|
+
}
|
|
3881
|
+
}
|
|
3882
|
+
function checkConfig() {
|
|
3883
|
+
if (!fs10.existsSync(CONFIG_PATH)) {
|
|
3884
|
+
return {
|
|
3885
|
+
name: "Config file",
|
|
3886
|
+
status: "warn",
|
|
3887
|
+
message: "Not created yet (will use defaults)"
|
|
3888
|
+
};
|
|
3889
|
+
}
|
|
3890
|
+
try {
|
|
3891
|
+
readConfig();
|
|
3892
|
+
return { name: "Config file", status: "pass", message: CONFIG_PATH };
|
|
3893
|
+
} catch (err) {
|
|
3894
|
+
return {
|
|
3895
|
+
name: "Config file",
|
|
3896
|
+
status: "fail",
|
|
3897
|
+
message: `Invalid config: ${err.message}`
|
|
3898
|
+
};
|
|
3899
|
+
}
|
|
3900
|
+
}
|
|
3901
|
+
function isPortAvailable(port) {
|
|
3902
|
+
return new Promise((resolve) => {
|
|
3903
|
+
const server = net.createServer();
|
|
3904
|
+
server.once("error", () => resolve(false));
|
|
3905
|
+
server.once("listening", () => {
|
|
3906
|
+
server.close(() => resolve(true));
|
|
3907
|
+
});
|
|
3908
|
+
server.listen(port, "0.0.0.0");
|
|
3909
|
+
});
|
|
3910
|
+
}
|
|
3911
|
+
async function checkPort() {
|
|
3912
|
+
const config = readConfig();
|
|
3913
|
+
const port = config.port;
|
|
3914
|
+
const available = await isPortAvailable(port);
|
|
3915
|
+
if (available) {
|
|
3916
|
+
return { name: `Port ${port} available`, status: "pass", message: "" };
|
|
3917
|
+
}
|
|
3918
|
+
return {
|
|
3919
|
+
name: `Port ${port} available`,
|
|
3920
|
+
status: "warn",
|
|
3921
|
+
message: `Port ${port} is in use (server may already be running)`
|
|
3922
|
+
};
|
|
3923
|
+
}
|
|
3924
|
+
function checkOpenClawConfig() {
|
|
3925
|
+
if (!fs10.existsSync(OPENCLAW_DIR)) {
|
|
3926
|
+
return {
|
|
3927
|
+
name: "OpenClaw directory",
|
|
3928
|
+
status: "warn",
|
|
3929
|
+
message: `${OPENCLAW_DIR} not found`
|
|
3930
|
+
};
|
|
3931
|
+
}
|
|
3932
|
+
if (!fs10.existsSync(OPENCLAW_CONFIG_PATH)) {
|
|
3933
|
+
return {
|
|
3934
|
+
name: "OpenClaw config",
|
|
3935
|
+
status: "warn",
|
|
3936
|
+
message: "Config not found (OpenClaw may not be installed)"
|
|
3937
|
+
};
|
|
3938
|
+
}
|
|
3939
|
+
return {
|
|
3940
|
+
name: "OpenClaw config",
|
|
3941
|
+
status: "pass",
|
|
3942
|
+
message: OPENCLAW_CONFIG_PATH
|
|
3943
|
+
};
|
|
3944
|
+
}
|
|
3945
|
+
function isPortInUse(port) {
|
|
3946
|
+
return new Promise((resolve) => {
|
|
3947
|
+
const socket = new net.Socket();
|
|
3948
|
+
socket.setTimeout(2e3);
|
|
3949
|
+
socket.once("connect", () => {
|
|
3950
|
+
socket.destroy();
|
|
3951
|
+
resolve(true);
|
|
3952
|
+
});
|
|
3953
|
+
socket.once("timeout", () => {
|
|
3954
|
+
socket.destroy();
|
|
3955
|
+
resolve(false);
|
|
3956
|
+
});
|
|
3957
|
+
socket.once("error", () => {
|
|
3958
|
+
resolve(false);
|
|
3959
|
+
});
|
|
3960
|
+
socket.connect(port, "127.0.0.1");
|
|
3961
|
+
});
|
|
3962
|
+
}
|
|
3963
|
+
async function checkOpenClawGateway() {
|
|
3964
|
+
if (!fs10.existsSync(OPENCLAW_CONFIG_PATH)) {
|
|
3965
|
+
return {
|
|
3966
|
+
name: "OpenClaw gateway",
|
|
3967
|
+
status: "warn",
|
|
3968
|
+
message: "Skipped (no OpenClaw config)"
|
|
3969
|
+
};
|
|
3970
|
+
}
|
|
3971
|
+
try {
|
|
3972
|
+
const raw = JSON.parse(fs10.readFileSync(OPENCLAW_CONFIG_PATH, "utf-8"));
|
|
3973
|
+
const port = raw?.gateway?.port ?? 18789;
|
|
3974
|
+
const reachable = await isPortInUse(port);
|
|
3975
|
+
if (reachable) {
|
|
3976
|
+
return {
|
|
3977
|
+
name: "OpenClaw gateway",
|
|
3978
|
+
status: "pass",
|
|
3979
|
+
message: `Reachable on port ${port}`
|
|
3980
|
+
};
|
|
3981
|
+
}
|
|
3982
|
+
return {
|
|
3983
|
+
name: "OpenClaw gateway",
|
|
3984
|
+
status: "warn",
|
|
3985
|
+
message: `Not reachable on port ${port}`
|
|
3986
|
+
};
|
|
3987
|
+
} catch {
|
|
3988
|
+
return {
|
|
3989
|
+
name: "OpenClaw gateway",
|
|
3990
|
+
status: "warn",
|
|
3991
|
+
message: "Could not read OpenClaw config"
|
|
3992
|
+
};
|
|
3993
|
+
}
|
|
3994
|
+
}
|
|
3995
|
+
function checkLogDir() {
|
|
3996
|
+
try {
|
|
3997
|
+
if (!fs10.existsSync(LOGS_DIR)) {
|
|
3998
|
+
fs10.mkdirSync(LOGS_DIR, { recursive: true });
|
|
3999
|
+
}
|
|
4000
|
+
fs10.accessSync(LOGS_DIR, fs10.constants.W_OK);
|
|
4001
|
+
return {
|
|
4002
|
+
name: "Log directory writable",
|
|
4003
|
+
status: "pass",
|
|
4004
|
+
message: LOGS_DIR
|
|
4005
|
+
};
|
|
4006
|
+
} catch {
|
|
4007
|
+
return {
|
|
4008
|
+
name: "Log directory writable",
|
|
4009
|
+
status: "fail",
|
|
4010
|
+
message: `Cannot write to ${LOGS_DIR}`
|
|
4011
|
+
};
|
|
4012
|
+
}
|
|
4013
|
+
}
|
|
4014
|
+
async function doctorCommand() {
|
|
4015
|
+
console.log(pc5.bold("\nSafeClaw Doctor") + pc5.dim(` v${VERSION}`));
|
|
4016
|
+
console.log(pc5.dim("\u2500".repeat(40)));
|
|
4017
|
+
console.log();
|
|
4018
|
+
const checks = [];
|
|
4019
|
+
checks.push(checkNodeVersion());
|
|
4020
|
+
checks.push(checkDataDir());
|
|
4021
|
+
checks.push(checkDatabase());
|
|
4022
|
+
checks.push(checkConfig());
|
|
4023
|
+
checks.push(await checkPort());
|
|
4024
|
+
checks.push(checkOpenClawConfig());
|
|
4025
|
+
checks.push(await checkOpenClawGateway());
|
|
4026
|
+
checks.push(checkLogDir());
|
|
4027
|
+
let hasFailures = false;
|
|
4028
|
+
for (const check of checks) {
|
|
4029
|
+
const icon = check.status === "pass" ? pc5.green("PASS") : check.status === "warn" ? pc5.yellow("WARN") : pc5.red("FAIL");
|
|
4030
|
+
console.log(` ${icon} ${check.name}`);
|
|
4031
|
+
if (check.status !== "pass") {
|
|
4032
|
+
console.log(pc5.dim(` ${check.message}`));
|
|
4033
|
+
}
|
|
4034
|
+
if (check.status === "fail") hasFailures = true;
|
|
4035
|
+
}
|
|
4036
|
+
console.log();
|
|
4037
|
+
const passCount = checks.filter((c) => c.status === "pass").length;
|
|
4038
|
+
const warnCount = checks.filter((c) => c.status === "warn").length;
|
|
4039
|
+
const failCount = checks.filter((c) => c.status === "fail").length;
|
|
4040
|
+
console.log(
|
|
4041
|
+
`${pc5.green(`${passCount} passed`)}` + (warnCount ? `, ${pc5.yellow(`${warnCount} warnings`)}` : "") + (failCount ? `, ${pc5.red(`${failCount} failed`)}` : "")
|
|
4042
|
+
);
|
|
4043
|
+
console.log();
|
|
4044
|
+
if (hasFailures) process.exit(1);
|
|
4045
|
+
}
|
|
4046
|
+
|
|
4047
|
+
// src/commands/config.ts
|
|
4048
|
+
import pc6 from "picocolors";
|
|
4049
|
+
var SETTABLE_KEYS = {
|
|
4050
|
+
port: {
|
|
4051
|
+
type: "number",
|
|
4052
|
+
description: "Server port (1024-65535)",
|
|
4053
|
+
validate: (v) => {
|
|
4054
|
+
const n = parseInt(v, 10);
|
|
4055
|
+
if (isNaN(n) || n < 1024 || n > 65535)
|
|
4056
|
+
return "Port must be an integer between 1024 and 65535";
|
|
4057
|
+
return null;
|
|
4058
|
+
}
|
|
4059
|
+
},
|
|
4060
|
+
autoOpenBrowser: {
|
|
4061
|
+
type: "boolean",
|
|
4062
|
+
description: "Auto-open browser on start (true/false)",
|
|
4063
|
+
validate: (v) => {
|
|
4064
|
+
if (v !== "true" && v !== "false")
|
|
4065
|
+
return 'Value must be "true" or "false"';
|
|
4066
|
+
return null;
|
|
4067
|
+
}
|
|
4068
|
+
},
|
|
4069
|
+
premium: {
|
|
4070
|
+
type: "boolean",
|
|
4071
|
+
description: "Premium features enabled (true/false)",
|
|
4072
|
+
validate: (v) => {
|
|
4073
|
+
if (v !== "true" && v !== "false")
|
|
4074
|
+
return 'Value must be "true" or "false"';
|
|
4075
|
+
return null;
|
|
4076
|
+
}
|
|
4077
|
+
}
|
|
4078
|
+
};
|
|
4079
|
+
async function configListCommand() {
|
|
4080
|
+
ensureDataDir();
|
|
4081
|
+
const config = readConfig();
|
|
4082
|
+
console.log(pc6.bold("\nSafeClaw Configuration"));
|
|
4083
|
+
console.log(pc6.dim("\u2500".repeat(40)));
|
|
4084
|
+
for (const [key, value] of Object.entries(config)) {
|
|
4085
|
+
const meta = SETTABLE_KEYS[key];
|
|
4086
|
+
const displayValue = typeof value === "string" ? `"${value}"` : String(value);
|
|
4087
|
+
const settable = meta ? "" : pc6.dim(" (read-only)");
|
|
4088
|
+
console.log(` ${pc6.cyan(key)}: ${displayValue}${settable}`);
|
|
4089
|
+
}
|
|
4090
|
+
console.log();
|
|
4091
|
+
}
|
|
4092
|
+
async function configGetCommand(key) {
|
|
4093
|
+
ensureDataDir();
|
|
4094
|
+
const config = readConfig();
|
|
4095
|
+
if (!(key in config)) {
|
|
4096
|
+
process.stderr.write(
|
|
4097
|
+
pc6.red(`Unknown config key: ${key}
|
|
4098
|
+
`) + pc6.dim(`Available keys: ${Object.keys(config).join(", ")}
|
|
4099
|
+
`)
|
|
4100
|
+
);
|
|
4101
|
+
process.exit(1);
|
|
4102
|
+
}
|
|
4103
|
+
const value = config[key];
|
|
4104
|
+
console.log(value);
|
|
4105
|
+
}
|
|
4106
|
+
async function configSetCommand(key, value) {
|
|
4107
|
+
ensureDataDir();
|
|
4108
|
+
const config = readConfig();
|
|
4109
|
+
const meta = SETTABLE_KEYS[key];
|
|
4110
|
+
if (!meta) {
|
|
4111
|
+
if (key in config) {
|
|
4112
|
+
process.stderr.write(pc6.red(`Config key "${key}" is read-only.
|
|
4113
|
+
`));
|
|
4114
|
+
} else {
|
|
4115
|
+
process.stderr.write(
|
|
4116
|
+
pc6.red(`Unknown config key: ${key}
|
|
4117
|
+
`) + pc6.dim(
|
|
4118
|
+
`Settable keys: ${Object.keys(SETTABLE_KEYS).join(", ")}
|
|
4119
|
+
`
|
|
4120
|
+
)
|
|
4121
|
+
);
|
|
4122
|
+
}
|
|
4123
|
+
process.exit(1);
|
|
4124
|
+
}
|
|
4125
|
+
if (meta.validate) {
|
|
4126
|
+
const error = meta.validate(value);
|
|
4127
|
+
if (error) {
|
|
4128
|
+
process.stderr.write(pc6.red(`Invalid value: ${error}
|
|
4129
|
+
`));
|
|
4130
|
+
process.exit(1);
|
|
4131
|
+
}
|
|
4132
|
+
}
|
|
4133
|
+
let coerced = value;
|
|
4134
|
+
if (meta.type === "number") coerced = parseInt(value, 10);
|
|
4135
|
+
if (meta.type === "boolean") coerced = value === "true";
|
|
4136
|
+
const updated = { ...config, [key]: coerced };
|
|
4137
|
+
writeConfig(updated);
|
|
4138
|
+
console.log(pc6.green(`Set ${key} = ${coerced}`));
|
|
4139
|
+
}
|
|
4140
|
+
|
|
4141
|
+
// src/commands/logs.ts
|
|
4142
|
+
import fs11 from "fs";
|
|
4143
|
+
import pc7 from "picocolors";
|
|
4144
|
+
async function logsCommand(options) {
|
|
4145
|
+
if (options.clear) {
|
|
4146
|
+
await clearLogs();
|
|
4147
|
+
return;
|
|
4148
|
+
}
|
|
4149
|
+
if (!fs11.existsSync(DEBUG_LOG_PATH)) {
|
|
4150
|
+
console.log(
|
|
4151
|
+
pc7.yellow("No log file found.") + " Run " + pc7.cyan("safeclaw start") + " to generate logs."
|
|
4152
|
+
);
|
|
4153
|
+
return;
|
|
4154
|
+
}
|
|
4155
|
+
if (options.follow) {
|
|
4156
|
+
await followLogs();
|
|
4157
|
+
} else {
|
|
4158
|
+
await tailLogs(parseInt(options.lines, 10) || 50);
|
|
4159
|
+
}
|
|
4160
|
+
}
|
|
4161
|
+
async function tailLogs(lineCount) {
|
|
4162
|
+
const content = fs11.readFileSync(DEBUG_LOG_PATH, "utf-8");
|
|
4163
|
+
const lines = content.split("\n").filter(Boolean);
|
|
4164
|
+
const tail = lines.slice(-lineCount);
|
|
4165
|
+
if (tail.length === 0) {
|
|
4166
|
+
console.log(pc7.dim("Log file is empty."));
|
|
4167
|
+
return;
|
|
4168
|
+
}
|
|
4169
|
+
console.log(
|
|
4170
|
+
pc7.dim(`Showing last ${tail.length} lines from ${DEBUG_LOG_PATH}
|
|
4171
|
+
`)
|
|
4172
|
+
);
|
|
4173
|
+
for (const line of tail) {
|
|
4174
|
+
process.stdout.write(line + "\n");
|
|
4175
|
+
}
|
|
4176
|
+
}
|
|
4177
|
+
async function followLogs() {
|
|
4178
|
+
console.log(pc7.dim(`Following ${DEBUG_LOG_PATH} (Ctrl+C to stop)
|
|
4179
|
+
`));
|
|
4180
|
+
if (fs11.existsSync(DEBUG_LOG_PATH)) {
|
|
4181
|
+
const content = fs11.readFileSync(DEBUG_LOG_PATH, "utf-8");
|
|
4182
|
+
const lines = content.split("\n").filter(Boolean);
|
|
4183
|
+
const tail = lines.slice(-20);
|
|
4184
|
+
for (const line of tail) {
|
|
4185
|
+
process.stdout.write(line + "\n");
|
|
4186
|
+
}
|
|
4187
|
+
}
|
|
4188
|
+
let position = fs11.existsSync(DEBUG_LOG_PATH) ? fs11.statSync(DEBUG_LOG_PATH).size : 0;
|
|
4189
|
+
const watcher = fs11.watch(DEBUG_LOG_PATH, () => {
|
|
4190
|
+
try {
|
|
4191
|
+
const stat = fs11.statSync(DEBUG_LOG_PATH);
|
|
4192
|
+
if (stat.size > position) {
|
|
4193
|
+
const fd = fs11.openSync(DEBUG_LOG_PATH, "r");
|
|
4194
|
+
const buffer = Buffer.alloc(stat.size - position);
|
|
4195
|
+
fs11.readSync(fd, buffer, 0, buffer.length, position);
|
|
4196
|
+
fs11.closeSync(fd);
|
|
4197
|
+
process.stdout.write(buffer.toString("utf-8"));
|
|
4198
|
+
position = stat.size;
|
|
4199
|
+
} else if (stat.size < position) {
|
|
4200
|
+
position = 0;
|
|
4201
|
+
}
|
|
4202
|
+
} catch {
|
|
4203
|
+
}
|
|
4204
|
+
});
|
|
4205
|
+
process.on("SIGINT", () => {
|
|
4206
|
+
watcher.close();
|
|
4207
|
+
console.log(pc7.dim("\nStopped following logs."));
|
|
4208
|
+
process.exit(0);
|
|
4209
|
+
});
|
|
4210
|
+
await new Promise(() => {
|
|
4211
|
+
});
|
|
4212
|
+
}
|
|
4213
|
+
async function clearLogs() {
|
|
4214
|
+
if (!fs11.existsSync(DEBUG_LOG_PATH)) {
|
|
4215
|
+
console.log(pc7.dim("No log file to clear."));
|
|
4216
|
+
return;
|
|
4217
|
+
}
|
|
4218
|
+
fs11.writeFileSync(DEBUG_LOG_PATH, "");
|
|
4219
|
+
console.log(pc7.green("Log file cleared."));
|
|
4220
|
+
}
|
|
4221
|
+
|
|
4222
|
+
// src/main.ts
|
|
4223
|
+
import pc8 from "picocolors";
|
|
4224
|
+
var program = new Command();
|
|
4225
|
+
program.name("safeclaw").description("Security management dashboard for AI agents").version(VERSION, "-V, --version");
|
|
4226
|
+
program.command("start").description("Launch the SafeClaw dashboard server").option("-p, --port <port>", "override server port").option("--no-open", "skip auto-opening browser").option("--verbose", "enable debug logging to console", false).action(async (options) => {
|
|
4227
|
+
await startCommand(options);
|
|
4228
|
+
});
|
|
4229
|
+
program.command("reset").description("Reset database and configuration to defaults").option("--force", "skip confirmation prompt", false).action(async (options) => {
|
|
4230
|
+
await resetCommand(options);
|
|
4231
|
+
});
|
|
4232
|
+
program.command("status").description("Show current SafeClaw status").option("--json", "output as JSON for scripting", false).action(async (options) => {
|
|
4233
|
+
await statusCommand(options);
|
|
4234
|
+
});
|
|
4235
|
+
program.command("doctor").description("Check system health and prerequisites").action(async () => {
|
|
4236
|
+
await doctorCommand();
|
|
4237
|
+
});
|
|
4238
|
+
var configCmd = program.command("config").description("Manage SafeClaw configuration");
|
|
4239
|
+
configCmd.command("list").description("Show all configuration values").action(async () => {
|
|
4240
|
+
await configListCommand();
|
|
4241
|
+
});
|
|
4242
|
+
configCmd.command("get").description("Get a configuration value").argument("<key>", "configuration key").action(async (key) => {
|
|
4243
|
+
await configGetCommand(key);
|
|
4244
|
+
});
|
|
4245
|
+
configCmd.command("set").description("Set a configuration value").argument("<key>", "configuration key").argument("<value>", "new value").action(async (key, value) => {
|
|
4246
|
+
await configSetCommand(key, value);
|
|
4247
|
+
});
|
|
4248
|
+
program.command("logs").description("View SafeClaw debug logs").option("-n, --lines <count>", "number of lines to show", "50").option("-f, --follow", "follow log output in real-time", false).option("--clear", "clear the log file", false).action(async (options) => {
|
|
4249
|
+
await logsCommand(options);
|
|
4250
|
+
});
|
|
4251
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
4252
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
4253
|
+
process.stderr.write(pc8.red(`Fatal error: ${error.message}
|
|
4254
|
+
`));
|
|
4255
|
+
process.exit(1);
|
|
4256
|
+
});
|
|
4257
|
+
//# sourceMappingURL=main.js.map
|