@haven_ai/connect 0.1.29-alpha.0 → 0.1.31-alpha.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 +92 -4
- package/dist/cli.cjs +2358 -1639
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +2352 -1633
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +2362 -1639
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +82 -9
- package/dist/index.d.ts +82 -9
- package/dist/index.js +2357 -1634
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
4
|
-
var
|
|
5
|
-
var sdk = require('@haven_ai/sdk');
|
|
3
|
+
var crypto = require('crypto');
|
|
4
|
+
var ethers = require('ethers');
|
|
6
5
|
var promises = require('fs/promises');
|
|
7
6
|
var os = require('os');
|
|
8
7
|
var path = require('path');
|
|
8
|
+
var mcp = require('@haven_ai/mcp');
|
|
9
|
+
var signer = require('@haven_ai/signer');
|
|
10
|
+
var sdk = require('@haven_ai/sdk');
|
|
9
11
|
var yaml = require('yaml');
|
|
10
12
|
var child_process = require('child_process');
|
|
11
13
|
var util = require('util');
|
|
12
|
-
var crypto = require('crypto');
|
|
13
|
-
var ethers = require('ethers');
|
|
14
14
|
var readline = require('readline');
|
|
15
15
|
var fs = require('fs');
|
|
16
16
|
var url = require('url');
|
|
@@ -30,10 +30,132 @@ var __export = (target, all) => {
|
|
|
30
30
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
+
// src/api.ts
|
|
34
|
+
function createConnectApiClient(baseUrl, fetchImpl = fetch) {
|
|
35
|
+
const root = baseUrl.replace(/\/+$/, "");
|
|
36
|
+
return {
|
|
37
|
+
resolveSetup: (input) => request(fetchImpl, `${root}/agent-connection-setups/resolve`, {
|
|
38
|
+
method: "POST",
|
|
39
|
+
body: JSON.stringify({
|
|
40
|
+
setup_token: input.setupToken,
|
|
41
|
+
connector_version: input.connectorVersion,
|
|
42
|
+
runtime: input.runtime
|
|
43
|
+
})
|
|
44
|
+
}),
|
|
45
|
+
registerSetup: (input) => request(fetchImpl, `${root}/agent-connection-setups/register`, {
|
|
46
|
+
method: "POST",
|
|
47
|
+
body: JSON.stringify({
|
|
48
|
+
setup_token: input.setupToken,
|
|
49
|
+
challenge_id: input.challengeId,
|
|
50
|
+
delegate_address: input.delegateAddress,
|
|
51
|
+
proof_signature: input.proofSignature,
|
|
52
|
+
api_key_hash: input.apiKeyHash,
|
|
53
|
+
api_key_prefix: input.apiKeyPrefix,
|
|
54
|
+
runtime: input.runtime,
|
|
55
|
+
connector_version: input.connectorVersion,
|
|
56
|
+
mcp_server_name: input.mcpServerName,
|
|
57
|
+
connector_context: input.connectorContext,
|
|
58
|
+
install_capabilities: input.installCapabilities && {
|
|
59
|
+
can_write_runtime_config: input.installCapabilities.canWriteRuntimeConfig,
|
|
60
|
+
restart_required: input.installCapabilities.restartRequired
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
}),
|
|
64
|
+
getAgentIdentity: (apiKey) => request(fetchImpl, `${root}/machine-payments/agent`, {
|
|
65
|
+
method: "GET",
|
|
66
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
67
|
+
}),
|
|
68
|
+
getConnectorStatus: (setupId, apiKey) => request(fetchImpl, `${root}/agent-connection-setups/${encodeURIComponent(setupId)}/connector-status`, {
|
|
69
|
+
method: "GET",
|
|
70
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
71
|
+
}),
|
|
72
|
+
updateInstallStatus: async (setupId, apiKey, input) => {
|
|
73
|
+
await request(fetchImpl, `${root}/agent-connection-setups/${encodeURIComponent(setupId)}/install-status`, {
|
|
74
|
+
method: "POST",
|
|
75
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
76
|
+
body: JSON.stringify({
|
|
77
|
+
runtime: input.runtime,
|
|
78
|
+
connector_version: input.connectorVersion,
|
|
79
|
+
runtime_mcp_mode: input.runtimeMcpMode,
|
|
80
|
+
hosted_mcp_configured: input.hostedMcpConfigured,
|
|
81
|
+
local_signer_configured: input.localSignerConfigured,
|
|
82
|
+
local_mcp_configured: input.localMcpConfigured,
|
|
83
|
+
credential_files_written: input.credentialFilesWritten,
|
|
84
|
+
signer_acknowledged: input.signerAcknowledged,
|
|
85
|
+
local_mcp_acknowledged: input.localMcpAcknowledged,
|
|
86
|
+
activation_command_available: input.activationCommandAvailable,
|
|
87
|
+
skill_installed: input.skillInstalled,
|
|
88
|
+
probe_result: input.probeResult,
|
|
89
|
+
restart_required: input.restartRequired,
|
|
90
|
+
next_user_action: input.nextUserAction,
|
|
91
|
+
error_code: input.errorCode ?? null,
|
|
92
|
+
environment_label: input.environmentLabel
|
|
93
|
+
})
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
async function request(fetchImpl, url, init) {
|
|
99
|
+
const response = await fetchImpl(url, {
|
|
100
|
+
...init,
|
|
101
|
+
headers: {
|
|
102
|
+
"Content-Type": "application/json",
|
|
103
|
+
...init.headers ?? {}
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
const text = await response.text();
|
|
107
|
+
const body = text ? JSON.parse(text) : null;
|
|
108
|
+
if (!response.ok) {
|
|
109
|
+
const message = body?.error ?? body?.message ?? `${response.status} ${response.statusText}`;
|
|
110
|
+
throw new ConnectRequestError(`Haven setup request failed: ${message}`, response.status);
|
|
111
|
+
}
|
|
112
|
+
return body;
|
|
113
|
+
}
|
|
114
|
+
var ConnectRequestError;
|
|
115
|
+
var init_api = __esm({
|
|
116
|
+
"src/api.ts"() {
|
|
117
|
+
ConnectRequestError = class extends Error {
|
|
118
|
+
constructor(message, status) {
|
|
119
|
+
super(message);
|
|
120
|
+
this.status = status;
|
|
121
|
+
this.name = "ConnectRequestError";
|
|
122
|
+
}
|
|
123
|
+
status;
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
function generateDelegateKey() {
|
|
128
|
+
return delegateKeyFromPrivateKey(ethers.Wallet.createRandom().privateKey);
|
|
129
|
+
}
|
|
130
|
+
function delegateKeyFromPrivateKey(privateKey) {
|
|
131
|
+
const wallet = new ethers.Wallet(privateKey);
|
|
132
|
+
return {
|
|
133
|
+
privateKey: wallet.privateKey,
|
|
134
|
+
address: wallet.address,
|
|
135
|
+
signChallenge: (message) => wallet.signMessage(message)
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function generateAgentApiKey() {
|
|
139
|
+
return `sk_agent_${crypto__default.default.randomBytes(24).toString("hex")}`;
|
|
140
|
+
}
|
|
141
|
+
function hashAgentApiKey(apiKey) {
|
|
142
|
+
return crypto__default.default.createHash("sha256").update(apiKey).digest("hex");
|
|
143
|
+
}
|
|
144
|
+
function agentApiKeyPrefix(apiKey) {
|
|
145
|
+
return apiKey.slice(0, 12);
|
|
146
|
+
}
|
|
147
|
+
var init_key = __esm({
|
|
148
|
+
"src/key.ts"() {
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
33
152
|
// src/redact.ts
|
|
34
153
|
function redactSecrets(value) {
|
|
35
154
|
return value.replace(API_KEY_RE, "sk_agent_[redacted]").replace(PRIVATE_KEY_RE, "0x[redacted-private-key]");
|
|
36
155
|
}
|
|
156
|
+
function redactForAutomation(value) {
|
|
157
|
+
return redactSecrets(value).replace(/(?:~|\/)[^\s`"']*\/(?:identity|signer|agent)\.json\b/g, "[credential-file-redacted]").replace(/(?:~|\/)[^\s`"']*\/\.env\b/g, "[credential-env-redacted]");
|
|
158
|
+
}
|
|
37
159
|
function shortAddress(address) {
|
|
38
160
|
if (!/^0x[0-9a-fA-F]{40}$/.test(address)) return address;
|
|
39
161
|
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
|
@@ -92,115 +214,411 @@ var init_server_names = __esm({
|
|
|
92
214
|
SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
93
215
|
}
|
|
94
216
|
});
|
|
95
|
-
function
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
217
|
+
async function preflightCredentialStorage(input = {}) {
|
|
218
|
+
const directory = defaultCredentialRoot(input.baseDir);
|
|
219
|
+
await promises.mkdir(directory, { recursive: true, mode: 448 });
|
|
220
|
+
await restrictPermissions(directory, 448, input.warn);
|
|
221
|
+
const probePath = path.join(directory, `.haven-connect-preflight-${crypto__default.default.randomBytes(8).toString("hex")}`);
|
|
222
|
+
try {
|
|
223
|
+
await writeOwnerOnlyJson(probePath, { ok: true }, input.warn);
|
|
224
|
+
} finally {
|
|
225
|
+
await promises.rm(probePath, { force: true }).catch(() => void 0);
|
|
226
|
+
}
|
|
227
|
+
return directory;
|
|
103
228
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
minimumNodeVersion: sdk.HAVEN_MINIMUM_NODE_VERSION,
|
|
121
|
-
supportedClients: ["codex-cli", "codex-desktop", "claude-code"],
|
|
122
|
-
requiredTools: mcp.registeredToolNames(),
|
|
123
|
-
/**
|
|
124
|
-
* The signer MCP's tool surface, DERIVED from the pinned @haven_ai/signer
|
|
125
|
-
* package (#1587) — same anti-drift rule as `requiredTools` above: a
|
|
126
|
-
* literal list here would rot the first time the signer gains a tool.
|
|
127
|
-
* The handshake probe requires all of them.
|
|
128
|
-
*/
|
|
129
|
-
requiredSignerTools: Object.keys(signer.toolSchemas)
|
|
130
|
-
};
|
|
229
|
+
async function writeCredentialFiles(input) {
|
|
230
|
+
const directory = defaultAgentDirectory(input.serverName ?? input.agentId, input.baseDir);
|
|
231
|
+
await promises.mkdir(directory, { recursive: true, mode: 448 });
|
|
232
|
+
await restrictPermissions(directory, 448, input.warn);
|
|
233
|
+
const identityPath = path.join(directory, "identity.json");
|
|
234
|
+
const signerPath = path.join(directory, "signer.json");
|
|
235
|
+
const agentPath = path.join(directory, "agent.json");
|
|
236
|
+
await assertDoesNotExist(identityPath);
|
|
237
|
+
await assertDoesNotExist(signerPath);
|
|
238
|
+
await assertDoesNotExist(agentPath);
|
|
239
|
+
await writeOwnerOnlyJson(signerPath, signerPayload(input), input.warn);
|
|
240
|
+
try {
|
|
241
|
+
await writeOwnerOnlyJson(identityPath, identityPayload(input), input.warn);
|
|
242
|
+
} catch (err) {
|
|
243
|
+
await promises.rm(signerPath, { force: true }).catch(() => void 0);
|
|
244
|
+
throw err;
|
|
131
245
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
return writeJsonRuntimeConfig(input, cursorConfigPath(input.homeDir), "mcpServers");
|
|
140
|
-
case "vscode":
|
|
141
|
-
return writeJsonRuntimeConfig(input, vscodeConfigPath(input.homeDir), "servers");
|
|
142
|
-
case "vscode-insiders":
|
|
143
|
-
return writeJsonRuntimeConfig(input, vscodeInsidersConfigPath(input.homeDir), "servers");
|
|
144
|
-
case "claude-desktop":
|
|
145
|
-
return writeJsonRuntimeConfig(input, claudeDesktopConfigPath(input.homeDir), "mcpServers");
|
|
146
|
-
case "hermes":
|
|
147
|
-
return writeHermesConfig(input, deps);
|
|
148
|
-
default:
|
|
149
|
-
return {
|
|
150
|
-
hostedConfigured: false,
|
|
151
|
-
signerConfigured: false,
|
|
152
|
-
localMcpConfigured: false,
|
|
153
|
-
runtimeMcpMode: "manual",
|
|
154
|
-
target: "manual runtime setup",
|
|
155
|
-
changed: false,
|
|
156
|
-
restartRequired: true,
|
|
157
|
-
messages: ["Runtime config needs to be added manually for this agent environment."],
|
|
158
|
-
errorCode: "manual_runtime_setup_required"
|
|
159
|
-
};
|
|
246
|
+
try {
|
|
247
|
+
await writeOwnerOnlyJson(agentPath, agentPayload(input), input.warn);
|
|
248
|
+
} catch (err) {
|
|
249
|
+
await promises.rm(signerPath, { force: true }).catch(() => void 0);
|
|
250
|
+
await promises.rm(identityPath, { force: true }).catch(() => void 0);
|
|
251
|
+
await promises.rm(agentPath, { force: true }).catch(() => void 0);
|
|
252
|
+
throw err;
|
|
160
253
|
}
|
|
254
|
+
return { directory, identityPath, signerPath, agentPath };
|
|
161
255
|
}
|
|
162
|
-
function
|
|
163
|
-
if (runtime === "vscode" || runtime === "vscode-insiders") {
|
|
164
|
-
return {
|
|
165
|
-
type: "http",
|
|
166
|
-
url: hostedMcpUrl,
|
|
167
|
-
headers: { Authorization: `Bearer ${apiKey}` }
|
|
168
|
-
};
|
|
169
|
-
}
|
|
256
|
+
function signerPayload(input) {
|
|
170
257
|
return {
|
|
171
|
-
|
|
172
|
-
|
|
258
|
+
delegate_key: input.delegateKey,
|
|
259
|
+
delegate_address: input.delegateAddress,
|
|
260
|
+
agent_id: input.agentId,
|
|
261
|
+
safe_address: input.safeAddress,
|
|
262
|
+
chain_id: input.chainId,
|
|
263
|
+
network: input.network,
|
|
264
|
+
x402_binding_signer: input.x402BindingSigner,
|
|
265
|
+
note: "Local signer credential. Haven backend never receives this private key."
|
|
173
266
|
};
|
|
174
267
|
}
|
|
175
|
-
function
|
|
176
|
-
return
|
|
177
|
-
|
|
178
|
-
|
|
268
|
+
function identityPayload(input) {
|
|
269
|
+
return {
|
|
270
|
+
api_key: input.apiKey,
|
|
271
|
+
agent_id: input.agentId,
|
|
272
|
+
safe_address: input.safeAddress,
|
|
273
|
+
chain_id: input.chainId,
|
|
274
|
+
network: input.network,
|
|
275
|
+
api_url: input.apiUrl,
|
|
276
|
+
hosted_mcp_url: input.hostedMcpUrl,
|
|
277
|
+
agent_budget: input.agentBudget,
|
|
278
|
+
note: "Haven API key identifies the agent only. It cannot spend without the local signer key and on-chain Haven wallet rules."
|
|
179
279
|
};
|
|
180
280
|
}
|
|
181
|
-
function
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
281
|
+
function agentPayload(input) {
|
|
282
|
+
return {
|
|
283
|
+
agent_id: input.agentId,
|
|
284
|
+
delegate_address: input.delegateAddress,
|
|
285
|
+
safe_address: input.safeAddress,
|
|
286
|
+
chain_id: input.chainId,
|
|
287
|
+
network: input.network,
|
|
288
|
+
agent_budget: input.agentBudget,
|
|
289
|
+
note: "Non-secret orientation for the agent: public delegate/Haven wallet identity + configured budget. Contains no API key or signing key. For the live remaining budget, call haven_get_allowances."
|
|
185
290
|
};
|
|
186
|
-
if (runtime === "vscode" || runtime === "vscode-insiders") return { type: "stdio", ...server };
|
|
187
|
-
return server;
|
|
188
291
|
}
|
|
189
|
-
function
|
|
190
|
-
const
|
|
191
|
-
const
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
292
|
+
async function readStoredCredentials(serverName, agentIdOrSlug, baseDir) {
|
|
293
|
+
const key = serverName ?? agentIdOrSlug;
|
|
294
|
+
const directory = key ? defaultAgentDirectory(key, baseDir) : await discoverSoleAgentDirectory(baseDir);
|
|
295
|
+
const identity = await readJsonFile(path.join(directory, "identity.json"));
|
|
296
|
+
if (!identity) {
|
|
297
|
+
throw new Error(
|
|
298
|
+
`No Haven credentials at ${directory}. Nothing to re-key \u2014 connect this agent first, or pass the --name you wired it under.`
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
const agent = await readJsonFile(path.join(directory, "agent.json")) ?? {};
|
|
302
|
+
const signer = await readJsonFile(path.join(directory, "signer.json")) ?? {};
|
|
303
|
+
const agentId = asString(identity.agent_id);
|
|
304
|
+
const apiKey = asString(identity.api_key);
|
|
305
|
+
const apiUrl = asString(identity.api_url);
|
|
306
|
+
const hostedMcpUrl = asString(identity.hosted_mcp_url);
|
|
307
|
+
if (!agentId || !apiKey || !apiUrl || !hostedMcpUrl) {
|
|
308
|
+
throw new Error(
|
|
309
|
+
`The credential set at ${directory} is incomplete (identity.json is missing agent_id, api_key, api_url or hosted_mcp_url). Re-key cannot rebuild it \u2014 reconnect the agent instead.`
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
return {
|
|
313
|
+
directory,
|
|
314
|
+
agentId,
|
|
315
|
+
apiKey,
|
|
316
|
+
apiUrl,
|
|
317
|
+
hostedMcpUrl,
|
|
318
|
+
delegateAddress: asString(agent.delegate_address) ?? asString(signer.delegate_address),
|
|
319
|
+
safeAddress: asString(identity.safe_address) ?? asString(agent.safe_address),
|
|
320
|
+
chainId: typeof identity.chain_id === "number" ? identity.chain_id : void 0,
|
|
321
|
+
network: asString(identity.network),
|
|
322
|
+
x402BindingSigner: asString(signer.x402_binding_signer),
|
|
323
|
+
agentBudget: Array.isArray(identity.agent_budget) ? identity.agent_budget : void 0
|
|
197
324
|
};
|
|
198
|
-
return `${JSON.stringify(config, null, 2)}
|
|
199
|
-
`;
|
|
200
325
|
}
|
|
201
|
-
function
|
|
202
|
-
|
|
203
|
-
|
|
326
|
+
async function discoverSoleAgentDirectory(baseDir) {
|
|
327
|
+
const root = defaultCredentialRoot(baseDir);
|
|
328
|
+
let entries = [];
|
|
329
|
+
try {
|
|
330
|
+
entries = await promises.readdir(root);
|
|
331
|
+
} catch {
|
|
332
|
+
throw new Error(`No Haven credentials found under ${root}. Connect an agent on this machine first.`);
|
|
333
|
+
}
|
|
334
|
+
const candidates = [];
|
|
335
|
+
for (const entry of entries) {
|
|
336
|
+
const directory = path.join(root, entry);
|
|
337
|
+
if (!await readJsonFile(path.join(directory, "identity.json"))) continue;
|
|
338
|
+
if (await readJsonFile(path.join(directory, "TOMBSTONE.json"))) continue;
|
|
339
|
+
candidates.push(directory);
|
|
340
|
+
}
|
|
341
|
+
if (candidates.length === 1) return candidates[0];
|
|
342
|
+
if (candidates.length === 0) {
|
|
343
|
+
throw new Error(`No Haven credentials found under ${root}. Connect an agent on this machine first.`);
|
|
344
|
+
}
|
|
345
|
+
throw new Error(
|
|
346
|
+
`Several agents are wired on this machine, so --rekey cannot tell which one you mean:
|
|
347
|
+
` + candidates.map((d) => ` ${d}`).join("\n") + "\nRe-run with --name <slug> to pick one."
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
async function rewriteCredentialFiles(input) {
|
|
351
|
+
const directory = defaultAgentDirectory(input.serverName ?? input.agentId, input.baseDir);
|
|
352
|
+
const identityPath = path.join(directory, "identity.json");
|
|
353
|
+
const signerPath = path.join(directory, "signer.json");
|
|
354
|
+
const agentPath = path.join(directory, "agent.json");
|
|
355
|
+
const targets = [
|
|
356
|
+
{ path: signerPath, payload: signerPayload(input) },
|
|
357
|
+
{ path: identityPath, payload: identityPayload(input) },
|
|
358
|
+
{ path: agentPath, payload: agentPayload(input) }
|
|
359
|
+
];
|
|
360
|
+
const originals = /* @__PURE__ */ new Map();
|
|
361
|
+
for (const { path } of targets) {
|
|
362
|
+
originals.set(path, await readRawFile(path));
|
|
363
|
+
}
|
|
364
|
+
const temps = [];
|
|
365
|
+
try {
|
|
366
|
+
for (const { path, payload } of targets) {
|
|
367
|
+
const temp = `${path}.rekey-${crypto__default.default.randomBytes(6).toString("hex")}.tmp`;
|
|
368
|
+
await writeOwnerOnlyJson(temp, payload, input.warn);
|
|
369
|
+
temps.push({ from: temp, to: path });
|
|
370
|
+
}
|
|
371
|
+
for (const { from, to } of temps) {
|
|
372
|
+
await promises.rename(from, to);
|
|
373
|
+
}
|
|
374
|
+
} catch (err) {
|
|
375
|
+
for (const { from } of temps) await promises.rm(from, { force: true }).catch(() => void 0);
|
|
376
|
+
for (const [path, contents] of originals) {
|
|
377
|
+
if (contents === null) {
|
|
378
|
+
await promises.rm(path, { force: true }).catch(() => void 0);
|
|
379
|
+
} else {
|
|
380
|
+
await promises.writeFile(path, contents, { mode: 384 }).catch(() => void 0);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
throw err;
|
|
384
|
+
}
|
|
385
|
+
return { directory, identityPath, signerPath, agentPath };
|
|
386
|
+
}
|
|
387
|
+
async function writeRekeyPending(directory, pending, warn) {
|
|
388
|
+
const path$1 = path.join(directory, REKEY_PENDING_FILENAME);
|
|
389
|
+
await promises.rm(path$1, { force: true }).catch(() => void 0);
|
|
390
|
+
await writeOwnerOnlyJson(path$1, { ...pending }, warn);
|
|
391
|
+
return path$1;
|
|
392
|
+
}
|
|
393
|
+
async function readRekeyPending(directory, now = Date.now()) {
|
|
394
|
+
const path$1 = path.join(directory, REKEY_PENDING_FILENAME);
|
|
395
|
+
const raw = await readJsonFile(path$1);
|
|
396
|
+
if (!raw) {
|
|
397
|
+
throw new Error(
|
|
398
|
+
`No re-key in progress at ${directory}. Run the connector with --rekey first \u2014 it prints the new signing address to paste into the Haven agent page.`
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
const pending = raw;
|
|
402
|
+
if (!pending.new_delegate_key || !pending.new_delegate_address || !pending.agent_id) {
|
|
403
|
+
throw new Error(`The pending re-key at ${path$1} is unreadable. Delete it and start again with --rekey.`);
|
|
404
|
+
}
|
|
405
|
+
if (pending.expires_at && Date.parse(pending.expires_at) < now) {
|
|
406
|
+
throw new Error(
|
|
407
|
+
`The re-key started at ${pending.started_at} has expired. Start again with --rekey \u2014 the address currently shown in your dashboard is no longer the one this machine holds.`
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
return pending;
|
|
411
|
+
}
|
|
412
|
+
async function inspectRekeyPending(directory, now = Date.now()) {
|
|
413
|
+
const path$1 = path.join(directory, REKEY_PENDING_FILENAME);
|
|
414
|
+
const raw = await readJsonFile(path$1);
|
|
415
|
+
if (!raw) {
|
|
416
|
+
const present = await readRawFile(path$1) !== null;
|
|
417
|
+
return present ? { state: "unreadable", path: path$1 } : null;
|
|
418
|
+
}
|
|
419
|
+
const agentId = asString(raw.agent_id);
|
|
420
|
+
const newDelegateAddress = asString(raw.new_delegate_address);
|
|
421
|
+
const startedAt = asString(raw.started_at);
|
|
422
|
+
const expiresAt = asString(raw.expires_at);
|
|
423
|
+
if (!agentId || !newDelegateAddress) {
|
|
424
|
+
return { state: "unreadable", path: path$1, ...startedAt ? { startedAt } : {} };
|
|
425
|
+
}
|
|
426
|
+
const expired = expiresAt !== void 0 && Date.parse(expiresAt) < now;
|
|
427
|
+
return {
|
|
428
|
+
state: expired ? "expired" : "pending",
|
|
429
|
+
path: path$1,
|
|
430
|
+
agentId,
|
|
431
|
+
newDelegateAddress,
|
|
432
|
+
...startedAt ? { startedAt } : {},
|
|
433
|
+
...expiresAt ? { expiresAt } : {}
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
async function clearRekeyPending(directory) {
|
|
437
|
+
await promises.rm(path.join(directory, REKEY_PENDING_FILENAME), { force: true }).catch(() => void 0);
|
|
438
|
+
}
|
|
439
|
+
async function readJsonFile(path) {
|
|
440
|
+
const raw = await readRawFile(path);
|
|
441
|
+
if (raw === null) return null;
|
|
442
|
+
try {
|
|
443
|
+
const parsed = JSON.parse(raw);
|
|
444
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
445
|
+
} catch {
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
async function readRawFile(path) {
|
|
450
|
+
try {
|
|
451
|
+
return await promises.readFile(path, "utf8");
|
|
452
|
+
} catch {
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
function asString(value) {
|
|
457
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
458
|
+
}
|
|
459
|
+
async function assertServerSlugAvailable(serverName, baseDir) {
|
|
460
|
+
const directory = defaultAgentDirectory(serverName, baseDir);
|
|
461
|
+
try {
|
|
462
|
+
await promises.stat(path.join(directory, "identity.json"));
|
|
463
|
+
} catch {
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
throw new Error(
|
|
467
|
+
`The name "${serverName}" is already wired on this machine (${directory} holds credentials). Pick a different --name, or revoke and remove that agent first \u2014 connect never overwrites credentials.`
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
function defaultAgentDirectory(agentId, baseDir = path.join(os.homedir(), ".haven", "agents")) {
|
|
471
|
+
return path.resolve(defaultCredentialRoot(baseDir), safePathPart(agentId));
|
|
472
|
+
}
|
|
473
|
+
function defaultCredentialRoot(baseDir = path.join(os.homedir(), ".haven", "agents")) {
|
|
474
|
+
return path.resolve(baseDir);
|
|
475
|
+
}
|
|
476
|
+
async function writeOwnerOnlyJson(path, value, warn) {
|
|
477
|
+
const json = JSON.stringify(dropUndefined(value), null, 2);
|
|
478
|
+
await promises.writeFile(path, `${json}
|
|
479
|
+
`, { mode: 384, flag: "wx" });
|
|
480
|
+
await restrictPermissions(path, 384, warn);
|
|
481
|
+
}
|
|
482
|
+
function safePathPart(value) {
|
|
483
|
+
return value.replace(/[^A-Za-z0-9_.-]/g, "_");
|
|
484
|
+
}
|
|
485
|
+
function dropUndefined(value) {
|
|
486
|
+
return Object.fromEntries(Object.entries(value).filter(([, field]) => field !== void 0));
|
|
487
|
+
}
|
|
488
|
+
async function assertDoesNotExist(path) {
|
|
489
|
+
try {
|
|
490
|
+
await promises.access(path);
|
|
491
|
+
} catch (err) {
|
|
492
|
+
if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") return;
|
|
493
|
+
throw err;
|
|
494
|
+
}
|
|
495
|
+
throw new Error(`Refusing to overwrite existing Haven credential file: ${path}`);
|
|
496
|
+
}
|
|
497
|
+
async function restrictPermissions(path, mode, warn) {
|
|
498
|
+
try {
|
|
499
|
+
await promises.chmod(path, mode);
|
|
500
|
+
} catch (err) {
|
|
501
|
+
warn?.(
|
|
502
|
+
`Warning: could not restrict permissions on ${path} to ${mode.toString(8)}. Move this credential to a private location or run chmod ${mode.toString(8)} ${path}. ${err instanceof Error ? err.message : String(err)}`
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
var REKEY_PENDING_FILENAME, REKEY_PENDING_TTL_MS;
|
|
507
|
+
var init_storage = __esm({
|
|
508
|
+
"src/storage.ts"() {
|
|
509
|
+
REKEY_PENDING_FILENAME = "rekey-pending.json";
|
|
510
|
+
REKEY_PENDING_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
function mcpPackageSpec() {
|
|
514
|
+
return `${exports.MCP_RUNTIME_MANIFEST.mcpPackage}@${exports.MCP_RUNTIME_MANIFEST.mcpVersion}`;
|
|
515
|
+
}
|
|
516
|
+
function sdkPackageSpec() {
|
|
517
|
+
return `${exports.MCP_RUNTIME_MANIFEST.sdkPackage}@${exports.MCP_RUNTIME_MANIFEST.sdkVersion}`;
|
|
518
|
+
}
|
|
519
|
+
function signerPackageSpec() {
|
|
520
|
+
return `${exports.MCP_RUNTIME_MANIFEST.signerPackage}@${exports.MCP_RUNTIME_MANIFEST.signerVersion}`;
|
|
521
|
+
}
|
|
522
|
+
exports.MCP_RUNTIME_MANIFEST = void 0;
|
|
523
|
+
var init_runtime_manifest = __esm({
|
|
524
|
+
"src/runtime-manifest.ts"() {
|
|
525
|
+
exports.MCP_RUNTIME_MANIFEST = {
|
|
526
|
+
mcpPackage: "@haven_ai/mcp",
|
|
527
|
+
mcpVersion: mcp.MCP_VERSION,
|
|
528
|
+
sdkPackage: "@haven_ai/sdk",
|
|
529
|
+
sdkVersion: "0.1.31-alpha.0",
|
|
530
|
+
signerPackage: "@haven_ai/signer",
|
|
531
|
+
signerVersion: "0.1.31-alpha.0",
|
|
532
|
+
// Sourced from the SDK, never a literal (#1161). This field read '20.0.0'
|
|
533
|
+
// while every package's `engines` said `>=24` and the docs said `>=24.0.0`,
|
|
534
|
+
// so the guard that was supposed to enforce the floor waved Node v23 through
|
|
535
|
+
// — including on the `--local` path where it does run. A hand-maintained
|
|
536
|
+
// second copy of a number is a drift waiting to happen; a guard test pins
|
|
537
|
+
// this against `package.json`'s `engines.node`.
|
|
538
|
+
minimumNodeVersion: sdk.HAVEN_MINIMUM_NODE_VERSION,
|
|
539
|
+
supportedClients: ["codex-cli", "codex-desktop", "claude-code"],
|
|
540
|
+
requiredTools: mcp.registeredToolNames(),
|
|
541
|
+
/**
|
|
542
|
+
* The signer MCP's tool surface, DERIVED from the pinned @haven_ai/signer
|
|
543
|
+
* package (#1587) — same anti-drift rule as `requiredTools` above: a
|
|
544
|
+
* literal list here would rot the first time the signer gains a tool.
|
|
545
|
+
* The handshake probe requires all of them.
|
|
546
|
+
*/
|
|
547
|
+
requiredSignerTools: Object.keys(signer.toolSchemas)
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
async function writeRuntimeConfig(input, deps = {}) {
|
|
552
|
+
switch (input.runtime) {
|
|
553
|
+
case "codex-cli":
|
|
554
|
+
case "codex-desktop":
|
|
555
|
+
return writeCodexConfig(input);
|
|
556
|
+
case "cursor":
|
|
557
|
+
return writeJsonRuntimeConfig(input, cursorConfigPath(input.homeDir), "mcpServers");
|
|
558
|
+
case "vscode":
|
|
559
|
+
return writeJsonRuntimeConfig(input, vscodeConfigPath(input.homeDir), "servers");
|
|
560
|
+
case "vscode-insiders":
|
|
561
|
+
return writeJsonRuntimeConfig(input, vscodeInsidersConfigPath(input.homeDir), "servers");
|
|
562
|
+
case "claude-desktop":
|
|
563
|
+
return writeJsonRuntimeConfig(input, claudeDesktopConfigPath(input.homeDir), "mcpServers");
|
|
564
|
+
case "hermes":
|
|
565
|
+
return writeHermesConfig(input, deps);
|
|
566
|
+
default:
|
|
567
|
+
return {
|
|
568
|
+
hostedConfigured: false,
|
|
569
|
+
signerConfigured: false,
|
|
570
|
+
localMcpConfigured: false,
|
|
571
|
+
runtimeMcpMode: "manual",
|
|
572
|
+
target: "manual runtime setup",
|
|
573
|
+
changed: false,
|
|
574
|
+
restartRequired: true,
|
|
575
|
+
messages: ["Runtime config needs to be added manually for this agent environment."],
|
|
576
|
+
errorCode: "manual_runtime_setup_required"
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
function buildHostedServer(hostedMcpUrl, apiKey, runtime) {
|
|
581
|
+
if (runtime === "vscode" || runtime === "vscode-insiders") {
|
|
582
|
+
return {
|
|
583
|
+
type: "http",
|
|
584
|
+
url: hostedMcpUrl,
|
|
585
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
return {
|
|
589
|
+
url: hostedMcpUrl,
|
|
590
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
function resolveSignerLaunchSpec(input) {
|
|
594
|
+
return input.signerCommand ?? {
|
|
595
|
+
command: "npx",
|
|
596
|
+
args: ["-y", signerPackageSpec(), "--credentials", input.signerPath]
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
function buildSignerServer(spec, runtime) {
|
|
600
|
+
const server = {
|
|
601
|
+
command: spec.command,
|
|
602
|
+
args: spec.args
|
|
603
|
+
};
|
|
604
|
+
if (runtime === "vscode" || runtime === "vscode-insiders") return { type: "stdio", ...server };
|
|
605
|
+
return server;
|
|
606
|
+
}
|
|
607
|
+
function mergeJsonMcpConfig(existingJson, serverRoot, hostedServer, signerServer, names = serverNamesFor(), configPath) {
|
|
608
|
+
const config = existingJson?.trim() ? parseJsonObject(existingJson, configPath) : {};
|
|
609
|
+
const existingRoot = config[serverRoot];
|
|
610
|
+
const servers = existingRoot && typeof existingRoot === "object" && !Array.isArray(existingRoot) ? existingRoot : {};
|
|
611
|
+
config[serverRoot] = {
|
|
612
|
+
...servers,
|
|
613
|
+
[names.hosted]: hostedServer,
|
|
614
|
+
[names.signer]: signerServer
|
|
615
|
+
};
|
|
616
|
+
return `${JSON.stringify(config, null, 2)}
|
|
617
|
+
`;
|
|
618
|
+
}
|
|
619
|
+
function mergeHermesYaml(existingYaml, hostedServer, signerServer, names = serverNamesFor(), configPath) {
|
|
620
|
+
if (!existingYaml?.trim()) {
|
|
621
|
+
return renderHermesYaml({ [names.hosted]: hostedServer, [names.signer]: signerServer });
|
|
204
622
|
}
|
|
205
623
|
const doc = yaml.parseDocument(existingYaml, { keepSourceTokens: true });
|
|
206
624
|
if (doc.errors.length > 0 || !yaml.isMap(doc.contents)) {
|
|
@@ -851,35 +1269,115 @@ var init_config_writers = __esm({
|
|
|
851
1269
|
};
|
|
852
1270
|
}
|
|
853
1271
|
});
|
|
854
|
-
async function
|
|
855
|
-
let response;
|
|
1272
|
+
async function acknowledgeLocalMcpConsent(identityPath, signerPath, log) {
|
|
856
1273
|
try {
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
1274
|
+
const input = await buildLocalMcpConsentInput(identityPath, signerPath);
|
|
1275
|
+
const decision = await mcp.ensureConsent(input, {
|
|
1276
|
+
credentialsPath: identityPath,
|
|
1277
|
+
writeAck: true,
|
|
1278
|
+
out: log ? { write: (chunk) => writeLogChunk(log, chunk) } : void 0
|
|
860
1279
|
});
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
return { status: "bad_response" };
|
|
1280
|
+
return {
|
|
1281
|
+
acknowledged: decision.ok,
|
|
1282
|
+
hash: decision.hash,
|
|
1283
|
+
reason: decision.reason
|
|
1284
|
+
};
|
|
1285
|
+
} catch (err) {
|
|
1286
|
+
return {
|
|
1287
|
+
acknowledged: false,
|
|
1288
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1289
|
+
};
|
|
872
1290
|
}
|
|
873
1291
|
}
|
|
874
|
-
async function
|
|
875
|
-
let response;
|
|
1292
|
+
async function getLocalMcpConsentStatus(identityPath, signerPath) {
|
|
876
1293
|
try {
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
1294
|
+
const input = await buildLocalMcpConsentInput(identityPath, signerPath);
|
|
1295
|
+
const hash = mcp.computeConsentHash(input);
|
|
1296
|
+
const stored = await readLocalMcpAckFile(localMcpAckPath(identityPath));
|
|
1297
|
+
if (stored === hash) {
|
|
1298
|
+
return { acknowledged: true, hash, reason: "ack_file_match" };
|
|
1299
|
+
}
|
|
1300
|
+
return {
|
|
1301
|
+
acknowledged: false,
|
|
1302
|
+
hash,
|
|
1303
|
+
reason: stored ? "ack_file_mismatch" : "ack_file_missing"
|
|
1304
|
+
};
|
|
1305
|
+
} catch (err) {
|
|
1306
|
+
return {
|
|
1307
|
+
acknowledged: false,
|
|
1308
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
function localMcpAckPath(identityPath) {
|
|
1313
|
+
return path.resolve(`${identityPath}.ack.json`);
|
|
1314
|
+
}
|
|
1315
|
+
async function buildLocalMcpConsentInput(identityPath, signerPath) {
|
|
1316
|
+
const credentials = await mcp.loadCredentials({ identityPath, signerPath });
|
|
1317
|
+
const unavailableDuringSetup = {
|
|
1318
|
+
getAllowances: async () => {
|
|
1319
|
+
throw new Error("Haven approval is not complete yet.");
|
|
1320
|
+
}
|
|
1321
|
+
};
|
|
1322
|
+
return mcp.consentInputFromClient(
|
|
1323
|
+
unavailableDuringSetup,
|
|
1324
|
+
{
|
|
1325
|
+
apiKey: credentials.apiKey,
|
|
1326
|
+
apiUrl: credentials.apiUrl,
|
|
1327
|
+
agentId: credentials.agentId,
|
|
1328
|
+
safeAddress: credentials.safeAddress,
|
|
1329
|
+
delegateAddress: credentials.delegateAddress,
|
|
1330
|
+
chainId: credentials.chainId,
|
|
1331
|
+
allowanceSummary: credentials.allowanceSummary
|
|
1332
|
+
},
|
|
1333
|
+
mcp.registeredToolNames()
|
|
1334
|
+
);
|
|
1335
|
+
}
|
|
1336
|
+
async function readLocalMcpAckFile(path) {
|
|
1337
|
+
try {
|
|
1338
|
+
const parsed = JSON.parse(await promises.readFile(path, "utf8"));
|
|
1339
|
+
return typeof parsed.ack === "string" ? parsed.ack : null;
|
|
1340
|
+
} catch {
|
|
1341
|
+
return null;
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
function writeLogChunk(log, chunk) {
|
|
1345
|
+
const message = String(chunk).trimEnd();
|
|
1346
|
+
if (message) log(message);
|
|
1347
|
+
}
|
|
1348
|
+
var init_local_mcp_consent = __esm({
|
|
1349
|
+
"src/local-mcp-consent.ts"() {
|
|
1350
|
+
}
|
|
1351
|
+
});
|
|
1352
|
+
async function probeHostedAgentIdentity(apiKey, apiUrl, fetchImpl = fetch) {
|
|
1353
|
+
let response;
|
|
1354
|
+
try {
|
|
1355
|
+
response = await fetchWithTimeout(fetchImpl, `${apiUrl.replace(/\/+$/, "")}/machine-payments/agent`, {
|
|
1356
|
+
method: "GET",
|
|
1357
|
+
headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" }
|
|
1358
|
+
});
|
|
1359
|
+
} catch {
|
|
1360
|
+
return { status: "network_error" };
|
|
1361
|
+
}
|
|
1362
|
+
if (response.status === 401 || response.status === 403) return { status: "unauthorized" };
|
|
1363
|
+
if (!response.ok) return { status: "bad_response" };
|
|
1364
|
+
try {
|
|
1365
|
+
const payload = JSON.parse(await response.text());
|
|
1366
|
+
if (typeof payload?.delegate_address !== "string") return { status: "bad_response" };
|
|
1367
|
+
return { status: "ok", agentId: payload.id, delegateAddress: payload.delegate_address };
|
|
1368
|
+
} catch {
|
|
1369
|
+
return { status: "bad_response" };
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
async function probeHostedMcpTools(apiKey, hostedMcpUrl, fetchImpl = fetch) {
|
|
1373
|
+
let response;
|
|
1374
|
+
try {
|
|
1375
|
+
response = await fetchWithTimeout(fetchImpl, hostedMcpUrl, {
|
|
1376
|
+
method: "POST",
|
|
1377
|
+
headers: {
|
|
1378
|
+
Authorization: `Bearer ${apiKey}`,
|
|
1379
|
+
"Content-Type": "application/json",
|
|
1380
|
+
Accept: "application/json, text/event-stream"
|
|
883
1381
|
},
|
|
884
1382
|
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list" })
|
|
885
1383
|
});
|
|
@@ -1166,98 +1664,378 @@ var init_signer_runtime = __esm({
|
|
|
1166
1664
|
SIGNER_INSTALL_HEARTBEAT_MS = 15e3;
|
|
1167
1665
|
}
|
|
1168
1666
|
});
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
}
|
|
1186
|
-
};
|
|
1667
|
+
async function prepareLocalMcpRuntime(input, deps = {}) {
|
|
1668
|
+
assertSupportedNodeVersion(input.nodeVersion);
|
|
1669
|
+
const homeDir = input.homeDir ?? os.homedir();
|
|
1670
|
+
const runtimeDirectory = path.resolve(homeDir, ".haven", "mcp-runtime", exports.MCP_RUNTIME_MANIFEST.mcpVersion);
|
|
1671
|
+
const npmCacheDirectory = path.resolve(homeDir, ".haven", "npm-cache");
|
|
1672
|
+
const cliPath = path.join(runtimeDirectory, "node_modules", "@haven_ai", "mcp", "dist", "cli.js");
|
|
1673
|
+
const messages = [];
|
|
1674
|
+
await promises.mkdir(runtimeDirectory, { recursive: true, mode: 448 });
|
|
1675
|
+
await promises.chmod(runtimeDirectory, 448).catch(() => void 0);
|
|
1676
|
+
await promises.mkdir(npmCacheDirectory, { recursive: true, mode: 448 });
|
|
1677
|
+
await promises.chmod(npmCacheDirectory, 448).catch(() => void 0);
|
|
1678
|
+
if (await installedRuntimeMatches2(runtimeDirectory, cliPath)) {
|
|
1679
|
+
messages.push(`Using existing local Haven MCP runtime ${mcpPackageSpec()}.`);
|
|
1680
|
+
} else {
|
|
1681
|
+
await installRuntimePackages2(runtimeDirectory, npmCacheDirectory, deps);
|
|
1682
|
+
messages.push(`Installed local Haven MCP runtime ${mcpPackageSpec()}.`);
|
|
1187
1683
|
}
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1684
|
+
await assertFileExists2(cliPath, "local Haven MCP CLI");
|
|
1685
|
+
const wrapperPath = path.join(input.credentialDirectory, "bin", "haven-mcp");
|
|
1686
|
+
await writeWrapper2({
|
|
1687
|
+
wrapperPath,
|
|
1688
|
+
cliPath,
|
|
1689
|
+
identityPath: input.identityPath,
|
|
1690
|
+
signerPath: input.signerPath
|
|
1691
|
+
});
|
|
1692
|
+
await writeRuntimeSidecar2({
|
|
1693
|
+
path: path.join(input.credentialDirectory, "mcp-runtime.json"),
|
|
1694
|
+
wrapperPath,
|
|
1695
|
+
runtimeDirectory,
|
|
1696
|
+
npmCacheDirectory,
|
|
1697
|
+
cliPath,
|
|
1698
|
+
serverName: input.serverName
|
|
1699
|
+
});
|
|
1700
|
+
messages.push(`Prepared stable local Haven MCP wrapper: ${wrapperPath}`);
|
|
1701
|
+
return {
|
|
1702
|
+
command: wrapperPath,
|
|
1703
|
+
args: [],
|
|
1704
|
+
wrapperPath,
|
|
1705
|
+
runtimeDirectory,
|
|
1706
|
+
npmCacheDirectory,
|
|
1707
|
+
cliPath,
|
|
1708
|
+
messages
|
|
1709
|
+
};
|
|
1193
1710
|
}
|
|
1194
|
-
function
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1711
|
+
function assertSupportedNodeVersion(nodeVersion = process.versions.node, minimumNodeVersion = exports.MCP_RUNTIME_MANIFEST.minimumNodeVersion, subject = "Haven setup") {
|
|
1712
|
+
if (!sdk.isSupportedNodeVersion(nodeVersion, minimumNodeVersion)) {
|
|
1713
|
+
throw new UnsupportedNodeVersionError(nodeVersion, minimumNodeVersion, subject);
|
|
1714
|
+
}
|
|
1198
1715
|
}
|
|
1199
|
-
async function
|
|
1200
|
-
const
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1716
|
+
async function installRuntimePackages2(runtimeDirectory, npmCacheDirectory, deps) {
|
|
1717
|
+
const { runCommand, onProgress } = deps;
|
|
1718
|
+
const baseArgs = [
|
|
1719
|
+
"install",
|
|
1720
|
+
"--prefix",
|
|
1721
|
+
runtimeDirectory,
|
|
1722
|
+
"--no-audit",
|
|
1723
|
+
"--no-fund",
|
|
1724
|
+
"--omit=dev",
|
|
1725
|
+
"--prefer-offline",
|
|
1726
|
+
mcpPackageSpec(),
|
|
1727
|
+
sdkPackageSpec()
|
|
1728
|
+
];
|
|
1729
|
+
const run = async (args) => {
|
|
1730
|
+
const startedAt = Date.now();
|
|
1731
|
+
const heartbeat = setInterval(() => {
|
|
1732
|
+
const seconds = Math.round((Date.now() - startedAt) / 1e3);
|
|
1733
|
+
onProgress?.(`Still installing the local Haven MCP runtime\u2026 (${seconds}s \u2014 a cold cache can take several minutes)`);
|
|
1734
|
+
}, SIGNER_INSTALL_HEARTBEAT_MS);
|
|
1735
|
+
heartbeat.unref?.();
|
|
1736
|
+
try {
|
|
1737
|
+
if (runCommand) await runCommand("npm", args);
|
|
1738
|
+
else await execFileAsync2("npm", args, { timeout: SIGNER_INSTALL_TIMEOUT_MS, maxBuffer: 1024 * 1024 });
|
|
1739
|
+
} finally {
|
|
1740
|
+
clearInterval(heartbeat);
|
|
1209
1741
|
}
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
throw new
|
|
1218
|
-
"runtime_unrecognized",
|
|
1219
|
-
`"${supplied}" is not an agent runtime Haven knows. Valid values: ${exports.RUNTIME_FLAG_VALUES} (the aliases cowork, codex and openclaw are accepted too). Re-run with one of those, or --runtime other to store credentials and finish the MCP setup by hand. Nothing was written and the Haven setup token is still unused.`,
|
|
1220
|
-
"rerun_connect_with_a_valid_runtime_name"
|
|
1221
|
-
);
|
|
1742
|
+
};
|
|
1743
|
+
try {
|
|
1744
|
+
await run(baseArgs);
|
|
1745
|
+
} catch {
|
|
1746
|
+
try {
|
|
1747
|
+
await run([...baseArgs, "--cache", npmCacheDirectory]);
|
|
1748
|
+
} catch (err) {
|
|
1749
|
+
throw new Error(`Could not install local Haven MCP runtime ${mcpPackageSpec()}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1222
1750
|
}
|
|
1223
|
-
return { runtime: detected, source: "detected", discardedHint: supplied };
|
|
1224
|
-
}
|
|
1225
|
-
if (detected && hint && detected !== hint) {
|
|
1226
|
-
return { runtime: detected, source: "detected", overrodeHint: hint };
|
|
1227
|
-
}
|
|
1228
|
-
if (hint) return { runtime: hint, source: "explicit" };
|
|
1229
|
-
if (detected) return { runtime: detected, source: "detected" };
|
|
1230
|
-
if (options.promptForRuntime) {
|
|
1231
|
-
return { runtime: await options.promptForRuntime(), source: "prompted" };
|
|
1232
1751
|
}
|
|
1233
|
-
return { runtime: null, source: "none" };
|
|
1234
|
-
}
|
|
1235
|
-
function restartRequiredForRuntime(runtime, env = process.env) {
|
|
1236
|
-
const mode = runtimeProfile(runtime, env).restartMode;
|
|
1237
|
-
return mode === "restart-session" || mode === "restart-app";
|
|
1238
1752
|
}
|
|
1239
|
-
function
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1753
|
+
async function installedRuntimeMatches2(runtimeDirectory, cliPath) {
|
|
1754
|
+
try {
|
|
1755
|
+
await assertFileExists2(cliPath, "local Haven MCP CLI");
|
|
1756
|
+
const [mcpPackage, sdkPackage] = await Promise.all([
|
|
1757
|
+
readPackageJson2(path.join(runtimeDirectory, "node_modules", "@haven_ai", "mcp", "package.json")),
|
|
1758
|
+
readPackageJson2(path.join(runtimeDirectory, "node_modules", "@haven_ai", "sdk", "package.json"))
|
|
1759
|
+
]);
|
|
1760
|
+
return mcpPackage.version === exports.MCP_RUNTIME_MANIFEST.mcpVersion && sdkPackage.version === exports.MCP_RUNTIME_MANIFEST.sdkVersion;
|
|
1761
|
+
} catch {
|
|
1762
|
+
return false;
|
|
1763
|
+
}
|
|
1247
1764
|
}
|
|
1248
|
-
function
|
|
1249
|
-
|
|
1250
|
-
if (env.CODEX_SANDBOX || env.CODEX_HOME || env.CODEX_CWD) return "codex-cli";
|
|
1251
|
-
if (env.VSCODE_CWD || env.VSCODE_IPC_HOOK_CLI || env.TERM_PROGRAM === "vscode") return "vscode";
|
|
1252
|
-
if (env.HERMES_HOME || env.HERMES_AGENT) return "hermes";
|
|
1253
|
-
return null;
|
|
1765
|
+
async function readPackageJson2(path) {
|
|
1766
|
+
return JSON.parse(await promises.readFile(path, "utf8"));
|
|
1254
1767
|
}
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1768
|
+
async function writeWrapper2(input) {
|
|
1769
|
+
await promises.mkdir(path.dirname(input.wrapperPath), { recursive: true, mode: 448 });
|
|
1770
|
+
await promises.chmod(path.dirname(input.wrapperPath), 448).catch(() => void 0);
|
|
1771
|
+
const source = [
|
|
1772
|
+
"#!/usr/bin/env node",
|
|
1773
|
+
"import { spawn } from 'node:child_process'",
|
|
1774
|
+
"",
|
|
1775
|
+
`const cliPath = ${JSON.stringify(input.cliPath)}`,
|
|
1776
|
+
`const identityPath = ${JSON.stringify(input.identityPath)}`,
|
|
1777
|
+
`const signerPath = ${JSON.stringify(input.signerPath)}`,
|
|
1778
|
+
"",
|
|
1779
|
+
"const child = spawn(process.execPath, [cliPath, '--identity', identityPath, '--signer', signerPath, ...process.argv.slice(2)], {",
|
|
1780
|
+
" stdio: 'inherit',",
|
|
1781
|
+
"})",
|
|
1782
|
+
"",
|
|
1783
|
+
"child.on('exit', (code, signal) => {",
|
|
1784
|
+
" if (signal) process.kill(process.pid, signal)",
|
|
1785
|
+
" else process.exit(code ?? 1)",
|
|
1786
|
+
"})",
|
|
1787
|
+
""
|
|
1788
|
+
].join("\n");
|
|
1789
|
+
await promises.writeFile(input.wrapperPath, source, { mode: 448 });
|
|
1790
|
+
await promises.chmod(input.wrapperPath, 448).catch(() => void 0);
|
|
1791
|
+
}
|
|
1792
|
+
async function writeRuntimeSidecar2(input) {
|
|
1793
|
+
const value = {
|
|
1794
|
+
...input.serverName ? { server_name: input.serverName } : {},
|
|
1795
|
+
mcp_package: exports.MCP_RUNTIME_MANIFEST.mcpPackage,
|
|
1796
|
+
mcp_version: exports.MCP_RUNTIME_MANIFEST.mcpVersion,
|
|
1797
|
+
sdk_package: exports.MCP_RUNTIME_MANIFEST.sdkPackage,
|
|
1798
|
+
sdk_version: exports.MCP_RUNTIME_MANIFEST.sdkVersion,
|
|
1799
|
+
minimum_node_version: exports.MCP_RUNTIME_MANIFEST.minimumNodeVersion,
|
|
1800
|
+
wrapper_path: input.wrapperPath,
|
|
1801
|
+
runtime_directory: input.runtimeDirectory,
|
|
1802
|
+
npm_cache_directory: input.npmCacheDirectory,
|
|
1803
|
+
cli_path: input.cliPath
|
|
1804
|
+
};
|
|
1805
|
+
await promises.writeFile(input.path, `${JSON.stringify(value, null, 2)}
|
|
1806
|
+
`, { mode: 384 });
|
|
1807
|
+
await promises.chmod(input.path, 384).catch(() => void 0);
|
|
1808
|
+
}
|
|
1809
|
+
async function assertFileExists2(path, label) {
|
|
1810
|
+
try {
|
|
1811
|
+
await promises.access(path);
|
|
1812
|
+
} catch {
|
|
1813
|
+
throw new Error(`Missing ${label}: ${path}`);
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
var execFileAsync2, UnsupportedNodeVersionError;
|
|
1817
|
+
var init_local_mcp_runtime = __esm({
|
|
1818
|
+
"src/local-mcp-runtime.ts"() {
|
|
1819
|
+
init_signer_runtime();
|
|
1820
|
+
init_runtime_manifest();
|
|
1821
|
+
execFileAsync2 = util.promisify(child_process.execFile);
|
|
1822
|
+
UnsupportedNodeVersionError = class extends Error {
|
|
1823
|
+
code = "local_mcp_unsupported_node_version";
|
|
1824
|
+
nodeVersion;
|
|
1825
|
+
minimumNodeVersion;
|
|
1826
|
+
constructor(nodeVersion, minimumNodeVersion, subject = "Haven setup") {
|
|
1827
|
+
super(sdk.unsupportedNodeVersionMessage({ subject, nodeVersion, minimumNodeVersion }));
|
|
1828
|
+
this.name = "UnsupportedNodeVersionError";
|
|
1829
|
+
this.nodeVersion = nodeVersion;
|
|
1830
|
+
this.minimumNodeVersion = minimumNodeVersion;
|
|
1831
|
+
}
|
|
1832
|
+
};
|
|
1833
|
+
}
|
|
1834
|
+
});
|
|
1835
|
+
async function installSkillForRuntime(runtime, deps = {}) {
|
|
1836
|
+
switch (runtime) {
|
|
1837
|
+
case "claude-code":
|
|
1838
|
+
return installSkillFile(
|
|
1839
|
+
path.resolve(deps.homeDir ?? os.homedir(), ".claude", "skills", sdk.SKILL_FOLDER_NAME),
|
|
1840
|
+
"~/.claude/skills/haven-pay"
|
|
1841
|
+
);
|
|
1842
|
+
case "hermes":
|
|
1843
|
+
return installSkillFile(
|
|
1844
|
+
path.join(hermesHome(deps), "skills", sdk.SKILL_FOLDER_NAME),
|
|
1845
|
+
"the Hermes skills folder"
|
|
1846
|
+
);
|
|
1847
|
+
case "codex-cli":
|
|
1848
|
+
case "codex-desktop":
|
|
1849
|
+
return installCodexAgentsSection(deps);
|
|
1850
|
+
default:
|
|
1851
|
+
return void 0;
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
async function installSkillFile(skillDir, label) {
|
|
1855
|
+
try {
|
|
1856
|
+
await promises.mkdir(skillDir, { recursive: true });
|
|
1857
|
+
const target = path.join(skillDir, "SKILL.md");
|
|
1858
|
+
await promises.writeFile(target, sdk.HAVEN_SKILL_MD, "utf8");
|
|
1859
|
+
return {
|
|
1860
|
+
installed: true,
|
|
1861
|
+
target,
|
|
1862
|
+
messages: [`Installed the generic Haven payment skill (${label}). It contains no secrets.`]
|
|
1863
|
+
};
|
|
1864
|
+
} catch (err) {
|
|
1865
|
+
return {
|
|
1866
|
+
installed: false,
|
|
1867
|
+
messages: [
|
|
1868
|
+
`Could not install the Haven payment skill: ${err instanceof Error ? err.message : String(err)}. Download it from the Haven dashboard instead.`
|
|
1869
|
+
]
|
|
1870
|
+
};
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
async function installCodexAgentsSection(deps) {
|
|
1874
|
+
try {
|
|
1875
|
+
const codexDir = path.resolve(deps.homeDir ?? os.homedir(), ".codex");
|
|
1876
|
+
const target = path.join(codexDir, "AGENTS.md");
|
|
1877
|
+
await promises.mkdir(codexDir, { recursive: true });
|
|
1878
|
+
const existing = await promises.readFile(target, "utf8").catch(() => null);
|
|
1879
|
+
const next = upsertManagedSection(existing, codexManagedSection());
|
|
1880
|
+
if (next !== existing) {
|
|
1881
|
+
await promises.writeFile(target, next, "utf8");
|
|
1882
|
+
}
|
|
1883
|
+
return {
|
|
1884
|
+
installed: true,
|
|
1885
|
+
target,
|
|
1886
|
+
messages: [
|
|
1887
|
+
"Installed the generic Haven payment guidance as a managed section in ~/.codex/AGENTS.md (Codex reads it as global instructions). It contains no secrets; your own content in that file is untouched."
|
|
1888
|
+
]
|
|
1889
|
+
};
|
|
1890
|
+
} catch (err) {
|
|
1891
|
+
return {
|
|
1892
|
+
installed: false,
|
|
1893
|
+
messages: [
|
|
1894
|
+
`Could not install the Haven payment guidance into ~/.codex/AGENTS.md: ${err instanceof Error ? err.message : String(err)}. Download the skill from the Haven dashboard instead.`
|
|
1895
|
+
]
|
|
1896
|
+
};
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
function codexManagedSection() {
|
|
1900
|
+
return `${CODEX_AGENTS_BEGIN_MARKER}
|
|
1901
|
+
|
|
1902
|
+
${sdk.HAVEN_SKILL_BODY_MD.trimEnd()}
|
|
1903
|
+
|
|
1904
|
+
${CODEX_AGENTS_END_MARKER}
|
|
1905
|
+
`;
|
|
1906
|
+
}
|
|
1907
|
+
function upsertManagedSection(existing, section) {
|
|
1908
|
+
if (existing === null || existing.trim() === "") return section;
|
|
1909
|
+
const begins = markerLineIndexes(existing, CODEX_AGENTS_BEGIN_MARKER);
|
|
1910
|
+
const ends = markerLineIndexes(existing, CODEX_AGENTS_END_MARKER);
|
|
1911
|
+
if (begins.length === 1 && ends.length === 1 && ends[0] > begins[0]) {
|
|
1912
|
+
const afterEnd = ends[0] + CODEX_AGENTS_END_MARKER.length;
|
|
1913
|
+
const tail = existing.startsWith("\r\n", afterEnd) ? existing.slice(afterEnd + 2) : existing.startsWith("\n", afterEnd) ? existing.slice(afterEnd + 1) : existing.slice(afterEnd);
|
|
1914
|
+
return existing.slice(0, begins[0]) + section + tail;
|
|
1915
|
+
}
|
|
1916
|
+
if (begins.length > 0 || ends.length > 0) {
|
|
1917
|
+
throw new Error(
|
|
1918
|
+
"found a damaged Haven marker section (orphaned or duplicated markers); remove the leftover marker lines and re-run setup"
|
|
1919
|
+
);
|
|
1920
|
+
}
|
|
1921
|
+
return `${existing.replace(/\n*$/, "\n\n")}${section}`;
|
|
1922
|
+
}
|
|
1923
|
+
function markerLineIndexes(text, marker) {
|
|
1924
|
+
const indexes = [];
|
|
1925
|
+
for (let from = 0; ; ) {
|
|
1926
|
+
const at = text.indexOf(marker, from);
|
|
1927
|
+
if (at === -1) return indexes;
|
|
1928
|
+
if (at === 0 || text[at - 1] === "\n") indexes.push(at);
|
|
1929
|
+
from = at + marker.length;
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
function hermesHome(deps) {
|
|
1933
|
+
const env = deps.env ?? process.env;
|
|
1934
|
+
return env.HERMES_HOME ?? path.join(deps.homeDir ?? os.homedir(), ".hermes");
|
|
1935
|
+
}
|
|
1936
|
+
var CODEX_AGENTS_BEGIN_MARKER, CODEX_AGENTS_END_MARKER;
|
|
1937
|
+
var init_skill_install = __esm({
|
|
1938
|
+
"src/skill-install.ts"() {
|
|
1939
|
+
CODEX_AGENTS_BEGIN_MARKER = "<!-- BEGIN haven-pay (managed by @haven_ai/connect; edits inside this section are overwritten on re-setup) -->";
|
|
1940
|
+
CODEX_AGENTS_END_MARKER = "<!-- END haven-pay -->";
|
|
1941
|
+
}
|
|
1942
|
+
});
|
|
1943
|
+
|
|
1944
|
+
// src/connect-error.ts
|
|
1945
|
+
function isConnectError(err) {
|
|
1946
|
+
return err instanceof exports.ConnectError;
|
|
1947
|
+
}
|
|
1948
|
+
exports.ConnectError = void 0;
|
|
1949
|
+
var init_connect_error = __esm({
|
|
1950
|
+
"src/connect-error.ts"() {
|
|
1951
|
+
exports.ConnectError = class extends Error {
|
|
1952
|
+
code;
|
|
1953
|
+
nextAction;
|
|
1954
|
+
details;
|
|
1955
|
+
constructor(code, message, nextAction2, details = {}) {
|
|
1956
|
+
super(message);
|
|
1957
|
+
this.name = "ConnectError";
|
|
1958
|
+
this.code = code;
|
|
1959
|
+
this.nextAction = nextAction2;
|
|
1960
|
+
this.details = details;
|
|
1961
|
+
}
|
|
1962
|
+
};
|
|
1963
|
+
}
|
|
1964
|
+
});
|
|
1965
|
+
|
|
1966
|
+
// src/runtime-registry.ts
|
|
1967
|
+
function runtimeProfile(runtime, env = process.env) {
|
|
1968
|
+
return RUNTIME_PROFILES[normalizeRuntime(runtime, env)];
|
|
1969
|
+
}
|
|
1970
|
+
function normalizeRuntime(runtime, env = process.env) {
|
|
1971
|
+
const explicit = normalizeRuntimeName(runtime);
|
|
1972
|
+
if (explicit) return explicit;
|
|
1973
|
+
return detectRuntime(env) ?? "other";
|
|
1974
|
+
}
|
|
1975
|
+
async function resolveRuntimeSelection(explicit, force, options = {}) {
|
|
1976
|
+
const env = options.env ?? process.env;
|
|
1977
|
+
if (force !== void 0) {
|
|
1978
|
+
const forced = normalizeRuntimeName(force);
|
|
1979
|
+
if (!forced) {
|
|
1980
|
+
throw new exports.ConnectError(
|
|
1981
|
+
"runtime_force_unrecognized",
|
|
1982
|
+
`Unknown --runtime-force value "${force}". Valid values: ${exports.RUNTIME_FLAG_VALUES}.`,
|
|
1983
|
+
"rerun_connect_with_a_valid_runtime_name",
|
|
1984
|
+
{ allowedRuntimes: RUNTIME_FLAG_VALUE_LIST }
|
|
1985
|
+
);
|
|
1986
|
+
}
|
|
1987
|
+
return { runtime: forced, source: "force" };
|
|
1988
|
+
}
|
|
1989
|
+
const detected = detectRuntime(env);
|
|
1990
|
+
const supplied = explicit?.trim() || options.selfReported?.trim() || void 0;
|
|
1991
|
+
const hint = normalizeRuntimeName(supplied);
|
|
1992
|
+
if (supplied && !hint) {
|
|
1993
|
+
if (!detected) {
|
|
1994
|
+
throw new exports.ConnectError(
|
|
1995
|
+
"runtime_unrecognized",
|
|
1996
|
+
`"${supplied}" is not an agent runtime Haven knows. Valid values: ${exports.RUNTIME_FLAG_VALUES} (the aliases cowork, codex and openclaw are accepted too). Re-run with one of those, or --runtime other to store credentials and finish the MCP setup by hand. Nothing was written and the Haven setup token is still unused.`,
|
|
1997
|
+
"rerun_connect_with_a_valid_runtime_name",
|
|
1998
|
+
{ allowedRuntimes: RUNTIME_FLAG_VALUE_LIST }
|
|
1999
|
+
);
|
|
2000
|
+
}
|
|
2001
|
+
return { runtime: detected, source: "detected", discardedHint: supplied };
|
|
2002
|
+
}
|
|
2003
|
+
if (detected && hint && detected !== hint) {
|
|
2004
|
+
return { runtime: detected, source: "detected", overrodeHint: hint };
|
|
2005
|
+
}
|
|
2006
|
+
if (hint) return { runtime: hint, source: "explicit" };
|
|
2007
|
+
if (detected) return { runtime: detected, source: "detected" };
|
|
2008
|
+
if (options.promptForRuntime) {
|
|
2009
|
+
return { runtime: await options.promptForRuntime(), source: "prompted" };
|
|
2010
|
+
}
|
|
2011
|
+
return { runtime: null, source: "none" };
|
|
2012
|
+
}
|
|
2013
|
+
function restartRequiredForRuntime(runtime, env = process.env) {
|
|
2014
|
+
const mode = runtimeProfile(runtime, env).restartMode;
|
|
2015
|
+
return mode === "restart-session" || mode === "restart-app";
|
|
2016
|
+
}
|
|
2017
|
+
function runtimeVerificationInstruction(runtime) {
|
|
2018
|
+
const label = RUNTIME_PROFILES[runtime].label;
|
|
2019
|
+
return `In ${label}, run the read-only \`haven_get_agent\` and \`haven_get_allowances\` tools to confirm the Haven wallet and live budget. Do not sign, fund, or create a payment to verify setup.`;
|
|
2020
|
+
}
|
|
2021
|
+
function normalizeRuntimeName(runtime) {
|
|
2022
|
+
const key = runtime?.trim().toLowerCase();
|
|
2023
|
+
if (!key) return null;
|
|
2024
|
+
return RUNTIME_ALIASES[key.replace(/\s+/g, "-")] ?? null;
|
|
2025
|
+
}
|
|
2026
|
+
function detectRuntime(env) {
|
|
2027
|
+
if (env.CLAUDECODE || env.CLAUDE_CODE || env.CLAUDECODE_CWD) return "claude-code";
|
|
2028
|
+
if (env.CODEX_SANDBOX || env.CODEX_HOME || env.CODEX_CWD) return "codex-cli";
|
|
2029
|
+
if (env.VSCODE_CWD || env.VSCODE_IPC_HOOK_CLI || env.TERM_PROGRAM === "vscode") return "vscode";
|
|
2030
|
+
if (env.HERMES_HOME || env.HERMES_AGENT) return "hermes";
|
|
2031
|
+
return null;
|
|
2032
|
+
}
|
|
2033
|
+
var RUNTIME_PROFILES, RUNTIME_ALIASES, RUNTIME_FLAG_VALUE_LIST; exports.RUNTIME_FLAG_VALUES = void 0;
|
|
2034
|
+
var init_runtime_registry = __esm({
|
|
2035
|
+
"src/runtime-registry.ts"() {
|
|
2036
|
+
init_connect_error();
|
|
2037
|
+
RUNTIME_PROFILES = {
|
|
2038
|
+
"claude-code": {
|
|
1261
2039
|
id: "claude-code",
|
|
1262
2040
|
label: "Claude Code",
|
|
1263
2041
|
restartMode: "restart-session",
|
|
@@ -1371,7 +2149,18 @@ var init_runtime_registry = __esm({
|
|
|
1371
2149
|
other: "other",
|
|
1372
2150
|
manual: "other"
|
|
1373
2151
|
};
|
|
1374
|
-
|
|
2152
|
+
RUNTIME_FLAG_VALUE_LIST = [
|
|
2153
|
+
"claude-code",
|
|
2154
|
+
"codex-cli",
|
|
2155
|
+
"codex-desktop",
|
|
2156
|
+
"cursor",
|
|
2157
|
+
"vscode",
|
|
2158
|
+
"vscode-insiders",
|
|
2159
|
+
"claude-desktop",
|
|
2160
|
+
"hermes",
|
|
2161
|
+
"other"
|
|
2162
|
+
];
|
|
2163
|
+
exports.RUNTIME_FLAG_VALUES = RUNTIME_FLAG_VALUE_LIST.join(", ");
|
|
1375
2164
|
}
|
|
1376
2165
|
});
|
|
1377
2166
|
async function acknowledgeLocalSignerConsent(signerPath, log) {
|
|
@@ -1447,1519 +2236,1319 @@ var init_signer_consent = __esm({
|
|
|
1447
2236
|
"src/signer-consent.ts"() {
|
|
1448
2237
|
}
|
|
1449
2238
|
});
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
].join("\n");
|
|
1485
|
-
}
|
|
1486
|
-
async function writeAgentTombstone(input) {
|
|
1487
|
-
const dirStat = await promises.stat(input.directory).catch(() => null);
|
|
1488
|
-
if (!dirStat?.isDirectory()) {
|
|
1489
|
-
throw new Error(`Not a directory: ${input.directory} \u2014 nothing to tombstone.`);
|
|
1490
|
-
}
|
|
1491
|
-
const info = {
|
|
1492
|
-
// reason / replaced_by are persisted to disk and re-emitted to the host's
|
|
1493
|
-
// MCP stderr log on EVERY stale probe, potentially for months — redact
|
|
1494
|
-
// like every other output path, at the write layer so any future caller
|
|
1495
|
-
// inherits it. (#1681 review, finding 1)
|
|
1496
|
-
agent_id: input.agentId,
|
|
1497
|
-
retired_at: input.retiredAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1498
|
-
reason: redactSecrets(input.reason),
|
|
1499
|
-
...input.replacedBy ? { replaced_by: redactSecrets(input.replacedBy) } : {}
|
|
1500
|
-
};
|
|
1501
|
-
const binDir = path.join(input.directory, "bin");
|
|
1502
|
-
await promises.mkdir(binDir, { recursive: true });
|
|
1503
|
-
const wrapperPath = path.join(binDir, "haven-signer.mjs");
|
|
1504
|
-
await promises.writeFile(wrapperPath, tombstoneScript(info), "utf8");
|
|
1505
|
-
await promises.chmod(wrapperPath, 493);
|
|
1506
|
-
await promises.writeFile(path.join(input.directory, TOMBSTONE_FILENAME), JSON.stringify(info, null, 2) + "\n", "utf8");
|
|
1507
|
-
return info;
|
|
1508
|
-
}
|
|
1509
|
-
async function readAgentTombstone(directory) {
|
|
1510
|
-
try {
|
|
1511
|
-
const parsed = JSON.parse(await promises.readFile(path.join(directory, TOMBSTONE_FILENAME), "utf8"));
|
|
1512
|
-
if (typeof parsed?.agent_id !== "string") return null;
|
|
1513
|
-
return parsed;
|
|
1514
|
-
} catch {
|
|
1515
|
-
return null;
|
|
1516
|
-
}
|
|
1517
|
-
}
|
|
1518
|
-
var TOMBSTONE_FILENAME, TOMBSTONE_MARKER;
|
|
1519
|
-
var init_tombstone = __esm({
|
|
1520
|
-
"src/tombstone.ts"() {
|
|
1521
|
-
init_redact();
|
|
1522
|
-
TOMBSTONE_FILENAME = "TOMBSTONE.json";
|
|
1523
|
-
TOMBSTONE_MARKER = "HAVEN-TOMBSTONE";
|
|
1524
|
-
}
|
|
1525
|
-
});
|
|
1526
|
-
|
|
1527
|
-
// src/doctor.ts
|
|
1528
|
-
var doctor_exports = {};
|
|
1529
|
-
__export(doctor_exports, {
|
|
1530
|
-
runDoctor: () => runDoctor,
|
|
1531
|
-
runRepair: () => runRepair
|
|
1532
|
-
});
|
|
1533
|
-
async function discoverCredentialDirectory(homeDir, explicit) {
|
|
1534
|
-
const root = explicit ? path.dirname(explicit) : path.join(homeDir, ".haven", "agents");
|
|
1535
|
-
let entries = [];
|
|
1536
|
-
try {
|
|
1537
|
-
entries = await promises.readdir(root);
|
|
1538
|
-
} catch {
|
|
1539
|
-
return explicit ? { directory: explicit, others: [] } : { others: [] };
|
|
2239
|
+
async function installRuntime(input, deps = {}) {
|
|
2240
|
+
const runtime = normalizeRuntime(input.runtime, deps.env);
|
|
2241
|
+
const profile = runtimeProfile(runtime, deps.env);
|
|
2242
|
+
const progress = deps.onProgress ?? (() => void 0);
|
|
2243
|
+
const localRuntime = input.localMcp === true && supportsLocalMcp(runtime);
|
|
2244
|
+
const consentMessages = [];
|
|
2245
|
+
const localMcpConsent = localRuntime ? await resolveLocalMcpConsent(input, consentMessages) : void 0;
|
|
2246
|
+
const signerConsent = localRuntime ? void 0 : await resolveSignerConsent(input, consentMessages);
|
|
2247
|
+
if (runtime === "other") {
|
|
2248
|
+
const signerCredentialReady2 = await probeLocalSignerCredential(input.signerPath);
|
|
2249
|
+
const signerReady = signerCredentialReady2 && signerConsent?.acknowledged;
|
|
2250
|
+
return {
|
|
2251
|
+
runtime,
|
|
2252
|
+
runtimeMcpMode: "manual",
|
|
2253
|
+
hostedMcpConfigured: false,
|
|
2254
|
+
localSignerConfigured: false,
|
|
2255
|
+
localMcpConfigured: false,
|
|
2256
|
+
probeResult: signerReady ? "manual_runtime_setup_required_local_signer_ready" : "manual_runtime_setup_required_local_signer_unavailable",
|
|
2257
|
+
restartRequired: true,
|
|
2258
|
+
nextUserAction: "return_to_haven_for_wallet_approval_then_configure_runtime",
|
|
2259
|
+
errorCode: "manual_runtime_setup_required",
|
|
2260
|
+
configTarget: "manual runtime setup",
|
|
2261
|
+
signerAcknowledged: signerConsent?.acknowledged,
|
|
2262
|
+
localMcpAcknowledged: false,
|
|
2263
|
+
messages: [
|
|
2264
|
+
...consentMessages,
|
|
2265
|
+
"Custom runtime: Haven did not auto-configure it. Your credentials are on disk (chmod 600) \u2014 read them at runtime; never paste a key into the agent prompt, memory, or logs.",
|
|
2266
|
+
` identity (hosted MCP Bearer): ${input.identityPath}`,
|
|
2267
|
+
` signer (local signing key): ${input.signerPath}`,
|
|
2268
|
+
"After wallet approval, wire the runtime to Haven by reference:",
|
|
2269
|
+
` Hosted MCP + local signer: point your MCP client at ${input.hostedMcpUrl} with the api_key from identity.json, then run npx -y ${signerPackageSpec()} --credentials ${input.signerPath}`,
|
|
2270
|
+
` Fully local MCP (no hosted dependency): npx -y ${mcpPackageSpec()} --identity ${input.identityPath} --signer ${input.signerPath}`
|
|
2271
|
+
]
|
|
2272
|
+
};
|
|
1540
2273
|
}
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
const directory = path.join(root, entry);
|
|
2274
|
+
let localRuntimeInstall;
|
|
2275
|
+
let localRuntimeError;
|
|
2276
|
+
if (localRuntime) {
|
|
1545
2277
|
try {
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
try {
|
|
1550
|
-
await promises.stat(path.join(directory, TOMBSTONE_FILENAME));
|
|
1551
|
-
tombstonedOnly.push(directory);
|
|
1552
|
-
} catch {
|
|
1553
|
-
}
|
|
2278
|
+
localRuntimeInstall = await prepareRuntimeForLocalMcp(input, deps);
|
|
2279
|
+
} catch (err) {
|
|
2280
|
+
localRuntimeError = err;
|
|
1554
2281
|
}
|
|
1555
2282
|
}
|
|
1556
|
-
|
|
1557
|
-
|
|
2283
|
+
if (localRuntimeError) {
|
|
2284
|
+
const errorCode2 = localRuntimePrepareErrorCode(localRuntimeError);
|
|
1558
2285
|
return {
|
|
1559
|
-
|
|
1560
|
-
|
|
2286
|
+
runtime,
|
|
2287
|
+
runtimeMcpMode: "local_stdio",
|
|
2288
|
+
hostedMcpConfigured: false,
|
|
2289
|
+
localSignerConfigured: false,
|
|
2290
|
+
localMcpConfigured: false,
|
|
2291
|
+
probeResult: errorCode2 === "local_mcp_unsupported_node_version" ? "local_stdio_mcp_unsupported_node_version" : "local_stdio_mcp_runtime_install_failed",
|
|
2292
|
+
restartRequired: true,
|
|
2293
|
+
nextUserAction: nextAction(runtime, profile.restartMode, errorCode2),
|
|
2294
|
+
errorCode: errorCode2,
|
|
2295
|
+
configTarget: profile.label,
|
|
2296
|
+
signerAcknowledged: signerConsent?.acknowledged,
|
|
2297
|
+
localMcpAcknowledged: localMcpConsent?.acknowledged,
|
|
2298
|
+
activationCommand: void 0,
|
|
2299
|
+
messages: [
|
|
2300
|
+
...consentMessages,
|
|
2301
|
+
`Could not prepare local Haven MCP runtime: ${localRuntimeError instanceof Error ? localRuntimeError.message : String(localRuntimeError)}`
|
|
2302
|
+
]
|
|
1561
2303
|
};
|
|
1562
2304
|
}
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
}
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
2305
|
+
let signerCommand;
|
|
2306
|
+
if (!localRuntime) {
|
|
2307
|
+
progress("Getting the signer ready\u2026");
|
|
2308
|
+
try {
|
|
2309
|
+
const signerRuntime = await prepareSignerForRuntime(input, deps);
|
|
2310
|
+
signerCommand = { command: signerRuntime.command, args: signerRuntime.args };
|
|
2311
|
+
consentMessages.push(...signerRuntime.messages);
|
|
2312
|
+
} catch (err) {
|
|
2313
|
+
return {
|
|
2314
|
+
runtime,
|
|
2315
|
+
runtimeMcpMode: "hosted_plus_signer",
|
|
2316
|
+
hostedMcpConfigured: false,
|
|
2317
|
+
localSignerConfigured: false,
|
|
2318
|
+
localMcpConfigured: false,
|
|
2319
|
+
probeResult: "signer_runtime_install_failed",
|
|
2320
|
+
restartRequired: false,
|
|
2321
|
+
nextUserAction: "The local Haven signer runtime could not be installed, so no configuration was written. Check your network (a cold install downloads the signer package set) and re-run: npx @haven_ai/connect@alpha",
|
|
2322
|
+
errorCode: "signer_runtime_install_failed",
|
|
2323
|
+
configTarget: profile.label,
|
|
2324
|
+
signerAcknowledged: signerConsent?.acknowledged,
|
|
2325
|
+
localMcpAcknowledged: localMcpConsent?.acknowledged,
|
|
2326
|
+
activationCommand: void 0,
|
|
2327
|
+
signerRuntimePrepared: false,
|
|
2328
|
+
messages: [
|
|
2329
|
+
...consentMessages,
|
|
2330
|
+
`Could not pre-install the local Haven signer: ${err instanceof Error ? err.message : String(err)}`,
|
|
2331
|
+
"No runtime configuration was written (fail-closed): a config pointing at an uninstalled signer looks wired but cannot start.",
|
|
2332
|
+
"Re-run `npx @haven_ai/connect@alpha` to retry the setup."
|
|
2333
|
+
]
|
|
2334
|
+
};
|
|
1574
2335
|
}
|
|
1575
|
-
return false;
|
|
1576
|
-
}
|
|
1577
|
-
if (sidecar?.wrapper_path && configText.includes(sidecar.wrapper_path)) return true;
|
|
1578
|
-
if (bareOwnerExists) return false;
|
|
1579
|
-
return isPrimary && Boolean(identity?.hosted_mcp_url && configText.includes(identity.hosted_mcp_url));
|
|
1580
|
-
}
|
|
1581
|
-
async function readIdentity(directory) {
|
|
1582
|
-
try {
|
|
1583
|
-
return JSON.parse(await promises.readFile(path.join(directory, "identity.json"), "utf8"));
|
|
1584
|
-
} catch {
|
|
1585
|
-
return void 0;
|
|
1586
2336
|
}
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
const
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
2337
|
+
const signerRuntimePrepared = localRuntime ? void 0 : signerCommand !== void 0;
|
|
2338
|
+
progress("Setting up your Haven tools\u2026");
|
|
2339
|
+
const configResult = localRuntime ? runtime === "claude-code" ? await configureClaudeCode(deps, localRuntimeInstall?.command ?? "", input.serverName) : await writeRuntimeConfig({
|
|
2340
|
+
runtime,
|
|
2341
|
+
hostedMcpUrl: input.hostedMcpUrl,
|
|
2342
|
+
apiKey: input.apiKey,
|
|
2343
|
+
identityPath: input.identityPath,
|
|
2344
|
+
signerPath: input.signerPath,
|
|
2345
|
+
serverName: input.serverName,
|
|
2346
|
+
credentialDirectory: input.credentialDirectory,
|
|
2347
|
+
localMcpCommand: localRuntimeInstall?.command,
|
|
2348
|
+
signerCommand,
|
|
2349
|
+
homeDir: deps.homeDir,
|
|
2350
|
+
mode: "local"
|
|
2351
|
+
}) : await writeHostedRuntimeConfig(deps, { ...input, runtime }, signerCommand);
|
|
2352
|
+
if (deps.onRuntimeConfigured) {
|
|
2353
|
+
const signerCredentialOnDisk = await probeLocalSignerCredential(input.signerPath);
|
|
2354
|
+
const earlyLocalMcpOk = configResult.runtimeMcpMode === "local_stdio" && configResult.localMcpConfigured && signerCredentialOnDisk && Boolean(localMcpConsent?.acknowledged);
|
|
2355
|
+
const earlySignerOk = configResult.runtimeMcpMode === "local_stdio" ? earlyLocalMcpOk : configResult.signerConfigured && signerCredentialOnDisk && Boolean(signerConsent?.acknowledged);
|
|
2356
|
+
try {
|
|
2357
|
+
await deps.onRuntimeConfigured({
|
|
2358
|
+
runtime,
|
|
2359
|
+
runtimeMcpMode: configResult.runtimeMcpMode,
|
|
2360
|
+
hostedMcpConfigured: configResult.hostedConfigured,
|
|
2361
|
+
localSignerConfigured: earlySignerOk,
|
|
2362
|
+
localMcpConfigured: earlyLocalMcpOk,
|
|
2363
|
+
signerAcknowledged: signerConsent?.acknowledged,
|
|
2364
|
+
localMcpAcknowledged: localMcpConsent?.acknowledged,
|
|
2365
|
+
restartRequired: configResult.restartRequired || restartRequiredForRuntime(runtime, deps.env),
|
|
2366
|
+
nextUserAction: nextAction(runtime, profile.restartMode, configResult.errorCode),
|
|
2367
|
+
errorCode: configResult.errorCode
|
|
2368
|
+
});
|
|
2369
|
+
} catch {
|
|
2370
|
+
}
|
|
1598
2371
|
}
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
detail: "No stored API key / hosted MCP URL to probe with.",
|
|
1645
|
-
repair: `Re-run the full setup: ${RERUN} --setup <token>.`
|
|
1646
|
-
});
|
|
1647
|
-
}
|
|
1648
|
-
const localDelegate = typeof signerFile?.delegate_address === "string" ? signerFile.delegate_address : void 0;
|
|
1649
|
-
if (identity?.api_key && identity.api_url) {
|
|
1650
|
-
const probe = await (deps.probeHostedIdentity ?? probeHostedAgentIdentity)(
|
|
1651
|
-
identity.api_key,
|
|
1652
|
-
identity.api_url,
|
|
1653
|
-
deps.fetch
|
|
1654
|
-
);
|
|
1655
|
-
if (probe.status !== "ok") {
|
|
1656
|
-
checks.push({
|
|
1657
|
-
id: "identity_match",
|
|
1658
|
-
label: "Hosted identity matches the local signing key",
|
|
1659
|
-
ok: false,
|
|
1660
|
-
detail: probe.status === "unauthorized" ? "The stored API key was rejected, so the agent it authenticates as cannot be compared with the local signing key." : `Could not read the hosted identity (${probe.status}) \u2014 the comparison did not happen, so it cannot be reported as a match.`,
|
|
1661
|
-
repair: probe.status === "unauthorized" ? `Re-run the full setup with a fresh token: ${RERUN} --setup <token>.` : `Restore network access to the Haven API, then re-run: ${RERUN} --doctor --runtime ${input.runtime}`
|
|
1662
|
-
});
|
|
1663
|
-
} else if (!localDelegate) {
|
|
1664
|
-
checks.push({
|
|
1665
|
-
id: "identity_match",
|
|
1666
|
-
label: "Hosted identity matches the local signing key",
|
|
1667
|
-
ok: false,
|
|
1668
|
-
detail: "signer.json holds no delegate_address to compare against the hosted identity.",
|
|
1669
|
-
repair: `Re-run the full setup with a fresh token: ${RERUN} --setup <token>.`
|
|
1670
|
-
});
|
|
1671
|
-
} else {
|
|
1672
|
-
const same = probe.delegateAddress?.toLowerCase() === localDelegate.toLowerCase();
|
|
1673
|
-
checks.push({
|
|
1674
|
-
id: "identity_match",
|
|
1675
|
-
label: "Hosted identity matches the local signing key",
|
|
1676
|
-
ok: same,
|
|
1677
|
-
detail: same ? `The stored API key authenticates as the agent whose signing key is in this directory (${shortAddress(localDelegate)}).` : `MISMATCH: the stored API key authenticates as agent ${probe.agentId ?? "unknown"} with delegate ${shortAddress(probe.delegateAddress ?? "unknown")}, but signer.json here holds ${shortAddress(localDelegate)}. This runtime would quote as one agent and sign as another.`,
|
|
1678
|
-
...same ? {} : {
|
|
1679
|
-
repair: `Re-run setup for this agent so its API key and signing key come from one run: ${RERUN} --setup <token>. Do not hand-edit either file.`
|
|
1680
|
-
}
|
|
1681
|
-
});
|
|
1682
|
-
}
|
|
1683
|
-
}
|
|
1684
|
-
if (sidecar) {
|
|
1685
|
-
const consent = await getLocalSignerConsentStatus(path.join(directory, "signer.json"));
|
|
1686
|
-
if (!consent.acknowledged) {
|
|
1687
|
-
checks.push({
|
|
1688
|
-
id: "signer_process",
|
|
1689
|
-
label: "Signer stdio handshake",
|
|
1690
|
-
ok: false,
|
|
1691
|
-
detail: "The local-tools consent is not acknowledged, so the signer refuses to start (by design).",
|
|
1692
|
-
repair: `Run: ${RERUN} --ack-local-tools --setup <token> (or re-run your original setup command with --ack-local-tools).`
|
|
1693
|
-
});
|
|
1694
|
-
} else {
|
|
1695
|
-
const probe = await (deps.probeSignerTools ?? probeLocalMcpTools)(
|
|
1696
|
-
sidecar.wrapper_path,
|
|
1697
|
-
[],
|
|
1698
|
-
exports.MCP_RUNTIME_MANIFEST.requiredSignerTools
|
|
1699
|
-
);
|
|
1700
|
-
const experimental = probe.capabilities?.experimental ?? probe.capabilities;
|
|
1701
|
-
const compat = experimental?.["haven/signer-compatibility"];
|
|
1702
|
-
signerCapabilities = compat ? { "haven/signer-compatibility": compat } : void 0;
|
|
1703
|
-
const compatDetail = compat ? ` Compat: x402 expected-context v${JSON.stringify(compat.x402_expected_context_versions ?? "?")}.` : "";
|
|
1704
|
-
checks.push({
|
|
1705
|
-
id: "signer_process",
|
|
1706
|
-
label: "Signer stdio handshake",
|
|
1707
|
-
ok: probe.status === "ok",
|
|
1708
|
-
detail: probe.status === "ok" ? `Signer started, listed ${probe.toolNames?.length ?? 0} tools${probe.serverInfo?.version ? ` (v${probe.serverInfo.version})` : ""}.${compatDetail}` : `Handshake failed: ${probe.status}.`,
|
|
1709
|
-
...probe.status === "ok" ? {} : { repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}` }
|
|
1710
|
-
});
|
|
1711
|
-
}
|
|
1712
|
-
} else {
|
|
1713
|
-
checks.push({
|
|
1714
|
-
id: "signer_process",
|
|
1715
|
-
label: "Signer stdio handshake",
|
|
1716
|
-
ok: false,
|
|
1717
|
-
detail: "Skipped \u2014 no prepared signer runtime to probe.",
|
|
1718
|
-
repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}`
|
|
1719
|
-
});
|
|
1720
|
-
}
|
|
1721
|
-
return { checks, ...signerCapabilities ? { signerCapabilities } : {} };
|
|
1722
|
-
}
|
|
1723
|
-
async function runDoctor(input, deps = {}) {
|
|
1724
|
-
const homeDir = deps.homeDir ?? os.homedir();
|
|
1725
|
-
const checks = [];
|
|
1726
|
-
let signerCapabilities;
|
|
1727
|
-
const { directory, others } = await discoverCredentialDirectory(homeDir, input.credentialsDir);
|
|
1728
|
-
const configPath = runtimeConfigPathFor(input.runtime, homeDir);
|
|
1729
|
-
let configText = null;
|
|
1730
|
-
if (configPath !== null) {
|
|
1731
|
-
try {
|
|
1732
|
-
configText = await promises.readFile(configPath, "utf8");
|
|
1733
|
-
} catch {
|
|
1734
|
-
configText = null;
|
|
1735
|
-
}
|
|
1736
|
-
}
|
|
1737
|
-
const allDirectories = directory ? [directory, ...others] : others;
|
|
1738
|
-
let bareOwnerExists = false;
|
|
1739
|
-
for (const dir of allDirectories) {
|
|
1740
|
-
const sidecar = await readRuntimeSidecar(dir);
|
|
1741
|
-
if (!sidecar?.server_name && sidecar?.wrapper_path && configText?.includes(sidecar.wrapper_path)) {
|
|
1742
|
-
bareOwnerExists = true;
|
|
1743
|
-
break;
|
|
1744
|
-
}
|
|
1745
|
-
}
|
|
1746
|
-
const inventory = [];
|
|
1747
|
-
const capabilitiesByDirectory = /* @__PURE__ */ new Map();
|
|
1748
|
-
const primaryChecksById = /* @__PURE__ */ new Map();
|
|
1749
|
-
for (const dir of allDirectories) {
|
|
1750
|
-
const identity = await readIdentity(dir);
|
|
1751
|
-
const sidecar = await readRuntimeSidecar(dir);
|
|
1752
|
-
const tombstone = await readAgentTombstone(dir);
|
|
1753
|
-
const slug = sidecar?.server_name;
|
|
1754
|
-
const names = serverNamesFor(slug);
|
|
1755
|
-
if (!identity?.api_key) {
|
|
1756
|
-
inventory.push({
|
|
1757
|
-
...slug ? { slug } : {},
|
|
1758
|
-
...tombstone?.agent_id ? { agentId: tombstone.agent_id } : {},
|
|
1759
|
-
directory: dir,
|
|
1760
|
-
classification: tombstone ? "retired" : "orphaned",
|
|
1761
|
-
checks: []
|
|
1762
|
-
});
|
|
1763
|
-
continue;
|
|
1764
|
-
}
|
|
1765
|
-
const wired = agentIsWired(configText, names, slug, identity, sidecar, dir === directory, bareOwnerExists);
|
|
1766
|
-
const entry = {
|
|
1767
|
-
...slug ? { slug } : {},
|
|
1768
|
-
...identity.agent_id ? { agentId: identity.agent_id } : {},
|
|
1769
|
-
directory: dir,
|
|
1770
|
-
classification: wired ? "wired" : "superseded",
|
|
1771
|
-
checks: []
|
|
1772
|
-
};
|
|
1773
|
-
if (wired) {
|
|
1774
|
-
const result = await checksForAgent({ directory: dir, identity, sidecar }, input, deps);
|
|
1775
|
-
entry.checks = result.checks;
|
|
1776
|
-
capabilitiesByDirectory.set(dir, result.signerCapabilities);
|
|
1777
|
-
}
|
|
1778
|
-
inventory.push(entry);
|
|
1779
|
-
}
|
|
1780
|
-
const wiredDirectories = inventory.filter((entry) => entry.classification === "wired").map((entry) => entry.directory);
|
|
1781
|
-
const primaryDirectory = input.credentialsDir ? directory : wiredDirectories.includes(directory ?? "") ? directory : wiredDirectories[0] ?? directory;
|
|
1782
|
-
if (primaryDirectory) {
|
|
1783
|
-
const primaryEntry = inventory.find((entry) => entry.directory === primaryDirectory);
|
|
1784
|
-
signerCapabilities = capabilitiesByDirectory.get(primaryDirectory);
|
|
1785
|
-
for (const check of primaryEntry?.checks ?? []) primaryChecksById.set(check.id, check);
|
|
1786
|
-
}
|
|
1787
|
-
if (!primaryDirectory) {
|
|
1788
|
-
checks.push({
|
|
1789
|
-
id: "credentials",
|
|
1790
|
-
label: "Agent credentials",
|
|
1791
|
-
ok: false,
|
|
1792
|
-
detail: "No agent credential directory with an identity.json under ~/.haven/agents.",
|
|
1793
|
-
repair: `Run the full setup once: ${RERUN} --setup <token from the Haven dashboard>.`
|
|
1794
|
-
});
|
|
1795
|
-
} else {
|
|
1796
|
-
const primaryIdentity = await readIdentity(primaryDirectory);
|
|
1797
|
-
const primarySidecar = await readRuntimeSidecar(primaryDirectory);
|
|
1798
|
-
if (primaryChecksById.size === 0) {
|
|
1799
|
-
const result = await checksForAgent(
|
|
1800
|
-
{ directory: primaryDirectory, identity: primaryIdentity, sidecar: primarySidecar },
|
|
1801
|
-
input,
|
|
1802
|
-
deps
|
|
1803
|
-
);
|
|
1804
|
-
signerCapabilities = result.signerCapabilities;
|
|
1805
|
-
for (const check of result.checks) primaryChecksById.set(check.id, check);
|
|
1806
|
-
}
|
|
1807
|
-
for (const id of ["credentials", "signer_runtime"]) {
|
|
1808
|
-
const check = primaryChecksById.get(id);
|
|
1809
|
-
if (check) checks.push(check);
|
|
1810
|
-
}
|
|
1811
|
-
}
|
|
1812
|
-
if (configPath === null) {
|
|
1813
|
-
checks.push({
|
|
1814
|
-
id: "runtime_config",
|
|
1815
|
-
label: "Runtime MCP config",
|
|
1816
|
-
ok: true,
|
|
1817
|
-
detail: `Runtime '${input.runtime}' has no file-based config the connector owns (CLI-managed) \u2014 skipping the file check.`
|
|
1818
|
-
});
|
|
1819
|
-
} else if (configText === null) {
|
|
1820
|
-
checks.push({
|
|
1821
|
-
id: "runtime_config",
|
|
1822
|
-
label: "Runtime MCP config",
|
|
1823
|
-
ok: false,
|
|
1824
|
-
detail: `No runtime config at ${configPath}.`,
|
|
1825
|
-
repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}`
|
|
1826
|
-
});
|
|
1827
|
-
} else {
|
|
1828
|
-
const primaryIdentity = await readIdentity(primaryDirectory ?? "");
|
|
1829
|
-
const primarySidecar = primaryDirectory ? await readRuntimeSidecar(primaryDirectory) : null;
|
|
1830
|
-
const hasHaven = primaryIdentity?.hosted_mcp_url ? configText.includes(primaryIdentity.hosted_mcp_url) : configText.includes("haven");
|
|
1831
|
-
const signerViaNpx = configText.includes("@haven_ai/signer");
|
|
1832
|
-
const wrapperReferenced = primarySidecar ? configText.includes(primarySidecar.wrapper_path) : false;
|
|
1833
|
-
const ok = hasHaven && !signerViaNpx && (primarySidecar ? wrapperReferenced : true);
|
|
1834
|
-
checks.push({
|
|
1835
|
-
id: "runtime_config",
|
|
1836
|
-
label: "Runtime MCP config",
|
|
1837
|
-
ok,
|
|
1838
|
-
detail: ok ? `Config at ${configPath} references the hosted server and the prepared signer wrapper.` : signerViaNpx ? `Config at ${configPath} still launches the signer via npx \u2014 the pre-#1586 shape that cannot start under a 120s startup timeout.` : `Config at ${configPath} is missing the Haven entries${primarySidecar && !wrapperReferenced ? " (or references a different signer wrapper)" : ""}.`,
|
|
1839
|
-
...ok ? {} : { repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}` }
|
|
1840
|
-
});
|
|
1841
|
-
}
|
|
1842
|
-
for (const id of ["hosted_mcp", "identity_match"]) {
|
|
1843
|
-
const check = primaryChecksById.get(id);
|
|
1844
|
-
if (check) checks.push(check);
|
|
1845
|
-
}
|
|
1846
|
-
const otherEntries = inventory.filter((entry) => entry.directory !== primaryDirectory);
|
|
1847
|
-
if (otherEntries.length > 0) {
|
|
1848
|
-
const live = [];
|
|
1849
|
-
const revoked = [];
|
|
1850
|
-
const unverifiable = [];
|
|
1851
|
-
const retired = [];
|
|
1852
|
-
for (const entry of otherEntries) {
|
|
1853
|
-
const identity = await readIdentity(entry.directory);
|
|
1854
|
-
const tombstone = await readAgentTombstone(entry.directory);
|
|
1855
|
-
const otherAgent = identity?.agent_id ?? tombstone?.agent_id ?? path.basename(entry.directory);
|
|
1856
|
-
const otherUrl = identity?.hosted_mcp_url ?? (identity?.api_url ? `${identity.api_url}/mcp` : void 0);
|
|
1857
|
-
if (!identity?.api_key || !otherUrl) {
|
|
1858
|
-
if (tombstone) retired.push(`${otherAgent} (retired ${tombstone.retired_at})`);
|
|
1859
|
-
else unverifiable.push(`${otherAgent} (no stored key/URL to probe)`);
|
|
1860
|
-
continue;
|
|
1861
|
-
}
|
|
1862
|
-
const suffix = tombstone ? " [tombstoned \u2014 key material still present]" : "";
|
|
1863
|
-
const probe = await (deps.probeHosted ?? probeHostedMcpTools)(identity.api_key, otherUrl, deps.fetch);
|
|
1864
|
-
if (probe.status === "ok") live.push({ label: `${otherAgent}${suffix}`, entry });
|
|
1865
|
-
else if (probe.status === "unauthorized") revoked.push(`${otherAgent}${suffix}`);
|
|
1866
|
-
else unverifiable.push(`${otherAgent} (${probe.status})${suffix}`);
|
|
1867
|
-
}
|
|
1868
|
-
const parts = [];
|
|
1869
|
-
if (live.length > 0) parts.push(`STILL SPEND-CAPABLE: ${live.map((item) => item.label).join(", ")}`);
|
|
1870
|
-
if (revoked.length > 0) parts.push(`already revoked: ${revoked.join(", ")}`);
|
|
1871
|
-
if (retired.length > 0) parts.push(`tombstoned (keys removed): ${retired.join(", ")}`);
|
|
1872
|
-
if (unverifiable.length > 0) parts.push(`could not verify: ${unverifiable.join(", ")}`);
|
|
1873
|
-
const supersededLive = live.filter((item) => item.entry.classification !== "wired").map((item) => item.label);
|
|
1874
|
-
checks.push({
|
|
1875
|
-
id: "superseded_agents",
|
|
1876
|
-
label: "Superseded agent credentials",
|
|
1877
|
-
ok: supersededLive.length === 0,
|
|
1878
|
-
detail: supersededLive.length > 0 ? `${otherEntries.length} other credential dir(s) found \u2014 ${parts.join("; ")}. A host started before your latest setup keeps authenticating (and spending) as the old agent.` : `${otherEntries.length} other credential dir(s) found \u2014 ${parts.join("; ")}.`,
|
|
1879
|
-
...supersededLive.length > 0 ? {
|
|
1880
|
-
repair: `Revoke ${supersededLive.join(", ")} on the Haven agent page, then remove the old director(y/ies) under ~/.haven/agents. Connect never revokes or deletes for you.`
|
|
1881
|
-
} : {}
|
|
1882
|
-
});
|
|
1883
|
-
}
|
|
1884
|
-
const signerProcess = primaryChecksById.get("signer_process");
|
|
1885
|
-
if (signerProcess) checks.push(signerProcess);
|
|
1886
|
-
const restart = restartRequiredForRuntime(input.runtime, deps.env);
|
|
1887
|
-
checks.push({
|
|
1888
|
-
id: "restart",
|
|
1889
|
-
label: "Runtime restart",
|
|
1890
|
-
ok: true,
|
|
1891
|
-
detail: restart ? "This runtime loads MCP config at startup \u2014 restart it after any repair before expecting the tools to appear." : "No restart requirement known for this runtime."
|
|
1892
|
-
});
|
|
1893
|
-
const wiredOk = inventory.filter((entry) => entry.classification === "wired").every((entry) => entry.checks.every((check) => check.ok));
|
|
1894
|
-
return {
|
|
1895
|
-
version: 1,
|
|
1896
|
-
ok: checks.every((check) => check.ok) && wiredOk,
|
|
1897
|
-
runtime: input.runtime,
|
|
1898
|
-
credentialDirectory: primaryDirectory,
|
|
1899
|
-
checks,
|
|
1900
|
-
agents: inventory,
|
|
1901
|
-
...signerCapabilities ? { signerCapabilities } : {}
|
|
1902
|
-
};
|
|
1903
|
-
}
|
|
1904
|
-
async function runRepair(input, deps = {}) {
|
|
1905
|
-
const homeDir = deps.homeDir ?? os.homedir();
|
|
1906
|
-
const messages = [];
|
|
1907
|
-
const { directory, others } = await discoverCredentialDirectory(homeDir, input.credentialsDir);
|
|
1908
|
-
if (others.length > 0) {
|
|
1909
|
-
messages.push(`Note: ${others.length} other agent credential dir(s) exist \u2014 run --doctor for their status.`);
|
|
1910
|
-
}
|
|
1911
|
-
if (!directory) {
|
|
1912
|
-
return {
|
|
1913
|
-
ok: false,
|
|
1914
|
-
messages: [`No agent credentials found to repair \u2014 run the full setup: ${RERUN} --setup <token>.`]
|
|
1915
|
-
};
|
|
1916
|
-
}
|
|
1917
|
-
let identity;
|
|
1918
|
-
try {
|
|
1919
|
-
identity = JSON.parse(await promises.readFile(path.join(directory, "identity.json"), "utf8"));
|
|
1920
|
-
} catch {
|
|
1921
|
-
return { ok: false, messages: ["identity.json is unreadable \u2014 re-run the full setup with a fresh token."] };
|
|
1922
|
-
}
|
|
1923
|
-
if (!identity.api_key || !(identity.hosted_mcp_url || identity.api_url)) {
|
|
1924
|
-
return { ok: false, messages: ["identity.json lacks the stored API key / hosted URL \u2014 re-run the full setup."] };
|
|
1925
|
-
}
|
|
1926
|
-
const configPath = runtimeConfigPathFor(input.runtime, homeDir);
|
|
1927
|
-
if (configPath) {
|
|
1928
|
-
try {
|
|
1929
|
-
const existing = await promises.readFile(configPath, "utf8");
|
|
1930
|
-
if (existing.includes("bin/haven-mcp") || existing.includes(".haven/mcp-runtime")) {
|
|
1931
|
-
return {
|
|
1932
|
-
ok: false,
|
|
1933
|
-
messages: [
|
|
1934
|
-
`The config at ${configPath} is the LOCAL-stdio topology (--local). Repair currently rewrites only the hosted+signer shape and will not touch it.`,
|
|
1935
|
-
"Re-run your original setup command (with --local) to repair a local-stdio install."
|
|
1936
|
-
]
|
|
1937
|
-
};
|
|
1938
|
-
}
|
|
1939
|
-
} catch {
|
|
1940
|
-
}
|
|
1941
|
-
}
|
|
1942
|
-
const signerPath = path.join(directory, "signer.json");
|
|
1943
|
-
const prepared = await prepareSignerRuntime(
|
|
1944
|
-
{ credentialDirectory: directory, signerPath, homeDir },
|
|
1945
|
-
{ runCommand: deps.runCommand }
|
|
1946
|
-
);
|
|
1947
|
-
messages.push(...prepared.messages);
|
|
1948
|
-
const configResult = await writeRuntimeConfig({
|
|
1949
|
-
runtime: input.runtime,
|
|
1950
|
-
hostedMcpUrl: identity.hosted_mcp_url ?? `${identity.api_url}/mcp`,
|
|
1951
|
-
apiKey: identity.api_key,
|
|
1952
|
-
identityPath: path.join(directory, "identity.json"),
|
|
1953
|
-
signerPath,
|
|
1954
|
-
credentialDirectory: directory,
|
|
1955
|
-
signerCommand: { command: prepared.command, args: prepared.args },
|
|
1956
|
-
homeDir,
|
|
1957
|
-
mode: "hosted"
|
|
1958
|
-
});
|
|
1959
|
-
messages.push(...configResult.messages);
|
|
1960
|
-
messages.push("Repair complete \u2014 restart the runtime, then verify with --doctor.");
|
|
1961
|
-
return { ok: true, messages };
|
|
1962
|
-
}
|
|
1963
|
-
var RERUN;
|
|
1964
|
-
var init_doctor = __esm({
|
|
1965
|
-
"src/doctor.ts"() {
|
|
1966
|
-
init_runtime_manifest();
|
|
1967
|
-
init_probes();
|
|
1968
|
-
init_signer_runtime();
|
|
1969
|
-
init_config_writers();
|
|
1970
|
-
init_runtime_registry();
|
|
1971
|
-
init_signer_consent();
|
|
1972
|
-
init_tombstone();
|
|
1973
|
-
init_server_names();
|
|
1974
|
-
init_redact();
|
|
1975
|
-
RERUN = "npx @haven_ai/connect@alpha";
|
|
1976
|
-
}
|
|
1977
|
-
});
|
|
1978
|
-
|
|
1979
|
-
// src/api.ts
|
|
1980
|
-
function createConnectApiClient(baseUrl, fetchImpl = fetch) {
|
|
1981
|
-
const root = baseUrl.replace(/\/+$/, "");
|
|
1982
|
-
return {
|
|
1983
|
-
resolveSetup: (input) => request(fetchImpl, `${root}/agent-connection-setups/resolve`, {
|
|
1984
|
-
method: "POST",
|
|
1985
|
-
body: JSON.stringify({
|
|
1986
|
-
setup_token: input.setupToken,
|
|
1987
|
-
connector_version: input.connectorVersion,
|
|
1988
|
-
runtime: input.runtime
|
|
1989
|
-
})
|
|
1990
|
-
}),
|
|
1991
|
-
registerSetup: (input) => request(fetchImpl, `${root}/agent-connection-setups/register`, {
|
|
1992
|
-
method: "POST",
|
|
1993
|
-
body: JSON.stringify({
|
|
1994
|
-
setup_token: input.setupToken,
|
|
1995
|
-
challenge_id: input.challengeId,
|
|
1996
|
-
delegate_address: input.delegateAddress,
|
|
1997
|
-
proof_signature: input.proofSignature,
|
|
1998
|
-
api_key_hash: input.apiKeyHash,
|
|
1999
|
-
api_key_prefix: input.apiKeyPrefix,
|
|
2000
|
-
runtime: input.runtime,
|
|
2001
|
-
connector_version: input.connectorVersion,
|
|
2002
|
-
connector_context: input.connectorContext,
|
|
2003
|
-
install_capabilities: input.installCapabilities && {
|
|
2004
|
-
can_write_runtime_config: input.installCapabilities.canWriteRuntimeConfig,
|
|
2005
|
-
restart_required: input.installCapabilities.restartRequired
|
|
2006
|
-
}
|
|
2007
|
-
})
|
|
2008
|
-
}),
|
|
2009
|
-
getConnectorStatus: (setupId, apiKey) => request(fetchImpl, `${root}/agent-connection-setups/${encodeURIComponent(setupId)}/connector-status`, {
|
|
2010
|
-
method: "GET",
|
|
2011
|
-
headers: { Authorization: `Bearer ${apiKey}` }
|
|
2012
|
-
}),
|
|
2013
|
-
updateInstallStatus: async (setupId, apiKey, input) => {
|
|
2014
|
-
await request(fetchImpl, `${root}/agent-connection-setups/${encodeURIComponent(setupId)}/install-status`, {
|
|
2015
|
-
method: "POST",
|
|
2016
|
-
headers: { Authorization: `Bearer ${apiKey}` },
|
|
2017
|
-
body: JSON.stringify({
|
|
2018
|
-
runtime: input.runtime,
|
|
2019
|
-
connector_version: input.connectorVersion,
|
|
2020
|
-
runtime_mcp_mode: input.runtimeMcpMode,
|
|
2021
|
-
hosted_mcp_configured: input.hostedMcpConfigured,
|
|
2022
|
-
local_signer_configured: input.localSignerConfigured,
|
|
2023
|
-
local_mcp_configured: input.localMcpConfigured,
|
|
2024
|
-
credential_files_written: input.credentialFilesWritten,
|
|
2025
|
-
signer_acknowledged: input.signerAcknowledged,
|
|
2026
|
-
local_mcp_acknowledged: input.localMcpAcknowledged,
|
|
2027
|
-
activation_command_available: input.activationCommandAvailable,
|
|
2028
|
-
skill_installed: input.skillInstalled,
|
|
2029
|
-
probe_result: input.probeResult,
|
|
2030
|
-
restart_required: input.restartRequired,
|
|
2031
|
-
next_user_action: input.nextUserAction,
|
|
2032
|
-
error_code: input.errorCode ?? null,
|
|
2033
|
-
environment_label: input.environmentLabel
|
|
2034
|
-
})
|
|
2035
|
-
});
|
|
2036
|
-
}
|
|
2037
|
-
};
|
|
2038
|
-
}
|
|
2039
|
-
var ConnectRequestError = class extends Error {
|
|
2040
|
-
constructor(message, status) {
|
|
2041
|
-
super(message);
|
|
2042
|
-
this.status = status;
|
|
2043
|
-
this.name = "ConnectRequestError";
|
|
2044
|
-
}
|
|
2045
|
-
status;
|
|
2046
|
-
};
|
|
2047
|
-
async function request(fetchImpl, url, init) {
|
|
2048
|
-
const response = await fetchImpl(url, {
|
|
2049
|
-
...init,
|
|
2050
|
-
headers: {
|
|
2051
|
-
"Content-Type": "application/json",
|
|
2052
|
-
...init.headers ?? {}
|
|
2053
|
-
}
|
|
2054
|
-
});
|
|
2055
|
-
const text = await response.text();
|
|
2056
|
-
const body = text ? JSON.parse(text) : null;
|
|
2057
|
-
if (!response.ok) {
|
|
2058
|
-
const message = body?.error ?? body?.message ?? `${response.status} ${response.statusText}`;
|
|
2059
|
-
throw new ConnectRequestError(`Haven setup request failed: ${message}`, response.status);
|
|
2060
|
-
}
|
|
2061
|
-
return body;
|
|
2062
|
-
}
|
|
2063
|
-
function generateDelegateKey() {
|
|
2064
|
-
return delegateKeyFromPrivateKey(ethers.Wallet.createRandom().privateKey);
|
|
2065
|
-
}
|
|
2066
|
-
function delegateKeyFromPrivateKey(privateKey) {
|
|
2067
|
-
const wallet = new ethers.Wallet(privateKey);
|
|
2068
|
-
return {
|
|
2069
|
-
privateKey: wallet.privateKey,
|
|
2070
|
-
address: wallet.address,
|
|
2071
|
-
signChallenge: (message) => wallet.signMessage(message)
|
|
2072
|
-
};
|
|
2073
|
-
}
|
|
2074
|
-
function generateAgentApiKey() {
|
|
2075
|
-
return `sk_agent_${crypto__default.default.randomBytes(24).toString("hex")}`;
|
|
2076
|
-
}
|
|
2077
|
-
function hashAgentApiKey(apiKey) {
|
|
2078
|
-
return crypto__default.default.createHash("sha256").update(apiKey).digest("hex");
|
|
2079
|
-
}
|
|
2080
|
-
function agentApiKeyPrefix(apiKey) {
|
|
2081
|
-
return apiKey.slice(0, 12);
|
|
2082
|
-
}
|
|
2083
|
-
|
|
2084
|
-
// src/runtime.ts
|
|
2085
|
-
init_redact();
|
|
2086
|
-
init_server_names();
|
|
2087
|
-
async function preflightCredentialStorage(input = {}) {
|
|
2088
|
-
const directory = defaultCredentialRoot(input.baseDir);
|
|
2089
|
-
await promises.mkdir(directory, { recursive: true, mode: 448 });
|
|
2090
|
-
await restrictPermissions(directory, 448, input.warn);
|
|
2091
|
-
const probePath = path.join(directory, `.haven-connect-preflight-${crypto__default.default.randomBytes(8).toString("hex")}`);
|
|
2092
|
-
try {
|
|
2093
|
-
await writeOwnerOnlyJson(probePath, { ok: true }, input.warn);
|
|
2094
|
-
} finally {
|
|
2095
|
-
await promises.rm(probePath, { force: true }).catch(() => void 0);
|
|
2096
|
-
}
|
|
2097
|
-
return directory;
|
|
2098
|
-
}
|
|
2099
|
-
async function writeCredentialFiles(input) {
|
|
2100
|
-
const directory = defaultAgentDirectory(input.serverName ?? input.agentId, input.baseDir);
|
|
2101
|
-
await promises.mkdir(directory, { recursive: true, mode: 448 });
|
|
2102
|
-
await restrictPermissions(directory, 448, input.warn);
|
|
2103
|
-
const identityPath = path.join(directory, "identity.json");
|
|
2104
|
-
const signerPath = path.join(directory, "signer.json");
|
|
2105
|
-
const agentPath = path.join(directory, "agent.json");
|
|
2106
|
-
await assertDoesNotExist(identityPath);
|
|
2107
|
-
await assertDoesNotExist(signerPath);
|
|
2108
|
-
await assertDoesNotExist(agentPath);
|
|
2109
|
-
await writeOwnerOnlyJson(
|
|
2110
|
-
signerPath,
|
|
2111
|
-
{
|
|
2112
|
-
delegate_key: input.delegateKey,
|
|
2113
|
-
delegate_address: input.delegateAddress,
|
|
2114
|
-
agent_id: input.agentId,
|
|
2115
|
-
safe_address: input.safeAddress,
|
|
2116
|
-
chain_id: input.chainId,
|
|
2117
|
-
network: input.network,
|
|
2118
|
-
x402_binding_signer: input.x402BindingSigner,
|
|
2119
|
-
note: "Local signer credential. Haven backend never receives this private key."
|
|
2120
|
-
},
|
|
2121
|
-
input.warn
|
|
2122
|
-
);
|
|
2123
|
-
try {
|
|
2124
|
-
await writeOwnerOnlyJson(
|
|
2125
|
-
identityPath,
|
|
2126
|
-
{
|
|
2127
|
-
api_key: input.apiKey,
|
|
2128
|
-
agent_id: input.agentId,
|
|
2129
|
-
safe_address: input.safeAddress,
|
|
2130
|
-
chain_id: input.chainId,
|
|
2131
|
-
network: input.network,
|
|
2132
|
-
api_url: input.apiUrl,
|
|
2133
|
-
hosted_mcp_url: input.hostedMcpUrl,
|
|
2134
|
-
agent_budget: input.agentBudget,
|
|
2135
|
-
note: "Haven API key identifies the agent only. It cannot spend without the local signer key and on-chain Haven wallet rules."
|
|
2136
|
-
},
|
|
2137
|
-
input.warn
|
|
2138
|
-
);
|
|
2139
|
-
} catch (err) {
|
|
2140
|
-
await promises.rm(signerPath, { force: true }).catch(() => void 0);
|
|
2141
|
-
throw err;
|
|
2142
|
-
}
|
|
2143
|
-
try {
|
|
2144
|
-
await writeOwnerOnlyJson(
|
|
2145
|
-
agentPath,
|
|
2146
|
-
{
|
|
2147
|
-
agent_id: input.agentId,
|
|
2148
|
-
delegate_address: input.delegateAddress,
|
|
2149
|
-
safe_address: input.safeAddress,
|
|
2150
|
-
chain_id: input.chainId,
|
|
2151
|
-
network: input.network,
|
|
2152
|
-
agent_budget: input.agentBudget,
|
|
2153
|
-
note: "Non-secret orientation for the agent: public delegate/Haven wallet identity + configured budget. Contains no API key or signing key. For the live remaining budget, call haven_get_allowances."
|
|
2154
|
-
},
|
|
2155
|
-
input.warn
|
|
2156
|
-
);
|
|
2157
|
-
} catch (err) {
|
|
2158
|
-
await promises.rm(signerPath, { force: true }).catch(() => void 0);
|
|
2159
|
-
await promises.rm(identityPath, { force: true }).catch(() => void 0);
|
|
2160
|
-
await promises.rm(agentPath, { force: true }).catch(() => void 0);
|
|
2161
|
-
throw err;
|
|
2162
|
-
}
|
|
2163
|
-
return { directory, identityPath, signerPath, agentPath };
|
|
2164
|
-
}
|
|
2165
|
-
async function assertServerSlugAvailable(serverName, baseDir) {
|
|
2166
|
-
const directory = defaultAgentDirectory(serverName, baseDir);
|
|
2167
|
-
try {
|
|
2168
|
-
await promises.stat(path.join(directory, "identity.json"));
|
|
2169
|
-
} catch {
|
|
2170
|
-
return;
|
|
2171
|
-
}
|
|
2172
|
-
throw new Error(
|
|
2173
|
-
`The name "${serverName}" is already wired on this machine (${directory} holds credentials). Pick a different --name, or revoke and remove that agent first \u2014 connect never overwrites credentials.`
|
|
2174
|
-
);
|
|
2175
|
-
}
|
|
2176
|
-
function defaultAgentDirectory(agentId, baseDir = path.join(os.homedir(), ".haven", "agents")) {
|
|
2177
|
-
return path.resolve(defaultCredentialRoot(baseDir), safePathPart(agentId));
|
|
2178
|
-
}
|
|
2179
|
-
function defaultCredentialRoot(baseDir = path.join(os.homedir(), ".haven", "agents")) {
|
|
2180
|
-
return path.resolve(baseDir);
|
|
2181
|
-
}
|
|
2182
|
-
async function writeOwnerOnlyJson(path, value, warn) {
|
|
2183
|
-
const json = JSON.stringify(dropUndefined(value), null, 2);
|
|
2184
|
-
await promises.writeFile(path, `${json}
|
|
2185
|
-
`, { mode: 384, flag: "wx" });
|
|
2186
|
-
await restrictPermissions(path, 384, warn);
|
|
2187
|
-
}
|
|
2188
|
-
function safePathPart(value) {
|
|
2189
|
-
return value.replace(/[^A-Za-z0-9_.-]/g, "_");
|
|
2190
|
-
}
|
|
2191
|
-
function dropUndefined(value) {
|
|
2192
|
-
return Object.fromEntries(Object.entries(value).filter(([, field]) => field !== void 0));
|
|
2193
|
-
}
|
|
2194
|
-
async function assertDoesNotExist(path) {
|
|
2195
|
-
try {
|
|
2196
|
-
await promises.access(path);
|
|
2197
|
-
} catch (err) {
|
|
2198
|
-
if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") return;
|
|
2199
|
-
throw err;
|
|
2200
|
-
}
|
|
2201
|
-
throw new Error(`Refusing to overwrite existing Haven credential file: ${path}`);
|
|
2372
|
+
progress("Almost there \u2014 just confirming everything connects\u2026");
|
|
2373
|
+
const localProbePromise = configResult.runtimeMcpMode === "local_stdio" && localRuntimeInstall ? runLocalMcpProbe(localRuntimeInstall, deps) : Promise.resolve(void 0);
|
|
2374
|
+
const signerProbePromise = configResult.runtimeMcpMode !== "local_stdio" && signerCommand ? (deps.probeSignerTools ?? probeLocalMcpTools)(
|
|
2375
|
+
signerCommand.command,
|
|
2376
|
+
signerCommand.args,
|
|
2377
|
+
exports.MCP_RUNTIME_MANIFEST.requiredSignerTools
|
|
2378
|
+
) : Promise.resolve(void 0);
|
|
2379
|
+
const [hostedProbe, signerCredentialReady, localMcpProbe, signerProbe] = await Promise.all([
|
|
2380
|
+
configResult.hostedConfigured ? probeHostedMcpTools(input.apiKey, input.hostedMcpUrl, deps.fetch) : Promise.resolve({ status: "bad_response" }),
|
|
2381
|
+
probeLocalSignerCredential(input.signerPath),
|
|
2382
|
+
localProbePromise,
|
|
2383
|
+
signerProbePromise
|
|
2384
|
+
]);
|
|
2385
|
+
const hostedOk = configResult.hostedConfigured && hostedProbe.status === "ok";
|
|
2386
|
+
const localMcpOk = configResult.runtimeMcpMode === "local_stdio" && configResult.localMcpConfigured && signerCredentialReady && Boolean(localMcpConsent?.acknowledged) && localMcpProbe?.status === "ok";
|
|
2387
|
+
const signerOk = configResult.runtimeMcpMode === "local_stdio" ? localMcpOk : configResult.signerConfigured && signerCredentialReady && Boolean(signerConsent?.acknowledged) && // #1587: no handshake, no green. A signer command that was registered
|
|
2388
|
+
// but not probed (manual topology) keeps the old semantics.
|
|
2389
|
+
(signerProbe === void 0 || signerProbe.status === "ok");
|
|
2390
|
+
const restartRequired = configResult.restartRequired || restartRequiredForRuntime(runtime, deps.env);
|
|
2391
|
+
const errorCode = configResult.errorCode ?? (configResult.runtimeMcpMode === "local_stdio" ? localMcpErrorCode(signerCredentialReady, localMcpConsent, localMcpProbe?.status) : hostedMcpErrorCode(configResult.hostedConfigured, hostedProbe.status) ?? signerConsentErrorCode(signerCredentialReady, signerConsent) ?? signerProbeErrorCode(signerProbe));
|
|
2392
|
+
const hostedProbeMessages = configResult.hostedConfigured && hostedProbe.status !== "ok" ? [`Hosted Haven MCP probe failed: ${hostedProbe.status}.`] : configResult.hostedConfigured ? ["Verified hosted Haven MCP tools with a read-only handshake."] : [];
|
|
2393
|
+
const signerProbeMessages = signerProbe ? signerProbe.status === "ok" ? ["Verified local Haven signer with a stdio handshake."] : [
|
|
2394
|
+
`Local Haven signer handshake failed: ${signerProbe.status}.`,
|
|
2395
|
+
"Re-run `npx @haven_ai/connect@alpha` to repair the signer setup."
|
|
2396
|
+
] : [];
|
|
2397
|
+
const localProbeMessages = localMcpProbe && localMcpProbe.status !== "ok" ? [`Local Haven MCP handshake failed: ${localMcpProbe.status}.`] : localMcpProbe?.status === "ok" ? ["Verified local Haven MCP tools with a stdio handshake."] : [];
|
|
2398
|
+
const skillInstall = !configResult.errorCode ? await installSkillForRuntime(runtime, { homeDir: deps.homeDir, env: deps.env }) : void 0;
|
|
2399
|
+
return {
|
|
2400
|
+
runtime,
|
|
2401
|
+
runtimeMcpMode: configResult.runtimeMcpMode,
|
|
2402
|
+
hostedMcpConfigured: hostedOk,
|
|
2403
|
+
localSignerConfigured: signerOk,
|
|
2404
|
+
localMcpConfigured: localMcpOk,
|
|
2405
|
+
probeResult: buildProbeResult(configResult.runtimeMcpMode, configResult.hostedConfigured, hostedProbe.status, signerOk, localMcpOk, localMcpProbe?.status),
|
|
2406
|
+
restartRequired,
|
|
2407
|
+
nextUserAction: nextAction(runtime, profile.restartMode, errorCode),
|
|
2408
|
+
errorCode,
|
|
2409
|
+
configTarget: configResult.target,
|
|
2410
|
+
signerAcknowledged: signerConsent?.acknowledged,
|
|
2411
|
+
localMcpAcknowledged: localMcpConsent?.acknowledged,
|
|
2412
|
+
activationCommand: configResult.activationCommand,
|
|
2413
|
+
skillInstalled: skillInstall?.installed,
|
|
2414
|
+
signerRuntimePrepared,
|
|
2415
|
+
messages: [...consentMessages, ...localRuntimeInstall?.messages ?? [], ...configResult.messages, ...hostedProbeMessages, ...signerProbeMessages, ...localProbeMessages, ...skillInstall?.messages ?? []]
|
|
2416
|
+
};
|
|
2202
2417
|
}
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
);
|
|
2210
|
-
}
|
|
2418
|
+
function runtimeInstallCapabilities(runtime, env = process.env) {
|
|
2419
|
+
const profile = runtimeProfile(runtime, env);
|
|
2420
|
+
return {
|
|
2421
|
+
canWriteRuntimeConfig: profile.canWriteRuntimeConfig,
|
|
2422
|
+
restartRequired: restartRequiredForRuntime(runtime, env)
|
|
2423
|
+
};
|
|
2211
2424
|
}
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2425
|
+
async function configureClaudeCode(deps, localMcpCommand, serverName) {
|
|
2426
|
+
const runCommand = deps.runCommand ?? defaultRunCommand;
|
|
2427
|
+
const serverJson = JSON.stringify({
|
|
2428
|
+
type: "stdio",
|
|
2429
|
+
command: localMcpCommand,
|
|
2430
|
+
args: [],
|
|
2431
|
+
env: {}
|
|
2432
|
+
});
|
|
2217
2433
|
try {
|
|
2218
|
-
|
|
2219
|
-
const
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2434
|
+
if (!localMcpCommand) throw new Error("local MCP wrapper command is required");
|
|
2435
|
+
const names = serverNamesFor(serverName);
|
|
2436
|
+
await runCommand("claude", ["mcp", "remove", names.hosted]).catch(() => void 0);
|
|
2437
|
+
await runCommand("claude", ["mcp", "remove", names.signer]).catch(() => void 0);
|
|
2438
|
+
await runCommand("claude", ["mcp", "add-json", names.hosted, serverJson, "--scope", "user"]).catch(async () => {
|
|
2439
|
+
await runCommand("claude", ["mcp", "add", names.hosted, "--scope", "user", "--", localMcpCommand]);
|
|
2223
2440
|
});
|
|
2441
|
+
const verified = await runCommand("claude", ["mcp", "get", names.hosted]).then(() => true).catch(() => false);
|
|
2224
2442
|
return {
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2443
|
+
hostedConfigured: false,
|
|
2444
|
+
signerConfigured: true,
|
|
2445
|
+
localMcpConfigured: true,
|
|
2446
|
+
runtimeMcpMode: "local_stdio",
|
|
2447
|
+
target: "Claude Code MCP config",
|
|
2448
|
+
changed: true,
|
|
2449
|
+
restartRequired: true,
|
|
2450
|
+
messages: [
|
|
2451
|
+
"Updated local Haven MCP entry with Claude Code.",
|
|
2452
|
+
...verified ? ["Verified Claude Code MCP entry."] : []
|
|
2453
|
+
]
|
|
2228
2454
|
};
|
|
2229
2455
|
} catch (err) {
|
|
2230
2456
|
return {
|
|
2231
|
-
|
|
2232
|
-
|
|
2457
|
+
hostedConfigured: false,
|
|
2458
|
+
signerConfigured: false,
|
|
2459
|
+
localMcpConfigured: false,
|
|
2460
|
+
runtimeMcpMode: "local_stdio",
|
|
2461
|
+
target: "Claude Code MCP config",
|
|
2462
|
+
changed: false,
|
|
2463
|
+
restartRequired: true,
|
|
2464
|
+
messages: [
|
|
2465
|
+
`Could not update Claude Code MCP config: ${err instanceof Error ? err.message : String(err)}`,
|
|
2466
|
+
"Install Claude Code or rerun the Haven setup command inside a Claude Code terminal."
|
|
2467
|
+
],
|
|
2468
|
+
errorCode: "claude_code_config_failed"
|
|
2233
2469
|
};
|
|
2234
2470
|
}
|
|
2235
2471
|
}
|
|
2236
|
-
async function
|
|
2472
|
+
async function writeHostedRuntimeConfig(deps, input, signerCommand) {
|
|
2473
|
+
if (input.runtime === "claude-code") {
|
|
2474
|
+
return configureClaudeCodeHosted(deps, input, signerCommand);
|
|
2475
|
+
}
|
|
2476
|
+
return writeRuntimeConfig({
|
|
2477
|
+
runtime: input.runtime,
|
|
2478
|
+
hostedMcpUrl: input.hostedMcpUrl,
|
|
2479
|
+
apiKey: input.apiKey,
|
|
2480
|
+
identityPath: input.identityPath,
|
|
2481
|
+
signerPath: input.signerPath,
|
|
2482
|
+
serverName: input.serverName,
|
|
2483
|
+
credentialDirectory: input.credentialDirectory,
|
|
2484
|
+
signerCommand,
|
|
2485
|
+
homeDir: deps.homeDir,
|
|
2486
|
+
mode: "hosted"
|
|
2487
|
+
});
|
|
2488
|
+
}
|
|
2489
|
+
async function configureClaudeCodeHosted(deps, input, signerCommand) {
|
|
2490
|
+
const runCommand = deps.runCommand ?? defaultRunCommand;
|
|
2491
|
+
const hostedJson = JSON.stringify({
|
|
2492
|
+
type: "http",
|
|
2493
|
+
url: input.hostedMcpUrl,
|
|
2494
|
+
headers: { Authorization: `Bearer ${input.apiKey}` }
|
|
2495
|
+
});
|
|
2496
|
+
const signerJson = JSON.stringify({
|
|
2497
|
+
type: "stdio",
|
|
2498
|
+
command: signerCommand?.command ?? "npx",
|
|
2499
|
+
args: signerCommand?.args ?? ["-y", signerPackageSpec(), "--credentials", input.signerPath],
|
|
2500
|
+
env: {}
|
|
2501
|
+
});
|
|
2237
2502
|
try {
|
|
2238
|
-
const
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2503
|
+
const names = serverNamesFor(input.serverName);
|
|
2504
|
+
await runCommand("claude", ["mcp", "remove", names.hosted]).catch(() => void 0);
|
|
2505
|
+
await runCommand("claude", ["mcp", "remove", names.signer]).catch(() => void 0);
|
|
2506
|
+
await runCommand("claude", ["mcp", "add-json", names.hosted, hostedJson, "--scope", "user"]);
|
|
2507
|
+
await runCommand("claude", ["mcp", "add-json", names.signer, signerJson, "--scope", "user"]);
|
|
2508
|
+
const verified = await runCommand("claude", ["mcp", "get", names.hosted]).then(() => true).catch(() => false);
|
|
2244
2509
|
return {
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2510
|
+
hostedConfigured: true,
|
|
2511
|
+
signerConfigured: true,
|
|
2512
|
+
localMcpConfigured: false,
|
|
2513
|
+
runtimeMcpMode: "hosted_plus_signer",
|
|
2514
|
+
target: "Claude Code MCP config",
|
|
2515
|
+
changed: true,
|
|
2516
|
+
restartRequired: true,
|
|
2517
|
+
messages: [
|
|
2518
|
+
"Updated hosted Haven MCP and local signer entries with Claude Code.",
|
|
2519
|
+
...verified ? ["Verified Claude Code MCP entry."] : []
|
|
2520
|
+
]
|
|
2248
2521
|
};
|
|
2249
2522
|
} catch (err) {
|
|
2250
2523
|
return {
|
|
2251
|
-
|
|
2252
|
-
|
|
2524
|
+
hostedConfigured: false,
|
|
2525
|
+
signerConfigured: false,
|
|
2526
|
+
localMcpConfigured: false,
|
|
2527
|
+
runtimeMcpMode: "hosted_plus_signer",
|
|
2528
|
+
target: "Claude Code MCP config",
|
|
2529
|
+
changed: false,
|
|
2530
|
+
restartRequired: true,
|
|
2531
|
+
messages: [
|
|
2532
|
+
`Could not update Claude Code MCP config: ${err instanceof Error ? err.message : String(err)}`,
|
|
2533
|
+
"Install Claude Code or rerun the Haven setup command inside a Claude Code terminal."
|
|
2534
|
+
],
|
|
2535
|
+
errorCode: "claude_code_config_failed"
|
|
2253
2536
|
};
|
|
2254
2537
|
}
|
|
2255
2538
|
}
|
|
2256
|
-
function
|
|
2257
|
-
|
|
2539
|
+
async function defaultRunCommand(command, args) {
|
|
2540
|
+
await execFileAsync3(command, args, { timeout: 1e4 });
|
|
2541
|
+
}
|
|
2542
|
+
function buildProbeResult(mode, hostedConfigured, hostedStatus, signerReady, localMcpReady, localMcpProbeStatus) {
|
|
2543
|
+
if (mode === "local_stdio") {
|
|
2544
|
+
if (localMcpReady) return "local_stdio_mcp_ready";
|
|
2545
|
+
return localMcpProbeStatus ? `local_stdio_mcp_${localMcpProbeStatus}` : "local_stdio_mcp_unavailable";
|
|
2546
|
+
}
|
|
2547
|
+
const hostedPart = hostedConfigured ? `hosted_${hostedStatus}` : "hosted_not_configured";
|
|
2548
|
+
const signerPart = signerReady ? "local_signer_ready" : "local_signer_unavailable";
|
|
2549
|
+
return `${hostedPart}_${signerPart}`.slice(0, 120);
|
|
2550
|
+
}
|
|
2551
|
+
async function resolveLocalMcpConsent(input, messages) {
|
|
2552
|
+
if (input.ackLocalTools || input.ackSigner) {
|
|
2553
|
+
const status = await acknowledgeLocalMcpConsent(input.identityPath, input.signerPath, (message) => messages.push(message));
|
|
2554
|
+
if (status.acknowledged) {
|
|
2555
|
+
messages.push("Prepared the local Haven tools acknowledgement.");
|
|
2556
|
+
} else {
|
|
2557
|
+
messages.push("Local Haven tools acknowledgement still needs attention.");
|
|
2558
|
+
}
|
|
2559
|
+
return status;
|
|
2560
|
+
}
|
|
2561
|
+
return getLocalMcpConsentStatus(input.identityPath, input.signerPath);
|
|
2562
|
+
}
|
|
2563
|
+
async function resolveSignerConsent(input, messages) {
|
|
2564
|
+
if (input.ackSigner || input.ackLocalTools) {
|
|
2565
|
+
const status = await acknowledgeLocalSignerConsent(input.signerPath, (message) => messages.push(message));
|
|
2566
|
+
if (status.acknowledged) {
|
|
2567
|
+
messages.push("Prepared the local Haven signer acknowledgement.");
|
|
2568
|
+
} else {
|
|
2569
|
+
messages.push("Local Haven signer acknowledgement still needs attention.");
|
|
2570
|
+
}
|
|
2571
|
+
return status;
|
|
2572
|
+
}
|
|
2573
|
+
return getLocalSignerConsentStatus(input.signerPath);
|
|
2574
|
+
}
|
|
2575
|
+
function signerConsentErrorCode(signerCredentialReady, signerConsent) {
|
|
2576
|
+
if (!signerCredentialReady) return "local_signer_credential_unavailable";
|
|
2577
|
+
if (!signerConsent?.acknowledged) return "local_signer_ack_required";
|
|
2578
|
+
return void 0;
|
|
2579
|
+
}
|
|
2580
|
+
function signerProbeErrorCode(probe) {
|
|
2581
|
+
if (!probe || probe.status === "ok") return void 0;
|
|
2582
|
+
return `local_signer_probe_${probe.status}`;
|
|
2583
|
+
}
|
|
2584
|
+
function hostedMcpErrorCode(hostedConfigured, hostedProbeStatus) {
|
|
2585
|
+
if (!hostedConfigured || hostedProbeStatus === "ok") return void 0;
|
|
2586
|
+
return `hosted_mcp_probe_${hostedProbeStatus}`;
|
|
2258
2587
|
}
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
}
|
|
2265
|
-
};
|
|
2266
|
-
return mcp.consentInputFromClient(
|
|
2267
|
-
unavailableDuringSetup,
|
|
2268
|
-
{
|
|
2269
|
-
apiKey: credentials.apiKey,
|
|
2270
|
-
apiUrl: credentials.apiUrl,
|
|
2271
|
-
agentId: credentials.agentId,
|
|
2272
|
-
safeAddress: credentials.safeAddress,
|
|
2273
|
-
delegateAddress: credentials.delegateAddress,
|
|
2274
|
-
chainId: credentials.chainId,
|
|
2275
|
-
allowanceSummary: credentials.allowanceSummary
|
|
2276
|
-
},
|
|
2277
|
-
mcp.registeredToolNames()
|
|
2278
|
-
);
|
|
2588
|
+
function localMcpErrorCode(signerCredentialReady, localMcpConsent, localMcpProbeStatus) {
|
|
2589
|
+
if (!signerCredentialReady) return "local_signer_credential_unavailable";
|
|
2590
|
+
if (!localMcpConsent?.acknowledged) return "local_mcp_ack_required";
|
|
2591
|
+
if (localMcpProbeStatus && localMcpProbeStatus !== "ok") return `local_mcp_probe_${localMcpProbeStatus}`;
|
|
2592
|
+
return void 0;
|
|
2279
2593
|
}
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2594
|
+
function nextAction(runtime, restartMode, errorCode) {
|
|
2595
|
+
if (errorCode) return "return_to_haven_for_wallet_approval_then_finish_runtime_setup";
|
|
2596
|
+
if (restartMode === "hot-reload") return "return_to_haven_for_wallet_approval";
|
|
2597
|
+
if (runtime === "codex-cli" || runtime === "codex-desktop") return "return_to_haven_for_wallet_approval_then_restart_codex";
|
|
2598
|
+
if (runtime === "claude-code") return "return_to_haven_for_wallet_approval_then_restart_claude_code";
|
|
2599
|
+
if (restartMode === "restart-app") return "return_to_haven_for_wallet_approval_then_restart_app";
|
|
2600
|
+
if (restartMode === "restart-session") return "return_to_haven_for_wallet_approval_then_restart_agent_session";
|
|
2601
|
+
return "return_to_haven_for_wallet_approval_then_configure_runtime";
|
|
2287
2602
|
}
|
|
2288
|
-
function
|
|
2289
|
-
|
|
2290
|
-
if (message) log(message);
|
|
2603
|
+
function supportsLocalMcp(runtime) {
|
|
2604
|
+
return runtime === "codex-cli" || runtime === "codex-desktop" || runtime === "claude-code";
|
|
2291
2605
|
}
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
// src/local-mcp-runtime.ts
|
|
2297
|
-
init_signer_runtime();
|
|
2298
|
-
init_runtime_manifest();
|
|
2299
|
-
var execFileAsync2 = util.promisify(child_process.execFile);
|
|
2300
|
-
var UnsupportedNodeVersionError = class extends Error {
|
|
2301
|
-
code = "local_mcp_unsupported_node_version";
|
|
2302
|
-
nodeVersion;
|
|
2303
|
-
minimumNodeVersion;
|
|
2304
|
-
constructor(nodeVersion, minimumNodeVersion, subject = "Haven setup") {
|
|
2305
|
-
super(sdk.unsupportedNodeVersionMessage({ subject, nodeVersion, minimumNodeVersion }));
|
|
2306
|
-
this.name = "UnsupportedNodeVersionError";
|
|
2307
|
-
this.nodeVersion = nodeVersion;
|
|
2308
|
-
this.minimumNodeVersion = minimumNodeVersion;
|
|
2309
|
-
}
|
|
2310
|
-
};
|
|
2311
|
-
async function prepareLocalMcpRuntime(input, deps = {}) {
|
|
2312
|
-
assertSupportedNodeVersion(input.nodeVersion);
|
|
2313
|
-
const homeDir = input.homeDir ?? os.homedir();
|
|
2314
|
-
const runtimeDirectory = path.resolve(homeDir, ".haven", "mcp-runtime", exports.MCP_RUNTIME_MANIFEST.mcpVersion);
|
|
2315
|
-
const npmCacheDirectory = path.resolve(homeDir, ".haven", "npm-cache");
|
|
2316
|
-
const cliPath = path.join(runtimeDirectory, "node_modules", "@haven_ai", "mcp", "dist", "cli.js");
|
|
2317
|
-
const messages = [];
|
|
2318
|
-
await promises.mkdir(runtimeDirectory, { recursive: true, mode: 448 });
|
|
2319
|
-
await promises.chmod(runtimeDirectory, 448).catch(() => void 0);
|
|
2320
|
-
await promises.mkdir(npmCacheDirectory, { recursive: true, mode: 448 });
|
|
2321
|
-
await promises.chmod(npmCacheDirectory, 448).catch(() => void 0);
|
|
2322
|
-
if (await installedRuntimeMatches2(runtimeDirectory, cliPath)) {
|
|
2323
|
-
messages.push(`Using existing local Haven MCP runtime ${mcpPackageSpec()}.`);
|
|
2324
|
-
} else {
|
|
2325
|
-
await installRuntimePackages2(runtimeDirectory, npmCacheDirectory, deps);
|
|
2326
|
-
messages.push(`Installed local Haven MCP runtime ${mcpPackageSpec()}.`);
|
|
2327
|
-
}
|
|
2328
|
-
await assertFileExists2(cliPath, "local Haven MCP CLI");
|
|
2329
|
-
const wrapperPath = path.join(input.credentialDirectory, "bin", "haven-mcp");
|
|
2330
|
-
await writeWrapper2({
|
|
2331
|
-
wrapperPath,
|
|
2332
|
-
cliPath,
|
|
2606
|
+
async function prepareRuntimeForLocalMcp(input, deps) {
|
|
2607
|
+
const prepare = deps.prepareLocalMcpRuntime ?? ((runtimeInput) => prepareLocalMcpRuntime(runtimeInput, { runCommand: deps.runCommand, onProgress: deps.onProgress }));
|
|
2608
|
+
return prepare({
|
|
2609
|
+
credentialDirectory: input.credentialDirectory,
|
|
2333
2610
|
identityPath: input.identityPath,
|
|
2334
|
-
signerPath: input.signerPath
|
|
2335
|
-
|
|
2336
|
-
await writeRuntimeSidecar2({
|
|
2337
|
-
path: path.join(input.credentialDirectory, "mcp-runtime.json"),
|
|
2338
|
-
wrapperPath,
|
|
2339
|
-
runtimeDirectory,
|
|
2340
|
-
npmCacheDirectory,
|
|
2341
|
-
cliPath,
|
|
2611
|
+
signerPath: input.signerPath,
|
|
2612
|
+
homeDir: deps.homeDir,
|
|
2342
2613
|
serverName: input.serverName
|
|
2343
2614
|
});
|
|
2344
|
-
messages.push(`Prepared stable local Haven MCP wrapper: ${wrapperPath}`);
|
|
2345
|
-
return {
|
|
2346
|
-
command: wrapperPath,
|
|
2347
|
-
args: [],
|
|
2348
|
-
wrapperPath,
|
|
2349
|
-
runtimeDirectory,
|
|
2350
|
-
npmCacheDirectory,
|
|
2351
|
-
cliPath,
|
|
2352
|
-
messages
|
|
2353
|
-
};
|
|
2354
2615
|
}
|
|
2355
|
-
function
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2616
|
+
async function prepareSignerForRuntime(input, deps) {
|
|
2617
|
+
const prepare = deps.prepareSignerRuntime ?? ((runtimeInput) => (
|
|
2618
|
+
// onProgress threaded through on purpose (#1586 review): without it the
|
|
2619
|
+
// install heartbeat was dead code in production and the console still
|
|
2620
|
+
// went silent for the whole cold install — the exact symptom the issue
|
|
2621
|
+
// set out to remove, at a longer timeout.
|
|
2622
|
+
prepareSignerRuntime(runtimeInput, { runCommand: deps.runCommand, onProgress: deps.onProgress })
|
|
2623
|
+
));
|
|
2624
|
+
return prepare({
|
|
2625
|
+
credentialDirectory: input.credentialDirectory,
|
|
2626
|
+
signerPath: input.signerPath,
|
|
2627
|
+
homeDir: deps.homeDir,
|
|
2628
|
+
serverName: input.serverName
|
|
2629
|
+
});
|
|
2359
2630
|
}
|
|
2360
|
-
async function
|
|
2361
|
-
const
|
|
2362
|
-
const baseArgs = [
|
|
2363
|
-
"install",
|
|
2364
|
-
"--prefix",
|
|
2365
|
-
runtimeDirectory,
|
|
2366
|
-
"--no-audit",
|
|
2367
|
-
"--no-fund",
|
|
2368
|
-
"--omit=dev",
|
|
2369
|
-
"--prefer-offline",
|
|
2370
|
-
mcpPackageSpec(),
|
|
2371
|
-
sdkPackageSpec()
|
|
2372
|
-
];
|
|
2373
|
-
const run = async (args) => {
|
|
2374
|
-
const startedAt = Date.now();
|
|
2375
|
-
const heartbeat = setInterval(() => {
|
|
2376
|
-
const seconds = Math.round((Date.now() - startedAt) / 1e3);
|
|
2377
|
-
onProgress?.(`Still installing the local Haven MCP runtime\u2026 (${seconds}s \u2014 a cold cache can take several minutes)`);
|
|
2378
|
-
}, SIGNER_INSTALL_HEARTBEAT_MS);
|
|
2379
|
-
heartbeat.unref?.();
|
|
2380
|
-
try {
|
|
2381
|
-
if (runCommand) await runCommand("npm", args);
|
|
2382
|
-
else await execFileAsync2("npm", args, { timeout: SIGNER_INSTALL_TIMEOUT_MS, maxBuffer: 1024 * 1024 });
|
|
2383
|
-
} finally {
|
|
2384
|
-
clearInterval(heartbeat);
|
|
2385
|
-
}
|
|
2386
|
-
};
|
|
2631
|
+
async function runLocalMcpProbe(runtimeInstall, deps) {
|
|
2632
|
+
const probe = deps.probeLocalMcpTools ?? probeLocalMcpTools;
|
|
2387
2633
|
try {
|
|
2388
|
-
await
|
|
2634
|
+
return await probe(runtimeInstall.command, runtimeInstall.args, exports.MCP_RUNTIME_MANIFEST.requiredTools);
|
|
2389
2635
|
} catch {
|
|
2390
|
-
|
|
2391
|
-
await run([...baseArgs, "--cache", npmCacheDirectory]);
|
|
2392
|
-
} catch (err) {
|
|
2393
|
-
throw new Error(`Could not install local Haven MCP runtime ${mcpPackageSpec()}: ${err instanceof Error ? err.message : String(err)}`);
|
|
2394
|
-
}
|
|
2636
|
+
return { status: "process_error" };
|
|
2395
2637
|
}
|
|
2396
2638
|
}
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
const [mcpPackage, sdkPackage] = await Promise.all([
|
|
2401
|
-
readPackageJson2(path.join(runtimeDirectory, "node_modules", "@haven_ai", "mcp", "package.json")),
|
|
2402
|
-
readPackageJson2(path.join(runtimeDirectory, "node_modules", "@haven_ai", "sdk", "package.json"))
|
|
2403
|
-
]);
|
|
2404
|
-
return mcpPackage.version === exports.MCP_RUNTIME_MANIFEST.mcpVersion && sdkPackage.version === exports.MCP_RUNTIME_MANIFEST.sdkVersion;
|
|
2405
|
-
} catch {
|
|
2406
|
-
return false;
|
|
2639
|
+
function localRuntimePrepareErrorCode(err) {
|
|
2640
|
+
if (err && typeof err === "object" && "code" in err && err.code === "local_mcp_unsupported_node_version") {
|
|
2641
|
+
return "local_mcp_unsupported_node_version";
|
|
2407
2642
|
}
|
|
2643
|
+
return "local_mcp_runtime_install_failed";
|
|
2408
2644
|
}
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2645
|
+
var execFileAsync3;
|
|
2646
|
+
var init_runtime_install = __esm({
|
|
2647
|
+
"src/runtime-install.ts"() {
|
|
2648
|
+
init_config_writers();
|
|
2649
|
+
init_server_names();
|
|
2650
|
+
init_local_mcp_consent();
|
|
2651
|
+
init_probes();
|
|
2652
|
+
init_local_mcp_runtime();
|
|
2653
|
+
init_signer_runtime();
|
|
2654
|
+
init_runtime_manifest();
|
|
2655
|
+
init_skill_install();
|
|
2656
|
+
init_runtime_registry();
|
|
2657
|
+
init_signer_consent();
|
|
2658
|
+
execFileAsync3 = util.promisify(child_process.execFile);
|
|
2659
|
+
}
|
|
2660
|
+
});
|
|
2661
|
+
|
|
2662
|
+
// src/tombstone.ts
|
|
2663
|
+
var tombstone_exports = {};
|
|
2664
|
+
__export(tombstone_exports, {
|
|
2665
|
+
TOMBSTONE_FILENAME: () => TOMBSTONE_FILENAME,
|
|
2666
|
+
TOMBSTONE_MARKER: () => TOMBSTONE_MARKER,
|
|
2667
|
+
readAgentTombstone: () => readAgentTombstone,
|
|
2668
|
+
writeAgentTombstone: () => writeAgentTombstone
|
|
2669
|
+
});
|
|
2670
|
+
function tombstoneScript(info) {
|
|
2671
|
+
const lines = [
|
|
2672
|
+
`${TOMBSTONE_MARKER}: this Haven agent was retired.`,
|
|
2418
2673
|
"",
|
|
2419
|
-
`
|
|
2420
|
-
`
|
|
2421
|
-
`
|
|
2674
|
+
` agent: ${info.agent_id}`,
|
|
2675
|
+
` retired at: ${info.retired_at}`,
|
|
2676
|
+
` reason: ${info.reason}`,
|
|
2677
|
+
...info.replaced_by ? [` replaced by: ${info.replaced_by}`] : [],
|
|
2422
2678
|
"",
|
|
2423
|
-
"
|
|
2424
|
-
"
|
|
2425
|
-
"
|
|
2679
|
+
"This process is running with a wiring snapshot that predates the",
|
|
2680
|
+
"retirement \u2014 it loaded its MCP config at startup and has kept it since.",
|
|
2681
|
+
"Restart THIS host to pick up the current wiring. If several long-lived",
|
|
2682
|
+
"hosts are running (a gateway, a TUI worker, an editor), restart EVERY",
|
|
2683
|
+
"one of them: each holds the snapshot from its own start time, so after",
|
|
2684
|
+
"a chain of recreations each can be parked on a DIFFERENT old agent.",
|
|
2426
2685
|
"",
|
|
2427
|
-
"
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
"
|
|
2686
|
+
"Then verify with: npx @haven_ai/connect@alpha --doctor --runtime <runtime>"
|
|
2687
|
+
];
|
|
2688
|
+
return [
|
|
2689
|
+
"#!/usr/bin/env node",
|
|
2690
|
+
`// ${TOMBSTONE_MARKER} \u2014 written by @haven_ai/connect (#1681). Safe to delete`,
|
|
2691
|
+
"// once every long-lived MCP host on this machine has been restarted.",
|
|
2692
|
+
`process.stderr.write(${JSON.stringify(lines.join("\n") + "\n")})`,
|
|
2693
|
+
"process.exit(1)",
|
|
2431
2694
|
""
|
|
2432
2695
|
].join("\n");
|
|
2433
|
-
await promises.writeFile(input.wrapperPath, source, { mode: 448 });
|
|
2434
|
-
await promises.chmod(input.wrapperPath, 448).catch(() => void 0);
|
|
2435
2696
|
}
|
|
2436
|
-
async function
|
|
2437
|
-
const
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2697
|
+
async function writeAgentTombstone(input) {
|
|
2698
|
+
const dirStat = await promises.stat(input.directory).catch(() => null);
|
|
2699
|
+
if (!dirStat?.isDirectory()) {
|
|
2700
|
+
throw new Error(`Not a directory: ${input.directory} \u2014 nothing to tombstone.`);
|
|
2701
|
+
}
|
|
2702
|
+
const info = {
|
|
2703
|
+
// reason / replaced_by are persisted to disk and re-emitted to the host's
|
|
2704
|
+
// MCP stderr log on EVERY stale probe, potentially for months — redact
|
|
2705
|
+
// like every other output path, at the write layer so any future caller
|
|
2706
|
+
// inherits it. (#1681 review, finding 1)
|
|
2707
|
+
agent_id: input.agentId,
|
|
2708
|
+
retired_at: input.retiredAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
2709
|
+
reason: redactSecrets(input.reason),
|
|
2710
|
+
...input.replacedBy ? { replaced_by: redactSecrets(input.replacedBy) } : {}
|
|
2448
2711
|
};
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2712
|
+
const binDir = path.join(input.directory, "bin");
|
|
2713
|
+
await promises.mkdir(binDir, { recursive: true });
|
|
2714
|
+
const wrapperPath = path.join(binDir, "haven-signer.mjs");
|
|
2715
|
+
await promises.writeFile(wrapperPath, tombstoneScript(info), "utf8");
|
|
2716
|
+
await promises.chmod(wrapperPath, 493);
|
|
2717
|
+
await promises.writeFile(path.join(input.directory, TOMBSTONE_FILENAME), JSON.stringify(info, null, 2) + "\n", "utf8");
|
|
2718
|
+
return info;
|
|
2452
2719
|
}
|
|
2453
|
-
async function
|
|
2720
|
+
async function readAgentTombstone(directory) {
|
|
2454
2721
|
try {
|
|
2455
|
-
await promises.
|
|
2722
|
+
const parsed = JSON.parse(await promises.readFile(path.join(directory, TOMBSTONE_FILENAME), "utf8"));
|
|
2723
|
+
if (typeof parsed?.agent_id !== "string") return null;
|
|
2724
|
+
return parsed;
|
|
2456
2725
|
} catch {
|
|
2457
|
-
|
|
2726
|
+
return null;
|
|
2458
2727
|
}
|
|
2459
2728
|
}
|
|
2729
|
+
var TOMBSTONE_FILENAME, TOMBSTONE_MARKER;
|
|
2730
|
+
var init_tombstone = __esm({
|
|
2731
|
+
"src/tombstone.ts"() {
|
|
2732
|
+
init_redact();
|
|
2733
|
+
TOMBSTONE_FILENAME = "TOMBSTONE.json";
|
|
2734
|
+
TOMBSTONE_MARKER = "HAVEN-TOMBSTONE";
|
|
2735
|
+
}
|
|
2736
|
+
});
|
|
2460
2737
|
|
|
2461
|
-
// src/
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2738
|
+
// src/rekey.ts
|
|
2739
|
+
var rekey_exports = {};
|
|
2740
|
+
__export(rekey_exports, {
|
|
2741
|
+
finishRekey: () => finishRekey,
|
|
2742
|
+
startRekey: () => startRekey
|
|
2743
|
+
});
|
|
2744
|
+
async function startRekey(options, deps = {}) {
|
|
2745
|
+
const now = deps.now ?? (() => Date.now());
|
|
2746
|
+
const stored = await readStoredCredentials(
|
|
2747
|
+
options.serverName,
|
|
2748
|
+
options.agentId,
|
|
2749
|
+
options.credentialsDir
|
|
2750
|
+
);
|
|
2751
|
+
const api = (deps.createApi ?? ((url) => createConnectApiClient(url)))(stored.apiUrl);
|
|
2752
|
+
const identity = await probeIdentity(api, stored.apiKey, "current");
|
|
2753
|
+
assertRekeyable(identity, stored);
|
|
2754
|
+
const key = (deps.generateKey ?? generateDelegateKey)();
|
|
2755
|
+
const startedAt = new Date(now()).toISOString();
|
|
2756
|
+
const expiresAt = new Date(now() + REKEY_PENDING_TTL_MS).toISOString();
|
|
2757
|
+
await writeRekeyPending(stored.directory, {
|
|
2758
|
+
agent_id: stored.agentId,
|
|
2759
|
+
new_delegate_address: key.address,
|
|
2760
|
+
new_delegate_key: key.privateKey,
|
|
2761
|
+
started_at: startedAt,
|
|
2762
|
+
expires_at: expiresAt
|
|
2763
|
+
});
|
|
2764
|
+
const finishCommand = [
|
|
2765
|
+
"npx @haven_ai/connect@alpha --rekey-finish",
|
|
2766
|
+
options.serverName ? `--name ${options.serverName}` : void 0,
|
|
2767
|
+
"--api-key <the key the dashboard showed you>",
|
|
2768
|
+
options.runtime ? `--runtime ${options.runtime}` : "--runtime <your runtime>"
|
|
2769
|
+
].filter(Boolean).join(" ");
|
|
2770
|
+
return {
|
|
2771
|
+
started: true,
|
|
2772
|
+
agentId: stored.agentId,
|
|
2773
|
+
directory: stored.directory,
|
|
2774
|
+
newDelegateAddress: key.address,
|
|
2775
|
+
expiresAt,
|
|
2776
|
+
messages: [
|
|
2777
|
+
`New signing key generated on this machine for agent ${identity.name || stored.agentId}.`,
|
|
2778
|
+
"",
|
|
2779
|
+
` New signing address: ${key.address}`,
|
|
2780
|
+
"",
|
|
2781
|
+
"The private half stays here \u2014 Haven never receives it, and there is no way to move it",
|
|
2782
|
+
"between machines. That is what keeps the account non-custodial.",
|
|
2783
|
+
"",
|
|
2784
|
+
'Next, on the Haven agent page: choose "Replace signing key" and paste that address.',
|
|
2785
|
+
"When it finishes it shows a new API key ONCE. Come back here and run:",
|
|
2786
|
+
"",
|
|
2787
|
+
` ${finishCommand}`,
|
|
2788
|
+
"",
|
|
2789
|
+
`Nothing has changed yet \u2014 the agent keeps working on its old key until you finish.`,
|
|
2790
|
+
`This pending re-key expires ${expiresAt}.`
|
|
2791
|
+
]
|
|
2792
|
+
};
|
|
2793
|
+
}
|
|
2794
|
+
async function finishRekey(options, deps = {}) {
|
|
2795
|
+
const now = deps.now ?? (() => Date.now());
|
|
2796
|
+
if (!options.newApiKey) {
|
|
2797
|
+
throw new Error("--rekey-finish needs --api-key <key> \u2014 the one the Haven agent page showed once.");
|
|
2798
|
+
}
|
|
2799
|
+
const stored = await readStoredCredentials(
|
|
2800
|
+
options.serverName,
|
|
2801
|
+
options.agentId,
|
|
2802
|
+
options.credentialsDir
|
|
2803
|
+
);
|
|
2804
|
+
const pending = await readRekeyPending(stored.directory, now());
|
|
2805
|
+
if (pending.agent_id !== stored.agentId) {
|
|
2806
|
+
throw new Error(
|
|
2807
|
+
`The pending re-key at ${stored.directory} belongs to agent ${pending.agent_id}, but that directory now holds ${stored.agentId}. Refusing to write a key into the wrong agent.`
|
|
2808
|
+
);
|
|
2809
|
+
}
|
|
2810
|
+
const api = (deps.createApi ?? ((url) => createConnectApiClient(url)))(stored.apiUrl);
|
|
2811
|
+
const identity = await probeIdentity(api, options.newApiKey, "new");
|
|
2812
|
+
if (identity.id !== stored.agentId) {
|
|
2813
|
+
throw new Error(
|
|
2814
|
+
`That API key belongs to agent ${identity.id}, not ${stored.agentId}. Nothing was changed.`
|
|
2815
|
+
);
|
|
2816
|
+
}
|
|
2817
|
+
const onChain = (identity.delegate_address ?? "").toLowerCase();
|
|
2818
|
+
const expected = pending.new_delegate_address.toLowerCase();
|
|
2819
|
+
if (onChain !== expected) {
|
|
2820
|
+
throw new Error(
|
|
2821
|
+
`Haven says this agent's signing address is ${identity.delegate_address ?? "unset"}, but this machine generated ${pending.new_delegate_address}. Nothing was changed. Either the re-key on the agent page used a different address, or it has not finished yet.`
|
|
2822
|
+
);
|
|
2823
|
+
}
|
|
2824
|
+
await rewriteCredentialFiles({
|
|
2825
|
+
baseDir: options.credentialsDir,
|
|
2826
|
+
agentId: stored.agentId,
|
|
2827
|
+
serverName: options.serverName,
|
|
2828
|
+
apiKey: options.newApiKey,
|
|
2829
|
+
delegateKey: pending.new_delegate_key,
|
|
2830
|
+
delegateAddress: pending.new_delegate_address,
|
|
2831
|
+
safeAddress: stored.safeAddress ?? identity.safe_address ?? void 0,
|
|
2832
|
+
chainId: stored.chainId ?? identity.chain_id ?? void 0,
|
|
2833
|
+
network: stored.network,
|
|
2834
|
+
agentBudget: stored.agentBudget,
|
|
2835
|
+
apiUrl: stored.apiUrl,
|
|
2836
|
+
hostedMcpUrl: stored.hostedMcpUrl,
|
|
2837
|
+
x402BindingSigner: stored.x402BindingSigner,
|
|
2838
|
+
warn: deps.log
|
|
2839
|
+
});
|
|
2840
|
+
await clearRekeyPending(stored.directory);
|
|
2841
|
+
const names = serverNamesFor(options.serverName);
|
|
2842
|
+
const messages = [
|
|
2843
|
+
`Agent ${identity.name || stored.agentId} is now on its new signing key.`,
|
|
2844
|
+
` Signing address: ${pending.new_delegate_address}`,
|
|
2845
|
+
` Credentials: ${stored.directory} (rewritten in place)`,
|
|
2846
|
+
` MCP servers: ${names.hosted} / ${names.signer} (unchanged names)`
|
|
2847
|
+
];
|
|
2848
|
+
let configRewritten = false;
|
|
2849
|
+
if (options.runtime) {
|
|
2850
|
+
const prepared = await (deps.prepareSigner ?? prepareSignerRuntime)(
|
|
2851
|
+
{
|
|
2852
|
+
credentialDirectory: stored.directory,
|
|
2853
|
+
signerPath: `${stored.directory}/signer.json`,
|
|
2854
|
+
homeDir: options.homeDir
|
|
2855
|
+
},
|
|
2856
|
+
{ runCommand: deps.runCommand }
|
|
2857
|
+
);
|
|
2858
|
+
const result = await (deps.writeConfig ?? writeHostedRuntimeConfig)(
|
|
2859
|
+
{ runCommand: deps.runCommand, homeDir: options.homeDir },
|
|
2860
|
+
{
|
|
2861
|
+
runtime: options.runtime,
|
|
2862
|
+
hostedMcpUrl: stored.hostedMcpUrl,
|
|
2863
|
+
apiKey: options.newApiKey,
|
|
2864
|
+
identityPath: `${stored.directory}/identity.json`,
|
|
2865
|
+
signerPath: `${stored.directory}/signer.json`,
|
|
2866
|
+
credentialDirectory: stored.directory,
|
|
2867
|
+
serverName: options.serverName
|
|
2868
|
+
},
|
|
2869
|
+
{ command: prepared.command, args: prepared.args }
|
|
2870
|
+
);
|
|
2871
|
+
configRewritten = result.hostedConfigured;
|
|
2872
|
+
messages.push(` Config: ${result.target}`);
|
|
2873
|
+
messages.push(...result.messages.map((line) => ` ${line}`));
|
|
2874
|
+
if (!configRewritten) {
|
|
2875
|
+
messages.push(
|
|
2876
|
+
"",
|
|
2877
|
+
"WARNING: the MCP config was NOT updated, so it still carries the OLD API key and every",
|
|
2878
|
+
`wired host will fail with 401. Fix the cause above and re-run with --runtime ${options.runtime},`,
|
|
2879
|
+
"or update the config by hand. The credential files on disk are already on the new key."
|
|
2477
2880
|
);
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2881
|
+
}
|
|
2882
|
+
} else {
|
|
2883
|
+
messages.push(
|
|
2884
|
+
"",
|
|
2885
|
+
"NOTE: no --runtime was given, so the MCP config still carries the OLD API key and every",
|
|
2886
|
+
"wired host will fail with 401. Re-run with --runtime <name> to rewrite it."
|
|
2887
|
+
);
|
|
2483
2888
|
}
|
|
2889
|
+
return {
|
|
2890
|
+
finished: true,
|
|
2891
|
+
agentId: stored.agentId,
|
|
2892
|
+
directory: stored.directory,
|
|
2893
|
+
newDelegateAddress: pending.new_delegate_address,
|
|
2894
|
+
serverNames: { hosted: names.hosted, signer: names.signer },
|
|
2895
|
+
configRewritten,
|
|
2896
|
+
messages
|
|
2897
|
+
};
|
|
2484
2898
|
}
|
|
2485
|
-
async function
|
|
2899
|
+
async function probeIdentity(api, apiKey, which) {
|
|
2486
2900
|
try {
|
|
2487
|
-
await
|
|
2488
|
-
const target = path.join(skillDir, "SKILL.md");
|
|
2489
|
-
await promises.writeFile(target, sdk.HAVEN_SKILL_MD, "utf8");
|
|
2490
|
-
return {
|
|
2491
|
-
installed: true,
|
|
2492
|
-
target,
|
|
2493
|
-
messages: [`Installed the generic Haven payment skill (${label}). It contains no secrets.`]
|
|
2494
|
-
};
|
|
2901
|
+
return await api.getAgentIdentity(apiKey);
|
|
2495
2902
|
} catch (err) {
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
};
|
|
2502
|
-
}
|
|
2503
|
-
}
|
|
2504
|
-
async function installCodexAgentsSection(deps) {
|
|
2505
|
-
try {
|
|
2506
|
-
const codexDir = path.resolve(deps.homeDir ?? os.homedir(), ".codex");
|
|
2507
|
-
const target = path.join(codexDir, "AGENTS.md");
|
|
2508
|
-
await promises.mkdir(codexDir, { recursive: true });
|
|
2509
|
-
const existing = await promises.readFile(target, "utf8").catch(() => null);
|
|
2510
|
-
const next = upsertManagedSection(existing, codexManagedSection());
|
|
2511
|
-
if (next !== existing) {
|
|
2512
|
-
await promises.writeFile(target, next, "utf8");
|
|
2903
|
+
const status = err?.status;
|
|
2904
|
+
if (status === 401 || status === 403) {
|
|
2905
|
+
throw new Error(
|
|
2906
|
+
which === "current" ? "This machine's Haven API key is no longer accepted. If a re-key already finished elsewhere, run --rekey-finish with the key that re-key produced instead of starting a new one." : "Haven rejected that API key. Check you pasted the whole key from the agent page \u2014 nothing was changed."
|
|
2907
|
+
);
|
|
2513
2908
|
}
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
]
|
|
2520
|
-
};
|
|
2521
|
-
} catch (err) {
|
|
2522
|
-
return {
|
|
2523
|
-
installed: false,
|
|
2524
|
-
messages: [
|
|
2525
|
-
`Could not install the Haven payment guidance into ~/.codex/AGENTS.md: ${err instanceof Error ? err.message : String(err)}. Download the skill from the Haven dashboard instead.`
|
|
2526
|
-
]
|
|
2527
|
-
};
|
|
2909
|
+
throw new Error(
|
|
2910
|
+
`Could not reach Haven to check this agent (${redactSecrets(
|
|
2911
|
+
err instanceof Error ? err.message : String(err)
|
|
2912
|
+
)}). Nothing was changed.`
|
|
2913
|
+
);
|
|
2528
2914
|
}
|
|
2529
2915
|
}
|
|
2530
|
-
function
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
${
|
|
2534
|
-
|
|
2535
|
-
${CODEX_AGENTS_END_MARKER}
|
|
2536
|
-
`;
|
|
2537
|
-
}
|
|
2538
|
-
function upsertManagedSection(existing, section) {
|
|
2539
|
-
if (existing === null || existing.trim() === "") return section;
|
|
2540
|
-
const begins = markerLineIndexes(existing, CODEX_AGENTS_BEGIN_MARKER);
|
|
2541
|
-
const ends = markerLineIndexes(existing, CODEX_AGENTS_END_MARKER);
|
|
2542
|
-
if (begins.length === 1 && ends.length === 1 && ends[0] > begins[0]) {
|
|
2543
|
-
const afterEnd = ends[0] + CODEX_AGENTS_END_MARKER.length;
|
|
2544
|
-
const tail = existing.startsWith("\r\n", afterEnd) ? existing.slice(afterEnd + 2) : existing.startsWith("\n", afterEnd) ? existing.slice(afterEnd + 1) : existing.slice(afterEnd);
|
|
2545
|
-
return existing.slice(0, begins[0]) + section + tail;
|
|
2916
|
+
function assertRekeyable(identity, stored) {
|
|
2917
|
+
if (identity.id !== stored.agentId) {
|
|
2918
|
+
throw new Error(
|
|
2919
|
+
`The credentials at ${stored.directory} say agent ${stored.agentId}, but Haven says that key belongs to ${identity.id}. Refusing to re-key an agent this directory does not own.`
|
|
2920
|
+
);
|
|
2546
2921
|
}
|
|
2547
|
-
if (
|
|
2922
|
+
if (identity.execution_rail === "legacy") {
|
|
2548
2923
|
throw new Error(
|
|
2549
|
-
|
|
2924
|
+
`Agent ${identity.name || identity.id} is on the legacy rail, which cannot be re-keyed \u2014 its authority is per-token Safe allowances, not a signed delegation there is anything to re-issue. Re-onboard the agent on the delegation rail instead.`
|
|
2550
2925
|
);
|
|
2551
2926
|
}
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
for (let from = 0; ; ) {
|
|
2557
|
-
const at = text.indexOf(marker, from);
|
|
2558
|
-
if (at === -1) return indexes;
|
|
2559
|
-
if (at === 0 || text[at - 1] === "\n") indexes.push(at);
|
|
2560
|
-
from = at + marker.length;
|
|
2927
|
+
if (identity.status === "revoked") {
|
|
2928
|
+
throw new Error(
|
|
2929
|
+
`Agent ${identity.name || identity.id} is revoked. Re-keying would hand a revoked agent fresh credentials \u2014 create a new agent instead.`
|
|
2930
|
+
);
|
|
2561
2931
|
}
|
|
2562
2932
|
}
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2933
|
+
var init_rekey = __esm({
|
|
2934
|
+
"src/rekey.ts"() {
|
|
2935
|
+
init_api();
|
|
2936
|
+
init_runtime_install();
|
|
2937
|
+
init_signer_runtime();
|
|
2938
|
+
init_key();
|
|
2939
|
+
init_redact();
|
|
2940
|
+
init_server_names();
|
|
2941
|
+
init_storage();
|
|
2942
|
+
}
|
|
2943
|
+
});
|
|
2567
2944
|
|
|
2568
|
-
// src/
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
const profile =
|
|
2575
|
-
const
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2945
|
+
// src/rekey-restart.ts
|
|
2946
|
+
var rekey_restart_exports = {};
|
|
2947
|
+
__export(rekey_restart_exports, {
|
|
2948
|
+
restartGuidance: () => restartGuidance
|
|
2949
|
+
});
|
|
2950
|
+
function restartGuidance(runtime) {
|
|
2951
|
+
const profile = runtime ? BY_RUNTIME[runtime] : void 0;
|
|
2952
|
+
const lines = ["The new key is only live in a process that started after now.", ""];
|
|
2953
|
+
if (profile) {
|
|
2954
|
+
lines.push(profile.how);
|
|
2955
|
+
for (const command of profile.commands) lines.push(` ${command}`);
|
|
2956
|
+
if (profile.commands.length > 0) lines.push("");
|
|
2957
|
+
} else {
|
|
2958
|
+
lines.push(
|
|
2959
|
+
runtime ? `No standard restart command is known for "${runtime}" \u2014 restart it the way you start it.` : "Pass --runtime <name> to get the restart command for a specific host."
|
|
2960
|
+
);
|
|
2961
|
+
lines.push("");
|
|
2962
|
+
}
|
|
2963
|
+
lines.push(SWEEP);
|
|
2964
|
+
return { commands: profile?.commands ?? [], lines };
|
|
2965
|
+
}
|
|
2966
|
+
var SWEEP, BY_RUNTIME;
|
|
2967
|
+
var init_rekey_restart = __esm({
|
|
2968
|
+
"src/rekey-restart.ts"() {
|
|
2969
|
+
SWEEP = "Then restart EVERY other long-lived MCP host on this machine \u2014 gateways, TUI workers, editors. Each holds the wiring snapshot from its own start time, so after a re-key each one is still presenting the OLD API key and will fail with 401 until it restarts.";
|
|
2970
|
+
BY_RUNTIME = {
|
|
2971
|
+
"claude-code": {
|
|
2972
|
+
commands: ["claude --continue"],
|
|
2973
|
+
how: "Exit this Claude Code session and start a new one:"
|
|
2974
|
+
},
|
|
2975
|
+
"codex-cli": {
|
|
2976
|
+
commands: ["codex resume --last"],
|
|
2977
|
+
how: "Start a fresh Codex CLI session:"
|
|
2978
|
+
},
|
|
2979
|
+
"codex-desktop": {
|
|
2980
|
+
commands: [],
|
|
2981
|
+
how: "Quit Codex Desktop completely (not just the window) and reopen it."
|
|
2982
|
+
},
|
|
2983
|
+
"claude-desktop": {
|
|
2984
|
+
commands: [],
|
|
2985
|
+
how: "Quit Claude Desktop completely (not just the window) and reopen it."
|
|
2986
|
+
},
|
|
2987
|
+
hermes: {
|
|
2988
|
+
commands: ["systemctl --user restart hermes-gateway", "/restart"],
|
|
2989
|
+
how: "Restart the Hermes gateway, or run /restart inside a Hermes session. The first form is the one that matters if you run it as a user service:"
|
|
2990
|
+
},
|
|
2991
|
+
cursor: {
|
|
2992
|
+
commands: [],
|
|
2993
|
+
how: 'Cursor hot-reloads MCP config, so it will pick the new key up on its own. If a tool call still fails with 401, reload the window (Cmd/Ctrl+Shift+P \u2192 "Reload Window").'
|
|
2994
|
+
},
|
|
2995
|
+
vscode: {
|
|
2996
|
+
commands: [],
|
|
2997
|
+
how: 'VS Code hot-reloads MCP config. If a tool call still fails with 401, reload the window (Cmd/Ctrl+Shift+P \u2192 "Reload Window").'
|
|
2998
|
+
},
|
|
2999
|
+
"vscode-insiders": {
|
|
3000
|
+
commands: [],
|
|
3001
|
+
how: 'VS Code Insiders hot-reloads MCP config. If a tool call still fails with 401, reload the window (Cmd/Ctrl+Shift+P \u2192 "Reload Window").'
|
|
3002
|
+
}
|
|
2605
3003
|
};
|
|
2606
3004
|
}
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
3005
|
+
});
|
|
3006
|
+
|
|
3007
|
+
// src/doctor.ts
|
|
3008
|
+
var doctor_exports = {};
|
|
3009
|
+
__export(doctor_exports, {
|
|
3010
|
+
runDoctor: () => runDoctor,
|
|
3011
|
+
runRepair: () => runRepair
|
|
3012
|
+
});
|
|
3013
|
+
async function discoverCredentialDirectory(homeDir, explicit) {
|
|
3014
|
+
const root = explicit ? path.dirname(explicit) : path.join(homeDir, ".haven", "agents");
|
|
3015
|
+
let entries = [];
|
|
3016
|
+
try {
|
|
3017
|
+
entries = await promises.readdir(root);
|
|
3018
|
+
} catch {
|
|
3019
|
+
return explicit ? { directory: explicit, others: [], parkedOnly: /* @__PURE__ */ new Set() } : { others: [], parkedOnly: /* @__PURE__ */ new Set() };
|
|
3020
|
+
}
|
|
3021
|
+
const candidates = [];
|
|
3022
|
+
const tombstonedOnly = [];
|
|
3023
|
+
const parkedOnly = [];
|
|
3024
|
+
for (const entry of entries) {
|
|
3025
|
+
const directory = path.join(root, entry);
|
|
2610
3026
|
try {
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
3027
|
+
const s = await promises.stat(path.join(directory, "identity.json"));
|
|
3028
|
+
candidates.push({ directory, mtimeMs: s.mtimeMs });
|
|
3029
|
+
} catch {
|
|
3030
|
+
try {
|
|
3031
|
+
await promises.stat(path.join(directory, TOMBSTONE_FILENAME));
|
|
3032
|
+
tombstonedOnly.push(directory);
|
|
3033
|
+
} catch {
|
|
3034
|
+
try {
|
|
3035
|
+
await promises.stat(path.join(directory, REKEY_PENDING_FILENAME));
|
|
3036
|
+
parkedOnly.push(directory);
|
|
3037
|
+
} catch {
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
2614
3040
|
}
|
|
2615
3041
|
}
|
|
2616
|
-
|
|
2617
|
-
|
|
3042
|
+
const parkedOnlySet = new Set(parkedOnly);
|
|
3043
|
+
candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
3044
|
+
if (explicit) {
|
|
2618
3045
|
return {
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
localSignerConfigured: false,
|
|
2623
|
-
localMcpConfigured: false,
|
|
2624
|
-
probeResult: errorCode2 === "local_mcp_unsupported_node_version" ? "local_stdio_mcp_unsupported_node_version" : "local_stdio_mcp_runtime_install_failed",
|
|
2625
|
-
restartRequired: true,
|
|
2626
|
-
nextUserAction: nextAction(runtime, profile.restartMode, errorCode2),
|
|
2627
|
-
errorCode: errorCode2,
|
|
2628
|
-
configTarget: profile.label,
|
|
2629
|
-
signerAcknowledged: signerConsent?.acknowledged,
|
|
2630
|
-
localMcpAcknowledged: localMcpConsent?.acknowledged,
|
|
2631
|
-
activationCommand: void 0,
|
|
2632
|
-
messages: [
|
|
2633
|
-
...consentMessages,
|
|
2634
|
-
`Could not prepare local Haven MCP runtime: ${localRuntimeError instanceof Error ? localRuntimeError.message : String(localRuntimeError)}`
|
|
2635
|
-
]
|
|
3046
|
+
directory: explicit,
|
|
3047
|
+
others: [...candidates.map((c) => c.directory), ...tombstonedOnly, ...parkedOnly].filter((d) => d !== explicit),
|
|
3048
|
+
parkedOnly: parkedOnlySet
|
|
2636
3049
|
};
|
|
2637
3050
|
}
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
probeResult: "signer_runtime_install_failed",
|
|
2653
|
-
restartRequired: false,
|
|
2654
|
-
nextUserAction: "The local Haven signer runtime could not be installed, so no configuration was written. Check your network (a cold install downloads the signer package set) and re-run: npx @haven_ai/connect@alpha",
|
|
2655
|
-
errorCode: "signer_runtime_install_failed",
|
|
2656
|
-
configTarget: profile.label,
|
|
2657
|
-
signerAcknowledged: signerConsent?.acknowledged,
|
|
2658
|
-
localMcpAcknowledged: localMcpConsent?.acknowledged,
|
|
2659
|
-
activationCommand: void 0,
|
|
2660
|
-
signerRuntimePrepared: false,
|
|
2661
|
-
messages: [
|
|
2662
|
-
...consentMessages,
|
|
2663
|
-
`Could not pre-install the local Haven signer: ${err instanceof Error ? err.message : String(err)}`,
|
|
2664
|
-
"No runtime configuration was written (fail-closed): a config pointing at an uninstalled signer looks wired but cannot start.",
|
|
2665
|
-
"Re-run `npx @haven_ai/connect@alpha` to retry the setup."
|
|
2666
|
-
]
|
|
2667
|
-
};
|
|
3051
|
+
if (candidates.length === 0 && tombstonedOnly.length === 0 && parkedOnly.length === 0) {
|
|
3052
|
+
return { others: [], parkedOnly: parkedOnlySet };
|
|
3053
|
+
}
|
|
3054
|
+
return {
|
|
3055
|
+
directory: candidates[0]?.directory,
|
|
3056
|
+
others: [...candidates.slice(1).map((c) => c.directory), ...tombstonedOnly, ...parkedOnly],
|
|
3057
|
+
parkedOnly: parkedOnlySet
|
|
3058
|
+
};
|
|
3059
|
+
}
|
|
3060
|
+
function agentIsWired(configText, names, slug, identity, sidecar, isPrimary, bareOwnerExists) {
|
|
3061
|
+
if (configText === null) return isPrimary;
|
|
3062
|
+
if (slug) {
|
|
3063
|
+
for (const name of [names.hosted, names.codexHosted, names.signer, names.codexSigner]) {
|
|
3064
|
+
if (new RegExp(`(^|[."'\\s\\[])${name}(["'\\]:\\s]|$)`, "m").test(configText)) return true;
|
|
2668
3065
|
}
|
|
3066
|
+
return false;
|
|
2669
3067
|
}
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
3068
|
+
if (sidecar?.wrapper_path && configText.includes(sidecar.wrapper_path)) return true;
|
|
3069
|
+
if (bareOwnerExists) return false;
|
|
3070
|
+
return isPrimary && Boolean(identity?.hosted_mcp_url && configText.includes(identity.hosted_mcp_url));
|
|
3071
|
+
}
|
|
3072
|
+
function rekeyPendingCheck(status, hostedDelegateAddress, runtime, slug) {
|
|
3073
|
+
const label = "Pending re-key";
|
|
3074
|
+
const nameFlag = slug ? ` --name ${slug}` : "";
|
|
3075
|
+
if (status.state === "unreadable") {
|
|
3076
|
+
return {
|
|
3077
|
+
id: "rekey_pending",
|
|
3078
|
+
label,
|
|
3079
|
+
ok: false,
|
|
3080
|
+
detail: `A re-key was started here but ${status.path} does not parse, so neither the address it generated nor when it started can be read. The file still holds what was a private key.`,
|
|
3081
|
+
repair: `Delete ${status.path}, then start again: ${RERUN} --rekey${nameFlag}`
|
|
3082
|
+
};
|
|
3083
|
+
}
|
|
3084
|
+
const started = status.startedAt ?? "an unknown time";
|
|
3085
|
+
const address = status.newDelegateAddress ?? "unknown";
|
|
3086
|
+
const completedOnHaven = hostedDelegateAddress !== void 0 && status.newDelegateAddress !== void 0 && hostedDelegateAddress.toLowerCase() === status.newDelegateAddress.toLowerCase();
|
|
3087
|
+
if (completedOnHaven) {
|
|
3088
|
+
return {
|
|
3089
|
+
id: "rekey_pending",
|
|
3090
|
+
label,
|
|
3091
|
+
ok: false,
|
|
3092
|
+
detail: `A re-key started ${started} has COMPLETED on Haven \u2014 the agent's signing address is already ${address}, the one this machine generated \u2014 but the local half was never finished, so the credential files here still hold the old key. Parked at ${status.path}.` + (status.state === "expired" ? " The local file is also past its 24h TTL, which --rekey-finish refuses, so the finish command below will not accept it any more." : ""),
|
|
3093
|
+
repair: status.state === "expired" ? `The parked key expired. Start again \u2014 ${RERUN} --rekey${nameFlag} \u2014 and re-run "Replace signing key" on the Haven agent page with the new address it prints.` : `Run: ${RERUN} --rekey-finish${nameFlag} --api-key <the key the agent page showed you> --runtime ${runtime}`
|
|
3094
|
+
};
|
|
3095
|
+
}
|
|
3096
|
+
const wedgeNote = "Haven is NOT yet on this address, so the re-key did not complete. This machine cannot tell whether the on-chain revoke on the agent page already ran: if it did not, closing this costs nothing; if it did, the agent's old delegations are revoked, no new ones were issued, and only an owner re-grant restores its spend authority (#1868). Check the agent page before assuming the harmless case.";
|
|
3097
|
+
if (status.state === "expired") {
|
|
3098
|
+
return {
|
|
3099
|
+
id: "rekey_pending",
|
|
3100
|
+
label,
|
|
3101
|
+
ok: false,
|
|
3102
|
+
detail: `A re-key started ${started} EXPIRED ${status.expiresAt ?? ""} without being finished. Its address was ${address}; the private half it generated is still on disk at ${status.path}. ` + wedgeNote,
|
|
3103
|
+
repair: `Either delete ${status.path} to drop the parked key, or start over: ${RERUN} --rekey${nameFlag}. Connect never deletes it for you \u2014 an expired TTL is a refusal to USE the key, not a licence to destroy key material you may still be mid-flow on.`
|
|
3104
|
+
};
|
|
3105
|
+
}
|
|
3106
|
+
return {
|
|
3107
|
+
id: "rekey_pending",
|
|
3108
|
+
label,
|
|
3109
|
+
ok: true,
|
|
3110
|
+
detail: `A re-key started ${started} is still open (expires ${status.expiresAt ?? "unknown"}). Paste this address into "Replace signing key" on the Haven agent page: ${address}. Parked at ${status.path}. ` + wedgeNote
|
|
3111
|
+
};
|
|
3112
|
+
}
|
|
3113
|
+
async function readIdentity(directory) {
|
|
3114
|
+
try {
|
|
3115
|
+
return JSON.parse(await promises.readFile(path.join(directory, "identity.json"), "utf8"));
|
|
3116
|
+
} catch {
|
|
3117
|
+
return void 0;
|
|
3118
|
+
}
|
|
3119
|
+
}
|
|
3120
|
+
async function checksForAgent(entry, input, deps) {
|
|
3121
|
+
const { directory, identity, sidecar } = entry;
|
|
3122
|
+
const checks = [];
|
|
3123
|
+
let signerCapabilities;
|
|
3124
|
+
let signerFile;
|
|
3125
|
+
try {
|
|
3126
|
+
const parsed = JSON.parse(await promises.readFile(path.join(directory, "signer.json"), "utf8"));
|
|
3127
|
+
signerFile = typeof parsed === "object" && parsed !== null ? parsed : void 0;
|
|
3128
|
+
} catch {
|
|
3129
|
+
signerFile = void 0;
|
|
3130
|
+
}
|
|
3131
|
+
const credentialsOk = Boolean(identity?.api_key) && signerFile !== void 0;
|
|
3132
|
+
checks.push({
|
|
3133
|
+
id: "credentials",
|
|
3134
|
+
label: "Agent credentials",
|
|
3135
|
+
ok: credentialsOk,
|
|
3136
|
+
detail: credentialsOk ? `identity.json and signer.json parse (agent ${identity?.agent_id ?? "unknown"})` : "identity.json or signer.json is missing or unparseable.",
|
|
3137
|
+
...credentialsOk ? {} : { repair: `Re-run the full setup with a fresh token: ${RERUN} --setup <token>.` }
|
|
2684
3138
|
});
|
|
2685
|
-
if (
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
3139
|
+
if (!sidecar) {
|
|
3140
|
+
checks.push({
|
|
3141
|
+
id: "signer_runtime",
|
|
3142
|
+
label: "Signer runtime (preinstalled wrapper)",
|
|
3143
|
+
ok: false,
|
|
3144
|
+
detail: "No signer-runtime.json sidecar \u2014 the pinned signer runtime was never prepared (or a pre-#1586 npx config).",
|
|
3145
|
+
repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}`
|
|
3146
|
+
});
|
|
3147
|
+
} else {
|
|
3148
|
+
const matches = await installedRuntimeMatches(sidecar.runtime_directory, sidecar.cli_path);
|
|
3149
|
+
const versionOk = sidecar.signer_version === exports.MCP_RUNTIME_MANIFEST.signerVersion;
|
|
3150
|
+
const ok = matches && versionOk;
|
|
3151
|
+
checks.push({
|
|
3152
|
+
id: "signer_runtime",
|
|
3153
|
+
label: "Signer runtime (preinstalled wrapper)",
|
|
3154
|
+
ok,
|
|
3155
|
+
detail: ok ? `Installed ${sidecar.signer_package}@${sidecar.signer_version} at ${sidecar.runtime_directory}` : matches ? `Installed version ${sidecar.signer_version} does not match the connector's pinned ${exports.MCP_RUNTIME_MANIFEST.signerVersion}.` : `Runtime directory is stale or empty (${sidecar.runtime_directory}) \u2014 the CLI or package versions are missing.`,
|
|
3156
|
+
...ok ? {} : { repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}` }
|
|
3157
|
+
});
|
|
3158
|
+
}
|
|
3159
|
+
const hostedUrl = identity?.hosted_mcp_url ?? (identity?.api_url ? `${identity.api_url}/mcp` : void 0);
|
|
3160
|
+
if (identity?.api_key && hostedUrl) {
|
|
3161
|
+
const probe = await (deps.probeHosted ?? probeHostedMcpTools)(identity.api_key, hostedUrl, deps.fetch);
|
|
3162
|
+
checks.push({
|
|
3163
|
+
id: "hosted_mcp",
|
|
3164
|
+
label: "Hosted Haven MCP",
|
|
3165
|
+
ok: probe.status === "ok",
|
|
3166
|
+
detail: probe.status === "ok" ? `Reachable and authorized (${hostedUrl}).` : `Probe failed: ${probe.status} (${hostedUrl}).`,
|
|
3167
|
+
...probe.status === "ok" ? {} : {
|
|
3168
|
+
repair: probe.status === "unauthorized" ? `The stored API key was rejected \u2014 re-run the full setup with a fresh token: ${RERUN} --setup <token>.` : "Check network access to the hosted MCP URL, then re-run --doctor."
|
|
3169
|
+
}
|
|
3170
|
+
});
|
|
3171
|
+
} else {
|
|
3172
|
+
checks.push({
|
|
3173
|
+
id: "hosted_mcp",
|
|
3174
|
+
label: "Hosted Haven MCP",
|
|
3175
|
+
ok: false,
|
|
3176
|
+
detail: "No stored API key / hosted MCP URL to probe with.",
|
|
3177
|
+
repair: `Re-run the full setup: ${RERUN} --setup <token>.`
|
|
3178
|
+
});
|
|
3179
|
+
}
|
|
3180
|
+
const localDelegate = typeof signerFile?.delegate_address === "string" ? signerFile.delegate_address : void 0;
|
|
3181
|
+
let hostedDelegateAddress;
|
|
3182
|
+
if (identity?.api_key && identity.api_url) {
|
|
3183
|
+
const probe = await (deps.probeHostedIdentity ?? probeHostedAgentIdentity)(
|
|
3184
|
+
identity.api_key,
|
|
3185
|
+
identity.api_url,
|
|
3186
|
+
deps.fetch
|
|
3187
|
+
);
|
|
3188
|
+
if (probe.status === "ok") hostedDelegateAddress = probe.delegateAddress;
|
|
3189
|
+
if (probe.status !== "ok") {
|
|
3190
|
+
checks.push({
|
|
3191
|
+
id: "identity_match",
|
|
3192
|
+
label: "Hosted identity matches the local signing key",
|
|
3193
|
+
ok: false,
|
|
3194
|
+
detail: probe.status === "unauthorized" ? "The stored API key was rejected, so the agent it authenticates as cannot be compared with the local signing key." : `Could not read the hosted identity (${probe.status}) \u2014 the comparison did not happen, so it cannot be reported as a match.`,
|
|
3195
|
+
repair: probe.status === "unauthorized" ? `Re-run the full setup with a fresh token: ${RERUN} --setup <token>.` : `Restore network access to the Haven API, then re-run: ${RERUN} --doctor --runtime ${input.runtime}`
|
|
3196
|
+
});
|
|
3197
|
+
} else if (!localDelegate) {
|
|
3198
|
+
checks.push({
|
|
3199
|
+
id: "identity_match",
|
|
3200
|
+
label: "Hosted identity matches the local signing key",
|
|
3201
|
+
ok: false,
|
|
3202
|
+
detail: "signer.json holds no delegate_address to compare against the hosted identity.",
|
|
3203
|
+
repair: `Re-run the full setup with a fresh token: ${RERUN} --setup <token>.`
|
|
3204
|
+
});
|
|
3205
|
+
} else {
|
|
3206
|
+
const same = probe.delegateAddress?.toLowerCase() === localDelegate.toLowerCase();
|
|
3207
|
+
checks.push({
|
|
3208
|
+
id: "identity_match",
|
|
3209
|
+
label: "Hosted identity matches the local signing key",
|
|
3210
|
+
ok: same,
|
|
3211
|
+
detail: same ? `The stored API key authenticates as the agent whose signing key is in this directory (${shortAddress(localDelegate)}).` : `MISMATCH: the stored API key authenticates as agent ${probe.agentId ?? "unknown"} with delegate ${shortAddress(probe.delegateAddress ?? "unknown")}, but signer.json here holds ${shortAddress(localDelegate)}. This runtime would quote as one agent and sign as another.`,
|
|
3212
|
+
...same ? {} : {
|
|
3213
|
+
repair: `Re-run setup for this agent so its API key and signing key come from one run: ${RERUN} --setup <token>. Do not hand-edit either file.`
|
|
3214
|
+
}
|
|
3215
|
+
});
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3218
|
+
const pending = await inspectRekeyPending(directory, deps.now?.() ?? Date.now());
|
|
3219
|
+
if (pending) {
|
|
3220
|
+
checks.push(rekeyPendingCheck(pending, hostedDelegateAddress, input.runtime, sidecar?.server_name));
|
|
3221
|
+
}
|
|
3222
|
+
if (sidecar) {
|
|
3223
|
+
const consent = await getLocalSignerConsentStatus(path.join(directory, "signer.json"));
|
|
3224
|
+
if (!consent.acknowledged) {
|
|
3225
|
+
checks.push({
|
|
3226
|
+
id: "signer_process",
|
|
3227
|
+
label: "Signer stdio handshake",
|
|
3228
|
+
ok: false,
|
|
3229
|
+
detail: "The local-tools consent is not acknowledged, so the signer refuses to start (by design).",
|
|
3230
|
+
repair: `Run: ${RERUN} --ack-local-tools --setup <token> (or re-run your original setup command with --ack-local-tools).`
|
|
3231
|
+
});
|
|
3232
|
+
} else {
|
|
3233
|
+
const probe = await (deps.probeSignerTools ?? probeLocalMcpTools)(
|
|
3234
|
+
sidecar.wrapper_path,
|
|
3235
|
+
[],
|
|
3236
|
+
exports.MCP_RUNTIME_MANIFEST.requiredSignerTools
|
|
3237
|
+
);
|
|
3238
|
+
const experimental = probe.capabilities?.experimental ?? probe.capabilities;
|
|
3239
|
+
const compat = experimental?.["haven/signer-compatibility"];
|
|
3240
|
+
signerCapabilities = compat ? { "haven/signer-compatibility": compat } : void 0;
|
|
3241
|
+
const compatDetail = compat ? ` Compat: x402 expected-context v${JSON.stringify(compat.x402_expected_context_versions ?? "?")}.` : "";
|
|
3242
|
+
checks.push({
|
|
3243
|
+
id: "signer_process",
|
|
3244
|
+
label: "Signer stdio handshake",
|
|
3245
|
+
ok: probe.status === "ok",
|
|
3246
|
+
detail: probe.status === "ok" ? `Signer started, listed ${probe.toolNames?.length ?? 0} tools${probe.serverInfo?.version ? ` (v${probe.serverInfo.version})` : ""}.${compatDetail}` : `Handshake failed: ${probe.status}.`,
|
|
3247
|
+
...probe.status === "ok" ? {} : { repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}` }
|
|
2701
3248
|
});
|
|
3249
|
+
}
|
|
3250
|
+
} else {
|
|
3251
|
+
checks.push({
|
|
3252
|
+
id: "signer_process",
|
|
3253
|
+
label: "Signer stdio handshake",
|
|
3254
|
+
ok: false,
|
|
3255
|
+
detail: "Skipped \u2014 no prepared signer runtime to probe.",
|
|
3256
|
+
repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}`
|
|
3257
|
+
});
|
|
3258
|
+
}
|
|
3259
|
+
return { checks, ...signerCapabilities ? { signerCapabilities } : {} };
|
|
3260
|
+
}
|
|
3261
|
+
async function runDoctor(input, deps = {}) {
|
|
3262
|
+
const homeDir = deps.homeDir ?? os.homedir();
|
|
3263
|
+
const checks = [];
|
|
3264
|
+
let signerCapabilities;
|
|
3265
|
+
const { directory, others, parkedOnly } = await discoverCredentialDirectory(homeDir, input.credentialsDir);
|
|
3266
|
+
const configPath = runtimeConfigPathFor(input.runtime, homeDir);
|
|
3267
|
+
let configText = null;
|
|
3268
|
+
if (configPath !== null) {
|
|
3269
|
+
try {
|
|
3270
|
+
configText = await promises.readFile(configPath, "utf8");
|
|
2702
3271
|
} catch {
|
|
3272
|
+
configText = null;
|
|
2703
3273
|
}
|
|
2704
3274
|
}
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
const
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
configResult.hostedConfigured ? probeHostedMcpTools(input.apiKey, input.hostedMcpUrl, deps.fetch) : Promise.resolve({ status: "bad_response" }),
|
|
2714
|
-
probeLocalSignerCredential(input.signerPath),
|
|
2715
|
-
localProbePromise,
|
|
2716
|
-
signerProbePromise
|
|
2717
|
-
]);
|
|
2718
|
-
const hostedOk = configResult.hostedConfigured && hostedProbe.status === "ok";
|
|
2719
|
-
const localMcpOk = configResult.runtimeMcpMode === "local_stdio" && configResult.localMcpConfigured && signerCredentialReady && Boolean(localMcpConsent?.acknowledged) && localMcpProbe?.status === "ok";
|
|
2720
|
-
const signerOk = configResult.runtimeMcpMode === "local_stdio" ? localMcpOk : configResult.signerConfigured && signerCredentialReady && Boolean(signerConsent?.acknowledged) && // #1587: no handshake, no green. A signer command that was registered
|
|
2721
|
-
// but not probed (manual topology) keeps the old semantics.
|
|
2722
|
-
(signerProbe === void 0 || signerProbe.status === "ok");
|
|
2723
|
-
const restartRequired = configResult.restartRequired || restartRequiredForRuntime(runtime, deps.env);
|
|
2724
|
-
const errorCode = configResult.errorCode ?? (configResult.runtimeMcpMode === "local_stdio" ? localMcpErrorCode(signerCredentialReady, localMcpConsent, localMcpProbe?.status) : hostedMcpErrorCode(configResult.hostedConfigured, hostedProbe.status) ?? signerConsentErrorCode(signerCredentialReady, signerConsent) ?? signerProbeErrorCode(signerProbe));
|
|
2725
|
-
const hostedProbeMessages = configResult.hostedConfigured && hostedProbe.status !== "ok" ? [`Hosted Haven MCP probe failed: ${hostedProbe.status}.`] : configResult.hostedConfigured ? ["Verified hosted Haven MCP tools with a read-only handshake."] : [];
|
|
2726
|
-
const signerProbeMessages = signerProbe ? signerProbe.status === "ok" ? ["Verified local Haven signer with a stdio handshake."] : [
|
|
2727
|
-
`Local Haven signer handshake failed: ${signerProbe.status}.`,
|
|
2728
|
-
"Re-run `npx @haven_ai/connect@alpha` to repair the signer setup."
|
|
2729
|
-
] : [];
|
|
2730
|
-
const localProbeMessages = localMcpProbe && localMcpProbe.status !== "ok" ? [`Local Haven MCP handshake failed: ${localMcpProbe.status}.`] : localMcpProbe?.status === "ok" ? ["Verified local Haven MCP tools with a stdio handshake."] : [];
|
|
2731
|
-
const skillInstall = !configResult.errorCode ? await installSkillForRuntime(runtime, { homeDir: deps.homeDir, env: deps.env }) : void 0;
|
|
2732
|
-
return {
|
|
2733
|
-
runtime,
|
|
2734
|
-
runtimeMcpMode: configResult.runtimeMcpMode,
|
|
2735
|
-
hostedMcpConfigured: hostedOk,
|
|
2736
|
-
localSignerConfigured: signerOk,
|
|
2737
|
-
localMcpConfigured: localMcpOk,
|
|
2738
|
-
probeResult: buildProbeResult(configResult.runtimeMcpMode, configResult.hostedConfigured, hostedProbe.status, signerOk, localMcpOk, localMcpProbe?.status),
|
|
2739
|
-
restartRequired,
|
|
2740
|
-
nextUserAction: nextAction(runtime, profile.restartMode, errorCode),
|
|
2741
|
-
errorCode,
|
|
2742
|
-
configTarget: configResult.target,
|
|
2743
|
-
signerAcknowledged: signerConsent?.acknowledged,
|
|
2744
|
-
localMcpAcknowledged: localMcpConsent?.acknowledged,
|
|
2745
|
-
activationCommand: configResult.activationCommand,
|
|
2746
|
-
skillInstalled: skillInstall?.installed,
|
|
2747
|
-
signerRuntimePrepared,
|
|
2748
|
-
messages: [...consentMessages, ...localRuntimeInstall?.messages ?? [], ...configResult.messages, ...hostedProbeMessages, ...signerProbeMessages, ...localProbeMessages, ...skillInstall?.messages ?? []]
|
|
2749
|
-
};
|
|
2750
|
-
}
|
|
2751
|
-
function runtimeInstallCapabilities(runtime, env = process.env) {
|
|
2752
|
-
const profile = runtimeProfile(runtime, env);
|
|
2753
|
-
return {
|
|
2754
|
-
canWriteRuntimeConfig: profile.canWriteRuntimeConfig,
|
|
2755
|
-
restartRequired: restartRequiredForRuntime(runtime, env)
|
|
2756
|
-
};
|
|
2757
|
-
}
|
|
2758
|
-
async function configureClaudeCode(deps, localMcpCommand, serverName) {
|
|
2759
|
-
const runCommand = deps.runCommand ?? defaultRunCommand;
|
|
2760
|
-
const serverJson = JSON.stringify({
|
|
2761
|
-
type: "stdio",
|
|
2762
|
-
command: localMcpCommand,
|
|
2763
|
-
args: [],
|
|
2764
|
-
env: {}
|
|
2765
|
-
});
|
|
2766
|
-
try {
|
|
2767
|
-
if (!localMcpCommand) throw new Error("local MCP wrapper command is required");
|
|
2768
|
-
const names = serverNamesFor(serverName);
|
|
2769
|
-
await runCommand("claude", ["mcp", "remove", names.hosted]).catch(() => void 0);
|
|
2770
|
-
await runCommand("claude", ["mcp", "remove", names.signer]).catch(() => void 0);
|
|
2771
|
-
await runCommand("claude", ["mcp", "add-json", names.hosted, serverJson, "--scope", "user"]).catch(async () => {
|
|
2772
|
-
await runCommand("claude", ["mcp", "add", names.hosted, "--scope", "user", "--", localMcpCommand]);
|
|
2773
|
-
});
|
|
2774
|
-
const verified = await runCommand("claude", ["mcp", "get", names.hosted]).then(() => true).catch(() => false);
|
|
2775
|
-
return {
|
|
2776
|
-
hostedConfigured: false,
|
|
2777
|
-
signerConfigured: true,
|
|
2778
|
-
localMcpConfigured: true,
|
|
2779
|
-
runtimeMcpMode: "local_stdio",
|
|
2780
|
-
target: "Claude Code MCP config",
|
|
2781
|
-
changed: true,
|
|
2782
|
-
restartRequired: true,
|
|
2783
|
-
messages: [
|
|
2784
|
-
"Updated local Haven MCP entry with Claude Code.",
|
|
2785
|
-
...verified ? ["Verified Claude Code MCP entry."] : []
|
|
2786
|
-
]
|
|
2787
|
-
};
|
|
2788
|
-
} catch (err) {
|
|
2789
|
-
return {
|
|
2790
|
-
hostedConfigured: false,
|
|
2791
|
-
signerConfigured: false,
|
|
2792
|
-
localMcpConfigured: false,
|
|
2793
|
-
runtimeMcpMode: "local_stdio",
|
|
2794
|
-
target: "Claude Code MCP config",
|
|
2795
|
-
changed: false,
|
|
2796
|
-
restartRequired: true,
|
|
2797
|
-
messages: [
|
|
2798
|
-
`Could not update Claude Code MCP config: ${err instanceof Error ? err.message : String(err)}`,
|
|
2799
|
-
"Install Claude Code or rerun the Haven setup command inside a Claude Code terminal."
|
|
2800
|
-
],
|
|
2801
|
-
errorCode: "claude_code_config_failed"
|
|
2802
|
-
};
|
|
3275
|
+
const allDirectories = directory ? [directory, ...others] : others;
|
|
3276
|
+
let bareOwnerExists = false;
|
|
3277
|
+
for (const dir of allDirectories) {
|
|
3278
|
+
const sidecar = await readRuntimeSidecar(dir);
|
|
3279
|
+
if (!sidecar?.server_name && sidecar?.wrapper_path && configText?.includes(sidecar.wrapper_path)) {
|
|
3280
|
+
bareOwnerExists = true;
|
|
3281
|
+
break;
|
|
3282
|
+
}
|
|
2803
3283
|
}
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
const
|
|
2807
|
-
const
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
]
|
|
2837
|
-
};
|
|
2838
|
-
} catch (err) {
|
|
2839
|
-
return {
|
|
2840
|
-
hostedConfigured: false,
|
|
2841
|
-
signerConfigured: false,
|
|
2842
|
-
localMcpConfigured: false,
|
|
2843
|
-
runtimeMcpMode: "hosted_plus_signer",
|
|
2844
|
-
target: "Claude Code MCP config",
|
|
2845
|
-
changed: false,
|
|
2846
|
-
restartRequired: true,
|
|
2847
|
-
messages: [
|
|
2848
|
-
`Could not update Claude Code MCP config: ${err instanceof Error ? err.message : String(err)}`,
|
|
2849
|
-
"Install Claude Code or rerun the Haven setup command inside a Claude Code terminal."
|
|
2850
|
-
],
|
|
2851
|
-
errorCode: "claude_code_config_failed"
|
|
3284
|
+
const inventory = [];
|
|
3285
|
+
const capabilitiesByDirectory = /* @__PURE__ */ new Map();
|
|
3286
|
+
const primaryChecksById = /* @__PURE__ */ new Map();
|
|
3287
|
+
for (const dir of allDirectories) {
|
|
3288
|
+
const identity = await readIdentity(dir);
|
|
3289
|
+
const sidecar = await readRuntimeSidecar(dir);
|
|
3290
|
+
const tombstone = await readAgentTombstone(dir);
|
|
3291
|
+
const slug = sidecar?.server_name;
|
|
3292
|
+
const names = serverNamesFor(slug);
|
|
3293
|
+
const rekeyPending = await inspectRekeyPending(dir, deps.now?.() ?? Date.now());
|
|
3294
|
+
if (!identity?.api_key) {
|
|
3295
|
+
const agentId = tombstone?.agent_id ?? rekeyPending?.agentId;
|
|
3296
|
+
inventory.push({
|
|
3297
|
+
...slug ? { slug } : {},
|
|
3298
|
+
...agentId ? { agentId } : {},
|
|
3299
|
+
directory: dir,
|
|
3300
|
+
// A tombstone is a deliberate record and outranks the discovery tell:
|
|
3301
|
+
// a retired directory that also holds a parked key stays `retired`.
|
|
3302
|
+
classification: tombstone ? "retired" : parkedOnly.has(dir) ? "parked" : "orphaned",
|
|
3303
|
+
checks: rekeyPending ? [rekeyPendingCheck(rekeyPending, void 0, input.runtime, slug)] : [],
|
|
3304
|
+
...rekeyPending ? { rekeyPending } : {}
|
|
3305
|
+
});
|
|
3306
|
+
continue;
|
|
3307
|
+
}
|
|
3308
|
+
const wired = agentIsWired(configText, names, slug, identity, sidecar, dir === directory, bareOwnerExists);
|
|
3309
|
+
const entry = {
|
|
3310
|
+
...slug ? { slug } : {},
|
|
3311
|
+
...identity.agent_id ? { agentId: identity.agent_id } : {},
|
|
3312
|
+
directory: dir,
|
|
3313
|
+
classification: wired ? "wired" : "superseded",
|
|
3314
|
+
checks: [],
|
|
3315
|
+
...rekeyPending ? { rekeyPending } : {}
|
|
2852
3316
|
};
|
|
3317
|
+
if (wired) {
|
|
3318
|
+
const result = await checksForAgent({ directory: dir, identity, sidecar }, input, deps);
|
|
3319
|
+
entry.checks = result.checks;
|
|
3320
|
+
capabilitiesByDirectory.set(dir, result.signerCapabilities);
|
|
3321
|
+
} else if (rekeyPending) {
|
|
3322
|
+
entry.checks = [rekeyPendingCheck(rekeyPending, void 0, input.runtime, slug)];
|
|
3323
|
+
}
|
|
3324
|
+
inventory.push(entry);
|
|
2853
3325
|
}
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
3326
|
+
const wiredDirectories = inventory.filter((entry) => entry.classification === "wired").map((entry) => entry.directory);
|
|
3327
|
+
const primaryDirectory = input.credentialsDir ? directory : wiredDirectories.includes(directory ?? "") ? directory : wiredDirectories[0] ?? directory;
|
|
3328
|
+
if (primaryDirectory) {
|
|
3329
|
+
const primaryEntry = inventory.find((entry) => entry.directory === primaryDirectory);
|
|
3330
|
+
signerCapabilities = capabilitiesByDirectory.get(primaryDirectory);
|
|
3331
|
+
for (const check of primaryEntry?.checks ?? []) primaryChecksById.set(check.id, check);
|
|
3332
|
+
}
|
|
3333
|
+
if (!primaryDirectory) {
|
|
3334
|
+
checks.push({
|
|
3335
|
+
id: "credentials",
|
|
3336
|
+
label: "Agent credentials",
|
|
3337
|
+
ok: false,
|
|
3338
|
+
detail: "No agent credential directory with an identity.json under ~/.haven/agents.",
|
|
3339
|
+
repair: `Run the full setup once: ${RERUN} --setup <token from the Haven dashboard>.`
|
|
3340
|
+
});
|
|
3341
|
+
} else {
|
|
3342
|
+
const primaryIdentity = await readIdentity(primaryDirectory);
|
|
3343
|
+
const primarySidecar = await readRuntimeSidecar(primaryDirectory);
|
|
3344
|
+
if (!primaryChecksById.has("credentials")) {
|
|
3345
|
+
const result = await checksForAgent(
|
|
3346
|
+
{ directory: primaryDirectory, identity: primaryIdentity, sidecar: primarySidecar },
|
|
3347
|
+
input,
|
|
3348
|
+
deps
|
|
3349
|
+
);
|
|
3350
|
+
signerCapabilities = result.signerCapabilities;
|
|
3351
|
+
for (const check of result.checks) primaryChecksById.set(check.id, check);
|
|
3352
|
+
}
|
|
3353
|
+
for (const id of ["credentials", "signer_runtime"]) {
|
|
3354
|
+
const check = primaryChecksById.get(id);
|
|
3355
|
+
if (check) checks.push(check);
|
|
3356
|
+
}
|
|
3357
|
+
}
|
|
3358
|
+
if (configPath === null) {
|
|
3359
|
+
checks.push({
|
|
3360
|
+
id: "runtime_config",
|
|
3361
|
+
label: "Runtime MCP config",
|
|
3362
|
+
ok: true,
|
|
3363
|
+
detail: `Runtime '${input.runtime}' has no file-based config the connector owns (CLI-managed) \u2014 skipping the file check.`
|
|
3364
|
+
});
|
|
3365
|
+
} else if (configText === null) {
|
|
3366
|
+
checks.push({
|
|
3367
|
+
id: "runtime_config",
|
|
3368
|
+
label: "Runtime MCP config",
|
|
3369
|
+
ok: false,
|
|
3370
|
+
detail: `No runtime config at ${configPath}.`,
|
|
3371
|
+
repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}`
|
|
3372
|
+
});
|
|
3373
|
+
} else {
|
|
3374
|
+
const primaryIdentity = await readIdentity(primaryDirectory ?? "");
|
|
3375
|
+
const primarySidecar = primaryDirectory ? await readRuntimeSidecar(primaryDirectory) : null;
|
|
3376
|
+
const hasHaven = primaryIdentity?.hosted_mcp_url ? configText.includes(primaryIdentity.hosted_mcp_url) : configText.includes("haven");
|
|
3377
|
+
const signerViaNpx = configText.includes("@haven_ai/signer");
|
|
3378
|
+
const wrapperReferenced = primarySidecar ? configText.includes(primarySidecar.wrapper_path) : false;
|
|
3379
|
+
const ok = hasHaven && !signerViaNpx && (primarySidecar ? wrapperReferenced : true);
|
|
3380
|
+
checks.push({
|
|
3381
|
+
id: "runtime_config",
|
|
3382
|
+
label: "Runtime MCP config",
|
|
3383
|
+
ok,
|
|
3384
|
+
detail: ok ? `Config at ${configPath} references the hosted server and the prepared signer wrapper.` : signerViaNpx ? `Config at ${configPath} still launches the signer via npx \u2014 the pre-#1586 shape that cannot start under a 120s startup timeout.` : `Config at ${configPath} is missing the Haven entries${primarySidecar && !wrapperReferenced ? " (or references a different signer wrapper)" : ""}.`,
|
|
3385
|
+
...ok ? {} : { repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}` }
|
|
3386
|
+
});
|
|
2862
3387
|
}
|
|
2863
|
-
const
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
}
|
|
2867
|
-
async function resolveLocalMcpConsent(input, messages) {
|
|
2868
|
-
if (input.ackLocalTools || input.ackSigner) {
|
|
2869
|
-
const status = await acknowledgeLocalMcpConsent(input.identityPath, input.signerPath, (message) => messages.push(message));
|
|
2870
|
-
if (status.acknowledged) {
|
|
2871
|
-
messages.push("Prepared the local Haven tools acknowledgement.");
|
|
2872
|
-
} else {
|
|
2873
|
-
messages.push("Local Haven tools acknowledgement still needs attention.");
|
|
2874
|
-
}
|
|
2875
|
-
return status;
|
|
3388
|
+
for (const id of ["hosted_mcp", "identity_match", "rekey_pending"]) {
|
|
3389
|
+
const check = primaryChecksById.get(id);
|
|
3390
|
+
if (check) checks.push(check);
|
|
2876
3391
|
}
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
const
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
3392
|
+
const otherEntries = inventory.filter((entry) => entry.directory !== primaryDirectory);
|
|
3393
|
+
if (otherEntries.length > 0) {
|
|
3394
|
+
const live = [];
|
|
3395
|
+
const revoked = [];
|
|
3396
|
+
const unverifiable = [];
|
|
3397
|
+
const retired = [];
|
|
3398
|
+
for (const entry of otherEntries) {
|
|
3399
|
+
const identity = await readIdentity(entry.directory);
|
|
3400
|
+
const tombstone = await readAgentTombstone(entry.directory);
|
|
3401
|
+
const otherAgent = entry.agentId ?? path.basename(entry.directory);
|
|
3402
|
+
const otherUrl = identity?.hosted_mcp_url ?? (identity?.api_url ? `${identity.api_url}/mcp` : void 0);
|
|
3403
|
+
if (!identity?.api_key || !otherUrl) {
|
|
3404
|
+
if (tombstone) retired.push(`${otherAgent} (retired ${tombstone.retired_at})`);
|
|
3405
|
+
else unverifiable.push(`${otherAgent} (no stored key/URL to probe)`);
|
|
3406
|
+
continue;
|
|
3407
|
+
}
|
|
3408
|
+
const suffix = tombstone ? " [tombstoned \u2014 key material still present]" : "";
|
|
3409
|
+
const probe = await (deps.probeHosted ?? probeHostedMcpTools)(identity.api_key, otherUrl, deps.fetch);
|
|
3410
|
+
if (probe.status === "ok") live.push({ label: `${otherAgent}${suffix}`, entry });
|
|
3411
|
+
else if (probe.status === "unauthorized") revoked.push(`${otherAgent}${suffix}`);
|
|
3412
|
+
else unverifiable.push(`${otherAgent} (${probe.status})${suffix}`);
|
|
2886
3413
|
}
|
|
2887
|
-
|
|
3414
|
+
const parts = [];
|
|
3415
|
+
if (live.length > 0) parts.push(`STILL SPEND-CAPABLE: ${live.map((item) => item.label).join(", ")}`);
|
|
3416
|
+
if (revoked.length > 0) parts.push(`already revoked: ${revoked.join(", ")}`);
|
|
3417
|
+
if (retired.length > 0) parts.push(`tombstoned (keys removed): ${retired.join(", ")}`);
|
|
3418
|
+
if (unverifiable.length > 0) parts.push(`could not verify: ${unverifiable.join(", ")}`);
|
|
3419
|
+
const supersededLive = live.filter((item) => item.entry.classification !== "wired").map((item) => item.label);
|
|
3420
|
+
checks.push({
|
|
3421
|
+
id: "superseded_agents",
|
|
3422
|
+
label: "Superseded agent credentials",
|
|
3423
|
+
ok: supersededLive.length === 0,
|
|
3424
|
+
detail: supersededLive.length > 0 ? `${otherEntries.length} other credential dir(s) found \u2014 ${parts.join("; ")}. A host started before your latest setup keeps authenticating (and spending) as the old agent.` : `${otherEntries.length} other credential dir(s) found \u2014 ${parts.join("; ")}.`,
|
|
3425
|
+
...supersededLive.length > 0 ? {
|
|
3426
|
+
repair: `Revoke ${supersededLive.join(", ")} on the Haven agent page, then remove the old director(y/ies) under ~/.haven/agents. Connect never revokes or deletes for you.`
|
|
3427
|
+
} : {}
|
|
3428
|
+
});
|
|
2888
3429
|
}
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
if (errorCode) return "return_to_haven_for_wallet_approval_then_finish_runtime_setup";
|
|
2912
|
-
if (restartMode === "hot-reload") return "return_to_haven_for_wallet_approval";
|
|
2913
|
-
if (runtime === "codex-cli" || runtime === "codex-desktop") return "return_to_haven_for_wallet_approval_then_restart_codex";
|
|
2914
|
-
if (runtime === "claude-code") return "return_to_haven_for_wallet_approval_then_restart_claude_code";
|
|
2915
|
-
if (restartMode === "restart-app") return "return_to_haven_for_wallet_approval_then_restart_app";
|
|
2916
|
-
if (restartMode === "restart-session") return "return_to_haven_for_wallet_approval_then_restart_agent_session";
|
|
2917
|
-
return "return_to_haven_for_wallet_approval_then_configure_runtime";
|
|
2918
|
-
}
|
|
2919
|
-
function supportsLocalMcp(runtime) {
|
|
2920
|
-
return runtime === "codex-cli" || runtime === "codex-desktop" || runtime === "claude-code";
|
|
2921
|
-
}
|
|
2922
|
-
async function prepareRuntimeForLocalMcp(input, deps) {
|
|
2923
|
-
const prepare = deps.prepareLocalMcpRuntime ?? ((runtimeInput) => prepareLocalMcpRuntime(runtimeInput, { runCommand: deps.runCommand, onProgress: deps.onProgress }));
|
|
2924
|
-
return prepare({
|
|
2925
|
-
credentialDirectory: input.credentialDirectory,
|
|
2926
|
-
identityPath: input.identityPath,
|
|
2927
|
-
signerPath: input.signerPath,
|
|
2928
|
-
homeDir: deps.homeDir,
|
|
2929
|
-
serverName: input.serverName
|
|
2930
|
-
});
|
|
2931
|
-
}
|
|
2932
|
-
async function prepareSignerForRuntime(input, deps) {
|
|
2933
|
-
const prepare = deps.prepareSignerRuntime ?? ((runtimeInput) => (
|
|
2934
|
-
// onProgress threaded through on purpose (#1586 review): without it the
|
|
2935
|
-
// install heartbeat was dead code in production and the console still
|
|
2936
|
-
// went silent for the whole cold install — the exact symptom the issue
|
|
2937
|
-
// set out to remove, at a longer timeout.
|
|
2938
|
-
prepareSignerRuntime(runtimeInput, { runCommand: deps.runCommand, onProgress: deps.onProgress })
|
|
2939
|
-
));
|
|
2940
|
-
return prepare({
|
|
2941
|
-
credentialDirectory: input.credentialDirectory,
|
|
2942
|
-
signerPath: input.signerPath,
|
|
2943
|
-
homeDir: deps.homeDir,
|
|
2944
|
-
serverName: input.serverName
|
|
3430
|
+
const parkedElsewhere = inventory.filter((entry) => entry.directory !== primaryDirectory && entry.rekeyPending).map((entry) => ({ entry, pending: entry.rekeyPending }));
|
|
3431
|
+
if (parkedElsewhere.length > 0) {
|
|
3432
|
+
const abandoned = parkedElsewhere.filter((item) => item.pending.state !== "pending");
|
|
3433
|
+
const describe = (item) => `${item.entry.slug ?? item.entry.agentId ?? path.basename(item.entry.directory)} (${item.pending.state}, ${item.pending.path})`;
|
|
3434
|
+
checks.push({
|
|
3435
|
+
id: "rekey_pending_elsewhere",
|
|
3436
|
+
label: "Parked re-keys in other credential directories",
|
|
3437
|
+
ok: abandoned.length === 0,
|
|
3438
|
+
detail: abandoned.length > 0 ? `ABANDONED re-key key material outside the agent this report describes: ${abandoned.map(describe).join(", ")}. Each holds a private key that was generated for a re-key nobody finished.` : `${parkedElsewhere.length} other director(y/ies) hold an open pending re-key: ${parkedElsewhere.map(describe).join(", ")}.`,
|
|
3439
|
+
...abandoned.length > 0 ? {
|
|
3440
|
+
repair: "Check the Haven agent page for each before deleting: if its on-chain revoke already ran, the agent has no spend authority until you re-grant it (#1868), and that is not visible from this machine. Connect never deletes key material for you."
|
|
3441
|
+
} : {}
|
|
3442
|
+
});
|
|
3443
|
+
}
|
|
3444
|
+
const signerProcess = primaryChecksById.get("signer_process");
|
|
3445
|
+
if (signerProcess) checks.push(signerProcess);
|
|
3446
|
+
const restart = restartRequiredForRuntime(input.runtime, deps.env);
|
|
3447
|
+
checks.push({
|
|
3448
|
+
id: "restart",
|
|
3449
|
+
label: "Runtime restart",
|
|
3450
|
+
ok: true,
|
|
3451
|
+
detail: restart ? "This runtime loads MCP config at startup \u2014 restart it after any repair before expecting the tools to appear." : "No restart requirement known for this runtime."
|
|
2945
3452
|
});
|
|
3453
|
+
const wiredOk = inventory.filter((entry) => entry.classification === "wired").every((entry) => entry.checks.every((check) => check.ok));
|
|
3454
|
+
return {
|
|
3455
|
+
version: 1,
|
|
3456
|
+
ok: checks.every((check) => check.ok) && wiredOk,
|
|
3457
|
+
runtime: input.runtime,
|
|
3458
|
+
credentialDirectory: primaryDirectory,
|
|
3459
|
+
checks,
|
|
3460
|
+
agents: inventory,
|
|
3461
|
+
...signerCapabilities ? { signerCapabilities } : {}
|
|
3462
|
+
};
|
|
2946
3463
|
}
|
|
2947
|
-
async function
|
|
2948
|
-
const
|
|
3464
|
+
async function runRepair(input, deps = {}) {
|
|
3465
|
+
const homeDir = deps.homeDir ?? os.homedir();
|
|
3466
|
+
const messages = [];
|
|
3467
|
+
const { directory, others } = await discoverCredentialDirectory(homeDir, input.credentialsDir);
|
|
3468
|
+
if (others.length > 0) {
|
|
3469
|
+
messages.push(`Note: ${others.length} other agent credential dir(s) exist \u2014 run --doctor for their status.`);
|
|
3470
|
+
}
|
|
3471
|
+
if (!directory) {
|
|
3472
|
+
return {
|
|
3473
|
+
ok: false,
|
|
3474
|
+
messages: [`No agent credentials found to repair \u2014 run the full setup: ${RERUN} --setup <token>.`]
|
|
3475
|
+
};
|
|
3476
|
+
}
|
|
3477
|
+
let identity;
|
|
2949
3478
|
try {
|
|
2950
|
-
|
|
3479
|
+
identity = JSON.parse(await promises.readFile(path.join(directory, "identity.json"), "utf8"));
|
|
2951
3480
|
} catch {
|
|
2952
|
-
return {
|
|
3481
|
+
return { ok: false, messages: ["identity.json is unreadable \u2014 re-run the full setup with a fresh token."] };
|
|
2953
3482
|
}
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
if (err && typeof err === "object" && "code" in err && err.code === "local_mcp_unsupported_node_version") {
|
|
2957
|
-
return "local_mcp_unsupported_node_version";
|
|
3483
|
+
if (!identity.api_key || !(identity.hosted_mcp_url || identity.api_url)) {
|
|
3484
|
+
return { ok: false, messages: ["identity.json lacks the stored API key / hosted URL \u2014 re-run the full setup."] };
|
|
2958
3485
|
}
|
|
2959
|
-
|
|
3486
|
+
const configPath = runtimeConfigPathFor(input.runtime, homeDir);
|
|
3487
|
+
if (configPath) {
|
|
3488
|
+
try {
|
|
3489
|
+
const existing = await promises.readFile(configPath, "utf8");
|
|
3490
|
+
if (existing.includes("bin/haven-mcp") || existing.includes(".haven/mcp-runtime")) {
|
|
3491
|
+
return {
|
|
3492
|
+
ok: false,
|
|
3493
|
+
messages: [
|
|
3494
|
+
`The config at ${configPath} is the LOCAL-stdio topology (--local). Repair currently rewrites only the hosted+signer shape and will not touch it.`,
|
|
3495
|
+
"Re-run your original setup command (with --local) to repair a local-stdio install."
|
|
3496
|
+
]
|
|
3497
|
+
};
|
|
3498
|
+
}
|
|
3499
|
+
} catch {
|
|
3500
|
+
}
|
|
3501
|
+
}
|
|
3502
|
+
const existingSidecar = await readRuntimeSidecar(directory);
|
|
3503
|
+
const serverName = existingSidecar?.server_name;
|
|
3504
|
+
const signerPath = path.join(directory, "signer.json");
|
|
3505
|
+
const prepared = await prepareSignerRuntime(
|
|
3506
|
+
{ credentialDirectory: directory, signerPath, homeDir, serverName },
|
|
3507
|
+
{ runCommand: deps.runCommand }
|
|
3508
|
+
);
|
|
3509
|
+
messages.push(...prepared.messages);
|
|
3510
|
+
const names = serverNamesFor(serverName);
|
|
3511
|
+
messages.push(`Rewriting MCP entries ${names.hosted} / ${names.signer}${serverName ? ` (agent "${serverName}")` : " (unnamed pair)"} \u2014 no other pair is touched.`);
|
|
3512
|
+
const configResult = await writeRuntimeConfig({
|
|
3513
|
+
runtime: input.runtime,
|
|
3514
|
+
hostedMcpUrl: identity.hosted_mcp_url ?? `${identity.api_url}/mcp`,
|
|
3515
|
+
apiKey: identity.api_key,
|
|
3516
|
+
identityPath: path.join(directory, "identity.json"),
|
|
3517
|
+
signerPath,
|
|
3518
|
+
credentialDirectory: directory,
|
|
3519
|
+
signerCommand: { command: prepared.command, args: prepared.args },
|
|
3520
|
+
homeDir,
|
|
3521
|
+
mode: "hosted",
|
|
3522
|
+
serverName
|
|
3523
|
+
});
|
|
3524
|
+
messages.push(...configResult.messages);
|
|
3525
|
+
messages.push("Repair complete \u2014 restart the runtime, then verify with --doctor.");
|
|
3526
|
+
return { ok: true, messages };
|
|
2960
3527
|
}
|
|
3528
|
+
var RERUN;
|
|
3529
|
+
var init_doctor = __esm({
|
|
3530
|
+
"src/doctor.ts"() {
|
|
3531
|
+
init_runtime_manifest();
|
|
3532
|
+
init_probes();
|
|
3533
|
+
init_signer_runtime();
|
|
3534
|
+
init_config_writers();
|
|
3535
|
+
init_runtime_registry();
|
|
3536
|
+
init_signer_consent();
|
|
3537
|
+
init_tombstone();
|
|
3538
|
+
init_server_names();
|
|
3539
|
+
init_storage();
|
|
3540
|
+
init_redact();
|
|
3541
|
+
RERUN = "npx @haven_ai/connect@alpha";
|
|
3542
|
+
}
|
|
3543
|
+
});
|
|
2961
3544
|
|
|
2962
3545
|
// src/runtime.ts
|
|
3546
|
+
init_api();
|
|
3547
|
+
init_key();
|
|
3548
|
+
init_redact();
|
|
3549
|
+
init_server_names();
|
|
3550
|
+
init_storage();
|
|
3551
|
+
init_runtime_install();
|
|
2963
3552
|
init_runtime_registry();
|
|
2964
3553
|
init_connect_error();
|
|
2965
3554
|
|
|
@@ -3132,8 +3721,9 @@ function defaultPromptIo() {
|
|
|
3132
3721
|
}
|
|
3133
3722
|
|
|
3134
3723
|
// src/runtime.ts
|
|
3724
|
+
init_local_mcp_runtime();
|
|
3135
3725
|
init_runtime_manifest();
|
|
3136
|
-
var CONNECTOR_VERSION = "0.1.
|
|
3726
|
+
var CONNECTOR_VERSION = "0.1.31-alpha.0";
|
|
3137
3727
|
var CONNECT_OUTCOME_SCHEMA_VERSION = 1;
|
|
3138
3728
|
async function runConnect(options, deps = {}) {
|
|
3139
3729
|
assertSupportedNodeVersion(deps.nodeVersion, exports.MCP_RUNTIME_MANIFEST.minimumNodeVersion);
|
|
@@ -3158,7 +3748,14 @@ async function runConnect(options, deps = {}) {
|
|
|
3158
3748
|
throw new exports.ConnectError(
|
|
3159
3749
|
"runtime_undetermined",
|
|
3160
3750
|
`Could not determine the agent runtime: nothing was detected in this environment and no --runtime was given. If you are an AI agent running this command: re-run it once, unchanged except for adding --runtime <name>, naming the harness you are running in \u2014 one of: ${exports.RUNTIME_FLAG_VALUES} (the aliases cowork, codex and openclaw are accepted too). Do not guess: if your harness is not one of those, use --runtime other, which stores the credentials and prints the manual MCP steps. Nothing was written and the Haven setup token is still unused.`,
|
|
3161
|
-
"rerun_connect_with_explicit_runtime"
|
|
3751
|
+
"rerun_connect_with_explicit_runtime",
|
|
3752
|
+
// #2091: the values must ride structurally too. The backend's setup
|
|
3753
|
+
// prompt permits a retry only with "one of the values that refusal
|
|
3754
|
+
// lists" — and --json discards prose, so a prose-only list deadlocked
|
|
3755
|
+
// every automation run in an undetected runtime (Codex in the field:
|
|
3756
|
+
// npx needs network, Codex runs network commands unsandboxed, and the
|
|
3757
|
+
// unsandboxed path carries none of the CODEX_* detection vars).
|
|
3758
|
+
{ allowedRuntimes: RUNTIME_FLAG_VALUE_LIST }
|
|
3162
3759
|
);
|
|
3163
3760
|
}
|
|
3164
3761
|
const runtime = selection.runtime;
|
|
@@ -3180,11 +3777,16 @@ async function runConnect(options, deps = {}) {
|
|
|
3180
3777
|
log(`runtime: ${runtime} (chosen at the prompt \u2014 nothing was detected in this environment)`);
|
|
3181
3778
|
}
|
|
3182
3779
|
log("Warming up your connection to Haven\u2026");
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3780
|
+
let setup;
|
|
3781
|
+
try {
|
|
3782
|
+
setup = await api.resolveSetup({
|
|
3783
|
+
setupToken: options.setupToken,
|
|
3784
|
+
connectorVersion,
|
|
3785
|
+
runtime
|
|
3786
|
+
});
|
|
3787
|
+
} catch (err) {
|
|
3788
|
+
throw deadSetupTokenError(err) ?? err;
|
|
3789
|
+
}
|
|
3188
3790
|
assertSetupChallengeIsUsable(setup.challenge.expires_at);
|
|
3189
3791
|
printSetupSummary(setup, log);
|
|
3190
3792
|
await preflightStorage({ baseDir: options.credentialsDir, warn: log });
|
|
@@ -3209,6 +3811,11 @@ async function runConnect(options, deps = {}) {
|
|
|
3209
3811
|
proofSignature,
|
|
3210
3812
|
apiKeyHash: hashAgentApiKey(localApiKey),
|
|
3211
3813
|
apiKeyPrefix: agentApiKeyPrefix(localApiKey),
|
|
3814
|
+
// #1878: report the pair we are ACTUALLY wiring, bare pair included, so
|
|
3815
|
+
// the dashboard can name it. Derived here rather than sent as the raw
|
|
3816
|
+
// slug — `serverNamesFor` is the one place the naming rule lives, and
|
|
3817
|
+
// the hosted name is what a user pastes into an MCP config.
|
|
3818
|
+
mcpServerName: serverNamesFor(options.serverName).hosted,
|
|
3212
3819
|
connectorContext: {
|
|
3213
3820
|
environment_label: options.environmentLabel ?? "Local workspace",
|
|
3214
3821
|
config_target: installCapabilities.canWriteRuntimeConfig ? "agent runtime MCP config" : "local credential files"
|
|
@@ -3216,6 +3823,8 @@ async function runConnect(options, deps = {}) {
|
|
|
3216
3823
|
installCapabilities
|
|
3217
3824
|
});
|
|
3218
3825
|
} catch (err) {
|
|
3826
|
+
const dead = deadSetupTokenError(err);
|
|
3827
|
+
if (dead) throw dead;
|
|
3219
3828
|
if (isExpiredSetupChallenge(err)) {
|
|
3220
3829
|
throw new Error(
|
|
3221
3830
|
"The Haven setup challenge expired while connecting. Return to Haven, start a fresh connection, and run its new Connect command. Do not reuse or paste credentials."
|
|
@@ -3413,7 +4022,17 @@ function failedConnectOutcome(runtimeHint, error) {
|
|
|
3413
4022
|
tools: ["haven_get_agent", "haven_get_allowances"],
|
|
3414
4023
|
instruction: "After a successful setup and activation, verify only with haven_get_agent and haven_get_allowances."
|
|
3415
4024
|
},
|
|
3416
|
-
error: {
|
|
4025
|
+
error: {
|
|
4026
|
+
code,
|
|
4027
|
+
next_action: nextAction2,
|
|
4028
|
+
// Only a ConnectError's message enters the JSON record: the vocabulary's
|
|
4029
|
+
// prose is connector-authored and safe to serialize, while a plain
|
|
4030
|
+
// Error can carry arbitrary server or filesystem detail (that stance is
|
|
4031
|
+
// pinned by test). Redaction stays on as belt-and-braces; plain-Error
|
|
4032
|
+
// runs still get their redacted message on stderr via the CLI mirror.
|
|
4033
|
+
...error instanceof exports.ConnectError && message ? { message: redactForAutomation(message) } : {},
|
|
4034
|
+
...error instanceof exports.ConnectError && error.details.allowedRuntimes ? { allowed_runtimes: error.details.allowedRuntimes } : {}
|
|
4035
|
+
}
|
|
3417
4036
|
};
|
|
3418
4037
|
}
|
|
3419
4038
|
function printSetupSummary(setup, log) {
|
|
@@ -3435,16 +4054,20 @@ function assertSetupChallengeIsUsable(expiresAt) {
|
|
|
3435
4054
|
"This Haven setup challenge is expired or invalid. Return to Haven, start a fresh connection, and rerun Connect. No local credentials were written."
|
|
3436
4055
|
);
|
|
3437
4056
|
}
|
|
4057
|
+
function deadSetupTokenError(err) {
|
|
4058
|
+
if (!(err instanceof ConnectRequestError) || err.status !== 410 && err.status !== 401) return null;
|
|
4059
|
+
return new exports.ConnectError(
|
|
4060
|
+
"setup_challenge_expired_or_invalid",
|
|
4061
|
+
"This Haven setup token is expired or invalid \u2014 tokens are single-use and expire 30 minutes after the dashboard issues them, and a mistyped token reads the same way. Return to Haven, start a fresh connection, and run its new Connect command. No local credentials were written.",
|
|
4062
|
+
"return_to_haven_for_fresh_setup"
|
|
4063
|
+
);
|
|
4064
|
+
}
|
|
3438
4065
|
function isExpiredSetupChallenge(err) {
|
|
3439
4066
|
return err instanceof Error && /(?:setup )?challenge.*expir|expir.*(?:setup )?challenge/i.test(err.message);
|
|
3440
4067
|
}
|
|
3441
4068
|
function secureLogger(log, redactPaths = false) {
|
|
3442
4069
|
return (message) => {
|
|
3443
|
-
|
|
3444
|
-
if (redactPaths) {
|
|
3445
|
-
safe = safe.replace(/(?:~|\/)[^\s`"']*\/(?:identity|signer|agent)\.json\b/g, "[credential-file-redacted]").replace(/(?:~|\/)[^\s`"']*\/\.env\b/g, "[credential-env-redacted]");
|
|
3446
|
-
}
|
|
3447
|
-
log(safe);
|
|
4070
|
+
log(redactPaths ? redactForAutomation(message) : redactSecrets(message));
|
|
3448
4071
|
};
|
|
3449
4072
|
}
|
|
3450
4073
|
function printRuntimeInstall(result, log) {
|
|
@@ -3612,6 +4235,8 @@ function parseArgs(argv, env = process.env) {
|
|
|
3612
4235
|
let json = false;
|
|
3613
4236
|
let doctor = false;
|
|
3614
4237
|
let repair = false;
|
|
4238
|
+
let rekeyPhase;
|
|
4239
|
+
let newApiKey;
|
|
3615
4240
|
let tombstoneDir;
|
|
3616
4241
|
let tombstoneReason;
|
|
3617
4242
|
let tombstoneReplacedBy;
|
|
@@ -3625,6 +4250,12 @@ function parseArgs(argv, env = process.env) {
|
|
|
3625
4250
|
doctor = true;
|
|
3626
4251
|
} else if (arg === "--repair") {
|
|
3627
4252
|
repair = true;
|
|
4253
|
+
} else if (arg === "--rekey") {
|
|
4254
|
+
rekeyPhase = "start";
|
|
4255
|
+
} else if (arg === "--rekey-finish") {
|
|
4256
|
+
rekeyPhase = "finish";
|
|
4257
|
+
} else if (arg === "--api-key") {
|
|
4258
|
+
newApiKey = requireValue(argv, ++i, arg);
|
|
3628
4259
|
} else if (arg === "--tombstone") {
|
|
3629
4260
|
tombstoneDir = requireValue(argv, ++i, arg);
|
|
3630
4261
|
} else if (arg === "--reason") {
|
|
@@ -3662,20 +4293,38 @@ function parseArgs(argv, env = process.env) {
|
|
|
3662
4293
|
}
|
|
3663
4294
|
}
|
|
3664
4295
|
const tombstone = tombstoneDir ? { directory: tombstoneDir, reason: tombstoneReason, replacedBy: tombstoneReplacedBy } : void 0;
|
|
4296
|
+
const rekey = rekeyPhase ? { phase: rekeyPhase, newApiKey } : void 0;
|
|
3665
4297
|
if (help) {
|
|
3666
|
-
return { options, help, json, doctor, repair, tombstone };
|
|
4298
|
+
return { options, help, json, doctor, repair, tombstone, rekey };
|
|
4299
|
+
}
|
|
4300
|
+
if (rekey) {
|
|
4301
|
+
if (options.setupToken) {
|
|
4302
|
+
throw new Error("--rekey replaces an existing agent's key; it does not take --setup. Drop one of them.");
|
|
4303
|
+
}
|
|
4304
|
+
if (rekey.phase === "start" && newApiKey !== void 0) {
|
|
4305
|
+
throw new Error(
|
|
4306
|
+
"--api-key belongs to --rekey-finish. --rekey generates the new key here and prints the address to paste into Haven; the API key does not exist yet."
|
|
4307
|
+
);
|
|
4308
|
+
}
|
|
4309
|
+
if (rekey.phase === "finish" && !newApiKey) {
|
|
4310
|
+
throw new Error("--rekey-finish needs --api-key <key> \u2014 the one the Haven agent page showed once.");
|
|
4311
|
+
}
|
|
4312
|
+
return { options, help, json, doctor, repair, tombstone, rekey };
|
|
4313
|
+
}
|
|
4314
|
+
if (newApiKey !== void 0) {
|
|
4315
|
+
throw new Error("--api-key requires --rekey-finish.");
|
|
3667
4316
|
}
|
|
3668
4317
|
if (!tombstoneDir && (tombstoneReason !== void 0 || tombstoneReplacedBy !== void 0)) {
|
|
3669
4318
|
throw new Error("--reason and --replaced-by require --tombstone <dir>.");
|
|
3670
4319
|
}
|
|
3671
4320
|
if (tombstone) {
|
|
3672
|
-
return { options, help, json, doctor, repair, tombstone };
|
|
4321
|
+
return { options, help, json, doctor, repair, tombstone, rekey };
|
|
3673
4322
|
}
|
|
3674
4323
|
if (doctor || repair) {
|
|
3675
4324
|
if (!options.runtime) {
|
|
3676
4325
|
throw new Error("--doctor/--repair need --runtime <runtime> (which config to examine).");
|
|
3677
4326
|
}
|
|
3678
|
-
return { options, help, json, doctor, repair, tombstone };
|
|
4327
|
+
return { options, help, json, doctor, repair, tombstone, rekey };
|
|
3679
4328
|
}
|
|
3680
4329
|
if (!options.setupToken) {
|
|
3681
4330
|
throw new Error("Missing --setup <hv_setup_...> setup token.");
|
|
@@ -3684,7 +4333,7 @@ function parseArgs(argv, env = process.env) {
|
|
|
3684
4333
|
throw new Error("Missing --api <Haven API URL>.");
|
|
3685
4334
|
}
|
|
3686
4335
|
options.apiBaseUrl = options.apiBaseUrl.replace(/\/+$/, "");
|
|
3687
|
-
return { options, help, json, doctor, repair, tombstone };
|
|
4336
|
+
return { options, help, json, doctor, repair, tombstone, rekey };
|
|
3688
4337
|
}
|
|
3689
4338
|
function helpText() {
|
|
3690
4339
|
return [
|
|
@@ -3721,6 +4370,14 @@ function helpText() {
|
|
|
3721
4370
|
" --repair Repair, then re-diagnose (implies --doctor): reinstall the pinned signer",
|
|
3722
4371
|
" runtime, rewrite the wrapper and runtime config from stored credentials.",
|
|
3723
4372
|
" Hosted topology only (refuses to touch a --local config). No keys, no token.",
|
|
4373
|
+
" --rekey Replace this agent's signing key (no token). Generates a fresh keypair HERE and",
|
|
4374
|
+
" prints its public address to paste into the Haven agent page. Nothing changes",
|
|
4375
|
+
" until you finish; the agent keeps working on its old key throughout.",
|
|
4376
|
+
" Add --name <slug> for a named agent. Refuses a legacy-rail or revoked agent.",
|
|
4377
|
+
" --rekey-finish Second half of --rekey: writes the new key and the API key the agent page",
|
|
4378
|
+
" showed once, in place at the same path, and rewrites only this agent's MCP",
|
|
4379
|
+
" config pair. Server names do not change, so wired hosts need only a restart.",
|
|
4380
|
+
" --api-key <key> The new API key, for --rekey-finish.",
|
|
3724
4381
|
" --tombstone <dir> Retire an agent credential directory in place (no token): replaces its signer",
|
|
3725
4382
|
" wrapper with a diagnostic that names the retirement in MCP stderr logs, and",
|
|
3726
4383
|
" writes TOMBSTONE.json. Touches NO key material and revokes nothing.",
|
|
@@ -3740,6 +4397,8 @@ function requireValue(argv, index, option) {
|
|
|
3740
4397
|
}
|
|
3741
4398
|
|
|
3742
4399
|
// src/index.ts
|
|
4400
|
+
init_api();
|
|
4401
|
+
init_key();
|
|
3743
4402
|
init_redact();
|
|
3744
4403
|
init_redact();
|
|
3745
4404
|
async function runCli(argv, io = {
|
|
@@ -3752,6 +4411,8 @@ async function runCli(argv, io = {
|
|
|
3752
4411
|
parsed = parseArgs(argv);
|
|
3753
4412
|
} catch (err) {
|
|
3754
4413
|
if (wantsJson) {
|
|
4414
|
+
io.stderr(`${redactForAutomation(err instanceof Error ? err.message : String(err))}
|
|
4415
|
+
`);
|
|
3755
4416
|
io.stdout(`${JSON.stringify(failedConnectOutcome(void 0, err))}
|
|
3756
4417
|
`);
|
|
3757
4418
|
} else {
|
|
@@ -3767,13 +4428,13 @@ async function runCli(argv, io = {
|
|
|
3767
4428
|
}
|
|
3768
4429
|
if (parsed.tombstone) {
|
|
3769
4430
|
const { writeAgentTombstone: writeAgentTombstone2 } = await Promise.resolve().then(() => (init_tombstone(), tombstone_exports));
|
|
3770
|
-
const { readFile:
|
|
4431
|
+
const { readFile: readFile12 } = await import('fs/promises');
|
|
3771
4432
|
const { join: join10 } = await import('path');
|
|
3772
4433
|
try {
|
|
3773
4434
|
let agentId = "unknown";
|
|
3774
4435
|
try {
|
|
3775
4436
|
const identity = JSON.parse(
|
|
3776
|
-
await
|
|
4437
|
+
await readFile12(join10(parsed.tombstone.directory, "identity.json"), "utf8")
|
|
3777
4438
|
);
|
|
3778
4439
|
agentId = identity.agent_id ?? "unknown";
|
|
3779
4440
|
} catch {
|
|
@@ -3800,6 +4461,64 @@ async function runCli(argv, io = {
|
|
|
3800
4461
|
return 0;
|
|
3801
4462
|
} catch (err) {
|
|
3802
4463
|
io.stderr(`${redactSecrets(err instanceof Error ? err.message : String(err))}
|
|
4464
|
+
`);
|
|
4465
|
+
return 1;
|
|
4466
|
+
}
|
|
4467
|
+
}
|
|
4468
|
+
if (parsed.rekey) {
|
|
4469
|
+
const { startRekey: startRekey2, finishRekey: finishRekey2 } = await Promise.resolve().then(() => (init_rekey(), rekey_exports));
|
|
4470
|
+
const { restartGuidance: restartGuidance2 } = await Promise.resolve().then(() => (init_rekey_restart(), rekey_restart_exports));
|
|
4471
|
+
const common = {
|
|
4472
|
+
serverName: parsed.options.serverName,
|
|
4473
|
+
credentialsDir: parsed.options.credentialsDir,
|
|
4474
|
+
runtime: parsed.options.runtime
|
|
4475
|
+
};
|
|
4476
|
+
try {
|
|
4477
|
+
if (parsed.rekey.phase === "start") {
|
|
4478
|
+
const result2 = await startRekey2(common);
|
|
4479
|
+
if (parsed.json) {
|
|
4480
|
+
io.stdout(
|
|
4481
|
+
`${redactSecrets(
|
|
4482
|
+
JSON.stringify({
|
|
4483
|
+
rekey: "started",
|
|
4484
|
+
agent_id: result2.agentId,
|
|
4485
|
+
new_delegate_address: result2.newDelegateAddress,
|
|
4486
|
+
expires_at: result2.expiresAt
|
|
4487
|
+
})
|
|
4488
|
+
)}
|
|
4489
|
+
`
|
|
4490
|
+
);
|
|
4491
|
+
} else {
|
|
4492
|
+
for (const line of result2.messages) io.stdout(redactSecrets(`${line}
|
|
4493
|
+
`));
|
|
4494
|
+
}
|
|
4495
|
+
return 0;
|
|
4496
|
+
}
|
|
4497
|
+
const result = await finishRekey2({ ...common, newApiKey: parsed.rekey.newApiKey });
|
|
4498
|
+
const restart = restartGuidance2(parsed.options.runtime);
|
|
4499
|
+
if (parsed.json) {
|
|
4500
|
+
io.stdout(
|
|
4501
|
+
`${redactSecrets(
|
|
4502
|
+
JSON.stringify({
|
|
4503
|
+
rekey: "finished",
|
|
4504
|
+
agent_id: result.agentId,
|
|
4505
|
+
new_delegate_address: result.newDelegateAddress,
|
|
4506
|
+
mcp_servers: result.serverNames,
|
|
4507
|
+
restart_commands: restart.commands
|
|
4508
|
+
})
|
|
4509
|
+
)}
|
|
4510
|
+
`
|
|
4511
|
+
);
|
|
4512
|
+
} else {
|
|
4513
|
+
for (const line of result.messages) io.stdout(redactSecrets(`${line}
|
|
4514
|
+
`));
|
|
4515
|
+
io.stdout("\n");
|
|
4516
|
+
for (const line of restart.lines) io.stdout(redactSecrets(`${line}
|
|
4517
|
+
`));
|
|
4518
|
+
}
|
|
4519
|
+
return 0;
|
|
4520
|
+
} catch (err) {
|
|
4521
|
+
io.stderr(`${redactSecrets(err instanceof Error ? err.message : String(err))}
|
|
3803
4522
|
`);
|
|
3804
4523
|
return 1;
|
|
3805
4524
|
}
|
|
@@ -3832,7 +4551,7 @@ async function runCli(argv, io = {
|
|
|
3832
4551
|
for (const agent of otherAgents) {
|
|
3833
4552
|
const name = agent.slug ? `${agent.slug} (${agent.agentId ?? "unknown"})` : agent.agentId ?? "unknown";
|
|
3834
4553
|
const failed = agent.checks.filter((check) => !check.ok);
|
|
3835
|
-
const verdict = agent.classification === "wired" ? failed.length === 0 ? "wired, all checks passed" : `wired, ${failed.length} check(s) FAILED` : agent.classification;
|
|
4554
|
+
const verdict = agent.classification === "wired" ? failed.length === 0 ? "wired, all checks passed" : `wired, ${failed.length} check(s) FAILED` : agent.classification === "parked" ? "parked re-key only \u2014 no identity.json in this directory, but key material is still there" : agent.classification;
|
|
3836
4555
|
io.stdout(redactSecrets(` ${failed.length > 0 ? "\u2717" : "\u2022"} ${name}: ${verdict}
|
|
3837
4556
|
`));
|
|
3838
4557
|
for (const check of failed) {
|
|
@@ -3874,6 +4593,8 @@ async function runCli(argv, io = {
|
|
|
3874
4593
|
return 0;
|
|
3875
4594
|
} catch (err) {
|
|
3876
4595
|
if (parsed.json) {
|
|
4596
|
+
io.stderr(`${redactForAutomation(err instanceof Error ? err.message : String(err))}
|
|
4597
|
+
`);
|
|
3877
4598
|
io.stdout(`${JSON.stringify(failedConnectOutcome(parsed.options.runtime, err))}
|
|
3878
4599
|
`);
|
|
3879
4600
|
} else {
|
|
@@ -3898,9 +4619,11 @@ function isCliEntrypoint(argvPath = process.argv[1], moduleUrl = (typeof documen
|
|
|
3898
4619
|
if (isCliEntrypoint()) void main();
|
|
3899
4620
|
|
|
3900
4621
|
// src/index.ts
|
|
4622
|
+
init_runtime_install();
|
|
3901
4623
|
init_signer_runtime();
|
|
3902
4624
|
init_runtime_registry();
|
|
3903
4625
|
init_connect_error();
|
|
4626
|
+
init_storage();
|
|
3904
4627
|
init_runtime_manifest();
|
|
3905
4628
|
|
|
3906
4629
|
exports.CONNECTOR_VERSION = CONNECTOR_VERSION;
|