Server: --tls-cert/--tls-key flags enable TLS (min TLS 1.2). Client: --tls enables TLS transport, --tls-ca for custom CA certs. Two integration tests: push/pull over TLS, reject untrusted client. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
14 KiB
14 KiB
PROJECT_PLAN.md
Implementation plan for sgard. See ARCHITECTURE.md for design details.
Step 1: Project Scaffolding
Remove old C++ source files and set up the Go project.
- Remove old files:
sgard.cc,proto/,CMakeLists.txt,scripts/,.clang-format,.clang-tidy,.idea/,.trunk/ go mod init github.com/kisom/sgard- Add dependencies:
gopkg.in/yaml.v3,github.com/spf13/cobra - Create directory structure:
cmd/sgard/,manifest/,store/,garden/ - Set up
cmd/sgard/main.gowith cobra root command and--repopersistent flag - Update CLAUDE.md to reflect Go project
- Verify:
go build ./...compiles clean
Step 2: Manifest Package
Can be done in parallel with Step 3.
manifest/manifest.go:ManifestandEntrystructs with YAML tags- Entry types:
file,directory,link - Mode as string type to avoid YAML octal coercion
- Per-file
updatedtimestamp
- Entry types:
manifest/manifest.go:Load(path)andSave(path)functions- Save uses atomic write (write to
.tmp, rename)
- Save uses atomic write (write to
manifest/manifest_test.go: round-trip marshal/unmarshal, atomic save, entry type validation
Step 3: Store Package
Can be done in parallel with Step 2.
store/store.go:Storestruct withrootpathstore/store.go:Write(data) (hash, error)— hash content, write toblobs/XX/YY/<hash>store/store.go:Read(hash) ([]byte, error)— read blob by hashstore/store.go:Exists(hash) bool— check if blob existsstore/store.go:Delete(hash) error— remove a blobstore/store_test.go: write/read round-trip, integrity check, missing blob error
Step 4: Garden Core — Init and Add
Depends on Steps 2 and 3.
garden/hasher.go:HashFile(path) (string, error)— SHA-256 of a filegarden/garden.go:Gardenstruct tying manifest + store + root pathgarden/garden.go:Open(root) (*Garden, error)— load existing repogarden/garden.go:Init(root) (*Garden, error)— create new repo (dirs + empty manifest)garden/garden.go:Add(paths []string) error— hash files, store blobs, add manifest entriesgarden/garden_test.go: init creates correct structure, add stores blob and updates manifest- Wire up CLI:
cmd/sgard/init.go,cmd/sgard/add.go - Verify:
go build ./cmd/sgard && ./sgard init && ./sgard add ~/.bashrc
Step 5: Checkpoint and Status
Depends on Step 4.
garden/garden.go:Checkpoint(message string) error— re-hash all tracked files, store changed blobs, update manifest timestampsgarden/garden.go:Status() ([]FileStatus, error)— compare current hashes to manifest; report modified/missing/okgarden/garden_test.go: checkpoint detects changed files, status reports correctly- Wire up CLI:
cmd/sgard/checkpoint.go,cmd/sgard/status.go
Step 6: Restore
Depends on Step 5.
garden/garden.go:Restore(paths []string, force bool, confirm func) error- Restore all files if paths is empty, otherwise just the specified paths
- Timestamp comparison: skip prompt if manifest
updatedis newer than file mtime - Prompt user if file on disk is newer or times match (unless
--force) - Create parent directories as needed
- Recreate symlinks for
linktype entries - Set file permissions from manifest
mode
garden/garden_test.go: restore writes correct content, respects permissions, handles symlinks- Wire up CLI:
cmd/sgard/restore.go
Step 7: Remaining Commands
These can be done in parallel with each other.
garden/remove.go:Remove(paths []string) error— remove manifest entriesgarden/verify.go:Verify() ([]VerifyResult, error)— check blobs against manifest hashesgarden/list.go:List() []Entry— return all manifest entriesgarden/diff.go:Diff(path string) (string, error)— diff stored blob vs current file- Wire up CLI:
cmd/sgard/remove.go,cmd/sgard/verify.go,cmd/sgard/list.go,cmd/sgard/diff.go - Tests for each
Step 8: Polish
- Lint setup (golangci-lint config)
- Clock abstraction: inject
jonboulle/clockworkinto Garden for deterministic timestamp tests - End-to-end test: init → add → checkpoint → modify file → status → restore → verify
- Ensure
go vet ./...andgo test ./...pass clean - Update CLAUDE.md, ARCHITECTURE.md, PROGRESS.md
Phase 2: gRPC Remote Sync
Step 9: Proto Definitions + Code Gen
- Write
proto/sgard/v1/sgard.proto— 5 RPCs (PushManifest, PushBlobs, PullManifest, PullBlobs, Prune), all messages - Add Makefile target for protoc code generation
- Add grpc, protobuf, x/crypto deps to go.mod
- Update flake.nix devShell with protoc tools
- Verify:
go build ./sgardpbcompiles
Step 10: Garden Accessor Methods
Can be done in parallel with Step 11.
garden/garden.go:GetManifest(),BlobExists(),ReadBlob(),WriteBlob(),ReplaceManifest()- Tests for each accessor
- Verify:
go test ./garden/...
Step 11: Proto-Manifest Conversion
Can be done in parallel with Step 10.
server/convert.go:ManifestToProto,ProtoToManifest, entry helpersserver/convert_test.go: round-trip test- Verify:
go test ./server/...
Step 12: Server Implementation (No Auth)
Depends on Steps 9, 10, 11.
server/server.go: Server struct with RWMutex, 5 RPC handlers (+ Prune)- PushManifest: timestamp compare, compute missing blobs
- PushBlobs: receive stream, write to store, replace manifest
- PullManifest: return manifest
- PullBlobs: stream requested blobs (64 KiB chunks)
- Prune: remove orphaned blobs (added store.List + garden.ListBlobs/DeleteBlob)
server/server_test.go: in-process test with bufconn, push+pull+prune
Step 12b: Directory Recursion and Mirror Command
garden/garden.go:Addrecurses directories — walk all files/symlinks, add each as its own entrygarden/mirror.go:MirrorUp(paths []string) error— walk directory, add new files, remove entries for files gone from disk, re-hash changedgarden/mirror.go:MirrorDown(paths []string, force bool, confirm func(string) bool) error— restore all tracked files under path, delete anything not in manifestgarden/mirror_test.go: tests for recursive add, mirror up (detects new/removed), mirror down (cleans extras)cmd/sgard/mirror.go:sgard mirror up <path>,sgard mirror down <path> [--force]- Update existing add tests for directory recursion
Step 13: Client Library (No Auth)
Depends on Step 12.
client/client.go: Client struct,Push(),Pull(),Prune()methodsclient/client_test.go: integration tests (push+pull cycle, server newer, up-to-date, prune)
Step 14: SSH Key Auth
server/auth.go: AuthInterceptor, parse authorized_keys, verify SSH signaturesclient/auth.go: LoadSigner (ssh-agent or key file), SSHCredentials (PerRPCCredentials)server/auth_test.go: valid key, reject unauthenticated, reject unauthorized key, reject expired timestampclient/auth_test.go: metadata generation, no-transport-security- Integration tests: authenticated push/pull succeeds, unauthenticated is rejected
Step 15: CLI Wiring + Prune
Depends on Steps 13, 14.
garden/prune.go:Prune() (int, error)— collect referenced hashes, delete orphaned blobsgarden/prune_test.go: prune removes orphaned, keeps referencedserver/server.go: Prune RPC (done in Step 12)proto/sgard/v1/sgard.proto: Prune RPC (done in Step 9)client/client.go: Prune() method (done in Step 13)cmd/sgard/prune.go: local prune; with--remoteprunes remote insteadcmd/sgard/main.go: add--remote,--ssh-keypersistent flags, resolveRemote()cmd/sgard/push.go,cmd/sgard/pull.gocmd/sgardd/main.go: flags, garden open, auth interceptor, gRPC serve- Verify: both binaries compile
Step 16: Polish + Release
- Update ARCHITECTURE.md, README.md, CLAUDE.md, PROGRESS.md
- Update flake.nix (add sgardd, updated vendorHash)
- Update .goreleaser.yaml (add sgardd build)
- E2e integration test: init two repos, push from one, pull into other (with auth)
- Verify: all tests pass, full push/pull cycle works
Phase 3: Encryption
Step 17: Encryption Core (Passphrase Only)
manifest/manifest.go: addEncrypted,PlaintextHashfields to Entry; addEncryptionsection withKekSlotsmap to Manifestgarden/encrypt.go:EncryptInit(passphrase string) error— generate DEK, derive KEK via Argon2id, wrap DEK, store in manifest encryption sectiongarden/encrypt.go:UnlockDEK(prompt) error— read slots, try passphrase, unwrap DEK; cache in memory for command durationgarden/encrypt.go: encrypt/decrypt helpers using XChaCha20-Poly1305 (nonce + seal/open)garden/garden.go: modify Add to accept encrypt flag — encrypt blob before storing, setencrypted: trueandplaintext_hashon entrygarden/garden.go: modify Checkpoint to re-encrypt changed encrypted entries (compares plaintext_hash)garden/garden.go: modify Restore to decrypt encrypted blobs before writinggarden/diff.go: modify Diff to decrypt stored blob before diffinggarden/garden.go: modify Status to useplaintext_hashfor encrypted entries- Tests: 10 encryption tests (init, persist, unlock, add-encrypted, restore round-trip, checkpoint, status, diff, requires-DEK)
- Verify:
go test ./... && go vet ./... && golangci-lint run ./...
Step 18: FIDO2 Support
Depends on Step 17.
garden/encrypt_fido2.go: FIDO2Device interface, AddFIDO2Slot, unlockFIDO2, defaultFIDO2Labelgarden/encrypt.go: UnlockDEK tries fido2/* slots first (credential_id matching), falls back to passphrasegarden/encrypt_fido2_test.go: mock FIDO2 device, 6 tests (add slot, duplicate rejected, unlock via FIDO2, fallback to passphrase, persistence, encrypted round-trip with FIDO2)- Verify:
go test ./... && go vet ./... && golangci-lint run ./...
Step 19: Encryption CLI + Slot Management
Depends on Steps 17, 18.
cmd/sgard/encrypt.go:sgard encrypt init [--fido2],add-fido2 [--label],remove-slot,list-slots,change-passphrasegarden/encrypt.go:RemoveSlot,ListSlots,ChangePassphrasemethodscmd/sgard/add.go: add--encryptflag with passphrase prompt- Update proto: add
encrypted,plaintext_hashto ManifestEntry; add KekSlot, Encryption messages, encryption field on Manifest - Update
server/convert.go: full encryption section conversion (Encryption, KekSlot) - Verify: both binaries compile,
go test ./..., lint clean
Step 20: Encryption Polish + Release
- E2e test: full encryption lifecycle (init, add encrypted+plaintext, checkpoint, modify, status, restore, verify, diff, slot management, passphrase change)
- Update ARCHITECTURE.md, README.md, CLAUDE.md
- Update flake.nix vendorHash
- Verify: all tests pass, lint clean
Future Steps (Not Phase 3)
Phase 4: Hardening + Completeness
Step 21: Lock/Unlock Toggle Commands
garden/lock.go:Lock(paths),Unlock(paths)— toggle locked flag on existing entriescmd/sgard/lock.go:sgard lock <path>...,sgard unlock <path>...- Tests: lock/unlock existing entry, persist, error on untracked, checkpoint/status behavior changes (6 tests)
Step 22: Shell Completion
- Cobra provides built-in
sgard completionfor bash, zsh, fish, powershell — no code needed - README updated with shell completion installation instructions
Step 23: TLS Transport for sgardd
cmd/sgardd/main.go: add--tls-cert,--tls-keyflags- Server uses
credentials.NewTLS()when cert/key provided, insecure otherwise - Client: add
--tlsflag and--tls-cafor custom CA - Update
cmd/sgard/main.goanddialRemote()for TLS - Tests: TLS connection with self-signed cert (push/pull cycle, reject untrusted client)
- Update ARCHITECTURE.md and README.md
Step 24: DEK Rotation
garden/encrypt.go:RotateDEK(promptPassphrase func() (string, error)) error— generate new DEK, re-encrypt all encrypted blobs, re-wrap with all existing KEK slotscmd/sgard/encrypt.go:sgard encrypt rotate-dek- Tests: rotate DEK, verify all encrypted entries still decrypt correctly
Step 25: Real FIDO2 Hardware Binding
- Evaluate approach: libfido2 CGo bindings vs subprocess (
fido2-token/fido2-cred) - Implement real
FIDO2Devicesatisfying the existing interface cmd/sgard/encrypt.go: wire real device intoadd-fido2and unlock resolution- Build tag or runtime detection for FIDO2 availability
- Tests: skip on CI without hardware, manual test instructions
Step 26: Test Cleanup
- Standardize all test calls to use
AddOptions{}struct (remove any legacy variadic patterns) - Ensure all tests use
t.TempDir()consistently - Review lint config, tighten if possible
- Verify test coverage for lock/unlock, encrypted locked files, dir-only locked entries
Step 27: Phase 4 Polish + Release
- Update all docs (ARCHITECTURE.md, README.md, CLAUDE.md, PROGRESS.md)
- Update flake.nix vendorHash if deps changed
- Update .goreleaser.yaml if needed
- E2e test covering TLS + encryption + locked files
- Verify: all tests pass, lint clean, both binaries compile
Phase 5: Multi-Repo + Per-Machine Inclusion
(To be planned)
Phase 6: Manifest Signing
(To be planned — requires trust model design)