Skip to content

Sync the pit's settings automatically, carry all of them, and undo the trust anchor - #449

Merged
ralyodio merged 3 commits into
mainfrom
worktree-autosync
Aug 29, 2026
Merged

Sync the pit's settings automatically, carry all of them, and undo the trust anchor#449
ralyodio merged 3 commits into
mainfrom
worktree-autosync

Conversation

@ralyodio

@ralyodio ralyodio commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Three commits, all in the "moshcode changes something on your machine and should be able to put it back" family.


1. Sync on its own, every five minutes

An open pit now runs /load then /save every five minutes, on by default.

PRD 0010 ruled background sync out, and it was right about the daemon it imagined: "a daemon that pushes silently is a daemon that overwrites silently." This one is allowed because it is never permitted to force. Both refusals the feature already had do all the work — /load declines when a settings file changed locally since the last sync (R4), /save declines on the 409 when another machine saved first (R3) — so the worst an unattended tick can do is stop and leave the decision with the person at the prompt. The non-goal is narrowed rather than dropped; R10 records what replaced it.

Load-then-save is the design, not an ordering accident. Loading first puts this machine at the account's revision before it pushes, so the ordinary two-machine case never becomes a conflict anyone has to resolve. When /load declines over local edits, the /save behind it pushes exactly those edits — what the manual conflict message already tells you to do.

Quiet on purpose: silent when nothing changed, when logged out, and when the network is down. Four things speak — settings that arrived from another machine, a revision pushed, a conflict, a rejected credential. Answers are read via --json, not by matching on English.

Lifecycle details the pit forced: stopped in the teardown (tui() is re-entered via backToPit, so a stray timer would be joined by another); holds no reference to rl (the loop rebuilds readline around a dozen commands); unref()ed, so a piped /quit exits now.

MOSHCODE_NO_AUTOSYNC=1 moshcode      # off
MOSHCODE_AUTOSYNC_MS=900000 moshcode # every fifteen minutes

2. Carry the rest of the settings, and enforce exclusions that were only comments

/save carried four files. Herd's notification preferences, pricing.json, the DNS filter policy, and business.json all started from nothing on a new machine. herd/config.json was the clearest miss — it sat next to rules.json, which has synced since the first version, so "my herd settings came across" was true of half of them.

Choosing what to add meant walking everything moshcode writes, and most of it must never move: a blocked-domain list is browsing history, a task ledger carries prompt text, a transcript is a full screen capture, and timers.json is a work ledger that loses entries under last-write-wins. All now named.

