@skylandnpm/octopus-proxy 0.1.35 → 0.1.38
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/{chunk-5OJQ46AF.js → chunk-6DTK4JVV.js} +368 -173
- package/dist/cli.js +1 -1
- package/dist/main.js +1 -1
- package/native/screen-unlock-macos.m +127 -0
- package/package.json +1 -1
|
@@ -11714,8 +11714,8 @@ async function ensureHostIdentity(stateDirectory) {
|
|
|
11714
11714
|
}
|
|
11715
11715
|
|
|
11716
11716
|
// src/daemon.ts
|
|
11717
|
-
import { mkdir as
|
|
11718
|
-
import { join as
|
|
11717
|
+
import { mkdir as mkdir11 } from "node:fs/promises";
|
|
11718
|
+
import { join as join14 } from "node:path";
|
|
11719
11719
|
|
|
11720
11720
|
// src/workspace-diff.ts
|
|
11721
11721
|
import { execFile as execFile2 } from "node:child_process";
|
|
@@ -12186,7 +12186,7 @@ function isAuthentication(operation) {
|
|
|
12186
12186
|
}
|
|
12187
12187
|
|
|
12188
12188
|
// src/journal.ts
|
|
12189
|
-
import { join as
|
|
12189
|
+
import { join as join6 } from "node:path";
|
|
12190
12190
|
import { createHash as createHash4 } from "node:crypto";
|
|
12191
12191
|
|
|
12192
12192
|
// ../../packages/protocol/src/index.ts
|
|
@@ -12250,6 +12250,8 @@ var Operations = [
|
|
|
12250
12250
|
"desktop.list",
|
|
12251
12251
|
"desktop.capture",
|
|
12252
12252
|
"desktop.read",
|
|
12253
|
+
"desktop.lockStatus",
|
|
12254
|
+
"desktop.unlock",
|
|
12253
12255
|
"artifacts.list",
|
|
12254
12256
|
"artifacts.history",
|
|
12255
12257
|
"artifacts.read",
|
|
@@ -12271,6 +12273,252 @@ var CommandSchema = z.object({
|
|
|
12271
12273
|
}).strict();
|
|
12272
12274
|
var isReadOperation = (operation) => operation === "desktop.capture" || /\.(status|snapshot|list|get|messages|diff|todos|children|logs|projects|read|search|history|models|agents|commands|providers|mcp)$/.test(operation);
|
|
12273
12275
|
|
|
12276
|
+
// src/screen-lock.ts
|
|
12277
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
12278
|
+
import { chmod as chmod3, mkdir as mkdir5, rename as rename4, rm, stat as stat6 } from "node:fs/promises";
|
|
12279
|
+
import { randomUUID as randomUUID7 } from "node:crypto";
|
|
12280
|
+
import { userInfo as userInfo2 } from "node:os";
|
|
12281
|
+
import { join as join5 } from "node:path";
|
|
12282
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
12283
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
12284
|
+
|
|
12285
|
+
// src/host-auth.ts
|
|
12286
|
+
import { spawn } from "node:child_process";
|
|
12287
|
+
import { chmod as chmod2, mkdir as mkdir4, rename as rename3, stat as stat5, unlink as unlink4 } from "node:fs/promises";
|
|
12288
|
+
import { userInfo, platform as platform2 } from "node:os";
|
|
12289
|
+
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
12290
|
+
import { join as join4 } from "node:path";
|
|
12291
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
12292
|
+
function helperPath(stateDirectory) {
|
|
12293
|
+
return join4(stateDirectory, "octopus-host-auth");
|
|
12294
|
+
}
|
|
12295
|
+
async function ensureHostAuth(stateDirectory) {
|
|
12296
|
+
await mkdir4(stateDirectory, { recursive: true, mode: 448 });
|
|
12297
|
+
const target = helperPath(stateDirectory);
|
|
12298
|
+
const system = platform2();
|
|
12299
|
+
if (system !== "darwin" && system !== "linux") throw new Error("Host authentication is supported only on macOS and Linux");
|
|
12300
|
+
const mac = system === "darwin";
|
|
12301
|
+
const source = fileURLToPath2(new URL(mac ? "../native/auth-macos.m" : "../native/auth-linux.c", import.meta.url));
|
|
12302
|
+
const sourceInfo = await stat5(source);
|
|
12303
|
+
const existing = await stat5(target).catch(() => null);
|
|
12304
|
+
if (existing?.isFile() && existing.mtimeMs >= sourceInfo.mtimeMs && (existing.mode & 63) === 0) return target;
|
|
12305
|
+
const temporary = `${target}.${randomUUID6()}`;
|
|
12306
|
+
const args = mac ? ["-O2", "-framework", "Foundation", "-framework", "OpenDirectory", source, "-o", target] : ["-O2", source, "-o", temporary, "-lpam"];
|
|
12307
|
+
if (mac) args[args.length - 1] = temporary;
|
|
12308
|
+
const compiler = spawn(mac ? "clang" : "cc", args, { stdio: ["ignore", "ignore", "pipe"] });
|
|
12309
|
+
let diagnostics = "";
|
|
12310
|
+
compiler.stderr.on("data", (chunk) => {
|
|
12311
|
+
diagnostics += String(chunk).slice(0, 4096);
|
|
12312
|
+
});
|
|
12313
|
+
const exit = await new Promise((resolve4, reject) => {
|
|
12314
|
+
compiler.on("error", reject);
|
|
12315
|
+
compiler.on("exit", (code) => resolve4(code ?? 1));
|
|
12316
|
+
});
|
|
12317
|
+
if (exit !== 0) {
|
|
12318
|
+
await unlink4(temporary).catch(() => {
|
|
12319
|
+
});
|
|
12320
|
+
throw new Error(`Host authentication helper build failed: ${diagnostics.trim()}`);
|
|
12321
|
+
}
|
|
12322
|
+
try {
|
|
12323
|
+
await chmod2(temporary, 448);
|
|
12324
|
+
await rename3(temporary, target);
|
|
12325
|
+
return target;
|
|
12326
|
+
} finally {
|
|
12327
|
+
await unlink4(temporary).catch(() => {
|
|
12328
|
+
});
|
|
12329
|
+
}
|
|
12330
|
+
}
|
|
12331
|
+
async function verifyHostPassword(stateDirectory, username, password) {
|
|
12332
|
+
if (username !== userInfo().username || !password || Buffer.byteLength(password) > 1024) return false;
|
|
12333
|
+
const path = await ensureHostAuth(stateDirectory);
|
|
12334
|
+
const child = spawn(path, [], { stdio: ["pipe", "pipe", "ignore"] });
|
|
12335
|
+
const bytes = Buffer.from(password, "utf8");
|
|
12336
|
+
const header2 = Buffer.alloc(2);
|
|
12337
|
+
header2.writeUInt16BE(bytes.length);
|
|
12338
|
+
child.stdin.end(Buffer.concat([header2, bytes]));
|
|
12339
|
+
bytes.fill(0);
|
|
12340
|
+
let output = "";
|
|
12341
|
+
child.stdout.on("data", (chunk) => {
|
|
12342
|
+
output += String(chunk).slice(0, 16);
|
|
12343
|
+
});
|
|
12344
|
+
const timer = setTimeout(() => child.kill("SIGKILL"), 1e4);
|
|
12345
|
+
try {
|
|
12346
|
+
const code = await new Promise((resolve4, reject) => {
|
|
12347
|
+
child.on("error", reject);
|
|
12348
|
+
child.on("exit", (value) => resolve4(value ?? 1));
|
|
12349
|
+
});
|
|
12350
|
+
return code === 0 && output.trim() === "OK";
|
|
12351
|
+
} finally {
|
|
12352
|
+
clearTimeout(timer);
|
|
12353
|
+
}
|
|
12354
|
+
}
|
|
12355
|
+
|
|
12356
|
+
// src/screen-lock.ts
|
|
12357
|
+
var ScreenLockError = class extends Error {
|
|
12358
|
+
};
|
|
12359
|
+
var nativeErrors = {
|
|
12360
|
+
permission: "\u8BF7\u5728 Proxy \u4E3B\u673A\u201C\u7CFB\u7EDF\u8BBE\u7F6E \u2192 \u9690\u79C1\u4E0E\u5B89\u5168\u6027 \u2192 \u8F85\u52A9\u529F\u80FD\u201D\u4E2D\u6388\u6743\u5B9E\u9645\u8FD0\u884C Proxy \u7684\u7A0B\u5E8F\uFF0C\u5E76\u91CD\u542F Proxy\u3002",
|
|
12361
|
+
session: "\u65E0\u6CD5\u8BBF\u95EE Proxy \u7528\u6237\u7684\u6D3B\u52A8\u56FE\u5F62\u4F1A\u8BDD\uFF1B\u4E0D\u652F\u6301\u767B\u5F55\u524D\u3001FileVault \u542F\u52A8\u89E3\u9501\u6216\u5207\u6362\u5230\u5176\u4ED6\u7528\u6237\u7684\u5C4F\u5E55\u3002",
|
|
12362
|
+
focus: "\u672A\u627E\u5230 loginwindow \u7684\u5BC6\u7801\u8F93\u5165\u6846\uFF0C\u8BF7\u5728\u4E3B\u673A\u663E\u793A\u5F53\u524D\u7528\u6237\u7684\u5BC6\u7801\u89E3\u9501\u754C\u9762\u540E\u91CD\u8BD5\u3002",
|
|
12363
|
+
changed: "\u4E3B\u673A\u9501\u5C4F\u72B6\u6001\u6216\u5BC6\u7801\u8F93\u5165\u7126\u70B9\u5DF2\u53D8\u5316\uFF0C\u5DF2\u505C\u6B62\u8F93\u5165\uFF0C\u8BF7\u91CD\u8BD5\u3002",
|
|
12364
|
+
timeout: "\u4E3B\u673A\u5C4F\u5E55\u4ECD\u9501\u5B9A\uFF0C\u7CFB\u7EDF\u672A\u63A5\u53D7\u89E3\u9501\u8F93\u5165\uFF1B\u8BF7\u68C0\u67E5\u8F85\u52A9\u529F\u80FD\u6743\u9650\u548C\u9501\u5C4F\u754C\u9762\u540E\u91CD\u8BD5\u3002",
|
|
12365
|
+
password: "\u4E3B\u673A\u5BC6\u7801\u683C\u5F0F\u65E0\u6548\u3002"
|
|
12366
|
+
};
|
|
12367
|
+
var executeScreenLock = (file, args, input, signal) => new Promise((resolve4, reject) => {
|
|
12368
|
+
if (signal?.aborted) {
|
|
12369
|
+
reject(new ScreenLockError("\u8FDE\u63A5\u5DF2\u5173\u95ED\uFF0C\u5C4F\u5E55\u89E3\u9501\u5DF2\u53D6\u6D88\u3002"));
|
|
12370
|
+
return;
|
|
12371
|
+
}
|
|
12372
|
+
const child = spawn2(file, args, { stdio: ["pipe", "pipe", "pipe"], signal });
|
|
12373
|
+
let output = "", diagnostic = "", timedOut = false;
|
|
12374
|
+
const timer = setTimeout(() => {
|
|
12375
|
+
timedOut = true;
|
|
12376
|
+
child.kill("SIGKILL");
|
|
12377
|
+
}, args[0] === "unlock" ? 45e3 : 15e3);
|
|
12378
|
+
child.stdout.on("data", (chunk) => {
|
|
12379
|
+
output += String(chunk);
|
|
12380
|
+
if (output.length > 4096) child.kill("SIGKILL");
|
|
12381
|
+
});
|
|
12382
|
+
child.stderr.on("data", (chunk) => {
|
|
12383
|
+
diagnostic = (diagnostic + String(chunk)).slice(0, 4096);
|
|
12384
|
+
});
|
|
12385
|
+
child.stdin.on("error", () => {
|
|
12386
|
+
});
|
|
12387
|
+
child.on("error", () => {
|
|
12388
|
+
clearTimeout(timer);
|
|
12389
|
+
reject(new ScreenLockError(signal?.aborted ? "\u8FDE\u63A5\u5DF2\u5173\u95ED\uFF0C\u5C4F\u5E55\u89E3\u9501\u5DF2\u53D6\u6D88\u3002" : "\u5C4F\u5E55\u89E3\u9501\u4F9D\u8D56\u4E0D\u53EF\u7528\uFF1BmacOS \u9700\u8981 Xcode Command Line Tools\uFF0CLinux \u9700\u8981 systemd loginctl\u3002"));
|
|
12390
|
+
});
|
|
12391
|
+
child.on("close", (code) => {
|
|
12392
|
+
clearTimeout(timer);
|
|
12393
|
+
if (signal?.aborted) reject(new ScreenLockError("\u8FDE\u63A5\u5DF2\u5173\u95ED\uFF0C\u5C4F\u5E55\u89E3\u9501\u5DF2\u53D6\u6D88\u3002"));
|
|
12394
|
+
else if (code !== 0 || output.length > 4096) reject(new ScreenLockError(timedOut ? "\u5C4F\u5E55\u89E3\u9501\u64CD\u4F5C\u8D85\u65F6\uFF0C\u8BF7\u68C0\u67E5\u4E3B\u673A\u72B6\u6001\u540E\u91CD\u8BD5\u3002" : nativeErrors[diagnostic.trim()] ?? "\u5C4F\u5E55\u89E3\u9501\u64CD\u4F5C\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u4E3B\u673A\u7684\u56FE\u5F62\u4F1A\u8BDD\u3001\u7CFB\u7EDF\u6743\u9650\u53CA\u4F9D\u8D56\u3002"));
|
|
12395
|
+
else resolve4(output);
|
|
12396
|
+
});
|
|
12397
|
+
child.stdin.end(input);
|
|
12398
|
+
});
|
|
12399
|
+
var PlatformScreenLock = class {
|
|
12400
|
+
constructor(stateDirectory, system = process.platform, environment = process.env, execute3 = executeScreenLock, authenticate = verifyHostPassword, account = userInfo2()) {
|
|
12401
|
+
this.stateDirectory = stateDirectory;
|
|
12402
|
+
this.system = system;
|
|
12403
|
+
this.environment = environment;
|
|
12404
|
+
this.execute = execute3;
|
|
12405
|
+
this.authenticate = authenticate;
|
|
12406
|
+
this.account = account;
|
|
12407
|
+
}
|
|
12408
|
+
helper;
|
|
12409
|
+
failures = 0;
|
|
12410
|
+
blockedUntil = 0;
|
|
12411
|
+
macHelper() {
|
|
12412
|
+
if (this.helper) return this.helper;
|
|
12413
|
+
this.helper = (async () => {
|
|
12414
|
+
await mkdir5(this.stateDirectory, { recursive: true, mode: 448 });
|
|
12415
|
+
const target = join5(this.stateDirectory, "octopus-screen-unlock");
|
|
12416
|
+
const source = fileURLToPath3(new URL("../native/screen-unlock-macos.m", import.meta.url));
|
|
12417
|
+
const sourceInfo = await stat6(source), existing = await stat6(target).catch(() => null);
|
|
12418
|
+
if (existing?.isFile() && existing.mtimeMs >= sourceInfo.mtimeMs && (existing.mode & 63) === 0) return target;
|
|
12419
|
+
const temporary = `${target}.${randomUUID7()}`;
|
|
12420
|
+
try {
|
|
12421
|
+
await this.execute("/usr/bin/clang", ["-O2", "-fobjc-arc", "-mmacosx-version-min=12.3", "-framework", "Foundation", "-framework", "AppKit", "-framework", "ApplicationServices", source, "-o", temporary]);
|
|
12422
|
+
await chmod3(temporary, 448);
|
|
12423
|
+
await rename4(temporary, target);
|
|
12424
|
+
return target;
|
|
12425
|
+
} finally {
|
|
12426
|
+
await rm(temporary, { force: true });
|
|
12427
|
+
}
|
|
12428
|
+
})();
|
|
12429
|
+
this.helper.catch(() => {
|
|
12430
|
+
this.helper = void 0;
|
|
12431
|
+
});
|
|
12432
|
+
return this.helper;
|
|
12433
|
+
}
|
|
12434
|
+
async linuxSession(signal) {
|
|
12435
|
+
let ids;
|
|
12436
|
+
if (this.environment.XDG_SESSION_ID) ids = [this.environment.XDG_SESSION_ID];
|
|
12437
|
+
else ids = (await this.execute("loginctl", ["--no-ask-password", "list-sessions", "--no-legend"], void 0, signal)).trim().split("\n").map((line) => line.trim().split(/\s+/)[0]).filter(Boolean);
|
|
12438
|
+
const sessions = [];
|
|
12439
|
+
for (const id of ids) {
|
|
12440
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(id)) continue;
|
|
12441
|
+
const text = await this.execute("loginctl", ["--no-ask-password", "show-session", id, "-p", "User", "-p", "Type", "-p", "Active", "-p", "LockedHint"], void 0, signal);
|
|
12442
|
+
const properties = Object.fromEntries(text.trim().split("\n").map((line) => line.split("=")));
|
|
12443
|
+
if (properties.User !== String(this.account.uid) || !["x11", "wayland"].includes(properties.Type) || properties.Active !== "yes") continue;
|
|
12444
|
+
if (!["yes", "no"].includes(properties.LockedHint)) throw new ScreenLockError("\u684C\u9762\u672A\u62A5\u544A\u6709\u6548\u7684\u9501\u5C4F\u72B6\u6001\uFF0C\u65E0\u6CD5\u786E\u8BA4\u89E3\u9501\u3002");
|
|
12445
|
+
sessions.push({ id, locked: properties.LockedHint === "yes" });
|
|
12446
|
+
}
|
|
12447
|
+
if (sessions.length !== 1) throw new ScreenLockError("\u65E0\u6CD5\u786E\u5B9A Proxy \u7528\u6237\u7684\u552F\u4E00\u6D3B\u52A8\u56FE\u5F62\u4F1A\u8BDD\uFF0C\u8BF7\u8BBE\u7F6E\u6B63\u786E\u7684 XDG_SESSION_ID\u3002");
|
|
12448
|
+
return sessions[0];
|
|
12449
|
+
}
|
|
12450
|
+
parseStatus(text) {
|
|
12451
|
+
let data;
|
|
12452
|
+
try {
|
|
12453
|
+
data = JSON.parse(text);
|
|
12454
|
+
} catch {
|
|
12455
|
+
throw new ScreenLockError("\u4E3B\u673A\u8FD4\u56DE\u7684\u9501\u5C4F\u72B6\u6001\u65E0\u6548\u3002");
|
|
12456
|
+
}
|
|
12457
|
+
if (!data || typeof data.locked !== "boolean" || typeof data.canUnlock !== "boolean") throw new ScreenLockError("\u4E3B\u673A\u8FD4\u56DE\u7684\u9501\u5C4F\u72B6\u6001\u65E0\u6548\u3002");
|
|
12458
|
+
return { locked: data.locked, canUnlock: data.canUnlock, account: this.account.username, ...!data.canUnlock ? { message: nativeErrors.permission } : {} };
|
|
12459
|
+
}
|
|
12460
|
+
async status() {
|
|
12461
|
+
if (this.system === "darwin") return this.parseStatus(await this.execute(await this.macHelper(), ["status"]));
|
|
12462
|
+
if (this.system === "linux") return { locked: (await this.linuxSession()).locked, canUnlock: true, account: this.account.username };
|
|
12463
|
+
throw new ScreenLockError("\u5C4F\u5E55\u89E3\u9501\u4EC5\u652F\u6301 macOS 12.3 \u53CA\u4EE5\u4E0A\u548C\u4F7F\u7528 systemd \u7684 Linux \u56FE\u5F62\u4F1A\u8BDD\u3002");
|
|
12464
|
+
}
|
|
12465
|
+
async unlock(password, signal) {
|
|
12466
|
+
const check = () => {
|
|
12467
|
+
if (signal.aborted) throw new ScreenLockError("\u8FDE\u63A5\u5DF2\u5173\u95ED\uFF0C\u5C4F\u5E55\u89E3\u9501\u5DF2\u53D6\u6D88\u3002");
|
|
12468
|
+
};
|
|
12469
|
+
check();
|
|
12470
|
+
if (!password || Buffer.byteLength(password) > 1024 || /[\x00-\x1f\x7f]/.test(password)) throw new ScreenLockError("\u4E3B\u673A\u5BC6\u7801\u683C\u5F0F\u65E0\u6548\u3002");
|
|
12471
|
+
if (Date.now() < this.blockedUntil) throw new ScreenLockError("\u5BC6\u7801\u9A8C\u8BC1\u5931\u8D25\u6B21\u6570\u8FC7\u591A\uFF0C\u8BF7\u7B49\u5F85 30 \u79D2\u540E\u91CD\u8BD5\u3002");
|
|
12472
|
+
const status = await this.status();
|
|
12473
|
+
check();
|
|
12474
|
+
if (!status.locked) return status;
|
|
12475
|
+
if (!status.canUnlock) throw new ScreenLockError(status.message ?? "\u4E3B\u673A\u4E0D\u652F\u6301\u5C4F\u5E55\u89E3\u9501\u3002");
|
|
12476
|
+
let valid;
|
|
12477
|
+
try {
|
|
12478
|
+
valid = await this.authenticate(this.stateDirectory, this.account.username, password);
|
|
12479
|
+
} catch {
|
|
12480
|
+
throw new ScreenLockError("\u65E0\u6CD5\u9A8C\u8BC1\u4E3B\u673A\u5BC6\u7801\uFF0C\u8BF7\u68C0\u67E5\u4E3B\u673A\u8BA4\u8BC1\u8F85\u52A9\u7A0B\u5E8F\u3002");
|
|
12481
|
+
}
|
|
12482
|
+
check();
|
|
12483
|
+
if (!valid) {
|
|
12484
|
+
if (++this.failures >= 3) {
|
|
12485
|
+
this.blockedUntil = Date.now() + 3e4;
|
|
12486
|
+
this.failures = 0;
|
|
12487
|
+
}
|
|
12488
|
+
throw new ScreenLockError("\u4E3B\u673A\u767B\u5F55\u5BC6\u7801\u9519\u8BEF\uFF0C\u8BF7\u91CD\u65B0\u8F93\u5165\u3002");
|
|
12489
|
+
}
|
|
12490
|
+
this.failures = 0;
|
|
12491
|
+
if (this.system === "darwin") {
|
|
12492
|
+
const helper = await this.macHelper();
|
|
12493
|
+
check();
|
|
12494
|
+
const bytes = Buffer.from(password, "utf8"), input = Buffer.alloc(bytes.length + 2);
|
|
12495
|
+
input.writeUInt16BE(bytes.length);
|
|
12496
|
+
bytes.copy(input, 2);
|
|
12497
|
+
bytes.fill(0);
|
|
12498
|
+
let result;
|
|
12499
|
+
try {
|
|
12500
|
+
result = this.parseStatus(await this.execute(helper, ["unlock"], input, signal));
|
|
12501
|
+
} finally {
|
|
12502
|
+
input.fill(0);
|
|
12503
|
+
}
|
|
12504
|
+
check();
|
|
12505
|
+
if (result.locked) throw new ScreenLockError(nativeErrors.timeout);
|
|
12506
|
+
return result;
|
|
12507
|
+
}
|
|
12508
|
+
const session = await this.linuxSession(signal);
|
|
12509
|
+
check();
|
|
12510
|
+
if (session.locked) await this.execute("loginctl", ["--no-ask-password", "unlock-session", session.id], void 0, signal);
|
|
12511
|
+
for (let i = 0; i < 20; i++) {
|
|
12512
|
+
check();
|
|
12513
|
+
const current = await this.linuxSession(signal);
|
|
12514
|
+
if (current.id !== session.id) throw new ScreenLockError("\u4E3B\u673A\u6D3B\u52A8\u4F1A\u8BDD\u5DF2\u53D8\u5316\uFF0C\u5DF2\u505C\u6B62\u89E3\u9501\u3002");
|
|
12515
|
+
if (!current.locked) return { ...status, locked: false };
|
|
12516
|
+
await delay(250, void 0, { signal }).catch(() => check());
|
|
12517
|
+
}
|
|
12518
|
+
throw new ScreenLockError("\u684C\u9762\u9501\u5C4F\u7A0B\u5E8F\u672A\u54CD\u5E94\u89E3\u9501\u8BF7\u6C42\uFF0C\u8BF7\u786E\u8BA4\u5B83\u652F\u6301 systemd-logind \u7684 Unlock \u4FE1\u53F7\u3002");
|
|
12519
|
+
}
|
|
12520
|
+
};
|
|
12521
|
+
|
|
12274
12522
|
// src/journal.ts
|
|
12275
12523
|
var Journal = class {
|
|
12276
12524
|
constructor(directory) {
|
|
@@ -12286,8 +12534,8 @@ var Journal = class {
|
|
|
12286
12534
|
return active.result;
|
|
12287
12535
|
}
|
|
12288
12536
|
const run4 = async () => {
|
|
12289
|
-
const path =
|
|
12290
|
-
const readOnly = isReadOperation(command.operation) || isAuthentication(command.operation);
|
|
12537
|
+
const path = join6(this.directory, `${command.id}.json`);
|
|
12538
|
+
const readOnly = command.operation.startsWith("desktop.") || isReadOperation(command.operation) || isAuthentication(command.operation);
|
|
12291
12539
|
if (!readOnly) {
|
|
12292
12540
|
const old = await readJson(path);
|
|
12293
12541
|
if (old) {
|
|
@@ -12300,7 +12548,7 @@ var Journal = class {
|
|
|
12300
12548
|
try {
|
|
12301
12549
|
response = { type: "response", id: command.id, botId: command.botId, status: "completed", data: await action() };
|
|
12302
12550
|
} catch (error) {
|
|
12303
|
-
response = this.failure(command, isAuthentication(command.operation) ? "\u8BA4\u8BC1\u64CD\u4F5C\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u7F16\u7801\u5DE5\u5177\u7684\u767B\u5F55\u72B6\u6001\u540E\u91CD\u8BD5" : error instanceof Error ? error.message : "Operation failed");
|
|
12551
|
+
response = this.failure(command, command.operation === "desktop.unlock" ? error instanceof ScreenLockError ? error.message : "\u5C4F\u5E55\u89E3\u9501\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u4E3B\u673A\u72B6\u6001\u4E0E\u6743\u9650\u540E\u91CD\u8BD5\u3002" : isAuthentication(command.operation) ? "\u8BA4\u8BC1\u64CD\u4F5C\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u7F16\u7801\u5DE5\u5177\u7684\u767B\u5F55\u72B6\u6001\u540E\u91CD\u8BD5" : error instanceof Error ? error.message : "Operation failed");
|
|
12304
12552
|
if (!readOnly && error instanceof Error && ["TimeoutError", "AbortError", "UnknownResultError"].includes(error.name)) response.status = "unknown";
|
|
12305
12553
|
}
|
|
12306
12554
|
if (!readOnly) await atomicJson(path, { hash, response, finishedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
@@ -12322,6 +12570,7 @@ var Journal = class {
|
|
|
12322
12570
|
return result;
|
|
12323
12571
|
}
|
|
12324
12572
|
queueKey(c) {
|
|
12573
|
+
if (c.operation.startsWith("desktop.")) return void 0;
|
|
12325
12574
|
if (isReadOperation(c.operation) || isAuthentication(c.operation)) return void 0;
|
|
12326
12575
|
const [group] = c.operation.split(".");
|
|
12327
12576
|
if (c.operation === "sessions.abort") return JSON.stringify(["abort", c.projectId, c.sessionId]);
|
|
@@ -12406,8 +12655,8 @@ function pageHistory(input, payload) {
|
|
|
12406
12655
|
|
|
12407
12656
|
// src/artifacts.ts
|
|
12408
12657
|
import { createHash as createHash5 } from "node:crypto";
|
|
12409
|
-
import { join as
|
|
12410
|
-
import { readFile as readFile4, writeFile as writeFile2, mkdir as
|
|
12658
|
+
import { join as join7 } from "node:path";
|
|
12659
|
+
import { readFile as readFile4, writeFile as writeFile2, mkdir as mkdir6, stat as stat7 } from "node:fs/promises";
|
|
12411
12660
|
var Artifacts = class {
|
|
12412
12661
|
constructor(workspace, directory, emit) {
|
|
12413
12662
|
this.workspace = workspace;
|
|
@@ -12423,7 +12672,7 @@ var Artifacts = class {
|
|
|
12423
12672
|
}
|
|
12424
12673
|
index(project) {
|
|
12425
12674
|
this.workspace.project(project);
|
|
12426
|
-
return
|
|
12675
|
+
return join7(this.directory, project, "index.json");
|
|
12427
12676
|
}
|
|
12428
12677
|
async list(project) {
|
|
12429
12678
|
return Object.values(await readJson(this.index(project)) ?? {});
|
|
@@ -12461,9 +12710,9 @@ var Artifacts = class {
|
|
|
12461
12710
|
changed = true;
|
|
12462
12711
|
}
|
|
12463
12712
|
if (previous?.hash !== hash || previous.deleted) {
|
|
12464
|
-
const dir =
|
|
12465
|
-
await
|
|
12466
|
-
await writeFile2(
|
|
12713
|
+
const dir = join7(this.directory, project, "blobs");
|
|
12714
|
+
await mkdir6(dir, { recursive: true, mode: 448 });
|
|
12715
|
+
await writeFile2(join7(dir, hash), data, { mode: 384 });
|
|
12467
12716
|
record2.revisions.push({ hash, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
12468
12717
|
changed = true;
|
|
12469
12718
|
}
|
|
@@ -12479,7 +12728,7 @@ var Artifacts = class {
|
|
|
12479
12728
|
}
|
|
12480
12729
|
for (const item of Object.values(index)) if (!seen.has(item.path) && !item.revisions.at(-1)?.deleted) {
|
|
12481
12730
|
try {
|
|
12482
|
-
await
|
|
12731
|
+
await stat7(await this.workspace.path(project, item.path));
|
|
12483
12732
|
} catch (e) {
|
|
12484
12733
|
if (e.code === "ENOENT") {
|
|
12485
12734
|
item.revisions.push({ hash: "", timestamp: (/* @__PURE__ */ new Date()).toISOString(), deleted: true });
|
|
@@ -12499,7 +12748,7 @@ var Artifacts = class {
|
|
|
12499
12748
|
}
|
|
12500
12749
|
async read(project, path, hash) {
|
|
12501
12750
|
if (!/^[a-f0-9]{64}$/.test(hash) || !(await this.history(project, path)).some((r) => r.hash === hash)) throw new Error("Unknown revision");
|
|
12502
|
-
return readFile4(
|
|
12751
|
+
return readFile4(join7(this.directory, project, "blobs", hash), "utf8");
|
|
12503
12752
|
}
|
|
12504
12753
|
associate(project, path, sessionId) {
|
|
12505
12754
|
return this.serial(project, () => this.associateUnlocked(project, path, sessionId));
|
|
@@ -12516,82 +12765,9 @@ var Artifacts = class {
|
|
|
12516
12765
|
// src/secure-chat.ts
|
|
12517
12766
|
import WebSocket from "ws";
|
|
12518
12767
|
import { Duplex } from "node:stream";
|
|
12519
|
-
import { createPublicKey, randomUUID as
|
|
12520
|
-
import { userInfo as
|
|
12768
|
+
import { createPublicKey, randomUUID as randomUUID8, verify } from "node:crypto";
|
|
12769
|
+
import { userInfo as userInfo3 } from "node:os";
|
|
12521
12770
|
import { TLSSocket, createSecureContext } from "node:tls";
|
|
12522
|
-
|
|
12523
|
-
// src/host-auth.ts
|
|
12524
|
-
import { spawn } from "node:child_process";
|
|
12525
|
-
import { chmod as chmod2, mkdir as mkdir5, rename as rename3, stat as stat6, unlink as unlink4 } from "node:fs/promises";
|
|
12526
|
-
import { userInfo, platform as platform2 } from "node:os";
|
|
12527
|
-
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
12528
|
-
import { join as join6 } from "node:path";
|
|
12529
|
-
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
12530
|
-
function helperPath(stateDirectory) {
|
|
12531
|
-
return join6(stateDirectory, "octopus-host-auth");
|
|
12532
|
-
}
|
|
12533
|
-
async function ensureHostAuth(stateDirectory) {
|
|
12534
|
-
await mkdir5(stateDirectory, { recursive: true, mode: 448 });
|
|
12535
|
-
const target = helperPath(stateDirectory);
|
|
12536
|
-
const system = platform2();
|
|
12537
|
-
if (system !== "darwin" && system !== "linux") throw new Error("Host authentication is supported only on macOS and Linux");
|
|
12538
|
-
const mac = system === "darwin";
|
|
12539
|
-
const source = fileURLToPath2(new URL(mac ? "../native/auth-macos.m" : "../native/auth-linux.c", import.meta.url));
|
|
12540
|
-
const sourceInfo = await stat6(source);
|
|
12541
|
-
const existing = await stat6(target).catch(() => null);
|
|
12542
|
-
if (existing?.isFile() && existing.mtimeMs >= sourceInfo.mtimeMs && (existing.mode & 63) === 0) return target;
|
|
12543
|
-
const temporary = `${target}.${randomUUID6()}`;
|
|
12544
|
-
const args = mac ? ["-O2", "-framework", "Foundation", "-framework", "OpenDirectory", source, "-o", target] : ["-O2", source, "-o", temporary, "-lpam"];
|
|
12545
|
-
if (mac) args[args.length - 1] = temporary;
|
|
12546
|
-
const compiler = spawn(mac ? "clang" : "cc", args, { stdio: ["ignore", "ignore", "pipe"] });
|
|
12547
|
-
let diagnostics = "";
|
|
12548
|
-
compiler.stderr.on("data", (chunk) => {
|
|
12549
|
-
diagnostics += String(chunk).slice(0, 4096);
|
|
12550
|
-
});
|
|
12551
|
-
const exit = await new Promise((resolve4, reject) => {
|
|
12552
|
-
compiler.on("error", reject);
|
|
12553
|
-
compiler.on("exit", (code) => resolve4(code ?? 1));
|
|
12554
|
-
});
|
|
12555
|
-
if (exit !== 0) {
|
|
12556
|
-
await unlink4(temporary).catch(() => {
|
|
12557
|
-
});
|
|
12558
|
-
throw new Error(`Host authentication helper build failed: ${diagnostics.trim()}`);
|
|
12559
|
-
}
|
|
12560
|
-
try {
|
|
12561
|
-
await chmod2(temporary, 448);
|
|
12562
|
-
await rename3(temporary, target);
|
|
12563
|
-
return target;
|
|
12564
|
-
} finally {
|
|
12565
|
-
await unlink4(temporary).catch(() => {
|
|
12566
|
-
});
|
|
12567
|
-
}
|
|
12568
|
-
}
|
|
12569
|
-
async function verifyHostPassword(stateDirectory, username, password) {
|
|
12570
|
-
if (username !== userInfo().username || !password || Buffer.byteLength(password) > 1024) return false;
|
|
12571
|
-
const path = await ensureHostAuth(stateDirectory);
|
|
12572
|
-
const child = spawn(path, [], { stdio: ["pipe", "pipe", "ignore"] });
|
|
12573
|
-
const bytes = Buffer.from(password, "utf8");
|
|
12574
|
-
const header2 = Buffer.alloc(2);
|
|
12575
|
-
header2.writeUInt16BE(bytes.length);
|
|
12576
|
-
child.stdin.end(Buffer.concat([header2, bytes]));
|
|
12577
|
-
bytes.fill(0);
|
|
12578
|
-
let output = "";
|
|
12579
|
-
child.stdout.on("data", (chunk) => {
|
|
12580
|
-
output += String(chunk).slice(0, 16);
|
|
12581
|
-
});
|
|
12582
|
-
const timer = setTimeout(() => child.kill("SIGKILL"), 1e4);
|
|
12583
|
-
try {
|
|
12584
|
-
const code = await new Promise((resolve4, reject) => {
|
|
12585
|
-
child.on("error", reject);
|
|
12586
|
-
child.on("exit", (value) => resolve4(value ?? 1));
|
|
12587
|
-
});
|
|
12588
|
-
return code === 0 && output.trim() === "OK";
|
|
12589
|
-
} finally {
|
|
12590
|
-
clearTimeout(timer);
|
|
12591
|
-
}
|
|
12592
|
-
}
|
|
12593
|
-
|
|
12594
|
-
// src/secure-chat.ts
|
|
12595
12771
|
var OPEN = 1;
|
|
12596
12772
|
var DATA = 2;
|
|
12597
12773
|
var CLOSE = 3;
|
|
@@ -12649,13 +12825,13 @@ var SecureChat = class {
|
|
|
12649
12825
|
}
|
|
12650
12826
|
outer;
|
|
12651
12827
|
sessions = /* @__PURE__ */ new Map();
|
|
12652
|
-
epoch =
|
|
12828
|
+
epoch = randomUUID8();
|
|
12653
12829
|
sequence = 0;
|
|
12654
12830
|
events = [];
|
|
12655
12831
|
eventBytes = 0;
|
|
12656
12832
|
stopping = false;
|
|
12657
12833
|
context;
|
|
12658
|
-
serviceAccount =
|
|
12834
|
+
serviceAccount = userInfo3().username;
|
|
12659
12835
|
timer;
|
|
12660
12836
|
start() {
|
|
12661
12837
|
this.connect();
|
|
@@ -12861,7 +13037,7 @@ var SecureChat = class {
|
|
|
12861
13037
|
};
|
|
12862
13038
|
|
|
12863
13039
|
// src/proxy-snapshot.ts
|
|
12864
|
-
import { randomUUID as
|
|
13040
|
+
import { randomUUID as randomUUID9 } from "node:crypto";
|
|
12865
13041
|
import { execFile as execFile3 } from "node:child_process";
|
|
12866
13042
|
import { promisify as promisify3 } from "node:util";
|
|
12867
13043
|
var execute2 = promisify3(execFile3);
|
|
@@ -12915,7 +13091,7 @@ async function buildProxySnapshot(config, runtime, adapter, requestedProjectId)
|
|
|
12915
13091
|
let modelCatalogStatus = capabilities.includes("configuration.models") ? "unavailable" : "unsupported";
|
|
12916
13092
|
if (status.healthy === true && projectId && capabilities.includes("configuration.models")) {
|
|
12917
13093
|
try {
|
|
12918
|
-
const command = { type: "command", id:
|
|
13094
|
+
const command = { type: "command", id: randomUUID9(), botId: config.botId, operation: "configuration.models", projectId, payload: {} };
|
|
12919
13095
|
models.push(...publicModels(await adapter.dispatch(command)));
|
|
12920
13096
|
modelCatalogStatus = models.length ? "available" : "unavailable";
|
|
12921
13097
|
} catch {
|
|
@@ -12942,11 +13118,11 @@ async function buildProxySnapshot(config, runtime, adapter, requestedProjectId)
|
|
|
12942
13118
|
}
|
|
12943
13119
|
|
|
12944
13120
|
// src/media.ts
|
|
12945
|
-
import { createHash as createHash6, randomUUID as
|
|
13121
|
+
import { createHash as createHash6, randomUUID as randomUUID10 } from "node:crypto";
|
|
12946
13122
|
import { constants as constants3 } from "node:fs";
|
|
12947
13123
|
import { open as open4, unlink as unlink5, realpath as realpath3 } from "node:fs/promises";
|
|
12948
13124
|
import { basename as basename3, isAbsolute as isAbsolute3, relative as relative3, resolve as resolve3, sep as sep2 } from "node:path";
|
|
12949
|
-
import { fileURLToPath as
|
|
13125
|
+
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
12950
13126
|
|
|
12951
13127
|
// src/screenshot-delivery.ts
|
|
12952
13128
|
var instructions = "\n\n[Octopus screenshot delivery]\nFor requested UI tests/screenshots, run the real application with the available test tools. Save each requested key-step or failure screenshot under the authorized project (prefer test-results/screenshots/ with a unique filename). After saving, immediately report it as  in your progress or final response. Octopus Proxy will attach the actual file to Claw. Never claim a screenshot or test succeeded without producing it; explain environment/tool blockers. Do not substitute generated mockups for test screenshots.\n[/Octopus screenshot delivery]";
|
|
@@ -13063,7 +13239,7 @@ var RemoteMedia = class {
|
|
|
13063
13239
|
} else {
|
|
13064
13240
|
let path = value.path ?? value.localPath ?? value.artifactUri ?? source.path ?? source.localPath ?? url;
|
|
13065
13241
|
if (typeof path !== "string") throw new Error("Image has no readable data or project path");
|
|
13066
|
-
if (path.startsWith("file:")) path =
|
|
13242
|
+
if (path.startsWith("file:")) path = fileURLToPath4(path);
|
|
13067
13243
|
if (/^[a-z][a-z0-9+.-]*:/i.test(path)) throw new Error("Only images inside the authorized project can be returned");
|
|
13068
13244
|
if (isAbsolute3(path)) {
|
|
13069
13245
|
const root = await this.workspace.root(project);
|
|
@@ -13159,7 +13335,7 @@ var RemoteMedia = class {
|
|
|
13159
13335
|
if (bytes.length === 0 || bytes.length > this.imageLimit()) throw new Error("Image is empty or exceeds size limit");
|
|
13160
13336
|
const extension = { "image/png": "png", "image/jpeg": "jpg", "image/gif": "gif", "image/webp": "webp", "application/pdf": "pdf" }[mime] ?? "bin";
|
|
13161
13337
|
const directory = await this.workspace.attachmentDirectory(project, "media", sha256), path = directory + "/asset." + extension;
|
|
13162
|
-
const temp = await this.workspace.path(project, directory + "/.octopus-media-" +
|
|
13338
|
+
const temp = await this.workspace.path(project, directory + "/.octopus-media-" + randomUUID10(), true);
|
|
13163
13339
|
const file = await open4(temp, constants3.O_WRONLY | constants3.O_CREAT | constants3.O_EXCL | constants3.O_NOFOLLOW, 384);
|
|
13164
13340
|
try {
|
|
13165
13341
|
await file.writeFile(bytes);
|
|
@@ -14577,7 +14753,7 @@ function applyEdits(text, edits) {
|
|
|
14577
14753
|
|
|
14578
14754
|
// src/project-config.ts
|
|
14579
14755
|
import { readFile as readFile5, writeFile as writeFile3, unlink as unlink6 } from "node:fs/promises";
|
|
14580
|
-
import { createHash as createHash7, randomUUID as
|
|
14756
|
+
import { createHash as createHash7, randomUUID as randomUUID11 } from "node:crypto";
|
|
14581
14757
|
var ProjectConfig = class {
|
|
14582
14758
|
constructor(workspace, request) {
|
|
14583
14759
|
this.workspace = workspace;
|
|
@@ -14628,7 +14804,7 @@ var ProjectConfig = class {
|
|
|
14628
14804
|
if (!this.validator(parse2(updated))) throw new Error("Merged OpenCode configuration is invalid: " + JSON.stringify(this.validator.errors));
|
|
14629
14805
|
if (exists) await this.workspace.write(project, name, updated, createHash7("sha256").update(original).digest("hex"));
|
|
14630
14806
|
else {
|
|
14631
|
-
const temporary = await this.workspace.path(project, `.octopus-config-${
|
|
14807
|
+
const temporary = await this.workspace.path(project, `.octopus-config-${randomUUID11()}.tmp`, true);
|
|
14632
14808
|
try {
|
|
14633
14809
|
await writeFile3(temporary, updated, { mode: 384, flag: "wx" });
|
|
14634
14810
|
await this.workspace.publish(project, temporary, name);
|
|
@@ -15086,11 +15262,11 @@ function redact(value) {
|
|
|
15086
15262
|
}
|
|
15087
15263
|
|
|
15088
15264
|
// src/runtime.ts
|
|
15089
|
-
import { spawn as
|
|
15265
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
15090
15266
|
import { createHash as createHash8 } from "node:crypto";
|
|
15091
|
-
import { mkdir as
|
|
15267
|
+
import { mkdir as mkdir7, open as open5, readFile as readFile6, rm as rm2, writeFile as writeFile4 } from "node:fs/promises";
|
|
15092
15268
|
import { createServer } from "node:net";
|
|
15093
|
-
import { join as
|
|
15269
|
+
import { join as join8 } from "node:path";
|
|
15094
15270
|
var Runtime = class {
|
|
15095
15271
|
constructor(config) {
|
|
15096
15272
|
this.config = config;
|
|
@@ -15106,7 +15282,7 @@ var Runtime = class {
|
|
|
15106
15282
|
return this.endpoint;
|
|
15107
15283
|
}
|
|
15108
15284
|
get endpointStatePath() {
|
|
15109
|
-
return
|
|
15285
|
+
return join8(this.config.stateDirectory, "opencode-endpoint.json");
|
|
15110
15286
|
}
|
|
15111
15287
|
get passwordHash() {
|
|
15112
15288
|
return createHash8("sha256").update(this.config.tool.password).digest("hex");
|
|
@@ -15168,13 +15344,13 @@ var Runtime = class {
|
|
|
15168
15344
|
throw new Error(`OpenCode \u5730\u5740 ${url.origin} \u5DF2\u6709 HTTP \u670D\u52A1\uFF0C\u4F46\u5065\u5EB7\u68C0\u67E5\u5931\u8D25\uFF08HTTP ${response.status}\uFF09\uFF1B\u8BF7\u68C0\u67E5\u670D\u52A1\u5BC6\u7801\u6216\u7AEF\u53E3`);
|
|
15169
15345
|
}
|
|
15170
15346
|
}
|
|
15171
|
-
await
|
|
15172
|
-
const logPath =
|
|
15347
|
+
await mkdir7(this.config.stateDirectory, { recursive: true, mode: 448 });
|
|
15348
|
+
const logPath = join8(this.config.stateDirectory, "opencode-serve.log");
|
|
15173
15349
|
const log = await open5(logPath, "a", 384);
|
|
15174
15350
|
const env = { ...process.env, OPENCODE_SERVER_PASSWORD: this.config.tool.password, OPENCODE_SERVER_USERNAME: "opencode" };
|
|
15175
15351
|
let child;
|
|
15176
15352
|
try {
|
|
15177
|
-
child =
|
|
15353
|
+
child = spawn3(this.config.tool.command, ["serve", "--hostname", url.hostname === "[::1]" ? "::1" : url.hostname, "--port", url.port || "80"], {
|
|
15178
15354
|
cwd: this.config.projects[0].directory,
|
|
15179
15355
|
env,
|
|
15180
15356
|
detached: true,
|
|
@@ -15212,7 +15388,7 @@ var Runtime = class {
|
|
|
15212
15388
|
await new Promise((resolve4) => setTimeout(resolve4, 250));
|
|
15213
15389
|
}
|
|
15214
15390
|
if (child.exitCode === null && child.signalCode === null) child.kill("SIGTERM");
|
|
15215
|
-
await
|
|
15391
|
+
await rm2(this.endpointStatePath, { force: true }).catch(() => {
|
|
15216
15392
|
});
|
|
15217
15393
|
this.endpoint = this.config.tool.baseUrl;
|
|
15218
15394
|
throw new Error(`OpenCode \u542F\u52A8\u5931\u8D25\u6216\u5065\u5EB7\u68C0\u67E5\u8D85\u65F6\uFF1B\u8BF7\u68C0\u67E5 ${logPath}\u3001\u670D\u52A1\u5730\u5740\u548C\u5BC6\u7801`);
|
|
@@ -15261,24 +15437,24 @@ var Runtime = class {
|
|
|
15261
15437
|
|
|
15262
15438
|
// src/tools/zcode-env.ts
|
|
15263
15439
|
import { existsSync } from "node:fs";
|
|
15264
|
-
import { dirname as dirname4, join as
|
|
15440
|
+
import { dirname as dirname4, join as join9 } from "node:path";
|
|
15265
15441
|
function zcodeEnvironment(command) {
|
|
15266
15442
|
const env = { ...process.env };
|
|
15267
15443
|
if (!env.ZCODE_BUILTIN_PROVIDER_CONFIG_FILE) {
|
|
15268
|
-
const bundled =
|
|
15444
|
+
const bundled = join9(dirname4(command), "..", "config", "provider", "zcode-builtin.json");
|
|
15269
15445
|
if (existsSync(bundled)) env.ZCODE_BUILTIN_PROVIDER_CONFIG_FILE = bundled;
|
|
15270
15446
|
}
|
|
15271
15447
|
return env;
|
|
15272
15448
|
}
|
|
15273
15449
|
|
|
15274
15450
|
// src/tools/stdio-runtime.ts
|
|
15275
|
-
import { spawn as
|
|
15451
|
+
import { spawn as spawn4, execFile as execFile4 } from "node:child_process";
|
|
15276
15452
|
import { promisify as promisify4 } from "node:util";
|
|
15277
|
-
import { mkdir as
|
|
15278
|
-
import { join as
|
|
15453
|
+
import { mkdir as mkdir8, open as open6 } from "node:fs/promises";
|
|
15454
|
+
import { join as join10 } from "node:path";
|
|
15279
15455
|
|
|
15280
15456
|
// src/tools/line-rpc.ts
|
|
15281
|
-
import { randomUUID as
|
|
15457
|
+
import { randomUUID as randomUUID12 } from "node:crypto";
|
|
15282
15458
|
import { crc32 } from "node:zlib";
|
|
15283
15459
|
import { StringDecoder } from "node:string_decoder";
|
|
15284
15460
|
function interrupted(message) {
|
|
@@ -15463,7 +15639,7 @@ var LineRpc = class {
|
|
|
15463
15639
|
}
|
|
15464
15640
|
};
|
|
15465
15641
|
function wrapEnvelope(frame) {
|
|
15466
|
-
return { wireVersion: 3, kind: "complete", deliveryKind: "desktop-continuous", logicalFrameId:
|
|
15642
|
+
return { wireVersion: 3, kind: "complete", deliveryKind: "desktop-continuous", logicalFrameId: randomUUID12(), logicalFrameOrdinal: 1, topic: "outbound", subscriptionId: "outbound", frame };
|
|
15467
15643
|
}
|
|
15468
15644
|
|
|
15469
15645
|
// src/tools/stdio-runtime.ts
|
|
@@ -15639,12 +15815,12 @@ ${bytes.toString("utf8")}`);
|
|
|
15639
15815
|
if (inFlight) return inFlight;
|
|
15640
15816
|
if (this.children.has(projectId)) return;
|
|
15641
15817
|
const spawnOnce = (async () => {
|
|
15642
|
-
const logPath =
|
|
15643
|
-
await
|
|
15818
|
+
const logPath = join10(this.config.stateDirectory, `${this.kind}-${projectId}.log`);
|
|
15819
|
+
await mkdir8(this.config.stateDirectory, { recursive: true, mode: 448 });
|
|
15644
15820
|
const log = await open6(logPath, "a", 384);
|
|
15645
15821
|
let child;
|
|
15646
15822
|
try {
|
|
15647
|
-
child =
|
|
15823
|
+
child = spawn4(this.config.tool.command, [...this.descriptor.serverArgs], { cwd: directory, env: this.kind === "zcode" ? zcodeEnvironment(this.config.tool.command) : { ...process.env }, stdio: ["pipe", "pipe", log.fd] });
|
|
15648
15824
|
} catch (error) {
|
|
15649
15825
|
await log.close();
|
|
15650
15826
|
throw error;
|
|
@@ -15693,14 +15869,14 @@ ${bytes.toString("utf8")}`);
|
|
|
15693
15869
|
return;
|
|
15694
15870
|
}
|
|
15695
15871
|
this.attempts.set(projectId, attempts);
|
|
15696
|
-
const
|
|
15872
|
+
const delay2 = Math.min(3e4, 1e3 * 2 ** attempts);
|
|
15697
15873
|
this.timers.set(projectId, setTimeout(() => {
|
|
15698
15874
|
this.timers.delete(projectId);
|
|
15699
15875
|
if (!this.wanted || this.detached) return;
|
|
15700
15876
|
const project = this.config.projects.find((entry) => entry.id === projectId);
|
|
15701
15877
|
if (project) void this.track(projectId, project.directory).catch(() => {
|
|
15702
15878
|
});
|
|
15703
|
-
},
|
|
15879
|
+
}, delay2));
|
|
15704
15880
|
}
|
|
15705
15881
|
kill(projectId) {
|
|
15706
15882
|
clearTimeout(this.timers.get(projectId));
|
|
@@ -15719,7 +15895,7 @@ ${bytes.toString("utf8")}`);
|
|
|
15719
15895
|
};
|
|
15720
15896
|
|
|
15721
15897
|
// src/tools/codex.ts
|
|
15722
|
-
import { join as
|
|
15898
|
+
import { join as join11 } from "node:path";
|
|
15723
15899
|
|
|
15724
15900
|
// src/adapter.ts
|
|
15725
15901
|
var DAEMON_OPERATIONS = [
|
|
@@ -15755,14 +15931,16 @@ var DAEMON_OPERATIONS = [
|
|
|
15755
15931
|
"terminal.attach",
|
|
15756
15932
|
"desktop.list",
|
|
15757
15933
|
"desktop.capture",
|
|
15758
|
-
"desktop.read"
|
|
15934
|
+
"desktop.read",
|
|
15935
|
+
"desktop.lockStatus",
|
|
15936
|
+
"desktop.unlock"
|
|
15759
15937
|
];
|
|
15760
15938
|
function unsupportedOperation(kind, operation) {
|
|
15761
15939
|
throw new Error(`Unsupported ${kind} operation: ${operation}`);
|
|
15762
15940
|
}
|
|
15763
15941
|
|
|
15764
15942
|
// src/tools/codex.ts
|
|
15765
|
-
import { randomUUID as
|
|
15943
|
+
import { randomUUID as randomUUID13 } from "node:crypto";
|
|
15766
15944
|
var OPERATIONS = [
|
|
15767
15945
|
...DAEMON_OPERATIONS,
|
|
15768
15946
|
...AUTH_OPERATIONS,
|
|
@@ -15857,7 +16035,7 @@ var CodexAdapter = class {
|
|
|
15857
16035
|
}
|
|
15858
16036
|
onServerRequest(project, id, method, params, respond) {
|
|
15859
16037
|
const detail = params ?? {};
|
|
15860
|
-
const requestId =
|
|
16038
|
+
const requestId = randomUUID13();
|
|
15861
16039
|
const threadId = String(detail.threadId ?? "");
|
|
15862
16040
|
if (["item/permissions/requestApproval", "item/commandExecution/requestApproval", "item/fileChange/requestApproval"].includes(method)) {
|
|
15863
16041
|
this.approvals.set(requestId, { project, method, requestId, threadId, detail, respond });
|
|
@@ -15872,21 +16050,21 @@ var CodexAdapter = class {
|
|
|
15872
16050
|
respond({ decision: "decline" });
|
|
15873
16051
|
}
|
|
15874
16052
|
async index(project) {
|
|
15875
|
-
const stored = await readJson(
|
|
16053
|
+
const stored = await readJson(join11(this.runtime.config.stateDirectory, `codex-${project}.json`));
|
|
15876
16054
|
return { threadIds: stored?.threadIds ?? [] };
|
|
15877
16055
|
}
|
|
15878
16056
|
async remember(project, threadId) {
|
|
15879
16057
|
const index = await this.index(project);
|
|
15880
16058
|
if (!index.threadIds.includes(threadId)) {
|
|
15881
16059
|
index.threadIds.push(threadId);
|
|
15882
|
-
await atomicJson(
|
|
16060
|
+
await atomicJson(join11(this.runtime.config.stateDirectory, `codex-${project}.json`), index);
|
|
15883
16061
|
}
|
|
15884
16062
|
}
|
|
15885
16063
|
async forget(project, threadId) {
|
|
15886
16064
|
const index = await this.index(project);
|
|
15887
16065
|
if (index.threadIds.includes(threadId)) {
|
|
15888
16066
|
index.threadIds = index.threadIds.filter((entry) => entry !== threadId);
|
|
15889
|
-
await atomicJson(
|
|
16067
|
+
await atomicJson(join11(this.runtime.config.stateDirectory, `codex-${project}.json`), index);
|
|
15890
16068
|
}
|
|
15891
16069
|
}
|
|
15892
16070
|
async resume(project, threadId, rpc) {
|
|
@@ -16229,8 +16407,8 @@ var CodexAdapter = class {
|
|
|
16229
16407
|
};
|
|
16230
16408
|
|
|
16231
16409
|
// src/tools/zcode.ts
|
|
16232
|
-
import { spawn as
|
|
16233
|
-
import { randomUUID as
|
|
16410
|
+
import { spawn as spawn5 } from "node:child_process";
|
|
16411
|
+
import { randomUUID as randomUUID14 } from "node:crypto";
|
|
16234
16412
|
import { realpath as realpath5 } from "node:fs/promises";
|
|
16235
16413
|
var OPERATIONS2 = [
|
|
16236
16414
|
...DAEMON_OPERATIONS,
|
|
@@ -16285,7 +16463,7 @@ var ZcodeAdapter = class {
|
|
|
16285
16463
|
}
|
|
16286
16464
|
onServerRequest(project, id, method, params, respond, respondError) {
|
|
16287
16465
|
const detail = params ?? {};
|
|
16288
|
-
const requestId =
|
|
16466
|
+
const requestId = randomUUID14();
|
|
16289
16467
|
const sessionId = String(detail.sessionId ?? "");
|
|
16290
16468
|
if (method === "session/requestRuntimePreferences") {
|
|
16291
16469
|
respond({ nativeSearchEnhancementsEnabled: false });
|
|
@@ -16422,7 +16600,7 @@ var ZcodeAdapter = class {
|
|
|
16422
16600
|
const flow = this.auth.create(project, () => {
|
|
16423
16601
|
child?.kill("SIGTERM");
|
|
16424
16602
|
});
|
|
16425
|
-
child =
|
|
16603
|
+
child = spawn5(this.runtime.config.tool.command, ["login", "--no-browser", "--json"], { cwd: await this.workspace.root(project), env: zcodeEnvironment(this.runtime.config.tool.command), stdio: ["ignore", "pipe", "pipe"] });
|
|
16426
16604
|
let output = "", diagnostic = "";
|
|
16427
16605
|
child.stdout?.on("data", (data) => {
|
|
16428
16606
|
output = (output + data.toString()).slice(-65536);
|
|
@@ -16560,8 +16738,8 @@ var ZcodeAdapter = class {
|
|
|
16560
16738
|
};
|
|
16561
16739
|
|
|
16562
16740
|
// src/tools/pi.ts
|
|
16563
|
-
import { randomUUID as
|
|
16564
|
-
import { readFile as readFile7, readdir as readdir3, rm as
|
|
16741
|
+
import { randomUUID as randomUUID15 } from "node:crypto";
|
|
16742
|
+
import { readFile as readFile7, readdir as readdir3, rm as rm3 } from "node:fs/promises";
|
|
16565
16743
|
import { dirname as dirname5 } from "node:path";
|
|
16566
16744
|
var OPERATIONS3 = [
|
|
16567
16745
|
...DAEMON_OPERATIONS,
|
|
@@ -16655,7 +16833,7 @@ var PiAdapter = class {
|
|
|
16655
16833
|
}
|
|
16656
16834
|
onQuestion(project, sessionId, detail) {
|
|
16657
16835
|
if (!["select", "confirm", "input", "editor"].includes(String(detail.method))) return;
|
|
16658
|
-
const id =
|
|
16836
|
+
const id = randomUUID15();
|
|
16659
16837
|
void this.rpc(project, sessionId).then((rpc) => {
|
|
16660
16838
|
if (rpc.exited) return;
|
|
16661
16839
|
const question = { id, project, sessionId, detail, rpc };
|
|
@@ -16761,7 +16939,7 @@ var PiAdapter = class {
|
|
|
16761
16939
|
return sessions.sort((a, b) => b.time.updated - a.time.updated);
|
|
16762
16940
|
}
|
|
16763
16941
|
if (op === "sessions.create") {
|
|
16764
|
-
const session2 = { id:
|
|
16942
|
+
const session2 = { id: randomUUID15(), title: typeof p.title === "string" && p.title.trim() ? p.title : "\u65B0\u4EFB\u52A1", directory: await this.workspace.root(project), time: { created: Date.now(), updated: Date.now() } };
|
|
16765
16943
|
try {
|
|
16766
16944
|
const rpc2 = await this.rpc(project, session2.id);
|
|
16767
16945
|
await rpc2.request("set_session_name", { name: session2.title });
|
|
@@ -16808,8 +16986,8 @@ var PiAdapter = class {
|
|
|
16808
16986
|
this.deleting.add(this.key(project, id));
|
|
16809
16987
|
try {
|
|
16810
16988
|
await this.runtime.closeSession(project, id);
|
|
16811
|
-
await
|
|
16812
|
-
await
|
|
16989
|
+
await rm3(this.runtime.sessionFile(project, id), { force: true });
|
|
16990
|
+
await rm3(this.metadataPath(project, id));
|
|
16813
16991
|
this.clearQuestions(project, id);
|
|
16814
16992
|
} finally {
|
|
16815
16993
|
this.deleting.delete(this.key(project, id));
|
|
@@ -16859,9 +17037,9 @@ var PiAdapter = class {
|
|
|
16859
17037
|
};
|
|
16860
17038
|
|
|
16861
17039
|
// src/tools/pi-runtime.ts
|
|
16862
|
-
import { execFile as execFile5, spawn as
|
|
16863
|
-
import { mkdir as
|
|
16864
|
-
import { join as
|
|
17040
|
+
import { execFile as execFile5, spawn as spawn6 } from "node:child_process";
|
|
17041
|
+
import { mkdir as mkdir9, open as open7 } from "node:fs/promises";
|
|
17042
|
+
import { join as join12 } from "node:path";
|
|
16865
17043
|
import { promisify as promisify5 } from "node:util";
|
|
16866
17044
|
var run3 = promisify5(execFile5);
|
|
16867
17045
|
var PiRuntime = class {
|
|
@@ -16947,7 +17125,7 @@ ${bytes.toString("utf8")}`);
|
|
|
16947
17125
|
sessionFile(projectId, sessionId) {
|
|
16948
17126
|
this.project(projectId);
|
|
16949
17127
|
if (!describeTool("pi").sessionPattern.test(sessionId)) throw new Error("Invalid session id");
|
|
16950
|
-
return
|
|
17128
|
+
return join12(this.config.stateDirectory, "pi-sessions", projectId, `${sessionId}.jsonl`);
|
|
16951
17129
|
}
|
|
16952
17130
|
project(id) {
|
|
16953
17131
|
const project = this.config.projects.find((entry) => entry.id === id);
|
|
@@ -16978,15 +17156,15 @@ ${bytes.toString("utf8")}`);
|
|
|
16978
17156
|
async launch(key, projectId, sessionId, handlers, generation) {
|
|
16979
17157
|
await this.kill(key);
|
|
16980
17158
|
const directory = this.project(projectId).directory;
|
|
16981
|
-
await
|
|
16982
|
-
const logPath =
|
|
17159
|
+
await mkdir9(join12(this.config.stateDirectory, "pi-sessions", projectId), { recursive: true, mode: 448 });
|
|
17160
|
+
const logPath = join12(this.config.stateDirectory, `pi-${key}.log`);
|
|
16983
17161
|
const log = await open7(logPath, "a", 384);
|
|
16984
17162
|
if (generation !== this.generation) {
|
|
16985
17163
|
await log.close();
|
|
16986
17164
|
throw new Error("Pi runtime stopped during startup");
|
|
16987
17165
|
}
|
|
16988
17166
|
const args = [...describeTool("pi").serverArgs, ...sessionId ? ["--session", this.sessionFile(projectId, sessionId)] : ["--no-session"]];
|
|
16989
|
-
const child =
|
|
17167
|
+
const child = spawn6(this.config.tool.command, args, { cwd: directory, env: { ...process.env }, stdio: ["pipe", "pipe", log.fd] });
|
|
16990
17168
|
child.once("close", () => {
|
|
16991
17169
|
void log.close();
|
|
16992
17170
|
});
|
|
@@ -17041,7 +17219,7 @@ function createAdapter(runtime, workspace, uploads, emit) {
|
|
|
17041
17219
|
}
|
|
17042
17220
|
|
|
17043
17221
|
// src/project-registry.ts
|
|
17044
|
-
import { realpath as realpath6, readFile as readFile8, stat as
|
|
17222
|
+
import { realpath as realpath6, readFile as readFile8, stat as stat8 } from "node:fs/promises";
|
|
17045
17223
|
import { isAbsolute as isAbsolute4 } from "node:path";
|
|
17046
17224
|
var ProjectMutationSchema = ProjectSchema.extend({
|
|
17047
17225
|
name: ProjectSchema.shape.name.trim().max(120),
|
|
@@ -17101,7 +17279,7 @@ var ProjectRegistry = class {
|
|
|
17101
17279
|
const parsed = ProjectMutationSchema.parse(payload);
|
|
17102
17280
|
const directory = expandHome(parsed.directory);
|
|
17103
17281
|
if (!isAbsolute4(directory)) throw new Error("\u9879\u76EE\u76EE\u5F55\u5FC5\u987B\u662F Proxy \u4E3B\u673A\u4E0A\u7684\u7EDD\u5BF9\u8DEF\u5F84");
|
|
17104
|
-
const info = await
|
|
17282
|
+
const info = await stat8(directory).catch(() => void 0);
|
|
17105
17283
|
if (!info?.isDirectory()) throw new Error(`\u9879\u76EE\u76EE\u5F55\u4E0D\u5B58\u5728\u6216\u4E0D\u662F\u76EE\u5F55\uFF1A${directory}`);
|
|
17106
17284
|
const canonical = await realpath6(directory);
|
|
17107
17285
|
if (this.config.projects.some((project2) => project2.id === parsed.id)) throw new Error(`\u9879\u76EE ID \u5DF2\u5B58\u5728\uFF1A${parsed.id}`);
|
|
@@ -17121,7 +17299,7 @@ var ProjectRegistry = class {
|
|
|
17121
17299
|
if (index < 0) throw new Error(`\u672A\u77E5\u6388\u6743\u9879\u76EE\uFF1A${parsed.id}`);
|
|
17122
17300
|
const directory = expandHome(parsed.directory);
|
|
17123
17301
|
if (!isAbsolute4(directory)) throw new Error("\u9879\u76EE\u76EE\u5F55\u5FC5\u987B\u662F Proxy \u4E3B\u673A\u4E0A\u7684\u7EDD\u5BF9\u8DEF\u5F84");
|
|
17124
|
-
const info = await
|
|
17302
|
+
const info = await stat8(directory).catch(() => void 0);
|
|
17125
17303
|
if (!info?.isDirectory()) throw new Error(`\u9879\u76EE\u76EE\u5F55\u4E0D\u5B58\u5728\u6216\u4E0D\u662F\u76EE\u5F55\uFF1A${directory}`);
|
|
17126
17304
|
const canonical = await realpath6(directory);
|
|
17127
17305
|
for (const project of this.config.projects) {
|
|
@@ -17168,10 +17346,10 @@ var ProjectRegistry = class {
|
|
|
17168
17346
|
};
|
|
17169
17347
|
|
|
17170
17348
|
// src/terminal.ts
|
|
17171
|
-
import { randomUUID as
|
|
17349
|
+
import { randomUUID as randomUUID16 } from "node:crypto";
|
|
17172
17350
|
import { access } from "node:fs/promises";
|
|
17173
17351
|
import { constants as constants4 } from "node:fs";
|
|
17174
|
-
import { platform as platform3, userInfo as
|
|
17352
|
+
import { platform as platform3, userInfo as userInfo4 } from "node:os";
|
|
17175
17353
|
import * as pty from "node-pty";
|
|
17176
17354
|
var TerminalManager = class {
|
|
17177
17355
|
constructor(workspace, emit) {
|
|
@@ -17196,13 +17374,13 @@ var TerminalManager = class {
|
|
|
17196
17374
|
const cwd = await this.workspace.path(project, typeof p.cwd === "string" ? p.cwd : ".");
|
|
17197
17375
|
const shell = process.env.SHELL || (platform3() === "darwin" ? "/bin/zsh" : "/bin/sh");
|
|
17198
17376
|
await access(shell, constants4.X_OK);
|
|
17199
|
-
const id =
|
|
17377
|
+
const id = randomUUID16();
|
|
17200
17378
|
const terminalProcess = pty.spawn(shell, [], {
|
|
17201
17379
|
name: "xterm-256color",
|
|
17202
17380
|
cols: 80,
|
|
17203
17381
|
rows: 24,
|
|
17204
17382
|
cwd,
|
|
17205
|
-
env: { PATH: process.env.PATH || "/usr/bin:/bin", HOME:
|
|
17383
|
+
env: { PATH: process.env.PATH || "/usr/bin:/bin", HOME: userInfo4().homedir, TERM: "xterm-256color", USER: userInfo4().username }
|
|
17206
17384
|
});
|
|
17207
17385
|
const terminal2 = { id, project, owner, process: terminalProcess, history: "" };
|
|
17208
17386
|
this.terminals.set(id, terminal2);
|
|
@@ -17255,10 +17433,10 @@ var TerminalManager = class {
|
|
|
17255
17433
|
// src/desktop.ts
|
|
17256
17434
|
import { execFile as execFile6 } from "node:child_process";
|
|
17257
17435
|
import { promisify as promisify6 } from "node:util";
|
|
17258
|
-
import { createHash as createHash9, randomUUID as
|
|
17259
|
-
import { chmod as
|
|
17260
|
-
import { join as
|
|
17261
|
-
import { fileURLToPath as
|
|
17436
|
+
import { createHash as createHash9, randomUUID as randomUUID17 } from "node:crypto";
|
|
17437
|
+
import { chmod as chmod4, mkdir as mkdir10, mkdtemp, readFile as readFile9, rename as rename5, rm as rm4, stat as stat9 } from "node:fs/promises";
|
|
17438
|
+
import { join as join13 } from "node:path";
|
|
17439
|
+
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
17262
17440
|
var DESKTOP_IMAGE_LIMIT = 20 * 1024 * 1024;
|
|
17263
17441
|
var DESKTOP_CHUNK_LIMIT = 512 * 1024;
|
|
17264
17442
|
var PNG_SIGNATURE = Buffer.from("89504e470d0a1a0a", "hex");
|
|
@@ -17323,19 +17501,19 @@ var PlatformDesktop = class {
|
|
|
17323
17501
|
macHelper() {
|
|
17324
17502
|
if (this.helper) return this.helper;
|
|
17325
17503
|
this.helper = (async () => {
|
|
17326
|
-
await
|
|
17327
|
-
const target =
|
|
17328
|
-
const source =
|
|
17329
|
-
const sourceInfo = await
|
|
17504
|
+
await mkdir10(this.stateDirectory, { recursive: true, mode: 448 });
|
|
17505
|
+
const target = join13(this.stateDirectory, "octopus-desktop-capture");
|
|
17506
|
+
const source = fileURLToPath5(new URL("../native/desktop-macos.m", import.meta.url));
|
|
17507
|
+
const sourceInfo = await stat9(source), existing = await stat9(target).catch(() => null);
|
|
17330
17508
|
if (existing?.isFile() && existing.mtimeMs >= sourceInfo.mtimeMs && (existing.mode & 63) === 0) return target;
|
|
17331
|
-
const temporary = `${target}.${
|
|
17509
|
+
const temporary = `${target}.${randomUUID17()}`;
|
|
17332
17510
|
try {
|
|
17333
17511
|
await this.execute("/usr/bin/clang", ["-O2", "-fobjc-arc", "-mmacosx-version-min=12.3", "-framework", "Foundation", "-framework", "AppKit", "-framework", "ScreenCaptureKit", "-framework", "CoreMedia", "-framework", "CoreVideo", "-framework", "VideoToolbox", "-framework", "ImageIO", source, "-o", temporary]);
|
|
17334
|
-
await
|
|
17335
|
-
await
|
|
17512
|
+
await chmod4(temporary, 448);
|
|
17513
|
+
await rename5(temporary, target);
|
|
17336
17514
|
return target;
|
|
17337
17515
|
} finally {
|
|
17338
|
-
await
|
|
17516
|
+
await rm4(temporary, { force: true });
|
|
17339
17517
|
}
|
|
17340
17518
|
})();
|
|
17341
17519
|
this.helper.catch(() => {
|
|
@@ -17419,9 +17597,9 @@ var PlatformDesktop = class {
|
|
|
17419
17597
|
async capture(displayId) {
|
|
17420
17598
|
const display = (await this.list()).find((item) => item.id === displayId);
|
|
17421
17599
|
if (!display) throw new Error("\u6240\u9009\u663E\u793A\u5668\u5DF2\u65AD\u5F00\uFF0C\u8BF7\u5237\u65B0\u663E\u793A\u5668\u5217\u8868\u540E\u91CD\u8BD5\u3002");
|
|
17422
|
-
await
|
|
17423
|
-
const directory = await mkdtemp(
|
|
17424
|
-
const output =
|
|
17600
|
+
await mkdir10(this.stateDirectory, { recursive: true, mode: 448 });
|
|
17601
|
+
const directory = await mkdtemp(join13(this.stateDirectory, "desktop-capture-"));
|
|
17602
|
+
const output = join13(directory, "screen.png");
|
|
17425
17603
|
try {
|
|
17426
17604
|
if (this.system === "darwin") {
|
|
17427
17605
|
await this.execute(await this.macHelper(), ["capture", display.id, output]);
|
|
@@ -17439,24 +17617,39 @@ var PlatformDesktop = class {
|
|
|
17439
17617
|
await this.execute("import", args);
|
|
17440
17618
|
}
|
|
17441
17619
|
}
|
|
17442
|
-
const info = await
|
|
17620
|
+
const info = await stat9(output);
|
|
17443
17621
|
if (!info.isFile() || !info.size || info.size > DESKTOP_IMAGE_LIMIT) throw new Error("\u684C\u9762\u622A\u56FE\u4E3A\u7A7A\u6216\u8D85\u8FC7 20 MiB \u4E0A\u9650\u3002");
|
|
17444
17622
|
return { display, bytes: await readFile9(output) };
|
|
17445
17623
|
} finally {
|
|
17446
|
-
await
|
|
17624
|
+
await rm4(directory, { recursive: true, force: true });
|
|
17447
17625
|
}
|
|
17448
17626
|
}
|
|
17449
17627
|
};
|
|
17450
17628
|
var DesktopManager = class {
|
|
17451
|
-
constructor(backend) {
|
|
17629
|
+
constructor(backend, screenLock) {
|
|
17452
17630
|
this.backend = backend;
|
|
17631
|
+
this.screenLock = screenLock;
|
|
17453
17632
|
}
|
|
17454
17633
|
captures = /* @__PURE__ */ new Map();
|
|
17455
17634
|
pending = /* @__PURE__ */ new Map();
|
|
17635
|
+
unlocking;
|
|
17456
17636
|
closed = false;
|
|
17457
17637
|
async dispatch(command, owner) {
|
|
17458
17638
|
if (this.closed) throw new Error("\u684C\u9762\u622A\u5C4F\u670D\u52A1\u5DF2\u505C\u6B62\u3002");
|
|
17459
17639
|
const p = command.payload;
|
|
17640
|
+
if (command.operation === "desktop.lockStatus" || command.operation === "desktop.unlock") {
|
|
17641
|
+
if (!this.screenLock) throw new ScreenLockError("\u6B64 Proxy \u4E0D\u652F\u6301\u5C4F\u5E55\u89E3\u9501\uFF0C\u8BF7\u5347\u7EA7 Proxy\u3002");
|
|
17642
|
+
if (command.operation === "desktop.lockStatus") return this.screenLock.status();
|
|
17643
|
+
if (typeof p.password !== "string" || !p.password || Buffer.byteLength(p.password) > 1024 || /[\x00-\x1f\x7f]/.test(p.password)) throw new ScreenLockError("\u8BF7\u8F93\u5165\u6709\u6548\u7684\u4E3B\u673A\u767B\u5F55\u5BC6\u7801\uFF08\u6700\u591A 1024 \u5B57\u8282\uFF0C\u4E0D\u80FD\u5305\u542B\u63A7\u5236\u5B57\u7B26\uFF09\u3002");
|
|
17644
|
+
if (this.unlocking) throw new ScreenLockError("\u6B63\u5728\u89E3\u9501\u4E3B\u673A\u5C4F\u5E55\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002");
|
|
17645
|
+
const pending = { owner, abort: new AbortController() };
|
|
17646
|
+
this.unlocking = pending;
|
|
17647
|
+
try {
|
|
17648
|
+
return await this.screenLock.unlock(p.password, pending.abort.signal);
|
|
17649
|
+
} finally {
|
|
17650
|
+
if (this.unlocking === pending) this.unlocking = void 0;
|
|
17651
|
+
}
|
|
17652
|
+
}
|
|
17460
17653
|
if (command.operation === "desktop.list") return this.backend.list();
|
|
17461
17654
|
if (command.operation === "desktop.capture") {
|
|
17462
17655
|
if (typeof p.displayId !== "string" || !p.displayId) throw new Error("\u8BF7\u9009\u62E9\u663E\u793A\u5668\u3002");
|
|
@@ -17470,7 +17663,7 @@ var DesktopManager = class {
|
|
|
17470
17663
|
if (bytes.length < 24 || !bytes.subarray(0, 8).equals(PNG_SIGNATURE) || bytes.toString("ascii", 12, 16) !== "IHDR") throw new Error("\u622A\u5C4F\u672A\u8FD4\u56DE\u6709\u6548\u7684 PNG \u56FE\u7247\u3002");
|
|
17471
17664
|
const width = bytes.readUInt32BE(16), height = bytes.readUInt32BE(20);
|
|
17472
17665
|
if (!width || !height) throw new Error("\u684C\u9762\u622A\u56FE\u5C3A\u5BF8\u65E0\u6548\u3002");
|
|
17473
|
-
const info = { id:
|
|
17666
|
+
const info = { id: randomUUID17(), display: publicDisplay(display), capturedAt: (/* @__PURE__ */ new Date()).toISOString(), mime: "image/png", width, height, size: bytes.length, sha256: createHash9("sha256").update(bytes).digest("hex") };
|
|
17474
17667
|
this.clearCapture(owner);
|
|
17475
17668
|
const timer = setTimeout(() => this.clearCapture(owner), 6e4);
|
|
17476
17669
|
timer.unref();
|
|
@@ -17500,9 +17693,11 @@ var DesktopManager = class {
|
|
|
17500
17693
|
closeOwned(owner) {
|
|
17501
17694
|
this.pending.delete(owner);
|
|
17502
17695
|
this.clearCapture(owner);
|
|
17696
|
+
if (this.unlocking?.owner === owner) this.unlocking.abort.abort();
|
|
17503
17697
|
}
|
|
17504
17698
|
close() {
|
|
17505
17699
|
this.closed = true;
|
|
17700
|
+
this.unlocking?.abort.abort();
|
|
17506
17701
|
this.pending.clear();
|
|
17507
17702
|
for (const owner of this.captures.keys()) this.clearCapture(owner);
|
|
17508
17703
|
}
|
|
@@ -17511,15 +17706,15 @@ var DesktopManager = class {
|
|
|
17511
17706
|
// src/daemon.ts
|
|
17512
17707
|
async function runDaemon(configPath, authenticate = verifyHostPassword) {
|
|
17513
17708
|
const config = await loadConfig(configPath);
|
|
17514
|
-
await
|
|
17515
|
-
const workspace = new Workspace(config.projects), runtime = createRuntime(config), uploads = new Uploads(workspace,
|
|
17709
|
+
await mkdir11(config.stateDirectory, { recursive: true, mode: 448 });
|
|
17710
|
+
const workspace = new Workspace(config.projects), runtime = createRuntime(config), uploads = new Uploads(workspace, join14(config.stateDirectory, "uploads")), journal = new Journal(join14(config.stateDirectory, "journal"));
|
|
17516
17711
|
let secureChat;
|
|
17517
17712
|
function emit(projectId, event, data) {
|
|
17518
17713
|
secureChat?.publish(projectId, event, data);
|
|
17519
17714
|
}
|
|
17520
17715
|
const terminals = new TerminalManager(workspace, (owner, project, event, data) => secureChat?.sendTo(owner, project, event, data));
|
|
17521
|
-
const desktop = new DesktopManager(new PlatformDesktop(config.stateDirectory));
|
|
17522
|
-
const adapter = createAdapter(runtime, workspace, uploads, emit), artifacts = new Artifacts(workspace,
|
|
17716
|
+
const desktop = new DesktopManager(new PlatformDesktop(config.stateDirectory), new PlatformScreenLock(config.stateDirectory));
|
|
17717
|
+
const adapter = createAdapter(runtime, workspace, uploads, emit), artifacts = new Artifacts(workspace, join14(config.stateDirectory, "artifacts"), emit);
|
|
17523
17718
|
const projectRegistry = new ProjectRegistry(configPath, config, runtime, adapter);
|
|
17524
17719
|
async function dispatch(c, secureSessionId) {
|
|
17525
17720
|
if (c.operation.startsWith("desktop.")) return desktop.dispatch(c, secureSessionId);
|
package/dist/cli.js
CHANGED
package/dist/main.js
CHANGED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#import <AppKit/AppKit.h>
|
|
2
|
+
#import <ApplicationServices/ApplicationServices.h>
|
|
3
|
+
#include <unistd.h>
|
|
4
|
+
#include <string.h>
|
|
5
|
+
|
|
6
|
+
static void wipe(void *buffer, size_t length) {
|
|
7
|
+
volatile unsigned char *bytes = buffer;
|
|
8
|
+
while (length--) *bytes++ = 0;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
static int fail(const char *code) { fprintf(stderr, "%s\n", code); return 1; }
|
|
12
|
+
|
|
13
|
+
// This WindowServer property is not a documented public lock-state API. Fail
|
|
14
|
+
// closed when the session is unavailable or is not this user's console desktop.
|
|
15
|
+
static BOOL lockState(BOOL *locked) {
|
|
16
|
+
NSDictionary *session = CFBridgingRelease(CGSessionCopyCurrentDictionary());
|
|
17
|
+
if (!session || ![session[(__bridge NSString *)kCGSessionOnConsoleKey] boolValue] ||
|
|
18
|
+
![session[(__bridge NSString *)kCGSessionLoginDoneKey] boolValue] ||
|
|
19
|
+
![session[(__bridge NSString *)kCGSessionUserIDKey] isKindOfClass:NSNumber.class] ||
|
|
20
|
+
[session[(__bridge NSString *)kCGSessionUserIDKey] unsignedIntValue] != getuid()) return NO;
|
|
21
|
+
id value = session[@"CGSSessionScreenIsLocked"];
|
|
22
|
+
if (value && ![value isKindOfClass:NSNumber.class]) return NO;
|
|
23
|
+
*locked = [value boolValue];
|
|
24
|
+
return YES;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
static void printStatus(BOOL locked) {
|
|
28
|
+
printf("{\"locked\":%s,\"canUnlock\":%s}", locked ? "true" : "false",
|
|
29
|
+
AXIsProcessTrusted() && CGPreflightPostEventAccess() ? "true" : "false");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
static AXUIElementRef passwordFocus(void) {
|
|
33
|
+
AXUIElementRef system = AXUIElementCreateSystemWide();
|
|
34
|
+
AXUIElementSetMessagingTimeout(system, 0.5);
|
|
35
|
+
CFTypeRef focused = NULL;
|
|
36
|
+
AXError result = AXUIElementCopyAttributeValue(system, kAXFocusedUIElementAttribute, &focused);
|
|
37
|
+
CFRelease(system);
|
|
38
|
+
if (result != kAXErrorSuccess || !focused || CFGetTypeID(focused) != AXUIElementGetTypeID()) {
|
|
39
|
+
if (focused) CFRelease(focused);
|
|
40
|
+
return NULL;
|
|
41
|
+
}
|
|
42
|
+
pid_t pid = 0;
|
|
43
|
+
AXUIElementRef field = (AXUIElementRef)focused;
|
|
44
|
+
AXUIElementSetMessagingTimeout(field, 0.5);
|
|
45
|
+
NSRunningApplication *app = nil;
|
|
46
|
+
if (AXUIElementGetPid(field, &pid) == kAXErrorSuccess) app = [NSRunningApplication runningApplicationWithProcessIdentifier:pid];
|
|
47
|
+
CFTypeRef subrole = NULL;
|
|
48
|
+
AXUIElementCopyAttributeValue(field, kAXSubroleAttribute, &subrole);
|
|
49
|
+
BOOL secure = subrole && CFEqual(subrole, kAXSecureTextFieldSubrole);
|
|
50
|
+
if (subrole) CFRelease(subrole);
|
|
51
|
+
if (!secure || ![app.bundleIdentifier isEqualToString:@"com.apple.loginwindow"] ||
|
|
52
|
+
![app.executableURL.path isEqualToString:@"/System/Library/CoreServices/loginwindow.app/Contents/MacOS/loginwindow"]) {
|
|
53
|
+
CFRelease(field); return NULL;
|
|
54
|
+
}
|
|
55
|
+
return field;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
static BOOL stillFocused(AXUIElementRef field) {
|
|
59
|
+
BOOL locked = NO;
|
|
60
|
+
if (!lockState(&locked) || !locked) return NO;
|
|
61
|
+
AXUIElementRef current = passwordFocus();
|
|
62
|
+
BOOL same = current && CFEqual(current, field);
|
|
63
|
+
if (current) CFRelease(current);
|
|
64
|
+
return same;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
static BOOL key(CGKeyCode code, CGEventFlags flags, const UniChar *text, size_t length) {
|
|
68
|
+
CGEventRef down = CGEventCreateKeyboardEvent(NULL, code, true);
|
|
69
|
+
CGEventRef up = CGEventCreateKeyboardEvent(NULL, code, false);
|
|
70
|
+
if (!down || !up) { if (down) CFRelease(down); if (up) CFRelease(up); return NO; }
|
|
71
|
+
CGEventSetFlags(down, flags); CGEventSetFlags(up, flags);
|
|
72
|
+
if (text) { CGEventKeyboardSetUnicodeString(down, length, text); CGEventKeyboardSetUnicodeString(up, length, text); }
|
|
73
|
+
CGEventPost(kCGHIDEventTap, down); CGEventPost(kCGHIDEventTap, up);
|
|
74
|
+
CFRelease(down); CFRelease(up);
|
|
75
|
+
usleep(20000);
|
|
76
|
+
return YES;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
int main(int argc, const char *argv[]) {
|
|
80
|
+
@autoreleasepool {
|
|
81
|
+
if (getuid() == 0 || argc != 2) return fail("session");
|
|
82
|
+
BOOL locked = NO;
|
|
83
|
+
if (!lockState(&locked)) return fail("session");
|
|
84
|
+
if (strcmp(argv[1], "status") == 0) { printStatus(locked); return 0; }
|
|
85
|
+
if (strcmp(argv[1], "unlock") != 0) return fail("session");
|
|
86
|
+
if (!locked) { printStatus(NO); return 0; }
|
|
87
|
+
if (!AXIsProcessTrusted() || !CGPreflightPostEventAccess()) return fail("permission");
|
|
88
|
+
unsigned char header[2], bytes[1025] = {0};
|
|
89
|
+
if (fread(header, 1, 2, stdin) != 2) return fail("password");
|
|
90
|
+
size_t count = ((size_t)header[0] << 8) | header[1];
|
|
91
|
+
if (!count || count > 1024 || fread(bytes, 1, count, stdin) != count) { wipe(bytes, sizeof(bytes)); return fail("password"); }
|
|
92
|
+
NSString *password = [[NSString alloc] initWithBytes:bytes length:count encoding:NSUTF8StringEncoding];
|
|
93
|
+
wipe(bytes, sizeof(bytes));
|
|
94
|
+
if (!password || [password rangeOfCharacterFromSet:NSCharacterSet.controlCharacterSet].location != NSNotFound) return fail("password");
|
|
95
|
+
// A non-text key wakes the password prompt. Never type a secret into an
|
|
96
|
+
// arbitrary app, username field, clipboard, or a blind screen coordinate.
|
|
97
|
+
if (!lockState(&locked) || !locked) return fail("changed");
|
|
98
|
+
if (!key(56, 0, NULL, 0)) return fail("focus");
|
|
99
|
+
AXUIElementRef field = NULL;
|
|
100
|
+
for (int i = 0; i < 20 && !field; i++) { usleep(100000); field = passwordFocus(); }
|
|
101
|
+
if (!field) return fail("focus");
|
|
102
|
+
if (!stillFocused(field) || !key(0, kCGEventFlagMaskCommand, NULL, 0) ||
|
|
103
|
+
!stillFocused(field) || !key(51, 0, NULL, 0)) { CFRelease(field); return fail("changed"); }
|
|
104
|
+
UniChar characters[1024] = {0};
|
|
105
|
+
NSUInteger length = password.length;
|
|
106
|
+
if (length > 1024) { CFRelease(field); return fail("password"); }
|
|
107
|
+
[password getCharacters:characters range:NSMakeRange(0, length)];
|
|
108
|
+
password = nil;
|
|
109
|
+
BOOL sent = YES;
|
|
110
|
+
// One Unicode scalar per event avoids event-string truncation. Check the
|
|
111
|
+
// secure loginwindow focus and console lock immediately before each event.
|
|
112
|
+
for (NSUInteger i = 0; i < length;) {
|
|
113
|
+
NSUInteger units = CFStringIsSurrogateHighCharacter(characters[i]) && i + 1 < length && CFStringIsSurrogateLowCharacter(characters[i + 1]) ? 2 : 1;
|
|
114
|
+
if (!stillFocused(field) || !key(0, 0, &characters[i], units)) { sent = NO; break; }
|
|
115
|
+
i += units;
|
|
116
|
+
}
|
|
117
|
+
wipe(characters, sizeof(characters));
|
|
118
|
+
if (!sent || !stillFocused(field) || !key(36, 0, NULL, 0)) { CFRelease(field); return fail("changed"); }
|
|
119
|
+
CFRelease(field);
|
|
120
|
+
for (int i = 0; i < 50; i++) {
|
|
121
|
+
usleep(100000);
|
|
122
|
+
if (!lockState(&locked)) return fail("session");
|
|
123
|
+
if (!locked) { printStatus(NO); return 0; }
|
|
124
|
+
}
|
|
125
|
+
return fail("timeout");
|
|
126
|
+
}
|
|
127
|
+
}
|