init should refuse a root_id that already exists on the remote #8

Closed
opened 2026-05-27 14:46:09 +00:00 by erik · 3 comments
Owner

Current behaviour (internal/config/init.go, cmd/dropbear/init.go)

init only inspects local state (.dropbear/root.toml via config.Validate) and only parses the remote config — it never talks to the bucket. As a result, dropbear init --root-id X ... on a fresh local path succeeds even when roots/X/ already has objects in the configured bucket. This silently creates a second "origin" device for an existing root, which can clobber peer state on the next sync.

Desired behaviour

init is for creating a root. restore is the only supported way to onboard a new device onto an existing root. Therefore:

  1. After parsing the remote config and loading credentials, init must probe the bucket for any object under /roots// (a List with MaxKeys=1 is enough; HeadObject on a known sentinel like heads/ would also work but listing is more robust to layout changes).
  2. If any object exists, init aborts before writing .dropbear/root.toml and prints a diagnostic pointing the user at dropbear restore.
  3. Exit code: a new code in the documented CLI table (or reuse exit 8 / a new INIT_REMOTE_ROOT_EXISTS diagnostic — pick whichever matches the existing exit-code policy).
  4. --force must not bypass this check (relabel-style safety: the remote is the source of truth for "this root_id is taken").
  5. Failure modes worth handling distinctly:
  • bucket unreachable / unauthorized → exit as today (9 / 10), don't silently fall through to "looks empty, proceed".
  • bucket reachable, prefix empty → proceed with local init.

Scope notes

  • New diagnostic constant in internal/config (e.g. DiagInitRemoteRootExists) + exit-code mapping in ExitCodeFor.
  • cmd/dropbear/init.go gains the probe between remoteconfig.Parse and config.Init (or push it into config.Init if you want the safety check non-bypassable by other callers; CLI-level is simpler).
  • Tests: at minimum a fake-store case where roots// contains a single key → init refuses; empty prefix → init proceeds; store returns ErrUnauthorized → init exits 9 without writing root.toml.
  • Docs: update wiki/runbooks/ (or whichever runbook covers onboarding) to state explicitly that restore is the device-onboarding path.

Out of scope

  • Multi-device adoption flag (--join, --adopt). If we ever want it, it should be a separate proposal; restore covers the use case today.
Current behaviour (internal/config/init.go, cmd/dropbear/init.go) init only inspects local state (.dropbear/root.toml via config.Validate) and only parses the remote config — it never talks to the bucket. As a result, dropbear init --root-id X ... on a fresh local path succeeds even when roots/X/ already has objects in the configured bucket. This silently creates a second "origin" device for an existing root, which can clobber peer state on the next sync. Desired behaviour init is for creating a root. restore is the only supported way to onboard a new device onto an existing root. Therefore: 1. After parsing the remote config and loading credentials, init must probe the bucket for any object under <prefix>/roots/<rootID>/ (a List with MaxKeys=1 is enough; HeadObject on a known sentinel like heads/ would also work but listing is more robust to layout changes). 2. If any object exists, init aborts before writing .dropbear/root.toml and prints a diagnostic pointing the user at dropbear restore. 3. Exit code: a new code in the documented CLI table (or reuse exit 8 / a new INIT_REMOTE_ROOT_EXISTS diagnostic — pick whichever matches the existing exit-code policy). 4. --force must not bypass this check (relabel-style safety: the remote is the source of truth for "this root_id is taken"). 5. Failure modes worth handling distinctly: - bucket unreachable / unauthorized → exit as today (9 / 10), don't silently fall through to "looks empty, proceed". - bucket reachable, prefix empty → proceed with local init. Scope notes - New diagnostic constant in internal/config (e.g. DiagInitRemoteRootExists) + exit-code mapping in ExitCodeFor. - cmd/dropbear/init.go gains the probe between remoteconfig.Parse and config.Init (or push it into config.Init if you want the safety check non-bypassable by other callers; CLI-level is simpler). - Tests: at minimum a fake-store case where roots/<rootID>/ contains a single key → init refuses; empty prefix → init proceeds; store returns ErrUnauthorized → init exits 9 without writing root.toml. - Docs: update wiki/runbooks/ (or whichever runbook covers onboarding) to state explicitly that restore is the device-onboarding path. Out of scope - Multi-device adoption flag (--join, --adopt). If we ever want it, it should be a separate proposal; restore covers the use case today.
Member

Brief — Developer

Phase: simple. Run fjx dev prompt simple for the cycle protocol.

<!-- pm:brief:dev --> # Brief — Developer **Phase:** simple. Run `fjx dev prompt simple` for the cycle protocol.
Member

Ledger for: agent-pm

