@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.
@@ -11714,8 +11714,8 @@ async function ensureHostIdentity(stateDirectory) {
11714
11714
  }
11715
11715
 
11716
11716
  // src/daemon.ts
11717
- import { mkdir as mkdir10 } from "node:fs/promises";
11718
- import { join as join13 } from "node:path";
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 join4 } from "node:path";
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 = join4(this.directory, `${command.id}.json`);
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 join5 } from "node:path";
12410
- import { readFile as readFile4, writeFile as writeFile2, mkdir as mkdir4, stat as stat5 } from "node:fs/promises";
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 join5(this.directory, project, "index.json");
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 = join5(this.directory, project, "blobs");
12465
- await mkdir4(dir, { recursive: true, mode: 448 });
12466
- await writeFile2(join5(dir, hash), data, { mode: 384 });
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 stat5(await this.workspace.path(project, item.path));
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(join5(this.directory, project, "blobs", hash), "utf8");
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 randomUUID7, verify } from "node:crypto";
12520
- import { userInfo as userInfo2 } from "node:os";
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 = randomUUID7();
12828
+ epoch = randomUUID8();
12653
12829
  sequence = 0;
12654
12830
  events = [];
12655
12831
  eventBytes = 0;
12656
12832
  stopping = false;
12657
12833
  context;
12658
- serviceAccount = userInfo2().username;
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 randomUUID8 } from "node:crypto";
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: randomUUID8(), botId: config.botId, operation: "configuration.models", projectId, payload: {} };
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 randomUUID9 } from "node:crypto";
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 fileURLToPath3 } from "node:url";
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 ![step and result](relative/project/path.png) 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 = fileURLToPath3(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-" + randomUUID9(), true);
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 randomUUID10 } from "node:crypto";
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-${randomUUID10()}.tmp`, true);
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 spawn2 } from "node:child_process";
15265
+ import { spawn as spawn3 } from "node:child_process";
15090
15266
  import { createHash as createHash8 } from "node:crypto";
15091
- import { mkdir as mkdir6, open as open5, readFile as readFile6, rm, writeFile as writeFile4 } from "node:fs/promises";
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 join7 } from "node:path";
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 join7(this.config.stateDirectory, "opencode-endpoint.json");
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 mkdir6(this.config.stateDirectory, { recursive: true, mode: 448 });
15172
- const logPath = join7(this.config.stateDirectory, "opencode-serve.log");
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 = spawn2(this.config.tool.command, ["serve", "--hostname", url.hostname === "[::1]" ? "::1" : url.hostname, "--port", url.port || "80"], {
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 rm(this.endpointStatePath, { force: true }).catch(() => {
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 join8 } from "node:path";
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 = join8(dirname4(command), "..", "config", "provider", "zcode-builtin.json");
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 spawn3, execFile as execFile4 } from "node:child_process";
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 mkdir7, open as open6 } from "node:fs/promises";
15278
- import { join as join9 } from "node:path";
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 randomUUID11 } from "node:crypto";
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: randomUUID11(), logicalFrameOrdinal: 1, topic: "outbound", subscriptionId: "outbound", frame };
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 = join9(this.config.stateDirectory, `${this.kind}-${projectId}.log`);
15643
- await mkdir7(this.config.stateDirectory, { recursive: true, mode: 448 });
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 = spawn3(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] });
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 delay = Math.min(3e4, 1e3 * 2 ** attempts);
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
- }, delay));
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 join10 } from "node:path";
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 randomUUID12 } from "node:crypto";
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 = randomUUID12();
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(join10(this.runtime.config.stateDirectory, `codex-${project}.json`));
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(join10(this.runtime.config.stateDirectory, `codex-${project}.json`), index);
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(join10(this.runtime.config.stateDirectory, `codex-${project}.json`), index);
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 spawn4 } from "node:child_process";
16233
- import { randomUUID as randomUUID13 } from "node:crypto";
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 = randomUUID13();
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 = spawn4(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"] });
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 randomUUID14 } from "node:crypto";
16564
- import { readFile as readFile7, readdir as readdir3, rm as rm2 } from "node:fs/promises";
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 = randomUUID14();
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: randomUUID14(), 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() } };
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 rm2(this.runtime.sessionFile(project, id), { force: true });
16812
- await rm2(this.metadataPath(project, id));
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 spawn5 } from "node:child_process";
16863
- import { mkdir as mkdir8, open as open7 } from "node:fs/promises";
16864
- import { join as join11 } from "node:path";
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 join11(this.config.stateDirectory, "pi-sessions", projectId, `${sessionId}.jsonl`);
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 mkdir8(join11(this.config.stateDirectory, "pi-sessions", projectId), { recursive: true, mode: 448 });
16982
- const logPath = join11(this.config.stateDirectory, `pi-${key}.log`);
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 = spawn5(this.config.tool.command, args, { cwd: directory, env: { ...process.env }, stdio: ["pipe", "pipe", log.fd] });
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 stat7 } from "node:fs/promises";
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 stat7(directory).catch(() => void 0);
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 stat7(directory).catch(() => void 0);
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 randomUUID15 } from "node:crypto";
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 userInfo3 } from "node:os";
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 = randomUUID15();
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: userInfo3().homedir, TERM: "xterm-256color", USER: userInfo3().username }
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 randomUUID16 } from "node:crypto";
17259
- import { chmod as chmod3, mkdir as mkdir9, mkdtemp, readFile as readFile9, rename as rename4, rm as rm3, stat as stat8 } from "node:fs/promises";
17260
- import { join as join12 } from "node:path";
17261
- import { fileURLToPath as fileURLToPath4 } from "node:url";
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 mkdir9(this.stateDirectory, { recursive: true, mode: 448 });
17327
- const target = join12(this.stateDirectory, "octopus-desktop-capture");
17328
- const source = fileURLToPath4(new URL("../native/desktop-macos.m", import.meta.url));
17329
- const sourceInfo = await stat8(source), existing = await stat8(target).catch(() => null);
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}.${randomUUID16()}`;
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 chmod3(temporary, 448);
17335
- await rename4(temporary, target);
17512
+ await chmod4(temporary, 448);
17513
+ await rename5(temporary, target);
17336
17514
  return target;
