[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled

This commit is contained in:
2026-09-13 18:24:01 +08:00
commit 3c72efcb7f
3255 changed files with 907009 additions and 0 deletions

View File

@@ -0,0 +1,639 @@
"use strict";
const { createHash, randomUUID } = require("node:crypto");
const fs = require("node:fs");
const fsp = require("node:fs/promises");
const path = require("node:path");
const { execFile: nodeExecFile, spawn: nodeSpawn } = require("node:child_process");
const { isPathInside } = require("./paths.cjs");
const { canonicalizeCompanionResource } = require("./permissionResources.cjs");
const { PluginRpcError, RPC_ERRORS } = require("./rpcRouter.cjs");
const MAX_COMPANIONS_PER_RUNTIME = 4;
const MAX_COMPANION_PENDING = 64;
const MAX_COMPANION_STDERR_BYTES = 64 * 1024;
const COMPANION_TERMINATION_GRACE_MS = 500;
const COMPANION_CONTAINMENT_TIMEOUT_MS = 2_000;
const PROCESS_TREE_POLL_MS = 25;
function platformKey(platform = process.platform, arch = process.arch) {
return `${platform}-${arch}`;
}
function assertCompanionId(value) {
try {
return canonicalizeCompanionResource(value);
} catch {
throw new PluginRpcError(RPC_ERRORS.invalidArgument, "Plugin companion ID is invalid");
}
}
function assertHandleId(value) {
if (typeof value !== "string" || value.length < 16 || value.length > 128) {
throw new PluginRpcError(RPC_ERRORS.invalidArgument, "Plugin companion handle is invalid");
}
return value;
}
function assertOperationId(value) {
if (
typeof value !== "string"
|| value.length < 1
|| value.length > 128
|| value.includes("\0")
) {
throw new PluginRpcError(RPC_ERRORS.invalidArgument, "Plugin companion operation ID is invalid");
}
return value;
}
function assertCompanionMethod(value) {
if (
typeof value !== "string"
|| value.length < 1
|| value.length > 256
|| value.startsWith("$/")
|| !/^[A-Za-z][A-Za-z0-9_.-]*$/u.test(value)
) throw new PluginRpcError(RPC_ERRORS.invalidArgument, "Plugin companion RPC method is invalid");
return value;
}
function assertAdvancedCompanionContext(context) {
if (context?.runtimeKind !== "utility" || typeof context.manifest?.main?.node !== "string") {
throw new PluginRpcError(
RPC_ERRORS.permissionDenied,
"Plugin companions require an advanced utility runtime",
{ pluginCode: "permission_denied" },
);
}
}
function rpcIdKey(id) {
return `${typeof id}:${String(id)}`;
}
function delay(milliseconds) {
return new Promise((resolve) => setTimeout(resolve, milliseconds));
}
async function waitUntilStopped(isAlive, timeoutMs) {
const deadline = Date.now() + timeoutMs;
while (isAlive()) {
if (Date.now() >= deadline) return false;
await delay(Math.min(PROCESS_TREE_POLL_MS, Math.max(1, deadline - Date.now())));
}
return true;
}
function isPosixProcessGroupAlive(pid, kill = process.kill) {
try {
kill(-pid, 0);
return true;
} catch (error) {
if (error?.code === "ESRCH") return false;
if (error?.code === "EPERM") return true;
throw error;
}
}
function signalPosixProcessGroup(pid, signal, kill = process.kill) {
try {
kill(-pid, signal);
return true;
} catch (error) {
if (error?.code === "ESRCH") return false;
throw error;
}
}
function runTaskkill(pid, force, execFile = nodeExecFile) {
const systemRoot = process.env.SystemRoot || "C:\\Windows";
const executable = path.join(systemRoot, "System32", "taskkill.exe");
const args = ["/PID", String(pid), "/T", ...(force ? ["/F"] : [])];
return new Promise((resolve) => {
execFile(executable, args, {
windowsHide: true,
timeout: force ? COMPANION_CONTAINMENT_TIMEOUT_MS : COMPANION_TERMINATION_GRACE_MS,
}, (error) => resolve(error == null));
});
}
async function terminateDirectChild(child) {
if (child.exitCode !== null || child.signalCode !== null) return;
try { child.kill(); } catch {}
if (await waitUntilStopped(
() => child.exitCode === null && child.signalCode === null,
COMPANION_TERMINATION_GRACE_MS,
)) return;
try { child.kill("SIGKILL"); } catch {}
if (await waitUntilStopped(
() => child.exitCode === null && child.signalCode === null,
COMPANION_CONTAINMENT_TIMEOUT_MS - COMPANION_TERMINATION_GRACE_MS,
)) return;
throw new Error("Plugin companion direct process did not exit");
}
async function terminateCompanionProcessTree(child, options = {}) {
const platform = options.platform ?? process.platform;
const pid = child?.pid;
if (!Number.isSafeInteger(pid) || pid <= 0) {
await terminateDirectChild(child);
return;
}
if (platform === "win32") {
const graceful = await runTaskkill(pid, false, options.execFile);
await (options.delay ?? delay)(COMPANION_TERMINATION_GRACE_MS);
const forced = await runTaskkill(pid, true, options.execFile);
if (!graceful && !forced && child.exitCode === null && child.signalCode === null) {
throw new Error("Plugin companion process tree could not be terminated");
}
return;
}
const kill = options.kill ?? process.kill;
signalPosixProcessGroup(pid, "SIGTERM", kill);
if (await waitUntilStopped(
() => isPosixProcessGroupAlive(pid, kill),
COMPANION_TERMINATION_GRACE_MS,
)) return;
signalPosixProcessGroup(pid, "SIGKILL", kill);
if (await waitUntilStopped(
() => isPosixProcessGroupAlive(pid, kill),
COMPANION_CONTAINMENT_TIMEOUT_MS - COMPANION_TERMINATION_GRACE_MS,
)) return;
throw new Error("Plugin companion process group could not be reaped");
}
async function sha256File(filePath) {
const hash = createHash("sha256");
const input = fs.createReadStream(filePath);
for await (const chunk of input) hash.update(chunk);
return hash.digest("hex");
}
async function resolveCompanionVariant(identity, companionId, targetPlatform = platformKey()) {
const companion = identity.manifest.companionExecutables?.find(({ id }) => id === companionId);
if (!companion) throw new PluginRpcError(RPC_ERRORS.notFound, "Plugin companion is not declared");
const variant = companion.variants.find(({ platforms }) => platforms.includes(targetPlatform));
if (!variant) throw new PluginRpcError(RPC_ERRORS.unsupported, "Plugin companion has no compatible binary");
const candidate = path.resolve(identity.packageRoot, ...variant.path.split("/"));
const [realRoot, realCandidate] = await Promise.all([
fsp.realpath(identity.packageRoot),
fsp.realpath(candidate),
]);
if (!isPathInside(realRoot, realCandidate)) {
throw new PluginRpcError(RPC_ERRORS.permissionDenied, "Plugin companion escapes its package");
}
const stats = await fsp.lstat(realCandidate);
if (!stats.isFile() || stats.isSymbolicLink()) {
throw new PluginRpcError(RPC_ERRORS.failedPrecondition, "Plugin companion is not a regular file");
}
const digest = await sha256File(realCandidate);
if (digest !== variant.sha256) {
throw new PluginRpcError(RPC_ERRORS.dataLoss, "Plugin companion digest mismatch");
}
return { companion, variant, executablePath: realCandidate };
}
class CompanionRpcPeer {
constructor(options) {
this.child = options.child;
this.contract = options.contract;
this.onProtocolError = options.onProtocolError;
this.pending = new Map();
this.retiredResponseIds = new Set();
this.nextId = 0;
this.closed = false;
this.decoder = new this.contract.ContentLengthFrameDecoder();
this.child.stdout.on("data", (chunk) => this.#acceptBytes(chunk));
this.child.stdout.on("end", () => {
try { this.decoder.finish(); }
catch (error) { this.#fail(error); }
});
}
#send(value) {
if (this.closed || !this.child.stdin.writable) {
throw new PluginRpcError(RPC_ERRORS.unavailable, "Plugin companion is closed");
}
const frame = this.contract.encodeContentLengthFrame(value);
this.child.stdin.write(frame);
}
#acceptBytes(chunk) {
if (this.closed) return;
try {
for (const message of this.decoder.push(chunk)) this.#accept(message);
} catch (error) {
this.#fail(error);
}
}
#accept(message) {
if (!message || typeof message !== "object" || Array.isArray(message) || message.jsonrpc !== "2.0") {
throw new Error("Plugin companion returned a non-RPC message");
}
if (Object.hasOwn(message, "id") && (Object.hasOwn(message, "result") || Object.hasOwn(message, "error"))) {
if (Object.hasOwn(message, "result") === Object.hasOwn(message, "error")) {
throw new Error("Plugin companion response must contain exactly one result or error");
}
const key = rpcIdKey(message.id);
const pending = this.pending.get(key);
if (!pending) {
if (this.retiredResponseIds.delete(key)) return;
throw new Error("Plugin companion returned an unknown response ID");
}
this.pending.delete(key);
clearTimeout(pending.timer);
if (message.error) {
pending.reject(new PluginRpcError(
Number.isInteger(message.error.code) ? message.error.code : RPC_ERRORS.internal,
typeof message.error.message === "string" ? message.error.message.slice(0, 2_048) : "Companion request failed",
message.error.data,
));
} else pending.resolve(message.result ?? null);
return;
}
if (Object.hasOwn(message, "id")) {
this.#send({
jsonrpc: "2.0",
id: message.id,
error: { code: -32601, message: "Companion-to-host methods are not exposed" },
});
}
}
#allocateRequestId() {
for (
let attempt = 0;
attempt <= this.pending.size + this.retiredResponseIds.size;
attempt += 1
) {
const id = this.nextId;
this.nextId = this.nextId === Number.MAX_SAFE_INTEGER ? 0 : this.nextId + 1;
const key = rpcIdKey(id);
if (!this.pending.has(key) && !this.retiredResponseIds.has(key)) return id;
}
throw new PluginRpcError(RPC_ERRORS.resourceExhausted, "No companion RPC correlation ID is available");
}
#retireResponseId(key) {
this.retiredResponseIds.add(key);
while (this.retiredResponseIds.size > MAX_COMPANION_PENDING) {
this.retiredResponseIds.delete(this.retiredResponseIds.values().next().value);
}
}
request(method, params, timeoutMs) {
if (this.closed) return Promise.reject(new PluginRpcError(RPC_ERRORS.unavailable, "Plugin companion is closed"));
if (this.pending.size >= MAX_COMPANION_PENDING) {
return Promise.reject(new PluginRpcError(RPC_ERRORS.resourceExhausted, "Too many companion requests"));
}
const id = this.#allocateRequestId();
const key = rpcIdKey(id);
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
this.pending.delete(key);
this.#retireResponseId(key);
reject(new PluginRpcError(RPC_ERRORS.deadlineExceeded, `Companion request timed out: ${method}`));
}, timeoutMs);
this.pending.set(key, { resolve, reject, timer });
try {
this.#send({ jsonrpc: "2.0", id, method, ...(params === undefined ? {} : { params }) });
} catch (error) {
this.pending.delete(key);
clearTimeout(timer);
reject(error);
}
});
}
#fail(error) {
if (this.closed) return;
this.close(error);
this.onProtocolError(error);
}
close(error = new PluginRpcError(RPC_ERRORS.unavailable, "Plugin companion closed")) {
if (this.closed) return;
this.closed = true;
for (const pending of this.pending.values()) {
clearTimeout(pending.timer);
pending.reject(error);
}
this.pending.clear();
this.retiredResponseIds.clear();
try { this.child.stdin.end(); } catch {}
}
}
class PluginCompanionSupervisor {
constructor(options) {
this.paths = options.paths;
this.spawn = options.spawn ?? nodeSpawn;
this.platform = options.platform ?? process.platform;
this.arch = options.arch ?? process.arch;
this.quotaManager = options.quotaManager ?? null;
this.onContainmentFailure = options.onContainmentFailure ?? (() => {});
this.terminateProcessTree = options.terminateProcessTree
?? ((child) => terminateCompanionProcessTree(child, { platform: this.platform }));
this.handles = new Map();
this.startingCounts = new Map();
this.contractPromise = null;
this.closed = false;
}
#loadContract() {
this.contractPromise ??= import("@netcatty/plugin-contract");
return this.contractPromise;
}
validateStart(params) {
if (!params || typeof params !== "object" || Array.isArray(params)) {
throw new PluginRpcError(RPC_ERRORS.invalidArgument, "Plugin companion start parameters are invalid");
}
return { companionId: assertCompanionId(params.companionId) };
}
validateRequest(params) {
if (!params || typeof params !== "object" || Array.isArray(params)) {
throw new PluginRpcError(RPC_ERRORS.invalidArgument, "Plugin companion request parameters are invalid");
}
const timeoutMs = params.timeoutMs ?? 30_000;
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 60_000) {
throw new PluginRpcError(RPC_ERRORS.invalidArgument, "Plugin companion request timeout is invalid");
}
let credentialLeases;
let operationId;
if (params.credentialLeases !== undefined) {
if (!params.credentialLeases || typeof params.credentialLeases !== "object"
|| Array.isArray(params.credentialLeases)
|| Object.keys(params.credentialLeases).length < 1
|| Object.keys(params.credentialLeases).length > 16) {
throw new PluginRpcError(RPC_ERRORS.invalidArgument, "Plugin companion credential leases are invalid");
}
operationId = assertOperationId(params.operationId);
credentialLeases = {};
for (const [name, lease] of Object.entries(params.credentialLeases)) {
if (!/^[A-Za-z][A-Za-z0-9_.-]{0,63}$/u.test(name)
|| !lease || typeof lease !== "object" || Array.isArray(lease)
|| lease.kind !== "secret-lease"
|| typeof lease.id !== "string" || lease.id.length < 16 || lease.id.length > 256) {
throw new PluginRpcError(RPC_ERRORS.invalidArgument, "Plugin companion credential lease is invalid");
}
credentialLeases[name] = Object.freeze({ kind: "secret-lease", id: lease.id });
}
Object.freeze(credentialLeases);
}
return {
handleId: assertHandleId(params.handleId),
method: assertCompanionMethod(params.method),
...(params.params === undefined ? {} : { params: params.params }),
...(credentialLeases === undefined ? {} : { credentialLeases, operationId }),
timeoutMs,
};
}
validateStop(params) {
if (!params || typeof params !== "object" || Array.isArray(params)) {
throw new PluginRpcError(RPC_ERRORS.invalidArgument, "Plugin companion stop parameters are invalid");
}
return { handleId: assertHandleId(params.handleId) };
}
describeStartAuthorization(params, context) {
assertAdvancedCompanionContext(context);
const value = this.validateStart(params);
return {
permission: "companion.execute",
resources: [value.companionId],
reason: `Launch companion ${value.companionId}`,
operationId: `companion.start:${value.companionId}`,
};
}
describeHandleAuthorization(params, context) {
const handleId = assertHandleId(params?.handleId);
const record = this.#ownedRecord(handleId, context);
return {
permission: "companion.execute",
resources: [record.companionId],
reason: `Use companion ${record.companionId}`,
operationId: `companion:${handleId}`,
};
}
#reserveStart(runtimeId) {
const activeCount = [...this.handles.values()].filter((record) => (
record.runtimeId === runtimeId
)).length;
const startingCount = this.startingCounts.get(runtimeId) ?? 0;
if (activeCount + startingCount >= MAX_COMPANIONS_PER_RUNTIME) {
throw new PluginRpcError(RPC_ERRORS.resourceExhausted, "Plugin companion process quota exceeded");
}
this.startingCounts.set(runtimeId, startingCount + 1);
let released = false;
return () => {
if (released) return;
released = true;
const remaining = (this.startingCounts.get(runtimeId) ?? 1) - 1;
if (remaining <= 0) this.startingCounts.delete(runtimeId);
else this.startingCounts.set(runtimeId, remaining);
};
}
async start(params, context) {
if (this.closed) throw new PluginRpcError(RPC_ERRORS.unavailable, "Plugin companion supervisor is closed");
assertAdvancedCompanionContext(context);
const { companionId } = this.validateStart(params);
const releaseReservation = this.#reserveStart(context.runtimeId);
return this.#startReserved(companionId, context).finally(releaseReservation);
}
async #startReserved(companionId, context) {
const dataDirectory = path.join(this.paths.data, context.pluginId);
await fsp.mkdir(dataDirectory, { recursive: true, mode: 0o700 });
const contract = await this.#loadContract();
await context.assertActive();
const resolved = await resolveCompanionVariant(
context,
companionId,
platformKey(this.platform, this.arch),
);
context.signal?.throwIfAborted();
const child = this.spawn(resolved.executablePath, [], {
cwd: dataDirectory,
env: {
LANG: process.env.LANG ?? "C.UTF-8",
LC_ALL: process.env.LC_ALL ?? "",
},
shell: false,
stdio: ["pipe", "pipe", "pipe"],
windowsHide: true,
detached: this.platform !== "win32",
});
const handleId = randomUUID();
const record = {
handleId,
companionId,
pluginId: context.pluginId,
pluginVersion: context.pluginVersion,
runtimeId: context.runtimeId,
runtimeKind: context.runtimeKind,
securityPrincipal: context.securityPrincipal,
child,
peer: null,
quotaResourceId: `${context.runtimeId}\0companion:${handleId}`,
stopping: false,
stderrBytes: 0,
};
const peer = new CompanionRpcPeer({
child,
contract,
onProtocolError: () => { void this.#stopRecord(record); },
});
record.peer = peer;
this.handles.set(handleId, record);
child.stderr.on("data", (chunk) => {
record.stderrBytes += chunk.byteLength;
if (record.stderrBytes > MAX_COMPANION_STDERR_BYTES) void this.#stopRecord(record);
});
child.once("exit", () => {
peer.close();
if (!record.stopping) void this.#stopRecord(record).catch(() => {});
});
try {
await new Promise((resolve, reject) => {
const onSpawn = () => {
child.removeListener("error", onError);
resolve();
};
const onError = (error) => {
child.removeListener("spawn", onSpawn);
reject(error);
};
child.once("spawn", onSpawn);
child.once("error", onError);
});
} catch (error) {
peer.close();
this.handles.delete(handleId);
throw new PluginRpcError(
RPC_ERRORS.unavailable,
`Plugin companion failed to start: ${error?.message ?? String(error)}`,
);
}
this.quotaManager?.trackProcess?.(
record.quotaResourceId,
Object.freeze({
pluginId: record.pluginId,
pluginVersion: record.pluginVersion,
runtimeId: record.runtimeId,
runtimeKind: record.runtimeKind,
securityPrincipal: record.securityPrincipal,
}),
{ getProcessId: () => child.pid },
);
child.once("error", () => { void this.#stopRecord(record); });
try {
await context.assertActive();
} catch (error) {
await this.#stopRecord(record);
throw error;
}
return { handleId };
}
#ownedRecord(handleId, context) {
const record = this.handles.get(handleId);
if (!record || record.pluginId !== context.pluginId || record.runtimeId !== context.runtimeId) {
throw new PluginRpcError(RPC_ERRORS.notFound, "Plugin companion handle was not found");
}
return record;
}
async request(params, context) {
const value = this.validateRequest(params);
const record = this.#ownedRecord(value.handleId, context);
this.quotaManager?.chargeBytes(
context.runtimeId,
"companion",
Buffer.byteLength(JSON.stringify(value.params ?? null)),
);
const result = await record.peer.request(value.method, value.params, value.timeoutMs);
this.quotaManager?.chargeBytes(context.runtimeId, "companion", Buffer.byteLength(JSON.stringify(result)));
await context.assertActive();
return result;
}
async stop(params, context) {
const value = this.validateStop(params);
await this.#stopRecord(this.#ownedRecord(value.handleId, context));
return null;
}
async #stopRecord(record) {
if (record.stopping) return record.stopPromise;
record.stopping = true;
record.stopPromise = Promise.resolve().then(() => this.terminateProcessTree(record.child)).then(() => {
record.peer.close();
this.quotaManager?.releaseProcess?.(record.quotaResourceId);
this.handles.delete(record.handleId);
}, async (error) => {
const containmentError = new PluginRpcError(
RPC_ERRORS.failedPrecondition,
`Plugin companion could not be reaped: ${record.companionId}`,
{ cause: error?.message ?? String(error) },
);
record.peer.close(containmentError);
record.stopping = false;
record.stopPromise = null;
await this.onContainmentFailure(Object.freeze({
pluginId: record.pluginId,
pluginVersion: record.pluginVersion,
runtimeId: record.runtimeId,
runtimeKind: record.runtimeKind,
securityPrincipal: record.securityPrincipal,
}), containmentError);
throw containmentError;
});
return record.stopPromise;
}
async releaseRuntime(runtimeId) {
const results = await Promise.allSettled([...this.handles.values()]
.filter((record) => record.runtimeId === runtimeId)
.map((record) => this.#stopRecord(record)));
const failures = results
.filter((result) => result.status === "rejected")
.map((result) => result.reason);
if (failures.length === 1) throw failures[0];
if (failures.length > 1) {
throw new AggregateError(failures, `Plugin companion cleanup failed for runtime: ${runtimeId}`);
}
}
async shutdown() {
this.closed = true;
await Promise.allSettled([...this.handles.values()].map((record) => this.#stopRecord(record)));
this.startingCounts.clear();
}
}
module.exports = {
COMPANION_CONTAINMENT_TIMEOUT_MS,
COMPANION_TERMINATION_GRACE_MS,
CompanionRpcPeer,
MAX_COMPANIONS_PER_RUNTIME,
MAX_COMPANION_PENDING,
PluginCompanionSupervisor,
assertCompanionId,
assertCompanionMethod,
platformKey,
resolveCompanionVariant,
rpcIdKey,
isPosixProcessGroupAlive,
signalPosixProcessGroup,
sha256File,
terminateCompanionProcessTree,
};