Skip to content

feat(migration): add offline v1-to-Reborn migration workflow - #5936

Closed
serrrfirat wants to merge 19 commits into
mainfrom
codex/v1-reborn-migration
Closed

serrrfirat wants to merge 19 commits into
mainfrom
codex/v1-reborn-migration

Conversation

@serrrfirat

@serrrfirat serrrfirat commented Jul 10, 2026 •

Copy link
Copy Markdown
Collaborator

Intent

Complete the safe offline v1-to-Reborn migration workflow for plan, apply, resume, verify, status, local-dev libSQL, production PostgreSQL, Docker and source-build companions. Preserve read-only sealed planning, target quarantine, deterministic replay, structural durable-store verification, explicit unsupported-data reporting, and all previously fixed CodeRabbit and review feedback. Address every remaining second-review issue from run 01KXDZDZ1VRJDXXWCACWXBGKS7: persist resumable Applying only after preflight or an atomic freshness claim; exclude only the exact nested Reborn target-owned tree from v1 home inventory; bind verified activation markers to the configured target fingerprint, scope and profile; add atomic run-bound libSQL target claims and durable parent-directory fsync; preserve or explicitly disposition source-agent memory scopes; reject divergent and deduplicate exact engine document duplicates; require absent-or-exact identity replay; pause routines missing next_fire_at; pause automations owned by suspended users; pause or disposition mission references to unmigrated projects; make missing source secret key material an unconditional blocker when secrets exist; make secret import atomic absent-or-exact; tolerate optional missing settings tables; classify heartbeat state truthfully as unsupported or operator action; validate PostgreSQL audit partition names exactly; stream PostgreSQL fingerprints without materializing all rows; report heartbeat loss only for actual heartbeat rows. Choose the safest fail-closed behavior for ambiguous policy cases. Revalidate the whole branch and update complete PR metadata. Live PostgreSQL and Docker end-to-end remain environmental evidence gaps unless available without external unsafe writes.

What Changed

  • Add the ironclaw-reborn migrate v1 plan, apply, resume, verify, and status workflow for libSQL and PostgreSQL, using sealed inventory manifests and durable-store verification.
  • Expand migration support for users, projects, threads, automations, memory, secrets, settings, and triggers with deterministic replay and explicit unsupported-data dispositions.
  • Quarantine runtime and extension activation against target-owned migration state, surface migration status in diagnostics, and package and document the companion for Docker and source builds.

Risk Assessment

🚨 High: The default nested local workflow can poison its own source inventory, while marker races and identity/ownership gaps can leave successful targets quarantined or migrated records inconsistently scoped.

Testing

Automated backend and CLI suites, source-built companion pairs, and end-to-end libSQL plus isolated live PostgreSQL migrations succeeded after focused fixes, with durable reviewer-visible evidence; Docker’s static/entrypoint tests passed, but a live image run remains unverified because no permitted daemon was available.

