Skip to content

integrate(rc2): land reviewed onion, Tari, topology, and harness fixes - #1972

Merged
VijitSingh97 merged 31 commits into
developfrom
codex/rc2-reviewed-integration
Sep 7, 2026
Merged

integrate(rc2): land reviewed onion, Tari, topology, and harness fixes#1972
VijitSingh97 merged 31 commits into
developfrom
codex/rc2-reviewed-integration

Conversation

@VijitSingh97

Copy link
Copy Markdown
Collaborator

Closes #1936.

Closes #1896.

Addresses #1853 now that the header and appliance quadlet plumbing land together. The merge seat
will close that issue by hand after the combined change lands, matching the original PR's intent.

Addresses the container half of #1903. Its host half, #1905, is already on develop; the merge seat
will close the issue by hand after the combined change lands, matching the original PR's intent.

Result

This integrates five already reviewed RC2 fixes on the current develop base so CI judges their
release-shaped interaction once:

  • the appliance dashboard quadlet carries the three dashboard-onion display settings;
  • the dashboard header shows the provisioned onion on Compose and appliance installs without
    receiving client keys;
  • tari.mode: off removes Tari merge-mining arguments before bridge setup;
  • the KVM browser-submit and prefill controls share the reviewed polling diagnostics;
  • topology labels incoming client hops without claiming a LAN boundary or complete listener
    inventory, and the opt-in Tor health probe appears in the egress model.

The #1880 documentation and code comments now describe both deployment paths because #1942 closes
the appliance gap in this same tree. The merge itself required no conflict resolution. Current
develop had grown the stack runner by one line since #1944 was reviewed, so one separator blank
line was removed to keep its 440-line ceiling; source order and behavior are unchanged.

#1856 remains open for its wider inventory and local-miner work. The remote configured-node route
classification is unchanged: direct/private paths do not become Tor routes. The existing Tor probe
remains opt-in and SOCKS-only, with no clearnet fallback.

Preserved source heads and review evidence

All five source heads are ancestors of this branch through normal merge commits. PRs #1939, #1952,
and #1947 are not included.

Focused checks on the combined tree

  • dashboard onion and topology frontend tests: 17 passed;
  • focused header/egress/Tor-probe pytest: 53 passed;
  • fake-daemon payload contracts: 31 passed;
  • quadlet render/parity domain: 37 passed;
  • Tari-off entrypoint domain: 14 passed;
  • sourced harness-tooling domain: 34 passed, including the wizard-state poll self-test;
  • poll self-test, shell syntax/ShellCheck/shfmt, focused Ruff/Biome, markdown/YAML, docs voice,
    operator-string, topology-class, file-budget, and generated pithead parity checks passed.

No full suite, container build, KVM run, hardware run, key access, release action, or deployment was
performed on this integration branch. The source reviews retain their stated runtime limits; remote
CI is the combined candidate gate.

Merge

This is a draft for the non-author merge seat. Do not merge until the exact-head combined review is
recorded and required CI is green.

VijitSingh97 and others added 30 commits September 6, 2026 02:55
…oard is on Tor

The dashboard onion had no surface anywhere in the browser: the only place
its address appeared was a `pithead status` line, and on the appliance nobody
has a shell to run that in. Someone who wanted to open the dashboard from a
phone over Tor had to reach the CLI first.

The three DASHBOARD_ONION_* values now reach the dashboard service, and the
header renders the URL in full under the machine name, with a copy control and
a note when client authorisation is on — an onion that is enabled but not yet
provisioned renders as nothing, because an empty row reads as breakage rather
than as "Tor is off". No client-auth key material is passed into the container.

host_display_addr moves to the new web/header.py beside the onion reader: both
are ways IN to the machine rather than readings off it, and views.py was at its
recorded file-budget ceiling. The frontend fixture generator patched
`views.detect_host_ipv4`, which the move would have left binding a dead
attribute while the fixture went machine-dependent in silence; it now patches
the function where it is looked up.

