openmausbot 0.1.78 → 0.1.80
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/dist/assets/index-Cu8BkIfo.js +305 -0
- package/dist/assets/{index-BqDJqf2C.js → index-D6RZKWks.js} +1 -1
- package/dist/assets/index-tfpeJAIG.css +1 -0
- package/dist/index.html +2 -2
- package/dist-server/container-mcp.js +11 -5
- package/dist-server/drivers/agents-proxy.js +21 -6
- package/dist-server/enterprise/server/index.js +0 -1
- package/dist-server/index.js +15942 -6859
- package/dist-server/local-computer.js +0 -1
- package/dist-server/mcp-server.js +8 -1
- package/dist-server/openmausbot.js +9303 -786
- package/dist-server/pair-cli.js +9303 -786
- package/dist-server/proxy-paths.js +0 -1
- package/dist-server/server/box.js +98 -41
- package/dist-server/server/browser-engine.js +1 -1
- package/dist-server/server/browser-runtime.js +11 -3
- package/dist-server/server/browser-tool-shape.js +72 -0
- package/dist-server/server/claude-accounts.js +1 -0
- package/dist-server/server/cli-setup.js +1 -1
- package/dist-server/server/composio.js +18 -0
- package/dist-server/server/config.js +16 -4
- package/dist-server/server/container-computer.js +0 -12
- package/dist-server/server/drivers/acp/core.js +4 -19
- package/dist-server/server/drivers/agents-proxy.js +27 -2
- package/dist-server/server/drivers/boxagent.js +48 -4
- package/dist-server/server/drivers/chat-mcp-tools.js +368 -0
- package/dist-server/server/drivers/chat-tool-approval.js +42 -0
- package/dist-server/server/drivers/claude.js +86 -38
- package/dist-server/server/drivers/codex.js +225 -75
- package/dist-server/server/drivers/grok.js +4 -0
- package/dist-server/server/drivers/minimax.js +11 -3
- package/dist-server/server/drivers/openai-chat-protocol.js +92 -0
- package/dist-server/server/drivers/openai-chat.js +332 -88
- package/dist-server/server/drivers/openai-compat.js +4 -0
- package/dist-server/server/drivers/pi.js +1 -9
- package/dist-server/server/harness/registry.js +2 -2
- package/dist-server/server/index.js +267 -50
- package/dist-server/server/managed-desktop.js +205 -0
- package/dist-server/server/model-context-window.js +20 -0
- package/dist-server/server/proxy-paths.js +0 -1
- package/dist-server/server/redact.js +1 -0
- package/dist-server/server/room-handoffs.js +57 -9
- package/dist-server/server/store.js +13 -1
- package/dist-server/server/tts/chatterbox.js +83 -0
- package/dist-server/server/tts/index.js +45 -12
- package/dist-server/server/workspace.js +12 -2
- package/dist-server/shared/ask-question.js +28 -0
- package/dist-server/shared/computer-contention.js +17 -0
- package/dist-server/vps-container-mcp.js +11 -5
- package/enterprise/server/index.js +0 -1
- package/package.json +1 -1
- package/dist/assets/index-DjwAroIZ.js +0 -305
- package/dist/assets/index-Pbb6Ao0s.css +0 -1
- package/dist-server/computer-proxy.js +0 -1196
- package/dist-server/server/computer-proxy.js +0 -1070
- package/dist-server/server/remote-computer.js +0 -170
|
@@ -1,1196 +0,0 @@
|
|
|
1
|
-
// server/computer-observation.ts
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
3
|
-
var emptyObservationMetrics = () => ({
|
|
4
|
-
screenshotsCaptured: 0,
|
|
5
|
-
screenshotsSentToModel: 0,
|
|
6
|
-
fullScreenObservations: 0,
|
|
7
|
-
croppedObservations: 0,
|
|
8
|
-
structuredBrowserObservations: 0,
|
|
9
|
-
computerActions: 0,
|
|
10
|
-
retries: 0,
|
|
11
|
-
verificationSuccesses: 0,
|
|
12
|
-
verificationFailures: 0
|
|
13
|
-
});
|
|
14
|
-
function normalizeCrop(raw, maxWidth, maxHeight) {
|
|
15
|
-
if (!raw || typeof raw !== "object") return null;
|
|
16
|
-
const value = raw;
|
|
17
|
-
const x = Math.round(Number(value.x));
|
|
18
|
-
const y = Math.round(Number(value.y));
|
|
19
|
-
const width = Math.round(Number(value.width));
|
|
20
|
-
const height = Math.round(Number(value.height));
|
|
21
|
-
if (![x, y, width, height, maxWidth, maxHeight].every(Number.isFinite) || maxWidth <= 0 || maxHeight <= 0 || x < 0 || y < 0 || width < 32 || height < 32) {
|
|
22
|
-
return null;
|
|
23
|
-
}
|
|
24
|
-
if (x + width > maxWidth || y + height > maxHeight) return null;
|
|
25
|
-
return { x, y, width, height };
|
|
26
|
-
}
|
|
27
|
-
function normalizeBrowserUrl(value) {
|
|
28
|
-
if (typeof value !== "string" || !value || value.length > 8192) return null;
|
|
29
|
-
try {
|
|
30
|
-
const url = new URL(value);
|
|
31
|
-
if (!/^https?:$/.test(url.protocol)) return null;
|
|
32
|
-
url.username = "";
|
|
33
|
-
url.password = "";
|
|
34
|
-
return url.toString();
|
|
35
|
-
} catch {
|
|
36
|
-
return null;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
function safeBrowserUrl(value) {
|
|
40
|
-
const normalized = normalizeBrowserUrl(value);
|
|
41
|
-
if (!normalized) return null;
|
|
42
|
-
const url = new URL(normalized);
|
|
43
|
-
url.search = "";
|
|
44
|
-
url.hash = "";
|
|
45
|
-
const safe = url.toString();
|
|
46
|
-
return safe.length <= 2048 ? safe : null;
|
|
47
|
-
}
|
|
48
|
-
function parseBrowserTargets(raw) {
|
|
49
|
-
if (raw.length > 1e6) return [];
|
|
50
|
-
try {
|
|
51
|
-
const parsed = JSON.parse(raw);
|
|
52
|
-
if (!Array.isArray(parsed)) return [];
|
|
53
|
-
return parsed.slice(0, 20).flatMap((item) => {
|
|
54
|
-
if (!item || typeof item !== "object") return [];
|
|
55
|
-
const value = item;
|
|
56
|
-
const comparisonUrl = normalizeBrowserUrl(value.url);
|
|
57
|
-
const url = safeBrowserUrl(value.url);
|
|
58
|
-
if (value.type !== "page" || !url || !comparisonUrl || typeof value.id !== "string") return [];
|
|
59
|
-
const title = typeof value.title === "string" ? value.title.replace(/\s+/g, " ").trim().slice(0, 200) : "";
|
|
60
|
-
return [{ id: value.id.slice(0, 100), title, url, comparisonUrl }];
|
|
61
|
-
});
|
|
62
|
-
} catch {
|
|
63
|
-
return [];
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
var ObservationCoordinator = class {
|
|
67
|
-
metrics = emptyObservationMetrics();
|
|
68
|
-
lastObservation = null;
|
|
69
|
-
noteAction(count = 1) {
|
|
70
|
-
this.metrics.computerActions += Math.max(0, Math.trunc(count));
|
|
71
|
-
}
|
|
72
|
-
noteRetry() {
|
|
73
|
-
this.metrics.retries += 1;
|
|
74
|
-
}
|
|
75
|
-
/** canonicalFrame must describe the full screenshot, even when the image
|
|
76
|
-
* returned to the model is cropped. A box-provided full-frame hash works. */
|
|
77
|
-
observeFrame(canonicalFrame, crop) {
|
|
78
|
-
this.metrics.screenshotsCaptured += 1;
|
|
79
|
-
const hash = canonicalFrame ? createHash("sha256").update(canonicalFrame).digest("hex") : null;
|
|
80
|
-
const view = crop ? `${crop.x},${crop.y},${crop.width},${crop.height}` : "full";
|
|
81
|
-
const signature = hash ? `${hash}:${view}` : null;
|
|
82
|
-
const changed = signature === null || signature !== this.lastObservation;
|
|
83
|
-
if (signature) this.lastObservation = signature;
|
|
84
|
-
if (changed) {
|
|
85
|
-
this.metrics.screenshotsSentToModel += 1;
|
|
86
|
-
if (crop) this.metrics.croppedObservations += 1;
|
|
87
|
-
else this.metrics.fullScreenObservations += 1;
|
|
88
|
-
}
|
|
89
|
-
return { changed, hash };
|
|
90
|
-
}
|
|
91
|
-
noteStructuredObservation() {
|
|
92
|
-
this.metrics.structuredBrowserObservations += 1;
|
|
93
|
-
}
|
|
94
|
-
noteVerification(ok) {
|
|
95
|
-
if (ok) this.metrics.verificationSuccesses += 1;
|
|
96
|
-
else this.metrics.verificationFailures += 1;
|
|
97
|
-
}
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
// server/control-client.ts
|
|
101
|
-
var DISENGAGED = { held: false, helpOpen: false };
|
|
102
|
-
var UNAVAILABLE = { held: true, helpOpen: false };
|
|
103
|
-
function createControlClient(options) {
|
|
104
|
-
const url = options?.url ?? process.env.OMB_CONTROL_URL ?? "";
|
|
105
|
-
const token2 = options?.token ?? process.env.OMB_CONTROL_TOKEN ?? "";
|
|
106
|
-
const cacheMs = options?.cacheMs ?? 750;
|
|
107
|
-
const fetchImpl = options?.fetchImpl ?? fetch;
|
|
108
|
-
const configured = Boolean(url && token2);
|
|
109
|
-
const headers = { authorization: `Bearer ${token2}`, "content-type": "application/json" };
|
|
110
|
-
let cachedAt = 0;
|
|
111
|
-
let cached = DISENGAGED;
|
|
112
|
-
async function read() {
|
|
113
|
-
try {
|
|
114
|
-
const res = await fetchImpl(url, { headers, signal: AbortSignal.timeout(2e3) });
|
|
115
|
-
if (!res.ok) return UNAVAILABLE;
|
|
116
|
-
const body = await res.json().catch(() => null);
|
|
117
|
-
if (typeof body?.held !== "boolean" || typeof body?.helpOpen !== "boolean") return UNAVAILABLE;
|
|
118
|
-
return {
|
|
119
|
-
held: body.held,
|
|
120
|
-
helpOpen: body.helpOpen,
|
|
121
|
-
...typeof body.blockedReason === "string" && body.blockedReason.trim() ? { blockedReason: body.blockedReason } : {}
|
|
122
|
-
};
|
|
123
|
-
} catch {
|
|
124
|
-
return UNAVAILABLE;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
return {
|
|
128
|
-
configured,
|
|
129
|
-
async state(fresh = false) {
|
|
130
|
-
if (!configured) return DISENGAGED;
|
|
131
|
-
const now = Date.now();
|
|
132
|
-
if (!fresh && now - cachedAt < cacheMs) return cached;
|
|
133
|
-
cached = await read();
|
|
134
|
-
cachedAt = Date.now();
|
|
135
|
-
return cached;
|
|
136
|
-
},
|
|
137
|
-
async requestHelp(reason) {
|
|
138
|
-
if (!configured) return null;
|
|
139
|
-
try {
|
|
140
|
-
const res = await fetchImpl(url, {
|
|
141
|
-
method: "POST",
|
|
142
|
-
headers,
|
|
143
|
-
body: JSON.stringify({ reason }),
|
|
144
|
-
signal: AbortSignal.timeout(2e3)
|
|
145
|
-
});
|
|
146
|
-
if (!res.ok) return null;
|
|
147
|
-
const body = await res.json().catch(() => null);
|
|
148
|
-
return typeof body?.requestId === "string" && body.requestId ? body.requestId : null;
|
|
149
|
-
} catch {
|
|
150
|
-
return null;
|
|
151
|
-
}
|
|
152
|
-
},
|
|
153
|
-
async expireHelp(requestId) {
|
|
154
|
-
if (!configured || !requestId) return;
|
|
155
|
-
try {
|
|
156
|
-
await fetchImpl(url, {
|
|
157
|
-
method: "DELETE",
|
|
158
|
-
headers,
|
|
159
|
-
body: JSON.stringify({ requestId }),
|
|
160
|
-
signal: AbortSignal.timeout(2e3)
|
|
161
|
-
});
|
|
162
|
-
} catch {
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
var CONTROL_REFUSAL = "A person has taken control of this computer, so this call was NOT performed. Do not retry it \u2014 the screen is changing under their hands. Call computer_request_help (no reason needed) to wait for them to finish, then take a fresh screenshot before your next action.";
|
|
168
|
-
|
|
169
|
-
// server/remote-computer.ts
|
|
170
|
-
var REMOTE_CUA_VERSION = "0.20.0";
|
|
171
|
-
var REMOTE_CUA_EXECUTABLE = "/opt/ogb/cua-driver";
|
|
172
|
-
var REMOTE_CUA_SOCKET = "/opt/ogb/run/cua.sock";
|
|
173
|
-
var REMOTE_CUA_SESSION = "openmausbot";
|
|
174
|
-
var REMOTE_CDP_HELPER = "/opt/ogb/openmausbot-cdp.mjs";
|
|
175
|
-
var CDP_HELPER_SOURCE = String.raw`const [action, encoded = ""] = process.argv.slice(2);
|
|
176
|
-
const input = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8") || "{}");
|
|
177
|
-
const pages = await fetch("http://127.0.0.1:9222/json/list").then((r) => r.json());
|
|
178
|
-
const page = pages.find((item) => item.type === "page" && item.webSocketDebuggerUrl);
|
|
179
|
-
if (!page) throw new Error("no debuggable browser page");
|
|
180
|
-
if (input.url && page.url !== input.url) throw new Error("page changed; take a new browser snapshot");
|
|
181
|
-
const socket = new WebSocket(page.webSocketDebuggerUrl);
|
|
182
|
-
await new Promise((resolve, reject) => {
|
|
183
|
-
socket.addEventListener("open", resolve, { once: true });
|
|
184
|
-
socket.addEventListener("error", () => reject(new Error("DevTools connection failed")), { once: true });
|
|
185
|
-
});
|
|
186
|
-
let nextId = 0;
|
|
187
|
-
const pending = new Map();
|
|
188
|
-
socket.addEventListener("message", (event) => {
|
|
189
|
-
const message = JSON.parse(String(event.data));
|
|
190
|
-
if (!message.id) return;
|
|
191
|
-
const waiter = pending.get(message.id);
|
|
192
|
-
if (!waiter) return;
|
|
193
|
-
pending.delete(message.id);
|
|
194
|
-
if (message.error) waiter.reject(new Error(message.error.message));
|
|
195
|
-
else waiter.resolve(message.result ?? {});
|
|
196
|
-
});
|
|
197
|
-
const send = (method, params = {}) => new Promise((resolve, reject) => {
|
|
198
|
-
const id = ++nextId;
|
|
199
|
-
pending.set(id, { resolve, reject });
|
|
200
|
-
socket.send(JSON.stringify({ id, method, params }));
|
|
201
|
-
});
|
|
202
|
-
const refId = (value) => {
|
|
203
|
-
const match = /^b(\d+)$/.exec(String(value ?? ""));
|
|
204
|
-
if (!match) throw new Error("invalid or stale browser ref; take a new snapshot");
|
|
205
|
-
return Number(match[1]);
|
|
206
|
-
};
|
|
207
|
-
if (action === "snapshot") {
|
|
208
|
-
await send("Accessibility.enable");
|
|
209
|
-
const { nodes = [] } = await send("Accessibility.getFullAXTree", { depth: 14 });
|
|
210
|
-
const useful = new Set(["button", "checkbox", "combobox", "heading", "link", "menuitem", "radio", "searchbox", "slider", "spinbutton", "switch", "tab", "textbox"]);
|
|
211
|
-
const elements = [];
|
|
212
|
-
for (const node of nodes) {
|
|
213
|
-
const role = String(node.role?.value ?? "").toLowerCase();
|
|
214
|
-
const name = String(node.name?.value ?? "").replace(/\s+/g, " ").trim().slice(0, 180);
|
|
215
|
-
const backend = Number(node.backendDOMNodeId ?? 0);
|
|
216
|
-
if (!backend || !useful.has(role) || (!name && role !== "textbox" && role !== "searchbox")) continue;
|
|
217
|
-
const disabled = node.properties?.some((property) => property.name === "disabled" && property.value?.value === true) ?? false;
|
|
218
|
-
elements.push({ ref: "b" + backend, role, name: name || "unnamed", disabled });
|
|
219
|
-
if (elements.length >= 250) break;
|
|
220
|
-
}
|
|
221
|
-
process.stdout.write(JSON.stringify({ title: String(page.title ?? "").slice(0, 200), url: page.url, elements }));
|
|
222
|
-
} else if (action === "click") {
|
|
223
|
-
const backendNodeId = refId(input.ref);
|
|
224
|
-
const { model } = await send("DOM.getBoxModel", { backendNodeId });
|
|
225
|
-
const quad = model?.border ?? model?.content;
|
|
226
|
-
if (!Array.isArray(quad) || quad.length < 8) throw new Error("element is not visible; take a new snapshot");
|
|
227
|
-
const x = (quad[0] + quad[2] + quad[4] + quad[6]) / 4;
|
|
228
|
-
const y = (quad[1] + quad[3] + quad[5] + quad[7]) / 4;
|
|
229
|
-
await send("Input.dispatchMouseEvent", { type: "mousePressed", x, y, button: "left", clickCount: 1 });
|
|
230
|
-
await send("Input.dispatchMouseEvent", { type: "mouseReleased", x, y, button: "left", clickCount: 1 });
|
|
231
|
-
process.stdout.write(JSON.stringify({ ok: true, ref: input.ref }));
|
|
232
|
-
} else if (action === "fill") {
|
|
233
|
-
const backendNodeId = refId(input.ref);
|
|
234
|
-
await send("DOM.focus", { backendNodeId });
|
|
235
|
-
await send("Input.dispatchKeyEvent", { type: "keyDown", key: "a", code: "KeyA", modifiers: 2 });
|
|
236
|
-
await send("Input.dispatchKeyEvent", { type: "keyUp", key: "a", code: "KeyA", modifiers: 2 });
|
|
237
|
-
await send("Input.dispatchKeyEvent", { type: "keyDown", key: "Backspace", code: "Backspace" });
|
|
238
|
-
await send("Input.dispatchKeyEvent", { type: "keyUp", key: "Backspace", code: "Backspace" });
|
|
239
|
-
await send("Input.insertText", { text: String(input.text ?? "") });
|
|
240
|
-
process.stdout.write(JSON.stringify({ ok: true, ref: input.ref }));
|
|
241
|
-
} else {
|
|
242
|
-
throw new Error("unknown browser action");
|
|
243
|
-
}
|
|
244
|
-
socket.close();`;
|
|
245
|
-
var shellQuote = (value) => `'${value.replace(/'/g, "'\\''")}'`;
|
|
246
|
-
var MAX_REMOTE_COMMAND_LENGTH = 4e3;
|
|
247
|
-
function isolatedRemoteCommand(command) {
|
|
248
|
-
return [
|
|
249
|
-
"exec env -i",
|
|
250
|
-
'HOME="$HOME"',
|
|
251
|
-
'USER="${USER:-$(id -un)}"',
|
|
252
|
-
'LOGNAME="${LOGNAME:-${USER:-$(id -un)}}"',
|
|
253
|
-
'PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"',
|
|
254
|
-
'DISPLAY="${DISPLAY:-:0}"',
|
|
255
|
-
'XAUTHORITY="${XAUTHORITY:-$HOME/.Xauthority}"',
|
|
256
|
-
'XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"',
|
|
257
|
-
'DBUS_SESSION_BUS_ADDRESS="${DBUS_SESSION_BUS_ADDRESS:-}"',
|
|
258
|
-
"/bin/bash -c",
|
|
259
|
-
shellQuote(command)
|
|
260
|
-
].join(" ");
|
|
261
|
-
}
|
|
262
|
-
function ensureRemoteCuaCommand() {
|
|
263
|
-
return [
|
|
264
|
-
`if [ -x ${REMOTE_CUA_EXECUTABLE} ]; then`,
|
|
265
|
-
` mkdir -p ${REMOTE_CUA_SOCKET.slice(0, REMOTE_CUA_SOCKET.lastIndexOf("/"))}`,
|
|
266
|
-
` if ! ${REMOTE_CUA_EXECUTABLE} status --socket ${REMOTE_CUA_SOCKET} >/dev/null 2>&1; then`,
|
|
267
|
-
` rm -f ${REMOTE_CUA_SOCKET}`,
|
|
268
|
-
' display=${DISPLAY:-$(find /tmp/.X11-unix -maxdepth 1 -name "X*" -printf ":%f\\n" 2>/dev/null | sed "s/:X/:/" | head -1)}',
|
|
269
|
-
" display=${display:-:0}",
|
|
270
|
-
` nohup env HOME="$HOME" DISPLAY="$display" CUA_DRIVER_INSTALL_CHANNEL=python_package CUA_DRIVER_RS_TELEMETRY_ENABLED=0 ${REMOTE_CUA_EXECUTABLE} serve --socket ${REMOTE_CUA_SOCKET} --permission-mode standard > /tmp/ogb-cua-driver.log 2>&1 &`,
|
|
271
|
-
` for i in 1 2 3 4 5 6 7 8 9 10; do ${REMOTE_CUA_EXECUTABLE} status --socket ${REMOTE_CUA_SOCKET} >/dev/null 2>&1 && break; sleep 0.2; done`,
|
|
272
|
-
" fi",
|
|
273
|
-
"fi"
|
|
274
|
-
].join("\n");
|
|
275
|
-
}
|
|
276
|
-
function semanticBrowserCommand(action, input) {
|
|
277
|
-
const encoded = Buffer.from(JSON.stringify(input ?? {})).toString("base64url");
|
|
278
|
-
return `node ${REMOTE_CDP_HELPER} ${action} ${shellQuote(encoded)}`;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// server/computer-proxy.ts
|
|
282
|
-
var BOX_API = process.env.OGB_BOX_API ?? "https://ascii.dev/api/box/v1";
|
|
283
|
-
var boxId = process.env.OGB_BOX_ID ?? "";
|
|
284
|
-
var token = process.env.OGB_BOX_TOKEN ?? "";
|
|
285
|
-
var CONTROL_POLL_MS = Math.max(Number(process.env.OMB_CONTROL_POLL_MS) || 1500, 25);
|
|
286
|
-
var CONTROL_WAIT_MS = Math.max(Number(process.env.OMB_CONTROL_WAIT_MS) || 6e5, 100);
|
|
287
|
-
var control = createControlClient({ cacheMs: Math.min(750, CONTROL_POLL_MS) });
|
|
288
|
-
var SHOT_WIDTH = 1280;
|
|
289
|
-
var JPEG_QUALITY = 75;
|
|
290
|
-
var SHOT_PATH = "/tmp/ogb-shot.jpg";
|
|
291
|
-
var SETTLE_MS = 350;
|
|
292
|
-
var ACTION_GAP_MS = 120;
|
|
293
|
-
var CHROME_PROFILE = "$HOME/.openmausbot/chrome-profile";
|
|
294
|
-
var CHROME_DEBUG_FLAGS = `--user-data-dir="${CHROME_PROFILE}" --password-store=basic --disable-session-crashed-bubble --no-first-run --remote-debugging-address=127.0.0.1 --remote-debugging-port=9222`;
|
|
295
|
-
var CHROME_PROFILE_SETUP = [
|
|
296
|
-
`profile="${CHROME_PROFILE}"`,
|
|
297
|
-
'mkdir -p "$profile" "$HOME/.config"',
|
|
298
|
-
'chmod 700 "$profile"',
|
|
299
|
-
'for browser_dir in "$HOME/.config/google-chrome" "$HOME/.config/chromium"; do',
|
|
300
|
-
' if [ -e "$browser_dir" ] && [ ! -L "$browser_dir" ]; then',
|
|
301
|
-
' if [ -d "$browser_dir" ] && ! cp -a -n "$browser_dir"/. "$profile"/; then',
|
|
302
|
-
' echo "failed to copy browser profile: $browser_dir" >&2',
|
|
303
|
-
" exit 1",
|
|
304
|
-
" fi",
|
|
305
|
-
' mv "$browser_dir" "$browser_dir.pre-openmausbot-$(date +%s)-$$"',
|
|
306
|
-
" fi",
|
|
307
|
-
' if [ -L "$browser_dir" ]; then rm -f "$browser_dir"; fi',
|
|
308
|
-
' ln -s "$profile" "$browser_dir"',
|
|
309
|
-
"done"
|
|
310
|
-
].join("\n");
|
|
311
|
-
var INLINE_MAX_BYTES = 4e5;
|
|
312
|
-
async function resumeBox() {
|
|
313
|
-
const auth = { authorization: `Bearer ${token}`, "content-type": "application/json" };
|
|
314
|
-
await fetch(`${BOX_API}/boxes/${boxId}/resume`, { method: "POST", headers: auth }).catch(() => null);
|
|
315
|
-
const deadline = Date.now() + 9e4;
|
|
316
|
-
while (Date.now() < deadline) {
|
|
317
|
-
await new Promise((r) => setTimeout(r, 2e3));
|
|
318
|
-
const res = await fetch(`${BOX_API}/boxes/${boxId}`, { headers: auth }).catch(() => null);
|
|
319
|
-
const body = await res?.json().catch(() => null);
|
|
320
|
-
const state = body?.box?.state;
|
|
321
|
-
if (state && ["idle", "ready", "running"].includes(state)) return true;
|
|
322
|
-
if (state === "error") return false;
|
|
323
|
-
}
|
|
324
|
-
return false;
|
|
325
|
-
}
|
|
326
|
-
async function runOnBox(command, timeoutMs = 6e4, allowWake = true) {
|
|
327
|
-
const isolatedCommand = isolatedRemoteCommand(command);
|
|
328
|
-
const res = await fetch(`${BOX_API}/boxes/${boxId}/commands`, {
|
|
329
|
-
method: "POST",
|
|
330
|
-
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
331
|
-
body: JSON.stringify({ command: isolatedCommand }),
|
|
332
|
-
signal: AbortSignal.timeout(timeoutMs)
|
|
333
|
-
});
|
|
334
|
-
const body = await res.json().catch(() => null);
|
|
335
|
-
if (res.status === 409 && allowWake) {
|
|
336
|
-
const code = body?.code ?? body?.error?.code ?? "";
|
|
337
|
-
if (/machine_not_running|box_starting|not_running|starting/i.test(String(code))) {
|
|
338
|
-
const woke = await resumeBox();
|
|
339
|
-
if (woke) return runOnBox(command, timeoutMs, false);
|
|
340
|
-
return { ok: false, exitCode: null, stdout: "", stderr: "the computer is asleep and did not wake in time" };
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
return {
|
|
344
|
-
ok: res.ok && body?.exitCode === 0,
|
|
345
|
-
exitCode: body?.exitCode ?? null,
|
|
346
|
-
stdout: body?.stdout ?? "",
|
|
347
|
-
stderr: body?.stderr ?? String(body?.message ?? (res.ok ? "" : `HTTP ${res.status}`))
|
|
348
|
-
};
|
|
349
|
-
}
|
|
350
|
-
var observations = new ObservationCoordinator();
|
|
351
|
-
function metricsText() {
|
|
352
|
-
return JSON.stringify(observations.metrics);
|
|
353
|
-
}
|
|
354
|
-
async function browserTargets(countObservation = true) {
|
|
355
|
-
const out = await runOnBox("curl -sf --max-time 2 http://127.0.0.1:9222/json/list", 5e3);
|
|
356
|
-
const targets = out.ok ? parseBrowserTargets(out.stdout) : [];
|
|
357
|
-
if (countObservation && targets.length) observations.noteStructuredObservation();
|
|
358
|
-
return targets;
|
|
359
|
-
}
|
|
360
|
-
async function waitForNavigation(value, attempts = 3) {
|
|
361
|
-
const expected = normalizeBrowserUrl(value);
|
|
362
|
-
if (!expected) {
|
|
363
|
-
observations.noteVerification(false);
|
|
364
|
-
return { ok: false, targets: [] };
|
|
365
|
-
}
|
|
366
|
-
let targets = [];
|
|
367
|
-
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
368
|
-
if (attempt > 0) {
|
|
369
|
-
observations.noteRetry();
|
|
370
|
-
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
371
|
-
}
|
|
372
|
-
targets = await browserTargets(false);
|
|
373
|
-
if (targets.some((target) => target.comparisonUrl === expected)) {
|
|
374
|
-
observations.noteVerification(true);
|
|
375
|
-
return { ok: true, targets };
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
observations.noteVerification(false);
|
|
379
|
-
return { ok: false, targets };
|
|
380
|
-
}
|
|
381
|
-
var ENV = "export DISPLAY=${DISPLAY:-:0}";
|
|
382
|
-
var CUA_ENV = "CUA_DRIVER_INSTALL_CHANNEL=python_package CUA_DRIVER_RS_TELEMETRY_ENABLED=0";
|
|
383
|
-
var GEOMETRY = [
|
|
384
|
-
"g=$(xdotool getdisplaygeometry 2>/dev/null)",
|
|
385
|
-
"W=${g%% *}",
|
|
386
|
-
"H=${g##* }",
|
|
387
|
-
`case "$W" in ''|*[!0-9]*) W=${SHOT_WIDTH}; H=0;; esac`
|
|
388
|
-
].join("; ");
|
|
389
|
-
function scaled(varName, value) {
|
|
390
|
-
const v = Math.round(value);
|
|
391
|
-
return `if [ "$W" -gt ${SHOT_WIDTH} ] 2>/dev/null; then ${varName}=$(( ${v} * W / ${SHOT_WIDTH} )); else ${varName}=${v}; fi`;
|
|
392
|
-
}
|
|
393
|
-
function cuaOrX11(tool, argumentsShell, fallback) {
|
|
394
|
-
return [
|
|
395
|
-
`if [ -x ${REMOTE_CUA_EXECUTABLE} ] && ${REMOTE_CUA_EXECUTABLE} status --socket ${REMOTE_CUA_SOCKET} >/dev/null 2>&1;`,
|
|
396
|
-
`then if CUA_OUT=$(env ${CUA_ENV} ${REMOTE_CUA_EXECUTABLE} call ${tool} ${argumentsShell} --socket ${REMOTE_CUA_SOCKET} 2>/tmp/ogb-cua-call.error);`,
|
|
397
|
-
`then echo "BACKEND CUA"; echo "CUA_RESULT $(printf %s "$CUA_OUT" | base64 -w0 2>/dev/null || printf %s "$CUA_OUT" | base64 | tr -d '\\n')"`,
|
|
398
|
-
`else ${fallback}; X11_RC=$?; echo "BACKEND X11"; [ "$X11_RC" -eq 0 ]; fi`,
|
|
399
|
-
`else ${fallback}; X11_RC=$?; echo "BACKEND X11"; [ "$X11_RC" -eq 0 ]; fi`
|
|
400
|
-
].join(" ");
|
|
401
|
-
}
|
|
402
|
-
function captureBlock(settleMs = SETTLE_MS, crop = null) {
|
|
403
|
-
const downscale = crop ? `if [ "$W" -gt ${SHOT_WIDTH} ] 2>/dev/null; then if ! command -v convert >/dev/null 2>&1 || ! convert "$f" -thumbnail ${SHOT_WIDTH}x -quality ${JPEG_QUALITY} "$f" 2>/dev/null; then echo CROP_FAILED; exit 0; fi; fi` : `if [ "$W" -gt ${SHOT_WIDTH} ] 2>/dev/null && command -v convert >/dev/null 2>&1; then convert "$f" -thumbnail ${SHOT_WIDTH}x -quality ${JPEG_QUALITY} "$f" 2>/dev/null || true; fi`;
|
|
404
|
-
const cropSteps = crop ? [
|
|
405
|
-
`if ! command -v convert >/dev/null 2>&1 || ! convert "$f" -crop ${crop.width}x${crop.height}+${crop.x}+${crop.y} +repage "$f" 2>/dev/null; then echo CROP_FAILED; exit 0; fi`,
|
|
406
|
-
`if [ ! -s "$f" ]; then echo CROP_FAILED; exit 0; fi`
|
|
407
|
-
] : [];
|
|
408
|
-
return [
|
|
409
|
-
settleMs > 0 ? `sleep ${(settleMs / 1e3).toFixed(2)}` : "true",
|
|
410
|
-
`f=${SHOT_PATH}`,
|
|
411
|
-
"raw=/tmp/ogb-shot.png",
|
|
412
|
-
`rm -f "$f" 2>/dev/null || true`,
|
|
413
|
-
`rm -f "$raw" 2>/dev/null || true`,
|
|
414
|
-
`if [ -x ${REMOTE_CUA_EXECUTABLE} ] && ${REMOTE_CUA_EXECUTABLE} status --socket ${REMOTE_CUA_SOCKET} >/dev/null 2>&1 && env ${CUA_ENV} ${REMOTE_CUA_EXECUTABLE} call get_desktop_state ${shellQuote2(JSON.stringify({ scope: "desktop", session: REMOTE_CUA_SESSION }))} --socket ${REMOTE_CUA_SOCKET} --screenshot-out-file "$raw" >/dev/null 2>&1 && command -v convert >/dev/null 2>&1 && convert "$raw" -quality ${JPEG_QUALITY} "$f" 2>/dev/null; then echo "CAPTURE CUA"; else scrot -o -q ${JPEG_QUALITY} "$f" 2>/dev/null || import -window root -quality ${JPEG_QUALITY} "$f" 2>/dev/null || ffmpeg -y -f x11grab -i "$DISPLAY" -frames:v 1 -q:v 6 "$f" >/dev/null 2>&1; echo "CAPTURE X11"; fi`,
|
|
415
|
-
// only re-encode when the display is bigger than the model's space —
|
|
416
|
-
// ImageMagick startup is the most expensive step in the old pipeline
|
|
417
|
-
downscale,
|
|
418
|
-
`if [ ! -s "$f" ]; then echo SHOT_FAILED; exit 0; fi`,
|
|
419
|
-
'echo "GEOM $W $H"',
|
|
420
|
-
`echo "HASH $(md5sum "$f" 2>/dev/null | cut -d' ' -f1)"`,
|
|
421
|
-
...cropSteps,
|
|
422
|
-
's=$(stat -c%s "$f" 2>/dev/null || echo 0)',
|
|
423
|
-
// SIZE is what makes the inline path safe: the frame is only trusted
|
|
424
|
-
// when the bytes we decoded match the bytes the box says it wrote
|
|
425
|
-
'echo "SIZE $s"',
|
|
426
|
-
`if [ "$s" -gt 0 ] && [ "$s" -le ${INLINE_MAX_BYTES} ]; then echo "B64 $(base64 -w0 "$f" 2>/dev/null || base64 "$f" | tr -d '\\n')"; fi`
|
|
427
|
-
].join("; ");
|
|
428
|
-
}
|
|
429
|
-
function wholeImage(bytes, expectedBytes) {
|
|
430
|
-
if (bytes.length < 512) return false;
|
|
431
|
-
if (expectedBytes && bytes.length !== expectedBytes) return false;
|
|
432
|
-
const jpeg = bytes[0] === 255 && bytes[1] === 216;
|
|
433
|
-
const png = bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71;
|
|
434
|
-
if (jpeg) {
|
|
435
|
-
const tail = bytes.subarray(Math.max(0, bytes.length - 32));
|
|
436
|
-
return tail.includes(Buffer.from([255, 217]));
|
|
437
|
-
}
|
|
438
|
-
if (png) {
|
|
439
|
-
const tail = bytes.subarray(Math.max(0, bytes.length - 12));
|
|
440
|
-
return tail.includes(Buffer.from("IEND", "ascii"));
|
|
441
|
-
}
|
|
442
|
-
return false;
|
|
443
|
-
}
|
|
444
|
-
async function fetchFrame(expectedBytes) {
|
|
445
|
-
const auth = { authorization: `Bearer ${token}` };
|
|
446
|
-
try {
|
|
447
|
-
const res = await fetch(
|
|
448
|
-
`${BOX_API}/boxes/${boxId}/artifacts?path=${encodeURIComponent(SHOT_PATH)}`,
|
|
449
|
-
{ headers: auth, signal: AbortSignal.timeout(3e4) }
|
|
450
|
-
);
|
|
451
|
-
if (res.ok) {
|
|
452
|
-
const bytes = Buffer.from(await res.arrayBuffer());
|
|
453
|
-
if (wholeImage(bytes, expectedBytes)) return bytes.toString("base64");
|
|
454
|
-
}
|
|
455
|
-
} catch {
|
|
456
|
-
}
|
|
457
|
-
try {
|
|
458
|
-
const res = await fetch(
|
|
459
|
-
`${BOX_API}/boxes/${boxId}/files?path=${encodeURIComponent(SHOT_PATH)}&encoding=base64`,
|
|
460
|
-
{ headers: auth, signal: AbortSignal.timeout(3e4) }
|
|
461
|
-
);
|
|
462
|
-
const body = await res.json().catch(() => null);
|
|
463
|
-
const content = body?.content;
|
|
464
|
-
if (!res.ok || typeof content !== "string" || !content) return null;
|
|
465
|
-
return wholeImage(Buffer.from(content, "base64"), expectedBytes) ? content : null;
|
|
466
|
-
} catch {
|
|
467
|
-
return null;
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
var inlineWorks = true;
|
|
471
|
-
var lastDisplayGeometry = null;
|
|
472
|
-
var semanticBrowserUrl = null;
|
|
473
|
-
var semanticBrowserRefs = /* @__PURE__ */ new Set();
|
|
474
|
-
function geometryFrom(stdout) {
|
|
475
|
-
const match = stdout.match(/^GEOM\s+(\d+)\s+(\d+)$/m);
|
|
476
|
-
if (!match) return null;
|
|
477
|
-
const width = Number(match[1]);
|
|
478
|
-
const height = Number(match[2]);
|
|
479
|
-
return width > 0 && height > 0 ? { width, height } : null;
|
|
480
|
-
}
|
|
481
|
-
function automationSummary(stdout) {
|
|
482
|
-
if (/^BACKEND CUA$/m.test(stdout)) {
|
|
483
|
-
const encoded = stdout.match(/^CUA_RESULT\s+([^\s]+)$/m)?.[1];
|
|
484
|
-
if (!encoded) return `Cua Driver ${REMOTE_CUA_VERSION}`;
|
|
485
|
-
try {
|
|
486
|
-
const result = JSON.parse(Buffer.from(encoded, "base64").toString("utf8"));
|
|
487
|
-
const details = [result.effect, result.route, result.escalation].filter((value) => typeof value === "string" && Boolean(value)).slice(0, 3);
|
|
488
|
-
return [`Cua Driver ${REMOTE_CUA_VERSION}`, ...details].join(" \xB7 ");
|
|
489
|
-
} catch {
|
|
490
|
-
return `Cua Driver ${REMOTE_CUA_VERSION}`;
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
return /^BACKEND X11$/m.test(stdout) ? "X11 fallback" : "automation backend unavailable";
|
|
494
|
-
}
|
|
495
|
-
async function observationBounds() {
|
|
496
|
-
let geometry = lastDisplayGeometry;
|
|
497
|
-
if (!geometry) {
|
|
498
|
-
const out = await runOnBox([ENV, GEOMETRY, 'echo "GEOM $W $H"'].join("; "), 15e3);
|
|
499
|
-
geometry = geometryFrom(out.stdout);
|
|
500
|
-
if (geometry) lastDisplayGeometry = geometry;
|
|
501
|
-
}
|
|
502
|
-
if (!geometry) return null;
|
|
503
|
-
const scale = geometry.width > SHOT_WIDTH ? SHOT_WIDTH / geometry.width : 1;
|
|
504
|
-
return {
|
|
505
|
-
width: Math.round(geometry.width * scale),
|
|
506
|
-
height: Math.round(geometry.height * scale)
|
|
507
|
-
};
|
|
508
|
-
}
|
|
509
|
-
async function frameFrom(out) {
|
|
510
|
-
if (/SHOT_FAILED|CROP_FAILED/.test(out.stdout)) return null;
|
|
511
|
-
let hash = null;
|
|
512
|
-
let geometry = null;
|
|
513
|
-
let inline = "";
|
|
514
|
-
let size = 0;
|
|
515
|
-
for (const line of out.stdout.split("\n")) {
|
|
516
|
-
if (line.startsWith("HASH ")) hash = line.slice(5).trim() || null;
|
|
517
|
-
else if (line.startsWith("SIZE ")) size = Number(line.slice(5).trim()) || 0;
|
|
518
|
-
else if (line.startsWith("GEOM ")) {
|
|
519
|
-
const [w, h] = line.slice(5).trim().split(/\s+/).map(Number);
|
|
520
|
-
if (Number.isFinite(w) && w > 0) geometry = { width: w, height: Number.isFinite(h) ? h : 0 };
|
|
521
|
-
} else if (line.startsWith("B64 ")) inline = line.slice(4).trim();
|
|
522
|
-
}
|
|
523
|
-
if (geometry?.height) lastDisplayGeometry = geometry;
|
|
524
|
-
if (inline && inlineWorks) {
|
|
525
|
-
const bytes = Buffer.from(inline, "base64");
|
|
526
|
-
if (wholeImage(bytes, size || void 0)) return { data: inline, mime: "image/jpeg", hash, geometry };
|
|
527
|
-
inlineWorks = false;
|
|
528
|
-
}
|
|
529
|
-
const fetched = await fetchFrame(size || void 0);
|
|
530
|
-
if (!fetched) return null;
|
|
531
|
-
return { data: fetched, mime: "image/jpeg", hash, geometry };
|
|
532
|
-
}
|
|
533
|
-
var send = (obj) => {
|
|
534
|
-
process.stdout.write(JSON.stringify(obj) + "\n");
|
|
535
|
-
};
|
|
536
|
-
var text = (id, t, isError = false) => send({ jsonrpc: "2.0", id, result: { content: [{ type: "text", text: t }], isError: isError || void 0 } });
|
|
537
|
-
function observed(id, note, frame, crop = null, followsAction = true, isError = false) {
|
|
538
|
-
if (!frame) {
|
|
539
|
-
return text(id, `${note}
|
|
540
|
-
(couldn't capture the screen \u2014 call screenshot to retry)`, isError);
|
|
541
|
-
}
|
|
542
|
-
const observation = observations.observeFrame(frame.hash ?? (crop ? null : frame.data), crop);
|
|
543
|
-
if (!observation.changed) {
|
|
544
|
-
const guidance = followsAction ? " Don't repeat the action \u2014 it may already have succeeded. If you expected a change, call screenshot again after it has had time to render." : " No new image is attached.";
|
|
545
|
-
return text(id, `${note}
|
|
546
|
-
(the screen is identical to the frame you already have.${guidance})`, isError);
|
|
547
|
-
}
|
|
548
|
-
send({
|
|
549
|
-
jsonrpc: "2.0",
|
|
550
|
-
id,
|
|
551
|
-
result: {
|
|
552
|
-
content: [
|
|
553
|
-
{ type: "text", text: note },
|
|
554
|
-
{ type: "image", data: frame.data, mimeType: frame.mime }
|
|
555
|
-
],
|
|
556
|
-
isError: isError || void 0
|
|
557
|
-
}
|
|
558
|
-
});
|
|
559
|
-
}
|
|
560
|
-
var OBSERVE_PROPS = {
|
|
561
|
-
observe: {
|
|
562
|
-
type: "boolean",
|
|
563
|
-
description: "default true \u2014 return a fresh screenshot with the result. Set false only when chaining mechanical steps you don't need to see."
|
|
564
|
-
},
|
|
565
|
-
settle_ms: { type: "number", description: "wait before the screenshot, default 350, max 3000" }
|
|
566
|
-
};
|
|
567
|
-
var TOOLS = [
|
|
568
|
-
{
|
|
569
|
-
name: "screenshot",
|
|
570
|
-
description: "See the bot's cloud computer screen when visual state is needed. First prefer browser_state for Chrome title/URL checks. The frame is captured fresh; byte-identical pixels are not resent.",
|
|
571
|
-
inputSchema: {
|
|
572
|
-
type: "object",
|
|
573
|
-
properties: {
|
|
574
|
-
region: {
|
|
575
|
-
type: "object",
|
|
576
|
-
description: "Optional crop in the coordinates of the last screenshot.",
|
|
577
|
-
properties: {
|
|
578
|
-
x: { type: "number" },
|
|
579
|
-
y: { type: "number" },
|
|
580
|
-
width: { type: "number" },
|
|
581
|
-
height: { type: "number" }
|
|
582
|
-
},
|
|
583
|
-
required: ["x", "y", "width", "height"]
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
},
|
|
588
|
-
{
|
|
589
|
-
name: "browser_state",
|
|
590
|
-
description: "Read structured Chrome page titles and safe URLs. Credentials, query strings, and fragments are removed before output.",
|
|
591
|
-
inputSchema: { type: "object", properties: {} }
|
|
592
|
-
},
|
|
593
|
-
{
|
|
594
|
-
name: "browser_snapshot",
|
|
595
|
-
description: "Read Chrome's semantic accessibility tree and return fresh element refs. Prefer this over screenshots for links, buttons, and form fields.",
|
|
596
|
-
inputSchema: { type: "object", properties: {} }
|
|
597
|
-
},
|
|
598
|
-
{
|
|
599
|
-
name: "browser_click",
|
|
600
|
-
description: "Click one element ref from the most recent browser_snapshot and return the resulting screen.",
|
|
601
|
-
inputSchema: {
|
|
602
|
-
type: "object",
|
|
603
|
-
properties: { ref: { type: "string" }, ...OBSERVE_PROPS },
|
|
604
|
-
required: ["ref"]
|
|
605
|
-
}
|
|
606
|
-
},
|
|
607
|
-
{
|
|
608
|
-
name: "browser_fill",
|
|
609
|
-
description: "Replace the text in one field ref from the most recent browser_snapshot and return the resulting screen.",
|
|
610
|
-
inputSchema: {
|
|
611
|
-
type: "object",
|
|
612
|
-
properties: { ref: { type: "string" }, text: { type: "string" }, ...OBSERVE_PROPS },
|
|
613
|
-
required: ["ref", "text"]
|
|
614
|
-
}
|
|
615
|
-
},
|
|
616
|
-
{
|
|
617
|
-
name: "wait_for_navigation",
|
|
618
|
-
description: "Verify that Chrome reached one exact http(s) URL, including its query and fragment, with at most three bounded checks.",
|
|
619
|
-
inputSchema: {
|
|
620
|
-
type: "object",
|
|
621
|
-
properties: { url: { type: "string" } },
|
|
622
|
-
required: ["url"]
|
|
623
|
-
}
|
|
624
|
-
},
|
|
625
|
-
{
|
|
626
|
-
name: "observation_metrics",
|
|
627
|
-
description: "Return this turn's observation, action, retry, and verification counters.",
|
|
628
|
-
inputSchema: { type: "object", properties: {} }
|
|
629
|
-
},
|
|
630
|
-
{
|
|
631
|
-
name: "computer_status",
|
|
632
|
-
description: "Report whether the cloud computer is using Cua Driver or the degraded X11 fallback.",
|
|
633
|
-
inputSchema: { type: "object", properties: {} }
|
|
634
|
-
},
|
|
635
|
-
{
|
|
636
|
-
name: "computer_request_help",
|
|
637
|
-
description: "Ask the person to take over this computer (a login, a CAPTCHA, anything you should not do alone) and wait until they hand control back. Also call it with no reason when an action was refused because a person is already driving. You cannot take control yourself \u2014 this only asks.",
|
|
638
|
-
inputSchema: {
|
|
639
|
-
type: "object",
|
|
640
|
-
properties: {
|
|
641
|
-
reason: {
|
|
642
|
-
type: "string",
|
|
643
|
-
description: "one short sentence the person will read \u2014 what you need their hands for"
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
},
|
|
648
|
-
{
|
|
649
|
-
name: "click",
|
|
650
|
-
description: "Click on the computer's screen and return the resulting screen. Use pixel coordinates exactly as they appear in the last frame you were given \u2014 any scaling to the real display is handled for you.",
|
|
651
|
-
inputSchema: {
|
|
652
|
-
type: "object",
|
|
653
|
-
properties: {
|
|
654
|
-
x: { type: "number" },
|
|
655
|
-
y: { type: "number" },
|
|
656
|
-
button: { type: "string", enum: ["left", "right"], description: "default left" },
|
|
657
|
-
double: { type: "boolean", description: "double-click" }
|
|
658
|
-
},
|
|
659
|
-
required: ["x", "y"]
|
|
660
|
-
}
|
|
661
|
-
},
|
|
662
|
-
{
|
|
663
|
-
name: "type_text",
|
|
664
|
-
description: "Type text at the current focus and return the resulting screen.",
|
|
665
|
-
inputSchema: {
|
|
666
|
-
type: "object",
|
|
667
|
-
properties: { text: { type: "string" }, ...OBSERVE_PROPS },
|
|
668
|
-
required: ["text"]
|
|
669
|
-
}
|
|
670
|
-
},
|
|
671
|
-
{
|
|
672
|
-
name: "press_key",
|
|
673
|
-
description: 'Press a key or chord and return the resulting screen. xdotool syntax: "Return", "Tab", "ctrl+c", "alt+F4", "ctrl+shift+t".',
|
|
674
|
-
inputSchema: {
|
|
675
|
-
type: "object",
|
|
676
|
-
properties: { keys: { type: "string" }, ...OBSERVE_PROPS },
|
|
677
|
-
required: ["keys"]
|
|
678
|
-
}
|
|
679
|
-
},
|
|
680
|
-
{
|
|
681
|
-
name: "scroll",
|
|
682
|
-
description: "Scroll the screen up or down by N clicks and return the resulting screen.",
|
|
683
|
-
inputSchema: {
|
|
684
|
-
type: "object",
|
|
685
|
-
properties: {
|
|
686
|
-
direction: { type: "string", enum: ["up", "down"] },
|
|
687
|
-
clicks: { type: "number", description: "default 3" },
|
|
688
|
-
...OBSERVE_PROPS
|
|
689
|
-
},
|
|
690
|
-
required: ["direction"]
|
|
691
|
-
}
|
|
692
|
-
},
|
|
693
|
-
{
|
|
694
|
-
name: "computer_batch",
|
|
695
|
-
description: "Run several UI actions in ONE go and return the screen at the end \u2014 much faster than separate calls (one round trip, one screenshot). Use it for mechanical sequences you can predict without looking in between, e.g. click a field, type, Tab, type, press Return. Stop the batch before anything whose outcome you need to see first.",
|
|
696
|
-
inputSchema: {
|
|
697
|
-
type: "object",
|
|
698
|
-
properties: {
|
|
699
|
-
actions: {
|
|
700
|
-
type: "array",
|
|
701
|
-
description: "in order; each is {action: click|type_text|press_key|scroll|wait, ...its params}",
|
|
702
|
-
items: {
|
|
703
|
-
type: "object",
|
|
704
|
-
properties: {
|
|
705
|
-
action: { type: "string", enum: ["click", "type_text", "press_key", "scroll", "wait"] },
|
|
706
|
-
x: { type: "number" },
|
|
707
|
-
y: { type: "number" },
|
|
708
|
-
button: { type: "string", enum: ["left", "right"] },
|
|
709
|
-
double: { type: "boolean" },
|
|
710
|
-
text: { type: "string" },
|
|
711
|
-
keys: { type: "string" },
|
|
712
|
-
direction: { type: "string", enum: ["up", "down"] },
|
|
713
|
-
clicks: { type: "number" },
|
|
714
|
-
ms: { type: "number", description: "wait: milliseconds, max 5000" }
|
|
715
|
-
},
|
|
716
|
-
required: ["action"]
|
|
717
|
-
}
|
|
718
|
-
},
|
|
719
|
-
...OBSERVE_PROPS
|
|
720
|
-
},
|
|
721
|
-
required: ["actions"]
|
|
722
|
-
}
|
|
723
|
-
},
|
|
724
|
-
{
|
|
725
|
-
name: "computer_exec",
|
|
726
|
-
description: "Run a shell command on the bot's cloud computer (Linux, passwordless sudo, X11 desktop). Returns stdout/stderr/exit code \u2014 and, unlike the UI tools, no screenshot unless you ask for one.",
|
|
727
|
-
inputSchema: {
|
|
728
|
-
type: "object",
|
|
729
|
-
properties: {
|
|
730
|
-
command: { type: "string", maxLength: MAX_REMOTE_COMMAND_LENGTH },
|
|
731
|
-
observe: {
|
|
732
|
-
type: "boolean",
|
|
733
|
-
description: "default false \u2014 set true to also return a screenshot (e.g. after launching a GUI app)"
|
|
734
|
-
}
|
|
735
|
-
},
|
|
736
|
-
required: ["command"]
|
|
737
|
-
}
|
|
738
|
-
},
|
|
739
|
-
{
|
|
740
|
-
name: "wait_for",
|
|
741
|
-
description: "Wait on the bot's cloud computer until a condition holds, then return in ONE call instead of repeatedly polling with computer_exec or screenshots. Give only the fields needed by the chosen condition.",
|
|
742
|
-
inputSchema: {
|
|
743
|
-
type: "object",
|
|
744
|
-
properties: {
|
|
745
|
-
condition: {
|
|
746
|
-
type: "string",
|
|
747
|
-
enum: ["http_ready", "tcp_ready", "output_matches", "file_exists"],
|
|
748
|
-
description: "http_ready = a URL returns a successful response; tcp_ready = a local port accepts; output_matches = a bounded, read-only command's output matches a pattern; file_exists = a path appears"
|
|
749
|
-
},
|
|
750
|
-
url: { type: "string", description: "http_ready only: the URL to poll, e.g. http://localhost:3000/health" },
|
|
751
|
-
port: { type: "integer", description: "tcp_ready only: the local TCP port, e.g. 5432" },
|
|
752
|
-
command: {
|
|
753
|
-
type: "string",
|
|
754
|
-
description: "output_matches only: a quick, read-only shell command whose combined output is checked each poll"
|
|
755
|
-
},
|
|
756
|
-
pattern: { type: "string", description: "output_matches only: extended regex the output must match, e.g. ready|listening" },
|
|
757
|
-
path: { type: "string", description: "file_exists only: absolute path on the computer" },
|
|
758
|
-
timeout_seconds: { type: "integer", description: "give up after this many seconds; default 60, max 240" },
|
|
759
|
-
...OBSERVE_PROPS
|
|
760
|
-
},
|
|
761
|
-
required: ["condition"]
|
|
762
|
-
}
|
|
763
|
-
},
|
|
764
|
-
{
|
|
765
|
-
name: "open_url",
|
|
766
|
-
description: "Open a URL in the computer's own Chrome, verify the exact destination when DevTools is available, and return the resulting screen.",
|
|
767
|
-
inputSchema: {
|
|
768
|
-
type: "object",
|
|
769
|
-
properties: { url: { type: "string" }, ...OBSERVE_PROPS },
|
|
770
|
-
required: ["url"]
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
];
|
|
774
|
-
var shellQuote2 = (s) => `'${s.replace(/'/g, "'\\''")}'`;
|
|
775
|
-
var settleOf = (args) => Math.min(Math.max(Number(args?.settle_ms) || SETTLE_MS, 0), 3e3);
|
|
776
|
-
var wantsFrame = (args) => args?.observe !== false;
|
|
777
|
-
function actionShell(a) {
|
|
778
|
-
const kind = String(a?.action ?? "");
|
|
779
|
-
if (kind === "click") {
|
|
780
|
-
const x = Math.round(Number(a.x));
|
|
781
|
-
const y = Math.round(Number(a.y));
|
|
782
|
-
if (!Number.isFinite(x) || !Number.isFinite(y)) return { error: "click needs numeric x,y" };
|
|
783
|
-
const btn = a.button === "right" ? 3 : 1;
|
|
784
|
-
const rep = a.double ? "--repeat 2 --delay 60 " : "";
|
|
785
|
-
const button = a.button === "right" ? "right" : "left";
|
|
786
|
-
const count = a.double ? 2 : 1;
|
|
787
|
-
const fallback = `xdotool mousemove $CX $CY click ${rep}${btn}`;
|
|
788
|
-
const args = `$(printf '{"x":%s,"y":%s,"button":"${button}","count":${count},"scope":"desktop","session":"${REMOTE_CUA_SESSION}"}' "$CX" "$CY")`;
|
|
789
|
-
return `${scaled("CX", x)}; ${scaled("CY", y)}; CUA_ARGS=${args}; ${cuaOrX11("click", '"$CUA_ARGS"', fallback)}`;
|
|
790
|
-
}
|
|
791
|
-
if (kind === "type_text") {
|
|
792
|
-
const t = String(a.text ?? "");
|
|
793
|
-
if (!t) return { error: "type_text needs text" };
|
|
794
|
-
const cuaArgs = shellQuote2(JSON.stringify({ text: t, scope: "desktop", session: REMOTE_CUA_SESSION }));
|
|
795
|
-
return cuaOrX11("type_text", cuaArgs, `xdotool type --clearmodifiers --delay 8 -- ${shellQuote2(t)}`);
|
|
796
|
-
}
|
|
797
|
-
if (kind === "press_key") {
|
|
798
|
-
const keys = String(a.keys ?? "").replace(/[^\w+]/g, "");
|
|
799
|
-
if (!keys) return { error: "press_key needs keys" };
|
|
800
|
-
const parts = keys.split("+").filter(Boolean);
|
|
801
|
-
const tool = parts.length > 1 ? "hotkey" : "press_key";
|
|
802
|
-
const cuaArgs = shellQuote2(
|
|
803
|
-
JSON.stringify(
|
|
804
|
-
parts.length > 1 ? { keys: parts, scope: "desktop", session: REMOTE_CUA_SESSION } : { key: parts[0]?.toLowerCase(), scope: "desktop", session: REMOTE_CUA_SESSION }
|
|
805
|
-
)
|
|
806
|
-
);
|
|
807
|
-
return cuaOrX11(tool, cuaArgs, `xdotool key ${keys}`);
|
|
808
|
-
}
|
|
809
|
-
if (kind === "scroll") {
|
|
810
|
-
const clicks = Math.min(Math.max(Math.round(Number(a.clicks) || 3), 1), 20);
|
|
811
|
-
const btn = a.direction === "up" ? 4 : 5;
|
|
812
|
-
const direction = a.direction === "up" ? "up" : "down";
|
|
813
|
-
const fallback = `xdotool click --repeat ${clicks} ${btn}`;
|
|
814
|
-
const args = `$(printf '{"x":%s,"y":%s,"direction":"${direction}","amount":${clicks},"by":"line","scope":"desktop","session":"${REMOTE_CUA_SESSION}"}' "$((W / 2))" "$((H / 2))")`;
|
|
815
|
-
return `CUA_ARGS=${args}; ${cuaOrX11("scroll", '"$CUA_ARGS"', fallback)}`;
|
|
816
|
-
}
|
|
817
|
-
if (kind === "wait") {
|
|
818
|
-
const ms = Math.min(Math.max(Number(a.ms) || 500, 0), 5e3);
|
|
819
|
-
return `sleep ${(ms / 1e3).toFixed(2)}`;
|
|
820
|
-
}
|
|
821
|
-
return { error: `unknown action ${kind || "(missing)"}` };
|
|
822
|
-
}
|
|
823
|
-
async function actAndObserve(id, actions, note, args, timeoutMs = 6e4) {
|
|
824
|
-
const parts = [];
|
|
825
|
-
for (const a of actions) {
|
|
826
|
-
const shell = actionShell(a);
|
|
827
|
-
if (typeof shell !== "string") return text(id, shell.error, true);
|
|
828
|
-
if (parts.length) parts.push(`sleep ${(ACTION_GAP_MS / 1e3).toFixed(2)}`);
|
|
829
|
-
parts.push(shell);
|
|
830
|
-
}
|
|
831
|
-
observations.noteAction(actions.filter((action) => action?.action !== "wait").length);
|
|
832
|
-
const observe = wantsFrame(args);
|
|
833
|
-
const guarded = `if { ${parts.join("; ")}; }; then ACT=ok; else ACT=failed; fi`;
|
|
834
|
-
const command = [
|
|
835
|
-
ENV,
|
|
836
|
-
GEOMETRY,
|
|
837
|
-
ensureRemoteCuaCommand(),
|
|
838
|
-
guarded,
|
|
839
|
-
observe ? captureBlock(settleOf(args)) : "true",
|
|
840
|
-
'echo "ACT $ACT"'
|
|
841
|
-
].join("; ");
|
|
842
|
-
const out = await runOnBox(command, timeoutMs);
|
|
843
|
-
const acted = /^ACT ok$/m.test(out.stdout);
|
|
844
|
-
if (!acted && !out.stdout.includes("GEOM")) {
|
|
845
|
-
return text(
|
|
846
|
-
id,
|
|
847
|
-
`${note.replace(/^./, (c) => c.toLowerCase())} failed: ${out.stderr.slice(0, 200) || `exit ${out.exitCode}`}`,
|
|
848
|
-
true
|
|
849
|
-
);
|
|
850
|
-
}
|
|
851
|
-
const backend = automationSummary(out.stdout);
|
|
852
|
-
const full = acted ? `${note}
|
|
853
|
-
(${backend})` : `${note}
|
|
854
|
-
(the action reported an error: ${out.stderr.slice(0, 160) || "no detail"}; ${backend})`;
|
|
855
|
-
if (!observe) return text(id, full, !acted);
|
|
856
|
-
return observed(id, full, await frameFrom(out));
|
|
857
|
-
}
|
|
858
|
-
async function semanticActAndObserve(id, action, ref, value, args) {
|
|
859
|
-
if (!semanticBrowserUrl || !semanticBrowserRefs.has(ref)) {
|
|
860
|
-
return text(id, "that browser ref is stale or unknown \u2014 take a new browser_snapshot", true);
|
|
861
|
-
}
|
|
862
|
-
const observe = wantsFrame(args);
|
|
863
|
-
const semantic = semanticBrowserCommand(action, {
|
|
864
|
-
ref,
|
|
865
|
-
...action === "fill" ? { text: value ?? "" } : {},
|
|
866
|
-
url: semanticBrowserUrl
|
|
867
|
-
});
|
|
868
|
-
const guarded = `if ${semantic}; then SEM=ok; else SEM=failed; fi`;
|
|
869
|
-
const command = [
|
|
870
|
-
ENV,
|
|
871
|
-
GEOMETRY,
|
|
872
|
-
guarded,
|
|
873
|
-
ensureRemoteCuaCommand(),
|
|
874
|
-
observe ? captureBlock(settleOf(args)) : "true",
|
|
875
|
-
'echo "SEM $SEM"'
|
|
876
|
-
].join("; ");
|
|
877
|
-
observations.noteAction();
|
|
878
|
-
const out = await runOnBox(command, action === "fill" ? 12e4 : 6e4);
|
|
879
|
-
const acted = /^SEM ok$/m.test(out.stdout);
|
|
880
|
-
semanticBrowserRefs.clear();
|
|
881
|
-
const note = acted ? action === "fill" ? `filled ${ref} with ${value?.length ?? 0} chars (trusted Chrome DevTools input)` : `clicked ${ref} (trusted Chrome DevTools input)` : `${action} ${ref} failed: ${out.stderr.slice(0, 200) || "the page changed; take a new browser_snapshot"}`;
|
|
882
|
-
if (!observe) return text(id, note, !acted);
|
|
883
|
-
return observed(id, note, await frameFrom(out));
|
|
884
|
-
}
|
|
885
|
-
var OPEN_WHILE_DRIVEN = /* @__PURE__ */ new Set(["computer_request_help", "computer_status", "observation_metrics"]);
|
|
886
|
-
async function call(id, name, args) {
|
|
887
|
-
if (!OPEN_WHILE_DRIVEN.has(name)) {
|
|
888
|
-
const state = await control.state(true);
|
|
889
|
-
if (state.held) return text(id, state.blockedReason ?? CONTROL_REFUSAL, true);
|
|
890
|
-
}
|
|
891
|
-
if (name === "computer_request_help") {
|
|
892
|
-
if (!control.configured) {
|
|
893
|
-
return text(id, "nobody can be paged for this computer right now \u2014 carry on carefully", true);
|
|
894
|
-
}
|
|
895
|
-
const initial = await control.state(true);
|
|
896
|
-
if (initial.held && initial.blockedReason) return text(id, initial.blockedReason, true);
|
|
897
|
-
const requestId = initial.held ? null : await control.requestHelp(String(args?.reason ?? ""));
|
|
898
|
-
if (!initial.held && requestId === null) {
|
|
899
|
-
return text(id, "The person could not be paged for this computer right now. Carry on carefully or tell them in chat.", true);
|
|
900
|
-
}
|
|
901
|
-
let sawHold = initial.held;
|
|
902
|
-
const deadline = Date.now() + CONTROL_WAIT_MS;
|
|
903
|
-
while (Date.now() < deadline) {
|
|
904
|
-
await new Promise((resolve) => setTimeout(resolve, CONTROL_POLL_MS));
|
|
905
|
-
const state = await control.state(true);
|
|
906
|
-
if (state.held && state.blockedReason) {
|
|
907
|
-
if (requestId) await control.expireHelp(requestId);
|
|
908
|
-
return text(id, state.blockedReason, true);
|
|
909
|
-
}
|
|
910
|
-
if (state.held) sawHold = true;
|
|
911
|
-
if (!state.held && !state.helpOpen) {
|
|
912
|
-
return text(
|
|
913
|
-
id,
|
|
914
|
-
sawHold ? "The person has finished driving and handed control back. The screen may have changed while they drove \u2014 take a fresh screenshot before your next action." : "The person saw your request and dismissed it without taking control. Carry on yourself."
|
|
915
|
-
);
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
if (requestId) await control.expireHelp(requestId);
|
|
919
|
-
return text(
|
|
920
|
-
id,
|
|
921
|
-
"Nobody took control within the wait window. Carry on carefully, or ask again if you are truly stuck.",
|
|
922
|
-
true
|
|
923
|
-
);
|
|
924
|
-
}
|
|
925
|
-
if (name === "screenshot") {
|
|
926
|
-
let crop = null;
|
|
927
|
-
if (args.region !== void 0) {
|
|
928
|
-
const bounds = await observationBounds();
|
|
929
|
-
if (!bounds) return text(id, "crop unavailable: could not determine the screenshot dimensions", true);
|
|
930
|
-
crop = normalizeCrop(args.region, bounds.width, bounds.height);
|
|
931
|
-
if (!crop) {
|
|
932
|
-
return text(
|
|
933
|
-
id,
|
|
934
|
-
`region must be at least 32\xD732 and stay within the ${bounds.width}\xD7${bounds.height} screenshot`,
|
|
935
|
-
true
|
|
936
|
-
);
|
|
937
|
-
}
|
|
938
|
-
}
|
|
939
|
-
const out = await runOnBox([ENV, GEOMETRY, ensureRemoteCuaCommand(), captureBlock(0, crop)].join("; "), 6e4);
|
|
940
|
-
if (/CROP_FAILED/.test(out.stdout)) {
|
|
941
|
-
return text(id, `crop failed: ${out.stderr.slice(0, 200) || "ImageMagick could not create the requested region"}`, true);
|
|
942
|
-
}
|
|
943
|
-
const frame = await frameFrom(out);
|
|
944
|
-
if (!frame) {
|
|
945
|
-
return text(id, `screenshot failed: ${out.stderr.slice(0, 200) || "capture produced no frame"}`, true);
|
|
946
|
-
}
|
|
947
|
-
return observed(id, crop ? "cropped screen captured" : "screen captured", frame, crop, false);
|
|
948
|
-
}
|
|
949
|
-
if (name === "browser_state") {
|
|
950
|
-
const targets = await browserTargets();
|
|
951
|
-
return text(
|
|
952
|
-
id,
|
|
953
|
-
targets.length ? `Structured browser state:
|
|
954
|
-
${targets.map((target) => `- ${target.title || "Untitled"}: ${target.url}`).join("\n")}` : "Structured browser state unavailable. Use screenshot only if visual state is necessary."
|
|
955
|
-
);
|
|
956
|
-
}
|
|
957
|
-
if (name === "browser_snapshot") {
|
|
958
|
-
const out = await runOnBox(semanticBrowserCommand("snapshot", {}), 2e4);
|
|
959
|
-
if (!out.ok) {
|
|
960
|
-
semanticBrowserUrl = null;
|
|
961
|
-
semanticBrowserRefs.clear();
|
|
962
|
-
return text(id, "Semantic browser state is unavailable. Open Chrome with open_url, or use screenshot.", true);
|
|
963
|
-
}
|
|
964
|
-
try {
|
|
965
|
-
const snapshot = JSON.parse(out.stdout);
|
|
966
|
-
if (!Array.isArray(snapshot.elements) || typeof snapshot.url !== "string") throw new Error("invalid snapshot");
|
|
967
|
-
semanticBrowserUrl = snapshot.url;
|
|
968
|
-
semanticBrowserRefs = new Set(snapshot.elements.map((element) => element.ref));
|
|
969
|
-
observations.noteStructuredObservation();
|
|
970
|
-
const publicUrl = safeBrowserUrl(snapshot.url) ?? "URL unavailable";
|
|
971
|
-
const lines = snapshot.elements.map(
|
|
972
|
-
(element) => `- [${element.ref}] ${element.role}${element.disabled ? " disabled" : ""}: ${element.name.replace(/\s+/g, " ").slice(0, 180)}`
|
|
973
|
-
);
|
|
974
|
-
return text(
|
|
975
|
-
id,
|
|
976
|
-
`Semantic browser snapshot \u2014 ${snapshot.title || "Untitled"}: ${publicUrl}
|
|
977
|
-
${lines.join("\n") || "No interactive elements found."}`
|
|
978
|
-
);
|
|
979
|
-
} catch {
|
|
980
|
-
semanticBrowserUrl = null;
|
|
981
|
-
semanticBrowserRefs.clear();
|
|
982
|
-
return text(id, "Chrome returned an invalid semantic snapshot; use screenshot.", true);
|
|
983
|
-
}
|
|
984
|
-
}
|
|
985
|
-
if (name === "browser_click") {
|
|
986
|
-
const ref = String(args.ref ?? "");
|
|
987
|
-
return semanticActAndObserve(id, "click", ref, void 0, args);
|
|
988
|
-
}
|
|
989
|
-
if (name === "browser_fill") {
|
|
990
|
-
const ref = String(args.ref ?? "");
|
|
991
|
-
return semanticActAndObserve(id, "fill", ref, String(args.text ?? ""), args);
|
|
992
|
-
}
|
|
993
|
-
if (name === "wait_for_navigation") {
|
|
994
|
-
const url = String(args.url ?? "");
|
|
995
|
-
const publicUrl = safeBrowserUrl(url);
|
|
996
|
-
if (!normalizeBrowserUrl(url) || !publicUrl) {
|
|
997
|
-
observations.noteVerification(false);
|
|
998
|
-
return text(id, "wait_for_navigation needs a valid http(s) URL", true);
|
|
999
|
-
}
|
|
1000
|
-
const result = await waitForNavigation(url);
|
|
1001
|
-
return text(
|
|
1002
|
-
id,
|
|
1003
|
-
result.ok ? `navigation verified: ${publicUrl}` : `navigation not verified after 3 checks. Current structured state: ${result.targets.map((target) => target.url).join(", ") || "unavailable"}. Use screenshot only if needed.`,
|
|
1004
|
-
!result.ok
|
|
1005
|
-
);
|
|
1006
|
-
}
|
|
1007
|
-
if (name === "observation_metrics") return text(id, metricsText());
|
|
1008
|
-
if (name === "computer_status") {
|
|
1009
|
-
const command = [
|
|
1010
|
-
ENV,
|
|
1011
|
-
ensureRemoteCuaCommand(),
|
|
1012
|
-
`if [ -x ${REMOTE_CUA_EXECUTABLE} ] && ${REMOTE_CUA_EXECUTABLE} status --socket ${REMOTE_CUA_SOCKET} >/dev/null 2>&1; then`,
|
|
1013
|
-
` echo "CUA $(${REMOTE_CUA_EXECUTABLE} --version)"`,
|
|
1014
|
-
` env ${CUA_ENV} ${REMOTE_CUA_EXECUTABLE} call health_report '{}' --socket ${REMOTE_CUA_SOCKET} 2>/dev/null || true`,
|
|
1015
|
-
"else echo 'X11 fallback'; fi"
|
|
1016
|
-
].join("\n");
|
|
1017
|
-
const out = await runOnBox(command, 2e4);
|
|
1018
|
-
if (!/^CUA /m.test(out.stdout)) {
|
|
1019
|
-
return text(id, "Cloud computer automation: X11 fallback (Cua Driver is still installing or needs repair).", true);
|
|
1020
|
-
}
|
|
1021
|
-
const overall = out.stdout.match(/"overall"\s*:\s*"(ok|degraded|failed)"/)?.[1] ?? "unknown";
|
|
1022
|
-
return text(id, `Cloud computer automation: Cua Driver ${REMOTE_CUA_VERSION} (${overall}).`);
|
|
1023
|
-
}
|
|
1024
|
-
if (name === "click") {
|
|
1025
|
-
const x = Math.round(Number(args.x));
|
|
1026
|
-
const y = Math.round(Number(args.y));
|
|
1027
|
-
if (!Number.isFinite(x) || !Number.isFinite(y)) return text(id, "click needs numeric x,y", true);
|
|
1028
|
-
const what = `${args.double ? "double-clicked" : args.button === "right" ? "right-clicked" : "clicked"} ${x},${y}`;
|
|
1029
|
-
return actAndObserve(id, [{ ...args, action: "click" }], what, args);
|
|
1030
|
-
}
|
|
1031
|
-
if (name === "type_text") {
|
|
1032
|
-
const t = String(args.text ?? "");
|
|
1033
|
-
if (!t) return text(id, "nothing to type", true);
|
|
1034
|
-
return actAndObserve(id, [{ action: "type_text", text: t }], `typed ${t.length} chars`, args, 12e4);
|
|
1035
|
-
}
|
|
1036
|
-
if (name === "press_key") {
|
|
1037
|
-
const keys = String(args.keys ?? "").replace(/[^\w+]/g, "");
|
|
1038
|
-
if (!keys) return text(id, "press_key needs keys", true);
|
|
1039
|
-
return actAndObserve(id, [{ action: "press_key", keys }], `pressed ${keys}`, args);
|
|
1040
|
-
}
|
|
1041
|
-
if (name === "scroll") {
|
|
1042
|
-
const clicks = Math.min(Math.max(Math.round(Number(args.clicks) || 3), 1), 20);
|
|
1043
|
-
const direction = args.direction === "up" ? "up" : "down";
|
|
1044
|
-
return actAndObserve(id, [{ action: "scroll", direction, clicks }], `scrolled ${direction} ${clicks}`, args);
|
|
1045
|
-
}
|
|
1046
|
-
if (name === "computer_batch") {
|
|
1047
|
-
const actions = Array.isArray(args.actions) ? args.actions.slice(0, 24) : [];
|
|
1048
|
-
if (!actions.length) return text(id, "computer_batch needs a non-empty actions array", true);
|
|
1049
|
-
const summary = actions.map(
|
|
1050
|
-
(a) => a.action === "click" ? `click ${Math.round(Number(a.x))},${Math.round(Number(a.y))}` : a.action === "type_text" ? `type ${String(a.text ?? "").length} chars` : a.action === "press_key" ? `key ${a.keys}` : a.action === "scroll" ? `scroll ${a.direction ?? "down"}` : `wait ${Math.min(Number(a.ms) || 500, 5e3)}ms`
|
|
1051
|
-
).join(" \u2192 ");
|
|
1052
|
-
return actAndObserve(id, actions, `ran ${actions.length} actions: ${summary}`, args, 18e4);
|
|
1053
|
-
}
|
|
1054
|
-
if (name === "computer_exec") {
|
|
1055
|
-
const command = String(args.command ?? "");
|
|
1056
|
-
if (command.length > MAX_REMOTE_COMMAND_LENGTH) {
|
|
1057
|
-
return text(id, `command is too long (maximum ${MAX_REMOTE_COMMAND_LENGTH} characters)`, true);
|
|
1058
|
-
}
|
|
1059
|
-
observations.noteAction();
|
|
1060
|
-
const out = await runOnBox(command, 12e4);
|
|
1061
|
-
const note = `exit ${out.exitCode}
|
|
1062
|
-
${out.stdout.slice(-6e3)}${out.stderr ? `
|
|
1063
|
-
[stderr]
|
|
1064
|
-
${out.stderr.slice(-2e3)}` : ""}`;
|
|
1065
|
-
if (args.observe !== true) return text(id, note);
|
|
1066
|
-
const shot = await runOnBox([ENV, GEOMETRY, ensureRemoteCuaCommand(), captureBlock()].join("; "), 6e4);
|
|
1067
|
-
return observed(id, note, await frameFrom(shot));
|
|
1068
|
-
}
|
|
1069
|
-
if (name === "wait_for") {
|
|
1070
|
-
const condition = String(args.condition ?? "").trim().toLowerCase();
|
|
1071
|
-
const timeout = Math.min(Math.max(Math.trunc(Number(args.timeout_seconds) || 60), 1), 240);
|
|
1072
|
-
let check = "";
|
|
1073
|
-
let label = "";
|
|
1074
|
-
if (condition === "http_ready") {
|
|
1075
|
-
const url = String(args.url ?? "").trim();
|
|
1076
|
-
if (!/^https?:\/\//i.test(url)) {
|
|
1077
|
-
return text(id, 'http_ready needs "url", e.g. {"condition":"http_ready","url":"http://localhost:3000/health"}.', true);
|
|
1078
|
-
}
|
|
1079
|
-
check = `curl -fsS -o /dev/null --connect-timeout 1 --max-time 1 ${shellQuote2(url)}`;
|
|
1080
|
-
label = url;
|
|
1081
|
-
} else if (condition === "tcp_ready") {
|
|
1082
|
-
const portNumber = Math.trunc(Number(args.port));
|
|
1083
|
-
if (!Number.isInteger(portNumber) || portNumber < 1 || portNumber > 65535) {
|
|
1084
|
-
return text(id, 'tcp_ready needs "port" 1-65535, e.g. {"condition":"tcp_ready","port":5432}.', true);
|
|
1085
|
-
}
|
|
1086
|
-
check = `timeout --kill-after=1s 1s bash -c ${shellQuote2(`echo > /dev/tcp/127.0.0.1/${portNumber}`)} 2>/dev/null`;
|
|
1087
|
-
label = `port ${portNumber}`;
|
|
1088
|
-
} else if (condition === "output_matches") {
|
|
1089
|
-
const probe = String(args.command ?? "").slice(0, 2e3);
|
|
1090
|
-
const pattern = String(args.pattern ?? "").slice(0, 500);
|
|
1091
|
-
if (!probe.trim() || !pattern.trim()) {
|
|
1092
|
-
return text(id, 'output_matches needs "command" and "pattern", e.g. {"condition":"output_matches","command":"tail -1 /tmp/build.log","pattern":"done|failed"}.', true);
|
|
1093
|
-
}
|
|
1094
|
-
check = `timeout --kill-after=1s 1s bash -c ${shellQuote2(probe)} 2>&1 | grep -Eq -- ${shellQuote2(pattern)}`;
|
|
1095
|
-
label = `/${pattern}/ in ${probe.slice(0, 80)}`;
|
|
1096
|
-
} else if (condition === "file_exists") {
|
|
1097
|
-
const target = String(args.path ?? "").trim();
|
|
1098
|
-
if (!target.startsWith("/")) {
|
|
1099
|
-
return text(id, 'file_exists needs an absolute "path", e.g. {"condition":"file_exists","path":"/tmp/render.done"}.', true);
|
|
1100
|
-
}
|
|
1101
|
-
check = `[ -e ${shellQuote2(target)} ]`;
|
|
1102
|
-
label = target;
|
|
1103
|
-
} else {
|
|
1104
|
-
return text(id, 'wait_for needs "condition": http_ready, tcp_ready, output_matches, or file_exists.', true);
|
|
1105
|
-
}
|
|
1106
|
-
const loop = [
|
|
1107
|
-
"MET=no",
|
|
1108
|
-
"START=$SECONDS",
|
|
1109
|
-
`END=$((SECONDS+${timeout}))`,
|
|
1110
|
-
`while [ "$SECONDS" -lt "$END" ]; do if ${check}; then MET=yes; break; fi; REMAIN=$((END-SECONDS)); [ "$REMAIN" -le 0 ] && break; [ "$REMAIN" -lt 2 ] && sleep "$REMAIN" || sleep 2; done`,
|
|
1111
|
-
'echo "WAIT_RESULT $MET ELAPSED $((SECONDS-START))"'
|
|
1112
|
-
].join("; ");
|
|
1113
|
-
observations.noteAction();
|
|
1114
|
-
const out = await runOnBox(loop, (timeout + 15) * 1e3);
|
|
1115
|
-
const marker = out.stdout.match(/^WAIT_RESULT (yes|no) ELAPSED (\d+)$/m);
|
|
1116
|
-
if (!out.ok || !marker) {
|
|
1117
|
-
const detail = out.stderr.slice(0, 300) || `exit ${out.exitCode ?? "unknown"}`;
|
|
1118
|
-
return text(id, `wait_for could not check ${label}: ${detail}`, true);
|
|
1119
|
-
}
|
|
1120
|
-
const met = marker[1] === "yes";
|
|
1121
|
-
const elapsed = marker[2];
|
|
1122
|
-
const note = met ? `condition met: ${label} (~${elapsed}s)` : `timed out after ${timeout}s waiting for ${label} \u2014 inspect with computer_exec (logs, process list) before waiting again.`;
|
|
1123
|
-
return text(id, note, !met);
|
|
1124
|
-
}
|
|
1125
|
-
if (name === "open_url") {
|
|
1126
|
-
const url = String(args.url ?? "");
|
|
1127
|
-
const normalized = normalizeBrowserUrl(url);
|
|
1128
|
-
const publicUrl = safeBrowserUrl(url);
|
|
1129
|
-
if (!normalized || !publicUrl) return text(id, "only valid http(s) URLs", true);
|
|
1130
|
-
const q = shellQuote2(normalized);
|
|
1131
|
-
const observe = wantsFrame(args);
|
|
1132
|
-
const command = [
|
|
1133
|
-
ENV,
|
|
1134
|
-
GEOMETRY,
|
|
1135
|
-
CHROME_PROFILE_SETUP,
|
|
1136
|
-
`(google-chrome ${CHROME_DEBUG_FLAGS} ${q} || chromium ${CHROME_DEBUG_FLAGS} ${q} || chromium-browser ${CHROME_DEBUG_FLAGS} ${q} || xdg-open ${q}) >/dev/null 2>&1 &`,
|
|
1137
|
-
'for i in 1 2 3 4 5 6 7 8 9 10 11 12; do xdotool search --onlyvisible --class "chrom" >/dev/null 2>&1 && break; sleep 0.25; done',
|
|
1138
|
-
ensureRemoteCuaCommand(),
|
|
1139
|
-
observe ? captureBlock(600) : "true"
|
|
1140
|
-
].join("; ");
|
|
1141
|
-
observations.noteAction();
|
|
1142
|
-
const out = await runOnBox(command, 6e4);
|
|
1143
|
-
const verification = await waitForNavigation(normalized, 1);
|
|
1144
|
-
const current = verification.targets.map((target) => target.url).join(", ") || "unavailable";
|
|
1145
|
-
const note = verification.ok ? `opened and navigation verified: ${publicUrl}` : `opened ${publicUrl}, but the exact destination was not verified. Current structured state: ${current}`;
|
|
1146
|
-
if (!observe) return text(id, note);
|
|
1147
|
-
return observed(id, note, await frameFrom(out));
|
|
1148
|
-
}
|
|
1149
|
-
return text(id, `unknown tool ${name}`, true);
|
|
1150
|
-
}
|
|
1151
|
-
async function handle(msg) {
|
|
1152
|
-
if (msg.method === "initialize") {
|
|
1153
|
-
return send({
|
|
1154
|
-
jsonrpc: "2.0",
|
|
1155
|
-
id: msg.id,
|
|
1156
|
-
result: {
|
|
1157
|
-
protocolVersion: msg.params?.protocolVersion ?? "2024-11-05",
|
|
1158
|
-
capabilities: { tools: {} },
|
|
1159
|
-
serverInfo: { name: "openmausbot-computer", version: "3" }
|
|
1160
|
-
}
|
|
1161
|
-
});
|
|
1162
|
-
}
|
|
1163
|
-
if (msg.method === "tools/list") return send({ jsonrpc: "2.0", id: msg.id, result: { tools: TOOLS } });
|
|
1164
|
-
if (msg.method === "tools/call") {
|
|
1165
|
-
try {
|
|
1166
|
-
return await call(msg.id, msg.params?.name, msg.params?.arguments ?? {});
|
|
1167
|
-
} catch (e) {
|
|
1168
|
-
const error = e instanceof Error ? e : new Error(String(e));
|
|
1169
|
-
const timedOut = error.name === "TimeoutError" || /timed?\s*out|timeout/i.test(error.message);
|
|
1170
|
-
return text(
|
|
1171
|
-
msg.id,
|
|
1172
|
-
timedOut ? "computer tool timed out. The action may or may not have completed; take a screenshot to inspect the current state before retrying it." : `computer tool failed: ${error.message}`,
|
|
1173
|
-
true
|
|
1174
|
-
);
|
|
1175
|
-
}
|
|
1176
|
-
}
|
|
1177
|
-
if (String(msg.method ?? "").startsWith("notifications/")) return;
|
|
1178
|
-
if (msg.id != null) {
|
|
1179
|
-
send({ jsonrpc: "2.0", id: msg.id, error: { code: -32601, message: `method not found: ${msg.method}` } });
|
|
1180
|
-
}
|
|
1181
|
-
}
|
|
1182
|
-
var buf = "";
|
|
1183
|
-
process.stdin.on("data", (chunk) => {
|
|
1184
|
-
buf += chunk;
|
|
1185
|
-
let nl;
|
|
1186
|
-
while ((nl = buf.indexOf("\n")) !== -1) {
|
|
1187
|
-
const line = buf.slice(0, nl);
|
|
1188
|
-
buf = buf.slice(nl + 1);
|
|
1189
|
-
if (!line.trim()) continue;
|
|
1190
|
-
try {
|
|
1191
|
-
void handle(JSON.parse(line));
|
|
1192
|
-
} catch {
|
|
1193
|
-
}
|
|
1194
|
-
}
|
|
1195
|
-
});
|
|
1196
|
-
process.stdin.on("end", () => process.exit(0));
|