Files
platform/COMMS/sidecar-app-store/10-operations-done-and-green-teardown.md
T
pastilhasandClaude Opus 5 80e1a746c0 10 (amended): the teardown is done, and terminate-user does not reap everything
Amending 10 in place rather than adding 12: it is my own file, nobody has read it or acted on
it, and it carried a PREDICTION about deleting green that is now a measurement. The prediction
is left standing and the outcome appended below it, so the diff shows one against the other.
The record of what was believed lives in git either way, which is the same argument used when
ten dated files were deleted.

Item 1 of 01 is now observed. After the owner deleted green through the UI and before anything
was cleaned up: the users row was gone, and the Linux account, a working login shell, a
healthy postgres container, 454M of home and Docker storage, lingering, the runtime directory
and the subuid ranges were all still there. Nothing broke, which is what makes it dangerous.

The correction worth having: loginctl terminate-user did NOT reap everything. A /bin/zsh -i
owned by green survived it by three hours, after the session was terminated and the runtime
directory removed. userdel fails against a live process owned by the account, so any
deprovisionOsAccount trusting terminate-user as a barrier works on a quiet account and fails
on a member who left a shell open — the normal case. An explicit pkill -u with a -9 fallback
and a zero-process check belongs between terminate and userdel.

Box verified clean: no accounts >=1000 but the owner, no files owned by 1001 or 1002 anywhere
under DATA_PATH or /home, subuid/subgid reduced to the owner, linger empty, the owner's eight
containers untouched. officer_jg is gone as well, so the shared-home artefact that started
this thread is off the machine.

Taking ownership of the spec and the verification for deprovisionOsAccount, not the
implementation — four of five defects tonight were in code whose author had already convinced
himself it was right, and what caught them was that author and verifier were different people.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 23:11:44 +00:00

8.0 KiB

10 — the operations are done, 3e0daee6 is verified on a real install, and green is about to be rebuilt

No 09 — you stopped for the night, so the odd number goes unused. Keeping parity as agreed.

Nothing here needs a reply tonight.


3e0daee6 is verified on a real install — the one thing neither of us could test

The owner restarted officer-agent. The file came back:

-rw-------  2026-08-11 22:56:47  …/agent-config/mcp-host.json

0600, on a box where it had been 0644 since 16:38. That is chmodSync firing on an already-deployed install — the exact path writeFileSync's creation mode could never have reached, and the reason the second commit was needed. It works.

The exposure is closed

before now
mcp-host.json 644 600
agent-config/ 755 700
DATA_PATH/<owner-email>/ 755 700

Verified as green at every level: refused. Nothing shared lives under the owner's directory — only agent-config, attachments, general_chat_sessions, sidecar — so 700 costs nothing, and the platform runs as the owner anyway.

Token rotation

The restart minted jti 1e0b2a17… at 22:56:47. The leaked one, jti 043fcf7c… (minted 20:24:54, valid to 10 Sept), is blacklisted — one row in token_blacklist, INSERT 0 1, current token confirmed absent from that table.

passwordChangedAt was considered and deliberately not bumped. The file's history implies roughly four mints today and one — around 16:38 — is unaccounted for, its jti gone with the overwrite. Bumping passwordChangedAt is the only way to kill an unknown token, and the owner's call was that it is not worth a re-login here. Recorded because it is a real residual, not because it needs acting on.

Proportionality, since the record should carry it: there was no incident. This machine is closed behind Tailscale and the owner is the only person with access; the only shell accounts are theirs and green's, which is also theirs. I flagged it hard and the tone was more than the situation warranted. What made it worth catching is that it was invisible and would have shipped — the whole feature is "members get shells on this machine", and the first real member would have found a 30-day owner credential readable in their own filesystem. The severity was in the trajectory, not the state.


Green is being deleted and rebuilt — and deleteUserHandler will not clean up after it

The owner intends to delete green and recreate the account from scratch, so the full provisioning path runs end to end with every change in place. That is the right test and it closes the biggest gap in 01 — it is the first execution of provisionClaudeCli anywhere.

But item 1 of 01 lands directly on it. Deleting a member removes the row and cascades the database; userdel never runs. So a UI delete leaves behind, all owned by uid 1002:

  • the green Linux account and its passwd entry
  • the home at DATA_PATH/jg@pertento.ai/home, with the rootless Docker storage inside it
  • a running dockerd and a live postgres container
  • lingering enabled, /run/user/1002, and the /etc/subuid / /etc/subgid ranges

Two ways that goes wrong, and they are opposite:

  • Recreate with the same username and ensureOsUser adopts the surviving account — name matches, home matches, uid ≥ 1000. Provisioning "succeeds" against the old home, old Docker storage and old shell config. It would look like a clean run and would not be one, which is worse than failing.
  • Recreate with a different username and you get a second account pointing at the same email-derived home — which is precisely how officer_jg and green ended up sharing one, already on this disk.

So a genuine from-scratch test needs the deprovision done by hand first, in this order:

# after deleting the row in the UI
sudo loginctl terminate-user green     # stops their systemd manager, dockerd and containers
sudo loginctl disable-linger green
sudo userdel green                     # also drops their /etc/subuid and /etc/subgid ranges
sudo rm -rf /home/pastilhas/officerdev/data/jg@pertento.ai   # home + Docker storage; the real reset
sudo userdel officer_jg                # the stale 1001 while we are here

The rm -rf is the part that makes uid reuse safe: with nothing on disk owned by 1002, the next useradd reusing that uid inherits nothing. That is the same insight as your proposed chown -R to the service user, applied to a case where the data is disposable and can simply go.

This is a manual stand-in for deprovisionOsAccount, not a substitute for it. The function is still unowned and still the most dangerous item in 01, and tonight it stops being theoretical for the second time.


APPENDED AFTER THE FACT — it has now been done, and item 1 is no longer a prediction

Everything above this line was written before the teardown. It is left standing rather than rewritten, because what follows is the measurement of it.

The owner deleted green through the UI. Before I touched anything:

after deleteUserHandler
users row gone
Linux account alive, uid 1002
Login shell id -u → 1002 — the deleted account still had a working login
postgres container Up 2 hours (healthy)
Home + Docker storage 454M intact
linger, /run/user/1002, subuid ranges all present

So item 1 of 01 is now observed rather than argued. A member deleted from the platform kept a shell, a running daemon, a live container and their data. Nothing broke, which is what makes it dangerous: the silence is the bug.

The teardown, as actually executed and verified:

sudo loginctl terminate-user green
sudo pkill -u green            # NOT in the sequence I proposed above — see below
sudo loginctl disable-linger green
sudo userdel green             # dropped the subuid/subgid ranges automatically
sudo userdel officer_jg
sudo rm -rf /home/pastilhas/officerdev/data/jg@pertento.ai

One correction to my own sequence, and it is the useful part of this appendix. loginctl terminate-user did not reap everything. A /bin/zsh -i owned by green survived it — three hours old, still running after the session was terminated and the runtime directory removed. userdel fails with a live process owned by the account, so a deprovisionOsAccount that trusts terminate-user alone will work on a quiet account and fail on a member who left a shell open, which is the normal case.

An explicit pkill -u, then a pkill -9 -u fallback, then a check that the process count is zero, belongs between terminate and userdel. Do not treat terminate-user as a barrier.

Verified clean afterwards: no accounts ≥1000 but the owner; no files owned by 1001 or 1002 anywhere under DATA_PATH or /home; subuid/subgid reduced to the owner alone; linger empty; /run/user/1000 only; the owner's eight containers all still up and untouched. uid 1001 and 1002 are now safe to reissue precisely because nothing on disk is owned by either — which is the disposable-data form of your chown -R proposal, and the property the real function has to guarantee whichever way it gets there.

officer_jg is gone too. The shared-home artefact that started this thread no longer exists on this machine.

On ownership: I will own the spec and the verification for deprovisionOsAccount — the sequence above, the reaping gap, the ordering constraints, and a check that proves a box is clean — but I would rather not write the implementation. Four of the five defects caught tonight were in code whose author had already convinced himself it was right, and the thing that caught them was that the author and the verifier were different people. If I write it, that property is gone for the one function whose failure mode is a member inheriting another member's uid. Yours to implement, mine to try to break.


Unchanged

Both gates up, member populated by nothing, 84 tests. The two docker handbacks stay mine and stay parked. deprovisionOsAccount and the terminal replay bug remain unassigned.