@fulcrum-governance/sdk 0.1.1 → 0.1.3

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.
Files changed (40) hide show
  1. package/CHANGELOG.md +42 -1
  2. package/README.md +299 -6
  3. package/dist/__tests__/clients.test.d.ts +1 -0
  4. package/dist/__tests__/clients.test.js +847 -0
  5. package/dist/clients/agent.d.ts +70 -0
  6. package/dist/clients/agent.js +127 -0
  7. package/dist/clients/approval.d.ts +67 -0
  8. package/dist/clients/approval.js +103 -0
  9. package/dist/clients/budget.d.ts +221 -0
  10. package/dist/clients/budget.js +181 -0
  11. package/dist/clients/checkpoint.d.ts +191 -0
  12. package/dist/clients/checkpoint.js +195 -0
  13. package/dist/clients/envelope.d.ts +73 -0
  14. package/dist/clients/envelope.js +95 -0
  15. package/dist/clients/eventstore.d.ts +87 -0
  16. package/dist/clients/eventstore.js +113 -0
  17. package/dist/clients/index.d.ts +15 -0
  18. package/dist/clients/index.js +35 -0
  19. package/dist/clients/metrics.d.ts +126 -0
  20. package/dist/clients/metrics.js +162 -0
  21. package/dist/clients/policy.d.ts +83 -0
  22. package/dist/clients/policy.js +102 -0
  23. package/dist/clients/tenant.d.ts +66 -0
  24. package/dist/clients/tenant.js +92 -0
  25. package/dist/index.d.ts +1 -0
  26. package/dist/index.js +25 -3
  27. package/dist/instrumentation/__tests__/autoGovern.test.d.ts +6 -0
  28. package/dist/instrumentation/__tests__/autoGovern.test.js +416 -0
  29. package/dist/instrumentation/__tests__/evaluator.test.d.ts +6 -0
  30. package/dist/instrumentation/__tests__/evaluator.test.js +712 -0
  31. package/dist/instrumentation/autoGovern.d.ts +57 -0
  32. package/dist/instrumentation/autoGovern.js +319 -0
  33. package/dist/instrumentation/evaluator.d.ts +50 -0
  34. package/dist/instrumentation/evaluator.js +218 -0
  35. package/dist/instrumentation/index.d.ts +28 -0
  36. package/dist/instrumentation/index.js +34 -0
  37. package/dist/instrumentation/types.d.ts +105 -0
  38. package/dist/instrumentation/types.js +20 -0
  39. package/package.json +4 -4
  40. package/proto/fulcrum/agent/v1/agent_service.proto +170 -0
