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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "keystone-cli",
3
- "version": "2.1.5",
3
+ "version": "2.1.6",
4
4
  "description": "A local-first, declarative, agentic workflow orchestrator built on Bun",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.ts CHANGED
@@ -962,7 +962,6 @@ program
962
962
  inputs: { task, auto_approve: options.auto_approve },
963
963
  workflowDir: dirname(devPath),
964
964
  logger,
965
- allowInsecure: true, // Trusted internal workflow
966
965
  });
967
966
 
968
967
  const outputs = await runner.run();
@@ -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 block command with ".." and "/" in secure mode', async () => {
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
- // It should throw BEFORE spawning
30
- // The error message I added was "Directory Traversal" or similar
31
- // Let's check the implementation: "Command blocked due to potential directory traversal"
32
- await expect(executeShell(step, mockContext)).rejects.toThrow('Command blocked');
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 block absolute path with ".." in secure mode', async () => {
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
- await expect(executeShell(step, mockContext)).rejects.toThrow('Command blocked');
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 without allowInsecure', async () => {
82
- // HTTP localhost is rejected for not using HTTPS
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.*HTTPS/
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 require HTTPS by default', async () => {
131
- await expect(MCPClient.createRemote('http://api.example.com/sse')).rejects.toThrow(
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
- 'http://api.example.com/sse',
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 === 'http://localhost:8080/sse') {
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('http://localhost:8080/sse', {}, 60000, {
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('http://localhost:8080/endpoint', expect.any(Object));
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 === 'http://localhost:8080/sse') {
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('http://localhost:8080/sse', {}, 60000, {
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('http://localhost:8080/sse', {}, 60000, {
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 throw error on shell injection risk', async () => {
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" ; rm -rf /tmp/foo',
119
+ run: 'echo "hello" ; echo "world"',
120
120
  };
121
121
 
122
- await expect(executeShell(step, context)).rejects.toThrow(/Security Error/);
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} when allowInsecure is true', async () => {
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 block parameter expansion like ${IFS} by default', async () => {
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
- await expect(executeShell(step, context)).rejects.toThrow(/Security Error/);
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 with allowInsecure', async () => {
151
- // {} and quotes now require allowInsecure due to strict whitelist
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 when allowInsecure is true', async () => {
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 block risky standard tools without allowInsecure', async () => {
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 risky command',
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 which should be rejected
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
- // May throw max iterations or complete
203
- try {
204
- await executeLlmStep(
205
- step,
206
- context,
207
- executeStepFn as unknown as (step: Step, context: ExpressionContext) => Promise<StepResult>,
208
- undefined,
209
- undefined,
210
- undefined,
211
- undefined
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
- // The key assertion: executeStepFn should NOT have been called for the risky command
218
- expect(executeStepFn).not.toHaveBeenCalled();
213
+ // executeStepFn should have been called for the command
214
+ expect(executeStepFn).toHaveBeenCalled();
219
215
  });
220
216
  });
@@ -140,8 +140,7 @@ export const STANDARD_TOOLS: AgentTool[] = [
140
140
  },
141
141
  required: ['pattern'],
142
142
  },
143
- execution: {
144
- },
143
+ execution: {},
145
144
  },
146
145
  {
147
146
  name: 'search_content',
@@ -421,7 +421,7 @@ describe('step-executor', () => {
421
421
  }),
422
422
  };
423
423
 
424
- it('should fail if allowInsecure is not set', async () => {
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('failed');
435
- expect(result.error).toContain('Script execution is disabled by default');
434
+ expect(result.status).toBe('success');
435
+ expect(result.output).toBe('script-result');
436
436
  });
437
437
 
438
- it('should execute script if allowInsecure is true', async () => {
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 allow insecure request when allowInsecure is true', async () => {
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-insecure',
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('success');
899
+ expect(result.status).toBe('failed');
900
+ expect(result.error).toContain('SSRF Protection');
900
901
  });
901
902
  });
902
903