-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathextensions.ts
More file actions
173 lines (158 loc) · 4.67 KB
/
extensions.ts
File metadata and controls
173 lines (158 loc) · 4.67 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
type RuntimeEventName =
| 'before_agent_start'
| 'before_model_call'
| 'after_model_call'
| 'model_retry'
| 'model_error'
| 'before_tool_call'
| 'after_tool_call'
| 'mcp_server_connected'
| 'mcp_server_disconnected'
| 'mcp_server_error'
| 'mcp_tool_call'
| 'turn_end';
interface RuntimeEventPayload {
event: RuntimeEventName;
[key: string]: unknown;
}
interface RuntimeExtension {
name: string;
onEvent?: (payload: RuntimeEventPayload) => void | Promise<void>;
onBeforeToolCall?: (
toolName: string,
args: Record<string, unknown>,
) => string | null | Promise<string | null>;
onAfterToolCall?: (
toolName: string,
args: Record<string, unknown>,
result: string,
) => void | Promise<void>;
}
const DANGEROUS_FILE_CONTENT_PATTERNS: Array<{ re: RegExp; reason: string }> = [
{
re: /\brm\s+-rf\s+\/(\s|$)/i,
reason:
'Detected destructive root delete pattern (`rm -rf /`) in file content.',
},
{
re: /:\(\)\s*\{.*\};\s*:/i,
reason: 'Detected fork-bomb pattern in file content.',
},
{
re: /\bcurl\b[^\n|]*\|\s*(sh|bash|zsh)\b/i,
reason:
'Detected remote shell execution pattern (`curl | sh`) in file content.',
},
];
const DANGEROUS_BASH_PATTERNS: Array<{ re: RegExp; reason: string }> = [
{
re: /\b(cat|sed|awk)\b[^|]*\.(env|pem|key|p12)\b[^|]*(\|\s*(curl|wget)|>\s*\/dev\/tcp)/i,
reason: 'Command appears to exfiltrate sensitive local files.',
},
{
re: /\b(printenv|env)\b[^|]*(\|\s*(curl|wget)|>\s*\/dev\/tcp)/i,
reason: 'Command appears to exfiltrate environment variables.',
},
];
const BINARY_OFFICE_FILE_RE = /\.(docx|xlsx|pptx|pdf)$/i;
function getBlockedBinaryOfficeFileReason(
toolName: string,
args: Record<string, unknown>,
): string | null {
if (toolName !== 'write' && toolName !== 'edit') return null;
const targetPath = String(args.path || '').trim();
if (!BINARY_OFFICE_FILE_RE.test(targetPath)) return null;
return `Refusing to ${toolName} plain text into binary Office/PDF file \`${targetPath}\`. Generate the artifact with a real tool or script instead of the file ${toolName} tool.`;
}
const securityHookExtension: RuntimeExtension = {
name: 'security-hook',
onBeforeToolCall: (toolName, args) => {
const binaryOfficeReason = getBlockedBinaryOfficeFileReason(toolName, args);
if (binaryOfficeReason) return binaryOfficeReason;
if (toolName === 'write' || toolName === 'edit') {
const content =
toolName === 'write'
? String(args.contents || '')
: String(args.new || '');
for (const pattern of DANGEROUS_FILE_CONTENT_PATTERNS) {
if (pattern.re.test(content)) return pattern.reason;
}
}
if (toolName === 'bash') {
const command = String(args.command || '');
for (const pattern of DANGEROUS_BASH_PATTERNS) {
if (pattern.re.test(command)) return pattern.reason;
}
}
return null;
},
};
const runtimeExtensions: RuntimeExtension[] = [securityHookExtension];
function parseArgs(argsJson: string): Record<string, unknown> {
try {
const parsed = JSON.parse(argsJson) as unknown;
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
return {};
return parsed as Record<string, unknown>;
} catch {
return {};
}
}
export async function emitRuntimeEvent(
payload: RuntimeEventPayload,
): Promise<void> {
for (const ext of runtimeExtensions) {
if (!ext.onEvent) continue;
try {
await ext.onEvent(payload);
} catch {
// Best effort: extension errors should not break request handling.
}
}
}
export async function runBeforeToolHooks(
toolName: string,
argsJson: string,
): Promise<string | null> {
const args = parseArgs(argsJson);
for (const ext of runtimeExtensions) {
if (!ext.onBeforeToolCall) continue;
try {
const blocked = await ext.onBeforeToolCall(toolName, args);
if (blocked) {
await emitRuntimeEvent({
event: 'before_tool_call',
toolName,
blocked: true,
extension: ext.name,
reason: blocked,
});
return blocked;
}
} catch {
// ignore broken extensions
}
}
await emitRuntimeEvent({
event: 'before_tool_call',
toolName,
blocked: false,
});
return null;
}
export async function runAfterToolHooks(
toolName: string,
argsJson: string,
result: string,
): Promise<void> {
const args = parseArgs(argsJson);
for (const ext of runtimeExtensions) {
if (!ext.onAfterToolCall) continue;
try {
await ext.onAfterToolCall(toolName, args, result);
} catch {
// ignore broken extensions
}
}
await emitRuntimeEvent({ event: 'after_tool_call', toolName });
}