Sync the pit's settings automatically, carry all of them, and undo the trust anchor - #449
Merged
Conversation
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
ThreatCrush Security Scan3 finding(s) in the 10 file(s) this pull request changes. HIGH/CRITICAL: 1 | MEDIUM: 1 | LOW: 1
79 pre-existing finding(s) elsewhere in the repository — **HIGH/CRITICAL**: 4 | **MEDIUM**: 66 | **LOW**: 9Not introduced by this pull request. The full set is in the Security tab.
…and 59 more. Full results in the Security tab. Snippets are redacted; ThreatCrush never prints matched credential material. |
`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
| 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" })); |
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
/loadthen/saveevery 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 —
/loaddeclines when a settings file changed locally since the last sync (R4),/savedeclines 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
/loaddeclines over local edits, the/savebehind 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 viabackToPit, so a stray timer would be joined by another); holds no reference torl(the loop rebuilds readline around a dozen commands);unref()ed, so a piped/quitexits now.2. Carry the rest of the settings, and enforce exclusions that were only comments
/savecarried four files. Herd's notification preferences,pricing.json, the DNS filter policy, andbusiness.jsonall started from nothing on a new machine.herd/config.jsonwas the clearest miss — it sat next torules.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.jsonis a work ledger that loses entries under last-write-wins. All now named.Two exclusions did not actually work.
NEVER_SYNCEDis 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/*.pidare excluded; that was true of the intent and enforced by nothing, with the allowlist quietly doing the work. It is a rule now, with.transcripton it.business.jsonsorts 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 disabledns enableinstalls 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 onedisableleft behind — a machine that randns enableonce kept trusting a locally generated root forever, including afterdisablereported 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.untrustPlanasks 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-trustis the symmetry with enable's--no-trust.Verification
autosync,untrustandsettings-sync-allowlistecho /quit | moshcodestarts and exits immediately (the timer does not hold it open)🤖 Generated with Claude Code
https://claude.ai/code/session_01XGe9mWC6FvUjT6p1HCVjba