@haven_ai/connect 0.1.22-alpha.0 → 0.1.23-alpha.1
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 +47 -7
- package/dist/cli.cjs +310 -63
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.cts +14 -0
- package/dist/cli.d.ts +14 -0
- package/dist/cli.js +308 -63
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +315 -53
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +101 -6
- package/dist/index.d.ts +101 -6
- package/dist/index.js +312 -55
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
package/dist/cli.d.cts
CHANGED
|
@@ -1 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
interface CliIo {
|
|
3
|
+
stdout: (message: string) => void;
|
|
4
|
+
stderr: (message: string) => void;
|
|
5
|
+
}
|
|
6
|
+
declare function runCli(argv: string[], io?: CliIo): Promise<number>;
|
|
7
|
+
/**
|
|
8
|
+
* npm executes package bins through a `node_modules/.bin` symlink. Node keeps
|
|
9
|
+
* that symlink in `process.argv[1]`, whereas `import.meta.url` identifies the
|
|
10
|
+
* real module path. Resolve both sides before comparing so a published
|
|
11
|
+
* `haven-connect` bin starts, while an ordinary `runCli` import remains inert.
|
|
12
|
+
*/
|
|
13
|
+
declare function isCliEntrypoint(argvPath?: string | undefined, moduleUrl?: string): boolean;
|
|
14
|
+
|
|
15
|
+
export { type CliIo, isCliEntrypoint, runCli };
|
package/dist/cli.d.ts
CHANGED
|
@@ -1 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
interface CliIo {
|
|
3
|
+
stdout: (message: string) => void;
|
|
4
|
+
stderr: (message: string) => void;
|
|
5
|
+
}
|
|
6
|
+
declare function runCli(argv: string[], io?: CliIo): Promise<number>;
|
|
7
|
+
/**
|
|
8
|
+
* npm executes package bins through a `node_modules/.bin` symlink. Node keeps
|
|
9
|
+
* that symlink in `process.argv[1]`, whereas `import.meta.url` identifies the
|
|
10
|
+
* real module path. Resolve both sides before comparing so a published
|
|
11
|
+
* `haven-connect` bin starts, while an ordinary `runCli` import remains inert.
|
|
12
|
+
*/
|
|
13
|
+
declare function isCliEntrypoint(argvPath?: string | undefined, moduleUrl?: string): boolean;
|
|
14
|
+
|
|
15
|
+
export { type CliIo, isCliEntrypoint, runCli };
|
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { realpathSync } from 'fs';
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from 'url';
|
|
4
|
+
import { HAVEN_MINIMUM_NODE_VERSION, isSupportedNodeVersion, unsupportedNodeVersionMessage, SKILL_FOLDER_NAME, HAVEN_SKILL_MD, resolveTokenFromAddress } from '@haven_ai/sdk';
|
|
2
5
|
import crypto from 'crypto';
|
|
3
6
|
import { Wallet } from 'ethers';
|
|
4
7
|
import { mkdir, rm, chmod, access, writeFile, readFile, unlink } from 'fs/promises';
|
|
@@ -8,7 +11,6 @@ import { execFile, spawn } from 'child_process';
|
|
|
8
11
|
import { promisify } from 'util';
|
|
9
12
|
import { parseDocument, isMap, stringify } from 'yaml';
|
|
10
13
|
import { registeredToolNames, MCP_VERSION, ensureConsent, computeConsentHash, loadCredentials, consentInputFromClient } from '@haven_ai/mcp';
|
|
11
|
-
import { HAVEN_MINIMUM_NODE_VERSION, isSupportedNodeVersion, unsupportedNodeVersionMessage, SKILL_FOLDER_NAME, HAVEN_SKILL_MD } from '@haven_ai/sdk';
|
|
12
14
|
import { ensureSignerConsent, computeSignerConsentHash, loadSignerCredentials, createEdgeSigner, toolSchemas } from '@haven_ai/signer';
|
|
13
15
|
|
|
14
16
|
// src/api.ts
|
|
@@ -41,6 +43,10 @@ function createConnectApiClient(baseUrl, fetchImpl = fetch) {
|
|
|
41
43
|
}
|
|
42
44
|
})
|
|
43
45
|
}),
|
|
46
|
+
getConnectorStatus: (setupId, apiKey) => request(fetchImpl, `${root}/agent-connection-setups/${encodeURIComponent(setupId)}/connector-status`, {
|
|
47
|
+
method: "GET",
|
|
48
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
49
|
+
}),
|
|
44
50
|
updateInstallStatus: async (setupId, apiKey, input) => {
|
|
45
51
|
await request(fetchImpl, `${root}/agent-connection-setups/${encodeURIComponent(setupId)}/install-status`, {
|
|
46
52
|
method: "POST",
|
|
@@ -67,6 +73,14 @@ function createConnectApiClient(baseUrl, fetchImpl = fetch) {
|
|
|
67
73
|
}
|
|
68
74
|
};
|
|
69
75
|
}
|
|
76
|
+
var ConnectRequestError = class extends Error {
|
|
77
|
+
constructor(message, status) {
|
|
78
|
+
super(message);
|
|
79
|
+
this.status = status;
|
|
80
|
+
this.name = "ConnectRequestError";
|
|
81
|
+
}
|
|
82
|
+
status;
|
|
83
|
+
};
|
|
70
84
|
async function request(fetchImpl, url, init) {
|
|
71
85
|
const response = await fetchImpl(url, {
|
|
72
86
|
...init,
|
|
@@ -79,7 +93,7 @@ async function request(fetchImpl, url, init) {
|
|
|
79
93
|
const body = text ? JSON.parse(text) : null;
|
|
80
94
|
if (!response.ok) {
|
|
81
95
|
const message = body?.error ?? body?.message ?? `${response.status} ${response.statusText}`;
|
|
82
|
-
throw new
|
|
96
|
+
throw new ConnectRequestError(`Haven setup request failed: ${message}`, response.status);
|
|
83
97
|
}
|
|
84
98
|
return body;
|
|
85
99
|
}
|
|
@@ -175,11 +189,12 @@ async function writeCredentialFiles(input) {
|
|
|
175
189
|
agentPath,
|
|
176
190
|
{
|
|
177
191
|
agent_id: input.agentId,
|
|
192
|
+
delegate_address: input.delegateAddress,
|
|
178
193
|
safe_address: input.safeAddress,
|
|
179
194
|
chain_id: input.chainId,
|
|
180
195
|
network: input.network,
|
|
181
196
|
agent_budget: input.agentBudget,
|
|
182
|
-
note: "Non-secret orientation for the agent: identity + configured budget. Contains no API key or signing key. For the live remaining budget, call haven_get_allowances."
|
|
197
|
+
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."
|
|
183
198
|
},
|
|
184
199
|
input.warn
|
|
185
200
|
);
|
|
@@ -231,9 +246,9 @@ var MCP_RUNTIME_MANIFEST = {
|
|
|
231
246
|
mcpPackage: "@haven_ai/mcp",
|
|
232
247
|
mcpVersion: MCP_VERSION,
|
|
233
248
|
sdkPackage: "@haven_ai/sdk",
|
|
234
|
-
sdkVersion: "0.1.
|
|
249
|
+
sdkVersion: "0.1.23-alpha.1",
|
|
235
250
|
signerPackage: "@haven_ai/signer",
|
|
236
|
-
signerVersion: "0.1.
|
|
251
|
+
signerVersion: "0.1.23-alpha.1",
|
|
237
252
|
// Sourced from the SDK, never a literal (#1161). This field read '20.0.0'
|
|
238
253
|
// while every package's `engines` said `>=24` and the docs said `>=24.0.0`,
|
|
239
254
|
// so the guard that was supposed to enforce the floor waved Node v23 through
|
|
@@ -1395,55 +1410,64 @@ var RUNTIME_PROFILES = {
|
|
|
1395
1410
|
id: "claude-code",
|
|
1396
1411
|
label: "Claude Code",
|
|
1397
1412
|
restartMode: "restart-session",
|
|
1398
|
-
canWriteRuntimeConfig: true
|
|
1413
|
+
canWriteRuntimeConfig: true,
|
|
1414
|
+
activationInstruction: "Start a new Claude Code session so it loads the Haven MCP entries."
|
|
1399
1415
|
},
|
|
1400
1416
|
"codex-cli": {
|
|
1401
1417
|
id: "codex-cli",
|
|
1402
1418
|
label: "Codex CLI",
|
|
1403
1419
|
restartMode: "restart-session",
|
|
1404
|
-
canWriteRuntimeConfig: true
|
|
1420
|
+
canWriteRuntimeConfig: true,
|
|
1421
|
+
activationInstruction: "Start a fresh Codex CLI session (for example, run `codex resume --last`)."
|
|
1405
1422
|
},
|
|
1406
1423
|
"codex-desktop": {
|
|
1407
1424
|
id: "codex-desktop",
|
|
1408
1425
|
label: "Codex Desktop",
|
|
1409
1426
|
restartMode: "restart-session",
|
|
1410
|
-
canWriteRuntimeConfig: true
|
|
1427
|
+
canWriteRuntimeConfig: true,
|
|
1428
|
+
activationInstruction: "Quit and reopen Codex Desktop so it loads the Haven MCP entries."
|
|
1411
1429
|
},
|
|
1412
1430
|
cursor: {
|
|
1413
1431
|
id: "cursor",
|
|
1414
1432
|
label: "Cursor",
|
|
1415
1433
|
restartMode: "hot-reload",
|
|
1416
|
-
canWriteRuntimeConfig: true
|
|
1434
|
+
canWriteRuntimeConfig: true,
|
|
1435
|
+
activationInstruction: "Wait for Cursor to hot-reload the Haven MCP entries; no app restart is required."
|
|
1417
1436
|
},
|
|
1418
1437
|
vscode: {
|
|
1419
1438
|
id: "vscode",
|
|
1420
1439
|
label: "VS Code",
|
|
1421
1440
|
restartMode: "hot-reload",
|
|
1422
|
-
canWriteRuntimeConfig: true
|
|
1441
|
+
canWriteRuntimeConfig: true,
|
|
1442
|
+
activationInstruction: "Wait for VS Code to hot-reload the Haven MCP entries; no app restart is required."
|
|
1423
1443
|
},
|
|
1424
1444
|
"vscode-insiders": {
|
|
1425
1445
|
id: "vscode-insiders",
|
|
1426
1446
|
label: "VS Code Insiders",
|
|
1427
1447
|
restartMode: "hot-reload",
|
|
1428
|
-
canWriteRuntimeConfig: true
|
|
1448
|
+
canWriteRuntimeConfig: true,
|
|
1449
|
+
activationInstruction: "Wait for VS Code Insiders to hot-reload the Haven MCP entries; no app restart is required."
|
|
1429
1450
|
},
|
|
1430
1451
|
"claude-desktop": {
|
|
1431
1452
|
id: "claude-desktop",
|
|
1432
1453
|
label: "Claude Desktop",
|
|
1433
1454
|
restartMode: "restart-app",
|
|
1434
|
-
canWriteRuntimeConfig: true
|
|
1455
|
+
canWriteRuntimeConfig: true,
|
|
1456
|
+
activationInstruction: "Quit and reopen Claude Desktop so it loads the Haven MCP entries."
|
|
1435
1457
|
},
|
|
1436
1458
|
hermes: {
|
|
1437
1459
|
id: "hermes",
|
|
1438
1460
|
label: "Hermes Agent",
|
|
1439
1461
|
restartMode: "restart-session",
|
|
1440
|
-
canWriteRuntimeConfig: true
|
|
1462
|
+
canWriteRuntimeConfig: true,
|
|
1463
|
+
activationInstruction: "Start a new Hermes session; in Hermes Gateway, run `/restart` instead."
|
|
1441
1464
|
},
|
|
1442
1465
|
other: {
|
|
1443
1466
|
id: "other",
|
|
1444
1467
|
label: "Other agent runtime",
|
|
1445
1468
|
restartMode: "manual",
|
|
1446
|
-
canWriteRuntimeConfig: false
|
|
1469
|
+
canWriteRuntimeConfig: false,
|
|
1470
|
+
activationInstruction: "Finish the manual MCP setup shown above, then start a fresh session in that runtime."
|
|
1447
1471
|
}
|
|
1448
1472
|
};
|
|
1449
1473
|
var RUNTIME_ALIASES = {
|
|
@@ -1495,8 +1519,9 @@ function restartRequiredForRuntime(runtime, env = process.env) {
|
|
|
1495
1519
|
const mode = runtimeProfile(runtime, env).restartMode;
|
|
1496
1520
|
return mode === "restart-session" || mode === "restart-app";
|
|
1497
1521
|
}
|
|
1498
|
-
function
|
|
1499
|
-
|
|
1522
|
+
function runtimeVerificationInstruction(runtime) {
|
|
1523
|
+
const label = RUNTIME_PROFILES[runtime].label;
|
|
1524
|
+
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.`;
|
|
1500
1525
|
}
|
|
1501
1526
|
function normalizeRuntimeName(runtime) {
|
|
1502
1527
|
const key = runtime?.trim().toLowerCase();
|
|
@@ -1682,11 +1707,12 @@ async function installRuntime(input, deps = {}) {
|
|
|
1682
1707
|
probeLocalSignerCredential(input.signerPath),
|
|
1683
1708
|
localProbePromise
|
|
1684
1709
|
]);
|
|
1685
|
-
const hostedOk = configResult.hostedConfigured && hostedProbe.status
|
|
1710
|
+
const hostedOk = configResult.hostedConfigured && hostedProbe.status === "ok";
|
|
1686
1711
|
const localMcpOk = configResult.runtimeMcpMode === "local_stdio" && configResult.localMcpConfigured && signerCredentialReady && Boolean(localMcpConsent?.acknowledged) && localMcpProbe?.status === "ok";
|
|
1687
1712
|
const signerOk = configResult.runtimeMcpMode === "local_stdio" ? localMcpOk : configResult.signerConfigured && signerCredentialReady && Boolean(signerConsent?.acknowledged);
|
|
1688
1713
|
const restartRequired = configResult.restartRequired || restartRequiredForRuntime(runtime, deps.env);
|
|
1689
|
-
const errorCode = configResult.errorCode ?? (configResult.runtimeMcpMode === "local_stdio" ? localMcpErrorCode(signerCredentialReady, localMcpConsent, localMcpProbe?.status) : signerConsentErrorCode(signerCredentialReady, signerConsent));
|
|
1714
|
+
const errorCode = configResult.errorCode ?? (configResult.runtimeMcpMode === "local_stdio" ? localMcpErrorCode(signerCredentialReady, localMcpConsent, localMcpProbe?.status) : hostedMcpErrorCode(configResult.hostedConfigured, hostedProbe.status) ?? signerConsentErrorCode(signerCredentialReady, signerConsent));
|
|
1715
|
+
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."] : [];
|
|
1690
1716
|
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."] : [];
|
|
1691
1717
|
const skillInstall = runtime === "claude-code" && !configResult.errorCode ? await installClaudeSkill(deps.homeDir) : void 0;
|
|
1692
1718
|
return {
|
|
@@ -1705,7 +1731,7 @@ async function installRuntime(input, deps = {}) {
|
|
|
1705
1731
|
activationCommand: configResult.activationCommand,
|
|
1706
1732
|
skillInstalled: skillInstall?.installed,
|
|
1707
1733
|
signerRuntimePrepared,
|
|
1708
|
-
messages: [...consentMessages, ...localRuntimeInstall?.messages ?? [], ...configResult.messages, ...localProbeMessages, ...skillInstall?.messages ?? []]
|
|
1734
|
+
messages: [...consentMessages, ...localRuntimeInstall?.messages ?? [], ...configResult.messages, ...hostedProbeMessages, ...localProbeMessages, ...skillInstall?.messages ?? []]
|
|
1709
1735
|
};
|
|
1710
1736
|
}
|
|
1711
1737
|
function runtimeInstallCapabilities(runtime, env = process.env) {
|
|
@@ -1866,6 +1892,10 @@ function signerConsentErrorCode(signerCredentialReady, signerConsent) {
|
|
|
1866
1892
|
if (!signerConsent?.acknowledged) return "local_signer_ack_required";
|
|
1867
1893
|
return void 0;
|
|
1868
1894
|
}
|
|
1895
|
+
function hostedMcpErrorCode(hostedConfigured, hostedProbeStatus) {
|
|
1896
|
+
if (!hostedConfigured || hostedProbeStatus === "ok") return void 0;
|
|
1897
|
+
return `hosted_mcp_probe_${hostedProbeStatus}`;
|
|
1898
|
+
}
|
|
1869
1899
|
function localMcpErrorCode(signerCredentialReady, localMcpConsent, localMcpProbeStatus) {
|
|
1870
1900
|
if (!signerCredentialReady) return "local_signer_credential_unavailable";
|
|
1871
1901
|
if (!localMcpConsent?.acknowledged) return "local_mcp_ack_required";
|
|
@@ -1917,13 +1947,17 @@ function localRuntimePrepareErrorCode(err) {
|
|
|
1917
1947
|
}
|
|
1918
1948
|
|
|
1919
1949
|
// src/runtime.ts
|
|
1920
|
-
var CONNECTOR_VERSION = "0.1.
|
|
1950
|
+
var CONNECTOR_VERSION = "0.1.23-alpha.1";
|
|
1951
|
+
var CONNECT_OUTCOME_SCHEMA_VERSION = 1;
|
|
1921
1952
|
async function runConnect(options, deps = {}) {
|
|
1922
1953
|
assertSupportedNodeVersion(deps.nodeVersion, MCP_RUNTIME_MANIFEST.minimumNodeVersion);
|
|
1923
1954
|
const connectorVersion = options.connectorVersion ?? CONNECTOR_VERSION;
|
|
1924
1955
|
const api = deps.api ?? createConnectApiClient(options.apiBaseUrl);
|
|
1925
|
-
const log = secureLogger(
|
|
1926
|
-
|
|
1956
|
+
const log = secureLogger(
|
|
1957
|
+
deps.log ?? ((message) => process.stdout.write(`${message}
|
|
1958
|
+
`)),
|
|
1959
|
+
deps.redactPaths === true
|
|
1960
|
+
);
|
|
1927
1961
|
const writeCredentials = deps.writeCredentials ?? writeCredentialFiles;
|
|
1928
1962
|
const preflightStorage = deps.preflightStorage ?? preflightCredentialStorage;
|
|
1929
1963
|
const runRuntimeInstall = deps.installRuntime ?? installRuntime;
|
|
@@ -1944,6 +1978,7 @@ async function runConnect(options, deps = {}) {
|
|
|
1944
1978
|
connectorVersion,
|
|
1945
1979
|
runtime: options.runtime
|
|
1946
1980
|
});
|
|
1981
|
+
assertSetupChallengeIsUsable(setup.challenge.expires_at);
|
|
1947
1982
|
printSetupSummary(setup, log);
|
|
1948
1983
|
await preflightStorage({ baseDir: options.credentialsDir, warn: log });
|
|
1949
1984
|
log("Checked local credential storage \u2014 all clear.");
|
|
@@ -1952,21 +1987,31 @@ async function runConnect(options, deps = {}) {
|
|
|
1952
1987
|
log("Minting a fresh signing key and API key \u2014 both stay on this machine.");
|
|
1953
1988
|
const proofSignature = await localKey.signChallenge(setup.challenge.message);
|
|
1954
1989
|
log("Introducing your agent to Haven\u2026");
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1990
|
+
let registration;
|
|
1991
|
+
try {
|
|
1992
|
+
registration = await api.registerSetup({
|
|
1993
|
+
setupToken: options.setupToken,
|
|
1994
|
+
connectorVersion,
|
|
1995
|
+
runtime: options.runtime,
|
|
1996
|
+
challengeId: setup.challenge.id,
|
|
1997
|
+
delegateAddress: localKey.address,
|
|
1998
|
+
proofSignature,
|
|
1999
|
+
apiKeyHash: hashAgentApiKey(localApiKey),
|
|
2000
|
+
apiKeyPrefix: agentApiKeyPrefix(localApiKey),
|
|
2001
|
+
connectorContext: {
|
|
2002
|
+
environment_label: options.environmentLabel ?? "Local workspace",
|
|
2003
|
+
config_target: installCapabilities.canWriteRuntimeConfig ? "agent runtime MCP config" : "local credential files"
|
|
2004
|
+
},
|
|
2005
|
+
installCapabilities
|
|
2006
|
+
});
|
|
2007
|
+
} catch (err) {
|
|
2008
|
+
if (isExpiredSetupChallenge(err)) {
|
|
2009
|
+
throw new Error(
|
|
2010
|
+
"The Haven setup challenge expired while connecting. Return to Haven, start a fresh connection, and run its new Connect command. Do not reuse or paste credentials."
|
|
2011
|
+
);
|
|
2012
|
+
}
|
|
2013
|
+
throw err;
|
|
2014
|
+
}
|
|
1970
2015
|
log(`Registered signing address with Haven: ${shortAddress(registration.delegate_address)}.`);
|
|
1971
2016
|
log("Tucking your credentials away safely on disk\u2026");
|
|
1972
2017
|
const credentialPaths = await writeCredentials({
|
|
@@ -2016,6 +2061,9 @@ async function runConnect(options, deps = {}) {
|
|
|
2016
2061
|
} else {
|
|
2017
2062
|
log("Haven setup on this machine is complete.");
|
|
2018
2063
|
}
|
|
2064
|
+
if (options.waitForApproval !== false && !runtimeInstall.errorCode) {
|
|
2065
|
+
await waitForBudgetApproval(api, registration.setup_id, localApiKey, log, options.approvalWait);
|
|
2066
|
+
}
|
|
2019
2067
|
printNextSteps(runtimeInstall, log);
|
|
2020
2068
|
try {
|
|
2021
2069
|
await api.updateInstallStatus(registration.setup_id, localApiKey, {
|
|
@@ -2043,7 +2091,69 @@ async function runConnect(options, deps = {}) {
|
|
|
2043
2091
|
setupId: registration.setup_id,
|
|
2044
2092
|
agentId: registration.agent_id,
|
|
2045
2093
|
delegateAddress: registration.delegate_address,
|
|
2046
|
-
credentialPaths
|
|
2094
|
+
credentialPaths,
|
|
2095
|
+
outcome: completionOutcome({
|
|
2096
|
+
runtimeInstall,
|
|
2097
|
+
delegateAddress: registration.delegate_address,
|
|
2098
|
+
setupChallengeExpiresAt: setup.challenge.expires_at,
|
|
2099
|
+
approvalRequired: registration.agent_status === "pending_approval"
|
|
2100
|
+
})
|
|
2101
|
+
};
|
|
2102
|
+
}
|
|
2103
|
+
function completionOutcome(input) {
|
|
2104
|
+
const { runtimeInstall } = input;
|
|
2105
|
+
const manualSetup = runtimeInstall.errorCode === "manual_runtime_setup_required";
|
|
2106
|
+
const nextAction2 = runtimeInstall.nextUserAction;
|
|
2107
|
+
const outcome = {
|
|
2108
|
+
schema_version: CONNECT_OUTCOME_SCHEMA_VERSION,
|
|
2109
|
+
outcome: runtimeInstall.errorCode ? "action_required" : "complete",
|
|
2110
|
+
runtime: runtimeInstall.runtime,
|
|
2111
|
+
topology: runtimeInstall.runtimeMcpMode,
|
|
2112
|
+
configuration: {
|
|
2113
|
+
hosted_mcp: runtimeInstall.hostedMcpConfigured,
|
|
2114
|
+
local_signer: runtimeInstall.localSignerConfigured,
|
|
2115
|
+
local_mcp: runtimeInstall.localMcpConfigured
|
|
2116
|
+
},
|
|
2117
|
+
probe: { result: runtimeInstall.probeResult },
|
|
2118
|
+
activation: {
|
|
2119
|
+
restart_required: runtimeInstall.restartRequired,
|
|
2120
|
+
instruction: manualSetup ? "Finish the manual MCP setup using the secret-free references shown in normal Connect output, then start a fresh session." : runtimeProfile(runtimeInstall.runtime).activationInstruction
|
|
2121
|
+
},
|
|
2122
|
+
next_action: nextAction2,
|
|
2123
|
+
approval: { required: input.approvalRequired, expires_at: null },
|
|
2124
|
+
verification: {
|
|
2125
|
+
tools: ["haven_get_agent", "haven_get_allowances"],
|
|
2126
|
+
instruction: runtimeVerificationInstruction(runtimeInstall.runtime)
|
|
2127
|
+
},
|
|
2128
|
+
// The backend contract supplies a 20-byte address. If an unexpected
|
|
2129
|
+
// malformed value arrives, do not echo it into an automation-facing
|
|
2130
|
+
// record; the human log has already been redacted separately.
|
|
2131
|
+
delegate_address: /^0x[0-9a-fA-F]{40}$/.test(input.delegateAddress) ? shortAddress(input.delegateAddress) : "[delegate-address-redacted]",
|
|
2132
|
+
...input.setupChallengeExpiresAt ? { setup_challenge_expires_at: input.setupChallengeExpiresAt } : {},
|
|
2133
|
+
...runtimeInstall.errorCode ? { error: { code: runtimeInstall.errorCode, next_action: nextAction2 } } : {}
|
|
2134
|
+
};
|
|
2135
|
+
return outcome;
|
|
2136
|
+
}
|
|
2137
|
+
function failedConnectOutcome(runtimeHint, error) {
|
|
2138
|
+
const message = error instanceof Error ? error.message : "";
|
|
2139
|
+
const code = /Node\.js >=/i.test(message) ? "unsupported_node_version" : /setup challenge.*expired|expired or invalid/i.test(message) ? "setup_challenge_expired_or_invalid" : /only available for Claude Code and Codex/i.test(message) ? "local_mcp_unsupported_runtime" : "connect_failed";
|
|
2140
|
+
const runtime = normalizeRuntime(runtimeHint);
|
|
2141
|
+
const nextAction2 = code === "setup_challenge_expired_or_invalid" ? "return_to_haven_for_fresh_setup" : code === "unsupported_node_version" ? "install_supported_node_and_rerun_connect" : code === "local_mcp_unsupported_runtime" ? "rerun_without_local_mcp" : "review_the_safe_error_output_and_start_a_fresh_haven_setup_if_needed";
|
|
2142
|
+
return {
|
|
2143
|
+
schema_version: CONNECT_OUTCOME_SCHEMA_VERSION,
|
|
2144
|
+
outcome: "failed",
|
|
2145
|
+
runtime,
|
|
2146
|
+
topology: "unknown",
|
|
2147
|
+
configuration: { hosted_mcp: false, local_signer: false, local_mcp: false },
|
|
2148
|
+
probe: { result: "not_run" },
|
|
2149
|
+
activation: { restart_required: false, instruction: "Resolve the reported problem before activating Haven tools." },
|
|
2150
|
+
next_action: nextAction2,
|
|
2151
|
+
approval: { required: false, expires_at: null },
|
|
2152
|
+
verification: {
|
|
2153
|
+
tools: ["haven_get_agent", "haven_get_allowances"],
|
|
2154
|
+
instruction: "After a successful setup and activation, verify only with haven_get_agent and haven_get_allowances."
|
|
2155
|
+
},
|
|
2156
|
+
error: { code, next_action: nextAction2 }
|
|
2047
2157
|
};
|
|
2048
2158
|
}
|
|
2049
2159
|
function printSetupSummary(setup, log) {
|
|
@@ -2056,10 +2166,26 @@ function printSetupSummary(setup, log) {
|
|
|
2056
2166
|
);
|
|
2057
2167
|
}
|
|
2058
2168
|
}
|
|
2059
|
-
log(`Setup challenge expires at ${setup.challenge.expires_at}.`);
|
|
2169
|
+
log(`Setup challenge expires at ${setup.challenge.expires_at}. If it expires, return to Haven for a fresh setup and rerun Connect \u2014 do not reuse or paste credentials.`);
|
|
2170
|
+
}
|
|
2171
|
+
function assertSetupChallengeIsUsable(expiresAt) {
|
|
2172
|
+
const expiresAtMs = Date.parse(expiresAt);
|
|
2173
|
+
if (!Number.isNaN(expiresAtMs) && expiresAtMs > Date.now()) return;
|
|
2174
|
+
throw new Error(
|
|
2175
|
+
"This Haven setup challenge is expired or invalid. Return to Haven, start a fresh connection, and rerun Connect. No local credentials were written."
|
|
2176
|
+
);
|
|
2177
|
+
}
|
|
2178
|
+
function isExpiredSetupChallenge(err) {
|
|
2179
|
+
return err instanceof Error && /(?:setup )?challenge.*expir|expir.*(?:setup )?challenge/i.test(err.message);
|
|
2060
2180
|
}
|
|
2061
|
-
function secureLogger(log) {
|
|
2062
|
-
return (message) =>
|
|
2181
|
+
function secureLogger(log, redactPaths = false) {
|
|
2182
|
+
return (message) => {
|
|
2183
|
+
let safe = redactSecrets(message);
|
|
2184
|
+
if (redactPaths) {
|
|
2185
|
+
safe = safe.replace(/(?:~|\/)[^\s`"']*\/(?:identity|signer|agent)\.json\b/g, "[credential-file-redacted]").replace(/(?:~|\/)[^\s`"']*\/\.env\b/g, "[credential-env-redacted]");
|
|
2186
|
+
}
|
|
2187
|
+
log(safe);
|
|
2188
|
+
};
|
|
2063
2189
|
}
|
|
2064
2190
|
function printRuntimeInstall(result, log) {
|
|
2065
2191
|
for (const message of result.messages) log(message);
|
|
@@ -2076,17 +2202,85 @@ function printRuntimeInstall(result, log) {
|
|
|
2076
2202
|
log("Local Haven signer still needs runtime setup.");
|
|
2077
2203
|
}
|
|
2078
2204
|
}
|
|
2079
|
-
function
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2205
|
+
function formatAtomicAmount(atomic, decimals) {
|
|
2206
|
+
const s = atomic.toString().padStart(decimals + 1, "0");
|
|
2207
|
+
const intPart = s.slice(0, s.length - decimals) || "0";
|
|
2208
|
+
const fracPart = s.slice(s.length - decimals).replace(/0+$/, "");
|
|
2209
|
+
return fracPart ? `${intPart}.${fracPart}` : intPart;
|
|
2210
|
+
}
|
|
2211
|
+
function describeResetPeriod(resetPeriodMin) {
|
|
2212
|
+
if (resetPeriodMin === 1440) return "per day";
|
|
2213
|
+
if (resetPeriodMin === 60) return "per hour";
|
|
2214
|
+
if (resetPeriodMin === 0) return "with no automatic reset";
|
|
2215
|
+
return `per ${resetPeriodMin} minutes`;
|
|
2216
|
+
}
|
|
2217
|
+
function describeApprovedBudget(budget) {
|
|
2218
|
+
const token = resolveTokenFromAddress(budget.token_address);
|
|
2219
|
+
const amount = token ? `${formatAtomicAmount(BigInt(budget.amount), token.decimals)} ${budget.token_symbol}` : `${budget.amount} ${budget.token_symbol} (atomic units)`;
|
|
2220
|
+
return `${amount} ${describeResetPeriod(budget.reset_period_min)}`;
|
|
2221
|
+
}
|
|
2222
|
+
async function waitForBudgetApproval(api, setupId, apiKey, log, options = {}) {
|
|
2223
|
+
const intervalMs = options.intervalMs ?? 5e3;
|
|
2224
|
+
const timeoutMs = options.timeoutMs ?? 18e4;
|
|
2225
|
+
const sleep = options.sleep ?? ((ms) => new Promise((resolve8) => setTimeout(resolve8, ms)));
|
|
2226
|
+
const maxPolls = Math.max(1, Math.floor(timeoutMs / intervalMs));
|
|
2227
|
+
const remindEvery = Math.max(1, Math.floor(3e4 / intervalMs));
|
|
2228
|
+
log("Registered with Haven \u2014 waiting for you to approve the budget in the dashboard\u2026");
|
|
2229
|
+
for (let i = 0; i < maxPolls; i++) {
|
|
2230
|
+
await sleep(intervalMs);
|
|
2231
|
+
let status;
|
|
2232
|
+
try {
|
|
2233
|
+
status = await api.getConnectorStatus(setupId, apiKey);
|
|
2234
|
+
} catch (err) {
|
|
2235
|
+
if (err instanceof ConnectRequestError && (err.status === 401 || err.status === 404)) {
|
|
2236
|
+
log("This setup ended in Haven \u2014 start a fresh connection from the dashboard when ready.");
|
|
2237
|
+
return "ended";
|
|
2238
|
+
}
|
|
2239
|
+
continue;
|
|
2240
|
+
}
|
|
2241
|
+
if (status.status === "active") {
|
|
2242
|
+
log(
|
|
2243
|
+
status.approved_budget ? `Budget approved \u{1F389} \u2014 I can now spend up to ${describeApprovedBudget(status.approved_budget)} from your Haven wallet.` : "Budget approved \u{1F389} \u2014 the agent can now spend within its Haven rules."
|
|
2244
|
+
);
|
|
2245
|
+
return "approved";
|
|
2246
|
+
}
|
|
2247
|
+
if (status.status === "cancelled" || status.status === "expired" || status.status === "failed") {
|
|
2248
|
+
log(`This setup ended in Haven (${status.status}) \u2014 start a fresh connection from the dashboard when ready.`);
|
|
2249
|
+
return "ended";
|
|
2250
|
+
}
|
|
2251
|
+
if ((i + 1) % remindEvery === 0) {
|
|
2252
|
+
log("Still waiting for budget approval in Haven\u2026");
|
|
2086
2253
|
}
|
|
2087
|
-
} else {
|
|
2088
|
-
log("After you approve, the Haven tools should appear in this runtime shortly.");
|
|
2089
2254
|
}
|
|
2255
|
+
log(
|
|
2256
|
+
"Budget approval is still pending in Haven. Approve it in the dashboard whenever you are ready \u2014 the agent tools unlock the moment you do. Verify later with the read-only haven_get_agent tool."
|
|
2257
|
+
);
|
|
2258
|
+
return "pending";
|
|
2259
|
+
}
|
|
2260
|
+
function completionHandoffLines(result) {
|
|
2261
|
+
if (result.errorCode === "manual_runtime_setup_required") {
|
|
2262
|
+
return [
|
|
2263
|
+
"Next steps:",
|
|
2264
|
+
"1. Return to Haven and approve the agent rules. Approval \u2014 not restarting \u2014 unlocks Haven tools.",
|
|
2265
|
+
"2. Finish the manual MCP setup using the secret-free file references printed above, then start a fresh session in your runtime.",
|
|
2266
|
+
`3. ${runtimeVerificationInstruction(result.runtime)}`
|
|
2267
|
+
];
|
|
2268
|
+
}
|
|
2269
|
+
if (result.errorCode) {
|
|
2270
|
+
return [
|
|
2271
|
+
"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."
|
|
2272
|
+
];
|
|
2273
|
+
}
|
|
2274
|
+
const profile = runtimeProfile(result.runtime);
|
|
2275
|
+
return [
|
|
2276
|
+
"Next steps:",
|
|
2277
|
+
"1. Return to Haven and approve the agent rules. Approval \u2014 not restarting \u2014 unlocks Haven tools.",
|
|
2278
|
+
`2. ${profile.activationInstruction}`,
|
|
2279
|
+
`3. ${runtimeVerificationInstruction(result.runtime)}`
|
|
2280
|
+
];
|
|
2281
|
+
}
|
|
2282
|
+
function printNextSteps(result, log) {
|
|
2283
|
+
for (const line of completionHandoffLines(result)) log(line);
|
|
2090
2284
|
}
|
|
2091
2285
|
|
|
2092
2286
|
// src/args.ts
|
|
@@ -2096,10 +2290,13 @@ function parseArgs(argv, env = process.env) {
|
|
|
2096
2290
|
connectorVersion: CONNECTOR_VERSION
|
|
2097
2291
|
};
|
|
2098
2292
|
let help = false;
|
|
2293
|
+
let json = false;
|
|
2099
2294
|
for (let i = 0; i < argv.length; i += 1) {
|
|
2100
2295
|
const arg = argv[i];
|
|
2101
2296
|
if (arg === "--help" || arg === "-h") {
|
|
2102
2297
|
help = true;
|
|
2298
|
+
} else if (arg === "--json") {
|
|
2299
|
+
json = true;
|
|
2103
2300
|
} else if (arg === "--setup" || arg === "--setup-token") {
|
|
2104
2301
|
options.setupToken = requireValue(argv, ++i, arg);
|
|
2105
2302
|
} else if (arg === "--api" || arg === "--api-url") {
|
|
@@ -2126,7 +2323,7 @@ function parseArgs(argv, env = process.env) {
|
|
|
2126
2323
|
}
|
|
2127
2324
|
}
|
|
2128
2325
|
if (help) {
|
|
2129
|
-
return { options, help };
|
|
2326
|
+
return { options, help, json };
|
|
2130
2327
|
}
|
|
2131
2328
|
if (!options.setupToken) {
|
|
2132
2329
|
throw new Error("Missing --setup <hv_setup_...> setup token.");
|
|
@@ -2135,7 +2332,7 @@ function parseArgs(argv, env = process.env) {
|
|
|
2135
2332
|
throw new Error("Missing --api <Haven API URL>.");
|
|
2136
2333
|
}
|
|
2137
2334
|
options.apiBaseUrl = options.apiBaseUrl.replace(/\/+$/, "");
|
|
2138
|
-
return { options, help };
|
|
2335
|
+
return { options, help, json };
|
|
2139
2336
|
}
|
|
2140
2337
|
function helpText() {
|
|
2141
2338
|
return [
|
|
@@ -2157,9 +2354,10 @@ function helpText() {
|
|
|
2157
2354
|
" --ack-signer Backward-compatible alias for --ack-local-tools.",
|
|
2158
2355
|
" --local Advanced: install the fully-local Haven MCP (no hosted dependency).",
|
|
2159
2356
|
" Only available for Claude Code and Codex. Default is hosted MCP + local signer.",
|
|
2357
|
+
" --json Emit one versioned, secret-free result object on stdout; progress stays on stderr.",
|
|
2160
2358
|
" --help Show this help.",
|
|
2161
2359
|
"",
|
|
2162
|
-
"The connector never prints the private key and never sends it to Haven."
|
|
2360
|
+
"The connector never prints the private key and never sends it to Haven. JSON output never includes credential contents or full credential paths."
|
|
2163
2361
|
].join("\n");
|
|
2164
2362
|
}
|
|
2165
2363
|
function requireValue(argv, index, option) {
|
|
@@ -2171,19 +2369,66 @@ function requireValue(argv, index, option) {
|
|
|
2171
2369
|
}
|
|
2172
2370
|
|
|
2173
2371
|
// src/cli.ts
|
|
2174
|
-
async function
|
|
2175
|
-
|
|
2372
|
+
async function runCli(argv, io = {
|
|
2373
|
+
stdout: (message) => process.stdout.write(message),
|
|
2374
|
+
stderr: (message) => process.stderr.write(message)
|
|
2375
|
+
}) {
|
|
2376
|
+
const wantsJson = argv.includes("--json");
|
|
2377
|
+
let parsed;
|
|
2378
|
+
try {
|
|
2379
|
+
parsed = parseArgs(argv);
|
|
2380
|
+
} catch (err) {
|
|
2381
|
+
if (wantsJson) {
|
|
2382
|
+
io.stdout(`${JSON.stringify(failedConnectOutcome(void 0, err))}
|
|
2383
|
+
`);
|
|
2384
|
+
} else {
|
|
2385
|
+
io.stderr(`${redactSecrets(err instanceof Error ? err.message : String(err))}
|
|
2386
|
+
`);
|
|
2387
|
+
}
|
|
2388
|
+
return 1;
|
|
2389
|
+
}
|
|
2176
2390
|
if (parsed.help) {
|
|
2177
|
-
|
|
2391
|
+
io.stdout(`${helpText()}
|
|
2178
2392
|
`);
|
|
2179
|
-
return;
|
|
2393
|
+
return 0;
|
|
2180
2394
|
}
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2395
|
+
try {
|
|
2396
|
+
const result = await runConnect(
|
|
2397
|
+
{ ...parsed.options, waitForApproval: !parsed.json },
|
|
2398
|
+
{
|
|
2399
|
+
log: (message) => (parsed.json ? io.stderr : io.stdout)(`${message}
|
|
2400
|
+
`),
|
|
2401
|
+
redactPaths: parsed.json
|
|
2402
|
+
}
|
|
2403
|
+
);
|
|
2404
|
+
if (parsed.json) io.stdout(`${JSON.stringify(result.outcome)}
|
|
2405
|
+
`);
|
|
2406
|
+
return 0;
|
|
2407
|
+
} catch (err) {
|
|
2408
|
+
if (parsed.json) {
|
|
2409
|
+
io.stdout(`${JSON.stringify(failedConnectOutcome(parsed.options.runtime, err))}
|
|
2410
|
+
`);
|
|
2411
|
+
} else {
|
|
2412
|
+
io.stderr(`${redactSecrets(err instanceof Error ? err.message : String(err))}
|
|
2185
2413
|
`);
|
|
2186
|
-
|
|
2187
|
-
|
|
2414
|
+
}
|
|
2415
|
+
return 1;
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
async function main() {
|
|
2419
|
+
const exitCode = await runCli(process.argv.slice(2));
|
|
2420
|
+
if (exitCode !== 0) process.exitCode = exitCode;
|
|
2421
|
+
}
|
|
2422
|
+
function isCliEntrypoint(argvPath = process.argv[1], moduleUrl = import.meta.url) {
|
|
2423
|
+
if (!argvPath) return false;
|
|
2424
|
+
try {
|
|
2425
|
+
return realpathSync(argvPath) === realpathSync(fileURLToPath(moduleUrl));
|
|
2426
|
+
} catch {
|
|
2427
|
+
return pathToFileURL(argvPath).href === moduleUrl;
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
if (isCliEntrypoint()) void main();
|
|
2431
|
+
|
|
2432
|
+
export { isCliEntrypoint, runCli };
|
|
2188
2433
|
//# sourceMappingURL=cli.js.map
|
|
2189
2434
|
//# sourceMappingURL=cli.js.map
|