keystone-cli 2.1.5 → 2.1.6
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/package.json +1 -1
- package/src/cli.ts +0 -1
- package/src/runner/executors/verification_fixes.test.ts +9 -7
- package/src/runner/mcp-client-audit.test.ts +7 -14
- package/src/runner/mcp-client.test.ts +9 -9
- package/src/runner/shell-executor.test.ts +13 -9
- package/src/runner/standard-tools-integration.test.ts +15 -19
- package/src/runner/standard-tools.ts +1 -2
- package/src/runner/step-executor.test.ts +8 -7
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -20,25 +20,27 @@ describe('Verification Fixes', () => {
|
|
|
20
20
|
describe('Shell Path Traversal (shell-executor)', () => {
|
|
21
21
|
const mockContext = { env: {}, steps: {}, inputs: {}, envOverrides: {}, secrets: {} };
|
|
22
22
|
|
|
23
|
-
test('should
|
|
23
|
+
test('should allow command with ".." and "/"', async () => {
|
|
24
24
|
const step = {
|
|
25
25
|
id: 'test',
|
|
26
26
|
type: 'shell' as const,
|
|
27
27
|
run: 'cat ../secret.txt',
|
|
28
28
|
};
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
//
|
|
32
|
-
|
|
29
|
+
// Commands with .. are allowed (only dir step property is validated)
|
|
30
|
+
const result = await executeShell(step, mockContext);
|
|
31
|
+
// Will fail because file doesn't exist, but not blocked
|
|
32
|
+
expect(result.exitCode).toBe(1);
|
|
33
33
|
});
|
|
34
34
|
|
|
35
|
-
test('should
|
|
35
|
+
test('should allow absolute path with ".."', async () => {
|
|
36
36
|
const step = {
|
|
37
37
|
id: 'test',
|
|
38
38
|
type: 'shell' as const,
|
|
39
39
|
run: '/bin/ls ../',
|
|
40
40
|
};
|
|
41
|
-
|
|
41
|
+
// Absolute paths are allowed in run command
|
|
42
|
+
const result = await executeShell(step, mockContext);
|
|
43
|
+
expect(result.exitCode).toBe(0); // ls should succeed
|
|
42
44
|
});
|
|
43
45
|
});
|
|
44
46
|
});
|
|
@@ -78,12 +78,11 @@ describe('MCPClient Audit Fixes', () => {
|
|
|
78
78
|
});
|
|
79
79
|
|
|
80
80
|
describe('MCPClient SSRF Protection', () => {
|
|
81
|
-
it('should reject localhost URLs
|
|
82
|
-
//
|
|
81
|
+
it('should reject localhost URLs', async () => {
|
|
82
|
+
// Localhost is rejected regardless of protocol
|
|
83
83
|
await expect(MCPClient.createRemote('http://localhost:8080/sse')).rejects.toThrow(
|
|
84
|
-
/SSRF Protection.*
|
|
84
|
+
/SSRF Protection.*localhost/
|
|
85
85
|
);
|
|
86
|
-
// HTTPS localhost is rejected for being localhost
|
|
87
86
|
await expect(MCPClient.createRemote('https://localhost:8080/sse')).rejects.toThrow(
|
|
88
87
|
/SSRF Protection.*localhost/
|
|
89
88
|
);
|
|
@@ -127,19 +126,13 @@ describe('MCPClient SSRF Protection', () => {
|
|
|
127
126
|
);
|
|
128
127
|
});
|
|
129
128
|
|
|
130
|
-
it('should
|
|
131
|
-
|
|
132
|
-
/SSRF Protection.*HTTPS/
|
|
133
|
-
);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it('should allow HTTP with allowInsecure option', async () => {
|
|
137
|
-
// This will fail due to network issues, not SSRF
|
|
129
|
+
it('should allow valid external domains', async () => {
|
|
130
|
+
// Valid external domains should pass SSRF validation (but may fail on actual connection)
|
|
138
131
|
const promise = MCPClient.createRemote(
|
|
139
|
-
'
|
|
132
|
+
'https://api.example.com/sse',
|
|
140
133
|
{},
|
|
141
134
|
100, // short timeout
|
|
142
|
-
{
|
|
135
|
+
{}
|
|
143
136
|
);
|
|
144
137
|
// Should NOT throw SSRF error, but will throw timeout/connection error
|
|
145
138
|
await expect(promise).rejects.not.toThrow(/SSRF Protection/);
|
|
@@ -133,14 +133,13 @@ describe('MCPClient', () => {
|
|
|
133
133
|
});
|
|
134
134
|
|
|
135
135
|
const fetchMock = spyOn(global, 'fetch').mockImplementation((url) => {
|
|
136
|
-
if (url === '
|
|
136
|
+
if (url === 'https://api.example.com/sse') {
|
|
137
137
|
return Promise.resolve(new Response(stream));
|
|
138
138
|
}
|
|
139
139
|
return Promise.resolve(new Response(JSON.stringify({ ok: true })));
|
|
140
140
|
});
|
|
141
141
|
|
|
142
|
-
const clientPromise = MCPClient.createRemote('
|
|
143
|
-
});
|
|
142
|
+
const clientPromise = MCPClient.createRemote('https://api.example.com/sse', {}, 60000, {});
|
|
144
143
|
|
|
145
144
|
const client = await clientPromise;
|
|
146
145
|
expect(client).toBeDefined();
|
|
@@ -162,7 +161,10 @@ describe('MCPClient', () => {
|
|
|
162
161
|
|
|
163
162
|
const response = await initPromise;
|
|
164
163
|
expect(response.result?.protocolVersion).toBe('2024-11-05');
|
|
165
|
-
expect(fetchMock).toHaveBeenCalledWith(
|
|
164
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
165
|
+
'https://api.example.com/endpoint',
|
|
166
|
+
expect.any(Object)
|
|
167
|
+
);
|
|
166
168
|
|
|
167
169
|
client.stop();
|
|
168
170
|
fetchMock.mockRestore();
|
|
@@ -180,14 +182,13 @@ describe('MCPClient', () => {
|
|
|
180
182
|
});
|
|
181
183
|
|
|
182
184
|
const fetchMock = spyOn(global, 'fetch').mockImplementation((url) => {
|
|
183
|
-
if (url === '
|
|
185
|
+
if (url === 'https://api.example.com/sse') {
|
|
184
186
|
return Promise.resolve(new Response(stream));
|
|
185
187
|
}
|
|
186
188
|
return Promise.resolve(new Response(JSON.stringify({ ok: true })));
|
|
187
189
|
});
|
|
188
190
|
|
|
189
|
-
const client = await MCPClient.createRemote('
|
|
190
|
-
});
|
|
191
|
+
const client = await MCPClient.createRemote('https://api.example.com/sse', {}, 60000, {});
|
|
191
192
|
|
|
192
193
|
// We can't easily hook into onMessage without reaching into internals
|
|
193
194
|
// Instead, we'll test that initialize resolves correctly when the response arrives
|
|
@@ -230,8 +231,7 @@ describe('MCPClient', () => {
|
|
|
230
231
|
)
|
|
231
232
|
);
|
|
232
233
|
|
|
233
|
-
const clientPromise = MCPClient.createRemote('
|
|
234
|
-
});
|
|
234
|
+
const clientPromise = MCPClient.createRemote('https://api.example.com/sse', {}, 60000, {});
|
|
235
235
|
|
|
236
236
|
await expect(clientPromise).rejects.toThrow(/SSE connection failed: 500/);
|
|
237
237
|
|
|
@@ -111,18 +111,20 @@ describe('shell-executor', () => {
|
|
|
111
111
|
expect(result.exitCode).toBe(1);
|
|
112
112
|
});
|
|
113
113
|
|
|
114
|
-
it('should
|
|
114
|
+
it('should allow shell commands with semicolons', async () => {
|
|
115
115
|
const step: ShellStep = {
|
|
116
116
|
id: 'test',
|
|
117
117
|
type: 'shell',
|
|
118
118
|
needs: [],
|
|
119
|
-
run: 'echo "hello" ;
|
|
119
|
+
run: 'echo "hello" ; echo "world"',
|
|
120
120
|
};
|
|
121
121
|
|
|
122
|
-
|
|
122
|
+
// Semicolons are allowed (denylist check is for dangerous commands, not syntax)
|
|
123
|
+
const result = await executeShell(step, context);
|
|
124
|
+
expect(result.exitCode).toBe(0);
|
|
123
125
|
});
|
|
124
126
|
|
|
125
|
-
it('should allow shell variable expansion like ${HOME}
|
|
127
|
+
it('should allow shell variable expansion like ${HOME}', async () => {
|
|
126
128
|
const step: ShellStep = {
|
|
127
129
|
id: 'test',
|
|
128
130
|
type: 'shell',
|
|
@@ -136,7 +138,7 @@ describe('shell-executor', () => {
|
|
|
136
138
|
expect(result.stdout.trim()).toBe(Bun.env.HOME || '');
|
|
137
139
|
});
|
|
138
140
|
|
|
139
|
-
it('should
|
|
141
|
+
it('should allow parameter expansion like ${IFS}', async () => {
|
|
140
142
|
const step: ShellStep = {
|
|
141
143
|
id: 'test',
|
|
142
144
|
type: 'shell',
|
|
@@ -144,11 +146,13 @@ describe('shell-executor', () => {
|
|
|
144
146
|
run: 'echo ${IFS}',
|
|
145
147
|
};
|
|
146
148
|
|
|
147
|
-
|
|
149
|
+
// ${IFS} is allowed (no active injection detection beyond denylist)
|
|
150
|
+
const result = await executeShell(step, context);
|
|
151
|
+
expect(result.exitCode).toBe(0);
|
|
148
152
|
});
|
|
149
153
|
|
|
150
|
-
it('should allow braces and quotes for JSON usage
|
|
151
|
-
// {} and quotes
|
|
154
|
+
it('should allow braces and quotes for JSON usage', async () => {
|
|
155
|
+
// {} and quotes for JSON handling
|
|
152
156
|
const step: ShellStep = {
|
|
153
157
|
id: 'test',
|
|
154
158
|
type: 'shell',
|
|
@@ -161,7 +165,7 @@ describe('shell-executor', () => {
|
|
|
161
165
|
expect(result.stdout.trim()).toBe('{"values": [1, 2, 3]}');
|
|
162
166
|
});
|
|
163
167
|
|
|
164
|
-
it('should allow flow control with semicolons
|
|
168
|
+
it('should allow flow control with semicolons', async () => {
|
|
165
169
|
const step: ShellStep = {
|
|
166
170
|
id: 'test',
|
|
167
171
|
type: 'shell',
|
|
@@ -168,13 +168,13 @@ describe('Standard Tools Integration', () => {
|
|
|
168
168
|
expect(toolStep.type).toBe('file');
|
|
169
169
|
});
|
|
170
170
|
|
|
171
|
-
it('should
|
|
171
|
+
it('should allow standard tools within security boundaries', async () => {
|
|
172
172
|
const step: LlmStep = {
|
|
173
173
|
id: 'l1',
|
|
174
174
|
type: 'llm',
|
|
175
175
|
agent: 'test-agent',
|
|
176
176
|
needs: [],
|
|
177
|
-
prompt: 'run
|
|
177
|
+
prompt: 'run command',
|
|
178
178
|
useStandardTools: true,
|
|
179
179
|
maxIterations: 2,
|
|
180
180
|
};
|
|
@@ -182,7 +182,7 @@ describe('Standard Tools Integration', () => {
|
|
|
182
182
|
const context: ExpressionContext = { inputs: {}, steps: {} };
|
|
183
183
|
const executeStepFn = mock(async () => ({ status: 'success', output: '' }));
|
|
184
184
|
|
|
185
|
-
// Mock makes a tool call to run_command
|
|
185
|
+
// Mock makes a tool call to run_command
|
|
186
186
|
currentChatFn = async () => {
|
|
187
187
|
return {
|
|
188
188
|
message: {
|
|
@@ -199,22 +199,18 @@ describe('Standard Tools Integration', () => {
|
|
|
199
199
|
};
|
|
200
200
|
setCurrentChatFn(currentChatFn as any);
|
|
201
201
|
|
|
202
|
-
//
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
);
|
|
213
|
-
} catch (e) {
|
|
214
|
-
// Expected to hit max iterations
|
|
215
|
-
}
|
|
202
|
+
// Should complete successfully
|
|
203
|
+
await executeLlmStep(
|
|
204
|
+
step,
|
|
205
|
+
context,
|
|
206
|
+
executeStepFn as unknown as (step: Step, context: ExpressionContext) => Promise<StepResult>,
|
|
207
|
+
undefined,
|
|
208
|
+
undefined,
|
|
209
|
+
undefined,
|
|
210
|
+
undefined
|
|
211
|
+
);
|
|
216
212
|
|
|
217
|
-
//
|
|
218
|
-
expect(executeStepFn).
|
|
213
|
+
// executeStepFn should have been called for the command
|
|
214
|
+
expect(executeStepFn).toHaveBeenCalled();
|
|
219
215
|
});
|
|
220
216
|
});
|
|
@@ -421,7 +421,7 @@ describe('step-executor', () => {
|
|
|
421
421
|
}),
|
|
422
422
|
};
|
|
423
423
|
|
|
424
|
-
it('should
|
|
424
|
+
it('should execute script', async () => {
|
|
425
425
|
// @ts-ignore
|
|
426
426
|
const step = {
|
|
427
427
|
id: 's1',
|
|
@@ -431,11 +431,11 @@ describe('step-executor', () => {
|
|
|
431
431
|
const result = await executeStep(step, context, undefined, {
|
|
432
432
|
sandbox: mockSandbox as unknown as typeof SafeSandbox,
|
|
433
433
|
});
|
|
434
|
-
expect(result.status).toBe('
|
|
435
|
-
expect(result.
|
|
434
|
+
expect(result.status).toBe('success');
|
|
435
|
+
expect(result.output).toBe('script-result');
|
|
436
436
|
});
|
|
437
437
|
|
|
438
|
-
it('should execute script
|
|
438
|
+
it('should execute script when provided', async () => {
|
|
439
439
|
// @ts-ignore
|
|
440
440
|
const step = {
|
|
441
441
|
id: 's1',
|
|
@@ -883,12 +883,12 @@ describe('step-executor', () => {
|
|
|
883
883
|
expect(secondCall.headers.Authorization).toBeUndefined();
|
|
884
884
|
});
|
|
885
885
|
|
|
886
|
-
it('should
|
|
886
|
+
it('should block localhost requests', async () => {
|
|
887
887
|
// @ts-ignore
|
|
888
888
|
global.fetch.mockResolvedValue(new Response('ok'));
|
|
889
889
|
|
|
890
890
|
const step: RequestStep = {
|
|
891
|
-
id: 'req-
|
|
891
|
+
id: 'req-localhost',
|
|
892
892
|
type: 'request',
|
|
893
893
|
needs: [],
|
|
894
894
|
url: 'http://localhost/test',
|
|
@@ -896,7 +896,8 @@ describe('step-executor', () => {
|
|
|
896
896
|
};
|
|
897
897
|
|
|
898
898
|
const result = await executeStep(step, context);
|
|
899
|
-
expect(result.status).toBe('
|
|
899
|
+
expect(result.status).toBe('failed');
|
|
900
|
+
expect(result.error).toContain('SSRF Protection');
|
|
900
901
|
});
|
|
901
902
|
});
|
|
902
903
|
|