@ottocode/server 0.1.206 → 0.1.208
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/package.json +3 -3
- package/src/routes/mcp.ts +200 -3
- package/src/routes/sessions.ts +1 -1
- package/src/runtime/message/service.ts +1 -1
- package/src/runtime/tools/guards.ts +159 -0
- package/src/tools/adapter.ts +40 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ottocode/server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.208",
|
|
4
4
|
"description": "HTTP API server for ottocode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -49,8 +49,8 @@
|
|
|
49
49
|
"typecheck": "tsc --noEmit"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@ottocode/sdk": "0.1.
|
|
53
|
-
"@ottocode/database": "0.1.
|
|
52
|
+
"@ottocode/sdk": "0.1.208",
|
|
53
|
+
"@ottocode/database": "0.1.208",
|
|
54
54
|
"drizzle-orm": "^0.44.5",
|
|
55
55
|
"hono": "^4.9.9",
|
|
56
56
|
"zod": "^4.1.8"
|
package/src/routes/mcp.ts
CHANGED
|
@@ -8,6 +8,39 @@ import {
|
|
|
8
8
|
addMCPServerToConfig,
|
|
9
9
|
removeMCPServerFromConfig,
|
|
10
10
|
} from '@ottocode/sdk';
|
|
11
|
+
import {
|
|
12
|
+
authorizeCopilot,
|
|
13
|
+
pollForCopilotTokenOnce,
|
|
14
|
+
getAuth,
|
|
15
|
+
setAuth,
|
|
16
|
+
} from '@ottocode/sdk';
|
|
17
|
+
|
|
18
|
+
const GITHUB_COPILOT_HOSTS = [
|
|
19
|
+
'api.githubcopilot.com',
|
|
20
|
+
'copilot-proxy.githubusercontent.com',
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
function isGitHubCopilotUrl(url?: string): boolean {
|
|
24
|
+
if (!url) return false;
|
|
25
|
+
try {
|
|
26
|
+
const parsed = new URL(url);
|
|
27
|
+
return GITHUB_COPILOT_HOSTS.some(
|
|
28
|
+
(h) => parsed.hostname === h || parsed.hostname.endsWith(`.${h}`),
|
|
29
|
+
);
|
|
30
|
+
} catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const copilotMCPSessions = new Map<
|
|
36
|
+
string,
|
|
37
|
+
{
|
|
38
|
+
deviceCode: string;
|
|
39
|
+
interval: number;
|
|
40
|
+
serverName: string;
|
|
41
|
+
createdAt: number;
|
|
42
|
+
}
|
|
43
|
+
>();
|
|
11
44
|
|
|
12
45
|
export function registerMCPRoutes(app: Hono) {
|
|
13
46
|
app.get('/v1/mcp/servers', async (c) => {
|
|
@@ -30,6 +63,7 @@ export function registerMCPRoutes(app: Hono) {
|
|
|
30
63
|
authRequired: status?.authRequired ?? false,
|
|
31
64
|
authenticated: status?.authenticated ?? false,
|
|
32
65
|
scope: s.scope ?? 'global',
|
|
66
|
+
...(isGitHubCopilotUrl(s.url) ? { authType: 'copilot-device' } : {}),
|
|
33
67
|
};
|
|
34
68
|
});
|
|
35
69
|
|
|
@@ -148,6 +182,37 @@ export function registerMCPRoutes(app: Hono) {
|
|
|
148
182
|
const status = (await manager.getStatusAsync()).find(
|
|
149
183
|
(s) => s.name === name,
|
|
150
184
|
);
|
|
185
|
+
|
|
186
|
+
if (isGitHubCopilotUrl(serverConfig.url) && !status?.connected) {
|
|
187
|
+
const MCP_SCOPES =
|
|
188
|
+
'repo read:org read:packages gist notifications read:project security_events';
|
|
189
|
+
const existingAuth = await getAuth('copilot');
|
|
190
|
+
const hasMCPScopes =
|
|
191
|
+
existingAuth?.type === 'oauth' && existingAuth.scopes === MCP_SCOPES;
|
|
192
|
+
|
|
193
|
+
if (!existingAuth || existingAuth.type !== 'oauth' || !hasMCPScopes) {
|
|
194
|
+
const deviceData = await authorizeCopilot({ mcp: true });
|
|
195
|
+
const sessionId = crypto.randomUUID();
|
|
196
|
+
copilotMCPSessions.set(sessionId, {
|
|
197
|
+
deviceCode: deviceData.deviceCode,
|
|
198
|
+
interval: deviceData.interval,
|
|
199
|
+
serverName: name,
|
|
200
|
+
createdAt: Date.now(),
|
|
201
|
+
});
|
|
202
|
+
return c.json({
|
|
203
|
+
ok: true,
|
|
204
|
+
name,
|
|
205
|
+
connected: false,
|
|
206
|
+
authRequired: true,
|
|
207
|
+
authType: 'copilot-device',
|
|
208
|
+
sessionId,
|
|
209
|
+
userCode: deviceData.userCode,
|
|
210
|
+
verificationUri: deviceData.verificationUri,
|
|
211
|
+
interval: deviceData.interval,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
151
216
|
return c.json({
|
|
152
217
|
ok: true,
|
|
153
218
|
name,
|
|
@@ -189,6 +254,48 @@ export function registerMCPRoutes(app: Hono) {
|
|
|
189
254
|
return c.json({ ok: false, error: `Server "${name}" not found` }, 404);
|
|
190
255
|
}
|
|
191
256
|
|
|
257
|
+
if (isGitHubCopilotUrl(serverConfig.url)) {
|
|
258
|
+
try {
|
|
259
|
+
const MCP_SCOPES =
|
|
260
|
+
'repo read:org read:packages gist notifications read:project security_events';
|
|
261
|
+
const existingAuth = await getAuth('copilot');
|
|
262
|
+
if (
|
|
263
|
+
existingAuth?.type === 'oauth' &&
|
|
264
|
+
existingAuth.refresh &&
|
|
265
|
+
existingAuth.scopes === MCP_SCOPES
|
|
266
|
+
) {
|
|
267
|
+
return c.json({
|
|
268
|
+
ok: true,
|
|
269
|
+
name,
|
|
270
|
+
authType: 'copilot-device',
|
|
271
|
+
authenticated: true,
|
|
272
|
+
message: 'Already authenticated with MCP scopes',
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const deviceData = await authorizeCopilot({ mcp: true });
|
|
277
|
+
const sessionId = crypto.randomUUID();
|
|
278
|
+
copilotMCPSessions.set(sessionId, {
|
|
279
|
+
deviceCode: deviceData.deviceCode,
|
|
280
|
+
interval: deviceData.interval,
|
|
281
|
+
serverName: name,
|
|
282
|
+
createdAt: Date.now(),
|
|
283
|
+
});
|
|
284
|
+
return c.json({
|
|
285
|
+
ok: true,
|
|
286
|
+
name,
|
|
287
|
+
authType: 'copilot-device',
|
|
288
|
+
sessionId,
|
|
289
|
+
userCode: deviceData.userCode,
|
|
290
|
+
verificationUri: deviceData.verificationUri,
|
|
291
|
+
interval: deviceData.interval,
|
|
292
|
+
});
|
|
293
|
+
} catch (err) {
|
|
294
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
295
|
+
return c.json({ ok: false, error: msg }, 500);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
192
299
|
try {
|
|
193
300
|
let manager = getMCPManager();
|
|
194
301
|
if (!manager) {
|
|
@@ -216,7 +323,66 @@ export function registerMCPRoutes(app: Hono) {
|
|
|
216
323
|
app.post('/v1/mcp/servers/:name/auth/callback', async (c) => {
|
|
217
324
|
const name = c.req.param('name');
|
|
218
325
|
const body = await c.req.json();
|
|
219
|
-
const { code } = body;
|
|
326
|
+
const { code, sessionId } = body;
|
|
327
|
+
|
|
328
|
+
if (sessionId) {
|
|
329
|
+
const session = copilotMCPSessions.get(sessionId);
|
|
330
|
+
if (!session || session.serverName !== name) {
|
|
331
|
+
return c.json({ ok: false, error: 'Session expired or invalid' }, 400);
|
|
332
|
+
}
|
|
333
|
+
try {
|
|
334
|
+
const result = await pollForCopilotTokenOnce(session.deviceCode);
|
|
335
|
+
if (result.status === 'complete') {
|
|
336
|
+
copilotMCPSessions.delete(sessionId);
|
|
337
|
+
await setAuth(
|
|
338
|
+
'copilot',
|
|
339
|
+
{
|
|
340
|
+
type: 'oauth',
|
|
341
|
+
refresh: result.accessToken,
|
|
342
|
+
access: result.accessToken,
|
|
343
|
+
expires: 0,
|
|
344
|
+
scopes:
|
|
345
|
+
'repo read:org read:packages gist notifications read:project security_events',
|
|
346
|
+
},
|
|
347
|
+
undefined,
|
|
348
|
+
'global',
|
|
349
|
+
);
|
|
350
|
+
const projectRoot = process.cwd();
|
|
351
|
+
const config = await loadMCPConfig(projectRoot, getGlobalConfigDir());
|
|
352
|
+
const serverConfig = config.servers.find((s) => s.name === name);
|
|
353
|
+
let mcpMgr = getMCPManager();
|
|
354
|
+
if (serverConfig) {
|
|
355
|
+
if (!mcpMgr) {
|
|
356
|
+
mcpMgr = await initializeMCP({ servers: [] }, projectRoot);
|
|
357
|
+
}
|
|
358
|
+
await mcpMgr.restartServer(serverConfig);
|
|
359
|
+
}
|
|
360
|
+
mcpMgr = getMCPManager();
|
|
361
|
+
const status = mcpMgr
|
|
362
|
+
? (await mcpMgr.getStatusAsync()).find((s) => s.name === name)
|
|
363
|
+
: undefined;
|
|
364
|
+
return c.json({
|
|
365
|
+
ok: true,
|
|
366
|
+
status: 'complete',
|
|
367
|
+
name,
|
|
368
|
+
connected: status?.connected ?? false,
|
|
369
|
+
tools: status?.tools ?? [],
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
if (result.status === 'pending') {
|
|
373
|
+
return c.json({ ok: true, status: 'pending' });
|
|
374
|
+
}
|
|
375
|
+
copilotMCPSessions.delete(sessionId);
|
|
376
|
+
return c.json({
|
|
377
|
+
ok: false,
|
|
378
|
+
status: 'error',
|
|
379
|
+
error: result.status === 'error' ? result.error : 'Unknown error',
|
|
380
|
+
});
|
|
381
|
+
} catch (err) {
|
|
382
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
383
|
+
return c.json({ ok: false, error: msg }, 500);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
220
386
|
|
|
221
387
|
if (!code) {
|
|
222
388
|
return c.json({ ok: false, error: 'code is required' }, 400);
|
|
@@ -249,8 +415,21 @@ export function registerMCPRoutes(app: Hono) {
|
|
|
249
415
|
|
|
250
416
|
app.get('/v1/mcp/servers/:name/auth/status', async (c) => {
|
|
251
417
|
const name = c.req.param('name');
|
|
252
|
-
const
|
|
418
|
+
const projectRoot = process.cwd();
|
|
419
|
+
const config = await loadMCPConfig(projectRoot, getGlobalConfigDir());
|
|
420
|
+
const serverConfig = config.servers.find((s) => s.name === name);
|
|
421
|
+
|
|
422
|
+
if (serverConfig && isGitHubCopilotUrl(serverConfig.url)) {
|
|
423
|
+
try {
|
|
424
|
+
const auth = await getAuth('copilot');
|
|
425
|
+
const authenticated = auth?.type === 'oauth' && !!auth.refresh;
|
|
426
|
+
return c.json({ authenticated, authType: 'copilot-device' });
|
|
427
|
+
} catch {
|
|
428
|
+
return c.json({ authenticated: false, authType: 'copilot-device' });
|
|
429
|
+
}
|
|
430
|
+
}
|
|
253
431
|
|
|
432
|
+
const manager = getMCPManager();
|
|
254
433
|
if (!manager) {
|
|
255
434
|
return c.json({ authenticated: false });
|
|
256
435
|
}
|
|
@@ -265,8 +444,26 @@ export function registerMCPRoutes(app: Hono) {
|
|
|
265
444
|
|
|
266
445
|
app.delete('/v1/mcp/servers/:name/auth', async (c) => {
|
|
267
446
|
const name = c.req.param('name');
|
|
268
|
-
const
|
|
447
|
+
const projectRoot = process.cwd();
|
|
448
|
+
const config = await loadMCPConfig(projectRoot, getGlobalConfigDir());
|
|
449
|
+
const serverConfig = config.servers.find((s) => s.name === name);
|
|
269
450
|
|
|
451
|
+
if (serverConfig && isGitHubCopilotUrl(serverConfig.url)) {
|
|
452
|
+
try {
|
|
453
|
+
const { removeAuth } = await import('@ottocode/sdk');
|
|
454
|
+
await removeAuth('copilot');
|
|
455
|
+
const manager = getMCPManager();
|
|
456
|
+
if (manager) {
|
|
457
|
+
await manager.stopServer(name);
|
|
458
|
+
}
|
|
459
|
+
return c.json({ ok: true, name });
|
|
460
|
+
} catch (err) {
|
|
461
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
462
|
+
return c.json({ ok: false, error: msg }, 500);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const manager = getMCPManager();
|
|
270
467
|
if (!manager) {
|
|
271
468
|
return c.json({ ok: false, error: 'No MCP manager active' }, 400);
|
|
272
469
|
}
|
package/src/routes/sessions.ts
CHANGED
|
@@ -800,7 +800,7 @@ export function registerSessionsRoutes(app: Hono) {
|
|
|
800
800
|
);
|
|
801
801
|
const { runSessionLoop } = await import('../runtime/agent/runner.ts');
|
|
802
802
|
|
|
803
|
-
const toolApprovalMode = cfg.defaults.toolApproval ?? '
|
|
803
|
+
const toolApprovalMode = cfg.defaults.toolApproval ?? 'dangerous';
|
|
804
804
|
|
|
805
805
|
enqueueAssistantRun(
|
|
806
806
|
{
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
export type GuardAction =
|
|
2
|
+
| { type: 'block'; reason: string }
|
|
3
|
+
| { type: 'approve'; reason: string }
|
|
4
|
+
| { type: 'allow' };
|
|
5
|
+
|
|
6
|
+
export function guardToolCall(toolName: string, args: unknown): GuardAction {
|
|
7
|
+
const a = (args ?? {}) as Record<string, unknown>;
|
|
8
|
+
|
|
9
|
+
switch (toolName) {
|
|
10
|
+
case 'bash':
|
|
11
|
+
return guardBashCommand(String(a.cmd ?? ''));
|
|
12
|
+
case 'terminal':
|
|
13
|
+
return guardTerminal(a);
|
|
14
|
+
case 'read':
|
|
15
|
+
return guardReadPath(String(a.path ?? ''));
|
|
16
|
+
case 'write':
|
|
17
|
+
case 'edit':
|
|
18
|
+
case 'multiedit':
|
|
19
|
+
return guardWritePath(toolName, a);
|
|
20
|
+
default:
|
|
21
|
+
return { type: 'allow' };
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function guardBashCommand(cmd: string): GuardAction {
|
|
26
|
+
const n = cmd.trim();
|
|
27
|
+
if (!n) return { type: 'allow' };
|
|
28
|
+
|
|
29
|
+
const blocked = checkBlockedCommand(n);
|
|
30
|
+
if (blocked) return { type: 'block', reason: blocked };
|
|
31
|
+
|
|
32
|
+
const approval = checkApprovalCommand(n);
|
|
33
|
+
if (approval) return { type: 'approve', reason: approval };
|
|
34
|
+
|
|
35
|
+
return { type: 'allow' };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function checkBlockedCommand(cmd: string): string | null {
|
|
39
|
+
if (isRecursiveDeleteRoot(cmd)) return 'Recursive delete of root filesystem';
|
|
40
|
+
if (isRecursiveDeleteHome(cmd)) return 'Recursive delete of home directory';
|
|
41
|
+
if (isForkBomb(cmd)) return 'Fork bomb detected';
|
|
42
|
+
if (isFilesystemFormat(cmd)) return 'Filesystem format command';
|
|
43
|
+
if (isRawDiskWrite(cmd)) return 'Raw disk write operation';
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isRecursiveDeleteRoot(cmd: string): boolean {
|
|
48
|
+
if (!/\brm\b/.test(cmd)) return false;
|
|
49
|
+
if (!hasRecursiveFlag(cmd)) return false;
|
|
50
|
+
return /\s\/(\s*$|\s*\*|\s*;|\s*&|\s*\|)/.test(cmd);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isRecursiveDeleteHome(cmd: string): boolean {
|
|
54
|
+
if (!/\brm\b/.test(cmd)) return false;
|
|
55
|
+
if (!hasRecursiveFlag(cmd)) return false;
|
|
56
|
+
return /\s~\/?\s*($|\*|;|&|\|)/.test(cmd);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function hasRecursiveFlag(cmd: string): boolean {
|
|
60
|
+
return /-\w*[rR]|--recursive/.test(cmd);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isForkBomb(cmd: string): boolean {
|
|
64
|
+
return /:\(\)\s*\{[^}]*:\s*\|\s*:/.test(cmd);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function isFilesystemFormat(cmd: string): boolean {
|
|
68
|
+
return /\bmkfs(\.\w+)?\s/.test(cmd);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function isRawDiskWrite(cmd: string): boolean {
|
|
72
|
+
if (/\bdd\b/.test(cmd) && /\bof=\/dev\//.test(cmd)) return true;
|
|
73
|
+
if (/>\s*\/dev\/[sv]d/.test(cmd)) return true;
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function checkApprovalCommand(cmd: string): string | null {
|
|
78
|
+
if (/\brm\b/.test(cmd) && hasRecursiveFlag(cmd)) {
|
|
79
|
+
return 'Recursive delete command';
|
|
80
|
+
}
|
|
81
|
+
if (/\bsudo\b/.test(cmd)) {
|
|
82
|
+
return 'Privilege escalation (sudo)';
|
|
83
|
+
}
|
|
84
|
+
if (/\b(chmod|chown)\b/.test(cmd) && /(-\w*R|--recursive)/.test(cmd)) {
|
|
85
|
+
return 'Recursive permission/ownership change';
|
|
86
|
+
}
|
|
87
|
+
if (/\b(curl|wget)\b/.test(cmd) && /\|\s*(bash|sh|zsh)\b/.test(cmd)) {
|
|
88
|
+
return 'Remote code execution via pipe to shell';
|
|
89
|
+
}
|
|
90
|
+
if (/\bgit\s+push\b.*--force/.test(cmd)) {
|
|
91
|
+
return 'Force push to remote';
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function guardTerminal(args: Record<string, unknown>): GuardAction {
|
|
97
|
+
const op = String(args.operation ?? '');
|
|
98
|
+
if (op === 'start' && typeof args.command === 'string') {
|
|
99
|
+
return guardBashCommand(args.command);
|
|
100
|
+
}
|
|
101
|
+
return { type: 'allow' };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const BLOCKED_READ_PATHS: Array<{ pattern: RegExp; reason: string }> = [
|
|
105
|
+
{ pattern: /^~?\/?\.ssh\/id_/, reason: 'SSH private key access' },
|
|
106
|
+
{ pattern: /^\/etc\/shadow$/, reason: 'System password hashes' },
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
const SENSITIVE_READ_PATHS: Array<{ pattern: RegExp; reason: string }> = [
|
|
110
|
+
{ pattern: /^\/etc\/passwd$/, reason: 'System password file' },
|
|
111
|
+
{ pattern: /^~?\/?\.ssh\//, reason: 'SSH directory access' },
|
|
112
|
+
{ pattern: /^~?\/?\.aws\//, reason: 'AWS credentials' },
|
|
113
|
+
{ pattern: /^~?\/?\.gnupg\//, reason: 'GPG keyring' },
|
|
114
|
+
{ pattern: /^~?\/?\.config\/gh\//, reason: 'GitHub CLI tokens' },
|
|
115
|
+
{ pattern: /^~?\/?\.npmrc$/, reason: 'npm auth tokens' },
|
|
116
|
+
{ pattern: /^~?\/?\.netrc$/, reason: 'Network credentials' },
|
|
117
|
+
{ pattern: /^~?\/?\.kube\//, reason: 'Kubernetes config' },
|
|
118
|
+
{ pattern: /^~?\/?\.docker\/config\.json$/, reason: 'Docker credentials' },
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
function guardReadPath(path: string): GuardAction {
|
|
122
|
+
if (!path) return { type: 'allow' };
|
|
123
|
+
const p = path.trim();
|
|
124
|
+
|
|
125
|
+
for (const { pattern, reason } of BLOCKED_READ_PATHS) {
|
|
126
|
+
if (pattern.test(p)) return { type: 'block', reason };
|
|
127
|
+
}
|
|
128
|
+
for (const { pattern, reason } of SENSITIVE_READ_PATHS) {
|
|
129
|
+
if (pattern.test(p)) return { type: 'approve', reason };
|
|
130
|
+
}
|
|
131
|
+
if (p.startsWith('/') || p.startsWith('~')) {
|
|
132
|
+
return { type: 'approve', reason: 'Reading path outside project root' };
|
|
133
|
+
}
|
|
134
|
+
return { type: 'allow' };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const SENSITIVE_WRITE_PATHS: Array<{ pattern: RegExp; reason: string }> = [
|
|
138
|
+
{ pattern: /(^|\/)\.env($|\.)/, reason: 'Writing to environment file' },
|
|
139
|
+
{ pattern: /(^|\/)\.git\/hooks\//, reason: 'Writing to git hooks' },
|
|
140
|
+
];
|
|
141
|
+
|
|
142
|
+
function guardWritePath(
|
|
143
|
+
toolName: string,
|
|
144
|
+
args: Record<string, unknown>,
|
|
145
|
+
): GuardAction {
|
|
146
|
+
const path =
|
|
147
|
+
typeof args.path === 'string'
|
|
148
|
+
? args.path
|
|
149
|
+
: typeof args.filePath === 'string'
|
|
150
|
+
? args.filePath
|
|
151
|
+
: '';
|
|
152
|
+
if (!path) return { type: 'allow' };
|
|
153
|
+
const p = path.trim();
|
|
154
|
+
|
|
155
|
+
for (const { pattern, reason } of SENSITIVE_WRITE_PATHS) {
|
|
156
|
+
if (pattern.test(p)) return { type: 'approve', reason };
|
|
157
|
+
}
|
|
158
|
+
return { type: 'allow' };
|
|
159
|
+
}
|
package/src/tools/adapter.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
requiresApproval,
|
|
18
18
|
requestApproval,
|
|
19
19
|
} from '../runtime/tools/approval.ts';
|
|
20
|
+
import { guardToolCall } from '../runtime/tools/guards.ts';
|
|
20
21
|
|
|
21
22
|
export type { ToolAdapterContext } from '../runtime/tools/context.ts';
|
|
22
23
|
|
|
@@ -38,6 +39,8 @@ type PendingCallMeta = {
|
|
|
38
39
|
stepIndex?: number;
|
|
39
40
|
args?: unknown;
|
|
40
41
|
approvalPromise?: Promise<boolean>;
|
|
42
|
+
blocked?: boolean;
|
|
43
|
+
blockReason?: string;
|
|
41
44
|
};
|
|
42
45
|
|
|
43
46
|
function getPendingQueue(
|
|
@@ -336,6 +339,19 @@ export function adaptTools(
|
|
|
336
339
|
args,
|
|
337
340
|
);
|
|
338
341
|
}
|
|
342
|
+
const guard = guardToolCall(name, args);
|
|
343
|
+
if (guard.type === 'block') {
|
|
344
|
+
meta.blocked = true;
|
|
345
|
+
meta.blockReason = guard.reason;
|
|
346
|
+
} else if (guard.type === 'approve' && !meta.approvalPromise) {
|
|
347
|
+
meta.approvalPromise = requestApproval(
|
|
348
|
+
ctx.sessionId,
|
|
349
|
+
ctx.messageId,
|
|
350
|
+
callId,
|
|
351
|
+
name,
|
|
352
|
+
args,
|
|
353
|
+
);
|
|
354
|
+
}
|
|
339
355
|
if (typeof base.onInputAvailable === 'function') {
|
|
340
356
|
// biome-ignore lint/suspicious/noExplicitAny: AI SDK types are complex
|
|
341
357
|
await base.onInputAvailable(options as any);
|
|
@@ -367,14 +383,36 @@ export function adaptTools(
|
|
|
367
383
|
|
|
368
384
|
const executeWithGuards = async (): Promise<ToolExecuteReturn> => {
|
|
369
385
|
try {
|
|
386
|
+
if (meta?.blocked) {
|
|
387
|
+
const blockedResult = {
|
|
388
|
+
ok: false,
|
|
389
|
+
error: `Blocked: ${meta.blockReason}`,
|
|
390
|
+
details: { reason: 'safety_guard' },
|
|
391
|
+
};
|
|
392
|
+
await persistToolErrorResult(blockedResult, {
|
|
393
|
+
callId: callIdFromQueue,
|
|
394
|
+
startTs: startTsFromQueue,
|
|
395
|
+
stepIndexForEvent,
|
|
396
|
+
args: meta?.args,
|
|
397
|
+
});
|
|
398
|
+
return blockedResult as ToolExecuteReturn;
|
|
399
|
+
}
|
|
370
400
|
// Await approval if it was requested in onInputAvailable
|
|
371
401
|
if (meta?.approvalPromise) {
|
|
372
402
|
const approved = await meta.approvalPromise;
|
|
373
403
|
if (!approved) {
|
|
374
|
-
|
|
404
|
+
const rejectedResult = {
|
|
375
405
|
ok: false,
|
|
376
406
|
error: 'Tool execution rejected by user',
|
|
377
|
-
|
|
407
|
+
details: { reason: 'user_rejected' },
|
|
408
|
+
};
|
|
409
|
+
await persistToolErrorResult(rejectedResult, {
|
|
410
|
+
callId: callIdFromQueue,
|
|
411
|
+
startTs: startTsFromQueue,
|
|
412
|
+
stepIndexForEvent,
|
|
413
|
+
args: meta?.args,
|
|
414
|
+
});
|
|
415
|
+
return rejectedResult as ToolExecuteReturn;
|
|
378
416
|
}
|
|
379
417
|
}
|
|
380
418
|
// Handle session-relative paths and cwd tools
|