Commit Graph

81 Commits

Author SHA1 Message Date
de5759ac77 Add file exclusion support (sgard exclude/include).
Paths added to the manifest's exclude list are skipped during Add,
MirrorUp, and MirrorDown directory walks. Excluding a directory
excludes everything under it. Already-tracked entries matching a
new exclusion are removed from the manifest.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
v3.2.0
2026-03-27 00:25:42 -07:00
b9b9082008 Bump VERSION to 3.1.7.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
v3.1.7
2026-03-26 11:57:26 -07:00
bd54491c1d Pull auto-inits repo, restores files, and add -r global shorthand.
pull now works on a fresh machine: inits ~/.sgard if missing, always
pulls when local manifest is empty, and restores all files after
downloading blobs. -r is now a global shorthand for --remote; list
uses resolveRemoteConfig() like prune.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:57:16 -07:00
57d252cee4 Bump VERSION to 3.1.6.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
v3.1.6
2026-03-26 11:27:17 -07:00
78030230c5 Update docs for VERSION file and build versioning.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:27:12 -07:00
adfb087037 Derive build version from git tags via VERSION file.
flake.nix reads from VERSION instead of hardcoding; Makefile gains
a version target that syncs VERSION from the latest git tag and
injects it into go build ldflags.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
v3.1.5
2026-03-26 11:26:16 -07:00
5570f82eb4 Add version in flake. v3.1.4 2026-03-26 11:14:28 -07:00
bffe7bde12 Add remote listing support to sgard list via -r flag.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
v3.1.3
2026-03-26 09:22:59 -07:00
3e0aabef4a Suppress passphrase echo in terminal prompts.
Use golang.org/x/term.ReadPassword so passphrases are not displayed
while typing, matching ssh behavior.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
v3.1.2
2026-03-25 21:49:56 -07:00
4ec71eae00 Deploy sgardd to rift and add persistent remote config.
Deployment: Dockerfile + docker-compose for sgardd on rift behind mc-proxy
(L4 SNI passthrough on :9443, multiplexed with metacrypt gRPC). TLS via
Metacrypt-issued cert, SSH-key auth.

