@kortix/agent-tunnel 0.13.53 → 0.13.55
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 +74 -14
- package/dist/agent-cli.js +464 -249
- package/package.json +1 -1
- package/src/agent/access.test.ts +89 -0
- package/src/agent/access.ts +146 -4
- package/src/agent/agent.test.ts +136 -6
- package/src/agent/agent.ts +143 -13
- package/src/agent/capabilities/desktop/cua-driver.test.ts +43 -1
- package/src/agent/capabilities/desktop/cua-driver.ts +44 -4
- package/src/agent/capabilities/desktop.ts +41 -68
- package/src/agent/capabilities/enabled-registry.ts +2 -1
- package/src/agent/capabilities/security.test.ts +54 -16
- package/src/agent/cli-help.test.ts +53 -2
- package/src/agent/cli.ts +127 -19
- package/src/agent/credential-probe.ts +4 -4
- package/src/agent/self-restart.test.ts +24 -0
- package/src/agent/self-restart.ts +47 -0
- package/src/agent/service-lifecycle.test.ts +46 -0
- package/src/agent/service.test.ts +27 -1
- package/src/agent/service.ts +22 -1
- package/src/agent/state-file.test.ts +11 -1
- package/src/agent/state-file.ts +13 -1
- package/src/index.ts +1 -1
- package/src/shared/index.ts +1 -1
- package/src/shared/permissions.ts +26 -58
- package/src/shared/types.ts +8 -36
package/src/agent/service.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'fs';
|
|
2
|
-
import { platform } from 'os';
|
|
2
|
+
import { homedir, platform } from 'os';
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
|
|
5
5
|
import {
|
|
@@ -128,6 +128,25 @@ export function runnerPartsFor(
|
|
|
128
128
|
return { command: runtime.execPath, args: [vendor(script, paths), 'run', '--service'], env };
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
/**
|
|
132
|
+
* The Kortix desktop app's own agent runtime, when the app is installed on
|
|
133
|
+
* this Mac. A CLI install runs on it, so the connection is the same agent
|
|
134
|
+
* build with the same bundled driver, and macOS grants Accessibility and
|
|
135
|
+
* Screen Recording to Kortix once for both, never to node or a terminal.
|
|
136
|
+
*/
|
|
137
|
+
export function installedAppRunner(
|
|
138
|
+
apps: readonly string[] = ['/Applications/Kortix.app', join(homedir(), 'Applications', 'Kortix.app')],
|
|
139
|
+
os: string = process.platform,
|
|
140
|
+
): { execPath: string; script: string } | null {
|
|
141
|
+
if (os !== 'darwin') return null;
|
|
142
|
+
for (const app of apps) {
|
|
143
|
+
const execPath = join(app, 'Contents', 'MacOS', 'Kortix');
|
|
144
|
+
const script = join(app, 'Contents', 'Resources', 'agent-tunnel', 'agent-cli.js');
|
|
145
|
+
if (existsSync(execPath) && existsSync(script)) return { execPath, script };
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
|
|
131
150
|
/** Where vendorRunner WOULD put the bundle, without copying anything. */
|
|
132
151
|
const plannedRunner = (script: string, paths: ServicePaths) =>
|
|
133
152
|
isEphemeralRunnerPath(script) ? paths.vendoredRunner : script;
|
|
@@ -135,6 +154,8 @@ const plannedRunner = (script: string, paths: ServicePaths) =>
|
|
|
135
154
|
function currentRunnerParts(
|
|
136
155
|
vendor: (script: string, paths: ServicePaths) => string = vendorRunner,
|
|
137
156
|
): RunnerParts {
|
|
157
|
+
const app = process.versions.electron ? null : installedAppRunner();
|
|
158
|
+
if (app) return runnerPartsFor(app.script, { execPath: app.execPath, electron: 'installed-app' }, getServicePaths(), vendor);
|
|
138
159
|
const script = process.argv[1];
|
|
139
160
|
if (script && existsSync(script)) return runnerPartsFor(script, undefined, getServicePaths(), vendor);
|
|
140
161
|
throw new Error(
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test';
|
|
2
|
-
import { mkdtempSync, readdirSync, rmSync, statSync } from 'node:fs';
|
|
2
|
+
import { mkdtempSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
|
|
@@ -29,4 +29,14 @@ describe('agent state file', () => {
|
|
|
29
29
|
rmSync(join(home, '..'), { recursive: true, force: true });
|
|
30
30
|
}
|
|
31
31
|
});
|
|
32
|
+
|
|
33
|
+
test('a writer that died without writing offline reads as offline', () => {
|
|
34
|
+
const home = mkdtempSync(join(tmpdir(), 'agent-tunnel-state-'));
|
|
35
|
+
try {
|
|
36
|
+
writeFileSync(stateFilePath(home), JSON.stringify({ tunnelId: 't', apiUrl: 'u', status: 'online', since: '', agentVersion: '0', pid: 2 ** 22 + 7 }));
|
|
37
|
+
expect(readAgentState(home)?.status).toBe('offline');
|
|
38
|
+
} finally {
|
|
39
|
+
rmSync(home, { recursive: true, force: true });
|
|
40
|
+
}
|
|
41
|
+
});
|
|
32
42
|
});
|
package/src/agent/state-file.ts
CHANGED
|
@@ -48,10 +48,22 @@ export function writeAgentState(
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
function isAlive(pid: unknown): boolean {
|
|
52
|
+
if (typeof pid !== 'number' || !Number.isSafeInteger(pid) || pid <= 0) return false;
|
|
53
|
+
try {
|
|
54
|
+
process.kill(pid, 0);
|
|
55
|
+
return true;
|
|
56
|
+
} catch (error) {
|
|
57
|
+
return (error as NodeJS.ErrnoException).code === 'EPERM';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The last status the agent wrote. A writer that died without writing `offline` reads as offline. */
|
|
51
62
|
export function readAgentState(home: string = agentTunnelHome()): AgentStateFile | null {
|
|
52
63
|
try {
|
|
53
64
|
const parsed = JSON.parse(readFileSync(stateFilePath(home), 'utf8')) as AgentStateFile;
|
|
54
|
-
|
|
65
|
+
if (!parsed || typeof parsed !== 'object' || typeof parsed.status !== 'string') return null;
|
|
66
|
+
return parsed.status !== 'offline' && !isAlive(parsed.pid) ? { ...parsed, status: 'offline' } : parsed;
|
|
55
67
|
} catch {
|
|
56
68
|
return null;
|
|
57
69
|
}
|
package/src/index.ts
CHANGED
package/src/shared/index.ts
CHANGED
|
@@ -19,54 +19,35 @@ export interface PermissionScopeValidationResult {
|
|
|
19
19
|
sanitized?: Record<string, unknown>;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
const
|
|
23
|
-
'desktop.cua.ensure': 'computer_use',
|
|
24
|
-
'desktop.cua.start_daemon': 'computer_use',
|
|
25
|
-
'desktop.cua.status': 'computer_use',
|
|
26
|
-
'desktop.cua.version': 'computer_use',
|
|
27
|
-
'desktop.cua.list_tools': 'computer_use',
|
|
28
|
-
'desktop.cua.describe': 'computer_use',
|
|
29
|
-
'desktop.cua.bring_to_front': 'windows',
|
|
30
|
-
'desktop.cua.check_for_update': 'computer_use',
|
|
31
|
-
'desktop.cua.check_permissions': 'computer_use',
|
|
32
|
-
'desktop.cua.click': 'mouse',
|
|
33
|
-
'desktop.cua.double_click': 'mouse',
|
|
34
|
-
'desktop.cua.drag': 'mouse',
|
|
35
|
-
'desktop.cua.end_session': 'computer_use',
|
|
36
|
-
'desktop.cua.get_accessibility_tree': 'accessibility',
|
|
37
|
-
'desktop.cua.get_agent_cursor_state': 'mouse',
|
|
38
|
-
'desktop.cua.get_config': 'computer_use',
|
|
39
|
-
'desktop.cua.get_cursor_position': 'mouse',
|
|
40
|
-
'desktop.cua.get_recording_state': 'computer_use',
|
|
41
|
-
'desktop.cua.get_screen_size': 'screenshot',
|
|
42
|
-
'desktop.cua.get_window_state': 'accessibility',
|
|
43
|
-
'desktop.cua.hotkey': 'keyboard',
|
|
44
|
-
'desktop.cua.kill_app': 'apps',
|
|
45
|
-
'desktop.cua.launch_app': 'apps',
|
|
46
|
-
'desktop.cua.list_apps': 'apps',
|
|
47
|
-
'desktop.cua.list_windows': 'windows',
|
|
48
|
-
'desktop.cua.move_cursor': 'mouse',
|
|
49
|
-
'desktop.cua.page': 'accessibility',
|
|
50
|
-
'desktop.cua.press_key': 'keyboard',
|
|
51
|
-
'desktop.cua.replay_trajectory': 'computer_use',
|
|
52
|
-
'desktop.cua.right_click': 'mouse',
|
|
53
|
-
'desktop.cua.scroll': 'keyboard',
|
|
54
|
-
'desktop.cua.set_agent_cursor_enabled': 'mouse',
|
|
55
|
-
'desktop.cua.set_agent_cursor_motion': 'mouse',
|
|
56
|
-
'desktop.cua.set_agent_cursor_style': 'mouse',
|
|
57
|
-
'desktop.cua.set_config': 'computer_use',
|
|
58
|
-
'desktop.cua.set_value': 'accessibility',
|
|
59
|
-
'desktop.cua.start_recording': 'screenshot',
|
|
60
|
-
'desktop.cua.install_ffmpeg': 'computer_use',
|
|
61
|
-
'desktop.cua.start_session': 'computer_use',
|
|
62
|
-
'desktop.cua.stop_recording': 'screenshot',
|
|
63
|
-
'desktop.cua.type_text': 'keyboard',
|
|
64
|
-
'desktop.cua.zoom': 'screenshot',
|
|
65
|
-
};
|
|
22
|
+
const CUA_PREFIX = 'desktop.cua.';
|
|
66
23
|
|
|
24
|
+
/** Every `desktop.cua.<tool>` is the desktop capability: the driver owns the tool list. */
|
|
67
25
|
export function capabilityForMethod(method: string): TunnelCapability | null {
|
|
68
26
|
const capability = (TunnelMethods as Readonly<Record<string, TunnelCapability | null>>)[method];
|
|
69
|
-
|
|
27
|
+
if (capability !== undefined) return capability;
|
|
28
|
+
return method.startsWith(CUA_PREFIX) && method.length > CUA_PREFIX.length ? 'desktop' : null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* `desktop.cua.<tool>` → `desktop.cua.call { tool, args }`. Every agent since
|
|
33
|
+
* 0.1.0 serves `desktop.cua.call` for any driver tool, so one wire method
|
|
34
|
+
* reaches every tool the installed driver has, with no per-tool list to keep
|
|
35
|
+
* in sync. Management methods (`ensure`, `status`, `describe`, …) pass through.
|
|
36
|
+
*/
|
|
37
|
+
export function normalizeDesktopCall(
|
|
38
|
+
method: string,
|
|
39
|
+
params: Record<string, unknown>,
|
|
40
|
+
): { method: string; params: Record<string, unknown> } {
|
|
41
|
+
if (!method.startsWith(CUA_PREFIX) || method in TunnelMethods) return { method, params };
|
|
42
|
+
const { permissionId, ...args } = params;
|
|
43
|
+
return {
|
|
44
|
+
method: 'desktop.cua.call',
|
|
45
|
+
params: {
|
|
46
|
+
tool: method.slice(CUA_PREFIX.length),
|
|
47
|
+
args,
|
|
48
|
+
...(permissionId === undefined ? {} : { permissionId }),
|
|
49
|
+
},
|
|
50
|
+
};
|
|
70
51
|
}
|
|
71
52
|
|
|
72
53
|
export function isTunnelCapability(value: string): value is TunnelCapability {
|
|
@@ -277,16 +258,3 @@ export function operationForMethod(method: string): string {
|
|
|
277
258
|
const separator = method.indexOf('.');
|
|
278
259
|
return separator === -1 ? method : method.slice(separator + 1);
|
|
279
260
|
}
|
|
280
|
-
|
|
281
|
-
export function desktopFeatureForMethod(
|
|
282
|
-
method: string,
|
|
283
|
-
args: Record<string, unknown> = {},
|
|
284
|
-
): string | undefined {
|
|
285
|
-
if (method === 'desktop.cua.call') {
|
|
286
|
-
const tool = args.tool;
|
|
287
|
-
if (typeof tool !== 'string' || tool.length === 0) return undefined;
|
|
288
|
-
const toolMethod = tool.startsWith('desktop.cua.') ? tool : `desktop.cua.${tool}`;
|
|
289
|
-
return DESKTOP_METHOD_FEATURES[toolMethod];
|
|
290
|
-
}
|
|
291
|
-
return DESKTOP_METHOD_FEATURES[method];
|
|
292
|
-
}
|
package/src/shared/types.ts
CHANGED
|
@@ -48,6 +48,10 @@ export const TunnelErrorCode = {
|
|
|
48
48
|
ACCESS_DENIED: -32011,
|
|
49
49
|
/** The owner turned computer access off on the machine. */
|
|
50
50
|
ACCESS_OFF: -32012,
|
|
51
|
+
/** macOS has not given the agent Accessibility or Screen Recording. */
|
|
52
|
+
DESKTOP_PERMISSION_MISSING: -32013,
|
|
53
|
+
/** A wider access asked for through Kortix that nobody confirmed on the machine. */
|
|
54
|
+
ACCESS_UNCONFIRMED: -32014,
|
|
51
55
|
} as const;
|
|
52
56
|
|
|
53
57
|
export type TunnelErrorCodeValue = (typeof TunnelErrorCode)[keyof typeof TunnelErrorCode];
|
|
@@ -68,47 +72,13 @@ export const TunnelMethods = {
|
|
|
68
72
|
'desktop.cua.list_tools': 'desktop',
|
|
69
73
|
'desktop.cua.describe': 'desktop',
|
|
70
74
|
'desktop.cua.call': 'desktop',
|
|
71
|
-
'desktop.cua.bring_to_front': 'desktop',
|
|
72
|
-
'desktop.cua.check_for_update': 'desktop',
|
|
73
|
-
'desktop.cua.check_permissions': 'desktop',
|
|
74
|
-
'desktop.cua.click': 'desktop',
|
|
75
|
-
'desktop.cua.double_click': 'desktop',
|
|
76
|
-
'desktop.cua.drag': 'desktop',
|
|
77
|
-
'desktop.cua.end_session': 'desktop',
|
|
78
|
-
'desktop.cua.get_accessibility_tree': 'desktop',
|
|
79
|
-
'desktop.cua.get_agent_cursor_state': 'desktop',
|
|
80
|
-
'desktop.cua.get_config': 'desktop',
|
|
81
|
-
'desktop.cua.get_cursor_position': 'desktop',
|
|
82
|
-
'desktop.cua.get_recording_state': 'desktop',
|
|
83
|
-
'desktop.cua.get_screen_size': 'desktop',
|
|
84
|
-
'desktop.cua.get_window_state': 'desktop',
|
|
85
|
-
'desktop.cua.hotkey': 'desktop',
|
|
86
|
-
'desktop.cua.kill_app': 'desktop',
|
|
87
|
-
'desktop.cua.launch_app': 'desktop',
|
|
88
|
-
'desktop.cua.list_apps': 'desktop',
|
|
89
|
-
'desktop.cua.list_windows': 'desktop',
|
|
90
|
-
'desktop.cua.move_cursor': 'desktop',
|
|
91
|
-
'desktop.cua.page': 'desktop',
|
|
92
|
-
'desktop.cua.press_key': 'desktop',
|
|
93
|
-
'desktop.cua.replay_trajectory': 'desktop',
|
|
94
|
-
'desktop.cua.right_click': 'desktop',
|
|
95
|
-
'desktop.cua.scroll': 'desktop',
|
|
96
|
-
'desktop.cua.set_agent_cursor_enabled': 'desktop',
|
|
97
|
-
'desktop.cua.set_agent_cursor_motion': 'desktop',
|
|
98
|
-
'desktop.cua.set_agent_cursor_style': 'desktop',
|
|
99
|
-
'desktop.cua.set_config': 'desktop',
|
|
100
|
-
'desktop.cua.set_value': 'desktop',
|
|
101
|
-
'desktop.cua.start_recording': 'desktop',
|
|
102
|
-
'desktop.cua.install_ffmpeg': 'desktop',
|
|
103
|
-
'desktop.cua.start_session': 'desktop',
|
|
104
|
-
'desktop.cua.stop_recording': 'desktop',
|
|
105
|
-
'desktop.cua.type_text': 'desktop',
|
|
106
|
-
'desktop.cua.zoom': 'desktop',
|
|
107
75
|
'tunnel.ping': null,
|
|
108
76
|
'tunnel.pong': null,
|
|
109
77
|
'tunnel.permission.revoked': null,
|
|
110
78
|
'tunnel.permissions.sync': null,
|
|
111
79
|
'tunnel.token.rotated': null,
|
|
80
|
+
/** The owner's access change, relayed by Kortix (owner-only route). Never a capability. */
|
|
81
|
+
'tunnel.access.set': null,
|
|
112
82
|
} as const;
|
|
113
83
|
|
|
114
84
|
export type TunnelMethod = keyof typeof TunnelMethods;
|
|
@@ -195,6 +165,8 @@ export interface TunnelAuthMessage {
|
|
|
195
165
|
* that enforces nothing connects with the same credential.
|
|
196
166
|
*/
|
|
197
167
|
reportsAccess?: boolean;
|
|
168
|
+
/** True from agents that serve `tunnel.access.set` (the owner's change, relayed). */
|
|
169
|
+
controlsAccess?: boolean;
|
|
198
170
|
}
|
|
199
171
|
|
|
200
172
|
/** Result returned by onAuthenticate hook on success. */
|