Enables glob workflows like `sgard add ~/.config/mcp/services/*` to
pick up new files without failing on ones already tracked.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
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>