sealnet-mcp 0.2.5 → 0.3.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 +19 -16
- package/dist/cli.cjs +973 -993
- package/dist/cli.d.cts +10 -7
- package/dist/cli.d.ts +10 -7
- package/dist/cli.js +3712 -3732
- package/dist/index.cjs +350 -272
- package/dist/index.d.cts +128 -102
- package/dist/index.d.ts +128 -102
- package/dist/index.js +328 -258
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -34,8 +34,9 @@ __export(cli_exports, {
|
|
|
34
34
|
main: () => main
|
|
35
35
|
});
|
|
36
36
|
module.exports = __toCommonJS(cli_exports);
|
|
37
|
-
var
|
|
37
|
+
var import_node_fs7 = require("fs");
|
|
38
38
|
var import_promises10 = require("fs/promises");
|
|
39
|
+
var import_node_path11 = require("path");
|
|
39
40
|
var import_commander = require("commander");
|
|
40
41
|
|
|
41
42
|
// ../../../node_modules/.pnpm/@noble+hashes@1.8.0/node_modules/@noble/hashes/esm/utils.js
|
|
@@ -6901,6 +6902,9 @@ async function generateIdentity() {
|
|
|
6901
6902
|
const { publicKey, secretKey } = await generateRecipientKeyPair();
|
|
6902
6903
|
return { secret: secretKey, public: publicKey };
|
|
6903
6904
|
}
|
|
6905
|
+
async function identityFromSeed(seed) {
|
|
6906
|
+
return { secret: seed, public: await recipientPublicKey(seed) };
|
|
6907
|
+
}
|
|
6904
6908
|
function identityToFields(id) {
|
|
6905
6909
|
return {
|
|
6906
6910
|
x25519_secret_b64: toB64(id.secret),
|
|
@@ -6934,20 +6938,27 @@ var import_node_crypto = require("crypto");
|
|
|
6934
6938
|
var import_node_os = require("os");
|
|
6935
6939
|
var import_node_path = require("path");
|
|
6936
6940
|
var LOCK_FILE_NAME = "state.lock";
|
|
6941
|
+
var LOCK_WAIT_MS = 5e3;
|
|
6942
|
+
var LOCK_RETRY_MS = 25;
|
|
6937
6943
|
var StateLockedError = class extends Error {
|
|
6938
6944
|
code = "state_locked";
|
|
6939
6945
|
holder;
|
|
6940
6946
|
lockPath;
|
|
6941
6947
|
constructor(lockPath, holder) {
|
|
6942
6948
|
const who = holder === null ? "holder info unreadable" : `pid ${holder.pid} on ${holder.hostname} since ${holder.acquired_at}`;
|
|
6943
|
-
super(
|
|
6944
|
-
`state directory is locked by another sealnet-mcp process (${who}). Only one writer may use a state dir at a time; stop the other instance or point this one at a different --state-dir. Lock file: ${lockPath}`
|
|
6945
|
-
);
|
|
6949
|
+
super(`state directory is locked by another sealnet-mcp process (${who}). Lock file: ${lockPath}`);
|
|
6946
6950
|
this.name = "StateLockedError";
|
|
6947
6951
|
this.holder = holder;
|
|
6948
6952
|
this.lockPath = lockPath;
|
|
6949
6953
|
}
|
|
6950
6954
|
};
|
|
6955
|
+
var StateBusyError = class extends Error {
|
|
6956
|
+
code = "state_busy";
|
|
6957
|
+
constructor(locked) {
|
|
6958
|
+
super(`${locked.message}; waited ${LOCK_WAIT_MS / 1e3} s, nothing was changed, safe to retry`);
|
|
6959
|
+
this.name = "StateBusyError";
|
|
6960
|
+
}
|
|
6961
|
+
};
|
|
6951
6962
|
var StateDirUnsafeError = class extends Error {
|
|
6952
6963
|
code = "state_dir_unsafe";
|
|
6953
6964
|
constructor(stateDir, reason) {
|
|
@@ -6966,6 +6977,9 @@ function defaultIsPidAlive(pid) {
|
|
|
6966
6977
|
return code === "EPERM";
|
|
6967
6978
|
}
|
|
6968
6979
|
}
|
|
6980
|
+
function holderMayRun(holder, deps = {}) {
|
|
6981
|
+
return holder.hostname !== (deps.host ?? (0, import_node_os.hostname)()) || (deps.isPidAlive ?? defaultIsPidAlive)(holder.pid);
|
|
6982
|
+
}
|
|
6969
6983
|
function parseHolder(raw) {
|
|
6970
6984
|
try {
|
|
6971
6985
|
const parsed = JSON.parse(raw);
|
|
@@ -7008,7 +7022,7 @@ async function tryCreate(lockPath, info) {
|
|
|
7008
7022
|
await (0, import_promises.unlink)(tmp).catch(() => void 0);
|
|
7009
7023
|
}
|
|
7010
7024
|
}
|
|
7011
|
-
async function tryBreakStale(lockPath, probed,
|
|
7025
|
+
async function tryBreakStale(lockPath, probed, deps) {
|
|
7012
7026
|
const claimPath = `${lockPath}.break-${process.pid}-${(0, import_node_crypto.randomBytes)(4).toString("hex")}`;
|
|
7013
7027
|
try {
|
|
7014
7028
|
await (0, import_promises.rename)(lockPath, claimPath);
|
|
@@ -7016,8 +7030,7 @@ async function tryBreakStale(lockPath, probed, isAlive, selfHost) {
|
|
|
7016
7030
|
return true;
|
|
7017
7031
|
}
|
|
7018
7032
|
const claimed = parseHolder(await (0, import_promises.readFile)(claimPath, "utf8").catch(() => ""));
|
|
7019
|
-
|
|
7020
|
-
if (sameHolder(claimed, probed) || claimedStale) {
|
|
7033
|
+
if (sameHolder(claimed, probed) || claimed === null || !holderMayRun(claimed, deps)) {
|
|
7021
7034
|
await (0, import_promises.unlink)(claimPath).catch(() => void 0);
|
|
7022
7035
|
return true;
|
|
7023
7036
|
}
|
|
@@ -7030,11 +7043,9 @@ async function tryBreakStale(lockPath, probed, isAlive, selfHost) {
|
|
|
7030
7043
|
}
|
|
7031
7044
|
async function acquireStateLock(stateDir, deps = {}) {
|
|
7032
7045
|
const lockPath = (0, import_node_path.join)(stateDir, LOCK_FILE_NAME);
|
|
7033
|
-
const isAlive = deps.isPidAlive ?? defaultIsPidAlive;
|
|
7034
|
-
const selfHost = deps.host ?? (0, import_node_os.hostname)();
|
|
7035
7046
|
const self2 = {
|
|
7036
7047
|
pid: deps.pid ?? process.pid,
|
|
7037
|
-
hostname:
|
|
7048
|
+
hostname: deps.host ?? (0, import_node_os.hostname)(),
|
|
7038
7049
|
acquired_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
7039
7050
|
};
|
|
7040
7051
|
await (0, import_promises.mkdir)(stateDir, { recursive: true, mode: 448 });
|
|
@@ -7045,11 +7056,10 @@ async function acquireStateLock(stateDir, deps = {}) {
|
|
|
7045
7056
|
await (0, import_promises.chmod)(stateDir, 448).catch(() => void 0);
|
|
7046
7057
|
if (!await tryCreate(lockPath, self2)) {
|
|
7047
7058
|
const holder = await readHolder(lockPath);
|
|
7048
|
-
|
|
7049
|
-
if (holder !== null && !stale) {
|
|
7059
|
+
if (holder !== null && holderMayRun(holder, deps)) {
|
|
7050
7060
|
throw new StateLockedError(lockPath, holder);
|
|
7051
7061
|
}
|
|
7052
|
-
if (!await tryBreakStale(lockPath, holder,
|
|
7062
|
+
if (!await tryBreakStale(lockPath, holder, deps)) {
|
|
7053
7063
|
throw new StateLockedError(lockPath, await readHolder(lockPath));
|
|
7054
7064
|
}
|
|
7055
7065
|
if (!await tryCreate(lockPath, self2)) {
|
|
@@ -7081,6 +7091,30 @@ async function acquireStateLock(stateDir, deps = {}) {
|
|
|
7081
7091
|
}
|
|
7082
7092
|
};
|
|
7083
7093
|
}
|
|
7094
|
+
var queues = /* @__PURE__ */ new Map();
|
|
7095
|
+
function withStateLock(stateDir, fn, deps = {}) {
|
|
7096
|
+
const run = (queues.get(stateDir) ?? Promise.resolve()).catch(() => void 0).then(async () => {
|
|
7097
|
+
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
7098
|
+
let lock;
|
|
7099
|
+
for (; ; ) {
|
|
7100
|
+
try {
|
|
7101
|
+
lock = await acquireStateLock(stateDir, deps);
|
|
7102
|
+
break;
|
|
7103
|
+
} catch (err2) {
|
|
7104
|
+
if (!(err2 instanceof StateLockedError)) throw err2;
|
|
7105
|
+
if (Date.now() >= deadline) throw new StateBusyError(err2);
|
|
7106
|
+
await new Promise((resolve5) => setTimeout(resolve5, LOCK_RETRY_MS));
|
|
7107
|
+
}
|
|
7108
|
+
}
|
|
7109
|
+
try {
|
|
7110
|
+
return await fn();
|
|
7111
|
+
} finally {
|
|
7112
|
+
await lock.release();
|
|
7113
|
+
}
|
|
7114
|
+
});
|
|
7115
|
+
queues.set(stateDir, run);
|
|
7116
|
+
return run;
|
|
7117
|
+
}
|
|
7084
7118
|
async function probeStateLock(stateDir, deps = {}) {
|
|
7085
7119
|
const lockPath = (0, import_node_path.join)(stateDir, LOCK_FILE_NAME);
|
|
7086
7120
|
const holder = await readHolder(lockPath);
|
|
@@ -7092,182 +7126,7 @@ async function probeStateLock(stateDir, deps = {}) {
|
|
|
7092
7126
|
return { held: false, holder: null, stale: false };
|
|
7093
7127
|
}
|
|
7094
7128
|
}
|
|
7095
|
-
|
|
7096
|
-
const sameHost = holder.hostname === (deps.host ?? (0, import_node_os.hostname)());
|
|
7097
|
-
const stale = sameHost && !isAlive(holder.pid);
|
|
7098
|
-
return { held: true, holder, stale };
|
|
7099
|
-
}
|
|
7100
|
-
|
|
7101
|
-
// src/passphrase.ts
|
|
7102
|
-
var import_node_child_process = require("child_process");
|
|
7103
|
-
var import_node_crypto2 = require("crypto");
|
|
7104
|
-
var SERVICE_NAME = "seal-mcp";
|
|
7105
|
-
var ACCOUNT_NAME = "state";
|
|
7106
|
-
var ENV_VAR = "SEAL_MCP_PASSPHRASE";
|
|
7107
|
-
var KEYCHAIN_SERVICE = SERVICE_NAME;
|
|
7108
|
-
var KEYCHAIN_ACCOUNT = ACCOUNT_NAME;
|
|
7109
|
-
var PASSPHRASE_ENV_VAR = ENV_VAR;
|
|
7110
|
-
var KeytarUnavailableError = class extends Error {
|
|
7111
|
-
code = "keytar_unavailable";
|
|
7112
|
-
constructor(detail) {
|
|
7113
|
-
super(`keytar unavailable: ${detail}`);
|
|
7114
|
-
this.name = "KeytarUnavailableError";
|
|
7115
|
-
}
|
|
7116
|
-
};
|
|
7117
|
-
var PassphraseNotFoundError = class extends Error {
|
|
7118
|
-
code = "passphrase_not_found";
|
|
7119
|
-
constructor() {
|
|
7120
|
-
super(
|
|
7121
|
-
`No passphrase found. Run \`sealnet-mcp init\` to store one in your OS keychain, or export ${ENV_VAR} for CI/headless use.`
|
|
7122
|
-
);
|
|
7123
|
-
this.name = "PassphraseNotFoundError";
|
|
7124
|
-
}
|
|
7125
|
-
};
|
|
7126
|
-
function keyringEntry(keyring, platform, service, account) {
|
|
7127
|
-
return platform === "win32" ? keyring.AsyncEntry.withTarget(`${service}/${account}`, service, account) : new keyring.AsyncEntry(service, account, { linux: { store: "secret-service" } });
|
|
7128
|
-
}
|
|
7129
|
-
function readKeytarLinuxEntry(service, account) {
|
|
7130
|
-
return new Promise((resolve5) => {
|
|
7131
|
-
(0, import_node_child_process.execFile)("secret-tool", ["lookup", "service", service, "account", account], { timeout: 5e3 }, (err2, stdout) => {
|
|
7132
|
-
const value = err2 ? "" : stdout.replace(/\n$/, "");
|
|
7133
|
-
resolve5(value.length > 0 ? value : null);
|
|
7134
|
-
});
|
|
7135
|
-
});
|
|
7136
|
-
}
|
|
7137
|
-
function keytarOverKeyring(keyring, platform = process.platform, readLegacy = readKeytarLinuxEntry) {
|
|
7138
|
-
const entry = (service, account) => keyringEntry(keyring, platform, service, account);
|
|
7139
|
-
const store = (service, account, password) => entry(service, account).setSecret(Buffer.from(password, "utf8"));
|
|
7140
|
-
return {
|
|
7141
|
-
async getPassword(service, account) {
|
|
7142
|
-
const secret = await entry(service, account).getSecret();
|
|
7143
|
-
if (secret !== void 0) return Buffer.from(secret).toString("utf8");
|
|
7144
|
-
const legacy = platform === "linux" ? await readLegacy(service, account) : null;
|
|
7145
|
-
if (legacy !== null) await store(service, account, legacy).catch(() => void 0);
|
|
7146
|
-
return legacy;
|
|
7147
|
-
},
|
|
7148
|
-
setPassword: store,
|
|
7149
|
-
deletePassword: (service, account) => entry(service, account).deleteCredential()
|
|
7150
|
-
};
|
|
7151
|
-
}
|
|
7152
|
-
var keytarCache = void 0;
|
|
7153
|
-
async function loadKeytar() {
|
|
7154
|
-
if (keytarCache !== void 0) return keytarCache;
|
|
7155
|
-
try {
|
|
7156
|
-
const mod = await import("@napi-rs/keyring");
|
|
7157
|
-
const keyring = mod.default ?? mod;
|
|
7158
|
-
keyringEntry(keyring, process.platform, SERVICE_NAME, ACCOUNT_NAME);
|
|
7159
|
-
keytarCache = keytarOverKeyring(keyring);
|
|
7160
|
-
} catch {
|
|
7161
|
-
keytarCache = null;
|
|
7162
|
-
}
|
|
7163
|
-
return keytarCache;
|
|
7164
|
-
}
|
|
7165
|
-
async function loadKeytarOrThrow() {
|
|
7166
|
-
const k = await loadKeytar();
|
|
7167
|
-
if (k === null) {
|
|
7168
|
-
throw new KeytarUnavailableError(
|
|
7169
|
-
"no OS keychain. On Linux start a Secret Service (gnome-keyring or KWallet); on macOS/Windows this should never happen \u2014 file a bug."
|
|
7170
|
-
);
|
|
7171
|
-
}
|
|
7172
|
-
return k;
|
|
7173
|
-
}
|
|
7174
|
-
async function resolvePassphraseFromStores(env = process.env) {
|
|
7175
|
-
const keytar = await loadKeytar();
|
|
7176
|
-
if (keytar !== null) {
|
|
7177
|
-
try {
|
|
7178
|
-
const stored = await keytar.getPassword(SERVICE_NAME, ACCOUNT_NAME);
|
|
7179
|
-
if (stored !== null && stored.length > 0) {
|
|
7180
|
-
return { passphrase: stored, source: "keychain" };
|
|
7181
|
-
}
|
|
7182
|
-
} catch {
|
|
7183
|
-
}
|
|
7184
|
-
}
|
|
7185
|
-
const fromEnv = env[ENV_VAR];
|
|
7186
|
-
if (fromEnv !== void 0 && fromEnv.length > 0) {
|
|
7187
|
-
return { passphrase: fromEnv, source: "env" };
|
|
7188
|
-
}
|
|
7189
|
-
return null;
|
|
7190
|
-
}
|
|
7191
|
-
async function passphraseForNewState(env = process.env) {
|
|
7192
|
-
const stored = await resolvePassphraseFromStores(env);
|
|
7193
|
-
if (stored !== null) return { ...stored, fresh: false };
|
|
7194
|
-
if (!(await probeKeychain()).available) return null;
|
|
7195
|
-
return { passphrase: (0, import_node_crypto2.randomBytes)(32).toString("base64url"), source: "keychain", fresh: true };
|
|
7196
|
-
}
|
|
7197
|
-
async function probeKeychain() {
|
|
7198
|
-
const keytar = await loadKeytar();
|
|
7199
|
-
if (keytar === null) return { available: false, hasEntry: false };
|
|
7200
|
-
try {
|
|
7201
|
-
const v = await keytar.getPassword(SERVICE_NAME, ACCOUNT_NAME);
|
|
7202
|
-
return { available: true, hasEntry: v !== null && v.length > 0 };
|
|
7203
|
-
} catch {
|
|
7204
|
-
return { available: false, hasEntry: false };
|
|
7205
|
-
}
|
|
7206
|
-
}
|
|
7207
|
-
async function setKeychainPassphrase(passphrase) {
|
|
7208
|
-
if (passphrase.length === 0) {
|
|
7209
|
-
throw new Error("refusing to store an empty passphrase in the keychain");
|
|
7210
|
-
}
|
|
7211
|
-
const keytar = await loadKeytarOrThrow();
|
|
7212
|
-
await keytar.setPassword(SERVICE_NAME, ACCOUNT_NAME, passphrase);
|
|
7213
|
-
}
|
|
7214
|
-
async function deleteKeychainPassphrase() {
|
|
7215
|
-
const keytar = await loadKeytar();
|
|
7216
|
-
if (keytar === null) return false;
|
|
7217
|
-
try {
|
|
7218
|
-
return await keytar.deletePassword(SERVICE_NAME, ACCOUNT_NAME);
|
|
7219
|
-
} catch {
|
|
7220
|
-
return false;
|
|
7221
|
-
}
|
|
7222
|
-
}
|
|
7223
|
-
async function promptHiddenPassphrase(prompt) {
|
|
7224
|
-
const stdin = process.stdin;
|
|
7225
|
-
if (!stdin.isTTY) {
|
|
7226
|
-
throw new Error(
|
|
7227
|
-
`Cannot prompt for passphrase: stdin is not a TTY. Set ${ENV_VAR} for non-interactive use.`
|
|
7228
|
-
);
|
|
7229
|
-
}
|
|
7230
|
-
process.stdout.write(prompt);
|
|
7231
|
-
const wasRaw = stdin.isRaw;
|
|
7232
|
-
stdin.setRawMode(true);
|
|
7233
|
-
stdin.resume();
|
|
7234
|
-
stdin.setEncoding("utf8");
|
|
7235
|
-
return new Promise((resolve5, reject) => {
|
|
7236
|
-
let buf = "";
|
|
7237
|
-
const cleanup = () => {
|
|
7238
|
-
stdin.removeListener("data", onData);
|
|
7239
|
-
stdin.setRawMode(wasRaw);
|
|
7240
|
-
stdin.pause();
|
|
7241
|
-
process.stdout.write("\n");
|
|
7242
|
-
};
|
|
7243
|
-
const onData = (chunk) => {
|
|
7244
|
-
const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
7245
|
-
for (let i = 0; i < text.length; i++) {
|
|
7246
|
-
const ch = text[i];
|
|
7247
|
-
if (ch === "") {
|
|
7248
|
-
cleanup();
|
|
7249
|
-
reject(new Error("cancelled"));
|
|
7250
|
-
return;
|
|
7251
|
-
}
|
|
7252
|
-
if (ch === "" && buf.length === 0) {
|
|
7253
|
-
cleanup();
|
|
7254
|
-
reject(new Error("cancelled"));
|
|
7255
|
-
return;
|
|
7256
|
-
}
|
|
7257
|
-
if (ch === "\r" || ch === "\n") {
|
|
7258
|
-
cleanup();
|
|
7259
|
-
resolve5(buf);
|
|
7260
|
-
return;
|
|
7261
|
-
}
|
|
7262
|
-
if (ch === "\x7F" || ch === "\b") {
|
|
7263
|
-
buf = buf.slice(0, -1);
|
|
7264
|
-
continue;
|
|
7265
|
-
}
|
|
7266
|
-
buf += ch;
|
|
7267
|
-
}
|
|
7268
|
-
};
|
|
7269
|
-
stdin.on("data", onData);
|
|
7270
|
-
});
|
|
7129
|
+
return { held: true, holder, stale: !holderMayRun(holder, deps) };
|
|
7271
7130
|
}
|
|
7272
7131
|
|
|
7273
7132
|
// src/paths.ts
|
|
@@ -7310,148 +7169,582 @@ function osDefaultDir(env, platform, home) {
|
|
|
7310
7169
|
}
|
|
7311
7170
|
}
|
|
7312
7171
|
|
|
7313
|
-
// src/
|
|
7314
|
-
var
|
|
7315
|
-
var
|
|
7316
|
-
var
|
|
7317
|
-
|
|
7318
|
-
// ../../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
|
|
7319
|
-
var external_exports = {};
|
|
7320
|
-
__export(external_exports, {
|
|
7321
|
-
BRAND: () => BRAND,
|
|
7322
|
-
DIRTY: () => DIRTY,
|
|
7323
|
-
EMPTY_PATH: () => EMPTY_PATH,
|
|
7324
|
-
INVALID: () => INVALID,
|
|
7325
|
-
NEVER: () => NEVER,
|
|
7326
|
-
OK: () => OK,
|
|
7327
|
-
ParseStatus: () => ParseStatus,
|
|
7328
|
-
Schema: () => ZodType,
|
|
7329
|
-
ZodAny: () => ZodAny,
|
|
7330
|
-
ZodArray: () => ZodArray,
|
|
7331
|
-
ZodBigInt: () => ZodBigInt,
|
|
7332
|
-
ZodBoolean: () => ZodBoolean,
|
|
7333
|
-
ZodBranded: () => ZodBranded,
|
|
7334
|
-
ZodCatch: () => ZodCatch,
|
|
7335
|
-
ZodDate: () => ZodDate,
|
|
7336
|
-
ZodDefault: () => ZodDefault,
|
|
7337
|
-
ZodDiscriminatedUnion: () => ZodDiscriminatedUnion,
|
|
7338
|
-
ZodEffects: () => ZodEffects,
|
|
7339
|
-
ZodEnum: () => ZodEnum,
|
|
7340
|
-
ZodError: () => ZodError,
|
|
7341
|
-
ZodFirstPartyTypeKind: () => ZodFirstPartyTypeKind,
|
|
7342
|
-
ZodFunction: () => ZodFunction,
|
|
7343
|
-
ZodIntersection: () => ZodIntersection,
|
|
7344
|
-
ZodIssueCode: () => ZodIssueCode,
|
|
7345
|
-
ZodLazy: () => ZodLazy,
|
|
7346
|
-
ZodLiteral: () => ZodLiteral,
|
|
7347
|
-
ZodMap: () => ZodMap,
|
|
7348
|
-
ZodNaN: () => ZodNaN,
|
|
7349
|
-
ZodNativeEnum: () => ZodNativeEnum,
|
|
7350
|
-
ZodNever: () => ZodNever,
|
|
7351
|
-
ZodNull: () => ZodNull,
|
|
7352
|
-
ZodNullable: () => ZodNullable,
|
|
7353
|
-
ZodNumber: () => ZodNumber,
|
|
7354
|
-
ZodObject: () => ZodObject,
|
|
7355
|
-
ZodOptional: () => ZodOptional,
|
|
7356
|
-
ZodParsedType: () => ZodParsedType,
|
|
7357
|
-
ZodPipeline: () => ZodPipeline,
|
|
7358
|
-
ZodPromise: () => ZodPromise,
|
|
7359
|
-
ZodReadonly: () => ZodReadonly,
|
|
7360
|
-
ZodRecord: () => ZodRecord,
|
|
7361
|
-
ZodSchema: () => ZodType,
|
|
7362
|
-
ZodSet: () => ZodSet,
|
|
7363
|
-
ZodString: () => ZodString,
|
|
7364
|
-
ZodSymbol: () => ZodSymbol,
|
|
7365
|
-
ZodTransformer: () => ZodEffects,
|
|
7366
|
-
ZodTuple: () => ZodTuple,
|
|
7367
|
-
ZodType: () => ZodType,
|
|
7368
|
-
ZodUndefined: () => ZodUndefined,
|
|
7369
|
-
ZodUnion: () => ZodUnion,
|
|
7370
|
-
ZodUnknown: () => ZodUnknown,
|
|
7371
|
-
ZodVoid: () => ZodVoid,
|
|
7372
|
-
addIssueToContext: () => addIssueToContext,
|
|
7373
|
-
any: () => anyType,
|
|
7374
|
-
array: () => arrayType,
|
|
7375
|
-
bigint: () => bigIntType,
|
|
7376
|
-
boolean: () => booleanType,
|
|
7377
|
-
coerce: () => coerce,
|
|
7378
|
-
custom: () => custom,
|
|
7379
|
-
date: () => dateType,
|
|
7380
|
-
datetimeRegex: () => datetimeRegex,
|
|
7381
|
-
defaultErrorMap: () => en_default,
|
|
7382
|
-
discriminatedUnion: () => discriminatedUnionType,
|
|
7383
|
-
effect: () => effectsType,
|
|
7384
|
-
enum: () => enumType,
|
|
7385
|
-
function: () => functionType,
|
|
7386
|
-
getErrorMap: () => getErrorMap,
|
|
7387
|
-
getParsedType: () => getParsedType,
|
|
7388
|
-
instanceof: () => instanceOfType,
|
|
7389
|
-
intersection: () => intersectionType,
|
|
7390
|
-
isAborted: () => isAborted,
|
|
7391
|
-
isAsync: () => isAsync,
|
|
7392
|
-
isDirty: () => isDirty,
|
|
7393
|
-
isValid: () => isValid,
|
|
7394
|
-
late: () => late,
|
|
7395
|
-
lazy: () => lazyType,
|
|
7396
|
-
literal: () => literalType,
|
|
7397
|
-
makeIssue: () => makeIssue,
|
|
7398
|
-
map: () => mapType,
|
|
7399
|
-
nan: () => nanType,
|
|
7400
|
-
nativeEnum: () => nativeEnumType,
|
|
7401
|
-
never: () => neverType,
|
|
7402
|
-
null: () => nullType,
|
|
7403
|
-
nullable: () => nullableType,
|
|
7404
|
-
number: () => numberType,
|
|
7405
|
-
object: () => objectType,
|
|
7406
|
-
objectUtil: () => objectUtil,
|
|
7407
|
-
oboolean: () => oboolean,
|
|
7408
|
-
onumber: () => onumber,
|
|
7409
|
-
optional: () => optionalType,
|
|
7410
|
-
ostring: () => ostring,
|
|
7411
|
-
pipeline: () => pipelineType,
|
|
7412
|
-
preprocess: () => preprocessType,
|
|
7413
|
-
promise: () => promiseType,
|
|
7414
|
-
quotelessJson: () => quotelessJson,
|
|
7415
|
-
record: () => recordType,
|
|
7416
|
-
set: () => setType,
|
|
7417
|
-
setErrorMap: () => setErrorMap,
|
|
7418
|
-
strictObject: () => strictObjectType,
|
|
7419
|
-
string: () => stringType,
|
|
7420
|
-
symbol: () => symbolType,
|
|
7421
|
-
transformer: () => effectsType,
|
|
7422
|
-
tuple: () => tupleType,
|
|
7423
|
-
undefined: () => undefinedType,
|
|
7424
|
-
union: () => unionType,
|
|
7425
|
-
unknown: () => unknownType,
|
|
7426
|
-
util: () => util,
|
|
7427
|
-
void: () => voidType
|
|
7428
|
-
});
|
|
7172
|
+
// src/seed.ts
|
|
7173
|
+
var import_node_child_process = require("child_process");
|
|
7174
|
+
var import_node_crypto3 = require("crypto");
|
|
7175
|
+
var import_node_fs3 = require("fs");
|
|
7176
|
+
var import_node_path4 = require("path");
|
|
7429
7177
|
|
|
7430
|
-
//
|
|
7431
|
-
var
|
|
7432
|
-
(
|
|
7433
|
-
|
|
7434
|
-
|
|
7435
|
-
|
|
7436
|
-
|
|
7437
|
-
|
|
7438
|
-
|
|
7439
|
-
|
|
7440
|
-
|
|
7441
|
-
|
|
7442
|
-
|
|
7443
|
-
|
|
7444
|
-
|
|
7445
|
-
|
|
7446
|
-
|
|
7447
|
-
|
|
7448
|
-
|
|
7449
|
-
|
|
7450
|
-
|
|
7451
|
-
|
|
7452
|
-
|
|
7453
|
-
|
|
7454
|
-
|
|
7178
|
+
// src/state.ts
|
|
7179
|
+
var import_node_crypto2 = require("crypto");
|
|
7180
|
+
var import_node_fs2 = require("fs");
|
|
7181
|
+
var import_node_path3 = require("path");
|
|
7182
|
+
var import_argon2 = __toESM(require("argon2"), 1);
|
|
7183
|
+
var FORMAT_TAG = "SEAL_MCP_STATE v1";
|
|
7184
|
+
var STATE_VERSION = 1;
|
|
7185
|
+
var ARGON2_MEMORY_KIB = 65536;
|
|
7186
|
+
var ARGON2_ITERATIONS = 3;
|
|
7187
|
+
var ARGON2_PARALLELISM = 4;
|
|
7188
|
+
var ARGON2_SALT_BYTES = 16;
|
|
7189
|
+
var KDF_BOUNDS = {
|
|
7190
|
+
memory_kib: { min: 8192, max: 1048576 },
|
|
7191
|
+
// 8 MiB .. 1 GiB
|
|
7192
|
+
iterations: { min: 1, max: 16 },
|
|
7193
|
+
parallelism: { min: 1, max: 16 }
|
|
7194
|
+
};
|
|
7195
|
+
var AES_KEY_BYTES = 32;
|
|
7196
|
+
var AES_NONCE_BYTES = 12;
|
|
7197
|
+
var StatePassphraseInvalid = class extends Error {
|
|
7198
|
+
constructor() {
|
|
7199
|
+
super("AES-256-GCM tag check failed: another passphrase, or a tampered file");
|
|
7200
|
+
this.name = "StatePassphraseInvalid";
|
|
7201
|
+
}
|
|
7202
|
+
};
|
|
7203
|
+
var StateCorrupt = class extends Error {
|
|
7204
|
+
constructor(reason) {
|
|
7205
|
+
super(`state.corrupt: ${reason}`);
|
|
7206
|
+
this.name = "StateCorrupt";
|
|
7207
|
+
}
|
|
7208
|
+
};
|
|
7209
|
+
function toB642(bytes) {
|
|
7210
|
+
return Buffer.from(bytes).toString("base64");
|
|
7211
|
+
}
|
|
7212
|
+
function fromB642(s) {
|
|
7213
|
+
return new Uint8Array(Buffer.from(s, "base64"));
|
|
7214
|
+
}
|
|
7215
|
+
var CURRENT_KDF_PARAMS = {
|
|
7216
|
+
memory_kib: ARGON2_MEMORY_KIB,
|
|
7217
|
+
iterations: ARGON2_ITERATIONS,
|
|
7218
|
+
parallelism: ARGON2_PARALLELISM
|
|
7219
|
+
};
|
|
7220
|
+
function boundedKdfParam(name, value) {
|
|
7221
|
+
if (typeof value !== "number" || !Number.isInteger(value)) {
|
|
7222
|
+
throw new StateCorrupt(`kdf.${name} is not an integer: ${String(value)}`);
|
|
7223
|
+
}
|
|
7224
|
+
const { min, max } = KDF_BOUNDS[name];
|
|
7225
|
+
if (value < min || value > max) {
|
|
7226
|
+
throw new StateCorrupt(`kdf.${name}=${value} outside sane bounds [${min}, ${max}]`);
|
|
7227
|
+
}
|
|
7228
|
+
return value;
|
|
7229
|
+
}
|
|
7230
|
+
async function deriveKek(passphrase, saltB64, params) {
|
|
7231
|
+
const salt = fromB642(saltB64);
|
|
7232
|
+
if (salt.length !== ARGON2_SALT_BYTES) {
|
|
7233
|
+
throw new StateCorrupt(`salt length is ${salt.length}, expected ${ARGON2_SALT_BYTES}`);
|
|
7234
|
+
}
|
|
7235
|
+
const kek = await import_argon2.default.hash(passphrase, {
|
|
7236
|
+
type: import_argon2.default.argon2id,
|
|
7237
|
+
raw: true,
|
|
7238
|
+
salt: Buffer.from(salt),
|
|
7239
|
+
memoryCost: params.memory_kib,
|
|
7240
|
+
timeCost: params.iterations,
|
|
7241
|
+
parallelism: params.parallelism,
|
|
7242
|
+
hashLength: AES_KEY_BYTES
|
|
7243
|
+
});
|
|
7244
|
+
return new Uint8Array(kek);
|
|
7245
|
+
}
|
|
7246
|
+
async function aesGcmEncrypt(kek, nonce, plaintext) {
|
|
7247
|
+
const key = await import_node_crypto2.webcrypto.subtle.importKey("raw", kek, "AES-GCM", false, ["encrypt"]);
|
|
7248
|
+
const ct = await import_node_crypto2.webcrypto.subtle.encrypt(
|
|
7249
|
+
{ name: "AES-GCM", iv: nonce, tagLength: 128 },
|
|
7250
|
+
key,
|
|
7251
|
+
plaintext
|
|
7252
|
+
);
|
|
7253
|
+
return new Uint8Array(ct);
|
|
7254
|
+
}
|
|
7255
|
+
async function aesGcmDecrypt(kek, nonce, ciphertextWithTag) {
|
|
7256
|
+
const key = await import_node_crypto2.webcrypto.subtle.importKey("raw", kek, "AES-GCM", false, ["decrypt"]);
|
|
7257
|
+
try {
|
|
7258
|
+
const pt = await import_node_crypto2.webcrypto.subtle.decrypt(
|
|
7259
|
+
{ name: "AES-GCM", iv: nonce, tagLength: 128 },
|
|
7260
|
+
key,
|
|
7261
|
+
ciphertextWithTag
|
|
7262
|
+
);
|
|
7263
|
+
return new Uint8Array(pt);
|
|
7264
|
+
} catch {
|
|
7265
|
+
throw new StatePassphraseInvalid();
|
|
7266
|
+
}
|
|
7267
|
+
}
|
|
7268
|
+
function createEmptyState(identity) {
|
|
7269
|
+
return {
|
|
7270
|
+
version: STATE_VERSION,
|
|
7271
|
+
identity,
|
|
7272
|
+
handles: {},
|
|
7273
|
+
audit: [],
|
|
7274
|
+
pending: {}
|
|
7275
|
+
};
|
|
7276
|
+
}
|
|
7277
|
+
function validateEnvelope(json) {
|
|
7278
|
+
if (typeof json !== "object" || json === null) {
|
|
7279
|
+
throw new StateCorrupt("envelope is not an object");
|
|
7280
|
+
}
|
|
7281
|
+
const e = json;
|
|
7282
|
+
if (e.version !== STATE_VERSION) {
|
|
7283
|
+
throw new StateCorrupt(`unknown version: ${String(e.version)}`);
|
|
7284
|
+
}
|
|
7285
|
+
if (e.format !== FORMAT_TAG) {
|
|
7286
|
+
throw new StateCorrupt(`unknown format tag: ${String(e.format)}`);
|
|
7287
|
+
}
|
|
7288
|
+
const kdf = e.kdf;
|
|
7289
|
+
if (!kdf || kdf.algo !== "argon2id" || typeof kdf.salt_b64 !== "string") {
|
|
7290
|
+
throw new StateCorrupt("kdf header missing or wrong algo");
|
|
7291
|
+
}
|
|
7292
|
+
const cipher = e.cipher;
|
|
7293
|
+
if (!cipher || cipher.algo !== "aes-256-gcm" || typeof cipher.nonce_b64 !== "string" || typeof cipher.ciphertext_b64 !== "string") {
|
|
7294
|
+
throw new StateCorrupt("cipher header missing or wrong algo");
|
|
7295
|
+
}
|
|
7296
|
+
}
|
|
7297
|
+
function validatePlaintext(json) {
|
|
7298
|
+
if (typeof json !== "object" || json === null) {
|
|
7299
|
+
throw new StateCorrupt("plaintext is not an object");
|
|
7300
|
+
}
|
|
7301
|
+
const s = json;
|
|
7302
|
+
if (s.version !== STATE_VERSION) {
|
|
7303
|
+
throw new StateCorrupt(`plaintext version: ${String(s.version)}`);
|
|
7304
|
+
}
|
|
7305
|
+
const id = s.identity;
|
|
7306
|
+
if (!id || typeof id.x25519_secret_b64 !== "string" || typeof id.x25519_public_b64 !== "string") {
|
|
7307
|
+
throw new StateCorrupt("identity missing or malformed");
|
|
7308
|
+
}
|
|
7309
|
+
if (typeof s.handles !== "object" || s.handles === null) {
|
|
7310
|
+
throw new StateCorrupt("handles is not an object");
|
|
7311
|
+
}
|
|
7312
|
+
if (!Array.isArray(s.audit)) {
|
|
7313
|
+
throw new StateCorrupt("audit is not an array");
|
|
7314
|
+
}
|
|
7315
|
+
if (s.pending !== void 0 && (typeof s.pending !== "object" || s.pending === null)) {
|
|
7316
|
+
throw new StateCorrupt("pending is not an object");
|
|
7317
|
+
}
|
|
7318
|
+
}
|
|
7319
|
+
async function loadState(passphrase, statePath) {
|
|
7320
|
+
const raw = await import_node_fs2.promises.readFile(statePath, "utf8");
|
|
7321
|
+
let envelope;
|
|
7322
|
+
try {
|
|
7323
|
+
envelope = JSON.parse(raw);
|
|
7324
|
+
} catch (err2) {
|
|
7325
|
+
throw new StateCorrupt(`envelope is not valid JSON: ${err2.message}`);
|
|
7326
|
+
}
|
|
7327
|
+
validateEnvelope(envelope);
|
|
7328
|
+
const kek = await deriveKek(passphrase, envelope.kdf.salt_b64, {
|
|
7329
|
+
memory_kib: boundedKdfParam("memory_kib", envelope.kdf.memory_kib),
|
|
7330
|
+
iterations: boundedKdfParam("iterations", envelope.kdf.iterations),
|
|
7331
|
+
parallelism: boundedKdfParam("parallelism", envelope.kdf.parallelism)
|
|
7332
|
+
});
|
|
7333
|
+
const nonce = fromB642(envelope.cipher.nonce_b64);
|
|
7334
|
+
if (nonce.length !== AES_NONCE_BYTES) {
|
|
7335
|
+
throw new StateCorrupt(`nonce length is ${nonce.length}, expected ${AES_NONCE_BYTES}`);
|
|
7336
|
+
}
|
|
7337
|
+
const ct = fromB642(envelope.cipher.ciphertext_b64);
|
|
7338
|
+
const ptBytes = await aesGcmDecrypt(kek, nonce, ct);
|
|
7339
|
+
const ptText = new TextDecoder("utf-8", { fatal: true }).decode(ptBytes);
|
|
7340
|
+
let plaintext;
|
|
7341
|
+
try {
|
|
7342
|
+
plaintext = JSON.parse(ptText);
|
|
7343
|
+
} catch (err2) {
|
|
7344
|
+
throw new StateCorrupt(`plaintext is not valid JSON: ${err2.message}`);
|
|
7345
|
+
}
|
|
7346
|
+
validatePlaintext(plaintext);
|
|
7347
|
+
if (plaintext.pending === void 0) {
|
|
7348
|
+
plaintext.pending = {};
|
|
7349
|
+
}
|
|
7350
|
+
return plaintext;
|
|
7351
|
+
}
|
|
7352
|
+
async function saveState(state, passphrase, statePath) {
|
|
7353
|
+
validatePlaintext(state);
|
|
7354
|
+
const salt = (0, import_node_crypto2.randomBytes)(ARGON2_SALT_BYTES);
|
|
7355
|
+
const nonce = (0, import_node_crypto2.randomBytes)(AES_NONCE_BYTES);
|
|
7356
|
+
const kek = await deriveKek(passphrase, toB642(salt), CURRENT_KDF_PARAMS);
|
|
7357
|
+
const ptBytes = new TextEncoder().encode(JSON.stringify(state));
|
|
7358
|
+
const ct = await aesGcmEncrypt(kek, nonce, ptBytes);
|
|
7359
|
+
const envelope = {
|
|
7360
|
+
version: STATE_VERSION,
|
|
7361
|
+
format: FORMAT_TAG,
|
|
7362
|
+
kdf: {
|
|
7363
|
+
algo: "argon2id",
|
|
7364
|
+
memory_kib: ARGON2_MEMORY_KIB,
|
|
7365
|
+
iterations: ARGON2_ITERATIONS,
|
|
7366
|
+
parallelism: ARGON2_PARALLELISM,
|
|
7367
|
+
salt_b64: toB642(salt)
|
|
7368
|
+
},
|
|
7369
|
+
cipher: {
|
|
7370
|
+
algo: "aes-256-gcm",
|
|
7371
|
+
nonce_b64: toB642(nonce),
|
|
7372
|
+
ciphertext_b64: toB642(ct),
|
|
7373
|
+
tag_included: true
|
|
7374
|
+
}
|
|
7375
|
+
};
|
|
7376
|
+
const dir = (0, import_node_path3.dirname)(statePath);
|
|
7377
|
+
await import_node_fs2.promises.mkdir(dir, { recursive: true, mode: 448 });
|
|
7378
|
+
const tmp = `${statePath}.tmp-${process.pid}-${Date.now()}`;
|
|
7379
|
+
const fh = await import_node_fs2.promises.open(tmp, "w", 384);
|
|
7380
|
+
try {
|
|
7381
|
+
await fh.writeFile(JSON.stringify(envelope), "utf8");
|
|
7382
|
+
await fh.sync();
|
|
7383
|
+
} finally {
|
|
7384
|
+
await fh.close();
|
|
7385
|
+
}
|
|
7386
|
+
await import_node_fs2.promises.rename(tmp, statePath);
|
|
7387
|
+
try {
|
|
7388
|
+
const dh = await import_node_fs2.promises.open(dir, "r");
|
|
7389
|
+
try {
|
|
7390
|
+
await dh.sync();
|
|
7391
|
+
} finally {
|
|
7392
|
+
await dh.close();
|
|
7393
|
+
}
|
|
7394
|
+
} catch {
|
|
7395
|
+
}
|
|
7396
|
+
await import_node_fs2.promises.chmod(dir, 448).catch(() => {
|
|
7397
|
+
});
|
|
7398
|
+
}
|
|
7399
|
+
function updateState(passphrase, statePath, mutate) {
|
|
7400
|
+
return withStateLock((0, import_node_path3.dirname)(statePath), async () => {
|
|
7401
|
+
const view = await loadState(passphrase, statePath);
|
|
7402
|
+
if (await mutate(view) !== false) await saveState(view, passphrase, statePath);
|
|
7403
|
+
return view;
|
|
7404
|
+
});
|
|
7405
|
+
}
|
|
7406
|
+
|
|
7407
|
+
// src/seed.ts
|
|
7408
|
+
var SEED_ENV_VAR = "SEAL_SEED";
|
|
7409
|
+
var EPHEMERAL_ENV_VAR = "SEAL_MCP_EPHEMERAL";
|
|
7410
|
+
var PASSPHRASE_ENV_VAR = "SEAL_MCP_PASSPHRASE";
|
|
7411
|
+
var SEED_FILE_NAME = "seed";
|
|
7412
|
+
var KEYCHAIN_SERVICE = "seal-mcp";
|
|
7413
|
+
var KEYCHAIN_ACCOUNT = "seed";
|
|
7414
|
+
var LEGACY_KEYCHAIN_ACCOUNT = "state";
|
|
7415
|
+
var SeedInvalidError = class extends Error {
|
|
7416
|
+
code = "seed_invalid";
|
|
7417
|
+
constructor(where) {
|
|
7418
|
+
super(
|
|
7419
|
+
`${where} is not an agent key: expected 43 base64url characters, as \`sealnet-mcp seed --force\` or \`seal keygen --export --force\` print them`
|
|
7420
|
+
);
|
|
7421
|
+
this.name = "SeedInvalidError";
|
|
7422
|
+
}
|
|
7423
|
+
};
|
|
7424
|
+
var IdentityConflictError = class extends Error {
|
|
7425
|
+
code = "identity_conflict";
|
|
7426
|
+
constructor(statePath) {
|
|
7427
|
+
super(
|
|
7428
|
+
`${statePath} does not open with this agent's key: it belongs to another key (a foreign ${SEED_ENV_VAR}?) or was tampered with. Unset ${SEED_ENV_VAR}, or run \`sealnet-mcp reset --force\` to start over with a new address.`
|
|
7429
|
+
);
|
|
7430
|
+
this.name = "IdentityConflictError";
|
|
7431
|
+
}
|
|
7432
|
+
};
|
|
7433
|
+
function keyringEntry(keyring, platform, service, account) {
|
|
7434
|
+
return platform === "win32" ? keyring.AsyncEntry.withTarget(`${service}/${account}`, service, account) : new keyring.AsyncEntry(service, account, { linux: { store: "secret-service" } });
|
|
7435
|
+
}
|
|
7436
|
+
function readKeytarLinuxEntry(service, account) {
|
|
7437
|
+
return new Promise((resolve5) => {
|
|
7438
|
+
(0, import_node_child_process.execFile)("secret-tool", ["lookup", "service", service, "account", account], { timeout: 5e3 }, (err2, stdout) => {
|
|
7439
|
+
const value = err2 ? "" : stdout.replace(/\n$/, "");
|
|
7440
|
+
resolve5(value.length > 0 ? value : null);
|
|
7441
|
+
});
|
|
7442
|
+
});
|
|
7443
|
+
}
|
|
7444
|
+
function keytarOverKeyring(keyring, platform = process.platform, readLegacy = readKeytarLinuxEntry) {
|
|
7445
|
+
const entry = (service, account) => keyringEntry(keyring, platform, service, account);
|
|
7446
|
+
const store = (service, account, password) => entry(service, account).setSecret(Buffer.from(password, "utf8"));
|
|
7447
|
+
return {
|
|
7448
|
+
async getPassword(service, account) {
|
|
7449
|
+
const secret = await entry(service, account).getSecret();
|
|
7450
|
+
if (secret !== void 0) return Buffer.from(secret).toString("utf8");
|
|
7451
|
+
const legacy = platform === "linux" ? await readLegacy(service, account) : null;
|
|
7452
|
+
if (legacy !== null) await store(service, account, legacy).catch(() => void 0);
|
|
7453
|
+
return legacy;
|
|
7454
|
+
},
|
|
7455
|
+
setPassword: store,
|
|
7456
|
+
deletePassword: (service, account) => entry(service, account).deleteCredential()
|
|
7457
|
+
};
|
|
7458
|
+
}
|
|
7459
|
+
var keytarCache = void 0;
|
|
7460
|
+
async function loadKeytar() {
|
|
7461
|
+
if (keytarCache !== void 0) return keytarCache;
|
|
7462
|
+
try {
|
|
7463
|
+
const mod = await import("@napi-rs/keyring");
|
|
7464
|
+
const keyring = mod.default ?? mod;
|
|
7465
|
+
keyringEntry(keyring, process.platform, KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);
|
|
7466
|
+
keytarCache = keytarOverKeyring(keyring);
|
|
7467
|
+
} catch {
|
|
7468
|
+
keytarCache = null;
|
|
7469
|
+
}
|
|
7470
|
+
return keytarCache;
|
|
7471
|
+
}
|
|
7472
|
+
async function keychainGet(account) {
|
|
7473
|
+
const keytar = await loadKeytar();
|
|
7474
|
+
if (keytar === null) return null;
|
|
7475
|
+
try {
|
|
7476
|
+
const value = await keytar.getPassword(KEYCHAIN_SERVICE, account);
|
|
7477
|
+
return value !== null && value.length > 0 ? value : null;
|
|
7478
|
+
} catch {
|
|
7479
|
+
return null;
|
|
7480
|
+
}
|
|
7481
|
+
}
|
|
7482
|
+
async function keychainDelete(account) {
|
|
7483
|
+
const keytar = await loadKeytar();
|
|
7484
|
+
if (keytar === null) return false;
|
|
7485
|
+
try {
|
|
7486
|
+
return await keytar.deletePassword(KEYCHAIN_SERVICE, account);
|
|
7487
|
+
} catch {
|
|
7488
|
+
return false;
|
|
7489
|
+
}
|
|
7490
|
+
}
|
|
7491
|
+
function parseSeed(text, where) {
|
|
7492
|
+
const seed = Buffer.from(text, "base64url");
|
|
7493
|
+
if (!/^[A-Za-z0-9_-]{43}$/.test(text) || seed.toString("base64url") !== text) throw new SeedInvalidError(where);
|
|
7494
|
+
return new Uint8Array(seed);
|
|
7495
|
+
}
|
|
7496
|
+
async function readSeed(stateDir, env = process.env) {
|
|
7497
|
+
const fromEnv = env[SEED_ENV_VAR]?.trim();
|
|
7498
|
+
if (fromEnv) return { seed: parseSeed(fromEnv, SEED_ENV_VAR), source: "env" };
|
|
7499
|
+
const stored = await keychainGet(KEYCHAIN_ACCOUNT);
|
|
7500
|
+
if (stored !== null) return { seed: parseSeed(stored, `keychain entry ${KEYCHAIN_SERVICE}/${KEYCHAIN_ACCOUNT}`), source: "keychain" };
|
|
7501
|
+
const file = (0, import_node_path4.join)(stateDir, SEED_FILE_NAME);
|
|
7502
|
+
let text;
|
|
7503
|
+
try {
|
|
7504
|
+
text = await import_node_fs3.promises.readFile(file, "utf8");
|
|
7505
|
+
} catch (err2) {
|
|
7506
|
+
if (err2.code === "ENOENT") return null;
|
|
7507
|
+
throw err2;
|
|
7508
|
+
}
|
|
7509
|
+
return { seed: parseSeed(text.trim(), file), source: "file" };
|
|
7510
|
+
}
|
|
7511
|
+
async function storeSeed(stateDir, seed) {
|
|
7512
|
+
const text = Buffer.from(seed).toString("base64url");
|
|
7513
|
+
const keytar = await loadKeytar();
|
|
7514
|
+
if (keytar !== null) {
|
|
7515
|
+
try {
|
|
7516
|
+
await keytar.setPassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT, text);
|
|
7517
|
+
return "keychain";
|
|
7518
|
+
} catch {
|
|
7519
|
+
}
|
|
7520
|
+
}
|
|
7521
|
+
const fh = await import_node_fs3.promises.open((0, import_node_path4.join)(stateDir, SEED_FILE_NAME), "wx", 384);
|
|
7522
|
+
try {
|
|
7523
|
+
await fh.writeFile(`${text}
|
|
7524
|
+
`, "utf8");
|
|
7525
|
+
await fh.sync();
|
|
7526
|
+
} finally {
|
|
7527
|
+
await fh.close();
|
|
7528
|
+
}
|
|
7529
|
+
return "file";
|
|
7530
|
+
}
|
|
7531
|
+
function statePassphrase(seed) {
|
|
7532
|
+
return Buffer.from((0, import_node_crypto3.hkdfSync)("sha256", seed, "seal-mcp:state:v1", "passphrase", 32)).toString("base64url");
|
|
7533
|
+
}
|
|
7534
|
+
async function legacyPassphrase(env) {
|
|
7535
|
+
const stored = await keychainGet(LEGACY_KEYCHAIN_ACCOUNT);
|
|
7536
|
+
if (stored !== null) return { passphrase: stored, source: "keychain" };
|
|
7537
|
+
const fromEnv = env[PASSPHRASE_ENV_VAR];
|
|
7538
|
+
return fromEnv ? { passphrase: fromEnv, source: "env" } : null;
|
|
7539
|
+
}
|
|
7540
|
+
async function tryOpen(passphrase, statePath) {
|
|
7541
|
+
try {
|
|
7542
|
+
return await loadState(passphrase, statePath);
|
|
7543
|
+
} catch (err2) {
|
|
7544
|
+
if (err2 instanceof StatePassphraseInvalid) return null;
|
|
7545
|
+
throw err2;
|
|
7546
|
+
}
|
|
7547
|
+
}
|
|
7548
|
+
async function requireIdentity(view, seed, statePath) {
|
|
7549
|
+
const { secret } = await loadIdentity(view.identity);
|
|
7550
|
+
if (!Buffer.from(secret).equals(Buffer.from(seed))) throw new IdentityConflictError(statePath);
|
|
7551
|
+
}
|
|
7552
|
+
function openAgentState(path, env = process.env) {
|
|
7553
|
+
return withStateLock(path.dir, async () => {
|
|
7554
|
+
let root = await readSeed(path.dir, env);
|
|
7555
|
+
if (!await import_node_fs3.promises.stat(path.statePath).then(() => true, () => false)) {
|
|
7556
|
+
if (root === null) {
|
|
7557
|
+
const { secret } = await generateIdentity();
|
|
7558
|
+
root = { seed: secret, source: await storeSeed(path.dir, secret) };
|
|
7559
|
+
}
|
|
7560
|
+
const passphrase2 = statePassphrase(root.seed);
|
|
7561
|
+
const identity = await identityFromSeed(root.seed);
|
|
7562
|
+
await saveState(createEmptyState(identityToFields(identity)), passphrase2, path.statePath);
|
|
7563
|
+
return { root, passphrase: passphrase2, created: true, moved: null };
|
|
7564
|
+
}
|
|
7565
|
+
if (root !== null) {
|
|
7566
|
+
const view2 = await tryOpen(statePassphrase(root.seed), path.statePath);
|
|
7567
|
+
if (view2 !== null) {
|
|
7568
|
+
await requireIdentity(view2, root.seed, path.statePath);
|
|
7569
|
+
return { root, passphrase: statePassphrase(root.seed), created: false, moved: null };
|
|
7570
|
+
}
|
|
7571
|
+
}
|
|
7572
|
+
const legacy = await legacyPassphrase(env);
|
|
7573
|
+
const view = legacy === null ? null : await tryOpen(legacy.passphrase, path.statePath);
|
|
7574
|
+
if (legacy === null || view === null) throw new IdentityConflictError(path.statePath);
|
|
7575
|
+
if (root !== null) await requireIdentity(view, root.seed, path.statePath);
|
|
7576
|
+
else {
|
|
7577
|
+
const { secret } = await loadIdentity(view.identity);
|
|
7578
|
+
root = { seed: secret, source: await storeSeed(path.dir, secret) };
|
|
7579
|
+
}
|
|
7580
|
+
const passphrase = statePassphrase(root.seed);
|
|
7581
|
+
await saveState(view, passphrase, path.statePath);
|
|
7582
|
+
await keychainDelete(LEGACY_KEYCHAIN_ACCOUNT);
|
|
7583
|
+
return { root, passphrase, created: false, moved: legacy.source };
|
|
7584
|
+
});
|
|
7585
|
+
}
|
|
7586
|
+
async function deleteRootSecret(stateDir) {
|
|
7587
|
+
let seedFile = true;
|
|
7588
|
+
await import_node_fs3.promises.unlink((0, import_node_path4.join)(stateDir, SEED_FILE_NAME)).catch((err2) => {
|
|
7589
|
+
if (err2.code !== "ENOENT") throw err2;
|
|
7590
|
+
seedFile = false;
|
|
7591
|
+
});
|
|
7592
|
+
const seed = await keychainDelete(KEYCHAIN_ACCOUNT);
|
|
7593
|
+
const legacy = await keychainDelete(LEGACY_KEYCHAIN_ACCOUNT);
|
|
7594
|
+
return { seedFile, keychain: seed || legacy };
|
|
7595
|
+
}
|
|
7596
|
+
async function probeKeychain() {
|
|
7597
|
+
const keytar = await loadKeytar();
|
|
7598
|
+
if (keytar === null) return { available: false, hasSeed: false };
|
|
7599
|
+
try {
|
|
7600
|
+
const value = await keytar.getPassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);
|
|
7601
|
+
return { available: true, hasSeed: value !== null && value.length > 0 };
|
|
7602
|
+
} catch {
|
|
7603
|
+
return { available: false, hasSeed: false };
|
|
7604
|
+
}
|
|
7605
|
+
}
|
|
7606
|
+
|
|
7607
|
+
// src/server.ts
|
|
7608
|
+
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
7609
|
+
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
7610
|
+
|
|
7611
|
+
// ../../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
|
|
7612
|
+
var external_exports = {};
|
|
7613
|
+
__export(external_exports, {
|
|
7614
|
+
BRAND: () => BRAND,
|
|
7615
|
+
DIRTY: () => DIRTY,
|
|
7616
|
+
EMPTY_PATH: () => EMPTY_PATH,
|
|
7617
|
+
INVALID: () => INVALID,
|
|
7618
|
+
NEVER: () => NEVER,
|
|
7619
|
+
OK: () => OK,
|
|
7620
|
+
ParseStatus: () => ParseStatus,
|
|
7621
|
+
Schema: () => ZodType,
|
|
7622
|
+
ZodAny: () => ZodAny,
|
|
7623
|
+
ZodArray: () => ZodArray,
|
|
7624
|
+
ZodBigInt: () => ZodBigInt,
|
|
7625
|
+
ZodBoolean: () => ZodBoolean,
|
|
7626
|
+
ZodBranded: () => ZodBranded,
|
|
7627
|
+
ZodCatch: () => ZodCatch,
|
|
7628
|
+
ZodDate: () => ZodDate,
|
|
7629
|
+
ZodDefault: () => ZodDefault,
|
|
7630
|
+
ZodDiscriminatedUnion: () => ZodDiscriminatedUnion,
|
|
7631
|
+
ZodEffects: () => ZodEffects,
|
|
7632
|
+
ZodEnum: () => ZodEnum,
|
|
7633
|
+
ZodError: () => ZodError,
|
|
7634
|
+
ZodFirstPartyTypeKind: () => ZodFirstPartyTypeKind,
|
|
7635
|
+
ZodFunction: () => ZodFunction,
|
|
7636
|
+
ZodIntersection: () => ZodIntersection,
|
|
7637
|
+
ZodIssueCode: () => ZodIssueCode,
|
|
7638
|
+
ZodLazy: () => ZodLazy,
|
|
7639
|
+
ZodLiteral: () => ZodLiteral,
|
|
7640
|
+
ZodMap: () => ZodMap,
|
|
7641
|
+
ZodNaN: () => ZodNaN,
|
|
7642
|
+
ZodNativeEnum: () => ZodNativeEnum,
|
|
7643
|
+
ZodNever: () => ZodNever,
|
|
7644
|
+
ZodNull: () => ZodNull,
|
|
7645
|
+
ZodNullable: () => ZodNullable,
|
|
7646
|
+
ZodNumber: () => ZodNumber,
|
|
7647
|
+
ZodObject: () => ZodObject,
|
|
7648
|
+
ZodOptional: () => ZodOptional,
|
|
7649
|
+
ZodParsedType: () => ZodParsedType,
|
|
7650
|
+
ZodPipeline: () => ZodPipeline,
|
|
7651
|
+
ZodPromise: () => ZodPromise,
|
|
7652
|
+
ZodReadonly: () => ZodReadonly,
|
|
7653
|
+
ZodRecord: () => ZodRecord,
|
|
7654
|
+
ZodSchema: () => ZodType,
|
|
7655
|
+
ZodSet: () => ZodSet,
|
|
7656
|
+
ZodString: () => ZodString,
|
|
7657
|
+
ZodSymbol: () => ZodSymbol,
|
|
7658
|
+
ZodTransformer: () => ZodEffects,
|
|
7659
|
+
ZodTuple: () => ZodTuple,
|
|
7660
|
+
ZodType: () => ZodType,
|
|
7661
|
+
ZodUndefined: () => ZodUndefined,
|
|
7662
|
+
ZodUnion: () => ZodUnion,
|
|
7663
|
+
ZodUnknown: () => ZodUnknown,
|
|
7664
|
+
ZodVoid: () => ZodVoid,
|
|
7665
|
+
addIssueToContext: () => addIssueToContext,
|
|
7666
|
+
any: () => anyType,
|
|
7667
|
+
array: () => arrayType,
|
|
7668
|
+
bigint: () => bigIntType,
|
|
7669
|
+
boolean: () => booleanType,
|
|
7670
|
+
coerce: () => coerce,
|
|
7671
|
+
custom: () => custom,
|
|
7672
|
+
date: () => dateType,
|
|
7673
|
+
datetimeRegex: () => datetimeRegex,
|
|
7674
|
+
defaultErrorMap: () => en_default,
|
|
7675
|
+
discriminatedUnion: () => discriminatedUnionType,
|
|
7676
|
+
effect: () => effectsType,
|
|
7677
|
+
enum: () => enumType,
|
|
7678
|
+
function: () => functionType,
|
|
7679
|
+
getErrorMap: () => getErrorMap,
|
|
7680
|
+
getParsedType: () => getParsedType,
|
|
7681
|
+
instanceof: () => instanceOfType,
|
|
7682
|
+
intersection: () => intersectionType,
|
|
7683
|
+
isAborted: () => isAborted,
|
|
7684
|
+
isAsync: () => isAsync,
|
|
7685
|
+
isDirty: () => isDirty,
|
|
7686
|
+
isValid: () => isValid,
|
|
7687
|
+
late: () => late,
|
|
7688
|
+
lazy: () => lazyType,
|
|
7689
|
+
literal: () => literalType,
|
|
7690
|
+
makeIssue: () => makeIssue,
|
|
7691
|
+
map: () => mapType,
|
|
7692
|
+
nan: () => nanType,
|
|
7693
|
+
nativeEnum: () => nativeEnumType,
|
|
7694
|
+
never: () => neverType,
|
|
7695
|
+
null: () => nullType,
|
|
7696
|
+
nullable: () => nullableType,
|
|
7697
|
+
number: () => numberType,
|
|
7698
|
+
object: () => objectType,
|
|
7699
|
+
objectUtil: () => objectUtil,
|
|
7700
|
+
oboolean: () => oboolean,
|
|
7701
|
+
onumber: () => onumber,
|
|
7702
|
+
optional: () => optionalType,
|
|
7703
|
+
ostring: () => ostring,
|
|
7704
|
+
pipeline: () => pipelineType,
|
|
7705
|
+
preprocess: () => preprocessType,
|
|
7706
|
+
promise: () => promiseType,
|
|
7707
|
+
quotelessJson: () => quotelessJson,
|
|
7708
|
+
record: () => recordType,
|
|
7709
|
+
set: () => setType,
|
|
7710
|
+
setErrorMap: () => setErrorMap,
|
|
7711
|
+
strictObject: () => strictObjectType,
|
|
7712
|
+
string: () => stringType,
|
|
7713
|
+
symbol: () => symbolType,
|
|
7714
|
+
transformer: () => effectsType,
|
|
7715
|
+
tuple: () => tupleType,
|
|
7716
|
+
undefined: () => undefinedType,
|
|
7717
|
+
union: () => unionType,
|
|
7718
|
+
unknown: () => unknownType,
|
|
7719
|
+
util: () => util,
|
|
7720
|
+
void: () => voidType
|
|
7721
|
+
});
|
|
7722
|
+
|
|
7723
|
+
// ../../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/util.js
|
|
7724
|
+
var util;
|
|
7725
|
+
(function(util2) {
|
|
7726
|
+
util2.assertEqual = (_) => {
|
|
7727
|
+
};
|
|
7728
|
+
function assertIs(_arg) {
|
|
7729
|
+
}
|
|
7730
|
+
util2.assertIs = assertIs;
|
|
7731
|
+
function assertNever(_x) {
|
|
7732
|
+
throw new Error();
|
|
7733
|
+
}
|
|
7734
|
+
util2.assertNever = assertNever;
|
|
7735
|
+
util2.arrayToEnum = (items) => {
|
|
7736
|
+
const obj = {};
|
|
7737
|
+
for (const item of items) {
|
|
7738
|
+
obj[item] = item;
|
|
7739
|
+
}
|
|
7740
|
+
return obj;
|
|
7741
|
+
};
|
|
7742
|
+
util2.getValidEnumValues = (obj) => {
|
|
7743
|
+
const validKeys = util2.objectKeys(obj).filter((k) => typeof obj[obj[k]] !== "number");
|
|
7744
|
+
const filtered = {};
|
|
7745
|
+
for (const k of validKeys) {
|
|
7746
|
+
filtered[k] = obj[k];
|
|
7747
|
+
}
|
|
7455
7748
|
return util2.objectValues(filtered);
|
|
7456
7749
|
};
|
|
7457
7750
|
util2.objectValues = (obj) => {
|
|
@@ -11421,7 +11714,7 @@ var seal_mcp_v1_default = {
|
|
|
11421
11714
|
required: ["handle", "receipt", "handoff_channel"],
|
|
11422
11715
|
properties: {
|
|
11423
11716
|
handle: { type: "string", pattern: "^h_[A-Za-z0-9_-]{12,}$", description: "Opaque server-internal ID. Used by `seal_revoke` and other tools. 12+ base64url chars after the `h_` prefix \u2014 the alphabet includes `-` and `_` (generateHandle emits base64url), so the pattern must too." },
|
|
11424
|
-
receipt: { type: "string", description: "Plain-text status the model should relay verbatim to the user. e.g. 'The seal link is on your clipboard. Paste it to the recipient.' In ephemeral mode (see `
|
|
11717
|
+
receipt: { type: "string", description: "Plain-text status the model should relay verbatim to the user. e.g. 'The seal link is on your clipboard. Paste it to the recipient.' In ephemeral mode (see `root_secret.ephemeral_mode`) it adds that the handle lives until the server restarts and the seal's expiry does not depend on it." },
|
|
11425
11718
|
handoff_channel: { type: "string", enum: ["clipboard", "file"] },
|
|
11426
11719
|
handoff_file: { type: "string", description: "ONLY when handoff_channel='file'. Absolute path to a 0o600 file containing the share link and a newline." },
|
|
11427
11720
|
expires_at: { type: "string", format: "date-time", description: "ISO 8601 UTC expiry timestamp." }
|
|
@@ -11757,7 +12050,7 @@ var seal_mcp_v1_default = {
|
|
|
11757
12050
|
type: "object",
|
|
11758
12051
|
required: ["x25519_secret_b64", "x25519_public_b64"],
|
|
11759
12052
|
properties: {
|
|
11760
|
-
x25519_secret_b64: { type: "string", description: "32 raw bytes,
|
|
12053
|
+
x25519_secret_b64: { type: "string", description: "32 raw bytes, base64url. The seed of `root_secret`: must equal it, else the server refuses with `identity_conflict`. Lost seed = lost address = incoming targeted seals unopenable; handoff continues to work without identity." },
|
|
11761
12054
|
x25519_public_b64: { type: "string", description: "32 raw bytes, derived from secret. Safe to publish; advertised so other parties can target this MCP." }
|
|
11762
12055
|
}
|
|
11763
12056
|
},
|
|
@@ -11778,7 +12071,7 @@ var seal_mcp_v1_default = {
|
|
|
11778
12071
|
},
|
|
11779
12072
|
pending: {
|
|
11780
12073
|
type: "object",
|
|
11781
|
-
description: "Durable operation journal (ISSUE-0623), keyed by handle. A seal_share journals BEFORE its first remote effect and checkpoints every step; completion removes the entry in the same envelope write that inserts the durable handle. OPTIONAL: envelopes written before the journal existed omit it and readers normalise to {}. Recovery semantics at server start: 'preparing' (no confirmed remote effect) is dropped; 'uploaded'/'awaiting_payment'/'compensating' (URL never delivered) is revoked, AWAITED \u2014 an unpaid seal cannot be finished after a restart, and a payment that lands after the revoke is refunded by the server; 'delivering' (URL possibly already delivered) is ADOPTED as a durable handle \u2014 never revoked, so the owner keeps control of a link the user may already hold.",
|
|
12074
|
+
description: "Durable operation journal (ISSUE-0623), keyed by handle. A seal_share journals BEFORE its first remote effect and checkpoints every step; completion removes the entry in the same envelope write that inserts the durable handle. OPTIONAL: envelopes written before the journal existed omit it and readers normalise to {}. Recovery semantics at server start, for entries whose `owner` cannot still be running (several MCP hosts share one state, SPEC-ADDRESS F1 \xA74): 'preparing' (no confirmed remote effect) is dropped; 'uploaded'/'awaiting_payment'/'compensating' (URL never delivered) is revoked, AWAITED \u2014 an unpaid seal cannot be finished after a restart, and a payment that lands after the revoke is refunded by the server; 'delivering' (URL possibly already delivered) is ADOPTED as a durable handle \u2014 never revoked, so the owner keeps control of a link the user may already hold.",
|
|
11782
12075
|
additionalProperties: {
|
|
11783
12076
|
type: "object",
|
|
11784
12077
|
required: ["tool", "mode", "started_at", "owner_token_b64", "expires_at", "step"],
|
|
@@ -11791,6 +12084,12 @@ var seal_mcp_v1_default = {
|
|
|
11791
12084
|
step: { type: "string", enum: ["preparing", "awaiting_payment", "uploaded", "delivering", "compensating"] },
|
|
11792
12085
|
seal_id: { type: "string", pattern: "^[A-Za-z0-9_-]{6,20}$", description: "Set once the upload confirms; (seal_id, owner_token_b64) is the full revoke capability." },
|
|
11793
12086
|
handoff_channels_attempted: { type: "array", items: { type: "string", enum: ["clipboard", "file"] }, description: "Channels performHandoff will try, in order (handoff mode only; journaled before delivery starts)." },
|
|
12087
|
+
owner: {
|
|
12088
|
+
type: "object",
|
|
12089
|
+
required: ["pid", "hostname"],
|
|
12090
|
+
properties: { pid: { type: "integer" }, hostname: { type: "string" } },
|
|
12091
|
+
description: "The process running the operation. Recovery leaves an entry alone while that process may still run: alive on this host, or on another host, which cannot be probed (the state lock's rule). Entries without it, from 0.2.x, are recovered as crashed."
|
|
12092
|
+
},
|
|
11794
12093
|
compensation: {
|
|
11795
12094
|
type: "object",
|
|
11796
12095
|
required: ["action", "outcome", "error", "at"],
|
|
@@ -11820,29 +12119,22 @@ var seal_mcp_v1_default = {
|
|
|
11820
12119
|
}
|
|
11821
12120
|
}
|
|
11822
12121
|
}
|
|
11823
|
-
}
|
|
11824
|
-
},
|
|
11825
|
-
|
|
11826
|
-
|
|
11827
|
-
|
|
11828
|
-
|
|
11829
|
-
|
|
11830
|
-
|
|
11831
|
-
default: true
|
|
11832
|
-
},
|
|
11833
|
-
{
|
|
11834
|
-
rank: 2,
|
|
11835
|
-
method: "env_var",
|
|
11836
|
-
implementation: "$SEAL_MCP_PASSPHRASE. Visible to same-user attackers via /proc/<pid>/environ on Linux. Warning logged on stderr at startup. Use only in CI / headless containers."
|
|
11837
|
-
},
|
|
11838
|
-
{
|
|
11839
|
-
rank: 3,
|
|
11840
|
-
method: "REFUSED",
|
|
11841
|
-
implementation: "Cleartext passphrase file on disk is explicitly refused even if user passes `--passphrase-file \u2026`. Error message points at options 1 and 2."
|
|
11842
|
-
}
|
|
12122
|
+
}
|
|
12123
|
+
},
|
|
12124
|
+
root_secret: {
|
|
12125
|
+
seed: "The agent's one root secret (SPEC-ADDRESS F1): a 32-byte X25519 secret, as text 43 base64url characters \u2014 the same bytes as `state.identity.x25519_secret_b64`, the CLI's `identity.v1.json::x25519_secret_b64url`, the SDK's `KeyPair.secretKey` and sealnet's `keygen().secret_key`. Its public half is the agent's address; it must survive a restart, a second MCP host on the machine and a move to the cloud.",
|
|
12126
|
+
ordered_sources: [
|
|
12127
|
+
{ rank: 1, source: "env", implementation: "$SEAL_SEED. How a container or a cloud session gets the same address as at home." },
|
|
12128
|
+
{ rank: 2, source: "keychain", implementation: "@napi-rs/keyring (the Secret Service on Linux, never the kernel keyring, which forgets on reboot; macOS Keychain; Windows Credential Manager), an OPTIONAL dependency loaded lazily: its absence never fails install or start. Service 'seal-mcp', account 'seed', the seed as UTF-8 bytes." },
|
|
12129
|
+
{ rank: 3, source: "file", implementation: "`<state_dir>/seed`, mode 0600 in the 0700 state directory, created on the first start where there is no keychain. The CLI's threat model (`secrets.rs`): a process running as this user reads it either way, keychain or file." }
|
|
11843
12130
|
],
|
|
11844
|
-
|
|
11845
|
-
|
|
12131
|
+
created: "The first `serve` (or `init`) with no seed in any source makes one and keeps it in the keychain, or in the file where there is none. `init` asks nothing.",
|
|
12132
|
+
state_passphrase: "base64url(HKDF-SHA256(ikm = seed, salt = 'seal-mcp:state:v1', info = 'passphrase', 32 bytes)). The state envelope (Argon2id \u2192 AES-256-GCM) is unchanged; only where its passphrase comes from changed. One root secret instead of a passphrase and a key.",
|
|
12133
|
+
identity_conflict: "`state.identity` must be the seed. A state that opens neither with the seed's passphrase nor with a migration passphrase belongs to another key (a foreign SEAL_SEED): `serve` refuses with `identity_conflict`, exit 3. Unset SEAL_SEED, or `sealnet-mcp reset --force`. The address never changes silently.",
|
|
12134
|
+
migration_0_2: "Once, when no seed exists in any source and a state does: open it with the old passphrase (keychain entry 'seal-mcp'/'state', including the Linux entry keytar wrote, or $SEAL_MCP_PASSPHRASE), take the seed from `state.identity`, store it (keychain 'seed', else the file), save the same state under the seed's passphrase (atomic, as every save), delete the old keychain entry. $SEAL_MCP_PASSPHRASE is read only here and warns that it is no longer used. Removed in the first minor release after no 0.2.x state is left, the condition `readKeytarLinuxEntry` carries.",
|
|
12135
|
+
ephemeral_mode: "Only `serve --ephemeral` or $SEAL_MCP_EPHEMERAL=1: a new key each start; identity, handles, journal and audit live only in process memory, nothing is written to disk. The `seal_share` receipt says the handle lives until the server restarts and the seal's expiry does not depend on it.",
|
|
12136
|
+
several_hosts: "Every MCP host of one user on one machine (Claude Desktop, Claude Code, Cursor) runs its own `serve` on the same state: one seed, one address. The state lock is taken per write, not for a process lifetime: each change re-reads `state.json`, applies this process's insertions and explicit deletions and saves. Waiting for the lock ends after 5 s with `state_busy`.",
|
|
12137
|
+
lost_seed: "The seed is the address: lost (`reset --force`, a wiped keychain with no file) means links addressed to the old key stay unopenable and paired owners must pair again; handoff seals are unaffected. `sealnet-mcp seed --force` prints it, to move the agent by setting SEAL_SEED elsewhere."
|
|
11846
12138
|
},
|
|
11847
12139
|
handoff_delivery: {
|
|
11848
12140
|
primary_channel: "system_clipboard",
|
|
@@ -11918,11 +12210,12 @@ var seal_mcp_v1_default = {
|
|
|
11918
12210
|
pro_file_not_granted: "seal_pro_file_get: the room's seal is visible but a file's key envelope is refused to this agent.",
|
|
11919
12211
|
pro_failed: "seal_pro_*: SEAL Pro answered with another error; the message has the status.",
|
|
11920
12212
|
state_persist_failed: "seal_share: the journal checkpoint (`uploaded` or `delivering`) could not be made durable. The fresh seal is auto-revoked (compensation outcome embedded in the message) because delivering a capability the journal cannot account for is worse than failing the call.",
|
|
12213
|
+
state_busy: "Another sealnet-mcp process held the state lock for more than 5 s. Nothing was changed by this call; safe to retry.",
|
|
11921
12214
|
runtime_unsupported: "Node runtime is missing a global the bundle assumes (e.g. `globalThis.File` or `globalThis.Blob`). Upgrade to Node \u226520.",
|
|
11922
12215
|
internal_inconsistency: "Server detected a state branch the type-system thought was unreachable (e.g. forward gate passed but recipientPub is null). Always a bug \u2014 caller should report.",
|
|
11923
|
-
|
|
12216
|
+
identity_conflict: "Startup: the state does not open with this seed's passphrase nor with a migration passphrase \u2014 it belongs to another key (a foreign SEAL_SEED), or it was tampered with. Server refuses to start (exit 3); see `root_secret.identity_conflict`.",
|
|
11924
12217
|
"state.corrupt": "Envelope JSON malformed or version unknown. Server refuses to start; user must rerun `sealnet-mcp init --force` (lose old data) or restore from backup.",
|
|
11925
|
-
state_dir_unsafe: "
|
|
12218
|
+
state_dir_unsafe: "The state directory is a symlink or not a real directory \u2014 the state lock and the envelope it protects would resolve to different places. Refused.",
|
|
11926
12219
|
base_url_invalid: "Startup/config: backend or public-host base URL is not an absolute http(s) URL, or carries a query string / fragment / embedded credentials. Every derived URL would be corrupt, so the value is rejected at configuration time."
|
|
11927
12220
|
},
|
|
11928
12221
|
current_format_version: 1,
|
|
@@ -11949,7 +12242,7 @@ function safeFilename(name) {
|
|
|
11949
12242
|
return cleaned.length > 0 ? cleaned : "seal-file";
|
|
11950
12243
|
}
|
|
11951
12244
|
async function receiveToDisk(opened, dir, name) {
|
|
11952
|
-
const
|
|
12245
|
+
const fs4 = await import(
|
|
11953
12246
|
/* webpackIgnore: true */
|
|
11954
12247
|
/* @vite-ignore */
|
|
11955
12248
|
"fs/promises"
|
|
@@ -11959,10 +12252,10 @@ async function receiveToDisk(opened, dir, name) {
|
|
|
11959
12252
|
/* @vite-ignore */
|
|
11960
12253
|
"path"
|
|
11961
12254
|
);
|
|
11962
|
-
await
|
|
12255
|
+
await fs4.mkdir(dir, { recursive: true });
|
|
11963
12256
|
const suffix = Array.from(crypto.getRandomValues(new Uint8Array(6)), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
11964
12257
|
const tmp = path.join(dir, `.seal-partial-${suffix}`);
|
|
11965
|
-
const fh = await
|
|
12258
|
+
const fh = await fs4.open(tmp, "wx", 384);
|
|
11966
12259
|
let placed = false;
|
|
11967
12260
|
try {
|
|
11968
12261
|
await opened.stream.pipeTo(
|
|
@@ -11980,12 +12273,12 @@ async function receiveToDisk(opened, dir, name) {
|
|
|
11980
12273
|
for (let i = 0; i < 1e3; i++) {
|
|
11981
12274
|
const candidate = path.join(dir, i === 0 ? name : `${stem} (${i})${ext}`);
|
|
11982
12275
|
try {
|
|
11983
|
-
await
|
|
12276
|
+
await fs4.link(tmp, candidate);
|
|
11984
12277
|
} catch (err2) {
|
|
11985
12278
|
if (err2.code === "EEXIST") continue;
|
|
11986
12279
|
throw err2;
|
|
11987
12280
|
}
|
|
11988
|
-
await
|
|
12281
|
+
await fs4.unlink(tmp);
|
|
11989
12282
|
placed = true;
|
|
11990
12283
|
return { path: candidate, sizeBytes, sha256: sha2562 };
|
|
11991
12284
|
}
|
|
@@ -11993,7 +12286,7 @@ async function receiveToDisk(opened, dir, name) {
|
|
|
11993
12286
|
} finally {
|
|
11994
12287
|
if (!placed) {
|
|
11995
12288
|
await fh.close().catch(() => void 0);
|
|
11996
|
-
await
|
|
12289
|
+
await fs4.unlink(tmp).catch(() => void 0);
|
|
11997
12290
|
}
|
|
11998
12291
|
}
|
|
11999
12292
|
}
|
|
@@ -12353,320 +12646,102 @@ async function loadWorkloadCredentials(env = process.env) {
|
|
|
12353
12646
|
const path = workloadCredentialsPath(env);
|
|
12354
12647
|
let text;
|
|
12355
12648
|
try {
|
|
12356
|
-
text = await (0, import_promises2.readFile)(path, "utf-8");
|
|
12357
|
-
} catch (err2) {
|
|
12358
|
-
if (err2.code === "ENOENT") return null;
|
|
12359
|
-
throw err2;
|
|
12360
|
-
}
|
|
12361
|
-
const file = JSON.parse(text);
|
|
12362
|
-
if (file.version !== 1 || !file.workload_id || !file.base_url || !file.seed_b64) {
|
|
12363
|
-
throw new Error(`${path} is not a SEAL Pro workload credentials file`);
|
|
12364
|
-
}
|
|
12365
|
-
return { workloadId: file.workload_id, baseUrl: file.base_url, seedB64: file.seed_b64 };
|
|
12366
|
-
}
|
|
12367
|
-
|
|
12368
|
-
// src/recovery.ts
|
|
12369
|
-
function nowIso(now) {
|
|
12370
|
-
return (now?.() ?? /* @__PURE__ */ new Date()).toISOString();
|
|
12371
|
-
}
|
|
12372
|
-
async function recoverPendingOperations(view, client, opts = {}) {
|
|
12373
|
-
const log4 = opts.log ?? ((line) => void process.stderr.write(`${line}
|
|
12374
|
-
`));
|
|
12375
|
-
const report = { adopted: [], revoked: [], stillFailing: [], dropped: [] };
|
|
12376
|
-
let changed = false;
|
|
12377
|
-
for (const [handle, op] of Object.entries(view.pending)) {
|
|
12378
|
-
if (op.tool !== "seal_share") {
|
|
12379
|
-
log4(`[seal-mcp] recovery: skipping unknown pending op tool=${String(op.tool)} handle=${handle}`);
|
|
12380
|
-
continue;
|
|
12381
|
-
}
|
|
12382
|
-
if (op.seal_id === void 0) {
|
|
12383
|
-
delete view.pending[handle];
|
|
12384
|
-
report.dropped.push(handle);
|
|
12385
|
-
changed = true;
|
|
12386
|
-
log4(`[seal-mcp] recovery: dropped pre-upload journal entry ${handle} (no remote effect)`);
|
|
12387
|
-
continue;
|
|
12388
|
-
}
|
|
12389
|
-
if (op.step === "delivering") {
|
|
12390
|
-
adoptAsHandle(view, handle, op, opts.now);
|
|
12391
|
-
report.adopted.push(handle);
|
|
12392
|
-
changed = true;
|
|
12393
|
-
log4(
|
|
12394
|
-
`[seal-mcp] recovery: adopted ${handle} as a durable handle \u2014 handoff was in flight (attempted: ${(op.handoff_channels_attempted ?? []).join("\u2192") || "unknown"}); the URL may have been delivered, so the seal stays live under owner control`
|
|
12395
|
-
);
|
|
12396
|
-
continue;
|
|
12397
|
-
}
|
|
12398
|
-
if (op.backend_url !== void 0 && op.backend_url !== client.baseUrl) {
|
|
12399
|
-
op.step = "compensating";
|
|
12400
|
-
op.compensation = {
|
|
12401
|
-
action: "revoke",
|
|
12402
|
-
outcome: "failed",
|
|
12403
|
-
error: `backend mismatch: op belongs to ${op.backend_url}, server configured for ${client.baseUrl}`,
|
|
12404
|
-
at: nowIso(opts.now)
|
|
12405
|
-
};
|
|
12406
|
-
report.stillFailing.push(handle);
|
|
12407
|
-
changed = true;
|
|
12408
|
-
log4(
|
|
12409
|
-
`[seal-mcp] recovery: NOT compensating ${handle} \u2014 it belongs to ${op.backend_url} but this server talks to ${client.baseUrl}; kept in journal until the server runs against the original backend`
|
|
12410
|
-
);
|
|
12411
|
-
continue;
|
|
12412
|
-
}
|
|
12413
|
-
try {
|
|
12414
|
-
await client.revoke(op.seal_id, op.owner_token_b64);
|
|
12415
|
-
delete view.pending[handle];
|
|
12416
|
-
report.revoked.push(handle);
|
|
12417
|
-
changed = true;
|
|
12418
|
-
log4(`[seal-mcp] recovery: revoked orphaned seal for ${handle} (step was '${op.step}')`);
|
|
12419
|
-
} catch (err2) {
|
|
12420
|
-
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
12421
|
-
op.step = "compensating";
|
|
12422
|
-
op.compensation = { action: "revoke", outcome: "failed", error: msg, at: nowIso(opts.now) };
|
|
12423
|
-
report.stillFailing.push(handle);
|
|
12424
|
-
changed = true;
|
|
12425
|
-
log4(
|
|
12426
|
-
`[seal-mcp] recovery: revoke retry for ${handle} failed (${msg}) \u2014 kept in journal, seal expires by TTL at ${op.expires_at}`
|
|
12427
|
-
);
|
|
12428
|
-
}
|
|
12429
|
-
}
|
|
12430
|
-
return { report, changed };
|
|
12431
|
-
}
|
|
12432
|
-
function adoptAsHandle(view, handle, op, now) {
|
|
12433
|
-
const sealId = op.seal_id;
|
|
12434
|
-
if (view.handles[handle] === void 0) {
|
|
12435
|
-
view.handles[handle] = {
|
|
12436
|
-
seal_id: sealId,
|
|
12437
|
-
owner_token_b64: op.owner_token_b64,
|
|
12438
|
-
created_at: op.started_at,
|
|
12439
|
-
mode: op.mode,
|
|
12440
|
-
expires_at: op.expires_at,
|
|
12441
|
-
// Propagate provenance so a later seal_revoke on the adopted
|
|
12442
|
-
// handle still refuses to target the wrong backend.
|
|
12443
|
-
backend_url: op.backend_url
|
|
12444
|
-
};
|
|
12445
|
-
view.audit.push({ ts: nowIso(now), tool: "seal_share", handle, mode: op.mode });
|
|
12446
|
-
}
|
|
12447
|
-
delete view.pending[handle];
|
|
12448
|
-
}
|
|
12449
|
-
|
|
12450
|
-
// src/state.ts
|
|
12451
|
-
var import_node_crypto3 = require("crypto");
|
|
12452
|
-
var import_node_fs2 = require("fs");
|
|
12453
|
-
var import_node_path3 = require("path");
|
|
12454
|
-
var import_argon2 = __toESM(require("argon2"), 1);
|
|
12455
|
-
var FORMAT_TAG = "SEAL_MCP_STATE v1";
|
|
12456
|
-
var STATE_VERSION = 1;
|
|
12457
|
-
var ARGON2_MEMORY_KIB = 65536;
|
|
12458
|
-
var ARGON2_ITERATIONS = 3;
|
|
12459
|
-
var ARGON2_PARALLELISM = 4;
|
|
12460
|
-
var ARGON2_SALT_BYTES = 16;
|
|
12461
|
-
var KDF_BOUNDS = {
|
|
12462
|
-
memory_kib: { min: 8192, max: 1048576 },
|
|
12463
|
-
// 8 MiB .. 1 GiB
|
|
12464
|
-
iterations: { min: 1, max: 16 },
|
|
12465
|
-
parallelism: { min: 1, max: 16 }
|
|
12466
|
-
};
|
|
12467
|
-
var AES_KEY_BYTES = 32;
|
|
12468
|
-
var AES_NONCE_BYTES = 12;
|
|
12469
|
-
var StatePassphraseInvalid = class extends Error {
|
|
12470
|
-
constructor() {
|
|
12471
|
-
super("state.passphrase_invalid: AES-256-GCM tag check failed");
|
|
12472
|
-
this.name = "StatePassphraseInvalid";
|
|
12473
|
-
}
|
|
12474
|
-
};
|
|
12475
|
-
var StateCorrupt = class extends Error {
|
|
12476
|
-
constructor(reason) {
|
|
12477
|
-
super(`state.corrupt: ${reason}`);
|
|
12478
|
-
this.name = "StateCorrupt";
|
|
12479
|
-
}
|
|
12480
|
-
};
|
|
12481
|
-
function toB642(bytes) {
|
|
12482
|
-
return Buffer.from(bytes).toString("base64");
|
|
12483
|
-
}
|
|
12484
|
-
function fromB642(s) {
|
|
12485
|
-
return new Uint8Array(Buffer.from(s, "base64"));
|
|
12486
|
-
}
|
|
12487
|
-
var CURRENT_KDF_PARAMS = {
|
|
12488
|
-
memory_kib: ARGON2_MEMORY_KIB,
|
|
12489
|
-
iterations: ARGON2_ITERATIONS,
|
|
12490
|
-
parallelism: ARGON2_PARALLELISM
|
|
12491
|
-
};
|
|
12492
|
-
function boundedKdfParam(name, value) {
|
|
12493
|
-
if (typeof value !== "number" || !Number.isInteger(value)) {
|
|
12494
|
-
throw new StateCorrupt(`kdf.${name} is not an integer: ${String(value)}`);
|
|
12495
|
-
}
|
|
12496
|
-
const { min, max } = KDF_BOUNDS[name];
|
|
12497
|
-
if (value < min || value > max) {
|
|
12498
|
-
throw new StateCorrupt(`kdf.${name}=${value} outside sane bounds [${min}, ${max}]`);
|
|
12499
|
-
}
|
|
12500
|
-
return value;
|
|
12501
|
-
}
|
|
12502
|
-
async function deriveKek(passphrase, saltB64, params) {
|
|
12503
|
-
const salt = fromB642(saltB64);
|
|
12504
|
-
if (salt.length !== ARGON2_SALT_BYTES) {
|
|
12505
|
-
throw new StateCorrupt(`salt length is ${salt.length}, expected ${ARGON2_SALT_BYTES}`);
|
|
12506
|
-
}
|
|
12507
|
-
const kek = await import_argon2.default.hash(passphrase, {
|
|
12508
|
-
type: import_argon2.default.argon2id,
|
|
12509
|
-
raw: true,
|
|
12510
|
-
salt: Buffer.from(salt),
|
|
12511
|
-
memoryCost: params.memory_kib,
|
|
12512
|
-
timeCost: params.iterations,
|
|
12513
|
-
parallelism: params.parallelism,
|
|
12514
|
-
hashLength: AES_KEY_BYTES
|
|
12515
|
-
});
|
|
12516
|
-
return new Uint8Array(kek);
|
|
12517
|
-
}
|
|
12518
|
-
async function aesGcmEncrypt(kek, nonce, plaintext) {
|
|
12519
|
-
const key = await import_node_crypto3.webcrypto.subtle.importKey("raw", kek, "AES-GCM", false, ["encrypt"]);
|
|
12520
|
-
const ct = await import_node_crypto3.webcrypto.subtle.encrypt(
|
|
12521
|
-
{ name: "AES-GCM", iv: nonce, tagLength: 128 },
|
|
12522
|
-
key,
|
|
12523
|
-
plaintext
|
|
12524
|
-
);
|
|
12525
|
-
return new Uint8Array(ct);
|
|
12526
|
-
}
|
|
12527
|
-
async function aesGcmDecrypt(kek, nonce, ciphertextWithTag) {
|
|
12528
|
-
const key = await import_node_crypto3.webcrypto.subtle.importKey("raw", kek, "AES-GCM", false, ["decrypt"]);
|
|
12529
|
-
try {
|
|
12530
|
-
const pt = await import_node_crypto3.webcrypto.subtle.decrypt(
|
|
12531
|
-
{ name: "AES-GCM", iv: nonce, tagLength: 128 },
|
|
12532
|
-
key,
|
|
12533
|
-
ciphertextWithTag
|
|
12534
|
-
);
|
|
12535
|
-
return new Uint8Array(pt);
|
|
12536
|
-
} catch {
|
|
12537
|
-
throw new StatePassphraseInvalid();
|
|
12538
|
-
}
|
|
12539
|
-
}
|
|
12540
|
-
function createEmptyState(identity) {
|
|
12541
|
-
return {
|
|
12542
|
-
version: STATE_VERSION,
|
|
12543
|
-
identity,
|
|
12544
|
-
handles: {},
|
|
12545
|
-
audit: [],
|
|
12546
|
-
pending: {}
|
|
12547
|
-
};
|
|
12548
|
-
}
|
|
12549
|
-
function validateEnvelope(json) {
|
|
12550
|
-
if (typeof json !== "object" || json === null) {
|
|
12551
|
-
throw new StateCorrupt("envelope is not an object");
|
|
12552
|
-
}
|
|
12553
|
-
const e = json;
|
|
12554
|
-
if (e.version !== STATE_VERSION) {
|
|
12555
|
-
throw new StateCorrupt(`unknown version: ${String(e.version)}`);
|
|
12556
|
-
}
|
|
12557
|
-
if (e.format !== FORMAT_TAG) {
|
|
12558
|
-
throw new StateCorrupt(`unknown format tag: ${String(e.format)}`);
|
|
12559
|
-
}
|
|
12560
|
-
const kdf = e.kdf;
|
|
12561
|
-
if (!kdf || kdf.algo !== "argon2id" || typeof kdf.salt_b64 !== "string") {
|
|
12562
|
-
throw new StateCorrupt("kdf header missing or wrong algo");
|
|
12563
|
-
}
|
|
12564
|
-
const cipher = e.cipher;
|
|
12565
|
-
if (!cipher || cipher.algo !== "aes-256-gcm" || typeof cipher.nonce_b64 !== "string" || typeof cipher.ciphertext_b64 !== "string") {
|
|
12566
|
-
throw new StateCorrupt("cipher header missing or wrong algo");
|
|
12567
|
-
}
|
|
12568
|
-
}
|
|
12569
|
-
function validatePlaintext(json) {
|
|
12570
|
-
if (typeof json !== "object" || json === null) {
|
|
12571
|
-
throw new StateCorrupt("plaintext is not an object");
|
|
12572
|
-
}
|
|
12573
|
-
const s = json;
|
|
12574
|
-
if (s.version !== STATE_VERSION) {
|
|
12575
|
-
throw new StateCorrupt(`plaintext version: ${String(s.version)}`);
|
|
12576
|
-
}
|
|
12577
|
-
const id = s.identity;
|
|
12578
|
-
if (!id || typeof id.x25519_secret_b64 !== "string" || typeof id.x25519_public_b64 !== "string") {
|
|
12579
|
-
throw new StateCorrupt("identity missing or malformed");
|
|
12580
|
-
}
|
|
12581
|
-
if (typeof s.handles !== "object" || s.handles === null) {
|
|
12582
|
-
throw new StateCorrupt("handles is not an object");
|
|
12583
|
-
}
|
|
12584
|
-
if (!Array.isArray(s.audit)) {
|
|
12585
|
-
throw new StateCorrupt("audit is not an array");
|
|
12586
|
-
}
|
|
12587
|
-
if (s.pending !== void 0 && (typeof s.pending !== "object" || s.pending === null)) {
|
|
12588
|
-
throw new StateCorrupt("pending is not an object");
|
|
12589
|
-
}
|
|
12590
|
-
}
|
|
12591
|
-
async function loadState(passphrase, statePath) {
|
|
12592
|
-
const raw = await import_node_fs2.promises.readFile(statePath, "utf8");
|
|
12593
|
-
let envelope;
|
|
12594
|
-
try {
|
|
12595
|
-
envelope = JSON.parse(raw);
|
|
12596
|
-
} catch (err2) {
|
|
12597
|
-
throw new StateCorrupt(`envelope is not valid JSON: ${err2.message}`);
|
|
12598
|
-
}
|
|
12599
|
-
validateEnvelope(envelope);
|
|
12600
|
-
const kek = await deriveKek(passphrase, envelope.kdf.salt_b64, {
|
|
12601
|
-
memory_kib: boundedKdfParam("memory_kib", envelope.kdf.memory_kib),
|
|
12602
|
-
iterations: boundedKdfParam("iterations", envelope.kdf.iterations),
|
|
12603
|
-
parallelism: boundedKdfParam("parallelism", envelope.kdf.parallelism)
|
|
12604
|
-
});
|
|
12605
|
-
const nonce = fromB642(envelope.cipher.nonce_b64);
|
|
12606
|
-
if (nonce.length !== AES_NONCE_BYTES) {
|
|
12607
|
-
throw new StateCorrupt(`nonce length is ${nonce.length}, expected ${AES_NONCE_BYTES}`);
|
|
12608
|
-
}
|
|
12609
|
-
const ct = fromB642(envelope.cipher.ciphertext_b64);
|
|
12610
|
-
const ptBytes = await aesGcmDecrypt(kek, nonce, ct);
|
|
12611
|
-
const ptText = new TextDecoder("utf-8", { fatal: true }).decode(ptBytes);
|
|
12612
|
-
let plaintext;
|
|
12613
|
-
try {
|
|
12614
|
-
plaintext = JSON.parse(ptText);
|
|
12649
|
+
text = await (0, import_promises2.readFile)(path, "utf-8");
|
|
12615
12650
|
} catch (err2) {
|
|
12616
|
-
|
|
12651
|
+
if (err2.code === "ENOENT") return null;
|
|
12652
|
+
throw err2;
|
|
12617
12653
|
}
|
|
12618
|
-
|
|
12619
|
-
if (
|
|
12620
|
-
|
|
12654
|
+
const file = JSON.parse(text);
|
|
12655
|
+
if (file.version !== 1 || !file.workload_id || !file.base_url || !file.seed_b64) {
|
|
12656
|
+
throw new Error(`${path} is not a SEAL Pro workload credentials file`);
|
|
12621
12657
|
}
|
|
12622
|
-
return
|
|
12658
|
+
return { workloadId: file.workload_id, baseUrl: file.base_url, seedB64: file.seed_b64 };
|
|
12623
12659
|
}
|
|
12624
|
-
|
|
12625
|
-
|
|
12626
|
-
|
|
12627
|
-
|
|
12628
|
-
|
|
12629
|
-
|
|
12630
|
-
const
|
|
12631
|
-
|
|
12632
|
-
|
|
12633
|
-
|
|
12634
|
-
|
|
12635
|
-
|
|
12636
|
-
|
|
12637
|
-
|
|
12638
|
-
|
|
12639
|
-
|
|
12640
|
-
|
|
12641
|
-
|
|
12642
|
-
|
|
12643
|
-
|
|
12644
|
-
|
|
12645
|
-
|
|
12660
|
+
|
|
12661
|
+
// src/recovery.ts
|
|
12662
|
+
function nowIso(now) {
|
|
12663
|
+
return (now?.() ?? /* @__PURE__ */ new Date()).toISOString();
|
|
12664
|
+
}
|
|
12665
|
+
async function recoverPendingOperations(view, client, opts = {}) {
|
|
12666
|
+
const log4 = opts.log ?? ((line) => void process.stderr.write(`${line}
|
|
12667
|
+
`));
|
|
12668
|
+
const report = { adopted: [], revoked: [], stillFailing: [], dropped: [] };
|
|
12669
|
+
let changed = false;
|
|
12670
|
+
for (const [handle, op] of Object.entries(view.pending)) {
|
|
12671
|
+
if (op.tool !== "seal_share") {
|
|
12672
|
+
log4(`[seal-mcp] recovery: skipping unknown pending op tool=${String(op.tool)} handle=${handle}`);
|
|
12673
|
+
continue;
|
|
12674
|
+
}
|
|
12675
|
+
if (op.owner !== void 0 && op.owner.pid !== process.pid && holderMayRun(op.owner)) {
|
|
12676
|
+
log4(`[seal-mcp] recovery: left ${handle} alone \u2014 pid ${op.owner.pid} on ${op.owner.hostname} may still run it`);
|
|
12677
|
+
continue;
|
|
12678
|
+
}
|
|
12679
|
+
if (op.seal_id === void 0) {
|
|
12680
|
+
delete view.pending[handle];
|
|
12681
|
+
report.dropped.push(handle);
|
|
12682
|
+
changed = true;
|
|
12683
|
+
log4(`[seal-mcp] recovery: dropped pre-upload journal entry ${handle} (no remote effect)`);
|
|
12684
|
+
continue;
|
|
12685
|
+
}
|
|
12686
|
+
if (op.step === "delivering") {
|
|
12687
|
+
adoptAsHandle(view, handle, op, opts.now);
|
|
12688
|
+
report.adopted.push(handle);
|
|
12689
|
+
changed = true;
|
|
12690
|
+
log4(
|
|
12691
|
+
`[seal-mcp] recovery: adopted ${handle} as a durable handle \u2014 handoff was in flight (attempted: ${(op.handoff_channels_attempted ?? []).join("\u2192") || "unknown"}); the URL may have been delivered, so the seal stays live under owner control`
|
|
12692
|
+
);
|
|
12693
|
+
continue;
|
|
12694
|
+
}
|
|
12695
|
+
if (op.backend_url !== void 0 && op.backend_url !== client.baseUrl) {
|
|
12696
|
+
op.step = "compensating";
|
|
12697
|
+
op.compensation = {
|
|
12698
|
+
action: "revoke",
|
|
12699
|
+
outcome: "failed",
|
|
12700
|
+
error: `backend mismatch: op belongs to ${op.backend_url}, server configured for ${client.baseUrl}`,
|
|
12701
|
+
at: nowIso(opts.now)
|
|
12702
|
+
};
|
|
12703
|
+
report.stillFailing.push(handle);
|
|
12704
|
+
changed = true;
|
|
12705
|
+
log4(
|
|
12706
|
+
`[seal-mcp] recovery: NOT compensating ${handle} \u2014 it belongs to ${op.backend_url} but this server talks to ${client.baseUrl}; kept in journal until the server runs against the original backend`
|
|
12707
|
+
);
|
|
12708
|
+
continue;
|
|
12646
12709
|
}
|
|
12647
|
-
};
|
|
12648
|
-
const dir = (0, import_node_path3.dirname)(statePath);
|
|
12649
|
-
await import_node_fs2.promises.mkdir(dir, { recursive: true, mode: 448 });
|
|
12650
|
-
const tmp = `${statePath}.tmp-${process.pid}-${Date.now()}`;
|
|
12651
|
-
const fh = await import_node_fs2.promises.open(tmp, "w", 384);
|
|
12652
|
-
try {
|
|
12653
|
-
await fh.writeFile(JSON.stringify(envelope), "utf8");
|
|
12654
|
-
await fh.sync();
|
|
12655
|
-
} finally {
|
|
12656
|
-
await fh.close();
|
|
12657
|
-
}
|
|
12658
|
-
await import_node_fs2.promises.rename(tmp, statePath);
|
|
12659
|
-
try {
|
|
12660
|
-
const dh = await import_node_fs2.promises.open(dir, "r");
|
|
12661
12710
|
try {
|
|
12662
|
-
await
|
|
12663
|
-
|
|
12664
|
-
|
|
12711
|
+
await client.revoke(op.seal_id, op.owner_token_b64);
|
|
12712
|
+
delete view.pending[handle];
|
|
12713
|
+
report.revoked.push(handle);
|
|
12714
|
+
changed = true;
|
|
12715
|
+
log4(`[seal-mcp] recovery: revoked orphaned seal for ${handle} (step was '${op.step}')`);
|
|
12716
|
+
} catch (err2) {
|
|
12717
|
+
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
12718
|
+
op.step = "compensating";
|
|
12719
|
+
op.compensation = { action: "revoke", outcome: "failed", error: msg, at: nowIso(opts.now) };
|
|
12720
|
+
report.stillFailing.push(handle);
|
|
12721
|
+
changed = true;
|
|
12722
|
+
log4(
|
|
12723
|
+
`[seal-mcp] recovery: revoke retry for ${handle} failed (${msg}) \u2014 kept in journal, seal expires by TTL at ${op.expires_at}`
|
|
12724
|
+
);
|
|
12665
12725
|
}
|
|
12666
|
-
} catch {
|
|
12667
12726
|
}
|
|
12668
|
-
|
|
12669
|
-
|
|
12727
|
+
return { report, changed };
|
|
12728
|
+
}
|
|
12729
|
+
function adoptAsHandle(view, handle, op, now) {
|
|
12730
|
+
const sealId = op.seal_id;
|
|
12731
|
+
if (view.handles[handle] === void 0) {
|
|
12732
|
+
view.handles[handle] = {
|
|
12733
|
+
seal_id: sealId,
|
|
12734
|
+
owner_token_b64: op.owner_token_b64,
|
|
12735
|
+
created_at: op.started_at,
|
|
12736
|
+
mode: op.mode,
|
|
12737
|
+
expires_at: op.expires_at,
|
|
12738
|
+
// Propagate provenance so a later seal_revoke on the adopted
|
|
12739
|
+
// handle still refuses to target the wrong backend.
|
|
12740
|
+
backend_url: op.backend_url
|
|
12741
|
+
};
|
|
12742
|
+
view.audit.push({ ts: nowIso(now), tool: "seal_share", handle, mode: op.mode });
|
|
12743
|
+
}
|
|
12744
|
+
delete view.pending[handle];
|
|
12670
12745
|
}
|
|
12671
12746
|
|
|
12672
12747
|
// src/tools/context.ts
|
|
@@ -12685,19 +12760,18 @@ var HANDLE_REGEX = /^h_[A-Za-z0-9_-]{12,}$/;
|
|
|
12685
12760
|
function isValidHandle(handle) {
|
|
12686
12761
|
return typeof handle === "string" && HANDLE_REGEX.test(handle);
|
|
12687
12762
|
}
|
|
12763
|
+
function auditEntry(ctx, tool, handle, mode) {
|
|
12764
|
+
return { ts: (ctx.now?.() ?? /* @__PURE__ */ new Date()).toISOString(), tool, handle, mode };
|
|
12765
|
+
}
|
|
12688
12766
|
function appendAudit(ctx, tool, handle, mode) {
|
|
12689
|
-
ctx
|
|
12690
|
-
|
|
12691
|
-
tool,
|
|
12692
|
-
handle,
|
|
12693
|
-
mode
|
|
12694
|
-
});
|
|
12767
|
+
const entry = auditEntry(ctx, tool, handle, mode);
|
|
12768
|
+
return ctx.update((state) => void state.audit.push(entry));
|
|
12695
12769
|
}
|
|
12696
|
-
function insertHandle(
|
|
12697
|
-
if (
|
|
12770
|
+
function insertHandle(state, handle, entry) {
|
|
12771
|
+
if (state.handles[handle]) {
|
|
12698
12772
|
throw new Error(`insertHandle: collision on ${handle}`);
|
|
12699
12773
|
}
|
|
12700
|
-
|
|
12774
|
+
state.handles[handle] = entry;
|
|
12701
12775
|
}
|
|
12702
12776
|
function lookupHandle(ctx, handle) {
|
|
12703
12777
|
const entry = ctx.state.handles[handle];
|
|
@@ -12720,6 +12794,7 @@ var ToolError = class extends Error {
|
|
|
12720
12794
|
// src/tools/list.ts
|
|
12721
12795
|
var SINGLE_FILE_COUNT = 1;
|
|
12722
12796
|
async function sealList(ctx, input = {}) {
|
|
12797
|
+
await appendAudit(ctx, "seal_list", null, null);
|
|
12723
12798
|
const now = (ctx.now?.() ?? /* @__PURE__ */ new Date()).getTime();
|
|
12724
12799
|
const entries = [];
|
|
12725
12800
|
for (const [handle, entry] of Object.entries(ctx.state.handles)) {
|
|
@@ -12736,15 +12811,13 @@ async function sealList(ctx, input = {}) {
|
|
|
12736
12811
|
});
|
|
12737
12812
|
}
|
|
12738
12813
|
entries.sort((a, b) => Date.parse(b.created_at) - Date.parse(a.created_at));
|
|
12739
|
-
appendAudit(ctx, "seal_list", null, null);
|
|
12740
|
-
await ctx.persist();
|
|
12741
12814
|
return { entries };
|
|
12742
12815
|
}
|
|
12743
12816
|
|
|
12744
12817
|
// src/tools/open.ts
|
|
12745
12818
|
var import_promises3 = require("fs/promises");
|
|
12746
12819
|
var import_node_os3 = require("os");
|
|
12747
|
-
var
|
|
12820
|
+
var import_node_path5 = require("path");
|
|
12748
12821
|
var INLINE_ALLOWED_MIMES = ["application/json", "text/csv", "text/plain"];
|
|
12749
12822
|
var INLINE_INJECTION_RISK_MIMES = /* @__PURE__ */ new Set([
|
|
12750
12823
|
"text/html",
|
|
@@ -12822,13 +12895,11 @@ async function sealOpen(ctx, input) {
|
|
|
12822
12895
|
if (mode === "inline") {
|
|
12823
12896
|
const refusal = classifyInlineRefusal(first.mime);
|
|
12824
12897
|
if (refusal !== null) {
|
|
12825
|
-
appendAudit(ctx, "seal_open", handle, mode);
|
|
12826
|
-
await ctx.persist();
|
|
12898
|
+
await appendAudit(ctx, "seal_open", handle, mode);
|
|
12827
12899
|
return { handle, refusal, mime: first.mime, note: refusalNote(refusal, first.mime) };
|
|
12828
12900
|
}
|
|
12829
12901
|
if (first.size > INLINE_CIPHERTEXT_MAX_BYTES) {
|
|
12830
|
-
appendAudit(ctx, "seal_open", handle, mode);
|
|
12831
|
-
await ctx.persist();
|
|
12902
|
+
await appendAudit(ctx, "seal_open", handle, mode);
|
|
12832
12903
|
return {
|
|
12833
12904
|
handle,
|
|
12834
12905
|
refusal: "inline_blocked.too_large",
|
|
@@ -12864,12 +12935,10 @@ async function sealOpen(ctx, input) {
|
|
|
12864
12935
|
err2
|
|
12865
12936
|
);
|
|
12866
12937
|
}
|
|
12867
|
-
appendAudit(ctx, "seal_open", handle, mode);
|
|
12868
|
-
await ctx.persist();
|
|
12938
|
+
await appendAudit(ctx, "seal_open", handle, mode);
|
|
12869
12939
|
return { handle, mime: bareMime(first.mime), sizeBytes: plaintext.byteLength, encoding: "utf-8", content };
|
|
12870
12940
|
}
|
|
12871
|
-
appendAudit(ctx, "seal_open", handle, mode);
|
|
12872
|
-
await ctx.persist();
|
|
12941
|
+
await appendAudit(ctx, "seal_open", handle, mode);
|
|
12873
12942
|
return {
|
|
12874
12943
|
handle,
|
|
12875
12944
|
filename: first.name,
|
|
@@ -12891,7 +12960,7 @@ async function openToDisk(ctx, seal, handle, input) {
|
|
|
12891
12960
|
`the link carries no key for any file in seal ${seal.sealId} (files appended after it was minted); ask the sender for a fresh link`
|
|
12892
12961
|
);
|
|
12893
12962
|
}
|
|
12894
|
-
const dir = (0,
|
|
12963
|
+
const dir = (0, import_node_path5.resolve)(input.dir ?? (0, import_node_path5.join)((0, import_node_os3.homedir)(), "Downloads"));
|
|
12895
12964
|
try {
|
|
12896
12965
|
await (0, import_promises3.mkdir)(dir, { recursive: true });
|
|
12897
12966
|
} catch (err2) {
|
|
@@ -12915,15 +12984,14 @@ async function openToDisk(ctx, seal, handle, input) {
|
|
|
12915
12984
|
throw streamError(err2, file.id);
|
|
12916
12985
|
}
|
|
12917
12986
|
}
|
|
12918
|
-
appendAudit(ctx, "seal_open", handle, "file");
|
|
12919
|
-
await ctx.persist();
|
|
12987
|
+
await appendAudit(ctx, "seal_open", handle, "file");
|
|
12920
12988
|
return { handle, files: out };
|
|
12921
12989
|
}
|
|
12922
12990
|
|
|
12923
12991
|
// src/tools/pro.ts
|
|
12924
12992
|
var import_promises4 = require("fs/promises");
|
|
12925
12993
|
var import_node_os4 = require("os");
|
|
12926
|
-
var
|
|
12994
|
+
var import_node_path6 = require("path");
|
|
12927
12995
|
var SECRET_CODES = {
|
|
12928
12996
|
not_found: "pro_secret_not_found",
|
|
12929
12997
|
not_granted: "pro_secret_not_granted",
|
|
@@ -12948,9 +13016,9 @@ async function asTool(fn, codes = SECRET_CODES) {
|
|
|
12948
13016
|
}
|
|
12949
13017
|
async function sealProSecretUse(source, input) {
|
|
12950
13018
|
const got = await asTool(() => source.getSecret(input.secret, input.version));
|
|
12951
|
-
const dir = await (0, import_promises4.mkdtemp)((0,
|
|
13019
|
+
const dir = await (0, import_promises4.mkdtemp)((0, import_node_path6.join)((0, import_node_os4.tmpdir)(), "seal-mcp-pro-"));
|
|
12952
13020
|
const name = (got.path?.split("/").pop() || got.id).replace(/[^A-Za-z0-9._-]/g, "_");
|
|
12953
|
-
const path = (0,
|
|
13021
|
+
const path = (0, import_node_path6.join)(dir, name);
|
|
12954
13022
|
try {
|
|
12955
13023
|
await (0, import_promises4.writeFile)(path, got.value, { mode: 384, flag: "wx" });
|
|
12956
13024
|
return { path, sizeBytes: got.value.length, secretId: got.id, version: got.version };
|
|
@@ -12964,7 +13032,7 @@ async function sealProSecretRequest(source, input) {
|
|
|
12964
13032
|
return asTool(() => source.waitForAccess(asked));
|
|
12965
13033
|
}
|
|
12966
13034
|
async function sealProFileGet(source, input) {
|
|
12967
|
-
const dir = (0,
|
|
13035
|
+
const dir = (0, import_node_path6.resolve)(input.dir ?? (0, import_node_path6.join)((0, import_node_os4.homedir)(), "Downloads"));
|
|
12968
13036
|
const ids = input.file ? [input.file] : (await asTool(() => source.listFiles(input.seal), FILE_CODES)).map((f) => f.id);
|
|
12969
13037
|
const files = [];
|
|
12970
13038
|
for (const id of ids) {
|
|
@@ -12976,16 +13044,16 @@ async function sealProFileGet(source, input) {
|
|
|
12976
13044
|
|
|
12977
13045
|
// src/tools/request.ts
|
|
12978
13046
|
var import_node_child_process2 = require("child_process");
|
|
12979
|
-
var
|
|
13047
|
+
var import_node_fs4 = require("fs");
|
|
12980
13048
|
var import_promises5 = require("fs/promises");
|
|
12981
13049
|
var import_node_os5 = require("os");
|
|
12982
|
-
var
|
|
13050
|
+
var import_node_path7 = require("path");
|
|
12983
13051
|
function findSealCli() {
|
|
12984
13052
|
const names = process.platform === "win32" ? ["seal.exe", "seal.cmd"] : ["seal"];
|
|
12985
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
13053
|
+
for (const dir of (process.env.PATH ?? "").split(import_node_path7.delimiter)) {
|
|
12986
13054
|
for (const name of names) {
|
|
12987
|
-
const candidate = (0,
|
|
12988
|
-
if (dir && (0,
|
|
13055
|
+
const candidate = (0, import_node_path7.join)(dir, name);
|
|
13056
|
+
if (dir && (0, import_node_fs4.existsSync)(candidate)) return candidate;
|
|
12989
13057
|
}
|
|
12990
13058
|
}
|
|
12991
13059
|
return null;
|
|
@@ -13019,8 +13087,7 @@ async function sealRequest(ctx, input, deps) {
|
|
|
13019
13087
|
if (input.whereUrl) cliArgs.push("--where", input.whereUrl);
|
|
13020
13088
|
const local = await (deps.runCli ?? runSealCli)(cliArgs);
|
|
13021
13089
|
if (local && "files" in local) {
|
|
13022
|
-
appendAudit(ctx, "seal_request", handle, "local");
|
|
13023
|
-
await ctx.persist();
|
|
13090
|
+
await appendAudit(ctx, "seal_request", handle, "local");
|
|
13024
13091
|
return {
|
|
13025
13092
|
handle,
|
|
13026
13093
|
source: "local",
|
|
@@ -13060,8 +13127,7 @@ async function sealRequest(ctx, input, deps) {
|
|
|
13060
13127
|
}
|
|
13061
13128
|
return receiveThroughLink(ctx, handle, intake.id, deps, input.onProgress);
|
|
13062
13129
|
}
|
|
13063
|
-
appendAudit(ctx, "seal_request", handle, "link");
|
|
13064
|
-
await ctx.persist();
|
|
13130
|
+
await appendAudit(ctx, "seal_request", handle, "link");
|
|
13065
13131
|
return { handle, url, emoji, expiresAt: intake.expires_at };
|
|
13066
13132
|
}
|
|
13067
13133
|
async function receiveThroughLink(ctx, handle, intakeId, deps, onProgress) {
|
|
@@ -13078,12 +13144,11 @@ async function receiveThroughLink(ctx, handle, intakeId, deps, onProgress) {
|
|
|
13078
13144
|
);
|
|
13079
13145
|
}
|
|
13080
13146
|
await deps.elicitationDone?.(intakeId).catch(() => void 0);
|
|
13081
|
-
const dir = await (0, import_promises5.mkdtemp)((0,
|
|
13147
|
+
const dir = await (0, import_promises5.mkdtemp)((0, import_node_path7.join)((0, import_node_os5.tmpdir)(), "seal-mcp-request-"));
|
|
13082
13148
|
const opened = await sealOpen(ctx, { url: `${deps.publicHost}${sharePath}`, mode: "file", dir, onProgress });
|
|
13083
13149
|
if (!("files" in opened)) throw new ToolError("internal_inconsistency", "seal_open mode=file returned no files");
|
|
13084
13150
|
ctx.requests?.delete(handle);
|
|
13085
|
-
appendAudit(ctx, "seal_request", handle, "link");
|
|
13086
|
-
await ctx.persist();
|
|
13151
|
+
await appendAudit(ctx, "seal_request", handle, "link");
|
|
13087
13152
|
return {
|
|
13088
13153
|
handle,
|
|
13089
13154
|
source: "link",
|
|
@@ -13096,6 +13161,7 @@ async function sealRevoke(ctx, input) {
|
|
|
13096
13161
|
if (!isValidHandle(input.handle)) {
|
|
13097
13162
|
throw new ToolError("handle_invalid", `handle does not match contract pattern: ${input.handle}`);
|
|
13098
13163
|
}
|
|
13164
|
+
await ctx.update(() => false);
|
|
13099
13165
|
const entry = lookupHandle(ctx, input.handle);
|
|
13100
13166
|
const now = (ctx.now?.() ?? /* @__PURE__ */ new Date()).toISOString();
|
|
13101
13167
|
if (entry.revoked_at) {
|
|
@@ -13130,16 +13196,18 @@ async function sealRevoke(ctx, input) {
|
|
|
13130
13196
|
err2
|
|
13131
13197
|
);
|
|
13132
13198
|
}
|
|
13133
|
-
|
|
13134
|
-
|
|
13135
|
-
|
|
13199
|
+
await ctx.update((state) => {
|
|
13200
|
+
state.handles[input.handle] = { ...entry, revoked_at: now };
|
|
13201
|
+
state.audit.push(auditEntry(ctx, "seal_revoke", input.handle, entry.mode));
|
|
13202
|
+
});
|
|
13136
13203
|
return { revoked: true, revokedAt: now };
|
|
13137
13204
|
}
|
|
13138
13205
|
|
|
13139
13206
|
// src/tools/share.ts
|
|
13140
|
-
var
|
|
13207
|
+
var import_node_fs6 = require("fs");
|
|
13141
13208
|
var import_promises9 = require("fs/promises");
|
|
13142
|
-
var
|
|
13209
|
+
var import_node_os8 = require("os");
|
|
13210
|
+
var import_node_path10 = require("path");
|
|
13143
13211
|
|
|
13144
13212
|
// ../../contracts/limits.json
|
|
13145
13213
|
var limits_default2 = {
|
|
@@ -13173,7 +13241,7 @@ var limits_default2 = {
|
|
|
13173
13241
|
// src/handoff.ts
|
|
13174
13242
|
var import_promises6 = require("fs/promises");
|
|
13175
13243
|
var import_node_os6 = require("os");
|
|
13176
|
-
var
|
|
13244
|
+
var import_node_path8 = require("path");
|
|
13177
13245
|
var ONE_HOUR_MS = 36e5;
|
|
13178
13246
|
var HandoffBothChannelsFailedError = class extends Error {
|
|
13179
13247
|
code = "handoff_both_channels_failed";
|
|
@@ -13189,10 +13257,10 @@ var HandoffBothChannelsFailedError = class extends Error {
|
|
|
13189
13257
|
function handoffDir(deps) {
|
|
13190
13258
|
const xdg = process.env.XDG_RUNTIME_DIR;
|
|
13191
13259
|
const base = deps.tmpdirOverride ?? (xdg && xdg.length > 0 ? xdg : (0, import_node_os6.tmpdir)());
|
|
13192
|
-
return (0,
|
|
13260
|
+
return (0, import_node_path8.join)(base, "seal-mcp");
|
|
13193
13261
|
}
|
|
13194
13262
|
function handoffPath(deps, handle) {
|
|
13195
|
-
return (0,
|
|
13263
|
+
return (0, import_node_path8.join)(handoffDir(deps), `handoff-${handle}.txt`);
|
|
13196
13264
|
}
|
|
13197
13265
|
async function writeClipboard(text) {
|
|
13198
13266
|
const mod = await import("clipboardy");
|
|
@@ -13259,10 +13327,10 @@ async function performHandoff(payload, deps = {}) {
|
|
|
13259
13327
|
}
|
|
13260
13328
|
|
|
13261
13329
|
// src/zip.ts
|
|
13262
|
-
var
|
|
13330
|
+
var import_node_fs5 = require("fs");
|
|
13263
13331
|
var import_promises7 = require("fs/promises");
|
|
13264
13332
|
var import_node_os7 = require("os");
|
|
13265
|
-
var
|
|
13333
|
+
var import_node_path9 = require("path");
|
|
13266
13334
|
var import_node_stream = require("stream");
|
|
13267
13335
|
var import_promises8 = require("stream/promises");
|
|
13268
13336
|
var ZIP32_MAX_BYTES = 4294967295;
|
|
@@ -13270,16 +13338,16 @@ async function collectFiles(dir) {
|
|
|
13270
13338
|
const found = [];
|
|
13271
13339
|
const walk = async (current) => {
|
|
13272
13340
|
for (const dirent of await (0, import_promises7.readdir)(current, { withFileTypes: true })) {
|
|
13273
|
-
const abs = (0,
|
|
13341
|
+
const abs = (0, import_node_path9.join)(current, dirent.name);
|
|
13274
13342
|
if (dirent.isDirectory()) await walk(abs);
|
|
13275
|
-
else if (dirent.isFile()) found.push({ rel: (0,
|
|
13343
|
+
else if (dirent.isFile()) found.push({ rel: (0, import_node_path9.relative)(dir, abs).split(import_node_path9.sep).join("/"), abs });
|
|
13276
13344
|
}
|
|
13277
13345
|
};
|
|
13278
13346
|
await walk(dir);
|
|
13279
13347
|
found.sort((a, b) => a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0);
|
|
13280
13348
|
return Promise.all(
|
|
13281
13349
|
found.map(async ({ rel, abs }) => {
|
|
13282
|
-
const file = new File([await (0,
|
|
13350
|
+
const file = new File([await (0, import_node_fs5.openAsBlob)(abs)], (0, import_node_path9.basename)(abs));
|
|
13283
13351
|
return Object.defineProperty(file, "webkitRelativePath", { value: rel });
|
|
13284
13352
|
})
|
|
13285
13353
|
);
|
|
@@ -13289,19 +13357,19 @@ async function planFolderArchive(dir) {
|
|
|
13289
13357
|
if (files.length === 0) {
|
|
13290
13358
|
throw new Error(`directory contains no regular files: ${dir}`);
|
|
13291
13359
|
}
|
|
13292
|
-
const folderName = (0,
|
|
13360
|
+
const folderName = (0, import_node_path9.basename)((0, import_node_path9.resolve)(dir)) || "folder";
|
|
13293
13361
|
const size = estimateZipSize({ files, folderName });
|
|
13294
13362
|
if (size > ZIP32_MAX_BYTES) {
|
|
13295
13363
|
throw new Error(`the folder's archive would be ${size} bytes, over the ZIP limit of ${ZIP32_MAX_BYTES}`);
|
|
13296
13364
|
}
|
|
13297
13365
|
const write = async () => {
|
|
13298
|
-
const tempDir = await (0, import_promises7.mkdtemp)((0,
|
|
13366
|
+
const tempDir = await (0, import_promises7.mkdtemp)((0, import_node_path9.join)((0, import_node_os7.tmpdir)(), "seal-mcp-zip-"));
|
|
13299
13367
|
const cleanup = () => (0, import_promises7.rm)(tempDir, { recursive: true, force: true });
|
|
13300
|
-
const path = (0,
|
|
13368
|
+
const path = (0, import_node_path9.join)(tempDir, `${folderName}.zip`);
|
|
13301
13369
|
try {
|
|
13302
13370
|
await (0, import_promises8.pipeline)(
|
|
13303
13371
|
import_node_stream.Readable.fromWeb(createZipStream2({ files, folderName })),
|
|
13304
|
-
(0,
|
|
13372
|
+
(0, import_node_fs5.createWriteStream)(path, { mode: 384 })
|
|
13305
13373
|
);
|
|
13306
13374
|
const written = (await (0, import_promises7.stat)(path)).size;
|
|
13307
13375
|
if (written !== size) {
|
|
@@ -13393,31 +13461,46 @@ var FORWARD_MAX_READS_FORCED = 1;
|
|
|
13393
13461
|
function nowIso2(ctx) {
|
|
13394
13462
|
return (ctx.now?.() ?? /* @__PURE__ */ new Date()).toISOString();
|
|
13395
13463
|
}
|
|
13396
|
-
|
|
13464
|
+
function journal(ctx, handle, op) {
|
|
13465
|
+
return ctx.update((state) => {
|
|
13466
|
+
state.pending[handle] = op;
|
|
13467
|
+
});
|
|
13468
|
+
}
|
|
13469
|
+
function dropJournal(ctx, handle) {
|
|
13470
|
+
return ctx.update((state) => {
|
|
13471
|
+
delete state.pending[handle];
|
|
13472
|
+
});
|
|
13473
|
+
}
|
|
13474
|
+
function complete(ctx, handle, entry) {
|
|
13475
|
+
return ctx.update((state) => {
|
|
13476
|
+
insertHandle(state, handle, entry);
|
|
13477
|
+
state.audit.push(auditEntry(ctx, "seal_share", handle, entry.mode));
|
|
13478
|
+
delete state.pending[handle];
|
|
13479
|
+
});
|
|
13480
|
+
}
|
|
13481
|
+
async function bestEffort(write) {
|
|
13397
13482
|
try {
|
|
13398
|
-
await
|
|
13483
|
+
await write;
|
|
13399
13484
|
} catch (err2) {
|
|
13400
13485
|
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
13401
|
-
process.stderr.write(`[seal-mcp] warn: journal
|
|
13486
|
+
process.stderr.write(`[seal-mcp] warn: journal write failed: ${msg}
|
|
13402
13487
|
`);
|
|
13403
13488
|
}
|
|
13404
13489
|
}
|
|
13405
|
-
async function compensateRevoke(ctx, handle) {
|
|
13406
|
-
const
|
|
13407
|
-
|
|
13408
|
-
if (!op || sealId === void 0) {
|
|
13490
|
+
async function compensateRevoke(ctx, handle, op) {
|
|
13491
|
+
const sealId = op.seal_id;
|
|
13492
|
+
if (sealId === void 0) {
|
|
13409
13493
|
return "no live seal to revoke";
|
|
13410
13494
|
}
|
|
13411
13495
|
try {
|
|
13412
13496
|
await ctx.client.revoke(sealId, op.owner_token_b64);
|
|
13413
|
-
|
|
13414
|
-
await persistBestEffort(ctx);
|
|
13497
|
+
await bestEffort(dropJournal(ctx, handle));
|
|
13415
13498
|
return "seal auto-revoked";
|
|
13416
13499
|
} catch (err2) {
|
|
13417
13500
|
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
13418
13501
|
op.step = "compensating";
|
|
13419
13502
|
op.compensation = { action: "revoke", outcome: "failed", error: msg, at: nowIso2(ctx) };
|
|
13420
|
-
await
|
|
13503
|
+
await bestEffort(journal(ctx, handle, op));
|
|
13421
13504
|
return `auto-revoke FAILED (${msg}) \u2014 the orphaned seal stays live until its TTL (${op.expires_at}); revoke is journaled and will be retried at next server start`;
|
|
13422
13505
|
}
|
|
13423
13506
|
}
|
|
@@ -13442,7 +13525,7 @@ async function openSource(path, mime, requestedTier) {
|
|
|
13442
13525
|
} catch (err2) {
|
|
13443
13526
|
throw failed("unable to open", err2);
|
|
13444
13527
|
}
|
|
13445
|
-
let name = (0,
|
|
13528
|
+
let name = (0, import_node_path10.basename)(path);
|
|
13446
13529
|
let source = path;
|
|
13447
13530
|
let cleanup = async () => void 0;
|
|
13448
13531
|
let tier;
|
|
@@ -13468,7 +13551,7 @@ async function openSource(path, mime, requestedTier) {
|
|
|
13468
13551
|
}
|
|
13469
13552
|
let fileBlob;
|
|
13470
13553
|
try {
|
|
13471
|
-
fileBlob = await (0,
|
|
13554
|
+
fileBlob = await (0, import_node_fs6.openAsBlob)(source);
|
|
13472
13555
|
} catch (err2) {
|
|
13473
13556
|
await cleanup();
|
|
13474
13557
|
throw failed("unable to open", err2);
|
|
@@ -13589,13 +13672,14 @@ async function uploadAndDeliver(ctx, input, deps, prepared) {
|
|
|
13589
13672
|
// backend this seal is actually created on — recovery refuses to
|
|
13590
13673
|
// "revoke" against a different backend whose 404 would be misread
|
|
13591
13674
|
// as success.
|
|
13592
|
-
backend_url: ctx.client.baseUrl
|
|
13675
|
+
backend_url: ctx.client.baseUrl,
|
|
13676
|
+
// SPEC-ADDRESS F1 §4: another MCP host starting now must not recover
|
|
13677
|
+
// (revoke) this upload while it runs.
|
|
13678
|
+
owner: { pid: process.pid, hostname: (0, import_node_os8.hostname)() }
|
|
13593
13679
|
};
|
|
13594
|
-
ctx.state.pending[handle] = op;
|
|
13595
13680
|
try {
|
|
13596
|
-
await ctx
|
|
13681
|
+
await journal(ctx, handle, op);
|
|
13597
13682
|
} catch (err2) {
|
|
13598
|
-
delete ctx.state.pending[handle];
|
|
13599
13683
|
throw new ToolError(
|
|
13600
13684
|
"state_persist_failed",
|
|
13601
13685
|
`refusing to upload: could not journal the operation before its first remote effect: ${err2 instanceof Error ? err2.message : String(err2)}`,
|
|
@@ -13616,7 +13700,7 @@ async function uploadAndDeliver(ctx, input, deps, prepared) {
|
|
|
13616
13700
|
// `uploaded` checkpoint after this call is the durability gate.
|
|
13617
13701
|
onSealCreated: async (sealId) => {
|
|
13618
13702
|
op.seal_id = sealId;
|
|
13619
|
-
await
|
|
13703
|
+
await bestEffort(journal(ctx, handle, op));
|
|
13620
13704
|
}
|
|
13621
13705
|
});
|
|
13622
13706
|
} catch (err2) {
|
|
@@ -13625,15 +13709,14 @@ async function uploadAndDeliver(ctx, input, deps, prepared) {
|
|
|
13625
13709
|
return awaitPayment(ctx, deps, prepared, { handle, op, ownerTokenB64, expiresAt, session });
|
|
13626
13710
|
}
|
|
13627
13711
|
if (op.seal_id !== void 0) {
|
|
13628
|
-
const outcome = await compensateRevoke(ctx, handle);
|
|
13712
|
+
const outcome = await compensateRevoke(ctx, handle, op);
|
|
13629
13713
|
throw new ToolError(
|
|
13630
13714
|
"upload_failed",
|
|
13631
13715
|
`seal upload failed after the seal was created: ${err2 instanceof Error ? err2.message : String(err2)} (${outcome})`,
|
|
13632
13716
|
err2
|
|
13633
13717
|
);
|
|
13634
13718
|
}
|
|
13635
|
-
|
|
13636
|
-
await persistBestEffort(ctx);
|
|
13719
|
+
await bestEffort(dropJournal(ctx, handle));
|
|
13637
13720
|
throw new ToolError(
|
|
13638
13721
|
"upload_failed",
|
|
13639
13722
|
`seal upload failed: ${err2 instanceof Error ? err2.message : String(err2)}`,
|
|
@@ -13646,9 +13729,9 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
|
|
|
13646
13729
|
op.seal_id = uploaded.sealId;
|
|
13647
13730
|
op.step = "uploaded";
|
|
13648
13731
|
try {
|
|
13649
|
-
await ctx
|
|
13732
|
+
await journal(ctx, handle, op);
|
|
13650
13733
|
} catch (err2) {
|
|
13651
|
-
const outcome = await compensateRevoke(ctx, handle);
|
|
13734
|
+
const outcome = await compensateRevoke(ctx, handle, op);
|
|
13652
13735
|
throw new ToolError(
|
|
13653
13736
|
"state_persist_failed",
|
|
13654
13737
|
`could not journal the live seal after upload: ${err2 instanceof Error ? err2.message : String(err2)} (${outcome})`,
|
|
@@ -13661,7 +13744,7 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
|
|
|
13661
13744
|
try {
|
|
13662
13745
|
shareBlobBytes = await encryptShareBlob(fileKeysB64, shareLinkKey, void 0);
|
|
13663
13746
|
} catch (err2) {
|
|
13664
|
-
const outcome = await compensateRevoke(ctx, handle);
|
|
13747
|
+
const outcome = await compensateRevoke(ctx, handle, op);
|
|
13665
13748
|
throw new ToolError(
|
|
13666
13749
|
"share_blob_failed",
|
|
13667
13750
|
`share blob encryption failed: ${err2 instanceof Error ? err2.message : String(err2)} (${outcome})`,
|
|
@@ -13673,7 +13756,7 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
|
|
|
13673
13756
|
try {
|
|
13674
13757
|
shareCode = await ctx.client.createShareLink(uploaded.sealId, ownerTokenB64, shareBlobB64);
|
|
13675
13758
|
} catch (err2) {
|
|
13676
|
-
const outcome = await compensateRevoke(ctx, handle);
|
|
13759
|
+
const outcome = await compensateRevoke(ctx, handle, op);
|
|
13677
13760
|
throw new ToolError(
|
|
13678
13761
|
"share_link_failed",
|
|
13679
13762
|
`POST /seal/${uploaded.sealId}/share failed: ${err2 instanceof Error ? err2.message : String(err2)} (${outcome})`,
|
|
@@ -13691,7 +13774,7 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
|
|
|
13691
13774
|
try {
|
|
13692
13775
|
fragment = await buildTargetedFragment(shareLinkKey, recipientPub, uploaded.sealId);
|
|
13693
13776
|
} catch (err2) {
|
|
13694
|
-
const outcome = await compensateRevoke(ctx, handle);
|
|
13777
|
+
const outcome = await compensateRevoke(ctx, handle, op);
|
|
13695
13778
|
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
13696
13779
|
if (message.includes("non-contributory")) {
|
|
13697
13780
|
throw new ToolError(
|
|
@@ -13712,7 +13795,7 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
|
|
|
13712
13795
|
const host = resolvePublicHost(deps.publicHost);
|
|
13713
13796
|
const shareUrl = `${host}/s/${uploaded.sealId}/${shareCode}#${fragment}`;
|
|
13714
13797
|
if (mode === "forward") {
|
|
13715
|
-
|
|
13798
|
+
await complete(ctx, handle, {
|
|
13716
13799
|
seal_id: uploaded.sealId,
|
|
13717
13800
|
owner_token_b64: ownerTokenB64,
|
|
13718
13801
|
created_at: nowIso2(ctx),
|
|
@@ -13720,9 +13803,6 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
|
|
|
13720
13803
|
expires_at: expiresAt,
|
|
13721
13804
|
backend_url: ctx.client.baseUrl
|
|
13722
13805
|
});
|
|
13723
|
-
appendAudit(ctx, "seal_share", handle, mode);
|
|
13724
|
-
delete ctx.state.pending[handle];
|
|
13725
|
-
await ctx.persist();
|
|
13726
13806
|
return {
|
|
13727
13807
|
handle,
|
|
13728
13808
|
shareUrl,
|
|
@@ -13734,9 +13814,9 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
|
|
|
13734
13814
|
op.step = "delivering";
|
|
13735
13815
|
op.handoff_channels_attempted = deps.handoff?.disableClipboard ? ["file"] : ["clipboard", "file"];
|
|
13736
13816
|
try {
|
|
13737
|
-
await ctx
|
|
13817
|
+
await journal(ctx, handle, op);
|
|
13738
13818
|
} catch (err2) {
|
|
13739
|
-
const outcome = await compensateRevoke(ctx, handle);
|
|
13819
|
+
const outcome = await compensateRevoke(ctx, handle, op);
|
|
13740
13820
|
throw new ToolError(
|
|
13741
13821
|
"state_persist_failed",
|
|
13742
13822
|
`could not journal the delivery attempt before handing off the URL: ${err2 instanceof Error ? err2.message : String(err2)} (${outcome})`,
|
|
@@ -13747,14 +13827,14 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
|
|
|
13747
13827
|
try {
|
|
13748
13828
|
handoff = await performHandoff({ handle, shareUrl, expireMs }, deps.handoff);
|
|
13749
13829
|
} catch (err2) {
|
|
13750
|
-
const outcome = await compensateRevoke(ctx, handle);
|
|
13830
|
+
const outcome = await compensateRevoke(ctx, handle, op);
|
|
13751
13831
|
throw new ToolError(
|
|
13752
13832
|
"handoff_both_channels_failed",
|
|
13753
13833
|
`both clipboard and file handoff channels failed \u2014 ${outcome}`,
|
|
13754
13834
|
err2
|
|
13755
13835
|
);
|
|
13756
13836
|
}
|
|
13757
|
-
|
|
13837
|
+
await complete(ctx, handle, {
|
|
13758
13838
|
seal_id: uploaded.sealId,
|
|
13759
13839
|
owner_token_b64: ownerTokenB64,
|
|
13760
13840
|
created_at: nowIso2(ctx),
|
|
@@ -13763,9 +13843,6 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
|
|
|
13763
13843
|
handoff_channel: handoff.channel,
|
|
13764
13844
|
backend_url: ctx.client.baseUrl
|
|
13765
13845
|
});
|
|
13766
|
-
appendAudit(ctx, "seal_share", handle, mode);
|
|
13767
|
-
delete ctx.state.pending[handle];
|
|
13768
|
-
await ctx.persist();
|
|
13769
13846
|
const delivered = handoff.channel === "clipboard" ? HANDOFF_OUTPUT.receipt_clipboard : HANDOFF_OUTPUT.receipt_file.replace("{file}", handoff.file ?? "");
|
|
13770
13847
|
const verified = uploaded.verification === "verified" ? delivered : `${delivered} The server has not finished verifying the file: for the next few seconds the recipient sees "not ready", after which the link works.`;
|
|
13771
13848
|
const receipt = ctx.ephemeral ? `${verified} This server keeps no state on disk: the handle lives until it restarts; the seal's expiry does not depend on it.` : verified;
|
|
@@ -13784,12 +13861,12 @@ async function awaitPayment(ctx, deps, prepared, { handle, op, ownerTokenB64, se
|
|
|
13784
13861
|
const tier = prepared.tier;
|
|
13785
13862
|
const sealId = op.seal_id;
|
|
13786
13863
|
op.step = "awaiting_payment";
|
|
13787
|
-
await
|
|
13864
|
+
await bestEffort(journal(ctx, handle, op));
|
|
13788
13865
|
let paymentUrl;
|
|
13789
13866
|
try {
|
|
13790
13867
|
paymentUrl = await ctx.client.createCheckout(sealId, ownerTokenB64, tier);
|
|
13791
13868
|
} catch (err2) {
|
|
13792
|
-
const outcome = await compensateRevoke(ctx, handle);
|
|
13869
|
+
const outcome = await compensateRevoke(ctx, handle, op);
|
|
13793
13870
|
throw new ToolError("payment_failed", `POST /billing/checkout failed: ${messageOf3(err2)} (${outcome})`, err2);
|
|
13794
13871
|
}
|
|
13795
13872
|
const deadline = paymentDeadline(op.expires_at);
|
|
@@ -13799,7 +13876,7 @@ async function awaitPayment(ctx, deps, prepared, { handle, op, ownerTokenB64, se
|
|
|
13799
13876
|
clearTimeout(timer);
|
|
13800
13877
|
ctx.payments?.delete(handle);
|
|
13801
13878
|
await prepared.cleanup();
|
|
13802
|
-
throw new ToolError(code, `${why} (${await compensateRevoke(ctx, handle)})`);
|
|
13879
|
+
throw new ToolError(code, `${why} (${await compensateRevoke(ctx, handle, op)})`);
|
|
13803
13880
|
};
|
|
13804
13881
|
const finish = async (onProgress) => {
|
|
13805
13882
|
if (running) throw new ToolError("payment_invalid", `share ${handle} is already being finished`);
|
|
@@ -13968,7 +14045,7 @@ function proFileGetOutputToWire(out) {
|
|
|
13968
14045
|
}
|
|
13969
14046
|
|
|
13970
14047
|
// src/server.ts
|
|
13971
|
-
var SERVER_VERSION = "0.
|
|
14048
|
+
var SERVER_VERSION = "0.3.0";
|
|
13972
14049
|
var toolMeta = (name) => {
|
|
13973
14050
|
const { title, description, annotations } = seal_mcp_v1_default.tools[name];
|
|
13974
14051
|
return { title, description, annotations };
|
|
@@ -13995,41 +14072,29 @@ function progressReporter(extra) {
|
|
|
13995
14072
|
}
|
|
13996
14073
|
async function createServer(options) {
|
|
13997
14074
|
logEngineToStderr();
|
|
13998
|
-
let view;
|
|
13999
|
-
let identity;
|
|
14000
|
-
let lock = null;
|
|
14001
14075
|
const client = new SealApi({ baseUrl: resolveBackendUrl(options.backendBaseUrl) });
|
|
14002
14076
|
const disk = options.state;
|
|
14077
|
+
let view;
|
|
14078
|
+
const update = async (mutate) => {
|
|
14079
|
+
if (disk === null) await mutate(view);
|
|
14080
|
+
else view = await updateState(disk.passphrase, disk.path, mutate);
|
|
14081
|
+
};
|
|
14082
|
+
let identity;
|
|
14003
14083
|
if (disk === null) {
|
|
14004
14084
|
identity = await generateIdentity();
|
|
14005
14085
|
view = createEmptyState(identityToFields(identity));
|
|
14006
14086
|
} else {
|
|
14007
|
-
|
|
14008
|
-
|
|
14009
|
-
try {
|
|
14010
|
-
view = await loadState(disk.passphrase, disk.path);
|
|
14011
|
-
identity = await loadIdentity(view.identity);
|
|
14012
|
-
const { changed } = await recoverPendingOperations(view, client);
|
|
14013
|
-
if (changed) {
|
|
14014
|
-
await saveState(view, disk.passphrase, disk.path);
|
|
14015
|
-
}
|
|
14016
|
-
} catch (err2) {
|
|
14017
|
-
await held.release();
|
|
14018
|
-
throw err2;
|
|
14019
|
-
}
|
|
14087
|
+
view = await updateState(disk.passphrase, disk.path, async (state) => (await recoverPendingOperations(state, client)).changed);
|
|
14088
|
+
identity = await loadIdentity(view.identity);
|
|
14020
14089
|
}
|
|
14021
14090
|
const publicHost = resolvePublicHost(options.publicHost);
|
|
14022
|
-
let persistChain = Promise.resolve();
|
|
14023
|
-
const persist = () => {
|
|
14024
|
-
if (disk === null) return Promise.resolve();
|
|
14025
|
-
persistChain = persistChain.catch(() => void 0).then(() => saveState(view, disk.passphrase, disk.path));
|
|
14026
|
-
return persistChain;
|
|
14027
|
-
};
|
|
14028
14091
|
const ctx = {
|
|
14029
|
-
state
|
|
14092
|
+
get state() {
|
|
14093
|
+
return view;
|
|
14094
|
+
},
|
|
14030
14095
|
identity,
|
|
14031
14096
|
client,
|
|
14032
|
-
|
|
14097
|
+
update,
|
|
14033
14098
|
ephemeral: disk === null,
|
|
14034
14099
|
requests: /* @__PURE__ */ new Map(),
|
|
14035
14100
|
payments: /* @__PURE__ */ new Map()
|
|
@@ -14232,18 +14297,12 @@ async function createServer(options) {
|
|
|
14232
14297
|
);
|
|
14233
14298
|
}
|
|
14234
14299
|
const transport = new import_stdio.StdioServerTransport();
|
|
14235
|
-
|
|
14236
|
-
await mcp.connect(transport);
|
|
14237
|
-
} catch (err2) {
|
|
14238
|
-
await lock?.release();
|
|
14239
|
-
throw err2;
|
|
14240
|
-
}
|
|
14300
|
+
await mcp.connect(transport);
|
|
14241
14301
|
const close = async () => {
|
|
14242
14302
|
try {
|
|
14243
14303
|
await mcp.close();
|
|
14244
14304
|
} finally {
|
|
14245
14305
|
await transport.close().catch(() => void 0);
|
|
14246
|
-
await lock?.release();
|
|
14247
14306
|
}
|
|
14248
14307
|
};
|
|
14249
14308
|
return { mcp, transport, close };
|
|
@@ -14257,7 +14316,7 @@ async function wrapTool(fn, toWire) {
|
|
|
14257
14316
|
structuredContent: wire
|
|
14258
14317
|
};
|
|
14259
14318
|
} catch (err2) {
|
|
14260
|
-
if (err2 instanceof ToolError) {
|
|
14319
|
+
if (err2 instanceof ToolError || err2 instanceof StateBusyError) {
|
|
14261
14320
|
return {
|
|
14262
14321
|
isError: true,
|
|
14263
14322
|
content: [
|
|
@@ -14279,7 +14338,6 @@ async function wrapTool(fn, toWire) {
|
|
|
14279
14338
|
// src/cli.ts
|
|
14280
14339
|
var PROGRAM_NAME = "sealnet-mcp";
|
|
14281
14340
|
var DESCRIPTION = "MCP server for SEAL \u2014 share files via Cursor / Claude Desktop without handing the model the share link. Handoff mode (default) delivers the link out-of-band (clipboard / 0o600 file); the model only gets an opaque handle.";
|
|
14282
|
-
var MIN_PASSPHRASE_LEN = 8;
|
|
14283
14341
|
function logStderr(line) {
|
|
14284
14342
|
process.stderr.write(`${line}
|
|
14285
14343
|
`);
|
|
@@ -14288,118 +14346,33 @@ function logStdout(line) {
|
|
|
14288
14346
|
process.stdout.write(`${line}
|
|
14289
14347
|
`);
|
|
14290
14348
|
}
|
|
14291
|
-
async function
|
|
14292
|
-
|
|
14293
|
-
await (0, import_promises10.stat)(p);
|
|
14294
|
-
return true;
|
|
14295
|
-
} catch {
|
|
14296
|
-
return false;
|
|
14297
|
-
}
|
|
14349
|
+
async function agentKey(path) {
|
|
14350
|
+
return await readSeed(path.dir) ?? (await openAgentState(path)).root;
|
|
14298
14351
|
}
|
|
14299
14352
|
async function runInit(opts) {
|
|
14300
14353
|
const path = resolveStateDir({ cliFlag: opts.stateDir });
|
|
14301
|
-
if (
|
|
14354
|
+
if (opts.force) await withStateLock(path.dir, () => import_node_fs7.promises.rm(path.statePath, { force: true }));
|
|
14355
|
+
const opened = await openAgentState(path);
|
|
14356
|
+
if (!opened.created) {
|
|
14302
14357
|
logStderr(`${PROGRAM_NAME} init: state file already exists at ${path.statePath}`);
|
|
14303
|
-
logStderr("Re-run with --force to
|
|
14358
|
+
logStderr("Re-run with --force to replace it with an empty one (its handles are lost; the address stays).");
|
|
14304
14359
|
process.exit(2);
|
|
14305
14360
|
}
|
|
14306
|
-
const
|
|
14307
|
-
let passphrase;
|
|
14308
|
-
if (fromEnv !== void 0 && fromEnv.length > 0) {
|
|
14309
|
-
passphrase = fromEnv;
|
|
14310
|
-
logStderr(
|
|
14311
|
-
`${PROGRAM_NAME} init: using passphrase from ${PASSPHRASE_ENV_VAR} (will also save to OS keychain).`
|
|
14312
|
-
);
|
|
14313
|
-
} else {
|
|
14314
|
-
passphrase = await promptHiddenPassphrase("Choose a passphrase (will not echo): ");
|
|
14315
|
-
if (passphrase.length < MIN_PASSPHRASE_LEN) {
|
|
14316
|
-
throw new Error(`Passphrase must be at least ${MIN_PASSPHRASE_LEN} characters.`);
|
|
14317
|
-
}
|
|
14318
|
-
const confirm = await promptHiddenPassphrase("Confirm passphrase: ");
|
|
14319
|
-
if (confirm !== passphrase) {
|
|
14320
|
-
throw new Error("Passphrases do not match.");
|
|
14321
|
-
}
|
|
14322
|
-
}
|
|
14323
|
-
logStderr(`${PROGRAM_NAME} init: generating X25519 identity ...`);
|
|
14324
|
-
const identity = await generateIdentity();
|
|
14325
|
-
const identityFields = identityToFields(identity);
|
|
14326
|
-
const state = createEmptyState(identityFields);
|
|
14327
|
-
logStderr(`${PROGRAM_NAME} init: writing encrypted state to ${path.statePath} ...`);
|
|
14328
|
-
const lock = await acquireStateLock(path.dir);
|
|
14329
|
-
let keychainStored = false;
|
|
14330
|
-
try {
|
|
14331
|
-
if (!opts.force && await pathExists(path.statePath)) {
|
|
14332
|
-
logStderr(`${PROGRAM_NAME} init: state file already exists at ${path.statePath}`);
|
|
14333
|
-
logStderr("Re-run with --force to overwrite (you will lose existing handles + identity).");
|
|
14334
|
-
process.exit(2);
|
|
14335
|
-
}
|
|
14336
|
-
await saveState(state, passphrase, path.statePath);
|
|
14337
|
-
try {
|
|
14338
|
-
await setKeychainPassphrase(passphrase);
|
|
14339
|
-
keychainStored = true;
|
|
14340
|
-
logStderr(
|
|
14341
|
-
`${PROGRAM_NAME} init: passphrase stored in OS keychain (service=${KEYCHAIN_SERVICE} account=${KEYCHAIN_ACCOUNT}).`
|
|
14342
|
-
);
|
|
14343
|
-
} catch (err2) {
|
|
14344
|
-
if (err2 instanceof KeytarUnavailableError) {
|
|
14345
|
-
logStderr(`${PROGRAM_NAME} init: warning: ${err2.message}`);
|
|
14346
|
-
logStderr(
|
|
14347
|
-
`${PROGRAM_NAME} init: state.json IS encrypted, but you must export ${PASSPHRASE_ENV_VAR} before every \`${PROGRAM_NAME} serve\` until the OS keychain works.`
|
|
14348
|
-
);
|
|
14349
|
-
} else {
|
|
14350
|
-
throw err2;
|
|
14351
|
-
}
|
|
14352
|
-
}
|
|
14353
|
-
} finally {
|
|
14354
|
-
await lock.release();
|
|
14355
|
-
}
|
|
14361
|
+
const identity = await identityFromSeed(opened.root.seed);
|
|
14356
14362
|
logStdout(`${PROGRAM_NAME} init: ready.`);
|
|
14357
|
-
logStdout(` state.json:
|
|
14358
|
-
logStdout(`
|
|
14359
|
-
logStdout(`
|
|
14363
|
+
logStdout(` state.json: ${path.statePath}`);
|
|
14364
|
+
logStdout(` key source: ${opened.root.source}`);
|
|
14365
|
+
logStdout(` address: ${Buffer.from(identity.public).toString("base64url")}`);
|
|
14360
14366
|
logStdout("");
|
|
14361
|
-
logStdout(
|
|
14362
|
-
"Share the pubkey with senders so they can target SEALs to your MCP server (see `seal send --to <pubkey>` from `seal-cli`)."
|
|
14363
|
-
);
|
|
14364
|
-
}
|
|
14365
|
-
async function createStateWithoutPrompt(path) {
|
|
14366
|
-
const resolved = await passphraseForNewState();
|
|
14367
|
-
if (resolved === null) return null;
|
|
14368
|
-
const lock = await acquireStateLock(path.dir);
|
|
14369
|
-
try {
|
|
14370
|
-
if (await pathExists(path.statePath)) return void 0;
|
|
14371
|
-
if (resolved.fresh) await setKeychainPassphrase(resolved.passphrase);
|
|
14372
|
-
const identity = await generateIdentity();
|
|
14373
|
-
await saveState(createEmptyState(identityToFields(identity)), resolved.passphrase, path.statePath);
|
|
14374
|
-
} finally {
|
|
14375
|
-
await lock.release();
|
|
14376
|
-
}
|
|
14377
|
-
logStderr(`${PROGRAM_NAME} serve: first start, state created at ${path.statePath}`);
|
|
14378
|
-
return resolved;
|
|
14367
|
+
logStdout("Senders use the address to deliver a link only this agent opens (`seal send --to <address>`).");
|
|
14379
14368
|
}
|
|
14380
14369
|
async function stateForServe(path) {
|
|
14381
|
-
|
|
14382
|
-
const created = await createStateWithoutPrompt(path);
|
|
14383
|
-
if (created !== void 0) return created;
|
|
14384
|
-
}
|
|
14385
|
-
const resolved = await resolvePassphraseFromStores();
|
|
14386
|
-
if (resolved === null) {
|
|
14387
|
-
throw new PassphraseNotFoundError();
|
|
14388
|
-
}
|
|
14389
|
-
if (resolved.source === "env") {
|
|
14390
|
-
logStderr(
|
|
14391
|
-
`${PROGRAM_NAME} serve: warning: passphrase read from ${PASSPHRASE_ENV_VAR}. Environment variables are readable via /proc/<pid>/environ on Linux and the process listing on Windows. Prefer the OS keychain (run \`${PROGRAM_NAME} init\`).`
|
|
14392
|
-
);
|
|
14393
|
-
}
|
|
14370
|
+
let opened;
|
|
14394
14371
|
try {
|
|
14395
|
-
|
|
14396
|
-
await loadIdentity(view.identity);
|
|
14372
|
+
opened = await openAgentState(path);
|
|
14397
14373
|
} catch (err2) {
|
|
14398
|
-
if (err2 instanceof
|
|
14399
|
-
logStderr(`${PROGRAM_NAME} serve:
|
|
14400
|
-
logStderr(
|
|
14401
|
-
`If you reset your OS keychain, re-run \`${PROGRAM_NAME} init --force\` (you will lose existing handles).`
|
|
14402
|
-
);
|
|
14374
|
+
if (err2 instanceof IdentityConflictError) {
|
|
14375
|
+
logStderr(`${PROGRAM_NAME} serve: ${err2.message}`);
|
|
14403
14376
|
process.exit(3);
|
|
14404
14377
|
}
|
|
14405
14378
|
if (err2 instanceof StateCorrupt) {
|
|
@@ -14408,29 +14381,27 @@ async function stateForServe(path) {
|
|
|
14408
14381
|
}
|
|
14409
14382
|
throw err2;
|
|
14410
14383
|
}
|
|
14411
|
-
|
|
14384
|
+
if (opened.created) logStderr(`${PROGRAM_NAME} serve: first start, state created at ${path.statePath}`);
|
|
14385
|
+
if (opened.moved !== null) logStderr(`${PROGRAM_NAME} serve: moved the 0.2 state under the agent's key (${opened.root.source})`);
|
|
14386
|
+
if (opened.moved === "env") {
|
|
14387
|
+
logStderr(
|
|
14388
|
+
`${PROGRAM_NAME} serve: ${PASSPHRASE_ENV_VAR} is no longer used; to move this agent to another machine, set ${SEED_ENV_VAR} there (\`${PROGRAM_NAME} seed --force\` prints it).`
|
|
14389
|
+
);
|
|
14390
|
+
}
|
|
14391
|
+
return opened;
|
|
14412
14392
|
}
|
|
14413
14393
|
async function runServe(opts) {
|
|
14414
14394
|
const path = resolveStateDir({ cliFlag: opts.stateDir });
|
|
14415
|
-
const
|
|
14395
|
+
const opened = opts.ephemeral || process.env[EPHEMERAL_ENV_VAR] === "1" ? null : await stateForServe(path);
|
|
14416
14396
|
logStderr(
|
|
14417
|
-
|
|
14397
|
+
opened === null ? `${PROGRAM_NAME} serve: ephemeral \u2014 a new key, identity and handles live in memory until exit, nothing on disk` : `${PROGRAM_NAME} serve: state OK at ${path.statePath} (key=${opened.root.source})`
|
|
14418
14398
|
);
|
|
14419
14399
|
logStderr(`${PROGRAM_NAME} serve: starting stdio MCP server ...`);
|
|
14420
|
-
|
|
14421
|
-
|
|
14422
|
-
|
|
14423
|
-
|
|
14424
|
-
|
|
14425
|
-
publicHost: opts.publicHost
|
|
14426
|
-
});
|
|
14427
|
-
} catch (err2) {
|
|
14428
|
-
if (err2 instanceof StateLockedError) {
|
|
14429
|
-
logStderr(`${PROGRAM_NAME} serve: ${err2.message}`);
|
|
14430
|
-
process.exit(5);
|
|
14431
|
-
}
|
|
14432
|
-
throw err2;
|
|
14433
|
-
}
|
|
14400
|
+
const handle = await createServer({
|
|
14401
|
+
state: opened === null ? null : { path: path.statePath, passphrase: opened.passphrase },
|
|
14402
|
+
backendBaseUrl: opts.backendUrl,
|
|
14403
|
+
publicHost: opts.publicHost
|
|
14404
|
+
});
|
|
14434
14405
|
const shutdown = async (signal) => {
|
|
14435
14406
|
logStderr(`${PROGRAM_NAME} serve: received ${signal}, shutting down ...`);
|
|
14436
14407
|
try {
|
|
@@ -14449,28 +14420,39 @@ async function runServe(opts) {
|
|
|
14449
14420
|
});
|
|
14450
14421
|
}
|
|
14451
14422
|
async function runPubkey(opts) {
|
|
14452
|
-
const
|
|
14453
|
-
|
|
14454
|
-
|
|
14423
|
+
const root = await agentKey(resolveStateDir({ cliFlag: opts.stateDir }));
|
|
14424
|
+
const { public: address } = await identityFromSeed(root.seed);
|
|
14425
|
+
logStdout(Buffer.from(address).toString("base64url"));
|
|
14426
|
+
logStderr(`${keyEmoji(address)} key: ${root.source}`);
|
|
14427
|
+
}
|
|
14428
|
+
async function runSeed(opts) {
|
|
14429
|
+
if (!opts.force) {
|
|
14430
|
+
throw new Error(`seed prints the agent's secret key \u2014 re-run with --force to confirm, to set ${SEED_ENV_VAR} elsewhere.`);
|
|
14455
14431
|
}
|
|
14456
|
-
const
|
|
14457
|
-
|
|
14458
|
-
|
|
14459
|
-
|
|
14432
|
+
const root = await agentKey(resolveStateDir({ cliFlag: opts.stateDir }));
|
|
14433
|
+
logStderr(
|
|
14434
|
+
`${PROGRAM_NAME} seed: this is the agent's secret key. Whoever holds it opens the links sent to this agent; put it only in ${SEED_ENV_VAR} of a machine you control.`
|
|
14435
|
+
);
|
|
14436
|
+
logStdout(Buffer.from(root.seed).toString("base64url"));
|
|
14437
|
+
}
|
|
14438
|
+
async function perms(file) {
|
|
14439
|
+
return (0, import_promises10.stat)(file).then(
|
|
14440
|
+
(st) => (st.mode & 511).toString(8).padStart(3, "0"),
|
|
14441
|
+
() => null
|
|
14442
|
+
);
|
|
14460
14443
|
}
|
|
14461
14444
|
async function runDoctor(opts) {
|
|
14462
14445
|
const path = resolveStateDir({ cliFlag: opts.stateDir });
|
|
14463
|
-
const
|
|
14464
|
-
let
|
|
14465
|
-
let
|
|
14466
|
-
let stateOwnerUid = null;
|
|
14446
|
+
const seedFile = (0, import_node_path11.join)(path.dir, SEED_FILE_NAME);
|
|
14447
|
+
let seedSource = null;
|
|
14448
|
+
let seedError = null;
|
|
14467
14449
|
try {
|
|
14468
|
-
|
|
14469
|
-
|
|
14470
|
-
|
|
14471
|
-
stateOwnerUid = typeof st.uid === "number" ? st.uid : null;
|
|
14472
|
-
} catch {
|
|
14450
|
+
seedSource = (await readSeed(path.dir))?.source ?? null;
|
|
14451
|
+
} catch (err2) {
|
|
14452
|
+
seedError = err2 instanceof Error ? err2.message : String(err2);
|
|
14473
14453
|
}
|
|
14454
|
+
const statePerms = await perms(path.statePath);
|
|
14455
|
+
const seedPerms = await perms(seedFile);
|
|
14474
14456
|
const kc = await probeKeychain();
|
|
14475
14457
|
const lockProbe = await probeStateLock(path.dir);
|
|
14476
14458
|
const client = new SealApi({ baseUrl: resolveBackendUrl(opts.backendUrl) });
|
|
@@ -14491,103 +14473,101 @@ async function runDoctor(opts) {
|
|
|
14491
14473
|
backendReachable = false;
|
|
14492
14474
|
backendError = err2 instanceof Error ? err2.message : String(err2);
|
|
14493
14475
|
}
|
|
14494
|
-
|
|
14495
|
-
|
|
14496
|
-
|
|
14497
|
-
|
|
14498
|
-
|
|
14499
|
-
|
|
14500
|
-
|
|
14501
|
-
|
|
14502
|
-
|
|
14503
|
-
|
|
14504
|
-
|
|
14505
|
-
|
|
14506
|
-
|
|
14507
|
-
|
|
14508
|
-
|
|
14509
|
-
|
|
14510
|
-
|
|
14511
|
-
|
|
14512
|
-
|
|
14513
|
-
|
|
14514
|
-
|
|
14515
|
-
|
|
14516
|
-
|
|
14517
|
-
|
|
14476
|
+
logStdout(
|
|
14477
|
+
JSON.stringify(
|
|
14478
|
+
{
|
|
14479
|
+
state_path: path.statePath,
|
|
14480
|
+
state_exists: statePerms !== null,
|
|
14481
|
+
state_perms_octal: statePerms,
|
|
14482
|
+
seed_source: seedSource,
|
|
14483
|
+
seed_error: seedError,
|
|
14484
|
+
seed_file_perms_octal: seedPerms,
|
|
14485
|
+
keychain_available: kc.available,
|
|
14486
|
+
keychain_has_seed: kc.hasSeed,
|
|
14487
|
+
env_seed_set: Boolean(process.env[SEED_ENV_VAR]),
|
|
14488
|
+
env_passphrase_set: Boolean(process.env[PASSPHRASE_ENV_VAR]),
|
|
14489
|
+
lock_held: lockProbe.held,
|
|
14490
|
+
lock_holder_pid: lockProbe.holder?.pid ?? null,
|
|
14491
|
+
lock_stale: lockProbe.stale,
|
|
14492
|
+
backend_url: client.baseUrl,
|
|
14493
|
+
public_host: resolvePublicHost(opts.publicHost),
|
|
14494
|
+
backend_reachable: backendReachable,
|
|
14495
|
+
backend_status: backendStatus,
|
|
14496
|
+
backend_error: backendError,
|
|
14497
|
+
platform: process.platform,
|
|
14498
|
+
node_version: process.version
|
|
14499
|
+
},
|
|
14500
|
+
null,
|
|
14501
|
+
2
|
|
14502
|
+
)
|
|
14503
|
+
);
|
|
14504
|
+
if (seedError !== null) logStderr(`hint: ${seedError}`);
|
|
14505
|
+
else if (seedSource === null) {
|
|
14506
|
+
logStderr(`hint: no agent key yet \u2014 the first \`${PROGRAM_NAME} serve\` makes it (${kc.available ? "in the OS keychain" : seedFile}).`);
|
|
14518
14507
|
}
|
|
14519
|
-
|
|
14520
|
-
logStderr(
|
|
14521
|
-
`hint: keychain unavailable. On Linux start a Secret Service (gnome-keyring or KWallet), or rely on ${PASSPHRASE_ENV_VAR}; with neither, \`serve\` runs ephemeral (handles live until restart).`
|
|
14522
|
-
);
|
|
14523
|
-
} else if (stateExists && !kc.hasEntry && !report.env_passphrase_set) {
|
|
14524
|
-
logStderr(`hint: keychain has no entry and ${PASSPHRASE_ENV_VAR} is unset \u2014 \`serve\` will fail.`);
|
|
14508
|
+
for (const [file, got] of [[path.statePath, statePerms], [seedFile, seedPerms]]) {
|
|
14509
|
+
if (got !== null && got !== "600") logStderr(`hint: ${file} perms are ${got}, expected 600. Run \`chmod 600 \u2026\`.`);
|
|
14525
14510
|
}
|
|
14526
14511
|
if (backendReachable === false) {
|
|
14527
14512
|
logStderr(`hint: backend ${client.baseUrl} unreachable (${backendError ?? "unknown"}).`);
|
|
14528
14513
|
}
|
|
14529
|
-
if (lockProbe.held &&
|
|
14530
|
-
logStderr(
|
|
14531
|
-
`hint: state dir is locked by pid ${lockProbe.holder?.pid ?? "?"} \u2014 a serve instance is running.`
|
|
14532
|
-
);
|
|
14533
|
-
} else if (lockProbe.held && lockProbe.stale) {
|
|
14534
|
-
logStderr("hint: stale state.lock detected (holder dead) \u2014 next serve/init will break it automatically.");
|
|
14514
|
+
if (lockProbe.held && lockProbe.stale) {
|
|
14515
|
+
logStderr("hint: stale state.lock detected (holder dead) \u2014 the next write breaks it automatically.");
|
|
14535
14516
|
}
|
|
14536
14517
|
}
|
|
14537
14518
|
async function runReset(opts) {
|
|
14538
14519
|
if (!opts.force) {
|
|
14539
14520
|
throw new Error(
|
|
14540
|
-
"reset is destructive \u2014 re-run with --force to confirm. This wipes state.json AND
|
|
14521
|
+
"reset is destructive \u2014 re-run with --force to confirm. This wipes state.json AND the agent's key: its address is lost for good."
|
|
14541
14522
|
);
|
|
14542
14523
|
}
|
|
14543
14524
|
const path = resolveStateDir({ cliFlag: opts.stateDir });
|
|
14544
|
-
const
|
|
14545
|
-
|
|
14546
|
-
|
|
14547
|
-
|
|
14548
|
-
|
|
14549
|
-
|
|
14550
|
-
|
|
14551
|
-
|
|
14552
|
-
const code = err2.code;
|
|
14553
|
-
if (code !== "ENOENT") {
|
|
14554
|
-
throw err2;
|
|
14555
|
-
}
|
|
14556
|
-
}
|
|
14557
|
-
keychainDeleted = await deleteKeychainPassphrase();
|
|
14558
|
-
} finally {
|
|
14559
|
-
await lock.release();
|
|
14560
|
-
}
|
|
14525
|
+
const result = await withStateLock(path.dir, async () => {
|
|
14526
|
+
let stateDeleted = true;
|
|
14527
|
+
await import_node_fs7.promises.unlink(path.statePath).catch((err2) => {
|
|
14528
|
+
if (err2.code !== "ENOENT") throw err2;
|
|
14529
|
+
stateDeleted = false;
|
|
14530
|
+
});
|
|
14531
|
+
return { stateDeleted, ...await deleteRootSecret(path.dir) };
|
|
14532
|
+
});
|
|
14561
14533
|
logStdout(
|
|
14562
14534
|
JSON.stringify(
|
|
14563
14535
|
{
|
|
14564
|
-
state_deleted: stateDeleted,
|
|
14565
|
-
|
|
14536
|
+
state_deleted: result.stateDeleted,
|
|
14537
|
+
seed_file_deleted: result.seedFile,
|
|
14538
|
+
keychain_deleted: result.keychain,
|
|
14566
14539
|
state_path: path.statePath
|
|
14567
14540
|
},
|
|
14568
14541
|
null,
|
|
14569
14542
|
2
|
|
14570
14543
|
)
|
|
14571
14544
|
);
|
|
14545
|
+
logStderr(
|
|
14546
|
+
`${PROGRAM_NAME} reset: the agent's address is gone for good \u2014 links sent to it can no longer be opened, and whoever sends to this agent needs its new address.`
|
|
14547
|
+
);
|
|
14548
|
+
if (process.env[SEED_ENV_VAR]) {
|
|
14549
|
+
logStderr(`${PROGRAM_NAME} reset: ${SEED_ENV_VAR} is still set: the next start uses the key from the environment.`);
|
|
14550
|
+
}
|
|
14572
14551
|
}
|
|
14573
14552
|
function buildCli() {
|
|
14574
14553
|
const program = new import_commander.Command();
|
|
14575
14554
|
program.name(PROGRAM_NAME).description(DESCRIPTION).version(SERVER_VERSION, "-V, --version", `Print the ${PROGRAM_NAME} version and exit.`);
|
|
14576
14555
|
program.command("init").description(
|
|
14577
|
-
"
|
|
14578
|
-
).option("--state-dir <path>", "Override default state directory").option("--force", "
|
|
14556
|
+
"Make the agent's key (SEAL_SEED, else the OS keychain, else a 0600 file) and an empty encrypted state.json, without starting the server (`serve` does this itself on its first start)."
|
|
14557
|
+
).option("--state-dir <path>", "Override default state directory").option("--force", "Replace an existing state.json with an empty one (DESTRUCTIVE; the address stays)", false).action(async (opts) => runInit(opts));
|
|
14579
14558
|
program.command("serve", { isDefault: true }).description(`Start the stdio MCP server (DEFAULT \u2014 bare \`${PROGRAM_NAME}\` runs this).`).option("--state-dir <path>", "Override default state directory").option("--backend-url <url>", "SEAL backend base URL (overrides SEAL_MCP_BACKEND_URL; default https://api.seal.net)").option(
|
|
14580
14559
|
"--public-host <url>",
|
|
14581
14560
|
"Public origin for minted share URLs (overrides SEAL_MCP_PUBLIC_HOST; default https://seal.net). Set BOTH this and --backend-url for self-hosted deployments."
|
|
14582
|
-
).option("--ephemeral",
|
|
14561
|
+
).option("--ephemeral", `A new key each start, handles in memory only, nothing on disk (also ${EPHEMERAL_ENV_VAR}=1)`, false).action(async (opts) => runServe(opts));
|
|
14583
14562
|
program.command("pubkey").description(
|
|
14584
|
-
"Print
|
|
14563
|
+
"Print the agent's address, its X25519 public key (base64url), on stdout; its emoji and the key's source on stderr. Senders use it as the `--to` of `seal send` to deliver targeted SEALs."
|
|
14585
14564
|
).option("--state-dir <path>", "Override default state directory").action(async (opts) => runPubkey(opts));
|
|
14565
|
+
program.command("seed").description(`Print the agent's secret key, to give another machine the same address through ${SEED_ENV_VAR}.`).option("--state-dir <path>", "Override default state directory").option("--force", "Confirm printing the secret key", false).action(async (opts) => runSeed(opts));
|
|
14586
14566
|
program.command("doctor").description(
|
|
14587
|
-
"Diagnose keychain availability,
|
|
14567
|
+
"Diagnose the agent's key, keychain availability, file permissions and backend reachability. Prints a JSON report to stdout + hints to stderr."
|
|
14588
14568
|
).option("--state-dir <path>", "Override default state directory").option("--backend-url <url>", "Backend URL to probe").option("--public-host <url>", "Public share-URL origin to report (overrides SEAL_MCP_PUBLIC_HOST)").action(async (opts) => runDoctor(opts));
|
|
14589
14569
|
program.command("reset").description(
|
|
14590
|
-
"Wipe state.json
|
|
14570
|
+
"Wipe state.json and the agent's key (seed file and keychain entries): destroys all locally-tracked handles, and the address is lost for good."
|
|
14591
14571
|
).option("--state-dir <path>", "Override default state directory").option("--force", "Confirm destructive operation", false).action(async (opts) => runReset(opts));
|
|
14592
14572
|
return program;
|
|
14593
14573
|
}
|