run402 4.16.0 → 4.17.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 +1 -1
- package/cli.mjs +1 -1
- package/lib/buzz-doctor-contract.mjs +97 -0
- package/lib/buzz-doctor-contract.test.mjs +127 -0
- package/lib/buzz-doctor.mjs +799 -0
- package/lib/buzz-doctor.test.mjs +217 -0
- package/lib/buzz.mjs +48 -3
- package/lib/buzz.test.mjs +63 -0
- package/lib/command-manifest.mjs +2 -1
- package/lib/diagnostic-telemetry-worker.mjs +4 -0
- package/lib/diagnostic-telemetry.mjs +300 -0
- package/lib/diagnostic-telemetry.test.mjs +181 -0
- package/lib/doctor.mjs +30 -0
- package/lib/wallet-context.mjs +1 -1
- package/package.json +1 -1
- package/sdk/dist/index.d.ts +1 -1
- package/sdk/dist/index.d.ts.map +1 -1
- package/sdk/dist/index.js +1 -1
- package/sdk/dist/index.js.map +1 -1
- package/sdk/dist/namespaces/buzz.d.ts +15 -1
- package/sdk/dist/namespaces/buzz.d.ts.map +1 -1
- package/sdk/dist/namespaces/buzz.js +49 -0
- package/sdk/dist/namespaces/buzz.js.map +1 -1
- package/sdk/dist/namespaces/buzz.types.d.ts +72 -1
- package/sdk/dist/namespaces/buzz.types.d.ts.map +1 -1
|
@@ -0,0 +1,799 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { accessSync, constants as fsConstants, existsSync, readFileSync, realpathSync } from "node:fs";
|
|
3
|
+
import { lookup as dnsLookup } from "node:dns/promises";
|
|
4
|
+
import { request as httpsRequest } from "node:https";
|
|
5
|
+
import { delimiter, extname, join } from "node:path";
|
|
6
|
+
import ipaddr from "ipaddr.js";
|
|
7
|
+
import { bech32 } from "@scure/base";
|
|
8
|
+
import { activeProfile, apiBase } from "./config.mjs";
|
|
9
|
+
import { currentRun402Version, detectInstallContext, doctorUpdateCheck } from "./update-check.mjs";
|
|
10
|
+
import { profileExists, readMeta } from "../core-dist/profiles.js";
|
|
11
|
+
import {
|
|
12
|
+
BUZZ_DOCTOR_CHECK_ORDER,
|
|
13
|
+
BUZZ_DOCTOR_CONTRACT,
|
|
14
|
+
BUZZ_DOCTOR_CONTRACT_ID,
|
|
15
|
+
} from "./buzz-doctor-contract.mjs";
|
|
16
|
+
|
|
17
|
+
const BUZZ_CAPABILITIES = JSON.parse(readFileSync(
|
|
18
|
+
new URL("../../buzz/fixtures/buzz-v0.5.2-cli-capabilities.json", import.meta.url),
|
|
19
|
+
"utf8",
|
|
20
|
+
));
|
|
21
|
+
|
|
22
|
+
export const BUZZ_DOCTOR_TIMEOUT_MS = 3_000;
|
|
23
|
+
export const BUZZ_DOCTOR_MAX_RESPONSE_BYTES = 64 * 1024;
|
|
24
|
+
export const BUZZ_DOCTOR_MIN_NODE_MAJOR = 22;
|
|
25
|
+
export const BUZZ_DOCTOR_MIN_RUN402_VERSION = "4.16.1";
|
|
26
|
+
|
|
27
|
+
const SPEND_NONE = Object.freeze({ currency: "USD", max_amount: "0" });
|
|
28
|
+
const SAFE_TOKEN = /^[A-Za-z0-9_./:=@+,-]+$/;
|
|
29
|
+
|
|
30
|
+
export const BUZZ_DOCTOR_REPAIR_MATRIX = Object.freeze({
|
|
31
|
+
BUZZ_PREFLIGHT_SHELL_UNAVAILABLE: { type: "repair_buzz_agent_runtime", surface: "buzz_settings" },
|
|
32
|
+
BUZZ_PREFLIGHT_NODE_UNAVAILABLE: { type: "repair_buzz_node_runtime", surface: "buzz_settings" },
|
|
33
|
+
BUZZ_PREFLIGHT_NODE_INCOMPATIBLE: { type: "repair_buzz_node_runtime", surface: "buzz_settings" },
|
|
34
|
+
BUZZ_PREFLIGHT_RUN402_UNAVAILABLE: { type: "install_run402_cli", surface: "shell" },
|
|
35
|
+
BUZZ_PREFLIGHT_RUN402_INCOMPATIBLE: { type: ["upgrade_run402_cli", "inspect_run402_cli", "install_run402_cli"], surface: "shell" },
|
|
36
|
+
BUZZ_PREFLIGHT_RUN402_UPDATE_AVAILABLE: { type: "upgrade_run402_cli", surface: "shell" },
|
|
37
|
+
BUZZ_PREFLIGHT_BUZZ_CLI_UNAVAILABLE: { type: "repair_buzz_cli_sidecar", surface: "buzz_settings" },
|
|
38
|
+
BUZZ_PREFLIGHT_BUZZ_CLI_INCOMPATIBLE: { type: "repair_buzz_cli_sidecar", surface: "buzz_settings" },
|
|
39
|
+
BUZZ_AGENT_TARGET_REQUIRED: { type: "rerun_buzz_setup_with_explicit_identity", surface: "buzz_chat" },
|
|
40
|
+
BUZZ_AGENT_TARGET_UNVERIFIED: { type: ["publish_buzz_public_profile", "repair_buzz_cli_sidecar"], surface: ["buzz_chat", "buzz_settings"] },
|
|
41
|
+
BUZZ_AGENT_TARGET_MISMATCH: { type: "select_expected_buzz_agent", surface: "buzz_chat" },
|
|
42
|
+
BUZZ_PREFLIGHT_API_UNREACHABLE: { type: "restore_run402_api_access", surface: "buzz_settings" },
|
|
43
|
+
BUZZ_PREFLIGHT_CONSOLE_UNREACHABLE: { type: "restore_run402_console_access", surface: "buzz_settings" },
|
|
44
|
+
BUZZ_PREFLIGHT_RELAY_UNSAFE: { type: "repair_buzz_relay_url", surface: "buzz_settings" },
|
|
45
|
+
BUZZ_PREFLIGHT_RELAY_UNREACHABLE: { type: "restore_buzz_relay_access", surface: "buzz_settings" },
|
|
46
|
+
BUZZ_PREFLIGHT_WALLET_PROFILE_REQUIRED: { type: "rerun_buzz_setup_with_explicit_identity", surface: "buzz_chat" },
|
|
47
|
+
BUZZ_PREFLIGHT_WALLET_PROFILE_NOT_FOUND: { type: "create_buzz_wallet_profile", surface: "shell" },
|
|
48
|
+
BUZZ_PREFLIGHT_WALLET_PROFILE_MISMATCH: { type: "rerun_buzz_doctor", surface: "shell" },
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export function normalizeNostrSubject(value) {
|
|
52
|
+
if (typeof value !== "string") return null;
|
|
53
|
+
const normalized = value.trim().toLowerCase();
|
|
54
|
+
if (/^[0-9a-f]{64}$/.test(normalized)) return normalized;
|
|
55
|
+
if (!normalized.startsWith("npub1")) return null;
|
|
56
|
+
try {
|
|
57
|
+
const decoded = bech32.decode(normalized);
|
|
58
|
+
if (decoded.prefix !== "npub") return null;
|
|
59
|
+
const bytes = Uint8Array.from(bech32.fromWords(decoded.words));
|
|
60
|
+
return bytes.length === 32 ? Buffer.from(bytes).toString("hex") : null;
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function parseBuzzDoctorArgs(all = []) {
|
|
67
|
+
const buzzIndexes = indexesOf(all, "--buzz");
|
|
68
|
+
const agentIndexes = indexesOf(all, "--buzz-agent");
|
|
69
|
+
if (agentIndexes.length > 0 && buzzIndexes.length === 0) {
|
|
70
|
+
return { error: usageError("BUZZ_MODE_REQUIRED", "--buzz-agent requires --buzz", "--buzz-agent") };
|
|
71
|
+
}
|
|
72
|
+
if (buzzIndexes.length > 1) return { error: usageError("DUPLICATE_FLAG", "--buzz may be supplied only once", "--buzz") };
|
|
73
|
+
if (agentIndexes.length > 1) return { error: usageError("DUPLICATE_FLAG", "--buzz-agent may be supplied only once", "--buzz-agent") };
|
|
74
|
+
if (buzzIndexes.length === 0) return { buzz: false, expectedSubjectHex: null };
|
|
75
|
+
let expectedSubjectHex = null;
|
|
76
|
+
if (agentIndexes.length === 1) {
|
|
77
|
+
const raw = all[agentIndexes[0] + 1];
|
|
78
|
+
if (!raw || raw.startsWith("--")) {
|
|
79
|
+
return { error: usageError("BAD_FLAG", "--buzz-agent requires an npub or 64-character hex value", "--buzz-agent") };
|
|
80
|
+
}
|
|
81
|
+
expectedSubjectHex = normalizeNostrSubject(raw);
|
|
82
|
+
if (!expectedSubjectHex) {
|
|
83
|
+
return { error: usageError("BAD_BUZZ_AGENT", "--buzz-agent must be an npub or 64-character hex public key", "--buzz-agent") };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return { buzz: true, expectedSubjectHex };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function buildBuzzDoctorReport({
|
|
90
|
+
expectedSubjectHex = null,
|
|
91
|
+
env = process.env,
|
|
92
|
+
argv = process.argv,
|
|
93
|
+
execPath = process.execPath,
|
|
94
|
+
cliExecutable = process.argv[1],
|
|
95
|
+
cwd = process.cwd(),
|
|
96
|
+
platform = process.platform,
|
|
97
|
+
now = Date.now(),
|
|
98
|
+
runCommand = defaultRunCommand,
|
|
99
|
+
lookup = dnsLookup,
|
|
100
|
+
pinnedRelayRead = defaultPinnedRelayRead,
|
|
101
|
+
originProbe = defaultOriginProbe,
|
|
102
|
+
updateCheck = doctorUpdateCheck,
|
|
103
|
+
getActiveProfile = activeProfile,
|
|
104
|
+
profileExistsImpl = profileExists,
|
|
105
|
+
readMetaImpl = readMeta,
|
|
106
|
+
apiOrigin = apiBase(),
|
|
107
|
+
consoleOrigin = "https://console.run402.com",
|
|
108
|
+
} = {}) {
|
|
109
|
+
const shellPath = configuredShell(env, platform);
|
|
110
|
+
const nodeExecutable = safeRealpath(execPath) ?? execPath ?? null;
|
|
111
|
+
const run402Executable = safeRealpath(cliExecutable) ?? cliExecutable ?? null;
|
|
112
|
+
const walletProfile = getActiveProfile();
|
|
113
|
+
const relay = parseRelayOrigin(env.BUZZ_RELAY_URL);
|
|
114
|
+
const binding = {
|
|
115
|
+
contract_id: BUZZ_DOCTOR_CONTRACT_ID,
|
|
116
|
+
expected_subject_hex: expectedSubjectHex,
|
|
117
|
+
wallet_profile: walletProfile,
|
|
118
|
+
node_executable: nodeExecutable,
|
|
119
|
+
run402_executable: run402Executable,
|
|
120
|
+
relay_origin: relay?.origin ?? null,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const sessionShell = checkSessionShell({ shellPath, platform, runCommand });
|
|
124
|
+
const nodeRuntime = checkNodeRuntime({ nodeExecutable, runCommand });
|
|
125
|
+
const run402Cli = await checkRun402Cli({ cwd, env, argv, run402Executable, updateCheck });
|
|
126
|
+
const buzzExecutable = findExecutable("buzz", env, platform);
|
|
127
|
+
const buzzCli = checkBuzzCli({ buzzExecutable, runCommand });
|
|
128
|
+
|
|
129
|
+
const [buzzAgentTarget, run402Api, run402Console, buzzRelay, walletProfileCheck] = await Promise.all([
|
|
130
|
+
checkBuzzAgentTarget({ expectedSubjectHex, buzzExecutable, buzzCli, runCommand }),
|
|
131
|
+
checkOrigin("run402_api", apiOrigin, "BUZZ_PREFLIGHT_API_UNREACHABLE", originProbe, apiRepair(apiOrigin)),
|
|
132
|
+
checkOrigin("run402_console", consoleOrigin, "BUZZ_PREFLIGHT_CONSOLE_UNREACHABLE", originProbe, consoleRepair()),
|
|
133
|
+
checkBuzzRelay({ relay, lookup, pinnedRelayRead }),
|
|
134
|
+
Promise.resolve(checkWalletProfile({ walletProfile, env, profileExistsImpl, readMetaImpl, expectedSubjectHex })),
|
|
135
|
+
]);
|
|
136
|
+
|
|
137
|
+
const checks = [
|
|
138
|
+
sessionShell,
|
|
139
|
+
nodeRuntime,
|
|
140
|
+
run402Cli,
|
|
141
|
+
buzzCli,
|
|
142
|
+
buzzAgentTarget,
|
|
143
|
+
run402Api,
|
|
144
|
+
run402Console,
|
|
145
|
+
buzzRelay,
|
|
146
|
+
walletProfileCheck,
|
|
147
|
+
];
|
|
148
|
+
if (checks.some((check, index) => check.name !== BUZZ_DOCTOR_CHECK_ORDER[index])) {
|
|
149
|
+
throw new Error("Buzz doctor check order drifted from the frozen contract");
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
ok: checks.every((check) => check.status !== "blocked"),
|
|
153
|
+
mode: "buzz",
|
|
154
|
+
contract_id: BUZZ_DOCTOR_CONTRACT_ID,
|
|
155
|
+
generated_at: new Date(now).toISOString(),
|
|
156
|
+
mutation_state: BUZZ_DOCTOR_CONTRACT.zero_mutation.mutation_state,
|
|
157
|
+
binding,
|
|
158
|
+
checks,
|
|
159
|
+
telemetry: {
|
|
160
|
+
status: env.RUN402_TELEMETRY === "0" ? "disabled" : "not_configured",
|
|
161
|
+
queued: false,
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function checkSessionShell({ shellPath, platform, runCommand }) {
|
|
167
|
+
if (!shellPath) return blocked("session_shell", "BUZZ_PREFLIGHT_SHELL_UNAVAILABLE", "The managed session has no configured shell executable.", shellRepair());
|
|
168
|
+
const args = platform === "win32" ? ["/d", "/s", "/c", "exit", "0"] : ["-c", ":"];
|
|
169
|
+
const result = runCommand(shellPath, args, { timeout: BUZZ_DOCTOR_TIMEOUT_MS, shell: false });
|
|
170
|
+
if (!commandPassed(result)) {
|
|
171
|
+
return blocked("session_shell", "BUZZ_PREFLIGHT_SHELL_UNAVAILABLE", "The configured shell cannot execute a bounded no-op.", shellRepair(), {
|
|
172
|
+
shell: safePath(shellPath),
|
|
173
|
+
failure: commandFailureClass(result),
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
return ok("session_shell", { shell: safePath(shellPath) });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function checkNodeRuntime({ nodeExecutable, runCommand }) {
|
|
180
|
+
if (!nodeExecutable) return blocked("node_runtime", "BUZZ_PREFLIGHT_NODE_UNAVAILABLE", "The managed session has no Node executable.", nodeRepair());
|
|
181
|
+
const result = runCommand(nodeExecutable, ["--version"], { timeout: BUZZ_DOCTOR_TIMEOUT_MS, shell: false });
|
|
182
|
+
if (!commandPassed(result)) {
|
|
183
|
+
return blocked("node_runtime", "BUZZ_PREFLIGHT_NODE_UNAVAILABLE", "The exact Node executable cannot be spawned.", nodeRepair(), {
|
|
184
|
+
executable: safePath(nodeExecutable),
|
|
185
|
+
failure: commandFailureClass(result),
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
const version = String(result.stdout ?? "").trim();
|
|
189
|
+
const major = Number(version.match(/^v?(\d+)/)?.[1]);
|
|
190
|
+
if (!Number.isSafeInteger(major) || major < BUZZ_DOCTOR_MIN_NODE_MAJOR) {
|
|
191
|
+
return blocked("node_runtime", "BUZZ_PREFLIGHT_NODE_INCOMPATIBLE", `Node ${BUZZ_DOCTOR_MIN_NODE_MAJOR}+ is required by this Run402 package.`, nodeRepair(), {
|
|
192
|
+
executable: safePath(nodeExecutable),
|
|
193
|
+
version: version || null,
|
|
194
|
+
required_major: BUZZ_DOCTOR_MIN_NODE_MAJOR,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
return ok("node_runtime", { executable: safePath(nodeExecutable), version, major, compatibility_source: "version" });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function checkRun402Cli({ cwd, env, argv, run402Executable, updateCheck }) {
|
|
201
|
+
if (!run402Executable) {
|
|
202
|
+
return blocked("run402_cli", "BUZZ_PREFLIGHT_RUN402_UNAVAILABLE", "The Run402 CLI executable is unavailable.", run402InstallRepair());
|
|
203
|
+
}
|
|
204
|
+
const version = currentRun402Version();
|
|
205
|
+
const install = detectInstallContext({ cwd, env, argv, execPath: run402Executable });
|
|
206
|
+
if (compareMajorMinorPatch(version, BUZZ_DOCTOR_MIN_RUN402_VERSION) < 0) {
|
|
207
|
+
return blocked("run402_cli", "BUZZ_PREFLIGHT_RUN402_INCOMPATIBLE", "The executing Run402 CLI predates the Buzz doctor contract.", run402RepairForInstall(install, cwd), {
|
|
208
|
+
version,
|
|
209
|
+
minimum_version: BUZZ_DOCTOR_MIN_RUN402_VERSION,
|
|
210
|
+
executable: safePath(run402Executable),
|
|
211
|
+
install_context: install.kind,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
let update = null;
|
|
215
|
+
try {
|
|
216
|
+
update = await updateCheck({ refresh: false, cwd, env, argv, execPath: run402Executable, current: version });
|
|
217
|
+
} catch {
|
|
218
|
+
update = null;
|
|
219
|
+
}
|
|
220
|
+
const value = {
|
|
221
|
+
version,
|
|
222
|
+
executable: safePath(run402Executable),
|
|
223
|
+
install_context: install.kind,
|
|
224
|
+
install_confidence: install.confidence,
|
|
225
|
+
package_manager: install.package_manager,
|
|
226
|
+
compatibility_source: "version_and_capability_probe",
|
|
227
|
+
required_capabilities: ["doctor --buzz", "doctor --buzz-agent", "wallets current", "org whoami", "identity link nostr"],
|
|
228
|
+
};
|
|
229
|
+
if (update?.status === "warning") {
|
|
230
|
+
const rawAction = update?.value?.next_actions?.[0];
|
|
231
|
+
return warning("run402_cli", "BUZZ_PREFLIGHT_RUN402_UPDATE_AVAILABLE", "A newer compatible Run402 CLI is available; setup may continue.", normalizeUpgradeAction(rawAction, install, cwd), value);
|
|
232
|
+
}
|
|
233
|
+
return ok("run402_cli", value);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function checkBuzzCli({ buzzExecutable, runCommand }) {
|
|
237
|
+
if (!buzzExecutable) {
|
|
238
|
+
return blocked("buzz_cli", "BUZZ_PREFLIGHT_BUZZ_CLI_UNAVAILABLE", "The managed Buzz CLI sidecar is unavailable.", buzzSidecarRepair());
|
|
239
|
+
}
|
|
240
|
+
const observations = [];
|
|
241
|
+
for (const probe of BUZZ_CAPABILITIES.help_probes) {
|
|
242
|
+
const args = probe.argv.slice(1);
|
|
243
|
+
const result = runCommand(buzzExecutable, args, { timeout: BUZZ_DOCTOR_TIMEOUT_MS, shell: false });
|
|
244
|
+
const output = String(result?.stdout ?? "");
|
|
245
|
+
const compatible = commandPassed(result) && probe.stdout_contains.every((fragment) => output.includes(fragment));
|
|
246
|
+
observations.push({ capability: args.join(" "), compatible });
|
|
247
|
+
if (!compatible) {
|
|
248
|
+
return blocked("buzz_cli", "BUZZ_PREFLIGHT_BUZZ_CLI_INCOMPATIBLE", "The managed Buzz CLI lacks a released capability required by setup.", buzzSidecarRepair(), {
|
|
249
|
+
executable: safePath(buzzExecutable),
|
|
250
|
+
fixture_id: BUZZ_CAPABILITIES.fixture_id,
|
|
251
|
+
compatibility_source: "capability_probe",
|
|
252
|
+
observations,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return ok("buzz_cli", {
|
|
257
|
+
executable: safePath(buzzExecutable),
|
|
258
|
+
fixture_id: BUZZ_CAPABILITIES.fixture_id,
|
|
259
|
+
install_context: "managed_buzz_sidecar",
|
|
260
|
+
compatibility_source: "capability_probe",
|
|
261
|
+
version_status: "version_unknown_capabilities_ok",
|
|
262
|
+
observations,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function checkBuzzAgentTarget({ expectedSubjectHex, buzzExecutable, buzzCli, runCommand }) {
|
|
267
|
+
if (!expectedSubjectHex) {
|
|
268
|
+
return blocked("buzz_agent_target", "BUZZ_AGENT_TARGET_REQUIRED", "Buzz setup requires the intended agent's public npub or hex subject.", targetRequiredRepair());
|
|
269
|
+
}
|
|
270
|
+
if (buzzCli.status === "blocked" || !buzzExecutable) {
|
|
271
|
+
return blocked("buzz_agent_target", "BUZZ_AGENT_TARGET_UNVERIFIED", "The active Buzz agent cannot be proven until the managed Buzz CLI is repaired.", buzzSidecarRepair(), {
|
|
272
|
+
expected_subject_hex: expectedSubjectHex,
|
|
273
|
+
observed_subject_hex: null,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
const result = runCommand(buzzExecutable, ["users", "get"], { timeout: BUZZ_DOCTOR_TIMEOUT_MS, shell: false });
|
|
277
|
+
if (!commandPassed(result)) {
|
|
278
|
+
return blocked("buzz_agent_target", "BUZZ_AGENT_TARGET_UNVERIFIED", "The released Buzz public self query did not return a verifiable profile.", targetUnverifiedRepair(), {
|
|
279
|
+
expected_subject_hex: expectedSubjectHex,
|
|
280
|
+
observed_subject_hex: null,
|
|
281
|
+
failure: commandFailureClass(result),
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
let parsed;
|
|
285
|
+
try { parsed = JSON.parse(String(result.stdout ?? "")); }
|
|
286
|
+
catch { parsed = null; }
|
|
287
|
+
const observed = Array.isArray(parsed) && parsed.length === 1 ? normalizeNostrSubject(parsed[0]?.pubkey) : null;
|
|
288
|
+
if (!observed) {
|
|
289
|
+
return blocked("buzz_agent_target", "BUZZ_AGENT_TARGET_UNVERIFIED", "The released Buzz public self query returned no single public signer profile.", targetUnverifiedRepair(), {
|
|
290
|
+
expected_subject_hex: expectedSubjectHex,
|
|
291
|
+
observed_subject_hex: null,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
if (observed !== expectedSubjectHex) {
|
|
295
|
+
return blocked("buzz_agent_target", "BUZZ_AGENT_TARGET_MISMATCH", "The active Buzz CLI identity is not the explicitly requested managed agent.", targetMismatchRepair(), {
|
|
296
|
+
expected_subject_hex: expectedSubjectHex,
|
|
297
|
+
observed_subject_hex: observed,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
return ok("buzz_agent_target", { expected_subject_hex: expectedSubjectHex, observed_subject_hex: observed, observation: "buzz users get" });
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function checkOrigin(name, origin, code, originProbe, repair) {
|
|
304
|
+
try {
|
|
305
|
+
const url = new URL("/status", origin);
|
|
306
|
+
if (name === "run402_console") url.pathname = "/";
|
|
307
|
+
const result = await originProbe(url, { timeoutMs: BUZZ_DOCTOR_TIMEOUT_MS, maxBytes: BUZZ_DOCTOR_MAX_RESPONSE_BYTES });
|
|
308
|
+
if (!result?.ok) throw new Error(result?.failure ?? `http_${result?.status ?? "unknown"}`);
|
|
309
|
+
return ok(name, { origin: new URL(origin).origin, elapsed_ms: result.elapsed_ms ?? null });
|
|
310
|
+
} catch (error) {
|
|
311
|
+
return blocked(name, code, `${name === "run402_api" ? "Run402 API" : "Run402 console"} reachability failed.`, repair, {
|
|
312
|
+
origin: safeOrigin(origin),
|
|
313
|
+
failure: safeFailure(error),
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async function checkBuzzRelay({ relay, lookup, pinnedRelayRead }) {
|
|
319
|
+
if (!relay) {
|
|
320
|
+
return blocked("buzz_relay", "BUZZ_PREFLIGHT_RELAY_UNSAFE", "BUZZ_RELAY_URL must be a credential-free public wss:// origin.", relayUnsafeRepair());
|
|
321
|
+
}
|
|
322
|
+
try {
|
|
323
|
+
const records = await lookup(relay.hostname, { all: true, verbatim: true });
|
|
324
|
+
if (!Array.isArray(records) || records.length === 0) throw new Error("dns_empty");
|
|
325
|
+
for (const record of records) {
|
|
326
|
+
if (!isPublicAddress(record.address)) throw new UnsafeRelayError("dns_non_public");
|
|
327
|
+
}
|
|
328
|
+
const result = await pinnedRelayRead(relay, records[0], {
|
|
329
|
+
timeoutMs: BUZZ_DOCTOR_TIMEOUT_MS,
|
|
330
|
+
maxBytes: BUZZ_DOCTOR_MAX_RESPONSE_BYTES,
|
|
331
|
+
});
|
|
332
|
+
if (!result?.ok) throw new Error(result?.failure ?? `http_${result?.status ?? "unknown"}`);
|
|
333
|
+
return ok("buzz_relay", {
|
|
334
|
+
origin: relay.origin,
|
|
335
|
+
elapsed_ms: result.elapsed_ms ?? null,
|
|
336
|
+
compatibility_source: "nip11_public_read",
|
|
337
|
+
address_count: records.length,
|
|
338
|
+
});
|
|
339
|
+
} catch (error) {
|
|
340
|
+
if (error instanceof UnsafeRelayError) {
|
|
341
|
+
return blocked("buzz_relay", "BUZZ_PREFLIGHT_RELAY_UNSAFE", "The configured Buzz relay is not a safe public destination.", relayUnsafeRepair(), {
|
|
342
|
+
origin: relay.origin,
|
|
343
|
+
failure: error.message,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
return blocked("buzz_relay", "BUZZ_PREFLIGHT_RELAY_UNREACHABLE", "The configured safe Buzz relay did not complete its bounded public read.", relayReachabilityRepair(relay.origin), {
|
|
347
|
+
origin: relay.origin,
|
|
348
|
+
failure: safeFailure(error),
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function checkWalletProfile({ walletProfile, env, profileExistsImpl, readMetaImpl, expectedSubjectHex }) {
|
|
354
|
+
const context = parseWalletContext(env.RUN402_ACTIVE_WALLET_JSON);
|
|
355
|
+
if (!walletProfile || walletProfile === "default") {
|
|
356
|
+
return blocked("wallet_profile", "BUZZ_PREFLIGHT_WALLET_PROFILE_REQUIRED", "Buzz setup requires a unique named Run402 wallet profile.", targetRequiredRepair(expectedSubjectHex));
|
|
357
|
+
}
|
|
358
|
+
if (context?.source !== "flag" || context?.sourceDetail !== "--wallet") {
|
|
359
|
+
return blocked("wallet_profile", "BUZZ_PREFLIGHT_WALLET_PROFILE_MISMATCH", "The Run402 profile was not selected by the explicit --wallet argument.", rerunDoctorRepair(walletProfile, expectedSubjectHex), {
|
|
360
|
+
profile_label: walletProfile,
|
|
361
|
+
selection_source: context?.source ?? "unknown",
|
|
362
|
+
selection_source_detail: context?.sourceDetail ?? null,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
if (!profileExistsImpl(walletProfile)) {
|
|
366
|
+
return blocked("wallet_profile", "BUZZ_PREFLIGHT_WALLET_PROFILE_NOT_FOUND", `The dedicated Run402 profile '${walletProfile}' does not exist yet.`, walletCreateRepair(walletProfile), {
|
|
367
|
+
profile_label: walletProfile,
|
|
368
|
+
profile_type: "eoa_wallet",
|
|
369
|
+
initialized: false,
|
|
370
|
+
selection_source: "explicit_argument",
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
const meta = readMetaImpl(walletProfile);
|
|
374
|
+
if (meta?.name && meta.name !== walletProfile) {
|
|
375
|
+
return blocked("wallet_profile", "BUZZ_PREFLIGHT_WALLET_PROFILE_MISMATCH", "The selected profile metadata does not match the explicit profile label.", rerunDoctorRepair(walletProfile, expectedSubjectHex), {
|
|
376
|
+
profile_label: walletProfile,
|
|
377
|
+
observed_label: meta.name,
|
|
378
|
+
initialized: true,
|
|
379
|
+
selection_source: "explicit_argument",
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
return ok("wallet_profile", {
|
|
383
|
+
profile_label: walletProfile,
|
|
384
|
+
profile_type: "eoa_wallet",
|
|
385
|
+
rail: meta?.rail ?? null,
|
|
386
|
+
initialized: true,
|
|
387
|
+
selection_source: "explicit_argument",
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export async function defaultOriginProbe(url, { timeoutMs = BUZZ_DOCTOR_TIMEOUT_MS } = {}) {
|
|
392
|
+
const started = Date.now();
|
|
393
|
+
const controller = new AbortController();
|
|
394
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
395
|
+
try {
|
|
396
|
+
const response = await fetch(url, {
|
|
397
|
+
method: "GET",
|
|
398
|
+
redirect: "manual",
|
|
399
|
+
signal: controller.signal,
|
|
400
|
+
headers: { accept: "application/json,text/html;q=0.5" },
|
|
401
|
+
});
|
|
402
|
+
await response.body?.cancel().catch(() => {});
|
|
403
|
+
return { ok: response.status >= 200 && response.status < 300, status: response.status, elapsed_ms: Date.now() - started };
|
|
404
|
+
} finally {
|
|
405
|
+
clearTimeout(timer);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export function defaultPinnedRelayRead(relay, record, { timeoutMs = BUZZ_DOCTOR_TIMEOUT_MS, maxBytes = BUZZ_DOCTOR_MAX_RESPONSE_BYTES } = {}) {
|
|
410
|
+
return new Promise((resolve, reject) => {
|
|
411
|
+
const started = Date.now();
|
|
412
|
+
const request = httpsRequest({
|
|
413
|
+
protocol: "https:",
|
|
414
|
+
hostname: relay.hostname,
|
|
415
|
+
port: relay.port || 443,
|
|
416
|
+
path: "/",
|
|
417
|
+
method: "GET",
|
|
418
|
+
servername: relay.hostname,
|
|
419
|
+
headers: { accept: "application/nostr+json,application/json" },
|
|
420
|
+
lookup: pinnedLookup(record, relay.hostname),
|
|
421
|
+
timeout: timeoutMs,
|
|
422
|
+
}, (response) => {
|
|
423
|
+
if (response.statusCode >= 300 && response.statusCode < 400) {
|
|
424
|
+
response.destroy();
|
|
425
|
+
reject(new Error("redirect_rejected"));
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
const chunks = [];
|
|
429
|
+
let bytes = 0;
|
|
430
|
+
response.on("data", (chunk) => {
|
|
431
|
+
bytes += chunk.length;
|
|
432
|
+
if (bytes > maxBytes) {
|
|
433
|
+
response.destroy(new Error("response_too_large"));
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
chunks.push(chunk);
|
|
437
|
+
});
|
|
438
|
+
response.on("end", () => {
|
|
439
|
+
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
440
|
+
reject(new Error(`http_${response.statusCode}`));
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
try {
|
|
444
|
+
const body = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
445
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) throw new Error("nip11_invalid");
|
|
446
|
+
resolve({ ok: true, status: response.statusCode, elapsed_ms: Date.now() - started });
|
|
447
|
+
} catch (error) {
|
|
448
|
+
reject(error);
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
});
|
|
452
|
+
request.on("timeout", () => request.destroy(new Error("timeout")));
|
|
453
|
+
request.on("error", reject);
|
|
454
|
+
request.end();
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
export function isPublicAddress(address) {
|
|
459
|
+
try {
|
|
460
|
+
return ipaddr.parse(address).range() === "unicast";
|
|
461
|
+
} catch {
|
|
462
|
+
return false;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
export function pinnedLookup(record, expectedHostname) {
|
|
467
|
+
return (hostname, _options, callback) => {
|
|
468
|
+
if (hostname !== expectedHostname) {
|
|
469
|
+
callback(new Error("relay_hostname_changed"));
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
callback(null, record.address, record.family);
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function defaultRunCommand(command, args, options = {}) {
|
|
477
|
+
return spawnSync(command, args, {
|
|
478
|
+
encoding: "utf8",
|
|
479
|
+
shell: false,
|
|
480
|
+
maxBuffer: BUZZ_DOCTOR_MAX_RESPONSE_BYTES,
|
|
481
|
+
timeout: BUZZ_DOCTOR_TIMEOUT_MS,
|
|
482
|
+
...options,
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function findExecutable(name, env, platform) {
|
|
487
|
+
const pathValue = typeof env.PATH === "string" ? env.PATH : "";
|
|
488
|
+
const extensions = platform === "win32"
|
|
489
|
+
? String(env.PATHEXT || ".EXE;.CMD;.BAT").split(";").filter(Boolean)
|
|
490
|
+
: [""];
|
|
491
|
+
for (const directory of pathValue.split(delimiter).filter(Boolean)) {
|
|
492
|
+
for (const extension of extensions) {
|
|
493
|
+
const candidate = join(directory, platform === "win32" && !extname(name) ? `${name}${extension}` : name);
|
|
494
|
+
try {
|
|
495
|
+
accessSync(candidate, platform === "win32" ? fsConstants.F_OK : fsConstants.X_OK);
|
|
496
|
+
return safeRealpath(candidate) ?? candidate;
|
|
497
|
+
} catch { /* keep searching */ }
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return null;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function configuredShell(env, platform) {
|
|
504
|
+
if (platform === "win32") return env.ComSpec || env.COMSPEC || null;
|
|
505
|
+
return typeof env.SHELL === "string" && env.SHELL.trim() ? env.SHELL.trim() : null;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function parseRelayOrigin(raw) {
|
|
509
|
+
if (typeof raw !== "string" || raw.trim() === "") return null;
|
|
510
|
+
try {
|
|
511
|
+
const url = new URL(raw);
|
|
512
|
+
if (url.protocol !== "wss:" || url.username || url.password || url.pathname !== "/" || url.search || url.hash) return null;
|
|
513
|
+
if (url.hostname === "localhost" || ipaddr.isValid(url.hostname)) return null;
|
|
514
|
+
return { origin: url.origin, hostname: url.hostname, port: url.port ? Number(url.port) : 443 };
|
|
515
|
+
} catch {
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function blocked(name, code, message, action, value) {
|
|
521
|
+
assertRepairMatches(code, action);
|
|
522
|
+
return { name, status: "blocked", code, ...(value ? { value } : {}), message, next_actions: [action] };
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function warning(name, code, message, action, value) {
|
|
526
|
+
assertRepairMatches(code, action);
|
|
527
|
+
return { name, status: "warning", code, ...(value ? { value } : {}), message, next_actions: [action] };
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function ok(name, value) {
|
|
531
|
+
return { name, status: "ok", ...(value ? { value } : {}) };
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function shellAction({ type, argv, why, cwd, safeToAutoExecute = false, requiresApproval = true }) {
|
|
535
|
+
return {
|
|
536
|
+
type,
|
|
537
|
+
surface: "shell",
|
|
538
|
+
command: shellCommand(argv),
|
|
539
|
+
argv,
|
|
540
|
+
...(cwd ? { cwd } : {}),
|
|
541
|
+
why,
|
|
542
|
+
safe_to_auto_execute: safeToAutoExecute,
|
|
543
|
+
requires_approval: requiresApproval,
|
|
544
|
+
destructive: false,
|
|
545
|
+
idempotent: true,
|
|
546
|
+
spend_impact: SPEND_NONE,
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function nonShellAction({ type, surface, command, why }) {
|
|
551
|
+
return {
|
|
552
|
+
type,
|
|
553
|
+
surface,
|
|
554
|
+
command,
|
|
555
|
+
why,
|
|
556
|
+
safe_to_auto_execute: false,
|
|
557
|
+
requires_approval: true,
|
|
558
|
+
destructive: false,
|
|
559
|
+
idempotent: true,
|
|
560
|
+
spend_impact: SPEND_NONE,
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function shellRepair() {
|
|
565
|
+
return nonShellAction({
|
|
566
|
+
type: "repair_buzz_agent_runtime",
|
|
567
|
+
surface: "buzz_settings",
|
|
568
|
+
command: "Open Buzz Desktop > Settings > Agents, select this agent, choose a runtime with command execution enabled, restart the agent, then rerun Run402 setup.",
|
|
569
|
+
why: "Run402 setup needs the managed agent's configured shell before it can diagnose or mutate anything.",
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function nodeRepair() {
|
|
574
|
+
return nonShellAction({
|
|
575
|
+
type: "repair_buzz_node_runtime",
|
|
576
|
+
surface: "buzz_settings",
|
|
577
|
+
command: "Open Buzz Desktop > Settings > Updates, install the available Buzz update, restart this agent, then rerun Run402 setup.",
|
|
578
|
+
why: `The managed session must provide a spawnable Node ${BUZZ_DOCTOR_MIN_NODE_MAJOR}+ runtime.`,
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function run402InstallRepair() {
|
|
583
|
+
return shellAction({
|
|
584
|
+
type: "install_run402_cli",
|
|
585
|
+
argv: ["npm", "install", "-g", "run402@latest"],
|
|
586
|
+
why: "Install Run402 in the user-global npm context used by the Buzz setup helper.",
|
|
587
|
+
safeToAutoExecute: true,
|
|
588
|
+
requiresApproval: false,
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function run402RepairForInstall(install, cwd) {
|
|
593
|
+
if (install?.kind === "global_npm") return run402InstallRepair();
|
|
594
|
+
if (install?.kind === "local_project") {
|
|
595
|
+
const manager = install.package_manager ?? "npm";
|
|
596
|
+
const argv = manager === "pnpm" ? ["pnpm", "add", "-D", "run402@latest"]
|
|
597
|
+
: manager === "yarn" ? ["yarn", "add", "-D", "run402@latest"]
|
|
598
|
+
: manager === "bun" ? ["bun", "add", "-d", "run402@latest"]
|
|
599
|
+
: ["npm", "install", "-D", "run402@latest"];
|
|
600
|
+
return shellAction({ type: "upgrade_run402_cli", argv, cwd, why: "Upgrade the project-local Run402 CLI that executed this diagnostic." });
|
|
601
|
+
}
|
|
602
|
+
return shellAction({
|
|
603
|
+
type: "inspect_run402_cli",
|
|
604
|
+
argv: ["run402", "doctor", "--refresh"],
|
|
605
|
+
cwd,
|
|
606
|
+
why: "The Run402 executable context is custom or ambiguous; inspect it without guessing a package-manager mutation.",
|
|
607
|
+
safeToAutoExecute: true,
|
|
608
|
+
requiresApproval: false,
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function normalizeUpgradeAction(rawAction, install, cwd) {
|
|
613
|
+
const argv = Array.isArray(rawAction?.argv) && rawAction.argv.every((item) => typeof item === "string")
|
|
614
|
+
? rawAction.argv
|
|
615
|
+
: run402RepairForInstall(install, cwd).argv;
|
|
616
|
+
return shellAction({
|
|
617
|
+
type: "upgrade_run402_cli",
|
|
618
|
+
argv,
|
|
619
|
+
cwd: rawAction?.cwd ?? cwd,
|
|
620
|
+
why: "Upgrade guidance is advisory because the executing Run402 CLI remains compatible.",
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function buzzSidecarRepair() {
|
|
625
|
+
return nonShellAction({
|
|
626
|
+
type: "repair_buzz_cli_sidecar",
|
|
627
|
+
surface: BUZZ_CAPABILITIES.managed_sidecar.repair_surface,
|
|
628
|
+
command: BUZZ_CAPABILITIES.managed_sidecar.repair_instruction,
|
|
629
|
+
why: "The Buzz CLI is bundled with Buzz Desktop; a public package-manager command would be an unverified repair path.",
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function targetRequiredRepair(expectedSubjectHex) {
|
|
634
|
+
return nonShellAction({
|
|
635
|
+
type: "rerun_buzz_setup_with_explicit_identity",
|
|
636
|
+
surface: "buzz_chat",
|
|
637
|
+
command: expectedSubjectHex
|
|
638
|
+
? "@Fizz rerun Run402 setup using your current public Buzz npub and a unique dedicated Run402 profile."
|
|
639
|
+
: "@Fizz rerun Run402 setup using your current public Buzz npub and a unique dedicated Run402 profile such as buzz-fizz.",
|
|
640
|
+
why: "The agent must bind setup to one explicit public Buzz identity and one dedicated Run402 profile.",
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function targetUnverifiedRepair() {
|
|
645
|
+
return nonShellAction({
|
|
646
|
+
type: "publish_buzz_public_profile",
|
|
647
|
+
surface: "buzz_chat",
|
|
648
|
+
command: "@Fizz publish or repair your public Buzz profile, then rerun Run402 setup.",
|
|
649
|
+
why: "The released read-only `buzz users get` boundary needs one public self profile to prove the active agent.",
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function targetMismatchRepair() {
|
|
654
|
+
return nonShellAction({
|
|
655
|
+
type: "select_expected_buzz_agent",
|
|
656
|
+
surface: "buzz_chat",
|
|
657
|
+
command: "Send the Run402 setup request to the intended Buzz agent, then have that agent rerun setup from its own managed session.",
|
|
658
|
+
why: "Run402 will not relink or infer identity when the active Buzz signer differs from the requested public agent.",
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function apiRepair(origin) {
|
|
663
|
+
return nonShellAction({
|
|
664
|
+
type: "restore_run402_api_access",
|
|
665
|
+
surface: "buzz_settings",
|
|
666
|
+
command: `Open Buzz Desktop > Settings > Agents, select this agent, allow HTTPS access to ${safeOrigin(origin)}, restart the agent, then rerun Run402 setup.`,
|
|
667
|
+
why: "The agent must reach the Run402 API before setup can safely initialize or verify state.",
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function consoleRepair() {
|
|
672
|
+
return nonShellAction({
|
|
673
|
+
type: "restore_run402_console_access",
|
|
674
|
+
surface: "buzz_settings",
|
|
675
|
+
command: "Open Buzz Desktop > Settings > Agents, select this agent, allow HTTPS access to https://console.run402.com, restart the agent, then rerun Run402 setup.",
|
|
676
|
+
why: "The no-terminal human adoption handoff requires the Run402 console origin.",
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function relayUnsafeRepair() {
|
|
681
|
+
return nonShellAction({
|
|
682
|
+
type: "repair_buzz_relay_url",
|
|
683
|
+
surface: "buzz_settings",
|
|
684
|
+
command: "Open Buzz Desktop > Settings > Communities, select the current community, choose its public wss:// relay, restart this agent, then rerun Run402 setup.",
|
|
685
|
+
why: "Run402 refuses literal, private, reserved, mixed-address, credential-bearing, or non-TLS relay destinations.",
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function relayReachabilityRepair(origin) {
|
|
690
|
+
return nonShellAction({
|
|
691
|
+
type: "restore_buzz_relay_access",
|
|
692
|
+
surface: "buzz_settings",
|
|
693
|
+
command: `Open Buzz Desktop > Settings > Communities, reconnect the current community relay ${origin}, restart this agent, then rerun Run402 setup.`,
|
|
694
|
+
why: "The current safe relay must complete a bounded public NIP-11 read before setup can proceed.",
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function walletCreateRepair(walletProfile) {
|
|
699
|
+
return shellAction({
|
|
700
|
+
type: "create_buzz_wallet_profile",
|
|
701
|
+
argv: ["run402", "wallets", "new", walletProfile],
|
|
702
|
+
why: "Create the explicitly named dedicated agent profile, then rerun the complete preflight before setup.",
|
|
703
|
+
safeToAutoExecute: true,
|
|
704
|
+
requiresApproval: false,
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function rerunDoctorRepair(walletProfile, expectedSubjectHex) {
|
|
709
|
+
const argv = ["run402", "--wallet", walletProfile, "doctor", "--buzz"];
|
|
710
|
+
if (expectedSubjectHex) argv.push("--buzz-agent", expectedSubjectHex);
|
|
711
|
+
return shellAction({
|
|
712
|
+
type: "rerun_buzz_doctor",
|
|
713
|
+
argv,
|
|
714
|
+
why: "Rerun the read-only diagnostic with the exact explicit profile and public Buzz identity.",
|
|
715
|
+
safeToAutoExecute: true,
|
|
716
|
+
requiresApproval: false,
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function shellCommand(argv) {
|
|
721
|
+
return argv.map((part) => SAFE_TOKEN.test(part) ? part : `'${part.replaceAll("'", "'\\''")}'`).join(" ");
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function commandPassed(result) {
|
|
725
|
+
return !result?.error && result?.status === 0;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function commandFailureClass(result) {
|
|
729
|
+
if (result?.error?.code === "ETIMEDOUT") return "timeout";
|
|
730
|
+
if (result?.error?.code === "ENOENT") return "not_found";
|
|
731
|
+
if (result?.error) return "spawn_error";
|
|
732
|
+
return Number.isInteger(result?.status) ? `exit_${result.status}` : "unknown";
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
function compareMajorMinorPatch(left, right) {
|
|
736
|
+
const parse = (value) => String(value).match(/^v?(\d+)\.(\d+)\.(\d+)/)?.slice(1, 4).map(Number) ?? null;
|
|
737
|
+
const a = parse(left);
|
|
738
|
+
const b = parse(right);
|
|
739
|
+
if (!a || !b) return -1;
|
|
740
|
+
for (let index = 0; index < 3; index += 1) {
|
|
741
|
+
if (a[index] !== b[index]) return a[index] < b[index] ? -1 : 1;
|
|
742
|
+
}
|
|
743
|
+
return 0;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function parseWalletContext(raw) {
|
|
747
|
+
try {
|
|
748
|
+
const parsed = JSON.parse(raw || "");
|
|
749
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
750
|
+
} catch {
|
|
751
|
+
return null;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function usageError(code, message, flag) {
|
|
756
|
+
return { code, message, details: { flag }, retryable: false, safe_to_retry: true };
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function indexesOf(values, expected) {
|
|
760
|
+
return values.flatMap((value, index) => value === expected ? [index] : []);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function safeRealpath(path) {
|
|
764
|
+
if (typeof path !== "string" || path.length === 0) return null;
|
|
765
|
+
try { return existsSync(path) ? realpathSync(path) : path; }
|
|
766
|
+
catch { return path; }
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function safePath(path) {
|
|
770
|
+
return typeof path === "string" ? path : null;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function safeOrigin(value) {
|
|
774
|
+
try { return new URL(value).origin; }
|
|
775
|
+
catch { return null; }
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function safeFailure(error) {
|
|
779
|
+
if (error?.name === "AbortError") return "timeout";
|
|
780
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
781
|
+
if (/certificate|tls|ssl/i.test(message)) return "tls";
|
|
782
|
+
if (/dns|getaddrinfo|enotfound/i.test(message)) return "dns";
|
|
783
|
+
if (/timeout|timed out|abort/i.test(message)) return "timeout";
|
|
784
|
+
if (/redirect/i.test(message)) return "redirect_rejected";
|
|
785
|
+
if (/http_\d+/.test(message)) return message.match(/http_\d+/)[0];
|
|
786
|
+
return "network";
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function assertRepairMatches(code, action) {
|
|
790
|
+
const expected = BUZZ_DOCTOR_REPAIR_MATRIX[code];
|
|
791
|
+
if (!expected) throw new Error(`No frozen Buzz doctor repair mapping for ${code}`);
|
|
792
|
+
const types = Array.isArray(expected.type) ? expected.type : [expected.type];
|
|
793
|
+
const surfaces = Array.isArray(expected.surface) ? expected.surface : [expected.surface];
|
|
794
|
+
if (!types.includes(action?.type) || !surfaces.includes(action?.surface)) {
|
|
795
|
+
throw new Error(`Buzz doctor repair drift for ${code}`);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
class UnsafeRelayError extends Error {}
|