@haven_ai/connect 0.1.28-alpha.0 → 0.1.30-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 +160 -4
- package/dist/cli.cjs +2823 -1345
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +2826 -1348
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +2839 -1345
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +241 -5
- package/dist/index.d.ts +241 -5
- package/dist/index.js +2839 -1350
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
import { Wallet } from 'ethers';
|
|
3
|
+
import { mkdir, rm, stat, readdir, readFile, chmod, access, writeFile, unlink, rename } from 'fs/promises';
|
|
4
|
+
import { homedir, platform } from 'os';
|
|
5
|
+
import { join, resolve, dirname, basename } from 'path';
|
|
1
6
|
import { ensureConsent, computeConsentHash, loadCredentials, consentInputFromClient, registeredToolNames, MCP_VERSION } from '@haven_ai/mcp';
|
|
2
7
|
import { ensureSignerConsent, computeSignerConsentHash, loadSignerCredentials, createEdgeSigner, toolSchemas } from '@haven_ai/signer';
|
|
3
|
-
import { isSupportedNodeVersion,
|
|
4
|
-
import { mkdir, rm, chmod, access, writeFile, readFile, unlink, readdir, stat } from 'fs/promises';
|
|
5
|
-
import { homedir, platform } from 'os';
|
|
6
|
-
import { join, resolve, dirname } from 'path';
|
|
8
|
+
import { isSupportedNodeVersion, SKILL_FOLDER_NAME, resolveTokenFromAddress, HAVEN_SKILL_MD, HAVEN_SKILL_BODY_MD, HAVEN_MINIMUM_NODE_VERSION, unsupportedNodeVersionMessage } from '@haven_ai/sdk';
|
|
7
9
|
import { parseDocument, isMap, stringify } from 'yaml';
|
|
8
|
-
import {
|
|
10
|
+
import { spawn, execFile } from 'child_process';
|
|
9
11
|
import { promisify } from 'util';
|
|
10
|
-
import
|
|
11
|
-
import { Wallet } from 'ethers';
|
|
12
|
+
import { createInterface } from 'readline';
|
|
12
13
|
import { realpathSync } from 'fs';
|
|
13
14
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
14
15
|
|
|
@@ -21,125 +22,605 @@ var __export = (target, all) => {
|
|
|
21
22
|
for (var name in all)
|
|
22
23
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
23
24
|
};
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
return
|
|
25
|
+
|
|
26
|
+
// src/api.ts
|
|
27
|
+
function createConnectApiClient(baseUrl, fetchImpl = fetch) {
|
|
28
|
+
const root = baseUrl.replace(/\/+$/, "");
|
|
29
|
+
return {
|
|
30
|
+
resolveSetup: (input) => request(fetchImpl, `${root}/agent-connection-setups/resolve`, {
|
|
31
|
+
method: "POST",
|
|
32
|
+
body: JSON.stringify({
|
|
33
|
+
setup_token: input.setupToken,
|
|
34
|
+
connector_version: input.connectorVersion,
|
|
35
|
+
runtime: input.runtime
|
|
36
|
+
})
|
|
37
|
+
}),
|
|
38
|
+
registerSetup: (input) => request(fetchImpl, `${root}/agent-connection-setups/register`, {
|
|
39
|
+
method: "POST",
|
|
40
|
+
body: JSON.stringify({
|
|
41
|
+
setup_token: input.setupToken,
|
|
42
|
+
challenge_id: input.challengeId,
|
|
43
|
+
delegate_address: input.delegateAddress,
|
|
44
|
+
proof_signature: input.proofSignature,
|
|
45
|
+
api_key_hash: input.apiKeyHash,
|
|
46
|
+
api_key_prefix: input.apiKeyPrefix,
|
|
47
|
+
runtime: input.runtime,
|
|
48
|
+
connector_version: input.connectorVersion,
|
|
49
|
+
mcp_server_name: input.mcpServerName,
|
|
50
|
+
connector_context: input.connectorContext,
|
|
51
|
+
install_capabilities: input.installCapabilities && {
|
|
52
|
+
can_write_runtime_config: input.installCapabilities.canWriteRuntimeConfig,
|
|
53
|
+
restart_required: input.installCapabilities.restartRequired
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
}),
|
|
57
|
+
getAgentIdentity: (apiKey) => request(fetchImpl, `${root}/machine-payments/agent`, {
|
|
58
|
+
method: "GET",
|
|
59
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
60
|
+
}),
|
|
61
|
+
getConnectorStatus: (setupId, apiKey) => request(fetchImpl, `${root}/agent-connection-setups/${encodeURIComponent(setupId)}/connector-status`, {
|
|
62
|
+
method: "GET",
|
|
63
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
64
|
+
}),
|
|
65
|
+
updateInstallStatus: async (setupId, apiKey, input) => {
|
|
66
|
+
await request(fetchImpl, `${root}/agent-connection-setups/${encodeURIComponent(setupId)}/install-status`, {
|
|
67
|
+
method: "POST",
|
|
68
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
69
|
+
body: JSON.stringify({
|
|
70
|
+
runtime: input.runtime,
|
|
71
|
+
connector_version: input.connectorVersion,
|
|
72
|
+
runtime_mcp_mode: input.runtimeMcpMode,
|
|
73
|
+
hosted_mcp_configured: input.hostedMcpConfigured,
|
|
74
|
+
local_signer_configured: input.localSignerConfigured,
|
|
75
|
+
local_mcp_configured: input.localMcpConfigured,
|
|
76
|
+
credential_files_written: input.credentialFilesWritten,
|
|
77
|
+
signer_acknowledged: input.signerAcknowledged,
|
|
78
|
+
local_mcp_acknowledged: input.localMcpAcknowledged,
|
|
79
|
+
activation_command_available: input.activationCommandAvailable,
|
|
80
|
+
skill_installed: input.skillInstalled,
|
|
81
|
+
probe_result: input.probeResult,
|
|
82
|
+
restart_required: input.restartRequired,
|
|
83
|
+
next_user_action: input.nextUserAction,
|
|
84
|
+
error_code: input.errorCode ?? null,
|
|
85
|
+
environment_label: input.environmentLabel
|
|
86
|
+
})
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
};
|
|
29
90
|
}
|
|
30
|
-
function
|
|
31
|
-
|
|
91
|
+
async function request(fetchImpl, url, init) {
|
|
92
|
+
const response = await fetchImpl(url, {
|
|
93
|
+
...init,
|
|
94
|
+
headers: {
|
|
95
|
+
"Content-Type": "application/json",
|
|
96
|
+
...init.headers ?? {}
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
const text = await response.text();
|
|
100
|
+
const body = text ? JSON.parse(text) : null;
|
|
101
|
+
if (!response.ok) {
|
|
102
|
+
const message = body?.error ?? body?.message ?? `${response.status} ${response.statusText}`;
|
|
103
|
+
throw new ConnectRequestError(`Haven setup request failed: ${message}`, response.status);
|
|
104
|
+
}
|
|
105
|
+
return body;
|
|
32
106
|
}
|
|
33
|
-
var
|
|
34
|
-
var
|
|
35
|
-
"src/
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
// Sourced from the SDK, never a literal (#1161). This field read '20.0.0'
|
|
44
|
-
// while every package's `engines` said `>=24` and the docs said `>=24.0.0`,
|
|
45
|
-
// so the guard that was supposed to enforce the floor waved Node v23 through
|
|
46
|
-
// — including on the `--local` path where it does run. A hand-maintained
|
|
47
|
-
// second copy of a number is a drift waiting to happen; a guard test pins
|
|
48
|
-
// this against `package.json`'s `engines.node`.
|
|
49
|
-
minimumNodeVersion: HAVEN_MINIMUM_NODE_VERSION,
|
|
50
|
-
supportedClients: ["codex-cli", "codex-desktop", "claude-code"],
|
|
51
|
-
requiredTools: registeredToolNames(),
|
|
52
|
-
/**
|
|
53
|
-
* The signer MCP's tool surface, DERIVED from the pinned @haven_ai/signer
|
|
54
|
-
* package (#1587) — same anti-drift rule as `requiredTools` above: a
|
|
55
|
-
* literal list here would rot the first time the signer gains a tool.
|
|
56
|
-
* The handshake probe requires all of them.
|
|
57
|
-
*/
|
|
58
|
-
requiredSignerTools: Object.keys(toolSchemas)
|
|
107
|
+
var ConnectRequestError;
|
|
108
|
+
var init_api = __esm({
|
|
109
|
+
"src/api.ts"() {
|
|
110
|
+
ConnectRequestError = class extends Error {
|
|
111
|
+
constructor(message, status) {
|
|
112
|
+
super(message);
|
|
113
|
+
this.status = status;
|
|
114
|
+
this.name = "ConnectRequestError";
|
|
115
|
+
}
|
|
116
|
+
status;
|
|
59
117
|
};
|
|
60
118
|
}
|
|
61
119
|
});
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
changed: false,
|
|
85
|
-
restartRequired: true,
|
|
86
|
-
messages: ["Runtime config needs to be added manually for this agent environment."],
|
|
87
|
-
errorCode: "manual_runtime_setup_required"
|
|
88
|
-
};
|
|
120
|
+
function generateDelegateKey() {
|
|
121
|
+
return delegateKeyFromPrivateKey(Wallet.createRandom().privateKey);
|
|
122
|
+
}
|
|
123
|
+
function delegateKeyFromPrivateKey(privateKey) {
|
|
124
|
+
const wallet = new Wallet(privateKey);
|
|
125
|
+
return {
|
|
126
|
+
privateKey: wallet.privateKey,
|
|
127
|
+
address: wallet.address,
|
|
128
|
+
signChallenge: (message) => wallet.signMessage(message)
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function generateAgentApiKey() {
|
|
132
|
+
return `sk_agent_${crypto.randomBytes(24).toString("hex")}`;
|
|
133
|
+
}
|
|
134
|
+
function hashAgentApiKey(apiKey) {
|
|
135
|
+
return crypto.createHash("sha256").update(apiKey).digest("hex");
|
|
136
|
+
}
|
|
137
|
+
function agentApiKeyPrefix(apiKey) {
|
|
138
|
+
return apiKey.slice(0, 12);
|
|
139
|
+
}
|
|
140
|
+
var init_key = __esm({
|
|
141
|
+
"src/key.ts"() {
|
|
89
142
|
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// src/redact.ts
|
|
146
|
+
function redactSecrets(value) {
|
|
147
|
+
return value.replace(API_KEY_RE, "sk_agent_[redacted]").replace(PRIVATE_KEY_RE, "0x[redacted-private-key]");
|
|
90
148
|
}
|
|
91
|
-
function
|
|
92
|
-
if (
|
|
149
|
+
function shortAddress(address) {
|
|
150
|
+
if (!/^0x[0-9a-fA-F]{40}$/.test(address)) return address;
|
|
151
|
+
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
|
152
|
+
}
|
|
153
|
+
var API_KEY_RE, PRIVATE_KEY_RE;
|
|
154
|
+
var init_redact = __esm({
|
|
155
|
+
"src/redact.ts"() {
|
|
156
|
+
API_KEY_RE = /sk_agent_[A-Za-z0-9]+/g;
|
|
157
|
+
PRIVATE_KEY_RE = /0x[0-9a-fA-F]{64}/g;
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// src/server-names.ts
|
|
162
|
+
function assertValidServerSlug(slug) {
|
|
163
|
+
if (slug.length === 0 || slug.length > 32 || !SLUG_RE.test(slug)) {
|
|
164
|
+
throw new Error(
|
|
165
|
+
`Invalid server name ${JSON.stringify(slug)}: use 1-32 lowercase letters, digits, and single hyphens (e.g. "research").`
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
if (slug === "haven" || slug === "haven-signer") {
|
|
169
|
+
throw new Error(
|
|
170
|
+
`Invalid server name ${JSON.stringify(slug)}: "haven" and "haven-signer" are the unnamed pair's own names \u2014 omit --name for the bare pair.`
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
if (slug === "signer" || slug.startsWith("signer-")) {
|
|
174
|
+
throw new Error(
|
|
175
|
+
`Invalid server name ${JSON.stringify(slug)}: "signer" and "signer-*" are reserved \u2014 they would collide with another pair's haven-signer-* entry.`
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function serverNamesFor(slug) {
|
|
180
|
+
if (slug === void 0) {
|
|
93
181
|
return {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
182
|
+
hosted: "haven",
|
|
183
|
+
signer: "haven-signer",
|
|
184
|
+
// Historical Codex table names — every wired Codex host has these.
|
|
185
|
+
codexHosted: "haven",
|
|
186
|
+
codexSigner: "haven_signer",
|
|
187
|
+
hermesEnvKey: "MCP_HAVEN_API_KEY"
|
|
97
188
|
};
|
|
98
189
|
}
|
|
190
|
+
assertValidServerSlug(slug);
|
|
99
191
|
return {
|
|
100
|
-
|
|
101
|
-
|
|
192
|
+
hosted: `haven-${slug}`,
|
|
193
|
+
signer: `haven-signer-${slug}`,
|
|
194
|
+
// Hyphens are valid TOML bare keys, so named Codex tables match the
|
|
195
|
+
// JSON/YAML names instead of inheriting the legacy underscore.
|
|
196
|
+
codexHosted: `haven-${slug}`,
|
|
197
|
+
codexSigner: `haven-signer-${slug}`,
|
|
198
|
+
hermesEnvKey: `MCP_HAVEN_${slug.toUpperCase().replace(/-/g, "_")}_API_KEY`
|
|
102
199
|
};
|
|
103
200
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
201
|
+
var SLUG_RE;
|
|
202
|
+
var init_server_names = __esm({
|
|
203
|
+
"src/server-names.ts"() {
|
|
204
|
+
SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
async function preflightCredentialStorage(input = {}) {
|
|
208
|
+
const directory = defaultCredentialRoot(input.baseDir);
|
|
209
|
+
await mkdir(directory, { recursive: true, mode: 448 });
|
|
210
|
+
await restrictPermissions(directory, 448, input.warn);
|
|
211
|
+
const probePath = join(directory, `.haven-connect-preflight-${crypto.randomBytes(8).toString("hex")}`);
|
|
212
|
+
try {
|
|
213
|
+
await writeOwnerOnlyJson(probePath, { ok: true }, input.warn);
|
|
214
|
+
} finally {
|
|
215
|
+
await rm(probePath, { force: true }).catch(() => void 0);
|
|
216
|
+
}
|
|
217
|
+
return directory;
|
|
218
|
+
}
|
|
219
|
+
async function writeCredentialFiles(input) {
|
|
220
|
+
const directory = defaultAgentDirectory(input.serverName ?? input.agentId, input.baseDir);
|
|
221
|
+
await mkdir(directory, { recursive: true, mode: 448 });
|
|
222
|
+
await restrictPermissions(directory, 448, input.warn);
|
|
223
|
+
const identityPath = join(directory, "identity.json");
|
|
224
|
+
const signerPath = join(directory, "signer.json");
|
|
225
|
+
const agentPath = join(directory, "agent.json");
|
|
226
|
+
await assertDoesNotExist(identityPath);
|
|
227
|
+
await assertDoesNotExist(signerPath);
|
|
228
|
+
await assertDoesNotExist(agentPath);
|
|
229
|
+
await writeOwnerOnlyJson(signerPath, signerPayload(input), input.warn);
|
|
230
|
+
try {
|
|
231
|
+
await writeOwnerOnlyJson(identityPath, identityPayload(input), input.warn);
|
|
232
|
+
} catch (err) {
|
|
233
|
+
await rm(signerPath, { force: true }).catch(() => void 0);
|
|
234
|
+
throw err;
|
|
235
|
+
}
|
|
236
|
+
try {
|
|
237
|
+
await writeOwnerOnlyJson(agentPath, agentPayload(input), input.warn);
|
|
238
|
+
} catch (err) {
|
|
239
|
+
await rm(signerPath, { force: true }).catch(() => void 0);
|
|
240
|
+
await rm(identityPath, { force: true }).catch(() => void 0);
|
|
241
|
+
await rm(agentPath, { force: true }).catch(() => void 0);
|
|
242
|
+
throw err;
|
|
243
|
+
}
|
|
244
|
+
return { directory, identityPath, signerPath, agentPath };
|
|
245
|
+
}
|
|
246
|
+
function signerPayload(input) {
|
|
247
|
+
return {
|
|
248
|
+
delegate_key: input.delegateKey,
|
|
249
|
+
delegate_address: input.delegateAddress,
|
|
250
|
+
agent_id: input.agentId,
|
|
251
|
+
safe_address: input.safeAddress,
|
|
252
|
+
chain_id: input.chainId,
|
|
253
|
+
network: input.network,
|
|
254
|
+
x402_binding_signer: input.x402BindingSigner,
|
|
255
|
+
note: "Local signer credential. Haven backend never receives this private key."
|
|
108
256
|
};
|
|
109
257
|
}
|
|
110
|
-
function
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
258
|
+
function identityPayload(input) {
|
|
259
|
+
return {
|
|
260
|
+
api_key: input.apiKey,
|
|
261
|
+
agent_id: input.agentId,
|
|
262
|
+
safe_address: input.safeAddress,
|
|
263
|
+
chain_id: input.chainId,
|
|
264
|
+
network: input.network,
|
|
265
|
+
api_url: input.apiUrl,
|
|
266
|
+
hosted_mcp_url: input.hostedMcpUrl,
|
|
267
|
+
agent_budget: input.agentBudget,
|
|
268
|
+
note: "Haven API key identifies the agent only. It cannot spend without the local signer key and on-chain Haven wallet rules."
|
|
114
269
|
};
|
|
115
|
-
if (runtime === "vscode" || runtime === "vscode-insiders") return { type: "stdio", ...server };
|
|
116
|
-
return server;
|
|
117
270
|
}
|
|
118
|
-
function
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
271
|
+
function agentPayload(input) {
|
|
272
|
+
return {
|
|
273
|
+
agent_id: input.agentId,
|
|
274
|
+
delegate_address: input.delegateAddress,
|
|
275
|
+
safe_address: input.safeAddress,
|
|
276
|
+
chain_id: input.chainId,
|
|
277
|
+
network: input.network,
|
|
278
|
+
agent_budget: input.agentBudget,
|
|
279
|
+
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."
|
|
126
280
|
};
|
|
127
|
-
return `${JSON.stringify(config, null, 2)}
|
|
128
|
-
`;
|
|
129
281
|
}
|
|
130
|
-
function
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
282
|
+
async function readStoredCredentials(serverName, agentIdOrSlug, baseDir) {
|
|
283
|
+
const key = serverName ?? agentIdOrSlug;
|
|
284
|
+
const directory = key ? defaultAgentDirectory(key, baseDir) : await discoverSoleAgentDirectory(baseDir);
|
|
285
|
+
const identity = await readJsonFile(join(directory, "identity.json"));
|
|
286
|
+
if (!identity) {
|
|
287
|
+
throw new Error(
|
|
288
|
+
`No Haven credentials at ${directory}. Nothing to re-key \u2014 connect this agent first, or pass the --name you wired it under.`
|
|
289
|
+
);
|
|
135
290
|
}
|
|
136
|
-
const
|
|
137
|
-
const
|
|
138
|
-
const
|
|
291
|
+
const agent = await readJsonFile(join(directory, "agent.json")) ?? {};
|
|
292
|
+
const signer = await readJsonFile(join(directory, "signer.json")) ?? {};
|
|
293
|
+
const agentId = asString(identity.agent_id);
|
|
294
|
+
const apiKey = asString(identity.api_key);
|
|
295
|
+
const apiUrl = asString(identity.api_url);
|
|
296
|
+
const hostedMcpUrl = asString(identity.hosted_mcp_url);
|
|
297
|
+
if (!agentId || !apiKey || !apiUrl || !hostedMcpUrl) {
|
|
298
|
+
throw new Error(
|
|
299
|
+
`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.`
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
directory,
|
|
304
|
+
agentId,
|
|
305
|
+
apiKey,
|
|
306
|
+
apiUrl,
|
|
307
|
+
hostedMcpUrl,
|
|
308
|
+
delegateAddress: asString(agent.delegate_address) ?? asString(signer.delegate_address),
|
|
309
|
+
safeAddress: asString(identity.safe_address) ?? asString(agent.safe_address),
|
|
310
|
+
chainId: typeof identity.chain_id === "number" ? identity.chain_id : void 0,
|
|
311
|
+
network: asString(identity.network),
|
|
312
|
+
x402BindingSigner: asString(signer.x402_binding_signer),
|
|
313
|
+
agentBudget: Array.isArray(identity.agent_budget) ? identity.agent_budget : void 0
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
async function discoverSoleAgentDirectory(baseDir) {
|
|
317
|
+
const root = defaultCredentialRoot(baseDir);
|
|
318
|
+
let entries = [];
|
|
319
|
+
try {
|
|
320
|
+
entries = await readdir(root);
|
|
321
|
+
} catch {
|
|
322
|
+
throw new Error(`No Haven credentials found under ${root}. Connect an agent on this machine first.`);
|
|
323
|
+
}
|
|
324
|
+
const candidates = [];
|
|
325
|
+
for (const entry of entries) {
|
|
326
|
+
const directory = join(root, entry);
|
|
327
|
+
if (!await readJsonFile(join(directory, "identity.json"))) continue;
|
|
328
|
+
if (await readJsonFile(join(directory, "TOMBSTONE.json"))) continue;
|
|
329
|
+
candidates.push(directory);
|
|
330
|
+
}
|
|
331
|
+
if (candidates.length === 1) return candidates[0];
|
|
332
|
+
if (candidates.length === 0) {
|
|
333
|
+
throw new Error(`No Haven credentials found under ${root}. Connect an agent on this machine first.`);
|
|
334
|
+
}
|
|
335
|
+
throw new Error(
|
|
336
|
+
`Several agents are wired on this machine, so --rekey cannot tell which one you mean:
|
|
337
|
+
` + candidates.map((d) => ` ${d}`).join("\n") + "\nRe-run with --name <slug> to pick one."
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
async function rewriteCredentialFiles(input) {
|
|
341
|
+
const directory = defaultAgentDirectory(input.serverName ?? input.agentId, input.baseDir);
|
|
342
|
+
const identityPath = join(directory, "identity.json");
|
|
343
|
+
const signerPath = join(directory, "signer.json");
|
|
344
|
+
const agentPath = join(directory, "agent.json");
|
|
345
|
+
const targets = [
|
|
346
|
+
{ path: signerPath, payload: signerPayload(input) },
|
|
347
|
+
{ path: identityPath, payload: identityPayload(input) },
|
|
348
|
+
{ path: agentPath, payload: agentPayload(input) }
|
|
349
|
+
];
|
|
350
|
+
const originals = /* @__PURE__ */ new Map();
|
|
351
|
+
for (const { path } of targets) {
|
|
352
|
+
originals.set(path, await readRawFile(path));
|
|
353
|
+
}
|
|
354
|
+
const temps = [];
|
|
355
|
+
try {
|
|
356
|
+
for (const { path, payload } of targets) {
|
|
357
|
+
const temp = `${path}.rekey-${crypto.randomBytes(6).toString("hex")}.tmp`;
|
|
358
|
+
await writeOwnerOnlyJson(temp, payload, input.warn);
|
|
359
|
+
temps.push({ from: temp, to: path });
|
|
360
|
+
}
|
|
361
|
+
for (const { from, to } of temps) {
|
|
362
|
+
await rename(from, to);
|
|
363
|
+
}
|
|
364
|
+
} catch (err) {
|
|
365
|
+
for (const { from } of temps) await rm(from, { force: true }).catch(() => void 0);
|
|
366
|
+
for (const [path, contents] of originals) {
|
|
367
|
+
if (contents === null) {
|
|
368
|
+
await rm(path, { force: true }).catch(() => void 0);
|
|
369
|
+
} else {
|
|
370
|
+
await writeFile(path, contents, { mode: 384 }).catch(() => void 0);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
throw err;
|
|
374
|
+
}
|
|
375
|
+
return { directory, identityPath, signerPath, agentPath };
|
|
376
|
+
}
|
|
377
|
+
async function writeRekeyPending(directory, pending, warn) {
|
|
378
|
+
const path = join(directory, REKEY_PENDING_FILENAME);
|
|
379
|
+
await rm(path, { force: true }).catch(() => void 0);
|
|
380
|
+
await writeOwnerOnlyJson(path, { ...pending }, warn);
|
|
381
|
+
return path;
|
|
382
|
+
}
|
|
383
|
+
async function readRekeyPending(directory, now = Date.now()) {
|
|
384
|
+
const path = join(directory, REKEY_PENDING_FILENAME);
|
|
385
|
+
const raw = await readJsonFile(path);
|
|
386
|
+
if (!raw) {
|
|
387
|
+
throw new Error(
|
|
388
|
+
`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.`
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
const pending = raw;
|
|
392
|
+
if (!pending.new_delegate_key || !pending.new_delegate_address || !pending.agent_id) {
|
|
393
|
+
throw new Error(`The pending re-key at ${path} is unreadable. Delete it and start again with --rekey.`);
|
|
394
|
+
}
|
|
395
|
+
if (pending.expires_at && Date.parse(pending.expires_at) < now) {
|
|
396
|
+
throw new Error(
|
|
397
|
+
`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.`
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
return pending;
|
|
401
|
+
}
|
|
402
|
+
async function inspectRekeyPending(directory, now = Date.now()) {
|
|
403
|
+
const path = join(directory, REKEY_PENDING_FILENAME);
|
|
404
|
+
const raw = await readJsonFile(path);
|
|
405
|
+
if (!raw) {
|
|
406
|
+
const present = await readRawFile(path) !== null;
|
|
407
|
+
return present ? { state: "unreadable", path } : null;
|
|
408
|
+
}
|
|
409
|
+
const agentId = asString(raw.agent_id);
|
|
410
|
+
const newDelegateAddress = asString(raw.new_delegate_address);
|
|
411
|
+
const startedAt = asString(raw.started_at);
|
|
412
|
+
const expiresAt = asString(raw.expires_at);
|
|
413
|
+
if (!agentId || !newDelegateAddress) {
|
|
414
|
+
return { state: "unreadable", path, ...startedAt ? { startedAt } : {} };
|
|
415
|
+
}
|
|
416
|
+
const expired = expiresAt !== void 0 && Date.parse(expiresAt) < now;
|
|
417
|
+
return {
|
|
418
|
+
state: expired ? "expired" : "pending",
|
|
419
|
+
path,
|
|
420
|
+
agentId,
|
|
421
|
+
newDelegateAddress,
|
|
422
|
+
...startedAt ? { startedAt } : {},
|
|
423
|
+
...expiresAt ? { expiresAt } : {}
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
async function clearRekeyPending(directory) {
|
|
427
|
+
await rm(join(directory, REKEY_PENDING_FILENAME), { force: true }).catch(() => void 0);
|
|
428
|
+
}
|
|
429
|
+
async function readJsonFile(path) {
|
|
430
|
+
const raw = await readRawFile(path);
|
|
431
|
+
if (raw === null) return null;
|
|
432
|
+
try {
|
|
433
|
+
const parsed = JSON.parse(raw);
|
|
434
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
435
|
+
} catch {
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
async function readRawFile(path) {
|
|
440
|
+
try {
|
|
441
|
+
return await readFile(path, "utf8");
|
|
442
|
+
} catch {
|
|
443
|
+
return null;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
function asString(value) {
|
|
447
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
448
|
+
}
|
|
449
|
+
async function assertServerSlugAvailable(serverName, baseDir) {
|
|
450
|
+
const directory = defaultAgentDirectory(serverName, baseDir);
|
|
451
|
+
try {
|
|
452
|
+
await stat(join(directory, "identity.json"));
|
|
453
|
+
} catch {
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
throw new Error(
|
|
457
|
+
`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.`
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
function defaultAgentDirectory(agentId, baseDir = join(homedir(), ".haven", "agents")) {
|
|
461
|
+
return resolve(defaultCredentialRoot(baseDir), safePathPart(agentId));
|
|
462
|
+
}
|
|
463
|
+
function defaultCredentialRoot(baseDir = join(homedir(), ".haven", "agents")) {
|
|
464
|
+
return resolve(baseDir);
|
|
465
|
+
}
|
|
466
|
+
async function writeOwnerOnlyJson(path, value, warn) {
|
|
467
|
+
const json = JSON.stringify(dropUndefined(value), null, 2);
|
|
468
|
+
await writeFile(path, `${json}
|
|
469
|
+
`, { mode: 384, flag: "wx" });
|
|
470
|
+
await restrictPermissions(path, 384, warn);
|
|
471
|
+
}
|
|
472
|
+
function safePathPart(value) {
|
|
473
|
+
return value.replace(/[^A-Za-z0-9_.-]/g, "_");
|
|
474
|
+
}
|
|
475
|
+
function dropUndefined(value) {
|
|
476
|
+
return Object.fromEntries(Object.entries(value).filter(([, field]) => field !== void 0));
|
|
477
|
+
}
|
|
478
|
+
async function assertDoesNotExist(path) {
|
|
479
|
+
try {
|
|
480
|
+
await access(path);
|
|
481
|
+
} catch (err) {
|
|
482
|
+
if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") return;
|
|
483
|
+
throw err;
|
|
484
|
+
}
|
|
485
|
+
throw new Error(`Refusing to overwrite existing Haven credential file: ${path}`);
|
|
486
|
+
}
|
|
487
|
+
async function restrictPermissions(path, mode, warn) {
|
|
488
|
+
try {
|
|
489
|
+
await chmod(path, mode);
|
|
490
|
+
} catch (err) {
|
|
491
|
+
warn?.(
|
|
492
|
+
`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)}`
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
var REKEY_PENDING_FILENAME, REKEY_PENDING_TTL_MS;
|
|
497
|
+
var init_storage = __esm({
|
|
498
|
+
"src/storage.ts"() {
|
|
499
|
+
REKEY_PENDING_FILENAME = "rekey-pending.json";
|
|
500
|
+
REKEY_PENDING_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
function mcpPackageSpec() {
|
|
504
|
+
return `${MCP_RUNTIME_MANIFEST.mcpPackage}@${MCP_RUNTIME_MANIFEST.mcpVersion}`;
|
|
505
|
+
}
|
|
506
|
+
function sdkPackageSpec() {
|
|
507
|
+
return `${MCP_RUNTIME_MANIFEST.sdkPackage}@${MCP_RUNTIME_MANIFEST.sdkVersion}`;
|
|
508
|
+
}
|
|
509
|
+
function signerPackageSpec() {
|
|
510
|
+
return `${MCP_RUNTIME_MANIFEST.signerPackage}@${MCP_RUNTIME_MANIFEST.signerVersion}`;
|
|
511
|
+
}
|
|
512
|
+
var MCP_RUNTIME_MANIFEST;
|
|
513
|
+
var init_runtime_manifest = __esm({
|
|
514
|
+
"src/runtime-manifest.ts"() {
|
|
515
|
+
MCP_RUNTIME_MANIFEST = {
|
|
516
|
+
mcpPackage: "@haven_ai/mcp",
|
|
517
|
+
mcpVersion: MCP_VERSION,
|
|
518
|
+
sdkPackage: "@haven_ai/sdk",
|
|
519
|
+
sdkVersion: "0.1.30-alpha.0",
|
|
520
|
+
signerPackage: "@haven_ai/signer",
|
|
521
|
+
signerVersion: "0.1.30-alpha.0",
|
|
522
|
+
// Sourced from the SDK, never a literal (#1161). This field read '20.0.0'
|
|
523
|
+
// while every package's `engines` said `>=24` and the docs said `>=24.0.0`,
|
|
524
|
+
// so the guard that was supposed to enforce the floor waved Node v23 through
|
|
525
|
+
// — including on the `--local` path where it does run. A hand-maintained
|
|
526
|
+
// second copy of a number is a drift waiting to happen; a guard test pins
|
|
527
|
+
// this against `package.json`'s `engines.node`.
|
|
528
|
+
minimumNodeVersion: HAVEN_MINIMUM_NODE_VERSION,
|
|
529
|
+
supportedClients: ["codex-cli", "codex-desktop", "claude-code"],
|
|
530
|
+
requiredTools: registeredToolNames(),
|
|
531
|
+
/**
|
|
532
|
+
* The signer MCP's tool surface, DERIVED from the pinned @haven_ai/signer
|
|
533
|
+
* package (#1587) — same anti-drift rule as `requiredTools` above: a
|
|
534
|
+
* literal list here would rot the first time the signer gains a tool.
|
|
535
|
+
* The handshake probe requires all of them.
|
|
536
|
+
*/
|
|
537
|
+
requiredSignerTools: Object.keys(toolSchemas)
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
});
|
|
541
|
+
async function writeRuntimeConfig(input, deps = {}) {
|
|
542
|
+
switch (input.runtime) {
|
|
543
|
+
case "codex-cli":
|
|
544
|
+
case "codex-desktop":
|
|
545
|
+
return writeCodexConfig(input);
|
|
546
|
+
case "cursor":
|
|
547
|
+
return writeJsonRuntimeConfig(input, cursorConfigPath(input.homeDir), "mcpServers");
|
|
548
|
+
case "vscode":
|
|
549
|
+
return writeJsonRuntimeConfig(input, vscodeConfigPath(input.homeDir), "servers");
|
|
550
|
+
case "vscode-insiders":
|
|
551
|
+
return writeJsonRuntimeConfig(input, vscodeInsidersConfigPath(input.homeDir), "servers");
|
|
552
|
+
case "claude-desktop":
|
|
553
|
+
return writeJsonRuntimeConfig(input, claudeDesktopConfigPath(input.homeDir), "mcpServers");
|
|
554
|
+
case "hermes":
|
|
555
|
+
return writeHermesConfig(input, deps);
|
|
556
|
+
default:
|
|
557
|
+
return {
|
|
558
|
+
hostedConfigured: false,
|
|
559
|
+
signerConfigured: false,
|
|
560
|
+
localMcpConfigured: false,
|
|
561
|
+
runtimeMcpMode: "manual",
|
|
562
|
+
target: "manual runtime setup",
|
|
563
|
+
changed: false,
|
|
564
|
+
restartRequired: true,
|
|
565
|
+
messages: ["Runtime config needs to be added manually for this agent environment."],
|
|
566
|
+
errorCode: "manual_runtime_setup_required"
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
function buildHostedServer(hostedMcpUrl, apiKey, runtime) {
|
|
571
|
+
if (runtime === "vscode" || runtime === "vscode-insiders") {
|
|
572
|
+
return {
|
|
573
|
+
type: "http",
|
|
574
|
+
url: hostedMcpUrl,
|
|
575
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
return {
|
|
579
|
+
url: hostedMcpUrl,
|
|
580
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
function resolveSignerLaunchSpec(input) {
|
|
584
|
+
return input.signerCommand ?? {
|
|
585
|
+
command: "npx",
|
|
586
|
+
args: ["-y", signerPackageSpec(), "--credentials", input.signerPath]
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
function buildSignerServer(spec, runtime) {
|
|
590
|
+
const server = {
|
|
591
|
+
command: spec.command,
|
|
592
|
+
args: spec.args
|
|
593
|
+
};
|
|
594
|
+
if (runtime === "vscode" || runtime === "vscode-insiders") return { type: "stdio", ...server };
|
|
595
|
+
return server;
|
|
596
|
+
}
|
|
597
|
+
function mergeJsonMcpConfig(existingJson, serverRoot, hostedServer, signerServer, names = serverNamesFor(), configPath) {
|
|
598
|
+
const config = existingJson?.trim() ? parseJsonObject(existingJson, configPath) : {};
|
|
599
|
+
const existingRoot = config[serverRoot];
|
|
600
|
+
const servers = existingRoot && typeof existingRoot === "object" && !Array.isArray(existingRoot) ? existingRoot : {};
|
|
601
|
+
config[serverRoot] = {
|
|
602
|
+
...servers,
|
|
603
|
+
[names.hosted]: hostedServer,
|
|
604
|
+
[names.signer]: signerServer
|
|
605
|
+
};
|
|
606
|
+
return `${JSON.stringify(config, null, 2)}
|
|
607
|
+
`;
|
|
608
|
+
}
|
|
609
|
+
function mergeHermesYaml(existingYaml, hostedServer, signerServer, names = serverNamesFor(), configPath) {
|
|
610
|
+
if (!existingYaml?.trim()) {
|
|
611
|
+
return renderHermesYaml({ [names.hosted]: hostedServer, [names.signer]: signerServer });
|
|
612
|
+
}
|
|
613
|
+
const doc = parseDocument(existingYaml, { keepSourceTokens: true });
|
|
614
|
+
if (doc.errors.length > 0 || !isMap(doc.contents)) {
|
|
615
|
+
throw new UnreadableRuntimeConfigError(configPath ?? "the Hermes config", "it is not a YAML object");
|
|
616
|
+
}
|
|
617
|
+
const mcpPair = doc.contents.items.find((item) => item.key?.toString() === "mcp_servers");
|
|
618
|
+
const existingServers = mcpPair && isMap(mcpPair.value) ? mcpPair.value.toJSON() : {};
|
|
619
|
+
const servers = isRecord(existingServers) ? existingServers : {};
|
|
139
620
|
const mergedServers = {
|
|
140
621
|
...servers,
|
|
141
|
-
|
|
142
|
-
|
|
622
|
+
[names.hosted]: hostedServer,
|
|
623
|
+
[names.signer]: signerServer
|
|
143
624
|
};
|
|
144
625
|
if (!mcpPair) {
|
|
145
626
|
return appendHermesMcpServers(existingYaml, mergedServers);
|
|
@@ -147,9 +628,9 @@ function mergeHermesYaml(existingYaml, hostedServer, signerServer) {
|
|
|
147
628
|
if (!mcpPair.value?.range) return replaceEmptyHermesMcpServers(existingYaml, mcpPair.key?.range, mergedServers);
|
|
148
629
|
return replaceHermesMcpServers(existingYaml, mcpPair.key?.range, mcpPair.value.range, mergedServers);
|
|
149
630
|
}
|
|
150
|
-
function mergeHermesEnv(existingEnv, apiKey) {
|
|
631
|
+
function mergeHermesEnv(existingEnv, apiKey, envKey = HERMES_API_KEY_ENV) {
|
|
151
632
|
if (/[\r\n]/.test(apiKey)) throw new Error("Hermes API key must be a single line");
|
|
152
|
-
const assignment = `${
|
|
633
|
+
const assignment = `${envKey}=${apiKey}`;
|
|
153
634
|
if (!existingEnv) return `${assignment}
|
|
154
635
|
`;
|
|
155
636
|
const lineEnding = existingEnv.includes("\r\n") ? "\r\n" : "\n";
|
|
@@ -158,12 +639,12 @@ function mergeHermesEnv(existingEnv, apiKey) {
|
|
|
158
639
|
if (hasTrailingNewline) lines.pop();
|
|
159
640
|
let found = false;
|
|
160
641
|
const merged = lines.flatMap((line) => {
|
|
161
|
-
if (isHermesEnvAssignment(line)) {
|
|
642
|
+
if (isHermesEnvAssignment(line, envKey)) {
|
|
162
643
|
if (found) return [];
|
|
163
644
|
found = true;
|
|
164
645
|
return [assignment];
|
|
165
646
|
}
|
|
166
|
-
if (isAmbiguousHermesEnvLine(line)) {
|
|
647
|
+
if (isAmbiguousHermesEnvLine(line, envKey)) {
|
|
167
648
|
throw new Error("Hermes environment contains an ambiguous managed key");
|
|
168
649
|
}
|
|
169
650
|
return [line];
|
|
@@ -173,11 +654,11 @@ function mergeHermesEnv(existingEnv, apiKey) {
|
|
|
173
654
|
}
|
|
174
655
|
return `${merged.join(lineEnding)}${hasTrailingNewline ? lineEnding : ""}`;
|
|
175
656
|
}
|
|
176
|
-
function isHermesEnvAssignment(line) {
|
|
177
|
-
return
|
|
657
|
+
function isHermesEnvAssignment(line, envKey) {
|
|
658
|
+
return new RegExp(`^\\s*(?:export[ \\t]+)?${envKey}[ \\t]*=`).test(line);
|
|
178
659
|
}
|
|
179
|
-
function isAmbiguousHermesEnvLine(line) {
|
|
180
|
-
return
|
|
660
|
+
function isAmbiguousHermesEnvLine(line, envKey) {
|
|
661
|
+
return new RegExp(`^\\s*(?:export[ \\t]+)?${envKey}\\b`).test(line);
|
|
181
662
|
}
|
|
182
663
|
function appendHermesMcpServers(source, servers) {
|
|
183
664
|
const documentEnd = /(?:^|\n)[ \t]*\.\.\.[ \t]*(?:#[^\n]*)?\r?\n?$/.exec(source);
|
|
@@ -236,11 +717,14 @@ function renderHermesMcpServerEntries(servers, indent) {
|
|
|
236
717
|
function isRecord(value) {
|
|
237
718
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
238
719
|
}
|
|
239
|
-
function mergeCodexToml(existingToml, localMcpCommand) {
|
|
240
|
-
let next = removeTomlTableTree(
|
|
720
|
+
function mergeCodexToml(existingToml, localMcpCommand, names = serverNamesFor()) {
|
|
721
|
+
let next = removeTomlTableTree(
|
|
722
|
+
removeTomlTableTree(existingToml, `mcp_servers.${names.codexHosted}`),
|
|
723
|
+
`mcp_servers.${names.codexSigner}`
|
|
724
|
+
);
|
|
241
725
|
next = next.trimEnd();
|
|
242
726
|
const block = [
|
|
243
|
-
|
|
727
|
+
`[mcp_servers.${names.codexHosted}]`,
|
|
244
728
|
`command = ${tomlString(localMcpCommand)}`,
|
|
245
729
|
"args = []",
|
|
246
730
|
"startup_timeout_sec = 120"
|
|
@@ -252,15 +736,18 @@ function mergeCodexToml(existingToml, localMcpCommand) {
|
|
|
252
736
|
`;
|
|
253
737
|
return merged;
|
|
254
738
|
}
|
|
255
|
-
function mergeCodexTomlHosted(existingToml, hostedMcpUrl, apiKey, signerSpec) {
|
|
256
|
-
let next = removeTomlTableTree(
|
|
739
|
+
function mergeCodexTomlHosted(existingToml, hostedMcpUrl, apiKey, signerSpec, names = serverNamesFor()) {
|
|
740
|
+
let next = removeTomlTableTree(
|
|
741
|
+
removeTomlTableTree(existingToml, `mcp_servers.${names.codexHosted}`),
|
|
742
|
+
`mcp_servers.${names.codexSigner}`
|
|
743
|
+
);
|
|
257
744
|
next = next.trimEnd();
|
|
258
745
|
const block = [
|
|
259
|
-
|
|
746
|
+
`[mcp_servers.${names.codexHosted}]`,
|
|
260
747
|
`url = ${tomlString(hostedMcpUrl)}`,
|
|
261
748
|
`http_headers = { "Authorization" = ${tomlString(`Bearer ${apiKey}`)} }`,
|
|
262
749
|
"",
|
|
263
|
-
|
|
750
|
+
`[mcp_servers.${names.codexSigner}]`,
|
|
264
751
|
`command = ${tomlString(signerSpec.command)}`,
|
|
265
752
|
`args = [${signerSpec.args.map((arg) => tomlString(arg)).join(", ")}]`,
|
|
266
753
|
"startup_timeout_sec = 120"
|
|
@@ -279,7 +766,9 @@ async function writeJsonRuntimeConfig(input, target, serverRoot) {
|
|
|
279
766
|
existing,
|
|
280
767
|
serverRoot,
|
|
281
768
|
buildHostedServer(input.hostedMcpUrl, input.apiKey, input.runtime),
|
|
282
|
-
buildSignerServer(resolveSignerLaunchSpec(input), input.runtime)
|
|
769
|
+
buildSignerServer(resolveSignerLaunchSpec(input), input.runtime),
|
|
770
|
+
serverNamesFor(input.serverName),
|
|
771
|
+
target
|
|
283
772
|
);
|
|
284
773
|
await writeOwnerOnlyText(target, merged);
|
|
285
774
|
return {
|
|
@@ -293,6 +782,7 @@ async function writeJsonRuntimeConfig(input, target, serverRoot) {
|
|
|
293
782
|
messages: [`Updated Haven MCP entries in ${configTargetLabel(input.runtime)}.`]
|
|
294
783
|
};
|
|
295
784
|
} catch (err) {
|
|
785
|
+
const unreadable = err instanceof UnreadableRuntimeConfigError;
|
|
296
786
|
return {
|
|
297
787
|
hostedConfigured: false,
|
|
298
788
|
signerConfigured: false,
|
|
@@ -301,8 +791,11 @@ async function writeJsonRuntimeConfig(input, target, serverRoot) {
|
|
|
301
791
|
target: configTargetLabel(input.runtime),
|
|
302
792
|
changed: false,
|
|
303
793
|
restartRequired: true,
|
|
304
|
-
messages:
|
|
305
|
-
|
|
794
|
+
messages: unreadable ? [
|
|
795
|
+
`Could not update ${configTargetLabel(input.runtime)}: ${err.message}.`,
|
|
796
|
+
`Nothing was written to ${err.configPath}. Fix the JSON there (or move the file aside), then run \`${REPAIR_COMMAND_PREFIX} ${input.runtime}\` to write the Haven entries from the credentials already stored on this machine. Do not re-run the setup command: its token is already used.`
|
|
797
|
+
] : [`Could not update ${configTargetLabel(input.runtime)}: ${err instanceof Error ? err.message : String(err)}`],
|
|
798
|
+
errorCode: unreadable ? "runtime_config_unreadable" : "runtime_config_write_failed"
|
|
306
799
|
};
|
|
307
800
|
}
|
|
308
801
|
}
|
|
@@ -311,8 +804,9 @@ async function writeHermesConfig(input, deps) {
|
|
|
311
804
|
const envTarget = hermesEnvPath(input.homeDir);
|
|
312
805
|
try {
|
|
313
806
|
const [existing, existingEnv] = await Promise.all([readOptional(target), readOptional(envTarget)]);
|
|
807
|
+
const names = serverNamesFor(input.serverName);
|
|
314
808
|
const hostedServer = {
|
|
315
|
-
...buildHostedServer(input.hostedMcpUrl, `\${${
|
|
809
|
+
...buildHostedServer(input.hostedMcpUrl, `\${${names.hermesEnvKey}}`, input.runtime),
|
|
316
810
|
enabled: true
|
|
317
811
|
};
|
|
318
812
|
const signerServer = {
|
|
@@ -322,9 +816,11 @@ async function writeHermesConfig(input, deps) {
|
|
|
322
816
|
const merged = mergeHermesYaml(
|
|
323
817
|
existing,
|
|
324
818
|
hostedServer,
|
|
325
|
-
signerServer
|
|
819
|
+
signerServer,
|
|
820
|
+
names,
|
|
821
|
+
target
|
|
326
822
|
);
|
|
327
|
-
const mergedEnv = mergeHermesEnv(existingEnv, input.apiKey);
|
|
823
|
+
const mergedEnv = mergeHermesEnv(existingEnv, input.apiKey, names.hermesEnvKey);
|
|
328
824
|
const writeText = deps.writeOwnerOnlyText ?? writeOwnerOnlyText;
|
|
329
825
|
await writeText(envTarget, mergedEnv);
|
|
330
826
|
try {
|
|
@@ -345,11 +841,13 @@ async function writeHermesConfig(input, deps) {
|
|
|
345
841
|
messages: [
|
|
346
842
|
`Updated Haven MCP entries in ${target}; stored the hosted MCP identity in ${envTarget}.`,
|
|
347
843
|
"Restart Hermes (start a new session; gateway users: /restart), then verify with `hermes mcp list`, `hermes mcp test haven`, and `hermes mcp test haven-signer`.",
|
|
844
|
+
"If several long-lived Hermes processes are running (a gateway plus TUI workers), restart EVERY one: each loads its MCP wiring at startup, so a process started before this setup keeps using its old snapshot.",
|
|
348
845
|
"If no mcp_* tools appear after restart, ensure the MCP SDK is installed in Hermes: pip install mcp"
|
|
349
846
|
]
|
|
350
847
|
};
|
|
351
848
|
} catch (err) {
|
|
352
849
|
const recoveryIncomplete = err instanceof HermesConfigRecoveryError;
|
|
850
|
+
const unreadable = err instanceof UnreadableRuntimeConfigError;
|
|
353
851
|
return {
|
|
354
852
|
hostedConfigured: false,
|
|
355
853
|
signerConfigured: false,
|
|
@@ -358,8 +856,11 @@ async function writeHermesConfig(input, deps) {
|
|
|
358
856
|
target: "Hermes Agent config",
|
|
359
857
|
changed: false,
|
|
360
858
|
restartRequired: true,
|
|
361
|
-
messages:
|
|
362
|
-
|
|
859
|
+
messages: unreadable ? [
|
|
860
|
+
`Could not update Hermes Agent config: ${err.message}.`,
|
|
861
|
+
`Nothing was written to ${err.configPath}. Fix the YAML there (or move the file aside), then run \`${REPAIR_COMMAND_PREFIX} hermes\` to write the Haven entries from the credentials already stored on this machine. Do not re-run the setup command: its token is already used.`
|
|
862
|
+
] : [recoveryIncomplete ? "Could not update Hermes Agent config. Recovery did not complete; inspect the Hermes configuration before retrying." : "Could not update Hermes Agent config. Existing configuration was left unchanged."],
|
|
863
|
+
errorCode: unreadable ? "runtime_config_unreadable" : "runtime_config_write_failed"
|
|
363
864
|
};
|
|
364
865
|
}
|
|
365
866
|
}
|
|
@@ -391,7 +892,7 @@ async function writeCodexConfig(input) {
|
|
|
391
892
|
if (!input.localMcpCommand) {
|
|
392
893
|
throw new Error("local MCP wrapper command is required");
|
|
393
894
|
}
|
|
394
|
-
const merged2 = mergeCodexToml(existing ?? "", input.localMcpCommand);
|
|
895
|
+
const merged2 = mergeCodexToml(existing ?? "", input.localMcpCommand, serverNamesFor(input.serverName));
|
|
395
896
|
await writeOwnerOnlyText(target, merged2);
|
|
396
897
|
return {
|
|
397
898
|
hostedConfigured: false,
|
|
@@ -406,7 +907,13 @@ async function writeCodexConfig(input) {
|
|
|
406
907
|
]
|
|
407
908
|
};
|
|
408
909
|
}
|
|
409
|
-
const merged = mergeCodexTomlHosted(
|
|
910
|
+
const merged = mergeCodexTomlHosted(
|
|
911
|
+
existing ?? "",
|
|
912
|
+
input.hostedMcpUrl,
|
|
913
|
+
input.apiKey,
|
|
914
|
+
resolveSignerLaunchSpec(input),
|
|
915
|
+
serverNamesFor(input.serverName)
|
|
916
|
+
);
|
|
410
917
|
await writeOwnerOnlyText(target, merged);
|
|
411
918
|
return {
|
|
412
919
|
hostedConfigured: true,
|
|
@@ -448,10 +955,15 @@ async function writeOwnerOnlyText(path, value) {
|
|
|
448
955
|
await writeFile(path, value, { mode: 384 });
|
|
449
956
|
await chmod(path, 384).catch(() => void 0);
|
|
450
957
|
}
|
|
451
|
-
function parseJsonObject(value) {
|
|
452
|
-
|
|
958
|
+
function parseJsonObject(value, configPath) {
|
|
959
|
+
let parsed;
|
|
960
|
+
try {
|
|
961
|
+
parsed = JSON.parse(value);
|
|
962
|
+
} catch {
|
|
963
|
+
throw new UnreadableRuntimeConfigError(configPath ?? "the runtime config", "it is not valid JSON");
|
|
964
|
+
}
|
|
453
965
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
454
|
-
throw new
|
|
966
|
+
throw new UnreadableRuntimeConfigError(configPath ?? "the runtime config", "the top level is not a JSON object");
|
|
455
967
|
}
|
|
456
968
|
return parsed;
|
|
457
969
|
}
|
|
@@ -718,10 +1230,11 @@ function configTargetLabel(runtime) {
|
|
|
718
1230
|
return "runtime MCP config";
|
|
719
1231
|
}
|
|
720
1232
|
}
|
|
721
|
-
var HERMES_API_KEY_ENV, HermesConfigRecoveryError, InvalidCodexTomlError;
|
|
1233
|
+
var HERMES_API_KEY_ENV, HermesConfigRecoveryError, InvalidCodexTomlError, REPAIR_COMMAND_PREFIX, UnreadableRuntimeConfigError;
|
|
722
1234
|
var init_config_writers = __esm({
|
|
723
1235
|
"src/config-writers.ts"() {
|
|
724
1236
|
init_runtime_manifest();
|
|
1237
|
+
init_server_names();
|
|
725
1238
|
HERMES_API_KEY_ENV = "MCP_HAVEN_API_KEY";
|
|
726
1239
|
HermesConfigRecoveryError = class extends Error {
|
|
727
1240
|
constructor() {
|
|
@@ -735,27 +1248,136 @@ var init_config_writers = __esm({
|
|
|
735
1248
|
this.name = "InvalidCodexTomlError";
|
|
736
1249
|
}
|
|
737
1250
|
};
|
|
1251
|
+
REPAIR_COMMAND_PREFIX = "npx @haven_ai/connect@alpha --doctor --repair --runtime";
|
|
1252
|
+
UnreadableRuntimeConfigError = class extends Error {
|
|
1253
|
+
configPath;
|
|
1254
|
+
constructor(configPath, detail) {
|
|
1255
|
+
super(`${configPath} is not a config Haven can merge into (${detail})`);
|
|
1256
|
+
this.name = "UnreadableRuntimeConfigError";
|
|
1257
|
+
this.configPath = configPath;
|
|
1258
|
+
}
|
|
1259
|
+
};
|
|
738
1260
|
}
|
|
739
1261
|
});
|
|
740
|
-
async function
|
|
741
|
-
let response;
|
|
1262
|
+
async function acknowledgeLocalMcpConsent(identityPath, signerPath, log) {
|
|
742
1263
|
try {
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
Accept: "application/json, text/event-stream"
|
|
749
|
-
},
|
|
750
|
-
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list" })
|
|
1264
|
+
const input = await buildLocalMcpConsentInput(identityPath, signerPath);
|
|
1265
|
+
const decision = await ensureConsent(input, {
|
|
1266
|
+
credentialsPath: identityPath,
|
|
1267
|
+
writeAck: true,
|
|
1268
|
+
out: log ? { write: (chunk) => writeLogChunk(log, chunk) } : void 0
|
|
751
1269
|
});
|
|
752
|
-
|
|
753
|
-
|
|
1270
|
+
return {
|
|
1271
|
+
acknowledged: decision.ok,
|
|
1272
|
+
hash: decision.hash,
|
|
1273
|
+
reason: decision.reason
|
|
1274
|
+
};
|
|
1275
|
+
} catch (err) {
|
|
1276
|
+
return {
|
|
1277
|
+
acknowledged: false,
|
|
1278
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1279
|
+
};
|
|
754
1280
|
}
|
|
755
|
-
|
|
756
|
-
|
|
1281
|
+
}
|
|
1282
|
+
async function getLocalMcpConsentStatus(identityPath, signerPath) {
|
|
757
1283
|
try {
|
|
758
|
-
const
|
|
1284
|
+
const input = await buildLocalMcpConsentInput(identityPath, signerPath);
|
|
1285
|
+
const hash = computeConsentHash(input);
|
|
1286
|
+
const stored = await readLocalMcpAckFile(localMcpAckPath(identityPath));
|
|
1287
|
+
if (stored === hash) {
|
|
1288
|
+
return { acknowledged: true, hash, reason: "ack_file_match" };
|
|
1289
|
+
}
|
|
1290
|
+
return {
|
|
1291
|
+
acknowledged: false,
|
|
1292
|
+
hash,
|
|
1293
|
+
reason: stored ? "ack_file_mismatch" : "ack_file_missing"
|
|
1294
|
+
};
|
|
1295
|
+
} catch (err) {
|
|
1296
|
+
return {
|
|
1297
|
+
acknowledged: false,
|
|
1298
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
function localMcpAckPath(identityPath) {
|
|
1303
|
+
return resolve(`${identityPath}.ack.json`);
|
|
1304
|
+
}
|
|
1305
|
+
async function buildLocalMcpConsentInput(identityPath, signerPath) {
|
|
1306
|
+
const credentials = await loadCredentials({ identityPath, signerPath });
|
|
1307
|
+
const unavailableDuringSetup = {
|
|
1308
|
+
getAllowances: async () => {
|
|
1309
|
+
throw new Error("Haven approval is not complete yet.");
|
|
1310
|
+
}
|
|
1311
|
+
};
|
|
1312
|
+
return consentInputFromClient(
|
|
1313
|
+
unavailableDuringSetup,
|
|
1314
|
+
{
|
|
1315
|
+
apiKey: credentials.apiKey,
|
|
1316
|
+
apiUrl: credentials.apiUrl,
|
|
1317
|
+
agentId: credentials.agentId,
|
|
1318
|
+
safeAddress: credentials.safeAddress,
|
|
1319
|
+
delegateAddress: credentials.delegateAddress,
|
|
1320
|
+
chainId: credentials.chainId,
|
|
1321
|
+
allowanceSummary: credentials.allowanceSummary
|
|
1322
|
+
},
|
|
1323
|
+
registeredToolNames()
|
|
1324
|
+
);
|
|
1325
|
+
}
|
|
1326
|
+
async function readLocalMcpAckFile(path) {
|
|
1327
|
+
try {
|
|
1328
|
+
const parsed = JSON.parse(await readFile(path, "utf8"));
|
|
1329
|
+
return typeof parsed.ack === "string" ? parsed.ack : null;
|
|
1330
|
+
} catch {
|
|
1331
|
+
return null;
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
function writeLogChunk(log, chunk) {
|
|
1335
|
+
const message = String(chunk).trimEnd();
|
|
1336
|
+
if (message) log(message);
|
|
1337
|
+
}
|
|
1338
|
+
var init_local_mcp_consent = __esm({
|
|
1339
|
+
"src/local-mcp-consent.ts"() {
|
|
1340
|
+
}
|
|
1341
|
+
});
|
|
1342
|
+
async function probeHostedAgentIdentity(apiKey, apiUrl, fetchImpl = fetch) {
|
|
1343
|
+
let response;
|
|
1344
|
+
try {
|
|
1345
|
+
response = await fetchWithTimeout(fetchImpl, `${apiUrl.replace(/\/+$/, "")}/machine-payments/agent`, {
|
|
1346
|
+
method: "GET",
|
|
1347
|
+
headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" }
|
|
1348
|
+
});
|
|
1349
|
+
} catch {
|
|
1350
|
+
return { status: "network_error" };
|
|
1351
|
+
}
|
|
1352
|
+
if (response.status === 401 || response.status === 403) return { status: "unauthorized" };
|
|
1353
|
+
if (!response.ok) return { status: "bad_response" };
|
|
1354
|
+
try {
|
|
1355
|
+
const payload = JSON.parse(await response.text());
|
|
1356
|
+
if (typeof payload?.delegate_address !== "string") return { status: "bad_response" };
|
|
1357
|
+
return { status: "ok", agentId: payload.id, delegateAddress: payload.delegate_address };
|
|
1358
|
+
} catch {
|
|
1359
|
+
return { status: "bad_response" };
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
async function probeHostedMcpTools(apiKey, hostedMcpUrl, fetchImpl = fetch) {
|
|
1363
|
+
let response;
|
|
1364
|
+
try {
|
|
1365
|
+
response = await fetchWithTimeout(fetchImpl, hostedMcpUrl, {
|
|
1366
|
+
method: "POST",
|
|
1367
|
+
headers: {
|
|
1368
|
+
Authorization: `Bearer ${apiKey}`,
|
|
1369
|
+
"Content-Type": "application/json",
|
|
1370
|
+
Accept: "application/json, text/event-stream"
|
|
1371
|
+
},
|
|
1372
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list" })
|
|
1373
|
+
});
|
|
1374
|
+
} catch {
|
|
1375
|
+
return { status: "network_error" };
|
|
1376
|
+
}
|
|
1377
|
+
if (response.status === 401 || response.status === 403) return { status: "unauthorized" };
|
|
1378
|
+
if (!response.ok) return { status: "bad_response" };
|
|
1379
|
+
try {
|
|
1380
|
+
const payload = parseJsonRpcPayload(await response.text());
|
|
759
1381
|
if (!payload || payload.error) return { status: "bad_response" };
|
|
760
1382
|
const tools = payload.result?.tools;
|
|
761
1383
|
return { status: "ok", toolCount: Array.isArray(tools) ? tools.length : void 0 };
|
|
@@ -774,7 +1396,7 @@ async function probeLocalSignerCredential(signerPath) {
|
|
|
774
1396
|
}
|
|
775
1397
|
}
|
|
776
1398
|
async function probeLocalMcpTools(command, args, requiredTools, timeoutMs = 1e4) {
|
|
777
|
-
return new Promise((
|
|
1399
|
+
return new Promise((resolve9) => {
|
|
778
1400
|
const child = spawn(command, args, { stdio: ["pipe", "pipe", "ignore"] });
|
|
779
1401
|
let stdout = "";
|
|
780
1402
|
let settled = false;
|
|
@@ -786,7 +1408,7 @@ async function probeLocalMcpTools(command, args, requiredTools, timeoutMs = 1e4)
|
|
|
786
1408
|
settled = true;
|
|
787
1409
|
clearTimeout(timeout);
|
|
788
1410
|
child.kill();
|
|
789
|
-
|
|
1411
|
+
resolve9(result);
|
|
790
1412
|
};
|
|
791
1413
|
const timeout = setTimeout(() => finish({ status: "timeout" }), timeoutMs);
|
|
792
1414
|
child.on("error", () => finish({ status: "process_error" }));
|
|
@@ -900,7 +1522,8 @@ async function prepareSignerRuntime(input, deps = {}) {
|
|
|
900
1522
|
wrapperPath,
|
|
901
1523
|
runtimeDirectory,
|
|
902
1524
|
npmCacheDirectory,
|
|
903
|
-
cliPath
|
|
1525
|
+
cliPath,
|
|
1526
|
+
serverName: input.serverName
|
|
904
1527
|
});
|
|
905
1528
|
messages.push(`Prepared stable local Haven signer wrapper: ${wrapperPath}`);
|
|
906
1529
|
return {
|
|
@@ -1001,6 +1624,7 @@ async function readRuntimeSidecar(credentialDirectory) {
|
|
|
1001
1624
|
}
|
|
1002
1625
|
async function writeRuntimeSidecar(input) {
|
|
1003
1626
|
const value = {
|
|
1627
|
+
...input.serverName ? { server_name: input.serverName } : {},
|
|
1004
1628
|
signer_package: MCP_RUNTIME_MANIFEST.signerPackage,
|
|
1005
1629
|
signer_version: MCP_RUNTIME_MANIFEST.signerVersion,
|
|
1006
1630
|
sdk_package: MCP_RUNTIME_MANIFEST.sdkPackage,
|
|
@@ -1030,61 +1654,394 @@ var init_signer_runtime = __esm({
|
|
|
1030
1654
|
SIGNER_INSTALL_HEARTBEAT_MS = 15e3;
|
|
1031
1655
|
}
|
|
1032
1656
|
});
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1657
|
+
async function prepareLocalMcpRuntime(input, deps = {}) {
|
|
1658
|
+
assertSupportedNodeVersion(input.nodeVersion);
|
|
1659
|
+
const homeDir = input.homeDir ?? homedir();
|
|
1660
|
+
const runtimeDirectory = resolve(homeDir, ".haven", "mcp-runtime", MCP_RUNTIME_MANIFEST.mcpVersion);
|
|
1661
|
+
const npmCacheDirectory = resolve(homeDir, ".haven", "npm-cache");
|
|
1662
|
+
const cliPath = join(runtimeDirectory, "node_modules", "@haven_ai", "mcp", "dist", "cli.js");
|
|
1663
|
+
const messages = [];
|
|
1664
|
+
await mkdir(runtimeDirectory, { recursive: true, mode: 448 });
|
|
1665
|
+
await chmod(runtimeDirectory, 448).catch(() => void 0);
|
|
1666
|
+
await mkdir(npmCacheDirectory, { recursive: true, mode: 448 });
|
|
1667
|
+
await chmod(npmCacheDirectory, 448).catch(() => void 0);
|
|
1668
|
+
if (await installedRuntimeMatches2(runtimeDirectory, cliPath)) {
|
|
1669
|
+
messages.push(`Using existing local Haven MCP runtime ${mcpPackageSpec()}.`);
|
|
1670
|
+
} else {
|
|
1671
|
+
await installRuntimePackages2(runtimeDirectory, npmCacheDirectory, deps);
|
|
1672
|
+
messages.push(`Installed local Haven MCP runtime ${mcpPackageSpec()}.`);
|
|
1673
|
+
}
|
|
1674
|
+
await assertFileExists2(cliPath, "local Haven MCP CLI");
|
|
1675
|
+
const wrapperPath = join(input.credentialDirectory, "bin", "haven-mcp");
|
|
1676
|
+
await writeWrapper2({
|
|
1677
|
+
wrapperPath,
|
|
1678
|
+
cliPath,
|
|
1679
|
+
identityPath: input.identityPath,
|
|
1680
|
+
signerPath: input.signerPath
|
|
1681
|
+
});
|
|
1682
|
+
await writeRuntimeSidecar2({
|
|
1683
|
+
path: join(input.credentialDirectory, "mcp-runtime.json"),
|
|
1684
|
+
wrapperPath,
|
|
1685
|
+
runtimeDirectory,
|
|
1686
|
+
npmCacheDirectory,
|
|
1687
|
+
cliPath,
|
|
1688
|
+
serverName: input.serverName
|
|
1689
|
+
});
|
|
1690
|
+
messages.push(`Prepared stable local Haven MCP wrapper: ${wrapperPath}`);
|
|
1691
|
+
return {
|
|
1692
|
+
command: wrapperPath,
|
|
1693
|
+
args: [],
|
|
1694
|
+
wrapperPath,
|
|
1695
|
+
runtimeDirectory,
|
|
1696
|
+
npmCacheDirectory,
|
|
1697
|
+
cliPath,
|
|
1698
|
+
messages
|
|
1699
|
+
};
|
|
1037
1700
|
}
|
|
1038
|
-
function
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1701
|
+
function assertSupportedNodeVersion(nodeVersion = process.versions.node, minimumNodeVersion = MCP_RUNTIME_MANIFEST.minimumNodeVersion, subject = "Haven setup") {
|
|
1702
|
+
if (!isSupportedNodeVersion(nodeVersion, minimumNodeVersion)) {
|
|
1703
|
+
throw new UnsupportedNodeVersionError(nodeVersion, minimumNodeVersion, subject);
|
|
1704
|
+
}
|
|
1042
1705
|
}
|
|
1043
|
-
function
|
|
1044
|
-
const
|
|
1045
|
-
|
|
1706
|
+
async function installRuntimePackages2(runtimeDirectory, npmCacheDirectory, deps) {
|
|
1707
|
+
const { runCommand, onProgress } = deps;
|
|
1708
|
+
const baseArgs = [
|
|
1709
|
+
"install",
|
|
1710
|
+
"--prefix",
|
|
1711
|
+
runtimeDirectory,
|
|
1712
|
+
"--no-audit",
|
|
1713
|
+
"--no-fund",
|
|
1714
|
+
"--omit=dev",
|
|
1715
|
+
"--prefer-offline",
|
|
1716
|
+
mcpPackageSpec(),
|
|
1717
|
+
sdkPackageSpec()
|
|
1718
|
+
];
|
|
1719
|
+
const run = async (args) => {
|
|
1720
|
+
const startedAt = Date.now();
|
|
1721
|
+
const heartbeat = setInterval(() => {
|
|
1722
|
+
const seconds = Math.round((Date.now() - startedAt) / 1e3);
|
|
1723
|
+
onProgress?.(`Still installing the local Haven MCP runtime\u2026 (${seconds}s \u2014 a cold cache can take several minutes)`);
|
|
1724
|
+
}, SIGNER_INSTALL_HEARTBEAT_MS);
|
|
1725
|
+
heartbeat.unref?.();
|
|
1726
|
+
try {
|
|
1727
|
+
if (runCommand) await runCommand("npm", args);
|
|
1728
|
+
else await execFileAsync2("npm", args, { timeout: SIGNER_INSTALL_TIMEOUT_MS, maxBuffer: 1024 * 1024 });
|
|
1729
|
+
} finally {
|
|
1730
|
+
clearInterval(heartbeat);
|
|
1731
|
+
}
|
|
1732
|
+
};
|
|
1733
|
+
try {
|
|
1734
|
+
await run(baseArgs);
|
|
1735
|
+
} catch {
|
|
1736
|
+
try {
|
|
1737
|
+
await run([...baseArgs, "--cache", npmCacheDirectory]);
|
|
1738
|
+
} catch (err) {
|
|
1739
|
+
throw new Error(`Could not install local Haven MCP runtime ${mcpPackageSpec()}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1046
1742
|
}
|
|
1047
|
-
function
|
|
1048
|
-
|
|
1049
|
-
|
|
1743
|
+
async function installedRuntimeMatches2(runtimeDirectory, cliPath) {
|
|
1744
|
+
try {
|
|
1745
|
+
await assertFileExists2(cliPath, "local Haven MCP CLI");
|
|
1746
|
+
const [mcpPackage, sdkPackage] = await Promise.all([
|
|
1747
|
+
readPackageJson2(join(runtimeDirectory, "node_modules", "@haven_ai", "mcp", "package.json")),
|
|
1748
|
+
readPackageJson2(join(runtimeDirectory, "node_modules", "@haven_ai", "sdk", "package.json"))
|
|
1749
|
+
]);
|
|
1750
|
+
return mcpPackage.version === MCP_RUNTIME_MANIFEST.mcpVersion && sdkPackage.version === MCP_RUNTIME_MANIFEST.sdkVersion;
|
|
1751
|
+
} catch {
|
|
1752
|
+
return false;
|
|
1753
|
+
}
|
|
1050
1754
|
}
|
|
1051
|
-
function
|
|
1052
|
-
|
|
1053
|
-
if (!key) return null;
|
|
1054
|
-
return RUNTIME_ALIASES[key.replace(/\s+/g, "-")] ?? null;
|
|
1755
|
+
async function readPackageJson2(path) {
|
|
1756
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
1055
1757
|
}
|
|
1056
|
-
function
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1758
|
+
async function writeWrapper2(input) {
|
|
1759
|
+
await mkdir(dirname(input.wrapperPath), { recursive: true, mode: 448 });
|
|
1760
|
+
await chmod(dirname(input.wrapperPath), 448).catch(() => void 0);
|
|
1761
|
+
const source = [
|
|
1762
|
+
"#!/usr/bin/env node",
|
|
1763
|
+
"import { spawn } from 'node:child_process'",
|
|
1764
|
+
"",
|
|
1765
|
+
`const cliPath = ${JSON.stringify(input.cliPath)}`,
|
|
1766
|
+
`const identityPath = ${JSON.stringify(input.identityPath)}`,
|
|
1767
|
+
`const signerPath = ${JSON.stringify(input.signerPath)}`,
|
|
1768
|
+
"",
|
|
1769
|
+
"const child = spawn(process.execPath, [cliPath, '--identity', identityPath, '--signer', signerPath, ...process.argv.slice(2)], {",
|
|
1770
|
+
" stdio: 'inherit',",
|
|
1771
|
+
"})",
|
|
1772
|
+
"",
|
|
1773
|
+
"child.on('exit', (code, signal) => {",
|
|
1774
|
+
" if (signal) process.kill(process.pid, signal)",
|
|
1775
|
+
" else process.exit(code ?? 1)",
|
|
1776
|
+
"})",
|
|
1777
|
+
""
|
|
1778
|
+
].join("\n");
|
|
1779
|
+
await writeFile(input.wrapperPath, source, { mode: 448 });
|
|
1780
|
+
await chmod(input.wrapperPath, 448).catch(() => void 0);
|
|
1062
1781
|
}
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1782
|
+
async function writeRuntimeSidecar2(input) {
|
|
1783
|
+
const value = {
|
|
1784
|
+
...input.serverName ? { server_name: input.serverName } : {},
|
|
1785
|
+
mcp_package: MCP_RUNTIME_MANIFEST.mcpPackage,
|
|
1786
|
+
mcp_version: MCP_RUNTIME_MANIFEST.mcpVersion,
|
|
1787
|
+
sdk_package: MCP_RUNTIME_MANIFEST.sdkPackage,
|
|
1788
|
+
sdk_version: MCP_RUNTIME_MANIFEST.sdkVersion,
|
|
1789
|
+
minimum_node_version: MCP_RUNTIME_MANIFEST.minimumNodeVersion,
|
|
1790
|
+
wrapper_path: input.wrapperPath,
|
|
1791
|
+
runtime_directory: input.runtimeDirectory,
|
|
1792
|
+
npm_cache_directory: input.npmCacheDirectory,
|
|
1793
|
+
cli_path: input.cliPath
|
|
1794
|
+
};
|
|
1795
|
+
await writeFile(input.path, `${JSON.stringify(value, null, 2)}
|
|
1796
|
+
`, { mode: 384 });
|
|
1797
|
+
await chmod(input.path, 384).catch(() => void 0);
|
|
1798
|
+
}
|
|
1799
|
+
async function assertFileExists2(path, label) {
|
|
1800
|
+
try {
|
|
1801
|
+
await access(path);
|
|
1802
|
+
} catch {
|
|
1803
|
+
throw new Error(`Missing ${label}: ${path}`);
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
var execFileAsync2, UnsupportedNodeVersionError;
|
|
1807
|
+
var init_local_mcp_runtime = __esm({
|
|
1808
|
+
"src/local-mcp-runtime.ts"() {
|
|
1809
|
+
init_signer_runtime();
|
|
1810
|
+
init_runtime_manifest();
|
|
1811
|
+
execFileAsync2 = promisify(execFile);
|
|
1812
|
+
UnsupportedNodeVersionError = class extends Error {
|
|
1813
|
+
code = "local_mcp_unsupported_node_version";
|
|
1814
|
+
nodeVersion;
|
|
1815
|
+
minimumNodeVersion;
|
|
1816
|
+
constructor(nodeVersion, minimumNodeVersion, subject = "Haven setup") {
|
|
1817
|
+
super(unsupportedNodeVersionMessage({ subject, nodeVersion, minimumNodeVersion }));
|
|
1818
|
+
this.name = "UnsupportedNodeVersionError";
|
|
1819
|
+
this.nodeVersion = nodeVersion;
|
|
1820
|
+
this.minimumNodeVersion = minimumNodeVersion;
|
|
1821
|
+
}
|
|
1822
|
+
};
|
|
1823
|
+
}
|
|
1824
|
+
});
|
|
1825
|
+
async function installSkillForRuntime(runtime, deps = {}) {
|
|
1826
|
+
switch (runtime) {
|
|
1827
|
+
case "claude-code":
|
|
1828
|
+
return installSkillFile(
|
|
1829
|
+
resolve(deps.homeDir ?? homedir(), ".claude", "skills", SKILL_FOLDER_NAME),
|
|
1830
|
+
"~/.claude/skills/haven-pay"
|
|
1831
|
+
);
|
|
1832
|
+
case "hermes":
|
|
1833
|
+
return installSkillFile(
|
|
1834
|
+
join(hermesHome(deps), "skills", SKILL_FOLDER_NAME),
|
|
1835
|
+
"the Hermes skills folder"
|
|
1836
|
+
);
|
|
1837
|
+
case "codex-cli":
|
|
1838
|
+
case "codex-desktop":
|
|
1839
|
+
return installCodexAgentsSection(deps);
|
|
1840
|
+
default:
|
|
1841
|
+
return void 0;
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
async function installSkillFile(skillDir, label) {
|
|
1845
|
+
try {
|
|
1846
|
+
await mkdir(skillDir, { recursive: true });
|
|
1847
|
+
const target = join(skillDir, "SKILL.md");
|
|
1848
|
+
await writeFile(target, HAVEN_SKILL_MD, "utf8");
|
|
1849
|
+
return {
|
|
1850
|
+
installed: true,
|
|
1851
|
+
target,
|
|
1852
|
+
messages: [`Installed the generic Haven payment skill (${label}). It contains no secrets.`]
|
|
1853
|
+
};
|
|
1854
|
+
} catch (err) {
|
|
1855
|
+
return {
|
|
1856
|
+
installed: false,
|
|
1857
|
+
messages: [
|
|
1858
|
+
`Could not install the Haven payment skill: ${err instanceof Error ? err.message : String(err)}. Download it from the Haven dashboard instead.`
|
|
1859
|
+
]
|
|
1860
|
+
};
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
async function installCodexAgentsSection(deps) {
|
|
1864
|
+
try {
|
|
1865
|
+
const codexDir = resolve(deps.homeDir ?? homedir(), ".codex");
|
|
1866
|
+
const target = join(codexDir, "AGENTS.md");
|
|
1867
|
+
await mkdir(codexDir, { recursive: true });
|
|
1868
|
+
const existing = await readFile(target, "utf8").catch(() => null);
|
|
1869
|
+
const next = upsertManagedSection(existing, codexManagedSection());
|
|
1870
|
+
if (next !== existing) {
|
|
1871
|
+
await writeFile(target, next, "utf8");
|
|
1872
|
+
}
|
|
1873
|
+
return {
|
|
1874
|
+
installed: true,
|
|
1875
|
+
target,
|
|
1876
|
+
messages: [
|
|
1877
|
+
"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."
|
|
1878
|
+
]
|
|
1879
|
+
};
|
|
1880
|
+
} catch (err) {
|
|
1881
|
+
return {
|
|
1882
|
+
installed: false,
|
|
1883
|
+
messages: [
|
|
1884
|
+
`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.`
|
|
1885
|
+
]
|
|
1886
|
+
};
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
function codexManagedSection() {
|
|
1890
|
+
return `${CODEX_AGENTS_BEGIN_MARKER}
|
|
1891
|
+
|
|
1892
|
+
${HAVEN_SKILL_BODY_MD.trimEnd()}
|
|
1893
|
+
|
|
1894
|
+
${CODEX_AGENTS_END_MARKER}
|
|
1895
|
+
`;
|
|
1896
|
+
}
|
|
1897
|
+
function upsertManagedSection(existing, section) {
|
|
1898
|
+
if (existing === null || existing.trim() === "") return section;
|
|
1899
|
+
const begins = markerLineIndexes(existing, CODEX_AGENTS_BEGIN_MARKER);
|
|
1900
|
+
const ends = markerLineIndexes(existing, CODEX_AGENTS_END_MARKER);
|
|
1901
|
+
if (begins.length === 1 && ends.length === 1 && ends[0] > begins[0]) {
|
|
1902
|
+
const afterEnd = ends[0] + CODEX_AGENTS_END_MARKER.length;
|
|
1903
|
+
const tail = existing.startsWith("\r\n", afterEnd) ? existing.slice(afterEnd + 2) : existing.startsWith("\n", afterEnd) ? existing.slice(afterEnd + 1) : existing.slice(afterEnd);
|
|
1904
|
+
return existing.slice(0, begins[0]) + section + tail;
|
|
1905
|
+
}
|
|
1906
|
+
if (begins.length > 0 || ends.length > 0) {
|
|
1907
|
+
throw new Error(
|
|
1908
|
+
"found a damaged Haven marker section (orphaned or duplicated markers); remove the leftover marker lines and re-run setup"
|
|
1909
|
+
);
|
|
1910
|
+
}
|
|
1911
|
+
return `${existing.replace(/\n*$/, "\n\n")}${section}`;
|
|
1912
|
+
}
|
|
1913
|
+
function markerLineIndexes(text, marker) {
|
|
1914
|
+
const indexes = [];
|
|
1915
|
+
for (let from = 0; ; ) {
|
|
1916
|
+
const at = text.indexOf(marker, from);
|
|
1917
|
+
if (at === -1) return indexes;
|
|
1918
|
+
if (at === 0 || text[at - 1] === "\n") indexes.push(at);
|
|
1919
|
+
from = at + marker.length;
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
function hermesHome(deps) {
|
|
1923
|
+
const env = deps.env ?? process.env;
|
|
1924
|
+
return env.HERMES_HOME ?? join(deps.homeDir ?? homedir(), ".hermes");
|
|
1925
|
+
}
|
|
1926
|
+
var CODEX_AGENTS_BEGIN_MARKER, CODEX_AGENTS_END_MARKER;
|
|
1927
|
+
var init_skill_install = __esm({
|
|
1928
|
+
"src/skill-install.ts"() {
|
|
1929
|
+
CODEX_AGENTS_BEGIN_MARKER = "<!-- BEGIN haven-pay (managed by @haven_ai/connect; edits inside this section are overwritten on re-setup) -->";
|
|
1930
|
+
CODEX_AGENTS_END_MARKER = "<!-- END haven-pay -->";
|
|
1931
|
+
}
|
|
1932
|
+
});
|
|
1933
|
+
|
|
1934
|
+
// src/connect-error.ts
|
|
1935
|
+
function isConnectError(err) {
|
|
1936
|
+
return err instanceof ConnectError;
|
|
1937
|
+
}
|
|
1938
|
+
var ConnectError;
|
|
1939
|
+
var init_connect_error = __esm({
|
|
1940
|
+
"src/connect-error.ts"() {
|
|
1941
|
+
ConnectError = class extends Error {
|
|
1942
|
+
code;
|
|
1943
|
+
nextAction;
|
|
1944
|
+
constructor(code, message, nextAction2) {
|
|
1945
|
+
super(message);
|
|
1946
|
+
this.name = "ConnectError";
|
|
1947
|
+
this.code = code;
|
|
1948
|
+
this.nextAction = nextAction2;
|
|
1949
|
+
}
|
|
1950
|
+
};
|
|
1951
|
+
}
|
|
1952
|
+
});
|
|
1953
|
+
|
|
1954
|
+
// src/runtime-registry.ts
|
|
1955
|
+
function runtimeProfile(runtime, env = process.env) {
|
|
1956
|
+
return RUNTIME_PROFILES[normalizeRuntime(runtime, env)];
|
|
1957
|
+
}
|
|
1958
|
+
function normalizeRuntime(runtime, env = process.env) {
|
|
1959
|
+
const explicit = normalizeRuntimeName(runtime);
|
|
1960
|
+
if (explicit) return explicit;
|
|
1961
|
+
return detectRuntime(env) ?? "other";
|
|
1962
|
+
}
|
|
1963
|
+
async function resolveRuntimeSelection(explicit, force, options = {}) {
|
|
1964
|
+
const env = options.env ?? process.env;
|
|
1965
|
+
if (force !== void 0) {
|
|
1966
|
+
const forced = normalizeRuntimeName(force);
|
|
1967
|
+
if (!forced) {
|
|
1968
|
+
throw new ConnectError(
|
|
1969
|
+
"runtime_force_unrecognized",
|
|
1970
|
+
`Unknown --runtime-force value "${force}". Valid values: ${RUNTIME_FLAG_VALUES}.`,
|
|
1971
|
+
"rerun_connect_with_a_valid_runtime_name"
|
|
1972
|
+
);
|
|
1973
|
+
}
|
|
1974
|
+
return { runtime: forced, source: "force" };
|
|
1975
|
+
}
|
|
1976
|
+
const detected = detectRuntime(env);
|
|
1977
|
+
const supplied = explicit?.trim() || options.selfReported?.trim() || void 0;
|
|
1978
|
+
const hint = normalizeRuntimeName(supplied);
|
|
1979
|
+
if (supplied && !hint) {
|
|
1980
|
+
if (!detected) {
|
|
1981
|
+
throw new ConnectError(
|
|
1982
|
+
"runtime_unrecognized",
|
|
1983
|
+
`"${supplied}" is not an agent runtime Haven knows. Valid values: ${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.`,
|
|
1984
|
+
"rerun_connect_with_a_valid_runtime_name"
|
|
1985
|
+
);
|
|
1986
|
+
}
|
|
1987
|
+
return { runtime: detected, source: "detected", discardedHint: supplied };
|
|
1988
|
+
}
|
|
1989
|
+
if (detected && hint && detected !== hint) {
|
|
1990
|
+
return { runtime: detected, source: "detected", overrodeHint: hint };
|
|
1991
|
+
}
|
|
1992
|
+
if (hint) return { runtime: hint, source: "explicit" };
|
|
1993
|
+
if (detected) return { runtime: detected, source: "detected" };
|
|
1994
|
+
if (options.promptForRuntime) {
|
|
1995
|
+
return { runtime: await options.promptForRuntime(), source: "prompted" };
|
|
1996
|
+
}
|
|
1997
|
+
return { runtime: null, source: "none" };
|
|
1998
|
+
}
|
|
1999
|
+
function restartRequiredForRuntime(runtime, env = process.env) {
|
|
2000
|
+
const mode = runtimeProfile(runtime, env).restartMode;
|
|
2001
|
+
return mode === "restart-session" || mode === "restart-app";
|
|
2002
|
+
}
|
|
2003
|
+
function runtimeVerificationInstruction(runtime) {
|
|
2004
|
+
const label = RUNTIME_PROFILES[runtime].label;
|
|
2005
|
+
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.`;
|
|
2006
|
+
}
|
|
2007
|
+
function normalizeRuntimeName(runtime) {
|
|
2008
|
+
const key = runtime?.trim().toLowerCase();
|
|
2009
|
+
if (!key) return null;
|
|
2010
|
+
return RUNTIME_ALIASES[key.replace(/\s+/g, "-")] ?? null;
|
|
2011
|
+
}
|
|
2012
|
+
function detectRuntime(env) {
|
|
2013
|
+
if (env.CLAUDECODE || env.CLAUDE_CODE || env.CLAUDECODE_CWD) return "claude-code";
|
|
2014
|
+
if (env.CODEX_SANDBOX || env.CODEX_HOME || env.CODEX_CWD) return "codex-cli";
|
|
2015
|
+
if (env.VSCODE_CWD || env.VSCODE_IPC_HOOK_CLI || env.TERM_PROGRAM === "vscode") return "vscode";
|
|
2016
|
+
if (env.HERMES_HOME || env.HERMES_AGENT) return "hermes";
|
|
2017
|
+
return null;
|
|
2018
|
+
}
|
|
2019
|
+
var RUNTIME_PROFILES, RUNTIME_ALIASES, RUNTIME_FLAG_VALUES;
|
|
2020
|
+
var init_runtime_registry = __esm({
|
|
2021
|
+
"src/runtime-registry.ts"() {
|
|
2022
|
+
init_connect_error();
|
|
2023
|
+
RUNTIME_PROFILES = {
|
|
2024
|
+
"claude-code": {
|
|
2025
|
+
id: "claude-code",
|
|
2026
|
+
label: "Claude Code",
|
|
2027
|
+
restartMode: "restart-session",
|
|
2028
|
+
canWriteRuntimeConfig: true,
|
|
2029
|
+
activationInstruction: "Start a new Claude Code session so it loads the Haven MCP entries."
|
|
2030
|
+
},
|
|
2031
|
+
"codex-cli": {
|
|
2032
|
+
id: "codex-cli",
|
|
2033
|
+
label: "Codex CLI",
|
|
2034
|
+
restartMode: "restart-session",
|
|
2035
|
+
canWriteRuntimeConfig: true,
|
|
2036
|
+
activationInstruction: "Start a fresh Codex CLI session (for example, run `codex resume --last`)."
|
|
2037
|
+
},
|
|
2038
|
+
"codex-desktop": {
|
|
2039
|
+
id: "codex-desktop",
|
|
2040
|
+
label: "Codex Desktop",
|
|
2041
|
+
restartMode: "restart-session",
|
|
2042
|
+
canWriteRuntimeConfig: true,
|
|
2043
|
+
activationInstruction: "Quit and reopen Codex Desktop so it loads the Haven MCP entries."
|
|
2044
|
+
},
|
|
1088
2045
|
cursor: {
|
|
1089
2046
|
id: "cursor",
|
|
1090
2047
|
label: "Cursor",
|
|
@@ -1133,6 +2090,11 @@ var init_runtime_registry = __esm({
|
|
|
1133
2090
|
"claude-code": "claude-code",
|
|
1134
2091
|
claudecode: "claude-code",
|
|
1135
2092
|
"claude_code": "claude-code",
|
|
2093
|
+
// #1682: Cowork runs Claude Code's config, so it resolves to the same
|
|
2094
|
+
// profile. The dashboard's command carries no --runtime for it (it is a
|
|
2095
|
+
// detected, command-path runtime), but an explicit `--runtime cowork` typed
|
|
2096
|
+
// by hand must not fall through to the no-runtime refusal.
|
|
2097
|
+
cowork: "claude-code",
|
|
1136
2098
|
codex: "codex-cli",
|
|
1137
2099
|
"codex-cli": "codex-cli",
|
|
1138
2100
|
codexcli: "codex-cli",
|
|
@@ -1162,9 +2124,18 @@ var init_runtime_registry = __esm({
|
|
|
1162
2124
|
"hermes-agent": "hermes",
|
|
1163
2125
|
hermes_agent: "hermes",
|
|
1164
2126
|
hermesagent: "hermes",
|
|
2127
|
+
// #1682: OpenClaw is a SNIPPET target — the user pastes an mcpServers entry
|
|
2128
|
+
// into ~/.openclaw/openclaw.json and restarts the gateway. That is exactly
|
|
2129
|
+
// the 'other' profile's behaviour (credentials written to disk, no config
|
|
2130
|
+
// auto-written, manual finish), so it resolves there rather than earning a
|
|
2131
|
+
// profile whose only distinguishing feature would be its label.
|
|
2132
|
+
openclaw: "other",
|
|
2133
|
+
"open-claw": "other",
|
|
2134
|
+
"open_claw": "other",
|
|
1165
2135
|
other: "other",
|
|
1166
2136
|
manual: "other"
|
|
1167
2137
|
};
|
|
2138
|
+
RUNTIME_FLAG_VALUES = "claude-code, codex-cli, codex-desktop, cursor, vscode, vscode-insiders, claude-desktop, hermes, other";
|
|
1168
2139
|
}
|
|
1169
2140
|
});
|
|
1170
2141
|
async function acknowledgeLocalSignerConsent(signerPath, log) {
|
|
@@ -1240,1254 +2211,1494 @@ var init_signer_consent = __esm({
|
|
|
1240
2211
|
"src/signer-consent.ts"() {
|
|
1241
2212
|
}
|
|
1242
2213
|
});
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
if (
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
2214
|
+
async function installRuntime(input, deps = {}) {
|
|
2215
|
+
const runtime = normalizeRuntime(input.runtime, deps.env);
|
|
2216
|
+
const profile = runtimeProfile(runtime, deps.env);
|
|
2217
|
+
const progress = deps.onProgress ?? (() => void 0);
|
|
2218
|
+
const localRuntime = input.localMcp === true && supportsLocalMcp(runtime);
|
|
2219
|
+
const consentMessages = [];
|
|
2220
|
+
const localMcpConsent = localRuntime ? await resolveLocalMcpConsent(input, consentMessages) : void 0;
|
|
2221
|
+
const signerConsent = localRuntime ? void 0 : await resolveSignerConsent(input, consentMessages);
|
|
2222
|
+
if (runtime === "other") {
|
|
2223
|
+
const signerCredentialReady2 = await probeLocalSignerCredential(input.signerPath);
|
|
2224
|
+
const signerReady = signerCredentialReady2 && signerConsent?.acknowledged;
|
|
2225
|
+
return {
|
|
2226
|
+
runtime,
|
|
2227
|
+
runtimeMcpMode: "manual",
|
|
2228
|
+
hostedMcpConfigured: false,
|
|
2229
|
+
localSignerConfigured: false,
|
|
2230
|
+
localMcpConfigured: false,
|
|
2231
|
+
probeResult: signerReady ? "manual_runtime_setup_required_local_signer_ready" : "manual_runtime_setup_required_local_signer_unavailable",
|
|
2232
|
+
restartRequired: true,
|
|
2233
|
+
nextUserAction: "return_to_haven_for_wallet_approval_then_configure_runtime",
|
|
2234
|
+
errorCode: "manual_runtime_setup_required",
|
|
2235
|
+
configTarget: "manual runtime setup",
|
|
2236
|
+
signerAcknowledged: signerConsent?.acknowledged,
|
|
2237
|
+
localMcpAcknowledged: false,
|
|
2238
|
+
messages: [
|
|
2239
|
+
...consentMessages,
|
|
2240
|
+
"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.",
|
|
2241
|
+
` identity (hosted MCP Bearer): ${input.identityPath}`,
|
|
2242
|
+
` signer (local signing key): ${input.signerPath}`,
|
|
2243
|
+
"After wallet approval, wire the runtime to Haven by reference:",
|
|
2244
|
+
` 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}`,
|
|
2245
|
+
` Fully local MCP (no hosted dependency): npx -y ${mcpPackageSpec()} --identity ${input.identityPath} --signer ${input.signerPath}`
|
|
2246
|
+
]
|
|
2247
|
+
};
|
|
1258
2248
|
}
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
2249
|
+
let localRuntimeInstall;
|
|
2250
|
+
let localRuntimeError;
|
|
2251
|
+
if (localRuntime) {
|
|
1262
2252
|
try {
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
}
|
|
1267
|
-
}
|
|
1268
|
-
if (candidates.length === 0) return {};
|
|
1269
|
-
candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1270
|
-
return {
|
|
1271
|
-
directory: candidates[0].directory,
|
|
1272
|
-
note: candidates.length > 1 ? `${candidates.length} agent credential dirs found; examining the newest.` : void 0
|
|
1273
|
-
};
|
|
1274
|
-
}
|
|
1275
|
-
async function runDoctor(input, deps = {}) {
|
|
1276
|
-
const homeDir = deps.homeDir ?? homedir();
|
|
1277
|
-
const checks = [];
|
|
1278
|
-
let signerCapabilities;
|
|
1279
|
-
const { directory, note } = await discoverCredentialDirectory(homeDir, input.credentialsDir);
|
|
1280
|
-
let identity;
|
|
1281
|
-
let signerParses = false;
|
|
1282
|
-
if (!directory) {
|
|
1283
|
-
checks.push({
|
|
1284
|
-
id: "credentials",
|
|
1285
|
-
label: "Agent credentials",
|
|
1286
|
-
ok: false,
|
|
1287
|
-
detail: "No agent credential directory with an identity.json under ~/.haven/agents.",
|
|
1288
|
-
repair: `Run the full setup once: ${RERUN} --setup <token from the Haven dashboard>.`
|
|
1289
|
-
});
|
|
1290
|
-
} else {
|
|
1291
|
-
try {
|
|
1292
|
-
identity = JSON.parse(await readFile(join(directory, "identity.json"), "utf8"));
|
|
1293
|
-
} catch {
|
|
1294
|
-
identity = void 0;
|
|
1295
|
-
}
|
|
1296
|
-
try {
|
|
1297
|
-
const signer = JSON.parse(await readFile(join(directory, "signer.json"), "utf8"));
|
|
1298
|
-
signerParses = typeof signer === "object" && signer !== null;
|
|
1299
|
-
} catch {
|
|
1300
|
-
signerParses = false;
|
|
1301
|
-
}
|
|
1302
|
-
const ok = Boolean(identity?.api_key) && signerParses;
|
|
1303
|
-
checks.push({
|
|
1304
|
-
id: "credentials",
|
|
1305
|
-
label: "Agent credentials",
|
|
1306
|
-
ok,
|
|
1307
|
-
detail: ok ? `identity.json and signer.json parse (agent ${identity?.agent_id ?? "unknown"})${note ? ` \u2014 ${note}` : ""}` : "identity.json or signer.json is missing or unparseable.",
|
|
1308
|
-
...ok ? {} : { repair: `Re-run the full setup with a fresh token: ${RERUN} --setup <token>.` }
|
|
1309
|
-
});
|
|
1310
|
-
}
|
|
1311
|
-
let sidecar = null;
|
|
1312
|
-
if (directory) {
|
|
1313
|
-
sidecar = await readRuntimeSidecar(directory);
|
|
1314
|
-
if (!sidecar) {
|
|
1315
|
-
checks.push({
|
|
1316
|
-
id: "signer_runtime",
|
|
1317
|
-
label: "Signer runtime (preinstalled wrapper)",
|
|
1318
|
-
ok: false,
|
|
1319
|
-
detail: "No signer-runtime.json sidecar \u2014 the pinned signer runtime was never prepared (or a pre-#1586 npx config).",
|
|
1320
|
-
repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}`
|
|
1321
|
-
});
|
|
1322
|
-
} else {
|
|
1323
|
-
const matches = await installedRuntimeMatches(sidecar.runtime_directory, sidecar.cli_path);
|
|
1324
|
-
const versionOk = sidecar.signer_version === MCP_RUNTIME_MANIFEST.signerVersion;
|
|
1325
|
-
const ok = matches && versionOk;
|
|
1326
|
-
checks.push({
|
|
1327
|
-
id: "signer_runtime",
|
|
1328
|
-
label: "Signer runtime (preinstalled wrapper)",
|
|
1329
|
-
ok,
|
|
1330
|
-
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 ${MCP_RUNTIME_MANIFEST.signerVersion}.` : `Runtime directory is stale or empty (${sidecar.runtime_directory}) \u2014 the CLI or package versions are missing.`,
|
|
1331
|
-
...ok ? {} : { repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}` }
|
|
1332
|
-
});
|
|
1333
|
-
}
|
|
1334
|
-
}
|
|
1335
|
-
const configPath = runtimeConfigPathFor(input.runtime, homeDir);
|
|
1336
|
-
if (configPath === null) {
|
|
1337
|
-
checks.push({
|
|
1338
|
-
id: "runtime_config",
|
|
1339
|
-
label: "Runtime MCP config",
|
|
1340
|
-
ok: true,
|
|
1341
|
-
detail: `Runtime '${input.runtime}' has no file-based config the connector owns (CLI-managed) \u2014 skipping the file check.`
|
|
1342
|
-
});
|
|
1343
|
-
} else {
|
|
1344
|
-
let configText = null;
|
|
1345
|
-
try {
|
|
1346
|
-
configText = await readFile(configPath, "utf8");
|
|
1347
|
-
} catch {
|
|
1348
|
-
configText = null;
|
|
1349
|
-
}
|
|
1350
|
-
if (configText === null) {
|
|
1351
|
-
checks.push({
|
|
1352
|
-
id: "runtime_config",
|
|
1353
|
-
label: "Runtime MCP config",
|
|
1354
|
-
ok: false,
|
|
1355
|
-
detail: `No runtime config at ${configPath}.`,
|
|
1356
|
-
repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}`
|
|
1357
|
-
});
|
|
1358
|
-
} else {
|
|
1359
|
-
const hasHaven = identity?.hosted_mcp_url ? configText.includes(identity.hosted_mcp_url) : configText.includes("haven");
|
|
1360
|
-
const signerViaNpx = configText.includes("@haven_ai/signer");
|
|
1361
|
-
const wrapperReferenced = sidecar ? configText.includes(sidecar.wrapper_path) : false;
|
|
1362
|
-
const ok = hasHaven && !signerViaNpx && (sidecar ? wrapperReferenced : true);
|
|
1363
|
-
checks.push({
|
|
1364
|
-
id: "runtime_config",
|
|
1365
|
-
label: "Runtime MCP config",
|
|
1366
|
-
ok,
|
|
1367
|
-
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${sidecar && !wrapperReferenced ? " (or references a different signer wrapper)" : ""}.`,
|
|
1368
|
-
...ok ? {} : { repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}` }
|
|
1369
|
-
});
|
|
1370
|
-
}
|
|
1371
|
-
}
|
|
1372
|
-
if (identity?.api_key && (identity.hosted_mcp_url || identity.api_url)) {
|
|
1373
|
-
const hostedUrl = identity.hosted_mcp_url ?? `${identity.api_url}/mcp`;
|
|
1374
|
-
const probe = await (deps.probeHosted ?? probeHostedMcpTools)(identity.api_key, hostedUrl, deps.fetch);
|
|
1375
|
-
checks.push({
|
|
1376
|
-
id: "hosted_mcp",
|
|
1377
|
-
label: "Hosted Haven MCP",
|
|
1378
|
-
ok: probe.status === "ok",
|
|
1379
|
-
detail: probe.status === "ok" ? `Reachable and authorized (${hostedUrl}).` : `Probe failed: ${probe.status} (${hostedUrl}).`,
|
|
1380
|
-
...probe.status === "ok" ? {} : {
|
|
1381
|
-
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."
|
|
1382
|
-
}
|
|
1383
|
-
});
|
|
1384
|
-
} else {
|
|
1385
|
-
checks.push({
|
|
1386
|
-
id: "hosted_mcp",
|
|
1387
|
-
label: "Hosted Haven MCP",
|
|
1388
|
-
ok: false,
|
|
1389
|
-
detail: "No stored API key / hosted MCP URL to probe with.",
|
|
1390
|
-
repair: `Re-run the full setup: ${RERUN} --setup <token>.`
|
|
1391
|
-
});
|
|
1392
|
-
}
|
|
1393
|
-
if (sidecar && directory) {
|
|
1394
|
-
const consent = await getLocalSignerConsentStatus(join(directory, "signer.json"));
|
|
1395
|
-
if (!consent.acknowledged) {
|
|
1396
|
-
checks.push({
|
|
1397
|
-
id: "signer_process",
|
|
1398
|
-
label: "Signer stdio handshake",
|
|
1399
|
-
ok: false,
|
|
1400
|
-
detail: "The local-tools consent is not acknowledged, so the signer refuses to start (by design).",
|
|
1401
|
-
repair: `Run: ${RERUN} --ack-local-tools --setup <token> (or re-run your original setup command with --ack-local-tools).`
|
|
1402
|
-
});
|
|
1403
|
-
} else {
|
|
1404
|
-
const probe = await (deps.probeSignerTools ?? probeLocalMcpTools)(
|
|
1405
|
-
sidecar.wrapper_path,
|
|
1406
|
-
[],
|
|
1407
|
-
MCP_RUNTIME_MANIFEST.requiredSignerTools
|
|
1408
|
-
);
|
|
1409
|
-
const experimental = probe.capabilities?.experimental ?? probe.capabilities;
|
|
1410
|
-
const compat = experimental?.["haven/signer-compatibility"];
|
|
1411
|
-
signerCapabilities = compat ? { "haven/signer-compatibility": compat } : void 0;
|
|
1412
|
-
const compatDetail = compat ? ` Compat: x402 expected-context v${JSON.stringify(compat.x402_expected_context_versions ?? "?")}.` : "";
|
|
1413
|
-
checks.push({
|
|
1414
|
-
id: "signer_process",
|
|
1415
|
-
label: "Signer stdio handshake",
|
|
1416
|
-
ok: probe.status === "ok",
|
|
1417
|
-
detail: probe.status === "ok" ? `Signer started, listed ${probe.toolNames?.length ?? 0} tools${probe.serverInfo?.version ? ` (v${probe.serverInfo.version})` : ""}.${compatDetail}` : `Handshake failed: ${probe.status}.`,
|
|
1418
|
-
...probe.status === "ok" ? {} : { repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}` }
|
|
1419
|
-
});
|
|
2253
|
+
localRuntimeInstall = await prepareRuntimeForLocalMcp(input, deps);
|
|
2254
|
+
} catch (err) {
|
|
2255
|
+
localRuntimeError = err;
|
|
1420
2256
|
}
|
|
1421
|
-
} else if (directory) {
|
|
1422
|
-
checks.push({
|
|
1423
|
-
id: "signer_process",
|
|
1424
|
-
label: "Signer stdio handshake",
|
|
1425
|
-
ok: false,
|
|
1426
|
-
detail: "Skipped \u2014 no prepared signer runtime to probe.",
|
|
1427
|
-
repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}`
|
|
1428
|
-
});
|
|
1429
2257
|
}
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
id: "restart",
|
|
1433
|
-
label: "Runtime restart",
|
|
1434
|
-
ok: true,
|
|
1435
|
-
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."
|
|
1436
|
-
});
|
|
1437
|
-
return {
|
|
1438
|
-
version: 1,
|
|
1439
|
-
ok: checks.every((check) => check.ok),
|
|
1440
|
-
runtime: input.runtime,
|
|
1441
|
-
credentialDirectory: directory,
|
|
1442
|
-
checks,
|
|
1443
|
-
...signerCapabilities ? { signerCapabilities } : {}
|
|
1444
|
-
};
|
|
1445
|
-
}
|
|
1446
|
-
async function runRepair(input, deps = {}) {
|
|
1447
|
-
const homeDir = deps.homeDir ?? homedir();
|
|
1448
|
-
const messages = [];
|
|
1449
|
-
const { directory, note } = await discoverCredentialDirectory(homeDir, input.credentialsDir);
|
|
1450
|
-
if (note) messages.push(`Note: ${note}`);
|
|
1451
|
-
if (!directory) {
|
|
2258
|
+
if (localRuntimeError) {
|
|
2259
|
+
const errorCode2 = localRuntimePrepareErrorCode(localRuntimeError);
|
|
1452
2260
|
return {
|
|
1453
|
-
|
|
1454
|
-
|
|
2261
|
+
runtime,
|
|
2262
|
+
runtimeMcpMode: "local_stdio",
|
|
2263
|
+
hostedMcpConfigured: false,
|
|
2264
|
+
localSignerConfigured: false,
|
|
2265
|
+
localMcpConfigured: false,
|
|
2266
|
+
probeResult: errorCode2 === "local_mcp_unsupported_node_version" ? "local_stdio_mcp_unsupported_node_version" : "local_stdio_mcp_runtime_install_failed",
|
|
2267
|
+
restartRequired: true,
|
|
2268
|
+
nextUserAction: nextAction(runtime, profile.restartMode, errorCode2),
|
|
2269
|
+
errorCode: errorCode2,
|
|
2270
|
+
configTarget: profile.label,
|
|
2271
|
+
signerAcknowledged: signerConsent?.acknowledged,
|
|
2272
|
+
localMcpAcknowledged: localMcpConsent?.acknowledged,
|
|
2273
|
+
activationCommand: void 0,
|
|
2274
|
+
messages: [
|
|
2275
|
+
...consentMessages,
|
|
2276
|
+
`Could not prepare local Haven MCP runtime: ${localRuntimeError instanceof Error ? localRuntimeError.message : String(localRuntimeError)}`
|
|
2277
|
+
]
|
|
1455
2278
|
};
|
|
1456
2279
|
}
|
|
1457
|
-
let
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
} catch {
|
|
1461
|
-
return { ok: false, messages: ["identity.json is unreadable \u2014 re-run the full setup with a fresh token."] };
|
|
1462
|
-
}
|
|
1463
|
-
if (!identity.api_key || !(identity.hosted_mcp_url || identity.api_url)) {
|
|
1464
|
-
return { ok: false, messages: ["identity.json lacks the stored API key / hosted URL \u2014 re-run the full setup."] };
|
|
1465
|
-
}
|
|
1466
|
-
const configPath = runtimeConfigPathFor(input.runtime, homeDir);
|
|
1467
|
-
if (configPath) {
|
|
2280
|
+
let signerCommand;
|
|
2281
|
+
if (!localRuntime) {
|
|
2282
|
+
progress("Getting the signer ready\u2026");
|
|
1468
2283
|
try {
|
|
1469
|
-
const
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
2284
|
+
const signerRuntime = await prepareSignerForRuntime(input, deps);
|
|
2285
|
+
signerCommand = { command: signerRuntime.command, args: signerRuntime.args };
|
|
2286
|
+
consentMessages.push(...signerRuntime.messages);
|
|
2287
|
+
} catch (err) {
|
|
2288
|
+
return {
|
|
2289
|
+
runtime,
|
|
2290
|
+
runtimeMcpMode: "hosted_plus_signer",
|
|
2291
|
+
hostedMcpConfigured: false,
|
|
2292
|
+
localSignerConfigured: false,
|
|
2293
|
+
localMcpConfigured: false,
|
|
2294
|
+
probeResult: "signer_runtime_install_failed",
|
|
2295
|
+
restartRequired: false,
|
|
2296
|
+
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",
|
|
2297
|
+
errorCode: "signer_runtime_install_failed",
|
|
2298
|
+
configTarget: profile.label,
|
|
2299
|
+
signerAcknowledged: signerConsent?.acknowledged,
|
|
2300
|
+
localMcpAcknowledged: localMcpConsent?.acknowledged,
|
|
2301
|
+
activationCommand: void 0,
|
|
2302
|
+
signerRuntimePrepared: false,
|
|
2303
|
+
messages: [
|
|
2304
|
+
...consentMessages,
|
|
2305
|
+
`Could not pre-install the local Haven signer: ${err instanceof Error ? err.message : String(err)}`,
|
|
2306
|
+
"No runtime configuration was written (fail-closed): a config pointing at an uninstalled signer looks wired but cannot start.",
|
|
2307
|
+
"Re-run `npx @haven_ai/connect@alpha` to retry the setup."
|
|
2308
|
+
]
|
|
2309
|
+
};
|
|
1480
2310
|
}
|
|
1481
2311
|
}
|
|
1482
|
-
const
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
}
|
|
1514
|
-
});
|
|
1515
|
-
|
|
1516
|
-
// src/api.ts
|
|
1517
|
-
function createConnectApiClient(baseUrl, fetchImpl = fetch) {
|
|
1518
|
-
const root = baseUrl.replace(/\/+$/, "");
|
|
1519
|
-
return {
|
|
1520
|
-
resolveSetup: (input) => request(fetchImpl, `${root}/agent-connection-setups/resolve`, {
|
|
1521
|
-
method: "POST",
|
|
1522
|
-
body: JSON.stringify({
|
|
1523
|
-
setup_token: input.setupToken,
|
|
1524
|
-
connector_version: input.connectorVersion,
|
|
1525
|
-
runtime: input.runtime
|
|
1526
|
-
})
|
|
1527
|
-
}),
|
|
1528
|
-
registerSetup: (input) => request(fetchImpl, `${root}/agent-connection-setups/register`, {
|
|
1529
|
-
method: "POST",
|
|
1530
|
-
body: JSON.stringify({
|
|
1531
|
-
setup_token: input.setupToken,
|
|
1532
|
-
challenge_id: input.challengeId,
|
|
1533
|
-
delegate_address: input.delegateAddress,
|
|
1534
|
-
proof_signature: input.proofSignature,
|
|
1535
|
-
api_key_hash: input.apiKeyHash,
|
|
1536
|
-
api_key_prefix: input.apiKeyPrefix,
|
|
1537
|
-
runtime: input.runtime,
|
|
1538
|
-
connector_version: input.connectorVersion,
|
|
1539
|
-
connector_context: input.connectorContext,
|
|
1540
|
-
install_capabilities: input.installCapabilities && {
|
|
1541
|
-
can_write_runtime_config: input.installCapabilities.canWriteRuntimeConfig,
|
|
1542
|
-
restart_required: input.installCapabilities.restartRequired
|
|
1543
|
-
}
|
|
1544
|
-
})
|
|
1545
|
-
}),
|
|
1546
|
-
getConnectorStatus: (setupId, apiKey) => request(fetchImpl, `${root}/agent-connection-setups/${encodeURIComponent(setupId)}/connector-status`, {
|
|
1547
|
-
method: "GET",
|
|
1548
|
-
headers: { Authorization: `Bearer ${apiKey}` }
|
|
1549
|
-
}),
|
|
1550
|
-
updateInstallStatus: async (setupId, apiKey, input) => {
|
|
1551
|
-
await request(fetchImpl, `${root}/agent-connection-setups/${encodeURIComponent(setupId)}/install-status`, {
|
|
1552
|
-
method: "POST",
|
|
1553
|
-
headers: { Authorization: `Bearer ${apiKey}` },
|
|
1554
|
-
body: JSON.stringify({
|
|
1555
|
-
runtime: input.runtime,
|
|
1556
|
-
connector_version: input.connectorVersion,
|
|
1557
|
-
runtime_mcp_mode: input.runtimeMcpMode,
|
|
1558
|
-
hosted_mcp_configured: input.hostedMcpConfigured,
|
|
1559
|
-
local_signer_configured: input.localSignerConfigured,
|
|
1560
|
-
local_mcp_configured: input.localMcpConfigured,
|
|
1561
|
-
credential_files_written: input.credentialFilesWritten,
|
|
1562
|
-
signer_acknowledged: input.signerAcknowledged,
|
|
1563
|
-
local_mcp_acknowledged: input.localMcpAcknowledged,
|
|
1564
|
-
activation_command_available: input.activationCommandAvailable,
|
|
1565
|
-
skill_installed: input.skillInstalled,
|
|
1566
|
-
probe_result: input.probeResult,
|
|
1567
|
-
restart_required: input.restartRequired,
|
|
1568
|
-
next_user_action: input.nextUserAction,
|
|
1569
|
-
error_code: input.errorCode ?? null,
|
|
1570
|
-
environment_label: input.environmentLabel
|
|
1571
|
-
})
|
|
2312
|
+
const signerRuntimePrepared = localRuntime ? void 0 : signerCommand !== void 0;
|
|
2313
|
+
progress("Setting up your Haven tools\u2026");
|
|
2314
|
+
const configResult = localRuntime ? runtime === "claude-code" ? await configureClaudeCode(deps, localRuntimeInstall?.command ?? "", input.serverName) : await writeRuntimeConfig({
|
|
2315
|
+
runtime,
|
|
2316
|
+
hostedMcpUrl: input.hostedMcpUrl,
|
|
2317
|
+
apiKey: input.apiKey,
|
|
2318
|
+
identityPath: input.identityPath,
|
|
2319
|
+
signerPath: input.signerPath,
|
|
2320
|
+
serverName: input.serverName,
|
|
2321
|
+
credentialDirectory: input.credentialDirectory,
|
|
2322
|
+
localMcpCommand: localRuntimeInstall?.command,
|
|
2323
|
+
signerCommand,
|
|
2324
|
+
homeDir: deps.homeDir,
|
|
2325
|
+
mode: "local"
|
|
2326
|
+
}) : await writeHostedRuntimeConfig(deps, { ...input, runtime }, signerCommand);
|
|
2327
|
+
if (deps.onRuntimeConfigured) {
|
|
2328
|
+
const signerCredentialOnDisk = await probeLocalSignerCredential(input.signerPath);
|
|
2329
|
+
const earlyLocalMcpOk = configResult.runtimeMcpMode === "local_stdio" && configResult.localMcpConfigured && signerCredentialOnDisk && Boolean(localMcpConsent?.acknowledged);
|
|
2330
|
+
const earlySignerOk = configResult.runtimeMcpMode === "local_stdio" ? earlyLocalMcpOk : configResult.signerConfigured && signerCredentialOnDisk && Boolean(signerConsent?.acknowledged);
|
|
2331
|
+
try {
|
|
2332
|
+
await deps.onRuntimeConfigured({
|
|
2333
|
+
runtime,
|
|
2334
|
+
runtimeMcpMode: configResult.runtimeMcpMode,
|
|
2335
|
+
hostedMcpConfigured: configResult.hostedConfigured,
|
|
2336
|
+
localSignerConfigured: earlySignerOk,
|
|
2337
|
+
localMcpConfigured: earlyLocalMcpOk,
|
|
2338
|
+
signerAcknowledged: signerConsent?.acknowledged,
|
|
2339
|
+
localMcpAcknowledged: localMcpConsent?.acknowledged,
|
|
2340
|
+
restartRequired: configResult.restartRequired || restartRequiredForRuntime(runtime, deps.env),
|
|
2341
|
+
nextUserAction: nextAction(runtime, profile.restartMode, configResult.errorCode),
|
|
2342
|
+
errorCode: configResult.errorCode
|
|
1572
2343
|
});
|
|
2344
|
+
} catch {
|
|
1573
2345
|
}
|
|
1574
|
-
};
|
|
1575
|
-
}
|
|
1576
|
-
var ConnectRequestError = class extends Error {
|
|
1577
|
-
constructor(message, status) {
|
|
1578
|
-
super(message);
|
|
1579
|
-
this.status = status;
|
|
1580
|
-
this.name = "ConnectRequestError";
|
|
1581
|
-
}
|
|
1582
|
-
status;
|
|
1583
|
-
};
|
|
1584
|
-
async function request(fetchImpl, url, init) {
|
|
1585
|
-
const response = await fetchImpl(url, {
|
|
1586
|
-
...init,
|
|
1587
|
-
headers: {
|
|
1588
|
-
"Content-Type": "application/json",
|
|
1589
|
-
...init.headers ?? {}
|
|
1590
|
-
}
|
|
1591
|
-
});
|
|
1592
|
-
const text = await response.text();
|
|
1593
|
-
const body = text ? JSON.parse(text) : null;
|
|
1594
|
-
if (!response.ok) {
|
|
1595
|
-
const message = body?.error ?? body?.message ?? `${response.status} ${response.statusText}`;
|
|
1596
|
-
throw new ConnectRequestError(`Haven setup request failed: ${message}`, response.status);
|
|
1597
|
-
}
|
|
1598
|
-
return body;
|
|
1599
|
-
}
|
|
1600
|
-
function generateDelegateKey() {
|
|
1601
|
-
return delegateKeyFromPrivateKey(Wallet.createRandom().privateKey);
|
|
1602
|
-
}
|
|
1603
|
-
function delegateKeyFromPrivateKey(privateKey) {
|
|
1604
|
-
const wallet = new Wallet(privateKey);
|
|
1605
|
-
return {
|
|
1606
|
-
privateKey: wallet.privateKey,
|
|
1607
|
-
address: wallet.address,
|
|
1608
|
-
signChallenge: (message) => wallet.signMessage(message)
|
|
1609
|
-
};
|
|
1610
|
-
}
|
|
1611
|
-
function generateAgentApiKey() {
|
|
1612
|
-
return `sk_agent_${crypto.randomBytes(24).toString("hex")}`;
|
|
1613
|
-
}
|
|
1614
|
-
function hashAgentApiKey(apiKey) {
|
|
1615
|
-
return crypto.createHash("sha256").update(apiKey).digest("hex");
|
|
1616
|
-
}
|
|
1617
|
-
function agentApiKeyPrefix(apiKey) {
|
|
1618
|
-
return apiKey.slice(0, 12);
|
|
1619
|
-
}
|
|
1620
|
-
|
|
1621
|
-
// src/redact.ts
|
|
1622
|
-
var API_KEY_RE = /sk_agent_[A-Za-z0-9]+/g;
|
|
1623
|
-
var PRIVATE_KEY_RE = /0x[0-9a-fA-F]{64}/g;
|
|
1624
|
-
function redactSecrets(value) {
|
|
1625
|
-
return value.replace(API_KEY_RE, "sk_agent_[redacted]").replace(PRIVATE_KEY_RE, "0x[redacted-private-key]");
|
|
1626
|
-
}
|
|
1627
|
-
function shortAddress(address) {
|
|
1628
|
-
if (!/^0x[0-9a-fA-F]{40}$/.test(address)) return address;
|
|
1629
|
-
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
|
1630
|
-
}
|
|
1631
|
-
async function preflightCredentialStorage(input = {}) {
|
|
1632
|
-
const directory = defaultCredentialRoot(input.baseDir);
|
|
1633
|
-
await mkdir(directory, { recursive: true, mode: 448 });
|
|
1634
|
-
await restrictPermissions(directory, 448, input.warn);
|
|
1635
|
-
const probePath = join(directory, `.haven-connect-preflight-${crypto.randomBytes(8).toString("hex")}`);
|
|
1636
|
-
try {
|
|
1637
|
-
await writeOwnerOnlyJson(probePath, { ok: true }, input.warn);
|
|
1638
|
-
} finally {
|
|
1639
|
-
await rm(probePath, { force: true }).catch(() => void 0);
|
|
1640
|
-
}
|
|
1641
|
-
return directory;
|
|
1642
|
-
}
|
|
1643
|
-
async function writeCredentialFiles(input) {
|
|
1644
|
-
const directory = defaultAgentDirectory(input.agentId, input.baseDir);
|
|
1645
|
-
await mkdir(directory, { recursive: true, mode: 448 });
|
|
1646
|
-
await restrictPermissions(directory, 448, input.warn);
|
|
1647
|
-
const identityPath = join(directory, "identity.json");
|
|
1648
|
-
const signerPath = join(directory, "signer.json");
|
|
1649
|
-
const agentPath = join(directory, "agent.json");
|
|
1650
|
-
await assertDoesNotExist(identityPath);
|
|
1651
|
-
await assertDoesNotExist(signerPath);
|
|
1652
|
-
await assertDoesNotExist(agentPath);
|
|
1653
|
-
await writeOwnerOnlyJson(
|
|
1654
|
-
signerPath,
|
|
1655
|
-
{
|
|
1656
|
-
delegate_key: input.delegateKey,
|
|
1657
|
-
delegate_address: input.delegateAddress,
|
|
1658
|
-
agent_id: input.agentId,
|
|
1659
|
-
safe_address: input.safeAddress,
|
|
1660
|
-
chain_id: input.chainId,
|
|
1661
|
-
network: input.network,
|
|
1662
|
-
x402_binding_signer: input.x402BindingSigner,
|
|
1663
|
-
note: "Local signer credential. Haven backend never receives this private key."
|
|
1664
|
-
},
|
|
1665
|
-
input.warn
|
|
1666
|
-
);
|
|
1667
|
-
try {
|
|
1668
|
-
await writeOwnerOnlyJson(
|
|
1669
|
-
identityPath,
|
|
1670
|
-
{
|
|
1671
|
-
api_key: input.apiKey,
|
|
1672
|
-
agent_id: input.agentId,
|
|
1673
|
-
safe_address: input.safeAddress,
|
|
1674
|
-
chain_id: input.chainId,
|
|
1675
|
-
network: input.network,
|
|
1676
|
-
api_url: input.apiUrl,
|
|
1677
|
-
hosted_mcp_url: input.hostedMcpUrl,
|
|
1678
|
-
agent_budget: input.agentBudget,
|
|
1679
|
-
note: "Haven API key identifies the agent only. It cannot spend without the local signer key and on-chain Haven wallet rules."
|
|
1680
|
-
},
|
|
1681
|
-
input.warn
|
|
1682
|
-
);
|
|
1683
|
-
} catch (err) {
|
|
1684
|
-
await rm(signerPath, { force: true }).catch(() => void 0);
|
|
1685
|
-
throw err;
|
|
1686
2346
|
}
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
throw err;
|
|
1733
|
-
}
|
|
1734
|
-
throw new Error(`Refusing to overwrite existing Haven credential file: ${path}`);
|
|
2347
|
+
progress("Almost there \u2014 just confirming everything connects\u2026");
|
|
2348
|
+
const localProbePromise = configResult.runtimeMcpMode === "local_stdio" && localRuntimeInstall ? runLocalMcpProbe(localRuntimeInstall, deps) : Promise.resolve(void 0);
|
|
2349
|
+
const signerProbePromise = configResult.runtimeMcpMode !== "local_stdio" && signerCommand ? (deps.probeSignerTools ?? probeLocalMcpTools)(
|
|
2350
|
+
signerCommand.command,
|
|
2351
|
+
signerCommand.args,
|
|
2352
|
+
MCP_RUNTIME_MANIFEST.requiredSignerTools
|
|
2353
|
+
) : Promise.resolve(void 0);
|
|
2354
|
+
const [hostedProbe, signerCredentialReady, localMcpProbe, signerProbe] = await Promise.all([
|
|
2355
|
+
configResult.hostedConfigured ? probeHostedMcpTools(input.apiKey, input.hostedMcpUrl, deps.fetch) : Promise.resolve({ status: "bad_response" }),
|
|
2356
|
+
probeLocalSignerCredential(input.signerPath),
|
|
2357
|
+
localProbePromise,
|
|
2358
|
+
signerProbePromise
|
|
2359
|
+
]);
|
|
2360
|
+
const hostedOk = configResult.hostedConfigured && hostedProbe.status === "ok";
|
|
2361
|
+
const localMcpOk = configResult.runtimeMcpMode === "local_stdio" && configResult.localMcpConfigured && signerCredentialReady && Boolean(localMcpConsent?.acknowledged) && localMcpProbe?.status === "ok";
|
|
2362
|
+
const signerOk = configResult.runtimeMcpMode === "local_stdio" ? localMcpOk : configResult.signerConfigured && signerCredentialReady && Boolean(signerConsent?.acknowledged) && // #1587: no handshake, no green. A signer command that was registered
|
|
2363
|
+
// but not probed (manual topology) keeps the old semantics.
|
|
2364
|
+
(signerProbe === void 0 || signerProbe.status === "ok");
|
|
2365
|
+
const restartRequired = configResult.restartRequired || restartRequiredForRuntime(runtime, deps.env);
|
|
2366
|
+
const errorCode = configResult.errorCode ?? (configResult.runtimeMcpMode === "local_stdio" ? localMcpErrorCode(signerCredentialReady, localMcpConsent, localMcpProbe?.status) : hostedMcpErrorCode(configResult.hostedConfigured, hostedProbe.status) ?? signerConsentErrorCode(signerCredentialReady, signerConsent) ?? signerProbeErrorCode(signerProbe));
|
|
2367
|
+
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."] : [];
|
|
2368
|
+
const signerProbeMessages = signerProbe ? signerProbe.status === "ok" ? ["Verified local Haven signer with a stdio handshake."] : [
|
|
2369
|
+
`Local Haven signer handshake failed: ${signerProbe.status}.`,
|
|
2370
|
+
"Re-run `npx @haven_ai/connect@alpha` to repair the signer setup."
|
|
2371
|
+
] : [];
|
|
2372
|
+
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."] : [];
|
|
2373
|
+
const skillInstall = !configResult.errorCode ? await installSkillForRuntime(runtime, { homeDir: deps.homeDir, env: deps.env }) : void 0;
|
|
2374
|
+
return {
|
|
2375
|
+
runtime,
|
|
2376
|
+
runtimeMcpMode: configResult.runtimeMcpMode,
|
|
2377
|
+
hostedMcpConfigured: hostedOk,
|
|
2378
|
+
localSignerConfigured: signerOk,
|
|
2379
|
+
localMcpConfigured: localMcpOk,
|
|
2380
|
+
probeResult: buildProbeResult(configResult.runtimeMcpMode, configResult.hostedConfigured, hostedProbe.status, signerOk, localMcpOk, localMcpProbe?.status),
|
|
2381
|
+
restartRequired,
|
|
2382
|
+
nextUserAction: nextAction(runtime, profile.restartMode, errorCode),
|
|
2383
|
+
errorCode,
|
|
2384
|
+
configTarget: configResult.target,
|
|
2385
|
+
signerAcknowledged: signerConsent?.acknowledged,
|
|
2386
|
+
localMcpAcknowledged: localMcpConsent?.acknowledged,
|
|
2387
|
+
activationCommand: configResult.activationCommand,
|
|
2388
|
+
skillInstalled: skillInstall?.installed,
|
|
2389
|
+
signerRuntimePrepared,
|
|
2390
|
+
messages: [...consentMessages, ...localRuntimeInstall?.messages ?? [], ...configResult.messages, ...hostedProbeMessages, ...signerProbeMessages, ...localProbeMessages, ...skillInstall?.messages ?? []]
|
|
2391
|
+
};
|
|
1735
2392
|
}
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
);
|
|
1743
|
-
}
|
|
2393
|
+
function runtimeInstallCapabilities(runtime, env = process.env) {
|
|
2394
|
+
const profile = runtimeProfile(runtime, env);
|
|
2395
|
+
return {
|
|
2396
|
+
canWriteRuntimeConfig: profile.canWriteRuntimeConfig,
|
|
2397
|
+
restartRequired: restartRequiredForRuntime(runtime, env)
|
|
2398
|
+
};
|
|
1744
2399
|
}
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
2400
|
+
async function configureClaudeCode(deps, localMcpCommand, serverName) {
|
|
2401
|
+
const runCommand = deps.runCommand ?? defaultRunCommand;
|
|
2402
|
+
const serverJson = JSON.stringify({
|
|
2403
|
+
type: "stdio",
|
|
2404
|
+
command: localMcpCommand,
|
|
2405
|
+
args: [],
|
|
2406
|
+
env: {}
|
|
2407
|
+
});
|
|
1749
2408
|
try {
|
|
1750
|
-
|
|
1751
|
-
const
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
2409
|
+
if (!localMcpCommand) throw new Error("local MCP wrapper command is required");
|
|
2410
|
+
const names = serverNamesFor(serverName);
|
|
2411
|
+
await runCommand("claude", ["mcp", "remove", names.hosted]).catch(() => void 0);
|
|
2412
|
+
await runCommand("claude", ["mcp", "remove", names.signer]).catch(() => void 0);
|
|
2413
|
+
await runCommand("claude", ["mcp", "add-json", names.hosted, serverJson, "--scope", "user"]).catch(async () => {
|
|
2414
|
+
await runCommand("claude", ["mcp", "add", names.hosted, "--scope", "user", "--", localMcpCommand]);
|
|
1755
2415
|
});
|
|
2416
|
+
const verified = await runCommand("claude", ["mcp", "get", names.hosted]).then(() => true).catch(() => false);
|
|
1756
2417
|
return {
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
2418
|
+
hostedConfigured: false,
|
|
2419
|
+
signerConfigured: true,
|
|
2420
|
+
localMcpConfigured: true,
|
|
2421
|
+
runtimeMcpMode: "local_stdio",
|
|
2422
|
+
target: "Claude Code MCP config",
|
|
2423
|
+
changed: true,
|
|
2424
|
+
restartRequired: true,
|
|
2425
|
+
messages: [
|
|
2426
|
+
"Updated local Haven MCP entry with Claude Code.",
|
|
2427
|
+
...verified ? ["Verified Claude Code MCP entry."] : []
|
|
2428
|
+
]
|
|
1760
2429
|
};
|
|
1761
2430
|
} catch (err) {
|
|
1762
2431
|
return {
|
|
1763
|
-
|
|
1764
|
-
|
|
2432
|
+
hostedConfigured: false,
|
|
2433
|
+
signerConfigured: false,
|
|
2434
|
+
localMcpConfigured: false,
|
|
2435
|
+
runtimeMcpMode: "local_stdio",
|
|
2436
|
+
target: "Claude Code MCP config",
|
|
2437
|
+
changed: false,
|
|
2438
|
+
restartRequired: true,
|
|
2439
|
+
messages: [
|
|
2440
|
+
`Could not update Claude Code MCP config: ${err instanceof Error ? err.message : String(err)}`,
|
|
2441
|
+
"Install Claude Code or rerun the Haven setup command inside a Claude Code terminal."
|
|
2442
|
+
],
|
|
2443
|
+
errorCode: "claude_code_config_failed"
|
|
1765
2444
|
};
|
|
1766
2445
|
}
|
|
1767
2446
|
}
|
|
1768
|
-
async function
|
|
2447
|
+
async function writeHostedRuntimeConfig(deps, input, signerCommand) {
|
|
2448
|
+
if (input.runtime === "claude-code") {
|
|
2449
|
+
return configureClaudeCodeHosted(deps, input, signerCommand);
|
|
2450
|
+
}
|
|
2451
|
+
return writeRuntimeConfig({
|
|
2452
|
+
runtime: input.runtime,
|
|
2453
|
+
hostedMcpUrl: input.hostedMcpUrl,
|
|
2454
|
+
apiKey: input.apiKey,
|
|
2455
|
+
identityPath: input.identityPath,
|
|
2456
|
+
signerPath: input.signerPath,
|
|
2457
|
+
serverName: input.serverName,
|
|
2458
|
+
credentialDirectory: input.credentialDirectory,
|
|
2459
|
+
signerCommand,
|
|
2460
|
+
homeDir: deps.homeDir,
|
|
2461
|
+
mode: "hosted"
|
|
2462
|
+
});
|
|
2463
|
+
}
|
|
2464
|
+
async function configureClaudeCodeHosted(deps, input, signerCommand) {
|
|
2465
|
+
const runCommand = deps.runCommand ?? defaultRunCommand;
|
|
2466
|
+
const hostedJson = JSON.stringify({
|
|
2467
|
+
type: "http",
|
|
2468
|
+
url: input.hostedMcpUrl,
|
|
2469
|
+
headers: { Authorization: `Bearer ${input.apiKey}` }
|
|
2470
|
+
});
|
|
2471
|
+
const signerJson = JSON.stringify({
|
|
2472
|
+
type: "stdio",
|
|
2473
|
+
command: signerCommand?.command ?? "npx",
|
|
2474
|
+
args: signerCommand?.args ?? ["-y", signerPackageSpec(), "--credentials", input.signerPath],
|
|
2475
|
+
env: {}
|
|
2476
|
+
});
|
|
1769
2477
|
try {
|
|
1770
|
-
const
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
2478
|
+
const names = serverNamesFor(input.serverName);
|
|
2479
|
+
await runCommand("claude", ["mcp", "remove", names.hosted]).catch(() => void 0);
|
|
2480
|
+
await runCommand("claude", ["mcp", "remove", names.signer]).catch(() => void 0);
|
|
2481
|
+
await runCommand("claude", ["mcp", "add-json", names.hosted, hostedJson, "--scope", "user"]);
|
|
2482
|
+
await runCommand("claude", ["mcp", "add-json", names.signer, signerJson, "--scope", "user"]);
|
|
2483
|
+
const verified = await runCommand("claude", ["mcp", "get", names.hosted]).then(() => true).catch(() => false);
|
|
1776
2484
|
return {
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
2485
|
+
hostedConfigured: true,
|
|
2486
|
+
signerConfigured: true,
|
|
2487
|
+
localMcpConfigured: false,
|
|
2488
|
+
runtimeMcpMode: "hosted_plus_signer",
|
|
2489
|
+
target: "Claude Code MCP config",
|
|
2490
|
+
changed: true,
|
|
2491
|
+
restartRequired: true,
|
|
2492
|
+
messages: [
|
|
2493
|
+
"Updated hosted Haven MCP and local signer entries with Claude Code.",
|
|
2494
|
+
...verified ? ["Verified Claude Code MCP entry."] : []
|
|
2495
|
+
]
|
|
1780
2496
|
};
|
|
1781
2497
|
} catch (err) {
|
|
1782
2498
|
return {
|
|
1783
|
-
|
|
1784
|
-
|
|
2499
|
+
hostedConfigured: false,
|
|
2500
|
+
signerConfigured: false,
|
|
2501
|
+
localMcpConfigured: false,
|
|
2502
|
+
runtimeMcpMode: "hosted_plus_signer",
|
|
2503
|
+
target: "Claude Code MCP config",
|
|
2504
|
+
changed: false,
|
|
2505
|
+
restartRequired: true,
|
|
2506
|
+
messages: [
|
|
2507
|
+
`Could not update Claude Code MCP config: ${err instanceof Error ? err.message : String(err)}`,
|
|
2508
|
+
"Install Claude Code or rerun the Haven setup command inside a Claude Code terminal."
|
|
2509
|
+
],
|
|
2510
|
+
errorCode: "claude_code_config_failed"
|
|
1785
2511
|
};
|
|
1786
2512
|
}
|
|
1787
2513
|
}
|
|
1788
|
-
function
|
|
1789
|
-
|
|
2514
|
+
async function defaultRunCommand(command, args) {
|
|
2515
|
+
await execFileAsync3(command, args, { timeout: 1e4 });
|
|
1790
2516
|
}
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
2517
|
+
function buildProbeResult(mode, hostedConfigured, hostedStatus, signerReady, localMcpReady, localMcpProbeStatus) {
|
|
2518
|
+
if (mode === "local_stdio") {
|
|
2519
|
+
if (localMcpReady) return "local_stdio_mcp_ready";
|
|
2520
|
+
return localMcpProbeStatus ? `local_stdio_mcp_${localMcpProbeStatus}` : "local_stdio_mcp_unavailable";
|
|
2521
|
+
}
|
|
2522
|
+
const hostedPart = hostedConfigured ? `hosted_${hostedStatus}` : "hosted_not_configured";
|
|
2523
|
+
const signerPart = signerReady ? "local_signer_ready" : "local_signer_unavailable";
|
|
2524
|
+
return `${hostedPart}_${signerPart}`.slice(0, 120);
|
|
2525
|
+
}
|
|
2526
|
+
async function resolveLocalMcpConsent(input, messages) {
|
|
2527
|
+
if (input.ackLocalTools || input.ackSigner) {
|
|
2528
|
+
const status = await acknowledgeLocalMcpConsent(input.identityPath, input.signerPath, (message) => messages.push(message));
|
|
2529
|
+
if (status.acknowledged) {
|
|
2530
|
+
messages.push("Prepared the local Haven tools acknowledgement.");
|
|
2531
|
+
} else {
|
|
2532
|
+
messages.push("Local Haven tools acknowledgement still needs attention.");
|
|
1796
2533
|
}
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
{
|
|
1801
|
-
apiKey: credentials.apiKey,
|
|
1802
|
-
apiUrl: credentials.apiUrl,
|
|
1803
|
-
agentId: credentials.agentId,
|
|
1804
|
-
safeAddress: credentials.safeAddress,
|
|
1805
|
-
delegateAddress: credentials.delegateAddress,
|
|
1806
|
-
chainId: credentials.chainId,
|
|
1807
|
-
allowanceSummary: credentials.allowanceSummary
|
|
1808
|
-
},
|
|
1809
|
-
registeredToolNames()
|
|
1810
|
-
);
|
|
2534
|
+
return status;
|
|
2535
|
+
}
|
|
2536
|
+
return getLocalMcpConsentStatus(input.identityPath, input.signerPath);
|
|
1811
2537
|
}
|
|
1812
|
-
async function
|
|
1813
|
-
|
|
1814
|
-
const
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
2538
|
+
async function resolveSignerConsent(input, messages) {
|
|
2539
|
+
if (input.ackSigner || input.ackLocalTools) {
|
|
2540
|
+
const status = await acknowledgeLocalSignerConsent(input.signerPath, (message) => messages.push(message));
|
|
2541
|
+
if (status.acknowledged) {
|
|
2542
|
+
messages.push("Prepared the local Haven signer acknowledgement.");
|
|
2543
|
+
} else {
|
|
2544
|
+
messages.push("Local Haven signer acknowledgement still needs attention.");
|
|
2545
|
+
}
|
|
2546
|
+
return status;
|
|
1818
2547
|
}
|
|
2548
|
+
return getLocalSignerConsentStatus(input.signerPath);
|
|
1819
2549
|
}
|
|
1820
|
-
function
|
|
1821
|
-
|
|
1822
|
-
if (
|
|
2550
|
+
function signerConsentErrorCode(signerCredentialReady, signerConsent) {
|
|
2551
|
+
if (!signerCredentialReady) return "local_signer_credential_unavailable";
|
|
2552
|
+
if (!signerConsent?.acknowledged) return "local_signer_ack_required";
|
|
2553
|
+
return void 0;
|
|
1823
2554
|
}
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
if (await installedRuntimeMatches2(runtimeDirectory, cliPath)) {
|
|
1855
|
-
messages.push(`Using existing local Haven MCP runtime ${mcpPackageSpec()}.`);
|
|
1856
|
-
} else {
|
|
1857
|
-
await installRuntimePackages2(runtimeDirectory, npmCacheDirectory, deps);
|
|
1858
|
-
messages.push(`Installed local Haven MCP runtime ${mcpPackageSpec()}.`);
|
|
1859
|
-
}
|
|
1860
|
-
await assertFileExists2(cliPath, "local Haven MCP CLI");
|
|
1861
|
-
const wrapperPath = join(input.credentialDirectory, "bin", "haven-mcp");
|
|
1862
|
-
await writeWrapper2({
|
|
1863
|
-
wrapperPath,
|
|
1864
|
-
cliPath,
|
|
2555
|
+
function signerProbeErrorCode(probe) {
|
|
2556
|
+
if (!probe || probe.status === "ok") return void 0;
|
|
2557
|
+
return `local_signer_probe_${probe.status}`;
|
|
2558
|
+
}
|
|
2559
|
+
function hostedMcpErrorCode(hostedConfigured, hostedProbeStatus) {
|
|
2560
|
+
if (!hostedConfigured || hostedProbeStatus === "ok") return void 0;
|
|
2561
|
+
return `hosted_mcp_probe_${hostedProbeStatus}`;
|
|
2562
|
+
}
|
|
2563
|
+
function localMcpErrorCode(signerCredentialReady, localMcpConsent, localMcpProbeStatus) {
|
|
2564
|
+
if (!signerCredentialReady) return "local_signer_credential_unavailable";
|
|
2565
|
+
if (!localMcpConsent?.acknowledged) return "local_mcp_ack_required";
|
|
2566
|
+
if (localMcpProbeStatus && localMcpProbeStatus !== "ok") return `local_mcp_probe_${localMcpProbeStatus}`;
|
|
2567
|
+
return void 0;
|
|
2568
|
+
}
|
|
2569
|
+
function nextAction(runtime, restartMode, errorCode) {
|
|
2570
|
+
if (errorCode) return "return_to_haven_for_wallet_approval_then_finish_runtime_setup";
|
|
2571
|
+
if (restartMode === "hot-reload") return "return_to_haven_for_wallet_approval";
|
|
2572
|
+
if (runtime === "codex-cli" || runtime === "codex-desktop") return "return_to_haven_for_wallet_approval_then_restart_codex";
|
|
2573
|
+
if (runtime === "claude-code") return "return_to_haven_for_wallet_approval_then_restart_claude_code";
|
|
2574
|
+
if (restartMode === "restart-app") return "return_to_haven_for_wallet_approval_then_restart_app";
|
|
2575
|
+
if (restartMode === "restart-session") return "return_to_haven_for_wallet_approval_then_restart_agent_session";
|
|
2576
|
+
return "return_to_haven_for_wallet_approval_then_configure_runtime";
|
|
2577
|
+
}
|
|
2578
|
+
function supportsLocalMcp(runtime) {
|
|
2579
|
+
return runtime === "codex-cli" || runtime === "codex-desktop" || runtime === "claude-code";
|
|
2580
|
+
}
|
|
2581
|
+
async function prepareRuntimeForLocalMcp(input, deps) {
|
|
2582
|
+
const prepare = deps.prepareLocalMcpRuntime ?? ((runtimeInput) => prepareLocalMcpRuntime(runtimeInput, { runCommand: deps.runCommand, onProgress: deps.onProgress }));
|
|
2583
|
+
return prepare({
|
|
2584
|
+
credentialDirectory: input.credentialDirectory,
|
|
1865
2585
|
identityPath: input.identityPath,
|
|
1866
|
-
signerPath: input.signerPath
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
path: join(input.credentialDirectory, "mcp-runtime.json"),
|
|
1870
|
-
wrapperPath,
|
|
1871
|
-
runtimeDirectory,
|
|
1872
|
-
npmCacheDirectory,
|
|
1873
|
-
cliPath
|
|
2586
|
+
signerPath: input.signerPath,
|
|
2587
|
+
homeDir: deps.homeDir,
|
|
2588
|
+
serverName: input.serverName
|
|
1874
2589
|
});
|
|
1875
|
-
messages.push(`Prepared stable local Haven MCP wrapper: ${wrapperPath}`);
|
|
1876
|
-
return {
|
|
1877
|
-
command: wrapperPath,
|
|
1878
|
-
args: [],
|
|
1879
|
-
wrapperPath,
|
|
1880
|
-
runtimeDirectory,
|
|
1881
|
-
npmCacheDirectory,
|
|
1882
|
-
cliPath,
|
|
1883
|
-
messages
|
|
1884
|
-
};
|
|
1885
2590
|
}
|
|
1886
|
-
function
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
2591
|
+
async function prepareSignerForRuntime(input, deps) {
|
|
2592
|
+
const prepare = deps.prepareSignerRuntime ?? ((runtimeInput) => (
|
|
2593
|
+
// onProgress threaded through on purpose (#1586 review): without it the
|
|
2594
|
+
// install heartbeat was dead code in production and the console still
|
|
2595
|
+
// went silent for the whole cold install — the exact symptom the issue
|
|
2596
|
+
// set out to remove, at a longer timeout.
|
|
2597
|
+
prepareSignerRuntime(runtimeInput, { runCommand: deps.runCommand, onProgress: deps.onProgress })
|
|
2598
|
+
));
|
|
2599
|
+
return prepare({
|
|
2600
|
+
credentialDirectory: input.credentialDirectory,
|
|
2601
|
+
signerPath: input.signerPath,
|
|
2602
|
+
homeDir: deps.homeDir,
|
|
2603
|
+
serverName: input.serverName
|
|
2604
|
+
});
|
|
1890
2605
|
}
|
|
1891
|
-
async function
|
|
1892
|
-
const
|
|
1893
|
-
const baseArgs = [
|
|
1894
|
-
"install",
|
|
1895
|
-
"--prefix",
|
|
1896
|
-
runtimeDirectory,
|
|
1897
|
-
"--no-audit",
|
|
1898
|
-
"--no-fund",
|
|
1899
|
-
"--omit=dev",
|
|
1900
|
-
"--prefer-offline",
|
|
1901
|
-
mcpPackageSpec(),
|
|
1902
|
-
sdkPackageSpec()
|
|
1903
|
-
];
|
|
1904
|
-
const run = async (args) => {
|
|
1905
|
-
const startedAt = Date.now();
|
|
1906
|
-
const heartbeat = setInterval(() => {
|
|
1907
|
-
const seconds = Math.round((Date.now() - startedAt) / 1e3);
|
|
1908
|
-
onProgress?.(`Still installing the local Haven MCP runtime\u2026 (${seconds}s \u2014 a cold cache can take several minutes)`);
|
|
1909
|
-
}, SIGNER_INSTALL_HEARTBEAT_MS);
|
|
1910
|
-
heartbeat.unref?.();
|
|
1911
|
-
try {
|
|
1912
|
-
if (runCommand) await runCommand("npm", args);
|
|
1913
|
-
else await execFileAsync2("npm", args, { timeout: SIGNER_INSTALL_TIMEOUT_MS, maxBuffer: 1024 * 1024 });
|
|
1914
|
-
} finally {
|
|
1915
|
-
clearInterval(heartbeat);
|
|
1916
|
-
}
|
|
1917
|
-
};
|
|
2606
|
+
async function runLocalMcpProbe(runtimeInstall, deps) {
|
|
2607
|
+
const probe = deps.probeLocalMcpTools ?? probeLocalMcpTools;
|
|
1918
2608
|
try {
|
|
1919
|
-
await
|
|
2609
|
+
return await probe(runtimeInstall.command, runtimeInstall.args, MCP_RUNTIME_MANIFEST.requiredTools);
|
|
1920
2610
|
} catch {
|
|
1921
|
-
|
|
1922
|
-
await run([...baseArgs, "--cache", npmCacheDirectory]);
|
|
1923
|
-
} catch (err) {
|
|
1924
|
-
throw new Error(`Could not install local Haven MCP runtime ${mcpPackageSpec()}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1925
|
-
}
|
|
2611
|
+
return { status: "process_error" };
|
|
1926
2612
|
}
|
|
1927
2613
|
}
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
const [mcpPackage, sdkPackage] = await Promise.all([
|
|
1932
|
-
readPackageJson2(join(runtimeDirectory, "node_modules", "@haven_ai", "mcp", "package.json")),
|
|
1933
|
-
readPackageJson2(join(runtimeDirectory, "node_modules", "@haven_ai", "sdk", "package.json"))
|
|
1934
|
-
]);
|
|
1935
|
-
return mcpPackage.version === MCP_RUNTIME_MANIFEST.mcpVersion && sdkPackage.version === MCP_RUNTIME_MANIFEST.sdkVersion;
|
|
1936
|
-
} catch {
|
|
1937
|
-
return false;
|
|
2614
|
+
function localRuntimePrepareErrorCode(err) {
|
|
2615
|
+
if (err && typeof err === "object" && "code" in err && err.code === "local_mcp_unsupported_node_version") {
|
|
2616
|
+
return "local_mcp_unsupported_node_version";
|
|
1938
2617
|
}
|
|
2618
|
+
return "local_mcp_runtime_install_failed";
|
|
1939
2619
|
}
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
2620
|
+
var execFileAsync3;
|
|
2621
|
+
var init_runtime_install = __esm({
|
|
2622
|
+
"src/runtime-install.ts"() {
|
|
2623
|
+
init_config_writers();
|
|
2624
|
+
init_server_names();
|
|
2625
|
+
init_local_mcp_consent();
|
|
2626
|
+
init_probes();
|
|
2627
|
+
init_local_mcp_runtime();
|
|
2628
|
+
init_signer_runtime();
|
|
2629
|
+
init_runtime_manifest();
|
|
2630
|
+
init_skill_install();
|
|
2631
|
+
init_runtime_registry();
|
|
2632
|
+
init_signer_consent();
|
|
2633
|
+
execFileAsync3 = promisify(execFile);
|
|
2634
|
+
}
|
|
2635
|
+
});
|
|
2636
|
+
|
|
2637
|
+
// src/tombstone.ts
|
|
2638
|
+
var tombstone_exports = {};
|
|
2639
|
+
__export(tombstone_exports, {
|
|
2640
|
+
TOMBSTONE_FILENAME: () => TOMBSTONE_FILENAME,
|
|
2641
|
+
TOMBSTONE_MARKER: () => TOMBSTONE_MARKER,
|
|
2642
|
+
readAgentTombstone: () => readAgentTombstone,
|
|
2643
|
+
writeAgentTombstone: () => writeAgentTombstone
|
|
2644
|
+
});
|
|
2645
|
+
function tombstoneScript(info) {
|
|
2646
|
+
const lines = [
|
|
2647
|
+
`${TOMBSTONE_MARKER}: this Haven agent was retired.`,
|
|
1949
2648
|
"",
|
|
1950
|
-
`
|
|
1951
|
-
`
|
|
1952
|
-
`
|
|
2649
|
+
` agent: ${info.agent_id}`,
|
|
2650
|
+
` retired at: ${info.retired_at}`,
|
|
2651
|
+
` reason: ${info.reason}`,
|
|
2652
|
+
...info.replaced_by ? [` replaced by: ${info.replaced_by}`] : [],
|
|
1953
2653
|
"",
|
|
1954
|
-
"
|
|
1955
|
-
"
|
|
1956
|
-
"
|
|
2654
|
+
"This process is running with a wiring snapshot that predates the",
|
|
2655
|
+
"retirement \u2014 it loaded its MCP config at startup and has kept it since.",
|
|
2656
|
+
"Restart THIS host to pick up the current wiring. If several long-lived",
|
|
2657
|
+
"hosts are running (a gateway, a TUI worker, an editor), restart EVERY",
|
|
2658
|
+
"one of them: each holds the snapshot from its own start time, so after",
|
|
2659
|
+
"a chain of recreations each can be parked on a DIFFERENT old agent.",
|
|
1957
2660
|
"",
|
|
1958
|
-
"
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
"
|
|
2661
|
+
"Then verify with: npx @haven_ai/connect@alpha --doctor --runtime <runtime>"
|
|
2662
|
+
];
|
|
2663
|
+
return [
|
|
2664
|
+
"#!/usr/bin/env node",
|
|
2665
|
+
`// ${TOMBSTONE_MARKER} \u2014 written by @haven_ai/connect (#1681). Safe to delete`,
|
|
2666
|
+
"// once every long-lived MCP host on this machine has been restarted.",
|
|
2667
|
+
`process.stderr.write(${JSON.stringify(lines.join("\n") + "\n")})`,
|
|
2668
|
+
"process.exit(1)",
|
|
1962
2669
|
""
|
|
1963
2670
|
].join("\n");
|
|
1964
|
-
await writeFile(input.wrapperPath, source, { mode: 448 });
|
|
1965
|
-
await chmod(input.wrapperPath, 448).catch(() => void 0);
|
|
1966
2671
|
}
|
|
1967
|
-
async function
|
|
1968
|
-
const
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
2672
|
+
async function writeAgentTombstone(input) {
|
|
2673
|
+
const dirStat = await stat(input.directory).catch(() => null);
|
|
2674
|
+
if (!dirStat?.isDirectory()) {
|
|
2675
|
+
throw new Error(`Not a directory: ${input.directory} \u2014 nothing to tombstone.`);
|
|
2676
|
+
}
|
|
2677
|
+
const info = {
|
|
2678
|
+
// reason / replaced_by are persisted to disk and re-emitted to the host's
|
|
2679
|
+
// MCP stderr log on EVERY stale probe, potentially for months — redact
|
|
2680
|
+
// like every other output path, at the write layer so any future caller
|
|
2681
|
+
// inherits it. (#1681 review, finding 1)
|
|
2682
|
+
agent_id: input.agentId,
|
|
2683
|
+
retired_at: input.retiredAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
2684
|
+
reason: redactSecrets(input.reason),
|
|
2685
|
+
...input.replacedBy ? { replaced_by: redactSecrets(input.replacedBy) } : {}
|
|
1978
2686
|
};
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
2687
|
+
const binDir = join(input.directory, "bin");
|
|
2688
|
+
await mkdir(binDir, { recursive: true });
|
|
2689
|
+
const wrapperPath = join(binDir, "haven-signer.mjs");
|
|
2690
|
+
await writeFile(wrapperPath, tombstoneScript(info), "utf8");
|
|
2691
|
+
await chmod(wrapperPath, 493);
|
|
2692
|
+
await writeFile(join(input.directory, TOMBSTONE_FILENAME), JSON.stringify(info, null, 2) + "\n", "utf8");
|
|
2693
|
+
return info;
|
|
2694
|
+
}
|
|
2695
|
+
async function readAgentTombstone(directory) {
|
|
1984
2696
|
try {
|
|
1985
|
-
await
|
|
2697
|
+
const parsed = JSON.parse(await readFile(join(directory, TOMBSTONE_FILENAME), "utf8"));
|
|
2698
|
+
if (typeof parsed?.agent_id !== "string") return null;
|
|
2699
|
+
return parsed;
|
|
1986
2700
|
} catch {
|
|
1987
|
-
|
|
2701
|
+
return null;
|
|
1988
2702
|
}
|
|
1989
2703
|
}
|
|
2704
|
+
var TOMBSTONE_FILENAME, TOMBSTONE_MARKER;
|
|
2705
|
+
var init_tombstone = __esm({
|
|
2706
|
+
"src/tombstone.ts"() {
|
|
2707
|
+
init_redact();
|
|
2708
|
+
TOMBSTONE_FILENAME = "TOMBSTONE.json";
|
|
2709
|
+
TOMBSTONE_MARKER = "HAVEN-TOMBSTONE";
|
|
2710
|
+
}
|
|
2711
|
+
});
|
|
1990
2712
|
|
|
1991
|
-
// src/
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2713
|
+
// src/rekey.ts
|
|
2714
|
+
var rekey_exports = {};
|
|
2715
|
+
__export(rekey_exports, {
|
|
2716
|
+
finishRekey: () => finishRekey,
|
|
2717
|
+
startRekey: () => startRekey
|
|
2718
|
+
});
|
|
2719
|
+
async function startRekey(options, deps = {}) {
|
|
2720
|
+
const now = deps.now ?? (() => Date.now());
|
|
2721
|
+
const stored = await readStoredCredentials(
|
|
2722
|
+
options.serverName,
|
|
2723
|
+
options.agentId,
|
|
2724
|
+
options.credentialsDir
|
|
2725
|
+
);
|
|
2726
|
+
const api = (deps.createApi ?? ((url) => createConnectApiClient(url)))(stored.apiUrl);
|
|
2727
|
+
const identity = await probeIdentity(api, stored.apiKey, "current");
|
|
2728
|
+
assertRekeyable(identity, stored);
|
|
2729
|
+
const key = (deps.generateKey ?? generateDelegateKey)();
|
|
2730
|
+
const startedAt = new Date(now()).toISOString();
|
|
2731
|
+
const expiresAt = new Date(now() + REKEY_PENDING_TTL_MS).toISOString();
|
|
2732
|
+
await writeRekeyPending(stored.directory, {
|
|
2733
|
+
agent_id: stored.agentId,
|
|
2734
|
+
new_delegate_address: key.address,
|
|
2735
|
+
new_delegate_key: key.privateKey,
|
|
2736
|
+
started_at: startedAt,
|
|
2737
|
+
expires_at: expiresAt
|
|
2738
|
+
});
|
|
2739
|
+
const finishCommand = [
|
|
2740
|
+
"npx @haven_ai/connect@alpha --rekey-finish",
|
|
2741
|
+
options.serverName ? `--name ${options.serverName}` : void 0,
|
|
2742
|
+
"--api-key <the key the dashboard showed you>",
|
|
2743
|
+
options.runtime ? `--runtime ${options.runtime}` : "--runtime <your runtime>"
|
|
2744
|
+
].filter(Boolean).join(" ");
|
|
2745
|
+
return {
|
|
2746
|
+
started: true,
|
|
2747
|
+
agentId: stored.agentId,
|
|
2748
|
+
directory: stored.directory,
|
|
2749
|
+
newDelegateAddress: key.address,
|
|
2750
|
+
expiresAt,
|
|
2751
|
+
messages: [
|
|
2752
|
+
`New signing key generated on this machine for agent ${identity.name || stored.agentId}.`,
|
|
2753
|
+
"",
|
|
2754
|
+
` New signing address: ${key.address}`,
|
|
2755
|
+
"",
|
|
2756
|
+
"The private half stays here \u2014 Haven never receives it, and there is no way to move it",
|
|
2757
|
+
"between machines. That is what keeps the account non-custodial.",
|
|
2758
|
+
"",
|
|
2759
|
+
'Next, on the Haven agent page: choose "Replace signing key" and paste that address.',
|
|
2760
|
+
"When it finishes it shows a new API key ONCE. Come back here and run:",
|
|
2761
|
+
"",
|
|
2762
|
+
` ${finishCommand}`,
|
|
2763
|
+
"",
|
|
2764
|
+
`Nothing has changed yet \u2014 the agent keeps working on its old key until you finish.`,
|
|
2765
|
+
`This pending re-key expires ${expiresAt}.`
|
|
2766
|
+
]
|
|
2767
|
+
};
|
|
2768
|
+
}
|
|
2769
|
+
async function finishRekey(options, deps = {}) {
|
|
2770
|
+
const now = deps.now ?? (() => Date.now());
|
|
2771
|
+
if (!options.newApiKey) {
|
|
2772
|
+
throw new Error("--rekey-finish needs --api-key <key> \u2014 the one the Haven agent page showed once.");
|
|
2773
|
+
}
|
|
2774
|
+
const stored = await readStoredCredentials(
|
|
2775
|
+
options.serverName,
|
|
2776
|
+
options.agentId,
|
|
2777
|
+
options.credentialsDir
|
|
2778
|
+
);
|
|
2779
|
+
const pending = await readRekeyPending(stored.directory, now());
|
|
2780
|
+
if (pending.agent_id !== stored.agentId) {
|
|
2781
|
+
throw new Error(
|
|
2782
|
+
`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.`
|
|
2783
|
+
);
|
|
2784
|
+
}
|
|
2785
|
+
const api = (deps.createApi ?? ((url) => createConnectApiClient(url)))(stored.apiUrl);
|
|
2786
|
+
const identity = await probeIdentity(api, options.newApiKey, "new");
|
|
2787
|
+
if (identity.id !== stored.agentId) {
|
|
2788
|
+
throw new Error(
|
|
2789
|
+
`That API key belongs to agent ${identity.id}, not ${stored.agentId}. Nothing was changed.`
|
|
2790
|
+
);
|
|
2791
|
+
}
|
|
2792
|
+
const onChain = (identity.delegate_address ?? "").toLowerCase();
|
|
2793
|
+
const expected = pending.new_delegate_address.toLowerCase();
|
|
2794
|
+
if (onChain !== expected) {
|
|
2795
|
+
throw new Error(
|
|
2796
|
+
`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.`
|
|
2797
|
+
);
|
|
2798
|
+
}
|
|
2799
|
+
await rewriteCredentialFiles({
|
|
2800
|
+
baseDir: options.credentialsDir,
|
|
2801
|
+
agentId: stored.agentId,
|
|
2802
|
+
serverName: options.serverName,
|
|
2803
|
+
apiKey: options.newApiKey,
|
|
2804
|
+
delegateKey: pending.new_delegate_key,
|
|
2805
|
+
delegateAddress: pending.new_delegate_address,
|
|
2806
|
+
safeAddress: stored.safeAddress ?? identity.safe_address ?? void 0,
|
|
2807
|
+
chainId: stored.chainId ?? identity.chain_id ?? void 0,
|
|
2808
|
+
network: stored.network,
|
|
2809
|
+
agentBudget: stored.agentBudget,
|
|
2810
|
+
apiUrl: stored.apiUrl,
|
|
2811
|
+
hostedMcpUrl: stored.hostedMcpUrl,
|
|
2812
|
+
x402BindingSigner: stored.x402BindingSigner,
|
|
2813
|
+
warn: deps.log
|
|
2814
|
+
});
|
|
2815
|
+
await clearRekeyPending(stored.directory);
|
|
2816
|
+
const names = serverNamesFor(options.serverName);
|
|
2817
|
+
const messages = [
|
|
2818
|
+
`Agent ${identity.name || stored.agentId} is now on its new signing key.`,
|
|
2819
|
+
` Signing address: ${pending.new_delegate_address}`,
|
|
2820
|
+
` Credentials: ${stored.directory} (rewritten in place)`,
|
|
2821
|
+
` MCP servers: ${names.hosted} / ${names.signer} (unchanged names)`
|
|
2822
|
+
];
|
|
2823
|
+
let configRewritten = false;
|
|
2824
|
+
if (options.runtime) {
|
|
2825
|
+
const prepared = await (deps.prepareSigner ?? prepareSignerRuntime)(
|
|
2826
|
+
{
|
|
2827
|
+
credentialDirectory: stored.directory,
|
|
2828
|
+
signerPath: `${stored.directory}/signer.json`,
|
|
2829
|
+
homeDir: options.homeDir
|
|
2830
|
+
},
|
|
2831
|
+
{ runCommand: deps.runCommand }
|
|
2832
|
+
);
|
|
2833
|
+
const result = await (deps.writeConfig ?? writeHostedRuntimeConfig)(
|
|
2834
|
+
{ runCommand: deps.runCommand, homeDir: options.homeDir },
|
|
2835
|
+
{
|
|
2836
|
+
runtime: options.runtime,
|
|
2837
|
+
hostedMcpUrl: stored.hostedMcpUrl,
|
|
2838
|
+
apiKey: options.newApiKey,
|
|
2839
|
+
identityPath: `${stored.directory}/identity.json`,
|
|
2840
|
+
signerPath: `${stored.directory}/signer.json`,
|
|
2841
|
+
credentialDirectory: stored.directory,
|
|
2842
|
+
serverName: options.serverName
|
|
2843
|
+
},
|
|
2844
|
+
{ command: prepared.command, args: prepared.args }
|
|
2845
|
+
);
|
|
2846
|
+
configRewritten = result.hostedConfigured;
|
|
2847
|
+
messages.push(` Config: ${result.target}`);
|
|
2848
|
+
messages.push(...result.messages.map((line) => ` ${line}`));
|
|
2849
|
+
if (!configRewritten) {
|
|
2850
|
+
messages.push(
|
|
2851
|
+
"",
|
|
2852
|
+
"WARNING: the MCP config was NOT updated, so it still carries the OLD API key and every",
|
|
2853
|
+
`wired host will fail with 401. Fix the cause above and re-run with --runtime ${options.runtime},`,
|
|
2854
|
+
"or update the config by hand. The credential files on disk are already on the new key."
|
|
2002
2855
|
);
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2856
|
+
}
|
|
2857
|
+
} else {
|
|
2858
|
+
messages.push(
|
|
2859
|
+
"",
|
|
2860
|
+
"NOTE: no --runtime was given, so the MCP config still carries the OLD API key and every",
|
|
2861
|
+
"wired host will fail with 401. Re-run with --runtime <name> to rewrite it."
|
|
2862
|
+
);
|
|
2863
|
+
}
|
|
2864
|
+
return {
|
|
2865
|
+
finished: true,
|
|
2866
|
+
agentId: stored.agentId,
|
|
2867
|
+
directory: stored.directory,
|
|
2868
|
+
newDelegateAddress: pending.new_delegate_address,
|
|
2869
|
+
serverNames: { hosted: names.hosted, signer: names.signer },
|
|
2870
|
+
configRewritten,
|
|
2871
|
+
messages
|
|
2872
|
+
};
|
|
2873
|
+
}
|
|
2874
|
+
async function probeIdentity(api, apiKey, which) {
|
|
2875
|
+
try {
|
|
2876
|
+
return await api.getAgentIdentity(apiKey);
|
|
2877
|
+
} catch (err) {
|
|
2878
|
+
const status = err?.status;
|
|
2879
|
+
if (status === 401 || status === 403) {
|
|
2880
|
+
throw new Error(
|
|
2881
|
+
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."
|
|
2007
2882
|
);
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2883
|
+
}
|
|
2884
|
+
throw new Error(
|
|
2885
|
+
`Could not reach Haven to check this agent (${redactSecrets(
|
|
2886
|
+
err instanceof Error ? err.message : String(err)
|
|
2887
|
+
)}). Nothing was changed.`
|
|
2888
|
+
);
|
|
2013
2889
|
}
|
|
2014
2890
|
}
|
|
2015
|
-
|
|
2891
|
+
function assertRekeyable(identity, stored) {
|
|
2892
|
+
if (identity.id !== stored.agentId) {
|
|
2893
|
+
throw new Error(
|
|
2894
|
+
`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.`
|
|
2895
|
+
);
|
|
2896
|
+
}
|
|
2897
|
+
if (identity.execution_rail === "legacy") {
|
|
2898
|
+
throw new Error(
|
|
2899
|
+
`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.`
|
|
2900
|
+
);
|
|
2901
|
+
}
|
|
2902
|
+
if (identity.status === "revoked") {
|
|
2903
|
+
throw new Error(
|
|
2904
|
+
`Agent ${identity.name || identity.id} is revoked. Re-keying would hand a revoked agent fresh credentials \u2014 create a new agent instead.`
|
|
2905
|
+
);
|
|
2906
|
+
}
|
|
2907
|
+
}
|
|
2908
|
+
var init_rekey = __esm({
|
|
2909
|
+
"src/rekey.ts"() {
|
|
2910
|
+
init_api();
|
|
2911
|
+
init_runtime_install();
|
|
2912
|
+
init_signer_runtime();
|
|
2913
|
+
init_key();
|
|
2914
|
+
init_redact();
|
|
2915
|
+
init_server_names();
|
|
2916
|
+
init_storage();
|
|
2917
|
+
}
|
|
2918
|
+
});
|
|
2919
|
+
|
|
2920
|
+
// src/rekey-restart.ts
|
|
2921
|
+
var rekey_restart_exports = {};
|
|
2922
|
+
__export(rekey_restart_exports, {
|
|
2923
|
+
restartGuidance: () => restartGuidance
|
|
2924
|
+
});
|
|
2925
|
+
function restartGuidance(runtime) {
|
|
2926
|
+
const profile = runtime ? BY_RUNTIME[runtime] : void 0;
|
|
2927
|
+
const lines = ["The new key is only live in a process that started after now.", ""];
|
|
2928
|
+
if (profile) {
|
|
2929
|
+
lines.push(profile.how);
|
|
2930
|
+
for (const command of profile.commands) lines.push(` ${command}`);
|
|
2931
|
+
if (profile.commands.length > 0) lines.push("");
|
|
2932
|
+
} else {
|
|
2933
|
+
lines.push(
|
|
2934
|
+
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."
|
|
2935
|
+
);
|
|
2936
|
+
lines.push("");
|
|
2937
|
+
}
|
|
2938
|
+
lines.push(SWEEP);
|
|
2939
|
+
return { commands: profile?.commands ?? [], lines };
|
|
2940
|
+
}
|
|
2941
|
+
var SWEEP, BY_RUNTIME;
|
|
2942
|
+
var init_rekey_restart = __esm({
|
|
2943
|
+
"src/rekey-restart.ts"() {
|
|
2944
|
+
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.";
|
|
2945
|
+
BY_RUNTIME = {
|
|
2946
|
+
"claude-code": {
|
|
2947
|
+
commands: ["claude --continue"],
|
|
2948
|
+
how: "Exit this Claude Code session and start a new one:"
|
|
2949
|
+
},
|
|
2950
|
+
"codex-cli": {
|
|
2951
|
+
commands: ["codex resume --last"],
|
|
2952
|
+
how: "Start a fresh Codex CLI session:"
|
|
2953
|
+
},
|
|
2954
|
+
"codex-desktop": {
|
|
2955
|
+
commands: [],
|
|
2956
|
+
how: "Quit Codex Desktop completely (not just the window) and reopen it."
|
|
2957
|
+
},
|
|
2958
|
+
"claude-desktop": {
|
|
2959
|
+
commands: [],
|
|
2960
|
+
how: "Quit Claude Desktop completely (not just the window) and reopen it."
|
|
2961
|
+
},
|
|
2962
|
+
hermes: {
|
|
2963
|
+
commands: ["systemctl --user restart hermes-gateway", "/restart"],
|
|
2964
|
+
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:"
|
|
2965
|
+
},
|
|
2966
|
+
cursor: {
|
|
2967
|
+
commands: [],
|
|
2968
|
+
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").'
|
|
2969
|
+
},
|
|
2970
|
+
vscode: {
|
|
2971
|
+
commands: [],
|
|
2972
|
+
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").'
|
|
2973
|
+
},
|
|
2974
|
+
"vscode-insiders": {
|
|
2975
|
+
commands: [],
|
|
2976
|
+
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").'
|
|
2977
|
+
}
|
|
2978
|
+
};
|
|
2979
|
+
}
|
|
2980
|
+
});
|
|
2981
|
+
|
|
2982
|
+
// src/doctor.ts
|
|
2983
|
+
var doctor_exports = {};
|
|
2984
|
+
__export(doctor_exports, {
|
|
2985
|
+
runDoctor: () => runDoctor,
|
|
2986
|
+
runRepair: () => runRepair
|
|
2987
|
+
});
|
|
2988
|
+
async function discoverCredentialDirectory(homeDir, explicit) {
|
|
2989
|
+
const root = explicit ? dirname(explicit) : join(homeDir, ".haven", "agents");
|
|
2990
|
+
let entries = [];
|
|
2016
2991
|
try {
|
|
2017
|
-
await
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2992
|
+
entries = await readdir(root);
|
|
2993
|
+
} catch {
|
|
2994
|
+
return explicit ? { directory: explicit, others: [], parkedOnly: /* @__PURE__ */ new Set() } : { others: [], parkedOnly: /* @__PURE__ */ new Set() };
|
|
2995
|
+
}
|
|
2996
|
+
const candidates = [];
|
|
2997
|
+
const tombstonedOnly = [];
|
|
2998
|
+
const parkedOnly = [];
|
|
2999
|
+
for (const entry of entries) {
|
|
3000
|
+
const directory = join(root, entry);
|
|
3001
|
+
try {
|
|
3002
|
+
const s = await stat(join(directory, "identity.json"));
|
|
3003
|
+
candidates.push({ directory, mtimeMs: s.mtimeMs });
|
|
3004
|
+
} catch {
|
|
3005
|
+
try {
|
|
3006
|
+
await stat(join(directory, TOMBSTONE_FILENAME));
|
|
3007
|
+
tombstonedOnly.push(directory);
|
|
3008
|
+
} catch {
|
|
3009
|
+
try {
|
|
3010
|
+
await stat(join(directory, REKEY_PENDING_FILENAME));
|
|
3011
|
+
parkedOnly.push(directory);
|
|
3012
|
+
} catch {
|
|
3013
|
+
}
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
}
|
|
3017
|
+
const parkedOnlySet = new Set(parkedOnly);
|
|
3018
|
+
candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
3019
|
+
if (explicit) {
|
|
2026
3020
|
return {
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
]
|
|
3021
|
+
directory: explicit,
|
|
3022
|
+
others: [...candidates.map((c) => c.directory), ...tombstonedOnly, ...parkedOnly].filter((d) => d !== explicit),
|
|
3023
|
+
parkedOnly: parkedOnlySet
|
|
2031
3024
|
};
|
|
2032
3025
|
}
|
|
3026
|
+
if (candidates.length === 0 && tombstonedOnly.length === 0 && parkedOnly.length === 0) {
|
|
3027
|
+
return { others: [], parkedOnly: parkedOnlySet };
|
|
3028
|
+
}
|
|
3029
|
+
return {
|
|
3030
|
+
directory: candidates[0]?.directory,
|
|
3031
|
+
others: [...candidates.slice(1).map((c) => c.directory), ...tombstonedOnly, ...parkedOnly],
|
|
3032
|
+
parkedOnly: parkedOnlySet
|
|
3033
|
+
};
|
|
2033
3034
|
}
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
const
|
|
2038
|
-
|
|
2039
|
-
const existing = await readFile(target, "utf8").catch(() => null);
|
|
2040
|
-
const next = upsertManagedSection(existing, codexManagedSection());
|
|
2041
|
-
if (next !== existing) {
|
|
2042
|
-
await writeFile(target, next, "utf8");
|
|
3035
|
+
function agentIsWired(configText, names, slug, identity, sidecar, isPrimary, bareOwnerExists) {
|
|
3036
|
+
if (configText === null) return isPrimary;
|
|
3037
|
+
if (slug) {
|
|
3038
|
+
for (const name of [names.hosted, names.codexHosted, names.signer, names.codexSigner]) {
|
|
3039
|
+
if (new RegExp(`(^|[."'\\s\\[])${name}(["'\\]:\\s]|$)`, "m").test(configText)) return true;
|
|
2043
3040
|
}
|
|
3041
|
+
return false;
|
|
3042
|
+
}
|
|
3043
|
+
if (sidecar?.wrapper_path && configText.includes(sidecar.wrapper_path)) return true;
|
|
3044
|
+
if (bareOwnerExists) return false;
|
|
3045
|
+
return isPrimary && Boolean(identity?.hosted_mcp_url && configText.includes(identity.hosted_mcp_url));
|
|
3046
|
+
}
|
|
3047
|
+
function rekeyPendingCheck(status, hostedDelegateAddress, runtime, slug) {
|
|
3048
|
+
const label = "Pending re-key";
|
|
3049
|
+
const nameFlag = slug ? ` --name ${slug}` : "";
|
|
3050
|
+
if (status.state === "unreadable") {
|
|
2044
3051
|
return {
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
3052
|
+
id: "rekey_pending",
|
|
3053
|
+
label,
|
|
3054
|
+
ok: false,
|
|
3055
|
+
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.`,
|
|
3056
|
+
repair: `Delete ${status.path}, then start again: ${RERUN} --rekey${nameFlag}`
|
|
2050
3057
|
};
|
|
2051
|
-
}
|
|
3058
|
+
}
|
|
3059
|
+
const started = status.startedAt ?? "an unknown time";
|
|
3060
|
+
const address = status.newDelegateAddress ?? "unknown";
|
|
3061
|
+
const completedOnHaven = hostedDelegateAddress !== void 0 && status.newDelegateAddress !== void 0 && hostedDelegateAddress.toLowerCase() === status.newDelegateAddress.toLowerCase();
|
|
3062
|
+
if (completedOnHaven) {
|
|
2052
3063
|
return {
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
3064
|
+
id: "rekey_pending",
|
|
3065
|
+
label,
|
|
3066
|
+
ok: false,
|
|
3067
|
+
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." : ""),
|
|
3068
|
+
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}`
|
|
2057
3069
|
};
|
|
2058
3070
|
}
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
${
|
|
2066
|
-
|
|
2067
|
-
}
|
|
2068
|
-
function upsertManagedSection(existing, section) {
|
|
2069
|
-
if (existing === null || existing.trim() === "") return section;
|
|
2070
|
-
const begins = markerLineIndexes(existing, CODEX_AGENTS_BEGIN_MARKER);
|
|
2071
|
-
const ends = markerLineIndexes(existing, CODEX_AGENTS_END_MARKER);
|
|
2072
|
-
if (begins.length === 1 && ends.length === 1 && ends[0] > begins[0]) {
|
|
2073
|
-
const afterEnd = ends[0] + CODEX_AGENTS_END_MARKER.length;
|
|
2074
|
-
const tail = existing.startsWith("\r\n", afterEnd) ? existing.slice(afterEnd + 2) : existing.startsWith("\n", afterEnd) ? existing.slice(afterEnd + 1) : existing.slice(afterEnd);
|
|
2075
|
-
return existing.slice(0, begins[0]) + section + tail;
|
|
2076
|
-
}
|
|
2077
|
-
if (begins.length > 0 || ends.length > 0) {
|
|
2078
|
-
throw new Error(
|
|
2079
|
-
"found a damaged Haven marker section (orphaned or duplicated markers); remove the leftover marker lines and re-run setup"
|
|
2080
|
-
);
|
|
3071
|
+
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.";
|
|
3072
|
+
if (status.state === "expired") {
|
|
3073
|
+
return {
|
|
3074
|
+
id: "rekey_pending",
|
|
3075
|
+
label,
|
|
3076
|
+
ok: false,
|
|
3077
|
+
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,
|
|
3078
|
+
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.`
|
|
3079
|
+
};
|
|
2081
3080
|
}
|
|
2082
|
-
return
|
|
3081
|
+
return {
|
|
3082
|
+
id: "rekey_pending",
|
|
3083
|
+
label,
|
|
3084
|
+
ok: true,
|
|
3085
|
+
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
|
|
3086
|
+
};
|
|
2083
3087
|
}
|
|
2084
|
-
function
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
if (at === 0 || text[at - 1] === "\n") indexes.push(at);
|
|
2090
|
-
from = at + marker.length;
|
|
3088
|
+
async function readIdentity(directory) {
|
|
3089
|
+
try {
|
|
3090
|
+
return JSON.parse(await readFile(join(directory, "identity.json"), "utf8"));
|
|
3091
|
+
} catch {
|
|
3092
|
+
return void 0;
|
|
2091
3093
|
}
|
|
2092
3094
|
}
|
|
2093
|
-
function
|
|
2094
|
-
const
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
const runtime = normalizeRuntime(input.runtime, deps.env);
|
|
2104
|
-
const profile = runtimeProfile(runtime, deps.env);
|
|
2105
|
-
const progress = deps.onProgress ?? (() => void 0);
|
|
2106
|
-
const localRuntime = input.localMcp === true && supportsLocalMcp(runtime);
|
|
2107
|
-
const consentMessages = [];
|
|
2108
|
-
const localMcpConsent = localRuntime ? await resolveLocalMcpConsent(input, consentMessages) : void 0;
|
|
2109
|
-
const signerConsent = localRuntime ? void 0 : await resolveSignerConsent(input, consentMessages);
|
|
2110
|
-
if (runtime === "other") {
|
|
2111
|
-
const signerCredentialReady2 = await probeLocalSignerCredential(input.signerPath);
|
|
2112
|
-
const signerReady = signerCredentialReady2 && signerConsent?.acknowledged;
|
|
2113
|
-
return {
|
|
2114
|
-
runtime,
|
|
2115
|
-
runtimeMcpMode: "manual",
|
|
2116
|
-
hostedMcpConfigured: false,
|
|
2117
|
-
localSignerConfigured: false,
|
|
2118
|
-
localMcpConfigured: false,
|
|
2119
|
-
probeResult: signerReady ? "manual_runtime_setup_required_local_signer_ready" : "manual_runtime_setup_required_local_signer_unavailable",
|
|
2120
|
-
restartRequired: true,
|
|
2121
|
-
nextUserAction: "return_to_haven_for_wallet_approval_then_configure_runtime",
|
|
2122
|
-
errorCode: "manual_runtime_setup_required",
|
|
2123
|
-
configTarget: "manual runtime setup",
|
|
2124
|
-
signerAcknowledged: signerConsent?.acknowledged,
|
|
2125
|
-
localMcpAcknowledged: false,
|
|
2126
|
-
messages: [
|
|
2127
|
-
...consentMessages,
|
|
2128
|
-
"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.",
|
|
2129
|
-
` identity (hosted MCP Bearer): ${input.identityPath}`,
|
|
2130
|
-
` signer (local signing key): ${input.signerPath}`,
|
|
2131
|
-
"After wallet approval, wire the runtime to Haven by reference:",
|
|
2132
|
-
` 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}`,
|
|
2133
|
-
` Fully local MCP (no hosted dependency): npx -y ${mcpPackageSpec()} --identity ${input.identityPath} --signer ${input.signerPath}`
|
|
2134
|
-
]
|
|
2135
|
-
};
|
|
3095
|
+
async function checksForAgent(entry, input, deps) {
|
|
3096
|
+
const { directory, identity, sidecar } = entry;
|
|
3097
|
+
const checks = [];
|
|
3098
|
+
let signerCapabilities;
|
|
3099
|
+
let signerFile;
|
|
3100
|
+
try {
|
|
3101
|
+
const parsed = JSON.parse(await readFile(join(directory, "signer.json"), "utf8"));
|
|
3102
|
+
signerFile = typeof parsed === "object" && parsed !== null ? parsed : void 0;
|
|
3103
|
+
} catch {
|
|
3104
|
+
signerFile = void 0;
|
|
2136
3105
|
}
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
3106
|
+
const credentialsOk = Boolean(identity?.api_key) && signerFile !== void 0;
|
|
3107
|
+
checks.push({
|
|
3108
|
+
id: "credentials",
|
|
3109
|
+
label: "Agent credentials",
|
|
3110
|
+
ok: credentialsOk,
|
|
3111
|
+
detail: credentialsOk ? `identity.json and signer.json parse (agent ${identity?.agent_id ?? "unknown"})` : "identity.json or signer.json is missing or unparseable.",
|
|
3112
|
+
...credentialsOk ? {} : { repair: `Re-run the full setup with a fresh token: ${RERUN} --setup <token>.` }
|
|
3113
|
+
});
|
|
3114
|
+
if (!sidecar) {
|
|
3115
|
+
checks.push({
|
|
3116
|
+
id: "signer_runtime",
|
|
3117
|
+
label: "Signer runtime (preinstalled wrapper)",
|
|
3118
|
+
ok: false,
|
|
3119
|
+
detail: "No signer-runtime.json sidecar \u2014 the pinned signer runtime was never prepared (or a pre-#1586 npx config).",
|
|
3120
|
+
repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}`
|
|
3121
|
+
});
|
|
3122
|
+
} else {
|
|
3123
|
+
const matches = await installedRuntimeMatches(sidecar.runtime_directory, sidecar.cli_path);
|
|
3124
|
+
const versionOk = sidecar.signer_version === MCP_RUNTIME_MANIFEST.signerVersion;
|
|
3125
|
+
const ok = matches && versionOk;
|
|
3126
|
+
checks.push({
|
|
3127
|
+
id: "signer_runtime",
|
|
3128
|
+
label: "Signer runtime (preinstalled wrapper)",
|
|
3129
|
+
ok,
|
|
3130
|
+
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 ${MCP_RUNTIME_MANIFEST.signerVersion}.` : `Runtime directory is stale or empty (${sidecar.runtime_directory}) \u2014 the CLI or package versions are missing.`,
|
|
3131
|
+
...ok ? {} : { repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}` }
|
|
3132
|
+
});
|
|
3133
|
+
}
|
|
3134
|
+
const hostedUrl = identity?.hosted_mcp_url ?? (identity?.api_url ? `${identity.api_url}/mcp` : void 0);
|
|
3135
|
+
if (identity?.api_key && hostedUrl) {
|
|
3136
|
+
const probe = await (deps.probeHosted ?? probeHostedMcpTools)(identity.api_key, hostedUrl, deps.fetch);
|
|
3137
|
+
checks.push({
|
|
3138
|
+
id: "hosted_mcp",
|
|
3139
|
+
label: "Hosted Haven MCP",
|
|
3140
|
+
ok: probe.status === "ok",
|
|
3141
|
+
detail: probe.status === "ok" ? `Reachable and authorized (${hostedUrl}).` : `Probe failed: ${probe.status} (${hostedUrl}).`,
|
|
3142
|
+
...probe.status === "ok" ? {} : {
|
|
3143
|
+
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."
|
|
3144
|
+
}
|
|
3145
|
+
});
|
|
3146
|
+
} else {
|
|
3147
|
+
checks.push({
|
|
3148
|
+
id: "hosted_mcp",
|
|
3149
|
+
label: "Hosted Haven MCP",
|
|
3150
|
+
ok: false,
|
|
3151
|
+
detail: "No stored API key / hosted MCP URL to probe with.",
|
|
3152
|
+
repair: `Re-run the full setup: ${RERUN} --setup <token>.`
|
|
3153
|
+
});
|
|
3154
|
+
}
|
|
3155
|
+
const localDelegate = typeof signerFile?.delegate_address === "string" ? signerFile.delegate_address : void 0;
|
|
3156
|
+
let hostedDelegateAddress;
|
|
3157
|
+
if (identity?.api_key && identity.api_url) {
|
|
3158
|
+
const probe = await (deps.probeHostedIdentity ?? probeHostedAgentIdentity)(
|
|
3159
|
+
identity.api_key,
|
|
3160
|
+
identity.api_url,
|
|
3161
|
+
deps.fetch
|
|
3162
|
+
);
|
|
3163
|
+
if (probe.status === "ok") hostedDelegateAddress = probe.delegateAddress;
|
|
3164
|
+
if (probe.status !== "ok") {
|
|
3165
|
+
checks.push({
|
|
3166
|
+
id: "identity_match",
|
|
3167
|
+
label: "Hosted identity matches the local signing key",
|
|
3168
|
+
ok: false,
|
|
3169
|
+
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.`,
|
|
3170
|
+
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}`
|
|
3171
|
+
});
|
|
3172
|
+
} else if (!localDelegate) {
|
|
3173
|
+
checks.push({
|
|
3174
|
+
id: "identity_match",
|
|
3175
|
+
label: "Hosted identity matches the local signing key",
|
|
3176
|
+
ok: false,
|
|
3177
|
+
detail: "signer.json holds no delegate_address to compare against the hosted identity.",
|
|
3178
|
+
repair: `Re-run the full setup with a fresh token: ${RERUN} --setup <token>.`
|
|
3179
|
+
});
|
|
3180
|
+
} else {
|
|
3181
|
+
const same = probe.delegateAddress?.toLowerCase() === localDelegate.toLowerCase();
|
|
3182
|
+
checks.push({
|
|
3183
|
+
id: "identity_match",
|
|
3184
|
+
label: "Hosted identity matches the local signing key",
|
|
3185
|
+
ok: same,
|
|
3186
|
+
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.`,
|
|
3187
|
+
...same ? {} : {
|
|
3188
|
+
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.`
|
|
3189
|
+
}
|
|
3190
|
+
});
|
|
2144
3191
|
}
|
|
2145
3192
|
}
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
runtime,
|
|
2150
|
-
runtimeMcpMode: "local_stdio",
|
|
2151
|
-
hostedMcpConfigured: false,
|
|
2152
|
-
localSignerConfigured: false,
|
|
2153
|
-
localMcpConfigured: false,
|
|
2154
|
-
probeResult: errorCode2 === "local_mcp_unsupported_node_version" ? "local_stdio_mcp_unsupported_node_version" : "local_stdio_mcp_runtime_install_failed",
|
|
2155
|
-
restartRequired: true,
|
|
2156
|
-
nextUserAction: nextAction(runtime, profile.restartMode, errorCode2),
|
|
2157
|
-
errorCode: errorCode2,
|
|
2158
|
-
configTarget: profile.label,
|
|
2159
|
-
signerAcknowledged: signerConsent?.acknowledged,
|
|
2160
|
-
localMcpAcknowledged: localMcpConsent?.acknowledged,
|
|
2161
|
-
activationCommand: void 0,
|
|
2162
|
-
messages: [
|
|
2163
|
-
...consentMessages,
|
|
2164
|
-
`Could not prepare local Haven MCP runtime: ${localRuntimeError instanceof Error ? localRuntimeError.message : String(localRuntimeError)}`
|
|
2165
|
-
]
|
|
2166
|
-
};
|
|
3193
|
+
const pending = await inspectRekeyPending(directory, deps.now?.() ?? Date.now());
|
|
3194
|
+
if (pending) {
|
|
3195
|
+
checks.push(rekeyPendingCheck(pending, hostedDelegateAddress, input.runtime, sidecar?.server_name));
|
|
2167
3196
|
}
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
"Re-run `npx @haven_ai/connect@alpha` to retry the setup."
|
|
2196
|
-
]
|
|
2197
|
-
};
|
|
3197
|
+
if (sidecar) {
|
|
3198
|
+
const consent = await getLocalSignerConsentStatus(join(directory, "signer.json"));
|
|
3199
|
+
if (!consent.acknowledged) {
|
|
3200
|
+
checks.push({
|
|
3201
|
+
id: "signer_process",
|
|
3202
|
+
label: "Signer stdio handshake",
|
|
3203
|
+
ok: false,
|
|
3204
|
+
detail: "The local-tools consent is not acknowledged, so the signer refuses to start (by design).",
|
|
3205
|
+
repair: `Run: ${RERUN} --ack-local-tools --setup <token> (or re-run your original setup command with --ack-local-tools).`
|
|
3206
|
+
});
|
|
3207
|
+
} else {
|
|
3208
|
+
const probe = await (deps.probeSignerTools ?? probeLocalMcpTools)(
|
|
3209
|
+
sidecar.wrapper_path,
|
|
3210
|
+
[],
|
|
3211
|
+
MCP_RUNTIME_MANIFEST.requiredSignerTools
|
|
3212
|
+
);
|
|
3213
|
+
const experimental = probe.capabilities?.experimental ?? probe.capabilities;
|
|
3214
|
+
const compat = experimental?.["haven/signer-compatibility"];
|
|
3215
|
+
signerCapabilities = compat ? { "haven/signer-compatibility": compat } : void 0;
|
|
3216
|
+
const compatDetail = compat ? ` Compat: x402 expected-context v${JSON.stringify(compat.x402_expected_context_versions ?? "?")}.` : "";
|
|
3217
|
+
checks.push({
|
|
3218
|
+
id: "signer_process",
|
|
3219
|
+
label: "Signer stdio handshake",
|
|
3220
|
+
ok: probe.status === "ok",
|
|
3221
|
+
detail: probe.status === "ok" ? `Signer started, listed ${probe.toolNames?.length ?? 0} tools${probe.serverInfo?.version ? ` (v${probe.serverInfo.version})` : ""}.${compatDetail}` : `Handshake failed: ${probe.status}.`,
|
|
3222
|
+
...probe.status === "ok" ? {} : { repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}` }
|
|
3223
|
+
});
|
|
2198
3224
|
}
|
|
3225
|
+
} else {
|
|
3226
|
+
checks.push({
|
|
3227
|
+
id: "signer_process",
|
|
3228
|
+
label: "Signer stdio handshake",
|
|
3229
|
+
ok: false,
|
|
3230
|
+
detail: "Skipped \u2014 no prepared signer runtime to probe.",
|
|
3231
|
+
repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}`
|
|
3232
|
+
});
|
|
2199
3233
|
}
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
signerCommand,
|
|
2211
|
-
homeDir: deps.homeDir,
|
|
2212
|
-
mode: localRuntime ? "local" : "hosted"
|
|
2213
|
-
});
|
|
2214
|
-
if (deps.onRuntimeConfigured) {
|
|
2215
|
-
const signerCredentialOnDisk = await probeLocalSignerCredential(input.signerPath);
|
|
2216
|
-
const earlyLocalMcpOk = configResult.runtimeMcpMode === "local_stdio" && configResult.localMcpConfigured && signerCredentialOnDisk && Boolean(localMcpConsent?.acknowledged);
|
|
2217
|
-
const earlySignerOk = configResult.runtimeMcpMode === "local_stdio" ? earlyLocalMcpOk : configResult.signerConfigured && signerCredentialOnDisk && Boolean(signerConsent?.acknowledged);
|
|
3234
|
+
return { checks, ...signerCapabilities ? { signerCapabilities } : {} };
|
|
3235
|
+
}
|
|
3236
|
+
async function runDoctor(input, deps = {}) {
|
|
3237
|
+
const homeDir = deps.homeDir ?? homedir();
|
|
3238
|
+
const checks = [];
|
|
3239
|
+
let signerCapabilities;
|
|
3240
|
+
const { directory, others, parkedOnly } = await discoverCredentialDirectory(homeDir, input.credentialsDir);
|
|
3241
|
+
const configPath = runtimeConfigPathFor(input.runtime, homeDir);
|
|
3242
|
+
let configText = null;
|
|
3243
|
+
if (configPath !== null) {
|
|
2218
3244
|
try {
|
|
2219
|
-
await
|
|
2220
|
-
runtime,
|
|
2221
|
-
runtimeMcpMode: configResult.runtimeMcpMode,
|
|
2222
|
-
hostedMcpConfigured: configResult.hostedConfigured,
|
|
2223
|
-
localSignerConfigured: earlySignerOk,
|
|
2224
|
-
localMcpConfigured: earlyLocalMcpOk,
|
|
2225
|
-
signerAcknowledged: signerConsent?.acknowledged,
|
|
2226
|
-
localMcpAcknowledged: localMcpConsent?.acknowledged,
|
|
2227
|
-
restartRequired: configResult.restartRequired || restartRequiredForRuntime(runtime, deps.env),
|
|
2228
|
-
nextUserAction: nextAction(runtime, profile.restartMode, configResult.errorCode),
|
|
2229
|
-
errorCode: configResult.errorCode
|
|
2230
|
-
});
|
|
3245
|
+
configText = await readFile(configPath, "utf8");
|
|
2231
3246
|
} catch {
|
|
3247
|
+
configText = null;
|
|
2232
3248
|
}
|
|
2233
3249
|
}
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
const
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
3250
|
+
const allDirectories = directory ? [directory, ...others] : others;
|
|
3251
|
+
let bareOwnerExists = false;
|
|
3252
|
+
for (const dir of allDirectories) {
|
|
3253
|
+
const sidecar = await readRuntimeSidecar(dir);
|
|
3254
|
+
if (!sidecar?.server_name && sidecar?.wrapper_path && configText?.includes(sidecar.wrapper_path)) {
|
|
3255
|
+
bareOwnerExists = true;
|
|
3256
|
+
break;
|
|
3257
|
+
}
|
|
3258
|
+
}
|
|
3259
|
+
const inventory = [];
|
|
3260
|
+
const capabilitiesByDirectory = /* @__PURE__ */ new Map();
|
|
3261
|
+
const primaryChecksById = /* @__PURE__ */ new Map();
|
|
3262
|
+
for (const dir of allDirectories) {
|
|
3263
|
+
const identity = await readIdentity(dir);
|
|
3264
|
+
const sidecar = await readRuntimeSidecar(dir);
|
|
3265
|
+
const tombstone = await readAgentTombstone(dir);
|
|
3266
|
+
const slug = sidecar?.server_name;
|
|
3267
|
+
const names = serverNamesFor(slug);
|
|
3268
|
+
const rekeyPending = await inspectRekeyPending(dir, deps.now?.() ?? Date.now());
|
|
3269
|
+
if (!identity?.api_key) {
|
|
3270
|
+
const agentId = tombstone?.agent_id ?? rekeyPending?.agentId;
|
|
3271
|
+
inventory.push({
|
|
3272
|
+
...slug ? { slug } : {},
|
|
3273
|
+
...agentId ? { agentId } : {},
|
|
3274
|
+
directory: dir,
|
|
3275
|
+
// A tombstone is a deliberate record and outranks the discovery tell:
|
|
3276
|
+
// a retired directory that also holds a parked key stays `retired`.
|
|
3277
|
+
classification: tombstone ? "retired" : parkedOnly.has(dir) ? "parked" : "orphaned",
|
|
3278
|
+
checks: rekeyPending ? [rekeyPendingCheck(rekeyPending, void 0, input.runtime, slug)] : [],
|
|
3279
|
+
...rekeyPending ? { rekeyPending } : {}
|
|
3280
|
+
});
|
|
3281
|
+
continue;
|
|
3282
|
+
}
|
|
3283
|
+
const wired = agentIsWired(configText, names, slug, identity, sidecar, dir === directory, bareOwnerExists);
|
|
3284
|
+
const entry = {
|
|
3285
|
+
...slug ? { slug } : {},
|
|
3286
|
+
...identity.agent_id ? { agentId: identity.agent_id } : {},
|
|
3287
|
+
directory: dir,
|
|
3288
|
+
classification: wired ? "wired" : "superseded",
|
|
3289
|
+
checks: [],
|
|
3290
|
+
...rekeyPending ? { rekeyPending } : {}
|
|
3291
|
+
};
|
|
3292
|
+
if (wired) {
|
|
3293
|
+
const result = await checksForAgent({ directory: dir, identity, sidecar }, input, deps);
|
|
3294
|
+
entry.checks = result.checks;
|
|
3295
|
+
capabilitiesByDirectory.set(dir, result.signerCapabilities);
|
|
3296
|
+
} else if (rekeyPending) {
|
|
3297
|
+
entry.checks = [rekeyPendingCheck(rekeyPending, void 0, input.runtime, slug)];
|
|
3298
|
+
}
|
|
3299
|
+
inventory.push(entry);
|
|
3300
|
+
}
|
|
3301
|
+
const wiredDirectories = inventory.filter((entry) => entry.classification === "wired").map((entry) => entry.directory);
|
|
3302
|
+
const primaryDirectory = input.credentialsDir ? directory : wiredDirectories.includes(directory ?? "") ? directory : wiredDirectories[0] ?? directory;
|
|
3303
|
+
if (primaryDirectory) {
|
|
3304
|
+
const primaryEntry = inventory.find((entry) => entry.directory === primaryDirectory);
|
|
3305
|
+
signerCapabilities = capabilitiesByDirectory.get(primaryDirectory);
|
|
3306
|
+
for (const check of primaryEntry?.checks ?? []) primaryChecksById.set(check.id, check);
|
|
3307
|
+
}
|
|
3308
|
+
if (!primaryDirectory) {
|
|
3309
|
+
checks.push({
|
|
3310
|
+
id: "credentials",
|
|
3311
|
+
label: "Agent credentials",
|
|
3312
|
+
ok: false,
|
|
3313
|
+
detail: "No agent credential directory with an identity.json under ~/.haven/agents.",
|
|
3314
|
+
repair: `Run the full setup once: ${RERUN} --setup <token from the Haven dashboard>.`
|
|
3315
|
+
});
|
|
3316
|
+
} else {
|
|
3317
|
+
const primaryIdentity = await readIdentity(primaryDirectory);
|
|
3318
|
+
const primarySidecar = await readRuntimeSidecar(primaryDirectory);
|
|
3319
|
+
if (!primaryChecksById.has("credentials")) {
|
|
3320
|
+
const result = await checksForAgent(
|
|
3321
|
+
{ directory: primaryDirectory, identity: primaryIdentity, sidecar: primarySidecar },
|
|
3322
|
+
input,
|
|
3323
|
+
deps
|
|
3324
|
+
);
|
|
3325
|
+
signerCapabilities = result.signerCapabilities;
|
|
3326
|
+
for (const check of result.checks) primaryChecksById.set(check.id, check);
|
|
3327
|
+
}
|
|
3328
|
+
for (const id of ["credentials", "signer_runtime"]) {
|
|
3329
|
+
const check = primaryChecksById.get(id);
|
|
3330
|
+
if (check) checks.push(check);
|
|
3331
|
+
}
|
|
3332
|
+
}
|
|
3333
|
+
if (configPath === null) {
|
|
3334
|
+
checks.push({
|
|
3335
|
+
id: "runtime_config",
|
|
3336
|
+
label: "Runtime MCP config",
|
|
3337
|
+
ok: true,
|
|
3338
|
+
detail: `Runtime '${input.runtime}' has no file-based config the connector owns (CLI-managed) \u2014 skipping the file check.`
|
|
3339
|
+
});
|
|
3340
|
+
} else if (configText === null) {
|
|
3341
|
+
checks.push({
|
|
3342
|
+
id: "runtime_config",
|
|
3343
|
+
label: "Runtime MCP config",
|
|
3344
|
+
ok: false,
|
|
3345
|
+
detail: `No runtime config at ${configPath}.`,
|
|
3346
|
+
repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}`
|
|
3347
|
+
});
|
|
3348
|
+
} else {
|
|
3349
|
+
const primaryIdentity = await readIdentity(primaryDirectory ?? "");
|
|
3350
|
+
const primarySidecar = primaryDirectory ? await readRuntimeSidecar(primaryDirectory) : null;
|
|
3351
|
+
const hasHaven = primaryIdentity?.hosted_mcp_url ? configText.includes(primaryIdentity.hosted_mcp_url) : configText.includes("haven");
|
|
3352
|
+
const signerViaNpx = configText.includes("@haven_ai/signer");
|
|
3353
|
+
const wrapperReferenced = primarySidecar ? configText.includes(primarySidecar.wrapper_path) : false;
|
|
3354
|
+
const ok = hasHaven && !signerViaNpx && (primarySidecar ? wrapperReferenced : true);
|
|
3355
|
+
checks.push({
|
|
3356
|
+
id: "runtime_config",
|
|
3357
|
+
label: "Runtime MCP config",
|
|
3358
|
+
ok,
|
|
3359
|
+
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)" : ""}.`,
|
|
3360
|
+
...ok ? {} : { repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}` }
|
|
3361
|
+
});
|
|
3362
|
+
}
|
|
3363
|
+
for (const id of ["hosted_mcp", "identity_match", "rekey_pending"]) {
|
|
3364
|
+
const check = primaryChecksById.get(id);
|
|
3365
|
+
if (check) checks.push(check);
|
|
3366
|
+
}
|
|
3367
|
+
const otherEntries = inventory.filter((entry) => entry.directory !== primaryDirectory);
|
|
3368
|
+
if (otherEntries.length > 0) {
|
|
3369
|
+
const live = [];
|
|
3370
|
+
const revoked = [];
|
|
3371
|
+
const unverifiable = [];
|
|
3372
|
+
const retired = [];
|
|
3373
|
+
for (const entry of otherEntries) {
|
|
3374
|
+
const identity = await readIdentity(entry.directory);
|
|
3375
|
+
const tombstone = await readAgentTombstone(entry.directory);
|
|
3376
|
+
const otherAgent = entry.agentId ?? basename(entry.directory);
|
|
3377
|
+
const otherUrl = identity?.hosted_mcp_url ?? (identity?.api_url ? `${identity.api_url}/mcp` : void 0);
|
|
3378
|
+
if (!identity?.api_key || !otherUrl) {
|
|
3379
|
+
if (tombstone) retired.push(`${otherAgent} (retired ${tombstone.retired_at})`);
|
|
3380
|
+
else unverifiable.push(`${otherAgent} (no stored key/URL to probe)`);
|
|
3381
|
+
continue;
|
|
3382
|
+
}
|
|
3383
|
+
const suffix = tombstone ? " [tombstoned \u2014 key material still present]" : "";
|
|
3384
|
+
const probe = await (deps.probeHosted ?? probeHostedMcpTools)(identity.api_key, otherUrl, deps.fetch);
|
|
3385
|
+
if (probe.status === "ok") live.push({ label: `${otherAgent}${suffix}`, entry });
|
|
3386
|
+
else if (probe.status === "unauthorized") revoked.push(`${otherAgent}${suffix}`);
|
|
3387
|
+
else unverifiable.push(`${otherAgent} (${probe.status})${suffix}`);
|
|
3388
|
+
}
|
|
3389
|
+
const parts = [];
|
|
3390
|
+
if (live.length > 0) parts.push(`STILL SPEND-CAPABLE: ${live.map((item) => item.label).join(", ")}`);
|
|
3391
|
+
if (revoked.length > 0) parts.push(`already revoked: ${revoked.join(", ")}`);
|
|
3392
|
+
if (retired.length > 0) parts.push(`tombstoned (keys removed): ${retired.join(", ")}`);
|
|
3393
|
+
if (unverifiable.length > 0) parts.push(`could not verify: ${unverifiable.join(", ")}`);
|
|
3394
|
+
const supersededLive = live.filter((item) => item.entry.classification !== "wired").map((item) => item.label);
|
|
3395
|
+
checks.push({
|
|
3396
|
+
id: "superseded_agents",
|
|
3397
|
+
label: "Superseded agent credentials",
|
|
3398
|
+
ok: supersededLive.length === 0,
|
|
3399
|
+
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("; ")}.`,
|
|
3400
|
+
...supersededLive.length > 0 ? {
|
|
3401
|
+
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.`
|
|
3402
|
+
} : {}
|
|
3403
|
+
});
|
|
3404
|
+
}
|
|
3405
|
+
const parkedElsewhere = inventory.filter((entry) => entry.directory !== primaryDirectory && entry.rekeyPending).map((entry) => ({ entry, pending: entry.rekeyPending }));
|
|
3406
|
+
if (parkedElsewhere.length > 0) {
|
|
3407
|
+
const abandoned = parkedElsewhere.filter((item) => item.pending.state !== "pending");
|
|
3408
|
+
const describe = (item) => `${item.entry.slug ?? item.entry.agentId ?? basename(item.entry.directory)} (${item.pending.state}, ${item.pending.path})`;
|
|
3409
|
+
checks.push({
|
|
3410
|
+
id: "rekey_pending_elsewhere",
|
|
3411
|
+
label: "Parked re-keys in other credential directories",
|
|
3412
|
+
ok: abandoned.length === 0,
|
|
3413
|
+
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(", ")}.`,
|
|
3414
|
+
...abandoned.length > 0 ? {
|
|
3415
|
+
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."
|
|
3416
|
+
} : {}
|
|
3417
|
+
});
|
|
3418
|
+
}
|
|
3419
|
+
const signerProcess = primaryChecksById.get("signer_process");
|
|
3420
|
+
if (signerProcess) checks.push(signerProcess);
|
|
3421
|
+
const restart = restartRequiredForRuntime(input.runtime, deps.env);
|
|
3422
|
+
checks.push({
|
|
3423
|
+
id: "restart",
|
|
3424
|
+
label: "Runtime restart",
|
|
3425
|
+
ok: true,
|
|
3426
|
+
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."
|
|
3427
|
+
});
|
|
3428
|
+
const wiredOk = inventory.filter((entry) => entry.classification === "wired").every((entry) => entry.checks.every((check) => check.ok));
|
|
2282
3429
|
return {
|
|
2283
|
-
|
|
2284
|
-
|
|
3430
|
+
version: 1,
|
|
3431
|
+
ok: checks.every((check) => check.ok) && wiredOk,
|
|
3432
|
+
runtime: input.runtime,
|
|
3433
|
+
credentialDirectory: primaryDirectory,
|
|
3434
|
+
checks,
|
|
3435
|
+
agents: inventory,
|
|
3436
|
+
...signerCapabilities ? { signerCapabilities } : {}
|
|
2285
3437
|
};
|
|
2286
3438
|
}
|
|
2287
|
-
async function
|
|
2288
|
-
const
|
|
2289
|
-
const
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
try {
|
|
2296
|
-
if (!localMcpCommand) throw new Error("local MCP wrapper command is required");
|
|
2297
|
-
await runCommand("claude", ["mcp", "remove", "haven"]).catch(() => void 0);
|
|
2298
|
-
await runCommand("claude", ["mcp", "remove", "haven-signer"]).catch(() => void 0);
|
|
2299
|
-
await runCommand("claude", ["mcp", "add-json", "haven", serverJson, "--scope", "user"]).catch(async () => {
|
|
2300
|
-
await runCommand("claude", ["mcp", "add", "haven", "--scope", "user", "--", localMcpCommand]);
|
|
2301
|
-
});
|
|
2302
|
-
const verified = await runCommand("claude", ["mcp", "get", "haven"]).then(() => true).catch(() => false);
|
|
2303
|
-
return {
|
|
2304
|
-
hostedConfigured: false,
|
|
2305
|
-
signerConfigured: true,
|
|
2306
|
-
localMcpConfigured: true,
|
|
2307
|
-
runtimeMcpMode: "local_stdio",
|
|
2308
|
-
target: "Claude Code MCP config",
|
|
2309
|
-
changed: true,
|
|
2310
|
-
restartRequired: true,
|
|
2311
|
-
messages: [
|
|
2312
|
-
"Updated local Haven MCP entry with Claude Code.",
|
|
2313
|
-
...verified ? ["Verified Claude Code MCP entry."] : []
|
|
2314
|
-
]
|
|
2315
|
-
};
|
|
2316
|
-
} catch (err) {
|
|
3439
|
+
async function runRepair(input, deps = {}) {
|
|
3440
|
+
const homeDir = deps.homeDir ?? homedir();
|
|
3441
|
+
const messages = [];
|
|
3442
|
+
const { directory, others } = await discoverCredentialDirectory(homeDir, input.credentialsDir);
|
|
3443
|
+
if (others.length > 0) {
|
|
3444
|
+
messages.push(`Note: ${others.length} other agent credential dir(s) exist \u2014 run --doctor for their status.`);
|
|
3445
|
+
}
|
|
3446
|
+
if (!directory) {
|
|
2317
3447
|
return {
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
localMcpConfigured: false,
|
|
2321
|
-
runtimeMcpMode: "local_stdio",
|
|
2322
|
-
target: "Claude Code MCP config",
|
|
2323
|
-
changed: false,
|
|
2324
|
-
restartRequired: true,
|
|
2325
|
-
messages: [
|
|
2326
|
-
`Could not update Claude Code MCP config: ${err instanceof Error ? err.message : String(err)}`,
|
|
2327
|
-
"Install Claude Code or rerun the Haven setup command inside a Claude Code terminal."
|
|
2328
|
-
],
|
|
2329
|
-
errorCode: "claude_code_config_failed"
|
|
3448
|
+
ok: false,
|
|
3449
|
+
messages: [`No agent credentials found to repair \u2014 run the full setup: ${RERUN} --setup <token>.`]
|
|
2330
3450
|
};
|
|
2331
3451
|
}
|
|
2332
|
-
|
|
2333
|
-
async function configureClaudeCodeHosted(deps, input, signerCommand) {
|
|
2334
|
-
const runCommand = deps.runCommand ?? defaultRunCommand;
|
|
2335
|
-
const hostedJson = JSON.stringify({
|
|
2336
|
-
type: "http",
|
|
2337
|
-
url: input.hostedMcpUrl,
|
|
2338
|
-
headers: { Authorization: `Bearer ${input.apiKey}` }
|
|
2339
|
-
});
|
|
2340
|
-
const signerJson = JSON.stringify({
|
|
2341
|
-
type: "stdio",
|
|
2342
|
-
command: signerCommand?.command ?? "npx",
|
|
2343
|
-
args: signerCommand?.args ?? ["-y", signerPackageSpec(), "--credentials", input.signerPath],
|
|
2344
|
-
env: {}
|
|
2345
|
-
});
|
|
3452
|
+
let identity;
|
|
2346
3453
|
try {
|
|
2347
|
-
await
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
await runCommand("claude", ["mcp", "add-json", "haven-signer", signerJson, "--scope", "user"]);
|
|
2351
|
-
const verified = await runCommand("claude", ["mcp", "get", "haven"]).then(() => true).catch(() => false);
|
|
2352
|
-
return {
|
|
2353
|
-
hostedConfigured: true,
|
|
2354
|
-
signerConfigured: true,
|
|
2355
|
-
localMcpConfigured: false,
|
|
2356
|
-
runtimeMcpMode: "hosted_plus_signer",
|
|
2357
|
-
target: "Claude Code MCP config",
|
|
2358
|
-
changed: true,
|
|
2359
|
-
restartRequired: true,
|
|
2360
|
-
messages: [
|
|
2361
|
-
"Updated hosted Haven MCP and local signer entries with Claude Code.",
|
|
2362
|
-
...verified ? ["Verified Claude Code MCP entry."] : []
|
|
2363
|
-
]
|
|
2364
|
-
};
|
|
2365
|
-
} catch (err) {
|
|
2366
|
-
return {
|
|
2367
|
-
hostedConfigured: false,
|
|
2368
|
-
signerConfigured: false,
|
|
2369
|
-
localMcpConfigured: false,
|
|
2370
|
-
runtimeMcpMode: "hosted_plus_signer",
|
|
2371
|
-
target: "Claude Code MCP config",
|
|
2372
|
-
changed: false,
|
|
2373
|
-
restartRequired: true,
|
|
2374
|
-
messages: [
|
|
2375
|
-
`Could not update Claude Code MCP config: ${err instanceof Error ? err.message : String(err)}`,
|
|
2376
|
-
"Install Claude Code or rerun the Haven setup command inside a Claude Code terminal."
|
|
2377
|
-
],
|
|
2378
|
-
errorCode: "claude_code_config_failed"
|
|
2379
|
-
};
|
|
3454
|
+
identity = JSON.parse(await readFile(join(directory, "identity.json"), "utf8"));
|
|
3455
|
+
} catch {
|
|
3456
|
+
return { ok: false, messages: ["identity.json is unreadable \u2014 re-run the full setup with a fresh token."] };
|
|
2380
3457
|
}
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
await execFileAsync3(command, args, { timeout: 1e4 });
|
|
2384
|
-
}
|
|
2385
|
-
function buildProbeResult(mode, hostedConfigured, hostedStatus, signerReady, localMcpReady, localMcpProbeStatus) {
|
|
2386
|
-
if (mode === "local_stdio") {
|
|
2387
|
-
if (localMcpReady) return "local_stdio_mcp_ready";
|
|
2388
|
-
return localMcpProbeStatus ? `local_stdio_mcp_${localMcpProbeStatus}` : "local_stdio_mcp_unavailable";
|
|
3458
|
+
if (!identity.api_key || !(identity.hosted_mcp_url || identity.api_url)) {
|
|
3459
|
+
return { ok: false, messages: ["identity.json lacks the stored API key / hosted URL \u2014 re-run the full setup."] };
|
|
2389
3460
|
}
|
|
2390
|
-
const
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
3461
|
+
const configPath = runtimeConfigPathFor(input.runtime, homeDir);
|
|
3462
|
+
if (configPath) {
|
|
3463
|
+
try {
|
|
3464
|
+
const existing = await readFile(configPath, "utf8");
|
|
3465
|
+
if (existing.includes("bin/haven-mcp") || existing.includes(".haven/mcp-runtime")) {
|
|
3466
|
+
return {
|
|
3467
|
+
ok: false,
|
|
3468
|
+
messages: [
|
|
3469
|
+
`The config at ${configPath} is the LOCAL-stdio topology (--local). Repair currently rewrites only the hosted+signer shape and will not touch it.`,
|
|
3470
|
+
"Re-run your original setup command (with --local) to repair a local-stdio install."
|
|
3471
|
+
]
|
|
3472
|
+
};
|
|
3473
|
+
}
|
|
3474
|
+
} catch {
|
|
2401
3475
|
}
|
|
2402
|
-
return status;
|
|
2403
3476
|
}
|
|
2404
|
-
|
|
3477
|
+
const existingSidecar = await readRuntimeSidecar(directory);
|
|
3478
|
+
const serverName = existingSidecar?.server_name;
|
|
3479
|
+
const signerPath = join(directory, "signer.json");
|
|
3480
|
+
const prepared = await prepareSignerRuntime(
|
|
3481
|
+
{ credentialDirectory: directory, signerPath, homeDir, serverName },
|
|
3482
|
+
{ runCommand: deps.runCommand }
|
|
3483
|
+
);
|
|
3484
|
+
messages.push(...prepared.messages);
|
|
3485
|
+
const names = serverNamesFor(serverName);
|
|
3486
|
+
messages.push(`Rewriting MCP entries ${names.hosted} / ${names.signer}${serverName ? ` (agent "${serverName}")` : " (unnamed pair)"} \u2014 no other pair is touched.`);
|
|
3487
|
+
const configResult = await writeRuntimeConfig({
|
|
3488
|
+
runtime: input.runtime,
|
|
3489
|
+
hostedMcpUrl: identity.hosted_mcp_url ?? `${identity.api_url}/mcp`,
|
|
3490
|
+
apiKey: identity.api_key,
|
|
3491
|
+
identityPath: join(directory, "identity.json"),
|
|
3492
|
+
signerPath,
|
|
3493
|
+
credentialDirectory: directory,
|
|
3494
|
+
signerCommand: { command: prepared.command, args: prepared.args },
|
|
3495
|
+
homeDir,
|
|
3496
|
+
mode: "hosted",
|
|
3497
|
+
serverName
|
|
3498
|
+
});
|
|
3499
|
+
messages.push(...configResult.messages);
|
|
3500
|
+
messages.push("Repair complete \u2014 restart the runtime, then verify with --doctor.");
|
|
3501
|
+
return { ok: true, messages };
|
|
2405
3502
|
}
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
3503
|
+
var RERUN;
|
|
3504
|
+
var init_doctor = __esm({
|
|
3505
|
+
"src/doctor.ts"() {
|
|
3506
|
+
init_runtime_manifest();
|
|
3507
|
+
init_probes();
|
|
3508
|
+
init_signer_runtime();
|
|
3509
|
+
init_config_writers();
|
|
3510
|
+
init_runtime_registry();
|
|
3511
|
+
init_signer_consent();
|
|
3512
|
+
init_tombstone();
|
|
3513
|
+
init_server_names();
|
|
3514
|
+
init_storage();
|
|
3515
|
+
init_redact();
|
|
3516
|
+
RERUN = "npx @haven_ai/connect@alpha";
|
|
3517
|
+
}
|
|
3518
|
+
});
|
|
3519
|
+
|
|
3520
|
+
// src/runtime.ts
|
|
3521
|
+
init_api();
|
|
3522
|
+
init_key();
|
|
3523
|
+
init_redact();
|
|
3524
|
+
init_server_names();
|
|
3525
|
+
init_storage();
|
|
3526
|
+
init_runtime_install();
|
|
3527
|
+
init_runtime_registry();
|
|
3528
|
+
init_connect_error();
|
|
3529
|
+
|
|
3530
|
+
// src/installed-clients.ts
|
|
3531
|
+
init_connect_error();
|
|
3532
|
+
init_config_writers();
|
|
3533
|
+
init_runtime_registry();
|
|
3534
|
+
var SCAN_ORDER = [
|
|
3535
|
+
"claude-code",
|
|
3536
|
+
"codex-cli",
|
|
3537
|
+
"cursor",
|
|
3538
|
+
"vscode",
|
|
3539
|
+
"vscode-insiders",
|
|
3540
|
+
"claude-desktop",
|
|
3541
|
+
"hermes"
|
|
3542
|
+
];
|
|
3543
|
+
function installedClientTargets(homeDir = homedir(), cwd = process.cwd(), env = process.env) {
|
|
3544
|
+
const targets = [
|
|
3545
|
+
{
|
|
3546
|
+
runtime: "claude-code",
|
|
3547
|
+
label: "Claude Code",
|
|
3548
|
+
// Claude Code is configured through its own CLI (`claude mcp add-json`),
|
|
3549
|
+
// not by writing a file this module owns — so its evidence is the
|
|
3550
|
+
// client directory, never a config path.
|
|
3551
|
+
configPath: null,
|
|
3552
|
+
markers: [join(homeDir, ".claude"), join(homeDir, ".claude.json")]
|
|
3553
|
+
},
|
|
3554
|
+
{
|
|
3555
|
+
runtime: "codex-cli",
|
|
3556
|
+
// Both Codex surfaces write the same ~/.codex/config.toml, so they are
|
|
3557
|
+
// ONE candidate. Splitting them would ask the user to answer a question
|
|
3558
|
+
// whose answers are the same write.
|
|
3559
|
+
label: "Codex (CLI or Desktop)",
|
|
3560
|
+
configPath: runtimeConfigPathFor("codex-cli", homeDir),
|
|
3561
|
+
markers: [join(homeDir, ".codex")]
|
|
3562
|
+
},
|
|
3563
|
+
{
|
|
3564
|
+
runtime: "cursor",
|
|
3565
|
+
label: "Cursor",
|
|
3566
|
+
configPath: runtimeConfigPathFor("cursor", homeDir),
|
|
3567
|
+
markers: [join(homeDir, ".cursor")]
|
|
3568
|
+
},
|
|
3569
|
+
{
|
|
3570
|
+
runtime: "vscode",
|
|
3571
|
+
label: "VS Code",
|
|
3572
|
+
configPath: runtimeConfigPathFor("vscode", homeDir),
|
|
3573
|
+
markers: [resolve(cwd, ".vscode")]
|
|
3574
|
+
},
|
|
3575
|
+
{
|
|
3576
|
+
runtime: "vscode-insiders",
|
|
3577
|
+
label: "VS Code Insiders",
|
|
3578
|
+
configPath: runtimeConfigPathFor("vscode-insiders", homeDir),
|
|
3579
|
+
markers: []
|
|
3580
|
+
},
|
|
3581
|
+
{
|
|
3582
|
+
runtime: "claude-desktop",
|
|
3583
|
+
label: "Claude Desktop",
|
|
3584
|
+
configPath: runtimeConfigPathFor("claude-desktop", homeDir),
|
|
3585
|
+
markers: []
|
|
3586
|
+
},
|
|
3587
|
+
{
|
|
3588
|
+
runtime: "hermes",
|
|
3589
|
+
label: "Hermes Agent",
|
|
3590
|
+
configPath: runtimeConfigPathFor("hermes", homeDir),
|
|
3591
|
+
markers: [env.HERMES_HOME ?? join(homeDir, ".hermes")]
|
|
3592
|
+
}
|
|
3593
|
+
];
|
|
3594
|
+
return targets.filter((target) => runtimeProfile(target.runtime, {}).canWriteRuntimeConfig);
|
|
3595
|
+
}
|
|
3596
|
+
async function scanInstalledClients(options = {}) {
|
|
3597
|
+
const exists = options.exists ?? pathExists;
|
|
3598
|
+
const targets = installedClientTargets(options.homeDir, options.cwd, options.env ?? process.env);
|
|
3599
|
+
const found = [];
|
|
3600
|
+
for (const target of targets) {
|
|
3601
|
+
if (target.configPath && await exists(target.configPath)) {
|
|
3602
|
+
found.push({
|
|
3603
|
+
runtime: target.runtime,
|
|
3604
|
+
label: target.label,
|
|
3605
|
+
detail: `MCP config found at ${target.configPath}`,
|
|
3606
|
+
configPath: target.configPath,
|
|
3607
|
+
evidence: "config-file"
|
|
3608
|
+
});
|
|
3609
|
+
continue;
|
|
3610
|
+
}
|
|
3611
|
+
for (const marker of target.markers) {
|
|
3612
|
+
if (!await exists(marker)) continue;
|
|
3613
|
+
found.push({
|
|
3614
|
+
runtime: target.runtime,
|
|
3615
|
+
label: target.label,
|
|
3616
|
+
detail: `installed (${marker})`,
|
|
3617
|
+
configPath: target.configPath,
|
|
3618
|
+
evidence: "client-directory"
|
|
3619
|
+
});
|
|
3620
|
+
break;
|
|
2413
3621
|
}
|
|
2414
|
-
return status;
|
|
2415
3622
|
}
|
|
2416
|
-
return
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
if (!signerCredentialReady) return "local_signer_credential_unavailable";
|
|
2420
|
-
if (!signerConsent?.acknowledged) return "local_signer_ack_required";
|
|
2421
|
-
return void 0;
|
|
2422
|
-
}
|
|
2423
|
-
function signerProbeErrorCode(probe) {
|
|
2424
|
-
if (!probe || probe.status === "ok") return void 0;
|
|
2425
|
-
return `local_signer_probe_${probe.status}`;
|
|
2426
|
-
}
|
|
2427
|
-
function hostedMcpErrorCode(hostedConfigured, hostedProbeStatus) {
|
|
2428
|
-
if (!hostedConfigured || hostedProbeStatus === "ok") return void 0;
|
|
2429
|
-
return `hosted_mcp_probe_${hostedProbeStatus}`;
|
|
2430
|
-
}
|
|
2431
|
-
function localMcpErrorCode(signerCredentialReady, localMcpConsent, localMcpProbeStatus) {
|
|
2432
|
-
if (!signerCredentialReady) return "local_signer_credential_unavailable";
|
|
2433
|
-
if (!localMcpConsent?.acknowledged) return "local_mcp_ack_required";
|
|
2434
|
-
if (localMcpProbeStatus && localMcpProbeStatus !== "ok") return `local_mcp_probe_${localMcpProbeStatus}`;
|
|
2435
|
-
return void 0;
|
|
2436
|
-
}
|
|
2437
|
-
function nextAction(runtime, restartMode, errorCode) {
|
|
2438
|
-
if (errorCode) return "return_to_haven_for_wallet_approval_then_finish_runtime_setup";
|
|
2439
|
-
if (restartMode === "hot-reload") return "return_to_haven_for_wallet_approval";
|
|
2440
|
-
if (runtime === "codex-cli" || runtime === "codex-desktop") return "return_to_haven_for_wallet_approval_then_restart_codex";
|
|
2441
|
-
if (runtime === "claude-code") return "return_to_haven_for_wallet_approval_then_restart_claude_code";
|
|
2442
|
-
if (restartMode === "restart-app") return "return_to_haven_for_wallet_approval_then_restart_app";
|
|
2443
|
-
if (restartMode === "restart-session") return "return_to_haven_for_wallet_approval_then_restart_agent_session";
|
|
2444
|
-
return "return_to_haven_for_wallet_approval_then_configure_runtime";
|
|
2445
|
-
}
|
|
2446
|
-
function supportsLocalMcp(runtime) {
|
|
2447
|
-
return runtime === "codex-cli" || runtime === "codex-desktop" || runtime === "claude-code";
|
|
2448
|
-
}
|
|
2449
|
-
async function prepareRuntimeForLocalMcp(input, deps) {
|
|
2450
|
-
const prepare = deps.prepareLocalMcpRuntime ?? ((runtimeInput) => prepareLocalMcpRuntime(runtimeInput, { runCommand: deps.runCommand, onProgress: deps.onProgress }));
|
|
2451
|
-
return prepare({
|
|
2452
|
-
credentialDirectory: input.credentialDirectory,
|
|
2453
|
-
identityPath: input.identityPath,
|
|
2454
|
-
signerPath: input.signerPath,
|
|
2455
|
-
homeDir: deps.homeDir
|
|
3623
|
+
return found.sort((a, b) => {
|
|
3624
|
+
if (a.evidence !== b.evidence) return a.evidence === "config-file" ? -1 : 1;
|
|
3625
|
+
return SCAN_ORDER.indexOf(a.runtime) - SCAN_ORDER.indexOf(b.runtime);
|
|
2456
3626
|
});
|
|
2457
3627
|
}
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
return prepare({
|
|
2467
|
-
credentialDirectory: input.credentialDirectory,
|
|
2468
|
-
signerPath: input.signerPath,
|
|
2469
|
-
homeDir: deps.homeDir
|
|
3628
|
+
var MAX_PROMPT_ATTEMPTS = 3;
|
|
3629
|
+
async function promptForInstalledClient(candidates, io = defaultPromptIo()) {
|
|
3630
|
+
if (candidates.length === 0) throw noInstalledClientsError();
|
|
3631
|
+
io.write("Haven could not detect which agent runtime this is.\n");
|
|
3632
|
+
io.write("These agent clients are installed on this machine:\n");
|
|
3633
|
+
candidates.forEach((candidate, index) => {
|
|
3634
|
+
io.write(` ${index + 1}) ${candidate.label} \u2014 ${candidate.detail}
|
|
3635
|
+
`);
|
|
2470
3636
|
});
|
|
3637
|
+
io.write("Haven writes an API key and a signing key into the client you pick, so pick the one your agent actually runs in.\n");
|
|
3638
|
+
for (let attempt = 0; attempt < MAX_PROMPT_ATTEMPTS; attempt += 1) {
|
|
3639
|
+
const answer = await io.question(`Which one? [1-${candidates.length}] (default 1 \u2014 ${candidates[0].label}): `);
|
|
3640
|
+
if (answer === null) throw promptAbortedError("the prompt was cancelled");
|
|
3641
|
+
const trimmed = answer.trim();
|
|
3642
|
+
if (trimmed === "") return candidates[0].runtime;
|
|
3643
|
+
const picked = Number.parseInt(trimmed, 10);
|
|
3644
|
+
if (Number.isInteger(picked) && picked >= 1 && picked <= candidates.length) {
|
|
3645
|
+
return candidates[picked - 1].runtime;
|
|
3646
|
+
}
|
|
3647
|
+
io.write(`"${trimmed}" is not one of 1-${candidates.length}.
|
|
3648
|
+
`);
|
|
3649
|
+
}
|
|
3650
|
+
throw promptAbortedError(`no valid choice after ${MAX_PROMPT_ATTEMPTS} attempts`);
|
|
3651
|
+
}
|
|
3652
|
+
async function resolveRuntimeByInstalledClientPrompt(options = {}) {
|
|
3653
|
+
const candidates = await scanInstalledClients(options);
|
|
3654
|
+
if (candidates.length === 0) throw noInstalledClientsError();
|
|
3655
|
+
return promptForInstalledClient(candidates, options.io ?? defaultPromptIo());
|
|
3656
|
+
}
|
|
3657
|
+
function noInstalledClientsError() {
|
|
3658
|
+
return new ConnectError(
|
|
3659
|
+
"runtime_no_installed_clients",
|
|
3660
|
+
"Could not determine the agent runtime: nothing was detected in this environment, and no agent client Haven can configure is installed on this machine. Re-run with --runtime <name> naming the client you want configured, or --runtime other to store credentials and finish the MCP setup by hand.",
|
|
3661
|
+
"rerun_connect_with_explicit_runtime"
|
|
3662
|
+
);
|
|
2471
3663
|
}
|
|
2472
|
-
|
|
2473
|
-
|
|
3664
|
+
function promptAbortedError(reason) {
|
|
3665
|
+
return new ConnectError(
|
|
3666
|
+
"runtime_prompt_aborted",
|
|
3667
|
+
`Runtime not chosen (${reason}). Nothing was written: no agent was created, no credentials were stored, and the Haven setup token is still unused. Run the setup command again, or pass --runtime <name> to skip the prompt.`,
|
|
3668
|
+
"rerun_connect_and_choose_a_runtime"
|
|
3669
|
+
);
|
|
3670
|
+
}
|
|
3671
|
+
async function pathExists(path) {
|
|
2474
3672
|
try {
|
|
2475
|
-
|
|
3673
|
+
await access(path);
|
|
3674
|
+
return true;
|
|
2476
3675
|
} catch {
|
|
2477
|
-
return
|
|
3676
|
+
return false;
|
|
2478
3677
|
}
|
|
2479
3678
|
}
|
|
2480
|
-
function
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
3679
|
+
function defaultPromptIo() {
|
|
3680
|
+
return {
|
|
3681
|
+
write: (text) => process.stdout.write(text),
|
|
3682
|
+
question: (query) => new Promise((resolvePromise) => {
|
|
3683
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
3684
|
+
let settled = false;
|
|
3685
|
+
const settle = (value) => {
|
|
3686
|
+
if (settled) return;
|
|
3687
|
+
settled = true;
|
|
3688
|
+
rl.close();
|
|
3689
|
+
resolvePromise(value);
|
|
3690
|
+
};
|
|
3691
|
+
rl.once("SIGINT", () => settle(null));
|
|
3692
|
+
rl.once("close", () => settle(null));
|
|
3693
|
+
rl.question(query, (answer) => settle(answer));
|
|
3694
|
+
})
|
|
3695
|
+
};
|
|
2485
3696
|
}
|
|
2486
3697
|
|
|
2487
3698
|
// src/runtime.ts
|
|
2488
|
-
|
|
3699
|
+
init_local_mcp_runtime();
|
|
2489
3700
|
init_runtime_manifest();
|
|
2490
|
-
var CONNECTOR_VERSION = "0.1.
|
|
3701
|
+
var CONNECTOR_VERSION = "0.1.30-alpha.0";
|
|
2491
3702
|
var CONNECT_OUTCOME_SCHEMA_VERSION = 1;
|
|
2492
3703
|
async function runConnect(options, deps = {}) {
|
|
2493
3704
|
assertSupportedNodeVersion(deps.nodeVersion, MCP_RUNTIME_MANIFEST.minimumNodeVersion);
|
|
@@ -2503,24 +3714,49 @@ async function runConnect(options, deps = {}) {
|
|
|
2503
3714
|
const runRuntimeInstall = deps.installRuntime ?? installRuntime;
|
|
2504
3715
|
const generateKey = deps.generateKey ?? generateDelegateKey;
|
|
2505
3716
|
const generateLocalApiKey = deps.generateApiKey ?? generateAgentApiKey;
|
|
2506
|
-
const
|
|
3717
|
+
const selection = await resolveRuntimeSelection(options.runtime, options.runtimeForce, {
|
|
3718
|
+
env: deps.env ?? process.env,
|
|
3719
|
+
selfReported: options.runtimeSelfReport,
|
|
3720
|
+
promptForRuntime: runtimeSelectionPrompt(options, deps)
|
|
3721
|
+
});
|
|
3722
|
+
if (!selection.runtime) {
|
|
3723
|
+
throw new ConnectError(
|
|
3724
|
+
"runtime_undetermined",
|
|
3725
|
+
`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: ${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.`,
|
|
3726
|
+
"rerun_connect_with_explicit_runtime"
|
|
3727
|
+
);
|
|
3728
|
+
}
|
|
3729
|
+
const runtime = selection.runtime;
|
|
3730
|
+
const installCapabilities = runtimeInstallCapabilities(runtime);
|
|
2507
3731
|
if (options.localMcp) {
|
|
2508
|
-
|
|
2509
|
-
if (!supportsLocalMcp(resolvedRuntime)) {
|
|
3732
|
+
if (!supportsLocalMcp(runtime)) {
|
|
2510
3733
|
throw new Error(
|
|
2511
|
-
`--local (fully-local Haven MCP) is only available for Claude Code and Codex. The detected runtime is ${runtimeProfile(
|
|
3734
|
+
`--local (fully-local Haven MCP) is only available for Claude Code and Codex. The detected runtime is ${runtimeProfile(runtime).label}. Re-run without --local to use the default hosted MCP + local signer setup.`
|
|
2512
3735
|
);
|
|
2513
3736
|
}
|
|
2514
3737
|
}
|
|
3738
|
+
if (selection.overrodeHint) {
|
|
3739
|
+
log(`runtime: ${runtime} (detected; ignoring the ${selection.overrodeHint} hint \u2014 pass --runtime-force ${selection.overrodeHint} to override)`);
|
|
3740
|
+
}
|
|
3741
|
+
if (selection.discardedHint) {
|
|
3742
|
+
log(`runtime: ${runtime} (detected; "${selection.discardedHint}" is not a runtime Haven knows \u2014 valid values: ${RUNTIME_FLAG_VALUES})`);
|
|
3743
|
+
}
|
|
3744
|
+
if (selection.source === "prompted") {
|
|
3745
|
+
log(`runtime: ${runtime} (chosen at the prompt \u2014 nothing was detected in this environment)`);
|
|
3746
|
+
}
|
|
2515
3747
|
log("Warming up your connection to Haven\u2026");
|
|
2516
3748
|
const setup = await api.resolveSetup({
|
|
2517
3749
|
setupToken: options.setupToken,
|
|
2518
3750
|
connectorVersion,
|
|
2519
|
-
runtime
|
|
3751
|
+
runtime
|
|
2520
3752
|
});
|
|
2521
3753
|
assertSetupChallengeIsUsable(setup.challenge.expires_at);
|
|
2522
3754
|
printSetupSummary(setup, log);
|
|
2523
3755
|
await preflightStorage({ baseDir: options.credentialsDir, warn: log });
|
|
3756
|
+
if (options.serverName) {
|
|
3757
|
+
assertValidServerSlug(options.serverName);
|
|
3758
|
+
await assertServerSlugAvailable(options.serverName, options.credentialsDir);
|
|
3759
|
+
}
|
|
2524
3760
|
log("Checked local credential storage \u2014 all clear.");
|
|
2525
3761
|
const localKey = generateKey();
|
|
2526
3762
|
const localApiKey = generateLocalApiKey();
|
|
@@ -2532,12 +3768,17 @@ async function runConnect(options, deps = {}) {
|
|
|
2532
3768
|
registration = await api.registerSetup({
|
|
2533
3769
|
setupToken: options.setupToken,
|
|
2534
3770
|
connectorVersion,
|
|
2535
|
-
runtime
|
|
3771
|
+
runtime,
|
|
2536
3772
|
challengeId: setup.challenge.id,
|
|
2537
3773
|
delegateAddress: localKey.address,
|
|
2538
3774
|
proofSignature,
|
|
2539
3775
|
apiKeyHash: hashAgentApiKey(localApiKey),
|
|
2540
3776
|
apiKeyPrefix: agentApiKeyPrefix(localApiKey),
|
|
3777
|
+
// #1878: report the pair we are ACTUALLY wiring, bare pair included, so
|
|
3778
|
+
// the dashboard can name it. Derived here rather than sent as the raw
|
|
3779
|
+
// slug — `serverNamesFor` is the one place the naming rule lives, and
|
|
3780
|
+
// the hosted name is what a user pastes into an MCP config.
|
|
3781
|
+
mcpServerName: serverNamesFor(options.serverName).hosted,
|
|
2541
3782
|
connectorContext: {
|
|
2542
3783
|
environment_label: options.environmentLabel ?? "Local workspace",
|
|
2543
3784
|
config_target: installCapabilities.canWriteRuntimeConfig ? "agent runtime MCP config" : "local credential files"
|
|
@@ -2557,6 +3798,7 @@ async function runConnect(options, deps = {}) {
|
|
|
2557
3798
|
const credentialPaths = await writeCredentials({
|
|
2558
3799
|
baseDir: options.credentialsDir,
|
|
2559
3800
|
agentId: registration.agent_id,
|
|
3801
|
+
serverName: options.serverName,
|
|
2560
3802
|
apiKey: localApiKey,
|
|
2561
3803
|
delegateKey: localKey.privateKey,
|
|
2562
3804
|
delegateAddress: localKey.address,
|
|
@@ -2584,7 +3826,7 @@ async function runConnect(options, deps = {}) {
|
|
|
2584
3826
|
);
|
|
2585
3827
|
}
|
|
2586
3828
|
const runtimeInstall = await runRuntimeInstall({
|
|
2587
|
-
runtime
|
|
3829
|
+
runtime,
|
|
2588
3830
|
hostedMcpUrl: registration.hosted_mcp_url,
|
|
2589
3831
|
apiKey: localApiKey,
|
|
2590
3832
|
signerPath: credentialPaths.signerPath,
|
|
@@ -2593,7 +3835,8 @@ async function runConnect(options, deps = {}) {
|
|
|
2593
3835
|
environmentLabel: options.environmentLabel ?? "Local workspace",
|
|
2594
3836
|
ackSigner: options.ackSigner,
|
|
2595
3837
|
ackLocalTools: options.ackLocalTools,
|
|
2596
|
-
localMcp: options.localMcp
|
|
3838
|
+
localMcp: options.localMcp,
|
|
3839
|
+
serverName: options.serverName
|
|
2597
3840
|
}, {
|
|
2598
3841
|
onProgress: log,
|
|
2599
3842
|
// #1543: report "runtime configured" the moment the config write settles,
|
|
@@ -2629,6 +3872,19 @@ async function runConnect(options, deps = {}) {
|
|
|
2629
3872
|
} else {
|
|
2630
3873
|
log("Haven setup on this machine is complete.");
|
|
2631
3874
|
}
|
|
3875
|
+
try {
|
|
3876
|
+
const supersededIds = await listOtherAgentIds(options.credentialsDir, credentialPaths.directory);
|
|
3877
|
+
if (supersededIds.length > 0) {
|
|
3878
|
+
log("");
|
|
3879
|
+
log(
|
|
3880
|
+
`Heads-up: this setup created a NEW agent. Your previous agent(s) \u2014 ${supersededIds.join(", ")} \u2014 still exist with their own keys, and any host that was already running keeps acting as them.`
|
|
3881
|
+
);
|
|
3882
|
+
log(
|
|
3883
|
+
`If you meant to replace them: revoke them on the Haven agent page, then restart EVERY long-lived host (gateways, TUI workers, editors) \u2014 each holds the MCP wiring snapshot from its own start time, so after repeated setups each can be stuck on a DIFFERENT old agent. Then remove their directories under ~/.haven/agents (or ${RERUN_HINT} --tombstone <dir> to leave a diagnostic in their place). Run ${RERUN_HINT} --doctor to check whether their keys are still live.`
|
|
3884
|
+
);
|
|
3885
|
+
}
|
|
3886
|
+
} catch {
|
|
3887
|
+
}
|
|
2632
3888
|
try {
|
|
2633
3889
|
await api.updateInstallStatus(registration.setup_id, localApiKey, {
|
|
2634
3890
|
runtime: runtimeInstall.runtime,
|
|
@@ -2703,11 +3959,16 @@ function completionOutcome(input) {
|
|
|
2703
3959
|
};
|
|
2704
3960
|
return outcome;
|
|
2705
3961
|
}
|
|
3962
|
+
function runtimeSelectionPrompt(options, deps) {
|
|
3963
|
+
if (options.interactive !== true) return void 0;
|
|
3964
|
+
if (!(deps.isTty ?? Boolean(process.stdin.isTTY))) return void 0;
|
|
3965
|
+
return deps.promptRuntime ?? (() => resolveRuntimeByInstalledClientPrompt());
|
|
3966
|
+
}
|
|
2706
3967
|
function failedConnectOutcome(runtimeHint, error) {
|
|
2707
3968
|
const message = error instanceof Error ? error.message : "";
|
|
2708
|
-
const code = /Node\.js >=/i.test(message) ? "unsupported_node_version" : /setup challenge.*expired|expired or invalid/i.test(message) ? "setup_challenge_expired_or_invalid" : /only available for Claude Code and Codex/i.test(message) ? "local_mcp_unsupported_runtime" : "connect_failed";
|
|
3969
|
+
const code = error instanceof ConnectError ? error.code : /Node\.js >=/i.test(message) ? "unsupported_node_version" : /setup challenge.*expired|expired or invalid/i.test(message) ? "setup_challenge_expired_or_invalid" : /only available for Claude Code and Codex/i.test(message) ? "local_mcp_unsupported_runtime" : "connect_failed";
|
|
2709
3970
|
const runtime = normalizeRuntime(runtimeHint);
|
|
2710
|
-
const nextAction2 = code === "setup_challenge_expired_or_invalid" ? "return_to_haven_for_fresh_setup" : code === "unsupported_node_version" ? "install_supported_node_and_rerun_connect" : code === "local_mcp_unsupported_runtime" ? "rerun_without_local_mcp" : "review_the_safe_error_output_and_start_a_fresh_haven_setup_if_needed";
|
|
3971
|
+
const nextAction2 = error instanceof ConnectError ? error.nextAction : code === "setup_challenge_expired_or_invalid" ? "return_to_haven_for_fresh_setup" : code === "unsupported_node_version" ? "install_supported_node_and_rerun_connect" : code === "local_mcp_unsupported_runtime" ? "rerun_without_local_mcp" : "review_the_safe_error_output_and_start_a_fresh_haven_setup_if_needed";
|
|
2711
3972
|
return {
|
|
2712
3973
|
schema_version: CONNECT_OUTCOME_SCHEMA_VERSION,
|
|
2713
3974
|
outcome: "failed",
|
|
@@ -2793,7 +4054,7 @@ function describeApprovedBudget(budget) {
|
|
|
2793
4054
|
async function waitForBudgetApproval(api, setupId, apiKey, log, options = {}) {
|
|
2794
4055
|
const intervalMs = options.intervalMs ?? 5e3;
|
|
2795
4056
|
const timeoutMs = options.timeoutMs ?? 18e4;
|
|
2796
|
-
const sleep = options.sleep ?? ((ms) => new Promise((
|
|
4057
|
+
const sleep = options.sleep ?? ((ms) => new Promise((resolve9) => setTimeout(resolve9, ms)));
|
|
2797
4058
|
const maxPolls = Math.max(1, Math.floor(timeoutMs / intervalMs));
|
|
2798
4059
|
const remindEvery = Math.max(1, Math.floor(3e4 / intervalMs));
|
|
2799
4060
|
let waitingAnnounced = false;
|
|
@@ -2879,11 +4140,39 @@ function activationInstructionWithWhy(profile) {
|
|
|
2879
4140
|
}
|
|
2880
4141
|
return profile.activationInstruction;
|
|
2881
4142
|
}
|
|
4143
|
+
var RERUN_HINT = "npx @haven_ai/connect@alpha";
|
|
4144
|
+
async function listOtherAgentIds(baseDir, currentDirectory) {
|
|
4145
|
+
const root = defaultCredentialRoot(baseDir);
|
|
4146
|
+
let entries = [];
|
|
4147
|
+
try {
|
|
4148
|
+
entries = await readdir(root);
|
|
4149
|
+
} catch {
|
|
4150
|
+
return [];
|
|
4151
|
+
}
|
|
4152
|
+
const ids = [];
|
|
4153
|
+
for (const entry of entries) {
|
|
4154
|
+
if (join(root, entry) === currentDirectory) continue;
|
|
4155
|
+
const identityPath = join(root, entry, "identity.json");
|
|
4156
|
+
try {
|
|
4157
|
+
await stat(identityPath);
|
|
4158
|
+
} catch {
|
|
4159
|
+
continue;
|
|
4160
|
+
}
|
|
4161
|
+
try {
|
|
4162
|
+
const identity = JSON.parse(await readFile(identityPath, "utf8"));
|
|
4163
|
+
ids.push(identity.agent_id ?? entry);
|
|
4164
|
+
} catch {
|
|
4165
|
+
ids.push(entry);
|
|
4166
|
+
}
|
|
4167
|
+
}
|
|
4168
|
+
return ids;
|
|
4169
|
+
}
|
|
2882
4170
|
function printNextSteps(result, log, approval) {
|
|
2883
4171
|
for (const line of completionHandoffLines(result, approval)) log(line);
|
|
2884
4172
|
}
|
|
2885
4173
|
|
|
2886
4174
|
// src/args.ts
|
|
4175
|
+
init_server_names();
|
|
2887
4176
|
function parseArgs(argv, env = process.env) {
|
|
2888
4177
|
const options = {
|
|
2889
4178
|
apiBaseUrl: env.HAVEN_API_URL ?? "http://localhost:3001",
|
|
@@ -2893,6 +4182,11 @@ function parseArgs(argv, env = process.env) {
|
|
|
2893
4182
|
let json = false;
|
|
2894
4183
|
let doctor = false;
|
|
2895
4184
|
let repair = false;
|
|
4185
|
+
let rekeyPhase;
|
|
4186
|
+
let newApiKey;
|
|
4187
|
+
let tombstoneDir;
|
|
4188
|
+
let tombstoneReason;
|
|
4189
|
+
let tombstoneReplacedBy;
|
|
2896
4190
|
for (let i = 0; i < argv.length; i += 1) {
|
|
2897
4191
|
const arg = argv[i];
|
|
2898
4192
|
if (arg === "--help" || arg === "-h") {
|
|
@@ -2903,14 +4197,31 @@ function parseArgs(argv, env = process.env) {
|
|
|
2903
4197
|
doctor = true;
|
|
2904
4198
|
} else if (arg === "--repair") {
|
|
2905
4199
|
repair = true;
|
|
4200
|
+
} else if (arg === "--rekey") {
|
|
4201
|
+
rekeyPhase = "start";
|
|
4202
|
+
} else if (arg === "--rekey-finish") {
|
|
4203
|
+
rekeyPhase = "finish";
|
|
4204
|
+
} else if (arg === "--api-key") {
|
|
4205
|
+
newApiKey = requireValue(argv, ++i, arg);
|
|
4206
|
+
} else if (arg === "--tombstone") {
|
|
4207
|
+
tombstoneDir = requireValue(argv, ++i, arg);
|
|
4208
|
+
} else if (arg === "--reason") {
|
|
4209
|
+
tombstoneReason = requireValue(argv, ++i, arg);
|
|
4210
|
+
} else if (arg === "--replaced-by") {
|
|
4211
|
+
tombstoneReplacedBy = requireValue(argv, ++i, arg);
|
|
2906
4212
|
} else if (arg === "--setup" || arg === "--setup-token") {
|
|
2907
4213
|
options.setupToken = requireValue(argv, ++i, arg);
|
|
2908
4214
|
} else if (arg === "--api" || arg === "--api-url") {
|
|
2909
4215
|
options.apiBaseUrl = requireValue(argv, ++i, arg);
|
|
2910
4216
|
} else if (arg === "--runtime") {
|
|
2911
4217
|
options.runtime = requireValue(argv, ++i, arg);
|
|
4218
|
+
} else if (arg === "--runtime-force") {
|
|
4219
|
+
options.runtimeForce = requireValue(argv, ++i, arg);
|
|
2912
4220
|
} else if (arg === "--credentials-dir") {
|
|
2913
4221
|
options.credentialsDir = requireValue(argv, ++i, arg);
|
|
4222
|
+
} else if (arg === "--name") {
|
|
4223
|
+
options.serverName = requireValue(argv, ++i, arg);
|
|
4224
|
+
assertValidServerSlug(options.serverName);
|
|
2914
4225
|
} else if (arg === "--environment-label") {
|
|
2915
4226
|
options.environmentLabel = requireValue(argv, ++i, arg);
|
|
2916
4227
|
} else if (arg === "--ack-local-tools") {
|
|
@@ -2928,14 +4239,39 @@ function parseArgs(argv, env = process.env) {
|
|
|
2928
4239
|
throw new Error(`Unknown option: ${arg}`);
|
|
2929
4240
|
}
|
|
2930
4241
|
}
|
|
4242
|
+
const tombstone = tombstoneDir ? { directory: tombstoneDir, reason: tombstoneReason, replacedBy: tombstoneReplacedBy } : void 0;
|
|
4243
|
+
const rekey = rekeyPhase ? { phase: rekeyPhase, newApiKey } : void 0;
|
|
2931
4244
|
if (help) {
|
|
2932
|
-
return { options, help, json, doctor, repair };
|
|
4245
|
+
return { options, help, json, doctor, repair, tombstone, rekey };
|
|
4246
|
+
}
|
|
4247
|
+
if (rekey) {
|
|
4248
|
+
if (options.setupToken) {
|
|
4249
|
+
throw new Error("--rekey replaces an existing agent's key; it does not take --setup. Drop one of them.");
|
|
4250
|
+
}
|
|
4251
|
+
if (rekey.phase === "start" && newApiKey !== void 0) {
|
|
4252
|
+
throw new Error(
|
|
4253
|
+
"--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."
|
|
4254
|
+
);
|
|
4255
|
+
}
|
|
4256
|
+
if (rekey.phase === "finish" && !newApiKey) {
|
|
4257
|
+
throw new Error("--rekey-finish needs --api-key <key> \u2014 the one the Haven agent page showed once.");
|
|
4258
|
+
}
|
|
4259
|
+
return { options, help, json, doctor, repair, tombstone, rekey };
|
|
4260
|
+
}
|
|
4261
|
+
if (newApiKey !== void 0) {
|
|
4262
|
+
throw new Error("--api-key requires --rekey-finish.");
|
|
4263
|
+
}
|
|
4264
|
+
if (!tombstoneDir && (tombstoneReason !== void 0 || tombstoneReplacedBy !== void 0)) {
|
|
4265
|
+
throw new Error("--reason and --replaced-by require --tombstone <dir>.");
|
|
4266
|
+
}
|
|
4267
|
+
if (tombstone) {
|
|
4268
|
+
return { options, help, json, doctor, repair, tombstone, rekey };
|
|
2933
4269
|
}
|
|
2934
4270
|
if (doctor || repair) {
|
|
2935
4271
|
if (!options.runtime) {
|
|
2936
4272
|
throw new Error("--doctor/--repair need --runtime <runtime> (which config to examine).");
|
|
2937
4273
|
}
|
|
2938
|
-
return { options, help, json, doctor, repair };
|
|
4274
|
+
return { options, help, json, doctor, repair, tombstone, rekey };
|
|
2939
4275
|
}
|
|
2940
4276
|
if (!options.setupToken) {
|
|
2941
4277
|
throw new Error("Missing --setup <hv_setup_...> setup token.");
|
|
@@ -2944,7 +4280,7 @@ function parseArgs(argv, env = process.env) {
|
|
|
2944
4280
|
throw new Error("Missing --api <Haven API URL>.");
|
|
2945
4281
|
}
|
|
2946
4282
|
options.apiBaseUrl = options.apiBaseUrl.replace(/\/+$/, "");
|
|
2947
|
-
return { options, help, json, doctor, repair };
|
|
4283
|
+
return { options, help, json, doctor, repair, tombstone, rekey };
|
|
2948
4284
|
}
|
|
2949
4285
|
function helpText() {
|
|
2950
4286
|
return [
|
|
@@ -2954,14 +4290,23 @@ function helpText() {
|
|
|
2954
4290
|
"sends Haven only the public signing address plus a proof signature.",
|
|
2955
4291
|
"",
|
|
2956
4292
|
"Usage:",
|
|
2957
|
-
" npx -y @haven_ai/connect --setup hv_setup_... --api https://api.haven.example --ack-local-tools
|
|
4293
|
+
" npx -y @haven_ai/connect --setup hv_setup_... --api https://api.haven.example --ack-local-tools",
|
|
2958
4294
|
"",
|
|
2959
4295
|
"Options:",
|
|
2960
4296
|
" --setup <token> Short-lived setup token from Haven.",
|
|
2961
4297
|
" --api <url> Haven backend API URL. Defaults to HAVEN_API_URL or http://localhost:3001.",
|
|
2962
4298
|
" --runtime <name> Agent runtime hint, such as claude-code, codex-cli, codex-desktop, cursor, vscode, claude-desktop, or hermes.",
|
|
4299
|
+
" Usually unnecessary: the connector detects the runtime it runs inside, and a detection",
|
|
4300
|
+
" that contradicts this hint wins (with a printed notice). When nothing is detected, an",
|
|
4301
|
+
" interactive terminal is offered the agent clients installed on this machine; this flag is",
|
|
4302
|
+
" how an agent, or a non-interactive run, answers instead. An unknown name is refused, never guessed.",
|
|
4303
|
+
" --runtime-force <name> Escape hatch: use exactly this runtime, ignoring environment detection.",
|
|
2963
4304
|
" --credentials-dir <path> Credential directory fallback. Defaults to ~/.haven/agents.",
|
|
2964
4305
|
" --environment-label <text> Non-sensitive label shown in Haven setup review.",
|
|
4306
|
+
" --name <slug> Wiring slug for a NAMED agent: writes haven-<slug> / haven-signer-<slug>",
|
|
4307
|
+
" MCP entries and stores credentials at ~/.haven/agents/<slug>/, so several",
|
|
4308
|
+
" agents can run side by side in one runtime. 1-32 lowercase letters, digits,",
|
|
4309
|
+
" single hyphens; immutable once wired. Omit for the bare haven / haven-signer pair.",
|
|
2965
4310
|
" --ack-local-tools Write the one-time local Haven tools acknowledgement during setup.",
|
|
2966
4311
|
" --ack-signer Backward-compatible alias for --ack-local-tools.",
|
|
2967
4312
|
" --local Advanced: install the fully-local Haven MCP (no hosted dependency).",
|
|
@@ -2972,6 +4317,19 @@ function helpText() {
|
|
|
2972
4317
|
" --repair Repair, then re-diagnose (implies --doctor): reinstall the pinned signer",
|
|
2973
4318
|
" runtime, rewrite the wrapper and runtime config from stored credentials.",
|
|
2974
4319
|
" Hosted topology only (refuses to touch a --local config). No keys, no token.",
|
|
4320
|
+
" --rekey Replace this agent's signing key (no token). Generates a fresh keypair HERE and",
|
|
4321
|
+
" prints its public address to paste into the Haven agent page. Nothing changes",
|
|
4322
|
+
" until you finish; the agent keeps working on its old key throughout.",
|
|
4323
|
+
" Add --name <slug> for a named agent. Refuses a legacy-rail or revoked agent.",
|
|
4324
|
+
" --rekey-finish Second half of --rekey: writes the new key and the API key the agent page",
|
|
4325
|
+
" showed once, in place at the same path, and rewrites only this agent's MCP",
|
|
4326
|
+
" config pair. Server names do not change, so wired hosts need only a restart.",
|
|
4327
|
+
" --api-key <key> The new API key, for --rekey-finish.",
|
|
4328
|
+
" --tombstone <dir> Retire an agent credential directory in place (no token): replaces its signer",
|
|
4329
|
+
" wrapper with a diagnostic that names the retirement in MCP stderr logs, and",
|
|
4330
|
+
" writes TOMBSTONE.json. Touches NO key material and revokes nothing.",
|
|
4331
|
+
" --reason <text> Reason recorded in the tombstone (with --tombstone).",
|
|
4332
|
+
" --replaced-by <agent-id> Successor agent recorded in the tombstone (with --tombstone).",
|
|
2975
4333
|
" --help Show this help.",
|
|
2976
4334
|
"",
|
|
2977
4335
|
"The connector never prints the private key and never sends it to Haven. JSON output never includes credential contents or full credential paths."
|
|
@@ -2984,6 +4342,12 @@ function requireValue(argv, index, option) {
|
|
|
2984
4342
|
}
|
|
2985
4343
|
return value;
|
|
2986
4344
|
}
|
|
4345
|
+
|
|
4346
|
+
// src/index.ts
|
|
4347
|
+
init_api();
|
|
4348
|
+
init_key();
|
|
4349
|
+
init_redact();
|
|
4350
|
+
init_redact();
|
|
2987
4351
|
async function runCli(argv, io = {
|
|
2988
4352
|
stdout: (message) => process.stdout.write(message),
|
|
2989
4353
|
stderr: (message) => process.stderr.write(message)
|
|
@@ -3007,6 +4371,103 @@ async function runCli(argv, io = {
|
|
|
3007
4371
|
`);
|
|
3008
4372
|
return 0;
|
|
3009
4373
|
}
|
|
4374
|
+
if (parsed.tombstone) {
|
|
4375
|
+
const { writeAgentTombstone: writeAgentTombstone2 } = await Promise.resolve().then(() => (init_tombstone(), tombstone_exports));
|
|
4376
|
+
const { readFile: readFile12 } = await import('fs/promises');
|
|
4377
|
+
const { join: join10 } = await import('path');
|
|
4378
|
+
try {
|
|
4379
|
+
let agentId = "unknown";
|
|
4380
|
+
try {
|
|
4381
|
+
const identity = JSON.parse(
|
|
4382
|
+
await readFile12(join10(parsed.tombstone.directory, "identity.json"), "utf8")
|
|
4383
|
+
);
|
|
4384
|
+
agentId = identity.agent_id ?? "unknown";
|
|
4385
|
+
} catch {
|
|
4386
|
+
}
|
|
4387
|
+
const info = await writeAgentTombstone2({
|
|
4388
|
+
directory: parsed.tombstone.directory,
|
|
4389
|
+
agentId,
|
|
4390
|
+
reason: parsed.tombstone.reason ?? "retired by operator via --tombstone",
|
|
4391
|
+
replacedBy: parsed.tombstone.replacedBy
|
|
4392
|
+
});
|
|
4393
|
+
if (parsed.json) {
|
|
4394
|
+
io.stdout(`${redactSecrets(JSON.stringify({ tombstoned: true, ...info }))}
|
|
4395
|
+
`);
|
|
4396
|
+
} else {
|
|
4397
|
+
io.stdout(redactSecrets(`Tombstoned agent ${info.agent_id} at ${parsed.tombstone.directory}.
|
|
4398
|
+
`));
|
|
4399
|
+
io.stdout(
|
|
4400
|
+
"Key files were NOT touched and nothing was revoked \u2014 revoke the agent on the Haven agent page if you have not already.\n"
|
|
4401
|
+
);
|
|
4402
|
+
io.stdout(
|
|
4403
|
+
"Restart EVERY long-lived MCP host (gateway, TUI workers, editors): each holds the wiring snapshot from its own start time, and the tombstone only speaks when a stale host next probes the old path.\n"
|
|
4404
|
+
);
|
|
4405
|
+
}
|
|
4406
|
+
return 0;
|
|
4407
|
+
} catch (err) {
|
|
4408
|
+
io.stderr(`${redactSecrets(err instanceof Error ? err.message : String(err))}
|
|
4409
|
+
`);
|
|
4410
|
+
return 1;
|
|
4411
|
+
}
|
|
4412
|
+
}
|
|
4413
|
+
if (parsed.rekey) {
|
|
4414
|
+
const { startRekey: startRekey2, finishRekey: finishRekey2 } = await Promise.resolve().then(() => (init_rekey(), rekey_exports));
|
|
4415
|
+
const { restartGuidance: restartGuidance2 } = await Promise.resolve().then(() => (init_rekey_restart(), rekey_restart_exports));
|
|
4416
|
+
const common = {
|
|
4417
|
+
serverName: parsed.options.serverName,
|
|
4418
|
+
credentialsDir: parsed.options.credentialsDir,
|
|
4419
|
+
runtime: parsed.options.runtime
|
|
4420
|
+
};
|
|
4421
|
+
try {
|
|
4422
|
+
if (parsed.rekey.phase === "start") {
|
|
4423
|
+
const result2 = await startRekey2(common);
|
|
4424
|
+
if (parsed.json) {
|
|
4425
|
+
io.stdout(
|
|
4426
|
+
`${redactSecrets(
|
|
4427
|
+
JSON.stringify({
|
|
4428
|
+
rekey: "started",
|
|
4429
|
+
agent_id: result2.agentId,
|
|
4430
|
+
new_delegate_address: result2.newDelegateAddress,
|
|
4431
|
+
expires_at: result2.expiresAt
|
|
4432
|
+
})
|
|
4433
|
+
)}
|
|
4434
|
+
`
|
|
4435
|
+
);
|
|
4436
|
+
} else {
|
|
4437
|
+
for (const line of result2.messages) io.stdout(redactSecrets(`${line}
|
|
4438
|
+
`));
|
|
4439
|
+
}
|
|
4440
|
+
return 0;
|
|
4441
|
+
}
|
|
4442
|
+
const result = await finishRekey2({ ...common, newApiKey: parsed.rekey.newApiKey });
|
|
4443
|
+
const restart = restartGuidance2(parsed.options.runtime);
|
|
4444
|
+
if (parsed.json) {
|
|
4445
|
+
io.stdout(
|
|
4446
|
+
`${redactSecrets(
|
|
4447
|
+
JSON.stringify({
|
|
4448
|
+
rekey: "finished",
|
|
4449
|
+
agent_id: result.agentId,
|
|
4450
|
+
new_delegate_address: result.newDelegateAddress,
|
|
4451
|
+
mcp_servers: result.serverNames,
|
|
4452
|
+
restart_commands: restart.commands
|
|
4453
|
+
})
|
|
4454
|
+
)}
|
|
4455
|
+
`
|
|
4456
|
+
);
|
|
4457
|
+
} else {
|
|
4458
|
+
for (const line of result.messages) io.stdout(redactSecrets(`${line}
|
|
4459
|
+
`));
|
|
4460
|
+
io.stdout("\n");
|
|
4461
|
+
for (const line of restart.lines) io.stdout(redactSecrets(`${line}
|
|
4462
|
+
`));
|
|
4463
|
+
}
|
|
4464
|
+
return 0;
|
|
4465
|
+
} catch (err) {
|
|
4466
|
+
io.stderr(`${redactSecrets(err instanceof Error ? err.message : String(err))}
|
|
4467
|
+
`);
|
|
4468
|
+
return 1;
|
|
4469
|
+
}
|
|
4470
|
+
}
|
|
3010
4471
|
if (parsed.doctor || parsed.repair) {
|
|
3011
4472
|
const { runDoctor: runDoctor2, runRepair: runRepair2 } = await Promise.resolve().then(() => (init_doctor(), doctor_exports));
|
|
3012
4473
|
const runtime = parsed.options.runtime ?? "";
|
|
@@ -3029,6 +4490,23 @@ async function runCli(argv, io = {
|
|
|
3029
4490
|
if (check.repair) io.stdout(redactSecrets(` \u21B3 repair: ${check.repair}
|
|
3030
4491
|
`));
|
|
3031
4492
|
}
|
|
4493
|
+
const otherAgents = report.agents.filter((agent) => agent.directory !== report.credentialDirectory);
|
|
4494
|
+
if (otherAgents.length > 0) {
|
|
4495
|
+
io.stdout("\nOther agents on this machine:\n");
|
|
4496
|
+
for (const agent of otherAgents) {
|
|
4497
|
+
const name = agent.slug ? `${agent.slug} (${agent.agentId ?? "unknown"})` : agent.agentId ?? "unknown";
|
|
4498
|
+
const failed = agent.checks.filter((check) => !check.ok);
|
|
4499
|
+
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;
|
|
4500
|
+
io.stdout(redactSecrets(` ${failed.length > 0 ? "\u2717" : "\u2022"} ${name}: ${verdict}
|
|
4501
|
+
`));
|
|
4502
|
+
for (const check of failed) {
|
|
4503
|
+
io.stdout(redactSecrets(` \u2717 ${check.label}: ${check.detail}
|
|
4504
|
+
`));
|
|
4505
|
+
if (check.repair) io.stdout(redactSecrets(` \u21B3 repair: ${check.repair}
|
|
4506
|
+
`));
|
|
4507
|
+
}
|
|
4508
|
+
}
|
|
4509
|
+
}
|
|
3032
4510
|
io.stdout(report.ok ? "All checks passed.\n" : "One or more checks FAILED \u2014 see repairs above.\n");
|
|
3033
4511
|
}
|
|
3034
4512
|
return report.ok ? 0 : 1;
|
|
@@ -3040,7 +4518,15 @@ async function runCli(argv, io = {
|
|
|
3040
4518
|
}
|
|
3041
4519
|
try {
|
|
3042
4520
|
const result = await runConnect(
|
|
3043
|
-
{
|
|
4521
|
+
{
|
|
4522
|
+
...parsed.options,
|
|
4523
|
+
waitForApproval: !parsed.json,
|
|
4524
|
+
// #1719: only a human-facing run may be asked which installed client to
|
|
4525
|
+
// configure. --json is the automation contract — it must fail with a
|
|
4526
|
+
// machine-readable code, never block on stdin. runConnect additionally
|
|
4527
|
+
// requires a real TTY before it prompts.
|
|
4528
|
+
interactive: !parsed.json
|
|
4529
|
+
},
|
|
3044
4530
|
{
|
|
3045
4531
|
log: (message) => (parsed.json ? io.stderr : io.stdout)(`${message}
|
|
3046
4532
|
`),
|
|
@@ -3076,10 +4562,13 @@ function isCliEntrypoint(argvPath = process.argv[1], moduleUrl = import.meta.url
|
|
|
3076
4562
|
if (isCliEntrypoint()) void main();
|
|
3077
4563
|
|
|
3078
4564
|
// src/index.ts
|
|
4565
|
+
init_runtime_install();
|
|
3079
4566
|
init_signer_runtime();
|
|
3080
4567
|
init_runtime_registry();
|
|
4568
|
+
init_connect_error();
|
|
4569
|
+
init_storage();
|
|
3081
4570
|
init_runtime_manifest();
|
|
3082
4571
|
|
|
3083
|
-
export { CONNECTOR_VERSION, CONNECT_OUTCOME_SCHEMA_VERSION, MCP_RUNTIME_MANIFEST, completionOutcome, createConnectApiClient, defaultAgentDirectory, delegateKeyFromPrivateKey, failedConnectOutcome, generateDelegateKey, helpText, installRuntime, mcpPackageSpec, normalizeRuntime, parseArgs, prepareSignerRuntime, redactSecrets, runCli, runConnect, runtimeInstallCapabilities, runtimeProfile, sdkPackageSpec, shortAddress, signerPackageSpec, writeCredentialFiles };
|
|
4572
|
+
export { CONNECTOR_VERSION, CONNECT_OUTCOME_SCHEMA_VERSION, ConnectError, MCP_RUNTIME_MANIFEST, RUNTIME_FLAG_VALUES, completionOutcome, createConnectApiClient, defaultAgentDirectory, delegateKeyFromPrivateKey, failedConnectOutcome, generateDelegateKey, helpText, installRuntime, isConnectError, mcpPackageSpec, normalizeRuntime, parseArgs, prepareSignerRuntime, promptForInstalledClient, redactSecrets, resolveRuntimeByInstalledClientPrompt, resolveRuntimeSelection, runCli, runConnect, runtimeInstallCapabilities, runtimeProfile, scanInstalledClients, sdkPackageSpec, shortAddress, signerPackageSpec, writeCredentialFiles };
|
|
3084
4573
|
//# sourceMappingURL=index.js.map
|
|
3085
4574
|
//# sourceMappingURL=index.js.map
|