feat(pithead): #1855 tari.mode "off" — a machine can decline to merge-mine (host half) - #1905
Conversation
…and whether to join the raffle The host half (#1905) taught the stack `tari.mode: "off"` and the no-JS form to write it. The page an operator actually sees could not say it: the select was `remoteTari ? "remote" : "local"`, two states for what is now three. A machine holding "off" rendered as "Run the bundled node on this machine" — the wizard misreported the machine to its own operator — and one touch of that control wrote a yes back. There was no way to answer No at all, which is the finding. The question is now "Merge-mine Tari?", No first and default, and the Tari payout address moved under it. It had to move: the field carried `required`, so left in the Payout addresses section it would have blocked submit on a form that never asks the question. It keeps `required` behind a yes — the same bar the Monero address holds. tariAnswer is the migration rule, and it reads the direction the host reads it. Only the literal "off" is a decline; a missing key is `local`, because that is what 28-parse-and-validate-config.sh does with a config written before this question existed. Reading an absent key as "off" would tell an upgraded 1.x machine it had declined merge-mining and write that decline back on submit. #1848 adds the raffle switch under Mining, opt-out where Tari is opt-in, its note lifted from the FAQ rather than invented. `xvb.enabled` is true in config.reference.json, so the default answer is the state the machine is already in. Both go in a new module: wizard.mjs was 884/889 and wizard.test.mjs 765/765, so the budget decided the design, as it did for #1850 and #1853. wizard.mjs comes out 21 lines lighter. Four existing render probes asserted the old heading and the retired Tari question. Corrected in place and line-neutral (765/765 holds). The three `doesNotMatch(/Payout addresses/)` among them mattered most: left alone they would have passed over a rendered "Payout address" forever. Prose the change falsifies, swept for rather than remembered: README's "every hash merge-mines Tari", appliance.md's "Required, like the Monero one: this stack always merge-mines both coins", and the two dev recipes whose M6 says to paste both addresses into a form that now asks for one. NOT done, and not mine: `./pithead setup` (24-config-wizard.sh) still demands a Tari address and never asks the mode, so the CLI path cannot decline. Filed separately. appliance.md says a remote Tari node saves ~200 GB while the wizard says a local one adds ~170 GB; both predate this change and I did not reconcile them by guessing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KZuzdtxNX8ae4rnDtBqEqR
…and whether to join the raffle The host half (#1905) taught the stack `tari.mode: "off"` and the no-JS form to write it. The page an operator actually sees could not say it: the select was `remoteTari ? "remote" : "local"`, two states for what is now three. A machine holding "off" rendered as "Run the bundled node on this machine" — the wizard misreported the machine to its own operator — and one touch of that control wrote a yes back. There was no way to answer No at all, which is the finding. The question is now "Merge-mine Tari?", No first and default, and the Tari payout address moved under it. It had to move: the field carried `required`, so left in the Payout addresses section it would have blocked submit on a form that never asks the question. It keeps `required` behind a yes — the same bar the Monero address holds. tariAnswer is the migration rule, and it reads the direction the host reads it. Only the literal "off" is a decline; a missing key is `local`, because that is what 28-parse-and-validate-config.sh does with a config written before this question existed. Reading an absent key as "off" would tell an upgraded 1.x machine it had declined merge-mining and write that decline back on submit. #1848 adds the raffle switch under Mining, opt-out where Tari is opt-in, its note lifted from the FAQ rather than invented. `xvb.enabled` is true in config.reference.json, so the default answer is the state the machine is already in. Both go in a new module: wizard.mjs was 884/889 and wizard.test.mjs 765/765, so the budget decided the design, as it did for #1850 and #1853. wizard.mjs comes out 21 lines lighter. Four existing render probes asserted the old heading and the retired Tari question. Corrected in place and line-neutral (765/765 holds). The three `doesNotMatch(/Payout addresses/)` among them mattered most: left alone they would have passed over a rendered "Payout address" forever. Prose the change falsifies, swept for rather than remembered: README's "every hash merge-mines Tari", appliance.md's "Required, like the Monero one: this stack always merge-mines both coins", and the two dev recipes whose M6 says to paste both addresses into a form that now asks for one. NOT done, and not mine: `./pithead setup` (24-config-wizard.sh) still demands a Tari address and never asks the mode, so the CLI path cannot decline. Filed separately. appliance.md says a remote Tari node saves ~200 GB while the wizard says a local one adds ~170 GB; both predate this change and I did not reconcile them by guessing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KZuzdtxNX8ae4rnDtBqEqR
… the decline Found by re-reading the KVM provision leg #1847 just merged, not by the tests: it posts back whatever `/api/wizard-state` served it, so I went to check what that is for a fresh box. It was `tari.mode: "local"`. The commit before this one gave the page three answers and put No first. That is not the same as No being the answer a new machine arrives holding. The state API builds the page's config as the reference merged under the last attempt, and for a machine with no attempt it fell through to a seed carrying only `local_miner.enabled`. So the reference decided, the reference says `local`, and every new operator would still have opened the wizard on "Yes — run a Tari node on this machine". The whole finding survives a correct component. The seed gains `"tari": {"mode": "off"}` and gets a name, because what it is FOR is now load-bearing: it is the page's answer for a machine that has none, and it is reached only through the `or`. The reference keeps saying `local` and must — it is the default for a config that ALREADY EXISTS, and moving it there would stop merge-mining on every upgraded install. Those two facts look contradictory and are the design; the comment says so where the constant lives. The tests go in a new sibling file for the reason its three siblings give: test_wizard.py is at its 974-line ceiling. They pin the served value rather than the component, and the pair that matters is the migration one — a last attempt with NO tari key at all is still a machine WITH answers, so it gets the reference's `local`, never the new machine's decline. The raffle needs no seed: #1848 is opt-out and the reference already says true. Asserted by moving the reference and watching the served value follow, since a value that happens to equal the reference proves nothing about its source. ⛔ THIS BRANCH MUST NOT MERGE BEFORE #1905. `28-parse-and-validate-config.sh:78-81` on `origin/develop` today is `local|remote` and errors on anything else, so a tree with this and without the host half refuses to provision ANY new machine — including #1847's browser-submit leg, which posts the served config whole. Told the appliance lane; it has recorded the order. PROVEN at this tree: 2582 passed / 0 failed, coverage 97.58% (make test-dashboard); the six wizard modules 132 passed settled from --junitxml; ruff lint+format, file-budget (wizard.py 645/674), pithead parity. A 4-mutation battery on the seed, each proven applied and restored: reverting to the old seed, answering `local`, forcing the seed over an existing attempt, and pinning the raffle each reddened the row that names them, none survived. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KZuzdtxNX8ae4rnDtBqEqR
… the decline Found by re-reading the KVM provision leg #1847 just merged, not by the tests: it posts back whatever `/api/wizard-state` served it, so I went to check what that is for a fresh box. It was `tari.mode: "local"`. The commit before this one gave the page three answers and put No first. That is not the same as No being the answer a new machine arrives holding. The state API builds the page's config as the reference merged under the last attempt, and for a machine with no attempt it fell through to a seed carrying only `local_miner.enabled`. So the reference decided, the reference says `local`, and every new operator would still have opened the wizard on "Yes — run a Tari node on this machine". The whole finding survives a correct component. The seed gains `"tari": {"mode": "off"}` and gets a name, because what it is FOR is now load-bearing: it is the page's answer for a machine that has none, and it is reached only through the `or`. The reference keeps saying `local` and must — it is the default for a config that ALREADY EXISTS, and moving it there would stop merge-mining on every upgraded install. Those two facts look contradictory and are the design; the comment says so where the constant lives. The tests go in a new sibling file for the reason its three siblings give: test_wizard.py is at its 974-line ceiling. They pin the served value rather than the component, and the pair that matters is the migration one — a last attempt with NO tari key at all is still a machine WITH answers, so it gets the reference's `local`, never the new machine's decline. The raffle needs no seed: #1848 is opt-out and the reference already says true. Asserted by moving the reference and watching the served value follow, since a value that happens to equal the reference proves nothing about its source. ⛔ THIS BRANCH MUST NOT MERGE BEFORE #1905. `28-parse-and-validate-config.sh:78-81` on `origin/develop` today is `local|remote` and errors on anything else, so a tree with this and without the host half refuses to provision ANY new machine — including #1847's browser-submit leg, which posts the served config whole. Told the appliance lane; it has recorded the order. PROVEN at this tree: 2582 passed / 0 failed, coverage 97.58% (make test-dashboard); the six wizard modules 132 passed settled from --junitxml; ruff lint+format, file-budget (wizard.py 645/674), pithead parity. A 4-mutation battery on the seed, each proven applied and restored: reverting to the old seed, answering `local`, forcing the seed over an existing attempt, and pinning the raffle each reddened the row that names them, none survived. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KZuzdtxNX8ae4rnDtBqEqR
… the decline Found by re-reading the KVM provision leg #1847 just merged, not by the tests: it posts back whatever `/api/wizard-state` served it, so I went to check what that is for a fresh box. It was `tari.mode: "local"`. The commit before this one gave the page three answers and put No first. That is not the same as No being the answer a new machine arrives holding. The state API builds the page's config as the reference merged under the last attempt, and for a machine with no attempt it fell through to a seed carrying only `local_miner.enabled`. So the reference decided, the reference says `local`, and every new operator would still have opened the wizard on "Yes — run a Tari node on this machine". The whole finding survives a correct component. The seed gains `"tari": {"mode": "off"}` and gets a name, because what it is FOR is now load-bearing: it is the page's answer for a machine that has none, and it is reached only through the `or`. The reference keeps saying `local` and must — it is the default for a config that ALREADY EXISTS, and moving it there would stop merge-mining on every upgraded install. Those two facts look contradictory and are the design; the comment says so where the constant lives. The tests go in a new sibling file for the reason its three siblings give: test_wizard.py is at its 974-line ceiling. They pin the served value rather than the component, and the pair that matters is the migration one — a last attempt with NO tari key at all is still a machine WITH answers, so it gets the reference's `local`, never the new machine's decline. The raffle needs no seed: #1848 is opt-out and the reference already says true. Asserted by moving the reference and watching the served value follow, since a value that happens to equal the reference proves nothing about its source. ⛔ THIS BRANCH MUST NOT MERGE BEFORE #1905. `28-parse-and-validate-config.sh:78-81` on `origin/develop` today is `local|remote` and errors on anything else, so a tree with this and without the host half refuses to provision ANY new machine — including #1847's browser-submit leg, which posts the served config whole. Told the appliance lane; it has recorded the order. PROVEN at this tree: 2582 passed / 0 failed, coverage 97.58% (make test-dashboard); the six wizard modules 132 passed settled from --junitxml; ruff lint+format, file-budget (wizard.py 645/674), pithead parity. A 4-mutation battery on the seed, each proven applied and restored: reverting to the old seed, answering `local`, forcing the seed over an existing attempt, and pinning the raffle each reddened the row that names them, none survived. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KZuzdtxNX8ae4rnDtBqEqR
…and whether to join the raffle The host half (#1905) taught the stack `tari.mode: "off"` and the no-JS form to write it. The page an operator actually sees could not say it: the select was `remoteTari ? "remote" : "local"`, two states for what is now three. A machine holding "off" rendered as "Run the bundled node on this machine" — the wizard misreported the machine to its own operator — and one touch of that control wrote a yes back. There was no way to answer No at all, which is the finding. The question is now "Merge-mine Tari?", No first and default, and the Tari payout address moved under it. It had to move: the field carried `required`, so left in the Payout addresses section it would have blocked submit on a form that never asks the question. It keeps `required` behind a yes — the same bar the Monero address holds. tariAnswer is the migration rule, and it reads the direction the host reads it. Only the literal "off" is a decline; a missing key is `local`, because that is what 28-parse-and-validate-config.sh does with a config written before this question existed. Reading an absent key as "off" would tell an upgraded 1.x machine it had declined merge-mining and write that decline back on submit. #1848 adds the raffle switch under Mining, opt-out where Tari is opt-in, its note lifted from the FAQ rather than invented. `xvb.enabled` is true in config.reference.json, so the default answer is the state the machine is already in. Both go in a new module: wizard.mjs was 884/889 and wizard.test.mjs 765/765, so the budget decided the design, as it did for #1850 and #1853. wizard.mjs comes out 21 lines lighter. Four existing render probes asserted the old heading and the retired Tari question. Corrected in place and line-neutral (765/765 holds). The three `doesNotMatch(/Payout addresses/)` among them mattered most: left alone they would have passed over a rendered "Payout address" forever. Prose the change falsifies, swept for rather than remembered: README's "every hash merge-mines Tari", appliance.md's "Required, like the Monero one: this stack always merge-mines both coins", and the two dev recipes whose M6 says to paste both addresses into a form that now asks for one. NOT done, and not mine: `./pithead setup` (24-config-wizard.sh) still demands a Tari address and never asks the mode, so the CLI path cannot decline. Filed separately. appliance.md says a remote Tari node saves ~200 GB while the wizard says a local one adds ~170 GB; both predate this change and I did not reconcile them by guessing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KZuzdtxNX8ae4rnDtBqEqR
… the decline Found by re-reading the KVM provision leg #1847 just merged, not by the tests: it posts back whatever `/api/wizard-state` served it, so I went to check what that is for a fresh box. It was `tari.mode: "local"`. The commit before this one gave the page three answers and put No first. That is not the same as No being the answer a new machine arrives holding. The state API builds the page's config as the reference merged under the last attempt, and for a machine with no attempt it fell through to a seed carrying only `local_miner.enabled`. So the reference decided, the reference says `local`, and every new operator would still have opened the wizard on "Yes — run a Tari node on this machine". The whole finding survives a correct component. The seed gains `"tari": {"mode": "off"}` and gets a name, because what it is FOR is now load-bearing: it is the page's answer for a machine that has none, and it is reached only through the `or`. The reference keeps saying `local` and must — it is the default for a config that ALREADY EXISTS, and moving it there would stop merge-mining on every upgraded install. Those two facts look contradictory and are the design; the comment says so where the constant lives. The tests go in a new sibling file for the reason its three siblings give: test_wizard.py is at its 974-line ceiling. They pin the served value rather than the component, and the pair that matters is the migration one — a last attempt with NO tari key at all is still a machine WITH answers, so it gets the reference's `local`, never the new machine's decline. The raffle needs no seed: #1848 is opt-out and the reference already says true. Asserted by moving the reference and watching the served value follow, since a value that happens to equal the reference proves nothing about its source. ⛔ THIS BRANCH MUST NOT MERGE BEFORE #1905. `28-parse-and-validate-config.sh:78-81` on `origin/develop` today is `local|remote` and errors on anything else, so a tree with this and without the host half refuses to provision ANY new machine — including #1847's browser-submit leg, which posts the served config whole. Told the appliance lane; it has recorded the order. PROVEN at this tree: 2582 passed / 0 failed, coverage 97.58% (make test-dashboard); the six wizard modules 132 passed settled from --junitxml; ruff lint+format, file-budget (wizard.py 645/674), pithead parity. A 4-mutation battery on the seed, each proven applied and restored: reverting to the old seed, answering `local`, forcing the seed over an existing attempt, and pinning the raffle each reddened the row that names them, none survived. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KZuzdtxNX8ae4rnDtBqEqR
…-mine The operator ruled Tari merge-mining opt-in for 2.0 after the first appliance image showed "Tari DOWN" on a box that never asked for Tari. This is the HOST half: the parser, render-env and compose. The dashboard surfaces and the wizard question are separate PRs. tari.mode accepts a third value "off": no local_tari profile, no Tari payout address required, and TARI_MODE now reaches the containers so p2pool's entrypoint can drop the --merge-mine triple from argv. THE MIGRATION TRAP, which is the point of the first test. Every 1.x install merge-mines and none of them ever wrote tari.mode, so the parser's default for a MISSING key stays "local". Flipping it would silently stop every upgraded machine with nothing in the logs. A new machine gets "off" written explicitly by the wizard, in its own PR. FOUND BY RUNNING THE TESTS, not by reading the code, and TWICE. `apply` still demanded a tari.wallet_address with tari.mode off, so an operator who does not merge-mine was being asked for a Tari payout address. The required-fields gate (28:29-33) runs BEFORE tari.mode was parsed, so the whole mode block moves above it and the gate reads $TARI_MODE. Side effect worth naming: an invalid tari.mode is now refused ahead of the wallet check rather than after it — the better error, but a change in order a reviewer should see. That was not enough, and the first run said so: THERE ARE TWO GATES on that address. tari_address_type (28:70) then rejected the EMPTY address before the mode was consulted, so `off` still could not apply, and four more assertions cascaded off that rc 1. The case subject becomes "$TARI_MODE:$(tari_address_type ...)" with an `off:*` arm — line-neutral, and it also makes the right thing happen for a machine that turns Tari off while a stale address sits in its config: an address nothing uses can no longer block an apply. The suite is 3413 passed / 0 failed at this head; the run before this fix was 3406/7. BOTH SLICES ARE LINE-NEUTRAL against their ceilings (485/485 and 494/494). The "off" branch cost nothing because it restructures the existing if/else into if/elif rather than adding an arm; the one new .env line was paid for out of a comment in the same block. No new slice was cut and no budget row was added. CARVED OUT AND NOW FILED AS #1903: build/p2pool/entrypoint.sh must drop --merge-mine and the two argv items after it when TARI_MODE=off. That file is the currency lane's, not in my grant. Two ephemerals were dispatched and both exited without doing the work. Until it lands, tari.mode "off" stops the bundled node and the dashboard surfaces but p2pool still passes merge-mine arguments pointing at a node that is not running. The two halves are order-independent by construction: the compose default is ${TARI_MODE:-local}, so an install predating this variable behaves exactly as before. pithead artifact: regenerated, lint-pithead-parity green. `pithead` is the appliance lane's path, not in my grant — it cleared this explicitly, having re-derived that its own in-flight branches touch slices 08/12/12a and not 28/33, and .lane-override is armed for that one file. Whichever of #1844 and #1855 merges SECOND regenerates from the new tip rather than hand-patching. tests/stack/run.sh: registering the new test domain costs 2 lines against a 440/440 ceiling with zero headroom, paid with two decorative divider lines in the same region. That file is not strictly inside my grant's "tier-1 tests" wording — flagged here for the reviewer to rule on rather than done quietly. PROVEN AT THIS TREE: bash -n and shfmt -i 4 on both slices and both test files, lint-pithead-parity, lint-file-budget, lint-yaml, test-compose, lint-md, lint-docs-voice. NOT PROVEN: make test-stack was STILL RUNNING when this commit was written, so the new domain's 12 assertions are UNVERIFIED at this head. An earlier run of an earlier tree reddened 5 of them, which is what surfaced the wallet-address defect above; the fix for it is in this commit and has not been re-run. Do not read this commit as a green suite. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KZuzdtxNX8ae4rnDtBqEqR
7a3c2a7 to
f89acc5
Compare
… stays intact Both blockers from the non-author review of #1905, plus its two minor findings. Re-derived each before fixing rather than taking the finding on confidence. F1 (blocker) — a machine that declined merge-mining mined NOTHING. TARI_REQUIRED came from dashboard.tari_required alone, default true, with no TARI_MODE input (33-render-env.sh:157), and nothing in dashboard/ reads TARI_MODE at all. With mode off there is no Tari node, so tari_synced is permanently false, so the sync gate `monero_synced and (tari_synced or not TARI_REQUIRED)` never opens, and its else arm stops p2pool and xmrig-proxy every cycle. The exact inverse of what the operator asked for. TARI_MODE off now decides tari_required outright; local and remote keep dashboard.tari_required as the override. F2 (blocker) — an empty TARI_WALLET_ADDRESS shifted p2pool's argv. With off, no wallet_address is written and `// empty` renders the variable empty. p2pool's `--merge-mine tari://<addr> <wallet>` triple is one unquoted line in the appliance Quadlet unit; systemd Exec= splits on whitespace and emits no empty word, so the value does not arrive empty, it VANISHES — and p2pool reads --onion-address as its payout address. Now renders the stock inert placeholder, the same treatment TARI_GRPC_ADDRESS already gets. #1903 (drop the triple when off) is still open and will retire this. F3 — preflight demanded ~200 GiB of Tari headroom on an off machine; the guard cleared only for remote. Now `!= local`, matching doctor's profile-keyed sibling and the same edit already made at 33-render-env.sh:295. F4 — a compose comment claimed the dashboard renders no Tari surface when off. It has no TARI_MODE consumer; the comment now carries the same honest disclaimer its p2pool sibling does. 33-render-env.sh was already at its 494/494 ceiling on develop and ceilings only go down, so F1 and F2 are line-neutral there by construction. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KZuzdtxNX8ae4rnDtBqEqR
All four findings fixed at
|
| # | Fix |
|---|---|
| F1 | TARI_MODE=off decides tari_required outright. local/remote keep dashboard.tari_required as the operator override. |
| F2 | TARI_WALLET_ADDRESS=${TARI_WALLET:-your_tari_wallet_address} — the same inert-placeholder treatment TARI_GRPC_ADDRESS already had. |
| F3 | != "local" rather than == "remote", matching doctor's profile-keyed sibling. |
| F4 | The comment now carries the same disclaimer its p2pool sibling does. |
The budget decided the shape, not taste. 33-render-env.sh was already 494/494 on develop, and the gate rejects any edit that raises a ceiling — so F1 and F2 are line-neutral there by construction: F1 folds into the existing jq line via --arg m "$TARI_MODE" (keeping your "reuse the parse-time validated globals" rule rather than re-reading .tari.mode), and its 6-line comment was rewritten inside its own 6 lines.
What I ran, and what I could not
lint-file-budget, lint-pithead-parity, lint-docs-voice, lint-operator-strings, lint-topology, lint-md, lint-yaml — all rc 0. bash -n on the test file, rc 0. pithead rebuilt (8 insertions / 8 deletions, matching the line-neutral slice edits).
I could NOT run tests/stack/run.sh: the bench is claimed for the #1318 battery, so my lane's rule is pytest/node only and CI is the gate. So the new assertions are not yet observed passing — CI is their first real run. Instead I verified both fixes at the jq/shell tier, extracting the shipped expressions out of the file rather than retyping them (a re-spelled control is mutated in lockstep with nothing):
off -> false local -> true
local+override -> false remote -> true <- the override SURVIVES
TARI_WALLET='' -> 'your_tari_wallet_address' (non-empty)
The third row is the one that matters: it shows F1's fix did not simply hardcode the flag.
Two things I added that you did not ask for
- A control leg for the new assertion.
off -> TARI_REQUIRED=falsealone proves only that the variable exists, so test (1) now asserts the missing-key/local machine readstrue. The pair discriminates; either half alone does not. - Your F2 reasoning is now in the test comment, including that tari.mode "off" still passes --merge-mine to P2Pool: the entrypoint drops no argv #1903 is open, not landed. Test (4)'s old comment asserted the entrypoint "drops the merge-mine triple from argv" as present-tense fact — it does not yet, and that false comment is part of why the empty wallet looked harmless. It was my comment, and it read as a safety property.
Merge order stands, and your mechanism for it is better than the one I gave you — the #1847 leg reaching it through the JSON path rather than build_config makes it firmer. #1905 first, then #1919.
Re-pass requested at 14133730. I am the author, so I merge neither.
Non-author delta pass — pithead pull 1905 (#1855 host half) at
|
…tead of a view that cannot change it The second reviewer's RETURN, taken. Two sentences this branch introduced told the operator they could turn merge-mining on later from the dashboard's Configuration view. They cannot. Re-derived at source rather than taken on the reviewer's word — all three legs hold: - `control_service.py` builds the editable set from EDITABLE_ENV_KEY_PATHS, whose only tari entries are `dashboard.tari_required`, `tari.mem_limit`, `tari.data_dir` and `tari.clearnet_initial_sync`. There is no `tari.mode`, so the field renders greyed. - The host agrees independently: no TARI_MODE in CONTROL_DASHBOARD_EDITABLE_KEYS or the confirm map (`42-control-policy-and-host-checks.sh:61-79`), and the policy is default-deny. - #1905 adds TARI_MODE to `.env` (`33-render-env.sh:470`) and adds it to NEITHER list — I checked both rather than assuming the host half covered it. The capability is real, only the route named was wrong: a set-up-again boot re-runs the wizard seeded from the live config (`12a-setup-again.sh:26-28`), so the select opens on the stored value. Both sentences now say that, following the phrasing `docs/appliance.md:250-254` already uses for the other host-only switches. NOTHING GUARDED THIS — no test asserted either sentence, which is why a false claim about another subsystem's editability shipped. Added one, scoped to TariSection ON PURPOSE: the XvbField sibling says "Changeable later" and that is TRUE (XVB_ENABLED -> xvb.enabled), so a needle swept over the whole form would have pinned the honest row. The needle forbids the PROMISE, not the phrase — the correct copy names the Configuration view too, in order to say it does not carry this switch, so a bare /Configuration view/ needle reddens on the fix as readily as on the defect. Proven: mutation control — reverting the copy to the old sentence reddens exactly this guard, restored by sha256. Frontend suite 594 passed rc 0; lint-js, lint-docs-voice, lint-operator-strings all rc 0, the last run AFTER staging. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KZuzdtxNX8ae4rnDtBqEqR
… 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
…and whether to join the raffle The host half (#1905) taught the stack `tari.mode: "off"` and the no-JS form to write it. The page an operator actually sees could not say it: the select was `remoteTari ? "remote" : "local"`, two states for what is now three. A machine holding "off" rendered as "Run the bundled node on this machine" — the wizard misreported the machine to its own operator — and one touch of that control wrote a yes back. There was no way to answer No at all, which is the finding. The question is now "Merge-mine Tari?", No first and default, and the Tari payout address moved under it. It had to move: the field carried `required`, so left in the Payout addresses section it would have blocked submit on a form that never asks the question. It keeps `required` behind a yes — the same bar the Monero address holds. tariAnswer is the migration rule, and it reads the direction the host reads it. Only the literal "off" is a decline; a missing key is `local`, because that is what 28-parse-and-validate-config.sh does with a config written before this question existed. Reading an absent key as "off" would tell an upgraded 1.x machine it had declined merge-mining and write that decline back on submit. note lifted from the FAQ rather than invented. `xvb.enabled` is true in config.reference.json, so the default answer is the state the machine is already in. Both go in a new module: wizard.mjs was 884/889 and wizard.test.mjs 765/765, so the budget decided the design, as it did for #1850 and #1853. wizard.mjs comes out 21 lines lighter. Four existing render probes asserted the old heading and the retired Tari question. Corrected in place and line-neutral (765/765 holds). The three `doesNotMatch(/Payout addresses/)` among them mattered most: left alone they would have passed over a rendered "Payout address" forever. Prose the change falsifies, swept for rather than remembered: README's "every hash merge-mines Tari", appliance.md's "Required, like the Monero one: this stack always merge-mines both coins", and the two dev recipes whose M6 says to paste both addresses into a form that now asks for one. NOT done, and not mine: `./pithead setup` (24-config-wizard.sh) still demands a Tari address and never asks the mode, so the CLI path cannot decline. Filed separately. appliance.md says a remote Tari node saves ~200 GB while the wizard says a local one adds ~170 GB; both predate this change and I did not reconcile them by guessing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KZuzdtxNX8ae4rnDtBqEqR (cherry picked from commit 0393a12)
… the decline Found by re-reading the KVM provision leg #1847 just merged, not by the tests: it posts back whatever `/api/wizard-state` served it, so I went to check what that is for a fresh box. It was `tari.mode: "local"`. The commit before this one gave the page three answers and put No first. That is not the same as No being the answer a new machine arrives holding. The state API builds the page's config as the reference merged under the last attempt, and for a machine with no attempt it fell through to a seed carrying only `local_miner.enabled`. So the reference decided, the reference says `local`, and every new operator would still have opened the wizard on "Yes — run a Tari node on this machine". The whole finding survives a correct component. The seed gains `"tari": {"mode": "off"}` and gets a name, because what it is FOR is now load-bearing: it is the page's answer for a machine that has none, and it is reached only through the `or`. The reference keeps saying `local` and must — it is the default for a config that ALREADY EXISTS, and moving it there would stop merge-mining on every upgraded install. Those two facts look contradictory and are the design; the comment says so where the constant lives. The tests go in a new sibling file for the reason its three siblings give: test_wizard.py is at its 974-line ceiling. They pin the served value rather than the component, and the pair that matters is the migration one — a last attempt with NO tari key at all is still a machine WITH answers, so it gets the reference's `local`, never the new machine's decline. The raffle needs no seed: #1848 is opt-out and the reference already says true. Asserted by moving the reference and watching the served value follow, since a value that happens to equal the reference proves nothing about its source. ⛔ THIS BRANCH MUST NOT MERGE BEFORE #1905. `28-parse-and-validate-config.sh:78-81` on `origin/develop` today is `local|remote` and errors on anything else, so a tree with this and without the host half refuses to provision ANY new machine — including #1847's browser-submit leg, which posts the served config whole. Told the appliance lane; it has recorded the order. PROVEN at this tree: 2582 passed / 0 failed, coverage 97.58% (make test-dashboard); the six wizard modules 132 passed settled from --junitxml; ruff lint+format, file-budget (wizard.py 645/674), pithead parity. A 4-mutation battery on the seed, each proven applied and restored: reverting to the old seed, answering `local`, forcing the seed over an existing attempt, and pinning the raffle each reddened the row that names them, none survived. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KZuzdtxNX8ae4rnDtBqEqR (cherry picked from commit c04d122)
…tead of a view that cannot change it The second reviewer's RETURN, taken. Two sentences this branch introduced told the operator they could turn merge-mining on later from the dashboard's Configuration view. They cannot. Re-derived at source rather than taken on the reviewer's word — all three legs hold: - `control_service.py` builds the editable set from EDITABLE_ENV_KEY_PATHS, whose only tari entries are `dashboard.tari_required`, `tari.mem_limit`, `tari.data_dir` and `tari.clearnet_initial_sync`. There is no `tari.mode`, so the field renders greyed. - The host agrees independently: no TARI_MODE in CONTROL_DASHBOARD_EDITABLE_KEYS or the confirm map (`42-control-policy-and-host-checks.sh:61-79`), and the policy is default-deny. - #1905 adds TARI_MODE to `.env` (`33-render-env.sh:470`) and adds it to NEITHER list — I checked both rather than assuming the host half covered it. The capability is real, only the route named was wrong: a set-up-again boot re-runs the wizard seeded from the live config (`12a-setup-again.sh:26-28`), so the select opens on the stored value. Both sentences now say that, following the phrasing `docs/appliance.md:250-254` already uses for the other host-only switches. NOTHING GUARDED THIS — no test asserted either sentence, which is why a false claim about another subsystem's editability shipped. Added one, scoped to TariSection ON PURPOSE: the XvbField sibling says "Changeable later" and that is TRUE (XVB_ENABLED -> xvb.enabled), so a needle swept over the whole form would have pinned the honest row. The needle forbids the PROMISE, not the phrase — the correct copy names the Configuration view too, in order to say it does not carry this switch, so a bare /Configuration view/ needle reddens on the fix as readily as on the defect. Proven: mutation control — reverting the copy to the old sentence reddens exactly this guard, restored by sha256. Frontend suite 594 passed rc 0; lint-js, lint-docs-voice, lint-operator-strings all rc 0, the last run AFTER staging. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KZuzdtxNX8ae4rnDtBqEqR (cherry picked from commit 30c8ddf)
Addresses #1855 (host half) — deliberately NOT
Closes.developis the default branch now, so the keyword fires, and this PR is one of three halves: the dashboard surfaces and the wizard question (#1848) are separate, and P2Pool's argv is #1903. Hand-close #1855 when all three land.What the operator sees change on the appliance
Nothing yet — by design. This is the host half: the parser accepts
tari.mode: "off", no Tari container starts, no Tari payout address is demanded, andTARI_MODEreaches the p2pool and dashboard containers. What an operator would see if they hand-wrote"mode": "off"today:applysucceeds with notari.wallet_addressat all, and no Tari node starts.The migration trap, which is the point of the first test
Every 1.x install merge-mines and none of them ever wrote
tari.mode. The parser's default for a MISSING key stayslocal. Flipping the default would silently stop every upgraded machine with nothing in the logs. A new machine gets"off"written explicitly by the wizard, in #1848's PR. Tier 1 proves it with a config that has notari.mode.Found by running the tests, twice, and both were product defects
The first:
applydemanded atari.wallet_addresswith the mode off, because the required-fields gate runs beforetari.modewas parsed. The mode block moved above it.That was not enough, and the first full suite run said so: there are TWO gates on that address.
tari_address_type(28-parse…sh:70) then rejected the EMPTY address before the mode was consulted, sooffstill could not apply — and four more assertions cascaded off that one rc 1, reading like four separate defects. The case subject becomes"$TARI_MODE:$(tari_address_type …)"with anoff:*arm. Line-neutral, and it is the right behaviour beyond the empty case: on a machine that turns Tari off with a stale address still in its config, an address nothing uses can no longer block an apply.The run that caught it was reported to me as "exit code 0" by a piped command, and an earlier claim of PROVEN was written from that. It was
3406 passed, 7 failed. At this head it is 3413 passed, 0 failed.Carved out and filed: #1903
build/p2pool/entrypoint.shdoes not readTARI_MODE, so on anoffmachine P2Pool is still passed--merge-mineat a node that is not running. That file is not this lane's path. The two halves are order-independent by construction — this PR rendersTARI_MODE, so an unset variable is notoffand argv is unchanged until #1903 lands. #1903 carries the two non-obvious checks: the Tor bridging leg keys off_prev = "--merge-mine", and_redact_argvmakes a positional assumption a shortened argv breaks.Four prose sites said otherwise and are corrected here.
docker-compose.yml,33-render-env.sh's comment,docs/architecture.mdanddocs/configuration.mdeach described the entrypoint dropping the triple as though it already did. They now say the variable is delivered and the argv change is #1903. Same defect class #1880 was returned for, found here by sweeping this branch's own added lines rather than by being told.Shared files, disclosed
docker-compose.yml—TARI_MODEon the p2pool and dashboard services. Serialized file;.lane-overrideused at commit and consumed.pithead— the built artifact, regenerated withscripts/build-pithead.sh. Whichever of this and feat(os): #1318 #1838 the boot menu names what it boots, and "Set up again" opens the wizard beside the saved role #1844 merges second regenerates it;appliancecleared the regen (its branches touch slices 08/12/12a, not 27/28/33).docs/— in_shared.paths.Not folded into
P2POOL_FLAGS, on purpose: that key is inCONTROL_DASHBOARD_EDITABLE_KEYS(42-control-policy-and-host-checks.sh:61), free-commit and not confirm-gated, so routing the Tari triple through it would move the Tari payout address out of the host-only perimeter into a field a dashboard operator can clear.Budgets
Both slices are line-neutral against their ceilings:
28-parse-and-validate-config.sh485/485,33-render-env.sh494/494. Theoffbranch cost nothing because it restructures the existing if/else into if/elif rather than adding an arm, and the one new.envline was paid out of a comment in the same block. No new slice cut, no budget row added.Tier and proof
Tier 1, new domain
tests/stack/test-tari-mode-off.sh, registered inrun.sh.make test-stack— 3413 passed, 0 failed, rc 0, read from the log file rather than from a notification. Zero✗marks in the whole run.make test-compose,lint-yaml,lint-md,lint-docs-voice,lint-operator-strings,lint-file-budget,lint-pithead-parity,lint-topology— all rc 0.bash -non both slices and the test file.make lint-sh(shellcheck is serialized behind theappliancelane's live build-machine claim), no docker build, no KVM, no browser.One PASS label interpolated a loop variable, which #1740 bars — a label carrying a value is indistinguishable from a measured one. Unrolled to three literal labels, which also shrank the file by a line.
Over-engineering pass (run before opening this PR)
Measured on the diff: zero new functions, helpers or declarations in
lib/(git diff origin/develop...HEAD -- lib/matchingfunction|local -a|declare: 0), one new file, and both slices line-neutral. Three judgement calls a reviewer should feel free to overturn:"$TARI_MODE:$(tari_address_type …)"is cleverer than anifwrapper, and I would normally write theif. It costs 2 lines and28-parse-and-validate-config.shis at 485/485 with zero headroom, so theifwould have needed a budget row or a slice cut for a two-line guard. The compound subject is a pattern the file already uses formonero_address_typeone block above. If a reviewer prefers theif, say so and I will pay for it out of a comment.test-monero-tari.sh. The honest cost: registering a domain spent 2 ofrun.sh's zero remaining lines (paid from two decorative dividers in my own region). Extending the existing Monero/Tari domain would have cost none. I chose the new file because Tari merge-mining is opt-in for 2.0: default off, a wizard switch, no Tari surfaces when off #1855's behaviour is a mode that turns Tari OFF, which is not what that domain is about, and because a future reader looking for "what happens when merge-mining is declined" should find one file. Reasonable people can disagree; it is onegit mvaway.$TARI_MODEdirectly (!= off,== local,== remote). A helper would read better and would be one more thing to keep in sync across a build artifact; at three sites in two slices it is not worth it yet. If the dashboard and wizard halves add more, that is the moment.Nothing in this diff introduces a config key, a flag, a fallback path or an interface that is not the one the operator ruling named.
🤖 Generated with Claude Code
https://claude.ai/code/session_01KZuzdtxNX8ae4rnDtBqEqR