totemlayer 0.1.96 → 0.1.115
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/README.md +48 -6
- package/bin/automatic-runtime.mjs +2 -1
- package/bin/cloud-supervisor.mjs +44 -0
- package/bin/codex-mission-bridge.mjs +6 -2
- package/bin/configured-native-installation.mjs +57 -0
- package/bin/connected-agent-service.mjs +5 -1
- package/bin/device-login.mjs +3 -1
- package/bin/inbox-queue.mjs +8 -5
- package/bin/mcp.mjs +18 -5
- package/bin/mission-agent-service.mjs +13 -4
- package/bin/native/darwin-lifetime +0 -0
- package/bin/native/darwin-lifetime.json +1 -1
- package/bin/native-claude-login.mjs +131 -0
- package/bin/native-claude-receiver.mjs +272 -0
- package/bin/native-cli-process.py +105 -0
- package/bin/native-harness-environment.mjs +20 -0
- package/bin/native-hermes-bridge.mjs +99 -0
- package/bin/native-hermes-installation.mjs +9 -0
- package/bin/native-hermes-mcp.mjs +107 -0
- package/bin/native-hermes-mcp.py +205 -0
- package/bin/native-hermes-metadata.py +40 -0
- package/bin/native-hermes-process.py +62 -0
- package/bin/native-hermes-receiver.mjs +28 -0
- package/bin/native-inbox-installation.mjs +144 -0
- package/bin/native-inbox-receiver.mjs +133 -0
- package/bin/native-kilo-installation.mjs +4 -0
- package/bin/native-kilo-receiver.mjs +1 -0
- package/bin/native-mcp-source.mjs +244 -0
- package/bin/native-opencode-bridge.mjs +108 -0
- package/bin/native-opencode-installation.mjs +15 -0
- package/bin/native-opencode-mcp.mjs +112 -0
- package/bin/native-opencode-receiver.mjs +22 -0
- package/bin/native-pi-bridge.mjs +170 -0
- package/bin/native-pi-installation.mjs +9 -0
- package/bin/native-pi-mcp.mjs +139 -0
- package/bin/native-pi-receiver.mjs +28 -0
- package/bin/network-retry.mjs +9 -1
- package/bin/platform-client.mjs +2 -5
- package/bin/runtime-installation.mjs +9 -4
- package/bin/sdk/errors.d.ts +1 -1
- package/bin/sdk/errors.js +1 -1
- package/bin/sdk/listener.d.ts +1 -0
- package/bin/sdk/listener.js +1 -1
- package/bin/sdk/transport.js +1 -1
- package/bin/setup-assets/api-manifest.json +1 -1
- package/bin/setup-assets/manifest.json +1 -1
- package/bin/setup-assets/setup.backup.md +2 -2
- package/bin/setup-assets/setup.md +2 -2
- package/bin/startup-diagnostics.mjs +19 -2
- package/bin/strict-installation.mjs +29 -3
- package/bin/supervisor-watchdog.mjs +44 -0
- package/bin/totem.mjs +6 -0
- package/bin/windows-private-path.mjs +227 -32
- package/package.json +2 -2
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
+
import { realpath } from "node:fs/promises";
|
|
4
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
5
|
+
import { createInterface } from "node:readline";
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
|
+
import { localClientEnvironment } from "./platform-client.mjs";
|
|
8
|
+
import { installNativeInboxReceiver } from "./native-inbox-installation.mjs";
|
|
9
|
+
import { runNativeInboxReceiver } from "./native-inbox-receiver.mjs";
|
|
10
|
+
import { unwrapNativeMcp } from "./native-mcp-source.mjs";
|
|
11
|
+
import { privateRead } from "./runtime-installation.mjs";
|
|
12
|
+
import { acquireSupervisorLock } from "./runtime-lock.mjs";
|
|
13
|
+
|
|
14
|
+
const fail = suffix => Object.assign(new Error(suffix), { code: "NATIVE_CLAUDE_" + suffix });
|
|
15
|
+
const digest = value => createHash("sha256").update(value).digest("hex");
|
|
16
|
+
const absolute = value => typeof value === "string" && isAbsolute(value) && !/[\0\r\n]/.test(value);
|
|
17
|
+
const uuid = /^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i;
|
|
18
|
+
const hash = /^[a-f0-9]{64}$/;
|
|
19
|
+
const watchdog = fileURLToPath(new URL("./native-cli-process.py", import.meta.url));
|
|
20
|
+
const checkers = ["totem_get_identity", "totem_start_listener", "totem_listen",
|
|
21
|
+
"totem_list_messages", "totem_list_chat_commands", "totem_get_chat", "totem_get_conversation", "totem_get_activity"];
|
|
22
|
+
const peerTools = ["totem_get_identity", "totem_list_messages", "totem_get_conversation",
|
|
23
|
+
"totem_get_message", "totem_reply_to_message", "totem_acknowledge_message"];
|
|
24
|
+
const ownerTools = ["totem_get_identity", "totem_get_chat", "totem_claim_chat_command",
|
|
25
|
+
"totem_renew_chat_command", "totem_complete_chat_command"];
|
|
26
|
+
|
|
27
|
+
function nativeEnvironment(environment) {
|
|
28
|
+
const env = localClientEnvironment(environment, {
|
|
29
|
+
TERM: "dumb", DO_NOT_TRACK: "1", DISABLE_AUTOUPDATER: "1",
|
|
30
|
+
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1", CLAUDE_CODE_DISABLE_CLAUDE_MDS: "1",
|
|
31
|
+
CLAUDE_CODE_DISABLE_AUTO_MEMORY: "1", CLAUDE_CODE_DISABLE_ORG_MEMORY: "1",
|
|
32
|
+
CLAUDE_CODE_DISABLE_CLAUDE_CODE_SKILL: "1", CLAUDE_CODE_DISABLE_CLAUDE_API_SKILL: "1",
|
|
33
|
+
});
|
|
34
|
+
for (const key of Object.keys(env))
|
|
35
|
+
if (/^(TOTEM|AGENTTRUST)_|^(OPENAI_API_KEY|ANTHROPIC_API_KEY|CLAUDE_CODE_OAUTH_TOKEN|CLAUDE_CONFIG_DIR)$/i.test(key)) delete env[key];
|
|
36
|
+
return env;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function inspectClaudeMcp(binding) {
|
|
40
|
+
if (!absolute(binding?.configPath) || !absolute(binding.cwd) || !uuid.test(binding.agentId ?? "") ||
|
|
41
|
+
!["PRODUCTION", "SANDBOX"].includes(binding.environment) ||
|
|
42
|
+
!/^[a-zA-Z0-9_-]{1,64}$/.test(binding.serverName ?? "") || !hash.test(binding.serverFingerprint ?? ""))
|
|
43
|
+
throw fail("MCP_BINDING_INVALID");
|
|
44
|
+
const config = await privateRead(binding.configPath);
|
|
45
|
+
const entries = Object.entries(config.mcpServers ?? {});
|
|
46
|
+
const [name, server] = entries[0] ?? [];
|
|
47
|
+
if (entries.length !== 1 || name !== binding.serverName ||
|
|
48
|
+
!absolute(server?.command) || !Array.isArray(server.args) ||
|
|
49
|
+
server.args.some(arg => typeof arg !== "string" || /[\0\r\n]|tn_(live|test)_|Bearer\s/i.test(arg)) ||
|
|
50
|
+
(server.type && server.type !== "stdio") ||
|
|
51
|
+
Object.entries(server.env ?? {}).some(([key, value]) =>
|
|
52
|
+
!/^(TOTEM_CONFIG_DIR|TOTEM_BASE_URL|TOTEM_NETWORK_API_KEY_FILE|TOTEM_EXPECTED_AGENT_ID|TOTEM_EXPECTED_ENVIRONMENT|TOTEM_AUTO_START|TOTEM_AUTO_UPDATE|HOME|PATH)$/.test(key) ||
|
|
53
|
+
typeof value !== "string" || /[\0\r\n]|tn_(live|test)_|Bearer\s/i.test(value)) ||
|
|
54
|
+
server.env?.TOTEM_AUTO_START !== "0" || server.env?.TOTEM_AUTO_UPDATE !== "0" ||
|
|
55
|
+
(server.env.TOTEM_EXPECTED_AGENT_ID && server.env.TOTEM_EXPECTED_AGENT_ID !== binding.agentId) ||
|
|
56
|
+
(server.env.TOTEM_EXPECTED_ENVIRONMENT && server.env.TOTEM_EXPECTED_ENVIRONMENT !== binding.environment) ||
|
|
57
|
+
digest(JSON.stringify(config)) !== binding.serverFingerprint)
|
|
58
|
+
throw fail("MCP_CONFIG_CHANGED");
|
|
59
|
+
return server;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function createClaudeNativeBridge(binding, options = {}) {
|
|
63
|
+
if (binding?.kind !== "claude" || binding.dedicated !== true || binding.provider !== "firstParty" ||
|
|
64
|
+
!uuid.test(binding.sessionId ?? "") || !hash.test(binding.accountFingerprint ?? "") ||
|
|
65
|
+
!/^claude-[a-z0-9.-]{1,95}$/.test(binding.model ?? "") || !/^2\.\d+\.\d+$/.test(binding.nativeVersion ?? "") ||
|
|
66
|
+
![binding.binary, binding.python, binding.cwd].every(absolute) || binding.permissionMode !== "dontAsk" ||
|
|
67
|
+
binding.mcp?.cwd !== binding.cwd) throw fail("BINDING_INVALID");
|
|
68
|
+
const saved = structuredClone(binding);
|
|
69
|
+
const env = nativeEnvironment(options.env ?? process.env);
|
|
70
|
+
let release, active, controller, closed = false;
|
|
71
|
+
|
|
72
|
+
async function invoke(prompt, purpose, workIds, signal, timeoutMs) {
|
|
73
|
+
await inspectClaudeMcp(saved.mcp);
|
|
74
|
+
const tools = purpose === "ownerChat" ? ownerTools : peerTools;
|
|
75
|
+
const args = ["--print", "--input-format", "stream-json", "--output-format", "stream-json", "--verbose",
|
|
76
|
+
"--resume", saved.sessionId, "--model", saved.model, "--strict-mcp-config",
|
|
77
|
+
"--permission-mode", saved.permissionMode, "--permission-prompts", "none", "--setting-sources", "user",
|
|
78
|
+
"--settings", '{"disableAllHooks":true}', "--tools", "ToolSearch", "--allowedTools",
|
|
79
|
+
["ToolSearch", ...tools.map(tool => "mcp__" + saved.mcp.serverName + "__" + tool)].join(",")];
|
|
80
|
+
if (prompt) args.push("--mcp-config", JSON.stringify({ mcpServers: { [saved.mcp.serverName]: {
|
|
81
|
+
command: process.execPath, args: [fileURLToPath(import.meta.url), "--mcp-filter",
|
|
82
|
+
JSON.stringify(saved.mcp), purpose, JSON.stringify(workIds)],
|
|
83
|
+
} } }));
|
|
84
|
+
const requestId = randomUUID();
|
|
85
|
+
const send = value => child.stdin.write(JSON.stringify(value) + "\n");
|
|
86
|
+
const endInput = () => child.stdin.write("\0close\n");
|
|
87
|
+
const deadline = AbortSignal.any([signal, AbortSignal.timeout(timeoutMs)]);
|
|
88
|
+
if (deadline.aborted) throw fail("TURN_CANCELLED");
|
|
89
|
+
const child = (options.spawn ?? spawn)(saved.python, ["-I", watchdog, saved.binary, ...args], {
|
|
90
|
+
cwd: saved.cwd, env, shell: false, stdio: ["pipe", "pipe", "ignore"], windowsHide: true,
|
|
91
|
+
});
|
|
92
|
+
const lines = createInterface({ input: child.stdout, crlfDelay: Infinity });
|
|
93
|
+
let initialized = false, confirmed = false, completed = false, error;
|
|
94
|
+
function stop(failure) {
|
|
95
|
+
error ??= failure;
|
|
96
|
+
child.stdin.end();
|
|
97
|
+
}
|
|
98
|
+
const cancel = () => stop(fail("TURN_CANCELLED"));
|
|
99
|
+
deadline.addEventListener("abort", cancel, { once: true });
|
|
100
|
+
try {
|
|
101
|
+
await new Promise((resolve, reject) => {
|
|
102
|
+
child.once("error", () => stop(fail("UNAVAILABLE")));
|
|
103
|
+
child.stdin.on("error", () => stop(fail("PROTOCOL_FAILED")));
|
|
104
|
+
child.once("close", code => {
|
|
105
|
+
if (error || code !== 0 || !initialized || (prompt && !completed))
|
|
106
|
+
reject(error ?? fail(!initialized ? "SESSION_CHANGED" : "TURN_FAILED"));
|
|
107
|
+
else resolve();
|
|
108
|
+
});
|
|
109
|
+
lines.on("line", line => {
|
|
110
|
+
if (error) return;
|
|
111
|
+
try {
|
|
112
|
+
if (Buffer.byteLength(line) > 2 * 1024 * 1024) throw fail("PROTOCOL_FAILED");
|
|
113
|
+
const event = JSON.parse(line);
|
|
114
|
+
if (event.session_id !== undefined && event.session_id !== saved.sessionId) throw fail("SESSION_CHANGED");
|
|
115
|
+
if (event.type === "control_response") {
|
|
116
|
+
const response = event.response;
|
|
117
|
+
if (initialized || response?.request_id !== requestId || response.subtype !== "success") throw fail("PROTOCOL_FAILED");
|
|
118
|
+
const data = response.response;
|
|
119
|
+
if (data?.claude_code_version !== saved.nativeVersion) throw fail("VERSION_CHANGED");
|
|
120
|
+
const email = data?.account?.email;
|
|
121
|
+
if (data.account?.apiProvider !== saved.provider || typeof email !== "string" ||
|
|
122
|
+
digest("claude:" + email.trim().toLowerCase()) !== saved.accountFingerprint) throw fail("ACCOUNT_CHANGED");
|
|
123
|
+
if (data.current_permission_mode !== saved.permissionMode) throw fail("POLICY_CHANGED");
|
|
124
|
+
initialized = true;
|
|
125
|
+
if (!prompt) endInput();
|
|
126
|
+
else send({ type: "user", session_id: saved.sessionId, parent_tool_use_id: null, message: { role: "user", content: prompt } });
|
|
127
|
+
} else if (event.type === "system" && event.subtype === "init") {
|
|
128
|
+
if (!initialized || event.session_id !== saved.sessionId) throw fail("SESSION_CHANGED");
|
|
129
|
+
if (event.model !== saved.model) throw fail("MODEL_CHANGED");
|
|
130
|
+
if (event.permissionMode !== saved.permissionMode) throw fail("POLICY_CHANGED");
|
|
131
|
+
confirmed = true;
|
|
132
|
+
} else if (event.type === "control_request") throw fail("INTERACTION_REQUIRED");
|
|
133
|
+
else if (event.type === "result") {
|
|
134
|
+
if (!initialized && !prompt) throw fail("SESSION_CHANGED");
|
|
135
|
+
if (!confirmed || event.session_id !== saved.sessionId || event.is_error !== false) throw fail("TURN_FAILED");
|
|
136
|
+
completed = true;
|
|
137
|
+
endInput();
|
|
138
|
+
}
|
|
139
|
+
} catch (cause) { stop(cause.code?.startsWith("NATIVE_CLAUDE_") ? cause : fail("PROTOCOL_FAILED")); }
|
|
140
|
+
});
|
|
141
|
+
if (deadline.aborted) cancel();
|
|
142
|
+
else send({ type: "control_request", request_id: requestId, request: { subtype: "initialize" } });
|
|
143
|
+
});
|
|
144
|
+
} finally {
|
|
145
|
+
deadline.removeEventListener("abort", cancel);
|
|
146
|
+
lines.close();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function execute(prompt, { purpose, workIds, signal, timeoutMs = 10000 } = {}) {
|
|
151
|
+
if (closed || active) throw fail("BUSY");
|
|
152
|
+
controller = new AbortController();
|
|
153
|
+
active = invoke(prompt, purpose, workIds, signal ? AbortSignal.any([controller.signal, signal]) : controller.signal, timeoutMs)
|
|
154
|
+
.finally(() => { active = undefined; });
|
|
155
|
+
return active;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
async open() {
|
|
160
|
+
if (closed || release || await realpath(saved.cwd) !== saved.cwd) throw fail("BINDING_INVALID");
|
|
161
|
+
release = await acquireSupervisorLock(join(saved.cwd, ".totem-claude-" + saved.sessionId));
|
|
162
|
+
try { await execute(); }
|
|
163
|
+
catch (error) { await release(); release = undefined; throw error; }
|
|
164
|
+
},
|
|
165
|
+
run(prompt, turn) {
|
|
166
|
+
if (!release || typeof prompt !== "string" || !prompt.trim() || Buffer.byteLength(prompt) > 128 * 1024 ||
|
|
167
|
+
!validWork(turn?.purpose, turn?.workIds))
|
|
168
|
+
throw fail("TURN_INVALID");
|
|
169
|
+
return execute(prompt, turn);
|
|
170
|
+
},
|
|
171
|
+
async close() {
|
|
172
|
+
closed = true;
|
|
173
|
+
controller?.abort();
|
|
174
|
+
await active?.catch(() => {});
|
|
175
|
+
await release?.();
|
|
176
|
+
release = undefined;
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function connectMcp(binding, options, allowed) {
|
|
182
|
+
const server = await inspectClaudeMcp(binding);
|
|
183
|
+
const [{ Client }, { StdioClientTransport }] = await Promise.all([
|
|
184
|
+
import("@modelcontextprotocol/client"), import("@modelcontextprotocol/client/stdio"),
|
|
185
|
+
]);
|
|
186
|
+
const client = new Client({ name: "totem-native-claude-checker", version: "1" });
|
|
187
|
+
const transport = new StdioClientTransport({
|
|
188
|
+
command: server.command, args: server.args, env: { ...nativeEnvironment(options.env ?? process.env), ...server.env },
|
|
189
|
+
cwd: binding.cwd, stderr: "ignore",
|
|
190
|
+
});
|
|
191
|
+
try {
|
|
192
|
+
await client.connect(transport, { signal: options.signal, timeout: 10000 });
|
|
193
|
+
const { tools } = await client.listTools({}, { signal: options.signal, timeout: 10000 });
|
|
194
|
+
return {
|
|
195
|
+
nativeIncoming: false,
|
|
196
|
+
listTools: () => tools.filter(tool => allowed.includes(tool.name)),
|
|
197
|
+
async call(name, args, { signal } = {}) {
|
|
198
|
+
if (!allowed.includes(name)) throw Object.assign(new Error("MCP_TOOL_UNAVAILABLE"), { code: "MCP_TOOL_UNAVAILABLE" });
|
|
199
|
+
await inspectClaudeMcp(binding);
|
|
200
|
+
return client.callTool({ name, arguments: args }, undefined, { signal, timeout: 10000 });
|
|
201
|
+
},
|
|
202
|
+
close: () => client.close(),
|
|
203
|
+
};
|
|
204
|
+
} catch (error) {
|
|
205
|
+
await client.close().catch(() => {});
|
|
206
|
+
throw error;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export const createClaudeMcpSurface = (binding, options = {}) => connectMcp(binding, options, checkers);
|
|
211
|
+
|
|
212
|
+
function validWork(purpose, ids) {
|
|
213
|
+
return ["peer", "ownerChat"].includes(purpose) && Array.isArray(ids) && ids.length > 0 &&
|
|
214
|
+
ids.length <= 16 && ids.every(id => typeof id === "string" && uuid.test(id));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function serveModelMcp(binding, purpose, workIds) {
|
|
218
|
+
if (!validWork(purpose, workIds)) throw fail("MCP_PURPOSE_INVALID");
|
|
219
|
+
const [{ Server }, { StdioServerTransport }] = await Promise.all([
|
|
220
|
+
import("@modelcontextprotocol/server"), import("@modelcontextprotocol/server/stdio"),
|
|
221
|
+
]);
|
|
222
|
+
const upstream = await connectMcp(binding, {}, purpose === "ownerChat" ? ownerTools : peerTools);
|
|
223
|
+
try {
|
|
224
|
+
const identity = unwrapNativeMcp(await upstream.call("totem_get_identity", {}));
|
|
225
|
+
if (identity.id !== binding.agentId || identity.environment !== binding.environment) throw fail("MCP_IDENTITY_CHANGED");
|
|
226
|
+
const tools = upstream.listTools();
|
|
227
|
+
const replied = new Set();
|
|
228
|
+
const server = new Server({ name: "totem-claude-scoped-mcp", version: "1" }, { capabilities: { tools: {} } });
|
|
229
|
+
server.setRequestHandler("tools/list", async () => ({ tools }));
|
|
230
|
+
server.setRequestHandler("tools/call", async (request, context) => {
|
|
231
|
+
const { name, arguments: args } = request.params;
|
|
232
|
+
const tool = tools.find(tool => tool.name === name);
|
|
233
|
+
const peerMessage = ["totem_get_message", "totem_reply_to_message", "totem_acknowledge_message"].includes(name);
|
|
234
|
+
const ownerCommand = ["totem_claim_chat_command", "totem_renew_chat_command", "totem_complete_chat_command"].includes(name);
|
|
235
|
+
const target = peerMessage ? args?.messageId : args?.commandId;
|
|
236
|
+
if (!tool || ((peerMessage || ownerCommand) && !workIds.includes(target)))
|
|
237
|
+
return { isError: true, content: [{ type: "text", text: "This operation is outside the current receiver work." }] };
|
|
238
|
+
if (name === "totem_acknowledge_message" && !replied.has(target))
|
|
239
|
+
return { isError: true, content: [{ type: "text", text: "Confirm the reply with the same stable key before acknowledging." }] };
|
|
240
|
+
const stable = ["totem_reply_to_message", "totem_complete_chat_command"].includes(name);
|
|
241
|
+
const idempotencyKey = stable ? "native-claude-" + digest(binding.agentId + ":" + (peerMessage ? "m:" : "c:") + target) : undefined;
|
|
242
|
+
const result = await upstream.call(name, stable ? { ...args, idempotencyKey } : args ?? {}, { signal: context.signal });
|
|
243
|
+
if (name === "totem_reply_to_message" && !result.isError) {
|
|
244
|
+
const receipt = unwrapNativeMcp(result);
|
|
245
|
+
if (receipt.inReplyToId === target && typeof receipt.id === "string" && receipt.id) replied.add(target);
|
|
246
|
+
}
|
|
247
|
+
return server.projectCallToolResult(result, tool.outputSchema);
|
|
248
|
+
});
|
|
249
|
+
process.stdin.once("end", () => { void upstream.close().catch(() => {}); });
|
|
250
|
+
await server.connect(new StdioServerTransport());
|
|
251
|
+
} catch (error) {
|
|
252
|
+
await upstream.close().catch(() => {});
|
|
253
|
+
throw error;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const adapter = { kind: "claude", createBridge: createClaudeNativeBridge, createSurface: createClaudeMcpSurface,
|
|
258
|
+
files: ["native-claude-receiver.mjs", "native-cli-process.py", "native-mcp-source.mjs", "native-inbox-receiver.mjs"] };
|
|
259
|
+
function bind(config) {
|
|
260
|
+
if (JSON.stringify(config?.native?.mcp) !== JSON.stringify(config?.mcp) ||
|
|
261
|
+
config?.mcp?.agentId !== config?.source?.agentId || config?.mcp?.environment !== config?.source?.environment)
|
|
262
|
+
throw fail("MCP_BINDING_CHANGED");
|
|
263
|
+
return config;
|
|
264
|
+
}
|
|
265
|
+
export const installClaudeNativeReceiver = (root, config, options) => installNativeInboxReceiver(root, bind(config), adapter, options);
|
|
266
|
+
export const runClaudeNativeReceiver = (config, options) => runNativeInboxReceiver(bind(config), adapter, options);
|
|
267
|
+
|
|
268
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href &&
|
|
269
|
+
process.argv[2] === "--mcp-filter") {
|
|
270
|
+
try { await serveModelMcp(JSON.parse(process.argv[3]), process.argv[4], JSON.parse(process.argv[5])); }
|
|
271
|
+
catch { process.stderr.write("NATIVE_CLAUDE_MCP_FILTER_FAILED\n"); process.exitCode = 1; }
|
|
272
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Stop the owned native CLI tree when its receiver's liveness pipe closes."""
|
|
2
|
+
import contextlib
|
|
3
|
+
import os
|
|
4
|
+
from queue import SimpleQueue
|
|
5
|
+
import signal
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
import threading
|
|
9
|
+
|
|
10
|
+
if len(sys.argv) < 4 or not os.path.isabs(sys.argv[1]):
|
|
11
|
+
raise SystemExit(2)
|
|
12
|
+
client = os.path.basename(sys.argv[1]).lower()
|
|
13
|
+
claude = client in ("claude", "claude.exe") and sys.argv[2:9] == [
|
|
14
|
+
"--print", "--input-format", "stream-json", "--output-format", "stream-json", "--verbose", "--resume",
|
|
15
|
+
]
|
|
16
|
+
local_cli = client in ("opencode", "opencode.exe", "kilo", "kilo.exe") and sys.argv[2:4] == ["--pure", "run"]
|
|
17
|
+
if not (claude or local_cli):
|
|
18
|
+
raise SystemExit(2)
|
|
19
|
+
child = subprocess.Popen(sys.argv[1:], stdin=subprocess.PIPE if claude else subprocess.DEVNULL,
|
|
20
|
+
start_new_session=os.name != "nt",
|
|
21
|
+
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP if os.name == "nt" else 0)
|
|
22
|
+
lock = threading.RLock()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def stop(*_):
|
|
26
|
+
with lock:
|
|
27
|
+
if child.poll() is not None:
|
|
28
|
+
return
|
|
29
|
+
if os.name == "nt":
|
|
30
|
+
with contextlib.suppress(OSError, subprocess.TimeoutExpired):
|
|
31
|
+
subprocess.run(["taskkill.exe", "/PID", str(child.pid), "/T", "/F"],
|
|
32
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=3)
|
|
33
|
+
else:
|
|
34
|
+
with contextlib.suppress(ProcessLookupError):
|
|
35
|
+
os.killpg(child.pid, signal.SIGTERM)
|
|
36
|
+
try:
|
|
37
|
+
child.wait(timeout=0.75)
|
|
38
|
+
except subprocess.TimeoutExpired:
|
|
39
|
+
with contextlib.suppress(ProcessLookupError):
|
|
40
|
+
if os.name == "nt":
|
|
41
|
+
child.kill()
|
|
42
|
+
else:
|
|
43
|
+
os.killpg(child.pid, signal.SIGKILL)
|
|
44
|
+
child.wait()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
requested = threading.Event()
|
|
48
|
+
protocol = SimpleQueue()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def request_stop(*_):
|
|
52
|
+
requested.set()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def stop_requested():
|
|
56
|
+
requested.wait()
|
|
57
|
+
stop()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def forward_protocol():
|
|
61
|
+
try:
|
|
62
|
+
while True:
|
|
63
|
+
line = protocol.get()
|
|
64
|
+
if line == b"\0close":
|
|
65
|
+
child.stdin.close()
|
|
66
|
+
elif not child.stdin.closed:
|
|
67
|
+
child.stdin.write(line + b"\n")
|
|
68
|
+
child.stdin.flush()
|
|
69
|
+
except OSError:
|
|
70
|
+
request_stop()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def parent_pipe():
|
|
74
|
+
try:
|
|
75
|
+
if claude:
|
|
76
|
+
# Closing Claude's stdin completes its protocol. Only an actual
|
|
77
|
+
# EOF from the receiver ends the watchdog's liveness channel.
|
|
78
|
+
pending = b""
|
|
79
|
+
while chunk := os.read(0, 65536):
|
|
80
|
+
pending += chunk
|
|
81
|
+
while b"\n" in pending:
|
|
82
|
+
line, pending = pending.split(b"\n", 1)
|
|
83
|
+
protocol.put(line)
|
|
84
|
+
else:
|
|
85
|
+
while os.read(0, 1):
|
|
86
|
+
pass
|
|
87
|
+
except OSError:
|
|
88
|
+
pass
|
|
89
|
+
finally:
|
|
90
|
+
request_stop()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
for sig in (signal.SIGTERM, signal.SIGINT):
|
|
94
|
+
signal.signal(sig, request_stop)
|
|
95
|
+
if claude:
|
|
96
|
+
threading.Thread(target=forward_protocol, daemon=True).start()
|
|
97
|
+
threading.Thread(target=parent_pipe, daemon=True).start()
|
|
98
|
+
stopper = threading.Thread(target=stop_requested, daemon=True)
|
|
99
|
+
stopper.start()
|
|
100
|
+
try:
|
|
101
|
+
result = child.wait()
|
|
102
|
+
finally:
|
|
103
|
+
request_stop()
|
|
104
|
+
stopper.join(timeout=3)
|
|
105
|
+
raise SystemExit(result if result >= 0 else 1)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Host-owned profile and CA bundle locations, not credentials or peer-supplied configuration.
|
|
2
|
+
export const NATIVE_HARNESS_ENVIRONMENT = Object.freeze([
|
|
3
|
+
"CODEX_HOME",
|
|
4
|
+
"CODEX_CA_CERTIFICATE",
|
|
5
|
+
"SSL_CERT_FILE",
|
|
6
|
+
"CLAUDE_CONFIG_DIR",
|
|
7
|
+
"PI_CODING_AGENT_DIR",
|
|
8
|
+
"HERMES_HOME",
|
|
9
|
+
"HERMES_MANAGED_DIR",
|
|
10
|
+
"OPENCLAW_STATE_DIR",
|
|
11
|
+
"OPENCLAW_AGENT_DIR",
|
|
12
|
+
"OPENCLAW_CONFIG_PATH",
|
|
13
|
+
"OPENCODE_CONFIG",
|
|
14
|
+
"OPENCODE_CONFIG_DIR",
|
|
15
|
+
"KILO_CONFIG",
|
|
16
|
+
"XDG_CONFIG_HOME",
|
|
17
|
+
"XDG_DATA_HOME",
|
|
18
|
+
"XDG_CACHE_HOME",
|
|
19
|
+
"XDG_RUNTIME_DIR",
|
|
20
|
+
]);
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { spawn, execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { isAbsolute } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { StringDecoder } from "node:string_decoder";
|
|
6
|
+
import { localClientEnvironment } from "./platform-client.mjs";
|
|
7
|
+
|
|
8
|
+
const execute = promisify(execFile), fail = code => Object.assign(new Error(code), { code });
|
|
9
|
+
const sessionPattern = /^\d{8}_\d{6}_[a-f0-9]{6}$/;
|
|
10
|
+
|
|
11
|
+
/** Runs the native CLI with its owned saved profile/session and tool policy.
|
|
12
|
+
* Native process/transcript completion remains separate from a Totem receipt.
|
|
13
|
+
*/
|
|
14
|
+
export function createHermesNativeBridge(binding, options = {}) {
|
|
15
|
+
if (binding?.dedicated !== true || !sessionPattern.test(binding.sessionId ?? "") ||
|
|
16
|
+
![binding.python, binding.nativeRoot, binding.home, binding.cwd].every(value => typeof value === "string" && isAbsolute(value) && !/[\0\r\n]/.test(value)) ||
|
|
17
|
+
![binding.provider, binding.model].every(value => typeof value === "string" && /^[a-zA-Z0-9._:/-]{1,128}$/.test(value)) ||
|
|
18
|
+
binding.toolsets !== "totem" || typeof binding.ignoreRules !== "boolean" ||
|
|
19
|
+
!/^[a-f0-9]{64}$/.test(binding.policyFingerprint ?? "")) throw fail("NATIVE_HERMES_BINDING_INVALID");
|
|
20
|
+
const saved = structuredClone(binding), env = localClientEnvironment(options.env ?? process.env);
|
|
21
|
+
for (const key of Object.keys(env)) if (/^(TOTEM|AGENTTRUST)_|^(OPENAI_API_KEY|ANTHROPIC_API_KEY|CLAUDE_CODE_OAUTH_TOKEN)$/i.test(key)) delete env[key];
|
|
22
|
+
Object.assign(env, { HERMES_HOME: saved.home, HERMES_SKIP_NODE_BOOTSTRAP: "1", PYTHONPATH: saved.nativeRoot, PYTHONDONTWRITEBYTECODE: "1" });
|
|
23
|
+
let busy = false, closed = false, active, controller;
|
|
24
|
+
async function inspect() {
|
|
25
|
+
if (closed) throw fail("NATIVE_HERMES_CLOSED");
|
|
26
|
+
let metadata;
|
|
27
|
+
try {
|
|
28
|
+
if (options.readMetadata) metadata = await options.readMetadata(saved, env);
|
|
29
|
+
else {
|
|
30
|
+
const result = await execute(saved.python, [fileURLToPath(new URL("./native-hermes-metadata.py", import.meta.url)), saved.nativeRoot, saved.home, saved.sessionId],
|
|
31
|
+
{ env, cwd: saved.nativeRoot, timeout: 10000, maxBuffer: 65536, windowsHide: true });
|
|
32
|
+
metadata = JSON.parse(result.stdout);
|
|
33
|
+
}
|
|
34
|
+
} catch { throw fail("NATIVE_HERMES_METADATA_UNAVAILABLE"); }
|
|
35
|
+
if (metadata.sessionId !== saved.sessionId) throw fail("NATIVE_HERMES_SESSION_CHANGED");
|
|
36
|
+
if (metadata.model !== saved.model || metadata.provider !== saved.provider ||
|
|
37
|
+
metadata.observedRouteModel !== saved.model || metadata.observedRouteProvider !== saved.provider) throw fail("NATIVE_HERMES_MODEL_CHANGED");
|
|
38
|
+
if (metadata.policyFingerprint !== saved.policyFingerprint) throw fail("NATIVE_HERMES_POLICY_CHANGED");
|
|
39
|
+
if (![metadata.apiCalls, metadata.messages].every(value => Number.isSafeInteger(value) && value >= 0)) throw fail("NATIVE_HERMES_METADATA_UNAVAILABLE");
|
|
40
|
+
return metadata;
|
|
41
|
+
}
|
|
42
|
+
function runProcess(prompt, signal, timeoutMs) {
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
let child, failure, output = "", diagnostics = "", bytes = 0, force;
|
|
45
|
+
const decoder = new StringDecoder("utf8");
|
|
46
|
+
const kill = type => {
|
|
47
|
+
if (!child?.pid) return;
|
|
48
|
+
try {
|
|
49
|
+
if (process.platform !== "win32") process.kill(-child.pid, type);
|
|
50
|
+
else execFile("taskkill.exe", ["/PID", String(child.pid), "/T", "/F"], { windowsHide: true, timeout: 3000 }, () => {});
|
|
51
|
+
} catch { /* This launcher signals only its own process group. */ }
|
|
52
|
+
};
|
|
53
|
+
const stop = code => { if (failure) return; failure = code; kill("SIGTERM"); force = setTimeout(() => kill("SIGKILL"), 1000); };
|
|
54
|
+
const abort = () => stop("NATIVE_HERMES_CANCELLED");
|
|
55
|
+
const args = ["-m", "hermes_cli.main", "chat", "--cli", "--provider", saved.provider, "--model", saved.model,
|
|
56
|
+
"--toolsets", saved.toolsets, ...(saved.ignoreRules ? ["--ignore-rules"] : []), "--quiet", "--max-turns", "12", "--source", "tool",
|
|
57
|
+
"--resume", saved.sessionId, "--no-restore-cwd", "--query", prompt];
|
|
58
|
+
try { child = (options.spawn ?? spawn)(saved.python, [fileURLToPath(new URL("./native-hermes-process.py", import.meta.url)), ...args], { env, cwd: saved.cwd, detached: process.platform !== "win32", windowsHide: true, stdio: ["pipe", "pipe", "pipe"] }); }
|
|
59
|
+
catch { return reject(fail("NATIVE_HERMES_START_FAILED")); }
|
|
60
|
+
child.stdin.on("error", () => {}); // The native child's exit closes its liveness pipe.
|
|
61
|
+
const timer = setTimeout(() => stop("NATIVE_HERMES_TURN_TIMEOUT"), timeoutMs);
|
|
62
|
+
signal.addEventListener("abort", abort, { once: true }); if (signal.aborted) abort();
|
|
63
|
+
child.stdout.on("data", chunk => { bytes += chunk.length; if (bytes > 1024 * 1024) stop("NATIVE_HERMES_OUTPUT_LIMIT"); else output += decoder.write(chunk); });
|
|
64
|
+
child.stderr.on("data", chunk => { bytes += chunk.length; if (bytes > 1024 * 1024) stop("NATIVE_HERMES_OUTPUT_LIMIT"); else diagnostics += chunk.toString("utf8"); });
|
|
65
|
+
child.once("error", () => { failure ??= "NATIVE_HERMES_START_FAILED"; });
|
|
66
|
+
child.once("close", code => {
|
|
67
|
+
clearTimeout(timer); clearTimeout(force); signal.removeEventListener("abort", abort); kill("SIGKILL");
|
|
68
|
+
if (failure) return reject(fail(failure));
|
|
69
|
+
if (/not logged in|authentication required|unauthorized|refresh token.*(?:expired|reused)/i.test(diagnostics)) return reject(fail("AGENT_AUTH_REQUIRED"));
|
|
70
|
+
if (/quota.*(?:exceeded|insufficient)|credit balance|usage limit/i.test(diagnostics)) return reject(fail("AGENT_QUOTA_EXCEEDED"));
|
|
71
|
+
if (code !== 0) return reject(fail("NATIVE_HERMES_EXIT_FAILED"));
|
|
72
|
+
const ids = [...(output + "\n" + diagnostics).matchAll(/(?:^|\n)session_id: ([A-Za-z0-9_-]+)\s*(?=\n|$)/g)].map(match => match[1]);
|
|
73
|
+
if (ids.length !== 1 || ids[0] !== saved.sessionId) return reject(fail("NATIVE_HERMES_SESSION_UNVERIFIED"));
|
|
74
|
+
resolve();
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
open: inspect, inspect,
|
|
80
|
+
async run(prompt, { signal, timeoutMs = 120000 } = {}) {
|
|
81
|
+
if (busy) throw fail("NATIVE_HERMES_BUSY");
|
|
82
|
+
if (typeof prompt !== "string" || !prompt.trim() || Buffer.byteLength(prompt) > 65536 || !Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 900000) throw fail("NATIVE_HERMES_PROMPT_INVALID");
|
|
83
|
+
if (closed || signal?.aborted) throw fail("NATIVE_HERMES_CANCELLED");
|
|
84
|
+
busy = true; controller = new AbortController();
|
|
85
|
+
const combined = AbortSignal.any([controller.signal, ...(signal ? [signal] : [])]);
|
|
86
|
+
active = (async () => {
|
|
87
|
+
const before = await inspect();
|
|
88
|
+
if (combined.aborted) throw fail("NATIVE_HERMES_CANCELLED");
|
|
89
|
+
await runProcess(prompt, combined, timeoutMs);
|
|
90
|
+
const after = await inspect();
|
|
91
|
+
if (combined.aborted) throw fail("NATIVE_HERMES_CANCELLED");
|
|
92
|
+
if (after.messages <= before.messages || after.apiCalls <= before.apiCalls) throw fail("NATIVE_HERMES_TRANSCRIPT_UNVERIFIED");
|
|
93
|
+
return { nativeProcessExited: true, nativeTranscriptAdvanced: true, sessionId: after.sessionId, model: after.model, provider: after.provider, totemReplyVerified: false };
|
|
94
|
+
})();
|
|
95
|
+
try { return await active; } finally { busy = false; active = undefined; controller = undefined; }
|
|
96
|
+
},
|
|
97
|
+
async close() { closed = true; controller?.abort(); await active?.catch(() => {}); },
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createHermesNativeBridge } from "./native-hermes-bridge.mjs";
|
|
2
|
+
import { createHermesMcpSurface } from "./native-hermes-mcp.mjs";
|
|
3
|
+
import { installNativeInboxReceiver } from "./native-inbox-installation.mjs";
|
|
4
|
+
|
|
5
|
+
export function installHermesNativeReceiver(root, config, options = {}) {
|
|
6
|
+
return installNativeInboxReceiver(root, config, { kind: "hermes", createBridge: createHermesNativeBridge, createSurface: createHermesMcpSurface,
|
|
7
|
+
files: ["native-hermes-bridge.mjs", "native-hermes-metadata.py", "native-hermes-process.py", "native-hermes-mcp.mjs", "native-hermes-mcp.py", "native-mcp-source.mjs", "native-hermes-receiver.mjs", "native-inbox-receiver.mjs"],
|
|
8
|
+
}, options);
|
|
9
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { isAbsolute } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { StringDecoder } from "node:string_decoder";
|
|
6
|
+
import { localClientEnvironment } from "./platform-client.mjs";
|
|
7
|
+
|
|
8
|
+
const fail = code => Object.assign(new Error(code), { code });
|
|
9
|
+
const limit = 8 * 1024 * 1024;
|
|
10
|
+
const tools = new Set(["totem_get_identity", "totem_get_activity", "totem_list_messages", "totem_list_chat_commands", "totem_get_chat", "totem_get_conversation", "totem_listen", "totem_start_listener"]);
|
|
11
|
+
const codeOf = value => /^[A-Z0-9_]{1,100}$/.test(value) ? value : "NATIVE_HERMES_MCP_CALL_FAILED";
|
|
12
|
+
|
|
13
|
+
/** Uses the native Hermes MCP task and its own OAuth manager; never reads or
|
|
14
|
+
* copies authentication files and never launches an LLM for empty checks.
|
|
15
|
+
*/
|
|
16
|
+
export async function createHermesMcpSurface(binding, options = {}) {
|
|
17
|
+
if (!binding || ![binding.python, binding.nativeRoot, binding.home, binding.cwd].every(value =>
|
|
18
|
+
typeof value === "string" && isAbsolute(value) && !/[\0\r\n]/.test(value)) ||
|
|
19
|
+
!/^[a-zA-Z0-9_-]{1,80}$/.test(binding.serverName ?? "") ||
|
|
20
|
+
!/^[a-f0-9]{64}$/.test(binding.policyFingerprint ?? "") ||
|
|
21
|
+
!/^[a-f0-9]{64}$/.test(binding.serverFingerprint ?? "") ||
|
|
22
|
+
!/^\d+\.\d+\.\d+(?:[-+][a-zA-Z0-9.-]+)?$/.test(binding.nativeVersion ?? "")) throw fail("NATIVE_HERMES_MCP_BINDING_INVALID");
|
|
23
|
+
const saved = structuredClone(binding), env = localClientEnvironment(options.env ?? process.env);
|
|
24
|
+
for (const key of Object.keys(env)) if (/^(TOTEM|AGENTTRUST)_|^(OPENAI_API_KEY|ANTHROPIC_API_KEY|CLAUDE_CODE_OAUTH_TOKEN)$/i.test(key)) delete env[key];
|
|
25
|
+
Object.assign(env, { HERMES_HOME: saved.home, HERMES_SKIP_NODE_BOOTSTRAP: "1", PYTHONPATH: saved.nativeRoot, PYTHONDONTWRITEBYTECODE: "1", PYTHONUNBUFFERED: "1" });
|
|
26
|
+
let child, ready, closed = false, closing, buffer = "", nativeTools = [], incoming = false;
|
|
27
|
+
const pending = new Map(), decoder = new StringDecoder("utf8");
|
|
28
|
+
const timeoutMs = options.timeoutMs ?? 25000;
|
|
29
|
+
let resolveReady, rejectReady;
|
|
30
|
+
const opened = new Promise((resolve, reject) => { resolveReady = resolve; rejectReady = reject; });
|
|
31
|
+
function terminal(code) {
|
|
32
|
+
const error = fail(code);
|
|
33
|
+
rejectReady(error);
|
|
34
|
+
for (const request of pending.values()) request.finish(error);
|
|
35
|
+
pending.clear();
|
|
36
|
+
void close();
|
|
37
|
+
}
|
|
38
|
+
function accept(record) {
|
|
39
|
+
if (record?.type === "fatal") return terminal(codeOf(record.error?.code));
|
|
40
|
+
if (record?.type === "ready" && !ready && Array.isArray(record.tools) && record.tools.length <= tools.size &&
|
|
41
|
+
record.tools.every(tool => tools.has(tool.name) && tool.inputSchema && typeof tool.inputSchema === "object")) {
|
|
42
|
+
ready = true; nativeTools = record.tools; incoming = record.nativeIncoming === true; resolveReady(); return;
|
|
43
|
+
}
|
|
44
|
+
if (record?.type !== "response" || !pending.has(record.id)) return terminal("NATIVE_HERMES_MCP_PROTOCOL_INVALID");
|
|
45
|
+
const request = pending.get(record.id); pending.delete(record.id);
|
|
46
|
+
if (record.error) {
|
|
47
|
+
const error = fail(codeOf(record.error.code));
|
|
48
|
+
if (Number.isInteger(record.error.status) && record.error.status >= 400 && record.error.status <= 599) error.status = record.error.status;
|
|
49
|
+
if (Number.isSafeInteger(record.error.retryAfterSeconds) && record.error.retryAfterSeconds > 0 && record.error.retryAfterSeconds <= 86400) error.retryAfterSeconds = record.error.retryAfterSeconds;
|
|
50
|
+
request.finish(error);
|
|
51
|
+
} else request.finish(undefined, record.result);
|
|
52
|
+
}
|
|
53
|
+
async function close() {
|
|
54
|
+
closed = true;
|
|
55
|
+
closing ??= (async () => {
|
|
56
|
+
if (!child) return;
|
|
57
|
+
child.stdin.end();
|
|
58
|
+
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
59
|
+
await new Promise(resolve => {
|
|
60
|
+
const term = setTimeout(() => child.kill("SIGTERM"), 1000);
|
|
61
|
+
const kill = setTimeout(() => child.kill("SIGKILL"), 3000);
|
|
62
|
+
child.once("close", () => { clearTimeout(term); clearTimeout(kill); resolve(); });
|
|
63
|
+
});
|
|
64
|
+
})();
|
|
65
|
+
await closing;
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
child = (options.spawn ?? spawn)(saved.python, [fileURLToPath(new URL("./native-hermes-mcp.py", import.meta.url))],
|
|
69
|
+
{ env, cwd: saved.cwd, windowsHide: true, stdio: ["pipe", "pipe", "pipe"] });
|
|
70
|
+
child.stdout.on("data", chunk => {
|
|
71
|
+
buffer += decoder.write(chunk);
|
|
72
|
+
if (Buffer.byteLength(buffer) > limit) return terminal("NATIVE_HERMES_MCP_OUTPUT_LIMIT");
|
|
73
|
+
for (;;) {
|
|
74
|
+
const end = buffer.indexOf("\n"); if (end < 0) break;
|
|
75
|
+
const line = buffer.slice(0, end); buffer = buffer.slice(end + 1);
|
|
76
|
+
try { accept(JSON.parse(line)); } catch { return terminal("NATIVE_HERMES_MCP_PROTOCOL_INVALID"); }
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
child.stderr.on("data", () => {}); // Native diagnostics can contain private URLs.
|
|
80
|
+
child.on("error", () => terminal("NATIVE_HERMES_MCP_UNAVAILABLE"));
|
|
81
|
+
child.once("close", () => terminal("NATIVE_HERMES_MCP_CLOSED"));
|
|
82
|
+
child.stdin.on("error", () => terminal("NATIVE_HERMES_MCP_CLOSED"));
|
|
83
|
+
child.stdin.write(JSON.stringify(saved) + "\n");
|
|
84
|
+
const abort = () => terminal("RUN_CANCELLED"), timer = setTimeout(() => terminal("TIMEOUT"), timeoutMs);
|
|
85
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
86
|
+
try { if (options.signal?.aborted) abort(); await opened; }
|
|
87
|
+
finally { clearTimeout(timer); options.signal?.removeEventListener("abort", abort); }
|
|
88
|
+
} catch (error) { await close(); throw fail(codeOf(error.code)); }
|
|
89
|
+
return {
|
|
90
|
+
get nativeIncoming() { return incoming; },
|
|
91
|
+
listTools() { if (closed) throw fail("NATIVE_HERMES_MCP_CLOSED"); return structuredClone(nativeTools); },
|
|
92
|
+
async call(name, args = {}, { signal } = {}) {
|
|
93
|
+
if (closed || signal?.aborted) throw fail("RUN_CANCELLED");
|
|
94
|
+
if (!tools.has(name) || !nativeTools.some(tool => tool.name === name)) throw fail("MCP_TOOL_UNAVAILABLE");
|
|
95
|
+
if (name === "totem_start_listener" && args.automatic !== true) throw fail("MCP_AUTOMATIC_START_UNAVAILABLE");
|
|
96
|
+
const id = randomUUID(), message = JSON.stringify({ id, name, arguments: args }) + "\n";
|
|
97
|
+
if (Buffer.byteLength(message) > limit) throw fail("NATIVE_HERMES_MCP_INPUT_LIMIT");
|
|
98
|
+
return new Promise((resolve, reject) => {
|
|
99
|
+
const abort = () => terminal("RUN_CANCELLED"), timer = setTimeout(() => terminal("TIMEOUT"), timeoutMs);
|
|
100
|
+
pending.set(id, { finish(error, result) { clearTimeout(timer); signal?.removeEventListener("abort", abort); if (error) reject(error); else resolve(result); } });
|
|
101
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
102
|
+
if (signal?.aborted) abort();
|
|
103
|
+
else child.stdin.write(message, error => { if (error) terminal("NATIVE_HERMES_MCP_CLOSED"); });
|
|
104
|
+
});
|
|
105
|
+
}, close,
|
|
106
|
+
};
|
|
107
|
+
}
|