@@ -0,0 +1,416 @@
1
+ "use strict";
2
+ /**
3
+ * Comprehensive unit tests for autoGovern module.
4
+ * Tests cover: activate/deactivate lifecycle, patching, metrics, fail-safe behavior.
5
+ * Target: >90% coverage
6
+ */
7
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
8
+ if (k2 === undefined) k2 = k;
9
+ var desc = Object.getOwnPropertyDescriptor(m, k);
10
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
11
+ desc = { enumerable: true, get: function() { return m[k]; } };
12
+ }
13
+ Object.defineProperty(o, k2, desc);
14
+ }) : (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ o[k2] = m[k];
17
+ }));
18
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
19
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
20
+ }) : function(o, v) {
21
+ o["default"] = v;
22
+ });
23
+ var __importStar = (this && this.__importStar) || (function () {
24
+ var ownKeys = function(o) {
25
+ ownKeys = Object.getOwnPropertyNames || function (o) {
26
+ var ar = [];
27
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
28
+ return ar;
29
+ };
30
+ return ownKeys(o);
31
+ };
32
+ return function (mod) {
33
+ if (mod && mod.__esModule) return mod;
34
+ var result = {};
35
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
36
+ __setModuleDefault(result, mod);
37
+ return result;
38
+ };
39
+ })();
40
+ Object.defineProperty(exports, "__esModule", { value: true });
41
+ const autoGovern = __importStar(require("../autoGovern"));
42
+ // Mock console methods
43
+ const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
44
+ const consoleInfoSpy = jest.spyOn(console, 'info').mockImplementation();
45
+ const consoleDebugSpy = jest.spyOn(console, 'debug').mockImplementation();
46
+ describe('autoGovern', () => {
47
+ beforeEach(() => {
48
+ jest.clearAllMocks();
49
+ });
50
+ afterEach(() => {
51
+ // Ensure deactivation after each test
52
+ if (autoGovern.isActive()) {
53
+ autoGovern.deactivate();
54
+ }
55
+ });
56
+ afterAll(() => {
57
+ consoleWarnSpy.mockRestore();
58
+ consoleInfoSpy.mockRestore();
59
+ consoleDebugSpy.mockRestore();
60
+ });
61
+ describe('activate', () => {
62
+ it('should activate governance', () => {
63
+ autoGovern.activate({ apiKey: 'test-key', tenantId: 'test-tenant' });
64
+ expect(autoGovern.isActive()).toBe(true);
65
+ expect(autoGovern.getEvaluator()).not.toBeNull();
66
+ expect(autoGovern.getConfig()).not.toBeNull();
67
+ });
68
+ it('should start the evaluator', () => {
69
+ autoGovern.activate({ apiKey: 'test-key', tenantId: 'test-tenant' });
70
+ const evaluator = autoGovern.getEvaluator();
71
+ expect(evaluator).not.toBeNull();
72
+ expect(evaluator.running).toBe(true);
73
+ });
74
+ it('should use default config when not provided', () => {
75
+ autoGovern.activate();
76
+ const config = autoGovern.getConfig();
77
+ expect(config).not.toBeNull();
78
+ expect(config.serverUrl).toBe('https://api.fulcrumlayer.io');
79
+ });
80
+ it('should merge user config with defaults', () => {
81
+ autoGovern.activate({
82
+ apiKey: 'custom-key',
83
+ tenantId: 'custom-tenant',
84
+ policySyncInterval: 60,
85
+ });
86
+ const config = autoGovern.getConfig();
87
+ expect(config.apiKey).toBe('custom-key');
88
+ expect(config.tenantId).toBe('custom-tenant');
89
+ expect(config.policySyncInterval).toBe(60);
90
+ });
91
+ it('should warn when already active', () => {
92
+ autoGovern.activate({ apiKey: 'test-key', tenantId: 'test-tenant' });
93
+ autoGovern.activate({ apiKey: 'test-key', tenantId: 'test-tenant' });
94
+ expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('already active'));
95
+ });
96
+ it('should warn when api key not set', () => {
97
+ autoGovern.activate({ tenantId: 'test-tenant' });
98
+ expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('api_key not set'));
99
+ });
100
+ it('should warn when tenant id not set', () => {
101
+ autoGovern.activate({ apiKey: 'test-key' });
102
+ expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('tenant_id not set'));
103
+ });
104
+ it('should log activation message', () => {
105
+ autoGovern.activate({ apiKey: 'test-key', tenantId: 'test-tenant' });
106
+ expect(consoleInfoSpy).toHaveBeenCalledWith(expect.stringContaining('Governance activated'));
107
+ });
108
+ });
109
+ describe('deactivate', () => {
110
+ it('should deactivate governance', () => {
111
+ autoGovern.activate({ apiKey: 'test-key', tenantId: 'test-tenant' });
112
+ autoGovern.deactivate();
113
+ expect(autoGovern.isActive()).toBe(false);
114
+ expect(autoGovern.getEvaluator()).toBeNull();
115
+ expect(autoGovern.getConfig()).toBeNull();
116
+ });
117
+ it('should stop the evaluator', () => {
118
+ autoGovern.activate({ apiKey: 'test-key', tenantId: 'test-tenant' });
119
+ const evaluator = autoGovern.getEvaluator();
120
+ autoGovern.deactivate();
121
+ expect(evaluator.running).toBe(false);
122
+ });
123
+ it('should be idempotent when not active', () => {
124
+ // Should not throw
125
+ expect(() => autoGovern.deactivate()).not.toThrow();
126
+ });
127
+ it('should log deactivation message', () => {
128
+ autoGovern.activate({ apiKey: 'test-key', tenantId: 'test-tenant' });
129
+ autoGovern.deactivate();
130
+ expect(consoleInfoSpy).toHaveBeenCalledWith(expect.stringContaining('Governance deactivated'));
131
+ });
132
+ });
133
+ describe('isActive', () => {
134
+ it('should return false initially', () => {
135
+ expect(autoGovern.isActive()).toBe(false);
136
+ });
137
+ it('should return true after activation', () => {
138
+ autoGovern.activate({ apiKey: 'test-key', tenantId: 'test-tenant' });
139
+ expect(autoGovern.isActive()).toBe(true);
140
+ });
141
+ it('should return false after deactivation', () => {
142
+ autoGovern.activate({ apiKey: 'test-key', tenantId: 'test-tenant' });
143
+ autoGovern.deactivate();
144
+ expect(autoGovern.isActive()).toBe(false);
145
+ });
146
+ });
147
+ describe('getEvaluator', () => {
148
+ it('should return null when not active', () => {
149
+ expect(autoGovern.getEvaluator()).toBeNull();
150
+ });
151
+ it('should return evaluator when active', () => {
152
+ autoGovern.activate({ apiKey: 'test-key', tenantId: 'test-tenant' });
153
+ expect(autoGovern.getEvaluator()).not.toBeNull();
154
+ });
155
+ });
156
+ describe('getConfig', () => {
157
+ it('should return null when not active', () => {
158
+ expect(autoGovern.getConfig()).toBeNull();
159
+ });
160
+ it('should return config when active', () => {
161
+ autoGovern.activate({ apiKey: 'test-key', tenantId: 'test-tenant' });
162
+ expect(autoGovern.getConfig()).not.toBeNull();
163
+ });
164
+ });
165
+ describe('metrics', () => {
166
+ it('should initialize metrics counters', () => {
167
+ const metrics = autoGovern.getMetrics();
168
+ expect(metrics.governedCalls).toBe(0);
169
+ expect(metrics.ungovernedCalls).toBe(0);
170
+ expect(metrics.deniedCalls).toBe(0);
171
+ expect(metrics.allowedCalls).toBe(0);
172
+ expect(metrics.patchFailures).toBe(0);
173
+ });
174
+ it('should record metrics', () => {
175
+ const before = autoGovern.getMetrics();
176
+ autoGovern.recordMetric('governedCalls', 5);
177
+ autoGovern.recordMetric('allowedCalls', 3);
178
+ const after = autoGovern.getMetrics();
179
+ expect(after.governedCalls).toBe(before.governedCalls + 5);
180
+ expect(after.allowedCalls).toBe(before.allowedCalls + 3);
181
+ });
182
+ it('should increment metrics by 1 when no increment specified', () => {
183
+ const before = autoGovern.getMetrics();
184
+ autoGovern.recordMetric('governedCalls');
185
+ const after = autoGovern.getMetrics();
186
+ expect(after.governedCalls).toBe(before.governedCalls + 1);
187
+ });
188
+ it('should return copy of metrics', () => {
189
+ const metrics1 = autoGovern.getMetrics();
190
+ const metrics2 = autoGovern.getMetrics();
191
+ expect(metrics1).not.toBe(metrics2);
192
+ expect(metrics1).toEqual(metrics2);
193
+ });
194
+ });
195
+ describe('evaluateCall', () => {
196
+ it('should allow when evaluator not initialized', () => {
197
+ const decision = autoGovern.evaluateCall('test_tool', 'test_action');
198
+ expect(decision.action).toBe('allow');
199
+ expect(decision.reason).toContain('not initialized');
200
+ });
201
+ it('should record ungoverned call metric when evaluator not initialized', () => {
202
+ const before = autoGovern.getMetrics();
203
+ autoGovern.evaluateCall('test_tool', 'test_action');
204
+ const after = autoGovern.getMetrics();
205
+ expect(after.ungovernedCalls).toBe(before.ungovernedCalls + 1);
206
+ });
207
+ it('should evaluate with active evaluator', () => {
208
+ autoGovern.activate({ apiKey: 'test-key', tenantId: 'test-tenant' });
209
+ const decision = autoGovern.evaluateCall('test_tool', 'test_action');
210
+ // Should allow by default (no policies)
211
+ expect(decision.action).toBe('allow');
212
+ });
213
+ it('should record governed call metrics', () => {
214
+ autoGovern.activate({ apiKey: 'test-key', tenantId: 'test-tenant' });
215
+ const before = autoGovern.getMetrics();
216
+ autoGovern.evaluateCall('test_tool', 'test_action');
217
+ const after = autoGovern.getMetrics();
218
+ expect(after.governedCalls).toBe(before.governedCalls + 1);
219
+ });
220
+ it('should record allowed call metrics', () => {
221
+ autoGovern.activate({ apiKey: 'test-key', tenantId: 'test-tenant' });
222
+ const before = autoGovern.getMetrics();
223
+ autoGovern.evaluateCall('test_tool', 'test_action');
224
+ const after = autoGovern.getMetrics();
225
+ expect(after.allowedCalls).toBe(before.allowedCalls + 1);
226
+ });
227
+ it('should record denied call metrics', () => {
228
+ autoGovern.activate({ apiKey: 'test-key', tenantId: 'test-tenant' });
229
+ // Add deny policy
230
+ const evaluator = autoGovern.getEvaluator();
231
+ evaluator.updatePolicies([
232
+ {
233
+ policyId: 'policy-1',
234
+ tenantId: 'test-tenant',
235
+ priority: 100,
236
+ status: 'active',
237
+ scope: { applyToAll: true },
238
+ rules: [
239
+ {
240
+ ruleId: 'rule-1',
241
+ name: 'Deny all',
242
+ enabled: true,
243
+ priority: 100,
244
+ conditions: [],
245
+ actions: [{ actionType: 'deny' }],
246
+ },
247
+ ],
248
+ },
249
+ ]);
250
+ const before = autoGovern.getMetrics();
251
+ autoGovern.evaluateCall('test_tool', 'test_action');
252
+ const after = autoGovern.getMetrics();
253
+ expect(after.deniedCalls).toBe(before.deniedCalls + 1);
254
+ });
255
+ it('should pass all parameters to evaluator', () => {
256
+ autoGovern.activate({ apiKey: 'test-key', tenantId: 'test-tenant' });
257
+ const decision = autoGovern.evaluateCall('test_tool', 'test_action', '/test/resource', { arg1: 'value1' }, { meta1: 'metavalue1' });
258
+ expect(decision).toBeDefined();
259
+ });
260
+ });
261
+ describe('patching', () => {
262
+ // Note: Actual patching behavior is difficult to test in Jest
263
+ // without mocking Node.js internals. These tests verify the
264
+ // activation flow calls patching functions.
265
+ it('should apply patches on activation', () => {
266
+ const config = {
267
+ apiKey: 'test-key',
268
+ tenantId: 'test-tenant',
269
+ enabledPatches: new Set(['fetch']),
270
+ };
271
+ autoGovern.activate(config);
272
+ // Verify patches were attempted (via config)
273
+ const activeConfig = autoGovern.getConfig();
274
+ expect(activeConfig.enabledPatches.has('fetch')).toBe(true);
275
+ });
276
+ it('should restore originals on deactivation', () => {
277
+ autoGovern.activate({ apiKey: 'test-key', tenantId: 'test-tenant' });
278
+ autoGovern.deactivate();
279
+ // Should not throw - originals restored
280
+ expect(() => autoGovern.deactivate()).not.toThrow();
281
+ });
282
+ });
283
+ describe('fail-safe behavior', () => {
284
+ it('should not crash on patch failures', () => {
285
+ // This would require mocking fs or child_process to fail
286
+ // For now, verify activation doesn't throw
287
+ expect(() => {
288
+ autoGovern.activate({ apiKey: 'test-key', tenantId: 'test-tenant' });
289
+ }).not.toThrow();
290
+ });
291
+ it('should allow calls when evaluation fails', () => {
292
+ autoGovern.activate({ apiKey: 'test-key', tenantId: 'test-tenant' });
293
+ // Break the evaluator
294
+ const evaluator = autoGovern.getEvaluator();
295
+ evaluator.policies = ['invalid']; // Cause error
296
+ const decision = autoGovern.evaluateCall('test_tool', 'test_action');
297
+ // Should still allow (fail-open)
298
+ expect(decision.action).toBe('allow');
299
+ });
300
+ });
301
+ });
302
+ describe('autoGovern - fetch patching integration', () => {
303
+ let originalFetch;
304
+ beforeEach(() => {
305
+ // Store original fetch
306
+ originalFetch = globalThis.fetch;
307
+ consoleWarnSpy.mockClear();
308
+ consoleDebugSpy.mockClear();
309
+ });
310
+ afterEach(() => {
311
+ // Restore original fetch
312
+ globalThis.fetch = originalFetch;
313
+ if (autoGovern.isActive()) {
314
+ autoGovern.deactivate();
315
+ }
316
+ });
317
+ it('should patch global fetch when enabled', () => {
318
+ autoGovern.activate({
319
+ apiKey: 'test-key',
320
+ tenantId: 'test-tenant',
321
+ enabledPatches: new Set(['fetch']),
322
+ });
323
+ // fetch should be different from original
324
+ expect(globalThis.fetch).not.toBe(originalFetch);
325
+ });
326
+ it('should restore original fetch on deactivation', () => {
327
+ autoGovern.activate({
328
+ apiKey: 'test-key',
329
+ tenantId: 'test-tenant',
330
+ enabledPatches: new Set(['fetch']),
331
+ });
332
+ autoGovern.deactivate();
333
+ // fetch should be restored
334
+ expect(globalThis.fetch).toBe(originalFetch);
335
+ });
336
+ it('should evaluate fetch calls when patched', async () => {
337
+ // Mock fetch to avoid actual network calls
338
+ const mockFetch = jest.fn().mockResolvedValue({
339
+ ok: true,
340
+ status: 200,
341
+ json: async () => ({ data: 'test' }),
342
+ });
343
+ globalThis.fetch = mockFetch;
344
+ autoGovern.activate({
345
+ apiKey: 'test-key',
346
+ tenantId: 'test-tenant',
347
+ enabledPatches: new Set(['fetch']),
348
+ });
349
+ const beforeGoverned = autoGovern.getMetrics().governedCalls;
350
+ try {
351
+ await globalThis.fetch('https://example.com/api');
352
+ }
353
+ catch (e) {
354
+ // Expected - governance wraps fetch
355
+ }
356
+ const afterGoverned = autoGovern.getMetrics().governedCalls;
357
+ // Should have evaluated the call
358
+ expect(afterGoverned).toBeGreaterThan(beforeGoverned);
359
+ });
360
+ it('should block fetch when policy denies', async () => {
361
+ // Mock fetch
362
+ const mockFetch = jest.fn();
363
+ globalThis.fetch = mockFetch;
364
+ autoGovern.activate({
365
+ apiKey: 'test-key',
366
+ tenantId: 'test-tenant',
367
+ enabledPatches: new Set(['fetch']),
368
+ });
369
+ // Add deny policy
370
+ const evaluator = autoGovern.getEvaluator();
371
+ evaluator.updatePolicies([
372
+ {
373
+ policyId: 'policy-1',
374
+ tenantId: 'test-tenant',
375
+ priority: 100,
376
+ status: 'active',
377
+ scope: { applyToAll: true },
378
+ rules: [
379
+ {
380
+ ruleId: 'rule-1',
381
+ name: 'Deny all HTTP requests',
382
+ enabled: true,
383
+ priority: 100,
384
+ conditions: [
385
+ {
386
+ field: 'action_type',
387
+ operator: 'equals',
388
+ value: 'http_request',
389
+ },
390
+ ],
391
+ actions: [{ actionType: 'deny' }],
392
+ },
393
+ ],
394
+ },
395
+ ]);
396
+ await expect(globalThis.fetch('https://example.com/api')).rejects.toThrow(/blocked by Fulcrum policy/);
397
+ // Original fetch should not have been called
398
+ expect(mockFetch).not.toHaveBeenCalled();
399
+ });
400
+ it('should allow fetch when policy allows', async () => {
401
+ // Mock fetch
402
+ const mockFetch = jest.fn().mockResolvedValue({
403
+ ok: true,
404
+ status: 200,
405
+ });
406
+ globalThis.fetch = mockFetch;
407
+ autoGovern.activate({
408
+ apiKey: 'test-key',
409
+ tenantId: 'test-tenant',
410
+ enabledPatches: new Set(['fetch']),
411
+ });
412
+ await globalThis.fetch('https://example.com/api');
413
+ // Original fetch should have been called
414
+ expect(mockFetch).toHaveBeenCalledWith('https://example.com/api', undefined);
415
+ });
416
+ });
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Comprehensive unit tests for PolicyEvaluator.
3
+ * Tests cover: policy evaluation logic, condition matching, fail-safe behavior.
4
+ * Target: >90% coverage
5
+ */
6
+ export {};