Skip to content

feat: Add multi-provider LLM support via rig-core - #36

Merged
ilblackdragon merged 1 commit into
nearai:mainfrom
zmanian:feat/multi-provider-llm
Feb 12, 2026
Merged

ilblackdragon merged 1 commit into
nearai:mainfrom
zmanian:feat/multi-provider-llm

Conversation

@zmanian

@zmanian zmanian commented Feb 11, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • Add OpenAI, Anthropic, Ollama, and OpenAI-compatible LLM backends via rig-core v0.30 adapter, keeping NEAR AI as the default
  • New LlmBackend enum and per-provider config structs with env var resolution (LLM_BACKEND, OPENAI_API_KEY, ANTHROPIC_API_KEY, OLLAMA_BASE_URL, etc.)
  • Generic RigAdapter<M: CompletionModel> bridges rig-core to the existing Arc<dyn LlmProvider> trait with full message, tool, and response conversion
  • Session authentication is now conditional on the NearAi backend; other backends skip it entirely

Why rig-core?

Multi-provider support was the top community ask -- users want to bring their own OpenAI/Anthropic keys or run local models via Ollama without needing a NEAR AI account. We evaluated several approaches:

Option 1: Hand-roll each provider's HTTP API. Straightforward but high surface area -- each provider has its own auth, message format, tool calling convention, streaming protocol, and error semantics. Maintaining parity across 4+ providers is an ongoing burden.

Option 2: Use rig-core as an adapter layer. rig-core (v0.30, 5.9K stars, actively maintained) provides a unified CompletionModel trait across 19+ providers with correct tool calling, message conversion, and usage tracking already implemented. We write one adapter (RigAdapter) and get all providers for free.

Option 3: Other multi-provider crates (e.g. llm-chain, langchain-rust). These are either less mature, more opinionated about the agent loop (we already have our own), or pull in heavier dependency trees.

We went with Option 2 because:

  • Minimal integration surface: One generic struct (RigAdapter<M: CompletionModel>) implementing our existing LlmProvider trait. All agent, reasoning, and tool code is untouched.
  • Low dependency cost: Most of rig-core's transitive deps (reqwest, serde, tokio, thiserror, tracing, futures) already exist in our tree. The incremental addition is small.
  • Provider correctness for free: Tool calling schemas, message role mapping, and token usage extraction are already battle-tested across providers. We don't need to reverse-engineer each API's quirks.
  • Future providers at zero marginal cost: Any new provider rig-core adds (Gemini, Groq, DeepSeek, etc.) becomes available by adding a few lines to our factory function.
  • No lock-in: The adapter is ~300 lines. If rig-core ever becomes unsuitable, replacing it with direct HTTP calls is straightforward since the LlmProvider trait boundary isolates all consumers.

NEAR AI remains the default and recommended backend. The rig-core adapter is purely additive -- it doesn't change any existing NEAR AI code paths.

Test plan

  • cargo fmt -- clean
  • cargo clippy --all --benches --tests --examples --all-features -- only pre-existing warnings
  • cargo test -- 675 unit tests pass, 0 new failures
  • Manual test: LLM_BACKEND=openai OPENAI_API_KEY=sk-... cargo run
  • Manual test: LLM_BACKEND=ollama OLLAMA_MODEL=llama3 cargo run
  • Manual test: LLM_BACKEND=anthropic ANTHROPIC_API_KEY=sk-ant-... cargo run

Generated with Claude Code

Add support for OpenAI, Anthropic, Ollama, and OpenAI-compatible
endpoints alongside the existing NEAR AI backend. Users can now
bring their own API keys via environment variables (LLM_BACKEND,
OPENAI_API_KEY, ANTHROPIC_API_KEY, etc.) while NEAR AI remains
the default.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@matthiasdebernardini

Copy link
Copy Markdown

Dont use ollama, use lmstudio or vllm

ollama is slow

