cognitive-modules-cli 2.2.0 → 2.2.1
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/cli.js +65 -12
- package/dist/commands/compose.d.ts +31 -0
- package/dist/commands/compose.js +148 -0
- package/dist/commands/index.d.ts +1 -0
- package/dist/commands/index.js +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +5 -1
- package/dist/modules/composition.d.ts +251 -0
- package/dist/modules/composition.js +1265 -0
- package/dist/modules/composition.test.d.ts +11 -0
- package/dist/modules/composition.test.js +450 -0
- package/dist/modules/index.d.ts +2 -0
- package/dist/modules/index.js +2 -0
- package/dist/modules/loader.d.ts +22 -2
- package/dist/modules/loader.js +167 -4
- package/dist/modules/policy.test.d.ts +10 -0
- package/dist/modules/policy.test.js +369 -0
- package/dist/modules/runner.d.ts +357 -1
- package/dist/modules/runner.js +1221 -64
- package/dist/modules/subagent.js +2 -0
- package/dist/modules/validator.d.ts +28 -0
- package/dist/modules/validator.js +629 -0
- package/dist/types.d.ts +92 -8
- package/package.json +2 -1
- package/src/cli.ts +73 -12
- package/src/commands/compose.ts +185 -0
- package/src/commands/index.ts +1 -0
- package/src/index.ts +35 -0
- package/src/modules/composition.test.ts +558 -0
- package/src/modules/composition.ts +1674 -0
- package/src/modules/index.ts +2 -0
- package/src/modules/loader.ts +196 -6
- package/src/modules/policy.test.ts +455 -0
- package/src/modules/runner.ts +1562 -74
- package/src/modules/subagent.ts +2 -0
- package/src/modules/validator.ts +700 -0
- package/src/types.ts +112 -8
- package/tsconfig.json +1 -1
package/dist/modules/loader.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Module Loader - Load and parse Cognitive Modules
|
|
3
|
-
* Supports
|
|
3
|
+
* Supports v0 (6-file), v1 (MODULE.md) and v2 (module.yaml + prompt.md) formats
|
|
4
4
|
*/
|
|
5
5
|
import * as fs from 'node:fs/promises';
|
|
6
6
|
import * as path from 'node:path';
|
|
@@ -11,12 +11,26 @@ const FRONTMATTER_REGEX = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n([\s\S]*))?/;
|
|
|
11
11
|
*/
|
|
12
12
|
async function detectFormat(modulePath) {
|
|
13
13
|
const v2Manifest = path.join(modulePath, 'module.yaml');
|
|
14
|
+
const v1Module = path.join(modulePath, 'MODULE.md');
|
|
15
|
+
const v0Module = path.join(modulePath, 'module.md');
|
|
14
16
|
try {
|
|
15
17
|
await fs.access(v2Manifest);
|
|
16
18
|
return 'v2';
|
|
17
19
|
}
|
|
18
20
|
catch {
|
|
19
|
-
|
|
21
|
+
try {
|
|
22
|
+
await fs.access(v1Module);
|
|
23
|
+
return 'v1';
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
try {
|
|
27
|
+
await fs.access(v0Module);
|
|
28
|
+
return 'v0';
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
throw new Error(`No module.yaml, MODULE.md, or module.md found in ${modulePath}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
20
34
|
}
|
|
21
35
|
}
|
|
22
36
|
/**
|
|
@@ -114,6 +128,54 @@ async function loadModuleV2(modulePath) {
|
|
|
114
128
|
required: metaRaw.required,
|
|
115
129
|
risk_rule: validatedRiskRule,
|
|
116
130
|
};
|
|
131
|
+
// Parse composition config (v2.2)
|
|
132
|
+
const compositionRaw = manifest.composition;
|
|
133
|
+
let composition;
|
|
134
|
+
if (compositionRaw) {
|
|
135
|
+
// Parse pattern
|
|
136
|
+
const pattern = compositionRaw.pattern ?? 'sequential';
|
|
137
|
+
// Parse requires (dependencies)
|
|
138
|
+
const requiresRaw = compositionRaw.requires;
|
|
139
|
+
const requires = requiresRaw?.map(dep => ({
|
|
140
|
+
name: dep.name,
|
|
141
|
+
version: dep.version,
|
|
142
|
+
optional: dep.optional,
|
|
143
|
+
fallback: dep.fallback,
|
|
144
|
+
timeout_ms: dep.timeout_ms
|
|
145
|
+
}));
|
|
146
|
+
// Parse dataflow
|
|
147
|
+
const dataflowRaw = compositionRaw.dataflow;
|
|
148
|
+
const dataflow = dataflowRaw?.map(step => ({
|
|
149
|
+
from: step.from,
|
|
150
|
+
to: step.to,
|
|
151
|
+
mapping: step.mapping,
|
|
152
|
+
condition: step.condition,
|
|
153
|
+
aggregate: step.aggregate,
|
|
154
|
+
aggregator: step.aggregator
|
|
155
|
+
}));
|
|
156
|
+
// Parse routing rules
|
|
157
|
+
const routingRaw = compositionRaw.routing;
|
|
158
|
+
const routing = routingRaw?.map(rule => ({
|
|
159
|
+
condition: rule.condition,
|
|
160
|
+
next: rule.next
|
|
161
|
+
}));
|
|
162
|
+
// Parse iteration config
|
|
163
|
+
const iterationRaw = compositionRaw.iteration;
|
|
164
|
+
const iteration = iterationRaw ? {
|
|
165
|
+
max_iterations: iterationRaw.max_iterations,
|
|
166
|
+
continue_condition: iterationRaw.continue_condition,
|
|
167
|
+
stop_condition: iterationRaw.stop_condition
|
|
168
|
+
} : undefined;
|
|
169
|
+
composition = {
|
|
170
|
+
pattern,
|
|
171
|
+
requires,
|
|
172
|
+
dataflow,
|
|
173
|
+
routing,
|
|
174
|
+
max_depth: compositionRaw.max_depth,
|
|
175
|
+
timeout_ms: compositionRaw.timeout_ms,
|
|
176
|
+
iteration
|
|
177
|
+
};
|
|
178
|
+
}
|
|
117
179
|
return {
|
|
118
180
|
name: manifest.name || path.basename(modulePath),
|
|
119
181
|
version: manifest.version || '1.0.0',
|
|
@@ -132,6 +194,7 @@ async function loadModuleV2(modulePath) {
|
|
|
132
194
|
enums,
|
|
133
195
|
compat,
|
|
134
196
|
metaConfig,
|
|
197
|
+
composition,
|
|
135
198
|
// Context and prompt
|
|
136
199
|
context: manifest.context,
|
|
137
200
|
prompt,
|
|
@@ -196,6 +259,63 @@ async function loadModuleV1(modulePath) {
|
|
|
196
259
|
format: 'v1',
|
|
197
260
|
};
|
|
198
261
|
}
|
|
262
|
+
/**
|
|
263
|
+
* Load v0 format module (6-file format - deprecated)
|
|
264
|
+
*/
|
|
265
|
+
async function loadModuleV0(modulePath) {
|
|
266
|
+
// Read module.md
|
|
267
|
+
const moduleFile = path.join(modulePath, 'module.md');
|
|
268
|
+
const moduleContent = await fs.readFile(moduleFile, 'utf-8');
|
|
269
|
+
// Parse frontmatter
|
|
270
|
+
const match = moduleContent.match(FRONTMATTER_REGEX);
|
|
271
|
+
if (!match) {
|
|
272
|
+
throw new Error(`Invalid module.md: missing YAML frontmatter in ${moduleFile}`);
|
|
273
|
+
}
|
|
274
|
+
const metadata = yaml.load(match[1]);
|
|
275
|
+
// Read schemas
|
|
276
|
+
const inputSchemaFile = path.join(modulePath, 'input.schema.json');
|
|
277
|
+
const outputSchemaFile = path.join(modulePath, 'output.schema.json');
|
|
278
|
+
const constraintsFile = path.join(modulePath, 'constraints.yaml');
|
|
279
|
+
const promptFile = path.join(modulePath, 'prompt.txt');
|
|
280
|
+
const inputSchemaContent = await fs.readFile(inputSchemaFile, 'utf-8');
|
|
281
|
+
const inputSchema = JSON.parse(inputSchemaContent);
|
|
282
|
+
const outputSchemaContent = await fs.readFile(outputSchemaFile, 'utf-8');
|
|
283
|
+
const outputSchema = JSON.parse(outputSchemaContent);
|
|
284
|
+
// Load constraints
|
|
285
|
+
const constraintsContent = await fs.readFile(constraintsFile, 'utf-8');
|
|
286
|
+
const constraintsRaw = yaml.load(constraintsContent);
|
|
287
|
+
// Load prompt
|
|
288
|
+
const prompt = await fs.readFile(promptFile, 'utf-8');
|
|
289
|
+
// Extract constraints
|
|
290
|
+
const constraints = {};
|
|
291
|
+
if (constraintsRaw) {
|
|
292
|
+
const operational = constraintsRaw.operational ?? {};
|
|
293
|
+
constraints.no_network = operational.no_external_network;
|
|
294
|
+
constraints.no_side_effects = operational.no_side_effects;
|
|
295
|
+
constraints.no_file_write = operational.no_file_write;
|
|
296
|
+
constraints.no_inventing_data = operational.no_inventing_data;
|
|
297
|
+
}
|
|
298
|
+
return {
|
|
299
|
+
name: metadata.name || path.basename(modulePath),
|
|
300
|
+
version: metadata.version || '1.0.0',
|
|
301
|
+
responsibility: metadata.responsibility || '',
|
|
302
|
+
excludes: metadata.excludes || [],
|
|
303
|
+
constraints: Object.keys(constraints).length > 0 ? constraints : undefined,
|
|
304
|
+
prompt,
|
|
305
|
+
inputSchema,
|
|
306
|
+
outputSchema,
|
|
307
|
+
dataSchema: outputSchema, // Alias for v2.2 compat
|
|
308
|
+
// v2.2 defaults for v0 modules
|
|
309
|
+
schemaStrictness: 'medium',
|
|
310
|
+
overflow: { enabled: false },
|
|
311
|
+
enums: { strategy: 'strict' },
|
|
312
|
+
compat: { accepts_v21_payload: true, runtime_auto_wrap: true },
|
|
313
|
+
// Metadata
|
|
314
|
+
location: modulePath,
|
|
315
|
+
format: 'v0',
|
|
316
|
+
formatVersion: 'v0.0',
|
|
317
|
+
};
|
|
318
|
+
}
|
|
199
319
|
/**
|
|
200
320
|
* Load a Cognitive Module (auto-detects format)
|
|
201
321
|
*/
|
|
@@ -204,9 +324,12 @@ export async function loadModule(modulePath) {
|
|
|
204
324
|
if (format === 'v2') {
|
|
205
325
|
return loadModuleV2(modulePath);
|
|
206
326
|
}
|
|
207
|
-
else {
|
|
327
|
+
else if (format === 'v1') {
|
|
208
328
|
return loadModuleV1(modulePath);
|
|
209
329
|
}
|
|
330
|
+
else {
|
|
331
|
+
return loadModuleV0(modulePath);
|
|
332
|
+
}
|
|
210
333
|
}
|
|
211
334
|
/**
|
|
212
335
|
* Check if a directory contains a valid module
|
|
@@ -214,6 +337,7 @@ export async function loadModule(modulePath) {
|
|
|
214
337
|
async function isValidModule(modulePath) {
|
|
215
338
|
const v2Manifest = path.join(modulePath, 'module.yaml');
|
|
216
339
|
const v1Module = path.join(modulePath, 'MODULE.md');
|
|
340
|
+
const v0Module = path.join(modulePath, 'module.md');
|
|
217
341
|
try {
|
|
218
342
|
await fs.access(v2Manifest);
|
|
219
343
|
return true;
|
|
@@ -224,7 +348,13 @@ async function isValidModule(modulePath) {
|
|
|
224
348
|
return true;
|
|
225
349
|
}
|
|
226
350
|
catch {
|
|
227
|
-
|
|
351
|
+
try {
|
|
352
|
+
await fs.access(v0Module);
|
|
353
|
+
return true;
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
228
358
|
}
|
|
229
359
|
}
|
|
230
360
|
}
|
|
@@ -271,3 +401,36 @@ export function getDefaultSearchPaths(cwd) {
|
|
|
271
401
|
path.join(home, '.cognitive', 'modules'),
|
|
272
402
|
];
|
|
273
403
|
}
|
|
404
|
+
// =============================================================================
|
|
405
|
+
// Utility Functions
|
|
406
|
+
// =============================================================================
|
|
407
|
+
/**
|
|
408
|
+
* Get module tier (exec, decision, exploration).
|
|
409
|
+
*/
|
|
410
|
+
export function getModuleTier(module) {
|
|
411
|
+
return module.tier;
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Get schema strictness level.
|
|
415
|
+
*/
|
|
416
|
+
export function getSchemaStrictness(module) {
|
|
417
|
+
return module.schemaStrictness ?? 'medium';
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Check if overflow (extensions.insights) is enabled.
|
|
421
|
+
*/
|
|
422
|
+
export function isOverflowEnabled(module) {
|
|
423
|
+
return module.overflow?.enabled ?? false;
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Get enum extension strategy.
|
|
427
|
+
*/
|
|
428
|
+
export function getEnumStrategy(module) {
|
|
429
|
+
return module.enums?.strategy ?? 'strict';
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Check if runtime should auto-wrap v2.1 to v2.2.
|
|
433
|
+
*/
|
|
434
|
+
export function shouldAutoWrap(module) {
|
|
435
|
+
return module.compat?.runtime_auto_wrap ?? true;
|
|
436
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Policy Enforcement
|
|
3
|
+
*
|
|
4
|
+
* Tests all policy enforcement functionality:
|
|
5
|
+
* - Tool policy checking (allowed/denied lists)
|
|
6
|
+
* - General policy checking (network, filesystem, etc.)
|
|
7
|
+
* - Tool call interception
|
|
8
|
+
* - Policy-aware executors
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Policy Enforcement
|
|
3
|
+
*
|
|
4
|
+
* Tests all policy enforcement functionality:
|
|
5
|
+
* - Tool policy checking (allowed/denied lists)
|
|
6
|
+
* - General policy checking (network, filesystem, etc.)
|
|
7
|
+
* - Tool call interception
|
|
8
|
+
* - Policy-aware executors
|
|
9
|
+
*/
|
|
10
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
11
|
+
import { checkToolPolicy, checkPolicy, checkToolAllowed, validateToolsAllowed, getDeniedActions, getDeniedTools, getAllowedTools, ToolCallInterceptor, createPolicyAwareExecutor, } from './runner.js';
|
|
12
|
+
// =============================================================================
|
|
13
|
+
// Test Fixtures
|
|
14
|
+
// =============================================================================
|
|
15
|
+
function createMockModule(overrides = {}) {
|
|
16
|
+
return {
|
|
17
|
+
name: 'test-module',
|
|
18
|
+
version: '1.0.0',
|
|
19
|
+
responsibility: 'Test module',
|
|
20
|
+
excludes: [],
|
|
21
|
+
prompt: 'Test prompt',
|
|
22
|
+
location: '/test',
|
|
23
|
+
format: 'v2',
|
|
24
|
+
...overrides,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
// =============================================================================
|
|
28
|
+
// checkToolPolicy Tests
|
|
29
|
+
// =============================================================================
|
|
30
|
+
describe('checkToolPolicy', () => {
|
|
31
|
+
it('should allow all tools when no policy defined', () => {
|
|
32
|
+
const module = createMockModule();
|
|
33
|
+
expect(checkToolPolicy('write_file', module).allowed).toBe(true);
|
|
34
|
+
expect(checkToolPolicy('shell', module).allowed).toBe(true);
|
|
35
|
+
expect(checkToolPolicy('any_tool', module).allowed).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
it('should deny tools in denied list', () => {
|
|
38
|
+
const module = createMockModule({
|
|
39
|
+
tools: {
|
|
40
|
+
policy: 'allow_by_default',
|
|
41
|
+
allowed: [],
|
|
42
|
+
denied: ['write_file', 'shell', 'network'],
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
const result = checkToolPolicy('write_file', module);
|
|
46
|
+
expect(result.allowed).toBe(false);
|
|
47
|
+
expect(result.reason).toContain('explicitly denied');
|
|
48
|
+
expect(result.policy).toBe('tools.denied');
|
|
49
|
+
});
|
|
50
|
+
it('should handle case-insensitive tool names', () => {
|
|
51
|
+
const module = createMockModule({
|
|
52
|
+
tools: {
|
|
53
|
+
policy: 'allow_by_default',
|
|
54
|
+
allowed: [],
|
|
55
|
+
denied: ['Write_File'],
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
expect(checkToolPolicy('write_file', module).allowed).toBe(false);
|
|
59
|
+
expect(checkToolPolicy('WRITE_FILE', module).allowed).toBe(false);
|
|
60
|
+
expect(checkToolPolicy('write-file', module).allowed).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
it('should enforce deny_by_default policy', () => {
|
|
63
|
+
const module = createMockModule({
|
|
64
|
+
tools: {
|
|
65
|
+
policy: 'deny_by_default',
|
|
66
|
+
allowed: ['read_file', 'list_dir'],
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
expect(checkToolPolicy('read_file', module).allowed).toBe(true);
|
|
70
|
+
expect(checkToolPolicy('list_dir', module).allowed).toBe(true);
|
|
71
|
+
const result = checkToolPolicy('write_file', module);
|
|
72
|
+
expect(result.allowed).toBe(false);
|
|
73
|
+
expect(result.reason).toContain('not in allowed list');
|
|
74
|
+
expect(result.policy).toBe('tools.policy');
|
|
75
|
+
});
|
|
76
|
+
it('should allow tools in allow_by_default mode (not in denied)', () => {
|
|
77
|
+
const module = createMockModule({
|
|
78
|
+
tools: {
|
|
79
|
+
policy: 'allow_by_default',
|
|
80
|
+
allowed: [],
|
|
81
|
+
denied: ['shell'],
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
expect(checkToolPolicy('read_file', module).allowed).toBe(true);
|
|
85
|
+
expect(checkToolPolicy('write_file', module).allowed).toBe(true);
|
|
86
|
+
expect(checkToolPolicy('shell', module).allowed).toBe(false);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
// =============================================================================
|
|
90
|
+
// checkPolicy Tests
|
|
91
|
+
// =============================================================================
|
|
92
|
+
describe('checkPolicy', () => {
|
|
93
|
+
it('should allow all actions when no policies defined', () => {
|
|
94
|
+
const module = createMockModule();
|
|
95
|
+
expect(checkPolicy('network', module).allowed).toBe(true);
|
|
96
|
+
expect(checkPolicy('filesystem_write', module).allowed).toBe(true);
|
|
97
|
+
expect(checkPolicy('side_effects', module).allowed).toBe(true);
|
|
98
|
+
expect(checkPolicy('code_execution', module).allowed).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
it('should deny actions marked as deny', () => {
|
|
101
|
+
const module = createMockModule({
|
|
102
|
+
policies: {
|
|
103
|
+
network: 'deny',
|
|
104
|
+
filesystem_write: 'deny',
|
|
105
|
+
side_effects: 'allow',
|
|
106
|
+
code_execution: 'deny',
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
const networkResult = checkPolicy('network', module);
|
|
110
|
+
expect(networkResult.allowed).toBe(false);
|
|
111
|
+
expect(networkResult.reason).toContain("'network' is denied");
|
|
112
|
+
expect(networkResult.policy).toBe('policies.network');
|
|
113
|
+
expect(checkPolicy('filesystem_write', module).allowed).toBe(false);
|
|
114
|
+
expect(checkPolicy('side_effects', module).allowed).toBe(true);
|
|
115
|
+
expect(checkPolicy('code_execution', module).allowed).toBe(false);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
// =============================================================================
|
|
119
|
+
// checkToolAllowed Tests (Combined Check)
|
|
120
|
+
// =============================================================================
|
|
121
|
+
describe('checkToolAllowed', () => {
|
|
122
|
+
it('should check both tool policy and general policies', () => {
|
|
123
|
+
const module = createMockModule({
|
|
124
|
+
policies: {
|
|
125
|
+
filesystem_write: 'deny',
|
|
126
|
+
side_effects: 'deny',
|
|
127
|
+
},
|
|
128
|
+
tools: {
|
|
129
|
+
policy: 'allow_by_default',
|
|
130
|
+
allowed: [],
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
// write_file maps to filesystem_write and side_effects
|
|
134
|
+
const result = checkToolAllowed('write_file', module);
|
|
135
|
+
expect(result.allowed).toBe(false);
|
|
136
|
+
expect(result.reason).toContain('filesystem_write');
|
|
137
|
+
});
|
|
138
|
+
it('should block tools that require denied actions', () => {
|
|
139
|
+
const module = createMockModule({
|
|
140
|
+
policies: {
|
|
141
|
+
network: 'deny',
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
// Network tools should be blocked
|
|
145
|
+
expect(checkToolAllowed('fetch', module).allowed).toBe(false);
|
|
146
|
+
expect(checkToolAllowed('http', module).allowed).toBe(false);
|
|
147
|
+
expect(checkToolAllowed('curl', module).allowed).toBe(false);
|
|
148
|
+
// Non-network tools should be allowed
|
|
149
|
+
expect(checkToolAllowed('read_file', module).allowed).toBe(true);
|
|
150
|
+
});
|
|
151
|
+
it('should block shell/exec when code_execution denied', () => {
|
|
152
|
+
const module = createMockModule({
|
|
153
|
+
policies: {
|
|
154
|
+
code_execution: 'deny',
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
expect(checkToolAllowed('shell', module).allowed).toBe(false);
|
|
158
|
+
expect(checkToolAllowed('exec', module).allowed).toBe(false);
|
|
159
|
+
expect(checkToolAllowed('code_interpreter', module).allowed).toBe(false);
|
|
160
|
+
});
|
|
161
|
+
it('should check explicit tools policy first', () => {
|
|
162
|
+
const module = createMockModule({
|
|
163
|
+
policies: {
|
|
164
|
+
network: 'allow', // Allow network in general
|
|
165
|
+
},
|
|
166
|
+
tools: {
|
|
167
|
+
policy: 'allow_by_default',
|
|
168
|
+
allowed: [],
|
|
169
|
+
denied: ['fetch'], // But explicitly deny fetch
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
const result = checkToolAllowed('fetch', module);
|
|
173
|
+
expect(result.allowed).toBe(false);
|
|
174
|
+
expect(result.policy).toBe('tools.denied');
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
// =============================================================================
|
|
178
|
+
// validateToolsAllowed Tests
|
|
179
|
+
// =============================================================================
|
|
180
|
+
describe('validateToolsAllowed', () => {
|
|
181
|
+
it('should return empty array when all tools allowed', () => {
|
|
182
|
+
const module = createMockModule();
|
|
183
|
+
const violations = validateToolsAllowed(['read_file', 'write_file', 'shell'], module);
|
|
184
|
+
expect(violations).toHaveLength(0);
|
|
185
|
+
});
|
|
186
|
+
it('should return all violations', () => {
|
|
187
|
+
const module = createMockModule({
|
|
188
|
+
policies: {
|
|
189
|
+
network: 'deny',
|
|
190
|
+
code_execution: 'deny',
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
const violations = validateToolsAllowed(['fetch', 'shell', 'read_file'], module);
|
|
194
|
+
expect(violations).toHaveLength(2);
|
|
195
|
+
expect(violations.some(v => v.reason?.includes('fetch'))).toBe(true);
|
|
196
|
+
expect(violations.some(v => v.reason?.includes('shell'))).toBe(true);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
// =============================================================================
|
|
200
|
+
// Helper Functions Tests
|
|
201
|
+
// =============================================================================
|
|
202
|
+
describe('getDeniedActions', () => {
|
|
203
|
+
it('should return list of denied actions', () => {
|
|
204
|
+
const module = createMockModule({
|
|
205
|
+
policies: {
|
|
206
|
+
network: 'deny',
|
|
207
|
+
filesystem_write: 'deny',
|
|
208
|
+
side_effects: 'allow',
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
const denied = getDeniedActions(module);
|
|
212
|
+
expect(denied).toContain('network');
|
|
213
|
+
expect(denied).toContain('filesystem_write');
|
|
214
|
+
expect(denied).not.toContain('side_effects');
|
|
215
|
+
});
|
|
216
|
+
it('should return empty array when no policies', () => {
|
|
217
|
+
const module = createMockModule();
|
|
218
|
+
expect(getDeniedActions(module)).toHaveLength(0);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
describe('getDeniedTools', () => {
|
|
222
|
+
it('should return denied tools list', () => {
|
|
223
|
+
const module = createMockModule({
|
|
224
|
+
tools: {
|
|
225
|
+
policy: 'allow_by_default',
|
|
226
|
+
allowed: [],
|
|
227
|
+
denied: ['shell', 'network', 'write_file'],
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
const denied = getDeniedTools(module);
|
|
231
|
+
expect(denied).toContain('shell');
|
|
232
|
+
expect(denied).toContain('network');
|
|
233
|
+
expect(denied).toContain('write_file');
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
describe('getAllowedTools', () => {
|
|
237
|
+
it('should return null for allow_by_default', () => {
|
|
238
|
+
const module = createMockModule({
|
|
239
|
+
tools: {
|
|
240
|
+
policy: 'allow_by_default',
|
|
241
|
+
allowed: ['read_file'],
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
expect(getAllowedTools(module)).toBeNull();
|
|
245
|
+
});
|
|
246
|
+
it('should return allowed list for deny_by_default', () => {
|
|
247
|
+
const module = createMockModule({
|
|
248
|
+
tools: {
|
|
249
|
+
policy: 'deny_by_default',
|
|
250
|
+
allowed: ['read_file', 'list_dir'],
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
const allowed = getAllowedTools(module);
|
|
254
|
+
expect(allowed).toEqual(['read_file', 'list_dir']);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
// =============================================================================
|
|
258
|
+
// ToolCallInterceptor Tests
|
|
259
|
+
// =============================================================================
|
|
260
|
+
describe('ToolCallInterceptor', () => {
|
|
261
|
+
let module;
|
|
262
|
+
let interceptor;
|
|
263
|
+
beforeEach(() => {
|
|
264
|
+
module = createMockModule({
|
|
265
|
+
policies: {
|
|
266
|
+
network: 'deny',
|
|
267
|
+
filesystem_write: 'deny',
|
|
268
|
+
},
|
|
269
|
+
tools: {
|
|
270
|
+
policy: 'deny_by_default',
|
|
271
|
+
allowed: ['read_file', 'list_dir'],
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
interceptor = new ToolCallInterceptor(module);
|
|
275
|
+
});
|
|
276
|
+
it('should check if tool is allowed', () => {
|
|
277
|
+
expect(interceptor.checkAllowed('read_file').allowed).toBe(true);
|
|
278
|
+
expect(interceptor.checkAllowed('write_file').allowed).toBe(false);
|
|
279
|
+
expect(interceptor.checkAllowed('fetch').allowed).toBe(false);
|
|
280
|
+
});
|
|
281
|
+
it('should execute allowed tool', async () => {
|
|
282
|
+
const mockExecutor = vi.fn().mockResolvedValue('file content');
|
|
283
|
+
interceptor.registerTool('read_file', mockExecutor);
|
|
284
|
+
const result = await interceptor.execute({
|
|
285
|
+
name: 'read_file',
|
|
286
|
+
arguments: { path: '/test.txt' },
|
|
287
|
+
});
|
|
288
|
+
expect(result.success).toBe(true);
|
|
289
|
+
expect(result.result).toBe('file content');
|
|
290
|
+
expect(mockExecutor).toHaveBeenCalledWith({ path: '/test.txt' });
|
|
291
|
+
});
|
|
292
|
+
it('should block denied tool', async () => {
|
|
293
|
+
const mockExecutor = vi.fn().mockResolvedValue('done');
|
|
294
|
+
interceptor.registerTool('write_file', mockExecutor);
|
|
295
|
+
const result = await interceptor.execute({
|
|
296
|
+
name: 'write_file',
|
|
297
|
+
arguments: { path: '/test.txt', content: 'hello' },
|
|
298
|
+
});
|
|
299
|
+
expect(result.success).toBe(false);
|
|
300
|
+
expect(result.error?.code).toBe('TOOL_NOT_ALLOWED');
|
|
301
|
+
expect(mockExecutor).not.toHaveBeenCalled();
|
|
302
|
+
});
|
|
303
|
+
it('should log all calls', async () => {
|
|
304
|
+
interceptor.registerTool('read_file', vi.fn().mockResolvedValue('ok'));
|
|
305
|
+
interceptor.registerTool('write_file', vi.fn().mockResolvedValue('ok'));
|
|
306
|
+
await interceptor.execute({ name: 'read_file', arguments: {} });
|
|
307
|
+
await interceptor.execute({ name: 'write_file', arguments: {} });
|
|
308
|
+
await interceptor.execute({ name: 'read_file', arguments: {} });
|
|
309
|
+
const log = interceptor.getCallLog();
|
|
310
|
+
expect(log).toHaveLength(3);
|
|
311
|
+
expect(log[0].tool).toBe('read_file');
|
|
312
|
+
expect(log[0].allowed).toBe(true);
|
|
313
|
+
expect(log[1].tool).toBe('write_file');
|
|
314
|
+
expect(log[1].allowed).toBe(false);
|
|
315
|
+
});
|
|
316
|
+
it('should get denied calls', async () => {
|
|
317
|
+
interceptor.registerTool('read_file', vi.fn().mockResolvedValue('ok'));
|
|
318
|
+
await interceptor.execute({ name: 'read_file', arguments: {} });
|
|
319
|
+
await interceptor.execute({ name: 'write_file', arguments: {} });
|
|
320
|
+
await interceptor.execute({ name: 'shell', arguments: {} });
|
|
321
|
+
const denied = interceptor.getDeniedCalls();
|
|
322
|
+
expect(denied).toHaveLength(2);
|
|
323
|
+
expect(denied.some(d => d.tool === 'write_file')).toBe(true);
|
|
324
|
+
expect(denied.some(d => d.tool === 'shell')).toBe(true);
|
|
325
|
+
});
|
|
326
|
+
it('should execute many and stop on policy violation', async () => {
|
|
327
|
+
interceptor.registerTool('read_file', vi.fn().mockResolvedValue('ok'));
|
|
328
|
+
interceptor.registerTool('list_dir', vi.fn().mockResolvedValue(['a', 'b']));
|
|
329
|
+
const results = await interceptor.executeMany([
|
|
330
|
+
{ name: 'read_file', arguments: {} },
|
|
331
|
+
{ name: 'write_file', arguments: {} }, // Blocked
|
|
332
|
+
{ name: 'list_dir', arguments: {} }, // Should not execute
|
|
333
|
+
]);
|
|
334
|
+
expect(results).toHaveLength(2);
|
|
335
|
+
expect(results[0].success).toBe(true);
|
|
336
|
+
expect(results[1].success).toBe(false);
|
|
337
|
+
});
|
|
338
|
+
it('should provide policy summary', () => {
|
|
339
|
+
const summary = interceptor.getPolicySummary();
|
|
340
|
+
expect(summary.deniedActions).toContain('network');
|
|
341
|
+
expect(summary.deniedActions).toContain('filesystem_write');
|
|
342
|
+
expect(summary.allowedTools).toEqual(['read_file', 'list_dir']);
|
|
343
|
+
expect(summary.toolsPolicy).toBe('deny_by_default');
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
// =============================================================================
|
|
347
|
+
// createPolicyAwareExecutor Tests
|
|
348
|
+
// =============================================================================
|
|
349
|
+
describe('createPolicyAwareExecutor', () => {
|
|
350
|
+
it('should execute allowed tool', async () => {
|
|
351
|
+
const module = createMockModule();
|
|
352
|
+
const executor = vi.fn().mockResolvedValue('result');
|
|
353
|
+
const safeExecutor = createPolicyAwareExecutor(module, 'read_file', executor);
|
|
354
|
+
const result = await safeExecutor({ path: '/test.txt' });
|
|
355
|
+
expect(result).toBe('result');
|
|
356
|
+
expect(executor).toHaveBeenCalledWith({ path: '/test.txt' });
|
|
357
|
+
});
|
|
358
|
+
it('should throw on policy violation', async () => {
|
|
359
|
+
const module = createMockModule({
|
|
360
|
+
policies: {
|
|
361
|
+
filesystem_write: 'deny',
|
|
362
|
+
},
|
|
363
|
+
});
|
|
364
|
+
const executor = vi.fn().mockResolvedValue('result');
|
|
365
|
+
const safeExecutor = createPolicyAwareExecutor(module, 'write_file', executor);
|
|
366
|
+
await expect(safeExecutor({ path: '/test.txt' })).rejects.toThrow('Policy violation');
|
|
367
|
+
expect(executor).not.toHaveBeenCalled();
|
|
368
|
+
});
|
|
369
|
+
});
|