@haven_ai/connect 0.1.30-alpha.0 → 0.1.32-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 +75 -3
- package/dist/cli.cjs +638 -56
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +639 -57
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +638 -56
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +246 -64
- package/dist/index.d.ts +246 -64
- package/dist/index.js +637 -58
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import crypto from 'crypto';
|
|
2
2
|
import { Wallet } from 'ethers';
|
|
3
|
-
import { mkdir, rm, stat, readdir, readFile, chmod, access,
|
|
3
|
+
import { mkdir, rm, stat, readdir, readFile, writeFile, chmod, access, unlink, rename } from 'fs/promises';
|
|
4
4
|
import { homedir, platform } from 'os';
|
|
5
5
|
import { join, resolve, dirname, basename } from 'path';
|
|
6
6
|
import { ensureConsent, computeConsentHash, loadCredentials, consentInputFromClient, registeredToolNames, MCP_VERSION } from '@haven_ai/mcp';
|
|
@@ -146,6 +146,9 @@ var init_key = __esm({
|
|
|
146
146
|
function redactSecrets(value) {
|
|
147
147
|
return value.replace(API_KEY_RE, "sk_agent_[redacted]").replace(PRIVATE_KEY_RE, "0x[redacted-private-key]");
|
|
148
148
|
}
|
|
149
|
+
function redactForAutomation(value) {
|
|
150
|
+
return redactSecrets(value).replace(/(?:~|\/)[^\s`"']*\/(?:identity|signer|agent)\.json\b/g, "[credential-file-redacted]").replace(/(?:~|\/)[^\s`"']*\/\.env\b/g, "[credential-env-redacted]");
|
|
151
|
+
}
|
|
149
152
|
function shortAddress(address) {
|
|
150
153
|
if (!/^0x[0-9a-fA-F]{40}$/.test(address)) return address;
|
|
151
154
|
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
|
@@ -493,11 +496,19 @@ async function restrictPermissions(path, mode, warn) {
|
|
|
493
496
|
);
|
|
494
497
|
}
|
|
495
498
|
}
|
|
496
|
-
|
|
499
|
+
async function writeConnectOutcomeRecord(directory, outcome, warn) {
|
|
500
|
+
const path = join(directory, CONNECT_OUTCOME_FILENAME);
|
|
501
|
+
await writeFile(path, `${JSON.stringify(outcome, null, 2)}
|
|
502
|
+
`, { mode: 384 });
|
|
503
|
+
await restrictPermissions(path, 384, warn);
|
|
504
|
+
return path;
|
|
505
|
+
}
|
|
506
|
+
var REKEY_PENDING_FILENAME, REKEY_PENDING_TTL_MS, CONNECT_OUTCOME_FILENAME;
|
|
497
507
|
var init_storage = __esm({
|
|
498
508
|
"src/storage.ts"() {
|
|
499
509
|
REKEY_PENDING_FILENAME = "rekey-pending.json";
|
|
500
510
|
REKEY_PENDING_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
511
|
+
CONNECT_OUTCOME_FILENAME = "last-connect-outcome.json";
|
|
501
512
|
}
|
|
502
513
|
});
|
|
503
514
|
function mcpPackageSpec() {
|
|
@@ -516,9 +527,9 @@ var init_runtime_manifest = __esm({
|
|
|
516
527
|
mcpPackage: "@haven_ai/mcp",
|
|
517
528
|
mcpVersion: MCP_VERSION,
|
|
518
529
|
sdkPackage: "@haven_ai/sdk",
|
|
519
|
-
sdkVersion: "0.1.
|
|
530
|
+
sdkVersion: "0.1.32-alpha.0",
|
|
520
531
|
signerPackage: "@haven_ai/signer",
|
|
521
|
-
signerVersion: "0.1.
|
|
532
|
+
signerVersion: "0.1.32-alpha.0",
|
|
522
533
|
// Sourced from the SDK, never a literal (#1161). This field read '20.0.0'
|
|
523
534
|
// while every package's `engines` said `>=24` and the docs said `>=24.0.0`,
|
|
524
535
|
// so the guard that was supposed to enforce the floor waved Node v23 through
|
|
@@ -660,6 +671,97 @@ function isHermesEnvAssignment(line, envKey) {
|
|
|
660
671
|
function isAmbiguousHermesEnvLine(line, envKey) {
|
|
661
672
|
return new RegExp(`^\\s*(?:export[ \\t]+)?${envKey}\\b`).test(line);
|
|
662
673
|
}
|
|
674
|
+
function removeHermesYaml(existingYaml, names, configPath) {
|
|
675
|
+
if (!existingYaml?.trim()) return existingYaml ?? "";
|
|
676
|
+
const doc = parseDocument(existingYaml, { keepSourceTokens: true });
|
|
677
|
+
if (doc.errors.length > 0 || !isMap(doc.contents)) {
|
|
678
|
+
throw new UnreadableRuntimeConfigError(configPath ?? "the Hermes config", "it is not a YAML object");
|
|
679
|
+
}
|
|
680
|
+
const mcpPair = doc.contents.items.find((item) => item.key?.toString() === "mcp_servers");
|
|
681
|
+
if (!mcpPair || !isMap(mcpPair.value)) return existingYaml;
|
|
682
|
+
const toRemove = mcpPair.value.items.filter((item) => {
|
|
683
|
+
const name = item.key?.toString();
|
|
684
|
+
return name === names.hosted || name === names.signer;
|
|
685
|
+
});
|
|
686
|
+
if (toRemove.length === 0) return existingYaml;
|
|
687
|
+
if (toRemove.length !== mcpPair.value.items.length && toRemove.some((item) => !item.key?.range || !item.value?.range)) {
|
|
688
|
+
throw new UnreadableRuntimeConfigError(configPath ?? "the Hermes config", "an MCP server entry cannot be safely removed");
|
|
689
|
+
}
|
|
690
|
+
let out = existingYaml;
|
|
691
|
+
const ranges = toRemove.map((item) => {
|
|
692
|
+
const keyStart = item.key.range[0];
|
|
693
|
+
const valueEnd = item.value.range[1];
|
|
694
|
+
const lineStart = existingYaml.lastIndexOf("\n", keyStart - 1) + 1;
|
|
695
|
+
let end = valueEnd;
|
|
696
|
+
if (existingYaml.slice(end).startsWith("\r\n")) end += 2;
|
|
697
|
+
else if (existingYaml[end] === "\n") end += 1;
|
|
698
|
+
return [lineStart, end];
|
|
699
|
+
});
|
|
700
|
+
ranges.sort((a, b) => b[0] - a[0]);
|
|
701
|
+
for (const [start, end] of ranges) out = out.slice(0, start) + out.slice(end);
|
|
702
|
+
if (toRemove.length === mcpPair.value.items.length) {
|
|
703
|
+
const reparsed = parseDocument(out, { keepSourceTokens: true });
|
|
704
|
+
if (!reparsed.errors.length && isMap(reparsed.contents)) {
|
|
705
|
+
const pair = reparsed.contents.items.find((item) => item.key?.toString() === "mcp_servers");
|
|
706
|
+
if (pair && pair.key?.range && pair.value?.range) {
|
|
707
|
+
const keyStart = pair.key.range[0];
|
|
708
|
+
const valueEnd = pair.value.range[1];
|
|
709
|
+
const lineStart = out.lastIndexOf("\n", keyStart - 1) + 1;
|
|
710
|
+
let end = valueEnd;
|
|
711
|
+
if (out.slice(end).startsWith("\r\n")) end += 2;
|
|
712
|
+
else if (out[end] === "\n") end += 1;
|
|
713
|
+
out = out.slice(0, lineStart) + out.slice(end);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
return out;
|
|
718
|
+
}
|
|
719
|
+
function removeHermesEnv(existingEnv, envKey) {
|
|
720
|
+
if (!existingEnv) return "";
|
|
721
|
+
const lineEnding = existingEnv.includes("\r\n") ? "\r\n" : "\n";
|
|
722
|
+
const hasTrailingNewline = /\r?\n$/.test(existingEnv);
|
|
723
|
+
const lines = existingEnv.split(/\r?\n/);
|
|
724
|
+
if (hasTrailingNewline) lines.pop();
|
|
725
|
+
let removed = 0;
|
|
726
|
+
const kept = lines.filter((line) => {
|
|
727
|
+
if (isHermesEnvAssignment(line, envKey)) {
|
|
728
|
+
removed += 1;
|
|
729
|
+
return false;
|
|
730
|
+
}
|
|
731
|
+
if (isAmbiguousHermesEnvLine(line, envKey)) {
|
|
732
|
+
throw new Error("Hermes environment contains an ambiguous managed key");
|
|
733
|
+
}
|
|
734
|
+
return true;
|
|
735
|
+
});
|
|
736
|
+
if (removed === 0) return existingEnv;
|
|
737
|
+
const joined = kept.join(lineEnding);
|
|
738
|
+
return hasTrailingNewline ? `${joined}${lineEnding}` : joined;
|
|
739
|
+
}
|
|
740
|
+
function removeJsonMcpConfig(existingJson, serverRoot, names, configPath) {
|
|
741
|
+
if (!existingJson?.trim()) return existingJson ?? "";
|
|
742
|
+
const config = parseJsonObject(existingJson, configPath);
|
|
743
|
+
const root = config[serverRoot];
|
|
744
|
+
if (!root || typeof root !== "object" || Array.isArray(root)) return existingJson;
|
|
745
|
+
const servers = root;
|
|
746
|
+
let removed = false;
|
|
747
|
+
for (const name of [names.hosted, names.signer]) {
|
|
748
|
+
if (name in servers) {
|
|
749
|
+
delete servers[name];
|
|
750
|
+
removed = true;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
if (!removed) return existingJson;
|
|
754
|
+
if (Object.keys(servers).length === 0) delete config[serverRoot];
|
|
755
|
+
return `${JSON.stringify(config, null, 2)}
|
|
756
|
+
`;
|
|
757
|
+
}
|
|
758
|
+
function removeCodexToml(existingToml, names) {
|
|
759
|
+
let next = removeTomlTableTree(
|
|
760
|
+
removeTomlTableTree(existingToml, `mcp_servers.${names.codexHosted}`),
|
|
761
|
+
`mcp_servers.${names.codexSigner}`
|
|
762
|
+
);
|
|
763
|
+
return next;
|
|
764
|
+
}
|
|
663
765
|
function appendHermesMcpServers(source, servers) {
|
|
664
766
|
const documentEnd = /(?:^|\n)[ \t]*\.\.\.[ \t]*(?:#[^\n]*)?\r?\n?$/.exec(source);
|
|
665
767
|
if (documentEnd) {
|
|
@@ -1941,11 +2043,13 @@ var init_connect_error = __esm({
|
|
|
1941
2043
|
ConnectError = class extends Error {
|
|
1942
2044
|
code;
|
|
1943
2045
|
nextAction;
|
|
1944
|
-
|
|
2046
|
+
details;
|
|
2047
|
+
constructor(code, message, nextAction2, details = {}) {
|
|
1945
2048
|
super(message);
|
|
1946
2049
|
this.name = "ConnectError";
|
|
1947
2050
|
this.code = code;
|
|
1948
2051
|
this.nextAction = nextAction2;
|
|
2052
|
+
this.details = details;
|
|
1949
2053
|
}
|
|
1950
2054
|
};
|
|
1951
2055
|
}
|
|
@@ -1968,7 +2072,8 @@ async function resolveRuntimeSelection(explicit, force, options = {}) {
|
|
|
1968
2072
|
throw new ConnectError(
|
|
1969
2073
|
"runtime_force_unrecognized",
|
|
1970
2074
|
`Unknown --runtime-force value "${force}". Valid values: ${RUNTIME_FLAG_VALUES}.`,
|
|
1971
|
-
"rerun_connect_with_a_valid_runtime_name"
|
|
2075
|
+
"rerun_connect_with_a_valid_runtime_name",
|
|
2076
|
+
{ allowedRuntimes: RUNTIME_FLAG_VALUE_LIST }
|
|
1972
2077
|
);
|
|
1973
2078
|
}
|
|
1974
2079
|
return { runtime: forced, source: "force" };
|
|
@@ -1981,7 +2086,8 @@ async function resolveRuntimeSelection(explicit, force, options = {}) {
|
|
|
1981
2086
|
throw new ConnectError(
|
|
1982
2087
|
"runtime_unrecognized",
|
|
1983
2088
|
`"${supplied}" is not an agent runtime Haven knows. Valid values: ${RUNTIME_FLAG_VALUES} (the aliases cowork, codex and openclaw are accepted too). Re-run with one of those, or --runtime other to store credentials and finish the MCP setup by hand. Nothing was written and the Haven setup token is still unused.`,
|
|
1984
|
-
"rerun_connect_with_a_valid_runtime_name"
|
|
2089
|
+
"rerun_connect_with_a_valid_runtime_name",
|
|
2090
|
+
{ allowedRuntimes: RUNTIME_FLAG_VALUE_LIST }
|
|
1985
2091
|
);
|
|
1986
2092
|
}
|
|
1987
2093
|
return { runtime: detected, source: "detected", discardedHint: supplied };
|
|
@@ -2016,7 +2122,7 @@ function detectRuntime(env) {
|
|
|
2016
2122
|
if (env.HERMES_HOME || env.HERMES_AGENT) return "hermes";
|
|
2017
2123
|
return null;
|
|
2018
2124
|
}
|
|
2019
|
-
var RUNTIME_PROFILES, RUNTIME_ALIASES, RUNTIME_FLAG_VALUES;
|
|
2125
|
+
var RUNTIME_PROFILES, RUNTIME_ALIASES, RUNTIME_FLAG_VALUE_LIST, RUNTIME_FLAG_VALUES;
|
|
2020
2126
|
var init_runtime_registry = __esm({
|
|
2021
2127
|
"src/runtime-registry.ts"() {
|
|
2022
2128
|
init_connect_error();
|
|
@@ -2135,7 +2241,18 @@ var init_runtime_registry = __esm({
|
|
|
2135
2241
|
other: "other",
|
|
2136
2242
|
manual: "other"
|
|
2137
2243
|
};
|
|
2138
|
-
|
|
2244
|
+
RUNTIME_FLAG_VALUE_LIST = [
|
|
2245
|
+
"claude-code",
|
|
2246
|
+
"codex-cli",
|
|
2247
|
+
"codex-desktop",
|
|
2248
|
+
"cursor",
|
|
2249
|
+
"vscode",
|
|
2250
|
+
"vscode-insiders",
|
|
2251
|
+
"claude-desktop",
|
|
2252
|
+
"hermes",
|
|
2253
|
+
"other"
|
|
2254
|
+
];
|
|
2255
|
+
RUNTIME_FLAG_VALUES = RUNTIME_FLAG_VALUE_LIST.join(", ");
|
|
2139
2256
|
}
|
|
2140
2257
|
});
|
|
2141
2258
|
async function acknowledgeLocalSignerConsent(signerPath, log) {
|
|
@@ -2634,14 +2751,49 @@ var init_runtime_install = __esm({
|
|
|
2634
2751
|
}
|
|
2635
2752
|
});
|
|
2636
2753
|
|
|
2754
|
+
// src/rekey-messages.ts
|
|
2755
|
+
var REKEY_FINISH_NEEDS_API_KEY;
|
|
2756
|
+
var init_rekey_messages = __esm({
|
|
2757
|
+
"src/rekey-messages.ts"() {
|
|
2758
|
+
REKEY_FINISH_NEEDS_API_KEY = "--rekey-finish needs --api-key <key> \u2014 the one the Haven agent page showed once.";
|
|
2759
|
+
}
|
|
2760
|
+
});
|
|
2761
|
+
|
|
2637
2762
|
// src/tombstone.ts
|
|
2638
2763
|
var tombstone_exports = {};
|
|
2639
2764
|
__export(tombstone_exports, {
|
|
2640
2765
|
TOMBSTONE_FILENAME: () => TOMBSTONE_FILENAME,
|
|
2641
2766
|
TOMBSTONE_MARKER: () => TOMBSTONE_MARKER,
|
|
2767
|
+
defaultTombstonesDir: () => defaultTombstonesDir,
|
|
2642
2768
|
readAgentTombstone: () => readAgentTombstone,
|
|
2769
|
+
readTombstoneRecords: () => readTombstoneRecords,
|
|
2643
2770
|
writeAgentTombstone: () => writeAgentTombstone
|
|
2644
2771
|
});
|
|
2772
|
+
function defaultTombstonesDir(baseDir) {
|
|
2773
|
+
return join(baseDir ?? join(homedir(), ".haven"), "tombstones");
|
|
2774
|
+
}
|
|
2775
|
+
async function readTombstoneRecords(tombstonesDir) {
|
|
2776
|
+
const root = tombstonesDir ?? defaultTombstonesDir();
|
|
2777
|
+
let entries = [];
|
|
2778
|
+
try {
|
|
2779
|
+
entries = await readdir(root);
|
|
2780
|
+
} catch {
|
|
2781
|
+
return [];
|
|
2782
|
+
}
|
|
2783
|
+
const records = [];
|
|
2784
|
+
for (const entry of entries) {
|
|
2785
|
+
if (!entry.endsWith(".json")) continue;
|
|
2786
|
+
const recordPath = join(root, entry);
|
|
2787
|
+
try {
|
|
2788
|
+
const parsed = JSON.parse(await readFile(recordPath, "utf8"));
|
|
2789
|
+
if (typeof parsed?.agent_id === "string") {
|
|
2790
|
+
records.push({ ...parsed, recordPath });
|
|
2791
|
+
}
|
|
2792
|
+
} catch {
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
return records;
|
|
2796
|
+
}
|
|
2645
2797
|
function tombstoneScript(info) {
|
|
2646
2798
|
const lines = [
|
|
2647
2799
|
`${TOMBSTONE_MARKER}: this Haven agent was retired.`,
|
|
@@ -2672,7 +2824,11 @@ function tombstoneScript(info) {
|
|
|
2672
2824
|
async function writeAgentTombstone(input) {
|
|
2673
2825
|
const dirStat = await stat(input.directory).catch(() => null);
|
|
2674
2826
|
if (!dirStat?.isDirectory()) {
|
|
2675
|
-
throw new
|
|
2827
|
+
throw new ConnectError(
|
|
2828
|
+
"tombstone_directory_not_found",
|
|
2829
|
+
`Not a directory: ${input.directory} \u2014 nothing to tombstone. Agent directories are named by their wiring SLUG when the agent has one, and by the agent id otherwise \u2014 so a path built from an agent id will not exist for a named agent. List ~/.haven/agents (or read the directories from --doctor --json) and pass one of those.`,
|
|
2830
|
+
"retry_with_an_existing_agent_directory"
|
|
2831
|
+
);
|
|
2676
2832
|
}
|
|
2677
2833
|
const info = {
|
|
2678
2834
|
// reason / replaced_by are persisted to disk and re-emitted to the host's
|
|
@@ -2689,8 +2845,13 @@ async function writeAgentTombstone(input) {
|
|
|
2689
2845
|
const wrapperPath = join(binDir, "haven-signer.mjs");
|
|
2690
2846
|
await writeFile(wrapperPath, tombstoneScript(info), "utf8");
|
|
2691
2847
|
await chmod(wrapperPath, 493);
|
|
2692
|
-
|
|
2693
|
-
|
|
2848
|
+
const record = JSON.stringify(info, null, 2) + "\n";
|
|
2849
|
+
await writeFile(join(input.directory, TOMBSTONE_FILENAME), record, "utf8");
|
|
2850
|
+
const root = input.tombstonesDir ?? defaultTombstonesDir();
|
|
2851
|
+
await mkdir(root, { recursive: true, mode: 448 });
|
|
2852
|
+
const recordPath = join(root, `${info.agent_id}.json`);
|
|
2853
|
+
await writeFile(recordPath, record, { mode: MIRROR_MODE });
|
|
2854
|
+
return { ...info, recordPath };
|
|
2694
2855
|
}
|
|
2695
2856
|
async function readAgentTombstone(directory) {
|
|
2696
2857
|
try {
|
|
@@ -2701,15 +2862,185 @@ async function readAgentTombstone(directory) {
|
|
|
2701
2862
|
return null;
|
|
2702
2863
|
}
|
|
2703
2864
|
}
|
|
2704
|
-
var TOMBSTONE_FILENAME, TOMBSTONE_MARKER;
|
|
2865
|
+
var TOMBSTONE_FILENAME, MIRROR_MODE, TOMBSTONE_MARKER;
|
|
2705
2866
|
var init_tombstone = __esm({
|
|
2706
2867
|
"src/tombstone.ts"() {
|
|
2707
2868
|
init_redact();
|
|
2869
|
+
init_connect_error();
|
|
2708
2870
|
TOMBSTONE_FILENAME = "TOMBSTONE.json";
|
|
2871
|
+
MIRROR_MODE = 384;
|
|
2709
2872
|
TOMBSTONE_MARKER = "HAVEN-TOMBSTONE";
|
|
2710
2873
|
}
|
|
2711
2874
|
});
|
|
2712
2875
|
|
|
2876
|
+
// src/unwire.ts
|
|
2877
|
+
var unwire_exports = {};
|
|
2878
|
+
__export(unwire_exports, {
|
|
2879
|
+
unwireAgent: () => unwireAgent
|
|
2880
|
+
});
|
|
2881
|
+
function identityAt(directory) {
|
|
2882
|
+
return readFile(join(directory, "identity.json"), "utf8").then((raw) => JSON.parse(raw)).catch(() => null);
|
|
2883
|
+
}
|
|
2884
|
+
function removeForModel(text, model, names, path) {
|
|
2885
|
+
switch (model.kind) {
|
|
2886
|
+
case "yaml":
|
|
2887
|
+
return removeHermesYaml(text, names, path);
|
|
2888
|
+
case "toml":
|
|
2889
|
+
return removeCodexToml(text, names);
|
|
2890
|
+
case "json":
|
|
2891
|
+
return removeJsonMcpConfig(text, model.serverRoot ?? "mcpServers", names, path);
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
function envLineValue(envText, envKey) {
|
|
2895
|
+
const line = envText.split(/\r?\n/).find((candidate) => new RegExp(`^\\s*(?:export[ \\t]+)?${envKey}[ \\t]*=`).test(candidate));
|
|
2896
|
+
if (!line) return void 0;
|
|
2897
|
+
let value = line.slice(line.indexOf("=") + 1);
|
|
2898
|
+
value = value.trim();
|
|
2899
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
2900
|
+
value = value.slice(1, -1);
|
|
2901
|
+
}
|
|
2902
|
+
return value;
|
|
2903
|
+
}
|
|
2904
|
+
async function readOptionalText(path) {
|
|
2905
|
+
try {
|
|
2906
|
+
return await readFile(path, "utf8");
|
|
2907
|
+
} catch {
|
|
2908
|
+
return null;
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2911
|
+
async function unwireAgent(input) {
|
|
2912
|
+
const homeDir = input.homeDir ?? homedir();
|
|
2913
|
+
const [identity, sidecar] = await Promise.all([identityAt(input.directory), readRuntimeSidecar(input.directory)]);
|
|
2914
|
+
const agentId = identity?.agent_id ?? "unknown";
|
|
2915
|
+
const slug = input.slug ?? sidecar?.server_name;
|
|
2916
|
+
const names = serverNamesFor(slug);
|
|
2917
|
+
const runtimes = [];
|
|
2918
|
+
let tombstoned = false;
|
|
2919
|
+
const tombstonePath = join(input.directory, TOMBSTONE_FILENAME);
|
|
2920
|
+
if (await readOptionalText(tombstonePath) === null) {
|
|
2921
|
+
await writeAgentTombstone({
|
|
2922
|
+
directory: input.directory,
|
|
2923
|
+
agentId,
|
|
2924
|
+
reason: input.reason ?? "unwired via --unwire",
|
|
2925
|
+
replacedBy: input.replacedBy,
|
|
2926
|
+
tombstonesDir: input.tombstonesDir
|
|
2927
|
+
});
|
|
2928
|
+
tombstoned = true;
|
|
2929
|
+
}
|
|
2930
|
+
for (const model of RUNTIMES) {
|
|
2931
|
+
const path = runtimeConfigPathFor(model.runtime, homeDir);
|
|
2932
|
+
if (path === null) continue;
|
|
2933
|
+
const text = await readOptionalText(path);
|
|
2934
|
+
if (text === null) continue;
|
|
2935
|
+
try {
|
|
2936
|
+
if (!slug) {
|
|
2937
|
+
const owned = sidecar?.wrapper_path != null && text.includes(sidecar.wrapper_path);
|
|
2938
|
+
if (!owned) {
|
|
2939
|
+
const pairPresent = removeForModel(text, model, names, path) !== text;
|
|
2940
|
+
if (!pairPresent) continue;
|
|
2941
|
+
runtimes.push({
|
|
2942
|
+
runtime: model.runtime,
|
|
2943
|
+
label: model.label,
|
|
2944
|
+
path,
|
|
2945
|
+
status: "refused",
|
|
2946
|
+
detail: "the bare haven / haven-signer pair in this config launches a different agent (no wrapper from this directory in the file); refusing to guess which one is yours"
|
|
2947
|
+
});
|
|
2948
|
+
continue;
|
|
2949
|
+
}
|
|
2950
|
+
}
|
|
2951
|
+
const next = removeForModel(text, model, names, path);
|
|
2952
|
+
if (next === text) continue;
|
|
2953
|
+
await writeFile(path, next, "utf8");
|
|
2954
|
+
runtimes.push({ runtime: model.runtime, label: model.label, path, status: "removed" });
|
|
2955
|
+
} catch (err) {
|
|
2956
|
+
if (err instanceof UnreadableRuntimeConfigError) {
|
|
2957
|
+
runtimes.push({ runtime: model.runtime, label: model.label, path, status: "unreadable", detail: err.message });
|
|
2958
|
+
continue;
|
|
2959
|
+
}
|
|
2960
|
+
throw err;
|
|
2961
|
+
}
|
|
2962
|
+
}
|
|
2963
|
+
const envPath = hermesEnvPath(homeDir);
|
|
2964
|
+
const envText = await readOptionalText(envPath);
|
|
2965
|
+
if (envText !== null) {
|
|
2966
|
+
try {
|
|
2967
|
+
if (!slug) {
|
|
2968
|
+
const value = envLineValue(envText, names.hermesEnvKey);
|
|
2969
|
+
if (value === void 0) {
|
|
2970
|
+
} else if (!identity?.api_key) {
|
|
2971
|
+
runtimes.push({
|
|
2972
|
+
runtime: "hermes",
|
|
2973
|
+
label: "Hermes env",
|
|
2974
|
+
path: envPath,
|
|
2975
|
+
status: "refused",
|
|
2976
|
+
detail: "MCP_HAVEN_API_KEY is shared by unnamed agents and this directory has no stored API key to compare against; refusing to remove another agent's credential"
|
|
2977
|
+
});
|
|
2978
|
+
} else if (value !== identity.api_key) {
|
|
2979
|
+
runtimes.push({
|
|
2980
|
+
runtime: "hermes",
|
|
2981
|
+
label: "Hermes env",
|
|
2982
|
+
path: envPath,
|
|
2983
|
+
status: "refused",
|
|
2984
|
+
detail: "MCP_HAVEN_API_KEY in the Hermes env holds a different agent's key; refusing to remove another agent's credential"
|
|
2985
|
+
});
|
|
2986
|
+
} else {
|
|
2987
|
+
const next = removeHermesEnv(envText, names.hermesEnvKey);
|
|
2988
|
+
if (next !== envText) {
|
|
2989
|
+
await writeFile(envPath, next, "utf8");
|
|
2990
|
+
runtimes.push({ runtime: "hermes", label: "Hermes env", path: envPath, status: "removed" });
|
|
2991
|
+
}
|
|
2992
|
+
}
|
|
2993
|
+
} else {
|
|
2994
|
+
const next = removeHermesEnv(envText, names.hermesEnvKey);
|
|
2995
|
+
if (next !== envText) {
|
|
2996
|
+
await writeFile(envPath, next, "utf8");
|
|
2997
|
+
runtimes.push({ runtime: "hermes", label: "Hermes env", path: envPath, status: "removed" });
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
3000
|
+
} catch (err) {
|
|
3001
|
+
if (err instanceof Error && err.message.includes("ambiguous managed key")) {
|
|
3002
|
+
runtimes.push({
|
|
3003
|
+
runtime: "hermes",
|
|
3004
|
+
label: "Hermes env",
|
|
3005
|
+
path: envPath,
|
|
3006
|
+
status: "refused",
|
|
3007
|
+
detail: err.message
|
|
3008
|
+
});
|
|
3009
|
+
} else {
|
|
3010
|
+
throw err;
|
|
3011
|
+
}
|
|
3012
|
+
}
|
|
3013
|
+
}
|
|
3014
|
+
await Promise.all([
|
|
3015
|
+
rm(join(input.directory, "signer.json"), { force: true }),
|
|
3016
|
+
rm(join(input.directory, REKEY_PENDING_FILENAME), { force: true })
|
|
3017
|
+
]);
|
|
3018
|
+
if (identity && identity.api_key !== void 0) {
|
|
3019
|
+
const { api_key: _dropped, ...rest } = identity;
|
|
3020
|
+
await writeFile(join(input.directory, "identity.json"), `${JSON.stringify(rest, null, 2)}
|
|
3021
|
+
`, { mode: 384 });
|
|
3022
|
+
}
|
|
3023
|
+
return { directory: input.directory, agentId, slug, tombstoned, runtimes };
|
|
3024
|
+
}
|
|
3025
|
+
var RUNTIMES;
|
|
3026
|
+
var init_unwire = __esm({
|
|
3027
|
+
"src/unwire.ts"() {
|
|
3028
|
+
init_config_writers();
|
|
3029
|
+
init_server_names();
|
|
3030
|
+
init_signer_runtime();
|
|
3031
|
+
init_tombstone();
|
|
3032
|
+
init_storage();
|
|
3033
|
+
RUNTIMES = [
|
|
3034
|
+
{ runtime: "hermes", label: "Hermes Agent config", kind: "yaml" },
|
|
3035
|
+
{ runtime: "codex-cli", label: "Codex config", kind: "toml" },
|
|
3036
|
+
{ runtime: "cursor", label: "Cursor MCP config", kind: "json", serverRoot: "mcpServers" },
|
|
3037
|
+
{ runtime: "vscode", label: "VS Code MCP config", kind: "json", serverRoot: "servers" },
|
|
3038
|
+
{ runtime: "vscode-insiders", label: "VS Code Insiders MCP config", kind: "json", serverRoot: "servers" },
|
|
3039
|
+
{ runtime: "claude-desktop", label: "Claude Desktop config", kind: "json", serverRoot: "mcpServers" }
|
|
3040
|
+
];
|
|
3041
|
+
}
|
|
3042
|
+
});
|
|
3043
|
+
|
|
2713
3044
|
// src/rekey.ts
|
|
2714
3045
|
var rekey_exports = {};
|
|
2715
3046
|
__export(rekey_exports, {
|
|
@@ -2769,7 +3100,7 @@ async function startRekey(options, deps = {}) {
|
|
|
2769
3100
|
async function finishRekey(options, deps = {}) {
|
|
2770
3101
|
const now = deps.now ?? (() => Date.now());
|
|
2771
3102
|
if (!options.newApiKey) {
|
|
2772
|
-
throw new Error(
|
|
3103
|
+
throw new Error(REKEY_FINISH_NEEDS_API_KEY);
|
|
2773
3104
|
}
|
|
2774
3105
|
const stored = await readStoredCredentials(
|
|
2775
3106
|
options.serverName,
|
|
@@ -2912,6 +3243,7 @@ var init_rekey = __esm({
|
|
|
2912
3243
|
init_signer_runtime();
|
|
2913
3244
|
init_key();
|
|
2914
3245
|
init_redact();
|
|
3246
|
+
init_rekey_messages();
|
|
2915
3247
|
init_server_names();
|
|
2916
3248
|
init_storage();
|
|
2917
3249
|
}
|
|
@@ -3391,6 +3723,15 @@ async function runDoctor(input, deps = {}) {
|
|
|
3391
3723
|
if (revoked.length > 0) parts.push(`already revoked: ${revoked.join(", ")}`);
|
|
3392
3724
|
if (retired.length > 0) parts.push(`tombstoned (keys removed): ${retired.join(", ")}`);
|
|
3393
3725
|
if (unverifiable.length > 0) parts.push(`could not verify: ${unverifiable.join(", ")}`);
|
|
3726
|
+
const knownIds = new Set([...inventory].map((e) => e.agentId ?? basename(e.directory)));
|
|
3727
|
+
const ghostRecords = (await readTombstoneRecords(join(homeDir, ".haven", "tombstones"))).filter(
|
|
3728
|
+
(rec) => !knownIds.has(rec.agent_id)
|
|
3729
|
+
);
|
|
3730
|
+
if (ghostRecords.length > 0) {
|
|
3731
|
+
parts.push(
|
|
3732
|
+
`retired records (dir removed): ${ghostRecords.map((rec) => `${rec.agent_id} (${rec.reason})`).join(", ")}`
|
|
3733
|
+
);
|
|
3734
|
+
}
|
|
3394
3735
|
const supersededLive = live.filter((item) => item.entry.classification !== "wired").map((item) => item.label);
|
|
3395
3736
|
checks.push({
|
|
3396
3737
|
id: "superseded_agents",
|
|
@@ -3625,6 +3966,14 @@ async function scanInstalledClients(options = {}) {
|
|
|
3625
3966
|
return SCAN_ORDER.indexOf(a.runtime) - SCAN_ORDER.indexOf(b.runtime);
|
|
3626
3967
|
});
|
|
3627
3968
|
}
|
|
3969
|
+
function installedClientHint(candidates) {
|
|
3970
|
+
const installedClients = candidates.map((candidate) => candidate.runtime);
|
|
3971
|
+
if (candidates.length === 1) {
|
|
3972
|
+
return { installedClients, suggestedRuntime: candidates[0].runtime };
|
|
3973
|
+
}
|
|
3974
|
+
const configured = candidates.filter((candidate) => candidate.evidence === "config-file");
|
|
3975
|
+
return configured.length === 1 ? { installedClients, suggestedRuntime: configured[0].runtime } : { installedClients };
|
|
3976
|
+
}
|
|
3628
3977
|
var MAX_PROMPT_ATTEMPTS = 3;
|
|
3629
3978
|
async function promptForInstalledClient(candidates, io = defaultPromptIo()) {
|
|
3630
3979
|
if (candidates.length === 0) throw noInstalledClientsError();
|
|
@@ -3698,9 +4047,35 @@ function defaultPromptIo() {
|
|
|
3698
4047
|
// src/runtime.ts
|
|
3699
4048
|
init_local_mcp_runtime();
|
|
3700
4049
|
init_runtime_manifest();
|
|
3701
|
-
var CONNECTOR_VERSION = "0.1.
|
|
4050
|
+
var CONNECTOR_VERSION = "0.1.32-alpha.0";
|
|
3702
4051
|
var CONNECT_OUTCOME_SCHEMA_VERSION = 1;
|
|
4052
|
+
var failureOutcomesByError = /* @__PURE__ */ new WeakMap();
|
|
4053
|
+
function failureOutcomeFor(runtimeHint, error) {
|
|
4054
|
+
if (error !== null && typeof error === "object") {
|
|
4055
|
+
const recorded = failureOutcomesByError.get(error);
|
|
4056
|
+
if (recorded) return recorded;
|
|
4057
|
+
}
|
|
4058
|
+
return failedConnectOutcome(runtimeHint, error);
|
|
4059
|
+
}
|
|
3703
4060
|
async function runConnect(options, deps = {}) {
|
|
4061
|
+
const trace = {};
|
|
4062
|
+
try {
|
|
4063
|
+
return await executeConnect(options, deps, trace);
|
|
4064
|
+
} catch (err) {
|
|
4065
|
+
const outcome = failedConnectOutcome(trace.runtime ?? options.runtime, err);
|
|
4066
|
+
if (err !== null && typeof err === "object") failureOutcomesByError.set(err, outcome);
|
|
4067
|
+
if (trace.directory) await recordConnectOutcome(deps, trace.directory, outcome);
|
|
4068
|
+
throw err;
|
|
4069
|
+
}
|
|
4070
|
+
}
|
|
4071
|
+
async function recordConnectOutcome(deps, directory, outcome) {
|
|
4072
|
+
try {
|
|
4073
|
+
return await (deps.writeOutcomeRecord ?? writeConnectOutcomeRecord)(directory, outcome);
|
|
4074
|
+
} catch {
|
|
4075
|
+
return void 0;
|
|
4076
|
+
}
|
|
4077
|
+
}
|
|
4078
|
+
async function executeConnect(options, deps, trace) {
|
|
3704
4079
|
assertSupportedNodeVersion(deps.nodeVersion, MCP_RUNTIME_MANIFEST.minimumNodeVersion);
|
|
3705
4080
|
const connectorVersion = options.connectorVersion ?? CONNECTOR_VERSION;
|
|
3706
4081
|
const api = deps.api ?? createConnectApiClient(options.apiBaseUrl);
|
|
@@ -3720,13 +4095,27 @@ async function runConnect(options, deps = {}) {
|
|
|
3720
4095
|
promptForRuntime: runtimeSelectionPrompt(options, deps)
|
|
3721
4096
|
});
|
|
3722
4097
|
if (!selection.runtime) {
|
|
4098
|
+
const hint = await installedClientHintFor(deps);
|
|
3723
4099
|
throw new ConnectError(
|
|
3724
4100
|
"runtime_undetermined",
|
|
3725
|
-
`Could not determine the agent runtime: nothing was detected in this environment and no --runtime was given. If you are an AI agent running this command: re-run it once, unchanged except for adding --runtime <name>, naming the harness you are running in \u2014 one of: ${RUNTIME_FLAG_VALUES} (the aliases cowork, codex and openclaw are accepted too). Do not guess: if your harness is not one of those, use --runtime other, which stores the credentials and prints the manual MCP steps. Nothing was written and the Haven setup token is still unused
|
|
3726
|
-
"rerun_connect_with_explicit_runtime"
|
|
4101
|
+
`Could not determine the agent runtime: nothing was detected in this environment and no --runtime was given. If you are an AI agent running this command: re-run it once, unchanged except for adding --runtime <name>, naming the harness you are running in \u2014 one of: ${RUNTIME_FLAG_VALUES} (the aliases cowork, codex and openclaw are accepted too). Do not guess: if your harness is not one of those, use --runtime other, which stores the credentials and prints the manual MCP steps. ` + installedClientProse(hint) + "Nothing was written and the Haven setup token is still unused.",
|
|
4102
|
+
"rerun_connect_with_explicit_runtime",
|
|
4103
|
+
// #2091: the values must ride structurally too. The backend's setup
|
|
4104
|
+
// prompt permits a retry only with "one of the values that refusal
|
|
4105
|
+
// lists" — and --json discards prose, so a prose-only list deadlocked
|
|
4106
|
+
// every automation run in an undetected runtime (Codex in the field:
|
|
4107
|
+
// npx needs network, Codex runs network commands unsandboxed, and the
|
|
4108
|
+
// unsandboxed path carries none of the CODEX_* detection vars).
|
|
4109
|
+
//
|
|
4110
|
+
// #2174: the list alone still leaves the retry a nine-value guess. The
|
|
4111
|
+
// scan below narrows it to what is actually on this machine — as a
|
|
4112
|
+
// HINT. It does not select, and this stays a refusal: see
|
|
4113
|
+
// `installedClientHint`.
|
|
4114
|
+
{ allowedRuntimes: RUNTIME_FLAG_VALUE_LIST, ...hint }
|
|
3727
4115
|
);
|
|
3728
4116
|
}
|
|
3729
4117
|
const runtime = selection.runtime;
|
|
4118
|
+
trace.runtime = runtime;
|
|
3730
4119
|
const installCapabilities = runtimeInstallCapabilities(runtime);
|
|
3731
4120
|
if (options.localMcp) {
|
|
3732
4121
|
if (!supportsLocalMcp(runtime)) {
|
|
@@ -3745,11 +4134,16 @@ async function runConnect(options, deps = {}) {
|
|
|
3745
4134
|
log(`runtime: ${runtime} (chosen at the prompt \u2014 nothing was detected in this environment)`);
|
|
3746
4135
|
}
|
|
3747
4136
|
log("Warming up your connection to Haven\u2026");
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
4137
|
+
let setup;
|
|
4138
|
+
try {
|
|
4139
|
+
setup = await api.resolveSetup({
|
|
4140
|
+
setupToken: options.setupToken,
|
|
4141
|
+
connectorVersion,
|
|
4142
|
+
runtime
|
|
4143
|
+
});
|
|
4144
|
+
} catch (err) {
|
|
4145
|
+
throw deadSetupTokenError(err) ?? err;
|
|
4146
|
+
}
|
|
3753
4147
|
assertSetupChallengeIsUsable(setup.challenge.expires_at);
|
|
3754
4148
|
printSetupSummary(setup, log);
|
|
3755
4149
|
await preflightStorage({ baseDir: options.credentialsDir, warn: log });
|
|
@@ -3786,6 +4180,8 @@ async function runConnect(options, deps = {}) {
|
|
|
3786
4180
|
installCapabilities
|
|
3787
4181
|
});
|
|
3788
4182
|
} catch (err) {
|
|
4183
|
+
const dead = deadSetupTokenError(err);
|
|
4184
|
+
if (dead) throw dead;
|
|
3789
4185
|
if (isExpiredSetupChallenge(err)) {
|
|
3790
4186
|
throw new Error(
|
|
3791
4187
|
"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."
|
|
@@ -3815,6 +4211,7 @@ async function runConnect(options, deps = {}) {
|
|
|
3815
4211
|
x402BindingSigner: setup.x402_binding_signer ?? void 0,
|
|
3816
4212
|
warn: log
|
|
3817
4213
|
});
|
|
4214
|
+
trace.directory = credentialPaths.directory;
|
|
3818
4215
|
log(`Stored Haven identity credential locally: ${credentialPaths.identityPath}`);
|
|
3819
4216
|
log(`Stored local signer credential locally: ${credentialPaths.signerPath}`);
|
|
3820
4217
|
log(`Stored non-secret agent orientation locally: ${credentialPaths.agentPath}`);
|
|
@@ -3872,12 +4269,13 @@ async function runConnect(options, deps = {}) {
|
|
|
3872
4269
|
} else {
|
|
3873
4270
|
log("Haven setup on this machine is complete.");
|
|
3874
4271
|
}
|
|
4272
|
+
let supersededAgentIds = [];
|
|
3875
4273
|
try {
|
|
3876
|
-
|
|
3877
|
-
if (
|
|
4274
|
+
supersededAgentIds = await listOtherAgentIds(options.credentialsDir, credentialPaths.directory);
|
|
4275
|
+
if (supersededAgentIds.length > 0) {
|
|
3878
4276
|
log("");
|
|
3879
4277
|
log(
|
|
3880
|
-
`Heads-up: this setup created a NEW agent. Your previous agent(s) \u2014 ${
|
|
4278
|
+
`Heads-up: this setup created a NEW agent. Your previous agent(s) \u2014 ${supersededAgentIds.join(", ")} \u2014 still exist with their own keys, and any host that was already running keeps acting as them.`
|
|
3881
4279
|
);
|
|
3882
4280
|
log(
|
|
3883
4281
|
`If you meant to replace them: revoke them on the Haven agent page, then restart EVERY long-lived host (gateways, TUI workers, editors) \u2014 each holds the MCP wiring snapshot from its own start time, so after repeated setups each can be stuck on a DIFFERENT old agent. Then remove their directories under ~/.haven/agents (or ${RERUN_HINT} --tombstone <dir> to leave a diagnostic in their place). Run ${RERUN_HINT} --doctor to check whether their keys are still live.`
|
|
@@ -3912,17 +4310,23 @@ async function runConnect(options, deps = {}) {
|
|
|
3912
4310
|
approval = await waitForBudgetApproval(api, registration.setup_id, localApiKey, log, options.approvalWait);
|
|
3913
4311
|
}
|
|
3914
4312
|
printNextSteps(runtimeInstall, log, approval);
|
|
4313
|
+
const outcome = completionOutcome({
|
|
4314
|
+
runtimeInstall,
|
|
4315
|
+
delegateAddress: registration.delegate_address,
|
|
4316
|
+
hostedMcpUrl: registration.hosted_mcp_url,
|
|
4317
|
+
supersededAgentIds,
|
|
4318
|
+
setupChallengeExpiresAt: setup.challenge.expires_at,
|
|
4319
|
+
approvalRequired: registration.agent_status === "pending_approval"
|
|
4320
|
+
});
|
|
4321
|
+
if (await recordConnectOutcome(deps, credentialPaths.directory, outcome)) {
|
|
4322
|
+
log(`Saved this run's outcome to ${CONNECT_OUTCOME_FILENAME} in the agent's credential directory.`);
|
|
4323
|
+
}
|
|
3915
4324
|
return {
|
|
3916
4325
|
setupId: registration.setup_id,
|
|
3917
4326
|
agentId: registration.agent_id,
|
|
3918
4327
|
delegateAddress: registration.delegate_address,
|
|
3919
4328
|
credentialPaths,
|
|
3920
|
-
outcome
|
|
3921
|
-
runtimeInstall,
|
|
3922
|
-
delegateAddress: registration.delegate_address,
|
|
3923
|
-
setupChallengeExpiresAt: setup.challenge.expires_at,
|
|
3924
|
-
approvalRequired: registration.agent_status === "pending_approval"
|
|
3925
|
-
})
|
|
4329
|
+
outcome
|
|
3926
4330
|
};
|
|
3927
4331
|
}
|
|
3928
4332
|
function completionOutcome(input) {
|
|
@@ -3954,11 +4358,35 @@ function completionOutcome(input) {
|
|
|
3954
4358
|
// malformed value arrives, do not echo it into an automation-facing
|
|
3955
4359
|
// record; the human log has already been redacted separately.
|
|
3956
4360
|
delegate_address: /^0x[0-9a-fA-F]{40}$/.test(input.delegateAddress) ? shortAddress(input.delegateAddress) : "[delegate-address-redacted]",
|
|
4361
|
+
// The endpoint Connect wrote into the runtime's MCP config, verbatim — the
|
|
4362
|
+
// same string that already sits in the user's own config file. Passed
|
|
4363
|
+
// through the secret filter anyway: this is the automation contract, the
|
|
4364
|
+
// value is server-supplied, and belts are cheap (the #1589 stance).
|
|
4365
|
+
...input.hostedMcpUrl ? { hosted_mcp_url: redactSecrets(input.hostedMcpUrl) } : {},
|
|
4366
|
+
// Always emitted on a completed run, empty list included: "no superseded
|
|
4367
|
+
// agents" is a fact a caller needs, and an omitted key would be
|
|
4368
|
+
// indistinguishable from an older connector that never reported it.
|
|
4369
|
+
superseded_agent_ids: input.supersededAgentIds ?? [],
|
|
3957
4370
|
...input.setupChallengeExpiresAt ? { setup_challenge_expires_at: input.setupChallengeExpiresAt } : {},
|
|
3958
4371
|
...runtimeInstall.errorCode ? { error: { code: runtimeInstall.errorCode, next_action: nextAction2 } } : {}
|
|
3959
4372
|
};
|
|
3960
4373
|
return outcome;
|
|
3961
4374
|
}
|
|
4375
|
+
async function installedClientHintFor(deps) {
|
|
4376
|
+
try {
|
|
4377
|
+
const candidates = await (deps.scanInstalledClients ?? scanInstalledClients)({ env: deps.env });
|
|
4378
|
+
const hint = installedClientHint(candidates);
|
|
4379
|
+
return hint.installedClients.length > 0 ? hint : {};
|
|
4380
|
+
} catch {
|
|
4381
|
+
return {};
|
|
4382
|
+
}
|
|
4383
|
+
}
|
|
4384
|
+
function installedClientProse(hint) {
|
|
4385
|
+
const found = hint.installedClients ?? [];
|
|
4386
|
+
if (found.length === 0) return "";
|
|
4387
|
+
const suggestion = hint.suggestedRuntime ? ` The likeliest is ${hint.suggestedRuntime}, but Haven will not choose for you \u2014 pass it yourself if it is right.` : " Haven will not choose between them for you.";
|
|
4388
|
+
return `Haven can see these agent clients installed here, likeliest first: ${found.join(", ")}.${suggestion} `;
|
|
4389
|
+
}
|
|
3962
4390
|
function runtimeSelectionPrompt(options, deps) {
|
|
3963
4391
|
if (options.interactive !== true) return void 0;
|
|
3964
4392
|
if (!(deps.isTty ?? Boolean(process.stdin.isTTY))) return void 0;
|
|
@@ -3983,7 +4411,19 @@ function failedConnectOutcome(runtimeHint, error) {
|
|
|
3983
4411
|
tools: ["haven_get_agent", "haven_get_allowances"],
|
|
3984
4412
|
instruction: "After a successful setup and activation, verify only with haven_get_agent and haven_get_allowances."
|
|
3985
4413
|
},
|
|
3986
|
-
error: {
|
|
4414
|
+
error: {
|
|
4415
|
+
code,
|
|
4416
|
+
next_action: nextAction2,
|
|
4417
|
+
// Only a ConnectError's message enters the JSON record: the vocabulary's
|
|
4418
|
+
// prose is connector-authored and safe to serialize, while a plain
|
|
4419
|
+
// Error can carry arbitrary server or filesystem detail (that stance is
|
|
4420
|
+
// pinned by test). Redaction stays on as belt-and-braces; plain-Error
|
|
4421
|
+
// runs still get their redacted message on stderr via the CLI mirror.
|
|
4422
|
+
...error instanceof ConnectError && message ? { message: redactForAutomation(message) } : {},
|
|
4423
|
+
...error instanceof ConnectError && error.details.allowedRuntimes ? { allowed_runtimes: error.details.allowedRuntimes } : {},
|
|
4424
|
+
...error instanceof ConnectError && error.details.installedClients?.length ? { installed_clients: error.details.installedClients } : {},
|
|
4425
|
+
...error instanceof ConnectError && error.details.suggestedRuntime ? { suggested_runtime: error.details.suggestedRuntime } : {}
|
|
4426
|
+
}
|
|
3987
4427
|
};
|
|
3988
4428
|
}
|
|
3989
4429
|
function printSetupSummary(setup, log) {
|
|
@@ -4005,16 +4445,20 @@ function assertSetupChallengeIsUsable(expiresAt) {
|
|
|
4005
4445
|
"This Haven setup challenge is expired or invalid. Return to Haven, start a fresh connection, and rerun Connect. No local credentials were written."
|
|
4006
4446
|
);
|
|
4007
4447
|
}
|
|
4448
|
+
function deadSetupTokenError(err) {
|
|
4449
|
+
if (!(err instanceof ConnectRequestError) || err.status !== 410 && err.status !== 401) return null;
|
|
4450
|
+
return new ConnectError(
|
|
4451
|
+
"setup_challenge_expired_or_invalid",
|
|
4452
|
+
"This Haven setup token is expired or invalid \u2014 tokens are single-use and expire 30 minutes after the dashboard issues them, and a mistyped token reads the same way. Return to Haven, start a fresh connection, and run its new Connect command. No local credentials were written.",
|
|
4453
|
+
"return_to_haven_for_fresh_setup"
|
|
4454
|
+
);
|
|
4455
|
+
}
|
|
4008
4456
|
function isExpiredSetupChallenge(err) {
|
|
4009
4457
|
return err instanceof Error && /(?:setup )?challenge.*expir|expir.*(?:setup )?challenge/i.test(err.message);
|
|
4010
4458
|
}
|
|
4011
4459
|
function secureLogger(log, redactPaths = false) {
|
|
4012
4460
|
return (message) => {
|
|
4013
|
-
|
|
4014
|
-
if (redactPaths) {
|
|
4015
|
-
safe = safe.replace(/(?:~|\/)[^\s`"']*\/(?:identity|signer|agent)\.json\b/g, "[credential-file-redacted]").replace(/(?:~|\/)[^\s`"']*\/\.env\b/g, "[credential-env-redacted]");
|
|
4016
|
-
}
|
|
4017
|
-
log(safe);
|
|
4461
|
+
log(redactPaths ? redactForAutomation(message) : redactSecrets(message));
|
|
4018
4462
|
};
|
|
4019
4463
|
}
|
|
4020
4464
|
function printRuntimeInstall(result, log) {
|
|
@@ -4173,6 +4617,7 @@ function printNextSteps(result, log, approval) {
|
|
|
4173
4617
|
|
|
4174
4618
|
// src/args.ts
|
|
4175
4619
|
init_server_names();
|
|
4620
|
+
init_rekey_messages();
|
|
4176
4621
|
function parseArgs(argv, env = process.env) {
|
|
4177
4622
|
const options = {
|
|
4178
4623
|
apiBaseUrl: env.HAVEN_API_URL ?? "http://localhost:3001",
|
|
@@ -4187,6 +4632,8 @@ function parseArgs(argv, env = process.env) {
|
|
|
4187
4632
|
let tombstoneDir;
|
|
4188
4633
|
let tombstoneReason;
|
|
4189
4634
|
let tombstoneReplacedBy;
|
|
4635
|
+
let unwire;
|
|
4636
|
+
let unwireDir;
|
|
4190
4637
|
for (let i = 0; i < argv.length; i += 1) {
|
|
4191
4638
|
const arg = argv[i];
|
|
4192
4639
|
if (arg === "--help" || arg === "-h") {
|
|
@@ -4205,6 +4652,13 @@ function parseArgs(argv, env = process.env) {
|
|
|
4205
4652
|
newApiKey = requireValue(argv, ++i, arg);
|
|
4206
4653
|
} else if (arg === "--tombstone") {
|
|
4207
4654
|
tombstoneDir = requireValue(argv, ++i, arg);
|
|
4655
|
+
} else if (arg === "--unwire") {
|
|
4656
|
+
unwire = unwire ?? {};
|
|
4657
|
+
const next = argv[i + 1];
|
|
4658
|
+
if (next !== void 0 && !next.startsWith("--")) {
|
|
4659
|
+
unwireDir = next;
|
|
4660
|
+
i += 1;
|
|
4661
|
+
}
|
|
4208
4662
|
} else if (arg === "--reason") {
|
|
4209
4663
|
tombstoneReason = requireValue(argv, ++i, arg);
|
|
4210
4664
|
} else if (arg === "--replaced-by") {
|
|
@@ -4254,18 +4708,37 @@ function parseArgs(argv, env = process.env) {
|
|
|
4254
4708
|
);
|
|
4255
4709
|
}
|
|
4256
4710
|
if (rekey.phase === "finish" && !newApiKey) {
|
|
4257
|
-
throw new Error(
|
|
4711
|
+
throw new Error(REKEY_FINISH_NEEDS_API_KEY);
|
|
4258
4712
|
}
|
|
4259
4713
|
return { options, help, json, doctor, repair, tombstone, rekey };
|
|
4260
4714
|
}
|
|
4261
4715
|
if (newApiKey !== void 0) {
|
|
4262
4716
|
throw new Error("--api-key requires --rekey-finish.");
|
|
4263
4717
|
}
|
|
4264
|
-
if (!tombstoneDir && (tombstoneReason !== void 0 || tombstoneReplacedBy !== void 0)) {
|
|
4265
|
-
throw new Error("--reason and --replaced-by require --tombstone <dir
|
|
4718
|
+
if (!tombstoneDir && !unwire && (tombstoneReason !== void 0 || tombstoneReplacedBy !== void 0)) {
|
|
4719
|
+
throw new Error("--reason and --replaced-by require --tombstone <dir> or --unwire.");
|
|
4720
|
+
}
|
|
4721
|
+
if (unwire && (tombstoneReason !== void 0 || tombstoneReplacedBy !== void 0)) {
|
|
4722
|
+
unwire = {
|
|
4723
|
+
...tombstoneReason !== void 0 ? { reason: tombstoneReason } : {},
|
|
4724
|
+
...tombstoneReplacedBy !== void 0 ? { replacedBy: tombstoneReplacedBy } : {},
|
|
4725
|
+
...unwire
|
|
4726
|
+
};
|
|
4266
4727
|
}
|
|
4267
4728
|
if (tombstone) {
|
|
4268
|
-
|
|
4729
|
+
if (unwire) {
|
|
4730
|
+
throw new Error("--unwire and --tombstone are separate operations; run one per invocation.");
|
|
4731
|
+
}
|
|
4732
|
+
return { options, help, json, doctor, repair, tombstone, rekey, unwire, unwireDir };
|
|
4733
|
+
}
|
|
4734
|
+
if (unwire) {
|
|
4735
|
+
if (options.setupToken) {
|
|
4736
|
+
throw new Error("--unwire removes existing wiring; it does not take --setup. Drop --setup.");
|
|
4737
|
+
}
|
|
4738
|
+
if (!unwireDir && !options.serverName && !options.credentialsDir) {
|
|
4739
|
+
throw new Error("--unwire needs a target: --unwire <dir>, --unwire --name <slug>, or --unwire --credentials-dir <path>.");
|
|
4740
|
+
}
|
|
4741
|
+
return { options, help, json, doctor, repair, tombstone, rekey, unwire, unwireDir };
|
|
4269
4742
|
}
|
|
4270
4743
|
if (doctor || repair) {
|
|
4271
4744
|
if (!options.runtime) {
|
|
@@ -4328,8 +4801,20 @@ function helpText() {
|
|
|
4328
4801
|
" --tombstone <dir> Retire an agent credential directory in place (no token): replaces its signer",
|
|
4329
4802
|
" wrapper with a diagnostic that names the retirement in MCP stderr logs, and",
|
|
4330
4803
|
" writes TOMBSTONE.json. Touches NO key material and revokes nothing.",
|
|
4331
|
-
" --
|
|
4332
|
-
"
|
|
4804
|
+
" --unwire [<dir>] Remove one agent\u2019s wiring from every runtime config it appears in (no token):",
|
|
4805
|
+
" tombstone-first, then drops the hosted + signer MCP pair from Hermes YAML, Codex",
|
|
4806
|
+
" TOML and the JSON configs (Cursor, VS Code, Insiders, Claude Desktop), plus the",
|
|
4807
|
+
" Hermes dotenv API-key line (MCP_HAVEN_API_KEY / MCP_HAVEN_<SLUG>_API_KEY).",
|
|
4808
|
+
" Target the directory directly, or add --name <slug> or --credentials-dir.",
|
|
4809
|
+
" An UNNAMED pair is only removed when this directory\u2019s wrapper is the one the",
|
|
4810
|
+
" config launches (or its key is the one the Hermes env holds); otherwise the",
|
|
4811
|
+
" command refuses rather than unwire a different agent \u2014 it never touches a",
|
|
4812
|
+
" pair another agent owns. Tears down the target directory: its signer key and",
|
|
4813
|
+
" API key are removed locally (record kept via the #2155 tombstone mirror) and",
|
|
4814
|
+
" nothing is ever revoked on the backend \u2014 that stays an owner action on the",
|
|
4815
|
+
" Haven agent page.",
|
|
4816
|
+
" --reason <text> Reason recorded in the tombstone (with --tombstone or --unwire).",
|
|
4817
|
+
" --replaced-by <agent-id> Successor agent recorded in the tombstone (with --tombstone or --unwire).",
|
|
4333
4818
|
" --help Show this help.",
|
|
4334
4819
|
"",
|
|
4335
4820
|
"The connector never prints the private key and never sends it to Haven. JSON output never includes credential contents or full credential paths."
|
|
@@ -4348,6 +4833,28 @@ init_api();
|
|
|
4348
4833
|
init_key();
|
|
4349
4834
|
init_redact();
|
|
4350
4835
|
init_redact();
|
|
4836
|
+
init_connect_error();
|
|
4837
|
+
function failSubcommand(io, json, err, envelope, fallback) {
|
|
4838
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4839
|
+
io.stderr(`${redactSecrets(message)}
|
|
4840
|
+
`);
|
|
4841
|
+
if (json) {
|
|
4842
|
+
io.stdout(
|
|
4843
|
+
`${redactSecrets(
|
|
4844
|
+
JSON.stringify({
|
|
4845
|
+
...envelope,
|
|
4846
|
+
error: {
|
|
4847
|
+
code: isConnectError(err) ? err.code : fallback.code,
|
|
4848
|
+
next_action: isConnectError(err) ? err.nextAction : fallback.nextAction,
|
|
4849
|
+
...isConnectError(err) && message ? { message } : {}
|
|
4850
|
+
}
|
|
4851
|
+
})
|
|
4852
|
+
)}
|
|
4853
|
+
`
|
|
4854
|
+
);
|
|
4855
|
+
}
|
|
4856
|
+
return 1;
|
|
4857
|
+
}
|
|
4351
4858
|
async function runCli(argv, io = {
|
|
4352
4859
|
stdout: (message) => process.stdout.write(message),
|
|
4353
4860
|
stderr: (message) => process.stderr.write(message)
|
|
@@ -4358,6 +4865,8 @@ async function runCli(argv, io = {
|
|
|
4358
4865
|
parsed = parseArgs(argv);
|
|
4359
4866
|
} catch (err) {
|
|
4360
4867
|
if (wantsJson) {
|
|
4868
|
+
io.stderr(`${redactForAutomation(err instanceof Error ? err.message : String(err))}
|
|
4869
|
+
`);
|
|
4361
4870
|
io.stdout(`${JSON.stringify(failedConnectOutcome(void 0, err))}
|
|
4362
4871
|
`);
|
|
4363
4872
|
} else {
|
|
@@ -4373,13 +4882,13 @@ async function runCli(argv, io = {
|
|
|
4373
4882
|
}
|
|
4374
4883
|
if (parsed.tombstone) {
|
|
4375
4884
|
const { writeAgentTombstone: writeAgentTombstone2 } = await Promise.resolve().then(() => (init_tombstone(), tombstone_exports));
|
|
4376
|
-
const { readFile:
|
|
4377
|
-
const { join:
|
|
4885
|
+
const { readFile: readFile13 } = await import('fs/promises');
|
|
4886
|
+
const { join: join11 } = await import('path');
|
|
4378
4887
|
try {
|
|
4379
4888
|
let agentId = "unknown";
|
|
4380
4889
|
try {
|
|
4381
4890
|
const identity = JSON.parse(
|
|
4382
|
-
await
|
|
4891
|
+
await readFile13(join11(parsed.tombstone.directory, "identity.json"), "utf8")
|
|
4383
4892
|
);
|
|
4384
4893
|
agentId = identity.agent_id ?? "unknown";
|
|
4385
4894
|
} catch {
|
|
@@ -4400,14 +4909,80 @@ async function runCli(argv, io = {
|
|
|
4400
4909
|
"Key files were NOT touched and nothing was revoked \u2014 revoke the agent on the Haven agent page if you have not already.\n"
|
|
4401
4910
|
);
|
|
4402
4911
|
io.stdout(
|
|
4403
|
-
|
|
4912
|
+
`A surviving tombstone record was mirrored to ${redactSecrets(info.recordPath)}
|
|
4913
|
+
`
|
|
4914
|
+
);
|
|
4915
|
+
io.stdout(
|
|
4916
|
+
"Restart EVERY long-lived MCP host (gateway, TUI workers, editors): each holds the wiring snapshot from its own start time, and the tombstone only speaks when a stale host next probes the old path. The mirrored record keeps this retirement observable even after the agent directory itself is deleted.\n"
|
|
4404
4917
|
);
|
|
4405
4918
|
}
|
|
4406
4919
|
return 0;
|
|
4407
4920
|
} catch (err) {
|
|
4408
|
-
io.
|
|
4409
|
-
|
|
4410
|
-
|
|
4921
|
+
return failSubcommand(io, parsed.json, err, { tombstoned: false }, {
|
|
4922
|
+
code: "tombstone_failed",
|
|
4923
|
+
nextAction: "review_the_error_and_retry_with_a_valid_agent_directory"
|
|
4924
|
+
});
|
|
4925
|
+
}
|
|
4926
|
+
}
|
|
4927
|
+
if (parsed.unwire) {
|
|
4928
|
+
const { unwireAgent: unwireAgent2 } = await Promise.resolve().then(() => (init_unwire(), unwire_exports));
|
|
4929
|
+
const { homedir: homedir10 } = await import('os');
|
|
4930
|
+
const { join: join11 } = await import('path');
|
|
4931
|
+
const homeDir = homedir10();
|
|
4932
|
+
const root = parsed.options.credentialsDir ?? join11(homeDir, ".haven", "agents");
|
|
4933
|
+
const directory = parsed.unwireDir ?? (parsed.options.serverName ? join11(root, parsed.options.serverName) : root);
|
|
4934
|
+
try {
|
|
4935
|
+
const result = await unwireAgent2({
|
|
4936
|
+
directory,
|
|
4937
|
+
slug: parsed.options.serverName,
|
|
4938
|
+
reason: parsed.unwire.reason,
|
|
4939
|
+
replacedBy: parsed.unwire.replacedBy,
|
|
4940
|
+
homeDir
|
|
4941
|
+
});
|
|
4942
|
+
const failures = result.runtimes.filter((r) => r.status === "refused" || r.status === "unreadable");
|
|
4943
|
+
if (parsed.json) {
|
|
4944
|
+
io.stdout(
|
|
4945
|
+
`${redactSecrets(
|
|
4946
|
+
JSON.stringify({
|
|
4947
|
+
unwired: true,
|
|
4948
|
+
agent_id: result.agentId,
|
|
4949
|
+
slug: result.slug ?? null,
|
|
4950
|
+
directory: result.directory,
|
|
4951
|
+
tombstoned: result.tombstoned,
|
|
4952
|
+
runtimes: result.runtimes.map((r) => ({
|
|
4953
|
+
runtime: r.runtime,
|
|
4954
|
+
label: r.label,
|
|
4955
|
+
status: r.status,
|
|
4956
|
+
...r.detail ? { detail: r.detail } : {}
|
|
4957
|
+
}))
|
|
4958
|
+
})
|
|
4959
|
+
)}
|
|
4960
|
+
`
|
|
4961
|
+
);
|
|
4962
|
+
} else {
|
|
4963
|
+
io.stdout(redactSecrets(`Unwired agent ${result.agentId} at ${result.directory}.
|
|
4964
|
+
`));
|
|
4965
|
+
io.stdout(
|
|
4966
|
+
result.tombstoned ? " \xB7 Tombstoned first: any long-lived host still resolving the old wrapper gets the HAVEN-TOMBSTONE diagnosis.\n" : " \xB7 Directory was already tombstoned.\n"
|
|
4967
|
+
);
|
|
4968
|
+
for (const r of result.runtimes) {
|
|
4969
|
+
const mark = r.status === "removed" ? "\u2713" : r.status === "clean" ? "\u2013" : "\u2717";
|
|
4970
|
+
io.stdout(redactSecrets(` ${mark} ${r.label}: ${r.status}${r.detail ? ` \u2014 ${r.detail}` : ""}
|
|
4971
|
+
`));
|
|
4972
|
+
}
|
|
4973
|
+
io.stdout(
|
|
4974
|
+
failures.length > 0 ? " Some entries were NOT removed (\u2717 above). Re-run `--unwire` after resolving each refusal \u2014\n it is idempotent.\n" : " Verify: `--doctor --runtime <runtime>` per host should report this agent as `retired` with a\n clean runtime-config check.\n"
|
|
4975
|
+
);
|
|
4976
|
+
io.stdout(
|
|
4977
|
+
"Restart EVERY long-lived MCP host (gateway, TUI workers, editors): each holds the wiring snapshot\nfrom its own start time. This directory\u2019s local key material was removed and the tombstone\nrecord + #2155 mirror survive \u2014 but nothing was REVOKED on the backend. If you have not\nalready, revoke the agent on the Haven agent page to stop it spending entirely.\n"
|
|
4978
|
+
);
|
|
4979
|
+
}
|
|
4980
|
+
return failures.length > 0 ? 1 : 0;
|
|
4981
|
+
} catch (err) {
|
|
4982
|
+
return failSubcommand(io, parsed.json, err, { unwired: false }, {
|
|
4983
|
+
code: "unwire_failed",
|
|
4984
|
+
nextAction: "review_the_error_and_rerun_unwire_which_is_idempotent"
|
|
4985
|
+
});
|
|
4411
4986
|
}
|
|
4412
4987
|
}
|
|
4413
4988
|
if (parsed.rekey) {
|
|
@@ -4463,9 +5038,10 @@ async function runCli(argv, io = {
|
|
|
4463
5038
|
}
|
|
4464
5039
|
return 0;
|
|
4465
5040
|
} catch (err) {
|
|
4466
|
-
io.
|
|
4467
|
-
|
|
4468
|
-
|
|
5041
|
+
return failSubcommand(io, parsed.json, err, { rekey: "failed" }, {
|
|
5042
|
+
code: "rekey_failed",
|
|
5043
|
+
nextAction: "review_the_error_and_rerun_the_rekey_phase"
|
|
5044
|
+
});
|
|
4469
5045
|
}
|
|
4470
5046
|
}
|
|
4471
5047
|
if (parsed.doctor || parsed.repair) {
|
|
@@ -4511,9 +5087,10 @@ async function runCli(argv, io = {
|
|
|
4511
5087
|
}
|
|
4512
5088
|
return report.ok ? 0 : 1;
|
|
4513
5089
|
} catch (err) {
|
|
4514
|
-
io.
|
|
4515
|
-
|
|
4516
|
-
|
|
5090
|
+
return failSubcommand(io, parsed.json, err, { doctor: "failed" }, {
|
|
5091
|
+
code: "doctor_failed",
|
|
5092
|
+
nextAction: "review_the_error_and_rerun_doctor"
|
|
5093
|
+
});
|
|
4517
5094
|
}
|
|
4518
5095
|
}
|
|
4519
5096
|
try {
|
|
@@ -4538,7 +5115,9 @@ async function runCli(argv, io = {
|
|
|
4538
5115
|
return 0;
|
|
4539
5116
|
} catch (err) {
|
|
4540
5117
|
if (parsed.json) {
|
|
4541
|
-
io.
|
|
5118
|
+
io.stderr(`${redactForAutomation(err instanceof Error ? err.message : String(err))}
|
|
5119
|
+
`);
|
|
5120
|
+
io.stdout(`${JSON.stringify(failureOutcomeFor(parsed.options.runtime, err))}
|
|
4542
5121
|
`);
|
|
4543
5122
|
} else {
|
|
4544
5123
|
io.stderr(`${redactSecrets(err instanceof Error ? err.message : String(err))}
|
|
@@ -4569,6 +5148,6 @@ init_connect_error();
|
|
|
4569
5148
|
init_storage();
|
|
4570
5149
|
init_runtime_manifest();
|
|
4571
5150
|
|
|
4572
|
-
export { CONNECTOR_VERSION, CONNECT_OUTCOME_SCHEMA_VERSION, ConnectError, MCP_RUNTIME_MANIFEST, RUNTIME_FLAG_VALUES, completionOutcome, createConnectApiClient, defaultAgentDirectory, delegateKeyFromPrivateKey, failedConnectOutcome, generateDelegateKey, helpText, installRuntime, isConnectError, mcpPackageSpec, normalizeRuntime, parseArgs, prepareSignerRuntime, promptForInstalledClient, redactSecrets, resolveRuntimeByInstalledClientPrompt, resolveRuntimeSelection, runCli, runConnect, runtimeInstallCapabilities, runtimeProfile, scanInstalledClients, sdkPackageSpec, shortAddress, signerPackageSpec, writeCredentialFiles };
|
|
5151
|
+
export { CONNECTOR_VERSION, CONNECT_OUTCOME_FILENAME, CONNECT_OUTCOME_SCHEMA_VERSION, ConnectError, MCP_RUNTIME_MANIFEST, RUNTIME_FLAG_VALUES, completionOutcome, createConnectApiClient, defaultAgentDirectory, delegateKeyFromPrivateKey, failedConnectOutcome, failureOutcomeFor, generateDelegateKey, helpText, installRuntime, installedClientHint, isConnectError, mcpPackageSpec, normalizeRuntime, parseArgs, prepareSignerRuntime, promptForInstalledClient, redactSecrets, resolveRuntimeByInstalledClientPrompt, resolveRuntimeSelection, runCli, runConnect, runtimeInstallCapabilities, runtimeProfile, scanInstalledClients, sdkPackageSpec, shortAddress, signerPackageSpec, writeConnectOutcomeRecord, writeCredentialFiles };
|
|
4573
5152
|
//# sourceMappingURL=index.js.map
|
|
4574
5153
|
//# sourceMappingURL=index.js.map
|