@ilblackdragon
ilblackdragon merged commit bb228f6 into nearai:main Feb 12, 2026
@github-actions github-actions Bot mentioned this pull request Feb 12, 2026
serrrfirat pushed a commit to serrrfirat/ironclaw that referenced this pull request Feb 16, 2026
Add support for OpenAI, Anthropic, Ollama, and OpenAI-compatible
endpoints alongside the existing NEAR AI backend. Users can now
bring their own API keys via environment variables (LLM_BACKEND,
OPENAI_API_KEY, ANTHROPIC_API_KEY, etc.) while NEAR AI remains
the default.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
jaswinder6991 pushed a commit to jaswinder6991/ironclaw that referenced this pull request Feb 26, 2026
Add support for OpenAI, Anthropic, Ollama, and OpenAI-compatible
endpoints alongside the existing NEAR AI backend. Users can now
bring their own API keys via environment variables (LLM_BACKEND,
OPENAI_API_KEY, ANTHROPIC_API_KEY, etc.) while NEAR AI remains
the default.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
bkutasi pushed a commit to bkutasi/ironclaw that referenced this pull request Mar 28, 2026
Add support for OpenAI, Anthropic, Ollama, and OpenAI-compatible
endpoints alongside the existing NEAR AI backend. Users can now
bring their own API keys via environment variables (LLM_BACKEND,
OPENAI_API_KEY, ANTHROPIC_API_KEY, etc.) while NEAR AI remains
the default.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
gagdiez added a commit to gagdiez/ironclaw that referenced this pull request Apr 9, 2026
Co-authored-by: Guillermo Alejandro Gallardo Diez <gagdiez@iR2.local>
zmanian added a commit that referenced this pull request May 5, 2026
Closes the remaining libSQL/Postgres/JSONL gaps surfaced in the latest
serrrfirat review pass on PR #3171.

