@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.
- package/CHANGELOG.md +42 -1
- package/README.md +299 -6
- package/dist/__tests__/clients.test.d.ts +1 -0
- package/dist/__tests__/clients.test.js +847 -0
- package/dist/clients/agent.d.ts +70 -0
- package/dist/clients/agent.js +127 -0
- package/dist/clients/approval.d.ts +67 -0
- package/dist/clients/approval.js +103 -0
- package/dist/clients/budget.d.ts +221 -0
- package/dist/clients/budget.js +181 -0
- package/dist/clients/checkpoint.d.ts +191 -0
- package/dist/clients/checkpoint.js +195 -0
- package/dist/clients/envelope.d.ts +73 -0
- package/dist/clients/envelope.js +95 -0
- package/dist/clients/eventstore.d.ts +87 -0
- package/dist/clients/eventstore.js +113 -0
- package/dist/clients/index.d.ts +15 -0
- package/dist/clients/index.js +35 -0
- package/dist/clients/metrics.d.ts +126 -0
- package/dist/clients/metrics.js +162 -0
- package/dist/clients/policy.d.ts +83 -0
- package/dist/clients/policy.js +102 -0
- package/dist/clients/tenant.d.ts +66 -0
- package/dist/clients/tenant.js +92 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +25 -3
- package/dist/instrumentation/__tests__/autoGovern.test.d.ts +6 -0
- package/dist/instrumentation/__tests__/autoGovern.test.js +416 -0
- package/dist/instrumentation/__tests__/evaluator.test.d.ts +6 -0
- package/dist/instrumentation/__tests__/evaluator.test.js +712 -0
- package/dist/instrumentation/autoGovern.d.ts +57 -0
- package/dist/instrumentation/autoGovern.js +319 -0
- package/dist/instrumentation/evaluator.d.ts +50 -0
- package/dist/instrumentation/evaluator.js +218 -0
- package/dist/instrumentation/index.d.ts +28 -0
- package/dist/instrumentation/index.js +34 -0
- package/dist/instrumentation/types.d.ts +105 -0
- package/dist/instrumentation/types.js +20 -0
- package/package.json +4 -4
- package/proto/fulcrum/agent/v1/agent_service.proto +170 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-governance activation for TypeScript/Node.js.
|
|
3
|
+
*
|
|
4
|
+
* This module provides the main entry point for activating Fulcrum governance.
|
|
5
|
+
* It patches Node.js builtins and framework decorators at runtime.
|
|
6
|
+
*/
|
|
7
|
+
import type { InstrumentationConfig, Decision, Metrics } from './types';
|
|
8
|
+
import { PolicyEvaluator } from './evaluator';
|
|
9
|
+
/**
|
|
10
|
+
* Activate Fulcrum governance for TypeScript/Node.js.
|
|
11
|
+
*
|
|
12
|
+
* This function should be called BEFORE importing frameworks that use
|
|
13
|
+
* tools. It patches builtins and library exports.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* import { activate } from 'fulcrum/instrumentation';
|
|
18
|
+
* activate({ apiKey: 'flk_...', tenantId: 'my-tenant' });
|
|
19
|
+
*
|
|
20
|
+
* // Now import frameworks - they will be governed
|
|
21
|
+
* import fetch from 'node-fetch';
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export declare function activate(userConfig?: InstrumentationConfig): void;
|
|
25
|
+
/**
|
|
26
|
+
* Deactivate Fulcrum governance.
|
|
27
|
+
*
|
|
28
|
+
* This stops the policy evaluator and removes patches where possible.
|
|
29
|
+
* Note: Some patches cannot be fully reverted.
|
|
30
|
+
*/
|
|
31
|
+
export declare function deactivate(): void;
|
|
32
|
+
/**
|
|
33
|
+
* Check if Fulcrum governance is currently active.
|
|
34
|
+
*/
|
|
35
|
+
export declare function isActive(): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Get the active policy evaluator (for internal use by patches).
|
|
38
|
+
*/
|
|
39
|
+
export declare function getEvaluator(): PolicyEvaluator | null;
|
|
40
|
+
/**
|
|
41
|
+
* Get the active configuration (for internal use by patches).
|
|
42
|
+
*/
|
|
43
|
+
export declare function getConfig(): Required<InstrumentationConfig> | null;
|
|
44
|
+
/**
|
|
45
|
+
* Record a metric value (for internal use by patches).
|
|
46
|
+
*/
|
|
47
|
+
export declare function recordMetric(metric: keyof Metrics, increment?: number): void;
|
|
48
|
+
/**
|
|
49
|
+
* Get current metrics counters.
|
|
50
|
+
*/
|
|
51
|
+
export declare function getMetrics(): Metrics;
|
|
52
|
+
/**
|
|
53
|
+
* Evaluate a tool call against policies.
|
|
54
|
+
*
|
|
55
|
+
* This is the main entry point for patches to check governance.
|
|
56
|
+
*/
|
|
57
|
+
export declare function evaluateCall(toolName: string, actionType: string, resource?: string, toolArgs?: Record<string, unknown>, metadata?: Record<string, unknown>): Decision;
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Auto-governance activation for TypeScript/Node.js.
|
|
4
|
+
*
|
|
5
|
+
* This module provides the main entry point for activating Fulcrum governance.
|
|
6
|
+
* It patches Node.js builtins and framework decorators at runtime.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.activate = activate;
|
|
10
|
+
exports.deactivate = deactivate;
|
|
11
|
+
exports.isActive = isActive;
|
|
12
|
+
exports.getEvaluator = getEvaluator;
|
|
13
|
+
exports.getConfig = getConfig;
|
|
14
|
+
exports.recordMetric = recordMetric;
|
|
15
|
+
exports.getMetrics = getMetrics;
|
|
16
|
+
exports.evaluateCall = evaluateCall;
|
|
17
|
+
const types_1 = require("./types");
|
|
18
|
+
const evaluator_1 = require("./evaluator");
|
|
19
|
+
// Global state
|
|
20
|
+
let evaluator = null;
|
|
21
|
+
let config = null;
|
|
22
|
+
let active = false;
|
|
23
|
+
const metrics = {
|
|
24
|
+
governedCalls: 0,
|
|
25
|
+
ungovernedCalls: 0,
|
|
26
|
+
deniedCalls: 0,
|
|
27
|
+
allowedCalls: 0,
|
|
28
|
+
patchFailures: 0,
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Activate Fulcrum governance for TypeScript/Node.js.
|
|
32
|
+
*
|
|
33
|
+
* This function should be called BEFORE importing frameworks that use
|
|
34
|
+
* tools. It patches builtins and library exports.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```typescript
|
|
38
|
+
* import { activate } from 'fulcrum/instrumentation';
|
|
39
|
+
* activate({ apiKey: 'flk_...', tenantId: 'my-tenant' });
|
|
40
|
+
*
|
|
41
|
+
* // Now import frameworks - they will be governed
|
|
42
|
+
* import fetch from 'node-fetch';
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
function activate(userConfig = {}) {
|
|
46
|
+
if (active) {
|
|
47
|
+
console.warn('[fulcrum] Governance already active');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
// Create configuration
|
|
51
|
+
config = { ...types_1.DEFAULT_CONFIG, ...userConfig };
|
|
52
|
+
// Log validation warnings
|
|
53
|
+
if (!config.apiKey) {
|
|
54
|
+
console.warn('[fulcrum] api_key not set - operating in offline mode');
|
|
55
|
+
}
|
|
56
|
+
if (!config.tenantId) {
|
|
57
|
+
console.warn('[fulcrum] tenant_id not set - required for policy evaluation');
|
|
58
|
+
}
|
|
59
|
+
// Create and start evaluator
|
|
60
|
+
evaluator = new evaluator_1.PolicyEvaluator(config);
|
|
61
|
+
evaluator.start();
|
|
62
|
+
// Apply patches
|
|
63
|
+
applyPatches();
|
|
64
|
+
active = true;
|
|
65
|
+
console.info(`[fulcrum] Governance activated (patches: ${[...config.enabledPatches].join(', ')})`);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Deactivate Fulcrum governance.
|
|
69
|
+
*
|
|
70
|
+
* This stops the policy evaluator and removes patches where possible.
|
|
71
|
+
* Note: Some patches cannot be fully reverted.
|
|
72
|
+
*/
|
|
73
|
+
function deactivate() {
|
|
74
|
+
if (!active)
|
|
75
|
+
return;
|
|
76
|
+
// Stop evaluator
|
|
77
|
+
if (evaluator) {
|
|
78
|
+
evaluator.stop();
|
|
79
|
+
evaluator = null;
|
|
80
|
+
}
|
|
81
|
+
// Restore originals
|
|
82
|
+
restoreOriginals();
|
|
83
|
+
active = false;
|
|
84
|
+
config = null;
|
|
85
|
+
console.info('[fulcrum] Governance deactivated');
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Check if Fulcrum governance is currently active.
|
|
89
|
+
*/
|
|
90
|
+
function isActive() {
|
|
91
|
+
return active;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Get the active policy evaluator (for internal use by patches).
|
|
95
|
+
*/
|
|
96
|
+
function getEvaluator() {
|
|
97
|
+
return evaluator;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Get the active configuration (for internal use by patches).
|
|
101
|
+
*/
|
|
102
|
+
function getConfig() {
|
|
103
|
+
return config;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Record a metric value (for internal use by patches).
|
|
107
|
+
*/
|
|
108
|
+
function recordMetric(metric, increment = 1) {
|
|
109
|
+
metrics[metric] += increment;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Get current metrics counters.
|
|
113
|
+
*/
|
|
114
|
+
function getMetrics() {
|
|
115
|
+
return { ...metrics };
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Evaluate a tool call against policies.
|
|
119
|
+
*
|
|
120
|
+
* This is the main entry point for patches to check governance.
|
|
121
|
+
*/
|
|
122
|
+
function evaluateCall(toolName, actionType, resource, toolArgs = {}, metadata) {
|
|
123
|
+
if (!evaluator) {
|
|
124
|
+
recordMetric('ungovernedCalls');
|
|
125
|
+
return {
|
|
126
|
+
action: 'allow',
|
|
127
|
+
reason: 'Evaluator not initialized',
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
const request = {
|
|
131
|
+
toolName,
|
|
132
|
+
actionType,
|
|
133
|
+
resource,
|
|
134
|
+
toolArgs,
|
|
135
|
+
metadata,
|
|
136
|
+
};
|
|
137
|
+
const decision = evaluator.evaluate(request);
|
|
138
|
+
// Record metrics
|
|
139
|
+
recordMetric('governedCalls');
|
|
140
|
+
if (decision.action === 'allow') {
|
|
141
|
+
recordMetric('allowedCalls');
|
|
142
|
+
}
|
|
143
|
+
else if (decision.action === 'deny') {
|
|
144
|
+
recordMetric('deniedCalls');
|
|
145
|
+
}
|
|
146
|
+
return decision;
|
|
147
|
+
}
|
|
148
|
+
// Store original functions for restoration
|
|
149
|
+
const originals = new Map();
|
|
150
|
+
function applyPatches() {
|
|
151
|
+
if (!config)
|
|
152
|
+
return;
|
|
153
|
+
// Patch global fetch if enabled
|
|
154
|
+
if (config.enabledPatches.has('fetch') && typeof globalThis.fetch === 'function') {
|
|
155
|
+
patchFetch();
|
|
156
|
+
}
|
|
157
|
+
// Patch fs module if enabled
|
|
158
|
+
if (config.enabledPatches.has('fs')) {
|
|
159
|
+
patchFs();
|
|
160
|
+
}
|
|
161
|
+
// Patch child_process if enabled
|
|
162
|
+
if (config.enabledPatches.has('childProcess')) {
|
|
163
|
+
patchChildProcess();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function patchFetch() {
|
|
167
|
+
try {
|
|
168
|
+
const originalFetch = globalThis.fetch;
|
|
169
|
+
originals.set('fetch', originalFetch);
|
|
170
|
+
globalThis.fetch = async function governedFetch(input, init) {
|
|
171
|
+
const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
|
|
172
|
+
const method = init?.method || 'GET';
|
|
173
|
+
const decision = evaluateCall('fetch', 'http_request', url, { method, url }, { headers: init?.headers });
|
|
174
|
+
if (decision.action === 'deny') {
|
|
175
|
+
throw new Error(`HTTP request to '${url}' blocked by Fulcrum policy: ${decision.reason}`);
|
|
176
|
+
}
|
|
177
|
+
return originalFetch(input, init);
|
|
178
|
+
};
|
|
179
|
+
console.debug('[fulcrum] Patched global fetch');
|
|
180
|
+
}
|
|
181
|
+
catch (err) {
|
|
182
|
+
console.warn(`[fulcrum] Failed to patch fetch: ${err instanceof Error ? err.message : err}`);
|
|
183
|
+
recordMetric('patchFailures');
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function patchFs() {
|
|
187
|
+
try {
|
|
188
|
+
// Dynamic import to avoid issues if fs is not available
|
|
189
|
+
const fs = require('fs');
|
|
190
|
+
const originalReadFile = fs.readFile;
|
|
191
|
+
const originalWriteFile = fs.writeFile;
|
|
192
|
+
const originalReadFileSync = fs.readFileSync;
|
|
193
|
+
const originalWriteFileSync = fs.writeFileSync;
|
|
194
|
+
originals.set('fs.readFile', originalReadFile);
|
|
195
|
+
originals.set('fs.writeFile', originalWriteFile);
|
|
196
|
+
originals.set('fs.readFileSync', originalReadFileSync);
|
|
197
|
+
originals.set('fs.writeFileSync', originalWriteFileSync);
|
|
198
|
+
fs.readFile = function governedReadFile(path, ...args) {
|
|
199
|
+
const decision = evaluateCall('fs.readFile', 'file_read', String(path), { path });
|
|
200
|
+
if (decision.action === 'deny') {
|
|
201
|
+
const callback = args[args.length - 1];
|
|
202
|
+
if (typeof callback === 'function') {
|
|
203
|
+
callback(new Error(`File read '${path}' blocked by Fulcrum policy: ${decision.reason}`));
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
throw new Error(`File read '${path}' blocked by Fulcrum policy: ${decision.reason}`);
|
|
207
|
+
}
|
|
208
|
+
return originalReadFile(path, ...args);
|
|
209
|
+
};
|
|
210
|
+
fs.writeFile = function governedWriteFile(path, data, ...args) {
|
|
211
|
+
const decision = evaluateCall('fs.writeFile', 'file_write', String(path), { path });
|
|
212
|
+
if (decision.action === 'deny') {
|
|
213
|
+
const callback = args[args.length - 1];
|
|
214
|
+
if (typeof callback === 'function') {
|
|
215
|
+
callback(new Error(`File write '${path}' blocked by Fulcrum policy: ${decision.reason}`));
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
throw new Error(`File write '${path}' blocked by Fulcrum policy: ${decision.reason}`);
|
|
219
|
+
}
|
|
220
|
+
return originalWriteFile(path, data, ...args);
|
|
221
|
+
};
|
|
222
|
+
fs.readFileSync = function governedReadFileSync(path, options) {
|
|
223
|
+
const decision = evaluateCall('fs.readFileSync', 'file_read', String(path), { path });
|
|
224
|
+
if (decision.action === 'deny') {
|
|
225
|
+
throw new Error(`File read '${path}' blocked by Fulcrum policy: ${decision.reason}`);
|
|
226
|
+
}
|
|
227
|
+
return originalReadFileSync(path, options);
|
|
228
|
+
};
|
|
229
|
+
fs.writeFileSync = function governedWriteFileSync(path, data, options) {
|
|
230
|
+
const decision = evaluateCall('fs.writeFileSync', 'file_write', String(path), { path });
|
|
231
|
+
if (decision.action === 'deny') {
|
|
232
|
+
throw new Error(`File write '${path}' blocked by Fulcrum policy: ${decision.reason}`);
|
|
233
|
+
}
|
|
234
|
+
return originalWriteFileSync(path, data, options);
|
|
235
|
+
};
|
|
236
|
+
console.debug('[fulcrum] Patched fs module');
|
|
237
|
+
}
|
|
238
|
+
catch (err) {
|
|
239
|
+
console.warn(`[fulcrum] Failed to patch fs: ${err instanceof Error ? err.message : err}`);
|
|
240
|
+
recordMetric('patchFailures');
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
function patchChildProcess() {
|
|
244
|
+
try {
|
|
245
|
+
const childProcess = require('child_process');
|
|
246
|
+
const originalSpawn = childProcess.spawn;
|
|
247
|
+
const originalExec = childProcess.exec;
|
|
248
|
+
const originalExecSync = childProcess.execSync;
|
|
249
|
+
originals.set('child_process.spawn', originalSpawn);
|
|
250
|
+
originals.set('child_process.exec', originalExec);
|
|
251
|
+
originals.set('child_process.execSync', originalExecSync);
|
|
252
|
+
childProcess.spawn = function governedSpawn(command, args, options) {
|
|
253
|
+
const decision = evaluateCall('child_process.spawn', 'shell_execute', command, { command, args });
|
|
254
|
+
if (decision.action === 'deny') {
|
|
255
|
+
throw new Error(`Command '${command}' blocked by Fulcrum policy: ${decision.reason}`);
|
|
256
|
+
}
|
|
257
|
+
return originalSpawn(command, args, options);
|
|
258
|
+
};
|
|
259
|
+
childProcess.exec = function governedExec(command, ...args) {
|
|
260
|
+
const decision = evaluateCall('child_process.exec', 'shell_execute', command, { command });
|
|
261
|
+
if (decision.action === 'deny') {
|
|
262
|
+
const callback = args[args.length - 1];
|
|
263
|
+
if (typeof callback === 'function') {
|
|
264
|
+
callback(new Error(`Command '${command}' blocked by Fulcrum policy: ${decision.reason}`));
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
throw new Error(`Command '${command}' blocked by Fulcrum policy: ${decision.reason}`);
|
|
268
|
+
}
|
|
269
|
+
return originalExec(command, ...args);
|
|
270
|
+
};
|
|
271
|
+
childProcess.execSync = function governedExecSync(command, options) {
|
|
272
|
+
const decision = evaluateCall('child_process.execSync', 'shell_execute', command, { command });
|
|
273
|
+
if (decision.action === 'deny') {
|
|
274
|
+
throw new Error(`Command '${command}' blocked by Fulcrum policy: ${decision.reason}`);
|
|
275
|
+
}
|
|
276
|
+
return originalExecSync(command, options);
|
|
277
|
+
};
|
|
278
|
+
console.debug('[fulcrum] Patched child_process module');
|
|
279
|
+
}
|
|
280
|
+
catch (err) {
|
|
281
|
+
console.warn(`[fulcrum] Failed to patch child_process: ${err instanceof Error ? err.message : err}`);
|
|
282
|
+
recordMetric('patchFailures');
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
function restoreOriginals() {
|
|
286
|
+
// Restore fetch
|
|
287
|
+
if (originals.has('fetch')) {
|
|
288
|
+
globalThis.fetch = originals.get('fetch');
|
|
289
|
+
}
|
|
290
|
+
// Restore fs
|
|
291
|
+
try {
|
|
292
|
+
const fs = require('fs');
|
|
293
|
+
if (originals.has('fs.readFile'))
|
|
294
|
+
fs.readFile = originals.get('fs.readFile');
|
|
295
|
+
if (originals.has('fs.writeFile'))
|
|
296
|
+
fs.writeFile = originals.get('fs.writeFile');
|
|
297
|
+
if (originals.has('fs.readFileSync'))
|
|
298
|
+
fs.readFileSync = originals.get('fs.readFileSync');
|
|
299
|
+
if (originals.has('fs.writeFileSync'))
|
|
300
|
+
fs.writeFileSync = originals.get('fs.writeFileSync');
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
// fs not available
|
|
304
|
+
}
|
|
305
|
+
// Restore child_process
|
|
306
|
+
try {
|
|
307
|
+
const childProcess = require('child_process');
|
|
308
|
+
if (originals.has('child_process.spawn'))
|
|
309
|
+
childProcess.spawn = originals.get('child_process.spawn');
|
|
310
|
+
if (originals.has('child_process.exec'))
|
|
311
|
+
childProcess.exec = originals.get('child_process.exec');
|
|
312
|
+
if (originals.has('child_process.execSync'))
|
|
313
|
+
childProcess.execSync = originals.get('child_process.execSync');
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
// child_process not available
|
|
317
|
+
}
|
|
318
|
+
originals.clear();
|
|
319
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Policy evaluator for SDK instrumentation.
|
|
3
|
+
*
|
|
4
|
+
* This module provides a lightweight policy evaluation client that:
|
|
5
|
+
* 1. Caches policies locally for fast evaluation
|
|
6
|
+
* 2. Syncs with fulcrum-server periodically
|
|
7
|
+
* 3. Fails open (allow) on errors to avoid crashing agents
|
|
8
|
+
*/
|
|
9
|
+
import type { InstrumentationConfig, Decision, EvaluationRequest, Policy } from './types';
|
|
10
|
+
/**
|
|
11
|
+
* Lightweight policy evaluator for SDK use.
|
|
12
|
+
*/
|
|
13
|
+
export declare class PolicyEvaluator {
|
|
14
|
+
private config;
|
|
15
|
+
private policies;
|
|
16
|
+
private lastSync;
|
|
17
|
+
private syncError;
|
|
18
|
+
private syncInterval;
|
|
19
|
+
private running;
|
|
20
|
+
constructor(config?: InstrumentationConfig);
|
|
21
|
+
/**
|
|
22
|
+
* Start the background policy sync.
|
|
23
|
+
*/
|
|
24
|
+
start(): void;
|
|
25
|
+
/**
|
|
26
|
+
* Stop the background policy sync.
|
|
27
|
+
*/
|
|
28
|
+
stop(): void;
|
|
29
|
+
/**
|
|
30
|
+
* Evaluate a request against cached policies.
|
|
31
|
+
*
|
|
32
|
+
* This method is designed to be fast (<1ms) and fail-safe.
|
|
33
|
+
* On any error, it returns ALLOW with a warning.
|
|
34
|
+
*/
|
|
35
|
+
evaluate(request: EvaluationRequest): Decision;
|
|
36
|
+
private evaluateInternal;
|
|
37
|
+
private policyApplies;
|
|
38
|
+
private ruleMatches;
|
|
39
|
+
private conditionMatches;
|
|
40
|
+
private getFieldValue;
|
|
41
|
+
private getRuleAction;
|
|
42
|
+
private syncPolicies;
|
|
43
|
+
/**
|
|
44
|
+
* Manually update the policy cache (for testing).
|
|
45
|
+
*/
|
|
46
|
+
updatePolicies(policies: Policy[]): void;
|
|
47
|
+
get policyCount(): number;
|
|
48
|
+
get lastSyncTime(): number;
|
|
49
|
+
get syncErrorMessage(): string | null;
|
|
50
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Policy evaluator for SDK instrumentation.
|
|
4
|
+
*
|
|
5
|
+
* This module provides a lightweight policy evaluation client that:
|
|
6
|
+
* 1. Caches policies locally for fast evaluation
|
|
7
|
+
* 2. Syncs with fulcrum-server periodically
|
|
8
|
+
* 3. Fails open (allow) on errors to avoid crashing agents
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.PolicyEvaluator = void 0;
|
|
12
|
+
const types_1 = require("./types");
|
|
13
|
+
/**
|
|
14
|
+
* Lightweight policy evaluator for SDK use.
|
|
15
|
+
*/
|
|
16
|
+
class PolicyEvaluator {
|
|
17
|
+
constructor(config = {}) {
|
|
18
|
+
this.policies = [];
|
|
19
|
+
this.lastSync = 0;
|
|
20
|
+
this.syncError = null;
|
|
21
|
+
this.syncInterval = null;
|
|
22
|
+
this.running = false;
|
|
23
|
+
this.config = { ...types_1.DEFAULT_CONFIG, ...config };
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Start the background policy sync.
|
|
27
|
+
*/
|
|
28
|
+
start() {
|
|
29
|
+
if (this.running)
|
|
30
|
+
return;
|
|
31
|
+
this.running = true;
|
|
32
|
+
// Initial sync
|
|
33
|
+
this.syncPolicies().catch((err) => {
|
|
34
|
+
console.warn('[fulcrum] Initial policy sync failed:', err.message);
|
|
35
|
+
});
|
|
36
|
+
// Periodic sync
|
|
37
|
+
this.syncInterval = setInterval(() => this.syncPolicies().catch(() => { }), this.config.policySyncInterval * 1000);
|
|
38
|
+
console.info('[fulcrum] Policy evaluator started');
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Stop the background policy sync.
|
|
42
|
+
*/
|
|
43
|
+
stop() {
|
|
44
|
+
if (!this.running)
|
|
45
|
+
return;
|
|
46
|
+
this.running = false;
|
|
47
|
+
if (this.syncInterval) {
|
|
48
|
+
clearInterval(this.syncInterval);
|
|
49
|
+
this.syncInterval = null;
|
|
50
|
+
}
|
|
51
|
+
console.info('[fulcrum] Policy evaluator stopped');
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Evaluate a request against cached policies.
|
|
55
|
+
*
|
|
56
|
+
* This method is designed to be fast (<1ms) and fail-safe.
|
|
57
|
+
* On any error, it returns ALLOW with a warning.
|
|
58
|
+
*/
|
|
59
|
+
evaluate(request) {
|
|
60
|
+
try {
|
|
61
|
+
return this.evaluateInternal(request);
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
console.warn(`[fulcrum] Policy evaluation error (failing open): ${err instanceof Error ? err.message : err}`);
|
|
65
|
+
return {
|
|
66
|
+
action: 'allow',
|
|
67
|
+
reason: `Evaluation error (fail-open): ${err instanceof Error ? err.message : err}`,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
evaluateInternal(request) {
|
|
72
|
+
if (this.policies.length === 0) {
|
|
73
|
+
return {
|
|
74
|
+
action: 'allow',
|
|
75
|
+
reason: 'No policies loaded',
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
// Evaluate policies in priority order
|
|
79
|
+
for (const policy of this.policies) {
|
|
80
|
+
if (policy.status !== 'active')
|
|
81
|
+
continue;
|
|
82
|
+
if (!this.policyApplies(policy, request))
|
|
83
|
+
continue;
|
|
84
|
+
for (const rule of policy.rules) {
|
|
85
|
+
if (!rule.enabled)
|
|
86
|
+
continue;
|
|
87
|
+
if (this.ruleMatches(rule, request)) {
|
|
88
|
+
const action = this.getRuleAction(rule);
|
|
89
|
+
return {
|
|
90
|
+
action,
|
|
91
|
+
reason: rule.name || 'Matched rule',
|
|
92
|
+
matchedPolicyId: policy.policyId,
|
|
93
|
+
matchedRuleId: rule.ruleId,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
action: 'allow',
|
|
100
|
+
reason: 'No rules matched',
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
policyApplies(policy, request) {
|
|
104
|
+
const scope = policy.scope;
|
|
105
|
+
if (!scope)
|
|
106
|
+
return true;
|
|
107
|
+
if (scope.applyToAll)
|
|
108
|
+
return true;
|
|
109
|
+
if (scope.toolNames && scope.toolNames.length > 0) {
|
|
110
|
+
if (!scope.toolNames.includes(request.toolName)) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
ruleMatches(rule, request) {
|
|
117
|
+
if (rule.conditions.length === 0)
|
|
118
|
+
return true;
|
|
119
|
+
// All conditions must match (implicit AND)
|
|
120
|
+
return rule.conditions.every((condition) => this.conditionMatches(condition, request));
|
|
121
|
+
}
|
|
122
|
+
conditionMatches(condition, request) {
|
|
123
|
+
const fieldValue = this.getFieldValue(condition.field, request);
|
|
124
|
+
if (fieldValue === undefined)
|
|
125
|
+
return false;
|
|
126
|
+
const value = condition.value || '';
|
|
127
|
+
const values = condition.values || [];
|
|
128
|
+
switch (condition.operator) {
|
|
129
|
+
case 'equals':
|
|
130
|
+
return String(fieldValue) === value;
|
|
131
|
+
case 'not_equals':
|
|
132
|
+
return String(fieldValue) !== value;
|
|
133
|
+
case 'contains':
|
|
134
|
+
return String(fieldValue).includes(value);
|
|
135
|
+
case 'starts_with':
|
|
136
|
+
return String(fieldValue).startsWith(value);
|
|
137
|
+
case 'ends_with':
|
|
138
|
+
return String(fieldValue).endsWith(value);
|
|
139
|
+
case 'in':
|
|
140
|
+
return values.includes(String(fieldValue));
|
|
141
|
+
case 'not_in':
|
|
142
|
+
return !values.includes(String(fieldValue));
|
|
143
|
+
case 'matches':
|
|
144
|
+
try {
|
|
145
|
+
return new RegExp(value).test(String(fieldValue));
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
default:
|
|
151
|
+
console.warn(`[fulcrum] Unknown operator: ${condition.operator}`);
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
getFieldValue(field, request) {
|
|
156
|
+
if (field === 'tool_name')
|
|
157
|
+
return request.toolName;
|
|
158
|
+
if (field === 'action_type')
|
|
159
|
+
return request.actionType;
|
|
160
|
+
if (field === 'resource')
|
|
161
|
+
return request.resource;
|
|
162
|
+
if (field.startsWith('args.')) {
|
|
163
|
+
const argName = field.slice(5);
|
|
164
|
+
return request.toolArgs[argName];
|
|
165
|
+
}
|
|
166
|
+
if (field.startsWith('metadata.')) {
|
|
167
|
+
const metaName = field.slice(9);
|
|
168
|
+
return request.metadata?.[metaName];
|
|
169
|
+
}
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
getRuleAction(rule) {
|
|
173
|
+
if (rule.actions.length === 0)
|
|
174
|
+
return 'allow';
|
|
175
|
+
const actionType = rule.actions[0].actionType;
|
|
176
|
+
const actionMap = {
|
|
177
|
+
allow: 'allow',
|
|
178
|
+
deny: 'deny',
|
|
179
|
+
warn: 'warn',
|
|
180
|
+
escalate: 'escalate',
|
|
181
|
+
require_approval: 'require_approval',
|
|
182
|
+
};
|
|
183
|
+
return actionMap[actionType] || 'allow';
|
|
184
|
+
}
|
|
185
|
+
async syncPolicies() {
|
|
186
|
+
if (!this.config.apiKey || !this.config.tenantId) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
// TODO: Implement actual HTTP/gRPC call to fulcrum-server
|
|
190
|
+
// For now, this is a no-op placeholder
|
|
191
|
+
//
|
|
192
|
+
// Example implementation:
|
|
193
|
+
// const response = await fetch(`${this.config.serverUrl}/api/v1/policies`, {
|
|
194
|
+
// headers: { Authorization: `Bearer ${this.config.apiKey}` },
|
|
195
|
+
// });
|
|
196
|
+
// const data = await response.json();
|
|
197
|
+
// this.policies = data.policies;
|
|
198
|
+
this.lastSync = Date.now();
|
|
199
|
+
this.syncError = null;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Manually update the policy cache (for testing).
|
|
203
|
+
*/
|
|
204
|
+
updatePolicies(policies) {
|
|
205
|
+
this.policies = [...policies];
|
|
206
|
+
this.lastSync = Date.now();
|
|
207
|
+
}
|
|
208
|
+
get policyCount() {
|
|
209
|
+
return this.policies.length;
|
|
210
|
+
}
|
|
211
|
+
get lastSyncTime() {
|
|
212
|
+
return this.lastSync;
|
|
213
|
+
}
|
|
214
|
+
get syncErrorMessage() {
|
|
215
|
+
return this.syncError;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
exports.PolicyEvaluator = PolicyEvaluator;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fulcrum Auto-Instrumentation for TypeScript/Node.js.
|
|
3
|
+
*
|
|
4
|
+
* This module provides zero-code-change governance for AI agent tool calls.
|
|
5
|
+
* It patches Node.js builtins and framework decorators at runtime.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* // IMPORTANT: activate BEFORE importing frameworks
|
|
10
|
+
* import { activate } from 'fulcrum/instrumentation';
|
|
11
|
+
* activate({ apiKey: 'flk_...', tenantId: 'tenant-123' });
|
|
12
|
+
*
|
|
13
|
+
* // Now import your frameworks - they will be automatically governed
|
|
14
|
+
* import { tool } from '@langchain/core/tools';
|
|
15
|
+
* import fetch from 'node-fetch';
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* The instrumentation patches:
|
|
19
|
+
* - global fetch (file access)
|
|
20
|
+
* - node:fs (file operations)
|
|
21
|
+
* - child_process (shell commands)
|
|
22
|
+
* - node-fetch, axios (HTTP clients)
|
|
23
|
+
*
|
|
24
|
+
* All patches are fail-safe: governance errors never crash your agent.
|
|
25
|
+
*/
|
|
26
|
+
export { activate, deactivate, isActive } from './autoGovern';
|
|
27
|
+
export { PolicyEvaluator } from './evaluator';
|
|
28
|
+
export type { InstrumentationConfig, Decision, EvaluationRequest, Action } from './types';
|