Status: closed @ 2026-06-04T23:38:38.842Z
Briefs:

Activity

  1. i8/closed : applied

(PM manages this ledger; agent section unused)

<!-- agent-pm:ledger --> ## Ledger for: agent-pm Status: closed @ 2026-06-04T23:38:38.842Z Briefs: - [dev](#issuecomment-2298) ### Activity 1. i8/closed : applied --- _(PM manages this ledger; agent section unused)_
Member

Ledger for: agent-dev

Status: crashed @ 2026-06-03T16:44:58.211Z


Work Completed Before Blocker

  • Implemented a remote root-existence probe in cmd/dropbear/register.go before remote device registration and before local config.Init writes .dropbear/root.toml.
  • Added config.DiagInitRemoteRootExists and mapped it to exit 7 (uninitialized) via config.ExitCodeFor.
  • Added objectstore.RootPrefix for the listable <prefix>/roots/<rootID>/ probe prefix.
  • Added focused register tests for: existing remote root refuses without local/remote writes; empty prefix proceeds; unauthorized list exits 9 without writing root.toml.
  • Updated wiki/runbooks/install-daemon.md to state that register/[roots.init] create new roots and dropbear restore is the onboarding path for existing roots.
  • Updated TestCleanRemoteDeletesRootPrefix setup so it registers an empty remote root before seeding objects, matching the new invariant.

Validation Evidence

  • gofmt -s -w cmd/dropbear/register.go cmd/dropbear/register_test.go internal/config/diagnostic.go internal/config/exitcode.go internal/objectstore/keys.go passed.
  • gofmt -s -w cmd/dropbear/clean_test.go passed.
  • GOCACHE=/work/.cache/go-build GOMODCACHE=/work/.cache/gomod GOTMPDIR=/work/.cache/go-tmp go test ./cmd/dropbear ./internal/config ./internal/objectstore passed.
  • GOCACHE=/work/.cache/go-build GOMODCACHE=/work/.cache/gomod GOTMPDIR=/work/.cache/go-tmp go vet ./... passed.
  • env TMPDIR=/work/.cache/go-tmp GOCACHE=/work/.cache/go-build GOMODCACHE=/work/.cache/gomod GOTMPDIR=/work/.cache/go-tmp GOLANGCI_LINT_CACHE=/work/.cache/golangci-lint just check passed with 0 issues.

Blocker: just validate cannot complete in this runtime

Required check 1: fjx help / invocation check

  • No fjx subcommand failed; the failing command is the repo gate just validate.
  • fjx dev ledger --help relevant line checked: -f, --body-file <path> - Path to file containing the ledger body (agent-managed section only). Use '-' to read from stdin.
  • fjx issue label --help relevant line checked: -A, --add <label> - Label to add (repeatable).
  • just --show validate confirms the failing gate shape: validate: just test::format test::vet test::lint test::units test::secrets test::audit.

Verbatim just validate failure after cache/tmp workarounds:

ok  	git.tfks.net/tfks/dropbear/cmd/dropbear	10.678s	coverage: 65.7% of statements
ok  	git.tfks.net/tfks/dropbear/internal/blobio	1.113s	coverage: 79.3% of statements
ok  	git.tfks.net/tfks/dropbear/internal/config	1.231s	coverage: 75.4% of statements
2026/06/03 16:42:01 WARN bootstrapped root root=/work/.cache/go-tmp/TestBootstrapCreatesMissingPath4123375555/001/does-not-exist-yet root_id=test-root device_id=test-device mode=bidirectional
2026/06/03 16:42:01 WARN bootstrapped root root=/work/.cache/go-tmp/TestBootstrapMkdirFailureSurfaces563763020/001/subdir root_id=test-root device_id=test-device mode=bidirectional
--- FAIL: TestBootstrapMkdirFailureSurfaces (0.01s)
    bootstrap_test.go:159: expected mkdir failure
FAIL
coverage: 67.5% of statements
FAIL	git.tfks.net/tfks/dropbear/internal/daemon	6.448s
--- FAIL: TestIgnoredPathNoWakeup (0.07s)
    fswatch_test.go:101: ignored path produced a wakeup
FAIL
coverage: 74.1% of statements
FAIL	git.tfks.net/tfks/dropbear/internal/fswatch	0.945s
FAIL
error: recipe `units` failed on line 23 with exit code 1
error: recipe `validate` failed on line 88 with exit code 1

Earlier environment errors before moving caches/temp paths:

