deepseek-computer-use 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +317 -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 +159 -0
- package/lib/safety.js +247 -0
- package/lib/tools.js +534 -0
- package/native/Driver.cs +1857 -0
- package/native/Json.cs +274 -0
- package/native/Overlay.cs +725 -0
- package/native/Program.cs +160 -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/safety.js
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safety layers for deepseek-computer-use.
|
|
3
|
+
*
|
|
4
|
+
* Three independent gates, in the order the action hits them:
|
|
5
|
+
* 1. hard deny - never automatable, not even with user approval
|
|
6
|
+
* (terminals, Run dialog, password managers, security
|
|
7
|
+
* software, auth dialogs, the agent's own app, Win key)
|
|
8
|
+
* 2. reachability - is this app inside allowApps and outside denyApps?
|
|
9
|
+
* 3. confirmation - should the user approve this specific action?
|
|
10
|
+
*
|
|
11
|
+
* Ported from Codex's computer-use guidance.md ("Non-negotiable Windows
|
|
12
|
+
* Automation Safety") and confirmations.md.
|
|
13
|
+
*
|
|
14
|
+
* @module deepseek-computer-use/safety
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export class PolicyError extends Error {
|
|
18
|
+
constructor(code, message) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = 'PolicyError';
|
|
21
|
+
this.code = code;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Processes that must never be driven by UI automation.
|
|
27
|
+
* Matched as lowercase substrings against the exe path, process name and title.
|
|
28
|
+
*/
|
|
29
|
+
export const HARD_DENY_APPS = [
|
|
30
|
+
// terminals and shells
|
|
31
|
+
'windowsterminal', 'windows terminal', 'wt.exe', 'cmd.exe', 'conhost', 'consolewindowclass',
|
|
32
|
+
'powershell.exe', 'pwsh.exe', 'powershell ise', 'mintty', 'git-bash', 'bash.exe', 'wsl.exe', 'ubuntu.exe',
|
|
33
|
+
// launch surfaces that can run arbitrary commands
|
|
34
|
+
'run dialog', '运行', 'shellexperiencehost',
|
|
35
|
+
// credential stores
|
|
36
|
+
'1password', 'keepass', 'bitwarden', 'lastpass', 'dashlane', 'keeper', 'enpass', 'nordpass', 'roboform',
|
|
37
|
+
// security and anti-malware
|
|
38
|
+
'securityhealth', 'windows security', 'windows 安全中心', 'msmpeng', 'defender', 'malwarebytes',
|
|
39
|
+
'avast', 'avg', 'kaspersky', 'bitdefender', 'eset', 'norton', 'mcafee', 'sophos', 'crowdstrike', 'sentinelone',
|
|
40
|
+
// authentication and elevation surfaces
|
|
41
|
+
'consent.exe', 'credentialuibroker', 'logonui', 'useraccountcontrol',
|
|
42
|
+
// the agent's own host and other assistants: automating them invites prompt loops
|
|
43
|
+
'deepseek harness', 'chatgpt', 'codex',
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
/** Window classes that are terminal-like regardless of the owning exe name. */
|
|
47
|
+
const HARD_DENY_CLASSES = [
|
|
48
|
+
'consolewindowclass', 'cascadia_hosting_window_class', 'mintty', '#32770',
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
/** Key tokens that are banned outright (Windows key family). */
|
|
52
|
+
const DENY_KEY_TOKENS = new Set([
|
|
53
|
+
'win', 'win_l', 'win_r', 'lwin', 'rwin', 'super', 'super_l', 'super_r', 'meta', 'meta_l', 'meta_r',
|
|
54
|
+
'cmd', 'command', 'os', 'os_l', 'os_r', 'windows', 'windows_l', 'windows_r', 'lwindows', 'rwindows',
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
function lower(value) {
|
|
58
|
+
return String(value ?? '').toLowerCase();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function matchesAny(haystack, needles) {
|
|
62
|
+
const value = lower(haystack);
|
|
63
|
+
return needles.some(needle => value.includes(needle.toLowerCase()));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Gate 1: refuse windows that must never be automated.
|
|
68
|
+
* Runs before allowApps/denyApps so a user cannot opt into it by accident.
|
|
69
|
+
*/
|
|
70
|
+
export function assertNotHardDenied(window, extraDeny = HARD_DENY_APPS) {
|
|
71
|
+
const app = window?.app ?? {};
|
|
72
|
+
const fields = {
|
|
73
|
+
appId: String(app.id ?? ''),
|
|
74
|
+
appName: String(app.name ?? ''),
|
|
75
|
+
displayName: String(app.displayName ?? ''),
|
|
76
|
+
title: String(window?.title ?? ''),
|
|
77
|
+
className: String(window?.className ?? ''),
|
|
78
|
+
};
|
|
79
|
+
const hit = Object.values(fields).find(value => matchesAny(value, extraDeny));
|
|
80
|
+
if (hit !== undefined) {
|
|
81
|
+
throw new PolicyError(
|
|
82
|
+
'hard_denied',
|
|
83
|
+
'Computer Use refuses to automate "' + (fields.title || fields.appName || fields.className) +
|
|
84
|
+
'": terminals, Run dialogs, credential stores, security software, authentication dialogs and the agent host are hard-denied and cannot be approved.',
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
const deniedClass = HARD_DENY_CLASSES.find(name => fields.className.toLowerCase().startsWith(name));
|
|
88
|
+
if (deniedClass !== undefined) {
|
|
89
|
+
throw new PolicyError(
|
|
90
|
+
'hard_denied',
|
|
91
|
+
'Computer Use refuses to automate window class "' + fields.className + '": console/dialog classes are hard-denied (Run dialog, terminals, generic system dialogs).',
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Gate 1 for keyboard: the Windows key family is banned, alone or in a chord.
|
|
98
|
+
* Windows+anything can open Run, Settings, lock the desktop or trigger shell jumps.
|
|
99
|
+
*/
|
|
100
|
+
export function assertKeyAllowed(key) {
|
|
101
|
+
const parts = String(key ?? '').split('+').map(part => lower(part).trim()).filter(part => part.length > 0);
|
|
102
|
+
const banned = parts.find(part => DENY_KEY_TOKENS.has(part));
|
|
103
|
+
if (banned !== undefined) {
|
|
104
|
+
throw new PolicyError(
|
|
105
|
+
'hard_denied',
|
|
106
|
+
'the Windows/Meta/Super key ("' + banned + '") is hard-denied: it opens Run, Settings and shell surfaces that Computer Use must never drive.',
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Gate 1 for launch targets. */
|
|
112
|
+
export function assertLaunchAllowed(app) {
|
|
113
|
+
const value = lower(app);
|
|
114
|
+
const banned = HARD_DENY_APPS.find(needle => value.includes(needle.toLowerCase()));
|
|
115
|
+
if (banned !== undefined) {
|
|
116
|
+
throw new PolicyError('hard_denied', 'Computer Use refuses to launch "' + app + '": that surface is hard-denied.');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Gate 2: allowApps / denyApps. */
|
|
121
|
+
export function assertAppAllowed(config, app, window) {
|
|
122
|
+
const id = String(app?.id ?? '');
|
|
123
|
+
const name = String(app?.name ?? app?.displayName ?? '');
|
|
124
|
+
const title = String(window?.title ?? '');
|
|
125
|
+
if (matchesAny(id, config.denyApps) || matchesAny(name, config.denyApps) || matchesAny(title, config.denyApps)) {
|
|
126
|
+
throw new PolicyError('app_denied', 'the Computer Use policy denies app "' + (name || id) + '" (denyApps)');
|
|
127
|
+
}
|
|
128
|
+
if (config.allowApps.length > 0 && !matchesAny(id, config.allowApps) && !matchesAny(name, config.allowApps)) {
|
|
129
|
+
throw new PolicyError('app_not_allowed', 'app "' + (name || id) + '" is outside allowApps; add it to the plugin config to permit this action');
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** The accessible label an action would hit, from the cached observation. */
|
|
134
|
+
export function labelOfElement(state, elementIndex) {
|
|
135
|
+
if (state === undefined || elementIndex === undefined || elementIndex === null) return '';
|
|
136
|
+
const elements = state.elements;
|
|
137
|
+
if (!Array.isArray(elements)) return '';
|
|
138
|
+
const hit = elements.find(element => Number(element.index) === Number(elementIndex));
|
|
139
|
+
if (hit === undefined) return '';
|
|
140
|
+
return String(hit.name ?? '') + ' ' + String(hit.role ?? '');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Gate 3: decide whether an action needs a human answer.
|
|
145
|
+
* 'sensitive' asks when the target label looks irreversible; launch_app always asks;
|
|
146
|
+
* 'always' asks for everything.
|
|
147
|
+
*/
|
|
148
|
+
export function needsApproval(config, { toolName, label = '' }) {
|
|
149
|
+
if (config.approval === 'never') return false;
|
|
150
|
+
if (config.approval === 'always') return true;
|
|
151
|
+
if (toolName === 'launch_app') return true;
|
|
152
|
+
if (label.length === 0) return false;
|
|
153
|
+
try {
|
|
154
|
+
return new RegExp(config.riskyPattern, 'i').test(label);
|
|
155
|
+
} catch {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Ask the DSH approval seam and fail closed.
|
|
162
|
+
* A missing answerer rejects the action; it never silently proceeds.
|
|
163
|
+
*/
|
|
164
|
+
export async function requestApproval(ctx, exec, { toolName, reason }) {
|
|
165
|
+
const approval = ctx?.get?.('approval');
|
|
166
|
+
// The user's session preset may disable prompting entirely ("approval: never",
|
|
167
|
+
// e.g. the full-access preset). Honour that instead of failing every action:
|
|
168
|
+
// the hard denies above still apply, and asking would only return "rejected".
|
|
169
|
+
try {
|
|
170
|
+
if (typeof approval?.effectivePolicy === 'function') {
|
|
171
|
+
const policy = approval.effectivePolicy(exec?.agent?.session);
|
|
172
|
+
if (policy === 'never') return;
|
|
173
|
+
}
|
|
174
|
+
} catch { /* policy lookup is best effort */ }
|
|
175
|
+
if (approval === undefined || approval === null || typeof approval.request !== 'function') {
|
|
176
|
+
throw new PolicyError(
|
|
177
|
+
'approval_unavailable',
|
|
178
|
+
'this action needs user confirmation but no approval service is mounted; switch the session approval policy or change approval mode in the plugin config',
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
const outcome = await approval.request({
|
|
182
|
+
agent: exec?.agent,
|
|
183
|
+
toolName,
|
|
184
|
+
callId: exec?.callId,
|
|
185
|
+
reason,
|
|
186
|
+
signal: exec?.signal,
|
|
187
|
+
});
|
|
188
|
+
if (outcome !== 'allowed-once') {
|
|
189
|
+
throw new PolicyError('approval_denied', 'the user did not approve this action (' + outcome + ')');
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Codex stops issuing input once a turn ends. The DSH session log is the only
|
|
195
|
+
* place that knows, so read it defensively: an unknown session shape never
|
|
196
|
+
* blocks an action, but a provably closed turn does.
|
|
197
|
+
*/
|
|
198
|
+
export function assertTurnOpen(exec) {
|
|
199
|
+
const session = exec?.agent?.session;
|
|
200
|
+
if (session === undefined || session === null) return;
|
|
201
|
+
if (typeof session.seq !== 'number' || typeof session.eventAt !== 'function') return;
|
|
202
|
+
const floor = Math.max(0, session.seq - 400);
|
|
203
|
+
for (let index = session.seq - 1; index >= floor; index -= 1) {
|
|
204
|
+
let type;
|
|
205
|
+
try { type = session.eventAt(index)?.type; } catch { return; }
|
|
206
|
+
if (type === 'turn/start') return;
|
|
207
|
+
if (type === 'turn/end') {
|
|
208
|
+
throw new PolicyError('turn_ended', 'the turn that requested this action has ended, so Computer Use stops issuing input. Ask the user to continue in a new turn.');
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* A token that identifies the current turn. Codex blocks further Computer Use
|
|
215
|
+
* calls for the rest of a turn once the user hits Esc; the helper cannot know
|
|
216
|
+
* about turns, so the plugin sends this token with every call.
|
|
217
|
+
*/
|
|
218
|
+
export function turnTokenOf(exec) {
|
|
219
|
+
const session = exec?.agent?.session;
|
|
220
|
+
if (session === undefined || session === null) return 'none';
|
|
221
|
+
if (typeof session.seq !== 'number' || typeof session.eventAt !== 'function') return 'none';
|
|
222
|
+
const floor = Math.max(0, session.seq - 400);
|
|
223
|
+
for (let index = session.seq - 1; index >= floor; index -= 1) {
|
|
224
|
+
let type;
|
|
225
|
+
try { type = session.eventAt(index)?.type; } catch { return 'none'; }
|
|
226
|
+
if (type === 'turn/start') {
|
|
227
|
+
// Include the session so a persisted Esc marker can never block a later session
|
|
228
|
+
// that happens to reuse the same turn index (Codex scopes markers the same way).
|
|
229
|
+
const id = session.id ?? session.sessionId ?? session.key;
|
|
230
|
+
const scope = typeof id === 'string' && id.length > 0 ? id : 'session';
|
|
231
|
+
return scope + ':' + 'turn-' + index;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return 'none';
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Windows whose pixels must never leave the machine ("appshot" guard). Codex mounts a
|
|
239
|
+
* prepared overlay around the capture; here the same intent is enforced by refusing the
|
|
240
|
+
* screenshot for the listed apps and reporting it.
|
|
241
|
+
*/
|
|
242
|
+
export function isRedacted(config, app) {
|
|
243
|
+
const list = Array.isArray(config?.redactApps) ? config.redactApps : [];
|
|
244
|
+
if (list.length === 0) return false;
|
|
245
|
+
const haystack = (String(app?.id ?? '') + ' ' + String(app?.name ?? '') + ' ' + String(app?.displayName ?? '')).toLowerCase();
|
|
246
|
+
return list.some(needle => haystack.includes(String(needle).toLowerCase()));
|
|
247
|
+
}
|