ramwisp 0.1.1

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 ADDED
@@ -0,0 +1,12 @@
1
+ # ramwisp
2
+
3
+ Subagentes do Claude Code e do Codex em máquinas próprias na nuvem, com a RAM que precisarem.
4
+ Cada subagente roda numa AWS Nitro Enclave efêmera: antes de enviar qualquer coisa, este MCP confere a
5
+ atestação do hardware (raiz AWS Nitro Enclaves G1 + PCR0 publicado) e cifra missão, projeto e credencial
6
+ só para aquela enclave.
7
+
8
+ ```bash
9
+ claude mcp add --scope user ramwisp -- npx -y ramwisp@latest
10
+ ```
11
+
12
+ No primeiro uso ele abre o navegador para você entrar na sua conta. Mais em https://ramwisp.duckdns.org
package/bin/wisp.js ADDED
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env node
2
+ // Sem argumentos: servidor MCP (é o que o Claude Code/Codex executa). Com argumentos: CLI para humanos.
3
+ import { API, getToken, logout, startLogin } from "../src/account.js";
4
+ import { killAgent, listAgents, result, sealed, spawnAgent, waitAgent } from "../src/client.js";
5
+ import { serve } from "../src/mcp.js";
6
+
7
+ const [cmd, ...rest] = process.argv.slice(2);
8
+ const flag = (name, d) => { const i = rest.indexOf(`--${name}`); return i >= 0 ? rest[i + 1] : d; };
9
+ const out = (x) => console.log(JSON.stringify(x, null, 2));
10
+
11
+ const HELP = `wisp — subagentes com RAM sob demanda (${API})
12
+
13
+ wisp login conecta esta máquina à sua conta (navegador)
14
+ wisp logout
15
+ wisp spawn "missão" [--ram 2] [--engine claude|codex] [--model M] [--auth auto|login|key] [--workspace DIR] [--wait]
16
+ wisp wait ID | result ID | kill ID
17
+ wisp ls
18
+ wisp setup mostra como adicionar o MCP ao Claude Code / Codex
19
+
20
+ Sem argumentos roda o servidor MCP (stdio).`;
21
+
22
+ async function main() {
23
+ switch (cmd) {
24
+ case undefined: case "mcp": return serve();
25
+ case "login": {
26
+ if (getToken()) return console.log(`já conectado a ${API} (wisp logout para trocar)`);
27
+ const l = await startLogin();
28
+ console.log(`${l.opened ? "Abri o navegador. Se não abriu, acesse" : "Abra"}: ${l.verification_uri_complete}\nCódigo: ${l.user_code}`);
29
+ await l.done;
30
+ return console.log("conectado ✓");
31
+ }
32
+ case "logout": logout(); return console.log("desconectado");
33
+ case "spawn": {
34
+ const r = await spawnAgent({ mission: rest[0], ram_gb: Number(flag("ram", 2)), engine: flag("engine", "claude"),
35
+ model: flag("model"), auth: flag("auth", "auto"), timeout_s: Number(flag("timeout", 1800)), label: flag("label"), workspace: flag("workspace") });
36
+ out(r);
37
+ console.error("esperando a máquina e a atestação para mandar a missão selada…");
38
+ await sealed(r.id);
39
+ const now = await result(r.id);
40
+ if (now.erro || ["failed", "killed", "expired"].includes(now.status)) { out(now); process.exit(1); }
41
+ if (rest.includes("--wait")) return out(await waitAgent(r.id, 3600));
42
+ return console.error(`missão entregue. Recolha com: wisp wait ${r.id}`);
43
+ }
44
+ case "wait": return out(await waitAgent(rest[0], Number(flag("max", 3600))));
45
+ case "result": return out(await result(rest[0]));
46
+ case "kill": return out(await killAgent(rest[0]));
47
+ case "ls": return out(await listAgents());
48
+ case "setup":
49
+ return console.log(`Claude Code:\n claude mcp add --scope user wisp -- npx -y ${API}/wisp.tgz\n\n` +
50
+ `Codex (~/.codex/config.toml):\n [mcp_servers.wisp]\n command = "npx"\n args = ["-y", "${API}/wisp.tgz"]\n tool_timeout_sec = 1000`);
51
+ default: return console.log(HELP);
52
+ }
53
+ }
54
+
55
+ main().catch((e) => { console.error("erro:", e.message); process.exit(1); });
@@ -0,0 +1,14 @@
1
+ -----BEGIN CERTIFICATE-----
2
+ MIICETCCAZagAwIBAgIRAPkxdWgbkK/hHUbMtOTn+FYwCgYIKoZIzj0EAwMwSTEL
3
+ MAkGA1UEBhMCVVMxDzANBgNVBAoMBkFtYXpvbjEMMAoGA1UECwwDQVdTMRswGQYD
4
+ VQQDDBJhd3Mubml0cm8tZW5jbGF2ZXMwHhcNMTkxMDI4MTMyODA1WhcNNDkxMDI4
5
+ MTQyODA1WjBJMQswCQYDVQQGEwJVUzEPMA0GA1UECgwGQW1hem9uMQwwCgYDVQQL
6
+ DANBV1MxGzAZBgNVBAMMEmF3cy5uaXRyby1lbmNsYXZlczB2MBAGByqGSM49AgEG
7
+ BSuBBAAiA2IABPwCVOumCMHzaHDimtqQvkY4MpJzbolL//Zy2YlES1BR5TSksfbb
8
+ 48C8WBoyt7F2Bw7eEtaaP+ohG2bnUs990d0JX28TcPQXCEPZ3BABIeTPYwEoCWZE
9
+ h8l5YoQwTcU/9KNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUkCW1DdkF
10
+ R+eWw5b6cp3PmanfS5YwDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2kAMGYC
11
+ MQCjfy+Rocm9Xue4YnwWmNJVA44fA0P5W2OpYow9OYCVRaEevL8uO1XYru5xtMPW
12
+ rfMCMQCi85sWBbJwKKXdS6BptQFuZbT73o/gBh1qUxl/nNr12UO8Yfwr6wPLb+6N
13
+ IwLz3/Y=
14
+ -----END CERTIFICATE-----
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "ramwisp",
3
+ "version": "0.1.1",
4
+ "description": "ramwisp: subagentes do Claude Code e do Codex em máquinas próprias na nuvem (AWS Nitro Enclaves), com a sua assinatura. MCP + CLI.",
5
+ "type": "module",
6
+ "bin": {
7
+ "ramwisp": "bin/wisp.js",
8
+ "wisp": "bin/wisp.js"
9
+ },
10
+ "files": [
11
+ "bin",
12
+ "src",
13
+ "nitro-root-g1.pem",
14
+ "pcrs.json",
15
+ "README.md"
16
+ ],
17
+ "engines": {
18
+ "node": ">=20"
19
+ },
20
+ "scripts": {
21
+ "test": "node --test test/"
22
+ },
23
+ "license": "UNLICENSED",
24
+ "keywords": [
25
+ "mcp",
26
+ "claude-code",
27
+ "codex",
28
+ "subagents",
29
+ "nitro-enclaves"
30
+ ],
31
+ "homepage": "https://ramwisp.duckdns.org"
32
+ }
package/pcrs.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "pcr0": [
3
+ "32d23882b517b1b6b835934e0b684f29e2add4096c1ca42302b33ce38d202f39c3bf8d2ffff9001e355810800e2d0a4e"
4
+ ],
5
+ "builds": {
6
+ "32d23882b517b1b6b835934e0b684f29e2add4096c1ca42302b33ce38d202f39c3bf8d2ffff9001e355810800e2d0a4e": "380f1c5"
7
+ }
8
+ }
package/src/account.js ADDED
@@ -0,0 +1,97 @@
1
+ // Conta wisp (não é a credencial do motor): token wsp_… guardado em ~/.config/wisp/credentials.json (0600).
2
+ import { spawn } from "node:child_process";
3
+ import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
4
+ import { homedir, hostname } from "node:os";
5
+ import { join } from "node:path";
6
+
7
+ export const API = (process.env.WISP_API ?? "https://ramwisp.duckdns.org").replace(/\/$/, "");
8
+ export const DIR = process.env.WISP_CONFIG_DIR ?? join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "wisp");
9
+ const CRED = join(DIR, "credentials.json");
10
+
11
+ export function ensureDir(sub = "") {
12
+ const d = join(DIR, sub);
13
+ mkdirSync(d, { recursive: true, mode: 0o700 });
14
+ return d;
15
+ }
16
+
17
+ export function writeSecret(path, data) {
18
+ writeFileSync(path, data, { mode: 0o600 });
19
+ chmodSync(path, 0o600);
20
+ }
21
+
22
+ export function getToken() {
23
+ if (process.env.WISP_TOKEN) return process.env.WISP_TOKEN;
24
+ try {
25
+ const c = JSON.parse(readFileSync(CRED, "utf8"));
26
+ return c.api === API ? c.token : null;
27
+ } catch { return null; }
28
+ }
29
+
30
+ export function saveToken(token) {
31
+ ensureDir();
32
+ writeSecret(CRED, JSON.stringify({ api: API, token, saved_at: new Date().toISOString() }));
33
+ }
34
+
35
+ export function logout() {
36
+ if (existsSync(CRED)) rmSync(CRED);
37
+ }
38
+
39
+ export async function call(method, path, body, { token = getToken(), timeoutMs = 30_000 } = {}) {
40
+ const r = await fetch(API + path, {
41
+ method,
42
+ headers: { "Content-Type": "application/json", ...(token ? { Authorization: `Bearer ${token}` } : {}) },
43
+ body: body === undefined ? undefined : JSON.stringify(body),
44
+ signal: AbortSignal.timeout(timeoutMs),
45
+ });
46
+ const text = await r.text();
47
+ let data = null;
48
+ try { data = text ? JSON.parse(text) : null; } catch { data = { error: text.slice(0, 300) }; }
49
+ if (!r.ok) {
50
+ const err = new Error(data?.error ?? `HTTP ${r.status}`);
51
+ err.status = r.status;
52
+ throw err;
53
+ }
54
+ return data;
55
+ }
56
+
57
+ export function openBrowser(url) {
58
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
59
+ const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
60
+ if (process.platform === "linux" && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) return false;
61
+ try {
62
+ const p = spawn(cmd, args, { stdio: "ignore", detached: true });
63
+ p.on("error", () => {});
64
+ p.unref();
65
+ return true;
66
+ } catch { return false; }
67
+ }
68
+
69
+ let pending = null; // login em andamento (um por processo)
70
+
71
+ /** Começa (ou reaproveita) o login pelo navegador. Resolve quando aprovado. */
72
+ export function startLogin() {
73
+ if (pending) return pending;
74
+ const p = (async () => {
75
+ const d = await call("POST", "/api/device/start", { client_name: `MCP em ${hostname()}` }, { token: null });
76
+ const opened = openBrowser(d.verification_uri_complete);
77
+ const done = (async () => {
78
+ const until = Date.now() + d.expires_in * 1000;
79
+ while (Date.now() < until) {
80
+ await new Promise((r) => setTimeout(r, d.interval * 1000));
81
+ try {
82
+ const r = await call("POST", "/api/device/poll", { device_code: d.device_code }, { token: null });
83
+ saveToken(r.token);
84
+ return r.token;
85
+ } catch (e) {
86
+ if (e.status !== 428) throw e;
87
+ }
88
+ }
89
+ throw new Error("o login expirou; tente de novo");
90
+ })();
91
+ done.finally(() => { pending = null; }).catch(() => {});
92
+ return { ...d, opened, done };
93
+ })();
94
+ pending = p;
95
+ p.catch(() => { pending = null; });
96
+ return p;
97
+ }
package/src/attest.js ADDED
@@ -0,0 +1,65 @@
1
+ // Confere o documento de atestação da enclave ANTES de qualquer segredo sair desta máquina.
2
+ // Raiz confiável: AWS Nitro Enclaves Root G1 (nitro-root-g1.pem, sha256 do zip oficial conferido).
3
+ // Imagens aceitas: PCR0 em pcrs.json (hash da imagem publicada da enclave).
4
+ import { X509Certificate, createHash, verify } from "node:crypto";
5
+ import { readFileSync } from "node:fs";
6
+ import { decode, encodeSigStructure } from "./cbor.js";
7
+
8
+ const here = new URL("..", import.meta.url);
9
+ const NITRO_ROOT = new X509Certificate(readFileSync(new URL("nitro-root-g1.pem", here)));
10
+ const DEV_PCR0 = createHash("sha384").update("wisp-dev-image").digest("hex");
11
+ const MAX_AGE_MS = 15 * 60_000;
12
+
13
+ export function allowedPcr0() {
14
+ const extra = (process.env.WISP_EXTRA_PCR0 ?? "").split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
15
+ const pinned = JSON.parse(readFileSync(new URL("pcrs.json", here), "utf8")).pcr0 ?? [];
16
+ return [...pinned.map((p) => p.toLowerCase()), ...extra];
17
+ }
18
+
19
+ /**
20
+ * @returns {{enclavePub: Buffer, pcr0: string, moduleId: string, timestamp: number, dev: boolean}}
21
+ * Lança erro se QUALQUER coisa não bater.
22
+ */
23
+ export function verifyAttestation(docB64, expectedNonce, { devRootPem } = {}) {
24
+ const doc = decode(Buffer.from(docB64, "base64"));
25
+ if (!Array.isArray(doc) || doc.length !== 4) throw new Error("atestação: não é COSE_Sign1");
26
+ const [protectedBytes, , payloadBytes, signature] = doc;
27
+ const prot = decode(protectedBytes);
28
+ if (!(prot instanceof Map) || prot.get(1) !== -35) throw new Error("atestação: algoritmo não é ES384");
29
+ const p = decode(payloadBytes);
30
+ const get = (k) => { const v = p.get(k); if (v === undefined || v === null) throw new Error(`atestação: falta ${k}`); return v; };
31
+
32
+ // cadeia: cabundle[0] é a raiz, depois intermediárias, por fim o certificado da enclave
33
+ const bundle = get("cabundle").map((der) => new X509Certificate(der));
34
+ const leaf = new X509Certificate(get("certificate"));
35
+ const dev = !!devRootPem;
36
+ const root = dev ? new X509Certificate(devRootPem) : NITRO_ROOT;
37
+ if (!bundle.length || !bundle[0].raw.equals(root.raw)) throw new Error("atestação: raiz não é a AWS Nitro Enclaves");
38
+ const chain = [...bundle, leaf];
39
+ const at = new Date();
40
+ for (let i = 0; i < chain.length; i++) {
41
+ const cert = chain[i];
42
+ const issuer = i === 0 ? root : chain[i - 1];
43
+ if (!cert.verify(issuer.publicKey)) throw new Error(`atestação: assinatura do certificado ${i} inválida`);
44
+ if (at < new Date(cert.validFrom) || at > new Date(cert.validTo)) throw new Error(`atestação: certificado ${i} fora da validade`);
45
+ if (i < chain.length - 1 && !cert.ca) throw new Error(`atestação: certificado ${i} não é CA`);
46
+ }
47
+ const ok = verify("sha384", encodeSigStructure(protectedBytes, payloadBytes),
48
+ { key: leaf.publicKey, dsaEncoding: "ieee-p1363" }, signature);
49
+ if (!ok) throw new Error("atestação: assinatura COSE inválida");
50
+
51
+ if (get("digest") !== "SHA384") throw new Error("atestação: digest inesperado");
52
+ const ts = get("timestamp");
53
+ if (Math.abs(Date.now() - ts) > MAX_AGE_MS) throw new Error("atestação: velha demais ou do futuro");
54
+ if (!Buffer.from(get("nonce")).equals(expectedNonce)) throw new Error("atestação: nonce não confere (repetição?)");
55
+ const ud = p.get("user_data");
56
+ if (!ud || Buffer.from(ud).toString() !== "wisp-v1") throw new Error("atestação: protocolo desconhecido");
57
+ const enclavePub = Buffer.from(get("public_key"));
58
+ if (enclavePub.length !== 32) throw new Error("atestação: chave pública inválida");
59
+
60
+ const pcr0 = Buffer.from(get("pcrs").get(0)).toString("hex");
61
+ if (/^0+$/.test(pcr0)) throw new Error("atestação: enclave em modo debug (PCR0 zerado) — recusado");
62
+ const allowed = dev ? [DEV_PCR0] : allowedPcr0();
63
+ if (!allowed.includes(pcr0)) throw new Error(`atestação: imagem não reconhecida (PCR0 ${pcr0.slice(0, 16)}…)`);
64
+ return { enclavePub, pcr0, moduleId: get("module_id"), timestamp: ts, dev };
65
+ }
package/src/cbor.js ADDED
@@ -0,0 +1,80 @@
1
+ // Decodificador CBOR mínimo (RFC 8949) para o documento de atestação. Mapas viram Map (chaves podem ser inteiros).
2
+
3
+ export function decode(buf) {
4
+ const b = Buffer.from(buf);
5
+ const [v] = item(b, 0);
6
+ return v;
7
+ }
8
+
9
+ function item(b, i) {
10
+ const ib = b[i++];
11
+ const major = ib >> 5, ai = ib & 31;
12
+ if (major === 7) {
13
+ if (ai === 20) return [false, i];
14
+ if (ai === 21) return [true, i];
15
+ if (ai === 22 || ai === 23) return [null, i];
16
+ if (ai === 26) return [b.readFloatBE(i), i + 4];
17
+ if (ai === 27) return [b.readDoubleBE(i), i + 8];
18
+ throw new Error(`cbor: simple ${ai}`);
19
+ }
20
+ if (ai === 31) return indefinite(b, i, major); // comprimento indefinido (o NSM da AWS usa)
21
+ let n;
22
+ if (ai < 24) n = ai;
23
+ else if (ai === 24) { n = b[i]; i += 1; }
24
+ else if (ai === 25) { n = b.readUInt16BE(i); i += 2; }
25
+ else if (ai === 26) { n = b.readUInt32BE(i); i += 4; }
26
+ else if (ai === 27) { n = Number(b.readBigUInt64BE(i)); i += 8; }
27
+ else throw new Error(`cbor: info adicional ${ai}`);
28
+ switch (major) {
29
+ case 0: return [n, i];
30
+ case 1: return [-1 - n, i];
31
+ case 2: return [b.subarray(i, i + n), i + n];
32
+ case 3: return [b.subarray(i, i + n).toString("utf8"), i + n];
33
+ case 4: {
34
+ const out = [];
35
+ for (let k = 0; k < n; k++) { let v; [v, i] = item(b, i); out.push(v); }
36
+ return [out, i];
37
+ }
38
+ case 5: {
39
+ const out = new Map();
40
+ for (let k = 0; k < n; k++) { let key, v; [key, i] = item(b, i); [v, i] = item(b, i); out.set(key, v); }
41
+ return [out, i];
42
+ }
43
+ case 6: return item(b, i); // tag (COSE_Sign1 = 18): devolve o conteúdo
44
+ default: throw new Error(`cbor: major ${major}`);
45
+ }
46
+ }
47
+
48
+ /** Codificador só para o Sig_structure do COSE: ["Signature1", bstr, bstr, bstr]. */
49
+ export function encodeSigStructure(protectedBytes, payload) {
50
+ const head = (major, len) => {
51
+ if (len < 24) return Buffer.from([(major << 5) | len]);
52
+ if (len < 256) return Buffer.from([(major << 5) | 24, len]);
53
+ if (len < 65536) { const h = Buffer.alloc(3); h[0] = (major << 5) | 25; h.writeUInt16BE(len, 1); return h; }
54
+ const h = Buffer.alloc(5); h[0] = (major << 5) | 26; h.writeUInt32BE(len, 1); return h;
55
+ };
56
+ const bstr = (x) => Buffer.concat([head(2, x.length), x]);
57
+ const ctx = Buffer.from("Signature1");
58
+ return Buffer.concat([head(4, 4), head(3, ctx.length), ctx, bstr(protectedBytes), bstr(Buffer.alloc(0)), bstr(payload)]);
59
+ }
60
+
61
+ /** Itens de comprimento indefinido: pedaços ou elementos até o byte de parada 0xFF. */
62
+ function indefinite(b, i, major) {
63
+ if (major === 2 || major === 3) {
64
+ const parts = [];
65
+ while (b[i] !== 0xff) { let v; [v, i] = item(b, i); parts.push(major === 2 ? Buffer.from(v) : Buffer.from(v, "utf8")); }
66
+ const all = Buffer.concat(parts);
67
+ return [major === 2 ? all : all.toString("utf8"), i + 1];
68
+ }
69
+ if (major === 4) {
70
+ const out = [];
71
+ while (b[i] !== 0xff) { let v; [v, i] = item(b, i); out.push(v); }
72
+ return [out, i + 1];
73
+ }
74
+ if (major === 5) {
75
+ const out = new Map();
76
+ while (b[i] !== 0xff) { let k, v; [k, i] = item(b, i); [v, i] = item(b, i); out.set(k, v); }
77
+ return [out, i + 1];
78
+ }
79
+ throw new Error(`cbor: indefinido em major ${major}`);
80
+ }
package/src/client.js ADDED
@@ -0,0 +1,187 @@
1
+ // Ciclo de um subagente do lado do cliente: cria o job, espera a atestação, confere, sela e recolhe o resultado.
2
+ import { spawnSync } from "node:child_process";
3
+ import { randomBytes } from "node:crypto";
4
+ import { existsSync, readdirSync, readFileSync, rmSync, statSync } from "node:fs";
5
+ import { join, resolve } from "node:path";
6
+ import { call, ensureDir, getToken, writeSecret } from "./account.js";
7
+ import { verifyAttestation } from "./attest.js";
8
+ import { exportKey, importKey, newClientKey, openOutput, sealInput } from "./crypto.js";
9
+ import { engineCredential } from "./creds.js";
10
+
11
+ export const FINAL = ["done", "failed", "killed", "expired"];
12
+ const sealers = new Map(); // id -> Promise (selagem em andamento neste processo)
13
+ const sealErrors = new Map();
14
+
15
+ export class LoginRequired extends Error {}
16
+
17
+ const keyFile = (id) => join(ensureDir("jobs"), `${id}.json`);
18
+ const MAX_WORKSPACE = 15 * 1024 * 1024; // compactado
19
+ const SKIP_DIRS = new Set(["node_modules", ".git", ".venv", "venv", "__pycache__", "dist", "build", ".next", "target", ".cache"]);
20
+
21
+ /** Copia do projeto: no git, o que ele rastreia + arquivos novos não ignorados (nunca o que está no .gitignore). */
22
+ export function packWorkspace(dir) {
23
+ const root = resolve(dir);
24
+ if (!existsSync(root) || !statSync(root).isDirectory()) throw new Error(`workspace não é um diretório: ${root}`);
25
+ let files;
26
+ const git = spawnSync("git", ["-C", root, "ls-files", "-z", "-co", "--exclude-standard"], { maxBuffer: 256 * 1024 * 1024 });
27
+ if (git.status === 0) {
28
+ files = git.stdout.toString().split("\0").filter((f) => f && existsSync(join(root, f)));
29
+ } else {
30
+ files = [];
31
+ const walk = (rel) => {
32
+ for (const e of readdirSync(join(root, rel), { withFileTypes: true })) {
33
+ const r = rel ? `${rel}/${e.name}` : e.name;
34
+ if (e.isDirectory()) { if (!SKIP_DIRS.has(e.name)) walk(r); }
35
+ else if (e.isFile() && !/^\.env(\.|$)/.test(e.name)) files.push(r);
36
+ }
37
+ };
38
+ walk("");
39
+ }
40
+ if (!files.length) throw new Error(`workspace vazio: ${root}`);
41
+ const tar = spawnSync("tar", ["-czf", "-", "-C", root, "--null", "-T", "-"], { input: files.join("\0"), maxBuffer: 256 * 1024 * 1024 });
42
+ if (tar.status !== 0) throw new Error(`tar falhou: ${tar.stderr.toString().slice(0, 300)}`);
43
+ if (tar.stdout.length > MAX_WORKSPACE) {
44
+ throw new Error(`projeto grande demais (${(tar.stdout.length / 1048576).toFixed(1)} MB compactado, máximo 15 MB): ` +
45
+ "aponte workspace para um subdiretório ou ignore arquivos pesados no .gitignore");
46
+ }
47
+ return { root, tgz: tar.stdout, files: files.length };
48
+ }
49
+ const devRoot = () => (process.env.WISP_DEV_ROOT ? readFileSync(process.env.WISP_DEV_ROOT, "utf8") : undefined);
50
+
51
+ export async function spawnAgent(o) {
52
+ if (!getToken()) throw new LoginRequired();
53
+ const engine = o.engine ?? "claude";
54
+ let timeout = o.timeout_s ?? 1800;
55
+ const cred = engineCredential(engine, o.auth ?? process.env.WISP_AUTH ?? "auto", timeout);
56
+ let note;
57
+ if (cred.left && cred.left - 300 < timeout) {
58
+ timeout = Math.max(60, Math.floor(cred.left - 300));
59
+ note = `timeout reduzido para ${timeout}s (validade do login local)`;
60
+ }
61
+ const ws = o.workspace ? packWorkspace(o.workspace) : null;
62
+ const priv = newClientKey();
63
+ const nonce = randomBytes(32);
64
+ const job = await call("POST", "/api/jobs", { engine, ram_gb: o.ram_gb ?? 2, timeout_s: timeout,
65
+ nonce: nonce.toString("base64"), label: o.label });
66
+ writeSecret(keyFile(job.id), JSON.stringify({ id: job.id, priv: exportKey(priv), nonce: nonce.toString("base64"), engine,
67
+ workspace: ws?.root }));
68
+ const payload = Buffer.from(JSON.stringify({ engine, model: o.model, mission: o.mission, turns: o.max_turns ?? 20,
69
+ timeout, auth: { kind: cred.kind, value: cred.value }, ...(ws ? { workspace_tgz: ws.tgz.toString("base64") } : {}) }));
70
+ const p = sealWhenReady(job.id, priv, nonce, payload).finally(() => { payload.fill(0); sealers.delete(job.id); });
71
+ sealers.set(job.id, p);
72
+ p.catch(() => {});
73
+ return { id: job.id, status: job.status, engine, ram_gb: job.ram_gb, instance_type: job.instance_type,
74
+ credential: cred.source, reserved_usd: +(job.hold_cents / 100).toFixed(4),
75
+ ...(ws ? { workspace: `${ws.root} (${ws.files} arquivos, ${(ws.tgz.length / 1024).toFixed(0)} KB, cifrado)` } : {}),
76
+ ...(note ? { note } : {}) };
77
+ }
78
+
79
+ async function sealWhenReady(id, priv, nonce, payload) {
80
+ const until = Date.now() + 20 * 60_000;
81
+ while (Date.now() < until) {
82
+ const j = await call("GET", `/api/jobs/${id}`).catch(() => null);
83
+ if (j && FINAL.includes(j.status)) return;
84
+ if (j?.attestation) {
85
+ let att;
86
+ try {
87
+ att = verifyAttestation(j.attestation, nonce, { devRootPem: devRoot() });
88
+ } catch (e) {
89
+ sealErrors.set(id, e.message);
90
+ await call("DELETE", `/api/jobs/${id}`).catch(() => {});
91
+ throw e;
92
+ }
93
+ const f = keyFile(id);
94
+ const k = JSON.parse(readFileSync(f, "utf8"));
95
+ writeSecret(f, JSON.stringify({ ...k, enclave_pub: att.enclavePub.toString("base64"), pcr0: att.pcr0, dev: att.dev }));
96
+ await call("POST", `/api/jobs/${id}/input`, { sealed: sealInput(priv, att.enclavePub, nonce, payload) });
97
+ return;
98
+ }
99
+ await new Promise((r) => setTimeout(r, 2000));
100
+ }
101
+ throw new Error("a máquina não ficou pronta em 20 min");
102
+ }
103
+
104
+ /** Espera a selagem terminar (útil para o CLI, que sai logo depois). */
105
+ export async function sealed(id) {
106
+ await sealers.get(id);
107
+ }
108
+
109
+ function parseOutput(stdout) {
110
+ const lines = stdout.trim().split("\n").filter(Boolean);
111
+ try {
112
+ const last = JSON.parse(lines.at(-1));
113
+ const isCodex = last?.type === "turn.completed" || lines.slice(0, 3).some((l) => l.includes('"thread.started"'));
114
+ if (!isCodex) return { engine: "claude", ...last };
115
+ const res = { engine: "codex", result: "" };
116
+ for (const l of lines) {
117
+ let ev; try { ev = JSON.parse(l); } catch { continue; }
118
+ const item = ev.item ?? {};
119
+ if (ev.type === "item.completed" && item.type === "agent_message") res.result = item.text ?? "";
120
+ else if (ev.type === "turn.completed") res.usage = ev.usage;
121
+ else if (ev.type === "turn.failed" || ev.type === "error") { res.is_error = true; res.erro = ev.error?.message ?? ev.message; }
122
+ }
123
+ return res;
124
+ } catch {
125
+ return { raw: stdout.trim().slice(-2000) };
126
+ }
127
+ }
128
+
129
+ /** Resultado final (e apaga a chave local e a saída cifrada no servidor), ou o status se ainda roda. */
130
+ export async function result(id) {
131
+ const j = await call("GET", `/api/jobs/${id}`);
132
+ const meta = { id, status: j.status, ram_gb: j.ram_gb, peak_mem_mib: j.peak_mem_mib, cost_usd: j.cost_cents != null ? +(j.cost_cents / 100).toFixed(4) : null };
133
+ if (sealErrors.has(id)) return { ...meta, status: "failed", erro: `recusado por segurança: ${sealErrors.get(id)}` };
134
+ if (!FINAL.includes(j.status)) return { ...meta, mem_used_mib: j.mem_used_mib };
135
+ if (j.status !== "done" || !j.output) {
136
+ rmSync(keyFile(id), { force: true });
137
+ return { ...meta, erro: j.error ?? (j.collected_at ? "resultado já recolhido antes" : j.status) };
138
+ }
139
+ const f = keyFile(id);
140
+ if (!existsSync(f)) return { ...meta, erro: "a chave para abrir esse resultado não está nesta máquina" };
141
+ const k = JSON.parse(readFileSync(f, "utf8"));
142
+ const out = JSON.parse(openOutput(importKey(k.priv), Buffer.from(k.enclave_pub, "base64"), Buffer.from(k.nonce, "base64"), j.output).toString());
143
+ await call("POST", `/api/jobs/${id}/collected`).catch(() => {});
144
+ rmSync(f, { force: true });
145
+ const parsed = parseOutput(out.stdout ?? "");
146
+ const res = { ...parsed, ...meta, exit_code: out.exit_code, duration_s: out.duration_s };
147
+ if (out.exit_code === 124) res.erro = "timeout";
148
+ if (out.patch) {
149
+ const f = join(ensureDir("patches"), `${id}.patch`);
150
+ writeSecret(f, out.patch);
151
+ res.patch_file = f;
152
+ res.patch_stat = out.patch_stat;
153
+ res.aplicar = k.workspace ? `git -C ${JSON.stringify(k.workspace)} apply ${JSON.stringify(f)}` : `git apply ${JSON.stringify(f)}`;
154
+ } else if (out.patch === "") {
155
+ res.patch_stat = "nenhuma mudança no projeto";
156
+ } else if (out.patch_error) {
157
+ res.patch_erro = out.patch_error;
158
+ res.patch_stat = out.patch_stat;
159
+ }
160
+ if (out.exit_code !== 0 && out.stderr_tail) res.stderr_tail = out.stderr_tail.slice(-1500);
161
+ return res;
162
+ }
163
+
164
+ export async function waitAgent(id, maxWaitS = 900) {
165
+ const until = Date.now() + maxWaitS * 1000;
166
+ for (;;) {
167
+ const r = await result(id);
168
+ if (FINAL.includes(r.status) || r.erro || Date.now() > until) return r;
169
+ await new Promise((res) => setTimeout(res, 3000));
170
+ }
171
+ }
172
+
173
+ export async function killAgent(id) {
174
+ const j = await call("DELETE", `/api/jobs/${id}`);
175
+ rmSync(keyFile(id), { force: true });
176
+ return { id, status: j.status, cost_usd: j.cost_cents != null ? +(j.cost_cents / 100).toFixed(4) : null };
177
+ }
178
+
179
+ export async function listAgents() {
180
+ const [me, jobs] = await Promise.all([call("GET", "/api/me"), call("GET", "/api/jobs?limit=20")]);
181
+ return {
182
+ conta: me.email, saldo_usd: +(me.credit_cents / 100).toFixed(2),
183
+ ram_disponivel_gb: me.tiers.filter((t) => t.available).map((t) => t.ram_gb),
184
+ agentes: jobs.filter((j) => !FINAL.includes(j.status) || (j.status === "done" && !j.collected_at)).map((j) => ({
185
+ id: j.id, status: j.status, engine: j.engine, ram_gb: j.ram_gb, mem_used_mib: j.mem_used_mib, label: j.label })),
186
+ };
187
+ }
package/src/creds.js ADDED
@@ -0,0 +1,65 @@
1
+ // Credencial do MOTOR (Claude/Codex), lida só nesta máquina. Sai daqui apenas cifrada para a enclave atestada.
2
+ // Só o access token vai: nunca o refresh token, então a sessão local nunca cai nem é rotacionada lá.
3
+ import { execFileSync, spawnSync } from "node:child_process";
4
+ import { existsSync, readFileSync } from "node:fs";
5
+ import { homedir } from "node:os";
6
+ import { join } from "node:path";
7
+
8
+ const MIN_LEFT_S = 600;
9
+
10
+ function claudeLogin() {
11
+ let raw = "";
12
+ if (process.platform === "darwin") {
13
+ try { raw = execFileSync("security", ["find-generic-password", "-s", "Claude Code-credentials", "-w"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }); } catch {}
14
+ }
15
+ if (!raw.trim()) {
16
+ const f = join(process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), ".claude"), ".credentials.json");
17
+ if (existsSync(f)) raw = readFileSync(f, "utf8");
18
+ }
19
+ try {
20
+ const o = JSON.parse(raw).claudeAiOauth;
21
+ return { token: o.accessToken, exp: o.expiresAt / 1000 };
22
+ } catch { return null; }
23
+ }
24
+
25
+ function claudeCredential(mode, needS) {
26
+ const key = process.env.WISP_ANTHROPIC_API_KEY || process.env.ANTHROPIC_API_KEY;
27
+ if (mode !== "login" && key) return { kind: "anthropic_key", value: key, left: null, source: "chave de API" };
28
+ if (mode === "key") throw new Error("modo chave: defina ANTHROPIC_API_KEY (ou WISP_ANTHROPIC_API_KEY) no env do MCP");
29
+ if (process.env.CLAUDE_CODE_OAUTH_TOKEN) return { kind: "claude_oauth", value: process.env.CLAUDE_CODE_OAUTH_TOKEN, left: null, source: "CLAUDE_CODE_OAUTH_TOKEN" };
30
+ let l = claudeLogin();
31
+ if (l && l.exp - Date.now() / 1000 < Math.max(MIN_LEFT_S, needS)) {
32
+ // quase vencendo: uma chamada mínima faz o Claude Code local renovar o próprio login
33
+ spawnSync("claude", ["-p", "ok", "--max-turns", "1"], { stdio: "ignore", timeout: 120_000 });
34
+ l = claudeLogin();
35
+ }
36
+ if (!l) throw new Error("sem login do Claude Code nesta máquina: rode `claude` e faça login, ou use uma chave (ANTHROPIC_API_KEY)");
37
+ const left = l.exp - Date.now() / 1000;
38
+ if (left < MIN_LEFT_S) throw new Error("o login local do Claude não renovou; abra o Claude Code uma vez e tente de novo");
39
+ return { kind: "claude_oauth", value: l.token, left, source: "login do Claude Code" };
40
+ }
41
+
42
+ const jwtExp = (t) => JSON.parse(Buffer.from(t.split(".")[1], "base64url").toString()).exp;
43
+
44
+ function codexCredential(mode) {
45
+ const key = process.env.WISP_OPENAI_API_KEY || process.env.OPENAI_API_KEY;
46
+ if (mode !== "login" && key) return { kind: "openai_key", value: key, left: null, source: "chave de API" };
47
+ const path = join(process.env.CODEX_HOME ?? join(homedir(), ".codex"), "auth.json");
48
+ let d;
49
+ try { d = JSON.parse(readFileSync(path, "utf8")); } catch {
50
+ throw new Error(mode === "key" ? "modo chave: defina OPENAI_API_KEY no env do MCP" : "sem login do Codex nesta máquina: rode `codex login`");
51
+ }
52
+ if (mode !== "login" && d.OPENAI_API_KEY) return { kind: "openai_key", value: d.OPENAI_API_KEY, left: null, source: "chave do Codex" };
53
+ if (mode === "key") throw new Error("modo chave: defina OPENAI_API_KEY no env do MCP");
54
+ const t = d.tokens ?? {};
55
+ const left = jwtExp(t.access_token) - Date.now() / 1000;
56
+ if (left < MIN_LEFT_S) throw new Error("o login do Codex venceu; abra o Codex uma vez nesta máquina");
57
+ const slim = { auth_mode: d.auth_mode ?? "chatgpt", OPENAI_API_KEY: null, last_refresh: new Date().toISOString(),
58
+ tokens: { id_token: t.id_token, access_token: t.access_token, account_id: t.account_id, refresh_token: "" } };
59
+ return { kind: "codex_auth", value: JSON.stringify(slim), left, source: "login do ChatGPT/Codex" };
60
+ }
61
+
62
+ /** mode: "auto" (chave se houver, senão login), "key" ou "login". */
63
+ export function engineCredential(engine, mode = process.env.WISP_AUTH ?? "auto", needS = 0) {
64
+ return engine === "codex" ? codexCredential(mode) : claudeCredential(mode, needS);
65
+ }
package/src/crypto.js ADDED
@@ -0,0 +1,41 @@
1
+ // Canal selado wisp-v1 (mesma especificação de enclave/sealed.py).
2
+ import { createCipheriv, createDecipheriv, createPrivateKey, createPublicKey, diffieHellman, generateKeyPairSync,
3
+ hkdfSync, randomBytes } from "node:crypto";
4
+
5
+ const V = Buffer.from("wisp-v1");
6
+ const rawPub = (pub) => Buffer.from(pub.export({ format: "jwk" }).x, "base64url");
7
+ const pubFromRaw = (raw) => createPublicKey({ key: { kty: "OKP", crv: "X25519", x: Buffer.from(raw).toString("base64url") }, format: "jwk" });
8
+
9
+ export function newClientKey() {
10
+ const { privateKey } = generateKeyPairSync("x25519");
11
+ return privateKey;
12
+ }
13
+
14
+ export const exportKey = (priv) => priv.export({ format: "der", type: "pkcs8" }).toString("base64");
15
+ export const importKey = (b64) => createPrivateKey({ key: Buffer.from(b64, "base64"), format: "der", type: "pkcs8" });
16
+
17
+ function key(priv, enclavePub, nonce, direction) {
18
+ const clientPub = rawPub(createPublicKey(priv));
19
+ const shared = diffieHellman({ privateKey: priv, publicKey: pubFromRaw(enclavePub) });
20
+ const info = Buffer.concat([V, Buffer.from(" " + direction), enclavePub, clientPub]);
21
+ return { k: Buffer.from(hkdfSync("sha256", shared, nonce, info, 32)), clientPub };
22
+ }
23
+
24
+ export function sealInput(priv, enclavePub, nonce, plaintext) {
25
+ const { k, clientPub } = key(priv, enclavePub, nonce, "in");
26
+ const iv = randomBytes(12);
27
+ const c = createCipheriv("chacha20-poly1305", k, iv, { authTagLength: 16 });
28
+ c.setAAD(Buffer.concat([V, Buffer.from(" in")]), { plaintextLength: plaintext.length });
29
+ const ct = Buffer.concat([c.update(plaintext), c.final(), c.getAuthTag()]);
30
+ k.fill(0);
31
+ return { c_pub: clientPub.toString("base64"), iv: iv.toString("base64"), ct: ct.toString("base64") };
32
+ }
33
+
34
+ export function openOutput(priv, enclavePub, nonce, msg) {
35
+ const { k } = key(priv, enclavePub, nonce, "out");
36
+ const data = Buffer.from(msg.ct, "base64");
37
+ const d = createDecipheriv("chacha20-poly1305", k, Buffer.from(msg.iv, "base64"), { authTagLength: 16 });
38
+ d.setAAD(Buffer.concat([V, Buffer.from(" out")]), { plaintextLength: data.length - 16 });
39
+ d.setAuthTag(data.subarray(data.length - 16));
40
+ return Buffer.concat([d.update(data.subarray(0, data.length - 16)), d.final()]);
41
+ }
package/src/mcp.js ADDED
@@ -0,0 +1,123 @@
1
+ // Servidor MCP (stdio, JSON-RPC por linha). Sem dependências.
2
+ import { createInterface } from "node:readline";
3
+ import { API, getToken, startLogin } from "./account.js";
4
+ import { LoginRequired, killAgent, listAgents, result, spawnAgent, waitAgent } from "./client.js";
5
+
6
+ const PROTOCOLS = ["2025-06-18", "2025-03-26", "2024-11-05"];
7
+ const VERSION = "0.1.1";
8
+
9
+ const INSTRUCTIONS = `wisp roda subagentes Claude Code ou Codex em máquinas efêmeras na nuvem, com a RAM que você pedir,
10
+ sem pesar esta máquina. Cada subagente nasce numa enclave isolada (AWS Nitro): antes de mandar qualquer coisa,
11
+ este MCP confere criptograficamente a imagem da enclave e cifra a missão e o login do usuário só para ela.
12
+ Nem o operador do wisp consegue ler a missão, a credencial ou o resultado. A máquina é destruída ao terminar.
13
+
14
+ Quando usar: o usuário pede subagente remoto / "roda no wisp" / "sobe N subagentes", ou há tarefas
15
+ independentes e pesadas (build, testes, pesquisa longa) que podem rodar em paralelo fora daqui.
16
+
17
+ Como usar bem:
18
+ - Paralelo: chame spawn_agent para todas as missões primeiro, depois wait_agent para cada id.
19
+ - Para trabalhar no código do usuário, passe workspace (ex.: o diretório do projeto): vai uma cópia cifrada,
20
+ o subagente trabalha nela e as mudanças voltam como patch (aplique com o comando em "aplicar" depois de revisar).
21
+ Sem workspace a máquina começa vazia: ponha todo o contexto na missão. Tem internet (HTTPS), mas não tem git/SSH do usuário.
22
+ - A máquina leva ~2-3 min para subir; wait_agent espera até 15 min por chamada (chame de novo se voltar running).
23
+ - Sempre recolha com wait_agent ou agent_result: a resposta só pode ser aberta nesta máquina.
24
+ - Cada subagente consome crédito do wisp (máquina) e a assinatura/chave do usuário (modelo). Não dispare dezenas.
25
+ - Se a resposta disser que é preciso entrar na conta, mostre o link ao usuário.`;
26
+
27
+ const TOOLS = [
28
+ { name: "spawn_agent", description: "Sobe um subagente efêmero numa máquina com a RAM pedida e devolve o id na hora.",
29
+ inputSchema: { type: "object", required: ["mission"], properties: {
30
+ mission: { type: "string", description: "Missão completa e autocontida, com todo o contexto necessário." },
31
+ engine: { type: "string", enum: ["claude", "codex"], default: "claude" },
32
+ model: { type: "string", description: "Modelo do motor. Omitir = padrão dele." },
33
+ ram_gb: { type: "integer", enum: [2, 4, 8, 16, 24], default: 2, description: "RAM do subagente." },
34
+ max_turns: { type: "integer", default: 20, description: "Só vale para claude." },
35
+ timeout_s: { type: "integer", default: 1800, minimum: 60, maximum: 7200 },
36
+ auth: { type: "string", enum: ["auto", "login", "key"], default: "auto",
37
+ description: "login = assinatura do usuário nesta máquina; key = chave de API do env; auto = chave se houver." },
38
+ workspace: { type: "string", description: "Caminho de um diretório/repositório DESTA máquina para mandar junto. Vai uma cópia cifrada (no git: arquivos rastreados + novos não ignorados; nunca o que está no .gitignore). O subagente trabalha em ~/work e as mudanças voltam como patch (patch_file + comando aplicar). Máx. 15 MB compactado." },
39
+ label: { type: "string", description: "Rótulo curto VISÍVEL no painel (não coloque nada sensível)." } } } },
40
+ { name: "wait_agent", description: "Espera o subagente terminar e devolve o resultado (campo result = resposta).",
41
+ inputSchema: { type: "object", required: ["id"], properties: {
42
+ id: { type: "string" }, max_wait_s: { type: "integer", default: 900, maximum: 1800 } } } },
43
+ { name: "agent_result", description: "Resultado sem esperar: devolve a resposta ou o status atual (running, RAM em uso).",
44
+ inputSchema: { type: "object", required: ["id"], properties: { id: { type: "string" } } } },
45
+ { name: "kill_agent", description: "Mata o subagente e destrói a máquina na hora.",
46
+ inputSchema: { type: "object", required: ["id"], properties: { id: { type: "string" } } } },
47
+ { name: "list_agents", description: "Saldo, RAM disponível e subagentes vivos ou com resultado para recolher.",
48
+ inputSchema: { type: "object", properties: {} } },
49
+ { name: "wisp_login", description: "Conecta este MCP à conta wisp do usuário (abre o navegador).",
50
+ inputSchema: { type: "object", properties: {} } },
51
+ ];
52
+
53
+ async function loginMessage() {
54
+ const l = await startLogin();
55
+ // com navegador aberto aqui, espera um pouco pela aprovação e segue sozinho
56
+ if (l.opened) {
57
+ const ok = await Promise.race([l.done.then(() => true).catch(() => false), new Promise((r) => setTimeout(() => r(false), 120_000))]);
58
+ if (ok) return null;
59
+ }
60
+ return `Para usar o wisp, conecte sua conta: abra ${l.verification_uri_complete} e confirme o código ${l.user_code}.\n` +
61
+ `Não tem conta? Crie lá mesmo, com crédito grátis. Depois repita o pedido.`;
62
+ }
63
+
64
+ async function runTool(name, a) {
65
+ if (name === "wisp_login") {
66
+ if (getToken()) return { ok: true, msg: `já conectado a ${API}` };
67
+ const msg = await loginMessage();
68
+ return msg ? { login_necessario: msg } : { ok: true, msg: "conta conectada" };
69
+ }
70
+ const attempt = () => {
71
+ switch (name) {
72
+ case "spawn_agent": return spawnAgent(a);
73
+ case "wait_agent": return waitAgent(a.id, a.max_wait_s ?? 900);
74
+ case "agent_result": return result(a.id);
75
+ case "kill_agent": return killAgent(a.id);
76
+ case "list_agents": return listAgents();
77
+ default: throw new Error(`ferramenta desconhecida: ${name}`);
78
+ }
79
+ };
80
+ try {
81
+ if (!getToken()) throw new LoginRequired();
82
+ return await attempt();
83
+ } catch (e) {
84
+ if (!(e instanceof LoginRequired) && e.status !== 401) throw e;
85
+ const msg = await loginMessage();
86
+ if (msg) return { login_necessario: msg };
87
+ return await attempt();
88
+ }
89
+ }
90
+
91
+ export function serve() {
92
+ const send = (m) => process.stdout.write(JSON.stringify(m) + "\n");
93
+ const rl = createInterface({ input: process.stdin });
94
+ rl.on("line", async (line) => {
95
+ let msg;
96
+ try { msg = JSON.parse(line); } catch { return; }
97
+ const { id, method, params } = msg;
98
+ if (id === undefined) return; // notificações
99
+ try {
100
+ let res;
101
+ if (method === "initialize") {
102
+ const v = PROTOCOLS.includes(params?.protocolVersion) ? params.protocolVersion : PROTOCOLS[0];
103
+ res = { protocolVersion: v, capabilities: { tools: {} }, serverInfo: { name: "ramwisp", version: VERSION }, instructions: INSTRUCTIONS };
104
+ } else if (method === "tools/list") {
105
+ res = { tools: TOOLS };
106
+ } else if (method === "ping") {
107
+ res = {};
108
+ } else if (method === "tools/call") {
109
+ try {
110
+ const out = await runTool(params.name, params.arguments ?? {});
111
+ res = { content: [{ type: "text", text: typeof out === "string" ? out : JSON.stringify(out, null, 1) }] };
112
+ } catch (e) {
113
+ res = { content: [{ type: "text", text: `erro: ${e.message}` }], isError: true };
114
+ }
115
+ } else {
116
+ return send({ jsonrpc: "2.0", id, error: { code: -32601, message: `método desconhecido: ${method}` } });
117
+ }
118
+ send({ jsonrpc: "2.0", id, result: res });
119
+ } catch (e) {
120
+ send({ jsonrpc: "2.0", id, error: { code: -32603, message: e.message } });
121
+ }
122
+ });
123
+ }