deepseek-computer-use 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +302 -2
- package/cordis.patch.yml +28 -0
- package/lib/bridge.js +224 -0
- package/lib/build-helper.mjs +106 -0
- package/lib/build-native.mjs +52 -0
- package/lib/build.mjs +22 -0
- package/lib/config.js +95 -0
- package/lib/index.js +156 -0
- package/lib/safety.js +247 -0
- package/lib/tools.js +534 -0
- package/native/Driver.cs +1803 -0
- package/native/Json.cs +274 -0
- package/native/Overlay.cs +669 -0
- package/native/Program.cs +158 -0
- package/native/audio/Audio.csproj +17 -0
- package/native/audio/Program.cs +95 -0
- package/native/wgcfw/Capture.csproj +23 -0
- package/native/wgcfw/Program.cs +249 -0
- package/package.json +66 -4
- package/skills/computer-use/SKILL.md +90 -0
package/lib/tools.js
ADDED
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Computer Use tool surface.
|
|
3
|
+
*
|
|
4
|
+
* Names and semantics follow Codex's window2 API (list_apps / list_windows /
|
|
5
|
+
* get_window / launch_app / get_window_state / click / press_key / type_text /
|
|
6
|
+
* scroll / set_value / drag / perform_secondary_action / activate_window) so a
|
|
7
|
+
* model trained on either harness moves between them without relearning, plus
|
|
8
|
+
* computer_use_health for DSH-side diagnostics.
|
|
9
|
+
*
|
|
10
|
+
* @module deepseek-computer-use/tools
|
|
11
|
+
*/
|
|
12
|
+
import {
|
|
13
|
+
assertAppAllowed,
|
|
14
|
+
assertKeyAllowed,
|
|
15
|
+
assertLaunchAllowed,
|
|
16
|
+
assertNotHardDenied,
|
|
17
|
+
assertTurnOpen,
|
|
18
|
+
isRedacted,
|
|
19
|
+
turnTokenOf,
|
|
20
|
+
labelOfElement,
|
|
21
|
+
needsApproval,
|
|
22
|
+
requestApproval,
|
|
23
|
+
} from './safety.js';
|
|
24
|
+
|
|
25
|
+
const WINDOW_SCHEMA = {
|
|
26
|
+
type: 'object',
|
|
27
|
+
required: true,
|
|
28
|
+
description: 'Target window object taken from list_windows() / list_apps() / get_window_state(). Pass the same object back; only its numeric id is used for addressing.',
|
|
29
|
+
properties: {
|
|
30
|
+
id: { type: 'number', required: true, description: 'Opaque window id (HWND) from list_windows() or list_apps().' },
|
|
31
|
+
app: { type: 'string', required: true, description: 'App identifier carried forward from the same window object (required by the window2 contract).' },
|
|
32
|
+
title: { type: 'string', description: 'Window title from the same window object; informational only.' },
|
|
33
|
+
},
|
|
34
|
+
additionalProperties: true,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const ELEMENT_INDEX_SCHEMA = {
|
|
38
|
+
type: 'number',
|
|
39
|
+
description: 'Element index from the latest get_window_state(include_text=true) call for the SAME window. Indexes expire after the snapshot TTL.',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const OUTPUT_SCHEMA = { type: 'object', additionalProperties: true };
|
|
43
|
+
|
|
44
|
+
function json(value) {
|
|
45
|
+
return { type: 'text', text: JSON.stringify(value, null, 2) };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function describeWindow(window) {
|
|
49
|
+
const title = window?.title ? '"' + window.title + '"' : '(untitled)';
|
|
50
|
+
const app = window?.app?.name ?? window?.app?.id ?? 'unknown';
|
|
51
|
+
return app + ' | ' + title + ' | id=' + window?.id;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function buildTools(config, deps) {
|
|
55
|
+
const { request, ctx, log, states } = deps;
|
|
56
|
+
const lastActionAt = new Map();
|
|
57
|
+
const consentedApps = new Set();
|
|
58
|
+
/** Codex treats computer audio as its own app ("computer-audio") and asks once. */
|
|
59
|
+
let audioApproved = false;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Codex asks the user to allow Computer Use per application before it drives
|
|
63
|
+
* one ("Computer Use was not approved to use <app>"). Same idea here, once per
|
|
64
|
+
* app for the lifetime of the plugin, and it fails closed.
|
|
65
|
+
*/
|
|
66
|
+
async function ensureAppConsent(window, exec) {
|
|
67
|
+
if (config.appConsent === 'never') return;
|
|
68
|
+
const app = window?.app ?? {};
|
|
69
|
+
const key = String(app.id ?? app.name ?? window?.id ?? 'unknown');
|
|
70
|
+
if (consentedApps.has(key)) return;
|
|
71
|
+
await requestApproval(ctx, exec, {
|
|
72
|
+
toolName: toolName('get_window_state'),
|
|
73
|
+
reason: 'Computer Use is about to drive "' + (app.displayName ?? app.name ?? key) + '" (' + key + '). Approve this app for the session.',
|
|
74
|
+
});
|
|
75
|
+
consentedApps.add(key);
|
|
76
|
+
// Tell the helper, which enforces the same fact on its side (Codex does this
|
|
77
|
+
// through x-oai-cua-approved-app on every request).
|
|
78
|
+
try { await request('approve_app', { app: key }, callOpts(exec)); } catch { /* enforcement is best effort */ }
|
|
79
|
+
}
|
|
80
|
+
const markAction = window => { if (window?.id !== undefined) lastActionAt.set(Number(window.id), Date.now()); };
|
|
81
|
+
const toolName = base => config.toolPrefix + base;
|
|
82
|
+
// The request budget is chosen per method in index.js (10s / 15s, Codex parity).
|
|
83
|
+
const callOpts = exec => ({ signal: exec?.signal, turnToken: turnTokenOf(exec) });
|
|
84
|
+
|
|
85
|
+
/** Resolve + authorize a window before any action touches it. */
|
|
86
|
+
async function authorizeWindow(args, exec) {
|
|
87
|
+
const raw = args?.window ?? (Number.isFinite(Number(args?.windowId)) ? { id: Number(args.windowId) } : undefined);
|
|
88
|
+
const id = Number(raw?.id);
|
|
89
|
+
if (!Number.isFinite(id)) throw new Error('a window object with a numeric "id" is required; list windows first with list_windows');
|
|
90
|
+
const info = await request('get_window', { window: { id } }, callOpts(exec));
|
|
91
|
+
// Gate 1 runs first: some surfaces are never automatable, approved or not.
|
|
92
|
+
assertNotHardDenied(info?.window ?? {});
|
|
93
|
+
assertAppAllowed(config, info?.window?.app ?? {}, info?.window ?? {});
|
|
94
|
+
await ensureAppConsent(info.window, exec);
|
|
95
|
+
return info.window;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function gate(action, exec, { window, toolName: tool, label, reason }) {
|
|
99
|
+
assertTurnOpen(exec);
|
|
100
|
+
const state = window?.id !== undefined ? states.get(Number(window.id)) : undefined;
|
|
101
|
+
const targetLabel = label ?? labelOfElement(state, action?.element_index);
|
|
102
|
+
if (needsApproval(config, { toolName: tool, label: targetLabel })) {
|
|
103
|
+
await requestApproval(ctx, exec, {
|
|
104
|
+
toolName: tool,
|
|
105
|
+
reason: reason ?? ('Windows UI action in ' + describeWindow(window) + (targetLabel ? ' on "' + targetLabel.trim() + '"' : '')),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function action(name, description, parameters, run) {
|
|
111
|
+
return {
|
|
112
|
+
name: toolName(name),
|
|
113
|
+
description,
|
|
114
|
+
parameters,
|
|
115
|
+
output: { schema: OUTPUT_SCHEMA, render: (_args, value) => [json(value)] },
|
|
116
|
+
isConcurrencySafe: () => false,
|
|
117
|
+
execute: run,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const tools = [];
|
|
122
|
+
|
|
123
|
+
tools.push(action(
|
|
124
|
+
'list_windows',
|
|
125
|
+
'List the open, on-screen windows that Computer Use can target: id, title, owning app, z-order and bounds. Start most tasks with list_apps() instead when you do not know the app yet.',
|
|
126
|
+
{},
|
|
127
|
+
async (args, exec) => request('list_windows', {}, callOpts(exec)),
|
|
128
|
+
));
|
|
129
|
+
|
|
130
|
+
tools.push(action(
|
|
131
|
+
'list_apps',
|
|
132
|
+
'List installed apps that currently own a targetable window, each with its open windows. Prefer this over list_windows when the task names an application rather than a window.',
|
|
133
|
+
{},
|
|
134
|
+
async (args, exec) => request('list_apps', {}, callOpts(exec)),
|
|
135
|
+
));
|
|
136
|
+
|
|
137
|
+
tools.push(action(
|
|
138
|
+
'get_window',
|
|
139
|
+
'Re-read one open window by id. Use it after losing a window binding, or to refresh bounds before coordinate work.',
|
|
140
|
+
{ window: WINDOW_SCHEMA },
|
|
141
|
+
async (args, exec) => {
|
|
142
|
+
const window = await authorizeWindow(args, exec);
|
|
143
|
+
return { window };
|
|
144
|
+
},
|
|
145
|
+
));
|
|
146
|
+
|
|
147
|
+
tools.push({
|
|
148
|
+
...action(
|
|
149
|
+
'launch_app',
|
|
150
|
+
'Launch an app so its window becomes targetable. Accepts an app id from list_apps() or an explicit .exe path. Launching needs user approval under the default policy.',
|
|
151
|
+
{
|
|
152
|
+
app: { type: 'string', required: true, description: 'App id from list_apps(), a process name, or an explicit .exe path.' },
|
|
153
|
+
args: { type: 'array', items: { type: 'string' }, description: 'Optional command line arguments, e.g. the document to open.' },
|
|
154
|
+
},
|
|
155
|
+
async (args, exec) => {
|
|
156
|
+
assertLaunchAllowed(args.app);
|
|
157
|
+
if (config.dryRun) return { dryRun: true, wouldLaunch: args.app };
|
|
158
|
+
|
|
159
|
+
if (needsApproval(config, { toolName: 'launch_app' })) {
|
|
160
|
+
await requestApproval(ctx, exec, { toolName: toolName('launch_app'), reason: 'launch application "' + args.app + '"' + (Array.isArray(args.args) && args.args.length > 0 ? ' with arguments ' + args.args.join(' ') : '') });
|
|
161
|
+
}
|
|
162
|
+
return request('launch_app', stripUndefined({ app: args.app, args: Array.isArray(args.args) ? args.args.map(String) : undefined }), callOpts(exec));
|
|
163
|
+
},
|
|
164
|
+
),
|
|
165
|
+
output: { schema: OUTPUT_SCHEMA, render: (_args, value) => [json(value)] },
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
tools.push({
|
|
169
|
+
...action(
|
|
170
|
+
'get_window_state',
|
|
171
|
+
'Observe one window. include_text returns the accessibility tree (numbered elements with role, name, bounds and available actions); include_screenshot returns the window image (works even when the window is occluded). Element indexes from this call feed click/set_value/perform_secondary_action until the snapshot TTL expires.',
|
|
172
|
+
{
|
|
173
|
+
window: WINDOW_SCHEMA,
|
|
174
|
+
include_screenshot: { type: 'boolean', description: 'Capture the window image (plus related transient UI such as open menus). Defaults to true, matching the Codex window2 API.' },
|
|
175
|
+
include_text: { type: 'boolean', description: 'Return the numbered accessibility tree. Defaults to false; ask for it when you need element indexes.' },
|
|
176
|
+
maxElements: { type: 'number', description: 'Cap on numbered elements, default from plugin config.' },
|
|
177
|
+
},
|
|
178
|
+
async (args, exec) => {
|
|
179
|
+
const window = await authorizeWindow(args, exec);
|
|
180
|
+
const includeText = args.include_text === true;
|
|
181
|
+
const redacted = isRedacted(config, window.app);
|
|
182
|
+
const includeShot = redacted ? false : (args.include_screenshot === undefined ? config.defaultIncludeScreenshot : args.include_screenshot === true);
|
|
183
|
+
if (!includeText && !includeShot) throw new Error('get_window_state must request include_text, include_screenshot, or both');
|
|
184
|
+
// Codex waits for the UI to settle before capturing after an action; the
|
|
185
|
+
// harness-side equivalent is a short settle window here.
|
|
186
|
+
const last = lastActionAt.get(Number(window.id));
|
|
187
|
+
if (last !== undefined) {
|
|
188
|
+
const wait = Math.min(config.settleMs, config.settleMs - (Date.now() - last));
|
|
189
|
+
if (wait > 0) await new Promise(resolve => setTimeout(resolve, wait));
|
|
190
|
+
}
|
|
191
|
+
const result = await request('get_window_state', {
|
|
192
|
+
window: { id: window.id },
|
|
193
|
+
include_text: includeText,
|
|
194
|
+
include_screenshot: includeShot,
|
|
195
|
+
maxElements: Number.isFinite(Number(args.maxElements)) ? Number(args.maxElements) : config.maxElements,
|
|
196
|
+
ttlMs: config.snapshotTtlMs,
|
|
197
|
+
}, callOpts(exec));
|
|
198
|
+
if (result?.accessibility) {
|
|
199
|
+
states.set(Number(window.id), { ...result.accessibility, at: Date.now() });
|
|
200
|
+
}
|
|
201
|
+
if (redacted) {
|
|
202
|
+
// appshot guard: never let these pixels leave the machine
|
|
203
|
+
result.redacted = true;
|
|
204
|
+
result.redactedNote = 'screenshots are disabled for this application by the redactApps policy';
|
|
205
|
+
result.screenshots = [];
|
|
206
|
+
} else if (Array.isArray(result?.screenshots)) {
|
|
207
|
+
const attached = await attachScreenshots(ctx, result.screenshots, window);
|
|
208
|
+
result.screenshots = attached;
|
|
209
|
+
}
|
|
210
|
+
return result;
|
|
211
|
+
},
|
|
212
|
+
),
|
|
213
|
+
output: {
|
|
214
|
+
schema: OUTPUT_SCHEMA,
|
|
215
|
+
render: (_args, value) => {
|
|
216
|
+
const parts = [];
|
|
217
|
+
const shots = Array.isArray(value?.screenshots) ? value.screenshots : [];
|
|
218
|
+
const header = describeWindow(value?.window) + '\n' + (shots.length > 0 ? shots.length + ' screenshot(s): ' + shots.map(shot => shot.id + ' ' + shot.width + 'x' + shot.height + '@' + shot.originX + ',' + shot.originY).join(', ') : 'no screenshot');
|
|
219
|
+
parts.push({ type: 'text', text: header });
|
|
220
|
+
if (value?.accessibility?.tree) parts.push({ type: 'text', text: value.accessibility.tree });
|
|
221
|
+
else if (value?.accessibility?.document_text) parts.push({ type: 'text', text: value.accessibility.document_text.slice(0, 4000) });
|
|
222
|
+
if (value?.accessibility?.selected_text) parts.push({ type: 'text', text: 'selected: ' + value.accessibility.selected_text.slice(0, 500) });
|
|
223
|
+
for (const shot of shots) {
|
|
224
|
+
if (shot.attachment !== undefined) parts.push({ type: 'image', attachment: shot.attachment });
|
|
225
|
+
}
|
|
226
|
+
return parts;
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
tools.push({
|
|
232
|
+
...action(
|
|
233
|
+
'click',
|
|
234
|
+
'Click an element from the latest observation (element_index) or a window-relative pixel coordinate (x, y). A plain left click on an element that exposes Invoke is delivered semantically, which survives occlusion; every other click moves the real pointer.',
|
|
235
|
+
{
|
|
236
|
+
window: WINDOW_SCHEMA,
|
|
237
|
+
element_index: ELEMENT_INDEX_SCHEMA,
|
|
238
|
+
x: { type: 'number', description: 'Window-relative X in pixels. Required with y when element_index is absent.' },
|
|
239
|
+
y: { type: 'number', description: 'Window-relative Y in pixels. Required with x when element_index is absent.' },
|
|
240
|
+
click_count: { type: 'number', description: 'Number of clicks, default 1 (2 = double click, 3 = triple click).' },
|
|
241
|
+
mouse_button: { type: 'string', enum: ['left', 'right', 'middle'], description: 'Which button to press, default left.' },
|
|
242
|
+
screenshotId: { type: 'string', description: 'Screenshot id from the latest get_window_state; when supplied, x/y are interpreted in that screenshot region (needed for a menu or tooltip screenshot).' },
|
|
243
|
+
},
|
|
244
|
+
async (args, exec) => {
|
|
245
|
+
const window = await authorizeWindow(args, exec);
|
|
246
|
+
const params = {
|
|
247
|
+
window: { id: window.id },
|
|
248
|
+
element_index: args.element_index === undefined ? undefined : Number(args.element_index),
|
|
249
|
+
x: args.x === undefined ? undefined : Number(args.x),
|
|
250
|
+
y: args.y === undefined ? undefined : Number(args.y),
|
|
251
|
+
click_count: args.click_count === undefined ? 1 : Number(args.click_count),
|
|
252
|
+
mouse_button: args.mouse_button ?? 'left',
|
|
253
|
+
};
|
|
254
|
+
params.screenshotId = args.screenshotId;
|
|
255
|
+
await gate(params, exec, { window, toolName: 'click' });
|
|
256
|
+
if (config.dryRun) return { dryRun: true, wouldClick: params };
|
|
257
|
+
const clicked = await request('click', stripUndefined(params), callOpts(exec));
|
|
258
|
+
markAction(window);
|
|
259
|
+
return clicked;
|
|
260
|
+
},
|
|
261
|
+
),
|
|
262
|
+
output: { schema: OUTPUT_SCHEMA, render: (_args, value) => [json(value)] },
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
tools.push(action(
|
|
266
|
+
'press_key',
|
|
267
|
+
'Press one key or a chord in a window, using X keysym style names: a, space, Return, Tab, Control_L+a, Control_L+Shift_L+period, KP_0. Whitespace around + is ignored and Ctrl/Control, Alt, Shift, period/greater and Numpad_0 aliases are accepted.',
|
|
268
|
+
{
|
|
269
|
+
window: WINDOW_SCHEMA,
|
|
270
|
+
key: { type: 'string', required: true, description: 'Key name or + separated chord, e.g. Control_L+s or Return.' },
|
|
271
|
+
},
|
|
272
|
+
async (args, exec) => {
|
|
273
|
+
assertKeyAllowed(args.key);
|
|
274
|
+
const window = await authorizeWindow(args, exec);
|
|
275
|
+
await gate({}, exec, { window, toolName: 'press_key', label: 'key ' + args.key, reason: undefined });
|
|
276
|
+
if (config.dryRun) return { dryRun: true, wouldPress: args.key };
|
|
277
|
+
const pressed = await request('press_key', { window: { id: window.id }, key: args.key }, callOpts(exec));
|
|
278
|
+
markAction(window);
|
|
279
|
+
return pressed;
|
|
280
|
+
},
|
|
281
|
+
));
|
|
282
|
+
|
|
283
|
+
tools.push(action(
|
|
284
|
+
'type_text',
|
|
285
|
+
'Type text into the focused control of a window. Unicode is injected as keystrokes, so this is layout independent (CJK included). The target window is brought to the foreground first, and the action is refused if that cannot be proven.',
|
|
286
|
+
{
|
|
287
|
+
window: WINDOW_SCHEMA,
|
|
288
|
+
text: { type: 'string', required: true, description: 'Text to type. Newlines and tabs become Return and Tab.' },
|
|
289
|
+
},
|
|
290
|
+
async (args, exec) => {
|
|
291
|
+
const window = await authorizeWindow(args, exec);
|
|
292
|
+
await gate({}, exec, { window, toolName: 'type_text', label: 'text ' + String(args.text).slice(0, 40) });
|
|
293
|
+
if (config.dryRun) return { dryRun: true, wouldType: args.text };
|
|
294
|
+
const typed = await request('type_text', { window: { id: window.id }, text: String(args.text) }, callOpts(exec));
|
|
295
|
+
markAction(window);
|
|
296
|
+
return typed;
|
|
297
|
+
},
|
|
298
|
+
));
|
|
299
|
+
|
|
300
|
+
tools.push(action(
|
|
301
|
+
'scroll',
|
|
302
|
+
'Scroll inside a window at a coordinate or element. Negative scrollY scrolls up, positive down; scrollX is horizontal.',
|
|
303
|
+
{
|
|
304
|
+
window: WINDOW_SCHEMA,
|
|
305
|
+
x: { type: 'number', required: true, description: 'X of the point to scroll from, in window/screenshot coordinates. Click a pane first if it needs focus.' },
|
|
306
|
+
y: { type: 'number', required: true, description: 'Y of the point to scroll from, in window/screenshot coordinates.' },
|
|
307
|
+
scrollX: { type: 'number', description: 'Horizontal delta in wheel notches; negative left, positive right.' },
|
|
308
|
+
scrollY: { type: 'number', description: 'Vertical delta in wheel notches; negative up, positive down.' },
|
|
309
|
+
screenshotId: { type: 'string', description: 'Screenshot id from the latest get_window_state for coordinate interpretation.' },
|
|
310
|
+
},
|
|
311
|
+
async (args, exec) => {
|
|
312
|
+
const window = await authorizeWindow(args, exec);
|
|
313
|
+
const params = stripUndefined({
|
|
314
|
+
window: { id: window.id },
|
|
315
|
+
x: Number(args.x),
|
|
316
|
+
y: Number(args.y),
|
|
317
|
+
scrollX: Number(args.scrollX ?? 0),
|
|
318
|
+
scrollY: Number(args.scrollY ?? 0),
|
|
319
|
+
screenshotId: args.screenshotId,
|
|
320
|
+
});
|
|
321
|
+
await gate(params, exec, { window, toolName: 'scroll' });
|
|
322
|
+
if (config.dryRun) return { dryRun: true, wouldScroll: params };
|
|
323
|
+
const scrolled = await request('scroll', params, callOpts(exec));
|
|
324
|
+
markAction(window);
|
|
325
|
+
return scrolled;
|
|
326
|
+
},
|
|
327
|
+
));
|
|
328
|
+
|
|
329
|
+
tools.push(action(
|
|
330
|
+
'drag',
|
|
331
|
+
'Drag inside a window from one window-relative coordinate to another (selection, sliders, drag and drop).',
|
|
332
|
+
{
|
|
333
|
+
window: WINDOW_SCHEMA,
|
|
334
|
+
from_x: { type: 'number', required: true, description: 'Start X, window-relative pixels.' },
|
|
335
|
+
from_y: { type: 'number', required: true, description: 'Start Y, window-relative pixels.' },
|
|
336
|
+
to_x: { type: 'number', required: true, description: 'End X, window-relative pixels.' },
|
|
337
|
+
to_y: { type: 'number', required: true, description: 'End Y, window-relative pixels.' },
|
|
338
|
+
screenshotId: { type: 'string', description: 'Screenshot id from the latest get_window_state for coordinate interpretation.' },
|
|
339
|
+
},
|
|
340
|
+
async (args, exec) => {
|
|
341
|
+
const window = await authorizeWindow(args, exec);
|
|
342
|
+
const params = {
|
|
343
|
+
window: { id: window.id },
|
|
344
|
+
from_x: Number(args.from_x), from_y: Number(args.from_y),
|
|
345
|
+
to_x: Number(args.to_x), to_y: Number(args.to_y),
|
|
346
|
+
screenshotId: args.screenshotId,
|
|
347
|
+
};
|
|
348
|
+
await gate(params, exec, { window, toolName: 'drag' });
|
|
349
|
+
if (config.dryRun) return { dryRun: true, wouldDrag: params };
|
|
350
|
+
const dragged = await request('drag', params, callOpts(exec));
|
|
351
|
+
markAction(window);
|
|
352
|
+
return dragged;
|
|
353
|
+
},
|
|
354
|
+
));
|
|
355
|
+
|
|
356
|
+
tools.push(action(
|
|
357
|
+
'set_value',
|
|
358
|
+
'Replace the value of a value-bearing element (text box, combo, search field) directly through the accessibility Value pattern. Cheaper and more reliable than click + select-all + type_text when the element supports it.',
|
|
359
|
+
{
|
|
360
|
+
window: WINDOW_SCHEMA,
|
|
361
|
+
element_index: { ...ELEMENT_INDEX_SCHEMA, required: true },
|
|
362
|
+
value: { type: 'string', required: true, description: 'Replacement value for the element.' },
|
|
363
|
+
},
|
|
364
|
+
async (args, exec) => {
|
|
365
|
+
const window = await authorizeWindow(args, exec);
|
|
366
|
+
const params = { window: { id: window.id }, element_index: Number(args.element_index), value: String(args.value) };
|
|
367
|
+
await gate(params, exec, { window, toolName: 'set_value' });
|
|
368
|
+
if (config.dryRun) return { dryRun: true, wouldSetValue: params };
|
|
369
|
+
const setValue = await request('set_value', params, callOpts(exec));
|
|
370
|
+
markAction(window);
|
|
371
|
+
return setValue;
|
|
372
|
+
},
|
|
373
|
+
));
|
|
374
|
+
|
|
375
|
+
tools.push(action(
|
|
376
|
+
'perform_secondary_action',
|
|
377
|
+
'Invoke a non-click accessibility action on an element: Raise, Invoke, Expand, Collapse, Toggle, Select, ScrollUp/ScrollDown/ScrollLeft/ScrollRight. Matching is case-insensitive.',
|
|
378
|
+
{
|
|
379
|
+
window: WINDOW_SCHEMA,
|
|
380
|
+
element_index: { ...ELEMENT_INDEX_SCHEMA, required: true },
|
|
381
|
+
action: { type: 'string', required: true, description: 'Action label, e.g. Expand or Scroll Down.' },
|
|
382
|
+
},
|
|
383
|
+
async (args, exec) => {
|
|
384
|
+
const window = await authorizeWindow(args, exec);
|
|
385
|
+
const params = { window: { id: window.id }, element_index: Number(args.element_index), action: String(args.action) };
|
|
386
|
+
await gate(params, exec, { window, toolName: 'perform_secondary_action' });
|
|
387
|
+
if (config.dryRun) return { dryRun: true, wouldPerform: params };
|
|
388
|
+
const performed = await request('perform_secondary_action', params, callOpts(exec));
|
|
389
|
+
markAction(window);
|
|
390
|
+
return performed;
|
|
391
|
+
},
|
|
392
|
+
));
|
|
393
|
+
|
|
394
|
+
tools.push(action(
|
|
395
|
+
'activate_window',
|
|
396
|
+
'Bring an open window to the foreground. Input tools activate their target automatically; use this only to focus a window for the user or before a manual step.',
|
|
397
|
+
{ window: WINDOW_SCHEMA },
|
|
398
|
+
async (args, exec) => {
|
|
399
|
+
const window = await authorizeWindow(args, exec);
|
|
400
|
+
if (config.dryRun) return { dryRun: true, wouldActivate: window.id };
|
|
401
|
+
return request('activate_window', { window: { id: window.id } }, callOpts(exec));
|
|
402
|
+
},
|
|
403
|
+
));
|
|
404
|
+
|
|
405
|
+
tools.push(action(
|
|
406
|
+
'start_audio_recording',
|
|
407
|
+
'Start recording the machine\'s audio output (WASAPI loopback) - useful when the task involves something that is said or played. Recording is capped by max_duration_ms and must be stopped with stop_audio_recording; the task must actually need the audio.',
|
|
408
|
+
{ max_duration_ms: { type: 'number', description: 'Hard cap in milliseconds (default 60000).' } },
|
|
409
|
+
async (args, exec) => {
|
|
410
|
+
if (!config.audio) throw new Error('audio recording is disabled for this plugin: set audio: true in the plugin config (or DSH_CU_AUDIO=1) and restart the host');
|
|
411
|
+
// Codex elicits "Allow Computer Use to record computer audio?" before the first
|
|
412
|
+
// recording of a session, separate from the per-app consent it asks for windows.
|
|
413
|
+
if (!audioApproved) {
|
|
414
|
+
await requestApproval(ctx, exec, {
|
|
415
|
+
toolName: toolName('start_audio_recording'),
|
|
416
|
+
reason: 'Allow Computer Use to record computer audio?',
|
|
417
|
+
});
|
|
418
|
+
audioApproved = true;
|
|
419
|
+
}
|
|
420
|
+
return request('start_audio_recording', {
|
|
421
|
+
maxDurationMs: Number(args.max_duration_ms ?? 60000),
|
|
422
|
+
approvedApp: 'computer-audio',
|
|
423
|
+
force: true,
|
|
424
|
+
}, callOpts(exec));
|
|
425
|
+
},
|
|
426
|
+
));
|
|
427
|
+
|
|
428
|
+
tools.push(action(
|
|
429
|
+
'stop_audio_recording',
|
|
430
|
+
'Stop the audio recording started by start_audio_recording and return the WAV file path plus the raw 24 kHz stereo WAV bytes (base64) and data URL.',
|
|
431
|
+
{},
|
|
432
|
+
async (args, exec) => request('stop_audio_recording', {}, callOpts(exec)),
|
|
433
|
+
));
|
|
434
|
+
|
|
435
|
+
tools.push(action(
|
|
436
|
+
'computer_use_health',
|
|
437
|
+
'Self-check for the Computer Use plugin: helper path and build state, Windows version, UI Automation availability, resolved policy (approval mode, app lists, dry-run) and live window count. Run this first when a Computer Use call fails unexpectedly.',
|
|
438
|
+
{},
|
|
439
|
+
async (args, exec) => {
|
|
440
|
+
const ping = await request('ping', {}, callOpts(exec));
|
|
441
|
+
let windows = null;
|
|
442
|
+
try {
|
|
443
|
+
const listed = await request('list_windows', {}, callOpts(exec));
|
|
444
|
+
windows = listed?.count ?? (listed?.windows?.length ?? null);
|
|
445
|
+
} catch (error) {
|
|
446
|
+
windows = 'unavailable: ' + error.message;
|
|
447
|
+
}
|
|
448
|
+
return {
|
|
449
|
+
ok: true,
|
|
450
|
+
platform: ping?.platform ?? 'unknown',
|
|
451
|
+
helper: deps.describeHelper?.() ?? null,
|
|
452
|
+
windows,
|
|
453
|
+
policy: {
|
|
454
|
+
approval: config.approval,
|
|
455
|
+
allowApps: config.allowApps,
|
|
456
|
+
denyApps: config.denyApps,
|
|
457
|
+
dryRun: config.dryRun,
|
|
458
|
+
defaultIncludeScreenshot: config.defaultIncludeScreenshot,
|
|
459
|
+
maxElements: config.maxElements,
|
|
460
|
+
snapshotTtlMs: config.snapshotTtlMs,
|
|
461
|
+
settleMs: config.settleMs,
|
|
462
|
+
appConsent: config.appConsent,
|
|
463
|
+
audio: config.audio,
|
|
464
|
+
toolPrefix: config.toolPrefix,
|
|
465
|
+
hardDeniedApps: 'terminals, Run dialog, password managers, security software, auth dialogs, agent host',
|
|
466
|
+
hardDeniedKeys: 'Windows/Meta/Super key family',
|
|
467
|
+
},
|
|
468
|
+
};
|
|
469
|
+
},
|
|
470
|
+
));
|
|
471
|
+
|
|
472
|
+
return tools;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function stripUndefined(value) {
|
|
476
|
+
const out = {};
|
|
477
|
+
for (const [key, item] of Object.entries(value)) {
|
|
478
|
+
if (item !== undefined) out[key] = item;
|
|
479
|
+
}
|
|
480
|
+
return out;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Persist captured PNGs through the DSH attachments service so the model gets
|
|
485
|
+
* image blocks instead of base64 blobs. A window state can carry several
|
|
486
|
+
* screenshots (the window plus open menus/tooltips), each with its own id and
|
|
487
|
+
* screen origin so coordinates can be interpreted later.
|
|
488
|
+
*/
|
|
489
|
+
async function attachScreenshots(ctx, screenshots, window) {
|
|
490
|
+
const attachments = ctx?.get?.('attachments');
|
|
491
|
+
const usable = attachments !== undefined && attachments !== null && typeof attachments.saveImage === 'function';
|
|
492
|
+
const out = [];
|
|
493
|
+
for (const shot of screenshots) {
|
|
494
|
+
const base = {
|
|
495
|
+
id: shot.id,
|
|
496
|
+
zIndex: shot.zIndex,
|
|
497
|
+
originX: shot.originX,
|
|
498
|
+
originY: shot.originY,
|
|
499
|
+
width: shot.width,
|
|
500
|
+
height: shot.height,
|
|
501
|
+
};
|
|
502
|
+
if (typeof shot.base64 !== 'string') {
|
|
503
|
+
out.push(base);
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
if (!usable) {
|
|
507
|
+
out.push({ ...base, note: 'no attachment service is mounted, so the image bytes were dropped' });
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
const data = Buffer.from(shot.base64, 'base64');
|
|
511
|
+
const ref = await attachments.saveImage({
|
|
512
|
+
data,
|
|
513
|
+
mediaType: 'image/png',
|
|
514
|
+
name: 'computer-use-' + (window?.id ?? 'window') + '-' + shot.id + '.png',
|
|
515
|
+
});
|
|
516
|
+
out.push({
|
|
517
|
+
...base,
|
|
518
|
+
attachmentId: ref.attachmentId,
|
|
519
|
+
mediaType: ref.mediaType,
|
|
520
|
+
bytes: ref.bytes,
|
|
521
|
+
width: ref.width ?? base.width,
|
|
522
|
+
height: ref.height ?? base.height,
|
|
523
|
+
attachment: {
|
|
524
|
+
attachmentId: ref.attachmentId,
|
|
525
|
+
mediaType: ref.mediaType,
|
|
526
|
+
bytes: ref.bytes,
|
|
527
|
+
width: ref.width,
|
|
528
|
+
height: ref.height,
|
|
529
|
+
name: ref.name ?? 'computer-use.png',
|
|
530
|
+
},
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
return out;
|
|
534
|
+
}
|