@p697/clawket 0.4.5 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/LICENSE +661 -0
  2. package/dist/index.js +707 -6
  3. package/package.json +4 -2
package/dist/index.js CHANGED
@@ -1133,10 +1133,11 @@ function resolveGatewayUrl(explicitUrl) {
1133
1133
  }
1134
1134
  function readOpenClawInfo() {
1135
1135
  const openclaw = resolveOpenClawPaths();
1136
+ const envGatewayPort = readGatewayPortEnv();
1136
1137
  if (!existsSync3(openclaw.configPath)) {
1137
1138
  return {
1138
1139
  configFound: false,
1139
- gatewayPort: null,
1140
+ gatewayPort: envGatewayPort,
1140
1141
  authMode: null,
1141
1142
  token: readGatewayTokenEnv(),
1142
1143
  password: readGatewayPasswordEnv()
@@ -1145,13 +1146,13 @@ function readOpenClawInfo() {
1145
1146
  try {
1146
1147
  const parsed = JSON.parse(readFileSync3(openclaw.configPath, "utf8"));
1147
1148
  const rawPort = parsed.gateway?.port;
1148
- const gatewayPort = typeof rawPort === "number" && Number.isInteger(rawPort) ? rawPort : null;
1149
+ const configuredGatewayPort = typeof rawPort === "number" && Number.isInteger(rawPort) ? rawPort : null;
1149
1150
  const authMode = parsed.gateway?.auth?.mode === "token" || parsed.gateway?.auth?.mode === "password" ? parsed.gateway.auth.mode : null;
1150
1151
  const token = readConfiguredSecret(parsed.gateway?.auth?.token) ?? readGatewayTokenEnv();
1151
1152
  const password = readConfiguredSecret(parsed.gateway?.auth?.password) ?? readGatewayPasswordEnv();
1152
1153
  return {
1153
1154
  configFound: true,
1154
- gatewayPort,
1155
+ gatewayPort: envGatewayPort ?? configuredGatewayPort,
1155
1156
  authMode,
1156
1157
  token,
1157
1158
  password
@@ -1159,7 +1160,7 @@ function readOpenClawInfo() {
1159
1160
  } catch {
1160
1161
  return {
1161
1162
  configFound: true,
1162
- gatewayPort: null,
1163
+ gatewayPort: envGatewayPort,
1163
1164
  authMode: null,
1164
1165
  token: readGatewayTokenEnv(),
1165
1166
  password: readGatewayPasswordEnv()
@@ -1233,6 +1234,337 @@ async function configureOpenClawLanAccess(params) {
1233
1234
  controlUiOrigin
1234
1235
  };
1235
1236
  }
1237
+ function stripAnsi(text) {
1238
+ return text.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "");
1239
+ }
1240
+ function collectCliOutput(parts) {
1241
+ return parts.filter((value) => typeof value === "string" && value.trim().length > 0).map((value) => stripAnsi(value.trim())).join("\n").trim();
1242
+ }
1243
+ function isUnsupportedJsonOptionOutput(text) {
1244
+ return /unknown option ['"]?--json['"]?/i.test(text);
1245
+ }
1246
+ async function readOpenClawPermissions() {
1247
+ const openclaw = resolveOpenClawPaths();
1248
+ const config = await readJsonFile(openclaw.configPath);
1249
+ const approvalsPath = resolveOpenClawExecApprovalsPath();
1250
+ const approvals = await readJsonFile(approvalsPath);
1251
+ const configEnv = readConfigEnvVars(config);
1252
+ const tools = readRecord(config?.tools);
1253
+ const currentAgent = resolveCurrentAgent(config);
1254
+ const agentTools = readRecord(currentAgent.record?.tools);
1255
+ const web = readRecord(tools?.web);
1256
+ const webSearch = readRecord(web?.search);
1257
+ const webFetch = readRecord(web?.fetch);
1258
+ const exec = resolveMergedExecConfig(tools, agentTools);
1259
+ const toolProfile = resolveToolProfile(tools, agentTools);
1260
+ const toolDeny = uniqueSortedStrings([
1261
+ ...readStringArray(tools?.deny),
1262
+ ...readStringArray(agentTools?.deny)
1263
+ ]);
1264
+ const searchEnabled = readBoolean(webSearch?.enabled, true);
1265
+ const searchProvider = readString(webSearch?.provider) ?? "auto";
1266
+ const searchConfigured = resolveWebSearchConfigured(webSearch, searchProvider, configEnv);
1267
+ const webReasons = [];
1268
+ let webStatus;
1269
+ let webSummary;
1270
+ if (toolDeny.includes("web_search") && toolDeny.includes("web_fetch")) {
1271
+ webStatus = "disabled";
1272
+ webSummary = "Web search and fetch are blocked by tool policy.";
1273
+ webReasons.push("Global tool policy denies both web_search and web_fetch.");
1274
+ } else if (!searchEnabled && !readBoolean(webFetch?.enabled, true)) {
1275
+ webStatus = "disabled";
1276
+ webSummary = "Web search and fetch are turned off.";
1277
+ webReasons.push("tools.web.search.enabled is false.");
1278
+ webReasons.push("tools.web.fetch.enabled is false.");
1279
+ } else if (searchEnabled && !searchConfigured) {
1280
+ webStatus = "configuration_needed";
1281
+ webSummary = "Web search is enabled, but no search provider key was found.";
1282
+ webReasons.push(searchProvider === "auto" ? "No supported web search provider API key was found in config or current environment." : `Provider "${searchProvider}" is selected, but its API key was not found in config or current environment.`);
1283
+ } else {
1284
+ webStatus = "available";
1285
+ webSummary = "Common web tools look available.";
1286
+ if (toolDeny.includes("web_search")) {
1287
+ webStatus = "restricted";
1288
+ webSummary = "Web fetch is available, but web search is blocked by tool policy.";
1289
+ webReasons.push("Global tool policy denies web_search.");
1290
+ } else if (toolDeny.includes("web_fetch")) {
1291
+ webStatus = "restricted";
1292
+ webSummary = "Web search is available, but web fetch is blocked by tool policy.";
1293
+ webReasons.push("Global tool policy denies web_fetch.");
1294
+ } else {
1295
+ if (!searchEnabled) {
1296
+ webReasons.push("tools.web.search.enabled is false.");
1297
+ }
1298
+ if (!readBoolean(webFetch?.enabled, true)) {
1299
+ webReasons.push("tools.web.fetch.enabled is false.");
1300
+ }
1301
+ }
1302
+ }
1303
+ const firecrawlConfigured = resolveFirecrawlConfigured(webFetch, configEnv);
1304
+ const configuredHost = readExecHost(exec?.host);
1305
+ const sandboxMode = readSandboxMode(config, currentAgent.id);
1306
+ const implicitSandboxFallback = configuredHost === "sandbox" && sandboxMode === "off";
1307
+ const effectiveHost = implicitSandboxFallback ? "gateway" : configuredHost;
1308
+ const configSecurity = readExecSecurity(exec?.security, configuredHost === "sandbox" ? "deny" : "allowlist");
1309
+ const configAsk = readExecAsk(exec?.ask, "on-miss");
1310
+ const safeBins = readStringArray(exec?.safeBins);
1311
+ const safeBinTrustedDirs = uniqueSortedStrings([
1312
+ "/bin",
1313
+ "/usr/bin",
1314
+ ...readStringArray(exec?.safeBinTrustedDirs)
1315
+ ]);
1316
+ const trustedDirWarnings = safeBinTrustedDirs.filter((dir) => ["/opt/homebrew/bin", "/usr/local/bin", "/opt/local/bin", "/snap/bin"].includes(dir)).map((dir) => `safe-bin trust includes ${dir}; this is often needed, but commands there are treated as explicitly trusted.`);
1317
+ const resolvedApprovals = resolveExecApprovalsSummary(approvals, {
1318
+ security: configSecurity,
1319
+ ask: configAsk
1320
+ });
1321
+ const usesHostApprovals = configuredHost === "gateway" || configuredHost === "node";
1322
+ const effectiveSecurity = usesHostApprovals ? minExecSecurity(configSecurity, resolvedApprovals.security) : configSecurity;
1323
+ const effectiveAsk = usesHostApprovals ? resolvedApprovals.ask === "off" ? "off" : maxExecAsk(configAsk, resolvedApprovals.ask) : "off";
1324
+ const toolPolicyDenied = deniesExecTool(toolDeny);
1325
+ const execToolAvailable = toolProfileAllowsExec(toolProfile) && !toolPolicyDenied;
1326
+ const execReasons = [];
1327
+ let execStatus;
1328
+ let execSummary;
1329
+ if (!execToolAvailable) {
1330
+ execStatus = "disabled";
1331
+ execSummary = "This agent cannot run commands right now.";
1332
+ if (!toolProfileAllowsExec(toolProfile)) {
1333
+ execReasons.push(`The current agent uses the ${toolProfile} tool profile, which does not expose command tools.`);
1334
+ }
1335
+ if (toolPolicyDenied) {
1336
+ execReasons.push("A tool deny rule is blocking command execution for the current agent.");
1337
+ }
1338
+ } else if (implicitSandboxFallback) {
1339
+ execStatus = "available";
1340
+ execSummary = "Commands currently run directly on this OpenClaw machine.";
1341
+ execReasons.push("The current agent still exposes command tools.");
1342
+ execReasons.push("Sandbox mode is off, so commands are not running in an isolated sandbox.");
1343
+ execReasons.push("OpenClaw is currently falling back to direct host execution on this machine.");
1344
+ } else if (effectiveHost === "sandbox") {
1345
+ execStatus = "available";
1346
+ execSummary = "Commands run inside OpenClaw's sandbox.";
1347
+ execReasons.push("The current agent still exposes command tools.");
1348
+ execReasons.push(`Sandbox mode is ${sandboxMode}.`);
1349
+ } else if (effectiveSecurity === "deny") {
1350
+ execStatus = "disabled";
1351
+ execSummary = "Command execution is disabled.";
1352
+ execReasons.push(`Effective exec security resolves to ${effectiveSecurity}.`);
1353
+ } else if (effectiveAsk !== "off") {
1354
+ execStatus = "needs_approval";
1355
+ execSummary = "Commands can run, but exec approvals currently require confirmation.";
1356
+ execReasons.push(`Effective exec ask policy is ${effectiveAsk}.`);
1357
+ if (effectiveSecurity === "allowlist" && resolvedApprovals.allowlistCount === 0 && safeBins.length === 0) {
1358
+ execReasons.push("No allowlist entries or safe bins are configured yet, so most commands will still be denied.");
1359
+ }
1360
+ } else if (effectiveSecurity === "allowlist") {
1361
+ execStatus = "restricted";
1362
+ execSummary = "Commands are limited to allowlisted executables and safe bins.";
1363
+ execReasons.push("Effective exec security is allowlist.");
1364
+ if (resolvedApprovals.allowlistCount === 0 && safeBins.length === 0) {
1365
+ execReasons.push("No allowlist entries or safe bins are configured yet.");
1366
+ }
1367
+ } else {
1368
+ execStatus = "available";
1369
+ execSummary = "Command execution is broadly available.";
1370
+ }
1371
+ if (usesHostApprovals && resolvedApprovals.security !== configSecurity) {
1372
+ execReasons.push(`exec-approvals.json is stricter than tools.exec.security (${configSecurity} -> ${resolvedApprovals.security}).`);
1373
+ }
1374
+ if (usesHostApprovals && resolvedApprovals.ask !== configAsk) {
1375
+ execReasons.push(`exec-approvals.json is stricter than tools.exec.ask (${configAsk} -> ${resolvedApprovals.ask}).`);
1376
+ }
1377
+ if (usesHostApprovals && safeBins.some(isInterpreterLikeSafeBin)) {
1378
+ execStatus = execStatus === "disabled" ? execStatus : "restricted";
1379
+ execReasons.push("Interpreter/runtime binaries appear in safeBins and may still be unsafe or blocked.");
1380
+ }
1381
+ if (usesHostApprovals) {
1382
+ execReasons.push(...trustedDirWarnings);
1383
+ }
1384
+ if (!usesHostApprovals) {
1385
+ execReasons.push(`The app controls tools.exec.security=${configSecurity} and tools.exec.ask=${configAsk}, but this current command path is not using OpenClaw's host approval flow.`);
1386
+ }
1387
+ const codeReasons = [];
1388
+ let codeStatus = execStatus;
1389
+ let codeSummary = execSummary;
1390
+ if (!execToolAvailable) {
1391
+ codeStatus = "disabled";
1392
+ codeSummary = "Scripts are currently unavailable because this agent cannot run commands.";
1393
+ } else if (implicitSandboxFallback) {
1394
+ codeStatus = "available";
1395
+ codeSummary = "Scripts follow the same direct command path as command execution on this machine.";
1396
+ codeReasons.push("Code execution follows the same direct command path as command execution.");
1397
+ } else if (effectiveHost === "sandbox") {
1398
+ codeStatus = "available";
1399
+ codeSummary = "Code runs inside OpenClaw's sandbox.";
1400
+ codeReasons.push("Code execution follows the same sandboxed path as command execution.");
1401
+ } else if (effectiveSecurity === "deny") {
1402
+ codeStatus = "disabled";
1403
+ codeSummary = "Code execution is disabled because command execution is disabled.";
1404
+ } else if (effectiveAsk !== "off") {
1405
+ codeStatus = "needs_approval";
1406
+ codeSummary = "Running scripts or code inherits the current exec approval requirement.";
1407
+ codeReasons.push("Interpreter and runtime commands inherit exec approval rules.");
1408
+ codeReasons.push("Approval-backed interpreter runs are conservative and may be denied when OpenClaw cannot bind one concrete file.");
1409
+ } else if (effectiveSecurity === "allowlist") {
1410
+ codeStatus = "restricted";
1411
+ codeSummary = "Running scripts is restricted by allowlist rules.";
1412
+ codeReasons.push("Interpreter and runtime commands usually need explicit allowlist entries.");
1413
+ } else {
1414
+ codeStatus = "available";
1415
+ codeSummary = "Code execution inherits the current command execution policy and looks available.";
1416
+ }
1417
+ if (safeBins.some(isInterpreterLikeSafeBin)) {
1418
+ codeStatus = codeStatus === "disabled" ? codeStatus : "restricted";
1419
+ codeReasons.push("Interpreter/runtime binaries should not rely on safeBins alone.");
1420
+ }
1421
+ return {
1422
+ configPath: openclaw.configPath,
1423
+ approvalsPath,
1424
+ web: {
1425
+ status: webStatus,
1426
+ summary: webSummary,
1427
+ reasons: uniqueSortedStrings(webReasons),
1428
+ searchEnabled,
1429
+ searchProvider,
1430
+ searchConfigured,
1431
+ fetchEnabled: readBoolean(webFetch?.enabled, true),
1432
+ firecrawlConfigured
1433
+ },
1434
+ exec: {
1435
+ currentAgentId: currentAgent.id,
1436
+ currentAgentName: currentAgent.name,
1437
+ toolProfile,
1438
+ execToolAvailable,
1439
+ hostApprovalsApply: usesHostApprovals,
1440
+ implicitSandboxFallback,
1441
+ status: execStatus,
1442
+ summary: execSummary,
1443
+ reasons: uniqueSortedStrings(execReasons),
1444
+ configuredHost,
1445
+ effectiveHost,
1446
+ sandboxMode,
1447
+ configSecurity,
1448
+ configAsk,
1449
+ approvalsExists: approvals != null,
1450
+ approvalsSecurity: resolvedApprovals.security,
1451
+ approvalsAsk: resolvedApprovals.ask,
1452
+ effectiveSecurity,
1453
+ effectiveAsk,
1454
+ allowlistCount: resolvedApprovals.allowlistCount,
1455
+ toolPolicyDenied,
1456
+ safeBins,
1457
+ safeBinTrustedDirs,
1458
+ trustedDirWarnings
1459
+ },
1460
+ codeExecution: {
1461
+ status: codeStatus,
1462
+ summary: codeSummary,
1463
+ reasons: uniqueSortedStrings(codeReasons),
1464
+ inheritsFromExec: true
1465
+ }
1466
+ };
1467
+ }
1468
+ async function runOpenClawDoctor() {
1469
+ const openclaw = resolveOpenClawPaths();
1470
+ try {
1471
+ const { stdout, stderr } = await runOpenClawCli(["doctor", "--json"], openclaw);
1472
+ try {
1473
+ const parsed = parseEmbeddedJsonValue(collectCliOutput([stdout, stderr]));
1474
+ const checks = Array.isArray(parsed.checks) ? parsed.checks.filter((c) => typeof c === "object" && c != null).map((c) => ({
1475
+ name: typeof c.name === "string" ? c.name : "unknown",
1476
+ status: typeof c.status === "string" ? c.status : "unknown",
1477
+ message: typeof c.message === "string" ? c.message : void 0
1478
+ })) : [];
1479
+ return {
1480
+ ok: parsed.ok === true,
1481
+ checks,
1482
+ summary: typeof parsed.summary === "string" ? parsed.summary : ""
1483
+ };
1484
+ } catch {
1485
+ return {
1486
+ ok: true,
1487
+ checks: [],
1488
+ summary: "",
1489
+ raw: collectCliOutput([stdout, stderr])
1490
+ };
1491
+ }
1492
+ } catch (error) {
1493
+ if (isExecFileError(error)) {
1494
+ const output = collectCliOutput([error.stdout, error.stderr]);
1495
+ if (output) {
1496
+ if (isUnsupportedJsonOptionOutput(output)) {
1497
+ } else {
1498
+ try {
1499
+ const parsed = parseEmbeddedJsonValue(output);
1500
+ const checks = Array.isArray(parsed.checks) ? parsed.checks.filter((c) => typeof c === "object" && c != null).map((c) => ({
1501
+ name: typeof c.name === "string" ? c.name : "unknown",
1502
+ status: typeof c.status === "string" ? c.status : "unknown",
1503
+ message: typeof c.message === "string" ? c.message : void 0
1504
+ })) : [];
1505
+ return {
1506
+ ok: parsed.ok === true,
1507
+ checks,
1508
+ summary: typeof parsed.summary === "string" ? parsed.summary : ""
1509
+ };
1510
+ } catch {
1511
+ return {
1512
+ ok: false,
1513
+ checks: [],
1514
+ summary: "",
1515
+ raw: output
1516
+ };
1517
+ }
1518
+ }
1519
+ }
1520
+ }
1521
+ try {
1522
+ const { stdout, stderr } = await runOpenClawCli(["doctor"], openclaw);
1523
+ return {
1524
+ ok: true,
1525
+ checks: [],
1526
+ summary: "",
1527
+ raw: collectCliOutput([stdout, stderr])
1528
+ };
1529
+ } catch (fallbackError) {
1530
+ if (isExecFileError(fallbackError)) {
1531
+ const output = collectCliOutput([fallbackError.stdout, fallbackError.stderr]);
1532
+ if (output) {
1533
+ return {
1534
+ ok: false,
1535
+ checks: [],
1536
+ summary: "",
1537
+ raw: output
1538
+ };
1539
+ }
1540
+ }
1541
+ throw fallbackError instanceof Error ? fallbackError : formatOpenClawCliError(["doctor"], fallbackError);
1542
+ }
1543
+ }
1544
+ }
1545
+ async function runOpenClawDoctorFix() {
1546
+ const openclaw = resolveOpenClawPaths();
1547
+ try {
1548
+ const { stdout, stderr } = await runOpenClawCli(["doctor", "--fix"], openclaw);
1549
+ return {
1550
+ ok: true,
1551
+ summary: "",
1552
+ raw: collectCliOutput([stdout, stderr])
1553
+ };
1554
+ } catch (error) {
1555
+ if (isExecFileError(error)) {
1556
+ const output = collectCliOutput([error.stdout, error.stderr]);
1557
+ if (output) {
1558
+ return {
1559
+ ok: false,
1560
+ summary: "",
1561
+ raw: output
1562
+ };
1563
+ }
1564
+ }
1565
+ throw formatOpenClawCliError(["doctor", "--fix"], error);
1566
+ }
1567
+ }
1236
1568
  async function restartOpenClawGateway() {
1237
1569
  const openclaw = resolveOpenClawPaths();
1238
1570
  const restart = await runOpenClawDaemonCommand(["gateway", "restart", "--json"], openclaw);
@@ -1292,6 +1624,226 @@ async function issueOpenClawBootstrapToken(params) {
1292
1624
  function readConfiguredSecret(value) {
1293
1625
  return typeof value === "string" && value.trim() ? value.trim() : null;
1294
1626
  }
1627
+ function readRecord(value) {
1628
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1629
+ return null;
1630
+ }
1631
+ return value;
1632
+ }
1633
+ function readString(value) {
1634
+ return typeof value === "string" && value.trim() ? value.trim() : null;
1635
+ }
1636
+ function readBoolean(value, fallback) {
1637
+ return typeof value === "boolean" ? value : fallback;
1638
+ }
1639
+ function readStringArray(value) {
1640
+ if (!Array.isArray(value)) {
1641
+ return [];
1642
+ }
1643
+ return uniqueSortedStrings(value.filter((entry) => typeof entry === "string").map((entry) => entry.trim()));
1644
+ }
1645
+ function uniqueSortedStrings(values) {
1646
+ return [...new Set(values.filter((value) => value.trim().length > 0))].sort();
1647
+ }
1648
+ function resolveCurrentAgent(config) {
1649
+ const agents = readRecord(config?.agents);
1650
+ const list = Array.isArray(agents?.list) ? agents.list : [];
1651
+ const entries = list.map((entry) => readRecord(entry)).filter((entry) => entry != null);
1652
+ const target = entries.find((entry) => entry.default === true) ?? entries.find((entry) => readString(entry.id) === "main") ?? entries[0] ?? null;
1653
+ const id = readString(target?.id) ?? "main";
1654
+ const name = readString(target?.name) ?? id;
1655
+ return { id, name, record: target };
1656
+ }
1657
+ function resolveToolProfile(globalTools, agentTools) {
1658
+ const raw = readString(agentTools?.profile) ?? readString(globalTools?.profile);
1659
+ return raw === "minimal" || raw === "coding" || raw === "messaging" || raw === "full" ? raw : "unset";
1660
+ }
1661
+ function toolProfileAllowsExec(profile) {
1662
+ return profile === "coding" || profile === "full" || profile === "unset";
1663
+ }
1664
+ function deniesExecTool(denyList) {
1665
+ const normalized = new Set(denyList.map((entry) => entry.trim().toLowerCase()).filter(Boolean));
1666
+ return normalized.has("*") || normalized.has("exec") || normalized.has("bash") || normalized.has("group:runtime");
1667
+ }
1668
+ function resolveMergedExecConfig(globalTools, agentTools) {
1669
+ const globalExec = readRecord(globalTools?.exec);
1670
+ const agentExec = readRecord(agentTools?.exec);
1671
+ if (!globalExec && !agentExec) {
1672
+ return null;
1673
+ }
1674
+ return {
1675
+ ...globalExec ?? {},
1676
+ ...agentExec ?? {}
1677
+ };
1678
+ }
1679
+ function readExecHost(value) {
1680
+ return value === "gateway" || value === "node" || value === "sandbox" ? value : "sandbox";
1681
+ }
1682
+ function readSandboxMode(config, agentId = "main") {
1683
+ const agents = readRecord(config?.agents);
1684
+ const defaults = readRecord(agents?.defaults);
1685
+ const defaultSandbox = readRecord(defaults?.sandbox);
1686
+ const list = Array.isArray(agents?.list) ? agents.list : [];
1687
+ const targetAgent = list.find((entry) => {
1688
+ const record = readRecord(entry);
1689
+ return readString(record?.id) === agentId;
1690
+ });
1691
+ const agentSandbox = readRecord(readRecord(targetAgent)?.sandbox);
1692
+ const value = readString(agentSandbox?.mode) ?? readString(defaultSandbox?.mode);
1693
+ return value === "all" || value === "non-main" || value === "off" ? value : "off";
1694
+ }
1695
+ function readExecSecurity(value, fallback) {
1696
+ return value === "deny" || value === "allowlist" || value === "full" ? value : fallback;
1697
+ }
1698
+ function readExecAsk(value, fallback) {
1699
+ return value === "off" || value === "on-miss" || value === "always" ? value : fallback;
1700
+ }
1701
+ function minExecSecurity(a, b) {
1702
+ const order = { deny: 0, allowlist: 1, full: 2 };
1703
+ return order[a] <= order[b] ? a : b;
1704
+ }
1705
+ function maxExecAsk(a, b) {
1706
+ const order = { off: 0, "on-miss": 1, always: 2 };
1707
+ return order[a] >= order[b] ? a : b;
1708
+ }
1709
+ function resolveWebSearchConfigured(webSearch, provider, configEnv) {
1710
+ const braveKey = readConfiguredSecret(webSearch?.apiKey) ?? readConfigEnvValue(configEnv, "BRAVE_API_KEY") ?? readEnvValue("BRAVE_API_KEY", "BRAVE_API_KEY");
1711
+ const gemini = readRecord(webSearch?.gemini);
1712
+ const geminiKey = readConfiguredSecret(gemini?.apiKey) ?? readConfigEnvValue(configEnv, "GEMINI_API_KEY") ?? readEnvValue("GEMINI_API_KEY", "GEMINI_API_KEY");
1713
+ const grok = readRecord(webSearch?.grok);
1714
+ const grokKey = readConfiguredSecret(grok?.apiKey) ?? readConfigEnvValue(configEnv, "XAI_API_KEY") ?? readEnvValue("XAI_API_KEY", "XAI_API_KEY");
1715
+ const kimi = readRecord(webSearch?.kimi);
1716
+ const kimiKey = readConfiguredSecret(kimi?.apiKey) ?? readConfigEnvValue(configEnv, "KIMI_API_KEY", "MOONSHOT_API_KEY") ?? readConfigEnvValue(configEnv, "MOONSHOT_API_KEY", "KIMI_API_KEY") ?? readEnvValue("KIMI_API_KEY", "MOONSHOT_API_KEY") ?? readEnvValue("MOONSHOT_API_KEY", "KIMI_API_KEY");
1717
+ const perplexity = readRecord(webSearch?.perplexity);
1718
+ const perplexityKey = readConfiguredSecret(perplexity?.apiKey) ?? readConfigEnvValue(configEnv, "PERPLEXITY_API_KEY", "OPENROUTER_API_KEY") ?? readConfigEnvValue(configEnv, "OPENROUTER_API_KEY", "PERPLEXITY_API_KEY") ?? readEnvValue("PERPLEXITY_API_KEY", "OPENROUTER_API_KEY") ?? readEnvValue("OPENROUTER_API_KEY", "PERPLEXITY_API_KEY");
1719
+ const configuredByProvider = {
1720
+ brave: Boolean(braveKey),
1721
+ gemini: Boolean(geminiKey),
1722
+ grok: Boolean(grokKey),
1723
+ kimi: Boolean(kimiKey),
1724
+ perplexity: Boolean(perplexityKey)
1725
+ };
1726
+ if (provider !== "auto") {
1727
+ return configuredByProvider[provider] === true;
1728
+ }
1729
+ return Object.values(configuredByProvider).some(Boolean);
1730
+ }
1731
+ function resolveFirecrawlConfigured(webFetch, configEnv) {
1732
+ const firecrawl = readRecord(webFetch?.firecrawl);
1733
+ return Boolean(readConfiguredSecret(firecrawl?.apiKey) ?? readConfigEnvValue(configEnv, "FIRECRAWL_API_KEY") ?? readEnvValue("FIRECRAWL_API_KEY", "FIRECRAWL_API_KEY"));
1734
+ }
1735
+ function readConfigEnvVars(config) {
1736
+ const env = readRecord(config?.env);
1737
+ if (!env) {
1738
+ return {};
1739
+ }
1740
+ const vars = readRecord(env.vars);
1741
+ const result = {};
1742
+ if (vars) {
1743
+ for (const [key, value] of Object.entries(vars)) {
1744
+ const normalized = key.trim();
1745
+ if (!normalized) {
1746
+ continue;
1747
+ }
1748
+ const secret = readConfiguredSecret(value);
1749
+ if (secret) {
1750
+ result[normalized] = secret;
1751
+ }
1752
+ }
1753
+ }
1754
+ for (const [key, value] of Object.entries(env)) {
1755
+ if (key === "vars" || key === "shellEnv") {
1756
+ continue;
1757
+ }
1758
+ const normalized = key.trim();
1759
+ if (!normalized) {
1760
+ continue;
1761
+ }
1762
+ const secret = readConfiguredSecret(value);
1763
+ if (secret) {
1764
+ result[normalized] = secret;
1765
+ }
1766
+ }
1767
+ return result;
1768
+ }
1769
+ function readConfigEnvValue(configEnv, primary, legacy) {
1770
+ const current = configEnv[primary]?.trim();
1771
+ if (current) {
1772
+ return current;
1773
+ }
1774
+ if (!legacy) {
1775
+ return null;
1776
+ }
1777
+ const fallback = configEnv[legacy]?.trim();
1778
+ return fallback || null;
1779
+ }
1780
+ function resolveExecApprovalsSummary(approvals, overrides) {
1781
+ const defaults = readRecord(approvals?.defaults);
1782
+ const agents = readRecord(approvals?.agents);
1783
+ const wildcard = readRecord(agents?.["*"]);
1784
+ const main2 = readRecord(agents?.main) ?? readRecord(agents?.default);
1785
+ const security = readExecSecurity(main2?.security ?? wildcard?.security ?? defaults?.security, readExecSecurity(defaults?.security, overrides.security));
1786
+ const ask = readExecAsk(main2?.ask ?? wildcard?.ask ?? defaults?.ask, readExecAsk(defaults?.ask, overrides.ask));
1787
+ const allowlistEntries = [
1788
+ ...readAllowlistEntries(wildcard?.allowlist),
1789
+ ...readAllowlistEntries(main2?.allowlist)
1790
+ ];
1791
+ return {
1792
+ security,
1793
+ ask,
1794
+ allowlistCount: uniqueSortedStrings(allowlistEntries.map((entry) => entry.pattern)).length
1795
+ };
1796
+ }
1797
+ function readAllowlistEntries(value) {
1798
+ if (!Array.isArray(value)) {
1799
+ return [];
1800
+ }
1801
+ const entries = [];
1802
+ for (const entry of value) {
1803
+ const record = readRecord(entry);
1804
+ const pattern = readString(record?.pattern);
1805
+ if (pattern) {
1806
+ entries.push({ pattern });
1807
+ }
1808
+ }
1809
+ return entries;
1810
+ }
1811
+ function isInterpreterLikeSafeBin(value) {
1812
+ const normalized = value.trim().toLowerCase().split(/[\\/]/).at(-1) ?? "";
1813
+ if (!normalized) {
1814
+ return false;
1815
+ }
1816
+ if ([
1817
+ "ash",
1818
+ "bash",
1819
+ "bun",
1820
+ "cmd",
1821
+ "cmd.exe",
1822
+ "dash",
1823
+ "deno",
1824
+ "fish",
1825
+ "ksh",
1826
+ "lua",
1827
+ "node",
1828
+ "nodejs",
1829
+ "perl",
1830
+ "php",
1831
+ "powershell",
1832
+ "powershell.exe",
1833
+ "pypy",
1834
+ "pwsh",
1835
+ "pwsh.exe",
1836
+ "python",
1837
+ "python2",
1838
+ "python3",
1839
+ "ruby",
1840
+ "sh",
1841
+ "zsh"
1842
+ ].includes(normalized)) {
1843
+ return true;
1844
+ }
1845
+ return /^(python|ruby|perl|php|node)\d+(?:\.\d+)?$/.test(normalized);
1846
+ }
1295
1847
  async function readOpenClawConfigString(configPath, openclaw = resolveOpenClawPaths()) {
1296
1848
  try {
1297
1849
  const { stdout } = await runOpenClawCli(["config", "get", configPath], openclaw);
@@ -1358,7 +1910,7 @@ async function runOpenClawCli(args, openclaw = resolveOpenClawPaths()) {
1358
1910
  });
1359
1911
  });
1360
1912
  } catch (error) {
1361
- throw formatOpenClawCliError(args, error);
1913
+ throw preserveExecFileErrorDetails(formatOpenClawCliError(args, error), error);
1362
1914
  }
1363
1915
  }
1364
1916
  function formatOpenClawCliError(args, error) {
@@ -1372,6 +1924,16 @@ function formatOpenClawCliError(args, error) {
1372
1924
  }
1373
1925
  return error instanceof Error ? error : new Error(`${messagePrefix} failed: ${String(error)}`);
1374
1926
  }
1927
+ function preserveExecFileErrorDetails(formatted, original) {
1928
+ if (!isExecFileError(original)) {
1929
+ return formatted;
1930
+ }
1931
+ return Object.assign(formatted, {
1932
+ code: original.code,
1933
+ stdout: original.stdout,
1934
+ stderr: original.stderr
1935
+ });
1936
+ }
1375
1937
  function isMissingConfigPathError(error) {
1376
1938
  return error instanceof Error && /Config path not found:/i.test(error.message);
1377
1939
  }
@@ -1499,6 +2061,13 @@ function readGatewayTokenEnv() {
1499
2061
  function readGatewayPasswordEnv() {
1500
2062
  return readEnvValue("OPENCLAW_GATEWAY_PASSWORD", "CLAWDBOT_GATEWAY_PASSWORD");
1501
2063
  }
2064
+ function readGatewayPortEnv() {
2065
+ const raw = readEnvValue("OPENCLAW_GATEWAY_PORT", "CLAWDBOT_GATEWAY_PORT");
2066
+ if (!raw)
2067
+ return null;
2068
+ const parsed = Number.parseInt(raw, 10);
2069
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
2070
+ }
1502
2071
  function readEnvValue(primary, legacy) {
1503
2072
  const current = process.env[primary]?.trim();
1504
2073
  if (current)
@@ -1516,6 +2085,9 @@ function resolveOpenClawPaths() {
1516
2085
  mediaDir: join3(stateDir, "media")
1517
2086
  };
1518
2087
  }
2088
+ function resolveOpenClawExecApprovalsPath() {
2089
+ return join3(resolveOpenClawHomeDir(), ".openclaw", "exec-approvals.json");
2090
+ }
1519
2091
  function resolveActiveOpenClawStateDir() {
1520
2092
  const explicitStateDir = readEnvValue("OPENCLAW_STATE_DIR", "CLAWDBOT_STATE_DIR");
1521
2093
  if (explicitStateDir) {
@@ -1541,6 +2113,21 @@ function buildOpenClawStateDirCandidates() {
1541
2113
  return true;
1542
2114
  });
1543
2115
  }
2116
+ function resolveOpenClawHomeDir() {
2117
+ const explicitHome = process.env.OPENCLAW_HOME?.trim();
2118
+ if (explicitHome) {
2119
+ return resolveUserPath(explicitHome);
2120
+ }
2121
+ const envHome = process.env.HOME?.trim();
2122
+ if (envHome) {
2123
+ return resolveUserPath(envHome);
2124
+ }
2125
+ const userProfile = process.env.USERPROFILE?.trim();
2126
+ if (userProfile) {
2127
+ return resolveUserPath(userProfile);
2128
+ }
2129
+ return resolve(homedir3());
2130
+ }
1544
2131
  function resolveUserPath(input) {
1545
2132
  const trimmed = input.trim();
1546
2133
  if (!trimmed)
@@ -1955,6 +2542,18 @@ var BridgeRuntime = class {
1955
2542
  await this.handleBootstrapRequest(control);
1956
2543
  return;
1957
2544
  }
2545
+ if (control.event === "doctor.request") {
2546
+ await this.handleDoctorRequest(control);
2547
+ return;
2548
+ }
2549
+ if (control.event === "doctor-fix.request") {
2550
+ await this.handleDoctorFixRequest(control);
2551
+ return;
2552
+ }
2553
+ if (control.event === "permissions.request") {
2554
+ await this.handlePermissionsRequest(control);
2555
+ return;
2556
+ }
1958
2557
  const { event, count } = control;
1959
2558
  if (event === "client_connected" || event === "client_count") {
1960
2559
  const previousClientCount = this.snapshot.clientCount;
@@ -2056,6 +2655,108 @@ var BridgeRuntime = class {
2056
2655
  });
2057
2656
  }
2058
2657
  }
2658
+ async handleDoctorRequest(control) {
2659
+ const requestId = control.requestId?.trim() ?? "";
2660
+ const replyTargetClientId = control.sourceClientId?.trim() || control.targetClientId?.trim() || "";
2661
+ if (!requestId) {
2662
+ this.log("relay doctor request dropped reason=missing_request_id");
2663
+ return;
2664
+ }
2665
+ this.log(`relay doctor request received requestId=${requestId}`);
2666
+ try {
2667
+ const result = await runOpenClawDoctor();
2668
+ this.log(`relay doctor completed requestId=${requestId} ok=${result.ok} checks=${result.checks.length}`);
2669
+ this.sendRelayControl({
2670
+ event: "doctor.result",
2671
+ requestId,
2672
+ targetClientId: replyTargetClientId || void 0,
2673
+ payload: {
2674
+ ok: result.ok,
2675
+ checks: result.checks,
2676
+ summary: result.summary,
2677
+ raw: result.raw
2678
+ }
2679
+ });
2680
+ } catch (error) {
2681
+ const message = error instanceof Error ? error.message : String(error);
2682
+ this.log(`relay doctor failed requestId=${requestId} error=${message}`);
2683
+ this.sendRelayControl({
2684
+ event: "doctor.error",
2685
+ requestId,
2686
+ targetClientId: replyTargetClientId || void 0,
2687
+ payload: {
2688
+ code: "doctor_failed",
2689
+ message
2690
+ }
2691
+ });
2692
+ }
2693
+ }
2694
+ async handleDoctorFixRequest(control) {
2695
+ const requestId = control.requestId?.trim() ?? "";
2696
+ const replyTargetClientId = control.sourceClientId?.trim() || control.targetClientId?.trim() || "";
2697
+ if (!requestId) {
2698
+ this.log("relay doctor-fix request dropped reason=missing_request_id");
2699
+ return;
2700
+ }
2701
+ this.log(`relay doctor-fix request received requestId=${requestId}`);
2702
+ try {
2703
+ const result = await runOpenClawDoctorFix();
2704
+ this.log(`relay doctor-fix completed requestId=${requestId} ok=${result.ok}`);
2705
+ this.sendRelayControl({
2706
+ event: "doctor-fix.result",
2707
+ requestId,
2708
+ targetClientId: replyTargetClientId || void 0,
2709
+ payload: {
2710
+ ok: result.ok,
2711
+ summary: result.summary,
2712
+ raw: result.raw
2713
+ }
2714
+ });
2715
+ } catch (error) {
2716
+ const message = error instanceof Error ? error.message : String(error);
2717
+ this.log(`relay doctor-fix failed requestId=${requestId} error=${message}`);
2718
+ this.sendRelayControl({
2719
+ event: "doctor-fix.error",
2720
+ requestId,
2721
+ targetClientId: replyTargetClientId || void 0,
2722
+ payload: {
2723
+ code: "doctor_fix_failed",
2724
+ message
2725
+ }
2726
+ });
2727
+ }
2728
+ }
2729
+ async handlePermissionsRequest(control) {
2730
+ const requestId = control.requestId?.trim() ?? "";
2731
+ const replyTargetClientId = control.sourceClientId?.trim() || control.targetClientId?.trim() || "";
2732
+ if (!requestId) {
2733
+ this.log("relay permissions request dropped reason=missing_request_id");
2734
+ return;
2735
+ }
2736
+ this.log(`relay permissions request received requestId=${requestId}`);
2737
+ try {
2738
+ const result = await readOpenClawPermissions();
2739
+ this.log(`relay permissions completed requestId=${requestId} execStatus=${result.exec.status} webStatus=${result.web.status}`);
2740
+ this.sendRelayControl({
2741
+ event: "permissions.result",
2742
+ requestId,
2743
+ targetClientId: replyTargetClientId || void 0,
2744
+ payload: result
2745
+ });
2746
+ } catch (error) {
2747
+ const message = error instanceof Error ? error.message : String(error);
2748
+ this.log(`relay permissions failed requestId=${requestId} error=${message}`);
2749
+ this.sendRelayControl({
2750
+ event: "permissions.error",
2751
+ requestId,
2752
+ targetClientId: replyTargetClientId || void 0,
2753
+ payload: {
2754
+ code: "permissions_failed",
2755
+ message
2756
+ }
2757
+ });
2758
+ }
2759
+ }
2059
2760
  forwardOrQueueGatewayMessage(message) {
2060
2761
  const isConnectHandshake = message.kind === "text" && isConnectHandshakeRequest(message.text);
2061
2762
  if (this.gatewayCloseBoundaryPending) {
@@ -2612,7 +3313,7 @@ function redactAuthorizationHeader(value) {
2612
3313
  return /^Bearer\s+/i.test(trimmed) ? "Bearer <redacted>" : "<redacted>";
2613
3314
  }
2614
3315
  function sanitizeRuntimeLogLine(line) {
2615
- return line;
3316
+ return line.replace(/\b(instanceId|clientId|sourceClientId|targetClientId|deviceId|requestId|reqId|traceId)=([^\s]+)/g, "$1=<redacted>").replace(/\b(relay|client)=([^\s]+)/g, "$1=<redacted>").replace(/\b(grs_[A-Za-z0-9_-]+|gct_[A-Za-z0-9_-]+)\b/g, "<redacted>");
2616
3317
  }
2617
3318
  function redactGatewayWsUrl(rawUrl) {
2618
3319
  const parsed = new URL(rawUrl);