@haven_ai/connect 0.1.31-alpha.0 → 0.1.33-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 +588 -43
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +589 -44
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +588 -43
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +214 -59
- package/dist/index.d.ts +214 -59
- package/dist/index.js +587 -45
- package/dist/index.js.map +1 -1
- package/package.json +15 -8
package/dist/index.cjs
CHANGED
|
@@ -503,11 +503,19 @@ async function restrictPermissions(path, mode, warn) {
|
|
|
503
503
|
);
|
|
504
504
|
}
|
|
505
505
|
}
|
|
506
|
-
|
|
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;
|
|
507
514
|
var init_storage = __esm({
|
|
508
515
|
"src/storage.ts"() {
|
|
509
516
|
REKEY_PENDING_FILENAME = "rekey-pending.json";
|
|
510
517
|
REKEY_PENDING_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
518
|
+
exports.CONNECT_OUTCOME_FILENAME = "last-connect-outcome.json";
|
|
511
519
|
}
|
|
512
520
|
});
|
|
513
521
|
function mcpPackageSpec() {
|
|
@@ -526,9 +534,9 @@ var init_runtime_manifest = __esm({
|
|
|
526
534
|
mcpPackage: "@haven_ai/mcp",
|
|
527
535
|
mcpVersion: mcp.MCP_VERSION,
|
|
528
536
|
sdkPackage: "@haven_ai/sdk",
|
|
529
|
-
sdkVersion: "0.1.
|
|
537
|
+
sdkVersion: "0.1.33-alpha.0",
|
|
530
538
|
signerPackage: "@haven_ai/signer",
|
|
531
|
-
signerVersion: "0.1.
|
|
539
|
+
signerVersion: "0.1.33-alpha.0",
|
|
532
540
|
// Sourced from the SDK, never a literal (#1161). This field read '20.0.0'
|
|
533
541
|
// while every package's `engines` said `>=24` and the docs said `>=24.0.0`,
|
|
534
542
|
// so the guard that was supposed to enforce the floor waved Node v23 through
|
|
@@ -670,6 +678,97 @@ function isHermesEnvAssignment(line, envKey) {
|
|
|
670
678
|
function isAmbiguousHermesEnvLine(line, envKey) {
|
|
671
679
|
return new RegExp(`^\\s*(?:export[ \\t]+)?${envKey}\\b`).test(line);
|
|
672
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
|
+
}
|
|
673
772
|
function appendHermesMcpServers(source, servers) {
|
|
674
773
|
const documentEnd = /(?:^|\n)[ \t]*\.\.\.[ \t]*(?:#[^\n]*)?\r?\n?$/.exec(source);
|
|
675
774
|
if (documentEnd) {
|
|
@@ -2659,14 +2758,49 @@ var init_runtime_install = __esm({
|
|
|
2659
2758
|
}
|
|
2660
2759
|
});
|
|
2661
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
|
+
|
|
2662
2769
|
// src/tombstone.ts
|
|
2663
2770
|
var tombstone_exports = {};
|
|
2664
2771
|
__export(tombstone_exports, {
|
|
2665
2772
|
TOMBSTONE_FILENAME: () => TOMBSTONE_FILENAME,
|
|
2666
2773
|
TOMBSTONE_MARKER: () => TOMBSTONE_MARKER,
|
|
2774
|
+
defaultTombstonesDir: () => defaultTombstonesDir,
|
|
2667
2775
|
readAgentTombstone: () => readAgentTombstone,
|
|
2776
|
+
readTombstoneRecords: () => readTombstoneRecords,
|
|
2668
2777
|
writeAgentTombstone: () => writeAgentTombstone
|
|
2669
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
|
+
}
|
|
2670
2804
|
function tombstoneScript(info) {
|
|
2671
2805
|
const lines = [
|
|
2672
2806
|
`${TOMBSTONE_MARKER}: this Haven agent was retired.`,
|
|
@@ -2697,7 +2831,11 @@ function tombstoneScript(info) {
|
|
|
2697
2831
|
async function writeAgentTombstone(input) {
|
|
2698
2832
|
const dirStat = await promises.stat(input.directory).catch(() => null);
|
|
2699
2833
|
if (!dirStat?.isDirectory()) {
|
|
2700
|
-
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
|
+
);
|
|
2701
2839
|
}
|
|
2702
2840
|
const info = {
|
|
2703
2841
|
// reason / replaced_by are persisted to disk and re-emitted to the host's
|
|
@@ -2714,8 +2852,13 @@ async function writeAgentTombstone(input) {
|
|
|
2714
2852
|
const wrapperPath = path.join(binDir, "haven-signer.mjs");
|
|
2715
2853
|
await promises.writeFile(wrapperPath, tombstoneScript(info), "utf8");
|
|
2716
2854
|
await promises.chmod(wrapperPath, 493);
|
|
2717
|
-
|
|
2718
|
-
|
|
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 };
|
|
2719
2862
|
}
|
|
2720
2863
|
async function readAgentTombstone(directory) {
|
|
2721
2864
|
try {
|
|
@@ -2726,15 +2869,185 @@ async function readAgentTombstone(directory) {
|
|
|
2726
2869
|
return null;
|
|
2727
2870
|
}
|
|
2728
2871
|
}
|
|
2729
|
-
var TOMBSTONE_FILENAME, TOMBSTONE_MARKER;
|
|
2872
|
+
var TOMBSTONE_FILENAME, MIRROR_MODE, TOMBSTONE_MARKER;
|
|
2730
2873
|
var init_tombstone = __esm({
|
|
2731
2874
|
"src/tombstone.ts"() {
|
|
2732
2875
|
init_redact();
|
|
2876
|
+
init_connect_error();
|
|
2733
2877
|
TOMBSTONE_FILENAME = "TOMBSTONE.json";
|
|
2878
|
+
MIRROR_MODE = 384;
|
|
2734
2879
|
TOMBSTONE_MARKER = "HAVEN-TOMBSTONE";
|
|
2735
2880
|
}
|
|
2736
2881
|
});
|
|
2737
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
|
+
|
|
2738
3051
|
// src/rekey.ts
|
|
2739
3052
|
var rekey_exports = {};
|
|
2740
3053
|
__export(rekey_exports, {
|
|
@@ -2794,7 +3107,7 @@ async function startRekey(options, deps = {}) {
|
|
|
2794
3107
|
async function finishRekey(options, deps = {}) {
|
|
2795
3108
|
const now = deps.now ?? (() => Date.now());
|
|
2796
3109
|
if (!options.newApiKey) {
|
|
2797
|
-
throw new Error(
|
|
3110
|
+
throw new Error(REKEY_FINISH_NEEDS_API_KEY);
|
|
2798
3111
|
}
|
|
2799
3112
|
const stored = await readStoredCredentials(
|
|
2800
3113
|
options.serverName,
|
|
@@ -2937,6 +3250,7 @@ var init_rekey = __esm({
|
|
|
2937
3250
|
init_signer_runtime();
|
|
2938
3251
|
init_key();
|
|
2939
3252
|
init_redact();
|
|
3253
|
+
init_rekey_messages();
|
|
2940
3254
|
init_server_names();
|
|
2941
3255
|
init_storage();
|
|
2942
3256
|
}
|
|
@@ -3416,6 +3730,15 @@ async function runDoctor(input, deps = {}) {
|
|
|
3416
3730
|
if (revoked.length > 0) parts.push(`already revoked: ${revoked.join(", ")}`);
|
|
3417
3731
|
if (retired.length > 0) parts.push(`tombstoned (keys removed): ${retired.join(", ")}`);
|
|
3418
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
|
+
}
|
|
3419
3742
|
const supersededLive = live.filter((item) => item.entry.classification !== "wired").map((item) => item.label);
|
|
3420
3743
|
checks.push({
|
|
3421
3744
|
id: "superseded_agents",
|
|
@@ -3650,6 +3973,14 @@ async function scanInstalledClients(options = {}) {
|
|
|
3650
3973
|
return SCAN_ORDER.indexOf(a.runtime) - SCAN_ORDER.indexOf(b.runtime);
|
|
3651
3974
|
});
|
|
3652
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
|
+
}
|
|
3653
3984
|
var MAX_PROMPT_ATTEMPTS = 3;
|
|
3654
3985
|
async function promptForInstalledClient(candidates, io = defaultPromptIo()) {
|
|
3655
3986
|
if (candidates.length === 0) throw noInstalledClientsError();
|
|
@@ -3723,9 +4054,35 @@ function defaultPromptIo() {
|
|
|
3723
4054
|
// src/runtime.ts
|
|
3724
4055
|
init_local_mcp_runtime();
|
|
3725
4056
|
init_runtime_manifest();
|
|
3726
|
-
var CONNECTOR_VERSION = "0.1.
|
|
4057
|
+
var CONNECTOR_VERSION = "0.1.33-alpha.0";
|
|
3727
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
|
+
}
|
|
3728
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) {
|
|
3729
4086
|
assertSupportedNodeVersion(deps.nodeVersion, exports.MCP_RUNTIME_MANIFEST.minimumNodeVersion);
|
|
3730
4087
|
const connectorVersion = options.connectorVersion ?? CONNECTOR_VERSION;
|
|
3731
4088
|
const api = deps.api ?? createConnectApiClient(options.apiBaseUrl);
|
|
@@ -3745,9 +4102,10 @@ async function runConnect(options, deps = {}) {
|
|
|
3745
4102
|
promptForRuntime: runtimeSelectionPrompt(options, deps)
|
|
3746
4103
|
});
|
|
3747
4104
|
if (!selection.runtime) {
|
|
4105
|
+
const hint = await installedClientHintFor(deps);
|
|
3748
4106
|
throw new exports.ConnectError(
|
|
3749
4107
|
"runtime_undetermined",
|
|
3750
|
-
`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
|
|
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.",
|
|
3751
4109
|
"rerun_connect_with_explicit_runtime",
|
|
3752
4110
|
// #2091: the values must ride structurally too. The backend's setup
|
|
3753
4111
|
// prompt permits a retry only with "one of the values that refusal
|
|
@@ -3755,10 +4113,16 @@ async function runConnect(options, deps = {}) {
|
|
|
3755
4113
|
// every automation run in an undetected runtime (Codex in the field:
|
|
3756
4114
|
// npx needs network, Codex runs network commands unsandboxed, and the
|
|
3757
4115
|
// unsandboxed path carries none of the CODEX_* detection vars).
|
|
3758
|
-
|
|
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 }
|
|
3759
4122
|
);
|
|
3760
4123
|
}
|
|
3761
4124
|
const runtime = selection.runtime;
|
|
4125
|
+
trace.runtime = runtime;
|
|
3762
4126
|
const installCapabilities = runtimeInstallCapabilities(runtime);
|
|
3763
4127
|
if (options.localMcp) {
|
|
3764
4128
|
if (!supportsLocalMcp(runtime)) {
|
|
@@ -3854,6 +4218,7 @@ async function runConnect(options, deps = {}) {
|
|
|
3854
4218
|
x402BindingSigner: setup.x402_binding_signer ?? void 0,
|
|
3855
4219
|
warn: log
|
|
3856
4220
|
});
|
|
4221
|
+
trace.directory = credentialPaths.directory;
|
|
3857
4222
|
log(`Stored Haven identity credential locally: ${credentialPaths.identityPath}`);
|
|
3858
4223
|
log(`Stored local signer credential locally: ${credentialPaths.signerPath}`);
|
|
3859
4224
|
log(`Stored non-secret agent orientation locally: ${credentialPaths.agentPath}`);
|
|
@@ -3901,6 +4266,9 @@ async function runConnect(options, deps = {}) {
|
|
|
3901
4266
|
errorCode: early.errorCode ?? null,
|
|
3902
4267
|
environmentLabel: options.environmentLabel ?? "Local workspace"
|
|
3903
4268
|
});
|
|
4269
|
+
if (!early.errorCode || early.errorCode === "manual_runtime_setup_required") {
|
|
4270
|
+
log("\u2192 Action needed: approve this agent's budget in the Haven dashboard \u2014 the approval button is live now. Setup continues here in the meantime.");
|
|
4271
|
+
}
|
|
3904
4272
|
} catch {
|
|
3905
4273
|
}
|
|
3906
4274
|
}
|
|
@@ -3911,12 +4279,13 @@ async function runConnect(options, deps = {}) {
|
|
|
3911
4279
|
} else {
|
|
3912
4280
|
log("Haven setup on this machine is complete.");
|
|
3913
4281
|
}
|
|
4282
|
+
let supersededAgentIds = [];
|
|
3914
4283
|
try {
|
|
3915
|
-
|
|
3916
|
-
if (
|
|
4284
|
+
supersededAgentIds = await listOtherAgentIds(options.credentialsDir, credentialPaths.directory);
|
|
4285
|
+
if (supersededAgentIds.length > 0) {
|
|
3917
4286
|
log("");
|
|
3918
4287
|
log(
|
|
3919
|
-
`Heads-up: this setup created a NEW agent. Your previous agent(s) \u2014 ${
|
|
4288
|
+
`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.`
|
|
3920
4289
|
);
|
|
3921
4290
|
log(
|
|
3922
4291
|
`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.`
|
|
@@ -3951,17 +4320,23 @@ async function runConnect(options, deps = {}) {
|
|
|
3951
4320
|
approval = await waitForBudgetApproval(api, registration.setup_id, localApiKey, log, options.approvalWait);
|
|
3952
4321
|
}
|
|
3953
4322
|
printNextSteps(runtimeInstall, log, approval);
|
|
4323
|
+
const outcome = completionOutcome({
|
|
4324
|
+
runtimeInstall,
|
|
4325
|
+
delegateAddress: registration.delegate_address,
|
|
4326
|
+
hostedMcpUrl: registration.hosted_mcp_url,
|
|
4327
|
+
supersededAgentIds,
|
|
4328
|
+
setupChallengeExpiresAt: setup.challenge.expires_at,
|
|
4329
|
+
approvalRequired: registration.agent_status === "pending_approval"
|
|
4330
|
+
});
|
|
4331
|
+
if (await recordConnectOutcome(deps, credentialPaths.directory, outcome)) {
|
|
4332
|
+
log(`Saved this run's outcome to ${exports.CONNECT_OUTCOME_FILENAME} in the agent's credential directory.`);
|
|
4333
|
+
}
|
|
3954
4334
|
return {
|
|
3955
4335
|
setupId: registration.setup_id,
|
|
3956
4336
|
agentId: registration.agent_id,
|
|
3957
4337
|
delegateAddress: registration.delegate_address,
|
|
3958
4338
|
credentialPaths,
|
|
3959
|
-
outcome
|
|
3960
|
-
runtimeInstall,
|
|
3961
|
-
delegateAddress: registration.delegate_address,
|
|
3962
|
-
setupChallengeExpiresAt: setup.challenge.expires_at,
|
|
3963
|
-
approvalRequired: registration.agent_status === "pending_approval"
|
|
3964
|
-
})
|
|
4339
|
+
outcome
|
|
3965
4340
|
};
|
|
3966
4341
|
}
|
|
3967
4342
|
function completionOutcome(input) {
|
|
@@ -3993,11 +4368,35 @@ function completionOutcome(input) {
|
|
|
3993
4368
|
// malformed value arrives, do not echo it into an automation-facing
|
|
3994
4369
|
// record; the human log has already been redacted separately.
|
|
3995
4370
|
delegate_address: /^0x[0-9a-fA-F]{40}$/.test(input.delegateAddress) ? shortAddress(input.delegateAddress) : "[delegate-address-redacted]",
|
|
4371
|
+
// The endpoint Connect wrote into the runtime's MCP config, verbatim — the
|
|
4372
|
+
// same string that already sits in the user's own config file. Passed
|
|
4373
|
+
// through the secret filter anyway: this is the automation contract, the
|
|
4374
|
+
// value is server-supplied, and belts are cheap (the #1589 stance).
|
|
4375
|
+
...input.hostedMcpUrl ? { hosted_mcp_url: redactSecrets(input.hostedMcpUrl) } : {},
|
|
4376
|
+
// Always emitted on a completed run, empty list included: "no superseded
|
|
4377
|
+
// agents" is a fact a caller needs, and an omitted key would be
|
|
4378
|
+
// indistinguishable from an older connector that never reported it.
|
|
4379
|
+
superseded_agent_ids: input.supersededAgentIds ?? [],
|
|
3996
4380
|
...input.setupChallengeExpiresAt ? { setup_challenge_expires_at: input.setupChallengeExpiresAt } : {},
|
|
3997
4381
|
...runtimeInstall.errorCode ? { error: { code: runtimeInstall.errorCode, next_action: nextAction2 } } : {}
|
|
3998
4382
|
};
|
|
3999
4383
|
return outcome;
|
|
4000
4384
|
}
|
|
4385
|
+
async function installedClientHintFor(deps) {
|
|
4386
|
+
try {
|
|
4387
|
+
const candidates = await (deps.scanInstalledClients ?? scanInstalledClients)({ env: deps.env });
|
|
4388
|
+
const hint = installedClientHint(candidates);
|
|
4389
|
+
return hint.installedClients.length > 0 ? hint : {};
|
|
4390
|
+
} catch {
|
|
4391
|
+
return {};
|
|
4392
|
+
}
|
|
4393
|
+
}
|
|
4394
|
+
function installedClientProse(hint) {
|
|
4395
|
+
const found = hint.installedClients ?? [];
|
|
4396
|
+
if (found.length === 0) return "";
|
|
4397
|
+
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.";
|
|
4398
|
+
return `Haven can see these agent clients installed here, likeliest first: ${found.join(", ")}.${suggestion} `;
|
|
4399
|
+
}
|
|
4001
4400
|
function runtimeSelectionPrompt(options, deps) {
|
|
4002
4401
|
if (options.interactive !== true) return void 0;
|
|
4003
4402
|
if (!(deps.isTty ?? Boolean(process.stdin.isTTY))) return void 0;
|
|
@@ -4031,7 +4430,9 @@ function failedConnectOutcome(runtimeHint, error) {
|
|
|
4031
4430
|
// pinned by test). Redaction stays on as belt-and-braces; plain-Error
|
|
4032
4431
|
// runs still get their redacted message on stderr via the CLI mirror.
|
|
4033
4432
|
...error instanceof exports.ConnectError && message ? { message: redactForAutomation(message) } : {},
|
|
4034
|
-
...error instanceof exports.ConnectError && error.details.allowedRuntimes ? { allowed_runtimes: error.details.allowedRuntimes } : {}
|
|
4433
|
+
...error instanceof exports.ConnectError && error.details.allowedRuntimes ? { allowed_runtimes: error.details.allowedRuntimes } : {},
|
|
4434
|
+
...error instanceof exports.ConnectError && error.details.installedClients?.length ? { installed_clients: error.details.installedClients } : {},
|
|
4435
|
+
...error instanceof exports.ConnectError && error.details.suggestedRuntime ? { suggested_runtime: error.details.suggestedRuntime } : {}
|
|
4035
4436
|
}
|
|
4036
4437
|
};
|
|
4037
4438
|
}
|
|
@@ -4104,6 +4505,13 @@ function describeApprovedBudget(budget) {
|
|
|
4104
4505
|
const amount = token ? `${formatAtomicAmount(BigInt(budget.amount), token.decimals)} ${budget.token_symbol}` : `${budget.amount} ${budget.token_symbol} (atomic units)`;
|
|
4105
4506
|
return `${amount} ${describeResetPeriod(budget.reset_period_min)}`;
|
|
4106
4507
|
}
|
|
4508
|
+
function describeWaitBound(timeoutMs) {
|
|
4509
|
+
if (timeoutMs >= 12e4) {
|
|
4510
|
+
const minutes = Math.round(timeoutMs / 6e4);
|
|
4511
|
+
return `${minutes} minutes`;
|
|
4512
|
+
}
|
|
4513
|
+
return `${Math.round(timeoutMs / 1e3)}s`;
|
|
4514
|
+
}
|
|
4107
4515
|
async function waitForBudgetApproval(api, setupId, apiKey, log, options = {}) {
|
|
4108
4516
|
const intervalMs = options.intervalMs ?? 5e3;
|
|
4109
4517
|
const timeoutMs = options.timeoutMs ?? 18e4;
|
|
@@ -4114,7 +4522,7 @@ async function waitForBudgetApproval(api, setupId, apiKey, log, options = {}) {
|
|
|
4114
4522
|
const announceWaiting = () => {
|
|
4115
4523
|
if (waitingAnnounced) return;
|
|
4116
4524
|
waitingAnnounced = true;
|
|
4117
|
-
log("
|
|
4525
|
+
log("Now waiting for you to approve the budget in the Haven dashboard \u2014 approval is what unlocks the agent's Haven tools\u2026");
|
|
4118
4526
|
};
|
|
4119
4527
|
for (let i = 0; i < maxPolls; i++) {
|
|
4120
4528
|
if (i > 0) await sleep(intervalMs);
|
|
@@ -4141,7 +4549,10 @@ async function waitForBudgetApproval(api, setupId, apiKey, log, options = {}) {
|
|
|
4141
4549
|
}
|
|
4142
4550
|
announceWaiting();
|
|
4143
4551
|
if (i > 0 && i % remindEvery === 0) {
|
|
4144
|
-
|
|
4552
|
+
const elapsedSeconds = Math.round(i * intervalMs / 1e3);
|
|
4553
|
+
log(
|
|
4554
|
+
`Still waiting for budget approval in Haven \u2014 ${elapsedSeconds}s elapsed, checking every ${Math.round(intervalMs / 1e3)}s. I'll stop waiting after ${describeWaitBound(timeoutMs)}; you can approve later and the tools unlock then.`
|
|
4555
|
+
);
|
|
4145
4556
|
}
|
|
4146
4557
|
}
|
|
4147
4558
|
log(
|
|
@@ -4226,6 +4637,7 @@ function printNextSteps(result, log, approval) {
|
|
|
4226
4637
|
|
|
4227
4638
|
// src/args.ts
|
|
4228
4639
|
init_server_names();
|
|
4640
|
+
init_rekey_messages();
|
|
4229
4641
|
function parseArgs(argv, env = process.env) {
|
|
4230
4642
|
const options = {
|
|
4231
4643
|
apiBaseUrl: env.HAVEN_API_URL ?? "http://localhost:3001",
|
|
@@ -4240,6 +4652,8 @@ function parseArgs(argv, env = process.env) {
|
|
|
4240
4652
|
let tombstoneDir;
|
|
4241
4653
|
let tombstoneReason;
|
|
4242
4654
|
let tombstoneReplacedBy;
|
|
4655
|
+
let unwire;
|
|
4656
|
+
let unwireDir;
|
|
4243
4657
|
for (let i = 0; i < argv.length; i += 1) {
|
|
4244
4658
|
const arg = argv[i];
|
|
4245
4659
|
if (arg === "--help" || arg === "-h") {
|
|
@@ -4258,6 +4672,13 @@ function parseArgs(argv, env = process.env) {
|
|
|
4258
4672
|
newApiKey = requireValue(argv, ++i, arg);
|
|
4259
4673
|
} else if (arg === "--tombstone") {
|
|
4260
4674
|
tombstoneDir = requireValue(argv, ++i, arg);
|
|
4675
|
+
} else if (arg === "--unwire") {
|
|
4676
|
+
unwire = unwire ?? {};
|
|
4677
|
+
const next = argv[i + 1];
|
|
4678
|
+
if (next !== void 0 && !next.startsWith("--")) {
|
|
4679
|
+
unwireDir = next;
|
|
4680
|
+
i += 1;
|
|
4681
|
+
}
|
|
4261
4682
|
} else if (arg === "--reason") {
|
|
4262
4683
|
tombstoneReason = requireValue(argv, ++i, arg);
|
|
4263
4684
|
} else if (arg === "--replaced-by") {
|
|
@@ -4307,18 +4728,37 @@ function parseArgs(argv, env = process.env) {
|
|
|
4307
4728
|
);
|
|
4308
4729
|
}
|
|
4309
4730
|
if (rekey.phase === "finish" && !newApiKey) {
|
|
4310
|
-
throw new Error(
|
|
4731
|
+
throw new Error(REKEY_FINISH_NEEDS_API_KEY);
|
|
4311
4732
|
}
|
|
4312
4733
|
return { options, help, json, doctor, repair, tombstone, rekey };
|
|
4313
4734
|
}
|
|
4314
4735
|
if (newApiKey !== void 0) {
|
|
4315
4736
|
throw new Error("--api-key requires --rekey-finish.");
|
|
4316
4737
|
}
|
|
4317
|
-
if (!tombstoneDir && (tombstoneReason !== void 0 || tombstoneReplacedBy !== void 0)) {
|
|
4318
|
-
throw new Error("--reason and --replaced-by require --tombstone <dir
|
|
4738
|
+
if (!tombstoneDir && !unwire && (tombstoneReason !== void 0 || tombstoneReplacedBy !== void 0)) {
|
|
4739
|
+
throw new Error("--reason and --replaced-by require --tombstone <dir> or --unwire.");
|
|
4740
|
+
}
|
|
4741
|
+
if (unwire && (tombstoneReason !== void 0 || tombstoneReplacedBy !== void 0)) {
|
|
4742
|
+
unwire = {
|
|
4743
|
+
...tombstoneReason !== void 0 ? { reason: tombstoneReason } : {},
|
|
4744
|
+
...tombstoneReplacedBy !== void 0 ? { replacedBy: tombstoneReplacedBy } : {},
|
|
4745
|
+
...unwire
|
|
4746
|
+
};
|
|
4319
4747
|
}
|
|
4320
4748
|
if (tombstone) {
|
|
4321
|
-
|
|
4749
|
+
if (unwire) {
|
|
4750
|
+
throw new Error("--unwire and --tombstone are separate operations; run one per invocation.");
|
|
4751
|
+
}
|
|
4752
|
+
return { options, help, json, doctor, repair, tombstone, rekey, unwire, unwireDir };
|
|
4753
|
+
}
|
|
4754
|
+
if (unwire) {
|
|
4755
|
+
if (options.setupToken) {
|
|
4756
|
+
throw new Error("--unwire removes existing wiring; it does not take --setup. Drop --setup.");
|
|
4757
|
+
}
|
|
4758
|
+
if (!unwireDir && !options.serverName && !options.credentialsDir) {
|
|
4759
|
+
throw new Error("--unwire needs a target: --unwire <dir>, --unwire --name <slug>, or --unwire --credentials-dir <path>.");
|
|
4760
|
+
}
|
|
4761
|
+
return { options, help, json, doctor, repair, tombstone, rekey, unwire, unwireDir };
|
|
4322
4762
|
}
|
|
4323
4763
|
if (doctor || repair) {
|
|
4324
4764
|
if (!options.runtime) {
|
|
@@ -4381,8 +4821,20 @@ function helpText() {
|
|
|
4381
4821
|
" --tombstone <dir> Retire an agent credential directory in place (no token): replaces its signer",
|
|
4382
4822
|
" wrapper with a diagnostic that names the retirement in MCP stderr logs, and",
|
|
4383
4823
|
" writes TOMBSTONE.json. Touches NO key material and revokes nothing.",
|
|
4384
|
-
" --
|
|
4385
|
-
"
|
|
4824
|
+
" --unwire [<dir>] Remove one agent\u2019s wiring from every runtime config it appears in (no token):",
|
|
4825
|
+
" tombstone-first, then drops the hosted + signer MCP pair from Hermes YAML, Codex",
|
|
4826
|
+
" TOML and the JSON configs (Cursor, VS Code, Insiders, Claude Desktop), plus the",
|
|
4827
|
+
" Hermes dotenv API-key line (MCP_HAVEN_API_KEY / MCP_HAVEN_<SLUG>_API_KEY).",
|
|
4828
|
+
" Target the directory directly, or add --name <slug> or --credentials-dir.",
|
|
4829
|
+
" An UNNAMED pair is only removed when this directory\u2019s wrapper is the one the",
|
|
4830
|
+
" config launches (or its key is the one the Hermes env holds); otherwise the",
|
|
4831
|
+
" command refuses rather than unwire a different agent \u2014 it never touches a",
|
|
4832
|
+
" pair another agent owns. Tears down the target directory: its signer key and",
|
|
4833
|
+
" API key are removed locally (record kept via the #2155 tombstone mirror) and",
|
|
4834
|
+
" nothing is ever revoked on the backend \u2014 that stays an owner action on the",
|
|
4835
|
+
" Haven agent page.",
|
|
4836
|
+
" --reason <text> Reason recorded in the tombstone (with --tombstone or --unwire).",
|
|
4837
|
+
" --replaced-by <agent-id> Successor agent recorded in the tombstone (with --tombstone or --unwire).",
|
|
4386
4838
|
" --help Show this help.",
|
|
4387
4839
|
"",
|
|
4388
4840
|
"The connector never prints the private key and never sends it to Haven. JSON output never includes credential contents or full credential paths."
|
|
@@ -4401,6 +4853,28 @@ init_api();
|
|
|
4401
4853
|
init_key();
|
|
4402
4854
|
init_redact();
|
|
4403
4855
|
init_redact();
|
|
4856
|
+
init_connect_error();
|
|
4857
|
+
function failSubcommand(io, json, err, envelope, fallback) {
|
|
4858
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4859
|
+
io.stderr(`${redactSecrets(message)}
|
|
4860
|
+
`);
|
|
4861
|
+
if (json) {
|
|
4862
|
+
io.stdout(
|
|
4863
|
+
`${redactSecrets(
|
|
4864
|
+
JSON.stringify({
|
|
4865
|
+
...envelope,
|
|
4866
|
+
error: {
|
|
4867
|
+
code: isConnectError(err) ? err.code : fallback.code,
|
|
4868
|
+
next_action: isConnectError(err) ? err.nextAction : fallback.nextAction,
|
|
4869
|
+
...isConnectError(err) && message ? { message } : {}
|
|
4870
|
+
}
|
|
4871
|
+
})
|
|
4872
|
+
)}
|
|
4873
|
+
`
|
|
4874
|
+
);
|
|
4875
|
+
}
|
|
4876
|
+
return 1;
|
|
4877
|
+
}
|
|
4404
4878
|
async function runCli(argv, io = {
|
|
4405
4879
|
stdout: (message) => process.stdout.write(message),
|
|
4406
4880
|
stderr: (message) => process.stderr.write(message)
|
|
@@ -4428,13 +4902,13 @@ async function runCli(argv, io = {
|
|
|
4428
4902
|
}
|
|
4429
4903
|
if (parsed.tombstone) {
|
|
4430
4904
|
const { writeAgentTombstone: writeAgentTombstone2 } = await Promise.resolve().then(() => (init_tombstone(), tombstone_exports));
|
|
4431
|
-
const { readFile:
|
|
4432
|
-
const { join:
|
|
4905
|
+
const { readFile: readFile13 } = await import('fs/promises');
|
|
4906
|
+
const { join: join11 } = await import('path');
|
|
4433
4907
|
try {
|
|
4434
4908
|
let agentId = "unknown";
|
|
4435
4909
|
try {
|
|
4436
4910
|
const identity = JSON.parse(
|
|
4437
|
-
await
|
|
4911
|
+
await readFile13(join11(parsed.tombstone.directory, "identity.json"), "utf8")
|
|
4438
4912
|
);
|
|
4439
4913
|
agentId = identity.agent_id ?? "unknown";
|
|
4440
4914
|
} catch {
|
|
@@ -4455,14 +4929,80 @@ async function runCli(argv, io = {
|
|
|
4455
4929
|
"Key files were NOT touched and nothing was revoked \u2014 revoke the agent on the Haven agent page if you have not already.\n"
|
|
4456
4930
|
);
|
|
4457
4931
|
io.stdout(
|
|
4458
|
-
|
|
4932
|
+
`A surviving tombstone record was mirrored to ${redactSecrets(info.recordPath)}
|
|
4933
|
+
`
|
|
4934
|
+
);
|
|
4935
|
+
io.stdout(
|
|
4936
|
+
"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"
|
|
4459
4937
|
);
|
|
4460
4938
|
}
|
|
4461
4939
|
return 0;
|
|
4462
4940
|
} catch (err) {
|
|
4463
|
-
io.
|
|
4464
|
-
|
|
4465
|
-
|
|
4941
|
+
return failSubcommand(io, parsed.json, err, { tombstoned: false }, {
|
|
4942
|
+
code: "tombstone_failed",
|
|
4943
|
+
nextAction: "review_the_error_and_retry_with_a_valid_agent_directory"
|
|
4944
|
+
});
|
|
4945
|
+
}
|
|
4946
|
+
}
|
|
4947
|
+
if (parsed.unwire) {
|
|
4948
|
+
const { unwireAgent: unwireAgent2 } = await Promise.resolve().then(() => (init_unwire(), unwire_exports));
|
|
4949
|
+
const { homedir: homedir10 } = await import('os');
|
|
4950
|
+
const { join: join11 } = await import('path');
|
|
4951
|
+
const homeDir = homedir10();
|
|
4952
|
+
const root = parsed.options.credentialsDir ?? join11(homeDir, ".haven", "agents");
|
|
4953
|
+
const directory = parsed.unwireDir ?? (parsed.options.serverName ? join11(root, parsed.options.serverName) : root);
|
|
4954
|
+
try {
|
|
4955
|
+
const result = await unwireAgent2({
|
|
4956
|
+
directory,
|
|
4957
|
+
slug: parsed.options.serverName,
|
|
4958
|
+
reason: parsed.unwire.reason,
|
|
4959
|
+
replacedBy: parsed.unwire.replacedBy,
|
|
4960
|
+
homeDir
|
|
4961
|
+
});
|
|
4962
|
+
const failures = result.runtimes.filter((r) => r.status === "refused" || r.status === "unreadable");
|
|
4963
|
+
if (parsed.json) {
|
|
4964
|
+
io.stdout(
|
|
4965
|
+
`${redactSecrets(
|
|
4966
|
+
JSON.stringify({
|
|
4967
|
+
unwired: true,
|
|
4968
|
+
agent_id: result.agentId,
|
|
4969
|
+
slug: result.slug ?? null,
|
|
4970
|
+
directory: result.directory,
|
|
4971
|
+
tombstoned: result.tombstoned,
|
|
4972
|
+
runtimes: result.runtimes.map((r) => ({
|
|
4973
|
+
runtime: r.runtime,
|
|
4974
|
+
label: r.label,
|
|
4975
|
+
status: r.status,
|
|
4976
|
+
...r.detail ? { detail: r.detail } : {}
|
|
4977
|
+
}))
|
|
4978
|
+
})
|
|
4979
|
+
)}
|
|
4980
|
+
`
|
|
4981
|
+
);
|
|
4982
|
+
} else {
|
|
4983
|
+
io.stdout(redactSecrets(`Unwired agent ${result.agentId} at ${result.directory}.
|
|
4984
|
+
`));
|
|
4985
|
+
io.stdout(
|
|
4986
|
+
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"
|
|
4987
|
+
);
|
|
4988
|
+
for (const r of result.runtimes) {
|
|
4989
|
+
const mark = r.status === "removed" ? "\u2713" : r.status === "clean" ? "\u2013" : "\u2717";
|
|
4990
|
+
io.stdout(redactSecrets(` ${mark} ${r.label}: ${r.status}${r.detail ? ` \u2014 ${r.detail}` : ""}
|
|
4991
|
+
`));
|
|
4992
|
+
}
|
|
4993
|
+
io.stdout(
|
|
4994
|
+
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"
|
|
4995
|
+
);
|
|
4996
|
+
io.stdout(
|
|
4997
|
+
"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"
|
|
4998
|
+
);
|
|
4999
|
+
}
|
|
5000
|
+
return failures.length > 0 ? 1 : 0;
|
|
5001
|
+
} catch (err) {
|
|
5002
|
+
return failSubcommand(io, parsed.json, err, { unwired: false }, {
|
|
5003
|
+
code: "unwire_failed",
|
|
5004
|
+
nextAction: "review_the_error_and_rerun_unwire_which_is_idempotent"
|
|
5005
|
+
});
|
|
4466
5006
|
}
|
|
4467
5007
|
}
|
|
4468
5008
|
if (parsed.rekey) {
|
|
@@ -4518,9 +5058,10 @@ async function runCli(argv, io = {
|
|
|
4518
5058
|
}
|
|
4519
5059
|
return 0;
|
|
4520
5060
|
} catch (err) {
|
|
4521
|
-
io.
|
|
4522
|
-
|
|
4523
|
-
|
|
5061
|
+
return failSubcommand(io, parsed.json, err, { rekey: "failed" }, {
|
|
5062
|
+
code: "rekey_failed",
|
|
5063
|
+
nextAction: "review_the_error_and_rerun_the_rekey_phase"
|
|
5064
|
+
});
|
|
4524
5065
|
}
|
|
4525
5066
|
}
|
|
4526
5067
|
if (parsed.doctor || parsed.repair) {
|
|
@@ -4566,9 +5107,10 @@ async function runCli(argv, io = {
|
|
|
4566
5107
|
}
|
|
4567
5108
|
return report.ok ? 0 : 1;
|
|
4568
5109
|
} catch (err) {
|
|
4569
|
-
io.
|
|
4570
|
-
|
|
4571
|
-
|
|
5110
|
+
return failSubcommand(io, parsed.json, err, { doctor: "failed" }, {
|
|
5111
|
+
code: "doctor_failed",
|
|
5112
|
+
nextAction: "review_the_error_and_rerun_doctor"
|
|
5113
|
+
});
|
|
4572
5114
|
}
|
|
4573
5115
|
}
|
|
4574
5116
|
try {
|
|
@@ -4595,7 +5137,7 @@ async function runCli(argv, io = {
|
|
|
4595
5137
|
if (parsed.json) {
|
|
4596
5138
|
io.stderr(`${redactForAutomation(err instanceof Error ? err.message : String(err))}
|
|
4597
5139
|
`);
|
|
4598
|
-
io.stdout(`${JSON.stringify(
|
|
5140
|
+
io.stdout(`${JSON.stringify(failureOutcomeFor(parsed.options.runtime, err))}
|
|
4599
5141
|
`);
|
|
4600
5142
|
} else {
|
|
4601
5143
|
io.stderr(`${redactSecrets(err instanceof Error ? err.message : String(err))}
|
|
@@ -4633,9 +5175,11 @@ exports.createConnectApiClient = createConnectApiClient;
|
|
|
4633
5175
|
exports.defaultAgentDirectory = defaultAgentDirectory;
|
|
4634
5176
|
exports.delegateKeyFromPrivateKey = delegateKeyFromPrivateKey;
|
|
4635
5177
|
exports.failedConnectOutcome = failedConnectOutcome;
|
|
5178
|
+
exports.failureOutcomeFor = failureOutcomeFor;
|
|
4636
5179
|
exports.generateDelegateKey = generateDelegateKey;
|
|
4637
5180
|
exports.helpText = helpText;
|
|
4638
5181
|
exports.installRuntime = installRuntime;
|
|
5182
|
+
exports.installedClientHint = installedClientHint;
|
|
4639
5183
|
exports.isConnectError = isConnectError;
|
|
4640
5184
|
exports.mcpPackageSpec = mcpPackageSpec;
|
|
4641
5185
|
exports.normalizeRuntime = normalizeRuntime;
|
|
@@ -4653,6 +5197,7 @@ exports.scanInstalledClients = scanInstalledClients;
|
|
|
4653
5197
|
exports.sdkPackageSpec = sdkPackageSpec;
|
|
4654
5198
|
exports.shortAddress = shortAddress;
|
|
4655
5199
|
exports.signerPackageSpec = signerPackageSpec;
|
|
5200
|
+
exports.writeConnectOutcomeRecord = writeConnectOutcomeRecord;
|
|
4656
5201
|
exports.writeCredentialFiles = writeCredentialFiles;
|
|
4657
5202
|
//# sourceMappingURL=index.cjs.map
|
|
4658
5203
|
//# sourceMappingURL=index.cjs.map
|