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/dist/hovclaw.js CHANGED
@@ -4,11 +4,11 @@ import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { log } from "@clack/prompts";
6
6
  import { Command } from "commander";
7
+ import crypto, { randomUUID, timingSafeEqual } from "node:crypto";
8
+ import WebSocket from "ws";
7
9
  import os from "node:os";
8
10
  import dotenv from "dotenv";
9
11
  import { z } from "zod";
10
- import crypto, { randomUUID, timingSafeEqual } from "node:crypto";
11
- import WebSocket from "ws";
12
12
  import fs$1 from "node:fs/promises";
13
13
  import { Agent } from "@mariozechner/pi-agent-core";
14
14
  import { Type, getModel, getOAuthApiKey, getProviders } from "@mariozechner/pi-ai";
@@ -39,132 +39,6 @@ var __exportAll = (all, no_symbols) => {
39
39
  return target;
40
40
  };
41
41
 
42
- //#endregion
43
- //#region src/compat/openclaw-mirror.ts
44
- function resolveOpenClawHome(env = process.env) {
45
- const override = env.OPENCLAW_STATE_DIR?.trim();
46
- if (override) return path.resolve(override.startsWith("~") ? path.join(os.homedir(), override.slice(1)) : override);
47
- return path.join(os.homedir(), ".openclaw");
48
- }
49
- function resolveOpenClawConfigPath(openclawHome) {
50
- return path.join(openclawHome, "openclaw.json");
51
- }
52
- function resolveOpenClawSharedSkillsPath(openclawHome) {
53
- return path.join(openclawHome, "skills");
54
- }
55
- function readJsonIfExists(filePath) {
56
- if (!fs.existsSync(filePath)) return null;
57
- try {
58
- return JSON.parse(fs.readFileSync(filePath, "utf8"));
59
- } catch {
60
- return null;
61
- }
62
- }
63
- function buildMirrorConfig(config) {
64
- const fallbackWorkspace = config.agents.defaults.workspace || path.join(config.hovclawHome, "workspace");
65
- const agentList = config.agents.list.length > 0 ? config.agents.list : [{
66
- id: "main",
67
- name: "Main",
68
- workspace: fallbackWorkspace,
69
- default: true
70
- }];
71
- const extraDirs = /* @__PURE__ */ new Set();
72
- extraDirs.add(config.skillsDir);
73
- for (const agent of agentList) {
74
- const workspace = (agent.workspace || fallbackWorkspace).trim();
75
- if (!workspace) continue;
76
- extraDirs.add(path.join(workspace, "skills"));
77
- }
78
- return {
79
- agent: { workspace: fallbackWorkspace },
80
- agents: {
81
- defaults: { workspace: fallbackWorkspace },
82
- list: agentList
83
- },
84
- skills: { load: { extraDirs: Array.from(extraDirs) } }
85
- };
86
- }
87
- function ensureDir(dirPath) {
88
- fs.mkdirSync(dirPath, {
89
- recursive: true,
90
- mode: 448
91
- });
92
- }
93
- function syncSkillsDir(sharedSkillsPath, sourceSkillsPath) {
94
- if (!fs.existsSync(sourceSkillsPath)) {
95
- ensureDir(sharedSkillsPath);
96
- return false;
97
- }
98
- try {
99
- if (fs.lstatSync(sharedSkillsPath).isSymbolicLink()) {
100
- const linkTarget = fs.readlinkSync(sharedSkillsPath);
101
- if (path.resolve(path.dirname(sharedSkillsPath), linkTarget) === sourceSkillsPath) return true;
102
- fs.unlinkSync(sharedSkillsPath);
103
- }
104
- } catch {}
105
- if (!fs.existsSync(sharedSkillsPath)) try {
106
- fs.symlinkSync(sourceSkillsPath, sharedSkillsPath, "dir");
107
- return true;
108
- } catch {}
109
- ensureDir(sharedSkillsPath);
110
- for (const entry of fs.readdirSync(sourceSkillsPath, { withFileTypes: true })) {
111
- const src = path.join(sourceSkillsPath, entry.name);
112
- const dst = path.join(sharedSkillsPath, entry.name);
113
- if (entry.isDirectory()) {
114
- fs.cpSync(src, dst, {
115
- recursive: true,
116
- force: true
117
- });
118
- continue;
119
- }
120
- if (entry.isFile()) fs.copyFileSync(src, dst);
121
- }
122
- return false;
123
- }
124
- function writeOpenClawMirror(config) {
125
- const openclawHome = resolveOpenClawHome();
126
- const configPath = resolveOpenClawConfigPath(openclawHome);
127
- const sharedSkillsPath = resolveOpenClawSharedSkillsPath(openclawHome);
128
- ensureDir(openclawHome);
129
- const mirrorConfig = buildMirrorConfig(config);
130
- fs.writeFileSync(configPath, `${JSON.stringify(mirrorConfig, null, 2)}\n`, {
131
- encoding: "utf8",
132
- mode: 384
133
- });
134
- fs.chmodSync(configPath, 384);
135
- return {
136
- openclawHome,
137
- configPath,
138
- sharedSkillsPath,
139
- linkedSharedSkills: syncSkillsDir(sharedSkillsPath, config.skillsDir)
140
- };
141
- }
142
- function getOpenClawMirrorStatus(config) {
143
- const openclawHome = resolveOpenClawHome();
144
- const configPath = resolveOpenClawConfigPath(openclawHome);
145
- const sharedSkillsPath = resolveOpenClawSharedSkillsPath(openclawHome);
146
- const expectedConfig = buildMirrorConfig(config);
147
- const currentConfig = readJsonIfExists(configPath);
148
- let linkedSharedSkills = false;
149
- try {
150
- if (fs.lstatSync(sharedSkillsPath).isSymbolicLink()) {
151
- const target = fs.readlinkSync(sharedSkillsPath);
152
- linkedSharedSkills = path.resolve(path.dirname(sharedSkillsPath), target) === path.resolve(config.skillsDir);
153
- }
154
- } catch {
155
- linkedSharedSkills = false;
156
- }
157
- return {
158
- openclawHome,
159
- configPath,
160
- sharedSkillsPath,
161
- linkedSharedSkills,
162
- configExists: fs.existsSync(configPath),
163
- configInSync: JSON.stringify(currentConfig) === JSON.stringify(expectedConfig),
164
- skillsPathExists: fs.existsSync(sharedSkillsPath)
165
- };
166
- }
167
-
168
42
  //#endregion
169
43
  //#region src/config.ts
170
44
  dotenv.config();
@@ -242,7 +116,27 @@ const DEFAULT_FILE_CONFIG = {
242
116
  "tail",
243
117
  "wc"
244
118
  ],
245
- tools: { bashEnabled: false }
119
+ tools: {
120
+ bashEnabled: false,
121
+ exec: {
122
+ enabled: false,
123
+ security: "allowlist",
124
+ ask: "on-miss",
125
+ approvalTimeoutMs: 12e4,
126
+ allowlist: [],
127
+ safeBins: [...[
128
+ "jq",
129
+ "grep",
130
+ "cut",
131
+ "sort",
132
+ "uniq",
133
+ "head",
134
+ "tail",
135
+ "tr",
136
+ "wc"
137
+ ]]
138
+ }
139
+ }
246
140
  },
