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/README.md +12 -6
- package/dist/{doctor-DJHTvhli.js → doctor-0iphhiTj.js} +1 -1
- package/dist/hovclaw.js +891 -137
- package/dist/index.js +1194 -167
- 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-qX1C6PLF.js → src-GZDRRc5A.js} +308 -35
- package/package.json +1 -1
package/dist/hovclaw.js
CHANGED
|
@@ -116,7 +116,27 @@ const DEFAULT_FILE_CONFIG = {
|
|
|
116
116
|
"tail",
|
|
117
117
|
"wc"
|
|
118
118
|
],
|
|
119
|
-
tools: {
|
|
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({
|
|
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:
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
|
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
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
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
|
-
`
|
|
4111
|
-
|
|
4112
|
-
|
|
4760
|
+
`approvalRequired: true`,
|
|
4761
|
+
`approvalId: ${approval.id}`,
|
|
4762
|
+
`expiresAtMs: ${approval.expiresAtMs}`,
|
|
4113
4763
|
"",
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
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
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
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
|
-
|
|
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
|
|
4163
|
-
name: "
|
|
4164
|
-
label: "
|
|
4165
|
-
description: "
|
|
4166
|
-
parameters:
|
|
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
|
-
|
|
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
|
|
4178
|
-
name: "
|
|
4179
|
-
label: "
|
|
4180
|
-
description: "
|
|
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
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
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
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
})
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
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
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
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.
|
|
4846
|
+
eventType: "tool.diagnose_device",
|
|
4218
4847
|
payload: {
|
|
4219
|
-
|
|
4220
|
-
|
|
4848
|
+
profile,
|
|
4849
|
+
commandCount: sections.length,
|
|
4850
|
+
failed: sections.filter((entry) => !entry.ok).length
|
|
4221
4851
|
}
|
|
4222
4852
|
});
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
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
|
-
|
|
4232
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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 {
|
|
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 };
|