@kortix/agent-tunnel 0.1.4 → 0.12.7

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 (67) hide show
  1. package/README.md +65 -0
  2. package/dist/agent-cli.js +4676 -3174
  3. package/dist/client-cli.js +202 -493
  4. package/package.json +24 -29
  5. package/src/agent/agent.ts +67 -17
  6. package/src/agent/capabilities/desktop/cua-driver.ts +284 -0
  7. package/src/agent/capabilities/desktop.ts +100 -167
  8. package/src/agent/capabilities/enabled-registry.ts +24 -0
  9. package/src/agent/capabilities/filesystem.ts +136 -32
  10. package/src/agent/capabilities/index.ts +1 -1
  11. package/src/agent/capabilities/security.test.ts +204 -0
  12. package/src/agent/capabilities/shell.ts +37 -3
  13. package/src/agent/cli-device-auth.test.ts +179 -0
  14. package/src/agent/cli-help.test.ts +25 -0
  15. package/src/agent/cli.ts +475 -38
  16. package/src/agent/config.test.ts +53 -0
  17. package/src/agent/config.ts +169 -7
  18. package/src/agent/index.ts +1 -0
  19. package/src/agent/security/command-validator.ts +4 -2
  20. package/src/agent/security/path-validator.ts +73 -18
  21. package/src/agent/security/permission-guard.test.ts +52 -0
  22. package/src/agent/security/permission-guard.ts +35 -8
  23. package/src/agent/service.test.ts +63 -0
  24. package/src/agent/service.ts +410 -0
  25. package/src/client/cli.test.ts +150 -547
  26. package/src/client/cli.ts +116 -539
  27. package/src/client/index.ts +1 -1
  28. package/src/client/tools.ts +95 -356
  29. package/src/client/tunnel-client.ts +50 -80
  30. package/src/index.ts +7 -1
  31. package/src/node-ws-polyfill.test.ts +18 -0
  32. package/src/node-ws-polyfill.ts +5 -3
  33. package/src/server/heartbeat.ts +13 -6
  34. package/src/server/relay.test.ts +72 -0
  35. package/src/server/relay.ts +50 -9
  36. package/src/server/server.test.ts +33 -0
  37. package/src/server/server.ts +26 -6
  38. package/src/server/ws-handler.test.ts +158 -0
  39. package/src/server/ws-handler.ts +94 -36
  40. package/src/shared/crypto.ts +2 -3
  41. package/src/shared/index.ts +8 -0
  42. package/src/shared/permissions.ts +292 -0
  43. package/src/shared/types.ts +70 -41
  44. package/dist/agent/index.d.ts +0 -140
  45. package/dist/agent/index.js +0 -21
  46. package/dist/agent/index.js.map +0 -1
  47. package/dist/chunk-7N7GSU6K.js +0 -34
  48. package/dist/client/index.d.ts +0 -183
  49. package/dist/client/index.js +0 -8
  50. package/dist/client/index.js.map +0 -1
  51. package/dist/index.d.ts +0 -7
  52. package/dist/index.js +0 -55
  53. package/dist/index.js.map +0 -1
  54. package/dist/server/index.d.ts +0 -89
  55. package/dist/server/index.js +0 -14
  56. package/dist/server/index.js.map +0 -1
  57. package/dist/shared/index.d.ts +0 -10
  58. package/dist/shared/index.js +0 -20
  59. package/dist/shared/index.js.map +0 -1
  60. package/dist/types-Dpwrd8Ai.d.ts +0 -194
  61. package/src/agent/capabilities/desktop/atspi-helper.ts +0 -345
  62. package/src/agent/capabilities/desktop/csharp-helper.ts +0 -914
  63. package/src/agent/capabilities/desktop/linux-driver.ts +0 -368
  64. package/src/agent/capabilities/desktop/macos-driver.ts +0 -601
  65. package/src/agent/capabilities/desktop/swift-helper.ts +0 -736
  66. package/src/agent/capabilities/desktop/types.ts +0 -201
  67. package/src/agent/capabilities/desktop/windows-driver.ts +0 -220
@@ -1,193 +1,126 @@
1
- import { platform } from 'os';
2
1
  import type { Capability, RpcHandler } from './index';
