hovclaw 0.1.3 → 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
@@ -116,7 +116,27 @@ const DEFAULT_FILE_CONFIG = {
116
116
  "tail",
117
117
  "wc"
118
118
  ],
119
- 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
+ }
120
140
  },
121
141
  channels: {
122
142
  discord: {
@@ -221,6 +241,22 @@ const commandsConfigSchema = z.object({
221
241
  useAccessGroups: z.boolean(),
222
242
  allowFrom: commandAllowFromSchema
223
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
+ });
224
260
  const telegramTopicConfigSchema = z.object({
225
261
  enabled: z.boolean().optional(),
226
262
  requireMention: z.boolean().optional(),
@@ -383,7 +419,10 @@ const fileConfigSchema = z.object({
383
419
  allowedReadRoots: z.array(z.string().min(1)),
384
420
  allowedWriteRoots: z.array(z.string().min(1)),
385
421
  allowedCommandPrefixes: z.array(z.string().min(1)).min(1),
386
- tools: z.object({ bashEnabled: z.boolean() })
422
+ tools: z.object({
423
+ bashEnabled: z.boolean(),
424
+ exec: runtimeExecConfigSchema
425
+ })
387
426
  }),
388
427
  channels: z.object({
389
428
  discord: z.object({
@@ -460,7 +499,21 @@ const partialFileConfigSchema = z.object({
460
499
  bindings: fileConfigSchema.shape.bindings.optional(),
461
500
  models: fileConfigSchema.shape.models.partial().optional(),
462
501
  commands: commandsConfigSchema.partial().optional(),
463
- 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(),
464
517
  channels: partialChannelsSchema,
465
518
  gateway: partialGatewayConfigSchema,
466
519
  scheduler: fileConfigSchema.shape.scheduler.partial().optional()
@@ -731,7 +784,17 @@ function mergeWithDefaults(partial) {
731
784
  allowedReadRoots: partial.runtime?.allowedReadRoots ?? DEFAULT_FILE_CONFIG.runtime.allowedReadRoots,
732
785
  allowedWriteRoots: partial.runtime?.allowedWriteRoots ?? DEFAULT_FILE_CONFIG.runtime.allowedWriteRoots,
733
786
  allowedCommandPrefixes: partial.runtime?.allowedCommandPrefixes ?? DEFAULT_FILE_CONFIG.runtime.allowedCommandPrefixes,
734
- 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
+ }
735
798
  },
736
799
  channels: {
737
800
  discord: {
@@ -843,7 +906,17 @@ function applyEnvOverrides(base, env) {
843
906
  allowedReadRoots: splitCsv(env.ALLOWED_READ_ROOTS, base.runtime.allowedReadRoots),
844
907
  allowedWriteRoots: splitCsv(env.ALLOWED_WRITE_ROOTS, base.runtime.allowedWriteRoots),
845
908
  allowedCommandPrefixes: splitCsv(env.ALLOWED_COMMAND_PREFIXES, base.runtime.allowedCommandPrefixes),
846
- 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
+ }
847
920
  },
848
921
  channels: {
849
922
  discord: {
@@ -948,7 +1021,17 @@ function loadConfig(env = process.env) {
948
1021
  allowedReadRoots: normalizeRoots(merged.runtime.allowedReadRoots),
949
1022
  allowedWriteRoots: normalizeRoots(merged.runtime.allowedWriteRoots),
950
1023
  allowedCommandPrefixes: merged.runtime.allowedCommandPrefixes,
951
- 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
+ }
952
1035
  },
953
1036
  channels: {
954
1037
  discord: {
@@ -1046,6 +1129,12 @@ function legacyEnvKeys() {
1046
1129
  "ALLOWED_WRITE_ROOTS",
1047
1130
  "ALLOWED_COMMAND_PREFIXES",
1048
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",
1049
1138
  "TOOL_TIMEOUT_MS",
1050
1139
  "TOOL_MAX_OUTPUT_BYTES",
1051
1140
  "ENABLE_DISCORD",
@@ -3002,7 +3091,7 @@ function redactSensitiveData(value) {
3002
3091
 
3003
3092
  //#endregion
3004
3093
  //#region src/db.ts
3005
- function nowIso$1() {
3094
+ function nowIso$2() {
3006
3095
  return (/* @__PURE__ */ new Date()).toISOString();
3007
3096
  }
3008
3097
  var HovClawDb = class {
@@ -3130,7 +3219,7 @@ var HovClawDb = class {
3130
3219
  return Boolean(row?.name);
3131
3220
  }
3132
3221
  upsertSession(parts, sessionKey, model, options) {
3133
- const createdAt = nowIso$1();
3222
+ const createdAt = nowIso$2();
3134
3223
  this.db.prepare(`
3135
3224
  INSERT INTO sessions (
3136
3225
  session_key,
@@ -3225,7 +3314,7 @@ var HovClawDb = class {
3225
3314
  ON CONFLICT(account_id) DO UPDATE SET
3226
3315
  last_update_id = excluded.last_update_id,
3227
3316
  updated_at = excluded.updated_at
3228
- `).run(accountId, updateId, nowIso$1());
3317
+ `).run(accountId, updateId, nowIso$2());
3229
3318
  }
3230
3319
  hasTelegramDedupe(accountId, updateId) {
3231
3320
  return typeof this.db.prepare(`
@@ -3238,7 +3327,7 @@ var HovClawDb = class {
3238
3327
  this.db.prepare(`
3239
3328
  INSERT OR IGNORE INTO telegram_dedupe (account_id, update_id, updated_at)
3240
3329
  VALUES (?, ?, ?)
3241
- `).run(accountId, updateId, nowIso$1());
3330
+ `).run(accountId, updateId, nowIso$2());
3242
3331
  }
3243
3332
  pruneTelegramDedupe(olderThanIso) {
3244
3333
  return this.db.prepare(`
@@ -3247,7 +3336,7 @@ var HovClawDb = class {
3247
3336
  `).run(olderThanIso).changes;
3248
3337
  }
3249
3338
  appendMessage(sessionKey, role, content) {
3250
- 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());
3251
3340
  }
3252
3341
  getMessages(sessionKey) {
3253
3342
  return this.db.prepare(`
@@ -3268,7 +3357,7 @@ var HovClawDb = class {
3268
3357
  ON CONFLICT(session_key) DO UPDATE SET
3269
3358
  state_json = excluded.state_json,
3270
3359
  updated_at = excluded.updated_at
3271
- `).run(sessionKey, stateJson, nowIso$1());
3360
+ `).run(sessionKey, stateJson, nowIso$2());
3272
3361
  }
3273
3362
  getAgentState(sessionKey) {
3274
3363
  return this.db.prepare(`SELECT state_json FROM agent_state WHERE session_key = ?`).get(sessionKey)?.state_json ?? null;
@@ -3289,7 +3378,7 @@ var HovClawDb = class {
3289
3378
  cost_usd,
3290
3379
  created_at
3291
3380
  ) VALUES (?, ?, ?, ?, ?, ?, ?)
3292
- `).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());
3293
3382
  }
3294
3383
  upsertScheduledJob(job) {
3295
3384
  this.db.prepare(`
@@ -3423,7 +3512,7 @@ var HovClawDb = class {
3423
3512
  this.db.prepare(`
3424
3513
  INSERT INTO audit_log (ts, session_key, actor, event_type, payload_json)
3425
3514
  VALUES (?, ?, ?, ?, ?)
3426
- `).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));
3427
3516
  }
3428
3517
  getAuditEvents(eventType) {
3429
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`;
@@ -3437,6 +3526,435 @@ var HovClawDb = class {
3437
3526
  }
3438
3527
  };
3439
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
+
3440
3958
  //#endregion
3441
3959
  //#region src/runtime/container-runtime.ts
3442
3960
  function startsWithAnyRoot$1(filePath, roots) {
@@ -4086,150 +4604,386 @@ function normalizeErrorMessage(error) {
4086
4604
  if (error instanceof Error) return error.message;
4087
4605
  return String(error);
4088
4606
  }
4089
- 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 }) {
4090
4714
  const parser = new Parser();
4091
- const bashTool = {
4092
- name: "bash",
4093
- label: "Bash",
4094
- description: "Run a shell command on allowed command prefixes only.",
4095
- parameters: Type.Object({
4096
- command: Type.String(),
4097
- timeoutMs: Type.Optional(Type.Number({
4098
- minimum: 1e3,
4099
- maximum: 12e4
4100
- }))
4101
- }),
4102
- execute: async (_toolCallId, params) => {
4103
- audit({
4104
- actor: "tool",
4105
- eventType: "tool.exec",
4106
- 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
4107
4758
  });
4108
- const result = await runtime.exec(params.command, { timeoutMs: params.timeoutMs });
4109
4759
  return textResult([
4110
- `exitCode: ${result.exitCode}`,
4111
- result.timedOut ? "timedOut: true" : "timedOut: false",
4112
- result.truncated ? "truncated: true" : "truncated: false",
4760
+ `approvalRequired: true`,
4761
+ `approvalId: ${approval.id}`,
4762
+ `expiresAtMs: ${approval.expiresAtMs}`,
4113
4763
  "",
4114
- result.stdout ? `stdout:\n${result.stdout}` : "stdout: <empty>",
4115
- "",
4116
- result.stderr ? `stderr:\n${result.stderr}` : "stderr: <empty>"
4117
- ].join("\n"), result);
4118
- }
4119
- };
4120
- const readFileTool = {
4121
- name: "read_file",
4122
- label: "Read File",
4123
- description: "Read a text file from an allowlisted path.",
4124
- parameters: Type.Object({
4125
- path: Type.String(),
4126
- maxBytes: Type.Optional(Type.Number({
4127
- minimum: 128,
4128
- maximum: 1e6
4129
- }))
4130
- }),
4131
- execute: async (_toolCallId, params) => {
4132
- audit({
4133
- actor: "tool",
4134
- eventType: "tool.read_file",
4135
- 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
4136
4772
  });
4137
- const result = await runtime.readFile(params.path, { maxOutputBytes: params.maxBytes });
4138
- return textResult(result.content, result);
4139
4773
  }
4140
- };
4141
- const writeFileTool = {
4142
- name: "write_file",
4143
- label: "Write File",
4144
- description: "Write UTF-8 text content to an allowlisted path.",
4145
- parameters: Type.Object({
4146
- path: Type.String(),
4147
- content: Type.String()
4148
- }),
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,
4149
4787
  execute: async (_toolCallId, params) => {
4150
- audit({
4151
- actor: "tool",
4152
- eventType: "tool.write_file",
4153
- payload: {
4154
- path: params.path,
4155
- bytes: Buffer.byteLength(params.content, "utf8")
4156
- }
4157
- });
4158
- const result = await runtime.writeFile(params.path, params.content);
4159
- return textResult(`Wrote ${result.bytesWritten} bytes to ${params.path}.`, result);
4788
+ return executeCommandWithPolicy("exec", params);
4160
4789
  }
4161
4790
  };
4162
- const webSearchTool = {
4163
- name: "web_search",
4164
- label: "Web Search",
4165
- description: "Fetch a URL and return readable article text.",
4166
- 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,
4167
4796
  execute: async (_toolCallId, params) => {
4168
- audit({
4169
- actor: "tool",
4170
- eventType: "tool.fetch_web",
4171
- payload: { url: params.url }
4172
- });
4173
- const result = await runtime.fetchWeb(params.url);
4174
- return textResult(`${result.title ? `# ${result.title}\n\n` : ""}${result.markdown}`.trim(), result);
4797
+ return executeCommandWithPolicy("bash", params);
4175
4798
  }
4176
4799
  };
4177
- const fetchPodcastFeedTool = {
4178
- name: "fetch_podcast_feed",
4179
- label: "Fetch Podcast Feed",
4180
- 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.",
4181
4804
  parameters: Type.Object({
4182
- urls: Type.Array(Type.String({ format: "uri" }), {
4183
- minItems: 1,
4184
- maxItems: 20
4185
- }),
4186
- limitPerFeed: Type.Optional(Type.Number({
4187
- minimum: 1,
4188
- maximum: 50
4805
+ profile: Type.Optional(Type.String({ default: "core" })),
4806
+ timeoutMs: Type.Optional(Type.Number({
4807
+ minimum: 1e3,
4808
+ maximum: 12e4
4189
4809
  }))
4190
4810
  }),
4191
4811
  execute: async (_toolCallId, params) => {
4192
- const limit = params.limitPerFeed ?? 5;
4193
- const output = [];
4194
- for (const url of params.urls) try {
4195
- const feed = await parser.parseURL(url);
4196
- const episodes = (feed.items ?? []).slice(0, limit).map((item) => ({
4197
- title: item.title ?? "Untitled episode",
4198
- publishedAt: item.pubDate || item.isoDate || null,
4199
- link: item.link ?? "",
4200
- summary: (item.contentSnippet || item.content || item.summary || "").replace(/\s+/g, " ").trim().slice(0, 400) || "No summary available."
4201
- }));
4202
- output.push({
4203
- url,
4204
- title: feed.title ?? "Untitled feed",
4205
- 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
4206
4831
  });
4207
4832
  } catch (error) {
4208
- output.push({
4209
- url,
4210
- title: "Unknown feed",
4211
- episodes: [],
4212
- 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
4213
4842
  });
4214
4843
  }
4215
4844
  audit({
4216
4845
  actor: "tool",
4217
- eventType: "tool.fetch_podcast_feed",
4846
+ eventType: "tool.diagnose_device",
4218
4847
  payload: {
4219
- urls: params.urls,
4220
- limitPerFeed: limit
4848
+ profile,
4849
+ commandCount: sections.length,
4850
+ failed: sections.filter((entry) => !entry.ok).length
4221
4851
  }
4222
4852
  });
4223
- return textResult(output.map((feed) => {
4224
- if (feed.error) return `Feed: ${feed.url}\nError: ${feed.error}`;
4225
- const episodesText = feed.episodes.map((episode, index) => `${index + 1}. ${episode.title}${episode.publishedAt ? ` (${episode.publishedAt})` : ""}\n${episode.link}\n${episode.summary}`).join("\n\n");
4226
- return `Feed: ${feed.title}\nSource: ${feed.url}\n\n${episodesText}`;
4227
- }).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
+ });
4228
4871
  }
4229
4872
  };
4230
- const tools = [];
4231
- if (bashEnabled) tools.push(bashTool);
4232
- 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);
4233
4987
  return tools;
4234
4988
  }
4235
4989
 
@@ -5479,7 +6233,7 @@ function registerGatewayCommands(program, deps = defaultGatewayCommandDeps) {
5479
6233
  gateway.command("run").description("Run HOVClaw daemon with gateway in foreground").action(async () => {
5480
6234
  if (!config.gateway.enabled) throw new Error("Gateway is disabled in config. Enable gateway.enabled first.");
5481
6235
  process.stdout.write(`Starting HOVClaw gateway on ${config.gateway.host}:${config.gateway.port}...\n`);
5482
- await import("./src-qX1C6PLF.js");
6236
+ await import("./src-GZDRRc5A.js");
5483
6237
  });
5484
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) => {
5485
6239
  const url = resolveGatewayWebUiUrl();
@@ -5777,13 +6531,13 @@ function loadCliVersion() {
5777
6531
  }
5778
6532
  function registerCoreCommands(program) {
5779
6533
  program.command("onboard").description("Run interactive onboarding wizard").action(async () => {
5780
- await (await import("./onboard-DL6VDf50.js")).main();
6534
+ await (await import("./onboard-Cc2XHLT4.js")).main();
5781
6535
  });
5782
6536
  program.command("login [provider]").description("Run OAuth login for a provider").action(async (provider) => {
5783
- await (await import("./login-BwvBMKdz.js")).main(provider ? [provider] : []);
6537
+ await (await import("./login-BtLE2Bye.js")).main(provider ? [provider] : []);
5784
6538
  });
5785
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) => {
5786
- const module = await import("./doctor-DJHTvhli.js");
6540
+ const module = await import("./doctor-0iphhiTj.js");
5787
6541
  const args = [];
5788
6542
  if (options.fix || options.repair) args.push("--fix");
5789
6543
  if (options.deep) args.push("--deep");
@@ -5791,7 +6545,7 @@ function registerCoreCommands(program) {
5791
6545
  await module.main(args);
5792
6546
  });
5793
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) => {
5794
- const module = await import("./reset-BJUhrojJ.js");
6548
+ const module = await import("./reset-ChNzCD2s.js");
5795
6549
  try {
5796
6550
  const result = await module.runResetCommand({
5797
6551
  scope: options.scope,
@@ -5907,4 +6661,4 @@ main().catch((error) => {
5907
6661
  });
5908
6662
 
5909
6663
  //#endregion
5910
- 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, 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 };