Skip to content

feat: filesystem-backed durable capture stores#376

Merged
jithinraj merged 3 commits intomainfrom
feat/capture-node
Feb 15, 2026
Merged

feat: filesystem-backed durable capture stores#376
jithinraj merged 3 commits intomainfrom
feat/capture-node

Conversation

@jithinraj
Copy link
Member

@jithinraj jithinraj commented Feb 15, 2026

Summary

New package @peac/capture-node providing Node.js implementations of the SpoolStore and DedupeIndex interfaces from @peac/capture-core. Production-grade durable storage for the Evidence Export Path.

  • FsSpoolStore: Append-only JSONL with explicit commit() durability, hard-cap limits, single-writer lockfile, crash recovery (tail truncation + chain verification), meta file cache for O(1) cold start, auto-commit timer, streaming reads, directory fsync on first create, read-only degraded mode on corruption
  • FsDedupeIndex: Map-backed append-only file, persist/reload across restarts, commit() via type guard (capture-core interface unchanged), directory fsync on first create
  • Streaming line reader: Custom parser enforcing maxLineBytes at Buffer level before string materialization

Threat model fix

maxLineBytes is enforced pre-materialization in both read() and fullScan(). The custom streaming line parser (line-reader.ts) operates on raw Buffer chunks -- oversized lines are never converted to JS strings, preventing OOM from a single large line in a spool file. Memory is freed immediately when a line exceeds the limit, and accumulatedBytes is capped to prevent counter overflow on pathological files.

This replaces readline (which materializes full strings before any size check) and fs.readFile() (which loads the entire file).

Corruption boundaries

Condition Behavior
Incomplete last line (crash artifact) Auto-truncated on startup, onWarning fired
Malformed JSON mid-file Spool marked corrupt, no auto-repair (could mask tampering)
Chain linkage broken Spool marked corrupt, prev_entry_digest chain failed
Oversized line (exceeds maxLineBytes) Spool marked corrupt, line never materialized as JS string

When corrupt, the spool enters read-only mode: new captures blocked, export/verify/query still work.

Durability contract

  • append() writes to OS page cache (no fsync) -- fast, not crash-safe alone
  • commit() calls fsync -- the explicit durability point
  • Auto-commit timer (default 5s) calls commit() periodically when dirty
  • Commit ordering: spool first (authoritative), dedupe second (best-effort)

Dependency audit triage

