hovclaw 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -16
- package/dist/{doctor-D52M80De.js → doctor-0iphhiTj.js} +2 -2
- package/dist/hovclaw.js +893 -292
- package/dist/index.js +1194 -264
- package/dist/{login-BwvBMKdz.js → login-BtLE2Bye.js} +1 -1
- package/dist/{onboard-DL6VDf50.js → onboard-Cc2XHLT4.js} +12 -2
- package/dist/{reset-BJUhrojJ.js → reset-ChNzCD2s.js} +1 -1
- package/dist/{src-Y6AqidKn.js → src-GZDRRc5A.js} +309 -40
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { B as loadCredentials, W as saveCredentials } from "./hovclaw.js";
|
|
2
2
|
import { n as runOAuthLogin, t as SUPPORTED_OAUTH_PROVIDERS } from "./oauth-CQsXP0kP.js";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { A as config, B as loadCredentials, F as getDefaultFileConfig, I as getHovclawHome, L as hasConfigFile, N as getConfigPath, P as getCredentialsPath, U as saveConfigFile, W as saveCredentials, g as ensureWorkspaceBootstrapForConfig, x as listAvailableSkills, z as loadConfig } from "./hovclaw.js";
|
|
2
2
|
import { n as runOAuthLogin } from "./oauth-CQsXP0kP.js";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
@@ -727,7 +727,17 @@ function asFileConfig(appConfig) {
|
|
|
727
727
|
allowedReadRoots: [...appConfig.runtime.allowedReadRoots],
|
|
728
728
|
allowedWriteRoots: [...appConfig.runtime.allowedWriteRoots],
|
|
729
729
|
allowedCommandPrefixes: [...appConfig.runtime.allowedCommandPrefixes],
|
|
730
|
-
tools: {
|
|
730
|
+
tools: {
|
|
731
|
+
bashEnabled: appConfig.runtime.tools.bashEnabled,
|
|
732
|
+
exec: {
|
|
733
|
+
enabled: appConfig.runtime.tools.exec.enabled,
|
|
734
|
+
security: appConfig.runtime.tools.exec.security,
|
|
735
|
+
ask: appConfig.runtime.tools.exec.ask,
|
|
736
|
+
approvalTimeoutMs: appConfig.runtime.tools.exec.approvalTimeoutMs,
|
|
737
|
+
allowlist: [...appConfig.runtime.tools.exec.allowlist],
|
|
738
|
+
safeBins: [...appConfig.runtime.tools.exec.safeBins]
|
|
739
|
+
}
|
|
740
|
+
}
|
|
731
741
|
},
|
|
732
742
|
channels: {
|
|
733
743
|
discord: {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { I as getHovclawHome, N as getConfigPath, P as getCredentialsPath, n as stopDaemon, z as loadConfig } from "./hovclaw.js";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { cancel, confirm, isCancel, log, select } from "@clack/prompts";
|
|
@@ -1,15 +1,46 @@
|
|
|
1
|
-
import { A as
|
|
1
|
+
import { A as config, C as listConfiguredModelRefs, D as PROTOCOL_VERSION, E as logger, F as getDefaultFileConfig, H as resolveTelegramAccountConfig, L as hasConfigFile, M as ensureConfigFromLegacyEnv, O as parseConnectParams, S as loadSkill, T as resolveModelAlias, U as saveConfigFile, V as loadFileConfig, _ as resolveAgentWorkspaceDir, a as LocalHostRuntime, b as toUserFacingAssistantError, c as evaluateExecPolicy, d as TelegramChannel, f as DiscordChannel, g as ensureWorkspaceBootstrapForConfig, h as WORKSPACE_CONTEXT_FILE_ORDER, i as createTools, k as parseGatewayFrame, l as HovClawDb, m as composeSessionKey, o as ContainerRuntime, p as PiAgentManager, r as TelegramPairingStore, s as ExecApprovalsManager, t as requestDaemonRestartFromCurrentProcess, u as redactSensitiveData, v as extractAssistantError, w as parseModelRef, x as listAvailableSkills, y as extractAssistantText, z as loadConfig } from "./hovclaw.js";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { ZodError, z } from "zod";
|
|
6
5
|
import { randomUUID } from "node:crypto";
|
|
7
6
|
import WebSocket, { WebSocketServer } from "ws";
|
|
7
|
+
import { ZodError, z } from "zod";
|
|
8
8
|
import fs$1 from "node:fs/promises";
|
|
9
9
|
import { createServer } from "node:http";
|
|
10
10
|
import { spawnSync } from "node:child_process";
|
|
11
11
|
import { Cron } from "croner";
|
|
12
12
|
|
|
13
|
+
//#region src/channels/command-auth.ts
|
|
14
|
+
function normalizeAllowFromEntry(value) {
|
|
15
|
+
return String(value).trim().toLowerCase();
|
|
16
|
+
}
|
|
17
|
+
function senderCandidates(msg) {
|
|
18
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
19
|
+
const userId = msg.userId.trim();
|
|
20
|
+
if (userId) candidates.add(userId.toLowerCase());
|
|
21
|
+
const displayName = msg.displayName.trim();
|
|
22
|
+
if (displayName.startsWith("@")) candidates.add(displayName.toLowerCase());
|
|
23
|
+
return Array.from(candidates);
|
|
24
|
+
}
|
|
25
|
+
function resolveCommandsAllowFrom(config, channel) {
|
|
26
|
+
const entries = config.commands.allowFrom;
|
|
27
|
+
if (Object.keys(entries).length === 0) return null;
|
|
28
|
+
const providerSpecific = entries[channel];
|
|
29
|
+
if (Array.isArray(providerSpecific)) return providerSpecific;
|
|
30
|
+
const global = entries["*"];
|
|
31
|
+
if (Array.isArray(global)) return global;
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
function isCommandAuthorized(config, msg) {
|
|
35
|
+
const allowFrom = resolveCommandsAllowFrom(config, msg.channel);
|
|
36
|
+
if (allowFrom === null) return true;
|
|
37
|
+
const normalizedAllow = new Set(allowFrom.map((entry) => normalizeAllowFromEntry(entry)));
|
|
38
|
+
if (normalizedAllow.has("*")) return true;
|
|
39
|
+
for (const candidate of senderCandidates(msg)) if (normalizedAllow.has(candidate)) return true;
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
//#endregion
|
|
13
44
|
//#region src/channels/plugins/discord.ts
|
|
14
45
|
function createDiscordPlugin() {
|
|
15
46
|
return {
|
|
@@ -157,37 +188,6 @@ function buildModelCatalog(aliases) {
|
|
|
157
188
|
}).filter((entry) => Boolean(entry)).sort((a, b) => a.ref.localeCompare(b.ref));
|
|
158
189
|
}
|
|
159
190
|
|
|
160
|
-
//#endregion
|
|
161
|
-
//#region src/channels/command-auth.ts
|
|
162
|
-
function normalizeAllowFromEntry(value) {
|
|
163
|
-
return String(value).trim().toLowerCase();
|
|
164
|
-
}
|
|
165
|
-
function senderCandidates(msg) {
|
|
166
|
-
const candidates = /* @__PURE__ */ new Set();
|
|
167
|
-
const userId = msg.userId.trim();
|
|
168
|
-
if (userId) candidates.add(userId.toLowerCase());
|
|
169
|
-
const displayName = msg.displayName.trim();
|
|
170
|
-
if (displayName.startsWith("@")) candidates.add(displayName.toLowerCase());
|
|
171
|
-
return Array.from(candidates);
|
|
172
|
-
}
|
|
173
|
-
function resolveCommandsAllowFrom(config, channel) {
|
|
174
|
-
const entries = config.commands.allowFrom;
|
|
175
|
-
if (Object.keys(entries).length === 0) return null;
|
|
176
|
-
const providerSpecific = entries[channel];
|
|
177
|
-
if (Array.isArray(providerSpecific)) return providerSpecific;
|
|
178
|
-
const global = entries["*"];
|
|
179
|
-
if (Array.isArray(global)) return global;
|
|
180
|
-
return [];
|
|
181
|
-
}
|
|
182
|
-
function isCommandAuthorized(config, msg) {
|
|
183
|
-
const allowFrom = resolveCommandsAllowFrom(config, msg.channel);
|
|
184
|
-
if (allowFrom === null) return true;
|
|
185
|
-
const normalizedAllow = new Set(allowFrom.map((entry) => normalizeAllowFromEntry(entry)));
|
|
186
|
-
if (normalizedAllow.has("*")) return true;
|
|
187
|
-
for (const candidate of senderCandidates(msg)) if (normalizedAllow.has(candidate)) return true;
|
|
188
|
-
return false;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
191
|
//#endregion
|
|
192
192
|
//#region src/channels/commands-registry.ts
|
|
193
193
|
const TELEGRAM_COMMAND_NAME_RE = /^[a-z0-9_]{1,32}$/;
|
|
@@ -298,6 +298,11 @@ const BASE_COMMANDS = [
|
|
|
298
298
|
nativeName: "bash",
|
|
299
299
|
description: "Run a shell command through the assistant (if enabled)"
|
|
300
300
|
},
|
|
301
|
+
{
|
|
302
|
+
key: "approve",
|
|
303
|
+
nativeName: "approve",
|
|
304
|
+
description: "Approve or deny pending exec request"
|
|
305
|
+
},
|
|
301
306
|
{
|
|
302
307
|
key: "restart",
|
|
303
308
|
nativeName: "restart",
|
|
@@ -543,7 +548,6 @@ function reloadRuntimeConfig() {
|
|
|
543
548
|
function persistFileConfig(next) {
|
|
544
549
|
saveConfigFile(next);
|
|
545
550
|
reloadRuntimeConfig();
|
|
546
|
-
writeOpenClawMirror(config);
|
|
547
551
|
}
|
|
548
552
|
function parseConfigPath(raw) {
|
|
549
553
|
const pathValue = raw?.trim();
|
|
@@ -1311,6 +1315,156 @@ function evaluateTelegramPolicy(params) {
|
|
|
1311
1315
|
return { allowed: true };
|
|
1312
1316
|
}
|
|
1313
1317
|
|
|
1318
|
+
//#endregion
|
|
1319
|
+
//#region src/exec-chat.ts
|
|
1320
|
+
const APPROVE_USAGE = "Usage: /approve <id> allow-once|allow-always|deny";
|
|
1321
|
+
const BASH_USAGE = "Usage: /bash <command>";
|
|
1322
|
+
function normalizeDecision(value) {
|
|
1323
|
+
const normalized = value.trim().toLowerCase();
|
|
1324
|
+
if (normalized === "allow-once" || normalized === "allow-always" || normalized === "deny") return normalized;
|
|
1325
|
+
return null;
|
|
1326
|
+
}
|
|
1327
|
+
function formatExecOutput(result) {
|
|
1328
|
+
const lines = [
|
|
1329
|
+
`exitCode: ${result.exitCode}`,
|
|
1330
|
+
result.timedOut ? "timedOut: true" : "timedOut: false",
|
|
1331
|
+
result.truncated ? "truncated: true" : "truncated: false",
|
|
1332
|
+
""
|
|
1333
|
+
];
|
|
1334
|
+
if (result.stdout) {
|
|
1335
|
+
lines.push("stdout:");
|
|
1336
|
+
lines.push(result.stdout);
|
|
1337
|
+
lines.push("");
|
|
1338
|
+
}
|
|
1339
|
+
if (result.stderr) {
|
|
1340
|
+
lines.push("stderr:");
|
|
1341
|
+
lines.push(result.stderr);
|
|
1342
|
+
}
|
|
1343
|
+
if (!result.stdout && !result.stderr) lines.push("No output.");
|
|
1344
|
+
return lines.join("\n").trim();
|
|
1345
|
+
}
|
|
1346
|
+
async function executeApprovedCommand(options) {
|
|
1347
|
+
const result = await options.runtime.exec(options.command, { timeoutMs: options.timeoutMs });
|
|
1348
|
+
await options.channel.sendMessage(options.target, [
|
|
1349
|
+
`Approval granted. Executed: ${options.command}`,
|
|
1350
|
+
"",
|
|
1351
|
+
formatExecOutput(result)
|
|
1352
|
+
].join("\n"));
|
|
1353
|
+
}
|
|
1354
|
+
async function handleExecChatCommand(options) {
|
|
1355
|
+
const trimmed = options.msg.text.trim();
|
|
1356
|
+
if (!trimmed.startsWith("/")) return { handled: false };
|
|
1357
|
+
const parsed = trimmed.split(/\s+/);
|
|
1358
|
+
const command = (parsed[0] || "").toLowerCase();
|
|
1359
|
+
if (command === "/approve") {
|
|
1360
|
+
if (!options.commandAuthorized) {
|
|
1361
|
+
await options.channel.sendMessage(options.target, "You are not authorized to use this command.");
|
|
1362
|
+
return { handled: true };
|
|
1363
|
+
}
|
|
1364
|
+
const approvalId = parsed[1]?.trim();
|
|
1365
|
+
const decision = parsed[2] ? normalizeDecision(parsed[2]) : null;
|
|
1366
|
+
if (!approvalId || !decision) {
|
|
1367
|
+
await options.channel.sendMessage(options.target, APPROVE_USAGE);
|
|
1368
|
+
return { handled: true };
|
|
1369
|
+
}
|
|
1370
|
+
if (!options.execApprovals.resolve(approvalId, decision, options.msg.userId)) {
|
|
1371
|
+
await options.channel.sendMessage(options.target, `Unknown or expired approval id: ${approvalId}`);
|
|
1372
|
+
return { handled: true };
|
|
1373
|
+
}
|
|
1374
|
+
await options.channel.sendMessage(options.target, `Approval ${decision} recorded for ${approvalId}.`);
|
|
1375
|
+
options.audit({
|
|
1376
|
+
sessionKey: options.sessionKey,
|
|
1377
|
+
actor: "channel",
|
|
1378
|
+
eventType: "exec.approval.resolve",
|
|
1379
|
+
payload: {
|
|
1380
|
+
id: approvalId,
|
|
1381
|
+
decision,
|
|
1382
|
+
resolvedBy: options.msg.userId
|
|
1383
|
+
}
|
|
1384
|
+
});
|
|
1385
|
+
return { handled: true };
|
|
1386
|
+
}
|
|
1387
|
+
if (command !== "/bash") return { handled: false };
|
|
1388
|
+
if (!options.execPolicy.enabled) {
|
|
1389
|
+
await options.channel.sendMessage(options.target, "/bash is disabled. Set runtime.tools.exec.enabled=true (or runtime.tools.bashEnabled=true) to enable.");
|
|
1390
|
+
return { handled: true };
|
|
1391
|
+
}
|
|
1392
|
+
if (!options.commandAuthorized) {
|
|
1393
|
+
await options.channel.sendMessage(options.target, "You are not authorized to use this command.");
|
|
1394
|
+
return { handled: true };
|
|
1395
|
+
}
|
|
1396
|
+
if (!options.msg.text.slice(5).trim()) {
|
|
1397
|
+
await options.channel.sendMessage(options.target, BASH_USAGE);
|
|
1398
|
+
return { handled: true };
|
|
1399
|
+
}
|
|
1400
|
+
const bashCommand = options.msg.text.slice(5).trim();
|
|
1401
|
+
const evaluation = evaluateExecPolicy({
|
|
1402
|
+
command: bashCommand,
|
|
1403
|
+
agentId: options.agentId,
|
|
1404
|
+
policy: options.execPolicy,
|
|
1405
|
+
manager: options.execApprovals,
|
|
1406
|
+
cwd: process.cwd(),
|
|
1407
|
+
env: process.env
|
|
1408
|
+
});
|
|
1409
|
+
options.audit({
|
|
1410
|
+
sessionKey: options.sessionKey,
|
|
1411
|
+
actor: "channel",
|
|
1412
|
+
eventType: "exec.chat.request",
|
|
1413
|
+
payload: {
|
|
1414
|
+
command: bashCommand,
|
|
1415
|
+
security: options.execPolicy.security,
|
|
1416
|
+
ask: options.execPolicy.ask,
|
|
1417
|
+
allowlistSatisfied: evaluation.allowlistSatisfied,
|
|
1418
|
+
requiresApproval: evaluation.requiresApproval
|
|
1419
|
+
}
|
|
1420
|
+
});
|
|
1421
|
+
if (evaluation.denied) {
|
|
1422
|
+
await options.channel.sendMessage(options.target, evaluation.deniedReason || "Command denied by policy.");
|
|
1423
|
+
return { handled: true };
|
|
1424
|
+
}
|
|
1425
|
+
if (evaluation.requiresApproval) {
|
|
1426
|
+
const approval = options.execApprovals.request({
|
|
1427
|
+
command: bashCommand,
|
|
1428
|
+
agentId: options.agentId,
|
|
1429
|
+
sessionKey: options.sessionKey,
|
|
1430
|
+
resolvedPath: evaluation.resolvedPath,
|
|
1431
|
+
timeoutMs: options.execPolicy.approvalTimeoutMs,
|
|
1432
|
+
target: {
|
|
1433
|
+
channel: options.target.channel,
|
|
1434
|
+
chatId: options.target.chatId,
|
|
1435
|
+
accountId: options.target.accountId
|
|
1436
|
+
},
|
|
1437
|
+
onApproved: async (record) => {
|
|
1438
|
+
if (record.decision === "deny") return;
|
|
1439
|
+
await executeApprovedCommand({
|
|
1440
|
+
runtime: options.runtime,
|
|
1441
|
+
channel: options.channel,
|
|
1442
|
+
target: options.target,
|
|
1443
|
+
command: bashCommand,
|
|
1444
|
+
timeoutMs: options.execPolicy.approvalTimeoutMs
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
});
|
|
1448
|
+
await options.channel.sendMessage(options.target, [
|
|
1449
|
+
"Approval required for this command.",
|
|
1450
|
+
`id: ${approval.id}`,
|
|
1451
|
+
`expiresAt: ${new Date(approval.expiresAtMs).toISOString()}`,
|
|
1452
|
+
"",
|
|
1453
|
+
`Approve with: /approve ${approval.id} allow-once|allow-always|deny`
|
|
1454
|
+
].join("\n"));
|
|
1455
|
+
return { handled: true };
|
|
1456
|
+
}
|
|
1457
|
+
try {
|
|
1458
|
+
const result = await options.runtime.exec(bashCommand, { timeoutMs: options.execPolicy.approvalTimeoutMs });
|
|
1459
|
+
if (evaluation.allowlistPattern && !evaluation.allowlistPattern.startsWith("safe-bin:") && evaluation.allowlistPattern !== "one-time-approval") options.execApprovals.recordAllowlistUse(options.agentId, evaluation.allowlistPattern, bashCommand, evaluation.resolvedPath);
|
|
1460
|
+
await options.channel.sendMessage(options.target, formatExecOutput(result));
|
|
1461
|
+
} catch (error) {
|
|
1462
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1463
|
+
await options.channel.sendMessage(options.target, `Command failed: ${message}`);
|
|
1464
|
+
}
|
|
1465
|
+
return { handled: true };
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1314
1468
|
//#endregion
|
|
1315
1469
|
//#region src/gateway/methods/agent.ts
|
|
1316
1470
|
const agentParamsSchema = z.object({
|
|
@@ -1483,14 +1637,12 @@ const configGetMethod = async (_params, context) => {
|
|
|
1483
1637
|
const configSetMethod = async (params, context) => {
|
|
1484
1638
|
const parsed = configSetParamsSchema.parse(params);
|
|
1485
1639
|
context.writeFileConfig(parsed.config);
|
|
1486
|
-
writeOpenClawMirror(loadConfig());
|
|
1487
1640
|
return { ok: true };
|
|
1488
1641
|
};
|
|
1489
1642
|
const configPatchMethod = async (params, context) => {
|
|
1490
1643
|
const parsed = configPatchParamsSchema.parse(params);
|
|
1491
1644
|
const merged = deepMerge(context.readFileConfig(), parsed.patch);
|
|
1492
1645
|
context.writeFileConfig(merged);
|
|
1493
|
-
writeOpenClawMirror(loadConfig());
|
|
1494
1646
|
return { ok: true };
|
|
1495
1647
|
};
|
|
1496
1648
|
|
|
@@ -1509,6 +1661,68 @@ const cronStatusMethod = async (_params, context) => {
|
|
|
1509
1661
|
};
|
|
1510
1662
|
};
|
|
1511
1663
|
|
|
1664
|
+
//#endregion
|
|
1665
|
+
//#region src/gateway/methods/exec-approvals.ts
|
|
1666
|
+
const requestParamsSchema = z.object({
|
|
1667
|
+
command: z.string().min(1),
|
|
1668
|
+
agentId: z.string().min(1).optional(),
|
|
1669
|
+
sessionKey: z.string().min(1).optional(),
|
|
1670
|
+
resolvedPath: z.string().min(1).optional(),
|
|
1671
|
+
timeoutMs: z.number().int().positive().optional()
|
|
1672
|
+
});
|
|
1673
|
+
const resolveParamsSchema = z.object({
|
|
1674
|
+
id: z.string().min(1),
|
|
1675
|
+
decision: z.enum([
|
|
1676
|
+
"allow-once",
|
|
1677
|
+
"allow-always",
|
|
1678
|
+
"deny"
|
|
1679
|
+
]),
|
|
1680
|
+
resolvedBy: z.string().min(1).optional()
|
|
1681
|
+
});
|
|
1682
|
+
const setParamsSchema = z.object({ file: z.unknown() });
|
|
1683
|
+
const execApprovalRequestMethod = async (params, context) => {
|
|
1684
|
+
const parsed = requestParamsSchema.parse(params);
|
|
1685
|
+
const record = context.execApprovals.request({
|
|
1686
|
+
command: parsed.command,
|
|
1687
|
+
agentId: parsed.agentId ?? "main",
|
|
1688
|
+
sessionKey: parsed.sessionKey,
|
|
1689
|
+
resolvedPath: parsed.resolvedPath,
|
|
1690
|
+
timeoutMs: parsed.timeoutMs
|
|
1691
|
+
});
|
|
1692
|
+
return {
|
|
1693
|
+
id: record.id,
|
|
1694
|
+
createdAtMs: record.createdAtMs,
|
|
1695
|
+
expiresAtMs: record.expiresAtMs,
|
|
1696
|
+
command: record.command,
|
|
1697
|
+
agentId: record.agentId
|
|
1698
|
+
};
|
|
1699
|
+
};
|
|
1700
|
+
const execApprovalResolveMethod = async (params, context) => {
|
|
1701
|
+
const parsed = resolveParamsSchema.parse(params);
|
|
1702
|
+
const record = context.execApprovals.resolve(parsed.id, parsed.decision, parsed.resolvedBy);
|
|
1703
|
+
if (!record) throw new Error(`Unknown or expired approval id: ${parsed.id}`);
|
|
1704
|
+
return {
|
|
1705
|
+
ok: true,
|
|
1706
|
+
id: record.id,
|
|
1707
|
+
decision: parsed.decision,
|
|
1708
|
+
resolvedBy: parsed.resolvedBy ?? null,
|
|
1709
|
+
resolvedAtMs: record.resolvedAtMs ?? Date.now()
|
|
1710
|
+
};
|
|
1711
|
+
};
|
|
1712
|
+
const execApprovalsGetMethod = async (_params, context) => {
|
|
1713
|
+
return {
|
|
1714
|
+
path: context.execApprovals.getConfigPath(),
|
|
1715
|
+
file: context.execApprovals.getSnapshot()
|
|
1716
|
+
};
|
|
1717
|
+
};
|
|
1718
|
+
const execApprovalsSetMethod = async (params, context) => {
|
|
1719
|
+
const parsed = setParamsSchema.parse(params);
|
|
1720
|
+
return {
|
|
1721
|
+
ok: true,
|
|
1722
|
+
file: context.execApprovals.setSnapshot(parsed.file)
|
|
1723
|
+
};
|
|
1724
|
+
};
|
|
1725
|
+
|
|
1512
1726
|
//#endregion
|
|
1513
1727
|
//#region src/gateway/methods/health.ts
|
|
1514
1728
|
const healthMethod = async (_params, context) => {
|
|
@@ -1747,14 +1961,20 @@ const gatewayMethodHandlers = {
|
|
|
1747
1961
|
"chat.abort": chatAbortMethod,
|
|
1748
1962
|
"cron.list": cronListMethod,
|
|
1749
1963
|
"cron.status": cronStatusMethod,
|
|
1750
|
-
"logs.tail": logsTailMethod
|
|
1964
|
+
"logs.tail": logsTailMethod,
|
|
1965
|
+
"exec.approval.request": execApprovalRequestMethod,
|
|
1966
|
+
"exec.approval.resolve": execApprovalResolveMethod,
|
|
1967
|
+
"exec.approvals.get": execApprovalsGetMethod,
|
|
1968
|
+
"exec.approvals.set": execApprovalsSetMethod
|
|
1751
1969
|
};
|
|
1752
1970
|
const gatewayEventNames = [
|
|
1753
1971
|
"tick",
|
|
1754
1972
|
"health",
|
|
1755
1973
|
"agent",
|
|
1756
1974
|
"chat",
|
|
1757
|
-
"shutdown"
|
|
1975
|
+
"shutdown",
|
|
1976
|
+
"exec.approval.requested",
|
|
1977
|
+
"exec.approval.resolved"
|
|
1758
1978
|
];
|
|
1759
1979
|
|
|
1760
1980
|
//#endregion
|
|
@@ -1888,6 +2108,7 @@ function resolveUiDirectory() {
|
|
|
1888
2108
|
}
|
|
1889
2109
|
var HovClawGatewayServer = class {
|
|
1890
2110
|
db;
|
|
2111
|
+
execApprovals;
|
|
1891
2112
|
agentManager;
|
|
1892
2113
|
channels;
|
|
1893
2114
|
readFileConfig;
|
|
@@ -1903,6 +2124,7 @@ var HovClawGatewayServer = class {
|
|
|
1903
2124
|
constructor(options) {
|
|
1904
2125
|
this.runtimeConfig = options.config;
|
|
1905
2126
|
this.db = options.db;
|
|
2127
|
+
this.execApprovals = options.execApprovals;
|
|
1906
2128
|
this.agentManager = options.agentManager;
|
|
1907
2129
|
this.channels = options.channels;
|
|
1908
2130
|
this.readFileConfig = options.readFileConfig;
|
|
@@ -2176,6 +2398,7 @@ var HovClawGatewayServer = class {
|
|
|
2176
2398
|
const context = {
|
|
2177
2399
|
config: this.runtimeConfig,
|
|
2178
2400
|
db: this.db,
|
|
2401
|
+
execApprovals: this.execApprovals,
|
|
2179
2402
|
agentManager: this.agentManager,
|
|
2180
2403
|
channels: this.channels,
|
|
2181
2404
|
startedAt: this.startedAt,
|
|
@@ -2604,7 +2827,6 @@ async function main() {
|
|
|
2604
2827
|
} catch (error) {
|
|
2605
2828
|
logger.warn({ error }, "Workspace bootstrap failed");
|
|
2606
2829
|
}
|
|
2607
|
-
writeOpenClawMirror(config);
|
|
2608
2830
|
const db = new HovClawDb(path.join(config.storeDir, "hovclaw.db"));
|
|
2609
2831
|
db.ping();
|
|
2610
2832
|
const runtime = config.runtime.mode === "container" ? new ContainerRuntime({
|
|
@@ -2623,10 +2845,40 @@ async function main() {
|
|
|
2623
2845
|
allowedWriteRoots: config.runtime.allowedWriteRoots,
|
|
2624
2846
|
allowedCommandPrefixes: config.runtime.allowedCommandPrefixes
|
|
2625
2847
|
});
|
|
2848
|
+
let emitGatewayEvent = null;
|
|
2849
|
+
const execApprovals = new ExecApprovalsManager({
|
|
2850
|
+
storeDir: config.storeDir,
|
|
2851
|
+
defaults: {
|
|
2852
|
+
security: config.runtime.tools.exec.security,
|
|
2853
|
+
ask: config.runtime.tools.exec.ask
|
|
2854
|
+
},
|
|
2855
|
+
onRequested: (record) => {
|
|
2856
|
+
emitGatewayEvent?.("exec.approval.requested", {
|
|
2857
|
+
id: record.id,
|
|
2858
|
+
request: {
|
|
2859
|
+
command: record.command,
|
|
2860
|
+
agentId: record.agentId,
|
|
2861
|
+
sessionKey: record.sessionKey,
|
|
2862
|
+
resolvedPath: record.resolvedPath
|
|
2863
|
+
},
|
|
2864
|
+
createdAtMs: record.createdAtMs,
|
|
2865
|
+
expiresAtMs: record.expiresAtMs
|
|
2866
|
+
});
|
|
2867
|
+
},
|
|
2868
|
+
onResolved: (record) => {
|
|
2869
|
+
emitGatewayEvent?.("exec.approval.resolved", {
|
|
2870
|
+
id: record.id,
|
|
2871
|
+
decision: record.decision,
|
|
2872
|
+
resolvedBy: record.resolvedBy ?? null,
|
|
2873
|
+
resolvedAtMs: record.resolvedAtMs
|
|
2874
|
+
});
|
|
2875
|
+
}
|
|
2876
|
+
});
|
|
2626
2877
|
const agentManager = new PiAgentManager(db, createTools({
|
|
2627
2878
|
runtime,
|
|
2628
2879
|
audit: (record) => db.appendAuditEvent(record),
|
|
2629
|
-
|
|
2880
|
+
execPolicy: config.runtime.tools.exec,
|
|
2881
|
+
execApprovals
|
|
2630
2882
|
}));
|
|
2631
2883
|
let lastMessageAt = null;
|
|
2632
2884
|
const telegramPairingStore = new TelegramPairingStore(config.storeDir);
|
|
@@ -2650,6 +2902,19 @@ async function main() {
|
|
|
2650
2902
|
if (!normalizedPrompt) return;
|
|
2651
2903
|
let thinkingLevel = config.commands.defaultThinkingLevel;
|
|
2652
2904
|
let thinkingLevelForced = false;
|
|
2905
|
+
const commandAuthorized = isCommandAuthorized(config, msg);
|
|
2906
|
+
if ((await handleExecChatCommand({
|
|
2907
|
+
msg,
|
|
2908
|
+
target,
|
|
2909
|
+
channel,
|
|
2910
|
+
runtime,
|
|
2911
|
+
execApprovals,
|
|
2912
|
+
execPolicy: config.runtime.tools.exec,
|
|
2913
|
+
commandAuthorized,
|
|
2914
|
+
agentId,
|
|
2915
|
+
sessionKey,
|
|
2916
|
+
audit: (record) => db.appendAuditEvent(record)
|
|
2917
|
+
})).handled) return;
|
|
2653
2918
|
if (msg.channel === "telegram") {
|
|
2654
2919
|
const policy = evaluateTelegramPolicy({
|
|
2655
2920
|
config,
|
|
@@ -2794,12 +3059,16 @@ async function main() {
|
|
|
2794
3059
|
const gatewayServer = config.gateway.enabled ? new HovClawGatewayServer({
|
|
2795
3060
|
config,
|
|
2796
3061
|
db,
|
|
3062
|
+
execApprovals,
|
|
2797
3063
|
agentManager,
|
|
2798
3064
|
channels,
|
|
2799
3065
|
getChannelStatus: () => channelPluginManager.buildStatusPayload(),
|
|
2800
3066
|
readFileConfig,
|
|
2801
3067
|
writeFileConfig
|
|
2802
3068
|
}) : null;
|
|
3069
|
+
emitGatewayEvent = gatewayServer ? (event, payload) => {
|
|
3070
|
+
gatewayServer.emitEvent(event, payload);
|
|
3071
|
+
} : null;
|
|
2803
3072
|
gatewayServer?.start();
|
|
2804
3073
|
const healthPortRaw = process.env.HEALTH_PORT || "8787";
|
|
2805
3074
|
const healthPort = Number(healthPortRaw);
|