This contradicts a stance docs/dashboard.md took: the doctor panel redacts the
onion "because the address is worth keeping out of" the container. That prose
is corrected rather than left standing — the panel still redacts, the header
does not, and the doc now says so instead of implying the address is absent
from the browser.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KZuzdtxNX8ae4rnDtBqEqR
…s, not three

The reviewer's finding on PR #1880 is true and I re-derived it at source. The
paragraph I added claimed `pithead doctor` prints the Monero, Tari and P2Pool
onion addresses and that an operator can run it to read one. It does not, and
they cannot:

- `06-doctor.sh:295` emits `dr_ok "$k set."` on the success path — the variable
  name and a verdict, never the value.
- The failure path (`:294`) prints `(value: '${onion:-empty}')`, and
  `onion_missing` (`32-onion-provisioning.sh:66-68`) is
  `[ -z "$1" ] || [ "$1" == "placeholder" ]`, so the only values it can print
  are the literals `empty` and `placeholder`.
- The remote-node path prints "not needed" and no value at all.
- The one onion the report carries in full is the dashboard's own
  (`06-doctor.sh:304` via `dashboard_onion_status`, `04-status.sh:38-49`).
- `doctor --json` (`06-doctor.sh:359-363`) `jq -Rs`-wraps that same text, so it
  is not a second document with more in it.

So the panel's redaction acts on exactly one address here — the one the header
now prints — and the honest version of the paragraph says that. The pointer for
the three node onions goes to the stack `.env` (`33-render-env.sh:382-384`),
which the encrypted backup archive carries (`45-control-backup.sh:104`); the
undefined term "emergency kit" is gone with it.

Tier: none — prose only, no behaviour change. Green at this head:
lint-docs-voice, lint-md, lint-topology, lint-file-budget,
lint-operator-strings. Repo-wide phrase sweep for the claim leaves one hit,
the corrected sentence.

What the operator sees change on the appliance: nothing in the product. The
Diagnostics doc no longer sends them to a command that will not answer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KZuzdtxNX8ae4rnDtBqEqR
…y announces itself

The design lane's non-author RETURN on PR #1880 (comment 5556336128) was posted
at b62d37f and I had not read it — my previous push answered only the other
reviewer's finding. Its three asks, addressed here:

1. The note rendered "Get the key withpithead onion-client-key". htm strips a
   whitespace run CONTAINING A NEWLINE from both ends of every static text
   chunk, so breaking the line before the <span> deletes the space. `${" "}` is
   the repo's existing answer at 13 sites in 3 files, because a field is pushed
   as its own child and never goes through that regex. Design measured the
   broken render at 1400px and 390px in both themes; the second reviewer
   derived the same result from the vendored parser's source.

2. The note named a shell command on a product whose first paragraph says
   nobody has a shell, and `dashboard.onion.client_auth` defaults to true while
   `dashboard.onion.*` is never committable from the Configuration view — so an
   appliance shows this note by default to a reader who cannot act on it. The
   sentence now says who it is for. The reachability gap itself is #1882, not
   this PR.

3. "Copied" was a status message with no live region (WCAG 4.1.3). The label
   itself is the status, so `aria-live="polite"` goes on the button.

The old tests were green on defect 1: one asserted "Client authorisation is on"
and one asserted "onion-client-key", and each matches one side of the join. The
new assertion strips tags and pins the whole phrase. Proven to fire: reverting
only the `${" "}` reddens it 7/8 and restoring greens it 8/8, with the mutation
asserting its own needle so a no-op substitution cannot pass as a control.

Green at this head: node --test onionurl 8/8, make test-frontend 574/574,
make lint-js (biome 2.5.0, which does lint CSS as well as JS), lint-file-budget.
Repo sweep: no doc or Python string quotes the old note wording.

What the operator sees change on the appliance: the client-auth line under the
onion URL reads as a sentence instead of running two words together, and it now
says the command is for a machine they can log in to.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KZuzdtxNX8ae4rnDtBqEqR
…nd says so