3
- import type { DesktopDriver } from './desktop/types';
4
-
5
- function createDriver(): DesktopDriver {
6
- switch (platform()) {
7
- case 'darwin': {
8
- const { MacOSDriver } = require('./desktop/macos-driver');
9
- return new MacOSDriver();
10
- }
11
- case 'win32': {
12
- const { WindowsDriver } = require('./desktop/windows-driver');
13
- return new WindowsDriver();
14
- }
15
- default: {
16
- const { LinuxDriver } = require('./desktop/linux-driver');
17
- return new LinuxDriver();
18
- }
2
+ import { CuaDriver } from './desktop/cua-driver';
3
+ import { desktopFeatureForMethod } from '../../shared/permissions';
4
+ import type { LocalPermission } from '../security/permission-guard';
5
+
6
+ const CUA_TOOLS = [
7
+ 'bring_to_front',
8
+ 'check_for_update',
9
+ 'check_permissions',
10
+ 'click',
11
+ 'double_click',
12
+ 'drag',
13
+ 'end_session',
14
+ 'get_accessibility_tree',
15
+ 'get_agent_cursor_state',
16
+ 'get_config',
17
+ 'get_cursor_position',
18
+ 'get_recording_state',
19
+ 'get_screen_size',
20
+ 'get_window_state',
21
+ 'hotkey',
22
+ 'kill_app',
23
+ 'launch_app',
24
+ 'list_apps',
25
+ 'list_windows',
26
+ 'move_cursor',
27
+ 'page',
28
+ 'press_key',
29
+ 'replay_trajectory',
30
+ 'right_click',
31
+ 'scroll',
32
+ 'set_agent_cursor_enabled',
33
+ 'set_agent_cursor_motion',
34
+ 'set_agent_cursor_style',
35
+ 'set_config',
36
+ 'set_value',
37
+ 'start_recording',
38
+ 'install_ffmpeg',
39
+ 'start_session',
40
+ 'stop_recording',
41
+ 'type_text',
42
+ 'zoom',
43
+ ];
44
+
45
+ const LOCAL_ONLY_CUA_TOOLS = new Set(['check_for_update', 'install_ffmpeg']);
46
+
47
+ function assertRemotelyCallableTool(tool: unknown): asserts tool is string {
48
+ if (typeof tool !== 'string' || tool.length === 0) {
49
+ throw new Error('CUA tool name is required');
50
+ }
51
+ if (LOCAL_ONLY_CUA_TOOLS.has(tool)) {
52
+ throw new Error(`CUA tool "${tool}" is local-only and cannot run through Agent Tunnel`);
19
53
  }
20
54
  }
21
55
 
22
56
  export function createDesktopCapability(): Capability {
23
- const driver = createDriver();
57
+ const cua = new CuaDriver();
24
58
  const methods = new Map<string, RpcHandler>();
25
59
 
26
- methods.set('desktop.screenshot', async (params) => {
27
- return driver.screenshot({
28
- region: params.region as any,
29
- windowId: params.windowId as number | undefined,
30
- });
31
- });
32
-
33
- methods.set('desktop.mouse.click', async (params) => {
34
- await driver.mouseClick({
35
- x: params.x as number,
36
- y: params.y as number,
37
- button: params.button as any,
38
- clicks: params.clicks as number | undefined,
39
- modifiers: params.modifiers as string[] | undefined,
40
- });
41
- return { ok: true };
42
- });
43
-
44
- methods.set('desktop.mouse.move', async (params) => {
45
- await driver.mouseMove({
46
- x: params.x as number,
47
- y: params.y as number,
48
- });
49
- return { ok: true };
50
- });
51
-
52
- methods.set('desktop.mouse.drag', async (params) => {
53
- await driver.mouseDrag({
54
- fromX: params.fromX as number,
55
- fromY: params.fromY as number,
56
- toX: params.toX as number,
57
- toY: params.toY as number,
58
- button: params.button as any,
59
- });
60
- return { ok: true };
61
- });
62
-
63
- methods.set('desktop.mouse.scroll', async (params) => {
64
- await driver.mouseScroll({
65
- x: params.x as number,
66
- y: params.y as number,
67
- deltaX: params.deltaX as number | undefined,
68
- deltaY: params.deltaY as number | undefined,
69
- });
70
- return { ok: true };
71
- });
72
-
73
- methods.set('desktop.mouse.position', async () => {
74
- return driver.mousePosition();
75
- });
76
-
77
- methods.set('desktop.keyboard.type', async (params) => {
78
- await driver.keyboardType({
79
- text: params.text as string,
80
- delay: params.delay as number | undefined,
81
- });
82
- return { ok: true };
83
- });
84
-
85
- methods.set('desktop.keyboard.key', async (params) => {
86
- await driver.keyboardKey({
87
- keys: params.keys as string[],
88
- });
89
- return { ok: true };
90
- });
91
-
92
- methods.set('desktop.window.list', async () => {
93
- return { windows: await driver.windowList() };
94
- });
95
-
96
- methods.set('desktop.window.focus', async (params) => {
97
- await driver.windowFocus(params.windowId as number);
98
- return { ok: true };
99
- });
100
-
101
- methods.set('desktop.window.resize', async (params) => {
102
- await driver.windowResize(params.windowId as number, {
103
- x: params.x as number | undefined,
104
- y: params.y as number | undefined,
105
- width: params.width as number | undefined,
106
- height: params.height as number | undefined,
107
- });
108
- return { ok: true };
109
- });
110
-
111
- methods.set('desktop.window.close', async (params) => {
112
- await driver.windowClose(params.windowId as number);
113
- return { ok: true };
114
- });
115
-
116
- methods.set('desktop.window.minimize', async (params) => {
117
- await driver.windowMinimize(params.windowId as number);
118
- return { ok: true };
119
- });
120
-
121
- methods.set('desktop.app.launch', async (params) => {
122
- await driver.appLaunch(params.app as string);
123
- return { ok: true };
124
- });
125
-
126
- methods.set('desktop.app.quit', async (params) => {
127
- await driver.appQuit(params.app as string);
128
- return { ok: true };
129
- });
60
+ const assertDesktopPermission = (params: Record<string, unknown>, method: string): void => {
61
+ const permission = params.__permission as LocalPermission | undefined;
62
+ if (permission?.capability !== 'desktop') {
63
+ throw new Error('Permission denied: desktop permission required');
64
+ }
65
+ const features = Array.isArray(permission.scope?.features)
66
+ ? permission.scope.features.filter((value): value is string => typeof value === 'string')
67
+ : [];
68
+ if (features.length === 0) return;
69
+ const feature = desktopFeatureForMethod(method, params);
70
+ if (!feature || !features.includes(feature)) {
71
+ throw new Error(
72
+ `Permission denied: desktop feature "${feature ?? 'unknown'}" is not allowed`,
73
+ );
74
+ }
75
+ };
130
76
 
131
- methods.set('desktop.app.list', async () => {
132
- return { apps: await driver.appList() };
77
+ methods.set('desktop.cua.ensure', async (params) => {
78
+ assertDesktopPermission(params, 'desktop.cua.ensure');
79
+ const binary = await cua.ensureInstalled();
80
+ const version = await cua.version().catch(() => undefined);
81
+ return { ok: true, binary, version };
133
82
  });
134
83
 
135
- methods.set('desktop.clipboard.read', async () => {
136
- return { text: await driver.clipboardRead() };
84
+ methods.set('desktop.cua.start_daemon', async (params) => {
85
+ assertDesktopPermission(params, 'desktop.cua.start_daemon');
86
+ return cua.startDaemon();
137
87
  });
138
88
 
139
- methods.set('desktop.clipboard.write', async (params) => {
140
- await driver.clipboardWrite(params.text as string);
141
- return { ok: true };
89
+ methods.set('desktop.cua.status', async (params) => {
90
+ assertDesktopPermission(params, 'desktop.cua.status');
91
+ return { status: await cua.status() };
142
92
  });
143
93
 
144
- methods.set('desktop.screen.info', async () => {
145
- return driver.screenInfo();
94
+ methods.set('desktop.cua.version', async (params) => {
95
+ assertDesktopPermission(params, 'desktop.cua.version');
96
+ return { version: await cua.version() };
146
97
  });
147
98
 
148
- methods.set('desktop.cursor.image', async (params) => {
149
- return driver.cursorImage(params.radius as number | undefined);
99
+ methods.set('desktop.cua.list_tools', async (params) => {
100
+ assertDesktopPermission(params, 'desktop.cua.list_tools');
101
+ return { tools: await cua.listTools() };
150
102
  });
151
103
 
152
- methods.set('desktop.ax.tree', async (params) => {
153
- return driver.axTree({
154
- pid: params.pid as number | undefined,
155
- maxDepth: params.maxDepth as number | undefined,
156
- roles: params.roles as string[] | undefined,
157
- });
104
+ methods.set('desktop.cua.describe', async (params) => {
105
+ assertDesktopPermission(params, 'desktop.cua.describe');
106
+ return { description: await cua.describe(params.tool as string) };
158
107
  });
159
108
 
160
- methods.set('desktop.ax.action', async (params) => {
161
- return driver.axAction({
162
- elementId: params.elementId as string,
163
- action: params.action as string,
164
- pid: params.pid as number | undefined,
165
- });
109
+ methods.set('desktop.cua.call', async (params) => {
110
+ assertDesktopPermission(params, 'desktop.cua.call');
111
+ const tool = params.tool;
112
+ assertRemotelyCallableTool(tool);
113
+ const args = (params.args || {}) as Record<string, unknown>;
114
+ return cua.call(tool, args);
166
115
  });
167
116
 
168
- methods.set('desktop.ax.set_value', async (params) => {
169
- return driver.axSetValue({
170
- elementId: params.elementId as string,
171
- value: params.value as string,
172
- pid: params.pid as number | undefined,
117
+ for (const tool of CUA_TOOLS) {
118
+ if (LOCAL_ONLY_CUA_TOOLS.has(tool)) continue;
119
+ methods.set(`desktop.cua.${tool}`, async (params) => {
120
+ assertDesktopPermission(params, `desktop.cua.${tool}`);
121
+ return cua.call(tool, params);
173
122
  });
174
- });
175
-
176
- methods.set('desktop.ax.focus', async (params) => {
177
- return driver.axFocus({
178
- elementId: params.elementId as string,
179
- pid: params.pid as number | undefined,
180
- });
181
- });
182
-
183
- methods.set('desktop.ax.search', async (params) => {
184
- return driver.axSearch({
185
- query: params.query as string,
186
- role: params.role as string | undefined,
187
- pid: params.pid as number | undefined,
188
- maxResults: params.maxResults as number | undefined,
189
- });
190
- });
123
+ }
191
124
 
192
125
  return {
193
126
  name: 'desktop',
@@ -0,0 +1,24 @@
1
+ import type { TunnelConfig } from '../config';
2
+ import { CapabilityRegistry } from './index';
3
+ import { createDesktopCapability } from './desktop';
4
+ import { findCuaDriverBinary } from './desktop/cua-driver';
5
+ import { createFilesystemCapability } from './filesystem';
6
+ import { createShellCapability } from './shell';
7
+
8
+ /** Build the local RPC surface from the browser-approved capability ceiling. */
9
+ export function createEnabledCapabilityRegistry(
10
+ config: TunnelConfig,
11
+ findDesktopDriver: () => string | null = findCuaDriverBinary,
12
+ ): CapabilityRegistry {
13
+ const registry = new CapabilityRegistry();
14
+ const enabled = new Set(config.enabledCapabilities ?? ['filesystem', 'shell', 'desktop']);
15
+ if (enabled.has('filesystem')) registry.register(createFilesystemCapability(config));
16
+ if (enabled.has('shell')) registry.register(createShellCapability(config));
17
+ // Do not advertise Computer Use unless this process can execute the trusted
18
+ // local driver. The server must see the real handler surface, not the user's
19
+ // desired capability list.
20
+ if (enabled.has('desktop') && findDesktopDriver()) {
21
+ registry.register(createDesktopCapability());
22
+ }
23
+ return registry;
24
+ }
@@ -5,50 +5,154 @@
5
5
  * even though the server already validates permissions.
6
6
  */
7
7
 
8
- import { readFile, writeFile, readdir, stat, unlink, mkdir } from 'fs/promises';
8
+ import { open, writeFile, readdir, stat, unlink, mkdir } from 'fs/promises';
9
9
  import { join, dirname } from 'path';
10
10
  import type { Capability, RpcHandler } from './index';
11
- import { validatePath } from '../security/path-validator';
11
+ import { validatePath, validateWritePath } from '../security/path-validator';
12
12
  import type { TunnelConfig } from '../config';
13
+ import type { LocalPermission } from '../security/permission-guard';
14
+ import { operationForMethod } from '../../shared/permissions';
15
+
16
+ function permissionFilesystemScope(params: Record<string, unknown>): {
17
+ paths?: string[];
18
+ allowedPaths?: string[];
19
+ blockedPaths?: string[];
20
+ excludePatterns?: string[];
21
+ operations?: string[];
22
+ maxFileSize?: number;
23
+ } {
24
+ const permission = params.__permission as LocalPermission | undefined;
25
+ if (permission?.capability !== 'filesystem') {
26
+ throw new Error('Permission denied: filesystem permission required');
27
+ }
28
+ return (permission.scope ?? {}) as {
29
+ paths?: string[];
30
+ blockedPaths?: string[];
31
+ excludePatterns?: string[];
32
+ operations?: string[];
33
+ maxFileSize?: number;
34
+ };
35
+ }
36
+
37
+ function scopedAllowedPaths(params: Record<string, unknown>): string[] {
38
+ const scope = permissionFilesystemScope(params);
39
+ return Array.isArray(scope.paths)
40
+ ? scope.paths.filter((p: unknown): p is string => typeof p === 'string' && p.trim().length > 0)
41
+ : [];
42
+ }
43
+
44
+ function effectiveBlockedPaths(config: TunnelConfig, params: Record<string, unknown>): string[] {
45
+ const scope = permissionFilesystemScope(params);
46
+ const scoped = Array.isArray(scope.blockedPaths)
47
+ ? scope.blockedPaths.filter((p): p is string => typeof p === 'string' && p.trim().length > 0)
48
+ : [];
49
+ return [...config.blockedPaths, ...scoped];
50
+ }
51
+
52
+ function validateFilesystemPath(
53
+ path: string,
54
+ config: TunnelConfig,
55
+ params: Record<string, unknown>,
56
+ write = false,
57
+ ): string {
58
+ const blocked = effectiveBlockedPaths(config, params);
59
+ const validator = write ? validateWritePath : validatePath;
60
+ const resolved = validator(path, config.allowedPaths, blocked);
61
+ const scoped = scopedAllowedPaths(params);
62
+ if (scoped.length > 0) validator(path, scoped, blocked);
63
+
64
+ const scope = permissionFilesystemScope(params);
65
+ const patterns = Array.isArray(scope.excludePatterns)
66
+ ? scope.excludePatterns.filter((value): value is string => typeof value === 'string')
67
+ : [];
68
+ if (patterns.some((pattern) => matchGlob(path, pattern) || matchGlob(resolved, pattern))) {
69
+ throw new Error(`Permission denied: path "${path}" matches an excluded pattern`);
70
+ }
71
+ return resolved;
72
+ }
73
+
74
+ function matchGlob(path: string, pattern: string): boolean {
75
+ const regex = pattern
76
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
77
+ .replace(/\*\*/g, '{{GLOBSTAR}}')
78
+ .replace(/\*/g, '[^/\\\\]*')
79
+ .replace(/\{\{GLOBSTAR\}\}/g, '.*');
80
+ return new RegExp(`^${regex}$`).test(path);
81
+ }
82
+
83
+ function parseEncoding(value: unknown): 'utf8' | 'base64' {
84
+ if (value === undefined || value === 'utf-8' || value === 'utf8') return 'utf8';
85
+ if (value === 'base64') return 'base64';
86
+ throw new Error('Encoding must be "utf-8" or "base64"');
87
+ }
88
+
89
+ function assertFilesystemOperation(params: Record<string, unknown>, method: string): void {
90
+ const scope = permissionFilesystemScope(params);
91
+ const operations = Array.isArray(scope.operations)
92
+ ? scope.operations.filter((value): value is string => typeof value === 'string')
93
+ : [];
94
+ const operation = operationForMethod(method);
95
+ if (operations.length > 0 && !operations.includes(operation)) {
96
+ throw new Error(`Permission denied: filesystem operation "${operation}" is not allowed`);
97
+ }
98
+ }
13
99
 
14
100
  export function createFilesystemCapability(config: TunnelConfig): Capability {
15
101
  const methods = new Map<string, RpcHandler>();
16
102
 
17
103
  methods.set('fs.read', async (params) => {
104
+ assertFilesystemOperation(params, 'fs.read');
18
105
  const path = params.path as string;
19
- const encoding = (params.encoding as BufferEncoding) || 'utf-8';
20
-
21
- validatePath(path, config.allowedPaths, config.blockedPaths);
22
-
23
- const stats = await stat(path);
24
- if (stats.size > config.maxFileSize) {
25
- throw new Error(`File exceeds max size (${stats.size} > ${config.maxFileSize})`);
106
+ const encoding = parseEncoding(params.encoding);
107
+
108
+ validateFilesystemPath(path, config, params);
109
+
110
+ const handle = await open(path, 'r');
111
+ try {
112
+ const stats = await handle.stat();
113
+ const scope = permissionFilesystemScope(params);
114
+ const maxFileSize = Math.min(
115
+ config.maxFileSize,
116
+ typeof scope.maxFileSize === 'number' ? scope.maxFileSize : config.maxFileSize,
117
+ );
118
+ if (stats.size > maxFileSize) {
119
+ throw new Error(`File exceeds max size (${stats.size} > ${maxFileSize})`);
120
+ }
121
+ const content = await handle.readFile({ encoding });
122
+ return {
123
+ content,
124
+ size: stats.size,
125
+ encoding,
126
+ };
127
+ } finally {
128
+ await handle.close();
26
129
  }
27
-
28
- const content = await readFile(path, { encoding });
29
-
30
- return {
31
- content,
32
- size: stats.size,
33
- encoding,
34
- };
35
130
  });
36
131
 
37
-
38
132
  methods.set('fs.write', async (params) => {
133
+ assertFilesystemOperation(params, 'fs.write');
39
134
  const path = params.path as string;
40
135
  const content = params.content as string;
41
- const encoding = (params.encoding as BufferEncoding) || 'utf-8';
42
-
43
- validatePath(path, config.allowedPaths, config.blockedPaths);
44
-
45
- if (content.length > config.maxFileSize) {
46
- throw new Error(`Content exceeds max size (${content.length} > ${config.maxFileSize})`);
136
+ const encoding = parseEncoding(params.encoding);
137
+
138
+ validateFilesystemPath(path, config, params, true);
139
+
140
+ const scope = permissionFilesystemScope(params);
141
+ const maxFileSize = Math.min(
142
+ config.maxFileSize,
143
+ typeof scope.maxFileSize === 'number' ? scope.maxFileSize : config.maxFileSize,
144
+ );
145
+ if (typeof content !== 'string') throw new Error('Content must be a string');
146
+ const contentBytes = Buffer.byteLength(content, encoding);
147
+ if (contentBytes > maxFileSize) {
148
+ throw new Error(`Content exceeds max size (${contentBytes} > ${maxFileSize})`);
47
149
  }
48
150
 
49
151
  await mkdir(dirname(path), { recursive: true });
152
+ validateFilesystemPath(path, config, params, true);
50
153
 
51
154
  await writeFile(path, content, { encoding });
155
+ validateFilesystemPath(path, config, params);
52
156
  const stats = await stat(path);
53
157
 
54
158
  return {
@@ -57,12 +161,12 @@ export function createFilesystemCapability(config: TunnelConfig): Capability {
57
161
  };
58
162
  });
59
163
 
60
-
61
164
  methods.set('fs.list', async (params) => {
165
+ assertFilesystemOperation(params, 'fs.list');
62
166
  const path = params.path as string;
63
- const recursive = params.recursive as boolean || false;
167
+ const recursive = (params.recursive as boolean) || false;
64
168
 
65
- validatePath(path, config.allowedPaths, config.blockedPaths);
169
+ validateFilesystemPath(path, config, params);
66
170
 
67
171
  const entries = await readdir(path, { withFileTypes: true });
68
172
 
@@ -88,19 +192,18 @@ export function createFilesystemCapability(config: TunnelConfig): Capability {
88
192
  isSymlink: sub.isSymbolicLink(),
89
193
  });
90
194
  }
91
- } catch {
92
- }
195
+ } catch {}
93
196
  }
94
197
  }
95
198
 
96
199
  return { entries: result, count: result.length };
97
200
  });
98
201
 
99
-
100
202
  methods.set('fs.stat', async (params) => {
203
+ assertFilesystemOperation(params, 'fs.stat');
101
204
  const path = params.path as string;
102
205
 
103
- validatePath(path, config.allowedPaths, config.blockedPaths);
206
+ validateFilesystemPath(path, config, params);
104
207
 
105
208
  const stats = await stat(path);
106
209
 
@@ -117,9 +220,10 @@ export function createFilesystemCapability(config: TunnelConfig): Capability {
117
220
  });
118
221
 
119
222
  methods.set('fs.delete', async (params) => {
223
+ assertFilesystemOperation(params, 'fs.delete');
120
224
  const path = params.path as string;
121
225
 
122
- validatePath(path, config.allowedPaths, config.blockedPaths);
226
+ validateFilesystemPath(path, config, params);
123
227
 
124
228
  await unlink(path);
125
229
 
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Capability Registry — extensible registry for tunnel capabilities.
3
3
  *
4
- * Each capability (filesystem, shell, network, etc.) registers its
4
+ * Each capability (filesystem, shell, CUA-backed desktop, etc.) registers its
5
5
  * RPC method handlers here. The TunnelAgent dispatches incoming
6
6
  * JSON-RPC requests to the matching handler.
7
7
  */