Skip to content

Add a Private mode toggle that routes Nexus through the attested local proxy - #43

Open
Marketen wants to merge 3 commits into
mainfrom
feat/nexus-attested-proxy
Open

Add a Private mode toggle that routes Nexus through the attested local proxy#43
Marketen wants to merge 3 commits into
mainfrom
feat/nexus-attested-proxy

Conversation

@Marketen

@Marketen Marketen commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Rebased onto current main (8a15e02). The branch previously carried four unrelated local commits — bypass login, seamless no auth ui, and two restart-loop fixes — because it was cut from a local master that had unpushed work. Those are gone; the PR is now a single commit touching 4 files.

What

The wizard hardcoded the direct Nexus endpoint, which is the path where TLS terminates at Cloudflare and prompts are readable there. A checkbox on the Nexus configuration screen switches model.base_url to the nexus-local-proxy package on the same DAppNode, which verifies the Gateway's AWS Nitro attestation against a pinned policy and encrypts bodies with EHBP past that point.

A toggle in this package rather than a separate one: a fork would double maintenance and split users.

The base URL is written in two places — env.OPENAI_BASE_URL and model.base_url in the generated config.yaml — and both now go through one nexusBaseUrl() helper so they cannot drift apart. config.yaml is the load-bearing one; the env vars get cleared by config migration on boot.

Default is off. The proxy fails closed, so opting in should be a deliberate choice made after reading what it changes. The toggle copy states that trade-off rather than presenting it as free, and links to the proxy's verification page.

The new Login-with-Nexus panel from #34 sits above it untouched — that flow only mints an API key and never writes a base URL, so these two write sites remain the only ones.

No manifest dependency is declared. nexus-local-proxy has never been published to the DAppNode registry: resolving it by name returns NOREPO, and a registry search for "nexus" returns nothing — only its IPFS artifacts exist, and it has been installed by hash. A dependencies entry would fail to resolve at install time and break installing Hermes at all. The toggle copy carries the requirement instead: it names the package, says this one will not install it, and tells the user to leave the toggle off until they have.

Supporting fixes in patch-config.py

  • The context_size lookup was gated on the literal string nexus-api.dappnode.com in base_url. With Private mode on that gate is false, so model.context_length would never be set and every model would silently use Hermes' 256K fallback. It now recognises either Nexus route, and falls back to the public catalog when the configured endpoint has no /models.

  • That lookup has never worked. Cloudflare fronts nexus-api.dappnode.com and 403s the default Python-urllib/<ver> User-Agent, so the fetch always failed and every Nexus user has been running on the 256K fallback. Confirmed on a live node: the running agent's config.yaml has no context_length at all. Upstream Hermes guards against the same WAF behaviour in providers/base.py. A real User-Agent fixes it — verified returning 1048576 and 204800 for Deepseek V4 Flash and MiniMax M2.7.

Verified on a real DAppNode

  • Both containers are on dnprivate_network, so nexus-local-proxy.dappnode.private resolves and is reachable from Hermes.
  • Pointed the live Hermes agent at the proxy and ran real inference: HTTP 200, correct completions, 3/3 runs. Config restored byte-identical afterwards.
  • Re-run after the rebase: wizard JS syntax-checked; buildEnv() / buildConfigYaml() exercised against a DOM stub — default is direct, toggle flips both write sites together, API key and context_length still land in config.yaml; patch-config.py helpers re-verified live.
  • fix: Ollama auto discover and URL fix #33's Ollama URL fix (env.OPENAI_BASE_URL = url) is preserved by the merge.

Dependency chain — now unblocked

The streaming bug this PR previously listed as a blocker is fixed in dappnode/dappnode-nexus-sdk#4 (merged). Remaining order:

  1. dappnode-nexus-sdk#3, bump nexus #4 — merged
  2. nexus-local-proxy#10 — merged, but the released v0.1.1 does not contain it. Its published manifest pins 5a77ef4; the release workflow started from the bump NousResearch/hermes-agent to v2026.5.7 #9 merge and entrypoint improvements #10 landed eight minutes into that run, while the follow-up run was skipped by the version gate. dappnode/DAppNodePackage-nexus-local-proxy#11 bumps to 0.1.2 from the correctly-pinned main.
  3. release nexus-local-proxy 0.1.2
  4. this PR — no longer version-coupled, since it declares no dependency

