sealnet-mcp 0.2.4 → 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 +20 -17
- package/dist/cli.cjs +980 -997
- package/dist/cli.d.cts +10 -7
- package/dist/cli.d.ts +10 -7
- package/dist/cli.js +3719 -3736
- package/dist/index.cjs +357 -276
- package/dist/index.d.cts +662 -178
- package/dist/index.d.ts +662 -178
- package/dist/index.js +335 -262
- package/package.json +2 -2
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) => {
|
|
@@ -11371,13 +11664,13 @@ var seal_mcp_v1_default = {
|
|
|
11371
11664
|
progress: "When a `tools/call` carries `_meta.progressToken`, `seal_share` and `seal_open mode=file` send `notifications/progress` with `progress` = ciphertext bytes done (upload: confirmed by storage) and `total` = ciphertext bytes in all; at most one per second, `progress` strictly increasing, the last one at `progress = total`."
|
|
11372
11665
|
},
|
|
11373
11666
|
server_instructions: {
|
|
11374
|
-
text: "SEAL moves files and secrets between people and agents without their content entering this conversation. When someone wants a file, folder or secret sent to them, to another machine or to another agent, call seal_share with its path and do not open, print or Read it first. When you need an API key, password, file or folder from the user, or they offer or ask how to give you one, call seal_request right away instead of explaining other ways or asking them to paste or upload it. Open a SEAL link someone gave you with seal_open (mode=file for secrets and large files).",
|
|
11667
|
+
text: "SEAL moves files and secrets between people and agents without their content entering this conversation. When someone wants a file, folder or secret sent to them, to another machine or to another agent, call seal_share with its path and do not open, print or Read it first. When you need an API key, password, file or folder from the user, or they offer or ask how to give you one, call seal_request right away instead of explaining other ways or asking them to paste or upload it. Open a SEAL link someone gave you with seal_open (mode=file for secrets and large files). Never read or print a SEAL handoff file.",
|
|
11375
11668
|
note: "The `instructions` of the `initialize` result (SPEC-AGENTS \xA78.1). Hosts that defer MCP tools behind a tool search (Claude Code) show the model only tool names until it searches, but put these instructions into the system prompt: this is the one text a model reads before it decides to look for a tool, or reads the file it was asked to send."
|
|
11376
11669
|
},
|
|
11377
11670
|
tools: {
|
|
11378
11671
|
seal_share: {
|
|
11379
11672
|
title: "Share a file via SEAL",
|
|
11380
|
-
description: "Give the user or another agent a file or a secret WITHOUT it passing through this chat. Works for
|
|
11673
|
+
description: "Give the user or another agent a file or a secret WITHOUT it passing through this chat. Works for large files (folders, dumps, exports, builds, videos \u2014 up to 1 GB free, more after one card payment by the user). Default mode `handoff`: the link goes to the user's clipboard and you get a handle. Where there is no clipboard it goes to a 0600 file and you get its path: do not read that file and do not print it. Mode `forward`: a link only the holder of the given X25519 key can open \u2014 use it for another agent. Never paste secrets or huge content into the chat instead.",
|
|
11381
11674
|
annotations: { destructiveHint: false, openWorldHint: true },
|
|
11382
11675
|
input: {
|
|
11383
11676
|
properties: {
|
|
@@ -11421,13 +11714,15 @@ 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." }
|
|
11428
11721
|
},
|
|
11429
11722
|
MUST_NOT_contain: ["share_url", "url", "password", "passphrase", "link_key", "owner_token"],
|
|
11430
|
-
rationale: "Anything in this object reaches the model. Any field that lets the model reconstruct or re-issue the share capability defeats handoff mode."
|
|
11723
|
+
rationale: "Anything in this object reaches the model. Any field that lets the model reconstruct or re-issue the share capability defeats handoff mode.",
|
|
11724
|
+
receipt_clipboard: "The seal link is on your clipboard. Paste it to the recipient.",
|
|
11725
|
+
receipt_file: "The seal link is in {file} (mode 0600, removed automatically later). Give it to the recipient. Anyone who can read files on this machine as this user, this agent included, can read it. In a cloud or container session prefer mode=forward to the recipient\u2019s key."
|
|
11431
11726
|
},
|
|
11432
11727
|
output_forward: {
|
|
11433
11728
|
type: "object",
|
|
@@ -11755,7 +12050,7 @@ var seal_mcp_v1_default = {
|
|
|
11755
12050
|
type: "object",
|
|
11756
12051
|
required: ["x25519_secret_b64", "x25519_public_b64"],
|
|
11757
12052
|
properties: {
|
|
11758
|
-
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." },
|
|
11759
12054
|
x25519_public_b64: { type: "string", description: "32 raw bytes, derived from secret. Safe to publish; advertised so other parties can target this MCP." }
|
|
11760
12055
|
}
|
|
11761
12056
|
},
|
|
@@ -11776,7 +12071,7 @@ var seal_mcp_v1_default = {
|
|
|
11776
12071
|
},
|
|
11777
12072
|
pending: {
|
|
11778
12073
|
type: "object",
|
|
11779
|
-
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.",
|
|
11780
12075
|
additionalProperties: {
|
|
11781
12076
|
type: "object",
|
|
11782
12077
|
required: ["tool", "mode", "started_at", "owner_token_b64", "expires_at", "step"],
|
|
@@ -11789,6 +12084,12 @@ var seal_mcp_v1_default = {
|
|
|
11789
12084
|
step: { type: "string", enum: ["preparing", "awaiting_payment", "uploaded", "delivering", "compensating"] },
|
|
11790
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." },
|
|
11791
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
|
+
},
|
|
11792
12093
|
compensation: {
|
|
11793
12094
|
type: "object",
|
|
11794
12095
|
required: ["action", "outcome", "error", "at"],
|
|
@@ -11818,29 +12119,22 @@ var seal_mcp_v1_default = {
|
|
|
11818
12119
|
}
|
|
11819
12120
|
}
|
|
11820
12121
|
}
|
|
11821
|
-
}
|
|
11822
|
-
},
|
|
11823
|
-
|
|
11824
|
-
|
|
11825
|
-
|
|
11826
|
-
|
|
11827
|
-
|
|
11828
|
-
|
|
11829
|
-
default: true
|
|
11830
|
-
},
|
|
11831
|
-
{
|
|
11832
|
-
rank: 2,
|
|
11833
|
-
method: "env_var",
|
|
11834
|
-
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."
|
|
11835
|
-
},
|
|
11836
|
-
{
|
|
11837
|
-
rank: 3,
|
|
11838
|
-
method: "REFUSED",
|
|
11839
|
-
implementation: "Cleartext passphrase file on disk is explicitly refused even if user passes `--passphrase-file \u2026`. Error message points at options 1 and 2."
|
|
11840
|
-
}
|
|
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." }
|
|
11841
12130
|
],
|
|
11842
|
-
|
|
11843
|
-
|
|
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."
|
|
11844
12138
|
},
|
|
11845
12139
|
handoff_delivery: {
|
|
11846
12140
|
primary_channel: "system_clipboard",
|
|
@@ -11916,11 +12210,12 @@ var seal_mcp_v1_default = {
|
|
|
11916
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.",
|
|
11917
12211
|
pro_failed: "seal_pro_*: SEAL Pro answered with another error; the message has the status.",
|
|
11918
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.",
|
|
11919
12214
|
runtime_unsupported: "Node runtime is missing a global the bundle assumes (e.g. `globalThis.File` or `globalThis.Blob`). Upgrade to Node \u226520.",
|
|
11920
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.",
|
|
11921
|
-
|
|
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`.",
|
|
11922
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.",
|
|
11923
|
-
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.",
|
|
11924
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."
|
|
11925
12220
|
},
|
|
11926
12221
|
current_format_version: 1,
|
|
@@ -11947,7 +12242,7 @@ function safeFilename(name) {
|
|
|
11947
12242
|
return cleaned.length > 0 ? cleaned : "seal-file";
|
|
11948
12243
|
}
|
|
11949
12244
|
async function receiveToDisk(opened, dir, name) {
|
|
11950
|
-
const
|
|
12245
|
+
const fs4 = await import(
|
|
11951
12246
|
/* webpackIgnore: true */
|
|
11952
12247
|
/* @vite-ignore */
|
|
11953
12248
|
"fs/promises"
|
|
@@ -11957,10 +12252,10 @@ async function receiveToDisk(opened, dir, name) {
|
|
|
11957
12252
|
/* @vite-ignore */
|
|
11958
12253
|
"path"
|
|
11959
12254
|
);
|
|
11960
|
-
await
|
|
12255
|
+
await fs4.mkdir(dir, { recursive: true });
|
|
11961
12256
|
const suffix = Array.from(crypto.getRandomValues(new Uint8Array(6)), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
11962
12257
|
const tmp = path.join(dir, `.seal-partial-${suffix}`);
|
|
11963
|
-
const fh = await
|
|
12258
|
+
const fh = await fs4.open(tmp, "wx", 384);
|
|
11964
12259
|
let placed = false;
|
|
11965
12260
|
try {
|
|
11966
12261
|
await opened.stream.pipeTo(
|
|
@@ -11978,12 +12273,12 @@ async function receiveToDisk(opened, dir, name) {
|
|
|
11978
12273
|
for (let i = 0; i < 1e3; i++) {
|
|
11979
12274
|
const candidate = path.join(dir, i === 0 ? name : `${stem} (${i})${ext}`);
|
|
11980
12275
|
try {
|
|
11981
|
-
await
|
|
12276
|
+
await fs4.link(tmp, candidate);
|
|
11982
12277
|
} catch (err2) {
|
|
11983
12278
|
if (err2.code === "EEXIST") continue;
|
|
11984
12279
|
throw err2;
|
|
11985
12280
|
}
|
|
11986
|
-
await
|
|
12281
|
+
await fs4.unlink(tmp);
|
|
11987
12282
|
placed = true;
|
|
11988
12283
|
return { path: candidate, sizeBytes, sha256: sha2562 };
|
|
11989
12284
|
}
|
|
@@ -11991,7 +12286,7 @@ async function receiveToDisk(opened, dir, name) {
|
|
|
11991
12286
|
} finally {
|
|
11992
12287
|
if (!placed) {
|
|
11993
12288
|
await fh.close().catch(() => void 0);
|
|
11994
|
-
await
|
|
12289
|
+
await fs4.unlink(tmp).catch(() => void 0);
|
|
11995
12290
|
}
|
|
11996
12291
|
}
|
|
11997
12292
|
}
|
|
@@ -12351,320 +12646,102 @@ async function loadWorkloadCredentials(env = process.env) {
|
|
|
12351
12646
|
const path = workloadCredentialsPath(env);
|
|
12352
12647
|
let text;
|
|
12353
12648
|
try {
|
|
12354
|
-
text = await (0, import_promises2.readFile)(path, "utf-8");
|
|
12355
|
-
} catch (err2) {
|
|
12356
|
-
if (err2.code === "ENOENT") return null;
|
|
12357
|
-
throw err2;
|
|
12358
|
-
}
|
|
12359
|
-
const file = JSON.parse(text);
|
|
12360
|
-
if (file.version !== 1 || !file.workload_id || !file.base_url || !file.seed_b64) {
|
|
12361
|
-
throw new Error(`${path} is not a SEAL Pro workload credentials file`);
|
|
12362
|
-
}
|
|
12363
|
-
return { workloadId: file.workload_id, baseUrl: file.base_url, seedB64: file.seed_b64 };
|
|
12364
|
-
}
|
|
12365
|
-
|
|
12366
|
-
// src/recovery.ts
|
|
12367
|
-
function nowIso(now) {
|
|
12368
|
-
return (now?.() ?? /* @__PURE__ */ new Date()).toISOString();
|
|
12369
|
-
}
|
|
12370
|
-
async function recoverPendingOperations(view, client, opts = {}) {
|
|
12371
|
-
const log4 = opts.log ?? ((line) => void process.stderr.write(`${line}
|
|
12372
|
-
`));
|
|
12373
|
-
const report = { adopted: [], revoked: [], stillFailing: [], dropped: [] };
|
|
12374
|
-
let changed = false;
|
|
12375
|
-
for (const [handle, op] of Object.entries(view.pending)) {
|
|
12376
|
-
if (op.tool !== "seal_share") {
|
|
12377
|
-
log4(`[seal-mcp] recovery: skipping unknown pending op tool=${String(op.tool)} handle=${handle}`);
|
|
12378
|
-
continue;
|
|
12379
|
-
}
|
|
12380
|
-
if (op.seal_id === void 0) {
|
|
12381
|
-
delete view.pending[handle];
|
|
12382
|
-
report.dropped.push(handle);
|
|
12383
|
-
changed = true;
|
|
12384
|
-
log4(`[seal-mcp] recovery: dropped pre-upload journal entry ${handle} (no remote effect)`);
|
|
12385
|
-
continue;
|
|
12386
|
-
}
|
|
12387
|
-
if (op.step === "delivering") {
|
|
12388
|
-
adoptAsHandle(view, handle, op, opts.now);
|
|
12389
|
-
report.adopted.push(handle);
|
|
12390
|
-
changed = true;
|
|
12391
|
-
log4(
|
|
12392
|
-
`[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`
|
|
12393
|
-
);
|
|
12394
|
-
continue;
|
|
12395
|
-
}
|
|
12396
|
-
if (op.backend_url !== void 0 && op.backend_url !== client.baseUrl) {
|
|
12397
|
-
op.step = "compensating";
|
|
12398
|
-
op.compensation = {
|
|
12399
|
-
action: "revoke",
|
|
12400
|
-
outcome: "failed",
|
|
12401
|
-
error: `backend mismatch: op belongs to ${op.backend_url}, server configured for ${client.baseUrl}`,
|
|
12402
|
-
at: nowIso(opts.now)
|
|
12403
|
-
};
|
|
12404
|
-
report.stillFailing.push(handle);
|
|
12405
|
-
changed = true;
|
|
12406
|
-
log4(
|
|
12407
|
-
`[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`
|
|
12408
|
-
);
|
|
12409
|
-
continue;
|
|
12410
|
-
}
|
|
12411
|
-
try {
|
|
12412
|
-
await client.revoke(op.seal_id, op.owner_token_b64);
|
|
12413
|
-
delete view.pending[handle];
|
|
12414
|
-
report.revoked.push(handle);
|
|
12415
|
-
changed = true;
|
|
12416
|
-
log4(`[seal-mcp] recovery: revoked orphaned seal for ${handle} (step was '${op.step}')`);
|
|
12417
|
-
} catch (err2) {
|
|
12418
|
-
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
12419
|
-
op.step = "compensating";
|
|
12420
|
-
op.compensation = { action: "revoke", outcome: "failed", error: msg, at: nowIso(opts.now) };
|
|
12421
|
-
report.stillFailing.push(handle);
|
|
12422
|
-
changed = true;
|
|
12423
|
-
log4(
|
|
12424
|
-
`[seal-mcp] recovery: revoke retry for ${handle} failed (${msg}) \u2014 kept in journal, seal expires by TTL at ${op.expires_at}`
|
|
12425
|
-
);
|
|
12426
|
-
}
|
|
12427
|
-
}
|
|
12428
|
-
return { report, changed };
|
|
12429
|
-
}
|
|
12430
|
-
function adoptAsHandle(view, handle, op, now) {
|
|
12431
|
-
const sealId = op.seal_id;
|
|
12432
|
-
if (view.handles[handle] === void 0) {
|
|
12433
|
-
view.handles[handle] = {
|
|
12434
|
-
seal_id: sealId,
|
|
12435
|
-
owner_token_b64: op.owner_token_b64,
|
|
12436
|
-
created_at: op.started_at,
|
|
12437
|
-
mode: op.mode,
|
|
12438
|
-
expires_at: op.expires_at,
|
|
12439
|
-
// Propagate provenance so a later seal_revoke on the adopted
|
|
12440
|
-
// handle still refuses to target the wrong backend.
|
|
12441
|
-
backend_url: op.backend_url
|
|
12442
|
-
};
|
|
12443
|
-
view.audit.push({ ts: nowIso(now), tool: "seal_share", handle, mode: op.mode });
|
|
12444
|
-
}
|
|
12445
|
-
delete view.pending[handle];
|
|
12446
|
-
}
|
|
12447
|
-
|
|
12448
|
-
// src/state.ts
|
|
12449
|
-
var import_node_crypto3 = require("crypto");
|
|
12450
|
-
var import_node_fs2 = require("fs");
|
|
12451
|
-
var import_node_path3 = require("path");
|
|
12452
|
-
var import_argon2 = __toESM(require("argon2"), 1);
|
|
12453
|
-
var FORMAT_TAG = "SEAL_MCP_STATE v1";
|
|
12454
|
-
var STATE_VERSION = 1;
|
|
12455
|
-
var ARGON2_MEMORY_KIB = 65536;
|
|
12456
|
-
var ARGON2_ITERATIONS = 3;
|
|
12457
|
-
var ARGON2_PARALLELISM = 4;
|
|
12458
|
-
var ARGON2_SALT_BYTES = 16;
|
|
12459
|
-
var KDF_BOUNDS = {
|
|
12460
|
-
memory_kib: { min: 8192, max: 1048576 },
|
|
12461
|
-
// 8 MiB .. 1 GiB
|
|
12462
|
-
iterations: { min: 1, max: 16 },
|
|
12463
|
-
parallelism: { min: 1, max: 16 }
|
|
12464
|
-
};
|
|
12465
|
-
var AES_KEY_BYTES = 32;
|
|
12466
|
-
var AES_NONCE_BYTES = 12;
|
|
12467
|
-
var StatePassphraseInvalid = class extends Error {
|
|
12468
|
-
constructor() {
|
|
12469
|
-
super("state.passphrase_invalid: AES-256-GCM tag check failed");
|
|
12470
|
-
this.name = "StatePassphraseInvalid";
|
|
12471
|
-
}
|
|
12472
|
-
};
|
|
12473
|
-
var StateCorrupt = class extends Error {
|
|
12474
|
-
constructor(reason) {
|
|
12475
|
-
super(`state.corrupt: ${reason}`);
|
|
12476
|
-
this.name = "StateCorrupt";
|
|
12477
|
-
}
|
|
12478
|
-
};
|
|
12479
|
-
function toB642(bytes) {
|
|
12480
|
-
return Buffer.from(bytes).toString("base64");
|
|
12481
|
-
}
|
|
12482
|
-
function fromB642(s) {
|
|
12483
|
-
return new Uint8Array(Buffer.from(s, "base64"));
|
|
12484
|
-
}
|
|
12485
|
-
var CURRENT_KDF_PARAMS = {
|
|
12486
|
-
memory_kib: ARGON2_MEMORY_KIB,
|
|
12487
|
-
iterations: ARGON2_ITERATIONS,
|
|
12488
|
-
parallelism: ARGON2_PARALLELISM
|
|
12489
|
-
};
|
|
12490
|
-
function boundedKdfParam(name, value) {
|
|
12491
|
-
if (typeof value !== "number" || !Number.isInteger(value)) {
|
|
12492
|
-
throw new StateCorrupt(`kdf.${name} is not an integer: ${String(value)}`);
|
|
12493
|
-
}
|
|
12494
|
-
const { min, max } = KDF_BOUNDS[name];
|
|
12495
|
-
if (value < min || value > max) {
|
|
12496
|
-
throw new StateCorrupt(`kdf.${name}=${value} outside sane bounds [${min}, ${max}]`);
|
|
12497
|
-
}
|
|
12498
|
-
return value;
|
|
12499
|
-
}
|
|
12500
|
-
async function deriveKek(passphrase, saltB64, params) {
|
|
12501
|
-
const salt = fromB642(saltB64);
|
|
12502
|
-
if (salt.length !== ARGON2_SALT_BYTES) {
|
|
12503
|
-
throw new StateCorrupt(`salt length is ${salt.length}, expected ${ARGON2_SALT_BYTES}`);
|
|
12504
|
-
}
|
|
12505
|
-
const kek = await import_argon2.default.hash(passphrase, {
|
|
12506
|
-
type: import_argon2.default.argon2id,
|
|
12507
|
-
raw: true,
|
|
12508
|
-
salt: Buffer.from(salt),
|
|
12509
|
-
memoryCost: params.memory_kib,
|
|
12510
|
-
timeCost: params.iterations,
|
|
12511
|
-
parallelism: params.parallelism,
|
|
12512
|
-
hashLength: AES_KEY_BYTES
|
|
12513
|
-
});
|
|
12514
|
-
return new Uint8Array(kek);
|
|
12515
|
-
}
|
|
12516
|
-
async function aesGcmEncrypt(kek, nonce, plaintext) {
|
|
12517
|
-
const key = await import_node_crypto3.webcrypto.subtle.importKey("raw", kek, "AES-GCM", false, ["encrypt"]);
|
|
12518
|
-
const ct = await import_node_crypto3.webcrypto.subtle.encrypt(
|
|
12519
|
-
{ name: "AES-GCM", iv: nonce, tagLength: 128 },
|
|
12520
|
-
key,
|
|
12521
|
-
plaintext
|
|
12522
|
-
);
|
|
12523
|
-
return new Uint8Array(ct);
|
|
12524
|
-
}
|
|
12525
|
-
async function aesGcmDecrypt(kek, nonce, ciphertextWithTag) {
|
|
12526
|
-
const key = await import_node_crypto3.webcrypto.subtle.importKey("raw", kek, "AES-GCM", false, ["decrypt"]);
|
|
12527
|
-
try {
|
|
12528
|
-
const pt = await import_node_crypto3.webcrypto.subtle.decrypt(
|
|
12529
|
-
{ name: "AES-GCM", iv: nonce, tagLength: 128 },
|
|
12530
|
-
key,
|
|
12531
|
-
ciphertextWithTag
|
|
12532
|
-
);
|
|
12533
|
-
return new Uint8Array(pt);
|
|
12534
|
-
} catch {
|
|
12535
|
-
throw new StatePassphraseInvalid();
|
|
12536
|
-
}
|
|
12537
|
-
}
|
|
12538
|
-
function createEmptyState(identity) {
|
|
12539
|
-
return {
|
|
12540
|
-
version: STATE_VERSION,
|
|
12541
|
-
identity,
|
|
12542
|
-
handles: {},
|
|
12543
|
-
audit: [],
|
|
12544
|
-
pending: {}
|
|
12545
|
-
};
|
|
12546
|
-
}
|
|
12547
|
-
function validateEnvelope(json) {
|
|
12548
|
-
if (typeof json !== "object" || json === null) {
|
|
12549
|
-
throw new StateCorrupt("envelope is not an object");
|
|
12550
|
-
}
|
|
12551
|
-
const e = json;
|
|
12552
|
-
if (e.version !== STATE_VERSION) {
|
|
12553
|
-
throw new StateCorrupt(`unknown version: ${String(e.version)}`);
|
|
12554
|
-
}
|
|
12555
|
-
if (e.format !== FORMAT_TAG) {
|
|
12556
|
-
throw new StateCorrupt(`unknown format tag: ${String(e.format)}`);
|
|
12557
|
-
}
|
|
12558
|
-
const kdf = e.kdf;
|
|
12559
|
-
if (!kdf || kdf.algo !== "argon2id" || typeof kdf.salt_b64 !== "string") {
|
|
12560
|
-
throw new StateCorrupt("kdf header missing or wrong algo");
|
|
12561
|
-
}
|
|
12562
|
-
const cipher = e.cipher;
|
|
12563
|
-
if (!cipher || cipher.algo !== "aes-256-gcm" || typeof cipher.nonce_b64 !== "string" || typeof cipher.ciphertext_b64 !== "string") {
|
|
12564
|
-
throw new StateCorrupt("cipher header missing or wrong algo");
|
|
12565
|
-
}
|
|
12566
|
-
}
|
|
12567
|
-
function validatePlaintext(json) {
|
|
12568
|
-
if (typeof json !== "object" || json === null) {
|
|
12569
|
-
throw new StateCorrupt("plaintext is not an object");
|
|
12570
|
-
}
|
|
12571
|
-
const s = json;
|
|
12572
|
-
if (s.version !== STATE_VERSION) {
|
|
12573
|
-
throw new StateCorrupt(`plaintext version: ${String(s.version)}`);
|
|
12574
|
-
}
|
|
12575
|
-
const id = s.identity;
|
|
12576
|
-
if (!id || typeof id.x25519_secret_b64 !== "string" || typeof id.x25519_public_b64 !== "string") {
|
|
12577
|
-
throw new StateCorrupt("identity missing or malformed");
|
|
12578
|
-
}
|
|
12579
|
-
if (typeof s.handles !== "object" || s.handles === null) {
|
|
12580
|
-
throw new StateCorrupt("handles is not an object");
|
|
12581
|
-
}
|
|
12582
|
-
if (!Array.isArray(s.audit)) {
|
|
12583
|
-
throw new StateCorrupt("audit is not an array");
|
|
12584
|
-
}
|
|
12585
|
-
if (s.pending !== void 0 && (typeof s.pending !== "object" || s.pending === null)) {
|
|
12586
|
-
throw new StateCorrupt("pending is not an object");
|
|
12587
|
-
}
|
|
12588
|
-
}
|
|
12589
|
-
async function loadState(passphrase, statePath) {
|
|
12590
|
-
const raw = await import_node_fs2.promises.readFile(statePath, "utf8");
|
|
12591
|
-
let envelope;
|
|
12592
|
-
try {
|
|
12593
|
-
envelope = JSON.parse(raw);
|
|
12594
|
-
} catch (err2) {
|
|
12595
|
-
throw new StateCorrupt(`envelope is not valid JSON: ${err2.message}`);
|
|
12596
|
-
}
|
|
12597
|
-
validateEnvelope(envelope);
|
|
12598
|
-
const kek = await deriveKek(passphrase, envelope.kdf.salt_b64, {
|
|
12599
|
-
memory_kib: boundedKdfParam("memory_kib", envelope.kdf.memory_kib),
|
|
12600
|
-
iterations: boundedKdfParam("iterations", envelope.kdf.iterations),
|
|
12601
|
-
parallelism: boundedKdfParam("parallelism", envelope.kdf.parallelism)
|
|
12602
|
-
});
|
|
12603
|
-
const nonce = fromB642(envelope.cipher.nonce_b64);
|
|
12604
|
-
if (nonce.length !== AES_NONCE_BYTES) {
|
|
12605
|
-
throw new StateCorrupt(`nonce length is ${nonce.length}, expected ${AES_NONCE_BYTES}`);
|
|
12606
|
-
}
|
|
12607
|
-
const ct = fromB642(envelope.cipher.ciphertext_b64);
|
|
12608
|
-
const ptBytes = await aesGcmDecrypt(kek, nonce, ct);
|
|
12609
|
-
const ptText = new TextDecoder("utf-8", { fatal: true }).decode(ptBytes);
|
|
12610
|
-
let plaintext;
|
|
12611
|
-
try {
|
|
12612
|
-
plaintext = JSON.parse(ptText);
|
|
12649
|
+
text = await (0, import_promises2.readFile)(path, "utf-8");
|
|
12613
12650
|
} catch (err2) {
|
|
12614
|
-
|
|
12651
|
+
if (err2.code === "ENOENT") return null;
|
|
12652
|
+
throw err2;
|
|
12615
12653
|
}
|
|
12616
|
-
|
|
12617
|
-
if (
|
|
12618
|
-
|
|
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`);
|
|
12619
12657
|
}
|
|
12620
|
-
return
|
|
12658
|
+
return { workloadId: file.workload_id, baseUrl: file.base_url, seedB64: file.seed_b64 };
|
|
12621
12659
|
}
|
|
12622
|
-
|
|
12623
|
-
|
|
12624
|
-
|
|
12625
|
-
|
|
12626
|
-
|
|
12627
|
-
|
|
12628
|
-
const
|
|
12629
|
-
|
|
12630
|
-
|
|
12631
|
-
|
|
12632
|
-
|
|
12633
|
-
|
|
12634
|
-
|
|
12635
|
-
|
|
12636
|
-
|
|
12637
|
-
|
|
12638
|
-
|
|
12639
|
-
|
|
12640
|
-
|
|
12641
|
-
|
|
12642
|
-
|
|
12643
|
-
|
|
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;
|
|
12644
12709
|
}
|
|
12645
|
-
};
|
|
12646
|
-
const dir = (0, import_node_path3.dirname)(statePath);
|
|
12647
|
-
await import_node_fs2.promises.mkdir(dir, { recursive: true, mode: 448 });
|
|
12648
|
-
const tmp = `${statePath}.tmp-${process.pid}-${Date.now()}`;
|
|
12649
|
-
const fh = await import_node_fs2.promises.open(tmp, "w", 384);
|
|
12650
|
-
try {
|
|
12651
|
-
await fh.writeFile(JSON.stringify(envelope), "utf8");
|
|
12652
|
-
await fh.sync();
|
|
12653
|
-
} finally {
|
|
12654
|
-
await fh.close();
|
|
12655
|
-
}
|
|
12656
|
-
await import_node_fs2.promises.rename(tmp, statePath);
|
|
12657
|
-
try {
|
|
12658
|
-
const dh = await import_node_fs2.promises.open(dir, "r");
|
|
12659
12710
|
try {
|
|
12660
|
-
await
|
|
12661
|
-
|
|
12662
|
-
|
|
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
|
+
);
|
|
12663
12725
|
}
|
|
12664
|
-
} catch {
|
|
12665
12726
|
}
|
|
12666
|
-
|
|
12667
|
-
|
|
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];
|
|
12668
12745
|
}
|
|
12669
12746
|
|
|
12670
12747
|
// src/tools/context.ts
|
|
@@ -12683,19 +12760,18 @@ var HANDLE_REGEX = /^h_[A-Za-z0-9_-]{12,}$/;
|
|
|
12683
12760
|
function isValidHandle(handle) {
|
|
12684
12761
|
return typeof handle === "string" && HANDLE_REGEX.test(handle);
|
|
12685
12762
|
}
|
|
12763
|
+
function auditEntry(ctx, tool, handle, mode) {
|
|
12764
|
+
return { ts: (ctx.now?.() ?? /* @__PURE__ */ new Date()).toISOString(), tool, handle, mode };
|
|
12765
|
+
}
|
|
12686
12766
|
function appendAudit(ctx, tool, handle, mode) {
|
|
12687
|
-
ctx
|
|
12688
|
-
|
|
12689
|
-
tool,
|
|
12690
|
-
handle,
|
|
12691
|
-
mode
|
|
12692
|
-
});
|
|
12767
|
+
const entry = auditEntry(ctx, tool, handle, mode);
|
|
12768
|
+
return ctx.update((state) => void state.audit.push(entry));
|
|
12693
12769
|
}
|
|
12694
|
-
function insertHandle(
|
|
12695
|
-
if (
|
|
12770
|
+
function insertHandle(state, handle, entry) {
|
|
12771
|
+
if (state.handles[handle]) {
|
|
12696
12772
|
throw new Error(`insertHandle: collision on ${handle}`);
|
|
12697
12773
|
}
|
|
12698
|
-
|
|
12774
|
+
state.handles[handle] = entry;
|
|
12699
12775
|
}
|
|
12700
12776
|
function lookupHandle(ctx, handle) {
|
|
12701
12777
|
const entry = ctx.state.handles[handle];
|
|
@@ -12718,6 +12794,7 @@ var ToolError = class extends Error {
|
|
|
12718
12794
|
// src/tools/list.ts
|
|
12719
12795
|
var SINGLE_FILE_COUNT = 1;
|
|
12720
12796
|
async function sealList(ctx, input = {}) {
|
|
12797
|
+
await appendAudit(ctx, "seal_list", null, null);
|
|
12721
12798
|
const now = (ctx.now?.() ?? /* @__PURE__ */ new Date()).getTime();
|
|
12722
12799
|
const entries = [];
|
|
12723
12800
|
for (const [handle, entry] of Object.entries(ctx.state.handles)) {
|
|
@@ -12734,15 +12811,13 @@ async function sealList(ctx, input = {}) {
|
|
|
12734
12811
|
});
|
|
12735
12812
|
}
|
|
12736
12813
|
entries.sort((a, b) => Date.parse(b.created_at) - Date.parse(a.created_at));
|
|
12737
|
-
appendAudit(ctx, "seal_list", null, null);
|
|
12738
|
-
await ctx.persist();
|
|
12739
12814
|
return { entries };
|
|
12740
12815
|
}
|
|
12741
12816
|
|
|
12742
12817
|
// src/tools/open.ts
|
|
12743
12818
|
var import_promises3 = require("fs/promises");
|
|
12744
12819
|
var import_node_os3 = require("os");
|
|
12745
|
-
var
|
|
12820
|
+
var import_node_path5 = require("path");
|
|
12746
12821
|
var INLINE_ALLOWED_MIMES = ["application/json", "text/csv", "text/plain"];
|
|
12747
12822
|
var INLINE_INJECTION_RISK_MIMES = /* @__PURE__ */ new Set([
|
|
12748
12823
|
"text/html",
|
|
@@ -12820,13 +12895,11 @@ async function sealOpen(ctx, input) {
|
|
|
12820
12895
|
if (mode === "inline") {
|
|
12821
12896
|
const refusal = classifyInlineRefusal(first.mime);
|
|
12822
12897
|
if (refusal !== null) {
|
|
12823
|
-
appendAudit(ctx, "seal_open", handle, mode);
|
|
12824
|
-
await ctx.persist();
|
|
12898
|
+
await appendAudit(ctx, "seal_open", handle, mode);
|
|
12825
12899
|
return { handle, refusal, mime: first.mime, note: refusalNote(refusal, first.mime) };
|
|
12826
12900
|
}
|
|
12827
12901
|
if (first.size > INLINE_CIPHERTEXT_MAX_BYTES) {
|
|
12828
|
-
appendAudit(ctx, "seal_open", handle, mode);
|
|
12829
|
-
await ctx.persist();
|
|
12902
|
+
await appendAudit(ctx, "seal_open", handle, mode);
|
|
12830
12903
|
return {
|
|
12831
12904
|
handle,
|
|
12832
12905
|
refusal: "inline_blocked.too_large",
|
|
@@ -12862,12 +12935,10 @@ async function sealOpen(ctx, input) {
|
|
|
12862
12935
|
err2
|
|
12863
12936
|
);
|
|
12864
12937
|
}
|
|
12865
|
-
appendAudit(ctx, "seal_open", handle, mode);
|
|
12866
|
-
await ctx.persist();
|
|
12938
|
+
await appendAudit(ctx, "seal_open", handle, mode);
|
|
12867
12939
|
return { handle, mime: bareMime(first.mime), sizeBytes: plaintext.byteLength, encoding: "utf-8", content };
|
|
12868
12940
|
}
|
|
12869
|
-
appendAudit(ctx, "seal_open", handle, mode);
|
|
12870
|
-
await ctx.persist();
|
|
12941
|
+
await appendAudit(ctx, "seal_open", handle, mode);
|
|
12871
12942
|
return {
|
|
12872
12943
|
handle,
|
|
12873
12944
|
filename: first.name,
|
|
@@ -12889,7 +12960,7 @@ async function openToDisk(ctx, seal, handle, input) {
|
|
|
12889
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`
|
|
12890
12961
|
);
|
|
12891
12962
|
}
|
|
12892
|
-
const dir = (0,
|
|
12963
|
+
const dir = (0, import_node_path5.resolve)(input.dir ?? (0, import_node_path5.join)((0, import_node_os3.homedir)(), "Downloads"));
|
|
12893
12964
|
try {
|
|
12894
12965
|
await (0, import_promises3.mkdir)(dir, { recursive: true });
|
|
12895
12966
|
} catch (err2) {
|
|
@@ -12913,15 +12984,14 @@ async function openToDisk(ctx, seal, handle, input) {
|
|
|
12913
12984
|
throw streamError(err2, file.id);
|
|
12914
12985
|
}
|
|
12915
12986
|
}
|
|
12916
|
-
appendAudit(ctx, "seal_open", handle, "file");
|
|
12917
|
-
await ctx.persist();
|
|
12987
|
+
await appendAudit(ctx, "seal_open", handle, "file");
|
|
12918
12988
|
return { handle, files: out };
|
|
12919
12989
|
}
|
|
12920
12990
|
|
|
12921
12991
|
// src/tools/pro.ts
|
|
12922
12992
|
var import_promises4 = require("fs/promises");
|
|
12923
12993
|
var import_node_os4 = require("os");
|
|
12924
|
-
var
|
|
12994
|
+
var import_node_path6 = require("path");
|
|
12925
12995
|
var SECRET_CODES = {
|
|
12926
12996
|
not_found: "pro_secret_not_found",
|
|
12927
12997
|
not_granted: "pro_secret_not_granted",
|
|
@@ -12946,9 +13016,9 @@ async function asTool(fn, codes = SECRET_CODES) {
|
|
|
12946
13016
|
}
|
|
12947
13017
|
async function sealProSecretUse(source, input) {
|
|
12948
13018
|
const got = await asTool(() => source.getSecret(input.secret, input.version));
|
|
12949
|
-
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-"));
|
|
12950
13020
|
const name = (got.path?.split("/").pop() || got.id).replace(/[^A-Za-z0-9._-]/g, "_");
|
|
12951
|
-
const path = (0,
|
|
13021
|
+
const path = (0, import_node_path6.join)(dir, name);
|
|
12952
13022
|
try {
|
|
12953
13023
|
await (0, import_promises4.writeFile)(path, got.value, { mode: 384, flag: "wx" });
|
|
12954
13024
|
return { path, sizeBytes: got.value.length, secretId: got.id, version: got.version };
|
|
@@ -12962,7 +13032,7 @@ async function sealProSecretRequest(source, input) {
|
|
|
12962
13032
|
return asTool(() => source.waitForAccess(asked));
|
|
12963
13033
|
}
|
|
12964
13034
|
async function sealProFileGet(source, input) {
|
|
12965
|
-
const dir = (0,
|
|
13035
|
+
const dir = (0, import_node_path6.resolve)(input.dir ?? (0, import_node_path6.join)((0, import_node_os4.homedir)(), "Downloads"));
|
|
12966
13036
|
const ids = input.file ? [input.file] : (await asTool(() => source.listFiles(input.seal), FILE_CODES)).map((f) => f.id);
|
|
12967
13037
|
const files = [];
|
|
12968
13038
|
for (const id of ids) {
|
|
@@ -12974,16 +13044,16 @@ async function sealProFileGet(source, input) {
|
|
|
12974
13044
|
|
|
12975
13045
|
// src/tools/request.ts
|
|
12976
13046
|
var import_node_child_process2 = require("child_process");
|
|
12977
|
-
var
|
|
13047
|
+
var import_node_fs4 = require("fs");
|
|
12978
13048
|
var import_promises5 = require("fs/promises");
|
|
12979
13049
|
var import_node_os5 = require("os");
|
|
12980
|
-
var
|
|
13050
|
+
var import_node_path7 = require("path");
|
|
12981
13051
|
function findSealCli() {
|
|
12982
13052
|
const names = process.platform === "win32" ? ["seal.exe", "seal.cmd"] : ["seal"];
|
|
12983
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
13053
|
+
for (const dir of (process.env.PATH ?? "").split(import_node_path7.delimiter)) {
|
|
12984
13054
|
for (const name of names) {
|
|
12985
|
-
const candidate = (0,
|
|
12986
|
-
if (dir && (0,
|
|
13055
|
+
const candidate = (0, import_node_path7.join)(dir, name);
|
|
13056
|
+
if (dir && (0, import_node_fs4.existsSync)(candidate)) return candidate;
|
|
12987
13057
|
}
|
|
12988
13058
|
}
|
|
12989
13059
|
return null;
|
|
@@ -13017,8 +13087,7 @@ async function sealRequest(ctx, input, deps) {
|
|
|
13017
13087
|
if (input.whereUrl) cliArgs.push("--where", input.whereUrl);
|
|
13018
13088
|
const local = await (deps.runCli ?? runSealCli)(cliArgs);
|
|
13019
13089
|
if (local && "files" in local) {
|
|
13020
|
-
appendAudit(ctx, "seal_request", handle, "local");
|
|
13021
|
-
await ctx.persist();
|
|
13090
|
+
await appendAudit(ctx, "seal_request", handle, "local");
|
|
13022
13091
|
return {
|
|
13023
13092
|
handle,
|
|
13024
13093
|
source: "local",
|
|
@@ -13058,8 +13127,7 @@ async function sealRequest(ctx, input, deps) {
|
|
|
13058
13127
|
}
|
|
13059
13128
|
return receiveThroughLink(ctx, handle, intake.id, deps, input.onProgress);
|
|
13060
13129
|
}
|
|
13061
|
-
appendAudit(ctx, "seal_request", handle, "link");
|
|
13062
|
-
await ctx.persist();
|
|
13130
|
+
await appendAudit(ctx, "seal_request", handle, "link");
|
|
13063
13131
|
return { handle, url, emoji, expiresAt: intake.expires_at };
|
|
13064
13132
|
}
|
|
13065
13133
|
async function receiveThroughLink(ctx, handle, intakeId, deps, onProgress) {
|
|
@@ -13076,12 +13144,11 @@ async function receiveThroughLink(ctx, handle, intakeId, deps, onProgress) {
|
|
|
13076
13144
|
);
|
|
13077
13145
|
}
|
|
13078
13146
|
await deps.elicitationDone?.(intakeId).catch(() => void 0);
|
|
13079
|
-
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-"));
|
|
13080
13148
|
const opened = await sealOpen(ctx, { url: `${deps.publicHost}${sharePath}`, mode: "file", dir, onProgress });
|
|
13081
13149
|
if (!("files" in opened)) throw new ToolError("internal_inconsistency", "seal_open mode=file returned no files");
|
|
13082
13150
|
ctx.requests?.delete(handle);
|
|
13083
|
-
appendAudit(ctx, "seal_request", handle, "link");
|
|
13084
|
-
await ctx.persist();
|
|
13151
|
+
await appendAudit(ctx, "seal_request", handle, "link");
|
|
13085
13152
|
return {
|
|
13086
13153
|
handle,
|
|
13087
13154
|
source: "link",
|
|
@@ -13094,6 +13161,7 @@ async function sealRevoke(ctx, input) {
|
|
|
13094
13161
|
if (!isValidHandle(input.handle)) {
|
|
13095
13162
|
throw new ToolError("handle_invalid", `handle does not match contract pattern: ${input.handle}`);
|
|
13096
13163
|
}
|
|
13164
|
+
await ctx.update(() => false);
|
|
13097
13165
|
const entry = lookupHandle(ctx, input.handle);
|
|
13098
13166
|
const now = (ctx.now?.() ?? /* @__PURE__ */ new Date()).toISOString();
|
|
13099
13167
|
if (entry.revoked_at) {
|
|
@@ -13128,16 +13196,18 @@ async function sealRevoke(ctx, input) {
|
|
|
13128
13196
|
err2
|
|
13129
13197
|
);
|
|
13130
13198
|
}
|
|
13131
|
-
|
|
13132
|
-
|
|
13133
|
-
|
|
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
|
+
});
|
|
13134
13203
|
return { revoked: true, revokedAt: now };
|
|
13135
13204
|
}
|
|
13136
13205
|
|
|
13137
13206
|
// src/tools/share.ts
|
|
13138
|
-
var
|
|
13207
|
+
var import_node_fs6 = require("fs");
|
|
13139
13208
|
var import_promises9 = require("fs/promises");
|
|
13140
|
-
var
|
|
13209
|
+
var import_node_os8 = require("os");
|
|
13210
|
+
var import_node_path10 = require("path");
|
|
13141
13211
|
|
|
13142
13212
|
// ../../contracts/limits.json
|
|
13143
13213
|
var limits_default2 = {
|
|
@@ -13171,7 +13241,7 @@ var limits_default2 = {
|
|
|
13171
13241
|
// src/handoff.ts
|
|
13172
13242
|
var import_promises6 = require("fs/promises");
|
|
13173
13243
|
var import_node_os6 = require("os");
|
|
13174
|
-
var
|
|
13244
|
+
var import_node_path8 = require("path");
|
|
13175
13245
|
var ONE_HOUR_MS = 36e5;
|
|
13176
13246
|
var HandoffBothChannelsFailedError = class extends Error {
|
|
13177
13247
|
code = "handoff_both_channels_failed";
|
|
@@ -13187,10 +13257,10 @@ var HandoffBothChannelsFailedError = class extends Error {
|
|
|
13187
13257
|
function handoffDir(deps) {
|
|
13188
13258
|
const xdg = process.env.XDG_RUNTIME_DIR;
|
|
13189
13259
|
const base = deps.tmpdirOverride ?? (xdg && xdg.length > 0 ? xdg : (0, import_node_os6.tmpdir)());
|
|
13190
|
-
return (0,
|
|
13260
|
+
return (0, import_node_path8.join)(base, "seal-mcp");
|
|
13191
13261
|
}
|
|
13192
13262
|
function handoffPath(deps, handle) {
|
|
13193
|
-
return (0,
|
|
13263
|
+
return (0, import_node_path8.join)(handoffDir(deps), `handoff-${handle}.txt`);
|
|
13194
13264
|
}
|
|
13195
13265
|
async function writeClipboard(text) {
|
|
13196
13266
|
const mod = await import("clipboardy");
|
|
@@ -13257,10 +13327,10 @@ async function performHandoff(payload, deps = {}) {
|
|
|
13257
13327
|
}
|
|
13258
13328
|
|
|
13259
13329
|
// src/zip.ts
|
|
13260
|
-
var
|
|
13330
|
+
var import_node_fs5 = require("fs");
|
|
13261
13331
|
var import_promises7 = require("fs/promises");
|
|
13262
13332
|
var import_node_os7 = require("os");
|
|
13263
|
-
var
|
|
13333
|
+
var import_node_path9 = require("path");
|
|
13264
13334
|
var import_node_stream = require("stream");
|
|
13265
13335
|
var import_promises8 = require("stream/promises");
|
|
13266
13336
|
var ZIP32_MAX_BYTES = 4294967295;
|
|
@@ -13268,16 +13338,16 @@ async function collectFiles(dir) {
|
|
|
13268
13338
|
const found = [];
|
|
13269
13339
|
const walk = async (current) => {
|
|
13270
13340
|
for (const dirent of await (0, import_promises7.readdir)(current, { withFileTypes: true })) {
|
|
13271
|
-
const abs = (0,
|
|
13341
|
+
const abs = (0, import_node_path9.join)(current, dirent.name);
|
|
13272
13342
|
if (dirent.isDirectory()) await walk(abs);
|
|
13273
|
-
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 });
|
|
13274
13344
|
}
|
|
13275
13345
|
};
|
|
13276
13346
|
await walk(dir);
|
|
13277
13347
|
found.sort((a, b) => a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0);
|
|
13278
13348
|
return Promise.all(
|
|
13279
13349
|
found.map(async ({ rel, abs }) => {
|
|
13280
|
-
const file = new File([await (0,
|
|
13350
|
+
const file = new File([await (0, import_node_fs5.openAsBlob)(abs)], (0, import_node_path9.basename)(abs));
|
|
13281
13351
|
return Object.defineProperty(file, "webkitRelativePath", { value: rel });
|
|
13282
13352
|
})
|
|
13283
13353
|
);
|
|
@@ -13287,19 +13357,19 @@ async function planFolderArchive(dir) {
|
|
|
13287
13357
|
if (files.length === 0) {
|
|
13288
13358
|
throw new Error(`directory contains no regular files: ${dir}`);
|
|
13289
13359
|
}
|
|
13290
|
-
const folderName = (0,
|
|
13360
|
+
const folderName = (0, import_node_path9.basename)((0, import_node_path9.resolve)(dir)) || "folder";
|
|
13291
13361
|
const size = estimateZipSize({ files, folderName });
|
|
13292
13362
|
if (size > ZIP32_MAX_BYTES) {
|
|
13293
13363
|
throw new Error(`the folder's archive would be ${size} bytes, over the ZIP limit of ${ZIP32_MAX_BYTES}`);
|
|
13294
13364
|
}
|
|
13295
13365
|
const write = async () => {
|
|
13296
|
-
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-"));
|
|
13297
13367
|
const cleanup = () => (0, import_promises7.rm)(tempDir, { recursive: true, force: true });
|
|
13298
|
-
const path = (0,
|
|
13368
|
+
const path = (0, import_node_path9.join)(tempDir, `${folderName}.zip`);
|
|
13299
13369
|
try {
|
|
13300
13370
|
await (0, import_promises8.pipeline)(
|
|
13301
13371
|
import_node_stream.Readable.fromWeb(createZipStream2({ files, folderName })),
|
|
13302
|
-
(0,
|
|
13372
|
+
(0, import_node_fs5.createWriteStream)(path, { mode: 384 })
|
|
13303
13373
|
);
|
|
13304
13374
|
const written = (await (0, import_promises7.stat)(path)).size;
|
|
13305
13375
|
if (written !== size) {
|
|
@@ -13363,6 +13433,7 @@ function guessMimeFromName(name) {
|
|
|
13363
13433
|
}
|
|
13364
13434
|
var VALID_TTL = ["5m", "30m", "1h", "1d", "7d"];
|
|
13365
13435
|
var EXPIRE_DEFAULT = seal_mcp_v1_default.tools.seal_share.expire_default;
|
|
13436
|
+
var HANDOFF_OUTPUT = seal_mcp_v1_default.tools.seal_share.output_handoff;
|
|
13366
13437
|
var TIER_BYTES2 = limits_default2.limits.TIER_BYTES;
|
|
13367
13438
|
var PAYMENT_RECEIPT = "The file is larger than 1 GB. Show the user the payment link; the upload continues after payment. Then call seal_share with `payment` set to this handle.";
|
|
13368
13439
|
function defaultExpire(mode, size) {
|
|
@@ -13390,31 +13461,46 @@ var FORWARD_MAX_READS_FORCED = 1;
|
|
|
13390
13461
|
function nowIso2(ctx) {
|
|
13391
13462
|
return (ctx.now?.() ?? /* @__PURE__ */ new Date()).toISOString();
|
|
13392
13463
|
}
|
|
13393
|
-
|
|
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) {
|
|
13394
13482
|
try {
|
|
13395
|
-
await
|
|
13483
|
+
await write;
|
|
13396
13484
|
} catch (err2) {
|
|
13397
13485
|
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
13398
|
-
process.stderr.write(`[seal-mcp] warn: journal
|
|
13486
|
+
process.stderr.write(`[seal-mcp] warn: journal write failed: ${msg}
|
|
13399
13487
|
`);
|
|
13400
13488
|
}
|
|
13401
13489
|
}
|
|
13402
|
-
async function compensateRevoke(ctx, handle) {
|
|
13403
|
-
const
|
|
13404
|
-
|
|
13405
|
-
if (!op || sealId === void 0) {
|
|
13490
|
+
async function compensateRevoke(ctx, handle, op) {
|
|
13491
|
+
const sealId = op.seal_id;
|
|
13492
|
+
if (sealId === void 0) {
|
|
13406
13493
|
return "no live seal to revoke";
|
|
13407
13494
|
}
|
|
13408
13495
|
try {
|
|
13409
13496
|
await ctx.client.revoke(sealId, op.owner_token_b64);
|
|
13410
|
-
|
|
13411
|
-
await persistBestEffort(ctx);
|
|
13497
|
+
await bestEffort(dropJournal(ctx, handle));
|
|
13412
13498
|
return "seal auto-revoked";
|
|
13413
13499
|
} catch (err2) {
|
|
13414
13500
|
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
13415
13501
|
op.step = "compensating";
|
|
13416
13502
|
op.compensation = { action: "revoke", outcome: "failed", error: msg, at: nowIso2(ctx) };
|
|
13417
|
-
await
|
|
13503
|
+
await bestEffort(journal(ctx, handle, op));
|
|
13418
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`;
|
|
13419
13505
|
}
|
|
13420
13506
|
}
|
|
@@ -13439,7 +13525,7 @@ async function openSource(path, mime, requestedTier) {
|
|
|
13439
13525
|
} catch (err2) {
|
|
13440
13526
|
throw failed("unable to open", err2);
|
|
13441
13527
|
}
|
|
13442
|
-
let name = (0,
|
|
13528
|
+
let name = (0, import_node_path10.basename)(path);
|
|
13443
13529
|
let source = path;
|
|
13444
13530
|
let cleanup = async () => void 0;
|
|
13445
13531
|
let tier;
|
|
@@ -13465,7 +13551,7 @@ async function openSource(path, mime, requestedTier) {
|
|
|
13465
13551
|
}
|
|
13466
13552
|
let fileBlob;
|
|
13467
13553
|
try {
|
|
13468
|
-
fileBlob = await (0,
|
|
13554
|
+
fileBlob = await (0, import_node_fs6.openAsBlob)(source);
|
|
13469
13555
|
} catch (err2) {
|
|
13470
13556
|
await cleanup();
|
|
13471
13557
|
throw failed("unable to open", err2);
|
|
@@ -13586,13 +13672,14 @@ async function uploadAndDeliver(ctx, input, deps, prepared) {
|
|
|
13586
13672
|
// backend this seal is actually created on — recovery refuses to
|
|
13587
13673
|
// "revoke" against a different backend whose 404 would be misread
|
|
13588
13674
|
// as success.
|
|
13589
|
-
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)() }
|
|
13590
13679
|
};
|
|
13591
|
-
ctx.state.pending[handle] = op;
|
|
13592
13680
|
try {
|
|
13593
|
-
await ctx
|
|
13681
|
+
await journal(ctx, handle, op);
|
|
13594
13682
|
} catch (err2) {
|
|
13595
|
-
delete ctx.state.pending[handle];
|
|
13596
13683
|
throw new ToolError(
|
|
13597
13684
|
"state_persist_failed",
|
|
13598
13685
|
`refusing to upload: could not journal the operation before its first remote effect: ${err2 instanceof Error ? err2.message : String(err2)}`,
|
|
@@ -13613,7 +13700,7 @@ async function uploadAndDeliver(ctx, input, deps, prepared) {
|
|
|
13613
13700
|
// `uploaded` checkpoint after this call is the durability gate.
|
|
13614
13701
|
onSealCreated: async (sealId) => {
|
|
13615
13702
|
op.seal_id = sealId;
|
|
13616
|
-
await
|
|
13703
|
+
await bestEffort(journal(ctx, handle, op));
|
|
13617
13704
|
}
|
|
13618
13705
|
});
|
|
13619
13706
|
} catch (err2) {
|
|
@@ -13622,15 +13709,14 @@ async function uploadAndDeliver(ctx, input, deps, prepared) {
|
|
|
13622
13709
|
return awaitPayment(ctx, deps, prepared, { handle, op, ownerTokenB64, expiresAt, session });
|
|
13623
13710
|
}
|
|
13624
13711
|
if (op.seal_id !== void 0) {
|
|
13625
|
-
const outcome = await compensateRevoke(ctx, handle);
|
|
13712
|
+
const outcome = await compensateRevoke(ctx, handle, op);
|
|
13626
13713
|
throw new ToolError(
|
|
13627
13714
|
"upload_failed",
|
|
13628
13715
|
`seal upload failed after the seal was created: ${err2 instanceof Error ? err2.message : String(err2)} (${outcome})`,
|
|
13629
13716
|
err2
|
|
13630
13717
|
);
|
|
13631
13718
|
}
|
|
13632
|
-
|
|
13633
|
-
await persistBestEffort(ctx);
|
|
13719
|
+
await bestEffort(dropJournal(ctx, handle));
|
|
13634
13720
|
throw new ToolError(
|
|
13635
13721
|
"upload_failed",
|
|
13636
13722
|
`seal upload failed: ${err2 instanceof Error ? err2.message : String(err2)}`,
|
|
@@ -13643,9 +13729,9 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
|
|
|
13643
13729
|
op.seal_id = uploaded.sealId;
|
|
13644
13730
|
op.step = "uploaded";
|
|
13645
13731
|
try {
|
|
13646
|
-
await ctx
|
|
13732
|
+
await journal(ctx, handle, op);
|
|
13647
13733
|
} catch (err2) {
|
|
13648
|
-
const outcome = await compensateRevoke(ctx, handle);
|
|
13734
|
+
const outcome = await compensateRevoke(ctx, handle, op);
|
|
13649
13735
|
throw new ToolError(
|
|
13650
13736
|
"state_persist_failed",
|
|
13651
13737
|
`could not journal the live seal after upload: ${err2 instanceof Error ? err2.message : String(err2)} (${outcome})`,
|
|
@@ -13658,7 +13744,7 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
|
|
|
13658
13744
|
try {
|
|
13659
13745
|
shareBlobBytes = await encryptShareBlob(fileKeysB64, shareLinkKey, void 0);
|
|
13660
13746
|
} catch (err2) {
|
|
13661
|
-
const outcome = await compensateRevoke(ctx, handle);
|
|
13747
|
+
const outcome = await compensateRevoke(ctx, handle, op);
|
|
13662
13748
|
throw new ToolError(
|
|
13663
13749
|
"share_blob_failed",
|
|
13664
13750
|
`share blob encryption failed: ${err2 instanceof Error ? err2.message : String(err2)} (${outcome})`,
|
|
@@ -13670,7 +13756,7 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
|
|
|
13670
13756
|
try {
|
|
13671
13757
|
shareCode = await ctx.client.createShareLink(uploaded.sealId, ownerTokenB64, shareBlobB64);
|
|
13672
13758
|
} catch (err2) {
|
|
13673
|
-
const outcome = await compensateRevoke(ctx, handle);
|
|
13759
|
+
const outcome = await compensateRevoke(ctx, handle, op);
|
|
13674
13760
|
throw new ToolError(
|
|
13675
13761
|
"share_link_failed",
|
|
13676
13762
|
`POST /seal/${uploaded.sealId}/share failed: ${err2 instanceof Error ? err2.message : String(err2)} (${outcome})`,
|
|
@@ -13688,7 +13774,7 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
|
|
|
13688
13774
|
try {
|
|
13689
13775
|
fragment = await buildTargetedFragment(shareLinkKey, recipientPub, uploaded.sealId);
|
|
13690
13776
|
} catch (err2) {
|
|
13691
|
-
const outcome = await compensateRevoke(ctx, handle);
|
|
13777
|
+
const outcome = await compensateRevoke(ctx, handle, op);
|
|
13692
13778
|
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
13693
13779
|
if (message.includes("non-contributory")) {
|
|
13694
13780
|
throw new ToolError(
|
|
@@ -13709,7 +13795,7 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
|
|
|
13709
13795
|
const host = resolvePublicHost(deps.publicHost);
|
|
13710
13796
|
const shareUrl = `${host}/s/${uploaded.sealId}/${shareCode}#${fragment}`;
|
|
13711
13797
|
if (mode === "forward") {
|
|
13712
|
-
|
|
13798
|
+
await complete(ctx, handle, {
|
|
13713
13799
|
seal_id: uploaded.sealId,
|
|
13714
13800
|
owner_token_b64: ownerTokenB64,
|
|
13715
13801
|
created_at: nowIso2(ctx),
|
|
@@ -13717,9 +13803,6 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
|
|
|
13717
13803
|
expires_at: expiresAt,
|
|
13718
13804
|
backend_url: ctx.client.baseUrl
|
|
13719
13805
|
});
|
|
13720
|
-
appendAudit(ctx, "seal_share", handle, mode);
|
|
13721
|
-
delete ctx.state.pending[handle];
|
|
13722
|
-
await ctx.persist();
|
|
13723
13806
|
return {
|
|
13724
13807
|
handle,
|
|
13725
13808
|
shareUrl,
|
|
@@ -13731,9 +13814,9 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
|
|
|
13731
13814
|
op.step = "delivering";
|
|
13732
13815
|
op.handoff_channels_attempted = deps.handoff?.disableClipboard ? ["file"] : ["clipboard", "file"];
|
|
13733
13816
|
try {
|
|
13734
|
-
await ctx
|
|
13817
|
+
await journal(ctx, handle, op);
|
|
13735
13818
|
} catch (err2) {
|
|
13736
|
-
const outcome = await compensateRevoke(ctx, handle);
|
|
13819
|
+
const outcome = await compensateRevoke(ctx, handle, op);
|
|
13737
13820
|
throw new ToolError(
|
|
13738
13821
|
"state_persist_failed",
|
|
13739
13822
|
`could not journal the delivery attempt before handing off the URL: ${err2 instanceof Error ? err2.message : String(err2)} (${outcome})`,
|
|
@@ -13744,14 +13827,14 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
|
|
|
13744
13827
|
try {
|
|
13745
13828
|
handoff = await performHandoff({ handle, shareUrl, expireMs }, deps.handoff);
|
|
13746
13829
|
} catch (err2) {
|
|
13747
|
-
const outcome = await compensateRevoke(ctx, handle);
|
|
13830
|
+
const outcome = await compensateRevoke(ctx, handle, op);
|
|
13748
13831
|
throw new ToolError(
|
|
13749
13832
|
"handoff_both_channels_failed",
|
|
13750
13833
|
`both clipboard and file handoff channels failed \u2014 ${outcome}`,
|
|
13751
13834
|
err2
|
|
13752
13835
|
);
|
|
13753
13836
|
}
|
|
13754
|
-
|
|
13837
|
+
await complete(ctx, handle, {
|
|
13755
13838
|
seal_id: uploaded.sealId,
|
|
13756
13839
|
owner_token_b64: ownerTokenB64,
|
|
13757
13840
|
created_at: nowIso2(ctx),
|
|
@@ -13760,10 +13843,7 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
|
|
|
13760
13843
|
handoff_channel: handoff.channel,
|
|
13761
13844
|
backend_url: ctx.client.baseUrl
|
|
13762
13845
|
});
|
|
13763
|
-
|
|
13764
|
-
delete ctx.state.pending[handle];
|
|
13765
|
-
await ctx.persist();
|
|
13766
|
-
const delivered = handoff.channel === "clipboard" ? "The seal link is on your clipboard. Paste it to the recipient." : `The seal link is in ${handoff.file} (mode 0600, removed automatically later). Give it to the recipient.`;
|
|
13846
|
+
const delivered = handoff.channel === "clipboard" ? HANDOFF_OUTPUT.receipt_clipboard : HANDOFF_OUTPUT.receipt_file.replace("{file}", handoff.file ?? "");
|
|
13767
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.`;
|
|
13768
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;
|
|
13769
13849
|
return {
|
|
@@ -13781,12 +13861,12 @@ async function awaitPayment(ctx, deps, prepared, { handle, op, ownerTokenB64, se
|
|
|
13781
13861
|
const tier = prepared.tier;
|
|
13782
13862
|
const sealId = op.seal_id;
|
|
13783
13863
|
op.step = "awaiting_payment";
|
|
13784
|
-
await
|
|
13864
|
+
await bestEffort(journal(ctx, handle, op));
|
|
13785
13865
|
let paymentUrl;
|
|
13786
13866
|
try {
|
|
13787
13867
|
paymentUrl = await ctx.client.createCheckout(sealId, ownerTokenB64, tier);
|
|
13788
13868
|
} catch (err2) {
|
|
13789
|
-
const outcome = await compensateRevoke(ctx, handle);
|
|
13869
|
+
const outcome = await compensateRevoke(ctx, handle, op);
|
|
13790
13870
|
throw new ToolError("payment_failed", `POST /billing/checkout failed: ${messageOf3(err2)} (${outcome})`, err2);
|
|
13791
13871
|
}
|
|
13792
13872
|
const deadline = paymentDeadline(op.expires_at);
|
|
@@ -13796,7 +13876,7 @@ async function awaitPayment(ctx, deps, prepared, { handle, op, ownerTokenB64, se
|
|
|
13796
13876
|
clearTimeout(timer);
|
|
13797
13877
|
ctx.payments?.delete(handle);
|
|
13798
13878
|
await prepared.cleanup();
|
|
13799
|
-
throw new ToolError(code, `${why} (${await compensateRevoke(ctx, handle)})`);
|
|
13879
|
+
throw new ToolError(code, `${why} (${await compensateRevoke(ctx, handle, op)})`);
|
|
13800
13880
|
};
|
|
13801
13881
|
const finish = async (onProgress) => {
|
|
13802
13882
|
if (running) throw new ToolError("payment_invalid", `share ${handle} is already being finished`);
|
|
@@ -13965,7 +14045,7 @@ function proFileGetOutputToWire(out) {
|
|
|
13965
14045
|
}
|
|
13966
14046
|
|
|
13967
14047
|
// src/server.ts
|
|
13968
|
-
var SERVER_VERSION = "0.
|
|
14048
|
+
var SERVER_VERSION = "0.3.0";
|
|
13969
14049
|
var toolMeta = (name) => {
|
|
13970
14050
|
const { title, description, annotations } = seal_mcp_v1_default.tools[name];
|
|
13971
14051
|
return { title, description, annotations };
|
|
@@ -13992,41 +14072,29 @@ function progressReporter(extra) {
|
|
|
13992
14072
|
}
|
|
13993
14073
|
async function createServer(options) {
|
|
13994
14074
|
logEngineToStderr();
|
|
13995
|
-
let view;
|
|
13996
|
-
let identity;
|
|
13997
|
-
let lock = null;
|
|
13998
14075
|
const client = new SealApi({ baseUrl: resolveBackendUrl(options.backendBaseUrl) });
|
|
13999
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;
|
|
14000
14083
|
if (disk === null) {
|
|
14001
14084
|
identity = await generateIdentity();
|
|
14002
14085
|
view = createEmptyState(identityToFields(identity));
|
|
14003
14086
|
} else {
|
|
14004
|
-
|
|
14005
|
-
|
|
14006
|
-
try {
|
|
14007
|
-
view = await loadState(disk.passphrase, disk.path);
|
|
14008
|
-
identity = await loadIdentity(view.identity);
|
|
14009
|
-
const { changed } = await recoverPendingOperations(view, client);
|
|
14010
|
-
if (changed) {
|
|
14011
|
-
await saveState(view, disk.passphrase, disk.path);
|
|
14012
|
-
}
|
|
14013
|
-
} catch (err2) {
|
|
14014
|
-
await held.release();
|
|
14015
|
-
throw err2;
|
|
14016
|
-
}
|
|
14087
|
+
view = await updateState(disk.passphrase, disk.path, async (state) => (await recoverPendingOperations(state, client)).changed);
|
|
14088
|
+
identity = await loadIdentity(view.identity);
|
|
14017
14089
|
}
|
|
14018
14090
|
const publicHost = resolvePublicHost(options.publicHost);
|
|
14019
|
-
let persistChain = Promise.resolve();
|
|
14020
|
-
const persist = () => {
|
|
14021
|
-
if (disk === null) return Promise.resolve();
|
|
14022
|
-
persistChain = persistChain.catch(() => void 0).then(() => saveState(view, disk.passphrase, disk.path));
|
|
14023
|
-
return persistChain;
|
|
14024
|
-
};
|
|
14025
14091
|
const ctx = {
|
|
14026
|
-
state
|
|
14092
|
+
get state() {
|
|
14093
|
+
return view;
|
|
14094
|
+
},
|
|
14027
14095
|
identity,
|
|
14028
14096
|
client,
|
|
14029
|
-
|
|
14097
|
+
update,
|
|
14030
14098
|
ephemeral: disk === null,
|
|
14031
14099
|
requests: /* @__PURE__ */ new Map(),
|
|
14032
14100
|
payments: /* @__PURE__ */ new Map()
|
|
@@ -14229,18 +14297,12 @@ async function createServer(options) {
|
|
|
14229
14297
|
);
|
|
14230
14298
|
}
|
|
14231
14299
|
const transport = new import_stdio.StdioServerTransport();
|
|
14232
|
-
|
|
14233
|
-
await mcp.connect(transport);
|
|
14234
|
-
} catch (err2) {
|
|
14235
|
-
await lock?.release();
|
|
14236
|
-
throw err2;
|
|
14237
|
-
}
|
|
14300
|
+
await mcp.connect(transport);
|
|
14238
14301
|
const close = async () => {
|
|
14239
14302
|
try {
|
|
14240
14303
|
await mcp.close();
|
|
14241
14304
|
} finally {
|
|
14242
14305
|
await transport.close().catch(() => void 0);
|
|
14243
|
-
await lock?.release();
|
|
14244
14306
|
}
|
|
14245
14307
|
};
|
|
14246
14308
|
return { mcp, transport, close };
|
|
@@ -14254,7 +14316,7 @@ async function wrapTool(fn, toWire) {
|
|
|
14254
14316
|
structuredContent: wire
|
|
14255
14317
|
};
|
|
14256
14318
|
} catch (err2) {
|
|
14257
|
-
if (err2 instanceof ToolError) {
|
|
14319
|
+
if (err2 instanceof ToolError || err2 instanceof StateBusyError) {
|
|
14258
14320
|
return {
|
|
14259
14321
|
isError: true,
|
|
14260
14322
|
content: [
|
|
@@ -14276,7 +14338,6 @@ async function wrapTool(fn, toWire) {
|
|
|
14276
14338
|
// src/cli.ts
|
|
14277
14339
|
var PROGRAM_NAME = "sealnet-mcp";
|
|
14278
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.";
|
|
14279
|
-
var MIN_PASSPHRASE_LEN = 8;
|
|
14280
14341
|
function logStderr(line) {
|
|
14281
14342
|
process.stderr.write(`${line}
|
|
14282
14343
|
`);
|
|
@@ -14285,118 +14346,33 @@ function logStdout(line) {
|
|
|
14285
14346
|
process.stdout.write(`${line}
|
|
14286
14347
|
`);
|
|
14287
14348
|
}
|
|
14288
|
-
async function
|
|
14289
|
-
|
|
14290
|
-
await (0, import_promises10.stat)(p);
|
|
14291
|
-
return true;
|
|
14292
|
-
} catch {
|
|
14293
|
-
return false;
|
|
14294
|
-
}
|
|
14349
|
+
async function agentKey(path) {
|
|
14350
|
+
return await readSeed(path.dir) ?? (await openAgentState(path)).root;
|
|
14295
14351
|
}
|
|
14296
14352
|
async function runInit(opts) {
|
|
14297
14353
|
const path = resolveStateDir({ cliFlag: opts.stateDir });
|
|
14298
|
-
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) {
|
|
14299
14357
|
logStderr(`${PROGRAM_NAME} init: state file already exists at ${path.statePath}`);
|
|
14300
|
-
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).");
|
|
14301
14359
|
process.exit(2);
|
|
14302
14360
|
}
|
|
14303
|
-
const
|
|
14304
|
-
let passphrase;
|
|
14305
|
-
if (fromEnv !== void 0 && fromEnv.length > 0) {
|
|
14306
|
-
passphrase = fromEnv;
|
|
14307
|
-
logStderr(
|
|
14308
|
-
`${PROGRAM_NAME} init: using passphrase from ${PASSPHRASE_ENV_VAR} (will also save to OS keychain).`
|
|
14309
|
-
);
|
|
14310
|
-
} else {
|
|
14311
|
-
passphrase = await promptHiddenPassphrase("Choose a passphrase (will not echo): ");
|
|
14312
|
-
if (passphrase.length < MIN_PASSPHRASE_LEN) {
|
|
14313
|
-
throw new Error(`Passphrase must be at least ${MIN_PASSPHRASE_LEN} characters.`);
|
|
14314
|
-
}
|
|
14315
|
-
const confirm = await promptHiddenPassphrase("Confirm passphrase: ");
|
|
14316
|
-
if (confirm !== passphrase) {
|
|
14317
|
-
throw new Error("Passphrases do not match.");
|
|
14318
|
-
}
|
|
14319
|
-
}
|
|
14320
|
-
logStderr(`${PROGRAM_NAME} init: generating X25519 identity ...`);
|
|
14321
|
-
const identity = await generateIdentity();
|
|
14322
|
-
const identityFields = identityToFields(identity);
|
|
14323
|
-
const state = createEmptyState(identityFields);
|
|
14324
|
-
logStderr(`${PROGRAM_NAME} init: writing encrypted state to ${path.statePath} ...`);
|
|
14325
|
-
const lock = await acquireStateLock(path.dir);
|
|
14326
|
-
let keychainStored = false;
|
|
14327
|
-
try {
|
|
14328
|
-
if (!opts.force && await pathExists(path.statePath)) {
|
|
14329
|
-
logStderr(`${PROGRAM_NAME} init: state file already exists at ${path.statePath}`);
|
|
14330
|
-
logStderr("Re-run with --force to overwrite (you will lose existing handles + identity).");
|
|
14331
|
-
process.exit(2);
|
|
14332
|
-
}
|
|
14333
|
-
await saveState(state, passphrase, path.statePath);
|
|
14334
|
-
try {
|
|
14335
|
-
await setKeychainPassphrase(passphrase);
|
|
14336
|
-
keychainStored = true;
|
|
14337
|
-
logStderr(
|
|
14338
|
-
`${PROGRAM_NAME} init: passphrase stored in OS keychain (service=${KEYCHAIN_SERVICE} account=${KEYCHAIN_ACCOUNT}).`
|
|
14339
|
-
);
|
|
14340
|
-
} catch (err2) {
|
|
14341
|
-
if (err2 instanceof KeytarUnavailableError) {
|
|
14342
|
-
logStderr(`${PROGRAM_NAME} init: warning: ${err2.message}`);
|
|
14343
|
-
logStderr(
|
|
14344
|
-
`${PROGRAM_NAME} init: state.json IS encrypted, but you must export ${PASSPHRASE_ENV_VAR} before every \`${PROGRAM_NAME} serve\` until the OS keychain works.`
|
|
14345
|
-
);
|
|
14346
|
-
} else {
|
|
14347
|
-
throw err2;
|
|
14348
|
-
}
|
|
14349
|
-
}
|
|
14350
|
-
} finally {
|
|
14351
|
-
await lock.release();
|
|
14352
|
-
}
|
|
14361
|
+
const identity = await identityFromSeed(opened.root.seed);
|
|
14353
14362
|
logStdout(`${PROGRAM_NAME} init: ready.`);
|
|
14354
|
-
logStdout(` state.json:
|
|
14355
|
-
logStdout(`
|
|
14356
|
-
logStdout(`
|
|
14363
|
+
logStdout(` state.json: ${path.statePath}`);
|
|
14364
|
+
logStdout(` key source: ${opened.root.source}`);
|
|
14365
|
+
logStdout(` address: ${Buffer.from(identity.public).toString("base64url")}`);
|
|
14357
14366
|
logStdout("");
|
|
14358
|
-
logStdout(
|
|
14359
|
-
"Share the pubkey with senders so they can target SEALs to your MCP server (see `seal send --to <pubkey>` from `seal-cli`)."
|
|
14360
|
-
);
|
|
14361
|
-
}
|
|
14362
|
-
async function createStateWithoutPrompt(path) {
|
|
14363
|
-
const resolved = await passphraseForNewState();
|
|
14364
|
-
if (resolved === null) return null;
|
|
14365
|
-
const lock = await acquireStateLock(path.dir);
|
|
14366
|
-
try {
|
|
14367
|
-
if (await pathExists(path.statePath)) return void 0;
|
|
14368
|
-
if (resolved.fresh) await setKeychainPassphrase(resolved.passphrase);
|
|
14369
|
-
const identity = await generateIdentity();
|
|
14370
|
-
await saveState(createEmptyState(identityToFields(identity)), resolved.passphrase, path.statePath);
|
|
14371
|
-
} finally {
|
|
14372
|
-
await lock.release();
|
|
14373
|
-
}
|
|
14374
|
-
logStderr(`${PROGRAM_NAME} serve: first start, state created at ${path.statePath}`);
|
|
14375
|
-
return resolved;
|
|
14367
|
+
logStdout("Senders use the address to deliver a link only this agent opens (`seal send --to <address>`).");
|
|
14376
14368
|
}
|
|
14377
14369
|
async function stateForServe(path) {
|
|
14378
|
-
|
|
14379
|
-
const created = await createStateWithoutPrompt(path);
|
|
14380
|
-
if (created !== void 0) return created;
|
|
14381
|
-
}
|
|
14382
|
-
const resolved = await resolvePassphraseFromStores();
|
|
14383
|
-
if (resolved === null) {
|
|
14384
|
-
throw new PassphraseNotFoundError();
|
|
14385
|
-
}
|
|
14386
|
-
if (resolved.source === "env") {
|
|
14387
|
-
logStderr(
|
|
14388
|
-
`${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\`).`
|
|
14389
|
-
);
|
|
14390
|
-
}
|
|
14370
|
+
let opened;
|
|
14391
14371
|
try {
|
|
14392
|
-
|
|
14393
|
-
await loadIdentity(view.identity);
|
|
14372
|
+
opened = await openAgentState(path);
|
|
14394
14373
|
} catch (err2) {
|
|
14395
|
-
if (err2 instanceof
|
|
14396
|
-
logStderr(`${PROGRAM_NAME} serve:
|
|
14397
|
-
logStderr(
|
|
14398
|
-
`If you reset your OS keychain, re-run \`${PROGRAM_NAME} init --force\` (you will lose existing handles).`
|
|
14399
|
-
);
|
|
14374
|
+
if (err2 instanceof IdentityConflictError) {
|
|
14375
|
+
logStderr(`${PROGRAM_NAME} serve: ${err2.message}`);
|
|
14400
14376
|
process.exit(3);
|
|
14401
14377
|
}
|
|
14402
14378
|
if (err2 instanceof StateCorrupt) {
|
|
@@ -14405,29 +14381,27 @@ async function stateForServe(path) {
|
|
|
14405
14381
|
}
|
|
14406
14382
|
throw err2;
|
|
14407
14383
|
}
|
|
14408
|
-
|
|
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;
|
|
14409
14392
|
}
|
|
14410
14393
|
async function runServe(opts) {
|
|
14411
14394
|
const path = resolveStateDir({ cliFlag: opts.stateDir });
|
|
14412
|
-
const
|
|
14395
|
+
const opened = opts.ephemeral || process.env[EPHEMERAL_ENV_VAR] === "1" ? null : await stateForServe(path);
|
|
14413
14396
|
logStderr(
|
|
14414
|
-
|
|
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})`
|
|
14415
14398
|
);
|
|
14416
14399
|
logStderr(`${PROGRAM_NAME} serve: starting stdio MCP server ...`);
|
|
14417
|
-
|
|
14418
|
-
|
|
14419
|
-
|
|
14420
|
-
|
|
14421
|
-
|
|
14422
|
-
publicHost: opts.publicHost
|
|
14423
|
-
});
|
|
14424
|
-
} catch (err2) {
|
|
14425
|
-
if (err2 instanceof StateLockedError) {
|
|
14426
|
-
logStderr(`${PROGRAM_NAME} serve: ${err2.message}`);
|
|
14427
|
-
process.exit(5);
|
|
14428
|
-
}
|
|
14429
|
-
throw err2;
|
|
14430
|
-
}
|
|
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
|
+
});
|
|
14431
14405
|
const shutdown = async (signal) => {
|
|
14432
14406
|
logStderr(`${PROGRAM_NAME} serve: received ${signal}, shutting down ...`);
|
|
14433
14407
|
try {
|
|
@@ -14446,28 +14420,39 @@ async function runServe(opts) {
|
|
|
14446
14420
|
});
|
|
14447
14421
|
}
|
|
14448
14422
|
async function runPubkey(opts) {
|
|
14449
|
-
const
|
|
14450
|
-
|
|
14451
|
-
|
|
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.`);
|
|
14452
14431
|
}
|
|
14453
|
-
const
|
|
14454
|
-
|
|
14455
|
-
|
|
14456
|
-
|
|
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
|
+
);
|
|
14457
14443
|
}
|
|
14458
14444
|
async function runDoctor(opts) {
|
|
14459
14445
|
const path = resolveStateDir({ cliFlag: opts.stateDir });
|
|
14460
|
-
const
|
|
14461
|
-
let
|
|
14462
|
-
let
|
|
14463
|
-
let stateOwnerUid = null;
|
|
14446
|
+
const seedFile = (0, import_node_path11.join)(path.dir, SEED_FILE_NAME);
|
|
14447
|
+
let seedSource = null;
|
|
14448
|
+
let seedError = null;
|
|
14464
14449
|
try {
|
|
14465
|
-
|
|
14466
|
-
|
|
14467
|
-
|
|
14468
|
-
stateOwnerUid = typeof st.uid === "number" ? st.uid : null;
|
|
14469
|
-
} catch {
|
|
14450
|
+
seedSource = (await readSeed(path.dir))?.source ?? null;
|
|
14451
|
+
} catch (err2) {
|
|
14452
|
+
seedError = err2 instanceof Error ? err2.message : String(err2);
|
|
14470
14453
|
}
|
|
14454
|
+
const statePerms = await perms(path.statePath);
|
|
14455
|
+
const seedPerms = await perms(seedFile);
|
|
14471
14456
|
const kc = await probeKeychain();
|
|
14472
14457
|
const lockProbe = await probeStateLock(path.dir);
|
|
14473
14458
|
const client = new SealApi({ baseUrl: resolveBackendUrl(opts.backendUrl) });
|
|
@@ -14488,103 +14473,101 @@ async function runDoctor(opts) {
|
|
|
14488
14473
|
backendReachable = false;
|
|
14489
14474
|
backendError = err2 instanceof Error ? err2.message : String(err2);
|
|
14490
14475
|
}
|
|
14491
|
-
|
|
14492
|
-
|
|
14493
|
-
|
|
14494
|
-
|
|
14495
|
-
|
|
14496
|
-
|
|
14497
|
-
|
|
14498
|
-
|
|
14499
|
-
|
|
14500
|
-
|
|
14501
|
-
|
|
14502
|
-
|
|
14503
|
-
|
|
14504
|
-
|
|
14505
|
-
|
|
14506
|
-
|
|
14507
|
-
|
|
14508
|
-
|
|
14509
|
-
|
|
14510
|
-
|
|
14511
|
-
|
|
14512
|
-
|
|
14513
|
-
|
|
14514
|
-
|
|
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}).`);
|
|
14515
14507
|
}
|
|
14516
|
-
|
|
14517
|
-
logStderr(
|
|
14518
|
-
`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).`
|
|
14519
|
-
);
|
|
14520
|
-
} else if (stateExists && !kc.hasEntry && !report.env_passphrase_set) {
|
|
14521
|
-
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\`.`);
|
|
14522
14510
|
}
|
|
14523
14511
|
if (backendReachable === false) {
|
|
14524
14512
|
logStderr(`hint: backend ${client.baseUrl} unreachable (${backendError ?? "unknown"}).`);
|
|
14525
14513
|
}
|
|
14526
|
-
if (lockProbe.held &&
|
|
14527
|
-
logStderr(
|
|
14528
|
-
`hint: state dir is locked by pid ${lockProbe.holder?.pid ?? "?"} \u2014 a serve instance is running.`
|
|
14529
|
-
);
|
|
14530
|
-
} else if (lockProbe.held && lockProbe.stale) {
|
|
14531
|
-
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.");
|
|
14532
14516
|
}
|
|
14533
14517
|
}
|
|
14534
14518
|
async function runReset(opts) {
|
|
14535
14519
|
if (!opts.force) {
|
|
14536
14520
|
throw new Error(
|
|
14537
|
-
"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."
|
|
14538
14522
|
);
|
|
14539
14523
|
}
|
|
14540
14524
|
const path = resolveStateDir({ cliFlag: opts.stateDir });
|
|
14541
|
-
const
|
|
14542
|
-
|
|
14543
|
-
|
|
14544
|
-
|
|
14545
|
-
|
|
14546
|
-
|
|
14547
|
-
|
|
14548
|
-
|
|
14549
|
-
const code = err2.code;
|
|
14550
|
-
if (code !== "ENOENT") {
|
|
14551
|
-
throw err2;
|
|
14552
|
-
}
|
|
14553
|
-
}
|
|
14554
|
-
keychainDeleted = await deleteKeychainPassphrase();
|
|
14555
|
-
} finally {
|
|
14556
|
-
await lock.release();
|
|
14557
|
-
}
|
|
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
|
+
});
|
|
14558
14533
|
logStdout(
|
|
14559
14534
|
JSON.stringify(
|
|
14560
14535
|
{
|
|
14561
|
-
state_deleted: stateDeleted,
|
|
14562
|
-
|
|
14536
|
+
state_deleted: result.stateDeleted,
|
|
14537
|
+
seed_file_deleted: result.seedFile,
|
|
14538
|
+
keychain_deleted: result.keychain,
|
|
14563
14539
|
state_path: path.statePath
|
|
14564
14540
|
},
|
|
14565
14541
|
null,
|
|
14566
14542
|
2
|
|
14567
14543
|
)
|
|
14568
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
|
+
}
|
|
14569
14551
|
}
|
|
14570
14552
|
function buildCli() {
|
|
14571
14553
|
const program = new import_commander.Command();
|
|
14572
14554
|
program.name(PROGRAM_NAME).description(DESCRIPTION).version(SERVER_VERSION, "-V, --version", `Print the ${PROGRAM_NAME} version and exit.`);
|
|
14573
14555
|
program.command("init").description(
|
|
14574
|
-
"
|
|
14575
|
-
).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));
|
|
14576
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(
|
|
14577
14559
|
"--public-host <url>",
|
|
14578
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."
|
|
14579
|
-
).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));
|
|
14580
14562
|
program.command("pubkey").description(
|
|
14581
|
-
"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."
|
|
14582
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));
|
|
14583
14566
|
program.command("doctor").description(
|
|
14584
|
-
"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."
|
|
14585
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));
|
|
14586
14569
|
program.command("reset").description(
|
|
14587
|
-
"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."
|
|
14588
14571
|
).option("--state-dir <path>", "Override default state directory").option("--force", "Confirm destructive operation", false).action(async (opts) => runReset(opts));
|
|
14589
14572
|
return program;
|
|
14590
14573
|
}
|