[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,259 @@
"use strict";
const { z } = require("zod");
const { listMcpTools } = require("./toolSurfaces.cjs");
const TERMINAL_EXECUTE_TOOLS = new Set(["terminal_execute"]);
const TERMINAL_START_TOOLS = new Set(["terminal_start"]);
const TERMINAL_POLL_TOOLS = new Set(["terminal_poll"]);
const TERMINAL_STOP_TOOLS = new Set(["terminal_stop"]);
const SESSION_CLOSE_TOOLS = new Set(["session_close"]);
const CONTEXT_TOOLS = new Set(["get_environment"]);
const ATTACHMENT_LIST_TOOLS = new Set(["list_attachments"]);
const ATTACHMENT_READ_TOOLS = new Set(["read_attachment"]);
const SFTP_WRITE_TOOLS = new Set([
"sftp_write_file",
"sftp_mkdir",
"sftp_delete",
"sftp_rename",
"sftp_chmod",
]);
function buildZodField(field) {
let schema = field.type === "number" ? z.number() : z.string();
if (field.type === "number") {
schema = schema.int().min(0);
}
if (field.description) {
schema = schema.describe(field.description);
}
if (field.optional) {
schema = schema.optional();
}
return schema;
}
function buildZodShapeObject(inputShape) {
const shape = {};
for (const [key, field] of Object.entries(inputShape || {})) {
shape[key] = buildZodField(field);
}
return shape;
}
function buildZodSchema(inputShape) {
return z.object(buildZodShapeObject(inputShape));
}
function isEmptyMcpInputShape(inputShape) {
return Object.keys(inputShape || {}).length === 0;
}
function formatRpcError(result) {
if (result?.error) return `Error: ${result.error}`;
if (result?.code) return `Error: Operation failed (${result.code})`;
return "Error: Operation failed";
}
/**
* True when the RPC result carries command-run evidence (output and/or exit
* code). Non-zero exits from PTY exec set ok:false without an error string —
* that is still a completed command, not an operational failure.
*/
function hasTerminalExecuteEvidence(result) {
if (!result || typeof result !== "object") return false;
if (typeof result.stdout === "string" && result.stdout.length > 0) return true;
if (typeof result.stderr === "string" && result.stderr.length > 0) return true;
return result.exitCode != null;
}
function formatTerminalExecuteResult(result) {
const parts = [];
if (result?.stdout) parts.push(result.stdout);
if (result?.stderr) parts.push(`[stderr] ${result.stderr}`);
if (result?.exitCode != null) {
parts.push(`[exit code: ${result.exitCode}]`);
}
if (result?.error) parts.push(`[error] ${result.error}`);
return parts.join("\n");
}
// Align with Catty executor wording when a command ran but produced no text.
const TERMINAL_EXECUTE_NO_OUTPUT_TEXT = "Command completed (no output)";
/**
* Format terminal_execute for MCP clients.
* Match Catty semantics: when the command actually ran, always surface
* stdout/stderr/exitCode so the model can judge the failure. Only pure
* operational failures (session missing, blocked, etc.) become a bare
* "Error: …" payload. See issue #2718.
*
* Successful empty results (serial/network raw PTY often returns ok:true with
* empty stdout/stderr and exitCode:null) must NOT fall back to
* "Error: Operation failed" — Codex P2 on PR #2724.
*/
function formatTerminalExecuteMcpResponse(result) {
if (!result || typeof result !== "object") {
return { content: [{ type: "text", text: formatRpcError(result) }], isError: true };
}
// Operational failure with no command evidence (session not found, busy, blocked…).
if (result.ok === false && !hasTerminalExecuteEvidence(result)) {
return { content: [{ type: "text", text: formatRpcError(result) }], isError: true };
}
// Empty successful payloads (serial/raw PTY): neutral text.
// Never fall back to formatRpcError here — that mislabels silent successes (#2724 P2).
// When result.error is set, formatTerminalExecuteResult already includes `[error] …`.
const text = formatTerminalExecuteResult(result) || TERMINAL_EXECUTE_NO_OUTPUT_TEXT;
// Mark isError only for operational/runtime failures that include an error
// string (timeout, cancel, stream lost). Non-zero exit alone is success of
// the tool call with a failed command — same as Catty ok:true + exitCode.
if (result.ok === false && result.error) {
return { content: [{ type: "text", text }], isError: true };
}
return { content: [{ type: "text", text }] };
}
function createToolHandler(toolDef, deps) {
const {
rpcCall,
scopeParams,
guardWriteOperation,
catalogDescription,
} = deps;
const { mcpTool, rpcMethod, description, inputShape, policy } = toolDef;
return async (args) => {
if (TERMINAL_EXECUTE_TOOLS.has(mcpTool) || TERMINAL_START_TOOLS.has(mcpTool)) {
const { sessionId, command } = args;
const guardErr = guardWriteOperation(command, { skipBlocklist: true });
if (guardErr) {
return { content: [{ type: "text", text: `Error: ${guardErr}` }], isError: true };
}
const result = await rpcCall(rpcMethod, { ...scopeParams, sessionId, command });
if (TERMINAL_START_TOOLS.has(mcpTool)) {
if (!result?.ok) {
return { content: [{ type: "text", text: formatRpcError(result) }], isError: true };
}
return {
content: [{
type: "text",
text: JSON.stringify({
jobId: result.jobId,
sessionId: result.sessionId,
status: result.status,
startedAt: result.startedAt,
outputMode: result.outputMode,
recommendedPollIntervalMs: result.recommendedPollIntervalMs,
}, null, 2),
}],
};
}
return formatTerminalExecuteMcpResponse(result);
}
if (TERMINAL_POLL_TOOLS.has(mcpTool) || TERMINAL_STOP_TOOLS.has(mcpTool)) {
const params = TERMINAL_POLL_TOOLS.has(mcpTool)
? { ...scopeParams, jobId: args.jobId, offset: args.offset || 0 }
: { ...scopeParams, jobId: args.jobId };
const result = await rpcCall(rpcMethod, params);
if (!result.ok) {
return { content: [{ type: "text", text: formatRpcError(result) }], isError: true };
}
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
if (SESSION_CLOSE_TOOLS.has(mcpTool)) {
const result = await rpcCall(rpcMethod, { ...scopeParams, sessionId: args.sessionId });
if (!result.ok) {
return { content: [{ type: "text", text: formatRpcError(result) }], isError: true };
}
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
if (CONTEXT_TOOLS.has(mcpTool)) {
const ctx = await rpcCall(rpcMethod, scopeParams);
return { content: [{ type: "text", text: JSON.stringify(ctx, null, 2) }] };
}
if (ATTACHMENT_LIST_TOOLS.has(mcpTool)) {
const result = await rpcCall(rpcMethod, scopeParams);
if (!result.ok) {
return {
content: [{ type: "text", text: formatRpcError(result) }],
isError: true,
};
}
return {
content: [{ type: "text", text: JSON.stringify(result.attachments || [], null, 2) }],
};
}
if (ATTACHMENT_READ_TOOLS.has(mcpTool)) {
const { filePath, filename } = args;
const result = await rpcCall(rpcMethod, { ...scopeParams, filePath, filename });
if (!result.ok) {
return {
content: [{ type: "text", text: formatRpcError(result) }],
isError: true,
};
}
const payload = {
filename: result.filename,
mediaType: result.mediaType,
filePath: result.filePath,
sizeBytes: result.sizeBytes,
...(result.text != null ? { text: result.text } : { base64Data: result.base64Data }),
};
return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
}
if (policy?.write) {
const guardErr = guardWriteOperation("", { skipBlocklist: true });
if (guardErr) {
return { content: [{ type: "text", text: `Error: ${guardErr}` }], isError: true };
}
}
const result = await rpcCall(rpcMethod, { ...scopeParams, ...args });
if (result && typeof result === "object" && result.ok === false) {
return {
content: [{ type: "text", text: formatRpcError(result) }],
isError: true,
};
}
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
};
}
function registerMcpTools(server, deps) {
const tools = listMcpTools();
for (const toolDef of tools) {
if (!toolDef.mcpTool || !toolDef.rpcMethod) continue;
const handler = createToolHandler(toolDef, deps);
const toolDescription = deps.catalogDescription(toolDef.mcpTool, toolDef.description);
// Empty Zod objects reject omitted `arguments`. MCP allows omitting them
// for no-arg tools; skip the schema so both omitted and {} are valid.
if (isEmptyMcpInputShape(toolDef.inputShape)) {
server.tool(toolDef.mcpTool, toolDescription, async () => handler({}));
continue;
}
server.tool(toolDef.mcpTool, toolDescription, buildZodShapeObject(toolDef.inputShape), handler);
}
return tools.length;
}
module.exports = {
buildZodSchema,
buildZodShapeObject,
isEmptyMcpInputShape,
formatRpcError,
formatTerminalExecuteResult,
formatTerminalExecuteMcpResponse,
hasTerminalExecuteEvidence,
registerMcpTools,
SFTP_WRITE_TOOLS,
};