One thing for a maintainer to decide

  • Version. The merge conflict was main's v2026.7.20 + 0.1.7 against my v2026.7.1 + 0.1.8. I kept main's upstream bump and set 0.1.8. But a 0.1.8 dev build is already installed on at least one node, and releases.json records 0.1.0/0.1.1/0.2.0 — the numbering in this repo needs a call.
  • Upstream bump. Re-checked against the v2026.7.20 image now that main has moved: CustomProfile.fetch_models() still defers to {base_url}/models, providers/base.py still resolves the URL the same way, and there is still no custom entry in the static fallback catalog. The picker rationale holds unchanged, so only the version question below is open.

🤖 Generated with Claude Code

Comment thread dappnode/patch-config.py Fixed
@Marketen

Copy link
Copy Markdown
Contributor Author

The streaming issue listed under Known issue, not from this PR is now fixed in dappnode-nexus-sdk#4.

Root cause was forwardEventStream consulting tracker.done only on the io.EOF branch. Hermes' client closes the response body on data: [DONE], which cancels the request context, tears down the upstream read, and surfaces truncated EHBP response frame prefix — a non-EOF error after a complete, authenticated response. The proxy then aborted the connection and recorded a failure.

Verified against the live Gateway with a client that closes on [DONE]: before, 0 encrypted / 1 failed; after, 3 encrypted / 0 failed. Fail-closed behaviour for a stream cut before the terminator is unchanged and still covered by tests.

Release chain for Private mode is now: dappnode-nexus-sdk#3 + #4 → nexus-local-proxy 0.1.1 → this PR.

@Marketen
Marketen marked this pull request as ready for review August 24, 2026 10:20
@tropibot

tropibot Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Dappnode bot has built and pinned the built packages to an IPFS node, for commit: cca08b6

This is a development version and should only be installed for testing purposes.

  1. Package hermes-agent.dnp.dappnode.eth

Install link

Hash: /ipfs/Qme33aW8avNYqtGumX689EZNTMrcEsh4cQJQtZFNzpbb33

(by dappnodebot/build-action)

@tropibot

tropibot Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

❌ Package harness failed

MCP tool 'dappnode_update_package' failed: Error: dappGet could not resolve request hermes-agent.dnp.dappnode.eth@/ipfs/QmZHiaUSn5BA2Habevik9TJc4bhFW27F38e88itRQ4EgpB, error on aggregate stage: A dependency of the requested package "hermes-agent.dnp.dappnode.eth" (version range: "/ipfs/QmZHiaUSn5BA2Habevik9TJc4bhFW27F38e88itRQ4EgpB") could not be resolved

Package Verdict Reason Duration Cleanup
hermes-agent.dnp.dappnode.eth ❌ Failed Candidate install failed (candidate_install_failed) 2m 28s ✅ Passed

Run overview

Context Value
Pull request dappnode/DAppNodePackage-Hermes-agent#43
Tested commit d9d2b4c1d2fa
Candidate artifact Install on DAppNode · /ipfs/QmZHiaUSn5BA2Habevik9TJc4bhFW27F38e88itRQ4EgpB
Baseline request automatic
Worker worker-01
Run ID gh-pr-43-d9d2b4c1d2fa-6a3acccdf57d7bf6
Execution 2026-08-24T10:20:39.375834451+00:002026-08-24T10:23:08.167353458+00:00 (✅ Completed)

Baseline vs candidate

Check Baseline Candidate
Version 0.1.8 not reported
Install ✅ Passed · 1m 53s ❌ Failed · 0s
Hard check ✅ Passed · 5 stable samples ❌ Failed · 0 stable samples
Containers 1 observed 0 observed
Stabilization 22s 0s
Log collection ✅ Passed · 1 container ❌ Failed · 0 containers

Hard-check reasons

  • Baseline: none
  • Candidate: candidate_containers_unstable

Container comparison

  • Added: none
  • Removed: none
  • Baseline inventory: none
  • Candidate inventory: none
  • Baseline non-running states: none
  • Candidate non-running states: none
  • Deterministic regressions: ✅ none
