Commit graph

77 commits

Author SHA1 Message Date
Brian Majewski
50b73083f1 feat: init born-multi recipients.txt + which recipients line (EGB-283)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 13:03:20 -07:00
Brian Majewski
cad5b66f77 feat: secrets recipients rm with lockout guards (EGB-283)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 12:56:50 -07:00
Brian Majewski
4ba0234bd7 fix: harden recipients add --name validation (EGB-283)
Require a non-empty argument after --name (dies if it is the last token)
and reject whitespace-only labels that would write a blank comment line.
Two regression tests added to test/recipients.bats.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 12:53:27 -07:00
Brian Majewski
c262661030 feat: secrets recipients add (EGB-283)
Add `secrets recipients add <age1...> [--name <label>]`: validates the
age key and optional display name, bootstraps recipients.txt with the
local pubkey on a legacy store (keeping the operator as a recipient),
rejects duplicates, appends the new key (with optional name comment),
then re-encrypts the entire store to the updated recipient list.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 12:50:00 -07:00
Brian Majewski
a10f390519 feat: shared _reencrypt_all + reencrypt cmd + dual rekey (EGB-283)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 12:44:44 -07:00
Brian Majewski
649aa78063 feat: secrets recipients list (EGB-283) 2026-06-24 12:39:52 -07:00
Brian Majewski
14cf86fba9 docs: plan fixups from Task 1 review (blob path, dangling-symlink fix to Task 8)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 12:37:55 -07:00
Brian Majewski
0f9de1c2fd feat: multi-recipient encrypt core + recipients.txt (EGB-283)
- Add RECIPIENTS_FILE_NAME / RECIPIENTS_FILE constants; update resolve_store to re-derive RECIPIENTS_FILE after store resolution.
- Add _validate_age_recipient (native age1 X25519 key format check, injection rail).
- Add RECIPIENT_ARGS global array and _load_recipients (absent → single pubkey legacy path; present → parse+validate recipients.txt, refuse symlink, die on bad/empty).
- Rewire all 5 push encrypt sites (push_dir_to_project, cmd_push inline, push_external_files ×2, cmd_push_workspaces) to use RECIPIENT_ARGS; drop pubkey threading from push_dir_to_project and push_external_files signatures.
- New test/recipients.bats (4 tests): legacy single-key, multi-recipient decrypt, invalid key rejection, symlink rejection.
- Fix test/test_helper.bash: set GIT_AUTHOR/COMMITTER env vars so git commit works with isolated $HOME.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 12:33:05 -07:00
Brian Majewski
3a7eea5529 docs: multi-recipient age encryption implementation plan (EGB-283)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 12:20:32 -07:00
Brian Majewski
9e2a563059 docs: multi-recipient age encryption design spec (EGB-283)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 12:12:14 -07:00
EGBT Technologies
836b418a12 Merge pull request 'v0.6.1.0 feat: secrets migrate guided flow (manifest-free + --status, EGB-710)' (#7) from brian/egb-710-secrets-make-migrate-a-guidedinteractive-flow-not-a-dead-end into main 2026-06-08 18:06:18 +02:00
Brian Majewski
3ac2a86a94 docs: fix stale manifest-dependency comments in migrate (EGB-710) 2026-06-08 08:26:16 -07:00
Brian Majewski
5729d84e75 docs: migrate --status + manifest-free copy-forward; bump 0.6.1.0 (EGB-710) 2026-06-08 08:01:44 -07:00
Brian Majewski
469b08203b feat: secrets migrate --status surveys per-project v2 readiness (EGB-710) 2026-06-08 07:50:32 -07:00
Brian Majewski
e75627deff test: migrate twins undeclared store blobs (finalize-consistency, EGB-710) 2026-06-08 07:18:22 -07:00
Brian Majewski
c121982dcd fix: migrate copy-forward is manifest-free, no dead-end on legacy projects (EGB-710) 2026-06-08 07:08:08 -07:00
Brian Majewski
679ddbc1b7 docs: EGB-710 implementation plan (migrate guided flow) 2026-06-08 06:46:50 -07:00
EGBT Technologies
633d19e5f7 Merge pull request 'v0.6.0.1 feat: secrets which prints manifest version (EGB-700)' (#6) from brian/egb-700-which-manifest-version into main 2026-06-08 15:23:18 +02:00
Brian Majewski
2549832f0e docs: sync test counts and which manifest-version note (EGB-700)
- README: test suite total 236 -> 237
- CLAUDE.md: manifest.bats 77 -> 78 tests
- CLAUDE.md: note `secrets which` now prints the .secrets.json schema
  version in its manifest header line alongside `format: vN`

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 06:21:26 -07:00
Brian Majewski
6a84846e64 chore: bump version and changelog (v0.6.0.1)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 06:19:57 -07:00
Brian Majewski
94ee6ec9a2 feat: secrets which prints the manifest version (EGB-700)
Completes EGB-700 — the store-format line (format: vN) shipped in v0.6.0.0;
this adds the manifest schema version to the `which` manifest header
(version N, validated == MANIFEST_VERSION by _check_manifest_file). Now a
single `secrets which` surfaces both the store format and the manifest
version for the dual-format debugging window. 1 bats test. Suite 237/237.
2026-06-08 06:02:11 -07:00
EGBT Technologies
eaa08e8561 Merge pull request 'v0.6.0.0 feat: store-format-v2 migration (EGB-703)' (#5) from brian/egb-703-store-format-v2-migration into main 2026-06-08 02:00:34 +02:00
Brian Majewski
c4da47f320 docs: correct test counts for migrate.bats (236 total)
README.md: test suite count 210 → 236, "both files" → "three files".
CLAUDE.md: testing command lists migrate.bats; Project Structure tree
adds migrate.bats (EGB-703 store-format-v2 migration, 26 tests).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 16:57:53 -07:00
Brian Majewski
cefa06280e chore: bump version and changelog (v0.6.0.0)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 16:56:27 -07:00
Brian Majewski
c10e89fb51 fix: pre-landing review for store-format-v2 (verify-window orphan, copy-forward push, --force, comment)
Pre-landing review (1 critical, 4 informational):
- CRITICAL: per-project `secrets verify` during the migration window flagged
  the freshly-written v2 twin as a spurious orphan and exited 1 (store still
  reads v1, so verify's expected set only held the .gradle-properties.age name).
  Breaks the documented migrate->verify->finalize workflow and CI. Fix: verify's
  orphan set now accounts for BOTH suffix forms of a properties external, so the
  twin is never a false orphan mid-migration. Regression test added.
- Copy-forward now pushes the twins (mirrors push/rekey) so a --finalize on
  another machine sees them; previously twins were local-only until finalize,
  a multi-machine footgun.
- Dropped the undocumented `--force` alias (keep `--yes`).
- Clarified the EGB-700 comment (which-format line, folded into EGB-703).

Deferred to EGB-701: the two finalize find-walks over *.gradle-properties.age
could collapse to one pass. Full suite 236/236.
2026-06-07 16:47:17 -07:00
Brian Majewski
5521d978a3 test: coverage for migrate gap branches (dotenv-only finalize, positional-arg die, already-v2 finalize, strict marker parse)
Coverage audit found 4 untested branches (all single-test fills): finalize on
a v1 dotenv/file-only store (v1count==0 stamp path — a real user scenario),
the migrate positional-arg die, finalize on an already-v2 store, and the
_store_format strict-parse defense (garbage marker reads as v1). Full suite 235/235.
2026-06-07 16:35:26 -07:00
Brian Majewski
2192b5a2df fix: tighten _store_format parse + correct finalize crash-orphan comment (EGB-703 review)
Self-review of the data-safety paths (verdict SHIP-SAFE) flagged two
non-blocking nits, both fixed:
- _store_format used `tr -dc '0-9'` which read garbage like "v2"/"x2x" as v2.
  Tightened to a strict exact match (modulo line endings) so only "2" reads as
  v2; anything else falls back to v1, the safe default.
- The stamp-before-delete comment claimed a re-run "cleans" crash-orphaned v1
  blobs; it doesn't (finalize early-returns once the store is v2). Corrected to
  note the orphans are harmless and `verify` flags them for manual removal.
2026-06-07 16:18:16 -07:00
Brian Majewski
e2ad661da5 feat: store-format-v2 self-describing migration (EGB-703, folds in EGB-700)
Stage 2 of EGB-677. Makes the store self-describing and unifies the legacy
external `properties` blob suffix, via a copy-forward migration that never
destroys data until an explicit, gated finalize.

Scope decision (see eureka): the EGB-677 CEO plan's "flatten dotenv blobs to
basename" was dropped as LOSSY — it discards the restore relpath that makes
the store self-describing and adds basename collisions. Engineering analysis
(4 parallel design agents) showed the store is already relpath-self-describing;
the only real v1→v2 delta is the `properties` suffix. This implements the
minimal, safe v2 that achieves the epic's self-describing goal.

What's added:
- `.secrets-format` marker (committed, one line `2`). Absence ⇒ v1 (every
  pre-EGB-703 store). `_store_format()` reads it; `init` stamps fresh stores
  born-v2. `secrets which` prints `format: vN` (EGB-700 folded in).
- `_external_blob_suffix(type)` — single source of truth for the external
  suffix (v2: gradle-properties → properties; file unchanged). push/pull/verify
  all route through it, so v1 and v2 stores never disagree on blob location.
- `secrets migrate` — per-project copy-forward (writes `.properties.age` twins
  beside v1 blobs; idempotent; needs the project manifest), `--dry-run`
  (reports old→new, writes nothing), `--finalize` (store-wide, the only
  destructive step: gates on `verify --all` green + every v1 blob twinned,
  cuts a `pre-v2-migrate-<sha>` recovery tag, stamps the marker, then drops v1
  blobs; refuses without `--yes`/operator confirmation).

rekey and verify --all stay format-agnostic (recursive find walk) — no change.

21 new bats tests (test/migrate.bats): marker/born-v2, format-aware suffix,
v1 back-compat, dry-run, copy-forward idempotency, no-manifest die, finalize
gates (verify-not-green refusal, untwinned refusal, recovery tag, confirmation),
and full v1→window→finalize round-trip. Updated 4 existing tests for the
born-v2 suffix. Full suite 231/231, bash 3.2 clean.
2026-06-07 16:07:31 -07:00
EGBT Technologies
144ff3692b Merge pull request 'v0.5.0.0 feat: secrets verify integrity check (EGB-698)' (#4) from brian/egb-698-secrets-verify-command into main 2026-06-08 00:34:02 +02:00
Brian Majewski
e33bc272d2 docs: surface secrets verify in troubleshooting, refresh test counts (v0.5.0.0)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 15:31:30 -07:00
Brian Majewski
d170fb03db chore: bump version and changelog (v0.5.0.0)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 15:29:01 -07:00
Brian Majewski
414c02b902 fix: pre-landing review fixes for verify (test assertions, ekeys discard, double-report, docs)
Pre-landing review (0 critical, all informational) auto-fixes:
- Tighten external-corrupt test to assert the decrypt-fail message, not any
  external finding (was *"external"*, now *"does not decrypt"*).
- Pin the verified-count in the nested+external happy-path test so a silent
  under-count (exit 0 while skipping a blob) is caught.
- Account for an unsafe dotenv entry in `expected` so a matching stray blob
  isn't double-reported as both unsafe and orphan.
- Discard the unused external `keys` read field (read -r etype epath _).
- Document the optional [project] positional in the README verify row.

Deferred to EGB-701 (stage-2 dedup): the external blob-path literal and the
find-walk overlap with cmd_rekey/cmd_list. Full suite 210/210.
2026-06-07 15:22:58 -07:00
Brian Majewski
33aad4f89a test: coverage for verify gap branches (external decrypt-fail, unsafe path, unknown flag, malformed manifest, empty --all)
Coverage audit found 5 untested branches in cmd_verify (all single-test
fills, no logic defects): external blob decrypt-failure (only the missing
case was covered), the rail-skip finding for an unsafe dotenv path in the
manifest, the unknown-flag die, a malformed manifest through the verify
entry point, and the empty-store 'verify --all' no-op. Full suite 210/210.
2026-06-07 15:14:54 -07:00
Brian Majewski
52528f2e06 feat: secrets verify — manifest↔store consistency + decrypt integrity (EGB-698)
Read-only integrity check, the safety net for the stage-2 store migration.

Default mode (current project) cross-checks $PWD/.secrets.json against the
store both ways — declared-but-missing blobs and orphaned blobs (no manifest
entry) — and decrypt-tests every dotenv + external blob with the current key,
streaming plaintext to /dev/null so nothing is ever written to disk. `verify
--all` decrypt-tests every blob in every project (integrity only; the store
carries no manifests, so consistency can't be checked store-wide). Both
recurse the whole project tree (find -type f), the same walk rekey/list use,
so nested manifest blobs are covered. Exits non-zero on any finding so it can
gate `migrate --finalize` and CI.

12 bats tests (clean, nested+external, missing blob, decrypt failure, orphan,
missing external, no-manifest die, symlink refusal, --all clean/corrupt/orphan,
nested decrypt failure). Full suite 205/205. bash 3.2 clean.
2026-06-07 14:55:42 -07:00
EGBT Technologies
fb71b956da Merge pull request 'v0.4.0.0 feat: .secrets.json manifest-driven sync (EGB-677 stage 1)' (#3) from brian/egb-677-unify-storage-model-manifest-driven-sync-store-format-v2 into main 2026-06-07 22:57:21 +02:00
Brian Majewski
a35bff8d4b docs: correct manifest.bats and total test counts for v0.4.0.0
CLAUDE.md: manifest.bats 58 -> 60 tests (actual @test count).
README.md: total 191 -> 193 tests across both files.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 13:53:05 -07:00
Brian Majewski
30c203cafd chore: bump version and changelog (v0.4.0.0)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 13:50:51 -07:00
Brian Majewski
588f290dcc chore: security review policy + operator runner, doc updates (EGB-677 stage 1)
Add .ship-policy.json (opts out AI adversarial/red-team/security-specialist
review; requires local operator sign-off) and test/run-security.sh (the
operator-local security regression subset). Document the policy in CLAUDE.md
and README, fix stale test counts (manifest.bats 41->58, total 174->191), and
update the storage-recursion note to reflect rekey/list now walking the full
project tree.
2026-06-07 13:12:39 -07:00
Brian Majewski
c6ea724ddb fix: rekey and list recurse into nested manifest blobs (EGB-677 stage 1)
Pre-landing review (testing + checklist specialists, reproduced) caught a
data-loss bug: cmd_rekey's decrypt/re-encrypt globs were non-recursive and
only special-cased external/. Nested manifest dotenv blobs
(<project>/<relpath>.age, new this branch) were never visited, so after a key
rotation they stayed encrypted under the discarded old key = permanently
undecryptable. cmd_list had the same blind spot (cosmetic: nested entries
invisible in listings).

Both now walk the entire project tree with `find -type f` (bash 3.2 safe,
includes dotfiles natively), unifying top-level / nested / external blobs into
one recursive pass and dropping the now-redundant external/ special-casing.

Regression tests: nested-blob rekey round-trip (survives rotation) + list
shows nested entry. Full suite 193/193.
2026-06-07 13:12:27 -07:00
Brian Majewski
5489f89446 test: coverage backfill — which UNSAFE marker, malformed-manifest add, dry-run declared list, frozen absorb suppression, file-type absorb round-trip, external dedup 2026-06-07 10:17:07 -07:00
Brian Majewski
ecc4f23494 test: coverage for stage-1 gap paths (json rail warns, symlink legacy, directed errors, dry-run/which branches) 2026-06-07 09:40:43 -07:00
Brian Majewski
89e851278b feat: jq gating, platform-aware install hints, stage-1 docs (EGB-677 stage 1)
- jq required only when .secrets.json exists or is being written;
  manifest-less projects run jq-free (features skipped with a notice) —
  clone-and-run for v1 users survives (no-jq fixture excludes /usr/bin,
  macOS ships jq there now)
- check_cmd: platform-aware hints (brew/apt-get/dnf/generic) instead of
  hardcoded brew — correct guidance on Linux/CI
- cmd_help: add command, push flags, manifest section with example
- README: manifest section, external files rewritten around
  .secrets.json (legacy .secrets-files documented as absorbed),
  troubleshooting entries, command table, test instructions
- CLAUDE.md: manifest architecture notes, bash-3.2 '[[ ]] || false'
  testing convention, project structure refresh
2026-06-07 08:58:16 -07:00
Brian Majewski
0049584d9b feat: manifest-driven pull — nested restore, restore-time rail, empty no-op (EGB-677 stage 1)
- pull with .secrets.json restores exactly the declared entries (nested
  paths get mkdir -p); stray store blobs are not restored
- dotenv rail re-runs at restore time: unsafe entries warn+skip (pull
  never dies on one bad entry), missing blobs warn with a directed hint
- empty manifest = warn no-op instead of a confusing 'not found' death
- manifest-less projects keep the legacy glob pull verbatim
2026-06-07 08:49:02 -07:00
Brian Majewski
d7e1400487 feat: external entries via .secrets.json + legacy absorb + properties rail (EGB-677 stage 1)
- .secrets.json external[] drives push/pull: type 'properties' (alias of
  gradle-properties; blob suffix stays legacy-compatible in stage 1) and
  type 'file'; same charset rails as the legacy parser
- push absorbs uncovered .secrets-files entries into the manifest
  (idempotent, gradle-properties → properties) with a delete hint
- pull: manifest wins entirely; a coexisting .secrets-files warns as
  superseded instead of being silently ignored
- basename rail generalized: properties targets must end '.properties'
  (was exact 'gradle.properties') — rc files/gitconfig still blocked;
  EGB-531 wrong-basename test updated for the sanctioned change
2026-06-07 08:44:02 -07:00
Brian Majewski
884da0965c feat: manifest-aware push — generator auto-add, autoAdd toggle, --frozen/--dry-run (EGB-677 stage 1)
- push syncs FROM the manifest; v1 store layout unchanged (nested
  entries land at <project>/<relpath>.age, same shape -w always used)
- discovery (root globs + quiet package.json workspace re-scan when a
  manifest exists) feeds the manifest as a generator; new files auto-add
  with ==> notice + undo guidance
- options.autoAdd committed toggle (default ON when absent); explicit
  false warns on undeclared files instead of enrolling them
- push --frozen: declared-only for one invocation; push --dry-run:
  reports would-add/would-sync, touches nothing
- bootstrap ordering: manifest written only after >=1 blob encrypts
- declared-but-missing warns and continues; unsafe manifest path dies
- jq // falsy gotcha: explicit autoAdd:false compared directly
2026-06-07 08:32:17 -07:00
Brian Majewski
18018dbd3b feat: .secrets.json manifest core — add command, rails, canonical form (EGB-677 stage 1)
- secrets add <path>: explicit manifest writer, idempotent, atomic write
- _validate_dotenv_rel_path: project-relative confinement rail (no .. /
  absolute / shell metas; @ allowed for npm-scoped workspace dirs)
- _check_manifest_file: refuses symlinks, malformed JSON (jq error with
  file named), unsupported schema versions (directed upgrade error)
- canonical serialization: jq --sort-keys + sorted/deduped dotenv —
  add order produces byte-identical manifests
- which: validates + summarizes the manifest (doubles as linter)
- jq required only when a manifest exists/is written
2026-06-07 08:24:35 -07:00
Brian Majewski
6dbc4e0d01 test: make [[ ]] assertions effective under bash 3.2 (EGB-677 precursor)
bats on macOS runs under system bash 3.2, where a failing [[ ]] compound
command mid-test does NOT trip the ERR trap — every mid-test
[[ "$output" == *...* ]] assertion in the suite was decorative.
Append '|| false' to all 123 standalone [[ ]] assertion lines so failures
become plain-command failures, which bats catches. Full suite still green
(133/133) — no latent false positives were hiding.
2026-06-07 08:21:29 -07:00
Brian Majewski
a3c71f293b Merge branch 'feat/external-file-type' (v0.3.0.0) 2026-06-07 06:44:40 -07:00
Brian Majewski
a314cb1fb1 docs: CLAUDE.md — record the 'file' external type (EGB-652) 2026-06-07 06:44:22 -07:00
Brian Majewski
e0474f3ce9 feat: 'file' external type — whole-file sync for .secrets-files (EGB-652)
Built for the Beacon Android upload keystore: binary files outside the
project root can now ride the encrypted store.

- manifest: 'file <path>' (no keys; keys present = rejected loudly)
- push: encrypts the file verbatim (age is binary-safe)
- pull: restores next to target (atomic same-fs mv), mode 600, TOCTOU
  symlink recheck, divergent existing target backed up to .secrets-bak
- _validate_external_target_path parameterized by type (basename
  restriction stays gradle-properties-only; $HOME/../symlink rails apply
  to both)
- README/help/CLAUDE docs + 7 bats tests (133/133 pass)
2026-06-07 06:43:45 -07:00