feat(memory): model memory as a userland extension — implements #3537#5205
feat(memory): model memory as a userland extension — implements #3537#5205BenKurrek wants to merge 23 commits into
Conversation
Lift the Reborn memory layer out of the kernel into a provider-neutral contract crate (ironclaw_memory) plus a native filesystem provider crate (ironclaw_memory_native), routing the first-party memory tools and prompt-context retrieval through an Arc<dyn MemoryService> facade. Strictly behavior-preserving: a behavior-equivalence audit against origin/main confirms the memory tools' observable behavior — input parsing, response JSON, error kinds, stored-document semantics, and prompt-context retrieval — is unchanged. The native provider keeps the existing filesystem storage format. Out of scope (follow-ups): registering the native provider as a distinct extension manifest and wiring provider selection; the prompt_doc_ref manifest rule, memory-profile binding resolver, and host-port scaffolding were ripped out, and development-time "hardening" deviations from origin behavior were reverted to exact origin behavior. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ce-wide allowlist PR #5163 made only the memory rules allowlist-based; the other ~30 `BoundaryRule` entries stayed blocklists that under-enforce — they forbid today's offenders but would silently admit a future internal dep (e.g. `ironclaw_turns`, `ironclaw_product_workflow`, `ironclaw_reborn`). Convert the whole harness to an allowlist: - `BoundaryRule` now carries `allowed: Vec<&'static str>` instead of `forbidden`. The runner computes `forbidden = workspace_ironclaw_crates() - allowed - crate_name`, so any unlisted internal dependency now fails the boundary test. - Each crate's `allowed` set is its actual normal `ironclaw_*` dependencies, matching the dependency guardrail documented in its CLAUDE.md/AGENTS.md. - The three previously-inline allowlist rules (`ironclaw_host_api`, `ironclaw_memory`, `ironclaw_memory_native`) are folded into `boundary_rules()` so the test body is a single uniform loop. Behavior-preserving on the current (correct) dependency graph; the win is forward enforcement. Addresses serrrfirat's deferred thread on #5163 (discussion_r3468163078). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ontext Before this change the native provider sanitized, wrapped, and hashed each memory snippet, and the host only *asserted* the `Untrusted memory content:` prefix in `admit_memory_context_snippet`. A future untrusted provider could pre-attach that prefix (or pre-shape the snippet) and slip text past the host's prompt-safety wrapper. Move all model-visible shaping into the host so the provider can never bypass prompt safety: - `MemoryServiceContextSnippet` now carries RAW snippet text plus the resolved scope/path components (`tenant_id`, `user_id`, `agent_id`, `project_id`, `relative_path`) — no `snippet_ref`/`safe_summary`/`model_content`. - The native `retrieve_context` ranks and scope-filters candidates, then returns them raw; it no longer sanitizes, truncates, hashes, or budgets. Removed the native `sanitize_snippet_text`, `truncate_to_char_boundary`, `validate_loop_safe_summary`, `memory_snippet_display_ref`, `feed_hash`, `collect_context_snippets`, the FNV/budget consts, and the prompt-envelope dep. - The host `memory_context.rs` builds the `memory-snippet:*` reference via the canonical `ironclaw_turns::run_profile::memory_snippet_display_ref`, sanitizes + wraps the raw text (`sanitize_snippet_text` relocated here), validates through the loop's own `LoopSafeSummary` gate (collapsing the native denylist copy into one source of truth), and enforces the per-snippet (512B) + aggregate (4 KiB) budgets in the admission loop with the same break semantics the native `collect_context_snippets` used. Behavior-preserving for the native provider: model-visible output is byte-for-byte identical (same wrapping, same FNV trailing-separator `memory-snippet:cb96ed00b13e6ae4` golden ref, same caps/ordering). New coverage proves a provider that returns text merely starting with the untrusted prefix is STILL re-sanitized + re-wrapped by the host (`adapter_re_sanitizes_provider_supplied_untrusted_prefix`, `sanitize_re_wraps_text_already_carrying_untrusted_prefix`), plus the legacy ref golden lock and the host-owned aggregate-budget test. Addresses serrrfirat's deferred security thread on #5163 (discussion_r3468163070; ref-stability discussion_r3466587649). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…facade Profile READS built the native repository/backend directly in `user_profile_source.rs` and duplicated the scope/path decision (`profile_scope_and_path` + `PROFILE_DOCUMENT_PATH`) that `NativeMemoryService` already owns for WRITES (`profile_set`). That coupled profile reads to the concrete native provider and left provider selection unable to swap reads with the rest of the memory facade. Add a provider-neutral profile read to the contract and route the host read through it: - New `MemoryService::profile_read(invocation) -> MemoryServiceProfileReadResponse` (raw document bytes) on the `ironclaw_memory` trait, with a native implementation that reuses the SAME `profile_scope_and_path` as `profile_set` — so the scope/path decision lives in exactly one place per provider. - `MemoryBackedUserProfileSource` now holds `Arc<dyn MemoryService>` and reads via `profile_read`; the host keeps only the parse + 64 KiB size-cap + validation. Deleted the duplicate host `profile_scope_and_path` / `PROFILE_DOCUMENT_PATH` and their re-export. - Production wiring uses the new `MemoryBackedUserProfileSource::from_filesystem` factory (host owns the native-provider choice, matching the memory capability); the composition layer keeps passing the workspace filesystem. Behavior-preserving: the unit tests assert identical parse/validation outcomes (now through a stub `MemoryService`), and the end-to-end `user_profile_roundtrip` test still proves the agent-scoped write → user-scoped read round trip, now through `MemoryService::profile_read`. Addresses serrrfirat's deferred thread on #5163 (discussion_r3466587663). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…n audit Both deltas live in the (host-driven) context path and are intentional; this commit records why so a future reader doesn't "fix" them back toward origin: - Native `retrieve_context` uses `.with_vector(false)` while origin's prompt-context search left `vector=true`. `false` is correct for the FTS-only native backend (no embeddings wired; a vector request fails closed) and matches the native `search` method. Documented inline. - Host `map_memory_service_error` maps a failed memory-scope build to `InvalidInvocation` (via the provider's `Input` kind) where origin used `Internal`. The arm is unreachable in practice — the host validates the context scope before calling `retrieve_context` — and `InvalidInvocation` fails closed on the same axis as query validation. Documented on the mapper. Comment-only; no behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
📝 WalkthroughSummary by CodeRabbit
WalkthroughThe PR adds host memory profiles, a bundled ChangesMemory rollout
Memory rollout
Possibly related PRs
Suggested reviewers
Poem
Estimated code review effort 🚥 Pre-merge checks | ✅ 3 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (3 passed)
Comment |
There was a problem hiding this comment.
Code Review
This pull request refactors the memory context and user profile loading architecture, shifting the responsibility of reference hashing, sanitization, untrusted-envelope wrapping, and model-visible budget enforcement from the native memory provider to the host runtime. It also introduces a provider-neutral profile_read method to the MemoryService trait and updates workspace dependency boundary tests to use allowlists. Feedback suggests optimizing the snippet sanitization logic by caching the untrusted envelope prefix length using OnceLock to avoid redundant allocations on every snippet.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
…llowups # Conflicts: # crates/ironclaw_architecture/tests/reborn_dependency_boundaries.rs # crates/ironclaw_host_runtime/src/memory_context.rs # crates/ironclaw_host_runtime/src/user_profile_source.rs # crates/ironclaw_host_runtime/tests/memory_prompt_context.rs # crates/ironclaw_memory/src/lib.rs # crates/ironclaw_memory/src/service.rs # crates/ironclaw_memory_native/src/service.rs # crates/ironclaw_memory_native/tests/memory_service_facade.rs
…fest + binding policy (#3537) Land the host-runtime side of the remaining #3537 milestones: - M1: author the three memory CapabilityProfileContracts (context_retrieval, interaction_log, document_store) as host-defined code in `memory_profiles`, with repo conformance tests driving the real catalog through the `ironclaw_capabilities` harness. - M2: register `host.storage.sql_transaction.first_party` + `host.events.audit` (new `ironclaw_host_api` constants) in `default_host_port_catalog()`. - M3: bundle the `ironclaw.memory.native` v2 Extension Manifest (HostBundled, first_party runtime) under `assets/memory_native/`, parsed/backed from host_runtime so the manifest's `service` must match the registered native provider identity ("TOML alone is not authority"). Conformance + schema validation tests over the real bundled schemas. - M4 (host side): fail-closed `MemoryBindingPolicy` (profile_id -> provider, default-native, production rejects disabled/unverified-third-party absent an (extension_id, profile_id, deployment_profile) override). The memory-tools dispatch site now consults the binding instead of hardwiring `NativeMemoryService::from_filesystem`; non-native bindings fail closed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the `[memory]` section to `RebornConfigFile` with `profile_bindings` (profile_id -> extension_id) and `admin_overrides` (scoped to (extension_id, profile_id, deployment_profile)). Validation is structural + deployment-agnostic (non-empty fields, valid override deployment_profile or `*`); profile-id validity and fail-closed production policy are owned by the host-runtime binding resolver, which holds the profile catalog. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…3537) Resolve the memory binding policy from the `[memory]` config section + the deployment profile at startup (fail-closed: production rejects memory.disabled / unverified third-party bindings without an override), and thread the resolved document-store binding to the builtin first-party handler registry on both the local-dev and production composition paths. The CLI resolves the policy in `build_services_input_with_options` and attaches it to `RebornBuildInput`; active third-party overrides are logged (redacted) at `debug!`. Replaces the hardwired native provider selection at the dispatch site with a config-driven, profile-bound resolution. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-profiles status (#3537) Add the two ADRs the issue references (0001 Extension Manifest v2 hard cutover, 0002 native memory uses host storage ports) and move memory-profiles.md from "draft zero-behavior" to Active, with an Implemented/Deferred split. Documents the gated remainder explicitly: the reborn_memory_* dual-backend SQL tables + concrete storage-port adapter + scoped HostPortView into the handler (boundary: composition crates cannot depend on the root ironclaw crate where the SQL backends live), and the default flip (blocked on /memory data + API compatibility tests). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
crates/ironclaw_architecture/tests/reborn_dependency_boundaries.rs (1)
1712-1727: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueStale "forbidden deps" wording after the denylist→allowlist switch.
Line 1715 still reads "Adding any of the forbidden deps here…", but rules no longer carry a
forbiddenlist — anything not inallowedis now rejected. Reword to the allowlist semantics so the contract comment matches the enforcement.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/ironclaw_architecture/tests/reborn_dependency_boundaries.rs` around lines 1712 - 1727, The contract comment in the `BoundaryRule` for `ironclaw_reborn_cli` still describes the rule as a forbidden-deps list, but the enforcement now uses an allowlist. Update the wording near the `allowed` field so it clearly says anything not in `allowed` is rejected, while keeping the same intent about protecting `ironclaw_reborn_composition`-mediated access to internal Reborn types.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@crates/ironclaw_host_runtime/assets/memory_native/schemas/memory/document-read.input.v1.json`:
- Around line 7-10: Tighten the document-read.input.v1.json schema so the path
field is restricted to scoped relative paths only, not just any non-empty
string. Update the schema around the path property to enforce a relative,
non-absolute form and reject traversal patterns like leading slashes or
parent-directory segments, keeping the change localized to the document-read
input schema definition.
In `@crates/ironclaw_host_runtime/src/memory_binding.rs`:
- Around line 343-349: The override lookup in `memory_binding.rs` is comparing
`extension_id` with raw string equality, so case-only differences like
`acme.honcho` vs `Acme.Honcho` are missed. Update the `has_override` check
inside `requires_certified_bindings()` to normalize both sides to lowercase
using `.to_ascii_lowercase()` before comparing extension names, while keeping
the existing profile and deployment matching logic unchanged.
- Around line 138-144: The redaction logic in redacted_summary uses byte slicing
on extension_id, which can panic for non-ASCII input. Update redacted_summary to
truncate safely by characters/graphemes instead of &ext[..12], preserving the
existing redacted_ext behavior while avoiding any direct byte-index slicing on
external strings.
In `@crates/ironclaw_host_runtime/src/memory_context.rs`:
- Around line 88-96: The host-runtime memory admission path in
`admit_memory_context_snippet` / `admit_memory_context_snippets` currently
trusts provider-returned scope too early, so revalidate each snippet against
`request.scope` and `request.actor` before calling
`admit_memory_context_snippet` or hashing/wrapping it. Add a scope check in the
snippet loop to reject any tenant/user/agent/project mismatch, then only admit
snippets that match the request’s scope and actor. Keep the existing byte limit
and admission flow unchanged for valid snippets.
In `@crates/ironclaw_host_runtime/tests/memory_native_schema_validation.rs`:
- Around line 52-68: The current test only validates
`context_retrieve_input_schema_accepts_valid_and_rejects_invalid`, so add
instance-level valid/invalid assertions for the new document-store and
interaction schemas as well. Use the existing `load_schema` and
`jsonschema::validator_for` pattern in this test module to cover
`document-read`, `document-write`, and `interaction-record`, including checks
for required fields, enum values, numeric bounds, and rejection of unexpected
properties. Keep the fixtures alongside the existing schema validation tests so
regressions in those contracts fail in the same suite.
In `@crates/ironclaw_memory/src/service.rs`:
- Around line 345-353: `MemoryServiceContextSnippet` is carrying identity
components as raw strings across the service boundary, so replace those fields
with the validated identifier newtypes used by the domain (for example
`TenantId`, `UserId`, `AgentId`, `ProjectId`) or add a typed wrapper before
hashing in `MemoryServiceContextSnippet`/`memory_context.rs`. Update any
constructors, serialization, and call sites so the context can only be built
from validated IDs, preserving the strong contract and preventing malformed or
swapped scope components from changing the snippet reference.
In `@crates/ironclaw_reborn_composition/src/factory.rs`:
- Around line 2914-2921: The document_store_binding() helper is silently falling
back to MemoryProviderBinding::Native when a supplied MemoryBindingPolicy cannot
resolve the document-store capability or has no matching binding. Change
document_store_binding(policy: Option<&MemoryBindingPolicy>) to return
Result<MemoryProviderBinding, RebornBuildError>, propagate
CapabilityProfileId::new(MEMORY_DOCUMENT_STORE_PROFILE_ID)? instead of dropping
the error, and make the Some(policy) / missing-binding path fail closed with an
error rather than defaulting to Native. Keep only the None policy case as the
native default, and update the call sites to use ? where document_store_binding
is consumed.
In `@crates/ironclaw_reborn_composition/src/runtime.rs`:
- Around line 3037-3042: The profile source selection in runtime.rs is still
hardwired to the native filesystem path inside the user_profile_source branch,
which bypasses the resolved memory binding. Update the logic around
local_runtime and MemoryBackedUserProfileSourceAdapter to use the same
binding-aware Arc<dyn MemoryService> that memory capabilities use, constructing
MemoryBackedUserProfileSource with that service instead of from_filesystem; if
no valid binding is available, fall back to EmptyUserProfileSource so profile
reads fail closed rather than opening context/profile.json.
---
Outside diff comments:
In `@crates/ironclaw_architecture/tests/reborn_dependency_boundaries.rs`:
- Around line 1712-1727: The contract comment in the `BoundaryRule` for
`ironclaw_reborn_cli` still describes the rule as a forbidden-deps list, but the
enforcement now uses an allowlist. Update the wording near the `allowed` field
so it clearly says anything not in `allowed` is rejected, while keeping the same
intent about protecting `ironclaw_reborn_composition`-mediated access to
internal Reborn types.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: c15673fc-f28f-4539-939f-0ac535fa233b
📒 Files selected for processing (40)
crates/ironclaw_architecture/tests/reborn_dependency_boundaries.rscrates/ironclaw_host_api/src/host_port.rscrates/ironclaw_host_runtime/assets/memory_native/manifest.tomlcrates/ironclaw_host_runtime/assets/memory_native/schemas/memory/context-retrieve.input.v1.jsoncrates/ironclaw_host_runtime/assets/memory_native/schemas/memory/context-retrieve.output.v1.jsoncrates/ironclaw_host_runtime/assets/memory_native/schemas/memory/document-read.input.v1.jsoncrates/ironclaw_host_runtime/assets/memory_native/schemas/memory/document-read.output.v1.jsoncrates/ironclaw_host_runtime/assets/memory_native/schemas/memory/document-write.input.v1.jsoncrates/ironclaw_host_runtime/assets/memory_native/schemas/memory/document-write.output.v1.jsoncrates/ironclaw_host_runtime/assets/memory_native/schemas/memory/interaction-record.input.v1.jsoncrates/ironclaw_host_runtime/assets/memory_native/schemas/memory/interaction-record.output.v1.jsoncrates/ironclaw_host_runtime/src/extension_contracts.rscrates/ironclaw_host_runtime/src/first_party_tools/memory.rscrates/ironclaw_host_runtime/src/first_party_tools/mod.rscrates/ironclaw_host_runtime/src/lib.rscrates/ironclaw_host_runtime/src/memory_binding.rscrates/ironclaw_host_runtime/src/memory_context.rscrates/ironclaw_host_runtime/src/memory_native_extension.rscrates/ironclaw_host_runtime/src/memory_profiles.rscrates/ironclaw_host_runtime/src/user_profile_source.rscrates/ironclaw_host_runtime/tests/memory_capability_profiles.rscrates/ironclaw_host_runtime/tests/memory_native_manifest.rscrates/ironclaw_host_runtime/tests/memory_native_schema_validation.rscrates/ironclaw_host_runtime/tests/memory_prompt_context.rscrates/ironclaw_host_runtime/tests/user_profile_roundtrip.rscrates/ironclaw_memory/src/lib.rscrates/ironclaw_memory/src/service.rscrates/ironclaw_memory_native/src/service.rscrates/ironclaw_memory_native/tests/memory_service_facade.rscrates/ironclaw_reborn_cli/src/runtime/mod.rscrates/ironclaw_reborn_composition/src/factory.rscrates/ironclaw_reborn_composition/src/input.rscrates/ironclaw_reborn_composition/src/lib.rscrates/ironclaw_reborn_composition/src/memory_binding.rscrates/ironclaw_reborn_composition/src/runtime.rscrates/ironclaw_reborn_config/src/config_file.rscrates/ironclaw_reborn_config/src/lib.rsdocs/adr/0001-capability-manifest-v2-hard-cutover.mddocs/adr/0002-native-memory-uses-host-storage-ports.mddocs/reborn/contracts/memory-profiles.md
… profile binding, fail-loud) Apply the substantive + cheap review findings from the bot review pass: - Scope-equality guard (security): the provider-neutral snippet admission path (`memory_context::admit_memory_context_snippet`) now drops any snippet whose tenant/user/agent/project scope does not match the request scope before it is hashed or admitted. Native filters earlier, but a pluggable/third-party provider must not inject cross-scope content. Adds drop/keep tests. - Profile reads honor the binding: the local-dev user-profile source now builds the native-backed reader only when the document-store profile is bound to native, degrading to empty otherwise — so profile reads stay consistent with the memory tools instead of silently staying native. The resolved binding is carried on the local-dev store-graph input / local-runtime services. - Fail-loud: `document_store_binding` returns `Result` and surfaces a missing document-store binding instead of silently falling back to native. - Char-safe redaction: `MemoryActiveOverride::redacted_summary` truncates by characters, not bytes (the byte slice could not panic — id is ASCII — but the char form follows the repo rule and is encoding-robust). - More schema coverage: valid/invalid instance fixtures for document-read, document-write, and interaction-record (previously only context-retrieve). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
crates/ironclaw_reborn_composition/src/runtime.rs (1)
3018-3042: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winUpdate the stale workspace-filesystem contract comment.
Lines 3018-3021 still say profile reads go through the workspace filesystem, but the code now gates on
memory_document_store_bindingand builds fromextension_filesystem. Soften or rewrite that header comment to match the new binding-aware path. As per coding guidelines, comments that promise cross-layer guarantees must match enforced behavior.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/ironclaw_reborn_composition/src/runtime.rs` around lines 3018 - 3042, The leading workspace-filesystem contract comment is stale and no longer matches the binding-aware implementation. Update the header comment near the profile-source setup in runtime.rs so it describes the current `memory_document_store_binding`-gated path and the use of `extension_filesystem`, and remove or soften any claim that profile reads always go through the workspace filesystem. Keep the surrounding references to `MemoryBackedUserProfileSource`, `EmptyUserProfileSource`, and `identity_context_source` aligned with the actual control flow.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/ironclaw_host_runtime/src/memory_context.rs`:
- Around line 420-464: Add a caller-level regression in
ProductionMemoryPromptContextService::load_memory_snippets to verify that
snippets with the correct tenant/user but mismatched agent_id or project_id are
filtered out before being returned. Keep the existing
admit_memory_context_snippet helper tests, but add an integration-style test in
the memory_prompt_context test suite that exercises the provider path and
asserts the bad snippet is dropped while matching scope data still passes
through.
In `@crates/ironclaw_reborn_composition/src/factory.rs`:
- Around line 2928-2948: The missing document-store binding case is only covered
at the helper level, so add a factory-level regression in the build path around
build_reborn_services in factory.rs. Exercise a resolved MemoryBindingPolicy
that omits memory.document_store.v1, then assert the build fails with
RebornBuildError::InvalidConfig from document_store_binding. Use the existing
factory/build symbols to keep the test anchored to the caller and ensure the
invariant is validated end-to-end.
---
Outside diff comments:
In `@crates/ironclaw_reborn_composition/src/runtime.rs`:
- Around line 3018-3042: The leading workspace-filesystem contract comment is
stale and no longer matches the binding-aware implementation. Update the header
comment near the profile-source setup in runtime.rs so it describes the current
`memory_document_store_binding`-gated path and the use of
`extension_filesystem`, and remove or soften any claim that profile reads always
go through the workspace filesystem. Keep the surrounding references to
`MemoryBackedUserProfileSource`, `EmptyUserProfileSource`, and
`identity_context_source` aligned with the actual control flow.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: be9e9128-9127-4643-a9e3-0a847a478f4b
📒 Files selected for processing (5)
crates/ironclaw_host_runtime/src/memory_binding.rscrates/ironclaw_host_runtime/src/memory_context.rscrates/ironclaw_host_runtime/tests/memory_native_schema_validation.rscrates/ironclaw_reborn_composition/src/factory.rscrates/ironclaw_reborn_composition/src/runtime.rs
…mer (#3537) Collapse the per-call-site memory provider construction into one resolver. Before, the memory tools and the user-profile reader each called `NativeMemoryService::from_filesystem` and re-checked the binding inline, so the "which provider, and is it permitted?" decision was duplicated. `MemoryServiceResolver` (host_runtime::memory_provider) is now that decision in one place: given a profile + per-invocation inputs (filesystem + optional prompt-write-safety sink) it resolves the bound provider or returns None (fail-closed) for disabled / unimplemented-third-party bindings. It wraps `Option<MemoryBindingPolicy>` (None = native default) so it is Default and the tool structs that hold it need no fallible constructor. - Memory tools (MemoryCapabilityState) hold a resolver and build their service through it; they no longer reference NativeMemoryService or MemoryProviderBinding. - The local-dev user-profile source builds through the same resolver (native → MemoryBackedUserProfileSource, disabled/third-party → EmptyUserProfileSource). - The context retriever already takes an injected service and draws from the resolver once production-wired. - Composition threads one `MemoryServiceResolver` (built once per runtime from the resolved policy) instead of a bare `MemoryProviderBinding`; the `document_store_binding` helper is removed. Behavior-preserving for the native default; verified by an adversarial review (completeness / fail-closed / behavior-preservation / dead-code) plus the existing memory + user_profile_roundtrip suites. fmt + clippy clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
crates/ironclaw_reborn_composition/src/runtime.rs (1)
3018-3021: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winUpdate the stale profile-source comment.
This still says profile context comes from
context/profile.jsonvia the workspace filesystem, but the changed code resolves throughMemoryServiceResolverandMemoryService::profile_read. Soften or rewrite these lines to avoid documenting the old provider/path contract.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/ironclaw_reborn_composition/src/runtime.rs` around lines 3018 - 3021, The comment above the profile resolution flow is stale and still documents the old `context/profile.json` workspace-filesystem contract; update it to match the current `MemoryServiceResolver` and `MemoryService::profile_read` path. Rewrite the text near the profile-source logic so it describes the current in-memory service-based resolution and the graceful `None` fallback without mentioning the deprecated provider or file path.Sources: Coding guidelines, Path instructions
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@crates/ironclaw_reborn_composition/src/runtime.rs`:
- Around line 3018-3021: The comment above the profile resolution flow is stale
and still documents the old `context/profile.json` workspace-filesystem
contract; update it to match the current `MemoryServiceResolver` and
`MemoryService::profile_read` path. Rewrite the text near the profile-source
logic so it describes the current in-memory service-based resolution and the
graceful `None` fallback without mentioning the deprecated provider or file
path.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: e9a1809b-6f40-4c01-aa32-7077341184c9
📒 Files selected for processing (6)
crates/ironclaw_host_runtime/src/first_party_tools/memory.rscrates/ironclaw_host_runtime/src/first_party_tools/mod.rscrates/ironclaw_host_runtime/src/lib.rscrates/ironclaw_host_runtime/src/memory_provider.rscrates/ironclaw_reborn_composition/src/factory.rscrates/ironclaw_reborn_composition/src/runtime.rs
…ry.native package Collapse the two capability declarations of the one filesystem-backed memory provider into one. The model-facing memory tools (read/write/search/tree) now belong to a dedicated `ironclaw.memory.native` first-party package on the same always-on lane as `builtin` (registered directly into the builtin extension registry, not the catalog/lifecycle extension lane), replacing the anonymous `builtin.memory_*` capabilities. - read/write `implements` the `memory.document_store.v1` profile; search/tree are native conveniences that implement no profile. - Input schemas are served inline by `resolve_native_memory_input_schema_ref` via a provider-keyed `surface.rs` branch — no asset materialization, mirroring the builtin package. - The package is trusted (per-turn `provider_trust` insert + a first-party `AdminEntry`) and granted the /memory mount via the local-dev capability policy, exactly as the builtin memory tools were. Provider-swapping stays on the document-store profile binding. Behavior, I/O, data, and on-by-default availability are unchanged; only the capability identity and the derived model tool names change (builtin__memory_* -> ironclaw__memory__native__*). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t (honor #3537) Path 1 code-constructed the native package in Rust; #3537 explicitly wants a bundled v2 Extension Manifest. This reworks native memory to parse the bundled `assets/memory_native/manifest.toml` and register the resulting package on the SAME always-on first-party lane (not the catalog/lifecycle lane), so it stays unconditionally available with no install/enable step — a v2 manifest on the always-on lane. - The manifest is reshaped from the dormant host_internal/SQL form into four model-visible memory tools: `read`/`write` implement `memory.document_store.v1` (their schema refs match the profile op refs); `search`/`tree` are native conveniences. No required host ports (filesystem-backed); the SQL/audit ports stay catalogued vocabulary for the deferred SQL milestone. - `memory_native_extension::native_memory_first_party_package()` parses the TOML (via `ExtensionManifestRecord`) into an `ExtensionPackage`; the composition registry insertion + trust + /memory grant from the prior commit are reused unchanged (same native capability ids). - Input schemas are served inline on the always-on lane via `include_str!` of the bundled asset files (the single source of truth) — no materialization. Prompt docs are added (required for model visibility) and bundled likewise. - Conformance tests updated to the lean scope: native satisfies `memory.document_store.v1`; the context_retrieval/interaction_log profiles remain defined for the deferred host-managed flow with no live implementer. `NATIVE_MEMORY_FIRST_PARTY_PROVIDER` now aliases the canonical `NATIVE_MEMORY_EXTENSION_ID` (single identity source). Behavior, I/O, data, and on-by-default availability remain unchanged from the prior commit; this changes the manifest authoring form (code -> bundled v2 TOML). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Update memory-profiles.md and ADR 0002 to reflect that ironclaw.memory.native is now live: its bundled v2 TOML manifest is parsed and registered on the always-on first-party lane, implementing memory.document_store.v1 via model-facing read/write tools (search/tree are native conveniences). The live provider is filesystem-backed and declares no host ports; the SQL/audit ports stay catalogued for the deferred SQL-backed milestone. The context_retrieval/interaction_log profiles remain defined with no live implementer (deferred host-managed flow). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… ids The projection display-preview summarizer keys on the `memory_<op>` short names via `capability_matches`, which matched `builtin.memory_<op>` by suffix. The native tools are now `ironclaw.memory.native.<op>` (suffix `.<op>`, not `.memory_<op>`), so memory input summaries — including write-content secret redaction — would have silently stopped rendering in production. Teach `capability_matches` the new id shape and update the projection test fixtures to the native ids so they exercise it. Also rename the now-misnamed `builtin_memory_search_dispatches_*` host_runtime test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 7
♻️ Duplicate comments (1)
crates/ironclaw_host_runtime/assets/memory_native/schemas/memory/document-read.input.v1.json (1)
4-7: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winConstrain
pathto scoped relative form.The schema still accepts absolute and
../traversal paths, so the read tool does not enforce the relative-path contract documented inread.mdand the manifest. Add the same pattern guard here.Based on the manifest wiring and the previous schema review note.
🐛 Suggested schema hardening
"path": { "type": "string", "description": "Relative memory document path to read" + , + "pattern": "^(?!/)(?!.*(?:^|/)\\.\\.(?:/|$))(?!.*\\\\)[A-Za-z0-9._/-]+$" }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/ironclaw_host_runtime/assets/memory_native/schemas/memory/document-read.input.v1.json` around lines 4 - 7, The document read input schema still allows unsafe path values; tighten the `path` field in the `memory/document-read.input.v1.json` schema to match the scoped relative-path contract. Update the `path` definition used by the read tool so it rejects absolute paths and `../` traversal, mirroring the same pattern guard used in the manifest and other memory schemas.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/ironclaw_host_runtime/assets/memory_native/manifest.toml`:
- Line 20: The manifest has duplicated capability table headers in the
memory_native bundle, which creates empty capability entries before the real
ones. Remove the extra repeated [[capabilities]] headers so each capability
block in manifest.toml maps to only one table, and verify the populated entries
remain under the existing capability sections.
In
`@crates/ironclaw_host_runtime/assets/memory_native/schemas/memory/document-write.input.v1.json`:
- Around line 4-40: The document-write schema currently allows invalid mixed or
incomplete requests because it only uses a strict object shape without enforcing
write-mode invariants. Update the schema in document-write.input.v1.json to add
explicit required fields and conditional branches (for plain writes, patch-mode
writes using old_string/new_string/replace_all, and the bootstrap clear path) so
the accepted shapes are mutually exclusive and invalid payloads like empty
objects or mixed modes are rejected at the schema boundary. Use the existing
target, content, append, old_string, new_string, replace_all, and bootstrap
fields as the anchors for these rules.
In
`@crates/ironclaw_host_runtime/assets/memory_native/schemas/memory/document-write.output.v1.json`:
- Around line 4-16: The document-write output schema does not actually
discriminate fields by status, so outcome-specific properties can appear or be
missing incorrectly. Update the schema in document-write.output.v1.json to use
status-based validation with oneOf or if/then/else so cleared, patched, and
written each require only their own fields and forbid the others. Keep the
existing status, path, and additionalProperties constraints, and make the rules
explicit around the status enum plus the message, replacements, and append
properties.
In
`@crates/ironclaw_host_runtime/assets/memory_native/schemas/memory/search.input.v1.json`:
- Around line 4-29: The memory search schema currently advertises alias fields
in search.input.v1.json, but the required constraint still forces query, so
q/text/pattern payloads fail validation. Update the schema so the search
contract accepts at least one of the canonical query inputs (for example via
oneOf/anyOf around query, q, text, and pattern) and clearly define how conflicts
are handled, or remove the alias fields entirely if they are not supported.
In
`@crates/ironclaw_host_runtime/assets/memory_native/schemas/memory/search.output.v1.json`:
- Around line 10-17: The search output schema still allows free-form values for
path, which can expose absolute or parent-relative paths to the model. Tighten
the memory/search.output.v1.json schema so the path field uses the same
scoped-relative restriction as the read schema, and keep the existing search
result shape in sync by updating the path property definition in the search
result schema.
In
`@crates/ironclaw_host_runtime/assets/memory_native/schemas/memory/tree.output.v1.json`:
- Around line 2-4: The tree schema currently uses the untyped items field in
memory/tree.output.v1.json, which removes validation for array elements
entirely. Update the schema so the array items are constrained to the actual
node shape used by the memory tree, ideally with a typed object or union that
distinguishes document vs directory entries, and keep the schema aligned with
the runtime types referenced by the tree output contract.
In `@crates/ironclaw_reborn_composition/src/local_dev_capability_policy.toml`:
- Around line 117-126: Remove the default network grant from the live
ironclaw.memory.native capability entries in local_dev_capability_policy.toml so
the native memory search/write grants match the filesystem-only trust model.
Update the relevant grants for ironclaw.memory.native.search and
ironclaw.memory.native.write to keep only the dispatch/filesystem authority
already reflected in factory.rs and runtime/local_dev.rs, and ensure no network
= "default" remains for these always-on memory tools.
---
Duplicate comments:
In
`@crates/ironclaw_host_runtime/assets/memory_native/schemas/memory/document-read.input.v1.json`:
- Around line 4-7: The document read input schema still allows unsafe path
values; tighten the `path` field in the `memory/document-read.input.v1.json`
schema to match the scoped relative-path contract. Update the `path` definition
used by the read tool so it rejects absolute paths and `../` traversal,
mirroring the same pattern guard used in the manifest and other memory schemas.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: a99c0ebd-432e-42a6-8503-2dd5dab92f8c
📒 Files selected for processing (25)
crates/ironclaw_host_runtime/assets/memory_native/manifest.tomlcrates/ironclaw_host_runtime/assets/memory_native/prompts/memory-native/read.mdcrates/ironclaw_host_runtime/assets/memory_native/prompts/memory-native/search.mdcrates/ironclaw_host_runtime/assets/memory_native/prompts/memory-native/tree.mdcrates/ironclaw_host_runtime/assets/memory_native/prompts/memory-native/write.mdcrates/ironclaw_host_runtime/assets/memory_native/schemas/memory/document-read.input.v1.jsoncrates/ironclaw_host_runtime/assets/memory_native/schemas/memory/document-read.output.v1.jsoncrates/ironclaw_host_runtime/assets/memory_native/schemas/memory/document-write.input.v1.jsoncrates/ironclaw_host_runtime/assets/memory_native/schemas/memory/document-write.output.v1.jsoncrates/ironclaw_host_runtime/assets/memory_native/schemas/memory/search.input.v1.jsoncrates/ironclaw_host_runtime/assets/memory_native/schemas/memory/search.output.v1.jsoncrates/ironclaw_host_runtime/assets/memory_native/schemas/memory/tree.input.v1.jsoncrates/ironclaw_host_runtime/assets/memory_native/schemas/memory/tree.output.v1.jsoncrates/ironclaw_host_runtime/src/first_party_tools/memory.rscrates/ironclaw_host_runtime/src/first_party_tools/mod.rscrates/ironclaw_host_runtime/src/first_party_tools/schemas.rscrates/ironclaw_host_runtime/src/lib.rscrates/ironclaw_host_runtime/src/memory_native_extension.rscrates/ironclaw_host_runtime/src/surface.rscrates/ironclaw_host_runtime/tests/memory_native_manifest.rscrates/ironclaw_reborn_composition/src/factory.rscrates/ironclaw_reborn_composition/src/local_dev_capability_policy.tomlcrates/ironclaw_reborn_composition/src/runtime/local_dev.rsdocs/adr/0002-native-memory-uses-host-storage-ports.mddocs/reborn/contracts/memory-profiles.md
…untime The native-memory consolidation (ffbd298, dc2fbb5) moved the memory_* capabilities out of the builtin package into the always-on `ironclaw.memory.native` package and reshaped the bundled input schemas to the four live document-store tools, but the host_runtime test suite + two schemas were left asserting the old shape — leaving `Test ironclaw_host_runtime` red on 18 memory tests. - first_party_builtin_tools.rs: resolve the memory capabilities from the native package (register it in the test registry + add its first-party trust entry) and drop the memory ids from `all_builtin_capability_ids`. - memory_native_schema_validation.rs: validate the four live tool schemas (read/write/search/tree); the removed context-retrieve / interaction-record schemas belong to the deferred host-managed flow and are no longer bundled. - search.input.v1.json: accept the `q`/`text`/`pattern` aliases that `MemoryServiceSearchRequest::from_tool_input` already honors (anyOf), so a model call using an alias is not rejected at the pre-dispatch schema boundary. - document-read.input.v1.json: reject empty and absolute paths at the schema (minLength + `^[^/]` pattern), matching the scoped-path contract. - ironclaw_memory service.rs: restore the pre-lift null-target handling — an explicit JSON `null` write target is treated as omitted (daily_log) rather than rejected, the #4547 behavior the lift had regressed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ackage Moving the memory_* capabilities into the `ironclaw.memory.native` package left two root-crate tests red, because the e2e harness and the e2e coverage allowlist still assumed memory lived in `builtin`: - reborn_trace_first_party_tool_coverage.rs: build the covered-capability set from the union of the builtin and native-memory packages, so the always-on first-party surface (which now spans two packages) is fully checked. - tests/support/reborn/harness.rs: register `native_memory_first_party_package` in the core-builtins runtime (via a shared `core_builtins_extension_registry` so the two core-builtins runtimes cannot drift), trust the native provider at the host-policy and per-run authority levels, and scope memory grants to filesystem effects so they fit the native provider's tight authority ceiling. Fixes `reborn_builtin_first_party_capability_e2e_coverage_is_complete` and `reborn_trace_memory_first_party_tools_parity` on `Reborn root tests` and `Tests (all-features)`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/ironclaw_memory/src/service.rs`:
- Around line 74-84: The `target` fallback in `service.rs` is currently
unreachable on the schema-validated path because `document-write.input.v1.json`
still only allows a string. Update the schema and the
`parse_write_command`/input parsing logic together so `target: null` is either
accepted intentionally or removed from the compatibility handling, and then add
a regression test covering the intended `target` behavior through the
schema-driven flow.
In `@tests/support/reborn/harness.rs`:
- Around line 2078-2094: The native-memory trust tuple added for
core_builtin_tools_from_runtime is still missing from
HostRuntimeCapabilityHarness::qa_smoke_tools(), where the MEMORY_* capability
IDs are still wired to builtin with no additional_provider_trust. Extract the
new trust tuple into a shared helper and reuse it in both harness constructors
so qa_smoke_tools() also trusts
ExtensionId::new(NATIVE_MEMORY_FIRST_PARTY_PROVIDER) with the same
DispatchCapability, ReadFilesystem, and WriteFilesystem effects. This keeps
create_capability_port() authorization aligned with the split provider model and
prevents QA-smoke memory grants from failing closed as untrusted.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 8ab5cbdb-b69d-41e2-a595-8e13de85095f
📒 Files selected for processing (7)
crates/ironclaw_host_runtime/assets/memory_native/schemas/memory/document-read.input.v1.jsoncrates/ironclaw_host_runtime/assets/memory_native/schemas/memory/search.input.v1.jsoncrates/ironclaw_host_runtime/tests/first_party_builtin_tools.rscrates/ironclaw_host_runtime/tests/memory_native_schema_validation.rscrates/ironclaw_memory/src/service.rstests/reborn_trace_first_party_tool_coverage.rstests/support/reborn/harness.rs
Make the memory document-store provider swappable to mem0 entirely through config, with no hardcoded native assumption in the kernel — demonstrating the #3537 memory architecture is genuinely pluggable. Follows the established `ironclaw_embeddings::create_provider` config-driven-factory idiom. - ironclaw_host_runtime/memory_provider.rs: remove the hardwired native-or-none logic; `MemoryServiceResolver` is now a provider-agnostic registry (`BTreeMap<extension_id, Arc<dyn MemoryService>>` + a `with_third_party_document_store_provider` builder). `resolve_document_store` matches the binding (Native -> build native / ThirdParty(id) -> registered instance, None if unregistered / Disabled -> None). It names no concrete third-party provider, so host_runtime keeps zero provider deps. - crates/ironclaw_memory_mem0: new provider crate implementing MemoryService over the mem0 REST API. Real reqwest transport behind a `Mem0Transport` trait (SSRF-checked base URL, `Authorization: Token` header) with a panic-free mock for tests. Depends only on ironclaw_memory + ironclaw_host_api. - ironclaw_reborn_composition: `create_document_store_provider(binding, deps)` factory (embeddings idiom: match Native/ThirdParty/Disabled, build mem0 over its real transport with check_base_url, fail-closed None on missing creds) plus `build_memory_service_resolver` that registers third-party providers; wired into all three resolver-construction sites in factory.rs. - Config: `[memory] mem0_base_url` + env MEMORY_MEM0_{API_KEY,BASE_URL,APP_ID} (API key as SecretString), mirroring EmbeddingsConfig. - Tests: end-to-end swap proof (config -> policy -> factory -> registry -> resolve_document_store returns mem0, not native; write+search route through the mem0 mock transport), mem0 unit tests, and a caller-level tool-dispatch test proving the unchanged ironclaw.memory.native.* tools transparently route to mem0 under a binding. Architecture boundary allowlist updated for the new crate (composition may depend on it; host_runtime may not). Scope: document-store swap only. The host-managed retrieve/record lifecycle and SQL storage-port backing remain the deferred #5264 follow-ups. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 7
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/ironclaw_architecture/tests/reborn_dependency_boundaries.rs`:
- Around line 1599-1611: The `ironclaw_memory_mem0` boundary entry only enforces
its allowed dependencies and does not actually prevent `ironclaw_host_runtime`
from naming this crate. Add an explicit boundary rule for
`ironclaw_host_runtime` in `reborn_dependency_boundaries.rs` that excludes
`ironclaw_memory_mem0`, or revise the nearby comment to describe the restriction
as intended rather than enforced. Use the existing `BoundaryRule` entries in the
test file to keep the new rule consistent with the other crate boundary
definitions.
In `@crates/ironclaw_memory_mem0/src/service.rs`:
- Around line 186-205: The mem0 read and profile paths only consume the first
list page, so results beyond page 1 are missed. Update the list-fetch logic in
the relevant service methods in service.rs to keep following mem0 pagination
using the page/next fields until all memories are collected, then rebuild the
document/profile from the full set. Apply this to the code paths that perform
the list call and reconstruct results so reads and profile_read do not stop at
the first page.
In `@crates/ironclaw_memory_mem0/src/transport.rs`:
- Around line 201-205: The mem0 body parsing in the transport boundary currently
hides malformed JSON by using `serde_json::from_str(&text).unwrap_or_default()`,
which silently swallows parse errors. Update the body handling in `transport.rs`
to either propagate the `serde_json::from_str` failure from this external IO
boundary or replace it with an explicitly documented tolerated fallback marked
with a `silent-ok` rationale and a log that records only the parse error; keep
the `body` construction in the same `let body = if text.trim().is_empty()` flow.
- Around line 157-160: The reqwest client built in Mem0HttpTransport::new is
missing a timeout, so add a bounded client timeout directly in the
reqwest::Client::builder chain before build() and keep the existing error
mapping intact. Use the existing client construction path in transport.rs to
ensure all mem0 HTTP requests inherit the limit rather than relying on upstream
callers.
In `@crates/ironclaw_memory_mem0/src/url_check.rs`:
- Around line 28-57: Reject non-path URL components in check_base_url: the
current reqwest::Url validation in url_check.rs still allows credentials, query
strings, and fragments in Mem0 base URLs. Update check_base_url to fail closed
when parsed.username()/parsed.password() are present, or when parsed.query() or
parsed.fragment() is set, and return Mem0Error::InvalidUrl with a clear reason.
Keep the existing scheme/host/IP checks intact while enforcing this stricter
boundary on base URLs.
In `@crates/ironclaw_reborn_composition/src/factory.rs`:
- Around line 3284-3287: The startup path is currently failing open when the
selected memory provider cannot be constructed, so update the
`build_memory_service_resolver()` flow used in `factory.rs` to fail closed
instead of returning a resolver without the third-party provider. Make the
resolver construction or the surrounding production validation return a
`Result`, and map missing or rejected provider handles from
`MemoryProviderDeps::for_third_party(...)` to `RebornBuildError::InvalidConfig`
before reporting ready. Apply the same fix to both call sites that build
`memory_resolver`, so production and migration-dry-run profiles refuse startup
when the selected memory provider is unavailable.
In `@crates/ironclaw_reborn_composition/src/lib.rs`:
- Around line 231-234: The crate root is re-exporting internal provider factory
seams instead of only facade-shaped composition APIs. Remove the public exposure
of MemoryProviderDeps, Mem0ConnectionConfig, and create_document_store_provider
from the lib.rs re-export, and keep them private or pub(crate) within
memory_provider_factory; if tests need access, gate that access behind
test-support rather than the production crate root. Keep
build_memory_service_resolver as the only surfaced composition entrypoint if it
is the intended facade.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 76613def-41db-434d-8a35-094f1930e00d
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock,!**/Cargo.lock
📒 Files selected for processing (20)
Cargo.tomlcrates/ironclaw_architecture/tests/reborn_dependency_boundaries.rscrates/ironclaw_host_runtime/src/first_party_tools/memory.rscrates/ironclaw_host_runtime/src/memory_provider.rscrates/ironclaw_memory_mem0/Cargo.tomlcrates/ironclaw_memory_mem0/src/config.rscrates/ironclaw_memory_mem0/src/error.rscrates/ironclaw_memory_mem0/src/lib.rscrates/ironclaw_memory_mem0/src/service.rscrates/ironclaw_memory_mem0/src/transport.rscrates/ironclaw_memory_mem0/src/url_check.rscrates/ironclaw_reborn_cli/src/runtime/mod.rscrates/ironclaw_reborn_composition/Cargo.tomlcrates/ironclaw_reborn_composition/src/factory.rscrates/ironclaw_reborn_composition/src/input.rscrates/ironclaw_reborn_composition/src/lib.rscrates/ironclaw_reborn_composition/src/memory_binding.rscrates/ironclaw_reborn_composition/src/memory_provider_factory.rscrates/ironclaw_reborn_composition/tests/memory_mem0_swap.rscrates/ironclaw_reborn_config/src/config_file.rs
Add a full-pipeline (`load_memory_snippets`) negative test proving the host drops memory-context snippets whose resolved tenant/user scope does not match the request scope, even when the provider returns them — keeping only the in-scope snippet. The scope guard was unit-tested at the `admit_*` level; this exercises it end-to-end against a malicious or buggy provider, which is now a live possibility with config-bound third-party providers like mem0 (#5264). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Re-target the mem0 document-store provider from mem0's hosted cloud API to a self-hosted mem0 open-source instance on localhost — no dependency on api.mem0.ai and no cloud API key. Proven end-to-end against a real local stack (mem0ai + Qdrant + an Ollama embedder): store -> search-recall -> verbatim read round-trips, with the data physically in the local vector store. - transport.rs: the API key is now optional (`Option<&str>` — the auth header is sent only when a key is present); add `.timeout(30s)` + `.redirect(none)` (review hardening, finding #2). - service.rs: local OSS paths (`/memories`, `/search`, `GET /memories?user_id=`) instead of the hosted `/v1/memories/...`; `add` sends `infer:false` so mem0 stores content verbatim (document-store semantics need only the embedder, not the extraction LLM). `profile_set` is now field-preserving (read-merge-write, latest selected by `created_at`) instead of last-writer-wins (review finding #1 — no more silent profile-field loss); merge + infer-false unit tests added. - lib.rs: extension id `mem0.cloud.memory` -> `mem0.local.memory`; docs rewritten to the local OSS surface. - composition factory: build the provider with an optional key (no longer fails closed on a missing key); reborn_cli defaults `MEMORY_MEM0_BASE_URL` to `http://localhost:8888`; config doc updated. - swap-test fixtures updated to the local API; new `tests/live_local_mem0.rs` (`#[ignore]`'d) drives the real transport against a running local mem0. The document-store swap is proven at the provider level. The full LLM-agent loop using memory remains blocked on this branch by the pre-existing #5206 worker-pool stall (fixed on main) and is a tracked follow-up (#5264). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
crates/ironclaw_memory_mem0/src/service.rs (1)
236-246: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winSort document parts before concatenating mem0 reads.
Line 246 joins matched memories in backend list order, but Line 454 notes mem0/Qdrant listing is not chronological. Multiple writes to the same
targetcan therefore read back in nondeterministic order.Suggested fix
- let parts: Vec<String> = response_items(&response.body) + let mut parts: Vec<(String, usize, String)> = response_items(&response.body) .into_iter() + .enumerate() - .filter(|item| item_metadata_str(item, TARGET_KEY) == Some(request.path.as_str())) - .filter_map(|item| item_text(item).map(str::to_string)) + .filter(|(_, item)| item_metadata_str(item, TARGET_KEY) == Some(request.path.as_str())) + .filter_map(|(index, item)| { + item_text(item) + .map(|text| (item_created_at(item).to_string(), index, text.to_string())) + }) .collect(); if parts.is_empty() { return Err(MemoryServiceError::input()); } - let content = parts.join("\n"); + parts.sort_by(|(left_created_at, left_index, _), (right_created_at, right_index, _)| { + left_created_at + .cmp(right_created_at) + .then(left_index.cmp(right_index)) + }); + let content = parts + .into_iter() + .map(|(_, _, part)| part) + .collect::<Vec<_>>() + .join("\n");Also applies to: 454-456
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/ironclaw_memory_mem0/src/service.rs` around lines 236 - 246, The reconstructed document in the mem0 read path is joined in backend list order, but that order is not guaranteed to be chronological. Update the logic in the response processing block that builds `parts` from `response_items(&response.body)` so the matched memories are sorted deterministically before `join("\n")`, using the available metadata/timestamp fields from the item payload. Apply the same ordering assumption consistently with the note in the later mem0/Qdrant listing logic so repeated writes to the same `target` read back in a stable order.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/ironclaw_memory_mem0/src/service.rs`:
- Around line 122-142: The fallback in fetch_latest_profile_object is silently
discarding malformed profile JSON and turning it into an empty object, which can
cause profile_set to overwrite existing fields. Update this method to
distinguish “no prior profile” from “parse failed” by propagating serde_json
parse errors as MemoryServiceError instead of using unwrap_or_default. Keep the
empty-object fallback only for genuinely missing profile data from
latest_profile_text/response_items, and ensure any intentional silent fallback
is explicitly justified with a silent-ok comment if retained.
In `@crates/ironclaw_reborn_config/src/config_file.rs`:
- Around line 122-131: Validate memory.mem0_base_url alongside the other
operator-supplied string fields in the config validation path, not just bindings
and overrides. Update the validation logic in config_file.rs so the
mem0_base_url value is trimmed and checked for inline-secret/private-URL
patterns the same way the existing operator-supplied strings are handled, and
reject whitespace-only or credential-bearing URLs before provider startup. Use
the existing config validation helpers and the mem0_base_url field on the config
struct as the main points to update.
---
Outside diff comments:
In `@crates/ironclaw_memory_mem0/src/service.rs`:
- Around line 236-246: The reconstructed document in the mem0 read path is
joined in backend list order, but that order is not guaranteed to be
chronological. Update the logic in the response processing block that builds
`parts` from `response_items(&response.body)` so the matched memories are sorted
deterministically before `join("\n")`, using the available metadata/timestamp
fields from the item payload. Apply the same ordering assumption consistently
with the note in the later mem0/Qdrant listing logic so repeated writes to the
same `target` read back in a stable order.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: a49e4842-2956-4e21-a069-008dc1fef1b0
📒 Files selected for processing (8)
crates/ironclaw_memory_mem0/src/lib.rscrates/ironclaw_memory_mem0/src/service.rscrates/ironclaw_memory_mem0/src/transport.rscrates/ironclaw_memory_mem0/tests/live_local_mem0.rscrates/ironclaw_reborn_cli/src/runtime/mod.rscrates/ironclaw_reborn_composition/src/memory_provider_factory.rscrates/ironclaw_reborn_composition/tests/memory_mem0_swap.rscrates/ironclaw_reborn_config/src/config_file.rs
Remove the `http://localhost:8888` default for the mem0 connection base URL so mem0 is fully opt-in: it activates only when an operator both binds the document-store profile to it AND supplies a base URL (the `[memory]` config or `MEMORY_MEM0_BASE_URL`). A bound-but-unconfigured mem0 fails closed in the factory, and the binding policy already defaults to native — so the shipped default memory layer is unchanged (native filesystem); mem0 never engages unless explicitly configured. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@coderabbitai full review |
✅ Action performedFull review finished. |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
crates/ironclaw_reborn_cli/src/runtime/mod.rs (1)
693-702: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winFail loud on unreadable mem0 env settings.
optional_nonempty_env()usesstd::env::var(name).ok(), soNotUnicodeis treated as “unset”. ForMEMORY_MEM0_BASE_URLthis can fall back to config, and forMEMORY_MEM0_API_KEYit silently drops auth. DistinguishNotPresentfrom unreadable values and propagate the latter.As per path instructions, “Fail loud” flags silent boundary/settings failures.
Suggested fix
- let mem0_base_url = optional_nonempty_env("MEMORY_MEM0_BASE_URL").or_else(|| { + let mem0_base_url = optional_nonempty_env("MEMORY_MEM0_BASE_URL")?.or_else(|| { config_file .as_ref() .and_then(|file| file.memory.as_ref()) .and_then(|memory| memory.mem0_base_url.clone()) }); let memory_provider_connection = ironclaw_reborn_composition::Mem0ConnectionConfig { base_url: mem0_base_url, - api_key: optional_nonempty_env("MEMORY_MEM0_API_KEY").map(SecretString::from), - app_id: optional_nonempty_env("MEMORY_MEM0_APP_ID"), + api_key: optional_nonempty_env("MEMORY_MEM0_API_KEY")?.map(SecretString::from), + app_id: optional_nonempty_env("MEMORY_MEM0_APP_ID")?, };-fn optional_nonempty_env(name: &str) -> Option<String> { - std::env::var(name) - .ok() - .map(|value| value.trim().to_string()) - .filter(|value| !value.is_empty()) +fn optional_nonempty_env(name: &str) -> anyhow::Result<Option<String>> { + match std::env::var(name) { + Ok(value) => Ok(Some(value.trim().to_string()).filter(|value| !value.is_empty())), + Err(std::env::VarError::NotPresent) => Ok(None), + Err(std::env::VarError::NotUnicode(_)) => { + Err(anyhow::anyhow!("{name} must be valid UTF-8")) + } + } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/ironclaw_reborn_cli/src/runtime/mod.rs` around lines 693 - 702, The mem0 environment lookup is treating unreadable values as missing, which causes silent fallback for MEMORY_MEM0_BASE_URL and drops MEMORY_MEM0_API_KEY auth data. Update optional_nonempty_env (or add a dedicated helper used here in runtime/mod.rs) to distinguish NotPresent from NotUnicode and return an error for unreadable env vars instead of converting them to None. Then have the Mem0ConnectionConfig construction for memory_provider_connection propagate that error so invalid settings fail loudly rather than falling back to config or empty credentials.Source: Path instructions
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@crates/ironclaw_reborn_cli/src/runtime/mod.rs`:
- Around line 693-702: The mem0 environment lookup is treating unreadable values
as missing, which causes silent fallback for MEMORY_MEM0_BASE_URL and drops
MEMORY_MEM0_API_KEY auth data. Update optional_nonempty_env (or add a dedicated
helper used here in runtime/mod.rs) to distinguish NotPresent from NotUnicode
and return an error for unreadable env vars instead of converting them to None.
Then have the Mem0ConnectionConfig construction for memory_provider_connection
propagate that error so invalid settings fail loudly rather than falling back to
config or empty credentials.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 55a7d7c2-3186-44d1-8944-01f81c13a281
📒 Files selected for processing (2)
crates/ironclaw_reborn_cli/src/runtime/mod.rscrates/ironclaw_reborn_config/src/config_file.rs
There was a problem hiding this comment.
Actionable comments posted: 11
♻️ Duplicate comments (7)
crates/ironclaw_host_runtime/src/memory_binding.rs (1)
347-353: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winNormalize override extension ids before matching.
Line 350 still does a byte-for-byte comparison between the bound
extension_idand the admin override. A case-only difference likeacme.honchovsAcme.Honchomakes production resolution reject the override and fail boot even though the extension id is semantically the same. Lowercase both sides once before theany(...)check. As per path instructions, "When comparing user-supplied strings ... extension names, normalize to lowercase with.to_ascii_lowercase()for case-insensitive comparison."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/ironclaw_host_runtime/src/memory_binding.rs` around lines 347 - 353, The override lookup in memory_binding’s certified-bindings path compares extension_id byte-for-byte, so case-only differences can prevent a valid admin override from matching. Normalize both the bound extension_id and each override.extension_id to lowercase with to_ascii_lowercase() before the any(...) check in the requires_certified_bindings block, keeping the existing profile_id and deployment_profile matching logic unchanged.Source: Path instructions
crates/ironclaw_memory_mem0/src/transport.rs (1)
223-227: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winDo not hide malformed mem0 response bodies.
Lines 223-227 still collapse
serde_json::from_str(&text)failures into a normalValue::Nullresponse. That masks upstream protocol breaks at an external IO boundary and makes them indistinguishable from an actually empty body. Either propagate the parse error, or keep the null fallback only with an explicit// silent-ok: ...rationale and a debug log of the parse failure. As per path instructions, "Fail loud: flag silent-failure patterns," and as per coding guidelines, "When a fallback is genuinely acceptable, it must be justified inline with a// silent-ok: <reason>comment naming the operation."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/ironclaw_memory_mem0/src/transport.rs` around lines 223 - 227, The mem0 response parsing in transport.rs is hiding malformed JSON by falling back to Value::Null in the body construction. Update the response handling around the serde_json::from_str path to either propagate the parse error from the transport boundary or, if null is truly acceptable, add an inline // silent-ok: reason comment naming the parse operation and emit a debug log with the parse failure. Keep the change localized to the body parsing logic so empty bodies and malformed bodies are no longer indistinguishable.Sources: Coding guidelines, Path instructions
crates/ironclaw_reborn_composition/src/lib.rs (1)
231-234: 📐 Maintainability & Code Quality | 🟠 Major | ⚖️ Poor tradeoffCrate root still re-exports provider-factory seams.
MemoryProviderDeps,Mem0ConnectionConfig, andcreate_document_store_providerare wiring/test seams, not facade-shaped composition API. As per coding guidelines (ironclaw_reborn_composition: expose facade-shaped handles only; keep lower substrate handles private) and path instructions (nopub usere-exports unless for downstream consumers), keep thesepub(crate)and gate any integration-test access behindtest-support.build_memory_service_resolveris the intended facade entrypoint.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/ironclaw_reborn_composition/src/lib.rs` around lines 231 - 234, The crate root is re-exporting provider-factory test/wiring seams that should stay private. Remove the public re-exports of MemoryProviderDeps, Mem0ConnectionConfig, and create_document_store_provider from the lib.rs facade, keep them pub(crate) inside memory_provider_factory, and expose any test-only access through test-support instead; keep build_memory_service_resolver as the only public composition entrypoint.Sources: Coding guidelines, Path instructions
crates/ironclaw_reborn_composition/src/local_dev_capability_policy.toml (1)
117-144: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winDrop
network = "default"from the live native-memory grants.The live
ironclaw.memory.nativeprovider is filesystem-backed only in this PR (the trust entries infactory.rsgrant only dispatch + read/write filesystem). Keepingnetwork = "default"onsearch/write/read/treehands the always-on memory tools broader authority than the implementation needs. As per coding guidelines (crates/**/*.rs: fail closed for network policy).Suggested fix
capability = "ironclaw.memory.native.search" effects = ["dispatch_capability", "read_filesystem"] mounts = "memory" -network = "default" @@ capability = "ironclaw.memory.native.write" effects = ["dispatch_capability", "read_filesystem", "write_filesystem"] mounts = "memory" -network = "default" @@ capability = "ironclaw.memory.native.read" effects = ["dispatch_capability", "read_filesystem"] mounts = "memory" -network = "default" @@ capability = "ironclaw.memory.native.tree" effects = ["dispatch_capability", "read_filesystem"] mounts = "memory" -network = "default"🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/ironclaw_reborn_composition/src/local_dev_capability_policy.toml` around lines 117 - 144, The live native-memory grants in the local dev policy are over-permissive by still setting network = "default" on the ironclaw.memory.native.search, ironclaw.memory.native.write, ironclaw.memory.native.read, and ironclaw.memory.native.tree entries. Remove the network setting from these native-memory grant blocks in local_dev_capability_policy.toml so they match the filesystem-only trust model used by factory.rs and fail closed for network access.Source: Coding guidelines
crates/ironclaw_reborn_cli/src/runtime/mod.rs (1)
693-704: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick winConfig-sourced
mem0_base_urlreaches the connection unchecked.The env path runs through
optional_nonempty_env(trim + non-empty), but the[memory].mem0_base_urlfallback is cloned verbatim — no trim, no inline-secret reject. The root cause is the missing config-layer validation flagged inconfig_file.rs; fixing it there closes this path too.check_base_urlin the transport is a last line of defense, not a substitute for the secrets guard.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/ironclaw_reborn_cli/src/runtime/mod.rs` around lines 693 - 704, The config fallback for Mem0 base URL is bypassing the same validation applied to env values, so fix the config-layer handling in the `config_file` path and ensure `[memory].mem0_base_url` is trimmed, non-empty validated, and rejected if it contains inline secrets before it reaches `Mem0ConnectionConfig`. Keep `optional_nonempty_env` as-is for env vars, but update the config parsing/validation logic so `memory.mem0_base_url` is normalized and checked centrally, then let `runtime/mod.rs` continue wiring it into `with_memory_provider_connection` unchanged.crates/ironclaw_reborn_composition/src/factory.rs (1)
3284-3287: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy liftProduction startup fails open when the bound memory provider can't be built.
build_memory_service_resolverlogs and returns a resolver without the third-party provider when mem0 URL/credentials are missing or rejected, so production reports ready while configured memory dispatch fails closed only at invocation time. Per coding guidelines (ironclaw_reborn_composition: production/migration-dry-run must fail closed on missing required handles), convert this to aResultor add a production validation step mapping a missing-selected-provider handle toRebornBuildError::InvalidConfig. Also applies to Lines 3324-3327 (Postgres arm).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/ironclaw_reborn_composition/src/factory.rs` around lines 3284 - 3287, The resolver setup in `build_memory_service_resolver` is allowing startup to succeed even when the selected third-party memory provider cannot be constructed, so update the `memory_resolver` path to fail closed by propagating a `Result` or validating the selected handle and converting missing/invalid production config into `RebornBuildError::InvalidConfig`. Apply the same production validation to the Postgres branch as well, and use the existing `MemoryProviderDeps::for_third_party` and `build_memory_service_resolver` flow as the entry points for the fix so the unsupported/missing provider is rejected before readiness is reported.Source: Coding guidelines
crates/ironclaw_reborn_config/src/config_file.rs (1)
945-980: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
memory.mem0_base_urlskips the inline-secret/trim guard.The
memoryvalidation arm checks bindings/overrides but never touchesmem0_base_url(Line 132). A credential-bearing or whitespace-only URL pasted here bypassesreject_inline_secret/trim and only fails later at transport construction. As per coding guidelines (crates/**/*.rs: do not expose raw secrets or private URLs across public surfaces).Suggested fix
if let Some(memory) = &self.memory { + if let Some(mem0_base_url) = &memory.mem0_base_url { + check_non_empty_trimmed(Cow::Borrowed("memory.mem0_base_url"), mem0_base_url)?; + } for (idx, binding) in memory.profile_bindings.iter().enumerate() {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/ironclaw_reborn_config/src/config_file.rs` around lines 945 - 980, The `memory` validation block in `config_file.rs` validates `profile_bindings` and `admin_overrides` but skips `mem0_base_url`, allowing whitespace-only or secret-like values to bypass the same trim/inline-secret checks. Update the `if let Some(memory) = &self.memory` section to validate `memory.mem0_base_url` with the existing `check_non_empty_trimmed` and `reject_inline_secret` helpers before validating the bindings/overrides, using the same `memory.mem0_base_url` field path pattern as the other fields.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@crates/ironclaw_host_runtime/assets/memory_native/prompts/memory-native/read.md`:
- Around line 1-3: The `read` prompt is incorrectly implying it can return
profile facts, but profile data is accessed through
`MemoryService::profile_read` and not the path-based document `read(path)`
surface. Update the `read.md` wording to describe only persistent document
content for notes/logs/etc., and remove “profile facts” so the prompt stays
aligned with `user_profile_source.rs` and the `MemoryService::profile_read` /
`builtin.profile_set` contract.
In
`@crates/ironclaw_host_runtime/assets/memory_native/schemas/memory/document-read.output.v1.json`:
- Around line 4-8: The output schema for the memory document read response
currently leaves word_count optional, even though NativeMemoryService::read()
and the mem0 read() path always populate it through MemoryServiceReadResponse.
Update the document-read.output.v1.json schema so word_count is included in the
required fields alongside path and content, and keep the field definition
aligned with the existing schema entry to enforce the tool contract.
In
`@crates/ironclaw_host_runtime/assets/memory_native/schemas/memory/document-write.output.v1.json`:
- Around line 10-13: The schema descriptions in the document-write output no
longer match the shared document-store contract, so update the
native/profile-level schema to reflect the actual statuses returned by the write
flow. In the document-write output schema and the profile registration in
memory_profiles, revise the field docs for
message/replacements/append/content_length so they describe all valid write
outcomes from the service.rs write path, or split the native-specific and shared
contracts if the meanings differ. Use the document-write.output.v1 schema and
the memory.document_store.v1 profile as the key places to align.
In
`@crates/ironclaw_host_runtime/assets/memory_native/schemas/memory/search.input.v1.json`:
- Around line 4-18: Tighten the search request schema in the memory search input
JSON so empty strings and ambiguous alias combinations are rejected at the
boundary. Update the schema around the query-related fields (`query`, `q`,
`text`, `pattern`) to require non-empty values and enforce exactly one alias
key, or else clearly define canonical precedence directly in the schema. Use the
existing search input schema definition to make the validation unambiguous
before downstream normalization.
In `@crates/ironclaw_host_runtime/src/user_profile_source.rs`:
- Around line 50-57: The `from_filesystem` helper in `UserProfileSource` is
bypassing the configured document-store binding by always constructing
`NativeMemoryService` directly. Update this path to use the already-resolved
`Arc<dyn MemoryService>` from `memory_provider`’s fail-closed selection logic,
or rename/restrict `from_filesystem` so it is explicitly native-only and cannot
be used as a generic memory-capability source. Ensure `UserProfileSource::new`
receives the bound `MemoryService` so profile reads and writes stay on the same
backend as `profile_set`.
In `@crates/ironclaw_host_runtime/tests/first_party_builtin_tools.rs`:
- Around line 8612-8617: The native-memory trust wiring in this test harness is
incomplete: trust_policy() is still granting ironclaw.memory.native the full
builtin_effects() ceiling, and provider_trust() only maps builtin, so
CapabilityCatalog::visible_capabilities can omit native providers and hide
least-privilege regressions. Update the trust helpers in
first_party_builtin_tools.rs so native memory has its own trust entry and a
narrower policy derived from its manifest’s declared filesystem effects, and
ensure provider_trust() includes ironclaw.memory.native alongside builtin so
surface tests expose native descriptors correctly.
In `@crates/ironclaw_memory_mem0/src/error.rs`:
- Around line 21-22: The `InvalidUrl` error in `error.rs` is leaking the
configured mem0 URL through its `Display` message. Update the `thiserror`
variant so `InvalidUrl` keeps the `reason` but omits or redacts the raw `url`
value, and make sure any caller constructing or logging this error no longer
exposes the configured URL on public error surfaces.
In `@crates/ironclaw_memory_mem0/src/service.rs`:
- Around line 181-218: The `MemoryService::write` path is silently treating
non-append writes as adds by always calling `Mem0HttpRequest::post(ADD_PATH,
...)` and returning `append: true`. Update `write` to explicitly reject any
request where `request.append` is false, alongside the existing unsupported
patch/empty-content checks, and return an operation/unsupported error instead of
proceeding. Keep the behavior aligned with the other fail-closed branches in
`write`, using the `MemoryServiceWriteRequest` fields and
`MemoryServiceError::operation_from` / `Mem0Error::Unsupported` for the
unsupported case.
- Around line 236-246: The document reconstruction in the mem0 service is using
raw response order, which can scramble multiple fragments for the same target.
Update the logic in response_items/item_metadata_str/item_text handling to sort
the matching items by created_at before building the final content, and use a
stable fallback ordering for items without timestamps so append order is
preserved. Keep the existing filtering by TARGET_KEY and join only after the
ordered list is produced, so the round-trip behavior promised by the module docs
is enforced.
In `@crates/ironclaw_reborn_composition/src/memory_provider_factory.rs`:
- Around line 119-123: The selected mem0 binding can currently be left active
even when no provider was created, because create_document_store_provider and
the resolver wiring in build_memory_service_resolver collapse unsupported ids,
missing config, and transport failures into None. Make this path fallible
instead: return an error when the chosen MemoryProviderBinding cannot be
realized, and propagate that error out of build_memory_service_resolver so
startup fails with context rather than registering a half-initialized resolver.
Use the existing create_document_store_provider and
MemoryProviderBinding/MemoryService symbols to locate the affected flow, and
avoid warn-and-continue behavior that leaves a selected binding without a
provider.
In `@docs/reborn/contracts/memory-profiles.md`:
- Around line 96-100: Update the Non-goals section so it no longer states there
is no Honcho/mem0 provider implementation, since the new mem0 crate and
composition wiring/tests already bind memory.document_store.v1 to mem0. Reword
that bullet to reflect the actual intent, such as that mem0 is not the default
or not certified, and keep the other non-goals unchanged.
---
Duplicate comments:
In `@crates/ironclaw_host_runtime/src/memory_binding.rs`:
- Around line 347-353: The override lookup in memory_binding’s
certified-bindings path compares extension_id byte-for-byte, so case-only
differences can prevent a valid admin override from matching. Normalize both the
bound extension_id and each override.extension_id to lowercase with
to_ascii_lowercase() before the any(...) check in the
requires_certified_bindings block, keeping the existing profile_id and
deployment_profile matching logic unchanged.
In `@crates/ironclaw_memory_mem0/src/transport.rs`:
- Around line 223-227: The mem0 response parsing in transport.rs is hiding
malformed JSON by falling back to Value::Null in the body construction. Update
the response handling around the serde_json::from_str path to either propagate
the parse error from the transport boundary or, if null is truly acceptable, add
an inline // silent-ok: reason comment naming the parse operation and emit a
debug log with the parse failure. Keep the change localized to the body parsing
logic so empty bodies and malformed bodies are no longer indistinguishable.
In `@crates/ironclaw_reborn_cli/src/runtime/mod.rs`:
- Around line 693-704: The config fallback for Mem0 base URL is bypassing the
same validation applied to env values, so fix the config-layer handling in the
`config_file` path and ensure `[memory].mem0_base_url` is trimmed, non-empty
validated, and rejected if it contains inline secrets before it reaches
`Mem0ConnectionConfig`. Keep `optional_nonempty_env` as-is for env vars, but
update the config parsing/validation logic so `memory.mem0_base_url` is
normalized and checked centrally, then let `runtime/mod.rs` continue wiring it
into `with_memory_provider_connection` unchanged.
In `@crates/ironclaw_reborn_composition/src/factory.rs`:
- Around line 3284-3287: The resolver setup in `build_memory_service_resolver`
is allowing startup to succeed even when the selected third-party memory
provider cannot be constructed, so update the `memory_resolver` path to fail
closed by propagating a `Result` or validating the selected handle and
converting missing/invalid production config into
`RebornBuildError::InvalidConfig`. Apply the same production validation to the
Postgres branch as well, and use the existing
`MemoryProviderDeps::for_third_party` and `build_memory_service_resolver` flow
as the entry points for the fix so the unsupported/missing provider is rejected
before readiness is reported.
In `@crates/ironclaw_reborn_composition/src/lib.rs`:
- Around line 231-234: The crate root is re-exporting provider-factory
test/wiring seams that should stay private. Remove the public re-exports of
MemoryProviderDeps, Mem0ConnectionConfig, and create_document_store_provider
from the lib.rs facade, keep them pub(crate) inside memory_provider_factory, and
expose any test-only access through test-support instead; keep
build_memory_service_resolver as the only public composition entrypoint.
In `@crates/ironclaw_reborn_composition/src/local_dev_capability_policy.toml`:
- Around line 117-144: The live native-memory grants in the local dev policy are
over-permissive by still setting network = "default" on the
ironclaw.memory.native.search, ironclaw.memory.native.write,
ironclaw.memory.native.read, and ironclaw.memory.native.tree entries. Remove the
network setting from these native-memory grant blocks in
local_dev_capability_policy.toml so they match the filesystem-only trust model
used by factory.rs and fail closed for network access.
In `@crates/ironclaw_reborn_config/src/config_file.rs`:
- Around line 945-980: The `memory` validation block in `config_file.rs`
validates `profile_bindings` and `admin_overrides` but skips `mem0_base_url`,
allowing whitespace-only or secret-like values to bypass the same
trim/inline-secret checks. Update the `if let Some(memory) = &self.memory`
section to validate `memory.mem0_base_url` with the existing
`check_non_empty_trimmed` and `reject_inline_secret` helpers before validating
the bindings/overrides, using the same `memory.mem0_base_url` field path pattern
as the other fields.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 3042703e-1241-405f-96ad-8d60d5af5069
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock,!**/Cargo.lock
📒 Files selected for processing (66)
Cargo.tomlcrates/ironclaw_architecture/tests/reborn_dependency_boundaries.rscrates/ironclaw_host_api/src/host_port.rscrates/ironclaw_host_runtime/assets/memory_native/manifest.tomlcrates/ironclaw_host_runtime/assets/memory_native/prompts/memory-native/read.mdcrates/ironclaw_host_runtime/assets/memory_native/prompts/memory-native/search.mdcrates/ironclaw_host_runtime/assets/memory_native/prompts/memory-native/tree.mdcrates/ironclaw_host_runtime/assets/memory_native/prompts/memory-native/write.mdcrates/ironclaw_host_runtime/assets/memory_native/schemas/memory/document-read.input.v1.jsoncrates/ironclaw_host_runtime/assets/memory_native/schemas/memory/document-read.output.v1.jsoncrates/ironclaw_host_runtime/assets/memory_native/schemas/memory/document-write.input.v1.jsoncrates/ironclaw_host_runtime/assets/memory_native/schemas/memory/document-write.output.v1.jsoncrates/ironclaw_host_runtime/assets/memory_native/schemas/memory/search.input.v1.jsoncrates/ironclaw_host_runtime/assets/memory_native/schemas/memory/search.output.v1.jsoncrates/ironclaw_host_runtime/assets/memory_native/schemas/memory/tree.input.v1.jsoncrates/ironclaw_host_runtime/assets/memory_native/schemas/memory/tree.output.v1.jsoncrates/ironclaw_host_runtime/src/extension_contracts.rscrates/ironclaw_host_runtime/src/first_party_tools/memory.rscrates/ironclaw_host_runtime/src/first_party_tools/mod.rscrates/ironclaw_host_runtime/src/first_party_tools/schemas.rscrates/ironclaw_host_runtime/src/lib.rscrates/ironclaw_host_runtime/src/memory_binding.rscrates/ironclaw_host_runtime/src/memory_context.rscrates/ironclaw_host_runtime/src/memory_native_extension.rscrates/ironclaw_host_runtime/src/memory_profiles.rscrates/ironclaw_host_runtime/src/memory_provider.rscrates/ironclaw_host_runtime/src/surface.rscrates/ironclaw_host_runtime/src/user_profile_source.rscrates/ironclaw_host_runtime/tests/first_party_builtin_tools.rscrates/ironclaw_host_runtime/tests/memory_capability_profiles.rscrates/ironclaw_host_runtime/tests/memory_native_manifest.rscrates/ironclaw_host_runtime/tests/memory_native_schema_validation.rscrates/ironclaw_host_runtime/tests/memory_prompt_context.rscrates/ironclaw_host_runtime/tests/user_profile_roundtrip.rscrates/ironclaw_memory/src/lib.rscrates/ironclaw_memory/src/service.rscrates/ironclaw_memory_mem0/Cargo.tomlcrates/ironclaw_memory_mem0/src/config.rscrates/ironclaw_memory_mem0/src/error.rscrates/ironclaw_memory_mem0/src/lib.rscrates/ironclaw_memory_mem0/src/service.rscrates/ironclaw_memory_mem0/src/transport.rscrates/ironclaw_memory_mem0/src/url_check.rscrates/ironclaw_memory_mem0/tests/live_local_mem0.rscrates/ironclaw_memory_native/src/service.rscrates/ironclaw_memory_native/tests/memory_service_facade.rscrates/ironclaw_reborn_cli/src/runtime/mod.rscrates/ironclaw_reborn_composition/Cargo.tomlcrates/ironclaw_reborn_composition/src/factory.rscrates/ironclaw_reborn_composition/src/input.rscrates/ironclaw_reborn_composition/src/lib.rscrates/ironclaw_reborn_composition/src/local_dev_capability_policy.tomlcrates/ironclaw_reborn_composition/src/memory_binding.rscrates/ironclaw_reborn_composition/src/memory_provider_factory.rscrates/ironclaw_reborn_composition/src/projection/display_preview.rscrates/ironclaw_reborn_composition/src/projection/tests/display_preview.rscrates/ironclaw_reborn_composition/src/runtime.rscrates/ironclaw_reborn_composition/src/runtime/local_dev.rscrates/ironclaw_reborn_composition/tests/memory_mem0_swap.rscrates/ironclaw_reborn_config/src/config_file.rscrates/ironclaw_reborn_config/src/lib.rsdocs/adr/0001-capability-manifest-v2-hard-cutover.mddocs/adr/0002-native-memory-uses-host-storage-ports.mddocs/reborn/contracts/memory-profiles.mdtests/reborn_trace_first_party_tool_coverage.rstests/support/reborn/harness.rs
| Read a persistent memory document by relative path, scoped to the current | ||
| tenant/user/agent/project. Returns the document content and its word count. | ||
| Use it to recall previously written notes, logs, or profile facts before acting. |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Don’t steer read toward profile facts.
Line 3 says this tool recalls “profile facts”, but host profile context comes from MemoryService::profile_read, not the document-store read(path) surface, and the write contract already routes timezone/locale/location through builtin.profile_set. This prompt teaches the model to reach for a path-based read that cannot fetch the profile document.
Proposed fix
-Use it to recall previously written notes, logs, or profile facts before acting.
+Use it to recall previously written notes or logs before acting.Based on learnings from the cross-file contract, crates/ironclaw_host_runtime/src/user_profile_source.rs uses MemoryService::profile_read, and as per coding guidelines, "Comments that promise guarantees across layers must either be enforced by code/tests or softened to describe intent."
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| Read a persistent memory document by relative path, scoped to the current | |
| tenant/user/agent/project. Returns the document content and its word count. | |
| Use it to recall previously written notes, logs, or profile facts before acting. | |
| Read a persistent memory document by relative path, scoped to the current | |
| tenant/user/agent/project. Returns the document content and its word count. | |
| Use it to recall previously written notes or logs before acting. |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@crates/ironclaw_host_runtime/assets/memory_native/prompts/memory-native/read.md`
around lines 1 - 3, The `read` prompt is incorrectly implying it can return
profile facts, but profile data is accessed through
`MemoryService::profile_read` and not the path-based document `read(path)`
surface. Update the `read.md` wording to describe only persistent document
content for notes/logs/etc., and remove “profile facts” so the prompt stays
aligned with `user_profile_source.rs` and the `MemoryService::profile_read` /
`builtin.profile_set` contract.
Source: Coding guidelines
| "path": { "type": "string", "description": "Relative memory document path that was read" }, | ||
| "content": { "type": "string", "description": "Full document content" }, | ||
| "word_count": { "type": "integer", "description": "Word count of the document content" } | ||
| }, | ||
| "required": ["path", "content"], |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
Require word_count in the output schema.
Both NativeMemoryService::read() and the mem0 read() path always return word_count via MemoryServiceReadResponse, but this schema leaves it optional. That weakens the tool contract and lets a future provider silently drop the field without failing schema validation.
Suggested fix
- "required": ["path", "content"],
+ "required": ["path", "content", "word_count"],📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "path": { "type": "string", "description": "Relative memory document path that was read" }, | |
| "content": { "type": "string", "description": "Full document content" }, | |
| "word_count": { "type": "integer", "description": "Word count of the document content" } | |
| }, | |
| "required": ["path", "content"], | |
| "path": { "type": "string", "description": "Relative memory document path that was read" }, | |
| "content": { "type": "string", "description": "Full document content" }, | |
| "word_count": { "type": "integer", "description": "Word count of the document content" } | |
| }, | |
| "required": ["path", "content", "word_count"], |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@crates/ironclaw_host_runtime/assets/memory_native/schemas/memory/document-read.output.v1.json`
around lines 4 - 8, The output schema for the memory document read response
currently leaves word_count optional, even though NativeMemoryService::read()
and the mem0 read() path always populate it through MemoryServiceReadResponse.
Update the document-read.output.v1.json schema so word_count is included in the
required fields alongside path and content, and keep the field definition
aligned with the existing schema entry to enforce the tool contract.
| "message": { "type": "string", "description": "Human-readable note (cleared status)" }, | ||
| "replacements": { "type": "integer", "description": "Number of replacements made (patched status)" }, | ||
| "content_length": { "type": "integer", "description": "Resulting content length in bytes" }, | ||
| "append": { "type": "boolean", "description": "Whether the write appended (written status)" } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
These descriptions no longer match the shared document-store contract.
crates/ironclaw_memory_mem0/src/service.rs:181-219 emits status = Written with message = Some(...), but this schema still documents message as cleared-only. Because crates/ironclaw_host_runtime/src/memory_profiles.rs:55-85 publishes this as the profile-level memory.document_store.v1 contract, consumers will infer the wrong invariants unless the descriptions are broadened or the native-only/profile-level schemas are split.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@crates/ironclaw_host_runtime/assets/memory_native/schemas/memory/document-write.output.v1.json`
around lines 10 - 13, The schema descriptions in the document-write output no
longer match the shared document-store contract, so update the
native/profile-level schema to reflect the actual statuses returned by the write
flow. In the document-write output schema and the profile registration in
memory_profiles, revise the field docs for
message/replacements/append/content_length so they describe all valid write
outcomes from the service.rs write path, or split the native-specific and shared
contracts if the meanings differ. Use the document-write.output.v1 schema and
the memory.document_store.v1 profile as the key places to align.
| "query": { | ||
| "type": "string", | ||
| "description": "Preferred natural language search query for persistent memory" | ||
| }, | ||
| "q": { | ||
| "type": "string", | ||
| "description": "Alias for query" | ||
| }, | ||
| "text": { | ||
| "type": "string", | ||
| "description": "Alias for query" | ||
| }, | ||
| "pattern": { | ||
| "type": "string", | ||
| "description": "Alias for query" |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
Tighten the query contract.
Line 28 only checks field presence, so {"query": ""} and conflicting alias payloads like {"query":"a","text":"b"} both validate. That leaves malformed/ambiguous requests for downstream normalization instead of rejecting them at the boundary. Add minLength: 1 to the query aliases and enforce exactly one alias key, or document canonical precedence in-schema.
Suggested schema tightening
"query": {
"type": "string",
+ "minLength": 1,
"description": "Preferred natural language search query for persistent memory"
},
"q": {
"type": "string",
+ "minLength": 1,
"description": "Alias for query"
},
"text": {
"type": "string",
+ "minLength": 1,
"description": "Alias for query"
},
"pattern": {
"type": "string",
+ "minLength": 1,
"description": "Alias for query"
},
@@
- "anyOf": [
+ "oneOf": [
{ "required": ["query"] },
{ "required": ["q"] },
{ "required": ["text"] },
{ "required": ["pattern"] }
],Also applies to: 28-33
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@crates/ironclaw_host_runtime/assets/memory_native/schemas/memory/search.input.v1.json`
around lines 4 - 18, Tighten the search request schema in the memory search
input JSON so empty strings and ambiguous alias combinations are rejected at the
boundary. Update the schema around the query-related fields (`query`, `q`,
`text`, `pattern`) to require non-empty values and enforce exactly one alias
key, or else clearly define canonical precedence directly in the schema. Use the
existing search input schema definition to make the validation unambiguous
before downstream normalization.
| /// Build a source backed by the native memory provider over `filesystem`. | ||
| /// The host owns the provider choice here (matching the memory capability), | ||
| /// while reads still flow through the provider-neutral | ||
| /// [`MemoryService::profile_read`]. | ||
| pub fn from_filesystem(filesystem: Arc<dyn RootFilesystem>) -> Self { | ||
| Self::new(Arc::new(NativeMemoryService::from_filesystem( | ||
| filesystem, None, | ||
| ))) |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
from_filesystem bypasses the configured document-store binding.
This helper always builds NativeMemoryService, but the PR’s binding contract says memory.document_store.v1 is selected through the fail-closed resolver. crates/ironclaw_host_runtime/src/memory_provider.rs:127-147 already has that selection logic. Any caller that uses from_filesystem will read profiles from native storage even when profile_set is bound to a third-party provider, splitting profile writes and reads across backends. Thread the resolved Arc<dyn MemoryService> in here, or make this helper explicitly native-only so it cannot masquerade as “matching the memory capability”.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/ironclaw_host_runtime/src/user_profile_source.rs` around lines 50 -
57, The `from_filesystem` helper in `UserProfileSource` is bypassing the
configured document-store binding by always constructing `NativeMemoryService`
directly. Update this path to use the already-resolved `Arc<dyn MemoryService>`
from `memory_provider`’s fail-closed selection logic, or rename/restrict
`from_filesystem` so it is explicitly native-only and cannot be used as a
generic memory-capability source. Ensure `UserProfileSource::new` receives the
bound `MemoryService` so profile reads and writes stay on the same backend as
`profile_set`.
| #[error("invalid mem0 base URL '{url}': {reason}")] | ||
| InvalidUrl { url: String, reason: String }, |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Do not format raw configured URLs into provider errors.
Line 21 exposes the configured mem0 URL through Display; this can leak credentials, query tokens, or private hosts when construction errors are logged. Keep the cause, but omit or redact the URL field.
Suggested fix
- #[error("invalid mem0 base URL '{url}': {reason}")]
- InvalidUrl { url: String, reason: String },
+ #[error("invalid mem0 base URL: {reason}")]
+ InvalidUrl { reason: String },As per coding guidelines, crates/**/*.rs: “Do not expose raw secrets, backend paths, private URLs, transport internals … across public surfaces.”
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| #[error("invalid mem0 base URL '{url}': {reason}")] | |
| InvalidUrl { url: String, reason: String }, | |
| #[error("invalid mem0 base URL: {reason}")] | |
| InvalidUrl { reason: String }, |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/ironclaw_memory_mem0/src/error.rs` around lines 21 - 22, The
`InvalidUrl` error in `error.rs` is leaking the configured mem0 URL through its
`Display` message. Update the `thiserror` variant so `InvalidUrl` keeps the
`reason` but omits or redacts the raw `url` value, and make sure any caller
constructing or logging this error no longer exposes the configured URL on
public error surfaces.
Source: Coding guidelines
| async fn write( | ||
| &self, | ||
| invocation: MemoryInvocation, | ||
| request: MemoryServiceWriteRequest, | ||
| ) -> Result<MemoryServiceWriteResponse, MemoryServiceError> { | ||
| // MAPPING GAP: mem0 stores discrete memories, not addressable documents, | ||
| // so it has no in-place substring patch. Fail explicitly rather than | ||
| // silently adding a new memory that ignores the requested edit. | ||
| if request.old_string.is_some() || request.new_string.is_some() { | ||
| return Err(MemoryServiceError::operation_from(Mem0Error::Unsupported { | ||
| operation: "write.patch", | ||
| detail: "mem0 has no in-place substring patch", | ||
| })); | ||
| } | ||
| // MAPPING GAP: an empty write is the native `bootstrap` clear, which has | ||
| // no mem0 analogue (no addressable document to truncate). Treat it as an | ||
| // invalid request, matching the native provider's empty-content rule. | ||
| if request.content.trim().is_empty() { | ||
| return Err(MemoryServiceError::input()); | ||
| } | ||
| let namespace = scope_namespace(&invocation.scope); | ||
| let metadata = json!({ TARGET_KEY: request.target, SOURCE_KEY: SOURCE_VALUE }); | ||
| let body = self.add_body(&namespace, &request.content, metadata); | ||
| let response = self | ||
| .transport | ||
| .execute(Mem0HttpRequest::post(ADD_PATH, body)) | ||
| .await | ||
| .map_err(MemoryServiceError::operation_from)?; | ||
| ensure_success(&response, "write").map_err(MemoryServiceError::operation_from)?; | ||
| Ok(MemoryServiceWriteResponse { | ||
| status: MemoryWriteStatus::Written, | ||
| path: request.target, | ||
| // mem0 is inherently additive: every write adds a memory. | ||
| append: true, | ||
| content_length: request.content.len(), | ||
| replacements: None, | ||
| message: Some("stored as a mem0 memory".to_string()), | ||
| }) |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Reject non-append writes instead of silently turning them into adds.
Line 202 ignores request.append and always emits POST /memories, then Lines 210-215 report append: true back to the caller. A replace-style write (append = false, no patch args) will therefore succeed while leaving the old fragments in place, and read() will later return both versions. This should fail closed as unsupported, like patch/clear, not silently change semantics. As per path instructions, "Fail loud: flag silent-failure patterns".
Suggested fix
if request.content.trim().is_empty() {
return Err(MemoryServiceError::input());
}
+ if !request.append {
+ return Err(MemoryServiceError::operation_from(Mem0Error::Unsupported {
+ operation: "write.replace",
+ detail: "mem0 only supports additive writes",
+ }));
+ }
let namespace = scope_namespace(&invocation.scope);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| async fn write( | |
| &self, | |
| invocation: MemoryInvocation, | |
| request: MemoryServiceWriteRequest, | |
| ) -> Result<MemoryServiceWriteResponse, MemoryServiceError> { | |
| // MAPPING GAP: mem0 stores discrete memories, not addressable documents, | |
| // so it has no in-place substring patch. Fail explicitly rather than | |
| // silently adding a new memory that ignores the requested edit. | |
| if request.old_string.is_some() || request.new_string.is_some() { | |
| return Err(MemoryServiceError::operation_from(Mem0Error::Unsupported { | |
| operation: "write.patch", | |
| detail: "mem0 has no in-place substring patch", | |
| })); | |
| } | |
| // MAPPING GAP: an empty write is the native `bootstrap` clear, which has | |
| // no mem0 analogue (no addressable document to truncate). Treat it as an | |
| // invalid request, matching the native provider's empty-content rule. | |
| if request.content.trim().is_empty() { | |
| return Err(MemoryServiceError::input()); | |
| } | |
| let namespace = scope_namespace(&invocation.scope); | |
| let metadata = json!({ TARGET_KEY: request.target, SOURCE_KEY: SOURCE_VALUE }); | |
| let body = self.add_body(&namespace, &request.content, metadata); | |
| let response = self | |
| .transport | |
| .execute(Mem0HttpRequest::post(ADD_PATH, body)) | |
| .await | |
| .map_err(MemoryServiceError::operation_from)?; | |
| ensure_success(&response, "write").map_err(MemoryServiceError::operation_from)?; | |
| Ok(MemoryServiceWriteResponse { | |
| status: MemoryWriteStatus::Written, | |
| path: request.target, | |
| // mem0 is inherently additive: every write adds a memory. | |
| append: true, | |
| content_length: request.content.len(), | |
| replacements: None, | |
| message: Some("stored as a mem0 memory".to_string()), | |
| }) | |
| async fn write( | |
| &self, | |
| invocation: MemoryInvocation, | |
| request: MemoryServiceWriteRequest, | |
| ) -> Result<MemoryServiceWriteResponse, MemoryServiceError> { | |
| // MAPPING GAP: mem0 stores discrete memories, not addressable documents, | |
| // so it has no in-place substring patch. Fail explicitly rather than | |
| // silently adding a new memory that ignores the requested edit. | |
| if request.old_string.is_some() || request.new_string.is_some() { | |
| return Err(MemoryServiceError::operation_from(Mem0Error::Unsupported { | |
| operation: "write.patch", | |
| detail: "mem0 has no in-place substring patch", | |
| })); | |
| } | |
| // MAPPING GAP: an empty write is the native `bootstrap` clear, which has | |
| // no mem0 analogue (no addressable document to truncate). Treat it as an | |
| // invalid request, matching the native provider's empty-content rule. | |
| if request.content.trim().is_empty() { | |
| return Err(MemoryServiceError::input()); | |
| } | |
| if !request.append { | |
| return Err(MemoryServiceError::operation_from(Mem0Error::Unsupported { | |
| operation: "write.replace", | |
| detail: "mem0 only supports additive writes", | |
| })); | |
| } | |
| let namespace = scope_namespace(&invocation.scope); | |
| let metadata = json!({ TARGET_KEY: request.target, SOURCE_KEY: SOURCE_VALUE }); | |
| let body = self.add_body(&namespace, &request.content, metadata); | |
| let response = self | |
| .transport | |
| .execute(Mem0HttpRequest::post(ADD_PATH, body)) | |
| .await | |
| .map_err(MemoryServiceError::operation_from)?; | |
| ensure_success(&response, "write").map_err(MemoryServiceError::operation_from)?; | |
| Ok(MemoryServiceWriteResponse { | |
| status: MemoryWriteStatus::Written, | |
| path: request.target, | |
| // mem0 is inherently additive: every write adds a memory. | |
| append: true, | |
| content_length: request.content.len(), | |
| replacements: None, | |
| message: Some("stored as a mem0 memory".to_string()), | |
| }) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/ironclaw_memory_mem0/src/service.rs` around lines 181 - 218, The
`MemoryService::write` path is silently treating non-append writes as adds by
always calling `Mem0HttpRequest::post(ADD_PATH, ...)` and returning `append:
true`. Update `write` to explicitly reject any request where `request.append` is
false, alongside the existing unsupported patch/empty-content checks, and return
an operation/unsupported error instead of proceeding. Keep the behavior aligned
with the other fail-closed branches in `write`, using the
`MemoryServiceWriteRequest` fields and `MemoryServiceError::operation_from` /
`Mem0Error::Unsupported` for the unsupported case.
Source: Path instructions
| // MAPPING GAP: mem0 is not path-addressable. Reconstruct a "document" by | ||
| // concatenating every memory tagged with the requested `target`. | ||
| let parts: Vec<String> = response_items(&response.body) | ||
| .into_iter() | ||
| .filter(|item| item_metadata_str(item, TARGET_KEY) == Some(request.path.as_str())) | ||
| .filter_map(|item| item_text(item).map(str::to_string)) | ||
| .collect(); | ||
| if parts.is_empty() { | ||
| return Err(MemoryServiceError::input()); | ||
| } | ||
| let content = parts.join("\n"); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Preserve append order when rebuilding a document.
This joins fragments in raw list order, but the same file already documents that mem0 list responses are not chronological. Two writes to the same target can therefore read back in arbitrary order, which breaks the "round-trip exactly" guarantee in the module docs. Sort matching items by created_at before joining, with a stable fallback for missing timestamps. As per coding guidelines, "Comments that promise guarantees across layers must either be enforced by code/tests or softened to describe intent".
Suggested fix
- let parts: Vec<String> = response_items(&response.body)
- .into_iter()
- .filter(|item| item_metadata_str(item, TARGET_KEY) == Some(request.path.as_str()))
- .filter_map(|item| item_text(item).map(str::to_string))
- .collect();
+ let mut parts: Vec<_> = response_items(&response.body)
+ .into_iter()
+ .enumerate()
+ .filter(|(_, item)| item_metadata_str(item, TARGET_KEY) == Some(request.path.as_str()))
+ .collect();
+ parts.sort_by(|(li, left), (ri, right)| {
+ item_created_at(left)
+ .cmp(item_created_at(right))
+ .then(li.cmp(ri))
+ });
+ let parts: Vec<String> = parts
+ .into_iter()
+ .filter_map(|(_, item)| item_text(item).map(str::to_string))
+ .collect();📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // MAPPING GAP: mem0 is not path-addressable. Reconstruct a "document" by | |
| // concatenating every memory tagged with the requested `target`. | |
| let parts: Vec<String> = response_items(&response.body) | |
| .into_iter() | |
| .filter(|item| item_metadata_str(item, TARGET_KEY) == Some(request.path.as_str())) | |
| .filter_map(|item| item_text(item).map(str::to_string)) | |
| .collect(); | |
| if parts.is_empty() { | |
| return Err(MemoryServiceError::input()); | |
| } | |
| let content = parts.join("\n"); | |
| // MAPPING GAP: mem0 is not path-addressable. Reconstruct a "document" by | |
| // concatenating every memory tagged with the requested `target`. | |
| let mut parts: Vec<_> = response_items(&response.body) | |
| .into_iter() | |
| .enumerate() | |
| .filter(|(_, item)| item_metadata_str(item, TARGET_KEY) == Some(request.path.as_str())) | |
| .collect(); | |
| parts.sort_by(|(li, left), (ri, right)| { | |
| item_created_at(left) | |
| .cmp(item_created_at(right)) | |
| .then(li.cmp(ri)) | |
| }); | |
| let parts: Vec<String> = parts | |
| .into_iter() | |
| .filter_map(|(_, item)| item_text(item).map(str::to_string)) | |
| .collect(); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/ironclaw_memory_mem0/src/service.rs` around lines 236 - 246, The
document reconstruction in the mem0 service is using raw response order, which
can scramble multiple fragments for the same target. Update the logic in
response_items/item_metadata_str/item_text handling to sort the matching items
by created_at before building the final content, and use a stable fallback
ordering for items without timestamps so append order is preserved. Keep the
existing filtering by TARGET_KEY and join only after the ordered list is
produced, so the round-trip behavior promised by the module docs is enforced.
Source: Coding guidelines
| pub fn create_document_store_provider( | ||
| binding: &MemoryProviderBinding, | ||
| deps: &MemoryProviderDeps, | ||
| ) -> Option<Arc<dyn MemoryService>> { | ||
| match binding { |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
Don't boot with a selected mem0 binding but no provider.
This factory collapses unsupported extension ids, missing mem0 connection config, and transport build failures into None, and build_memory_service_resolver then returns a resolver with the third-party binding still selected but no provider registered. The breakage only surfaces later when memory tools resolve the document store, which is exactly the half-initialized warn-and-continue state the repo forbids. Make provider construction and resolver wiring fallible so startup fails with context when the chosen binding cannot actually be realized. As per path instructions, "Fail loud: flag silent-failure patterns," and as per coding guidelines, "Do not use if let Err(e) = ... { warn!(...) } followed by caching/inserting/continuing without propagating the error — it poisons downstream state with half-initialized values."
Also applies to: 137-193, 204-231
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/ironclaw_reborn_composition/src/memory_provider_factory.rs` around
lines 119 - 123, The selected mem0 binding can currently be left active even
when no provider was created, because create_document_store_provider and the
resolver wiring in build_memory_service_resolver collapse unsupported ids,
missing config, and transport failures into None. Make this path fallible
instead: return an error when the chosen MemoryProviderBinding cannot be
realized, and propagate that error out of build_memory_service_resolver so
startup fails with context rather than registering a half-initialized resolver.
Use the existing create_document_store_provider and
MemoryProviderBinding/MemoryService symbols to locate the affected flow, and
avoid warn-and-continue behavior that leaves a selected binding without a
provider.
Sources: Coding guidelines, Path instructions
| ## Non-goals | ||
|
|
||
| - no production manifest v2 parser changes; | ||
| - no profile binding configuration; | ||
| - no native memory implementation; | ||
| - no runtime dispatch changes; | ||
| - no third-party certification flow. | ||
| - no third-party certification flow; | ||
| - no Honcho/mem0 provider implementation; | ||
| - no migration of legacy `memory_documents` rows from this crate. |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Update the non-goals list to match the implementation.
This section still says there is "no Honcho/mem0 provider implementation", but this PR adds crates/ironclaw_memory_mem0 plus the composition wiring/tests that bind memory.document_store.v1 to mem0. Leaving this here makes the contract doc stale on merge; if the intent is "not default" or "not certified", say that explicitly instead.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/reborn/contracts/memory-profiles.md` around lines 96 - 100, Update the
Non-goals section so it no longer states there is no Honcho/mem0 provider
implementation, since the new mem0 crate and composition wiring/tests already
bind memory.document_store.v1 to mem0. Reword that bullet to reflect the actual
intent, such as that mem0 is not the default or not certified, and keep the
other non-goals unchanged.
Reborn: model memory as a userland extension — implements #3537
Implements the bulk of #3537 — Extension Manifest v2 architecture, source-aware trust, host-defined capability profiles + conformance, the memory profile-binding policy, and the always-on native document-store provider. It also includes a config-driven third-party document-store provider backed by a self-hosted, fully-local mem0 (mem0 OSS on localhost — no api.mem0.ai, optional key), demonstrating the provider is swappable entirely through config with no hardcoded native assumption (the
host_runtimeresolver names no concrete provider; construction follows theironclaw_embeddings::create_provideridiom in composition). Proven end-to-end at the provider level against a real local mem0 stack (store -> search-recall -> verbatim read). It does not fully close #3537: native SQL storage-port backing (reborn_memory_*tables), the host-managedmemory.context.retrieve/memory.interaction.recordflow, the default flip +/memorymigration, andmemory.semantic_search.v1remain follow-ups tracked in #5264.Memory is now a host-bundled userland extension that implements host-defined capability profiles, bound per profile through a fail-closed
profile_id → extension_idpolicy — consolidated to a single, always-on native surface.End state: one always-on native memory extension
ironclaw.memory.nativeis a bundled v2 Extension Manifest (schema_version = "reborn.extension_manifest.v2") parsed fromcrates/ironclaw_host_runtime/assets/memory_native/manifest.tomland registered on the always-on first-party lane (the same lane as the builtin toolset) — not the catalog/lifecycle lane — so its tools are unconditionally available with no install/enable step. It declares fourmodel-visible memory tools (read/write/search/tree);read/writeimplementsmemory.document_store.v1(their schema refs match the profile's operation refs), andsearch/treeare native conveniences that implement no profile. The model-facing tool names areironclaw__memory__native__{read,write,search,tree}.This replaces the prior two declarations of the one filesystem-backed provider — the live
builtin.memory_*tools and a separate, dormanthost_internalnative manifest — with one.builtin.memory_*is removed; provider-swapping is governed by the document-store profile binding (config-driven), not by install/enable.The live provider is filesystem-backed and declares no host ports; input schemas are served inline (
include_str!of the bundled asset files, the single source of truth) on the always-on lane rather than materialized. Behavior, I/O, the scoped/memorymount, and on-by-default availability are unchanged frombuiltin.memory_*; only the capability identity + derived tool names change.Kept #3537 machinery
ironclaw_host_runtime::memory_profiles): the three host-defined contracts (memory.context_retrieval.v1,memory.interaction_log.v1,memory.document_store.v1) and the semantic conformance harness. The nativeread/writecapabilities are proven to satisfymemory.document_store.v1.default_host_port_catalog()):host.storage.sql_transaction.first_party+host.events.auditstay catalogued as deferred-future vocabulary.MemoryBindingPolicy, fail-closed; the[memory]config section;MemoryServiceResolversingle construction point): default-native, production rejectsmemory.disabledand unverified third-party bindings absent an(extension_id, profile_id, deployment_profile)admin override.MemoryServicefacade.Deferred (not stubbed)
context_retrieval/interaction_logare defined and conformance-testable but have no live implementer; wiring the host turn pipeline to invokememory.context.retrievebefore model calls and record sanitized interactions viamemory.interaction.recordafterward is deferred. The live native surface ships only the model-facing document-store tools.reborn_memory_*dual-backend tables behindhost.storage.sql_transaction.first_party, behind non-defaultmemory-native-*features (the reborn-composition crates cannot depend on the rootironclawcrate where the Postgres/libSQL backends live). Seedocs/adr/0002-native-memory-uses-host-storage-ports.md.memory.semantic_search.v1— unchanged from the issue's deferral (/memorydata/API compatibility decision; host-mediated embedding port).Gates
cargo fmt --all -- --check— cleancargo clippy -p ironclaw_host_runtime -p ironclaw_reborn_composition --tests --features "root-llm-provider webui-v2-beta libsql"— zero warningscargo test -p ironclaw_host_runtime— green (the 3sandbox_processfailures are pre-existing, Docker-only)cargo test -p ironclaw_reborn_composition --features "root-llm-provider webui-v2-beta libsql"— green (the 1skill_learned_bubblefailure is pre-existing under this feature combo onmain)cargo test -p ironclaw_architecture— green (no new cross-crate dependency edges)python3 scripts/check_no_panics.py --base main --head HEAD— OK🤖 Generated with Claude Code