Container inventory

Baseline

Container Service State Image Created
DAppNodePackage-hermes-agent.hermes-agent.dnp.dappnode.eth hermes-agent ✅ Running hermes-agent.hermes-agent.dnp.dappnode.eth:0.1.8 1787566954

Candidate

No container snapshot was available.

Runtime analysis advisory

⚪ Inconclusive — Log analysis was unavailable

Analyzer Status Summary
Heuristic ⚪ Inconclusive Log analysis was unavailable
Baseline evidence ⚪ Inconclusive Analysis unavailable
Candidate evidence ⚪ Inconclusive Analysis unavailable

No new runtime findings were reported.

Analyzer diagnostics
  • run ended before comparative log analysis

Cleanup and diagnostics

Check Result
Cleanup ✅ Passed
Leftover packages none
Cleanup error none
Phase Code Message
Candidate install candidate_install_failed MCP tool 'dappnode_update_package' failed: Error: dappGet could not resolve request hermes-agent.dnp.dappnode.eth@/ipfs/QmZHiaUSn5BA2Habevik9TJc4bhFW27F38e88itRQ4EgpB, error on aggregate stage: A dependency of the requested package "hermes-agent.dnp.dappnode.eth" (version range: "/ipfs/QmZHiaUSn5BA2Habevik9TJc4bhFW27F38e88itRQ4EgpB") could not be resolved

Deterministic checks decide the verdict. Runtime log analysis is advisory. TropiBot publishes bounded summaries and never posts raw logs.

@Marketen
Marketen force-pushed the feat/nexus-attested-proxy branch from d9d2b4c to 0aa3e19 Compare August 24, 2026 10:24
…l proxy

The wizard hardcoded the direct Nexus endpoint, which is the path where TLS
terminates at Cloudflare and prompts are readable there. A checkbox on the
Nexus configuration screen now switches model.base_url to the nexus-local-proxy
package on the same DAppNode, which verifies the Gateway's AWS Nitro
attestation against a pinned policy and encrypts bodies with EHBP past that
point. It is a toggle in this package rather than a separate package: a fork
would double maintenance and split users.

The base URL is written in two places -- env.OPENAI_BASE_URL and model.base_url
in the generated config.yaml -- and both now go through one nexusBaseUrl()
helper so they cannot drift apart. config.yaml is the load-bearing one; the
env vars get cleared by config migration on boot.

Default is off. The proxy fails closed, so opting in should be a deliberate
choice made after reading what it changes rather than something a user
discovers when inference stops working. The toggle copy states that trade-off
rather than presenting it as free -- a verification failure surfaces as
connection errors with no silent fallback to the unprotected path -- and links
to the proxy's verification page.

No manifest dependency is declared. nexus-local-proxy has never been published
to the DAppNode registry: resolving it by name returns NOREPO and it does not
appear in a registry search, because only its IPFS artifacts exist and it has
been installed by hash. A dependencies entry would therefore fail to resolve at
install time and break installing Hermes at all. The toggle copy carries the
requirement instead -- it names the package, says this one will not install it,
and tells the user to leave the toggle off until they have.

Two supporting fixes in patch-config.py:

- The context_size lookup was gated on the literal string nexus-api.dappnode.com
  in base_url. With Private mode on that gate is false, so model.context_length
  would never be set and every model would silently use Hermes' 256K fallback
  -- wrong for all of them (Deepseek V4 Flash is 1048576, MiniMax M2.7 is
  204800). It now recognises either Nexus route, and falls back to the public
  catalog when the configured endpoint has no /models, which is the case on
  nexus-local-proxy releases before 0.1.1.

- That lookup has in fact never worked. Cloudflare fronts nexus-api.dappnode.com
  and 403s the default Python-urllib User-Agent, so the fetch always failed and
  every Nexus user has been running on the 256K fallback. Upstream Hermes
  already guards against the same WAF behaviour in providers/base.py. Sending a
  real User-Agent fixes it; verified live, returning 1048576 and 204800 for the
  two models above, both directly and through a running proxy.

Verified: wizard JS syntax-checked, and buildEnv()/buildConfigYaml() exercised
against a DOM stub to confirm both write sites flip together with the toggle
while the API key and context_length still land in config.yaml.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Marketen
Marketen force-pushed the feat/nexus-attested-proxy branch from 0aa3e19 to cca08b6 Compare August 24, 2026 10:36
@tropibot

tropibot Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

🛠️ Package harness hit an infrastructure error

MCP tool 'dappnode_update_package' failed: Error: Can't download hermes-agent.dnp.dappnode.eth image: Could not get block QmYWCjhr9rT5FVF2zXevK1Q51LhYqXDhMTWZ4u8JjLJW7L

Package Verdict Reason Duration Cleanup
hermes-agent.dnp.dappnode.eth 🛠️ Infrastructure error Candidate install failed (candidate_install_failed) 12m 52s ✅ Passed

Run overview

Context Value
Pull request dappnode/DAppNodePackage-Hermes-agent#43
Tested commit cca08b645159
Candidate artifact Install on DAppNode · /ipfs/Qme33aW8avNYqtGumX689EZNTMrcEsh4cQJQtZFNzpbb33
Baseline request automatic
Worker worker-01
Run ID gh-pr-43-cca08b645159-2cbc0e9d4bad1b25
Execution 2026-08-24T10:47:48.030488779+00:002026-08-24T11:00:40.117052409+00:00 (✅ Completed)

Baseline vs candidate

Check Baseline Candidate
Version 0.1.8 not reported
Install ✅ Passed · 1m 42s ❌ Failed · 0s
Hard check ✅ Passed · 5 stable samples ❌ Failed · 0 stable samples
Containers 1 observed 0 observed
Stabilization 22s 0s
Log collection ✅ Passed · 1 container ❌ Failed · 0 containers

Hard-check reasons

  • Baseline: none
  • Candidate: candidate_containers_unstable

Container comparison

  • Added: none
  • Removed: none
  • Baseline inventory: none
  • Candidate inventory: none
  • Baseline non-running states: none
  • Candidate non-running states: none
  • Deterministic regressions: ✅ none
Container inventory

Baseline

Container Service State Image Created
DAppNodePackage-hermes-agent.hermes-agent.dnp.dappnode.eth hermes-agent ✅ Running hermes-agent.hermes-agent.dnp.dappnode.eth:0.1.8 1787568571

Candidate

No container snapshot was available.

Runtime analysis advisory

⚪ Inconclusive — Log analysis was unavailable

Analyzer Status Summary
Heuristic ⚪ Inconclusive Log analysis was unavailable
Baseline evidence ⚪ Inconclusive Analysis unavailable
Candidate evidence ⚪ Inconclusive Analysis unavailable

No new runtime findings were reported.

Analyzer diagnostics
  • run ended before comparative log analysis

Cleanup and diagnostics

Check Result
Cleanup ✅ Passed
Leftover packages none
Cleanup error none
Phase Code Message
Candidate install candidate_install_failed MCP tool 'dappnode_update_package' failed: Error: Can't download hermes-agent.dnp.dappnode.eth image: Could not get block QmYWCjhr9rT5FVF2zXevK1Q51LhYqXDhMTWZ4u8JjLJW7L

Deterministic checks decide the verdict. Runtime log analysis is advisory. TropiBot publishes bounded summaries and never posts raw logs.

Marketen and others added 2 commits August 25, 2026 21:16
The dependency came out earlier because nexus-local-proxy.dnp.dappnode.eth had
no registry entry -- resolving it by name returned NOREPO, so declaring it would
have broken installing Hermes at all. It is now shipped as a core package via
DNP_CORE, so it is guaranteed present and the dependency resolves.

That makes the toggle copy accurate again: it no longer tells the user to go
install the proxy themselves, only what routing through it changes and that it
fails closed.

Requires nexus-local-proxy 0.2.0 and the DNP_CORE release that carries it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Marketen

Copy link
Copy Markdown
Contributor Author

Updated. Two commits added on top:

  • 5467e02 added the nexus-local-proxy dependency once the proxy was made a core package
  • e29d976 reverts it — the package still has no registry entry (NOREPO), so the dependency will be added back when it is actually published

Net effect on this PR is unchanged from before: no manifest dependency, and the toggle copy tells the user to install nexus-local-proxy themselves.

Related, now open:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants