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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>
Add now recursively walks directories instead of creating a single
"directory" type entry. Extract addEntry helper for reuse. Implement
MirrorUp (sync filesystem state into manifest) and MirrorDown (sync
manifest state to filesystem with untracked file cleanup). Add CLI
mirror command with up/down subcommands.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Step 10: GetManifest, BlobExists, ReadBlob, WriteBlob, ReplaceManifest
accessor methods on Garden. 5 tests.
Step 11: ManifestToProto/ProtoToManifest conversion functions in
server package with time.Time <-> timestamppb handling. Round-trip
test covering all 3 entry types.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Expose GetManifest, BlobExists, ReadBlob, WriteBlob, and
ReplaceManifest on *Garden to support future gRPC and higher-level
operations without breaking encapsulation. Includes 5 unit tests.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>