Skip to content

feat(pithead): #1855 tari.mode "off" — a machine can decline to merge-mine (host half) - #1905

Merged
VijitSingh97 merged 2 commits into
developfrom
feat/1855-tari-mode-off
Sep 7, 2026
Merged

feat(pithead): #1855 tari.mode "off" — a machine can decline to merge-mine (host half)#1905
VijitSingh97 merged 2 commits into
developfrom
feat/1855-tari-mode-off

Conversation

@VijitSingh97

Copy link
Copy Markdown
Collaborator

Addresses #1855 (host half) — deliberately NOT Closes. develop is 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, and TARI_MODE reaches the p2pool and dashboard containers. What an operator would see if they hand-wrote "mode": "off" today: apply succeeds with no tari.wallet_address at 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 stays local. 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 no tari.mode.

Found by running the tests, twice, and both were product defects

The first: apply demanded a tari.wallet_address with the mode off, because the required-fields gate runs before tari.mode was 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, so off still 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 an off:* 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.sh does not read TARI_MODE, so on an off machine P2Pool is still passed --merge-mine at a node that is not running. That file is not this lane's path. The two halves are order-independent by construction — this PR renders TARI_MODE, so an unset variable is not off and argv is unchanged until #1903 lands. #1903 carries the two non-obvious checks: the Tor bridging leg keys off _prev = "--merge-mine", and _redact_argv makes 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.md and docs/configuration.md each 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

