Skip to content

fix(install): the three #288 review follow-ups — #289, #290, #291 - #295

Open
CrazyWillBear wants to merge 5 commits into
mainfrom
fix/review-fixes-289-291
Open

fix(install): the three #288 review follow-ups — #289, #290, #291#295
CrazyWillBear wants to merge 5 commits into
mainfrom
fix/review-fixes-289-291

Conversation

@CrazyWillBear

Copy link
Copy Markdown
Collaborator

Closes #289, closes #290, closes #291 — the three review-fix follow-ups from the /my-review of PR #288, one commit each.

Context: PRD #286's migration is abandoned (Apple refused a second Apple ID), so the dedicated path ships unvalidated. That reframes these three — #291 is the one with live-user reach, firing on every interactive chief uninstall including single-user installs.

#291 — uninstall offered to delete an account it never created

_account_wanted was reached on every interactive uninstall. Answering yes ran sudo userdel --remove chief against whatever pre-existing account happened to carry that name, taking its home with it. Separately, execute(step)'s return code was discarded, so "system account chief removed." printed even when both steps failed.

Gated the whole branch on posture.chief_account(repo_dir / ACCOUNT_REPORT) — the question is only asked when this install's own report names an account that resolves to a real passwd entry. --remove-account on a single-user box is now a no-op. The account name comes from the report rather than a CLI default no caller ever passed, so the now-dead user parameter is gone.

#289grant_reason was weaker than the guarantee three docs make for it

It rejected only the literal home path, / and relative paths, so ~/.., /home and /Users all passed — chgrp -R chief /Users two keystrokes away. Now judged on the resolved path: refuse /, anything the home is inside, and any non-directory (which also catches the typo that previously aborted _run after the account and tree steps had landed).

Moved to dedicated_ask.py, its only caller — account.py goes 195 → 185, off the 200-line cap it was one edit from. Account name gated on ^[a-z_][a-z0-9_-]{0,31}$ before it reaches argv and Path("/Users") / user.

#290 — dual-store double-delivery of group chats

The two stores are polled independently and the twin dedup was off wholesale in dedicated mode, so a group both accounts are in held every message twice: two stranger rows, two monitor evaluations, two classifier calls, two wakes.

Now records every message in the window regardless — so whichever store reads it first primes the key — and suppresses only where a repeat is an artefact. Non-dedicated behavior is bit-identical.

Known trade: the key is (thread_key, sender, text) over 5s, so two genuinely identical group messages seconds apart collapse on the owner's-store path. That path is monitor-only; noted in a comment.

Not done

/etc is still grantable. #289 listed it, but the documented guarantee is about the home root, and /etc is an existing directory the owner typed deliberately. A system-path denylist is a different promise than the docs make — say so if you want it.

Validation

uv run pytest 790 passed, ruff check . and mypy . clean.

Six new tests, all failing before the fix. #290 and #291 are exercised against real fakes; #289's grant_reason is a pure function and fully unit-tested, but the chgrp it gates has still never run on real hardware — same caveat as the rest of the dedicated path.

