mycomesh-consumer 0.1.52 → 0.2.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/README.md +27 -135
- package/bin/mycomesh-consumer.mjs +5 -8
- package/networks/mycomesh-v11-sepolia.json +27 -0
- package/package.json +8 -21
- package/src/chain.mjs +52 -0
- package/src/cli.mjs +131 -643
- package/src/consumer.mjs +144 -473
- package/src/disputes.mjs +54 -0
- package/src/eip712.mjs +151 -0
- package/src/protocol.mjs +90 -0
- package/src/secure-envelope.mjs +324 -0
- package/src/sse.mjs +101 -0
- package/bin/mycomesh.mjs +0 -10
- package/networks/v10-controlled-test.json +0 -119
- package/networks/v10-dynamic-20260926.ca.crt +0 -25
- package/networks/v10-dynamic-20260926.json +0 -154
- package/src/consumer-discovery.mjs +0 -355
- package/src/consumer-history.mjs +0 -287
- package/src/consumer-request-journal.mjs +0 -189
- package/src/consumer-reserved.mjs +0 -112
- package/src/consumer-runtime.mjs +0 -4176
- package/src/consumer-sessions.mjs +0 -205
- package/src/release.mjs +0 -12
- /package/networks/{v10-controlled-test.ca.crt → mycomesh-testnet-ca.crt} +0 -0
package/src/consumer.mjs
CHANGED
|
@@ -1,502 +1,173 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
1
|
+
// V11 Consumer runtime: one custodied deposit, any Provider, relay-blind requests.
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { createServer, request as httpRequest } from "node:http";
|
|
3
4
|
import { request as httpsRequest } from "node:https";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
8
|
-
|
|
5
|
+
import { dirname, resolve } from "node:path";
|
|
6
|
+
import { rootCertificates } from "node:tls";
|
|
7
|
+
import { addressOf } from "./eip712.mjs";
|
|
8
|
+
import { openResponse, prepareRequest, verifiedProvider } from "./protocol.mjs";
|
|
9
|
+
import { recordRequest } from "./disputes.mjs";
|
|
10
|
+
import { chatSse, responseSse } from "./sse.mjs";
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
DEFAULT_MAX_FEE_UNITS,
|
|
13
|
-
DEFAULT_RELAY_URL,
|
|
14
|
-
NativeConsumerState,
|
|
15
|
-
createConsumerServer,
|
|
16
|
-
parseNetworkConfig,
|
|
17
|
-
paymentKeyAddress,
|
|
18
|
-
} from "./consumer-runtime.mjs";
|
|
12
|
+
const PROVIDER_CACHE_MS = 30_000;
|
|
13
|
+
const MAX_BODY = 16 * 1024 * 1024;
|
|
19
14
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
);
|
|
26
|
-
export const V10_DYNAMIC_PROVIDER_AI_NETWORK = fileURLToPath(
|
|
27
|
-
new URL("../networks/v10-dynamic-20260926.json", import.meta.url),
|
|
28
|
-
);
|
|
29
|
-
export const API_COMMANDS = new Set(["health", "models", "responses", "chat"]);
|
|
30
|
-
const API_VALUE_OPTIONS = new Set([
|
|
31
|
-
"--base-url",
|
|
32
|
-
"--api-key",
|
|
33
|
-
"--timeout",
|
|
34
|
-
"--session-id",
|
|
35
|
-
"--json",
|
|
36
|
-
"--model",
|
|
37
|
-
"--input",
|
|
38
|
-
"--message",
|
|
39
|
-
"--system",
|
|
40
|
-
"--max-output-tokens",
|
|
41
|
-
"--max-tokens",
|
|
42
|
-
"--max-completion-tokens",
|
|
43
|
-
]);
|
|
44
|
-
|
|
45
|
-
export const CONSUMER_HELP = `Usage: mycomesh-consumer [options] [-- codex-options]
|
|
46
|
-
|
|
47
|
-
Start the local MycoMesh Consumer without Docker, Python, or a public Gateway.
|
|
48
|
-
The process manages a persisted access key, selects healthy Relays, and
|
|
49
|
-
exposes an OpenAI-compatible loopback API.
|
|
50
|
-
|
|
51
|
-
Options:
|
|
52
|
-
--no-browser Print the local credentials URL without opening it
|
|
53
|
-
--codex Start an optional Codex client after Relay readiness
|
|
54
|
-
--no-codex Keep only the Consumer API running (default)
|
|
55
|
-
--stop Stop a previously started native Consumer
|
|
56
|
-
--reset-local Confirm and delete the local Consumer data directory
|
|
57
|
-
--codex-command PATH Codex executable for --codex (default: codex on PATH)
|
|
58
|
-
--ready-timeout SEC Relay readiness timeout (default: 1800)
|
|
59
|
-
--data-dir DIR Payment key and history directory
|
|
60
|
-
--relay URLS Comma-separated Relay URLs for automatic failover
|
|
61
|
-
--network-config FILE Use a trusted network manifest and its backup Relays
|
|
62
|
-
--ca-file FILE Private CA PEM for an explicit controlled-test network
|
|
63
|
-
--controlled-test Explicitly opt into a controlled test committee
|
|
64
|
-
--v10-controlled-test Select the bundled V10 fixed-budget controlled testnet
|
|
65
|
-
--v10-dynamic-provider-ai Select the bundled V10 dynamic Provider-AI testnet
|
|
66
|
-
--proxy URL Optional outbound HTTP proxy
|
|
67
|
-
--host HOST Listen address (default: 127.0.0.1)
|
|
68
|
-
--port PORT Listen port (default: 8110)
|
|
69
|
-
--max-fee UNITS Maximum fee per request (default: 100000)
|
|
70
|
-
--dry-run Print the native startup plan
|
|
71
|
-
--doctor Check local Consumer configuration without starting or paying
|
|
72
|
-
--doctor-json Emit the same check as stable JSON for automation
|
|
73
|
-
-h, --help Show this help
|
|
74
|
-
-v, --version Show the package version
|
|
75
|
-
|
|
76
|
-
Each Consumer process starts locked. Connect your browser wallet on the setup
|
|
77
|
-
page. For first use, choose Enable API access and confirm the wallet request;
|
|
78
|
-
returning users reuse the existing authorization. Copy the API URL and key (or
|
|
79
|
-
the combined export) into your client. Never enter a wallet private key.
|
|
80
|
-
The page has no browser conversation state.`;
|
|
81
|
-
|
|
82
|
-
class ConsumerCliError extends Error {
|
|
83
|
-
constructor(message, exitCode = 1) {
|
|
84
|
-
super(message);
|
|
85
|
-
this.name = "ConsumerCliError";
|
|
86
|
-
this.exitCode = exitCode;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export async function main(argv, dependencies = {}) {
|
|
91
|
-
const env = dependencies.env ?? process.env;
|
|
92
|
-
const stdout = dependencies.stdout ?? process.stdout;
|
|
93
|
-
const stderr = dependencies.stderr ?? process.stderr;
|
|
94
|
-
try {
|
|
95
|
-
const parsed = parseArguments(argv, env);
|
|
96
|
-
if (parsed.help) {
|
|
97
|
-
stdout.write(`${CONSUMER_HELP}\n`);
|
|
98
|
-
return 0;
|
|
99
|
-
}
|
|
100
|
-
if (parsed.version) {
|
|
101
|
-
stdout.write(`${CONSUMER_RELEASE_VERSION}\n`);
|
|
102
|
-
return 0;
|
|
103
|
-
}
|
|
104
|
-
if (parsed.doctor || parsed.doctorJson) {
|
|
105
|
-
return await consumerDoctor({ parsed, env, stdout, fetch: dependencies.fetch, json: parsed.doctorJson });
|
|
106
|
-
}
|
|
107
|
-
if (parsed.dryRun) {
|
|
108
|
-
stdout.write(`Native Consumer: ${parsed.host}:${parsed.port}\n`);
|
|
109
|
-
stdout.write(`Data directory: ${parsed.dataDir}\n`);
|
|
110
|
-
stdout.write(`Relays: ${!parsed.relayUrlsExplicit && parsed.networkConfig ? "from selected network manifest (legacy default if absent)" : parsed.relayUrls}\n`);
|
|
111
|
-
stdout.write("Docker: disabled\n");
|
|
112
|
-
return 0;
|
|
113
|
-
}
|
|
114
|
-
if (parsed.stop) return stopConsumer(parsed, stdout, stderr);
|
|
115
|
-
if (parsed.resetLocal) return await resetConsumer(parsed, env, stdout, stderr);
|
|
116
|
-
|
|
117
|
-
const state = dependencies.createState
|
|
118
|
-
? dependencies.createState(parsed, env)
|
|
119
|
-
: new NativeConsumerState({
|
|
120
|
-
env: { ...env, MYCOMESH_CONSUMER_DATA_DIR: parsed.dataDir },
|
|
121
|
-
dataDir: parsed.dataDir,
|
|
122
|
-
relayUrls: parsed.relayUrlsExplicit ? parsed.relayUrls : undefined,
|
|
123
|
-
networkConfig: parsed.networkConfig,
|
|
124
|
-
caFile: parsed.caFile,
|
|
125
|
-
allowControlledTest: parsed.allowControlledTest,
|
|
126
|
-
proxy: parsed.proxy,
|
|
127
|
-
baseUrl: parsed.baseUrl,
|
|
128
|
-
maxFeeUnits: parsed.maxFeeUnits,
|
|
129
|
-
});
|
|
130
|
-
const runtime = dependencies.createServer
|
|
131
|
-
? dependencies.createServer(state, parsed)
|
|
132
|
-
: createConsumerServer(state, { host: parsed.host, port: parsed.port });
|
|
133
|
-
await runtime.listen();
|
|
134
|
-
const rootUrl = `${parsed.scheme}://${parsed.hostForUrl}:${parsed.port}`;
|
|
135
|
-
const credentialsUrl = `${rootUrl}/`;
|
|
136
|
-
writePid(parsed.dataDir);
|
|
137
|
-
stdout.write(`MycoMesh Consumer setup: ${credentialsUrl}\n`);
|
|
138
|
-
stdout.write(`OpenAI API: ${state.baseUrl}\n`);
|
|
139
|
-
if (!parsed.noBrowser) openBrowser(credentialsUrl, stderr);
|
|
140
|
-
|
|
141
|
-
const shutdown = async () => {
|
|
142
|
-
clearPid(parsed.dataDir);
|
|
143
|
-
await state.stopShare?.();
|
|
144
|
-
await runtime.close();
|
|
145
|
-
};
|
|
146
|
-
if (parsed.noCodex) {
|
|
147
|
-
await waitForSignal(shutdown);
|
|
148
|
-
return 0;
|
|
149
|
-
}
|
|
150
|
-
await waitUntilReady(state, parsed.readyTimeout, stdout);
|
|
151
|
-
await waitUntilUnlocked(state, parsed.readyTimeout, stdout);
|
|
152
|
-
const code = await runCodex(parsed, state, dependencies.spawn ?? defaultSpawn, stdout, stderr);
|
|
153
|
-
await shutdown();
|
|
154
|
-
return code;
|
|
155
|
-
} catch (error) {
|
|
156
|
-
const exitCode = error instanceof ConsumerCliError ? error.exitCode : 1;
|
|
157
|
-
const message = error?.code === "EADDRINUSE"
|
|
158
|
-
? `port ${error.port || "requested"} is already in use; stop the old Consumer or choose --port with a matching --base-url`
|
|
159
|
-
: error instanceof Error ? error.message : String(error);
|
|
160
|
-
stderr.write(`mycomesh consumer: ${message}\n`);
|
|
161
|
-
return exitCode;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
export function parseArguments(argv, env = process.env) {
|
|
166
|
-
const parsed = {
|
|
167
|
-
baseUrl: env.MYCOMESH_CONSUMER_PUBLIC_BASE_URL || DEFAULT_BASE_URL,
|
|
168
|
-
baseUrlExplicit: Boolean(env.MYCOMESH_CONSUMER_PUBLIC_BASE_URL),
|
|
169
|
-
dataDir: env.MYCOMESH_CONSUMER_DATA_DIR || join(env.HOME || process.cwd(), ".mycomesh", "consumer"),
|
|
170
|
-
relayUrls: env.MYCOMESH_V8_RELAY_URLS || env.MYCOMESH_CONSUMER_RELAY_URL || DEFAULT_RELAY_URL,
|
|
171
|
-
relayUrlsExplicit: Boolean(env.MYCOMESH_V8_RELAY_URLS || env.MYCOMESH_CONSUMER_RELAY_URL),
|
|
172
|
-
networkConfig: env.MYCOMESH_CONSUMER_NETWORK_CONFIG || undefined,
|
|
173
|
-
caFile: env.MYCOMESH_CONSUMER_CA_FILE || undefined,
|
|
174
|
-
proxy: env.MYCOMESH_CONSUMER_PROXY || "",
|
|
175
|
-
codexCommand: env.MYCOMESH_CODEX_COMMAND || "codex",
|
|
176
|
-
readyTimeout: parsePositive(env.MYCOMESH_CONSUMER_READY_TIMEOUT_SECONDS || "1800", "ready timeout", 86400),
|
|
177
|
-
maxFeeUnits: parsePositive(env.MYCOMESH_V8_MAX_FEE_UNITS || String(DEFAULT_MAX_FEE_UNITS), "max fee"),
|
|
178
|
-
host: env.MYCOMESH_CONSUMER_HOST || "127.0.0.1",
|
|
179
|
-
hostForUrl: env.MYCOMESH_CONSUMER_HOST || "127.0.0.1",
|
|
180
|
-
port: parsePositive(env.MYCOMESH_CONSUMER_PORT || "8110", "port", 65535),
|
|
181
|
-
scheme: "http",
|
|
182
|
-
allowControlledTest: false,
|
|
183
|
-
v10ControlledTest: false,
|
|
184
|
-
v10DynamicProviderAi: false,
|
|
185
|
-
noBrowser: false,
|
|
186
|
-
noCodex: env.MYCOMESH_CONSUMER_START_CODEX !== "1",
|
|
187
|
-
stop: false,
|
|
188
|
-
resetLocal: false,
|
|
189
|
-
dryRun: false,
|
|
190
|
-
doctor: false,
|
|
191
|
-
doctorJson: false,
|
|
192
|
-
help: false,
|
|
193
|
-
version: false,
|
|
194
|
-
codexArgs: [],
|
|
195
|
-
};
|
|
196
|
-
for (let index = 0; index < argv.length; index += 1) {
|
|
197
|
-
const token = argv[index];
|
|
198
|
-
if (token === "--") { parsed.codexArgs = argv.slice(index + 1); break; }
|
|
199
|
-
if (token === "-h" || token === "--help") { parsed.help = true; continue; }
|
|
200
|
-
if (token === "-v" || token === "--version") { parsed.version = true; continue; }
|
|
201
|
-
if (token === "--controlled-test") { parsed.allowControlledTest = true; continue; }
|
|
202
|
-
if (token === "--v10-controlled-test") {
|
|
203
|
-
parsed.v10ControlledTest = true;
|
|
204
|
-
parsed.allowControlledTest = true;
|
|
205
|
-
continue;
|
|
206
|
-
}
|
|
207
|
-
if (token === "--v10-dynamic-provider-ai") {
|
|
208
|
-
parsed.v10DynamicProviderAi = true;
|
|
209
|
-
parsed.allowControlledTest = true;
|
|
210
|
-
continue;
|
|
211
|
-
}
|
|
212
|
-
if (token === "--no-browser") { parsed.noBrowser = true; continue; }
|
|
213
|
-
if (token === "--codex") { parsed.noCodex = false; continue; }
|
|
214
|
-
if (token === "--no-codex") { parsed.noCodex = true; continue; }
|
|
215
|
-
if (token === "--stop") { parsed.stop = true; continue; }
|
|
216
|
-
if (token === "--reset-local") { parsed.resetLocal = true; continue; }
|
|
217
|
-
if (token === "--dry-run") { parsed.dryRun = true; continue; }
|
|
218
|
-
if (token === "--doctor") { parsed.doctor = true; continue; }
|
|
219
|
-
if (token === "--doctor-json") { parsed.doctorJson = true; continue; }
|
|
220
|
-
const separator = token.indexOf("=");
|
|
221
|
-
const name = separator === -1 ? token : token.slice(0, separator);
|
|
222
|
-
let value = separator === -1 ? undefined : token.slice(separator + 1);
|
|
223
|
-
const options = new Set(["--base-url", "--data-dir", "--relay", "--network-config", "--ca-file", "--proxy", "--codex-command", "--ready-timeout", "--host", "--port", "--max-fee"]);
|
|
224
|
-
if (!options.has(name)) throw new ConsumerCliError(`unknown option: ${token}`, 2);
|
|
225
|
-
if (value === undefined) { index += 1; value = argv[index]; }
|
|
226
|
-
if (!value) throw new ConsumerCliError(`${name} requires a value`, 2);
|
|
227
|
-
if (name === "--base-url") { parsed.baseUrl = value; parsed.baseUrlExplicit = true; }
|
|
228
|
-
if (name === "--data-dir") parsed.dataDir = value;
|
|
229
|
-
if (name === "--relay") { parsed.relayUrls = value; parsed.relayUrlsExplicit = true; }
|
|
230
|
-
if (name === "--network-config") parsed.networkConfig = value;
|
|
231
|
-
if (name === "--ca-file") parsed.caFile = value;
|
|
232
|
-
if (name === "--proxy") parsed.proxy = value;
|
|
233
|
-
if (name === "--codex-command") parsed.codexCommand = value;
|
|
234
|
-
if (name === "--ready-timeout") parsed.readyTimeout = parsePositive(value, name, 86400);
|
|
235
|
-
if (name === "--host") { parsed.host = value; parsed.hostForUrl = value.includes(":") ? `[${value}]` : value; }
|
|
236
|
-
if (name === "--port") parsed.port = parsePositive(value, name, 65535);
|
|
237
|
-
if (name === "--max-fee") parsed.maxFeeUnits = parsePositive(value, name);
|
|
238
|
-
}
|
|
239
|
-
if (parsed.v10ControlledTest && !parsed.networkConfig) {
|
|
240
|
-
parsed.networkConfig = env.MYCOMESH_V10_NETWORK_CONFIG || V10_CONTROLLED_TEST_NETWORK;
|
|
241
|
-
}
|
|
242
|
-
if (parsed.v10DynamicProviderAi && !parsed.networkConfig) {
|
|
243
|
-
parsed.networkConfig = env.MYCOMESH_V10_DYNAMIC_NETWORK_CONFIG || V10_DYNAMIC_PROVIDER_AI_NETWORK;
|
|
244
|
-
}
|
|
245
|
-
if (!parsed.baseUrlExplicit) parsed.baseUrl = `http://${parsed.hostForUrl}:${parsed.port}/v1`;
|
|
246
|
-
try { new URL(parsed.baseUrl); } catch { throw new ConsumerCliError("--base-url must be an absolute URL", 2); }
|
|
247
|
-
return parsed;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// Read-only preflight. This deliberately does not construct NativeConsumerState:
|
|
251
|
-
// its constructor creates a payment key on first run. A doctor command must be
|
|
252
|
-
// safe to run from installers, CI and support scripts without changing state.
|
|
253
|
-
async function consumerDoctor({ parsed, env, stdout, fetch: injectedFetch, json }) {
|
|
254
|
-
const checks = [];
|
|
255
|
-
const add = (id, status, message, details = undefined) => {
|
|
256
|
-
const check = { id, status, message };
|
|
257
|
-
if (details !== undefined) check.details = details;
|
|
258
|
-
checks.push(check);
|
|
259
|
-
};
|
|
260
|
-
const dataDir = parsed.dataDir;
|
|
261
|
-
if (existsSync(dataDir)) add("data_dir", "ready", "Consumer data directory exists", { path: dataDir });
|
|
262
|
-
else add("data_dir", "setup_required", "Consumer data directory has not been created", { path: dataDir });
|
|
263
|
-
|
|
264
|
-
const keyPath = join(dataDir, "payment-key");
|
|
265
|
-
const configuredKey = String(env.MYCOMESH_V8_PAYMENT_KEY || "").trim();
|
|
266
|
-
let paymentAddress;
|
|
267
|
-
try {
|
|
268
|
-
if (configuredKey) paymentAddress = paymentKeyAddress(configuredKey);
|
|
269
|
-
else if (existsSync(keyPath)) paymentAddress = paymentKeyAddress(readFileSync(keyPath, "utf8").trim());
|
|
270
|
-
else throw Object.assign(new Error("payment key is not initialized"), { code: "setup_required" });
|
|
271
|
-
add("payment_key", "ready", "Payment key is present and valid", {
|
|
272
|
-
source: configuredKey ? "environment" : "data_dir",
|
|
273
|
-
address: paymentAddress,
|
|
274
|
-
});
|
|
275
|
-
} catch (error) {
|
|
276
|
-
add("payment_key", error.code === "setup_required" ? "setup_required" : "blocked", error.message);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
let network;
|
|
280
|
-
try {
|
|
281
|
-
network = parseNetworkConfig(parsed.networkConfig, { allowControlledTest: parsed.allowControlledTest });
|
|
282
|
-
add("network_manifest", "ready", parsed.networkConfig ? "Settlement network manifest is valid" : "Using built-in settlement network", {
|
|
283
|
-
path: parsed.networkConfig || null,
|
|
284
|
-
protocol_version: network.protocol_version,
|
|
285
|
-
chain_id: network.chain_id,
|
|
286
|
-
settlement_contract: network.settlement_contract,
|
|
287
|
-
});
|
|
288
|
-
} catch (error) {
|
|
289
|
-
add("network_manifest", "blocked", error.message, { path: parsed.networkConfig || null });
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
const relayUrls = network?.relay_urls?.length ? network.relay_urls : String(parsed.relayUrls || "")
|
|
293
|
-
.split(",").map((value) => value.trim()).filter(Boolean);
|
|
294
|
-
const validRelays = [];
|
|
295
|
-
for (const value of relayUrls) {
|
|
296
|
-
try {
|
|
297
|
-
const url = new URL(value);
|
|
298
|
-
if (!/^https?:$/.test(url.protocol) || url.username || url.password || url.search || url.hash) throw new Error("URL must be an HTTPS origin without credentials or query");
|
|
299
|
-
validRelays.push(url.href.replace(/\/+$/, ""));
|
|
300
|
-
} catch (error) {
|
|
301
|
-
add("relay_urls", "blocked", `Invalid Relay URL: ${error.message}`, { url: value });
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
if (validRelays.length) add("relay_urls", "ready", `${validRelays.length} Relay URL${validRelays.length === 1 ? "" : "s"} configured`, { urls: validRelays });
|
|
305
|
-
else if (!checks.some((check) => check.id === "relay_urls")) add("relay_urls", "blocked", "No usable Relay URL is configured");
|
|
306
|
-
|
|
307
|
-
const pidPath = join(dataDir, "consumer.pid");
|
|
308
|
-
if (!existsSync(pidPath)) add("local_process", "ready", "No stale Consumer pid file found", { running: false });
|
|
309
|
-
else {
|
|
310
|
-
const pid = Number(readFileSync(pidPath, "utf8").trim());
|
|
311
|
-
let running = false;
|
|
312
|
-
try { if (Number.isInteger(pid) && pid > 1) { process.kill(pid, 0); running = true; } } catch (error) { if (error.code !== "ESRCH") running = true; }
|
|
313
|
-
add("local_process", running ? "ready" : "blocked", running ? "Consumer process is running" : "Consumer pid file is stale", { pid, running });
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
const fetchImpl = injectedFetch || ((url, options) => consumerDoctorFetch(url, options, parsed));
|
|
317
|
-
const health = [];
|
|
318
|
-
if (validRelays.length && typeof fetchImpl === "function") {
|
|
319
|
-
await Promise.all(validRelays.map(async (relay) => {
|
|
320
|
-
const controller = new AbortController();
|
|
321
|
-
const timer = setTimeout(() => controller.abort(), 1500);
|
|
322
|
-
try {
|
|
323
|
-
const response = await fetchImpl(`${relay}/health`, { method: "GET", redirect: "error", signal: controller.signal });
|
|
324
|
-
health.push({ url: relay, status: response.ok ? "ready" : "blocked", http_status: response.status });
|
|
325
|
-
await response.body?.cancel?.();
|
|
326
|
-
} catch (error) { health.push({ url: relay, status: "blocked", error: error.message }); }
|
|
327
|
-
finally { clearTimeout(timer); }
|
|
328
|
-
}));
|
|
329
|
-
const healthy = health.filter((item) => item.status === "ready").length;
|
|
330
|
-
const relayStatus = healthy === 0 ? "blocked" : healthy < health.length ? "degraded" : "ready";
|
|
331
|
-
add("relay_health", relayStatus, healthy ? `${healthy}/${health.length} Relay health checks passed` : "All Relay health checks failed", { relays: health });
|
|
332
|
-
} else add("relay_health", "setup_required", "Relay health was not checked because no usable Relay URL is configured");
|
|
333
|
-
|
|
334
|
-
const hasBlocked = checks.some((check) => check.status === "blocked");
|
|
335
|
-
const hasSetup = checks.some((check) => check.status === "setup_required");
|
|
336
|
-
const hasDegraded = checks.some((check) => check.status === "degraded");
|
|
337
|
-
const report = {
|
|
338
|
-
schema: "mycomesh.consumer.doctor.v1",
|
|
339
|
-
status: hasBlocked ? "blocked" : hasSetup ? "setup_required" : hasDegraded ? "degraded" : "ready",
|
|
340
|
-
release: { version: CONSUMER_RELEASE_VERSION },
|
|
341
|
-
config: { data_dir: dataDir, host: parsed.host, port: parsed.port, base_url: parsed.baseUrl },
|
|
342
|
-
checks,
|
|
343
|
-
};
|
|
344
|
-
if (json) stdout.write(`${JSON.stringify(report)}\n`);
|
|
345
|
-
else {
|
|
346
|
-
stdout.write(`Consumer doctor: ${report.status}\n`);
|
|
347
|
-
for (const check of checks) stdout.write(`${check.status === "ready" ? "✓" : check.status === "setup_required" ? "!" : check.status === "degraded" ? "~" : "✗"} ${check.id}: ${check.message}\n`);
|
|
348
|
-
}
|
|
349
|
-
return report.status === "blocked" ? 1 : 0;
|
|
15
|
+
export function loadNetwork(path) {
|
|
16
|
+
const network = JSON.parse(readFileSync(path, "utf8"));
|
|
17
|
+
if (network.schema !== "mycomesh.v11.network.v1") throw new Error("not a MycoMesh V11 network manifest");
|
|
18
|
+
if (network.tls_ca_file) network.tls_ca = readFileSync(resolve(dirname(path), network.tls_ca_file), "utf8");
|
|
19
|
+
return network;
|
|
350
20
|
}
|
|
351
21
|
|
|
352
|
-
function
|
|
22
|
+
export function httpJson(url, { method = "GET", body, ca, timeoutMs = 330_000 } = {}) {
|
|
353
23
|
const target = new URL(url);
|
|
354
|
-
|
|
355
|
-
const
|
|
24
|
+
const data = body === undefined ? undefined : Buffer.from(JSON.stringify(body));
|
|
25
|
+
const send = target.protocol === "https:" ? httpsRequest : httpRequest;
|
|
356
26
|
return new Promise((resolvePromise, reject) => {
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
status:
|
|
370
|
-
|
|
27
|
+
const req = send(target, {
|
|
28
|
+
method, headers: { accept: "application/json", ...(data ? { "content-type": "application/json", "content-length": data.length } : {}) },
|
|
29
|
+
...(target.protocol === "https:" && ca ? { ca: [...rootCertificates, ca] } : {}),
|
|
30
|
+
}, (res) => {
|
|
31
|
+
const chunks = [];
|
|
32
|
+
let size = 0;
|
|
33
|
+
res.on("data", (chunk) => {
|
|
34
|
+
size += chunk.length;
|
|
35
|
+
if (size > MAX_BODY) { req.destroy(new Error("response too large")); return; }
|
|
36
|
+
chunks.push(chunk);
|
|
37
|
+
});
|
|
38
|
+
res.on("end", () => {
|
|
39
|
+
try { resolvePromise({ status: res.statusCode, body: JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}") }); }
|
|
40
|
+
catch (error) { reject(error); }
|
|
371
41
|
});
|
|
372
42
|
});
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
request.once("close", () => options.signal?.removeEventListener("abort", abort));
|
|
378
|
-
request.end();
|
|
43
|
+
req.setTimeout(timeoutMs, () => req.destroy(new Error("request timed out")));
|
|
44
|
+
req.on("error", reject);
|
|
45
|
+
if (data) req.write(data);
|
|
46
|
+
req.end();
|
|
379
47
|
});
|
|
380
48
|
}
|
|
381
49
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
50
|
+
export class Consumer {
|
|
51
|
+
constructor({ network, keyPrivate, maxFee, journalDir, fetchJson = httpJson, now = () => Math.floor(Date.now() / 1000) }) {
|
|
52
|
+
this.journalDir = journalDir;
|
|
53
|
+
this.network = network;
|
|
54
|
+
this.keyPrivate = keyPrivate;
|
|
55
|
+
this.key = addressOf(keyPrivate);
|
|
56
|
+
this.maxFee = maxFee;
|
|
57
|
+
this.fetchJson = fetchJson;
|
|
58
|
+
this.now = now;
|
|
59
|
+
this.deployment = { chainId: network.chain_id, settlement: network.settlement.toLowerCase() };
|
|
60
|
+
this.cache = new Map();
|
|
61
|
+
}
|
|
388
62
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
if (
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
return
|
|
63
|
+
async providers(relay) {
|
|
64
|
+
const cached = this.cache.get(relay.url);
|
|
65
|
+
if (cached && Date.now() - cached.at < PROVIDER_CACHE_MS) return cached.providers;
|
|
66
|
+
const { status, body } = await this.fetchJson(`${relay.url}/providers`, { ca: this.network.tls_ca, timeoutMs: 10_000 });
|
|
67
|
+
if (status !== 200) throw new Error(`Relay ${relay.url} /providers returned ${status}`);
|
|
68
|
+
const now = this.now();
|
|
69
|
+
const providers = (body.providers || []).filter((descriptor) => {
|
|
70
|
+
try { verifiedProvider(descriptor, this.deployment, now); return true; } catch { return false; }
|
|
71
|
+
});
|
|
72
|
+
this.cache.set(relay.url, { at: Date.now(), providers });
|
|
73
|
+
return providers;
|
|
400
74
|
}
|
|
401
|
-
return false;
|
|
402
|
-
}
|
|
403
75
|
|
|
404
|
-
async
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
try { await state.chooseRelay(); return; } catch (error) {
|
|
409
|
-
if (Date.now() - started >= timeoutSeconds * 1000) throw new ConsumerCliError(`timed out waiting for a healthy Relay: ${error.message}`);
|
|
410
|
-
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
76
|
+
async models() {
|
|
77
|
+
const names = new Set();
|
|
78
|
+
for (const relay of this.network.relays) {
|
|
79
|
+
try { for (const provider of await this.providers(relay)) for (const model of provider.models || []) names.add(model); } catch {}
|
|
411
80
|
}
|
|
81
|
+
return [...names].sort();
|
|
412
82
|
}
|
|
413
|
-
}
|
|
414
83
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
84
|
+
/** Try Relays in order; fail over only while the request provably was not dispatched. */
|
|
85
|
+
async request({ endpoint, model, content, maxOutputTokens = 4096, options = {}, provider }) {
|
|
86
|
+
let lastError;
|
|
87
|
+
for (const relay of this.network.relays) {
|
|
88
|
+
let candidates;
|
|
89
|
+
try {
|
|
90
|
+
candidates = (await this.providers(relay)).filter((descriptor) => (descriptor.models || []).includes(model)
|
|
91
|
+
&& (!provider || descriptor.provider_signer === provider.toLowerCase()));
|
|
92
|
+
}
|
|
93
|
+
catch (error) { lastError = error; continue; }
|
|
94
|
+
candidates.sort((a, b) => (a.prices.output_per_1k - b.prices.output_per_1k) || (a.prices.input_per_1k - b.prices.input_per_1k));
|
|
95
|
+
for (const descriptor of candidates) {
|
|
96
|
+
const prepared = prepareRequest({
|
|
97
|
+
descriptor, deployment: this.deployment, keyPrivate: this.keyPrivate, relaySigner: relay.signer,
|
|
98
|
+
endpoint, model, content, maxOutputTokens, maxFee: this.maxFee, options, now: this.now(),
|
|
99
|
+
});
|
|
100
|
+
let reply;
|
|
101
|
+
try {
|
|
102
|
+
reply = await this.fetchJson(`${relay.url}/v11/requests`, { method: "POST", body: prepared.payload, ca: this.network.tls_ca });
|
|
103
|
+
} catch (error) {
|
|
104
|
+
// The request may have reached the Relay: never replay it elsewhere.
|
|
105
|
+
throw Object.assign(new Error(`request outcome unknown: ${error.message}`), { code: "outcome_unknown", requestId: prepared.authorization.request_id });
|
|
106
|
+
}
|
|
107
|
+
if (reply.status === 200) {
|
|
108
|
+
const opened = openResponse(prepared, reply.body, this.deployment, this.now());
|
|
109
|
+
// Only this machine can ever reveal the plaintexts, so keep them for the dispute window.
|
|
110
|
+
if (this.journalDir) recordRequest(this.journalDir, prepared, opened, relay.url);
|
|
111
|
+
return opened;
|
|
112
|
+
}
|
|
113
|
+
lastError = Object.assign(new Error(reply.body?.error || `Relay returned ${reply.status}`), { status: reply.status });
|
|
114
|
+
if (reply.body?.dispatched !== false || ![502, 503].includes(reply.status)) throw lastError;
|
|
115
|
+
}
|
|
421
116
|
}
|
|
422
|
-
|
|
117
|
+
throw lastError || new Error(`no Provider serves model ${model}`);
|
|
423
118
|
}
|
|
424
119
|
}
|
|
425
120
|
|
|
426
|
-
function
|
|
427
|
-
return
|
|
428
|
-
const args = [
|
|
429
|
-
"-c", 'model="gpt-5.5"',
|
|
430
|
-
"-c", 'model_provider="mycomesh"',
|
|
431
|
-
"-c", 'model_providers.mycomesh.name="MycoMesh"',
|
|
432
|
-
"-c", `model_providers.mycomesh.base_url="${state.baseUrl}"`,
|
|
433
|
-
"-c", 'model_providers.mycomesh.env_key="OPENAI_API_KEY"',
|
|
434
|
-
"-c", 'model_providers.mycomesh.wire_api="responses"',
|
|
435
|
-
...parsed.codexArgs,
|
|
436
|
-
];
|
|
437
|
-
const env = { ...process.env, OPENAI_BASE_URL: state.baseUrl, OPENAI_API_KEY: state.paymentKey };
|
|
438
|
-
let child;
|
|
439
|
-
try { child = spawn(parsed.codexCommand, args, { env, stdio: "inherit" }); }
|
|
440
|
-
catch (error) { stderr.write(`mycomesh consumer: could not start Codex: ${error.message}\n`); resolve(127); return; }
|
|
441
|
-
stdout.write("Opening Codex through the native MycoMesh Consumer.\n");
|
|
442
|
-
child.once("error", (error) => { stderr.write(`mycomesh consumer: Codex failed: ${error.message}\n`); resolve(127); });
|
|
443
|
-
child.once("exit", (code, signal) => resolve(typeof code === "number" ? code : 128 + ({ SIGINT: 2, SIGTERM: 15 }[signal] || 1)));
|
|
444
|
-
});
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
function writePid(dataDir) {
|
|
448
|
-
try { writeFileSync(join(dataDir, "consumer.pid"), `${process.pid}\n`, { mode: 0o600 }); } catch {}
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
function clearPid(dataDir) {
|
|
452
|
-
try { unlinkSync(join(dataDir, "consumer.pid")); } catch {}
|
|
121
|
+
function openaiError(message, code = "mycomesh_error") {
|
|
122
|
+
return { error: { message, type: "mycomesh_error", code } };
|
|
453
123
|
}
|
|
454
124
|
|
|
455
|
-
function
|
|
456
|
-
const
|
|
457
|
-
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
clearPid(parsed.dataDir);
|
|
463
|
-
return 0;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
async function resetConsumer(parsed, env, stdout, stderr) {
|
|
467
|
-
const confirmed = env.MYCOMESH_CONFIRM_RESET === "RESET";
|
|
468
|
-
if (!confirmed && process.stdin.isTTY) {
|
|
469
|
-
const prompt = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
470
|
-
const answer = await prompt.question("This removes the local payment key and history. Type RESET to continue: ");
|
|
471
|
-
prompt.close();
|
|
472
|
-
if (answer !== "RESET") throw new ConsumerCliError("local Consumer reset cancelled", 2);
|
|
473
|
-
} else if (!confirmed) {
|
|
474
|
-
throw new ConsumerCliError("set MYCOMESH_CONFIRM_RESET=RESET to reset a non-interactive Consumer", 2);
|
|
125
|
+
async function readJson(req) {
|
|
126
|
+
const chunks = [];
|
|
127
|
+
let size = 0;
|
|
128
|
+
for await (const chunk of req) {
|
|
129
|
+
size += chunk.length;
|
|
130
|
+
if (size > MAX_BODY) throw new Error("request body too large");
|
|
131
|
+
chunks.push(chunk);
|
|
475
132
|
}
|
|
476
|
-
|
|
477
|
-
rmSync(parsed.dataDir, { recursive: true, force: true });
|
|
478
|
-
stdout.write("Native Consumer local state removed.\n");
|
|
479
|
-
return 0;
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
function openBrowser(url, stderr) {
|
|
483
|
-
const platform = process.platform;
|
|
484
|
-
let command;
|
|
485
|
-
let args;
|
|
486
|
-
if (platform === "darwin") { command = "open"; args = [url]; }
|
|
487
|
-
else if (platform === "win32") { command = "cmd"; args = ["/c", "start", "", url]; }
|
|
488
|
-
else { command = "xdg-open"; args = [url]; }
|
|
489
|
-
try {
|
|
490
|
-
const child = defaultSpawn(command, args, { stdio: "ignore", detached: true });
|
|
491
|
-
child.unref?.();
|
|
492
|
-
} catch { stderr.write(`Open ${url} in a browser.\n`); }
|
|
133
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}");
|
|
493
134
|
}
|
|
494
135
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
const
|
|
499
|
-
|
|
500
|
-
|
|
136
|
+
/** Local OpenAI-compatible endpoint; nothing leaves this machine unsealed except pricing and routing. */
|
|
137
|
+
export function serveConsumer(consumer, { host = "127.0.0.1", port = 8110, apiKey } = {}) {
|
|
138
|
+
const server = createServer(async (req, res) => {
|
|
139
|
+
const send = (status, body, type = "application/json") => {
|
|
140
|
+
res.writeHead(status, { "content-type": type, "cache-control": "no-store" });
|
|
141
|
+
res.end(type === "application/json" ? JSON.stringify(body) : body);
|
|
142
|
+
};
|
|
143
|
+
try {
|
|
144
|
+
if (apiKey && req.headers.authorization !== `Bearer ${apiKey}`) return send(401, openaiError("invalid API key", "unauthorized"));
|
|
145
|
+
const path = new URL(req.url, "http://localhost").pathname.replace(/^\/v1\/v1\//, "/v1/");
|
|
146
|
+
if (req.method === "GET" && path === "/health") return send(200, { ok: true, protocol: 11, key: consumer.key });
|
|
147
|
+
if (req.method === "GET" && path === "/v1/models") {
|
|
148
|
+
return send(200, { object: "list", data: (await consumer.models()).map((id) => ({ id, object: "model", owned_by: "mycomesh" })) });
|
|
149
|
+
}
|
|
150
|
+
if (req.method !== "POST" || !["/v1/responses", "/v1/chat/completions", "/responses", "/chat/completions"].includes(path)) {
|
|
151
|
+
return send(404, openaiError("not found", "not_found"));
|
|
152
|
+
}
|
|
153
|
+
const body = await readJson(req);
|
|
154
|
+
const chat = path.endsWith("/chat/completions");
|
|
155
|
+
const { model, stream, stream_options: streamOptions, ...rest } = body;
|
|
156
|
+
const content = chat ? rest.messages : rest.input;
|
|
157
|
+
delete rest.messages;
|
|
158
|
+
delete rest.input;
|
|
159
|
+
const maxOutputTokens = rest.max_output_tokens ?? rest.max_tokens ?? rest.max_completion_tokens ?? 4096;
|
|
160
|
+
delete rest.max_output_tokens;
|
|
161
|
+
delete rest.max_tokens;
|
|
162
|
+
delete rest.max_completion_tokens;
|
|
163
|
+
const { response } = await consumer.request({ endpoint: chat ? "chat" : "responses", model, content, maxOutputTokens, options: rest });
|
|
164
|
+
const output = response.output;
|
|
165
|
+
if (stream) return send(200, chat ? chatSse(output, { includeUsage: streamOptions?.include_usage === true }) : responseSse(output), "text/event-stream");
|
|
166
|
+
return send(200, output);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
const status = error.code === "outcome_unknown" ? 504 : (error.status && error.status < 500 ? error.status : 502);
|
|
169
|
+
return send(status, openaiError(error.message, error.code || "request_failed"));
|
|
170
|
+
}
|
|
501
171
|
});
|
|
172
|
+
return new Promise((resolvePromise) => server.listen(port, host, () => resolvePromise(server)));
|
|
502
173
|
}
|