diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml
index d7eaf92f..c2d107f2 100644
--- a/.github/workflows/verify.yml
+++ b/.github/workflows/verify.yml
@@ -77,6 +77,24 @@ jobs:
- name: clippy
run: cargo clippy --workspace --all-targets --all-features -- -D warnings
+ - name: Trust-surface — loomweave-core must not link an HTTP client
+ # PRD-0001 / clarion-141e9c08c8: the plugin-supervisor + SEI crate must
+ # not carry an outbound HTTP client; provider HTTP lives in loomweave-llm.
+ # `--prefix none` flattens the tree so the anchor matches an indented dep
+ # (a plain `^reqwest` against the tree output never matches and would pass
+ # vacuously). `reqwest` stays legitimate in federation/cli — this is a
+ # per-crate ban cargo-deny's [bans] cannot express.
+ run: |
+ set -euo pipefail
+ # Capture first so a `cargo tree` failure exits the step (under `set -e`)
+ # instead of being swallowed by the pipe and read as "no reqwest".
+ deps="$(cargo tree -p loomweave-core --edges normal --prefix none)"
+ if grep -qE '^reqwest v' <<<"$deps"; then
+ echo "::error::loomweave-core links reqwest; the provider HTTP must stay in loomweave-llm (PRD-0001)"
+ exit 1
+ fi
+ echo "OK: loomweave-core has no reqwest in its dependency tree"
+
- name: install cargo-nextest
uses: taiki-e/install-action@e310bff3ef77234d477d6bb655da153a5c49d1db
diff --git a/Cargo.lock b/Cargo.lock
index 683c1081..766cc3db 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1081,6 +1081,7 @@ dependencies = [
"loomweave-analysis",
"loomweave-core",
"loomweave-federation",
+ "loomweave-llm",
"loomweave-mcp",
"loomweave-plugin-fixture",
"loomweave-scanner",
@@ -1108,10 +1109,7 @@ dependencies = [
name = "loomweave-core"
version = "1.3.1"
dependencies = [
- "async-trait",
- "fs2",
"nix",
- "reqwest",
"serde",
"serde_json",
"tempfile",
@@ -1136,6 +1134,22 @@ dependencies = [
"thiserror 1.0.69",
]
+[[package]]
+name = "loomweave-llm"
+version = "1.3.1"
+dependencies = [
+ "async-trait",
+ "fs2",
+ "reqwest",
+ "serde",
+ "serde_json",
+ "tempfile",
+ "thiserror 1.0.69",
+ "tokio",
+ "tracing",
+ "which",
+]
+
[[package]]
name = "loomweave-mcp"
version = "1.3.1"
@@ -1144,6 +1158,7 @@ dependencies = [
"blake3",
"loomweave-core",
"loomweave-federation",
+ "loomweave-llm",
"loomweave-storage",
"nix",
"reqwest",
diff --git a/Cargo.toml b/Cargo.toml
index b8bbd8cb..8d9c7890 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -3,6 +3,7 @@ resolver = "3"
members = [
"crates/loomweave-analysis",
"crates/loomweave-core",
+ "crates/loomweave-llm",
"crates/loomweave-federation",
"crates/loomweave-storage",
"crates/loomweave-cli",
diff --git a/crates/loomweave-cli/Cargo.toml b/crates/loomweave-cli/Cargo.toml
index a9c43976..cb3f8edd 100644
--- a/crates/loomweave-cli/Cargo.toml
+++ b/crates/loomweave-cli/Cargo.toml
@@ -20,6 +20,7 @@ axum.workspace = true
blake3.workspace = true
clap.workspace = true
loomweave-core = { path = "../loomweave-core", version = "1.3.1" }
+loomweave-llm = { path = "../loomweave-llm", version = "1.3.1" }
loomweave-analysis = { path = "../loomweave-analysis", version = "1.3.1" }
loomweave-federation = { path = "../loomweave-federation", version = "1.3.1" }
loomweave-mcp = { path = "../loomweave-mcp", version = "1.3.1" }
diff --git a/crates/loomweave-cli/src/analyze.rs b/crates/loomweave-cli/src/analyze.rs
index 48482635..520f9092 100644
--- a/crates/loomweave-cli/src/analyze.rs
+++ b/crates/loomweave-cli/src/analyze.rs
@@ -25,9 +25,10 @@ use uuid::Uuid;
use loomweave_core::plugin::host::FINDING_PLUGIN_ABORTED;
use loomweave_core::{
AcceptedEdge, AcceptedEntity, AnalyzeFileOutcome, CrashLoopBreaker, CrashLoopState,
- DiscoveredPlugin, EmbeddingProvider, FINDING_DISABLED_CRASH_LOOP, HostError, HostFinding,
- UnresolvedCallSite, discover,
+ DiscoveredPlugin, FINDING_DISABLED_CRASH_LOOP, HostError, HostFinding, UnresolvedCallSite,
+ discover,
};
+use loomweave_llm::EmbeddingProvider;
use loomweave_storage::{
DEFAULT_BATCH_SIZE, DEFAULT_CHANNEL_CAPACITY, EmbeddingKey, EmbeddingStore, GitRename,
NewEntityDescriptor, PriorIndexEntry, SeiBindingRecord, SeiDecision, SeiLineageEntry,
@@ -8061,8 +8062,8 @@ mod tests {
async fn semantic_embedding_population_skips_fresh_sidecar_rows() {
use std::sync::Arc;
- use loomweave_core::{EmbeddingProvider, EmbeddingRecording, RecordingEmbeddingProvider};
use loomweave_federation::config::SemanticSearchConfig;
+ use loomweave_llm::{EmbeddingProvider, EmbeddingRecording, RecordingEmbeddingProvider};
use loomweave_storage::{EmbeddingKey, EmbeddingStore, pragma, schema};
let project = tempfile::tempdir().unwrap();
@@ -8130,8 +8131,8 @@ mod tests {
async fn semantic_embedding_population_skips_briefing_blocked_entities() {
use std::sync::Arc;
- use loomweave_core::{EmbeddingProvider, EmbeddingRecording, RecordingEmbeddingProvider};
use loomweave_federation::config::SemanticSearchConfig;
+ use loomweave_llm::{EmbeddingProvider, EmbeddingRecording, RecordingEmbeddingProvider};
use loomweave_storage::{pragma, schema};
let project = tempfile::tempdir().unwrap();
diff --git a/crates/loomweave-cli/src/serve.rs b/crates/loomweave-cli/src/serve.rs
index 664f1fb8..87356a82 100644
--- a/crates/loomweave-cli/src/serve.rs
+++ b/crates/loomweave-cli/src/serve.rs
@@ -6,17 +6,17 @@ use std::thread;
use std::time::Duration;
use anyhow::{Context, Result, anyhow};
-use loomweave_core::{
- ApiEmbeddingProvider, ApiEmbeddingProviderConfig, ClaudeCliProvider, ClaudeCliProviderConfig,
- CodexCliProvider, CodexCliProviderConfig, EmbeddingProvider, EmbeddingProviderError,
- LlmProvider, OpenRouterProvider, OpenRouterProviderConfig, Recording, RecordingProvider,
- TrafficLoggingProvider,
-};
use loomweave_federation::config::{
LlmConfig, McpConfig, ProviderSelection, SemanticProviderKind, SemanticSearchConfig,
select_provider_with_env,
};
use loomweave_federation::filigree::FiligreeHttpClient;
+use loomweave_llm::{
+ ApiEmbeddingProvider, ApiEmbeddingProviderConfig, ClaudeCliProvider, ClaudeCliProviderConfig,
+ CodexCliProvider, CodexCliProviderConfig, EmbeddingProvider, EmbeddingProviderError,
+ LlmProvider, OpenRouterProvider, OpenRouterProviderConfig, Recording, RecordingProvider,
+ TrafficLoggingProvider,
+};
use loomweave_storage::{DEFAULT_BATCH_SIZE, DEFAULT_CHANNEL_CAPACITY, ReaderPool, Writer};
pub fn run(path: &Path, config_path: Option<&Path>) -> Result<()> {
diff --git a/crates/loomweave-cli/tests/serve.rs b/crates/loomweave-cli/tests/serve.rs
index 9d6712d1..00d165ee 100644
--- a/crates/loomweave-cli/tests/serve.rs
+++ b/crates/loomweave-cli/tests/serve.rs
@@ -10,10 +10,8 @@ use std::time::{Duration, Instant};
use assert_cmd::Command;
use hmac::{Hmac, Mac};
-use loomweave_core::{
- LEAF_SUMMARY_PROMPT_TEMPLATE_ID,
- plugin::{ContentLengthCeiling, Frame, read_frame, write_frame},
-};
+use loomweave_core::plugin::{ContentLengthCeiling, Frame, read_frame, write_frame};
+use loomweave_llm::LEAF_SUMMARY_PROMPT_TEMPLATE_ID;
use rusqlite::{Connection, params};
use serde::Deserialize;
use serde_json::Value;
diff --git a/crates/loomweave-core/Cargo.toml b/crates/loomweave-core/Cargo.toml
index 1bc5a4e8..db1cb835 100644
--- a/crates/loomweave-core/Cargo.toml
+++ b/crates/loomweave-core/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "loomweave-core"
-description = "Loomweave core: entity-ID assembler, sandboxed JSON-RPC plugin host, manifest parser, and LLM/embedding provider traits."
+description = "Loomweave core: entity-ID assembler, sandboxed JSON-RPC plugin host, and manifest parser."
version.workspace = true
edition.workspace = true
license.workspace = true
@@ -11,9 +11,6 @@ rust-version.workspace = true
workspace = true
[dependencies]
-async-trait.workspace = true
-fs2.workspace = true
-reqwest.workspace = true
serde.workspace = true
serde_json.workspace = true
tempfile.workspace = true
diff --git a/crates/loomweave-core/src/lib.rs b/crates/loomweave-core/src/lib.rs
index a3ce6582..e038906f 100644
--- a/crates/loomweave-core/src/lib.rs
+++ b/crates/loomweave-core/src/lib.rs
@@ -1,4 +1,4 @@
-//! loomweave-core — domain types, identifiers, and provider traits.
+//! loomweave-core — domain types, identifiers, and the sandboxed plugin host.
//!
//! # Re-export policy (ticket clarion-29acbcd042)
//!
@@ -6,29 +6,15 @@
//! root. Implementation types (`Frame`, `TransportError`, `RequestEnvelope`, etc.)
//! remain accessible via `loomweave_core::plugin::transport::*` and siblings.
-pub mod embedding_provider;
pub mod entity_id;
pub mod errors;
pub mod hardened_git;
-pub mod llm_provider;
pub mod plugin;
pub mod store;
-pub use embedding_provider::{
- ApiEmbeddingProvider, ApiEmbeddingProviderConfig, EmbeddingProvider, EmbeddingProviderError,
- EmbeddingRecording, RecordingEmbeddingProvider,
-};
pub use entity_id::{EntityId, EntityIdError, entity_id};
pub use errors::{HttpErrorCode, McpErrorCode};
pub use hardened_git::{hardened_git_command, list_untracked_files};
-pub use llm_provider::{
- CachingModel, ClaudeCliProvider, ClaudeCliProviderConfig, CodexCliProvider,
- CodexCliProviderConfig, INFERRED_CALLS_PROMPT_VERSION, InferredCallsPromptInput,
- LEAF_SUMMARY_PROMPT_TEMPLATE_ID, LeafSummaryPromptInput, LlmProvider, LlmProviderError,
- LlmPurpose, LlmRequest, LlmResponse, OpenRouterProvider, OpenRouterProviderConfig,
- PromptTemplate, Recording, RecordingProvider, TrafficLoggingProvider,
- build_coding_agent_provider_prompt, build_inferred_calls_prompt, build_leaf_summary_prompt,
-};
pub use plugin::{
// host (Task 6) — facade for callers that spawn/connect plugins
AcceptedEdge,
diff --git a/crates/loomweave-llm/Cargo.toml b/crates/loomweave-llm/Cargo.toml
new file mode 100644
index 00000000..988169ca
--- /dev/null
+++ b/crates/loomweave-llm/Cargo.toml
@@ -0,0 +1,23 @@
+[package]
+name = "loomweave-llm"
+description = "Loomweave LLM + embedding provider traits, concrete providers (OpenRouter / Codex CLI / Claude CLI), and the outbound HTTP/CLI transport for summaries and embeddings."
+version.workspace = true
+edition.workspace = true
+license.workspace = true
+repository.workspace = true
+rust-version.workspace = true
+
+[lints]
+workspace = true
+
+[dependencies]
+async-trait.workspace = true
+fs2.workspace = true
+reqwest.workspace = true
+serde.workspace = true
+serde_json.workspace = true
+tempfile.workspace = true
+thiserror.workspace = true
+tokio.workspace = true
+tracing.workspace = true
+which.workspace = true
diff --git a/crates/loomweave-core/src/embedding_provider.rs b/crates/loomweave-llm/src/embedding_provider.rs
similarity index 100%
rename from crates/loomweave-core/src/embedding_provider.rs
rename to crates/loomweave-llm/src/embedding_provider.rs
diff --git a/crates/loomweave-llm/src/lib.rs b/crates/loomweave-llm/src/lib.rs
new file mode 100644
index 00000000..9da0efba
--- /dev/null
+++ b/crates/loomweave-llm/src/lib.rs
@@ -0,0 +1,21 @@
+//! loomweave-llm — LLM + embedding provider traits, concrete providers, and the
+//! outbound HTTP/CLI transport for Loomweave summaries and embeddings.
+//!
+//! Extracted from `loomweave-core` (PRD-0001, clarion-141e9c08c8) so the
+//! plugin-supervisor + SEI crate does not link an outbound HTTP client.
+
+pub mod embedding_provider;
+pub mod llm_provider;
+
+pub use embedding_provider::{
+ ApiEmbeddingProvider, ApiEmbeddingProviderConfig, EmbeddingProvider, EmbeddingProviderError,
+ EmbeddingRecording, RecordingEmbeddingProvider,
+};
+pub use llm_provider::{
+ CachingModel, ClaudeCliProvider, ClaudeCliProviderConfig, CodexCliProvider,
+ CodexCliProviderConfig, INFERRED_CALLS_PROMPT_VERSION, InferredCallsPromptInput,
+ LEAF_SUMMARY_PROMPT_TEMPLATE_ID, LeafSummaryPromptInput, LlmProvider, LlmProviderError,
+ LlmPurpose, LlmRequest, LlmResponse, OpenRouterProvider, OpenRouterProviderConfig,
+ PromptTemplate, Recording, RecordingProvider, TrafficLoggingProvider,
+ build_coding_agent_provider_prompt, build_inferred_calls_prompt, build_leaf_summary_prompt,
+};
diff --git a/crates/loomweave-core/src/llm_provider.rs b/crates/loomweave-llm/src/llm_provider.rs
similarity index 100%
rename from crates/loomweave-core/src/llm_provider.rs
rename to crates/loomweave-llm/src/llm_provider.rs
diff --git a/crates/loomweave-mcp/Cargo.toml b/crates/loomweave-mcp/Cargo.toml
index 0cddcfa7..68c13c16 100644
--- a/crates/loomweave-mcp/Cargo.toml
+++ b/crates/loomweave-mcp/Cargo.toml
@@ -14,6 +14,7 @@ workspace = true
async-trait.workspace = true
blake3.workspace = true
loomweave-core = { path = "../loomweave-core", version = "1.3.1" }
+loomweave-llm = { path = "../loomweave-llm", version = "1.3.1" }
loomweave-federation = { path = "../loomweave-federation", version = "1.3.1" }
loomweave-storage = { path = "../loomweave-storage", version = "1.3.1" }
reqwest.workspace = true
diff --git a/crates/loomweave-mcp/src/lib.rs b/crates/loomweave-mcp/src/lib.rs
index d7ca86b8..caf00cd8 100644
--- a/crates/loomweave-mcp/src/lib.rs
+++ b/crates/loomweave-mcp/src/lib.rs
@@ -15,10 +15,8 @@ use std::collections::{BTreeSet, HashMap};
use std::path::{Component, Path, PathBuf};
use std::sync::{Arc, Mutex};
-use loomweave_core::{
- EdgeConfidence, EmbeddingProvider, LlmProvider, LlmProviderError, LlmRequest, LlmResponse,
- McpErrorCode,
-};
+use loomweave_core::{EdgeConfidence, McpErrorCode};
+use loomweave_llm::{EmbeddingProvider, LlmProvider, LlmProviderError, LlmRequest, LlmResponse};
use rusqlite::{Connection, OpenFlags, OptionalExtension};
use serde::ser::SerializeStruct;
use serde::{Deserialize, Serialize};
@@ -6128,7 +6126,7 @@ mod tests {
use std::sync::Arc;
use std::time::Duration;
- use loomweave_core::{CachingModel, LlmProvider, LlmProviderError, LlmRequest, LlmResponse};
+ use loomweave_llm::{CachingModel, LlmProvider, LlmProviderError, LlmRequest, LlmResponse};
use loomweave_storage::{
EntityRow, InferredEdgeCacheKey, ReaderPool, UnresolvedCallSiteRow, pragma, schema,
};
diff --git a/crates/loomweave-mcp/src/tools/status.rs b/crates/loomweave-mcp/src/tools/status.rs
index 90f2d936..b8f1bbd2 100644
--- a/crates/loomweave-mcp/src/tools/status.rs
+++ b/crates/loomweave-mcp/src/tools/status.rs
@@ -6,7 +6,8 @@
use std::collections::HashMap;
-use loomweave_core::{LeafSummaryPromptInput, McpErrorCode, build_leaf_summary_prompt};
+use loomweave_core::McpErrorCode;
+use loomweave_llm::{LeafSummaryPromptInput, build_leaf_summary_prompt};
use serde_json::{Value, json};
use loomweave_storage::{
diff --git a/crates/loomweave-mcp/src/tools/summary.rs b/crates/loomweave-mcp/src/tools/summary.rs
index 3907bcb9..f43c81f9 100644
--- a/crates/loomweave-mcp/src/tools/summary.rs
+++ b/crates/loomweave-mcp/src/tools/summary.rs
@@ -8,10 +8,11 @@ use std::collections::HashSet;
use std::path::Path;
use std::sync::Arc;
-use loomweave_core::{
- EdgeConfidence, INFERRED_CALLS_PROMPT_VERSION, InferredCallsPromptInput,
- LEAF_SUMMARY_PROMPT_TEMPLATE_ID, LeafSummaryPromptInput, LlmPurpose, LlmRequest, McpErrorCode,
- build_inferred_calls_prompt, build_leaf_summary_prompt,
+use loomweave_core::{EdgeConfidence, McpErrorCode};
+use loomweave_llm::{
+ INFERRED_CALLS_PROMPT_VERSION, InferredCallsPromptInput, LEAF_SUMMARY_PROMPT_TEMPLATE_ID,
+ LeafSummaryPromptInput, LlmPurpose, LlmRequest, build_inferred_calls_prompt,
+ build_leaf_summary_prompt,
};
use serde_json::{Value, json};
use tokio::sync::{broadcast, mpsc, oneshot};
diff --git a/crates/loomweave-mcp/tests/catalogue_tools.rs b/crates/loomweave-mcp/tests/catalogue_tools.rs
index a9aea1c2..748f2236 100644
--- a/crates/loomweave-mcp/tests/catalogue_tools.rs
+++ b/crates/loomweave-mcp/tests/catalogue_tools.rs
@@ -6,7 +6,7 @@
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
-use loomweave_core::{EmbeddingRecording, RecordingEmbeddingProvider};
+use loomweave_llm::{EmbeddingRecording, RecordingEmbeddingProvider};
use loomweave_mcp::config::SemanticSearchConfig;
use loomweave_mcp::filigree::{
EntityAssociationsResponse, FiligreeClientError, FiligreeLookup, WardlineFinding,
diff --git a/crates/loomweave-mcp/tests/storage_tools.rs b/crates/loomweave-mcp/tests/storage_tools.rs
index f9ee5f58..33d4b880 100644
--- a/crates/loomweave-mcp/tests/storage_tools.rs
+++ b/crates/loomweave-mcp/tests/storage_tools.rs
@@ -8,12 +8,6 @@ use std::{
},
};
-use loomweave_core::{
- CachingModel, INFERRED_CALLS_PROMPT_VERSION, InferredCallsPromptInput,
- LEAF_SUMMARY_PROMPT_TEMPLATE_ID, LeafSummaryPromptInput, LlmProvider, LlmProviderError,
- LlmPurpose, LlmRequest, LlmResponse, OpenRouterProvider, OpenRouterProviderConfig, Recording,
- RecordingProvider, build_inferred_calls_prompt, build_leaf_summary_prompt,
-};
use loomweave_federation::{
loomweave_port::publish_port,
loomweave_url::{
@@ -21,6 +15,12 @@ use loomweave_federation::{
SOURCE_NONE as LOOMWEAVE_SOURCE_NONE,
},
};
+use loomweave_llm::{
+ CachingModel, INFERRED_CALLS_PROMPT_VERSION, InferredCallsPromptInput,
+ LEAF_SUMMARY_PROMPT_TEMPLATE_ID, LeafSummaryPromptInput, LlmProvider, LlmProviderError,
+ LlmPurpose, LlmRequest, LlmResponse, OpenRouterProvider, OpenRouterProviderConfig, Recording,
+ RecordingProvider, build_inferred_calls_prompt, build_leaf_summary_prompt,
+};
use loomweave_mcp::{
DiagnosticsContext, LlmDiagnostics, McpToolPolicy, ServerState,
config::{FiligreeConfig, LlmConfig, LlmProviderKind},
diff --git a/site/src/pages/index.astro b/site/src/pages/index.astro
index c16b9de8..305f650e 100644
--- a/site/src/pages/index.astro
+++ b/site/src/pages/index.astro
@@ -117,7 +117,7 @@ const quickStart =
SEI is a stable coordinate, not a credential — keying a fact on it
+ grants no authority. Loomweave is deconfliction-first, not security; never treat
+ its structural graph or its identities as an access-control or compliance boundary.
+