Skip to content

Conversation

@flyingrobots
Copy link
Member

@flyingrobots flyingrobots commented Feb 8, 2026

Summary

  • Vault — GC-safe ref-based storage via refs/cas/vault. Assets indexed by slug; git gc can no longer silently discard stored data. VaultService domain service with GitRefPort/GitRefAdapter for ref operations.
  • CLI vault subcommandsvault init, vault list, vault info, vault remove, vault history. store --tree auto-vaults. restore uses --oid/--slug flags (breaking: no more positional arg).
  • v3.0.0 release housekeeping — version bumps (package.json, jsr.json), CHANGELOG stamped, README install section + "What's new in v3.0.0" blurb.

Test plan

  • 474 unit tests pass (npm test)
  • ESLint clean (npx eslint .)
  • CI: Node/Bun/Deno matrix passes
  • After merge: tag v3.0.0, push tag, CI publishes to npm + JSR

Summary by CodeRabbit

  • New Features

    • Added Vault: a ref-based storage system with optional encryption and slug-based asset indexing.
    • Added vault CLI commands for management: init, list, info, remove, and history.
  • Breaking Changes

    • Restore command now requires --oid or --slug flags; positional tree-oid argument removed.
  • Documentation

    • Updated guides and API documentation with vault feature descriptions and usage examples.

Add vault subsystem backed by refs/cas/vault with init, add, list,
remove, resolve, and metadata APIs. Purge completed milestones M1–M7
from ROADMAP (3,153 → 1,675 lines).
Extract all vault logic from facade into VaultService with proper
hexagonal architecture. Add GitRefPort/GitRefAdapter for ref/commit
operations. Add vault info and vault history CLI commands. Add vault
integration tests.
@coderabbitai
Copy link

coderabbitai bot commented Feb 8, 2026

Warning

Rate limit exceeded

@flyingrobots has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 18 minutes and 42 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

This PR introduces a Vault subsystem: a ref-based, GC-safe vault index with optional encryption and CLI management; it adds VaultService, GitRefPort/GitRefAdapter, facade/vault APIs, CLI vault commands, docs/tests, and removes AUDITS.md.

Changes

Cohort / File(s) Summary
Documentation
AUDITS.md, CHANGELOG.md, GUIDE.md, README.md, docs/API.md
Removed AUDITS.md; added and reorganized Vault documentation across CHANGELOG, GUIDE, README, and API docs (new Vault types, methods, and error codes).
Facade / Public Surface
index.js, index.d.ts
Added public exports VaultService and GitRefAdapter; added ContentAddressableStore vault accessor and methods (getVaultService, initVault, addToVault, listVault, removeFromVault, resolveVaultEntry, getVaultMetadata), and exposed VAULT_REF.
Domain Service
src/domain/services/VaultService.js
New VaultService: slug validation, metadata parsing/validation (including encryption/KDF), read/write state, commit creation, CAS-safe ref updates with retry, add/list/remove/resolve entry APIs.
Ports & Adapters
src/ports/GitRefPort.js, src/infrastructure/adapters/GitRefAdapter.js, src/infrastructure/adapters/GitPersistenceAdapter.js
Added abstract GitRefPort; implemented GitRefAdapter wrapping plumbing with a resilience policy; adjusted GitPersistenceAdapter default policy to a 30s timeout (no retry).
CLI
bin/git-cas.js
Extended CLI with vault subcommands (init, list, info, remove, history) and vault-aware store/restore flows; added key derivation/resolution helpers and vault encryption handling.
Tests
test/integration/vault.test.js, test/integration/vault-cli.test.js, test/unit/vault/VaultService.test.js
Added extensive unit and integration tests for VaultService and CLI (unencrypted/encrypted init, store/resolve/restore, listing, removal, CAS retry, metadata validation).
Config & Metadata
docker-compose.yml, package.json, jsr.json
Added Git author/committer env vars to docker-compose; bumped package version to 3.0.0.

Sequence Diagram