CLI: `sgard remote set/show` saves addr, TLS, and CA path to
<repo>/remote.yaml so push/pull work without flags.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
v2.1.1
2026-03-25 21:23:21 -07:00
d2161fdadc fix vendorHash for default (non-fido2) package
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
v3.1.1
2026-03-25 14:00:58 -07:00
cefa9b7970 Add sgard info command for detailed file inspection.
Shows path, type, status, mode, hash, timestamps, encryption,
lock state, and targeting labels for a single tracked file.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
v3.1.0
2026-03-25 11:24:23 -07:00
e37e788885 Step 32: Phase 5 polish.
E2e test covering targeting labels through push/pull cycle. Updated
README with targeting docs and commands. All project docs updated.
Phase 5 complete.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
v3.0.0
2026-03-24 22:57:59 -07:00
2ff9fe2f50 Step 31: Proto + sync update for targeting.
Added only/never repeated string fields to ManifestEntry proto.
Updated convert.go for round-trip. Targeting test in convert_test.go.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 22:55:02 -07:00
60c0c50acb Step 30: Targeting CLI commands.
tag add/remove/list for machine-local tags. identity prints full label
set. --only/--never flags on add. target command to set/clear targeting
on existing entries. SetTargeting garden method.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 22:53:07 -07:00
d4d1d316db Step 29: Operations respect targeting.
Checkpoint, Restore, and Status now skip entries that don't match the
machine's identity labels. Status reports non-matching as "skipped".
Add accepts Only/Never in AddOptions, propagated through addEntry.
6 tests covering skip/process/skipped-status/restore-skip/add-with.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 22:51:27 -07:00
589f76c10e Step 28: Machine identity and targeting core.
Entry gains Only/Never fields for per-machine targeting. Machine
identity = short hostname + os:<GOOS> + arch:<GOARCH> + tag:<name>.
Tags stored in local <repo>/tags file (added to .gitignore by init).
EntryApplies() matching: only=any-match, never=no-match, both=error.
13 tests covering matching, identity, tags CRUD, gitignore.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 22:47:02 -07:00
7797de7d48 Plan Phase 5: per-machine targeting with only/never labels.
Machine identity = hostname + os:<GOOS> + arch:<GOARCH> + tag:<name>.
Entry-level only/never fields for selective restore/checkpoint.
Local tags file for machine-specific labels. Steps 28–32 planned.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 22:42:56 -07:00
c8281398d1 Step 27: Phase 4 polish.
E2e integration test covering TLS + encryption + locked files in a
push/pull cycle (integration/phase4_test.go). Final doc updates.
Phase 4 complete.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
v2.1.0
2026-03-24 16:18:42 -07:00
3cac9a3530 Step 26: Test cleanup.
Tightened lint config (added copyloopvar, durationcheck, makezero,
nilerr, bodyclose). Added 3 combo tests: encrypted+locked files,
dir-only+locked entries, lock/unlock toggle on encrypted entries.
Fixed stale API signatures in ARCHITECTURE.md. All tests already
used t.TempDir() and AddOptions{} consistently.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 12:45:29 -07:00
490db0599c Step 25: Real FIDO2 hardware key support.
HardwareFIDO2 implements FIDO2Device via go-libfido2 (CGo bindings to
Yubico's libfido2). Gated behind //go:build fido2 tag to keep default
builds CGo-free. Nix flake adds sgard-fido2 package variant.

CLI changes: --fido2-pin flag, unlockDEK helper tries FIDO2 first,
add-fido2/encrypt init --fido2 use real hardware, auto-unlock added
to restore/checkpoint/diff for encrypted entries.

Tested manually: add-fido2, add --encrypt, restore, checkpoint, diff
all work with hardware FIDO2 key (touch-to-unlock, no passphrase).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 12:40:46 -07:00
5529fff649 Step 24: DEK rotation.
RotateDEK generates a new DEK, re-encrypts all encrypted blobs, and
re-wraps with all existing KEK slots (passphrase + FIDO2). CLI wired
as `sgard encrypt rotate-dek`. 4 tests covering rotation, persistence,
FIDO2 re-wrap, and requires-unlock guard.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 12:01:57 -07:00
3fabd86150 Step 23: TLS transport for sgardd and sgard client.
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>
2026-03-24 11:57:03 -07:00
c00d9c65c3 Step 22: Shell completion docs for bash, zsh, fish.
Cobra provides built-in sgard completion subcommand — no additional
code needed. README updated with installation instructions for each
shell.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 11:10:28 -07:00
d2bba75365 Step 21: Lock/unlock toggle commands.
garden/lock.go: Lock() and Unlock() toggle the locked flag on
existing tracked entries. Errors on untracked paths. Persists
to manifest.

cmd/sgard/lock.go: sgard lock <path>..., sgard unlock <path>...

6 tests: lock/unlock existing entry, persistence, error on untracked,
checkpoint behavior changes after lock, status changes between
drifted and modified after unlock.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 11:07:40 -07:00
0cf81ab6a1 Add Phase 4-6 roadmap to ARCHITECTURE.md.
Phase 4: TLS transport, DEK rotation.
Phase 5: Multi-repo + per-machine inclusion.
Phase 6: Manifest signing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 11:00:13 -07:00
1eb801fe63 Plan Phase 4: lock/unlock, shell completion, TLS, DEK rotation, FIDO2 hardware, test cleanup.
Steps 21-27. Phase 5 (multi-repo + per-machine) and Phase 6
(manifest signing) noted as future.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 10:57:05 -07:00
11202940c9 Add motivating examples for locked files and --dir to README.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
v2.0.0
2026-03-24 10:14:40 -07:00
0929d77e90 Add locked files and directory-only entries.
Locked files (--lock): repo-authoritative entries. Checkpoint skips
them (preserves repo version). Status reports "drifted" instead of
"modified". Restore always overwrites if hash differs, no prompt.
Use case: system-managed files the OS overwrites.

Directory-only entries (--dir): track directory itself without
recursing. Restore ensures directory exists with correct permissions.
Use case: directories that must exist but contents are managed
elsewhere.

Add refactored to use AddOptions struct (Encrypt, Lock, DirOnly)
instead of variadic bools.

Proto: ManifestEntry gains locked field. convert.go updated.
7 new tests. ARCHITECTURE.md and README.md updated.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 09:56:57 -07:00
7accc6cac6 Step 20: Encryption polish — e2e test, docs, flake.
E2e encryption test: full lifecycle covering init, add encrypted +
plaintext, checkpoint, modify, status (no DEK needed), re-checkpoint,
restore, verify, re-open with unlock, diff, slot management, passphrase
change, old passphrase rejection.

Docs updated:
- ARCHITECTURE.md: package structure (encrypt.go, encrypt_fido2.go,
  encrypt CLI), Garden struct (dek field, encryption methods), auth.go
  descriptions updated for JWT
- README.md: encryption commands table, encryption section with usage
- CLAUDE.md: added jwt/argon2/chacha20 deps, encryption file mentions

flake.nix: vendorHash updated for new deps.

Phase 3 complete.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 09:34:05 -07:00
76a53320c1 Step 19: Encryption CLI, slot management, proto updates.
CLI: sgard encrypt init [--fido2], add-fido2 [--label], remove-slot,
list-slots, change-passphrase. sgard add --encrypt flag with
passphrase prompt for DEK unlock.

Garden: RemoveSlot (refuses last slot), ListSlots, ChangePassphrase
(re-wraps DEK with new passphrase, fresh salt).

Proto: ManifestEntry gains encrypted + plaintext_hash fields. New
KekSlot and Encryption messages. Manifest gains encryption field.

server/convert.go: full round-trip conversion for encryption section
including KekSlot map.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 09:25:20 -07:00
5bb65795c8 Step 18: FIDO2 support with interface and mock.
FIDO2Device interface abstracts hardware interaction (Register, Derive,
Available, MatchesCredential). Real libfido2 implementation deferred;
mock device used for full test coverage.

AddFIDO2Slot: registers FIDO2 credential, derives KEK via HMAC-secret,
wraps DEK, adds fido2/<label> slot to manifest.

UnlockDEK: tries all fido2/* slots first (checks credential_id against
connected device), falls back to passphrase. User never specifies
which method.

6 tests: add slot, reject duplicate, unlock via FIDO2, fallback to
passphrase when device unavailable, slot persistence, encrypted
round-trip unlocked via FIDO2.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 09:15:20 -07:00
3b961b5d8a Step 17: Encryption core — passphrase-only, selective per-file.
Manifest schema: Entry gains Encrypted, PlaintextHash fields.
Manifest gains Encryption section with KekSlots map (passphrase slot
with Argon2id params, salt, and wrapped DEK as base64).

garden/encrypt.go: EncryptInit (generate DEK, wrap with passphrase KEK),
UnlockDEK (derive KEK, unwrap), encryptBlob/decryptBlob using
XChaCha20-Poly1305 with random 24-byte nonces.

Modified operations:
- Add: optional encrypt flag, stores encrypted blob + plaintext_hash
- Checkpoint: detects changes via plaintext_hash, re-encrypts
- Restore: decrypts encrypted blobs before writing
- Diff: decrypts stored blob before comparing
- Status: compares against plaintext_hash for encrypted entries

10 tests covering init, persistence, unlock, add-encrypted, restore
round-trip, checkpoint, status, diff, requires-DEK guard.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 08:50:53 -07:00
582f2116d2 Change sgardd default repo path to /srv/sgard.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 08:40:55 -07:00
529d45f8eb Add Phase 3 encryption plan (Steps 17-20) and update progress.
Step 17: Encryption core — Argon2id KEK, XChaCha20 DEK wrapping,
  selective per-file encryption, manifest schema changes.
Step 18: FIDO2 support — hmac-secret slots, credential_id matching,
  automatic unlock resolution.
Step 19: CLI + slot management — encrypt init/add-fido2/remove-slot/
  list-slots/change-passphrase, proto updates.
Step 20: Polish — e2e encrypted push/pull test, doc updates.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 08:35:29 -07:00
f6bdb93066 KEK slots: named map with passphrase + fido2/<label> convention.
Slots are a map keyed by user-chosen label. One passphrase slot
(universal fallback), zero or more fido2/<label> slots (default to
hostname, overridable via --label).

FIDO2 slots carry credential_id to match connected devices without
prompting for touch. Unlock tries all fido2/* slots first, falls
back to passphrase.

CLI: add-fido2 [--label], remove-slot, list-slots, change-passphrase.
New FIDO2 slots propagate to server on next push.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 08:32:55 -07:00
e24b66776c Encryption config lives in the manifest, syncs with push/pull.
Wrapped DEKs and salts stored inline as base64 in the manifest's
encryption section. No separate files (encryption.yaml, salt files,
dek.enc.*) — the manifest is fully self-contained.

Pulling to a new machine gives you everything needed to decrypt.
Server never has the DEK. FIDO2 cross-machine note: device-bound
hmac-secret requires add-fido2 on each machine; passphrase fallback
enables cross-machine decryption.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 08:23:25 -07:00
079b235c9d Refine encryption: FIDO2 preferred with passphrase fallback.
Automatic unlock resolution: try FIDO2 first (no typing, just touch),
fall back to passphrase if device not present. User never specifies
which method — sgard reads encryption.yaml and walks sources in order.

encrypt init --fido2 creates both sources (FIDO2 primary + passphrase
fallback) to prevent lockout on FIDO2 key loss. Separate salt files
per KEK source.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 08:18:51 -07:00
4d9e156eea Update encryption design: selective per-file encryption, punt signing.
Encryption is per-file (--encrypt flag on add), not per-repo. A repo
can have a mix of encrypted and plaintext blobs. Commands that only
touch plaintext entries never prompt for the DEK.

Manifest signing deferred — the trust model (which key signs, how do
pulling clients verify across multiple machines) needs proper design.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 08:06:27 -07:00
c6b92a70b1 Document encryption design in ARCHITECTURE.md.
Two-layer key hierarchy: DEK (random, encrypts blobs) wrapped by
KEK (derived from passphrase via Argon2id or FIDO2 hmac-secret).

XChaCha20-Poly1305 for both blob encryption and DEK wrapping.
Post-encryption hashing (manifest hash = SHA-256 of ciphertext).
Plaintext hash stored separately for efficient status checks.
Multiple KEK sources per repo. Server never sees the DEK.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 07:36:44 -07:00
edef642025 Implement JWT token auth with transparent auto-renewal.
Replace per-call SSH signing with a two-layer auth system:

Server: AuthInterceptor verifies JWT tokens (HMAC-SHA256 signed with
repo-local jwt.key). Authenticate RPC accepts SSH-signed challenges
and issues 30-day JWTs. Expired-but-valid tokens return a
ReauthChallenge in error details (server-provided nonce for fast
re-auth). Authenticate RPC is exempt from token requirement.

Client: TokenCredentials replaces SSHCredentials as the primary
PerRPCCredentials. NewWithAuth creates clients with auto-renewal —
EnsureAuth obtains initial token, retryOnAuth catches Unauthenticated
errors and re-authenticates transparently. Token cached at
$XDG_STATE_HOME/sgard/token (0600).

CLI: dialRemote() helper handles token loading, connection setup,
and initial auth. Push/pull/prune commands simplified to use it.

Proto: Added Authenticate RPC, AuthenticateRequest/Response,
ReauthChallenge messages.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 00:52:16 -07:00
b7b1b27064 Refine auth flow: server-provided reauth challenge for expired tokens.
Two rejection paths: expired-but-valid tokens get a ReauthChallenge
with a server-generated nonce (fast path, saves a round trip).
Invalid/corrupted tokens get plain Unauthenticated (client falls back
to full self-generated auth flow).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 00:40:26 -07:00
66af104155 Document JWT token auth design in ARCHITECTURE.md.
Two-layer auth: SSH key signing to obtain a 30-day JWT, then
token-based auth for all subsequent requests. Auto-renewal is
transparent — client interceptor catches Unauthenticated, re-signs,
caches new token, retries. Server is stateless (JWT signed with
repo-local secret key). Token cached at $XDG_STATE_HOME/sgard/token.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 00:36:58 -07:00
92d64d5540 Fix doc inconsistencies between README and ARCHITECTURE.
- ARCHITECTURE.md: move mirror/prune to local command table, fix
  remove description (prune cleans blobs, not checkpoint), fix
  Phase 2 section to only list remote commands
- README.md: add --force to mirror down, fix prune --remote
  description, build instructions include both binaries

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 00:26:35 -07:00
5f1bc4e14c Step 16: Polish — docs, flake, goreleaser, e2e test.
Phase 2 complete.

ARCHITECTURE.md: full rewrite covering gRPC protocol, SSH auth,
updated package structure, all Garden methods, design decisions.
README.md: add remote sync section, mirror/prune commands, sgardd usage.
CLAUDE.md: add gRPC/proto/x-crypto deps, server/client/sgardpb packages.
flake.nix: build both sgard + sgardd, updated vendorHash.
goreleaser: add sgardd build target.
E2e test: full push/pull cycle with SSH auth between two clients.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 00:10:04 -07:00
94963bb8d6 Step 15: CLI wiring, prune, and sgardd daemon.
Local prune: garden.Prune() removes orphaned blobs. 2 tests.

CLI commands: sgard push, sgard pull (with SSH auth via --ssh-key
or ssh-agent), sgard prune (local by default, remote with --remote).

Server daemon: cmd/sgardd with --listen, --repo, --authorized-keys
flags. Runs gRPC server with optional SSH key auth interceptor.

Root command gains --remote and --ssh-key persistent flags with
resolveRemote() (flag > env > repo/remote file).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 00:03:51 -07:00
4b841cdd82 Step 14: SSH key auth for gRPC.
Server: AuthInterceptor parses authorized_keys, extracts SSH signature
from gRPC metadata (nonce + timestamp signed by client's SSH key),
verifies against authorized public keys with 5-minute timestamp window.

Client: SSHCredentials implements PerRPCCredentials, signs nonce+timestamp
per request. LoadSigner resolves key from flag, ssh-agent, or default paths.

8 tests: valid auth, reject unauthenticated, reject unauthorized key,
reject expired timestamp, metadata generation, plus 2 integration tests
(authenticated succeeds, unauthenticated rejected).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 23:58:09 -07:00
525c3f0b4f Step 13: Client library with Push, Pull, and Prune.
Client orchestrates the two-step push/pull protocol: manifest exchange
followed by chunked blob streaming. Push detects server-newer (returns
ErrServerNewer) and up-to-date states. Pull computes missing blobs
locally and streams only what's needed. Prune delegates to server RPC.

6 integration tests via in-process bufconn server.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 23:53:03 -07:00
0078b6b0f4 Steps 12 & 12b: gRPC server and directory recursion + mirror.
Step 12: GardenSync gRPC server with 5 RPC handlers — PushManifest
(timestamp comparison, missing blob detection), PushBlobs (chunked
streaming, manifest replacement), PullManifest, PullBlobs, Prune.
Added store.List() and garden.ListBlobs()/DeleteBlob() for prune.
In-process tests via bufconn.

Step 12b: Add now recurses directories (walks files/symlinks, skips
dir entries). Mirror up syncs filesystem → manifest (add new, remove
deleted, rehash changed). Mirror down syncs manifest → filesystem
(restore + delete untracked with optional confirm). 7 tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 23:48:04 -07:00
19217ec216 Merge branch 'worktree-agent-a0166844'
# Conflicts:
#	garden/garden.go
2026-03-23 23:44:30 -07:00