local-commander-agent 0.3.43 → 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 +12 -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 +176 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
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
|
+
|
|
10
|
+
## 0.3.44 - 2026-10-22
|
|
11
|
+
|
|
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.
|
|
13
|
+
- Includes everything in 0.3.43.
|
|
14
|
+
|
|
3
15
|
## 0.3.43 - 2026-10-21
|
|
4
16
|
|
|
5
17
|
- Faster: file search on Windows reads files in batches through one helper process instead of starting one per file. A search over 300 files now takes well under a second. Results are unchanged.
|
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");
|
|
@@ -27330,7 +27402,7 @@ async function choosePosixFolder() {
|
|
|
27330
27402
|
}
|
|
27331
27403
|
async function choosePosixAccess() {
|
|
27332
27404
|
return terminal2(async (ask) => {
|
|
27333
|
-
if (process.platform === "darwin") await ask("
|
|
27405
|
+
if (process.platform === "darwin") await ask("Local Commander runs each command as its own launchd job and stops that job's processes when the command ends; work handed to other services is not stopped. Press Enter to review access: ");
|
|
27334
27406
|
const choice = await ask("Local Commander access\n1. One folder: read and write files\n2. Full Access: account capabilities across this computer, including commands and localhost\nEnter 1 or 2 (empty cancels): ");
|
|
27335
27407
|
if (choice !== "1" && choice !== "2") return null;
|
|
27336
27408
|
const folder = choice === "2" ? "/" : await ask("Absolute folder path: ");
|
|
@@ -28370,11 +28442,14 @@ var WindowsPrivateStore = class _WindowsPrivateStore {
|
|
|
28370
28442
|
}
|
|
28371
28443
|
/** Replace scope only after an explicit local dialog; updates never call this implicitly. */
|
|
28372
28444
|
async configureAccess() {
|
|
28445
|
+
let account = "";
|
|
28373
28446
|
const expected = await this.run(() => this.withLock(async () => {
|
|
28374
|
-
|
|
28447
|
+
const enrollment = (await this.readState())?.enrollment;
|
|
28448
|
+
if (!enrollment) throw fail2("ENROLLMENT_REQUIRED");
|
|
28449
|
+
account = enrollment.accountEmail;
|
|
28375
28450
|
return this.rawHash();
|
|
28376
28451
|
}));
|
|
28377
|
-
const decision = await this.chooseAccess();
|
|
28452
|
+
const decision = await this.chooseAccess(account);
|
|
28378
28453
|
if (decision === null) return null;
|
|
28379
28454
|
const selected = parseAccessSelection(decision);
|
|
28380
28455
|
if (!selected) return null;
|
|
@@ -28417,9 +28492,9 @@ var WindowsPrivateStore = class _WindowsPrivateStore {
|
|
|
28417
28492
|
return structuredClone(workspaces);
|
|
28418
28493
|
}));
|
|
28419
28494
|
}
|
|
28420
|
-
chooseAccess() {
|
|
28495
|
+
chooseAccess(account) {
|
|
28421
28496
|
if (this.posix) return this.run(() => choosePosixAccess());
|
|
28422
|
-
return this.run(async () => parseAccessSelection(await _WindowsPrivateStore.invoke(this.bridge, "choose-access", {}, 24e4, true)));
|
|
28497
|
+
return this.run(async () => parseAccessSelection(await _WindowsPrivateStore.invoke(this.bridge, "choose-access", { data: Buffer.from(account, "utf8").toString("base64") }, 24e4, true)));
|
|
28423
28498
|
}
|
|
28424
28499
|
loadPermissionHistory() {
|
|
28425
28500
|
return this.run(async () => structuredClone((await this.readState())?.permissionHistory ?? []));
|
|
@@ -31006,6 +31081,7 @@ var NativeRelayBridge = class {
|
|
|
31006
31081
|
async connected(connection) {
|
|
31007
31082
|
if (this.connection) await this.disconnected();
|
|
31008
31083
|
this.connection = { ...connection };
|
|
31084
|
+
await this.journal.recordConnection?.(connection.connectionEpoch);
|
|
31009
31085
|
this.runtime.connected(connection, (frame) => {
|
|
31010
31086
|
if (frame.type === "control_result") this.control.get(frame.requestId)?.resolve(frame);
|
|
31011
31087
|
else if ("binding" in frame) this.execution.get(frame.binding.executionId)?.resolve(frame);
|
|
@@ -31033,6 +31109,22 @@ var NativeRelayBridge = class {
|
|
|
31033
31109
|
}
|
|
31034
31110
|
}
|
|
31035
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
|
+
}
|
|
31036
31128
|
async disconnected() {
|
|
31037
31129
|
this.connection = null;
|
|
31038
31130
|
for (const pending of [...this.execution.values(), ...this.control.values()]) pending.resolve(null);
|
|
@@ -46032,7 +46124,8 @@ async function connectOnce(options) {
|
|
|
46032
46124
|
tenantId,
|
|
46033
46125
|
deviceId,
|
|
46034
46126
|
guardIdentity: true,
|
|
46035
|
-
...options.aiConnectivity ? { aiConnectivity: true } : {}
|
|
46127
|
+
...options.aiConnectivity ? { aiConnectivity: true } : {},
|
|
46128
|
+
...options.onHeldLeases ? { heldLeases: true } : {}
|
|
46036
46129
|
}, maxFrameBytes);
|
|
46037
46130
|
const challenge = validateChallenge(await waitForMessage(socket, maxFrameBytes, connectTimeoutMs, "RELAY_HANDSHAKE_TIMEOUT"), protocolVersion2);
|
|
46038
46131
|
const machine = challenge.machineIdentity ? options.machineIdentity?.(deviceId) ?? null : null;
|
|
@@ -46066,8 +46159,22 @@ async function connectOnce(options) {
|
|
|
46066
46159
|
const { safeSlots, active, queued, paused } = nativeStatus();
|
|
46067
46160
|
sendJson(socket, NativeStatusSchema.parse({ type: "nativeStatus", protocolVersion: 1, connectionEpoch, agentVersion: options.agentVersion, safeSlots, active, queued, paused }), maxFrameBytes);
|
|
46068
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
|
+
}
|
|
46069
46175
|
if (monitorHeartbeat) socket.on("ping", (data) => {
|
|
46070
46176
|
if (data.toString() !== "lcmcp-monitor:" + connectionEpoch) return;
|
|
46177
|
+
armWatchdog();
|
|
46071
46178
|
try {
|
|
46072
46179
|
void Promise.resolve(options.onMonitorHeartbeat?.()).catch(() => {
|
|
46073
46180
|
});
|
|
@@ -46155,6 +46262,15 @@ async function connectOnce(options) {
|
|
|
46155
46262
|
ack.data.ok ? pending.resolve() : pending.reject(new RelayClientError("RELAY_RESULT_REJECTED"));
|
|
46156
46263
|
return;
|
|
46157
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
|
+
}
|
|
46158
46274
|
if (frame.type === "workspace_ack") {
|
|
46159
46275
|
const ack = ServerFrameSchema.safeParse(frame);
|
|
46160
46276
|
if (!ack.success || ack.data.type !== "workspace_ack" || !pendingWorkspace.has(ack.data.requestId)) fail5("RELAY_FRAME_INVALID");
|
|
@@ -46364,6 +46480,51 @@ async function readMachineGuid(timeoutMs2 = 5e3) {
|
|
|
46364
46480
|
child.on("close", (code) => finish(code === 0 ? pattern.exec(output2)?.[1]?.toLowerCase() ?? null : null));
|
|
46365
46481
|
});
|
|
46366
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
|
+
}
|
|
46367
46528
|
function machineProof(deviceId, machineGuid, computerName = os5.hostname()) {
|
|
46368
46529
|
const name2 = computerName.slice(0, 120);
|
|
46369
46530
|
if (!name2 || name2.trim() !== name2 || /[\u0000-\u001f\u007f]/.test(name2)) return null;
|
|
@@ -49273,7 +49434,7 @@ async function restoreContentAudit(input2, output2, key) {
|
|
|
49273
49434
|
}
|
|
49274
49435
|
|
|
49275
49436
|
// agent/src/main.ts
|
|
49276
|
-
var AGENT_VERSION2 = true ? "0.3.
|
|
49437
|
+
var AGENT_VERSION2 = true ? "0.3.45" : "0.3.2";
|
|
49277
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" : "";
|
|
49278
49439
|
var AI_CONNECTION_SHIPPED = false;
|
|
49279
49440
|
function processAlive(pid) {
|
|
@@ -49352,7 +49513,8 @@ async function runAgent(options) {
|
|
|
49352
49513
|
throw error62;
|
|
49353
49514
|
}
|
|
49354
49515
|
const machineGuid = await readMachineGuid();
|
|
49355
|
-
|
|
49516
|
+
let computerName = "";
|
|
49517
|
+
const machineIdentity = machineGuid ? (deviceId) => machineProof(deviceId, machineGuid, computerName) : void 0;
|
|
49356
49518
|
const ledger = options.efficiencyLedger;
|
|
49357
49519
|
monitor(() => options.monitor?.setLifetimeSavings(ledger?.savingsSnapshot?.()));
|
|
49358
49520
|
const efficiencyLedger = ledger && {
|
|
@@ -49419,6 +49581,7 @@ async function runAgent(options) {
|
|
|
49419
49581
|
});
|
|
49420
49582
|
try {
|
|
49421
49583
|
while (!options.signal.aborted) {
|
|
49584
|
+
computerName = await readComputerName();
|
|
49422
49585
|
let ready;
|
|
49423
49586
|
const readyPromise = new Promise((resolve) => {
|
|
49424
49587
|
ready = resolve;
|
|
@@ -49433,6 +49596,11 @@ async function runAgent(options) {
|
|
|
49433
49596
|
safeSlots: runtime.status().safeSlots,
|
|
49434
49597
|
status: () => runtime.status(),
|
|
49435
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
|
+
},
|
|
49436
49604
|
handleRequest: async (request) => {
|
|
49437
49605
|
await readyPromise;
|
|
49438
49606
|
monitor(() => {
|