Skip to content

refactor(workspace): dissolve ironclaw_storage + HsmBackend placeholder - #3678

Closed
ilblackdragon wants to merge 2 commits into
reborn/fs-run-statefrom
reborn/fs-storage-dissolution
Closed

ilblackdragon wants to merge 2 commits into
reborn/fs-run-statefrom
reborn/fs-storage-dissolution

Conversation

@ilblackdragon

Copy link
Copy Markdown
Member

Two follow-on commits on top of the universal-FS-dispatch cascade. Closes tasks #9, #18, and the demonstrable portion of #19 of the rework plan.

Summary

Commit 1: dissolve ironclaw_storage crate.

The crate predated the unified RootFilesystem surface introduced by PR #3659. Its BlobStore/RecordStore traits, StorageKey/StorageVersion/PutCondition types, and StoredBlob/StoredRecord shapes parallel the new unified put/get/CasExpectation/RecordVersion machinery — a textbook duplicate-dispatch smell from .claude/rules/architecture.md.

Only ironclaw_outbound consumed any of the crate, and only 5 small helpers (encode_json, decode_json, redacted_backend_error, StorageError::Backend, ABSENT_SCOPE_COMPONENT). All other types and the entire BlobStore/RecordStore surface (660 LOC) were unused — their intended consumers already moved to RootFilesystem directly.

  • Inlined the 5 helpers into crates/ironclaw_outbound/src/db.rs
  • Removed ironclaw_storage = { path = ... } from outbound Cargo.toml
  • Removed \"crates/ironclaw_storage\" from workspace members
  • Removed the BoundaryRule for ironclaw_storage from architecture tests
  • Updated the ironclaw_outbound BoundaryRule to permit ironclaw_filesystem (it was stale: FilesystemOutboundStateStore landed in the prior cascade PR feat(outbound): FilesystemOutboundStateStore on the unified surface #3670)
  • Deleted the crate directory

Commit 2: HsmBackend placeholder + database.md scoping.

HsmBackend (crates/ironclaw_filesystem/src/hsm.rs) demonstrates the universal-dispatch seam end-to-end: one trait, one file, declared restricted capabilities, mount-time validation rejects over-claiming descriptors, swap-by-wiring works without consumer edits. Five tests cover all of those gates including the rejection paths. A production HSM swaps the in-process placeholder for a real session handle; the trait + capability machinery is reusable as-is.

.claude/rules/database.md is now scoped via its paths: frontmatter to src/db/**, src/history/**, and migrations/** — exactly the legacy directories that predate the universal FS dispatch — with a 'Status & Direction' preamble pointing new persistence work at ScopedFilesystem and the rework plan.

Test plan

  • `cargo check --workspace --all-features` (clean after dissolution)
  • `cargo test -p ironclaw_outbound --all-features` (6 passed including filesystem store contract)
  • `cargo test -p ironclaw_architecture` (14 boundary tests pass with the stale-rule fix)
  • `cargo test -p ironclaw_filesystem --all-features` (35 passed — 25 unit incl. 5 new HSM + 5 catalog + 33 db-backed + 20 filesystem-contract)
  • `cargo clippy -p ironclaw_filesystem --all-features --tests` clean
  • `cargo test --locked --lib` (4967 tests pass at workspace root)

Stack

Built on top of #3672 (reborn/fs-run-state) → #3671 (authorization) → #3670 (outbound) → #3666 (processes) → #3659 (foundation).

What this does NOT do

Tasks #10, #14, #15, #16, #17 of the rework plan are still outstanding (secrets, event_store, memory, engine Store, src/db dissolution). Each is a substantial separate PR. The full grep-acceptance gate from #19 (≤1 store per crate, cfg-gates centralized in filesystem) cannot be met until those migrations land.

The ironclaw_storage crate predates the unified RootFilesystem surface
introduced by PR #3659 (universal FS dispatch). Its `BlobStore`/`RecordStore`
traits, `StorageKey`/`StorageVersion`/`PutCondition` types, and
`StoredBlob`/`StoredRecord` shapes parallel the new unified put/get
/CasExpectation/RecordVersion machinery on `RootFilesystem` — a textbook
duplicate-dispatch smell flagged by .claude/rules/architecture.md.

Only `ironclaw_outbound` consumed any of the crate, and only 5 small
helpers (`encode_json`, `decode_json`, `redacted_backend_error`,
`StorageError::Backend`, `ABSENT_SCOPE_COMPONENT`). All other types and
the entire `BlobStore`/`RecordStore` surface (660 LOC) were unused —
their intended consumers already moved to `RootFilesystem` directly.

Inlined the 5 helpers into `crates/ironclaw_outbound/src/db.rs`:
- `encode_json`/`decode_json` → direct `serde_json::to_string`/`from_str`
- `redacted_backend_error` → local log+collapse to `OutboundError::Backend`
  (preserves the redaction boundary required by ironclaw_outbound/CLAUDE.md)
- `ABSENT_SCOPE_COMPONENT` → local const ""

Removed the crate's workspace membership, the outbound dep, the
forbidden-edges BoundaryRule, and the crate directory.

Also updated the ironclaw_outbound BoundaryRule to permit a normal
dependency on `ironclaw_filesystem` — `FilesystemOutboundStateStore`
landed in the prior cascade PR and the boundary rule was stale.
…egacy

Two changes that close out the demoable parts of the universal-FS-dispatch
rework (tasks #18 and the demonstrable portion of #19 from the plan).

**HsmBackend placeholder** (`crates/ironclaw_filesystem/src/hsm.rs`).
Demonstrates that a new backend is a single-file change: implements the
one `RootFilesystem` trait, declares a restricted capability surface
(`Read` + `Write` + `Stat` + `Delete` + `TxnCapability::Cas` — no records,
no query, no index, no events, no multi-key transactions), and routes
`put`/`get`/`delete`/`stat`/`list_dir` through an in-process placeholder.

Five tests prove the seam works end-to-end:

- `hsm_supports_encrypted_bytes_round_trip` — bytes put/get works.
- `hsm_rejects_structured_records` — `put` with `RecordKind::Some` or
  non-empty `indexed` returns `Unsupported`, so a consumer cannot
  accidentally route records through encryption-only storage.
- `hsm_rejects_query_and_index_ops` — `query`/`ensure_index` return
  `Unsupported` consistent with the declared capabilities.
- `composite_rejects_overclaimed_hsm_descriptor` — mount-time
  validation (`validate_mount_capabilities`) refuses a descriptor that
  claims `Query`/`IndexExact` over a backend that doesn't deliver,
  failing with `FilesystemError::DescriptorOverclaims { missing, .. }`.
- `composite_routes_to_hsm_under_secrets_mount` — the acceptance gate:
  mounting HsmBackend at `/secrets` and routing put/get through the
  composite works with no consumer-visible changes. Indexed projection
  is still rejected because the declared capabilities advertise no
  index/query support.

A real HSM implementation replaces the in-memory placeholder with an
HSM session handle; the trait surface, capability declarations, and
mount-time validation are reusable as-is. The placeholder is *not* a
security boundary — it is a seam demonstration.

**database.md scoped to legacy directories**. The dual-backend rule
file (`.claude/rules/database.md`) is `paths`-scoped to `src/db/**`,
`src/history/**`, and `migrations/**` — exactly the legacy surface that
predates the universal FS dispatch. Added a "Status & Direction"
preamble pointing new persistence work at `ScopedFilesystem` and the
`2026-05-14-universal-fs-dispatch.md` plan, with the existing
per-crate dual-backend guidance kept (and tagged "legacy") for code
still inside those directories.
@github-actions github-actions Bot added scope: docs Documentation scope: dependencies Dependency updates size: XL 500+ changed lines risk: medium Business logic, config, or moderate-risk modules contributor: core 20+ merged PRs labels May 15, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request migrates the persistence architecture from per-crate storage traits to a universal RootFilesystem mount table system. As part of this transition, the ironclaw_storage crate has been removed, and its core utilities have been inlined into the ironclaw_outbound crate. A new HsmBackend was added to ironclaw_filesystem to demonstrate the new dispatch model. Feedback indicates that HsmBackend should explicitly declare Capability::List in its capabilities to remain consistent with its implementation of the list_dir method.

Comment on lines +53 to +60
fn declared_capabilities() -> BackendCapabilities {
BackendCapabilities::empty()
.with(Capability::Read)
.with(Capability::Write)
.with(Capability::Stat)
.with(Capability::Delete)
.with_txn(TxnCapability::Cas)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The HsmBackend implements list_dir by delegating to the inner backend, but it does not declare Capability::List in its capabilities. While the legacy capability plane is currently descriptor-driven for validation, new backends should accurately advertise their supported operations for consistency and future-proofing.

    fn declared_capabilities() -> BackendCapabilities {
        BackendCapabilities::empty()
            .with(Capability::Read)
            .with(Capability::Write)
            .with(Capability::List)
            .with(Capability::Stat)
            .with(Capability::Delete)
            .with_txn(TxnCapability::Cas)
    }
References
  1. Capabilities should be handled consistently across all capability kinds and accurately advertised on all surfaces.

@ilblackdragon

Copy link
Copy Markdown
Member Author

Superseded by #3679 — single unified PR on top of #3659 (foundation) that contains all the consumer-crate work applying the universal FS dispatch. This PR's commit is preserved in the unified branch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contributor: core 20+ merged PRs risk: medium Business logic, config, or moderate-risk modules scope: dependencies Dependency updates scope: docs Documentation size: XL 500+ changed lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant