sealnet-mcp 0.2.3 → 0.2.5
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 +2 -2
- package/dist/cli.cjs +46 -15
- package/dist/cli.js +46 -15
- package/dist/index.cjs +42 -11
- package/dist/index.d.cts +558 -101
- package/dist/index.d.ts +558 -101
- package/dist/index.js +42 -11
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ Model Context Protocol (MCP) server for **SEAL**.
|
|
|
8
8
|
|
|
9
9
|
When an MCP-connected AI agent (Claude Desktop, Cursor, Cline, etc.) creates a SEAL via this server:
|
|
10
10
|
|
|
11
|
-
- **handoff mode (default)**: the share link (the key is in its fragment, there is no password) is delivered to the human user via **system clipboard** (primary) or **0o600 local file** (fallback when clipboard is unavailable — headless, SSH, WSL without display). The `tool_result` returned to the model contains **only** an opaque handle and a human-readable receipt. The
|
|
11
|
+
- **handoff mode (default)**: the share link (the key is in its fragment, there is no password) is delivered to the human user via **system clipboard** (primary) or **0o600 local file** (fallback when clipboard is unavailable — headless, SSH, WSL without display). The `tool_result` returned to the model contains **only** an opaque handle and a human-readable receipt. The link is never written into the conversation; on the file channel the receipt names the file, and an agent that can read files on that machine can read it.
|
|
12
12
|
- **forward mode (explicit opt-in)**: model receives `{ handle, share_url, expires_at, max_reads, mode }` in `tool_result`; the link opens only with the recipient's X25519 key. Requires short TTL (≤30m), `max_reads=1`, mandatory `to` (recipient pubkey). Every forward is audit-logged in the encrypted state file.
|
|
13
13
|
|
|
14
14
|
## Tool surface
|
|
@@ -35,7 +35,7 @@ All state lives encrypted at `${XDG_CONFIG_HOME}/seal-mcp/state.json` (mode `0o6
|
|
|
35
35
|
|
|
36
36
|
Passphrase delivery, in priority order:
|
|
37
37
|
|
|
38
|
-
1. **OS keychain** (default) — `
|
|
38
|
+
1. **OS keychain** (default) — `@napi-rs/keyring` (optional dependency) reads from the Secret Service on Linux / macOS Keychain / Windows Credential Manager. The first `serve` stores a random passphrase there itself; `sealnet-mcp init` lets you choose one instead.
|
|
39
39
|
2. **`SEAL_MCP_PASSPHRASE` env var** — explicit opt-in for CI/headless. Warning logged.
|
|
40
40
|
3. **Cleartext on disk** — explicit refusal even if user tries `--passphrase-file …`.
|
|
41
41
|
|
package/dist/cli.cjs
CHANGED
|
@@ -7099,6 +7099,7 @@ async function probeStateLock(stateDir, deps = {}) {
|
|
|
7099
7099
|
}
|
|
7100
7100
|
|
|
7101
7101
|
// src/passphrase.ts
|
|
7102
|
+
var import_node_child_process = require("child_process");
|
|
7102
7103
|
var import_node_crypto2 = require("crypto");
|
|
7103
7104
|
var SERVICE_NAME = "seal-mcp";
|
|
7104
7105
|
var ACCOUNT_NAME = "state";
|
|
@@ -7122,23 +7123,50 @@ var PassphraseNotFoundError = class extends Error {
|
|
|
7122
7123
|
this.name = "PassphraseNotFoundError";
|
|
7123
7124
|
}
|
|
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
|
+
}
|
|
7125
7152
|
var keytarCache = void 0;
|
|
7126
7153
|
async function loadKeytar() {
|
|
7127
7154
|
if (keytarCache !== void 0) return keytarCache;
|
|
7128
7155
|
try {
|
|
7129
|
-
const mod = await import("
|
|
7130
|
-
|
|
7131
|
-
|
|
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);
|
|
7132
7160
|
} catch {
|
|
7133
7161
|
keytarCache = null;
|
|
7134
|
-
return null;
|
|
7135
7162
|
}
|
|
7163
|
+
return keytarCache;
|
|
7136
7164
|
}
|
|
7137
7165
|
async function loadKeytarOrThrow() {
|
|
7138
7166
|
const k = await loadKeytar();
|
|
7139
7167
|
if (k === null) {
|
|
7140
7168
|
throw new KeytarUnavailableError(
|
|
7141
|
-
"
|
|
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."
|
|
7142
7170
|
);
|
|
7143
7171
|
}
|
|
7144
7172
|
return k;
|
|
@@ -11343,13 +11371,13 @@ var seal_mcp_v1_default = {
|
|
|
11343
11371
|
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`."
|
|
11344
11372
|
},
|
|
11345
11373
|
server_instructions: {
|
|
11346
|
-
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).",
|
|
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). Never read or print a SEAL handoff file.",
|
|
11347
11375
|
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."
|
|
11348
11376
|
},
|
|
11349
11377
|
tools: {
|
|
11350
11378
|
seal_share: {
|
|
11351
11379
|
title: "Share a file via SEAL",
|
|
11352
|
-
description: "Give the user or another agent a file or a secret WITHOUT it passing through this chat. Works for
|
|
11380
|
+
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.",
|
|
11353
11381
|
annotations: { destructiveHint: false, openWorldHint: true },
|
|
11354
11382
|
input: {
|
|
11355
11383
|
properties: {
|
|
@@ -11399,7 +11427,9 @@ var seal_mcp_v1_default = {
|
|
|
11399
11427
|
expires_at: { type: "string", format: "date-time", description: "ISO 8601 UTC expiry timestamp." }
|
|
11400
11428
|
},
|
|
11401
11429
|
MUST_NOT_contain: ["share_url", "url", "password", "passphrase", "link_key", "owner_token"],
|
|
11402
|
-
rationale: "Anything in this object reaches the model. Any field that lets the model reconstruct or re-issue the share capability defeats handoff mode."
|
|
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.",
|
|
11431
|
+
receipt_clipboard: "The seal link is on your clipboard. Paste it to the recipient.",
|
|
11432
|
+
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."
|
|
11403
11433
|
},
|
|
11404
11434
|
output_forward: {
|
|
11405
11435
|
type: "object",
|
|
@@ -11797,7 +11827,7 @@ var seal_mcp_v1_default = {
|
|
|
11797
11827
|
{
|
|
11798
11828
|
rank: 1,
|
|
11799
11829
|
method: "os_keychain",
|
|
11800
|
-
implementation: "
|
|
11830
|
+
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. It replaced keytar in 0.2.4 and finds the entry keytar wrote (Windows credential `seal-mcp/state`, the passphrase as UTF-8 bytes; on Linux the old entry is read once through `secret-tool` and stored anew). Service id: 'seal-mcp', account: 'state'. Set during `sealnet-mcp init` (chosen by the user), or by the first `sealnet-mcp serve` without state, which generates a random 32-byte passphrase itself \u2014 no prompt, no `init` step. Read on every subsequent process start.",
|
|
11801
11831
|
default: true
|
|
11802
11832
|
},
|
|
11803
11833
|
{
|
|
@@ -12945,7 +12975,7 @@ async function sealProFileGet(source, input) {
|
|
|
12945
12975
|
}
|
|
12946
12976
|
|
|
12947
12977
|
// src/tools/request.ts
|
|
12948
|
-
var
|
|
12978
|
+
var import_node_child_process2 = require("child_process");
|
|
12949
12979
|
var import_node_fs3 = require("fs");
|
|
12950
12980
|
var import_promises5 = require("fs/promises");
|
|
12951
12981
|
var import_node_os5 = require("os");
|
|
@@ -12964,7 +12994,7 @@ function runSealCli(args) {
|
|
|
12964
12994
|
const cli = findSealCli();
|
|
12965
12995
|
if (!cli) return Promise.resolve(null);
|
|
12966
12996
|
return new Promise((resolve5) => {
|
|
12967
|
-
(0,
|
|
12997
|
+
(0, import_node_child_process2.execFile)(cli, [...args], { maxBuffer: 1024 * 1024 }, (_err, stdout) => {
|
|
12968
12998
|
try {
|
|
12969
12999
|
resolve5(JSON.parse(stdout));
|
|
12970
13000
|
} catch {
|
|
@@ -13335,6 +13365,7 @@ function guessMimeFromName(name) {
|
|
|
13335
13365
|
}
|
|
13336
13366
|
var VALID_TTL = ["5m", "30m", "1h", "1d", "7d"];
|
|
13337
13367
|
var EXPIRE_DEFAULT = seal_mcp_v1_default.tools.seal_share.expire_default;
|
|
13368
|
+
var HANDOFF_OUTPUT = seal_mcp_v1_default.tools.seal_share.output_handoff;
|
|
13338
13369
|
var TIER_BYTES2 = limits_default2.limits.TIER_BYTES;
|
|
13339
13370
|
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.";
|
|
13340
13371
|
function defaultExpire(mode, size) {
|
|
@@ -13735,7 +13766,7 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
|
|
|
13735
13766
|
appendAudit(ctx, "seal_share", handle, mode);
|
|
13736
13767
|
delete ctx.state.pending[handle];
|
|
13737
13768
|
await ctx.persist();
|
|
13738
|
-
const delivered = handoff.channel === "clipboard" ?
|
|
13769
|
+
const delivered = handoff.channel === "clipboard" ? HANDOFF_OUTPUT.receipt_clipboard : HANDOFF_OUTPUT.receipt_file.replace("{file}", handoff.file ?? "");
|
|
13739
13770
|
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.`;
|
|
13740
13771
|
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;
|
|
13741
13772
|
return {
|
|
@@ -13937,7 +13968,7 @@ function proFileGetOutputToWire(out) {
|
|
|
13937
13968
|
}
|
|
13938
13969
|
|
|
13939
13970
|
// src/server.ts
|
|
13940
|
-
var SERVER_VERSION = "0.2.
|
|
13971
|
+
var SERVER_VERSION = "0.2.5";
|
|
13941
13972
|
var toolMeta = (name) => {
|
|
13942
13973
|
const { title, description, annotations } = seal_mcp_v1_default.tools[name];
|
|
13943
13974
|
return { title, description, annotations };
|
|
@@ -14313,7 +14344,7 @@ async function runInit(opts) {
|
|
|
14313
14344
|
if (err2 instanceof KeytarUnavailableError) {
|
|
14314
14345
|
logStderr(`${PROGRAM_NAME} init: warning: ${err2.message}`);
|
|
14315
14346
|
logStderr(
|
|
14316
|
-
`${PROGRAM_NAME} init: state.json IS encrypted, but you must export ${PASSPHRASE_ENV_VAR} before every \`${PROGRAM_NAME} serve\` until
|
|
14347
|
+
`${PROGRAM_NAME} init: state.json IS encrypted, but you must export ${PASSPHRASE_ENV_VAR} before every \`${PROGRAM_NAME} serve\` until the OS keychain works.`
|
|
14317
14348
|
);
|
|
14318
14349
|
} else {
|
|
14319
14350
|
throw err2;
|
|
@@ -14487,7 +14518,7 @@ async function runDoctor(opts) {
|
|
|
14487
14518
|
}
|
|
14488
14519
|
if (!kc.available) {
|
|
14489
14520
|
logStderr(
|
|
14490
|
-
`hint: keychain unavailable.
|
|
14521
|
+
`hint: keychain unavailable. On Linux start a Secret Service (gnome-keyring or KWallet), or rely on ${PASSPHRASE_ENV_VAR}; with neither, \`serve\` runs ephemeral (handles live until restart).`
|
|
14491
14522
|
);
|
|
14492
14523
|
} else if (stateExists && !kc.hasEntry && !report.env_passphrase_set) {
|
|
14493
14524
|
logStderr(`hint: keychain has no entry and ${PASSPHRASE_ENV_VAR} is unset \u2014 \`serve\` will fail.`);
|
package/dist/cli.js
CHANGED
|
@@ -7071,6 +7071,7 @@ async function probeStateLock(stateDir, deps = {}) {
|
|
|
7071
7071
|
}
|
|
7072
7072
|
|
|
7073
7073
|
// src/passphrase.ts
|
|
7074
|
+
import { execFile } from "child_process";
|
|
7074
7075
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
7075
7076
|
var SERVICE_NAME = "seal-mcp";
|
|
7076
7077
|
var ACCOUNT_NAME = "state";
|
|
@@ -7094,23 +7095,50 @@ var PassphraseNotFoundError = class extends Error {
|
|
|
7094
7095
|
this.name = "PassphraseNotFoundError";
|
|
7095
7096
|
}
|
|
7096
7097
|
};
|
|
7098
|
+
function keyringEntry(keyring, platform, service, account) {
|
|
7099
|
+
return platform === "win32" ? keyring.AsyncEntry.withTarget(`${service}/${account}`, service, account) : new keyring.AsyncEntry(service, account, { linux: { store: "secret-service" } });
|
|
7100
|
+
}
|
|
7101
|
+
function readKeytarLinuxEntry(service, account) {
|
|
7102
|
+
return new Promise((resolve5) => {
|
|
7103
|
+
execFile("secret-tool", ["lookup", "service", service, "account", account], { timeout: 5e3 }, (err2, stdout) => {
|
|
7104
|
+
const value = err2 ? "" : stdout.replace(/\n$/, "");
|
|
7105
|
+
resolve5(value.length > 0 ? value : null);
|
|
7106
|
+
});
|
|
7107
|
+
});
|
|
7108
|
+
}
|
|
7109
|
+
function keytarOverKeyring(keyring, platform = process.platform, readLegacy = readKeytarLinuxEntry) {
|
|
7110
|
+
const entry = (service, account) => keyringEntry(keyring, platform, service, account);
|
|
7111
|
+
const store = (service, account, password) => entry(service, account).setSecret(Buffer.from(password, "utf8"));
|
|
7112
|
+
return {
|
|
7113
|
+
async getPassword(service, account) {
|
|
7114
|
+
const secret = await entry(service, account).getSecret();
|
|
7115
|
+
if (secret !== void 0) return Buffer.from(secret).toString("utf8");
|
|
7116
|
+
const legacy = platform === "linux" ? await readLegacy(service, account) : null;
|
|
7117
|
+
if (legacy !== null) await store(service, account, legacy).catch(() => void 0);
|
|
7118
|
+
return legacy;
|
|
7119
|
+
},
|
|
7120
|
+
setPassword: store,
|
|
7121
|
+
deletePassword: (service, account) => entry(service, account).deleteCredential()
|
|
7122
|
+
};
|
|
7123
|
+
}
|
|
7097
7124
|
var keytarCache = void 0;
|
|
7098
7125
|
async function loadKeytar() {
|
|
7099
7126
|
if (keytarCache !== void 0) return keytarCache;
|
|
7100
7127
|
try {
|
|
7101
|
-
const mod = await import("
|
|
7102
|
-
|
|
7103
|
-
|
|
7128
|
+
const mod = await import("@napi-rs/keyring");
|
|
7129
|
+
const keyring = mod.default ?? mod;
|
|
7130
|
+
keyringEntry(keyring, process.platform, SERVICE_NAME, ACCOUNT_NAME);
|
|
7131
|
+
keytarCache = keytarOverKeyring(keyring);
|
|
7104
7132
|
} catch {
|
|
7105
7133
|
keytarCache = null;
|
|
7106
|
-
return null;
|
|
7107
7134
|
}
|
|
7135
|
+
return keytarCache;
|
|
7108
7136
|
}
|
|
7109
7137
|
async function loadKeytarOrThrow() {
|
|
7110
7138
|
const k = await loadKeytar();
|
|
7111
7139
|
if (k === null) {
|
|
7112
7140
|
throw new KeytarUnavailableError(
|
|
7113
|
-
"
|
|
7141
|
+
"no OS keychain. On Linux start a Secret Service (gnome-keyring or KWallet); on macOS/Windows this should never happen \u2014 file a bug."
|
|
7114
7142
|
);
|
|
7115
7143
|
}
|
|
7116
7144
|
return k;
|
|
@@ -11315,13 +11343,13 @@ var seal_mcp_v1_default = {
|
|
|
11315
11343
|
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`."
|
|
11316
11344
|
},
|
|
11317
11345
|
server_instructions: {
|
|
11318
|
-
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).",
|
|
11346
|
+
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.",
|
|
11319
11347
|
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."
|
|
11320
11348
|
},
|
|
11321
11349
|
tools: {
|
|
11322
11350
|
seal_share: {
|
|
11323
11351
|
title: "Share a file via SEAL",
|
|
11324
|
-
description: "Give the user or another agent a file or a secret WITHOUT it passing through this chat. Works for
|
|
11352
|
+
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.",
|
|
11325
11353
|
annotations: { destructiveHint: false, openWorldHint: true },
|
|
11326
11354
|
input: {
|
|
11327
11355
|
properties: {
|
|
@@ -11371,7 +11399,9 @@ var seal_mcp_v1_default = {
|
|
|
11371
11399
|
expires_at: { type: "string", format: "date-time", description: "ISO 8601 UTC expiry timestamp." }
|
|
11372
11400
|
},
|
|
11373
11401
|
MUST_NOT_contain: ["share_url", "url", "password", "passphrase", "link_key", "owner_token"],
|
|
11374
|
-
rationale: "Anything in this object reaches the model. Any field that lets the model reconstruct or re-issue the share capability defeats handoff mode."
|
|
11402
|
+
rationale: "Anything in this object reaches the model. Any field that lets the model reconstruct or re-issue the share capability defeats handoff mode.",
|
|
11403
|
+
receipt_clipboard: "The seal link is on your clipboard. Paste it to the recipient.",
|
|
11404
|
+
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."
|
|
11375
11405
|
},
|
|
11376
11406
|
output_forward: {
|
|
11377
11407
|
type: "object",
|
|
@@ -11769,7 +11799,7 @@ var seal_mcp_v1_default = {
|
|
|
11769
11799
|
{
|
|
11770
11800
|
rank: 1,
|
|
11771
11801
|
method: "os_keychain",
|
|
11772
|
-
implementation: "
|
|
11802
|
+
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. It replaced keytar in 0.2.4 and finds the entry keytar wrote (Windows credential `seal-mcp/state`, the passphrase as UTF-8 bytes; on Linux the old entry is read once through `secret-tool` and stored anew). Service id: 'seal-mcp', account: 'state'. Set during `sealnet-mcp init` (chosen by the user), or by the first `sealnet-mcp serve` without state, which generates a random 32-byte passphrase itself \u2014 no prompt, no `init` step. Read on every subsequent process start.",
|
|
11773
11803
|
default: true
|
|
11774
11804
|
},
|
|
11775
11805
|
{
|
|
@@ -12917,7 +12947,7 @@ async function sealProFileGet(source, input) {
|
|
|
12917
12947
|
}
|
|
12918
12948
|
|
|
12919
12949
|
// src/tools/request.ts
|
|
12920
|
-
import { execFile } from "child_process";
|
|
12950
|
+
import { execFile as execFile2 } from "child_process";
|
|
12921
12951
|
import { existsSync } from "fs";
|
|
12922
12952
|
import { mkdtemp as mkdtemp2 } from "fs/promises";
|
|
12923
12953
|
import { tmpdir as tmpdir2 } from "os";
|
|
@@ -12936,7 +12966,7 @@ function runSealCli(args) {
|
|
|
12936
12966
|
const cli = findSealCli();
|
|
12937
12967
|
if (!cli) return Promise.resolve(null);
|
|
12938
12968
|
return new Promise((resolve5) => {
|
|
12939
|
-
|
|
12969
|
+
execFile2(cli, [...args], { maxBuffer: 1024 * 1024 }, (_err, stdout) => {
|
|
12940
12970
|
try {
|
|
12941
12971
|
resolve5(JSON.parse(stdout));
|
|
12942
12972
|
} catch {
|
|
@@ -13307,6 +13337,7 @@ function guessMimeFromName(name) {
|
|
|
13307
13337
|
}
|
|
13308
13338
|
var VALID_TTL = ["5m", "30m", "1h", "1d", "7d"];
|
|
13309
13339
|
var EXPIRE_DEFAULT = seal_mcp_v1_default.tools.seal_share.expire_default;
|
|
13340
|
+
var HANDOFF_OUTPUT = seal_mcp_v1_default.tools.seal_share.output_handoff;
|
|
13310
13341
|
var TIER_BYTES2 = limits_default2.limits.TIER_BYTES;
|
|
13311
13342
|
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.";
|
|
13312
13343
|
function defaultExpire(mode, size) {
|
|
@@ -13707,7 +13738,7 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
|
|
|
13707
13738
|
appendAudit(ctx, "seal_share", handle, mode);
|
|
13708
13739
|
delete ctx.state.pending[handle];
|
|
13709
13740
|
await ctx.persist();
|
|
13710
|
-
const delivered = handoff.channel === "clipboard" ?
|
|
13741
|
+
const delivered = handoff.channel === "clipboard" ? HANDOFF_OUTPUT.receipt_clipboard : HANDOFF_OUTPUT.receipt_file.replace("{file}", handoff.file ?? "");
|
|
13711
13742
|
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.`;
|
|
13712
13743
|
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;
|
|
13713
13744
|
return {
|
|
@@ -13909,7 +13940,7 @@ function proFileGetOutputToWire(out) {
|
|
|
13909
13940
|
}
|
|
13910
13941
|
|
|
13911
13942
|
// src/server.ts
|
|
13912
|
-
var SERVER_VERSION = "0.2.
|
|
13943
|
+
var SERVER_VERSION = "0.2.5";
|
|
13913
13944
|
var toolMeta = (name) => {
|
|
13914
13945
|
const { title, description, annotations } = seal_mcp_v1_default.tools[name];
|
|
13915
13946
|
return { title, description, annotations };
|
|
@@ -14285,7 +14316,7 @@ async function runInit(opts) {
|
|
|
14285
14316
|
if (err2 instanceof KeytarUnavailableError) {
|
|
14286
14317
|
logStderr(`${PROGRAM_NAME} init: warning: ${err2.message}`);
|
|
14287
14318
|
logStderr(
|
|
14288
|
-
`${PROGRAM_NAME} init: state.json IS encrypted, but you must export ${PASSPHRASE_ENV_VAR} before every \`${PROGRAM_NAME} serve\` until
|
|
14319
|
+
`${PROGRAM_NAME} init: state.json IS encrypted, but you must export ${PASSPHRASE_ENV_VAR} before every \`${PROGRAM_NAME} serve\` until the OS keychain works.`
|
|
14289
14320
|
);
|
|
14290
14321
|
} else {
|
|
14291
14322
|
throw err2;
|
|
@@ -14459,7 +14490,7 @@ async function runDoctor(opts) {
|
|
|
14459
14490
|
}
|
|
14460
14491
|
if (!kc.available) {
|
|
14461
14492
|
logStderr(
|
|
14462
|
-
`hint: keychain unavailable.
|
|
14493
|
+
`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).`
|
|
14463
14494
|
);
|
|
14464
14495
|
} else if (stateExists && !kc.hasEntry && !report.env_passphrase_set) {
|
|
14465
14496
|
logStderr(`hint: keychain has no entry and ${PASSPHRASE_ENV_VAR} is unset \u2014 \`serve\` will fail.`);
|
package/dist/index.cjs
CHANGED
|
@@ -4138,13 +4138,13 @@ var seal_mcp_v1_default = {
|
|
|
4138
4138
|
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`."
|
|
4139
4139
|
},
|
|
4140
4140
|
server_instructions: {
|
|
4141
|
-
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).",
|
|
4141
|
+
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.",
|
|
4142
4142
|
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."
|
|
4143
4143
|
},
|
|
4144
4144
|
tools: {
|
|
4145
4145
|
seal_share: {
|
|
4146
4146
|
title: "Share a file via SEAL",
|
|
4147
|
-
description: "Give the user or another agent a file or a secret WITHOUT it passing through this chat. Works for
|
|
4147
|
+
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.",
|
|
4148
4148
|
annotations: { destructiveHint: false, openWorldHint: true },
|
|
4149
4149
|
input: {
|
|
4150
4150
|
properties: {
|
|
@@ -4194,7 +4194,9 @@ var seal_mcp_v1_default = {
|
|
|
4194
4194
|
expires_at: { type: "string", format: "date-time", description: "ISO 8601 UTC expiry timestamp." }
|
|
4195
4195
|
},
|
|
4196
4196
|
MUST_NOT_contain: ["share_url", "url", "password", "passphrase", "link_key", "owner_token"],
|
|
4197
|
-
rationale: "Anything in this object reaches the model. Any field that lets the model reconstruct or re-issue the share capability defeats handoff mode."
|
|
4197
|
+
rationale: "Anything in this object reaches the model. Any field that lets the model reconstruct or re-issue the share capability defeats handoff mode.",
|
|
4198
|
+
receipt_clipboard: "The seal link is on your clipboard. Paste it to the recipient.",
|
|
4199
|
+
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."
|
|
4198
4200
|
},
|
|
4199
4201
|
output_forward: {
|
|
4200
4202
|
type: "object",
|
|
@@ -4592,7 +4594,7 @@ var seal_mcp_v1_default = {
|
|
|
4592
4594
|
{
|
|
4593
4595
|
rank: 1,
|
|
4594
4596
|
method: "os_keychain",
|
|
4595
|
-
implementation: "
|
|
4597
|
+
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. It replaced keytar in 0.2.4 and finds the entry keytar wrote (Windows credential `seal-mcp/state`, the passphrase as UTF-8 bytes; on Linux the old entry is read once through `secret-tool` and stored anew). Service id: 'seal-mcp', account: 'state'. Set during `sealnet-mcp init` (chosen by the user), or by the first `sealnet-mcp serve` without state, which generates a random 32-byte passphrase itself \u2014 no prompt, no `init` step. Read on every subsequent process start.",
|
|
4596
4598
|
default: true
|
|
4597
4599
|
},
|
|
4598
4600
|
{
|
|
@@ -13190,6 +13192,7 @@ function guessMimeFromName(name) {
|
|
|
13190
13192
|
}
|
|
13191
13193
|
var VALID_TTL = ["5m", "30m", "1h", "1d", "7d"];
|
|
13192
13194
|
var EXPIRE_DEFAULT = seal_mcp_v1_default.tools.seal_share.expire_default;
|
|
13195
|
+
var HANDOFF_OUTPUT = seal_mcp_v1_default.tools.seal_share.output_handoff;
|
|
13193
13196
|
var TIER_BYTES2 = limits_default2.limits.TIER_BYTES;
|
|
13194
13197
|
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.";
|
|
13195
13198
|
function defaultExpire(mode, size) {
|
|
@@ -13590,7 +13593,7 @@ async function deliver(ctx, deps, { mode, recipientPub, expireMs }, { handle, op
|
|
|
13590
13593
|
appendAudit(ctx, "seal_share", handle, mode);
|
|
13591
13594
|
delete ctx.state.pending[handle];
|
|
13592
13595
|
await ctx.persist();
|
|
13593
|
-
const delivered = handoff.channel === "clipboard" ?
|
|
13596
|
+
const delivered = handoff.channel === "clipboard" ? HANDOFF_OUTPUT.receipt_clipboard : HANDOFF_OUTPUT.receipt_file.replace("{file}", handoff.file ?? "");
|
|
13594
13597
|
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.`;
|
|
13595
13598
|
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;
|
|
13596
13599
|
return {
|
|
@@ -13792,7 +13795,7 @@ function proFileGetOutputToWire(out) {
|
|
|
13792
13795
|
}
|
|
13793
13796
|
|
|
13794
13797
|
// src/server.ts
|
|
13795
|
-
var SERVER_VERSION = "0.2.
|
|
13798
|
+
var SERVER_VERSION = "0.2.5";
|
|
13796
13799
|
var toolMeta = (name) => {
|
|
13797
13800
|
const { title, description, annotations } = seal_mcp_v1_default.tools[name];
|
|
13798
13801
|
return { title, description, annotations };
|
|
@@ -14141,6 +14144,7 @@ function osDefaultDir(env, platform, home) {
|
|
|
14141
14144
|
}
|
|
14142
14145
|
|
|
14143
14146
|
// src/passphrase.ts
|
|
14147
|
+
var import_node_child_process2 = require("child_process");
|
|
14144
14148
|
var import_node_crypto4 = require("crypto");
|
|
14145
14149
|
var SERVICE_NAME = "seal-mcp";
|
|
14146
14150
|
var ACCOUNT_NAME = "state";
|
|
@@ -14164,23 +14168,50 @@ var PassphraseNotFoundError = class extends Error {
|
|
|
14164
14168
|
this.name = "PassphraseNotFoundError";
|
|
14165
14169
|
}
|
|
14166
14170
|
};
|
|
14171
|
+
function keyringEntry(keyring, platform, service, account) {
|
|
14172
|
+
return platform === "win32" ? keyring.AsyncEntry.withTarget(`${service}/${account}`, service, account) : new keyring.AsyncEntry(service, account, { linux: { store: "secret-service" } });
|
|
14173
|
+
}
|
|
14174
|
+
function readKeytarLinuxEntry(service, account) {
|
|
14175
|
+
return new Promise((resolve5) => {
|
|
14176
|
+
(0, import_node_child_process2.execFile)("secret-tool", ["lookup", "service", service, "account", account], { timeout: 5e3 }, (err2, stdout) => {
|
|
14177
|
+
const value = err2 ? "" : stdout.replace(/\n$/, "");
|
|
14178
|
+
resolve5(value.length > 0 ? value : null);
|
|
14179
|
+
});
|
|
14180
|
+
});
|
|
14181
|
+
}
|
|
14182
|
+
function keytarOverKeyring(keyring, platform = process.platform, readLegacy = readKeytarLinuxEntry) {
|
|
14183
|
+
const entry = (service, account) => keyringEntry(keyring, platform, service, account);
|
|
14184
|
+
const store = (service, account, password) => entry(service, account).setSecret(Buffer.from(password, "utf8"));
|
|
14185
|
+
return {
|
|
14186
|
+
async getPassword(service, account) {
|
|
14187
|
+
const secret = await entry(service, account).getSecret();
|
|
14188
|
+
if (secret !== void 0) return Buffer.from(secret).toString("utf8");
|
|
14189
|
+
const legacy = platform === "linux" ? await readLegacy(service, account) : null;
|
|
14190
|
+
if (legacy !== null) await store(service, account, legacy).catch(() => void 0);
|
|
14191
|
+
return legacy;
|
|
14192
|
+
},
|
|
14193
|
+
setPassword: store,
|
|
14194
|
+
deletePassword: (service, account) => entry(service, account).deleteCredential()
|
|
14195
|
+
};
|
|
14196
|
+
}
|
|
14167
14197
|
var keytarCache = void 0;
|
|
14168
14198
|
async function loadKeytar() {
|
|
14169
14199
|
if (keytarCache !== void 0) return keytarCache;
|
|
14170
14200
|
try {
|
|
14171
|
-
const mod = await import("
|
|
14172
|
-
|
|
14173
|
-
|
|
14201
|
+
const mod = await import("@napi-rs/keyring");
|
|
14202
|
+
const keyring = mod.default ?? mod;
|
|
14203
|
+
keyringEntry(keyring, process.platform, SERVICE_NAME, ACCOUNT_NAME);
|
|
14204
|
+
keytarCache = keytarOverKeyring(keyring);
|
|
14174
14205
|
} catch {
|
|
14175
14206
|
keytarCache = null;
|
|
14176
|
-
return null;
|
|
14177
14207
|
}
|
|
14208
|
+
return keytarCache;
|
|
14178
14209
|
}
|
|
14179
14210
|
async function loadKeytarOrThrow() {
|
|
14180
14211
|
const k = await loadKeytar();
|
|
14181
14212
|
if (k === null) {
|
|
14182
14213
|
throw new KeytarUnavailableError(
|
|
14183
|
-
"
|
|
14214
|
+
"no OS keychain. On Linux start a Secret Service (gnome-keyring or KWallet); on macOS/Windows this should never happen \u2014 file a bug."
|
|
14184
14215
|
);
|
|
14185
14216
|
}
|
|
14186
14217
|
return k;
|