safeclaw 0.1.5 → 0.1.6

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/dist/main.js CHANGED
@@ -8,13 +8,13 @@ var __export = (target, all) => {
8
8
  import { Command } from "commander";
9
9
 
10
10
  // src/lib/version.ts
11
- var VERSION = "0.1.5";
11
+ var VERSION = "0.1.6";
12
12
 
13
13
  // src/server/index.ts
14
14
  import Fastify from "fastify";
15
15
  import fastifyStatic from "@fastify/static";
16
16
  import fastifyCors from "@fastify/cors";
17
- import fs8 from "fs";
17
+ import fs10 from "fs";
18
18
 
19
19
  // src/lib/paths.ts
20
20
  import fs from "fs";
@@ -32,6 +32,7 @@ var OPENCLAW_IDENTITY_DIR = path.join(OPENCLAW_DIR, "identity");
32
32
  var OPENCLAW_DEVICE_JSON = path.join(OPENCLAW_IDENTITY_DIR, "device.json");
33
33
  var OPENCLAW_DEVICE_AUTH_JSON = path.join(OPENCLAW_IDENTITY_DIR, "device-auth.json");
34
34
  var OPENCLAW_EXEC_APPROVALS_PATH = path.join(OPENCLAW_DIR, "exec-approvals.json");
35
+ var SRT_SETTINGS_PATH = path.join(HOME, ".srt-settings.json");
35
36
  function getPublicDir() {
36
37
  const currentDir = path.dirname(new URL(import.meta.url).pathname);
37
38
  const bundledPath = path.resolve(currentDir, "..", "public");
@@ -84,8 +85,12 @@ var safeClawConfigSchema = z.object({
84
85
  port: z.number().int().min(1024).max(65535).default(54335),
85
86
  autoOpenBrowser: z.boolean().default(true),
86
87
  premium: z.boolean().default(false),
87
- userId: z.string().nullable().default(null)
88
- });
88
+ userId: z.string().nullable().default(null),
89
+ srt: z.object({
90
+ enabled: z.boolean(),
91
+ settingsPath: z.string().optional()
92
+ }).optional()
93
+ }).passthrough();
89
94
  var activityTypeSchema = z.enum([
90
95
  "file_read",
91
96
  "file_write",
@@ -263,7 +268,7 @@ var execApprovalEntrySchema = z.object({
263
268
  requestedAt: z.string(),
264
269
  expiresAt: z.string(),
265
270
  decision: execDecisionSchema.nullable(),
266
- decidedBy: z.enum(["user", "auto-deny"]).nullable(),
271
+ decidedBy: z.enum(["user", "auto-deny", "auto-approve", "access-control"]).nullable(),
267
272
  decidedAt: z.string().nullable()
268
273
  });
269
274
  var allowlistPatternSchema = z.object({
@@ -272,6 +277,104 @@ var allowlistPatternSchema = z.object({
272
277
  var allowlistStateSchema = z.object({
273
278
  patterns: z.array(allowlistPatternSchema)
274
279
  });
280
+ var skillScanCategoryIdSchema = z.enum([
281
+ "SK-HID",
282
+ "SK-INJ",
283
+ "SK-EXE",
284
+ "SK-EXF",
285
+ "SK-SEC",
286
+ "SK-SFA",
287
+ "SK-MEM",
288
+ "SK-SUP",
289
+ "SK-B64",
290
+ "SK-IMG",
291
+ "SK-SYS",
292
+ "SK-ARG",
293
+ "SK-XTL",
294
+ "SK-PRM",
295
+ "SK-STR"
296
+ ]);
297
+ var skillScanRequestSchema = z.object({
298
+ content: z.string().min(1).max(5e5)
299
+ });
300
+ var skillScanFindingSchema = z.object({
301
+ categoryId: skillScanCategoryIdSchema,
302
+ categoryName: z.string(),
303
+ severity: threatLevelSchema,
304
+ reason: z.string(),
305
+ evidence: z.string().optional(),
306
+ owaspRef: z.string().optional(),
307
+ remediation: z.string().optional(),
308
+ lineNumber: z.number().optional()
309
+ });
310
+ var skillScanResultSchema = z.object({
311
+ overallSeverity: threatLevelSchema,
312
+ findings: z.array(skillScanFindingSchema),
313
+ summary: z.object({
314
+ critical: z.number(),
315
+ high: z.number(),
316
+ medium: z.number(),
317
+ low: z.number()
318
+ }),
319
+ scannedAt: z.string(),
320
+ contentLength: z.number(),
321
+ scanDurationMs: z.number()
322
+ });
323
+ var skillCleanResultSchema = z.object({
324
+ cleanedContent: z.string(),
325
+ removedCount: z.number()
326
+ });
327
+ var securityLayerStatusSchema = z.enum([
328
+ "configured",
329
+ "partial",
330
+ "unconfigured",
331
+ "error"
332
+ ]);
333
+ var securityCheckSchema = z.object({
334
+ id: z.string(),
335
+ label: z.string(),
336
+ passed: z.boolean(),
337
+ detail: z.string(),
338
+ severity: z.enum(["info", "warning", "critical"])
339
+ }).passthrough();
340
+ var securityLayerSchema = z.object({
341
+ id: z.string(),
342
+ name: z.string(),
343
+ status: securityLayerStatusSchema,
344
+ checks: z.array(securityCheckSchema),
345
+ passedCount: z.number(),
346
+ totalCount: z.number()
347
+ }).passthrough();
348
+ var securityPostureSchema = z.object({
349
+ layers: z.array(securityLayerSchema),
350
+ overallScore: z.number(),
351
+ configuredLayers: z.number(),
352
+ partialLayers: z.number(),
353
+ unconfiguredLayers: z.number(),
354
+ totalLayers: z.number(),
355
+ checkedAt: z.string()
356
+ }).passthrough();
357
+ var srtNetworkConfigSchema = z.object({
358
+ allowedDomains: z.array(z.string()),
359
+ deniedDomains: z.array(z.string()),
360
+ allowLocalBinding: z.boolean()
361
+ }).passthrough();
362
+ var srtFilesystemConfigSchema = z.object({
363
+ denyRead: z.array(z.string()),
364
+ allowWrite: z.array(z.string()),
365
+ denyWrite: z.array(z.string())
366
+ }).passthrough();
367
+ var srtSettingsSchema = z.object({
368
+ network: srtNetworkConfigSchema,
369
+ filesystem: srtFilesystemConfigSchema
370
+ }).passthrough();
371
+ var srtStatusSchema = z.object({
372
+ installed: z.boolean(),
373
+ version: z.string().nullable(),
374
+ enabled: z.boolean(),
375
+ settingsPath: z.string(),
376
+ settings: srtSettingsSchema.nullable()
377
+ }).passthrough();
275
378
 
276
379
  // src/lib/config.ts
277
380
  var DEFAULT_CONFIG = {
@@ -279,7 +382,10 @@ var DEFAULT_CONFIG = {
279
382
  port: 54335,
280
383
  autoOpenBrowser: true,
281
384
  premium: false,
282
- userId: null
385
+ userId: null,
386
+ srt: {
387
+ enabled: false
388
+ }
283
389
  };
284
390
  function ensureDataDir() {
285
391
  if (!fs2.existsSync(SAFECLAW_DIR)) {
@@ -403,7 +509,7 @@ var execApprovals = sqliteTable("exec_approvals", {
403
509
  enum: ["allow-once", "allow-always", "deny"]
404
510
  }),
405
511
  decidedBy: text("decided_by", {
406
- enum: ["user", "auto-deny"]
512
+ enum: ["user", "auto-deny", "auto-approve", "access-control"]
407
513
  }),
408
514
  decidedAt: text("decided_at"),
409
515
  matchedPattern: text("matched_pattern")
@@ -469,8 +575,9 @@ function writeOpenClawConfig(updates) {
469
575
  const current = readOpenClawConfig();
470
576
  if (!current) return null;
471
577
  const merged = deepMerge(current, updates);
472
- fs4.writeFileSync(OPENCLAW_CONFIG_PATH, JSON.stringify(merged, null, 2));
473
- return merged;
578
+ const cleaned = stripNulls(merged);
579
+ fs4.writeFileSync(OPENCLAW_CONFIG_PATH, JSON.stringify(cleaned, null, 2));
580
+ return cleaned;
474
581
  }
475
582
  function deepMerge(target, source) {
476
583
  const result = { ...target };
@@ -485,6 +592,21 @@ function deepMerge(target, source) {
485
592
  }
486
593
  return result;
487
594
  }
595
+ function stripNulls(obj) {
596
+ const result = {};
597
+ for (const [key, val] of Object.entries(obj)) {
598
+ if (val === null) continue;
599
+ if (typeof val === "object" && !Array.isArray(val)) {
600
+ const cleaned = stripNulls(val);
601
+ if (Object.keys(cleaned).length > 0) {
602
+ result[key] = cleaned;
603
+ }
604
+ } else {
605
+ result[key] = val;
606
+ }
607
+ }
608
+ return result;
609
+ }
488
610
 
489
611
  // src/lib/openclaw-client.ts
490
612
  import { EventEmitter } from "events";
@@ -1311,8 +1433,228 @@ var SessionWatcher = class extends EventEmitter2 {
1311
1433
  };
1312
1434
 
1313
1435
  // src/services/exec-approval-service.ts
1314
- import { eq, desc, sql as sql2 } from "drizzle-orm";
1436
+ import { eq as eq2, desc, sql as sql3 } from "drizzle-orm";
1315
1437
  import path3 from "path";
1438
+
1439
+ // src/services/access-control.ts
1440
+ import { eq, and, sql as sql2 } from "drizzle-orm";
1441
+ var TOOL_GROUP_MAP = {
1442
+ filesystem: "group:fs",
1443
+ system_commands: "group:runtime",
1444
+ network: "group:web"
1445
+ };
1446
+ function deriveMcpServerStates(config, denyList) {
1447
+ const pluginEntries = config.plugins?.entries ?? {};
1448
+ return Object.keys(pluginEntries).map((name) => {
1449
+ const pluginEnabled = pluginEntries[name].enabled !== false;
1450
+ const toolsDenyBlocked = denyList.includes(`mcp__${name}`);
1451
+ return {
1452
+ name,
1453
+ pluginEnabled,
1454
+ toolsDenyBlocked,
1455
+ effectivelyEnabled: pluginEnabled && !toolsDenyBlocked
1456
+ };
1457
+ });
1458
+ }
1459
+ function deriveAccessState() {
1460
+ const config = readOpenClawConfig();
1461
+ if (!config) {
1462
+ return {
1463
+ toggles: [
1464
+ { category: "filesystem", enabled: true },
1465
+ { category: "mcp_servers", enabled: true },
1466
+ { category: "network", enabled: true },
1467
+ { category: "system_commands", enabled: true }
1468
+ ],
1469
+ mcpServers: [],
1470
+ openclawConfigAvailable: false
1471
+ };
1472
+ }
1473
+ const denyList = config.tools?.deny ?? [];
1474
+ const filesystemEnabled = !denyList.includes("group:fs");
1475
+ const systemCommandsEnabled = !denyList.includes("group:runtime");
1476
+ const networkEnabled = !denyList.includes("group:web") && config.browser?.enabled !== false;
1477
+ const pluginEntries = config.plugins?.entries ?? {};
1478
+ const pluginNames = Object.keys(pluginEntries);
1479
+ const mcpEnabled = pluginNames.length === 0 || pluginNames.some((name) => pluginEntries[name].enabled !== false);
1480
+ const toggles = [
1481
+ { category: "filesystem", enabled: filesystemEnabled },
1482
+ { category: "mcp_servers", enabled: mcpEnabled },
1483
+ { category: "network", enabled: networkEnabled },
1484
+ { category: "system_commands", enabled: systemCommandsEnabled }
1485
+ ];
1486
+ const mcpServers = deriveMcpServerStates(config, denyList);
1487
+ return { toggles, mcpServers, openclawConfigAvailable: true };
1488
+ }
1489
+ async function applyAccessToggle(category, enabled) {
1490
+ const config = readOpenClawConfig();
1491
+ if (!config) {
1492
+ throw new Error("OpenClaw config not found");
1493
+ }
1494
+ if (category === "mcp_servers") {
1495
+ await applyMcpToggle(config, enabled);
1496
+ } else if (category === "network") {
1497
+ applyNetworkToggle(config, enabled);
1498
+ } else {
1499
+ applyToolGroupToggle(config, category, enabled);
1500
+ }
1501
+ await updateAuditDb(category, enabled);
1502
+ return deriveAccessState();
1503
+ }
1504
+ async function applyMcpServerToggle(serverName, enabled) {
1505
+ const config = readOpenClawConfig();
1506
+ if (!config) {
1507
+ throw new Error("OpenClaw config not found");
1508
+ }
1509
+ const denyPattern = `mcp__${serverName}`;
1510
+ const currentDeny = [...config.tools?.deny ?? []];
1511
+ if (enabled) {
1512
+ const filtered = currentDeny.filter((entry) => entry !== denyPattern);
1513
+ writeOpenClawConfig({ tools: { deny: filtered } });
1514
+ } else {
1515
+ if (!currentDeny.includes(denyPattern)) {
1516
+ currentDeny.push(denyPattern);
1517
+ }
1518
+ writeOpenClawConfig({ tools: { deny: currentDeny } });
1519
+ }
1520
+ await updateAuditDb(`mcp_server:${serverName}`, enabled);
1521
+ return deriveAccessState();
1522
+ }
1523
+ function applyToolGroupToggle(config, category, enabled) {
1524
+ const groupName = TOOL_GROUP_MAP[category];
1525
+ if (!groupName) return;
1526
+ const currentDeny = [...config.tools?.deny ?? []];
1527
+ if (enabled) {
1528
+ const filtered = currentDeny.filter((entry) => entry !== groupName);
1529
+ writeOpenClawConfig({ tools: { deny: filtered } });
1530
+ } else {
1531
+ if (!currentDeny.includes(groupName)) {
1532
+ currentDeny.push(groupName);
1533
+ }
1534
+ writeOpenClawConfig({ tools: { deny: currentDeny } });
1535
+ }
1536
+ }
1537
+ function applyNetworkToggle(config, enabled) {
1538
+ const currentDeny = [...config.tools?.deny ?? []];
1539
+ const groupName = "group:web";
1540
+ if (enabled) {
1541
+ const filtered = currentDeny.filter((entry) => entry !== groupName);
1542
+ writeOpenClawConfig({
1543
+ tools: { deny: filtered },
1544
+ browser: { enabled: true }
1545
+ });
1546
+ } else {
1547
+ if (!currentDeny.includes(groupName)) {
1548
+ currentDeny.push(groupName);
1549
+ }
1550
+ writeOpenClawConfig({
1551
+ tools: { deny: currentDeny },
1552
+ browser: { enabled: false }
1553
+ });
1554
+ }
1555
+ }
1556
+ async function applyMcpToggle(config, enabled) {
1557
+ const pluginEntries = config.plugins?.entries ?? {};
1558
+ const pluginNames = Object.keys(pluginEntries);
1559
+ if (pluginNames.length === 0) return;
1560
+ const currentDeny = [...config.tools?.deny ?? []];
1561
+ if (!enabled) {
1562
+ const stateMap = {};
1563
+ for (const name of pluginNames) {
1564
+ stateMap[name] = pluginEntries[name].enabled !== false;
1565
+ }
1566
+ await savePreviousPluginState(stateMap);
1567
+ const disabledEntries = {};
1568
+ for (const name of pluginNames) {
1569
+ disabledEntries[name] = { enabled: false };
1570
+ }
1571
+ for (const name of pluginNames) {
1572
+ const denyPattern = `mcp__${name}`;
1573
+ if (!currentDeny.includes(denyPattern)) {
1574
+ currentDeny.push(denyPattern);
1575
+ }
1576
+ }
1577
+ writeOpenClawConfig({
1578
+ plugins: { entries: disabledEntries },
1579
+ tools: { deny: currentDeny }
1580
+ });
1581
+ } else {
1582
+ const previousState = await loadPreviousPluginState();
1583
+ const restoredEntries = {};
1584
+ for (const name of pluginNames) {
1585
+ restoredEntries[name] = {
1586
+ enabled: previousState?.[name] ?? true
1587
+ };
1588
+ }
1589
+ const mcpDenyPatterns = new Set(pluginNames.map((n) => `mcp__${n}`));
1590
+ const filteredDeny = currentDeny.filter(
1591
+ (entry) => !mcpDenyPatterns.has(entry)
1592
+ );
1593
+ writeOpenClawConfig({
1594
+ plugins: { entries: restoredEntries },
1595
+ tools: { deny: filteredDeny }
1596
+ });
1597
+ }
1598
+ }
1599
+ async function savePreviousPluginState(stateMap) {
1600
+ const db = getDb();
1601
+ const existing = await db.select().from(schema_exports.accessConfig).where(
1602
+ and(
1603
+ eq(schema_exports.accessConfig.category, "mcp_servers"),
1604
+ eq(schema_exports.accessConfig.key, "previous_plugin_state")
1605
+ )
1606
+ );
1607
+ if (existing.length > 0) {
1608
+ await db.update(schema_exports.accessConfig).set({
1609
+ value: JSON.stringify(stateMap),
1610
+ updatedAt: sql2`datetime('now')`
1611
+ }).where(
1612
+ and(
1613
+ eq(schema_exports.accessConfig.category, "mcp_servers"),
1614
+ eq(schema_exports.accessConfig.key, "previous_plugin_state")
1615
+ )
1616
+ );
1617
+ } else {
1618
+ await db.insert(schema_exports.accessConfig).values({
1619
+ category: "mcp_servers",
1620
+ key: "previous_plugin_state",
1621
+ value: JSON.stringify(stateMap)
1622
+ });
1623
+ }
1624
+ }
1625
+ async function loadPreviousPluginState() {
1626
+ const db = getDb();
1627
+ const rows = await db.select().from(schema_exports.accessConfig).where(
1628
+ and(
1629
+ eq(schema_exports.accessConfig.category, "mcp_servers"),
1630
+ eq(schema_exports.accessConfig.key, "previous_plugin_state")
1631
+ )
1632
+ );
1633
+ if (rows.length === 0) return null;
1634
+ try {
1635
+ return JSON.parse(rows[0].value);
1636
+ } catch {
1637
+ return null;
1638
+ }
1639
+ }
1640
+ async function updateAuditDb(category, enabled) {
1641
+ const db = getDb();
1642
+ try {
1643
+ await db.update(schema_exports.accessConfig).set({
1644
+ value: enabled ? "true" : "false",
1645
+ updatedAt: sql2`datetime('now')`
1646
+ }).where(
1647
+ and(
1648
+ eq(schema_exports.accessConfig.category, category),
1649
+ eq(schema_exports.accessConfig.key, "enabled")
1650
+ )
1651
+ );
1652
+ } catch (err) {
1653
+ logger.warn({ err, category, enabled }, "Failed to update audit DB");
1654
+ }
1655
+ }
1656
+
1657
+ // src/services/exec-approval-service.ts
1316
1658
  var DEFAULT_TIMEOUT_MS = 6e5;
1317
1659
  var instance = null;
1318
1660
  function matchesPattern(command, pattern) {
@@ -1320,6 +1662,37 @@ function matchesPattern(command, pattern) {
1320
1662
  const regexStr = "^" + escaped.replace(/\*/g, ".*") + "$";
1321
1663
  return new RegExp(regexStr, "i").test(command);
1322
1664
  }
1665
+ var NETWORK_BINARIES = /* @__PURE__ */ new Set([
1666
+ "curl",
1667
+ "wget",
1668
+ "httpie",
1669
+ "http",
1670
+ "ssh",
1671
+ "scp",
1672
+ "sftp",
1673
+ "nc",
1674
+ "ncat",
1675
+ "netcat",
1676
+ "dig",
1677
+ "nslookup",
1678
+ "host",
1679
+ "ping",
1680
+ "traceroute",
1681
+ "tracepath",
1682
+ "mtr",
1683
+ "telnet",
1684
+ "ftp",
1685
+ "lftp",
1686
+ "rsync",
1687
+ "socat",
1688
+ "nmap"
1689
+ ]);
1690
+ function isNetworkCommand(command) {
1691
+ const firstToken = command.trim().split(/\s+/)[0];
1692
+ if (!firstToken) return false;
1693
+ const basename = firstToken.includes("/") ? firstToken.split("/").pop() : firstToken;
1694
+ return NETWORK_BINARIES.has(basename.toLowerCase());
1695
+ }
1323
1696
  var ExecApprovalService = class {
1324
1697
  client;
1325
1698
  io;
@@ -1360,7 +1733,58 @@ var ExecApprovalService = class {
1360
1733
  handleRequest(request) {
1361
1734
  const matchedPattern = this.findMatchingPattern(request.command);
1362
1735
  if (!matchedPattern) {
1736
+ if (isNetworkCommand(request.command)) {
1737
+ try {
1738
+ const accessState = deriveAccessState();
1739
+ const networkToggle = accessState.toggles.find(
1740
+ (t) => t.category === "network"
1741
+ );
1742
+ if (networkToggle && !networkToggle.enabled) {
1743
+ this.resolveToGateway(request.id, "deny");
1744
+ const now3 = /* @__PURE__ */ new Date();
1745
+ const entry3 = {
1746
+ id: request.id,
1747
+ command: request.command,
1748
+ cwd: request.cwd,
1749
+ security: request.security,
1750
+ sessionKey: request.sessionKey,
1751
+ requestedAt: now3.toISOString(),
1752
+ expiresAt: now3.toISOString(),
1753
+ decision: "deny",
1754
+ decidedBy: "access-control",
1755
+ decidedAt: now3.toISOString()
1756
+ };
1757
+ this.persistApproval(entry3, null);
1758
+ this.io.emit("safeclaw:execApprovalResolved", entry3);
1759
+ logger.info(
1760
+ { command: request.command },
1761
+ "Network command auto-denied (network toggle OFF)"
1762
+ );
1763
+ return;
1764
+ }
1765
+ } catch {
1766
+ logger.debug(
1767
+ { command: request.command },
1768
+ "Could not read access state for network check, falling through"
1769
+ );
1770
+ }
1771
+ }
1363
1772
  this.resolveToGateway(request.id, "allow-once");
1773
+ const now2 = /* @__PURE__ */ new Date();
1774
+ const entry2 = {
1775
+ id: request.id,
1776
+ command: request.command,
1777
+ cwd: request.cwd,
1778
+ security: request.security,
1779
+ sessionKey: request.sessionKey,
1780
+ requestedAt: now2.toISOString(),
1781
+ expiresAt: now2.toISOString(),
1782
+ decision: "allow-once",
1783
+ decidedBy: "auto-approve",
1784
+ decidedAt: now2.toISOString()
1785
+ };
1786
+ this.persistApproval(entry2, null);
1787
+ this.io.emit("safeclaw:execApprovalResolved", entry2);
1364
1788
  logger.debug(
1365
1789
  { command: request.command },
1366
1790
  "Command auto-approved (not restricted)"
@@ -1479,7 +1903,7 @@ var ExecApprovalService = class {
1479
1903
  decision: entry.decision,
1480
1904
  decidedBy: entry.decidedBy,
1481
1905
  decidedAt: entry.decidedAt
1482
- }).where(eq(schema_exports.execApprovals.id, entry.id)).run();
1906
+ }).where(eq2(schema_exports.execApprovals.id, entry.id)).run();
1483
1907
  } catch (err) {
1484
1908
  logger.error(
1485
1909
  { err, id: entry.id },
@@ -1528,7 +1952,7 @@ var ExecApprovalService = class {
1528
1952
  );
1529
1953
  try {
1530
1954
  const db = getDb();
1531
- db.delete(schema_exports.restrictedPatterns).where(eq(schema_exports.restrictedPatterns.pattern, pattern)).run();
1955
+ db.delete(schema_exports.restrictedPatterns).where(eq2(schema_exports.restrictedPatterns.pattern, pattern)).run();
1532
1956
  } catch (err) {
1533
1957
  logger.error({ err, pattern }, "Failed to remove restricted pattern from database");
1534
1958
  }
@@ -1631,7 +2055,7 @@ var ExecApprovalService = class {
1631
2055
  getHistory(limit = 50) {
1632
2056
  try {
1633
2057
  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();
2058
+ const rows = db.select().from(schema_exports.execApprovals).where(sql3`${schema_exports.execApprovals.decision} IS NOT NULL`).orderBy(desc(schema_exports.execApprovals.decidedAt)).limit(limit).all();
1635
2059
  return rows.map((r) => ({
1636
2060
  id: r.id,
1637
2061
  command: r.command,
@@ -1792,7 +2216,7 @@ var SECRET_PATTERNS = [
1792
2216
  function scanForSecrets(content) {
1793
2217
  if (!content) return { types: [], maxSeverity: "NONE" };
1794
2218
  const found = /* @__PURE__ */ new Set();
1795
- let maxSeverity = "NONE";
2219
+ let maxSeverity2 = "NONE";
1796
2220
  const severityOrder = {
1797
2221
  NONE: 0,
1798
2222
  LOW: 1,
@@ -1803,12 +2227,12 @@ function scanForSecrets(content) {
1803
2227
  for (const { pattern, type, severity } of SECRET_PATTERNS) {
1804
2228
  if (pattern.test(content)) {
1805
2229
  found.add(type);
1806
- if (severityOrder[severity] > severityOrder[maxSeverity]) {
1807
- maxSeverity = severity;
2230
+ if (severityOrder[severity] > severityOrder[maxSeverity2]) {
2231
+ maxSeverity2 = severity;
1808
2232
  }
1809
2233
  }
1810
2234
  }
1811
- return { types: Array.from(found), maxSeverity };
2235
+ return { types: Array.from(found), maxSeverity: maxSeverity2 };
1812
2236
  }
1813
2237
 
1814
2238
  // src/lib/threat-patterns.ts
@@ -2445,7 +2869,7 @@ function analyzeActivityThreat(activityType, detail, targetPath, contentPreview,
2445
2869
  }
2446
2870
 
2447
2871
  // src/services/openclaw-monitor.ts
2448
- import { eq as eq2, ne, desc as desc2, and, sql as sql3 } from "drizzle-orm";
2872
+ import { eq as eq3, ne, desc as desc2, and as and2, sql as sql4 } from "drizzle-orm";
2449
2873
  var instance2 = null;
2450
2874
  var OpenClawMonitor = class {
2451
2875
  client;
@@ -2510,7 +2934,7 @@ var OpenClawMonitor = class {
2510
2934
  parsed.toolName
2511
2935
  );
2512
2936
  const db = getDb();
2513
- const existingSession = await db.select().from(schema_exports.openclawSessions).where(eq2(schema_exports.openclawSessions.id, parsed.openclawSessionId)).limit(1);
2937
+ const existingSession = await db.select().from(schema_exports.openclawSessions).where(eq3(schema_exports.openclawSessions.id, parsed.openclawSessionId)).limit(1);
2514
2938
  if (existingSession.length === 0) {
2515
2939
  await db.insert(schema_exports.openclawSessions).values({
2516
2940
  id: parsed.openclawSessionId,
@@ -2599,7 +3023,7 @@ var OpenClawMonitor = class {
2599
3023
  }
2600
3024
  async handleSessionStart(sessionId, model) {
2601
3025
  const db = getDb();
2602
- const existing = await db.select().from(schema_exports.openclawSessions).where(eq2(schema_exports.openclawSessions.id, sessionId)).limit(1);
3026
+ const existing = await db.select().from(schema_exports.openclawSessions).where(eq3(schema_exports.openclawSessions.id, sessionId)).limit(1);
2603
3027
  if (existing.length === 0) {
2604
3028
  await db.insert(schema_exports.openclawSessions).values({
2605
3029
  id: sessionId,
@@ -2607,7 +3031,7 @@ var OpenClawMonitor = class {
2607
3031
  model: model ?? null
2608
3032
  });
2609
3033
  } else {
2610
- await db.update(schema_exports.openclawSessions).set({ status: "ACTIVE", model: model ?? null }).where(eq2(schema_exports.openclawSessions.id, sessionId));
3034
+ await db.update(schema_exports.openclawSessions).set({ status: "ACTIVE", model: model ?? null }).where(eq3(schema_exports.openclawSessions.id, sessionId));
2611
3035
  }
2612
3036
  const session = await this.buildSessionPayload(sessionId);
2613
3037
  if (session) {
@@ -2618,8 +3042,8 @@ var OpenClawMonitor = class {
2618
3042
  const db = getDb();
2619
3043
  await db.update(schema_exports.openclawSessions).set({
2620
3044
  status: "ENDED",
2621
- endedAt: sql3`datetime('now')`
2622
- }).where(eq2(schema_exports.openclawSessions.id, sessionId));
3045
+ endedAt: sql4`datetime('now')`
3046
+ }).where(eq3(schema_exports.openclawSessions.id, sessionId));
2623
3047
  const session = await this.buildSessionPayload(sessionId);
2624
3048
  if (session) {
2625
3049
  this.io.emit("safeclaw:openclawSessionUpdate", session);
@@ -2627,9 +3051,9 @@ var OpenClawMonitor = class {
2627
3051
  }
2628
3052
  async buildSessionPayload(sessionId) {
2629
3053
  const db = getDb();
2630
- const [row] = await db.select().from(schema_exports.openclawSessions).where(eq2(schema_exports.openclawSessions.id, sessionId));
3054
+ const [row] = await db.select().from(schema_exports.openclawSessions).where(eq3(schema_exports.openclawSessions.id, sessionId));
2631
3055
  if (!row) return null;
2632
- const activities = await db.select().from(schema_exports.agentActivities).where(eq2(schema_exports.agentActivities.openclawSessionId, sessionId));
3056
+ const activities = await db.select().from(schema_exports.agentActivities).where(eq3(schema_exports.agentActivities.openclawSessionId, sessionId));
2633
3057
  const threatSummary = {
2634
3058
  NONE: 0,
2635
3059
  LOW: 0,
@@ -2691,8 +3115,8 @@ var OpenClawMonitor = class {
2691
3115
  await db.update(schema_exports.agentActivities).set({
2692
3116
  resolved: resolved ? 1 : 0,
2693
3117
  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));
3118
+ }).where(eq3(schema_exports.agentActivities.id, activityId));
3119
+ const [row] = await db.select().from(schema_exports.agentActivities).where(eq3(schema_exports.agentActivities.id, activityId));
2696
3120
  if (!row) return null;
2697
3121
  return this.mapRowToActivity(row);
2698
3122
  }
@@ -2700,19 +3124,19 @@ var OpenClawMonitor = class {
2700
3124
  const db = getDb();
2701
3125
  const conditions = [ne(schema_exports.agentActivities.threatLevel, "NONE")];
2702
3126
  if (severity) {
2703
- conditions.push(eq2(schema_exports.agentActivities.threatLevel, severity));
3127
+ conditions.push(eq3(schema_exports.agentActivities.threatLevel, severity));
2704
3128
  }
2705
3129
  if (resolved !== void 0) {
2706
- conditions.push(eq2(schema_exports.agentActivities.resolved, resolved ? 1 : 0));
3130
+ conditions.push(eq3(schema_exports.agentActivities.resolved, resolved ? 1 : 0));
2707
3131
  }
2708
- const rows = await db.select().from(schema_exports.agentActivities).where(and(...conditions)).orderBy(desc2(schema_exports.agentActivities.id)).limit(limit);
3132
+ const rows = await db.select().from(schema_exports.agentActivities).where(and2(...conditions)).orderBy(desc2(schema_exports.agentActivities.id)).limit(limit);
2709
3133
  return rows.map((r) => this.mapRowToActivity(r));
2710
3134
  }
2711
3135
  async getActivities(sessionId, limit = 50) {
2712
3136
  const db = getDb();
2713
3137
  let rows;
2714
3138
  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);
3139
+ rows = await db.select().from(schema_exports.agentActivities).where(eq3(schema_exports.agentActivities.openclawSessionId, sessionId)).orderBy(desc2(schema_exports.agentActivities.id)).limit(limit);
2716
3140
  } else {
2717
3141
  rows = await db.select().from(schema_exports.agentActivities).orderBy(desc2(schema_exports.agentActivities.id)).limit(limit);
2718
3142
  }
@@ -2767,222 +3191,130 @@ function getOpenClawMonitor() {
2767
3191
  return instance2;
2768
3192
  }
2769
3193
 
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
- };
3194
+ // src/lib/srt-config.ts
3195
+ import fs8 from "fs";
3196
+ import { execSync } from "child_process";
3197
+ var DEFAULT_SRT_SETTINGS = {
3198
+ network: {
3199
+ allowedDomains: [],
3200
+ deniedDomains: [],
3201
+ allowLocalBinding: false
3202
+ },
3203
+ filesystem: {
3204
+ denyRead: [],
3205
+ allowWrite: [],
3206
+ denyWrite: []
2803
3207
  }
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 };
3208
+ };
3209
+ var srtInstalledCache = null;
3210
+ function getSrtSettingsPath() {
3211
+ const config = readConfig();
3212
+ return config.srt?.settingsPath ?? SRT_SETTINGS_PATH;
2819
3213
  }
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);
3214
+ function isSrtInstalled() {
3215
+ if (srtInstalledCache?.installed) return srtInstalledCache;
3216
+ try {
3217
+ const output = execSync("srt --version", { timeout: 3e3, encoding: "utf-8" }).trim();
3218
+ const version = output.replace(/^srt\s*/i, "").trim() || output;
3219
+ srtInstalledCache = { installed: true, version };
3220
+ return srtInstalledCache;
3221
+ } catch {
3222
+ return { installed: false, version: null };
2831
3223
  }
2832
- await updateAuditDb(category, enabled);
2833
- return deriveAccessState();
2834
3224
  }
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 } });
3225
+ function readSrtSettings() {
3226
+ const settingsPath = getSrtSettingsPath();
3227
+ if (!fs8.existsSync(settingsPath)) return null;
3228
+ try {
3229
+ const raw = JSON.parse(fs8.readFileSync(settingsPath, "utf-8"));
3230
+ return srtSettingsSchema.parse(raw);
3231
+ } catch {
3232
+ return null;
2850
3233
  }
2851
- await updateAuditDb(`mcp_server:${serverName}`, enabled);
2852
- return deriveAccessState();
2853
3234
  }
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
- }
3235
+ function writeSrtSettings(settings) {
3236
+ const settingsPath = getSrtSettingsPath();
3237
+ fs8.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
2867
3238
  }
2868
- function applyNetworkToggle(config, enabled) {
2869
- const currentDeny = [...config.tools?.deny ?? []];
2870
- const groupName = "group:web";
3239
+ function ensureSrtSettings() {
3240
+ const existing = readSrtSettings();
3241
+ if (existing) return existing;
3242
+ writeSrtSettings(DEFAULT_SRT_SETTINGS);
3243
+ return DEFAULT_SRT_SETTINGS;
3244
+ }
3245
+ function getSrtStatus() {
3246
+ const { installed, version } = isSrtInstalled();
3247
+ const config = readConfig();
3248
+ const enabled = config.srt?.enabled ?? false;
3249
+ const settingsPath = getSrtSettingsPath();
3250
+ const settings = readSrtSettings();
3251
+ return { installed, version, enabled, settingsPath, settings };
3252
+ }
3253
+ function toggleSrt(enabled) {
3254
+ const config = readConfig();
3255
+ writeConfig({
3256
+ ...config,
3257
+ srt: { ...config.srt, enabled }
3258
+ });
2871
3259
  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
- });
3260
+ ensureSrtSettings();
2885
3261
  }
3262
+ return getSrtStatus();
2886
3263
  }
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
- });
3264
+ function addAllowedDomain(domain) {
3265
+ const settings = ensureSrtSettings();
3266
+ const normalized = domain.trim().toLowerCase();
3267
+ if (!normalized) return settings;
3268
+ settings.network.deniedDomains = settings.network.deniedDomains.filter(
3269
+ (d) => d !== normalized
3270
+ );
3271
+ if (!settings.network.allowedDomains.includes(normalized)) {
3272
+ settings.network.allowedDomains.push(normalized);
2928
3273
  }
3274
+ writeSrtSettings(settings);
3275
+ return settings;
2929
3276
  }
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
- )
3277
+ function removeAllowedDomain(domain) {
3278
+ const settings = ensureSrtSettings();
3279
+ const normalized = domain.trim().toLowerCase();
3280
+ settings.network.allowedDomains = settings.network.allowedDomains.filter(
3281
+ (d) => d !== normalized
2937
3282
  );
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
- }
3283
+ writeSrtSettings(settings);
3284
+ return settings;
2955
3285
  }
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
- )
3286
+ function addDeniedDomain(domain) {
3287
+ const settings = ensureSrtSettings();
3288
+ const normalized = domain.trim().toLowerCase();
3289
+ if (!normalized) return settings;
3290
+ settings.network.allowedDomains = settings.network.allowedDomains.filter(
3291
+ (d) => d !== normalized
2963
3292
  );
2964
- if (rows.length === 0) return null;
2965
- try {
2966
- return JSON.parse(rows[0].value);
2967
- } catch {
2968
- return null;
3293
+ if (!settings.network.deniedDomains.includes(normalized)) {
3294
+ settings.network.deniedDomains.push(normalized);
2969
3295
  }
3296
+ writeSrtSettings(settings);
3297
+ return settings;
2970
3298
  }
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");
3299
+ function removeDeniedDomain(domain) {
3300
+ const settings = ensureSrtSettings();
3301
+ const normalized = domain.trim().toLowerCase();
3302
+ settings.network.deniedDomains = settings.network.deniedDomains.filter(
3303
+ (d) => d !== normalized
3304
+ );
3305
+ writeSrtSettings(settings);
3306
+ return settings;
3307
+ }
3308
+ function updateSrtSettings(updates) {
3309
+ const settings = ensureSrtSettings();
3310
+ if (updates.network) {
3311
+ settings.network = { ...settings.network, ...updates.network };
2985
3312
  }
3313
+ if (updates.filesystem) {
3314
+ settings.filesystem = { ...settings.filesystem, ...updates.filesystem };
3315
+ }
3316
+ writeSrtSettings(settings);
3317
+ return settings;
2986
3318
  }
2987
3319
 
2988
3320
  // src/server/socket.ts
@@ -3237,6 +3569,38 @@ function setupSocketIO(httpServer) {
3237
3569
  logger.error({ err, pattern }, "Failed to remove restricted pattern");
3238
3570
  }
3239
3571
  });
3572
+ socket.on("safeclaw:getSrtStatus", () => {
3573
+ socket.emit("safeclaw:srtStatus", getSrtStatus());
3574
+ });
3575
+ socket.on("safeclaw:toggleSrt", ({ enabled }) => {
3576
+ const status = toggleSrt(enabled);
3577
+ logger.info(`SRT toggled: enabled=${enabled}`);
3578
+ io.emit("safeclaw:srtStatus", status);
3579
+ });
3580
+ socket.on("safeclaw:updateSrtDomains", ({ list, action, domain }) => {
3581
+ try {
3582
+ if (list === "allow") {
3583
+ if (action === "add") addAllowedDomain(domain);
3584
+ else removeAllowedDomain(domain);
3585
+ } else {
3586
+ if (action === "add") addDeniedDomain(domain);
3587
+ else removeDeniedDomain(domain);
3588
+ }
3589
+ logger.info({ list, action, domain }, "SRT domain updated");
3590
+ io.emit("safeclaw:srtStatus", getSrtStatus());
3591
+ } catch (err) {
3592
+ logger.error({ err, list, action, domain }, "Failed to update SRT domain");
3593
+ }
3594
+ });
3595
+ socket.on("safeclaw:updateSrtSettings", (updates) => {
3596
+ try {
3597
+ updateSrtSettings(updates);
3598
+ logger.info({ updates }, "SRT settings updated");
3599
+ io.emit("safeclaw:srtStatus", getSrtStatus());
3600
+ } catch (err) {
3601
+ logger.error({ err }, "Failed to update SRT settings");
3602
+ }
3603
+ });
3240
3604
  socket.on("disconnect", () => {
3241
3605
  logger.info(`Client disconnected: ${socket.id}`);
3242
3606
  });
@@ -3245,7 +3609,1054 @@ function setupSocketIO(httpServer) {
3245
3609
  }
3246
3610
 
3247
3611
  // src/server/routes.ts
3248
- import { desc as desc4, eq as eq5, and as and4, sql as sql6 } from "drizzle-orm";
3612
+ import { desc as desc4, eq as eq6, and as and5, sql as sql7 } from "drizzle-orm";
3613
+
3614
+ // src/lib/skill-scanner-patterns.ts
3615
+ var HIDDEN_CONTENT_PATTERNS = [
3616
+ // HTML comments with instructional content
3617
+ { pattern: /<!--[\s\S]*?(?:instruction|command|execute|ignore|override|system|prompt|inject|do not|must|should|always|never)[\s\S]*?-->/i, label: "HTML comment with instructions", severity: "CRITICAL" },
3618
+ { pattern: /<!--[\s\S]{50,}?-->/s, label: "Large HTML comment (>50 chars)", severity: "HIGH" },
3619
+ // Zero-width Unicode characters
3620
+ { pattern: /[\u200B\u200C\u200D\u200E\u200F]/, label: "Zero-width Unicode character (U+200B-200F)", severity: "CRITICAL" },
3621
+ { pattern: /[\u2060\u2061\u2062\u2063\u2064]/, label: "Invisible Unicode separator (U+2060-2064)", severity: "CRITICAL" },
3622
+ { pattern: /[\uFEFF]/, label: "Zero-width no-break space (BOM)", severity: "HIGH" },
3623
+ // CSS hiding
3624
+ { pattern: /display\s*:\s*none/i, label: "CSS display:none", severity: "HIGH" },
3625
+ { pattern: /opacity\s*:\s*0(?:[;\s]|$)/i, label: "CSS opacity:0", severity: "HIGH" },
3626
+ { pattern: /visibility\s*:\s*hidden/i, label: "CSS visibility:hidden", severity: "HIGH" },
3627
+ // Bidi overrides
3628
+ { pattern: /[\u202A\u202B\u202C\u202D\u202E]/, label: "Bidi override character", severity: "CRITICAL" },
3629
+ { pattern: /[\u2066\u2067\u2068\u2069]/, label: "Bidi isolate character", severity: "CRITICAL" }
3630
+ ];
3631
+ var PROMPT_INJECTION_PATTERNS = [
3632
+ { pattern: /\b(?:ignore|disregard|forget|override)\s+(?:all\s+)?(?:previous|prior|above|your)\s+(?:instructions?|rules?|prompts?|guidelines?|constraints?)/i, label: "Override previous instructions", severity: "CRITICAL" },
3633
+ { pattern: /\b(?:you\s+(?:are|must|should|will)\s+(?:now|henceforth|from\s+now)\s+(?:be|act|behave|respond)\s+(?:as|like))/i, label: "Role reassignment", severity: "CRITICAL" },
3634
+ { pattern: /\bnew\s+instructions?\s*:/i, label: "New instructions directive", severity: "CRITICAL" },
3635
+ { pattern: /\[INST\]|\[\/INST\]|<\|im_start\|>|<\|im_end\|>/i, label: "Special model tokens", severity: "CRITICAL" },
3636
+ { pattern: /\bsystem\s*prompt\s*(?:override|injection|:)/i, label: "System prompt manipulation", severity: "CRITICAL" },
3637
+ { pattern: /\bdo\s+not\s+(?:follow|obey|listen\s+to)\s+(?:the|your|any)\s+(?:original|previous|prior)/i, label: "Instruction disobedience", severity: "CRITICAL" },
3638
+ { pattern: /\b(?:IMPORTANT|CRITICAL|URGENT)\s*:\s*(?:ignore|override|disregard|you must)/i, label: "Urgent override phrasing", severity: "HIGH" },
3639
+ { pattern: /\b(?:pretend|imagine|suppose)\s+(?:you\s+are|that\s+you)/i, label: "Persona manipulation", severity: "MEDIUM" },
3640
+ { pattern: /\bact\s+as\s+(?:a|an|if)\b/i, label: "Act-as directive", severity: "MEDIUM" }
3641
+ ];
3642
+ var SHELL_EXECUTION_PATTERNS = [
3643
+ { pattern: /(?:curl|wget)\s+[^\n]*\|\s*(?:sh|bash|zsh|node|python)/i, label: "Download and execute (curl|bash)", severity: "CRITICAL" },
3644
+ { pattern: /\beval\s*\(/, label: "eval() call", severity: "CRITICAL" },
3645
+ { pattern: /\bexec\s*\(/, label: "exec() call", severity: "HIGH" },
3646
+ { pattern: /\bnpx\s+-y\s+/, label: "npx -y (auto-confirm remote package)", severity: "HIGH" },
3647
+ { pattern: /\/dev\/tcp\//, label: "Reverse shell via /dev/tcp", severity: "CRITICAL" },
3648
+ { pattern: /\bnc\s+.*-e\s+/, label: "Netcat with exec (nc -e)", severity: "CRITICAL" },
3649
+ { pattern: /\bmkfifo\b.*\bnc\b/s, label: "Named pipe + netcat (reverse shell)", severity: "CRITICAL" },
3650
+ { pattern: /python[23]?\s+-c\s+.*(?:socket|subprocess|os\.system)/i, label: "Python one-liner with system access", severity: "CRITICAL" },
3651
+ { pattern: /\bphp\s+-r\s+.*(?:exec|system|passthru|shell_exec)/i, label: "PHP exec one-liner", severity: "CRITICAL" },
3652
+ { pattern: /\bperl\s+-e\s+.*(?:socket|exec|system)/i, label: "Perl exec one-liner", severity: "CRITICAL" },
3653
+ { pattern: /\bruby\s+-e\s+.*(?:exec|system|spawn)/i, label: "Ruby exec one-liner", severity: "CRITICAL" },
3654
+ { pattern: /\bchmod\s+\+x\b/, label: "Make file executable", severity: "MEDIUM" }
3655
+ ];
3656
+ var DATA_EXFILTRATION_PATTERNS = [
3657
+ { pattern: /pastebin\.com/, label: "pastebin.com", severity: "HIGH" },
3658
+ { pattern: /paste\.ee/, label: "paste.ee", severity: "HIGH" },
3659
+ { pattern: /transfer\.sh/, label: "transfer.sh", severity: "HIGH" },
3660
+ { pattern: /ngrok\.io/, label: "ngrok.io", severity: "HIGH" },
3661
+ { pattern: /requestbin/i, label: "requestbin", severity: "HIGH" },
3662
+ { pattern: /webhook\.site/, label: "webhook.site", severity: "HIGH" },
3663
+ { pattern: /pipedream\.net/, label: "pipedream.net", severity: "HIGH" },
3664
+ { pattern: /hookbin\.com/, label: "hookbin.com", severity: "HIGH" },
3665
+ { pattern: /beeceptor\.com/, label: "beeceptor.com", severity: "HIGH" },
3666
+ { pattern: /postb\.in/, label: "postb.in", severity: "HIGH" },
3667
+ { pattern: /https?:\/\/hooks\.slack\.com\/services\//, label: "Slack webhook URL", severity: "HIGH" },
3668
+ { pattern: /https?:\/\/discord(?:app)?\.com\/api\/webhooks\//, label: "Discord webhook URL", severity: "HIGH" },
3669
+ { pattern: /https?:\/\/api\.telegram\.org\/bot/, label: "Telegram bot API URL", severity: "HIGH" },
3670
+ { pattern: /https?:\/\/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/, label: "Raw IP URL", severity: "HIGH" }
3671
+ ];
3672
+ var MEMORY_POISONING_PATTERNS = [
3673
+ { pattern: /\bSOUL\.md\b/, label: "SOUL.md reference", severity: "CRITICAL" },
3674
+ { pattern: /\bMEMORY\.md\b/, label: "MEMORY.md reference", severity: "CRITICAL" },
3675
+ { pattern: /\.claude\//, label: ".claude/ directory reference", severity: "CRITICAL" },
3676
+ { pattern: /\bCLAUDE\.md\b/, label: "CLAUDE.md reference", severity: "CRITICAL" },
3677
+ { pattern: /\.cursorrules\b/, label: ".cursorrules reference", severity: "CRITICAL" },
3678
+ { pattern: /\.windsurfrules\b/, label: ".windsurfrules reference", severity: "CRITICAL" },
3679
+ { pattern: /\.clinerules\b/, label: ".clinerules reference", severity: "HIGH" },
3680
+ { pattern: /\bCODEX\.md\b/, label: "CODEX.md reference", severity: "HIGH" },
3681
+ { pattern: /(?:write|modify|edit|append|create|overwrite)\s+(?:to\s+)?(?:the\s+)?(?:SOUL|MEMORY|CLAUDE|CODEX)\.md/i, label: "Instruction to modify agent config file", severity: "CRITICAL" },
3682
+ { pattern: /(?:write|modify|edit|append|create|overwrite)\s+(?:to\s+)?(?:the\s+)?\.(?:cursorrules|windsurfrules|clinerules)/i, label: "Instruction to modify IDE rules file", severity: "CRITICAL" }
3683
+ ];
3684
+ var SUPPLY_CHAIN_PATTERNS = [
3685
+ { pattern: /https?:\/\/raw\.githubusercontent\.com\/[^\s]+\.(?:sh|py|js|rb|pl)\b/, label: "Raw GitHub script URL", severity: "HIGH" },
3686
+ { pattern: /https?:\/\/[^\s]+\.(?:sh|py|js|rb|pl)(?:\s|$)/, label: "External script URL", severity: "HIGH" },
3687
+ { pattern: /\bnpm\s+install\s+(?:-g\s+)?[a-zA-Z@]/, label: "npm install command", severity: "HIGH" },
3688
+ { pattern: /\bpip\s+install\s+[a-zA-Z]/, label: "pip install command", severity: "HIGH" },
3689
+ { pattern: /\bgem\s+install\s+/, label: "gem install command", severity: "HIGH" },
3690
+ { pattern: /\bcargo\s+install\s+/, label: "cargo install command", severity: "MEDIUM" },
3691
+ { pattern: /\bgo\s+install\s+/, label: "go install command", severity: "MEDIUM" },
3692
+ { pattern: /\bbrew\s+install\s+/, label: "brew install command", severity: "MEDIUM" }
3693
+ ];
3694
+ var ENCODED_PAYLOAD_PATTERNS = [
3695
+ { pattern: /[A-Za-z0-9+/]{40,}={0,2}/, label: "Base64 string (>40 chars)", severity: "HIGH" },
3696
+ { pattern: /\batob\s*\(/, label: "atob() call (base64 decode)", severity: "HIGH" },
3697
+ { pattern: /\bbtoa\s*\(/, label: "btoa() call (base64 encode)", severity: "MEDIUM" },
3698
+ { pattern: /\bBuffer\.from\s*\([^)]*,\s*['"]base64['"]\)/, label: "Buffer.from base64", severity: "HIGH" },
3699
+ { pattern: /(?:\\x[0-9a-fA-F]{2}){8,}/, label: "Hex escape sequence (8+ bytes)", severity: "HIGH" },
3700
+ { pattern: /String\.fromCharCode\s*\((?:\s*\d+\s*,?\s*){5,}\)/, label: "String.fromCharCode (5+ codes)", severity: "HIGH" },
3701
+ { pattern: /\|\s*base64\s+(?:-d|--decode)/, label: "Piped base64 decode", severity: "CRITICAL" }
3702
+ ];
3703
+ var IMAGE_EXFIL_PATTERNS = [
3704
+ // URL-based exfiltration (original 5)
3705
+ { pattern: /!\[.*?\]\(https?:\/\/[^\s)]*(?:\?|&)(?:data|content|file|token|secret|key|password|env)=[^\s)]*\)/i, label: "Image URL with exfil query params", severity: "CRITICAL" },
3706
+ { pattern: /!\[.*?\]\(https?:\/\/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}[^\s)]*\)/, label: "Image from raw IP address", severity: "CRITICAL" },
3707
+ { pattern: /!\[.*?\]\([^)]*\$\{[^}]+\}[^)]*\)/, label: "Variable interpolation in image URL", severity: "CRITICAL" },
3708
+ { pattern: /!\[.*?\]\([^)]*\$\([^)]+\)[^)]*\)/, label: "Command substitution in image URL", severity: "CRITICAL" },
3709
+ { pattern: /<img[^>]+src\s*=\s*["'][^"']*\$\{[^}]+\}[^"']*["']/i, label: "Variable interpolation in img src", severity: "CRITICAL" },
3710
+ // Group A: Data URI image payloads
3711
+ { pattern: /!\[.*?\]\(data:image\/[^\s)]+\)/i, label: "Data URI image in markdown", severity: "CRITICAL" },
3712
+ { pattern: /<img[^>]+src\s*=\s*["']data:image\/[^"']+["']/i, label: "Data URI image in HTML img tag", severity: "CRITICAL" },
3713
+ { pattern: /data:image\/[^;]+;base64,[A-Za-z0-9+/]{200,}/, label: "Large base64 data URI (steganographic carrier)", severity: "CRITICAL" },
3714
+ // Group B: SVG embedded scripts & data
3715
+ { pattern: /<svg[\s>][\s\S]*?<script[\s>]/i, label: "SVG with embedded script tag", severity: "CRITICAL" },
3716
+ { pattern: /<svg[\s>][\s\S]*?\bon(?:load|error|click|mouseover)\s*=/i, label: "SVG with event handler", severity: "CRITICAL" },
3717
+ { pattern: /<svg[\s>][\s\S]*?<foreignObject[\s>]/i, label: "SVG with foreignObject (arbitrary HTML embed)", severity: "HIGH" },
3718
+ { pattern: /data:image\/svg\+xml[^"'\s)]+/i, label: "SVG data URI (inline payload + script risk)", severity: "CRITICAL" },
3719
+ // Group C: Web beacons / tracking pixels
3720
+ { pattern: /<img[^>]+(?:width\s*=\s*["']?1["']?[^>]+height\s*=\s*["']?1["']?|height\s*=\s*["']?1["']?[^>]+width\s*=\s*["']?1["']?)/i, label: "1x1 tracking pixel (web beacon)", severity: "HIGH" },
3721
+ { pattern: /<img[^>]+style\s*=\s*["'][^"']*(?:display\s*:\s*none|visibility\s*:\s*hidden|opacity\s*:\s*0|width\s*:\s*0|height\s*:\s*0)/i, label: "CSS-hidden image beacon", severity: "HIGH" },
3722
+ // Group D: Steganography tool references
3723
+ { pattern: /\b(?:steghide|stegano|openstego|zsteg|stegsolve|stegdetect|steganograph(?:y|ic)?|outguess|pixelknot|deepsteg|stegpy)\b/i, label: "Steganography tool/library reference", severity: "HIGH" },
3724
+ // Group E: Canvas pixel manipulation
3725
+ { pattern: /\b(?:getImageData|putImageData|createImageData|toDataURL|drawImage)\s*\(/i, label: "Canvas API pixel manipulation", severity: "HIGH" },
3726
+ // Group F: Double extension disguise
3727
+ { pattern: /\.(?:png|jpe?g|gif|bmp|webp|svg|ico|tiff?)\.(?:exe|sh|bat|cmd|ps1|py|rb|pl|js|vbs|com|scr|msi)\b/i, label: "Double file extension (executable disguised as image)", severity: "CRITICAL" },
3728
+ // Group G: Obfuscated image URLs
3729
+ { pattern: /!\[.*?\]\([^)]*(?:%[0-9a-fA-F]{2}){5,}[^)]*\)/, label: "Excessive URL encoding in image URL", severity: "MEDIUM" }
3730
+ ];
3731
+ var SYSTEM_PROMPT_EXTRACTION_PATTERNS = [
3732
+ { pattern: /\b(?:reveal|show|print|output|display|repeat|echo)\s+(?:your\s+)?(?:system\s+)?(?:prompt|instructions|rules|guidelines)/i, label: "System prompt reveal request", severity: "HIGH" },
3733
+ { pattern: /\brepeat\s+(?:the\s+)?(?:words?|text|everything)\s+above\b/i, label: "Repeat words above", severity: "HIGH" },
3734
+ { pattern: /\bprint\s+everything\s+above\b/i, label: "Print everything above", severity: "HIGH" },
3735
+ { pattern: /\bwhat\s+(?:are|were)\s+your\s+(?:original\s+)?(?:instructions|directives|rules)\b/i, label: "Ask for original instructions", severity: "MEDIUM" },
3736
+ { pattern: /\btell\s+me\s+your\s+(?:system\s+)?prompt\b/i, label: "Ask for system prompt", severity: "HIGH" }
3737
+ ];
3738
+ var ARGUMENT_INJECTION_PATTERNS = [
3739
+ { pattern: /\$\([^)]+\)/, label: "Command substitution $()", severity: "CRITICAL" },
3740
+ { pattern: /\$\{[^}]+\}/, label: "Variable expansion ${}", severity: "HIGH" },
3741
+ { pattern: /`[^`]+`/, label: "Backtick command substitution", severity: "HIGH" },
3742
+ { pattern: /;\s*(?:rm|cat|curl|wget|nc|python|perl|ruby|sh|bash)\b/, label: "Shell metachar chained command", severity: "CRITICAL" },
3743
+ { pattern: /\|\s*(?:sh|bash|zsh|python|perl|ruby|node)\b/, label: "Pipe to interpreter", severity: "CRITICAL" },
3744
+ { pattern: /&&\s*(?:rm|curl|wget|nc|python|perl|ruby)\b/, label: "AND-chained dangerous command", severity: "HIGH" },
3745
+ // GTFOBINS exploitation flags
3746
+ { pattern: /\b(?:tar|zip|find|vim|less|more|man|nmap)\b.*--(?:exec|checkpoint-action|to-command)/i, label: "GTFOBINS exploitation flags", severity: "CRITICAL" }
3747
+ ];
3748
+ var CROSS_TOOL_PATTERNS = [
3749
+ { pattern: /(?:read|cat|view)\s+(?:the\s+)?(?:file|content)[\s\S]{0,100}(?:send|post|upload|transmit|exfiltrate)/is, label: "Read-then-exfiltrate pattern", severity: "HIGH" },
3750
+ { pattern: /(?:first|step\s*1)[\s\S]{0,200}(?:then|step\s*2|next|after\s+that)[\s\S]{0,200}(?:then|step\s*3|finally)/is, label: "Multi-step tool invocation", severity: "HIGH" },
3751
+ { pattern: /\b(?:use_mcp_tool|call_tool|execute_tool|run_tool|invoke_tool)\b/i, label: "Direct tool reference", severity: "MEDIUM" },
3752
+ { pattern: /\b(?:read_file|write_file|execute_command|list_directory|search_files)\s*\(/i, label: "Tool function call syntax", severity: "MEDIUM" }
3753
+ ];
3754
+ var EXCESSIVE_PERMISSION_PATTERNS = [
3755
+ { pattern: /\bunrestricted\s+access\b/i, label: "Unrestricted access request", severity: "HIGH" },
3756
+ { pattern: /\bbypass\s+(?:security|restrictions?|permissions?|safeguards?|protections?|filters?)\b/i, label: "Security bypass request", severity: "HIGH" },
3757
+ { pattern: /\bno\s+restrictions?\b/i, label: "No restrictions request", severity: "HIGH" },
3758
+ { pattern: /\b(?:root|admin(?:istrator)?|superuser)\s+(?:access|privileges?|permissions?)\b/i, label: "Root/admin access request", severity: "HIGH" },
3759
+ { pattern: /\bdisable\s+(?:all\s+)?(?:safety|security|restrictions?|protections?|checks?|filters?)\b/i, label: "Disable safety request", severity: "HIGH" },
3760
+ { pattern: /\bfull\s+(?:system\s+)?(?:access|control|permissions?)\b/i, label: "Full access request", severity: "MEDIUM" }
3761
+ ];
3762
+
3763
+ // src/lib/skill-scanner.ts
3764
+ var SEVERITY_ORDER2 = {
3765
+ NONE: 0,
3766
+ LOW: 1,
3767
+ MEDIUM: 2,
3768
+ HIGH: 3,
3769
+ CRITICAL: 4
3770
+ };
3771
+ function getLineNumber(content, index) {
3772
+ let line = 1;
3773
+ for (let i = 0; i < index && i < content.length; i++) {
3774
+ if (content[i] === "\n") line++;
3775
+ }
3776
+ return line;
3777
+ }
3778
+ function maxSeverity(a, b) {
3779
+ return SEVERITY_ORDER2[a] >= SEVERITY_ORDER2[b] ? a : b;
3780
+ }
3781
+ function runPatternScan(content, patterns, categoryId, categoryName, owaspRef, remediation) {
3782
+ const findings = [];
3783
+ const seen = /* @__PURE__ */ new Set();
3784
+ for (const { pattern, label, severity } of patterns) {
3785
+ const globalPattern = new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : pattern.flags + "g");
3786
+ let match;
3787
+ while ((match = globalPattern.exec(content)) !== null) {
3788
+ const key = `${categoryId}:${label}:${match.index}`;
3789
+ if (seen.has(key)) break;
3790
+ seen.add(key);
3791
+ const evidence = match[0].length > 120 ? match[0].slice(0, 120) + "..." : match[0];
3792
+ findings.push({
3793
+ categoryId,
3794
+ categoryName,
3795
+ severity,
3796
+ reason: label,
3797
+ evidence,
3798
+ owaspRef,
3799
+ remediation,
3800
+ lineNumber: getLineNumber(content, match.index)
3801
+ });
3802
+ break;
3803
+ }
3804
+ }
3805
+ return findings;
3806
+ }
3807
+ function scanHiddenContent(content) {
3808
+ return runPatternScan(
3809
+ content,
3810
+ HIDDEN_CONTENT_PATTERNS,
3811
+ "SK-HID",
3812
+ "Hidden Content",
3813
+ "LLM01",
3814
+ "Remove all hidden content. HTML comments, zero-width characters, and CSS hiding can conceal malicious instructions from human reviewers."
3815
+ );
3816
+ }
3817
+ function scanPromptInjection(content) {
3818
+ return runPatternScan(
3819
+ content,
3820
+ PROMPT_INJECTION_PATTERNS,
3821
+ "SK-INJ",
3822
+ "Prompt Injection",
3823
+ "LLM01",
3824
+ "Remove prompt injection vectors. These phrases attempt to override the agent's instructions and redirect its behavior."
3825
+ );
3826
+ }
3827
+ function scanShellExecution(content) {
3828
+ return runPatternScan(
3829
+ content,
3830
+ SHELL_EXECUTION_PATTERNS,
3831
+ "SK-EXE",
3832
+ "Shell Execution",
3833
+ "LLM06",
3834
+ "Remove dangerous shell commands. Skill definitions should not contain executable code, reverse shells, or remote code execution patterns."
3835
+ );
3836
+ }
3837
+ function scanDataExfiltration(content) {
3838
+ const findings = runPatternScan(
3839
+ content,
3840
+ DATA_EXFILTRATION_PATTERNS,
3841
+ "SK-EXF",
3842
+ "Data Exfiltration",
3843
+ "LLM02",
3844
+ "Remove or replace exfiltration URLs. These destinations are commonly used to steal data from compromised systems."
3845
+ );
3846
+ for (const { pattern, label } of EXFILTRATION_URLS) {
3847
+ const globalPattern = new RegExp(pattern.source, "gi");
3848
+ const match = globalPattern.exec(content);
3849
+ if (match) {
3850
+ const alreadyFound = findings.some((f) => f.evidence?.includes(match[0]));
3851
+ if (!alreadyFound) {
3852
+ findings.push({
3853
+ categoryId: "SK-EXF",
3854
+ categoryName: "Data Exfiltration",
3855
+ severity: "HIGH",
3856
+ reason: `Exfiltration URL: ${label}`,
3857
+ evidence: match[0],
3858
+ owaspRef: "LLM02",
3859
+ remediation: "Remove or replace exfiltration URLs.",
3860
+ lineNumber: getLineNumber(content, match.index)
3861
+ });
3862
+ }
3863
+ }
3864
+ }
3865
+ return findings;
3866
+ }
3867
+ function scanEmbeddedSecrets(content) {
3868
+ const { types } = scanForSecrets(content);
3869
+ if (types.length === 0) return [];
3870
+ const findings = [];
3871
+ for (const secretType of types) {
3872
+ findings.push({
3873
+ categoryId: "SK-SEC",
3874
+ categoryName: "Embedded Secrets",
3875
+ severity: "CRITICAL",
3876
+ reason: `Embedded credential: ${secretType}`,
3877
+ owaspRef: "LLM02",
3878
+ remediation: "Remove all hardcoded credentials. Use environment variables or a secrets manager instead."
3879
+ });
3880
+ }
3881
+ return findings;
3882
+ }
3883
+ function scanSensitiveFileRefs(content) {
3884
+ const findings = [];
3885
+ const seen = /* @__PURE__ */ new Set();
3886
+ for (const { pattern, label, readSeverity } of SENSITIVE_PATH_RULES) {
3887
+ const globalPattern = new RegExp(pattern.source, "g");
3888
+ const match = globalPattern.exec(content);
3889
+ if (match && !seen.has(label)) {
3890
+ seen.add(label);
3891
+ findings.push({
3892
+ categoryId: "SK-SFA",
3893
+ categoryName: "Sensitive File References",
3894
+ severity: readSeverity,
3895
+ reason: `Reference to sensitive path: ${label}`,
3896
+ evidence: match[0],
3897
+ owaspRef: "LLM02",
3898
+ remediation: "Remove references to sensitive files and directories. Skills should not instruct agents to access credentials, keys, or system auth files.",
3899
+ lineNumber: getLineNumber(content, match.index)
3900
+ });
3901
+ }
3902
+ }
3903
+ return findings;
3904
+ }
3905
+ function scanMemoryPoisoning(content) {
3906
+ return runPatternScan(
3907
+ content,
3908
+ MEMORY_POISONING_PATTERNS,
3909
+ "SK-MEM",
3910
+ "Memory/Config Poisoning",
3911
+ "LLM05",
3912
+ "Remove instructions that modify agent memory or configuration files. This is a persistence technique that can compromise future sessions."
3913
+ );
3914
+ }
3915
+ function scanSupplyChainRisk(content) {
3916
+ return runPatternScan(
3917
+ content,
3918
+ SUPPLY_CHAIN_PATTERNS,
3919
+ "SK-SUP",
3920
+ "Supply Chain Risk",
3921
+ "LLM03",
3922
+ "Verify all external dependencies and script URLs. Prefer pinned versions and checksums over arbitrary remote scripts."
3923
+ );
3924
+ }
3925
+ function scanEncodedPayloads(content) {
3926
+ return runPatternScan(
3927
+ content,
3928
+ ENCODED_PAYLOAD_PATTERNS,
3929
+ "SK-B64",
3930
+ "Encoded Payloads",
3931
+ "LLM01",
3932
+ "Decode and inspect encoded content. Base64 and hex encoding is commonly used to evade pattern detection in malicious skills."
3933
+ );
3934
+ }
3935
+ function scanImageExfiltration(content) {
3936
+ return runPatternScan(
3937
+ content,
3938
+ IMAGE_EXFIL_PATTERNS,
3939
+ "SK-IMG",
3940
+ "Image Exfiltration",
3941
+ "LLM02",
3942
+ "Remove image tags with dynamic, suspicious, or embedded content. Images can exfiltrate data via query parameters, inline data URIs, SVG scripts, tracking pixels, or steganographic encoding. Avoid data: URIs, SVG images with scripts, and references to steganography tools."
3943
+ );
3944
+ }
3945
+ function scanSystemPromptExtraction(content) {
3946
+ return runPatternScan(
3947
+ content,
3948
+ SYSTEM_PROMPT_EXTRACTION_PATTERNS,
3949
+ "SK-SYS",
3950
+ "System Prompt Extraction",
3951
+ "LLM01",
3952
+ "Remove instructions that attempt to extract system prompts. This information can be used to craft more effective attacks."
3953
+ );
3954
+ }
3955
+ function scanArgumentInjection(content) {
3956
+ return runPatternScan(
3957
+ content,
3958
+ ARGUMENT_INJECTION_PATTERNS,
3959
+ "SK-ARG",
3960
+ "Argument Injection",
3961
+ "LLM01",
3962
+ "Remove shell metacharacters and command substitution patterns. These can be used to inject arbitrary commands via tool arguments."
3963
+ );
3964
+ }
3965
+ function scanCrossToolChaining(content) {
3966
+ return runPatternScan(
3967
+ content,
3968
+ CROSS_TOOL_PATTERNS,
3969
+ "SK-XTL",
3970
+ "Cross-Tool Chaining",
3971
+ "LLM05",
3972
+ "Review multi-step tool invocation instructions carefully. Attackers chain legitimate tools to achieve malicious outcomes."
3973
+ );
3974
+ }
3975
+ function scanExcessivePermissions(content) {
3976
+ return runPatternScan(
3977
+ content,
3978
+ EXCESSIVE_PERMISSION_PATTERNS,
3979
+ "SK-PRM",
3980
+ "Excessive Permissions",
3981
+ "LLM01",
3982
+ "Remove requests for unrestricted access or security bypasses. Skills should operate with minimal required permissions."
3983
+ );
3984
+ }
3985
+ function scanSuspiciousStructure(content) {
3986
+ const findings = [];
3987
+ if (content.length > 1e4) {
3988
+ findings.push({
3989
+ categoryId: "SK-STR",
3990
+ categoryName: "Suspicious Structure",
3991
+ severity: "MEDIUM",
3992
+ reason: `Unusually large skill definition (${content.length.toLocaleString()} characters)`,
3993
+ remediation: "Large skill definitions have more surface area for hidden threats. Consider splitting into smaller, focused skills."
3994
+ });
3995
+ }
3996
+ const lines = content.split("\n").filter((l) => l.trim().length > 0);
3997
+ if (lines.length > 10) {
3998
+ const imperativePattern = /^\s*(?:you\s+(?:must|should|will|need\s+to)|always|never|do\s+not|ensure|make\s+sure|immediately|execute|run|create|write|read|send|post|upload|download|install|delete|remove|modify|change|update)/i;
3999
+ const imperativeCount = lines.filter((l) => imperativePattern.test(l)).length;
4000
+ const ratio = imperativeCount / lines.length;
4001
+ if (ratio > 0.3) {
4002
+ findings.push({
4003
+ categoryId: "SK-STR",
4004
+ categoryName: "Suspicious Structure",
4005
+ severity: "MEDIUM",
4006
+ reason: `High imperative instruction density (${Math.round(ratio * 100)}% of lines are directives)`,
4007
+ remediation: "Skills with a high density of imperative instructions may be attempting to control agent behavior beyond their stated purpose."
4008
+ });
4009
+ }
4010
+ }
4011
+ return findings;
4012
+ }
4013
+ function scanSkillDefinition(content) {
4014
+ const startTime = performance.now();
4015
+ const allFindings = [
4016
+ ...scanHiddenContent(content),
4017
+ ...scanPromptInjection(content),
4018
+ ...scanShellExecution(content),
4019
+ ...scanDataExfiltration(content),
4020
+ ...scanEmbeddedSecrets(content),
4021
+ ...scanSensitiveFileRefs(content),
4022
+ ...scanMemoryPoisoning(content),
4023
+ ...scanSupplyChainRisk(content),
4024
+ ...scanEncodedPayloads(content),
4025
+ ...scanImageExfiltration(content),
4026
+ ...scanSystemPromptExtraction(content),
4027
+ ...scanArgumentInjection(content),
4028
+ ...scanCrossToolChaining(content),
4029
+ ...scanExcessivePermissions(content),
4030
+ ...scanSuspiciousStructure(content)
4031
+ ];
4032
+ const scanDurationMs = Math.round(performance.now() - startTime);
4033
+ const summary = { critical: 0, high: 0, medium: 0, low: 0 };
4034
+ let overallSeverity = "NONE";
4035
+ for (const f of allFindings) {
4036
+ if (f.severity === "CRITICAL") summary.critical++;
4037
+ else if (f.severity === "HIGH") summary.high++;
4038
+ else if (f.severity === "MEDIUM") summary.medium++;
4039
+ else if (f.severity === "LOW") summary.low++;
4040
+ overallSeverity = maxSeverity(overallSeverity, f.severity);
4041
+ }
4042
+ allFindings.sort((a, b) => SEVERITY_ORDER2[b.severity] - SEVERITY_ORDER2[a.severity]);
4043
+ return {
4044
+ overallSeverity,
4045
+ findings: allFindings,
4046
+ summary,
4047
+ scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
4048
+ contentLength: content.length,
4049
+ scanDurationMs
4050
+ };
4051
+ }
4052
+ function collectRanges(content, pattern) {
4053
+ const ranges = [];
4054
+ const flags = pattern.flags.includes("g") ? pattern.flags : pattern.flags + "g";
4055
+ const re = new RegExp(pattern.source, flags);
4056
+ let m;
4057
+ while ((m = re.exec(content)) !== null) {
4058
+ ranges.push({ start: m.index, end: m.index + m[0].length });
4059
+ if (m[0].length === 0) break;
4060
+ }
4061
+ return ranges;
4062
+ }
4063
+ function cleanSkillDefinition(content) {
4064
+ const ranges = [];
4065
+ const allScanPatterns = [
4066
+ ...HIDDEN_CONTENT_PATTERNS,
4067
+ ...PROMPT_INJECTION_PATTERNS,
4068
+ ...SHELL_EXECUTION_PATTERNS,
4069
+ ...DATA_EXFILTRATION_PATTERNS,
4070
+ ...MEMORY_POISONING_PATTERNS,
4071
+ ...SUPPLY_CHAIN_PATTERNS,
4072
+ ...ENCODED_PAYLOAD_PATTERNS,
4073
+ ...IMAGE_EXFIL_PATTERNS,
4074
+ ...SYSTEM_PROMPT_EXTRACTION_PATTERNS,
4075
+ ...ARGUMENT_INJECTION_PATTERNS,
4076
+ ...CROSS_TOOL_PATTERNS,
4077
+ ...EXCESSIVE_PERMISSION_PATTERNS
4078
+ ];
4079
+ for (const { pattern } of allScanPatterns) {
4080
+ ranges.push(...collectRanges(content, pattern));
4081
+ }
4082
+ for (const { pattern } of EXFILTRATION_URLS) {
4083
+ ranges.push(...collectRanges(content, pattern));
4084
+ }
4085
+ for (const { pattern } of SENSITIVE_PATH_RULES) {
4086
+ ranges.push(...collectRanges(content, pattern));
4087
+ }
4088
+ for (const { pattern } of SECRET_PATTERNS) {
4089
+ ranges.push(...collectRanges(content, pattern));
4090
+ }
4091
+ if (ranges.length === 0) return { cleanedContent: content, removedCount: 0 };
4092
+ ranges.sort((a, b) => a.start - b.start);
4093
+ const merged = [{ ...ranges[0] }];
4094
+ for (let i = 1; i < ranges.length; i++) {
4095
+ const last = merged[merged.length - 1];
4096
+ if (ranges[i].start <= last.end) {
4097
+ last.end = Math.max(last.end, ranges[i].end);
4098
+ } else {
4099
+ merged.push({ ...ranges[i] });
4100
+ }
4101
+ }
4102
+ let cleaned = content;
4103
+ for (let i = merged.length - 1; i >= 0; i--) {
4104
+ cleaned = cleaned.slice(0, merged[i].start) + cleaned.slice(merged[i].end);
4105
+ }
4106
+ cleaned = cleaned.replace(/\n{3,}/g, "\n\n").trim() + "\n";
4107
+ return { cleanedContent: cleaned, removedCount: ranges.length };
4108
+ }
4109
+
4110
+ // src/services/security-posture.ts
4111
+ import fs9 from "fs";
4112
+ import { eq as eq5, ne as ne2, and as and4, sql as sql6 } from "drizzle-orm";
4113
+ function buildLayer(id, name, checks) {
4114
+ const passedCount = checks.filter((c) => c.passed).length;
4115
+ const totalCount = checks.length;
4116
+ let status;
4117
+ if (passedCount === totalCount) {
4118
+ status = "configured";
4119
+ } else if (passedCount === 0) {
4120
+ status = "unconfigured";
4121
+ } else {
4122
+ status = "partial";
4123
+ }
4124
+ return { id, name, status, checks, passedCount, totalCount };
4125
+ }
4126
+ function checkSandboxLayer() {
4127
+ const config = readOpenClawConfig();
4128
+ const sandbox = config?.agents?.defaults?.sandbox;
4129
+ const mode = sandbox?.mode;
4130
+ const workspaceAccess = sandbox?.workspaceAccess;
4131
+ const dockerNetwork = sandbox?.docker?.network;
4132
+ const checks = [
4133
+ {
4134
+ id: "sandbox-mode",
4135
+ label: "Sandbox mode enabled",
4136
+ passed: mode === "all" || mode === "non-main",
4137
+ detail: mode ? `Sandbox mode is "${mode}"` : "Sandbox mode is not configured",
4138
+ severity: "critical"
4139
+ },
4140
+ {
4141
+ id: "sandbox-workspace",
4142
+ label: "Workspace access restricted",
4143
+ passed: workspaceAccess === "ro" || workspaceAccess === "none",
4144
+ detail: workspaceAccess ? `Workspace access is "${workspaceAccess}"` : "Workspace access not configured (defaults to rw)",
4145
+ severity: "warning"
4146
+ },
4147
+ {
4148
+ id: "sandbox-network",
4149
+ label: "Docker network isolated",
4150
+ passed: dockerNetwork != null && dockerNetwork !== "host",
4151
+ detail: dockerNetwork ? `Docker network set to "${dockerNetwork}"` : "Docker network not configured",
4152
+ severity: "info"
4153
+ }
4154
+ ];
4155
+ return buildLayer("sandbox", "Sandbox Isolation", checks);
4156
+ }
4157
+ function checkFilesystemLayer() {
4158
+ const accessState = deriveAccessState();
4159
+ const fsToggle = accessState.toggles.find((t) => t.category === "filesystem");
4160
+ const config = readOpenClawConfig();
4161
+ const workspace = config?.agents?.defaults?.workspace;
4162
+ const workspaceAccess = config?.agents?.defaults?.sandbox?.workspaceAccess;
4163
+ const checks = [
4164
+ {
4165
+ id: "fs-toggle",
4166
+ label: "Filesystem access controlled",
4167
+ passed: fsToggle != null && !fsToggle.enabled,
4168
+ detail: fsToggle?.enabled === false ? "Filesystem tool group is disabled" : "Filesystem tool group is enabled (agent has file access)",
4169
+ severity: "warning"
4170
+ },
4171
+ {
4172
+ id: "fs-workspace",
4173
+ label: "Workspace path configured",
4174
+ passed: workspace != null && workspace.length > 0,
4175
+ detail: workspace ? `Workspace: ${workspace}` : "No explicit workspace path configured",
4176
+ severity: "info"
4177
+ },
4178
+ {
4179
+ id: "fs-workspace-restriction",
4180
+ label: "Workspace access restricted",
4181
+ passed: workspaceAccess === "ro" || workspaceAccess === "none",
4182
+ detail: workspaceAccess ? `Workspace access level: "${workspaceAccess}"` : "Workspace access not restricted (defaults to rw)",
4183
+ severity: "warning"
4184
+ }
4185
+ ];
4186
+ return buildLayer("filesystem", "Filesystem Access", checks);
4187
+ }
4188
+ function checkNetworkLayer() {
4189
+ const accessState = deriveAccessState();
4190
+ const netToggle = accessState.toggles.find((t) => t.category === "network");
4191
+ const config = readOpenClawConfig();
4192
+ const browserEnabled = config?.browser?.enabled;
4193
+ const sandbox = config?.agents?.defaults?.sandbox;
4194
+ const dockerNetwork = sandbox?.docker?.network;
4195
+ const sandboxMode = sandbox?.mode;
4196
+ const checks = [
4197
+ {
4198
+ id: "net-toggle",
4199
+ label: "Network access controlled",
4200
+ passed: netToggle != null && !netToggle.enabled,
4201
+ detail: netToggle?.enabled === false ? "Network tool group is disabled" : "Network tool group is enabled",
4202
+ severity: "warning"
4203
+ },
4204
+ {
4205
+ id: "net-browser",
4206
+ label: "Browser disabled",
4207
+ passed: browserEnabled === false,
4208
+ detail: browserEnabled === false ? "Browser is disabled" : "Browser is enabled (agent can browse web)",
4209
+ severity: "info"
4210
+ },
4211
+ {
4212
+ id: "net-docker-isolation",
4213
+ label: "Network isolated in sandbox",
4214
+ passed: (sandboxMode === "all" || sandboxMode === "non-main") && dockerNetwork != null && dockerNetwork !== "host",
4215
+ detail: sandboxMode === "all" || sandboxMode === "non-main" ? dockerNetwork && dockerNetwork !== "host" ? `Sandboxed with isolated network "${dockerNetwork}"` : "Sandboxed but no network isolation configured" : "Not sandboxed \u2014 no network isolation",
4216
+ severity: "critical"
4217
+ }
4218
+ ];
4219
+ return buildLayer("network", "Network & Egress Control", checks);
4220
+ }
4221
+ async function checkCommandExecLayer() {
4222
+ const accessState = deriveAccessState();
4223
+ const sysToggle = accessState.toggles.find(
4224
+ (t) => t.category === "system_commands"
4225
+ );
4226
+ const config = readOpenClawConfig();
4227
+ const execSecurity = config?.tools?.exec?.security;
4228
+ const db = getDb();
4229
+ const patternRows = await db.select().from(schema_exports.restrictedPatterns);
4230
+ const patternCount = patternRows.length;
4231
+ const patternTexts = patternRows.map((r) => r.pattern.toLowerCase());
4232
+ const criticalPatterns = ["sudo", "rm -rf", "chmod", "curl"];
4233
+ const hasCritical = criticalPatterns.some(
4234
+ (cp) => patternTexts.some((pt) => pt.includes(cp))
4235
+ );
4236
+ const checks = [
4237
+ {
4238
+ id: "exec-toggle",
4239
+ label: "System commands controlled",
4240
+ passed: sysToggle != null && !sysToggle.enabled,
4241
+ detail: sysToggle?.enabled === false ? "System commands tool group is disabled" : "System commands tool group is enabled",
4242
+ severity: "warning"
4243
+ },
4244
+ {
4245
+ id: "exec-security-mode",
4246
+ label: "Exec security mode restrictive",
4247
+ passed: execSecurity === "deny" || execSecurity === "allowlist",
4248
+ detail: execSecurity ? `Exec security mode: "${execSecurity}"` : "Exec security mode not configured",
4249
+ severity: "critical"
4250
+ },
4251
+ {
4252
+ id: "exec-patterns",
4253
+ label: "Restricted patterns configured",
4254
+ passed: patternCount > 0,
4255
+ detail: patternCount > 0 ? `${patternCount} restricted pattern(s) in blocklist` : "No restricted patterns \u2014 all commands pass through",
4256
+ severity: "warning"
4257
+ },
4258
+ {
4259
+ id: "exec-critical-patterns",
4260
+ label: "Critical command patterns blocked",
4261
+ passed: hasCritical,
4262
+ detail: hasCritical ? "Critical patterns (sudo, rm -rf, chmod, curl|bash) present" : "No critical patterns found \u2014 consider adding sudo, rm -rf, chmod",
4263
+ severity: "critical"
4264
+ }
4265
+ ];
4266
+ return buildLayer("exec", "Command Execution Controls", checks);
4267
+ }
4268
+ function checkMcpLayer() {
4269
+ const accessState = deriveAccessState();
4270
+ const mcpToggle = accessState.toggles.find(
4271
+ (t) => t.category === "mcp_servers"
4272
+ );
4273
+ const servers = accessState.mcpServers;
4274
+ const config = readOpenClawConfig();
4275
+ const denyList = config?.tools?.deny ?? [];
4276
+ const mcpDenyEntries = denyList.filter((d) => d.startsWith("mcp__"));
4277
+ const totalServers = servers.length;
4278
+ const enabledServers = servers.filter((s) => s.effectivelyEnabled).length;
4279
+ const disabledServers = totalServers - enabledServers;
4280
+ const checks = [
4281
+ {
4282
+ id: "mcp-toggle",
4283
+ label: "MCP servers controlled",
4284
+ passed: mcpToggle != null && accessState.openclawConfigAvailable,
4285
+ detail: !accessState.openclawConfigAvailable ? "OpenClaw config unavailable" : mcpToggle?.enabled ? "MCP servers toggle is enabled" : "MCP servers toggle is disabled (all servers blocked)",
4286
+ severity: "info"
4287
+ },
4288
+ {
4289
+ id: "mcp-server-review",
4290
+ label: "Servers individually reviewed",
4291
+ passed: totalServers === 0 || disabledServers > 0 || mcpDenyEntries.length > 0,
4292
+ detail: totalServers === 0 ? "No MCP servers configured" : disabledServers > 0 ? `${disabledServers}/${totalServers} server(s) disabled` : "All servers enabled \u2014 consider reviewing each server",
4293
+ severity: "warning"
4294
+ },
4295
+ {
4296
+ id: "mcp-tools-deny",
4297
+ label: "MCP tools in deny list",
4298
+ passed: mcpDenyEntries.length > 0,
4299
+ detail: mcpDenyEntries.length > 0 ? `${mcpDenyEntries.length} MCP tool deny entr(ies) configured` : "No MCP-specific deny entries",
4300
+ severity: "info"
4301
+ }
4302
+ ];
4303
+ return buildLayer("mcp", "MCP Server Security", checks);
4304
+ }
4305
+ async function checkSecretLayer() {
4306
+ const db = getDb();
4307
+ const secretActivities = await db.select().from(schema_exports.agentActivities).where(
4308
+ and4(
4309
+ eq5(schema_exports.agentActivities.resolved, 0),
4310
+ sql6`${schema_exports.agentActivities.threatFindings} LIKE '%TC-SEC%'`
4311
+ )
4312
+ );
4313
+ const sfaActivities = await db.select().from(schema_exports.agentActivities).where(
4314
+ and4(
4315
+ eq5(schema_exports.agentActivities.resolved, 0),
4316
+ sql6`${schema_exports.agentActivities.threatFindings} LIKE '%TC-SFA%'`
4317
+ )
4318
+ );
4319
+ const checks = [
4320
+ {
4321
+ id: "secret-scanner",
4322
+ label: "Secret scanner active",
4323
+ passed: true,
4324
+ detail: "Built-in secret scanner is always active",
4325
+ severity: "info"
4326
+ },
4327
+ {
4328
+ id: "secret-exposure",
4329
+ label: "No unresolved secret exposures",
4330
+ passed: secretActivities.length === 0,
4331
+ detail: secretActivities.length === 0 ? "No unresolved secret exposure threats" : `${secretActivities.length} unresolved secret exposure threat(s)`,
4332
+ severity: "critical"
4333
+ },
4334
+ {
4335
+ id: "secret-file-access",
4336
+ label: "No unresolved sensitive file access",
4337
+ passed: sfaActivities.length === 0,
4338
+ detail: sfaActivities.length === 0 ? "No unresolved sensitive file access threats" : `${sfaActivities.length} unresolved sensitive file access threat(s)`,
4339
+ severity: "warning"
4340
+ }
4341
+ ];
4342
+ return buildLayer("secrets", "Secret & Credential Protection", checks);
4343
+ }
4344
+ async function checkThreatMonitoringLayer() {
4345
+ const monitor = getOpenClawMonitor();
4346
+ const connectionStatus = monitor?.getStatus() ?? "not_configured";
4347
+ const config = readOpenClawConfig();
4348
+ const db = getDb();
4349
+ const activeSessions = await db.select({ count: sql6`count(*)` }).from(schema_exports.openclawSessions).where(eq5(schema_exports.openclawSessions.status, "ACTIVE"));
4350
+ const totalThreats = await db.select({ count: sql6`count(*)` }).from(schema_exports.agentActivities).where(ne2(schema_exports.agentActivities.threatLevel, "NONE"));
4351
+ const resolvedThreats = await db.select({ count: sql6`count(*)` }).from(schema_exports.agentActivities).where(
4352
+ and4(
4353
+ ne2(schema_exports.agentActivities.threatLevel, "NONE"),
4354
+ eq5(schema_exports.agentActivities.resolved, 1)
4355
+ )
4356
+ );
4357
+ const total = totalThreats[0]?.count ?? 0;
4358
+ const resolved = resolvedThreats[0]?.count ?? 0;
4359
+ const resolutionRate = total > 0 ? Math.round(resolved / total * 100) : 100;
4360
+ const checks = [
4361
+ {
4362
+ id: "monitor-connection",
4363
+ label: "OpenClaw connected",
4364
+ passed: connectionStatus === "connected",
4365
+ detail: connectionStatus === "connected" ? "Connected to OpenClaw gateway" : config ? `Connection status: ${connectionStatus}` : "OpenClaw config not found",
4366
+ severity: "critical"
4367
+ },
4368
+ {
4369
+ id: "monitor-sessions",
4370
+ label: "Session tracking active",
4371
+ passed: (activeSessions[0]?.count ?? 0) > 0 || connectionStatus === "connected",
4372
+ detail: (activeSessions[0]?.count ?? 0) > 0 ? `${activeSessions[0].count} active session(s)` : connectionStatus === "connected" ? "Connected, no active sessions" : "No active sessions (disconnected)",
4373
+ severity: "info"
4374
+ },
4375
+ {
4376
+ id: "monitor-resolution",
4377
+ label: "Threats being resolved",
4378
+ passed: resolutionRate >= 50 || total === 0,
4379
+ detail: total === 0 ? "No threats detected yet" : `${resolved}/${total} threats resolved (${resolutionRate}%)`,
4380
+ severity: "warning"
4381
+ }
4382
+ ];
4383
+ return buildLayer("monitoring", "Threat Monitoring", checks);
4384
+ }
4385
+ async function checkHumanInLoopLayer() {
4386
+ const monitor = getOpenClawMonitor();
4387
+ const db = getDb();
4388
+ const patternRows = await db.select().from(schema_exports.restrictedPatterns);
4389
+ const patternCount = patternRows.length;
4390
+ const totalApprovals = await db.select({ count: sql6`count(*)` }).from(schema_exports.execApprovals);
4391
+ const timedOut = await db.select({ count: sql6`count(*)` }).from(schema_exports.execApprovals).where(eq5(schema_exports.execApprovals.decidedBy, "auto-deny"));
4392
+ const total = totalApprovals[0]?.count ?? 0;
4393
+ const timedOutCount = timedOut[0]?.count ?? 0;
4394
+ const timeoutRate = total > 0 ? Math.round(timedOutCount / total * 100) : 0;
4395
+ const checks = [
4396
+ {
4397
+ id: "hitl-active",
4398
+ label: "Exec approval system active",
4399
+ passed: monitor != null,
4400
+ detail: monitor ? "Exec approval system is running" : "Exec approval system not initialized",
4401
+ severity: "critical"
4402
+ },
4403
+ {
4404
+ id: "hitl-timeout-rate",
4405
+ label: "Approval timeout rate acceptable",
4406
+ passed: timeoutRate < 20 || total === 0,
4407
+ detail: total === 0 ? "No approval requests yet" : `${timedOutCount}/${total} approvals timed out (${timeoutRate}%)`,
4408
+ severity: "warning"
4409
+ },
4410
+ {
4411
+ id: "hitl-patterns",
4412
+ label: "Restricted patterns configured",
4413
+ passed: patternCount > 0,
4414
+ detail: patternCount > 0 ? `${patternCount} restricted pattern(s) for interception` : "No restricted patterns \u2014 nothing to intercept",
4415
+ severity: "warning"
4416
+ }
4417
+ ];
4418
+ return buildLayer("human-in-loop", "Human-in-the-Loop Controls", checks);
4419
+ }
4420
+ async function checkEgressProxyLayer() {
4421
+ const proxyConfigured = !!(process.env.HTTP_PROXY || process.env.HTTPS_PROXY || process.env.http_proxy || process.env.https_proxy);
4422
+ const noProxy = process.env.NO_PROXY || process.env.no_proxy || "";
4423
+ const proxyBypassed = noProxy.trim() === "*";
4424
+ const srtStatus = getSrtStatus();
4425
+ const srtActive = srtStatus.enabled && srtStatus.installed;
4426
+ const srtHasDomainRules = srtStatus.settings != null && (srtStatus.settings.network.allowedDomains.length > 0 || srtStatus.settings.network.deniedDomains.length > 0);
4427
+ const db = getDb();
4428
+ const exfilActivities = await db.select({ count: sql6`count(*)` }).from(schema_exports.agentActivities).where(
4429
+ and4(
4430
+ eq5(schema_exports.agentActivities.resolved, 0),
4431
+ sql6`${schema_exports.agentActivities.threatFindings} LIKE '%TC-EXF%'`
4432
+ )
4433
+ );
4434
+ const netActivities = await db.select({ count: sql6`count(*)` }).from(schema_exports.agentActivities).where(
4435
+ and4(
4436
+ eq5(schema_exports.agentActivities.resolved, 0),
4437
+ sql6`${schema_exports.agentActivities.threatFindings} LIKE '%TC-NET%'`
4438
+ )
4439
+ );
4440
+ const exfilCount = exfilActivities[0]?.count ?? 0;
4441
+ const netCount = netActivities[0]?.count ?? 0;
4442
+ const checks = [
4443
+ {
4444
+ id: "egress-filtering-configured",
4445
+ label: "Egress filtering configured",
4446
+ passed: proxyConfigured || srtActive,
4447
+ detail: srtActive ? "Sandbox Runtime (srt) is active \u2014 network egress is filtered" : proxyConfigured ? "HTTP/HTTPS proxy environment variables are set" : "No egress filtering \u2014 configure srt or set HTTP_PROXY/HTTPS_PROXY",
4448
+ severity: "warning"
4449
+ },
4450
+ {
4451
+ id: "egress-domain-rules",
4452
+ label: "Domain filtering rules configured",
4453
+ passed: srtHasDomainRules,
4454
+ detail: srtHasDomainRules ? `srt domain rules: ${srtStatus.settings.network.allowedDomains.length} allowed, ${srtStatus.settings.network.deniedDomains.length} denied` : srtActive ? "srt is active but no domain allow/deny rules configured" : "No domain filtering rules \u2014 enable srt and add allowed domains",
4455
+ severity: "warning"
4456
+ },
4457
+ {
4458
+ id: "egress-no-proxy-safe",
4459
+ label: "Proxy not globally bypassed",
4460
+ passed: !proxyBypassed,
4461
+ detail: proxyBypassed ? "NO_PROXY is set to '*' \u2014 all proxy filtering is bypassed" : noProxy ? `NO_PROXY exceptions: ${noProxy}` : "No NO_PROXY exceptions set",
4462
+ severity: "critical"
4463
+ },
4464
+ {
4465
+ id: "egress-exfiltration-clean",
4466
+ label: "No unresolved exfiltration threats",
4467
+ passed: exfilCount === 0,
4468
+ detail: exfilCount === 0 ? "No unresolved data exfiltration threats" : `${exfilCount} unresolved exfiltration threat(s)`,
4469
+ severity: "critical"
4470
+ },
4471
+ {
4472
+ id: "egress-network-threats-clean",
4473
+ label: "No unresolved network threats",
4474
+ passed: netCount === 0,
4475
+ detail: netCount === 0 ? "No unresolved network threats" : `${netCount} unresolved network threat(s)`,
4476
+ severity: "warning"
4477
+ }
4478
+ ];
4479
+ return buildLayer("egress-proxy", "Egress Proxy & Domain Filtering", checks);
4480
+ }
4481
+ function checkGatewaySecurityLayer() {
4482
+ const config = readOpenClawConfig();
4483
+ const deviceExists = fs9.existsSync(OPENCLAW_DEVICE_JSON);
4484
+ const authMode = config?.gateway ? config.gateway?.auth ? config.gateway.auth?.mode : void 0 : void 0;
4485
+ const gwBind = config?.gateway ? config.gateway?.bind : void 0;
4486
+ const bindLocal = gwBind === void 0 || gwBind === null || gwBind === "127.0.0.1" || gwBind === "localhost" || gwBind === "loopback" || gwBind === "::1";
4487
+ const channels = config?.channels;
4488
+ const whatsapp = channels?.whatsapp;
4489
+ const allowFrom = whatsapp?.allowFrom;
4490
+ const channelRestricted = !whatsapp || Array.isArray(allowFrom) && allowFrom.length > 0;
4491
+ const checks = [
4492
+ {
4493
+ id: "gw-device-identity",
4494
+ label: "Device identity configured",
4495
+ passed: deviceExists,
4496
+ detail: deviceExists ? "Ed25519 device identity file exists" : "No device identity found \u2014 gateway authentication unavailable",
4497
+ severity: "critical"
4498
+ },
4499
+ {
4500
+ id: "gw-auth-mode",
4501
+ label: "Gateway authentication enabled",
4502
+ passed: authMode != null && authMode !== "",
4503
+ detail: authMode ? `Gateway auth mode: "${authMode}"` : "Gateway auth mode not configured",
4504
+ severity: "critical"
4505
+ },
4506
+ {
4507
+ id: "gw-bind-local",
4508
+ label: "Gateway bound to localhost",
4509
+ passed: bindLocal,
4510
+ detail: bindLocal ? `Gateway bind: ${gwBind ?? "default (localhost)"}` : `Gateway bound to ${gwBind} \u2014 accessible from network`,
4511
+ severity: "warning"
4512
+ },
4513
+ {
4514
+ id: "gw-channel-restricted",
4515
+ label: "External channels restricted",
4516
+ passed: channelRestricted,
4517
+ detail: channelRestricted ? whatsapp ? `WhatsApp allowFrom has ${allowFrom?.length ?? 0} entry/entries` : "No external channels configured" : "WhatsApp is open to all senders \u2014 restrict with allowFrom",
4518
+ severity: "info"
4519
+ }
4520
+ ];
4521
+ return buildLayer("gateway", "Gateway & Inbound Security", checks);
4522
+ }
4523
+ async function checkSupplyChainLayer() {
4524
+ const db = getDb();
4525
+ const supplyThreats = await db.select({ count: sql6`count(*)` }).from(schema_exports.agentActivities).where(
4526
+ and4(
4527
+ eq5(schema_exports.agentActivities.resolved, 0),
4528
+ sql6`${schema_exports.agentActivities.threatFindings} LIKE '%TC-SUP%'`
4529
+ )
4530
+ );
4531
+ const patternRows = await db.select().from(schema_exports.restrictedPatterns);
4532
+ const patternTexts = patternRows.map((r) => r.pattern.toLowerCase());
4533
+ const supplyKeywords = ["npm install", "pip install", "brew install", "curl"];
4534
+ const hasSupplyPattern = supplyKeywords.some(
4535
+ (kw) => patternTexts.some((pt) => pt.includes(kw))
4536
+ );
4537
+ const config = readOpenClawConfig();
4538
+ const execSecurity = config?.tools?.exec?.security;
4539
+ const supplyCount = supplyThreats[0]?.count ?? 0;
4540
+ const checks = [
4541
+ {
4542
+ id: "supply-chain-threats-clean",
4543
+ label: "No unresolved supply chain threats",
4544
+ passed: supplyCount === 0,
4545
+ detail: supplyCount === 0 ? "No unresolved supply chain threats" : `${supplyCount} unresolved supply chain threat(s)`,
4546
+ severity: "critical"
4547
+ },
4548
+ {
4549
+ id: "supply-exec-restricted",
4550
+ label: "Package install commands restricted",
4551
+ passed: hasSupplyPattern,
4552
+ detail: hasSupplyPattern ? "Restricted patterns cover package install commands" : "No restricted patterns for npm install, pip install, curl, etc.",
4553
+ severity: "warning"
4554
+ },
4555
+ {
4556
+ id: "supply-exec-mode",
4557
+ label: "Exec security prevents blind installs",
4558
+ passed: execSecurity === "deny" || execSecurity === "allowlist",
4559
+ detail: execSecurity ? `Exec security mode: "${execSecurity}"` : "Exec security mode not configured",
4560
+ severity: "warning"
4561
+ }
4562
+ ];
4563
+ return buildLayer("supply-chain", "Supply Chain Protection", checks);
4564
+ }
4565
+ async function checkInputOutputLayer() {
4566
+ const db = getDb();
4567
+ const injectionThreats = await db.select({ count: sql6`count(*)` }).from(schema_exports.agentActivities).where(
4568
+ and4(
4569
+ eq5(schema_exports.agentActivities.resolved, 0),
4570
+ sql6`${schema_exports.agentActivities.threatFindings} LIKE '%TC-INJ%'`
4571
+ )
4572
+ );
4573
+ const mcpPoisoning = await db.select({ count: sql6`count(*)` }).from(schema_exports.agentActivities).where(
4574
+ and4(
4575
+ eq5(schema_exports.agentActivities.resolved, 0),
4576
+ sql6`${schema_exports.agentActivities.threatFindings} LIKE '%TC-MCP%'`
4577
+ )
4578
+ );
4579
+ const injCount = injectionThreats[0]?.count ?? 0;
4580
+ const mcpCount = mcpPoisoning[0]?.count ?? 0;
4581
+ const checks = [
4582
+ {
4583
+ id: "io-injection-clean",
4584
+ label: "No unresolved prompt injection threats",
4585
+ passed: injCount === 0,
4586
+ detail: injCount === 0 ? "No unresolved prompt injection threats" : `${injCount} unresolved prompt injection threat(s)`,
4587
+ severity: "critical"
4588
+ },
4589
+ {
4590
+ id: "io-mcp-poisoning-clean",
4591
+ label: "No unresolved MCP poisoning threats",
4592
+ passed: mcpCount === 0,
4593
+ detail: mcpCount === 0 ? "No unresolved MCP tool poisoning threats" : `${mcpCount} unresolved MCP tool poisoning threat(s)`,
4594
+ severity: "critical"
4595
+ },
4596
+ {
4597
+ id: "io-content-scanner-active",
4598
+ label: "Content threat scanner active",
4599
+ passed: true,
4600
+ detail: "Built-in 10-category threat classifier is always active",
4601
+ severity: "info"
4602
+ },
4603
+ {
4604
+ id: "io-skill-scanner-available",
4605
+ label: "Skill definition scanner available",
4606
+ passed: true,
4607
+ detail: "Built-in SK-* skill scanner is available for MCP auditing",
4608
+ severity: "info"
4609
+ }
4610
+ ];
4611
+ return buildLayer("input-output", "Input/Output Validation", checks);
4612
+ }
4613
+ async function computeSecurityPosture() {
4614
+ const [
4615
+ execLayer,
4616
+ egressLayer,
4617
+ secretLayer,
4618
+ supplyChainLayer,
4619
+ inputOutputLayer,
4620
+ monitoringLayer,
4621
+ humanLayer
4622
+ ] = await Promise.all([
4623
+ checkCommandExecLayer(),
4624
+ checkEgressProxyLayer(),
4625
+ checkSecretLayer(),
4626
+ checkSupplyChainLayer(),
4627
+ checkInputOutputLayer(),
4628
+ checkThreatMonitoringLayer(),
4629
+ checkHumanInLoopLayer()
4630
+ ]);
4631
+ const layers = [
4632
+ checkSandboxLayer(),
4633
+ checkFilesystemLayer(),
4634
+ checkNetworkLayer(),
4635
+ egressLayer,
4636
+ execLayer,
4637
+ checkMcpLayer(),
4638
+ checkGatewaySecurityLayer(),
4639
+ secretLayer,
4640
+ supplyChainLayer,
4641
+ inputOutputLayer,
4642
+ monitoringLayer,
4643
+ humanLayer
4644
+ ];
4645
+ const totalChecks = layers.reduce((sum, l) => sum + l.totalCount, 0);
4646
+ const passedChecks = layers.reduce((sum, l) => sum + l.passedCount, 0);
4647
+ const overallScore = totalChecks > 0 ? Math.round(passedChecks / totalChecks * 100) : 0;
4648
+ return {
4649
+ layers,
4650
+ overallScore,
4651
+ configuredLayers: layers.filter((l) => l.status === "configured").length,
4652
+ partialLayers: layers.filter((l) => l.status === "partial").length,
4653
+ unconfiguredLayers: layers.filter((l) => l.status === "unconfigured").length,
4654
+ totalLayers: layers.length,
4655
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString()
4656
+ };
4657
+ }
4658
+
4659
+ // src/server/routes.ts
3249
4660
  async function registerRoutes(app) {
3250
4661
  app.get("/api/health", async () => {
3251
4662
  return { status: "ok", timestamp: (/* @__PURE__ */ new Date()).toISOString() };
@@ -3280,11 +4691,11 @@ async function registerRoutes(app) {
3280
4691
  const db = getDb();
3281
4692
  await db.update(schema_exports.accessConfig).set({
3282
4693
  value: enabled ? "true" : "false",
3283
- updatedAt: sql6`datetime('now')`
4694
+ updatedAt: sql7`datetime('now')`
3284
4695
  }).where(
3285
- and4(
3286
- eq5(schema_exports.accessConfig.category, category),
3287
- eq5(schema_exports.accessConfig.key, "enabled")
4696
+ and5(
4697
+ eq6(schema_exports.accessConfig.category, category),
4698
+ eq6(schema_exports.accessConfig.key, "enabled")
3288
4699
  )
3289
4700
  );
3290
4701
  return deriveAccessState();
@@ -3313,8 +4724,8 @@ async function registerRoutes(app) {
3313
4724
  const { action } = request.body;
3314
4725
  const db = getDb();
3315
4726
  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)));
4727
+ await db.update(schema_exports.commandLogs).set({ status: newStatus, decisionBy: "USER" }).where(eq6(schema_exports.commandLogs.id, Number(id)));
4728
+ const [updated] = await db.select().from(schema_exports.commandLogs).where(eq6(schema_exports.commandLogs.id, Number(id)));
3318
4729
  return updated;
3319
4730
  });
3320
4731
  app.get("/api/openclaw/config", async () => {
@@ -3416,6 +4827,59 @@ async function registerRoutes(app) {
3416
4827
  const patterns = monitor.getExecApprovalService().removeRestrictedPattern(pattern);
3417
4828
  return { patterns: patterns.map((p) => ({ pattern: p })) };
3418
4829
  });
4830
+ app.get("/api/security-posture", async () => {
4831
+ return computeSecurityPosture();
4832
+ });
4833
+ app.post("/api/skill-scanner/scan", async (request, reply) => {
4834
+ const parsed = skillScanRequestSchema.safeParse(request.body);
4835
+ if (!parsed.success) {
4836
+ return reply.status(400).send({ error: "Invalid request. Provide { content: string } (1 to 500K chars)." });
4837
+ }
4838
+ const result = scanSkillDefinition(parsed.data.content);
4839
+ return result;
4840
+ });
4841
+ app.post("/api/skill-scanner/clean", async (request, reply) => {
4842
+ const parsed = skillScanRequestSchema.safeParse(request.body);
4843
+ if (!parsed.success) {
4844
+ return reply.status(400).send({ error: "Invalid request. Provide { content: string } (1 to 500K chars)." });
4845
+ }
4846
+ const result = cleanSkillDefinition(parsed.data.content);
4847
+ return result;
4848
+ });
4849
+ app.get("/api/srt/status", async () => {
4850
+ return getSrtStatus();
4851
+ });
4852
+ app.put("/api/srt/toggle", async (request) => {
4853
+ const { enabled } = request.body;
4854
+ return toggleSrt(enabled);
4855
+ });
4856
+ app.get("/api/srt/settings", async () => {
4857
+ return readSrtSettings() ?? { error: "SRT settings file not found" };
4858
+ });
4859
+ app.put("/api/srt/settings", async (request) => {
4860
+ const updates = request.body;
4861
+ return updateSrtSettings(updates);
4862
+ });
4863
+ app.post("/api/srt/domains/:list", async (request, reply) => {
4864
+ const { list } = request.params;
4865
+ const { domain } = request.body;
4866
+ if (!domain?.trim()) {
4867
+ return reply.status(400).send({ error: "Domain is required" });
4868
+ }
4869
+ if (list === "allow") return addAllowedDomain(domain);
4870
+ if (list === "deny") return addDeniedDomain(domain);
4871
+ return reply.status(400).send({ error: "List must be 'allow' or 'deny'" });
4872
+ });
4873
+ app.delete("/api/srt/domains/:list", async (request, reply) => {
4874
+ const { list } = request.params;
4875
+ const { domain } = request.body;
4876
+ if (!domain?.trim()) {
4877
+ return reply.status(400).send({ error: "Domain is required" });
4878
+ }
4879
+ if (list === "allow") return removeAllowedDomain(domain);
4880
+ if (list === "deny") return removeDeniedDomain(domain);
4881
+ return reply.status(400).send({ error: "List must be 'allow' or 'deny'" });
4882
+ });
3419
4883
  }
3420
4884
 
3421
4885
  // src/server/index.ts
@@ -3426,7 +4890,7 @@ async function createAppServer(port) {
3426
4890
  await app.register(fastifyCors, { origin: "*" });
3427
4891
  await registerRoutes(app);
3428
4892
  const publicDir = getPublicDir();
3429
- if (fs8.existsSync(publicDir) && fs8.readdirSync(publicDir).filter((f) => f !== ".gitkeep").length > 0) {
4893
+ if (fs10.existsSync(publicDir) && fs10.readdirSync(publicDir).filter((f) => f !== ".gitkeep").length > 0) {
3430
4894
  await app.register(fastifyStatic, {
3431
4895
  root: publicDir,
3432
4896
  prefix: "/",
@@ -3688,7 +5152,7 @@ Permission denied for port ${port}.
3688
5152
  }
3689
5153
 
3690
5154
  // src/commands/reset.ts
3691
- import fs9 from "fs";
5155
+ import fs11 from "fs";
3692
5156
  import readline from "readline/promises";
3693
5157
  import pc3 from "picocolors";
3694
5158
  async function resetCommand(options) {
@@ -3707,12 +5171,12 @@ async function resetCommand(options) {
3707
5171
  }
3708
5172
  }
3709
5173
  console.log(pc3.bold("Resetting SafeClaw..."));
3710
- if (fs9.existsSync(DB_PATH)) {
3711
- fs9.unlinkSync(DB_PATH);
5174
+ if (fs11.existsSync(DB_PATH)) {
5175
+ fs11.unlinkSync(DB_PATH);
3712
5176
  const walPath = DB_PATH + "-wal";
3713
5177
  const shmPath = DB_PATH + "-shm";
3714
- if (fs9.existsSync(walPath)) fs9.unlinkSync(walPath);
3715
- if (fs9.existsSync(shmPath)) fs9.unlinkSync(shmPath);
5178
+ if (fs11.existsSync(walPath)) fs11.unlinkSync(walPath);
5179
+ if (fs11.existsSync(shmPath)) fs11.unlinkSync(shmPath);
3716
5180
  console.log(pc3.green(" Database deleted."));
3717
5181
  } else {
3718
5182
  console.log(pc3.dim(" No database found, skipping."));
@@ -3725,11 +5189,11 @@ async function resetCommand(options) {
3725
5189
  }
3726
5190
 
3727
5191
  // src/commands/status.ts
3728
- import fs10 from "fs";
5192
+ import fs12 from "fs";
3729
5193
  import Database3 from "better-sqlite3";
3730
5194
  import pc4 from "picocolors";
3731
5195
  async function statusCommand(options) {
3732
- const exists = fs10.existsSync(SAFECLAW_DIR);
5196
+ const exists = fs12.existsSync(SAFECLAW_DIR);
3733
5197
  if (!exists) {
3734
5198
  if (options.json) {
3735
5199
  console.log(JSON.stringify({ initialized: false }, null, 2));
@@ -3743,7 +5207,7 @@ async function statusCommand(options) {
3743
5207
  const config = readConfig();
3744
5208
  let logCount = 0;
3745
5209
  let activityCount = 0;
3746
- const dbExists = fs10.existsSync(DB_PATH);
5210
+ const dbExists = fs12.existsSync(DB_PATH);
3747
5211
  if (dbExists) {
3748
5212
  try {
3749
5213
  const sqlite = new Database3(DB_PATH, { readonly: true });
@@ -3757,14 +5221,14 @@ async function statusCommand(options) {
3757
5221
  activityCount = -1;
3758
5222
  }
3759
5223
  }
3760
- const openclawConfigExists = fs10.existsSync(OPENCLAW_CONFIG_PATH);
5224
+ const openclawConfigExists = fs12.existsSync(OPENCLAW_CONFIG_PATH);
3761
5225
  if (options.json) {
3762
5226
  const data = {
3763
5227
  version: VERSION,
3764
5228
  initialized: true,
3765
5229
  dataDir: SAFECLAW_DIR,
3766
5230
  database: dbExists ? "exists" : "not_found",
3767
- config: fs10.existsSync(CONFIG_PATH) ? "exists" : "not_found",
5231
+ config: fs12.existsSync(CONFIG_PATH) ? "exists" : "not_found",
3768
5232
  port: config.port,
3769
5233
  autoOpenBrowser: config.autoOpenBrowser,
3770
5234
  premium: config.premium,
@@ -3782,7 +5246,7 @@ async function statusCommand(options) {
3782
5246
  ` ${pc4.dim("Database:")} ${dbExists ? pc4.green("exists") : pc4.red("not found")}`
3783
5247
  );
3784
5248
  console.log(
3785
- ` ${pc4.dim("Config:")} ${fs10.existsSync(CONFIG_PATH) ? pc4.green("exists") : pc4.red("not found")}`
5249
+ ` ${pc4.dim("Config:")} ${fs12.existsSync(CONFIG_PATH) ? pc4.green("exists") : pc4.red("not found")}`
3786
5250
  );
3787
5251
  console.log(` ${pc4.dim("Port:")} ${config.port}`);
3788
5252
  console.log(` ${pc4.dim("Auto-open:")} ${config.autoOpenBrowser ? "Yes" : "No"}`);
@@ -3800,7 +5264,7 @@ async function statusCommand(options) {
3800
5264
  }
3801
5265
 
3802
5266
  // src/commands/doctor.ts
3803
- import fs11 from "fs";
5267
+ import fs13 from "fs";
3804
5268
  import net from "net";
3805
5269
  import Database4 from "better-sqlite3";
3806
5270
  import pc5 from "picocolors";
@@ -3822,7 +5286,7 @@ function checkNodeVersion() {
3822
5286
  function checkDataDir() {
3823
5287
  try {
3824
5288
  ensureDataDir();
3825
- fs11.accessSync(SAFECLAW_DIR, fs11.constants.W_OK);
5289
+ fs13.accessSync(SAFECLAW_DIR, fs13.constants.W_OK);
3826
5290
  return {
3827
5291
  name: "Data directory writable",
3828
5292
  status: "pass",
@@ -3837,7 +5301,7 @@ function checkDataDir() {
3837
5301
  }
3838
5302
  }
3839
5303
  function checkDatabase() {
3840
- if (!fs11.existsSync(DB_PATH)) {
5304
+ if (!fs13.existsSync(DB_PATH)) {
3841
5305
  return {
3842
5306
  name: "Database",
3843
5307
  status: "warn",
@@ -3880,7 +5344,7 @@ function checkDatabase() {
3880
5344
  }
3881
5345
  }
3882
5346
  function checkConfig() {
3883
- if (!fs11.existsSync(CONFIG_PATH)) {
5347
+ if (!fs13.existsSync(CONFIG_PATH)) {
3884
5348
  return {
3885
5349
  name: "Config file",
3886
5350
  status: "warn",
@@ -3922,14 +5386,14 @@ async function checkPort() {
3922
5386
  };
3923
5387
  }
3924
5388
  function checkOpenClawConfig() {
3925
- if (!fs11.existsSync(OPENCLAW_DIR)) {
5389
+ if (!fs13.existsSync(OPENCLAW_DIR)) {
3926
5390
  return {
3927
5391
  name: "OpenClaw directory",
3928
5392
  status: "warn",
3929
5393
  message: `${OPENCLAW_DIR} not found`
3930
5394
  };
3931
5395
  }
3932
- if (!fs11.existsSync(OPENCLAW_CONFIG_PATH)) {
5396
+ if (!fs13.existsSync(OPENCLAW_CONFIG_PATH)) {
3933
5397
  return {
3934
5398
  name: "OpenClaw config",
3935
5399
  status: "warn",
@@ -3961,7 +5425,7 @@ function isPortInUse(port) {
3961
5425
  });
3962
5426
  }
3963
5427
  async function checkOpenClawGateway() {
3964
- if (!fs11.existsSync(OPENCLAW_CONFIG_PATH)) {
5428
+ if (!fs13.existsSync(OPENCLAW_CONFIG_PATH)) {
3965
5429
  return {
3966
5430
  name: "OpenClaw gateway",
3967
5431
  status: "warn",
@@ -3969,7 +5433,7 @@ async function checkOpenClawGateway() {
3969
5433
  };
3970
5434
  }
3971
5435
  try {
3972
- const raw = JSON.parse(fs11.readFileSync(OPENCLAW_CONFIG_PATH, "utf-8"));
5436
+ const raw = JSON.parse(fs13.readFileSync(OPENCLAW_CONFIG_PATH, "utf-8"));
3973
5437
  const port = raw?.gateway?.port ?? 18789;
3974
5438
  const reachable = await isPortInUse(port);
3975
5439
  if (reachable) {
@@ -3994,10 +5458,10 @@ async function checkOpenClawGateway() {
3994
5458
  }
3995
5459
  function checkLogDir() {
3996
5460
  try {
3997
- if (!fs11.existsSync(LOGS_DIR)) {
3998
- fs11.mkdirSync(LOGS_DIR, { recursive: true });
5461
+ if (!fs13.existsSync(LOGS_DIR)) {
5462
+ fs13.mkdirSync(LOGS_DIR, { recursive: true });
3999
5463
  }
4000
- fs11.accessSync(LOGS_DIR, fs11.constants.W_OK);
5464
+ fs13.accessSync(LOGS_DIR, fs13.constants.W_OK);
4001
5465
  return {
4002
5466
  name: "Log directory writable",
4003
5467
  status: "pass",
@@ -4139,14 +5603,14 @@ async function configSetCommand(key, value) {
4139
5603
  }
4140
5604
 
4141
5605
  // src/commands/logs.ts
4142
- import fs12 from "fs";
5606
+ import fs14 from "fs";
4143
5607
  import pc7 from "picocolors";
4144
5608
  async function logsCommand(options) {
4145
5609
  if (options.clear) {
4146
5610
  await clearLogs();
4147
5611
  return;
4148
5612
  }
4149
- if (!fs12.existsSync(DEBUG_LOG_PATH)) {
5613
+ if (!fs14.existsSync(DEBUG_LOG_PATH)) {
4150
5614
  console.log(
4151
5615
  pc7.yellow("No log file found.") + " Run " + pc7.cyan("safeclaw start") + " to generate logs."
4152
5616
  );
@@ -4159,7 +5623,7 @@ async function logsCommand(options) {
4159
5623
  }
4160
5624
  }
4161
5625
  async function tailLogs(lineCount) {
4162
- const content = fs12.readFileSync(DEBUG_LOG_PATH, "utf-8");
5626
+ const content = fs14.readFileSync(DEBUG_LOG_PATH, "utf-8");
4163
5627
  const lines = content.split("\n").filter(Boolean);
4164
5628
  const tail = lines.slice(-lineCount);
4165
5629
  if (tail.length === 0) {
@@ -4177,23 +5641,23 @@ async function tailLogs(lineCount) {
4177
5641
  async function followLogs() {
4178
5642
  console.log(pc7.dim(`Following ${DEBUG_LOG_PATH} (Ctrl+C to stop)
4179
5643
  `));
4180
- if (fs12.existsSync(DEBUG_LOG_PATH)) {
4181
- const content = fs12.readFileSync(DEBUG_LOG_PATH, "utf-8");
5644
+ if (fs14.existsSync(DEBUG_LOG_PATH)) {
5645
+ const content = fs14.readFileSync(DEBUG_LOG_PATH, "utf-8");
4182
5646
  const lines = content.split("\n").filter(Boolean);
4183
5647
  const tail = lines.slice(-20);
4184
5648
  for (const line of tail) {
4185
5649
  process.stdout.write(line + "\n");
4186
5650
  }
4187
5651
  }
4188
- let position = fs12.existsSync(DEBUG_LOG_PATH) ? fs12.statSync(DEBUG_LOG_PATH).size : 0;
4189
- const watcher = fs12.watch(DEBUG_LOG_PATH, () => {
5652
+ let position = fs14.existsSync(DEBUG_LOG_PATH) ? fs14.statSync(DEBUG_LOG_PATH).size : 0;
5653
+ const watcher = fs14.watch(DEBUG_LOG_PATH, () => {
4190
5654
  try {
4191
- const stat = fs12.statSync(DEBUG_LOG_PATH);
5655
+ const stat = fs14.statSync(DEBUG_LOG_PATH);
4192
5656
  if (stat.size > position) {
4193
- const fd = fs12.openSync(DEBUG_LOG_PATH, "r");
5657
+ const fd = fs14.openSync(DEBUG_LOG_PATH, "r");
4194
5658
  const buffer = Buffer.alloc(stat.size - position);
4195
- fs12.readSync(fd, buffer, 0, buffer.length, position);
4196
- fs12.closeSync(fd);
5659
+ fs14.readSync(fd, buffer, 0, buffer.length, position);
5660
+ fs14.closeSync(fd);
4197
5661
  process.stdout.write(buffer.toString("utf-8"));
4198
5662
  position = stat.size;
4199
5663
  } else if (stat.size < position) {
@@ -4211,11 +5675,11 @@ async function followLogs() {
4211
5675
  });
4212
5676
  }
4213
5677
  async function clearLogs() {
4214
- if (!fs12.existsSync(DEBUG_LOG_PATH)) {
5678
+ if (!fs14.existsSync(DEBUG_LOG_PATH)) {
4215
5679
  console.log(pc7.dim("No log file to clear."));
4216
5680
  return;
4217
5681
  }
4218
- fs12.writeFileSync(DEBUG_LOG_PATH, "");
5682
+ fs14.writeFileSync(DEBUG_LOG_PATH, "");
4219
5683
  console.log(pc7.green("Log file cleared."));
4220
5684
  }
4221
5685