Skip to content
Open
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
14 changes: 14 additions & 0 deletions dappnode/dappnode/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions dappnode_package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
6 changes: 3 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down
77 changes: 74 additions & 3 deletions setup-wizard/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,7 @@ <h2 style="text-align:center">Configuration Saved!</h2>
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 {}
Expand Down Expand Up @@ -553,12 +554,42 @@ <h2>Configure DAppNode Nexus</h2>
<div class="hint">Sign up at <strong>nexus.dappnode.com</strong> and create an API key from your dashboard.</div>
<input type="password" id="api-key" placeholder="Paste your Nexus API key here">
</div>
<div class="field">
<label>Verified TEE mode <span class="optional-badge">stronger privacy</span></label>
<div class="hint">
Sends prompts through the <strong>Nexus Local Proxy</strong> 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 &mdash; and encrypts prompts and replies so nothing in between can read them.
If that check fails, nothing is sent.
</div>
<label style="display:flex;align-items:center;gap:8px;font-weight:normal;margin-top:6px;">
<input type="checkbox" id="nexus-tee-enabled" style="width:auto;">
Use the verified TEE Gateway
</label>
<div id="nexus-tee-panel" style="display:none;margin-top:10px;">
<button class="probe-btn" id="nexus-probe-btn" onclick="probeNexus()">
<span id="nexus-probe-icon">&#128269;</span> Check the local proxy
</button>
<div id="nexus-probe-result"></div>
</div>
</div>
<div class="field">
<label>Model</label>
<div class="hint">Browse the available models at <a href="https://nexus.dappnode.com/models" target="_blank" style="color:var(--primary)">nexus.dappnode.com/models</a> and paste the full <code>provider/model</code> ID below.</div>
<input type="text" id="nexus-model-input" placeholder="e.g. minimax/minimax-m2.7">
<div id="nexus-model-container"></div>
</div>
`;
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;
}

Expand Down Expand Up @@ -695,6 +726,46 @@ <h2>Configure ${p.name}</h2>
}
}

// 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 = `<div class="probe-result ok"><strong>Gateway verified.</strong> The local proxy checked ${data.checks || 0} things and confirmed the Gateway is running DAppNode release <code>${rev}</code> inside sealed hardware.<br>Your prompts will be encrypted to that enclave.</div>`;
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 = `<div class="probe-result fail"><strong>Proxy running, but not verified.</strong> ${data.reason || "Status: " + (data.status || "unknown")}.<br>It will not carry prompts until it can verify the Gateway. Leave TEE mode off until this reads verified.</div>`;
} else {
resultEl.innerHTML = `<div class="probe-result fail"><strong>Nexus Local Proxy not found.</strong> Install the <strong>Nexus Local Proxy</strong> package on this DAppNode, then check again. Without it, TEE mode cannot be used.</div>`;
}
} catch {
resultEl.innerHTML = `<div class="probe-result fail">Could not check the local proxy.</div>`;
}
btn.disabled = false; icon.textContent = "\uD83D\uDD0D";
}

async function probeOllama() {
const btn = document.getElementById("probe-btn");
const icon = document.getElementById("probe-icon");
Expand Down Expand Up @@ -738,7 +809,7 @@ <h2>Configure ${p.name}</h2>
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") {
Expand Down Expand Up @@ -789,7 +860,7 @@ <h2>Configure ${p.name}</h2>
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"}"`);
Expand Down
55 changes: 55 additions & 0 deletions setup-wizard/server.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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();
Expand Down
Loading