From baa7cfc9ce83ddb70d31d7414a2bebb750be3bb1 Mon Sep 17 00:00:00 2001 From: Marketen Date: Thu, 30 Apr 2026 20:28:24 +0000 Subject: [PATCH 1/2] bump NousResearch/hermes-agent to v2026.4.30 --- dappnode_package.json | 4 ++-- docker-compose.yml | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dappnode_package.json b/dappnode_package.json index dbf6664..19504a5 100644 --- a/dappnode_package.json +++ b/dappnode_package.json @@ -1,7 +1,7 @@ { "name": "hermes-agent.dnp.dappnode.eth", - "version": "0.1.0", - "upstreamVersion": "v2026.4.23", + "version": "0.1.2", + "upstreamVersion": "v2026.4.30", "upstreamRepo": "NousResearch/hermes-agent", "upstreamArg": "UPSTREAM_VERSION", "shortDescription": "Self-improving AI agent with multi-LLM support and messaging gateway", diff --git a/docker-compose.yml b/docker-compose.yml index e7145c6..26efa8a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,7 +5,7 @@ services: context: . dockerfile: Dockerfile args: - UPSTREAM_VERSION: v2026.4.23 + UPSTREAM_VERSION: v2026.4.30 image: hermes-agent.dnp.dappnode.eth:0.1.0 container_name: DAppNodePackage-hermes-agent.dnp.dappnode.eth restart: unless-stopped @@ -15,8 +15,8 @@ services: NODE_ENV: production API_SERVER_ENABLED: "true" API_SERVER_PORT: "3000" - API_SERVER_HOST: "0.0.0.0" - API_SERVER_KEY: "dappnode" + API_SERVER_HOST: 0.0.0.0 + API_SERVER_KEY: dappnode API_SERVER_CORS_ORIGINS: "*" GATEWAY_ALLOW_ALL_USERS: "true" volumes: From e20dba43c34010e54aae960dccbb221f8a807236 Mon Sep 17 00:00:00 2001 From: Marketen Date: Tue, 8 Sep 2026 10:46:14 +0200 Subject: [PATCH 2/2] Add a Verified TEE mode toggle to the Nexus provider Nexus also runs a Gateway inside a trusted execution environment, reachable through the Nexus Local Proxy package. Hermes could already be pointed at it by hand-editing the base URL, which meant nobody did. The toggle switches OPENAI_BASE_URL between the standard Nexus API and the local proxy. That is the whole change: Hermes speaks plain OpenAI HTTP either way and never implements attestation itself. The proxy is the only thing on the node that verifies the enclave and encrypts prompt bodies to it, so pointing at it is all that is required, and there is no second implementation of security-critical code to keep in step. Enabling it probes the proxy rather than assuming it is there. The probe reads the proxy's own verification state instead of attesting anything here, and reports what was checked and which Gateway release is running, so the user can see the claim rather than take the toggle's word for it. A node without the package installed is told to install it; a proxy that is running but has not verified says so, because it will not carry prompts in that state. The probe also pulls the Gateway's model catalog through the proxy, so the model field offers real IDs instead of asking the user to copy one from a web page. The toggle is restored from the saved base URL, so reopening the wizard does not silently turn it back off. Co-Authored-By: Claude Opus 5 --- dappnode/dappnode/SKILL.md | 14 +++++++ setup-wizard/index.html | 77 ++++++++++++++++++++++++++++++++++++-- setup-wizard/server.cjs | 55 +++++++++++++++++++++++++++ 3 files changed, 143 insertions(+), 3 deletions(-) diff --git a/dappnode/dappnode/SKILL.md b/dappnode/dappnode/SKILL.md index d955e43..897b497 100644 --- a/dappnode/dappnode/SKILL.md +++ b/dappnode/dappnode/SKILL.md @@ -70,6 +70,20 @@ To configure, use the Setup Wizard at `http://hermes-agent.dappnode:8080` and se 1. Sign up at `https://nexus.dappnode.com` and create an API key 2. In the Setup Wizard or `config.yaml`, set the provider to Nexus with base URL `https://nexus-api.dappnode.com/v1` +#### Verified TEE mode (optional) +Nexus also runs a Gateway inside a trusted execution environment. In that mode prompts +go to the **Nexus Local Proxy** package on this DAppNode instead of directly to the Nexus +API. That proxy verifies the Gateway is running the exact software DAppNode published, +inside genuine sealed hardware, before any prompt leaves the machine, and encrypts prompt +and reply bodies to that enclave. If verification fails it refuses to carry traffic. + +Requires the **Nexus Local Proxy** package to be installed. Enable it with the "Verified +TEE mode" toggle in the Setup Wizard, which switches the base URL to +`http://nexus-local-proxy.dappnode.private:3301/v1`. The API key and model ID are +unchanged. Check status at `http://nexus-local-proxy.dappnode.private:3301/verification`. + +Hermes speaks plain OpenAI HTTP either way; nothing else about the configuration differs. + ## Troubleshooting ### Package Not Reachable diff --git a/setup-wizard/index.html b/setup-wizard/index.html index ba28dfa..40de303 100644 --- a/setup-wizard/index.html +++ b/setup-wizard/index.html @@ -398,6 +398,7 @@

Configuration Saved!

const raw = data.config || ""; const provMatch = raw.match(/provider:\s*["']?([^"'\n]+)/); const modelMatch = raw.match(/default:\s*["']?([^"'\n]+)/); + nexusTeeEnabled = (env.OPENAI_BASE_URL || "").startsWith("http://nexus-local-proxy.dappnode.private:3301"); document.getElementById("st-provider").textContent = provMatch ? provMatch[1].trim() : (env.LLM_MODEL ? "custom" : "—"); document.getElementById("st-model").textContent = modelMatch ? modelMatch[1].trim() : (env.LLM_MODEL || "—"); } catch {} @@ -553,12 +554,42 @@

Configure DAppNode Nexus

Sign up at nexus.dappnode.com and create an API key from your dashboard.
+
+ +
+ Sends prompts through the Nexus Local Proxy on this DAppNode instead of straight to the Nexus API. + The proxy checks the Gateway is running inside genuine sealed hardware, running the exact software DAppNode published, before + anything you type leaves this machine — and encrypts prompts and replies so nothing in between can read them. + If that check fails, nothing is sent. +
+ + +
Browse the available models at nexus.dappnode.com/models and paste the full provider/model ID below.
- +
`; + document.getElementById("nexus-model-container") + .appendChild(createModelInput("nexus-model", [], "e.g. minimax/minimax-m2.7")); + const teeToggle = document.getElementById("nexus-tee-enabled"); + teeToggle.checked = nexusTeeEnabled; + teeToggle.addEventListener("change", (e) => { + nexusTeeEnabled = e.target.checked; + document.getElementById("nexus-tee-panel").style.display = e.target.checked ? "block" : "none"; + if (e.target.checked) probeNexus(); + }); + document.getElementById("nexus-tee-panel").style.display = nexusTeeEnabled ? "block" : "none"; + if (nexusTeeEnabled) probeNexus(); return; } @@ -695,6 +726,46 @@

Configure ${p.name}

} } + // Whether prompts go through the local verifying proxy. Restored from the + // saved base URL so reopening the wizard does not silently turn it off. + let nexusTeeEnabled = false; + const NEXUS_TEE_BASE_URL = "http://nexus-local-proxy.dappnode.private:3301/v1"; + const NEXUS_STANDARD_BASE_URL = "https://nexus-api.dappnode.com/v1"; + + function nexusBaseUrl() { + return nexusTeeEnabled ? NEXUS_TEE_BASE_URL : NEXUS_STANDARD_BASE_URL; + } + + async function probeNexus() { + const btn = document.getElementById("nexus-probe-btn"); + const icon = document.getElementById("nexus-probe-icon"); + const resultEl = document.getElementById("nexus-probe-result"); + if (!btn || !resultEl) return; + btn.disabled = true; icon.textContent = "\u23F3"; resultEl.innerHTML = ""; + try { + const resp = await fetch("/api/nexus/probe"); + const data = await resp.json(); + if (data.reachable && data.verified) { + const rev = data.sourceRevision ? data.sourceRevision.slice(0, 12) : "unknown"; + resultEl.innerHTML = `
Gateway verified. The local proxy checked ${data.checks || 0} things and confirmed the Gateway is running DAppNode release ${rev} inside sealed hardware.
Your prompts will be encrypted to that enclave.
`; + if (data.models && data.models.length) { + const mc = document.getElementById("nexus-model-container"); + const previous = (document.getElementById("nexus-model-input") || {}).value || ""; + mc.innerHTML = ""; + mc.appendChild(createModelInput("nexus-model", data.models, "Select or type a model...")); + document.getElementById("nexus-model-input").value = previous || data.models[0]; + } + } else if (data.reachable) { + resultEl.innerHTML = `
Proxy running, but not verified. ${data.reason || "Status: " + (data.status || "unknown")}.
It will not carry prompts until it can verify the Gateway. Leave TEE mode off until this reads verified.
`; + } else { + resultEl.innerHTML = `
Nexus Local Proxy not found. Install the Nexus Local Proxy package on this DAppNode, then check again. Without it, TEE mode cannot be used.
`; + } + } catch { + resultEl.innerHTML = `
Could not check the local proxy.
`; + } + btn.disabled = false; icon.textContent = "\uD83D\uDD0D"; + } + async function probeOllama() { const btn = document.getElementById("probe-btn"); const icon = document.getElementById("probe-icon"); @@ -738,7 +809,7 @@

Configure ${p.name}

if (p.id === "nexus") { const apiKey = (document.getElementById("api-key").value || "").trim(); if (apiKey) env.NEXUS_API_KEY = apiKey; - env.OPENAI_BASE_URL = "https://nexus-api.dappnode.com/v1"; + env.OPENAI_BASE_URL = nexusBaseUrl(); env.OPENAI_API_KEY = apiKey; env.LLM_MODEL = getModelValue() || "minimax/minimax-m2.7"; } else if (p.id === "ollama") { @@ -789,7 +860,7 @@

Configure ${p.name}

if (p.id === "nexus") { lines.push(` default: "${getModelValue() || "minimax/minimax-m2.7"}"`); lines.push(` provider: "custom"`); - lines.push(` base_url: "https://nexus-api.dappnode.com/v1"`); + lines.push(` base_url: "${nexusBaseUrl()}"`); } else if (p.id === "ollama") { const url = (document.getElementById("ollama-url").value || "").trim() || "http://ollama.dappnode:11434"; lines.push(` default: "${getModelValue() || "llama3"}"`); diff --git a/setup-wizard/server.cjs b/setup-wizard/server.cjs index 343e877..c301e14 100644 --- a/setup-wizard/server.cjs +++ b/setup-wizard/server.cjs @@ -12,6 +12,14 @@ const CONFIG_FILE = path.join(HERMES_HOME, "config.yaml"); const ENV_FILE = path.join(HERMES_HOME, ".env"); const HTML_FILE = path.join(__dirname, "index.html"); +// The Nexus Local Proxy is the only thing on a DAppNode that verifies the +// Nexus Gateway's enclave attestation and encrypts prompt bodies to it. Hermes +// never speaks that protocol itself: it points at the proxy and lets it do the +// verifying, which is why TEE mode is a base-URL switch and nothing more. +const NEXUS_TEE_BASE_URL = "http://nexus-local-proxy.dappnode.private:3301/v1"; +const NEXUS_STANDARD_BASE_URL = "https://nexus-api.dappnode.com/v1"; +const NEXUS_TEE_VERIFICATION_URL = "http://nexus-local-proxy.dappnode.private:3301/v1/verification"; + const OLLAMA_CANDIDATES = [ "http://ollama.ollama-nvidia-openwebui.dappnode:11434", "http://ollama.ollama-amd-openwebui.dappnode:11434", @@ -93,6 +101,46 @@ function readEnv() { catch { return {}; } } +// probeNexusProxy reports whether the local proxy is installed and has +// verified the Gateway. It reads the proxy's own verification state rather +// than attesting anything here: one verifier on the node, and everything else +// reads its answer. +async function probeNexusProxy() { + try { + const resp = await fetch(NEXUS_TEE_VERIFICATION_URL, { signal: AbortSignal.timeout(5000) }); + if (!resp.ok) { + return { reachable: true, verified: false, reason: `verification endpoint returned HTTP ${resp.status}` }; + } + const data = await resp.json(); + const current = data.current || {}; + return { + reachable: true, + verified: data.status === "verified", + status: data.status || "unknown", + gateway: data.gateway || null, + sourceRevision: current.source_revision || null, + checks: Array.isArray(current.checks) ? current.checks.length : 0, + models: await fetchNexusModels(), + }; + } catch { + return { reachable: false, verified: false, reason: "not installed or not running" }; + } +} + +// The proxy passes the Gateway's public model catalog through, so the wizard +// can offer real model IDs instead of asking the user to copy one from a web +// page. A failure here is not fatal: the model field stays free-text. +async function fetchNexusModels() { + try { + const resp = await fetch(`${NEXUS_TEE_BASE_URL}/models`, { signal: AbortSignal.timeout(5000) }); + if (!resp.ok) return []; + const data = await resp.json(); + return (data.data || []).map((m) => m.id).filter(Boolean).sort(); + } catch { + return []; + } +} + async function probeOllama() { for (const url of OLLAMA_CANDIDATES) { try { @@ -201,6 +249,13 @@ const server = http.createServer(async (req, res) => { return; } + // Probe the Nexus Local Proxy + if (req.method === "GET" && url.pathname === "/api/nexus/probe") { + const result = await probeNexusProxy(); + json(res, 200, result); + return; + } + // Probe Ollama if (req.method === "GET" && url.pathname === "/api/ollama/probe") { const result = await probeOllama();