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