ai-agent-guardrails 0.0.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/README.md +155 -0
- package/dist/index.d.ts +246 -0
- package/dist/index.js +358 -0
- package/dist/index.js.map +1 -0
- package/package.json +55 -0
- package/src/audit.ts +76 -0
- package/src/guard-tools.ts +158 -0
- package/src/index.ts +30 -0
- package/src/policy.ts +134 -0
- package/src/redaction.ts +107 -0
- package/src/types.ts +117 -0
- package/src/utils.ts +47 -0
- package/tsconfig.json +9 -0
- package/tsup.config.ts +10 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import type { GuardToolsOptions, ToolLike, GuardContext } from './types.js';
|
|
2
|
+
import { createDefaultContext, withTimeout } from './utils.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Wrap a toolset with guardrails enforcement
|
|
6
|
+
*
|
|
7
|
+
* This is the main entry point for the guardrails package. It wraps AI SDK tools
|
|
8
|
+
* with policy enforcement, budget checks, timeouts, and audit logging.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* const tools = guardTools(mcpTools, {
|
|
13
|
+
* policy: createSimplePolicy({ requireApprovalForRisk: ['write', 'admin'] }),
|
|
14
|
+
* audit: new ConsoleAuditSink(),
|
|
15
|
+
* timeoutMs: 10_000,
|
|
16
|
+
* });
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export function guardTools<T extends Record<string, ToolLike>>(
|
|
20
|
+
tools: T,
|
|
21
|
+
opts: GuardToolsOptions
|
|
22
|
+
): T {
|
|
23
|
+
const ctx = opts.ctx ?? createDefaultContext();
|
|
24
|
+
const audit = opts.audit;
|
|
25
|
+
const timeoutMs = opts.timeoutMs ?? 15_000;
|
|
26
|
+
const redactor = opts.redactor;
|
|
27
|
+
|
|
28
|
+
const wrapped: Record<string, ToolLike> = {};
|
|
29
|
+
|
|
30
|
+
for (const [toolName, tool] of Object.entries(tools)) {
|
|
31
|
+
const originalExecute = tool.execute;
|
|
32
|
+
|
|
33
|
+
// Set needsApproval based on policy decision
|
|
34
|
+
const needsApproval =
|
|
35
|
+
tool.needsApproval ??
|
|
36
|
+
(async (input: unknown) => {
|
|
37
|
+
try {
|
|
38
|
+
const { risk, reason } = await opts.policy.classify(toolName, input);
|
|
39
|
+
const decision = await opts.policy.decide({ toolName, input, ctx, risk, reason });
|
|
40
|
+
|
|
41
|
+
// Return true if decision requires approval
|
|
42
|
+
return Boolean((decision as any).needsApproval);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error(`[guardTools] Error in needsApproval check for ${toolName}:`, error);
|
|
45
|
+
// Fail closed: require approval on error
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
wrapped[toolName] = {
|
|
51
|
+
...tool,
|
|
52
|
+
needsApproval,
|
|
53
|
+
execute: originalExecute
|
|
54
|
+
? async (input: any) => {
|
|
55
|
+
const timestamp = Date.now();
|
|
56
|
+
|
|
57
|
+
// Redact input before logging if redactor is provided
|
|
58
|
+
const redactedInput = redactor ? redactor.redact(input) : input;
|
|
59
|
+
|
|
60
|
+
audit?.emit({
|
|
61
|
+
type: 'tool_call_attempted',
|
|
62
|
+
toolName,
|
|
63
|
+
input: redactedInput,
|
|
64
|
+
requestId: ctx.requestId,
|
|
65
|
+
timestamp,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Check tool call budget
|
|
69
|
+
ctx.toolCalls += 1;
|
|
70
|
+
if (ctx.toolCalls > ctx.maxToolCalls) {
|
|
71
|
+
const reason = `Tool budget exceeded (maxToolCalls=${ctx.maxToolCalls})`;
|
|
72
|
+
audit?.emit({
|
|
73
|
+
type: 'budget_exceeded',
|
|
74
|
+
reason,
|
|
75
|
+
requestId: ctx.requestId,
|
|
76
|
+
timestamp: Date.now(),
|
|
77
|
+
});
|
|
78
|
+
throw new Error(reason);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Check elapsed time budget
|
|
82
|
+
if (ctx.maxDurationMs) {
|
|
83
|
+
const elapsed = Date.now() - ctx.startTime;
|
|
84
|
+
if (elapsed > ctx.maxDurationMs) {
|
|
85
|
+
const reason = `Time budget exceeded (maxDurationMs=${ctx.maxDurationMs})`;
|
|
86
|
+
audit?.emit({
|
|
87
|
+
type: 'budget_exceeded',
|
|
88
|
+
reason,
|
|
89
|
+
requestId: ctx.requestId,
|
|
90
|
+
timestamp: Date.now(),
|
|
91
|
+
});
|
|
92
|
+
throw new Error(reason);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Classify and decide
|
|
97
|
+
const { risk, reason } = await opts.policy.classify(toolName, input);
|
|
98
|
+
const decision = await opts.policy.decide({ toolName, input, ctx, risk, reason });
|
|
99
|
+
|
|
100
|
+
// Block if not allowed
|
|
101
|
+
if (!decision.allow) {
|
|
102
|
+
audit?.emit({
|
|
103
|
+
type: 'tool_call_blocked',
|
|
104
|
+
toolName,
|
|
105
|
+
reason: decision.reason,
|
|
106
|
+
requestId: ctx.requestId,
|
|
107
|
+
timestamp: Date.now(),
|
|
108
|
+
});
|
|
109
|
+
throw new Error(`Tool call blocked: ${decision.reason}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Log if approval is needed (AI SDK will handle the actual approval flow)
|
|
113
|
+
if ((decision as any).needsApproval) {
|
|
114
|
+
audit?.emit({
|
|
115
|
+
type: 'tool_call_needs_approval',
|
|
116
|
+
toolName,
|
|
117
|
+
reason: (decision as any).reason ?? 'approval required',
|
|
118
|
+
requestId: ctx.requestId,
|
|
119
|
+
timestamp: Date.now(),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Execute with timeout
|
|
124
|
+
const t0 = Date.now();
|
|
125
|
+
try {
|
|
126
|
+
const result = await withTimeout(originalExecute(input), timeoutMs);
|
|
127
|
+
|
|
128
|
+
// Redact output if redactor is provided
|
|
129
|
+
const redactedResult = redactor ? redactor.redact(result) : result;
|
|
130
|
+
|
|
131
|
+
audit?.emit({
|
|
132
|
+
type: 'tool_call_executed',
|
|
133
|
+
toolName,
|
|
134
|
+
durationMs: Date.now() - t0,
|
|
135
|
+
requestId: ctx.requestId,
|
|
136
|
+
timestamp: Date.now(),
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
return result; // Return original result, not redacted (redaction is for logging only)
|
|
140
|
+
} catch (error: any) {
|
|
141
|
+
if (error.message?.includes('timed out')) {
|
|
142
|
+
audit?.emit({
|
|
143
|
+
type: 'tool_call_timeout',
|
|
144
|
+
toolName,
|
|
145
|
+
timeoutMs,
|
|
146
|
+
requestId: ctx.requestId,
|
|
147
|
+
timestamp: Date.now(),
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
: undefined,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return wrapped as T;
|
|
158
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Types
|
|
2
|
+
export type {
|
|
3
|
+
Risk,
|
|
4
|
+
GuardDecision,
|
|
5
|
+
GuardContext,
|
|
6
|
+
GuardPolicy,
|
|
7
|
+
AuditEvent,
|
|
8
|
+
AuditSink,
|
|
9
|
+
GuardToolsOptions,
|
|
10
|
+
ToolLike,
|
|
11
|
+
Redactor,
|
|
12
|
+
} from './types.js';
|
|
13
|
+
|
|
14
|
+
// Core functions
|
|
15
|
+
export { guardTools } from './guard-tools.js';
|
|
16
|
+
export { createDefaultContext, withTimeout } from './utils.js';
|
|
17
|
+
|
|
18
|
+
// Policy utilities
|
|
19
|
+
export { createSimplePolicy, PolicyBuilder } from './policy.js';
|
|
20
|
+
|
|
21
|
+
// Audit sinks
|
|
22
|
+
export { InMemoryAuditSink, ConsoleAuditSink, FileAuditSink } from './audit.js';
|
|
23
|
+
|
|
24
|
+
// Redaction utilities
|
|
25
|
+
export {
|
|
26
|
+
createRegexRedactor,
|
|
27
|
+
createDefaultRedactor,
|
|
28
|
+
createFieldRedactor,
|
|
29
|
+
composeRedactors,
|
|
30
|
+
} from './redaction.js';
|
package/src/policy.ts
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import type { Risk, GuardPolicy, GuardDecision, GuardContext } from './types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Create a simple policy with allowlist/denylist
|
|
5
|
+
*/
|
|
6
|
+
export function createSimplePolicy(options: {
|
|
7
|
+
allowlist?: string[];
|
|
8
|
+
denylist?: string[];
|
|
9
|
+
requireApprovalForRisk?: Risk[];
|
|
10
|
+
}): GuardPolicy {
|
|
11
|
+
const { allowlist, denylist, requireApprovalForRisk = ['write', 'admin'] } = options;
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
classify(toolName: string) {
|
|
15
|
+
// Classify based on tool name patterns
|
|
16
|
+
const lowered = toolName.toLowerCase();
|
|
17
|
+
|
|
18
|
+
if (
|
|
19
|
+
lowered.includes('delete') ||
|
|
20
|
+
lowered.includes('remove') ||
|
|
21
|
+
lowered.includes('destroy') ||
|
|
22
|
+
lowered.includes('drop')
|
|
23
|
+
) {
|
|
24
|
+
return { risk: 'admin', reason: 'destructive operation' };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (
|
|
28
|
+
lowered.includes('create') ||
|
|
29
|
+
lowered.includes('write') ||
|
|
30
|
+
lowered.includes('update') ||
|
|
31
|
+
lowered.includes('modify') ||
|
|
32
|
+
lowered.includes('insert') ||
|
|
33
|
+
lowered.includes('send') ||
|
|
34
|
+
lowered.includes('post')
|
|
35
|
+
) {
|
|
36
|
+
return { risk: 'write', reason: 'write operation' };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return { risk: 'read', reason: 'read-only operation' };
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
decide({ toolName, risk, ctx }): GuardDecision {
|
|
43
|
+
// Check denylist first
|
|
44
|
+
if (denylist && denylist.includes(toolName)) {
|
|
45
|
+
return { allow: false, reason: `Tool '${toolName}' is in denylist` };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Check allowlist if provided
|
|
49
|
+
if (allowlist && !allowlist.includes(toolName)) {
|
|
50
|
+
return { allow: false, reason: `Tool '${toolName}' is not in allowlist` };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Check if approval is required for this risk level
|
|
54
|
+
if (requireApprovalForRisk.includes(risk)) {
|
|
55
|
+
return {
|
|
56
|
+
allow: true,
|
|
57
|
+
needsApproval: true,
|
|
58
|
+
reason: `${risk} operation requires approval`,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { allow: true };
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Create a composable policy builder
|
|
69
|
+
*/
|
|
70
|
+
export class PolicyBuilder {
|
|
71
|
+
private classifiers: Array<(toolName: string, input: unknown) => { risk: Risk; reason?: string } | null> = [];
|
|
72
|
+
private rules: Array<
|
|
73
|
+
(args: {
|
|
74
|
+
toolName: string;
|
|
75
|
+
input: unknown;
|
|
76
|
+
ctx: GuardContext;
|
|
77
|
+
risk: Risk;
|
|
78
|
+
reason?: string;
|
|
79
|
+
}) => GuardDecision | null
|
|
80
|
+
> = [];
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Add a classifier function
|
|
84
|
+
*/
|
|
85
|
+
addClassifier(
|
|
86
|
+
classifier: (toolName: string, input: unknown) => { risk: Risk; reason?: string } | null
|
|
87
|
+
): this {
|
|
88
|
+
this.classifiers.push(classifier);
|
|
89
|
+
return this;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Add a decision rule
|
|
94
|
+
*/
|
|
95
|
+
addRule(
|
|
96
|
+
rule: (args: {
|
|
97
|
+
toolName: string;
|
|
98
|
+
input: unknown;
|
|
99
|
+
ctx: GuardContext;
|
|
100
|
+
risk: Risk;
|
|
101
|
+
reason?: string;
|
|
102
|
+
}) => GuardDecision | null
|
|
103
|
+
): this {
|
|
104
|
+
this.rules.push(rule);
|
|
105
|
+
return this;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Build the final policy
|
|
110
|
+
*/
|
|
111
|
+
build(): GuardPolicy {
|
|
112
|
+
return {
|
|
113
|
+
classify: (toolName: string, input: unknown) => {
|
|
114
|
+
// Try each classifier in order
|
|
115
|
+
for (const classifier of this.classifiers) {
|
|
116
|
+
const result = classifier(toolName, input);
|
|
117
|
+
if (result) return result;
|
|
118
|
+
}
|
|
119
|
+
// Default to read if no classifier matches
|
|
120
|
+
return { risk: 'read' };
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
decide: args => {
|
|
124
|
+
// Try each rule in order
|
|
125
|
+
for (const rule of this.rules) {
|
|
126
|
+
const decision = rule(args);
|
|
127
|
+
if (decision) return decision;
|
|
128
|
+
}
|
|
129
|
+
// Default to allow if no rule blocks
|
|
130
|
+
return { allow: true };
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
package/src/redaction.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { Redactor } from './types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Default patterns for secret detection
|
|
5
|
+
*/
|
|
6
|
+
const DEFAULT_SECRET_PATTERNS = [
|
|
7
|
+
/\b(sk-[a-zA-Z0-9]{48})\b/g, // OpenAI API keys
|
|
8
|
+
/\b(sk_live_[a-zA-Z0-9]{24,})\b/g, // Stripe live keys
|
|
9
|
+
/\b(sk_test_[a-zA-Z0-9]{24,})\b/g, // Stripe test keys
|
|
10
|
+
/\b([a-zA-Z0-9_-]{40})\b/g, // GitHub tokens (40 chars)
|
|
11
|
+
/\b(ghp_[a-zA-Z0-9]{36})\b/g, // GitHub personal access tokens
|
|
12
|
+
/\b(gho_[a-zA-Z0-9]{36})\b/g, // GitHub OAuth tokens
|
|
13
|
+
/\b(AKIA[0-9A-Z]{16})\b/g, // AWS access keys
|
|
14
|
+
/-----BEGIN\s+(?:RSA\s+)?PRIVATE\s+KEY-----/g, // Private keys
|
|
15
|
+
/\b([a-zA-Z0-9_-]{32,})\b/g, // Generic long tokens
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Default patterns for PII detection
|
|
20
|
+
*/
|
|
21
|
+
const DEFAULT_PII_PATTERNS = [
|
|
22
|
+
/\b\d{3}-\d{2}-\d{4}\b/g, // SSN
|
|
23
|
+
/\b\d{16}\b/g, // Credit card numbers
|
|
24
|
+
/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, // Email addresses
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Create a regex-based redactor
|
|
29
|
+
*/
|
|
30
|
+
export function createRegexRedactor(patterns: RegExp[], replacement = '[REDACTED]'): Redactor {
|
|
31
|
+
return {
|
|
32
|
+
redact(value: unknown): unknown {
|
|
33
|
+
if (typeof value === 'string') {
|
|
34
|
+
let redacted = value;
|
|
35
|
+
for (const pattern of patterns) {
|
|
36
|
+
redacted = redacted.replace(pattern, replacement);
|
|
37
|
+
}
|
|
38
|
+
return redacted;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (Array.isArray(value)) {
|
|
42
|
+
return value.map(item => this.redact(item));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (value && typeof value === 'object') {
|
|
46
|
+
const result: Record<string, unknown> = {};
|
|
47
|
+
for (const [key, val] of Object.entries(value)) {
|
|
48
|
+
result[key] = this.redact(val);
|
|
49
|
+
}
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return value;
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Create a default redactor with common secret and PII patterns
|
|
60
|
+
*/
|
|
61
|
+
export function createDefaultRedactor(): Redactor {
|
|
62
|
+
return createRegexRedactor([...DEFAULT_SECRET_PATTERNS, ...DEFAULT_PII_PATTERNS]);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Field-based redactor that redacts specific fields
|
|
67
|
+
*/
|
|
68
|
+
export function createFieldRedactor(fieldsToRedact: string[], replacement = '[REDACTED]'): Redactor {
|
|
69
|
+
const fieldSet = new Set(fieldsToRedact.map(f => f.toLowerCase()));
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
redact(value: unknown): unknown {
|
|
73
|
+
if (Array.isArray(value)) {
|
|
74
|
+
return value.map(item => this.redact(item));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (value && typeof value === 'object') {
|
|
78
|
+
const result: Record<string, unknown> = {};
|
|
79
|
+
for (const [key, val] of Object.entries(value)) {
|
|
80
|
+
if (fieldSet.has(key.toLowerCase())) {
|
|
81
|
+
result[key] = replacement;
|
|
82
|
+
} else {
|
|
83
|
+
result[key] = this.redact(val);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return value;
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Composite redactor that chains multiple redactors
|
|
96
|
+
*/
|
|
97
|
+
export function composeRedactors(...redactors: Redactor[]): Redactor {
|
|
98
|
+
return {
|
|
99
|
+
redact(value: unknown): unknown {
|
|
100
|
+
let result = value;
|
|
101
|
+
for (const redactor of redactors) {
|
|
102
|
+
result = redactor.redact(result);
|
|
103
|
+
}
|
|
104
|
+
return result;
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Risk classification for tools
|
|
3
|
+
*/
|
|
4
|
+
export type Risk = 'read' | 'write' | 'admin';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Decision outcome from policy evaluation
|
|
8
|
+
*/
|
|
9
|
+
export type GuardDecision =
|
|
10
|
+
| { allow: true }
|
|
11
|
+
| { allow: false; reason: string }
|
|
12
|
+
| { allow: true; needsApproval: true; reason: string };
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Context passed through guard execution
|
|
16
|
+
*/
|
|
17
|
+
export type GuardContext = {
|
|
18
|
+
requestId: string;
|
|
19
|
+
toolCalls: number;
|
|
20
|
+
maxToolCalls: number;
|
|
21
|
+
startTime: number;
|
|
22
|
+
maxDurationMs?: number;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Tool-like interface that matches AI SDK tool structure
|
|
27
|
+
*/
|
|
28
|
+
export type ToolLike = {
|
|
29
|
+
description?: string;
|
|
30
|
+
inputSchema?: unknown;
|
|
31
|
+
parameters?: unknown;
|
|
32
|
+
needsApproval?: boolean | ((input: any) => boolean | Promise<boolean>);
|
|
33
|
+
execute?: (input: any) => Promise<any>;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Policy interface for classification and decision making
|
|
38
|
+
*/
|
|
39
|
+
export type GuardPolicy = {
|
|
40
|
+
/**
|
|
41
|
+
* Classify a tool call by risk level
|
|
42
|
+
*/
|
|
43
|
+
classify: (
|
|
44
|
+
toolName: string,
|
|
45
|
+
input: unknown
|
|
46
|
+
) => Promise<{ risk: Risk; reason?: string }> | { risk: Risk; reason?: string };
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Decide whether to allow, block, or require approval
|
|
50
|
+
*/
|
|
51
|
+
decide: (args: {
|
|
52
|
+
toolName: string;
|
|
53
|
+
input: unknown;
|
|
54
|
+
ctx: GuardContext;
|
|
55
|
+
risk: Risk;
|
|
56
|
+
reason?: string;
|
|
57
|
+
}) => Promise<GuardDecision> | GuardDecision;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Audit event types
|
|
62
|
+
*/
|
|
63
|
+
export type AuditEvent =
|
|
64
|
+
| { type: 'tool_call_attempted'; toolName: string; input: unknown; requestId: string; timestamp: number }
|
|
65
|
+
| { type: 'tool_call_blocked'; toolName: string; reason: string; requestId: string; timestamp: number }
|
|
66
|
+
| {
|
|
67
|
+
type: 'tool_call_needs_approval';
|
|
68
|
+
toolName: string;
|
|
69
|
+
reason: string;
|
|
70
|
+
requestId: string;
|
|
71
|
+
timestamp: number;
|
|
72
|
+
}
|
|
73
|
+
| {
|
|
74
|
+
type: 'tool_call_executed';
|
|
75
|
+
toolName: string;
|
|
76
|
+
durationMs: number;
|
|
77
|
+
requestId: string;
|
|
78
|
+
timestamp: number;
|
|
79
|
+
}
|
|
80
|
+
| {
|
|
81
|
+
type: 'tool_call_timeout';
|
|
82
|
+
toolName: string;
|
|
83
|
+
timeoutMs: number;
|
|
84
|
+
requestId: string;
|
|
85
|
+
timestamp: number;
|
|
86
|
+
}
|
|
87
|
+
| {
|
|
88
|
+
type: 'budget_exceeded';
|
|
89
|
+
reason: string;
|
|
90
|
+
requestId: string;
|
|
91
|
+
timestamp: number;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Audit sink interface for logging events
|
|
96
|
+
*/
|
|
97
|
+
export type AuditSink = {
|
|
98
|
+
emit: (event: AuditEvent) => void | Promise<void>;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Options for guardTools wrapper
|
|
103
|
+
*/
|
|
104
|
+
export type GuardToolsOptions = {
|
|
105
|
+
policy: GuardPolicy;
|
|
106
|
+
ctx?: GuardContext;
|
|
107
|
+
audit?: AuditSink;
|
|
108
|
+
timeoutMs?: number;
|
|
109
|
+
redactor?: Redactor;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Redactor interface for PII/secret removal
|
|
114
|
+
*/
|
|
115
|
+
export type Redactor = {
|
|
116
|
+
redact: (value: unknown) => unknown;
|
|
117
|
+
};
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { GuardContext } from './types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generate a unique request ID
|
|
5
|
+
*/
|
|
6
|
+
function cryptoRandomId(): string {
|
|
7
|
+
// Use crypto.randomUUID() in Node 20+ and modern browsers
|
|
8
|
+
if (typeof globalThis.crypto?.randomUUID === 'function') {
|
|
9
|
+
return globalThis.crypto.randomUUID();
|
|
10
|
+
}
|
|
11
|
+
// Fallback for environments without crypto.randomUUID
|
|
12
|
+
return `req_${Math.random().toString(16).slice(2)}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Create a default guard context with budget limits
|
|
17
|
+
*/
|
|
18
|
+
export function createDefaultContext(requestId?: string): GuardContext {
|
|
19
|
+
return {
|
|
20
|
+
requestId: requestId ?? cryptoRandomId(),
|
|
21
|
+
toolCalls: 0,
|
|
22
|
+
maxToolCalls: 8,
|
|
23
|
+
startTime: Date.now(),
|
|
24
|
+
maxDurationMs: 60_000, // 60 seconds default
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Wrap a promise with a timeout
|
|
30
|
+
*/
|
|
31
|
+
export function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
|
|
32
|
+
return new Promise((resolve, reject) => {
|
|
33
|
+
const timeoutId = setTimeout(() => {
|
|
34
|
+
reject(new Error(`Operation timed out after ${ms}ms`));
|
|
35
|
+
}, ms);
|
|
36
|
+
|
|
37
|
+
promise
|
|
38
|
+
.then(value => {
|
|
39
|
+
clearTimeout(timeoutId);
|
|
40
|
+
resolve(value);
|
|
41
|
+
})
|
|
42
|
+
.catch(error => {
|
|
43
|
+
clearTimeout(timeoutId);
|
|
44
|
+
reject(error);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
}
|
package/tsconfig.json
ADDED