Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 19 additions & 11 deletions darknode.js
Original file line number Diff line number Diff line change
Expand Up @@ -803,10 +803,11 @@ function serveDir(port, dir) {
if (_rp !== _root && !_rp.startsWith(_root + path.sep)) { res.writeHead(403); return res.end("forbidden"); }
fs.stat(p, (e, st) => {
if (e) { res.writeHead(404); return res.end("not found"); }
if (st.isDirectory()) { const items = fs.readdirSync(p); res.writeHead(200, { "Content-Type": "text/html" }); const h = (x) => String(x).replace(/[&<>"']/g, (c) => "&#" + c.charCodeAt(0) + ";"), base = rel.replace(/\/$/, "").split("/").map(encodeURIComponent).join("/");
if (st.isDirectory()) { let items; try { items = fs.readdirSync(p); } catch (_) { res.writeHead(403); return res.end("forbidden"); } res.writeHead(200, { "Content-Type": "text/html" }); const h = (x) => String(x).replace(/[&<>"']/g, (c) => "&#" + c.charCodeAt(0) + ";"), base = rel.replace(/\/$/, "").split("/").map(encodeURIComponent).join("/");
return res.end(items.map((i) => `<a href="${h(base + "/" + encodeURIComponent(i))}">${h(i)}</a>`).join("<br>")); } // names are text, not markup, and #/?/space in a name stay part of the link
console.log(" " + green("GET ") + req.url + gray(" " + (req.socket.remoteAddress || "")));
res.writeHead(200); fs.createReadStream(p).pipe(res);
const rs = fs.createReadStream(p); // an unreadable file errored with no handler and took the server down
rs.on("open", () => { res.writeHead(200); rs.pipe(res); }); rs.on("error", () => { if (!res.headersSent) res.writeHead(403); res.end(); });
});
});
srv.listen(port, () => { h1("HTTP file server"); console.log(" serving " + bold(path.resolve(dir)) + " on " + cyan("http://0.0.0.0:" + port)); console.log(" " + gray("Ctrl-C to stop") + "\n"); });
Expand Down Expand Up @@ -836,7 +837,9 @@ function nexusUpdate(opts) {
const hush = (msg) => (silent ? "" : msg);
const cp = require("child_process"), fs = require("fs"), path = require("path");
const dir = __dirname;
const run = (c, a) => cp.spawnSync(c, a, { cwd: dir, encoding: "utf8", timeout: 120000 });
// Windows npm is npm.cmd, which spawn can only run through a shell (args here are fixed or semver-checked).
const W = process.platform === "win32", sp = (c, a, o) => cp.spawnSync(W && c === "npm" ? "npm.cmd" : c, a, W && c === "npm" ? { ...o, shell: true } : o);
const run = (c, a) => sp(c, a, { cwd: dir, encoding: "utf8", timeout: 120000 });
if (!fs.existsSync(path.join(dir, ".git"))) {
// installed from npm (global) — compare against the registry and, on apply, reinstall.
if (!hasBin("npm")) return hush("installed at " + dir + "\nnot a git checkout and npm not found — reinstall to update.");
Expand All @@ -846,7 +849,7 @@ function nexusUpdate(opts) {
if (!semverGt(latest, VERSION)) return hush("up to date — v" + VERSION + " is the latest (npm).");
const L = ["update available: v" + VERSION + " → v" + latest + " (npm)"];
if (!apply) { L.push("run " + cyan("darknode update") + " to install (npm i -g darknode-cli@latest)."); return L.join("\n"); }
const ni = cp.spawnSync("npm", ["install", "-g", "darknode-cli@" + latest, "--no-audit", "--no-fund"], { encoding: "utf8", timeout: 600000 });
const ni = sp("npm", ["install", "-g", "darknode-cli@" + latest, "--no-audit", "--no-fund"], { encoding: "utf8", timeout: 600000 });
if (ni.status !== 0) { L.push("auto-update failed (a global npm install may need elevated permissions): " + (((ni.stderr || "") + (ni.error ? ni.error.message : "")).trim().split("\n").slice(-2).join(" ") || "npm error") + "\n install manually: npm install -g darknode-cli@latest"); return L.join("\n"); }
L.push(green("updated") + " to v" + latest + " — restart Nexus to load it."); return L.join("\n");
}
Expand All @@ -872,7 +875,7 @@ function nexusUpdate(opts) {
const pull = run("git", ["merge", "--ff-only", "--quiet", remote]); // the commit just compared: `git pull` re-resolved @{u}, which is missing when we fell back to origin/main
if (pull.status !== 0) { L.push("update failed: " + (((pull.stderr || "") + (pull.error ? pull.error.message : "")).trim() || "git pull error")); return L.join("\n"); }
const changed = (run("git", ["diff", "--name-only", local, "HEAD"]).stdout) || "";
if (/package(-lock)?\.json/.test(changed) && hasBin("npm")) { const ni = cp.spawnSync("npm", ["install", "--silent", "--no-audit", "--no-fund", "--no-package-lock"], { cwd: dir, encoding: "utf8", timeout: 600000 }); if (ni.status !== 0) L.push("note: npm install after update reported an issue — run `npm install` in " + dir); }
if (/package(-lock)?\.json/.test(changed) && hasBin("npm")) { const ni = sp("npm", ["install", "--silent", "--no-audit", "--no-fund", "--no-package-lock"], { cwd: dir, encoding: "utf8", timeout: 600000 }); if (ni.status !== 0) L.push("note: npm install after update reported an issue — run `npm install` in " + dir); }
let newV = VERSION; try { newV = JSON.parse(stripBom(fs.readFileSync(path.join(dir, "package.json"), "utf8"))).version || newV; } catch (_) {}
L.push(green("updated") + " v" + VERSION + " → v" + newV + " — restart Nexus (" + cyan("/exit") + ", then " + cyan("darknode nexus") + ") to load it.");
return L.join("\n");
Expand Down Expand Up @@ -1302,25 +1305,30 @@ async function githubDeviceFlow(clientId, log) {
throw new Error("GitHub device login timed out");
}
async function googleLoopbackFlow(clientId, clientSecret, log) {
const http = require("http");
const http = require("http"), crypto = require("crypto");
// state + PKCE: without them any local process or web page could hand this listener its
// own auth code and sign the user in as someone else (login CSRF).
const state = crypto.randomBytes(16).toString("hex"), verifier = crypto.randomBytes(32).toString("base64url");
const challenge = crypto.createHash("sha256").update(verifier).digest("base64url");
return await new Promise((resolve, reject) => {
let done = false;
const server = http.createServer(async (req, res) => {
try {
const u = new URL(req.url, "http://127.0.0.1"); const code = u.searchParams.get("code"), err = u.searchParams.get("error");
if (err) { res.end("Login failed: " + err); done = true; server.close(); return reject(new Error("Google: " + err)); }
if ((code || err) && u.searchParams.get("state") !== state) { res.writeHead(400, { "Content-Type": "text/plain" }); return res.end("Login request did not come from this sign-in."); }
if (err) { res.writeHead(200, { "Content-Type": "text/plain" }); res.end("Login failed: " + err); done = true; server.close(); return reject(new Error("Google: " + err)); }
if (!code) { res.end("Waiting…"); return; }
res.writeHead(200, { "Content-Type": "text/html" });
res.end("<!doctype html><meta charset=utf-8><body style='font-family:-apple-system,sans-serif;background:#0b1220;color:#eaf0fb;display:flex;height:100vh;margin:0;align-items:center;justify-content:center'><div style='text-align:center'><h2 style='margin:0 0 8px'>Nexus — signed in</h2><p style='color:#8ca0c4'>You can close this tab and return to the terminal.</p></div>");
const port = server.address().port; done = true; server.close();
const tk = await httpReq({ url: "https://oauth2.googleapis.com/token", method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: "code=" + encodeURIComponent(code) + "&client_id=" + encodeURIComponent(clientId) + "&client_secret=" + encodeURIComponent(clientSecret) + "&redirect_uri=" + encodeURIComponent("http://127.0.0.1:" + port) + "&grant_type=authorization_code" });
const tk = await httpReq({ url: "https://oauth2.googleapis.com/token", method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: "code=" + encodeURIComponent(code) + "&client_id=" + encodeURIComponent(clientId) + "&client_secret=" + encodeURIComponent(clientSecret) + "&redirect_uri=" + encodeURIComponent("http://127.0.0.1:" + port) + "&grant_type=authorization_code&code_verifier=" + encodeURIComponent(verifier) });
if (!tk.json || !tk.json.id_token) return reject(new Error("Google token exchange failed" + (tk.json && tk.json.error ? ": " + (tk.json.error_description || tk.json.error) : "")));
resolve(tk.json.id_token);
} catch (e) { reject(e); }
});
server.listen(0, "127.0.0.1", () => {
const port = server.address().port, redirect = "http://127.0.0.1:" + port;
const url = "https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=" + encodeURIComponent(clientId) + "&redirect_uri=" + encodeURIComponent(redirect) + "&scope=" + encodeURIComponent("openid email profile") + "&access_type=offline&prompt=select_account";
const url = "https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=" + encodeURIComponent(clientId) + "&redirect_uri=" + encodeURIComponent(redirect) + "&scope=" + encodeURIComponent("openid email profile") + "&access_type=offline&prompt=select_account&state=" + state + "&code_challenge=" + challenge + "&code_challenge_method=S256";
log("opening browser to sign in with Google…"); openBrowser(url);
});
setTimeout(() => { if (done) return; try { server.close(); } catch (_) {} reject(new Error("Google login timed out")); }, 180000);
Expand Down Expand Up @@ -1408,7 +1416,7 @@ function loginConfigWrite(kv) { const cur = firebaseCfg() || {}; let n = 0; for
function ollamaReachable() { return new Promise((res) => { const r = require("http").get({ host: ollamaAddr().host, port: ollamaAddr().port, path: "/api/tags", timeout: 1500 }, () => res(true)); r.on("error", () => res(false)); r.on("timeout", () => { r.destroy(); res(false); }); }); }
function streamCmd(cmdOrBin, args, log, shell) { return new Promise((resolve) => { const cp = require("child_process"); let p; try { p = shell ? cp.spawn("sh", ["-c", cmdOrBin], { stdio: ["ignore", "pipe", "pipe"] }) : cp.spawn(cmdOrBin, args, { stdio: ["ignore", "pipe", "pipe"] }); } catch (e) { log(red(e.message)); return resolve(1); } const onData = (b) => String(b).split(/\r?\n/).forEach((l) => { if (l.trim()) log(gray(l.slice(0, 200))); }); p.stdout.on("data", onData); p.stderr.on("data", onData); p.on("close", (code) => resolve(code || 0)); p.on("error", (e) => { log(red(e.message)); resolve(1); }); }); }
async function installOllama(log) { if (process.platform === "win32") { log("Windows: download the installer from " + cyan("https://ollama.com/download") + ", then re-run setup."); return false; } log("installing Ollama (official script)…"); return (await streamCmd("curl -fsSL https://ollama.com/install.sh | sh", null, log, true)) === 0; }
async function ensureOllamaServer(log) { if (await ollamaReachable()) return true; log("starting the Ollama server…"); try { require("child_process").spawn("ollama", ["serve"], { stdio: "ignore", detached: true }).unref(); } catch (_) {} for (let i = 0; i < 20; i++) { await new Promise((r) => setTimeout(r, 500)); if (await ollamaReachable()) return true; } return false; }
async function ensureOllamaServer(log) { if (await ollamaReachable()) return true; log("starting the Ollama server…"); let missing = false; try { const ch = require("child_process").spawn("ollama", ["serve"], { stdio: "ignore", detached: true }); ch.on("error", () => { missing = true; }); ch.unref(); } catch (_) { missing = true; } /* ollama not installed: spawn emits 'error' (ENOENT), which crashed Nexus when unhandled */ for (let i = 0; i < 20 && !missing; i++) { await new Promise((r) => setTimeout(r, 500)); if (await ollamaReachable()) return true; } return false; }
async function ollamaPull(model, log) { log("pulling " + bold(model) + gray(" — several GB, this can take a while…")); return (await streamCmd("ollama", ["pull", model], log, false)) === 0; }
function firstRunPending() { const st = readGlobal("state.json", null); return !st || !st.setupDone; }
// The local models Nexus ships with — a strong coding model + an agentic/security
Expand Down Expand Up @@ -2342,7 +2350,7 @@ async function deviceTool(name, a, cwd) {
if (name === "find" || name === "find_files" || name === "glob") return require("./lib/nexus/search").findTool(a, cwd, rp);
if (name === "http_fetch" || name === "web_fetch" || name === "fetch_url" || name === "http") return await require("./lib/nexus/http-fetch").httpFetchTool(a, { isLocal: _ssrfLocal }); // redirects re-checked by the SSRF guard on every hop
if (name === "sysinfo" || name === "system_info") {
let disk = ""; try { disk = _cp.execSync("df -h " + JSON.stringify(cwd) + " 2>/dev/null", { encoding: "utf8" }).trim().split("\n").slice(-1)[0]; } catch (_) {}
let disk = ""; try { disk = _cp.execFileSync("df", ["-h", cwd], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim().split("\n").slice(-1)[0]; } catch (_) {}
const c = os.cpus()[0] || {};
return { platform: os.platform(), arch: os.arch(), release: os.release(), hostname: os.hostname(), cpus: os.cpus().length + " x " + (c.model || "?"), memory: (os.totalmem() / 1e9).toFixed(1) + "GB total, " + (os.freemem() / 1e9).toFixed(1) + "GB free", loadavg: os.loadavg().map((n) => n.toFixed(2)).join(" "), uptime: (os.uptime() / 3600).toFixed(1) + "h", cwd, node: process.version, disk };
}
Expand Down
2 changes: 1 addition & 1 deletion lib/nexus/darknode-mcp.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ function importSpecifiers(src) {
function resolveSpec(spec, from) {
const s = String(spec).split(/[?#]/)[0];
if (/^[a-z][a-z0-9+.-]*:/i.test(s)) return null; // http:, data:, node: ...
if (s.startsWith("/js/")) return path.posix.normalize(s.slice(4));
if (s.startsWith("/js/")) { const r = path.posix.normalize(s.slice(4)); return r.startsWith("../") || r === ".." ? null : r; } // never write outside the engines cache
if (s.startsWith("./") || s.startsWith("../")) { const r = path.posix.normalize(path.posix.join(path.posix.dirname(from), s)); return r.startsWith("../") ? null : r; }
return null; // bare package name: not ours
}
Expand Down
16 changes: 15 additions & 1 deletion test/piped-stdin-audit.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,24 @@ const ok = (name, cond) => { if (cond) pass++; else { fail++; console.log(" \x1
ok("the reader returns every line in order, then null at the end", JSON.stringify(got) === JSON.stringify(["a", "b", "c", null, null]));

const home = fs.mkdtempSync(path.join(os.tmpdir(), "nx-pipe-")), dir = fs.mkdtempSync(path.join(os.tmpdir(), "nx-pipe-cwd-"));
const res = cp.spawnSync(process.execPath, [path.join(__dirname, "..", "darknode.js"), "nexus", "--new"], { cwd: dir, input: "/auto\n/auto\n/clear\n", encoding: "utf8", timeout: 30000, env: { ...process.env, HOME: home, NO_COLOR: "1", DARKNODE_FAST: "1" } });
// A stand-in Ollama (model list only), so the session starts on machines with no engine
// installed (CI); no slash command here reaches the model.
const http = require("http");
const fake = http.createServer((req, res) => { res.writeHead(200, { "content-type": "application/json" }); res.end(req.url.startsWith("/api/tags") ? JSON.stringify({ models: [{ name: "qwen2.5-coder:7b", model: "qwen2.5-coder:7b", size: 1 }] }) : "{}"); });
await new Promise((r) => fake.listen(0, "127.0.0.1", r));
const res = await new Promise((resolve) => {
const ch = cp.spawn(process.execPath, [path.join(__dirname, "..", "darknode.js"), "nexus", "--new"], { cwd: dir, env: { ...process.env, HOME: home, NO_COLOR: "1", DARKNODE_FAST: "1", OLLAMA_HOST: "127.0.0.1", OLLAMA_PORT: String(fake.address().port) } });
let stdout = "", stderr = "";
ch.stdout.on("data", (d) => { stdout += d; }); ch.stderr.on("data", (d) => { stderr += d; });
const t = setTimeout(() => ch.kill("SIGKILL"), 30000);
ch.on("close", (status, signal) => { clearTimeout(t); resolve({ status, signal, stdout, stderr }); });
ch.stdin.end("/auto\n/auto\n/clear\n");
});
fake.close();
const out = res.stdout || "";
ok("all three piped commands ran", (out.match(/auto-approve ON/g) || []).length === 1 && (out.match(/auto-approve OFF/g) || []).length === 1 && /conversation cleared/.test(out));
ok("end of input ends the session cleanly", res.status === 0 && /stay sharp/.test(out));
if (fail) console.log(" status=" + res.status + " signal=" + res.signal + " error=" + (res.error && res.error.message) + "\n stdout tail: " + JSON.stringify(out.slice(-600)) + "\n stderr tail: " + JSON.stringify(String(res.stderr || "").slice(-600)));
fs.rmSync(home, { recursive: true, force: true }); fs.rmSync(dir, { recursive: true, force: true });

console.log("\n" + (fail ? "\x1b[31m" : "\x1b[32m") + pass + " passed, " + fail + " failed\x1b[0m (piped stdin)");
Expand Down
2 changes: 1 addition & 1 deletion test/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -4137,7 +4137,7 @@ ok("shouldCheck: first launch runs; throttled within 4h; runs after", shouldChec
if (req.url === "/split") { res.writeHead(200, { "content-type": "text/plain" }); const b = Buffer.from("añb"); res.write(b.subarray(0, 2)); setTimeout(() => res.end(b.subarray(2)), 20); return; }
res.writeHead(404); res.end();
});
await new Promise((r) => srv.listen(0, "127.0.0.1", r));
await new Promise((r) => srv.listen(0, r));
const base = "http://127.0.0.1:" + srv.address().port;
const deps = { isLocal: (u) => /blocked\.internal/.test(u) }; // the real guard would also block 127.0.0.1; here only the redirect target is "private"
let r = await HF.httpFetchTool({ url: base + "/r1" }, deps);
Expand Down
Loading