local-commander-agent 0.3.44 → 0.3.45
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/README.md +14 -14
- package/bin/localcommander.mjs +3 -1
- package/bin/posix.mjs +66 -0
- package/package.json +57 -47
- package/runtime/JobHost.exe +0 -0
- package/runtime/LocalCommanderAgent.exe +0 -0
- package/runtime/OwnerBridge.exe +0 -0
- package/runtime/agent.mjs +168 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Change history
|
|
2
2
|
|
|
3
|
+
## 0.3.45 - 2026-10-22
|
|
4
|
+
|
|
5
|
+
- New: macOS 14 or later on Apple Silicon, and Linux x64 or arm64 with a systemd user session. Run the same `npx local-commander-agent@latest` in Terminal; npm downloads only the runtime for your computer. Setup pairs the computer, asks what may be used in the terminal, and runs the agent as a background service that starts when you sign in.
|
|
6
|
+
- Fixed: a computer that lost its connection without noticing (after sleep, a network change or a server restart) now reconnects within about three minutes instead of staying offline.
|
|
7
|
+
- Fixed: a request that never reached a sleeping computer no longer keeps one of its execution slots busy; the computer frees it when it reconnects.
|
|
8
|
+
- Includes everything in 0.3.44.
|
|
9
|
+
|
|
3
10
|
## 0.3.44 - 2026-10-22
|
|
4
11
|
|
|
5
12
|
- New: when you set up a computer, the window that asks what may be used now shows the Local Commander account the computer joined. Someone helping you can approve your computer from their own phone with the code you read to them. Continue only if you know that account; Cancel shares nothing.
|
package/README.md
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
# Local Commander Agent
|
|
2
2
|
|
|
3
|
-
**Connect your AI assistant to your
|
|
3
|
+
**Connect your AI assistant to your computer through [Local Commander](https://localcmd.app).** The agent lets an authorized MCP client work with local files, run explicitly approved commands, and access approved localhost services on your paired computer.
|
|
4
4
|
|
|
5
|
-
> **Requires the Local Commander service at https://localcmd.app.** This npm package is its
|
|
5
|
+
> **Requires the Local Commander service at https://localcmd.app.** This npm package is its computer companion, not a standalone MCP server, an offline AI assistant, or a JavaScript SDK. Installing the package alone is not enough: you need a Local Commander account, an approved device pairing, a running agent, an internet connection, and an authorized MCP connection from your AI client. Local management commands such as status and stop do not require the service to be online.
|
|
6
6
|
|
|
7
7
|
[Website and sign-in](https://localcmd.app) ┬╖ [Support](https://localcmd.app/support) ┬╖ [Privacy](https://localcmd.app/privacy)
|
|
8
8
|
|
|
9
9
|
## Before you start
|
|
10
10
|
|
|
11
|
-
Use **Windows 11 x64** and a currently supported Node.js LTS version meeting the package's Node.js 20+ minimum. Node.js/npm are needed to launch `npx`; the installed background agent includes its own Node runtime. You do not need Docker, WSL, a repository checkout, a database, or administrator rights for normal setup. Your AI client must support authenticated remote MCP connections; availability depends on the client and workspace policy.
|
|
11
|
+
Use **Windows 11 x64**, **macOS 14 or later on Apple Silicon**, or **Linux with a systemd user session** (Ubuntu 24.04 x64 or arm64), and a currently supported Node.js LTS version meeting the package's Node.js 20+ minimum. Node.js/npm are needed to launch `npx`; the installed background agent includes its own Node runtime. You do not need Docker, WSL, a repository checkout, a database, or administrator rights for normal setup. Your AI client must support authenticated remote MCP connections; availability depends on the client and workspace policy.
|
|
12
12
|
|
|
13
13
|
## Install and connect
|
|
14
14
|
|
|
15
|
-
1. Open PowerShell or Windows Terminal
|
|
15
|
+
1. Open a terminal as your normal user (PowerShell or Windows Terminal on Windows, Terminal on macOS or Linux) and run:
|
|
16
16
|
|
|
17
17
|
```powershell
|
|
18
18
|
npx local-commander-agent@latest
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
2. The installer opens a browser. Sign in to [Local Commander](https://localcmd.app), check the displayed pairing code, and approve this computer. Approve only a pairing you just started yourself.
|
|
22
|
-
3. Choose access in the local Windows
|
|
22
|
+
3. Choose access in the local dialog (a window on Windows, a terminal prompt on macOS and Linux):
|
|
23
23
|
- **Full access - all connected drives (default):** approve the available drive letters, file reads/writes, installed commands and localhost services at once. There is no folder picker or separate approval for Node.
|
|
24
24
|
- **Selected folders:** choose one folder in the folder picker. It starts read/write; more folders and write permission can be added later.
|
|
25
25
|
|
|
@@ -32,15 +32,15 @@ Use **Windows 11 x64** and a currently supported Node.js LTS version meeting the
|
|
|
32
32
|
|
|
33
33
|
Complete the browser authorization with the same Local Commander account. Device pairing and AI-client authorization are separate steps. For an AI client that runs on this same computer, you can instead use direct local MCP (see "Direct local MCP" below). Do not configure `npx local-commander-agent` itself as a stdio server.
|
|
34
34
|
|
|
35
|
-
You can close the installation terminal. The agent runs in the background and starts when
|
|
35
|
+
You can close the installation terminal. The agent runs in the background and starts when you sign in to this computer. The computer must be awake and online for remote AI tasks. As a first test, ask the connected assistant to list the files in a folder you approved.
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
On Windows, installation and update also add `localcommander` to your **user PATH**. Open a new CMD/terminal window, then run `localcommander status`, `localcommander monitor --watch`, or `localcommander logs --follow` from any folder. No global npm installation is needed. The launcher uses your required Node.js installation and an installed copy of the CLI, so clearing npm's cache does not remove the command. Fetch newer releases with `npx local-commander-agent@latest update`. Uninstall removes Local Commander's PATH entry and launcher while preserving your other entries.
|
|
38
38
|
|
|
39
39
|
## Direct local MCP (AI clients on this computer)
|
|
40
40
|
|
|
41
|
-
An AI client that runs on the same
|
|
41
|
+
An AI client that runs on the same computer, such as Claude Code, Codex or Cursor, can use your approved workspaces directly through the running agent. No cloud round trip happens per call. It keeps working while the internet or localcmd.app is unavailable, and calls take milliseconds instead of a cloud admission. Hosted assistants such as ChatGPT or claude.ai on the web still use `https://localcmd.app/mcp`.
|
|
42
42
|
|
|
43
|
-
1. Turn it on once. This is an owner choice for this
|
|
43
|
+
1. Turn it on once. This is an owner choice for this user account, and `off` revokes it immediately:
|
|
44
44
|
|
|
45
45
|
```powershell
|
|
46
46
|
localcommander mcp on
|
|
@@ -57,15 +57,15 @@ An AI client that runs on the same Windows computer, such as Claude Code, Codex
|
|
|
57
57
|
Other clients: { "command": "cmd", "args": ["/c", "localcommander", "mcp"] }
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
-
The tools are the same as the remote ones: `workspace_list`, file reads and writes, search, Git, `project_snapshot`, `command_run` and `localhost_request`. Each takes an optional `workspace` (a grant id, a root such as `D:\`, or a display name). The same workspace grants, Full Access or scoped mode and owner denials apply, and commands run hidden with your
|
|
60
|
+
The tools are the same as the remote ones: `workspace_list`, file reads and writes, search, Git, `project_snapshot`, `command_run` and `localhost_request`. Each takes an optional `workspace` (a grant id, a root such as `D:\`, or a display name). The same workspace grants, Full Access or scoped mode and owner denials apply, and commands run hidden with your user account's rights.
|
|
61
61
|
|
|
62
|
-
The agent listens only on `127.0.0.1`, and only while direct local MCP is on. Every connection must first prove a key that only this
|
|
62
|
+
The agent listens only on `127.0.0.1`, and only while direct local MCP is on. Every connection must first prove a key that only this user's protected Local Commander state can derive, so other users and other computers cannot connect. Check the state with `localcommander mcp status`; turn it off with `localcommander mcp off`. While an enterprise Guard policy is configured, direct local calls are refused so that Guard evidence cannot be bypassed.
|
|
63
63
|
|
|
64
64
|
## What тАЬall connected drivesтАЭ means
|
|
65
65
|
|
|
66
66
|
It expands the **filesystem scope** to the drive letters shown at the moment you consent. It includes available local fixed and removable drives. Mapped network drives and `subst` letters are not included. New drive letters, unavailable drives and unmounted volumes are not automatically added; run `configure-access` again after making the desired drive available. Grants are path/drive-letter based, not tied to a particular removable disk's hardware identity: reusing an approved letter gives access under that letter.
|
|
67
67
|
|
|
68
|
-
Full access allows installed commands resolved from absolute owner PATH entries or an absolute executable path, plus localhost services. Explicit command/port denials and write restrictions still apply. It does not elevate
|
|
68
|
+
Full access allows installed commands resolved from absolute owner PATH entries or an absolute executable path, plus localhost services. Explicit command/port denials and write restrictions still apply. It does not elevate privileges. Protected Local Commander state/runtime and secret-file filters apply to file tools; programs run with your user account's rights, so these controls are not an OS sandbox. ChatGPT/Claude or your workspace may still ask for their own action confirmations. Existing installations keep their current permissions until the owner runs `configure-access` and selects full access using a release that supports it.
|
|
69
69
|
|
|
70
70
|
## Manage the installation
|
|
71
71
|
|
|
@@ -143,7 +143,7 @@ Do not paste `(Get-Command git).Source` into CMD: CMD passes it literally instea
|
|
|
143
143
|
|
|
144
144
|
Under full access, `deny-command` and `deny-localhost` exclude the named command or port while other commands/ports remain available. Native `.exe`/`.com` commands receive argument arrays directly. `.cmd`/`.bat` launchers use a hidden PowerShell wrapper and reject cmd metacharacters in arguments; use an explicit interpreter for more complex scripts. Allowing a general interpreter is not containment against it launching a denied program.
|
|
145
145
|
|
|
146
|
-
Under **owner-approved Full Access**, installed Windows shells, script hosts and administration executables (including PowerShell and CMD) work without per-command registration. They execute under the
|
|
146
|
+
Under **owner-approved Full Access**, installed Windows shells, script hosts and administration executables (including PowerShell and CMD) work without per-command registration. They execute under the user account's own rights, not elevated privileges. Owner-defined `deny-command` exclusions and explicit write/localhost restrictions still apply. A general interpreter can access resources beyond file-tool path checks; Full Access is not an OS sandbox. Existing scoped grants stay scoped until the owner deliberately changes access with `configure-access`.
|
|
147
147
|
|
|
148
148
|
|
|
149
149
|
For a remote MCP client with an unknown device/workspace, use `workspace_list`, then `execution_session_open`, perform the requested work and read results. Reuse valid, already authorized session and target context where supported. `diagnostics` is for faults, not a mandatory startup roundtrip. Diagnostics reports quota separately from the device's execution slot limit. Its optional `repairSafe` mode releases only expired reservations from this OAuth connection that never acquired a dispatch lease. It never retries work, changes permissions or clears unknown executions.
|
|
@@ -153,7 +153,7 @@ For a remote MCP client with an unknown device/workspace, use `workspace_list`,
|
|
|
153
153
|
| Symptom | What to check |
|
|
154
154
|
| --- | --- |
|
|
155
155
|
| `npx` is not recognized | Install supported Node.js LTS, then open a new terminal. |
|
|
156
|
-
| Unsupported OS or architecture |
|
|
156
|
+
| Unsupported OS or architecture | Supported: Windows 11 x64, macOS 14 or later on Apple Silicon, Linux x64 or arm64 with systemd. Windows ARM64 and Intel Macs are not supported. |
|
|
157
157
|
| Pairing does not complete | Open https://localcmd.app, verify your account and internet access, then rerun the install command to resume. Do not share pairing codes. |
|
|
158
158
|
| `ACCESS_SETUP_CANCELLED` | No filesystem grant was saved by that dialog. Rerun setup and choose the intended scope. |
|
|
159
159
|
| `STATE_CHANGED` | Local account/policy/permissions changed while the dialog was open. Review current state and reopen the dialog. |
|
package/bin/localcommander.mjs
CHANGED
|
@@ -723,8 +723,10 @@ function help() {
|
|
|
723
723
|
].join("\n");
|
|
724
724
|
}
|
|
725
725
|
async function main() {
|
|
726
|
-
requireWindows();
|
|
727
726
|
if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/u.test(VERSION)) fail("PACKAGE_VERSION_INVALID");
|
|
727
|
+
// macOS and Linux run the per-platform runtime package through its own lifecycle (#253).
|
|
728
|
+
if (process.platform !== "win32") return (await import("./posix.mjs")).runPosix(process.argv.slice(2), VERSION);
|
|
729
|
+
requireWindows();
|
|
728
730
|
const command = process.argv[2] || "bootstrap";
|
|
729
731
|
const args = process.argv.slice(3);
|
|
730
732
|
if (command === "-h" || command === "--help" || command === "help") {
|
package/bin/posix.mjs
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
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
|
+
export async function runPosix(argv, version) {
|
|
32
|
+
const name = PACKAGES[process.platform + "-" + process.arch];
|
|
33
|
+
if (!name) fail(process.platform === "darwin" ? "MACOS_APPLE_SILICON_REQUIRED" : process.platform === "linux" ? "LINUX_X64_OR_ARM64_REQUIRED" : "PLATFORM_UNSUPPORTED");
|
|
34
|
+
const command = argv[0] || "bootstrap", args = argv.slice(1);
|
|
35
|
+
if (command === "-h" || command === "--help" || command === "help") { process.stdout.write(posixHelp()); return; }
|
|
36
|
+
const lifecycle = COMMANDS[command];
|
|
37
|
+
if (!lifecycle) fail("ARGUMENT_INVALID");
|
|
38
|
+
if (lifecycle === "uninstall" ? args.length > 1 || (args.length === 1 && args[0] !== "--purge-state") : args.length) fail("ARGUMENT_INVALID");
|
|
39
|
+
if (typeof process.getuid === "function" && process.getuid() === 0) fail("POSIX_OWNER_REQUIRED");
|
|
40
|
+
|
|
41
|
+
let packageJson;
|
|
42
|
+
try { packageJson = createRequire(import.meta.url).resolve(name + "/package.json"); } catch { fail("PLATFORM_PACKAGE_MISSING"); }
|
|
43
|
+
const meta = JSON.parse(await fs.readFile(packageJson, "utf8"));
|
|
44
|
+
if (meta.name !== name || meta.version !== version) fail("PLATFORM_PACKAGE_MISMATCH");
|
|
45
|
+
const source = path.join(path.dirname(packageJson), "runtime");
|
|
46
|
+
const manifest = JSON.parse(await fs.readFile(path.join(source, "manifest.json"), "utf8"));
|
|
47
|
+
const files = Object.keys(manifest?.files ?? {});
|
|
48
|
+
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");
|
|
49
|
+
|
|
50
|
+
// Canonical parent: on macOS os.tmpdir() is under /var, a symlink to /private/var, and the lifecycle rejects aliases.
|
|
51
|
+
const stage = await fs.mkdtemp(path.join(await fs.realpath(os.tmpdir()), "local-commander-"));
|
|
52
|
+
try {
|
|
53
|
+
await fs.chmod(stage, 0o700);
|
|
54
|
+
for (const file of [...files, "manifest.json"]) {
|
|
55
|
+
const target = path.join(stage, file);
|
|
56
|
+
await fs.copyFile(path.join(source, file), target, constants.COPYFILE_EXCL);
|
|
57
|
+
await fs.chmod(target, EXECUTABLES.has(file) ? 0o700 : 0o600);
|
|
58
|
+
}
|
|
59
|
+
// The lifecycle verifies every file against the manifest, then copies them into its own versions directory.
|
|
60
|
+
const result = spawnSync(path.join(stage, "node"), [path.join(stage, "lifecycle.mjs"), lifecycle, ...args], { stdio: "inherit" });
|
|
61
|
+
if (result.error) throw result.error;
|
|
62
|
+
process.exitCode = result.status ?? 1;
|
|
63
|
+
} finally {
|
|
64
|
+
await fs.rm(stage, { recursive: true, force: true });
|
|
65
|
+
}
|
|
66
|
+
}
|
package/package.json
CHANGED
|
@@ -1,47 +1,57 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "local-commander-agent",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"description": "
|
|
5
|
-
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
"localcommander": "bin/localcommander.mjs"
|
|
8
|
-
},
|
|
9
|
-
"os": [
|
|
10
|
-
"win32"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
"
|
|
17
|
-
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
"
|
|
34
|
-
"agent"
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
"
|
|
38
|
-
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
|
|
47
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "local-commander-agent",
|
|
3
|
+
"version": "0.3.45",
|
|
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.45",
|
|
20
|
+
"local-commander-agent-linux-arm64": "0.3.45",
|
|
21
|
+
"local-commander-agent-linux-x64": "0.3.45"
|
|
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
|
+
}
|
package/runtime/JobHost.exe
CHANGED
|
Binary file
|
|
Binary file
|
package/runtime/OwnerBridge.exe
CHANGED
|
Binary file
|
package/runtime/agent.mjs
CHANGED
|
@@ -25388,6 +25388,7 @@ async function secretCommand(executable, args, input2) {
|
|
|
25388
25388
|
async function secretServicePresent() {
|
|
25389
25389
|
const bus = path3.join("/run/user", String(process.getuid()), "bus");
|
|
25390
25390
|
if (!process.env.DBUS_SESSION_BUS_ADDRESS && await absent(bus)) return false;
|
|
25391
|
+
if (await absent("/usr/bin/gdbus")) return false;
|
|
25391
25392
|
const args = ["call", "--session", "--dest", "org.freedesktop.DBus", "--object-path", "/org/freedesktop/DBus", "--method"];
|
|
25392
25393
|
for (const method of ["org.freedesktop.DBus.ListNames", "org.freedesktop.DBus.ListActivatableNames"]) {
|
|
25393
25394
|
const output2 = (await secretCommand("/usr/bin/gdbus", [...args, method])).toString("utf8");
|
|
@@ -26484,6 +26485,7 @@ var NativeCapacitySchema = external_exports.object({ type: external_exports.lite
|
|
|
26484
26485
|
var NATIVE_STATUS_CAPABILITY = "nativeStatusV1";
|
|
26485
26486
|
var NativeStatusSchema = NativeCapacitySchema.extend({ type: external_exports.literal("nativeStatus"), active: external_exports.number().int().min(0).max(32), queued: external_exports.number().int().min(0).max(256), paused: external_exports.boolean() }).strict().refine((v) => v.active <= v.safeSlots);
|
|
26486
26487
|
var TerminalEventSchema = external_exports.object({ type: external_exports.literal("executionResult"), protocolVersion: external_exports.literal(1), requestId: uuid11, tenantId: uuid11, deviceId: uuid11, connectionEpoch: epoch2, binding: ExecutionBindingSchema, state: external_exports.enum(["finished", "failed", "cancelled", "unknown", "rejected"]), resultCode: external_exports.string().regex(/^[A-Z0-9_]{1,40}$/), metrics: TerminalMetricsSchema.optional() }).strict();
|
|
26488
|
+
var HeldLeasesFrameSchema = external_exports.object({ type: external_exports.literal("heldLeases"), protocolVersion: external_exports.literal(1), connectionEpoch: epoch2, bindings: external_exports.array(ExecutionBindingSchema).min(1).max(64) }).strict();
|
|
26487
26489
|
var TerminalEventAckSchema = external_exports.object({ type: external_exports.literal("executionResultAck"), protocolVersion: external_exports.literal(1), requestId: uuid11, ok: external_exports.boolean() }).strict();
|
|
26488
26490
|
function canonicalJson(value) {
|
|
26489
26491
|
function canonical2(v) {
|
|
@@ -26763,9 +26765,19 @@ var NativeJournal = class _NativeJournal {
|
|
|
26763
26765
|
sizes = /* @__PURE__ */ new Map();
|
|
26764
26766
|
digests = /* @__PURE__ */ new Map();
|
|
26765
26767
|
totalBytes = 0;
|
|
26768
|
+
/** First connection epoch this journal saw (#448): from then on every received request has an entry. "0" for a journal
|
|
26769
|
+
* an older agent created (continuous since pairing); null until a new journal's first connection. */
|
|
26770
|
+
firstEpoch = null;
|
|
26766
26771
|
static async open(directory, box, options = {}) {
|
|
26767
26772
|
const maxEntries = options.maxEntries ?? 2e3, maxBytes = options.maxBytes ?? 64 * 1048576, retentionMs = options.retentionMs ?? 864e5;
|
|
26768
26773
|
if (!Number.isInteger(maxEntries) || maxEntries < 1 || maxEntries > 1e4 || !Number.isInteger(maxBytes) || maxBytes < 1024 || maxBytes > 64 * 1048576 || !Number.isInteger(retentionMs) || retentionMs < 0 || retentionMs > 30 * 864e5) throw failure2("JOURNAL_LIMIT_INVALID");
|
|
26774
|
+
let existed = true;
|
|
26775
|
+
try {
|
|
26776
|
+
await lstat2(path6.resolve(directory));
|
|
26777
|
+
} catch (e) {
|
|
26778
|
+
if (e.code !== "ENOENT") throw e;
|
|
26779
|
+
existed = false;
|
|
26780
|
+
}
|
|
26769
26781
|
const dir = await safeDirectory(directory), lockPath = path6.join(dir, "journal.lock"), nonce2 = randomBytes4(16).toString("hex");
|
|
26770
26782
|
const guard = options.lockHelper ? await acquirePosixJournalGuard(dir, options.lockHelper) : void 0;
|
|
26771
26783
|
let file2;
|
|
@@ -26781,6 +26793,7 @@ var NativeJournal = class _NativeJournal {
|
|
|
26781
26793
|
await file2.writeFile(JSON.stringify({ pid: process.pid, nonce: nonce2, createdAt: journal.now().toISOString() }));
|
|
26782
26794
|
await file2.sync();
|
|
26783
26795
|
await journal.load();
|
|
26796
|
+
await journal.loadStart(existed);
|
|
26784
26797
|
return journal;
|
|
26785
26798
|
} catch (e) {
|
|
26786
26799
|
await file2.close().catch(() => {
|
|
@@ -26862,6 +26875,65 @@ var NativeJournal = class _NativeJournal {
|
|
|
26862
26875
|
this.digests.set(id2, digest2(envelope));
|
|
26863
26876
|
}
|
|
26864
26877
|
}
|
|
26878
|
+
async loadStart(existed) {
|
|
26879
|
+
const filename = path6.join(this.dir, "journal.start");
|
|
26880
|
+
let envelope;
|
|
26881
|
+
try {
|
|
26882
|
+
const stat2 = await lstat2(filename);
|
|
26883
|
+
if (!stat2.isFile() || stat2.isSymbolicLink() || stat2.size > 4096) throw failure2("JOURNAL_CORRUPT");
|
|
26884
|
+
envelope = await readFile3(filename);
|
|
26885
|
+
} catch (e) {
|
|
26886
|
+
if (e.code !== "ENOENT") throw e;
|
|
26887
|
+
return this.writeStart(existed ? "0" : null);
|
|
26888
|
+
}
|
|
26889
|
+
try {
|
|
26890
|
+
const value = JSON.parse(this.box.open(envelope, "journal-start").toString("utf8"));
|
|
26891
|
+
if (value?.version !== 1 || Object.keys(value).length !== 2 || value.firstEpoch !== null && (typeof value.firstEpoch !== "string" || !/^(0|[1-9]\d{0,18})$/.test(value.firstEpoch))) throw Error();
|
|
26892
|
+
this.firstEpoch = value.firstEpoch;
|
|
26893
|
+
} catch {
|
|
26894
|
+
throw failure2("JOURNAL_CORRUPT");
|
|
26895
|
+
}
|
|
26896
|
+
}
|
|
26897
|
+
async writeStart(firstEpoch) {
|
|
26898
|
+
const temporary = path6.join(this.dir, "journal-start." + this.lock.nonce + "." + randomBytes4(8).toString("hex") + ".tmp");
|
|
26899
|
+
const handle = await open3(temporary, constants2.O_CREAT | constants2.O_EXCL | constants2.O_WRONLY, 384);
|
|
26900
|
+
try {
|
|
26901
|
+
await handle.writeFile(this.box.seal(Buffer.from(JSON.stringify({ version: 1, firstEpoch })), "journal-start"));
|
|
26902
|
+
await handle.sync();
|
|
26903
|
+
await handle.close();
|
|
26904
|
+
await rename(temporary, path6.join(this.dir, "journal.start"));
|
|
26905
|
+
if (process.platform !== "win32") {
|
|
26906
|
+
const d = await open3(this.dir, constants2.O_RDONLY);
|
|
26907
|
+
try {
|
|
26908
|
+
await d.sync();
|
|
26909
|
+
} finally {
|
|
26910
|
+
await d.close();
|
|
26911
|
+
}
|
|
26912
|
+
}
|
|
26913
|
+
} catch (e) {
|
|
26914
|
+
await handle.close().catch(() => {
|
|
26915
|
+
});
|
|
26916
|
+
await rm2(temporary, { force: true }).catch(() => {
|
|
26917
|
+
});
|
|
26918
|
+
throw e;
|
|
26919
|
+
}
|
|
26920
|
+
this.firstEpoch = firstEpoch;
|
|
26921
|
+
}
|
|
26922
|
+
/** Called on every connection before any request of it is served; only the first one is recorded. */
|
|
26923
|
+
recordConnection(connectionEpoch) {
|
|
26924
|
+
return this.enqueue(async () => {
|
|
26925
|
+
if (!/^[1-9]\d{0,18}$/.test(connectionEpoch)) throw failure2("JOURNAL_INPUT_INVALID");
|
|
26926
|
+
if (this.firstEpoch === null) await this.writeStart(connectionEpoch);
|
|
26927
|
+
});
|
|
26928
|
+
}
|
|
26929
|
+
/** True only when this journal existed for the whole connection that leased the request and has no entry for it:
|
|
26930
|
+
* every received request is journaled synchronously on arrival, before it can start (#448). */
|
|
26931
|
+
neverReceived(executionId, connectionEpoch) {
|
|
26932
|
+
return this.enqueue(async () => {
|
|
26933
|
+
if (!uuid14.test(executionId) || !/^[1-9]\d{0,18}$/.test(connectionEpoch)) throw failure2("JOURNAL_INPUT_INVALID");
|
|
26934
|
+
return this.firstEpoch !== null && BigInt(connectionEpoch) >= BigInt(this.firstEpoch) && !this.entries.has(executionId);
|
|
26935
|
+
});
|
|
26936
|
+
}
|
|
26865
26937
|
async persist(entry) {
|
|
26866
26938
|
const st = await lstat2(this.dir);
|
|
26867
26939
|
if (!st.isDirectory() || st.isSymbolicLink()) throw failure2("JOURNAL_DIRECTORY_INVALID");
|
|
@@ -31009,6 +31081,7 @@ var NativeRelayBridge = class {
|
|
|
31009
31081
|
async connected(connection) {
|
|
31010
31082
|
if (this.connection) await this.disconnected();
|
|
31011
31083
|
this.connection = { ...connection };
|
|
31084
|
+
await this.journal.recordConnection?.(connection.connectionEpoch);
|
|
31012
31085
|
this.runtime.connected(connection, (frame) => {
|
|
31013
31086
|
if (frame.type === "control_result") this.control.get(frame.requestId)?.resolve(frame);
|
|
31014
31087
|
else if ("binding" in frame) this.execution.get(frame.binding.executionId)?.resolve(frame);
|
|
@@ -31036,6 +31109,22 @@ var NativeRelayBridge = class {
|
|
|
31036
31109
|
}
|
|
31037
31110
|
}
|
|
31038
31111
|
}
|
|
31112
|
+
/** Leases the cloud holds from earlier connections that this device never accepted (#448). Each one the journal
|
|
31113
|
+
* never received gets the same never-started receipt as #143; the cloud checks it against its own rows. */
|
|
31114
|
+
async heldLeases(bindings) {
|
|
31115
|
+
for (const binding2 of bindings) {
|
|
31116
|
+
const connection = this.connection, id2 = binding2.executionId;
|
|
31117
|
+
if (!connection || binding2.tenantId !== connection.tenantId || binding2.deviceId !== connection.deviceId || BigInt(binding2.connectionEpoch) >= BigInt(connection.connectionEpoch)) continue;
|
|
31118
|
+
if (this.terminalSending.has(id2)) continue;
|
|
31119
|
+
this.terminalSending.add(id2);
|
|
31120
|
+
try {
|
|
31121
|
+
if (await this.journal.neverReceived?.(id2, binding2.connectionEpoch)) await this.sendTerminal(binding2, "rejected", "PRE_ACCEPTANCE_REJECTED");
|
|
31122
|
+
} catch {
|
|
31123
|
+
} finally {
|
|
31124
|
+
this.terminalSending.delete(id2);
|
|
31125
|
+
}
|
|
31126
|
+
}
|
|
31127
|
+
}
|
|
31039
31128
|
async disconnected() {
|
|
31040
31129
|
this.connection = null;
|
|
31041
31130
|
for (const pending of [...this.execution.values(), ...this.control.values()]) pending.resolve(null);
|
|
@@ -46035,7 +46124,8 @@ async function connectOnce(options) {
|
|
|
46035
46124
|
tenantId,
|
|
46036
46125
|
deviceId,
|
|
46037
46126
|
guardIdentity: true,
|
|
46038
|
-
...options.aiConnectivity ? { aiConnectivity: true } : {}
|
|
46127
|
+
...options.aiConnectivity ? { aiConnectivity: true } : {},
|
|
46128
|
+
...options.onHeldLeases ? { heldLeases: true } : {}
|
|
46039
46129
|
}, maxFrameBytes);
|
|
46040
46130
|
const challenge = validateChallenge(await waitForMessage(socket, maxFrameBytes, connectTimeoutMs, "RELAY_HANDSHAKE_TIMEOUT"), protocolVersion2);
|
|
46041
46131
|
const machine = challenge.machineIdentity ? options.machineIdentity?.(deviceId) ?? null : null;
|
|
@@ -46069,8 +46159,22 @@ async function connectOnce(options) {
|
|
|
46069
46159
|
const { safeSlots, active, queued, paused } = nativeStatus();
|
|
46070
46160
|
sendJson(socket, NativeStatusSchema.parse({ type: "nativeStatus", protocolVersion: 1, connectionEpoch, agentVersion: options.agentVersion, safeSlots, active, queued, paused }), maxFrameBytes);
|
|
46071
46161
|
};
|
|
46162
|
+
let watchdog;
|
|
46163
|
+
const armWatchdog = () => {
|
|
46164
|
+
clearTimeout(watchdog);
|
|
46165
|
+
watchdog = setTimeout(() => {
|
|
46166
|
+
process.stderr.write("RELAY_HEARTBEAT_LOST\n");
|
|
46167
|
+
socket.terminate();
|
|
46168
|
+
}, monitorHeartbeat * 3);
|
|
46169
|
+
watchdog.unref();
|
|
46170
|
+
};
|
|
46171
|
+
if (monitorHeartbeat) {
|
|
46172
|
+
armWatchdog();
|
|
46173
|
+
socket.once("close", () => clearTimeout(watchdog));
|
|
46174
|
+
}
|
|
46072
46175
|
if (monitorHeartbeat) socket.on("ping", (data) => {
|
|
46073
46176
|
if (data.toString() !== "lcmcp-monitor:" + connectionEpoch) return;
|
|
46177
|
+
armWatchdog();
|
|
46074
46178
|
try {
|
|
46075
46179
|
void Promise.resolve(options.onMonitorHeartbeat?.()).catch(() => {
|
|
46076
46180
|
});
|
|
@@ -46158,6 +46262,15 @@ async function connectOnce(options) {
|
|
|
46158
46262
|
ack.data.ok ? pending.resolve() : pending.reject(new RelayClientError("RELAY_RESULT_REJECTED"));
|
|
46159
46263
|
return;
|
|
46160
46264
|
}
|
|
46265
|
+
if (frame.type === "heldLeases") {
|
|
46266
|
+
const held = HeldLeasesFrameSchema.safeParse(frame);
|
|
46267
|
+
if (!held.success || !options.onHeldLeases || held.data.connectionEpoch !== connectionEpoch) fail5("RELAY_FRAME_INVALID");
|
|
46268
|
+
try {
|
|
46269
|
+
options.onHeldLeases(held.data.bindings);
|
|
46270
|
+
} catch {
|
|
46271
|
+
}
|
|
46272
|
+
return;
|
|
46273
|
+
}
|
|
46161
46274
|
if (frame.type === "workspace_ack") {
|
|
46162
46275
|
const ack = ServerFrameSchema.safeParse(frame);
|
|
46163
46276
|
if (!ack.success || ack.data.type !== "workspace_ack" || !pendingWorkspace.has(ack.data.requestId)) fail5("RELAY_FRAME_INVALID");
|
|
@@ -46367,6 +46480,51 @@ async function readMachineGuid(timeoutMs2 = 5e3) {
|
|
|
46367
46480
|
child.on("close", (code) => finish(code === 0 ? pattern.exec(output2)?.[1]?.toLowerCase() ?? null : null));
|
|
46368
46481
|
});
|
|
46369
46482
|
}
|
|
46483
|
+
async function readComputerName(timeoutMs2 = 5e3) {
|
|
46484
|
+
const valid = (name2) => !!name2 && name2.length <= 120 && name2.trim() === name2 && !/[\u0000-\u001f\u007f]/.test(name2);
|
|
46485
|
+
if (process.platform === "linux") {
|
|
46486
|
+
let handle;
|
|
46487
|
+
try {
|
|
46488
|
+
handle = await open7("/etc/hostname", constants8.O_RDONLY | constants8.O_NOFOLLOW);
|
|
46489
|
+
const data = Buffer.alloc(257), { bytesRead } = await handle.read(data, 0, data.length, 0);
|
|
46490
|
+
const name2 = data.subarray(0, bytesRead).toString("utf8").trim();
|
|
46491
|
+
if (bytesRead <= 256 && valid(name2)) return name2;
|
|
46492
|
+
} catch {
|
|
46493
|
+
} finally {
|
|
46494
|
+
await handle?.close().catch(() => {
|
|
46495
|
+
});
|
|
46496
|
+
}
|
|
46497
|
+
}
|
|
46498
|
+
if (process.platform === "darwin") {
|
|
46499
|
+
const name2 = await new Promise((resolve) => {
|
|
46500
|
+
let output2 = "", settled = false;
|
|
46501
|
+
const finish = (value) => {
|
|
46502
|
+
if (!settled) {
|
|
46503
|
+
settled = true;
|
|
46504
|
+
clearTimeout(timer);
|
|
46505
|
+
resolve(value);
|
|
46506
|
+
}
|
|
46507
|
+
};
|
|
46508
|
+
const child = spawn5("/usr/sbin/scutil", ["--get", "ComputerName"], { shell: false, stdio: ["ignore", "pipe", "ignore"], env: buildChildEnvironment(process.env) });
|
|
46509
|
+
const timer = setTimeout(() => {
|
|
46510
|
+
child.kill();
|
|
46511
|
+
finish("");
|
|
46512
|
+
}, timeoutMs2);
|
|
46513
|
+
child.stdout.setEncoding("utf8");
|
|
46514
|
+
child.stdout.on("data", (chunk) => {
|
|
46515
|
+
if (output2.length + chunk.length <= 1024) output2 += chunk;
|
|
46516
|
+
else {
|
|
46517
|
+
child.kill();
|
|
46518
|
+
finish("");
|
|
46519
|
+
}
|
|
46520
|
+
});
|
|
46521
|
+
child.on("error", () => finish(""));
|
|
46522
|
+
child.on("close", (code) => finish(code === 0 ? output2.replace(/\n$/, "") : ""));
|
|
46523
|
+
});
|
|
46524
|
+
if (valid(name2)) return name2;
|
|
46525
|
+
}
|
|
46526
|
+
return os5.hostname();
|
|
46527
|
+
}
|
|
46370
46528
|
function machineProof(deviceId, machineGuid, computerName = os5.hostname()) {
|
|
46371
46529
|
const name2 = computerName.slice(0, 120);
|
|
46372
46530
|
if (!name2 || name2.trim() !== name2 || /[\u0000-\u001f\u007f]/.test(name2)) return null;
|
|
@@ -49276,7 +49434,7 @@ async function restoreContentAudit(input2, output2, key) {
|
|
|
49276
49434
|
}
|
|
49277
49435
|
|
|
49278
49436
|
// agent/src/main.ts
|
|
49279
|
-
var AGENT_VERSION2 = true ? "0.3.
|
|
49437
|
+
var AGENT_VERSION2 = true ? "0.3.45" : "0.3.2";
|
|
49280
49438
|
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" : "";
|
|
49281
49439
|
var AI_CONNECTION_SHIPPED = false;
|
|
49282
49440
|
function processAlive(pid) {
|
|
@@ -49355,7 +49513,8 @@ async function runAgent(options) {
|
|
|
49355
49513
|
throw error62;
|
|
49356
49514
|
}
|
|
49357
49515
|
const machineGuid = await readMachineGuid();
|
|
49358
|
-
|
|
49516
|
+
let computerName = "";
|
|
49517
|
+
const machineIdentity = machineGuid ? (deviceId) => machineProof(deviceId, machineGuid, computerName) : void 0;
|
|
49359
49518
|
const ledger = options.efficiencyLedger;
|
|
49360
49519
|
monitor(() => options.monitor?.setLifetimeSavings(ledger?.savingsSnapshot?.()));
|
|
49361
49520
|
const efficiencyLedger = ledger && {
|
|
@@ -49422,6 +49581,7 @@ async function runAgent(options) {
|
|
|
49422
49581
|
});
|
|
49423
49582
|
try {
|
|
49424
49583
|
while (!options.signal.aborted) {
|
|
49584
|
+
computerName = await readComputerName();
|
|
49425
49585
|
let ready;
|
|
49426
49586
|
const readyPromise = new Promise((resolve) => {
|
|
49427
49587
|
ready = resolve;
|
|
@@ -49436,6 +49596,11 @@ async function runAgent(options) {
|
|
|
49436
49596
|
safeSlots: runtime.status().safeSlots,
|
|
49437
49597
|
status: () => runtime.status(),
|
|
49438
49598
|
onIncidentAck: (ack) => incidentProducer.ack(ack),
|
|
49599
|
+
// After bridge.connected, like requests: the journal has recorded this connection first.
|
|
49600
|
+
onHeldLeases: (bindings) => {
|
|
49601
|
+
void readyPromise.then(() => bridge.heldLeases(bindings)).catch(() => {
|
|
49602
|
+
});
|
|
49603
|
+
},
|
|
49439
49604
|
handleRequest: async (request) => {
|
|
49440
49605
|
await readyPromise;
|
|
49441
49606
|
monitor(() => {
|