@haven_ai/connect 0.1.26-alpha.0 → 0.1.27-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 +68 -2
- package/dist/cli.cjs +1220 -660
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1224 -664
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1234 -670
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +73 -7
- package/dist/index.d.ts +73 -7
- package/dist/index.js +1217 -652
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,262 +1,25 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import { mkdir, rm, chmod, access, writeFile, readFile, unlink } from 'fs/promises';
|
|
1
|
+
import { ensureConsent, computeConsentHash, loadCredentials, consentInputFromClient, registeredToolNames, MCP_VERSION } from '@haven_ai/mcp';
|
|
2
|
+
import { ensureSignerConsent, computeSignerConsentHash, loadSignerCredentials, createEdgeSigner, toolSchemas } from '@haven_ai/signer';
|
|
3
|
+
import { isSupportedNodeVersion, unsupportedNodeVersionMessage, SKILL_FOLDER_NAME, resolveTokenFromAddress, HAVEN_SKILL_MD, HAVEN_SKILL_BODY_MD, HAVEN_MINIMUM_NODE_VERSION } from '@haven_ai/sdk';
|
|
4
|
+
import { mkdir, rm, chmod, access, writeFile, readFile, unlink, readdir, stat } from 'fs/promises';
|
|
5
5
|
import { homedir, platform } from 'os';
|
|
6
6
|
import { join, resolve, dirname } from 'path';
|
|
7
|
+
import { parseDocument, isMap, stringify } from 'yaml';
|
|
7
8
|
import { execFile, spawn } from 'child_process';
|
|
8
9
|
import { promisify } from 'util';
|
|
9
|
-
import
|
|
10
|
-
import {
|
|
11
|
-
import { ensureSignerConsent, computeSignerConsentHash, loadSignerCredentials, createEdgeSigner, toolSchemas } from '@haven_ai/signer';
|
|
10
|
+
import crypto from 'crypto';
|
|
11
|
+
import { Wallet } from 'ethers';
|
|
12
12
|
import { realpathSync } from 'fs';
|
|
13
13
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
return
|
|
19
|
-
resolveSetup: (input) => request(fetchImpl, `${root}/agent-connection-setups/resolve`, {
|
|
20
|
-
method: "POST",
|
|
21
|
-
body: JSON.stringify({
|
|
22
|
-
setup_token: input.setupToken,
|
|
23
|
-
connector_version: input.connectorVersion,
|
|
24
|
-
runtime: input.runtime
|
|
25
|
-
})
|
|
26
|
-
}),
|
|
27
|
-
registerSetup: (input) => request(fetchImpl, `${root}/agent-connection-setups/register`, {
|
|
28
|
-
method: "POST",
|
|
29
|
-
body: JSON.stringify({
|
|
30
|
-
setup_token: input.setupToken,
|
|
31
|
-
challenge_id: input.challengeId,
|
|
32
|
-
delegate_address: input.delegateAddress,
|
|
33
|
-
proof_signature: input.proofSignature,
|
|
34
|
-
api_key_hash: input.apiKeyHash,
|
|
35
|
-
api_key_prefix: input.apiKeyPrefix,
|
|
36
|
-
runtime: input.runtime,
|
|
37
|
-
connector_version: input.connectorVersion,
|
|
38
|
-
connector_context: input.connectorContext,
|
|
39
|
-
install_capabilities: input.installCapabilities && {
|
|
40
|
-
can_write_runtime_config: input.installCapabilities.canWriteRuntimeConfig,
|
|
41
|
-
restart_required: input.installCapabilities.restartRequired
|
|
42
|
-
}
|
|
43
|
-
})
|
|
44
|
-
}),
|
|
45
|
-
getConnectorStatus: (setupId, apiKey) => request(fetchImpl, `${root}/agent-connection-setups/${encodeURIComponent(setupId)}/connector-status`, {
|
|
46
|
-
method: "GET",
|
|
47
|
-
headers: { Authorization: `Bearer ${apiKey}` }
|
|
48
|
-
}),
|
|
49
|
-
updateInstallStatus: async (setupId, apiKey, input) => {
|
|
50
|
-
await request(fetchImpl, `${root}/agent-connection-setups/${encodeURIComponent(setupId)}/install-status`, {
|
|
51
|
-
method: "POST",
|
|
52
|
-
headers: { Authorization: `Bearer ${apiKey}` },
|
|
53
|
-
body: JSON.stringify({
|
|
54
|
-
runtime: input.runtime,
|
|
55
|
-
connector_version: input.connectorVersion,
|
|
56
|
-
runtime_mcp_mode: input.runtimeMcpMode,
|
|
57
|
-
hosted_mcp_configured: input.hostedMcpConfigured,
|
|
58
|
-
local_signer_configured: input.localSignerConfigured,
|
|
59
|
-
local_mcp_configured: input.localMcpConfigured,
|
|
60
|
-
credential_files_written: input.credentialFilesWritten,
|
|
61
|
-
signer_acknowledged: input.signerAcknowledged,
|
|
62
|
-
local_mcp_acknowledged: input.localMcpAcknowledged,
|
|
63
|
-
activation_command_available: input.activationCommandAvailable,
|
|
64
|
-
skill_installed: input.skillInstalled,
|
|
65
|
-
probe_result: input.probeResult,
|
|
66
|
-
restart_required: input.restartRequired,
|
|
67
|
-
next_user_action: input.nextUserAction,
|
|
68
|
-
error_code: input.errorCode ?? null,
|
|
69
|
-
environment_label: input.environmentLabel
|
|
70
|
-
})
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
var ConnectRequestError = class extends Error {
|
|
76
|
-
constructor(message, status) {
|
|
77
|
-
super(message);
|
|
78
|
-
this.status = status;
|
|
79
|
-
this.name = "ConnectRequestError";
|
|
80
|
-
}
|
|
81
|
-
status;
|
|
15
|
+
var __defProp = Object.defineProperty;
|
|
16
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
17
|
+
var __esm = (fn, res) => function __init() {
|
|
18
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
82
19
|
};
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
headers: {
|
|
87
|
-
"Content-Type": "application/json",
|
|
88
|
-
...init.headers ?? {}
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
|
-
const text = await response.text();
|
|
92
|
-
const body = text ? JSON.parse(text) : null;
|
|
93
|
-
if (!response.ok) {
|
|
94
|
-
const message = body?.error ?? body?.message ?? `${response.status} ${response.statusText}`;
|
|
95
|
-
throw new ConnectRequestError(`Haven setup request failed: ${message}`, response.status);
|
|
96
|
-
}
|
|
97
|
-
return body;
|
|
98
|
-
}
|
|
99
|
-
function generateDelegateKey() {
|
|
100
|
-
return delegateKeyFromPrivateKey(Wallet.createRandom().privateKey);
|
|
101
|
-
}
|
|
102
|
-
function delegateKeyFromPrivateKey(privateKey) {
|
|
103
|
-
const wallet = new Wallet(privateKey);
|
|
104
|
-
return {
|
|
105
|
-
privateKey: wallet.privateKey,
|
|
106
|
-
address: wallet.address,
|
|
107
|
-
signChallenge: (message) => wallet.signMessage(message)
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
function generateAgentApiKey() {
|
|
111
|
-
return `sk_agent_${crypto.randomBytes(24).toString("hex")}`;
|
|
112
|
-
}
|
|
113
|
-
function hashAgentApiKey(apiKey) {
|
|
114
|
-
return crypto.createHash("sha256").update(apiKey).digest("hex");
|
|
115
|
-
}
|
|
116
|
-
function agentApiKeyPrefix(apiKey) {
|
|
117
|
-
return apiKey.slice(0, 12);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// src/redact.ts
|
|
121
|
-
var API_KEY_RE = /sk_agent_[A-Za-z0-9]+/g;
|
|
122
|
-
var PRIVATE_KEY_RE = /0x[0-9a-fA-F]{64}/g;
|
|
123
|
-
function redactSecrets(value) {
|
|
124
|
-
return value.replace(API_KEY_RE, "sk_agent_[redacted]").replace(PRIVATE_KEY_RE, "0x[redacted-private-key]");
|
|
125
|
-
}
|
|
126
|
-
function shortAddress(address) {
|
|
127
|
-
if (!/^0x[0-9a-fA-F]{40}$/.test(address)) return address;
|
|
128
|
-
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
|
129
|
-
}
|
|
130
|
-
async function preflightCredentialStorage(input = {}) {
|
|
131
|
-
const directory = defaultCredentialRoot(input.baseDir);
|
|
132
|
-
await mkdir(directory, { recursive: true, mode: 448 });
|
|
133
|
-
await restrictPermissions(directory, 448, input.warn);
|
|
134
|
-
const probePath = join(directory, `.haven-connect-preflight-${crypto.randomBytes(8).toString("hex")}`);
|
|
135
|
-
try {
|
|
136
|
-
await writeOwnerOnlyJson(probePath, { ok: true }, input.warn);
|
|
137
|
-
} finally {
|
|
138
|
-
await rm(probePath, { force: true }).catch(() => void 0);
|
|
139
|
-
}
|
|
140
|
-
return directory;
|
|
141
|
-
}
|
|
142
|
-
async function writeCredentialFiles(input) {
|
|
143
|
-
const directory = defaultAgentDirectory(input.agentId, input.baseDir);
|
|
144
|
-
await mkdir(directory, { recursive: true, mode: 448 });
|
|
145
|
-
await restrictPermissions(directory, 448, input.warn);
|
|
146
|
-
const identityPath = join(directory, "identity.json");
|
|
147
|
-
const signerPath = join(directory, "signer.json");
|
|
148
|
-
const agentPath = join(directory, "agent.json");
|
|
149
|
-
await assertDoesNotExist(identityPath);
|
|
150
|
-
await assertDoesNotExist(signerPath);
|
|
151
|
-
await assertDoesNotExist(agentPath);
|
|
152
|
-
await writeOwnerOnlyJson(
|
|
153
|
-
signerPath,
|
|
154
|
-
{
|
|
155
|
-
delegate_key: input.delegateKey,
|
|
156
|
-
delegate_address: input.delegateAddress,
|
|
157
|
-
agent_id: input.agentId,
|
|
158
|
-
safe_address: input.safeAddress,
|
|
159
|
-
chain_id: input.chainId,
|
|
160
|
-
network: input.network,
|
|
161
|
-
x402_binding_signer: input.x402BindingSigner,
|
|
162
|
-
note: "Local signer credential. Haven backend never receives this private key."
|
|
163
|
-
},
|
|
164
|
-
input.warn
|
|
165
|
-
);
|
|
166
|
-
try {
|
|
167
|
-
await writeOwnerOnlyJson(
|
|
168
|
-
identityPath,
|
|
169
|
-
{
|
|
170
|
-
api_key: input.apiKey,
|
|
171
|
-
agent_id: input.agentId,
|
|
172
|
-
safe_address: input.safeAddress,
|
|
173
|
-
chain_id: input.chainId,
|
|
174
|
-
network: input.network,
|
|
175
|
-
api_url: input.apiUrl,
|
|
176
|
-
hosted_mcp_url: input.hostedMcpUrl,
|
|
177
|
-
agent_budget: input.agentBudget,
|
|
178
|
-
note: "Haven API key identifies the agent only. It cannot spend without the local signer key and on-chain Haven wallet rules."
|
|
179
|
-
},
|
|
180
|
-
input.warn
|
|
181
|
-
);
|
|
182
|
-
} catch (err) {
|
|
183
|
-
await rm(signerPath, { force: true }).catch(() => void 0);
|
|
184
|
-
throw err;
|
|
185
|
-
}
|
|
186
|
-
try {
|
|
187
|
-
await writeOwnerOnlyJson(
|
|
188
|
-
agentPath,
|
|
189
|
-
{
|
|
190
|
-
agent_id: input.agentId,
|
|
191
|
-
delegate_address: input.delegateAddress,
|
|
192
|
-
safe_address: input.safeAddress,
|
|
193
|
-
chain_id: input.chainId,
|
|
194
|
-
network: input.network,
|
|
195
|
-
agent_budget: input.agentBudget,
|
|
196
|
-
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."
|
|
197
|
-
},
|
|
198
|
-
input.warn
|
|
199
|
-
);
|
|
200
|
-
} catch (err) {
|
|
201
|
-
await rm(signerPath, { force: true }).catch(() => void 0);
|
|
202
|
-
await rm(identityPath, { force: true }).catch(() => void 0);
|
|
203
|
-
await rm(agentPath, { force: true }).catch(() => void 0);
|
|
204
|
-
throw err;
|
|
205
|
-
}
|
|
206
|
-
return { directory, identityPath, signerPath, agentPath };
|
|
207
|
-
}
|
|
208
|
-
function defaultAgentDirectory(agentId, baseDir = join(homedir(), ".haven", "agents")) {
|
|
209
|
-
return resolve(defaultCredentialRoot(baseDir), safePathPart(agentId));
|
|
210
|
-
}
|
|
211
|
-
function defaultCredentialRoot(baseDir = join(homedir(), ".haven", "agents")) {
|
|
212
|
-
return resolve(baseDir);
|
|
213
|
-
}
|
|
214
|
-
async function writeOwnerOnlyJson(path, value, warn) {
|
|
215
|
-
const json = JSON.stringify(dropUndefined(value), null, 2);
|
|
216
|
-
await writeFile(path, `${json}
|
|
217
|
-
`, { mode: 384, flag: "wx" });
|
|
218
|
-
await restrictPermissions(path, 384, warn);
|
|
219
|
-
}
|
|
220
|
-
function safePathPart(value) {
|
|
221
|
-
return value.replace(/[^A-Za-z0-9_.-]/g, "_");
|
|
222
|
-
}
|
|
223
|
-
function dropUndefined(value) {
|
|
224
|
-
return Object.fromEntries(Object.entries(value).filter(([, field]) => field !== void 0));
|
|
225
|
-
}
|
|
226
|
-
async function assertDoesNotExist(path) {
|
|
227
|
-
try {
|
|
228
|
-
await access(path);
|
|
229
|
-
} catch (err) {
|
|
230
|
-
if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") return;
|
|
231
|
-
throw err;
|
|
232
|
-
}
|
|
233
|
-
throw new Error(`Refusing to overwrite existing Haven credential file: ${path}`);
|
|
234
|
-
}
|
|
235
|
-
async function restrictPermissions(path, mode, warn) {
|
|
236
|
-
try {
|
|
237
|
-
await chmod(path, mode);
|
|
238
|
-
} catch (err) {
|
|
239
|
-
warn?.(
|
|
240
|
-
`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)}`
|
|
241
|
-
);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
var MCP_RUNTIME_MANIFEST = {
|
|
245
|
-
mcpPackage: "@haven_ai/mcp",
|
|
246
|
-
mcpVersion: MCP_VERSION,
|
|
247
|
-
sdkPackage: "@haven_ai/sdk",
|
|
248
|
-
sdkVersion: "0.1.26-alpha.0",
|
|
249
|
-
signerPackage: "@haven_ai/signer",
|
|
250
|
-
signerVersion: "0.1.26-alpha.0",
|
|
251
|
-
// Sourced from the SDK, never a literal (#1161). This field read '20.0.0'
|
|
252
|
-
// while every package's `engines` said `>=24` and the docs said `>=24.0.0`,
|
|
253
|
-
// so the guard that was supposed to enforce the floor waved Node v23 through
|
|
254
|
-
// — including on the `--local` path where it does run. A hand-maintained
|
|
255
|
-
// second copy of a number is a drift waiting to happen; a guard test pins
|
|
256
|
-
// this against `package.json`'s `engines.node`.
|
|
257
|
-
minimumNodeVersion: HAVEN_MINIMUM_NODE_VERSION,
|
|
258
|
-
supportedClients: ["codex-cli", "codex-desktop", "claude-code"],
|
|
259
|
-
requiredTools: registeredToolNames()
|
|
20
|
+
var __export = (target, all) => {
|
|
21
|
+
for (var name in all)
|
|
22
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
260
23
|
};
|
|
261
24
|
function mcpPackageSpec() {
|
|
262
25
|
return `${MCP_RUNTIME_MANIFEST.mcpPackage}@${MCP_RUNTIME_MANIFEST.mcpVersion}`;
|
|
@@ -267,21 +30,35 @@ function sdkPackageSpec() {
|
|
|
267
30
|
function signerPackageSpec() {
|
|
268
31
|
return `${MCP_RUNTIME_MANIFEST.signerPackage}@${MCP_RUNTIME_MANIFEST.signerVersion}`;
|
|
269
32
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
33
|
+
var MCP_RUNTIME_MANIFEST;
|
|
34
|
+
var init_runtime_manifest = __esm({
|
|
35
|
+
"src/runtime-manifest.ts"() {
|
|
36
|
+
MCP_RUNTIME_MANIFEST = {
|
|
37
|
+
mcpPackage: "@haven_ai/mcp",
|
|
38
|
+
mcpVersion: MCP_VERSION,
|
|
39
|
+
sdkPackage: "@haven_ai/sdk",
|
|
40
|
+
sdkVersion: "0.1.27-alpha.0",
|
|
41
|
+
signerPackage: "@haven_ai/signer",
|
|
42
|
+
signerVersion: "0.1.27-alpha.0",
|
|
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)
|
|
59
|
+
};
|
|
283
60
|
}
|
|
284
|
-
};
|
|
61
|
+
});
|
|
285
62
|
async function writeRuntimeConfig(input, deps = {}) {
|
|
286
63
|
switch (input.runtime) {
|
|
287
64
|
case "codex-cli":
|
|
@@ -866,6 +643,25 @@ function hasBalancedTomlContainers(value) {
|
|
|
866
643
|
function tomlString(value) {
|
|
867
644
|
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
868
645
|
}
|
|
646
|
+
function runtimeConfigPathFor(runtime, homeDir = homedir()) {
|
|
647
|
+
switch (runtime) {
|
|
648
|
+
case "cursor":
|
|
649
|
+
return cursorConfigPath(homeDir);
|
|
650
|
+
case "codex-cli":
|
|
651
|
+
case "codex-desktop":
|
|
652
|
+
return codexConfigPath(homeDir);
|
|
653
|
+
case "vscode":
|
|
654
|
+
return vscodeConfigPath(homeDir);
|
|
655
|
+
case "vscode-insiders":
|
|
656
|
+
return vscodeInsidersConfigPath(homeDir);
|
|
657
|
+
case "claude-desktop":
|
|
658
|
+
return claudeDesktopConfigPath(homeDir);
|
|
659
|
+
case "hermes":
|
|
660
|
+
return hermesConfigPath(homeDir);
|
|
661
|
+
default:
|
|
662
|
+
return null;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
869
665
|
function cursorConfigPath(homeDir = homedir()) {
|
|
870
666
|
return resolve(homeDir, ".cursor", "mcp.json");
|
|
871
667
|
}
|
|
@@ -922,82 +718,25 @@ function configTargetLabel(runtime) {
|
|
|
922
718
|
return "runtime MCP config";
|
|
923
719
|
}
|
|
924
720
|
}
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
hash: decision.hash,
|
|
936
|
-
reason: decision.reason
|
|
937
|
-
};
|
|
938
|
-
} catch (err) {
|
|
939
|
-
return {
|
|
940
|
-
acknowledged: false,
|
|
941
|
-
error: err instanceof Error ? err.message : String(err)
|
|
942
|
-
};
|
|
943
|
-
}
|
|
944
|
-
}
|
|
945
|
-
async function getLocalMcpConsentStatus(identityPath, signerPath) {
|
|
946
|
-
try {
|
|
947
|
-
const input = await buildLocalMcpConsentInput(identityPath, signerPath);
|
|
948
|
-
const hash = computeConsentHash(input);
|
|
949
|
-
const stored = await readLocalMcpAckFile(localMcpAckPath(identityPath));
|
|
950
|
-
if (stored === hash) {
|
|
951
|
-
return { acknowledged: true, hash, reason: "ack_file_match" };
|
|
952
|
-
}
|
|
953
|
-
return {
|
|
954
|
-
acknowledged: false,
|
|
955
|
-
hash,
|
|
956
|
-
reason: stored ? "ack_file_mismatch" : "ack_file_missing"
|
|
721
|
+
var HERMES_API_KEY_ENV, HermesConfigRecoveryError, InvalidCodexTomlError;
|
|
722
|
+
var init_config_writers = __esm({
|
|
723
|
+
"src/config-writers.ts"() {
|
|
724
|
+
init_runtime_manifest();
|
|
725
|
+
HERMES_API_KEY_ENV = "MCP_HAVEN_API_KEY";
|
|
726
|
+
HermesConfigRecoveryError = class extends Error {
|
|
727
|
+
constructor() {
|
|
728
|
+
super("Hermes configuration recovery did not complete");
|
|
729
|
+
this.name = "HermesConfigRecoveryError";
|
|
730
|
+
}
|
|
957
731
|
};
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
732
|
+
InvalidCodexTomlError = class extends Error {
|
|
733
|
+
constructor(message) {
|
|
734
|
+
super(message);
|
|
735
|
+
this.name = "InvalidCodexTomlError";
|
|
736
|
+
}
|
|
962
737
|
};
|
|
963
738
|
}
|
|
964
|
-
}
|
|
965
|
-
function localMcpAckPath(identityPath) {
|
|
966
|
-
return resolve(`${identityPath}.ack.json`);
|
|
967
|
-
}
|
|
968
|
-
async function buildLocalMcpConsentInput(identityPath, signerPath) {
|
|
969
|
-
const credentials = await loadCredentials({ identityPath, signerPath });
|
|
970
|
-
const unavailableDuringSetup = {
|
|
971
|
-
getAllowances: async () => {
|
|
972
|
-
throw new Error("Haven approval is not complete yet.");
|
|
973
|
-
}
|
|
974
|
-
};
|
|
975
|
-
return consentInputFromClient(
|
|
976
|
-
unavailableDuringSetup,
|
|
977
|
-
{
|
|
978
|
-
apiKey: credentials.apiKey,
|
|
979
|
-
apiUrl: credentials.apiUrl,
|
|
980
|
-
agentId: credentials.agentId,
|
|
981
|
-
safeAddress: credentials.safeAddress,
|
|
982
|
-
delegateAddress: credentials.delegateAddress,
|
|
983
|
-
chainId: credentials.chainId,
|
|
984
|
-
allowanceSummary: credentials.allowanceSummary
|
|
985
|
-
},
|
|
986
|
-
registeredToolNames()
|
|
987
|
-
);
|
|
988
|
-
}
|
|
989
|
-
async function readLocalMcpAckFile(path) {
|
|
990
|
-
try {
|
|
991
|
-
const parsed = JSON.parse(await readFile(path, "utf8"));
|
|
992
|
-
return typeof parsed.ack === "string" ? parsed.ack : null;
|
|
993
|
-
} catch {
|
|
994
|
-
return null;
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
function writeLogChunk(log, chunk) {
|
|
998
|
-
const message = String(chunk).trimEnd();
|
|
999
|
-
if (message) log(message);
|
|
1000
|
-
}
|
|
739
|
+
});
|
|
1001
740
|
async function probeHostedMcpTools(apiKey, hostedMcpUrl, fetchImpl = fetch) {
|
|
1002
741
|
let response;
|
|
1003
742
|
try {
|
|
@@ -1040,6 +779,8 @@ async function probeLocalMcpTools(command, args, requiredTools, timeoutMs = 1e4)
|
|
|
1040
779
|
let stdout = "";
|
|
1041
780
|
let settled = false;
|
|
1042
781
|
let sawInitialize = false;
|
|
782
|
+
let serverInfo;
|
|
783
|
+
let capabilities;
|
|
1043
784
|
const finish = (result) => {
|
|
1044
785
|
if (settled) return;
|
|
1045
786
|
settled = true;
|
|
@@ -1071,6 +812,9 @@ async function probeLocalMcpTools(command, args, requiredTools, timeoutMs = 1e4)
|
|
|
1071
812
|
}
|
|
1072
813
|
if (payload.id === 1 && !sawInitialize) {
|
|
1073
814
|
sawInitialize = true;
|
|
815
|
+
const init = payload.result;
|
|
816
|
+
serverInfo = init?.serverInfo;
|
|
817
|
+
capabilities = init?.capabilities;
|
|
1074
818
|
writeJsonRpc(child, { jsonrpc: "2.0", method: "notifications/initialized", params: {} });
|
|
1075
819
|
writeJsonRpc(child, { jsonrpc: "2.0", id: 2, method: "tools/list", params: {} });
|
|
1076
820
|
continue;
|
|
@@ -1079,7 +823,7 @@ async function probeLocalMcpTools(command, args, requiredTools, timeoutMs = 1e4)
|
|
|
1079
823
|
const tools = payload.result?.tools;
|
|
1080
824
|
const toolNames = Array.isArray(tools) ? tools.map((tool) => tool && typeof tool === "object" && "name" in tool ? tool.name : void 0).filter((name) => typeof name === "string") : [];
|
|
1081
825
|
const missing = requiredTools.filter((name) => !toolNames.includes(name));
|
|
1082
|
-
finish({ status: missing.length === 0 ? "ok" : "missing_tools", toolNames });
|
|
826
|
+
finish({ status: missing.length === 0 ? "ok" : "missing_tools", toolNames, serverInfo, capabilities });
|
|
1083
827
|
return;
|
|
1084
828
|
}
|
|
1085
829
|
}
|
|
@@ -1128,51 +872,37 @@ async function fetchWithTimeout(fetchImpl, url, init) {
|
|
|
1128
872
|
clearTimeout(timeout);
|
|
1129
873
|
}
|
|
1130
874
|
}
|
|
1131
|
-
var
|
|
1132
|
-
|
|
1133
|
-
code = "local_mcp_unsupported_node_version";
|
|
1134
|
-
nodeVersion;
|
|
1135
|
-
minimumNodeVersion;
|
|
1136
|
-
constructor(nodeVersion, minimumNodeVersion, subject = "Haven setup") {
|
|
1137
|
-
super(unsupportedNodeVersionMessage({ subject, nodeVersion, minimumNodeVersion }));
|
|
1138
|
-
this.name = "UnsupportedNodeVersionError";
|
|
1139
|
-
this.nodeVersion = nodeVersion;
|
|
1140
|
-
this.minimumNodeVersion = minimumNodeVersion;
|
|
875
|
+
var init_probes = __esm({
|
|
876
|
+
"src/probes.ts"() {
|
|
1141
877
|
}
|
|
1142
|
-
};
|
|
1143
|
-
async function
|
|
1144
|
-
assertSupportedNodeVersion(input.nodeVersion);
|
|
878
|
+
});
|
|
879
|
+
async function prepareSignerRuntime(input, deps = {}) {
|
|
1145
880
|
const homeDir = input.homeDir ?? homedir();
|
|
1146
|
-
const runtimeDirectory = resolve(homeDir, ".haven", "
|
|
881
|
+
const runtimeDirectory = resolve(homeDir, ".haven", "signer-runtime", MCP_RUNTIME_MANIFEST.signerVersion);
|
|
1147
882
|
const npmCacheDirectory = resolve(homeDir, ".haven", "npm-cache");
|
|
1148
|
-
const cliPath = join(runtimeDirectory, "node_modules", "@haven_ai", "
|
|
883
|
+
const cliPath = join(runtimeDirectory, "node_modules", "@haven_ai", "signer", "dist", "cli.js");
|
|
1149
884
|
const messages = [];
|
|
1150
885
|
await mkdir(runtimeDirectory, { recursive: true, mode: 448 });
|
|
1151
886
|
await chmod(runtimeDirectory, 448).catch(() => void 0);
|
|
1152
887
|
await mkdir(npmCacheDirectory, { recursive: true, mode: 448 });
|
|
1153
888
|
await chmod(npmCacheDirectory, 448).catch(() => void 0);
|
|
1154
889
|
if (await installedRuntimeMatches(runtimeDirectory, cliPath)) {
|
|
1155
|
-
messages.push(`Using existing local Haven
|
|
890
|
+
messages.push(`Using existing local Haven signer runtime ${signerPackageSpec()}.`);
|
|
1156
891
|
} else {
|
|
1157
|
-
await installRuntimePackages(runtimeDirectory, npmCacheDirectory, deps
|
|
1158
|
-
messages.push(`Installed local Haven
|
|
892
|
+
await installRuntimePackages(runtimeDirectory, npmCacheDirectory, deps);
|
|
893
|
+
messages.push(`Installed local Haven signer runtime ${signerPackageSpec()}.`);
|
|
1159
894
|
}
|
|
1160
|
-
await assertFileExists(cliPath, "local Haven
|
|
1161
|
-
const wrapperPath = join(input.credentialDirectory, "bin", "haven-
|
|
1162
|
-
await writeWrapper({
|
|
1163
|
-
wrapperPath,
|
|
1164
|
-
cliPath,
|
|
1165
|
-
identityPath: input.identityPath,
|
|
1166
|
-
signerPath: input.signerPath
|
|
1167
|
-
});
|
|
895
|
+
await assertFileExists(cliPath, "local Haven signer CLI");
|
|
896
|
+
const wrapperPath = join(input.credentialDirectory, "bin", "haven-signer.mjs");
|
|
897
|
+
await writeWrapper({ wrapperPath, cliPath, signerPath: input.signerPath });
|
|
1168
898
|
await writeRuntimeSidecar({
|
|
1169
|
-
path: join(input.credentialDirectory, "
|
|
899
|
+
path: join(input.credentialDirectory, "signer-runtime.json"),
|
|
1170
900
|
wrapperPath,
|
|
1171
901
|
runtimeDirectory,
|
|
1172
902
|
npmCacheDirectory,
|
|
1173
903
|
cliPath
|
|
1174
904
|
});
|
|
1175
|
-
messages.push(`Prepared stable local Haven
|
|
905
|
+
messages.push(`Prepared stable local Haven signer wrapper: ${wrapperPath}`);
|
|
1176
906
|
return {
|
|
1177
907
|
command: wrapperPath,
|
|
1178
908
|
args: [],
|
|
@@ -1183,12 +913,8 @@ async function prepareLocalMcpRuntime(input, deps = {}) {
|
|
|
1183
913
|
messages
|
|
1184
914
|
};
|
|
1185
915
|
}
|
|
1186
|
-
function
|
|
1187
|
-
|
|
1188
|
-
throw new UnsupportedNodeVersionError(nodeVersion, minimumNodeVersion, subject);
|
|
1189
|
-
}
|
|
1190
|
-
}
|
|
1191
|
-
async function installRuntimePackages(runtimeDirectory, npmCacheDirectory, runCommand) {
|
|
916
|
+
async function installRuntimePackages(runtimeDirectory, npmCacheDirectory, deps) {
|
|
917
|
+
const { runCommand, onProgress } = deps;
|
|
1192
918
|
const baseArgs = [
|
|
1193
919
|
"install",
|
|
1194
920
|
"--prefix",
|
|
@@ -1197,12 +923,22 @@ async function installRuntimePackages(runtimeDirectory, npmCacheDirectory, runCo
|
|
|
1197
923
|
"--no-fund",
|
|
1198
924
|
"--omit=dev",
|
|
1199
925
|
"--prefer-offline",
|
|
1200
|
-
|
|
926
|
+
signerPackageSpec(),
|
|
1201
927
|
sdkPackageSpec()
|
|
1202
928
|
];
|
|
1203
929
|
const run = async (args) => {
|
|
1204
|
-
|
|
1205
|
-
|
|
930
|
+
const startedAt = Date.now();
|
|
931
|
+
const heartbeat = setInterval(() => {
|
|
932
|
+
const seconds = Math.round((Date.now() - startedAt) / 1e3);
|
|
933
|
+
onProgress?.(`Still installing the local Haven signer runtime\u2026 (${seconds}s \u2014 a cold cache can take several minutes)`);
|
|
934
|
+
}, SIGNER_INSTALL_HEARTBEAT_MS);
|
|
935
|
+
heartbeat.unref?.();
|
|
936
|
+
try {
|
|
937
|
+
if (runCommand) await runCommand("npm", args);
|
|
938
|
+
else await execFileAsync("npm", args, { timeout: SIGNER_INSTALL_TIMEOUT_MS, maxBuffer: 1024 * 1024 });
|
|
939
|
+
} finally {
|
|
940
|
+
clearInterval(heartbeat);
|
|
941
|
+
}
|
|
1206
942
|
};
|
|
1207
943
|
try {
|
|
1208
944
|
await run(baseArgs);
|
|
@@ -1210,18 +946,20 @@ async function installRuntimePackages(runtimeDirectory, npmCacheDirectory, runCo
|
|
|
1210
946
|
try {
|
|
1211
947
|
await run([...baseArgs, "--cache", npmCacheDirectory]);
|
|
1212
948
|
} catch (err) {
|
|
1213
|
-
throw new Error(
|
|
949
|
+
throw new Error(
|
|
950
|
+
`Could not install local Haven signer runtime ${signerPackageSpec()}: ${err instanceof Error ? err.message : String(err)}`
|
|
951
|
+
);
|
|
1214
952
|
}
|
|
1215
953
|
}
|
|
1216
954
|
}
|
|
1217
955
|
async function installedRuntimeMatches(runtimeDirectory, cliPath) {
|
|
1218
956
|
try {
|
|
1219
|
-
await assertFileExists(cliPath, "local Haven
|
|
1220
|
-
const [
|
|
1221
|
-
readPackageJson(join(runtimeDirectory, "node_modules", "@haven_ai", "
|
|
957
|
+
await assertFileExists(cliPath, "local Haven signer CLI");
|
|
958
|
+
const [signerPackage, sdkPackage] = await Promise.all([
|
|
959
|
+
readPackageJson(join(runtimeDirectory, "node_modules", "@haven_ai", "signer", "package.json")),
|
|
1222
960
|
readPackageJson(join(runtimeDirectory, "node_modules", "@haven_ai", "sdk", "package.json"))
|
|
1223
961
|
]);
|
|
1224
|
-
return
|
|
962
|
+
return signerPackage.version === MCP_RUNTIME_MANIFEST.signerVersion && sdkPackage.version === MCP_RUNTIME_MANIFEST.sdkVersion;
|
|
1225
963
|
} catch {
|
|
1226
964
|
return false;
|
|
1227
965
|
}
|
|
@@ -1237,10 +975,9 @@ async function writeWrapper(input) {
|
|
|
1237
975
|
"import { spawn } from 'node:child_process'",
|
|
1238
976
|
"",
|
|
1239
977
|
`const cliPath = ${JSON.stringify(input.cliPath)}`,
|
|
1240
|
-
`const identityPath = ${JSON.stringify(input.identityPath)}`,
|
|
1241
978
|
`const signerPath = ${JSON.stringify(input.signerPath)}`,
|
|
1242
979
|
"",
|
|
1243
|
-
"const child = spawn(process.execPath, [cliPath, '--
|
|
980
|
+
"const child = spawn(process.execPath, [cliPath, '--credentials', signerPath, ...process.argv.slice(2)], {",
|
|
1244
981
|
" stdio: 'inherit',",
|
|
1245
982
|
"})",
|
|
1246
983
|
"",
|
|
@@ -1253,57 +990,889 @@ async function writeWrapper(input) {
|
|
|
1253
990
|
await writeFile(input.wrapperPath, source, { mode: 448 });
|
|
1254
991
|
await chmod(input.wrapperPath, 448).catch(() => void 0);
|
|
1255
992
|
}
|
|
1256
|
-
async function
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
993
|
+
async function readRuntimeSidecar(credentialDirectory) {
|
|
994
|
+
try {
|
|
995
|
+
return JSON.parse(
|
|
996
|
+
await readFile(join(credentialDirectory, "signer-runtime.json"), "utf8")
|
|
997
|
+
);
|
|
998
|
+
} catch {
|
|
999
|
+
return null;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
async function writeRuntimeSidecar(input) {
|
|
1003
|
+
const value = {
|
|
1004
|
+
signer_package: MCP_RUNTIME_MANIFEST.signerPackage,
|
|
1005
|
+
signer_version: MCP_RUNTIME_MANIFEST.signerVersion,
|
|
1006
|
+
sdk_package: MCP_RUNTIME_MANIFEST.sdkPackage,
|
|
1007
|
+
sdk_version: MCP_RUNTIME_MANIFEST.sdkVersion,
|
|
1008
|
+
wrapper_path: input.wrapperPath,
|
|
1009
|
+
runtime_directory: input.runtimeDirectory,
|
|
1010
|
+
npm_cache_directory: input.npmCacheDirectory,
|
|
1011
|
+
cli_path: input.cliPath
|
|
1012
|
+
};
|
|
1013
|
+
await writeFile(input.path, `${JSON.stringify(value, null, 2)}
|
|
1014
|
+
`, { mode: 384 });
|
|
1015
|
+
await chmod(input.path, 384).catch(() => void 0);
|
|
1016
|
+
}
|
|
1017
|
+
async function assertFileExists(path, label) {
|
|
1018
|
+
try {
|
|
1019
|
+
await access(path);
|
|
1020
|
+
} catch {
|
|
1021
|
+
throw new Error(`Missing ${label}: ${path}`);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
var execFileAsync, SIGNER_INSTALL_TIMEOUT_MS, SIGNER_INSTALL_HEARTBEAT_MS;
|
|
1025
|
+
var init_signer_runtime = __esm({
|
|
1026
|
+
"src/signer-runtime.ts"() {
|
|
1027
|
+
init_runtime_manifest();
|
|
1028
|
+
execFileAsync = promisify(execFile);
|
|
1029
|
+
SIGNER_INSTALL_TIMEOUT_MS = 6e5;
|
|
1030
|
+
SIGNER_INSTALL_HEARTBEAT_MS = 15e3;
|
|
1031
|
+
}
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1034
|
+
// src/runtime-registry.ts
|
|
1035
|
+
function runtimeProfile(runtime, env = process.env) {
|
|
1036
|
+
return RUNTIME_PROFILES[normalizeRuntime(runtime, env)];
|
|
1037
|
+
}
|
|
1038
|
+
function normalizeRuntime(runtime, env = process.env) {
|
|
1039
|
+
const explicit = normalizeRuntimeName(runtime);
|
|
1040
|
+
if (explicit) return explicit;
|
|
1041
|
+
return detectRuntime(env) ?? "other";
|
|
1042
|
+
}
|
|
1043
|
+
function restartRequiredForRuntime(runtime, env = process.env) {
|
|
1044
|
+
const mode = runtimeProfile(runtime, env).restartMode;
|
|
1045
|
+
return mode === "restart-session" || mode === "restart-app";
|
|
1046
|
+
}
|
|
1047
|
+
function runtimeVerificationInstruction(runtime) {
|
|
1048
|
+
const label = RUNTIME_PROFILES[runtime].label;
|
|
1049
|
+
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.`;
|
|
1050
|
+
}
|
|
1051
|
+
function normalizeRuntimeName(runtime) {
|
|
1052
|
+
const key = runtime?.trim().toLowerCase();
|
|
1053
|
+
if (!key) return null;
|
|
1054
|
+
return RUNTIME_ALIASES[key.replace(/\s+/g, "-")] ?? null;
|
|
1055
|
+
}
|
|
1056
|
+
function detectRuntime(env) {
|
|
1057
|
+
if (env.CLAUDECODE || env.CLAUDE_CODE || env.CLAUDECODE_CWD) return "claude-code";
|
|
1058
|
+
if (env.CODEX_SANDBOX || env.CODEX_HOME || env.CODEX_CWD) return "codex-cli";
|
|
1059
|
+
if (env.VSCODE_CWD || env.VSCODE_IPC_HOOK_CLI || env.TERM_PROGRAM === "vscode") return "vscode";
|
|
1060
|
+
if (env.HERMES_HOME || env.HERMES_AGENT) return "hermes";
|
|
1061
|
+
return null;
|
|
1062
|
+
}
|
|
1063
|
+
var RUNTIME_PROFILES, RUNTIME_ALIASES;
|
|
1064
|
+
var init_runtime_registry = __esm({
|
|
1065
|
+
"src/runtime-registry.ts"() {
|
|
1066
|
+
RUNTIME_PROFILES = {
|
|
1067
|
+
"claude-code": {
|
|
1068
|
+
id: "claude-code",
|
|
1069
|
+
label: "Claude Code",
|
|
1070
|
+
restartMode: "restart-session",
|
|
1071
|
+
canWriteRuntimeConfig: true,
|
|
1072
|
+
activationInstruction: "Start a new Claude Code session so it loads the Haven MCP entries."
|
|
1073
|
+
},
|
|
1074
|
+
"codex-cli": {
|
|
1075
|
+
id: "codex-cli",
|
|
1076
|
+
label: "Codex CLI",
|
|
1077
|
+
restartMode: "restart-session",
|
|
1078
|
+
canWriteRuntimeConfig: true,
|
|
1079
|
+
activationInstruction: "Start a fresh Codex CLI session (for example, run `codex resume --last`)."
|
|
1080
|
+
},
|
|
1081
|
+
"codex-desktop": {
|
|
1082
|
+
id: "codex-desktop",
|
|
1083
|
+
label: "Codex Desktop",
|
|
1084
|
+
restartMode: "restart-session",
|
|
1085
|
+
canWriteRuntimeConfig: true,
|
|
1086
|
+
activationInstruction: "Quit and reopen Codex Desktop so it loads the Haven MCP entries."
|
|
1087
|
+
},
|
|
1088
|
+
cursor: {
|
|
1089
|
+
id: "cursor",
|
|
1090
|
+
label: "Cursor",
|
|
1091
|
+
restartMode: "hot-reload",
|
|
1092
|
+
canWriteRuntimeConfig: true,
|
|
1093
|
+
activationInstruction: "Wait for Cursor to hot-reload the Haven MCP entries; no app restart is required."
|
|
1094
|
+
},
|
|
1095
|
+
vscode: {
|
|
1096
|
+
id: "vscode",
|
|
1097
|
+
label: "VS Code",
|
|
1098
|
+
restartMode: "hot-reload",
|
|
1099
|
+
canWriteRuntimeConfig: true,
|
|
1100
|
+
activationInstruction: "Wait for VS Code to hot-reload the Haven MCP entries; no app restart is required."
|
|
1101
|
+
},
|
|
1102
|
+
"vscode-insiders": {
|
|
1103
|
+
id: "vscode-insiders",
|
|
1104
|
+
label: "VS Code Insiders",
|
|
1105
|
+
restartMode: "hot-reload",
|
|
1106
|
+
canWriteRuntimeConfig: true,
|
|
1107
|
+
activationInstruction: "Wait for VS Code Insiders to hot-reload the Haven MCP entries; no app restart is required."
|
|
1108
|
+
},
|
|
1109
|
+
"claude-desktop": {
|
|
1110
|
+
id: "claude-desktop",
|
|
1111
|
+
label: "Claude Desktop",
|
|
1112
|
+
restartMode: "restart-app",
|
|
1113
|
+
canWriteRuntimeConfig: true,
|
|
1114
|
+
activationInstruction: "Quit and reopen Claude Desktop so it loads the Haven MCP entries."
|
|
1115
|
+
},
|
|
1116
|
+
hermes: {
|
|
1117
|
+
id: "hermes",
|
|
1118
|
+
label: "Hermes Agent",
|
|
1119
|
+
restartMode: "restart-session",
|
|
1120
|
+
canWriteRuntimeConfig: true,
|
|
1121
|
+
activationInstruction: "Start a new Hermes session; in Hermes Gateway, run `/restart` instead."
|
|
1122
|
+
},
|
|
1123
|
+
other: {
|
|
1124
|
+
id: "other",
|
|
1125
|
+
label: "Other agent runtime",
|
|
1126
|
+
restartMode: "manual",
|
|
1127
|
+
canWriteRuntimeConfig: false,
|
|
1128
|
+
activationInstruction: "Finish the manual MCP setup shown above, then start a fresh session in that runtime."
|
|
1129
|
+
}
|
|
1130
|
+
};
|
|
1131
|
+
RUNTIME_ALIASES = {
|
|
1132
|
+
claude: "claude-code",
|
|
1133
|
+
"claude-code": "claude-code",
|
|
1134
|
+
claudecode: "claude-code",
|
|
1135
|
+
"claude_code": "claude-code",
|
|
1136
|
+
codex: "codex-cli",
|
|
1137
|
+
"codex-cli": "codex-cli",
|
|
1138
|
+
codexcli: "codex-cli",
|
|
1139
|
+
"codex_cli": "codex-cli",
|
|
1140
|
+
"codex-desktop": "codex-desktop",
|
|
1141
|
+
"codex_desktop": "codex-desktop",
|
|
1142
|
+
codexdesktop: "codex-desktop",
|
|
1143
|
+
"codex-app": "codex-desktop",
|
|
1144
|
+
"codex_app": "codex-desktop",
|
|
1145
|
+
codexapp: "codex-desktop",
|
|
1146
|
+
cursor: "cursor",
|
|
1147
|
+
vscode: "vscode",
|
|
1148
|
+
"vs-code": "vscode",
|
|
1149
|
+
"vs_code": "vscode",
|
|
1150
|
+
code: "vscode",
|
|
1151
|
+
"vscode-insiders": "vscode-insiders",
|
|
1152
|
+
"vscode_insiders": "vscode-insiders",
|
|
1153
|
+
vscodeinsiders: "vscode-insiders",
|
|
1154
|
+
"vs-code-insiders": "vscode-insiders",
|
|
1155
|
+
"code-insiders": "vscode-insiders",
|
|
1156
|
+
insiders: "vscode-insiders",
|
|
1157
|
+
"claude-desktop": "claude-desktop",
|
|
1158
|
+
"claude_desktop": "claude-desktop",
|
|
1159
|
+
claudesktop: "claude-desktop",
|
|
1160
|
+
desktop: "claude-desktop",
|
|
1161
|
+
hermes: "hermes",
|
|
1162
|
+
"hermes-agent": "hermes",
|
|
1163
|
+
hermes_agent: "hermes",
|
|
1164
|
+
hermesagent: "hermes",
|
|
1165
|
+
other: "other",
|
|
1166
|
+
manual: "other"
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
});
|
|
1170
|
+
async function acknowledgeLocalSignerConsent(signerPath, log) {
|
|
1171
|
+
try {
|
|
1172
|
+
const input = await buildSignerConsentInput(signerPath);
|
|
1173
|
+
const decision = await ensureSignerConsent(input, {
|
|
1174
|
+
credentialsPath: signerPath,
|
|
1175
|
+
writeAck: true,
|
|
1176
|
+
out: log ? { write: (chunk) => writeLogChunk2(log, chunk) } : void 0
|
|
1177
|
+
});
|
|
1178
|
+
return {
|
|
1179
|
+
acknowledged: decision.ok,
|
|
1180
|
+
hash: decision.hash,
|
|
1181
|
+
reason: decision.reason
|
|
1182
|
+
};
|
|
1183
|
+
} catch (err) {
|
|
1184
|
+
return {
|
|
1185
|
+
acknowledged: false,
|
|
1186
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
async function getLocalSignerConsentStatus(signerPath) {
|
|
1191
|
+
try {
|
|
1192
|
+
const input = await buildSignerConsentInput(signerPath);
|
|
1193
|
+
const hash = computeSignerConsentHash(input);
|
|
1194
|
+
const stored = await readSignerAckFile(signerAckPath(signerPath));
|
|
1195
|
+
if (stored === hash) {
|
|
1196
|
+
return { acknowledged: true, hash, reason: "ack_file_match" };
|
|
1197
|
+
}
|
|
1198
|
+
return {
|
|
1199
|
+
acknowledged: false,
|
|
1200
|
+
hash,
|
|
1201
|
+
reason: stored ? "ack_file_mismatch" : "ack_file_missing"
|
|
1202
|
+
};
|
|
1203
|
+
} catch (err) {
|
|
1204
|
+
return {
|
|
1205
|
+
acknowledged: false,
|
|
1206
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
function signerAckPath(signerPath) {
|
|
1211
|
+
return resolve(`${signerPath}.signer-ack.json`);
|
|
1212
|
+
}
|
|
1213
|
+
async function buildSignerConsentInput(signerPath) {
|
|
1214
|
+
const credentials = await loadSignerCredentials(signerPath);
|
|
1215
|
+
const signer = createEdgeSigner(credentials.delegateKey, {
|
|
1216
|
+
x402BindingSigner: credentials.x402BindingSigner
|
|
1217
|
+
});
|
|
1218
|
+
return {
|
|
1219
|
+
delegateAddress: signer.delegateAddress,
|
|
1220
|
+
safeAddress: credentials.safeAddress,
|
|
1221
|
+
agentId: credentials.agentId,
|
|
1222
|
+
chainId: credentials.chainId,
|
|
1223
|
+
network: credentials.network,
|
|
1224
|
+
toolNames: Object.keys(toolSchemas)
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
async function readSignerAckFile(path) {
|
|
1228
|
+
try {
|
|
1229
|
+
const parsed = JSON.parse(await readFile(path, "utf8"));
|
|
1230
|
+
return typeof parsed.ack === "string" ? parsed.ack : null;
|
|
1231
|
+
} catch {
|
|
1232
|
+
return null;
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
function writeLogChunk2(log, chunk) {
|
|
1236
|
+
const message = String(chunk).trimEnd();
|
|
1237
|
+
if (message) log(message);
|
|
1238
|
+
}
|
|
1239
|
+
var init_signer_consent = __esm({
|
|
1240
|
+
"src/signer-consent.ts"() {
|
|
1241
|
+
}
|
|
1242
|
+
});
|
|
1243
|
+
|
|
1244
|
+
// src/doctor.ts
|
|
1245
|
+
var doctor_exports = {};
|
|
1246
|
+
__export(doctor_exports, {
|
|
1247
|
+
runDoctor: () => runDoctor,
|
|
1248
|
+
runRepair: () => runRepair
|
|
1249
|
+
});
|
|
1250
|
+
async function discoverCredentialDirectory(homeDir, explicit) {
|
|
1251
|
+
if (explicit) return { directory: explicit };
|
|
1252
|
+
const root = join(homeDir, ".haven", "agents");
|
|
1253
|
+
let entries = [];
|
|
1254
|
+
try {
|
|
1255
|
+
entries = await readdir(root);
|
|
1256
|
+
} catch {
|
|
1257
|
+
return {};
|
|
1258
|
+
}
|
|
1259
|
+
const candidates = [];
|
|
1260
|
+
for (const entry of entries) {
|
|
1261
|
+
const directory = join(root, entry);
|
|
1262
|
+
try {
|
|
1263
|
+
const s = await stat(join(directory, "identity.json"));
|
|
1264
|
+
candidates.push({ directory, mtimeMs: s.mtimeMs });
|
|
1265
|
+
} catch {
|
|
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
|
+
});
|
|
1420
|
+
}
|
|
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
|
+
}
|
|
1430
|
+
const restart = restartRequiredForRuntime(input.runtime, deps.env);
|
|
1431
|
+
checks.push({
|
|
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) {
|
|
1452
|
+
return {
|
|
1453
|
+
ok: false,
|
|
1454
|
+
messages: [`No agent credentials found to repair \u2014 run the full setup: ${RERUN} --setup <token>.`]
|
|
1455
|
+
};
|
|
1456
|
+
}
|
|
1457
|
+
let identity;
|
|
1458
|
+
try {
|
|
1459
|
+
identity = JSON.parse(await readFile(join(directory, "identity.json"), "utf8"));
|
|
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) {
|
|
1468
|
+
try {
|
|
1469
|
+
const existing = await readFile(configPath, "utf8");
|
|
1470
|
+
if (existing.includes("bin/haven-mcp") || existing.includes(".haven/mcp-runtime")) {
|
|
1471
|
+
return {
|
|
1472
|
+
ok: false,
|
|
1473
|
+
messages: [
|
|
1474
|
+
`The config at ${configPath} is the LOCAL-stdio topology (--local). Repair currently rewrites only the hosted+signer shape and will not touch it.`,
|
|
1475
|
+
"Re-run your original setup command (with --local) to repair a local-stdio install."
|
|
1476
|
+
]
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
} catch {
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
const signerPath = join(directory, "signer.json");
|
|
1483
|
+
const prepared = await prepareSignerRuntime(
|
|
1484
|
+
{ credentialDirectory: directory, signerPath, homeDir },
|
|
1485
|
+
{ runCommand: deps.runCommand }
|
|
1486
|
+
);
|
|
1487
|
+
messages.push(...prepared.messages);
|
|
1488
|
+
const configResult = await writeRuntimeConfig({
|
|
1489
|
+
runtime: input.runtime,
|
|
1490
|
+
hostedMcpUrl: identity.hosted_mcp_url ?? `${identity.api_url}/mcp`,
|
|
1491
|
+
apiKey: identity.api_key,
|
|
1492
|
+
identityPath: join(directory, "identity.json"),
|
|
1493
|
+
signerPath,
|
|
1494
|
+
credentialDirectory: directory,
|
|
1495
|
+
signerCommand: { command: prepared.command, args: prepared.args },
|
|
1496
|
+
homeDir,
|
|
1497
|
+
mode: "hosted"
|
|
1498
|
+
});
|
|
1499
|
+
messages.push(...configResult.messages);
|
|
1500
|
+
messages.push("Repair complete \u2014 restart the runtime, then verify with --doctor.");
|
|
1501
|
+
return { ok: true, messages };
|
|
1502
|
+
}
|
|
1503
|
+
var RERUN;
|
|
1504
|
+
var init_doctor = __esm({
|
|
1505
|
+
"src/doctor.ts"() {
|
|
1506
|
+
init_runtime_manifest();
|
|
1507
|
+
init_probes();
|
|
1508
|
+
init_signer_runtime();
|
|
1509
|
+
init_config_writers();
|
|
1510
|
+
init_runtime_registry();
|
|
1511
|
+
init_signer_consent();
|
|
1512
|
+
RERUN = "npx @haven_ai/connect@alpha";
|
|
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
|
+
})
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
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
|
+
}
|
|
1687
|
+
try {
|
|
1688
|
+
await writeOwnerOnlyJson(
|
|
1689
|
+
agentPath,
|
|
1690
|
+
{
|
|
1691
|
+
agent_id: input.agentId,
|
|
1692
|
+
delegate_address: input.delegateAddress,
|
|
1693
|
+
safe_address: input.safeAddress,
|
|
1694
|
+
chain_id: input.chainId,
|
|
1695
|
+
network: input.network,
|
|
1696
|
+
agent_budget: input.agentBudget,
|
|
1697
|
+
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."
|
|
1698
|
+
},
|
|
1699
|
+
input.warn
|
|
1700
|
+
);
|
|
1701
|
+
} catch (err) {
|
|
1702
|
+
await rm(signerPath, { force: true }).catch(() => void 0);
|
|
1703
|
+
await rm(identityPath, { force: true }).catch(() => void 0);
|
|
1704
|
+
await rm(agentPath, { force: true }).catch(() => void 0);
|
|
1705
|
+
throw err;
|
|
1706
|
+
}
|
|
1707
|
+
return { directory, identityPath, signerPath, agentPath };
|
|
1708
|
+
}
|
|
1709
|
+
function defaultAgentDirectory(agentId, baseDir = join(homedir(), ".haven", "agents")) {
|
|
1710
|
+
return resolve(defaultCredentialRoot(baseDir), safePathPart(agentId));
|
|
1711
|
+
}
|
|
1712
|
+
function defaultCredentialRoot(baseDir = join(homedir(), ".haven", "agents")) {
|
|
1713
|
+
return resolve(baseDir);
|
|
1714
|
+
}
|
|
1715
|
+
async function writeOwnerOnlyJson(path, value, warn) {
|
|
1716
|
+
const json = JSON.stringify(dropUndefined(value), null, 2);
|
|
1717
|
+
await writeFile(path, `${json}
|
|
1718
|
+
`, { mode: 384, flag: "wx" });
|
|
1719
|
+
await restrictPermissions(path, 384, warn);
|
|
1720
|
+
}
|
|
1721
|
+
function safePathPart(value) {
|
|
1722
|
+
return value.replace(/[^A-Za-z0-9_.-]/g, "_");
|
|
1723
|
+
}
|
|
1724
|
+
function dropUndefined(value) {
|
|
1725
|
+
return Object.fromEntries(Object.entries(value).filter(([, field]) => field !== void 0));
|
|
1726
|
+
}
|
|
1727
|
+
async function assertDoesNotExist(path) {
|
|
1728
|
+
try {
|
|
1729
|
+
await access(path);
|
|
1730
|
+
} catch (err) {
|
|
1731
|
+
if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") return;
|
|
1732
|
+
throw err;
|
|
1733
|
+
}
|
|
1734
|
+
throw new Error(`Refusing to overwrite existing Haven credential file: ${path}`);
|
|
1735
|
+
}
|
|
1736
|
+
async function restrictPermissions(path, mode, warn) {
|
|
1737
|
+
try {
|
|
1738
|
+
await chmod(path, mode);
|
|
1739
|
+
} catch (err) {
|
|
1740
|
+
warn?.(
|
|
1741
|
+
`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)}`
|
|
1742
|
+
);
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
// src/runtime-install.ts
|
|
1747
|
+
init_config_writers();
|
|
1748
|
+
async function acknowledgeLocalMcpConsent(identityPath, signerPath, log) {
|
|
1749
|
+
try {
|
|
1750
|
+
const input = await buildLocalMcpConsentInput(identityPath, signerPath);
|
|
1751
|
+
const decision = await ensureConsent(input, {
|
|
1752
|
+
credentialsPath: identityPath,
|
|
1753
|
+
writeAck: true,
|
|
1754
|
+
out: log ? { write: (chunk) => writeLogChunk(log, chunk) } : void 0
|
|
1755
|
+
});
|
|
1756
|
+
return {
|
|
1757
|
+
acknowledged: decision.ok,
|
|
1758
|
+
hash: decision.hash,
|
|
1759
|
+
reason: decision.reason
|
|
1760
|
+
};
|
|
1761
|
+
} catch (err) {
|
|
1762
|
+
return {
|
|
1763
|
+
acknowledged: false,
|
|
1764
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1765
|
+
};
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
async function getLocalMcpConsentStatus(identityPath, signerPath) {
|
|
1769
|
+
try {
|
|
1770
|
+
const input = await buildLocalMcpConsentInput(identityPath, signerPath);
|
|
1771
|
+
const hash = computeConsentHash(input);
|
|
1772
|
+
const stored = await readLocalMcpAckFile(localMcpAckPath(identityPath));
|
|
1773
|
+
if (stored === hash) {
|
|
1774
|
+
return { acknowledged: true, hash, reason: "ack_file_match" };
|
|
1775
|
+
}
|
|
1776
|
+
return {
|
|
1777
|
+
acknowledged: false,
|
|
1778
|
+
hash,
|
|
1779
|
+
reason: stored ? "ack_file_mismatch" : "ack_file_missing"
|
|
1780
|
+
};
|
|
1781
|
+
} catch (err) {
|
|
1782
|
+
return {
|
|
1783
|
+
acknowledged: false,
|
|
1784
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1785
|
+
};
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
function localMcpAckPath(identityPath) {
|
|
1789
|
+
return resolve(`${identityPath}.ack.json`);
|
|
1790
|
+
}
|
|
1791
|
+
async function buildLocalMcpConsentInput(identityPath, signerPath) {
|
|
1792
|
+
const credentials = await loadCredentials({ identityPath, signerPath });
|
|
1793
|
+
const unavailableDuringSetup = {
|
|
1794
|
+
getAllowances: async () => {
|
|
1795
|
+
throw new Error("Haven approval is not complete yet.");
|
|
1796
|
+
}
|
|
1267
1797
|
};
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1798
|
+
return consentInputFromClient(
|
|
1799
|
+
unavailableDuringSetup,
|
|
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
|
+
);
|
|
1271
1811
|
}
|
|
1272
|
-
async function
|
|
1812
|
+
async function readLocalMcpAckFile(path) {
|
|
1273
1813
|
try {
|
|
1274
|
-
await
|
|
1814
|
+
const parsed = JSON.parse(await readFile(path, "utf8"));
|
|
1815
|
+
return typeof parsed.ack === "string" ? parsed.ack : null;
|
|
1275
1816
|
} catch {
|
|
1276
|
-
|
|
1817
|
+
return null;
|
|
1277
1818
|
}
|
|
1278
1819
|
}
|
|
1820
|
+
function writeLogChunk(log, chunk) {
|
|
1821
|
+
const message = String(chunk).trimEnd();
|
|
1822
|
+
if (message) log(message);
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
// src/runtime-install.ts
|
|
1826
|
+
init_probes();
|
|
1827
|
+
|
|
1828
|
+
// src/local-mcp-runtime.ts
|
|
1829
|
+
init_signer_runtime();
|
|
1830
|
+
init_runtime_manifest();
|
|
1279
1831
|
var execFileAsync2 = promisify(execFile);
|
|
1280
|
-
|
|
1832
|
+
var UnsupportedNodeVersionError = class extends Error {
|
|
1833
|
+
code = "local_mcp_unsupported_node_version";
|
|
1834
|
+
nodeVersion;
|
|
1835
|
+
minimumNodeVersion;
|
|
1836
|
+
constructor(nodeVersion, minimumNodeVersion, subject = "Haven setup") {
|
|
1837
|
+
super(unsupportedNodeVersionMessage({ subject, nodeVersion, minimumNodeVersion }));
|
|
1838
|
+
this.name = "UnsupportedNodeVersionError";
|
|
1839
|
+
this.nodeVersion = nodeVersion;
|
|
1840
|
+
this.minimumNodeVersion = minimumNodeVersion;
|
|
1841
|
+
}
|
|
1842
|
+
};
|
|
1843
|
+
async function prepareLocalMcpRuntime(input, deps = {}) {
|
|
1844
|
+
assertSupportedNodeVersion(input.nodeVersion);
|
|
1281
1845
|
const homeDir = input.homeDir ?? homedir();
|
|
1282
|
-
const runtimeDirectory = resolve(homeDir, ".haven", "
|
|
1846
|
+
const runtimeDirectory = resolve(homeDir, ".haven", "mcp-runtime", MCP_RUNTIME_MANIFEST.mcpVersion);
|
|
1283
1847
|
const npmCacheDirectory = resolve(homeDir, ".haven", "npm-cache");
|
|
1284
|
-
const cliPath = join(runtimeDirectory, "node_modules", "@haven_ai", "
|
|
1848
|
+
const cliPath = join(runtimeDirectory, "node_modules", "@haven_ai", "mcp", "dist", "cli.js");
|
|
1285
1849
|
const messages = [];
|
|
1286
1850
|
await mkdir(runtimeDirectory, { recursive: true, mode: 448 });
|
|
1287
1851
|
await chmod(runtimeDirectory, 448).catch(() => void 0);
|
|
1288
1852
|
await mkdir(npmCacheDirectory, { recursive: true, mode: 448 });
|
|
1289
1853
|
await chmod(npmCacheDirectory, 448).catch(() => void 0);
|
|
1290
1854
|
if (await installedRuntimeMatches2(runtimeDirectory, cliPath)) {
|
|
1291
|
-
messages.push(`Using existing local Haven
|
|
1855
|
+
messages.push(`Using existing local Haven MCP runtime ${mcpPackageSpec()}.`);
|
|
1292
1856
|
} else {
|
|
1293
|
-
await installRuntimePackages2(runtimeDirectory, npmCacheDirectory, deps
|
|
1294
|
-
messages.push(`Installed local Haven
|
|
1857
|
+
await installRuntimePackages2(runtimeDirectory, npmCacheDirectory, deps);
|
|
1858
|
+
messages.push(`Installed local Haven MCP runtime ${mcpPackageSpec()}.`);
|
|
1295
1859
|
}
|
|
1296
|
-
await assertFileExists2(cliPath, "local Haven
|
|
1297
|
-
const wrapperPath = join(input.credentialDirectory, "bin", "haven-
|
|
1298
|
-
await writeWrapper2({
|
|
1860
|
+
await assertFileExists2(cliPath, "local Haven MCP CLI");
|
|
1861
|
+
const wrapperPath = join(input.credentialDirectory, "bin", "haven-mcp");
|
|
1862
|
+
await writeWrapper2({
|
|
1863
|
+
wrapperPath,
|
|
1864
|
+
cliPath,
|
|
1865
|
+
identityPath: input.identityPath,
|
|
1866
|
+
signerPath: input.signerPath
|
|
1867
|
+
});
|
|
1299
1868
|
await writeRuntimeSidecar2({
|
|
1300
|
-
path: join(input.credentialDirectory, "
|
|
1869
|
+
path: join(input.credentialDirectory, "mcp-runtime.json"),
|
|
1301
1870
|
wrapperPath,
|
|
1302
1871
|
runtimeDirectory,
|
|
1303
1872
|
npmCacheDirectory,
|
|
1304
1873
|
cliPath
|
|
1305
1874
|
});
|
|
1306
|
-
messages.push(`Prepared stable local Haven
|
|
1875
|
+
messages.push(`Prepared stable local Haven MCP wrapper: ${wrapperPath}`);
|
|
1307
1876
|
return {
|
|
1308
1877
|
command: wrapperPath,
|
|
1309
1878
|
args: [],
|
|
@@ -1314,7 +1883,13 @@ async function prepareSignerRuntime(input, deps = {}) {
|
|
|
1314
1883
|
messages
|
|
1315
1884
|
};
|
|
1316
1885
|
}
|
|
1317
|
-
|
|
1886
|
+
function assertSupportedNodeVersion(nodeVersion = process.versions.node, minimumNodeVersion = MCP_RUNTIME_MANIFEST.minimumNodeVersion, subject = "Haven setup") {
|
|
1887
|
+
if (!isSupportedNodeVersion(nodeVersion, minimumNodeVersion)) {
|
|
1888
|
+
throw new UnsupportedNodeVersionError(nodeVersion, minimumNodeVersion, subject);
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
async function installRuntimePackages2(runtimeDirectory, npmCacheDirectory, deps) {
|
|
1892
|
+
const { runCommand, onProgress } = deps;
|
|
1318
1893
|
const baseArgs = [
|
|
1319
1894
|
"install",
|
|
1320
1895
|
"--prefix",
|
|
@@ -1323,12 +1898,22 @@ async function installRuntimePackages2(runtimeDirectory, npmCacheDirectory, runC
|
|
|
1323
1898
|
"--no-fund",
|
|
1324
1899
|
"--omit=dev",
|
|
1325
1900
|
"--prefer-offline",
|
|
1326
|
-
|
|
1901
|
+
mcpPackageSpec(),
|
|
1327
1902
|
sdkPackageSpec()
|
|
1328
1903
|
];
|
|
1329
1904
|
const run = async (args) => {
|
|
1330
|
-
|
|
1331
|
-
|
|
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
|
+
}
|
|
1332
1917
|
};
|
|
1333
1918
|
try {
|
|
1334
1919
|
await run(baseArgs);
|
|
@@ -1336,20 +1921,18 @@ async function installRuntimePackages2(runtimeDirectory, npmCacheDirectory, runC
|
|
|
1336
1921
|
try {
|
|
1337
1922
|
await run([...baseArgs, "--cache", npmCacheDirectory]);
|
|
1338
1923
|
} catch (err) {
|
|
1339
|
-
throw new Error(
|
|
1340
|
-
`Could not install local Haven signer runtime ${signerPackageSpec()}: ${err instanceof Error ? err.message : String(err)}`
|
|
1341
|
-
);
|
|
1924
|
+
throw new Error(`Could not install local Haven MCP runtime ${mcpPackageSpec()}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1342
1925
|
}
|
|
1343
1926
|
}
|
|
1344
1927
|
}
|
|
1345
1928
|
async function installedRuntimeMatches2(runtimeDirectory, cliPath) {
|
|
1346
1929
|
try {
|
|
1347
|
-
await assertFileExists2(cliPath, "local Haven
|
|
1348
|
-
const [
|
|
1349
|
-
readPackageJson2(join(runtimeDirectory, "node_modules", "@haven_ai", "
|
|
1930
|
+
await assertFileExists2(cliPath, "local Haven MCP CLI");
|
|
1931
|
+
const [mcpPackage, sdkPackage] = await Promise.all([
|
|
1932
|
+
readPackageJson2(join(runtimeDirectory, "node_modules", "@haven_ai", "mcp", "package.json")),
|
|
1350
1933
|
readPackageJson2(join(runtimeDirectory, "node_modules", "@haven_ai", "sdk", "package.json"))
|
|
1351
1934
|
]);
|
|
1352
|
-
return
|
|
1935
|
+
return mcpPackage.version === MCP_RUNTIME_MANIFEST.mcpVersion && sdkPackage.version === MCP_RUNTIME_MANIFEST.sdkVersion;
|
|
1353
1936
|
} catch {
|
|
1354
1937
|
return false;
|
|
1355
1938
|
}
|
|
@@ -1365,9 +1948,10 @@ async function writeWrapper2(input) {
|
|
|
1365
1948
|
"import { spawn } from 'node:child_process'",
|
|
1366
1949
|
"",
|
|
1367
1950
|
`const cliPath = ${JSON.stringify(input.cliPath)}`,
|
|
1951
|
+
`const identityPath = ${JSON.stringify(input.identityPath)}`,
|
|
1368
1952
|
`const signerPath = ${JSON.stringify(input.signerPath)}`,
|
|
1369
1953
|
"",
|
|
1370
|
-
"const child = spawn(process.execPath, [cliPath, '--
|
|
1954
|
+
"const child = spawn(process.execPath, [cliPath, '--identity', identityPath, '--signer', signerPath, ...process.argv.slice(2)], {",
|
|
1371
1955
|
" stdio: 'inherit',",
|
|
1372
1956
|
"})",
|
|
1373
1957
|
"",
|
|
@@ -1382,10 +1966,11 @@ async function writeWrapper2(input) {
|
|
|
1382
1966
|
}
|
|
1383
1967
|
async function writeRuntimeSidecar2(input) {
|
|
1384
1968
|
const value = {
|
|
1385
|
-
|
|
1386
|
-
|
|
1969
|
+
mcp_package: MCP_RUNTIME_MANIFEST.mcpPackage,
|
|
1970
|
+
mcp_version: MCP_RUNTIME_MANIFEST.mcpVersion,
|
|
1387
1971
|
sdk_package: MCP_RUNTIME_MANIFEST.sdkPackage,
|
|
1388
1972
|
sdk_version: MCP_RUNTIME_MANIFEST.sdkVersion,
|
|
1973
|
+
minimum_node_version: MCP_RUNTIME_MANIFEST.minimumNodeVersion,
|
|
1389
1974
|
wrapper_path: input.wrapperPath,
|
|
1390
1975
|
runtime_directory: input.runtimeDirectory,
|
|
1391
1976
|
npm_cache_directory: input.npmCacheDirectory,
|
|
@@ -1402,6 +1987,10 @@ async function assertFileExists2(path, label) {
|
|
|
1402
1987
|
throw new Error(`Missing ${label}: ${path}`);
|
|
1403
1988
|
}
|
|
1404
1989
|
}
|
|
1990
|
+
|
|
1991
|
+
// src/runtime-install.ts
|
|
1992
|
+
init_signer_runtime();
|
|
1993
|
+
init_runtime_manifest();
|
|
1405
1994
|
var CODEX_AGENTS_BEGIN_MARKER = "<!-- BEGIN haven-pay (managed by @haven_ai/connect; edits inside this section are overwritten on re-setup) -->";
|
|
1406
1995
|
var CODEX_AGENTS_END_MARKER = "<!-- END haven-pay -->";
|
|
1407
1996
|
async function installSkillForRuntime(runtime, deps = {}) {
|
|
@@ -1506,208 +2095,9 @@ function hermesHome(deps) {
|
|
|
1506
2095
|
return env.HERMES_HOME ?? join(deps.homeDir ?? homedir(), ".hermes");
|
|
1507
2096
|
}
|
|
1508
2097
|
|
|
1509
|
-
// src/runtime-registry.ts
|
|
1510
|
-
var RUNTIME_PROFILES = {
|
|
1511
|
-
"claude-code": {
|
|
1512
|
-
id: "claude-code",
|
|
1513
|
-
label: "Claude Code",
|
|
1514
|
-
restartMode: "restart-session",
|
|
1515
|
-
canWriteRuntimeConfig: true,
|
|
1516
|
-
activationInstruction: "Start a new Claude Code session so it loads the Haven MCP entries."
|
|
1517
|
-
},
|
|
1518
|
-
"codex-cli": {
|
|
1519
|
-
id: "codex-cli",
|
|
1520
|
-
label: "Codex CLI",
|
|
1521
|
-
restartMode: "restart-session",
|
|
1522
|
-
canWriteRuntimeConfig: true,
|
|
1523
|
-
activationInstruction: "Start a fresh Codex CLI session (for example, run `codex resume --last`)."
|
|
1524
|
-
},
|
|
1525
|
-
"codex-desktop": {
|
|
1526
|
-
id: "codex-desktop",
|
|
1527
|
-
label: "Codex Desktop",
|
|
1528
|
-
restartMode: "restart-session",
|
|
1529
|
-
canWriteRuntimeConfig: true,
|
|
1530
|
-
activationInstruction: "Quit and reopen Codex Desktop so it loads the Haven MCP entries."
|
|
1531
|
-
},
|
|
1532
|
-
cursor: {
|
|
1533
|
-
id: "cursor",
|
|
1534
|
-
label: "Cursor",
|
|
1535
|
-
restartMode: "hot-reload",
|
|
1536
|
-
canWriteRuntimeConfig: true,
|
|
1537
|
-
activationInstruction: "Wait for Cursor to hot-reload the Haven MCP entries; no app restart is required."
|
|
1538
|
-
},
|
|
1539
|
-
vscode: {
|
|
1540
|
-
id: "vscode",
|
|
1541
|
-
label: "VS Code",
|
|
1542
|
-
restartMode: "hot-reload",
|
|
1543
|
-
canWriteRuntimeConfig: true,
|
|
1544
|
-
activationInstruction: "Wait for VS Code to hot-reload the Haven MCP entries; no app restart is required."
|
|
1545
|
-
},
|
|
1546
|
-
"vscode-insiders": {
|
|
1547
|
-
id: "vscode-insiders",
|
|
1548
|
-
label: "VS Code Insiders",
|
|
1549
|
-
restartMode: "hot-reload",
|
|
1550
|
-
canWriteRuntimeConfig: true,
|
|
1551
|
-
activationInstruction: "Wait for VS Code Insiders to hot-reload the Haven MCP entries; no app restart is required."
|
|
1552
|
-
},
|
|
1553
|
-
"claude-desktop": {
|
|
1554
|
-
id: "claude-desktop",
|
|
1555
|
-
label: "Claude Desktop",
|
|
1556
|
-
restartMode: "restart-app",
|
|
1557
|
-
canWriteRuntimeConfig: true,
|
|
1558
|
-
activationInstruction: "Quit and reopen Claude Desktop so it loads the Haven MCP entries."
|
|
1559
|
-
},
|
|
1560
|
-
hermes: {
|
|
1561
|
-
id: "hermes",
|
|
1562
|
-
label: "Hermes Agent",
|
|
1563
|
-
restartMode: "restart-session",
|
|
1564
|
-
canWriteRuntimeConfig: true,
|
|
1565
|
-
activationInstruction: "Start a new Hermes session; in Hermes Gateway, run `/restart` instead."
|
|
1566
|
-
},
|
|
1567
|
-
other: {
|
|
1568
|
-
id: "other",
|
|
1569
|
-
label: "Other agent runtime",
|
|
1570
|
-
restartMode: "manual",
|
|
1571
|
-
canWriteRuntimeConfig: false,
|
|
1572
|
-
activationInstruction: "Finish the manual MCP setup shown above, then start a fresh session in that runtime."
|
|
1573
|
-
}
|
|
1574
|
-
};
|
|
1575
|
-
var RUNTIME_ALIASES = {
|
|
1576
|
-
claude: "claude-code",
|
|
1577
|
-
"claude-code": "claude-code",
|
|
1578
|
-
claudecode: "claude-code",
|
|
1579
|
-
"claude_code": "claude-code",
|
|
1580
|
-
codex: "codex-cli",
|
|
1581
|
-
"codex-cli": "codex-cli",
|
|
1582
|
-
codexcli: "codex-cli",
|
|
1583
|
-
"codex_cli": "codex-cli",
|
|
1584
|
-
"codex-desktop": "codex-desktop",
|
|
1585
|
-
"codex_desktop": "codex-desktop",
|
|
1586
|
-
codexdesktop: "codex-desktop",
|
|
1587
|
-
"codex-app": "codex-desktop",
|
|
1588
|
-
"codex_app": "codex-desktop",
|
|
1589
|
-
codexapp: "codex-desktop",
|
|
1590
|
-
cursor: "cursor",
|
|
1591
|
-
vscode: "vscode",
|
|
1592
|
-
"vs-code": "vscode",
|
|
1593
|
-
"vs_code": "vscode",
|
|
1594
|
-
code: "vscode",
|
|
1595
|
-
"vscode-insiders": "vscode-insiders",
|
|
1596
|
-
"vscode_insiders": "vscode-insiders",
|
|
1597
|
-
vscodeinsiders: "vscode-insiders",
|
|
1598
|
-
"vs-code-insiders": "vscode-insiders",
|
|
1599
|
-
"code-insiders": "vscode-insiders",
|
|
1600
|
-
insiders: "vscode-insiders",
|
|
1601
|
-
"claude-desktop": "claude-desktop",
|
|
1602
|
-
"claude_desktop": "claude-desktop",
|
|
1603
|
-
claudesktop: "claude-desktop",
|
|
1604
|
-
desktop: "claude-desktop",
|
|
1605
|
-
hermes: "hermes",
|
|
1606
|
-
"hermes-agent": "hermes",
|
|
1607
|
-
hermes_agent: "hermes",
|
|
1608
|
-
hermesagent: "hermes",
|
|
1609
|
-
other: "other",
|
|
1610
|
-
manual: "other"
|
|
1611
|
-
};
|
|
1612
|
-
function runtimeProfile(runtime, env = process.env) {
|
|
1613
|
-
return RUNTIME_PROFILES[normalizeRuntime(runtime, env)];
|
|
1614
|
-
}
|
|
1615
|
-
function normalizeRuntime(runtime, env = process.env) {
|
|
1616
|
-
const explicit = normalizeRuntimeName(runtime);
|
|
1617
|
-
if (explicit) return explicit;
|
|
1618
|
-
return detectRuntime(env) ?? "other";
|
|
1619
|
-
}
|
|
1620
|
-
function restartRequiredForRuntime(runtime, env = process.env) {
|
|
1621
|
-
const mode = runtimeProfile(runtime, env).restartMode;
|
|
1622
|
-
return mode === "restart-session" || mode === "restart-app";
|
|
1623
|
-
}
|
|
1624
|
-
function runtimeVerificationInstruction(runtime) {
|
|
1625
|
-
const label = RUNTIME_PROFILES[runtime].label;
|
|
1626
|
-
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.`;
|
|
1627
|
-
}
|
|
1628
|
-
function normalizeRuntimeName(runtime) {
|
|
1629
|
-
const key = runtime?.trim().toLowerCase();
|
|
1630
|
-
if (!key) return null;
|
|
1631
|
-
return RUNTIME_ALIASES[key.replace(/\s+/g, "-")] ?? null;
|
|
1632
|
-
}
|
|
1633
|
-
function detectRuntime(env) {
|
|
1634
|
-
if (env.CLAUDECODE || env.CLAUDE_CODE || env.CLAUDECODE_CWD) return "claude-code";
|
|
1635
|
-
if (env.CODEX_SANDBOX || env.CODEX_HOME || env.CODEX_CWD) return "codex-cli";
|
|
1636
|
-
if (env.VSCODE_CWD || env.VSCODE_IPC_HOOK_CLI || env.TERM_PROGRAM === "vscode") return "vscode";
|
|
1637
|
-
if (env.HERMES_HOME || env.HERMES_AGENT) return "hermes";
|
|
1638
|
-
return null;
|
|
1639
|
-
}
|
|
1640
|
-
async function acknowledgeLocalSignerConsent(signerPath, log) {
|
|
1641
|
-
try {
|
|
1642
|
-
const input = await buildSignerConsentInput(signerPath);
|
|
1643
|
-
const decision = await ensureSignerConsent(input, {
|
|
1644
|
-
credentialsPath: signerPath,
|
|
1645
|
-
writeAck: true,
|
|
1646
|
-
out: log ? { write: (chunk) => writeLogChunk2(log, chunk) } : void 0
|
|
1647
|
-
});
|
|
1648
|
-
return {
|
|
1649
|
-
acknowledged: decision.ok,
|
|
1650
|
-
hash: decision.hash,
|
|
1651
|
-
reason: decision.reason
|
|
1652
|
-
};
|
|
1653
|
-
} catch (err) {
|
|
1654
|
-
return {
|
|
1655
|
-
acknowledged: false,
|
|
1656
|
-
error: err instanceof Error ? err.message : String(err)
|
|
1657
|
-
};
|
|
1658
|
-
}
|
|
1659
|
-
}
|
|
1660
|
-
async function getLocalSignerConsentStatus(signerPath) {
|
|
1661
|
-
try {
|
|
1662
|
-
const input = await buildSignerConsentInput(signerPath);
|
|
1663
|
-
const hash = computeSignerConsentHash(input);
|
|
1664
|
-
const stored = await readSignerAckFile(signerAckPath(signerPath));
|
|
1665
|
-
if (stored === hash) {
|
|
1666
|
-
return { acknowledged: true, hash, reason: "ack_file_match" };
|
|
1667
|
-
}
|
|
1668
|
-
return {
|
|
1669
|
-
acknowledged: false,
|
|
1670
|
-
hash,
|
|
1671
|
-
reason: stored ? "ack_file_mismatch" : "ack_file_missing"
|
|
1672
|
-
};
|
|
1673
|
-
} catch (err) {
|
|
1674
|
-
return {
|
|
1675
|
-
acknowledged: false,
|
|
1676
|
-
error: err instanceof Error ? err.message : String(err)
|
|
1677
|
-
};
|
|
1678
|
-
}
|
|
1679
|
-
}
|
|
1680
|
-
function signerAckPath(signerPath) {
|
|
1681
|
-
return resolve(`${signerPath}.signer-ack.json`);
|
|
1682
|
-
}
|
|
1683
|
-
async function buildSignerConsentInput(signerPath) {
|
|
1684
|
-
const credentials = await loadSignerCredentials(signerPath);
|
|
1685
|
-
const signer = createEdgeSigner(credentials.delegateKey, {
|
|
1686
|
-
x402BindingSigner: credentials.x402BindingSigner
|
|
1687
|
-
});
|
|
1688
|
-
return {
|
|
1689
|
-
delegateAddress: signer.delegateAddress,
|
|
1690
|
-
safeAddress: credentials.safeAddress,
|
|
1691
|
-
agentId: credentials.agentId,
|
|
1692
|
-
chainId: credentials.chainId,
|
|
1693
|
-
network: credentials.network,
|
|
1694
|
-
toolNames: Object.keys(toolSchemas)
|
|
1695
|
-
};
|
|
1696
|
-
}
|
|
1697
|
-
async function readSignerAckFile(path) {
|
|
1698
|
-
try {
|
|
1699
|
-
const parsed = JSON.parse(await readFile(path, "utf8"));
|
|
1700
|
-
return typeof parsed.ack === "string" ? parsed.ack : null;
|
|
1701
|
-
} catch {
|
|
1702
|
-
return null;
|
|
1703
|
-
}
|
|
1704
|
-
}
|
|
1705
|
-
function writeLogChunk2(log, chunk) {
|
|
1706
|
-
const message = String(chunk).trimEnd();
|
|
1707
|
-
if (message) log(message);
|
|
1708
|
-
}
|
|
1709
|
-
|
|
1710
2098
|
// src/runtime-install.ts
|
|
2099
|
+
init_runtime_registry();
|
|
2100
|
+
init_signer_consent();
|
|
1711
2101
|
var execFileAsync3 = promisify(execFile);
|
|
1712
2102
|
async function installRuntime(input, deps = {}) {
|
|
1713
2103
|
const runtime = normalizeRuntime(input.runtime, deps.env);
|
|
@@ -1783,9 +2173,28 @@ async function installRuntime(input, deps = {}) {
|
|
|
1783
2173
|
signerCommand = { command: signerRuntime.command, args: signerRuntime.args };
|
|
1784
2174
|
consentMessages.push(...signerRuntime.messages);
|
|
1785
2175
|
} catch (err) {
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
2176
|
+
return {
|
|
2177
|
+
runtime,
|
|
2178
|
+
runtimeMcpMode: "hosted_plus_signer",
|
|
2179
|
+
hostedMcpConfigured: false,
|
|
2180
|
+
localSignerConfigured: false,
|
|
2181
|
+
localMcpConfigured: false,
|
|
2182
|
+
probeResult: "signer_runtime_install_failed",
|
|
2183
|
+
restartRequired: false,
|
|
2184
|
+
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",
|
|
2185
|
+
errorCode: "signer_runtime_install_failed",
|
|
2186
|
+
configTarget: profile.label,
|
|
2187
|
+
signerAcknowledged: signerConsent?.acknowledged,
|
|
2188
|
+
localMcpAcknowledged: localMcpConsent?.acknowledged,
|
|
2189
|
+
activationCommand: void 0,
|
|
2190
|
+
signerRuntimePrepared: false,
|
|
2191
|
+
messages: [
|
|
2192
|
+
...consentMessages,
|
|
2193
|
+
`Could not pre-install the local Haven signer: ${err instanceof Error ? err.message : String(err)}`,
|
|
2194
|
+
"No runtime configuration was written (fail-closed): a config pointing at an uninstalled signer looks wired but cannot start.",
|
|
2195
|
+
"Re-run `npx @haven_ai/connect@alpha` to retry the setup."
|
|
2196
|
+
]
|
|
2197
|
+
};
|
|
1789
2198
|
}
|
|
1790
2199
|
}
|
|
1791
2200
|
const signerRuntimePrepared = localRuntime ? void 0 : signerCommand !== void 0;
|
|
@@ -1802,19 +2211,51 @@ async function installRuntime(input, deps = {}) {
|
|
|
1802
2211
|
homeDir: deps.homeDir,
|
|
1803
2212
|
mode: localRuntime ? "local" : "hosted"
|
|
1804
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);
|
|
2218
|
+
try {
|
|
2219
|
+
await deps.onRuntimeConfigured({
|
|
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
|
+
});
|
|
2231
|
+
} catch {
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
1805
2234
|
progress("Almost there \u2014 just confirming everything connects\u2026");
|
|
1806
2235
|
const localProbePromise = configResult.runtimeMcpMode === "local_stdio" && localRuntimeInstall ? runLocalMcpProbe(localRuntimeInstall, deps) : Promise.resolve(void 0);
|
|
1807
|
-
const
|
|
2236
|
+
const signerProbePromise = configResult.runtimeMcpMode !== "local_stdio" && signerCommand ? (deps.probeSignerTools ?? probeLocalMcpTools)(
|
|
2237
|
+
signerCommand.command,
|
|
2238
|
+
signerCommand.args,
|
|
2239
|
+
MCP_RUNTIME_MANIFEST.requiredSignerTools
|
|
2240
|
+
) : Promise.resolve(void 0);
|
|
2241
|
+
const [hostedProbe, signerCredentialReady, localMcpProbe, signerProbe] = await Promise.all([
|
|
1808
2242
|
configResult.hostedConfigured ? probeHostedMcpTools(input.apiKey, input.hostedMcpUrl, deps.fetch) : Promise.resolve({ status: "bad_response" }),
|
|
1809
2243
|
probeLocalSignerCredential(input.signerPath),
|
|
1810
|
-
localProbePromise
|
|
2244
|
+
localProbePromise,
|
|
2245
|
+
signerProbePromise
|
|
1811
2246
|
]);
|
|
1812
2247
|
const hostedOk = configResult.hostedConfigured && hostedProbe.status === "ok";
|
|
1813
2248
|
const localMcpOk = configResult.runtimeMcpMode === "local_stdio" && configResult.localMcpConfigured && signerCredentialReady && Boolean(localMcpConsent?.acknowledged) && localMcpProbe?.status === "ok";
|
|
1814
|
-
const signerOk = configResult.runtimeMcpMode === "local_stdio" ? localMcpOk : configResult.signerConfigured && signerCredentialReady && Boolean(signerConsent?.acknowledged)
|
|
2249
|
+
const signerOk = configResult.runtimeMcpMode === "local_stdio" ? localMcpOk : configResult.signerConfigured && signerCredentialReady && Boolean(signerConsent?.acknowledged) && // #1587: no handshake, no green. A signer command that was registered
|
|
2250
|
+
// but not probed (manual topology) keeps the old semantics.
|
|
2251
|
+
(signerProbe === void 0 || signerProbe.status === "ok");
|
|
1815
2252
|
const restartRequired = configResult.restartRequired || restartRequiredForRuntime(runtime, deps.env);
|
|
1816
|
-
const errorCode = configResult.errorCode ?? (configResult.runtimeMcpMode === "local_stdio" ? localMcpErrorCode(signerCredentialReady, localMcpConsent, localMcpProbe?.status) : hostedMcpErrorCode(configResult.hostedConfigured, hostedProbe.status) ?? signerConsentErrorCode(signerCredentialReady, signerConsent));
|
|
2253
|
+
const errorCode = configResult.errorCode ?? (configResult.runtimeMcpMode === "local_stdio" ? localMcpErrorCode(signerCredentialReady, localMcpConsent, localMcpProbe?.status) : hostedMcpErrorCode(configResult.hostedConfigured, hostedProbe.status) ?? signerConsentErrorCode(signerCredentialReady, signerConsent) ?? signerProbeErrorCode(signerProbe));
|
|
1817
2254
|
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."] : [];
|
|
2255
|
+
const signerProbeMessages = signerProbe ? signerProbe.status === "ok" ? ["Verified local Haven signer with a stdio handshake."] : [
|
|
2256
|
+
`Local Haven signer handshake failed: ${signerProbe.status}.`,
|
|
2257
|
+
"Re-run `npx @haven_ai/connect@alpha` to repair the signer setup."
|
|
2258
|
+
] : [];
|
|
1818
2259
|
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."] : [];
|
|
1819
2260
|
const skillInstall = !configResult.errorCode ? await installSkillForRuntime(runtime, { homeDir: deps.homeDir, env: deps.env }) : void 0;
|
|
1820
2261
|
return {
|
|
@@ -1833,7 +2274,7 @@ async function installRuntime(input, deps = {}) {
|
|
|
1833
2274
|
activationCommand: configResult.activationCommand,
|
|
1834
2275
|
skillInstalled: skillInstall?.installed,
|
|
1835
2276
|
signerRuntimePrepared,
|
|
1836
|
-
messages: [...consentMessages, ...localRuntimeInstall?.messages ?? [], ...configResult.messages, ...hostedProbeMessages, ...localProbeMessages, ...skillInstall?.messages ?? []]
|
|
2277
|
+
messages: [...consentMessages, ...localRuntimeInstall?.messages ?? [], ...configResult.messages, ...hostedProbeMessages, ...signerProbeMessages, ...localProbeMessages, ...skillInstall?.messages ?? []]
|
|
1837
2278
|
};
|
|
1838
2279
|
}
|
|
1839
2280
|
function runtimeInstallCapabilities(runtime, env = process.env) {
|
|
@@ -1853,10 +2294,11 @@ async function configureClaudeCode(deps, localMcpCommand) {
|
|
|
1853
2294
|
});
|
|
1854
2295
|
try {
|
|
1855
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);
|
|
1856
2299
|
await runCommand("claude", ["mcp", "add-json", "haven", serverJson, "--scope", "user"]).catch(async () => {
|
|
1857
2300
|
await runCommand("claude", ["mcp", "add", "haven", "--scope", "user", "--", localMcpCommand]);
|
|
1858
2301
|
});
|
|
1859
|
-
await runCommand("claude", ["mcp", "remove", "haven-signer"]).catch(() => void 0);
|
|
1860
2302
|
const verified = await runCommand("claude", ["mcp", "get", "haven"]).then(() => true).catch(() => false);
|
|
1861
2303
|
return {
|
|
1862
2304
|
hostedConfigured: false,
|
|
@@ -1978,6 +2420,10 @@ function signerConsentErrorCode(signerCredentialReady, signerConsent) {
|
|
|
1978
2420
|
if (!signerConsent?.acknowledged) return "local_signer_ack_required";
|
|
1979
2421
|
return void 0;
|
|
1980
2422
|
}
|
|
2423
|
+
function signerProbeErrorCode(probe) {
|
|
2424
|
+
if (!probe || probe.status === "ok") return void 0;
|
|
2425
|
+
return `local_signer_probe_${probe.status}`;
|
|
2426
|
+
}
|
|
1981
2427
|
function hostedMcpErrorCode(hostedConfigured, hostedProbeStatus) {
|
|
1982
2428
|
if (!hostedConfigured || hostedProbeStatus === "ok") return void 0;
|
|
1983
2429
|
return `hosted_mcp_probe_${hostedProbeStatus}`;
|
|
@@ -2001,7 +2447,7 @@ function supportsLocalMcp(runtime) {
|
|
|
2001
2447
|
return runtime === "codex-cli" || runtime === "codex-desktop" || runtime === "claude-code";
|
|
2002
2448
|
}
|
|
2003
2449
|
async function prepareRuntimeForLocalMcp(input, deps) {
|
|
2004
|
-
const prepare = deps.prepareLocalMcpRuntime ?? ((runtimeInput) => prepareLocalMcpRuntime(runtimeInput, { runCommand: deps.runCommand }));
|
|
2450
|
+
const prepare = deps.prepareLocalMcpRuntime ?? ((runtimeInput) => prepareLocalMcpRuntime(runtimeInput, { runCommand: deps.runCommand, onProgress: deps.onProgress }));
|
|
2005
2451
|
return prepare({
|
|
2006
2452
|
credentialDirectory: input.credentialDirectory,
|
|
2007
2453
|
identityPath: input.identityPath,
|
|
@@ -2010,7 +2456,13 @@ async function prepareRuntimeForLocalMcp(input, deps) {
|
|
|
2010
2456
|
});
|
|
2011
2457
|
}
|
|
2012
2458
|
async function prepareSignerForRuntime(input, deps) {
|
|
2013
|
-
const prepare = deps.prepareSignerRuntime ?? ((runtimeInput) =>
|
|
2459
|
+
const prepare = deps.prepareSignerRuntime ?? ((runtimeInput) => (
|
|
2460
|
+
// onProgress threaded through on purpose (#1586 review): without it the
|
|
2461
|
+
// install heartbeat was dead code in production and the console still
|
|
2462
|
+
// went silent for the whole cold install — the exact symptom the issue
|
|
2463
|
+
// set out to remove, at a longer timeout.
|
|
2464
|
+
prepareSignerRuntime(runtimeInput, { runCommand: deps.runCommand, onProgress: deps.onProgress })
|
|
2465
|
+
));
|
|
2014
2466
|
return prepare({
|
|
2015
2467
|
credentialDirectory: input.credentialDirectory,
|
|
2016
2468
|
signerPath: input.signerPath,
|
|
@@ -2033,7 +2485,9 @@ function localRuntimePrepareErrorCode(err) {
|
|
|
2033
2485
|
}
|
|
2034
2486
|
|
|
2035
2487
|
// src/runtime.ts
|
|
2036
|
-
|
|
2488
|
+
init_runtime_registry();
|
|
2489
|
+
init_runtime_manifest();
|
|
2490
|
+
var CONNECTOR_VERSION = "0.1.27-alpha.0";
|
|
2037
2491
|
var CONNECT_OUTCOME_SCHEMA_VERSION = 1;
|
|
2038
2492
|
async function runConnect(options, deps = {}) {
|
|
2039
2493
|
assertSupportedNodeVersion(deps.nodeVersion, MCP_RUNTIME_MANIFEST.minimumNodeVersion);
|
|
@@ -2140,7 +2594,35 @@ async function runConnect(options, deps = {}) {
|
|
|
2140
2594
|
ackSigner: options.ackSigner,
|
|
2141
2595
|
ackLocalTools: options.ackLocalTools,
|
|
2142
2596
|
localMcp: options.localMcp
|
|
2143
|
-
}, {
|
|
2597
|
+
}, {
|
|
2598
|
+
onProgress: log,
|
|
2599
|
+
// #1543: report "runtime configured" the moment the config write settles,
|
|
2600
|
+
// so the dashboard can expose its budget-approval controls without
|
|
2601
|
+
// waiting on the probes and skill install — a tail that approval does not
|
|
2602
|
+
// depend on. Silent and best-effort: the complete report below remains
|
|
2603
|
+
// authoritative (it overwrites these keys, adding the probe verdicts and
|
|
2604
|
+
// skill state), so an early failure costs nothing but the head start.
|
|
2605
|
+
onRuntimeConfigured: async (early) => {
|
|
2606
|
+
try {
|
|
2607
|
+
await api.updateInstallStatus(registration.setup_id, localApiKey, {
|
|
2608
|
+
runtime: early.runtime,
|
|
2609
|
+
connectorVersion,
|
|
2610
|
+
runtimeMcpMode: early.runtimeMcpMode,
|
|
2611
|
+
hostedMcpConfigured: early.hostedMcpConfigured,
|
|
2612
|
+
localSignerConfigured: early.localSignerConfigured,
|
|
2613
|
+
localMcpConfigured: early.localMcpConfigured,
|
|
2614
|
+
credentialFilesWritten: true,
|
|
2615
|
+
signerAcknowledged: early.signerAcknowledged,
|
|
2616
|
+
localMcpAcknowledged: early.localMcpAcknowledged,
|
|
2617
|
+
restartRequired: early.restartRequired,
|
|
2618
|
+
nextUserAction: early.nextUserAction,
|
|
2619
|
+
errorCode: early.errorCode ?? null,
|
|
2620
|
+
environmentLabel: options.environmentLabel ?? "Local workspace"
|
|
2621
|
+
});
|
|
2622
|
+
} catch {
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
});
|
|
2144
2626
|
printRuntimeInstall(runtimeInstall, log);
|
|
2145
2627
|
if (runtimeInstall.errorCode) {
|
|
2146
2628
|
log("Haven setup needs a couple more steps on this machine \u2014 see the notes above.");
|
|
@@ -2169,10 +2651,11 @@ async function runConnect(options, deps = {}) {
|
|
|
2169
2651
|
} catch (err) {
|
|
2170
2652
|
log(`Could not report install status to Haven: ${err instanceof Error ? err.message : String(err)}`);
|
|
2171
2653
|
}
|
|
2654
|
+
let approval;
|
|
2172
2655
|
if (options.waitForApproval !== false && !runtimeInstall.errorCode) {
|
|
2173
|
-
await waitForBudgetApproval(api, registration.setup_id, localApiKey, log, options.approvalWait);
|
|
2656
|
+
approval = await waitForBudgetApproval(api, registration.setup_id, localApiKey, log, options.approvalWait);
|
|
2174
2657
|
}
|
|
2175
|
-
printNextSteps(runtimeInstall, log);
|
|
2658
|
+
printNextSteps(runtimeInstall, log, approval);
|
|
2176
2659
|
return {
|
|
2177
2660
|
setupId: registration.setup_id,
|
|
2178
2661
|
agentId: registration.agent_id,
|
|
@@ -2313,9 +2796,14 @@ async function waitForBudgetApproval(api, setupId, apiKey, log, options = {}) {
|
|
|
2313
2796
|
const sleep = options.sleep ?? ((ms) => new Promise((resolve8) => setTimeout(resolve8, ms)));
|
|
2314
2797
|
const maxPolls = Math.max(1, Math.floor(timeoutMs / intervalMs));
|
|
2315
2798
|
const remindEvery = Math.max(1, Math.floor(3e4 / intervalMs));
|
|
2316
|
-
|
|
2799
|
+
let waitingAnnounced = false;
|
|
2800
|
+
const announceWaiting = () => {
|
|
2801
|
+
if (waitingAnnounced) return;
|
|
2802
|
+
waitingAnnounced = true;
|
|
2803
|
+
log("Registered with Haven \u2014 waiting for you to approve the budget in the dashboard\u2026");
|
|
2804
|
+
};
|
|
2317
2805
|
for (let i = 0; i < maxPolls; i++) {
|
|
2318
|
-
await sleep(intervalMs);
|
|
2806
|
+
if (i > 0) await sleep(intervalMs);
|
|
2319
2807
|
let status;
|
|
2320
2808
|
try {
|
|
2321
2809
|
status = await api.getConnectorStatus(setupId, apiKey);
|
|
@@ -2324,6 +2812,7 @@ async function waitForBudgetApproval(api, setupId, apiKey, log, options = {}) {
|
|
|
2324
2812
|
log("This setup ended in Haven \u2014 start a fresh connection from the dashboard when ready.");
|
|
2325
2813
|
return "ended";
|
|
2326
2814
|
}
|
|
2815
|
+
announceWaiting();
|
|
2327
2816
|
continue;
|
|
2328
2817
|
}
|
|
2329
2818
|
if (status.status === "active") {
|
|
@@ -2336,7 +2825,8 @@ async function waitForBudgetApproval(api, setupId, apiKey, log, options = {}) {
|
|
|
2336
2825
|
log(`This setup ended in Haven (${status.status}) \u2014 start a fresh connection from the dashboard when ready.`);
|
|
2337
2826
|
return "ended";
|
|
2338
2827
|
}
|
|
2339
|
-
|
|
2828
|
+
announceWaiting();
|
|
2829
|
+
if (i > 0 && i % remindEvery === 0) {
|
|
2340
2830
|
log("Still waiting for budget approval in Haven\u2026");
|
|
2341
2831
|
}
|
|
2342
2832
|
}
|
|
@@ -2345,11 +2835,11 @@ async function waitForBudgetApproval(api, setupId, apiKey, log, options = {}) {
|
|
|
2345
2835
|
);
|
|
2346
2836
|
return "pending";
|
|
2347
2837
|
}
|
|
2348
|
-
function completionHandoffLines(result) {
|
|
2838
|
+
function completionHandoffLines(result, approval) {
|
|
2349
2839
|
if (result.errorCode === "manual_runtime_setup_required") {
|
|
2350
2840
|
return [
|
|
2351
2841
|
"Next steps:",
|
|
2352
|
-
"1. Return to Haven and approve the
|
|
2842
|
+
"1. Return to Haven and approve the budget. Approval \u2014 not restarting \u2014 unlocks Haven tools.",
|
|
2353
2843
|
"2. Finish the manual MCP setup using the secret-free file references printed above, then start a fresh session in your runtime.",
|
|
2354
2844
|
`3. ${runtimeVerificationInstruction(result.runtime)}`
|
|
2355
2845
|
];
|
|
@@ -2359,16 +2849,38 @@ function completionHandoffLines(result) {
|
|
|
2359
2849
|
"Recovery: runtime setup is not complete. Resolve the reported problem, then return to Haven for a fresh connection and run its new Connect command. Do not manually edit runtime config or paste credentials into prompts, logs, or config."
|
|
2360
2850
|
];
|
|
2361
2851
|
}
|
|
2852
|
+
if (approval === "ended") {
|
|
2853
|
+
return [
|
|
2854
|
+
"Next step: this setup ended in Haven before the budget was approved, so there is nothing left to approve on it \u2014 start a fresh connection from the dashboard when ready."
|
|
2855
|
+
];
|
|
2856
|
+
}
|
|
2362
2857
|
const profile = runtimeProfile(result.runtime);
|
|
2858
|
+
const activation = activationInstructionWithWhy(profile);
|
|
2859
|
+
if (approval === "approved") {
|
|
2860
|
+
return [
|
|
2861
|
+
"Next steps \u2014 the budget is already approved, so there is nothing more to do in Haven:",
|
|
2862
|
+
`1. ${activation}`,
|
|
2863
|
+
`2. ${runtimeVerificationInstruction(result.runtime)}`
|
|
2864
|
+
];
|
|
2865
|
+
}
|
|
2363
2866
|
return [
|
|
2364
2867
|
"Next steps:",
|
|
2365
|
-
"1. Return to Haven and approve the
|
|
2366
|
-
`2. ${
|
|
2868
|
+
"1. Return to Haven and approve the budget. Approval \u2014 not restarting \u2014 unlocks Haven tools.",
|
|
2869
|
+
`2. ${activation}`,
|
|
2367
2870
|
`3. ${runtimeVerificationInstruction(result.runtime)}`
|
|
2368
2871
|
];
|
|
2369
2872
|
}
|
|
2370
|
-
function
|
|
2371
|
-
|
|
2873
|
+
function activationInstructionWithWhy(profile) {
|
|
2874
|
+
if (profile.restartMode === "restart-session") {
|
|
2875
|
+
return `${profile.activationInstruction} (The entries are already written and verified \u2014 ${profile.label} only reads MCP config when a session starts, which is why this step is still needed.)`;
|
|
2876
|
+
}
|
|
2877
|
+
if (profile.restartMode === "restart-app") {
|
|
2878
|
+
return `${profile.activationInstruction} (The entries are already written and verified \u2014 ${profile.label} only reads MCP config at app launch, which is why this step is still needed.)`;
|
|
2879
|
+
}
|
|
2880
|
+
return profile.activationInstruction;
|
|
2881
|
+
}
|
|
2882
|
+
function printNextSteps(result, log, approval) {
|
|
2883
|
+
for (const line of completionHandoffLines(result, approval)) log(line);
|
|
2372
2884
|
}
|
|
2373
2885
|
|
|
2374
2886
|
// src/args.ts
|
|
@@ -2379,12 +2891,18 @@ function parseArgs(argv, env = process.env) {
|
|
|
2379
2891
|
};
|
|
2380
2892
|
let help = false;
|
|
2381
2893
|
let json = false;
|
|
2894
|
+
let doctor = false;
|
|
2895
|
+
let repair = false;
|
|
2382
2896
|
for (let i = 0; i < argv.length; i += 1) {
|
|
2383
2897
|
const arg = argv[i];
|
|
2384
2898
|
if (arg === "--help" || arg === "-h") {
|
|
2385
2899
|
help = true;
|
|
2386
2900
|
} else if (arg === "--json") {
|
|
2387
2901
|
json = true;
|
|
2902
|
+
} else if (arg === "--doctor") {
|
|
2903
|
+
doctor = true;
|
|
2904
|
+
} else if (arg === "--repair") {
|
|
2905
|
+
repair = true;
|
|
2388
2906
|
} else if (arg === "--setup" || arg === "--setup-token") {
|
|
2389
2907
|
options.setupToken = requireValue(argv, ++i, arg);
|
|
2390
2908
|
} else if (arg === "--api" || arg === "--api-url") {
|
|
@@ -2411,7 +2929,13 @@ function parseArgs(argv, env = process.env) {
|
|
|
2411
2929
|
}
|
|
2412
2930
|
}
|
|
2413
2931
|
if (help) {
|
|
2414
|
-
return { options, help, json };
|
|
2932
|
+
return { options, help, json, doctor, repair };
|
|
2933
|
+
}
|
|
2934
|
+
if (doctor || repair) {
|
|
2935
|
+
if (!options.runtime) {
|
|
2936
|
+
throw new Error("--doctor/--repair need --runtime <runtime> (which config to examine).");
|
|
2937
|
+
}
|
|
2938
|
+
return { options, help, json, doctor, repair };
|
|
2415
2939
|
}
|
|
2416
2940
|
if (!options.setupToken) {
|
|
2417
2941
|
throw new Error("Missing --setup <hv_setup_...> setup token.");
|
|
@@ -2420,7 +2944,7 @@ function parseArgs(argv, env = process.env) {
|
|
|
2420
2944
|
throw new Error("Missing --api <Haven API URL>.");
|
|
2421
2945
|
}
|
|
2422
2946
|
options.apiBaseUrl = options.apiBaseUrl.replace(/\/+$/, "");
|
|
2423
|
-
return { options, help, json };
|
|
2947
|
+
return { options, help, json, doctor, repair };
|
|
2424
2948
|
}
|
|
2425
2949
|
function helpText() {
|
|
2426
2950
|
return [
|
|
@@ -2443,6 +2967,11 @@ function helpText() {
|
|
|
2443
2967
|
" --local Advanced: install the fully-local Haven MCP (no hosted dependency).",
|
|
2444
2968
|
" Only available for Claude Code and Codex. Default is hosted MCP + local signer.",
|
|
2445
2969
|
" --json Emit one versioned, secret-free result object on stdout; progress stays on stderr.",
|
|
2970
|
+
" --doctor Diagnose an existing setup (read-only, no token): config, credentials,",
|
|
2971
|
+
" signer runtime, hosted MCP, and a live signer handshake. Exits non-zero on any failure.",
|
|
2972
|
+
" --repair Repair, then re-diagnose (implies --doctor): reinstall the pinned signer",
|
|
2973
|
+
" runtime, rewrite the wrapper and runtime config from stored credentials.",
|
|
2974
|
+
" Hosted topology only (refuses to touch a --local config). No keys, no token.",
|
|
2446
2975
|
" --help Show this help.",
|
|
2447
2976
|
"",
|
|
2448
2977
|
"The connector never prints the private key and never sends it to Haven. JSON output never includes credential contents or full credential paths."
|
|
@@ -2478,6 +3007,37 @@ async function runCli(argv, io = {
|
|
|
2478
3007
|
`);
|
|
2479
3008
|
return 0;
|
|
2480
3009
|
}
|
|
3010
|
+
if (parsed.doctor || parsed.repair) {
|
|
3011
|
+
const { runDoctor: runDoctor2, runRepair: runRepair2 } = await Promise.resolve().then(() => (init_doctor(), doctor_exports));
|
|
3012
|
+
const runtime = parsed.options.runtime ?? "";
|
|
3013
|
+
const credentialsDir = parsed.options.credentialsDir;
|
|
3014
|
+
try {
|
|
3015
|
+
if (parsed.repair) {
|
|
3016
|
+
const repair = await runRepair2({ runtime, credentialsDir });
|
|
3017
|
+
for (const message of repair.messages) io.stderr(`${redactSecrets(message)}
|
|
3018
|
+
`);
|
|
3019
|
+
if (!repair.ok) return 1;
|
|
3020
|
+
}
|
|
3021
|
+
const report = await runDoctor2({ runtime, credentialsDir });
|
|
3022
|
+
if (parsed.json) {
|
|
3023
|
+
io.stdout(`${redactSecrets(JSON.stringify(report))}
|
|
3024
|
+
`);
|
|
3025
|
+
} else {
|
|
3026
|
+
for (const check of report.checks) {
|
|
3027
|
+
io.stdout(redactSecrets(`${check.ok ? "\u2713" : "\u2717"} ${check.label}: ${check.detail}
|
|
3028
|
+
`));
|
|
3029
|
+
if (check.repair) io.stdout(redactSecrets(` \u21B3 repair: ${check.repair}
|
|
3030
|
+
`));
|
|
3031
|
+
}
|
|
3032
|
+
io.stdout(report.ok ? "All checks passed.\n" : "One or more checks FAILED \u2014 see repairs above.\n");
|
|
3033
|
+
}
|
|
3034
|
+
return report.ok ? 0 : 1;
|
|
3035
|
+
} catch (err) {
|
|
3036
|
+
io.stderr(`${redactSecrets(err instanceof Error ? err.message : String(err))}
|
|
3037
|
+
`);
|
|
3038
|
+
return 1;
|
|
3039
|
+
}
|
|
3040
|
+
}
|
|
2481
3041
|
try {
|
|
2482
3042
|
const result = await runConnect(
|
|
2483
3043
|
{ ...parsed.options, waitForApproval: !parsed.json },
|
|
@@ -2515,6 +3075,11 @@ function isCliEntrypoint(argvPath = process.argv[1], moduleUrl = import.meta.url
|
|
|
2515
3075
|
}
|
|
2516
3076
|
if (isCliEntrypoint()) void main();
|
|
2517
3077
|
|
|
3078
|
+
// src/index.ts
|
|
3079
|
+
init_signer_runtime();
|
|
3080
|
+
init_runtime_registry();
|
|
3081
|
+
init_runtime_manifest();
|
|
3082
|
+
|
|
2518
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 };
|
|
2519
3084
|
//# sourceMappingURL=index.js.map
|
|
2520
3085
|
//# sourceMappingURL=index.js.map
|