skimpyclaw 0.1.9 → 0.3.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__/cron.test.js +51 -1
- package/dist/__tests__/doctor.runner.test.js +5 -1
- package/dist/__tests__/heartbeat.test.js +5 -5
- 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 +176 -0
- package/dist/__tests__/setup.test.js +28 -2
- package/dist/__tests__/skills.test.js +2 -11
- package/dist/__tests__/tools.test.js +6 -1
- package/dist/agent.js +3 -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.d.ts +6 -0
- package/dist/cron.js +59 -3
- 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 +3 -0
- package/dist/providers/anthropic.js +1 -1
- package/dist/providers/codex.js +1 -1
- package/dist/providers/openai.js +2 -2
- 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 +38 -0
- package/dist/sandbox/runtime.js +187 -0
- package/dist/service.js +25 -0
- package/dist/setup.d.ts +11 -0
- package/dist/setup.js +335 -13
- 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 +6 -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 +9 -2
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -43,6 +43,10 @@ Commands:
|
|
|
43
43
|
tools list List available tools (built-in + MCP)
|
|
44
44
|
tools install <name> Add MCP server (--command <cmd> [--args ...] or --url <url>)
|
|
45
45
|
tools remove <name> Remove MCP server
|
|
46
|
+
sandbox status Show active sandbox containers
|
|
47
|
+
sandbox prune Force-prune all sandbox containers
|
|
48
|
+
sandbox init Auto-setup sandbox runtime/image/config (supports --profile)
|
|
49
|
+
sandbox doctor Sandbox-specific diagnostics and hints
|
|
46
50
|
help Show this help
|
|
47
51
|
`);
|
|
48
52
|
}
|
|
@@ -724,6 +728,211 @@ async function commandTools(args) {
|
|
|
724
728
|
console.error('Usage: skimpyclaw tools <list|install|remove>');
|
|
725
729
|
return 1;
|
|
726
730
|
}
|
|
731
|
+
const SANDBOX_CLI_BY_PROFILE = {
|
|
732
|
+
minimal: ['bash', 'curl', 'git', 'gh', 'jq', 'python3', 'rg', 'pnpm'],
|
|
733
|
+
dev: ['bash', 'curl', 'git', 'gh', 'jq', 'python3', 'rg', 'pnpm', 'gcc', 'g++', 'make'],
|
|
734
|
+
full: ['bash', 'curl', 'git', 'gh', 'jq', 'python3', 'rg', 'pnpm', 'gcc', 'g++', 'make', 'pip3', 'sqlite3'],
|
|
735
|
+
};
|
|
736
|
+
function defaultSandboxNetwork(runtime) {
|
|
737
|
+
return runtime === 'container' ? 'default' : 'bridge';
|
|
738
|
+
}
|
|
739
|
+
function detectSandboxRuntime(preferred) {
|
|
740
|
+
if (preferred === 'container' || preferred === 'docker') {
|
|
741
|
+
return spawnSync(preferred, ['--version'], { encoding: 'utf-8' }).status === 0 ? preferred : null;
|
|
742
|
+
}
|
|
743
|
+
if (spawnSync('container', ['--version'], { encoding: 'utf-8' }).status === 0) {
|
|
744
|
+
return 'container';
|
|
745
|
+
}
|
|
746
|
+
if (spawnSync('docker', ['--version'], { encoding: 'utf-8' }).status === 0) {
|
|
747
|
+
return 'docker';
|
|
748
|
+
}
|
|
749
|
+
return null;
|
|
750
|
+
}
|
|
751
|
+
function isSandboxRuntimeRunning(runtime) {
|
|
752
|
+
if (runtime === 'container') {
|
|
753
|
+
return spawnSync('container', ['system', 'status'], { encoding: 'utf-8' }).status === 0;
|
|
754
|
+
}
|
|
755
|
+
return spawnSync('docker', ['info'], { encoding: 'utf-8' }).status === 0;
|
|
756
|
+
}
|
|
757
|
+
function sandboxNetworkExists(runtime, network) {
|
|
758
|
+
if (runtime === 'container') {
|
|
759
|
+
const result = spawnSync('container', ['network', 'ls'], { encoding: 'utf-8' });
|
|
760
|
+
if (result.status !== 0)
|
|
761
|
+
return false;
|
|
762
|
+
return result.stdout.split('\n').some((line) => line.trim().split(/\s+/)[0] === network);
|
|
763
|
+
}
|
|
764
|
+
const result = spawnSync('docker', ['network', 'inspect', network], { encoding: 'utf-8' });
|
|
765
|
+
return result.status === 0;
|
|
766
|
+
}
|
|
767
|
+
function sandboxImageExists(runtime, image) {
|
|
768
|
+
return spawnSync(runtime, ['image', 'inspect', image], { encoding: 'utf-8' }).status === 0;
|
|
769
|
+
}
|
|
770
|
+
function resolveSandboxDir() {
|
|
771
|
+
const cwdSandbox = join(process.cwd(), 'sandbox');
|
|
772
|
+
if (existsSync(join(cwdSandbox, 'Dockerfile'))) {
|
|
773
|
+
return cwdSandbox;
|
|
774
|
+
}
|
|
775
|
+
return null;
|
|
776
|
+
}
|
|
777
|
+
function parseSandboxOption(args, flag) {
|
|
778
|
+
const idx = args.indexOf(flag);
|
|
779
|
+
if (idx === -1 || idx + 1 >= args.length)
|
|
780
|
+
return undefined;
|
|
781
|
+
return args[idx + 1];
|
|
782
|
+
}
|
|
783
|
+
function runSandboxImageCheck(runtime, image, network, cmd) {
|
|
784
|
+
const result = spawnSync(runtime, ['run', '--rm', '--network', network, image, 'sh', '-lc', cmd], { encoding: 'utf-8' });
|
|
785
|
+
if (result.status === 0) {
|
|
786
|
+
return { ok: true, detail: (result.stdout || '').trim() || 'ok' };
|
|
787
|
+
}
|
|
788
|
+
const detail = `${(result.stderr || '').trim()} ${(result.stdout || '').trim()}`.trim() || `exit ${result.status ?? 1}`;
|
|
789
|
+
return { ok: false, detail };
|
|
790
|
+
}
|
|
791
|
+
function printSandboxCheck(ok, name, detail, hint) {
|
|
792
|
+
const prefix = ok ? '✓' : '✗';
|
|
793
|
+
console.log(`${prefix} ${name}: ${detail}`);
|
|
794
|
+
if (!ok && hint) {
|
|
795
|
+
console.log(` → ${hint}`);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
async function commandSandbox(args) {
|
|
799
|
+
const sub = args[0];
|
|
800
|
+
if (sub === 'status') {
|
|
801
|
+
const rt = detectSandboxRuntime();
|
|
802
|
+
if (!rt) {
|
|
803
|
+
console.log('No container runtime found (install Docker or Apple Containers).');
|
|
804
|
+
return 1;
|
|
805
|
+
}
|
|
806
|
+
const result = spawnSync(rt, ['ps', '--format', '{{.Names}}'], { encoding: 'utf-8' });
|
|
807
|
+
const lines = (result.stdout || '').trim().split('\n').filter(Boolean);
|
|
808
|
+
const containers = lines.filter((line) => line.includes('skimpyclaw-sbx'));
|
|
809
|
+
if (containers.length === 0) {
|
|
810
|
+
console.log('No active sandbox containers.');
|
|
811
|
+
}
|
|
812
|
+
else {
|
|
813
|
+
console.log(`Active sandbox containers (${containers.length}):`);
|
|
814
|
+
containers.forEach((c) => console.log(` ${c}`));
|
|
815
|
+
}
|
|
816
|
+
return 0;
|
|
817
|
+
}
|
|
818
|
+
if (sub === 'prune') {
|
|
819
|
+
const { cleanupOrphans } = await import('./sandbox/index.js');
|
|
820
|
+
const count = await cleanupOrphans();
|
|
821
|
+
console.log(`Pruned ${count} sandbox container(s).`);
|
|
822
|
+
return 0;
|
|
823
|
+
}
|
|
824
|
+
if (sub === 'init') {
|
|
825
|
+
const runtimeFlag = parseSandboxOption(args, '--runtime');
|
|
826
|
+
const profileFlag = parseSandboxOption(args, '--profile') || 'minimal';
|
|
827
|
+
const imageFlag = parseSandboxOption(args, '--image');
|
|
828
|
+
const networkFlag = parseSandboxOption(args, '--network');
|
|
829
|
+
const validProfiles = ['minimal', 'dev', 'full'];
|
|
830
|
+
if (!validProfiles.includes(profileFlag)) {
|
|
831
|
+
console.error(`Invalid profile "${profileFlag}". Use one of: minimal, dev, full`);
|
|
832
|
+
return 1;
|
|
833
|
+
}
|
|
834
|
+
const profile = profileFlag;
|
|
835
|
+
const runtime = detectSandboxRuntime(runtimeFlag);
|
|
836
|
+
if (!runtime) {
|
|
837
|
+
console.error('No supported runtime found. Install Apple Containers or Docker.');
|
|
838
|
+
return 1;
|
|
839
|
+
}
|
|
840
|
+
const network = networkFlag || defaultSandboxNetwork(runtime);
|
|
841
|
+
const image = imageFlag || 'skimpyclaw-sandbox:latest';
|
|
842
|
+
if (!isSandboxRuntimeRunning(runtime)) {
|
|
843
|
+
const hint = runtime === 'container' ? 'Run: container system start' : 'Start Docker Desktop (or run `docker info`).';
|
|
844
|
+
console.error(`Runtime "${runtime}" is not running.`);
|
|
845
|
+
console.error(hint);
|
|
846
|
+
return 1;
|
|
847
|
+
}
|
|
848
|
+
if (!sandboxNetworkExists(runtime, network)) {
|
|
849
|
+
const hint = runtime === 'container'
|
|
850
|
+
? 'Create/list networks with `container network ls`.'
|
|
851
|
+
: 'Create/list networks with `docker network ls`.';
|
|
852
|
+
console.error(`Sandbox network "${network}" not found for runtime "${runtime}".`);
|
|
853
|
+
console.error(hint);
|
|
854
|
+
return 1;
|
|
855
|
+
}
|
|
856
|
+
const sandboxDir = resolveSandboxDir();
|
|
857
|
+
if (!sandboxDir) {
|
|
858
|
+
console.error('Could not find sandbox/Dockerfile from current directory.');
|
|
859
|
+
console.error('Run from repo root (contains ./sandbox) or build image manually.');
|
|
860
|
+
return 1;
|
|
861
|
+
}
|
|
862
|
+
console.log(`Building sandbox image "${image}" (runtime=${runtime}, profile=${profile})...`);
|
|
863
|
+
const build = spawnSync(runtime, ['build', '--build-arg', `SKIMPY_PROFILE=${profile}`, '-t', image, sandboxDir], { stdio: 'inherit' });
|
|
864
|
+
if (build.status !== 0) {
|
|
865
|
+
console.error('Sandbox image build failed.');
|
|
866
|
+
return 1;
|
|
867
|
+
}
|
|
868
|
+
const raw = loadRawConfig();
|
|
869
|
+
const sandbox = raw.sandbox ?? {};
|
|
870
|
+
sandbox.enabled = true;
|
|
871
|
+
sandbox.runtime = runtime;
|
|
872
|
+
sandbox.network = network;
|
|
873
|
+
sandbox.image = image;
|
|
874
|
+
raw.sandbox = sandbox;
|
|
875
|
+
saveConfig(raw);
|
|
876
|
+
console.log('Updated config: sandbox.enabled=true');
|
|
877
|
+
console.log(`Updated config: sandbox.runtime="${runtime}"`);
|
|
878
|
+
console.log(`Updated config: sandbox.network="${network}"`);
|
|
879
|
+
console.log(`Updated config: sandbox.image="${image}"`);
|
|
880
|
+
const required = SANDBOX_CLI_BY_PROFILE[profile];
|
|
881
|
+
const checkCmd = `for c in ${required.join(' ')}; do command -v "$c" >/dev/null || { echo "missing:$c"; exit 1; }; done; echo cli-ok`;
|
|
882
|
+
const cliCheck = runSandboxImageCheck(runtime, image, network, checkCmd);
|
|
883
|
+
const netCheck = runSandboxImageCheck(runtime, image, network, 'curl -fsS --max-time 8 https://example.com >/dev/null && echo net-ok');
|
|
884
|
+
const hostCheck = runSandboxImageCheck(runtime, image, network, 'hostname');
|
|
885
|
+
printSandboxCheck(hostCheck.ok, 'sandbox_hostname', hostCheck.detail);
|
|
886
|
+
printSandboxCheck(cliCheck.ok, 'sandbox_tools', cliCheck.detail, 'Rebuild image or choose a lighter profile.');
|
|
887
|
+
printSandboxCheck(netCheck.ok, 'sandbox_network_egress', netCheck.detail, 'Try a different sandbox.network or check runtime DNS/network settings.');
|
|
888
|
+
if (!hostCheck.ok || !cliCheck.ok || !netCheck.ok) {
|
|
889
|
+
return 1;
|
|
890
|
+
}
|
|
891
|
+
console.log('\nSandbox init complete. Restart Skimpy to apply runtime config.');
|
|
892
|
+
return 0;
|
|
893
|
+
}
|
|
894
|
+
if (sub === 'doctor') {
|
|
895
|
+
const config = loadConfig();
|
|
896
|
+
const runtime = detectSandboxRuntime(config.sandbox?.runtime);
|
|
897
|
+
const image = config.sandbox?.image || 'skimpyclaw-sandbox:latest';
|
|
898
|
+
const network = config.sandbox?.network || (runtime ? defaultSandboxNetwork(runtime) : 'unknown');
|
|
899
|
+
const profileFlag = parseSandboxOption(args, '--profile') || 'minimal';
|
|
900
|
+
const profile = (['minimal', 'dev', 'full'].includes(profileFlag) ? profileFlag : 'minimal');
|
|
901
|
+
let failed = false;
|
|
902
|
+
printSandboxCheck(config.sandbox?.enabled === true, 'sandbox_enabled', config.sandbox?.enabled ? 'enabled' : 'disabled', 'Run: skimpyclaw sandbox init');
|
|
903
|
+
if (!config.sandbox?.enabled)
|
|
904
|
+
failed = true;
|
|
905
|
+
printSandboxCheck(!!runtime, 'runtime_detected', runtime || 'none', 'Install Docker or Apple Containers.');
|
|
906
|
+
if (!runtime)
|
|
907
|
+
return 1;
|
|
908
|
+
printSandboxCheck(isSandboxRuntimeRunning(runtime), 'runtime_running', runtime, runtime === 'container' ? 'Run: container system start' : 'Start Docker Desktop.');
|
|
909
|
+
if (!isSandboxRuntimeRunning(runtime))
|
|
910
|
+
failed = true;
|
|
911
|
+
const networkOk = sandboxNetworkExists(runtime, network);
|
|
912
|
+
printSandboxCheck(networkOk, 'network_exists', network, `Use "${runtime === 'container' ? 'container' : 'docker'} network ls" and update sandbox.network.`);
|
|
913
|
+
if (!networkOk)
|
|
914
|
+
failed = true;
|
|
915
|
+
const imageOk = sandboxImageExists(runtime, image);
|
|
916
|
+
printSandboxCheck(imageOk, 'image_exists', image, `Build image: ${runtime} build -t ${image} sandbox/`);
|
|
917
|
+
if (!imageOk)
|
|
918
|
+
failed = true;
|
|
919
|
+
if (imageOk && networkOk) {
|
|
920
|
+
const required = SANDBOX_CLI_BY_PROFILE[profile];
|
|
921
|
+
const checkCmd = `for c in ${required.join(' ')}; do command -v "$c" >/dev/null || { echo "missing:$c"; exit 1; }; done; echo cli-ok`;
|
|
922
|
+
const cliCheck = runSandboxImageCheck(runtime, image, network, checkCmd);
|
|
923
|
+
printSandboxCheck(cliCheck.ok, 'image_toolchain', cliCheck.detail, 'Rebuild with: skimpyclaw sandbox init --profile dev');
|
|
924
|
+
if (!cliCheck.ok)
|
|
925
|
+
failed = true;
|
|
926
|
+
const netCheck = runSandboxImageCheck(runtime, image, network, 'curl -fsS --max-time 8 https://api.duckduckgo.com/?q=skimpyclaw&format=json >/dev/null && echo net-ok');
|
|
927
|
+
printSandboxCheck(netCheck.ok, 'network_egress', netCheck.detail, 'Some sources may timeout; verify DNS/network in runtime.');
|
|
928
|
+
if (!netCheck.ok)
|
|
929
|
+
failed = true;
|
|
930
|
+
}
|
|
931
|
+
return failed ? 1 : 0;
|
|
932
|
+
}
|
|
933
|
+
console.log('Usage: skimpyclaw sandbox <status|prune|init|doctor>');
|
|
934
|
+
return 1;
|
|
935
|
+
}
|
|
727
936
|
export async function runCli(argv = process.argv.slice(2)) {
|
|
728
937
|
const [command, ...args] = argv;
|
|
729
938
|
if (!command || command === 'help' || command === '--help' || command === '-h') {
|
|
@@ -787,6 +996,9 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
787
996
|
if (command === 'tools') {
|
|
788
997
|
return await commandTools(args);
|
|
789
998
|
}
|
|
999
|
+
if (command === 'sandbox') {
|
|
1000
|
+
return await commandSandbox(args);
|
|
1001
|
+
}
|
|
790
1002
|
console.error(`Unknown command: ${command}`);
|
|
791
1003
|
printHelp();
|
|
792
1004
|
return 1;
|
|
@@ -10,6 +10,7 @@ import { buildCodeAgentArgs, notifyCodeAgentResult } from './utils.js';
|
|
|
10
10
|
import { parseStreamJsonForLive, parseClaudeOutput, parseCodexOutput } from './parser.js';
|
|
11
11
|
import { startTrace, addEvent, endTrace } from '../audit.js';
|
|
12
12
|
import { buildUsageRecord, recordUsage } from '../usage.js';
|
|
13
|
+
import { ensureContainer, SANDBOX_DEFAULTS, getRuntime } from '../sandbox/index.js';
|
|
13
14
|
const CANCELLED_MESSAGE = 'Cancelled by user';
|
|
14
15
|
/** Run build/test validation. Shared by solo agents and team orchestrator. */
|
|
15
16
|
export function runValidation(workdir) {
|
|
@@ -87,6 +88,13 @@ export async function runCodeAgentBackground(id, agent, task, workdir, validate,
|
|
|
87
88
|
logStream.write(`Task: ${task.slice(0, 500)}\n`);
|
|
88
89
|
logStream.write(`Workdir: ${workdir}\n\n`);
|
|
89
90
|
try {
|
|
91
|
+
// Resolve sandbox container name if enabled (used for spawn wrapping)
|
|
92
|
+
let sandboxContainer;
|
|
93
|
+
if (options?.sandboxConfig?.enabled) {
|
|
94
|
+
const merged = { ...SANDBOX_DEFAULTS, ...options.sandboxConfig };
|
|
95
|
+
sandboxContainer = await ensureContainer(`code-${id}`, merged, options.allowedPaths || [workdir]);
|
|
96
|
+
console.log(`[code-agent] Running in sandbox container: ${sandboxContainer}`);
|
|
97
|
+
}
|
|
90
98
|
ensureNotCancelled();
|
|
91
99
|
const exitCode = await new Promise((resolvePromise, reject) => {
|
|
92
100
|
const spawnEnv = { ...process.env };
|
|
@@ -94,8 +102,11 @@ export async function runCodeAgentBackground(id, agent, task, workdir, validate,
|
|
|
94
102
|
// Apply extra env vars (e.g. team mode feature flag)
|
|
95
103
|
if (options?.env)
|
|
96
104
|
Object.assign(spawnEnv, options.env);
|
|
97
|
-
|
|
98
|
-
|
|
105
|
+
// When sandbox is enabled, wrap the spawn: container exec <name> <cmd> <args>
|
|
106
|
+
const spawnCmd = sandboxContainer ? getRuntime() : cmd;
|
|
107
|
+
const spawnArgs = sandboxContainer ? ['exec', sandboxContainer, cmd, ...args] : args;
|
|
108
|
+
const proc = spawn(spawnCmd, spawnArgs, {
|
|
109
|
+
cwd: sandboxContainer ? undefined : workdir, // container has its own cwd
|
|
99
110
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
100
111
|
env: spawnEnv,
|
|
101
112
|
});
|
|
@@ -277,8 +288,10 @@ export async function runCodeAgentBackground(id, agent, task, workdir, validate,
|
|
|
277
288
|
const retryExitCode = await new Promise((resolveRetry, rejectRetry) => {
|
|
278
289
|
const spawnEnv = { ...process.env };
|
|
279
290
|
delete spawnEnv.CLAUDECODE;
|
|
280
|
-
const
|
|
281
|
-
|
|
291
|
+
const retrySpawnCmd = sandboxContainer ? getRuntime() : retryCmd;
|
|
292
|
+
const retrySpawnArgs = sandboxContainer ? ['exec', sandboxContainer, retryCmd, ...retryArgs] : retryArgs;
|
|
293
|
+
const retryProc = spawn(retrySpawnCmd, retrySpawnArgs, {
|
|
294
|
+
cwd: sandboxContainer ? undefined : workdir,
|
|
282
295
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
283
296
|
env: spawnEnv,
|
|
284
297
|
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { SandboxConfig } from '../types.js';
|
|
1
2
|
export interface CodeAgentTask {
|
|
2
3
|
id: string;
|
|
3
4
|
agent: string;
|
|
@@ -44,6 +45,10 @@ export interface CodeAgentBackgroundOptions {
|
|
|
44
45
|
maxTimeoutMinutes?: number;
|
|
45
46
|
/** Skip sending notification on completion (parent handles it) */
|
|
46
47
|
skipNotification?: boolean;
|
|
48
|
+
/** Sandbox configuration — when enabled, run CLI inside container */
|
|
49
|
+
sandboxConfig?: SandboxConfig;
|
|
50
|
+
/** Paths to mount into the sandbox container */
|
|
51
|
+
allowedPaths?: string[];
|
|
47
52
|
}
|
|
48
53
|
export interface BuildCodeAgentArgsInput {
|
|
49
54
|
task: string;
|
package/dist/cron.d.ts
CHANGED
|
@@ -23,6 +23,12 @@ export declare function parseDualOutput(response: string): {
|
|
|
23
23
|
voice: string | null;
|
|
24
24
|
text: string;
|
|
25
25
|
};
|
|
26
|
+
/**
|
|
27
|
+
* Post-run guard for the pr-review cron job.
|
|
28
|
+
* Validates that the agent actually used code_with_agent when PRs were found.
|
|
29
|
+
* Non-throwing — logs a warning and returns an alert message (or null if OK).
|
|
30
|
+
*/
|
|
31
|
+
export declare function validatePrReviewOutput(output: string): string | null;
|
|
26
32
|
export declare function getCronJobs(): {
|
|
27
33
|
id: string;
|
|
28
34
|
name: string;
|
package/dist/cron.js
CHANGED
|
@@ -9,6 +9,7 @@ import { startTrace, addEvent, endTrace } from './audit.js';
|
|
|
9
9
|
import { sendActiveChannelProactiveMessage, sendActiveChannelProactiveVoice, getActiveChannelId } from './channels.js';
|
|
10
10
|
import { parseAndSaveDigest } from './digests.js';
|
|
11
11
|
import { synthesizeSpeech } from './voice.js';
|
|
12
|
+
import { ensureContainer, SANDBOX_DEFAULTS, sandboxBash } from './sandbox/index.js';
|
|
12
13
|
const scheduledJobs = new Map();
|
|
13
14
|
let configWatcher = null;
|
|
14
15
|
function getCronLogDir() {
|
|
@@ -138,7 +139,7 @@ async function executeJobPayload(jobDef, config) {
|
|
|
138
139
|
channel: getActiveChannelId() || 'telegram',
|
|
139
140
|
trigger: 'cron',
|
|
140
141
|
sessionId: jobDef.id,
|
|
141
|
-
metadata: { jobName: jobDef.name },
|
|
142
|
+
metadata: { jobName: jobDef.name, isCronJob: true },
|
|
142
143
|
});
|
|
143
144
|
appendCronLogLine(jobDef.id, `Agent turn completed (${response.length} chars)`);
|
|
144
145
|
// Parse dual output (voice + text) if delimiters present
|
|
@@ -148,6 +149,19 @@ async function executeJobPayload(jobDef, config) {
|
|
|
148
149
|
}
|
|
149
150
|
// Use text portion for log output and notifications
|
|
150
151
|
logEntry.output = textPortion.slice(0, 5000);
|
|
152
|
+
// Post-run guard for pr-review job
|
|
153
|
+
if (jobDef.id === 'pr-review') {
|
|
154
|
+
const guardAlert = validatePrReviewOutput(textPortion);
|
|
155
|
+
if (guardAlert) {
|
|
156
|
+
appendCronLogLine(jobDef.id, guardAlert);
|
|
157
|
+
try {
|
|
158
|
+
await sendActiveChannelProactiveMessage(config, guardAlert);
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
// Non-critical
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
151
165
|
// Parse and save digest from the text portion
|
|
152
166
|
try {
|
|
153
167
|
parseAndSaveDigest(jobDef.id, jobDef.name, textPortion);
|
|
@@ -189,7 +203,7 @@ async function executeJobPayload(jobDef, config) {
|
|
|
189
203
|
});
|
|
190
204
|
appendCronLogLine(jobDef.id, `Script started: ${(jobDef.payload.script || '').slice(0, 100)}`);
|
|
191
205
|
try {
|
|
192
|
-
const output = await executeScript(jobDef);
|
|
206
|
+
const output = await executeScript(jobDef, config);
|
|
193
207
|
logEntry.output = output.slice(0, 50000);
|
|
194
208
|
appendCronLogLine(jobDef.id, `Script completed (${output.length} chars)`);
|
|
195
209
|
addEvent(scriptTraceId, {
|
|
@@ -248,7 +262,7 @@ async function executeJobPayload(jobDef, config) {
|
|
|
248
262
|
}
|
|
249
263
|
}
|
|
250
264
|
}
|
|
251
|
-
async function executeScript(jobDef) {
|
|
265
|
+
async function executeScript(jobDef, config) {
|
|
252
266
|
const script = expandVariables(jobDef.payload.script || '');
|
|
253
267
|
if (!script) {
|
|
254
268
|
throw new Error(`Script payload is empty for job: ${jobDef.id}`);
|
|
@@ -258,6 +272,19 @@ async function executeScript(jobDef) {
|
|
|
258
272
|
throw new Error(`Working directory does not exist: ${cwd}`);
|
|
259
273
|
}
|
|
260
274
|
const timeoutMs = jobDef.payload.timeoutMs || 600000; // 10 min default
|
|
275
|
+
// Sandbox routing for script payloads
|
|
276
|
+
const sandboxCfg = config.sandbox;
|
|
277
|
+
if (sandboxCfg?.enabled) {
|
|
278
|
+
const merged = { ...SANDBOX_DEFAULTS, ...sandboxCfg };
|
|
279
|
+
const containerName = await ensureContainer(`cron-${jobDef.id}`, merged, jobDef.payload.tools?.allowedPaths || []);
|
|
280
|
+
console.log(`[cron:script] Running in sandbox container: ${containerName}`);
|
|
281
|
+
console.log(`[cron:script] Running: ${script.slice(0, 100)}${script.length > 100 ? '...' : ''}`);
|
|
282
|
+
if (cwd)
|
|
283
|
+
console.log(`[cron:script] cwd: ${cwd}`);
|
|
284
|
+
const output = await sandboxBash(containerName, script, cwd, timeoutMs);
|
|
285
|
+
console.log(`[cron:script] Sandbox completed (${output.length} chars)`);
|
|
286
|
+
return output;
|
|
287
|
+
}
|
|
261
288
|
return new Promise((resolve, reject) => {
|
|
262
289
|
const startTime = Date.now();
|
|
263
290
|
console.log(`[cron:script] Running: ${script.slice(0, 100)}${script.length > 100 ? '...' : ''}`);
|
|
@@ -342,6 +369,35 @@ export function parseDualOutput(response) {
|
|
|
342
369
|
text: text || response,
|
|
343
370
|
};
|
|
344
371
|
}
|
|
372
|
+
/**
|
|
373
|
+
* Post-run guard for the pr-review cron job.
|
|
374
|
+
* Validates that the agent actually used code_with_agent when PRs were found.
|
|
375
|
+
* Non-throwing — logs a warning and returns an alert message (or null if OK).
|
|
376
|
+
*/
|
|
377
|
+
export function validatePrReviewOutput(output) {
|
|
378
|
+
// Check for the machine-readable result line
|
|
379
|
+
const resultMatch = output.match(/\[PR_REVIEW_RESULT:\s*(.+?)\]/);
|
|
380
|
+
if (!resultMatch) {
|
|
381
|
+
return '⚠️ PR Pre-Review: Missing [PR_REVIEW_RESULT] line in output. The agent may not have followed the prompt correctly.';
|
|
382
|
+
}
|
|
383
|
+
const resultLine = resultMatch[1].trim();
|
|
384
|
+
// NO_CANDIDATES is fine — nothing to review
|
|
385
|
+
if (resultLine === 'NO_CANDIDATES') {
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
// Parse CANDIDATES=N CODE_AGENT_CALLS=M BLOCKED=B
|
|
389
|
+
const candidatesMatch = resultLine.match(/CANDIDATES=(\d+)/);
|
|
390
|
+
const callsMatch = resultLine.match(/CODE_AGENT_CALLS=(\d+)/);
|
|
391
|
+
const blockedMatch = resultLine.match(/BLOCKED=(\d+)/);
|
|
392
|
+
const candidates = candidatesMatch ? parseInt(candidatesMatch[1], 10) : 0;
|
|
393
|
+
const calls = callsMatch ? parseInt(callsMatch[1], 10) : 0;
|
|
394
|
+
const blocked = blockedMatch ? parseInt(blockedMatch[1], 10) : 0;
|
|
395
|
+
// If there were candidates but zero code_with_agent calls (and not all blocked), alert
|
|
396
|
+
if (candidates > 0 && calls === 0 && blocked < candidates) {
|
|
397
|
+
return `⚠️ PR Pre-Review: Found ${candidates} PR candidate(s) but code_with_agent was never called (blocked: ${blocked}). The agent likely wrote inline commentary instead of delegating.`;
|
|
398
|
+
}
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
345
401
|
function expandVariables(message) {
|
|
346
402
|
const now = new Date();
|
|
347
403
|
const date = now.toLocaleDateString('en-US', {
|
package/dist/discord.js
CHANGED
|
@@ -47,8 +47,8 @@ const chatHistory = new Map();
|
|
|
47
47
|
const loadedFromDisk = new Set();
|
|
48
48
|
const DEFAULT_DISCORD_TOOLS = {
|
|
49
49
|
enabled: true,
|
|
50
|
-
allowedPaths: [join(homedir(), '.skimpyclaw')
|
|
51
|
-
maxIterations:
|
|
50
|
+
allowedPaths: [join(homedir(), '.skimpyclaw')],
|
|
51
|
+
maxIterations: 30,
|
|
52
52
|
bashTimeout: 15000,
|
|
53
53
|
};
|
|
54
54
|
let client = null;
|
package/dist/doctor/checks.d.ts
CHANGED
|
@@ -15,4 +15,5 @@ export declare function checkVoiceDependencies(config: Config): Promise<DoctorCh
|
|
|
15
15
|
export declare function checkMcpConfig(config: Config): Promise<DoctorCheckResult>;
|
|
16
16
|
export declare function checkGatewayHostBindable(host: string): Promise<DoctorCheckResult>;
|
|
17
17
|
export declare function checkSkimpyclawDirWritable(): Promise<DoctorCheckResult>;
|
|
18
|
+
export declare function checkSandboxAvailable(config: Config): Promise<DoctorCheckResult>;
|
|
18
19
|
export declare function checkPortAvailability(port: number): Promise<DoctorCheckResult>;
|
package/dist/doctor/checks.js
CHANGED
|
@@ -341,6 +341,53 @@ export async function checkSkimpyclawDirWritable() {
|
|
|
341
341
|
return fail(name, category, `Directory not writable: ${base}`, 'Fix permissions for ~/.skimpyclaw.');
|
|
342
342
|
}
|
|
343
343
|
}
|
|
344
|
+
export async function checkSandboxAvailable(config) {
|
|
345
|
+
const name = 'sandbox_available';
|
|
346
|
+
const category = 'runtime';
|
|
347
|
+
if (!config.sandbox?.enabled) {
|
|
348
|
+
return ok(name, category, 'Sandbox disabled');
|
|
349
|
+
}
|
|
350
|
+
// Determine which runtime to check
|
|
351
|
+
const explicit = config.sandbox.runtime;
|
|
352
|
+
let rt = null;
|
|
353
|
+
if (explicit) {
|
|
354
|
+
const check = spawnSync(explicit, ['--version'], { encoding: 'utf-8' });
|
|
355
|
+
if (check.status !== 0) {
|
|
356
|
+
return fail(name, category, `${explicit} CLI not found`, `Install ${explicit} or change sandbox.runtime in config.`);
|
|
357
|
+
}
|
|
358
|
+
rt = explicit;
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
// Auto-detect: prefer container, fall back to docker
|
|
362
|
+
const containerCheck = spawnSync('container', ['--version'], { encoding: 'utf-8' });
|
|
363
|
+
if (containerCheck.status === 0) {
|
|
364
|
+
rt = 'container';
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
const dockerCheck = spawnSync('docker', ['--version'], { encoding: 'utf-8' });
|
|
368
|
+
if (dockerCheck.status === 0) {
|
|
369
|
+
rt = 'docker';
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
if (!rt) {
|
|
373
|
+
return fail(name, category, 'No container runtime found', 'Install Apple Containers or Docker, or disable sandbox in config.');
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
// For Apple Containers, check system is running
|
|
377
|
+
if (rt === 'container') {
|
|
378
|
+
const systemCheck = spawnSync('container', ['system', 'status'], { encoding: 'utf-8' });
|
|
379
|
+
if (systemCheck.status !== 0) {
|
|
380
|
+
return fail(name, category, 'Container system not running', 'Run "container system start" to start the container runtime.');
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
// Check if sandbox image exists
|
|
384
|
+
const image = config.sandbox.image || 'skimpyclaw-sandbox';
|
|
385
|
+
const imageCheck = spawnSync(rt, ['image', 'inspect', image], { encoding: 'utf-8' });
|
|
386
|
+
if (imageCheck.status !== 0) {
|
|
387
|
+
return fail(name, category, `Sandbox image "${image}" not found`, `Build it: ${rt} build -t ${image} sandbox/`);
|
|
388
|
+
}
|
|
389
|
+
return ok(name, category, `Runtime: ${rt}, image "${image}" available`);
|
|
390
|
+
}
|
|
344
391
|
export async function checkPortAvailability(port) {
|
|
345
392
|
const name = 'gateway_port_available';
|
|
346
393
|
const category = 'runtime';
|
package/dist/doctor/runner.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { loadConfig } from '../config.js';
|
|
2
|
-
import { checkNodeVersion, checkPackageManagerAvailable, checkTypeScriptCompile, checkConfigExistsAndValidJson, checkRequiredEnvVars, checkEnvVarPatterns, checkAllowedPathsWritable, checkProviderAuth, checkTelegramToken, checkDiscordToken, checkBrowserBinaryIfEnabled, checkVoiceDependencies, checkMcpConfig, checkGatewayHostBindable, checkSkimpyclawDirWritable, checkPortAvailability, } from './checks.js';
|
|
2
|
+
import { checkNodeVersion, checkPackageManagerAvailable, checkTypeScriptCompile, checkConfigExistsAndValidJson, checkRequiredEnvVars, checkEnvVarPatterns, checkAllowedPathsWritable, checkProviderAuth, checkTelegramToken, checkDiscordToken, checkBrowserBinaryIfEnabled, checkVoiceDependencies, checkMcpConfig, checkGatewayHostBindable, checkSkimpyclawDirWritable, checkPortAvailability, checkSandboxAvailable, } from './checks.js';
|
|
3
3
|
export function computeExitCode(report) {
|
|
4
4
|
if (report.checks.some((check) => !check.ok && check.fatal)) {
|
|
5
5
|
return 2;
|
|
@@ -104,6 +104,7 @@ export async function runDoctor() {
|
|
|
104
104
|
checks.push(await runSafe('gateway_host_bindable', 'runtime', () => checkGatewayHostBindable(config.gateway.host ?? '127.0.0.1')));
|
|
105
105
|
checks.push(await runSafe('skimpyclaw_dirs_writable', 'runtime', () => checkSkimpyclawDirWritable()));
|
|
106
106
|
checks.push(await runSafe('gateway_port_available', 'runtime', () => checkPortAvailability(config.gateway.port)));
|
|
107
|
+
checks.push(await runSafe('sandbox_available', 'runtime', () => checkSandboxAvailable(config)));
|
|
107
108
|
const report = buildReport(startedAt, checks);
|
|
108
109
|
return { report, exitCode: report.exitCode };
|
|
109
110
|
}
|
package/dist/exec-approval.d.ts
CHANGED
|
@@ -3,6 +3,10 @@ export interface RiskClassification {
|
|
|
3
3
|
tier: RiskTier;
|
|
4
4
|
reason: string;
|
|
5
5
|
}
|
|
6
|
+
export declare function tokenizeShellCommand(command: string): string[];
|
|
7
|
+
export declare function getCommandSegments(command: string): string[][];
|
|
8
|
+
export declare function getSegmentCommandIndex(segment: string[]): number;
|
|
9
|
+
export declare function getExecutableName(token: string): string;
|
|
6
10
|
/**
|
|
7
11
|
* Classify the risk tier of a shell command.
|
|
8
12
|
* Returns the highest-tier match found.
|
package/dist/exec-approval.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Exec Approval Gate — risk classification and pending approval registry for Bash commands
|
|
2
2
|
import { randomUUID } from 'crypto';
|
|
3
3
|
import { EventEmitter } from 'events';
|
|
4
|
-
function tokenizeShellCommand(command) {
|
|
4
|
+
export function tokenizeShellCommand(command) {
|
|
5
5
|
const tokens = [];
|
|
6
6
|
let current = '';
|
|
7
7
|
let quote = null;
|
|
@@ -71,7 +71,7 @@ function tokenizeShellCommand(command) {
|
|
|
71
71
|
function isCommandSeparator(token) {
|
|
72
72
|
return token === '&&' || token === '||' || token === '|' || token === ';';
|
|
73
73
|
}
|
|
74
|
-
function getCommandSegments(command) {
|
|
74
|
+
export function getCommandSegments(command) {
|
|
75
75
|
const tokens = tokenizeShellCommand(command);
|
|
76
76
|
const segments = [];
|
|
77
77
|
let start = 0;
|
|
@@ -135,14 +135,14 @@ function isGitForcePushSegment(segment) {
|
|
|
135
135
|
}
|
|
136
136
|
return false;
|
|
137
137
|
}
|
|
138
|
-
function getSegmentCommandIndex(segment) {
|
|
138
|
+
export function getSegmentCommandIndex(segment) {
|
|
139
139
|
let idx = 0;
|
|
140
140
|
while (idx < segment.length && segment[idx].includes('=') && !segment[idx].startsWith('-')) {
|
|
141
141
|
idx++;
|
|
142
142
|
}
|
|
143
143
|
return idx;
|
|
144
144
|
}
|
|
145
|
-
function getExecutableName(token) {
|
|
145
|
+
export function getExecutableName(token) {
|
|
146
146
|
const normalized = token.toLowerCase();
|
|
147
147
|
const slash = normalized.lastIndexOf('/');
|
|
148
148
|
return slash >= 0 ? normalized.slice(slash + 1) : normalized;
|
package/dist/gateway.js
CHANGED
|
@@ -3,6 +3,7 @@ import Fastify from 'fastify';
|
|
|
3
3
|
import { existsSync } from 'fs';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
5
|
import { join } from 'path';
|
|
6
|
+
import { timingSafeEqual } from 'crypto';
|
|
6
7
|
import { runAgentTurn } from './agent.js';
|
|
7
8
|
import { getCronJobs, runCronJob } from './cron.js';
|
|
8
9
|
import { registerDashboardAPI } from './api.js';
|
|
@@ -32,6 +33,15 @@ export async function createGateway(cfg) {
|
|
|
32
33
|
level: 'info',
|
|
33
34
|
},
|
|
34
35
|
});
|
|
36
|
+
// Block cross-origin requests — deny all CORS preflight and tag responses
|
|
37
|
+
fastify.addHook('onRequest', async (request, reply) => {
|
|
38
|
+
if (request.method === 'OPTIONS') {
|
|
39
|
+
return reply.code(403).send({ error: 'CORS not allowed' });
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
fastify.addHook('onSend', async (_request, reply) => {
|
|
43
|
+
reply.header('Access-Control-Allow-Origin', 'null'); // deny all origins
|
|
44
|
+
});
|
|
35
45
|
// Health check
|
|
36
46
|
fastify.get('/health', async () => {
|
|
37
47
|
return { status: 'ok', uptime: Date.now() - startTime.getTime() };
|
|
@@ -96,10 +106,31 @@ export async function createGateway(cfg) {
|
|
|
96
106
|
fastify.post('/reload', async () => {
|
|
97
107
|
return { status: 'ok', note: 'Restart required for config changes' };
|
|
98
108
|
});
|
|
99
|
-
// Ensure dashboard token exists
|
|
109
|
+
// Ensure dashboard token exists
|
|
100
110
|
const dashboardToken = ensureDashboardToken(config);
|
|
101
|
-
console.log(`[dashboard] Access token: ${dashboardToken}`);
|
|
102
111
|
console.log(`[dashboard] URL: http://localhost:${config.gateway.port}/dashboard`);
|
|
112
|
+
// Auth guard for gateway write endpoints (same token as dashboard)
|
|
113
|
+
const PROTECTED_ROUTES = new Set(['/message', '/model', '/reload']);
|
|
114
|
+
fastify.addHook('onRequest', async (request, reply) => {
|
|
115
|
+
const url = request.url;
|
|
116
|
+
// Protect write endpoints + cron trigger
|
|
117
|
+
if (!PROTECTED_ROUTES.has(url) && !url.startsWith('/cron/'))
|
|
118
|
+
return;
|
|
119
|
+
if (request.method === 'GET')
|
|
120
|
+
return; // GET /health, GET /status are fine
|
|
121
|
+
if (!dashboardToken)
|
|
122
|
+
return; // No token configured, allow access
|
|
123
|
+
const authHeader = request.headers.authorization;
|
|
124
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
125
|
+
return reply.code(401).send({ error: 'Unauthorized: Bearer token required' });
|
|
126
|
+
}
|
|
127
|
+
const provided = authHeader.slice(7);
|
|
128
|
+
const tokenBuf = Buffer.from(dashboardToken, 'utf8');
|
|
129
|
+
const providedBuf = Buffer.from(provided, 'utf8');
|
|
130
|
+
if (tokenBuf.length !== providedBuf.length || !timingSafeEqual(tokenBuf, providedBuf)) {
|
|
131
|
+
return reply.code(401).send({ error: 'Unauthorized: Invalid token' });
|
|
132
|
+
}
|
|
133
|
+
});
|
|
103
134
|
// Register dashboard API routes (includes auth hook)
|
|
104
135
|
registerDashboardAPI(fastify, config);
|
|
105
136
|
// Register dashboard frontend (framework app)
|
package/dist/heartbeat.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { join } from 'path';
|
|
7
7
|
import { homedir } from 'os';
|
|
8
8
|
import { runAgentTurn } from './agent.js';
|
|
9
|
+
import { pruneIdle, SANDBOX_DEFAULTS } from './sandbox/index.js';
|
|
9
10
|
import { getActiveChannelId, isActiveChannelSilenced, sendActiveChannelProactiveMessage, } from './channels.js';
|
|
10
11
|
let heartbeatTimer = null;
|
|
11
12
|
let running = false;
|
|
@@ -78,6 +79,8 @@ export function stopHeartbeat() {
|
|
|
78
79
|
}
|
|
79
80
|
}
|
|
80
81
|
export async function runHeartbeatCheck(config) {
|
|
82
|
+
// Prune idle sandbox containers
|
|
83
|
+
pruneIdle(config.sandbox?.idleTimeoutMs ?? SANDBOX_DEFAULTS.idleTimeoutMs ?? 3_600_000).catch(() => { });
|
|
81
84
|
if (running) {
|
|
82
85
|
console.log('[heartbeat] Skipping — previous check still running');
|
|
83
86
|
return 'Skipped — previous check still running';
|
|
@@ -122,7 +122,7 @@ export async function chatWithToolsAnthropic(params) {
|
|
|
122
122
|
const modelId = stripProvider(options.model);
|
|
123
123
|
const maxIterations = toolConfig.maxIterations || 20;
|
|
124
124
|
// Resolve tools once at start of agent loop
|
|
125
|
-
const includeSpawn = !!(toolContext?.
|
|
125
|
+
const includeSpawn = !!(toolContext?.fullConfig && (toolContext?.chatId || toolContext?.isCronJob));
|
|
126
126
|
const toolDefs = await getToolDefinitions(toolConfig, { includeSpawnSubagent: includeSpawn, projects: toolContext?.fullConfig?.projects });
|
|
127
127
|
// Enable prompt caching for system + tools
|
|
128
128
|
const cacheEnabled = config.models?.promptCaching !== false;
|
package/dist/providers/codex.js
CHANGED
|
@@ -286,7 +286,7 @@ export async function chatWithToolsCodex(params) {
|
|
|
286
286
|
}
|
|
287
287
|
// Get tool definitions
|
|
288
288
|
const { getToolDefinitions } = await import('../tools.js');
|
|
289
|
-
const includeSpawn = !!(toolContext?.
|
|
289
|
+
const includeSpawn = !!(toolContext?.fullConfig && (toolContext?.chatId || toolContext?.isCronJob));
|
|
290
290
|
const toolDefs = await getToolDefinitions(toolConfig, {
|
|
291
291
|
includeSpawnSubagent: includeSpawn,
|
|
292
292
|
includeMcp: false,
|