247
141
  channels: {
248
142
  discord: {
@@ -347,6 +241,22 @@ const commandsConfigSchema = z.object({
347
241
  useAccessGroups: z.boolean(),
348
242
  allowFrom: commandAllowFromSchema
349
243
  });
244
+ const runtimeExecConfigSchema = z.object({
245
+ enabled: z.boolean(),
246
+ security: z.enum([
247
+ "deny",
248
+ "allowlist",
249
+ "full"
250
+ ]),
251
+ ask: z.enum([
252
+ "off",
253
+ "on-miss",
254
+ "always"
255
+ ]),
256
+ approvalTimeoutMs: z.number().int().positive(),
257
+ allowlist: z.array(z.string().min(1)),
258
+ safeBins: z.array(z.string().min(1))
259
+ });
350
260
  const telegramTopicConfigSchema = z.object({
351
261
  enabled: z.boolean().optional(),
352
262
  requireMention: z.boolean().optional(),
@@ -509,7 +419,10 @@ const fileConfigSchema = z.object({
509
419
  allowedReadRoots: z.array(z.string().min(1)),
510
420
  allowedWriteRoots: z.array(z.string().min(1)),
511
421
  allowedCommandPrefixes: z.array(z.string().min(1)).min(1),
512
- tools: z.object({ bashEnabled: z.boolean() })
422
+ tools: z.object({
423
+ bashEnabled: z.boolean(),
424
+ exec: runtimeExecConfigSchema
425
+ })
513
426
  }),
514
427
  channels: z.object({
515
428
  discord: z.object({
@@ -586,7 +499,21 @@ const partialFileConfigSchema = z.object({
586
499
  bindings: fileConfigSchema.shape.bindings.optional(),
587
500
  models: fileConfigSchema.shape.models.partial().optional(),
588
501
  commands: commandsConfigSchema.partial().optional(),
589
- runtime: fileConfigSchema.shape.runtime.partial().optional(),
502
+ runtime: z.object({
503
+ mode: z.enum(["local", "container"]).optional(),
504
+ containerImage: z.string().min(1).optional(),
505
+ idleTimeoutMs: z.number().int().positive().optional(),
506
+ networkMode: z.string().min(1).optional(),
507
+ timeoutMs: z.number().int().positive().optional(),
508
+ maxOutputBytes: z.number().int().positive().optional(),
509
+ allowedReadRoots: z.array(z.string().min(1)).optional(),
510
+ allowedWriteRoots: z.array(z.string().min(1)).optional(),
511
+ allowedCommandPrefixes: z.array(z.string().min(1)).min(1).optional(),
512
+ tools: z.object({
513
+ bashEnabled: z.boolean().optional(),
514
+ exec: runtimeExecConfigSchema.partial().optional()
515
+ }).optional()
516
+ }).optional(),
590
517
  channels: partialChannelsSchema,
591
518
  gateway: partialGatewayConfigSchema,
592
519
  scheduler: fileConfigSchema.shape.scheduler.partial().optional()
@@ -857,7 +784,17 @@ function mergeWithDefaults(partial) {
857
784
  allowedReadRoots: partial.runtime?.allowedReadRoots ?? DEFAULT_FILE_CONFIG.runtime.allowedReadRoots,
858
785
  allowedWriteRoots: partial.runtime?.allowedWriteRoots ?? DEFAULT_FILE_CONFIG.runtime.allowedWriteRoots,
859
786
  allowedCommandPrefixes: partial.runtime?.allowedCommandPrefixes ?? DEFAULT_FILE_CONFIG.runtime.allowedCommandPrefixes,
860
- tools: { bashEnabled: partial.runtime?.tools?.bashEnabled ?? DEFAULT_FILE_CONFIG.runtime.tools.bashEnabled }
787
+ tools: {
788
+ bashEnabled: partial.runtime?.tools?.bashEnabled ?? DEFAULT_FILE_CONFIG.runtime.tools.bashEnabled,
789
+ exec: {
790
+ enabled: partial.runtime?.tools?.exec?.enabled ?? partial.runtime?.tools?.bashEnabled ?? DEFAULT_FILE_CONFIG.runtime.tools.exec.enabled,
791
+ security: partial.runtime?.tools?.exec?.security ?? DEFAULT_FILE_CONFIG.runtime.tools.exec.security,
792
+ ask: partial.runtime?.tools?.exec?.ask ?? DEFAULT_FILE_CONFIG.runtime.tools.exec.ask,
793
+ approvalTimeoutMs: partial.runtime?.tools?.exec?.approvalTimeoutMs ?? DEFAULT_FILE_CONFIG.runtime.tools.exec.approvalTimeoutMs,
794
+ allowlist: partial.runtime?.tools?.exec?.allowlist ?? DEFAULT_FILE_CONFIG.runtime.tools.exec.allowlist,
795
+ safeBins: partial.runtime?.tools?.exec?.safeBins ?? DEFAULT_FILE_CONFIG.runtime.tools.exec.safeBins
796
+ }
797
+ }
861
798
  },
862
799
  channels: {
863
800
  discord: {
@@ -969,7 +906,17 @@ function applyEnvOverrides(base, env) {
969
906
  allowedReadRoots: splitCsv(env.ALLOWED_READ_ROOTS, base.runtime.allowedReadRoots),
970
907
  allowedWriteRoots: splitCsv(env.ALLOWED_WRITE_ROOTS, base.runtime.allowedWriteRoots),
971
908
  allowedCommandPrefixes: splitCsv(env.ALLOWED_COMMAND_PREFIXES, base.runtime.allowedCommandPrefixes),
972
- tools: { bashEnabled: toBool(env.RUNTIME_BASH_ENABLED, base.runtime.tools.bashEnabled) }
909
+ tools: {
910
+ bashEnabled: toBool(env.RUNTIME_BASH_ENABLED, base.runtime.tools.bashEnabled),
911
+ exec: {
912
+ enabled: toBool(env.RUNTIME_EXEC_ENABLED, toBool(env.RUNTIME_BASH_ENABLED, base.runtime.tools.exec.enabled)),
913
+ security: env.RUNTIME_EXEC_SECURITY === "deny" || env.RUNTIME_EXEC_SECURITY === "allowlist" || env.RUNTIME_EXEC_SECURITY === "full" ? env.RUNTIME_EXEC_SECURITY : base.runtime.tools.exec.security,
914
+ ask: env.RUNTIME_EXEC_ASK === "off" || env.RUNTIME_EXEC_ASK === "on-miss" || env.RUNTIME_EXEC_ASK === "always" ? env.RUNTIME_EXEC_ASK : base.runtime.tools.exec.ask,
915
+ approvalTimeoutMs: toPositiveInt(env.RUNTIME_EXEC_APPROVAL_TIMEOUT_MS, base.runtime.tools.exec.approvalTimeoutMs),
916
+ allowlist: splitCsv(env.RUNTIME_EXEC_ALLOWLIST, base.runtime.tools.exec.allowlist),
917
+ safeBins: splitCsv(env.RUNTIME_EXEC_SAFE_BINS, base.runtime.tools.exec.safeBins)
918
+ }
919
+ }
973
920
  },
974
921
  channels: {
975
922
  discord: {
@@ -1074,7 +1021,17 @@ function loadConfig(env = process.env) {
1074
1021
  allowedReadRoots: normalizeRoots(merged.runtime.allowedReadRoots),
1075
1022
  allowedWriteRoots: normalizeRoots(merged.runtime.allowedWriteRoots),
1076
1023
  allowedCommandPrefixes: merged.runtime.allowedCommandPrefixes,
1077
- tools: { bashEnabled: merged.runtime.tools.bashEnabled }
1024
+ tools: {
1025
+ bashEnabled: merged.runtime.tools.bashEnabled,
1026
+ exec: {
1027
+ enabled: merged.runtime.tools.exec.enabled || merged.runtime.tools.bashEnabled,
1028
+ security: merged.runtime.tools.exec.security,
1029
+ ask: merged.runtime.tools.exec.ask,
1030
+ approvalTimeoutMs: merged.runtime.tools.exec.approvalTimeoutMs,
1031
+ allowlist: [...merged.runtime.tools.exec.allowlist],
1032
+ safeBins: [...merged.runtime.tools.exec.safeBins]
1033
+ }
1034
+ }
1078
1035
  },
1079
1036
  channels: {
1080
1037
  discord: {
@@ -1172,6 +1129,12 @@ function legacyEnvKeys() {
1172
1129
  "ALLOWED_WRITE_ROOTS",
1173
1130
  "ALLOWED_COMMAND_PREFIXES",
1174
1131
  "RUNTIME_BASH_ENABLED",
1132
+ "RUNTIME_EXEC_ENABLED",
1133
+ "RUNTIME_EXEC_SECURITY",
1134
+ "RUNTIME_EXEC_ASK",
1135
+ "RUNTIME_EXEC_APPROVAL_TIMEOUT_MS",
1136
+ "RUNTIME_EXEC_ALLOWLIST",
1137
+ "RUNTIME_EXEC_SAFE_BINS",
1175
1138
  "TOOL_TIMEOUT_MS",
1176
1139
  "TOOL_MAX_OUTPUT_BYTES",
1177
1140
  "ENABLE_DISCORD",
@@ -1251,31 +1214,6 @@ function getProviderApiKeyFromEnv(provider, env = process.env) {
1251
1214
  }
1252
1215
  const config = loadConfig();
1253
1216
 
1254
- //#endregion
1255
- //#region src/cli/compat.ts
1256
- function renderStatus(status) {
1257
- const lines = [];
1258
- lines.push(`OpenClaw home: ${status.openclawHome}`);
1259
- lines.push(`Config path: ${status.configPath}`);
1260
- lines.push(`Config exists: ${status.configExists ? "yes" : "no"}`);
1261
- lines.push(`Config in sync: ${status.configInSync ? "yes" : "no"}`);
1262
- lines.push(`Skills path: ${status.sharedSkillsPath}`);
1263
- lines.push(`Skills exists: ${status.skillsPathExists ? "yes" : "no"}`);
1264
- lines.push(`Skills symlinked: ${status.linkedSharedSkills ? "yes" : "no"}`);
1265
- return lines.join("\n");
1266
- }
1267
- function registerCompatCommands(program) {
1268
- program.command("compat").description("Compatibility helpers").command("status").description("Show OpenClaw mirror compatibility status").option("--sync", "Rewrite mirror before reading status").option("--json", "Print JSON output").action((options) => {
1269
- if (options.sync) writeOpenClawMirror(config);
1270
- const status = getOpenClawMirrorStatus(config);
1271
- if (options.json) {
1272
- process.stdout.write(`${JSON.stringify(status, null, 2)}\n`);
1273
- return;
1274
- }
1275
- process.stdout.write(`${renderStatus(status)}\n`);
1276
- });
1277
- }
1278
-
1279
1217
  //#endregion
1280
1218
  //#region src/gateway/protocol/schema.ts
1281
1219
  const nonEmptyStringSchema = z.string().min(1);
@@ -3153,7 +3091,7 @@ function redactSensitiveData(value) {
3153
3091
 
3154
3092
  //#endregion
3155
3093
  //#region src/db.ts
3156
- function nowIso$1() {
3094
+ function nowIso$2() {
3157
3095
  return (/* @__PURE__ */ new Date()).toISOString();
3158
3096
  }
3159
3097
  var HovClawDb = class {
@@ -3281,7 +3219,7 @@ var HovClawDb = class {
3281
3219
  return Boolean(row?.name);
3282
3220
  }
3283
3221
  upsertSession(parts, sessionKey, model, options) {
3284
- const createdAt = nowIso$1();
3222
+ const createdAt = nowIso$2();
3285
3223
  this.db.prepare(`
3286
3224
  INSERT INTO sessions (
3287
3225
  session_key,
@@ -3376,7 +3314,7 @@ var HovClawDb = class {
3376
3314
  ON CONFLICT(account_id) DO UPDATE SET
3377
3315
  last_update_id = excluded.last_update_id,
3378
3316
  updated_at = excluded.updated_at
3379
- `).run(accountId, updateId, nowIso$1());
3317
+ `).run(accountId, updateId, nowIso$2());
3380
3318
  }
3381
3319
  hasTelegramDedupe(accountId, updateId) {
3382
3320
  return typeof this.db.prepare(`
@@ -3389,7 +3327,7 @@ var HovClawDb = class {
3389
3327
  this.db.prepare(`
3390
3328
  INSERT OR IGNORE INTO telegram_dedupe (account_id, update_id, updated_at)
3391
3329
  VALUES (?, ?, ?)
3392
- `).run(accountId, updateId, nowIso$1());
3330
+ `).run(accountId, updateId, nowIso$2());
3393
3331
  }
3394
3332
  pruneTelegramDedupe(olderThanIso) {
3395
3333
  return this.db.prepare(`
@@ -3398,7 +3336,7 @@ var HovClawDb = class {
3398
3336
  `).run(olderThanIso).changes;
3399
3337
  }
3400
3338
  appendMessage(sessionKey, role, content) {
3401
- this.db.prepare(`INSERT INTO messages (session_key, role, content, created_at) VALUES (?, ?, ?, ?)`).run(sessionKey, role, content, nowIso$1());
3339
+ this.db.prepare(`INSERT INTO messages (session_key, role, content, created_at) VALUES (?, ?, ?, ?)`).run(sessionKey, role, content, nowIso$2());
3402
3340
  }
3403
3341
  getMessages(sessionKey) {
3404
3342
  return this.db.prepare(`
@@ -3419,7 +3357,7 @@ var HovClawDb = class {
3419
3357
  ON CONFLICT(session_key) DO UPDATE SET
3420
3358
  state_json = excluded.state_json,
3421
3359
  updated_at = excluded.updated_at
3422
- `).run(sessionKey, stateJson, nowIso$1());
3360
+ `).run(sessionKey, stateJson, nowIso$2());
3423
3361
  }
3424
3362
  getAgentState(sessionKey) {
3425
3363
  return this.db.prepare(`SELECT state_json FROM agent_state WHERE session_key = ?`).get(sessionKey)?.state_json ?? null;
@@ -3440,7 +3378,7 @@ var HovClawDb = class {
3440
3378
  cost_usd,
3441
3379
  created_at
3442
3380
  ) VALUES (?, ?, ?, ?, ?, ?, ?)
3443
- `).run(record.sessionKey, record.provider, record.model, record.inputTokens, record.outputTokens, record.costUsd, record.createdAt ?? nowIso$1());
3381
+ `).run(record.sessionKey, record.provider, record.model, record.inputTokens, record.outputTokens, record.costUsd, record.createdAt ?? nowIso$2());
3444
3382
  }
3445
3383
  upsertScheduledJob(job) {
3446
3384
  this.db.prepare(`
@@ -3574,7 +3512,7 @@ var HovClawDb = class {
3574
3512
  this.db.prepare(`
3575
3513
  INSERT INTO audit_log (ts, session_key, actor, event_type, payload_json)
3576
3514
  VALUES (?, ?, ?, ?, ?)
3577
- `).run(record.ts ?? nowIso$1(), record.sessionKey ?? null, record.actor, record.eventType, JSON.stringify(sanitizedPayload));
3515
+ `).run(record.ts ?? nowIso$2(), record.sessionKey ?? null, record.actor, record.eventType, JSON.stringify(sanitizedPayload));
3578
3516
  }
3579
3517
  getAuditEvents(eventType) {
3580
3518
  const query = eventType ? `SELECT ts, session_key, actor, event_type, payload_json FROM audit_log WHERE event_type = ? ORDER BY id DESC` : `SELECT ts, session_key, actor, event_type, payload_json FROM audit_log ORDER BY id DESC`;
@@ -3588,6 +3526,435 @@ var HovClawDb = class {
3588
3526
  }
3589
3527
  };
3590
3528
 
3529
+ //#endregion
3530
+ //#region src/exec-approvals.ts
3531
+ const DEFAULT_FILE_VERSION = 1;
3532
+ const DEFAULT_APPROVAL_TIMEOUT_MS = 12e4;
3533
+ const ONE_TIME_APPROVAL_TTL_MS = 5 * 6e4;
3534
+ const SHELL_DISALLOWED_PATTERN = /[;&|`$()<>]|[\r\n]/;
3535
+ const SHELL_QUOTE_OR_ESCAPE_PATTERN = /["'\\]/;
3536
+ function nowIso$1() {
3537
+ return (/* @__PURE__ */ new Date()).toISOString();
3538
+ }
3539
+ function normalizeExecSecurity(value) {
3540
+ if (value === "deny" || value === "allowlist" || value === "full") return value;
3541
+ return null;
3542
+ }
3543
+ function normalizeExecAsk(value) {
3544
+ if (value === "off" || value === "on-miss" || value === "always") return value;
3545
+ return null;
3546
+ }
3547
+ function normalizeAllowlist(entries) {
3548
+ if (!Array.isArray(entries)) return [];
3549
+ const out = [];
3550
+ const seen = /* @__PURE__ */ new Set();
3551
+ for (const entry of entries) {
3552
+ if (!entry || typeof entry !== "object") continue;
3553
+ const pattern = typeof entry.pattern === "string" ? entry.pattern.trim() : "";
3554
+ if (!pattern) continue;
3555
+ const lowered = pattern.toLowerCase();
3556
+ if (seen.has(lowered)) continue;
3557
+ seen.add(lowered);
3558
+ out.push({
3559
+ pattern,
3560
+ lastUsedAt: typeof entry.lastUsedAt === "string" ? entry.lastUsedAt : void 0,
3561
+ lastUsedCommand: typeof entry.lastUsedCommand === "string" ? entry.lastUsedCommand : void 0,
3562
+ lastResolvedPath: typeof entry.lastResolvedPath === "string" ? entry.lastResolvedPath : void 0
3563
+ });
3564
+ }
3565
+ return out;
3566
+ }
3567
+ function normalizeFile(value, defaults) {
3568
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {
3569
+ version: DEFAULT_FILE_VERSION,
3570
+ defaults: {
3571
+ security: defaults.security,
3572
+ ask: defaults.ask
3573
+ },
3574
+ agents: {}
3575
+ };
3576
+ const raw = value;
3577
+ const security = typeof raw.defaults?.security === "string" ? normalizeExecSecurity(raw.defaults.security) : null;
3578
+ const ask = typeof raw.defaults?.ask === "string" ? normalizeExecAsk(raw.defaults.ask) : null;
3579
+ const agents = {};
3580
+ if (raw.agents && typeof raw.agents === "object" && !Array.isArray(raw.agents)) for (const [agentId, agentValue] of Object.entries(raw.agents)) {
3581
+ if (!agentId.trim()) continue;
3582
+ agents[agentId] = { allowlist: agentValue && typeof agentValue === "object" && !Array.isArray(agentValue) ? normalizeAllowlist(agentValue.allowlist) : [] };
3583
+ }
3584
+ return {
3585
+ version: DEFAULT_FILE_VERSION,
3586
+ defaults: {
3587
+ security: security ?? defaults.security,
3588
+ ask: ask ?? defaults.ask
3589
+ },
3590
+ agents
3591
+ };
3592
+ }
3593
+ function normalizePathForMatch(value) {
3594
+ if (process.platform === "win32") return value.replace(/\\/g, "/").toLowerCase();
3595
+ return value;
3596
+ }
3597
+ function resolveExecutablePath(executable, cwd, env) {
3598
+ if (!executable.trim()) return;
3599
+ if (executable.includes("/") || executable.includes("\\")) {
3600
+ const absolute = path.isAbsolute(executable) ? path.resolve(executable) : path.resolve(cwd, executable);
3601
+ if (fs.existsSync(absolute)) return absolute;
3602
+ return;
3603
+ }
3604
+ const pathEntries = (env.PATH || process.env.PATH || "").split(path.delimiter).filter(Boolean);
3605
+ for (const entry of pathEntries) {
3606
+ const candidate = path.join(entry, executable);
3607
+ if (fs.existsSync(candidate)) return candidate;
3608
+ }
3609
+ }
3610
+ function analyzeCommand(command, cwd, env) {
3611
+ const trimmed = command.trim();
3612
+ if (!trimmed) return {
3613
+ ok: false,
3614
+ reason: "empty command",
3615
+ args: []
3616
+ };
3617
+ if (SHELL_DISALLOWED_PATTERN.test(trimmed)) return {
3618
+ ok: false,
3619
+ reason: "disallowed shell syntax",
3620
+ args: []
3621
+ };
3622
+ if (SHELL_QUOTE_OR_ESCAPE_PATTERN.test(trimmed)) return {
3623
+ ok: false,
3624
+ reason: "quoted/escaped shell syntax is not allowed",
3625
+ args: []
3626
+ };
3627
+ const tokens = trimmed.split(/\s+/).filter(Boolean);
3628
+ if (tokens.length === 0) return {
3629
+ ok: false,
3630
+ reason: "empty command",
3631
+ args: []
3632
+ };
3633
+ const executableRaw = tokens[0];
3634
+ const resolvedPath = resolveExecutablePath(executableRaw, cwd, env);
3635
+ return {
3636
+ ok: true,
3637
+ executableRaw,
3638
+ executableName: resolvedPath ? path.basename(resolvedPath) : path.basename(executableRaw),
3639
+ resolvedPath,
3640
+ args: tokens.slice(1)
3641
+ };
3642
+ }
3643
+ function hasPathLikeArg(value) {
3644
+ if (!value || value === "-") return false;
3645
+ if (value.startsWith("/") || value.startsWith("./") || value.startsWith("../") || value.startsWith("~/") || /^[A-Za-z]:[\\/]/.test(value)) return true;
3646
+ return value.includes("/") || value.includes("\\");
3647
+ }
3648
+ function isSafeBinUsage(analysis, safeBins) {
3649
+ if (!analysis.ok || !analysis.executableName) return false;
3650
+ const executable = analysis.executableName.toLowerCase();
3651
+ if (!safeBins.has(executable)) return false;
3652
+ for (const arg of analysis.args) {
3653
+ if (arg.startsWith("-")) {
3654
+ const eqIndex = arg.indexOf("=");
3655
+ if (eqIndex > 0 && hasPathLikeArg(arg.slice(eqIndex + 1))) return false;
3656
+ continue;
3657
+ }
3658
+ if (hasPathLikeArg(arg)) return false;
3659
+ }
3660
+ return true;
3661
+ }
3662
+ function matchAllowlistPattern(pattern, executableName, resolvedPath) {
3663
+ const trimmed = pattern.trim();
3664
+ if (!trimmed) return false;
3665
+ const normalizedPattern = normalizePathForMatch(trimmed);
3666
+ const normalizedExecutable = executableName.toLowerCase();
3667
+ const normalizedResolved = resolvedPath ? normalizePathForMatch(resolvedPath) : "";
3668
+ if (!trimmed.includes("/") && !trimmed.includes("\\") && !trimmed.endsWith("*")) return normalizedPattern === normalizedExecutable;
3669
+ if (trimmed.endsWith("*")) {
3670
+ const prefix = normalizePathForMatch(trimmed.slice(0, -1));
3671
+ return normalizedResolved.length > 0 && normalizedResolved.startsWith(prefix) || normalizedExecutable.startsWith(prefix);
3672
+ }
3673
+ if (normalizedResolved.length > 0 && normalizedResolved === normalizedPattern) return true;
3674
+ return normalizedExecutable === normalizedPattern;
3675
+ }
3676
+ function makeCommandApprovalKey(agentId, command) {
3677
+ return crypto.createHash("sha256").update(`${agentId}::${command.trim()}`).digest("hex");
3678
+ }
3679
+ var ExecApprovalsManager = class {
3680
+ filePath;
3681
+ onRequested;
3682
+ onResolved;
3683
+ defaults;
3684
+ pending = /* @__PURE__ */ new Map();
3685
+ oneTimeApprovals = /* @__PURE__ */ new Map();
3686
+ constructor(options) {
3687
+ this.filePath = path.join(options.storeDir, "exec-approvals.json");
3688
+ this.defaults = options.defaults;
3689
+ this.onRequested = options.onRequested;
3690
+ this.onResolved = options.onResolved;
3691
+ }
3692
+ getConfigPath() {
3693
+ return this.filePath;
3694
+ }
3695
+ ensureDir() {
3696
+ fs.mkdirSync(path.dirname(this.filePath), {
3697
+ recursive: true,
3698
+ mode: 448
3699
+ });
3700
+ }
3701
+ readRaw() {
3702
+ if (!fs.existsSync(this.filePath)) return null;
3703
+ const raw = fs.readFileSync(this.filePath, "utf8");
3704
+ return JSON.parse(raw);
3705
+ }
3706
+ writeFile(value) {
3707
+ this.ensureDir();
3708
+ fs.writeFileSync(this.filePath, `${JSON.stringify(value, null, 2)}\n`, {
3709
+ encoding: "utf8",
3710
+ mode: 384
3711
+ });
3712
+ fs.chmodSync(this.filePath, 384);
3713
+ }
3714
+ getSnapshot() {
3715
+ try {
3716
+ return normalizeFile(this.readRaw(), this.defaults);
3717
+ } catch {
3718
+ return normalizeFile(null, this.defaults);
3719
+ }
3720
+ }
3721
+ setSnapshot(next) {
3722
+ const normalized = normalizeFile(next, this.defaults);
3723
+ this.writeFile(normalized);
3724
+ return normalized;
3725
+ }
3726
+ resolveDefaults(policy) {
3727
+ const snapshot = this.getSnapshot();
3728
+ return {
3729
+ security: snapshot.defaults.security ?? policy.security,
3730
+ ask: snapshot.defaults.ask ?? policy.ask
3731
+ };
3732
+ }
3733
+ getAgentAllowlist(agentId, policyAllowlist) {
3734
+ const byAgent = this.getSnapshot().agents[agentId]?.allowlist ?? [];
3735
+ const merged = [];
3736
+ const seen = /* @__PURE__ */ new Set();
3737
+ for (const entry of policyAllowlist) {
3738
+ const pattern = entry.trim();
3739
+ if (!pattern) continue;
3740
+ const key = pattern.toLowerCase();
3741
+ if (seen.has(key)) continue;
3742
+ seen.add(key);
3743
+ merged.push({ pattern });
3744
+ }
3745
+ for (const entry of byAgent) {
3746
+ const pattern = entry.pattern.trim();
3747
+ if (!pattern) continue;
3748
+ const key = pattern.toLowerCase();
3749
+ if (seen.has(key)) continue;
3750
+ seen.add(key);
3751
+ merged.push(entry);
3752
+ }
3753
+ return merged;
3754
+ }
3755
+ addAllowlistEntry(agentId, pattern) {
3756
+ const normalizedPattern = pattern.trim();
3757
+ if (!normalizedPattern) return;
3758
+ const snapshot = this.getSnapshot();
3759
+ const current = snapshot.agents[agentId]?.allowlist ?? [];
3760
+ if (current.some((entry) => entry.pattern.toLowerCase() === normalizedPattern.toLowerCase())) return;
3761
+ const nextAllowlist = [...current, {
3762
+ pattern: normalizedPattern,
3763
+ lastUsedAt: nowIso$1()
3764
+ }];
3765
+ snapshot.agents[agentId] = { allowlist: nextAllowlist };
3766
+ this.writeFile(snapshot);
3767
+ }
3768
+ recordAllowlistUse(agentId, pattern, command, resolvedPath) {
3769
+ const snapshot = this.getSnapshot();
3770
+ const current = snapshot.agents[agentId]?.allowlist ?? [];
3771
+ const lowered = pattern.toLowerCase();
3772
+ const nextAllowlist = current.map((entry) => {
3773
+ if (entry.pattern.toLowerCase() !== lowered) return entry;
3774
+ return {
3775
+ ...entry,
3776
+ lastUsedAt: nowIso$1(),
3777
+ lastUsedCommand: command,
3778
+ lastResolvedPath: resolvedPath
3779
+ };
3780
+ });
3781
+ snapshot.agents[agentId] = { allowlist: nextAllowlist };
3782
+ this.writeFile(snapshot);
3783
+ }
3784
+ request(params) {
3785
+ const now = Date.now();
3786
+ const timeoutMs = Math.max(5e3, Number.isFinite(params.timeoutMs) ? Math.floor(params.timeoutMs) : DEFAULT_APPROVAL_TIMEOUT_MS);
3787
+ const record = {
3788
+ id: crypto.randomUUID(),
3789
+ createdAtMs: now,
3790
+ expiresAtMs: now + timeoutMs,
3791
+ command: params.command,
3792
+ agentId: params.agentId,
3793
+ sessionKey: params.sessionKey,
3794
+ resolvedPath: params.resolvedPath,
3795
+ timeoutMs,
3796
+ target: params.target
3797
+ };
3798
+ this.pending.set(record.id, {
3799
+ record,
3800
+ onApproved: params.onApproved
3801
+ });
3802
+ this.onRequested?.(record);
3803
+ return record;
3804
+ }
3805
+ getPending(recordId) {
3806
+ const pending = this.pending.get(recordId);
3807
+ if (!pending) return null;
3808
+ if (pending.record.expiresAtMs <= Date.now()) {
3809
+ this.pending.delete(recordId);
3810
+ return null;
3811
+ }
3812
+ return pending.record;
3813
+ }
3814
+ resolve(recordId, decision, resolvedBy) {
3815
+ const pending = this.pending.get(recordId);
3816
+ if (!pending) return null;
3817
+ if (pending.record.expiresAtMs <= Date.now()) {
3818
+ this.pending.delete(recordId);
3819
+ return null;
3820
+ }
3821
+ this.pending.delete(recordId);
3822
+ const resolvedRecord = {
3823
+ ...pending.record,
3824
+ decision,
3825
+ resolvedBy,
3826
+ resolvedAtMs: Date.now()
3827
+ };
3828
+ if (decision === "allow-always" && resolvedRecord.resolvedPath) this.addAllowlistEntry(resolvedRecord.agentId, resolvedRecord.resolvedPath);
3829
+ if (decision === "allow-once") {
3830
+ const key = makeCommandApprovalKey(resolvedRecord.agentId, resolvedRecord.command);
3831
+ this.oneTimeApprovals.set(key, {
3832
+ key,
3833
+ expiresAtMs: Date.now() + ONE_TIME_APPROVAL_TTL_MS
3834
+ });
3835
+ }
3836
+ this.onResolved?.(resolvedRecord);
3837
+ if ((decision === "allow-once" || decision === "allow-always") && pending.onApproved) pending.onApproved(resolvedRecord).catch(() => {});
3838
+ return resolvedRecord;
3839
+ }
3840
+ consumeOneTimeApproval(agentId, command) {
3841
+ const key = makeCommandApprovalKey(agentId, command);
3842
+ const pending = this.oneTimeApprovals.get(key);
3843
+ if (!pending) return false;
3844
+ this.oneTimeApprovals.delete(key);
3845
+ if (pending.expiresAtMs <= Date.now()) return false;
3846
+ return true;
3847
+ }
3848
+ };
3849
+ function evaluateExecPolicy(params) {
3850
+ const cwd = params.cwd || process.cwd();
3851
+ const env = params.env || process.env;
3852
+ const analysis = analyzeCommand(params.command, cwd, env);
3853
+ const defaults = params.manager.resolveDefaults(params.policy);
3854
+ const security = defaults.security;
3855
+ const ask = defaults.ask;
3856
+ if (!analysis.ok) return {
3857
+ analysisOk: false,
3858
+ reason: analysis.reason,
3859
+ executableName: analysis.executableName,
3860
+ resolvedPath: analysis.resolvedPath,
3861
+ allowlistSatisfied: false,
3862
+ safeBinSatisfied: false,
3863
+ requiresApproval: false,
3864
+ denied: true,
3865
+ deniedReason: analysis.reason ? `exec denied: ${analysis.reason}` : "exec denied: invalid command"
3866
+ };
3867
+ if (security === "deny") return {
3868
+ analysisOk: analysis.ok,
3869
+ reason: analysis.reason,
3870
+ executableName: analysis.executableName,
3871
+ resolvedPath: analysis.resolvedPath,
3872
+ allowlistSatisfied: false,
3873
+ safeBinSatisfied: false,
3874
+ requiresApproval: false,
3875
+ denied: true,
3876
+ deniedReason: "exec denied: security=deny"
3877
+ };
3878
+ const mergedAllowlist = params.manager.getAgentAllowlist(params.agentId, params.policy.allowlist);
3879
+ const safeBins = new Set(params.policy.safeBins.map((entry) => entry.trim().toLowerCase()).filter(Boolean));
3880
+ const oneTimeApproved = params.manager.consumeOneTimeApproval(params.agentId, params.command);
3881
+ const safeBinSatisfied = isSafeBinUsage(analysis, safeBins);
3882
+ let allowlistSatisfied = false;
3883
+ let allowlistPattern;
3884
+ if (analysis.ok && analysis.executableName) if (oneTimeApproved) {
3885
+ allowlistSatisfied = true;
3886
+ allowlistPattern = "one-time-approval";
3887
+ } else {
3888
+ for (const entry of mergedAllowlist) if (matchAllowlistPattern(entry.pattern, analysis.executableName, analysis.resolvedPath)) {
3889
+ allowlistSatisfied = true;
3890
+ allowlistPattern = entry.pattern;
3891
+ break;
3892
+ }
3893
+ if (!allowlistSatisfied && safeBinSatisfied) {
3894
+ allowlistSatisfied = true;
3895
+ allowlistPattern = `safe-bin:${analysis.executableName.toLowerCase()}`;
3896
+ }
3897
+ }
3898
+ if (security === "full") {
3899
+ if (ask === "always") return {
3900
+ analysisOk: analysis.ok,
3901
+ reason: analysis.reason,
3902
+ executableName: analysis.executableName,
3903
+ resolvedPath: analysis.resolvedPath,
3904
+ allowlistSatisfied,
3905
+ allowlistPattern,
3906
+ safeBinSatisfied,
3907
+ requiresApproval: true,
3908
+ denied: false
3909
+ };
3910
+ return {
3911
+ analysisOk: analysis.ok,
3912
+ reason: analysis.reason,
3913
+ executableName: analysis.executableName,
3914
+ resolvedPath: analysis.resolvedPath,
3915
+ allowlistSatisfied,
3916
+ allowlistPattern,
3917
+ safeBinSatisfied,
3918
+ requiresApproval: false,
3919
+ denied: false
3920
+ };
3921
+ }
3922
+ if (allowlistSatisfied && ask !== "always") return {
3923
+ analysisOk: analysis.ok,
3924
+ reason: analysis.reason,
3925
+ executableName: analysis.executableName,
3926
+ resolvedPath: analysis.resolvedPath,
3927
+ allowlistSatisfied,
3928
+ allowlistPattern,
3929
+ safeBinSatisfied,
3930
+ requiresApproval: false,
3931
+ denied: false
3932
+ };
3933
+ if (ask === "off") return {
3934
+ analysisOk: analysis.ok,
3935
+ reason: analysis.reason,
3936
+ executableName: analysis.executableName,
3937
+ resolvedPath: analysis.resolvedPath,
3938
+ allowlistSatisfied,
3939
+ allowlistPattern,
3940
+ safeBinSatisfied,
3941
+ requiresApproval: false,
3942
+ denied: true,
3943
+ deniedReason: allowlistSatisfied ? "exec denied: ask=off" : "exec denied: allowlist miss"
3944
+ };
3945
+ return {
3946
+ analysisOk: analysis.ok,
3947
+ reason: analysis.reason,
3948
+ executableName: analysis.executableName,
3949
+ resolvedPath: analysis.resolvedPath,
3950
+ allowlistSatisfied,
3951
+ allowlistPattern,
3952
+ safeBinSatisfied,
3953
+ requiresApproval: true,
3954
+ denied: false
3955
+ };
3956
+ }
3957
+
3591
3958
  //#endregion
3592
3959
  //#region src/runtime/container-runtime.ts
3593
3960
  function startsWithAnyRoot$1(filePath, roots) {
@@ -4237,150 +4604,386 @@ function normalizeErrorMessage(error) {
4237
4604
  if (error instanceof Error) return error.message;
4238
4605
  return String(error);
4239
4606
  }
4240
- function createTools({ runtime, audit, bashEnabled }) {
4607
+ function resolveExecAgentId() {
4608
+ const context = getRuntimeSessionContext();
4609
+ if (context?.agentName?.trim()) return context.agentName.trim();
4610
+ if (context?.sessionKey?.trim()) {
4611
+ const first = context.sessionKey.split(":")[0];
4612
+ if (first?.trim()) return first.trim();
4613
+ }
4614
+ return "main";
4615
+ }
4616
+ function formatExecOutput(result) {
4617
+ return [
4618
+ `exitCode: ${result.exitCode}`,
4619
+ result.timedOut ? "timedOut: true" : "timedOut: false",
4620
+ result.truncated ? "truncated: true" : "truncated: false",
4621
+ "",
4622
+ result.stdout ? `stdout:\n${result.stdout}` : "stdout: <empty>",
4623
+ "",
4624
+ result.stderr ? `stderr:\n${result.stderr}` : "stderr: <empty>"
4625
+ ].join("\n");
4626
+ }
4627
+ function firstToken(command) {
4628
+ const trimmed = command.trim();
4629
+ if (!trimmed) return "";
4630
+ return trimmed.split(/\s+/)[0] ?? "";
4631
+ }
4632
+ function diagnosticsCommandsForPlatform(platform) {
4633
+ const common = [
4634
+ {
4635
+ key: "os",
4636
+ command: "uname -a"
4637
+ },
4638
+ {
4639
+ key: "uptime",
4640
+ command: "uptime"
4641
+ },
4642
+ {
4643
+ key: "disk",
4644
+ command: "df -h"
4645
+ },
4646
+ {
4647
+ key: "processes",
4648
+ command: "ps -A"
4649
+ }
4650
+ ];
4651
+ if (platform === "darwin") return [
4652
+ ...common,
4653
+ {
4654
+ key: "memory",
4655
+ command: "vm_stat"
4656
+ },
4657
+ {
4658
+ key: "cpu",
4659
+ command: "sysctl -n machdep.cpu.brand_string"
4660
+ },
4661
+ {
4662
+ key: "network",
4663
+ command: "ifconfig"
4664
+ }
4665
+ ];
4666
+ if (platform === "linux") return [
4667
+ ...common,
4668
+ {
4669
+ key: "memory",
4670
+ command: "free -m"
4671
+ },
4672
+ {
4673
+ key: "cpu",
4674
+ command: "lscpu"
4675
+ },
4676
+ {
4677
+ key: "network",
4678
+ command: "ip addr"
4679
+ }
4680
+ ];
4681
+ if (platform === "win32") return [
4682
+ {
4683
+ key: "os",
4684
+ command: "ver"
4685
+ },
4686
+ {
4687
+ key: "uptime",
4688
+ command: "net stats workstation"
4689
+ },
4690
+ {
4691
+ key: "disk",
4692
+ command: "wmic logicaldisk get size,freespace,caption"
4693
+ },
4694
+ {
4695
+ key: "processes",
4696
+ command: "tasklist"
4697
+ },
4698
+ {
4699
+ key: "cpu",
4700
+ command: "wmic cpu get name"
4701
+ },
4702
+ {
4703
+ key: "memory",
4704
+ command: "wmic os get totalvisiblememorysize,freephysicalmemory"
4705
+ },
4706
+ {
4707
+ key: "network",
4708
+ command: "ipconfig"
4709
+ }
4710
+ ];
4711
+ return common;
4712
+ }
4713
+ function createTools({ runtime, audit, execPolicy, execApprovals }) {
4241
4714
  const parser = new Parser();
4242
- const bashTool = {
4243
- name: "bash",
4244
- label: "Bash",
4245
- description: "Run a shell command on allowed command prefixes only.",
4246
- parameters: Type.Object({
4247
- command: Type.String(),
4248
- timeoutMs: Type.Optional(Type.Number({
4249
- minimum: 1e3,
4250
- maximum: 12e4
4251
- }))
4252
- }),
4253
- execute: async (_toolCallId, params) => {
4254
- audit({
4255
- actor: "tool",
4256
- eventType: "tool.exec",
4257
- payload: { command: params.command }
4715
+ const execSchema = Type.Object({
4716
+ command: Type.String(),
4717
+ timeoutMs: Type.Optional(Type.Number({
4718
+ minimum: 1e3,
4719
+ maximum: 12e4
4720
+ }))
4721
+ });
4722
+ async function executeCommandWithPolicy(toolName, params) {
4723
+ const command = params.command.trim();
4724
+ if (!command) throw new Error("Command cannot be empty.");
4725
+ const sessionContext = getRuntimeSessionContext();
4726
+ const agentId = resolveExecAgentId();
4727
+ const evaluation = evaluateExecPolicy({
4728
+ command,
4729
+ agentId,
4730
+ policy: execPolicy,
4731
+ manager: execApprovals,
4732
+ cwd: sessionContext?.workspaceDir,
4733
+ env: process.env
4734
+ });
4735
+ audit({
4736
+ sessionKey: sessionContext?.sessionKey,
4737
+ actor: "tool",
4738
+ eventType: "tool.exec",
4739
+ payload: {
4740
+ toolName,
4741
+ command,
4742
+ security: execPolicy.security,
4743
+ ask: execPolicy.ask,
4744
+ allowlistSatisfied: evaluation.allowlistSatisfied,
4745
+ safeBinSatisfied: evaluation.safeBinSatisfied,
4746
+ analysisOk: evaluation.analysisOk,
4747
+ requiresApproval: evaluation.requiresApproval
4748
+ }
4749
+ });
4750
+ if (evaluation.denied) throw new Error(evaluation.deniedReason || "exec denied by policy");
4751
+ if (evaluation.requiresApproval) {
4752
+ const approval = execApprovals.request({
4753
+ command,
4754
+ agentId,
4755
+ sessionKey: sessionContext?.sessionKey,
4756
+ resolvedPath: evaluation.resolvedPath,
4757
+ timeoutMs: execPolicy.approvalTimeoutMs
4258
4758
  });
4259
- const result = await runtime.exec(params.command, { timeoutMs: params.timeoutMs });
4260
4759
  return textResult([
4261
- `exitCode: ${result.exitCode}`,
4262
- result.timedOut ? "timedOut: true" : "timedOut: false",
4263
- result.truncated ? "truncated: true" : "truncated: false",
4760
+ `approvalRequired: true`,
4761
+ `approvalId: ${approval.id}`,
4762
+ `expiresAtMs: ${approval.expiresAtMs}`,
4264
4763
  "",
4265
- result.stdout ? `stdout:\n${result.stdout}` : "stdout: <empty>",
4266
- "",
4267
- result.stderr ? `stderr:\n${result.stderr}` : "stderr: <empty>"
4268
- ].join("\n"), result);
4269
- }
4270
- };
4271
- const readFileTool = {
4272
- name: "read_file",
4273
- label: "Read File",
4274
- description: "Read a text file from an allowlisted path.",
4275
- parameters: Type.Object({
4276
- path: Type.String(),
4277
- maxBytes: Type.Optional(Type.Number({
4278
- minimum: 128,
4279
- maximum: 1e6
4280
- }))
4281
- }),
4282
- execute: async (_toolCallId, params) => {
4283
- audit({
4284
- actor: "tool",
4285
- eventType: "tool.read_file",
4286
- payload: { path: params.path }
4764
+ "Run /approve <id> allow-once|allow-always|deny, then retry the command."
4765
+ ].join("\n"), {
4766
+ status: "approval-pending",
4767
+ approvalId: approval.id,
4768
+ expiresAtMs: approval.expiresAtMs,
4769
+ command,
4770
+ resolvedPath: evaluation.resolvedPath,
4771
+ analysisOk: evaluation.analysisOk
4287
4772
  });
4288
- const result = await runtime.readFile(params.path, { maxOutputBytes: params.maxBytes });
4289
- return textResult(result.content, result);
4290
4773
  }
4291
- };
4292
- const writeFileTool = {
4293
- name: "write_file",
4294
- label: "Write File",
4295
- description: "Write UTF-8 text content to an allowlisted path.",
4296
- parameters: Type.Object({
4297
- path: Type.String(),
4298
- content: Type.String()
4299
- }),
4774
+ const result = await runtime.exec(command, { timeoutMs: params.timeoutMs });
4775
+ if (evaluation.allowlistPattern && !evaluation.allowlistPattern.startsWith("safe-bin:") && evaluation.allowlistPattern !== "one-time-approval") execApprovals.recordAllowlistUse(agentId, evaluation.allowlistPattern, command, evaluation.resolvedPath);
4776
+ return textResult(formatExecOutput(result), {
4777
+ ...result,
4778
+ command,
4779
+ resolvedPath: evaluation.resolvedPath
4780
+ });
4781
+ }
4782
+ const execTool = {
4783
+ name: "exec",
4784
+ label: "Exec",
4785
+ description: "Run a shell command with approval and allowlist policy.",
4786
+ parameters: execSchema,
4300
4787
  execute: async (_toolCallId, params) => {
4301
- audit({
4302
- actor: "tool",
4303
- eventType: "tool.write_file",
4304
- payload: {
4305
- path: params.path,
4306
- bytes: Buffer.byteLength(params.content, "utf8")
4307
- }
4308
- });
4309
- const result = await runtime.writeFile(params.path, params.content);
4310
- return textResult(`Wrote ${result.bytesWritten} bytes to ${params.path}.`, result);
4788
+ return executeCommandWithPolicy("exec", params);
4311
4789
  }
4312
4790
  };
4313
- const webSearchTool = {
4314
- name: "web_search",
4315
- label: "Web Search",
4316
- description: "Fetch a URL and return readable article text.",
4317
- parameters: Type.Object({ url: Type.String({ format: "uri" }) }),
4791
+ const bashTool = {
4792
+ name: "bash",
4793
+ label: "Bash",
4794
+ description: "Compatibility alias for exec tool policy.",
4795
+ parameters: execSchema,
4318
4796
  execute: async (_toolCallId, params) => {
4319
- audit({
4320
- actor: "tool",
4321
- eventType: "tool.fetch_web",
4322
- payload: { url: params.url }
4323
- });
4324
- const result = await runtime.fetchWeb(params.url);
4325
- return textResult(`${result.title ? `# ${result.title}\n\n` : ""}${result.markdown}`.trim(), result);
4797
+ return executeCommandWithPolicy("bash", params);
4326
4798
  }
4327
4799
  };
4328
- const fetchPodcastFeedTool = {
4329
- name: "fetch_podcast_feed",
4330
- label: "Fetch Podcast Feed",
4331
- description: "Fetch one or more RSS podcast feeds and return recent episodes.",
4800
+ const diagnoseDeviceTool = {
4801
+ name: "diagnose_device",
4802
+ label: "Diagnose Device",
4803
+ description: "Run read-only core diagnostics for OS, CPU, memory, disk, network, and process status.",
4332
4804
  parameters: Type.Object({
4333
- urls: Type.Array(Type.String({ format: "uri" }), {
4334
- minItems: 1,
4335
- maxItems: 20
4336
- }),
4337
- limitPerFeed: Type.Optional(Type.Number({
4338
- minimum: 1,
4339
- maximum: 50
4805
+ profile: Type.Optional(Type.String({ default: "core" })),
4806
+ timeoutMs: Type.Optional(Type.Number({
4807
+ minimum: 1e3,
4808
+ maximum: 12e4
4340
4809
  }))
4341
4810
  }),
4342
4811
  execute: async (_toolCallId, params) => {
4343
- const limit = params.limitPerFeed ?? 5;
4344
- const output = [];
4345
- for (const url of params.urls) try {
4346
- const feed = await parser.parseURL(url);
4347
- const episodes = (feed.items ?? []).slice(0, limit).map((item) => ({
4348
- title: item.title ?? "Untitled episode",
4349
- publishedAt: item.pubDate || item.isoDate || null,
4350
- link: item.link ?? "",
4351
- summary: (item.contentSnippet || item.content || item.summary || "").replace(/\s+/g, " ").trim().slice(0, 400) || "No summary available."
4352
- }));
4353
- output.push({
4354
- url,
4355
- title: feed.title ?? "Untitled feed",
4356
- episodes
4812
+ const profile = (params.profile || "core").trim().toLowerCase();
4813
+ if (profile !== "core") throw new Error(`Unsupported diagnostic profile: ${profile}`);
4814
+ const timeoutMs = params.timeoutMs ?? 1e4;
4815
+ const commands = diagnosticsCommandsForPlatform(process.platform);
4816
+ const sections = [];
4817
+ for (const entry of commands) try {
4818
+ const result = await runtime.exec(entry.command, {
4819
+ timeoutMs,
4820
+ allowedCommandPrefixes: [firstToken(entry.command)]
4821
+ });
4822
+ sections.push({
4823
+ key: entry.key,
4824
+ command: entry.command,
4825
+ ok: result.exitCode === 0 && !result.timedOut,
4826
+ exitCode: result.exitCode,
4827
+ stdout: result.stdout,
4828
+ stderr: result.stderr,
4829
+ timedOut: result.timedOut,
4830
+ truncated: result.truncated
4357
4831
  });
4358
4832
  } catch (error) {
4359
- output.push({
4360
- url,
4361
- title: "Unknown feed",
4362
- episodes: [],
4363
- error: normalizeErrorMessage(error)
4833
+ sections.push({
4834
+ key: entry.key,
4835
+ command: entry.command,
4836
+ ok: false,
4837
+ exitCode: 1,
4838
+ stdout: "",
4839
+ stderr: normalizeErrorMessage(error),
4840
+ timedOut: false,
4841
+ truncated: false
4364
4842
  });
4365
4843
  }
4366
4844
  audit({
4367
4845
  actor: "tool",
4368
- eventType: "tool.fetch_podcast_feed",
4846
+ eventType: "tool.diagnose_device",
4369
4847
  payload: {
4370
- urls: params.urls,
4371
- limitPerFeed: limit
4848
+ profile,
4849
+ commandCount: sections.length,
4850
+ failed: sections.filter((entry) => !entry.ok).length
4372
4851
  }
4373
4852
  });
4374
- return textResult(output.map((feed) => {
4375
- if (feed.error) return `Feed: ${feed.url}\nError: ${feed.error}`;
4376
- const episodesText = feed.episodes.map((episode, index) => `${index + 1}. ${episode.title}${episode.publishedAt ? ` (${episode.publishedAt})` : ""}\n${episode.link}\n${episode.summary}`).join("\n\n");
4377
- return `Feed: ${feed.title}\nSource: ${feed.url}\n\n${episodesText}`;
4378
- }).join("\n\n---\n\n"), output);
4853
+ const summaryLines = [
4854
+ `profile: ${profile}`,
4855
+ `platform: ${process.platform}`,
4856
+ `sections: ${sections.length}`,
4857
+ `failed: ${sections.filter((entry) => !entry.ok).length}`
4858
+ ];
4859
+ for (const section of sections) {
4860
+ summaryLines.push("");
4861
+ summaryLines.push(`[${section.key}] ${section.command}`);
4862
+ summaryLines.push(`ok=${section.ok} exitCode=${section.exitCode} timedOut=${section.timedOut}`);
4863
+ if (section.stdout) summaryLines.push(`stdout:\n${section.stdout}`);
4864
+ if (section.stderr) summaryLines.push(`stderr:\n${section.stderr}`);
4865
+ }
4866
+ return textResult(summaryLines.join("\n"), {
4867
+ profile,
4868
+ platform: process.platform,
4869
+ sections
4870
+ });
4379
4871
  }
4380
4872
  };
4381
- const tools = [];
4382
- if (bashEnabled) tools.push(bashTool);
4383
- tools.push(readFileTool, writeFileTool, webSearchTool, fetchPodcastFeedTool);
4873
+ const tools = [
4874
+ {
4875
+ name: "read_file",
4876
+ label: "Read File",
4877
+ description: "Read a text file from an allowlisted path.",
4878
+ parameters: Type.Object({
4879
+ path: Type.String(),
4880
+ maxBytes: Type.Optional(Type.Number({
4881
+ minimum: 128,
4882
+ maximum: 1e6
4883
+ }))
4884
+ }),
4885
+ execute: async (_toolCallId, params) => {
4886
+ audit({
4887
+ actor: "tool",
4888
+ eventType: "tool.read_file",
4889
+ payload: { path: params.path }
4890
+ });
4891
+ const result = await runtime.readFile(params.path, { maxOutputBytes: params.maxBytes });
4892
+ return textResult(result.content, result);
4893
+ }
4894
+ },
4895
+ {
4896
+ name: "write_file",
4897
+ label: "Write File",
4898
+ description: "Write UTF-8 text content to an allowlisted path.",
4899
+ parameters: Type.Object({
4900
+ path: Type.String(),
4901
+ content: Type.String()
4902
+ }),
4903
+ execute: async (_toolCallId, params) => {
4904
+ audit({
4905
+ actor: "tool",
4906
+ eventType: "tool.write_file",
4907
+ payload: {
4908
+ path: params.path,
4909
+ bytes: Buffer.byteLength(params.content, "utf8")
4910
+ }
4911
+ });
4912
+ const result = await runtime.writeFile(params.path, params.content);
4913
+ return textResult(`Wrote ${result.bytesWritten} bytes to ${params.path}.`, result);
4914
+ }
4915
+ },
4916
+ {
4917
+ name: "web_search",
4918
+ label: "Web Search",
4919
+ description: "Fetch a URL and return readable article text.",
4920
+ parameters: Type.Object({ url: Type.String({ format: "uri" }) }),
4921
+ execute: async (_toolCallId, params) => {
4922
+ audit({
4923
+ actor: "tool",
4924
+ eventType: "tool.fetch_web",
4925
+ payload: { url: params.url }
4926
+ });
4927
+ const result = await runtime.fetchWeb(params.url);
4928
+ return textResult(`${result.title ? `# ${result.title}\n\n` : ""}${result.markdown}`.trim(), result);
4929
+ }
4930
+ },
4931
+ {
4932
+ name: "fetch_podcast_feed",
4933
+ label: "Fetch Podcast Feed",
4934
+ description: "Fetch one or more RSS podcast feeds and return recent episodes.",
4935
+ parameters: Type.Object({
4936
+ urls: Type.Array(Type.String({ format: "uri" }), {
4937
+ minItems: 1,
4938
+ maxItems: 20
4939
+ }),
4940
+ limitPerFeed: Type.Optional(Type.Number({
4941
+ minimum: 1,
4942
+ maximum: 50
4943
+ }))
4944
+ }),
4945
+ execute: async (_toolCallId, params) => {
4946
+ const limit = params.limitPerFeed ?? 5;
4947
+ const output = [];
4948
+ for (const url of params.urls) try {
4949
+ const feed = await parser.parseURL(url);
4950
+ const episodes = (feed.items ?? []).slice(0, limit).map((item) => ({
4951
+ title: item.title ?? "Untitled episode",
4952
+ publishedAt: item.pubDate || item.isoDate || null,
4953
+ link: item.link ?? "",
4954
+ summary: (item.contentSnippet || item.content || item.summary || "").replace(/\s+/g, " ").trim().slice(0, 400) || "No summary available."
4955
+ }));
4956
+ output.push({
4957
+ url,
4958
+ title: feed.title ?? "Untitled feed",
4959
+ episodes
4960
+ });
4961
+ } catch (error) {
4962
+ output.push({
4963
+ url,
4964
+ title: "Unknown feed",
4965
+ episodes: [],
4966
+ error: normalizeErrorMessage(error)
4967
+ });
4968
+ }
4969
+ audit({
4970
+ actor: "tool",
4971
+ eventType: "tool.fetch_podcast_feed",
4972
+ payload: {
4973
+ urls: params.urls,
4974
+ limitPerFeed: limit
4975
+ }
4976
+ });
4977
+ return textResult(output.map((feed) => {
4978
+ if (feed.error) return `Feed: ${feed.url}\nError: ${feed.error}`;
4979
+ const episodesText = feed.episodes.map((episode, index) => `${index + 1}. ${episode.title}${episode.publishedAt ? ` (${episode.publishedAt})` : ""}\n${episode.link}\n${episode.summary}`).join("\n\n");
4980
+ return `Feed: ${feed.title}\nSource: ${feed.url}\n\n${episodesText}`;
4981
+ }).join("\n\n---\n\n"), output);
4982
+ }
4983
+ },
4984
+ diagnoseDeviceTool
4985
+ ];
4986
+ if (execPolicy.enabled) tools.unshift(execTool, bashTool);
4384
4987
  return tools;
4385
4988
  }
4386
4989
 
@@ -4455,7 +5058,6 @@ function readFileConfigForCli(env = process.env) {
4455
5058
  }
4456
5059
  function writeFileConfigForCli(next, env = process.env) {
4457
5060
  saveConfigFile(next, env);
4458
- writeOpenClawMirror(loadConfig(env));
4459
5061
  }
4460
5062
 
4461
5063
  //#endregion
@@ -5631,7 +6233,7 @@ function registerGatewayCommands(program, deps = defaultGatewayCommandDeps) {
5631
6233
  gateway.command("run").description("Run HOVClaw daemon with gateway in foreground").action(async () => {
5632
6234
  if (!config.gateway.enabled) throw new Error("Gateway is disabled in config. Enable gateway.enabled first.");
5633
6235
  process.stdout.write(`Starting HOVClaw gateway on ${config.gateway.host}:${config.gateway.port}...\n`);
5634
- await import("./src-Y6AqidKn.js");
6236
+ await import("./src-GZDRRc5A.js");
5635
6237
  });
5636
6238
  gateway.command("open-ui").description("Open the minimal gateway web UI in your default browser").option("--no-open", "Print URL but do not open browser").option("--json", "Print JSON output").action(async (options) => {
5637
6239
  const url = resolveGatewayWebUiUrl();
@@ -5929,13 +6531,13 @@ function loadCliVersion() {
5929
6531
  }
5930
6532
  function registerCoreCommands(program) {
5931
6533
  program.command("onboard").description("Run interactive onboarding wizard").action(async () => {
5932
- await (await import("./onboard-DL6VDf50.js")).main();
6534
+ await (await import("./onboard-Cc2XHLT4.js")).main();
5933
6535
  });
5934
6536
  program.command("login [provider]").description("Run OAuth login for a provider").action(async (provider) => {
5935
- await (await import("./login-BwvBMKdz.js")).main(provider ? [provider] : []);
6537
+ await (await import("./login-BtLE2Bye.js")).main(provider ? [provider] : []);
5936
6538
  });
5937
6539
  program.command("doctor").description("Run installation and config health checks").option("--fix", "Attempt auto-repair").option("--repair", "Attempt auto-repair").option("--deep", "Run deep checks").option("--json", "Print JSON output").action(async (options) => {
5938
- const module = await import("./doctor-D52M80De.js");
6540
+ const module = await import("./doctor-0iphhiTj.js");
5939
6541
  const args = [];
5940
6542
  if (options.fix || options.repair) args.push("--fix");
5941
6543
  if (options.deep) args.push("--deep");
@@ -5943,7 +6545,7 @@ function registerCoreCommands(program) {
5943
6545
  await module.main(args);
5944
6546
  });
5945
6547
  program.command("reset").description("Reset local config/state while keeping HovClaw installed").option("--scope <scope>", "config|config+creds+sessions|full").option("--yes", "Skip confirmation prompts").option("--non-interactive", "Disable prompts (requires --scope + --yes)").option("--dry-run", "Print reset plan without deleting files").option("--json", "Print JSON output").action(async (options) => {
5946
- const module = await import("./reset-BJUhrojJ.js");
6548
+ const module = await import("./reset-ChNzCD2s.js");
5947
6549
  try {
5948
6550
  const result = await module.runResetCommand({
5949
6551
  scope: options.scope,
@@ -6051,7 +6653,6 @@ async function main() {
6051
6653
  registerPairingCommands(program);
6052
6654
  registerGatewayCommands(program);
6053
6655
  registerSkillsCommands(program);
6054
- registerCompatCommands(program);
6055
6656
  await program.parseAsync(process.argv);
6056
6657
  }
6057
6658
  main().catch((error) => {
@@ -6060,4 +6661,4 @@ main().catch((error) => {
6060
6661
  });
6061
6662
 
6062
6663
  //#endregion
6063
- export { ensureConfigFromLegacyEnv as A, resolveTelegramAccountConfig as B, resolveModelAlias as C, parseGatewayFrame as D, parseConnectParams as E, hasConfigFile as F, saveCredentials as H, hasCredentialsFile as I, loadConfig as L, getCredentialsPath as M, getDefaultFileConfig as N, config as O, getHovclawHome as P, loadCredentials as R, parseModelRef as S, PROTOCOL_VERSION as T, writeOpenClawMirror as U, saveConfigFile as V, extractAssistantText as _, LocalHostRuntime as a, loadSkill as b, redactSensitiveData as c, PiAgentManager as d, composeSessionKey as f, extractAssistantError as g, resolveAgentWorkspaceDir as h, createTools as i, getConfigPath as j, detectLegacyEnvConfig as k, TelegramChannel as l, ensureWorkspaceBootstrapForConfig as m, stopDaemon as n, ContainerRuntime as o, WORKSPACE_CONTEXT_FILE_ORDER as p, TelegramPairingStore as r, HovClawDb as s, requestDaemonRestartFromCurrentProcess as t, DiscordChannel as u, toUserFacingAssistantError as v, logger as w, listConfiguredModelRefs as x, listAvailableSkills as y, loadFileConfig as z };
6664
+ export { config as A, loadCredentials as B, listConfiguredModelRefs as C, PROTOCOL_VERSION as D, logger as E, getDefaultFileConfig as F, resolveTelegramAccountConfig as H, getHovclawHome as I, hasConfigFile as L, ensureConfigFromLegacyEnv as M, getConfigPath as N, parseConnectParams as O, getCredentialsPath as P, hasCredentialsFile as R, loadSkill as S, resolveModelAlias as T, saveConfigFile as U, loadFileConfig as V, saveCredentials as W, resolveAgentWorkspaceDir as _, LocalHostRuntime as a, toUserFacingAssistantError as b, evaluateExecPolicy as c, TelegramChannel as d, DiscordChannel as f, ensureWorkspaceBootstrapForConfig as g, WORKSPACE_CONTEXT_FILE_ORDER as h, createTools as i, detectLegacyEnvConfig as j, parseGatewayFrame as k, HovClawDb as l, composeSessionKey as m, stopDaemon as n, ContainerRuntime as o, PiAgentManager as p, TelegramPairingStore as r, ExecApprovalsManager as s, requestDaemonRestartFromCurrentProcess as t, redactSensitiveData as u, extractAssistantError as v, parseModelRef as w, listAvailableSkills as x, extractAssistantText as y, loadConfig as z };