The prose promised the header to the reader who cannot have it. `dashboard_onion()`
reads three environment variables; docker-compose.yml passes them, and the appliance's
podman quadlet for the dashboard does not (`36-quadlet-units.sh` writes that unit's
`Environment=` line with none of the three; all three rendered `os/quadlet/*dashboard.container`
carry zero `onion` matches against a `HOST_IP` control of one, while `tor.container` carries
two — so the sweep is not blind to the string). On an appliance the server therefore sends no
onion and the block is correctly absent — which is exactly the machine the removed sentences
named as the reason the block exists.

The code is right and fails in the safe direction; the documentation was wrong, so the
documentation moves. The quadlet gap is #1896, filed against the paths that own it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KZuzdtxNX8ae4rnDtBqEqR
`aria-live` on the button made the label the status message, and the label never came
back down: `copied` was set true and nothing ever set it false, so the region's content
stopped changing and a live region with unchanged content announces nothing. The first
copy was announced and every copy after it was silent — to exactly the reader the
attribute was added for. It also left the control named "Copied": a state, where a
returning reader needs the action.

The button keeps one name, "Copy address". The confirmation moves to a sibling
`role="status"`, which is rendered empty rather than conditionally — a live region
inserted with its message already inside it presents no change to announce — and comes
down on a timer, so the next copy is a change again. A failed copy clears a standing
confirmation and disarms the pending timer, so the previous "Copied" cannot read as this
attempt's answer.

Proven to fire, each mutation naming its own row: removing the revert timer reddens the
clears-itself test; dropping the `clearTimeout` reddens the failed-copy test; putting the
confirmation back in the label, or making the status region conditional, reddens the
markup test. Restored tree byte-identical to pre-battery.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KZuzdtxNX8ae4rnDtBqEqR
…s not sanitise

CodeQL read the test's `replace(/<[^>]*>/g, '')` as an incomplete multi-character
sanitizer and posted a new high-severity alert, which held the required CodeQL
check red. The string never reaches a DOM — it is read by assert.match — but a
sanitizer-shaped line in a security-reviewed surface is worth not writing.

Same extraction via split/join, plus the half that was undocumented and is load
bearing: this substitutes NOTHING for a tag. The sibling in xvbview.test.mjs
substitutes a space, and a space would rejoin "to,<span>pithead" into readable
text and pass over the very defect the assertion exists to catch.

Proven by a controlled pair, not by the suite going green: with the explicit
${" "} removed from onionurl.mjs:90 the assertion fails on
"log in to,pithead onion-client-key prints it." and the two narrower assertions
above it stay green, which is what the comment claims; restored by sha256 and
10/10 pass again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KZuzdtxNX8ae4rnDtBqEqR
…mes what it saw

The RC1 battery's provision phase reddened on the bare word no-served-config,
which covered a non-200, a 5 s timeout on a cold first read and a non-JSON body
alike. The helper now reads /api/state up to six times, 5 s apart, and when no
read serves a config the reason carries the status, curl's rc, the try count
and the head of the body.

Measured on the host: a timeout parses to http=000 with an empty body, a live
200 without .config falls through to the reason, a JSON body with a trailing
newline yields .config intact. run.sh is set -uo pipefail, so the failing
assignment cannot exit the harness.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NeSaJPWy7AkhYcYpGVkxBJ
…n values

The appliance's dashboard unit never received DASHBOARD_ONION_ENABLED / _ADDRESS /
_CLIENT_AUTH, so the header's .onion block (#1853, compose side in #1880) could not render on
the machine it was written for. The quadlet writer now puts the three on the dashboard unit's
Environment= line, next to the other DASHBOARD_* flags, read from the rendered .env like every
other value; the three fixtures are re-rendered (one line each). Display-only: the client keys
are still never passed in.