Evidence: Local-dev libSQL migration lifecycle
{
  "phase": "plan",
  "status": "planned",
  "target_exists": false,
  "target_empty": true,
  "source_settings": {
    "count": 1,
    "disposition": "unsupported"
  },
  "source_users": {
    "count": 1,
    "disposition": "imported"
  }
}
{"phase":"post-plan","target_exists":false}
{
  "phase": "apply",
  "status": "applied",
  "stats": {
    "users": 1,
    "threads": 0,
    "messages": 0,
    "projects": 0,
    "routines": 0,
    "missions": 0,
    "triggers": 0,
    "trigger_runs": 0,
    "jobs": 0,
    "memory_documents": 0,
    "secrets": 0,
    "extensions": 0,
    "identities": 0,
    "heartbeats": 0,
    "settings": 0
  },
  "losses": [
    {
      "domain": "memory",
      "field": "*",
      "reason": "no_target_concept"
    },
    {
      "domain": "identity",
      "field": "*",
      "reason": "no_target_concept"
    },
    {
      "domain": "setting",
      "field": "timezone",
      "reason": "no_target_concept"
    }
  ]
}
{
  "phase": "resume",
  "status": "applied",
  "stats": {
    "users": 1,
    "threads": 0,
    "messages": 0,
    "projects": 0,
    "routines": 0,
    "missions": 0,
    "triggers": 0,
    "trigger_runs": 0,
    "jobs": 0,
    "memory_documents": 0,
    "secrets": 0,
    "extensions": 0,
    "identities": 0,
    "heartbeats": 0,
    "settings": 0
  },
  "losses": [
    {
      "domain": "memory",
      "field": "*",
      "reason": "no_target_concept"
    },
    {
      "domain": "identity",
      "field": "*",
      "reason": "no_target_concept"
    },
    {
      "domain": "setting",
      "field": "timezone",
      "reason": "no_target_concept"
    }
  ]
}
{
  "phase": "status",
  "status": "verified",
  "target_fingerprint_match": true
}
[{"phase":"durable_state","status":"verified","profile":"local-dev","target_backend":"libsql","tenant_id":"reborn-cli","agent_id":"reborn-cli-agent"}]
[{"phase":"durable_users","count":1}]
Evidence: Production PostgreSQL migration lifecycle
{
  "phase": "plan",
  "status": "planned",
  "target_exists": null,
  "target_empty": null,
  "source_settings": {
    "count": 1,
    "disposition": "unsupported"
  },
  "source_users": {
    "count": 1,
    "disposition": "imported"
  }
}
{"phase":"post-plan","target_public_tables":0}
{
  "phase": "apply",
  "status": "applied",
  "stats": {
    "users": 1,
    "threads": 0,
    "messages": 0,
    "projects": 0,
    "routines": 0,
    "missions": 0,
    "triggers": 0,
    "trigger_runs": 0,
    "jobs": 0,
    "memory_documents": 0,
    "secrets": 0,
    "extensions": 0,
    "identities": 0,
    "heartbeats": 0,
    "settings": 0
  },
  "losses": [
    {
      "domain": "memory",
      "field": "*",
      "reason": "no_target_concept"
    },
    {
      "domain": "identity",
      "field": "*",
      "reason": "no_target_concept"
    },
    {
      "domain": "setting",
      "field": "timezone",
      "reason": "no_target_concept"
    }
  ]
}
{
  "phase": "resume",
  "status": "applied",
  "stats": {
    "users": 1,
    "threads": 0,
    "messages": 0,
    "projects": 0,
    "routines": 0,
    "missions": 0,
    "triggers": 0,
    "trigger_runs": 0,
    "jobs": 0,
    "memory_documents": 0,
    "secrets": 0,
    "extensions": 0,
    "identities": 0,
    "heartbeats": 0,
    "settings": 0
  },
  "losses": [
    {
      "domain": "memory",
      "field": "*",
      "reason": "no_target_concept"
    },
    {
      "domain": "identity",
      "field": "*",
      "reason": "no_target_concept"
    },
    {
      "domain": "setting",
      "field": "timezone",
      "reason": "no_target_concept"
    }
  ]
}
{
  "phase": "status",
  "status": "verified",
  "target_fingerprint_match": true
}
{"phase" : "durable_state", "status" : "verified", "profile" : "production", "target_backend" : "postgres", "tenant_id" : "reborn-cli", "agent_id" : "reborn-cli-agent"}
{"phase" : "durable_users", "count" : 1}
- Outcome: ⚠️ 1 warning across 2 runs (42m28s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 10 issues (6 errors, 4 warnings)
  • 🚨 crates/ironclaw_reborn_migration/src/main.rs:275 - Apply persists Applying before source/manifest preflight. A crash or secret-input error in this window leaves an invalid plan quarantined as active, and the unconditional marker overwrite is not a freshness claim. Run preflight before persisting the transition or atomically claim the expected prior state.
  • 🚨 crates/ironclaw_reborn_migration/src/target.rs:109 - Shared migration state is a no-op for libSQL, so concurrent runs can both observe an empty target and write mixed state; direct library callers also have no target-owned quarantine. Add a transactional, run-bound libSQL claim that accepts only absent or exact matching state.
  • 🚨 crates/ironclaw_reborn_cli/src/commands/migrate.rs:288 - Activation accepts a marker using only schema, protocol, and status. Neither local nor PostgreSQL state is compared with the current target fingerprint, profile, tenant, or agent, so a stale verified marker can unlock an unverified target after configuration changes. Persist and require an exact target/scope match.
  • 🚨 crates/ironclaw_reborn_migration/src/main.rs:511 - The marker writer syncs the temporary file but not its parent directory after rename. A power loss can remove the only local quarantine marker after target writes became durable, allowing startup of a partial migration. Fsync the parent directory after marker and manifest installation/removal.
  • 🚨 crates/ironclaw_reborn_migration/src/target.rs:613 - Trigger replay performs get_trigger followed by overwrite-capable upsert_trigger; a divergent record inserted between them is overwritten. Replace this with an atomic insert-if-absent followed by exact conflict reconciliation.
  • ⚠️ crates/ironclaw_reborn_migration/src/target.rs:270 - Verification inserts the configured tenant directly into SQL LIKE patterns. Legal tenant IDs containing % or _ broaden the match and can let other tenant paths satisfy verification counts. Escape wildcard characters and use an explicit SQL escape clause.
  • 🚨 crates/ironclaw_reborn_migration/src/inventory.rs:551 - Home inventory excludes target files but still hashes/counts a newly created target directory and its known-directory ancestors. If the target is nested below a known v1 directory, resume sees migration-created metadata as source drift and cannot continue. Exclude the exact target-owned subtree and its target-only ancestor contribution while retaining siblings.
  • ⚠️ crates/ironclaw_reborn_migration/src/source.rs:297 - PostgreSQL inventory loads every JSON-encoded row with client.query and then copies all rows into another Vec<String>. Large production/audit tables can exhaust migrator memory, and this scan runs repeatedly during plan and preflight. Stream rows into the digest and count incrementally.
  • 🚨 crates/ironclaw_reborn_migration/src/convert/memory.rs:62 - All source memory documents, including unscoped and arbitrary source-agent documents, are flattened into the configured target agent. This silently changes ownership; duplicate paths either abort or inflate expected counts. Decide whether source agent scopes should be preserved or explicitly dispositioned, then keep each representable scope distinct.
  • 🚨 crates/ironclaw_reborn_migration/src/convert/memory.rs:86 - Replay treats matching content with missing/different metadata as irreconcilable, but the native backend commits content and metadata separately. If metadata persistence fails after content commits, every resume fails permanently. Make the pair atomic or safely complete a recognized partial migration write.
  • ⚠️ crates/ironclaw_memory_native/src/backend.rs:452 - The new metadata read checks only file_documents, ignoring the backend's explicit metadata capability. A backend configured with metadata disabled can therefore execute an unsupported operation. Fail closed on capabilities.metadata before repository access.
  • 🚨 crates/ironclaw_reborn_migration/src/convert/automations.rs:124 - An enabled routine with no durable next_fire_at remains scheduled and falls back to created_at, commonly making it immediately overdue after cutover. Pause these routines and record the required operator action, matching the mission behavior.
  • 🚨 crates/ironclaw_reborn_migration/src/convert/automations.rs:124 - Routine and mission trigger state is derived only from the automation record; it never checks the migrated owner's lifecycle. Automations belonging to suspended, deactivated, or synthesized-suspended users can therefore execute after cutover. Force their triggers to Paused.
  • 🚨 crates/ironclaw_reborn_migration/src/convert/automations.rs:401 - Mission triggers and threads retain project_id without confirming that the project converter imported that project. Unparseable or missing project documents can leave scheduled work scoped to a nonexistent project. Track imported project IDs and pause/disposition unresolved references.
  • 🚨 crates/ironclaw_reborn_migration/src/convert/automations.rs:283 - Engine thread documents are inserted into a map with unconditional replacement, while mission duplicates are processed repeatedly. Divergent duplicate documents across layouts/scopes are silently selected by iteration order and exact duplicates inflate counts. Deduplicate exact representations and reject divergent records by durable ID.
  • 🚨 crates/ironclaw_reborn_migration/src/convert/secrets.rs:30 - When source key material is absent, the converter reports one loss and returns success even if source secrets exist, allowing the run to reach Applied/Verified with every secret omitted. Make a missing source key an apply blocker whenever the inventory contains secret rows.
  • 🚨 crates/ironclaw_reborn_migration/src/convert/secrets.rs:181 - Secret import checks metadata/material and then calls replacement-capable put. A concurrent value inserted after the check can be overwritten, violating absent-or-exact replay. Add an atomic insert-if-absent/exact-match import operation.
  • 🚨 crates/ironclaw_reborn_migration/src/inventory.rs:372 - Audit partition recognition checks only suffix length and the m position, so malformed names such as secret_usage_log_yABCDmZZ bypass the unknown-table blocker. Require the exact yYYYYmMM digit shape and a valid month.
  • 🚨 crates/ironclaw_reborn_migration/src/inventory.rs:90 - Inventory labels heartbeat_state as semantically converted even though the converter writes no target state, and the converter reports a loss for every discovered user rather than actual heartbeat rows. Classify nonzero heartbeat data as unsupported/operator action and enumerate only real heartbeat owners.
  • 🚨 crates/ironclaw_reborn_migration/src/convert/settings.rs:25 - Optional historical schemas without a settings table pass discovery but fail here because get_all_settings propagates the missing-table error. Treat only an absent settings table as empty while preserving all other read failures.

🔧 Fix: Harden Reborn migration safety and deterministic replay
10 issues (6 errors, 4 warnings) still open:

  • 🚨 crates/ironclaw_reborn_migration/src/inventory.rs:408 - Home inventory excludes the target database files and key, but not other Reborn-owned control files. With default paths, the v1 home is ~/.ironclaw and Reborn lives under ~/.ironclaw/reborn; config, manifest, and migration-state files therefore become unknown source content or source drift, preventing local apply. Exclude the exact Reborn-owned subtree/control paths without hiding v1 siblings.
  • 🚨 crates/ironclaw_reborn_migration/src/inventory.rs:650 - Excluded target-only ancestor directories are still added to counts and checksums when the containing known directory has a legitimate sibling. Creating a nested target under such a directory changes the sealed inventory and blocks resume. Skip recursively empty excluded-only children before hashing or counting them.
  • 🚨 crates/ironclaw_reborn_migration/src/main.rs:287 - The local Applying marker is installed before the atomic target-owned run claim. Concurrent plans can both pass preflight, and the losing run can later overwrite the winner's local state with its own Planned or Failed marker while durable state remains bound to the winner, permanently quarantining a successful migration. Claim first and condition local updates on the claimed run.
  • ⚠️ crates/ironclaw_reborn_migration/src/main.rs:293 - When the duplicated inner preflight fails, this branch writes a local Planned marker even though no durable claim exists. The activation guard rejects all local-only markers, leaving an otherwise untouched target quarantined after a transient preflight failure. Durably remove/restore the prior marker instead.
  • 🚨 crates/ironclaw_reborn_migration/src/convert/secrets.rs:36 - If source secrets exist but the target secret store is unavailable, the converter records one loss and succeeds. Applied checkpoints then expect zero secrets, allowing verification while every source secret was omitted. Treat missing target encryption/store capability as an apply blocker whenever secret rows exist.
  • 🚨 crates/ironclaw_reborn_migration/src/convert/projects.rs:96 - Duplicate project IDs are compared only after deserializing through a subset DTO and normalizing to ProjectRecord. Source documents differing in ignored engine fields or missing-versus-default representation are accepted as exact duplicates, silently discarding divergent state. Retain and compare the raw parsed representation as well.
  • 🚨 crates/ironclaw_reborn_migration/src/convert/projects.rs:47 - An engine project's embedded user_id overrides the memory-document owner, but synthesized-user discovery sees only table-level owners. An embedded owner with no other source row is never created, so projects and mission threads can reference a nonexistent Reborn user. Decide which owner is authoritative, then reject/report mismatches or synthesize embedded owners before importing references.
  • ⚠️ crates/ironclaw_reborn_migration/src/convert/users.rs:44 - API-token reauthentication losses are enumerated only for canonical users rows. On older schemas without that table, or for orphan token owners, the synthesized user is imported but its token hashes disappear from the per-record apply report. Enumerate tokens for every discovered owner and deduplicate canonical owners.
  • ⚠️ crates/ironclaw_reborn_migration/src/target.rs:1013 - The migrator opens PostgreSQL with a separate legacy TLS policy rather than the composition-owned production policy. It ignores DATABASE_SSLMODE and the explicit remote-cleartext setting; even local prefer behavior differs, so a production-openable target may be unmigratable. Reuse the composition PostgreSQL pool builder and options.
  • ⚠️ crates/ironclaw_reborn_cli/src/commands/doctor.rs:303 - When durable target state is absent, doctor trusts only the raw status string in the onboarding manifest. An edited or corrupted planned manifest containing verified therefore reports a passing verification despite no durable verification state. Validate the sealed manifest and reserve verified for matching target-owned state.
⚠️ **Test** - 1 warning
  • ⚠️ Live PostgreSQL and Docker end-to-end evidence remains unavailable. PostgreSQL 18.4 is reachable, but migration verification would require creating and mutating source/target databases outside the worktree; Docker’s daemon is unavailable, and starting/building it would create prohibited external state. Run those workflows in an authorized disposable environment if live backend evidence is required.
  • cargo test -p ironclaw_reborn_migration --no-default-features --features libsql (initial run found stale lifecycle assertions; clean after test-only fixes)
  • cargo test -p ironclaw_reborn_cli --no-default-features --features libsql --test migration_cli
  • cargo test -p ironclaw_reborn_cli --no-default-features --features libsql --test smoke docker
  • cargo test -p ironclaw_reborn_cli --no-default-features --features libsql --bin ironclaw-reborn commands::migrate::tests
  • cargo test -p ironclaw_secrets filesystem_secret_store_atomic_insert_never_overwrites_divergent_material
  • cargo test -p ironclaw_reborn_identity import_migrated_user
  • cargo test -p ironclaw_triggers --features libsql --test repository_contract libsql_insert_if_absent_never_overwrites
  • cargo test -p ironclaw_reborn_migration --no-default-features --features postgres (feature-path tests; not live PostgreSQL E2E)
  • Built the libSQL companion, seeded a v1 database with a user and no optional settings table, then ran the real ironclaw-reborn migrate v1 plan, apply, resume, verify, and status --json commands
  • Checked target absence after planning with test ! -e "$TARGET", compared source snapshots with shasum -a 256 and diff -u, and queried root_filesystem_entries using sqlite3 -json
  • Read-only environment checks: docker version, docker context show, docker ps, pg_isready, and a read-only psql ... SELECT current_user, current_database(), current_setting('server_version') query
  • git diff --check and final worktree review; removed package build outputs with cargo clean -p ironclaw_reborn_cli -p ironclaw_reborn_migration

🔧 Fix: Align migration tests with run-bound lifecycle
1 warning still open:

  • ⚠️ Dockerfile.reborn - Live Docker image build/runtime evidence remains unavailable. Dockerfile and entrypoint smoke coverage passed, but the Docker daemon is not running; starting Docker Desktop or building through another external daemon would violate this run’s system-state boundary. Run the image build and migration entrypoint in an authorized Docker environment if live container evidence is required.
  • bash scripts/codebase-graph.sh status (graph unavailable; used documented guidance/targeted search fallback)
  • cargo test -p ironclaw_reborn_migration --no-default-features --features libsql
  • cargo test -p ironclaw_reborn_migration --no-default-features --features postgres
  • cargo test -p ironclaw_reborn_cli (initial failures diagnosed and fixed; focused retry passed)
  • cargo test -p ironclaw_reborn_cli --features libsql
  • cargo test -p ironclaw_reborn_cli --features postgres --test smoke
  • cargo build -p ironclaw_reborn_cli --features libsql --bin ironclaw-reborn plus matching libSQL migration companion build
  • Primary CLI plan → apply → resume → verify → status --json against an isolated v1 libSQL snapshot, followed by durable SQLite state/user readback
  • cargo build -p ironclaw_reborn_cli --features postgres --bin ironclaw-reborn plus matching PostgreSQL migration companion build
  • Initialized PostgreSQL 18 under the evidence directory, applied the v1 schema, ran primary CLI plan → apply → resume → verify → status --json, queried durable shared state/user rows, then stopped PostgreSQL
  • Dockerfile/entrypoint behavior exercised by the CLI smoke suites
  • docker info --format '{{.ServerVersion}}' (daemon unavailable)
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-5936 July 10, 2026 12:50 Destroyed
@ironloopai

ironloopai Bot commented Jul 10, 2026 •

Copy link
Copy Markdown
Contributor

🔎 IronLoop Review Status

Head: c64d03054e5e2f199d0e65109c74fd9c14175003
Result: 1 blocking finding across 1 reviewer.
Next: Address the blocking findings, push fixes, then re-run the relevant reviewer.
Updated: 2026-07-10T13:04:27.420Z

Current reviewers:

Reviewer State Verdict Findings Last update
ironloop/common-reviewer (reviewer) Completed Changes requested 1 blocking finding / 1 note 2026-07-10T13:04:27.279Z
Reviewer summaries
Reviewer Detail
ironloop/common-reviewer (reviewer) Changes requested; 1 blocking finding; Found one blocking migration lifecycle bug and one non-blocking strict-mode bug. Cargo-based validation could not be run because cargo is not installed in the reviewer environment.
Recent activity
Time Reviewer State Detail
2026-07-10T12:50:24.689Z ironloop/common-reviewer (reviewer) Queued Accepted review request for head c64d030.
2026-07-10T12:50:24.689Z ironloop/common-reviewer (reviewer) Queued Waiting for this reviewer lane to become available.
2026-07-10T12:50:24.795Z ironloop/common-reviewer (reviewer) Queued Added to the local review work handoff.
2026-07-10T12:50:25.629Z ironloop/common-reviewer (reviewer) Started Reviewer worker started attempt 1.
2026-07-10T12:50:28.478Z ironloop/common-reviewer (reviewer) Workspace ready Prepared isolated checkout (merge_ref) at b1284cb.
2026-07-10T13:04:15.488Z ironloop/common-reviewer (reviewer) Running Codex is reviewing; process live; elapsed 13m 49s; timeout in 6m 12s; last heartbeat 2026-07-10T13:04:15.488Z. Codex emitted stderr output at 2026-07-10T13:03:54.681Z.
2026-07-10T13:04:27.279Z ironloop/common-reviewer (reviewer) Result captured Changes requested; 1 blocking finding.
2026-07-10T13:04:27.279Z ironloop/common-reviewer (reviewer) Completed Review completed and terminal status was persisted.
Available commands
  • @ironloopai help
  • @ironloopai agents
  • @ironloopai review
  • @ironloopai review --agent <agent>
  • @ironloopai status
Run metadata

Admission: webhook accepted the request and IronLoop persisted reviewer state before this projection.

@coderabbitai

coderabbitai Bot commented Jul 10, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added offline v1-to-Reborn migration with plan, apply, resume, verify, and status commands.
    • Docker and source builds now include the same-release migration companion binary.
    • Added onboarding options to plan or skip v1 migration.
    • Added deterministic, resumable migration manifests with safety checks and structural verification.
    • Expanded migration coverage for users, projects, memory, conversations, secrets, and scheduled automations.
  • Bug Fixes

    • Normal startup and extension commands now stop when migration state is incomplete or invalid.
    • Added safeguards against overwriting conflicting migrated data.
  • Documentation

    • Added operator guidance for migration, cutover, rollback, Docker usage, and onboarding.

Walkthrough

The PR adds a manifest-driven v1-to-Reborn migration lifecycle with a same-release companion CLI, read-only source inspection, deterministic target writes, onboarding and quarantine gates, Docker packaging, backend support, and operator documentation.

Changes

v1 migration bridge

Layer / File(s) Summary
Migration contracts and source inspection
crates/ironclaw_reborn_migration/src/{manifest,options,report,source,inventory}.rs
Defines sealed manifests, lifecycle states, source fingerprints, inventory dispositions, split-key inputs, read-only source access, and backend inventory collection.
Plan, apply, resume, and verify
crates/ironclaw_reborn_migration/src/{lib,main,target}.rs
Adds lifecycle execution, integrity checks, atomic state persistence, target emptiness checks, and structural durable readback verification.
Deterministic conversion
crates/ironclaw_reborn_migration/src/convert/*, src/target_ids.rs
Adds user/project conversion, deterministic trigger/thread/message identifiers, compare-before-write behavior, memory conflict checks, and disposition-only extension handling.
CLI and runtime gating
crates/ironclaw_reborn_cli/src/commands/*, src/context.rs
Adds companion discovery and handshake validation, migration argument forwarding, onboarding states, doctor reporting, and quarantine checks before activation.
Composition, identity, memory, and persistence contracts
crates/ironclaw_reborn_composition/src/*, crates/ironclaw_reborn_identity/src/*, crates/ironclaw_memory*/src/*, crates/ironclaw_secrets/src/*, crates/ironclaw_triggers/src/*
Adds non-activating target resolution, migrated-user conflict handling, metadata reads, atomic secret writes, and insert-if-absent trigger persistence.
Packaging and validation
Dockerfile.reborn, docker/reborn/entrypoint.sh, src/db/libsql/mod.rs, crates/ironclaw_reborn_migration/tests/*, crates/ironclaw_reborn_cli/tests/*
Packages the companion, adds read-only source opening, bypasses target seeding for migration operations, and expands lifecycle safety, CLI, backend, project, and roundtrip tests.
Operator and repository contracts
FEATURE_PARITY.md, README.md, docs/reborn/*, src/setup/README.md, crates/*/{AGENTS,CLAUDE}.md
Documents the offline migration workflow, source/target safety boundaries, onboarding behavior, quarantine semantics, compatibility limits, and crate ownership.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related issues

Possibly related PRs

  • nearai/ironclaw#4379 — Modifies the shared doctor command implementation used by the new migration status check.
  • nearai/ironclaw#5627 — Directly relates to the migration companion and manifest/plan/apply/verify lifecycle.
  • nearai/ironclaw#5898 — Relates to migrated automation trigger persistence and delivery-target behavior.

Suggested reviewers: ilblackdragon, benkurrek, henrypark133, italic-jinxin

Sequence Diagram(s)

sequenceDiagram
  participant Operator
  participant RebornCLI
  participant MigrationCompanion
  participant V1Snapshot
  participant RebornTarget
  Operator->>RebornCLI: migrate v1 plan/apply/resume/verify
  RebornCLI->>MigrationCompanion: handshake and forward operation
  MigrationCompanion->>V1Snapshot: inspect read-only source
  MigrationCompanion->>RebornTarget: apply, resume, or verify migration state
  RebornCLI->>RebornCLI: enforce activation state
Loading
🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers intent and testing, but it does not follow the required template or include a linked issue. Rewrite it to match the template: add Summary bullets, Change Type, Linked Issue, Validation checklist, Security Impact, Trust-Boundary Checklist, Database Impact, Blast Radius, Rollback Plan, and Review Follow-Through.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change and uses Conventional Commits style.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

❤️ Share

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

@github-actions github-actions Bot added size: XL 500+ changed lines scope: sandbox Docker sandbox scope: docs Documentation scope: dependencies Dependency updates risk: medium Business logic, config, or moderate-risk modules contributor: core 20+ merged PRs and removed size: XL 500+ changed lines labels Jul 10, 2026

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a comprehensive, multi-stage offline migration workflow to transition state from IronClaw v1 and engine-v2 to Reborn. It adds a same-release migration companion executable (ironclaw-reborn-migration) and implements a structured lifecycle consisting of read-only planning, stopped-source snapshot applying/resuming, and target verification. The changes also include deterministic target identifier generation, user and project migration, and updated onboarding and Docker configurations. The review feedback suggests two key improvements: using injective length-prefixed encoding instead of null-delimited concatenation for deterministic UUID generation to prevent separator-collision attacks, and canonicalizing the executable path in resolve_companion to ensure correct resolution when the binary is invoked via a symlink.

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.

Comment on lines +75 to +87
fn scoped_uuid(
&self,
domain: &str,
source_primary_id: &str,
tenant_id: &str,
agent_id: &str,
) -> Uuid {
let seed = format!(
"{MIGRATION_ID_SCHEMA}\0{}\0{}\0{domain}\0{source_primary_id}\0{tenant_id}\0{agent_id}",
self.manifest_schema_version, self.source_fingerprint
);
Uuid::new_v5(&MIGRATION_NAMESPACE, seed.as_bytes())
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Generating deterministic UUIDs by concatenating multiple string components with a null delimiter (\0) is vulnerable to separator-collision attacks if any component can contain the delimiter or if the structure changes. To ensure complete injectivity and eliminate collision risks, use an injective length-prefixed encoding instead of simple concatenation.

    fn scoped_uuid(
        &self,
        domain: &str,
        source_primary_id: &str,
        tenant_id: &str,
        agent_id: &str,
    ) -> Uuid {
        let mut seed = Vec::new();
        for component in &[
            MIGRATION_ID_SCHEMA,
            &self.manifest_schema_version.to_string(),
            &self.source_fingerprint,
            domain,
            source_primary_id,
            tenant_id,
            agent_id,
        ] {
            seed.extend_from_slice(&(component.len() as u64).to_le_bytes());
            seed.extend_from_slice(component.as_bytes());
        }
        Uuid::new_v5(&MIGRATION_NAMESPACE, &seed)
    }
References
  1. When generating deterministic identifiers or hashes from multiple string components, use an injective length-prefixed encoding rather than simple concatenation with a delimiter to eliminate the risk of separator-collision attacks or accidental collisions.
  2. Prefer using u64 for length prefixes in binary encodings to ensure that conversions from usize are infallible on all supported platforms, avoiding panics and potential DoS vectors caused by .expect() on overflow.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 5a3bfa2. Deterministic ID seeds now use injective length-prefixed field framing under a new ID schema, and the manifest schema is bumped to v3 so earlier rehearsal manifests cannot be mixed with the new IDs. The regression test constructs the prior null-separator collision and proves the IDs differ.

Comment on lines +384 to +390
fn resolve_companion(current_exe: &Path) -> anyhow::Result<PathBuf> {
let bin_dir = current_exe.parent().with_context(|| {
format!(
"cannot resolve the installation directory for {}",
current_exe.display()
)
})?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

If the running executable is invoked via a symlink (e.g., when installed via stow or custom symlinks in /usr/local/bin), std::env::current_exe() may return the symlink path. Using .parent() on the symlink path will resolve to the directory containing the symlink rather than the directory containing the actual binary, failing to locate the companion. Canonicalizing the path first ensures symlinks are resolved correctly.

fn resolve_companion(current_exe: &Path) -> anyhow::Result<PathBuf> {
    let current_exe = current_exe.canonicalize().with_context(|| {
        format!(
            "failed to canonicalize executable path {}",
            current_exe.display()
        )
    })?;
    let bin_dir = current_exe.parent().with_context(|| {
        format!(
            "cannot resolve the installation directory for {}",
            current_exe.display()
        )
    })?;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

No code change by design. This launcher intentionally rejects symlinked companions and install directories writable by another user; canonicalizing the invoked binary to make symlink installs work would weaken that documented same-directory/native-companion trust boundary. Existing migration CLI tests cover missing, cross-release, and writable companion rejection.

@ironloopai ironloopai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

❌ IronLoop Review: reviewer

Review at a glance

Verdict Blocking Notes Inline Head
❌ Changes requested 1 1 2 c64d03054e5e

Head: c64d03054e5e2f199d0e65109c74fd9c14175003
Next: Fix the blocking findings, push the PR branch, then re-run this reviewer.

Run details

Status: Current
Needs human: no
Needs validation: no

Summary

Found one blocking migration lifecycle bug and one non-blocking strict-mode bug. Cargo-based validation could not be run because cargo is not installed in the reviewer environment.

Findings

Blocking: 1 / Notes: 1

Blocking findings

1. ❌ [MEDIUM] Resume rejects interrupted applying manifests

Location: crates/ironclaw_reborn_migration/src/lib.rs:152
An interrupted apply leaves the manifest in applying because the CLI persists that state before running converters, and resume explicitly accepts an applying manifest. However this path immediately calls manifest.transition(MigrationStatus::Applying), while MigrationManifest::transition does not allow Applying -> Applying. The first resume attempt therefore fails before replaying converters. Treat an already-applying resume as already transitioned, or make that transition idempotent.

Non-blocking notes (1)
1. 💬 [LOW] Strict planning fails on absent lossy categories

Location: crates/ironclaw_reborn_migration/src/main.rs:586-596
manifest_has_strict_loss flags every archive-only, reauth, or reinstall disposition without checking whether the inventory entry actually exists. The inventory builder emits zero-count entries for all known tables and home artifacts, so plan --strict will fail even when, for example, there are no API tokens or WASM installs. Gate these dispositions on entry.count > 0 while keeping blockers unconditional.

Developer follow-up

After fixing this feedback:

  1. Push the fix to this PR branch.
  2. Re-run this reviewer with @ironloopai review --agent reviewer if you only changed this reviewer's findings.
  3. Re-run all reviewers with @ironloopai review when the fix may affect multiple areas.
  4. Use @ironloopai status to check queued/running/completed/failed/superseded state while reviewers run.

let source = source::V1Source::open(&source_options.source).await?;
let mut target = target::RebornTarget::open(&target_options).await?;
let mut report = MigrationReport::new(false);
let mut applied_manifest = manifest.transition(MigrationStatus::Applying)?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This breaks resume from the exact state left by an interrupted apply. The CLI writes applying before converter execution, but resume_migration then tries to transition that applying manifest to applying again, which MigrationManifest::transition rejects. Treat already-applying resumes as already transitioned, or allow the transition to be idempotent.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Already addressed before 5a3bfa2 and re-verified here. The resume path preserves an on-disk Applying manifest instead of attempting Applying -> Applying. Both full migration backend suites pass at the pushed head.

}

fn manifest_has_strict_loss(manifest: &MigrationManifest) -> bool {
manifest.inventory.iter().any(|entry| {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This should ignore zero-count lossy dispositions. The planner emits entries for every known source category even when absent, so --strict currently fails on missing API-token/WASM/archive-only categories in otherwise clean sources.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 5a3bfa2. Strict mode now treats a lossy disposition as failure only when entry.count > 0, while any blocker remains unconditional. A caller-level companion test runs a real local-dev/libSQL plan --strict against an empty source and verifies success plus manifest creation.

@railway-app

railway-app Bot commented Jul 10, 2026 •

Copy link
Copy Markdown

🚅 Deployed to the ironclaw-pr-5936 environment in ironclaw-ci-preview

Service Status Web Updated (UTC)
ironclaw ✅ Success (View Logs) Web Jul 13, 2026 at 9:01 pm

@serrrfirat
serrrfirat marked this pull request as ready for review July 13, 2026 08:19

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🤖 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_reborn_cli/src/commands/doctor.rs`:
- Around line 44-46: Replace the silent `.ok()?` handling with explicit
propagation or add a `// silent-ok: <reason>` annotation only where fallback is
intentionally valid. Apply this to `migration_state` in
`crates/ironclaw_reborn_cli/src/commands/doctor.rs` lines 44-46,
`status_from_document` in `crates/ironclaw_reborn_cli/src/commands/doctor.rs`
lines 60-62, and `read_target_state_status` in
`crates/ironclaw_reborn_cli/src/commands/migrate.rs` lines 240-242; ensure each
annotation names the specific read if retaining silent fallback.

In `@crates/ironclaw_reborn_cli/src/commands/migrate.rs`:
- Around line 225-268: Introduce a canonical wire-facing migration lifecycle
enum in the migration contract, covering all statuses and providing an
is_activation_safe()/is_quarantined() policy helper. In
crates/ironclaw_reborn_cli/src/commands/migrate.rs lines 225-268, update
ensure_activation_allowed and read_target_state_status to deserialize and use
the shared enum instead of repeating status literals; in
crates/ironclaw_reborn_cli/src/commands/doctor.rs lines 63-72, parse through the
same enum and remove its duplicated accepted-status list.

In `@crates/ironclaw_reborn_cli/src/commands/onboard.rs`:
- Around line 67-81: Replace the string values assigned to migration_state with
a dedicated enum representing explicitly_skipped, planned, available, and
not_detected. Update the match sites around the onboarding summary and
pending_steps to match enum variants, including any formatting or serialization
needed for existing output, while preserving current behavior.

In `@crates/ironclaw_reborn_composition/src/input.rs`:
- Around line 924-936: Update resolve_postgres_migration_target to return the
named ResolvedPostgresTargetConfig instead of a positional tuple, and make that
type pub(crate) so callers can access its named url and secret_master_key
fields. Preserve the existing resolve_postgres_target_config resolution and
error propagation while removing the tuple destructuring.

In `@crates/ironclaw_reborn_migration/src/inventory.rs`:
- Around line 596-602: The checksum assertion around content_digest is
tautological because its prefixes differ and therefore cannot detect content
hashing. Replace it with a meaningful invariant test: create two files with
identical size and mtime but different contents and assert their manifest
checksums match, or remove this assertion and retain only the existing
metadata-independence check.

In `@crates/ironclaw_reborn_migration/src/lib.rs`:
- Line 152: The resume path in resume_migration must avoid rejecting an on-disk
manifest already in MigrationStatus::Applying. Before calling
MigrationManifest::transition(MigrationStatus::Applying), preserve the existing
Applying manifest unchanged; only transition manifests in other permitted resume
states, while retaining the existing applied_manifest flow for newly applying
migrations.
- Around line 445-449: The Postgres branch in the redacted store descriptor
currently hashes the full secret URL, exposing credentials to offline guessing.
Update the locator fingerprint logic around SourceDb handling and locator_hash
to hash only non-secret Postgres locator components, or use the target master
key for an HMAC, while preserving the MigrationManifest redacted
locator-fingerprint contract.

In `@crates/ironclaw_reborn_migration/src/main.rs`:
- Around line 585-597: Update manifest_has_strict_loss to consider an entry
lossy only when entry.count > 0, while preserving blocker handling and the
existing lossy disposition checks for non-empty categories.

In `@crates/ironclaw_reborn_migration/src/source.rs`:
- Line 188: Update the schema lookup return path in the source backend to
propagate row decoding failures instead of converting them to None via
ok().flatten(). Map the try_get error through the existing
source_read_error("schema", error) mechanism, preserving the successful optional
schema-version result and parity with the libSQL path.
🪄 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: bdb9cdba-6d30-4cad-8df1-0f5b42c51357

📥 Commits

Reviewing files that changed from the base of the PR and between 5ae88dd and c64d030.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock, !**/Cargo.lock
📒 Files selected for processing (54)
  • Dockerfile.reborn
  • FEATURE_PARITY.md
  • README.md
  • crates/ironclaw_reborn_cli/Cargo.toml
  • crates/ironclaw_reborn_cli/src/commands/doctor.rs
  • crates/ironclaw_reborn_cli/src/commands/migrate.rs
  • crates/ironclaw_reborn_cli/src/commands/mod.rs
  • crates/ironclaw_reborn_cli/src/commands/onboard.rs
  • crates/ironclaw_reborn_cli/src/context.rs
  • crates/ironclaw_reborn_cli/tests/migration_cli.rs
  • crates/ironclaw_reborn_cli/tests/smoke.rs
  • crates/ironclaw_reborn_composition/src/admin_user_directory.rs
  • crates/ironclaw_reborn_composition/src/factory.rs
  • crates/ironclaw_reborn_composition/src/input.rs
  • crates/ironclaw_reborn_composition/src/lib.rs
  • crates/ironclaw_reborn_composition/src/migration_support.rs
  • crates/ironclaw_reborn_identity/CONTRACT.md
  • crates/ironclaw_reborn_identity/src/filesystem_store/directory.rs
  • crates/ironclaw_reborn_identity/src/filesystem_store/tests.rs
  • crates/ironclaw_reborn_identity/src/lib.rs
  • crates/ironclaw_reborn_identity/src/user_directory.rs
  • crates/ironclaw_reborn_migration/CLAUDE.md
  • crates/ironclaw_reborn_migration/Cargo.toml
  • crates/ironclaw_reborn_migration/src/convert/automations.rs
  • crates/ironclaw_reborn_migration/src/convert/extensions.rs
  • crates/ironclaw_reborn_migration/src/convert/memory.rs
  • crates/ironclaw_reborn_migration/src/convert/mod.rs
  • crates/ironclaw_reborn_migration/src/convert/projects.rs
  • crates/ironclaw_reborn_migration/src/convert/secrets.rs
  • crates/ironclaw_reborn_migration/src/convert/threads.rs
  • crates/ironclaw_reborn_migration/src/convert/users.rs
  • crates/ironclaw_reborn_migration/src/inventory.rs
  • crates/ironclaw_reborn_migration/src/lib.rs
  • crates/ironclaw_reborn_migration/src/main.rs
  • crates/ironclaw_reborn_migration/src/manifest.rs
  • crates/ironclaw_reborn_migration/src/mounts.rs
  • crates/ironclaw_reborn_migration/src/options.rs
  • crates/ironclaw_reborn_migration/src/report.rs
  • crates/ironclaw_reborn_migration/src/source.rs
  • crates/ironclaw_reborn_migration/src/target.rs
  • crates/ironclaw_reborn_migration/src/target_ids.rs
  • crates/ironclaw_reborn_migration/src/v2_model.rs
  • crates/ironclaw_reborn_migration/tests/companion_cli.rs
  • crates/ironclaw_reborn_migration/tests/migration_roundtrip.rs
  • crates/ironclaw_reborn_migration/tests/migration_safety.rs
  • crates/ironclaw_reborn_migration/tests/project_migration.rs
  • docker/reborn/entrypoint.sh
  • docs/internal/2026-06-26-legacy-vs-reborn-feature-comparison.md
  • docs/reborn-binary.md
  • docs/reborn/README.md
  • docs/reborn/deploy-reborn-cli-docker.md
  • docs/reborn/onboarding.md
  • docs/reborn/v1-migration.md
  • src/db/libsql/mod.rs

Comment thread crates/ironclaw_reborn_cli/src/commands/doctor.rs Outdated
Comment thread crates/ironclaw_reborn_cli/src/commands/migrate.rs Outdated
Comment thread crates/ironclaw_reborn_cli/src/commands/onboard.rs
Comment thread crates/ironclaw_reborn_composition/src/input.rs
Comment on lines +596 to +602
let mut content_digest = Fnv1a64::new();
content_digest.update(canary);
assert_ne!(
checksum,
format!("fnv1a64:{:016x}", content_digest.finish()),
"manifest checksums must never be hashes of secret-bearing file contents"
);

@coderabbitai coderabbitai Bot Jul 13, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

This assertion is tautological and does not test the invariant. checksum is prefixed metadata-fnv1a64: while the comparison target is fnv1a64:…, so assert_ne! always passes regardless of whether contents were hashed. Exercise the actual invariant instead, e.g. two files with identical size/mtime but different contents must produce the same checksum (or drop this weak assertion and keep only the metadata-independence check).

🤖 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_migration/src/inventory.rs` around lines 596 - 602,
The checksum assertion around content_digest is tautological because its
prefixes differ and therefore cannot detect content hashing. Replace it with a
meaningful invariant test: create two files with identical size and mtime but
different contents and assert their manifest checksums match, or remove this
assertion and retain only the existing metadata-independence check.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Already addressed before 5a3bfa2 and re-verified at the pushed head. home_checksum_is_not_derived_from_secret_file_contents now creates same-size/same-mtime files with different contents and asserts equal metadata-only checksums; the tautological prefix comparison is gone.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd inventory.rs crates/ironclaw_reborn_migration/src | xargs sed -n '540,610p'

Length of output: 2898


🏁 Script executed:

#!/bin/bash
fd inventory.rs crates/ironclaw_reborn_migration/src | xargs grep -n "home_checksum_is_not_derived_from_secret_file_contents" -A 40

Length of output: 1870


@serrrfirat, checked the pushed head — the test still contains the same tautological assertion I flagged, not the fix you described.

checksum is prefixed "metadata-fnv1a64:..." while the comparison target is built as format!("fnv1a64:{:016x}", ...) (no metadata- prefix). These two strings can never be equal regardless of whether content is hashed, so assert_ne! is vacuous — it doesn't exercise the invariant at all.

There's no same-size/same-mtime-different-content test present; the only test is home_checksum_is_not_derived_from_secret_file_contents at Line 651-667, unchanged from what I originally flagged.

Possibly you're referencing a local branch state or a different commit than what's currently pushed. Can you confirm the commit SHA you tested against, or push the update if it's still pending?

Comment thread crates/ironclaw_reborn_migration/src/lib.rs Outdated
Comment thread crates/ironclaw_reborn_migration/src/lib.rs
Comment thread crates/ironclaw_reborn_migration/src/main.rs
Comment thread crates/ironclaw_reborn_migration/src/source.rs Outdated
@serrrfirat
serrrfirat force-pushed the codex/v1-reborn-migration branch from c64d030 to 3d3cd5a Compare July 13, 2026 11:43
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-5936 July 13, 2026 11:43 Destroyed
@github-actions github-actions Bot added scope: setup Onboarding / setup size: XL 500+ changed lines risk: high Safety, secrets, auth, or critical infrastructure and removed risk: medium Business logic, config, or moderate-risk modules labels Jul 13, 2026
@serrrfirat serrrfirat changed the title Add safe v1 to Reborn migration workflow feat(migration): add safe v1-to-Reborn migration workflow Jul 13, 2026
@serrrfirat
serrrfirat force-pushed the codex/v1-reborn-migration branch from 3d3cd5a to e695c80 Compare July 13, 2026 12:10
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-5936 July 13, 2026 12:10 Destroyed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
crates/ironclaw_reborn_cli/src/commands/doctor.rs (1)

1-1: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Silent error handling throughout the migration state detection chain. The migration_state → status_from_document path discards errors at three sites, masking permission-denied, corruption, and parse failures behind opaque fallback strings.

  • crates/ironclaw_reborn_cli/src/commands/doctor.rs#L255-257: bind the Err in read_activation_state_status and log with debug! before returning "invalid".
  • crates/ironclaw_reborn_cli/src/commands/doctor.rs#L260-261: replace .ok()? on the onboarding-marker read/parse with explicit NotFound-aware propagation or add // silent-ok: <reason> naming the onboarding-marker read.
  • crates/ironclaw_reborn_cli/src/commands/doctor.rs#L276-277: same for the manifest read/parse in status_from_document.
🤖 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/commands/doctor.rs` at line 1, Update the
migration-state detection chain in migration_state and status_from_document to
stop silently discarding errors. In read_activation_state_status, bind the
activation-state read error, log it with debug!, then return "invalid"; replace
the onboarding-marker .ok()? handling with explicit NotFound-aware propagation
or a reasoned silent-ok annotation, and apply the same treatment to manifest
read/parse errors in status_from_document.

Sources: Coding guidelines, Path instructions

♻️ Duplicate comments (1)
crates/ironclaw_reborn_cli/src/commands/doctor.rs (1)

260-261: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

.ok()? on IO/parse reads still present — previously flagged.

Lines 260–261 (migration_state) and 276–277 (status_from_document) use .ok()? on std::fs::read_to_string and serde_json::from_str, dropping permission-denied and corruption errors into None. No // silent-ok: <reason> annotation is present. This is the same fail-loud violation flagged in the prior review; the patterns persist in the reorganized code.

Also applies to: 276-277

🤖 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/commands/doctor.rs` around lines 260 - 261,
Update migration_state and status_from_document to stop converting file-read and
JSON-parse failures into None via .ok()?. Propagate or explicitly handle the
std::fs::read_to_string and serde_json::from_str errors according to the
surrounding command error flow, or add the required // silent-ok: <reason>
annotation only if intentionally suppressing them.

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.

Inline comments:
In `@crates/ironclaw_reborn_cli/src/commands/doctor.rs`:
- Around line 255-257: Update the Err arm handling read_activation_state_status
to bind the error, log it with debug! including relevant context, then return
the existing "invalid" fallback. Preserve the Ok(None) behavior and the current
status output.

---

Outside diff comments:
In `@crates/ironclaw_reborn_cli/src/commands/doctor.rs`:
- Line 1: Update the migration-state detection chain in migration_state and
status_from_document to stop silently discarding errors. In
read_activation_state_status, bind the activation-state read error, log it with
debug!, then return "invalid"; replace the onboarding-marker .ok()? handling
with explicit NotFound-aware propagation or a reasoned silent-ok annotation, and
apply the same treatment to manifest read/parse errors in status_from_document.

---

Duplicate comments:
In `@crates/ironclaw_reborn_cli/src/commands/doctor.rs`:
- Around line 260-261: Update migration_state and status_from_document to stop
converting file-read and JSON-parse failures into None via .ok()?. Propagate or
explicitly handle the std::fs::read_to_string and serde_json::from_str errors
according to the surrounding command error flow, or add the required //
silent-ok: <reason> annotation only if intentionally suppressing them.
🪄 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: bc184b66-06e2-4898-9d86-4b9ebae5a5c4

📥 Commits

Reviewing files that changed from the base of the PR and between c64d030 and e695c80.

⛔ Files ignored due to path filters (2)
  • CHANGELOG.md is excluded by !CHANGELOG.md
  • Cargo.lock is excluded by !**/*.lock, !**/Cargo.lock
📒 Files selected for processing (13)
  • Dockerfile.reborn
  • FEATURE_PARITY.md
  • README.md
  • crates/AGENTS.md
  • crates/README.md
  • crates/ironclaw_reborn_cli/AGENTS.md
  • crates/ironclaw_reborn_cli/Cargo.toml
  • crates/ironclaw_reborn_cli/src/commands/doctor.rs
  • crates/ironclaw_reborn_cli/src/commands/extension.rs
  • crates/ironclaw_reborn_cli/src/commands/migrate.rs
  • crates/ironclaw_reborn_cli/src/commands/mod.rs
  • crates/ironclaw_reborn_cli/src/commands/onboard.rs
  • crates/ironclaw_reborn_cli/src/context.rs

Comment thread crates/ironclaw_reborn_cli/src/commands/doctor.rs Outdated
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-5936 July 13, 2026 12:33 Destroyed
@github-actions

github-actions Bot commented Jul 13, 2026 •

Copy link
Copy Markdown
Contributor

Coverage ratchet

Ratchet mode: ENFORCING

RATCHET PASS: global
  observed: 85.2% (303641 / 356399 lines)
  floor:    85.3% (tolerance 0.5pp -> effective floor 84.8%)
  denominator: 356399 lines now vs 320188 at floor capture (+36211 lines, +11.31%) — material change (>5%)

⚠️ 2 Reborn crate(s) have 0 int-tier coverage (target: 0) — ironclaw_prompt_envelope, ironclaw_scripts

Reborn integration-tier coverage

Line coverage (Reborn crates): 85.2% — 303641 / 356399 lines

Per-crate breakdown (63 crates, lowest-covered first)
Crate Line % Covered / Total
ironclaw_prompt_envelope 0% 0 / 88
ironclaw_scripts 0% 0 / 345
ironclaw_runtime_policy 31.75% 80 / 252
ironclaw_event_projections 43.31% 673 / 1554
ironclaw_run_state 53.07% 225 / 424
ironclaw_authorization 53.89% 464 / 861
ironclaw_triggers 60.3% 1835 / 3043
ironclaw_observability 61.54% 16 / 26
ironclaw_webui_v2 62.93% 2679 / 4257
ironclaw_mcp 63.03% 578 / 917
ironclaw_reborn_cli 64.74% 4800 / 7414
ironclaw_filesystem 67.1% 3833 / 5712
ironclaw_dispatcher 67.15% 92 / 137
ironclaw_reborn_migration 68.66% 3724 / 5424
ironclaw_memory 69% 790 / 1145
ironclaw_trust 72.88% 661 / 907
ironclaw_capabilities 74.39% 1685 / 2265
ironclaw_wasm_limiter 74.6% 47 / 63
ironclaw_reborn_event_store 74.67% 958 / 1283
ironclaw_extractors 74.72% 538 / 720
ironclaw_first_party_extensions 78.23% 5439 / 6953
ironclaw_llm 78.36% 20328 / 25941
ironclaw_product_context 78.57% 11 / 14
ironclaw_process_sandbox 80.65% 671 / 832
ironclaw_wasm_product_adapters 80.71% 1448 / 1794
ironclaw_memory_native 81.06% 3205 / 3954
ironclaw_events 82.86% 1765 / 2130
ironclaw_secrets 82.95% 2876 / 3467
ironclaw_reborn_identity 83.43% 453 / 543
ironclaw_wasm 83.97% 1011 / 1204
ironclaw_auth 83.99% 3147 / 3747
ironclaw_reborn_config 84.06% 1814 / 2158
ironclaw_processes 84.44% 993 / 1176
ironclaw_turns 84.85% 13560 / 15982
ironclaw_common 84.85% 1490 / 1756
ironclaw_host_api 85.17% 2664 / 3128
ironclaw_product_workflow 85.56% 10836 / 12665
ironclaw_projects 85.92% 659 / 767
ironclaw_threads 86.07% 4404 / 5117
ironclaw_network 86.12% 670 / 778
ironclaw_slack_v2_adapter 86.79% 1806 / 2081
ironclaw_product_adapters 86.98% 3207 / 3687
ironclaw_skills 87.58% 4470 / 5104
ironclaw_hooks 87.75% 9917 / 11302
ironclaw_product_adapter_registry 88.06% 531 / 603
ironclaw_reborn_traces 88.19% 11946 / 13546
ironclaw_host_runtime 88.53% 17271 / 19509
ironclaw_reborn_composition 89.03% 79962 / 89814
ironclaw_runner 89.28% 16695 / 18700
ironclaw_approvals 89.41% 1587 / 1775
ironclaw_extensions 89.45% 3019 / 3375
ironclaw_reborn_openai_compat 89.46% 3768 / 4212
ironclaw_conversations 90.33% 3120 / 3454
ironclaw_event_streams 90.82% 1009 / 1111
ironclaw_loop_host 92.51% 14755 / 15950
ironclaw_resources 92.83% 4736 / 5102
ironclaw_attachments 93.06% 630 / 677
ironclaw_reborn_webui_ingress 93.19% 2217 / 2379
ironclaw_telegram_v2_adapter 93.51% 2508 / 2682
ironclaw_agent_loop 94.57% 8789 / 9294
ironclaw_safety 95.04% 3677 / 3869
ironclaw_first_party_extension_ports 95.24% 3343 / 3510
ironclaw_outbound 95.59% 3556 / 3720

This table itself is informational and never gates the PR on its own — not the percentage, not the per-crate holes, not the 0-coverage callout. A separate coverage ratchet (dry-run until enforce=true; see tests/integration/coverage-floor.toml) can fail the build on specific configured floors.

Exemptions (3 entry/entries excluded from the accounting above)
Module / Crate Reason Issue
crate: ironclaw_embeddings v1-only: consumed only by root ironclaw (src/app.rs, src/tools/builtin/memory.rs, src/workspace/mod.rs, src/config/{mod,embeddings}.rs); no crates/* dependents. Covered by "Tests (Legacy)". #5657
crate: ironclaw_gateway v1-only: consumed only by root ironclaw (src/channels/web/platform/static_files.rs, src/channels/web/handlers/frontend.rs); no crates/* dependents. Covered by "Tests (Legacy)". #5657
crate: ironclaw_tui v1-only: consumed only by root ironclaw (src/main.rs, src/channels/tui.rs); no crates/* dependents. Crate's own doc comment confirms it bridges INTO v1, not Reborn. Covered by "Tests (Legacy)". #5657

@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-5936 July 13, 2026 14:50 Destroyed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (2)
crates/ironclaw_reborn_cli/src/commands/onboard.rs (1)

71-109: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Contradictory --migrate-v1 + --skip-v1-migration silently resolved without feedback.

When both flags are set, skip_v1_migration wins with no diagnostic, unlike the existing --import-history deprecation warning right above it. A user who genuinely intended --migrate-v1 gets silently skipped.

🩹 Proposed fix
-        let outcome = write_default_config_files(home, self.force, ExistingConfigPolicy::Preserve)?;
-        let migration_state = if self.skip_v1_migration {
+        let outcome = write_default_config_files(home, self.force, ExistingConfigPolicy::Preserve)?;
+        if self.skip_v1_migration && migration_requested {
+            eprintln!(
+                "warning: --skip-v1-migration overrides --migrate-v1/--import-history; v1 migration will not be planned"
+            );
+        }
+        let migration_state = if self.skip_v1_migration {
             OnboardingMigrationState::ExplicitlySkipped
🤖 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/commands/onboard.rs` around lines 71 - 109, In
the onboarding flag-handling flow, add an explicit diagnostic when both
migrate_v1 and skip_v1_migration are set, before the migration_state selection.
Preserve the existing precedence where skip_v1_migration produces
ExplicitlySkipped, while clearly informing the user that --migrate-v1 was
ignored.
crates/ironclaw_reborn_migration/src/source.rs (1)

104-119: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

.map_err(|_| ...) fully discards the Postgres connection error — log before redacting.

Redacting the error message to callers is correct (avoids leaking the connection string), but the closure drops the bound error entirely instead of logging it internally first. As per coding guidelines, "Do not use .map_err(|_| OtherError) or any closure that discards its error binding and substitutes a generic error; carry the cause instead or log the bound error before mapping." This also leaves the Postgres branch inconsistent with the libSQL branch just above it, which carries the cause via source_open_error.

🪵 Proposed fix
                     let backend = ironclaw::db::postgres::PgBackend::new(&config)
                         .await
-                        .map_err(|_| {
+                        .map_err(|error| {
+                            tracing::debug!(error = %error, "v1 postgres source connection failed");
                             MigrationError::OpenSource(
                                 "PostgreSQL source connection failed (connection details redacted)"
                                     .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_reborn_migration/src/source.rs` around lines 104 - 119,
Update the PostgreSQL backend creation in the SourceDb::Postgres branch to
retain and log the underlying error before converting it to the redacted
MigrationError::OpenSource message. Replace the discard binding in the
PgBackend::new(...).await map_err closure with a named error binding and follow
the existing source_open_error or equivalent logging pattern used by the libSQL
branch, while preserving redacted caller-facing details.

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_reborn_migration/src/lib.rs`:
- Around line 40-46: The manifest_target_matches function silently converts
target_descriptor errors into a normal mismatch. Propagate the target_descriptor
error to callers by changing the function’s return type and updating its call
sites, or retain the false fallback only with an inline silent-ok annotation and
explicit error logging; preserve the existing backend and locator_fingerprint
comparisons.

---

Outside diff comments:
In `@crates/ironclaw_reborn_cli/src/commands/onboard.rs`:
- Around line 71-109: In the onboarding flag-handling flow, add an explicit
diagnostic when both migrate_v1 and skip_v1_migration are set, before the
migration_state selection. Preserve the existing precedence where
skip_v1_migration produces ExplicitlySkipped, while clearly informing the user
that --migrate-v1 was ignored.

In `@crates/ironclaw_reborn_migration/src/source.rs`:
- Around line 104-119: Update the PostgreSQL backend creation in the
SourceDb::Postgres branch to retain and log the underlying error before
converting it to the redacted MigrationError::OpenSource message. Replace the
discard binding in the PgBackend::new(...).await map_err closure with a named
error binding and follow the existing source_open_error or equivalent logging
pattern used by the libSQL branch, while preserving redacted caller-facing
details.
🪄 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: 05d6e5a7-3c22-475f-bb3f-d2aab1accf9c

📥 Commits

Reviewing files that changed from the base of the PR and between 066a187 and 5a3bfa2.

📒 Files selected for processing (12)
  • crates/ironclaw_reborn_cli/src/commands/doctor.rs
  • crates/ironclaw_reborn_cli/src/commands/migrate.rs
  • crates/ironclaw_reborn_cli/src/commands/onboard.rs
  • crates/ironclaw_reborn_composition/src/input.rs
  • crates/ironclaw_reborn_composition/src/migration_support.rs
  • crates/ironclaw_reborn_migration/src/lib.rs
  • crates/ironclaw_reborn_migration/src/main.rs
  • crates/ironclaw_reborn_migration/src/manifest.rs
  • crates/ironclaw_reborn_migration/src/source.rs
  • crates/ironclaw_reborn_migration/src/target_ids.rs
  • crates/ironclaw_reborn_migration/tests/companion_cli.rs
  • docs/reborn/v1-migration.md

Comment thread crates/ironclaw_reborn_migration/src/lib.rs
@serrrfirat
serrrfirat force-pushed the codex/v1-reborn-migration branch from 5a3bfa2 to 1b9420c Compare July 13, 2026 16:35

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 16

🤖 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_native/src/service.rs`:
- Around line 31-35: Stop re-exporting MemoryServiceMetadataResponse through
ironclaw_memory_native: in crates/ironclaw_memory_native/src/service.rs lines
31-35, make its import private for local implementation use, and in
crates/ironclaw_memory_native/src/lib.rs lines 61-65 remove the facade re-export
so consumers import the type from ironclaw_memory.

In `@crates/ironclaw_reborn_cli/src/commands/doctor.rs`:
- Around line 269-275: Remove marker.display() and path.display() from
user-facing error contexts in the doctor command, including the onboarding
marker read/parse handling around serde_json::from_str and the migration
manifest handling at the referenced later block. Return opaque messages such as
“failed to read onboarding marker,” “invalid onboarding marker,” or “invalid
migration manifest,” while retaining absolute paths only in debug! diagnostics.

In `@crates/ironclaw_reborn_cli/src/commands/onboard.rs`:
- Around line 78-87: Update the dry-run paths in the onboarding command,
including print_dry_run calls, to use the same marker-overwrite condition as the
real execution path. Ensure --dry-run --migrate-v1 reports rewriting an existing
marker when migration_requested and the applicable force logic would rewrite it,
while preserving the existing behavior otherwise.

In `@crates/ironclaw_reborn_identity/src/lib.rs`:
- Around line 121-125: Change the UserImportConflict payload from String to
UserId to preserve typed identity validation. Update all constructors and call
sites that create this error to pass user.user_id.clone() directly, and retain
the existing error message formatting.

In `@crates/ironclaw_reborn_migration/src/convert/extensions.rs`:
- Around line 39-62: Update record_tool_disposition and
record_channel_disposition, along with their call sites in the user iteration,
to accept the current user and include that user in each loss record’s
source_id. Ensure tool and channel identifiers remain uniquely attributable per
user while preserving the existing disposition details.

In `@crates/ironclaw_reborn_migration/src/convert/memory.rs`:
- Around line 37-47: Update the engine-document filtering around is_engine_path
and has_engine_converter so only paths matching the owning converters’ exact
engine-path classifiers are treated as converter-owned; do not suppress losses
for near matches such as nested mission.json or non-thread /threads/ paths.
Ensure every known v1 artifact receives an explicit migration or loss
disposition, and add negative tests covering these malformed or unsupported
near-match paths.

In `@crates/ironclaw_reborn_migration/src/convert/projects.rs`:
- Around line 71-77: Remove project.workspace_path from the legacy_engine_v2
metadata object in the project conversion logic, or replace it with a non-path
redacted migration marker. Ensure canonical ProjectRecord metadata never
persists or exposes the raw backend workspace path while preserving the other
legacy fields.

In `@crates/ironclaw_reborn_migration/src/convert/secrets.rs`:
- Around line 181-199: Replace the raw error interpolation in the metadata and
material_matches mappings within the secret migration flow with a fixed safe
operator-facing reason. Preserve the typed backend error as an internal
source/diagnostic cause, and ensure MigrationError exposed through the CLI never
includes the original error text.

In `@crates/ironclaw_reborn_migration/src/convert/threads.rs`:
- Around line 226-274: The idempotent replay validation in the User branch must
compare the complete persisted inbound-message identity, not only message ID,
kind, and content. Update the replay read/check around accept_inbound_message to
also validate actor_id and source_binding_id (preferably via a full-record
comparison or equivalent service-level enforcement), and reject divergent
deterministic-slot collisions without overwriting.

In `@crates/ironclaw_reborn_migration/src/convert/users.rs`:
- Around line 41-50: Decouple API-token reporting from the user loop in the
migration flow around report_api_tokens and the related ranges: enumerate token
owners or token rows directly so orphaned tokens and tokens remain discoverable
when the users table is absent, deduplicate processing by token ID, and assign
every known v1 persistent API-token category a re-authentication or reinstall
disposition.

In `@crates/ironclaw_reborn_migration/src/inventory.rs`:
- Around line 430-498: The home artifact enumeration in the read_dir(home)
branch is nondeterministic because entries are appended to out in filesystem
order. Collect the eligible directory entries before processing, sort them by a
stable path or filename key, then run the existing type, count, checksum, and
InventoryEntry construction logic in that sorted order so inventory_checksum
remains reproducible.

In `@crates/ironclaw_reborn_migration/src/source.rs`:
- Around line 288-313: Update the PostgreSQL branch of collect_inventory so it
does not sort or materialize all row JSON values into encoded_rows. Stream each
table’s rows from client, increment the count, and fold each encoded row into an
order-independent digest while processing; update or reuse
postgres_table_content_checksum as needed to produce the final checksum without
retaining every row.

In `@crates/ironclaw_reborn_migration/tests/migration_roundtrip.rs`:
- Around line 886-911: Update dry_run_reports_without_writing to call
plan_migration directly instead of run_migration, preserving the manifest and
dry-run assertions. Remove the now-unused legacy wrapper import and keep the
test on the explicit planning lifecycle.

In `@crates/ironclaw_secrets/src/lib.rs`:
- Around line 1023-1033: Implement material_matches in the shared secret-store
adapter path instead of returning StoreUnavailable, delegating the comparison to
the underlying store so ScopedSecretsStoreAdapter<S> and InMemorySecretStore can
participate in migration compare-and-apply. Trace the existing adapter
delegation methods to preserve scope and error behavior, then add a migration
test covering the full compare-and-apply caller chain in the secrets conversion
flow.

In `@docker/reborn/entrypoint.sh`:
- Around line 37-50: Update the migrate dispatch logic in
docker/reborn/entrypoint.sh so v1:verify exits directly through ironclaw-reborn
like other read-only operations, before persistent-volume checks or config
seeding. Remove it from the mutating/resume whitelist while preserving the
existing handling for v1:apply and v1:resume.

In `@src/db/libsql/mod.rs`:
- Around line 94-97: Update Database::new_local_read_only to open the source
database in immutable mode, using libsql’s immutable option or equivalent rather
than only SQLITE_OPEN_READ_ONLY. Ensure opening the database cannot create or
modify its -wal/-shm files or otherwise alter the source directory.
🪄 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: 4d7a53ff-10dd-4914-83fa-b5b359dbac6c

📥 Commits

Reviewing files that changed from the base of the PR and between 5a3bfa2 and 1b9420c.

⛔ Files ignored due to path filters (2)
  • CHANGELOG.md is excluded by !CHANGELOG.md
  • Cargo.lock is excluded by !**/*.lock, !**/Cargo.lock
📒 Files selected for processing (74)
  • Dockerfile.reborn
  • FEATURE_PARITY.md
  • README.md
  • crates/AGENTS.md
  • crates/README.md
  • crates/ironclaw_memory/src/lib.rs
  • crates/ironclaw_memory/src/service.rs
  • crates/ironclaw_memory_native/CLAUDE.md
  • crates/ironclaw_memory_native/src/backend.rs
  • crates/ironclaw_memory_native/src/lib.rs
  • crates/ironclaw_memory_native/src/service.rs
  • crates/ironclaw_reborn_cli/AGENTS.md
  • crates/ironclaw_reborn_cli/Cargo.toml
  • crates/ironclaw_reborn_cli/src/commands/doctor.rs
  • crates/ironclaw_reborn_cli/src/commands/extension.rs
  • crates/ironclaw_reborn_cli/src/commands/migrate.rs
  • crates/ironclaw_reborn_cli/src/commands/mod.rs
  • crates/ironclaw_reborn_cli/src/commands/onboard.rs
  • crates/ironclaw_reborn_cli/src/context.rs
  • crates/ironclaw_reborn_cli/tests/migration_cli.rs
  • crates/ironclaw_reborn_cli/tests/smoke.rs
  • crates/ironclaw_reborn_composition/AGENTS.md
  • crates/ironclaw_reborn_composition/CLAUDE.md
  • crates/ironclaw_reborn_composition/src/admin_user_directory.rs
  • crates/ironclaw_reborn_composition/src/factory.rs
  • crates/ironclaw_reborn_composition/src/input.rs
  • crates/ironclaw_reborn_composition/src/lib.rs
  • crates/ironclaw_reborn_composition/src/migration_support.rs
  • crates/ironclaw_reborn_identity/CONTRACT.md
  • crates/ironclaw_reborn_identity/src/filesystem_store/directory.rs
  • crates/ironclaw_reborn_identity/src/filesystem_store/tests.rs
  • crates/ironclaw_reborn_identity/src/lib.rs
  • crates/ironclaw_reborn_identity/src/user_directory.rs
  • crates/ironclaw_reborn_migration/CLAUDE.md
  • crates/ironclaw_reborn_migration/Cargo.toml
  • crates/ironclaw_reborn_migration/src/convert/automations.rs
  • crates/ironclaw_reborn_migration/src/convert/extensions.rs
  • crates/ironclaw_reborn_migration/src/convert/memory.rs
  • crates/ironclaw_reborn_migration/src/convert/mod.rs
  • crates/ironclaw_reborn_migration/src/convert/projects.rs
  • crates/ironclaw_reborn_migration/src/convert/secrets.rs
  • crates/ironclaw_reborn_migration/src/convert/threads.rs
  • crates/ironclaw_reborn_migration/src/convert/users.rs
  • crates/ironclaw_reborn_migration/src/error.rs
  • crates/ironclaw_reborn_migration/src/inventory.rs
  • crates/ironclaw_reborn_migration/src/lib.rs
  • crates/ironclaw_reborn_migration/src/main.rs
  • crates/ironclaw_reborn_migration/src/manifest.rs
  • crates/ironclaw_reborn_migration/src/mounts.rs
  • crates/ironclaw_reborn_migration/src/options.rs
  • crates/ironclaw_reborn_migration/src/report.rs
  • crates/ironclaw_reborn_migration/src/source.rs
  • crates/ironclaw_reborn_migration/src/target.rs
  • crates/ironclaw_reborn_migration/src/target_ids.rs
  • crates/ironclaw_reborn_migration/src/v2_model.rs
  • crates/ironclaw_reborn_migration/tests/companion_cli.rs
  • crates/ironclaw_reborn_migration/tests/migration_roundtrip.rs
  • crates/ironclaw_reborn_migration/tests/migration_safety.rs
  • crates/ironclaw_reborn_migration/tests/project_migration.rs
  • crates/ironclaw_secrets/src/filesystem_store.rs
  • crates/ironclaw_secrets/src/lib.rs
  • docker/reborn/entrypoint.sh
  • docs/internal/2026-06-26-legacy-vs-reborn-feature-comparison.md
  • docs/plans/8513-cli-smoke-and-secrets-decomposition.md
  • docs/plans/composition-pubuse.snapshot
  • docs/reborn-binary.md
  • docs/reborn/README.md
  • docs/reborn/contracts/memory.md
  • docs/reborn/contracts/migration-compatibility.md
  • docs/reborn/deploy-reborn-cli-docker.md
  • docs/reborn/onboarding.md
  • docs/reborn/v1-migration.md
  • src/db/libsql/mod.rs
  • src/setup/README.md

Comment on lines +31 to +35
MemoryServiceErrorKind, MemoryServiceMetadataResponse, MemoryServiceProfileSetRequest,
MemoryServiceProfileSetResponse, MemoryServiceReadRequest, MemoryServiceReadResponse,
MemoryServiceSearchRequest, MemoryServiceSearchResponse, MemoryServiceSearchResult,
MemoryServiceTreeRequest, MemoryServiceTreeResponse, MemoryServiceWriteRequest,
MemoryServiceWriteResponse, MemoryWriteStatus, memory_context_disabled,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Import MemoryServiceMetadataResponse from its owning crate.

The new two-stage re-export repackages an ironclaw_memory contract type through ironclaw_memory_native.

  • crates/ironclaw_memory_native/src/service.rs#L31-L35: make this a private use for the implementation instead of pub use.
  • crates/ironclaw_memory_native/src/lib.rs#L61-L65: remove the facade re-export; downstream consumers should import it from ironclaw_memory.

As per coding guidelines, “consumers should import the owner’s type” and pub use must not be a path-preservation shim.

📍 Affects 2 files
  • crates/ironclaw_memory_native/src/service.rs#L31-L35 (this comment)
  • crates/ironclaw_memory_native/src/lib.rs#L61-L65
🤖 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_native/src/service.rs` around lines 31 - 35, Stop
re-exporting MemoryServiceMetadataResponse through ironclaw_memory_native: in
crates/ironclaw_memory_native/src/service.rs lines 31-35, make its import
private for local implementation use, and in
crates/ironclaw_memory_native/src/lib.rs lines 61-65 remove the facade re-export
so consumers import the type from ironclaw_memory.

Source: Coding guidelines

Comment on lines +269 to +275
return Err(error).with_context(|| {
format!("failed to read onboarding marker at {}", marker.display())
});
}
};
let document: serde_json::Value = serde_json::from_str(&body)
.with_context(|| format!("invalid onboarding marker at {}", marker.display()))?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Do not expose absolute migration paths in doctor output.

These contexts include marker.display() and path.display(); Line 236 then embeds the error in a user-facing check. Keep paths in debug! diagnostics and return an opaque message such as invalid onboarding marker or invalid migration manifest.

As per coding guidelines, “at channel boundaries, do not let internal identifiers, tracebacks, transport errors, absolute paths, internal file names, or wire-format prefixes reach user-facing output.”

Also applies to: 303-306

🤖 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/commands/doctor.rs` around lines 269 - 275,
Remove marker.display() and path.display() from user-facing error contexts in
the doctor command, including the onboarding marker read/parse handling around
serde_json::from_str and the migration manifest handling at the referenced later
block. Return opaque messages such as “failed to read onboarding marker,”
“invalid onboarding marker,” or “invalid migration manifest,” while retaining
absolute paths only in debug! diagnostics.

Source: Coding guidelines

Comment on lines 78 to +87
if self.dry_run {
print_dry_run(home, &marker_path, self.force, self.import_history);
print_dry_run(
home,
&marker_path,
self.force,
migration_requested,
self.skip_v1_migration,
source.as_ref(),
&manifest_path,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Make dry-run use the actual marker overwrite condition.

With an existing marker, --dry-run --migrate-v1 reports preservation, but the real command forces a rewrite.

Proposed fix
+        let force_marker =
+            self.force || migration_requested || self.skip_v1_migration;
+
         if self.dry_run {
             print_dry_run(
                 home,
                 &marker_path,
-                self.force,
+                force_marker,
                 migration_requested,
                 self.skip_v1_migration,
                 source.as_ref(),
                 &manifest_path,
             );
@@
-            self.force || migration_requested || self.skip_v1_migration,
+            force_marker,

Also applies to: 110-116

🤖 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/commands/onboard.rs` around lines 78 - 87,
Update the dry-run paths in the onboarding command, including print_dry_run
calls, to use the same marker-overwrite condition as the real execution path.
Ensure --dry-run --migrate-v1 reports rewriting an existing marker when
migration_requested and the applicable force logic would rewrite it, while
preserving the existing behavior otherwise.

Comment on lines +121 to +125
/// A historical import targeted a user id that already contains different
/// canonical state. Migration must stop rather than overwrite live or
/// previously imported user data.
#[error("migrated user conflicts with existing record for id: {0}")]
UserImportConflict(String),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Keep the conflicting identity typed as UserId.

Using String weakens this public contract and permits invalid identifiers.

Proposed fix
-    UserImportConflict(String),
+    UserImportConflict(UserId),

Update constructors to pass user.user_id.clone() directly.

As per coding guidelines, “Identifiers must use newtypes such as CredentialName, ExtensionName, ThreadId, or UserId.”

📝 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.

Suggested change
/// A historical import targeted a user id that already contains different
/// canonical state. Migration must stop rather than overwrite live or
/// previously imported user data.
#[error("migrated user conflicts with existing record for id: {0}")]
UserImportConflict(String),
/// A historical import targeted a user id that already contains different
/// canonical state. Migration must stop rather than overwrite live or
/// previously imported user data.
#[error("migrated user conflicts with existing record for id: {0}")]
UserImportConflict(UserId),
🤖 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_identity/src/lib.rs` around lines 121 - 125, Change
the UserImportConflict payload from String to UserId to preserve typed identity
validation. Update all constructors and call sites that create this error to
pass user.user_id.clone() directly, and retain the existing error message
formatting.

Source: Coding guidelines

Comment on lines 39 to +62
for user in users {
if let Some(store) = tool_store.as_ref() {
let tools = store
for tool in store
.list(&user)
.await
.map_err(|e| MigrationError::ReadSource {
.map_err(|error| MigrationError::ReadSource {
domain: "wasm_tools".into(),
reason: e.to_string(),
})?;
for tool in tools {
let bindings = tool_credential_bindings(store.as_ref(), &tool, report).await?;
collect_installation(
report,
&mut candidates_by_extension,
InstallInput {
owner: &user,
raw_name: &tool.name,
version: &tool.version,
description: &tool.description,
active: tool.status == ToolStatus::Active,
updated_at: tool.updated_at,
bindings,
},
);
reason: error.to_string(),
})?
{
record_tool_disposition(store.as_ref(), &tool, report).await?;
}
}

if let Some(store) = channel_store.as_ref() {
let channels = store
for channel in store
.list(&user)
.await
.map_err(|e| MigrationError::ReadSource {
.map_err(|error| MigrationError::ReadSource {
domain: "wasm_channels".into(),
reason: e.to_string(),
})?;
for channel in channels {
collect_installation(
report,
&mut candidates_by_extension,
channel_input(&user, &channel),
);
report.record_loss(
Domain::Extension,
format!("channel:{}", channel.name),
"credential_binding",
LossReason::NoTargetField,
"v1 has no explicit channel→secret join; the credential value still \
migrates via the secrets converter, but the installation binding is \
not auto-linked"
.to_string(),
);
reason: error.to_string(),
})?
{
record_channel_disposition(&channel, report);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Include user scope in extension loss identifiers.

tool:{name} and channel:{name} collide across users, making per-record losses ambiguous. Pass user into both helpers and include it in source_id.

Proposed fix
-                record_tool_disposition(store.as_ref(), &tool, report).await?;
+                record_tool_disposition(store.as_ref(), &user, &tool, report).await?;

-                record_channel_disposition(&channel, report);
+                record_channel_disposition(&user, &channel, report);

 async fn record_tool_disposition(
     store: &dyn WasmToolStore,
+    user: &str,
     tool: &StoredWasmTool,
     report: &mut MigrationReport,
 ) -> Result<(), MigrationError> {
-    let source_id = format!("tool:{}", tool.name);
+    let source_id = format!("user:{user}:tool:{}", tool.name);

-fn record_channel_disposition(channel: &StoredWasmChannel, report: &mut MigrationReport) {
-    let source_id = format!("channel:{}", channel.name);
+fn record_channel_disposition(
+    user: &str,
+    channel: &StoredWasmChannel,
+    report: &mut MigrationReport,
+) {
+    let source_id = format!("user:{user}:channel:{}", channel.name);

As per coding guidelines, migration records must preserve user scope and converter loss details must remain attributable to the source record.

Also applies to: 69-107

🤖 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_migration/src/convert/extensions.rs` around lines 39 -
62, Update record_tool_disposition and record_channel_disposition, along with
their call sites in the user iteration, to accept the current user and include
that user in each loss record’s source_id. Ensure tool and channel identifiers
remain uniquely attributable per user while preserving the existing disposition
details.

Source: Coding guidelines

Comment on lines +288 to +313
for row in rows {
let name: String = row
.try_get(0)
.map_err(|error| source_read_error("inventory", error))?;
let sql = format!(
"SELECT to_jsonb(source_row)::text FROM {} AS source_row \
ORDER BY to_jsonb(source_row)::text",
quote_identifier(&name),
);
let rows = client
.query(&sql, &[])
.await
.map_err(|error| source_read_error(&name, error))?;
let encoded_rows = rows
.iter()
.map(|row| {
row.try_get::<_, String>(0)
.map_err(|error| source_read_error(&name, error))
})
.collect::<Result<Vec<_>, _>>()?;
inventory.push(RawTableInventory {
name: name.clone(),
count: encoded_rows.len() as u64,
checksum: postgres_table_content_checksum(&name, &encoded_rows),
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm table_inventory is on the planning hot path and gauge v1 table cardinality expectations.
rg -nP '\btable_inventory\s*\(' crates/ironclaw_reborn_migration/src -C2
rg -nP 'to_jsonb\(source_row\)' crates/ironclaw_reborn_migration/src/source.rs -C2

Repository: nearai/ironclaw

Length of output: 1996


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the table_inventory implementation and the fingerprint/plan call path.
sed -n '120,340p' crates/ironclaw_reborn_migration/src/source.rs
printf '\n--- lib.rs call site ---\n'
sed -n '450,510p' crates/ironclaw_reborn_migration/src/lib.rs

# Check for any comments/docs indicating expected table sizes or streaming constraints.
rg -n "messages|audit|inventory|fingerprint|table_inventory|stream" crates/ironclaw_reborn_migration -g '!target'

Repository: nearai/ironclaw

Length of output: 32374


PG table_inventory should not materialize every row during planning.
collect_inventory() calls this on the plan/fingerprint path, and the PG branch reads to_jsonb(source_row)::text for every row of every table, sorts by the full JSON text, then collects the whole result set into Vec<String>. On large messages/audit tables this is an OOM/latency risk; stream rows and fold an order-independent digest 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 `@crates/ironclaw_reborn_migration/src/source.rs` around lines 288 - 313,
Update the PostgreSQL branch of collect_inventory so it does not sort or
materialize all row JSON values into encoded_rows. Stream each table’s rows from
client, increment the count, and fold each encoded row into an order-independent
digest while processing; update or reuse postgres_table_content_checksum as
needed to produce the final checksum without retaining every row.

Comment on lines 886 to 911
#[tokio::test]
async fn dry_run_reports_without_writing() {
let dir = tempfile::tempdir().unwrap();
let src = seed_v1_fixture(dir.path()).await;
let dst = dir.path().join("reborn-dry.db");
let target_parent = dir.path().join("missing-target");
let dst = target_parent.join("reborn-dry.db");

let report = run_migration(options(src, dst.clone(), true))
.await
.expect("dry run");

// Same counts as a real run …
assert_eq!(report.stats.threads, 3);
assert_eq!(report.stats.routines, 2);
assert_eq!(report.stats.missions, 2);
// Planning reports inventory through the manifest without invoking writers.
let manifest = report.manifest.expect("plan manifest");
assert!(
manifest
.inventory
.iter()
.any(|item| item.source_name == "conversations" && item.count >= 2)
);
assert!(report.dry_run);

// … but nothing was written to the Reborn store.
// No read helper is invoked here: opening libSQL would itself create state.
assert!(
reborn_triggers(&dst).await.is_empty(),
"dry run wrote triggers"
);
assert_eq!(
reborn_thread_doc_count(&dst).await,
0,
"dry run wrote thread docs"
!dst.exists() && !target_parent.exists(),
"planning created the target path or its parent"
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm run_migration is deprecated and whether a plan-only path suffices for the dry-run assertions.
rg -nP '\brun_migration\b' crates/ironclaw_reborn_migration/src
ast-grep run --pattern 'pub async fn run_migration($$$) { $$$ }' --lang rust crates/ironclaw_reborn_migration/src

Repository: nearai/ironclaw

Length of output: 809


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect migration entrypoints and the dry-run test context.
ast-grep outline crates/ironclaw_reborn_migration/src/lib.rs --view expanded
printf '\n--- lib.rs around run_migration/plan_migration ---\n'
sed -n '220,360p' crates/ironclaw_reborn_migration/src/lib.rs
printf '\n--- test file around dry_run_reports_without_writing ---\n'
sed -n '1,140p' crates/ironclaw_reborn_migration/tests/migration_roundtrip.rs
sed -n '860,930p' crates/ironclaw_reborn_migration/tests/migration_roundtrip.rs

Repository: nearai/ironclaw

Length of output: 14957


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '51,131p' crates/ironclaw_reborn_migration/src/lib.rs

Repository: nearai/ironclaw

Length of output: 3529


Use plan_migration here instead of run_migration. This test only needs the source-read-only planning path, which already returns the manifest without opening the target; drop the legacy wrapper and its import to stay on the explicit plan|apply|resume|verify lifecycle.

🤖 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_migration/tests/migration_roundtrip.rs` around lines
886 - 911, Update dry_run_reports_without_writing to call plan_migration
directly instead of run_migration, preserving the manifest and dry-run
assertions. Remove the now-unused legacy wrapper import and keep the test on the
explicit planning lifecycle.

Source: Coding guidelines

Comment thread crates/ironclaw_secrets/src/lib.rs
Comment on lines +37 to +50
# Migration is always an explicit operator workflow. Read-only planning,
# status, and help must not let the container entrypoint create target state or
# seed config first. Mutating/resume/verify operations continue through the
# normal persistent-volume and config checks below.
if [ "${1:-}" = "migrate" ]; then
for migration_arg in "$@"; do
case "$migration_arg" in
-h|--help) exec ironclaw-reborn "$@" ;;
esac
done
case "${2:-}:${3:-}" in
v1:apply|v1:resume|v1:verify) ;;
*) exec ironclaw-reborn "$@" ;;
esac

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Keep verify read-only at the container entrypoint.

Whitelisting v1:verify runs normal initialization and config seeding before verification. An empty target can therefore be mutated before it is inspected.

Proposed fix
-    v1:apply|v1:resume|v1:verify) ;;
+    v1:apply|v1:resume) ;;

As per coding guidelines, “verify must perform read-only checks.”

📝 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.

Suggested change
# Migration is always an explicit operator workflow. Read-only planning,
# status, and help must not let the container entrypoint create target state or
# seed config first. Mutating/resume/verify operations continue through the
# normal persistent-volume and config checks below.
if [ "${1:-}" = "migrate" ]; then
for migration_arg in "$@"; do
case "$migration_arg" in
-h|--help) exec ironclaw-reborn "$@" ;;
esac
done
case "${2:-}:${3:-}" in
v1:apply|v1:resume|v1:verify) ;;
*) exec ironclaw-reborn "$@" ;;
esac
# Migration is always an explicit operator workflow. Read-only planning,
# status, and help must not let the container entrypoint create target state or
# seed config first. Mutating/resume/verify operations continue through the
# normal persistent-volume and config checks below.
if [ "${1:-}" = "migrate" ]; then
for migration_arg in "$@"; do
case "$migration_arg" in
-h|--help) exec ironclaw-reborn "$@" ;;
esac
done
case "${2:-}:${3:-}" in
v1:apply|v1:resume) ;;
*) exec ironclaw-reborn "$@" ;;
esac
🤖 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 `@docker/reborn/entrypoint.sh` around lines 37 - 50, Update the migrate
dispatch logic in docker/reborn/entrypoint.sh so v1:verify exits directly
through ironclaw-reborn like other read-only operations, before
persistent-volume checks or config seeding. Remove it from the mutating/resume
whitelist while preserving the existing handling for v1:apply and v1:resume.

Source: Coding guidelines

Comment thread src/db/libsql/mod.rs
Comment on lines +94 to +97
pub async fn new_local_read_only(path: &Path) -> Result<Self, DatabaseError> {
let db = libsql::Builder::new_local(path)
.flags(libsql::OpenFlags::SQLITE_OPEN_READ_ONLY)
.build()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🌐 Web query:

For SQLite WAL databases and libsql 0.9, can opening a database with SQLITE_OPEN_READ_ONLY create or modify -shm or -wal files, and which immutable URI flag or libsql API prevents all source-directory writes?

💡 Result:

When opening a SQLite database (including those using WAL mode) with the SQLITE_OPEN_READ_ONLY flag, the database connection is prohibited from performing write operations [1][2]. However, because WAL mode relies on the creation and modification of auxiliary files—specifically the write-ahead log (-wal) and shared-memory (-shm) files—opening a WAL database in read-only mode can fail if the process does not have the necessary permissions to create or update these files in the source directory [3][4][5]. Even in read-only mode, SQLite's VFS (Virtual File System) layer may attempt to open these auxiliary files to ensure consistent access, which effectively requires write access to the filesystem if those files do not already exist or need to be recovered [3][4]. To prevent all writes to the source directory when opening a database, the immutable URI parameter is used [1]. By appending?immutable=1 to the database connection string, you instruct SQLite (and by extension, libsql) to treat the database file as completely read-only and static [1]. This flag disables all attempts to create or modify auxiliary files, including the journal or WAL files, as it guarantees that no other process will modify the database and that the database is stored in a location that cannot be changed [1]. This is the standard mechanism to ensure the source directory remains untouched [1].

Citations:


🏁 Script executed:

sed -n '1,180p' src/db/libsql/mod.rs

Repository: nearai/ironclaw

Length of output: 6577


🏁 Script executed:

sed -n '520,590p' src/db/libsql/mod.rs

Repository: nearai/ironclaw

Length of output: 2996


🏁 Script executed:

rg -n "new_local_read_only|immutable=1|SQLITE_OPEN_READ_ONLY|new_local\(" src/db/libsql src/db -g '*.rs'

Repository: nearai/ironclaw

Length of output: 5999


🏁 Script executed:

python3 - <<'PY'
from pathlib import Path
p = Path('src/db/libsql/mod.rs')
for start, end in [(1,180),(520,590)]:
    print(f"\n--- {start}-{end} ---")
    with p.open() as f:
        for i, line in enumerate(f, 1):
            if start <= i <= end:
                print(f"{i:4}: {line.rstrip()}")
PY

Repository: nearai/ironclaw

Length of output: 10958


🏁 Script executed:

sed -n '1,180p' src/db/libsql/mod.rs && printf '\n====\n' && sed -n '520,590p' src/db/libsql/mod.rs

Repository: nearai/ironclaw

Length of output: 9426


Use immutable mode for the read-only source path
SQLITE_OPEN_READ_ONLY only blocks writes to source.db; WAL access can still create or touch -wal/-shm. The source-immutability rule for migration tooling isn’t covered by a single-file snapshot, so this path should use immutable open mode (immutable=1 or equivalent) or assert the entire source directory stays unchanged.

🤖 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 `@src/db/libsql/mod.rs` around lines 94 - 97, Update
Database::new_local_read_only to open the source database in immutable mode,
using libsql’s immutable option or equivalent rather than only
SQLITE_OPEN_READ_ONLY. Ensure opening the database cannot create or modify its
-wal/-shm files or otherwise alter the source directory.

Source: Coding guidelines

@serrrfirat
serrrfirat force-pushed the codex/v1-reborn-migration branch from 1b9420c to 9e41fe2 Compare July 13, 2026 18:54
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-5936 July 13, 2026 18:54 Destroyed
@serrrfirat serrrfirat changed the title feat(reborn): add safe v1 migration workflow feat(migration): add offline v1-to-Reborn migration workflow Jul 13, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 23

♻️ Duplicate comments (4)
docker/reborn/entrypoint.sh (1)

41-51: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

v1:verify still routed through mutating init/config-seeding before running.

v1:verify remains whitelisted next to v1:apply/v1:resume, so it falls through to the normal persistent-volume and config-file installation logic below this guard rather than exiting immediately like plan/status/--help. This lets the entrypoint create/seed target state before a nominally read-only verify even runs — same issue raised in a prior review, still present.

🐛 Proposed fix
-    v1:apply|v1:resume|v1:verify) ;;
+    v1:apply|v1:resume) ;;

As per coding guidelines, "verify must perform read-only target-data checks... it may write lifecycle/quarantine state but must not boot a full Reborn runtime."

🤖 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 `@docker/reborn/entrypoint.sh` around lines 41 - 51, Update the migrate command
routing in the entrypoint guard so v1:verify exits through the read-only
migration command path instead of remaining in the mutating whitelist with
v1:apply and v1:resume. Preserve the existing handling for apply/resume while
ensuring verify runs target-data checks without persistent-volume or config-file
initialization and without booting the full Reborn runtime.

Source: Coding guidelines

crates/ironclaw_reborn_cli/src/commands/onboard.rs (1)

78-89: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Dry-run still reports marker preservation when the real run would force a rewrite.

print_dry_run at Line 82 receives plain self.force, but the actual marker write at Line 113 uses self.force || migration_requested || self.skip_v1_migration. --dry-run --migrate-v1 (or --skip-v1-migration) against an existing marker prints would_preserve while the real invocation with the same flags rewrites the marker. This is the same mismatch raised in a prior review and does not appear to have been fixed here.

🐛 Proposed fix
+        let force_marker = self.force || migration_requested || self.skip_v1_migration;
+
         if self.dry_run {
             print_dry_run(
                 home,
                 &marker_path,
-                self.force,
+                force_marker,
                 migration_requested,
                 self.skip_v1_migration,
                 source.as_ref(),
                 &manifest_path,
             );
             return Ok(());
         }
@@
         let marker_action = write_onboarding_marker(
             home,
             &marker_path,
-            self.force || migration_requested || self.skip_v1_migration,
+            force_marker,
             migration_state,
             &manifest_path,
         )?;

Also applies to: 110-116

🤖 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/commands/onboard.rs` around lines 78 - 89,
Update the dry-run call in the onboarding flow around print_dry_run to pass the
same effective force condition used by the real marker write: self.force ||
migration_requested || self.skip_v1_migration. Keep marker reporting consistent
between dry-run and execution, including when migration or skip-migration flags
are set.
crates/ironclaw_reborn_migration/src/convert/projects.rs (1)

71-78: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Still persisting the legacy host workspace_path into project metadata — unresolved from last review.

project.workspace_path is a raw v1 host filesystem path, copied verbatim into canonical ProjectRecord.metadata where it becomes readable through the project surface. Per the coding guideline "do not expose raw... backend paths... across public surfaces," this should be dropped or redacted to a marker.

🔒 Proposed fix
             metadata: json!({
                 "legacy_engine_v2": {
                     "goals": project.goals,
                     "metrics": project.metrics,
                     "metadata": project.metadata,
-                    "workspace_path": project.workspace_path,
+                    "workspace_migrated": project.workspace_path.is_some(),
                 }
             }),

As per coding guidelines, "Do not expose raw secrets, backend paths, private URLs, transport internals, raw SQL/backend errors, or unredacted runtime or user content across public surfaces."

🤖 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_migration/src/convert/projects.rs` around lines 71 -
78, Remove the workspace_path field from the legacy_engine_v2 object constructed
in the project conversion metadata, or replace its raw value with the project’s
established redaction marker. Keep the goals, metrics, and metadata fields
unchanged, and ensure no raw v1 host filesystem path reaches
ProjectRecord.metadata.

Source: Coding guidelines

crates/ironclaw_reborn_migration/src/convert/memory.rs (1)

37-49: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

has_engine_converter still uses the same loose grammar the last review flagged — and no mission converter exists in this diff to back it up.

ends_with("mission.json") and contains("/threads/") && ends_with(".json") match any nested path, not just the owning converter's exact grammar (mirrors projects.rs's project_slug, which does use exact segment matching). Worse: convert/mod.rs registers automations, extensions, heartbeat, memory, projects, secrets, settings, threads, users — there is no missions converter module, and threads.rs is presumably the v1 threads table converter, not an engine-v2 /threads/*.json blob reader. If nothing actually consumes these paths, has_engine_converter returning true causes line 48 to silently drop the document with no record_loss — the exact scenario "every known v1 artifact must receive an explicit disposition" was written to prevent. The tests only cover exact-path positives/negatives, not nested near-matches (engine/runtime/x/mission.json, foo/threads/bar.json).

♻️ Proposed fix: exact segment grammar mirroring project_slug
 fn has_engine_converter(path: &str) -> bool {
-    if path.ends_with("mission.json") || (path.contains("/threads/") && path.ends_with(".json")) {
-        return true;
-    }
     let segments: Vec<_> = path.trim_matches('/').split('/').collect();
     matches!(
         segments.as_slice(),
         ["engine", "projects", slug, "project.json"]
             | [".system", "engine", "projects", slug, "project.json"]
+            | ["engine", "missions", _, "mission.json"]
+            | [".system", "engine", "missions", _, "mission.json"]
+            | ["engine", "threads", _]
+            | [".system", "engine", "threads", _]
             if !slug.is_empty()
     )
 }

Run this to confirm no mission/thread converter actually reads memory_documents at engine-v2 paths:

#!/bin/bash
fd . crates/ironclaw_reborn_migration/src/convert -e rs
rg -n 'mission' crates/ironclaw_reborn_migration/src/convert/automations.rs crates/ironclaw_reborn_migration/src/convert/threads.rs 2>/dev/null
rg -n 'memory_documents|all_memory_documents' crates/ironclaw_reborn_migration/src -g '*.rs'

Also applies to: 175-186, 188-200

🤖 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_migration/src/convert/memory.rs` around lines 37 - 49,
The engine-path converter detection used by has_engine_converter must only
recognize paths with an actually supported exact grammar. Remove mission and
engine-v2 thread matches unless corresponding converters consume those
documents, and tighten any retained matching to exact path segments rather than
broad suffix or substring checks, mirroring projects.rs project_slug behavior.
Ensure unsupported nested near-matches return false so the caller records
LossReason::NoTargetConcept instead of silently dropping documents.
🤖 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_reborn_cli/src/commands/extension.rs`:
- Around line 135-155: Update the quarantine test around
ExtensionCommand::execute to create valid matching local and durable migration
state with status “applying,” including all required binding fields. Invoke the
command through ExtensionCommand::execute and assert the specific
applying-status lifecycle error, ensuring the test exercises policy gating
rather than marker deserialization failure.

In `@crates/ironclaw_reborn_cli/src/commands/migrate.rs`:
- Around line 31-42: Update MigrationStateRecord to include release_version and
require it to equal env!("CARGO_PKG_VERSION") when reading and validating
migration state from both local and durable backends. Ensure equality covers
release/protocol, profile, backend, locator fingerprint, tenant, and agent, and
add a regression test confirming matching current-release state is accepted
while older-release state is rejected.
- Around line 398-402: Update the target-resolution match in the migration
command to propagate resolver failures instead of converting Err(_) to Ok(None).
Add contextual error information while preserving the existing successful target
path and None behavior only for cases where resolution legitimately returns no
target.

In `@crates/ironclaw_reborn_composition/src/migration_support.rs`:
- Around line 195-211: Update the locator-fingerprint resolution around ancestor
canonicalization to propagate failures from canonicalize instead of using
unwrap_or(ancestor). Convert the error into RebornBuildError::InvalidConfig
through the enclosing function’s existing error path, while preserving the
missing-suffix reconstruction and hashing behavior after successful
canonicalization.

In `@crates/ironclaw_reborn_migration/src/convert/secrets.rs`:
- Around line 175-181: The put_if_absent_or_matches error mapping must not
expose raw SecretStoreError text through MigrationError::WriteTarget.reason. In
the surrounding migration function, replace e.to_string() with the same fixed,
non-sensitive reason pattern already used by secret_collision, while preserving
the existing domain and error propagation.

In `@crates/ironclaw_reborn_migration/src/lib.rs`:
- Around line 299-313: Retire the unsafe one-shot apply path in run_migration:
keep this wrapper limited to planning/dry-run, or require explicit lifecycle
safety acknowledgements before calling apply_migration instead of fabricating
ApplyAcknowledgements::offline_snapshot(). In
crates/ironclaw_reborn_migration/src/options.rs lines 50-60, remove target-key
derivation from the source key and require independent source and target keys.
- Around line 459-465: Enforce exact release compatibility at both public
lifecycle boundaries: update the manifest validation in
crates/ironclaw_reborn_migration/src/lib.rs (lines 459-465) to reject differing
release information, and add the same compatibility check in
crates/ironclaw_reborn_migration/src/main.rs (lines 575-587) before persisting
any CLI lifecycle transition. Use the existing manifest and version symbols,
preserving the current invalid-input error behavior.
- Around line 207-231: Centralize post-Applying failure handling in the
migration library: wrap V1Source::open, RebornTarget::open, converter execution,
and related post-claim errors so each produces, seals, persists, and returns one
authoritative Failed manifest. In crates/ironclaw_reborn_migration/src/lib.rs
lines 207-231, return the failed manifest with the error; in
crates/ironclaw_reborn_migration/src/main.rs lines 290-300, persist that
returned manifest instead of deriving another; and in lines 333-339 and 364-371,
reuse the authoritative resume and verification failure states without creating
competing lifecycle records.
- Around line 87-90: Update the target emptiness checks using target_empty in
both plan and apply flows to propagate filesystem metadata errors instead of
treating path.exists() failures as empty. Change canonicalish() to return and
propagate current_dir() and canonicalize() errors rather than falling back to
"." or the last reachable ancestor, preserving distinct-store and emptiness
validation for successful probes.

In `@crates/ironclaw_reborn_migration/src/manifest.rs`:
- Around line 154-168: Preserve specialized identity and profile types
throughout the resolved migration scope instead of converting them to strings.
In crates/ironclaw_reborn_migration/src/manifest.rs:154-168, update
ResolvedScope fields to use the owning domain types; in
crates/ironclaw_reborn_migration/src/options.rs:21-26, replace the raw profile
string with the profile type; in
crates/ironclaw_reborn_migration/src/main.rs:183-190, retain that resolved
profile type; and in crates/ironclaw_reborn_migration/src/main.rs:444-450,
remove the conversion to an owned string.
- Around line 267-316: Convert manifest persistence and all migration lifecycle
filesystem I/O to Tokio async: make Manifest::write_atomic async using async
filesystem primitives, update target-state persistence in
crates/ironclaw_reborn_migration/src/main.rs:454-533 to await it, and make
manifest reads and report persistence in
crates/ironclaw_reborn_migration/src/main.rs:575-599 async with awaited Tokio
operations; preserve the existing atomic, overwrite, permissions, and cleanup
behavior.

In `@crates/ironclaw_reborn_migration/src/mounts.rs`:
- Around line 19-34: Replace migration_mount_view_is_the_production_mount_view
with an integration test that invokes a real migration repository writer using
the test scope, then reopens the written record through
ironclaw_reborn_composition::invocation_mount_view. Assert the persisted record
is accessible through the production resolver, covering both resolver usage and
correct scope construction; avoid directly comparing the two mount-view
functions.

In `@crates/ironclaw_reborn_migration/src/source.rs`:
- Around line 799-807: Centralize sanitization for migration backend errors:
update source_open_error and source_read_error to emit the raw diagnostic via
debug! while returning stable redacted MigrationError messages; in
crates/ironclaw_reborn_migration/src/target.rs lines 1102-1107, remove {error}
from the public message and route direct target-driver mappings through the
existing sanitized helper. Ensure no raw backend error crosses a public surface.
- Around line 67-74: Update the metadata check in the async SourceDb::LibSql
branch of open to use Tokio’s asynchronous filesystem metadata API instead of
std::fs::metadata, while preserving the existing MigrationError::OpenSource
message and error propagation.

In `@crates/ironclaw_reborn_migration/src/target_ids.rs`:
- Around line 36-53: Introduce a private MigrationIdDomain enum for the
supported deterministic ID domains, with centralized stable seed labels for
routine, mission, and thread-binding. Update trigger_id and the related methods
around lines 75–97 to accept/use this enum and derive the scoped UUID seed from
its canonical label, eliminating direct stringly typed domain values while
preserving existing ID stability.
- Around line 23-33: Update MigrationIdentity::from_report to call
manifest.validate_plan_hash()? after confirming the manifest exists and before
copying manifest_schema_version or source_fingerprint. Propagate validation
failures as MigrationError and only derive target IDs from a validated sealed
manifest.

In `@crates/ironclaw_reborn_migration/src/target.rs`:
- Around line 1075-1098: Update the advisory-lock cleanup in the target lock
probe so both target and source unlock operations are attempted and their
results are propagated with contextual errors instead of discarded. Preserve the
original query error while reporting cleanup failures appropriately, and ensure
no path returns while either identity lock may remain held.
- Around line 46-50: Update target_is_empty for TargetStore::LibSql to open an
existing database read-only and inspect its durable tables for actual rows,
returning empty for pre-created empty or schema-only files while preserving
PostgreSQL parity. Keep the missing-path behavior unchanged, and add a
regression test covering an existing empty LibSQL file.

In `@crates/ironclaw_reborn_migration/tests/companion_cli.rs`:
- Around line 36-54: Expand
lifecycle_help_never_accepts_raw_postgres_urls_or_keys to invoke help for every
relevant v1 lifecycle subcommand, including plan, apply, and resume. Assert each
command exposes the supported source flags and rejects --source-postgres-url,
--target-postgres, and --secret-master-key, preserving the existing success and
output diagnostics.

In `@crates/ironclaw_reborn_migration/tests/migration_roundtrip.rs`:
- Around line 952-982: The test currently expects the stale manifest to fail
during the lifecycle transition before trigger collision reconciliation runs.
Add a separate resume scenario using an allowed migration state, invoke
resume_migration through the normal caller, and assert the divergent target-slot
collision error. Keep the existing assertion that the “cron-light” prompt
remains “divergent target prompt,” verifying the collision fails without
overwriting.

In `@crates/ironclaw_reborn_migration/tests/migration_safety.rs`:
- Around line 162-185: Update
plan_is_source_read_only_and_does_not_create_target to snapshot every file under
the source store directory, including relative paths and bytes, before
plan_migration and compare the complete snapshot afterward. Avoid
source_contents() or any reopening of the source during verification, and retain
the assertions that planning does not create the target database or parent
directory.

In `@crates/ironclaw_secrets/src/lib.rs`:
- Around line 1021-1037: Implement put_if_absent_or_matches for both
ScopedSecretsStoreAdapter<S> and InMemorySecretStore instead of inheriting the
StoreUnavailable default. Delegate through ScopedSecretsStoreAdapter to its
wrapped store with the appropriate scope handling, and add the equivalent atomic
insert, expiry, and material-match behavior to InMemorySecretStore so callers
preserve SecretPutOutcome semantics.

In `@docs/reborn-binary.md`:
- Around line 395-402: Clarify the onboarding documentation’s statement about v1
import state so it applies only to normal onboarding. Explicitly exempt the
read-only --migrate-v1 migration planning path, which reads detected v1 state
without applying changes, while preserving the existing behavior descriptions
for --dry-run and --import-history.

---

Duplicate comments:
In `@crates/ironclaw_reborn_cli/src/commands/onboard.rs`:
- Around line 78-89: Update the dry-run call in the onboarding flow around
print_dry_run to pass the same effective force condition used by the real marker
write: self.force || migration_requested || self.skip_v1_migration. Keep marker
reporting consistent between dry-run and execution, including when migration or
skip-migration flags are set.

In `@crates/ironclaw_reborn_migration/src/convert/memory.rs`:
- Around line 37-49: The engine-path converter detection used by
has_engine_converter must only recognize paths with an actually supported exact
grammar. Remove mission and engine-v2 thread matches unless corresponding
converters consume those documents, and tighten any retained matching to exact
path segments rather than broad suffix or substring checks, mirroring
projects.rs project_slug behavior. Ensure unsupported nested near-matches return
false so the caller records LossReason::NoTargetConcept instead of silently
dropping documents.

In `@crates/ironclaw_reborn_migration/src/convert/projects.rs`:
- Around line 71-78: Remove the workspace_path field from the legacy_engine_v2
object constructed in the project conversion metadata, or replace its raw value
with the project’s established redaction marker. Keep the goals, metrics, and
metadata fields unchanged, and ensure no raw v1 host filesystem path reaches
ProjectRecord.metadata.

In `@docker/reborn/entrypoint.sh`:
- Around line 41-51: Update the migrate command routing in the entrypoint guard
so v1:verify exits through the read-only migration command path instead of
remaining in the mutating whitelist with v1:apply and v1:resume. Preserve the
existing handling for apply/resume while ensuring verify runs target-data checks
without persistent-volume or config-file initialization and without booting the
full Reborn runtime.
🪄 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: 0cc06fdf-c6d7-4b83-9e9f-0f0272bc351d

📥 Commits

Reviewing files that changed from the base of the PR and between 1b9420c and 9e41fe2.

⛔ Files ignored due to path filters (2)
  • CHANGELOG.md is excluded by !CHANGELOG.md
  • Cargo.lock is excluded by !**/*.lock, !**/Cargo.lock
📒 Files selected for processing (83)
  • Dockerfile.reborn
  • FEATURE_PARITY.md
  • README.md
  • crates/AGENTS.md
  • crates/README.md
  • crates/ironclaw_memory/src/lib.rs
  • crates/ironclaw_memory/src/service.rs
  • crates/ironclaw_memory_native/CLAUDE.md
  • crates/ironclaw_memory_native/src/backend.rs
  • crates/ironclaw_memory_native/src/lib.rs
  • crates/ironclaw_memory_native/src/service.rs
  • crates/ironclaw_reborn_cli/AGENTS.md
  • crates/ironclaw_reborn_cli/Cargo.toml
  • crates/ironclaw_reborn_cli/src/commands/doctor.rs
  • crates/ironclaw_reborn_cli/src/commands/extension.rs
  • crates/ironclaw_reborn_cli/src/commands/migrate.rs
  • crates/ironclaw_reborn_cli/src/commands/mod.rs
  • crates/ironclaw_reborn_cli/src/commands/onboard.rs
  • crates/ironclaw_reborn_cli/src/context.rs
  • crates/ironclaw_reborn_cli/tests/migration_cli.rs
  • crates/ironclaw_reborn_cli/tests/smoke.rs
  • crates/ironclaw_reborn_composition/AGENTS.md
  • crates/ironclaw_reborn_composition/CLAUDE.md
  • crates/ironclaw_reborn_composition/src/admin_user_directory.rs
  • crates/ironclaw_reborn_composition/src/factory.rs
  • crates/ironclaw_reborn_composition/src/input.rs
  • crates/ironclaw_reborn_composition/src/lib.rs
  • crates/ironclaw_reborn_composition/src/migration_support.rs
  • crates/ironclaw_reborn_identity/CONTRACT.md
  • crates/ironclaw_reborn_identity/src/filesystem_store/directory.rs
  • crates/ironclaw_reborn_identity/src/filesystem_store/tests.rs
  • crates/ironclaw_reborn_identity/src/lib.rs
  • crates/ironclaw_reborn_identity/src/user_directory.rs
  • crates/ironclaw_reborn_migration/CLAUDE.md
  • crates/ironclaw_reborn_migration/Cargo.toml
  • crates/ironclaw_reborn_migration/src/convert/automations.rs
  • crates/ironclaw_reborn_migration/src/convert/extensions.rs
  • crates/ironclaw_reborn_migration/src/convert/heartbeat.rs
  • crates/ironclaw_reborn_migration/src/convert/memory.rs
  • crates/ironclaw_reborn_migration/src/convert/mod.rs
  • crates/ironclaw_reborn_migration/src/convert/projects.rs
  • crates/ironclaw_reborn_migration/src/convert/secrets.rs
  • crates/ironclaw_reborn_migration/src/convert/settings.rs
  • crates/ironclaw_reborn_migration/src/convert/threads.rs
  • crates/ironclaw_reborn_migration/src/convert/users.rs
  • crates/ironclaw_reborn_migration/src/error.rs
  • crates/ironclaw_reborn_migration/src/inventory.rs
  • crates/ironclaw_reborn_migration/src/lib.rs
  • crates/ironclaw_reborn_migration/src/main.rs
  • crates/ironclaw_reborn_migration/src/manifest.rs
  • crates/ironclaw_reborn_migration/src/mounts.rs
  • crates/ironclaw_reborn_migration/src/options.rs
  • crates/ironclaw_reborn_migration/src/report.rs
  • crates/ironclaw_reborn_migration/src/source.rs
  • crates/ironclaw_reborn_migration/src/target.rs
  • crates/ironclaw_reborn_migration/src/target_ids.rs
  • crates/ironclaw_reborn_migration/src/v2_model.rs
  • crates/ironclaw_reborn_migration/tests/companion_cli.rs
  • crates/ironclaw_reborn_migration/tests/migration_roundtrip.rs
  • crates/ironclaw_reborn_migration/tests/migration_safety.rs
  • crates/ironclaw_reborn_migration/tests/project_migration.rs
  • crates/ironclaw_secrets/src/filesystem_store.rs
  • crates/ironclaw_secrets/src/lib.rs
  • crates/ironclaw_triggers/src/lib.rs
  • crates/ironclaw_triggers/src/libsql.rs
  • crates/ironclaw_triggers/src/postgres.rs
  • crates/ironclaw_triggers/tests/repository_contract.rs
  • docker/reborn/entrypoint.sh
  • docs/internal/2026-06-26-legacy-vs-reborn-feature-comparison.md
  • docs/plans/8513-cli-smoke-and-secrets-decomposition.md
  • docs/plans/composition-pubuse.snapshot
  • docs/reborn-binary.md
  • docs/reborn/README.md
  • docs/reborn/contracts/memory.md
  • docs/reborn/contracts/migration-compatibility.md
  • docs/reborn/contracts/secrets.md
  • docs/reborn/contracts/triggers.md
  • docs/reborn/deploy-reborn-cli-docker.md
  • docs/reborn/onboarding.md
  • docs/reborn/setup-slack-for-reborn-binary.md
  • docs/reborn/v1-migration.md
  • src/db/libsql/mod.rs
  • src/setup/README.md

Comment on lines +135 to +155
serde_json::json!({
"schema_version": "ironclaw.reborn.migration-state/v1",
"migration_protocol_version": 1,
"release_version": env!("CARGO_PKG_VERSION"),
"status": "applying",
})
.to_string(),
)
.expect("write marker");
let command = ExtensionCommand {
confirm_host_access: false,
command: ExtensionSubcommand::Search(ExtensionSearchCommand {
query: None,
json: false,
}),
};

let error = command
.execute(context)
.expect_err("quarantine must reject extension lifecycle commands");
assert!(error.to_string().contains("quarantined"), "{error:#}");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Exercise the applying status instead of a malformed marker.

This JSON omits required binding fields, so deserialization fails and the generic "quarantined" assertion passes before lifecycle policy runs. Create matching local/durable state and assert the applying-status error specifically.

As per path instructions, “Test through the caller” when a helper gates a side effect.

🤖 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/commands/extension.rs` around lines 135 - 155,
Update the quarantine test around ExtensionCommand::execute to create valid
matching local and durable migration state with status “applying,” including all
required binding fields. Invoke the command through ExtensionCommand::execute
and assert the specific applying-status lifecycle error, ensuring the test
exercises policy gating rather than marker deserialization failure.

Source: Path instructions

Comment on lines +31 to +42
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
struct MigrationStateRecord {
schema_version: String,
migration_protocol_version: u32,
run_id: String,
status: String,
profile: String,
target_backend: String,
target_locator_fingerprint: String,
tenant_id: String,
agent_id: String,
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Bind quarantine state to the current release.

MigrationStateRecord drops release_version, so local/durable equality can accept a verified claim created by an older release. Read it from both backends and require env!("CARGO_PKG_VERSION"); add a matching-state regression test.

As per coding guidelines, the lifecycle claim must bind “release/protocol, profile, backend, locator fingerprint, tenant, and agent.”

🤖 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/commands/migrate.rs` around lines 31 - 42,
Update MigrationStateRecord to include release_version and require it to equal
env!("CARGO_PKG_VERSION") when reading and validating migration state from both
local and durable backends. Ensure equality covers release/protocol, profile,
backend, locator fingerprint, tenant, and agent, and add a regression test
confirming matching current-release state is accepted while older-release state
is rejected.

Source: Coding guidelines

Comment on lines +398 to +402
let target =
match ironclaw_reborn_composition::resolve_reborn_migration_target(context.boot_config()) {
Ok(target) => target,
Err(_) => return Ok(None),
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Propagate target-resolution failures; this currently fails open.

When no local marker exists, converting this error to None lets read_activation_state_status report no quarantine state. Preserve the resolver error with context.

Proposed fix
-            Err(_) => return Ok(None),
+            Err(error) => {
+                return Err(error).context(
+                    "failed to resolve the Reborn target for migration quarantine inspection",
+                );
+            }

As per path instructions, “Fail loud” and propagate errors with context instead of discarding them.

📝 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.

Suggested change
let target =
match ironclaw_reborn_composition::resolve_reborn_migration_target(context.boot_config()) {
Ok(target) => target,
Err(_) => return Ok(None),
};
let target =
match ironclaw_reborn_composition::resolve_reborn_migration_target(context.boot_config()) {
Ok(target) => target,
Err(error) => {
return Err(error).context(
"failed to resolve the Reborn target for migration quarantine inspection",
);
}
};
🤖 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/commands/migrate.rs` around lines 398 - 402,
Update the target-resolution match in the migration command to propagate
resolver failures instead of converting Err(_) to Ok(None). Add contextual error
information while preserving the existing successful target path and None
behavior only for cases where resolution legitimately returns no target.

Source: Path instructions

Comment on lines +195 to +211
let mut ancestor = normalized.clone();
let mut missing_suffix = Vec::new();
while !ancestor.exists() {
let Some(name) = ancestor.file_name() else {
break;
};
missing_suffix.push(name.to_os_string());
if !ancestor.pop() {
break;
}
}
let mut resolved = ancestor.canonicalize().unwrap_or(ancestor);
for component in missing_suffix.into_iter().rev() {
resolved.push(component);
}
ironclaw_common::hashing::sha256_hex(resolved.as_os_str().as_encoded_bytes())
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Silent canonicalize() fallback risks locator-fingerprint drift.

ancestor.canonicalize().unwrap_or(ancestor) swallows any IO error from canonicalizing an ancestor that was just confirmed to exist (permission error, symlink race, etc.) and silently falls back to the un-canonicalized path. Since this fingerprint is the target identity used for quarantine/activation-guard comparisons and lifecycle-claim binding, an intermittent canonicalize failure on one call and success on another for the same target produces two different fingerprints for the same physical database — defeating the "atomic, run-bound... locator fingerprint" guarantee this function exists to provide.

Propagate the canonicalize error into RebornBuildError::InvalidConfig instead of silently falling back, or add an explicit // silent-ok: <reason> comment if the fallback is intentionally acceptable.

🔧 Proposed fix
-    let mut resolved = ancestor.canonicalize().unwrap_or(ancestor);
+    let resolved_ancestor = ancestor.canonicalize().map_err(|error| {
+        // propagate to caller; this fn signature would need to become fallible
+        error
+    })?;
+    let mut resolved = resolved_ancestor;
🤖 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/migration_support.rs` around lines 195
- 211, Update the locator-fingerprint resolution around ancestor
canonicalization to propagate failures from canonicalize instead of using
unwrap_or(ancestor). Convert the error into RebornBuildError::InvalidConfig
through the enclosing function’s existing error path, while preserving the
missing-suffix reconstruction and hashing behavior after successful
canonicalization.

Source: Coding guidelines

Comment thread crates/ironclaw_reborn_migration/src/convert/secrets.rs
Comment on lines +36 to +54
#[test]
fn lifecycle_help_never_accepts_raw_postgres_urls_or_keys() {
let output = Command::new(companion_bin())
.args(["v1", "plan", "--help"])
.env_clear()
.output()
.expect("run migration companion help");

assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("--source-postgres"), "stdout: {stdout}");
assert!(stdout.contains("--source-home"), "stdout: {stdout}");
assert!(
!stdout.contains("--source-postgres-url"),
"stdout: {stdout}"
);
assert!(!stdout.contains("--target-postgres"), "stdout: {stdout}");
assert!(!stdout.contains("--secret-master-key"), "stdout: {stdout}");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Cover every source-bearing lifecycle subcommand.

This only inspects v1 plan --help; apply or resume could expose raw URL/key flags without failing the test. Apply the forbidden-flag assertions to every relevant subcommand.

As per coding guidelines, “Do not accept PostgreSQL source URLs or source keys as raw CLI values.”

🤖 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_migration/tests/companion_cli.rs` around lines 36 -
54, Expand lifecycle_help_never_accepts_raw_postgres_urls_or_keys to invoke help
for every relevant v1 lifecycle subcommand, including plan, apply, and resume.
Assert each command exposes the supported source flags and rejects
--source-postgres-url, --target-postgres, and --secret-master-key, preserving
the existing success and output diagnostics.

Source: Coding guidelines

Comment on lines +952 to 982
// A stale applied manifest cannot reopen a verified target claim or
// overwrite operator/runtime state.
overwrite_trigger_prompt(&dst, "cron-light", "divergent target prompt").await;
let resume_manifest = report2.manifest.as_ref().expect("resume manifest");
let collision = resume_migration(
options(src, dst.clone(), false),
resume_manifest,
MigrationSecretInputs {
source_master_key: Some(SecretString::from(MASTER_KEY)),
target_master_key: None,
},
ApplyAcknowledgements::offline_snapshot(),
)
.await
.expect_err("verified target claim must fail closed");
assert!(
collision
.to_string()
.contains("invalid shared migration state transition verified -> applying"),
"unexpected collision error: {collision}"
);
assert_eq!(
installations[0]["owner"]["user_ids"],
serde_json::json!([USER, USER_BOB])
reborn_triggers(&dst)
.await
.into_iter()
.find(|trigger| trigger.name == "cron-light")
.expect("cron-light trigger")
.prompt,
"divergent target prompt",
"migration must not overwrite a divergent deterministic slot"
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

This path never exercises trigger collision reconciliation.

After verification, resume fails at verified -> applying, before the divergent trigger is read. Add a separate resume from an allowed lifecycle state and assert the divergent-slot error plus unchanged prompt.

As per coding guidelines, divergent target collisions must fail without overwriting and side-effect gates must be tested through the caller.

🤖 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_migration/tests/migration_roundtrip.rs` around lines
952 - 982, The test currently expects the stale manifest to fail during the
lifecycle transition before trigger collision reconciliation runs. Add a
separate resume scenario using an allowed migration state, invoke
resume_migration through the normal caller, and assert the divergent target-slot
collision error. Keep the existing assertion that the “cron-light” prompt
remains “divergent target prompt,” verifying the collision fails without
overwriting.

Source: Coding guidelines

Comment on lines +162 to +185
async fn plan_is_source_read_only_and_does_not_create_target() {
let directory = tempfile::tempdir().expect("tempdir");
let source = directory.path().join("source-with-password-canary.db");
let target = directory.path().join("new-reborn-home").join("target.db");
seed_source(&source).await;
let before = source_contents(&source).await;
let bytes_before = std::fs::read(&source).expect("read source snapshot");

let manifest = plan_migration(&options(source.clone(), target.clone()))
.await
.expect("plan");

assert_eq!(manifest.status, MigrationStatus::Planned);
assert_eq!(source_contents(&source).await, before);
assert_eq!(
std::fs::read(&source).expect("read source after plan"),
bytes_before,
"planning modified source database bytes"
);
assert!(!target.exists(), "planning created the target database");
assert!(
!target.parent().expect("target parent").exists(),
"planning created the target home"
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Snapshot the whole source store, not only the main DB file.

source_contents() reopens libSQL, while the byte check ignores -wal, -shm, and other sidecars. Snapshot relative paths and bytes for the entire source directory before and after planning without reopening it.

As per coding guidelines, source readers must be read-only and migration must preserve source immutability.

🤖 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_migration/tests/migration_safety.rs` around lines 162
- 185, Update plan_is_source_read_only_and_does_not_create_target to snapshot
every file under the source store directory, including relative paths and bytes,
before plan_migration and compare the complete snapshot afterward. Avoid
source_contents() or any reopening of the source during verification, and retain
the assertions that planning does not create the target database or parent
directory.

Source: Coding guidelines

Comment on lines +1021 to +1037
/// Atomically inserts a secret only when its deterministic slot is absent.
///
/// An existing record is never replaced. Instead, the store compares its
/// expiry and material without exposing the stored value and reports
/// whether it is an exact match or divergent state.
async fn put_if_absent_or_matches(
&self,
scope: ResourceScope,
handle: SecretHandle,
material: SecretMaterial,
expires_at: Option<Timestamp>,
) -> Result<SecretPutOutcome, SecretStoreError> {
let _ = (scope, handle, material, expires_at);
Err(SecretStoreError::StoreUnavailable {
reason: "atomic secret insertion is unavailable".to_string(),
})
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -nP -C3 'impl(?:<[^>]+>)?\s+SecretStore\s+for' crates -g '*.rs'
ast-grep run \
  --pattern 'async fn put_if_absent_or_matches($$$) { $$$ }' \
  --lang rust crates

Repository: nearai/ironclaw

Length of output: 9475


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the SecretStore trait and the new default method.
sed -n '880,1075p' crates/ironclaw_secrets/src/lib.rs

# List every SecretStore impl and whether it mentions the new method.
rg -n -C2 'impl(?:<[^>]+>)?\s+SecretStore\s+for|put_if_absent_or_matches\(' crates -g '*.rs'

Repository: nearai/ironclaw

Length of output: 16257


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the wrapper and in-memory implementations around the trait impls.
sed -n '1210,1285p' crates/ironclaw_secrets/src/lib.rs
sed -n '1490,1565p' crates/ironclaw_secrets/src/lib.rs
sed -n '340,455p' crates/ironclaw_secrets/src/filesystem_store.rs

# Check where put_if_absent_or_matches is called in production code.
rg -n -C2 'put_if_absent_or_matches\(' crates src -g '*.rs'

Repository: nearai/ironclaw

Length of output: 11283


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the migration wiring around the atomic insert call.
sed -n '1,260p' crates/ironclaw_reborn_migration/src/convert/secrets.rs

# Find constructors/usages of the scoped adapter and in-memory store.
rg -n -C3 'ScopedSecretsStoreAdapter|InMemorySecretStore::new|InMemorySecretStore::default|put_if_absent_or_matches\(' crates -g '*.rs'

Repository: nearai/ironclaw

Length of output: 50372


Forward put_if_absent_or_matches in the wrapper stores ScopedSecretsStoreAdapter<S> and InMemorySecretStore still inherit the StoreUnavailable default in crates/ironclaw_secrets/src/lib.rs, so any caller through those paths loses atomic-insert semantics. Make the method required or delegate it in each wrapper.

🤖 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_secrets/src/lib.rs` around lines 1021 - 1037, Implement
put_if_absent_or_matches for both ScopedSecretsStoreAdapter<S> and
InMemorySecretStore instead of inheriting the StoreUnavailable default. Delegate
through ScopedSecretsStoreAdapter to its wrapped store with the appropriate
scope handling, and add the equivalent atomic insert, expiry, and material-match
behavior to InMemorySecretStore so callers preserve SecretPutOutcome semantics.

Source: Coding guidelines

Comment thread docs/reborn-binary.md
Comment on lines +395 to +402
cargo run -q -p ironclaw_reborn_cli --bin ironclaw-reborn -- onboard --migrate-v1
```

`--dry-run` reports what would be initialized without writing files.
`--import-history` reserves the history-import step in the summary (not wired
yet). See `docs/reborn/onboarding.md` for the full slice description and the
completion-marker schema.
`--migrate-v1` explicitly runs only the migration planning step after detecting
a source; it never applies automatically. `--import-history` is a deprecated
hidden alias. See `docs/reborn/onboarding.md` for the completion-marker schema
and `docs/reborn/v1-migration.md` for cutover and rollback.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Clarify that migration onboarding reads v1 state.

Lines 399-402 introduce --migrate-v1, but the section above says onboarding does not call into v1 import state. Scope that claim to normal onboarding or explicitly exempt read-only migration planning.

🤖 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-binary.md` around lines 395 - 402, Clarify the onboarding
documentation’s statement about v1 import state so it applies only to normal
onboarding. Explicitly exempt the read-only --migrate-v1 migration planning
path, which reads detected v1 state without applying changes, while preserving
the existing behavior descriptions for --dry-run and --import-history.

serrrfirat and others added 19 commits July 13, 2026 23:29
…get` to Reborn (#4379)

* feat: migrate read-only commands `doctor`, `status` and `config list/get` to Reborn

* fix: fix bug when `llm.default.` placeholders were omiitted

* refactor: refactor `Renderable` to write to a `Write` instead of directly to stdout

* test: expand unit test coverage

* fix: include `slack.enabled` config keys in `config` output by refactoring `flatten_config`

* chore: suppress `check_no_panics` with safety comment

* fix: get rid of unnecessary panic in `flatten_config`

* fix: use `is_dir` instead of `exists` for `reborn_home` check

* fix: prevent race condition when file could be deleted between syscalls

* test: add smoke test for `doctor --json` output

* test: add test for negative integer coercion to float in config values

* test: check all three outcome icons are rendered in `doctor` output

* test: test `build_config_get_dto` when it returns `Some(value)`

* test: remove duplicae `test_context` function

* test: tighten `doctor_reports_explicit_profile` to perform same-line check for "profile" and "production" in stdout

* test: use `IgnoredAny` instead of `Value` for better perf

* test: test `Failed` branch of `driver_check`

* test: additionally test `collect_leaf_entries` with array values

* fix: ensure non-string elements aren't lost in `collect_leaf_entries`

* chore: document chosen trade-off in `build_config_get_dto`

* style: fix formatting

* chore: fix clippy warning, add `Serialize` to `SlackChannelRouteSection`

* chore: skip serializing if `channel_routes` is empty

* fix: address Reborn CLI review findings

* fix: gate provider diagnostics with LLM feature

* test: update composition facade snapshot

* fix: keep CLI rendering before test modules

---------

Co-authored-by: Firat Sertgoz <f@nuff.tech>
@serrrfirat
serrrfirat force-pushed the codex/v1-reborn-migration branch from 9e41fe2 to 9f215c2 Compare July 13, 2026 20:40
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-5936 July 13, 2026 20:40 Destroyed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

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/factory.rs (1)

1253-1280: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a RebornServices-level regression for lifecycle auth attachment

build_reborn_services wires LifecycleProductFacadeSlot after product auth is created, so the tests in crates/ironclaw_reborn_composition/src/lifecycle_auth_continuation.rs still miss the real attachment path. Add one test that exercises auth completion after the slot is attached, and one that keeps lifecycle support absent so the path stays fail-closed.

🤖 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 1253 - 1280,
Add RebornServices-level regression tests around build_reborn_services, covering
auth completion after LifecycleProductFacadeSlot is attached and confirming
lifecycle support remains fail-closed when absent. Exercise the actual factory
wiring through the returned service, rather than only testing
lifecycle_auth_continuation.rs components in isolation, and preserve existing
product-auth behavior in both cases.

Sources: Coding guidelines, Path instructions

♻️ Duplicate comments (4)
crates/ironclaw_reborn_migration/src/convert/secrets.rs (1)

175-181: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Do not expose SecretStoreError through the migration CLI.

e.to_string() still places backend internals in the operator-facing reason. Use a fixed safe message, consistent with secret_collision.

This repeats the earlier unresolved review finding. As per coding guidelines, raw backend paths, SQL, and transport internals must not cross public surfaces.

Proposed fix
-        .map_err(|e| MigrationError::WriteTarget {
+        .map_err(|_| MigrationError::WriteTarget {
             domain: format!("secret {user_id}:{name}"),
-            reason: e.to_string(),
+            reason: "failed to write deterministic target secret slot".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_reborn_migration/src/convert/secrets.rs` around lines 175 -
181, Update the error mapping around put_if_absent_or_matches in the migration
conversion flow to replace e.to_string() with the same fixed safe message used
by secret_collision. Keep backend SecretStoreError details out of
MigrationError::WriteTarget.reason while preserving the existing domain value.

Source: Coding guidelines

docker/reborn/entrypoint.sh (1)

37-50: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Keep verify read-only at the entrypoint.

v1:verify still falls through to normal initialization and config seeding, allowing target mutation before verification. Only apply and resume should enter the mutating path.

This repeats the earlier unresolved review finding and conflicts with the PR’s strict verification contract.

Proposed fix
-# seed config first. Mutating/resume/verify operations continue through the
+# seed config first. Mutating/resume operations continue through the
 # normal persistent-volume and config checks below.
@@
-    v1:apply|v1:resume|v1:verify) ;;
+    v1:apply|v1:resume) ;;
🤖 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 `@docker/reborn/entrypoint.sh` around lines 37 - 50, Update the migration
dispatch condition in the entrypoint so only v1:apply and v1:resume continue
into the mutating initialization path; route v1:verify through the immediate
exec path alongside read-only planning, status, and help operations, preserving
argument handling for other migration commands.
crates/ironclaw_reborn_migration/src/convert/projects.rs (1)

71-77: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Do not persist the legacy host workspace path.

workspace_path can expose an absolute source-host path through canonical project metadata. Remove it or replace it with a non-path migration marker.

This repeats the earlier unresolved review finding. As per coding guidelines, raw backend paths must not cross public surfaces.

Proposed fix
                 "legacy_engine_v2": {
                     "goals": project.goals,
                     "metrics": project.metrics,
                     "metadata": project.metadata,
-                    "workspace_path": project.workspace_path,
                 }
🤖 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_migration/src/convert/projects.rs` around lines 71 -
77, Remove workspace_path from the legacy_engine_v2 metadata constructed in the
project conversion flow, using the surrounding metadata fields in the projects
conversion implementation as the change point. Do not persist or expose the host
filesystem path; retain the other legacy project fields unchanged.

Source: Coding guidelines

crates/ironclaw_reborn_migration/src/convert/memory.rs (1)

38-48: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Use the owning converters’ exact path classifiers.

ends_with("mission.json") and contains("/threads/") still classify unrelated runtime paths as supported, causing them to be skipped without a loss disposition. The tests also omit the previously identified mission/thread near matches. Reuse shared exact classifiers and add negative coverage for nested mission.json and unrelated /threads/ paths.

This repeats the earlier unresolved review finding. The migration contract requires every unsupported artifact to receive an explicit disposition.

Also applies to: 175-199

🤖 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_migration/src/convert/memory.rs` around lines 38 - 48,
Replace the broad path checks used by the engine migration flow with the owning
converters’ exact shared classifiers, including the logic behind
v2_model::is_engine_path and has_engine_converter. Ensure nested mission.json
files and unrelated /threads/ paths are not treated as supported, and record an
explicit LossReason::NoTargetConcept disposition before continuing. Extend the
relevant tests with these negative near-match cases.
🤖 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_native/src/backend.rs`:
- Around line 1201-1215: Add a caller-level async test alongside
metadata_capability_rejects_direct_backend_metadata_reads that invokes
read_document_metadata with alpha_context() and beta_path(), then asserts the
operation fails before any cross-scope repository read occurs. Keep the
capability setup enabled for metadata so the failure specifically verifies
ensure_path_matches_context scope enforcement.

In `@crates/ironclaw_reborn_cli/Cargo.toml`:
- Line 92: Update the optional libsql dependency declaration to retain only the
features required by the CLI’s local Builder::new_local and
OpenFlags::SQLITE_OPEN_READ_ONLY usage, removing replication, remote, and tls
unless another consumer in this crate demonstrably requires them.

In `@crates/ironclaw_reborn_composition/src/lib.rs`:
- Around line 923-937: Update open_reborn_postgres_pool_with_max_size to resolve
TLS settings through input::postgres_pool_tls_options_from_env(), mapping
failures to RebornCompositionError::InvalidConfig like
open_reborn_postgres_pool. Pass the resolved tls_options to
ironclaw_reborn_event_store::open_postgres_pool_with_tls_options(url, max_size,
tls_options) instead of the default-TLS helper.

In `@crates/ironclaw_reborn_migration/src/convert/automations.rs`:
- Around line 521-529: Update the thread migration flow around thread_project_id
to detect when mission.project_id is present but excluded because it is not in
imported_project_ids, and call record_loss for that dropped project scope.
Mirror the degraded-migration handling used by resolve_mission_project(),
ensuring MigrationReport records the loss before continuing with thread imports.

In `@crates/ironclaw_reborn_migration/src/convert/projects.rs`:
- Around line 47-56: Update the project ownership handling before calling
report.valid_user_id in the project conversion flow: when both project.user_id
and document.user_id are present, require them to match; on a mismatch, record
the ownership loss through the existing report mechanism and skip the project.
Preserve the current valid_user_id validation for non-conflicting ownership and
ensure migrated records retain the document’s tenant/user scope.

---

Outside diff comments:
In `@crates/ironclaw_reborn_composition/src/factory.rs`:
- Around line 1253-1280: Add RebornServices-level regression tests around
build_reborn_services, covering auth completion after LifecycleProductFacadeSlot
is attached and confirming lifecycle support remains fail-closed when absent.
Exercise the actual factory wiring through the returned service, rather than
only testing lifecycle_auth_continuation.rs components in isolation, and
preserve existing product-auth behavior in both cases.

---

Duplicate comments:
In `@crates/ironclaw_reborn_migration/src/convert/memory.rs`:
- Around line 38-48: Replace the broad path checks used by the engine migration
flow with the owning converters’ exact shared classifiers, including the logic
behind v2_model::is_engine_path and has_engine_converter. Ensure nested
mission.json files and unrelated /threads/ paths are not treated as supported,
and record an explicit LossReason::NoTargetConcept disposition before
continuing. Extend the relevant tests with these negative near-match cases.

In `@crates/ironclaw_reborn_migration/src/convert/projects.rs`:
- Around line 71-77: Remove workspace_path from the legacy_engine_v2 metadata
constructed in the project conversion flow, using the surrounding metadata
fields in the projects conversion implementation as the change point. Do not
persist or expose the host filesystem path; retain the other legacy project
fields unchanged.

In `@crates/ironclaw_reborn_migration/src/convert/secrets.rs`:
- Around line 175-181: Update the error mapping around put_if_absent_or_matches
in the migration conversion flow to replace e.to_string() with the same fixed
safe message used by secret_collision. Keep backend SecretStoreError details out
of MigrationError::WriteTarget.reason while preserving the existing domain
value.

In `@docker/reborn/entrypoint.sh`:
- Around line 37-50: Update the migration dispatch condition in the entrypoint
so only v1:apply and v1:resume continue into the mutating initialization path;
route v1:verify through the immediate exec path alongside read-only planning,
status, and help operations, preserving argument handling for other migration
commands.
🪄 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: d5e4f0c5-fdd7-4b1f-972b-ff67a9606231

📥 Commits

Reviewing files that changed from the base of the PR and between 9e41fe2 and 9f215c2.

⛔ Files ignored due to path filters (2)
  • CHANGELOG.md is excluded by !CHANGELOG.md
  • Cargo.lock is excluded by !**/*.lock, !**/Cargo.lock
📒 Files selected for processing (83)
  • Dockerfile.reborn
  • FEATURE_PARITY.md
  • README.md
  • crates/AGENTS.md
  • crates/README.md
  • crates/ironclaw_memory/src/lib.rs
  • crates/ironclaw_memory/src/service.rs
  • crates/ironclaw_memory_native/CLAUDE.md
  • crates/ironclaw_memory_native/src/backend.rs
  • crates/ironclaw_memory_native/src/lib.rs
  • crates/ironclaw_memory_native/src/service.rs
  • crates/ironclaw_reborn_cli/AGENTS.md
  • crates/ironclaw_reborn_cli/Cargo.toml
  • crates/ironclaw_reborn_cli/src/commands/doctor.rs
  • crates/ironclaw_reborn_cli/src/commands/extension.rs
  • crates/ironclaw_reborn_cli/src/commands/migrate.rs
  • crates/ironclaw_reborn_cli/src/commands/mod.rs
  • crates/ironclaw_reborn_cli/src/commands/onboard.rs
  • crates/ironclaw_reborn_cli/src/context.rs
  • crates/ironclaw_reborn_cli/tests/migration_cli.rs
  • crates/ironclaw_reborn_cli/tests/smoke.rs
  • crates/ironclaw_reborn_composition/AGENTS.md
  • crates/ironclaw_reborn_composition/CLAUDE.md
  • crates/ironclaw_reborn_composition/src/admin_user_directory.rs
  • crates/ironclaw_reborn_composition/src/factory.rs
  • crates/ironclaw_reborn_composition/src/input.rs
  • crates/ironclaw_reborn_composition/src/lib.rs
  • crates/ironclaw_reborn_composition/src/migration_support.rs
  • crates/ironclaw_reborn_identity/CONTRACT.md
  • crates/ironclaw_reborn_identity/src/filesystem_store/directory.rs
  • crates/ironclaw_reborn_identity/src/filesystem_store/tests.rs
  • crates/ironclaw_reborn_identity/src/lib.rs
  • crates/ironclaw_reborn_identity/src/user_directory.rs
  • crates/ironclaw_reborn_migration/CLAUDE.md
  • crates/ironclaw_reborn_migration/Cargo.toml
  • crates/ironclaw_reborn_migration/src/convert/automations.rs
  • crates/ironclaw_reborn_migration/src/convert/extensions.rs
  • crates/ironclaw_reborn_migration/src/convert/heartbeat.rs
  • crates/ironclaw_reborn_migration/src/convert/memory.rs
  • crates/ironclaw_reborn_migration/src/convert/mod.rs
  • crates/ironclaw_reborn_migration/src/convert/projects.rs
  • crates/ironclaw_reborn_migration/src/convert/secrets.rs
  • crates/ironclaw_reborn_migration/src/convert/settings.rs
  • crates/ironclaw_reborn_migration/src/convert/threads.rs
  • crates/ironclaw_reborn_migration/src/convert/users.rs
  • crates/ironclaw_reborn_migration/src/error.rs
  • crates/ironclaw_reborn_migration/src/inventory.rs
  • crates/ironclaw_reborn_migration/src/lib.rs
  • crates/ironclaw_reborn_migration/src/main.rs
  • crates/ironclaw_reborn_migration/src/manifest.rs
  • crates/ironclaw_reborn_migration/src/mounts.rs
  • crates/ironclaw_reborn_migration/src/options.rs
  • crates/ironclaw_reborn_migration/src/report.rs
  • crates/ironclaw_reborn_migration/src/source.rs
  • crates/ironclaw_reborn_migration/src/target.rs
  • crates/ironclaw_reborn_migration/src/target_ids.rs
  • crates/ironclaw_reborn_migration/src/v2_model.rs
  • crates/ironclaw_reborn_migration/tests/companion_cli.rs
  • crates/ironclaw_reborn_migration/tests/migration_roundtrip.rs
  • crates/ironclaw_reborn_migration/tests/migration_safety.rs
  • crates/ironclaw_reborn_migration/tests/project_migration.rs
  • crates/ironclaw_secrets/src/filesystem_store.rs
  • crates/ironclaw_secrets/src/lib.rs
  • crates/ironclaw_triggers/src/lib.rs
  • crates/ironclaw_triggers/src/libsql.rs
  • crates/ironclaw_triggers/src/postgres.rs
  • crates/ironclaw_triggers/tests/repository_contract.rs
  • docker/reborn/entrypoint.sh
  • docs/internal/2026-06-26-legacy-vs-reborn-feature-comparison.md
  • docs/plans/8513-cli-smoke-and-secrets-decomposition.md
  • docs/plans/composition-pubuse.snapshot
  • docs/reborn-binary.md
  • docs/reborn/README.md
  • docs/reborn/contracts/memory.md
  • docs/reborn/contracts/migration-compatibility.md
  • docs/reborn/contracts/secrets.md
  • docs/reborn/contracts/triggers.md
  • docs/reborn/deploy-reborn-cli-docker.md
  • docs/reborn/onboarding.md
  • docs/reborn/setup-slack-for-reborn-binary.md
  • docs/reborn/v1-migration.md
  • src/db/libsql/mod.rs
  • src/setup/README.md

Comment on lines +1201 to +1215
#[tokio::test]
async fn metadata_capability_rejects_direct_backend_metadata_reads() {
let backend = make_backend().with_capabilities(
MemoryBackendCapabilities::default()
.set_file_documents(true)
.set_metadata(false),
);
let result = backend
.read_document_metadata(&alpha_context(), &alpha_path())
.await;
assert!(
result.is_err(),
"metadata reads must fail closed when metadata is unsupported"
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Add a scope-mismatch regression for metadata reads.

The new operation also gates repository access through ensure_path_matches_context, but this test covers only capability denial. Add an alpha_context()/beta_path() test proving no cross-scope metadata read reaches the repository.

As per coding guidelines, “when a helper gates a side effect… add a caller-level test.” As per path instructions, “Test through the caller.”

Proposed regression test
+    #[tokio::test]
+    async fn read_document_metadata_rejects_path_outside_authorized_context() {
+        let backend = make_backend();
+        let result = backend
+            .read_document_metadata(&alpha_context(), &beta_path())
+            .await;
+        assert!(
+            result.is_err(),
+            "expected scope mismatch on metadata read to fail closed"
+        );
+    }
📝 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.

Suggested change
#[tokio::test]
async fn metadata_capability_rejects_direct_backend_metadata_reads() {
let backend = make_backend().with_capabilities(
MemoryBackendCapabilities::default()
.set_file_documents(true)
.set_metadata(false),
);
let result = backend
.read_document_metadata(&alpha_context(), &alpha_path())
.await;
assert!(
result.is_err(),
"metadata reads must fail closed when metadata is unsupported"
);
}
#[tokio::test]
async fn metadata_capability_rejects_direct_backend_metadata_reads() {
let backend = make_backend().with_capabilities(
MemoryBackendCapabilities::default()
.set_file_documents(true)
.set_metadata(false),
);
let result = backend
.read_document_metadata(&alpha_context(), &alpha_path())
.await;
assert!(
result.is_err(),
"metadata reads must fail closed when metadata is unsupported"
);
}
#[tokio::test]
async fn read_document_metadata_rejects_path_outside_authorized_context() {
let backend = make_backend();
let result = backend
.read_document_metadata(&alpha_context(), &beta_path())
.await;
assert!(
result.is_err(),
"expected scope mismatch on metadata read to fail closed"
);
}
🤖 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_native/src/backend.rs` around lines 1201 - 1215, Add a
caller-level async test alongside
metadata_capability_rejects_direct_backend_metadata_reads that invokes
read_document_metadata with alpha_context() and beta_path(), then asserts the
operation fails before any cross-scope repository read occurs. Keep the
capability setup enabled for metadata so the failure specifically verifies
ensure_path_matches_context scope enforcement.

Sources: Coding guidelines, Path instructions

ironclaw_reborn_traces = { path = "../ironclaw_reborn_traces", version = "0.1.0" }
ironclaw_reborn_webui_ingress = { path = "../ironclaw_reborn_webui_ingress", version = "0.1.0", optional = true }
reqwest = { version = "0.12", default-features = false, features = ["json", "multipart", "rustls-tls-native-roots", "stream"] }
libsql = { version = "0.9", optional = true, default-features = false, features = ["core", "replication", "remote", "tls"] }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP 'libsql::(Builder|OpenFlags|Database|Connection|replication|Cipher|remote|sync)' crates/ironclaw_reborn_cli/src

Repository: nearai/ironclaw

Length of output: 381


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== repo guidance files ==\n'
fd -a 'CLAUDE.md|AGENTS.md|rules' . -t f

printf '\n== Cargo.toml context ==\n'
sed -n '1,180p' crates/ironclaw_reborn_cli/Cargo.toml

printf '\n== migrate.rs libsql context ==\n'
sed -n '450,540p' crates/ironclaw_reborn_cli/src/commands/migrate.rs

printf '\n== libsql references in CLI crate ==\n'
rg -n 'libsql' crates/ironclaw_reborn_cli/src crates/ironclaw_reborn_cli/Cargo.toml

Repository: nearai/ironclaw

Length of output: 19228


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== root AGENTS ==\n'
cat -n AGENTS.md

printf '\n== crate AGENTS ==\n'
cat -n crates/ironclaw_reborn_cli/AGENTS.md

printf '\n== nearby migration docs ==\n'
fd -a 'CLAUDE.md|AGENTS.md' crates/ironclaw_reborn_migration crates/ironclaw_reborn_cli crates/ironclaw_reborn_composition

Repository: nearai/ironclaw

Length of output: 18531


🌐 Web query:

libsql 0.9 Cargo features core replication remote tls Builder::new_local OpenFlags SQLITE_OPEN_READ_ONLY docs

💡 Result:

In the libsql Rust crate (version 0.9.x), feature flags are used to manage dependencies and reduce compile times [1]. Key features include: - core: Enables the core C code required for local database operations and embedded replicas [1][2]. It is enabled by default [3]. - replication: Includes the core feature and adds HTTP-based replication logic to sync remote databases locally [1]. It is enabled by default [3]. - remote: Provides pure HTTP functionality for executing queries against a remote database [1]. It is enabled by default [3]. - tls: Enables built-in TLS support via hyper-rustls. If disabled, you must provide your own TLS connector for features requiring HTTP [1][2]. It is enabled by default [3]. The Builder struct is the recommended way to construct a Database instance [4][5]. The Builder::new_local(path) method creates a builder for a local-only database that does not perform networking or connect to remote servers [6][7]. This method requires the core feature to be enabled [6]. For database opening modes, the OpenFlags struct (provided by the core feature) allows you to specify flags such as SQLITE_OPEN_READ_ONLY, SQLITE_OPEN_READ_WRITE, and SQLITE_OPEN_CREATE [8][4][9]. While older Database::open methods are deprecated in favor of the Builder pattern, OpenFlags remains the standard mechanism for defining access modes [4][9]. Note that the default flags for a database are SQLITE_OPEN_READ_WRITE | SQLITE_OPEN_CREATE [4][9].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== root AGENTS ==\n'
cat -n AGENTS.md

printf '\n== crate AGENTS ==\n'
cat -n crates/ironclaw_reborn_cli/AGENTS.md

printf '\n== nearby guidance files ==\n'
fd -a 'CLAUDE.md|AGENTS.md' crates/ironclaw_reborn_migration crates/ironclaw_reborn_cli crates/ironclaw_reborn_composition

Repository: nearai/ironclaw

Length of output: 18531


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== runtime libsql context A ==\n'
sed -n '1228,1260p' crates/ironclaw_reborn_cli/src/runtime/mod.rs

printf '\n== runtime libsql context B ==\n'
sed -n '1908,1942p' crates/ironclaw_reborn_cli/src/runtime/mod.rs

printf '\n== runtime libsql context C ==\n'
sed -n '2230,2262p' crates/ironclaw_reborn_cli/src/runtime/mod.rs

Repository: nearai/ironclaw

Length of output: 4233


Trim the libSQL feature set crates/ironclaw_reborn_cli/AGENTS.md says to keep the CLI small and explicit. This crate only uses libsql::Builder::new_local plus OpenFlags::SQLITE_OPEN_READ_ONLY; replication, remote, and tls don’t show up on this path. Drop them unless another consumer needs the networked libSQL code paths.

🤖 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/Cargo.toml` at line 92, Update the optional libsql
dependency declaration to retain only the features required by the CLI’s local
Builder::new_local and OpenFlags::SQLITE_OPEN_READ_ONLY usage, removing
replication, remote, and tls unless another consumer in this crate demonstrably
requires them.

Comment on lines 923 to +937
pub fn open_reborn_postgres_pool(
url: secrecy::SecretString,
) -> Result<deadpool_postgres::Pool, RebornCompositionError> {
Ok(ironclaw_reborn_event_store::open_postgres_pool(url)?)
let tls_options = input::postgres_pool_tls_options_from_env().map_err(|error| {
RebornCompositionError::InvalidConfig {
reason: error.to_string(),
}
})?;
Ok(
ironclaw_reborn_event_store::open_postgres_pool_with_tls_options(
url,
ironclaw_reborn_event_store::DEFAULT_POSTGRES_POOL_MAX_SIZE,
tls_options,
)?,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n -C4 \
  'fn open_postgres_pool_with_(tls_options|max_size)|open_reborn_postgres_pool_with_max_size' \
  crates

Repository: nearai/ironclaw

Length of output: 2477


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '900,960p' crates/ironclaw_reborn_composition/src/lib.rs

printf '\n---\n'

sed -n '80,130p' crates/ironclaw_reborn_event_store/src/lib.rs

printf '\n---\n'

rg -n -C3 'postgres_pool_tls_options_from_env|open_postgres_pool_with_tls_options|open_postgres_pool_with_max_size' crates/ironclaw_reborn_composition crates/ironclaw_reborn_event_store

Repository: nearai/ironclaw

Length of output: 9002


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C3 'open_reborn_postgres_pool_with_max_size\(' crates src tests

Repository: nearai/ironclaw

Length of output: 800


Apply the env-derived TLS policy here too.

open_reborn_postgres_pool_with_max_size still calls open_postgres_pool_with_max_size, which uses Default::default() TLS options. That leaves a public production storage entrypoint on a different TLS path than open_reborn_postgres_pool. Resolve postgres_pool_tls_options_from_env() here as well and call open_postgres_pool_with_tls_options(url, max_size, tls_options).

🤖 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 923 - 937, Update
open_reborn_postgres_pool_with_max_size to resolve TLS settings through
input::postgres_pool_tls_options_from_env(), mapping failures to
RebornCompositionError::InvalidConfig like open_reborn_postgres_pool. Pass the
resolved tls_options to
ironclaw_reborn_event_store::open_postgres_pool_with_tls_options(url, max_size,
tls_options) instead of the default-TLS helper.

Comment on lines +521 to +529
let thread_project_id = mission
.project_id
.filter(|id| imported_project_ids.contains(id.to_string().as_str()));
let mut migrated_thread_ids = BTreeSet::new();
for tid in &mission.thread_history {
let Some(thread) = engine_threads.get(tid) else {
if !migrated_thread_ids.insert(*tid) {
continue;
}
let Some(indexed_thread) = engine_threads.get(tid) else {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP 'project_id' crates/ironclaw_reborn_migration/src/convert/automations.rs | rg -n 'record_loss|record_mission_field_losses|fn record_mission_field_losses'
ast-grep run --pattern 'fn record_mission_field_losses($$$) { $$$ }' --lang rust crates/ironclaw_reborn_migration/src/convert/automations.rs

Repository: nearai/ironclaw

Length of output: 1884


🏁 Script executed:

#!/bin/bash
sed -n '430,540p' crates/ironclaw_reborn_migration/src/convert/automations.rs
printf '\n---\n'
sed -n '740,780p' crates/ironclaw_reborn_migration/src/convert/automations.rs
printf '\n---\n'
rg -n 'resolve_mission_project|record_mission_field_losses|project_id|LossReason::Degraded|record_loss' crates/ironclaw_reborn_migration/src/convert/automations.rs

Repository: nearai/ironclaw

Length of output: 8734


🏁 Script executed:

#!/bin/bash
sed -n '521,575p' crates/ironclaw_reborn_migration/src/convert/automations.rs
printf '\n---\n'
sed -n '699,730p' crates/ironclaw_reborn_migration/src/convert/automations.rs
printf '\n---\n'
rg -n 'MissionThread|thread_project_id|project_id:' crates/ironclaw_reborn_migration/src/convert/automations.rs

Repository: nearai/ironclaw

Length of output: 3588


Record a loss when thread project scope is dropped

thread_project_id filters out an unimported mission.project_id without calling record_loss, so thread imports can lose project scope silently. resolve_mission_project() already treats this as a degraded migration for trigger records; mirror that here so the MigrationReport accounts for the missing scope too.

🤖 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_migration/src/convert/automations.rs` around lines 521
- 529, Update the thread migration flow around thread_project_id to detect when
mission.project_id is present but excluded because it is not in
imported_project_ids, and call record_loss for that dropped project scope.
Mirror the degraded-migration handling used by resolve_mission_project(),
ensuring MigrationReport records the loss before continuing with thread imports.

Comment on lines +47 to +56
let owner_raw = if project.user_id.is_empty() {
document.user_id.as_str()
} else {
project.user_id.as_str()
};
let Some(owner_user_id) =
report.valid_user_id(Domain::Project, &source_id, "user_id", owner_raw)
else {
continue;
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Reject conflicting project ownership instead of trusting embedded state.

When project.user_id is non-empty, it silently overrides document.user_id. A document stored under one user can therefore migrate as another user’s project. Require both IDs to match, or record an ownership loss and skip.

As per coding guidelines, preserve tenant, user, agent, and project scope on migrated records.

Proposed fix
-        let owner_raw = if project.user_id.is_empty() {
-            document.user_id.as_str()
-        } else {
-            project.user_id.as_str()
-        };
+        if !project.user_id.is_empty() && project.user_id != document.user_id {
+            report.record_loss(
+                Domain::Project,
+                source_id,
+                "user_id",
+                LossReason::Unparseable,
+                "project owner does not match the source document scope",
+            );
+            continue;
+        }
+        let owner_raw = document.user_id.as_str();
📝 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.

Suggested change
let owner_raw = if project.user_id.is_empty() {
document.user_id.as_str()
} else {
project.user_id.as_str()
};
let Some(owner_user_id) =
report.valid_user_id(Domain::Project, &source_id, "user_id", owner_raw)
else {
continue;
};
if !project.user_id.is_empty() && project.user_id != document.user_id {
report.record_loss(
Domain::Project,
source_id,
"user_id",
LossReason::Unparseable,
"project owner does not match the source document scope",
);
continue;
}
let owner_raw = document.user_id.as_str();
let Some(owner_user_id) =
report.valid_user_id(Domain::Project, &source_id, "user_id", owner_raw)
else {
continue;
};
🤖 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_migration/src/convert/projects.rs` around lines 47 -
56, Update the project ownership handling before calling report.valid_user_id in
the project conversion flow: when both project.user_id and document.user_id are
present, require them to match; on a mismatch, record the ownership loss through
the existing report mechanism and skip the project. Preserve the current
valid_user_id validation for non-conflicting ownership and ensure migrated
records retain the document’s tenant/user scope.

Source: Coding guidelines

Copy link
Copy Markdown
Member

Closing this because it still updates legacy top-level src/ setup/config paths (src/setup/*, legacy setup/config integration surface). Product work has moved to the Reborn workspace under crates/, so this PR is outdated as an implementation path despite the Reborn crate work in the branch.

I opened #6323 to track the offline v1-to-Reborn migration workflow as Reborn-native work with implementation notes.

This branch was successfully deployed

No deployments
ironclaw-ci-preview / ironclaw-pr-5936 — 9f215c20 Deployed Jul 13, 2026 by railway-app[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contributor: core 20+ merged PRs risk: high Safety, secrets, auth, or critical infrastructure scope: dependencies Dependency updates scope: docs Documentation scope: sandbox Docker sandbox scope: setup Onboarding / setup size: XL 500+ changed lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants