@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/cli.cjs
CHANGED
|
@@ -154,6 +154,9 @@ var init_key = __esm({
|
|
|
154
154
|
function redactSecrets(value) {
|
|
155
155
|
return value.replace(API_KEY_RE, "sk_agent_[redacted]").replace(PRIVATE_KEY_RE, "0x[redacted-private-key]");
|
|
156
156
|
}
|
|
157
|
+
function redactForAutomation(value) {
|
|
158
|
+
return redactSecrets(value).replace(/(?:~|\/)[^\s`"']*\/(?:identity|signer|agent)\.json\b/g, "[credential-file-redacted]").replace(/(?:~|\/)[^\s`"']*\/\.env\b/g, "[credential-env-redacted]");
|
|
159
|
+
}
|
|
157
160
|
function shortAddress(address) {
|
|
158
161
|
if (!/^0x[0-9a-fA-F]{40}$/.test(address)) return address;
|
|
159
162
|
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
|
@@ -501,11 +504,19 @@ async function restrictPermissions(path, mode, warn) {
|
|
|
501
504
|
);
|
|
502
505
|
}
|
|
503
506
|
}
|
|
504
|
-
|
|
507
|
+
async function writeConnectOutcomeRecord(directory, outcome, warn) {
|
|
508
|
+
const path$1 = path.join(directory, CONNECT_OUTCOME_FILENAME);
|
|
509
|
+
await promises.writeFile(path$1, `${JSON.stringify(outcome, null, 2)}
|
|
510
|
+
`, { mode: 384 });
|
|
511
|
+
await restrictPermissions(path$1, 384, warn);
|
|
512
|
+
return path$1;
|
|
513
|
+
}
|
|
514
|
+
var REKEY_PENDING_FILENAME, REKEY_PENDING_TTL_MS, CONNECT_OUTCOME_FILENAME;
|
|
505
515
|
var init_storage = __esm({
|
|
506
516
|
"src/storage.ts"() {
|
|
507
517
|
REKEY_PENDING_FILENAME = "rekey-pending.json";
|
|
508
518
|
REKEY_PENDING_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
519
|
+
CONNECT_OUTCOME_FILENAME = "last-connect-outcome.json";
|
|
509
520
|
}
|
|
510
521
|
});
|
|
511
522
|
function mcpPackageSpec() {
|
|
@@ -524,9 +535,9 @@ var init_runtime_manifest = __esm({
|
|
|
524
535
|
mcpPackage: "@haven_ai/mcp",
|
|
525
536
|
mcpVersion: mcp.MCP_VERSION,
|
|
526
537
|
sdkPackage: "@haven_ai/sdk",
|
|
527
|
-
sdkVersion: "0.1.
|
|
538
|
+
sdkVersion: "0.1.32-alpha.0",
|
|
528
539
|
signerPackage: "@haven_ai/signer",
|
|
529
|
-
signerVersion: "0.1.
|
|
540
|
+
signerVersion: "0.1.32-alpha.0",
|
|
530
541
|
// Sourced from the SDK, never a literal (#1161). This field read '20.0.0'
|
|
531
542
|
// while every package's `engines` said `>=24` and the docs said `>=24.0.0`,
|
|
532
543
|
// so the guard that was supposed to enforce the floor waved Node v23 through
|
|
@@ -668,6 +679,97 @@ function isHermesEnvAssignment(line, envKey) {
|
|
|
668
679
|
function isAmbiguousHermesEnvLine(line, envKey) {
|
|
669
680
|
return new RegExp(`^\\s*(?:export[ \\t]+)?${envKey}\\b`).test(line);
|
|
670
681
|
}
|
|
682
|
+
function removeHermesYaml(existingYaml, names, configPath) {
|
|
683
|
+
if (!existingYaml?.trim()) return existingYaml ?? "";
|
|
684
|
+
const doc = yaml.parseDocument(existingYaml, { keepSourceTokens: true });
|
|
685
|
+
if (doc.errors.length > 0 || !yaml.isMap(doc.contents)) {
|
|
686
|
+
throw new UnreadableRuntimeConfigError(configPath ?? "the Hermes config", "it is not a YAML object");
|
|
687
|
+
}
|
|
688
|
+
const mcpPair = doc.contents.items.find((item) => item.key?.toString() === "mcp_servers");
|
|
689
|
+
if (!mcpPair || !yaml.isMap(mcpPair.value)) return existingYaml;
|
|
690
|
+
const toRemove = mcpPair.value.items.filter((item) => {
|
|
691
|
+
const name = item.key?.toString();
|
|
692
|
+
return name === names.hosted || name === names.signer;
|
|
693
|
+
});
|
|
694
|
+
if (toRemove.length === 0) return existingYaml;
|
|
695
|
+
if (toRemove.length !== mcpPair.value.items.length && toRemove.some((item) => !item.key?.range || !item.value?.range)) {
|
|
696
|
+
throw new UnreadableRuntimeConfigError(configPath ?? "the Hermes config", "an MCP server entry cannot be safely removed");
|
|
697
|
+
}
|
|
698
|
+
let out = existingYaml;
|
|
699
|
+
const ranges = toRemove.map((item) => {
|
|
700
|
+
const keyStart = item.key.range[0];
|
|
701
|
+
const valueEnd = item.value.range[1];
|
|
702
|
+
const lineStart = existingYaml.lastIndexOf("\n", keyStart - 1) + 1;
|
|
703
|
+
let end = valueEnd;
|
|
704
|
+
if (existingYaml.slice(end).startsWith("\r\n")) end += 2;
|
|
705
|
+
else if (existingYaml[end] === "\n") end += 1;
|
|
706
|
+
return [lineStart, end];
|
|
707
|
+
});
|
|
708
|
+
ranges.sort((a, b) => b[0] - a[0]);
|
|
709
|
+
for (const [start, end] of ranges) out = out.slice(0, start) + out.slice(end);
|
|
710
|
+
if (toRemove.length === mcpPair.value.items.length) {
|
|
711
|
+
const reparsed = yaml.parseDocument(out, { keepSourceTokens: true });
|
|
712
|
+
if (!reparsed.errors.length && yaml.isMap(reparsed.contents)) {
|
|
713
|
+
const pair = reparsed.contents.items.find((item) => item.key?.toString() === "mcp_servers");
|
|
714
|
+
if (pair && pair.key?.range && pair.value?.range) {
|
|
715
|
+
const keyStart = pair.key.range[0];
|
|
716
|
+
const valueEnd = pair.value.range[1];
|
|
717
|
+
const lineStart = out.lastIndexOf("\n", keyStart - 1) + 1;
|
|
718
|
+
let end = valueEnd;
|
|
719
|
+
if (out.slice(end).startsWith("\r\n")) end += 2;
|
|
720
|
+
else if (out[end] === "\n") end += 1;
|
|
721
|
+
out = out.slice(0, lineStart) + out.slice(end);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
return out;
|
|
726
|
+
}
|
|
727
|
+
function removeHermesEnv(existingEnv, envKey) {
|
|
728
|
+
if (!existingEnv) return "";
|
|
729
|
+
const lineEnding = existingEnv.includes("\r\n") ? "\r\n" : "\n";
|
|
730
|
+
const hasTrailingNewline = /\r?\n$/.test(existingEnv);
|
|
731
|
+
const lines = existingEnv.split(/\r?\n/);
|
|
732
|
+
if (hasTrailingNewline) lines.pop();
|
|
733
|
+
let removed = 0;
|
|
734
|
+
const kept = lines.filter((line) => {
|
|
735
|
+
if (isHermesEnvAssignment(line, envKey)) {
|
|
736
|
+
removed += 1;
|
|
737
|
+
return false;
|
|
738
|
+
}
|
|
739
|
+
if (isAmbiguousHermesEnvLine(line, envKey)) {
|
|
740
|
+
throw new Error("Hermes environment contains an ambiguous managed key");
|
|
741
|
+
}
|
|
742
|
+
return true;
|
|
743
|
+
});
|
|
744
|
+
if (removed === 0) return existingEnv;
|
|
745
|
+
const joined = kept.join(lineEnding);
|
|
746
|
+
return hasTrailingNewline ? `${joined}${lineEnding}` : joined;
|
|
747
|
+
}
|
|
748
|
+
function removeJsonMcpConfig(existingJson, serverRoot, names, configPath) {
|
|
749
|
+
if (!existingJson?.trim()) return existingJson ?? "";
|
|
750
|
+
const config = parseJsonObject(existingJson, configPath);
|
|
751
|
+
const root = config[serverRoot];
|
|
752
|
+
if (!root || typeof root !== "object" || Array.isArray(root)) return existingJson;
|
|
753
|
+
const servers = root;
|
|
754
|
+
let removed = false;
|
|
755
|
+
for (const name of [names.hosted, names.signer]) {
|
|
756
|
+
if (name in servers) {
|
|
757
|
+
delete servers[name];
|
|
758
|
+
removed = true;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
if (!removed) return existingJson;
|
|
762
|
+
if (Object.keys(servers).length === 0) delete config[serverRoot];
|
|
763
|
+
return `${JSON.stringify(config, null, 2)}
|
|
764
|
+
`;
|
|
765
|
+
}
|
|
766
|
+
function removeCodexToml(existingToml, names) {
|
|
767
|
+
let next = removeTomlTableTree(
|
|
768
|
+
removeTomlTableTree(existingToml, `mcp_servers.${names.codexHosted}`),
|
|
769
|
+
`mcp_servers.${names.codexSigner}`
|
|
770
|
+
);
|
|
771
|
+
return next;
|
|
772
|
+
}
|
|
671
773
|
function appendHermesMcpServers(source, servers) {
|
|
672
774
|
const documentEnd = /(?:^|\n)[ \t]*\.\.\.[ \t]*(?:#[^\n]*)?\r?\n?$/.exec(source);
|
|
673
775
|
if (documentEnd) {
|
|
@@ -1940,17 +2042,22 @@ var init_skill_install = __esm({
|
|
|
1940
2042
|
});
|
|
1941
2043
|
|
|
1942
2044
|
// src/connect-error.ts
|
|
2045
|
+
function isConnectError(err) {
|
|
2046
|
+
return err instanceof ConnectError;
|
|
2047
|
+
}
|
|
1943
2048
|
var ConnectError;
|
|
1944
2049
|
var init_connect_error = __esm({
|
|
1945
2050
|
"src/connect-error.ts"() {
|
|
1946
2051
|
ConnectError = class extends Error {
|
|
1947
2052
|
code;
|
|
1948
2053
|
nextAction;
|
|
1949
|
-
|
|
2054
|
+
details;
|
|
2055
|
+
constructor(code, message, nextAction2, details = {}) {
|
|
1950
2056
|
super(message);
|
|
1951
2057
|
this.name = "ConnectError";
|
|
1952
2058
|
this.code = code;
|
|
1953
2059
|
this.nextAction = nextAction2;
|
|
2060
|
+
this.details = details;
|
|
1954
2061
|
}
|
|
1955
2062
|
};
|
|
1956
2063
|
}
|
|
@@ -1973,7 +2080,8 @@ async function resolveRuntimeSelection(explicit, force, options = {}) {
|
|
|
1973
2080
|
throw new ConnectError(
|
|
1974
2081
|
"runtime_force_unrecognized",
|
|
1975
2082
|
`Unknown --runtime-force value "${force}". Valid values: ${RUNTIME_FLAG_VALUES}.`,
|
|
1976
|
-
"rerun_connect_with_a_valid_runtime_name"
|
|
2083
|
+
"rerun_connect_with_a_valid_runtime_name",
|
|
2084
|
+
{ allowedRuntimes: RUNTIME_FLAG_VALUE_LIST }
|
|
1977
2085
|
);
|
|
1978
2086
|
}
|
|
1979
2087
|
return { runtime: forced, source: "force" };
|
|
@@ -1986,7 +2094,8 @@ async function resolveRuntimeSelection(explicit, force, options = {}) {
|
|
|
1986
2094
|
throw new ConnectError(
|
|
1987
2095
|
"runtime_unrecognized",
|
|
1988
2096
|
`"${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.`,
|
|
1989
|
-
"rerun_connect_with_a_valid_runtime_name"
|
|
2097
|
+
"rerun_connect_with_a_valid_runtime_name",
|
|
2098
|
+
{ allowedRuntimes: RUNTIME_FLAG_VALUE_LIST }
|
|
1990
2099
|
);
|
|
1991
2100
|
}
|
|
1992
2101
|
return { runtime: detected, source: "detected", discardedHint: supplied };
|
|
@@ -2021,7 +2130,7 @@ function detectRuntime(env) {
|
|
|
2021
2130
|
if (env.HERMES_HOME || env.HERMES_AGENT) return "hermes";
|
|
2022
2131
|
return null;
|
|
2023
2132
|
}
|
|
2024
|
-
var RUNTIME_PROFILES, RUNTIME_ALIASES, RUNTIME_FLAG_VALUES;
|
|
2133
|
+
var RUNTIME_PROFILES, RUNTIME_ALIASES, RUNTIME_FLAG_VALUE_LIST, RUNTIME_FLAG_VALUES;
|
|
2025
2134
|
var init_runtime_registry = __esm({
|
|
2026
2135
|
"src/runtime-registry.ts"() {
|
|
2027
2136
|
init_connect_error();
|
|
@@ -2140,7 +2249,18 @@ var init_runtime_registry = __esm({
|
|
|
2140
2249
|
other: "other",
|
|
2141
2250
|
manual: "other"
|
|
2142
2251
|
};
|
|
2143
|
-
|
|
2252
|
+
RUNTIME_FLAG_VALUE_LIST = [
|
|
2253
|
+
"claude-code",
|
|
2254
|
+
"codex-cli",
|
|
2255
|
+
"codex-desktop",
|
|
2256
|
+
"cursor",
|
|
2257
|
+
"vscode",
|
|
2258
|
+
"vscode-insiders",
|
|
2259
|
+
"claude-desktop",
|
|
2260
|
+
"hermes",
|
|
2261
|
+
"other"
|
|
2262
|
+
];
|
|
2263
|
+
RUNTIME_FLAG_VALUES = RUNTIME_FLAG_VALUE_LIST.join(", ");
|
|
2144
2264
|
}
|
|
2145
2265
|
});
|
|
2146
2266
|
async function acknowledgeLocalSignerConsent(signerPath, log) {
|
|
@@ -2639,14 +2759,49 @@ var init_runtime_install = __esm({
|
|
|
2639
2759
|
}
|
|
2640
2760
|
});
|
|
2641
2761
|
|
|
2762
|
+
// src/rekey-messages.ts
|
|
2763
|
+
var REKEY_FINISH_NEEDS_API_KEY;
|
|
2764
|
+
var init_rekey_messages = __esm({
|
|
2765
|
+
"src/rekey-messages.ts"() {
|
|
2766
|
+
REKEY_FINISH_NEEDS_API_KEY = "--rekey-finish needs --api-key <key> \u2014 the one the Haven agent page showed once.";
|
|
2767
|
+
}
|
|
2768
|
+
});
|
|
2769
|
+
|
|
2642
2770
|
// src/tombstone.ts
|
|
2643
2771
|
var tombstone_exports = {};
|
|
2644
2772
|
__export(tombstone_exports, {
|
|
2645
2773
|
TOMBSTONE_FILENAME: () => TOMBSTONE_FILENAME,
|
|
2646
2774
|
TOMBSTONE_MARKER: () => TOMBSTONE_MARKER,
|
|
2775
|
+
defaultTombstonesDir: () => defaultTombstonesDir,
|
|
2647
2776
|
readAgentTombstone: () => readAgentTombstone,
|
|
2777
|
+
readTombstoneRecords: () => readTombstoneRecords,
|
|
2648
2778
|
writeAgentTombstone: () => writeAgentTombstone
|
|
2649
2779
|
});
|
|
2780
|
+
function defaultTombstonesDir(baseDir) {
|
|
2781
|
+
return path.join(baseDir ?? path.join(os.homedir(), ".haven"), "tombstones");
|
|
2782
|
+
}
|
|
2783
|
+
async function readTombstoneRecords(tombstonesDir) {
|
|
2784
|
+
const root = tombstonesDir ?? defaultTombstonesDir();
|
|
2785
|
+
let entries = [];
|
|
2786
|
+
try {
|
|
2787
|
+
entries = await promises.readdir(root);
|
|
2788
|
+
} catch {
|
|
2789
|
+
return [];
|
|
2790
|
+
}
|
|
2791
|
+
const records = [];
|
|
2792
|
+
for (const entry of entries) {
|
|
2793
|
+
if (!entry.endsWith(".json")) continue;
|
|
2794
|
+
const recordPath = path.join(root, entry);
|
|
2795
|
+
try {
|
|
2796
|
+
const parsed = JSON.parse(await promises.readFile(recordPath, "utf8"));
|
|
2797
|
+
if (typeof parsed?.agent_id === "string") {
|
|
2798
|
+
records.push({ ...parsed, recordPath });
|
|
2799
|
+
}
|
|
2800
|
+
} catch {
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
return records;
|
|
2804
|
+
}
|
|
2650
2805
|
function tombstoneScript(info) {
|
|
2651
2806
|
const lines = [
|
|
2652
2807
|
`${TOMBSTONE_MARKER}: this Haven agent was retired.`,
|
|
@@ -2677,7 +2832,11 @@ function tombstoneScript(info) {
|
|
|
2677
2832
|
async function writeAgentTombstone(input) {
|
|
2678
2833
|
const dirStat = await promises.stat(input.directory).catch(() => null);
|
|
2679
2834
|
if (!dirStat?.isDirectory()) {
|
|
2680
|
-
throw new
|
|
2835
|
+
throw new ConnectError(
|
|
2836
|
+
"tombstone_directory_not_found",
|
|
2837
|
+
`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.`,
|
|
2838
|
+
"retry_with_an_existing_agent_directory"
|
|
2839
|
+
);
|
|
2681
2840
|
}
|
|
2682
2841
|
const info = {
|
|
2683
2842
|
// reason / replaced_by are persisted to disk and re-emitted to the host's
|
|
@@ -2694,8 +2853,13 @@ async function writeAgentTombstone(input) {
|
|
|
2694
2853
|
const wrapperPath = path.join(binDir, "haven-signer.mjs");
|
|
2695
2854
|
await promises.writeFile(wrapperPath, tombstoneScript(info), "utf8");
|
|
2696
2855
|
await promises.chmod(wrapperPath, 493);
|
|
2697
|
-
|
|
2698
|
-
|
|
2856
|
+
const record = JSON.stringify(info, null, 2) + "\n";
|
|
2857
|
+
await promises.writeFile(path.join(input.directory, TOMBSTONE_FILENAME), record, "utf8");
|
|
2858
|
+
const root = input.tombstonesDir ?? defaultTombstonesDir();
|
|
2859
|
+
await promises.mkdir(root, { recursive: true, mode: 448 });
|
|
2860
|
+
const recordPath = path.join(root, `${info.agent_id}.json`);
|
|
2861
|
+
await promises.writeFile(recordPath, record, { mode: MIRROR_MODE });
|
|
2862
|
+
return { ...info, recordPath };
|
|
2699
2863
|
}
|
|
2700
2864
|
async function readAgentTombstone(directory) {
|
|
2701
2865
|
try {
|
|
@@ -2706,15 +2870,185 @@ async function readAgentTombstone(directory) {
|
|
|
2706
2870
|
return null;
|
|
2707
2871
|
}
|
|
2708
2872
|
}
|
|
2709
|
-
var TOMBSTONE_FILENAME, TOMBSTONE_MARKER;
|
|
2873
|
+
var TOMBSTONE_FILENAME, MIRROR_MODE, TOMBSTONE_MARKER;
|
|
2710
2874
|
var init_tombstone = __esm({
|
|
2711
2875
|
"src/tombstone.ts"() {
|
|
2712
2876
|
init_redact();
|
|
2877
|
+
init_connect_error();
|
|
2713
2878
|
TOMBSTONE_FILENAME = "TOMBSTONE.json";
|
|
2879
|
+
MIRROR_MODE = 384;
|
|
2714
2880
|
TOMBSTONE_MARKER = "HAVEN-TOMBSTONE";
|
|
2715
2881
|
}
|
|
2716
2882
|
});
|
|
2717
2883
|
|
|
2884
|
+
// src/unwire.ts
|
|
2885
|
+
var unwire_exports = {};
|
|
2886
|
+
__export(unwire_exports, {
|
|
2887
|
+
unwireAgent: () => unwireAgent
|
|
2888
|
+
});
|
|
2889
|
+
function identityAt(directory) {
|
|
2890
|
+
return promises.readFile(path.join(directory, "identity.json"), "utf8").then((raw) => JSON.parse(raw)).catch(() => null);
|
|
2891
|
+
}
|
|
2892
|
+
function removeForModel(text, model, names, path) {
|
|
2893
|
+
switch (model.kind) {
|
|
2894
|
+
case "yaml":
|
|
2895
|
+
return removeHermesYaml(text, names, path);
|
|
2896
|
+
case "toml":
|
|
2897
|
+
return removeCodexToml(text, names);
|
|
2898
|
+
case "json":
|
|
2899
|
+
return removeJsonMcpConfig(text, model.serverRoot ?? "mcpServers", names, path);
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2902
|
+
function envLineValue(envText, envKey) {
|
|
2903
|
+
const line = envText.split(/\r?\n/).find((candidate) => new RegExp(`^\\s*(?:export[ \\t]+)?${envKey}[ \\t]*=`).test(candidate));
|
|
2904
|
+
if (!line) return void 0;
|
|
2905
|
+
let value = line.slice(line.indexOf("=") + 1);
|
|
2906
|
+
value = value.trim();
|
|
2907
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
2908
|
+
value = value.slice(1, -1);
|
|
2909
|
+
}
|
|
2910
|
+
return value;
|
|
2911
|
+
}
|
|
2912
|
+
async function readOptionalText(path) {
|
|
2913
|
+
try {
|
|
2914
|
+
return await promises.readFile(path, "utf8");
|
|
2915
|
+
} catch {
|
|
2916
|
+
return null;
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
async function unwireAgent(input) {
|
|
2920
|
+
const homeDir = input.homeDir ?? os.homedir();
|
|
2921
|
+
const [identity, sidecar] = await Promise.all([identityAt(input.directory), readRuntimeSidecar(input.directory)]);
|
|
2922
|
+
const agentId = identity?.agent_id ?? "unknown";
|
|
2923
|
+
const slug = input.slug ?? sidecar?.server_name;
|
|
2924
|
+
const names = serverNamesFor(slug);
|
|
2925
|
+
const runtimes = [];
|
|
2926
|
+
let tombstoned = false;
|
|
2927
|
+
const tombstonePath = path.join(input.directory, TOMBSTONE_FILENAME);
|
|
2928
|
+
if (await readOptionalText(tombstonePath) === null) {
|
|
2929
|
+
await writeAgentTombstone({
|
|
2930
|
+
directory: input.directory,
|
|
2931
|
+
agentId,
|
|
2932
|
+
reason: input.reason ?? "unwired via --unwire",
|
|
2933
|
+
replacedBy: input.replacedBy,
|
|
2934
|
+
tombstonesDir: input.tombstonesDir
|
|
2935
|
+
});
|
|
2936
|
+
tombstoned = true;
|
|
2937
|
+
}
|
|
2938
|
+
for (const model of RUNTIMES) {
|
|
2939
|
+
const path = runtimeConfigPathFor(model.runtime, homeDir);
|
|
2940
|
+
if (path === null) continue;
|
|
2941
|
+
const text = await readOptionalText(path);
|
|
2942
|
+
if (text === null) continue;
|
|
2943
|
+
try {
|
|
2944
|
+
if (!slug) {
|
|
2945
|
+
const owned = sidecar?.wrapper_path != null && text.includes(sidecar.wrapper_path);
|
|
2946
|
+
if (!owned) {
|
|
2947
|
+
const pairPresent = removeForModel(text, model, names, path) !== text;
|
|
2948
|
+
if (!pairPresent) continue;
|
|
2949
|
+
runtimes.push({
|
|
2950
|
+
runtime: model.runtime,
|
|
2951
|
+
label: model.label,
|
|
2952
|
+
path,
|
|
2953
|
+
status: "refused",
|
|
2954
|
+
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"
|
|
2955
|
+
});
|
|
2956
|
+
continue;
|
|
2957
|
+
}
|
|
2958
|
+
}
|
|
2959
|
+
const next = removeForModel(text, model, names, path);
|
|
2960
|
+
if (next === text) continue;
|
|
2961
|
+
await promises.writeFile(path, next, "utf8");
|
|
2962
|
+
runtimes.push({ runtime: model.runtime, label: model.label, path, status: "removed" });
|
|
2963
|
+
} catch (err) {
|
|
2964
|
+
if (err instanceof UnreadableRuntimeConfigError) {
|
|
2965
|
+
runtimes.push({ runtime: model.runtime, label: model.label, path, status: "unreadable", detail: err.message });
|
|
2966
|
+
continue;
|
|
2967
|
+
}
|
|
2968
|
+
throw err;
|
|
2969
|
+
}
|
|
2970
|
+
}
|
|
2971
|
+
const envPath = hermesEnvPath(homeDir);
|
|
2972
|
+
const envText = await readOptionalText(envPath);
|
|
2973
|
+
if (envText !== null) {
|
|
2974
|
+
try {
|
|
2975
|
+
if (!slug) {
|
|
2976
|
+
const value = envLineValue(envText, names.hermesEnvKey);
|
|
2977
|
+
if (value === void 0) {
|
|
2978
|
+
} else if (!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 is shared by unnamed agents and this directory has no stored API key to compare against; refusing to remove another agent's credential"
|
|
2985
|
+
});
|
|
2986
|
+
} else if (value !== identity.api_key) {
|
|
2987
|
+
runtimes.push({
|
|
2988
|
+
runtime: "hermes",
|
|
2989
|
+
label: "Hermes env",
|
|
2990
|
+
path: envPath,
|
|
2991
|
+
status: "refused",
|
|
2992
|
+
detail: "MCP_HAVEN_API_KEY in the Hermes env holds a different agent's key; refusing to remove another agent's credential"
|
|
2993
|
+
});
|
|
2994
|
+
} else {
|
|
2995
|
+
const next = removeHermesEnv(envText, names.hermesEnvKey);
|
|
2996
|
+
if (next !== envText) {
|
|
2997
|
+
await promises.writeFile(envPath, next, "utf8");
|
|
2998
|
+
runtimes.push({ runtime: "hermes", label: "Hermes env", path: envPath, status: "removed" });
|
|
2999
|
+
}
|
|
3000
|
+
}
|
|
3001
|
+
} else {
|
|
3002
|
+
const next = removeHermesEnv(envText, names.hermesEnvKey);
|
|
3003
|
+
if (next !== envText) {
|
|
3004
|
+
await promises.writeFile(envPath, next, "utf8");
|
|
3005
|
+
runtimes.push({ runtime: "hermes", label: "Hermes env", path: envPath, status: "removed" });
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
3008
|
+
} catch (err) {
|
|
3009
|
+
if (err instanceof Error && err.message.includes("ambiguous managed key")) {
|
|
3010
|
+
runtimes.push({
|
|
3011
|
+
runtime: "hermes",
|
|
3012
|
+
label: "Hermes env",
|
|
3013
|
+
path: envPath,
|
|
3014
|
+
status: "refused",
|
|
3015
|
+
detail: err.message
|
|
3016
|
+
});
|
|
3017
|
+
} else {
|
|
3018
|
+
throw err;
|
|
3019
|
+
}
|
|
3020
|
+
}
|
|
3021
|
+
}
|
|
3022
|
+
await Promise.all([
|
|
3023
|
+
promises.rm(path.join(input.directory, "signer.json"), { force: true }),
|
|
3024
|
+
promises.rm(path.join(input.directory, REKEY_PENDING_FILENAME), { force: true })
|
|
3025
|
+
]);
|
|
3026
|
+
if (identity && identity.api_key !== void 0) {
|
|
3027
|
+
const { api_key: _dropped, ...rest } = identity;
|
|
3028
|
+
await promises.writeFile(path.join(input.directory, "identity.json"), `${JSON.stringify(rest, null, 2)}
|
|
3029
|
+
`, { mode: 384 });
|
|
3030
|
+
}
|
|
3031
|
+
return { directory: input.directory, agentId, slug, tombstoned, runtimes };
|
|
3032
|
+
}
|
|
3033
|
+
var RUNTIMES;
|
|
3034
|
+
var init_unwire = __esm({
|
|
3035
|
+
"src/unwire.ts"() {
|
|
3036
|
+
init_config_writers();
|
|
3037
|
+
init_server_names();
|
|
3038
|
+
init_signer_runtime();
|
|
3039
|
+
init_tombstone();
|
|
3040
|
+
init_storage();
|
|
3041
|
+
RUNTIMES = [
|
|
3042
|
+
{ runtime: "hermes", label: "Hermes Agent config", kind: "yaml" },
|
|
3043
|
+
{ runtime: "codex-cli", label: "Codex config", kind: "toml" },
|
|
3044
|
+
{ runtime: "cursor", label: "Cursor MCP config", kind: "json", serverRoot: "mcpServers" },
|
|
3045
|
+
{ runtime: "vscode", label: "VS Code MCP config", kind: "json", serverRoot: "servers" },
|
|
3046
|
+
{ runtime: "vscode-insiders", label: "VS Code Insiders MCP config", kind: "json", serverRoot: "servers" },
|
|
3047
|
+
{ runtime: "claude-desktop", label: "Claude Desktop config", kind: "json", serverRoot: "mcpServers" }
|
|
3048
|
+
];
|
|
3049
|
+
}
|
|
3050
|
+
});
|
|
3051
|
+
|
|
2718
3052
|
// src/rekey.ts
|
|
2719
3053
|
var rekey_exports = {};
|
|
2720
3054
|
__export(rekey_exports, {
|
|
@@ -2774,7 +3108,7 @@ async function startRekey(options, deps = {}) {
|
|
|
2774
3108
|
async function finishRekey(options, deps = {}) {
|
|
2775
3109
|
const now = deps.now ?? (() => Date.now());
|
|
2776
3110
|
if (!options.newApiKey) {
|
|
2777
|
-
throw new Error(
|
|
3111
|
+
throw new Error(REKEY_FINISH_NEEDS_API_KEY);
|
|
2778
3112
|
}
|
|
2779
3113
|
const stored = await readStoredCredentials(
|
|
2780
3114
|
options.serverName,
|
|
@@ -2917,6 +3251,7 @@ var init_rekey = __esm({
|
|
|
2917
3251
|
init_signer_runtime();
|
|
2918
3252
|
init_key();
|
|
2919
3253
|
init_redact();
|
|
3254
|
+
init_rekey_messages();
|
|
2920
3255
|
init_server_names();
|
|
2921
3256
|
init_storage();
|
|
2922
3257
|
}
|
|
@@ -3396,6 +3731,15 @@ async function runDoctor(input, deps = {}) {
|
|
|
3396
3731
|
if (revoked.length > 0) parts.push(`already revoked: ${revoked.join(", ")}`);
|
|
3397
3732
|
if (retired.length > 0) parts.push(`tombstoned (keys removed): ${retired.join(", ")}`);
|
|
3398
3733
|
if (unverifiable.length > 0) parts.push(`could not verify: ${unverifiable.join(", ")}`);
|
|
3734
|
+
const knownIds = new Set([...inventory].map((e) => e.agentId ?? path.basename(e.directory)));
|
|
3735
|
+
const ghostRecords = (await readTombstoneRecords(path.join(homeDir, ".haven", "tombstones"))).filter(
|
|
3736
|
+
(rec) => !knownIds.has(rec.agent_id)
|
|
3737
|
+
);
|
|
3738
|
+
if (ghostRecords.length > 0) {
|
|
3739
|
+
parts.push(
|
|
3740
|
+
`retired records (dir removed): ${ghostRecords.map((rec) => `${rec.agent_id} (${rec.reason})`).join(", ")}`
|
|
3741
|
+
);
|
|
3742
|
+
}
|
|
3399
3743
|
const supersededLive = live.filter((item) => item.entry.classification !== "wired").map((item) => item.label);
|
|
3400
3744
|
checks.push({
|
|
3401
3745
|
id: "superseded_agents",
|
|
@@ -3630,6 +3974,14 @@ async function scanInstalledClients(options = {}) {
|
|
|
3630
3974
|
return SCAN_ORDER.indexOf(a.runtime) - SCAN_ORDER.indexOf(b.runtime);
|
|
3631
3975
|
});
|
|
3632
3976
|
}
|
|
3977
|
+
function installedClientHint(candidates) {
|
|
3978
|
+
const installedClients = candidates.map((candidate) => candidate.runtime);
|
|
3979
|
+
if (candidates.length === 1) {
|
|
3980
|
+
return { installedClients, suggestedRuntime: candidates[0].runtime };
|
|
3981
|
+
}
|
|
3982
|
+
const configured = candidates.filter((candidate) => candidate.evidence === "config-file");
|
|
3983
|
+
return configured.length === 1 ? { installedClients, suggestedRuntime: configured[0].runtime } : { installedClients };
|
|
3984
|
+
}
|
|
3633
3985
|
var MAX_PROMPT_ATTEMPTS = 3;
|
|
3634
3986
|
async function promptForInstalledClient(candidates, io = defaultPromptIo()) {
|
|
3635
3987
|
if (candidates.length === 0) throw noInstalledClientsError();
|
|
@@ -3703,9 +4055,35 @@ function defaultPromptIo() {
|
|
|
3703
4055
|
// src/runtime.ts
|
|
3704
4056
|
init_local_mcp_runtime();
|
|
3705
4057
|
init_runtime_manifest();
|
|
3706
|
-
var CONNECTOR_VERSION = "0.1.
|
|
4058
|
+
var CONNECTOR_VERSION = "0.1.32-alpha.0";
|
|
3707
4059
|
var CONNECT_OUTCOME_SCHEMA_VERSION = 1;
|
|
4060
|
+
var failureOutcomesByError = /* @__PURE__ */ new WeakMap();
|
|
4061
|
+
function failureOutcomeFor(runtimeHint, error) {
|
|
4062
|
+
if (error !== null && typeof error === "object") {
|
|
4063
|
+
const recorded = failureOutcomesByError.get(error);
|
|
4064
|
+
if (recorded) return recorded;
|
|
4065
|
+
}
|
|
4066
|
+
return failedConnectOutcome(runtimeHint, error);
|
|
4067
|
+
}
|
|
3708
4068
|
async function runConnect(options, deps = {}) {
|
|
4069
|
+
const trace = {};
|
|
4070
|
+
try {
|
|
4071
|
+
return await executeConnect(options, deps, trace);
|
|
4072
|
+
} catch (err) {
|
|
4073
|
+
const outcome = failedConnectOutcome(trace.runtime ?? options.runtime, err);
|
|
4074
|
+
if (err !== null && typeof err === "object") failureOutcomesByError.set(err, outcome);
|
|
4075
|
+
if (trace.directory) await recordConnectOutcome(deps, trace.directory, outcome);
|
|
4076
|
+
throw err;
|
|
4077
|
+
}
|
|
4078
|
+
}
|
|
4079
|
+
async function recordConnectOutcome(deps, directory, outcome) {
|
|
4080
|
+
try {
|
|
4081
|
+
return await (deps.writeOutcomeRecord ?? writeConnectOutcomeRecord)(directory, outcome);
|
|
4082
|
+
} catch {
|
|
4083
|
+
return void 0;
|
|
4084
|
+
}
|
|
4085
|
+
}
|
|
4086
|
+
async function executeConnect(options, deps, trace) {
|
|
3709
4087
|
assertSupportedNodeVersion(deps.nodeVersion, MCP_RUNTIME_MANIFEST.minimumNodeVersion);
|
|
3710
4088
|
const connectorVersion = options.connectorVersion ?? CONNECTOR_VERSION;
|
|
3711
4089
|
const api = deps.api ?? createConnectApiClient(options.apiBaseUrl);
|
|
@@ -3725,13 +4103,27 @@ async function runConnect(options, deps = {}) {
|
|
|
3725
4103
|
promptForRuntime: runtimeSelectionPrompt(options, deps)
|
|
3726
4104
|
});
|
|
3727
4105
|
if (!selection.runtime) {
|
|
4106
|
+
const hint = await installedClientHintFor(deps);
|
|
3728
4107
|
throw new ConnectError(
|
|
3729
4108
|
"runtime_undetermined",
|
|
3730
|
-
`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
|
|
3731
|
-
"rerun_connect_with_explicit_runtime"
|
|
4109
|
+
`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.",
|
|
4110
|
+
"rerun_connect_with_explicit_runtime",
|
|
4111
|
+
// #2091: the values must ride structurally too. The backend's setup
|
|
4112
|
+
// prompt permits a retry only with "one of the values that refusal
|
|
4113
|
+
// lists" — and --json discards prose, so a prose-only list deadlocked
|
|
4114
|
+
// every automation run in an undetected runtime (Codex in the field:
|
|
4115
|
+
// npx needs network, Codex runs network commands unsandboxed, and the
|
|
4116
|
+
// unsandboxed path carries none of the CODEX_* detection vars).
|
|
4117
|
+
//
|
|
4118
|
+
// #2174: the list alone still leaves the retry a nine-value guess. The
|
|
4119
|
+
// scan below narrows it to what is actually on this machine — as a
|
|
4120
|
+
// HINT. It does not select, and this stays a refusal: see
|
|
4121
|
+
// `installedClientHint`.
|
|
4122
|
+
{ allowedRuntimes: RUNTIME_FLAG_VALUE_LIST, ...hint }
|
|
3732
4123
|
);
|
|
3733
4124
|
}
|
|
3734
4125
|
const runtime = selection.runtime;
|
|
4126
|
+
trace.runtime = runtime;
|
|
3735
4127
|
const installCapabilities = runtimeInstallCapabilities(runtime);
|
|
3736
4128
|
if (options.localMcp) {
|
|
3737
4129
|
if (!supportsLocalMcp(runtime)) {
|
|
@@ -3750,11 +4142,16 @@ async function runConnect(options, deps = {}) {
|
|
|
3750
4142
|
log(`runtime: ${runtime} (chosen at the prompt \u2014 nothing was detected in this environment)`);
|
|
3751
4143
|
}
|
|
3752
4144
|
log("Warming up your connection to Haven\u2026");
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
4145
|
+
let setup;
|
|
4146
|
+
try {
|
|
4147
|
+
setup = await api.resolveSetup({
|
|
4148
|
+
setupToken: options.setupToken,
|
|
4149
|
+
connectorVersion,
|
|
4150
|
+
runtime
|
|
4151
|
+
});
|
|
4152
|
+
} catch (err) {
|
|
4153
|
+
throw deadSetupTokenError(err) ?? err;
|
|
4154
|
+
}
|
|
3758
4155
|
assertSetupChallengeIsUsable(setup.challenge.expires_at);
|
|
3759
4156
|
printSetupSummary(setup, log);
|
|
3760
4157
|
await preflightStorage({ baseDir: options.credentialsDir, warn: log });
|
|
@@ -3791,6 +4188,8 @@ async function runConnect(options, deps = {}) {
|
|
|
3791
4188
|
installCapabilities
|
|
3792
4189
|
});
|
|
3793
4190
|
} catch (err) {
|
|
4191
|
+
const dead = deadSetupTokenError(err);
|
|
4192
|
+
if (dead) throw dead;
|
|
3794
4193
|
if (isExpiredSetupChallenge(err)) {
|
|
3795
4194
|
throw new Error(
|
|
3796
4195
|
"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."
|
|
@@ -3820,6 +4219,7 @@ async function runConnect(options, deps = {}) {
|
|
|
3820
4219
|
x402BindingSigner: setup.x402_binding_signer ?? void 0,
|
|
3821
4220
|
warn: log
|
|
3822
4221
|
});
|
|
4222
|
+
trace.directory = credentialPaths.directory;
|
|
3823
4223
|
log(`Stored Haven identity credential locally: ${credentialPaths.identityPath}`);
|
|
3824
4224
|
log(`Stored local signer credential locally: ${credentialPaths.signerPath}`);
|
|
3825
4225
|
log(`Stored non-secret agent orientation locally: ${credentialPaths.agentPath}`);
|
|
@@ -3877,12 +4277,13 @@ async function runConnect(options, deps = {}) {
|
|
|
3877
4277
|
} else {
|
|
3878
4278
|
log("Haven setup on this machine is complete.");
|
|
3879
4279
|
}
|
|
4280
|
+
let supersededAgentIds = [];
|
|
3880
4281
|
try {
|
|
3881
|
-
|
|
3882
|
-
if (
|
|
4282
|
+
supersededAgentIds = await listOtherAgentIds(options.credentialsDir, credentialPaths.directory);
|
|
4283
|
+
if (supersededAgentIds.length > 0) {
|
|
3883
4284
|
log("");
|
|
3884
4285
|
log(
|
|
3885
|
-
`Heads-up: this setup created a NEW agent. Your previous agent(s) \u2014 ${
|
|
4286
|
+
`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.`
|
|
3886
4287
|
);
|
|
3887
4288
|
log(
|
|
3888
4289
|
`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.`
|
|
@@ -3917,17 +4318,23 @@ async function runConnect(options, deps = {}) {
|
|
|
3917
4318
|
approval = await waitForBudgetApproval(api, registration.setup_id, localApiKey, log, options.approvalWait);
|
|
3918
4319
|
}
|
|
3919
4320
|
printNextSteps(runtimeInstall, log, approval);
|
|
4321
|
+
const outcome = completionOutcome({
|
|
4322
|
+
runtimeInstall,
|
|
4323
|
+
delegateAddress: registration.delegate_address,
|
|
4324
|
+
hostedMcpUrl: registration.hosted_mcp_url,
|
|
4325
|
+
supersededAgentIds,
|
|
4326
|
+
setupChallengeExpiresAt: setup.challenge.expires_at,
|
|
4327
|
+
approvalRequired: registration.agent_status === "pending_approval"
|
|
4328
|
+
});
|
|
4329
|
+
if (await recordConnectOutcome(deps, credentialPaths.directory, outcome)) {
|
|
4330
|
+
log(`Saved this run's outcome to ${CONNECT_OUTCOME_FILENAME} in the agent's credential directory.`);
|
|
4331
|
+
}
|
|
3920
4332
|
return {
|
|
3921
4333
|
setupId: registration.setup_id,
|
|
3922
4334
|
agentId: registration.agent_id,
|
|
3923
4335
|
delegateAddress: registration.delegate_address,
|
|
3924
4336
|
credentialPaths,
|
|
3925
|
-
outcome
|
|
3926
|
-
runtimeInstall,
|
|
3927
|
-
delegateAddress: registration.delegate_address,
|
|
3928
|
-
setupChallengeExpiresAt: setup.challenge.expires_at,
|
|
3929
|
-
approvalRequired: registration.agent_status === "pending_approval"
|
|
3930
|
-
})
|
|
4337
|
+
outcome
|
|
3931
4338
|
};
|
|
3932
4339
|
}
|
|
3933
4340
|
function completionOutcome(input) {
|
|
@@ -3959,11 +4366,35 @@ function completionOutcome(input) {
|
|
|
3959
4366
|
// malformed value arrives, do not echo it into an automation-facing
|
|
3960
4367
|
// record; the human log has already been redacted separately.
|
|
3961
4368
|
delegate_address: /^0x[0-9a-fA-F]{40}$/.test(input.delegateAddress) ? shortAddress(input.delegateAddress) : "[delegate-address-redacted]",
|
|
4369
|
+
// The endpoint Connect wrote into the runtime's MCP config, verbatim — the
|
|
4370
|
+
// same string that already sits in the user's own config file. Passed
|
|
4371
|
+
// through the secret filter anyway: this is the automation contract, the
|
|
4372
|
+
// value is server-supplied, and belts are cheap (the #1589 stance).
|
|
4373
|
+
...input.hostedMcpUrl ? { hosted_mcp_url: redactSecrets(input.hostedMcpUrl) } : {},
|
|
4374
|
+
// Always emitted on a completed run, empty list included: "no superseded
|
|
4375
|
+
// agents" is a fact a caller needs, and an omitted key would be
|
|
4376
|
+
// indistinguishable from an older connector that never reported it.
|
|
4377
|
+
superseded_agent_ids: input.supersededAgentIds ?? [],
|
|
3962
4378
|
...input.setupChallengeExpiresAt ? { setup_challenge_expires_at: input.setupChallengeExpiresAt } : {},
|
|
3963
4379
|
...runtimeInstall.errorCode ? { error: { code: runtimeInstall.errorCode, next_action: nextAction2 } } : {}
|
|
3964
4380
|
};
|
|
3965
4381
|
return outcome;
|
|
3966
4382
|
}
|
|
4383
|
+
async function installedClientHintFor(deps) {
|
|
4384
|
+
try {
|
|
4385
|
+
const candidates = await (deps.scanInstalledClients ?? scanInstalledClients)({ env: deps.env });
|
|
4386
|
+
const hint = installedClientHint(candidates);
|
|
4387
|
+
return hint.installedClients.length > 0 ? hint : {};
|
|
4388
|
+
} catch {
|
|
4389
|
+
return {};
|
|
4390
|
+
}
|
|
4391
|
+
}
|
|
4392
|
+
function installedClientProse(hint) {
|
|
4393
|
+
const found = hint.installedClients ?? [];
|
|
4394
|
+
if (found.length === 0) return "";
|
|
4395
|
+
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.";
|
|
4396
|
+
return `Haven can see these agent clients installed here, likeliest first: ${found.join(", ")}.${suggestion} `;
|
|
4397
|
+
}
|
|
3967
4398
|
function runtimeSelectionPrompt(options, deps) {
|
|
3968
4399
|
if (options.interactive !== true) return void 0;
|
|
3969
4400
|
if (!(deps.isTty ?? Boolean(process.stdin.isTTY))) return void 0;
|
|
@@ -3988,7 +4419,19 @@ function failedConnectOutcome(runtimeHint, error) {
|
|
|
3988
4419
|
tools: ["haven_get_agent", "haven_get_allowances"],
|
|
3989
4420
|
instruction: "After a successful setup and activation, verify only with haven_get_agent and haven_get_allowances."
|
|
3990
4421
|
},
|
|
3991
|
-
error: {
|
|
4422
|
+
error: {
|
|
4423
|
+
code,
|
|
4424
|
+
next_action: nextAction2,
|
|
4425
|
+
// Only a ConnectError's message enters the JSON record: the vocabulary's
|
|
4426
|
+
// prose is connector-authored and safe to serialize, while a plain
|
|
4427
|
+
// Error can carry arbitrary server or filesystem detail (that stance is
|
|
4428
|
+
// pinned by test). Redaction stays on as belt-and-braces; plain-Error
|
|
4429
|
+
// runs still get their redacted message on stderr via the CLI mirror.
|
|
4430
|
+
...error instanceof ConnectError && message ? { message: redactForAutomation(message) } : {},
|
|
4431
|
+
...error instanceof ConnectError && error.details.allowedRuntimes ? { allowed_runtimes: error.details.allowedRuntimes } : {},
|
|
4432
|
+
...error instanceof ConnectError && error.details.installedClients?.length ? { installed_clients: error.details.installedClients } : {},
|
|
4433
|
+
...error instanceof ConnectError && error.details.suggestedRuntime ? { suggested_runtime: error.details.suggestedRuntime } : {}
|
|
4434
|
+
}
|
|
3992
4435
|
};
|
|
3993
4436
|
}
|
|
3994
4437
|
function printSetupSummary(setup, log) {
|
|
@@ -4010,16 +4453,20 @@ function assertSetupChallengeIsUsable(expiresAt) {
|
|
|
4010
4453
|
"This Haven setup challenge is expired or invalid. Return to Haven, start a fresh connection, and rerun Connect. No local credentials were written."
|
|
4011
4454
|
);
|
|
4012
4455
|
}
|
|
4456
|
+
function deadSetupTokenError(err) {
|
|
4457
|
+
if (!(err instanceof ConnectRequestError) || err.status !== 410 && err.status !== 401) return null;
|
|
4458
|
+
return new ConnectError(
|
|
4459
|
+
"setup_challenge_expired_or_invalid",
|
|
4460
|
+
"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.",
|
|
4461
|
+
"return_to_haven_for_fresh_setup"
|
|
4462
|
+
);
|
|
4463
|
+
}
|
|
4013
4464
|
function isExpiredSetupChallenge(err) {
|
|
4014
4465
|
return err instanceof Error && /(?:setup )?challenge.*expir|expir.*(?:setup )?challenge/i.test(err.message);
|
|
4015
4466
|
}
|
|
4016
4467
|
function secureLogger(log, redactPaths = false) {
|
|
4017
4468
|
return (message) => {
|
|
4018
|
-
|
|
4019
|
-
if (redactPaths) {
|
|
4020
|
-
safe = safe.replace(/(?:~|\/)[^\s`"']*\/(?:identity|signer|agent)\.json\b/g, "[credential-file-redacted]").replace(/(?:~|\/)[^\s`"']*\/\.env\b/g, "[credential-env-redacted]");
|
|
4021
|
-
}
|
|
4022
|
-
log(safe);
|
|
4469
|
+
log(redactPaths ? redactForAutomation(message) : redactSecrets(message));
|
|
4023
4470
|
};
|
|
4024
4471
|
}
|
|
4025
4472
|
function printRuntimeInstall(result, log) {
|
|
@@ -4178,6 +4625,7 @@ function printNextSteps(result, log, approval) {
|
|
|
4178
4625
|
|
|
4179
4626
|
// src/args.ts
|
|
4180
4627
|
init_server_names();
|
|
4628
|
+
init_rekey_messages();
|
|
4181
4629
|
function parseArgs(argv, env = process.env) {
|
|
4182
4630
|
const options = {
|
|
4183
4631
|
apiBaseUrl: env.HAVEN_API_URL ?? "http://localhost:3001",
|
|
@@ -4192,6 +4640,8 @@ function parseArgs(argv, env = process.env) {
|
|
|
4192
4640
|
let tombstoneDir;
|
|
4193
4641
|
let tombstoneReason;
|
|
4194
4642
|
let tombstoneReplacedBy;
|
|
4643
|
+
let unwire;
|
|
4644
|
+
let unwireDir;
|
|
4195
4645
|
for (let i = 0; i < argv.length; i += 1) {
|
|
4196
4646
|
const arg = argv[i];
|
|
4197
4647
|
if (arg === "--help" || arg === "-h") {
|
|
@@ -4210,6 +4660,13 @@ function parseArgs(argv, env = process.env) {
|
|
|
4210
4660
|
newApiKey = requireValue(argv, ++i, arg);
|
|
4211
4661
|
} else if (arg === "--tombstone") {
|
|
4212
4662
|
tombstoneDir = requireValue(argv, ++i, arg);
|
|
4663
|
+
} else if (arg === "--unwire") {
|
|
4664
|
+
unwire = unwire ?? {};
|
|
4665
|
+
const next = argv[i + 1];
|
|
4666
|
+
if (next !== void 0 && !next.startsWith("--")) {
|
|
4667
|
+
unwireDir = next;
|
|
4668
|
+
i += 1;
|
|
4669
|
+
}
|
|
4213
4670
|
} else if (arg === "--reason") {
|
|
4214
4671
|
tombstoneReason = requireValue(argv, ++i, arg);
|
|
4215
4672
|
} else if (arg === "--replaced-by") {
|
|
@@ -4259,18 +4716,37 @@ function parseArgs(argv, env = process.env) {
|
|
|
4259
4716
|
);
|
|
4260
4717
|
}
|
|
4261
4718
|
if (rekey.phase === "finish" && !newApiKey) {
|
|
4262
|
-
throw new Error(
|
|
4719
|
+
throw new Error(REKEY_FINISH_NEEDS_API_KEY);
|
|
4263
4720
|
}
|
|
4264
4721
|
return { options, help, json, doctor, repair, tombstone, rekey };
|
|
4265
4722
|
}
|
|
4266
4723
|
if (newApiKey !== void 0) {
|
|
4267
4724
|
throw new Error("--api-key requires --rekey-finish.");
|
|
4268
4725
|
}
|
|
4269
|
-
if (!tombstoneDir && (tombstoneReason !== void 0 || tombstoneReplacedBy !== void 0)) {
|
|
4270
|
-
throw new Error("--reason and --replaced-by require --tombstone <dir
|
|
4726
|
+
if (!tombstoneDir && !unwire && (tombstoneReason !== void 0 || tombstoneReplacedBy !== void 0)) {
|
|
4727
|
+
throw new Error("--reason and --replaced-by require --tombstone <dir> or --unwire.");
|
|
4728
|
+
}
|
|
4729
|
+
if (unwire && (tombstoneReason !== void 0 || tombstoneReplacedBy !== void 0)) {
|
|
4730
|
+
unwire = {
|
|
4731
|
+
...tombstoneReason !== void 0 ? { reason: tombstoneReason } : {},
|
|
4732
|
+
...tombstoneReplacedBy !== void 0 ? { replacedBy: tombstoneReplacedBy } : {},
|
|
4733
|
+
...unwire
|
|
4734
|
+
};
|
|
4271
4735
|
}
|
|
4272
4736
|
if (tombstone) {
|
|
4273
|
-
|
|
4737
|
+
if (unwire) {
|
|
4738
|
+
throw new Error("--unwire and --tombstone are separate operations; run one per invocation.");
|
|
4739
|
+
}
|
|
4740
|
+
return { options, help, json, doctor, repair, tombstone, rekey, unwire, unwireDir };
|
|
4741
|
+
}
|
|
4742
|
+
if (unwire) {
|
|
4743
|
+
if (options.setupToken) {
|
|
4744
|
+
throw new Error("--unwire removes existing wiring; it does not take --setup. Drop --setup.");
|
|
4745
|
+
}
|
|
4746
|
+
if (!unwireDir && !options.serverName && !options.credentialsDir) {
|
|
4747
|
+
throw new Error("--unwire needs a target: --unwire <dir>, --unwire --name <slug>, or --unwire --credentials-dir <path>.");
|
|
4748
|
+
}
|
|
4749
|
+
return { options, help, json, doctor, repair, tombstone, rekey, unwire, unwireDir };
|
|
4274
4750
|
}
|
|
4275
4751
|
if (doctor || repair) {
|
|
4276
4752
|
if (!options.runtime) {
|
|
@@ -4333,8 +4809,20 @@ function helpText() {
|
|
|
4333
4809
|
" --tombstone <dir> Retire an agent credential directory in place (no token): replaces its signer",
|
|
4334
4810
|
" wrapper with a diagnostic that names the retirement in MCP stderr logs, and",
|
|
4335
4811
|
" writes TOMBSTONE.json. Touches NO key material and revokes nothing.",
|
|
4336
|
-
" --
|
|
4337
|
-
"
|
|
4812
|
+
" --unwire [<dir>] Remove one agent\u2019s wiring from every runtime config it appears in (no token):",
|
|
4813
|
+
" tombstone-first, then drops the hosted + signer MCP pair from Hermes YAML, Codex",
|
|
4814
|
+
" TOML and the JSON configs (Cursor, VS Code, Insiders, Claude Desktop), plus the",
|
|
4815
|
+
" Hermes dotenv API-key line (MCP_HAVEN_API_KEY / MCP_HAVEN_<SLUG>_API_KEY).",
|
|
4816
|
+
" Target the directory directly, or add --name <slug> or --credentials-dir.",
|
|
4817
|
+
" An UNNAMED pair is only removed when this directory\u2019s wrapper is the one the",
|
|
4818
|
+
" config launches (or its key is the one the Hermes env holds); otherwise the",
|
|
4819
|
+
" command refuses rather than unwire a different agent \u2014 it never touches a",
|
|
4820
|
+
" pair another agent owns. Tears down the target directory: its signer key and",
|
|
4821
|
+
" API key are removed locally (record kept via the #2155 tombstone mirror) and",
|
|
4822
|
+
" nothing is ever revoked on the backend \u2014 that stays an owner action on the",
|
|
4823
|
+
" Haven agent page.",
|
|
4824
|
+
" --reason <text> Reason recorded in the tombstone (with --tombstone or --unwire).",
|
|
4825
|
+
" --replaced-by <agent-id> Successor agent recorded in the tombstone (with --tombstone or --unwire).",
|
|
4338
4826
|
" --help Show this help.",
|
|
4339
4827
|
"",
|
|
4340
4828
|
"The connector never prints the private key and never sends it to Haven. JSON output never includes credential contents or full credential paths."
|
|
@@ -4350,6 +4838,28 @@ function requireValue(argv, index, option) {
|
|
|
4350
4838
|
|
|
4351
4839
|
// src/cli.ts
|
|
4352
4840
|
init_redact();
|
|
4841
|
+
init_connect_error();
|
|
4842
|
+
function failSubcommand(io, json, err, envelope, fallback) {
|
|
4843
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4844
|
+
io.stderr(`${redactSecrets(message)}
|
|
4845
|
+
`);
|
|
4846
|
+
if (json) {
|
|
4847
|
+
io.stdout(
|
|
4848
|
+
`${redactSecrets(
|
|
4849
|
+
JSON.stringify({
|
|
4850
|
+
...envelope,
|
|
4851
|
+
error: {
|
|
4852
|
+
code: isConnectError(err) ? err.code : fallback.code,
|
|
4853
|
+
next_action: isConnectError(err) ? err.nextAction : fallback.nextAction,
|
|
4854
|
+
...isConnectError(err) && message ? { message } : {}
|
|
4855
|
+
}
|
|
4856
|
+
})
|
|
4857
|
+
)}
|
|
4858
|
+
`
|
|
4859
|
+
);
|
|
4860
|
+
}
|
|
4861
|
+
return 1;
|
|
4862
|
+
}
|
|
4353
4863
|
async function runCli(argv, io = {
|
|
4354
4864
|
stdout: (message) => process.stdout.write(message),
|
|
4355
4865
|
stderr: (message) => process.stderr.write(message)
|
|
@@ -4360,6 +4870,8 @@ async function runCli(argv, io = {
|
|
|
4360
4870
|
parsed = parseArgs(argv);
|
|
4361
4871
|
} catch (err) {
|
|
4362
4872
|
if (wantsJson) {
|
|
4873
|
+
io.stderr(`${redactForAutomation(err instanceof Error ? err.message : String(err))}
|
|
4874
|
+
`);
|
|
4363
4875
|
io.stdout(`${JSON.stringify(failedConnectOutcome(void 0, err))}
|
|
4364
4876
|
`);
|
|
4365
4877
|
} else {
|
|
@@ -4375,13 +4887,13 @@ async function runCli(argv, io = {
|
|
|
4375
4887
|
}
|
|
4376
4888
|
if (parsed.tombstone) {
|
|
4377
4889
|
const { writeAgentTombstone: writeAgentTombstone2 } = await Promise.resolve().then(() => (init_tombstone(), tombstone_exports));
|
|
4378
|
-
const { readFile:
|
|
4379
|
-
const { join:
|
|
4890
|
+
const { readFile: readFile13 } = await import('fs/promises');
|
|
4891
|
+
const { join: join11 } = await import('path');
|
|
4380
4892
|
try {
|
|
4381
4893
|
let agentId = "unknown";
|
|
4382
4894
|
try {
|
|
4383
4895
|
const identity = JSON.parse(
|
|
4384
|
-
await
|
|
4896
|
+
await readFile13(join11(parsed.tombstone.directory, "identity.json"), "utf8")
|
|
4385
4897
|
);
|
|
4386
4898
|
agentId = identity.agent_id ?? "unknown";
|
|
4387
4899
|
} catch {
|
|
@@ -4402,14 +4914,80 @@ async function runCli(argv, io = {
|
|
|
4402
4914
|
"Key files were NOT touched and nothing was revoked \u2014 revoke the agent on the Haven agent page if you have not already.\n"
|
|
4403
4915
|
);
|
|
4404
4916
|
io.stdout(
|
|
4405
|
-
|
|
4917
|
+
`A surviving tombstone record was mirrored to ${redactSecrets(info.recordPath)}
|
|
4918
|
+
`
|
|
4919
|
+
);
|
|
4920
|
+
io.stdout(
|
|
4921
|
+
"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"
|
|
4406
4922
|
);
|
|
4407
4923
|
}
|
|
4408
4924
|
return 0;
|
|
4409
4925
|
} catch (err) {
|
|
4410
|
-
io.
|
|
4411
|
-
|
|
4412
|
-
|
|
4926
|
+
return failSubcommand(io, parsed.json, err, { tombstoned: false }, {
|
|
4927
|
+
code: "tombstone_failed",
|
|
4928
|
+
nextAction: "review_the_error_and_retry_with_a_valid_agent_directory"
|
|
4929
|
+
});
|
|
4930
|
+
}
|
|
4931
|
+
}
|
|
4932
|
+
if (parsed.unwire) {
|
|
4933
|
+
const { unwireAgent: unwireAgent2 } = await Promise.resolve().then(() => (init_unwire(), unwire_exports));
|
|
4934
|
+
const { homedir: homedir10 } = await import('os');
|
|
4935
|
+
const { join: join11 } = await import('path');
|
|
4936
|
+
const homeDir = homedir10();
|
|
4937
|
+
const root = parsed.options.credentialsDir ?? join11(homeDir, ".haven", "agents");
|
|
4938
|
+
const directory = parsed.unwireDir ?? (parsed.options.serverName ? join11(root, parsed.options.serverName) : root);
|
|
4939
|
+
try {
|
|
4940
|
+
const result = await unwireAgent2({
|
|
4941
|
+
directory,
|
|
4942
|
+
slug: parsed.options.serverName,
|
|
4943
|
+
reason: parsed.unwire.reason,
|
|
4944
|
+
replacedBy: parsed.unwire.replacedBy,
|
|
4945
|
+
homeDir
|
|
4946
|
+
});
|
|
4947
|
+
const failures = result.runtimes.filter((r) => r.status === "refused" || r.status === "unreadable");
|
|
4948
|
+
if (parsed.json) {
|
|
4949
|
+
io.stdout(
|
|
4950
|
+
`${redactSecrets(
|
|
4951
|
+
JSON.stringify({
|
|
4952
|
+
unwired: true,
|
|
4953
|
+
agent_id: result.agentId,
|
|
4954
|
+
slug: result.slug ?? null,
|
|
4955
|
+
directory: result.directory,
|
|
4956
|
+
tombstoned: result.tombstoned,
|
|
4957
|
+
runtimes: result.runtimes.map((r) => ({
|
|
4958
|
+
runtime: r.runtime,
|
|
4959
|
+
label: r.label,
|
|
4960
|
+
status: r.status,
|
|
4961
|
+
...r.detail ? { detail: r.detail } : {}
|
|
4962
|
+
}))
|
|
4963
|
+
})
|
|
4964
|
+
)}
|
|
4965
|
+
`
|
|
4966
|
+
);
|
|
4967
|
+
} else {
|
|
4968
|
+
io.stdout(redactSecrets(`Unwired agent ${result.agentId} at ${result.directory}.
|
|
4969
|
+
`));
|
|
4970
|
+
io.stdout(
|
|
4971
|
+
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"
|
|
4972
|
+
);
|
|
4973
|
+
for (const r of result.runtimes) {
|
|
4974
|
+
const mark = r.status === "removed" ? "\u2713" : r.status === "clean" ? "\u2013" : "\u2717";
|
|
4975
|
+
io.stdout(redactSecrets(` ${mark} ${r.label}: ${r.status}${r.detail ? ` \u2014 ${r.detail}` : ""}
|
|
4976
|
+
`));
|
|
4977
|
+
}
|
|
4978
|
+
io.stdout(
|
|
4979
|
+
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"
|
|
4980
|
+
);
|
|
4981
|
+
io.stdout(
|
|
4982
|
+
"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"
|
|
4983
|
+
);
|
|
4984
|
+
}
|
|
4985
|
+
return failures.length > 0 ? 1 : 0;
|
|
4986
|
+
} catch (err) {
|
|
4987
|
+
return failSubcommand(io, parsed.json, err, { unwired: false }, {
|
|
4988
|
+
code: "unwire_failed",
|
|
4989
|
+
nextAction: "review_the_error_and_rerun_unwire_which_is_idempotent"
|
|
4990
|
+
});
|
|
4413
4991
|
}
|
|
4414
4992
|
}
|
|
4415
4993
|
if (parsed.rekey) {
|
|
@@ -4465,9 +5043,10 @@ async function runCli(argv, io = {
|
|
|
4465
5043
|
}
|
|
4466
5044
|
return 0;
|
|
4467
5045
|
} catch (err) {
|
|
4468
|
-
io.
|
|
4469
|
-
|
|
4470
|
-
|
|
5046
|
+
return failSubcommand(io, parsed.json, err, { rekey: "failed" }, {
|
|
5047
|
+
code: "rekey_failed",
|
|
5048
|
+
nextAction: "review_the_error_and_rerun_the_rekey_phase"
|
|
5049
|
+
});
|
|
4471
5050
|
}
|
|
4472
5051
|
}
|
|
4473
5052
|
if (parsed.doctor || parsed.repair) {
|
|
@@ -4513,9 +5092,10 @@ async function runCli(argv, io = {
|
|
|
4513
5092
|
}
|
|
4514
5093
|
return report.ok ? 0 : 1;
|
|
4515
5094
|
} catch (err) {
|
|
4516
|
-
io.
|
|
4517
|
-
|
|
4518
|
-
|
|
5095
|
+
return failSubcommand(io, parsed.json, err, { doctor: "failed" }, {
|
|
5096
|
+
code: "doctor_failed",
|
|
5097
|
+
nextAction: "review_the_error_and_rerun_doctor"
|
|
5098
|
+
});
|
|
4519
5099
|
}
|
|
4520
5100
|
}
|
|
4521
5101
|
try {
|
|
@@ -4540,7 +5120,9 @@ async function runCli(argv, io = {
|
|
|
4540
5120
|
return 0;
|
|
4541
5121
|
} catch (err) {
|
|
4542
5122
|
if (parsed.json) {
|
|
4543
|
-
io.
|
|
5123
|
+
io.stderr(`${redactForAutomation(err instanceof Error ? err.message : String(err))}
|
|
5124
|
+
`);
|
|
5125
|
+
io.stdout(`${JSON.stringify(failureOutcomeFor(parsed.options.runtime, err))}
|
|
4544
5126
|
`);
|
|
4545
5127
|
} else {
|
|
4546
5128
|
io.stderr(`${redactSecrets(err instanceof Error ? err.message : String(err))}
|