skimpyclaw 0.1.8 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/bash-path-validation.test.d.ts +1 -0
- package/dist/__tests__/bash-path-validation.test.js +164 -0
- package/dist/__tests__/doctor.runner.test.js +5 -1
- package/dist/__tests__/heartbeat.test.js +30 -3
- package/dist/__tests__/sandbox-bridge.test.d.ts +1 -0
- package/dist/__tests__/sandbox-bridge.test.js +116 -0
- package/dist/__tests__/sandbox-manager.test.d.ts +1 -0
- package/dist/__tests__/sandbox-manager.test.js +119 -0
- package/dist/__tests__/sandbox-mount-security.test.d.ts +1 -0
- package/dist/__tests__/sandbox-mount-security.test.js +131 -0
- package/dist/__tests__/sandbox-runtime.test.d.ts +1 -0
- package/dist/__tests__/sandbox-runtime.test.js +140 -0
- package/dist/__tests__/setup.test.js +32 -3
- package/dist/__tests__/skills.test.js +2 -11
- package/dist/__tests__/tools.test.js +6 -1
- package/dist/__tests__/voice.test.js +12 -0
- package/dist/agent.js +2 -0
- package/dist/api.js +5 -1
- package/dist/channels/telegram/utils.js +2 -2
- package/dist/cli.js +212 -0
- package/dist/code-agents/executor.js +17 -4
- package/dist/code-agents/types.d.ts +5 -0
- package/dist/cron.js +16 -2
- package/dist/discord.js +2 -2
- package/dist/doctor/checks.d.ts +1 -0
- package/dist/doctor/checks.js +47 -0
- package/dist/doctor/runner.js +2 -1
- package/dist/exec-approval.d.ts +4 -0
- package/dist/exec-approval.js +4 -4
- package/dist/gateway.js +33 -2
- package/dist/heartbeat.js +7 -3
- package/dist/providers/openai.js +1 -1
- package/dist/sandbox/bridge.d.ts +5 -0
- package/dist/sandbox/bridge.js +63 -0
- package/dist/sandbox/index.d.ts +5 -0
- package/dist/sandbox/index.js +4 -0
- package/dist/sandbox/manager.d.ts +7 -0
- package/dist/sandbox/manager.js +89 -0
- package/dist/sandbox/mount-security.d.ts +12 -0
- package/dist/sandbox/mount-security.js +118 -0
- package/dist/sandbox/runtime.d.ts +33 -0
- package/dist/sandbox/runtime.js +167 -0
- package/dist/service.js +17 -0
- package/dist/setup.d.ts +11 -0
- package/dist/setup.js +336 -23
- package/dist/skills.d.ts +1 -2
- package/dist/skills.js +1 -13
- package/dist/tools/bash-path-validation.d.ts +22 -0
- package/dist/tools/bash-path-validation.js +130 -0
- package/dist/tools/bash-tool.js +23 -1
- package/dist/tools/definitions.d.ts +0 -7
- package/dist/tools/definitions.js +0 -5
- package/dist/tools/execute-context.d.ts +4 -0
- package/dist/tools/path-utils.js +16 -2
- package/dist/tools.js +84 -2
- package/dist/types.d.ts +10 -0
- package/dist/voice.js +5 -1
- package/package.json +1 -1
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// Bash argument path validation — extracts file paths from command tokens
|
|
2
|
+
// and validates them against the allowedPaths list.
|
|
3
|
+
import { resolve } from 'path';
|
|
4
|
+
import { homedir } from 'os';
|
|
5
|
+
import { getCommandSegments, getSegmentCommandIndex, getExecutableName, } from '../exec-approval.js';
|
|
6
|
+
import { isPathAllowed } from './path-utils.js';
|
|
7
|
+
// --- Path-like token detection ---
|
|
8
|
+
/**
|
|
9
|
+
* Returns true if a shell token looks like a file/directory path.
|
|
10
|
+
* Matches: /foo, ./foo, ../foo, ~/foo
|
|
11
|
+
* Does NOT match: flags (-f, --file), bare words (foo), URLs (https://...)
|
|
12
|
+
*/
|
|
13
|
+
export function isPathLikeToken(token) {
|
|
14
|
+
const t = token.trim();
|
|
15
|
+
if (!t || t === '-' || t === '--')
|
|
16
|
+
return false;
|
|
17
|
+
// Absolute path
|
|
18
|
+
if (t.startsWith('/'))
|
|
19
|
+
return true;
|
|
20
|
+
// Relative paths
|
|
21
|
+
if (t.startsWith('./') || t.startsWith('../') || t === '.' || t === '..')
|
|
22
|
+
return true;
|
|
23
|
+
// Home-relative path
|
|
24
|
+
if (t.startsWith('~/') || t === '~')
|
|
25
|
+
return true;
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
// --- Interpreter script target extraction ---
|
|
29
|
+
const INTERPRETERS = new Set([
|
|
30
|
+
'python', 'python3', 'python3.11', 'python3.12', 'python3.13',
|
|
31
|
+
'node', 'deno', 'bun',
|
|
32
|
+
'perl', 'ruby', 'php', 'lua',
|
|
33
|
+
'bash', 'sh', 'zsh', 'fish',
|
|
34
|
+
]);
|
|
35
|
+
/** Flags that consume the next argument (so it's not a script path). */
|
|
36
|
+
const INTERPRETER_FLAGS_WITH_VALUE = new Set([
|
|
37
|
+
'-c', '-e', '-m', '-W', '-X', '-O',
|
|
38
|
+
'--eval', '--execute', '--require',
|
|
39
|
+
]);
|
|
40
|
+
/** Flags that mean "read from stdin" or "inline code follows" — stop looking for script path. */
|
|
41
|
+
const INTERPRETER_INLINE_FLAGS = new Set([
|
|
42
|
+
'-c', '-e', '--eval', '--execute', '-r', '-Command',
|
|
43
|
+
]);
|
|
44
|
+
/**
|
|
45
|
+
* For an interpreter command segment, extract the script file path if present.
|
|
46
|
+
* Returns null if the command uses inline execution (-c, -e) or reads from stdin.
|
|
47
|
+
*/
|
|
48
|
+
export function extractScriptTarget(segment) {
|
|
49
|
+
const cmdIdx = getSegmentCommandIndex(segment);
|
|
50
|
+
if (cmdIdx >= segment.length)
|
|
51
|
+
return null;
|
|
52
|
+
const cmd = getExecutableName(segment[cmdIdx]);
|
|
53
|
+
if (!INTERPRETERS.has(cmd))
|
|
54
|
+
return null;
|
|
55
|
+
const args = segment.slice(cmdIdx + 1);
|
|
56
|
+
for (let i = 0; i < args.length; i++) {
|
|
57
|
+
const arg = args[i];
|
|
58
|
+
// Inline execution — no script file to validate
|
|
59
|
+
if (INTERPRETER_INLINE_FLAGS.has(arg))
|
|
60
|
+
return null;
|
|
61
|
+
// Flag that consumes next arg — skip both
|
|
62
|
+
if (INTERPRETER_FLAGS_WITH_VALUE.has(arg)) {
|
|
63
|
+
i++;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
// Skip other flags (single or double dash)
|
|
67
|
+
if (arg.startsWith('-'))
|
|
68
|
+
continue;
|
|
69
|
+
// First non-flag argument is the script path
|
|
70
|
+
return arg;
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
// --- Path extraction from full command ---
|
|
75
|
+
/**
|
|
76
|
+
* Extract all file-path-like tokens from a shell command.
|
|
77
|
+
* Handles piped/chained commands. Resolves ~ and relative paths using cwd.
|
|
78
|
+
* Also extracts script targets from interpreter commands.
|
|
79
|
+
*/
|
|
80
|
+
export function extractPathsFromCommand(command, cwd) {
|
|
81
|
+
const segments = getCommandSegments(command);
|
|
82
|
+
const paths = [];
|
|
83
|
+
const baseCwd = cwd || process.cwd();
|
|
84
|
+
for (const segment of segments) {
|
|
85
|
+
const cmdIdx = getSegmentCommandIndex(segment);
|
|
86
|
+
// Check for interpreter script target
|
|
87
|
+
const scriptTarget = extractScriptTarget(segment);
|
|
88
|
+
if (scriptTarget) {
|
|
89
|
+
paths.push(resolvePath(scriptTarget, baseCwd));
|
|
90
|
+
}
|
|
91
|
+
// Check all non-command tokens for path-like values
|
|
92
|
+
for (let i = cmdIdx + 1; i < segment.length; i++) {
|
|
93
|
+
const token = segment[i];
|
|
94
|
+
if (isPathLikeToken(token)) {
|
|
95
|
+
paths.push(resolvePath(token, baseCwd));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Deduplicate
|
|
100
|
+
return [...new Set(paths)];
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Resolve a token to an absolute path, expanding ~ and relative paths.
|
|
104
|
+
*/
|
|
105
|
+
function resolvePath(token, cwd) {
|
|
106
|
+
if (token.startsWith('~/') || token === '~') {
|
|
107
|
+
return resolve(homedir(), token.slice(2) || '.');
|
|
108
|
+
}
|
|
109
|
+
return resolve(cwd, token);
|
|
110
|
+
}
|
|
111
|
+
// --- Validation ---
|
|
112
|
+
/**
|
|
113
|
+
* Validate that all file paths in a bash command are within allowedPaths.
|
|
114
|
+
* Returns null if all paths are valid, or an error message string if any are outside.
|
|
115
|
+
*/
|
|
116
|
+
export function validateBashPaths(command, cwd, allowedPaths) {
|
|
117
|
+
// If no allowedPaths configured, skip validation (permissive mode)
|
|
118
|
+
if (!allowedPaths || allowedPaths.length === 0)
|
|
119
|
+
return null;
|
|
120
|
+
const paths = extractPathsFromCommand(command, cwd);
|
|
121
|
+
const blocked = [];
|
|
122
|
+
for (const p of paths) {
|
|
123
|
+
if (!isPathAllowed(p, allowedPaths)) {
|
|
124
|
+
blocked.push(p);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (blocked.length === 0)
|
|
128
|
+
return null;
|
|
129
|
+
return `Error: Command references paths outside allowed directories: ${blocked.join(', ')}`;
|
|
130
|
+
}
|
package/dist/tools/bash-tool.js
CHANGED
|
@@ -2,6 +2,23 @@ import { exec } from 'child_process';
|
|
|
2
2
|
import { isBashCommandSafe } from '../security.js';
|
|
3
3
|
import { classifyCommandRisk, requiresApproval, createApprovalRequest, waitForApproval, } from '../exec-approval.js';
|
|
4
4
|
import { isPathAllowed } from './path-utils.js';
|
|
5
|
+
import { validateBashPaths } from './bash-path-validation.js';
|
|
6
|
+
/** Env var name patterns that should never be exposed to model-executed commands. */
|
|
7
|
+
const SENSITIVE_ENV_PATTERNS = [
|
|
8
|
+
/api.?key/i, /token/i, /secret/i, /password/i, /credential/i,
|
|
9
|
+
/^ANTHROPIC_/i, /^OPENAI_/i, /^CLAUDE/i, /^CODEX_/i, /^MINIMAX_/i,
|
|
10
|
+
/^KIMI_/i, /^TOGETHER_/i, /^GROQ_/i, /^OPENROUTER_/i,
|
|
11
|
+
];
|
|
12
|
+
/** Create a sanitized copy of process.env with secrets stripped. */
|
|
13
|
+
function sanitizeEnv() {
|
|
14
|
+
const env = { ...process.env };
|
|
15
|
+
for (const key of Object.keys(env)) {
|
|
16
|
+
if (SENSITIVE_ENV_PATTERNS.some(p => p.test(key))) {
|
|
17
|
+
delete env[key];
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return env;
|
|
21
|
+
}
|
|
5
22
|
export async function executeBash(command, cwd, config, context) {
|
|
6
23
|
// Hard block: existing safety filter (always enforced)
|
|
7
24
|
if (!isBashCommandSafe(command)) {
|
|
@@ -10,6 +27,11 @@ export async function executeBash(command, cwd, config, context) {
|
|
|
10
27
|
if (cwd && !isPathAllowed(cwd, config.allowedPaths)) {
|
|
11
28
|
return Promise.resolve('Error: Working directory not in allowed paths.');
|
|
12
29
|
}
|
|
30
|
+
// Validate file paths referenced in command arguments
|
|
31
|
+
const pathError = validateBashPaths(command, cwd, config.allowedPaths);
|
|
32
|
+
if (pathError) {
|
|
33
|
+
return Promise.resolve(pathError);
|
|
34
|
+
}
|
|
13
35
|
// Exec approval gate: classify risk and check if approval is needed
|
|
14
36
|
const approvalConfig = config.execApproval;
|
|
15
37
|
if (approvalConfig?.enabled !== false) {
|
|
@@ -44,7 +66,7 @@ export async function executeBash(command, cwd, config, context) {
|
|
|
44
66
|
exec(command, {
|
|
45
67
|
cwd: cwd || undefined,
|
|
46
68
|
timeout,
|
|
47
|
-
env:
|
|
69
|
+
env: sanitizeEnv(),
|
|
48
70
|
maxBuffer: 5 * 1024 * 1024,
|
|
49
71
|
}, (error, stdout, stderr) => {
|
|
50
72
|
if (error) {
|
|
@@ -126,11 +126,6 @@ export const SPAWN_SUBAGENT_TOOL = {
|
|
|
126
126
|
},
|
|
127
127
|
model: { type: 'string', description: 'Optional model override (e.g. claude-opus, claude-think)' },
|
|
128
128
|
label: { type: 'string', description: 'Short label for status display (e.g. "write tests", "check logs")' },
|
|
129
|
-
allowedPaths: {
|
|
130
|
-
type: 'array',
|
|
131
|
-
items: { type: 'string' },
|
|
132
|
-
description: 'Additional file paths the subagent can access beyond defaults',
|
|
133
|
-
},
|
|
134
129
|
},
|
|
135
130
|
required: ['task', 'type'],
|
|
136
131
|
},
|
|
@@ -23,4 +23,8 @@ export interface ExecuteToolContext {
|
|
|
23
23
|
trigger?: string;
|
|
24
24
|
/** Agent ID for usage tracking */
|
|
25
25
|
agentId?: string;
|
|
26
|
+
/** Sandbox configuration for containerized tool execution */
|
|
27
|
+
sandboxConfig?: import('../types.js').SandboxConfig;
|
|
28
|
+
/** Session ID for sandbox container mapping */
|
|
29
|
+
sessionId?: string;
|
|
26
30
|
}
|
package/dist/tools/path-utils.js
CHANGED
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
import { resolve, sep } from 'path';
|
|
2
|
+
import { realpathSync } from 'fs';
|
|
3
|
+
/**
|
|
4
|
+
* Resolve a path to its real location, following symlinks.
|
|
5
|
+
* Falls back to path.resolve() if the path doesn't exist yet (e.g. for writes).
|
|
6
|
+
*/
|
|
7
|
+
function safeRealpath(filePath) {
|
|
8
|
+
try {
|
|
9
|
+
return realpathSync(filePath);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
// Path doesn't exist yet — resolve logically (normalizes .. but can't follow symlinks)
|
|
13
|
+
return resolve(filePath);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
2
16
|
export function isPathAllowed(filePath, allowedPaths) {
|
|
3
|
-
const resolved =
|
|
17
|
+
const resolved = safeRealpath(filePath);
|
|
4
18
|
return allowedPaths.some((allowed) => {
|
|
5
|
-
const allowedRoot =
|
|
19
|
+
const allowedRoot = safeRealpath(allowed);
|
|
6
20
|
return resolved === allowedRoot || resolved.startsWith(`${allowedRoot}${sep}`);
|
|
7
21
|
});
|
|
8
22
|
}
|
package/dist/tools.js
CHANGED
|
@@ -6,6 +6,8 @@ import { fromClaudeCodeName, toClaudeCodeName, BUILTIN_TOOL_DEFINITIONS, BROWSER
|
|
|
6
6
|
import { executeReadFile, executeWriteFileLocked, executeListDirectory } from './tools/file-tools.js';
|
|
7
7
|
import { executeBash } from './tools/bash-tool.js';
|
|
8
8
|
import { executeBrowser, cleanupBrowser } from './tools/browser-tool.js';
|
|
9
|
+
import { ensureContainer, SANDBOX_DEFAULTS, translatePath, validateMountPaths } from './sandbox/index.js';
|
|
10
|
+
import { sandboxBash, sandboxReadFile, sandboxWriteFile, sandboxListDir, sandboxGlob } from './sandbox/index.js';
|
|
9
11
|
// Re-export from code-agents module for backward compatibility
|
|
10
12
|
export {
|
|
11
13
|
// Registry functions
|
|
@@ -240,7 +242,23 @@ async function executeWebSearch(query) {
|
|
|
240
242
|
export async function executeTool(name, input, config, context) {
|
|
241
243
|
try {
|
|
242
244
|
// Route MCP tools BEFORE normalization to preserve server/tool name casing
|
|
245
|
+
// NOTE: MCP servers run as external processes with full host access.
|
|
246
|
+
// We validate path-like arguments as a best-effort check, but MCP servers
|
|
247
|
+
// are a trusted boundary — only configure servers you trust.
|
|
243
248
|
if (name.startsWith('mcp__')) {
|
|
249
|
+
if (config.allowedPaths?.length) {
|
|
250
|
+
const { isPathAllowed } = await import('./tools/path-utils.js');
|
|
251
|
+
for (const [key, value] of Object.entries(input)) {
|
|
252
|
+
if (typeof value === 'string' && (value.startsWith('/') || value.startsWith('~/') || value.startsWith('./'))) {
|
|
253
|
+
const { resolve } = await import('path');
|
|
254
|
+
const { homedir } = await import('os');
|
|
255
|
+
const resolved = value.startsWith('~/') ? resolve(homedir(), value.slice(2)) : resolve(value);
|
|
256
|
+
if (!isPathAllowed(resolved, config.allowedPaths)) {
|
|
257
|
+
return `Error: MCP tool argument "${key}" references path outside allowed directories: ${value}`;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
244
262
|
return await executeMcpToolGeneric(name, input);
|
|
245
263
|
}
|
|
246
264
|
// Route spawn_subagent
|
|
@@ -264,6 +282,69 @@ export async function executeTool(name, input, config, context) {
|
|
|
264
282
|
}
|
|
265
283
|
// Map Claude Code names to internal names for built-in tools
|
|
266
284
|
const normalized = fromClaudeCodeName(name).toLowerCase().replace(/-/g, '_');
|
|
285
|
+
// --- Sandbox routing ---
|
|
286
|
+
const sandboxCfg = context?.sandboxConfig;
|
|
287
|
+
if (sandboxCfg?.enabled) {
|
|
288
|
+
const SANDBOXED_TOOLS = new Set(['bash', 'read_file', 'write_file', 'list_directory', 'glob']);
|
|
289
|
+
// macOS-only commands that must run on the host (not available in Linux containers)
|
|
290
|
+
const MACOS_HOST_COMMANDS = new Set([
|
|
291
|
+
'osascript', 'open', 'say', 'pbcopy', 'pbpaste', 'defaults',
|
|
292
|
+
'icalBuddy', 'shortcuts', 'caffeinate', 'networksetup', 'launchctl',
|
|
293
|
+
'security', 'xattr', 'ditto', 'hdiutil', 'diskutil', 'sw_vers',
|
|
294
|
+
]);
|
|
295
|
+
const needsHost = normalized === 'bash' && input.command &&
|
|
296
|
+
MACOS_HOST_COMMANDS.has(input.command.trim().split(/[\s;|&]/)[0]);
|
|
297
|
+
if (SANDBOXED_TOOLS.has(normalized) && !needsHost) {
|
|
298
|
+
const sessionId = context?.sessionId || context?.chatId?.toString() || 'default';
|
|
299
|
+
const merged = { ...SANDBOX_DEFAULTS, ...sandboxCfg };
|
|
300
|
+
const containerName = await ensureContainer(sessionId, merged, config.allowedPaths);
|
|
301
|
+
const mounts = validateMountPaths(config.allowedPaths);
|
|
302
|
+
const tp = (p) => translatePath(p, mounts);
|
|
303
|
+
// Translate host paths in bash commands so they resolve inside the container
|
|
304
|
+
const translateBashPaths = (cmd) => {
|
|
305
|
+
let translated = cmd;
|
|
306
|
+
// Sort mounts by host path length descending to match most specific first
|
|
307
|
+
const sorted = [...mounts].sort((a, b) => b.host.length - a.host.length);
|
|
308
|
+
for (const mount of sorted) {
|
|
309
|
+
translated = translated.replaceAll(mount.host, mount.container);
|
|
310
|
+
}
|
|
311
|
+
// Also translate ~ and $HOME references to /workspace/config
|
|
312
|
+
const home = homedir();
|
|
313
|
+
const homeMounts = sorted.filter(m => m.host.startsWith(home));
|
|
314
|
+
for (const mount of homeMounts) {
|
|
315
|
+
const tildeForm = '~' + mount.host.slice(home.length);
|
|
316
|
+
translated = translated.replaceAll(tildeForm, mount.container);
|
|
317
|
+
const envForm = '$HOME' + mount.host.slice(home.length);
|
|
318
|
+
translated = translated.replaceAll(envForm, mount.container);
|
|
319
|
+
}
|
|
320
|
+
return translated;
|
|
321
|
+
};
|
|
322
|
+
// Reverse-translate container paths back to host paths in file content.
|
|
323
|
+
// Prevents the agent from writing /workspace/... paths into config files.
|
|
324
|
+
const reverseTranslatePaths = (content) => {
|
|
325
|
+
let reversed = content;
|
|
326
|
+
const sorted = [...mounts].sort((a, b) => b.container.length - a.container.length);
|
|
327
|
+
for (const mount of sorted) {
|
|
328
|
+
reversed = reversed.replaceAll(mount.container, mount.host);
|
|
329
|
+
}
|
|
330
|
+
return reversed;
|
|
331
|
+
};
|
|
332
|
+
switch (normalized) {
|
|
333
|
+
case 'bash':
|
|
334
|
+
return await sandboxBash(containerName, translateBashPaths(input.command), input.cwd ? tp(input.cwd) : undefined, config.bashTimeout);
|
|
335
|
+
case 'read_file':
|
|
336
|
+
return await sandboxReadFile(containerName, tp(input.file_path || input.path));
|
|
337
|
+
case 'write_file':
|
|
338
|
+
return await sandboxWriteFile(containerName, tp(input.file_path || input.path), reverseTranslatePaths(input.content));
|
|
339
|
+
case 'list_directory':
|
|
340
|
+
return await sandboxListDir(containerName, tp(input.path));
|
|
341
|
+
case 'glob':
|
|
342
|
+
return await sandboxGlob(containerName, tp(input.base || input.path || '/workspace'), input.pattern || '*');
|
|
343
|
+
default:
|
|
344
|
+
break; // fall through
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
267
348
|
switch (normalized) {
|
|
268
349
|
case '$web_search':
|
|
269
350
|
case 'web_search':
|
|
@@ -300,7 +381,8 @@ async function executeSpawnSubagent(input, context) {
|
|
|
300
381
|
const type = input.type;
|
|
301
382
|
const model = input.model;
|
|
302
383
|
const label = input.label;
|
|
303
|
-
|
|
384
|
+
// NOTE: allowedPaths deliberately NOT accepted from model input (security — prevents path escalation).
|
|
385
|
+
// Subagents inherit their preset's allowedPaths from config.
|
|
304
386
|
if (!task || !type) {
|
|
305
387
|
return 'Error: task and type are required';
|
|
306
388
|
}
|
|
@@ -308,7 +390,7 @@ async function executeSpawnSubagent(input, context) {
|
|
|
308
390
|
return `Error: Invalid type "${type}". Must be coding or research.`;
|
|
309
391
|
}
|
|
310
392
|
try {
|
|
311
|
-
const subagentTask = dispatchSubagent(type, task, context.chatId, context.fullConfig, model, context.history, { label
|
|
393
|
+
const subagentTask = dispatchSubagent(type, task, context.chatId, context.fullConfig, model, context.history, { label });
|
|
312
394
|
const labelStr = label ? ` "${label}"` : '';
|
|
313
395
|
return JSON.stringify({
|
|
314
396
|
status: 'accepted',
|
package/dist/types.d.ts
CHANGED
|
@@ -79,6 +79,7 @@ export interface Config {
|
|
|
79
79
|
/** Named project paths. Keys are short names (e.g. "skimpyclaw"), values are absolute paths.
|
|
80
80
|
* Project paths are automatically added to tool allowedPaths and available to code_with_agent by name. */
|
|
81
81
|
projects?: Record<string, string>;
|
|
82
|
+
sandbox?: SandboxConfig;
|
|
82
83
|
}
|
|
83
84
|
export interface AgentConfig {
|
|
84
85
|
identity: {
|
|
@@ -134,6 +135,15 @@ export interface CronPayload {
|
|
|
134
135
|
tools?: ToolConfig;
|
|
135
136
|
sendAsVoice?: boolean;
|
|
136
137
|
}
|
|
138
|
+
export interface SandboxConfig {
|
|
139
|
+
enabled: boolean;
|
|
140
|
+
runtime?: 'container' | 'docker';
|
|
141
|
+
image?: string;
|
|
142
|
+
cpus?: number;
|
|
143
|
+
memory?: string;
|
|
144
|
+
network?: string;
|
|
145
|
+
idleTimeoutMs?: number;
|
|
146
|
+
}
|
|
137
147
|
export interface ToolConfig {
|
|
138
148
|
enabled: boolean;
|
|
139
149
|
allowedPaths: string[];
|
package/dist/voice.js
CHANGED
|
@@ -191,7 +191,8 @@ function getSTTProvider(config) {
|
|
|
191
191
|
if (!provider)
|
|
192
192
|
return false;
|
|
193
193
|
// macOS voice provider is TTS-only and must never be used for transcription.
|
|
194
|
-
|
|
194
|
+
const normalizedName = name.trim().toLowerCase();
|
|
195
|
+
if (normalizedName === 'macos')
|
|
195
196
|
return false;
|
|
196
197
|
return Boolean(provider.stt || provider.apiKey);
|
|
197
198
|
};
|
|
@@ -275,6 +276,9 @@ export async function transcribeAudio(audioPath, config) {
|
|
|
275
276
|
// No local whisper — try API provider directly
|
|
276
277
|
const sttProvider = getSTTProvider(config);
|
|
277
278
|
if (!sttProvider) {
|
|
279
|
+
if (config.providers?.macos) {
|
|
280
|
+
throw new Error('No voice transcription provider configured. "macos" is TTS-only. Install local whisper or configure an API STT provider (e.g. openai.stt).');
|
|
281
|
+
}
|
|
278
282
|
throw new Error('No voice transcription available. Install whisper (pip install openai-whisper) or configure an API provider.');
|
|
279
283
|
}
|
|
280
284
|
return transcribeWithAPI(audioPath, sttProvider.name, sttProvider.provider);
|