Commit Graph
3 Commits
Author SHA1 Message Date
Omar SobhandClaude Sonnet 4.6 70d1d410bb fix: add hot/slab ReadWritePaths to serve service unit
ProtectSystem=strict in claw-store-serve.service only listed
/var/lib/claw-store as writable. The serve process shells out to
`claw-store activate/deactivate` which also needs to write to:
  /hot/targets  — create/remove hot target dirs
  /slab/projects — write/remove .cargo/config.toml

Subprocess inherits the service's mount namespace, so both paths were
silently read-only, causing activate/deactivate from the dashboard to
return {"ok":false,"error":"Read-only file system (os error 30)"} while
the same commands worked fine from a login shell.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-06-30 11:11:41 +00:00
Omar SobhandClaude Opus 4.7 d5651614ba fix(serve): drop ProtectHome — was blocking SSH peer probe
The v0.2.0 unit set ProtectHome=true to lock down /home from a
hypothetical RCE in axum. Side effect: ssh in peer_reachable
(serve.rs:145) failed with "Host key verification failed: Permission
denied" because ~/.ssh/known_hosts was unreachable. Adding
BindReadOnlyPaths=/home/osobh/.ssh didn't help — systemd applies
ProtectHome before the bind mounts run, so /home is already an
inaccessible barrier when the bind lands. Result: both dashboards
showed peer_reachable=false even though LAN ping + SSH worked fine
from a shell.

Two options to keep some sandboxing:
  1. ProtectHome=tmpfs + BindReadOnlyPaths=/home/osobh/.ssh — bind
     into an empty tmpfs view of /home.
  2. Drop ProtectHome entirely — keep ProtectSystem=strict +
     ReadWritePaths=/var/lib/claw-store + NoNewPrivileges + PrivateTmp.

Going with (2) for now. The threat model is local-host RCE in a
read-mostly axum service the dashboard pokes; ProtectSystem alone
prevents writing anywhere outside /var/lib/claw-store. Re-introduce
(1) when we have a clean justification.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-28 13:08:02 +00:00
Omar SobhandClaude Opus 4.7 af50adec19 feat(v0.2.0): atomic+flocked manifest, pinned projects, serve systemd unit
The two biggest pain points coming out of the architecture review:

  1. The manifest at /var/lib/claw-store/projects.toml was the only
     piece of writable state but had no locking, no atomic writes,
     and three concurrent writers (daemon poll tick, every CLI verb,
     and the dashboard shelling out via /api/activate). Two writers
     interleaving silently dropped one of them; a crash mid-write
     left a corrupt half-written TOML that the next reader parsed
     as an empty manifest.

  2. Reboot survival: the dashboard had no systemd unit and was a
     stray hand-launched process. Architect lost its dashboard on
     todays reboot.

This commit lands:

- Manifest::update(path, FnOnce(&mut Manifest)) — locked-atomic
  load-mutate-save in one transaction. Uses libc::flock(LOCK_EX) on
  a sidecar .lock file (so the data file can be replaced by rename
  without invalidating the lock) and tempfile + persist for the
  rename. Concurrent writers serialise; readers see the previous
  state or the new state, never a torn write. Manifest::load uses
  LOCK_SH so it never races a mid-rename.

- Project.pinned: bool with #[serde(default)] so legacy manifests
  parse cleanly. hot::gc_stale_targets and hot::gc_by_space both
  skip pinned projects, with a WARN log when every remaining
  project is pinned but were still over budget — operator intent
  beats space pressure.

- claw-store pin <project> / unpin <project> CLI verbs.
  status command surfaces pin marker (📌).
  activate preserves an existing rows pinned flag so re-activating
  doesnt silently unpin.

- claw-store-serve.service systemd unit. Type=simple, Restart=
  on-failure, RestartSec=15, ProtectSystem=strict + ReadWritePaths
  =/var/lib/claw-store, ProtectHome, NoNewPrivileges, PrivateTmp.

- daemon poll tick reloads the manifest from disk at the start of
  each cycle (so CLI activations between ticks are visible) and
  routes its GC write through Manifest::update (so it cant race a
  concurrent CLI pin).

- libc + tempfile move from dev-deps into runtime deps.
- empty-manifest fallthrough on load (treat "" as default) so a
  half-written tempfile crashed pre-rename doesnt hard-fail the
  daemon next boot.

- 25 tests passing incl. new ones: legacy-toml-parses, update-
  serializes-two-sequential-writers, pinned-survives-stale-gc,
  pinned-survives-space-gc-even-when-lru.

Version bumped 0.1.0 → 0.2.0.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-28 12:59:41 +00:00