Docs updated in the same change: docs/OPERATIONS.md (uninstall's account question) and docs/SECURITY.md (the grant claim now states what is actually enforced).

…reate

`_account_wanted` was reached on every interactive `chief uninstall`,
including single-user installs where no dedicated account exists. Answering
yes ran `sudo userdel --remove chief` against whatever pre-existing account
happened to carry that name, taking its home with it. And `execute(step)`'s
return code was discarded, so "system account chief removed." printed even
when both steps failed.

Gate the whole branch on `posture.chief_account(repo_dir / ACCOUNT_REPORT)`,
which resolves the installer's own report to a real passwd entry — so the
account name comes from the report rather than a CLI default that no caller
ever passes (the now-dead `user` parameter is gone). Report failure honestly.

Closes #291
`grant_reason` rejected only the literal home path, `/` and relative paths, so
`~/..`, `/home` and `/Users` all passed — defeating the "home directory root
is never offered" claim in docs/SECURITY.md, docs/OPERATIONS.md and PRD #286,
and putting a recursive `chgrp -R chief /Users` two keystrokes away. It also
took paths that do not exist, whose `chgrp` then aborts `_run` *after* the
account and tree steps have landed.

Judge the resolved path: refuse `/`, anything the home is inside, and any
non-directory. Moved to dedicated_ask.py, its only caller, which keeps
account.py off the 200-line cap it was one edit away from.

The account name is now gated on `^[a-z_][a-z0-9_-]{0,31}$` before it reaches
argv and `Path("/Users") / user` — `default_home("darwin", "/etc")` returns
`/etc`, i.e. `useradd --home-dir /etc`.

tests/test_install.py also carries the uninstall tests for #291, committed in
1c75a13 — the two sets interleave in one file.

Closes #289
Dedicated mode switched the twin dedup off wholesale. That is right for
chief's own store — two identical texts seconds apart there are genuinely two
messages, not one row recorded twice — but the two stores are polled
independently, so a group chat both accounts are in holds every message twice
under different rowids: two stranger rows, two bus events, two monitor
evaluations, two classifier calls, two wakes.

Record every message in the dedup window regardless, so whichever store reads
it first primes the key, and suppress only where a repeat is an artefact. The
key is `(thread_key, sender, text)` over 5s, so two genuinely identical group
messages seconds apart do collapse on the owner's-store path — that path is
monitor-only, which is the trade this takes.

Closes #290
…dedup race

The high finding was a regression this branch introduced: `--purge-data`
rmtree's `data/`, and the new account gate then read `data/account-setup` out
of the directory it had just deleted. `chief uninstall --purge-data
--remove-account --yes` kept the account, its home and chief's whole message
store, and reported that no account was ever created — where main removed it.
The report is now read before the purge.

- `grant_reason` refuses anything not strictly inside the owner's home. That
  is the question the wizard actually asks ("which of *your* directories"),
  and it subsumes every earlier clause: `/`, the home root, `~/..`, and the
  system roots. `chmod -R g+rwX /etc` hands chief group-write on `sudoers`,
  which is root by another name and is ruled out by the no-escalation promise
  in docs/SECURITY.md.
- The dedup only held when chief's store was polled first. The two stores keep
  independent cursors, so the owner's copy can land a tick ahead, and the
  window cannot break the tie — it is measured on the row's own date, which is
  identical in both. `RecentDedup.see` now records which store logged a
  sighting and returns it, so a cross-store repeat is suppressed either way
  round and only a `mine`→`mine` repeat stays exempt.
- `install.sh` no longer `rm -f`s the account report before the offer. A
  `--single-user` re-run, a non-interactive re-run, or a declined offer wiped
  it on a box that does have an account, turning `--remove-account` into a
  silent no-op forever after. It is written aside and moved into place only on
  a create/adopt answer.
- Uninstall stops at the first failed step and returns 1: `groupdel --force`
  after a failed `userdel` deletes the group out from under an account that is
  still there, and exit 0 read as success to anything scripted.
- The uninstall test fixtures wrote the developer's own username into a record
  that drives `sudo userdel --remove`. Inert only because every call site
  passes a recording executor — the default is a live `subprocess.run`. Now
  `nobody`.
- OPERATIONS.md no longer claims the gate means "created by this install":
  `chief_account` accepts `mode=existing` too, so an adopted account is
  removable the same way. Documented rather than gated, per owner's call.
- Two comments corrected: the account-name check guards an *absolute* name
  swallowing the `Path("/Users") / user` join, not `..`; and the dedup comment
  now states the collapse cost #290 asked it to state.

Four new regression tests, each failing before its fix.
@CrazyWillBear

Copy link
Copy Markdown
Collaborator Author

Review findings addressed — 71d15da

/my-review (opus, xhigh) came back CHANGES REQUESTED: 1 high, 5 medium, 3 low. All nine are handled. Done-check green: 793 passed, ruff + mypy + shellcheck clean.

The high was a regression this branch introduced, and the reviewer was right that no test covered the combination:

Sev Finding Resolution
high --purge-data rmtree's data/, then the new gate reads data/account-setup out of it — so --purge-data --remove-account --yes kept the account and reported none ever existed. main removed it. Report read before the purge. Regression test.
medium /etc and other system roots still grantable; chmod -R g+rwX /etc is root via sudoers, contradicting SECURITY.md's no-escalation bullet grant_reason now refuses anything not strictly inside the owner's home — which is the question the wizard actually asks, and subsumes every earlier clause
medium Dedup only held when chief's store polled first; independent cursors mean the owner's copy can land a tick ahead, and the window can't break the tie (measured on the row's own date, identical in both stores) RecentDedup.see records which store logged a sighting and returns it; cross-store repeats suppressed either order, only minemine stays exempt. Regression test for the owner-first order.
medium install.sh:96 unconditional rm -f of the report — any --single-user, non-interactive, or declined re-run stranded a real account, making --remove-account a permanent no-op Written aside via mktemp, moved into place only on a create/adopt answer
medium Test fixtures wrote the developer's real username into a record driving sudo userdel --remove; inert only because every call site passes a recording executor, while the default is a live subprocess.run nobody
medium Gate accepts mode=existing, so an adopted account is still removable — contradicting the new "created by this install" doc claim Doc fixed, behavior kept (owner's call): removal stays explicit opt-in and interactively confirmed, and OPERATIONS.md now says created or adopted, spelling out that --remove-account on an existing install deletes a user predating chief
low groupdel --force ran after a failed userdel, and exit 0 read as success Stops at the first failure, returns 1. Regression test.
low _name docstring named the wrong escape (.., and useradd on a macOS sysadminctl path) Corrected — it guards an absolute name swallowing the Path("/Users") / user join
low Dedup comment stated the opposite side of the trade from what #290 asked for Now states the collapse cost, including the one-to-one cross-store case

Worth recording: the reviewer noted #290's implementation is stronger than the issue text specified — the issue's literal "run the dedup for non-mine rows only" would never have primed the window from chief's store and would have fixed nothing.

Three verifier agents went over 71d15da. The cross-store dedup it added was
only half right: recording `mine` alongside each sighting meant the owner-store
copy overwrote the flag, so the next row from chief's own store lost its
exemption and a genuine second message was dropped where main delivered it.

    dedicated, group both accounts are in, "ok" twice 2s apart:
      chief r1  deliver
      owner r1  DROP   (correct — but rewrites mine -> False)
      chief r2  DROP   <- real message, lost

The key was the problem, not the bookkeeping: (thread_key, sender, text)
cannot tell one message read from two stores apart from two people typing the
same thing. `guid` can, and the store module already knew it — the comment
above DEDUP_WINDOW_NS records that self-DM twins carry *different* guids,
which is exactly what makes the two cases separable:

- twins (same text, different guid) -> the text heuristic, unchanged, and now
  restored to main's semantics verbatim for the non-dedicated path;
- both stores (same message, same guid) -> exact, no heuristic, no false
  collapse, and read skew cannot defeat it because the window is measured on
  the row's own date, which is identical in both copies.

Single-store installs are unaffected: a guid is only ever read once there, so
the new check never matches. If guids should turn out not to match across two
Apple IDs the failure is a double delivery, never a lost message.

Also from the verifiers:

- `mktemp` with no template (install.sh) would likely fail on BSD/macOS — the
  only platform the dedicated path runs on — and no CI job reaches the line,
  since it needs an interactive tty. Now takes a template, and the report is
  chmod'd 0644: mktemp makes it 0600 and the tree chown has already run, so
  chief could not read its own install record.
- `chief account` defaulted `--report` to None and the launcher forwards none,
  so the documented alternative to re-running install.sh (docs/OPERATIONS.md)
  recorded nothing — and after 1c75a13 that is an account chief can never
  remove. Defaulted to data/account-setup.
- `uninstall --purge-data` with the account kept (the default) deletes the
  only record of it, so nothing can remove it afterwards. Says so, with the
  manual command, while it is still useful.
- `--keep-account` had no test on either side of this change.
- docs/SECURITY.md described the old grant rule; the stale `useradd` claim
  survived in a test docstring after being fixed in the code.

796 passed, ruff + mypy + shellcheck clean.
@CrazyWillBear

Copy link
Copy Markdown
Collaborator Author

Verification round — 54e9f54

Three agents (opus on the high, opus on the 5 mediums, sonnet on the 3 lows) adversarially verified 71d15da. Six of nine confirmed fixed; one was half-done, one was a regression I introduced, and they turned up four defects nobody had looked for. All addressed. 796 passed, ruff + mypy + shellcheck clean.

The one that mattered: M2 was fixed wrong

The verifier simulated my dedup against the real RecentDedup:

dedicated, group both accounts are in, someone sends "ok" twice 2s apart:
  chief r1  deliver
  owner r1  DROP    (correct — but rewrites the stored mine flag to False)
  chief r2  DROP    <- genuine second message, LOST. main delivered it.

I had traded a double-delivery for silent message loss, and my own comment claiming "only a minemine repeat stays exempt" was false in exactly the two-store configuration the fix exists for.

The key was wrong, not the bookkeeping. (thread_key, sender, text) cannot separate one message read from two stores from two people typing the same thing — and imessage_store.py already recorded the fact that makes them separable: self-DM twins carry different guids. So:

  • twins (same text, different guid) → the text heuristic, now restored to main's semantics verbatim for the non-dedicated path
  • both stores (same message, same guid) → exact, no heuristic, no false collapse

Read skew can't defeat it either, since the window is measured on the row's own date, which is identical in both copies. Single-store installs never match the new check. And if guids turn out not to match across two Apple IDs, it degrades to a double delivery — never a lost message. New test test_a_genuine_repeat_in_a_shared_group_is_not_swallowed covers what regressed.

Found by the verifiers, not in the original review

Sev Defect Fix
medium mktemp with no template would likely fail on BSD/macOS — the only platform the dedicated path runs on. Needs an interactive tty, so no CI job reaches the line and shellcheck can't see it. Takes a template
medium chief account defaulted --report to None and the launcher forwards none, so the documented alternative to re-running install.sh recorded nothing — and after 1c75a13 that's an account chief can never remove Defaults to data/account-setup
medium uninstall --purge-data with the account kept (the default) destroys the only record of it, stranding it permanently — the same failure class this PR fixed for install.sh Says so, with the manual command
low Report landed 0600 owner-only after the mv; the tree chown had already run, so chief couldn't read its own install record chmod 0644
low --keep-account had no test on either side of this change Added
low SECURITY.md still described the old grant rule; the stale useradd claim survived in a test docstring after being fixed in the code Both corrected

Confirmed genuinely fixed

M1 — verified against a built symlink tree: symlinked parents are caught (.resolve() walks the chain), both sides resolved so a symlinked home still admits its own subdirectories, and macOS firmlinks don't break legitimate grants. M4, M5 (clause-by-clause against the code), L1 (return traced through to the process exit code), L3. The high was verified empirically by running the new test's body against the pre-fix module loaded from 9305028.

Non-dedicated mode was verified byte-for-byte equivalent to 77338af — the single-user install has no regression from any of this.

Known and accepted

$HOME=/ would let /etc through (is_relative_to("/") is true for everything); bind mounts defeat resolve() but need root already. Neither is a regression, both need a machine already lost.

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

Labels

None yet

Projects

None yet

1 participant