failed to initialize build cache at /agent-home/.cache/go-build: mkdir /agent-home/.cache: permission denied
failed to initialize build cache at /tmp/dropbear-go-build: mkdir /tmp/dropbear-go-build: no space left on device
go: could not create module cache: mkdir /usr/local/gopath/pkg: permission denied
error: recipe `format` could not be run because of an IO error while trying to create a temporary directory or write a file to that directory: No space left on device (os error 28) at path "/tmp/just-Jz8wsd"
2026/06/03 16:41:04 failed to initialize build cache at /agent-home/.cache/golangci-lint: mkdir /agent-home/.cache: permission denied
fork/exec /dev/shm/dropbear-tmp/go-build3680510217/b001/daemon.test: permission denied
mkdir: cannot create directory '/dev/dropbear-tmp': Permission denied

Search results:

  • rg -n "TestBootstrapMkdirFailureSurfaces|TestIgnoredPathNoWakeup|fakeowner|no space left on device|GOTMPDIR|GOCACHE|just validate" wiki returned only wiki/runbooks/validate.md:17:1. Run just validate.
  • fjx dev ledger showed no prior mention of these symptoms in the issue ledger; it only contained the initial working plan/evidence.

Environment evidence:

Filesystem            Size  Used Avail Use% Mounted on
overlay                59G   57G     0 100% /
/run/host_mark/Users  461G  445G   16G  97% /work
overlay                59G   57G     0 100% /tmp
shm                    64M     0   64M   0% /dev/shm

mount shows /work and /worktrees are fakeowner; using /work/.cache/go-tmp makes the daemon chmod test fail because permission bits do not behave like normal Linux filesystem permissions. /dev/shm has normal tmpfs space but is noexec, so Go test binaries cannot run there. /dev is not writable. The root overlay and /tmp have no available space.

Required check 3: phase prompt / brief re-read

  • Re-read fjx dev prompt simple.
  • Re-checked the simple prompt step: “Read the issue, make the change, open the PR” and “If during the work you discover the change isn't actually simple ... stop and report it in the ledger rather than expanding scope.” The implementation remains scoped to issue #8; the blocker is the required validation gate in this runtime, not a scope/design discovery.

Required check 4: classification

  • Classification: just validate flake/environment.

Smallest concrete unblock needed

Provide an executable temp/cache location with normal Linux permission semantics and enough space for go test -race ./..., or free space on the root overlay /tmp, then rerun:

