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.
@@ -1,4 +1,4 @@
1
- import { H as saveCredentials, R as loadCredentials } from "./hovclaw.js";
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 { F as hasConfigFile, H as saveCredentials, L as loadConfig, M as getCredentialsPath, N as getDefaultFileConfig, O as config, P as getHovclawHome, R as loadCredentials, V as saveConfigFile, j as getConfigPath, m as ensureWorkspaceBootstrapForConfig, y as listAvailableSkills } from "./hovclaw.js";
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: { bashEnabled: appConfig.runtime.tools.bashEnabled }
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 { L as loadConfig, M as getCredentialsPath, P as getHovclawHome, j as getConfigPath, n as stopDaemon } from "./hovclaw.js";
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 ensureConfigFromLegacyEnv, B as resolveTelegramAccountConfig, C as resolveModelAlias, D as parseGatewayFrame, E as parseConnectParams, F as hasConfigFile, L as loadConfig, N as getDefaultFileConfig, O as config, S as parseModelRef, T as PROTOCOL_VERSION, U as writeOpenClawMirror, V as saveConfigFile, _ as extractAssistantText, a as LocalHostRuntime, b as loadSkill, c as redactSensitiveData, d as PiAgentManager, f as composeSessionKey, g as extractAssistantError, h as resolveAgentWorkspaceDir, i as createTools, l as TelegramChannel, m as ensureWorkspaceBootstrapForConfig, o as ContainerRuntime, p as WORKSPACE_CONTEXT_FILE_ORDER, r as TelegramPairingStore, s as HovClawDb, t as requestDaemonRestartFromCurrentProcess, u as DiscordChannel, v as toUserFacingAssistantError, w as logger, x as listConfiguredModelRefs, y as listAvailableSkills, z as loadFileConfig } from "./hovclaw.js";
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
- bashEnabled: config.runtime.tools.bashEnabled
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hovclaw",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Multi-channel AI agent gateway",
5
5
  "license": "MIT",
6
6
  "type": "module",