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