Two exclusions did not actually work. NEVER_SYNCED is an exact-string match, so naming a directory there was inert — hence a separate prefix list. And the header has claimed since the first version that *.sock/*.pid are excluded; that was true of the intent and enforced by nothing, with the allowlist quietly doing the work. It is a rule now, with .transcript on it.

business.json sorts last: the 256 KiB total is spent in allowlist order, so the file most likely to outgrow it must not push the small ones out.

3. Take the local root back out on dns disable

dns enable installs a CA as a system trust anchor and nothing ever removed it. The restore point it writes is the right shape for resolver config and the wrong shape for a trust store, so the one change with security consequences was the one disable left behind — a machine that ran dns enable once kept trusting a locally generated root forever, including after disable reported it was as it was.

Removal is deliberately not trustPlan(...).steps.reverse(). Installing is gated on the root being safe to install; none of that bears on taking it out, and a root that should never have been trusted is the one you most need to withdraw. untrustPlan asks two questions — can the store be reached, does the undo need the file — and never refuses.

The three stores do not undo alike. NSS forgets by nickname and the Debian store by filename, so both still work after the root is deleted (the ordinary case). The macOS keychain can only be told with the certificate in hand, and says so rather than leaving an anchor behind and reporting success.

--keep-trust is the symmetry with enable's --no-trust.


Verification

  • 37 new tests across autosync, untrust and settings-sync-allowlist
  • Full suite: 2356 pass, 0 fail, 332 skipped
  • echo /quit | moshcode starts and exits immediately (the timer does not hold it open)

🤖 Generated with Claude Code

https://claude.ai/code/session_01XGe9mWC6FvUjT6p1HCVjba

The aliases you built on the desktop should be on the laptop by the time you
sit down at it, and `/save` only got typed by people who remembered it existed.
An open pit now runs `/load` then `/save` every five minutes.

PRD 0010 ruled background sync out, and it was right about the daemon it had in
mind: "a daemon that pushes silently is a daemon that overwrites silently."
This one is allowed because it is never permitted to force. Both refusals the
feature already had do the work — `/load` declines when a settings file changed
locally since the last sync, `/save` declines on the 409 when another machine
saved first — so the worst an unattended tick can do is stop and leave the
decision with the person at the prompt. The non-goal is narrowed rather than
dropped, and R10 records what replaced it.

Loading before saving is the design, not an ordering accident. It puts this
machine at the account's revision before it pushes, so the ordinary
two-machine case never becomes a conflict anyone has to resolve; and when
`/load` declines over local edits, the `/save` behind it pushes exactly those
edits, which is what the manual conflict message tells you to do anyway.

Quiet on purpose. Nothing is printed when nothing changed, when there is no
token, or when the network is down — a line every five minutes saying "still
fine" trains you to stop reading the pit. Four things speak: settings that
arrived from another machine, a revision this machine pushed, a conflict, and a
credential the app rejected. Answers are read via `--json` rather than by
matching on English.

Three lifecycle details the pit forced: the timer is stopped in the teardown
because `tui()` is re-entered after an engine session and would otherwise
collect one timer per entry; it holds no reference to `rl`, which the loop
closes and rebuilds around a dozen commands; and it is unref'd, so a piped
`/quit` exits now rather than in five minutes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XGe9mWC6FvUjT6p1HCVjba
@github-actions

github-actions Bot commented Aug 29, 2026

Copy link
Copy Markdown

ThreatCrush Security Scan

3 finding(s) in the 10 file(s) this pull request changes.

HIGH/CRITICAL: 1 | MEDIUM: 1 | LOW: 1

Severity Rule Location
HIGH tls-verification-disabled src/dns.mjs:766
MEDIUM js-timing-unsafe-mac-compare src/settings-sync.mjs:397
LOW secret-generic-credential test/settings-sync-allowlist.test.mjs:120
79 pre-existing finding(s) elsewhere in the repository — **HIGH/CRITICAL**: 4 | **MEDIUM**: 66 | **LOW**: 9

Not introduced by this pull request. The full set is in the Security tab.

Severity Rule Location
HIGH js-ssrf-outbound-request apps/pwa/public/sw.js:45
HIGH tls-verification-disabled apps/pwa/src/lib/moshpit-gateway.mjs:299
HIGH sh-remote-script-execution install.sh:79
HIGH sh-remote-script-execution install.sh:83
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:138
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:152
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:178
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:365
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:369
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:414
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:663
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:859
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:861
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:920
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:966
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:1036
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:1139
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:1162
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:1184
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:1383

…and 59 more. Full results in the Security tab.

Snippets are redacted; ThreatCrush never prints matched credential material.

ralyodio and others added 2 commits August 29, 2026 16:39
`dns enable` installs a CA as a system trust anchor, and nothing ever removed
it. The restore point it writes is the right shape for resolver config — files
in /etc, captured and replayed — and the wrong shape for a trust store, so the
one change with security consequences was the one `disable` left behind. A
machine that ran `dns enable` once kept trusting a locally generated root
forever, including after `dns disable` reported the machine was as it was.

Removal is deliberately not `trustPlan(...).steps.reverse()`. Installing is
gated on the root being safe to install — name constraints, a certificate that
parses — and none of that has any bearing on taking it back out: a root that
should never have been trusted is the one you most need to be able to withdraw.
So untrustPlan asks two questions only, whether the store can be reached and
whether the undo needs the certificate file, and never refuses.

The three stores do not undo alike. NSS forgets by nickname and the Debian
store by filename, so both still work after the root itself is deleted, which
is the ordinary case — someone who wants rid of this removes the certificate
first and asks questions after. The macOS keychain can only be told with the
certificate in hand, and that is the case that has to say so out loud rather
than leave an anchor behind and report success.

Two smaller things the shape forced. `update-ca-certificates --fresh` rather
than a bare refresh: the bare form adds what is new, and it is the rebuild that
drops the symlink for a source file that is no longer there. And certutil's
complaint that the nickname is not in the database describes the state we were
trying to reach, so it reads as "was not there" rather than FAIL — running
`dns disable` twice is ordinary.

`--keep-trust` is the symmetry with enable's `--no-trust`, for turning
resolution off for an afternoon without paying to install the root again.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XGe9mWC6FvUjT6p1HCVjba
…only comments

`/save` carried four files. Everything else moshcode lets you configure — herd's
notification preferences, the per-model prices you typed into `pricing.json` by
hand, the DNS filter's categories and your own allow and block lists, and the
clients, teams, rates and invoices in `business.json` — started from nothing on
a new machine. `herd/config.json` was the clearest miss: it sat next to
`rules.json`, which has been synced since the first version, so "my herd
settings came across" was true of half of them.

Picking what to add meant walking everything moshcode writes, and most of it
must never move. A blocked-domain list is browsing history. A task ledger
carries prompt text and output. A transcript is a full screen capture of a
session. `timers.json` is a work ledger, and two machines appending hours under
a last-write-wins sync is how you lose an afternoon's entries. All of those are
now named.

Two of the exclusions did not work. `NEVER_SYNCED` is an exact-string match, so
naming a directory in it would have been inert — the entries under it would not
match and would fall through — which is why the subtree rules are a separate
prefix list. And the header comment has claimed since the first version that
`*.sock` and `*.pid` are excluded "because they describe processes on exactly
one box"; that was true of the intent and enforced by nothing, with the
allowlist quietly doing the work. It is a rule now, and `.transcript` is on it.

`business.json` goes last on purpose. The 256 KiB total is spent in allowlist
order, so the file most likely to grow past it has to sort after the small ones
— otherwise a year of invoices silently pushes your aliases out of the snapshot
instead of being skipped itself.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XGe9mWC6FvUjT6p1HCVjba
@ralyodio ralyodio changed the title Sync the pit's settings on its own, every five minutes Sync the pit's settings automatically, carry all of them, and undo the trust anchor Aug 29, 2026
fs.writeFileSync(path.join(moshcode, "business.json"), JSON.stringify({ clients: [{ name: "acme" }] }));

// The two that must not come along, sitting right next to ones that do.
fs.writeFileSync(path.join(moshcode, "credentials.json"), JSON.stringify({ token: "mck_secret" }));
@ralyodio
ralyodio merged commit a29a971 into main Aug 29, 2026
6 checks passed
@ralyodio ralyodio mentioned this pull request Aug 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants