local-commander-agent 0.3.48 → 0.3.50

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.
@@ -1,38 +1,38 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
3
- import { createHash } from "node:crypto";
4
-
5
- const HASH=/^[a-f0-9]{64}$/;
6
- /** Signed bundles may be set up during install; an unsigned pilot only by an explicit owner command. */
7
- export function implicitSetupAllowed(manifest){return manifest?.signed===true;}
8
- const FILES=["JobHost.exe","libcronet.dll","sing-box.exe","wintun.dll","sing-box.LICENSE","wintun.LICENSE"];
9
- async function plainDirectory(directory) {
10
- const stat=await fs.lstat(directory);
11
- if(!stat.isDirectory()||stat.isSymbolicLink()||(await fs.realpath(directory)).toLowerCase()!==path.resolve(directory).toLowerCase())throw Error("NETWORK_BUNDLE_DIRECTORY_INVALID");
12
- }
13
- async function bytes(file,max) {
14
- const stat=await fs.lstat(file);
15
- if(!stat.isFile()||stat.isSymbolicLink()||stat.size<1||stat.size>max)throw Error("NETWORK_BUNDLE_FILE_INVALID");
16
- return fs.readFile(file);
17
- }
18
- export async function inspectNetworkBundle(directory) {
19
- try {await plainDirectory(directory);}catch(error){if(error.code==="ENOENT")return null;throw error;}
20
- const manifest=JSON.parse((await bytes(path.join(directory,"manifest.json"),8192)).toString("utf8"));
21
- if(manifest.schemaVersion!==1||manifest.configured!==true||!(manifest.signed===true||manifest.signing==="unsigned-pilot")||!HASH.test(manifest.bundleId)||!HASH.test(manifest.helperSha256)||
22
- !manifest.files||typeof manifest.files!=="object"||Array.isArray(manifest.files)||Object.keys(manifest.files).sort().join()!==[...FILES].sort().join())throw Error("NETWORK_BUNDLE_NOT_DEPLOYABLE");
23
- const names=[...FILES,"NetworkService.exe","manifest.json"];
24
- if((await fs.readdir(directory)).sort().join()!==names.sort().join())throw Error("NETWORK_BUNDLE_ALLOWLIST_FAILED");
25
- for(const name of [...FILES,"NetworkService.exe"]){const expected=name==="NetworkService.exe"?manifest.helperSha256:manifest.files[name];
26
- if(!HASH.test(expected)||createHash("sha256").update(await bytes(path.join(directory,name),128*1024*1024)).digest("hex")!==expected)throw Error("NETWORK_BUNDLE_DIGEST_MISMATCH");}
27
- return {bundleId:manifest.bundleId,files:names,manifest};
28
- }
29
- export async function copyNetworkBundle(source,destination) {
30
- const bundle=await inspectNetworkBundle(source);if(!bundle)return false;
31
- if(path.resolve(source).toLowerCase()===path.resolve(destination).toLowerCase())return true;
32
- const exists=await fs.lstat(destination).catch(e=>{if(e.code!=="ENOENT")throw e;return null;});
33
- if(exists){const previous=await inspectNetworkBundle(destination);if(previous?.bundleId===bundle.bundleId&&previous.manifest.helperSha256===bundle.manifest.helperSha256)return true;throw Error("NETWORK_BUNDLE_DESTINATION_CONFLICT");}
34
- await fs.mkdir(destination,{recursive:false,mode:0o700});
35
- try{for(const name of bundle.files)await fs.copyFile(path.join(source,name),path.join(destination,name),fs.constants.COPYFILE_EXCL);await inspectNetworkBundle(destination);}
36
- catch(error){for(const name of bundle.files)await fs.rm(path.join(destination,name),{force:true}).catch(()=>{});await fs.rmdir(destination).catch(()=>{});throw error;}
37
- return true;
38
- }
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { createHash } from "node:crypto";
4
+
5
+ const HASH=/^[a-f0-9]{64}$/;
6
+ /** Signed bundles may be set up during install; an unsigned pilot only by an explicit owner command. */
7
+ export function implicitSetupAllowed(manifest){return manifest?.signed===true;}
8
+ const FILES=["JobHost.exe","libcronet.dll","sing-box.exe","wintun.dll","sing-box.LICENSE","wintun.LICENSE"];
9
+ async function plainDirectory(directory) {
10
+ const stat=await fs.lstat(directory);
11
+ if(!stat.isDirectory()||stat.isSymbolicLink()||(await fs.realpath(directory)).toLowerCase()!==path.resolve(directory).toLowerCase())throw Error("NETWORK_BUNDLE_DIRECTORY_INVALID");
12
+ }
13
+ async function bytes(file,max) {
14
+ const stat=await fs.lstat(file);
15
+ if(!stat.isFile()||stat.isSymbolicLink()||stat.size<1||stat.size>max)throw Error("NETWORK_BUNDLE_FILE_INVALID");
16
+ return fs.readFile(file);
17
+ }
18
+ export async function inspectNetworkBundle(directory) {
19
+ try {await plainDirectory(directory);}catch(error){if(error.code==="ENOENT")return null;throw error;}
20
+ const manifest=JSON.parse((await bytes(path.join(directory,"manifest.json"),8192)).toString("utf8"));
21
+ if(manifest.schemaVersion!==1||manifest.configured!==true||!(manifest.signed===true||manifest.signing==="unsigned-pilot")||!HASH.test(manifest.bundleId)||!HASH.test(manifest.helperSha256)||
22
+ !manifest.files||typeof manifest.files!=="object"||Array.isArray(manifest.files)||Object.keys(manifest.files).sort().join()!==[...FILES].sort().join())throw Error("NETWORK_BUNDLE_NOT_DEPLOYABLE");
23
+ const names=[...FILES,"NetworkService.exe","manifest.json"];
24
+ if((await fs.readdir(directory)).sort().join()!==names.sort().join())throw Error("NETWORK_BUNDLE_ALLOWLIST_FAILED");
25
+ for(const name of [...FILES,"NetworkService.exe"]){const expected=name==="NetworkService.exe"?manifest.helperSha256:manifest.files[name];
26
+ if(!HASH.test(expected)||createHash("sha256").update(await bytes(path.join(directory,name),128*1024*1024)).digest("hex")!==expected)throw Error("NETWORK_BUNDLE_DIGEST_MISMATCH");}
27
+ return {bundleId:manifest.bundleId,files:names,manifest};
28
+ }
29
+ export async function copyNetworkBundle(source,destination) {
30
+ const bundle=await inspectNetworkBundle(source);if(!bundle)return false;
31
+ if(path.resolve(source).toLowerCase()===path.resolve(destination).toLowerCase())return true;
32
+ const exists=await fs.lstat(destination).catch(e=>{if(e.code!=="ENOENT")throw e;return null;});
33
+ if(exists){const previous=await inspectNetworkBundle(destination);if(previous?.bundleId===bundle.bundleId&&previous.manifest.helperSha256===bundle.manifest.helperSha256)return true;throw Error("NETWORK_BUNDLE_DESTINATION_CONFLICT");}
34
+ await fs.mkdir(destination,{recursive:false,mode:0o700});
35
+ try{for(const name of bundle.files)await fs.copyFile(path.join(source,name),path.join(destination,name),fs.constants.COPYFILE_EXCL);await inspectNetworkBundle(destination);}
36
+ catch(error){for(const name of bundle.files)await fs.rm(path.join(destination,name),{force:true}).catch(()=>{});await fs.rmdir(destination).catch(()=>{});throw error;}
37
+ return true;
38
+ }
package/bin/posix.mjs CHANGED
@@ -1,84 +1,84 @@
1
- // macOS and Linux: the runtime ships in a per-platform package (an optional dependency, so npm installs only the
2
- // matching one). Its lifecycle accepts only a private, owner-only artifact directory, so the files are copied from the
3
- // npx cache into a fresh private directory and the lifecycle installs from there into its own versions directory.
4
- import { constants } from "node:fs";
5
- import fs from "node:fs/promises";
6
- import os from "node:os";
7
- import path from "node:path";
8
- import { spawnSync } from "node:child_process";
9
- import { createRequire } from "node:module";
10
-
11
- const PACKAGES = { "darwin-arm64": "local-commander-agent-darwin-arm64", "linux-x64": "local-commander-agent-linux-x64", "linux-arm64": "local-commander-agent-linux-arm64" };
12
- // npm command -> lifecycle command. The default (`npx local-commander-agent@latest`) is setup, as on Windows.
13
- const COMMANDS = { bootstrap: "setup", install: "setup", setup: "setup", update: "update", pair: "pair", access: "access", "configure-access": "access",
14
- start: "start", stop: "stop", status: "status", rollback: "rollback", uninstall: "uninstall" };
15
- const EXECUTABLES = new Set(["node", "posix-host", "owner-store"]);
16
-
17
- function fail(code) { const error = new Error(code); error.code = code; throw error; }
18
-
19
- export function posixHelp() {
20
- return [
21
- "Local Commander",
22
- "",
23
- " npx local-commander-agent@latest set up this computer, pair it and choose access",
24
- " npx local-commander-agent@latest update install this version, keeping pairing and access",
25
- " npx local-commander-agent@latest status|start|stop|pair|access|rollback",
26
- " npx local-commander-agent@latest uninstall [--purge-state]",
27
- ""
28
- ].join("\n");
29
- }
30
-
31
- // Owner opt-out for take_screenshot under Full Access (#486): a marker in the installed service agent's state folder
32
- // (the service runs without XDG_DATA_HOME). No runtime staging is needed; the agent checks it on each capture.
33
- async function screenshotCommand(args) {
34
- const action = args[0] || "status";
35
- if (args.length > 1 || !["status", "on", "off"].includes(action)) fail("ARGUMENT_INVALID");
36
- if (typeof process.getuid === "function" && process.getuid() === 0) fail("POSIX_OWNER_REQUIRED");
37
- const state = path.join(os.homedir(), process.platform === "darwin" ? "Library/Application Support" : ".local/share", "LocalCommander", "Agent");
38
- const folder = await fs.lstat(state).catch(() => null);
39
- if (!folder?.isDirectory() || folder.uid !== process.getuid()) fail("AGENT_NOT_INSTALLED");
40
- const file = path.join(state, "screenshot-disabled-by-owner");
41
- if (action === "off") await fs.open(file, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600)
42
- .then(handle => handle.close(), error => { if (error?.code !== "EEXIST") throw error; });
43
- if (action === "on") await fs.rm(file, { force: true });
44
- const off = await fs.lstat(file).then(() => true, error => error?.code !== "ENOENT");
45
- process.stdout.write(JSON.stringify({ screenshot: off ? "off" : "on" }) + "\n");
46
- }
47
-
48
- export async function runPosix(argv, version) {
49
- const name = PACKAGES[process.platform + "-" + process.arch];
50
- if (!name) fail(process.platform === "darwin" ? "MACOS_APPLE_SILICON_REQUIRED" : process.platform === "linux" ? "LINUX_X64_OR_ARM64_REQUIRED" : "PLATFORM_UNSUPPORTED");
51
- const command = argv[0] || "bootstrap", args = argv.slice(1);
52
- if (command === "-h" || command === "--help" || command === "help") { process.stdout.write(posixHelp()); return; }
53
- if (command === "screenshot") { await screenshotCommand(args); return; }
54
- const lifecycle = COMMANDS[command];
55
- if (!lifecycle) fail("ARGUMENT_INVALID");
56
- if (lifecycle === "uninstall" ? args.length > 1 || (args.length === 1 && args[0] !== "--purge-state") : args.length) fail("ARGUMENT_INVALID");
57
- if (typeof process.getuid === "function" && process.getuid() === 0) fail("POSIX_OWNER_REQUIRED");
58
-
59
- let packageJson;
60
- try { packageJson = createRequire(import.meta.url).resolve(name + "/package.json"); } catch { fail("PLATFORM_PACKAGE_MISSING"); }
61
- const meta = JSON.parse(await fs.readFile(packageJson, "utf8"));
62
- if (meta.name !== name || meta.version !== version) fail("PLATFORM_PACKAGE_MISMATCH");
63
- const source = path.join(path.dirname(packageJson), "runtime");
64
- const manifest = JSON.parse(await fs.readFile(path.join(source, "manifest.json"), "utf8"));
65
- const files = Object.keys(manifest?.files ?? {});
66
- if (manifest?.platform !== process.platform || manifest?.arch !== process.arch || !files.length || files.some(file => !/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u.test(file))) fail("PLATFORM_PACKAGE_INVALID");
67
-
68
- // Canonical parent: on macOS os.tmpdir() is under /var, a symlink to /private/var, and the lifecycle rejects aliases.
69
- const stage = await fs.mkdtemp(path.join(await fs.realpath(os.tmpdir()), "local-commander-"));
70
- try {
71
- await fs.chmod(stage, 0o700);
72
- for (const file of [...files, "manifest.json"]) {
73
- const target = path.join(stage, file);
74
- await fs.copyFile(path.join(source, file), target, constants.COPYFILE_EXCL);
75
- await fs.chmod(target, EXECUTABLES.has(file) ? 0o700 : 0o600);
76
- }
77
- // The lifecycle verifies every file against the manifest, then copies them into its own versions directory.
78
- const result = spawnSync(path.join(stage, "node"), [path.join(stage, "lifecycle.mjs"), lifecycle, ...args], { stdio: "inherit" });
79
- if (result.error) throw result.error;
80
- process.exitCode = result.status ?? 1;
81
- } finally {
82
- await fs.rm(stage, { recursive: true, force: true });
83
- }
84
- }
1
+ // macOS and Linux: the runtime ships in a per-platform package (an optional dependency, so npm installs only the
2
+ // matching one). Its lifecycle accepts only a private, owner-only artifact directory, so the files are copied from the
3
+ // npx cache into a fresh private directory and the lifecycle installs from there into its own versions directory.
4
+ import { constants } from "node:fs";
5
+ import fs from "node:fs/promises";
6
+ import os from "node:os";
7
+ import path from "node:path";
8
+ import { spawnSync } from "node:child_process";
9
+ import { createRequire } from "node:module";
10
+
11
+ const PACKAGES = { "darwin-arm64": "local-commander-agent-darwin-arm64", "linux-x64": "local-commander-agent-linux-x64", "linux-arm64": "local-commander-agent-linux-arm64" };
12
+ // npm command -> lifecycle command. The default (`npx local-commander-agent@latest`) is setup, as on Windows.
13
+ const COMMANDS = { bootstrap: "setup", install: "setup", setup: "setup", update: "update", pair: "pair", access: "access", "configure-access": "access",
14
+ start: "start", stop: "stop", status: "status", rollback: "rollback", uninstall: "uninstall" };
15
+ const EXECUTABLES = new Set(["node", "posix-host", "owner-store"]);
16
+
17
+ function fail(code) { const error = new Error(code); error.code = code; throw error; }
18
+
19
+ export function posixHelp() {
20
+ return [
21
+ "Local Commander",
22
+ "",
23
+ " npx local-commander-agent@latest set up this computer, pair it and choose access",
24
+ " npx local-commander-agent@latest update install this version, keeping pairing and access",
25
+ " npx local-commander-agent@latest status|start|stop|pair|access|rollback",
26
+ " npx local-commander-agent@latest uninstall [--purge-state]",
27
+ ""
28
+ ].join("\n");
29
+ }
30
+
31
+ // Owner opt-out for take_screenshot under Full Access (#486): a marker in the installed service agent's state folder
32
+ // (the service runs without XDG_DATA_HOME). No runtime staging is needed; the agent checks it on each capture.
33
+ async function screenshotCommand(args) {
34
+ const action = args[0] || "status";
35
+ if (args.length > 1 || !["status", "on", "off"].includes(action)) fail("ARGUMENT_INVALID");
36
+ if (typeof process.getuid === "function" && process.getuid() === 0) fail("POSIX_OWNER_REQUIRED");
37
+ const state = path.join(os.homedir(), process.platform === "darwin" ? "Library/Application Support" : ".local/share", "LocalCommander", "Agent");
38
+ const folder = await fs.lstat(state).catch(() => null);
39
+ if (!folder?.isDirectory() || folder.uid !== process.getuid()) fail("AGENT_NOT_INSTALLED");
40
+ const file = path.join(state, "screenshot-disabled-by-owner");
41
+ if (action === "off") await fs.open(file, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600)
42
+ .then(handle => handle.close(), error => { if (error?.code !== "EEXIST") throw error; });
43
+ if (action === "on") await fs.rm(file, { force: true });
44
+ const off = await fs.lstat(file).then(() => true, error => error?.code !== "ENOENT");
45
+ process.stdout.write(JSON.stringify({ screenshot: off ? "off" : "on" }) + "\n");
46
+ }
47
+
48
+ export async function runPosix(argv, version) {
49
+ const name = PACKAGES[process.platform + "-" + process.arch];
50
+ if (!name) fail(process.platform === "darwin" ? "MACOS_APPLE_SILICON_REQUIRED" : process.platform === "linux" ? "LINUX_X64_OR_ARM64_REQUIRED" : "PLATFORM_UNSUPPORTED");
51
+ const command = argv[0] || "bootstrap", args = argv.slice(1);
52
+ if (command === "-h" || command === "--help" || command === "help") { process.stdout.write(posixHelp()); return; }
53
+ if (command === "screenshot") { await screenshotCommand(args); return; }
54
+ const lifecycle = COMMANDS[command];
55
+ if (!lifecycle) fail("ARGUMENT_INVALID");
56
+ if (lifecycle === "uninstall" ? args.length > 1 || (args.length === 1 && args[0] !== "--purge-state") : args.length) fail("ARGUMENT_INVALID");
57
+ if (typeof process.getuid === "function" && process.getuid() === 0) fail("POSIX_OWNER_REQUIRED");
58
+
59
+ let packageJson;
60
+ try { packageJson = createRequire(import.meta.url).resolve(name + "/package.json"); } catch { fail("PLATFORM_PACKAGE_MISSING"); }
61
+ const meta = JSON.parse(await fs.readFile(packageJson, "utf8"));
62
+ if (meta.name !== name || meta.version !== version) fail("PLATFORM_PACKAGE_MISMATCH");
63
+ const source = path.join(path.dirname(packageJson), "runtime");
64
+ const manifest = JSON.parse(await fs.readFile(path.join(source, "manifest.json"), "utf8"));
65
+ const files = Object.keys(manifest?.files ?? {});
66
+ if (manifest?.platform !== process.platform || manifest?.arch !== process.arch || !files.length || files.some(file => !/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u.test(file))) fail("PLATFORM_PACKAGE_INVALID");
67
+
68
+ // Canonical parent: on macOS os.tmpdir() is under /var, a symlink to /private/var, and the lifecycle rejects aliases.
69
+ const stage = await fs.mkdtemp(path.join(await fs.realpath(os.tmpdir()), "local-commander-"));
70
+ try {
71
+ await fs.chmod(stage, 0o700);
72
+ for (const file of [...files, "manifest.json"]) {
73
+ const target = path.join(stage, file);
74
+ await fs.copyFile(path.join(source, file), target, constants.COPYFILE_EXCL);
75
+ await fs.chmod(target, EXECUTABLES.has(file) ? 0o700 : 0o600);
76
+ }
77
+ // The lifecycle verifies every file against the manifest, then copies them into its own versions directory.
78
+ const result = spawnSync(path.join(stage, "node"), [path.join(stage, "lifecycle.mjs"), lifecycle, ...args], { stdio: "inherit" });
79
+ if (result.error) throw result.error;
80
+ process.exitCode = result.status ?? 1;
81
+ } finally {
82
+ await fs.rm(stage, { recursive: true, force: true });
83
+ }
84
+ }
package/package.json CHANGED
@@ -1,57 +1,57 @@
1
- {
2
- "name": "local-commander-agent",
3
- "version": "0.3.48",
4
- "description": "Computer companion for Local Commander: connect AI clients to local files, approved commands and localhost. Requires https://localcmd.app.",
5
- "type": "module",
6
- "bin": {
7
- "localcommander": "bin/localcommander.mjs"
8
- },
9
- "os": [
10
- "win32",
11
- "darwin",
12
- "linux"
13
- ],
14
- "cpu": [
15
- "x64",
16
- "arm64"
17
- ],
18
- "optionalDependencies": {
19
- "local-commander-agent-darwin-arm64": "0.3.48",
20
- "local-commander-agent-linux-arm64": "0.3.48",
21
- "local-commander-agent-linux-x64": "0.3.48"
22
- },
23
- "engines": {
24
- "node": ">=20.0.0"
25
- },
26
- "files": [
27
- "bin/",
28
- "runtime/",
29
- "README.md",
30
- "CHANGELOG.md"
31
- ],
32
- "scripts": {
33
- "prepack": "node ../../scripts/build-npm-agent.mjs",
34
- "prepublishOnly": "node ../../scripts/check-npm-publish-auth.mjs && node ../../scripts/verify-npm-agent.mjs"
35
- },
36
- "publishConfig": {
37
- "access": "public"
38
- },
39
- "keywords": [
40
- "mcp",
41
- "local",
42
- "agent",
43
- "chatgpt",
44
- "claude",
45
- "windows",
46
- "macos",
47
- "linux"
48
- ],
49
- "localCommander": {
50
- "controlPlaneOrigin": "https://localcmd.app"
51
- },
52
- "homepage": "https://localcmd.app",
53
- "bugs": {
54
- "url": "https://localcmd.app/support",
55
- "email": "peter.mamrukov@localcmd.app"
56
- }
57
- }
1
+ {
2
+ "name": "local-commander-agent",
3
+ "version": "0.3.50",
4
+ "description": "Computer companion for Local Commander: connect AI clients to local files, approved commands and localhost. Requires https://localcmd.app.",
5
+ "type": "module",
6
+ "bin": {
7
+ "localcommander": "bin/localcommander.mjs"
8
+ },
9
+ "os": [
10
+ "win32",
11
+ "darwin",
12
+ "linux"
13
+ ],
14
+ "cpu": [
15
+ "x64",
16
+ "arm64"
17
+ ],
18
+ "optionalDependencies": {
19
+ "local-commander-agent-darwin-arm64": "0.3.50",
20
+ "local-commander-agent-linux-arm64": "0.3.50",
21
+ "local-commander-agent-linux-x64": "0.3.50"
22
+ },
23
+ "engines": {
24
+ "node": ">=20.0.0"
25
+ },
26
+ "files": [
27
+ "bin/",
28
+ "runtime/",
29
+ "README.md",
30
+ "CHANGELOG.md"
31
+ ],
32
+ "scripts": {
33
+ "prepack": "node ../../scripts/build-npm-agent.mjs",
34
+ "prepublishOnly": "node ../../scripts/check-npm-publish-auth.mjs && node ../../scripts/verify-npm-agent.mjs"
35
+ },
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
39
+ "keywords": [
40
+ "mcp",
41
+ "local",
42
+ "agent",
43
+ "chatgpt",
44
+ "claude",
45
+ "windows",
46
+ "macos",
47
+ "linux"
48
+ ],
49
+ "localCommander": {
50
+ "controlPlaneOrigin": "https://localcmd.app"
51
+ },
52
+ "homepage": "https://localcmd.app",
53
+ "bugs": {
54
+ "url": "https://localcmd.app/support",
55
+ "email": "peter.mamrukov@localcmd.app"
56
+ }
57
+ }
Binary file
Binary file
Binary file
package/runtime/agent.mjs CHANGED
@@ -31105,7 +31105,7 @@ var NativeTools = class {
31105
31105
  if (call.name === "command_run") {
31106
31106
  if (!["win32", "linux", "darwin"].includes(process.platform)) fail3("COMMAND_NOT_ALLOWED");
31107
31107
  const cwd2 = await resolve(call.args.cwd, true), program = await this.resolveProgram(call.args.command, workspace2, call.args.args);
31108
- const claims2 = writeClaim(cwd2);
31108
+ const claims2 = call.args.cwd === void 0 ? [{ path: cwd2, namespace: "process", mode: "read" }] : writeClaim(cwd2);
31109
31109
  return { call, claims: claims2, execution: { ...program, cwd: cwd2 }, run: async (signal, onStarted, onOutput, onSupervisor) => {
31110
31110
  let r;
31111
31111
  try {
@@ -31373,7 +31373,10 @@ var NativeScheduler = class {
31373
31373
  } catch {
31374
31374
  }
31375
31375
  }
31376
- schedule(id2, claims, deadlineAt, run2, signal) {
31376
+ /** lockWaitMs: how long a job may wait behind a conflicting claim before it is refused with
31377
+ * RESOURCE_LOCKED (proved not started), so the caller hears back inside its relay window
31378
+ * instead of timing out (#504). Waiting only for a free slot still lasts until the deadline. */
31379
+ schedule(id2, claims, deadlineAt, run2, signal, lockWaitMs) {
31377
31380
  if (!this.accepting) return Promise.reject(Error("AGENT_PAUSED"));
31378
31381
  if (!id2 || id2.length > 100 || this.active.has(id2) || this.waiting.some((j) => j.id === id2)) return Promise.reject(Error("DUPLICATE_SCHEDULED_EXECUTION"));
31379
31382
  if (!Array.isArray(claims) || !claims.length || claims.length > 32 || claims.some((c) => !c || !path19.isAbsolute(c.path) || !["read", "write"].includes(c.mode))) return Promise.reject(Error("INVALID_RESOURCE_CLAIM"));
@@ -31394,6 +31397,12 @@ var NativeScheduler = class {
31394
31397
  };
31395
31398
  job.aborted = () => remove("CANCELLED_BEFORE_ACCEPTANCE");
31396
31399
  job.timer = setTimeout(() => remove("ADMISSION_DEADLINE_EXPIRED"), Math.min(2147483647, Math.max(1, deadline - Date.now())));
31400
+ if (lockWaitMs !== void 0 && Number.isFinite(lockWaitMs) && lockWaitMs >= 0) {
31401
+ const lockIn = Math.min(lockWaitMs, deadline - Date.now() - 1e3);
31402
+ if (lockIn > 0) job.lockTimer = setTimeout(() => {
31403
+ if (this.lockBlocked(job)) remove("RESOURCE_LOCKED");
31404
+ }, lockIn);
31405
+ }
31397
31406
  signal?.addEventListener("abort", job.aborted, { once: true });
31398
31407
  this.waiting.push(job);
31399
31408
  this.observe({ kind: "queued", executionId: id2, deadlineAt: deadline });
@@ -31402,8 +31411,21 @@ var NativeScheduler = class {
31402
31411
  }
31403
31412
  cleanup(job) {
31404
31413
  clearTimeout(job.timer);
31414
+ clearTimeout(job.lockTimer);
31405
31415
  if (job.aborted) job.signal?.removeEventListener("abort", job.aborted);
31406
31416
  }
31417
+ /** True when a waiting job is held by a conflicting claim: a running job, or an earlier conflicting
31418
+ * waiter that is itself lock-blocked. A wait that is only for a free slot is never a lock. */
31419
+ lockBlocked(job) {
31420
+ const index = this.waiting.indexOf(job);
31421
+ if (index < 0) return false;
31422
+ const blocked2 = [];
31423
+ for (let i = 0; i <= index; i++) {
31424
+ const current2 = this.waiting[i];
31425
+ blocked2[i] = [...this.active.values()].some((claims) => claimsConflict(claims, current2.claims)) || this.waiting.slice(0, i).some((older, j) => blocked2[j] === true && claimsConflict(older.claims, current2.claims));
31426
+ }
31427
+ return blocked2[index] === true;
31428
+ }
31407
31429
  pump() {
31408
31430
  for (let i = 0; i < this.waiting.length && this.active.size < this.safeSlots; ) {
31409
31431
  const job = this.waiting[i];
@@ -32106,6 +32128,7 @@ var knownCodes = /* @__PURE__ */ new Set([
32106
32128
  "WORKSPACE_CHANGED",
32107
32129
  "WORKSPACE_UNAVAILABLE",
32108
32130
  "RESOURCE_BUSY",
32131
+ "RESOURCE_LOCKED",
32109
32132
  "AGENT_PAUSED",
32110
32133
  "CANCELLED_BEFORE_ACCEPTANCE",
32111
32134
  "ADMISSION_DEADLINE_EXPIRED",
@@ -32120,6 +32143,7 @@ var knownCodes = /* @__PURE__ */ new Set([
32120
32143
  "SCREEN_ENCODER_UNAVAILABLE",
32121
32144
  "SCREEN_RECORDING_PERMISSION_REQUIRED"
32122
32145
  ]);
32146
+ var LOCK_WAIT_MS = 2e4;
32123
32147
  function safeCode(error62) {
32124
32148
  const code = error62 instanceof NativeToolError ? error62.code : error62 instanceof Error ? error62.message : "";
32125
32149
  return knownCodes.has(code) || /^GUARD_[A-Z_]{1,30}$/.test(code) ? code : "TOOL_FAILED";
@@ -32331,7 +32355,7 @@ var NativeRuntime = class {
32331
32355
  const final = await this.options.journal.finish(binding2.executionId, image === void 0 ? journalOutput : { ...journalOutput, stdout: screenshotMetadata(image) }, resultCode);
32332
32356
  const outbound = await outboundResult(runnable.call, journalResult(final.result, resultCode), binding2, this.optionalTelemetry, this.options.efficiencyLedger);
32333
32357
  this.send({ type: "result", version: 1, binding: binding2, result: image === void 0 ? outbound : { ...outbound, stdout: image } });
32334
- }, abort.signal);
32358
+ }, abort.signal, LOCK_WAIT_MS);
32335
32359
  context.task = work.catch(async (error62) => {
32336
32360
  if (!context.accepted) {
32337
32361
  if (context.guardPermit) await this.options.guard.rejected(context.guardPermit, safeCode(error62)).catch(() => {
@@ -47386,9 +47410,9 @@ function waitForMessage(socket, maxBytes, timeoutMs2, timeoutCode) {
47386
47410
  cleanup();
47387
47411
  reject(new RelayClientError(error62.code === "WS_ERR_UNSUPPORTED_MESSAGE_LENGTH" ? "RELAY_FRAME_TOO_LARGE" : "RELAY_SOCKET_FAILED"));
47388
47412
  };
47389
- const onClose = () => {
47413
+ const onClose = (code, reason) => {
47390
47414
  cleanup();
47391
- reject(new RelayClientError("RELAY_SOCKET_CLOSED"));
47415
+ reject(new RelayClientError(code === 1008 && reason.toString("utf8") === "device_quarantined" ? "RELAY_DEVICE_QUARANTINED" : "RELAY_SOCKET_CLOSED"));
47392
47416
  };
47393
47417
  socket.once("message", onMessage);
47394
47418
  socket.once("error", onError);
@@ -48984,7 +49008,7 @@ var NativeEventBuffer = class {
48984
49008
 
48985
49009
  // agent/src/native-event-supervisor.ts
48986
49010
  var eventKinds = /* @__PURE__ */ new Set(["queued", "admitted", "released", "queue_rejected", "prepared", "accepted", "running", "result", "not_accepted", "connected", "disconnected", "mailbox_expired"]);
48987
- var codes = /* @__PURE__ */ new Set(["SUCCESS", "EXECUTION_UNKNOWN", "AGENT_RESTARTED", "ADMISSION_DEADLINE_EXPIRED", "CANCELLED_BEFORE_ACCEPTANCE", "RESOURCE_BUSY", "AGENT_PAUSED", "PREPARATION_EXPIRED", "CANCELLED", "TIMEOUT", "USER_PROGRAM_FAILED", "TOOL_FAILED", "MAILBOX_EXPIRED"]);
49011
+ var codes = /* @__PURE__ */ new Set(["SUCCESS", "EXECUTION_UNKNOWN", "AGENT_RESTARTED", "ADMISSION_DEADLINE_EXPIRED", "CANCELLED_BEFORE_ACCEPTANCE", "RESOURCE_BUSY", "RESOURCE_LOCKED", "AGENT_PAUSED", "PREPARATION_EXPIRED", "CANCELLED", "TIMEOUT", "USER_PROGRAM_FAILED", "TOOL_FAILED", "MAILBOX_EXPIRED"]);
48988
49012
  var NativeEventSupervisor = class {
48989
49013
  constructor(onChange) {
48990
49014
  this.onChange = onChange;
@@ -50816,9 +50840,10 @@ async function restoreContentAudit(input2, output2, key) {
50816
50840
  }
50817
50841
 
50818
50842
  // agent/src/main.ts
50819
- var AGENT_VERSION2 = true ? "0.3.48" : "0.3.2";
50843
+ var AGENT_VERSION2 = true ? "0.3.50" : "0.3.2";
50820
50844
  var NETWORK_PUBLIC_KEY = true ? "-----BEGIN PUBLIC KEY-----\nMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAvmu+81vWtJBQDZNHcAIW\nzCSxpG+R9RBsum2df5mP2cOW+wyUDTpnYquFge1qgI5YYTF96j0xCyXL8B9FeNWs\n939opWNmv53j+CQlS4cEIQhBbZSCu0eC4icsVEzBhvf68MB+E668SOtZBcVTEnyh\n96PTcupxS10rQJBWDinbKBXRC2gvGYeu01rWa/nsrndy4vAYP5t3yQtdOC8e8mr0\n+2Mg2Qhiq+KiTCtDd+L9oXvoLqqJYbzUIveNGha25AWxF1Weblp0gOM87hell5gV\nK7q4ZLUq/wBRGYNmQRdLyV7/WRE1FFPSW21/SBZQ/5aU4UqJU/MjMcrJUrnRKngz\nYKmo0czkxS3N7Q2KVxomgR1AQSuQFKMrboZYKjShADp9RIYUxzs7cYg51Of5PnHh\n6Ax+VevDGfzjMD+yHuyxYZqCtX4OwgolJxz63UQbzKlris+8xb5lLYTbIuz90Pss\nn6HK/F5May25c6oMmS3nh/NsiZopL1OvdtWKTt1L4WlRAgMBAAE=\n-----END PUBLIC KEY-----\n" : "";
50821
50845
  var AI_CONNECTION_SHIPPED = false;
50846
+ var QUARANTINE_RETRY_MS = 6e4;
50822
50847
  function processAlive(pid) {
50823
50848
  if (!Number.isInteger(pid) || pid <= 0) return false;
50824
50849
  try {
@@ -50964,7 +50989,7 @@ async function runAgent(options) {
50964
50989
  try {
50965
50990
  while (!options.signal.aborted) {
50966
50991
  computerName = await readComputerName();
50967
- let ready;
50992
+ let ready, wait = delay4;
50968
50993
  const readyPromise = new Promise((resolve) => {
50969
50994
  ready = resolve;
50970
50995
  });
@@ -51053,6 +51078,8 @@ async function runAgent(options) {
51053
51078
  ready();
51054
51079
  monitor(() => options.monitor?.connectionFailed(error62));
51055
51080
  if (error62 instanceof RelayClientError && ["RELAY_CONNECT_FAILED", "RELAY_CONNECT_CLOSED", "RELAY_CONNECT_TIMEOUT", "RELAY_HANDSHAKE_TIMEOUT"].includes(error62.code)) {
51081
+ } else if (error62 instanceof RelayClientError && error62.code === "RELAY_DEVICE_QUARANTINED") {
51082
+ wait = Math.max(delay4, QUARANTINE_RETRY_MS);
51056
51083
  } else throw error62;
51057
51084
  } finally {
51058
51085
  connectivity?.close();
@@ -51070,7 +51097,7 @@ async function runAgent(options) {
51070
51097
  await bridge.disconnected();
51071
51098
  connection = null;
51072
51099
  }
51073
- if (!options.signal.aborted) await aborted2(options.signal, delay4);
51100
+ if (!options.signal.aborted) await aborted2(options.signal, wait);
51074
51101
  }
51075
51102
  } finally {
51076
51103
  try {