sequenceDiagram
    participant Client as CLI/Client
    participant CAS as ContentAddressableStore
    participant Vault as VaultService
    participant GitRef as GitRefAdapter
    participant GitPlumb as Git Plumbing
    participant Crypto as Crypto Port

    Client->>CAS: store(file, { --vault-passphrase? })
    CAS->>Vault: addToVault({ slug, treeOid })
    Vault->>Crypto: deriveKey(metadata?, passphrase?)
    Crypto-->>Vault: encryptionKey
    Vault->>GitRef: resolveRef(refs/cas/vault)
    GitRef->>GitPlumb: execute(rev-parse ...)
    GitPlumb-->>GitRef: commitOid
    Vault->>Vault: readState() / validateSlug / prepare tree entries
    Vault->>GitRef: createCommit({ treeOid, parentOid, message })
    GitRef->>GitPlumb: execute(commit-tree ...)
    GitPlumb-->>GitRef: newCommitOid
    Vault->>GitRef: updateRef({ ref, newOid, expectedOldOid })
    GitRef->>GitPlumb: execute(update-ref ...)
    GitPlumb-->>GitRef: success
    Vault-->>CAS: { commitOid }
    CAS-->>Client: { treeOid, commitOid }

    Client->>CAS: restore({ --slug | --oid }, { --vault-passphrase? })
    CAS->>Vault: resolveVaultEntry({ slug })
    Vault->>GitRef: resolveRef/resolveTree as needed
    Vault-->>CAS: treeOid
    CAS->>Crypto: deriveKey(metadata?, passphrase?)
    Crypto-->>CAS: decryptionKey
    CAS-->>Client: decrypted content
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I hopped through refs and found a vault,

slugs tucked safe where commits exalt.
Keys are woven, retries hum—
a little rabbit says "well done!" 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 61.90% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title "v3.0.0 — Vault" clearly identifies the main addition (Vault subsystem) and version release, accurately reflecting the core changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch vault

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Fix all issues with AI agents
In `@bin/git-cas.js`:
- Around line 47-62: resolveEncryptionKey currently returns undefined silently
when opts.vaultPassphrase is provided but metadata.encryption is falsy; update
resolveEncryptionKey to emit a clear warning to stderr (e.g., via console.warn
or process.stderr.write) in the branch after const metadata = await
cas.getVaultMetadata() when metadata?.encryption is not set, stating that the
provided --vault-passphrase will be ignored because the vault is not encrypted;
keep the existing return undefined behavior but ensure the message references
opts.vaultPassphrase, cas.getVaultMetadata, and metadata.encryption so users
understand why their passphrase had no effect.

In `@docs/API.md`:
- Line 1384: Update the API docs row for the VAULT_METADATA_INVALID error to
reference the actual exported method name: change the "Thrown By" value from
`_readVaultState()` to `readState()` so it matches the VaultService
implementation (readState) and avoids confusion for consumers; ensure any other
doc references using `_readVaultState()` are replaced with `readState()` for
consistency.
- Line 734: The docs import uses an invalid subpath "@git-stunts/cas/vault";
either change the example to a valid exported path (e.g., import VaultService
from '@git-stunts/cas/service' or import from the package root '@git-stunts/cas'
if the root re-exports VaultService) or add the "./vault" subpath to
package.json exports; update the line referencing "VaultService" accordingly so
it matches one of the existing exports (".", "./service", or "./schema").
- Around line 492-497: The fenced code block that displays "refs/cas/vault →
commit → tree" and the subsequent tree entries is missing a language identifier;
update the opening fence from ``` to ```text (i.e., add the language identifier
"text") so markdownlint MD040 is satisfied and the block is treated as plain
text.