env TMPDIR=<normal-exec-tmp> GOCACHE=<writable-cache> GOMODCACHE=<writable-mod-cache> GOTMPDIR=<normal-exec-tmp> GOLANGCI_LINT_CACHE=<writable-lint-cache> just validate
<!-- agent-dev:ledger --> ## Ledger for: agent-dev Status: crashed @ 2026-06-03T16:44:58.211Z --- ## Work Completed Before Blocker - Implemented a remote root-existence probe in `cmd/dropbear/register.go` before remote device registration and before local `config.Init` writes `.dropbear/root.toml`. - Added `config.DiagInitRemoteRootExists` and mapped it to exit 7 (`uninitialized`) via `config.ExitCodeFor`. - Added `objectstore.RootPrefix` for the listable `<prefix>/roots/<rootID>/` probe prefix. - Added focused register tests for: existing remote root refuses without local/remote writes; empty prefix proceeds; unauthorized list exits 9 without writing `root.toml`. - Updated `wiki/runbooks/install-daemon.md` to state that register/`[roots.init]` create new roots and `dropbear restore` is the onboarding path for existing roots. - Updated `TestCleanRemoteDeletesRootPrefix` setup so it registers an empty remote root before seeding objects, matching the new invariant. ## Validation Evidence - `gofmt -s -w cmd/dropbear/register.go cmd/dropbear/register_test.go internal/config/diagnostic.go internal/config/exitcode.go internal/objectstore/keys.go` passed. - `gofmt -s -w cmd/dropbear/clean_test.go` passed. - `GOCACHE=/work/.cache/go-build GOMODCACHE=/work/.cache/gomod GOTMPDIR=/work/.cache/go-tmp go test ./cmd/dropbear ./internal/config ./internal/objectstore` passed. - `GOCACHE=/work/.cache/go-build GOMODCACHE=/work/.cache/gomod GOTMPDIR=/work/.cache/go-tmp go vet ./...` passed. - `env TMPDIR=/work/.cache/go-tmp GOCACHE=/work/.cache/go-build GOMODCACHE=/work/.cache/gomod GOTMPDIR=/work/.cache/go-tmp GOLANGCI_LINT_CACHE=/work/.cache/golangci-lint just check` passed with `0 issues.` ## Blocker: `just validate` cannot complete in this runtime ### Required check 1: fjx help / invocation check - No `fjx` subcommand failed; the failing command is the repo gate `just validate`. - `fjx dev ledger --help` relevant line checked: `-f, --body-file <path> - Path to file containing the ledger body (agent-managed section only). Use '-' to read from stdin.` - `fjx issue label --help` relevant line checked: `-A, --add <label> - Label to add (repeatable)`. - `just --show validate` confirms the failing gate shape: `validate: just test::format test::vet test::lint test::units test::secrets test::audit`. ### Required check 2: verbatim error and prior-symptom search Verbatim `just validate` failure after cache/tmp workarounds: ```text ok git.tfks.net/tfks/dropbear/cmd/dropbear 10.678s coverage: 65.7% of statements ok git.tfks.net/tfks/dropbear/internal/blobio 1.113s coverage: 79.3% of statements ok git.tfks.net/tfks/dropbear/internal/config 1.231s coverage: 75.4% of statements 2026/06/03 16:42:01 WARN bootstrapped root root=/work/.cache/go-tmp/TestBootstrapCreatesMissingPath4123375555/001/does-not-exist-yet root_id=test-root device_id=test-device mode=bidirectional 2026/06/03 16:42:01 WARN bootstrapped root root=/work/.cache/go-tmp/TestBootstrapMkdirFailureSurfaces563763020/001/subdir root_id=test-root device_id=test-device mode=bidirectional --- FAIL: TestBootstrapMkdirFailureSurfaces (0.01s) bootstrap_test.go:159: expected mkdir failure FAIL coverage: 67.5% of statements FAIL git.tfks.net/tfks/dropbear/internal/daemon 6.448s --- FAIL: TestIgnoredPathNoWakeup (0.07s) fswatch_test.go:101: ignored path produced a wakeup FAIL coverage: 74.1% of statements FAIL git.tfks.net/tfks/dropbear/internal/fswatch 0.945s FAIL error: recipe `units` failed on line 23 with exit code 1 error: recipe `validate` failed on line 88 with exit code 1 ``` Earlier environment errors before moving caches/temp paths: ```text failed to initialize build cache at /agent-home/.cache/go-build: mkdir /agent-home/.cache: permission denied failed to initialize build cache at /tmp/dropbear-go-build: mkdir /tmp/dropbear-go-build: no space left on device go: could not create module cache: mkdir /usr/local/gopath/pkg: permission denied error: recipe `format` could not be run because of an IO error while trying to create a temporary directory or write a file to that directory: No space left on device (os error 28) at path "/tmp/just-Jz8wsd" 2026/06/03 16:41:04 failed to initialize build cache at /agent-home/.cache/golangci-lint: mkdir /agent-home/.cache: permission denied fork/exec /dev/shm/dropbear-tmp/go-build3680510217/b001/daemon.test: permission denied mkdir: cannot create directory '/dev/dropbear-tmp': Permission denied ``` Search results: - `rg -n "TestBootstrapMkdirFailureSurfaces|TestIgnoredPathNoWakeup|fakeowner|no space left on device|GOTMPDIR|GOCACHE|just validate" wiki` returned only `wiki/runbooks/validate.md:17:1. Run just validate.` - `fjx dev ledger` showed no prior mention of these symptoms in the issue ledger; it only contained the initial working plan/evidence. Environment evidence: ```text Filesystem Size Used Avail Use% Mounted on overlay 59G 57G 0 100% / /run/host_mark/Users 461G 445G 16G 97% /work overlay 59G 57G 0 100% /tmp shm 64M 0 64M 0% /dev/shm ``` `mount` shows `/work` and `/worktrees` are `fakeowner`; using `/work/.cache/go-tmp` makes the daemon chmod test fail because permission bits do not behave like normal Linux filesystem permissions. `/dev/shm` has normal tmpfs space but is `noexec`, so Go test binaries cannot run there. `/dev` is not writable. The root overlay and `/tmp` have no available space. ### Required check 3: phase prompt / brief re-read - Re-read `fjx dev prompt simple`. - Re-checked the simple prompt step: “Read the issue, make the change, open the PR” and “If during the work you discover the change isn't actually simple ... stop and report it in the ledger rather than expanding scope.” The implementation remains scoped to issue #8; the blocker is the required validation gate in this runtime, not a scope/design discovery. ### Required check 4: classification - Classification: `just validate` flake/environment. ### Smallest concrete unblock needed Provide an executable temp/cache location with normal Linux permission semantics and enough space for `go test -race ./...`, or free space on the root overlay `/tmp`, then rerun: ```sh env TMPDIR=<normal-exec-tmp> GOCACHE=<writable-cache> GOMODCACHE=<writable-mod-cache> GOTMPDIR=<normal-exec-tmp> GOLANGCI_LINT_CACHE=<writable-lint-cache> just validate ```
agent-dev removed their assignment 2026-06-03 16:44:57 +00:00
Sign in to join this conversation.
No description provided.