hovclaw 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -16
- package/dist/{doctor-D52M80De.js → doctor-0iphhiTj.js} +2 -2
- package/dist/hovclaw.js +893 -292
- package/dist/index.js +1194 -264
- package/dist/{login-BwvBMKdz.js → login-BtLE2Bye.js} +1 -1
- package/dist/{onboard-DL6VDf50.js → onboard-Cc2XHLT4.js} +12 -2
- package/dist/{reset-BJUhrojJ.js → reset-ChNzCD2s.js} +1 -1
- package/dist/{src-Y6AqidKn.js → src-GZDRRc5A.js} +309 -40
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -97,7 +97,27 @@ const DEFAULT_FILE_CONFIG = {
|
|
|
97
97
|
"tail",
|
|
98
98
|
"wc"
|
|
99
99
|
],
|
|
100
|
-
tools: {
|
|
100
|
+
tools: {
|
|
101
|
+
bashEnabled: false,
|
|
102
|
+
exec: {
|
|
103
|
+
enabled: false,
|
|
104
|
+
security: "allowlist",
|
|
105
|
+
ask: "on-miss",
|
|
106
|
+
approvalTimeoutMs: 12e4,
|
|
107
|
+
allowlist: [],
|
|
108
|
+
safeBins: [...[
|
|
109
|
+
"jq",
|
|
110
|
+
"grep",
|
|
111
|
+
"cut",
|
|
112
|
+
"sort",
|
|
113
|
+
"uniq",
|
|
114
|
+
"head",
|
|
115
|
+
"tail",
|
|
116
|
+
"tr",
|
|
117
|
+
"wc"
|
|
118
|
+
]]
|
|
119
|
+
}
|
|
120
|
+
}
|
|
101
121
|
},
|
|
102
122
|
channels: {
|
|
103
123
|
discord: {
|
|
@@ -202,6 +222,22 @@ const commandsConfigSchema = z.object({
|
|
|
202
222
|
useAccessGroups: z.boolean(),
|
|
203
223
|
allowFrom: commandAllowFromSchema
|
|
204
224
|
});
|
|
225
|
+
const runtimeExecConfigSchema = z.object({
|
|
226
|
+
enabled: z.boolean(),
|
|
227
|
+
security: z.enum([
|
|
228
|
+
"deny",
|
|
229
|
+
"allowlist",
|
|
230
|
+
"full"
|
|
231
|
+
]),
|
|
232
|
+
ask: z.enum([
|
|
233
|
+
"off",
|
|
234
|
+
"on-miss",
|
|
235
|
+
"always"
|
|
236
|
+
]),
|
|
237
|
+
approvalTimeoutMs: z.number().int().positive(),
|
|
238
|
+
allowlist: z.array(z.string().min(1)),
|
|
239
|
+
safeBins: z.array(z.string().min(1))
|
|
240
|
+
});
|
|
205
241
|
const telegramTopicConfigSchema = z.object({
|
|
206
242
|
enabled: z.boolean().optional(),
|
|
207
243
|
requireMention: z.boolean().optional(),
|
|
@@ -364,7 +400,10 @@ const fileConfigSchema = z.object({
|
|
|
364
400
|
allowedReadRoots: z.array(z.string().min(1)),
|
|
365
401
|
allowedWriteRoots: z.array(z.string().min(1)),
|
|
366
402
|
allowedCommandPrefixes: z.array(z.string().min(1)).min(1),
|
|
367
|
-
tools: z.object({
|
|
403
|
+
tools: z.object({
|
|
404
|
+
bashEnabled: z.boolean(),
|
|
405
|
+
exec: runtimeExecConfigSchema
|
|
406
|
+
})
|
|
368
407
|
}),
|
|
369
408
|
channels: z.object({
|
|
370
409
|
discord: z.object({
|
|
@@ -441,7 +480,21 @@ const partialFileConfigSchema = z.object({
|
|
|
441
480
|
bindings: fileConfigSchema.shape.bindings.optional(),
|
|
442
481
|
models: fileConfigSchema.shape.models.partial().optional(),
|
|
443
482
|
commands: commandsConfigSchema.partial().optional(),
|
|
444
|
-
runtime:
|
|
483
|
+
runtime: z.object({
|
|
484
|
+
mode: z.enum(["local", "container"]).optional(),
|
|
485
|
+
containerImage: z.string().min(1).optional(),
|
|
486
|
+
idleTimeoutMs: z.number().int().positive().optional(),
|
|
487
|
+
networkMode: z.string().min(1).optional(),
|
|
488
|
+
timeoutMs: z.number().int().positive().optional(),
|
|
489
|
+
maxOutputBytes: z.number().int().positive().optional(),
|
|
490
|
+
allowedReadRoots: z.array(z.string().min(1)).optional(),
|
|
491
|
+
allowedWriteRoots: z.array(z.string().min(1)).optional(),
|
|
492
|
+
allowedCommandPrefixes: z.array(z.string().min(1)).min(1).optional(),
|
|
493
|
+
tools: z.object({
|
|
494
|
+
bashEnabled: z.boolean().optional(),
|
|
495
|
+
exec: runtimeExecConfigSchema.partial().optional()
|
|
496
|
+
}).optional()
|
|
497
|
+
}).optional(),
|
|
445
498
|
channels: partialChannelsSchema,
|
|
446
499
|
gateway: partialGatewayConfigSchema,
|
|
447
500
|
scheduler: fileConfigSchema.shape.scheduler.partial().optional()
|
|
@@ -712,7 +765,17 @@ function mergeWithDefaults(partial) {
|
|
|
712
765
|
allowedReadRoots: partial.runtime?.allowedReadRoots ?? DEFAULT_FILE_CONFIG.runtime.allowedReadRoots,
|
|
713
766
|
allowedWriteRoots: partial.runtime?.allowedWriteRoots ?? DEFAULT_FILE_CONFIG.runtime.allowedWriteRoots,
|
|
714
767
|
allowedCommandPrefixes: partial.runtime?.allowedCommandPrefixes ?? DEFAULT_FILE_CONFIG.runtime.allowedCommandPrefixes,
|
|
715
|
-
tools: {
|
|
768
|
+
tools: {
|
|
769
|
+
bashEnabled: partial.runtime?.tools?.bashEnabled ?? DEFAULT_FILE_CONFIG.runtime.tools.bashEnabled,
|
|
770
|
+
exec: {
|
|
771
|
+
enabled: partial.runtime?.tools?.exec?.enabled ?? partial.runtime?.tools?.bashEnabled ?? DEFAULT_FILE_CONFIG.runtime.tools.exec.enabled,
|
|
772
|
+
security: partial.runtime?.tools?.exec?.security ?? DEFAULT_FILE_CONFIG.runtime.tools.exec.security,
|
|
773
|
+
ask: partial.runtime?.tools?.exec?.ask ?? DEFAULT_FILE_CONFIG.runtime.tools.exec.ask,
|
|
774
|
+
approvalTimeoutMs: partial.runtime?.tools?.exec?.approvalTimeoutMs ?? DEFAULT_FILE_CONFIG.runtime.tools.exec.approvalTimeoutMs,
|
|
775
|
+
allowlist: partial.runtime?.tools?.exec?.allowlist ?? DEFAULT_FILE_CONFIG.runtime.tools.exec.allowlist,
|
|
776
|
+
safeBins: partial.runtime?.tools?.exec?.safeBins ?? DEFAULT_FILE_CONFIG.runtime.tools.exec.safeBins
|
|
777
|
+
}
|
|
778
|
+
}
|
|
716
779
|
},
|
|
717
780
|
channels: {
|
|
718
781
|
discord: {
|
|
@@ -824,7 +887,17 @@ function applyEnvOverrides(base, env) {
|
|
|
824
887
|
allowedReadRoots: splitCsv(env.ALLOWED_READ_ROOTS, base.runtime.allowedReadRoots),
|
|
825
888
|
allowedWriteRoots: splitCsv(env.ALLOWED_WRITE_ROOTS, base.runtime.allowedWriteRoots),
|
|
826
889
|
allowedCommandPrefixes: splitCsv(env.ALLOWED_COMMAND_PREFIXES, base.runtime.allowedCommandPrefixes),
|
|
827
|
-
tools: {
|
|
890
|
+
tools: {
|
|
891
|
+
bashEnabled: toBool(env.RUNTIME_BASH_ENABLED, base.runtime.tools.bashEnabled),
|
|
892
|
+
exec: {
|
|
893
|
+
enabled: toBool(env.RUNTIME_EXEC_ENABLED, toBool(env.RUNTIME_BASH_ENABLED, base.runtime.tools.exec.enabled)),
|
|
894
|
+
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,
|
|
895
|
+
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,
|
|
896
|
+
approvalTimeoutMs: toPositiveInt(env.RUNTIME_EXEC_APPROVAL_TIMEOUT_MS, base.runtime.tools.exec.approvalTimeoutMs),
|
|
897
|
+
allowlist: splitCsv(env.RUNTIME_EXEC_ALLOWLIST, base.runtime.tools.exec.allowlist),
|
|
898
|
+
safeBins: splitCsv(env.RUNTIME_EXEC_SAFE_BINS, base.runtime.tools.exec.safeBins)
|
|
899
|
+
}
|
|
900
|
+
}
|
|
828
901
|
},
|
|
829
902
|
channels: {
|
|
830
903
|
discord: {
|
|
@@ -929,7 +1002,17 @@ function loadConfig(env = process.env) {
|
|
|
929
1002
|
allowedReadRoots: normalizeRoots(merged.runtime.allowedReadRoots),
|
|
930
1003
|
allowedWriteRoots: normalizeRoots(merged.runtime.allowedWriteRoots),
|
|
931
1004
|
allowedCommandPrefixes: merged.runtime.allowedCommandPrefixes,
|
|
932
|
-
tools: {
|
|
1005
|
+
tools: {
|
|
1006
|
+
bashEnabled: merged.runtime.tools.bashEnabled,
|
|
1007
|
+
exec: {
|
|
1008
|
+
enabled: merged.runtime.tools.exec.enabled || merged.runtime.tools.bashEnabled,
|
|
1009
|
+
security: merged.runtime.tools.exec.security,
|
|
1010
|
+
ask: merged.runtime.tools.exec.ask,
|
|
1011
|
+
approvalTimeoutMs: merged.runtime.tools.exec.approvalTimeoutMs,
|
|
1012
|
+
allowlist: [...merged.runtime.tools.exec.allowlist],
|
|
1013
|
+
safeBins: [...merged.runtime.tools.exec.safeBins]
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
933
1016
|
},
|
|
934
1017
|
channels: {
|
|
935
1018
|
discord: {
|
|
@@ -1027,6 +1110,12 @@ function legacyEnvKeys() {
|
|
|
1027
1110
|
"ALLOWED_WRITE_ROOTS",
|
|
1028
1111
|
"ALLOWED_COMMAND_PREFIXES",
|
|
1029
1112
|
"RUNTIME_BASH_ENABLED",
|
|
1113
|
+
"RUNTIME_EXEC_ENABLED",
|
|
1114
|
+
"RUNTIME_EXEC_SECURITY",
|
|
1115
|
+
"RUNTIME_EXEC_ASK",
|
|
1116
|
+
"RUNTIME_EXEC_APPROVAL_TIMEOUT_MS",
|
|
1117
|
+
"RUNTIME_EXEC_ALLOWLIST",
|
|
1118
|
+
"RUNTIME_EXEC_SAFE_BINS",
|
|
1030
1119
|
"TOOL_TIMEOUT_MS",
|
|
1031
1120
|
"TOOL_MAX_OUTPUT_BYTES",
|
|
1032
1121
|
"ENABLE_DISCORD",
|
|
@@ -2050,6 +2139,37 @@ var PiAgentManager = class {
|
|
|
2050
2139
|
}
|
|
2051
2140
|
};
|
|
2052
2141
|
|
|
2142
|
+
//#endregion
|
|
2143
|
+
//#region src/channels/command-auth.ts
|
|
2144
|
+
function normalizeAllowFromEntry(value) {
|
|
2145
|
+
return String(value).trim().toLowerCase();
|
|
2146
|
+
}
|
|
2147
|
+
function senderCandidates(msg) {
|
|
2148
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
2149
|
+
const userId = msg.userId.trim();
|
|
2150
|
+
if (userId) candidates.add(userId.toLowerCase());
|
|
2151
|
+
const displayName = msg.displayName.trim();
|
|
2152
|
+
if (displayName.startsWith("@")) candidates.add(displayName.toLowerCase());
|
|
2153
|
+
return Array.from(candidates);
|
|
2154
|
+
}
|
|
2155
|
+
function resolveCommandsAllowFrom(config, channel) {
|
|
2156
|
+
const entries = config.commands.allowFrom;
|
|
2157
|
+
if (Object.keys(entries).length === 0) return null;
|
|
2158
|
+
const providerSpecific = entries[channel];
|
|
2159
|
+
if (Array.isArray(providerSpecific)) return providerSpecific;
|
|
2160
|
+
const global = entries["*"];
|
|
2161
|
+
if (Array.isArray(global)) return global;
|
|
2162
|
+
return [];
|
|
2163
|
+
}
|
|
2164
|
+
function isCommandAuthorized(config, msg) {
|
|
2165
|
+
const allowFrom = resolveCommandsAllowFrom(config, msg.channel);
|
|
2166
|
+
if (allowFrom === null) return true;
|
|
2167
|
+
const normalizedAllow = new Set(allowFrom.map((entry) => normalizeAllowFromEntry(entry)));
|
|
2168
|
+
if (normalizedAllow.has("*")) return true;
|
|
2169
|
+
for (const candidate of senderCandidates(msg)) if (normalizedAllow.has(candidate)) return true;
|
|
2170
|
+
return false;
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2053
2173
|
//#endregion
|
|
2054
2174
|
//#region src/channels/discord.ts
|
|
2055
2175
|
const DISCORD_CHUNK_LIMIT = 1900;
|
|
@@ -3084,99 +3204,6 @@ async function requestDaemonRestartFromCurrentProcess(env = process.env) {
|
|
|
3084
3204
|
};
|
|
3085
3205
|
}
|
|
3086
3206
|
|
|
3087
|
-
//#endregion
|
|
3088
|
-
//#region src/compat/openclaw-mirror.ts
|
|
3089
|
-
function resolveOpenClawHome(env = process.env) {
|
|
3090
|
-
const override = env.OPENCLAW_STATE_DIR?.trim();
|
|
3091
|
-
if (override) return path.resolve(override.startsWith("~") ? path.join(os.homedir(), override.slice(1)) : override);
|
|
3092
|
-
return path.join(os.homedir(), ".openclaw");
|
|
3093
|
-
}
|
|
3094
|
-
function resolveOpenClawConfigPath(openclawHome) {
|
|
3095
|
-
return path.join(openclawHome, "openclaw.json");
|
|
3096
|
-
}
|
|
3097
|
-
function resolveOpenClawSharedSkillsPath(openclawHome) {
|
|
3098
|
-
return path.join(openclawHome, "skills");
|
|
3099
|
-
}
|
|
3100
|
-
function buildMirrorConfig(config) {
|
|
3101
|
-
const fallbackWorkspace = config.agents.defaults.workspace || path.join(config.hovclawHome, "workspace");
|
|
3102
|
-
const agentList = config.agents.list.length > 0 ? config.agents.list : [{
|
|
3103
|
-
id: "main",
|
|
3104
|
-
name: "Main",
|
|
3105
|
-
workspace: fallbackWorkspace,
|
|
3106
|
-
default: true
|
|
3107
|
-
}];
|
|
3108
|
-
const extraDirs = /* @__PURE__ */ new Set();
|
|
3109
|
-
extraDirs.add(config.skillsDir);
|
|
3110
|
-
for (const agent of agentList) {
|
|
3111
|
-
const workspace = (agent.workspace || fallbackWorkspace).trim();
|
|
3112
|
-
if (!workspace) continue;
|
|
3113
|
-
extraDirs.add(path.join(workspace, "skills"));
|
|
3114
|
-
}
|
|
3115
|
-
return {
|
|
3116
|
-
agent: { workspace: fallbackWorkspace },
|
|
3117
|
-
agents: {
|
|
3118
|
-
defaults: { workspace: fallbackWorkspace },
|
|
3119
|
-
list: agentList
|
|
3120
|
-
},
|
|
3121
|
-
skills: { load: { extraDirs: Array.from(extraDirs) } }
|
|
3122
|
-
};
|
|
3123
|
-
}
|
|
3124
|
-
function ensureDir(dirPath) {
|
|
3125
|
-
fs.mkdirSync(dirPath, {
|
|
3126
|
-
recursive: true,
|
|
3127
|
-
mode: 448
|
|
3128
|
-
});
|
|
3129
|
-
}
|
|
3130
|
-
function syncSkillsDir(sharedSkillsPath, sourceSkillsPath) {
|
|
3131
|
-
if (!fs.existsSync(sourceSkillsPath)) {
|
|
3132
|
-
ensureDir(sharedSkillsPath);
|
|
3133
|
-
return false;
|
|
3134
|
-
}
|
|
3135
|
-
try {
|
|
3136
|
-
if (fs.lstatSync(sharedSkillsPath).isSymbolicLink()) {
|
|
3137
|
-
const linkTarget = fs.readlinkSync(sharedSkillsPath);
|
|
3138
|
-
if (path.resolve(path.dirname(sharedSkillsPath), linkTarget) === sourceSkillsPath) return true;
|
|
3139
|
-
fs.unlinkSync(sharedSkillsPath);
|
|
3140
|
-
}
|
|
3141
|
-
} catch {}
|
|
3142
|
-
if (!fs.existsSync(sharedSkillsPath)) try {
|
|
3143
|
-
fs.symlinkSync(sourceSkillsPath, sharedSkillsPath, "dir");
|
|
3144
|
-
return true;
|
|
3145
|
-
} catch {}
|
|
3146
|
-
ensureDir(sharedSkillsPath);
|
|
3147
|
-
for (const entry of fs.readdirSync(sourceSkillsPath, { withFileTypes: true })) {
|
|
3148
|
-
const src = path.join(sourceSkillsPath, entry.name);
|
|
3149
|
-
const dst = path.join(sharedSkillsPath, entry.name);
|
|
3150
|
-
if (entry.isDirectory()) {
|
|
3151
|
-
fs.cpSync(src, dst, {
|
|
3152
|
-
recursive: true,
|
|
3153
|
-
force: true
|
|
3154
|
-
});
|
|
3155
|
-
continue;
|
|
3156
|
-
}
|
|
3157
|
-
if (entry.isFile()) fs.copyFileSync(src, dst);
|
|
3158
|
-
}
|
|
3159
|
-
return false;
|
|
3160
|
-
}
|
|
3161
|
-
function writeOpenClawMirror(config) {
|
|
3162
|
-
const openclawHome = resolveOpenClawHome();
|
|
3163
|
-
const configPath = resolveOpenClawConfigPath(openclawHome);
|
|
3164
|
-
const sharedSkillsPath = resolveOpenClawSharedSkillsPath(openclawHome);
|
|
3165
|
-
ensureDir(openclawHome);
|
|
3166
|
-
const mirrorConfig = buildMirrorConfig(config);
|
|
3167
|
-
fs.writeFileSync(configPath, `${JSON.stringify(mirrorConfig, null, 2)}\n`, {
|
|
3168
|
-
encoding: "utf8",
|
|
3169
|
-
mode: 384
|
|
3170
|
-
});
|
|
3171
|
-
fs.chmodSync(configPath, 384);
|
|
3172
|
-
return {
|
|
3173
|
-
openclawHome,
|
|
3174
|
-
configPath,
|
|
3175
|
-
sharedSkillsPath,
|
|
3176
|
-
linkedSharedSkills: syncSkillsDir(sharedSkillsPath, config.skillsDir)
|
|
3177
|
-
};
|
|
3178
|
-
}
|
|
3179
|
-
|
|
3180
3207
|
//#endregion
|
|
3181
3208
|
//#region src/models/catalog.ts
|
|
3182
3209
|
function buildModelCatalog(aliases) {
|
|
@@ -3197,37 +3224,6 @@ function buildModelCatalog(aliases) {
|
|
|
3197
3224
|
}).filter((entry) => Boolean(entry)).sort((a, b) => a.ref.localeCompare(b.ref));
|
|
3198
3225
|
}
|
|
3199
3226
|
|
|
3200
|
-
//#endregion
|
|
3201
|
-
//#region src/channels/command-auth.ts
|
|
3202
|
-
function normalizeAllowFromEntry(value) {
|
|
3203
|
-
return String(value).trim().toLowerCase();
|
|
3204
|
-
}
|
|
3205
|
-
function senderCandidates(msg) {
|
|
3206
|
-
const candidates = /* @__PURE__ */ new Set();
|
|
3207
|
-
const userId = msg.userId.trim();
|
|
3208
|
-
if (userId) candidates.add(userId.toLowerCase());
|
|
3209
|
-
const displayName = msg.displayName.trim();
|
|
3210
|
-
if (displayName.startsWith("@")) candidates.add(displayName.toLowerCase());
|
|
3211
|
-
return Array.from(candidates);
|
|
3212
|
-
}
|
|
3213
|
-
function resolveCommandsAllowFrom(config, channel) {
|
|
3214
|
-
const entries = config.commands.allowFrom;
|
|
3215
|
-
if (Object.keys(entries).length === 0) return null;
|
|
3216
|
-
const providerSpecific = entries[channel];
|
|
3217
|
-
if (Array.isArray(providerSpecific)) return providerSpecific;
|
|
3218
|
-
const global = entries["*"];
|
|
3219
|
-
if (Array.isArray(global)) return global;
|
|
3220
|
-
return [];
|
|
3221
|
-
}
|
|
3222
|
-
function isCommandAuthorized(config, msg) {
|
|
3223
|
-
const allowFrom = resolveCommandsAllowFrom(config, msg.channel);
|
|
3224
|
-
if (allowFrom === null) return true;
|
|
3225
|
-
const normalizedAllow = new Set(allowFrom.map((entry) => normalizeAllowFromEntry(entry)));
|
|
3226
|
-
if (normalizedAllow.has("*")) return true;
|
|
3227
|
-
for (const candidate of senderCandidates(msg)) if (normalizedAllow.has(candidate)) return true;
|
|
3228
|
-
return false;
|
|
3229
|
-
}
|
|
3230
|
-
|
|
3231
3227
|
//#endregion
|
|
3232
3228
|
//#region src/channels/commands-registry.ts
|
|
3233
3229
|
const TELEGRAM_COMMAND_NAME_RE = /^[a-z0-9_]{1,32}$/;
|
|
@@ -3338,6 +3334,11 @@ const BASE_COMMANDS = [
|
|
|
3338
3334
|
nativeName: "bash",
|
|
3339
3335
|
description: "Run a shell command through the assistant (if enabled)"
|
|
3340
3336
|
},
|
|
3337
|
+
{
|
|
3338
|
+
key: "approve",
|
|
3339
|
+
nativeName: "approve",
|
|
3340
|
+
description: "Approve or deny pending exec request"
|
|
3341
|
+
},
|
|
3341
3342
|
{
|
|
3342
3343
|
key: "restart",
|
|
3343
3344
|
nativeName: "restart",
|
|
@@ -3583,7 +3584,6 @@ function reloadRuntimeConfig() {
|
|
|
3583
3584
|
function persistFileConfig(next) {
|
|
3584
3585
|
saveConfigFile(next);
|
|
3585
3586
|
reloadRuntimeConfig();
|
|
3586
|
-
writeOpenClawMirror(config);
|
|
3587
3587
|
}
|
|
3588
3588
|
function parseConfigPath(raw) {
|
|
3589
3589
|
const pathValue = raw?.trim();
|
|
@@ -4271,7 +4271,7 @@ function emptyState() {
|
|
|
4271
4271
|
pending: {}
|
|
4272
4272
|
};
|
|
4273
4273
|
}
|
|
4274
|
-
function nowIso$
|
|
4274
|
+
function nowIso$2() {
|
|
4275
4275
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
4276
4276
|
}
|
|
4277
4277
|
function randomCode() {
|
|
@@ -4312,7 +4312,7 @@ var TelegramPairingStore = class {
|
|
|
4312
4312
|
...pendingByAccount,
|
|
4313
4313
|
[code]: {
|
|
4314
4314
|
userId,
|
|
4315
|
-
createdAt: nowIso$
|
|
4315
|
+
createdAt: nowIso$2()
|
|
4316
4316
|
}
|
|
4317
4317
|
};
|
|
4318
4318
|
this.writeState(state);
|
|
@@ -4476,7 +4476,7 @@ function redactSensitiveData(value) {
|
|
|
4476
4476
|
|
|
4477
4477
|
//#endregion
|
|
4478
4478
|
//#region src/db.ts
|
|
4479
|
-
function nowIso() {
|
|
4479
|
+
function nowIso$1() {
|
|
4480
4480
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
4481
4481
|
}
|
|
4482
4482
|
var HovClawDb = class {
|
|
@@ -4604,7 +4604,7 @@ var HovClawDb = class {
|
|
|
4604
4604
|
return Boolean(row?.name);
|
|
4605
4605
|
}
|
|
4606
4606
|
upsertSession(parts, sessionKey, model, options) {
|
|
4607
|
-
const createdAt = nowIso();
|
|
4607
|
+
const createdAt = nowIso$1();
|
|
4608
4608
|
this.db.prepare(`
|
|
4609
4609
|
INSERT INTO sessions (
|
|
4610
4610
|
session_key,
|
|
@@ -4699,7 +4699,7 @@ var HovClawDb = class {
|
|
|
4699
4699
|
ON CONFLICT(account_id) DO UPDATE SET
|
|
4700
4700
|
last_update_id = excluded.last_update_id,
|
|
4701
4701
|
updated_at = excluded.updated_at
|
|
4702
|
-
`).run(accountId, updateId, nowIso());
|
|
4702
|
+
`).run(accountId, updateId, nowIso$1());
|
|
4703
4703
|
}
|
|
4704
4704
|
hasTelegramDedupe(accountId, updateId) {
|
|
4705
4705
|
return typeof this.db.prepare(`
|
|
@@ -4712,7 +4712,7 @@ var HovClawDb = class {
|
|
|
4712
4712
|
this.db.prepare(`
|
|
4713
4713
|
INSERT OR IGNORE INTO telegram_dedupe (account_id, update_id, updated_at)
|
|
4714
4714
|
VALUES (?, ?, ?)
|
|
4715
|
-
`).run(accountId, updateId, nowIso());
|
|
4715
|
+
`).run(accountId, updateId, nowIso$1());
|
|
4716
4716
|
}
|
|
4717
4717
|
pruneTelegramDedupe(olderThanIso) {
|
|
4718
4718
|
return this.db.prepare(`
|
|
@@ -4721,7 +4721,7 @@ var HovClawDb = class {
|
|
|
4721
4721
|
`).run(olderThanIso).changes;
|
|
4722
4722
|
}
|
|
4723
4723
|
appendMessage(sessionKey, role, content) {
|
|
4724
|
-
this.db.prepare(`INSERT INTO messages (session_key, role, content, created_at) VALUES (?, ?, ?, ?)`).run(sessionKey, role, content, nowIso());
|
|
4724
|
+
this.db.prepare(`INSERT INTO messages (session_key, role, content, created_at) VALUES (?, ?, ?, ?)`).run(sessionKey, role, content, nowIso$1());
|
|
4725
4725
|
}
|
|
4726
4726
|
getMessages(sessionKey) {
|
|
4727
4727
|
return this.db.prepare(`
|
|
@@ -4742,7 +4742,7 @@ var HovClawDb = class {
|
|
|
4742
4742
|
ON CONFLICT(session_key) DO UPDATE SET
|
|
4743
4743
|
state_json = excluded.state_json,
|
|
4744
4744
|
updated_at = excluded.updated_at
|
|
4745
|
-
`).run(sessionKey, stateJson, nowIso());
|
|
4745
|
+
`).run(sessionKey, stateJson, nowIso$1());
|
|
4746
4746
|
}
|
|
4747
4747
|
getAgentState(sessionKey) {
|
|
4748
4748
|
return this.db.prepare(`SELECT state_json FROM agent_state WHERE session_key = ?`).get(sessionKey)?.state_json ?? null;
|
|
@@ -4763,7 +4763,7 @@ var HovClawDb = class {
|
|
|
4763
4763
|
cost_usd,
|
|
4764
4764
|
created_at
|
|
4765
4765
|
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
4766
|
-
`).run(record.sessionKey, record.provider, record.model, record.inputTokens, record.outputTokens, record.costUsd, record.createdAt ?? nowIso());
|
|
4766
|
+
`).run(record.sessionKey, record.provider, record.model, record.inputTokens, record.outputTokens, record.costUsd, record.createdAt ?? nowIso$1());
|
|
4767
4767
|
}
|
|
4768
4768
|
upsertScheduledJob(job) {
|
|
4769
4769
|
this.db.prepare(`
|
|
@@ -4897,7 +4897,7 @@ var HovClawDb = class {
|
|
|
4897
4897
|
this.db.prepare(`
|
|
4898
4898
|
INSERT INTO audit_log (ts, session_key, actor, event_type, payload_json)
|
|
4899
4899
|
VALUES (?, ?, ?, ?, ?)
|
|
4900
|
-
`).run(record.ts ?? nowIso(), record.sessionKey ?? null, record.actor, record.eventType, JSON.stringify(sanitizedPayload));
|
|
4900
|
+
`).run(record.ts ?? nowIso$1(), record.sessionKey ?? null, record.actor, record.eventType, JSON.stringify(sanitizedPayload));
|
|
4901
4901
|
}
|
|
4902
4902
|
getAuditEvents(eventType) {
|
|
4903
4903
|
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`;
|
|
@@ -4911,6 +4911,585 @@ var HovClawDb = class {
|
|
|
4911
4911
|
}
|
|
4912
4912
|
};
|
|
4913
4913
|
|
|
4914
|
+
//#endregion
|
|
4915
|
+
//#region src/exec-approvals.ts
|
|
4916
|
+
const DEFAULT_FILE_VERSION = 1;
|
|
4917
|
+
const DEFAULT_APPROVAL_TIMEOUT_MS = 12e4;
|
|
4918
|
+
const ONE_TIME_APPROVAL_TTL_MS = 5 * 6e4;
|
|
4919
|
+
const SHELL_DISALLOWED_PATTERN = /[;&|`$()<>]|[\r\n]/;
|
|
4920
|
+
const SHELL_QUOTE_OR_ESCAPE_PATTERN = /["'\\]/;
|
|
4921
|
+
function nowIso() {
|
|
4922
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
4923
|
+
}
|
|
4924
|
+
function normalizeExecSecurity(value) {
|
|
4925
|
+
if (value === "deny" || value === "allowlist" || value === "full") return value;
|
|
4926
|
+
return null;
|
|
4927
|
+
}
|
|
4928
|
+
function normalizeExecAsk(value) {
|
|
4929
|
+
if (value === "off" || value === "on-miss" || value === "always") return value;
|
|
4930
|
+
return null;
|
|
4931
|
+
}
|
|
4932
|
+
function normalizeAllowlist(entries) {
|
|
4933
|
+
if (!Array.isArray(entries)) return [];
|
|
4934
|
+
const out = [];
|
|
4935
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4936
|
+
for (const entry of entries) {
|
|
4937
|
+
if (!entry || typeof entry !== "object") continue;
|
|
4938
|
+
const pattern = typeof entry.pattern === "string" ? entry.pattern.trim() : "";
|
|
4939
|
+
if (!pattern) continue;
|
|
4940
|
+
const lowered = pattern.toLowerCase();
|
|
4941
|
+
if (seen.has(lowered)) continue;
|
|
4942
|
+
seen.add(lowered);
|
|
4943
|
+
out.push({
|
|
4944
|
+
pattern,
|
|
4945
|
+
lastUsedAt: typeof entry.lastUsedAt === "string" ? entry.lastUsedAt : void 0,
|
|
4946
|
+
lastUsedCommand: typeof entry.lastUsedCommand === "string" ? entry.lastUsedCommand : void 0,
|
|
4947
|
+
lastResolvedPath: typeof entry.lastResolvedPath === "string" ? entry.lastResolvedPath : void 0
|
|
4948
|
+
});
|
|
4949
|
+
}
|
|
4950
|
+
return out;
|
|
4951
|
+
}
|
|
4952
|
+
function normalizeFile(value, defaults) {
|
|
4953
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {
|
|
4954
|
+
version: DEFAULT_FILE_VERSION,
|
|
4955
|
+
defaults: {
|
|
4956
|
+
security: defaults.security,
|
|
4957
|
+
ask: defaults.ask
|
|
4958
|
+
},
|
|
4959
|
+
agents: {}
|
|
4960
|
+
};
|
|
4961
|
+
const raw = value;
|
|
4962
|
+
const security = typeof raw.defaults?.security === "string" ? normalizeExecSecurity(raw.defaults.security) : null;
|
|
4963
|
+
const ask = typeof raw.defaults?.ask === "string" ? normalizeExecAsk(raw.defaults.ask) : null;
|
|
4964
|
+
const agents = {};
|
|
4965
|
+
if (raw.agents && typeof raw.agents === "object" && !Array.isArray(raw.agents)) for (const [agentId, agentValue] of Object.entries(raw.agents)) {
|
|
4966
|
+
if (!agentId.trim()) continue;
|
|
4967
|
+
agents[agentId] = { allowlist: agentValue && typeof agentValue === "object" && !Array.isArray(agentValue) ? normalizeAllowlist(agentValue.allowlist) : [] };
|
|
4968
|
+
}
|
|
4969
|
+
return {
|
|
4970
|
+
version: DEFAULT_FILE_VERSION,
|
|
4971
|
+
defaults: {
|
|
4972
|
+
security: security ?? defaults.security,
|
|
4973
|
+
ask: ask ?? defaults.ask
|
|
4974
|
+
},
|
|
4975
|
+
agents
|
|
4976
|
+
};
|
|
4977
|
+
}
|
|
4978
|
+
function normalizePathForMatch(value) {
|
|
4979
|
+
if (process.platform === "win32") return value.replace(/\\/g, "/").toLowerCase();
|
|
4980
|
+
return value;
|
|
4981
|
+
}
|
|
4982
|
+
function resolveExecutablePath(executable, cwd, env) {
|
|
4983
|
+
if (!executable.trim()) return;
|
|
4984
|
+
if (executable.includes("/") || executable.includes("\\")) {
|
|
4985
|
+
const absolute = path.isAbsolute(executable) ? path.resolve(executable) : path.resolve(cwd, executable);
|
|
4986
|
+
if (fs.existsSync(absolute)) return absolute;
|
|
4987
|
+
return;
|
|
4988
|
+
}
|
|
4989
|
+
const pathEntries = (env.PATH || process.env.PATH || "").split(path.delimiter).filter(Boolean);
|
|
4990
|
+
for (const entry of pathEntries) {
|
|
4991
|
+
const candidate = path.join(entry, executable);
|
|
4992
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
4993
|
+
}
|
|
4994
|
+
}
|
|
4995
|
+
function analyzeCommand(command, cwd, env) {
|
|
4996
|
+
const trimmed = command.trim();
|
|
4997
|
+
if (!trimmed) return {
|
|
4998
|
+
ok: false,
|
|
4999
|
+
reason: "empty command",
|
|
5000
|
+
args: []
|
|
5001
|
+
};
|
|
5002
|
+
if (SHELL_DISALLOWED_PATTERN.test(trimmed)) return {
|
|
5003
|
+
ok: false,
|
|
5004
|
+
reason: "disallowed shell syntax",
|
|
5005
|
+
args: []
|
|
5006
|
+
};
|
|
5007
|
+
if (SHELL_QUOTE_OR_ESCAPE_PATTERN.test(trimmed)) return {
|
|
5008
|
+
ok: false,
|
|
5009
|
+
reason: "quoted/escaped shell syntax is not allowed",
|
|
5010
|
+
args: []
|
|
5011
|
+
};
|
|
5012
|
+
const tokens = trimmed.split(/\s+/).filter(Boolean);
|
|
5013
|
+
if (tokens.length === 0) return {
|
|
5014
|
+
ok: false,
|
|
5015
|
+
reason: "empty command",
|
|
5016
|
+
args: []
|
|
5017
|
+
};
|
|
5018
|
+
const executableRaw = tokens[0];
|
|
5019
|
+
const resolvedPath = resolveExecutablePath(executableRaw, cwd, env);
|
|
5020
|
+
return {
|
|
5021
|
+
ok: true,
|
|
5022
|
+
executableRaw,
|
|
5023
|
+
executableName: resolvedPath ? path.basename(resolvedPath) : path.basename(executableRaw),
|
|
5024
|
+
resolvedPath,
|
|
5025
|
+
args: tokens.slice(1)
|
|
5026
|
+
};
|
|
5027
|
+
}
|
|
5028
|
+
function hasPathLikeArg(value) {
|
|
5029
|
+
if (!value || value === "-") return false;
|
|
5030
|
+
if (value.startsWith("/") || value.startsWith("./") || value.startsWith("../") || value.startsWith("~/") || /^[A-Za-z]:[\\/]/.test(value)) return true;
|
|
5031
|
+
return value.includes("/") || value.includes("\\");
|
|
5032
|
+
}
|
|
5033
|
+
function isSafeBinUsage(analysis, safeBins) {
|
|
5034
|
+
if (!analysis.ok || !analysis.executableName) return false;
|
|
5035
|
+
const executable = analysis.executableName.toLowerCase();
|
|
5036
|
+
if (!safeBins.has(executable)) return false;
|
|
5037
|
+
for (const arg of analysis.args) {
|
|
5038
|
+
if (arg.startsWith("-")) {
|
|
5039
|
+
const eqIndex = arg.indexOf("=");
|
|
5040
|
+
if (eqIndex > 0 && hasPathLikeArg(arg.slice(eqIndex + 1))) return false;
|
|
5041
|
+
continue;
|
|
5042
|
+
}
|
|
5043
|
+
if (hasPathLikeArg(arg)) return false;
|
|
5044
|
+
}
|
|
5045
|
+
return true;
|
|
5046
|
+
}
|
|
5047
|
+
function matchAllowlistPattern(pattern, executableName, resolvedPath) {
|
|
5048
|
+
const trimmed = pattern.trim();
|
|
5049
|
+
if (!trimmed) return false;
|
|
5050
|
+
const normalizedPattern = normalizePathForMatch(trimmed);
|
|
5051
|
+
const normalizedExecutable = executableName.toLowerCase();
|
|
5052
|
+
const normalizedResolved = resolvedPath ? normalizePathForMatch(resolvedPath) : "";
|
|
5053
|
+
if (!trimmed.includes("/") && !trimmed.includes("\\") && !trimmed.endsWith("*")) return normalizedPattern === normalizedExecutable;
|
|
5054
|
+
if (trimmed.endsWith("*")) {
|
|
5055
|
+
const prefix = normalizePathForMatch(trimmed.slice(0, -1));
|
|
5056
|
+
return normalizedResolved.length > 0 && normalizedResolved.startsWith(prefix) || normalizedExecutable.startsWith(prefix);
|
|
5057
|
+
}
|
|
5058
|
+
if (normalizedResolved.length > 0 && normalizedResolved === normalizedPattern) return true;
|
|
5059
|
+
return normalizedExecutable === normalizedPattern;
|
|
5060
|
+
}
|
|
5061
|
+
function makeCommandApprovalKey(agentId, command) {
|
|
5062
|
+
return crypto.createHash("sha256").update(`${agentId}::${command.trim()}`).digest("hex");
|
|
5063
|
+
}
|
|
5064
|
+
var ExecApprovalsManager = class {
|
|
5065
|
+
filePath;
|
|
5066
|
+
onRequested;
|
|
5067
|
+
onResolved;
|
|
5068
|
+
defaults;
|
|
5069
|
+
pending = /* @__PURE__ */ new Map();
|
|
5070
|
+
oneTimeApprovals = /* @__PURE__ */ new Map();
|
|
5071
|
+
constructor(options) {
|
|
5072
|
+
this.filePath = path.join(options.storeDir, "exec-approvals.json");
|
|
5073
|
+
this.defaults = options.defaults;
|
|
5074
|
+
this.onRequested = options.onRequested;
|
|
5075
|
+
this.onResolved = options.onResolved;
|
|
5076
|
+
}
|
|
5077
|
+
getConfigPath() {
|
|
5078
|
+
return this.filePath;
|
|
5079
|
+
}
|
|
5080
|
+
ensureDir() {
|
|
5081
|
+
fs.mkdirSync(path.dirname(this.filePath), {
|
|
5082
|
+
recursive: true,
|
|
5083
|
+
mode: 448
|
|
5084
|
+
});
|
|
5085
|
+
}
|
|
5086
|
+
readRaw() {
|
|
5087
|
+
if (!fs.existsSync(this.filePath)) return null;
|
|
5088
|
+
const raw = fs.readFileSync(this.filePath, "utf8");
|
|
5089
|
+
return JSON.parse(raw);
|
|
5090
|
+
}
|
|
5091
|
+
writeFile(value) {
|
|
5092
|
+
this.ensureDir();
|
|
5093
|
+
fs.writeFileSync(this.filePath, `${JSON.stringify(value, null, 2)}\n`, {
|
|
5094
|
+
encoding: "utf8",
|
|
5095
|
+
mode: 384
|
|
5096
|
+
});
|
|
5097
|
+
fs.chmodSync(this.filePath, 384);
|
|
5098
|
+
}
|
|
5099
|
+
getSnapshot() {
|
|
5100
|
+
try {
|
|
5101
|
+
return normalizeFile(this.readRaw(), this.defaults);
|
|
5102
|
+
} catch {
|
|
5103
|
+
return normalizeFile(null, this.defaults);
|
|
5104
|
+
}
|
|
5105
|
+
}
|
|
5106
|
+
setSnapshot(next) {
|
|
5107
|
+
const normalized = normalizeFile(next, this.defaults);
|
|
5108
|
+
this.writeFile(normalized);
|
|
5109
|
+
return normalized;
|
|
5110
|
+
}
|
|
5111
|
+
resolveDefaults(policy) {
|
|
5112
|
+
const snapshot = this.getSnapshot();
|
|
5113
|
+
return {
|
|
5114
|
+
security: snapshot.defaults.security ?? policy.security,
|
|
5115
|
+
ask: snapshot.defaults.ask ?? policy.ask
|
|
5116
|
+
};
|
|
5117
|
+
}
|
|
5118
|
+
getAgentAllowlist(agentId, policyAllowlist) {
|
|
5119
|
+
const byAgent = this.getSnapshot().agents[agentId]?.allowlist ?? [];
|
|
5120
|
+
const merged = [];
|
|
5121
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5122
|
+
for (const entry of policyAllowlist) {
|
|
5123
|
+
const pattern = entry.trim();
|
|
5124
|
+
if (!pattern) continue;
|
|
5125
|
+
const key = pattern.toLowerCase();
|
|
5126
|
+
if (seen.has(key)) continue;
|
|
5127
|
+
seen.add(key);
|
|
5128
|
+
merged.push({ pattern });
|
|
5129
|
+
}
|
|
5130
|
+
for (const entry of byAgent) {
|
|
5131
|
+
const pattern = entry.pattern.trim();
|
|
5132
|
+
if (!pattern) continue;
|
|
5133
|
+
const key = pattern.toLowerCase();
|
|
5134
|
+
if (seen.has(key)) continue;
|
|
5135
|
+
seen.add(key);
|
|
5136
|
+
merged.push(entry);
|
|
5137
|
+
}
|
|
5138
|
+
return merged;
|
|
5139
|
+
}
|
|
5140
|
+
addAllowlistEntry(agentId, pattern) {
|
|
5141
|
+
const normalizedPattern = pattern.trim();
|
|
5142
|
+
if (!normalizedPattern) return;
|
|
5143
|
+
const snapshot = this.getSnapshot();
|
|
5144
|
+
const current = snapshot.agents[agentId]?.allowlist ?? [];
|
|
5145
|
+
if (current.some((entry) => entry.pattern.toLowerCase() === normalizedPattern.toLowerCase())) return;
|
|
5146
|
+
const nextAllowlist = [...current, {
|
|
5147
|
+
pattern: normalizedPattern,
|
|
5148
|
+
lastUsedAt: nowIso()
|
|
5149
|
+
}];
|
|
5150
|
+
snapshot.agents[agentId] = { allowlist: nextAllowlist };
|
|
5151
|
+
this.writeFile(snapshot);
|
|
5152
|
+
}
|
|
5153
|
+
recordAllowlistUse(agentId, pattern, command, resolvedPath) {
|
|
5154
|
+
const snapshot = this.getSnapshot();
|
|
5155
|
+
const current = snapshot.agents[agentId]?.allowlist ?? [];
|
|
5156
|
+
const lowered = pattern.toLowerCase();
|
|
5157
|
+
const nextAllowlist = current.map((entry) => {
|
|
5158
|
+
if (entry.pattern.toLowerCase() !== lowered) return entry;
|
|
5159
|
+
return {
|
|
5160
|
+
...entry,
|
|
5161
|
+
lastUsedAt: nowIso(),
|
|
5162
|
+
lastUsedCommand: command,
|
|
5163
|
+
lastResolvedPath: resolvedPath
|
|
5164
|
+
};
|
|
5165
|
+
});
|
|
5166
|
+
snapshot.agents[agentId] = { allowlist: nextAllowlist };
|
|
5167
|
+
this.writeFile(snapshot);
|
|
5168
|
+
}
|
|
5169
|
+
request(params) {
|
|
5170
|
+
const now = Date.now();
|
|
5171
|
+
const timeoutMs = Math.max(5e3, Number.isFinite(params.timeoutMs) ? Math.floor(params.timeoutMs) : DEFAULT_APPROVAL_TIMEOUT_MS);
|
|
5172
|
+
const record = {
|
|
5173
|
+
id: crypto.randomUUID(),
|
|
5174
|
+
createdAtMs: now,
|
|
5175
|
+
expiresAtMs: now + timeoutMs,
|
|
5176
|
+
command: params.command,
|
|
5177
|
+
agentId: params.agentId,
|
|
5178
|
+
sessionKey: params.sessionKey,
|
|
5179
|
+
resolvedPath: params.resolvedPath,
|
|
5180
|
+
timeoutMs,
|
|
5181
|
+
target: params.target
|
|
5182
|
+
};
|
|
5183
|
+
this.pending.set(record.id, {
|
|
5184
|
+
record,
|
|
5185
|
+
onApproved: params.onApproved
|
|
5186
|
+
});
|
|
5187
|
+
this.onRequested?.(record);
|
|
5188
|
+
return record;
|
|
5189
|
+
}
|
|
5190
|
+
getPending(recordId) {
|
|
5191
|
+
const pending = this.pending.get(recordId);
|
|
5192
|
+
if (!pending) return null;
|
|
5193
|
+
if (pending.record.expiresAtMs <= Date.now()) {
|
|
5194
|
+
this.pending.delete(recordId);
|
|
5195
|
+
return null;
|
|
5196
|
+
}
|
|
5197
|
+
return pending.record;
|
|
5198
|
+
}
|
|
5199
|
+
resolve(recordId, decision, resolvedBy) {
|
|
5200
|
+
const pending = this.pending.get(recordId);
|
|
5201
|
+
if (!pending) return null;
|
|
5202
|
+
if (pending.record.expiresAtMs <= Date.now()) {
|
|
5203
|
+
this.pending.delete(recordId);
|
|
5204
|
+
return null;
|
|
5205
|
+
}
|
|
5206
|
+
this.pending.delete(recordId);
|
|
5207
|
+
const resolvedRecord = {
|
|
5208
|
+
...pending.record,
|
|
5209
|
+
decision,
|
|
5210
|
+
resolvedBy,
|
|
5211
|
+
resolvedAtMs: Date.now()
|
|
5212
|
+
};
|
|
5213
|
+
if (decision === "allow-always" && resolvedRecord.resolvedPath) this.addAllowlistEntry(resolvedRecord.agentId, resolvedRecord.resolvedPath);
|
|
5214
|
+
if (decision === "allow-once") {
|
|
5215
|
+
const key = makeCommandApprovalKey(resolvedRecord.agentId, resolvedRecord.command);
|
|
5216
|
+
this.oneTimeApprovals.set(key, {
|
|
5217
|
+
key,
|
|
5218
|
+
expiresAtMs: Date.now() + ONE_TIME_APPROVAL_TTL_MS
|
|
5219
|
+
});
|
|
5220
|
+
}
|
|
5221
|
+
this.onResolved?.(resolvedRecord);
|
|
5222
|
+
if ((decision === "allow-once" || decision === "allow-always") && pending.onApproved) pending.onApproved(resolvedRecord).catch(() => {});
|
|
5223
|
+
return resolvedRecord;
|
|
5224
|
+
}
|
|
5225
|
+
consumeOneTimeApproval(agentId, command) {
|
|
5226
|
+
const key = makeCommandApprovalKey(agentId, command);
|
|
5227
|
+
const pending = this.oneTimeApprovals.get(key);
|
|
5228
|
+
if (!pending) return false;
|
|
5229
|
+
this.oneTimeApprovals.delete(key);
|
|
5230
|
+
if (pending.expiresAtMs <= Date.now()) return false;
|
|
5231
|
+
return true;
|
|
5232
|
+
}
|
|
5233
|
+
};
|
|
5234
|
+
function evaluateExecPolicy(params) {
|
|
5235
|
+
const cwd = params.cwd || process.cwd();
|
|
5236
|
+
const env = params.env || process.env;
|
|
5237
|
+
const analysis = analyzeCommand(params.command, cwd, env);
|
|
5238
|
+
const defaults = params.manager.resolveDefaults(params.policy);
|
|
5239
|
+
const security = defaults.security;
|
|
5240
|
+
const ask = defaults.ask;
|
|
5241
|
+
if (!analysis.ok) return {
|
|
5242
|
+
analysisOk: false,
|
|
5243
|
+
reason: analysis.reason,
|
|
5244
|
+
executableName: analysis.executableName,
|
|
5245
|
+
resolvedPath: analysis.resolvedPath,
|
|
5246
|
+
allowlistSatisfied: false,
|
|
5247
|
+
safeBinSatisfied: false,
|
|
5248
|
+
requiresApproval: false,
|
|
5249
|
+
denied: true,
|
|
5250
|
+
deniedReason: analysis.reason ? `exec denied: ${analysis.reason}` : "exec denied: invalid command"
|
|
5251
|
+
};
|
|
5252
|
+
if (security === "deny") return {
|
|
5253
|
+
analysisOk: analysis.ok,
|
|
5254
|
+
reason: analysis.reason,
|
|
5255
|
+
executableName: analysis.executableName,
|
|
5256
|
+
resolvedPath: analysis.resolvedPath,
|
|
5257
|
+
allowlistSatisfied: false,
|
|
5258
|
+
safeBinSatisfied: false,
|
|
5259
|
+
requiresApproval: false,
|
|
5260
|
+
denied: true,
|
|
5261
|
+
deniedReason: "exec denied: security=deny"
|
|
5262
|
+
};
|
|
5263
|
+
const mergedAllowlist = params.manager.getAgentAllowlist(params.agentId, params.policy.allowlist);
|
|
5264
|
+
const safeBins = new Set(params.policy.safeBins.map((entry) => entry.trim().toLowerCase()).filter(Boolean));
|
|
5265
|
+
const oneTimeApproved = params.manager.consumeOneTimeApproval(params.agentId, params.command);
|
|
5266
|
+
const safeBinSatisfied = isSafeBinUsage(analysis, safeBins);
|
|
5267
|
+
let allowlistSatisfied = false;
|
|
5268
|
+
let allowlistPattern;
|
|
5269
|
+
if (analysis.ok && analysis.executableName) if (oneTimeApproved) {
|
|
5270
|
+
allowlistSatisfied = true;
|
|
5271
|
+
allowlistPattern = "one-time-approval";
|
|
5272
|
+
} else {
|
|
5273
|
+
for (const entry of mergedAllowlist) if (matchAllowlistPattern(entry.pattern, analysis.executableName, analysis.resolvedPath)) {
|
|
5274
|
+
allowlistSatisfied = true;
|
|
5275
|
+
allowlistPattern = entry.pattern;
|
|
5276
|
+
break;
|
|
5277
|
+
}
|
|
5278
|
+
if (!allowlistSatisfied && safeBinSatisfied) {
|
|
5279
|
+
allowlistSatisfied = true;
|
|
5280
|
+
allowlistPattern = `safe-bin:${analysis.executableName.toLowerCase()}`;
|
|
5281
|
+
}
|
|
5282
|
+
}
|
|
5283
|
+
if (security === "full") {
|
|
5284
|
+
if (ask === "always") return {
|
|
5285
|
+
analysisOk: analysis.ok,
|
|
5286
|
+
reason: analysis.reason,
|
|
5287
|
+
executableName: analysis.executableName,
|
|
5288
|
+
resolvedPath: analysis.resolvedPath,
|
|
5289
|
+
allowlistSatisfied,
|
|
5290
|
+
allowlistPattern,
|
|
5291
|
+
safeBinSatisfied,
|
|
5292
|
+
requiresApproval: true,
|
|
5293
|
+
denied: false
|
|
5294
|
+
};
|
|
5295
|
+
return {
|
|
5296
|
+
analysisOk: analysis.ok,
|
|
5297
|
+
reason: analysis.reason,
|
|
5298
|
+
executableName: analysis.executableName,
|
|
5299
|
+
resolvedPath: analysis.resolvedPath,
|
|
5300
|
+
allowlistSatisfied,
|
|
5301
|
+
allowlistPattern,
|
|
5302
|
+
safeBinSatisfied,
|
|
5303
|
+
requiresApproval: false,
|
|
5304
|
+
denied: false
|
|
5305
|
+
};
|
|
5306
|
+
}
|
|
5307
|
+
if (allowlistSatisfied && ask !== "always") return {
|
|
5308
|
+
analysisOk: analysis.ok,
|
|
5309
|
+
reason: analysis.reason,
|
|
5310
|
+
executableName: analysis.executableName,
|
|
5311
|
+
resolvedPath: analysis.resolvedPath,
|
|
5312
|
+
allowlistSatisfied,
|
|
5313
|
+
allowlistPattern,
|
|
5314
|
+
safeBinSatisfied,
|
|
5315
|
+
requiresApproval: false,
|
|
5316
|
+
denied: false
|
|
5317
|
+
};
|
|
5318
|
+
if (ask === "off") return {
|
|
5319
|
+
analysisOk: analysis.ok,
|
|
5320
|
+
reason: analysis.reason,
|
|
5321
|
+
executableName: analysis.executableName,
|
|
5322
|
+
resolvedPath: analysis.resolvedPath,
|
|
5323
|
+
allowlistSatisfied,
|
|
5324
|
+
allowlistPattern,
|
|
5325
|
+
safeBinSatisfied,
|
|
5326
|
+
requiresApproval: false,
|
|
5327
|
+
denied: true,
|
|
5328
|
+
deniedReason: allowlistSatisfied ? "exec denied: ask=off" : "exec denied: allowlist miss"
|
|
5329
|
+
};
|
|
5330
|
+
return {
|
|
5331
|
+
analysisOk: analysis.ok,
|
|
5332
|
+
reason: analysis.reason,
|
|
5333
|
+
executableName: analysis.executableName,
|
|
5334
|
+
resolvedPath: analysis.resolvedPath,
|
|
5335
|
+
allowlistSatisfied,
|
|
5336
|
+
allowlistPattern,
|
|
5337
|
+
safeBinSatisfied,
|
|
5338
|
+
requiresApproval: true,
|
|
5339
|
+
denied: false
|
|
5340
|
+
};
|
|
5341
|
+
}
|
|
5342
|
+
|
|
5343
|
+
//#endregion
|
|
5344
|
+
//#region src/exec-chat.ts
|
|
5345
|
+
const APPROVE_USAGE = "Usage: /approve <id> allow-once|allow-always|deny";
|
|
5346
|
+
const BASH_USAGE = "Usage: /bash <command>";
|
|
5347
|
+
function normalizeDecision(value) {
|
|
5348
|
+
const normalized = value.trim().toLowerCase();
|
|
5349
|
+
if (normalized === "allow-once" || normalized === "allow-always" || normalized === "deny") return normalized;
|
|
5350
|
+
return null;
|
|
5351
|
+
}
|
|
5352
|
+
function formatExecOutput$1(result) {
|
|
5353
|
+
const lines = [
|
|
5354
|
+
`exitCode: ${result.exitCode}`,
|
|
5355
|
+
result.timedOut ? "timedOut: true" : "timedOut: false",
|
|
5356
|
+
result.truncated ? "truncated: true" : "truncated: false",
|
|
5357
|
+
""
|
|
5358
|
+
];
|
|
5359
|
+
if (result.stdout) {
|
|
5360
|
+
lines.push("stdout:");
|
|
5361
|
+
lines.push(result.stdout);
|
|
5362
|
+
lines.push("");
|
|
5363
|
+
}
|
|
5364
|
+
if (result.stderr) {
|
|
5365
|
+
lines.push("stderr:");
|
|
5366
|
+
lines.push(result.stderr);
|
|
5367
|
+
}
|
|
5368
|
+
if (!result.stdout && !result.stderr) lines.push("No output.");
|
|
5369
|
+
return lines.join("\n").trim();
|
|
5370
|
+
}
|
|
5371
|
+
async function executeApprovedCommand(options) {
|
|
5372
|
+
const result = await options.runtime.exec(options.command, { timeoutMs: options.timeoutMs });
|
|
5373
|
+
await options.channel.sendMessage(options.target, [
|
|
5374
|
+
`Approval granted. Executed: ${options.command}`,
|
|
5375
|
+
"",
|
|
5376
|
+
formatExecOutput$1(result)
|
|
5377
|
+
].join("\n"));
|
|
5378
|
+
}
|
|
5379
|
+
async function handleExecChatCommand(options) {
|
|
5380
|
+
const trimmed = options.msg.text.trim();
|
|
5381
|
+
if (!trimmed.startsWith("/")) return { handled: false };
|
|
5382
|
+
const parsed = trimmed.split(/\s+/);
|
|
5383
|
+
const command = (parsed[0] || "").toLowerCase();
|
|
5384
|
+
if (command === "/approve") {
|
|
5385
|
+
if (!options.commandAuthorized) {
|
|
5386
|
+
await options.channel.sendMessage(options.target, "You are not authorized to use this command.");
|
|
5387
|
+
return { handled: true };
|
|
5388
|
+
}
|
|
5389
|
+
const approvalId = parsed[1]?.trim();
|
|
5390
|
+
const decision = parsed[2] ? normalizeDecision(parsed[2]) : null;
|
|
5391
|
+
if (!approvalId || !decision) {
|
|
5392
|
+
await options.channel.sendMessage(options.target, APPROVE_USAGE);
|
|
5393
|
+
return { handled: true };
|
|
5394
|
+
}
|
|
5395
|
+
if (!options.execApprovals.resolve(approvalId, decision, options.msg.userId)) {
|
|
5396
|
+
await options.channel.sendMessage(options.target, `Unknown or expired approval id: ${approvalId}`);
|
|
5397
|
+
return { handled: true };
|
|
5398
|
+
}
|
|
5399
|
+
await options.channel.sendMessage(options.target, `Approval ${decision} recorded for ${approvalId}.`);
|
|
5400
|
+
options.audit({
|
|
5401
|
+
sessionKey: options.sessionKey,
|
|
5402
|
+
actor: "channel",
|
|
5403
|
+
eventType: "exec.approval.resolve",
|
|
5404
|
+
payload: {
|
|
5405
|
+
id: approvalId,
|
|
5406
|
+
decision,
|
|
5407
|
+
resolvedBy: options.msg.userId
|
|
5408
|
+
}
|
|
5409
|
+
});
|
|
5410
|
+
return { handled: true };
|
|
5411
|
+
}
|
|
5412
|
+
if (command !== "/bash") return { handled: false };
|
|
5413
|
+
if (!options.execPolicy.enabled) {
|
|
5414
|
+
await options.channel.sendMessage(options.target, "/bash is disabled. Set runtime.tools.exec.enabled=true (or runtime.tools.bashEnabled=true) to enable.");
|
|
5415
|
+
return { handled: true };
|
|
5416
|
+
}
|
|
5417
|
+
if (!options.commandAuthorized) {
|
|
5418
|
+
await options.channel.sendMessage(options.target, "You are not authorized to use this command.");
|
|
5419
|
+
return { handled: true };
|
|
5420
|
+
}
|
|
5421
|
+
if (!options.msg.text.slice(5).trim()) {
|
|
5422
|
+
await options.channel.sendMessage(options.target, BASH_USAGE);
|
|
5423
|
+
return { handled: true };
|
|
5424
|
+
}
|
|
5425
|
+
const bashCommand = options.msg.text.slice(5).trim();
|
|
5426
|
+
const evaluation = evaluateExecPolicy({
|
|
5427
|
+
command: bashCommand,
|
|
5428
|
+
agentId: options.agentId,
|
|
5429
|
+
policy: options.execPolicy,
|
|
5430
|
+
manager: options.execApprovals,
|
|
5431
|
+
cwd: process.cwd(),
|
|
5432
|
+
env: process.env
|
|
5433
|
+
});
|
|
5434
|
+
options.audit({
|
|
5435
|
+
sessionKey: options.sessionKey,
|
|
5436
|
+
actor: "channel",
|
|
5437
|
+
eventType: "exec.chat.request",
|
|
5438
|
+
payload: {
|
|
5439
|
+
command: bashCommand,
|
|
5440
|
+
security: options.execPolicy.security,
|
|
5441
|
+
ask: options.execPolicy.ask,
|
|
5442
|
+
allowlistSatisfied: evaluation.allowlistSatisfied,
|
|
5443
|
+
requiresApproval: evaluation.requiresApproval
|
|
5444
|
+
}
|
|
5445
|
+
});
|
|
5446
|
+
if (evaluation.denied) {
|
|
5447
|
+
await options.channel.sendMessage(options.target, evaluation.deniedReason || "Command denied by policy.");
|
|
5448
|
+
return { handled: true };
|
|
5449
|
+
}
|
|
5450
|
+
if (evaluation.requiresApproval) {
|
|
5451
|
+
const approval = options.execApprovals.request({
|
|
5452
|
+
command: bashCommand,
|
|
5453
|
+
agentId: options.agentId,
|
|
5454
|
+
sessionKey: options.sessionKey,
|
|
5455
|
+
resolvedPath: evaluation.resolvedPath,
|
|
5456
|
+
timeoutMs: options.execPolicy.approvalTimeoutMs,
|
|
5457
|
+
target: {
|
|
5458
|
+
channel: options.target.channel,
|
|
5459
|
+
chatId: options.target.chatId,
|
|
5460
|
+
accountId: options.target.accountId
|
|
5461
|
+
},
|
|
5462
|
+
onApproved: async (record) => {
|
|
5463
|
+
if (record.decision === "deny") return;
|
|
5464
|
+
await executeApprovedCommand({
|
|
5465
|
+
runtime: options.runtime,
|
|
5466
|
+
channel: options.channel,
|
|
5467
|
+
target: options.target,
|
|
5468
|
+
command: bashCommand,
|
|
5469
|
+
timeoutMs: options.execPolicy.approvalTimeoutMs
|
|
5470
|
+
});
|
|
5471
|
+
}
|
|
5472
|
+
});
|
|
5473
|
+
await options.channel.sendMessage(options.target, [
|
|
5474
|
+
"Approval required for this command.",
|
|
5475
|
+
`id: ${approval.id}`,
|
|
5476
|
+
`expiresAt: ${new Date(approval.expiresAtMs).toISOString()}`,
|
|
5477
|
+
"",
|
|
5478
|
+
`Approve with: /approve ${approval.id} allow-once|allow-always|deny`
|
|
5479
|
+
].join("\n"));
|
|
5480
|
+
return { handled: true };
|
|
5481
|
+
}
|
|
5482
|
+
try {
|
|
5483
|
+
const result = await options.runtime.exec(bashCommand, { timeoutMs: options.execPolicy.approvalTimeoutMs });
|
|
5484
|
+
if (evaluation.allowlistPattern && !evaluation.allowlistPattern.startsWith("safe-bin:") && evaluation.allowlistPattern !== "one-time-approval") options.execApprovals.recordAllowlistUse(options.agentId, evaluation.allowlistPattern, bashCommand, evaluation.resolvedPath);
|
|
5485
|
+
await options.channel.sendMessage(options.target, formatExecOutput$1(result));
|
|
5486
|
+
} catch (error) {
|
|
5487
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5488
|
+
await options.channel.sendMessage(options.target, `Command failed: ${message}`);
|
|
5489
|
+
}
|
|
5490
|
+
return { handled: true };
|
|
5491
|
+
}
|
|
5492
|
+
|
|
4914
5493
|
//#endregion
|
|
4915
5494
|
//#region src/gateway/methods/agent.ts
|
|
4916
5495
|
const agentParamsSchema = z.object({
|
|
@@ -5083,14 +5662,12 @@ const configGetMethod = async (_params, context) => {
|
|
|
5083
5662
|
const configSetMethod = async (params, context) => {
|
|
5084
5663
|
const parsed = configSetParamsSchema.parse(params);
|
|
5085
5664
|
context.writeFileConfig(parsed.config);
|
|
5086
|
-
writeOpenClawMirror(loadConfig());
|
|
5087
5665
|
return { ok: true };
|
|
5088
5666
|
};
|
|
5089
5667
|
const configPatchMethod = async (params, context) => {
|
|
5090
5668
|
const parsed = configPatchParamsSchema.parse(params);
|
|
5091
5669
|
const merged = deepMerge(context.readFileConfig(), parsed.patch);
|
|
5092
5670
|
context.writeFileConfig(merged);
|
|
5093
|
-
writeOpenClawMirror(loadConfig());
|
|
5094
5671
|
return { ok: true };
|
|
5095
5672
|
};
|
|
5096
5673
|
|
|
@@ -5109,6 +5686,68 @@ const cronStatusMethod = async (_params, context) => {
|
|
|
5109
5686
|
};
|
|
5110
5687
|
};
|
|
5111
5688
|
|
|
5689
|
+
//#endregion
|
|
5690
|
+
//#region src/gateway/methods/exec-approvals.ts
|
|
5691
|
+
const requestParamsSchema = z.object({
|
|
5692
|
+
command: z.string().min(1),
|
|
5693
|
+
agentId: z.string().min(1).optional(),
|
|
5694
|
+
sessionKey: z.string().min(1).optional(),
|
|
5695
|
+
resolvedPath: z.string().min(1).optional(),
|
|
5696
|
+
timeoutMs: z.number().int().positive().optional()
|
|
5697
|
+
});
|
|
5698
|
+
const resolveParamsSchema = z.object({
|
|
5699
|
+
id: z.string().min(1),
|
|
5700
|
+
decision: z.enum([
|
|
5701
|
+
"allow-once",
|
|
5702
|
+
"allow-always",
|
|
5703
|
+
"deny"
|
|
5704
|
+
]),
|
|
5705
|
+
resolvedBy: z.string().min(1).optional()
|
|
5706
|
+
});
|
|
5707
|
+
const setParamsSchema = z.object({ file: z.unknown() });
|
|
5708
|
+
const execApprovalRequestMethod = async (params, context) => {
|
|
5709
|
+
const parsed = requestParamsSchema.parse(params);
|
|
5710
|
+
const record = context.execApprovals.request({
|
|
5711
|
+
command: parsed.command,
|
|
5712
|
+
agentId: parsed.agentId ?? "main",
|
|
5713
|
+
sessionKey: parsed.sessionKey,
|
|
5714
|
+
resolvedPath: parsed.resolvedPath,
|
|
5715
|
+
timeoutMs: parsed.timeoutMs
|
|
5716
|
+
});
|
|
5717
|
+
return {
|
|
5718
|
+
id: record.id,
|
|
5719
|
+
createdAtMs: record.createdAtMs,
|
|
5720
|
+
expiresAtMs: record.expiresAtMs,
|
|
5721
|
+
command: record.command,
|
|
5722
|
+
agentId: record.agentId
|
|
5723
|
+
};
|
|
5724
|
+
};
|
|
5725
|
+
const execApprovalResolveMethod = async (params, context) => {
|
|
5726
|
+
const parsed = resolveParamsSchema.parse(params);
|
|
5727
|
+
const record = context.execApprovals.resolve(parsed.id, parsed.decision, parsed.resolvedBy);
|
|
5728
|
+
if (!record) throw new Error(`Unknown or expired approval id: ${parsed.id}`);
|
|
5729
|
+
return {
|
|
5730
|
+
ok: true,
|
|
5731
|
+
id: record.id,
|
|
5732
|
+
decision: parsed.decision,
|
|
5733
|
+
resolvedBy: parsed.resolvedBy ?? null,
|
|
5734
|
+
resolvedAtMs: record.resolvedAtMs ?? Date.now()
|
|
5735
|
+
};
|
|
5736
|
+
};
|
|
5737
|
+
const execApprovalsGetMethod = async (_params, context) => {
|
|
5738
|
+
return {
|
|
5739
|
+
path: context.execApprovals.getConfigPath(),
|
|
5740
|
+
file: context.execApprovals.getSnapshot()
|
|
5741
|
+
};
|
|
5742
|
+
};
|
|
5743
|
+
const execApprovalsSetMethod = async (params, context) => {
|
|
5744
|
+
const parsed = setParamsSchema.parse(params);
|
|
5745
|
+
return {
|
|
5746
|
+
ok: true,
|
|
5747
|
+
file: context.execApprovals.setSnapshot(parsed.file)
|
|
5748
|
+
};
|
|
5749
|
+
};
|
|
5750
|
+
|
|
5112
5751
|
//#endregion
|
|
5113
5752
|
//#region src/gateway/methods/health.ts
|
|
5114
5753
|
const healthMethod = async (_params, context) => {
|
|
@@ -5347,14 +5986,20 @@ const gatewayMethodHandlers = {
|
|
|
5347
5986
|
"chat.abort": chatAbortMethod,
|
|
5348
5987
|
"cron.list": cronListMethod,
|
|
5349
5988
|
"cron.status": cronStatusMethod,
|
|
5350
|
-
"logs.tail": logsTailMethod
|
|
5989
|
+
"logs.tail": logsTailMethod,
|
|
5990
|
+
"exec.approval.request": execApprovalRequestMethod,
|
|
5991
|
+
"exec.approval.resolve": execApprovalResolveMethod,
|
|
5992
|
+
"exec.approvals.get": execApprovalsGetMethod,
|
|
5993
|
+
"exec.approvals.set": execApprovalsSetMethod
|
|
5351
5994
|
};
|
|
5352
5995
|
const gatewayEventNames = [
|
|
5353
5996
|
"tick",
|
|
5354
5997
|
"health",
|
|
5355
5998
|
"agent",
|
|
5356
5999
|
"chat",
|
|
5357
|
-
"shutdown"
|
|
6000
|
+
"shutdown",
|
|
6001
|
+
"exec.approval.requested",
|
|
6002
|
+
"exec.approval.resolved"
|
|
5358
6003
|
];
|
|
5359
6004
|
|
|
5360
6005
|
//#endregion
|
|
@@ -5570,6 +6215,7 @@ function resolveUiDirectory() {
|
|
|
5570
6215
|
}
|
|
5571
6216
|
var HovClawGatewayServer = class {
|
|
5572
6217
|
db;
|
|
6218
|
+
execApprovals;
|
|
5573
6219
|
agentManager;
|
|
5574
6220
|
channels;
|
|
5575
6221
|
readFileConfig;
|
|
@@ -5585,6 +6231,7 @@ var HovClawGatewayServer = class {
|
|
|
5585
6231
|
constructor(options) {
|
|
5586
6232
|
this.runtimeConfig = options.config;
|
|
5587
6233
|
this.db = options.db;
|
|
6234
|
+
this.execApprovals = options.execApprovals;
|
|
5588
6235
|
this.agentManager = options.agentManager;
|
|
5589
6236
|
this.channels = options.channels;
|
|
5590
6237
|
this.readFileConfig = options.readFileConfig;
|
|
@@ -5858,6 +6505,7 @@ var HovClawGatewayServer = class {
|
|
|
5858
6505
|
const context = {
|
|
5859
6506
|
config: this.runtimeConfig,
|
|
5860
6507
|
db: this.db,
|
|
6508
|
+
execApprovals: this.execApprovals,
|
|
5861
6509
|
agentManager: this.agentManager,
|
|
5862
6510
|
channels: this.channels,
|
|
5863
6511
|
startedAt: this.startedAt,
|
|
@@ -6851,150 +7499,386 @@ function normalizeErrorMessage(error) {
|
|
|
6851
7499
|
if (error instanceof Error) return error.message;
|
|
6852
7500
|
return String(error);
|
|
6853
7501
|
}
|
|
6854
|
-
function
|
|
7502
|
+
function resolveExecAgentId() {
|
|
7503
|
+
const context = getRuntimeSessionContext();
|
|
7504
|
+
if (context?.agentName?.trim()) return context.agentName.trim();
|
|
7505
|
+
if (context?.sessionKey?.trim()) {
|
|
7506
|
+
const first = context.sessionKey.split(":")[0];
|
|
7507
|
+
if (first?.trim()) return first.trim();
|
|
7508
|
+
}
|
|
7509
|
+
return "main";
|
|
7510
|
+
}
|
|
7511
|
+
function formatExecOutput(result) {
|
|
7512
|
+
return [
|
|
7513
|
+
`exitCode: ${result.exitCode}`,
|
|
7514
|
+
result.timedOut ? "timedOut: true" : "timedOut: false",
|
|
7515
|
+
result.truncated ? "truncated: true" : "truncated: false",
|
|
7516
|
+
"",
|
|
7517
|
+
result.stdout ? `stdout:\n${result.stdout}` : "stdout: <empty>",
|
|
7518
|
+
"",
|
|
7519
|
+
result.stderr ? `stderr:\n${result.stderr}` : "stderr: <empty>"
|
|
7520
|
+
].join("\n");
|
|
7521
|
+
}
|
|
7522
|
+
function firstToken(command) {
|
|
7523
|
+
const trimmed = command.trim();
|
|
7524
|
+
if (!trimmed) return "";
|
|
7525
|
+
return trimmed.split(/\s+/)[0] ?? "";
|
|
7526
|
+
}
|
|
7527
|
+
function diagnosticsCommandsForPlatform(platform) {
|
|
7528
|
+
const common = [
|
|
7529
|
+
{
|
|
7530
|
+
key: "os",
|
|
7531
|
+
command: "uname -a"
|
|
7532
|
+
},
|
|
7533
|
+
{
|
|
7534
|
+
key: "uptime",
|
|
7535
|
+
command: "uptime"
|
|
7536
|
+
},
|
|
7537
|
+
{
|
|
7538
|
+
key: "disk",
|
|
7539
|
+
command: "df -h"
|
|
7540
|
+
},
|
|
7541
|
+
{
|
|
7542
|
+
key: "processes",
|
|
7543
|
+
command: "ps -A"
|
|
7544
|
+
}
|
|
7545
|
+
];
|
|
7546
|
+
if (platform === "darwin") return [
|
|
7547
|
+
...common,
|
|
7548
|
+
{
|
|
7549
|
+
key: "memory",
|
|
7550
|
+
command: "vm_stat"
|
|
7551
|
+
},
|
|
7552
|
+
{
|
|
7553
|
+
key: "cpu",
|
|
7554
|
+
command: "sysctl -n machdep.cpu.brand_string"
|
|
7555
|
+
},
|
|
7556
|
+
{
|
|
7557
|
+
key: "network",
|
|
7558
|
+
command: "ifconfig"
|
|
7559
|
+
}
|
|
7560
|
+
];
|
|
7561
|
+
if (platform === "linux") return [
|
|
7562
|
+
...common,
|
|
7563
|
+
{
|
|
7564
|
+
key: "memory",
|
|
7565
|
+
command: "free -m"
|
|
7566
|
+
},
|
|
7567
|
+
{
|
|
7568
|
+
key: "cpu",
|
|
7569
|
+
command: "lscpu"
|
|
7570
|
+
},
|
|
7571
|
+
{
|
|
7572
|
+
key: "network",
|
|
7573
|
+
command: "ip addr"
|
|
7574
|
+
}
|
|
7575
|
+
];
|
|
7576
|
+
if (platform === "win32") return [
|
|
7577
|
+
{
|
|
7578
|
+
key: "os",
|
|
7579
|
+
command: "ver"
|
|
7580
|
+
},
|
|
7581
|
+
{
|
|
7582
|
+
key: "uptime",
|
|
7583
|
+
command: "net stats workstation"
|
|
7584
|
+
},
|
|
7585
|
+
{
|
|
7586
|
+
key: "disk",
|
|
7587
|
+
command: "wmic logicaldisk get size,freespace,caption"
|
|
7588
|
+
},
|
|
7589
|
+
{
|
|
7590
|
+
key: "processes",
|
|
7591
|
+
command: "tasklist"
|
|
7592
|
+
},
|
|
7593
|
+
{
|
|
7594
|
+
key: "cpu",
|
|
7595
|
+
command: "wmic cpu get name"
|
|
7596
|
+
},
|
|
7597
|
+
{
|
|
7598
|
+
key: "memory",
|
|
7599
|
+
command: "wmic os get totalvisiblememorysize,freephysicalmemory"
|
|
7600
|
+
},
|
|
7601
|
+
{
|
|
7602
|
+
key: "network",
|
|
7603
|
+
command: "ipconfig"
|
|
7604
|
+
}
|
|
7605
|
+
];
|
|
7606
|
+
return common;
|
|
7607
|
+
}
|
|
7608
|
+
function createTools({ runtime, audit, execPolicy, execApprovals }) {
|
|
6855
7609
|
const parser = new Parser();
|
|
6856
|
-
const
|
|
6857
|
-
|
|
6858
|
-
|
|
6859
|
-
|
|
6860
|
-
|
|
6861
|
-
|
|
6862
|
-
|
|
6863
|
-
|
|
6864
|
-
|
|
6865
|
-
|
|
6866
|
-
|
|
6867
|
-
|
|
6868
|
-
|
|
6869
|
-
|
|
6870
|
-
|
|
6871
|
-
|
|
7610
|
+
const execSchema = Type.Object({
|
|
7611
|
+
command: Type.String(),
|
|
7612
|
+
timeoutMs: Type.Optional(Type.Number({
|
|
7613
|
+
minimum: 1e3,
|
|
7614
|
+
maximum: 12e4
|
|
7615
|
+
}))
|
|
7616
|
+
});
|
|
7617
|
+
async function executeCommandWithPolicy(toolName, params) {
|
|
7618
|
+
const command = params.command.trim();
|
|
7619
|
+
if (!command) throw new Error("Command cannot be empty.");
|
|
7620
|
+
const sessionContext = getRuntimeSessionContext();
|
|
7621
|
+
const agentId = resolveExecAgentId();
|
|
7622
|
+
const evaluation = evaluateExecPolicy({
|
|
7623
|
+
command,
|
|
7624
|
+
agentId,
|
|
7625
|
+
policy: execPolicy,
|
|
7626
|
+
manager: execApprovals,
|
|
7627
|
+
cwd: sessionContext?.workspaceDir,
|
|
7628
|
+
env: process.env
|
|
7629
|
+
});
|
|
7630
|
+
audit({
|
|
7631
|
+
sessionKey: sessionContext?.sessionKey,
|
|
7632
|
+
actor: "tool",
|
|
7633
|
+
eventType: "tool.exec",
|
|
7634
|
+
payload: {
|
|
7635
|
+
toolName,
|
|
7636
|
+
command,
|
|
7637
|
+
security: execPolicy.security,
|
|
7638
|
+
ask: execPolicy.ask,
|
|
7639
|
+
allowlistSatisfied: evaluation.allowlistSatisfied,
|
|
7640
|
+
safeBinSatisfied: evaluation.safeBinSatisfied,
|
|
7641
|
+
analysisOk: evaluation.analysisOk,
|
|
7642
|
+
requiresApproval: evaluation.requiresApproval
|
|
7643
|
+
}
|
|
7644
|
+
});
|
|
7645
|
+
if (evaluation.denied) throw new Error(evaluation.deniedReason || "exec denied by policy");
|
|
7646
|
+
if (evaluation.requiresApproval) {
|
|
7647
|
+
const approval = execApprovals.request({
|
|
7648
|
+
command,
|
|
7649
|
+
agentId,
|
|
7650
|
+
sessionKey: sessionContext?.sessionKey,
|
|
7651
|
+
resolvedPath: evaluation.resolvedPath,
|
|
7652
|
+
timeoutMs: execPolicy.approvalTimeoutMs
|
|
6872
7653
|
});
|
|
6873
|
-
const result = await runtime.exec(params.command, { timeoutMs: params.timeoutMs });
|
|
6874
7654
|
return textResult([
|
|
6875
|
-
`
|
|
6876
|
-
|
|
6877
|
-
|
|
6878
|
-
"",
|
|
6879
|
-
result.stdout ? `stdout:\n${result.stdout}` : "stdout: <empty>",
|
|
7655
|
+
`approvalRequired: true`,
|
|
7656
|
+
`approvalId: ${approval.id}`,
|
|
7657
|
+
`expiresAtMs: ${approval.expiresAtMs}`,
|
|
6880
7658
|
"",
|
|
6881
|
-
|
|
6882
|
-
].join("\n"),
|
|
6883
|
-
|
|
6884
|
-
|
|
6885
|
-
|
|
6886
|
-
|
|
6887
|
-
|
|
6888
|
-
|
|
6889
|
-
parameters: Type.Object({
|
|
6890
|
-
path: Type.String(),
|
|
6891
|
-
maxBytes: Type.Optional(Type.Number({
|
|
6892
|
-
minimum: 128,
|
|
6893
|
-
maximum: 1e6
|
|
6894
|
-
}))
|
|
6895
|
-
}),
|
|
6896
|
-
execute: async (_toolCallId, params) => {
|
|
6897
|
-
audit({
|
|
6898
|
-
actor: "tool",
|
|
6899
|
-
eventType: "tool.read_file",
|
|
6900
|
-
payload: { path: params.path }
|
|
7659
|
+
"Run /approve <id> allow-once|allow-always|deny, then retry the command."
|
|
7660
|
+
].join("\n"), {
|
|
7661
|
+
status: "approval-pending",
|
|
7662
|
+
approvalId: approval.id,
|
|
7663
|
+
expiresAtMs: approval.expiresAtMs,
|
|
7664
|
+
command,
|
|
7665
|
+
resolvedPath: evaluation.resolvedPath,
|
|
7666
|
+
analysisOk: evaluation.analysisOk
|
|
6901
7667
|
});
|
|
6902
|
-
const result = await runtime.readFile(params.path, { maxOutputBytes: params.maxBytes });
|
|
6903
|
-
return textResult(result.content, result);
|
|
6904
7668
|
}
|
|
6905
|
-
|
|
6906
|
-
|
|
6907
|
-
|
|
6908
|
-
|
|
6909
|
-
|
|
6910
|
-
|
|
6911
|
-
|
|
6912
|
-
|
|
6913
|
-
|
|
7669
|
+
const result = await runtime.exec(command, { timeoutMs: params.timeoutMs });
|
|
7670
|
+
if (evaluation.allowlistPattern && !evaluation.allowlistPattern.startsWith("safe-bin:") && evaluation.allowlistPattern !== "one-time-approval") execApprovals.recordAllowlistUse(agentId, evaluation.allowlistPattern, command, evaluation.resolvedPath);
|
|
7671
|
+
return textResult(formatExecOutput(result), {
|
|
7672
|
+
...result,
|
|
7673
|
+
command,
|
|
7674
|
+
resolvedPath: evaluation.resolvedPath
|
|
7675
|
+
});
|
|
7676
|
+
}
|
|
7677
|
+
const execTool = {
|
|
7678
|
+
name: "exec",
|
|
7679
|
+
label: "Exec",
|
|
7680
|
+
description: "Run a shell command with approval and allowlist policy.",
|
|
7681
|
+
parameters: execSchema,
|
|
6914
7682
|
execute: async (_toolCallId, params) => {
|
|
6915
|
-
|
|
6916
|
-
actor: "tool",
|
|
6917
|
-
eventType: "tool.write_file",
|
|
6918
|
-
payload: {
|
|
6919
|
-
path: params.path,
|
|
6920
|
-
bytes: Buffer.byteLength(params.content, "utf8")
|
|
6921
|
-
}
|
|
6922
|
-
});
|
|
6923
|
-
const result = await runtime.writeFile(params.path, params.content);
|
|
6924
|
-
return textResult(`Wrote ${result.bytesWritten} bytes to ${params.path}.`, result);
|
|
7683
|
+
return executeCommandWithPolicy("exec", params);
|
|
6925
7684
|
}
|
|
6926
7685
|
};
|
|
6927
|
-
const
|
|
6928
|
-
name: "
|
|
6929
|
-
label: "
|
|
6930
|
-
description: "
|
|
6931
|
-
parameters:
|
|
7686
|
+
const bashTool = {
|
|
7687
|
+
name: "bash",
|
|
7688
|
+
label: "Bash",
|
|
7689
|
+
description: "Compatibility alias for exec tool policy.",
|
|
7690
|
+
parameters: execSchema,
|
|
6932
7691
|
execute: async (_toolCallId, params) => {
|
|
6933
|
-
|
|
6934
|
-
actor: "tool",
|
|
6935
|
-
eventType: "tool.fetch_web",
|
|
6936
|
-
payload: { url: params.url }
|
|
6937
|
-
});
|
|
6938
|
-
const result = await runtime.fetchWeb(params.url);
|
|
6939
|
-
return textResult(`${result.title ? `# ${result.title}\n\n` : ""}${result.markdown}`.trim(), result);
|
|
7692
|
+
return executeCommandWithPolicy("bash", params);
|
|
6940
7693
|
}
|
|
6941
7694
|
};
|
|
6942
|
-
const
|
|
6943
|
-
name: "
|
|
6944
|
-
label: "
|
|
6945
|
-
description: "
|
|
7695
|
+
const diagnoseDeviceTool = {
|
|
7696
|
+
name: "diagnose_device",
|
|
7697
|
+
label: "Diagnose Device",
|
|
7698
|
+
description: "Run read-only core diagnostics for OS, CPU, memory, disk, network, and process status.",
|
|
6946
7699
|
parameters: Type.Object({
|
|
6947
|
-
|
|
6948
|
-
|
|
6949
|
-
|
|
6950
|
-
|
|
6951
|
-
limitPerFeed: Type.Optional(Type.Number({
|
|
6952
|
-
minimum: 1,
|
|
6953
|
-
maximum: 50
|
|
7700
|
+
profile: Type.Optional(Type.String({ default: "core" })),
|
|
7701
|
+
timeoutMs: Type.Optional(Type.Number({
|
|
7702
|
+
minimum: 1e3,
|
|
7703
|
+
maximum: 12e4
|
|
6954
7704
|
}))
|
|
6955
7705
|
}),
|
|
6956
7706
|
execute: async (_toolCallId, params) => {
|
|
6957
|
-
const
|
|
6958
|
-
|
|
6959
|
-
|
|
6960
|
-
|
|
6961
|
-
|
|
6962
|
-
|
|
6963
|
-
|
|
6964
|
-
|
|
6965
|
-
|
|
6966
|
-
})
|
|
6967
|
-
|
|
6968
|
-
|
|
6969
|
-
|
|
6970
|
-
|
|
7707
|
+
const profile = (params.profile || "core").trim().toLowerCase();
|
|
7708
|
+
if (profile !== "core") throw new Error(`Unsupported diagnostic profile: ${profile}`);
|
|
7709
|
+
const timeoutMs = params.timeoutMs ?? 1e4;
|
|
7710
|
+
const commands = diagnosticsCommandsForPlatform(process.platform);
|
|
7711
|
+
const sections = [];
|
|
7712
|
+
for (const entry of commands) try {
|
|
7713
|
+
const result = await runtime.exec(entry.command, {
|
|
7714
|
+
timeoutMs,
|
|
7715
|
+
allowedCommandPrefixes: [firstToken(entry.command)]
|
|
7716
|
+
});
|
|
7717
|
+
sections.push({
|
|
7718
|
+
key: entry.key,
|
|
7719
|
+
command: entry.command,
|
|
7720
|
+
ok: result.exitCode === 0 && !result.timedOut,
|
|
7721
|
+
exitCode: result.exitCode,
|
|
7722
|
+
stdout: result.stdout,
|
|
7723
|
+
stderr: result.stderr,
|
|
7724
|
+
timedOut: result.timedOut,
|
|
7725
|
+
truncated: result.truncated
|
|
6971
7726
|
});
|
|
6972
7727
|
} catch (error) {
|
|
6973
|
-
|
|
6974
|
-
|
|
6975
|
-
|
|
6976
|
-
|
|
6977
|
-
|
|
7728
|
+
sections.push({
|
|
7729
|
+
key: entry.key,
|
|
7730
|
+
command: entry.command,
|
|
7731
|
+
ok: false,
|
|
7732
|
+
exitCode: 1,
|
|
7733
|
+
stdout: "",
|
|
7734
|
+
stderr: normalizeErrorMessage(error),
|
|
7735
|
+
timedOut: false,
|
|
7736
|
+
truncated: false
|
|
6978
7737
|
});
|
|
6979
7738
|
}
|
|
6980
7739
|
audit({
|
|
6981
7740
|
actor: "tool",
|
|
6982
|
-
eventType: "tool.
|
|
7741
|
+
eventType: "tool.diagnose_device",
|
|
6983
7742
|
payload: {
|
|
6984
|
-
|
|
6985
|
-
|
|
7743
|
+
profile,
|
|
7744
|
+
commandCount: sections.length,
|
|
7745
|
+
failed: sections.filter((entry) => !entry.ok).length
|
|
6986
7746
|
}
|
|
6987
7747
|
});
|
|
6988
|
-
|
|
6989
|
-
|
|
6990
|
-
|
|
6991
|
-
|
|
6992
|
-
|
|
7748
|
+
const summaryLines = [
|
|
7749
|
+
`profile: ${profile}`,
|
|
7750
|
+
`platform: ${process.platform}`,
|
|
7751
|
+
`sections: ${sections.length}`,
|
|
7752
|
+
`failed: ${sections.filter((entry) => !entry.ok).length}`
|
|
7753
|
+
];
|
|
7754
|
+
for (const section of sections) {
|
|
7755
|
+
summaryLines.push("");
|
|
7756
|
+
summaryLines.push(`[${section.key}] ${section.command}`);
|
|
7757
|
+
summaryLines.push(`ok=${section.ok} exitCode=${section.exitCode} timedOut=${section.timedOut}`);
|
|
7758
|
+
if (section.stdout) summaryLines.push(`stdout:\n${section.stdout}`);
|
|
7759
|
+
if (section.stderr) summaryLines.push(`stderr:\n${section.stderr}`);
|
|
7760
|
+
}
|
|
7761
|
+
return textResult(summaryLines.join("\n"), {
|
|
7762
|
+
profile,
|
|
7763
|
+
platform: process.platform,
|
|
7764
|
+
sections
|
|
7765
|
+
});
|
|
6993
7766
|
}
|
|
6994
7767
|
};
|
|
6995
|
-
const tools = [
|
|
6996
|
-
|
|
6997
|
-
|
|
7768
|
+
const tools = [
|
|
7769
|
+
{
|
|
7770
|
+
name: "read_file",
|
|
7771
|
+
label: "Read File",
|
|
7772
|
+
description: "Read a text file from an allowlisted path.",
|
|
7773
|
+
parameters: Type.Object({
|
|
7774
|
+
path: Type.String(),
|
|
7775
|
+
maxBytes: Type.Optional(Type.Number({
|
|
7776
|
+
minimum: 128,
|
|
7777
|
+
maximum: 1e6
|
|
7778
|
+
}))
|
|
7779
|
+
}),
|
|
7780
|
+
execute: async (_toolCallId, params) => {
|
|
7781
|
+
audit({
|
|
7782
|
+
actor: "tool",
|
|
7783
|
+
eventType: "tool.read_file",
|
|
7784
|
+
payload: { path: params.path }
|
|
7785
|
+
});
|
|
7786
|
+
const result = await runtime.readFile(params.path, { maxOutputBytes: params.maxBytes });
|
|
7787
|
+
return textResult(result.content, result);
|
|
7788
|
+
}
|
|
7789
|
+
},
|
|
7790
|
+
{
|
|
7791
|
+
name: "write_file",
|
|
7792
|
+
label: "Write File",
|
|
7793
|
+
description: "Write UTF-8 text content to an allowlisted path.",
|
|
7794
|
+
parameters: Type.Object({
|
|
7795
|
+
path: Type.String(),
|
|
7796
|
+
content: Type.String()
|
|
7797
|
+
}),
|
|
7798
|
+
execute: async (_toolCallId, params) => {
|
|
7799
|
+
audit({
|
|
7800
|
+
actor: "tool",
|
|
7801
|
+
eventType: "tool.write_file",
|
|
7802
|
+
payload: {
|
|
7803
|
+
path: params.path,
|
|
7804
|
+
bytes: Buffer.byteLength(params.content, "utf8")
|
|
7805
|
+
}
|
|
7806
|
+
});
|
|
7807
|
+
const result = await runtime.writeFile(params.path, params.content);
|
|
7808
|
+
return textResult(`Wrote ${result.bytesWritten} bytes to ${params.path}.`, result);
|
|
7809
|
+
}
|
|
7810
|
+
},
|
|
7811
|
+
{
|
|
7812
|
+
name: "web_search",
|
|
7813
|
+
label: "Web Search",
|
|
7814
|
+
description: "Fetch a URL and return readable article text.",
|
|
7815
|
+
parameters: Type.Object({ url: Type.String({ format: "uri" }) }),
|
|
7816
|
+
execute: async (_toolCallId, params) => {
|
|
7817
|
+
audit({
|
|
7818
|
+
actor: "tool",
|
|
7819
|
+
eventType: "tool.fetch_web",
|
|
7820
|
+
payload: { url: params.url }
|
|
7821
|
+
});
|
|
7822
|
+
const result = await runtime.fetchWeb(params.url);
|
|
7823
|
+
return textResult(`${result.title ? `# ${result.title}\n\n` : ""}${result.markdown}`.trim(), result);
|
|
7824
|
+
}
|
|
7825
|
+
},
|
|
7826
|
+
{
|
|
7827
|
+
name: "fetch_podcast_feed",
|
|
7828
|
+
label: "Fetch Podcast Feed",
|
|
7829
|
+
description: "Fetch one or more RSS podcast feeds and return recent episodes.",
|
|
7830
|
+
parameters: Type.Object({
|
|
7831
|
+
urls: Type.Array(Type.String({ format: "uri" }), {
|
|
7832
|
+
minItems: 1,
|
|
7833
|
+
maxItems: 20
|
|
7834
|
+
}),
|
|
7835
|
+
limitPerFeed: Type.Optional(Type.Number({
|
|
7836
|
+
minimum: 1,
|
|
7837
|
+
maximum: 50
|
|
7838
|
+
}))
|
|
7839
|
+
}),
|
|
7840
|
+
execute: async (_toolCallId, params) => {
|
|
7841
|
+
const limit = params.limitPerFeed ?? 5;
|
|
7842
|
+
const output = [];
|
|
7843
|
+
for (const url of params.urls) try {
|
|
7844
|
+
const feed = await parser.parseURL(url);
|
|
7845
|
+
const episodes = (feed.items ?? []).slice(0, limit).map((item) => ({
|
|
7846
|
+
title: item.title ?? "Untitled episode",
|
|
7847
|
+
publishedAt: item.pubDate || item.isoDate || null,
|
|
7848
|
+
link: item.link ?? "",
|
|
7849
|
+
summary: (item.contentSnippet || item.content || item.summary || "").replace(/\s+/g, " ").trim().slice(0, 400) || "No summary available."
|
|
7850
|
+
}));
|
|
7851
|
+
output.push({
|
|
7852
|
+
url,
|
|
7853
|
+
title: feed.title ?? "Untitled feed",
|
|
7854
|
+
episodes
|
|
7855
|
+
});
|
|
7856
|
+
} catch (error) {
|
|
7857
|
+
output.push({
|
|
7858
|
+
url,
|
|
7859
|
+
title: "Unknown feed",
|
|
7860
|
+
episodes: [],
|
|
7861
|
+
error: normalizeErrorMessage(error)
|
|
7862
|
+
});
|
|
7863
|
+
}
|
|
7864
|
+
audit({
|
|
7865
|
+
actor: "tool",
|
|
7866
|
+
eventType: "tool.fetch_podcast_feed",
|
|
7867
|
+
payload: {
|
|
7868
|
+
urls: params.urls,
|
|
7869
|
+
limitPerFeed: limit
|
|
7870
|
+
}
|
|
7871
|
+
});
|
|
7872
|
+
return textResult(output.map((feed) => {
|
|
7873
|
+
if (feed.error) return `Feed: ${feed.url}\nError: ${feed.error}`;
|
|
7874
|
+
const episodesText = feed.episodes.map((episode, index) => `${index + 1}. ${episode.title}${episode.publishedAt ? ` (${episode.publishedAt})` : ""}\n${episode.link}\n${episode.summary}`).join("\n\n");
|
|
7875
|
+
return `Feed: ${feed.title}\nSource: ${feed.url}\n\n${episodesText}`;
|
|
7876
|
+
}).join("\n\n---\n\n"), output);
|
|
7877
|
+
}
|
|
7878
|
+
},
|
|
7879
|
+
diagnoseDeviceTool
|
|
7880
|
+
];
|
|
7881
|
+
if (execPolicy.enabled) tools.unshift(execTool, bashTool);
|
|
6998
7882
|
return tools;
|
|
6999
7883
|
}
|
|
7000
7884
|
|
|
@@ -7082,7 +7966,6 @@ async function main() {
|
|
|
7082
7966
|
} catch (error) {
|
|
7083
7967
|
logger.warn({ error }, "Workspace bootstrap failed");
|
|
7084
7968
|
}
|
|
7085
|
-
writeOpenClawMirror(config);
|
|
7086
7969
|
const db = new HovClawDb(path.join(config.storeDir, "hovclaw.db"));
|
|
7087
7970
|
db.ping();
|
|
7088
7971
|
const runtime = config.runtime.mode === "container" ? new ContainerRuntime({
|
|
@@ -7101,10 +7984,40 @@ async function main() {
|
|
|
7101
7984
|
allowedWriteRoots: config.runtime.allowedWriteRoots,
|
|
7102
7985
|
allowedCommandPrefixes: config.runtime.allowedCommandPrefixes
|
|
7103
7986
|
});
|
|
7987
|
+
let emitGatewayEvent = null;
|
|
7988
|
+
const execApprovals = new ExecApprovalsManager({
|
|
7989
|
+
storeDir: config.storeDir,
|
|
7990
|
+
defaults: {
|
|
7991
|
+
security: config.runtime.tools.exec.security,
|
|
7992
|
+
ask: config.runtime.tools.exec.ask
|
|
7993
|
+
},
|
|
7994
|
+
onRequested: (record) => {
|
|
7995
|
+
emitGatewayEvent?.("exec.approval.requested", {
|
|
7996
|
+
id: record.id,
|
|
7997
|
+
request: {
|
|
7998
|
+
command: record.command,
|
|
7999
|
+
agentId: record.agentId,
|
|
8000
|
+
sessionKey: record.sessionKey,
|
|
8001
|
+
resolvedPath: record.resolvedPath
|
|
8002
|
+
},
|
|
8003
|
+
createdAtMs: record.createdAtMs,
|
|
8004
|
+
expiresAtMs: record.expiresAtMs
|
|
8005
|
+
});
|
|
8006
|
+
},
|
|
8007
|
+
onResolved: (record) => {
|
|
8008
|
+
emitGatewayEvent?.("exec.approval.resolved", {
|
|
8009
|
+
id: record.id,
|
|
8010
|
+
decision: record.decision,
|
|
8011
|
+
resolvedBy: record.resolvedBy ?? null,
|
|
8012
|
+
resolvedAtMs: record.resolvedAtMs
|
|
8013
|
+
});
|
|
8014
|
+
}
|
|
8015
|
+
});
|
|
7104
8016
|
const agentManager = new PiAgentManager(db, createTools({
|
|
7105
8017
|
runtime,
|
|
7106
8018
|
audit: (record) => db.appendAuditEvent(record),
|
|
7107
|
-
|
|
8019
|
+
execPolicy: config.runtime.tools.exec,
|
|
8020
|
+
execApprovals
|
|
7108
8021
|
}));
|
|
7109
8022
|
let lastMessageAt = null;
|
|
7110
8023
|
const telegramPairingStore = new TelegramPairingStore(config.storeDir);
|
|
@@ -7128,6 +8041,19 @@ async function main() {
|
|
|
7128
8041
|
if (!normalizedPrompt) return;
|
|
7129
8042
|
let thinkingLevel = config.commands.defaultThinkingLevel;
|
|
7130
8043
|
let thinkingLevelForced = false;
|
|
8044
|
+
const commandAuthorized = isCommandAuthorized(config, msg);
|
|
8045
|
+
if ((await handleExecChatCommand({
|
|
8046
|
+
msg,
|
|
8047
|
+
target,
|
|
8048
|
+
channel,
|
|
8049
|
+
runtime,
|
|
8050
|
+
execApprovals,
|
|
8051
|
+
execPolicy: config.runtime.tools.exec,
|
|
8052
|
+
commandAuthorized,
|
|
8053
|
+
agentId,
|
|
8054
|
+
sessionKey,
|
|
8055
|
+
audit: (record) => db.appendAuditEvent(record)
|
|
8056
|
+
})).handled) return;
|
|
7131
8057
|
if (msg.channel === "telegram") {
|
|
7132
8058
|
const policy = evaluateTelegramPolicy({
|
|
7133
8059
|
config,
|
|
@@ -7272,12 +8198,16 @@ async function main() {
|
|
|
7272
8198
|
const gatewayServer = config.gateway.enabled ? new HovClawGatewayServer({
|
|
7273
8199
|
config,
|
|
7274
8200
|
db,
|
|
8201
|
+
execApprovals,
|
|
7275
8202
|
agentManager,
|
|
7276
8203
|
channels,
|
|
7277
8204
|
getChannelStatus: () => channelPluginManager.buildStatusPayload(),
|
|
7278
8205
|
readFileConfig,
|
|
7279
8206
|
writeFileConfig
|
|
7280
8207
|
}) : null;
|
|
8208
|
+
emitGatewayEvent = gatewayServer ? (event, payload) => {
|
|
8209
|
+
gatewayServer.emitEvent(event, payload);
|
|
8210
|
+
} : null;
|
|
7281
8211
|
gatewayServer?.start();
|
|
7282
8212
|
const healthPortRaw = process.env.HEALTH_PORT || "8787";
|
|
7283
8213
|
const healthPort = Number(healthPortRaw);
|