libSQL production gate (#34, #36, #41):
- Reject `:memory:` for Production via the InMemory error variant.
- Case-insensitive scheme matching for `HTTP://` / `HTTPS://` /
  `LibSQL://`, so a mixed-case URL no longer falls through to
  Builder::new_local and silently creates a node-local SQLite path.
- Reject bare hostname-like values (e.g. `db.example.com`) for
  Production via a new ProductionLibsqlAmbiguousTarget error; LocalDev
  still accepts ergonomic forms like `events.db`.

libSQL backend hardening (#45, #49, #50):
- Skip `create_dir_all("")` for `events.db` in cwd.
- Enable `journal_mode=WAL` + `synchronous=NORMAL` once at build time
  for file-backed local stores so a long replay reader can no longer
  block writer commits past `busy_timeout`.
- Mirror the v1 retry pattern (`src/db/libsql/mod.rs::connect`): three
  attempts with exponential backoff so concurrent transient
  "unable to open database file" errors do not surface as durable-log
  failures.

Postgres backend hardening (#46):
- Honor `sslmode=require` for loopback configs so a TLS-only local
  Postgres / loopback proxy is accepted instead of forced to NoTls.

JSONL hardening (#38, #40, #42, #43, #44):
- Make JSONL constructors crate-private so production composition
  cannot bypass the single-node-durable acceptance gate.
- Hash path components (SHA-256 / 16-hex-char prefix + 32-byte URL-
  encoded hint) so case-distinct IDs (`Alice` vs `alice`) cannot
  collide on case-insensitive filesystems and 256-byte scope IDs no
  longer overflow the 255-byte filename limit.
- Snapshot file length before write/flush/sync; truncate back on any
  error so a partial write never leaves a torn JSON tail that wedges
  every subsequent append.
- Create directories with `0o700` and stream files with `0o600` on
  Unix so durable history is not world-readable under the typical
  `umask 022`.

SQL replay correctness (#37):
- After fetching filtered rows, run a small unfiltered COUNT over the
  scanned cursor window in both libSQL and Postgres backends; mismatch
  surfaces a missing entry row as `EventError::ReplayGap`. JSONL
  already detects this via line-by-line cursor sequencing.

Regression tests:
- libSQL/Postgres production gate (case-insensitive scheme, in-memory,
  bare hostname).
- Hashed-path-component case distinctness and length boundedness.
- Atomic JSONL append: failed serialise leaves file at pre-append
  length and the next append still advances cursor cleanly.
- Unix `0o700` JSONL root permissions.
- libpq quoted single-quote socket path is local; whitespace-around-
  `=` keyword strings classify remote correctly.
- libSQL replay surfaces a deleted entry row as `ReplayGap`.

Deferred with explicit acknowledgement:
- #39 (`ReadScope.invocation_id`) — module-doc note; needs a
  cross-crate change to `ironclaw_events` plus every replay caller.
- #48 (replay holds writer-blocking locks) — inline note at the JSONL
  read path; needs a stream-bytes-snapshot redesign coordinated with
  the durable-log contract.

Test plan
- cargo test -p ironclaw_reborn_event_store
- cargo test -p ironclaw_reborn_event_store --features "libsql postgres"
- cargo test -p ironclaw_host_runtime
- cargo test -p ironclaw_events
- cargo test -p ironclaw_architecture reborn_crate_dependency_boundaries_hold
- cargo clippy -p ironclaw_reborn_event_store --all-targets -- -D warnings
- cargo clippy -p ironclaw_reborn_event_store --features "libsql postgres" --all-targets -- -D warnings
- cargo clippy -p ironclaw_reborn_event_store --no-default-features --all-targets -- -D warnings
- cargo clippy -p ironclaw_host_runtime --all-targets -- -D warnings
- cargo fmt --all -- --check

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
serrrfirat added a commit that referenced this pull request May 6, 2026
* Add Reborn event store backends

* Address Reborn event store review feedback

* Address Reborn event store review feedback (round 2)

Postgres TLS / fail-closed (comment 3178548634): the Postgres event-store
client now uses tokio-postgres-rustls for any non-loopback URL, mirroring
src/db/tls.rs (native certs with webpki-roots fallback). Local sockets and
loopback hosts continue to use NoTls; unparseable URLs with a scheme are
treated as remote so a typo cannot silently downgrade to plaintext.

next_cursor advancement (comment 3178548646): JSONL, libSQL, and Postgres
read paths now track the highest scanned cursor independently from the last
matched cursor, and return max(last_matched, last_scanned). Without this,
a matched record followed by a filtered-out record would leave next_cursor
pinned to the matched cursor and the filtered record would be rescanned on
every subsequent replay.

JSONL bounded replay (comment 3178548670): replays now stream the JSONL
file line-by-line via BufReader, decoding only the cursor envelope until
a line crosses `after`, and stop as soon as `limit` matches are collected.
A `limit = 1` request against a multi-gigabyte stream no longer pays
full-file allocation or full-stream parse latency.

JSONL cross-process locking (comment 3178548701): JSONL appends now take
an OS-level exclusive advisory lock (std::fs::File::lock) for the entire
read-tail-cursor + write window. Two IronClaw processes pointing at the
same JSONL root will block on this lock and emit monotonically-sequenced
cursors instead of corrupting the stream with duplicates. Readers take a
shared lock to avoid observing partially-written tail lines.

New deps on the event-store crate (postgres feature only):
tokio-postgres-rustls, rustls, rustls-native-certs, webpki-roots, url.
All four were already used elsewhere in the workspace; no new external
crates are introduced. File locking uses stdlib (Rust 1.89+ stable).

Regression tests added:
- jsonl_replay_advances_next_cursor_past_trailing_filtered_records
- jsonl_concurrent_appenders_emit_monotonic_cursors_through_file_lock
- jsonl_bounded_replay_does_not_parse_the_whole_file
- postgres_store::tests::{local_postgres_urls_are_recognised,
  remote_postgres_urls_require_tls,
  unparseable_postgres_url_with_scheme_falls_closed_to_remote}

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(reborn_event_store): close TLS bypass for libpq keyword strings and http:// libsql in production

Two High-severity findings from PR #3171 review:

1. is_local_postgres_url() previously returned true for any string without
   "://", so libpq keyword form like "host=db.example.com user=..." took
   the NoTls branch and connected over plaintext. Walk the keyword list,
   look at the actual host, and only treat localhost / socket paths as
   local. Tests cover remote keyword strings, socket paths, and missing
   host=.

2. Production profile previously accepted http:// libSQL URLs and forwarded
   the auth token in cleartext. Reject http:// for RebornProfile::Production
   with a typed error before the build call. LocalDev / Test still accept
   http:// for running against local sqld instances. Tests cover both.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(reborn_event_store): use deadpool-postgres pool for transparent reconnection

Replaces the single Arc<Client> in PostgresStore with a deadpool_postgres::Pool.
The previous design built one tokio_postgres::Client at startup; if the
underlying connection task exited (idle timeout, server restart, failover,
transient network drop), all subsequent append/read operations would fail
forever because nothing rebuilt the connection.

deadpool's Manager replaces broken Clients on the next pool.get() call, so
each call site sees a live connection without per-site reconnect logic.
This mirrors v1's connection-management story (src/db/postgres.rs,
src/db/tls.rs both use deadpool-postgres) and avoids two divergent
reconnection paths in one binary.

Addresses serrrfirat's Medium-severity finding on PR #3171.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(tests): fix post-merge clippy warning

* fix(reborn_event_store): close TLS-mode + libpq-config + JSONL fsync gaps

Three follow-ups from PR #3171 round-3 review:

1. CRITICAL — remote Postgres TLS not actually enforced. Passing a rustls
   connector is not enough on its own: tokio-postgres only consults the
   connector when Config::ssl_mode is Prefer or Require, and `sslmode=disable`
   in the connection string returns a plaintext stream before TLS is
   attempted. Add `enforce_remote_ssl_mode`: reject Disable for non-local
   configs, and force Prefer (the default) up to Require so the server
   cannot decline TLS without failing the connection.

2. HIGH — local/remote detector missed valid libpq forms. Re-parsing the
   raw connection string failed on `hostaddr=10.0.0.5` (numeric-IP keyword,
   no `host=` entry), `postgresql:///db?host=db.example.com` (URL with
   empty authority + host in query), and `host=/var/run/postgresql,
   db.example.com` (mixed socket/TCP list). Replace with
   `is_local_postgres_config` that walks the parsed `Config::get_hosts()`
   and `Config::get_hostaddrs()` so all libpq normalisations land in the
   same code path.

3. MEDIUM — JSONL append fsynced file contents but not the parent
   directory entry. On POSIX the new file's directory entry must also be
   fsynced for crash durability; without it the first append can vanish
   after a power loss even though `append()` returned success. Detect
   first-create via `path.exists()` pre-open and fsync the parent dir
   after `sync_data()`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(reborn_event_store): address PR #3171 round-3 review findings

Closes the remaining libSQL/Postgres/JSONL gaps surfaced in the latest
serrrfirat review pass on PR #3171.

libSQL production gate (#34, #36, #41):
- Reject `:memory:` for Production via the InMemory error variant.
- Case-insensitive scheme matching for `HTTP://` / `HTTPS://` /
  `LibSQL://`, so a mixed-case URL no longer falls through to
  Builder::new_local and silently creates a node-local SQLite path.
- Reject bare hostname-like values (e.g. `db.example.com`) for
  Production via a new ProductionLibsqlAmbiguousTarget error; LocalDev
  still accepts ergonomic forms like `events.db`.

libSQL backend hardening (#45, #49, #50):
- Skip `create_dir_all("")` for `events.db` in cwd.
- Enable `journal_mode=WAL` + `synchronous=NORMAL` once at build time
  for file-backed local stores so a long replay reader can no longer
  block writer commits past `busy_timeout`.
- Mirror the v1 retry pattern (`src/db/libsql/mod.rs::connect`): three
  attempts with exponential backoff so concurrent transient
  "unable to open database file" errors do not surface as durable-log
  failures.

Postgres backend hardening (#46):
- Honor `sslmode=require` for loopback configs so a TLS-only local
  Postgres / loopback proxy is accepted instead of forced to NoTls.

JSONL hardening (#38, #40, #42, #43, #44):
- Make JSONL constructors crate-private so production composition
  cannot bypass the single-node-durable acceptance gate.
- Hash path components (SHA-256 / 16-hex-char prefix + 32-byte URL-
  encoded hint) so case-distinct IDs (`Alice` vs `alice`) cannot
  collide on case-insensitive filesystems and 256-byte scope IDs no
  longer overflow the 255-byte filename limit.
- Snapshot file length before write/flush/sync; truncate back on any
  error so a partial write never leaves a torn JSON tail that wedges
  every subsequent append.
- Create directories with `0o700` and stream files with `0o600` on
  Unix so durable history is not world-readable under the typical
  `umask 022`.

SQL replay correctness (#37):
- After fetching filtered rows, run a small unfiltered COUNT over the
  scanned cursor window in both libSQL and Postgres backends; mismatch
  surfaces a missing entry row as `EventError::ReplayGap`. JSONL
  already detects this via line-by-line cursor sequencing.

Regression tests:
- libSQL/Postgres production gate (case-insensitive scheme, in-memory,
  bare hostname).
- Hashed-path-component case distinctness and length boundedness.
- Atomic JSONL append: failed serialise leaves file at pre-append
  length and the next append still advances cursor cleanly.
- Unix `0o700` JSONL root permissions.
- libpq quoted single-quote socket path is local; whitespace-around-
  `=` keyword strings classify remote correctly.
- libSQL replay surfaces a deleted entry row as `ReplayGap`.

Deferred with explicit acknowledgement:
- #39 (`ReadScope.invocation_id`) — module-doc note; needs a
  cross-crate change to `ironclaw_events` plus every replay caller.
- #48 (replay holds writer-blocking locks) — inline note at the JSONL
  read path; needs a stream-bytes-snapshot redesign coordinated with
  the durable-log contract.

Test plan
- cargo test -p ironclaw_reborn_event_store
- cargo test -p ironclaw_reborn_event_store --features "libsql postgres"
- cargo test -p ironclaw_host_runtime
- cargo test -p ironclaw_events
- cargo test -p ironclaw_architecture reborn_crate_dependency_boundaries_hold
- cargo clippy -p ironclaw_reborn_event_store --all-targets -- -D warnings
- cargo clippy -p ironclaw_reborn_event_store --features "libsql postgres" --all-targets -- -D warnings
- cargo clippy -p ironclaw_reborn_event_store --no-default-features --all-targets -- -D warnings
- cargo clippy -p ironclaw_host_runtime --all-targets -- -D warnings
- cargo fmt --all -- --check

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: serrrfirat <f@nuff.tech>
theredspoon referenced this pull request in theredspoon/ironclaw Jun 21, 2026
* Add Reborn event store backends

* Address Reborn event store review feedback

* Address Reborn event store review feedback (round 2)

Postgres TLS / fail-closed (comment 3178548634): the Postgres event-store
client now uses tokio-postgres-rustls for any non-loopback URL, mirroring
src/db/tls.rs (native certs with webpki-roots fallback). Local sockets and
loopback hosts continue to use NoTls; unparseable URLs with a scheme are
treated as remote so a typo cannot silently downgrade to plaintext.

next_cursor advancement (comment 3178548646): JSONL, libSQL, and Postgres
read paths now track the highest scanned cursor independently from the last
matched cursor, and return max(last_matched, last_scanned). Without this,
a matched record followed by a filtered-out record would leave next_cursor
pinned to the matched cursor and the filtered record would be rescanned on
every subsequent replay.

JSONL bounded replay (comment 3178548670): replays now stream the JSONL
file line-by-line via BufReader, decoding only the cursor envelope until
a line crosses `after`, and stop as soon as `limit` matches are collected.
A `limit = 1` request against a multi-gigabyte stream no longer pays
full-file allocation or full-stream parse latency.

JSONL cross-process locking (comment 3178548701): JSONL appends now take
an OS-level exclusive advisory lock (std::fs::File::lock) for the entire
read-tail-cursor + write window. Two IronClaw processes pointing at the
same JSONL root will block on this lock and emit monotonically-sequenced
cursors instead of corrupting the stream with duplicates. Readers take a
shared lock to avoid observing partially-written tail lines.

New deps on the event-store crate (postgres feature only):
tokio-postgres-rustls, rustls, rustls-native-certs, webpki-roots, url.
All four were already used elsewhere in the workspace; no new external
crates are introduced. File locking uses stdlib (Rust 1.89+ stable).

Regression tests added:
- jsonl_replay_advances_next_cursor_past_trailing_filtered_records
- jsonl_concurrent_appenders_emit_monotonic_cursors_through_file_lock
- jsonl_bounded_replay_does_not_parse_the_whole_file
- postgres_store::tests::{local_postgres_urls_are_recognised,
  remote_postgres_urls_require_tls,
  unparseable_postgres_url_with_scheme_falls_closed_to_remote}

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(reborn_event_store): close TLS bypass for libpq keyword strings and http:// libsql in production

Two High-severity findings from PR nearai#3171 review:

1. is_local_postgres_url() previously returned true for any string without
   "://", so libpq keyword form like "host=db.example.com user=..." took
   the NoTls branch and connected over plaintext. Walk the keyword list,
   look at the actual host, and only treat localhost / socket paths as
   local. Tests cover remote keyword strings, socket paths, and missing
   host=.

2. Production profile previously accepted http:// libSQL URLs and forwarded
   the auth token in cleartext. Reject http:// for RebornProfile::Production
   with a typed error before the build call. LocalDev / Test still accept
   http:// for running against local sqld instances. Tests cover both.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(reborn_event_store): use deadpool-postgres pool for transparent reconnection

Replaces the single Arc<Client> in PostgresStore with a deadpool_postgres::Pool.
The previous design built one tokio_postgres::Client at startup; if the
underlying connection task exited (idle timeout, server restart, failover,
transient network drop), all subsequent append/read operations would fail
forever because nothing rebuilt the connection.

deadpool's Manager replaces broken Clients on the next pool.get() call, so
each call site sees a live connection without per-site reconnect logic.
This mirrors v1's connection-management story (src/db/postgres.rs,
src/db/tls.rs both use deadpool-postgres) and avoids two divergent
reconnection paths in one binary.

Addresses serrrfirat's Medium-severity finding on PR nearai#3171.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(tests): fix post-merge clippy warning

* fix(reborn_event_store): close TLS-mode + libpq-config + JSONL fsync gaps

Three follow-ups from PR nearai#3171 round-3 review:

1. CRITICAL — remote Postgres TLS not actually enforced. Passing a rustls
   connector is not enough on its own: tokio-postgres only consults the
   connector when Config::ssl_mode is Prefer or Require, and `sslmode=disable`
   in the connection string returns a plaintext stream before TLS is
   attempted. Add `enforce_remote_ssl_mode`: reject Disable for non-local
   configs, and force Prefer (the default) up to Require so the server
   cannot decline TLS without failing the connection.

2. HIGH — local/remote detector missed valid libpq forms. Re-parsing the
   raw connection string failed on `hostaddr=10.0.0.5` (numeric-IP keyword,
   no `host=` entry), `postgresql:///db?host=db.example.com` (URL with
   empty authority + host in query), and `host=/var/run/postgresql,
   db.example.com` (mixed socket/TCP list). Replace with
   `is_local_postgres_config` that walks the parsed `Config::get_hosts()`
   and `Config::get_hostaddrs()` so all libpq normalisations land in the
   same code path.

3. MEDIUM — JSONL append fsynced file contents but not the parent
   directory entry. On POSIX the new file's directory entry must also be
   fsynced for crash durability; without it the first append can vanish
   after a power loss even though `append()` returned success. Detect
   first-create via `path.exists()` pre-open and fsync the parent dir
   after `sync_data()`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(reborn_event_store): address PR nearai#3171 round-3 review findings

Closes the remaining libSQL/Postgres/JSONL gaps surfaced in the latest
serrrfirat review pass on PR nearai#3171.

libSQL production gate (#34, #36, #41):
- Reject `:memory:` for Production via the InMemory error variant.
- Case-insensitive scheme matching for `HTTP://` / `HTTPS://` /
  `LibSQL://`, so a mixed-case URL no longer falls through to
  Builder::new_local and silently creates a node-local SQLite path.
- Reject bare hostname-like values (e.g. `db.example.com`) for
  Production via a new ProductionLibsqlAmbiguousTarget error; LocalDev
  still accepts ergonomic forms like `events.db`.

libSQL backend hardening (#45, #49, #50):
- Skip `create_dir_all("")` for `events.db` in cwd.
- Enable `journal_mode=WAL` + `synchronous=NORMAL` once at build time
  for file-backed local stores so a long replay reader can no longer
  block writer commits past `busy_timeout`.
- Mirror the v1 retry pattern (`src/db/libsql/mod.rs::connect`): three
  attempts with exponential backoff so concurrent transient
  "unable to open database file" errors do not surface as durable-log
  failures.

Postgres backend hardening (#46):
- Honor `sslmode=require` for loopback configs so a TLS-only local
  Postgres / loopback proxy is accepted instead of forced to NoTls.

JSONL hardening (#38, #40, #42, #43, #44):
- Make JSONL constructors crate-private so production composition
  cannot bypass the single-node-durable acceptance gate.
- Hash path components (SHA-256 / 16-hex-char prefix + 32-byte URL-
  encoded hint) so case-distinct IDs (`Alice` vs `alice`) cannot
  collide on case-insensitive filesystems and 256-byte scope IDs no
  longer overflow the 255-byte filename limit.
- Snapshot file length before write/flush/sync; truncate back on any
  error so a partial write never leaves a torn JSON tail that wedges
  every subsequent append.
- Create directories with `0o700` and stream files with `0o600` on
  Unix so durable history is not world-readable under the typical
  `umask 022`.

SQL replay correctness (#37):
- After fetching filtered rows, run a small unfiltered COUNT over the
  scanned cursor window in both libSQL and Postgres backends; mismatch
  surfaces a missing entry row as `EventError::ReplayGap`. JSONL
  already detects this via line-by-line cursor sequencing.

Regression tests:
- libSQL/Postgres production gate (case-insensitive scheme, in-memory,
  bare hostname).
- Hashed-path-component case distinctness and length boundedness.
- Atomic JSONL append: failed serialise leaves file at pre-append
  length and the next append still advances cursor cleanly.
- Unix `0o700` JSONL root permissions.
- libpq quoted single-quote socket path is local; whitespace-around-
  `=` keyword strings classify remote correctly.
- libSQL replay surfaces a deleted entry row as `ReplayGap`.

Deferred with explicit acknowledgement:
- #39 (`ReadScope.invocation_id`) — module-doc note; needs a
  cross-crate change to `ironclaw_events` plus every replay caller.
- #48 (replay holds writer-blocking locks) — inline note at the JSONL
  read path; needs a stream-bytes-snapshot redesign coordinated with
  the durable-log contract.

Test plan
- cargo test -p ironclaw_reborn_event_store
- cargo test -p ironclaw_reborn_event_store --features "libsql postgres"
- cargo test -p ironclaw_host_runtime
- cargo test -p ironclaw_events
- cargo test -p ironclaw_architecture reborn_crate_dependency_boundaries_hold
- cargo clippy -p ironclaw_reborn_event_store --all-targets -- -D warnings
- cargo clippy -p ironclaw_reborn_event_store --features "libsql postgres" --all-targets -- -D warnings
- cargo clippy -p ironclaw_reborn_event_store --no-default-features --all-targets -- -D warnings
- cargo clippy -p ironclaw_host_runtime --all-targets -- -D warnings
- cargo fmt --all -- --check

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: serrrfirat <f@nuff.tech>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants