gclm-code 1.0.0 → 1.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 +1 -1
- package/bin/gc.js +53 -25
- package/bin/install-runtime.js +253 -0
- package/package.json +10 -5
- package/vendor/manifest.json +92 -0
- package/vendor/modules/node_modules/@ant/claude-for-chrome-mcp/package.json +9 -0
- package/vendor/modules/node_modules/@ant/claude-for-chrome-mcp/src/bridgeClient.ts +1126 -0
- package/vendor/modules/node_modules/@ant/claude-for-chrome-mcp/src/browserTools.ts +546 -0
- package/vendor/modules/node_modules/@ant/claude-for-chrome-mcp/src/index.ts +15 -0
- package/vendor/modules/node_modules/@ant/claude-for-chrome-mcp/src/mcpServer.ts +96 -0
- package/vendor/modules/node_modules/@ant/claude-for-chrome-mcp/src/mcpSocketClient.ts +493 -0
- package/vendor/modules/node_modules/@ant/claude-for-chrome-mcp/src/mcpSocketPool.ts +327 -0
- package/vendor/modules/node_modules/@ant/claude-for-chrome-mcp/src/toolCalls.ts +301 -0
- package/vendor/modules/node_modules/@ant/claude-for-chrome-mcp/src/types.ts +134 -0
- package/vendor/modules/node_modules/@ant/computer-use-input/package.json +9 -0
- package/vendor/modules/node_modules/@ant/computer-use-input/src/driver-jxa.js +341 -0
- package/vendor/modules/node_modules/@ant/computer-use-input/src/driver-swift.swift +417 -0
- package/vendor/modules/node_modules/@ant/computer-use-input/src/implementation.js +204 -0
- package/vendor/modules/node_modules/@ant/computer-use-input/src/index.js +5 -0
- package/vendor/modules/node_modules/@ant/computer-use-mcp/package.json +11 -0
- package/vendor/modules/node_modules/@ant/computer-use-mcp/src/deniedApps.ts +553 -0
- package/vendor/modules/node_modules/@ant/computer-use-mcp/src/imageResize.ts +108 -0
- package/vendor/modules/node_modules/@ant/computer-use-mcp/src/index.ts +69 -0
- package/vendor/modules/node_modules/@ant/computer-use-mcp/src/keyBlocklist.ts +153 -0
- package/vendor/modules/node_modules/@ant/computer-use-mcp/src/mcpServer.ts +313 -0
- package/vendor/modules/node_modules/@ant/computer-use-mcp/src/pixelCompare.ts +171 -0
- package/vendor/modules/node_modules/@ant/computer-use-mcp/src/sentinelApps.ts +43 -0
- package/vendor/modules/node_modules/@ant/computer-use-mcp/src/subGates.ts +19 -0
- package/vendor/modules/node_modules/@ant/computer-use-mcp/src/toolCalls.ts +3872 -0
- package/vendor/modules/node_modules/@ant/computer-use-mcp/src/tools.ts +706 -0
- package/vendor/modules/node_modules/@ant/computer-use-mcp/src/types.ts +635 -0
- package/vendor/modules/node_modules/@ant/computer-use-swift/package.json +9 -0
- package/vendor/modules/node_modules/@ant/computer-use-swift/src/driver-jxa.js +108 -0
- package/vendor/modules/node_modules/@ant/computer-use-swift/src/implementation.js +706 -0
- package/vendor/modules/node_modules/@ant/computer-use-swift/src/index.js +7 -0
- package/vendor/modules/node_modules/audio-capture-napi/package.json +8 -0
- package/vendor/modules/node_modules/audio-capture-napi/src/index.ts +226 -0
- package/vendor/modules/node_modules/image-processor-napi/package.json +11 -0
- package/vendor/modules/node_modules/image-processor-napi/src/index.ts +396 -0
- package/vendor/modules/node_modules/modifiers-napi/package.json +8 -0
- package/vendor/modules/node_modules/modifiers-napi/src/index.ts +79 -0
- package/vendor/modules/node_modules/url-handler-napi/package.json +8 -0
- package/vendor/modules/node_modules/url-handler-napi/src/index.ts +62 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
ComputerExecutor,
|
|
3
|
+
DisplayGeometry,
|
|
4
|
+
FrontmostApp,
|
|
5
|
+
InstalledApp,
|
|
6
|
+
ResolvePrepareCaptureResult,
|
|
7
|
+
RunningApp,
|
|
8
|
+
ScreenshotResult,
|
|
9
|
+
} from "./executor.js";
|
|
10
|
+
|
|
11
|
+
export type {
|
|
12
|
+
AppGrant,
|
|
13
|
+
CuAppPermTier,
|
|
14
|
+
ComputerUseHostAdapter,
|
|
15
|
+
ComputerUseOverrides,
|
|
16
|
+
ComputerUseSessionContext,
|
|
17
|
+
CoordinateMode,
|
|
18
|
+
CuGrantFlags,
|
|
19
|
+
CuPermissionRequest,
|
|
20
|
+
CuPermissionResponse,
|
|
21
|
+
CuSubGates,
|
|
22
|
+
CuTeachPermissionRequest,
|
|
23
|
+
Logger,
|
|
24
|
+
ResolvedAppRequest,
|
|
25
|
+
ScreenshotDims,
|
|
26
|
+
TeachStepRequest,
|
|
27
|
+
TeachStepResult,
|
|
28
|
+
} from "./types.js";
|
|
29
|
+
|
|
30
|
+
export { DEFAULT_GRANT_FLAGS } from "./types.js";
|
|
31
|
+
|
|
32
|
+
export {
|
|
33
|
+
SENTINEL_BUNDLE_IDS,
|
|
34
|
+
getSentinelCategory,
|
|
35
|
+
} from "./sentinelApps.js";
|
|
36
|
+
export type { SentinelCategory } from "./sentinelApps.js";
|
|
37
|
+
|
|
38
|
+
export {
|
|
39
|
+
categoryToTier,
|
|
40
|
+
getDefaultTierForApp,
|
|
41
|
+
getDeniedCategory,
|
|
42
|
+
getDeniedCategoryByDisplayName,
|
|
43
|
+
getDeniedCategoryForApp,
|
|
44
|
+
isPolicyDenied,
|
|
45
|
+
} from "./deniedApps.js";
|
|
46
|
+
export type { DeniedCategory } from "./deniedApps.js";
|
|
47
|
+
|
|
48
|
+
export { isSystemKeyCombo, normalizeKeySequence } from "./keyBlocklist.js";
|
|
49
|
+
|
|
50
|
+
export { ALL_SUB_GATES_OFF, ALL_SUB_GATES_ON } from "./subGates.js";
|
|
51
|
+
|
|
52
|
+
export { API_RESIZE_PARAMS, targetImageSize } from "./imageResize.js";
|
|
53
|
+
export type { ResizeParams } from "./imageResize.js";
|
|
54
|
+
|
|
55
|
+
export { defersLockAcquire, handleToolCall } from "./toolCalls.js";
|
|
56
|
+
export type {
|
|
57
|
+
CuCallTelemetry,
|
|
58
|
+
CuCallToolResult,
|
|
59
|
+
CuErrorKind,
|
|
60
|
+
} from "./toolCalls.js";
|
|
61
|
+
|
|
62
|
+
export { bindSessionContext, createComputerUseMcpServer } from "./mcpServer.js";
|
|
63
|
+
export { buildComputerUseTools } from "./tools.js";
|
|
64
|
+
|
|
65
|
+
export {
|
|
66
|
+
comparePixelAtLocation,
|
|
67
|
+
validateClickTarget,
|
|
68
|
+
} from "./pixelCompare.js";
|
|
69
|
+
export type { CropRawPatchFn, PixelCompareResult } from "./pixelCompare.js";
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Key combos that cross app boundaries or terminate processes. Gated behind
|
|
3
|
+
* the `systemKeyCombos` grant flag. When that flag is off, the `key` tool
|
|
4
|
+
* rejects these and returns a tool error telling the model to request the
|
|
5
|
+
* flag; all other combos work normally.
|
|
6
|
+
*
|
|
7
|
+
* Matching is canonicalized: every modifier alias the Rust executor accepts
|
|
8
|
+
* collapses to one canonical name. Without this, `command+q` / `meta+q` /
|
|
9
|
+
* `cmd+alt+escape` bypass the gate — see keyBlocklist.test.ts for the three
|
|
10
|
+
* bypass forms and the Rust parity check that catches future alias drift.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Every modifier alias enigo_wrap.rs accepts (two copies: :351-359, :564-572),
|
|
15
|
+
* mapped to one canonical per Key:: variant. Left/right variants collapse —
|
|
16
|
+
* the blocklist doesn't distinguish which Ctrl.
|
|
17
|
+
*
|
|
18
|
+
* Canonical names are Rust's own variant names lowercased. Blocklist entries
|
|
19
|
+
* below use ONLY these. "meta" reads odd for Cmd+Q but it's honest: Rust
|
|
20
|
+
* sends Key::Meta, which is Cmd on darwin and Win on win32.
|
|
21
|
+
*/
|
|
22
|
+
const CANONICAL_MODIFIER: Readonly<Record<string, string>> = {
|
|
23
|
+
// Key::Meta — "meta"|"super"|"command"|"cmd"|"windows"|"win"
|
|
24
|
+
meta: "meta",
|
|
25
|
+
super: "meta",
|
|
26
|
+
command: "meta",
|
|
27
|
+
cmd: "meta",
|
|
28
|
+
windows: "meta",
|
|
29
|
+
win: "meta",
|
|
30
|
+
// Key::Control + LControl + RControl
|
|
31
|
+
ctrl: "ctrl",
|
|
32
|
+
control: "ctrl",
|
|
33
|
+
lctrl: "ctrl",
|
|
34
|
+
lcontrol: "ctrl",
|
|
35
|
+
rctrl: "ctrl",
|
|
36
|
+
rcontrol: "ctrl",
|
|
37
|
+
// Key::Shift + LShift + RShift
|
|
38
|
+
shift: "shift",
|
|
39
|
+
lshift: "shift",
|
|
40
|
+
rshift: "shift",
|
|
41
|
+
// Key::Alt and Key::Option — distinct Rust variants but same keycode on
|
|
42
|
+
// darwin (kVK_Option). Collapse: cmd+alt+escape and cmd+option+escape
|
|
43
|
+
// both Force Quit.
|
|
44
|
+
alt: "alt",
|
|
45
|
+
option: "alt",
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/** Sort order for canonicals. ctrl < alt < shift < meta. */
|
|
49
|
+
const MODIFIER_ORDER = ["ctrl", "alt", "shift", "meta"];
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Canonical-form entries only. Every modifier must be a CANONICAL_MODIFIER
|
|
53
|
+
* *value* (not key), modifiers must be in MODIFIER_ORDER, non-modifier last.
|
|
54
|
+
* The self-consistency test enforces this.
|
|
55
|
+
*/
|
|
56
|
+
const BLOCKED_DARWIN = new Set([
|
|
57
|
+
"meta+q", // Cmd+Q — quit frontmost app
|
|
58
|
+
"shift+meta+q", // Cmd+Shift+Q — log out
|
|
59
|
+
"alt+meta+escape", // Cmd+Option+Esc — Force Quit dialog
|
|
60
|
+
"meta+tab", // Cmd+Tab — app switcher
|
|
61
|
+
"meta+space", // Cmd+Space — Spotlight
|
|
62
|
+
"ctrl+meta+q", // Ctrl+Cmd+Q — lock screen
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
const BLOCKED_WIN32 = new Set([
|
|
66
|
+
"ctrl+alt+delete", // Secure Attention Sequence
|
|
67
|
+
"alt+f4", // close window
|
|
68
|
+
"alt+tab", // window switcher
|
|
69
|
+
"meta+l", // Win+L — lock
|
|
70
|
+
"meta+d", // Win+D — show desktop
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Partition into sorted-canonical modifiers and non-modifier keys.
|
|
75
|
+
* Shared by normalizeKeySequence (join for display) and isSystemKeyCombo
|
|
76
|
+
* (check mods+each-key to catch the cmd+q+a suffix bypass).
|
|
77
|
+
*/
|
|
78
|
+
function partitionKeys(seq: string): { mods: string[]; keys: string[] } {
|
|
79
|
+
const parts = seq
|
|
80
|
+
.toLowerCase()
|
|
81
|
+
.split("+")
|
|
82
|
+
.map((p) => p.trim())
|
|
83
|
+
.filter(Boolean);
|
|
84
|
+
const mods: string[] = [];
|
|
85
|
+
const keys: string[] = [];
|
|
86
|
+
for (const p of parts) {
|
|
87
|
+
const canonical = CANONICAL_MODIFIER[p];
|
|
88
|
+
if (canonical !== undefined) {
|
|
89
|
+
mods.push(canonical);
|
|
90
|
+
} else {
|
|
91
|
+
keys.push(p);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Dedupe: "cmd+command+q" → "meta+q", not "meta+meta+q".
|
|
95
|
+
const uniqueMods = [...new Set(mods)];
|
|
96
|
+
uniqueMods.sort(
|
|
97
|
+
(a, b) => MODIFIER_ORDER.indexOf(a) - MODIFIER_ORDER.indexOf(b),
|
|
98
|
+
);
|
|
99
|
+
return { mods: uniqueMods, keys };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Normalize "Cmd + Shift + Q" → "shift+meta+q": lowercase, trim, alias →
|
|
104
|
+
* canonical, dedupe, sort modifiers, non-modifiers last.
|
|
105
|
+
*/
|
|
106
|
+
export function normalizeKeySequence(seq: string): string {
|
|
107
|
+
const { mods, keys } = partitionKeys(seq);
|
|
108
|
+
return [...mods, ...keys].join("+");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* True if the sequence would fire a blocked OS shortcut.
|
|
113
|
+
*
|
|
114
|
+
* Checks mods + EACH non-modifier key individually, not just the full
|
|
115
|
+
* joined string. `cmd+q+a` → Rust presses Cmd, then Q (Cmd+Q fires here),
|
|
116
|
+
* then A. Exact-match against "meta+q+a" misses; checking "meta+q" and
|
|
117
|
+
* "meta+a" separately catches the Q.
|
|
118
|
+
*
|
|
119
|
+
* Modifiers-only sequences ("cmd+shift") are checked as-is — no key to
|
|
120
|
+
* pair with, and no blocklist entry is modifier-only, so this is a no-op
|
|
121
|
+
* that falls through to false. Covers the click-modifier case where
|
|
122
|
+
* `left_click(text="cmd")` is legitimate.
|
|
123
|
+
*/
|
|
124
|
+
export function isSystemKeyCombo(
|
|
125
|
+
seq: string,
|
|
126
|
+
platform: "darwin" | "win32",
|
|
127
|
+
): boolean {
|
|
128
|
+
const blocklist = platform === "darwin" ? BLOCKED_DARWIN : BLOCKED_WIN32;
|
|
129
|
+
const { mods, keys } = partitionKeys(seq);
|
|
130
|
+
const prefix = mods.length > 0 ? mods.join("+") + "+" : "";
|
|
131
|
+
|
|
132
|
+
// No non-modifier keys (e.g. "cmd+shift" as click-modifiers) — check the
|
|
133
|
+
// whole thing. Never matches (no blocklist entry is modifier-only) but
|
|
134
|
+
// keeps the contract simple: every call reaches a .has().
|
|
135
|
+
if (keys.length === 0) {
|
|
136
|
+
return blocklist.has(mods.join("+"));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// mods + each key. Any hit blocks the whole sequence.
|
|
140
|
+
for (const key of keys) {
|
|
141
|
+
if (blocklist.has(prefix + key)) {
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export const _test = {
|
|
149
|
+
CANONICAL_MODIFIER,
|
|
150
|
+
BLOCKED_DARWIN,
|
|
151
|
+
BLOCKED_WIN32,
|
|
152
|
+
MODIFIER_ORDER,
|
|
153
|
+
};
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP server factory + session-context binder.
|
|
3
|
+
*
|
|
4
|
+
* Two entry points:
|
|
5
|
+
*
|
|
6
|
+
* `bindSessionContext` — the wrapper closure. Takes a `ComputerUseSessionContext`
|
|
7
|
+
* (getters + callbacks backed by host session state), returns a dispatcher.
|
|
8
|
+
* Reusable by both the MCP CallTool handler here AND Cowork's
|
|
9
|
+
* `InternalServerDefinition.handleToolCall` (which doesn't go through MCP).
|
|
10
|
+
* This replaces the duplicated wrapper closures in apps/desktop/…/serverDef.ts
|
|
11
|
+
* and the Gclm Code CLI's CU host wrapper — both did the same thing: build `ComputerUseOverrides`
|
|
12
|
+
* fresh from getters, call `handleToolCall`, stash screenshot, merge permissions.
|
|
13
|
+
*
|
|
14
|
+
* `createComputerUseMcpServer` — the Server object. When `context` is provided,
|
|
15
|
+
* the CallTool handler is real (uses `bindSessionContext`). When not, it's the
|
|
16
|
+
* legacy stub that returns a not-wired error. The tool-schema ListTools handler
|
|
17
|
+
* is the same either way.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
21
|
+
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
22
|
+
import {
|
|
23
|
+
CallToolRequestSchema,
|
|
24
|
+
ListToolsRequestSchema,
|
|
25
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
26
|
+
|
|
27
|
+
import type { ScreenshotResult } from "./executor.js";
|
|
28
|
+
import type { CuCallToolResult } from "./toolCalls.js";
|
|
29
|
+
import {
|
|
30
|
+
defersLockAcquire,
|
|
31
|
+
handleToolCall,
|
|
32
|
+
resetMouseButtonHeld,
|
|
33
|
+
} from "./toolCalls.js";
|
|
34
|
+
import { buildComputerUseTools } from "./tools.js";
|
|
35
|
+
import type {
|
|
36
|
+
AppGrant,
|
|
37
|
+
ComputerUseHostAdapter,
|
|
38
|
+
ComputerUseOverrides,
|
|
39
|
+
ComputerUseSessionContext,
|
|
40
|
+
CoordinateMode,
|
|
41
|
+
CuGrantFlags,
|
|
42
|
+
CuPermissionResponse,
|
|
43
|
+
} from "./types.js";
|
|
44
|
+
import { DEFAULT_GRANT_FLAGS } from "./types.js";
|
|
45
|
+
|
|
46
|
+
const DEFAULT_LOCK_HELD_MESSAGE =
|
|
47
|
+
"Another Gclm Code session is currently using the computer. Wait for that " +
|
|
48
|
+
"session to finish, or find a non-computer-use approach.";
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Dedupe `granted` into `existing` on bundleId, spread truthy-only flags over
|
|
52
|
+
* defaults+existing. Truthy-only: a subsequent `request_access` that doesn't
|
|
53
|
+
* request clipboard can't revoke an earlier clipboard grant — revocation lives
|
|
54
|
+
* in a Settings page, not here.
|
|
55
|
+
*
|
|
56
|
+
* Same merge both hosts implemented independently today.
|
|
57
|
+
*/
|
|
58
|
+
function mergePermissionResponse(
|
|
59
|
+
existing: readonly AppGrant[],
|
|
60
|
+
existingFlags: CuGrantFlags,
|
|
61
|
+
response: CuPermissionResponse,
|
|
62
|
+
): { apps: AppGrant[]; flags: CuGrantFlags } {
|
|
63
|
+
const seen = new Set(existing.map((a) => a.bundleId));
|
|
64
|
+
const apps = [
|
|
65
|
+
...existing,
|
|
66
|
+
...response.granted.filter((g) => !seen.has(g.bundleId)),
|
|
67
|
+
];
|
|
68
|
+
const truthyFlags = Object.fromEntries(
|
|
69
|
+
Object.entries(response.flags).filter(([, v]) => v === true),
|
|
70
|
+
);
|
|
71
|
+
const flags: CuGrantFlags = {
|
|
72
|
+
...DEFAULT_GRANT_FLAGS,
|
|
73
|
+
...existingFlags,
|
|
74
|
+
...truthyFlags,
|
|
75
|
+
};
|
|
76
|
+
return { apps, flags };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Bind session state to a reusable dispatcher. The returned function is the
|
|
81
|
+
* wrapper closure: async lock gate → build overrides fresh → `handleToolCall`
|
|
82
|
+
* → stash screenshot → strip piggybacked fields.
|
|
83
|
+
*
|
|
84
|
+
* The last-screenshot blob is held in a closure cell here (not on `ctx`), so
|
|
85
|
+
* hosts don't need to guarantee `ctx` object identity across calls — they just
|
|
86
|
+
* need to hold onto the returned dispatcher. Cowork caches per
|
|
87
|
+
* `InternalServerContext` in a WeakMap; the CLI host constructs once at server creation.
|
|
88
|
+
*/
|
|
89
|
+
export function bindSessionContext(
|
|
90
|
+
adapter: ComputerUseHostAdapter,
|
|
91
|
+
coordinateMode: CoordinateMode,
|
|
92
|
+
ctx: ComputerUseSessionContext,
|
|
93
|
+
): (name: string, args: unknown) => Promise<CuCallToolResult> {
|
|
94
|
+
const { logger, serverName } = adapter;
|
|
95
|
+
|
|
96
|
+
// Screenshot blob persists here across calls — NOT on `ctx`. Hosts hold
|
|
97
|
+
// onto the returned dispatcher; that's the identity that matters.
|
|
98
|
+
let lastScreenshot: ScreenshotResult | undefined;
|
|
99
|
+
|
|
100
|
+
const wrapPermission = ctx.onPermissionRequest
|
|
101
|
+
? async (
|
|
102
|
+
req: Parameters<NonNullable<typeof ctx.onPermissionRequest>>[0],
|
|
103
|
+
signal: AbortSignal,
|
|
104
|
+
): Promise<CuPermissionResponse> => {
|
|
105
|
+
const response = await ctx.onPermissionRequest!(req, signal);
|
|
106
|
+
const { apps, flags } = mergePermissionResponse(
|
|
107
|
+
ctx.getAllowedApps(),
|
|
108
|
+
ctx.getGrantFlags(),
|
|
109
|
+
response,
|
|
110
|
+
);
|
|
111
|
+
logger.debug(
|
|
112
|
+
`[${serverName}] permission result: granted=${response.granted.length} denied=${response.denied.length}`,
|
|
113
|
+
);
|
|
114
|
+
ctx.onAllowedAppsChanged?.(apps, flags);
|
|
115
|
+
return response;
|
|
116
|
+
}
|
|
117
|
+
: undefined;
|
|
118
|
+
|
|
119
|
+
const wrapTeachPermission = ctx.onTeachPermissionRequest
|
|
120
|
+
? async (
|
|
121
|
+
req: Parameters<NonNullable<typeof ctx.onTeachPermissionRequest>>[0],
|
|
122
|
+
signal: AbortSignal,
|
|
123
|
+
): Promise<CuPermissionResponse> => {
|
|
124
|
+
const response = await ctx.onTeachPermissionRequest!(req, signal);
|
|
125
|
+
logger.debug(
|
|
126
|
+
`[${serverName}] teach permission result: granted=${response.granted.length} denied=${response.denied.length}`,
|
|
127
|
+
);
|
|
128
|
+
// Teach doesn't request grant flags — preserve existing.
|
|
129
|
+
const { apps } = mergePermissionResponse(
|
|
130
|
+
ctx.getAllowedApps(),
|
|
131
|
+
ctx.getGrantFlags(),
|
|
132
|
+
response,
|
|
133
|
+
);
|
|
134
|
+
ctx.onAllowedAppsChanged?.(apps, {
|
|
135
|
+
...DEFAULT_GRANT_FLAGS,
|
|
136
|
+
...ctx.getGrantFlags(),
|
|
137
|
+
});
|
|
138
|
+
return response;
|
|
139
|
+
}
|
|
140
|
+
: undefined;
|
|
141
|
+
|
|
142
|
+
return async (name, args) => {
|
|
143
|
+
// ─── Async lock gate ─────────────────────────────────────────────────
|
|
144
|
+
// Replaces the sync Gate-3 in `handleToolCall` — we pass
|
|
145
|
+
// `checkCuLock: undefined` below so it no-ops. Hosts with
|
|
146
|
+
// cross-process locks (O_EXCL file) await the real primitive here
|
|
147
|
+
// instead of pre-computing + feeding a fake sync result.
|
|
148
|
+
if (ctx.checkCuLock) {
|
|
149
|
+
const lock = await ctx.checkCuLock();
|
|
150
|
+
if (lock.holder !== undefined && !lock.isSelf) {
|
|
151
|
+
const text =
|
|
152
|
+
ctx.formatLockHeldMessage?.(lock.holder) ?? DEFAULT_LOCK_HELD_MESSAGE;
|
|
153
|
+
return {
|
|
154
|
+
content: [{ type: "text", text }],
|
|
155
|
+
isError: true,
|
|
156
|
+
telemetry: { error_kind: "cu_lock_held" },
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
if (lock.holder === undefined && !defersLockAcquire(name)) {
|
|
160
|
+
await ctx.acquireCuLock?.();
|
|
161
|
+
// Re-check: the awaits above yield the microtask queue, so another
|
|
162
|
+
// session's check+acquire can interleave with ours. Hosts where
|
|
163
|
+
// acquire is a no-op when already held (Cowork's CuLockManager) give
|
|
164
|
+
// no signal that we lost — verify we're now the holder before
|
|
165
|
+
// proceeding. The CLI's O_EXCL file lock would surface this as a throw from
|
|
166
|
+
// acquire instead; this re-check is a belt-and-suspenders for that
|
|
167
|
+
// path too.
|
|
168
|
+
const recheck = await ctx.checkCuLock();
|
|
169
|
+
if (recheck.holder !== undefined && !recheck.isSelf) {
|
|
170
|
+
const text =
|
|
171
|
+
ctx.formatLockHeldMessage?.(recheck.holder) ??
|
|
172
|
+
DEFAULT_LOCK_HELD_MESSAGE;
|
|
173
|
+
return {
|
|
174
|
+
content: [{ type: "text", text }],
|
|
175
|
+
isError: true,
|
|
176
|
+
telemetry: { error_kind: "cu_lock_held" },
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
// Fresh holder → any prior session's mouseButtonHeld is stale.
|
|
180
|
+
// Mirrors what Gate-3 does on the acquire branch. After the
|
|
181
|
+
// re-check so we only clear module state when we actually won.
|
|
182
|
+
resetMouseButtonHeld();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ─── Build overrides fresh ───────────────────────────────────────────
|
|
187
|
+
// Blob-first; dims-fallback with base64:"" when the closure cell is
|
|
188
|
+
// unset (cross-respawn). scaleCoord reads dims; pixelCompare sees "" →
|
|
189
|
+
// isEmpty → skip.
|
|
190
|
+
const dimsFallback = lastScreenshot
|
|
191
|
+
? undefined
|
|
192
|
+
: ctx.getLastScreenshotDims?.();
|
|
193
|
+
|
|
194
|
+
// Per-call AbortController for dialog dismissal. Aborted in `finally` —
|
|
195
|
+
// if handleToolCall finishes (MCP timeout, throw) before the user
|
|
196
|
+
// answers, the host's dialog handler sees the abort and tears down.
|
|
197
|
+
const dialogAbort = new AbortController();
|
|
198
|
+
|
|
199
|
+
const overrides: ComputerUseOverrides = {
|
|
200
|
+
allowedApps: [...ctx.getAllowedApps()],
|
|
201
|
+
grantFlags: ctx.getGrantFlags(),
|
|
202
|
+
userDeniedBundleIds: ctx.getUserDeniedBundleIds(),
|
|
203
|
+
coordinateMode,
|
|
204
|
+
selectedDisplayId: ctx.getSelectedDisplayId(),
|
|
205
|
+
displayPinnedByModel: ctx.getDisplayPinnedByModel?.(),
|
|
206
|
+
displayResolvedForApps: ctx.getDisplayResolvedForApps?.(),
|
|
207
|
+
lastScreenshot:
|
|
208
|
+
lastScreenshot ??
|
|
209
|
+
(dimsFallback ? { ...dimsFallback, base64: "" } : undefined),
|
|
210
|
+
onPermissionRequest: wrapPermission
|
|
211
|
+
? (req) => wrapPermission(req, dialogAbort.signal)
|
|
212
|
+
: undefined,
|
|
213
|
+
onTeachPermissionRequest: wrapTeachPermission
|
|
214
|
+
? (req) => wrapTeachPermission(req, dialogAbort.signal)
|
|
215
|
+
: undefined,
|
|
216
|
+
onAppsHidden: ctx.onAppsHidden,
|
|
217
|
+
getClipboardStash: ctx.getClipboardStash,
|
|
218
|
+
onClipboardStashChanged: ctx.onClipboardStashChanged,
|
|
219
|
+
onResolvedDisplayUpdated: ctx.onResolvedDisplayUpdated,
|
|
220
|
+
onDisplayPinned: ctx.onDisplayPinned,
|
|
221
|
+
onDisplayResolvedForApps: ctx.onDisplayResolvedForApps,
|
|
222
|
+
onTeachModeActivated: ctx.onTeachModeActivated,
|
|
223
|
+
onTeachStep: ctx.onTeachStep,
|
|
224
|
+
onTeachWorking: ctx.onTeachWorking,
|
|
225
|
+
getTeachModeActive: ctx.getTeachModeActive,
|
|
226
|
+
// Undefined → handleToolCall's sync Gate-3 no-ops. The async gate
|
|
227
|
+
// above already ran.
|
|
228
|
+
checkCuLock: undefined,
|
|
229
|
+
acquireCuLock: undefined,
|
|
230
|
+
isAborted: ctx.isAborted,
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
logger.debug(
|
|
234
|
+
`[${serverName}] tool=${name} allowedApps=${overrides.allowedApps.length} coordMode=${coordinateMode}`,
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
// ─── Dispatch ────────────────────────────────────────────────────────
|
|
238
|
+
try {
|
|
239
|
+
const result = await handleToolCall(adapter, name, args, overrides);
|
|
240
|
+
|
|
241
|
+
if (result.screenshot) {
|
|
242
|
+
lastScreenshot = result.screenshot;
|
|
243
|
+
const { base64: _blob, ...dims } = result.screenshot;
|
|
244
|
+
logger.debug(`[${serverName}] screenshot dims: ${JSON.stringify(dims)}`);
|
|
245
|
+
ctx.onScreenshotCaptured?.(dims);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return result;
|
|
249
|
+
} finally {
|
|
250
|
+
dialogAbort.abort();
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function createComputerUseMcpServer(
|
|
256
|
+
adapter: ComputerUseHostAdapter,
|
|
257
|
+
coordinateMode: CoordinateMode,
|
|
258
|
+
context?: ComputerUseSessionContext,
|
|
259
|
+
): Server {
|
|
260
|
+
const { serverName, logger } = adapter;
|
|
261
|
+
|
|
262
|
+
const server = new Server(
|
|
263
|
+
{ name: serverName, version: "0.1.3" },
|
|
264
|
+
{ capabilities: { tools: {}, logging: {} } },
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
const tools = buildComputerUseTools(
|
|
268
|
+
adapter.executor.capabilities,
|
|
269
|
+
coordinateMode,
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
server.setRequestHandler(ListToolsRequestSchema, async () =>
|
|
273
|
+
adapter.isDisabled() ? { tools: [] } : { tools },
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
if (context) {
|
|
277
|
+
const dispatch = bindSessionContext(adapter, coordinateMode, context);
|
|
278
|
+
server.setRequestHandler(
|
|
279
|
+
CallToolRequestSchema,
|
|
280
|
+
async (request): Promise<CallToolResult> => {
|
|
281
|
+
const { screenshot: _s, telemetry: _t, ...result } = await dispatch(
|
|
282
|
+
request.params.name,
|
|
283
|
+
request.params.arguments ?? {},
|
|
284
|
+
);
|
|
285
|
+
return result;
|
|
286
|
+
},
|
|
287
|
+
);
|
|
288
|
+
return server;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Legacy: no context → stub handler. Reached only if something calls the
|
|
292
|
+
// server over MCP transport WITHOUT going through a binder (a wiring
|
|
293
|
+
// regression). Clear error instead of silent failure.
|
|
294
|
+
server.setRequestHandler(
|
|
295
|
+
CallToolRequestSchema,
|
|
296
|
+
async (request): Promise<CallToolResult> => {
|
|
297
|
+
logger.warn(
|
|
298
|
+
`[${serverName}] tool call "${request.params.name}" reached the stub handler — no session context bound. Per-session state unavailable.`,
|
|
299
|
+
);
|
|
300
|
+
return {
|
|
301
|
+
content: [
|
|
302
|
+
{
|
|
303
|
+
type: "text",
|
|
304
|
+
text: "This computer-use server instance is not wired to a session. Per-session app permissions are not available on this code path.",
|
|
305
|
+
},
|
|
306
|
+
],
|
|
307
|
+
isError: true,
|
|
308
|
+
};
|
|
309
|
+
},
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
return server;
|
|
313
|
+
}
|