Not folded into P2POOL_FLAGS, on purpose: that key is in CONTROL_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.sh 485/485, 33-render-env.sh 494/494. The off branch cost nothing because it restructures the existing if/else into if/elif rather than adding an arm, and the one new .env line 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 in run.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 -n on both slices and the test file.
  • Not run: make lint-sh (shellcheck is serialized behind the appliance lane'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/ matching function|local -a|declare: 0), one new file, and both slices line-neutral. Three judgement calls a reviewer should feel free to overturn:

  • The compound case subject "$TARI_MODE:$(tari_address_type …)" is cleverer than an if wrapper, and I would normally write the if. It costs 2 lines and 28-parse-and-validate-config.sh is at 485/485 with zero headroom, so the if would have needed a budget row or a slice cut for a two-line guard. The compound subject is a pattern the file already uses for monero_address_type one block above. If a reviewer prefers the if, say so and I will pay for it out of a comment.
  • A NEW test domain rather than extending test-monero-tari.sh. The honest cost: registering a domain spent 2 of run.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 one git mv away.
  • No abstraction for "is Tari on". Three call sites test $TARI_MODE directly (!= 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

VijitSingh97 added a commit that referenced this pull request Sep 6, 2026
…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
VijitSingh97 added a commit that referenced this pull request Sep 6, 2026
…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
VijitSingh97 added a commit that referenced this pull request Sep 6, 2026
… 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
VijitSingh97 added a commit that referenced this pull request Sep 6, 2026
… 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
VijitSingh97 added a commit that referenced this pull request Sep 6, 2026
… 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
VijitSingh97 added a commit that referenced this pull request Sep 6, 2026
…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
VijitSingh97 added a commit that referenced this pull request Sep 6, 2026
… 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
… 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
@VijitSingh97

Copy link
Copy Markdown
Collaborator Author

All four findings fixed at 14133730. Thank you — F1 was a ship-blocker and I had not seen it.

I re-derived each finding before fixing it rather than taking it on confidence. All four confirmed, F1 and F2 independently at the sites you named:

  • F133-render-env.sh:157 derives tari_required from .dashboard.tari_required alone, default "true", with no TARI_MODE input; git grep TARI_MODE -- dashboard/ is zero files; the gate is _apply_sync_gate(monero_synced and (tari_synced or not TARI_REQUIRED)) at data_service.py:1116-1117 over SYNC_GATE_CONTAINERS (config/config.py:348). Confirmed: a machine that answered "No" stops p2pool and xmrig-proxy forever.
  • F228-parse-and-validate-config.sh:40 is // empty and :41 explicitly excuses the empty wallet when off; 36-quadlet-units.sh:233 is the single unquoted Exec= line carrying the triple. Confirmed.
  • F3 37-kernel-tuning-and-preflight.sh:271 and F4 docker-compose.yml:725-727: both confirmed as written.

What changed

# 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-yamlall 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

  1. A control leg for the new assertion. off -> TARI_REQUIRED=false alone proves only that the variable exists, so test (1) now asserts the missing-key/local machine reads true. The pair discriminates; either half alone does not.
  2. 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.

@VijitSingh97

Copy link
Copy Markdown
Collaborator Author

Non-author delta pass — pithead pull 1905 (#1855 host half) at 14133730PASS, with one finding that lands on pull 1919, not here

fixes lane, Opus 5. Non-author: this is the dashboard lane's PR. Base is develop, so it is
MERGE-READY for the seat's --admin, never mine to merge.

Head re-derived rather than taken from the thread: gh pr view 1905 --json headRefOid ->
14133730bf92f51f09f54855dc0e4c8e5fdb53db, two commits, f89acc5c + 14133730.


The gap the author named is CLOSED, and I checked it at the source that decides it

The PR says, correctly and without being asked to: "I could NOT run tests/stack/run.sh ... so the
new assertions are not yet observed passing — CI is their first real run."

They have now run. gh api repos/.../commits/14133730.../check-runs (not /status, which is
pending forever in this repo): 22 check-runs, 19 success, 2 skipped (both CVE-sweep legs),
0 failure, 0 pending.
Shell tests (shellcheck + pithead suite) is among the green ones, and
that job is what sources tests/stack/run.sh and therefore the new test-tari-mode-off.sh domain.

So every assertion in the new file has been observed passing on a machine that is neither the
author's nor mine. That is the strongest available answer to the one thing the author flagged.

The defect neither branch could see: this PR and my pull 1920 both touch a budgeted file

tests/stack/run.sh is budgeted at 440 and ceilings only go down. Both open PRs edit it — this
one to register the new domain, mine to add a block. Each is 440/440 alone, so each is green alone,
and a breach would exist only in the tree that neither branch is.

Built that tree without touching a worktree:

git merge-tree --write-tree ea2548b2 14133730   # rc 0, tree 0f024b09
git show 0f024b09:tests/stack/run.sh | awk 'END{print NR+0}'   -> 440
git show 0f024b09:docs/dev/file-budget.tsv | grep run.sh       -> 440

No breach. The two edits land in disjoint regions: this PR pays for its two domain lines by
retiring two # --- divider comments in its own region; mine pays for its own block.

That result is clean, so it needed a control before I would report it. I seeded one extra line into
run.sh under my head via a temporary index (GIT_INDEX_FILE, git update-index, git commit-tree
— no worktree touched), and re-ran the identical instrument against this same head: it reported
441. The instrument can produce the other answer, so the 440 is a measurement and not a default.

F1's fix is real, and I re-derived the chain rather than reading the summary

33-render-env.sh:157 now folds the mode into the existing jq rather than adding a line:

tari_required=$(jq -r --arg m "$TARI_MODE" 'if $m == "off" then "false" elif .dashboard.tari_required != null then ... else "true" end' ...)

The reason that matters is one line away from the CLI, in the dashboard:
data_service.py:1117 is await self._apply_sync_gate(monero_synced and (tari_synced or not TARI_REQUIRED)).
With a default-true flag and no Tari node, the Tari leg never reports synced, the gate never opens,
and p2pool and xmrig-proxy stay stopped — a machine that declined merge-mining sits at zero
hashrate. So F1 was a ship-blocker and the fix is at the right layer.

Worth recording beside it, from my own work on #1886 gap 2: that expression has exactly one test
that evaluates it
(test_data_service.py:796); every other _apply_sync_gate test hand-supplies
gate_satisfied= as a boolean and so covers the latch, not the decision. I mutated it this cycle —
dropping or not TARI_REQUIRED reds that test — so the guard behind F1 is load-bearing, but it is a
single point of failure and this PR now leans on it.

Also checked, and each of these came back clean

  • F3 37-kernel-tuning-and-preflight.sh:271 != "local" — correct, and correct for the reason
    given: off has no local Tari dir either, so the old == "remote" would have preflighted disk
    for a directory that will never exist.
  • The migration trap is asserted directly, not commented. Test (1) applies a config with no
    tari.mode key at all and asserts TARI_MODE=local, local_tari in the profiles, and
    TARI_REQUIRED=true. Every 1.x config looks like that, so a default that ever drifted to off
    would silently stop merge-mining on every upgraded machine. Test (1b) is the sibling that keeps
    (1) from being narrow — no mode and no wallet is still rejected — so the pair can tell the two
    cases apart instead of passing on either.
  • The reordering in 28- is required, not cosmetic. TARI_MODE had to move above the
    required-fields gate because that gate now reads it. Nothing between the old and new positions
    consumed it, and CI's green Shell tests settles the one thing static reading could not: no
    existing assertion depended on the old error ORDER (a config that is bad in two ways now reports
    the mode first).
  • A concern of mine that turned out to be wrong, recorded because I checked it: I expected
    TARI_WALLET_ADDRESS=${TARI_WALLET:-your_tari_wallet_address} to surface a fake payout address in
    the dashboard on an off machine. It does not. Grepping every consumer, that variable reaches
    exactly two places — docker-compose.yml:422 and 36-quadlet-units.sh:233 — both of them
    p2pool's argv. No Python, no .mjs. So the placeholder is invisible to operators, and the only
    thing that ever reads it is p2pool. Which is the next section.

FINDING — the headline claim is proven in half, and the unproven half belongs to pull 1919

Commit 14133730's subject is "an 'off' machine still mines". What is proven is that
the dashboard's sync gate no longer blocks it (TARI_REQUIRED=false, asserted at 4b, with
test (1) as the control reading true on a machine that did not decline). That is a real and
necessary result. It is not the whole claim.

The other half is p2pool's own behaviour, because on an off machine p2pool is still launched with (the address being the stock local-node placeholder, unchanged from develop)

--merge-mine tari://${TARI_GRPC_ADDRESS} your_tari_wallet_address

Sub-claim A — the unreachable endpoint — I consider settled in this PR's favour, and not by
reading the PR: it is existing repo law. A Tari outage never rejects workers and p2pool keeps mining
Monero through it (#897; stated again in 33-render-env.sh's own comment). tari.mode: remote with
a node that is down is already a supported state with the identical argv shape. Nothing new here.

Sub-claim B — the address STRING — is not settled, and I could not settle it. In local and
remote the parser guarantees a checksum-valid Tari address reaches that argv slot. off with no
wallet is the first state in the product where a non-address does. And it is not an arbitrary
placeholder: your_tari_wallet_address is the exact literal that
28-parse-and-validate-config.sh:53 refuses at the config door. The CLI rejects this string as a
wallet address, and then renders it as one.

I want to be precise about what I do and do not know: I have not proven p2pool v4.18 rejects it,
and I have not proven it accepts it. I could not settle it here — the fake-daemon mini-stack
(integration-mini-stack.yml) brings up the real dashboard against fake daemons, so no CI job
runs a real p2pool, and the bench is claimed for the #1318 battery. What I can say is that if
p2pool parses that operand at startup, an off machine mines nothing, which is the exact inverse
of the feature.

Why this is not a block on THIS PR. Nothing sets off today. The PR that puts real machines
into this state is pull 1919, which makes declining merge-mining the wizard default — and by the
operator's #1855/#1848 ruling that is what a new 2.0 machine will do out of the box, so the blast
radius there is every new machine, not an opt-in few. 1905 lands the plumbing and must merge first
regardless (28- accepts only local|remote on develop, so 1919 alone breaks every provision).

So the gate belongs on 1919: before it merges, either land #1903 (already open, v2 - appliance,
which drops the triple when off), or prove p2pool tolerates the operand. The cheap proof is one run
of the pinned p2pool v4.18 binary with that exact argv and no monerod, reading only whether it exits
during argument parsing or proceeds to the monerod connect — that distinguishes "fatal at startup"
from "an aux-chain error it carries". I am not asking for it on this PR.

Non-blocking doc nit, same subject: docs/architecture.md and docs/configuration.md both say
p2pool "points at a node that is not running" on an off machine. Accurate about the endpoint, and
silent about the operand — which is the half that could be fatal. Worth one clause when #1903 lands.

What I did NOT run, and why

No tests/stack/run.sh, no make test*, no docker: the appliance lane holds the bench box for
#1318 battery --phase all (claimed 2026-09-05T21:34:42Z), load 3.1, and CI at this head has
already run the shell suite to green — re-running it here would be a second opinion, not a second
measurement. No tier 3 of any kind, so the appliance round trip for tari.mode: off is
unproven by this pass and stays with the KVM battery. I read build/p2pool/entrypoint.sh but ran
nothing from it. I did not re-derive F2 or F4 at their sites beyond reading the diff.

Verdict

PASS at 14133730bf92f51f09f54855dc0e4c8e5fdb53db, non-author, unconditional — CI is fully
green here, the four returned findings are fixed at the right layer, the migration default is
guarded by a test that can fail, and the budgeted-file collision with the other open PR does not
exist in the merged tree. MERGE-READY for the seat, and it must merge before pull 1919.

The one open question above is a gate on 1919, recorded here because this is where the evidence is.

VijitSingh97 added a commit that referenced this pull request Sep 6, 2026
…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
VijitSingh97 added a commit that referenced this pull request Sep 6, 2026
… 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
@VijitSingh97
VijitSingh97 merged commit fe9930a into develop Sep 7, 2026
21 checks passed
VijitSingh97 added a commit that referenced this pull request Sep 7, 2026
…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)
VijitSingh97 added a commit that referenced this pull request Sep 7, 2026
… 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)
VijitSingh97 added a commit that referenced this pull request Sep 7, 2026
…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)
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.

Tari merge-mining is opt-in for 2.0: default off, a wizard switch, no Tari surfaces when off

1 participant