skimpyclaw 0.3.14 → 0.4.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/README.md +47 -37
- package/dist/__tests__/adapter-types.test.d.ts +4 -0
- package/dist/__tests__/adapter-types.test.js +63 -0
- package/dist/__tests__/anthropic-adapter.test.d.ts +4 -0
- package/dist/__tests__/anthropic-adapter.test.js +264 -0
- package/dist/__tests__/api.test.js +0 -1
- package/dist/__tests__/cli.integration.test.js +2 -4
- package/dist/__tests__/cli.test.js +0 -1
- package/dist/__tests__/code-agents-notifications.test.js +137 -0
- package/dist/__tests__/code-agents-parser.test.js +19 -1
- package/dist/__tests__/code-agents-preflight.test.js +3 -28
- package/dist/__tests__/code-agents-utils.test.js +34 -9
- package/dist/__tests__/code-agents-worktrees.test.js +116 -0
- package/dist/__tests__/codex-adapter.test.js +184 -0
- package/dist/__tests__/codex-auth.test.js +66 -0
- package/dist/__tests__/codex-provider-gating.test.js +35 -0
- package/dist/__tests__/codex-unified-loop.test.js +111 -0
- package/dist/__tests__/config-security.test.js +127 -0
- package/dist/__tests__/config.test.js +23 -0
- package/dist/__tests__/context-manager.test.js +243 -164
- package/dist/__tests__/cron-run.test.js +250 -0
- package/dist/__tests__/cron.test.js +12 -38
- package/dist/__tests__/digests.test.js +67 -0
- package/dist/__tests__/discord-attachments.test.js +211 -0
- package/dist/__tests__/discord-docs.test.d.ts +1 -0
- package/dist/__tests__/discord-docs.test.js +27 -0
- package/dist/__tests__/discord-thread-agents.test.d.ts +1 -0
- package/dist/__tests__/discord-thread-agents.test.js +115 -0
- package/dist/__tests__/discord-thread-context.test.d.ts +1 -0
- package/dist/__tests__/discord-thread-context.test.js +42 -0
- package/dist/__tests__/doctor.formatters.test.js +4 -4
- package/dist/__tests__/doctor.index.test.js +1 -1
- package/dist/__tests__/doctor.runner.test.js +3 -15
- package/dist/__tests__/env-sanitizer.test.d.ts +1 -0
- package/dist/__tests__/env-sanitizer.test.js +45 -0
- package/dist/__tests__/exec-approval.test.js +61 -0
- package/dist/__tests__/fetch-tool.test.d.ts +1 -0
- package/dist/__tests__/fetch-tool.test.js +85 -0
- package/dist/__tests__/gateway-status-auth.test.d.ts +1 -0
- package/dist/__tests__/gateway-status-auth.test.js +72 -0
- package/dist/__tests__/heartbeat.test.js +3 -3
- package/dist/__tests__/interactive-sessions.test.d.ts +1 -0
- package/dist/__tests__/interactive-sessions.test.js +96 -0
- package/dist/__tests__/langfuse.test.js +6 -18
- package/dist/__tests__/model-selection.test.js +3 -4
- package/dist/__tests__/providers-init.test.js +2 -8
- package/dist/__tests__/providers-routing.test.js +1 -1
- package/dist/__tests__/providers-utils.test.js +13 -3
- package/dist/__tests__/sessions.test.js +14 -10
- package/dist/__tests__/setup.test.js +12 -29
- package/dist/__tests__/skills.test.js +10 -7
- package/dist/__tests__/stream-formatter.test.d.ts +1 -0
- package/dist/__tests__/stream-formatter.test.js +114 -0
- package/dist/__tests__/token-efficiency.test.js +131 -15
- package/dist/__tests__/tool-loop.test.d.ts +4 -0
- package/dist/__tests__/tool-loop.test.js +505 -0
- package/dist/__tests__/tools.test.js +101 -276
- package/dist/__tests__/utils.test.d.ts +1 -0
- package/dist/__tests__/utils.test.js +14 -0
- package/dist/__tests__/voice.test.js +21 -0
- package/dist/agent.js +35 -4
- package/dist/api.js +113 -37
- package/dist/channels/discord/attachments.d.ts +50 -0
- package/dist/channels/discord/attachments.js +137 -0
- package/dist/channels/discord/delegation.d.ts +5 -0
- package/dist/channels/discord/delegation.js +136 -0
- package/dist/channels/discord/handlers.js +694 -7
- package/dist/channels/discord/index.d.ts +16 -1
- package/dist/channels/discord/index.js +64 -1
- package/dist/channels/discord/thread-agents.d.ts +54 -0
- package/dist/channels/discord/thread-agents.js +323 -0
- package/dist/channels/discord/threads.d.ts +58 -0
- package/dist/channels/discord/threads.js +192 -0
- package/dist/channels/discord/types.js +4 -2
- package/dist/channels/discord/utils.d.ts +16 -0
- package/dist/channels/discord/utils.js +86 -6
- package/dist/channels/telegram/index.d.ts +1 -1
- package/dist/channels/telegram/types.js +1 -1
- package/dist/channels/telegram/utils.js +9 -3
- package/dist/channels.d.ts +1 -1
- package/dist/cli.js +20 -400
- package/dist/code-agents/executor.d.ts +1 -1
- package/dist/code-agents/executor.js +101 -45
- package/dist/code-agents/index.d.ts +2 -7
- package/dist/code-agents/index.js +111 -80
- package/dist/code-agents/interactive-resume.d.ts +6 -0
- package/dist/code-agents/interactive-resume.js +98 -0
- package/dist/code-agents/interactive-sessions.d.ts +20 -0
- package/dist/code-agents/interactive-sessions.js +132 -0
- package/dist/code-agents/parser.js +5 -1
- package/dist/code-agents/registry.d.ts +7 -1
- package/dist/code-agents/registry.js +11 -23
- package/dist/code-agents/stream-formatter.d.ts +8 -0
- package/dist/code-agents/stream-formatter.js +92 -0
- package/dist/code-agents/types.d.ts +16 -24
- package/dist/code-agents/utils.d.ts +35 -11
- package/dist/code-agents/utils.js +349 -95
- package/dist/code-agents/worktrees.d.ts +37 -0
- package/dist/code-agents/worktrees.js +116 -0
- package/dist/config.d.ts +2 -4
- package/dist/config.js +123 -23
- package/dist/cron.d.ts +1 -6
- package/dist/cron.js +175 -82
- package/dist/dashboard/assets/index-B345aOO-.js +65 -0
- package/dist/dashboard/assets/index-ZWK4dalJ.css +1 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/digests.d.ts +1 -0
- package/dist/digests.js +132 -42
- package/dist/doctor/checks.d.ts +0 -3
- package/dist/doctor/checks.js +1 -108
- package/dist/doctor/runner.js +1 -4
- package/dist/env-sanitizer.d.ts +2 -0
- package/dist/env-sanitizer.js +61 -0
- package/dist/exec-approval.d.ts +11 -1
- package/dist/exec-approval.js +17 -4
- package/dist/gateway.d.ts +3 -1
- package/dist/gateway.js +17 -7
- package/dist/heartbeat.js +1 -6
- package/dist/langfuse.js +3 -29
- package/dist/model-selection.js +3 -1
- package/dist/providers/adapter.d.ts +118 -0
- package/dist/providers/adapter.js +6 -0
- package/dist/providers/adapters/anthropic-adapter.d.ts +22 -0
- package/dist/providers/adapters/anthropic-adapter.js +204 -0
- package/dist/providers/adapters/codex-adapter.d.ts +26 -0
- package/dist/providers/adapters/codex-adapter.js +203 -0
- package/dist/providers/anthropic.d.ts +1 -0
- package/dist/providers/anthropic.js +10 -272
- package/dist/providers/codex.d.ts +21 -0
- package/dist/providers/codex.js +149 -330
- package/dist/providers/content.d.ts +1 -1
- package/dist/providers/content.js +2 -2
- package/dist/providers/context-manager.d.ts +18 -6
- package/dist/providers/context-manager.js +199 -223
- package/dist/providers/index.d.ts +9 -1
- package/dist/providers/index.js +73 -64
- package/dist/providers/loop-utils.d.ts +20 -0
- package/dist/providers/loop-utils.js +30 -0
- package/dist/providers/tool-loop.d.ts +12 -0
- package/dist/providers/tool-loop.js +251 -0
- package/dist/providers/utils.d.ts +19 -3
- package/dist/providers/utils.js +100 -29
- package/dist/secure-store.d.ts +8 -0
- package/dist/secure-store.js +80 -0
- package/dist/service.js +3 -28
- package/dist/sessions.d.ts +3 -0
- package/dist/sessions.js +147 -18
- package/dist/setup-templates.js +13 -25
- package/dist/setup.d.ts +10 -6
- package/dist/setup.js +84 -292
- package/dist/skills.js +3 -11
- package/dist/tools/agent-delegation.d.ts +19 -0
- package/dist/tools/agent-delegation.js +49 -0
- package/dist/tools/bash-tool.js +89 -34
- package/dist/tools/definitions.d.ts +199 -302
- package/dist/tools/definitions.js +70 -123
- package/dist/tools/execute-context.d.ts +13 -4
- package/dist/tools/fetch-tool.js +109 -13
- package/dist/tools/file-tools.js +7 -1
- package/dist/tools.d.ts +7 -7
- package/dist/tools.js +133 -151
- package/dist/types.d.ts +37 -30
- package/dist/utils.js +4 -6
- package/dist/voice.d.ts +1 -1
- package/dist/voice.js +17 -4
- package/package.json +33 -23
- package/templates/TOOLS.md +0 -27
- package/dist/__tests__/audit.test.js +0 -122
- package/dist/__tests__/code-agents-orchestrator.test.js +0 -216
- package/dist/__tests__/code-agents-sandbox.test.js +0 -163
- package/dist/__tests__/orchestrator.test.js +0 -425
- package/dist/__tests__/sandbox-bridge.test.js +0 -116
- package/dist/__tests__/sandbox-manager.test.js +0 -144
- package/dist/__tests__/sandbox-mount-security.test.js +0 -139
- package/dist/__tests__/sandbox-runtime.test.js +0 -176
- package/dist/__tests__/subagent.test.js +0 -240
- package/dist/__tests__/telegram.test.js +0 -42
- package/dist/code-agents/orchestrator.d.ts +0 -29
- package/dist/code-agents/orchestrator.js +0 -694
- package/dist/code-agents/worktree.d.ts +0 -40
- package/dist/code-agents/worktree.js +0 -215
- package/dist/dashboard/assets/index-BoTHPby4.js +0 -65
- package/dist/dashboard/assets/index-D4mufvBg.css +0 -1
- package/dist/dashboard.d.ts +0 -8
- package/dist/dashboard.js +0 -4071
- package/dist/discord.d.ts +0 -8
- package/dist/discord.js +0 -792
- package/dist/mcp-context-a8c.d.ts +0 -13
- package/dist/mcp-context-a8c.js +0 -34
- package/dist/orchestrator.d.ts +0 -15
- package/dist/orchestrator.js +0 -676
- package/dist/providers/openai.d.ts +0 -10
- package/dist/providers/openai.js +0 -355
- package/dist/sandbox/bridge.d.ts +0 -5
- package/dist/sandbox/bridge.js +0 -63
- package/dist/sandbox/index.d.ts +0 -5
- package/dist/sandbox/index.js +0 -4
- package/dist/sandbox/manager.d.ts +0 -7
- package/dist/sandbox/manager.js +0 -100
- package/dist/sandbox/mount-security.d.ts +0 -12
- package/dist/sandbox/mount-security.js +0 -122
- package/dist/sandbox/runtime.d.ts +0 -39
- package/dist/sandbox/runtime.js +0 -192
- package/dist/sandbox-utils.d.ts +0 -6
- package/dist/sandbox-utils.js +0 -36
- package/dist/subagent.d.ts +0 -19
- package/dist/subagent.js +0 -407
- package/dist/telegram.d.ts +0 -2
- package/dist/telegram.js +0 -11
- package/dist/tools/browser-tool.d.ts +0 -3
- package/dist/tools/browser-tool.js +0 -266
- package/sandbox/Dockerfile +0 -40
- /package/dist/__tests__/{audit.test.d.ts → code-agents-notifications.test.d.ts} +0 -0
- /package/dist/__tests__/{code-agents-orchestrator.test.d.ts → code-agents-worktrees.test.d.ts} +0 -0
- /package/dist/__tests__/{code-agents-sandbox.test.d.ts → codex-adapter.test.d.ts} +0 -0
- /package/dist/__tests__/{orchestrator.test.d.ts → codex-auth.test.d.ts} +0 -0
- /package/dist/__tests__/{sandbox-bridge.test.d.ts → codex-provider-gating.test.d.ts} +0 -0
- /package/dist/__tests__/{sandbox-manager.test.d.ts → codex-unified-loop.test.d.ts} +0 -0
- /package/dist/__tests__/{sandbox-mount-security.test.d.ts → config-security.test.d.ts} +0 -0
- /package/dist/__tests__/{sandbox-runtime.test.d.ts → cron-run.test.d.ts} +0 -0
- /package/dist/__tests__/{subagent.test.d.ts → digests.test.d.ts} +0 -0
- /package/dist/__tests__/{telegram.test.d.ts → discord-attachments.test.d.ts} +0 -0
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
const { mockCreateContainer, mockRemoveContainer, mockIsContainerRunning, mockValidateMountPaths, } = vi.hoisted(() => ({
|
|
3
|
-
mockCreateContainer: vi.fn(),
|
|
4
|
-
mockRemoveContainer: vi.fn(),
|
|
5
|
-
mockIsContainerRunning: vi.fn(),
|
|
6
|
-
mockValidateMountPaths: vi.fn(),
|
|
7
|
-
}));
|
|
8
|
-
vi.mock('../sandbox/runtime.js', () => ({
|
|
9
|
-
createContainer: mockCreateContainer,
|
|
10
|
-
removeContainer: mockRemoveContainer,
|
|
11
|
-
isContainerRunning: mockIsContainerRunning,
|
|
12
|
-
}));
|
|
13
|
-
vi.mock('../sandbox/mount-security.js', () => ({
|
|
14
|
-
validateMountPaths: mockValidateMountPaths,
|
|
15
|
-
}));
|
|
16
|
-
import { ensureContainer, releaseContainer, pruneIdle, releaseAll, resetForTesting, SANDBOX_DEFAULTS, } from '../sandbox/manager.js';
|
|
17
|
-
const testConfig = { ...SANDBOX_DEFAULTS, enabled: true };
|
|
18
|
-
describe('sandbox/manager', () => {
|
|
19
|
-
beforeEach(() => {
|
|
20
|
-
resetForTesting();
|
|
21
|
-
vi.clearAllMocks();
|
|
22
|
-
mockValidateMountPaths.mockReturnValue([
|
|
23
|
-
{ host: '/home/user/project', container: '/workspace/project', readOnly: false },
|
|
24
|
-
]);
|
|
25
|
-
mockCreateContainer.mockResolvedValue(undefined);
|
|
26
|
-
mockRemoveContainer.mockResolvedValue(undefined);
|
|
27
|
-
});
|
|
28
|
-
describe('ensureContainer', () => {
|
|
29
|
-
it('creates container on first call', async () => {
|
|
30
|
-
const name = await ensureContainer('sess1', testConfig, ['/home/user/project']);
|
|
31
|
-
expect(name).toBe('skimpyclaw-sbx-sess1');
|
|
32
|
-
expect(mockCreateContainer).toHaveBeenCalledTimes(1);
|
|
33
|
-
});
|
|
34
|
-
it('reuses on second call if running', async () => {
|
|
35
|
-
mockIsContainerRunning
|
|
36
|
-
.mockResolvedValueOnce(false)
|
|
37
|
-
.mockResolvedValueOnce(true);
|
|
38
|
-
await ensureContainer('sess1', testConfig, ['/p']);
|
|
39
|
-
const name = await ensureContainer('sess1', testConfig, ['/p']);
|
|
40
|
-
expect(name).toBe('skimpyclaw-sbx-sess1');
|
|
41
|
-
expect(mockCreateContainer).toHaveBeenCalledTimes(1); // only first call
|
|
42
|
-
});
|
|
43
|
-
it('recreates if container died', async () => {
|
|
44
|
-
mockIsContainerRunning.mockResolvedValue(false);
|
|
45
|
-
await ensureContainer('sess1', testConfig, ['/p']);
|
|
46
|
-
// Second call — container exists in map but isContainerRunning returns false
|
|
47
|
-
await ensureContainer('sess1', testConfig, ['/p']);
|
|
48
|
-
expect(mockCreateContainer).toHaveBeenCalledTimes(2);
|
|
49
|
-
});
|
|
50
|
-
it('adopts existing running container after process restart', async () => {
|
|
51
|
-
mockIsContainerRunning.mockResolvedValue(true);
|
|
52
|
-
const name = await ensureContainer('default', testConfig, ['/p']);
|
|
53
|
-
expect(name).toBe('skimpyclaw-sbx-default');
|
|
54
|
-
expect(mockCreateContainer).not.toHaveBeenCalled();
|
|
55
|
-
expect(mockRemoveContainer).not.toHaveBeenCalled();
|
|
56
|
-
});
|
|
57
|
-
it('removes stale named container before creating', async () => {
|
|
58
|
-
mockIsContainerRunning.mockResolvedValue(false);
|
|
59
|
-
const name = await ensureContainer('default', testConfig, ['/p']);
|
|
60
|
-
expect(name).toBe('skimpyclaw-sbx-default');
|
|
61
|
-
expect(mockRemoveContainer).toHaveBeenCalledWith('skimpyclaw-sbx-default');
|
|
62
|
-
expect(mockCreateContainer).toHaveBeenCalledTimes(1);
|
|
63
|
-
});
|
|
64
|
-
it('expands ${VAR} references in env from process.env', async () => {
|
|
65
|
-
process.env.MY_SECRET = 'hunter2';
|
|
66
|
-
try {
|
|
67
|
-
const configWithEnv = {
|
|
68
|
-
...testConfig,
|
|
69
|
-
env: { TOKEN: '${MY_SECRET}', PLAIN: 'literal' },
|
|
70
|
-
};
|
|
71
|
-
await ensureContainer('env-test', configWithEnv, ['/p']);
|
|
72
|
-
const opts = mockCreateContainer.mock.calls[0][1];
|
|
73
|
-
expect(opts.env).toEqual({ TOKEN: 'hunter2', PLAIN: 'literal' });
|
|
74
|
-
}
|
|
75
|
-
finally {
|
|
76
|
-
delete process.env.MY_SECRET;
|
|
77
|
-
}
|
|
78
|
-
});
|
|
79
|
-
it('expands unset ${VAR} to empty string', async () => {
|
|
80
|
-
delete process.env.NONEXISTENT_VAR_XYZ;
|
|
81
|
-
const configWithEnv = {
|
|
82
|
-
...testConfig,
|
|
83
|
-
env: { VAL: '${NONEXISTENT_VAR_XYZ}' },
|
|
84
|
-
};
|
|
85
|
-
await ensureContainer('env-test2', configWithEnv, ['/p']);
|
|
86
|
-
const opts = mockCreateContainer.mock.calls[0][1];
|
|
87
|
-
expect(opts.env).toEqual({ VAL: '' });
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
describe('releaseContainer', () => {
|
|
91
|
-
it('removes container and clears from map', async () => {
|
|
92
|
-
await ensureContainer('sess1', testConfig, ['/p']);
|
|
93
|
-
await releaseContainer('sess1');
|
|
94
|
-
expect(mockRemoveContainer).toHaveBeenCalledWith('skimpyclaw-sbx-sess1');
|
|
95
|
-
// Next ensureContainer should create fresh
|
|
96
|
-
await ensureContainer('sess1', testConfig, ['/p']);
|
|
97
|
-
expect(mockCreateContainer).toHaveBeenCalledTimes(2);
|
|
98
|
-
});
|
|
99
|
-
it('no-ops for unknown session', async () => {
|
|
100
|
-
await expect(releaseContainer('unknown')).resolves.toBeUndefined();
|
|
101
|
-
expect(mockRemoveContainer).not.toHaveBeenCalled();
|
|
102
|
-
});
|
|
103
|
-
});
|
|
104
|
-
describe('pruneIdle', () => {
|
|
105
|
-
it('removes containers older than threshold', async () => {
|
|
106
|
-
vi.useFakeTimers();
|
|
107
|
-
try {
|
|
108
|
-
await ensureContainer('old', testConfig, ['/p']);
|
|
109
|
-
// Advance time by 10 seconds so the container is idle
|
|
110
|
-
vi.advanceTimersByTime(10_000);
|
|
111
|
-
const pruned = await pruneIdle(5_000);
|
|
112
|
-
expect(pruned).toBe(1);
|
|
113
|
-
expect(mockRemoveContainer).toHaveBeenCalledWith('skimpyclaw-sbx-old');
|
|
114
|
-
}
|
|
115
|
-
finally {
|
|
116
|
-
vi.useRealTimers();
|
|
117
|
-
}
|
|
118
|
-
});
|
|
119
|
-
it('keeps recent containers', async () => {
|
|
120
|
-
await ensureContainer('new', testConfig, ['/p']);
|
|
121
|
-
const pruned = await pruneIdle(60_000);
|
|
122
|
-
expect(pruned).toBe(0);
|
|
123
|
-
});
|
|
124
|
-
});
|
|
125
|
-
describe('releaseAll', () => {
|
|
126
|
-
it('removes all containers', async () => {
|
|
127
|
-
mockIsContainerRunning.mockResolvedValue(true);
|
|
128
|
-
await ensureContainer('a', testConfig, ['/p']);
|
|
129
|
-
await ensureContainer('b', testConfig, ['/p']);
|
|
130
|
-
await releaseAll();
|
|
131
|
-
expect(mockRemoveContainer).toHaveBeenCalledTimes(2);
|
|
132
|
-
});
|
|
133
|
-
});
|
|
134
|
-
describe('resetForTesting', () => {
|
|
135
|
-
it('clears state without removing containers', async () => {
|
|
136
|
-
mockIsContainerRunning.mockResolvedValue(false);
|
|
137
|
-
await ensureContainer('x', testConfig, ['/p']);
|
|
138
|
-
resetForTesting();
|
|
139
|
-
// Next call should create fresh (map is empty)
|
|
140
|
-
await ensureContainer('x', testConfig, ['/p']);
|
|
141
|
-
expect(mockCreateContainer).toHaveBeenCalledTimes(2);
|
|
142
|
-
});
|
|
143
|
-
});
|
|
144
|
-
});
|
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
vi.mock('fs', async () => {
|
|
3
|
-
const actual = await vi.importActual('fs');
|
|
4
|
-
return {
|
|
5
|
-
...actual,
|
|
6
|
-
existsSync: vi.fn(),
|
|
7
|
-
realpathSync: vi.fn(),
|
|
8
|
-
};
|
|
9
|
-
});
|
|
10
|
-
import { existsSync, realpathSync } from 'fs';
|
|
11
|
-
import { isBlockedPath, validateMountPaths, translatePath } from '../sandbox/mount-security.js';
|
|
12
|
-
const mockExistsSync = existsSync;
|
|
13
|
-
const mockRealpathSync = realpathSync;
|
|
14
|
-
describe('sandbox/mount-security', () => {
|
|
15
|
-
beforeEach(() => {
|
|
16
|
-
vi.clearAllMocks();
|
|
17
|
-
});
|
|
18
|
-
describe('isBlockedPath', () => {
|
|
19
|
-
it('blocks .ssh', () => {
|
|
20
|
-
expect(isBlockedPath('/home/user/.ssh')).toBe(true);
|
|
21
|
-
expect(isBlockedPath('/home/user/.ssh/id_rsa')).toBe(true);
|
|
22
|
-
});
|
|
23
|
-
it('blocks .gnupg', () => {
|
|
24
|
-
expect(isBlockedPath('/home/user/.gnupg')).toBe(true);
|
|
25
|
-
});
|
|
26
|
-
it('blocks .aws', () => {
|
|
27
|
-
expect(isBlockedPath('/home/user/.aws')).toBe(true);
|
|
28
|
-
});
|
|
29
|
-
it('blocks credentials', () => {
|
|
30
|
-
expect(isBlockedPath('/some/path/credentials')).toBe(true);
|
|
31
|
-
});
|
|
32
|
-
it('blocks .env', () => {
|
|
33
|
-
expect(isBlockedPath('/project/.env')).toBe(true);
|
|
34
|
-
});
|
|
35
|
-
it('blocks .kube', () => {
|
|
36
|
-
expect(isBlockedPath('/home/user/.kube/config')).toBe(true);
|
|
37
|
-
});
|
|
38
|
-
it('blocks .docker/config.json compound pattern', () => {
|
|
39
|
-
expect(isBlockedPath('/home/user/.docker/config.json')).toBe(true);
|
|
40
|
-
});
|
|
41
|
-
it('allows normal paths', () => {
|
|
42
|
-
expect(isBlockedPath('/home/user/project')).toBe(false);
|
|
43
|
-
expect(isBlockedPath('/workspace/src/index.ts')).toBe(false);
|
|
44
|
-
expect(isBlockedPath('/tmp/test')).toBe(false);
|
|
45
|
-
});
|
|
46
|
-
});
|
|
47
|
-
describe('validateMountPaths', () => {
|
|
48
|
-
beforeEach(() => {
|
|
49
|
-
mockExistsSync.mockReturnValue(true);
|
|
50
|
-
mockRealpathSync.mockImplementation((p) => p);
|
|
51
|
-
});
|
|
52
|
-
it('maps to /workspace/<basename>', () => {
|
|
53
|
-
const mounts = validateMountPaths(['/home/user/myproject']);
|
|
54
|
-
const projectMount = mounts.find((m) => m.host === '/home/user/myproject');
|
|
55
|
-
expect(projectMount).toBeDefined();
|
|
56
|
-
expect(projectMount.container).toBe('/workspace/myproject');
|
|
57
|
-
expect(projectMount.readOnly).toBe(false);
|
|
58
|
-
});
|
|
59
|
-
it('deduplicates basenames with suffix', () => {
|
|
60
|
-
const mounts = validateMountPaths(['/a/project', '/b/project']);
|
|
61
|
-
const containers = mounts.filter((m) => m.host.endsWith('/project')).map((m) => m.container);
|
|
62
|
-
expect(containers).toContain('/workspace/project');
|
|
63
|
-
expect(containers).toContain('/workspace/project_1');
|
|
64
|
-
});
|
|
65
|
-
it('adds ~/.skimpyclaw at /workspace/config', () => {
|
|
66
|
-
const mounts = validateMountPaths(['/home/user/project']);
|
|
67
|
-
const configMount = mounts.find((m) => m.container === '/workspace/config');
|
|
68
|
-
expect(configMount).toBeDefined();
|
|
69
|
-
});
|
|
70
|
-
it('throws on blocked paths', () => {
|
|
71
|
-
expect(() => validateMountPaths(['/home/user/.ssh'])).toThrow('Blocked path');
|
|
72
|
-
});
|
|
73
|
-
it('skips missing paths', () => {
|
|
74
|
-
mockExistsSync.mockImplementation((p) => {
|
|
75
|
-
// Only ~/.skimpyclaw exists for the auto-add
|
|
76
|
-
return typeof p === 'string' && p.includes('.skimpyclaw');
|
|
77
|
-
});
|
|
78
|
-
const mounts = validateMountPaths(['/nonexistent/path']);
|
|
79
|
-
// Should only have the auto-added config mount
|
|
80
|
-
const nonConfig = mounts.filter((m) => m.container !== '/workspace/config');
|
|
81
|
-
expect(nonConfig).toHaveLength(0);
|
|
82
|
-
});
|
|
83
|
-
it('does not duplicate ~/.skimpyclaw if already in list', () => {
|
|
84
|
-
// Simulate ~/.skimpyclaw being passed explicitly
|
|
85
|
-
const home = process.env.HOME || '/Users/katre';
|
|
86
|
-
const skDir = `${home}/.skimpyclaw`;
|
|
87
|
-
mockRealpathSync.mockImplementation((p) => p);
|
|
88
|
-
// .skimpyclaw is NOT blocked, so it should be mounted
|
|
89
|
-
// But it should not appear twice
|
|
90
|
-
const mounts = validateMountPaths([skDir]);
|
|
91
|
-
const configMounts = mounts.filter((m) => m.host === skDir);
|
|
92
|
-
expect(configMounts).toHaveLength(1);
|
|
93
|
-
});
|
|
94
|
-
});
|
|
95
|
-
describe('translatePath', () => {
|
|
96
|
-
const mounts = [
|
|
97
|
-
{ host: '/Users/katre/Sites/skimpyclaw', container: '/workspace/skimpyclaw', readOnly: false },
|
|
98
|
-
{ host: '/Users/katre/.skimpyclaw', container: '/workspace/config', readOnly: false },
|
|
99
|
-
];
|
|
100
|
-
it('translates host path to container path', () => {
|
|
101
|
-
expect(translatePath('/Users/katre/Sites/skimpyclaw/src/index.ts', mounts))
|
|
102
|
-
.toBe('/workspace/skimpyclaw/src/index.ts');
|
|
103
|
-
});
|
|
104
|
-
it('translates exact mount root', () => {
|
|
105
|
-
expect(translatePath('/Users/katre/Sites/skimpyclaw', mounts))
|
|
106
|
-
.toBe('/workspace/skimpyclaw');
|
|
107
|
-
});
|
|
108
|
-
it('translates config path', () => {
|
|
109
|
-
expect(translatePath('/Users/katre/.skimpyclaw/config.json', mounts))
|
|
110
|
-
.toBe('/workspace/config/config.json');
|
|
111
|
-
});
|
|
112
|
-
it('translates /Users path to /System/Volumes/Data mount on macOS', () => {
|
|
113
|
-
const macMounts = [
|
|
114
|
-
{ host: '/System/Volumes/Data/Users/katre/.skimpyclaw', container: '/workspace/config', readOnly: false },
|
|
115
|
-
];
|
|
116
|
-
expect(translatePath('/Users/katre/.skimpyclaw/state/japan-flight-watch.json', macMounts))
|
|
117
|
-
.toBe('/workspace/config/state/japan-flight-watch.json');
|
|
118
|
-
});
|
|
119
|
-
it('returns original path if no mount matches', () => {
|
|
120
|
-
expect(translatePath('/tmp/random/file', mounts)).toBe('/tmp/random/file');
|
|
121
|
-
});
|
|
122
|
-
it('expands ~ to home directory before matching', () => {
|
|
123
|
-
const home = process.env.HOME || '/Users/katre';
|
|
124
|
-
const homeMounts = [
|
|
125
|
-
{ host: `${home}/.skimpyclaw`, container: '/workspace/config', readOnly: false },
|
|
126
|
-
];
|
|
127
|
-
expect(translatePath('~/.skimpyclaw/agents/main/HEARTBEAT.md', homeMounts))
|
|
128
|
-
.toBe('/workspace/config/agents/main/HEARTBEAT.md');
|
|
129
|
-
});
|
|
130
|
-
it('matches most specific mount first', () => {
|
|
131
|
-
const nestedMounts = [
|
|
132
|
-
{ host: '/Users/katre', container: '/workspace/home', readOnly: false },
|
|
133
|
-
{ host: '/Users/katre/Sites/skimpyclaw', container: '/workspace/skimpyclaw', readOnly: false },
|
|
134
|
-
];
|
|
135
|
-
expect(translatePath('/Users/katre/Sites/skimpyclaw/src/file.ts', nestedMounts))
|
|
136
|
-
.toBe('/workspace/skimpyclaw/src/file.ts');
|
|
137
|
-
});
|
|
138
|
-
});
|
|
139
|
-
});
|
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
const { mockSpawn, mockSpawnSync } = vi.hoisted(() => ({
|
|
3
|
-
mockSpawn: vi.fn(),
|
|
4
|
-
mockSpawnSync: vi.fn().mockReturnValue({ status: 0, stdout: '', stderr: '' }),
|
|
5
|
-
}));
|
|
6
|
-
vi.mock('child_process', () => ({ spawn: mockSpawn, spawnSync: mockSpawnSync }));
|
|
7
|
-
import { createContainer, execInContainer, removeContainer, isContainerRunning, cleanupOrphans, setRuntime, resetRuntime, probeRuntime, } from '../sandbox/runtime.js';
|
|
8
|
-
function fakeChild(exitCode, stdout = '', stderr = '', opts) {
|
|
9
|
-
const stdoutCallbacks = [];
|
|
10
|
-
const stderrCallbacks = [];
|
|
11
|
-
const eventCallbacks = {};
|
|
12
|
-
const child = {
|
|
13
|
-
stdout: { on: (_e, cb) => stdoutCallbacks.push(cb) },
|
|
14
|
-
stderr: { on: (_e, cb) => stderrCallbacks.push(cb) },
|
|
15
|
-
stdin: opts?.stdinNeeded ? { write: vi.fn(), end: vi.fn() } : null,
|
|
16
|
-
on(event, cb) {
|
|
17
|
-
if (!eventCallbacks[event])
|
|
18
|
-
eventCallbacks[event] = [];
|
|
19
|
-
eventCallbacks[event].push(cb);
|
|
20
|
-
return child;
|
|
21
|
-
},
|
|
22
|
-
};
|
|
23
|
-
// Emit data and close on next tick (setTimeout ensures listeners are attached first)
|
|
24
|
-
setTimeout(() => {
|
|
25
|
-
if (stdout)
|
|
26
|
-
for (const cb of stdoutCallbacks)
|
|
27
|
-
cb(Buffer.from(stdout));
|
|
28
|
-
if (stderr)
|
|
29
|
-
for (const cb of stderrCallbacks)
|
|
30
|
-
cb(Buffer.from(stderr));
|
|
31
|
-
for (const cb of eventCallbacks['close'] ?? [])
|
|
32
|
-
cb(exitCode);
|
|
33
|
-
}, 0);
|
|
34
|
-
return child;
|
|
35
|
-
}
|
|
36
|
-
describe('sandbox/runtime', () => {
|
|
37
|
-
beforeEach(() => {
|
|
38
|
-
vi.clearAllMocks();
|
|
39
|
-
resetRuntime();
|
|
40
|
-
setRuntime('container'); // skip auto-detection in tests
|
|
41
|
-
});
|
|
42
|
-
describe('createContainer', () => {
|
|
43
|
-
it('builds correct docker args', async () => {
|
|
44
|
-
mockSpawn.mockReturnValue(fakeChild(0));
|
|
45
|
-
await createContainer('test-ctr', {
|
|
46
|
-
image: 'myimg',
|
|
47
|
-
cpus: 2,
|
|
48
|
-
memory: '1G',
|
|
49
|
-
network: 'none',
|
|
50
|
-
user: '501:20',
|
|
51
|
-
mounts: [{ host: '/a', container: '/b', readOnly: true }],
|
|
52
|
-
});
|
|
53
|
-
const args = mockSpawn.mock.calls[0][1];
|
|
54
|
-
expect(args).toContain('--name');
|
|
55
|
-
expect(args).toContain('test-ctr');
|
|
56
|
-
expect(args).toContain('--cpus');
|
|
57
|
-
expect(args).toContain('2');
|
|
58
|
-
expect(args).toContain('--memory');
|
|
59
|
-
expect(args).toContain('1G');
|
|
60
|
-
expect(args).toContain('--network');
|
|
61
|
-
expect(args).toContain('none');
|
|
62
|
-
expect(args).toContain('--user');
|
|
63
|
-
expect(args).toContain('501:20');
|
|
64
|
-
expect(args.some((a) => a.includes('type=bind,src=/a,dst=/b,ro'))).toBe(true);
|
|
65
|
-
expect(args).toContain('myimg');
|
|
66
|
-
});
|
|
67
|
-
it('throws on non-zero exit', async () => {
|
|
68
|
-
mockSpawn.mockReturnValue(fakeChild(1, '', 'boom'));
|
|
69
|
-
await expect(createContainer('c1', { image: 'img' })).rejects.toThrow('Failed to create container');
|
|
70
|
-
});
|
|
71
|
-
});
|
|
72
|
-
describe('execInContainer', () => {
|
|
73
|
-
it('builds sh -c command', async () => {
|
|
74
|
-
mockSpawn.mockReturnValue(fakeChild(0, 'hello'));
|
|
75
|
-
const result = await execInContainer('ctr', ['echo hello']);
|
|
76
|
-
expect(result.stdout).toBe('hello');
|
|
77
|
-
const args = mockSpawn.mock.calls[0][1];
|
|
78
|
-
expect(args).toContain('sh');
|
|
79
|
-
expect(args).toContain('-c');
|
|
80
|
-
});
|
|
81
|
-
it('handles stdin', async () => {
|
|
82
|
-
mockSpawn.mockReturnValue(fakeChild(0, '', '', { stdinNeeded: true }));
|
|
83
|
-
await execInContainer('ctr', ['cat'], { stdin: 'data' });
|
|
84
|
-
const args = mockSpawn.mock.calls[0][1];
|
|
85
|
-
expect(args).toContain('-i');
|
|
86
|
-
});
|
|
87
|
-
it('handles env vars', async () => {
|
|
88
|
-
mockSpawn.mockReturnValue(fakeChild(0));
|
|
89
|
-
await execInContainer('ctr', ['cmd'], { env: { FOO: 'bar' } });
|
|
90
|
-
const args = mockSpawn.mock.calls[0][1];
|
|
91
|
-
expect(args).toContain('-e');
|
|
92
|
-
expect(args).toContain('FOO=bar');
|
|
93
|
-
});
|
|
94
|
-
});
|
|
95
|
-
describe('removeContainer', () => {
|
|
96
|
-
it('calls stop then rm', async () => {
|
|
97
|
-
mockSpawn.mockImplementation((_cmd, args) => {
|
|
98
|
-
return fakeChild(0);
|
|
99
|
-
});
|
|
100
|
-
await removeContainer('ctr');
|
|
101
|
-
expect(mockSpawn).toHaveBeenCalledTimes(2);
|
|
102
|
-
expect(mockSpawn.mock.calls[0][1]).toContain('stop');
|
|
103
|
-
expect(mockSpawn.mock.calls[1][1]).toContain('rm');
|
|
104
|
-
}, 10_000);
|
|
105
|
-
it('does not throw on failure', async () => {
|
|
106
|
-
mockSpawn.mockImplementation(() => fakeChild(1, '', 'fail'));
|
|
107
|
-
await expect(removeContainer('ctr')).resolves.toBeUndefined();
|
|
108
|
-
}, 10_000);
|
|
109
|
-
});
|
|
110
|
-
describe('isContainerRunning', () => {
|
|
111
|
-
it('returns true when inspect shows running state', async () => {
|
|
112
|
-
mockSpawn.mockReturnValue(fakeChild(0, '{"State": {"Status": "running"}}'));
|
|
113
|
-
expect(await isContainerRunning('ctr')).toBe(true);
|
|
114
|
-
});
|
|
115
|
-
it('returns false otherwise', async () => {
|
|
116
|
-
mockSpawn.mockReturnValue(fakeChild(1));
|
|
117
|
-
expect(await isContainerRunning('ctr')).toBe(false);
|
|
118
|
-
});
|
|
119
|
-
});
|
|
120
|
-
describe('probeRuntime', () => {
|
|
121
|
-
it('returns preferred runtime when available', () => {
|
|
122
|
-
mockSpawnSync.mockReturnValue({ status: 0 });
|
|
123
|
-
expect(probeRuntime('docker')).toBe('docker');
|
|
124
|
-
expect(mockSpawnSync).toHaveBeenCalledWith('docker', ['--version'], { stdio: 'ignore' });
|
|
125
|
-
});
|
|
126
|
-
it('falls back to auto-detect when preferred is unavailable', () => {
|
|
127
|
-
mockSpawnSync.mockImplementation((cmd) => {
|
|
128
|
-
// preferred 'docker' fails, but 'container' succeeds
|
|
129
|
-
if (cmd === 'docker')
|
|
130
|
-
return { status: 1 };
|
|
131
|
-
if (cmd === 'container')
|
|
132
|
-
return { status: 0 };
|
|
133
|
-
return { status: 1 };
|
|
134
|
-
});
|
|
135
|
-
expect(probeRuntime('docker')).toBe('container');
|
|
136
|
-
});
|
|
137
|
-
it('returns null when no runtime is available', () => {
|
|
138
|
-
mockSpawnSync.mockReturnValue({ status: 1 });
|
|
139
|
-
expect(probeRuntime('docker')).toBeNull();
|
|
140
|
-
});
|
|
141
|
-
it('auto-detects without preferred runtime', () => {
|
|
142
|
-
mockSpawnSync.mockImplementation((cmd) => {
|
|
143
|
-
if (cmd === 'container')
|
|
144
|
-
return { status: 0 };
|
|
145
|
-
return { status: 1 };
|
|
146
|
-
});
|
|
147
|
-
expect(probeRuntime()).toBe('container');
|
|
148
|
-
});
|
|
149
|
-
it('prefers container over docker in auto-detect', () => {
|
|
150
|
-
mockSpawnSync.mockReturnValue({ status: 0 });
|
|
151
|
-
expect(probeRuntime()).toBe('container');
|
|
152
|
-
// First call should be to 'container'
|
|
153
|
-
expect(mockSpawnSync.mock.calls[0][0]).toBe('container');
|
|
154
|
-
});
|
|
155
|
-
});
|
|
156
|
-
describe('cleanupOrphans', () => {
|
|
157
|
-
it('lists containers, filters by prefix, removes matches', async () => {
|
|
158
|
-
let callCount = 0;
|
|
159
|
-
mockSpawn.mockImplementation(() => {
|
|
160
|
-
callCount++;
|
|
161
|
-
if (callCount === 1) {
|
|
162
|
-
// ps call
|
|
163
|
-
return fakeChild(0, 'skimpyclaw-sbx-abc\nother-ctr\nskimpyclaw-sbx-def\n');
|
|
164
|
-
}
|
|
165
|
-
// stop/rm calls
|
|
166
|
-
return fakeChild(0);
|
|
167
|
-
});
|
|
168
|
-
const count = await cleanupOrphans();
|
|
169
|
-
expect(count).toBe(2);
|
|
170
|
-
});
|
|
171
|
-
it('returns 0 on ps failure', async () => {
|
|
172
|
-
mockSpawn.mockImplementation(() => fakeChild(1));
|
|
173
|
-
expect(await cleanupOrphans()).toBe(0);
|
|
174
|
-
});
|
|
175
|
-
});
|
|
176
|
-
});
|