pi-subagents 0.80.0 → 0.82.0
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 +66 -0
- package/bundle/agents.js +18 -0
- package/bundle/background-work.js +16 -0
- package/bundle/capability-ceiling.js +22 -0
- package/bundle/child-tool-plan.js +11 -0
- package/bundle/chunk-43SCHIMD.js +57 -0
- package/bundle/chunk-4QFZZV3O.js +57 -0
- package/bundle/chunk-4SDU2BZX.js +1190 -0
- package/bundle/chunk-52UBALJL.js +753 -0
- package/bundle/chunk-5LFLVSTM.js +260 -0
- package/bundle/chunk-5TYMSZX3.js +235 -0
- package/bundle/chunk-62OCY2S4.js +14 -0
- package/bundle/chunk-7F6RKT6W.js +132 -0
- package/bundle/chunk-7IVSUMQQ.js +1337 -0
- package/bundle/chunk-7J4ZXMY7.js +18 -0
- package/bundle/chunk-7PCKZHR5.js +1234 -0
- package/bundle/chunk-7YMXOYEU.js +1373 -0
- package/bundle/chunk-B5EKXDHE.js +16870 -0
- package/bundle/chunk-CIZQXJ5Q.js +8619 -0
- package/bundle/chunk-CTPAE36Y.js +94 -0
- package/bundle/chunk-D4EHT5SJ.js +14 -0
- package/bundle/chunk-GC4DSPMH.js +946 -0
- package/bundle/chunk-GJECHSAR.js +179 -0
- package/bundle/chunk-M2CTFYT3.js +4164 -0
- package/bundle/chunk-MHZ4G23A.js +1296 -0
- package/bundle/chunk-N2BN7BF5.js +159 -0
- package/bundle/chunk-NF7IEKBH.js +490 -0
- package/bundle/chunk-NGGAFDL6.js +742 -0
- package/bundle/chunk-NIXWDZZV.js +9 -0
- package/bundle/chunk-NRQ75MGH.js +8327 -0
- package/bundle/chunk-OYF6AFDY.js +458 -0
- package/bundle/chunk-PLCCBLPY.js +526 -0
- package/bundle/chunk-RMQFMMAT.js +416 -0
- package/bundle/chunk-SIJ4S3EN.js +276 -0
- package/bundle/chunk-UKG4V45O.js +228 -0
- package/bundle/chunk-VH5OAWVG.js +4096 -0
- package/bundle/chunk-W2SZ22EM.js +5462 -0
- package/bundle/chunk-WVD4UEL4.js +9367 -0
- package/bundle/chunk-XSWOTIA3.js +175 -0
- package/bundle/chunk-YFCK7H2S.js +129 -0
- package/bundle/chunk-YKEWIB4M.js +380 -0
- package/bundle/chunk-YYREO77V.js +137 -0
- package/bundle/chunk-Z7KVHQHL.js +173 -0
- package/bundle/chunk-ZLINIYTP.js +8613 -0
- package/bundle/chunk-ZQGSDHL6.js +128 -0
- package/bundle/control-channel.js +11 -0
- package/bundle/delegation.js +14 -0
- package/bundle/external-job-provider.js +20 -0
- package/bundle/external-runs.js +21 -0
- package/bundle/index.js +30 -0
- package/bundle/inspectors.js +8 -0
- package/bundle/intercom-bridge.js +11 -0
- package/bundle/preflight.js +425 -0
- package/bundle/project-panes.js +28 -0
- package/bundle/required-child-extensions.js +6 -0
- package/bundle/shared-types.js +8 -0
- package/bundle/workflow-resources.js +7 -0
- package/docs/agents.md +31 -0
- package/docs/configuration.md +9 -1
- package/docs/extension-api.md +27 -0
- package/docs/observability.md +6 -2
- package/docs/tool-reference.md +3 -1
- package/docs/workflows.md +16 -0
- package/package.json +19 -19
- package/skills/pi-subagents/references/execution-controls.md +5 -4
- package/src/agents/agent-management.js +3 -3
- package/src/agents/agent-memory.js +3 -1
- package/src/agents/agents.js +6 -6
- package/src/agents/frontmatter.d.ts +4 -0
- package/src/agents/frontmatter.js +24 -3
- package/src/agents/skills.js +2 -1
- package/src/api/delegation.d.ts +20 -0
- package/src/extension/config.js +3 -0
- package/src/extension/herdr-pi-bridge.js +17 -10
- package/src/extension/index.js +109 -9
- package/src/extension/schemas.js +3 -3
- package/src/intercom/native-supervisor-channel.js +20 -0
- package/src/missions/lifecycle.js +7 -1
- package/src/runs/background/active-async-capacity.d.ts +1 -0
- package/src/runs/background/active-async-capacity.js +9 -3
- package/src/runs/background/async-execution.js +20 -10
- package/src/runs/background/async-job-tracker.d.ts +6 -1
- package/src/runs/background/async-job-tracker.js +9 -4
- package/src/runs/background/async-status.d.ts +2 -0
- package/src/runs/background/async-status.js +21 -2
- package/src/runs/background/await-async-run.js +3 -7
- package/src/runs/background/boot-id.d.ts +12 -0
- package/src/runs/background/boot-id.js +47 -0
- package/src/runs/background/interrupted-elsewhere.d.ts +22 -0
- package/src/runs/background/interrupted-elsewhere.js +68 -0
- package/src/runs/background/notify.d.ts +2 -0
- package/src/runs/background/notify.js +13 -0
- package/src/runs/background/result-delivery-ownership.d.ts +6 -0
- package/src/runs/background/result-delivery-ownership.js +19 -1
- package/src/runs/background/result-watcher.d.ts +3 -1
- package/src/runs/background/result-watcher.js +24 -8
- package/src/runs/background/run-child-session.d.ts +1 -6
- package/src/runs/background/run-child-session.js +19 -1
- package/src/runs/background/run-status.js +2 -1
- package/src/runs/background/stale-run-reconciler.d.ts +25 -0
- package/src/runs/background/stale-run-reconciler.js +54 -13
- package/src/runs/background/subagent-runner-bootstrap.js +3 -0
- package/src/runs/background/subagent-runner.d.ts +4 -0
- package/src/runs/background/subagent-runner.js +13 -5
- package/src/runs/background/subagent-wait.d.ts +1 -1
- package/src/runs/background/subagent-wait.js +6 -1
- package/src/runs/background/terminal-run-index.d.ts +1 -0
- package/src/runs/background/terminal-run-index.js +4 -2
- package/src/runs/foreground/async-stop-action.js +3 -1
- package/src/runs/foreground/execution.js +45 -5
- package/src/runs/foreground/foreground-history.d.ts +16 -2
- package/src/runs/foreground/foreground-history.js +133 -31
- package/src/runs/foreground/subagent-executor.d.ts +11 -0
- package/src/runs/foreground/subagent-executor.js +277 -74
- package/src/runs/shared/child-launch.d.ts +2 -2
- package/src/runs/shared/child-session.d.ts +3 -2
- package/src/runs/shared/child-session.js +51 -8
- package/src/runs/shared/child-tool-plan.d.ts +7 -0
- package/src/runs/shared/child-tool-plan.js +21 -6
- package/src/runs/shared/herdr-connection.d.ts +17 -8
- package/src/runs/shared/herdr-connection.js +19 -7
- package/src/runs/shared/herdr-external-adapters.d.ts +3 -2
- package/src/runs/shared/herdr-external-adapters.js +1 -1
- package/src/runs/shared/herdr-machine.d.ts +12 -2
- package/src/runs/shared/herdr-machine.js +55 -9
- package/src/runs/shared/herdr-pane-host.d.ts +88 -0
- package/src/runs/shared/herdr-pane-host.js +155 -0
- package/src/runs/shared/herdr-placed-run.d.ts +27 -150
- package/src/runs/shared/herdr-placed-run.js +48 -395
- package/src/runs/shared/local-machine-access.d.ts +5 -0
- package/src/runs/shared/local-machine-access.js +28 -0
- package/src/runs/shared/orca-progress-tabs.d.ts +0 -1
- package/src/runs/shared/orca-progress-tabs.js +1 -24
- package/src/runs/shared/parallel-utils.d.ts +1 -1
- package/src/runs/shared/placed-run-owner.d.ts +49 -0
- package/src/runs/shared/placed-run-owner.js +93 -0
- package/src/runs/shared/placement-host.d.ts +99 -0
- package/src/runs/shared/placement-host.js +157 -0
- package/src/runs/shared/rex-machine.d.ts +25 -0
- package/src/runs/shared/rex-machine.js +83 -0
- package/src/runs/shared/rex-pane-host.d.ts +62 -0
- package/src/runs/shared/rex-pane-host.js +327 -0
- package/src/runs/shared/structured-output.d.ts +1 -0
- package/src/runs/shared/structured-output.js +1 -0
- package/src/shared/accessible-dir.js +4 -1
- package/src/shared/completion-owner.d.ts +5 -0
- package/src/shared/completion-owner.js +5 -0
- package/src/shared/provider-diagnostics.d.ts +9 -0
- package/src/shared/provider-diagnostics.js +75 -0
- package/src/shared/session-identity.d.ts +2 -0
- package/src/shared/session-identity.js +26 -0
- package/src/shared/types.d.ts +48 -6
- package/src/slash/delegation-adapters.d.ts +3 -1
- package/src/slash/delegation-adapters.js +7 -0
- package/src/slash/prompt-template-bridge.js +3 -1
- package/src/slash/subagents-admin.d.ts +1 -0
- package/src/slash/subagents-admin.js +1 -1
- package/src/tui/fleet-layout.d.ts +20 -0
- package/src/tui/fleet-layout.js +17 -0
- package/src/tui/fleet-viewport.d.ts +12 -0
- package/src/tui/fleet-viewport.js +35 -0
- package/src/tui/fleet.d.ts +6 -6
- package/src/tui/fleet.js +93 -66
- package/src/tui/render.d.ts +6 -0
- package/src/tui/render.js +3 -3
- package/src/tui/subagent-messages.js +1 -0
- package/src/workflows/scripted-workflow.d.ts +1 -1
- package/src/workflows/structured-workflow-scripts.d.ts +1 -0
- package/src/workflows/structured-workflow-scripts.js +5 -4
- package/src/workflows/workflow-auto-resume.d.ts +32 -0
- package/src/workflows/workflow-auto-resume.js +102 -0
- package/src/workflows/workflow-launch-record.d.ts +27 -0
- package/src/workflows/workflow-launch-record.js +48 -0
- package/src/workflows/workflow-resources.js +2 -2
- package/src/workflows/workflow-resume-claim.d.ts +82 -0
- package/src/workflows/workflow-resume-claim.js +210 -0
- package/src/workflows/workflow-resume.d.ts +56 -0
- package/src/workflows/workflow-resume.js +106 -0
- package/src/workflows/workflow-reuse.d.ts +15 -9
- package/src/workflows/workflow-reuse.js +77 -14
- package/src/workflows/workflow-revival.d.ts +2 -0
- package/src/workflows/workflow-revival.js +2 -1
- package/src/workflows/workflow-stop-cause.d.ts +6 -0
- package/src/workflows/workflow-stop-cause.js +8 -0
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
// src/runs/shared/rex-machine.ts
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as os from "node:os";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
var REX_LOCAL_MACHINE = "local";
|
|
7
|
+
var REX_EXIT_GONE = 3;
|
|
8
|
+
var REX_EXIT_UNREACHABLE = 4;
|
|
9
|
+
function isRexLocalMachine(machine) {
|
|
10
|
+
return machine?.provider === "rex" && !("target" in machine);
|
|
11
|
+
}
|
|
12
|
+
var REX_BUNDLE_BIN = "Library/Application Support/com.superlogical.rex/RexServer/Versions/Current/Rex Server.app/Contents/MacOS/rex";
|
|
13
|
+
var REX_CALL_TIMEOUT_MS = 15e3;
|
|
14
|
+
var REX_MAX_OUTPUT = 1024 * 1024;
|
|
15
|
+
function rexGlobalArgs(server) {
|
|
16
|
+
return server ? ["--autostart=false", "-S", server] : ["--autostart=false"];
|
|
17
|
+
}
|
|
18
|
+
function executable(file) {
|
|
19
|
+
try {
|
|
20
|
+
fs.accessSync(file, fs.constants.X_OK);
|
|
21
|
+
return fs.statSync(file).isFile();
|
|
22
|
+
} catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function resolveRexBinary(env = process.env) {
|
|
27
|
+
const fromDir = env.REX_BIN_DIR ? [path.join(env.REX_BIN_DIR, "rex")] : [];
|
|
28
|
+
const fromPath = (env.PATH ?? "").split(path.delimiter).filter((dir) => path.isAbsolute(dir)).map((dir) => path.join(dir, "rex"));
|
|
29
|
+
const found = [...fromDir, ...fromPath, path.join(os.homedir(), REX_BUNDLE_BIN)].find(executable);
|
|
30
|
+
if (!found) throw new Error("The Rex CLI was not found: set REX_BIN_DIR, put rex on PATH, or install Rex.");
|
|
31
|
+
return found;
|
|
32
|
+
}
|
|
33
|
+
function rexCommandEnv(source = process.env) {
|
|
34
|
+
const { REX_SESSION: _session, REX_BLOCK: _block, REX_SERVER: _server, ...env } = source;
|
|
35
|
+
return env;
|
|
36
|
+
}
|
|
37
|
+
function sessionConfirmed(stdout, session) {
|
|
38
|
+
try {
|
|
39
|
+
const parsed = JSON.parse(stdout);
|
|
40
|
+
return parsed?.session_id === session;
|
|
41
|
+
} catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function resolveRexLocalPlacement(input) {
|
|
46
|
+
if (process.platform === "win32") throw new Error("Machine 'local' runs the child in a Rex pane, which is not supported on Windows.");
|
|
47
|
+
const env = input.env ?? process.env, session = env.REX_SESSION?.trim(), server = env.REX_SERVER?.trim();
|
|
48
|
+
if (!session) throw new Error("Machine 'local' opens the child in a new tab of this Rex session, but this Pi is not running inside Rex (REX_SESSION is not set).");
|
|
49
|
+
const result = spawnSync(resolveRexBinary(env), [...rexGlobalArgs(server), "session", "inspect", session, "--json"], { encoding: "utf8", env: rexCommandEnv(env), timeout: REX_CALL_TIMEOUT_MS, maxBuffer: REX_MAX_OUTPUT, windowsHide: true });
|
|
50
|
+
if (result.error) throw new Error(`Could not ask Rex about this session: ${result.error.message}`, { cause: result.error });
|
|
51
|
+
if (result.status === REX_EXIT_GONE) throw new Error(`Machine 'local' needs this Pi's Rex session, but the Rex server does not know session ${session}.`);
|
|
52
|
+
if (result.status === REX_EXIT_UNREACHABLE) throw new Error("Machine 'local' needs the Rex server, but it is unreachable.");
|
|
53
|
+
if (result.status !== 0) throw new Error(`rex session inspect exited with code ${result.status}: ${(result.stderr || result.stdout).trim().slice(0, 512)}`);
|
|
54
|
+
if (!sessionConfirmed(result.stdout, session)) throw new Error(`The Rex server did not confirm session ${session}.`);
|
|
55
|
+
const stepCwd = input.stepCwd?.trim();
|
|
56
|
+
const home = stepCwd === "~" || stepCwd?.startsWith("~/") ? path.join(os.homedir(), stepCwd.slice(1)) : void 0;
|
|
57
|
+
const requested = home ?? (stepCwd ? path.resolve(input.cwd, stepCwd) : input.cwd);
|
|
58
|
+
let cwd;
|
|
59
|
+
try {
|
|
60
|
+
cwd = fs.realpathSync(requested);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
throw new Error(`Machine 'local' cwd does not exist: ${requested}.`, { cause: error });
|
|
63
|
+
}
|
|
64
|
+
if (!fs.statSync(cwd).isDirectory()) throw new Error(`Machine 'local' cwd is not a directory: ${requested}.`);
|
|
65
|
+
const machine = { provider: "rex", id: REX_LOCAL_MACHINE, session, cwd };
|
|
66
|
+
if (server) machine.server = server;
|
|
67
|
+
return machine;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/runs/shared/herdr-connection.ts
|
|
71
|
+
import { spawn, spawnSync as spawnSync2 } from "node:child_process";
|
|
72
|
+
import * as fs2 from "node:fs";
|
|
73
|
+
import * as net from "node:net";
|
|
74
|
+
import * as os2 from "node:os";
|
|
75
|
+
import * as path2 from "node:path";
|
|
76
|
+
var MAX_DISCOVERY_BYTES = 256 * 1024;
|
|
77
|
+
var MAX_RPC_BYTES = 4 * 1024 * 1024;
|
|
78
|
+
var SUBSCRIPTION_ACK_TIMEOUT_MS = 15e3;
|
|
79
|
+
var HERDR_REMOTE_PATH = "$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin";
|
|
80
|
+
var HERDR_SSH_BASE = ["-T", "-o", "BatchMode=yes", "-o", "NumberOfPasswordPrompts=0", "-o", "StrictHostKeyChecking=yes", "-o", "ConnectTimeout=10", "-o", "ConnectionAttempts=1", "-o", "ForwardAgent=no", "-o", "ExitOnForwardFailure=yes", "-o", "ServerAliveInterval=15", "-o", "ServerAliveCountMax=4", "-o", "SendEnv=-*", "-o", "SetEnv=PI_SUBAGENTS_SSH_GUARD="];
|
|
81
|
+
function hardenedSshEnv(source = process.env) {
|
|
82
|
+
return { PATH: process.platform === "win32" ? source.PATH : "/usr/bin:/bin:/usr/sbin:/sbin", ...source.SystemRoot ? { SystemRoot: source.SystemRoot } : {}, ...source.WINDIR ? { WINDIR: source.WINDIR } : {} };
|
|
83
|
+
}
|
|
84
|
+
function herdrSshArgs(source = process.env) {
|
|
85
|
+
return [...HERDR_SSH_BASE, ...source.SSH_AUTH_SOCK ? ["-o", `IdentityAgent=${source.SSH_AUTH_SOCK}`] : []];
|
|
86
|
+
}
|
|
87
|
+
var HerdrRpcError = class extends Error {
|
|
88
|
+
code;
|
|
89
|
+
requestId;
|
|
90
|
+
constructor(message, requestId, code) {
|
|
91
|
+
super(message);
|
|
92
|
+
this.name = "HerdrRpcError";
|
|
93
|
+
this.requestId = requestId;
|
|
94
|
+
this.code = code;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
var HerdrTransportError = class extends Error {
|
|
98
|
+
constructor(message, options) {
|
|
99
|
+
super(message, options);
|
|
100
|
+
this.name = "HerdrTransportError";
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
function decodeHerdrJsonLine(line) {
|
|
104
|
+
try {
|
|
105
|
+
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(line));
|
|
106
|
+
} catch {
|
|
107
|
+
throw new Error("Herdr emitted malformed JSON or UTF-8.");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function shellQuoteRemote(value) {
|
|
111
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
112
|
+
}
|
|
113
|
+
function remoteShellCommand(script, args = []) {
|
|
114
|
+
return `sh -c ${shellQuoteRemote(`PATH="${HERDR_REMOTE_PATH}"; export PATH; ${script}`)} sh${args.map((arg) => ` ${shellQuoteRemote(arg)}`).join("")}`;
|
|
115
|
+
}
|
|
116
|
+
function canonicalHerdrSession(session) {
|
|
117
|
+
return session && session !== "default" ? session : null;
|
|
118
|
+
}
|
|
119
|
+
function sshEnvCommand(session, command) {
|
|
120
|
+
const selected = canonicalHerdrSession(session), assignment = selected ? ` HERDR_SESSION=${shellQuoteRemote(selected)}` : "";
|
|
121
|
+
return `/usr/bin/env -u HERDR_SOCKET_PATH -u HERDR_SESSION${assignment} ${remoteShellCommand(command)}`;
|
|
122
|
+
}
|
|
123
|
+
function runHerdrRemoteCommand(machine, command, options = {}) {
|
|
124
|
+
return spawnSync2(options.sshBin ?? "ssh", [...herdrSshArgs(options.env), machine.target, command], { encoding: "utf8", env: hardenedSshEnv(options.env), timeout: options.timeout ?? 15e3, maxBuffer: options.maxBuffer ?? MAX_DISCOVERY_BYTES, windowsHide: true });
|
|
125
|
+
}
|
|
126
|
+
function runHerdrRemoteCommandAsync(machine, command, options = {}) {
|
|
127
|
+
return runBoundedCommand(options.sshBin ?? "ssh", [...herdrSshArgs(options.env), machine.target, command], { env: hardenedSshEnv(options.env), timeout: options.timeout, maxBuffer: options.maxBuffer });
|
|
128
|
+
}
|
|
129
|
+
function runBoundedCommand(file, args, options) {
|
|
130
|
+
return new Promise((resolve2) => {
|
|
131
|
+
const child = spawn(file, args, { env: options.env, stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
|
|
132
|
+
const stdout = [], stderr = [], maxBuffer = options.maxBuffer ?? MAX_DISCOVERY_BYTES;
|
|
133
|
+
let stdoutBytes = 0, stderrBytes = 0, error, settled = false, escalation;
|
|
134
|
+
const finish = (status, spawnError = error) => {
|
|
135
|
+
if (settled) return;
|
|
136
|
+
settled = true;
|
|
137
|
+
clearTimeout(timer);
|
|
138
|
+
if (escalation) clearTimeout(escalation);
|
|
139
|
+
resolve2({ status, stdout: Buffer.concat(stdout).toString("utf8"), stderr: Buffer.concat(stderr).toString("utf8"), ...spawnError ? { error: spawnError } : {} });
|
|
140
|
+
};
|
|
141
|
+
const terminate = () => {
|
|
142
|
+
child.kill("SIGTERM");
|
|
143
|
+
escalation ??= setTimeout(() => {
|
|
144
|
+
if (!settled) {
|
|
145
|
+
child.kill("SIGKILL");
|
|
146
|
+
finish(child.exitCode ?? null);
|
|
147
|
+
}
|
|
148
|
+
}, 1e3);
|
|
149
|
+
escalation.unref?.();
|
|
150
|
+
};
|
|
151
|
+
const collect = (target, stream) => (chunk) => {
|
|
152
|
+
const bytes = stream === "stdout" ? stdoutBytes += chunk.byteLength : stderrBytes += chunk.byteLength;
|
|
153
|
+
if (bytes > maxBuffer) {
|
|
154
|
+
error ??= new Error(`${stream} maxBuffer length exceeded`);
|
|
155
|
+
terminate();
|
|
156
|
+
} else target.push(chunk);
|
|
157
|
+
};
|
|
158
|
+
child.stdout.on("data", collect(stdout, "stdout"));
|
|
159
|
+
child.stderr.on("data", collect(stderr, "stderr"));
|
|
160
|
+
child.once("error", (spawnError) => finish(null, spawnError));
|
|
161
|
+
child.once("close", (status) => finish(status));
|
|
162
|
+
const timer = setTimeout(terminate, options.timeout ?? 15e3);
|
|
163
|
+
timer.unref?.();
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
function parseHerdrEndpoint(value, expectedSession) {
|
|
167
|
+
let parsed;
|
|
168
|
+
try {
|
|
169
|
+
parsed = JSON.parse(value);
|
|
170
|
+
} catch {
|
|
171
|
+
throw new Error("Remote Herdr endpoint discovery returned malformed JSON.");
|
|
172
|
+
}
|
|
173
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("Remote Herdr endpoint discovery returned no endpoint.");
|
|
174
|
+
const p = parsed;
|
|
175
|
+
if (typeof p.socket !== "string" || !path2.posix.isAbsolute(p.socket) || typeof p.version !== "string" || typeof p.protocol !== "number") throw new Error("Remote Herdr endpoint discovery returned incomplete identity.");
|
|
176
|
+
const session = canonicalHerdrSession(typeof p.session === "string" ? p.session : null), expected = canonicalHerdrSession(expectedSession);
|
|
177
|
+
if (expected !== session) throw new Error(`Remote Herdr session identity mismatch: expected ${expected ?? "default"}, received ${session ?? "default"}.`);
|
|
178
|
+
if (p.running !== true || p.compatible !== true || p.endpoint_compatible === false) throw new Error("The selected remote Herdr session is stopped or incompatible.");
|
|
179
|
+
return { socket: p.socket, session, version: p.version, protocol: p.protocol, compatible: true, running: true };
|
|
180
|
+
}
|
|
181
|
+
async function discoverHerdrEndpoint(machine, options = {}) {
|
|
182
|
+
const discovery = 'herdr_path=$(command -v herdr) || exit 127; case "$herdr_path" in /*/herdr) ;; *) exit 126;; esac; exec "$herdr_path" status server --json';
|
|
183
|
+
const result = await runHerdrRemoteCommandAsync(machine, sshEnvCommand(machine.session, discovery), { ...options, timeout: 15e3, maxBuffer: MAX_DISCOVERY_BYTES });
|
|
184
|
+
if (result.error) throw new Error(`Remote Herdr discovery failed: ${result.error.message}`);
|
|
185
|
+
if (result.status !== 0) throw new Error(`Remote Herdr discovery failed with code ${result.status}: ${(result.stderr || result.stdout).trim()}`);
|
|
186
|
+
return parseHerdrEndpoint(result.stdout, machine.session);
|
|
187
|
+
}
|
|
188
|
+
var SocketRpcClient = class {
|
|
189
|
+
#socketPath;
|
|
190
|
+
#subscriptionAckTimeoutMs;
|
|
191
|
+
#next = 0;
|
|
192
|
+
constructor(socketPath, subscriptionAckTimeoutMs = SUBSCRIPTION_ACK_TIMEOUT_MS) {
|
|
193
|
+
this.#socketPath = socketPath;
|
|
194
|
+
this.#subscriptionAckTimeoutMs = subscriptionAckTimeoutMs;
|
|
195
|
+
}
|
|
196
|
+
call(method, params = {}, timeoutMs = 15e3) {
|
|
197
|
+
return new Promise((resolve2, reject) => {
|
|
198
|
+
const socket = net.createConnection(this.#socketPath);
|
|
199
|
+
let bytes = 0;
|
|
200
|
+
let data = Buffer.alloc(0);
|
|
201
|
+
let finished = false;
|
|
202
|
+
const id = String(++this.#next);
|
|
203
|
+
const timer = setTimeout(() => finish(new HerdrTransportError(`Herdr RPC '${method}' timed out.`)), timeoutMs);
|
|
204
|
+
timer.unref?.();
|
|
205
|
+
const finish = (error, value) => {
|
|
206
|
+
if (finished) return;
|
|
207
|
+
finished = true;
|
|
208
|
+
clearTimeout(timer);
|
|
209
|
+
socket.destroy();
|
|
210
|
+
error ? reject(error) : resolve2(value);
|
|
211
|
+
};
|
|
212
|
+
socket.on("connect", () => socket.write(`${JSON.stringify({ id, method, params })}
|
|
213
|
+
`));
|
|
214
|
+
socket.on("data", (chunk) => {
|
|
215
|
+
bytes += chunk.byteLength;
|
|
216
|
+
if (bytes > MAX_RPC_BYTES) {
|
|
217
|
+
finish(new Error("Herdr RPC response exceeded its byte bound."));
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
data = Buffer.concat([data, chunk]);
|
|
221
|
+
const newline = data.indexOf(10);
|
|
222
|
+
if (newline < 0) return;
|
|
223
|
+
let frame;
|
|
224
|
+
try {
|
|
225
|
+
frame = decodeHerdrJsonLine(data.subarray(0, newline));
|
|
226
|
+
} catch {
|
|
227
|
+
finish(new Error("Herdr RPC returned malformed JSON or UTF-8."));
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const record = frame;
|
|
231
|
+
if (record.id !== id) {
|
|
232
|
+
finish(new Error("Herdr RPC response identity mismatch."));
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (record.error) finish(new HerdrRpcError(String(record.error.message ?? "Herdr RPC failed."), id, typeof record.error.code === "string" ? record.error.code : void 0));
|
|
236
|
+
else finish(void 0, record.result);
|
|
237
|
+
});
|
|
238
|
+
socket.on("error", (error) => finish(new HerdrTransportError(error.message, { cause: error })));
|
|
239
|
+
socket.on("close", () => finish(new HerdrTransportError(`Herdr RPC '${method}' ended before a response.`)));
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
async subscribe(subscriptions, listener, onDisconnect) {
|
|
243
|
+
const socket = net.createConnection(this.#socketPath);
|
|
244
|
+
const id = String(++this.#next);
|
|
245
|
+
let buffer = Buffer.alloc(0), bytes = 0, acknowledged = false, intentional = false, notified = false, settled = false;
|
|
246
|
+
let resolveAck, rejectAck;
|
|
247
|
+
const acknowledgement = new Promise((resolve2, reject) => {
|
|
248
|
+
resolveAck = resolve2;
|
|
249
|
+
rejectAck = reject;
|
|
250
|
+
});
|
|
251
|
+
const failHandshake = (error) => {
|
|
252
|
+
if (settled) return;
|
|
253
|
+
settled = true;
|
|
254
|
+
clearTimeout(timer);
|
|
255
|
+
socket.destroy();
|
|
256
|
+
rejectAck(error);
|
|
257
|
+
};
|
|
258
|
+
const disconnect = (error) => {
|
|
259
|
+
if (intentional || notified) return;
|
|
260
|
+
notified = true;
|
|
261
|
+
if (!acknowledged) failHandshake(error);
|
|
262
|
+
else onDisconnect?.(error);
|
|
263
|
+
};
|
|
264
|
+
const timer = setTimeout(() => failHandshake(new Error("Herdr subscription acknowledgement timed out.")), this.#subscriptionAckTimeoutMs);
|
|
265
|
+
timer.unref?.();
|
|
266
|
+
socket.on("data", (chunk) => {
|
|
267
|
+
bytes += chunk.byteLength;
|
|
268
|
+
if (bytes > (acknowledged ? MAX_RPC_BYTES * 32 : MAX_RPC_BYTES)) {
|
|
269
|
+
disconnect(new Error("Herdr subscription exceeded its byte bound."));
|
|
270
|
+
socket.destroy();
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
274
|
+
for (; ; ) {
|
|
275
|
+
const i = buffer.indexOf(10);
|
|
276
|
+
if (i < 0) break;
|
|
277
|
+
const line = buffer.subarray(0, i);
|
|
278
|
+
buffer = buffer.subarray(i + 1);
|
|
279
|
+
let value;
|
|
280
|
+
try {
|
|
281
|
+
value = decodeHerdrJsonLine(line);
|
|
282
|
+
} catch {
|
|
283
|
+
disconnect(new Error("Herdr subscription emitted malformed JSON or UTF-8."));
|
|
284
|
+
socket.destroy();
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const record = value;
|
|
288
|
+
if (!acknowledged) {
|
|
289
|
+
if (record.id === id) {
|
|
290
|
+
if (record.error) {
|
|
291
|
+
failHandshake(new HerdrRpcError(String(record.error.message ?? "Herdr subscription failed."), id, typeof record.error.code === "string" ? record.error.code : void 0));
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
if (record.result?.type !== "subscription_started") {
|
|
295
|
+
failHandshake(new Error("Herdr subscription returned an invalid acknowledgement."));
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
acknowledged = true;
|
|
299
|
+
settled = true;
|
|
300
|
+
clearTimeout(timer);
|
|
301
|
+
resolveAck();
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if (record.id === "" && record.error && record.error.code === "invalid_request") {
|
|
305
|
+
failHandshake(new HerdrRpcError(String(record.error.message ?? "Herdr subscription request was invalid."), "", "invalid_request"));
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
failHandshake(new Error("Herdr subscription emitted an event before acknowledgement."));
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (record.error) {
|
|
312
|
+
disconnect(new HerdrRpcError(String(record.error.message ?? "Herdr subscription failed."), typeof record.id === "string" ? record.id : "", typeof record.error.code === "string" ? record.error.code : void 0));
|
|
313
|
+
socket.destroy();
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
listener(value);
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
socket.on("error", (error) => disconnect(error));
|
|
320
|
+
socket.on("close", () => disconnect(new Error("Herdr subscription connection was lost.")));
|
|
321
|
+
socket.on("connect", () => socket.write(`${JSON.stringify({ id, method: "events.subscribe", params: { subscriptions } })}
|
|
322
|
+
`));
|
|
323
|
+
await acknowledgement;
|
|
324
|
+
return () => {
|
|
325
|
+
intentional = true;
|
|
326
|
+
clearTimeout(timer);
|
|
327
|
+
socket.destroy();
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
async function waitForSocket(socketPath, child) {
|
|
332
|
+
for (let attempt = 0; attempt < 100; attempt++) {
|
|
333
|
+
if (child.exitCode !== null) throw new Error(`SSH StreamLocal forwarding exited with code ${child.exitCode}.`);
|
|
334
|
+
try {
|
|
335
|
+
const stat = fs2.lstatSync(socketPath);
|
|
336
|
+
if (stat.isSocket()) {
|
|
337
|
+
fs2.chmodSync(socketPath, 384);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
} catch {
|
|
341
|
+
}
|
|
342
|
+
await new Promise((resolve2) => setTimeout(resolve2, 25));
|
|
343
|
+
}
|
|
344
|
+
throw new Error("SSH StreamLocal forwarding did not create its local socket.");
|
|
345
|
+
}
|
|
346
|
+
async function terminateChild(child) {
|
|
347
|
+
const wait = (ms) => child.exitCode !== null || child.signalCode !== null ? Promise.resolve() : Promise.race([new Promise((resolve2) => child.once("exit", () => resolve2())), new Promise((resolve2) => setTimeout(resolve2, ms))]);
|
|
348
|
+
if (child.exitCode === null && child.signalCode === null) child.kill("SIGTERM");
|
|
349
|
+
await wait(1e3);
|
|
350
|
+
if (child.exitCode === null && child.signalCode === null) {
|
|
351
|
+
child.kill("SIGKILL");
|
|
352
|
+
await wait(1e3);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
function openSshSockets(machine, options) {
|
|
356
|
+
const dir = fs2.mkdtempSync(path2.join(os2.tmpdir(), "pi-subagents-herdr-"));
|
|
357
|
+
fs2.chmodSync(dir, 448);
|
|
358
|
+
const children = /* @__PURE__ */ new Set();
|
|
359
|
+
const forward = async (remotePath, name) => {
|
|
360
|
+
if (!path2.posix.isAbsolute(remotePath)) throw new Error("Remote socket path must be absolute.");
|
|
361
|
+
const local = path2.join(dir, `${name}.sock`);
|
|
362
|
+
const child = spawn(options.sshBin ?? "ssh", [...herdrSshArgs(options.env), "-N", "-o", "StreamLocalBindUnlink=yes", "-L", `${local}:${remotePath}`, machine.target], { env: hardenedSshEnv(options.env), stdio: "ignore", windowsHide: true });
|
|
363
|
+
children.add(child);
|
|
364
|
+
const stop = async () => {
|
|
365
|
+
await terminateChild(child);
|
|
366
|
+
children.delete(child);
|
|
367
|
+
try {
|
|
368
|
+
fs2.rmSync(local, { force: true });
|
|
369
|
+
} catch {
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
try {
|
|
373
|
+
await waitForSocket(local, child);
|
|
374
|
+
} catch (error) {
|
|
375
|
+
await stop();
|
|
376
|
+
throw error;
|
|
377
|
+
}
|
|
378
|
+
return { socketPath: local, close: stop };
|
|
379
|
+
};
|
|
380
|
+
return { forwardRemoteSocket: forward, close: async () => {
|
|
381
|
+
await Promise.all([...children].map(terminateChild));
|
|
382
|
+
children.clear();
|
|
383
|
+
fs2.rmSync(dir, { recursive: true, force: true });
|
|
384
|
+
} };
|
|
385
|
+
}
|
|
386
|
+
function sshMachineAccess(machine, options = {}) {
|
|
387
|
+
return { shellCommand: remoteShellCommand, run: (command, limits = {}) => runHerdrRemoteCommand(machine, command, { ...options, ...limits }), runAsync: (command, limits = {}) => runHerdrRemoteCommandAsync(machine, command, { ...options, ...limits }), openSockets: () => openSshSockets(machine, options) };
|
|
388
|
+
}
|
|
389
|
+
async function connectHerdrMachine(machine, options = {}) {
|
|
390
|
+
if (process.platform === "win32") throw new Error("Pane-native Herdr placement requires OpenSSH StreamLocal forwarding and is not supported on Windows.");
|
|
391
|
+
const endpoint = await discoverHerdrEndpoint(machine, options);
|
|
392
|
+
const sockets = sshMachineAccess(machine, options).openSockets();
|
|
393
|
+
const primary = await sockets.forwardRemoteSocket(endpoint.socket, "herdr").catch(async (error) => {
|
|
394
|
+
await sockets.close();
|
|
395
|
+
throw error;
|
|
396
|
+
});
|
|
397
|
+
return { endpoint, socketPath: primary.socketPath, client: new SocketRpcClient(primary.socketPath), forwardRemoteSocket: sockets.forwardRemoteSocket, close: sockets.close };
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export {
|
|
401
|
+
remoteShellCommand,
|
|
402
|
+
runBoundedCommand,
|
|
403
|
+
sshMachineAccess,
|
|
404
|
+
connectHerdrMachine,
|
|
405
|
+
REX_LOCAL_MACHINE,
|
|
406
|
+
REX_EXIT_GONE,
|
|
407
|
+
REX_EXIT_UNREACHABLE,
|
|
408
|
+
isRexLocalMachine,
|
|
409
|
+
REX_BUNDLE_BIN,
|
|
410
|
+
REX_CALL_TIMEOUT_MS,
|
|
411
|
+
REX_MAX_OUTPUT,
|
|
412
|
+
rexGlobalArgs,
|
|
413
|
+
resolveRexBinary,
|
|
414
|
+
rexCommandEnv,
|
|
415
|
+
resolveRexLocalPlacement
|
|
416
|
+
};
|