@instaclaw/dispatch 0.6.2 → 0.7.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/dist/connection.js +61 -1
- package/dist/executor.d.ts +2 -0
- package/dist/executor.js +72 -0
- package/dist/index.js +40 -7
- package/dist/own-pages.d.ts +3 -0
- package/dist/own-pages.js +46 -0
- package/dist/permissions.js +1 -1
- package/dist/scope.d.ts +51 -0
- package/dist/scope.js +88 -0
- package/dist/screenshot.d.ts +2 -0
- package/dist/screenshot.js +1 -1
- package/dist/supervisor.d.ts +2 -0
- package/dist/supervisor.js +118 -29
- package/package.json +2 -2
package/dist/connection.js
CHANGED
|
@@ -15,9 +15,69 @@ export function connect(opts) {
|
|
|
15
15
|
const url = buildAuthUrl(opts.vmAddress, opts.port, opts.gatewayToken);
|
|
16
16
|
console.log(` Connecting to ${opts.vmAddress}:${opts.port}...`);
|
|
17
17
|
ws = new WebSocket(url, {
|
|
18
|
-
rejectUnauthorized
|
|
18
|
+
// rejectUnauthorized stays false ON PURPOSE and is NOT the defect.
|
|
19
|
+
//
|
|
20
|
+
// The VM generates its own certificate with CN=dispatch-server (see
|
|
21
|
+
// dispatch-server.js), so ordinary hostname verification could never pass:
|
|
22
|
+
// we connect by IP address, and no CA has ever vouched for that name. Turning
|
|
23
|
+
// this to true without shipping something else in its place would simply break
|
|
24
|
+
// every connection.
|
|
25
|
+
//
|
|
26
|
+
// The real defect was that the TOFU half was designed and never built:
|
|
27
|
+
// certFingerprint and onCertFingerprint were declared in this file, threaded
|
|
28
|
+
// through from index.ts:180 and :206, and then never computed, never invoked,
|
|
29
|
+
// and never compared. So the transport accepted ANY certificate, forever,
|
|
30
|
+
// including an attacker's. That is what the block below closes.
|
|
31
|
+
rejectUnauthorized: false,
|
|
19
32
|
handshakeTimeout: 10000,
|
|
20
33
|
});
|
|
34
|
+
// ── Trust On First Use, the half that was missing ──────────────────────────
|
|
35
|
+
// On the first connection we record the peer's certificate fingerprint. On every
|
|
36
|
+
// later connection we compare. A changed fingerprint means the machine answering
|
|
37
|
+
// on this address is not the machine we paired with, which is exactly the
|
|
38
|
+
// dangling-DNS and address-reassignment case this fleet has already been shown to
|
|
39
|
+
// produce.
|
|
40
|
+
//
|
|
41
|
+
// SHIPS UNARMED: mismatch WARNS by default and only refuses when
|
|
42
|
+
// INSTACLAW_DISPATCH_PIN_ENFORCE is set. Enforcement is a separate word, because
|
|
43
|
+
// turning it on before every paired client has recorded a fingerprint would lock
|
|
44
|
+
// people out of their own machines.
|
|
45
|
+
ws.on("upgrade", (res) => {
|
|
46
|
+
try {
|
|
47
|
+
const sock = res.socket;
|
|
48
|
+
if (typeof sock.getPeerCertificate !== "function")
|
|
49
|
+
return;
|
|
50
|
+
const peer = sock.getPeerCertificate();
|
|
51
|
+
const fp = peer && peer.fingerprint256;
|
|
52
|
+
if (!fp)
|
|
53
|
+
return;
|
|
54
|
+
if (!opts.certFingerprint) {
|
|
55
|
+
// First use: adopt and persist via the callback index.ts already supplies.
|
|
56
|
+
opts.onCertFingerprint?.(fp);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (opts.certFingerprint === fp)
|
|
60
|
+
return;
|
|
61
|
+
const msg = `certificate fingerprint CHANGED for ${opts.vmAddress}.\n` +
|
|
62
|
+
` expected ${opts.certFingerprint}\n` +
|
|
63
|
+
` received ${fp}\n` +
|
|
64
|
+
` The machine answering on this address is not the one you paired with.`;
|
|
65
|
+
if (process.env.INSTACLAW_DISPATCH_PIN_ENFORCE) {
|
|
66
|
+
console.error(` REFUSING: ${msg}`);
|
|
67
|
+
try {
|
|
68
|
+
ws?.close(1008, "cert fingerprint mismatch");
|
|
69
|
+
}
|
|
70
|
+
catch { /* closing anyway */ }
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
console.warn(` WARNING: ${msg}\n Set INSTACLAW_DISPATCH_PIN_ENFORCE=1 to refuse instead of warn.`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// Never let the pin check break a working connection. It is a check, not a
|
|
78
|
+
// dependency.
|
|
79
|
+
}
|
|
80
|
+
});
|
|
21
81
|
ws.on("open", () => {
|
|
22
82
|
reconnectDelay = 1000; // Reset backoff
|
|
23
83
|
console.log(` Connected to agent on ${opts.vmAddress}`);
|
package/dist/executor.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { DispatchCommand } from "./types.js";
|
|
2
|
+
/** Call once at start, before any command. Throws (the client must not start) on a start refusal. */
|
|
3
|
+
export declare function initScope(): Promise<boolean>;
|
|
2
4
|
export interface ExecutionResult {
|
|
3
5
|
success: boolean;
|
|
4
6
|
data?: Record<string, unknown>;
|
package/dist/executor.js
CHANGED
|
@@ -4,6 +4,30 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { execSync, exec as execCb } from "child_process";
|
|
6
6
|
import { captureScreenshot } from "./screenshot.js";
|
|
7
|
+
import { commandNamesOwnPage, OWN_PAGE_REFUSAL } from "./own-pages.js";
|
|
8
|
+
import { Scope, loadScopeConfig } from "./scope.js";
|
|
9
|
+
// PASS 43 SCOPED MODE (scope.ts, PRD 43.11): null unless INSTACLAW_DISPATCH_SCOPE_FILE is set.
|
|
10
|
+
let scope = null;
|
|
11
|
+
async function liveWindows() { return (await (await getUC()).windowList()); }
|
|
12
|
+
/** Call once at start, before any command. Throws (the client must not start) on a start refusal. */
|
|
13
|
+
export async function initScope() {
|
|
14
|
+
const cfg = loadScopeConfig();
|
|
15
|
+
if (!cfg)
|
|
16
|
+
return false;
|
|
17
|
+
const base = await liveWindows();
|
|
18
|
+
const why = Scope.startRefusal(cfg, base);
|
|
19
|
+
if (why)
|
|
20
|
+
throw new Error(`scoped mode refused to start: ${why}`);
|
|
21
|
+
scope = new Scope(cfg, base);
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
/** The kill point: the refusal goes back to the agent, then the client exits (no retry in another form). */
|
|
25
|
+
function scopeRefused(v, what) {
|
|
26
|
+
const msg = scope.refuse(v, what);
|
|
27
|
+
console.log(` SCOPE REFUSED: ${v.why}; stopping (would have touched: ${v.wouldTouch})`);
|
|
28
|
+
setTimeout(() => process.exit(3), 500);
|
|
29
|
+
return { success: false, error: msg };
|
|
30
|
+
}
|
|
7
31
|
// Dynamic import for usecomputer (ESM)
|
|
8
32
|
let uc = null;
|
|
9
33
|
// Track the most recent coordMap from screenshots for click mapping
|
|
@@ -31,10 +55,22 @@ export async function executeCommand(command) {
|
|
|
31
55
|
case "drag":
|
|
32
56
|
return await doDrag(params);
|
|
33
57
|
case "windows":
|
|
58
|
+
if (scope)
|
|
59
|
+
return { success: true, data: { windows: scope.windows(await liveWindows()) } };
|
|
34
60
|
return await listWindows();
|
|
35
61
|
case "batch":
|
|
36
62
|
return await executeBatch(params);
|
|
37
63
|
case "exec":
|
|
64
|
+
// THE OWN-PAGES REFUSAL (own-pages.ts). Closes the exec vector only; read that file for
|
|
65
|
+
// what it deliberately does NOT close.
|
|
66
|
+
if (scope) {
|
|
67
|
+
const v = scope.exec(String(params?.command ?? command.command ?? ""));
|
|
68
|
+
if (!v.ok)
|
|
69
|
+
return scopeRefused(v, "exec");
|
|
70
|
+
}
|
|
71
|
+
if (commandNamesOwnPage(String(command.command ?? ""))) {
|
|
72
|
+
throw new Error(OWN_PAGE_REFUSAL);
|
|
73
|
+
}
|
|
38
74
|
return await doExec(params);
|
|
39
75
|
default:
|
|
40
76
|
return { success: false, error: `Unknown command type: ${type}` };
|
|
@@ -121,7 +157,15 @@ async function executeBatch(params) {
|
|
|
121
157
|
return batchResult;
|
|
122
158
|
}
|
|
123
159
|
async function takeScreenshot(params) {
|
|
160
|
+
let windowId;
|
|
161
|
+
if (scope) {
|
|
162
|
+
const v = scope.screenshot(await liveWindows());
|
|
163
|
+
if (!v.ok)
|
|
164
|
+
return scopeRefused(v, "screenshot");
|
|
165
|
+
windowId = v.windowId;
|
|
166
|
+
}
|
|
124
167
|
const capture = await captureScreenshot({
|
|
168
|
+
windowId,
|
|
125
169
|
format: params.format || "webp",
|
|
126
170
|
quality: Number(params.quality) || 55,
|
|
127
171
|
maxWidth: Number(params.maxWidth) || 1280,
|
|
@@ -158,12 +202,22 @@ async function doClick(params) {
|
|
|
158
202
|
// If mapping fails, fall back to raw coords
|
|
159
203
|
}
|
|
160
204
|
}
|
|
205
|
+
if (scope) {
|
|
206
|
+
const v = scope.point(await liveWindows(), point, "a click");
|
|
207
|
+
if (!v.ok)
|
|
208
|
+
return scopeRefused(v, "click");
|
|
209
|
+
}
|
|
161
210
|
await usecomputer.click({ point, button: "left", count: 1 });
|
|
162
211
|
return { success: true, data: { action: "click", x, y, mappedX: point.x, mappedY: point.y, coordMap: coordMap || "none" } };
|
|
163
212
|
}
|
|
164
213
|
async function doType(params) {
|
|
165
214
|
const usecomputer = await getUC();
|
|
166
215
|
const text = String(params.text);
|
|
216
|
+
if (scope) {
|
|
217
|
+
const v = scope.keys(await liveWindows(), "typing");
|
|
218
|
+
if (!v.ok)
|
|
219
|
+
return scopeRefused(v, "type");
|
|
220
|
+
}
|
|
167
221
|
try {
|
|
168
222
|
await usecomputer.typeText({ text, delayMs: null });
|
|
169
223
|
}
|
|
@@ -181,6 +235,11 @@ async function doType(params) {
|
|
|
181
235
|
async function doPress(params) {
|
|
182
236
|
const usecomputer = await getUC();
|
|
183
237
|
const key = String(params.key);
|
|
238
|
+
if (scope) {
|
|
239
|
+
const v = scope.keys(await liveWindows(), "a key press");
|
|
240
|
+
if (!v.ok)
|
|
241
|
+
return scopeRefused(v, "press");
|
|
242
|
+
}
|
|
184
243
|
await usecomputer.press({ key, count: null, delayMs: null });
|
|
185
244
|
return { success: true, data: { action: "press", key } };
|
|
186
245
|
}
|
|
@@ -188,6 +247,11 @@ async function doScroll(params) {
|
|
|
188
247
|
const usecomputer = await getUC();
|
|
189
248
|
const direction = String(params.direction);
|
|
190
249
|
const amount = Number(params.amount || 3);
|
|
250
|
+
if (scope) {
|
|
251
|
+
const v = scope.point(await liveWindows(), await usecomputer.mousePosition(), "a scroll");
|
|
252
|
+
if (!v.ok)
|
|
253
|
+
return scopeRefused(v, "scroll");
|
|
254
|
+
}
|
|
191
255
|
await usecomputer.scroll({ direction, amount, at: null });
|
|
192
256
|
return { success: true, data: { action: "scroll", direction, amount } };
|
|
193
257
|
}
|
|
@@ -204,6 +268,14 @@ async function doDrag(params) {
|
|
|
204
268
|
}
|
|
205
269
|
catch { }
|
|
206
270
|
}
|
|
271
|
+
if (scope) {
|
|
272
|
+
const w = await liveWindows();
|
|
273
|
+
for (const [p, n] of [[from, "a drag start"], [to, "a drag end"]]) {
|
|
274
|
+
const v = scope.point(w, p, n);
|
|
275
|
+
if (!v.ok)
|
|
276
|
+
return scopeRefused(v, "drag");
|
|
277
|
+
}
|
|
278
|
+
}
|
|
207
279
|
await usecomputer.drag({ from, to, durationMs: null, button: null });
|
|
208
280
|
return { success: true, data: { action: "drag" } };
|
|
209
281
|
}
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import { loadConfig, saveConfig, getConfigPath } from "./config.js";
|
|
|
11
11
|
import { checkPermissions, runPermissionTest } from "./permissions.js";
|
|
12
12
|
import { connect, disconnect } from "./connection.js";
|
|
13
13
|
import { closeSupervisor, setMode } from "./supervisor.js";
|
|
14
|
+
import { initScope } from "./executor.js";
|
|
14
15
|
import chalk from "chalk";
|
|
15
16
|
import readline from "readline";
|
|
16
17
|
import os from "os";
|
|
@@ -20,12 +21,19 @@ const API_BASE = "https://instaclaw.io";
|
|
|
20
21
|
// Temp file to persist pairing code + args across Terminal restarts
|
|
21
22
|
const RETRY_FILE = path.join(os.tmpdir(), "instaclaw-dispatch-retry.json");
|
|
22
23
|
const RETRY_MAX_AGE_MS = 10 * 60 * 1000; // 10 minutes
|
|
24
|
+
/** The client's own version, read from its package.json (one directory above dist/). */
|
|
25
|
+
const CLIENT_VERSION = (() => { try {
|
|
26
|
+
return JSON.parse(fs.readFileSync(path.join(path.dirname(new URL(import.meta.url).pathname), "..", "package.json"), "utf8")).version;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return "unknown";
|
|
30
|
+
} })();
|
|
23
31
|
async function main() {
|
|
32
|
+
const args = parseArgs(); // first: --help and unknown flags exit here, before any banner, check or capture
|
|
24
33
|
console.log(`
|
|
25
|
-
${chalk.bold("InstaClaw Dispatch")} ${chalk.dim("
|
|
34
|
+
${chalk.bold("InstaClaw Dispatch")} ${chalk.dim("Remote Computer Control")}
|
|
26
35
|
${chalk.dim("─────────────────────────────────────────────")}
|
|
27
36
|
`);
|
|
28
|
-
const args = parseArgs();
|
|
29
37
|
// ── Auto-retry: check if we're resuming after a Terminal restart ──
|
|
30
38
|
const retry = loadRetryState();
|
|
31
39
|
if (!args.pair && !args.token && retry) {
|
|
@@ -53,7 +61,7 @@ async function main() {
|
|
|
53
61
|
console.log(` → Enable "${terminal}"`);
|
|
54
62
|
console.log("");
|
|
55
63
|
console.log(chalk.yellow(` ⚠ Enabling Accessibility will close ${terminal}.`));
|
|
56
|
-
console.log(chalk.yellow(` Don't worry
|
|
64
|
+
console.log(chalk.yellow(` Don't worry. Just reopen ${terminal} and run the same command.`));
|
|
57
65
|
if (args.pair) {
|
|
58
66
|
console.log(chalk.green(` Your pairing code ${chalk.bold(args.pair)} will still work.`));
|
|
59
67
|
}
|
|
@@ -83,13 +91,13 @@ async function main() {
|
|
|
83
91
|
console.log(` After granting permissions and reopening ${terminal}, run:`);
|
|
84
92
|
console.log("");
|
|
85
93
|
if (args.pair) {
|
|
86
|
-
console.log(` ${chalk.cyan(`npx @instaclaw/dispatch@
|
|
94
|
+
console.log(` ${chalk.cyan(`npx @instaclaw/dispatch@latest --pair ${args.pair}`)}`);
|
|
87
95
|
}
|
|
88
96
|
else {
|
|
89
97
|
console.log(` ${chalk.cyan("npx @instaclaw/dispatch")}`);
|
|
90
98
|
}
|
|
91
99
|
console.log("");
|
|
92
|
-
console.log(` ${chalk.dim("Or just reopen Terminal
|
|
100
|
+
console.log(` ${chalk.dim("Or just reopen Terminal and we'll auto-resume if possible.")}`);
|
|
93
101
|
console.log("");
|
|
94
102
|
console.log(` Press ${chalk.bold("Enter")} when ready (or grant Accessibility and Terminal will restart)...`);
|
|
95
103
|
await waitForEnter();
|
|
@@ -97,7 +105,7 @@ async function main() {
|
|
|
97
105
|
const recheck = checkPermissions();
|
|
98
106
|
if (recheck.accessibility === false) {
|
|
99
107
|
console.log(`\n ${chalk.yellow("⚠")} Accessibility still not granted. Click/type commands will fail.`);
|
|
100
|
-
console.log(` ${chalk.dim("Continuing anyway
|
|
108
|
+
console.log(` ${chalk.dim("Continuing anyway. Screenshots may still work.\n")}`);
|
|
101
109
|
}
|
|
102
110
|
else {
|
|
103
111
|
console.log(`\n ${chalk.green("✓")} Permissions: all OK\n`);
|
|
@@ -114,7 +122,7 @@ async function main() {
|
|
|
114
122
|
}
|
|
115
123
|
else {
|
|
116
124
|
console.log(` ${chalk.yellow("⚠")} ${test.error}`);
|
|
117
|
-
console.log(" Continuing anyway
|
|
125
|
+
console.log(" Continuing anyway. Some commands may fail.\n");
|
|
118
126
|
}
|
|
119
127
|
clearRetryState();
|
|
120
128
|
}
|
|
@@ -159,6 +167,15 @@ async function main() {
|
|
|
159
167
|
console.log(` ${chalk.dim("VM:")} ${config.vmAddress}:${config.port}`);
|
|
160
168
|
console.log(` ${chalk.dim("Mode:")} ${config.mode}`);
|
|
161
169
|
console.log(` ${chalk.dim("Token:")} ${config.gatewayToken.substring(0, 8)}...\n`);
|
|
170
|
+
// ── 2b. PASS 43 scoped mode (scope.ts): fixed at start, before any command can arrive; a refusal to start ends here. ──
|
|
171
|
+
try {
|
|
172
|
+
if (await initScope())
|
|
173
|
+
console.log(` ${chalk.yellow("Scoped mode")}: only windows this test opens; the first refusal stops the client.`);
|
|
174
|
+
}
|
|
175
|
+
catch (e) {
|
|
176
|
+
console.log(` ${chalk.red("✗")} ${e.message}`);
|
|
177
|
+
process.exit(3);
|
|
178
|
+
}
|
|
162
179
|
// ── 3. Connect to VM ──
|
|
163
180
|
connect({
|
|
164
181
|
vmAddress: config.vmAddress,
|
|
@@ -339,6 +356,22 @@ function parseArgs() {
|
|
|
339
356
|
else if ((args[i] === "--pair" || args[i] === "--code") && args[i + 1]) {
|
|
340
357
|
result.pair = args[++i];
|
|
341
358
|
}
|
|
359
|
+
else {
|
|
360
|
+
// PASS 43 (PRD 43.18): --help, --version and ANY argument this client does not know print and exit BEFORE anything
|
|
361
|
+
// starts. An unknown flag used to fall through and start the full client: its permission check runs a full screen
|
|
362
|
+
// capture (measured 2026-10-31 by `--help` on a Mac it was never meant to touch).
|
|
363
|
+
const known = args[i] === "--help" || args[i] === "-h" || args[i] === "--version" || args[i] === "-V";
|
|
364
|
+
if (args[i] === "--version" || args[i] === "-V") {
|
|
365
|
+
console.log(CLIENT_VERSION);
|
|
366
|
+
process.exit(0);
|
|
367
|
+
}
|
|
368
|
+
console.log(`${known ? "" : ` Unknown argument: ${args[i]}\n\n`} Usage:
|
|
369
|
+
npx @instaclaw/dispatch --pair ABCD-1234 [--autonomous] pair with the code from your dashboard
|
|
370
|
+
npx @instaclaw/dispatch --token TOKEN --vm HOST direct connect (advanced)
|
|
371
|
+
npx @instaclaw/dispatch --help | --version
|
|
372
|
+
Nothing was started.`);
|
|
373
|
+
process.exit(known ? 0 : 2);
|
|
374
|
+
}
|
|
342
375
|
}
|
|
343
376
|
return result;
|
|
344
377
|
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
/** True when a shell command names one of our own web origins, gateway subdomains excepted. */
|
|
2
|
+
export declare function commandNamesOwnPage(command: string): boolean;
|
|
3
|
+
export declare const OWN_PAGE_REFUSAL = "Refused: this would act on an InstaClaw page. Your agent is not allowed to approve its own actions.";
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE OWN-PAGES REFUSAL, Node CLI. Parity with the Mac app's OwnPages.swift and the extension's
|
|
3
|
+
* isForbiddenAgentUrl.
|
|
4
|
+
*
|
|
5
|
+
* READ THIS BEFORE TRUSTING IT. This closes ONE vector and deliberately does not pretend to close
|
|
6
|
+
* the one that matters most.
|
|
7
|
+
*
|
|
8
|
+
* WHAT IT CLOSES: an `exec` whose command text names one of our own web origins, so the agent
|
|
9
|
+
* cannot curl an approval endpoint or drive a browser at our pages by naming them.
|
|
10
|
+
*
|
|
11
|
+
* WHAT IT CANNOT CLOSE, and no client side check ever will: this CLI can `click` at coordinates and
|
|
12
|
+
* `type` text on the owner's real desktop. To approve its own spend it needs only `screenshot` to
|
|
13
|
+
* find the button and `click` to press it. A click carries no text, so no pattern can match it, and
|
|
14
|
+
* from the operating system's point of view it is indistinguishable from the owner's own click.
|
|
15
|
+
* CONNECTORS reached the same conclusion independently in lib/owner-request-origin.ts: "a click on
|
|
16
|
+
* the owner's screen is indistinguishable from the owner's click. THIS MODULE CANNOT" detect it.
|
|
17
|
+
* FRONTIER reached it too, in lib/frontier-approval-outofband.ts: "a URL-pattern block does not
|
|
18
|
+
* survive... Dispatch bypasses the browser entirely."
|
|
19
|
+
*
|
|
20
|
+
* SO THE REAL FIX IS NOT HERE. It is the out of band completion token FRONTIER already built, which
|
|
21
|
+
* requires a factor delivered to a surface the agent has no path to. That module is PURE and INERT
|
|
22
|
+
* with no route importing it. Until it is wired, a machine running this CLI in autonomous mode can
|
|
23
|
+
* approve its own spend, and adding more client side checks here would be theatre that makes the
|
|
24
|
+
* hole look closed.
|
|
25
|
+
*/
|
|
26
|
+
const FORBIDDEN_HOSTS = ["instaclaw.io", "www.instaclaw.io"];
|
|
27
|
+
/** True when a shell command names one of our own web origins, gateway subdomains excepted. */
|
|
28
|
+
export function commandNamesOwnPage(command) {
|
|
29
|
+
const c = String(command || "").toLowerCase();
|
|
30
|
+
for (const host of FORBIDDEN_HOSTS) {
|
|
31
|
+
let idx = 0;
|
|
32
|
+
for (;;) {
|
|
33
|
+
const at = c.indexOf(host, idx);
|
|
34
|
+
if (at === -1)
|
|
35
|
+
break;
|
|
36
|
+
const prefix = c.slice(0, at);
|
|
37
|
+
// The customer's own gateway (vm-123.vm.instaclaw.io) is explicitly allowed.
|
|
38
|
+
if (!prefix.endsWith(".vm.") && !prefix.endsWith("vm."))
|
|
39
|
+
return true;
|
|
40
|
+
idx = at + host.length;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
export const OWN_PAGE_REFUSAL = "Refused: this would act on an InstaClaw page. Your agent is not allowed to approve its own actions.";
|
|
46
|
+
//# sourceMappingURL=own-pages.js.map
|
package/dist/permissions.js
CHANGED
|
@@ -59,7 +59,7 @@ export async function runPermissionTest() {
|
|
|
59
59
|
fs.unlinkSync(tmpPath);
|
|
60
60
|
if (size > 500)
|
|
61
61
|
return { ok: true };
|
|
62
|
-
return { ok: false, error: "Screenshot captured but empty
|
|
62
|
+
return { ok: false, error: "Screenshot captured but empty. Screen Recording permission may be missing" };
|
|
63
63
|
}
|
|
64
64
|
return { ok: false, error: "Screenshot file not created" };
|
|
65
65
|
}
|
package/dist/scope.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export type Win = {
|
|
2
|
+
id: number;
|
|
3
|
+
ownerPid: number;
|
|
4
|
+
ownerName: string;
|
|
5
|
+
title: string;
|
|
6
|
+
x: number;
|
|
7
|
+
y: number;
|
|
8
|
+
width: number;
|
|
9
|
+
height: number;
|
|
10
|
+
};
|
|
11
|
+
export type ScopeConfig = {
|
|
12
|
+
apps: string[];
|
|
13
|
+
folder: string;
|
|
14
|
+
log: string;
|
|
15
|
+
};
|
|
16
|
+
export type Verdict = {
|
|
17
|
+
ok: true;
|
|
18
|
+
windowId?: number;
|
|
19
|
+
} | {
|
|
20
|
+
ok: false;
|
|
21
|
+
why: string;
|
|
22
|
+
wouldTouch: string;
|
|
23
|
+
};
|
|
24
|
+
export declare class Scope {
|
|
25
|
+
readonly cfg: ScopeConfig;
|
|
26
|
+
readonly baselineIds: Set<number>;
|
|
27
|
+
readonly baselinePids: Set<number>;
|
|
28
|
+
constructor(cfg: ScopeConfig, baseline: Win[]);
|
|
29
|
+
/** Refuse to start when an allowed app already has a window: its windows could be the owner's. */
|
|
30
|
+
static startRefusal(cfg: ScopeConfig, baseline: Win[]): string | null;
|
|
31
|
+
isTestWindow(w: Win): boolean;
|
|
32
|
+
/** The topmost window containing a desktop point (front to back), if any. */
|
|
33
|
+
static topAt(wins: Win[], p: {
|
|
34
|
+
x: number;
|
|
35
|
+
y: number;
|
|
36
|
+
}): Win | null;
|
|
37
|
+
private describe;
|
|
38
|
+
screenshot(wins: Win[]): Verdict;
|
|
39
|
+
windows(wins: Win[]): Win[];
|
|
40
|
+
point(wins: Win[], p: {
|
|
41
|
+
x: number;
|
|
42
|
+
y: number;
|
|
43
|
+
}, what: string): Verdict;
|
|
44
|
+
keys(wins: Win[], what: string): Verdict;
|
|
45
|
+
exec(command: string): Verdict;
|
|
46
|
+
/** The kill point: log what it would have touched (locally), then the caller exits. */
|
|
47
|
+
refuse(v: Extract<Verdict, {
|
|
48
|
+
ok: false;
|
|
49
|
+
}>, command: string): string;
|
|
50
|
+
}
|
|
51
|
+
export declare function loadScopeConfig(): ScopeConfig | null;
|
package/dist/scope.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCOPED MODE (DISPATCH pass 43, PRD 43.11): the client acts ONLY inside windows the test opened itself.
|
|
3
|
+
*
|
|
4
|
+
* Written for one machine first: Cooper's own Mac, under his line "only windows you open yourself, nothing closed, quit,
|
|
5
|
+
* deleted or typed into that you did not open". It is fail closed by construction:
|
|
6
|
+
* - At start, every window that exists is recorded and refused forever (the BASELINE). If an allowed app is already
|
|
7
|
+
* running, the client REFUSES TO START: a window of a running app cannot be told apart from the owner's own.
|
|
8
|
+
* - A screenshot is of the frontmost TEST window only, never the full screen (his screen holds terminals and keys).
|
|
9
|
+
* `windows` returns test windows only, so no title of his reaches a model.
|
|
10
|
+
* - A click, drag or scroll lands only when the desktop point is inside a test window AND that window is the topmost
|
|
11
|
+
* one at the point. Typing and keys only when the frontmost window is a test window.
|
|
12
|
+
* - `exec` runs only the exact allowed commands (open an allowed app; make the test folder).
|
|
13
|
+
* - THE KILL POINT: the first refusal logs, locally only, what it would have touched, and the client exits. The agent
|
|
14
|
+
* is told only "refused: outside the test's own windows", never a title of the owner's.
|
|
15
|
+
* Enabled when INSTACLAW_DISPATCH_SCOPE_FILE names a JSON file: { "apps": ["TextEdit"], "folder": "/Users/x/dir", "log": "/path" }.
|
|
16
|
+
*
|
|
17
|
+
* ASSUMPTION, stated: the window list is front to back (CGWindowListCopyWindowInfo's order for on screen windows). It is
|
|
18
|
+
* proven on the rented Mac before his Mac (J5a), never assumed there.
|
|
19
|
+
*/
|
|
20
|
+
import * as fs from "node:fs";
|
|
21
|
+
export class Scope {
|
|
22
|
+
cfg;
|
|
23
|
+
baselineIds;
|
|
24
|
+
baselinePids;
|
|
25
|
+
constructor(cfg, baseline) {
|
|
26
|
+
this.cfg = cfg;
|
|
27
|
+
this.baselineIds = new Set(baseline.map((w) => w.id));
|
|
28
|
+
this.baselinePids = new Set(baseline.map((w) => w.ownerPid));
|
|
29
|
+
}
|
|
30
|
+
/** Refuse to start when an allowed app already has a window: its windows could be the owner's. */
|
|
31
|
+
static startRefusal(cfg, baseline) {
|
|
32
|
+
if (!cfg.apps.length)
|
|
33
|
+
return "no allowed app is named";
|
|
34
|
+
if (!cfg.folder.startsWith("/") || cfg.folder.includes(".."))
|
|
35
|
+
return "the test folder is not an absolute plain path";
|
|
36
|
+
const open = baseline.filter((w) => cfg.apps.includes(w.ownerName));
|
|
37
|
+
return open.length ? `${open[0].ownerName} is already open; close it yourself first, or the test cannot tell your windows from its own` : null;
|
|
38
|
+
}
|
|
39
|
+
isTestWindow(w) {
|
|
40
|
+
return !this.baselineIds.has(w.id) && !this.baselinePids.has(w.ownerPid) && this.cfg.apps.includes(w.ownerName);
|
|
41
|
+
}
|
|
42
|
+
/** The topmost window containing a desktop point (front to back), if any. */
|
|
43
|
+
static topAt(wins, p) {
|
|
44
|
+
return wins.find((w) => p.x >= w.x && p.x < w.x + w.width && p.y >= w.y && p.y < w.y + w.height) ?? null;
|
|
45
|
+
}
|
|
46
|
+
describe(w) { return w ? `${w.ownerName} window ${w.id} "${w.title.slice(0, 60)}"` : "no window (the desktop or the menu bar)"; }
|
|
47
|
+
screenshot(wins) {
|
|
48
|
+
const t = wins.find((w) => this.isTestWindow(w));
|
|
49
|
+
return t ? { ok: true, windowId: t.id } : { ok: false, why: "no test window is open", wouldTouch: "a capture of the whole screen" };
|
|
50
|
+
}
|
|
51
|
+
windows(wins) { return wins.filter((w) => this.isTestWindow(w)); }
|
|
52
|
+
point(wins, p, what) {
|
|
53
|
+
const top = Scope.topAt(wins, p);
|
|
54
|
+
return top && this.isTestWindow(top) ? { ok: true } : { ok: false, why: `${what} outside the test's own windows`, wouldTouch: this.describe(top) };
|
|
55
|
+
}
|
|
56
|
+
keys(wins, what) {
|
|
57
|
+
const front = wins[0] ?? null;
|
|
58
|
+
return front && this.isTestWindow(front) ? { ok: true } : { ok: false, why: `${what} while the front window is not the test's`, wouldTouch: this.describe(front) };
|
|
59
|
+
}
|
|
60
|
+
exec(command) {
|
|
61
|
+
const c = command.trim();
|
|
62
|
+
const apps = this.cfg.apps.map((a) => a.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
|
|
63
|
+
if (new RegExp(`^open -a (${apps})$`).test(c))
|
|
64
|
+
return { ok: true };
|
|
65
|
+
if (c === `mkdir -p ${this.cfg.folder}` || c === `mkdir -p "${this.cfg.folder}"`)
|
|
66
|
+
return { ok: true };
|
|
67
|
+
return { ok: false, why: "a shell command outside the allowed list", wouldTouch: `the shell: ${c.slice(0, 80)}` };
|
|
68
|
+
}
|
|
69
|
+
/** The kill point: log what it would have touched (locally), then the caller exits. */
|
|
70
|
+
refuse(v, command) {
|
|
71
|
+
const line = JSON.stringify({ at: new Date().toISOString(), refused: command, why: v.why, would_touch: v.wouldTouch });
|
|
72
|
+
try {
|
|
73
|
+
fs.appendFileSync(this.cfg.log, line + "\n");
|
|
74
|
+
}
|
|
75
|
+
catch { }
|
|
76
|
+
return "refused: outside the test's own windows; the test stopped here";
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
export function loadScopeConfig() {
|
|
80
|
+
const f = process.env.INSTACLAW_DISPATCH_SCOPE_FILE;
|
|
81
|
+
if (!f)
|
|
82
|
+
return null;
|
|
83
|
+
const j = JSON.parse(fs.readFileSync(f, "utf8"));
|
|
84
|
+
if (!Array.isArray(j.apps) || typeof j.folder !== "string" || typeof j.log !== "string")
|
|
85
|
+
throw new Error("scope file needs apps, folder and log");
|
|
86
|
+
return { apps: j.apps.map(String), folder: j.folder, log: j.log };
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=scope.js.map
|
package/dist/screenshot.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ export interface ScreenshotOptions {
|
|
|
3
3
|
quality?: number;
|
|
4
4
|
/** Max width — downscale if screen is larger (default 1280) */
|
|
5
5
|
maxWidth?: number;
|
|
6
|
+
/** PASS 43 scoped mode: capture this one window only, never the full screen. */
|
|
7
|
+
windowId?: number;
|
|
6
8
|
}
|
|
7
9
|
export interface ScreenshotCapture {
|
|
8
10
|
buffer: Buffer;
|
package/dist/screenshot.js
CHANGED
|
@@ -26,7 +26,7 @@ export async function captureScreenshot(opts = {}) {
|
|
|
26
26
|
const quality = opts.quality || 55;
|
|
27
27
|
const maxWidth = opts.maxWidth || 1280;
|
|
28
28
|
try {
|
|
29
|
-
const result = await uc.screenshot({ path: tmpPath, display: null, window: null, region: null, annotate: null });
|
|
29
|
+
const result = await uc.screenshot({ path: tmpPath, display: null, window: opts.windowId ?? null, region: null, annotate: null });
|
|
30
30
|
const pngBuffer = fs.readFileSync(tmpPath);
|
|
31
31
|
let outputBuffer;
|
|
32
32
|
let outWidth = result.imageWidth;
|
package/dist/supervisor.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { DispatchCommand } from "./types.js";
|
|
2
2
|
export declare function setMode(m: "supervised" | "autonomous"): void;
|
|
3
3
|
export declare function requestApproval(command: DispatchCommand): Promise<boolean>;
|
|
4
|
+
/** True when the agent's own stated intent names a consequential act. */
|
|
5
|
+
export declare function descriptionIsConsequential(description: unknown): boolean;
|
|
4
6
|
export declare function closeSupervisor(): void;
|
package/dist/supervisor.js
CHANGED
|
@@ -39,20 +39,44 @@ export function setMode(m) {
|
|
|
39
39
|
mode = m;
|
|
40
40
|
}
|
|
41
41
|
export async function requestApproval(command) {
|
|
42
|
-
//
|
|
42
|
+
// Passive commands (a screenshot, the window list, status) are approved without a per action prompt ONLY when the
|
|
43
|
+
// owner opted in (autonomous) or is at the terminal that started the session. PASS 43 (PRD 43.18): they were approved
|
|
44
|
+
// unconditionally, so a session that never opted in, with no terminal, handed out a FULL SCREEN capture and every
|
|
45
|
+
// window's title. Measured on main's 0.7.0 at 2026-10-31 20:3xZ: screenshot, windows and status all true.
|
|
43
46
|
if (AUTO_APPROVE_TYPES.has(command.type)) {
|
|
44
|
-
|
|
47
|
+
if (rl || mode === "autonomous")
|
|
48
|
+
return true;
|
|
49
|
+
console.log(`\n Agent wants to: ${formatCommand(command)}`);
|
|
50
|
+
console.log(" REFUSED. This session did not opt in, and nothing here can show you what it would see.");
|
|
51
|
+
return false;
|
|
45
52
|
}
|
|
46
53
|
const desc = formatCommand(command);
|
|
47
54
|
const dangerous = isDangerous(command);
|
|
48
|
-
//
|
|
55
|
+
// THE NO-TTY PATH, CORRECTED 2026-09-23. It previously read "can't prompt, therefore approve".
|
|
56
|
+
//
|
|
57
|
+
// That is the wrong direction and it was the more serious half of this hole. `rl` exists only
|
|
58
|
+
// when stdin is a TTY, so ANY script, launchd job, systemd unit, CI runner or wrapper fell into
|
|
59
|
+
// this branch and received full auto-approval while the session was in the DEFAULT supervised
|
|
60
|
+
// mode, which nobody opted into. Measured against the published 0.6.2 on 2026-09-23: supervised
|
|
61
|
+
// with no TTY returned screenshot=true click=true type=true.
|
|
62
|
+
//
|
|
63
|
+
// Being unable to ask for consent is not the same as having it. We now refuse unless autonomous
|
|
64
|
+
// was explicitly chosen, which preserves the documented path exactly (the dashboard prints
|
|
65
|
+
// --autonomous) and closes the silent one.
|
|
49
66
|
if (!rl) {
|
|
50
67
|
if (dangerous) {
|
|
51
68
|
console.log(`\n BLOCKED (dangerous, no TTY): ${desc}`);
|
|
52
69
|
return false;
|
|
53
70
|
}
|
|
54
|
-
|
|
55
|
-
|
|
71
|
+
if (mode !== "autonomous") {
|
|
72
|
+
console.log(`\n Agent wants to: ${desc}`);
|
|
73
|
+
console.log(" REFUSED. This session did not opt in, and nothing here can show you");
|
|
74
|
+
console.log(" what is happening or let you stop it.");
|
|
75
|
+
console.log(" Start Dispatch from your dashboard, or re-run with --autonomous.");
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
console.log(`\n Agent: ${desc}`);
|
|
79
|
+
console.log(" Auto-approved (autonomous, non-interactive)");
|
|
56
80
|
return true;
|
|
57
81
|
}
|
|
58
82
|
// Autonomous mode — auto-approve non-dangerous actions
|
|
@@ -63,7 +87,7 @@ export async function requestApproval(command) {
|
|
|
63
87
|
// Supervised mode OR dangerous action in autonomous mode — prompt user
|
|
64
88
|
console.log(`\n Agent wants to: ${desc}`);
|
|
65
89
|
if (dangerous) {
|
|
66
|
-
console.log(" ⚠️ DANGEROUS ACTION
|
|
90
|
+
console.log(" ⚠️ DANGEROUS ACTION, requires confirmation");
|
|
67
91
|
}
|
|
68
92
|
return new Promise((resolve) => {
|
|
69
93
|
rl.question(" [Enter] to approve, [n] to deny: ", (answer) => {
|
|
@@ -75,23 +99,72 @@ export async function requestApproval(command) {
|
|
|
75
99
|
});
|
|
76
100
|
});
|
|
77
101
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
102
|
+
/**
|
|
103
|
+
* Per-type danger classification.
|
|
104
|
+
*
|
|
105
|
+
* EXHAUSTIVE BY CONSTRUCTION. This is a Record over the declared union, so adding an
|
|
106
|
+
* eleventh member to DispatchCommand["type"] is a COMPILE ERROR until it is classified
|
|
107
|
+
* here. Before 2026-09-23 this was an if chain that handled four of the ten declared
|
|
108
|
+
* types and let the other six reach a bare "return false".
|
|
109
|
+
*
|
|
110
|
+
* WHAT IS DELIBERATELY UNCHANGED, and why. click, scroll and drag stay not dangerous.
|
|
111
|
+
* They are the ordinary acting verbs of autonomous mode, and the dashboard's own
|
|
112
|
+
* fallback command ends in --autonomous, so classifying them dangerous would refuse the
|
|
113
|
+
* product's documented use. The protection against acting with no person present is the
|
|
114
|
+
* no TTY branch in confirmAction, never this table. Every verdict for the ten known
|
|
115
|
+
* types is preserved exactly as it shipped.
|
|
116
|
+
*/
|
|
117
|
+
/**
|
|
118
|
+
* CONSEQUENTIAL INTENT, read from the command's OWN description.
|
|
119
|
+
*
|
|
120
|
+
* Cooper's ruling of 2026-09-23: an opted-in session acts without asking per action, and pauses
|
|
121
|
+
* only before an act that leaves the customer's account or cannot be undone, namely sending,
|
|
122
|
+
* buying, deleting or submitting.
|
|
123
|
+
*
|
|
124
|
+
* A click carries coordinates, so this layer cannot know what sits under the cursor. But the
|
|
125
|
+
* BOX sends a `description` alongside it, which is the agent stating its own intent, and before
|
|
126
|
+
* 2026-09-23 nothing read it. Measured that day against the published 0.6.2 and against this
|
|
127
|
+
* build: "Send the email", "Buy it now" and "Delete the file" were ALL auto-approved.
|
|
128
|
+
*
|
|
129
|
+
* HONEST LIMIT, stated because it decides where the real fix lives: this only sees intent the
|
|
130
|
+
* agent chose to write down. An agent that sends no description, or writes "click the button",
|
|
131
|
+
* passes straight through. So this is the LAST line and not the authoritative one. The
|
|
132
|
+
* authoritative pause belongs in the agent's own instructions, which CORTEX owns, and this is
|
|
133
|
+
* filed there. Defence in depth, not a substitute.
|
|
134
|
+
*/
|
|
135
|
+
const CONSEQUENTIAL_INTENT_PATTERNS = [
|
|
136
|
+
// Leaves the customer's account
|
|
137
|
+
/\bsend(s|ing)?\b/i, /\bsubmit(s|ting)?\b/i, /\bpost(s|ing)?\b/i,
|
|
138
|
+
/\bpublish(es|ing)?\b/i, /\bshare(s|ing)?\b/i, /\breply\b/i, /\bforward(s|ing)?\b/i,
|
|
139
|
+
// Spends money
|
|
140
|
+
/\bbuy(s|ing)?\b/i, /\bpurchase(s|ing)?\b/i, /\bpay(s|ing|ment)?\b/i,
|
|
141
|
+
/\bcheckout\b/i, /\border\b/i, /\btransfer(s|ring)?\b/i, /\bwithdraw(s|ing)?\b/i,
|
|
142
|
+
// Cannot be undone
|
|
143
|
+
/\bdelete(s|ing)?\b/i, /\bremove(s|ing)?\b/i, /\btrash\b/i, /\berase(s|ing)?\b/i,
|
|
144
|
+
/\bconfirm(s|ing)?\b/i, /\bapprove(s|ing)?\b/i, /\baccept(s|ing)?\b/i,
|
|
145
|
+
];
|
|
146
|
+
/** True when the agent's own stated intent names a consequential act. */
|
|
147
|
+
export function descriptionIsConsequential(description) {
|
|
148
|
+
const d = String(description || "");
|
|
149
|
+
if (!d)
|
|
150
|
+
return false;
|
|
151
|
+
return CONSEQUENTIAL_INTENT_PATTERNS.some((pat) => pat.test(d));
|
|
152
|
+
}
|
|
153
|
+
const DANGER_BY_TYPE = {
|
|
154
|
+
// Reads. They observe the computer and change nothing.
|
|
155
|
+
screenshot: () => false,
|
|
156
|
+
status: () => false,
|
|
157
|
+
windows: () => false,
|
|
158
|
+
// Acting verbs, deliberately not dangerous. See the note above.
|
|
159
|
+
click: () => false,
|
|
160
|
+
scroll: () => false,
|
|
161
|
+
drag: () => false,
|
|
162
|
+
type: (p) => DANGEROUS_TEXT_PATTERNS.some((pat) => pat.test(String(p.text || ""))),
|
|
163
|
+
press: (p) => DANGEROUS_KEY_COMBOS.has(String(p.key || "").toLowerCase()),
|
|
164
|
+
exec: (p) => {
|
|
90
165
|
const command = String(p.command || "");
|
|
91
|
-
// Check against text patterns (reuses the same dangerous detection)
|
|
92
166
|
if (DANGEROUS_TEXT_PATTERNS.some((pat) => pat.test(command)))
|
|
93
167
|
return true;
|
|
94
|
-
// Additional exec-specific dangerous patterns
|
|
95
168
|
if (/\brm\s+-rf\b/.test(command))
|
|
96
169
|
return true;
|
|
97
170
|
if (/\bsudo\b/.test(command))
|
|
@@ -101,21 +174,37 @@ function isDangerous(cmd) {
|
|
|
101
174
|
if (/\bcurl\b.*\|\s*(bash|sh)\b/.test(command))
|
|
102
175
|
return true;
|
|
103
176
|
return false;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
if (cmd.type === "batch") {
|
|
177
|
+
},
|
|
178
|
+
batch: (p) => {
|
|
107
179
|
const actions = p.actions;
|
|
108
|
-
if (Array.isArray(actions))
|
|
109
|
-
return
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
180
|
+
if (!Array.isArray(actions))
|
|
181
|
+
return false;
|
|
182
|
+
return actions.some((a) => isDangerous({
|
|
183
|
+
id: "",
|
|
184
|
+
type: a.type,
|
|
185
|
+
params: a.params || {},
|
|
186
|
+
}));
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
function isDangerous(cmd) {
|
|
190
|
+
// THE INTENT CHECK, before the per-type table. Applies to EVERY type, because a described
|
|
191
|
+
// exec or drag is just as consequential as a described click.
|
|
192
|
+
if (descriptionIsConsequential(cmd.description))
|
|
193
|
+
return true;
|
|
194
|
+
const classify = DANGER_BY_TYPE[cmd.type];
|
|
195
|
+
// FAIL CLOSED on a type this build has never heard of. A newer or a hostile server can
|
|
196
|
+
// send one, and before 2026-09-23 it reached "return false" and was called safe.
|
|
197
|
+
// Calling it dangerous costs nothing real, because the executor cannot run a type it
|
|
198
|
+
// does not implement either, and it means the next type added cannot arrive unclassified.
|
|
199
|
+
if (typeof classify !== "function")
|
|
200
|
+
return true;
|
|
201
|
+
return classify(cmd.params);
|
|
113
202
|
}
|
|
114
203
|
function formatCommand(cmd) {
|
|
115
204
|
const p = cmd.params;
|
|
116
205
|
switch (cmd.type) {
|
|
117
206
|
case "click":
|
|
118
|
-
return `Click at (${p.x}, ${p.y})${cmd.description ?
|
|
207
|
+
return `Click at (${p.x}, ${p.y})${cmd.description ? `, "${cmd.description}"` : ""}`;
|
|
119
208
|
case "type":
|
|
120
209
|
return `Type: "${String(p.text).substring(0, 50)}${String(p.text).length > 50 ? "..." : ""}"`;
|
|
121
210
|
case "press":
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@instaclaw/dispatch",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Let your InstaClaw AI agent control your computer
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "Let your InstaClaw AI agent control your computer \u2014 screenshots, clicks, typing, and more via a secure WebSocket relay",
|
|
5
5
|
"bin": {
|
|
6
6
|
"instaclaw-dispatch": "./dist/index.js"
|
|
7
7
|
},
|