@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.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import crypto from 'crypto';
|
|
2
2
|
import { Wallet } from 'ethers';
|
|
3
|
-
import { mkdir, rm, stat, readdir, readFile, chmod, access,
|
|
3
|
+
import { mkdir, rm, stat, readdir, readFile, writeFile, chmod, access, unlink, rename } from 'fs/promises';
|
|
4
4
|
import { homedir, platform } from 'os';
|
|
5
5
|
import { join, resolve, dirname, basename } from 'path';
|
|
6
6
|
import { ensureConsent, computeConsentHash, loadCredentials, consentInputFromClient, registeredToolNames, MCP_VERSION } from '@haven_ai/mcp';
|
|
@@ -496,11 +496,19 @@ async function restrictPermissions(path, mode, warn) {
|
|
|
496
496
|
);
|
|
497
497
|
}
|
|
498
498
|
}
|
|
499
|
-
|
|
499
|
+
async function writeConnectOutcomeRecord(directory, outcome, warn) {
|
|
500
|
+
const path = join(directory, CONNECT_OUTCOME_FILENAME);
|
|
501
|
+
await writeFile(path, `${JSON.stringify(outcome, null, 2)}
|
|
502
|
+
`, { mode: 384 });
|
|
503
|
+
await restrictPermissions(path, 384, warn);
|
|
504
|
+
return path;
|
|
505
|
+
}
|
|
506
|
+
var REKEY_PENDING_FILENAME, REKEY_PENDING_TTL_MS, CONNECT_OUTCOME_FILENAME;
|
|
500
507
|
var init_storage = __esm({
|
|
501
508
|
"src/storage.ts"() {
|
|
502
509
|
REKEY_PENDING_FILENAME = "rekey-pending.json";
|
|
503
510
|
REKEY_PENDING_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
511
|
+
CONNECT_OUTCOME_FILENAME = "last-connect-outcome.json";
|
|
504
512
|
}
|
|
505
513
|
});
|
|
506
514
|
function mcpPackageSpec() {
|
|
@@ -519,9 +527,9 @@ var init_runtime_manifest = __esm({
|
|
|
519
527
|
mcpPackage: "@haven_ai/mcp",
|
|
520
528
|
mcpVersion: MCP_VERSION,
|
|
521
529
|
sdkPackage: "@haven_ai/sdk",
|
|
522
|
-
sdkVersion: "0.1.
|
|
530
|
+
sdkVersion: "0.1.33-alpha.0",
|
|
523
531
|
signerPackage: "@haven_ai/signer",
|
|
524
|
-
signerVersion: "0.1.
|
|
532
|
+
signerVersion: "0.1.33-alpha.0",
|
|
525
533
|
// Sourced from the SDK, never a literal (#1161). This field read '20.0.0'
|
|
526
534
|
// while every package's `engines` said `>=24` and the docs said `>=24.0.0`,
|
|
527
535
|
// so the guard that was supposed to enforce the floor waved Node v23 through
|
|
@@ -663,6 +671,97 @@ function isHermesEnvAssignment(line, envKey) {
|
|
|
663
671
|
function isAmbiguousHermesEnvLine(line, envKey) {
|
|
664
672
|
return new RegExp(`^\\s*(?:export[ \\t]+)?${envKey}\\b`).test(line);
|
|
665
673
|
}
|
|
674
|
+
function removeHermesYaml(existingYaml, names, configPath) {
|
|
675
|
+
if (!existingYaml?.trim()) return existingYaml ?? "";
|
|
676
|
+
const doc = parseDocument(existingYaml, { keepSourceTokens: true });
|
|
677
|
+
if (doc.errors.length > 0 || !isMap(doc.contents)) {
|
|
678
|
+
throw new UnreadableRuntimeConfigError(configPath ?? "the Hermes config", "it is not a YAML object");
|
|
679
|
+
}
|
|
680
|
+
const mcpPair = doc.contents.items.find((item) => item.key?.toString() === "mcp_servers");
|
|
681
|
+
if (!mcpPair || !isMap(mcpPair.value)) return existingYaml;
|
|
682
|
+
const toRemove = mcpPair.value.items.filter((item) => {
|
|
683
|
+
const name = item.key?.toString();
|
|
684
|
+
return name === names.hosted || name === names.signer;
|
|
685
|
+
});
|
|
686
|
+
if (toRemove.length === 0) return existingYaml;
|
|
687
|
+
if (toRemove.length !== mcpPair.value.items.length && toRemove.some((item) => !item.key?.range || !item.value?.range)) {
|
|
688
|
+
throw new UnreadableRuntimeConfigError(configPath ?? "the Hermes config", "an MCP server entry cannot be safely removed");
|
|
689
|
+
}
|
|
690
|
+
let out = existingYaml;
|
|
691
|
+
const ranges = toRemove.map((item) => {
|
|
692
|
+
const keyStart = item.key.range[0];
|
|
693
|
+
const valueEnd = item.value.range[1];
|
|
694
|
+
const lineStart = existingYaml.lastIndexOf("\n", keyStart - 1) + 1;
|
|
695
|
+
let end = valueEnd;
|
|
696
|
+
if (existingYaml.slice(end).startsWith("\r\n")) end += 2;
|
|
697
|
+
else if (existingYaml[end] === "\n") end += 1;
|
|
698
|
+
return [lineStart, end];
|
|
699
|
+
});
|
|
700
|
+
ranges.sort((a, b) => b[0] - a[0]);
|
|
701
|
+
for (const [start, end] of ranges) out = out.slice(0, start) + out.slice(end);
|
|
702
|
+
if (toRemove.length === mcpPair.value.items.length) {
|
|
703
|
+
const reparsed = parseDocument(out, { keepSourceTokens: true });
|
|
704
|
+
if (!reparsed.errors.length && isMap(reparsed.contents)) {
|
|
705
|
+
const pair = reparsed.contents.items.find((item) => item.key?.toString() === "mcp_servers");
|
|
706
|
+
if (pair && pair.key?.range && pair.value?.range) {
|
|
707
|
+
const keyStart = pair.key.range[0];
|
|
708
|
+
const valueEnd = pair.value.range[1];
|
|
709
|
+
const lineStart = out.lastIndexOf("\n", keyStart - 1) + 1;
|
|
710
|
+
let end = valueEnd;
|
|
711
|
+
if (out.slice(end).startsWith("\r\n")) end += 2;
|
|
712
|
+
else if (out[end] === "\n") end += 1;
|
|
713
|
+
out = out.slice(0, lineStart) + out.slice(end);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
return out;
|
|
718
|
+
}
|
|
719
|
+
function removeHermesEnv(existingEnv, envKey) {
|
|
720
|
+
if (!existingEnv) return "";
|
|
721
|
+
const lineEnding = existingEnv.includes("\r\n") ? "\r\n" : "\n";
|
|
722
|
+
const hasTrailingNewline = /\r?\n$/.test(existingEnv);
|
|
723
|
+
const lines = existingEnv.split(/\r?\n/);
|
|
724
|
+
if (hasTrailingNewline) lines.pop();
|
|
725
|
+
let removed = 0;
|
|
726
|
+
const kept = lines.filter((line) => {
|
|
727
|
+
if (isHermesEnvAssignment(line, envKey)) {
|
|
728
|
+
removed += 1;
|
|
729
|
+
return false;
|
|
730
|
+
}
|
|
731
|
+
if (isAmbiguousHermesEnvLine(line, envKey)) {
|
|
732
|
+
throw new Error("Hermes environment contains an ambiguous managed key");
|
|
733
|
+
}
|
|
734
|
+
return true;
|
|
735
|
+
});
|
|
736
|
+
if (removed === 0) return existingEnv;
|
|
737
|
+
const joined = kept.join(lineEnding);
|
|
738
|
+
return hasTrailingNewline ? `${joined}${lineEnding}` : joined;
|
|
739
|
+
}
|
|
740
|
+
function removeJsonMcpConfig(existingJson, serverRoot, names, configPath) {
|
|
741
|
+
if (!existingJson?.trim()) return existingJson ?? "";
|
|
742
|
+
const config = parseJsonObject(existingJson, configPath);
|
|
743
|
+
const root = config[serverRoot];
|
|
744
|
+
if (!root || typeof root !== "object" || Array.isArray(root)) return existingJson;
|
|
745
|
+
const servers = root;
|
|
746
|
+
let removed = false;
|
|
747
|
+
for (const name of [names.hosted, names.signer]) {
|
|
748
|
+
if (name in servers) {
|
|
749
|
+
delete servers[name];
|
|
750
|
+
removed = true;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
if (!removed) return existingJson;
|
|
754
|
+
if (Object.keys(servers).length === 0) delete config[serverRoot];
|
|
755
|
+
return `${JSON.stringify(config, null, 2)}
|
|
756
|
+
`;
|
|
757
|
+
}
|
|
758
|
+
function removeCodexToml(existingToml, names) {
|
|
759
|
+
let next = removeTomlTableTree(
|
|
760
|
+
removeTomlTableTree(existingToml, `mcp_servers.${names.codexHosted}`),
|
|
761
|
+
`mcp_servers.${names.codexSigner}`
|
|
762
|
+
);
|
|
763
|
+
return next;
|
|
764
|
+
}
|
|
666
765
|
function appendHermesMcpServers(source, servers) {
|
|
667
766
|
const documentEnd = /(?:^|\n)[ \t]*\.\.\.[ \t]*(?:#[^\n]*)?\r?\n?$/.exec(source);
|
|
668
767
|
if (documentEnd) {
|
|
@@ -2652,14 +2751,49 @@ var init_runtime_install = __esm({
|
|
|
2652
2751
|
}
|
|
2653
2752
|
});
|
|
2654
2753
|
|
|
2754
|
+
// src/rekey-messages.ts
|
|
2755
|
+
var REKEY_FINISH_NEEDS_API_KEY;
|
|
2756
|
+
var init_rekey_messages = __esm({
|
|
2757
|
+
"src/rekey-messages.ts"() {
|
|
2758
|
+
REKEY_FINISH_NEEDS_API_KEY = "--rekey-finish needs --api-key <key> \u2014 the one the Haven agent page showed once.";
|
|
2759
|
+
}
|
|
2760
|
+
});
|
|
2761
|
+
|
|
2655
2762
|
// src/tombstone.ts
|
|
2656
2763
|
var tombstone_exports = {};
|
|
2657
2764
|
__export(tombstone_exports, {
|
|
2658
2765
|
TOMBSTONE_FILENAME: () => TOMBSTONE_FILENAME,
|
|
2659
2766
|
TOMBSTONE_MARKER: () => TOMBSTONE_MARKER,
|
|
2767
|
+
defaultTombstonesDir: () => defaultTombstonesDir,
|
|
2660
2768
|
readAgentTombstone: () => readAgentTombstone,
|
|
2769
|
+
readTombstoneRecords: () => readTombstoneRecords,
|
|
2661
2770
|
writeAgentTombstone: () => writeAgentTombstone
|
|
2662
2771
|
});
|
|
2772
|
+
function defaultTombstonesDir(baseDir) {
|
|
2773
|
+
return join(baseDir ?? join(homedir(), ".haven"), "tombstones");
|
|
2774
|
+
}
|
|
2775
|
+
async function readTombstoneRecords(tombstonesDir) {
|
|
2776
|
+
const root = tombstonesDir ?? defaultTombstonesDir();
|
|
2777
|
+
let entries = [];
|
|
2778
|
+
try {
|
|
2779
|
+
entries = await readdir(root);
|
|
2780
|
+
} catch {
|
|
2781
|
+
return [];
|
|
2782
|
+
}
|
|
2783
|
+
const records = [];
|
|
2784
|
+
for (const entry of entries) {
|
|
2785
|
+
if (!entry.endsWith(".json")) continue;
|
|
2786
|
+
const recordPath = join(root, entry);
|
|
2787
|
+
try {
|
|
2788
|
+
const parsed = JSON.parse(await readFile(recordPath, "utf8"));
|
|
2789
|
+
if (typeof parsed?.agent_id === "string") {
|
|
2790
|
+
records.push({ ...parsed, recordPath });
|
|
2791
|
+
}
|
|
2792
|
+
} catch {
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
return records;
|
|
2796
|
+
}
|
|
2663
2797
|
function tombstoneScript(info) {
|
|
2664
2798
|
const lines = [
|
|
2665
2799
|
`${TOMBSTONE_MARKER}: this Haven agent was retired.`,
|
|
@@ -2690,7 +2824,11 @@ function tombstoneScript(info) {
|
|
|
2690
2824
|
async function writeAgentTombstone(input) {
|
|
2691
2825
|
const dirStat = await stat(input.directory).catch(() => null);
|
|
2692
2826
|
if (!dirStat?.isDirectory()) {
|
|
2693
|
-
throw new
|
|
2827
|
+
throw new ConnectError(
|
|
2828
|
+
"tombstone_directory_not_found",
|
|
2829
|
+
`Not a directory: ${input.directory} \u2014 nothing to tombstone. Agent directories are named by their wiring SLUG when the agent has one, and by the agent id otherwise \u2014 so a path built from an agent id will not exist for a named agent. List ~/.haven/agents (or read the directories from --doctor --json) and pass one of those.`,
|
|
2830
|
+
"retry_with_an_existing_agent_directory"
|
|
2831
|
+
);
|
|
2694
2832
|
}
|
|
2695
2833
|
const info = {
|
|
2696
2834
|
// reason / replaced_by are persisted to disk and re-emitted to the host's
|
|
@@ -2707,8 +2845,13 @@ async function writeAgentTombstone(input) {
|
|
|
2707
2845
|
const wrapperPath = join(binDir, "haven-signer.mjs");
|
|
2708
2846
|
await writeFile(wrapperPath, tombstoneScript(info), "utf8");
|
|
2709
2847
|
await chmod(wrapperPath, 493);
|
|
2710
|
-
|
|
2711
|
-
|
|
2848
|
+
const record = JSON.stringify(info, null, 2) + "\n";
|
|
2849
|
+
await writeFile(join(input.directory, TOMBSTONE_FILENAME), record, "utf8");
|
|
2850
|
+
const root = input.tombstonesDir ?? defaultTombstonesDir();
|
|
2851
|
+
await mkdir(root, { recursive: true, mode: 448 });
|
|
2852
|
+
const recordPath = join(root, `${info.agent_id}.json`);
|
|
2853
|
+
await writeFile(recordPath, record, { mode: MIRROR_MODE });
|
|
2854
|
+
return { ...info, recordPath };
|
|
2712
2855
|
}
|
|
2713
2856
|
async function readAgentTombstone(directory) {
|
|
2714
2857
|
try {
|
|
@@ -2719,15 +2862,185 @@ async function readAgentTombstone(directory) {
|
|
|
2719
2862
|
return null;
|
|
2720
2863
|
}
|
|
2721
2864
|
}
|
|
2722
|
-
var TOMBSTONE_FILENAME, TOMBSTONE_MARKER;
|
|
2865
|
+
var TOMBSTONE_FILENAME, MIRROR_MODE, TOMBSTONE_MARKER;
|
|
2723
2866
|
var init_tombstone = __esm({
|
|
2724
2867
|
"src/tombstone.ts"() {
|
|
2725
2868
|
init_redact();
|
|
2869
|
+
init_connect_error();
|
|
2726
2870
|
TOMBSTONE_FILENAME = "TOMBSTONE.json";
|
|
2871
|
+
MIRROR_MODE = 384;
|
|
2727
2872
|
TOMBSTONE_MARKER = "HAVEN-TOMBSTONE";
|
|
2728
2873
|
}
|
|
2729
2874
|
});
|
|
2730
2875
|
|
|
2876
|
+
// src/unwire.ts
|
|
2877
|
+
var unwire_exports = {};
|
|
2878
|
+
__export(unwire_exports, {
|
|
2879
|
+
unwireAgent: () => unwireAgent
|
|
2880
|
+
});
|
|
2881
|
+
function identityAt(directory) {
|
|
2882
|
+
return readFile(join(directory, "identity.json"), "utf8").then((raw) => JSON.parse(raw)).catch(() => null);
|
|
2883
|
+
}
|
|
2884
|
+
function removeForModel(text, model, names, path) {
|
|
2885
|
+
switch (model.kind) {
|
|
2886
|
+
case "yaml":
|
|
2887
|
+
return removeHermesYaml(text, names, path);
|
|
2888
|
+
case "toml":
|
|
2889
|
+
return removeCodexToml(text, names);
|
|
2890
|
+
case "json":
|
|
2891
|
+
return removeJsonMcpConfig(text, model.serverRoot ?? "mcpServers", names, path);
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
function envLineValue(envText, envKey) {
|
|
2895
|
+
const line = envText.split(/\r?\n/).find((candidate) => new RegExp(`^\\s*(?:export[ \\t]+)?${envKey}[ \\t]*=`).test(candidate));
|
|
2896
|
+
if (!line) return void 0;
|
|
2897
|
+
let value = line.slice(line.indexOf("=") + 1);
|
|
2898
|
+
value = value.trim();
|
|
2899
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
2900
|
+
value = value.slice(1, -1);
|
|
2901
|
+
}
|
|
2902
|
+
return value;
|
|
2903
|
+
}
|
|
2904
|
+
async function readOptionalText(path) {
|
|
2905
|
+
try {
|
|
2906
|
+
return await readFile(path, "utf8");
|
|
2907
|
+
} catch {
|
|
2908
|
+
return null;
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2911
|
+
async function unwireAgent(input) {
|
|
2912
|
+
const homeDir = input.homeDir ?? homedir();
|
|
2913
|
+
const [identity, sidecar] = await Promise.all([identityAt(input.directory), readRuntimeSidecar(input.directory)]);
|
|
2914
|
+
const agentId = identity?.agent_id ?? "unknown";
|
|
2915
|
+
const slug = input.slug ?? sidecar?.server_name;
|
|
2916
|
+
const names = serverNamesFor(slug);
|
|
2917
|
+
const runtimes = [];
|
|
2918
|
+
let tombstoned = false;
|
|
2919
|
+
const tombstonePath = join(input.directory, TOMBSTONE_FILENAME);
|
|
2920
|
+
if (await readOptionalText(tombstonePath) === null) {
|
|
2921
|
+
await writeAgentTombstone({
|
|
2922
|
+
directory: input.directory,
|
|
2923
|
+
agentId,
|
|
2924
|
+
reason: input.reason ?? "unwired via --unwire",
|
|
2925
|
+
replacedBy: input.replacedBy,
|
|
2926
|
+
tombstonesDir: input.tombstonesDir
|
|
2927
|
+
});
|
|
2928
|
+
tombstoned = true;
|
|
2929
|
+
}
|
|
2930
|
+
for (const model of RUNTIMES) {
|
|
2931
|
+
const path = runtimeConfigPathFor(model.runtime, homeDir);
|
|
2932
|
+
if (path === null) continue;
|
|
2933
|
+
const text = await readOptionalText(path);
|
|
2934
|
+
if (text === null) continue;
|
|
2935
|
+
try {
|
|
2936
|
+
if (!slug) {
|
|
2937
|
+
const owned = sidecar?.wrapper_path != null && text.includes(sidecar.wrapper_path);
|
|
2938
|
+
if (!owned) {
|
|
2939
|
+
const pairPresent = removeForModel(text, model, names, path) !== text;
|
|
2940
|
+
if (!pairPresent) continue;
|
|
2941
|
+
runtimes.push({
|
|
2942
|
+
runtime: model.runtime,
|
|
2943
|
+
label: model.label,
|
|
2944
|
+
path,
|
|
2945
|
+
status: "refused",
|
|
2946
|
+
detail: "the bare haven / haven-signer pair in this config launches a different agent (no wrapper from this directory in the file); refusing to guess which one is yours"
|
|
2947
|
+
});
|
|
2948
|
+
continue;
|
|
2949
|
+
}
|
|
2950
|
+
}
|
|
2951
|
+
const next = removeForModel(text, model, names, path);
|
|
2952
|
+
if (next === text) continue;
|
|
2953
|
+
await writeFile(path, next, "utf8");
|
|
2954
|
+
runtimes.push({ runtime: model.runtime, label: model.label, path, status: "removed" });
|
|
2955
|
+
} catch (err) {
|
|
2956
|
+
if (err instanceof UnreadableRuntimeConfigError) {
|
|
2957
|
+
runtimes.push({ runtime: model.runtime, label: model.label, path, status: "unreadable", detail: err.message });
|
|
2958
|
+
continue;
|
|
2959
|
+
}
|
|
2960
|
+
throw err;
|
|
2961
|
+
}
|
|
2962
|
+
}
|
|
2963
|
+
const envPath = hermesEnvPath(homeDir);
|
|
2964
|
+
const envText = await readOptionalText(envPath);
|
|
2965
|
+
if (envText !== null) {
|
|
2966
|
+
try {
|
|
2967
|
+
if (!slug) {
|
|
2968
|
+
const value = envLineValue(envText, names.hermesEnvKey);
|
|
2969
|
+
if (value === void 0) {
|
|
2970
|
+
} else if (!identity?.api_key) {
|
|
2971
|
+
runtimes.push({
|
|
2972
|
+
runtime: "hermes",
|
|
2973
|
+
label: "Hermes env",
|
|
2974
|
+
path: envPath,
|
|
2975
|
+
status: "refused",
|
|
2976
|
+
detail: "MCP_HAVEN_API_KEY is shared by unnamed agents and this directory has no stored API key to compare against; refusing to remove another agent's credential"
|
|
2977
|
+
});
|
|
2978
|
+
} else if (value !== identity.api_key) {
|
|
2979
|
+
runtimes.push({
|
|
2980
|
+
runtime: "hermes",
|
|
2981
|
+
label: "Hermes env",
|
|
2982
|
+
path: envPath,
|
|
2983
|
+
status: "refused",
|
|
2984
|
+
detail: "MCP_HAVEN_API_KEY in the Hermes env holds a different agent's key; refusing to remove another agent's credential"
|
|
2985
|
+
});
|
|
2986
|
+
} else {
|
|
2987
|
+
const next = removeHermesEnv(envText, names.hermesEnvKey);
|
|
2988
|
+
if (next !== envText) {
|
|
2989
|
+
await writeFile(envPath, next, "utf8");
|
|
2990
|
+
runtimes.push({ runtime: "hermes", label: "Hermes env", path: envPath, status: "removed" });
|
|
2991
|
+
}
|
|
2992
|
+
}
|
|
2993
|
+
} else {
|
|
2994
|
+
const next = removeHermesEnv(envText, names.hermesEnvKey);
|
|
2995
|
+
if (next !== envText) {
|
|
2996
|
+
await writeFile(envPath, next, "utf8");
|
|
2997
|
+
runtimes.push({ runtime: "hermes", label: "Hermes env", path: envPath, status: "removed" });
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
3000
|
+
} catch (err) {
|
|
3001
|
+
if (err instanceof Error && err.message.includes("ambiguous managed key")) {
|
|
3002
|
+
runtimes.push({
|
|
3003
|
+
runtime: "hermes",
|
|
3004
|
+
label: "Hermes env",
|
|
3005
|
+
path: envPath,
|
|
3006
|
+
status: "refused",
|
|
3007
|
+
detail: err.message
|
|
3008
|
+
});
|
|
3009
|
+
} else {
|
|
3010
|
+
throw err;
|
|
3011
|
+
}
|
|
3012
|
+
}
|
|
3013
|
+
}
|
|
3014
|
+
await Promise.all([
|
|
3015
|
+
rm(join(input.directory, "signer.json"), { force: true }),
|
|
3016
|
+
rm(join(input.directory, REKEY_PENDING_FILENAME), { force: true })
|
|
3017
|
+
]);
|
|
3018
|
+
if (identity && identity.api_key !== void 0) {
|
|
3019
|
+
const { api_key: _dropped, ...rest } = identity;
|
|
3020
|
+
await writeFile(join(input.directory, "identity.json"), `${JSON.stringify(rest, null, 2)}
|
|
3021
|
+
`, { mode: 384 });
|
|
3022
|
+
}
|
|
3023
|
+
return { directory: input.directory, agentId, slug, tombstoned, runtimes };
|
|
3024
|
+
}
|
|
3025
|
+
var RUNTIMES;
|
|
3026
|
+
var init_unwire = __esm({
|
|
3027
|
+
"src/unwire.ts"() {
|
|
3028
|
+
init_config_writers();
|
|
3029
|
+
init_server_names();
|
|
3030
|
+
init_signer_runtime();
|
|
3031
|
+
init_tombstone();
|
|
3032
|
+
init_storage();
|
|
3033
|
+
RUNTIMES = [
|
|
3034
|
+
{ runtime: "hermes", label: "Hermes Agent config", kind: "yaml" },
|
|
3035
|
+
{ runtime: "codex-cli", label: "Codex config", kind: "toml" },
|
|
3036
|
+
{ runtime: "cursor", label: "Cursor MCP config", kind: "json", serverRoot: "mcpServers" },
|
|
3037
|
+
{ runtime: "vscode", label: "VS Code MCP config", kind: "json", serverRoot: "servers" },
|
|
3038
|
+
{ runtime: "vscode-insiders", label: "VS Code Insiders MCP config", kind: "json", serverRoot: "servers" },
|
|
3039
|
+
{ runtime: "claude-desktop", label: "Claude Desktop config", kind: "json", serverRoot: "mcpServers" }
|
|
3040
|
+
];
|
|
3041
|
+
}
|
|
3042
|
+
});
|
|
3043
|
+
|
|
2731
3044
|
// src/rekey.ts
|
|
2732
3045
|
var rekey_exports = {};
|
|
2733
3046
|
__export(rekey_exports, {
|
|
@@ -2787,7 +3100,7 @@ async function startRekey(options, deps = {}) {
|
|
|
2787
3100
|
async function finishRekey(options, deps = {}) {
|
|
2788
3101
|
const now = deps.now ?? (() => Date.now());
|
|
2789
3102
|
if (!options.newApiKey) {
|
|
2790
|
-
throw new Error(
|
|
3103
|
+
throw new Error(REKEY_FINISH_NEEDS_API_KEY);
|
|
2791
3104
|
}
|
|
2792
3105
|
const stored = await readStoredCredentials(
|
|
2793
3106
|
options.serverName,
|
|
@@ -2930,6 +3243,7 @@ var init_rekey = __esm({
|
|
|
2930
3243
|
init_signer_runtime();
|
|
2931
3244
|
init_key();
|
|
2932
3245
|
init_redact();
|
|
3246
|
+
init_rekey_messages();
|
|
2933
3247
|
init_server_names();
|
|
2934
3248
|
init_storage();
|
|
2935
3249
|
}
|
|
@@ -3409,6 +3723,15 @@ async function runDoctor(input, deps = {}) {
|
|
|
3409
3723
|
if (revoked.length > 0) parts.push(`already revoked: ${revoked.join(", ")}`);
|
|
3410
3724
|
if (retired.length > 0) parts.push(`tombstoned (keys removed): ${retired.join(", ")}`);
|
|
3411
3725
|
if (unverifiable.length > 0) parts.push(`could not verify: ${unverifiable.join(", ")}`);
|
|
3726
|
+
const knownIds = new Set([...inventory].map((e) => e.agentId ?? basename(e.directory)));
|
|
3727
|
+
const ghostRecords = (await readTombstoneRecords(join(homeDir, ".haven", "tombstones"))).filter(
|
|
3728
|
+
(rec) => !knownIds.has(rec.agent_id)
|
|
3729
|
+
);
|
|
3730
|
+
if (ghostRecords.length > 0) {
|
|
3731
|
+
parts.push(
|
|
3732
|
+
`retired records (dir removed): ${ghostRecords.map((rec) => `${rec.agent_id} (${rec.reason})`).join(", ")}`
|
|
3733
|
+
);
|
|
3734
|
+
}
|
|
3412
3735
|
const supersededLive = live.filter((item) => item.entry.classification !== "wired").map((item) => item.label);
|
|
3413
3736
|
checks.push({
|
|
3414
3737
|
id: "superseded_agents",
|
|
@@ -3643,6 +3966,14 @@ async function scanInstalledClients(options = {}) {
|
|
|
3643
3966
|
return SCAN_ORDER.indexOf(a.runtime) - SCAN_ORDER.indexOf(b.runtime);
|
|
3644
3967
|
});
|
|
3645
3968
|
}
|
|
3969
|
+
function installedClientHint(candidates) {
|
|
3970
|
+
const installedClients = candidates.map((candidate) => candidate.runtime);
|
|
3971
|
+
if (candidates.length === 1) {
|
|
3972
|
+
return { installedClients, suggestedRuntime: candidates[0].runtime };
|
|
3973
|
+
}
|
|
3974
|
+
const configured = candidates.filter((candidate) => candidate.evidence === "config-file");
|
|
3975
|
+
return configured.length === 1 ? { installedClients, suggestedRuntime: configured[0].runtime } : { installedClients };
|
|
3976
|
+
}
|
|
3646
3977
|
var MAX_PROMPT_ATTEMPTS = 3;
|
|
3647
3978
|
async function promptForInstalledClient(candidates, io = defaultPromptIo()) {
|
|
3648
3979
|
if (candidates.length === 0) throw noInstalledClientsError();
|
|
@@ -3716,9 +4047,35 @@ function defaultPromptIo() {
|
|
|
3716
4047
|
// src/runtime.ts
|
|
3717
4048
|
init_local_mcp_runtime();
|
|
3718
4049
|
init_runtime_manifest();
|
|
3719
|
-
var CONNECTOR_VERSION = "0.1.
|
|
4050
|
+
var CONNECTOR_VERSION = "0.1.33-alpha.0";
|
|
3720
4051
|
var CONNECT_OUTCOME_SCHEMA_VERSION = 1;
|
|
4052
|
+
var failureOutcomesByError = /* @__PURE__ */ new WeakMap();
|
|
4053
|
+
function failureOutcomeFor(runtimeHint, error) {
|
|
4054
|
+
if (error !== null && typeof error === "object") {
|
|
4055
|
+
const recorded = failureOutcomesByError.get(error);
|
|
4056
|
+
if (recorded) return recorded;
|
|
4057
|
+
}
|
|
4058
|
+
return failedConnectOutcome(runtimeHint, error);
|
|
4059
|
+
}
|
|
3721
4060
|
async function runConnect(options, deps = {}) {
|
|
4061
|
+
const trace = {};
|
|
4062
|
+
try {
|
|
4063
|
+
return await executeConnect(options, deps, trace);
|
|
4064
|
+
} catch (err) {
|
|
4065
|
+
const outcome = failedConnectOutcome(trace.runtime ?? options.runtime, err);
|
|
4066
|
+
if (err !== null && typeof err === "object") failureOutcomesByError.set(err, outcome);
|
|
4067
|
+
if (trace.directory) await recordConnectOutcome(deps, trace.directory, outcome);
|
|
4068
|
+
throw err;
|
|
4069
|
+
}
|
|
4070
|
+
}
|
|
4071
|
+
async function recordConnectOutcome(deps, directory, outcome) {
|
|
4072
|
+
try {
|
|
4073
|
+
return await (deps.writeOutcomeRecord ?? writeConnectOutcomeRecord)(directory, outcome);
|
|
4074
|
+
} catch {
|
|
4075
|
+
return void 0;
|
|
4076
|
+
}
|
|
4077
|
+
}
|
|
4078
|
+
async function executeConnect(options, deps, trace) {
|
|
3722
4079
|
assertSupportedNodeVersion(deps.nodeVersion, MCP_RUNTIME_MANIFEST.minimumNodeVersion);
|
|
3723
4080
|
const connectorVersion = options.connectorVersion ?? CONNECTOR_VERSION;
|
|
3724
4081
|
const api = deps.api ?? createConnectApiClient(options.apiBaseUrl);
|
|
@@ -3738,9 +4095,10 @@ async function runConnect(options, deps = {}) {
|
|
|
3738
4095
|
promptForRuntime: runtimeSelectionPrompt(options, deps)
|
|
3739
4096
|
});
|
|
3740
4097
|
if (!selection.runtime) {
|
|
4098
|
+
const hint = await installedClientHintFor(deps);
|
|
3741
4099
|
throw new ConnectError(
|
|
3742
4100
|
"runtime_undetermined",
|
|
3743
|
-
`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
|
|
4101
|
+
`Could not determine the agent runtime: nothing was detected in this environment and no --runtime was given. If you are an AI agent running this command: re-run it once, unchanged except for adding --runtime <name>, naming the harness you are running in \u2014 one of: ${RUNTIME_FLAG_VALUES} (the aliases cowork, codex and openclaw are accepted too). Do not guess: if your harness is not one of those, use --runtime other, which stores the credentials and prints the manual MCP steps. ` + installedClientProse(hint) + "Nothing was written and the Haven setup token is still unused.",
|
|
3744
4102
|
"rerun_connect_with_explicit_runtime",
|
|
3745
4103
|
// #2091: the values must ride structurally too. The backend's setup
|
|
3746
4104
|
// prompt permits a retry only with "one of the values that refusal
|
|
@@ -3748,10 +4106,16 @@ async function runConnect(options, deps = {}) {
|
|
|
3748
4106
|
// every automation run in an undetected runtime (Codex in the field:
|
|
3749
4107
|
// npx needs network, Codex runs network commands unsandboxed, and the
|
|
3750
4108
|
// unsandboxed path carries none of the CODEX_* detection vars).
|
|
3751
|
-
|
|
4109
|
+
//
|
|
4110
|
+
// #2174: the list alone still leaves the retry a nine-value guess. The
|
|
4111
|
+
// scan below narrows it to what is actually on this machine — as a
|
|
4112
|
+
// HINT. It does not select, and this stays a refusal: see
|
|
4113
|
+
// `installedClientHint`.
|
|
4114
|
+
{ allowedRuntimes: RUNTIME_FLAG_VALUE_LIST, ...hint }
|
|
3752
4115
|
);
|
|
3753
4116
|
}
|
|
3754
4117
|
const runtime = selection.runtime;
|
|
4118
|
+
trace.runtime = runtime;
|
|
3755
4119
|
const installCapabilities = runtimeInstallCapabilities(runtime);
|
|
3756
4120
|
if (options.localMcp) {
|
|
3757
4121
|
if (!supportsLocalMcp(runtime)) {
|
|
@@ -3847,6 +4211,7 @@ async function runConnect(options, deps = {}) {
|
|
|
3847
4211
|
x402BindingSigner: setup.x402_binding_signer ?? void 0,
|
|
3848
4212
|
warn: log
|
|
3849
4213
|
});
|
|
4214
|
+
trace.directory = credentialPaths.directory;
|
|
3850
4215
|
log(`Stored Haven identity credential locally: ${credentialPaths.identityPath}`);
|
|
3851
4216
|
log(`Stored local signer credential locally: ${credentialPaths.signerPath}`);
|
|
3852
4217
|
log(`Stored non-secret agent orientation locally: ${credentialPaths.agentPath}`);
|
|
@@ -3894,6 +4259,9 @@ async function runConnect(options, deps = {}) {
|
|
|
3894
4259
|
errorCode: early.errorCode ?? null,
|
|
3895
4260
|
environmentLabel: options.environmentLabel ?? "Local workspace"
|
|
3896
4261
|
});
|
|
4262
|
+
if (!early.errorCode || early.errorCode === "manual_runtime_setup_required") {
|
|
4263
|
+
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.");
|
|
4264
|
+
}
|
|
3897
4265
|
} catch {
|
|
3898
4266
|
}
|
|
3899
4267
|
}
|
|
@@ -3904,12 +4272,13 @@ async function runConnect(options, deps = {}) {
|
|
|
3904
4272
|
} else {
|
|
3905
4273
|
log("Haven setup on this machine is complete.");
|
|
3906
4274
|
}
|
|
4275
|
+
let supersededAgentIds = [];
|
|
3907
4276
|
try {
|
|
3908
|
-
|
|
3909
|
-
if (
|
|
4277
|
+
supersededAgentIds = await listOtherAgentIds(options.credentialsDir, credentialPaths.directory);
|
|
4278
|
+
if (supersededAgentIds.length > 0) {
|
|
3910
4279
|
log("");
|
|
3911
4280
|
log(
|
|
3912
|
-
`Heads-up: this setup created a NEW agent. Your previous agent(s) \u2014 ${
|
|
4281
|
+
`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.`
|
|
3913
4282
|
);
|
|
3914
4283
|
log(
|
|
3915
4284
|
`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.`
|
|
@@ -3944,17 +4313,23 @@ async function runConnect(options, deps = {}) {
|
|
|
3944
4313
|
approval = await waitForBudgetApproval(api, registration.setup_id, localApiKey, log, options.approvalWait);
|
|
3945
4314
|
}
|
|
3946
4315
|
printNextSteps(runtimeInstall, log, approval);
|
|
4316
|
+
const outcome = completionOutcome({
|
|
4317
|
+
runtimeInstall,
|
|
4318
|
+
delegateAddress: registration.delegate_address,
|
|
4319
|
+
hostedMcpUrl: registration.hosted_mcp_url,
|
|
4320
|
+
supersededAgentIds,
|
|
4321
|
+
setupChallengeExpiresAt: setup.challenge.expires_at,
|
|
4322
|
+
approvalRequired: registration.agent_status === "pending_approval"
|
|
4323
|
+
});
|
|
4324
|
+
if (await recordConnectOutcome(deps, credentialPaths.directory, outcome)) {
|
|
4325
|
+
log(`Saved this run's outcome to ${CONNECT_OUTCOME_FILENAME} in the agent's credential directory.`);
|
|
4326
|
+
}
|
|
3947
4327
|
return {
|
|
3948
4328
|
setupId: registration.setup_id,
|
|
3949
4329
|
agentId: registration.agent_id,
|
|
3950
4330
|
delegateAddress: registration.delegate_address,
|
|
3951
4331
|
credentialPaths,
|
|
3952
|
-
outcome
|
|
3953
|
-
runtimeInstall,
|
|
3954
|
-
delegateAddress: registration.delegate_address,
|
|
3955
|
-
setupChallengeExpiresAt: setup.challenge.expires_at,
|
|
3956
|
-
approvalRequired: registration.agent_status === "pending_approval"
|
|
3957
|
-
})
|
|
4332
|
+
outcome
|
|
3958
4333
|
};
|
|
3959
4334
|
}
|
|
3960
4335
|
function completionOutcome(input) {
|
|
@@ -3986,11 +4361,35 @@ function completionOutcome(input) {
|
|
|
3986
4361
|
// malformed value arrives, do not echo it into an automation-facing
|
|
3987
4362
|
// record; the human log has already been redacted separately.
|
|
3988
4363
|
delegate_address: /^0x[0-9a-fA-F]{40}$/.test(input.delegateAddress) ? shortAddress(input.delegateAddress) : "[delegate-address-redacted]",
|
|
4364
|
+
// The endpoint Connect wrote into the runtime's MCP config, verbatim — the
|
|
4365
|
+
// same string that already sits in the user's own config file. Passed
|
|
4366
|
+
// through the secret filter anyway: this is the automation contract, the
|
|
4367
|
+
// value is server-supplied, and belts are cheap (the #1589 stance).
|
|
4368
|
+
...input.hostedMcpUrl ? { hosted_mcp_url: redactSecrets(input.hostedMcpUrl) } : {},
|
|
4369
|
+
// Always emitted on a completed run, empty list included: "no superseded
|
|
4370
|
+
// agents" is a fact a caller needs, and an omitted key would be
|
|
4371
|
+
// indistinguishable from an older connector that never reported it.
|
|
4372
|
+
superseded_agent_ids: input.supersededAgentIds ?? [],
|
|
3989
4373
|
...input.setupChallengeExpiresAt ? { setup_challenge_expires_at: input.setupChallengeExpiresAt } : {},
|
|
3990
4374
|
...runtimeInstall.errorCode ? { error: { code: runtimeInstall.errorCode, next_action: nextAction2 } } : {}
|
|
3991
4375
|
};
|
|
3992
4376
|
return outcome;
|
|
3993
4377
|
}
|
|
4378
|
+
async function installedClientHintFor(deps) {
|
|
4379
|
+
try {
|
|
4380
|
+
const candidates = await (deps.scanInstalledClients ?? scanInstalledClients)({ env: deps.env });
|
|
4381
|
+
const hint = installedClientHint(candidates);
|
|
4382
|
+
return hint.installedClients.length > 0 ? hint : {};
|
|
4383
|
+
} catch {
|
|
4384
|
+
return {};
|
|
4385
|
+
}
|
|
4386
|
+
}
|
|
4387
|
+
function installedClientProse(hint) {
|
|
4388
|
+
const found = hint.installedClients ?? [];
|
|
4389
|
+
if (found.length === 0) return "";
|
|
4390
|
+
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.";
|
|
4391
|
+
return `Haven can see these agent clients installed here, likeliest first: ${found.join(", ")}.${suggestion} `;
|
|
4392
|
+
}
|
|
3994
4393
|
function runtimeSelectionPrompt(options, deps) {
|
|
3995
4394
|
if (options.interactive !== true) return void 0;
|
|
3996
4395
|
if (!(deps.isTty ?? Boolean(process.stdin.isTTY))) return void 0;
|
|
@@ -4024,7 +4423,9 @@ function failedConnectOutcome(runtimeHint, error) {
|
|
|
4024
4423
|
// pinned by test). Redaction stays on as belt-and-braces; plain-Error
|
|
4025
4424
|
// runs still get their redacted message on stderr via the CLI mirror.
|
|
4026
4425
|
...error instanceof ConnectError && message ? { message: redactForAutomation(message) } : {},
|
|
4027
|
-
...error instanceof ConnectError && error.details.allowedRuntimes ? { allowed_runtimes: error.details.allowedRuntimes } : {}
|
|
4426
|
+
...error instanceof ConnectError && error.details.allowedRuntimes ? { allowed_runtimes: error.details.allowedRuntimes } : {},
|
|
4427
|
+
...error instanceof ConnectError && error.details.installedClients?.length ? { installed_clients: error.details.installedClients } : {},
|
|
4428
|
+
...error instanceof ConnectError && error.details.suggestedRuntime ? { suggested_runtime: error.details.suggestedRuntime } : {}
|
|
4028
4429
|
}
|
|
4029
4430
|
};
|
|
4030
4431
|
}
|
|
@@ -4097,6 +4498,13 @@ function describeApprovedBudget(budget) {
|
|
|
4097
4498
|
const amount = token ? `${formatAtomicAmount(BigInt(budget.amount), token.decimals)} ${budget.token_symbol}` : `${budget.amount} ${budget.token_symbol} (atomic units)`;
|
|
4098
4499
|
return `${amount} ${describeResetPeriod(budget.reset_period_min)}`;
|
|
4099
4500
|
}
|
|
4501
|
+
function describeWaitBound(timeoutMs) {
|
|
4502
|
+
if (timeoutMs >= 12e4) {
|
|
4503
|
+
const minutes = Math.round(timeoutMs / 6e4);
|
|
4504
|
+
return `${minutes} minutes`;
|
|
4505
|
+
}
|
|
4506
|
+
return `${Math.round(timeoutMs / 1e3)}s`;
|
|
4507
|
+
}
|
|
4100
4508
|
async function waitForBudgetApproval(api, setupId, apiKey, log, options = {}) {
|
|
4101
4509
|
const intervalMs = options.intervalMs ?? 5e3;
|
|
4102
4510
|
const timeoutMs = options.timeoutMs ?? 18e4;
|
|
@@ -4107,7 +4515,7 @@ async function waitForBudgetApproval(api, setupId, apiKey, log, options = {}) {
|
|
|
4107
4515
|
const announceWaiting = () => {
|
|
4108
4516
|
if (waitingAnnounced) return;
|
|
4109
4517
|
waitingAnnounced = true;
|
|
4110
|
-
log("
|
|
4518
|
+
log("Now waiting for you to approve the budget in the Haven dashboard \u2014 approval is what unlocks the agent's Haven tools\u2026");
|
|
4111
4519
|
};
|
|
4112
4520
|
for (let i = 0; i < maxPolls; i++) {
|
|
4113
4521
|
if (i > 0) await sleep(intervalMs);
|
|
@@ -4134,7 +4542,10 @@ async function waitForBudgetApproval(api, setupId, apiKey, log, options = {}) {
|
|
|
4134
4542
|
}
|
|
4135
4543
|
announceWaiting();
|
|
4136
4544
|
if (i > 0 && i % remindEvery === 0) {
|
|
4137
|
-
|
|
4545
|
+
const elapsedSeconds = Math.round(i * intervalMs / 1e3);
|
|
4546
|
+
log(
|
|
4547
|
+
`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.`
|
|
4548
|
+
);
|
|
4138
4549
|
}
|
|
4139
4550
|
}
|
|
4140
4551
|
log(
|
|
@@ -4219,6 +4630,7 @@ function printNextSteps(result, log, approval) {
|
|
|
4219
4630
|
|
|
4220
4631
|
// src/args.ts
|
|
4221
4632
|
init_server_names();
|
|
4633
|
+
init_rekey_messages();
|
|
4222
4634
|
function parseArgs(argv, env = process.env) {
|
|
4223
4635
|
const options = {
|
|
4224
4636
|
apiBaseUrl: env.HAVEN_API_URL ?? "http://localhost:3001",
|
|
@@ -4233,6 +4645,8 @@ function parseArgs(argv, env = process.env) {
|
|
|
4233
4645
|
let tombstoneDir;
|
|
4234
4646
|
let tombstoneReason;
|
|
4235
4647
|
let tombstoneReplacedBy;
|
|
4648
|
+
let unwire;
|
|
4649
|
+
let unwireDir;
|
|
4236
4650
|
for (let i = 0; i < argv.length; i += 1) {
|
|
4237
4651
|
const arg = argv[i];
|
|
4238
4652
|
if (arg === "--help" || arg === "-h") {
|
|
@@ -4251,6 +4665,13 @@ function parseArgs(argv, env = process.env) {
|
|
|
4251
4665
|
newApiKey = requireValue(argv, ++i, arg);
|
|
4252
4666
|
} else if (arg === "--tombstone") {
|
|
4253
4667
|
tombstoneDir = requireValue(argv, ++i, arg);
|
|
4668
|
+
} else if (arg === "--unwire") {
|
|
4669
|
+
unwire = unwire ?? {};
|
|
4670
|
+
const next = argv[i + 1];
|
|
4671
|
+
if (next !== void 0 && !next.startsWith("--")) {
|
|
4672
|
+
unwireDir = next;
|
|
4673
|
+
i += 1;
|
|
4674
|
+
}
|
|
4254
4675
|
} else if (arg === "--reason") {
|
|
4255
4676
|
tombstoneReason = requireValue(argv, ++i, arg);
|
|
4256
4677
|
} else if (arg === "--replaced-by") {
|
|
@@ -4300,18 +4721,37 @@ function parseArgs(argv, env = process.env) {
|
|
|
4300
4721
|
);
|
|
4301
4722
|
}
|
|
4302
4723
|
if (rekey.phase === "finish" && !newApiKey) {
|
|
4303
|
-
throw new Error(
|
|
4724
|
+
throw new Error(REKEY_FINISH_NEEDS_API_KEY);
|
|
4304
4725
|
}
|
|
4305
4726
|
return { options, help, json, doctor, repair, tombstone, rekey };
|
|
4306
4727
|
}
|
|
4307
4728
|
if (newApiKey !== void 0) {
|
|
4308
4729
|
throw new Error("--api-key requires --rekey-finish.");
|
|
4309
4730
|
}
|
|
4310
|
-
if (!tombstoneDir && (tombstoneReason !== void 0 || tombstoneReplacedBy !== void 0)) {
|
|
4311
|
-
throw new Error("--reason and --replaced-by require --tombstone <dir
|
|
4731
|
+
if (!tombstoneDir && !unwire && (tombstoneReason !== void 0 || tombstoneReplacedBy !== void 0)) {
|
|
4732
|
+
throw new Error("--reason and --replaced-by require --tombstone <dir> or --unwire.");
|
|
4733
|
+
}
|
|
4734
|
+
if (unwire && (tombstoneReason !== void 0 || tombstoneReplacedBy !== void 0)) {
|
|
4735
|
+
unwire = {
|
|
4736
|
+
...tombstoneReason !== void 0 ? { reason: tombstoneReason } : {},
|
|
4737
|
+
...tombstoneReplacedBy !== void 0 ? { replacedBy: tombstoneReplacedBy } : {},
|
|
4738
|
+
...unwire
|
|
4739
|
+
};
|
|
4312
4740
|
}
|
|
4313
4741
|
if (tombstone) {
|
|
4314
|
-
|
|
4742
|
+
if (unwire) {
|
|
4743
|
+
throw new Error("--unwire and --tombstone are separate operations; run one per invocation.");
|
|
4744
|
+
}
|
|
4745
|
+
return { options, help, json, doctor, repair, tombstone, rekey, unwire, unwireDir };
|
|
4746
|
+
}
|
|
4747
|
+
if (unwire) {
|
|
4748
|
+
if (options.setupToken) {
|
|
4749
|
+
throw new Error("--unwire removes existing wiring; it does not take --setup. Drop --setup.");
|
|
4750
|
+
}
|
|
4751
|
+
if (!unwireDir && !options.serverName && !options.credentialsDir) {
|
|
4752
|
+
throw new Error("--unwire needs a target: --unwire <dir>, --unwire --name <slug>, or --unwire --credentials-dir <path>.");
|
|
4753
|
+
}
|
|
4754
|
+
return { options, help, json, doctor, repair, tombstone, rekey, unwire, unwireDir };
|
|
4315
4755
|
}
|
|
4316
4756
|
if (doctor || repair) {
|
|
4317
4757
|
if (!options.runtime) {
|
|
@@ -4374,8 +4814,20 @@ function helpText() {
|
|
|
4374
4814
|
" --tombstone <dir> Retire an agent credential directory in place (no token): replaces its signer",
|
|
4375
4815
|
" wrapper with a diagnostic that names the retirement in MCP stderr logs, and",
|
|
4376
4816
|
" writes TOMBSTONE.json. Touches NO key material and revokes nothing.",
|
|
4377
|
-
" --
|
|
4378
|
-
"
|
|
4817
|
+
" --unwire [<dir>] Remove one agent\u2019s wiring from every runtime config it appears in (no token):",
|
|
4818
|
+
" tombstone-first, then drops the hosted + signer MCP pair from Hermes YAML, Codex",
|
|
4819
|
+
" TOML and the JSON configs (Cursor, VS Code, Insiders, Claude Desktop), plus the",
|
|
4820
|
+
" Hermes dotenv API-key line (MCP_HAVEN_API_KEY / MCP_HAVEN_<SLUG>_API_KEY).",
|
|
4821
|
+
" Target the directory directly, or add --name <slug> or --credentials-dir.",
|
|
4822
|
+
" An UNNAMED pair is only removed when this directory\u2019s wrapper is the one the",
|
|
4823
|
+
" config launches (or its key is the one the Hermes env holds); otherwise the",
|
|
4824
|
+
" command refuses rather than unwire a different agent \u2014 it never touches a",
|
|
4825
|
+
" pair another agent owns. Tears down the target directory: its signer key and",
|
|
4826
|
+
" API key are removed locally (record kept via the #2155 tombstone mirror) and",
|
|
4827
|
+
" nothing is ever revoked on the backend \u2014 that stays an owner action on the",
|
|
4828
|
+
" Haven agent page.",
|
|
4829
|
+
" --reason <text> Reason recorded in the tombstone (with --tombstone or --unwire).",
|
|
4830
|
+
" --replaced-by <agent-id> Successor agent recorded in the tombstone (with --tombstone or --unwire).",
|
|
4379
4831
|
" --help Show this help.",
|
|
4380
4832
|
"",
|
|
4381
4833
|
"The connector never prints the private key and never sends it to Haven. JSON output never includes credential contents or full credential paths."
|
|
@@ -4394,6 +4846,28 @@ init_api();
|
|
|
4394
4846
|
init_key();
|
|
4395
4847
|
init_redact();
|
|
4396
4848
|
init_redact();
|
|
4849
|
+
init_connect_error();
|
|
4850
|
+
function failSubcommand(io, json, err, envelope, fallback) {
|
|
4851
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4852
|
+
io.stderr(`${redactSecrets(message)}
|
|
4853
|
+
`);
|
|
4854
|
+
if (json) {
|
|
4855
|
+
io.stdout(
|
|
4856
|
+
`${redactSecrets(
|
|
4857
|
+
JSON.stringify({
|
|
4858
|
+
...envelope,
|
|
4859
|
+
error: {
|
|
4860
|
+
code: isConnectError(err) ? err.code : fallback.code,
|
|
4861
|
+
next_action: isConnectError(err) ? err.nextAction : fallback.nextAction,
|
|
4862
|
+
...isConnectError(err) && message ? { message } : {}
|
|
4863
|
+
}
|
|
4864
|
+
})
|
|
4865
|
+
)}
|
|
4866
|
+
`
|
|
4867
|
+
);
|
|
4868
|
+
}
|
|
4869
|
+
return 1;
|
|
4870
|
+
}
|
|
4397
4871
|
async function runCli(argv, io = {
|
|
4398
4872
|
stdout: (message) => process.stdout.write(message),
|
|
4399
4873
|
stderr: (message) => process.stderr.write(message)
|
|
@@ -4421,13 +4895,13 @@ async function runCli(argv, io = {
|
|
|
4421
4895
|
}
|
|
4422
4896
|
if (parsed.tombstone) {
|
|
4423
4897
|
const { writeAgentTombstone: writeAgentTombstone2 } = await Promise.resolve().then(() => (init_tombstone(), tombstone_exports));
|
|
4424
|
-
const { readFile:
|
|
4425
|
-
const { join:
|
|
4898
|
+
const { readFile: readFile13 } = await import('fs/promises');
|
|
4899
|
+
const { join: join11 } = await import('path');
|
|
4426
4900
|
try {
|
|
4427
4901
|
let agentId = "unknown";
|
|
4428
4902
|
try {
|
|
4429
4903
|
const identity = JSON.parse(
|
|
4430
|
-
await
|
|
4904
|
+
await readFile13(join11(parsed.tombstone.directory, "identity.json"), "utf8")
|
|
4431
4905
|
);
|
|
4432
4906
|
agentId = identity.agent_id ?? "unknown";
|
|
4433
4907
|
} catch {
|
|
@@ -4448,14 +4922,80 @@ async function runCli(argv, io = {
|
|
|
4448
4922
|
"Key files were NOT touched and nothing was revoked \u2014 revoke the agent on the Haven agent page if you have not already.\n"
|
|
4449
4923
|
);
|
|
4450
4924
|
io.stdout(
|
|
4451
|
-
|
|
4925
|
+
`A surviving tombstone record was mirrored to ${redactSecrets(info.recordPath)}
|
|
4926
|
+
`
|
|
4927
|
+
);
|
|
4928
|
+
io.stdout(
|
|
4929
|
+
"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"
|
|
4452
4930
|
);
|
|
4453
4931
|
}
|
|
4454
4932
|
return 0;
|
|
4455
4933
|
} catch (err) {
|
|
4456
|
-
io.
|
|
4457
|
-
|
|
4458
|
-
|
|
4934
|
+
return failSubcommand(io, parsed.json, err, { tombstoned: false }, {
|
|
4935
|
+
code: "tombstone_failed",
|
|
4936
|
+
nextAction: "review_the_error_and_retry_with_a_valid_agent_directory"
|
|
4937
|
+
});
|
|
4938
|
+
}
|
|
4939
|
+
}
|
|
4940
|
+
if (parsed.unwire) {
|
|
4941
|
+
const { unwireAgent: unwireAgent2 } = await Promise.resolve().then(() => (init_unwire(), unwire_exports));
|
|
4942
|
+
const { homedir: homedir10 } = await import('os');
|
|
4943
|
+
const { join: join11 } = await import('path');
|
|
4944
|
+
const homeDir = homedir10();
|
|
4945
|
+
const root = parsed.options.credentialsDir ?? join11(homeDir, ".haven", "agents");
|
|
4946
|
+
const directory = parsed.unwireDir ?? (parsed.options.serverName ? join11(root, parsed.options.serverName) : root);
|
|
4947
|
+
try {
|
|
4948
|
+
const result = await unwireAgent2({
|
|
4949
|
+
directory,
|
|
4950
|
+
slug: parsed.options.serverName,
|
|
4951
|
+
reason: parsed.unwire.reason,
|
|
4952
|
+
replacedBy: parsed.unwire.replacedBy,
|
|
4953
|
+
homeDir
|
|
4954
|
+
});
|
|
4955
|
+
const failures = result.runtimes.filter((r) => r.status === "refused" || r.status === "unreadable");
|
|
4956
|
+
if (parsed.json) {
|
|
4957
|
+
io.stdout(
|
|
4958
|
+
`${redactSecrets(
|
|
4959
|
+
JSON.stringify({
|
|
4960
|
+
unwired: true,
|
|
4961
|
+
agent_id: result.agentId,
|
|
4962
|
+
slug: result.slug ?? null,
|
|
4963
|
+
directory: result.directory,
|
|
4964
|
+
tombstoned: result.tombstoned,
|
|
4965
|
+
runtimes: result.runtimes.map((r) => ({
|
|
4966
|
+
runtime: r.runtime,
|
|
4967
|
+
label: r.label,
|
|
4968
|
+
status: r.status,
|
|
4969
|
+
...r.detail ? { detail: r.detail } : {}
|
|
4970
|
+
}))
|
|
4971
|
+
})
|
|
4972
|
+
)}
|
|
4973
|
+
`
|
|
4974
|
+
);
|
|
4975
|
+
} else {
|
|
4976
|
+
io.stdout(redactSecrets(`Unwired agent ${result.agentId} at ${result.directory}.
|
|
4977
|
+
`));
|
|
4978
|
+
io.stdout(
|
|
4979
|
+
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"
|
|
4980
|
+
);
|
|
4981
|
+
for (const r of result.runtimes) {
|
|
4982
|
+
const mark = r.status === "removed" ? "\u2713" : r.status === "clean" ? "\u2013" : "\u2717";
|
|
4983
|
+
io.stdout(redactSecrets(` ${mark} ${r.label}: ${r.status}${r.detail ? ` \u2014 ${r.detail}` : ""}
|
|
4984
|
+
`));
|
|
4985
|
+
}
|
|
4986
|
+
io.stdout(
|
|
4987
|
+
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"
|
|
4988
|
+
);
|
|
4989
|
+
io.stdout(
|
|
4990
|
+
"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"
|
|
4991
|
+
);
|
|
4992
|
+
}
|
|
4993
|
+
return failures.length > 0 ? 1 : 0;
|
|
4994
|
+
} catch (err) {
|
|
4995
|
+
return failSubcommand(io, parsed.json, err, { unwired: false }, {
|
|
4996
|
+
code: "unwire_failed",
|
|
4997
|
+
nextAction: "review_the_error_and_rerun_unwire_which_is_idempotent"
|
|
4998
|
+
});
|
|
4459
4999
|
}
|
|
4460
5000
|
}
|
|
4461
5001
|
if (parsed.rekey) {
|
|
@@ -4511,9 +5051,10 @@ async function runCli(argv, io = {
|
|
|
4511
5051
|
}
|
|
4512
5052
|
return 0;
|
|
4513
5053
|
} catch (err) {
|
|
4514
|
-
io.
|
|
4515
|
-
|
|
4516
|
-
|
|
5054
|
+
return failSubcommand(io, parsed.json, err, { rekey: "failed" }, {
|
|
5055
|
+
code: "rekey_failed",
|
|
5056
|
+
nextAction: "review_the_error_and_rerun_the_rekey_phase"
|
|
5057
|
+
});
|
|
4517
5058
|
}
|
|
4518
5059
|
}
|
|
4519
5060
|
if (parsed.doctor || parsed.repair) {
|
|
@@ -4559,9 +5100,10 @@ async function runCli(argv, io = {
|
|
|
4559
5100
|
}
|
|
4560
5101
|
return report.ok ? 0 : 1;
|
|
4561
5102
|
} catch (err) {
|
|
4562
|
-
io.
|
|
4563
|
-
|
|
4564
|
-
|
|
5103
|
+
return failSubcommand(io, parsed.json, err, { doctor: "failed" }, {
|
|
5104
|
+
code: "doctor_failed",
|
|
5105
|
+
nextAction: "review_the_error_and_rerun_doctor"
|
|
5106
|
+
});
|
|
4565
5107
|
}
|
|
4566
5108
|
}
|
|
4567
5109
|
try {
|
|
@@ -4588,7 +5130,7 @@ async function runCli(argv, io = {
|
|
|
4588
5130
|
if (parsed.json) {
|
|
4589
5131
|
io.stderr(`${redactForAutomation(err instanceof Error ? err.message : String(err))}
|
|
4590
5132
|
`);
|
|
4591
|
-
io.stdout(`${JSON.stringify(
|
|
5133
|
+
io.stdout(`${JSON.stringify(failureOutcomeFor(parsed.options.runtime, err))}
|
|
4592
5134
|
`);
|
|
4593
5135
|
} else {
|
|
4594
5136
|
io.stderr(`${redactSecrets(err instanceof Error ? err.message : String(err))}
|
|
@@ -4619,6 +5161,6 @@ init_connect_error();
|
|
|
4619
5161
|
init_storage();
|
|
4620
5162
|
init_runtime_manifest();
|
|
4621
5163
|
|
|
4622
|
-
export { CONNECTOR_VERSION, CONNECT_OUTCOME_SCHEMA_VERSION, ConnectError, MCP_RUNTIME_MANIFEST, RUNTIME_FLAG_VALUES, completionOutcome, createConnectApiClient, defaultAgentDirectory, delegateKeyFromPrivateKey, failedConnectOutcome, generateDelegateKey, helpText, installRuntime, isConnectError, mcpPackageSpec, normalizeRuntime, parseArgs, prepareSignerRuntime, promptForInstalledClient, redactSecrets, resolveRuntimeByInstalledClientPrompt, resolveRuntimeSelection, runCli, runConnect, runtimeInstallCapabilities, runtimeProfile, scanInstalledClients, sdkPackageSpec, shortAddress, signerPackageSpec, writeCredentialFiles };
|
|
5164
|
+
export { CONNECTOR_VERSION, CONNECT_OUTCOME_FILENAME, CONNECT_OUTCOME_SCHEMA_VERSION, ConnectError, MCP_RUNTIME_MANIFEST, RUNTIME_FLAG_VALUES, completionOutcome, createConnectApiClient, defaultAgentDirectory, delegateKeyFromPrivateKey, failedConnectOutcome, failureOutcomeFor, generateDelegateKey, helpText, installRuntime, installedClientHint, isConnectError, mcpPackageSpec, normalizeRuntime, parseArgs, prepareSignerRuntime, promptForInstalledClient, redactSecrets, resolveRuntimeByInstalledClientPrompt, resolveRuntimeSelection, runCli, runConnect, runtimeInstallCapabilities, runtimeProfile, scanInstalledClients, sdkPackageSpec, shortAddress, signerPackageSpec, writeConnectOutcomeRecord, writeCredentialFiles };
|
|
4623
5165
|
//# sourceMappingURL=index.js.map
|
|
4624
5166
|
//# sourceMappingURL=index.js.map
|