test-render-quadlet gains the rule that would have caught this: every DASHBOARD_* key the
compose dashboard service carries must be on the rendered quadlet unit, with a control row that
the compose parse sees the service's own keys.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NeSaJPWy7AkhYcYpGVkxBJ
The host half (#1855) renders TARI_MODE=off and starts no Tari node, but the p2pool
entrypoint still exec'd an argv carrying `--merge-mine <url> <address>`, so an off machine
merge-mined against a node that was not there. When TARI_MODE is the literal "off" the
entrypoint drops the triple, in either spelling, before the Tor block reads argv, so the
merge-mine bridge cannot fire on it either; a flag in the address slot is kept, the rule
_redact_argv already applies. Unset or any other value leaves argv exactly as it arrived.

A sibling fragment, test-p2pool-tari-off.sh, pins the off shape (both spellings, the
positional launch line, no Tari bridge), the flag-in-value-slot rule, and the untouched argv
for local and for an unset variable. test-monero-tari.sh is at its ceiling, so the rows live
beside it; run.sh gains the stanza and loses a separator, line-neutral.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NeSaJPWy7AkhYcYpGVkxBJ
… drop line is claimed only on a drop

Review return on #1944 (R1, R2, N1, N2): rows 7-9 asserted one token, so a
strip that ate the flag alone, or the address alone, read as untouched; they
now pin ARGC. The "dropped from the launch" line fires only when a triple was
removed, with a row for the no-triple argv. The entrypoint comment says that
until #1905 renders TARI_MODE the block strips nothing.

Out-of-lane (currency: build/p2pool/entrypoint.sh; tests: the fragment),
disclosed in the PR body.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NeSaJPWy7AkhYcYpGVkxBJ
…ute, not the dashboard's

The bench said it, six of six: http=404 body "404: Not Found". The helper
read /api/state, which only the provisioned dashboard serves; the wizard
registers /api/wizard-state (wizard.py:526), the route run.sh's own
pre-fill check already reads. Both reads in the helper move to it. The
browser-shaped provision leg could not have passed on any tree since #1847.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NeSaJPWy7AkhYcYpGVkxBJ
… state route too

The fourth /api/state read in tests/os — the fixture-works control at the top of the
reinstall pre-fill submit leg — used the wizard's session jar against a route only the
provisioned dashboard registers. The wizard serves /api/wizard-state (wizard.py:526), and its
`config` carries `monero.wallet_address`, the field the control compares. Without this the first
install run to get past the restore leg (PR #1947) would have redded with "pre-fill NOT armed
(served wallet: none)" — loud, but naming the wrong cause.

Found by the fixes lane at source (git grep at d973163: zero "/api/state" in wizard.py, two
"/api/wizard-state"); re-derived here before the edit. The three /api/state reads left in
run.sh are basic-auth reads against the provisioned dashboard and stay.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NeSaJPWy7AkhYcYpGVkxBJ
…hat it saw

One helper, wizard_state_poll, now does the /api/state read for both consumers: up to six
reads 5 s apart until a jq filter yields a value, and on no hit a reason naming the status,
curl's rc, the read count and the head of the body. provision_browser_submit (#1932) calls it
for `.config`; the reinstall pre-fill leg's fixture-works control (#1936) calls it for the
served wallet, so its red names the wallet it saw, or what the reads saw when none carried one,
instead of "served wallet: none" for a timeout, a 401 and a non-JSON page alike.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NeSaJPWy7AkhYcYpGVkxBJ
…e option comment is true

The HTML shape's body now exceeds the 60-byte bound, so `head -c 60` is pinned (the reviewer's
M3 mutant, `60`→`40`, now reddens one check); the self-test's `set` line names the options
run.sh actually uses, `-uo pipefail`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NeSaJPWy7AkhYcYpGVkxBJ
…nly the poll's logic

The fixes lane's review mutated /api/wizard-state to a bogus path at every site and the
self-test still passed: the curl shim branched on $shape and never read its arguments, so the
URL was invisible to it by construction. The shim now records the URL (curl's last argument)
instead of a placeholder, and one case asserts it once against the wizard's registered route
(#1932). Measured here: the mutated copy now fails that case with the bogus route in the
message; the read-count cases are unchanged (one line per call either way).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NeSaJPWy7AkhYcYpGVkxBJ
…ads consecutive trailing comments

The gate's shfmt -i 4 -d aligned curl() {'s existing trailing comment against the new one on the
echo line; an early lint-sh failure also skips the suite steps, so --self-test never ran in CI at
3d34444. Diagnosed and the fix measured by the fixes lane; re-run here: shfmt clean, self-test
passes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NeSaJPWy7AkhYcYpGVkxBJ
…machine"

The Stack Topology diagram routed the rigs -> xmrig-proxy stratum hop and the
browser -> caddy HTTPS hop as LOCAL, while its own legend defines Local as "this
machine" and LAN as "a hop that leaves it but stays on your network", and both
nodes sit in the LAN zone (topology_graph.py:39-40). The comment above those two
edges already read "Ingress from your LAN"; only the code disagreed. Neither hop
is a leak either way, so no leak count, headline or legend entry changes.

Second half: the #424 Tor egress probe now appears in both privacy views.
tor_heal sends PROBE_URL through TOR_SOCKS_PROXY every five minutes while
tor.auto_heal is on (tor_heal.py:63, :130-134). It was in neither the "All
connections" list nor the diagram, nor anywhere in docs/privacy.md, so an
operator auditing that list was reading one that omitted a connection.

egress.py is at its file-budget ceiling, so edge/ext_node move to
topology_graph.py, whose docstring already records "egress has no room" as the
reason the route vocabulary lives there. That rename collided with the loop
variable in the firewall-tagging pass, which the suite caught, not review.

Tier: unit. test_tor_probe_egress.py is a new module because test_egress.py is
at 451/451; the ingress test there is corrected in place, line-neutral, and now
checks both edges' route where it checked only the rigs one.

The frontend fixture is regenerated. It was ALREADY stale on develop and had
been hand-edited: it carried tari values the generator does not produce at
defaults and indentation json.dump cannot emit. Its drift guard compares dotted
key paths only, never values, so value drift there is invisible to CI.

What the operator sees: the two ingress arrows turn blue (LAN) rather than grey,
and "All connections" gains a Tor egress probe row, inactive unless
tor.auto_heal is on.

NOT done, and #1856 stays open for it: the on-box local_miner internal edge
(row 1's second half) needs a new node id, which touches TOPOLOGY_NODES, the
frontend POS map and two canonical-id lists, one of them in test_egress.py at
its ceiling. Rows 4/5/15 are #1849's and row 2b is #1853's, per the issue.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KZuzdtxNX8ae4rnDtBqEqR
Missed in the previous commit: it was staged before this edit, so the tree
shipped a comment naming _notify_knobs, which this branch renamed to
_shared_knobs. Comment only, line-neutral, test_egress.py stays 451/451.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KZuzdtxNX8ae4rnDtBqEqR
…e-pr1940

# Conflicts:
#	tests/os/provision-browser-submit.sh
#	tests/os/reinstall-prefill-submit-leg.sh
@VijitSingh97
VijitSingh97 marked this pull request as ready for review September 7, 2026 05:37
@VijitSingh97

Copy link
Copy Markdown
Collaborator Author

Independent integration review: PASS at 19a9bc8491f68bb3860dae9f0b80bcc357d839ce.

Sol/high security and Luna/high verification confirmed the five exact source heads, required onion dependency order, intended exclusions, and the two integration-only adjustments. Source reviews carry their runtime limits. Focused results: 17 frontend, 53 Python, 31 fake contracts, 37 Quadlet, 14 Tari-entrypoint and 34 harness assertions passed; relevant lint and generated CLI parity passed. Full required PR CI and the combined runtime/OS battery remain separate gates. Original PRs stay open until this replacement merges.

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

Labels

None yet

Projects

None yet

1 participant