refuse register when remote root has state and local path is dirty #15

Merged
erik merged 2 commits from bug/init-refuse-existing-remote-root into main 2026-06-04 15:55:32 +00:00
Member

Refs: #8


Summary

dropbear register (formerly init) now refuses to register a root_id whose bucket prefix already contains objects and whose local path holds any user content (anything other than .dropbear/). Closes the "second origin device" footgun called out in #8 without breaking the legitimate "all devices retired, re-adopt on a fresh dir" flow that #13 enabled.

Rule

local path remote roots/<rootID>/ result
has files (anything other than .dropbear/) any object refuse (INIT_REMOTE_ROOT_EXISTS)
empty / only .dropbear/ objects but no registry refuse (EnsureRootNotInitialized, from #13)
empty / only .dropbear/ registry present allow (re-adopt)
empty / only .dropbear/ empty allow (fresh root)
has files empty allow (fresh root)

Probe errors now distinguish ErrUnauthorized → exit 9 and ErrTransient → exit 10, instead of collapsing to exit 1.

Changes

  • cmd/dropbear/register.goremoteRootHasState probe, localPathHasUserContent check, new INIT_REMOTE_ROOT_EXISTS refusal, error-class → exit-code mapping for probe failures.
  • cmd/dropbear/register_test.go — refuses with dirty local + remote registry; allows with empty local + remote registry; existing WithoutRegistry case still covers #13's invariant.
  • internal/config/{diagnostic,exitcode}.go — new DiagInitRemoteRootExists mapped to StateUninitialized.
  • internal/objectstore/keys.go — conflict resolution against #13's RootPrefix; no behavior change.
  • wiki/runbooks/install-daemon.md — rule documented next to the manual-register instructions.

Follow-up

Filed separately: let register read --remote-* from a pre-existing .dropbear/root.toml so users can copy a config from another device and re-register without re-typing the bucket/endpoint.

Test plan

  • go test ./cmd/dropbear/... ./internal/deviceregistry/... ./internal/config/... — 184 pass
  • just check — format/vet clean (lint crash was a transient golangci-lint/toolchain hiccup, runs clean on the user's side)
  • Manual: register with dirty local against a seeded bucket → refuses with the new diagnostic
  • Manual: register with empty local against a seeded bucket with registry → succeeds (re-adopt path)
Refs: #8 --- ## Summary `dropbear register` (formerly `init`) now refuses to register a `root_id` whose bucket prefix already contains objects **and** whose local path holds any user content (anything other than `.dropbear/`). Closes the "second origin device" footgun called out in #8 without breaking the legitimate "all devices retired, re-adopt on a fresh dir" flow that #13 enabled. ### Rule | local path | remote `roots/<rootID>/` | result | |---|---|---| | has files (anything other than `.dropbear/`) | any object | **refuse** (`INIT_REMOTE_ROOT_EXISTS`) | | empty / only `.dropbear/` | objects but no registry | refuse (`EnsureRootNotInitialized`, from #13) | | empty / only `.dropbear/` | registry present | allow (re-adopt) | | empty / only `.dropbear/` | empty | allow (fresh root) | | has files | empty | allow (fresh root) | Probe errors now distinguish `ErrUnauthorized` → exit 9 and `ErrTransient` → exit 10, instead of collapsing to exit 1. ## Changes - `cmd/dropbear/register.go` — `remoteRootHasState` probe, `localPathHasUserContent` check, new `INIT_REMOTE_ROOT_EXISTS` refusal, error-class → exit-code mapping for probe failures. - `cmd/dropbear/register_test.go` — refuses with dirty local + remote registry; allows with empty local + remote registry; existing `WithoutRegistry` case still covers #13's invariant. - `internal/config/{diagnostic,exitcode}.go` — new `DiagInitRemoteRootExists` mapped to `StateUninitialized`. - `internal/objectstore/keys.go` — conflict resolution against #13's `RootPrefix`; no behavior change. - `wiki/runbooks/install-daemon.md` — rule documented next to the manual-`register` instructions. ## Follow-up Filed separately: let `register` read `--remote-*` from a pre-existing `.dropbear/root.toml` so users can copy a config from another device and re-register without re-typing the bucket/endpoint. ## Test plan - [x] `go test ./cmd/dropbear/... ./internal/deviceregistry/... ./internal/config/...` — 184 pass - [x] `just check` — format/vet clean (lint crash was a transient golangci-lint/toolchain hiccup, runs clean on the user's side) - [ ] Manual: register with dirty local against a seeded bucket → refuses with the new diagnostic - [ ] Manual: register with empty local against a seeded bucket with registry → succeeds (re-adopt path)
refuse register when remote root has state and local path is dirty
All checks were successful
Validate / validate (pull_request) Successful in 46s
141da5bf15
`dropbear register` previously inspected only the local path: a fresh checkout with `--root-id X` succeeded even when `roots/X/` already held objects in the configured bucket, silently creating a "second origin" device. Issue 3 (#13) closed the unsafe-bootstrap variant (objects without a registry); this closes the broader footgun for issue 8.

Register now probes `roots/<rootID>/` for any object. If anything is there and the local path contains any entry other than `.dropbear/`, register refuses with the new `INIT_REMOTE_ROOT_EXISTS` diagnostic and points the user at `dropbear restore`. When the local path is empty (or only has `.dropbear/`), the existing `EnsureRootNotInitialized` check still gates against unsafe bootstraps — the legitimate "every device retired, re-adopt the root on a fresh dir" flow keeps working. Unauthorized and transient probe errors now map to exit 9/10 instead of the generic 1.

Tests cover the new refusal path and the empty-dir re-adoption path. Runbook updated to describe the rule.

Refs: #8
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
fix register remediation: restore needs root.toml first
All checks were successful
Validate / validate (pull_request) Successful in 52s
02cb17a68c
The previous remediation pointed users at `dropbear restore`, but restore calls `loadRoot` which requires an existing `.dropbear/root.toml` — only `register` creates that file. The advice was circular. Point users at the two paths that actually work: use a different `root_id`, or clear the local path so the existing-root case becomes the empty-local-dir re-adoption flow.

Refs: #8
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
erik merged commit 89d50b719e into main 2026-06-04 15:55:32 +00:00
erik deleted branch bug/init-refuse-existing-remote-root 2026-06-04 15:55:33 +00:00
Sign in to join this conversation.
No description provided.