6 vulnerabilities found in monorepo (2 high, 3 moderate, 1 low). None affect @peac/capture-node (zero external deps). Findings are in surfaces/*, examples/*, and one low-severity transitive in @peac/middleware-express. Will be addressed via dependency bumps.

Severity Package Issue Affected scope
high wrangler 3.114.15 OS command injection in pages deploy surfaces/workers/cloudflare (dev-only)
high next 15.5.9 HTTP deserialization DoS (insecure RSC) surfaces/nextjs/middleware (dev/example)
moderate esbuild 0.17.19 Dev server request vulnerability transitive via wrangler
moderate undici 5.29.0 Decompression DoS transitive via wrangler/miniflare
moderate next 15.5.9 Image Optimizer DoS surfaces/nextjs/middleware
low qs 6.14.1 arrayLimit bypass examples/ + @peac/middleware-express

Test plan

  • 69 tests across 5 test files (spool store, dedupe index, lockfile, integration, line reader)
  • SIGKILL durability test (subprocess killed with SIGKILL, committed entries survive)
  • Randomized chunk-boundary fuzz tests for line parser
  • CRLF normalization tests (pure CRLF, mixed, cross-chunk-boundary)
  • Full monorepo gate: 3712 tests, 140 files, 74 build targets
  • pnpm lint, pnpm typecheck:core, pnpm format:check -- all clean
  • scripts/guard.sh, scripts/check-planning-leak.sh, scripts/check-publish-list.sh -- all pass

Files

New package: packages/capture/node/

File Description
src/fs-spool-store.ts FsSpoolStore: streaming JSONL, hard-cap, lockfile, meta file, crash recovery
src/fs-dedupe-index.ts FsDedupeIndex: Map-backed, append-only file, last-write-wins
src/line-reader.ts Streaming line parser with pre-materialization size enforcement
src/lockfile.ts Single-writer lockfile with opt-in stale break
src/errors.ts SpoolFullError, SpoolCorruptError, LockfileError, CorruptReason
src/index.ts Public exports
tests/fs-spool-store.test.ts 26 tests (append, commit, read, hard-cap, crash recovery, SIGKILL, meta)
tests/fs-dedupe-index.test.ts 13 tests (set, has, markEmitted, persistence, reload)
tests/lockfile.test.ts 7 tests (acquire, release, stale detection, concurrent access)
tests/integration.test.ts 5 tests (full CaptureSession with fs stores)
tests/line-reader.test.ts 18 tests (parsing, CRLF, maxLineBytes, fuzz, truncate)
tests/fixtures/spool-crash-writer.ts Subprocess fixture for SIGKILL durability test
README.md Durability contract, corruption boundaries, diagnostics API, reset procedure

Modified

File Change
docs/ARCHITECTURE.md Version bump to 0.10.11
pnpm-lock.yaml New package resolution
scripts/guard.sh Allow npm install in package READMEs (consumer-facing docs)
scripts/check-publish-list.sh Add @peac/capture-node (40th package)

New package providing Node.js implementations of the SpoolStore and
DedupeIndex interfaces from @peac/capture-core. Production-grade
durable storage for the Evidence Export Path.

FsSpoolStore:
- Append-only JSONL with explicit commit() durability model
- Hard-cap limits (maxEntries, maxFileBytes) -- SpoolFullError on breach
- Single-writer lockfile guard (opt-in stale break)
- Crash recovery: incomplete tail truncation, chain linkage verification
- Meta file cache (spool.jsonl.meta.json) for O(1) cold start
- Auto-commit timer (default 5s) for periodic fsync
- Streaming reads via custom line parser (no full-file materialization)
- Directory fsync on first create (best-effort)
- Read-only degraded mode on corruption (export/verify still work)

FsDedupeIndex:
- Map-backed append-only file with last-write-wins semantics
- Persist/reload across restarts, duplicate line tolerance
- commit() exposed via type guard (capture-core interface unchanged)
- Directory fsync on first create (best-effort)

Line reader (threat model fix):
- Pre-materialization maxLineBytes enforcement at Buffer level
- Oversized lines never converted to JS string (prevents OOM)
- Memory freed immediately when line exceeds limit
- CRLF normalization (safe for cross-platform spool files)
- Used by both read() and fullScan()

Also:
- docs/ARCHITECTURE.md version bump to 0.10.11
- scripts/check-publish-list.sh updated for 40th package
- 69 tests across 5 test files (including SIGKILL durability test)
- Zero external dependencies (only @peac/capture-core)
Package READMEs are consumer-facing docs where `npm install` is the
standard install instruction. Add `packages/.*/README\.md` to the
NPM_ALLOW pattern so guard.sh does not flag them.
Remove unused imports (FsDedupeIndex, FsSpoolStore, SpoolFullError)
and unused variables (giantSize, store) flagged by code quality bot.
@jithinraj jithinraj changed the title feat: @peac/capture-node -- filesystem-backed durable capture stores feat: filesystem-backed durable capture stores Feb 15, 2026
@jithinraj jithinraj merged commit 0cdff6a into main Feb 15, 2026
7 checks passed
jithinraj added a commit that referenced this pull request Feb 15, 2026
The CI publish-manifest closure check failed because @peac/capture-node
was not in the manifest or directory mappings. This package was added in
PR #376 but the publish-related scripts were not updated.

- publish-manifest.json: add capture-node (20 -> 21 packages)
- check-publish-closure.ts: add capture-node nested mapping
- pack-smoke.mjs: add capture-node PKG_DIRS mapping
jithinraj added a commit that referenced this pull request Feb 15, 2026
* feat(adapter-openclaw): plugin activation bridge and keygen utility

Add activate() as the single canonical entry point that wires all adapter
components from config. Calls existing factory functions (resolveSigner,
createPluginInstance, createFileReceiptWriter) with FsSpoolStore and
FsDedupeIndex from @peac/capture-node -- no logic duplication.

Add generateSigningKey() for Ed25519 keypair generation via @peac/crypto,
with keygenCli() for CLI usage and peac-keygen bin entry.

Changes:
- activate.ts: high-level entry wiring stores, signer, writer, instance, tools
- keygen.ts: Ed25519 key generation, JWK file output with 0o600 permissions
- keygen-cli.ts: CLI entry point for peac-keygen binary
- index.ts: export activate, keygen types and functions
- package.json: add @peac/capture-node dep, bin entry, bump @types/node to ^22
- tsup.config.ts: add keygen-cli entry point
- openclaw.plugin.json: bump version 0.10.6 -> 0.10.12
- check-publish-list.sh: add adapter-openclaw to tested, capture-core to
  tracked, fix hardcoded count 40 -> 42

Tests: 22 new tests (12 activate + 10 keygen), 161 total adapter tests passing.

* fix: add @peac/capture-node to publish manifest and directory mappings

The CI publish-manifest closure check failed because @peac/capture-node
was not in the manifest or directory mappings. This package was added in
PR #376 but the publish-related scripts were not updated.

- publish-manifest.json: add capture-node (20 -> 21 packages)
- check-publish-closure.ts: add capture-node nested mapping
- pack-smoke.mjs: add capture-node PKG_DIRS mapping

* fix: address PR review findings

- createStatusTool: accept getter function for live stats instead of
  stale snapshot. Backwards-compatible: still accepts static PluginStats.
- activate.ts: pass stats getter to createStatusTool
- activate.ts: add explicit type annotation on onWarning callback
- tsconfig.core.json: add packages/capture/node/src to includes so CI
  typecheck resolves @peac/capture-node imports

* fix: add @peac/capture-node path mapping to tsconfig.base.json

typecheck:core failed because the paths block was missing
capture-node, so TS could not resolve the bare specifier
imported in adapter-openclaw/src/activate.ts.

* chore: align plugin manifest version + add duplicate JSON key guard

- openclaw.plugin.json version 0.10.12 -> 0.10.11 to match package.json
- Add scripts/check-json-dupes.mjs: detects duplicate keys in JSON files
  (JSON parsers silently accept duplicates with last-write-wins)
- Wire check-json-dupes into guard.sh alongside existing bidi scanner
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