@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/cli.cjs
CHANGED
|
@@ -1,270 +1,33 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
var
|
|
5
|
-
var
|
|
4
|
+
var mcp = require('@haven_ai/mcp');
|
|
5
|
+
var signer = require('@haven_ai/signer');
|
|
6
6
|
var sdk = require('@haven_ai/sdk');
|
|
7
|
-
var crypto = require('crypto');
|
|
8
|
-
var ethers = require('ethers');
|
|
9
7
|
var promises = require('fs/promises');
|
|
10
8
|
var os = require('os');
|
|
11
9
|
var path = require('path');
|
|
10
|
+
var yaml = require('yaml');
|
|
12
11
|
var child_process = require('child_process');
|
|
13
12
|
var util = require('util');
|
|
14
|
-
var
|
|
15
|
-
var
|
|
16
|
-
var
|
|
13
|
+
var fs = require('fs');
|
|
14
|
+
var url = require('url');
|
|
15
|
+
var crypto = require('crypto');
|
|
16
|
+
var ethers = require('ethers');
|
|
17
17
|
|
|
18
18
|
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
19
19
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
20
20
|
|
|
21
21
|
var crypto__default = /*#__PURE__*/_interopDefault(crypto);
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
return
|
|
27
|
-
resolveSetup: (input) => request(fetchImpl, `${root}/agent-connection-setups/resolve`, {
|
|
28
|
-
method: "POST",
|
|
29
|
-
body: JSON.stringify({
|
|
30
|
-
setup_token: input.setupToken,
|
|
31
|
-
connector_version: input.connectorVersion,
|
|
32
|
-
runtime: input.runtime
|
|
33
|
-
})
|
|
34
|
-
}),
|
|
35
|
-
registerSetup: (input) => request(fetchImpl, `${root}/agent-connection-setups/register`, {
|
|
36
|
-
method: "POST",
|
|
37
|
-
body: JSON.stringify({
|
|
38
|
-
setup_token: input.setupToken,
|
|
39
|
-
challenge_id: input.challengeId,
|
|
40
|
-
delegate_address: input.delegateAddress,
|
|
41
|
-
proof_signature: input.proofSignature,
|
|
42
|
-
api_key_hash: input.apiKeyHash,
|
|
43
|
-
api_key_prefix: input.apiKeyPrefix,
|
|
44
|
-
runtime: input.runtime,
|
|
45
|
-
connector_version: input.connectorVersion,
|
|
46
|
-
connector_context: input.connectorContext,
|
|
47
|
-
install_capabilities: input.installCapabilities && {
|
|
48
|
-
can_write_runtime_config: input.installCapabilities.canWriteRuntimeConfig,
|
|
49
|
-
restart_required: input.installCapabilities.restartRequired
|
|
50
|
-
}
|
|
51
|
-
})
|
|
52
|
-
}),
|
|
53
|
-
getConnectorStatus: (setupId, apiKey) => request(fetchImpl, `${root}/agent-connection-setups/${encodeURIComponent(setupId)}/connector-status`, {
|
|
54
|
-
method: "GET",
|
|
55
|
-
headers: { Authorization: `Bearer ${apiKey}` }
|
|
56
|
-
}),
|
|
57
|
-
updateInstallStatus: async (setupId, apiKey, input) => {
|
|
58
|
-
await request(fetchImpl, `${root}/agent-connection-setups/${encodeURIComponent(setupId)}/install-status`, {
|
|
59
|
-
method: "POST",
|
|
60
|
-
headers: { Authorization: `Bearer ${apiKey}` },
|
|
61
|
-
body: JSON.stringify({
|
|
62
|
-
runtime: input.runtime,
|
|
63
|
-
connector_version: input.connectorVersion,
|
|
64
|
-
runtime_mcp_mode: input.runtimeMcpMode,
|
|
65
|
-
hosted_mcp_configured: input.hostedMcpConfigured,
|
|
66
|
-
local_signer_configured: input.localSignerConfigured,
|
|
67
|
-
local_mcp_configured: input.localMcpConfigured,
|
|
68
|
-
credential_files_written: input.credentialFilesWritten,
|
|
69
|
-
signer_acknowledged: input.signerAcknowledged,
|
|
70
|
-
local_mcp_acknowledged: input.localMcpAcknowledged,
|
|
71
|
-
activation_command_available: input.activationCommandAvailable,
|
|
72
|
-
skill_installed: input.skillInstalled,
|
|
73
|
-
probe_result: input.probeResult,
|
|
74
|
-
restart_required: input.restartRequired,
|
|
75
|
-
next_user_action: input.nextUserAction,
|
|
76
|
-
error_code: input.errorCode ?? null,
|
|
77
|
-
environment_label: input.environmentLabel
|
|
78
|
-
})
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
var ConnectRequestError = class extends Error {
|
|
84
|
-
constructor(message, status) {
|
|
85
|
-
super(message);
|
|
86
|
-
this.status = status;
|
|
87
|
-
this.name = "ConnectRequestError";
|
|
88
|
-
}
|
|
89
|
-
status;
|
|
23
|
+
var __defProp = Object.defineProperty;
|
|
24
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
25
|
+
var __esm = (fn, res) => function __init() {
|
|
26
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
90
27
|
};
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
headers: {
|
|
95
|
-
"Content-Type": "application/json",
|
|
96
|
-
...init.headers ?? {}
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
const text = await response.text();
|
|
100
|
-
const body = text ? JSON.parse(text) : null;
|
|
101
|
-
if (!response.ok) {
|
|
102
|
-
const message = body?.error ?? body?.message ?? `${response.status} ${response.statusText}`;
|
|
103
|
-
throw new ConnectRequestError(`Haven setup request failed: ${message}`, response.status);
|
|
104
|
-
}
|
|
105
|
-
return body;
|
|
106
|
-
}
|
|
107
|
-
function generateDelegateKey() {
|
|
108
|
-
return delegateKeyFromPrivateKey(ethers.Wallet.createRandom().privateKey);
|
|
109
|
-
}
|
|
110
|
-
function delegateKeyFromPrivateKey(privateKey) {
|
|
111
|
-
const wallet = new ethers.Wallet(privateKey);
|
|
112
|
-
return {
|
|
113
|
-
privateKey: wallet.privateKey,
|
|
114
|
-
address: wallet.address,
|
|
115
|
-
signChallenge: (message) => wallet.signMessage(message)
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
function generateAgentApiKey() {
|
|
119
|
-
return `sk_agent_${crypto__default.default.randomBytes(24).toString("hex")}`;
|
|
120
|
-
}
|
|
121
|
-
function hashAgentApiKey(apiKey) {
|
|
122
|
-
return crypto__default.default.createHash("sha256").update(apiKey).digest("hex");
|
|
123
|
-
}
|
|
124
|
-
function agentApiKeyPrefix(apiKey) {
|
|
125
|
-
return apiKey.slice(0, 12);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// src/redact.ts
|
|
129
|
-
var API_KEY_RE = /sk_agent_[A-Za-z0-9]+/g;
|
|
130
|
-
var PRIVATE_KEY_RE = /0x[0-9a-fA-F]{64}/g;
|
|
131
|
-
function redactSecrets(value) {
|
|
132
|
-
return value.replace(API_KEY_RE, "sk_agent_[redacted]").replace(PRIVATE_KEY_RE, "0x[redacted-private-key]");
|
|
133
|
-
}
|
|
134
|
-
function shortAddress(address) {
|
|
135
|
-
if (!/^0x[0-9a-fA-F]{40}$/.test(address)) return address;
|
|
136
|
-
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
|
137
|
-
}
|
|
138
|
-
async function preflightCredentialStorage(input = {}) {
|
|
139
|
-
const directory = defaultCredentialRoot(input.baseDir);
|
|
140
|
-
await promises.mkdir(directory, { recursive: true, mode: 448 });
|
|
141
|
-
await restrictPermissions(directory, 448, input.warn);
|
|
142
|
-
const probePath = path.join(directory, `.haven-connect-preflight-${crypto__default.default.randomBytes(8).toString("hex")}`);
|
|
143
|
-
try {
|
|
144
|
-
await writeOwnerOnlyJson(probePath, { ok: true }, input.warn);
|
|
145
|
-
} finally {
|
|
146
|
-
await promises.rm(probePath, { force: true }).catch(() => void 0);
|
|
147
|
-
}
|
|
148
|
-
return directory;
|
|
149
|
-
}
|
|
150
|
-
async function writeCredentialFiles(input) {
|
|
151
|
-
const directory = defaultAgentDirectory(input.agentId, input.baseDir);
|
|
152
|
-
await promises.mkdir(directory, { recursive: true, mode: 448 });
|
|
153
|
-
await restrictPermissions(directory, 448, input.warn);
|
|
154
|
-
const identityPath = path.join(directory, "identity.json");
|
|
155
|
-
const signerPath = path.join(directory, "signer.json");
|
|
156
|
-
const agentPath = path.join(directory, "agent.json");
|
|
157
|
-
await assertDoesNotExist(identityPath);
|
|
158
|
-
await assertDoesNotExist(signerPath);
|
|
159
|
-
await assertDoesNotExist(agentPath);
|
|
160
|
-
await writeOwnerOnlyJson(
|
|
161
|
-
signerPath,
|
|
162
|
-
{
|
|
163
|
-
delegate_key: input.delegateKey,
|
|
164
|
-
delegate_address: input.delegateAddress,
|
|
165
|
-
agent_id: input.agentId,
|
|
166
|
-
safe_address: input.safeAddress,
|
|
167
|
-
chain_id: input.chainId,
|
|
168
|
-
network: input.network,
|
|
169
|
-
x402_binding_signer: input.x402BindingSigner,
|
|
170
|
-
note: "Local signer credential. Haven backend never receives this private key."
|
|
171
|
-
},
|
|
172
|
-
input.warn
|
|
173
|
-
);
|
|
174
|
-
try {
|
|
175
|
-
await writeOwnerOnlyJson(
|
|
176
|
-
identityPath,
|
|
177
|
-
{
|
|
178
|
-
api_key: input.apiKey,
|
|
179
|
-
agent_id: input.agentId,
|
|
180
|
-
safe_address: input.safeAddress,
|
|
181
|
-
chain_id: input.chainId,
|
|
182
|
-
network: input.network,
|
|
183
|
-
api_url: input.apiUrl,
|
|
184
|
-
hosted_mcp_url: input.hostedMcpUrl,
|
|
185
|
-
agent_budget: input.agentBudget,
|
|
186
|
-
note: "Haven API key identifies the agent only. It cannot spend without the local signer key and on-chain Haven wallet rules."
|
|
187
|
-
},
|
|
188
|
-
input.warn
|
|
189
|
-
);
|
|
190
|
-
} catch (err) {
|
|
191
|
-
await promises.rm(signerPath, { force: true }).catch(() => void 0);
|
|
192
|
-
throw err;
|
|
193
|
-
}
|
|
194
|
-
try {
|
|
195
|
-
await writeOwnerOnlyJson(
|
|
196
|
-
agentPath,
|
|
197
|
-
{
|
|
198
|
-
agent_id: input.agentId,
|
|
199
|
-
delegate_address: input.delegateAddress,
|
|
200
|
-
safe_address: input.safeAddress,
|
|
201
|
-
chain_id: input.chainId,
|
|
202
|
-
network: input.network,
|
|
203
|
-
agent_budget: input.agentBudget,
|
|
204
|
-
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."
|
|
205
|
-
},
|
|
206
|
-
input.warn
|
|
207
|
-
);
|
|
208
|
-
} catch (err) {
|
|
209
|
-
await promises.rm(signerPath, { force: true }).catch(() => void 0);
|
|
210
|
-
await promises.rm(identityPath, { force: true }).catch(() => void 0);
|
|
211
|
-
await promises.rm(agentPath, { force: true }).catch(() => void 0);
|
|
212
|
-
throw err;
|
|
213
|
-
}
|
|
214
|
-
return { directory, identityPath, signerPath, agentPath };
|
|
215
|
-
}
|
|
216
|
-
function defaultAgentDirectory(agentId, baseDir = path.join(os.homedir(), ".haven", "agents")) {
|
|
217
|
-
return path.resolve(defaultCredentialRoot(baseDir), safePathPart(agentId));
|
|
218
|
-
}
|
|
219
|
-
function defaultCredentialRoot(baseDir = path.join(os.homedir(), ".haven", "agents")) {
|
|
220
|
-
return path.resolve(baseDir);
|
|
221
|
-
}
|
|
222
|
-
async function writeOwnerOnlyJson(path, value, warn) {
|
|
223
|
-
const json = JSON.stringify(dropUndefined(value), null, 2);
|
|
224
|
-
await promises.writeFile(path, `${json}
|
|
225
|
-
`, { mode: 384, flag: "wx" });
|
|
226
|
-
await restrictPermissions(path, 384, warn);
|
|
227
|
-
}
|
|
228
|
-
function safePathPart(value) {
|
|
229
|
-
return value.replace(/[^A-Za-z0-9_.-]/g, "_");
|
|
230
|
-
}
|
|
231
|
-
function dropUndefined(value) {
|
|
232
|
-
return Object.fromEntries(Object.entries(value).filter(([, field]) => field !== void 0));
|
|
233
|
-
}
|
|
234
|
-
async function assertDoesNotExist(path) {
|
|
235
|
-
try {
|
|
236
|
-
await promises.access(path);
|
|
237
|
-
} catch (err) {
|
|
238
|
-
if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") return;
|
|
239
|
-
throw err;
|
|
240
|
-
}
|
|
241
|
-
throw new Error(`Refusing to overwrite existing Haven credential file: ${path}`);
|
|
242
|
-
}
|
|
243
|
-
async function restrictPermissions(path, mode, warn) {
|
|
244
|
-
try {
|
|
245
|
-
await promises.chmod(path, mode);
|
|
246
|
-
} catch (err) {
|
|
247
|
-
warn?.(
|
|
248
|
-
`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)}`
|
|
249
|
-
);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
var MCP_RUNTIME_MANIFEST = {
|
|
253
|
-
mcpPackage: "@haven_ai/mcp",
|
|
254
|
-
mcpVersion: mcp.MCP_VERSION,
|
|
255
|
-
sdkPackage: "@haven_ai/sdk",
|
|
256
|
-
sdkVersion: "0.1.26-alpha.0",
|
|
257
|
-
signerPackage: "@haven_ai/signer",
|
|
258
|
-
signerVersion: "0.1.26-alpha.0",
|
|
259
|
-
// Sourced from the SDK, never a literal (#1161). This field read '20.0.0'
|
|
260
|
-
// while every package's `engines` said `>=24` and the docs said `>=24.0.0`,
|
|
261
|
-
// so the guard that was supposed to enforce the floor waved Node v23 through
|
|
262
|
-
// — including on the `--local` path where it does run. A hand-maintained
|
|
263
|
-
// second copy of a number is a drift waiting to happen; a guard test pins
|
|
264
|
-
// this against `package.json`'s `engines.node`.
|
|
265
|
-
minimumNodeVersion: sdk.HAVEN_MINIMUM_NODE_VERSION,
|
|
266
|
-
supportedClients: ["codex-cli", "codex-desktop", "claude-code"],
|
|
267
|
-
requiredTools: mcp.registeredToolNames()
|
|
28
|
+
var __export = (target, all) => {
|
|
29
|
+
for (var name in all)
|
|
30
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
268
31
|
};
|
|
269
32
|
function mcpPackageSpec() {
|
|
270
33
|
return `${MCP_RUNTIME_MANIFEST.mcpPackage}@${MCP_RUNTIME_MANIFEST.mcpVersion}`;
|
|
@@ -275,21 +38,35 @@ function sdkPackageSpec() {
|
|
|
275
38
|
function signerPackageSpec() {
|
|
276
39
|
return `${MCP_RUNTIME_MANIFEST.signerPackage}@${MCP_RUNTIME_MANIFEST.signerVersion}`;
|
|
277
40
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
41
|
+
var MCP_RUNTIME_MANIFEST;
|
|
42
|
+
var init_runtime_manifest = __esm({
|
|
43
|
+
"src/runtime-manifest.ts"() {
|
|
44
|
+
MCP_RUNTIME_MANIFEST = {
|
|
45
|
+
mcpPackage: "@haven_ai/mcp",
|
|
46
|
+
mcpVersion: mcp.MCP_VERSION,
|
|
47
|
+
sdkPackage: "@haven_ai/sdk",
|
|
48
|
+
sdkVersion: "0.1.27-alpha.0",
|
|
49
|
+
signerPackage: "@haven_ai/signer",
|
|
50
|
+
signerVersion: "0.1.27-alpha.0",
|
|
51
|
+
// Sourced from the SDK, never a literal (#1161). This field read '20.0.0'
|
|
52
|
+
// while every package's `engines` said `>=24` and the docs said `>=24.0.0`,
|
|
53
|
+
// so the guard that was supposed to enforce the floor waved Node v23 through
|
|
54
|
+
// — including on the `--local` path where it does run. A hand-maintained
|
|
55
|
+
// second copy of a number is a drift waiting to happen; a guard test pins
|
|
56
|
+
// this against `package.json`'s `engines.node`.
|
|
57
|
+
minimumNodeVersion: sdk.HAVEN_MINIMUM_NODE_VERSION,
|
|
58
|
+
supportedClients: ["codex-cli", "codex-desktop", "claude-code"],
|
|
59
|
+
requiredTools: mcp.registeredToolNames(),
|
|
60
|
+
/**
|
|
61
|
+
* The signer MCP's tool surface, DERIVED from the pinned @haven_ai/signer
|
|
62
|
+
* package (#1587) — same anti-drift rule as `requiredTools` above: a
|
|
63
|
+
* literal list here would rot the first time the signer gains a tool.
|
|
64
|
+
* The handshake probe requires all of them.
|
|
65
|
+
*/
|
|
66
|
+
requiredSignerTools: Object.keys(signer.toolSchemas)
|
|
67
|
+
};
|
|
291
68
|
}
|
|
292
|
-
};
|
|
69
|
+
});
|
|
293
70
|
async function writeRuntimeConfig(input, deps = {}) {
|
|
294
71
|
switch (input.runtime) {
|
|
295
72
|
case "codex-cli":
|
|
@@ -874,14 +651,33 @@ function hasBalancedTomlContainers(value) {
|
|
|
874
651
|
function tomlString(value) {
|
|
875
652
|
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
876
653
|
}
|
|
877
|
-
function
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
654
|
+
function runtimeConfigPathFor(runtime, homeDir = os.homedir()) {
|
|
655
|
+
switch (runtime) {
|
|
656
|
+
case "cursor":
|
|
657
|
+
return cursorConfigPath(homeDir);
|
|
658
|
+
case "codex-cli":
|
|
659
|
+
case "codex-desktop":
|
|
660
|
+
return codexConfigPath(homeDir);
|
|
661
|
+
case "vscode":
|
|
662
|
+
return vscodeConfigPath(homeDir);
|
|
663
|
+
case "vscode-insiders":
|
|
664
|
+
return vscodeInsidersConfigPath(homeDir);
|
|
665
|
+
case "claude-desktop":
|
|
666
|
+
return claudeDesktopConfigPath(homeDir);
|
|
667
|
+
case "hermes":
|
|
668
|
+
return hermesConfigPath(homeDir);
|
|
669
|
+
default:
|
|
670
|
+
return null;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
function cursorConfigPath(homeDir = os.homedir()) {
|
|
674
|
+
return path.resolve(homeDir, ".cursor", "mcp.json");
|
|
675
|
+
}
|
|
676
|
+
function codexConfigPath(homeDir = os.homedir()) {
|
|
677
|
+
return path.resolve(homeDir, ".codex", "config.toml");
|
|
678
|
+
}
|
|
679
|
+
function vscodeConfigPath(homeDir = os.homedir()) {
|
|
680
|
+
if (os.platform() === "darwin") return path.resolve(homeDir, "Library", "Application Support", "Code", "User", "mcp.json");
|
|
885
681
|
if (os.platform() === "win32") {
|
|
886
682
|
return path.resolve(process.env.APPDATA ?? path.join(homeDir, "AppData", "Roaming"), "Code", "User", "mcp.json");
|
|
887
683
|
}
|
|
@@ -930,82 +726,25 @@ function configTargetLabel(runtime) {
|
|
|
930
726
|
return "runtime MCP config";
|
|
931
727
|
}
|
|
932
728
|
}
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
hash: decision.hash,
|
|
944
|
-
reason: decision.reason
|
|
945
|
-
};
|
|
946
|
-
} catch (err) {
|
|
947
|
-
return {
|
|
948
|
-
acknowledged: false,
|
|
949
|
-
error: err instanceof Error ? err.message : String(err)
|
|
950
|
-
};
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
async function getLocalMcpConsentStatus(identityPath, signerPath) {
|
|
954
|
-
try {
|
|
955
|
-
const input = await buildLocalMcpConsentInput(identityPath, signerPath);
|
|
956
|
-
const hash = mcp.computeConsentHash(input);
|
|
957
|
-
const stored = await readLocalMcpAckFile(localMcpAckPath(identityPath));
|
|
958
|
-
if (stored === hash) {
|
|
959
|
-
return { acknowledged: true, hash, reason: "ack_file_match" };
|
|
960
|
-
}
|
|
961
|
-
return {
|
|
962
|
-
acknowledged: false,
|
|
963
|
-
hash,
|
|
964
|
-
reason: stored ? "ack_file_mismatch" : "ack_file_missing"
|
|
729
|
+
var HERMES_API_KEY_ENV, HermesConfigRecoveryError, InvalidCodexTomlError;
|
|
730
|
+
var init_config_writers = __esm({
|
|
731
|
+
"src/config-writers.ts"() {
|
|
732
|
+
init_runtime_manifest();
|
|
733
|
+
HERMES_API_KEY_ENV = "MCP_HAVEN_API_KEY";
|
|
734
|
+
HermesConfigRecoveryError = class extends Error {
|
|
735
|
+
constructor() {
|
|
736
|
+
super("Hermes configuration recovery did not complete");
|
|
737
|
+
this.name = "HermesConfigRecoveryError";
|
|
738
|
+
}
|
|
965
739
|
};
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
740
|
+
InvalidCodexTomlError = class extends Error {
|
|
741
|
+
constructor(message) {
|
|
742
|
+
super(message);
|
|
743
|
+
this.name = "InvalidCodexTomlError";
|
|
744
|
+
}
|
|
970
745
|
};
|
|
971
746
|
}
|
|
972
|
-
}
|
|
973
|
-
function localMcpAckPath(identityPath) {
|
|
974
|
-
return path.resolve(`${identityPath}.ack.json`);
|
|
975
|
-
}
|
|
976
|
-
async function buildLocalMcpConsentInput(identityPath, signerPath) {
|
|
977
|
-
const credentials = await mcp.loadCredentials({ identityPath, signerPath });
|
|
978
|
-
const unavailableDuringSetup = {
|
|
979
|
-
getAllowances: async () => {
|
|
980
|
-
throw new Error("Haven approval is not complete yet.");
|
|
981
|
-
}
|
|
982
|
-
};
|
|
983
|
-
return mcp.consentInputFromClient(
|
|
984
|
-
unavailableDuringSetup,
|
|
985
|
-
{
|
|
986
|
-
apiKey: credentials.apiKey,
|
|
987
|
-
apiUrl: credentials.apiUrl,
|
|
988
|
-
agentId: credentials.agentId,
|
|
989
|
-
safeAddress: credentials.safeAddress,
|
|
990
|
-
delegateAddress: credentials.delegateAddress,
|
|
991
|
-
chainId: credentials.chainId,
|
|
992
|
-
allowanceSummary: credentials.allowanceSummary
|
|
993
|
-
},
|
|
994
|
-
mcp.registeredToolNames()
|
|
995
|
-
);
|
|
996
|
-
}
|
|
997
|
-
async function readLocalMcpAckFile(path) {
|
|
998
|
-
try {
|
|
999
|
-
const parsed = JSON.parse(await promises.readFile(path, "utf8"));
|
|
1000
|
-
return typeof parsed.ack === "string" ? parsed.ack : null;
|
|
1001
|
-
} catch {
|
|
1002
|
-
return null;
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1005
|
-
function writeLogChunk(log, chunk) {
|
|
1006
|
-
const message = String(chunk).trimEnd();
|
|
1007
|
-
if (message) log(message);
|
|
1008
|
-
}
|
|
747
|
+
});
|
|
1009
748
|
async function probeHostedMcpTools(apiKey, hostedMcpUrl, fetchImpl = fetch) {
|
|
1010
749
|
let response;
|
|
1011
750
|
try {
|
|
@@ -1048,6 +787,8 @@ async function probeLocalMcpTools(command, args, requiredTools, timeoutMs = 1e4)
|
|
|
1048
787
|
let stdout = "";
|
|
1049
788
|
let settled = false;
|
|
1050
789
|
let sawInitialize = false;
|
|
790
|
+
let serverInfo;
|
|
791
|
+
let capabilities;
|
|
1051
792
|
const finish = (result) => {
|
|
1052
793
|
if (settled) return;
|
|
1053
794
|
settled = true;
|
|
@@ -1079,6 +820,9 @@ async function probeLocalMcpTools(command, args, requiredTools, timeoutMs = 1e4)
|
|
|
1079
820
|
}
|
|
1080
821
|
if (payload.id === 1 && !sawInitialize) {
|
|
1081
822
|
sawInitialize = true;
|
|
823
|
+
const init = payload.result;
|
|
824
|
+
serverInfo = init?.serverInfo;
|
|
825
|
+
capabilities = init?.capabilities;
|
|
1082
826
|
writeJsonRpc(child, { jsonrpc: "2.0", method: "notifications/initialized", params: {} });
|
|
1083
827
|
writeJsonRpc(child, { jsonrpc: "2.0", id: 2, method: "tools/list", params: {} });
|
|
1084
828
|
continue;
|
|
@@ -1087,7 +831,7 @@ async function probeLocalMcpTools(command, args, requiredTools, timeoutMs = 1e4)
|
|
|
1087
831
|
const tools = payload.result?.tools;
|
|
1088
832
|
const toolNames = Array.isArray(tools) ? tools.map((tool) => tool && typeof tool === "object" && "name" in tool ? tool.name : void 0).filter((name) => typeof name === "string") : [];
|
|
1089
833
|
const missing = requiredTools.filter((name) => !toolNames.includes(name));
|
|
1090
|
-
finish({ status: missing.length === 0 ? "ok" : "missing_tools", toolNames });
|
|
834
|
+
finish({ status: missing.length === 0 ? "ok" : "missing_tools", toolNames, serverInfo, capabilities });
|
|
1091
835
|
return;
|
|
1092
836
|
}
|
|
1093
837
|
}
|
|
@@ -1136,51 +880,37 @@ async function fetchWithTimeout(fetchImpl, url, init) {
|
|
|
1136
880
|
clearTimeout(timeout);
|
|
1137
881
|
}
|
|
1138
882
|
}
|
|
1139
|
-
var
|
|
1140
|
-
|
|
1141
|
-
code = "local_mcp_unsupported_node_version";
|
|
1142
|
-
nodeVersion;
|
|
1143
|
-
minimumNodeVersion;
|
|
1144
|
-
constructor(nodeVersion, minimumNodeVersion, subject = "Haven setup") {
|
|
1145
|
-
super(sdk.unsupportedNodeVersionMessage({ subject, nodeVersion, minimumNodeVersion }));
|
|
1146
|
-
this.name = "UnsupportedNodeVersionError";
|
|
1147
|
-
this.nodeVersion = nodeVersion;
|
|
1148
|
-
this.minimumNodeVersion = minimumNodeVersion;
|
|
883
|
+
var init_probes = __esm({
|
|
884
|
+
"src/probes.ts"() {
|
|
1149
885
|
}
|
|
1150
|
-
};
|
|
1151
|
-
async function
|
|
1152
|
-
assertSupportedNodeVersion(input.nodeVersion);
|
|
886
|
+
});
|
|
887
|
+
async function prepareSignerRuntime(input, deps = {}) {
|
|
1153
888
|
const homeDir = input.homeDir ?? os.homedir();
|
|
1154
|
-
const runtimeDirectory = path.resolve(homeDir, ".haven", "
|
|
889
|
+
const runtimeDirectory = path.resolve(homeDir, ".haven", "signer-runtime", MCP_RUNTIME_MANIFEST.signerVersion);
|
|
1155
890
|
const npmCacheDirectory = path.resolve(homeDir, ".haven", "npm-cache");
|
|
1156
|
-
const cliPath = path.join(runtimeDirectory, "node_modules", "@haven_ai", "
|
|
891
|
+
const cliPath = path.join(runtimeDirectory, "node_modules", "@haven_ai", "signer", "dist", "cli.js");
|
|
1157
892
|
const messages = [];
|
|
1158
893
|
await promises.mkdir(runtimeDirectory, { recursive: true, mode: 448 });
|
|
1159
894
|
await promises.chmod(runtimeDirectory, 448).catch(() => void 0);
|
|
1160
895
|
await promises.mkdir(npmCacheDirectory, { recursive: true, mode: 448 });
|
|
1161
896
|
await promises.chmod(npmCacheDirectory, 448).catch(() => void 0);
|
|
1162
897
|
if (await installedRuntimeMatches(runtimeDirectory, cliPath)) {
|
|
1163
|
-
messages.push(`Using existing local Haven
|
|
898
|
+
messages.push(`Using existing local Haven signer runtime ${signerPackageSpec()}.`);
|
|
1164
899
|
} else {
|
|
1165
|
-
await installRuntimePackages(runtimeDirectory, npmCacheDirectory, deps
|
|
1166
|
-
messages.push(`Installed local Haven
|
|
900
|
+
await installRuntimePackages(runtimeDirectory, npmCacheDirectory, deps);
|
|
901
|
+
messages.push(`Installed local Haven signer runtime ${signerPackageSpec()}.`);
|
|
1167
902
|
}
|
|
1168
|
-
await assertFileExists(cliPath, "local Haven
|
|
1169
|
-
const wrapperPath = path.join(input.credentialDirectory, "bin", "haven-
|
|
1170
|
-
await writeWrapper({
|
|
1171
|
-
wrapperPath,
|
|
1172
|
-
cliPath,
|
|
1173
|
-
identityPath: input.identityPath,
|
|
1174
|
-
signerPath: input.signerPath
|
|
1175
|
-
});
|
|
903
|
+
await assertFileExists(cliPath, "local Haven signer CLI");
|
|
904
|
+
const wrapperPath = path.join(input.credentialDirectory, "bin", "haven-signer.mjs");
|
|
905
|
+
await writeWrapper({ wrapperPath, cliPath, signerPath: input.signerPath });
|
|
1176
906
|
await writeRuntimeSidecar({
|
|
1177
|
-
path: path.join(input.credentialDirectory, "
|
|
907
|
+
path: path.join(input.credentialDirectory, "signer-runtime.json"),
|
|
1178
908
|
wrapperPath,
|
|
1179
909
|
runtimeDirectory,
|
|
1180
910
|
npmCacheDirectory,
|
|
1181
911
|
cliPath
|
|
1182
912
|
});
|
|
1183
|
-
messages.push(`Prepared stable local Haven
|
|
913
|
+
messages.push(`Prepared stable local Haven signer wrapper: ${wrapperPath}`);
|
|
1184
914
|
return {
|
|
1185
915
|
command: wrapperPath,
|
|
1186
916
|
args: [],
|
|
@@ -1191,12 +921,8 @@ async function prepareLocalMcpRuntime(input, deps = {}) {
|
|
|
1191
921
|
messages
|
|
1192
922
|
};
|
|
1193
923
|
}
|
|
1194
|
-
function
|
|
1195
|
-
|
|
1196
|
-
throw new UnsupportedNodeVersionError(nodeVersion, minimumNodeVersion, subject);
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
async function installRuntimePackages(runtimeDirectory, npmCacheDirectory, runCommand) {
|
|
924
|
+
async function installRuntimePackages(runtimeDirectory, npmCacheDirectory, deps) {
|
|
925
|
+
const { runCommand, onProgress } = deps;
|
|
1200
926
|
const baseArgs = [
|
|
1201
927
|
"install",
|
|
1202
928
|
"--prefix",
|
|
@@ -1205,12 +931,22 @@ async function installRuntimePackages(runtimeDirectory, npmCacheDirectory, runCo
|
|
|
1205
931
|
"--no-fund",
|
|
1206
932
|
"--omit=dev",
|
|
1207
933
|
"--prefer-offline",
|
|
1208
|
-
|
|
934
|
+
signerPackageSpec(),
|
|
1209
935
|
sdkPackageSpec()
|
|
1210
936
|
];
|
|
1211
937
|
const run = async (args) => {
|
|
1212
|
-
|
|
1213
|
-
|
|
938
|
+
const startedAt = Date.now();
|
|
939
|
+
const heartbeat = setInterval(() => {
|
|
940
|
+
const seconds = Math.round((Date.now() - startedAt) / 1e3);
|
|
941
|
+
onProgress?.(`Still installing the local Haven signer runtime\u2026 (${seconds}s \u2014 a cold cache can take several minutes)`);
|
|
942
|
+
}, SIGNER_INSTALL_HEARTBEAT_MS);
|
|
943
|
+
heartbeat.unref?.();
|
|
944
|
+
try {
|
|
945
|
+
if (runCommand) await runCommand("npm", args);
|
|
946
|
+
else await execFileAsync("npm", args, { timeout: SIGNER_INSTALL_TIMEOUT_MS, maxBuffer: 1024 * 1024 });
|
|
947
|
+
} finally {
|
|
948
|
+
clearInterval(heartbeat);
|
|
949
|
+
}
|
|
1214
950
|
};
|
|
1215
951
|
try {
|
|
1216
952
|
await run(baseArgs);
|
|
@@ -1218,18 +954,20 @@ async function installRuntimePackages(runtimeDirectory, npmCacheDirectory, runCo
|
|
|
1218
954
|
try {
|
|
1219
955
|
await run([...baseArgs, "--cache", npmCacheDirectory]);
|
|
1220
956
|
} catch (err) {
|
|
1221
|
-
throw new Error(
|
|
957
|
+
throw new Error(
|
|
958
|
+
`Could not install local Haven signer runtime ${signerPackageSpec()}: ${err instanceof Error ? err.message : String(err)}`
|
|
959
|
+
);
|
|
1222
960
|
}
|
|
1223
961
|
}
|
|
1224
962
|
}
|
|
1225
963
|
async function installedRuntimeMatches(runtimeDirectory, cliPath) {
|
|
1226
964
|
try {
|
|
1227
|
-
await assertFileExists(cliPath, "local Haven
|
|
1228
|
-
const [
|
|
1229
|
-
readPackageJson(path.join(runtimeDirectory, "node_modules", "@haven_ai", "
|
|
965
|
+
await assertFileExists(cliPath, "local Haven signer CLI");
|
|
966
|
+
const [signerPackage, sdkPackage] = await Promise.all([
|
|
967
|
+
readPackageJson(path.join(runtimeDirectory, "node_modules", "@haven_ai", "signer", "package.json")),
|
|
1230
968
|
readPackageJson(path.join(runtimeDirectory, "node_modules", "@haven_ai", "sdk", "package.json"))
|
|
1231
969
|
]);
|
|
1232
|
-
return
|
|
970
|
+
return signerPackage.version === MCP_RUNTIME_MANIFEST.signerVersion && sdkPackage.version === MCP_RUNTIME_MANIFEST.sdkVersion;
|
|
1233
971
|
} catch {
|
|
1234
972
|
return false;
|
|
1235
973
|
}
|
|
@@ -1245,10 +983,9 @@ async function writeWrapper(input) {
|
|
|
1245
983
|
"import { spawn } from 'node:child_process'",
|
|
1246
984
|
"",
|
|
1247
985
|
`const cliPath = ${JSON.stringify(input.cliPath)}`,
|
|
1248
|
-
`const identityPath = ${JSON.stringify(input.identityPath)}`,
|
|
1249
986
|
`const signerPath = ${JSON.stringify(input.signerPath)}`,
|
|
1250
987
|
"",
|
|
1251
|
-
"const child = spawn(process.execPath, [cliPath, '--
|
|
988
|
+
"const child = spawn(process.execPath, [cliPath, '--credentials', signerPath, ...process.argv.slice(2)], {",
|
|
1252
989
|
" stdio: 'inherit',",
|
|
1253
990
|
"})",
|
|
1254
991
|
"",
|
|
@@ -1261,57 +998,889 @@ async function writeWrapper(input) {
|
|
|
1261
998
|
await promises.writeFile(input.wrapperPath, source, { mode: 448 });
|
|
1262
999
|
await promises.chmod(input.wrapperPath, 448).catch(() => void 0);
|
|
1263
1000
|
}
|
|
1264
|
-
async function
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1001
|
+
async function readRuntimeSidecar(credentialDirectory) {
|
|
1002
|
+
try {
|
|
1003
|
+
return JSON.parse(
|
|
1004
|
+
await promises.readFile(path.join(credentialDirectory, "signer-runtime.json"), "utf8")
|
|
1005
|
+
);
|
|
1006
|
+
} catch {
|
|
1007
|
+
return null;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
async function writeRuntimeSidecar(input) {
|
|
1011
|
+
const value = {
|
|
1012
|
+
signer_package: MCP_RUNTIME_MANIFEST.signerPackage,
|
|
1013
|
+
signer_version: MCP_RUNTIME_MANIFEST.signerVersion,
|
|
1014
|
+
sdk_package: MCP_RUNTIME_MANIFEST.sdkPackage,
|
|
1015
|
+
sdk_version: MCP_RUNTIME_MANIFEST.sdkVersion,
|
|
1016
|
+
wrapper_path: input.wrapperPath,
|
|
1017
|
+
runtime_directory: input.runtimeDirectory,
|
|
1018
|
+
npm_cache_directory: input.npmCacheDirectory,
|
|
1019
|
+
cli_path: input.cliPath
|
|
1020
|
+
};
|
|
1021
|
+
await promises.writeFile(input.path, `${JSON.stringify(value, null, 2)}
|
|
1022
|
+
`, { mode: 384 });
|
|
1023
|
+
await promises.chmod(input.path, 384).catch(() => void 0);
|
|
1024
|
+
}
|
|
1025
|
+
async function assertFileExists(path, label) {
|
|
1026
|
+
try {
|
|
1027
|
+
await promises.access(path);
|
|
1028
|
+
} catch {
|
|
1029
|
+
throw new Error(`Missing ${label}: ${path}`);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
var execFileAsync, SIGNER_INSTALL_TIMEOUT_MS, SIGNER_INSTALL_HEARTBEAT_MS;
|
|
1033
|
+
var init_signer_runtime = __esm({
|
|
1034
|
+
"src/signer-runtime.ts"() {
|
|
1035
|
+
init_runtime_manifest();
|
|
1036
|
+
execFileAsync = util.promisify(child_process.execFile);
|
|
1037
|
+
SIGNER_INSTALL_TIMEOUT_MS = 6e5;
|
|
1038
|
+
SIGNER_INSTALL_HEARTBEAT_MS = 15e3;
|
|
1039
|
+
}
|
|
1040
|
+
});
|
|
1041
|
+
|
|
1042
|
+
// src/runtime-registry.ts
|
|
1043
|
+
function runtimeProfile(runtime, env = process.env) {
|
|
1044
|
+
return RUNTIME_PROFILES[normalizeRuntime(runtime, env)];
|
|
1045
|
+
}
|
|
1046
|
+
function normalizeRuntime(runtime, env = process.env) {
|
|
1047
|
+
const explicit = normalizeRuntimeName(runtime);
|
|
1048
|
+
if (explicit) return explicit;
|
|
1049
|
+
return detectRuntime(env) ?? "other";
|
|
1050
|
+
}
|
|
1051
|
+
function restartRequiredForRuntime(runtime, env = process.env) {
|
|
1052
|
+
const mode = runtimeProfile(runtime, env).restartMode;
|
|
1053
|
+
return mode === "restart-session" || mode === "restart-app";
|
|
1054
|
+
}
|
|
1055
|
+
function runtimeVerificationInstruction(runtime) {
|
|
1056
|
+
const label = RUNTIME_PROFILES[runtime].label;
|
|
1057
|
+
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.`;
|
|
1058
|
+
}
|
|
1059
|
+
function normalizeRuntimeName(runtime) {
|
|
1060
|
+
const key = runtime?.trim().toLowerCase();
|
|
1061
|
+
if (!key) return null;
|
|
1062
|
+
return RUNTIME_ALIASES[key.replace(/\s+/g, "-")] ?? null;
|
|
1063
|
+
}
|
|
1064
|
+
function detectRuntime(env) {
|
|
1065
|
+
if (env.CLAUDECODE || env.CLAUDE_CODE || env.CLAUDECODE_CWD) return "claude-code";
|
|
1066
|
+
if (env.CODEX_SANDBOX || env.CODEX_HOME || env.CODEX_CWD) return "codex-cli";
|
|
1067
|
+
if (env.VSCODE_CWD || env.VSCODE_IPC_HOOK_CLI || env.TERM_PROGRAM === "vscode") return "vscode";
|
|
1068
|
+
if (env.HERMES_HOME || env.HERMES_AGENT) return "hermes";
|
|
1069
|
+
return null;
|
|
1070
|
+
}
|
|
1071
|
+
var RUNTIME_PROFILES, RUNTIME_ALIASES;
|
|
1072
|
+
var init_runtime_registry = __esm({
|
|
1073
|
+
"src/runtime-registry.ts"() {
|
|
1074
|
+
RUNTIME_PROFILES = {
|
|
1075
|
+
"claude-code": {
|
|
1076
|
+
id: "claude-code",
|
|
1077
|
+
label: "Claude Code",
|
|
1078
|
+
restartMode: "restart-session",
|
|
1079
|
+
canWriteRuntimeConfig: true,
|
|
1080
|
+
activationInstruction: "Start a new Claude Code session so it loads the Haven MCP entries."
|
|
1081
|
+
},
|
|
1082
|
+
"codex-cli": {
|
|
1083
|
+
id: "codex-cli",
|
|
1084
|
+
label: "Codex CLI",
|
|
1085
|
+
restartMode: "restart-session",
|
|
1086
|
+
canWriteRuntimeConfig: true,
|
|
1087
|
+
activationInstruction: "Start a fresh Codex CLI session (for example, run `codex resume --last`)."
|
|
1088
|
+
},
|
|
1089
|
+
"codex-desktop": {
|
|
1090
|
+
id: "codex-desktop",
|
|
1091
|
+
label: "Codex Desktop",
|
|
1092
|
+
restartMode: "restart-session",
|
|
1093
|
+
canWriteRuntimeConfig: true,
|
|
1094
|
+
activationInstruction: "Quit and reopen Codex Desktop so it loads the Haven MCP entries."
|
|
1095
|
+
},
|
|
1096
|
+
cursor: {
|
|
1097
|
+
id: "cursor",
|
|
1098
|
+
label: "Cursor",
|
|
1099
|
+
restartMode: "hot-reload",
|
|
1100
|
+
canWriteRuntimeConfig: true,
|
|
1101
|
+
activationInstruction: "Wait for Cursor to hot-reload the Haven MCP entries; no app restart is required."
|
|
1102
|
+
},
|
|
1103
|
+
vscode: {
|
|
1104
|
+
id: "vscode",
|
|
1105
|
+
label: "VS Code",
|
|
1106
|
+
restartMode: "hot-reload",
|
|
1107
|
+
canWriteRuntimeConfig: true,
|
|
1108
|
+
activationInstruction: "Wait for VS Code to hot-reload the Haven MCP entries; no app restart is required."
|
|
1109
|
+
},
|
|
1110
|
+
"vscode-insiders": {
|
|
1111
|
+
id: "vscode-insiders",
|
|
1112
|
+
label: "VS Code Insiders",
|
|
1113
|
+
restartMode: "hot-reload",
|
|
1114
|
+
canWriteRuntimeConfig: true,
|
|
1115
|
+
activationInstruction: "Wait for VS Code Insiders to hot-reload the Haven MCP entries; no app restart is required."
|
|
1116
|
+
},
|
|
1117
|
+
"claude-desktop": {
|
|
1118
|
+
id: "claude-desktop",
|
|
1119
|
+
label: "Claude Desktop",
|
|
1120
|
+
restartMode: "restart-app",
|
|
1121
|
+
canWriteRuntimeConfig: true,
|
|
1122
|
+
activationInstruction: "Quit and reopen Claude Desktop so it loads the Haven MCP entries."
|
|
1123
|
+
},
|
|
1124
|
+
hermes: {
|
|
1125
|
+
id: "hermes",
|
|
1126
|
+
label: "Hermes Agent",
|
|
1127
|
+
restartMode: "restart-session",
|
|
1128
|
+
canWriteRuntimeConfig: true,
|
|
1129
|
+
activationInstruction: "Start a new Hermes session; in Hermes Gateway, run `/restart` instead."
|
|
1130
|
+
},
|
|
1131
|
+
other: {
|
|
1132
|
+
id: "other",
|
|
1133
|
+
label: "Other agent runtime",
|
|
1134
|
+
restartMode: "manual",
|
|
1135
|
+
canWriteRuntimeConfig: false,
|
|
1136
|
+
activationInstruction: "Finish the manual MCP setup shown above, then start a fresh session in that runtime."
|
|
1137
|
+
}
|
|
1138
|
+
};
|
|
1139
|
+
RUNTIME_ALIASES = {
|
|
1140
|
+
claude: "claude-code",
|
|
1141
|
+
"claude-code": "claude-code",
|
|
1142
|
+
claudecode: "claude-code",
|
|
1143
|
+
"claude_code": "claude-code",
|
|
1144
|
+
codex: "codex-cli",
|
|
1145
|
+
"codex-cli": "codex-cli",
|
|
1146
|
+
codexcli: "codex-cli",
|
|
1147
|
+
"codex_cli": "codex-cli",
|
|
1148
|
+
"codex-desktop": "codex-desktop",
|
|
1149
|
+
"codex_desktop": "codex-desktop",
|
|
1150
|
+
codexdesktop: "codex-desktop",
|
|
1151
|
+
"codex-app": "codex-desktop",
|
|
1152
|
+
"codex_app": "codex-desktop",
|
|
1153
|
+
codexapp: "codex-desktop",
|
|
1154
|
+
cursor: "cursor",
|
|
1155
|
+
vscode: "vscode",
|
|
1156
|
+
"vs-code": "vscode",
|
|
1157
|
+
"vs_code": "vscode",
|
|
1158
|
+
code: "vscode",
|
|
1159
|
+
"vscode-insiders": "vscode-insiders",
|
|
1160
|
+
"vscode_insiders": "vscode-insiders",
|
|
1161
|
+
vscodeinsiders: "vscode-insiders",
|
|
1162
|
+
"vs-code-insiders": "vscode-insiders",
|
|
1163
|
+
"code-insiders": "vscode-insiders",
|
|
1164
|
+
insiders: "vscode-insiders",
|
|
1165
|
+
"claude-desktop": "claude-desktop",
|
|
1166
|
+
"claude_desktop": "claude-desktop",
|
|
1167
|
+
claudesktop: "claude-desktop",
|
|
1168
|
+
desktop: "claude-desktop",
|
|
1169
|
+
hermes: "hermes",
|
|
1170
|
+
"hermes-agent": "hermes",
|
|
1171
|
+
hermes_agent: "hermes",
|
|
1172
|
+
hermesagent: "hermes",
|
|
1173
|
+
other: "other",
|
|
1174
|
+
manual: "other"
|
|
1175
|
+
};
|
|
1176
|
+
}
|
|
1177
|
+
});
|
|
1178
|
+
async function acknowledgeLocalSignerConsent(signerPath, log) {
|
|
1179
|
+
try {
|
|
1180
|
+
const input = await buildSignerConsentInput(signerPath);
|
|
1181
|
+
const decision = await signer.ensureSignerConsent(input, {
|
|
1182
|
+
credentialsPath: signerPath,
|
|
1183
|
+
writeAck: true,
|
|
1184
|
+
out: log ? { write: (chunk) => writeLogChunk2(log, chunk) } : void 0
|
|
1185
|
+
});
|
|
1186
|
+
return {
|
|
1187
|
+
acknowledged: decision.ok,
|
|
1188
|
+
hash: decision.hash,
|
|
1189
|
+
reason: decision.reason
|
|
1190
|
+
};
|
|
1191
|
+
} catch (err) {
|
|
1192
|
+
return {
|
|
1193
|
+
acknowledged: false,
|
|
1194
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
async function getLocalSignerConsentStatus(signerPath) {
|
|
1199
|
+
try {
|
|
1200
|
+
const input = await buildSignerConsentInput(signerPath);
|
|
1201
|
+
const hash = signer.computeSignerConsentHash(input);
|
|
1202
|
+
const stored = await readSignerAckFile(signerAckPath(signerPath));
|
|
1203
|
+
if (stored === hash) {
|
|
1204
|
+
return { acknowledged: true, hash, reason: "ack_file_match" };
|
|
1205
|
+
}
|
|
1206
|
+
return {
|
|
1207
|
+
acknowledged: false,
|
|
1208
|
+
hash,
|
|
1209
|
+
reason: stored ? "ack_file_mismatch" : "ack_file_missing"
|
|
1210
|
+
};
|
|
1211
|
+
} catch (err) {
|
|
1212
|
+
return {
|
|
1213
|
+
acknowledged: false,
|
|
1214
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
function signerAckPath(signerPath) {
|
|
1219
|
+
return path.resolve(`${signerPath}.signer-ack.json`);
|
|
1220
|
+
}
|
|
1221
|
+
async function buildSignerConsentInput(signerPath) {
|
|
1222
|
+
const credentials = await signer.loadSignerCredentials(signerPath);
|
|
1223
|
+
const signer$1 = signer.createEdgeSigner(credentials.delegateKey, {
|
|
1224
|
+
x402BindingSigner: credentials.x402BindingSigner
|
|
1225
|
+
});
|
|
1226
|
+
return {
|
|
1227
|
+
delegateAddress: signer$1.delegateAddress,
|
|
1228
|
+
safeAddress: credentials.safeAddress,
|
|
1229
|
+
agentId: credentials.agentId,
|
|
1230
|
+
chainId: credentials.chainId,
|
|
1231
|
+
network: credentials.network,
|
|
1232
|
+
toolNames: Object.keys(signer.toolSchemas)
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
async function readSignerAckFile(path) {
|
|
1236
|
+
try {
|
|
1237
|
+
const parsed = JSON.parse(await promises.readFile(path, "utf8"));
|
|
1238
|
+
return typeof parsed.ack === "string" ? parsed.ack : null;
|
|
1239
|
+
} catch {
|
|
1240
|
+
return null;
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
function writeLogChunk2(log, chunk) {
|
|
1244
|
+
const message = String(chunk).trimEnd();
|
|
1245
|
+
if (message) log(message);
|
|
1246
|
+
}
|
|
1247
|
+
var init_signer_consent = __esm({
|
|
1248
|
+
"src/signer-consent.ts"() {
|
|
1249
|
+
}
|
|
1250
|
+
});
|
|
1251
|
+
|
|
1252
|
+
// src/doctor.ts
|
|
1253
|
+
var doctor_exports = {};
|
|
1254
|
+
__export(doctor_exports, {
|
|
1255
|
+
runDoctor: () => runDoctor,
|
|
1256
|
+
runRepair: () => runRepair
|
|
1257
|
+
});
|
|
1258
|
+
async function discoverCredentialDirectory(homeDir, explicit) {
|
|
1259
|
+
if (explicit) return { directory: explicit };
|
|
1260
|
+
const root = path.join(homeDir, ".haven", "agents");
|
|
1261
|
+
let entries = [];
|
|
1262
|
+
try {
|
|
1263
|
+
entries = await promises.readdir(root);
|
|
1264
|
+
} catch {
|
|
1265
|
+
return {};
|
|
1266
|
+
}
|
|
1267
|
+
const candidates = [];
|
|
1268
|
+
for (const entry of entries) {
|
|
1269
|
+
const directory = path.join(root, entry);
|
|
1270
|
+
try {
|
|
1271
|
+
const s = await promises.stat(path.join(directory, "identity.json"));
|
|
1272
|
+
candidates.push({ directory, mtimeMs: s.mtimeMs });
|
|
1273
|
+
} catch {
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
if (candidates.length === 0) return {};
|
|
1277
|
+
candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1278
|
+
return {
|
|
1279
|
+
directory: candidates[0].directory,
|
|
1280
|
+
note: candidates.length > 1 ? `${candidates.length} agent credential dirs found; examining the newest.` : void 0
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
1283
|
+
async function runDoctor(input, deps = {}) {
|
|
1284
|
+
const homeDir = deps.homeDir ?? os.homedir();
|
|
1285
|
+
const checks = [];
|
|
1286
|
+
let signerCapabilities;
|
|
1287
|
+
const { directory, note } = await discoverCredentialDirectory(homeDir, input.credentialsDir);
|
|
1288
|
+
let identity;
|
|
1289
|
+
let signerParses = false;
|
|
1290
|
+
if (!directory) {
|
|
1291
|
+
checks.push({
|
|
1292
|
+
id: "credentials",
|
|
1293
|
+
label: "Agent credentials",
|
|
1294
|
+
ok: false,
|
|
1295
|
+
detail: "No agent credential directory with an identity.json under ~/.haven/agents.",
|
|
1296
|
+
repair: `Run the full setup once: ${RERUN} --setup <token from the Haven dashboard>.`
|
|
1297
|
+
});
|
|
1298
|
+
} else {
|
|
1299
|
+
try {
|
|
1300
|
+
identity = JSON.parse(await promises.readFile(path.join(directory, "identity.json"), "utf8"));
|
|
1301
|
+
} catch {
|
|
1302
|
+
identity = void 0;
|
|
1303
|
+
}
|
|
1304
|
+
try {
|
|
1305
|
+
const signer = JSON.parse(await promises.readFile(path.join(directory, "signer.json"), "utf8"));
|
|
1306
|
+
signerParses = typeof signer === "object" && signer !== null;
|
|
1307
|
+
} catch {
|
|
1308
|
+
signerParses = false;
|
|
1309
|
+
}
|
|
1310
|
+
const ok = Boolean(identity?.api_key) && signerParses;
|
|
1311
|
+
checks.push({
|
|
1312
|
+
id: "credentials",
|
|
1313
|
+
label: "Agent credentials",
|
|
1314
|
+
ok,
|
|
1315
|
+
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.",
|
|
1316
|
+
...ok ? {} : { repair: `Re-run the full setup with a fresh token: ${RERUN} --setup <token>.` }
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
let sidecar = null;
|
|
1320
|
+
if (directory) {
|
|
1321
|
+
sidecar = await readRuntimeSidecar(directory);
|
|
1322
|
+
if (!sidecar) {
|
|
1323
|
+
checks.push({
|
|
1324
|
+
id: "signer_runtime",
|
|
1325
|
+
label: "Signer runtime (preinstalled wrapper)",
|
|
1326
|
+
ok: false,
|
|
1327
|
+
detail: "No signer-runtime.json sidecar \u2014 the pinned signer runtime was never prepared (or a pre-#1586 npx config).",
|
|
1328
|
+
repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}`
|
|
1329
|
+
});
|
|
1330
|
+
} else {
|
|
1331
|
+
const matches = await installedRuntimeMatches(sidecar.runtime_directory, sidecar.cli_path);
|
|
1332
|
+
const versionOk = sidecar.signer_version === MCP_RUNTIME_MANIFEST.signerVersion;
|
|
1333
|
+
const ok = matches && versionOk;
|
|
1334
|
+
checks.push({
|
|
1335
|
+
id: "signer_runtime",
|
|
1336
|
+
label: "Signer runtime (preinstalled wrapper)",
|
|
1337
|
+
ok,
|
|
1338
|
+
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.`,
|
|
1339
|
+
...ok ? {} : { repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}` }
|
|
1340
|
+
});
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
const configPath = runtimeConfigPathFor(input.runtime, homeDir);
|
|
1344
|
+
if (configPath === null) {
|
|
1345
|
+
checks.push({
|
|
1346
|
+
id: "runtime_config",
|
|
1347
|
+
label: "Runtime MCP config",
|
|
1348
|
+
ok: true,
|
|
1349
|
+
detail: `Runtime '${input.runtime}' has no file-based config the connector owns (CLI-managed) \u2014 skipping the file check.`
|
|
1350
|
+
});
|
|
1351
|
+
} else {
|
|
1352
|
+
let configText = null;
|
|
1353
|
+
try {
|
|
1354
|
+
configText = await promises.readFile(configPath, "utf8");
|
|
1355
|
+
} catch {
|
|
1356
|
+
configText = null;
|
|
1357
|
+
}
|
|
1358
|
+
if (configText === null) {
|
|
1359
|
+
checks.push({
|
|
1360
|
+
id: "runtime_config",
|
|
1361
|
+
label: "Runtime MCP config",
|
|
1362
|
+
ok: false,
|
|
1363
|
+
detail: `No runtime config at ${configPath}.`,
|
|
1364
|
+
repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}`
|
|
1365
|
+
});
|
|
1366
|
+
} else {
|
|
1367
|
+
const hasHaven = identity?.hosted_mcp_url ? configText.includes(identity.hosted_mcp_url) : configText.includes("haven");
|
|
1368
|
+
const signerViaNpx = configText.includes("@haven_ai/signer");
|
|
1369
|
+
const wrapperReferenced = sidecar ? configText.includes(sidecar.wrapper_path) : false;
|
|
1370
|
+
const ok = hasHaven && !signerViaNpx && (sidecar ? wrapperReferenced : true);
|
|
1371
|
+
checks.push({
|
|
1372
|
+
id: "runtime_config",
|
|
1373
|
+
label: "Runtime MCP config",
|
|
1374
|
+
ok,
|
|
1375
|
+
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)" : ""}.`,
|
|
1376
|
+
...ok ? {} : { repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}` }
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
if (identity?.api_key && (identity.hosted_mcp_url || identity.api_url)) {
|
|
1381
|
+
const hostedUrl = identity.hosted_mcp_url ?? `${identity.api_url}/mcp`;
|
|
1382
|
+
const probe = await (deps.probeHosted ?? probeHostedMcpTools)(identity.api_key, hostedUrl, deps.fetch);
|
|
1383
|
+
checks.push({
|
|
1384
|
+
id: "hosted_mcp",
|
|
1385
|
+
label: "Hosted Haven MCP",
|
|
1386
|
+
ok: probe.status === "ok",
|
|
1387
|
+
detail: probe.status === "ok" ? `Reachable and authorized (${hostedUrl}).` : `Probe failed: ${probe.status} (${hostedUrl}).`,
|
|
1388
|
+
...probe.status === "ok" ? {} : {
|
|
1389
|
+
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."
|
|
1390
|
+
}
|
|
1391
|
+
});
|
|
1392
|
+
} else {
|
|
1393
|
+
checks.push({
|
|
1394
|
+
id: "hosted_mcp",
|
|
1395
|
+
label: "Hosted Haven MCP",
|
|
1396
|
+
ok: false,
|
|
1397
|
+
detail: "No stored API key / hosted MCP URL to probe with.",
|
|
1398
|
+
repair: `Re-run the full setup: ${RERUN} --setup <token>.`
|
|
1399
|
+
});
|
|
1400
|
+
}
|
|
1401
|
+
if (sidecar && directory) {
|
|
1402
|
+
const consent = await getLocalSignerConsentStatus(path.join(directory, "signer.json"));
|
|
1403
|
+
if (!consent.acknowledged) {
|
|
1404
|
+
checks.push({
|
|
1405
|
+
id: "signer_process",
|
|
1406
|
+
label: "Signer stdio handshake",
|
|
1407
|
+
ok: false,
|
|
1408
|
+
detail: "The local-tools consent is not acknowledged, so the signer refuses to start (by design).",
|
|
1409
|
+
repair: `Run: ${RERUN} --ack-local-tools --setup <token> (or re-run your original setup command with --ack-local-tools).`
|
|
1410
|
+
});
|
|
1411
|
+
} else {
|
|
1412
|
+
const probe = await (deps.probeSignerTools ?? probeLocalMcpTools)(
|
|
1413
|
+
sidecar.wrapper_path,
|
|
1414
|
+
[],
|
|
1415
|
+
MCP_RUNTIME_MANIFEST.requiredSignerTools
|
|
1416
|
+
);
|
|
1417
|
+
const experimental = probe.capabilities?.experimental ?? probe.capabilities;
|
|
1418
|
+
const compat = experimental?.["haven/signer-compatibility"];
|
|
1419
|
+
signerCapabilities = compat ? { "haven/signer-compatibility": compat } : void 0;
|
|
1420
|
+
const compatDetail = compat ? ` Compat: x402 expected-context v${JSON.stringify(compat.x402_expected_context_versions ?? "?")}.` : "";
|
|
1421
|
+
checks.push({
|
|
1422
|
+
id: "signer_process",
|
|
1423
|
+
label: "Signer stdio handshake",
|
|
1424
|
+
ok: probe.status === "ok",
|
|
1425
|
+
detail: probe.status === "ok" ? `Signer started, listed ${probe.toolNames?.length ?? 0} tools${probe.serverInfo?.version ? ` (v${probe.serverInfo.version})` : ""}.${compatDetail}` : `Handshake failed: ${probe.status}.`,
|
|
1426
|
+
...probe.status === "ok" ? {} : { repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}` }
|
|
1427
|
+
});
|
|
1428
|
+
}
|
|
1429
|
+
} else if (directory) {
|
|
1430
|
+
checks.push({
|
|
1431
|
+
id: "signer_process",
|
|
1432
|
+
label: "Signer stdio handshake",
|
|
1433
|
+
ok: false,
|
|
1434
|
+
detail: "Skipped \u2014 no prepared signer runtime to probe.",
|
|
1435
|
+
repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}`
|
|
1436
|
+
});
|
|
1437
|
+
}
|
|
1438
|
+
const restart = restartRequiredForRuntime(input.runtime, deps.env);
|
|
1439
|
+
checks.push({
|
|
1440
|
+
id: "restart",
|
|
1441
|
+
label: "Runtime restart",
|
|
1442
|
+
ok: true,
|
|
1443
|
+
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."
|
|
1444
|
+
});
|
|
1445
|
+
return {
|
|
1446
|
+
version: 1,
|
|
1447
|
+
ok: checks.every((check) => check.ok),
|
|
1448
|
+
runtime: input.runtime,
|
|
1449
|
+
credentialDirectory: directory,
|
|
1450
|
+
checks,
|
|
1451
|
+
...signerCapabilities ? { signerCapabilities } : {}
|
|
1452
|
+
};
|
|
1453
|
+
}
|
|
1454
|
+
async function runRepair(input, deps = {}) {
|
|
1455
|
+
const homeDir = deps.homeDir ?? os.homedir();
|
|
1456
|
+
const messages = [];
|
|
1457
|
+
const { directory, note } = await discoverCredentialDirectory(homeDir, input.credentialsDir);
|
|
1458
|
+
if (note) messages.push(`Note: ${note}`);
|
|
1459
|
+
if (!directory) {
|
|
1460
|
+
return {
|
|
1461
|
+
ok: false,
|
|
1462
|
+
messages: [`No agent credentials found to repair \u2014 run the full setup: ${RERUN} --setup <token>.`]
|
|
1463
|
+
};
|
|
1464
|
+
}
|
|
1465
|
+
let identity;
|
|
1466
|
+
try {
|
|
1467
|
+
identity = JSON.parse(await promises.readFile(path.join(directory, "identity.json"), "utf8"));
|
|
1468
|
+
} catch {
|
|
1469
|
+
return { ok: false, messages: ["identity.json is unreadable \u2014 re-run the full setup with a fresh token."] };
|
|
1470
|
+
}
|
|
1471
|
+
if (!identity.api_key || !(identity.hosted_mcp_url || identity.api_url)) {
|
|
1472
|
+
return { ok: false, messages: ["identity.json lacks the stored API key / hosted URL \u2014 re-run the full setup."] };
|
|
1473
|
+
}
|
|
1474
|
+
const configPath = runtimeConfigPathFor(input.runtime, homeDir);
|
|
1475
|
+
if (configPath) {
|
|
1476
|
+
try {
|
|
1477
|
+
const existing = await promises.readFile(configPath, "utf8");
|
|
1478
|
+
if (existing.includes("bin/haven-mcp") || existing.includes(".haven/mcp-runtime")) {
|
|
1479
|
+
return {
|
|
1480
|
+
ok: false,
|
|
1481
|
+
messages: [
|
|
1482
|
+
`The config at ${configPath} is the LOCAL-stdio topology (--local). Repair currently rewrites only the hosted+signer shape and will not touch it.`,
|
|
1483
|
+
"Re-run your original setup command (with --local) to repair a local-stdio install."
|
|
1484
|
+
]
|
|
1485
|
+
};
|
|
1486
|
+
}
|
|
1487
|
+
} catch {
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
const signerPath = path.join(directory, "signer.json");
|
|
1491
|
+
const prepared = await prepareSignerRuntime(
|
|
1492
|
+
{ credentialDirectory: directory, signerPath, homeDir },
|
|
1493
|
+
{ runCommand: deps.runCommand }
|
|
1494
|
+
);
|
|
1495
|
+
messages.push(...prepared.messages);
|
|
1496
|
+
const configResult = await writeRuntimeConfig({
|
|
1497
|
+
runtime: input.runtime,
|
|
1498
|
+
hostedMcpUrl: identity.hosted_mcp_url ?? `${identity.api_url}/mcp`,
|
|
1499
|
+
apiKey: identity.api_key,
|
|
1500
|
+
identityPath: path.join(directory, "identity.json"),
|
|
1501
|
+
signerPath,
|
|
1502
|
+
credentialDirectory: directory,
|
|
1503
|
+
signerCommand: { command: prepared.command, args: prepared.args },
|
|
1504
|
+
homeDir,
|
|
1505
|
+
mode: "hosted"
|
|
1506
|
+
});
|
|
1507
|
+
messages.push(...configResult.messages);
|
|
1508
|
+
messages.push("Repair complete \u2014 restart the runtime, then verify with --doctor.");
|
|
1509
|
+
return { ok: true, messages };
|
|
1510
|
+
}
|
|
1511
|
+
var RERUN;
|
|
1512
|
+
var init_doctor = __esm({
|
|
1513
|
+
"src/doctor.ts"() {
|
|
1514
|
+
init_runtime_manifest();
|
|
1515
|
+
init_probes();
|
|
1516
|
+
init_signer_runtime();
|
|
1517
|
+
init_config_writers();
|
|
1518
|
+
init_runtime_registry();
|
|
1519
|
+
init_signer_consent();
|
|
1520
|
+
RERUN = "npx @haven_ai/connect@alpha";
|
|
1521
|
+
}
|
|
1522
|
+
});
|
|
1523
|
+
|
|
1524
|
+
// src/api.ts
|
|
1525
|
+
function createConnectApiClient(baseUrl, fetchImpl = fetch) {
|
|
1526
|
+
const root = baseUrl.replace(/\/+$/, "");
|
|
1527
|
+
return {
|
|
1528
|
+
resolveSetup: (input) => request(fetchImpl, `${root}/agent-connection-setups/resolve`, {
|
|
1529
|
+
method: "POST",
|
|
1530
|
+
body: JSON.stringify({
|
|
1531
|
+
setup_token: input.setupToken,
|
|
1532
|
+
connector_version: input.connectorVersion,
|
|
1533
|
+
runtime: input.runtime
|
|
1534
|
+
})
|
|
1535
|
+
}),
|
|
1536
|
+
registerSetup: (input) => request(fetchImpl, `${root}/agent-connection-setups/register`, {
|
|
1537
|
+
method: "POST",
|
|
1538
|
+
body: JSON.stringify({
|
|
1539
|
+
setup_token: input.setupToken,
|
|
1540
|
+
challenge_id: input.challengeId,
|
|
1541
|
+
delegate_address: input.delegateAddress,
|
|
1542
|
+
proof_signature: input.proofSignature,
|
|
1543
|
+
api_key_hash: input.apiKeyHash,
|
|
1544
|
+
api_key_prefix: input.apiKeyPrefix,
|
|
1545
|
+
runtime: input.runtime,
|
|
1546
|
+
connector_version: input.connectorVersion,
|
|
1547
|
+
connector_context: input.connectorContext,
|
|
1548
|
+
install_capabilities: input.installCapabilities && {
|
|
1549
|
+
can_write_runtime_config: input.installCapabilities.canWriteRuntimeConfig,
|
|
1550
|
+
restart_required: input.installCapabilities.restartRequired
|
|
1551
|
+
}
|
|
1552
|
+
})
|
|
1553
|
+
}),
|
|
1554
|
+
getConnectorStatus: (setupId, apiKey) => request(fetchImpl, `${root}/agent-connection-setups/${encodeURIComponent(setupId)}/connector-status`, {
|
|
1555
|
+
method: "GET",
|
|
1556
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
1557
|
+
}),
|
|
1558
|
+
updateInstallStatus: async (setupId, apiKey, input) => {
|
|
1559
|
+
await request(fetchImpl, `${root}/agent-connection-setups/${encodeURIComponent(setupId)}/install-status`, {
|
|
1560
|
+
method: "POST",
|
|
1561
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
1562
|
+
body: JSON.stringify({
|
|
1563
|
+
runtime: input.runtime,
|
|
1564
|
+
connector_version: input.connectorVersion,
|
|
1565
|
+
runtime_mcp_mode: input.runtimeMcpMode,
|
|
1566
|
+
hosted_mcp_configured: input.hostedMcpConfigured,
|
|
1567
|
+
local_signer_configured: input.localSignerConfigured,
|
|
1568
|
+
local_mcp_configured: input.localMcpConfigured,
|
|
1569
|
+
credential_files_written: input.credentialFilesWritten,
|
|
1570
|
+
signer_acknowledged: input.signerAcknowledged,
|
|
1571
|
+
local_mcp_acknowledged: input.localMcpAcknowledged,
|
|
1572
|
+
activation_command_available: input.activationCommandAvailable,
|
|
1573
|
+
skill_installed: input.skillInstalled,
|
|
1574
|
+
probe_result: input.probeResult,
|
|
1575
|
+
restart_required: input.restartRequired,
|
|
1576
|
+
next_user_action: input.nextUserAction,
|
|
1577
|
+
error_code: input.errorCode ?? null,
|
|
1578
|
+
environment_label: input.environmentLabel
|
|
1579
|
+
})
|
|
1580
|
+
});
|
|
1581
|
+
}
|
|
1582
|
+
};
|
|
1583
|
+
}
|
|
1584
|
+
var ConnectRequestError = class extends Error {
|
|
1585
|
+
constructor(message, status) {
|
|
1586
|
+
super(message);
|
|
1587
|
+
this.status = status;
|
|
1588
|
+
this.name = "ConnectRequestError";
|
|
1589
|
+
}
|
|
1590
|
+
status;
|
|
1591
|
+
};
|
|
1592
|
+
async function request(fetchImpl, url, init) {
|
|
1593
|
+
const response = await fetchImpl(url, {
|
|
1594
|
+
...init,
|
|
1595
|
+
headers: {
|
|
1596
|
+
"Content-Type": "application/json",
|
|
1597
|
+
...init.headers ?? {}
|
|
1598
|
+
}
|
|
1599
|
+
});
|
|
1600
|
+
const text = await response.text();
|
|
1601
|
+
const body = text ? JSON.parse(text) : null;
|
|
1602
|
+
if (!response.ok) {
|
|
1603
|
+
const message = body?.error ?? body?.message ?? `${response.status} ${response.statusText}`;
|
|
1604
|
+
throw new ConnectRequestError(`Haven setup request failed: ${message}`, response.status);
|
|
1605
|
+
}
|
|
1606
|
+
return body;
|
|
1607
|
+
}
|
|
1608
|
+
function generateDelegateKey() {
|
|
1609
|
+
return delegateKeyFromPrivateKey(ethers.Wallet.createRandom().privateKey);
|
|
1610
|
+
}
|
|
1611
|
+
function delegateKeyFromPrivateKey(privateKey) {
|
|
1612
|
+
const wallet = new ethers.Wallet(privateKey);
|
|
1613
|
+
return {
|
|
1614
|
+
privateKey: wallet.privateKey,
|
|
1615
|
+
address: wallet.address,
|
|
1616
|
+
signChallenge: (message) => wallet.signMessage(message)
|
|
1617
|
+
};
|
|
1618
|
+
}
|
|
1619
|
+
function generateAgentApiKey() {
|
|
1620
|
+
return `sk_agent_${crypto__default.default.randomBytes(24).toString("hex")}`;
|
|
1621
|
+
}
|
|
1622
|
+
function hashAgentApiKey(apiKey) {
|
|
1623
|
+
return crypto__default.default.createHash("sha256").update(apiKey).digest("hex");
|
|
1624
|
+
}
|
|
1625
|
+
function agentApiKeyPrefix(apiKey) {
|
|
1626
|
+
return apiKey.slice(0, 12);
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
// src/redact.ts
|
|
1630
|
+
var API_KEY_RE = /sk_agent_[A-Za-z0-9]+/g;
|
|
1631
|
+
var PRIVATE_KEY_RE = /0x[0-9a-fA-F]{64}/g;
|
|
1632
|
+
function redactSecrets(value) {
|
|
1633
|
+
return value.replace(API_KEY_RE, "sk_agent_[redacted]").replace(PRIVATE_KEY_RE, "0x[redacted-private-key]");
|
|
1634
|
+
}
|
|
1635
|
+
function shortAddress(address) {
|
|
1636
|
+
if (!/^0x[0-9a-fA-F]{40}$/.test(address)) return address;
|
|
1637
|
+
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
|
1638
|
+
}
|
|
1639
|
+
async function preflightCredentialStorage(input = {}) {
|
|
1640
|
+
const directory = defaultCredentialRoot(input.baseDir);
|
|
1641
|
+
await promises.mkdir(directory, { recursive: true, mode: 448 });
|
|
1642
|
+
await restrictPermissions(directory, 448, input.warn);
|
|
1643
|
+
const probePath = path.join(directory, `.haven-connect-preflight-${crypto__default.default.randomBytes(8).toString("hex")}`);
|
|
1644
|
+
try {
|
|
1645
|
+
await writeOwnerOnlyJson(probePath, { ok: true }, input.warn);
|
|
1646
|
+
} finally {
|
|
1647
|
+
await promises.rm(probePath, { force: true }).catch(() => void 0);
|
|
1648
|
+
}
|
|
1649
|
+
return directory;
|
|
1650
|
+
}
|
|
1651
|
+
async function writeCredentialFiles(input) {
|
|
1652
|
+
const directory = defaultAgentDirectory(input.agentId, input.baseDir);
|
|
1653
|
+
await promises.mkdir(directory, { recursive: true, mode: 448 });
|
|
1654
|
+
await restrictPermissions(directory, 448, input.warn);
|
|
1655
|
+
const identityPath = path.join(directory, "identity.json");
|
|
1656
|
+
const signerPath = path.join(directory, "signer.json");
|
|
1657
|
+
const agentPath = path.join(directory, "agent.json");
|
|
1658
|
+
await assertDoesNotExist(identityPath);
|
|
1659
|
+
await assertDoesNotExist(signerPath);
|
|
1660
|
+
await assertDoesNotExist(agentPath);
|
|
1661
|
+
await writeOwnerOnlyJson(
|
|
1662
|
+
signerPath,
|
|
1663
|
+
{
|
|
1664
|
+
delegate_key: input.delegateKey,
|
|
1665
|
+
delegate_address: input.delegateAddress,
|
|
1666
|
+
agent_id: input.agentId,
|
|
1667
|
+
safe_address: input.safeAddress,
|
|
1668
|
+
chain_id: input.chainId,
|
|
1669
|
+
network: input.network,
|
|
1670
|
+
x402_binding_signer: input.x402BindingSigner,
|
|
1671
|
+
note: "Local signer credential. Haven backend never receives this private key."
|
|
1672
|
+
},
|
|
1673
|
+
input.warn
|
|
1674
|
+
);
|
|
1675
|
+
try {
|
|
1676
|
+
await writeOwnerOnlyJson(
|
|
1677
|
+
identityPath,
|
|
1678
|
+
{
|
|
1679
|
+
api_key: input.apiKey,
|
|
1680
|
+
agent_id: input.agentId,
|
|
1681
|
+
safe_address: input.safeAddress,
|
|
1682
|
+
chain_id: input.chainId,
|
|
1683
|
+
network: input.network,
|
|
1684
|
+
api_url: input.apiUrl,
|
|
1685
|
+
hosted_mcp_url: input.hostedMcpUrl,
|
|
1686
|
+
agent_budget: input.agentBudget,
|
|
1687
|
+
note: "Haven API key identifies the agent only. It cannot spend without the local signer key and on-chain Haven wallet rules."
|
|
1688
|
+
},
|
|
1689
|
+
input.warn
|
|
1690
|
+
);
|
|
1691
|
+
} catch (err) {
|
|
1692
|
+
await promises.rm(signerPath, { force: true }).catch(() => void 0);
|
|
1693
|
+
throw err;
|
|
1694
|
+
}
|
|
1695
|
+
try {
|
|
1696
|
+
await writeOwnerOnlyJson(
|
|
1697
|
+
agentPath,
|
|
1698
|
+
{
|
|
1699
|
+
agent_id: input.agentId,
|
|
1700
|
+
delegate_address: input.delegateAddress,
|
|
1701
|
+
safe_address: input.safeAddress,
|
|
1702
|
+
chain_id: input.chainId,
|
|
1703
|
+
network: input.network,
|
|
1704
|
+
agent_budget: input.agentBudget,
|
|
1705
|
+
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."
|
|
1706
|
+
},
|
|
1707
|
+
input.warn
|
|
1708
|
+
);
|
|
1709
|
+
} catch (err) {
|
|
1710
|
+
await promises.rm(signerPath, { force: true }).catch(() => void 0);
|
|
1711
|
+
await promises.rm(identityPath, { force: true }).catch(() => void 0);
|
|
1712
|
+
await promises.rm(agentPath, { force: true }).catch(() => void 0);
|
|
1713
|
+
throw err;
|
|
1714
|
+
}
|
|
1715
|
+
return { directory, identityPath, signerPath, agentPath };
|
|
1716
|
+
}
|
|
1717
|
+
function defaultAgentDirectory(agentId, baseDir = path.join(os.homedir(), ".haven", "agents")) {
|
|
1718
|
+
return path.resolve(defaultCredentialRoot(baseDir), safePathPart(agentId));
|
|
1719
|
+
}
|
|
1720
|
+
function defaultCredentialRoot(baseDir = path.join(os.homedir(), ".haven", "agents")) {
|
|
1721
|
+
return path.resolve(baseDir);
|
|
1722
|
+
}
|
|
1723
|
+
async function writeOwnerOnlyJson(path, value, warn) {
|
|
1724
|
+
const json = JSON.stringify(dropUndefined(value), null, 2);
|
|
1725
|
+
await promises.writeFile(path, `${json}
|
|
1726
|
+
`, { mode: 384, flag: "wx" });
|
|
1727
|
+
await restrictPermissions(path, 384, warn);
|
|
1728
|
+
}
|
|
1729
|
+
function safePathPart(value) {
|
|
1730
|
+
return value.replace(/[^A-Za-z0-9_.-]/g, "_");
|
|
1731
|
+
}
|
|
1732
|
+
function dropUndefined(value) {
|
|
1733
|
+
return Object.fromEntries(Object.entries(value).filter(([, field]) => field !== void 0));
|
|
1734
|
+
}
|
|
1735
|
+
async function assertDoesNotExist(path) {
|
|
1736
|
+
try {
|
|
1737
|
+
await promises.access(path);
|
|
1738
|
+
} catch (err) {
|
|
1739
|
+
if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") return;
|
|
1740
|
+
throw err;
|
|
1741
|
+
}
|
|
1742
|
+
throw new Error(`Refusing to overwrite existing Haven credential file: ${path}`);
|
|
1743
|
+
}
|
|
1744
|
+
async function restrictPermissions(path, mode, warn) {
|
|
1745
|
+
try {
|
|
1746
|
+
await promises.chmod(path, mode);
|
|
1747
|
+
} catch (err) {
|
|
1748
|
+
warn?.(
|
|
1749
|
+
`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)}`
|
|
1750
|
+
);
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
// src/runtime-install.ts
|
|
1755
|
+
init_config_writers();
|
|
1756
|
+
async function acknowledgeLocalMcpConsent(identityPath, signerPath, log) {
|
|
1757
|
+
try {
|
|
1758
|
+
const input = await buildLocalMcpConsentInput(identityPath, signerPath);
|
|
1759
|
+
const decision = await mcp.ensureConsent(input, {
|
|
1760
|
+
credentialsPath: identityPath,
|
|
1761
|
+
writeAck: true,
|
|
1762
|
+
out: log ? { write: (chunk) => writeLogChunk(log, chunk) } : void 0
|
|
1763
|
+
});
|
|
1764
|
+
return {
|
|
1765
|
+
acknowledged: decision.ok,
|
|
1766
|
+
hash: decision.hash,
|
|
1767
|
+
reason: decision.reason
|
|
1768
|
+
};
|
|
1769
|
+
} catch (err) {
|
|
1770
|
+
return {
|
|
1771
|
+
acknowledged: false,
|
|
1772
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1773
|
+
};
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
async function getLocalMcpConsentStatus(identityPath, signerPath) {
|
|
1777
|
+
try {
|
|
1778
|
+
const input = await buildLocalMcpConsentInput(identityPath, signerPath);
|
|
1779
|
+
const hash = mcp.computeConsentHash(input);
|
|
1780
|
+
const stored = await readLocalMcpAckFile(localMcpAckPath(identityPath));
|
|
1781
|
+
if (stored === hash) {
|
|
1782
|
+
return { acknowledged: true, hash, reason: "ack_file_match" };
|
|
1783
|
+
}
|
|
1784
|
+
return {
|
|
1785
|
+
acknowledged: false,
|
|
1786
|
+
hash,
|
|
1787
|
+
reason: stored ? "ack_file_mismatch" : "ack_file_missing"
|
|
1788
|
+
};
|
|
1789
|
+
} catch (err) {
|
|
1790
|
+
return {
|
|
1791
|
+
acknowledged: false,
|
|
1792
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1793
|
+
};
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
function localMcpAckPath(identityPath) {
|
|
1797
|
+
return path.resolve(`${identityPath}.ack.json`);
|
|
1798
|
+
}
|
|
1799
|
+
async function buildLocalMcpConsentInput(identityPath, signerPath) {
|
|
1800
|
+
const credentials = await mcp.loadCredentials({ identityPath, signerPath });
|
|
1801
|
+
const unavailableDuringSetup = {
|
|
1802
|
+
getAllowances: async () => {
|
|
1803
|
+
throw new Error("Haven approval is not complete yet.");
|
|
1804
|
+
}
|
|
1275
1805
|
};
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1806
|
+
return mcp.consentInputFromClient(
|
|
1807
|
+
unavailableDuringSetup,
|
|
1808
|
+
{
|
|
1809
|
+
apiKey: credentials.apiKey,
|
|
1810
|
+
apiUrl: credentials.apiUrl,
|
|
1811
|
+
agentId: credentials.agentId,
|
|
1812
|
+
safeAddress: credentials.safeAddress,
|
|
1813
|
+
delegateAddress: credentials.delegateAddress,
|
|
1814
|
+
chainId: credentials.chainId,
|
|
1815
|
+
allowanceSummary: credentials.allowanceSummary
|
|
1816
|
+
},
|
|
1817
|
+
mcp.registeredToolNames()
|
|
1818
|
+
);
|
|
1279
1819
|
}
|
|
1280
|
-
async function
|
|
1820
|
+
async function readLocalMcpAckFile(path) {
|
|
1281
1821
|
try {
|
|
1282
|
-
await promises.
|
|
1822
|
+
const parsed = JSON.parse(await promises.readFile(path, "utf8"));
|
|
1823
|
+
return typeof parsed.ack === "string" ? parsed.ack : null;
|
|
1283
1824
|
} catch {
|
|
1284
|
-
|
|
1825
|
+
return null;
|
|
1285
1826
|
}
|
|
1286
1827
|
}
|
|
1828
|
+
function writeLogChunk(log, chunk) {
|
|
1829
|
+
const message = String(chunk).trimEnd();
|
|
1830
|
+
if (message) log(message);
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
// src/runtime-install.ts
|
|
1834
|
+
init_probes();
|
|
1835
|
+
|
|
1836
|
+
// src/local-mcp-runtime.ts
|
|
1837
|
+
init_signer_runtime();
|
|
1838
|
+
init_runtime_manifest();
|
|
1287
1839
|
var execFileAsync2 = util.promisify(child_process.execFile);
|
|
1288
|
-
|
|
1840
|
+
var UnsupportedNodeVersionError = class extends Error {
|
|
1841
|
+
code = "local_mcp_unsupported_node_version";
|
|
1842
|
+
nodeVersion;
|
|
1843
|
+
minimumNodeVersion;
|
|
1844
|
+
constructor(nodeVersion, minimumNodeVersion, subject = "Haven setup") {
|
|
1845
|
+
super(sdk.unsupportedNodeVersionMessage({ subject, nodeVersion, minimumNodeVersion }));
|
|
1846
|
+
this.name = "UnsupportedNodeVersionError";
|
|
1847
|
+
this.nodeVersion = nodeVersion;
|
|
1848
|
+
this.minimumNodeVersion = minimumNodeVersion;
|
|
1849
|
+
}
|
|
1850
|
+
};
|
|
1851
|
+
async function prepareLocalMcpRuntime(input, deps = {}) {
|
|
1852
|
+
assertSupportedNodeVersion(input.nodeVersion);
|
|
1289
1853
|
const homeDir = input.homeDir ?? os.homedir();
|
|
1290
|
-
const runtimeDirectory = path.resolve(homeDir, ".haven", "
|
|
1854
|
+
const runtimeDirectory = path.resolve(homeDir, ".haven", "mcp-runtime", MCP_RUNTIME_MANIFEST.mcpVersion);
|
|
1291
1855
|
const npmCacheDirectory = path.resolve(homeDir, ".haven", "npm-cache");
|
|
1292
|
-
const cliPath = path.join(runtimeDirectory, "node_modules", "@haven_ai", "
|
|
1856
|
+
const cliPath = path.join(runtimeDirectory, "node_modules", "@haven_ai", "mcp", "dist", "cli.js");
|
|
1293
1857
|
const messages = [];
|
|
1294
1858
|
await promises.mkdir(runtimeDirectory, { recursive: true, mode: 448 });
|
|
1295
1859
|
await promises.chmod(runtimeDirectory, 448).catch(() => void 0);
|
|
1296
1860
|
await promises.mkdir(npmCacheDirectory, { recursive: true, mode: 448 });
|
|
1297
1861
|
await promises.chmod(npmCacheDirectory, 448).catch(() => void 0);
|
|
1298
1862
|
if (await installedRuntimeMatches2(runtimeDirectory, cliPath)) {
|
|
1299
|
-
messages.push(`Using existing local Haven
|
|
1863
|
+
messages.push(`Using existing local Haven MCP runtime ${mcpPackageSpec()}.`);
|
|
1300
1864
|
} else {
|
|
1301
|
-
await installRuntimePackages2(runtimeDirectory, npmCacheDirectory, deps
|
|
1302
|
-
messages.push(`Installed local Haven
|
|
1865
|
+
await installRuntimePackages2(runtimeDirectory, npmCacheDirectory, deps);
|
|
1866
|
+
messages.push(`Installed local Haven MCP runtime ${mcpPackageSpec()}.`);
|
|
1303
1867
|
}
|
|
1304
|
-
await assertFileExists2(cliPath, "local Haven
|
|
1305
|
-
const wrapperPath = path.join(input.credentialDirectory, "bin", "haven-
|
|
1306
|
-
await writeWrapper2({
|
|
1868
|
+
await assertFileExists2(cliPath, "local Haven MCP CLI");
|
|
1869
|
+
const wrapperPath = path.join(input.credentialDirectory, "bin", "haven-mcp");
|
|
1870
|
+
await writeWrapper2({
|
|
1871
|
+
wrapperPath,
|
|
1872
|
+
cliPath,
|
|
1873
|
+
identityPath: input.identityPath,
|
|
1874
|
+
signerPath: input.signerPath
|
|
1875
|
+
});
|
|
1307
1876
|
await writeRuntimeSidecar2({
|
|
1308
|
-
path: path.join(input.credentialDirectory, "
|
|
1877
|
+
path: path.join(input.credentialDirectory, "mcp-runtime.json"),
|
|
1309
1878
|
wrapperPath,
|
|
1310
1879
|
runtimeDirectory,
|
|
1311
1880
|
npmCacheDirectory,
|
|
1312
1881
|
cliPath
|
|
1313
1882
|
});
|
|
1314
|
-
messages.push(`Prepared stable local Haven
|
|
1883
|
+
messages.push(`Prepared stable local Haven MCP wrapper: ${wrapperPath}`);
|
|
1315
1884
|
return {
|
|
1316
1885
|
command: wrapperPath,
|
|
1317
1886
|
args: [],
|
|
@@ -1322,7 +1891,13 @@ async function prepareSignerRuntime(input, deps = {}) {
|
|
|
1322
1891
|
messages
|
|
1323
1892
|
};
|
|
1324
1893
|
}
|
|
1325
|
-
|
|
1894
|
+
function assertSupportedNodeVersion(nodeVersion = process.versions.node, minimumNodeVersion = MCP_RUNTIME_MANIFEST.minimumNodeVersion, subject = "Haven setup") {
|
|
1895
|
+
if (!sdk.isSupportedNodeVersion(nodeVersion, minimumNodeVersion)) {
|
|
1896
|
+
throw new UnsupportedNodeVersionError(nodeVersion, minimumNodeVersion, subject);
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
async function installRuntimePackages2(runtimeDirectory, npmCacheDirectory, deps) {
|
|
1900
|
+
const { runCommand, onProgress } = deps;
|
|
1326
1901
|
const baseArgs = [
|
|
1327
1902
|
"install",
|
|
1328
1903
|
"--prefix",
|
|
@@ -1331,12 +1906,22 @@ async function installRuntimePackages2(runtimeDirectory, npmCacheDirectory, runC
|
|
|
1331
1906
|
"--no-fund",
|
|
1332
1907
|
"--omit=dev",
|
|
1333
1908
|
"--prefer-offline",
|
|
1334
|
-
|
|
1909
|
+
mcpPackageSpec(),
|
|
1335
1910
|
sdkPackageSpec()
|
|
1336
1911
|
];
|
|
1337
1912
|
const run = async (args) => {
|
|
1338
|
-
|
|
1339
|
-
|
|
1913
|
+
const startedAt = Date.now();
|
|
1914
|
+
const heartbeat = setInterval(() => {
|
|
1915
|
+
const seconds = Math.round((Date.now() - startedAt) / 1e3);
|
|
1916
|
+
onProgress?.(`Still installing the local Haven MCP runtime\u2026 (${seconds}s \u2014 a cold cache can take several minutes)`);
|
|
1917
|
+
}, SIGNER_INSTALL_HEARTBEAT_MS);
|
|
1918
|
+
heartbeat.unref?.();
|
|
1919
|
+
try {
|
|
1920
|
+
if (runCommand) await runCommand("npm", args);
|
|
1921
|
+
else await execFileAsync2("npm", args, { timeout: SIGNER_INSTALL_TIMEOUT_MS, maxBuffer: 1024 * 1024 });
|
|
1922
|
+
} finally {
|
|
1923
|
+
clearInterval(heartbeat);
|
|
1924
|
+
}
|
|
1340
1925
|
};
|
|
1341
1926
|
try {
|
|
1342
1927
|
await run(baseArgs);
|
|
@@ -1344,20 +1929,18 @@ async function installRuntimePackages2(runtimeDirectory, npmCacheDirectory, runC
|
|
|
1344
1929
|
try {
|
|
1345
1930
|
await run([...baseArgs, "--cache", npmCacheDirectory]);
|
|
1346
1931
|
} catch (err) {
|
|
1347
|
-
throw new Error(
|
|
1348
|
-
`Could not install local Haven signer runtime ${signerPackageSpec()}: ${err instanceof Error ? err.message : String(err)}`
|
|
1349
|
-
);
|
|
1932
|
+
throw new Error(`Could not install local Haven MCP runtime ${mcpPackageSpec()}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1350
1933
|
}
|
|
1351
1934
|
}
|
|
1352
1935
|
}
|
|
1353
1936
|
async function installedRuntimeMatches2(runtimeDirectory, cliPath) {
|
|
1354
1937
|
try {
|
|
1355
|
-
await assertFileExists2(cliPath, "local Haven
|
|
1356
|
-
const [
|
|
1357
|
-
readPackageJson2(path.join(runtimeDirectory, "node_modules", "@haven_ai", "
|
|
1938
|
+
await assertFileExists2(cliPath, "local Haven MCP CLI");
|
|
1939
|
+
const [mcpPackage, sdkPackage] = await Promise.all([
|
|
1940
|
+
readPackageJson2(path.join(runtimeDirectory, "node_modules", "@haven_ai", "mcp", "package.json")),
|
|
1358
1941
|
readPackageJson2(path.join(runtimeDirectory, "node_modules", "@haven_ai", "sdk", "package.json"))
|
|
1359
1942
|
]);
|
|
1360
|
-
return
|
|
1943
|
+
return mcpPackage.version === MCP_RUNTIME_MANIFEST.mcpVersion && sdkPackage.version === MCP_RUNTIME_MANIFEST.sdkVersion;
|
|
1361
1944
|
} catch {
|
|
1362
1945
|
return false;
|
|
1363
1946
|
}
|
|
@@ -1373,9 +1956,10 @@ async function writeWrapper2(input) {
|
|
|
1373
1956
|
"import { spawn } from 'node:child_process'",
|
|
1374
1957
|
"",
|
|
1375
1958
|
`const cliPath = ${JSON.stringify(input.cliPath)}`,
|
|
1959
|
+
`const identityPath = ${JSON.stringify(input.identityPath)}`,
|
|
1376
1960
|
`const signerPath = ${JSON.stringify(input.signerPath)}`,
|
|
1377
1961
|
"",
|
|
1378
|
-
"const child = spawn(process.execPath, [cliPath, '--
|
|
1962
|
+
"const child = spawn(process.execPath, [cliPath, '--identity', identityPath, '--signer', signerPath, ...process.argv.slice(2)], {",
|
|
1379
1963
|
" stdio: 'inherit',",
|
|
1380
1964
|
"})",
|
|
1381
1965
|
"",
|
|
@@ -1390,10 +1974,11 @@ async function writeWrapper2(input) {
|
|
|
1390
1974
|
}
|
|
1391
1975
|
async function writeRuntimeSidecar2(input) {
|
|
1392
1976
|
const value = {
|
|
1393
|
-
|
|
1394
|
-
|
|
1977
|
+
mcp_package: MCP_RUNTIME_MANIFEST.mcpPackage,
|
|
1978
|
+
mcp_version: MCP_RUNTIME_MANIFEST.mcpVersion,
|
|
1395
1979
|
sdk_package: MCP_RUNTIME_MANIFEST.sdkPackage,
|
|
1396
1980
|
sdk_version: MCP_RUNTIME_MANIFEST.sdkVersion,
|
|
1981
|
+
minimum_node_version: MCP_RUNTIME_MANIFEST.minimumNodeVersion,
|
|
1397
1982
|
wrapper_path: input.wrapperPath,
|
|
1398
1983
|
runtime_directory: input.runtimeDirectory,
|
|
1399
1984
|
npm_cache_directory: input.npmCacheDirectory,
|
|
@@ -1410,6 +1995,10 @@ async function assertFileExists2(path, label) {
|
|
|
1410
1995
|
throw new Error(`Missing ${label}: ${path}`);
|
|
1411
1996
|
}
|
|
1412
1997
|
}
|
|
1998
|
+
|
|
1999
|
+
// src/runtime-install.ts
|
|
2000
|
+
init_signer_runtime();
|
|
2001
|
+
init_runtime_manifest();
|
|
1413
2002
|
var CODEX_AGENTS_BEGIN_MARKER = "<!-- BEGIN haven-pay (managed by @haven_ai/connect; edits inside this section are overwritten on re-setup) -->";
|
|
1414
2003
|
var CODEX_AGENTS_END_MARKER = "<!-- END haven-pay -->";
|
|
1415
2004
|
async function installSkillForRuntime(runtime, deps = {}) {
|
|
@@ -1514,208 +2103,9 @@ function hermesHome(deps) {
|
|
|
1514
2103
|
return env.HERMES_HOME ?? path.join(deps.homeDir ?? os.homedir(), ".hermes");
|
|
1515
2104
|
}
|
|
1516
2105
|
|
|
1517
|
-
// src/runtime-registry.ts
|
|
1518
|
-
var RUNTIME_PROFILES = {
|
|
1519
|
-
"claude-code": {
|
|
1520
|
-
id: "claude-code",
|
|
1521
|
-
label: "Claude Code",
|
|
1522
|
-
restartMode: "restart-session",
|
|
1523
|
-
canWriteRuntimeConfig: true,
|
|
1524
|
-
activationInstruction: "Start a new Claude Code session so it loads the Haven MCP entries."
|
|
1525
|
-
},
|
|
1526
|
-
"codex-cli": {
|
|
1527
|
-
id: "codex-cli",
|
|
1528
|
-
label: "Codex CLI",
|
|
1529
|
-
restartMode: "restart-session",
|
|
1530
|
-
canWriteRuntimeConfig: true,
|
|
1531
|
-
activationInstruction: "Start a fresh Codex CLI session (for example, run `codex resume --last`)."
|
|
1532
|
-
},
|
|
1533
|
-
"codex-desktop": {
|
|
1534
|
-
id: "codex-desktop",
|
|
1535
|
-
label: "Codex Desktop",
|
|
1536
|
-
restartMode: "restart-session",
|
|
1537
|
-
canWriteRuntimeConfig: true,
|
|
1538
|
-
activationInstruction: "Quit and reopen Codex Desktop so it loads the Haven MCP entries."
|
|
1539
|
-
},
|
|
1540
|
-
cursor: {
|
|
1541
|
-
id: "cursor",
|
|
1542
|
-
label: "Cursor",
|
|
1543
|
-
restartMode: "hot-reload",
|
|
1544
|
-
canWriteRuntimeConfig: true,
|
|
1545
|
-
activationInstruction: "Wait for Cursor to hot-reload the Haven MCP entries; no app restart is required."
|
|
1546
|
-
},
|
|
1547
|
-
vscode: {
|
|
1548
|
-
id: "vscode",
|
|
1549
|
-
label: "VS Code",
|
|
1550
|
-
restartMode: "hot-reload",
|
|
1551
|
-
canWriteRuntimeConfig: true,
|
|
1552
|
-
activationInstruction: "Wait for VS Code to hot-reload the Haven MCP entries; no app restart is required."
|
|
1553
|
-
},
|
|
1554
|
-
"vscode-insiders": {
|
|
1555
|
-
id: "vscode-insiders",
|
|
1556
|
-
label: "VS Code Insiders",
|
|
1557
|
-
restartMode: "hot-reload",
|
|
1558
|
-
canWriteRuntimeConfig: true,
|
|
1559
|
-
activationInstruction: "Wait for VS Code Insiders to hot-reload the Haven MCP entries; no app restart is required."
|
|
1560
|
-
},
|
|
1561
|
-
"claude-desktop": {
|
|
1562
|
-
id: "claude-desktop",
|
|
1563
|
-
label: "Claude Desktop",
|
|
1564
|
-
restartMode: "restart-app",
|
|
1565
|
-
canWriteRuntimeConfig: true,
|
|
1566
|
-
activationInstruction: "Quit and reopen Claude Desktop so it loads the Haven MCP entries."
|
|
1567
|
-
},
|
|
1568
|
-
hermes: {
|
|
1569
|
-
id: "hermes",
|
|
1570
|
-
label: "Hermes Agent",
|
|
1571
|
-
restartMode: "restart-session",
|
|
1572
|
-
canWriteRuntimeConfig: true,
|
|
1573
|
-
activationInstruction: "Start a new Hermes session; in Hermes Gateway, run `/restart` instead."
|
|
1574
|
-
},
|
|
1575
|
-
other: {
|
|
1576
|
-
id: "other",
|
|
1577
|
-
label: "Other agent runtime",
|
|
1578
|
-
restartMode: "manual",
|
|
1579
|
-
canWriteRuntimeConfig: false,
|
|
1580
|
-
activationInstruction: "Finish the manual MCP setup shown above, then start a fresh session in that runtime."
|
|
1581
|
-
}
|
|
1582
|
-
};
|
|
1583
|
-
var RUNTIME_ALIASES = {
|
|
1584
|
-
claude: "claude-code",
|
|
1585
|
-
"claude-code": "claude-code",
|
|
1586
|
-
claudecode: "claude-code",
|
|
1587
|
-
"claude_code": "claude-code",
|
|
1588
|
-
codex: "codex-cli",
|
|
1589
|
-
"codex-cli": "codex-cli",
|
|
1590
|
-
codexcli: "codex-cli",
|
|
1591
|
-
"codex_cli": "codex-cli",
|
|
1592
|
-
"codex-desktop": "codex-desktop",
|
|
1593
|
-
"codex_desktop": "codex-desktop",
|
|
1594
|
-
codexdesktop: "codex-desktop",
|
|
1595
|
-
"codex-app": "codex-desktop",
|
|
1596
|
-
"codex_app": "codex-desktop",
|
|
1597
|
-
codexapp: "codex-desktop",
|
|
1598
|
-
cursor: "cursor",
|
|
1599
|
-
vscode: "vscode",
|
|
1600
|
-
"vs-code": "vscode",
|
|
1601
|
-
"vs_code": "vscode",
|
|
1602
|
-
code: "vscode",
|
|
1603
|
-
"vscode-insiders": "vscode-insiders",
|
|
1604
|
-
"vscode_insiders": "vscode-insiders",
|
|
1605
|
-
vscodeinsiders: "vscode-insiders",
|
|
1606
|
-
"vs-code-insiders": "vscode-insiders",
|
|
1607
|
-
"code-insiders": "vscode-insiders",
|
|
1608
|
-
insiders: "vscode-insiders",
|
|
1609
|
-
"claude-desktop": "claude-desktop",
|
|
1610
|
-
"claude_desktop": "claude-desktop",
|
|
1611
|
-
claudesktop: "claude-desktop",
|
|
1612
|
-
desktop: "claude-desktop",
|
|
1613
|
-
hermes: "hermes",
|
|
1614
|
-
"hermes-agent": "hermes",
|
|
1615
|
-
hermes_agent: "hermes",
|
|
1616
|
-
hermesagent: "hermes",
|
|
1617
|
-
other: "other",
|
|
1618
|
-
manual: "other"
|
|
1619
|
-
};
|
|
1620
|
-
function runtimeProfile(runtime, env = process.env) {
|
|
1621
|
-
return RUNTIME_PROFILES[normalizeRuntime(runtime, env)];
|
|
1622
|
-
}
|
|
1623
|
-
function normalizeRuntime(runtime, env = process.env) {
|
|
1624
|
-
const explicit = normalizeRuntimeName(runtime);
|
|
1625
|
-
if (explicit) return explicit;
|
|
1626
|
-
return detectRuntime(env) ?? "other";
|
|
1627
|
-
}
|
|
1628
|
-
function restartRequiredForRuntime(runtime, env = process.env) {
|
|
1629
|
-
const mode = runtimeProfile(runtime, env).restartMode;
|
|
1630
|
-
return mode === "restart-session" || mode === "restart-app";
|
|
1631
|
-
}
|
|
1632
|
-
function runtimeVerificationInstruction(runtime) {
|
|
1633
|
-
const label = RUNTIME_PROFILES[runtime].label;
|
|
1634
|
-
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.`;
|
|
1635
|
-
}
|
|
1636
|
-
function normalizeRuntimeName(runtime) {
|
|
1637
|
-
const key = runtime?.trim().toLowerCase();
|
|
1638
|
-
if (!key) return null;
|
|
1639
|
-
return RUNTIME_ALIASES[key.replace(/\s+/g, "-")] ?? null;
|
|
1640
|
-
}
|
|
1641
|
-
function detectRuntime(env) {
|
|
1642
|
-
if (env.CLAUDECODE || env.CLAUDE_CODE || env.CLAUDECODE_CWD) return "claude-code";
|
|
1643
|
-
if (env.CODEX_SANDBOX || env.CODEX_HOME || env.CODEX_CWD) return "codex-cli";
|
|
1644
|
-
if (env.VSCODE_CWD || env.VSCODE_IPC_HOOK_CLI || env.TERM_PROGRAM === "vscode") return "vscode";
|
|
1645
|
-
if (env.HERMES_HOME || env.HERMES_AGENT) return "hermes";
|
|
1646
|
-
return null;
|
|
1647
|
-
}
|
|
1648
|
-
async function acknowledgeLocalSignerConsent(signerPath, log) {
|
|
1649
|
-
try {
|
|
1650
|
-
const input = await buildSignerConsentInput(signerPath);
|
|
1651
|
-
const decision = await signer.ensureSignerConsent(input, {
|
|
1652
|
-
credentialsPath: signerPath,
|
|
1653
|
-
writeAck: true,
|
|
1654
|
-
out: log ? { write: (chunk) => writeLogChunk2(log, chunk) } : void 0
|
|
1655
|
-
});
|
|
1656
|
-
return {
|
|
1657
|
-
acknowledged: decision.ok,
|
|
1658
|
-
hash: decision.hash,
|
|
1659
|
-
reason: decision.reason
|
|
1660
|
-
};
|
|
1661
|
-
} catch (err) {
|
|
1662
|
-
return {
|
|
1663
|
-
acknowledged: false,
|
|
1664
|
-
error: err instanceof Error ? err.message : String(err)
|
|
1665
|
-
};
|
|
1666
|
-
}
|
|
1667
|
-
}
|
|
1668
|
-
async function getLocalSignerConsentStatus(signerPath) {
|
|
1669
|
-
try {
|
|
1670
|
-
const input = await buildSignerConsentInput(signerPath);
|
|
1671
|
-
const hash = signer.computeSignerConsentHash(input);
|
|
1672
|
-
const stored = await readSignerAckFile(signerAckPath(signerPath));
|
|
1673
|
-
if (stored === hash) {
|
|
1674
|
-
return { acknowledged: true, hash, reason: "ack_file_match" };
|
|
1675
|
-
}
|
|
1676
|
-
return {
|
|
1677
|
-
acknowledged: false,
|
|
1678
|
-
hash,
|
|
1679
|
-
reason: stored ? "ack_file_mismatch" : "ack_file_missing"
|
|
1680
|
-
};
|
|
1681
|
-
} catch (err) {
|
|
1682
|
-
return {
|
|
1683
|
-
acknowledged: false,
|
|
1684
|
-
error: err instanceof Error ? err.message : String(err)
|
|
1685
|
-
};
|
|
1686
|
-
}
|
|
1687
|
-
}
|
|
1688
|
-
function signerAckPath(signerPath) {
|
|
1689
|
-
return path.resolve(`${signerPath}.signer-ack.json`);
|
|
1690
|
-
}
|
|
1691
|
-
async function buildSignerConsentInput(signerPath) {
|
|
1692
|
-
const credentials = await signer.loadSignerCredentials(signerPath);
|
|
1693
|
-
const signer$1 = signer.createEdgeSigner(credentials.delegateKey, {
|
|
1694
|
-
x402BindingSigner: credentials.x402BindingSigner
|
|
1695
|
-
});
|
|
1696
|
-
return {
|
|
1697
|
-
delegateAddress: signer$1.delegateAddress,
|
|
1698
|
-
safeAddress: credentials.safeAddress,
|
|
1699
|
-
agentId: credentials.agentId,
|
|
1700
|
-
chainId: credentials.chainId,
|
|
1701
|
-
network: credentials.network,
|
|
1702
|
-
toolNames: Object.keys(signer.toolSchemas)
|
|
1703
|
-
};
|
|
1704
|
-
}
|
|
1705
|
-
async function readSignerAckFile(path) {
|
|
1706
|
-
try {
|
|
1707
|
-
const parsed = JSON.parse(await promises.readFile(path, "utf8"));
|
|
1708
|
-
return typeof parsed.ack === "string" ? parsed.ack : null;
|
|
1709
|
-
} catch {
|
|
1710
|
-
return null;
|
|
1711
|
-
}
|
|
1712
|
-
}
|
|
1713
|
-
function writeLogChunk2(log, chunk) {
|
|
1714
|
-
const message = String(chunk).trimEnd();
|
|
1715
|
-
if (message) log(message);
|
|
1716
|
-
}
|
|
1717
|
-
|
|
1718
2106
|
// src/runtime-install.ts
|
|
2107
|
+
init_runtime_registry();
|
|
2108
|
+
init_signer_consent();
|
|
1719
2109
|
var execFileAsync3 = util.promisify(child_process.execFile);
|
|
1720
2110
|
async function installRuntime(input, deps = {}) {
|
|
1721
2111
|
const runtime = normalizeRuntime(input.runtime, deps.env);
|
|
@@ -1791,9 +2181,28 @@ async function installRuntime(input, deps = {}) {
|
|
|
1791
2181
|
signerCommand = { command: signerRuntime.command, args: signerRuntime.args };
|
|
1792
2182
|
consentMessages.push(...signerRuntime.messages);
|
|
1793
2183
|
} catch (err) {
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
2184
|
+
return {
|
|
2185
|
+
runtime,
|
|
2186
|
+
runtimeMcpMode: "hosted_plus_signer",
|
|
2187
|
+
hostedMcpConfigured: false,
|
|
2188
|
+
localSignerConfigured: false,
|
|
2189
|
+
localMcpConfigured: false,
|
|
2190
|
+
probeResult: "signer_runtime_install_failed",
|
|
2191
|
+
restartRequired: false,
|
|
2192
|
+
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",
|
|
2193
|
+
errorCode: "signer_runtime_install_failed",
|
|
2194
|
+
configTarget: profile.label,
|
|
2195
|
+
signerAcknowledged: signerConsent?.acknowledged,
|
|
2196
|
+
localMcpAcknowledged: localMcpConsent?.acknowledged,
|
|
2197
|
+
activationCommand: void 0,
|
|
2198
|
+
signerRuntimePrepared: false,
|
|
2199
|
+
messages: [
|
|
2200
|
+
...consentMessages,
|
|
2201
|
+
`Could not pre-install the local Haven signer: ${err instanceof Error ? err.message : String(err)}`,
|
|
2202
|
+
"No runtime configuration was written (fail-closed): a config pointing at an uninstalled signer looks wired but cannot start.",
|
|
2203
|
+
"Re-run `npx @haven_ai/connect@alpha` to retry the setup."
|
|
2204
|
+
]
|
|
2205
|
+
};
|
|
1797
2206
|
}
|
|
1798
2207
|
}
|
|
1799
2208
|
const signerRuntimePrepared = localRuntime ? void 0 : signerCommand !== void 0;
|
|
@@ -1810,19 +2219,51 @@ async function installRuntime(input, deps = {}) {
|
|
|
1810
2219
|
homeDir: deps.homeDir,
|
|
1811
2220
|
mode: localRuntime ? "local" : "hosted"
|
|
1812
2221
|
});
|
|
2222
|
+
if (deps.onRuntimeConfigured) {
|
|
2223
|
+
const signerCredentialOnDisk = await probeLocalSignerCredential(input.signerPath);
|
|
2224
|
+
const earlyLocalMcpOk = configResult.runtimeMcpMode === "local_stdio" && configResult.localMcpConfigured && signerCredentialOnDisk && Boolean(localMcpConsent?.acknowledged);
|
|
2225
|
+
const earlySignerOk = configResult.runtimeMcpMode === "local_stdio" ? earlyLocalMcpOk : configResult.signerConfigured && signerCredentialOnDisk && Boolean(signerConsent?.acknowledged);
|
|
2226
|
+
try {
|
|
2227
|
+
await deps.onRuntimeConfigured({
|
|
2228
|
+
runtime,
|
|
2229
|
+
runtimeMcpMode: configResult.runtimeMcpMode,
|
|
2230
|
+
hostedMcpConfigured: configResult.hostedConfigured,
|
|
2231
|
+
localSignerConfigured: earlySignerOk,
|
|
2232
|
+
localMcpConfigured: earlyLocalMcpOk,
|
|
2233
|
+
signerAcknowledged: signerConsent?.acknowledged,
|
|
2234
|
+
localMcpAcknowledged: localMcpConsent?.acknowledged,
|
|
2235
|
+
restartRequired: configResult.restartRequired || restartRequiredForRuntime(runtime, deps.env),
|
|
2236
|
+
nextUserAction: nextAction(runtime, profile.restartMode, configResult.errorCode),
|
|
2237
|
+
errorCode: configResult.errorCode
|
|
2238
|
+
});
|
|
2239
|
+
} catch {
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
1813
2242
|
progress("Almost there \u2014 just confirming everything connects\u2026");
|
|
1814
2243
|
const localProbePromise = configResult.runtimeMcpMode === "local_stdio" && localRuntimeInstall ? runLocalMcpProbe(localRuntimeInstall, deps) : Promise.resolve(void 0);
|
|
1815
|
-
const
|
|
2244
|
+
const signerProbePromise = configResult.runtimeMcpMode !== "local_stdio" && signerCommand ? (deps.probeSignerTools ?? probeLocalMcpTools)(
|
|
2245
|
+
signerCommand.command,
|
|
2246
|
+
signerCommand.args,
|
|
2247
|
+
MCP_RUNTIME_MANIFEST.requiredSignerTools
|
|
2248
|
+
) : Promise.resolve(void 0);
|
|
2249
|
+
const [hostedProbe, signerCredentialReady, localMcpProbe, signerProbe] = await Promise.all([
|
|
1816
2250
|
configResult.hostedConfigured ? probeHostedMcpTools(input.apiKey, input.hostedMcpUrl, deps.fetch) : Promise.resolve({ status: "bad_response" }),
|
|
1817
2251
|
probeLocalSignerCredential(input.signerPath),
|
|
1818
|
-
localProbePromise
|
|
2252
|
+
localProbePromise,
|
|
2253
|
+
signerProbePromise
|
|
1819
2254
|
]);
|
|
1820
2255
|
const hostedOk = configResult.hostedConfigured && hostedProbe.status === "ok";
|
|
1821
2256
|
const localMcpOk = configResult.runtimeMcpMode === "local_stdio" && configResult.localMcpConfigured && signerCredentialReady && Boolean(localMcpConsent?.acknowledged) && localMcpProbe?.status === "ok";
|
|
1822
|
-
const signerOk = configResult.runtimeMcpMode === "local_stdio" ? localMcpOk : configResult.signerConfigured && signerCredentialReady && Boolean(signerConsent?.acknowledged)
|
|
2257
|
+
const signerOk = configResult.runtimeMcpMode === "local_stdio" ? localMcpOk : configResult.signerConfigured && signerCredentialReady && Boolean(signerConsent?.acknowledged) && // #1587: no handshake, no green. A signer command that was registered
|
|
2258
|
+
// but not probed (manual topology) keeps the old semantics.
|
|
2259
|
+
(signerProbe === void 0 || signerProbe.status === "ok");
|
|
1823
2260
|
const restartRequired = configResult.restartRequired || restartRequiredForRuntime(runtime, deps.env);
|
|
1824
|
-
const errorCode = configResult.errorCode ?? (configResult.runtimeMcpMode === "local_stdio" ? localMcpErrorCode(signerCredentialReady, localMcpConsent, localMcpProbe?.status) : hostedMcpErrorCode(configResult.hostedConfigured, hostedProbe.status) ?? signerConsentErrorCode(signerCredentialReady, signerConsent));
|
|
2261
|
+
const errorCode = configResult.errorCode ?? (configResult.runtimeMcpMode === "local_stdio" ? localMcpErrorCode(signerCredentialReady, localMcpConsent, localMcpProbe?.status) : hostedMcpErrorCode(configResult.hostedConfigured, hostedProbe.status) ?? signerConsentErrorCode(signerCredentialReady, signerConsent) ?? signerProbeErrorCode(signerProbe));
|
|
1825
2262
|
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."] : [];
|
|
2263
|
+
const signerProbeMessages = signerProbe ? signerProbe.status === "ok" ? ["Verified local Haven signer with a stdio handshake."] : [
|
|
2264
|
+
`Local Haven signer handshake failed: ${signerProbe.status}.`,
|
|
2265
|
+
"Re-run `npx @haven_ai/connect@alpha` to repair the signer setup."
|
|
2266
|
+
] : [];
|
|
1826
2267
|
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."] : [];
|
|
1827
2268
|
const skillInstall = !configResult.errorCode ? await installSkillForRuntime(runtime, { homeDir: deps.homeDir, env: deps.env }) : void 0;
|
|
1828
2269
|
return {
|
|
@@ -1841,7 +2282,7 @@ async function installRuntime(input, deps = {}) {
|
|
|
1841
2282
|
activationCommand: configResult.activationCommand,
|
|
1842
2283
|
skillInstalled: skillInstall?.installed,
|
|
1843
2284
|
signerRuntimePrepared,
|
|
1844
|
-
messages: [...consentMessages, ...localRuntimeInstall?.messages ?? [], ...configResult.messages, ...hostedProbeMessages, ...localProbeMessages, ...skillInstall?.messages ?? []]
|
|
2285
|
+
messages: [...consentMessages, ...localRuntimeInstall?.messages ?? [], ...configResult.messages, ...hostedProbeMessages, ...signerProbeMessages, ...localProbeMessages, ...skillInstall?.messages ?? []]
|
|
1845
2286
|
};
|
|
1846
2287
|
}
|
|
1847
2288
|
function runtimeInstallCapabilities(runtime, env = process.env) {
|
|
@@ -1861,10 +2302,11 @@ async function configureClaudeCode(deps, localMcpCommand) {
|
|
|
1861
2302
|
});
|
|
1862
2303
|
try {
|
|
1863
2304
|
if (!localMcpCommand) throw new Error("local MCP wrapper command is required");
|
|
2305
|
+
await runCommand("claude", ["mcp", "remove", "haven"]).catch(() => void 0);
|
|
2306
|
+
await runCommand("claude", ["mcp", "remove", "haven-signer"]).catch(() => void 0);
|
|
1864
2307
|
await runCommand("claude", ["mcp", "add-json", "haven", serverJson, "--scope", "user"]).catch(async () => {
|
|
1865
2308
|
await runCommand("claude", ["mcp", "add", "haven", "--scope", "user", "--", localMcpCommand]);
|
|
1866
2309
|
});
|
|
1867
|
-
await runCommand("claude", ["mcp", "remove", "haven-signer"]).catch(() => void 0);
|
|
1868
2310
|
const verified = await runCommand("claude", ["mcp", "get", "haven"]).then(() => true).catch(() => false);
|
|
1869
2311
|
return {
|
|
1870
2312
|
hostedConfigured: false,
|
|
@@ -1986,6 +2428,10 @@ function signerConsentErrorCode(signerCredentialReady, signerConsent) {
|
|
|
1986
2428
|
if (!signerConsent?.acknowledged) return "local_signer_ack_required";
|
|
1987
2429
|
return void 0;
|
|
1988
2430
|
}
|
|
2431
|
+
function signerProbeErrorCode(probe) {
|
|
2432
|
+
if (!probe || probe.status === "ok") return void 0;
|
|
2433
|
+
return `local_signer_probe_${probe.status}`;
|
|
2434
|
+
}
|
|
1989
2435
|
function hostedMcpErrorCode(hostedConfigured, hostedProbeStatus) {
|
|
1990
2436
|
if (!hostedConfigured || hostedProbeStatus === "ok") return void 0;
|
|
1991
2437
|
return `hosted_mcp_probe_${hostedProbeStatus}`;
|
|
@@ -2009,7 +2455,7 @@ function supportsLocalMcp(runtime) {
|
|
|
2009
2455
|
return runtime === "codex-cli" || runtime === "codex-desktop" || runtime === "claude-code";
|
|
2010
2456
|
}
|
|
2011
2457
|
async function prepareRuntimeForLocalMcp(input, deps) {
|
|
2012
|
-
const prepare = deps.prepareLocalMcpRuntime ?? ((runtimeInput) => prepareLocalMcpRuntime(runtimeInput, { runCommand: deps.runCommand }));
|
|
2458
|
+
const prepare = deps.prepareLocalMcpRuntime ?? ((runtimeInput) => prepareLocalMcpRuntime(runtimeInput, { runCommand: deps.runCommand, onProgress: deps.onProgress }));
|
|
2013
2459
|
return prepare({
|
|
2014
2460
|
credentialDirectory: input.credentialDirectory,
|
|
2015
2461
|
identityPath: input.identityPath,
|
|
@@ -2018,7 +2464,13 @@ async function prepareRuntimeForLocalMcp(input, deps) {
|
|
|
2018
2464
|
});
|
|
2019
2465
|
}
|
|
2020
2466
|
async function prepareSignerForRuntime(input, deps) {
|
|
2021
|
-
const prepare = deps.prepareSignerRuntime ?? ((runtimeInput) =>
|
|
2467
|
+
const prepare = deps.prepareSignerRuntime ?? ((runtimeInput) => (
|
|
2468
|
+
// onProgress threaded through on purpose (#1586 review): without it the
|
|
2469
|
+
// install heartbeat was dead code in production and the console still
|
|
2470
|
+
// went silent for the whole cold install — the exact symptom the issue
|
|
2471
|
+
// set out to remove, at a longer timeout.
|
|
2472
|
+
prepareSignerRuntime(runtimeInput, { runCommand: deps.runCommand, onProgress: deps.onProgress })
|
|
2473
|
+
));
|
|
2022
2474
|
return prepare({
|
|
2023
2475
|
credentialDirectory: input.credentialDirectory,
|
|
2024
2476
|
signerPath: input.signerPath,
|
|
@@ -2041,7 +2493,9 @@ function localRuntimePrepareErrorCode(err) {
|
|
|
2041
2493
|
}
|
|
2042
2494
|
|
|
2043
2495
|
// src/runtime.ts
|
|
2044
|
-
|
|
2496
|
+
init_runtime_registry();
|
|
2497
|
+
init_runtime_manifest();
|
|
2498
|
+
var CONNECTOR_VERSION = "0.1.27-alpha.0";
|
|
2045
2499
|
var CONNECT_OUTCOME_SCHEMA_VERSION = 1;
|
|
2046
2500
|
async function runConnect(options, deps = {}) {
|
|
2047
2501
|
assertSupportedNodeVersion(deps.nodeVersion, MCP_RUNTIME_MANIFEST.minimumNodeVersion);
|
|
@@ -2148,7 +2602,35 @@ async function runConnect(options, deps = {}) {
|
|
|
2148
2602
|
ackSigner: options.ackSigner,
|
|
2149
2603
|
ackLocalTools: options.ackLocalTools,
|
|
2150
2604
|
localMcp: options.localMcp
|
|
2151
|
-
}, {
|
|
2605
|
+
}, {
|
|
2606
|
+
onProgress: log,
|
|
2607
|
+
// #1543: report "runtime configured" the moment the config write settles,
|
|
2608
|
+
// so the dashboard can expose its budget-approval controls without
|
|
2609
|
+
// waiting on the probes and skill install — a tail that approval does not
|
|
2610
|
+
// depend on. Silent and best-effort: the complete report below remains
|
|
2611
|
+
// authoritative (it overwrites these keys, adding the probe verdicts and
|
|
2612
|
+
// skill state), so an early failure costs nothing but the head start.
|
|
2613
|
+
onRuntimeConfigured: async (early) => {
|
|
2614
|
+
try {
|
|
2615
|
+
await api.updateInstallStatus(registration.setup_id, localApiKey, {
|
|
2616
|
+
runtime: early.runtime,
|
|
2617
|
+
connectorVersion,
|
|
2618
|
+
runtimeMcpMode: early.runtimeMcpMode,
|
|
2619
|
+
hostedMcpConfigured: early.hostedMcpConfigured,
|
|
2620
|
+
localSignerConfigured: early.localSignerConfigured,
|
|
2621
|
+
localMcpConfigured: early.localMcpConfigured,
|
|
2622
|
+
credentialFilesWritten: true,
|
|
2623
|
+
signerAcknowledged: early.signerAcknowledged,
|
|
2624
|
+
localMcpAcknowledged: early.localMcpAcknowledged,
|
|
2625
|
+
restartRequired: early.restartRequired,
|
|
2626
|
+
nextUserAction: early.nextUserAction,
|
|
2627
|
+
errorCode: early.errorCode ?? null,
|
|
2628
|
+
environmentLabel: options.environmentLabel ?? "Local workspace"
|
|
2629
|
+
});
|
|
2630
|
+
} catch {
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
});
|
|
2152
2634
|
printRuntimeInstall(runtimeInstall, log);
|
|
2153
2635
|
if (runtimeInstall.errorCode) {
|
|
2154
2636
|
log("Haven setup needs a couple more steps on this machine \u2014 see the notes above.");
|
|
@@ -2177,10 +2659,11 @@ async function runConnect(options, deps = {}) {
|
|
|
2177
2659
|
} catch (err) {
|
|
2178
2660
|
log(`Could not report install status to Haven: ${err instanceof Error ? err.message : String(err)}`);
|
|
2179
2661
|
}
|
|
2662
|
+
let approval;
|
|
2180
2663
|
if (options.waitForApproval !== false && !runtimeInstall.errorCode) {
|
|
2181
|
-
await waitForBudgetApproval(api, registration.setup_id, localApiKey, log, options.approvalWait);
|
|
2664
|
+
approval = await waitForBudgetApproval(api, registration.setup_id, localApiKey, log, options.approvalWait);
|
|
2182
2665
|
}
|
|
2183
|
-
printNextSteps(runtimeInstall, log);
|
|
2666
|
+
printNextSteps(runtimeInstall, log, approval);
|
|
2184
2667
|
return {
|
|
2185
2668
|
setupId: registration.setup_id,
|
|
2186
2669
|
agentId: registration.agent_id,
|
|
@@ -2321,9 +2804,14 @@ async function waitForBudgetApproval(api, setupId, apiKey, log, options = {}) {
|
|
|
2321
2804
|
const sleep = options.sleep ?? ((ms) => new Promise((resolve8) => setTimeout(resolve8, ms)));
|
|
2322
2805
|
const maxPolls = Math.max(1, Math.floor(timeoutMs / intervalMs));
|
|
2323
2806
|
const remindEvery = Math.max(1, Math.floor(3e4 / intervalMs));
|
|
2324
|
-
|
|
2807
|
+
let waitingAnnounced = false;
|
|
2808
|
+
const announceWaiting = () => {
|
|
2809
|
+
if (waitingAnnounced) return;
|
|
2810
|
+
waitingAnnounced = true;
|
|
2811
|
+
log("Registered with Haven \u2014 waiting for you to approve the budget in the dashboard\u2026");
|
|
2812
|
+
};
|
|
2325
2813
|
for (let i = 0; i < maxPolls; i++) {
|
|
2326
|
-
await sleep(intervalMs);
|
|
2814
|
+
if (i > 0) await sleep(intervalMs);
|
|
2327
2815
|
let status;
|
|
2328
2816
|
try {
|
|
2329
2817
|
status = await api.getConnectorStatus(setupId, apiKey);
|
|
@@ -2332,6 +2820,7 @@ async function waitForBudgetApproval(api, setupId, apiKey, log, options = {}) {
|
|
|
2332
2820
|
log("This setup ended in Haven \u2014 start a fresh connection from the dashboard when ready.");
|
|
2333
2821
|
return "ended";
|
|
2334
2822
|
}
|
|
2823
|
+
announceWaiting();
|
|
2335
2824
|
continue;
|
|
2336
2825
|
}
|
|
2337
2826
|
if (status.status === "active") {
|
|
@@ -2344,7 +2833,8 @@ async function waitForBudgetApproval(api, setupId, apiKey, log, options = {}) {
|
|
|
2344
2833
|
log(`This setup ended in Haven (${status.status}) \u2014 start a fresh connection from the dashboard when ready.`);
|
|
2345
2834
|
return "ended";
|
|
2346
2835
|
}
|
|
2347
|
-
|
|
2836
|
+
announceWaiting();
|
|
2837
|
+
if (i > 0 && i % remindEvery === 0) {
|
|
2348
2838
|
log("Still waiting for budget approval in Haven\u2026");
|
|
2349
2839
|
}
|
|
2350
2840
|
}
|
|
@@ -2353,11 +2843,11 @@ async function waitForBudgetApproval(api, setupId, apiKey, log, options = {}) {
|
|
|
2353
2843
|
);
|
|
2354
2844
|
return "pending";
|
|
2355
2845
|
}
|
|
2356
|
-
function completionHandoffLines(result) {
|
|
2846
|
+
function completionHandoffLines(result, approval) {
|
|
2357
2847
|
if (result.errorCode === "manual_runtime_setup_required") {
|
|
2358
2848
|
return [
|
|
2359
2849
|
"Next steps:",
|
|
2360
|
-
"1. Return to Haven and approve the
|
|
2850
|
+
"1. Return to Haven and approve the budget. Approval \u2014 not restarting \u2014 unlocks Haven tools.",
|
|
2361
2851
|
"2. Finish the manual MCP setup using the secret-free file references printed above, then start a fresh session in your runtime.",
|
|
2362
2852
|
`3. ${runtimeVerificationInstruction(result.runtime)}`
|
|
2363
2853
|
];
|
|
@@ -2367,16 +2857,38 @@ function completionHandoffLines(result) {
|
|
|
2367
2857
|
"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."
|
|
2368
2858
|
];
|
|
2369
2859
|
}
|
|
2860
|
+
if (approval === "ended") {
|
|
2861
|
+
return [
|
|
2862
|
+
"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."
|
|
2863
|
+
];
|
|
2864
|
+
}
|
|
2370
2865
|
const profile = runtimeProfile(result.runtime);
|
|
2866
|
+
const activation = activationInstructionWithWhy(profile);
|
|
2867
|
+
if (approval === "approved") {
|
|
2868
|
+
return [
|
|
2869
|
+
"Next steps \u2014 the budget is already approved, so there is nothing more to do in Haven:",
|
|
2870
|
+
`1. ${activation}`,
|
|
2871
|
+
`2. ${runtimeVerificationInstruction(result.runtime)}`
|
|
2872
|
+
];
|
|
2873
|
+
}
|
|
2371
2874
|
return [
|
|
2372
2875
|
"Next steps:",
|
|
2373
|
-
"1. Return to Haven and approve the
|
|
2374
|
-
`2. ${
|
|
2876
|
+
"1. Return to Haven and approve the budget. Approval \u2014 not restarting \u2014 unlocks Haven tools.",
|
|
2877
|
+
`2. ${activation}`,
|
|
2375
2878
|
`3. ${runtimeVerificationInstruction(result.runtime)}`
|
|
2376
2879
|
];
|
|
2377
2880
|
}
|
|
2378
|
-
function
|
|
2379
|
-
|
|
2881
|
+
function activationInstructionWithWhy(profile) {
|
|
2882
|
+
if (profile.restartMode === "restart-session") {
|
|
2883
|
+
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.)`;
|
|
2884
|
+
}
|
|
2885
|
+
if (profile.restartMode === "restart-app") {
|
|
2886
|
+
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.)`;
|
|
2887
|
+
}
|
|
2888
|
+
return profile.activationInstruction;
|
|
2889
|
+
}
|
|
2890
|
+
function printNextSteps(result, log, approval) {
|
|
2891
|
+
for (const line of completionHandoffLines(result, approval)) log(line);
|
|
2380
2892
|
}
|
|
2381
2893
|
|
|
2382
2894
|
// src/args.ts
|
|
@@ -2387,12 +2899,18 @@ function parseArgs(argv, env = process.env) {
|
|
|
2387
2899
|
};
|
|
2388
2900
|
let help = false;
|
|
2389
2901
|
let json = false;
|
|
2902
|
+
let doctor = false;
|
|
2903
|
+
let repair = false;
|
|
2390
2904
|
for (let i = 0; i < argv.length; i += 1) {
|
|
2391
2905
|
const arg = argv[i];
|
|
2392
2906
|
if (arg === "--help" || arg === "-h") {
|
|
2393
2907
|
help = true;
|
|
2394
2908
|
} else if (arg === "--json") {
|
|
2395
2909
|
json = true;
|
|
2910
|
+
} else if (arg === "--doctor") {
|
|
2911
|
+
doctor = true;
|
|
2912
|
+
} else if (arg === "--repair") {
|
|
2913
|
+
repair = true;
|
|
2396
2914
|
} else if (arg === "--setup" || arg === "--setup-token") {
|
|
2397
2915
|
options.setupToken = requireValue(argv, ++i, arg);
|
|
2398
2916
|
} else if (arg === "--api" || arg === "--api-url") {
|
|
@@ -2419,7 +2937,13 @@ function parseArgs(argv, env = process.env) {
|
|
|
2419
2937
|
}
|
|
2420
2938
|
}
|
|
2421
2939
|
if (help) {
|
|
2422
|
-
return { options, help, json };
|
|
2940
|
+
return { options, help, json, doctor, repair };
|
|
2941
|
+
}
|
|
2942
|
+
if (doctor || repair) {
|
|
2943
|
+
if (!options.runtime) {
|
|
2944
|
+
throw new Error("--doctor/--repair need --runtime <runtime> (which config to examine).");
|
|
2945
|
+
}
|
|
2946
|
+
return { options, help, json, doctor, repair };
|
|
2423
2947
|
}
|
|
2424
2948
|
if (!options.setupToken) {
|
|
2425
2949
|
throw new Error("Missing --setup <hv_setup_...> setup token.");
|
|
@@ -2428,7 +2952,7 @@ function parseArgs(argv, env = process.env) {
|
|
|
2428
2952
|
throw new Error("Missing --api <Haven API URL>.");
|
|
2429
2953
|
}
|
|
2430
2954
|
options.apiBaseUrl = options.apiBaseUrl.replace(/\/+$/, "");
|
|
2431
|
-
return { options, help, json };
|
|
2955
|
+
return { options, help, json, doctor, repair };
|
|
2432
2956
|
}
|
|
2433
2957
|
function helpText() {
|
|
2434
2958
|
return [
|
|
@@ -2451,6 +2975,11 @@ function helpText() {
|
|
|
2451
2975
|
" --local Advanced: install the fully-local Haven MCP (no hosted dependency).",
|
|
2452
2976
|
" Only available for Claude Code and Codex. Default is hosted MCP + local signer.",
|
|
2453
2977
|
" --json Emit one versioned, secret-free result object on stdout; progress stays on stderr.",
|
|
2978
|
+
" --doctor Diagnose an existing setup (read-only, no token): config, credentials,",
|
|
2979
|
+
" signer runtime, hosted MCP, and a live signer handshake. Exits non-zero on any failure.",
|
|
2980
|
+
" --repair Repair, then re-diagnose (implies --doctor): reinstall the pinned signer",
|
|
2981
|
+
" runtime, rewrite the wrapper and runtime config from stored credentials.",
|
|
2982
|
+
" Hosted topology only (refuses to touch a --local config). No keys, no token.",
|
|
2454
2983
|
" --help Show this help.",
|
|
2455
2984
|
"",
|
|
2456
2985
|
"The connector never prints the private key and never sends it to Haven. JSON output never includes credential contents or full credential paths."
|
|
@@ -2488,6 +3017,37 @@ async function runCli(argv, io = {
|
|
|
2488
3017
|
`);
|
|
2489
3018
|
return 0;
|
|
2490
3019
|
}
|
|
3020
|
+
if (parsed.doctor || parsed.repair) {
|
|
3021
|
+
const { runDoctor: runDoctor2, runRepair: runRepair2 } = await Promise.resolve().then(() => (init_doctor(), doctor_exports));
|
|
3022
|
+
const runtime = parsed.options.runtime ?? "";
|
|
3023
|
+
const credentialsDir = parsed.options.credentialsDir;
|
|
3024
|
+
try {
|
|
3025
|
+
if (parsed.repair) {
|
|
3026
|
+
const repair = await runRepair2({ runtime, credentialsDir });
|
|
3027
|
+
for (const message of repair.messages) io.stderr(`${redactSecrets(message)}
|
|
3028
|
+
`);
|
|
3029
|
+
if (!repair.ok) return 1;
|
|
3030
|
+
}
|
|
3031
|
+
const report = await runDoctor2({ runtime, credentialsDir });
|
|
3032
|
+
if (parsed.json) {
|
|
3033
|
+
io.stdout(`${redactSecrets(JSON.stringify(report))}
|
|
3034
|
+
`);
|
|
3035
|
+
} else {
|
|
3036
|
+
for (const check of report.checks) {
|
|
3037
|
+
io.stdout(redactSecrets(`${check.ok ? "\u2713" : "\u2717"} ${check.label}: ${check.detail}
|
|
3038
|
+
`));
|
|
3039
|
+
if (check.repair) io.stdout(redactSecrets(` \u21B3 repair: ${check.repair}
|
|
3040
|
+
`));
|
|
3041
|
+
}
|
|
3042
|
+
io.stdout(report.ok ? "All checks passed.\n" : "One or more checks FAILED \u2014 see repairs above.\n");
|
|
3043
|
+
}
|
|
3044
|
+
return report.ok ? 0 : 1;
|
|
3045
|
+
} catch (err) {
|
|
3046
|
+
io.stderr(`${redactSecrets(err instanceof Error ? err.message : String(err))}
|
|
3047
|
+
`);
|
|
3048
|
+
return 1;
|
|
3049
|
+
}
|
|
3050
|
+
}
|
|
2491
3051
|
try {
|
|
2492
3052
|
const result = await runConnect(
|
|
2493
3053
|
{ ...parsed.options, waitForApproval: !parsed.json },
|