17337
17515
  } finally {
17338
- await rm3(temporary, { force: true });
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 mkdir9(this.stateDirectory, { recursive: true, mode: 448 });
17423
- const directory = await mkdtemp(join12(this.stateDirectory, "desktop-capture-"));
17424
- const output = join12(directory, "screen.png");
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 stat8(output);
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 rm3(directory, { recursive: true, force: true });
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: randomUUID16(), display: publicDisplay(display), capturedAt: (/* @__PURE__ */ new Date()).toISOString(), mime: "image/png", width, height, size: bytes.length, sha256: createHash9("sha256").update(bytes).digest("hex") };
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 mkdir10(config.stateDirectory, { recursive: true, mode: 448 });
17515
- const workspace = new Workspace(config.projects), runtime = createRuntime(config), uploads = new Uploads(workspace, join13(config.stateDirectory, "uploads")), journal = new Journal(join13(config.stateDirectory, "journal"));
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, join13(config.stateDirectory, "artifacts"), emit);
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
@@ -27,7 +27,7 @@ import {
27
27
  runDaemon,
28
28
  supportedPlatform,
29
29
  systemdUnitPath
30
- } from "./chunk-5OJQ46AF.js";
30
+ } from "./chunk-6DTK4JVV.js";
31
31
 
32
32
  // src/cli.ts
33
33
  import { spawn } from "node:child_process";
package/dist/main.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  ConfigError,
4
4
  runDaemon
5
- } from "./chunk-5OJQ46AF.js";
5
+ } from "./chunk-6DTK4JVV.js";
6
6
 
7
7
  // src/main.ts
8
8
  runDaemon(process.env.OCTOPUS_PROXY_CONFIG ?? "proxy.json").catch((error) => {
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skylandnpm/octopus-proxy",
3
- "version": "0.1.35",
3
+ "version": "0.1.38",
4
4
  "type": "module",
5
5
  "description": "Octopus host proxy: OpenCode, Codex, ZCode and Pi adapters with service installer CLI",
6
6
  "bin": {