In `@GUIDE.md`:
- Around line 975-983: The fenced code block under the "Vault Tree Structure"
section (the block containing "refs/cas/vault → commit → tree" and the file/tree
lines) is missing a language specifier; update the opening fence from ``` to
```text so the block is tagged as plain text (i.e., change the fenced block that
wraps the refs/cas/vault diagram to use ```text).

In `@src/domain/services/VaultService.js`:
- Around line 104-123: The metadata validator (static `#validateMetadata`)
currently requires cipher, kdf.algorithm and kdf.salt but omits kdf.keyLength;
update `#validateMetadata` to also require kdf.keyLength (matching VaultMetadata
and `#buildEncryptionMeta`) by checking it exists and is a positive integer (or
valid number) and include it in the thrown CasError payload when missing/invalid
so manually edited .vault.json files fail validation early.
🧹 Nitpick comments (8)
test/integration/vault.test.js (2)

184-220: Encrypted vault tests only cover init/metadata — no encrypted store→restore round-trip.

The encrypted vault block verifies KDF metadata storage and the re-init guard, but doesn't exercise the full encrypted workflow: store with --vault-passphrase → add to vault → resolve → restore with passphrase → assert buffer equality. This would validate that deriveVaultKey and the end-to-end encrypted flow work through the real stack.


47-52: tempFile dirs leak if a test assertion fails before the rmSync call.

Minor concern since these tests run in Docker, but you could move cleanup to afterAll/afterEach or collect dirs for bulk cleanup. Not blocking.

bin/git-cas.js (3)

196-197: Vault passphrase is visible in the process argument list.

--vault-passphrase <pass> passes the secret as a CLI argument, which is visible via ps, /proc/*/cmdline, shell history, etc. Consider supporting --vault-passphrase-stdin or an environment variable (GIT_CAS_VAULT_PASSPHRASE) as a more secure alternative. Not blocking for this PR, but worth tracking.

Also applies to: 159-159


275-294: vault history hardcodes refs/cas/vault instead of reusing VaultService.VAULT_REF.

Line 284 uses the string literal 'refs/cas/vault' rather than importing the constant. If the ref path ever changes, this will silently diverge.

♻️ Use the constant
+import VaultService from '../src/domain/services/VaultService.js';
 // ...
-      const args = ['log', '--oneline', 'refs/cas/vault'];
+      const args = ['log', '--oneline', VaultService.VAULT_REF];

64-76: readManifestFromTree duplicates functionality from cas.readManifest({ treeOid }), but direct substitution would introduce subtle behavior changes.

The helper uses startsWith('manifest.') for entry matching (permissive), while the facade uses exact match against the codec-specific extension. Additionally, the facade handles v2 manifests with submanifest resolution—a feature the helper lacks. If the vault context never uses these extended features and entry matching is intended to be lenient, the refactoring is valid; otherwise, verify the behavioral equivalence before simplifying.

test/unit/vault/VaultService.test.js (1)

492-536: CAS retry tests use real setTimeout delays — consider using fake timers.

The retry logic in VaultService uses real setTimeout with exponential backoff (50ms, 100ms). In the exhausted test (3 retries), this adds ~150ms of real wall-clock time. While tolerable now, as the test suite grows this can accumulate.

Consider using vi.useFakeTimers() and advancing timers to make these tests deterministic and faster.

src/domain/services/VaultService.js (2)

223-237: #casUpdateRef treats all errors as VAULT_CONFLICT — intentional but worth a doc note.

Any failure from updateRef (including transient I/O errors, permission issues, etc.) is wrapped as VAULT_CONFLICT. This is safe from a correctness standpoint since the caller will retry, but it means non-conflict errors get masked. A caller debugging a permission issue would see VAULT_CONFLICT instead of the root cause.

Consider preserving the original error in the meta for diagnostics:

Proposed fix
-    } catch {
+    } catch (err) {
       throw new CasError(
         'Concurrent vault update detected',
         'VAULT_CONFLICT',
-        { expectedParent: expectedOldOid, newCommit: newOid },
+        { expectedParent: expectedOldOid, newCommit: newOid, originalError: err },
       );
     }

300-322: initVault does not use #retryMutation, so it's not CAS-safe against concurrent calls.

Unlike addToVault and removeFromVault, initVault calls writeCommit directly. If two concurrent initVault calls race, the second will get a VAULT_CONFLICT without automatic retry. While this is documented in the error codes table, it's an inconsistency in the retry posture.

This may be acceptable given initVault is typically called once, but flagging for awareness.

…tests

- Percent-encode slashes in vault slugs so git tree entries are valid
- Remove redundant alfred retry policy from adapters (plumbing already retries)
- Add Dockerized vault CLI integration test exercising all subcommands
- Configure git identity env vars in docker-compose for commit-tree support
@flyingrobots flyingrobots changed the title feat: vault — GC-safe ref-based asset index v3.0.0 — Vault Feb 8, 2026
#validateMetadata now rejects encryption metadata missing kdf.keyLength,
preventing downstream KDF failures from manually edited .vault.json files.
#casUpdateRef now includes the original error in the VAULT_CONFLICT meta
for better diagnostics when non-conflict errors (e.g., permissions) occur.
resolveEncryptionKey now emits a stderr warning when a passphrase is
provided but the vault has no encryption config, preventing silent
unencrypted store/restore surprises.
Replace hardcoded 'refs/cas/vault' string with
ContentAddressableStore.VAULT_REF to prevent divergence.
- Add `text` language identifier to fenced code blocks (MD040)
- Fix invalid import path @git-stunts/cas/vault → @git-stunts/cas
- Fix _readVaultState() → readState() in error codes table
- Extract #validateEncryption helper to reduce #validateMetadata complexity
- Split kdf.keyLength test into own describe block to stay under max-lines
@flyingrobots
Copy link
Member Author

@coderabbitai Please review 🥕

@coderabbitai
Copy link

coderabbitai bot commented Feb 9, 2026

@flyingrobots: I'll review PR #7 for you! 🥕 This looks like a substantial addition with the Vault subsystem, introducing GC-safe ref-based storage, the new VaultService domain service, and comprehensive CLI integration. I'll examine the architecture, implementation, tests, and documentation.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/infrastructure/adapters/GitPersistenceAdapter.js (1)

25-25: ⚠️ Potential issue | 🟡 Minor

Stale JSDoc: still mentions "2 retries".

The @param description says "30 s timeout + 2 retries" but the retry layer was removed. Line 19 was updated; this one was missed.

📝 Proposed fix
-   * `@param` {import('@git-stunts/alfred').Policy} [options.policy] - Resilience policy (defaults to 30 s timeout + 2 retries).
+   * `@param` {import('@git-stunts/alfred').Policy} [options.policy] - Resilience policy (defaults to 30 s timeout, no retry).
🤖 Fix all issues with AI agents
In `@bin/git-cas.js`:
- Line 12: Update the CLI version string from '2.0.0' to '3.0.0' by changing the
argument passed to the .version(...) call (the .version method on the
commander/CLI setup in bin/git-cas.js) so the CLI reports the correct v3.0.0
release.
🧹 Nitpick comments (6)
docker-compose.yml (1)

8-11: LGTM — sensible git identity for containerised tests.

Setting explicit author/committer env vars avoids "Please tell me who you are" errors inside containers that lack a global git config. Values are clearly synthetic, no real PII.

Minor DRY nit: the same four vars are copy-pasted into all three services. A YAML anchor keeps them in sync:

♻️ Optional: deduplicate with a YAML anchor
+x-git-env: &git-env
+  - GIT_AUTHOR_NAME=git-cas-test
+  - GIT_AUTHOR_EMAIL=test@git-cas.local
+  - GIT_COMMITTER_NAME=git-cas-test
+  - GIT_COMMITTER_EMAIL=test@git-cas.local
+
 services:
   test-node:
     build:
       context: .
       target: node
     environment:
       - GIT_STUNTS_DOCKER=1
-      - GIT_AUTHOR_NAME=git-cas-test
-      - GIT_AUTHOR_EMAIL=test@git-cas.local
-      - GIT_COMMITTER_NAME=git-cas-test
-      - GIT_COMMITTER_EMAIL=test@git-cas.local
+      *git-env

(Repeat for test-bun and test-deno.)

src/domain/services/VaultService.js (2)

271-290: CAS retry delay uses setTimeout — consider timer cleanup for testability.

The retry loop's setTimeout (line 285) works but means tests exercising retries incur real wall-clock delays (50ms, 100ms). This is acceptable at current scale (MAX_CAS_RETRIES = 3, max total ~350ms) but worth noting if retry counts increase.


326-348: initVault doesn't use #retryMutation — concurrent inits will throw VAULT_CONFLICT.

Unlike addToVault/removeFromVault, initVault calls writeCommit directly without retry. A concurrent initVault would fail with VAULT_CONFLICT and no automatic retry. This seems intentional for a one-time setup operation, but it's worth confirming that callers (CLI) are prepared for this.

bin/git-cas.js (1)

276-295: vault history builds git args from user input — injection risk is mitigated by array-based execution.

opts.maxCount is interpolated into a flag string (-${opts.maxCount}), but since plumbing.execute({ args }) uses an args array rather than shell string interpolation, this is safe from command injection. However, non-numeric input will produce a confusing git error.

Consider parsing maxCount as an integer for friendlier validation:

Proposed improvement
   .option('-n, --max-count <n>', 'Limit number of commits')
   .action(async (opts) => {
     try {
       const runner = ShellRunnerFactory.create();
       const plumbing = new GitPlumbing({ runner, cwd: opts.cwd || '.' });
       const args = ['log', '--oneline', ContentAddressableStore.VAULT_REF];
       if (opts.maxCount) {
+        const n = parseInt(opts.maxCount, 10);
+        if (Number.isNaN(n) || n <= 0) {
+          process.stderr.write('error: --max-count must be a positive integer\n');
+          process.exit(1);
+        }
-        args.push(`-${opts.maxCount}`);
+        args.push(`-${n}`);
       }
test/integration/vault-cli.test.js (1)

52-57: Shared mutable state (storeOid, repoDir) across describe blocks creates implicit ordering dependency.

storeOid is set in the first describe block and implicitly relied upon by assertions in vault list (line 86). The restore/remove describe block also depends on the vault state from the first block. This works because vitest runs describe blocks sequentially within a file, but it makes the tests brittle if anyone reorders them.

This is a common trade-off in integration tests; just flagging for awareness.

Also applies to: 72-129

test/unit/vault/VaultService.test.js (1)

142-159: Unit tests don't exercise decodeSlug with percent-encoded tree entry names.

Mock tree entries use raw names like 'demo/hello' and 'a/b/c' rather than the percent-encoded forms ('demo%2Fhello', 'a%2Fb%2Fc') that readTree would return in production. The encodeSlug path is well-tested (line 244 checks for 'demo%2Fhello'), but the decodeSlug path is only exercised by integration tests.

Consider adding a test with encoded mock names to verify the full round-trip at the unit level:

Example test for decode path
it('decodes percent-encoded tree entry names', async () => {
  const ref = mockRef();
  const persistence = mockPersistence();
  setupExistingVault({ ref, persistence, metaJson: JSON.stringify({ version: 1 }), entries: [
    { mode: '040000', type: 'tree', oid: 'tree-a', name: 'demo%2Fhello' },
  ] });
  const vault = createVault({ ref, persistence });
  const state = await vault.readState();
  expect(state.entries.get('demo/hello')).toBe('tree-a');
});

Also applies to: 313-339

- CLI version string 2.0.0 → 3.0.0
- Stale JSDoc in GitPersistenceAdapter (remove mention of retries)
- Validate --max-count as positive integer in vault history
- Add decodeSlug round-trip unit test with percent-encoded entry names
- Update CHANGELOG with final fixes and test count
@flyingrobots flyingrobots merged commit aa0840e into main Feb 9, 2026
3 checks passed
@flyingrobots flyingrobots deleted the vault branch February 9, 2026 00:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant