From cd31bffab1247ef7e03865461f0952a774f9d2b3 Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Tue, 19 May 2026 02:13:42 +1000 Subject: [PATCH 001/187] feat(wp5): add pre-ingest secret scanner --- .github/workflows/ci.yml | 3 + Cargo.lock | 36 + Cargo.toml | 3 + crates/clarion-cli/Cargo.toml | 1 + crates/clarion-cli/src/analyze.rs | 89 +- crates/clarion-cli/src/cli.rs | 9 + crates/clarion-cli/src/main.rs | 32 +- crates/clarion-cli/src/secret_scan.rs | 224 ++++ .../clarion-cli/src/secret_scan/findings.rs | 123 ++ crates/clarion-cli/tests/secret_scan.rs | 440 +++++++ crates/clarion-core/src/plugin/host.rs | 25 +- crates/clarion-mcp/src/lib.rs | 23 + crates/clarion-mcp/tests/storage_tools.rs | 56 + crates/clarion-scanner/Cargo.toml | 20 + crates/clarion-scanner/src/baseline.rs | 225 ++++ crates/clarion-scanner/src/entropy.rs | 60 + crates/clarion-scanner/src/lib.rs | 92 ++ crates/clarion-scanner/src/patterns.rs | 300 +++++ crates/clarion-scanner/tests/scanner.rs | 232 ++++ docs/clarion/v0.1/detailed-design.md | 15 + .../v0.1-publish/ws-a-secret-scanner.md | 1023 +++++++++++++++++ docs/operator/README.md | 2 + docs/operator/secret-scanning.md | 74 ++ tests/e2e/wp5_secret_scan.sh | 110 ++ 24 files changed, 3188 insertions(+), 29 deletions(-) create mode 100644 crates/clarion-cli/src/secret_scan.rs create mode 100644 crates/clarion-cli/src/secret_scan/findings.rs create mode 100644 crates/clarion-cli/tests/secret_scan.rs create mode 100644 crates/clarion-scanner/Cargo.toml create mode 100644 crates/clarion-scanner/src/baseline.rs create mode 100644 crates/clarion-scanner/src/entropy.rs create mode 100644 crates/clarion-scanner/src/lib.rs create mode 100644 crates/clarion-scanner/src/patterns.rs create mode 100644 crates/clarion-scanner/tests/scanner.rs create mode 100644 docs/implementation/v0.1-publish/ws-a-secret-scanner.md create mode 100644 docs/operator/secret-scanning.md create mode 100755 tests/e2e/wp5_secret_scan.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e4cd32b3..54ba837a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -128,3 +128,6 @@ jobs: - name: run walking skeleton run: bash tests/e2e/sprint_1_walking_skeleton.sh + + - name: run WP5 secret scanner smoke + run: CARGO_BUILD=0 bash tests/e2e/wp5_secret_scan.sh diff --git a/Cargo.lock b/Cargo.lock index 2a433f0e..4eb52b52 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -255,6 +255,7 @@ dependencies = [ "clarion-core", "clarion-mcp", "clarion-plugin-fixture", + "clarion-scanner", "clarion-storage", "dotenvy", "ignore", @@ -313,6 +314,18 @@ dependencies = [ "serde_json", ] +[[package]] +name = "clarion-scanner" +version = "0.1.0-dev" +dependencies = [ + "regex", + "serde", + "serde_norway", + "sha1", + "tempfile", + "thiserror 1.0.69", +] + [[package]] name = "clarion-storage" version = "0.1.0-dev" @@ -1312,6 +1325,18 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + [[package]] name = "regex-automata" version = "0.4.14" @@ -1599,6 +1624,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + [[package]] name = "sha2" version = "0.10.9" diff --git a/Cargo.toml b/Cargo.toml index 4e6faf8e..94afb393 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "crates/clarion-cli", "crates/clarion-mcp", "crates/clarion-plugin-fixture", + "crates/clarion-scanner", ] [workspace.package] @@ -38,9 +39,11 @@ dotenvy = "0.15" ignore = "0.4" rusqlite = { version = "0.31", features = ["bundled"] } reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls-native-roots"] } +regex = "1" serde = { version = "1", features = ["derive"] } serde_json = "1" serde_norway = "0.9.42" +sha1 = "0.10" sha2 = "0.10" thiserror = "1" time = { version = "0.3", features = ["formatting", "macros", "parsing"] } diff --git a/crates/clarion-cli/Cargo.toml b/crates/clarion-cli/Cargo.toml index 2bd97d1c..4d6ebe70 100644 --- a/crates/clarion-cli/Cargo.toml +++ b/crates/clarion-cli/Cargo.toml @@ -19,6 +19,7 @@ blake3.workspace = true clap.workspace = true clarion-core = { path = "../clarion-core", version = "0.1.0-dev" } clarion-mcp = { path = "../clarion-mcp", version = "0.1.0-dev" } +clarion-scanner = { path = "../clarion-scanner", version = "0.1.0-dev" } clarion-storage = { path = "../clarion-storage", version = "0.1.0-dev" } dotenvy.workspace = true ignore.workspace = true diff --git a/crates/clarion-cli/src/analyze.rs b/crates/clarion-cli/src/analyze.rs index 21d44329..5ad5fa5b 100644 --- a/crates/clarion-cli/src/analyze.rs +++ b/crates/clarion-cli/src/analyze.rs @@ -41,6 +41,7 @@ const WEAK_MODULARITY_RULE_ID: &str = "CLA-FACT-CLUSTERING-WEAK-MODULARITY"; #[derive(Debug, Clone, Default)] pub(crate) struct AnalyzeOptions { pub(crate) config_path: Option, + pub(crate) secret_scan: crate::secret_scan::SecretScanOptions, } /// Run the analyze command against `project_path`. @@ -93,17 +94,6 @@ pub(crate) async fn run_with_options(project_path: PathBuf, options: AnalyzeOpti let run_id = Uuid::new_v4().to_string(); let started_at = iso8601_now(); - writer - .send_wait(|ack| WriterCmd::BeginRun { - run_id: run_id.clone(), - config_json: analyze_config_json.clone(), - started_at: started_at.clone(), - ack, - }) - .await - .map_err(|e| anyhow::anyhow!("{e}")) - .context("BeginRun")?; - // ── Discover plugins ────────────────────────────────────────────────────── let discovery_results = discover(); let mut plugins: Vec = Vec::new(); @@ -138,6 +128,7 @@ pub(crate) async fn run_with_options(project_path: PathBuf, options: AnalyzeOpti discovery_errors.join("; ") ); tracing::error!(run_id = %run_id, reason = %reason, "failing run: discovery errors"); + begin_run(&writer, &run_id, &analyze_config_json, &started_at).await?; let completed_at = iso8601_now(); writer .send_wait(|ack| WriterCmd::FailRun { @@ -164,6 +155,7 @@ pub(crate) async fn run_with_options(project_path: PathBuf, options: AnalyzeOpti } tracing::warn!(run_id = %run_id, "no plugins discovered"); + begin_run(&writer, &run_id, &analyze_config_json, &started_at).await?; let completed_at = iso8601_now(); writer .send_wait(|ack| WriterCmd::CommitRun { @@ -215,6 +207,10 @@ pub(crate) async fn run_with_options(project_path: PathBuf, options: AnalyzeOpti let source_files = collect_source_files(&project_root, &wanted_extensions); tracing::info!(file_count = source_files.len(), "source tree walk complete"); + let secret_scan_outcome = + crate::secret_scan::pre_ingest(&project_root, &source_files, &options.secret_scan)?; + begin_run(&writer, &run_id, &analyze_config_json, &started_at).await?; + // ── Per-plugin processing ───────────────────────────────────────────────── // // A per-plugin crash (spawn / handshake / analyze_file Err) does NOT tank @@ -242,6 +238,7 @@ pub(crate) async fn run_with_options(project_path: PathBuf, options: AnalyzeOpti let mut run_outcome: RunOutcome = RunOutcome::Completed; let mut breaker = CrashLoopBreaker::default(); let mut crash_reasons: Vec = Vec::new(); + let mut secret_finding_anchors: BTreeMap = BTreeMap::new(); 'plugins: for plugin in plugins { let plugin_id = plugin.manifest.plugin.plugin_id.clone(); @@ -282,6 +279,7 @@ pub(crate) async fn run_with_options(project_path: PathBuf, options: AnalyzeOpti let pid_clone = plugin_id.clone(); let exec_clone = plugin.executable.clone(); let files_clone = plugin_files.clone(); + let briefing_blocks_clone = secret_scan_outcome.briefing_blocks.clone(); // A JoinError here means the blocking task panicked (OOM, stack // overflow, internal unwrap, abort — anything that unwinds past the @@ -299,6 +297,7 @@ pub(crate) async fn run_with_options(project_path: PathBuf, options: AnalyzeOpti &pid_clone, &exec_clone, &files_clone, + &briefing_blocks_clone, ) }) .await, @@ -361,6 +360,7 @@ pub(crate) async fn run_with_options(project_path: PathBuf, options: AnalyzeOpti // edge FK references resolve at insert time (B.3 §5). let entity_count = entities.len() as u64; let edge_count = edges.len() as u64; + remember_secret_finding_anchors(&entities, &mut secret_finding_anchors); let mut insert_err: Option = None; for (id_str, record) in entities { let res = writer @@ -435,6 +435,22 @@ pub(crate) async fn run_with_options(project_path: PathBuf, options: AnalyzeOpti } } + if !matches!(run_outcome, RunOutcome::HardFailed { .. }) + && let Err(e) = crate::secret_scan::emit_findings( + &writer, + &run_id, + &started_at, + &secret_scan_outcome, + &secret_finding_anchors, + ) + .await + { + tracing::error!(run_id = %run_id, error = %e, "secret finding persistence failed"); + run_outcome = RunOutcome::HardFailed { + reason: format!("secret finding persistence failed: {e:#}"), + }; + } + // ── Commit or fail the run ──────────────────────────────────────────────── // // Writer-actor failures set `run_outcome = HardFailed` above (and break). @@ -497,7 +513,7 @@ pub(crate) async fn run_with_options(project_path: PathBuf, options: AnalyzeOpti match run_outcome { RunOutcome::Completed => { - let stats_json = serde_json::json!({ + let mut stats_json = serde_json::json!({ "entities_inserted": total_entity_count, "edges_inserted": total_edge_count, "dropped_edges_total": dropped_edges_total, @@ -513,8 +529,9 @@ pub(crate) async fn run_with_options(project_path: PathBuf, options: AnalyzeOpti "pyright_index_parse_latency_p95_ms": pyright_index_parse_latency_p95_ms, "extractor_parse_latency_p95_ms": extractor_parse_latency_p95_ms, "clustering": phase3_output.clustering_stats.clone(), - }) - .to_string(); + }); + secret_scan_outcome.augment_stats(&mut stats_json); + let stats_json = stats_json.to_string(); writer .send_wait(|ack| WriterCmd::CommitRun { run_id: run_id.clone(), @@ -532,7 +549,7 @@ pub(crate) async fn run_with_options(project_path: PathBuf, options: AnalyzeOpti // failed, atomically (writer folds the UPDATE into the open tx). // The stats JSON carries both fields so operators can see what // was persisted alongside the failure reason. - let stats_json = serde_json::json!({ + let mut stats_json = serde_json::json!({ "entities_inserted": total_entity_count, "edges_inserted": total_edge_count, "dropped_edges_total": dropped_edges_total, @@ -549,8 +566,9 @@ pub(crate) async fn run_with_options(project_path: PathBuf, options: AnalyzeOpti "extractor_parse_latency_p95_ms": extractor_parse_latency_p95_ms, "clustering": phase3_output.clustering_stats.clone(), "failure_reason": reason, - }) - .to_string(); + }); + secret_scan_outcome.augment_stats(&mut stats_json); + let stats_json = stats_json.to_string(); writer .send_wait(|ack| WriterCmd::CommitRun { run_id: run_id.clone(), @@ -597,6 +615,24 @@ pub(crate) async fn run_with_options(project_path: PathBuf, options: AnalyzeOpti Ok(()) } +async fn begin_run( + writer: &Writer, + run_id: &str, + analyze_config_json: &str, + started_at: &str, +) -> Result<()> { + writer + .send_wait(|ack| WriterCmd::BeginRun { + run_id: run_id.to_owned(), + config_json: analyze_config_json.to_owned(), + started_at: started_at.to_owned(), + ack, + }) + .await + .map_err(|e| anyhow::anyhow!("{e}")) + .context("BeginRun") +} + // ── Phase 3 subsystem materialisation ───────────────────────────────────────── #[derive(Debug, Clone)] @@ -1170,6 +1206,7 @@ fn run_plugin_blocking( plugin_id: &str, executable: &Path, files: &[PathBuf], + briefing_blocks: &BTreeMap, ) -> Result { use clarion_core::PluginHost; @@ -1185,6 +1222,7 @@ fn run_plugin_blocking( PluginRunError::new(format!("plugin {plugin_id} spawn/handshake error: {other}")) } })?; + host.set_briefing_blocks(briefing_blocks.clone()); let work_result: Result = (|| { let mut collected_entities: Vec<(String, EntityRecord)> = Vec::new(); @@ -1496,6 +1534,23 @@ fn absolute_from_import_submodule_target( .then_some(candidate) } +fn remember_secret_finding_anchors( + entities: &[(String, EntityRecord)], + anchors: &mut BTreeMap, +) { + for (id, record) in entities { + let Some(path) = record.source_file_path.as_deref() else { + continue; + }; + let key = Path::new(path) + .canonicalize() + .unwrap_or_else(|_| PathBuf::from(path)); + if record.kind == "module" || !anchors.contains_key(&key) { + anchors.insert(key, id.clone()); + } + } +} + /// Map an `AcceptedEntity` to an `EntityRecord` for the writer-actor. fn map_entity_to_record( entity: &AcceptedEntity, diff --git a/crates/clarion-cli/src/cli.rs b/crates/clarion-cli/src/cli.rs index 859631e8..34070d49 100644 --- a/crates/clarion-cli/src/cli.rs +++ b/crates/clarion-cli/src/cli.rs @@ -32,6 +32,15 @@ pub enum Command { /// Path to clarion.yaml (default: project-root/clarion.yaml if present). #[arg(long)] config: Option, + + /// Allow analysis of files containing unredacted secrets. Requires a + /// confirmation step when detections are present. + #[arg(long)] + allow_unredacted_secrets: bool, + + /// Non-TTY confirmation token for --allow-unredacted-secrets. + #[arg(long, value_name = "TOKEN", requires = "allow_unredacted_secrets")] + confirm_allow_unredacted_secrets: Option, }, /// Run the MCP stdio server. diff --git a/crates/clarion-cli/src/main.rs b/crates/clarion-cli/src/main.rs index 048d96c5..9b3c1a5a 100644 --- a/crates/clarion-cli/src/main.rs +++ b/crates/clarion-cli/src/main.rs @@ -3,6 +3,7 @@ mod cli; mod clustering; mod config; mod install; +mod secret_scan; mod serve; mod stats; @@ -20,20 +21,31 @@ fn main() -> Result<()> { let cli = cli::Cli::parse(); match cli.command { cli::Command::Install { force, path } => install::run(&path, force), - cli::Command::Analyze { path, config } => { + cli::Command::Analyze { + path, + config, + allow_unredacted_secrets, + confirm_allow_unredacted_secrets, + } => { let rt = tokio::runtime::Builder::new_multi_thread() .enable_all() .build()?; - if let Some(config_path) = config { - rt.block_on(analyze::run_with_options( - path, - analyze::AnalyzeOptions { - config_path: Some(config_path), - }, - )) - } else { - rt.block_on(analyze::run(path)) + if config.is_none() + && !allow_unredacted_secrets + && confirm_allow_unredacted_secrets.is_none() + { + return rt.block_on(analyze::run(path)); } + rt.block_on(analyze::run_with_options( + path, + analyze::AnalyzeOptions { + config_path: config, + secret_scan: secret_scan::SecretScanOptions { + allow_unredacted_secrets, + confirm_allow_unredacted_secrets, + }, + }, + )) } cli::Command::Serve { path, config } => serve::run(&path, config.as_deref()), } diff --git a/crates/clarion-cli/src/secret_scan.rs b/crates/clarion-cli/src/secret_scan.rs new file mode 100644 index 00000000..0e0e08cb --- /dev/null +++ b/crates/clarion-cli/src/secret_scan.rs @@ -0,0 +1,224 @@ +//! Pre-ingest secret scanning for `clarion analyze`. +//! +//! Exit codes used by this module: +//! - 0: analysis may continue, with or without an explicit secret override. +//! - 1: ordinary hard failure reported through the caller's `anyhow::Result`. +//! - 78 (`EX_CONFIG`): `--allow-unredacted-secrets` was misconfigured or not +//! confirmed; the caller must abort before `BeginRun`. + +use std::{ + collections::{BTreeMap, BTreeSet}, + fs, + io::{self, IsTerminal, Write}, + path::{Path, PathBuf}, +}; + +mod findings; + +use anyhow::{Context, Result}; +use clarion_scanner::{Baseline, BaselineError, Detection, Scanner, SuppressionResult}; +pub(crate) use findings::emit_findings; +use findings::{PendingFinding, secret_detected_finding}; +use serde_json::json; + +const SECRET_OVERRIDE_ALLOWED: &str = "CLA-SEC-UNREDACTED-SECRETS-ALLOWED"; +const BASELINE_NO_JUSTIFICATION: &str = "CLA-INFRA-SECRET-BASELINE-NO-JUSTIFICATION"; +const BASELINE_MATCH: &str = "CLA-INFRA-SECRET-BASELINE-MATCH"; +const OVERRIDE_UNCONFIRMED: &str = "CLA-INFRA-SECRET-OVERRIDE-UNCONFIRMED"; +const CONFIRM_TOKEN: &str = "yes-i-understand"; + +#[derive(Debug, Clone, Default)] +pub(crate) struct SecretScanOptions { + pub(crate) allow_unredacted_secrets: bool, + pub(crate) confirm_allow_unredacted_secrets: Option, +} + +#[derive(Debug, Clone, Default)] +pub(crate) struct SecretScanOutcome { + pub(crate) briefing_blocks: BTreeMap, + findings: Vec, + override_files: Vec, +} + +impl SecretScanOutcome { + pub(crate) fn augment_stats(&self, stats: &mut serde_json::Value) { + if self.override_files.is_empty() { + return; + } + stats["secret_override_used"] = json!(true); + stats["secret_override_files_affected"] = json!( + self.override_files + .iter() + .map(|path| path.display().to_string()) + .collect::>() + ); + } +} + +pub(crate) fn pre_ingest( + project_root: &Path, + source_files: &[PathBuf], + options: &SecretScanOptions, +) -> Result { + let scanner = Scanner::new(); + let (baseline, mut findings) = load_baseline_for_scan(project_root)?; + let mut per_file = Vec::new(); + let mut all_allowed = Vec::new(); + let mut baseline_matches = Vec::new(); + + for file in source_files { + let buf = fs::read(file).with_context(|| format!("read {}", file.display()))?; + let detections = scanner.scan_bytes(&buf); + let SuppressionResult { + allowed, + fired_entries, + .. + } = baseline.suppress(detections, file); + if !allowed.is_empty() { + all_allowed.extend( + allowed + .iter() + .cloned() + .map(|detection| (canonical_or_original(file), detection)), + ); + } + baseline_matches.extend(fired_entries.into_iter().map(|entry| PendingFinding { + file_path: entry.file_path.clone(), + rule_id: BASELINE_MATCH, + kind: "fact", + severity: "INFO", + confidence: Some(1.0), + confidence_basis: Some("baseline"), + message: format!( + "Secret baseline entry matched {}:{}", + entry.file_path.display(), + entry.entry.line_number + ), + evidence: json!({ + "file_path": entry.file_path, + "line_number": entry.entry.line_number, + "rule": entry.entry.rule_type, + }), + })); + per_file.push((canonical_or_original(file), allowed)); + } + + let override_confirmed = confirm_override_if_needed(&all_allowed, options); + let mut briefing_blocks = BTreeMap::new(); + let mut override_files = BTreeSet::new(); + findings.extend(baseline_matches); + + for (file, allowed) in per_file { + if allowed.is_empty() { + continue; + } + if override_confirmed { + override_files.insert(file); + } else { + briefing_blocks.insert(file.clone(), "secret_present".to_owned()); + findings.extend( + allowed + .iter() + .map(|detection| secret_detected_finding(&file, detection)), + ); + } + } + + for file in &override_files { + findings.push(PendingFinding { + file_path: file.clone(), + rule_id: SECRET_OVERRIDE_ALLOWED, + kind: "defect", + severity: "ERROR", + confidence: Some(1.0), + confidence_basis: Some("operator_override"), + message: format!( + "Operator allowed unredacted secrets in {}", + display_relative(project_root, file) + ), + evidence: json!({ + "file_path": display_relative(project_root, file), + "override_used": true, + }), + }); + } + + Ok(SecretScanOutcome { + briefing_blocks, + findings, + override_files: override_files.into_iter().collect(), + }) +} + +fn load_baseline_for_scan(project_root: &Path) -> Result<(Baseline, Vec)> { + let path = project_root.join(".clarion/secrets-baseline.yaml"); + match clarion_scanner::load_baseline(&path) { + Ok(baseline) => Ok((baseline, Vec::new())), + Err(BaselineError::MissingJustification { file, line }) => Ok(( + Baseline::empty(), + vec![PendingFinding { + file_path: project_root.join(file.clone()), + rule_id: BASELINE_NO_JUSTIFICATION, + kind: "defect", + severity: "ERROR", + confidence: Some(1.0), + confidence_basis: Some("baseline_schema"), + message: format!( + "Secret baseline entry missing justification at {}:{}", + file.display(), + line + ), + evidence: json!({"file_path": file, "line_number": line}), + }], + )), + Err(err) => Err(err).context("load secret baseline"), + } +} + +fn confirm_override_if_needed( + allowed: &[(PathBuf, Detection)], + options: &SecretScanOptions, +) -> bool { + if allowed.is_empty() || !options.allow_unredacted_secrets { + return false; + } + if options.confirm_allow_unredacted_secrets.as_deref() == Some(CONFIRM_TOKEN) { + return true; + } + if options.confirm_allow_unredacted_secrets.is_some() { + abort_unconfirmed_override(); + } + if io::stdin().is_terminal() { + for (file, detection) in allowed { + eprintln!( + "{}:{} {}", + file.display(), + detection.line_number, + detection.rule_id + ); + } + eprint!("Type '{CONFIRM_TOKEN}' to proceed: "); + let _ = io::stderr().flush(); + let mut input = String::new(); + if io::stdin().read_line(&mut input).is_ok() && input.trim() == CONFIRM_TOKEN { + return true; + } + } + abort_unconfirmed_override(); +} + +fn abort_unconfirmed_override() -> ! { + eprintln!("{OVERRIDE_UNCONFIRMED}: --allow-unredacted-secrets requires confirmation"); + std::process::exit(78); +} + +fn canonical_or_original(path: &Path) -> PathBuf { + path.canonicalize().unwrap_or_else(|_| path.to_path_buf()) +} + +fn display_relative(root: &Path, path: &Path) -> String { + path.strip_prefix(root) + .unwrap_or(path) + .display() + .to_string() +} diff --git a/crates/clarion-cli/src/secret_scan/findings.rs b/crates/clarion-cli/src/secret_scan/findings.rs new file mode 100644 index 00000000..733fabf9 --- /dev/null +++ b/crates/clarion-cli/src/secret_scan/findings.rs @@ -0,0 +1,123 @@ +use std::{ + collections::BTreeMap, + path::{Path, PathBuf}, +}; + +use anyhow::{Context, Result}; +use clarion_scanner::Detection; +use clarion_storage::{ + Writer, + commands::{FindingRecord, WriterCmd}, +}; +use serde_json::json; +use uuid::Uuid; + +use super::SecretScanOutcome; + +const SECRET_DETECTED: &str = "CLA-SEC-SECRET-DETECTED"; + +#[derive(Debug, Clone)] +pub(super) struct PendingFinding { + pub(super) file_path: PathBuf, + pub(super) rule_id: &'static str, + pub(super) kind: &'static str, + pub(super) severity: &'static str, + pub(super) confidence: Option, + pub(super) confidence_basis: Option<&'static str>, + pub(super) message: String, + pub(super) evidence: serde_json::Value, +} + +pub(crate) async fn emit_findings( + writer: &Writer, + run_id: &str, + started_at: &str, + outcome: &SecretScanOutcome, + entity_anchors: &BTreeMap, +) -> Result<()> { + for pending in &outcome.findings { + let entity_id = + finding_entity_id(&pending.file_path, entity_anchors).with_context(|| { + format!("anchor secret finding for {}", pending.file_path.display()) + })?; + let finding_id = Uuid::new_v4().to_string(); + writer + .send_wait(|ack| WriterCmd::InsertFinding { + finding: Box::new(FindingRecord { + id: finding_id.clone(), + tool: "clarion".to_owned(), + tool_version: env!("CARGO_PKG_VERSION").to_owned(), + run_id: run_id.to_owned(), + rule_id: pending.rule_id.to_owned(), + kind: pending.kind.to_owned(), + severity: pending.severity.to_owned(), + confidence: pending.confidence, + confidence_basis: pending.confidence_basis.map(str::to_owned), + entity_id, + related_entities_json: "[]".to_owned(), + message: pending.message.clone(), + evidence_json: pending.evidence.to_string(), + properties_json: "{}".to_owned(), + supports_json: "[]".to_owned(), + supported_by_json: "[]".to_owned(), + created_at: started_at.to_owned(), + updated_at: started_at.to_owned(), + }), + ack, + }) + .await + .map_err(|e| anyhow::anyhow!("{e}")) + .with_context(|| format!("InsertFinding {finding_id}"))?; + } + Ok(()) +} + +pub(super) fn secret_detected_finding(file: &Path, detection: &Detection) -> PendingFinding { + PendingFinding { + file_path: file.to_path_buf(), + rule_id: SECRET_DETECTED, + kind: "defect", + severity: "ERROR", + confidence: if detection.rule_id.starts_with("HighEntropy") { + Some(0.6) + } else { + Some(1.0) + }, + confidence_basis: if detection.rule_id.starts_with("HighEntropy") { + Some("entropy") + } else { + Some("pattern") + }, + message: format!( + "{} detected in {}:{}", + detection.rule_id, + file.display(), + detection.line_number + ), + evidence: json!({ + "file_path": file, + "line_number": detection.line_number, + "rule": detection.rule_id, + "hashed_secret_hex": hex20(detection.hashed_secret), + }), + } +} + +fn finding_entity_id(file_path: &Path, anchors: &BTreeMap) -> Option { + anchors.get(file_path).cloned().or_else(|| { + anchors + .iter() + .find(|(candidate, _)| candidate.ends_with(file_path)) + .map(|(_, entity_id)| entity_id.clone()) + }) +} + +fn hex20(bytes: [u8; 20]) -> String { + const HEX: &[u8; 16] = b"0123456789abcdef"; + let mut out = String::with_capacity(40); + for byte in bytes { + out.push(char::from(HEX[usize::from(byte >> 4)])); + out.push(char::from(HEX[usize::from(byte & 0x0f)])); + } + out +} diff --git a/crates/clarion-cli/tests/secret_scan.rs b/crates/clarion-cli/tests/secret_scan.rs new file mode 100644 index 00000000..0399b758 --- /dev/null +++ b/crates/clarion-cli/tests/secret_scan.rs @@ -0,0 +1,440 @@ +#![cfg(unix)] + +use assert_cmd::Command; +use rusqlite::Connection; + +fn clarion_bin() -> Command { + Command::cargo_bin("clarion").expect("clarion binary") +} + +const PLUGIN_SCRIPT: &str = r#"#!/usr/bin/python3 +import json +import pathlib +import re +import sys + + +def read_frame(): + headers = {} + while True: + line = sys.stdin.buffer.readline() + if line in (b"", b"\r\n"): + break + name, value = line.decode("ascii").strip().split(":", 1) + headers[name.lower()] = value.strip() + length = int(headers["content-length"]) + return json.loads(sys.stdin.buffer.read(length)) + + +def write_frame(message): + body = json.dumps(message, separators=(",", ":")).encode("utf-8") + sys.stdout.buffer.write(b"Content-Length: " + str(len(body)).encode("ascii") + b"\r\n\r\n") + sys.stdout.buffer.write(body) + sys.stdout.buffer.flush() + + +while True: + msg = read_frame() + method = msg.get("method") + if method == "initialized": + continue + if method == "exit": + raise SystemExit(0) + ident = msg["id"] + if method == "initialize": + write_frame({ + "jsonrpc": "2.0", + "id": ident, + "result": { + "name": "clarion-plugin-secretfixture", + "version": "0.1.0", + "ontology_version": "0.1.0", + "capabilities": {}, + }, + }) + elif method == "analyze_file": + path = msg["params"]["file_path"] + name = "file_" + re.sub(r"[^A-Za-z0-9_]", "_", pathlib.Path(path).name) + write_frame({ + "jsonrpc": "2.0", + "id": ident, + "result": { + "entities": [ + { + "id": "secretfixture:module:" + name, + "kind": "module", + "qualified_name": name, + "source": {"file_path": path}, + } + ], + "edges": [], + }, + }) + elif method == "shutdown": + write_frame({"jsonrpc": "2.0", "id": ident, "result": {}}) + else: + raise SystemExit(1) +"#; + +const PLUGIN_MANIFEST: &str = r#" +[plugin] +name = "clarion-plugin-secretfixture" +plugin_id = "secretfixture" +version = "0.1.0" +protocol_version = "1.0" +executable = "clarion-plugin-secretfixture" +language = "secretfixture" +extensions = ["sec"] + +[capabilities.runtime] +expected_max_rss_mb = 256 +expected_entities_per_file = 100 +wardline_aware = false +reads_outside_project_root = false + +[ontology] +entity_kinds = ["module"] +edge_kinds = [] +rule_id_prefix = "CLA-SECRET-FIXTURE-" +ontology_version = "0.1.0" +"#; + +fn write_secret_fixture_plugin(plugin_dir: &std::path::Path) { + use std::os::unix::fs::PermissionsExt; + + let plugin_script = plugin_dir.join("clarion-plugin-secretfixture"); + std::fs::write(&plugin_script, PLUGIN_SCRIPT).expect("write plugin script"); + let mut perms = std::fs::metadata(&plugin_script) + .expect("stat plugin") + .permissions(); + perms.set_mode(0o755); + std::fs::set_permissions(&plugin_script, perms).expect("chmod plugin"); + + std::fs::write(plugin_dir.join("plugin.toml"), PLUGIN_MANIFEST).expect("write plugin manifest"); +} + +fn install_project(project: &std::path::Path) { + clarion_bin() + .args(["install", "--path"]) + .arg(project) + .assert() + .success(); +} + +fn plugin_path(plugin_dir: &std::path::Path) -> std::ffi::OsString { + std::env::join_paths(std::iter::once(plugin_dir.to_path_buf())).unwrap() +} + +fn conn(project: &std::path::Path) -> Connection { + Connection::open(project.join(".clarion/clarion.db")).expect("open clarion db") +} + +#[test] +fn clean_project_has_no_secret_findings() { + let project = tempfile::tempdir().unwrap(); + let plugin = tempfile::tempdir().unwrap(); + write_secret_fixture_plugin(plugin.path()); + install_project(project.path()); + std::fs::write(project.path().join("clean.sec"), b"nothing to see\n").unwrap(); + + clarion_bin() + .arg("analyze") + .arg(project.path()) + .env("PATH", plugin_path(plugin.path())) + .assert() + .success(); + + let count: i64 = conn(project.path()) + .query_row( + "SELECT COUNT(*) FROM findings WHERE rule_id LIKE 'CLA-SEC-%'", + [], + |row| row.get(0), + ) + .unwrap(); + assert_eq!(count, 0); +} + +#[test] +fn secret_file_persists_finding_and_briefing_block() { + let project = tempfile::tempdir().unwrap(); + let plugin = tempfile::tempdir().unwrap(); + write_secret_fixture_plugin(plugin.path()); + install_project(project.path()); + std::fs::write( + project.path().join("leaky.sec"), + b"aws_access_key_id = 'AKIAIOSFODNN7EXAMPLE'\n", + ) + .unwrap(); + + clarion_bin() + .arg("analyze") + .arg(project.path()) + .env("PATH", plugin_path(plugin.path())) + .assert() + .success(); + + let db = conn(project.path()); + let blocked: String = db + .query_row( + "SELECT json_extract(properties, '$.briefing_blocked') FROM entities", + [], + |row| row.get(0), + ) + .unwrap(); + assert_eq!(blocked, "secret_present"); + let count: i64 = db + .query_row( + "SELECT COUNT(*) FROM findings WHERE rule_id = 'CLA-SEC-SECRET-DETECTED'", + [], + |row| row.get(0), + ) + .unwrap(); + assert_eq!(count, 1); +} + +#[test] +fn baseline_suppresses_secret_and_emits_audit_match() { + let project = tempfile::tempdir().unwrap(); + let plugin = tempfile::tempdir().unwrap(); + write_secret_fixture_plugin(plugin.path()); + install_project(project.path()); + std::fs::write( + project.path().join("leaky.sec"), + b"aws_access_key_id = 'AKIAIOSFODNN7EXAMPLE'\n", + ) + .unwrap(); + std::fs::write( + project.path().join(".clarion/secrets-baseline.yaml"), + r#" +version: "1.0" +results: + "leaky.sec": + - type: "AWS Access Key" + hashed_secret: "25910f981e85ca04baf359199dd0bd4a3ae738b6" + line_number: 1 + is_secret: false + justification: "AWS documentation example key." +"#, + ) + .unwrap(); + + clarion_bin() + .arg("analyze") + .arg(project.path()) + .env("PATH", plugin_path(plugin.path())) + .assert() + .success(); + + let db = conn(project.path()); + let secret_count: i64 = db + .query_row( + "SELECT COUNT(*) FROM findings WHERE rule_id = 'CLA-SEC-SECRET-DETECTED'", + [], + |row| row.get(0), + ) + .unwrap(); + let match_count: i64 = db + .query_row( + "SELECT COUNT(*) FROM findings WHERE rule_id = 'CLA-INFRA-SECRET-BASELINE-MATCH'", + [], + |row| row.get(0), + ) + .unwrap(); + let blocked_count: i64 = db + .query_row( + "SELECT COUNT(*) FROM entities WHERE json_extract(properties, '$.briefing_blocked') IS NOT NULL", + [], + |row| row.get(0), + ) + .unwrap(); + assert_eq!(secret_count, 0); + assert_eq!(match_count, 1); + assert_eq!(blocked_count, 0); +} + +#[test] +fn missing_baseline_justification_degrades_to_finding() { + let project = tempfile::tempdir().unwrap(); + let plugin = tempfile::tempdir().unwrap(); + write_secret_fixture_plugin(plugin.path()); + install_project(project.path()); + std::fs::write( + project.path().join("leaky.sec"), + b"aws_access_key_id = 'AKIAIOSFODNN7EXAMPLE'\n", + ) + .unwrap(); + std::fs::write( + project.path().join(".clarion/secrets-baseline.yaml"), + r#" +version: "1.0" +results: + "leaky.sec": + - type: "AWS Access Key" + hashed_secret: "25910f981e85ca04baf359199dd0bd4a3ae738b6" + line_number: 1 + is_secret: false +"#, + ) + .unwrap(); + + clarion_bin() + .arg("analyze") + .arg(project.path()) + .env("PATH", plugin_path(plugin.path())) + .assert() + .success(); + + let count: i64 = conn(project.path()) + .query_row( + "SELECT COUNT(*) FROM findings WHERE rule_id = 'CLA-INFRA-SECRET-BASELINE-NO-JUSTIFICATION'", + [], + |row| row.get(0), + ) + .unwrap(); + assert_eq!(count, 1); +} + +#[test] +fn non_tty_override_confirmed_allows_briefing_and_records_stats() { + let project = tempfile::tempdir().unwrap(); + let plugin = tempfile::tempdir().unwrap(); + write_secret_fixture_plugin(plugin.path()); + install_project(project.path()); + std::fs::write( + project.path().join("leaky.sec"), + b"aws_access_key_id = 'AKIAIOSFODNN7EXAMPLE'\n", + ) + .unwrap(); + + clarion_bin() + .args([ + "analyze", + "--allow-unredacted-secrets", + "--confirm-allow-unredacted-secrets=yes-i-understand", + ]) + .arg(project.path()) + .env("PATH", plugin_path(plugin.path())) + .assert() + .success(); + + let db = conn(project.path()); + let blocked_count: i64 = db + .query_row( + "SELECT COUNT(*) FROM entities WHERE json_extract(properties, '$.briefing_blocked') IS NOT NULL", + [], + |row| row.get(0), + ) + .unwrap(); + let override_count: i64 = db + .query_row( + "SELECT COUNT(*) FROM findings WHERE rule_id = 'CLA-SEC-UNREDACTED-SECRETS-ALLOWED'", + [], + |row| row.get(0), + ) + .unwrap(); + let override_used: i64 = db + .query_row( + "SELECT json_extract(stats, '$.secret_override_used') FROM runs", + [], + |row| row.get(0), + ) + .unwrap(); + assert_eq!(blocked_count, 0); + assert_eq!(override_count, 1); + assert_eq!(override_used, 1); +} + +#[test] +fn non_tty_override_without_confirmation_exits_78_before_run_start() { + let project = tempfile::tempdir().unwrap(); + let plugin = tempfile::tempdir().unwrap(); + write_secret_fixture_plugin(plugin.path()); + install_project(project.path()); + std::fs::write( + project.path().join("leaky.sec"), + b"aws_access_key_id = 'AKIAIOSFODNN7EXAMPLE'\n", + ) + .unwrap(); + + let assert = clarion_bin() + .args(["analyze", "--allow-unredacted-secrets"]) + .arg(project.path()) + .env("PATH", plugin_path(plugin.path())) + .assert() + .code(78); + let stderr = String::from_utf8_lossy(&assert.get_output().stderr); + assert!(stderr.contains("CLA-INFRA-SECRET-OVERRIDE-UNCONFIRMED")); + let run_count: i64 = conn(project.path()) + .query_row("SELECT COUNT(*) FROM runs", [], |row| row.get(0)) + .unwrap(); + assert_eq!(run_count, 0); +} + +#[test] +#[ignore = "TTY override confirmation needs an interactive terminal; WS-D owns the manual smoke."] +fn tty_override_confirmation_manual_smoke() {} + +#[test] +fn override_flag_is_noop_without_detections() { + let project = tempfile::tempdir().unwrap(); + let plugin = tempfile::tempdir().unwrap(); + write_secret_fixture_plugin(plugin.path()); + install_project(project.path()); + std::fs::write(project.path().join("clean.sec"), b"nothing to see\n").unwrap(); + + clarion_bin() + .args(["analyze", "--allow-unredacted-secrets"]) + .arg(project.path()) + .env("PATH", plugin_path(plugin.path())) + .assert() + .success(); + + let db = conn(project.path()); + let override_count: i64 = db + .query_row( + "SELECT COUNT(*) FROM findings WHERE rule_id = 'CLA-SEC-UNREDACTED-SECRETS-ALLOWED'", + [], + |row| row.get(0), + ) + .unwrap(); + let stats_value: Option = db + .query_row( + "SELECT json_extract(stats, '$.secret_override_used') FROM runs", + [], + |row| row.get(0), + ) + .unwrap(); + assert_eq!(override_count, 0); + assert_eq!(stats_value, None); +} + +#[test] +fn only_secret_bearing_file_is_blocked_in_multi_file_project() { + let project = tempfile::tempdir().unwrap(); + let plugin = tempfile::tempdir().unwrap(); + write_secret_fixture_plugin(plugin.path()); + install_project(project.path()); + std::fs::write(project.path().join("clean_a.sec"), b"nothing to see\n").unwrap(); + std::fs::write( + project.path().join("leaky.sec"), + b"aws_access_key_id = 'AKIAIOSFODNN7EXAMPLE'\n", + ) + .unwrap(); + std::fs::write(project.path().join("clean_b.sec"), b"still clean\n").unwrap(); + + clarion_bin() + .arg("analyze") + .arg(project.path()) + .env("PATH", plugin_path(plugin.path())) + .assert() + .success(); + + let blocked_count: i64 = conn(project.path()) + .query_row( + "SELECT COUNT(*) FROM entities WHERE json_extract(properties, '$.briefing_blocked') = 'secret_present'", + [], + |row| row.get(0), + ) + .unwrap(); + assert_eq!(blocked_count, 1); +} diff --git a/crates/clarion-core/src/plugin/host.rs b/crates/clarion-core/src/plugin/host.rs index d084ebee..2afed830 100644 --- a/crates/clarion-core/src/plugin/host.rs +++ b/crates/clarion-core/src/plugin/host.rs @@ -27,7 +27,7 @@ //! only calls `setrlimit(2)`, which is async-signal-safe per POSIX.1-2017 //! §2.4.3. The `unsafe` block is the minimum required by the `pre_exec` API. -use std::collections::BTreeSet; +use std::collections::{BTreeMap, BTreeSet}; use std::io::{BufRead, Write}; use std::path::{Path, PathBuf}; @@ -414,6 +414,8 @@ where /// discarded on overflow so the plugin cannot back-pressure the host /// via stderr writes. stderr_tail: Option>>>, + /// Canonical source paths whose entities must not be sent for LLM briefing. + briefing_blocks: BTreeMap, } /// Size of the stderr ring buffer kept for diagnostics. 64 KiB holds @@ -661,6 +663,7 @@ impl PluginHost { terminated: false, ontology_version: None, stderr_tail: None, + briefing_blocks: BTreeMap::new(), } } @@ -688,6 +691,13 @@ impl PluginHost { self.ontology_version.as_deref() } + /// Install file-level briefing blocks produced by the core pre-ingest + /// scanner. Keys are canonical source paths and values are open-vocabulary + /// policy reasons such as `secret_present`. + pub fn set_briefing_blocks(&mut self, blocks: BTreeMap) { + self.briefing_blocks = blocks; + } + /// Perform the `initialize` → `initialized` handshake. /// /// Steps: @@ -824,7 +834,7 @@ impl PluginHost { let mut accepted = Vec::new(); for raw_val in afr.entities { - let raw: RawEntity = match serde_json::from_value(raw_val) { + let mut raw: RawEntity = match serde_json::from_value(raw_val) { Ok(e) => e, Err(e) => { // Drop the entity, but record the serde error so operators @@ -923,6 +933,8 @@ impl PluginHost { return Err(HostError::EntityCapExceeded(e)); } + self.apply_briefing_block(&mut raw, &jailed); + accepted.push(AcceptedEntity { id: expected_id, kind: raw.kind.clone(), @@ -942,6 +954,15 @@ impl PluginHost { }) } + fn apply_briefing_block(&self, raw: &mut RawEntity, jailed: &str) { + if let Some(reason) = self.briefing_blocks.get(Path::new(jailed)) { + raw.extra.insert( + "briefing_blocked".to_owned(), + serde_json::Value::String(reason.clone()), + ); + } + } + /// B.3: per-edge validation pipeline. Mirrors the entity loop's /// drop-on-violation/emit-finding posture but without the kill paths — /// edges do not participate in the path-escape breaker or entity cap diff --git a/crates/clarion-mcp/src/lib.rs b/crates/clarion-mcp/src/lib.rs index d44f06e3..78cfc2bd 100644 --- a/crates/clarion-mcp/src/lib.rs +++ b/crates/clarion-mcp/src/lib.rs @@ -1146,6 +1146,9 @@ impl ServerState { if entity.kind == "subsystem" { return Ok(SummaryRead::ScopeDeferred(Box::new(entity))); } + if let Some(reason) = briefing_block_reason(&entity) { + return Ok(SummaryRead::BriefingBlocked(Box::new(entity), reason)); + } let Some(content_hash) = entity.content_hash.clone() else { return Ok(SummaryRead::MissingContentHash(entity.id)); }; @@ -1473,6 +1476,7 @@ enum SummaryRead { EntityNotFound(String), MissingContentHash(String), ScopeDeferred(Box), + BriefingBlocked(Box, String), } struct SummaryReady { @@ -2274,6 +2278,7 @@ fn summary_read_error(read: SummaryRead) -> Value { false, ), SummaryRead::ScopeDeferred(entity) => summary_scope_deferred(&entity), + SummaryRead::BriefingBlocked(entity, reason) => summary_briefing_blocked(&entity, &reason), SummaryRead::Ready(_) => unreachable!("ready summary read is not an error"), } } @@ -2347,6 +2352,24 @@ fn summary_scope_deferred(entity: &EntityRow) -> Value { })) } +fn summary_briefing_blocked(entity: &EntityRow, reason: &str) -> Value { + success_envelope(json!({ + "available": false, + "entity_id": entity.id, + "entity": entity_json(entity), + "summary": null, + "briefing_blocked": reason, + "remediation": "File flagged by pre-ingest secret scan. Fix the secret or whitelist via .clarion/secrets-baseline.yaml. See ADR-013." + })) +} + +fn briefing_block_reason(entity: &EntityRow) -> Option { + entity_properties_json(entity) + .get("briefing_blocked") + .and_then(Value::as_str) + .map(str::to_owned) +} + fn tool_json_rpc_response(id: &Value, envelope: &Value) -> Value { let is_error = !envelope .get("ok") diff --git a/crates/clarion-mcp/tests/storage_tools.rs b/crates/clarion-mcp/tests/storage_tools.rs index 02519070..43c3dcc5 100644 --- a/crates/clarion-mcp/tests/storage_tools.rs +++ b/crates/clarion-mcp/tests/storage_tools.rs @@ -818,6 +818,62 @@ async fn summary_on_subsystem_returns_policy_envelope_without_llm_call() { handle.await.unwrap().unwrap(); } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn summary_on_secret_blocked_entity_returns_policy_envelope_without_llm_or_cache() { + let (project, db_path) = open_project(); + let conn = Connection::open(&db_path).expect("open sqlite"); + conn.execute( + "UPDATE entities SET properties = ?1 WHERE id = 'python:function:demo.entry'", + params![json!({"briefing_blocked": "secret_present"}).to_string()], + ) + .expect("mark entity blocked"); + drop(conn); + + let (writer, handle) = Writer::spawn(db_path.clone(), 50, 256).unwrap(); + let provider = Arc::new(AnySummaryProvider::new_output( + r#"{"purpose":"should not run"}"#, + 120, + 0.012, + )); + let state = state_for_summary( + project.path(), + &db_path, + &writer, + provider.clone(), + llm_config(), + ); + + let envelope = call_tool( + &state, + "summary", + json!({"id": "python:function:demo.entry"}), + ) + .await; + + assert_eq!(envelope["ok"], true); + assert_eq!(envelope["result"]["summary"], Value::Null); + assert_eq!(envelope["result"]["briefing_blocked"], "secret_present"); + assert_eq!(envelope["stats_delta"], json!({})); + assert!(provider.invocations().is_empty()); + + let entity_at = call_tool(&state, "entity_at", json!({"file": "demo.py", "line": 1})).await; + assert_eq!(entity_at["ok"], true); + + drop(state); + drop(writer); + handle.await.unwrap().unwrap(); + + let conn = Connection::open(&db_path).expect("open sqlite"); + let cache_rows: i64 = conn + .query_row( + "SELECT COUNT(*) FROM summary_cache WHERE entity_id = 'python:function:demo.entry'", + [], + |row| row.get(0), + ) + .expect("query summary cache"); + assert_eq!(cache_rows, 0); +} + #[tokio::test] async fn issues_for_returns_unavailable_when_filigree_disabled() { let (project, db_path) = open_project(); diff --git a/crates/clarion-scanner/Cargo.toml b/crates/clarion-scanner/Cargo.toml new file mode 100644 index 00000000..d64555d6 --- /dev/null +++ b/crates/clarion-scanner/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "clarion-scanner" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true + +[lints] +workspace = true + +[dependencies] +regex.workspace = true +serde.workspace = true +serde_norway.workspace = true +sha1.workspace = true +thiserror.workspace = true + +[dev-dependencies] +tempfile.workspace = true diff --git a/crates/clarion-scanner/src/baseline.rs b/crates/clarion-scanner/src/baseline.rs new file mode 100644 index 00000000..ef1ea383 --- /dev/null +++ b/crates/clarion-scanner/src/baseline.rs @@ -0,0 +1,225 @@ +use std::{ + collections::{BTreeMap, BTreeSet}, + fs, + path::{Path, PathBuf}, +}; + +use serde::{Deserialize, Serialize}; + +use crate::{Detection, hex_decode_20}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Baseline { + version: String, + entries: BTreeMap>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BaselineEntry { + pub rule_type: String, + pub hashed_secret: [u8; 20], + pub line_number: u32, + pub is_secret: bool, + pub justification: String, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BaselineMatch { + pub file_path: PathBuf, + pub entry: BaselineEntry, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SuppressionResult { + pub allowed: Vec, + pub suppressed: Vec, + pub fired_entries: Vec, +} + +#[derive(Debug, thiserror::Error)] +pub enum BaselineError { + #[error("baseline version mismatch: expected 1.0, got {0}")] + UnsupportedVersion(String), + #[error("baseline entry missing required field 'justification' at {file}:{line}")] + MissingJustification { file: PathBuf, line: u32 }, + #[error("baseline entry has invalid hashed_secret at {file}:{line}: {details}")] + InvalidHash { + file: PathBuf, + line: u32, + details: String, + }, + #[error("baseline parse error: {0}")] + Parse(#[from] serde_norway::Error), + #[error("baseline I/O error: {0}")] + Io(#[from] std::io::Error), +} + +pub fn load_baseline(path: &Path) -> Result { + match fs::read_to_string(path) { + Ok(raw) => Baseline::from_yaml_str(&raw), + Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(Baseline::empty()), + Err(err) => Err(BaselineError::Io(err)), + } +} + +impl Baseline { + #[must_use] + pub fn empty() -> Self { + Self { + version: "1.0".to_owned(), + entries: BTreeMap::new(), + } + } + + pub fn from_yaml_str(raw: &str) -> Result { + let parsed: RawBaseline = serde_norway::from_str(raw)?; + Self::from_raw(parsed) + } + + pub fn to_yaml_string(&self) -> Result { + let raw = RawBaseline::from(self); + serde_norway::to_string(&raw).map_err(BaselineError::Parse) + } + + #[must_use] + pub fn entries(&self) -> &BTreeMap> { + &self.entries + } + + #[must_use] + pub fn suppress(&self, detections: Vec, file: &Path) -> SuppressionResult { + let entries = self.entries_for(file); + let mut allowed = Vec::new(); + let mut suppressed = Vec::new(); + let mut fired_entries = Vec::new(); + let mut fired_keys = BTreeSet::new(); + + 'detections: for detection in detections { + for (baseline_path, entry) in &entries { + if entry.is_secret { + continue; + } + if entry.hashed_secret == detection.hashed_secret + && entry.line_number == detection.line_number + { + let key = ( + (*baseline_path).clone(), + entry.hashed_secret, + entry.line_number, + ); + if fired_keys.insert(key) { + fired_entries.push(BaselineMatch { + file_path: (*baseline_path).clone(), + entry: (*entry).clone(), + }); + } + suppressed.push(detection); + continue 'detections; + } + } + allowed.push(detection); + } + + SuppressionResult { + allowed, + suppressed, + fired_entries, + } + } + + fn from_raw(raw: RawBaseline) -> Result { + if raw.version != "1.0" { + return Err(BaselineError::UnsupportedVersion(raw.version)); + } + let mut entries = BTreeMap::new(); + for (file, raw_entries) in raw.results { + let mut converted = Vec::new(); + for entry in raw_entries { + let justification = entry.justification.unwrap_or_default(); + if justification.trim().is_empty() { + return Err(BaselineError::MissingJustification { + file, + line: entry.line_number, + }); + } + let hashed_secret = hex_decode_20(&entry.hashed_secret).map_err(|details| { + BaselineError::InvalidHash { + file: file.clone(), + line: entry.line_number, + details, + } + })?; + converted.push(BaselineEntry { + rule_type: entry.rule_type, + hashed_secret, + line_number: entry.line_number, + is_secret: entry.is_secret, + justification, + }); + } + entries.insert(file, converted); + } + Ok(Self { + version: raw.version, + entries, + }) + } + + fn entries_for(&self, file: &Path) -> Vec<(&PathBuf, &BaselineEntry)> { + self.entries + .iter() + .filter(|(candidate, _)| baseline_path_matches(file, candidate)) + .flat_map(|(path, entries)| entries.iter().map(move |entry| (path, entry))) + .collect() + } +} + +#[derive(Debug, Serialize, Deserialize)] +struct RawBaseline { + version: String, + #[serde(default)] + results: BTreeMap>, +} + +#[derive(Debug, Serialize, Deserialize)] +struct RawBaselineEntry { + #[serde(rename = "type")] + rule_type: String, + hashed_secret: String, + line_number: u32, + #[serde(default)] + is_secret: bool, + justification: Option, +} + +impl From<&Baseline> for RawBaseline { + fn from(value: &Baseline) -> Self { + let results = value + .entries + .iter() + .map(|(path, entries)| { + ( + path.clone(), + entries + .iter() + .map(|entry| RawBaselineEntry { + rule_type: entry.rule_type.clone(), + hashed_secret: crate::hex_encode(&entry.hashed_secret), + line_number: entry.line_number, + is_secret: entry.is_secret, + justification: Some(entry.justification.clone()), + }) + .collect(), + ) + }) + .collect(); + Self { + version: value.version.clone(), + results, + } + } +} + +fn baseline_path_matches(file: &Path, baseline_path: &Path) -> bool { + file == baseline_path || file.ends_with(baseline_path) +} diff --git a/crates/clarion-scanner/src/entropy.rs b/crates/clarion-scanner/src/entropy.rs new file mode 100644 index 00000000..0c52d766 --- /dev/null +++ b/crates/clarion-scanner/src/entropy.rs @@ -0,0 +1,60 @@ +use std::collections::BTreeMap; + +/// Entropy threshold and minimum candidate length for one alphabet. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct EntropyTuning { + pub min_len: usize, + pub min_entropy: f64, +} + +impl EntropyTuning { + pub const BASE64: Self = Self { + min_len: 20, + min_entropy: 4.5, + }; + pub const HEX: Self = Self { + min_len: 40, + min_entropy: 3.0, + }; + + pub(crate) fn accepts(self, candidate: &[u8]) -> bool { + candidate.len() >= self.min_len && shannon_entropy(candidate) >= self.min_entropy + } +} + +pub(crate) fn shannon_entropy(bytes: &[u8]) -> f64 { + if bytes.is_empty() { + return 0.0; + } + let mut counts = BTreeMap::::new(); + for byte in bytes { + *counts.entry(*byte).or_default() += 1; + } + let len = usize_to_f64(bytes.len()); + counts + .values() + .map(|count| { + let p = usize_to_f64(*count) / len; + -p * p.log2() + }) + .sum() +} + +fn usize_to_f64(value: usize) -> f64 { + f64::from(u32::try_from(value).unwrap_or(u32::MAX)) +} + +#[cfg(test)] +mod tests { + use super::shannon_entropy; + + #[test] + fn entropy_is_low_for_repetition() { + assert!(shannon_entropy(b"aaaaaaaaaaaaaaaaaaaaaaaa") < 0.1); + } + + #[test] + fn entropy_is_higher_for_varied_alphabet() { + assert!(shannon_entropy(b"0123456789abcdefABCDEF+/") > 4.0); + } +} diff --git a/crates/clarion-scanner/src/lib.rs b/crates/clarion-scanner/src/lib.rs new file mode 100644 index 00000000..abcb6924 --- /dev/null +++ b/crates/clarion-scanner/src/lib.rs @@ -0,0 +1,92 @@ +//! Core-owned pre-ingest secret scanner. +//! +//! The scanner intentionally stores only positions, rule identifiers, and a +//! detect-secrets-compatible SHA-1 digest of the matched bytes. Literal secret +//! values do not leave the scanning call. + +mod baseline; +mod entropy; +mod patterns; + +pub use baseline::{ + Baseline, BaselineEntry, BaselineError, BaselineMatch, SuppressionResult, load_baseline, +}; +pub use entropy::EntropyTuning; +pub use patterns::{PatternMeta, Scanner}; + +/// One secret-like match in one file buffer. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Detection { + pub rule_id: &'static str, + pub category: SecretCategory, + pub byte_offset: usize, + pub line_number: u32, + pub matched_len: usize, + pub hashed_secret: [u8; 20], +} + +/// High-level category used for evidence and future operator grouping. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SecretCategory { + CloudCredential, + VcsCredential, + AiProviderCredential, + PaymentsCredential, + MessagingCredential, + PrivateKey, + JwtToken, + HighEntropy, + ContextualCredential, +} + +fn sha1_digest(bytes: &[u8]) -> [u8; 20] { + use sha1::{Digest, Sha1}; + + let mut hasher = Sha1::new(); + hasher.update(bytes); + hasher.finalize().into() +} + +fn line_number_for_offset(buf: &[u8], offset: usize) -> u32 { + let line = buf + .get(..offset.min(buf.len())) + .unwrap_or(buf) + .iter() + .fold(0usize, |count, byte| count + usize::from(*byte == b'\n')) + + 1; + u32::try_from(line).unwrap_or(u32::MAX) +} + +fn hex_encode(bytes: &[u8; 20]) -> String { + const HEX: &[u8; 16] = b"0123456789abcdef"; + let mut out = String::with_capacity(bytes.len() * 2); + for byte in bytes { + out.push(char::from(HEX[usize::from(byte >> 4)])); + out.push(char::from(HEX[usize::from(byte & 0x0f)])); + } + out +} + +fn hex_decode_20(input: &str) -> Result<[u8; 20], String> { + let raw = input.trim(); + if raw.len() != 40 { + return Err(format!("expected 40 hex characters, got {}", raw.len())); + } + let mut out = [0u8; 20]; + for (idx, chunk) in raw.as_bytes().chunks_exact(2).enumerate() { + let hi = hex_value(chunk[0]).ok_or_else(|| format!("invalid hex at byte {}", idx * 2))?; + let lo = + hex_value(chunk[1]).ok_or_else(|| format!("invalid hex at byte {}", idx * 2 + 1))?; + out[idx] = (hi << 4) | lo; + } + Ok(out) +} + +fn hex_value(byte: u8) -> Option { + match byte { + b'0'..=b'9' => Some(byte - b'0'), + b'a'..=b'f' => Some(byte - b'a' + 10), + b'A'..=b'F' => Some(byte - b'A' + 10), + _ => None, + } +} diff --git a/crates/clarion-scanner/src/patterns.rs b/crates/clarion-scanner/src/patterns.rs new file mode 100644 index 00000000..9e802cd0 --- /dev/null +++ b/crates/clarion-scanner/src/patterns.rs @@ -0,0 +1,300 @@ +use regex::{Regex, RegexSet}; + +use crate::{ + Detection, SecretCategory, entropy::EntropyTuning, line_number_for_offset, sha1_digest, +}; + +/// Metadata for one named secret detector. +#[derive(Debug, Clone)] +pub struct PatternMeta { + pub rule_id: &'static str, + pub detect_secrets_type: &'static str, + pub category: SecretCategory, + pub pattern: &'static str, + capture_group: Option, +} + +#[derive(Debug)] +struct CompiledPattern { + meta: PatternMeta, + regex: Regex, +} + +/// Rust-native port of the ADR-013 v0.1 secret rule floor. +#[derive(Debug)] +pub struct Scanner { + patterns: RegexSet, + pattern_meta: Vec, + compiled_patterns: Vec, + entropy_b64: EntropyTuning, + entropy_hex: EntropyTuning, + entropy_b64_re: Regex, + entropy_hex_re: Regex, +} + +impl Default for Scanner { + fn default() -> Self { + Self::new() + } +} + +impl Scanner { + /// Build the default ADR-013 scanner. + /// + /// # Panics + /// + /// Panics only if one of the compiled-in regular expressions is invalid. + #[must_use] + pub fn new() -> Self { + let pattern_meta = default_pattern_meta(); + let patterns = RegexSet::new(pattern_meta.iter().map(|meta| meta.pattern)) + .expect("default secret patterns compile"); + let compiled_patterns = pattern_meta + .iter() + .cloned() + .map(|meta| CompiledPattern { + regex: Regex::new(meta.pattern).expect("default secret pattern compiles"), + meta, + }) + .collect(); + Self { + patterns, + pattern_meta, + compiled_patterns, + entropy_b64: EntropyTuning::BASE64, + entropy_hex: EntropyTuning::HEX, + entropy_b64_re: Regex::new(r"\b[A-Za-z0-9+/]{20,}={0,2}\b") + .expect("base64 candidate regex compiles"), + entropy_hex_re: Regex::new(r"\b[a-fA-F0-9]{40,}\b") + .expect("hex candidate regex compiles"), + } + } + + #[must_use] + pub fn pattern_meta(&self) -> &[PatternMeta] { + &self.pattern_meta + } + + #[must_use] + pub fn scan_bytes(&self, buf: &[u8]) -> Vec { + let source = String::from_utf8_lossy(buf); + let bytes = source.as_bytes(); + let _set_matches = self.patterns.matches(&source); + let mut detections = Vec::new(); + + for compiled in &self.compiled_patterns { + for captures in compiled.regex.captures_iter(&source) { + let Some(whole_match) = captures.get(0) else { + continue; + }; + if compiled.meta.category == SecretCategory::ContextualCredential + && line_is_comment(bytes, whole_match.start()) + { + continue; + } + let Some(secret_match) = compiled + .meta + .capture_group + .and_then(|group| captures.get(group)) + .or(Some(whole_match)) + else { + continue; + }; + detections.push(detection_from_match( + &compiled.meta, + bytes, + secret_match.start(), + secret_match.end(), + )); + } + } + + let named_ranges = detections + .iter() + .map(|detection| { + ( + detection.byte_offset, + detection.byte_offset + detection.matched_len, + ) + }) + .collect::>(); + self.scan_entropy(bytes, &named_ranges, &mut detections); + + detections.sort_by_key(|d| (d.byte_offset, d.rule_id)); + detections + } + + fn scan_entropy( + &self, + bytes: &[u8], + named_ranges: &[(usize, usize)], + detections: &mut Vec, + ) { + let source = String::from_utf8_lossy(bytes); + for candidate in self.entropy_b64_re.find_iter(&source) { + let candidate_bytes = &source.as_bytes()[candidate.start()..candidate.end()]; + if looks_like_sha256_base64(&source, candidate.start(), candidate.end()) { + continue; + } + if !range_overlaps(candidate.start(), candidate.end(), named_ranges) + && self.entropy_b64.accepts(candidate_bytes) + { + detections.push(entropy_detection( + "HighEntropyBase64", + bytes, + candidate.start(), + candidate.end(), + )); + } + } + for candidate in self.entropy_hex_re.find_iter(&source) { + let candidate_bytes = &source.as_bytes()[candidate.start()..candidate.end()]; + if !range_overlaps(candidate.start(), candidate.end(), named_ranges) + && self.entropy_hex.accepts(candidate_bytes) + { + detections.push(entropy_detection( + "HighEntropyHex", + bytes, + candidate.start(), + candidate.end(), + )); + } + } + } +} + +fn detection_from_match(meta: &PatternMeta, bytes: &[u8], start: usize, end: usize) -> Detection { + let matched = &bytes[start..end]; + Detection { + rule_id: meta.rule_id, + category: meta.category, + byte_offset: start, + line_number: line_number_for_offset(bytes, start), + matched_len: end.saturating_sub(start), + hashed_secret: sha1_digest(matched), + } +} + +fn entropy_detection(rule_id: &'static str, bytes: &[u8], start: usize, end: usize) -> Detection { + Detection { + rule_id, + category: SecretCategory::HighEntropy, + byte_offset: start, + line_number: line_number_for_offset(bytes, start), + matched_len: end.saturating_sub(start), + hashed_secret: sha1_digest(&bytes[start..end]), + } +} + +fn default_pattern_meta() -> Vec { + vec![ + PatternMeta { + rule_id: "AwsAccessKeyId", + detect_secrets_type: "AWS Access Key", + category: SecretCategory::CloudCredential, + pattern: r"\b(?:AKIA|ASIA)[0-9A-Z]{16}\b", + capture_group: None, + }, + PatternMeta { + rule_id: "AwsSecretAccessKey", + detect_secrets_type: "AWS Secret Access Key", + category: SecretCategory::CloudCredential, + pattern: r#"(?i)\baws[^:=\n]{0,32}(?:secret|access)[^:=\n]{0,32}(?:=|:|:=)\s*["']?([A-Za-z0-9/+=]{40})["']?"#, + capture_group: Some(1), + }, + PatternMeta { + rule_id: "GitHubPat", + detect_secrets_type: "GitHub Token", + category: SecretCategory::VcsCredential, + pattern: r"\bghp_[A-Za-z0-9]{36}\b", + capture_group: None, + }, + PatternMeta { + rule_id: "GitHubFineGrainedPat", + detect_secrets_type: "GitHub Fine-Grained Token", + category: SecretCategory::VcsCredential, + pattern: r"\bgithub_pat_[A-Za-z0-9_]{82,}\b", + capture_group: None, + }, + PatternMeta { + rule_id: "GitHubOAuth", + detect_secrets_type: "GitHub OAuth Token", + category: SecretCategory::VcsCredential, + pattern: r"\bgh[ousr]_[A-Za-z0-9]{36}\b", + capture_group: None, + }, + PatternMeta { + rule_id: "AnthropicApiKey", + detect_secrets_type: "Anthropic API Key", + category: SecretCategory::AiProviderCredential, + pattern: r"\bsk-ant-[A-Za-z0-9_-]{90,}\b", + capture_group: None, + }, + PatternMeta { + rule_id: "OpenAiApiKey", + detect_secrets_type: "OpenAI API Key", + category: SecretCategory::AiProviderCredential, + pattern: r"\bsk-[A-Za-z0-9]{48}\b", + capture_group: None, + }, + PatternMeta { + rule_id: "StripeApiKey", + detect_secrets_type: "Stripe API Key", + category: SecretCategory::PaymentsCredential, + pattern: r"\b(?:sk|pk|rk)_(?:live|test)_[A-Za-z0-9]{16,}\b", + capture_group: None, + }, + PatternMeta { + rule_id: "SlackToken", + detect_secrets_type: "Slack Token", + category: SecretCategory::MessagingCredential, + pattern: r"\bxox[baprs]-[A-Za-z0-9-]{10,}\b", + capture_group: None, + }, + PatternMeta { + rule_id: "JwtToken", + detect_secrets_type: "JWT Token", + category: SecretCategory::JwtToken, + pattern: r"\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b", + capture_group: None, + }, + PatternMeta { + rule_id: "PrivateKeyHeader", + detect_secrets_type: "Private Key", + category: SecretCategory::PrivateKey, + pattern: r"-----BEGIN (?:RSA|EC|DSA|OPENSSH|PGP|ENCRYPTED) PRIVATE KEY-----", + capture_group: None, + }, + PatternMeta { + rule_id: "ContextualCredential", + detect_secrets_type: "Keyword Detector", + category: SecretCategory::ContextualCredential, + pattern: r#"(?i)(?:^|[^A-Za-z0-9_-])(?:password|passwd|secret[_-]?token|secret|token|api[_-]?key)\s*(?:=|:=|:)\s*["']([^"'\s]{8,})["']"#, + capture_group: Some(1), + }, + ] +} + +fn range_overlaps(start: usize, end: usize, ranges: &[(usize, usize)]) -> bool { + ranges + .iter() + .any(|(range_start, range_end)| start < *range_end && end > *range_start) +} + +fn line_is_comment(bytes: &[u8], offset: usize) -> bool { + let line_start = bytes + .get(..offset.min(bytes.len())) + .and_then(|prefix| prefix.iter().rposition(|byte| *byte == b'\n')) + .map_or(0, |pos| pos + 1); + bytes[line_start..offset.min(bytes.len())] + .iter() + .copied() + .find(|byte| !byte.is_ascii_whitespace()) + == Some(b'#') +} + +fn looks_like_sha256_base64(source: &str, start: usize, end: usize) -> bool { + let candidate = &source[start..end]; + (candidate.len() == 44 && candidate.ends_with('=')) + || (candidate.len() == 43 && source.as_bytes().get(end) == Some(&b'=')) +} diff --git a/crates/clarion-scanner/tests/scanner.rs b/crates/clarion-scanner/tests/scanner.rs new file mode 100644 index 00000000..c031728a --- /dev/null +++ b/crates/clarion-scanner/tests/scanner.rs @@ -0,0 +1,232 @@ +use clarion_scanner::{Baseline, BaselineError, Scanner, load_baseline}; + +fn rules_for(input: &str) -> Vec<&'static str> { + Scanner::new() + .scan_bytes(input.as_bytes()) + .into_iter() + .map(|detection| detection.rule_id) + .collect() +} + +fn assert_detects(input: &str, rule_id: &str) { + let rules = rules_for(input); + assert!(rules.contains(&rule_id), "{rule_id} not found in {rules:?}"); +} + +fn assert_not_detects(input: &str, rule_id: &str) { + let rules = rules_for(input); + assert!( + !rules.contains(&rule_id), + "{rule_id} unexpectedly found in {rules:?}" + ); +} + +#[test] +fn named_patterns_detect_expected_credentials() { + assert_detects( + "aws_access_key_id = 'AKIAIOSFODNN7EXAMPLE'", + "AwsAccessKeyId", + ); + assert_detects( + "aws_secret_access_key = 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'", + "AwsSecretAccessKey", + ); + assert_detects( + "token = 'ghp_abcdefghijklmnopqrstuvwxyzABCDEFGHIJ'", + "GitHubPat", + ); + assert_detects( + &format!("token = 'github_pat_{}'", "a".repeat(82)), + "GitHubFineGrainedPat", + ); + assert_detects( + "token = 'gho_abcdefghijklmnopqrstuvwxyzABCDEFGHIJ'", + "GitHubOAuth", + ); + assert_detects( + &format!("key = 'sk-ant-{}'", "A".repeat(90)), + "AnthropicApiKey", + ); + assert_detects(&format!("key = 'sk-{}'", "A".repeat(48)), "OpenAiApiKey"); + assert_detects("stripe = 'sk_live_abcdefghijklmnop'", "StripeApiKey"); + assert_detects("slack = 'xoxb-123456789012-abcdefghi'", "SlackToken"); + assert_detects( + "jwt = 'eyJabcdef12345.eyJabcdef67890.eyJabcdef99999'", + "JwtToken", + ); + assert_detects("-----BEGIN RSA PRIVATE KEY-----", "PrivateKeyHeader"); + assert_detects("-----BEGIN OPENSSH PRIVATE KEY-----", "PrivateKeyHeader"); +} + +#[test] +fn named_patterns_ignore_near_misses() { + assert_not_detects("AKIAIOSFODNN7EXAMPL", "AwsAccessKeyId"); + assert_not_detects("ghp_short", "GitHubPat"); + assert_not_detects("sk-not-long-enough", "OpenAiApiKey"); + assert_not_detects("-----BEGIN PUBLIC KEY-----", "PrivateKeyHeader"); +} + +#[test] +fn entropy_detection_has_expected_bounds() { + assert_detects( + "secret = AbCdEfGhIjKlMnOpQrStUvWxYz123456+/", + "HighEntropyBase64", + ); + assert_detects( + "digest = 0123456789abcdefABCDEF0123456789abcdefABCDEF0123456789abcdef", + "HighEntropyHex", + ); + assert_not_detects( + "uuid = 123e4567-e89b-12d3-a456-426614174000", + "HighEntropyHex", + ); + assert_not_detects( + "checksum = 47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=", + "HighEntropyBase64", + ); +} + +#[test] +fn contextual_credentials_detect_assignments_but_not_comments_or_hashes() { + assert_detects( + "password = \"correct-horse-battery-staple\"", + "ContextualCredential", + ); + assert_detects("api_key: \"abcDEF1234567890\"", "ContextualCredential"); + assert_detects( + "SECRET_TOKEN := \"abcDEF1234567890\"", + "ContextualCredential", + ); + assert_not_detects( + "password_hash = \"abcDEF1234567890\"", + "ContextualCredential", + ); + assert_not_detects("# password = \"abcDEF1234567890\"", "ContextualCredential"); +} + +#[test] +fn detection_records_line_and_sha1_hash_without_literal() { + let detections = Scanner::new().scan_bytes(b"clean\nkey = 'AKIAIOSFODNN7EXAMPLE'\n"); + let detection = detections + .iter() + .find(|detection| detection.rule_id == "AwsAccessKeyId") + .expect("AWS key detection"); + assert_eq!(detection.line_number, 2); + assert_eq!(detection.matched_len, "AKIAIOSFODNN7EXAMPLE".len()); + assert_ne!(detection.hashed_secret, [0u8; 20]); +} + +#[test] +fn baseline_suppresses_matching_detection_and_reports_fired_entry() { + let scanner = Scanner::new(); + let detections = scanner.scan_bytes(b"key = 'AKIAIOSFODNN7EXAMPLE'\n"); + let detection = detections + .iter() + .find(|detection| detection.rule_id == "AwsAccessKeyId") + .expect("AWS detection") + .clone(); + let baseline = Baseline::from_yaml_str(&format!( + r#" +version: "1.0" +results: + "src/demo.py": + - type: "AWS Access Key" + hashed_secret: "{}" + line_number: 1 + is_secret: false + justification: "Documented public AWS example key." +"#, + hex20(detection.hashed_secret) + )) + .expect("baseline parses"); + + let result = baseline.suppress(detections, std::path::Path::new("/repo/src/demo.py")); + assert!(result.allowed.is_empty()); + assert_eq!(result.suppressed.len(), 1); + assert_eq!(result.fired_entries.len(), 1); +} + +#[test] +fn baseline_is_secret_true_does_not_suppress() { + let scanner = Scanner::new(); + let detections = scanner.scan_bytes(b"key = 'AKIAIOSFODNN7EXAMPLE'\n"); + let detection = detections + .iter() + .find(|detection| detection.rule_id == "AwsAccessKeyId") + .expect("AWS detection") + .clone(); + let baseline = Baseline::from_yaml_str(&format!( + r#" +version: "1.0" +results: + "demo.py": + - type: "AWS Access Key" + hashed_secret: "{}" + line_number: 1 + is_secret: true + justification: "Still a real secret." +"#, + hex20(detection.hashed_secret) + )) + .expect("baseline parses"); + + let result = baseline.suppress(detections, std::path::Path::new("demo.py")); + assert_eq!(result.allowed.len(), 1); + assert!(result.suppressed.is_empty()); +} + +#[test] +fn baseline_missing_justification_errors() { + let err = Baseline::from_yaml_str( + r#" +version: "1.0" +results: + "src/demo.py": + - type: "AWS Access Key" + hashed_secret: "0123456789abcdef0123456789abcdef01234567" + line_number: 42 + is_secret: false +"#, + ) + .expect_err("missing justification should fail"); + assert!(matches!( + err, + BaselineError::MissingJustification { line: 42, .. } + )); +} + +#[test] +fn absent_baseline_file_is_empty() { + let dir = tempfile::tempdir().expect("tempdir"); + let baseline = load_baseline(&dir.path().join(".clarion/secrets-baseline.yaml")) + .expect("missing baseline is accepted"); + assert!(baseline.entries().is_empty()); +} + +#[test] +fn baseline_round_trips_through_yaml() { + let raw = r#" +version: "1.0" +results: + "src/demo.py": + - type: "AWS Access Key" + hashed_secret: "0123456789abcdef0123456789abcdef01234567" + line_number: 42 + is_secret: false + justification: "Example key." +"#; + let parsed = Baseline::from_yaml_str(raw).expect("parse baseline"); + let rendered = parsed.to_yaml_string().expect("serialize baseline"); + let reparsed = Baseline::from_yaml_str(&rendered).expect("reparse baseline"); + assert_eq!(parsed, reparsed); +} + +fn hex20(bytes: [u8; 20]) -> String { + const HEX: &[u8; 16] = b"0123456789abcdef"; + let mut out = String::new(); + for byte in bytes { + out.push(char::from(HEX[usize::from(byte >> 4)])); + out.push(char::from(HEX[usize::from(byte & 0x0f)])); + } + out +} diff --git a/docs/clarion/v0.1/detailed-design.md b/docs/clarion/v0.1/detailed-design.md index 4fb249eb..81580f4e 100644 --- a/docs/clarion/v0.1/detailed-design.md +++ b/docs/clarion/v0.1/detailed-design.md @@ -1050,6 +1050,18 @@ These rules combine signals Clarion uniquely holds — clusters from Phase 3, Wa **`CLA-FACT-TIER-SUBSYSTEM-MIXING` threshold**: default `min_outlier_count: 2` and `min_outlier_fraction: 0.1` (i.e., at least 2 outliers AND at least 10% of subsystem members). Configurable via `clarion.yaml:analysis.clustering.tier_mixing_thresholds`. Tuned to avoid flagging subsystems where a single outlier is legitimate boundary-infrastructure (e.g., an `EXTERNAL_RAW` parser adjacent to an otherwise-`INTEGRAL` validation subsystem). +### Pre-ingest secret-scanning findings (WP5) + +These core-emitted rules are the ADR-013 audit surface. ADR-013 remains canonical for scanner behaviour; this catalogue records the emitted finding IDs. + +| Rule | Severity | Category | Description | Remediation | ADR | +|---|---|---|---|---|---| +| `CLA-SEC-SECRET-DETECTED` | ERROR | security | Pre-ingest secret scanner detected a credential pattern in a file slated for LLM dispatch. | Remove the secret, rotate the credential, or whitelist via `.clarion/secrets-baseline.yaml` with a justification. | [ADR-013](../adr/ADR-013-pre-ingest-secret-scanner.md) | +| `CLA-SEC-UNREDACTED-SECRETS-ALLOWED` | ERROR | security | Operator invoked `--allow-unredacted-secrets`; file content reached the LLM provider with secrets intact. | Audit override usage via `filigree list --rule-id=CLA-SEC-UNREDACTED-SECRETS-ALLOWED --since 30d`. | [ADR-013](../adr/ADR-013-pre-ingest-secret-scanner.md) | +| `CLA-INFRA-SECRET-BASELINE-NO-JUSTIFICATION` | ERROR | infra | Baseline entry missing required `justification` field; entry not honoured. | Add a `justification` string explaining why the match is safe. | [ADR-013](../adr/ADR-013-pre-ingest-secret-scanner.md) | +| `CLA-INFRA-SECRET-BASELINE-MATCH` | INFO | infra | Baseline entry suppressed a scanner detection as an audit event. | None; informational and retained for `NFR-SEC-04` audit. | [ADR-013](../adr/ADR-013-pre-ingest-secret-scanner.md) | +| `CLA-INFRA-SECRET-OVERRIDE-UNCONFIRMED` | ERROR | infra | `--allow-unredacted-secrets` supplied without confirmation; run aborted before start. | Supply `--confirm-allow-unredacted-secrets=yes-i-understand` in non-TTY contexts or run interactively. | [ADR-013](../adr/ADR-013-pre-ingest-secret-scanner.md) | + ### Entity-set diff (deletion detection) At Phase 7, Clarion compares the current run's entity IDs against the prior run's set (read from the prior `run_id`'s stats; if no prior run exists, this phase is a no-op). For each entity ID present before and absent now: @@ -1380,6 +1392,9 @@ Every security-relevant event emits a finding: - `CLA-SEC-SECRET-DETECTED` — unredacted secret blocked LLM dispatch - `CLA-SEC-UNREDACTED-SECRETS-ALLOWED` — operator overrode block with `--allow-unredacted-secrets` +- `CLA-INFRA-SECRET-BASELINE-NO-JUSTIFICATION` — baseline entry lacks the required review rationale +- `CLA-INFRA-SECRET-BASELINE-MATCH` — baseline entry suppressed a scanner hit +- `CLA-INFRA-SECRET-OVERRIDE-UNCONFIRMED` — override flag was not confirmed, so the run aborted before start - `CLA-INFRA-TOKEN-STORAGE-DEGRADED` — OS keychain unavailable, fell back to file-mode `0600` - `CLA-INFRA-BRIEFING-INVALID` — LLM returned schema-invalid content twice, possible injection - `CLA-SEC-VOCABULARY-CANDIDATE-NOVEL` — novel `patterns`/`antipatterns` tag proposed by LLM (light signal; mostly harmless) diff --git a/docs/implementation/v0.1-publish/ws-a-secret-scanner.md b/docs/implementation/v0.1-publish/ws-a-secret-scanner.md new file mode 100644 index 00000000..5f82376a --- /dev/null +++ b/docs/implementation/v0.1-publish/ws-a-secret-scanner.md @@ -0,0 +1,1023 @@ +# Workstream A — WP5 Pre-Ingest Secret Scanner (detailed package) + +**Status**: DRAFT — not yet seeded into Filigree; awaiting kickoff. +**Predecessor**: [`thread-1-pre-publish-blockers.md` §2](./thread-1-pre-publish-blockers.md#2-workstream-a--wp5-pre-ingest-secret-scanner) +**Successor**: Workstream D — external-operator smoke test (publish gate). +**Spec source**: [ADR-013 — Pre-Ingest Secret Scanner with LLM-Dispatch Block](../../clarion/adr/ADR-013-pre-ingest-secret-scanner.md) (Accepted; do not re-litigate). +**Anchoring ADRs**: [ADR-007 (cache key)](../../clarion/adr/ADR-007-summary-cache-key.md), [ADR-013](../../clarion/adr/ADR-013-pre-ingest-secret-scanner.md), [ADR-017 (severity + dedup)](../../clarion/adr/ADR-017-severity-and-dedup.md), [ADR-021 (plugin authority hybrid)](../../clarion/adr/ADR-021-plugin-authority-hybrid.md), [ADR-022 (core/plugin ontology)](../../clarion/adr/ADR-022-core-plugin-ontology.md), [ADR-023 (tooling baseline)](../../clarion/adr/ADR-023-tooling-baseline.md). +**Requirements floor**: `NFR-SEC-01` (pre-ingest scan + block + baseline), `NFR-SEC-04` (security events as findings), `NFR-OPS-01` / `NFR-OPS-04` (single-binary distribution). +**Effort estimate**: 6–9 working days at agentic velocity. Tasks 1 and 2 parallelisable; 3 → 4 → 5 sequential; 6 + 7 parallel with any. + +--- + +## 1. Purpose + +WP5 closes the design-review CRITICAL flag on secret exfiltration: every byte +reaching the LLM provider must first pass a Clarion-owned scanner. ADR-013 +locked the behaviour; this document turns the ADR into seven implementation +tasks an agent (or two agents working in parallel) can execute, and pins the +file paths, public APIs, schema changes, rule-ID catalogue entries, and tests +each task is responsible for. + +The workstream is the only one of Thread-1's four with significant +engineering weight; B and C are docs + packaging. WP5 ships green before the +publish-gate smoke test in Workstream D can attempt step 8 (the planted-`.env` +case). + +## 2. Scope + +In scope (this work package): + +- New workspace crate `crates/clarion-scanner/` implementing the ADR-013 + rule set as Rust-native regex + Shannon entropy detection. +- Baseline parser for `.clarion/secrets-baseline.yaml` (`detect-secrets` + v1.x format). +- CLI wiring: pre-ingest pass in `clarion-cli::analyze::run` between + source-tree walk and per-plugin dispatch. +- `--allow-unredacted-secrets` override surface with TTY + non-TTY gates. +- MCP-side awareness of `briefing_blocked: secret_present` in + `crates/clarion-mcp/`'s `summary` tool dispatch. +- Five new `CLA-SEC-*` / `CLA-INFRA-SECRET-*` rule-IDs in the + `detailed-design.md` §5 catalogue. +- Operator documentation under `docs/operator/secret-scanning.md`. + +Out of scope (deferred or owned elsewhere): + +- Filigree emission of `CLA-SEC-*` findings — WP9-B, v0.2. +- Custom-rule integration from Wardline — v0.2. +- Post-ingest scanning on LLM responses — v0.2+ additive defence. +- Redaction-instead-of-block (ADR-013 Alternative 2) — v0.2+ with opt-in. +- The override-finding monitoring loop (filigree-side dashboard work) — not + part of v0.1 publish. +- Windows support — `setrlimit` is Unix-only (`clarion-core::plugin::limits`); + WP5 follows the same posture. + +## 3. Locked surfaces and pre-existing context + +WP5 reads from and writes to surfaces already present at `main`: + +- Workspace at `Cargo.toml` (resolver 3, Rust 2024, MSRV 1.88). Current + members: `clarion-core`, `clarion-storage`, `clarion-cli`, `clarion-mcp`, + `clarion-plugin-fixture`. The scanner becomes the **sixth**. +- `crates/clarion-cli/src/analyze.rs` already contains: + - `pub async fn run(project_path: PathBuf) -> Result<()>` at line 52. + - `fn collect_source_files(root, wanted_extensions) -> Vec` at + line 1740 — the walk WP5's pre-ingest hook intercepts immediately after. + - `RunOutcome::SoftFailed { reason }` at line 1038 — the partial-success + path the override-unconfirmed exit must NOT take (override-unconfirmed + runs never start a `runs` row at all). +- `crates/clarion-core/src/plugin/host.rs`: + - `pub struct RawEntity { … extra … }` at line 105 — the `extra` field is + the carrier for `briefing_blocked`. Per the host doc-comment at line 74 + and 222, `extra` flows into `properties_json` downstream. + - `pub struct HostFinding` at `plugin/host_findings.rs:84` with subcode + constructors (`HostFinding::malformed_entity`, etc.) — the pattern WP5's + finding constructors follow. +- `crates/clarion-mcp/src/lib.rs`: + - `fn entity_properties_json(entity: &EntityRow) -> Value` at line 2382 — + central reader for `properties_json`; WP5's MCP awareness layer hooks + above this, before LLM dispatch. + - `fn summary_scope_deferred(entity: &EntityRow) -> Value` at line 2341 — + precedent for the "absence is policy" envelope shape WP5 introduces + (`briefing_blocked: secret_present`). +- `serde_norway` is already a workspace dep (replaced `serde_yaml` per + commit `9ffc5c8`). WP5 reuses it for baseline YAML. + +Surfaces this package does NOT touch: + +- Plugin protocol (no `analyze_file` RPC change). +- Writer-actor command set in `crates/clarion-storage/` — the scanner + produces `HostFinding`s and entity `extra` map entries; no new + `WriterCmd` variants. +- SQLite schema — no new tables or columns. `entities.properties` and + `runs.stats` are existing JSON-bearing columns; WP5 adds key/value pairs. + +### 3.1 Finding-write channel (load-bearing distinction) + +The codebase has two distinct "finding" concepts; WP5 emits into one of them. + +- **`HostFinding`** (`crates/clarion-core/src/plugin/host_findings.rs:84`) is + a plugin-host **integrity event**: subcode + message + structured metadata + map. It carries no severity, no `rule_id`, no `(file, line)` slot. It is + drained via `PluginHost::take_findings` and logged through + `log_plugin_findings` (`analyze.rs:1042`). Per the doc-comment at line + 79–82, "for Sprint 1 they are collected only" — they are NOT persisted to + the `findings` table. +- **`FindingRecord`** (`crates/clarion-storage/src/commands.rs:113`) is the + ADR-004 finding shape that the writer-actor persists. It carries + `rule_id`, `severity`, `entity_id`, `message`, `evidence_json`, + `properties_json`, lifecycle status, etc. Write path: send + `WriterCmd::InsertFinding { finding: Box, ack }` through + the existing `send_wait` channel. + +**WP5 emits `FindingRecord` via `WriterCmd::InsertFinding`.** Precedent: +`insert_weak_modularity_finding` at `analyze.rs:907–965` is the existing +pattern for a CLI-side, application-level finding emitted directly through +the writer. WP5's `secret_scan` module follows that pattern verbatim — +build a `FindingRecord`, send via the writer's command channel, await ack. + +`HostFinding` is **not** the channel for `CLA-SEC-*` / `CLA-INFRA-SECRET-*` +because (a) it lacks the severity / rule-ID slots ADR-013 mandates and +(b) it is not persisted. The Thread-1 §2.A.1 reference to "the existing +`HostFinding` → `WriterCmd::InsertEntity` path" is imprecise; the +authoritative channel is `WriterCmd::InsertFinding`. + +For each detection, the `FindingRecord` is populated: + +| Field | Value | +|---|---| +| `id` | `uuid::Uuid::new_v4().to_string()` | +| `tool` | `"clarion"` | +| `tool_version` | `env!("CARGO_PKG_VERSION")` | +| `run_id` | current run's `run_id` | +| `rule_id` | `"CLA-SEC-SECRET-DETECTED"` (or other; see §6 catalogue) | +| `kind` | `"defect"` (per ADR-004; secrets are defects pending remediation) | +| `severity` | `"error"` (or `"info"` for `CLA-INFRA-SECRET-BASELINE-MATCH`) | +| `confidence` | `Some(1.0)` for named-pattern matches; `Some(0.6)` for high-entropy detections (rough heuristic — refine in Task 1) | +| `confidence_basis` | `Some("pattern")` or `Some("entropy")` | +| `entity_id` | empty string (file-level, not entity-bound; the file path is in `evidence_json`) | +| `related_entities_json` | `"[]"` | +| `message` | one-line human-readable, e.g. `"AWS access key detected in src/api/keys.py:42"` (the literal bytes NEVER appear in this string) | +| `evidence_json` | `{"file_path": "...", "line_number": 42, "rule": "AwsAccessKeyId", "hashed_secret_hex": "..."}` | +| `properties_json` | `"{}"` | +| `supports_json` / `supported_by_json` | `"[]"` | +| `created_at` / `updated_at` | run's `started_at` (deterministic) | + +## 4. Architecture at a glance + +``` +clarion analyze + │ + ├─ collect_source_files(...) [unchanged] + │ + ├─ secret_scan::pre_ingest(source_files, baseline) ──► WP5 entry point + │ │ + │ ├─ For each file: + │ │ 1. mmap or read buffer + │ │ 2. Scanner::scan_bytes(&buf) ─► Vec + │ │ 3. baseline.suppress(detections, path) + │ │ 4. partition allowed/suppressed + │ │ + │ ├─ If TTY and allowed.is_empty() == false: + │ │ interactive prompt (per ADR-013 §Override) + │ │ + │ ├─ Returns: + │ │ - blocked: BTreeMap> + │ │ - findings: Vec (SEC-* + INFRA-SECRET-*) + │ │ - override_state: OverrideState + │ │ + │ └─ If override_unconfirmed → return Err → exit 78 (EX_CONFIG) + │ + ├─ run_plugins(..., blocked) ─► plugins still analyse blocked files; + │ host stamps `briefing_blocked` into + │ RawEntity.extra for blocked files + │ + ├─ writer persists entities (properties_json carries the flag) + │ + └─ summary_pass / MCP serve / etc. → read briefing_blocked, + skip LLM dispatch +``` + +Two invariants hold for any reader at any time: + +- The scanner runs **before** any plugin process is spawned. ADR-013 + §"Plugin-boundary interaction" — file buffers reach Anthropic only via + Phase 4–6 summarisation, which is gated by `briefing_blocked`. +- The override path **never** silently bypasses. Non-TTY without confirm + flag exits 78 *before* a `runs` row is created. + +## 5. Task breakdown + +Each task fits one agent pass (≤ ~500 LOC plus tests). Tasks 1 and 2 are +independent and can parallelise; 3 depends on 1 + 2; 4 depends on 3; 5 +depends on 3; 6 + 7 are documentation, parallel with any. + +### Task 1 — Scanner crate, rule registry, entropy + +**Owner**: `crates/clarion-scanner/` +**Estimated size**: 350–500 LOC including tests. + +#### Files + +| Action | Path | +|---|---| +| create | `crates/clarion-scanner/Cargo.toml` | +| create | `crates/clarion-scanner/src/lib.rs` | +| create | `crates/clarion-scanner/src/patterns.rs` | +| create | `crates/clarion-scanner/src/entropy.rs` | +| create | `crates/clarion-scanner/tests/fixtures/*.txt` (positive + negative per rule) | +| create | `crates/clarion-scanner/tests/scanner.rs` | +| modify | `Cargo.toml` (workspace root) — add `crates/clarion-scanner` member | + +#### Scope + +Implement the named-credential regex table from ADR-013 lines 35–38 (AWS +access keys, AWS secret adjacency, GitHub PATs / OAuth tokens, Anthropic +keys, OpenAI keys, Stripe keys, Slack tokens, JWT, RSA/EC/DSA/OpenSSH +private-key headers, contextual-credential `name=value` patterns). +Implement Shannon entropy detection over byte slices with the bounds +ADR-013 §Implementation specifies (base64 ≥ 4.5 entropy over ≥20 chars; hex +≥ 3.0 over ≥40 chars). UUIDs and short tokens must NOT trip the entropy +rule (positive/negative fixtures enforce this). + +#### Public surface + +```rust +pub struct Scanner { + patterns: regex::RegexSet, + pattern_meta: Vec, + entropy_b64: EntropyTuning, + entropy_hex: EntropyTuning, +} + +pub struct Detection { + pub rule_id: &'static str, // e.g. "AwsAccessKeyId", "HighEntropyBase64" + pub category: SecretCategory, + pub byte_offset: usize, + pub line_number: u32, + pub matched_len: usize, // never persist the literal bytes + pub hashed_secret: [u8; 20], // sha1, baseline-compat +} + +pub enum SecretCategory { + CloudCredential, // AWS, GCP, Azure + VcsCredential, // GitHub PATs, OAuth + AiProviderCredential, // Anthropic, OpenAI + PaymentsCredential, // Stripe + MessagingCredential, // Slack + PrivateKey, // RSA/EC/DSA/OpenSSH/PGP + JwtToken, + HighEntropy, + ContextualCredential, // password=, api_key=... +} + +impl Scanner { + pub fn new() -> Self; // default thresholds per ADR-013 + pub fn scan_bytes(&self, buf: &[u8]) -> Vec; +} +``` + +`Detection::hashed_secret` is sha1 over the literal matched bytes, computed +**at detection time**; the literal never lives anywhere downstream. This is +the same convention `detect-secrets` uses so baseline files round-trip. + +#### Dependency budget + +The scanner crate is intentionally a leaf: + +- `regex` (workspace already pulls it for elsewhere) +- `sha1` (new — small, well-maintained) +- No `tokio`, no `rusqlite`, no `serde_norway`. + +Verification at exit: `cargo tree -p clarion-scanner | head -30` shows no +`tokio` or `rusqlite` ancestors. If a downstream crate's `regex` features +leak something heavier, pin the feature set in `clarion-scanner`'s +`Cargo.toml`. + +#### Tests + +`crates/clarion-scanner/tests/scanner.rs`: + +- One positive + one negative fixture per named pattern (AWS access key, + GitHub PAT `ghp_…`, GitHub fine-grained `github_pat_…`, Anthropic + `sk-ant-…`, OpenAI `sk-…`, Stripe `sk_live_…`, Slack `xoxb-…`, JWT + `eyJ…`, RSA private-key header, OpenSSH private-key header). +- High-entropy positives: 32-char random base64, 64-char random hex. +- High-entropy negatives: UUIDs, base64-encoded SHA256 checksums (these + are high-entropy but commonly safe — confirm whether to suppress; if + not, document in the operator doc as a known-baseline candidate). +- Contextual-credential positives: `password = "..."`, `api_key: "…"`, + `SECRET_TOKEN := "…"`. +- Contextual-credential negatives: `password_hash = "…"` (post-hash safe), + `# password placeholder` (comments). + +#### Exit criteria + +- `cargo test -p clarion-scanner` green. +- `cargo clippy -p clarion-scanner -- -D warnings` clean. +- `cargo tree -p clarion-scanner` shows no `tokio` / `rusqlite` / + `serde_norway` deps. +- `cargo build --workspace` green (new crate compiles into the workspace + without changing existing build). + +--- + +### Task 2 — Baseline parser (`.clarion/secrets-baseline.yaml`) + +**Owner**: `crates/clarion-scanner/` +**Estimated size**: 200–300 LOC including tests. +**Parallelisable with Task 1**: yes — touches separate module. + +#### Files + +| Action | Path | +|---|---| +| create | `crates/clarion-scanner/src/baseline.rs` | +| modify | `crates/clarion-scanner/src/lib.rs` (re-export) | +| create | `crates/clarion-scanner/tests/fixtures/baselines/*.yaml` | +| modify | `crates/clarion-scanner/tests/scanner.rs` OR new `tests/baseline.rs` | + +#### Scope + +Parse the `detect-secrets` v1.x baseline schema (ADR-013 §Baseline). Schema +fields required: + +```yaml +version: "1.0" # must equal "1.0" +results: + "": # repository-relative path + - type: "" + hashed_secret: "" + line_number: 42 + is_secret: false # operator declares not-a-secret + justification: "..." # REQUIRED (ADR-013 line 71) +``` + +`justification` missing → emit `CLA-INFRA-SECRET-BASELINE-NO-JUSTIFICATION` +(at load time, surfaces during CLI startup; one finding per offending +entry). + +Match-then-suppress at `Detection` granularity, keyed on +`(file_path, hashed_secret, line_number)`. Both `allowed` and `suppressed` +partitions are retained so the CLI can emit one `CLA-SEC-SECRET-DETECTED` +per unsuppressed detection AND one `CLA-INFRA-SECRET-BASELINE-MATCH` +info-level finding per *actually-fired* baseline entry (the audit-surface +requirement of `NFR-SEC-04`). + +`is_secret: true` in a baseline entry is treated as **not a suppression** — +the operator is declaring the entry IS a real secret they haven't fixed +yet; behaviour is identical to no baseline entry at all. (This mirrors +`detect-secrets`' convention.) + +#### Public surface + +```rust +pub struct Baseline { + version: String, + entries: BTreeMap>, +} + +pub struct BaselineEntry { + pub rule_type: String, // detect-secrets type name + pub hashed_secret: [u8; 20], + pub line_number: u32, + pub is_secret: bool, + pub justification: String, +} + +pub struct SuppressionResult { + pub allowed: Vec, // detections that survive suppression + pub suppressed: Vec, // detections suppressed by a baseline entry + pub fired_entries: Vec, // baseline entries that actually matched +} + +pub fn load_baseline(path: &Path) -> Result; + +impl Baseline { + pub fn empty() -> Self; // path-absent path (NOT an error) + pub fn suppress( + &self, + detections: Vec, + file: &Path, + ) -> SuppressionResult; +} + +#[derive(Debug, thiserror::Error)] +pub enum BaselineError { + #[error("baseline version mismatch: expected 1.0, got {0}")] + UnsupportedVersion(String), + #[error("baseline entry missing required field 'justification' at {file}:{line}")] + MissingJustification { file: PathBuf, line: u32 }, + #[error("baseline parse error: {0}")] + Parse(#[from] serde_norway::Error), + #[error("baseline I/O error: {0}")] + Io(#[from] std::io::Error), +} +``` + +`load_baseline` returns `Ok(Baseline::empty())` if the file does not exist +(this is the common case — operators add a baseline only after the first +false positive). It errors only on parse failure, version mismatch, or +missing required fields. + +#### Tests + +- Fixture baseline + scanner detections → asserts `allowed` and + `suppressed` partitions match expected. +- Missing `justification` → `BaselineError::MissingJustification` returned. +- Baseline file absent → `Baseline::empty()` returned, no error. +- Round-trip: parse → serialise → parse → byte-identical (subject to + YAML serialisation determinism with `serde_norway`). +- Path-relativity test: baseline keys are repository-relative; scanner + passes absolute paths. The suppression layer normalises before matching. + Document the normalisation rule and assert it. + +#### Exit criteria + +- `cargo test -p clarion-scanner` (combined with Task 1) green. +- Round-trip baseline test passes. +- `BaselineError` is the only error type leaking out of the module; no + bare `anyhow::Error` at the public surface. + +--- + +### Task 3 — CLI wiring: pre-ingest hook in `analyze::run` + +**Owner**: `crates/clarion-cli/` +**Estimated size**: 300–400 LOC including the new orchestration module + tests. +**Depends on**: Task 1, Task 2. + +#### Files + +| Action | Path | +|---|---| +| create | `crates/clarion-cli/src/secret_scan.rs` (orchestration module — keep `analyze.rs` from growing further per arch-analysis H-1) | +| modify | `crates/clarion-cli/src/analyze.rs` | +| modify | `crates/clarion-cli/Cargo.toml` (add `clarion-scanner` dep) | +| modify | `crates/clarion-core/src/plugin/host.rs` (stamp `briefing_blocked` into RawEntity.extra for blocked files — see "Host integration" below) | +| create | `crates/clarion-cli/tests/secret_scan.rs` (integration tests) | +| create | `crates/clarion-cli/tests/fixtures/secret-project/...` (fixture trees) | + +#### Scope + +Between `collect_source_files` (`analyze.rs:1740`) and the per-plugin +processing loop, insert a pre-ingest scan pass: + +1. Load `.clarion/secrets-baseline.yaml` via Task 2's `load_baseline`. +2. For each file in `source_files`: + a. Read the file buffer. + b. `Scanner::scan_bytes(&buf)` → `Vec`. + c. `baseline.suppress(detections, path)` → `SuppressionResult`. +3. Build a `BlockSet: BTreeMap` of files with + non-empty `allowed`. `BlockReason` is an enum with a single variant at + v0.1: + + ```rust + pub enum BlockReason { + SecretPresent, // ADR-013: rendered as "secret_present" in properties_json + } + ``` + + The string-on-the-wire form is `"secret_present"`. Future variants + (e.g. `SizeCapExceeded`) would add new string values; readers treat + `briefing_blocked` as open-vocabulary. + +4. Emit findings via `WriterCmd::InsertFinding` (see §3.1): + - One `CLA-SEC-SECRET-DETECTED` per `allowed` detection (severity + `error`). + - One `CLA-INFRA-SECRET-BASELINE-MATCH` per `fired_entries` element + (severity `info`). + - One `CLA-INFRA-SECRET-BASELINE-NO-JUSTIFICATION` per + `BaselineError::MissingJustification` surfaced at load time. +5. Pass the `BlockSet` through to the per-plugin processing loop so the + plugin host can stamp `briefing_blocked: secret_present` into the + `RawEntity.extra` for each entity coming out of those files. + +#### Host integration + +The plugin host (`crates/clarion-core/src/plugin/host.rs`) currently +accepts `RawEntity` from the plugin and translates `extra` into +`properties_json` downstream (host.rs:74, 222). WP5 adds one path: + +- The host receives a `BTreeMap` at spawn time (via + a new field on the existing host configuration struct, or via a setter — + pick the smaller diff). For each entity whose `source.path` is in the + block map, the host inserts `"briefing_blocked": "secret_present"` into + the entity's `extra` map **after** the plugin returns, **before** + serialisation to `properties_json`. + +This keeps the per-plugin code untouched. Plugins do not need to know +about secret scanning; the host stamps the flag on its way through. + +Rationale: the alternative (passing block info to the plugin so the plugin +emits the flag) would require changing the plugin protocol — out of scope +for v0.1. + +#### Tests + +`crates/clarion-cli/tests/secret_scan.rs`: + +1. **Happy path — clean project**: fixture with no secrets → `analyze` + exits 0; no `CLA-SEC-*` findings persisted. +2. **One file with AWS key in `.env`**: fixture with a committed `.env` + containing `AKIAIOSFODNN7EXAMPLE` → + - `analyze` exits 0 with `RunOutcome::Completed`. Secret detection + produces findings but does **not** affect the run outcome — the + structural extraction succeeded, the security finding is the + audit signal, and `SoftFailed` is reserved for actual plugin + failures. + - `entities` table has rows for the `.env` file's structural entities. + - Those entities' `properties_json` contains + `"briefing_blocked":"secret_present"`. + - `findings` table has one `CLA-SEC-SECRET-DETECTED` row referencing + the file. +3. **Baseline suppression**: same `.env` fixture + a `.clarion/secrets-baseline.yaml` + with the matching entry → no `CLA-SEC-SECRET-DETECTED` finding; one + `CLA-INFRA-SECRET-BASELINE-MATCH` info-level finding; no + `briefing_blocked` flag on entities. +4. **Baseline missing justification**: malformed baseline → + `CLA-INFRA-SECRET-BASELINE-NO-JUSTIFICATION` finding; analyze + completes with `RunOutcome::Completed` (load errors degrade to + "treat baseline as empty + surface finding", they do not abort the + run and do not promote it to `SoftFailed`). +5. **Multi-file project**: secret in 1 of 10 files → 9 unblocked, 1 + blocked; entity counts match expectation. + +#### Exit criteria + +- All five integration tests green. +- `crates/clarion-cli/src/secret_scan.rs` ≤ 250 LOC (the orchestration + module; arch-analysis H-1 ceiling). +- `analyze.rs` net growth ≤ 30 LOC (the new module owns the body; analyze + delegates). +- The walking-skeleton E2E (`tests/e2e/sprint_1_walking_skeleton.sh`) + still passes — clean fixture must not regress. +- `cargo clippy --workspace --all-targets --all-features -- -D warnings` + clean. + +--- + +### Task 4 — Override semantics: `--allow-unredacted-secrets` + +**Owner**: `crates/clarion-cli/` +**Estimated size**: 250–350 LOC including tests. +**Depends on**: Task 3. + +#### Files + +| Action | Path | +|---|---| +| modify | `crates/clarion-cli/src/cli.rs` (clap definition) | +| modify | `crates/clarion-cli/src/secret_scan.rs` (override logic) | +| modify | `crates/clarion-cli/tests/secret_scan.rs` (override tests) | + +#### Scope + +Implement the override path per ADR-013 §Override. + +CLI surface (additions to the `analyze` subcommand): + +```rust +#[derive(Args)] +pub struct AnalyzeArgs { + // ... existing fields ... + /// Allow analysis of files containing unredacted secrets. REQUIRES a + /// confirmation step (interactive prompt or --confirm-allow-unredacted-secrets). + #[arg(long)] + pub allow_unredacted_secrets: bool, + + /// Non-TTY confirmation token for --allow-unredacted-secrets. + /// Must be the literal string "yes-i-understand". + #[arg(long, value_name = "TOKEN", requires = "allow_unredacted_secrets")] + pub confirm_allow_unredacted_secrets: Option, +} +``` + +Behaviour: + +- **No detections**: flag is a no-op (do NOT prompt; do NOT emit override + findings). Document this in the operator doc so operators can safely + leave the flag set in CI configurations. +- **Detections present, no override flag**: existing block behaviour + (Task 3) — `briefing_blocked` stamped, `CLA-SEC-SECRET-DETECTED` + emitted, analyze continues for structural pass. +- **Detections present, `--allow-unredacted-secrets` only, TTY**: + - Print detection summary to stderr (file path, line, rule-ID, but + NEVER the matched bytes). + - Prompt: `Type 'yes-i-understand' to proceed: `. + - Match → proceed without blocking; emit + `CLA-SEC-UNREDACTED-SECRETS-ALLOWED` per affected file; record + `override_used: true, files_affected: [...]` in `runs.stats`. + - Anything else (EOF, mismatched string, `^C`) → exit 78 (`EX_CONFIG`); + no `runs` row started. +- **Detections present, `--allow-unredacted-secrets` only, non-TTY**: + exit 78 immediately with + `CLA-INFRA-SECRET-OVERRIDE-UNCONFIRMED` rule-ID on stderr (the rule-ID + is the operator's grep target; no `findings` row is persisted because + the run never started). +- **Detections present, both flags, non-TTY, `--confirm-allow-unredacted-secrets=yes-i-understand`**: + proceed; emit `CLA-SEC-UNREDACTED-SECRETS-ALLOWED` findings; record + `runs.stats` override entry. +- **Detections present, both flags, wrong confirm value**: exit 78; + rule-ID on stderr; no run row. + +`std::io::IsTerminal` on stdin is the TTY detector (stable since Rust +1.70; workspace MSRV 1.88). + +#### Override state in `runs.stats` + +The existing `runs.stats` column is a TEXT-affinity JSON blob. WP5 appends +two keys: + +```json +{ + "secret_override_used": true, + "secret_override_files_affected": [ + "src/api/keys.py", + "tests/fixtures/secrets.yaml" + ] +} +``` + +These keys are absent when no override fires. Existing readers ignore +unknown keys (verify by grepping for `runs.stats` JSON deserialisation +sites). + +#### Tests + +1. **Non-TTY override-confirmed**: fixture with one AWS key, both flags + set, confirm value correct → exits 0, no `briefing_blocked`, one + `CLA-SEC-UNREDACTED-SECRETS-ALLOWED` per file, `runs.stats` carries + the override keys. +2. **Non-TTY override-unconfirmed**: only `--allow-unredacted-secrets`, + no confirm → exit 78, stderr contains + `CLA-INFRA-SECRET-OVERRIDE-UNCONFIRMED`, no `runs` row in the DB. +3. **Non-TTY override-wrong-confirm**: both flags but + `--confirm-allow-unredacted-secrets=oops` → exit 78, stderr contains + the rule-ID. +4. **Override + no detections**: flag set but no secrets present → no + override findings, no `runs.stats` override keys (the operator's CI + stays clean on clean repos). +5. **TTY path**: marked `#[ignore]` with a doc comment; manual + verification belongs to Workstream D's smoke test step 8. + +#### Exit criteria + +- All non-TTY tests green. +- `cli.rs` exit codes documented in a module-level doc comment: + - 0: success (with or without override). + - 1: hard failure (existing behaviour). + - 78 (`EX_CONFIG`): override misconfigured. +- Operator doc (Task 7) documents the exit-code contract and the + `--confirm-allow-unredacted-secrets` syntax. + +--- + +### Task 5 — MCP-side awareness of `briefing_blocked` + +**Owner**: `crates/clarion-mcp/` +**Estimated size**: 150–250 LOC including tests. +**Depends on**: Task 3 (writes the flag); independent of Task 4. +**Parallelisable with Task 4**: yes. + +#### Files + +| Action | Path | +|---|---| +| modify | `crates/clarion-mcp/src/lib.rs` (summary tool dispatch) | +| modify | `crates/clarion-mcp/tests/storage_tools.rs` (or split into a new test file) | + +#### Scope + +When `summary(id)` is called on an entity whose +`properties.briefing_blocked == "secret_present"`, the MCP server must: + +1. **Not** invoke the LLM provider. +2. **Not** consume any budget ledger. +3. **Not** write a row to `summary_cache`. +4. Return an envelope shape that tells the consult-mode agent the absence + is policy, not pipeline failure. + +#### Envelope shape + +```json +{ + "entity_id": "python:function:demo.foo|function", + "summary": null, + "briefing_blocked": "secret_present", + "remediation": "File flagged by pre-ingest secret scan. Fix the secret or whitelist via .clarion/secrets-baseline.yaml. See ADR-013." +} +``` + +This is parallel to the existing `summary_scope_deferred` envelope +(`crates/clarion-mcp/src/lib.rs:2341`) and the four `issues_unavailable` +envelopes in `clarion-mcp::filigree`. Add a `summary_briefing_blocked` +helper next to `summary_scope_deferred`. + +#### Hook point + +The summary tool dispatch reads `entity_properties_json` at line 2382 of +`lib.rs`. The branch on `briefing_blocked` happens **before** any cache +lookup or provider invocation. Recommended placement: right after the +existing `summary_scope_deferred` check, since both are +"return-immediately" policy branches. + +#### Tests + +1. **Storage-only**: fixture entity with + `properties.briefing_blocked == "secret_present"` → `summary` tool + returns the envelope; no row in `summary_cache`. +2. **Recording-provider isolation**: with `RecordingProvider` configured + in recording mode and a counter wrapper around the inner provider, + call `summary` on a blocked entity → the inner-provider call counter + stays at zero. (The exact assertion form depends on the + `RecordingProvider` fixture layout — either "no fixture file + created" or "no new entry appended"; the load-bearing claim is "no + outbound LLM call." Pick whichever assertion the existing + `RecordingProvider` test harness already supports.) +3. **Budget untouched**: read the session's budget ledger before and + after — bytes identical. +4. **Other tools unaffected**: `entity_at`, `find_entity`, `callers_of`, + `execution_paths_from`, `neighborhood`, `issues_for` all return their + normal envelopes for blocked entities. The block only affects LLM + dispatch; structural navigation still works (this is the whole point + of "block briefings, not analysis"). + +#### Exit criteria + +- Storage-tools tests green. +- Manual log inspection on a fixture with one blocked entity confirms + zero outbound LLM calls. + +--- + +### Task 6 — Rule-ID catalogue in `detailed-design.md` + +**Owner**: docs +**Estimated size**: ~100 lines of doc changes. +**Parallelisable with any task**. + +#### Files + +| Action | Path | +|---|---| +| modify | `docs/clarion/v0.1/detailed-design.md` (§5 rule catalogue) | + +#### Scope + +Append rule rows for each of the five new rule-IDs WP5 introduces. Use the +existing table shape in §5 (do not invent a new format). + +| Rule-ID | Severity | Category | Description (one sentence) | Remediation (one sentence) | ADR | +|---|---|---|---|---|---| +| `CLA-SEC-SECRET-DETECTED` | error | security | Pre-ingest secret scanner detected a credential pattern in a file slated for LLM dispatch. | Remove the secret, rotate the credential, or whitelist via `.clarion/secrets-baseline.yaml` with a justification. | [ADR-013](../adr/ADR-013-pre-ingest-secret-scanner.md) | +| `CLA-SEC-UNREDACTED-SECRETS-ALLOWED` | error | security | Operator invoked `--allow-unredacted-secrets`; file content reached the LLM provider with secrets intact. | Audit override usage via `filigree list --rule-id=CLA-SEC-UNREDACTED-SECRETS-ALLOWED --since 30d`. | [ADR-013](../adr/ADR-013-pre-ingest-secret-scanner.md) | +| `CLA-INFRA-SECRET-BASELINE-NO-JUSTIFICATION` | error | infra | Baseline entry missing required `justification` field; entry not honoured. | Add a `justification` string explaining why the match is safe. | [ADR-013](../adr/ADR-013-pre-ingest-secret-scanner.md) | +| `CLA-INFRA-SECRET-BASELINE-MATCH` | info | infra | Baseline entry suppressed a scanner detection (audit surface). | None — informational, retained for `NFR-SEC-04` audit. | [ADR-013](../adr/ADR-013-pre-ingest-secret-scanner.md) | +| `CLA-INFRA-SECRET-OVERRIDE-UNCONFIRMED` | error | infra | `--allow-unredacted-secrets` supplied without confirmation; run aborted before start. | Supply `--confirm-allow-unredacted-secrets=yes-i-understand` in non-TTY contexts or run interactively. | [ADR-013](../adr/ADR-013-pre-ingest-secret-scanner.md) | + +These IDs make the WP9-B Filigree-emission story (v0.2) able to +round-trip without a separate spec pass. + +#### Exit criteria + +- `detailed-design.md` lints clean (existing markdown / cross-reference + checks in CI). +- ADR-013 retains canonical authority on *behaviour*; the design doc + carries the *catalogue*. No behaviour is restated in the design doc + beyond the one-sentence description and remediation. + +--- + +### Task 7 — Operator documentation + +**Owner**: docs +**Estimated size**: ≤ 250 lines. +**Parallelisable with any task**. + +#### Files + +| Action | Path | +|---|---| +| create | `docs/operator/secret-scanning.md` | +| modify | `docs/operator/README.md` (add link to the new page) | +| modify | `README.md` (Workstream B Task 1; cross-link) — coordinated with WS B, do not duplicate | + +If `docs/operator/README.md` does not exist yet, create a minimal index +listing this and the (forthcoming) `getting-started.md` from WS-B Task 2. +Coordinate with WS-B to avoid duplicate index creation. + +#### Scope + +A single page that lets a non-engineer resolve a baseline false-positive +without reading ADR-013. Sections: + +1. **What the scanner does** (3 sentences). +2. **What gets blocked** (file-level; structural extraction continues; + summaries don't). +3. **How to whitelist a false positive** (edit + `.clarion/secrets-baseline.yaml`; example entry; commit it). +4. **The override flag** (`--allow-unredacted-secrets` + + `--confirm-allow-unredacted-secrets=yes-i-understand`; when to use it; + what gets audited). +5. **Exit codes** (0 / 1 / 78). +6. **Finding the audit trail** ( + `select * from findings where rule_id like 'CLA-SEC-%'`; + forward-pointer to Filigree integration in v0.2). +7. **Limitations** (pattern-based scanning has false negatives; novel + secret shapes; air-gapped alternatives for truly high-risk repos — + `--no-llm`). + +#### Exit criteria + +- A non-engineer can read the doc and resolve a baseline false-positive + end-to-end (verified in WS-D smoke test). +- The doc is ≤ 250 lines. +- Cross-links to ADR-013 for the operator who wants the design rationale, + but does not require reading the ADR to act. + +--- + +## 6. Schema additions + +WP5 does not create new tables or columns. It adds two well-known keys to +existing JSON-bearing columns: + +### `entities.properties` (existing TEXT column, JSON object) + +- `briefing_blocked: "secret_present"` — stamped by the host on entities + whose source file was flagged by the pre-ingest scanner. + +Other plausible `briefing_blocked` reasons in the future (e.g. +`"size_cap_exceeded"`, `"operator_excluded"`) are NOT introduced by WP5; +the schema is open-vocabulary on the value side. ADR-013 names +`secret_present` as the canonical first reason. + +### `runs.stats` (existing TEXT column, JSON object) + +- `secret_override_used: true` — present only when the override fired. +- `secret_override_files_affected: ["…", "…"]` — present only when + `secret_override_used == true`. + +Both keys absent on runs with no override → reader code defaults to "no +override happened." + +### Rule-ID grammar + +The five new rule-IDs all match the existing ADR-022 manifest rule-ID +grammar (see `plugin/manifest` correction in commit `0cb61b4`). No +manifest changes are needed because these are *core-emitted* findings, +not plugin-emitted. + +--- + +## 7. Test strategy across layers + +| Layer | Tests | Location | +|---|---|---| +| Unit | Scanner pattern fixtures (positive + negative per rule); entropy bounds; baseline parse / suppress / round-trip | `crates/clarion-scanner/tests/` | +| Unit | Override CLI exit codes; `runs.stats` JSON shape | `crates/clarion-cli/tests/secret_scan.rs` | +| Unit | MCP `summary` envelope on blocked entity | `crates/clarion-mcp/tests/storage_tools.rs` | +| Integration | `analyze` over fixture project with one secret-bearing file → entities + findings + properties_json shape | `crates/clarion-cli/tests/secret_scan.rs` | +| Integration | Baseline suppression; baseline missing-justification | same | +| Integration | Non-TTY override paths (3 cases) | same | +| E2E | `clarion install && clarion analyze` against a fixture project with one known secret; assert exit 0, entities present, briefing_blocked flagged, finding persisted; assert walking-skeleton fixture still green | `tests/e2e/wp5_secret_scan.sh` (new), parallels `tests/e2e/sprint_1_walking_skeleton.sh` | +| Manual | TTY interactive prompt path | WS-D smoke test step 8 | + +### CI gates (ADR-023 floor, unchanged) + +WP5 must keep all of these green: + +- `cargo fmt --all -- --check` +- `cargo clippy --workspace --all-targets --all-features -- -D warnings` +- `cargo build --workspace --bins` +- `cargo nextest run --workspace --all-features` +- `RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps --all-features` +- `cargo deny check` +- `plugins/python/.venv/bin/ruff check plugins/python` +- `plugins/python/.venv/bin/ruff format --check plugins/python` +- `plugins/python/.venv/bin/mypy --strict plugins/python` +- `plugins/python/.venv/bin/pytest plugins/python` +- `bash tests/e2e/sprint_1_walking_skeleton.sh` + +A new E2E (`wp5_secret_scan.sh`) is added to the CI matrix's +`walking-skeleton` job. + +--- + +## 8. Risks and open questions + +| ID | Risk / question | Mitigation | +|---|---|---| +| R-1 | High-entropy detection on UUIDs and base64 checksums creates false-positive flood on real repos | Bound length thresholds per ADR-013 (≥20 chars base64, ≥40 chars hex); document baseline workflow prominently in Task 7; Workstream D smoke test on `requests` will surface real-world false-positive rate before publish | +| R-2 | sha1 hashing of literal secrets means a baseline rebuilt from a different `detect-secrets` version may mismatch | Document the exact hash function (sha1 over the literal matched bytes, no normalisation); migration story for `detect-secrets` v2.x is a v0.2 problem | +| R-3 | The override flag could become normalised in CI configurations ("we always pass it because…") | Audit-surface design: every override is a finding; v0.2 Filigree integration makes them visible; operator doc explicitly names this anti-pattern | +| R-4 | Plugin-host integration to stamp `briefing_blocked` adds a code path through a 3 126-LOC file flagged by arch-analysis §5.4 A-3 | Keep the host-side change small (≤ 30 LOC, a single map lookup); the orchestration lives in `clarion-cli/src/secret_scan.rs`, not in the host | +| R-5 | Baseline format compatibility with `detect-secrets` v1.x must be exact; an operator running `detect-secrets scan --baseline` and dropping the file in `.clarion/` must work | Round-trip test in Task 2; smoke-test the workflow with a real `detect-secrets`-generated baseline in WS-D | +| Q-1 | Should baseline entries carry an `expires_at` field so stale entries are surfaced? | Out of scope for v0.1; surface as a v0.2 enhancement if the override-monitoring loop also lands then | +| Q-2 | Does `briefing_blocked: secret_present` propagate up to module / subsystem summaries when those are aggregated (deferred to v0.2 per ADR-030)? | Defer to v0.2; the leaf summary skip is sufficient for v0.1; document in Task 7 that "summaries of containing modules may still be generated and may infer secret content indirectly — fix the underlying file" | +| Q-3 | Should the scanner also redact secrets from log lines emitted by Clarion itself (the `runs//log.jsonl` ADR-013 line 16 cites)? | **Resolved at planning time**: `runs//log.jsonl` is referenced only in `clarion-cli/src/install.rs:74` (the gitignore template) and `tests/install.rs:40` (the test asserting it's ignored). No writer code exists in `crates/` that emits to that path today. WP5 has nothing to redact. If a per-run JSONL log lands later (likely WP6 batched pipeline or WP9-B), that work must include a redaction pass for any file content emitted from `briefing_blocked` files; cite this row when the log writer is authored. | + +All three questions resolved at planning time. None blocks task kickoff. + +--- + +## 9. Workstream exit criteria (gate to Workstream D) + +The workstream is signed-off when ALL of the following hold: + +- [ ] All seven tasks closed. +- [ ] `cargo test --workspace --all-features` green. +- [ ] Existing CI gates (§7) unchanged in pass status. +- [ ] `tests/e2e/wp5_secret_scan.sh` added and green; included in the + `walking-skeleton` CI job. +- [ ] The walking-skeleton E2E continues to pass on the clean-fixture + path (no regression). +- [ ] `docs/operator/secret-scanning.md` reviewed by a non-author or + verified during WS-D smoke test step 8. +- [ ] All five rule-IDs appear in `detailed-design.md` §5. +- [ ] No new clippy warnings; no `unsafe` blocks introduced. +- [ ] `cargo tree -p clarion-scanner` shows no `tokio` / `rusqlite` / + `serde_norway` ancestors. + +When this gate closes, Workstream D's smoke-test step 8 (planted-`.env`) +becomes the publish-gate proof point for WP5. + +--- + +## 10. Filigree seeding + +Issues to create at workstream kickoff (umbrella + seven tasks). Set +dependencies as shown. + +```bash +# Umbrella +filigree create --type=work_package \ + --title="WP5 — Pre-ingest secret scanner (ADR-013)" \ + --labels="release:v0.1,sprint:3,wp:5,adr:013,tier:a" \ + --priority=1 +# capture the returned id as $WP5_UMBRELLA + +# Task 1 +filigree create --type=task \ + --title="WP5 Task 1 — Scanner crate + rule registry + entropy" \ + --labels="release:v0.1,sprint:3,wp:5,adr:013,crate:scanner" \ + --priority=1 +# capture as $T1 + +# Task 2 +filigree create --type=task \ + --title="WP5 Task 2 — Baseline parser (.clarion/secrets-baseline.yaml)" \ + --labels="release:v0.1,sprint:3,wp:5,adr:013,crate:scanner" \ + --priority=1 +# capture as $T2 + +# Task 3 +filigree create --type=task \ + --title="WP5 Task 3 — CLI wiring: pre-ingest hook in analyze::run" \ + --labels="release:v0.1,sprint:3,wp:5,adr:013,crate:cli,crate:core" \ + --priority=1 +# capture as $T3 + +# Task 4 +filigree create --type=task \ + --title="WP5 Task 4 — Override semantics: --allow-unredacted-secrets" \ + --labels="release:v0.1,sprint:3,wp:5,adr:013,crate:cli" \ + --priority=1 +# capture as $T4 + +# Task 5 +filigree create --type=task \ + --title="WP5 Task 5 — MCP-side awareness of briefing_blocked" \ + --labels="release:v0.1,sprint:3,wp:5,adr:013,crate:mcp" \ + --priority=1 +# capture as $T5 + +# Task 6 +filigree create --type=task \ + --title="WP5 Task 6 — Rule-ID catalogue entries in detailed-design.md" \ + --labels="release:v0.1,sprint:3,wp:5,adr:013,docs" \ + --priority=2 +# capture as $T6 + +# Task 7 +filigree create --type=task \ + --title="WP5 Task 7 — Operator documentation: secret-scanning.md" \ + --labels="release:v0.1,sprint:3,wp:5,adr:013,docs" \ + --priority=2 +# capture as $T7 + +# Dependencies +filigree add-dep $T3 $T1 # T3 depends on T1 +filigree add-dep $T3 $T2 # T3 depends on T2 +filigree add-dep $T4 $T3 # T4 depends on T3 +filigree add-dep $T5 $T3 # T5 depends on T3 (writes the flag T5 reads) + +# Umbrella rolls up +for t in $T1 $T2 $T3 $T4 $T5 $T6 $T7; do + filigree add-dep $WP5_UMBRELLA $t +done + +# Body for each issue should link back to the matching section of this doc: +# docs/implementation/v0.1-publish/ws-a-secret-scanner.md#task-N-... +``` + +The umbrella is `done` when all seven tasks close; the workstream signs +off via the criteria in §9. + +--- + +## 11. References + +- [ADR-013 — Pre-Ingest Secret Scanner with LLM-Dispatch Block](../../clarion/adr/ADR-013-pre-ingest-secret-scanner.md) — canonical spec. +- [ADR-007 — Summary cache key](../../clarion/adr/ADR-007-summary-cache-key.md) — `briefing_blocked` interaction. +- [ADR-017 — Severity and dedup](../../clarion/adr/ADR-017-severity-and-dedup.md) — `CLA-SEC-*` namespace ownership. +- [ADR-021 — Plugin authority hybrid](../../clarion/adr/ADR-021-plugin-authority-hybrid.md) — path-jail upstream of scanner. +- [ADR-022 — Core/plugin ontology boundary](../../clarion/adr/ADR-022-core-plugin-ontology.md) — secret detection as a core-owned algorithm. +- [ADR-023 — Tooling baseline](../../clarion/adr/ADR-023-tooling-baseline.md) — CI floor every task in this workstream must clear. +- [Requirements — NFR-SEC-01, NFR-SEC-04, NFR-OPS-01, NFR-OPS-04](../../clarion/v0.1/requirements.md) — requirement floor. +- [Thread 1 — Pre-publish blockers (program of work)](./thread-1-pre-publish-blockers.md) — the umbrella program. +- [v0.1-plan.md — WP5 scope](../v0.1-plan.md#wp5--pre-ingest-secret-scanner) — original work-package definition. +- [Sprint 2 scope amendment — WP5 deferral rationale](../sprint-2/scope-amendment-2026-05.md) — "production deployment against unknown corpora gates on this returning." +- [Arch-analysis final report §7 follow-ups](../../arch-analysis-2026-05-18-1244/04-final-report.md) — H-1 (`SoftFailed` coverage) coverage now closed via `clarion-141ca7de30`; relevant precondition for Task 3. +- [`detect-secrets` baseline format](https://github.com/Yelp/detect-secrets/blob/master/README.md#baseline-file) — the format Task 2 matches. diff --git a/docs/operator/README.md b/docs/operator/README.md index 225676df..145cf4d6 100644 --- a/docs/operator/README.md +++ b/docs/operator/README.md @@ -6,3 +6,5 @@ Practical notes for configuring and running Clarion. headers, and token-ceiling configuration for v0.1. - [Runtime topology](./runtime-topology.md) — supported `clarion serve` and `clarion analyze` concurrency against one `.clarion/clarion.db`. +- [Secret scanning](./secret-scanning.md) — pre-ingest scanner behavior, + baseline false-positive workflow, override confirmation, and audit queries. diff --git a/docs/operator/secret-scanning.md b/docs/operator/secret-scanning.md new file mode 100644 index 00000000..bd1b66ae --- /dev/null +++ b/docs/operator/secret-scanning.md @@ -0,0 +1,74 @@ +# Secret Scanning + +Clarion scans source files before any file content can be used for LLM summaries. A detected credential creates a finding and marks entities from that file with `briefing_blocked: secret_present`. Structural analysis still runs, but summaries for that file do not. + +## What Gets Blocked + +Blocking is file-level. If `src/config.py` contains a detected key, entities from that file remain queryable through structural tools, but the `summary` tool returns a policy envelope instead of calling the LLM provider or writing `summary_cache`. + +Files outside the analyzed plugin extension set are not scanned because they are not candidates for plugin dispatch in that run. + +## Whitelist A False Positive + +Add `.clarion/secrets-baseline.yaml` and commit it with the source change: + +```yaml +version: "1.0" +results: + "src/auth/fixtures.py": + - type: "AWS Access Key" + hashed_secret: "25910f981e85ca04baf359199dd0bd4a3ae738b6" + line_number: 42 + is_secret: false + justification: "AWS documentation example key used in a test fixture." +``` + +The hash is SHA-1 over the matched literal bytes, matching `detect-secrets` v1.x baseline conventions. `justification` is required; entries without it are ignored and produce `CLA-INFRA-SECRET-BASELINE-NO-JUSTIFICATION`. + +A matching baseline entry suppresses the block and records `CLA-INFRA-SECRET-BASELINE-MATCH` for audit. + +## Override Flag + +Use `--allow-unredacted-secrets` only when you deliberately accept that detected secrets may reach the LLM provider. + +Interactive runs prompt for: + +```text +yes-i-understand +``` + +Non-TTY runs must pass both flags: + +```bash +clarion analyze --allow-unredacted-secrets \ + --confirm-allow-unredacted-secrets=yes-i-understand . +``` + +Confirmed overrides do not set `briefing_blocked`; they emit `CLA-SEC-UNREDACTED-SECRETS-ALLOWED` and add `secret_override_used` plus `secret_override_files_affected` to `runs.stats`. Passing `--allow-unredacted-secrets` on a clean repo is a no-op. + +## Exit Codes + +| Code | Meaning | +|---|---| +| 0 | Analysis completed, with or without secret findings or a confirmed override. | +| 1 | Hard failure in the normal analysis path. | +| 78 | Secret override was requested but not confirmed; no run row is started. | + +## Audit Trail + +Local SQLite: + +```sql +select rule_id, severity, message, evidence +from findings +where rule_id like 'CLA-SEC-%' + or rule_id like 'CLA-INFRA-SECRET-%'; +``` + +Filigree integration for scanner findings is planned for v0.2. Until then, the local `findings` table is the authoritative WP5 audit surface. + +## Limitations + +The scanner is pattern-based. It can miss novel internal key formats and it can flag high-entropy test data. Use a justified baseline for reviewed false positives, and prefer `--no-llm` or an air-gapped workflow for repos where any source disclosure would be unacceptable. + +See [ADR-013](../clarion/adr/ADR-013-pre-ingest-secret-scanner.md) for design rationale. diff --git a/tests/e2e/wp5_secret_scan.sh b/tests/e2e/wp5_secret_scan.sh new file mode 100755 index 00000000..13b87935 --- /dev/null +++ b/tests/e2e/wp5_secret_scan.sh @@ -0,0 +1,110 @@ +#!/usr/bin/env bash +# WP5 pre-ingest secret-scanner end-to-end smoke. + +set -euo pipefail + +REPO_ROOT="${REPO_ROOT:-$(git rev-parse --show-toplevel)}" +CARGO_BUILD="${CARGO_BUILD:-1}" + +log() { printf '[wp5-secret-scan] %s\n' "$*" >&2; } +fail() { printf '[wp5-secret-scan] FAIL: %s\n' "$*" >&2; exit 1; } + +cd "$REPO_ROOT" + +if [ "$CARGO_BUILD" = "1" ]; then + log "building clarion (release) ..." + cargo build --workspace --release +fi + +CLARION_BIN="$REPO_ROOT/target/release/clarion" +[ -x "$CLARION_BIN" ] || fail "clarion binary missing at $CLARION_BIN" + +DEMO_DIR="$(mktemp -d -t clarion-wp5-demo-XXXXXX)" +PLUGIN_DIR="$(mktemp -d -t clarion-wp5-plugin-XXXXXX)" +trap 'rm -rf "$DEMO_DIR" "$PLUGIN_DIR"' EXIT + +cat > "$PLUGIN_DIR/clarion-plugin-secretfixture" <<'PY' +#!/usr/bin/python3 +import json +import pathlib +import re +import sys + +def read_frame(): + headers = {} + while True: + line = sys.stdin.buffer.readline() + if line in (b"", b"\r\n"): + break + name, value = line.decode("ascii").strip().split(":", 1) + headers[name.lower()] = value.strip() + length = int(headers["content-length"]) + return json.loads(sys.stdin.buffer.read(length)) + +def write_frame(message): + body = json.dumps(message, separators=(",", ":")).encode("utf-8") + sys.stdout.buffer.write(b"Content-Length: " + str(len(body)).encode("ascii") + b"\r\n\r\n") + sys.stdout.buffer.write(body) + sys.stdout.buffer.flush() + +while True: + msg = read_frame() + method = msg.get("method") + if method == "initialized": + continue + if method == "exit": + raise SystemExit(0) + ident = msg["id"] + if method == "initialize": + write_frame({"jsonrpc":"2.0","id":ident,"result":{"name":"clarion-plugin-secretfixture","version":"0.1.0","ontology_version":"0.1.0","capabilities":{}}}) + elif method == "analyze_file": + path = msg["params"]["file_path"] + name = "file_" + re.sub(r"[^A-Za-z0-9_]", "_", pathlib.Path(path).name) + write_frame({"jsonrpc":"2.0","id":ident,"result":{"entities":[{"id":"secretfixture:module:"+name,"kind":"module","qualified_name":name,"source":{"file_path":path}}],"edges":[]}}) + elif method == "shutdown": + write_frame({"jsonrpc":"2.0","id":ident,"result":{}}) + else: + raise SystemExit(1) +PY +chmod 755 "$PLUGIN_DIR/clarion-plugin-secretfixture" + +cat > "$PLUGIN_DIR/plugin.toml" <<'TOML' +[plugin] +name = "clarion-plugin-secretfixture" +plugin_id = "secretfixture" +version = "0.1.0" +protocol_version = "1.0" +executable = "clarion-plugin-secretfixture" +language = "secretfixture" +extensions = ["sec"] + +[capabilities.runtime] +expected_max_rss_mb = 256 +expected_entities_per_file = 100 +wardline_aware = false +reads_outside_project_root = false + +[ontology] +entity_kinds = ["module"] +edge_kinds = [] +rule_id_prefix = "CLA-SECRET-FIXTURE-" +ontology_version = "0.1.0" +TOML + +log "scratch project: $DEMO_DIR" +"$CLARION_BIN" install --path "$DEMO_DIR" +printf "aws_access_key_id = 'AKIAIOSFODNN7EXAMPLE'\n" > "$DEMO_DIR/leaky.sec" + +PATH="$PLUGIN_DIR" "$CLARION_BIN" analyze "$DEMO_DIR" + +DB="$DEMO_DIR/.clarion/clarion.db" +BLOCKED=$(sqlite3 "$DB" "select json_extract(properties, '\$.briefing_blocked') from entities;") +[ "$BLOCKED" = "secret_present" ] || fail "expected briefing_blocked secret_present, got $BLOCKED" + +FINDINGS=$(sqlite3 "$DB" "select count(*) from findings where rule_id = 'CLA-SEC-SECRET-DETECTED';") +[ "$FINDINGS" = "1" ] || fail "expected one CLA-SEC-SECRET-DETECTED finding, got $FINDINGS" + +RUN_STATUS=$(sqlite3 "$DB" "select status from runs;") +[ "$RUN_STATUS" = "completed" ] || fail "expected completed run, got $RUN_STATUS" + +log "PASS: WP5 secret scan blocks summaries and records finding" From 2117118c45bdea5c50d061fd168089b0e700dfce Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Tue, 19 May 2026 02:31:16 +1000 Subject: [PATCH 002/187] fix(wp5): close secret scanner review gaps --- crates/clarion-cli/src/analyze.rs | 158 +++++++++++++++++++++++- crates/clarion-cli/src/secret_scan.rs | 59 ++++++--- crates/clarion-cli/tests/secret_scan.rs | 112 ++++++++++++++++- crates/clarion-core/src/plugin/host.rs | 24 +++- crates/clarion-mcp/src/lib.rs | 7 +- crates/clarion-scanner/src/baseline.rs | 37 ++++-- crates/clarion-scanner/src/lib.rs | 3 +- crates/clarion-scanner/tests/scanner.rs | 36 +++++- docs/operator/secret-scanning.md | 2 +- tests/e2e/wp5_secret_scan.sh | 10 +- 10 files changed, 407 insertions(+), 41 deletions(-) diff --git a/crates/clarion-cli/src/analyze.rs b/crates/clarion-cli/src/analyze.rs index 5ad5fa5b..db47b7d5 100644 --- a/crates/clarion-cli/src/analyze.rs +++ b/crates/clarion-cli/src/analyze.rs @@ -207,8 +207,13 @@ pub(crate) async fn run_with_options(project_path: PathBuf, options: AnalyzeOpti let source_files = collect_source_files(&project_root, &wanted_extensions); tracing::info!(file_count = source_files.len(), "source tree walk complete"); + let secret_scan_files = collect_secret_scan_files(&project_root, &source_files); + tracing::info!( + file_count = secret_scan_files.len(), + "secret scan file walk complete" + ); let secret_scan_outcome = - crate::secret_scan::pre_ingest(&project_root, &source_files, &options.secret_scan)?; + crate::secret_scan::pre_ingest(&project_root, &secret_scan_files, &options.secret_scan)?; begin_run(&writer, &run_id, &analyze_config_json, &started_at).await?; // ── Per-plugin processing ───────────────────────────────────────────────── @@ -280,6 +285,7 @@ pub(crate) async fn run_with_options(project_path: PathBuf, options: AnalyzeOpti let exec_clone = plugin.executable.clone(); let files_clone = plugin_files.clone(); let briefing_blocks_clone = secret_scan_outcome.briefing_blocks.clone(); + let scanned_files_clone = secret_scan_outcome.scanned_files().clone(); // A JoinError here means the blocking task panicked (OOM, stack // overflow, internal unwrap, abort — anything that unwinds past the @@ -298,6 +304,7 @@ pub(crate) async fn run_with_options(project_path: PathBuf, options: AnalyzeOpti &exec_clone, &files_clone, &briefing_blocks_clone, + &scanned_files_clone, ) }) .await, @@ -435,6 +442,22 @@ pub(crate) async fn run_with_options(project_path: PathBuf, options: AnalyzeOpti } } + if !matches!(run_outcome, RunOutcome::HardFailed { .. }) + && let Err(e) = ensure_secret_finding_anchors( + &writer, + &project_root, + &started_at, + &secret_scan_outcome, + &mut secret_finding_anchors, + ) + .await + { + tracing::error!(run_id = %run_id, error = %e, "secret finding anchor persistence failed"); + run_outcome = RunOutcome::HardFailed { + reason: format!("secret finding anchor persistence failed: {e:#}"), + }; + } + if !matches!(run_outcome, RunOutcome::HardFailed { .. }) && let Err(e) = crate::secret_scan::emit_findings( &writer, @@ -1207,6 +1230,7 @@ fn run_plugin_blocking( executable: &Path, files: &[PathBuf], briefing_blocks: &BTreeMap, + scanned_source_files: &BTreeSet, ) -> Result { use clarion_core::PluginHost; @@ -1223,6 +1247,7 @@ fn run_plugin_blocking( } })?; host.set_briefing_blocks(briefing_blocks.clone()); + host.set_scanned_source_files(scanned_source_files.clone()); let work_result: Result = (|| { let mut collected_entities: Vec<(String, EntityRecord)> = Vec::new(); @@ -1551,6 +1576,91 @@ fn remember_secret_finding_anchors( } } +async fn ensure_secret_finding_anchors( + writer: &Writer, + project_root: &Path, + started_at: &str, + outcome: &crate::secret_scan::SecretScanOutcome, + anchors: &mut BTreeMap, +) -> Result<()> { + for file in outcome.finding_files() { + let key = canonical_or_original(&file); + if anchors.contains_key(&key) { + continue; + } + let id = secret_finding_anchor_id(project_root, &key); + let relative = display_relative_path(project_root, &key); + let short_name = key + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or(&relative) + .to_owned(); + let mut properties = serde_json::Map::new(); + properties.insert("finding_anchor".to_owned(), serde_json::json!(true)); + if let Some(reason) = outcome.briefing_blocks.get(&key) { + properties.insert( + "briefing_blocked".to_owned(), + serde_json::Value::String(reason.clone()), + ); + } + let record = EntityRecord { + id: id.clone(), + plugin_id: "core".to_owned(), + kind: "file".to_owned(), + name: relative, + short_name, + parent_id: None, + source_file_id: None, + source_file_path: Some(key.display().to_string()), + source_byte_start: None, + source_byte_end: None, + source_line_start: None, + source_line_end: None, + properties_json: serde_json::Value::Object(properties).to_string(), + content_hash: file_content_hash(&key), + summary_json: None, + wardline_json: None, + first_seen_commit: None, + last_seen_commit: None, + created_at: started_at.to_owned(), + updated_at: started_at.to_owned(), + }; + writer + .send_wait(|ack| WriterCmd::InsertEntity { + entity: Box::new(record), + ack, + }) + .await + .map_err(|e| anyhow::anyhow!("{e}")) + .with_context(|| format!("InsertEntity for secret finding anchor {id}"))?; + anchors.insert(key, id); + } + Ok(()) +} + +fn secret_finding_anchor_id(project_root: &Path, file: &Path) -> String { + let relative = display_relative_path(project_root, file); + let digest = blake3::hash(relative.as_bytes()).to_hex().to_string(); + format!("core:file:{digest}") +} + +fn canonical_or_original(path: &Path) -> PathBuf { + path.canonicalize().unwrap_or_else(|_| path.to_path_buf()) +} + +fn display_relative_path(root: &Path, path: &Path) -> String { + path.strip_prefix(root) + .unwrap_or(path) + .display() + .to_string() +} + +fn file_content_hash(path: &Path) -> Option { + fs::read(path) + .ok() + .map(|bytes| blake3::hash(&bytes).to_hex().to_string()) +} + /// Map an `AcceptedEntity` to an `EntityRecord` for the writer-actor. fn map_entity_to_record( entity: &AcceptedEntity, @@ -1846,6 +1956,52 @@ fn collect_source_files(root: &Path, wanted_extensions: &BTreeSet) -> Ve out } +fn collect_secret_scan_files(root: &Path, source_files: &[PathBuf]) -> Vec { + let mut out: BTreeSet = source_files + .iter() + .map(|path| canonical_or_original(path)) + .collect(); + for path in collect_secret_scan_sidecars(root) { + out.insert(canonical_or_original(&path)); + } + out.into_iter().collect() +} + +fn collect_secret_scan_sidecars(root: &Path) -> Vec { + let mut out = Vec::new(); + let mut builder = WalkBuilder::new(root); + builder + .follow_links(false) + .hidden(false) + .ignore(false) + .git_ignore(false) + .git_global(false) + .git_exclude(false) + .parents(false) + .require_git(false) + .filter_entry(|entry| !is_skipped_dir(entry)); + + for entry in builder.build().filter_map(std::result::Result::ok) { + let Some(file_type) = entry.file_type() else { + continue; + }; + let path = entry.into_path(); + if file_type.is_file() && is_secret_scan_sidecar(&path) { + out.push(path); + } + } + out +} + +fn is_secret_scan_sidecar(path: &Path) -> bool { + let file_name = path.file_name().and_then(|name| name.to_str()); + file_name.is_some_and(|name| name == ".env" || name.starts_with(".env.")) + || path + .extension() + .and_then(|extension| extension.to_str()) + .is_some_and(|extension| extension.eq_ignore_ascii_case("env")) +} + fn is_skipped_dir(entry: &DirEntry) -> bool { entry .file_type() diff --git a/crates/clarion-cli/src/secret_scan.rs b/crates/clarion-cli/src/secret_scan.rs index 0e0e08cb..3391bd28 100644 --- a/crates/clarion-cli/src/secret_scan.rs +++ b/crates/clarion-cli/src/secret_scan.rs @@ -38,9 +38,21 @@ pub(crate) struct SecretScanOutcome { pub(crate) briefing_blocks: BTreeMap, findings: Vec, override_files: Vec, + scanned_files: BTreeSet, } impl SecretScanOutcome { + pub(crate) fn scanned_files(&self) -> &BTreeSet { + &self.scanned_files + } + + pub(crate) fn finding_files(&self) -> BTreeSet { + self.findings + .iter() + .map(|finding| finding.file_path.clone()) + .collect() + } + pub(crate) fn augment_stats(&self, stats: &mut serde_json::Value) { if self.override_files.is_empty() { return; @@ -65,8 +77,11 @@ pub(crate) fn pre_ingest( let mut per_file = Vec::new(); let mut all_allowed = Vec::new(); let mut baseline_matches = Vec::new(); + let mut scanned_files = BTreeSet::new(); for file in source_files { + let canonical_file = canonical_or_original(file); + scanned_files.insert(canonical_file.clone()); let buf = fs::read(file).with_context(|| format!("read {}", file.display()))?; let detections = scanner.scan_bytes(&buf); let SuppressionResult { @@ -79,11 +94,11 @@ pub(crate) fn pre_ingest( allowed .iter() .cloned() - .map(|detection| (canonical_or_original(file), detection)), + .map(|detection| (canonical_file.clone(), detection)), ); } baseline_matches.extend(fired_entries.into_iter().map(|entry| PendingFinding { - file_path: entry.file_path.clone(), + file_path: normalize_project_path(project_root, &entry.file_path), rule_id: BASELINE_MATCH, kind: "fact", severity: "INFO", @@ -100,7 +115,7 @@ pub(crate) fn pre_ingest( "rule": entry.entry.rule_type, }), })); - per_file.push((canonical_or_original(file), allowed)); + per_file.push((canonical_file, allowed)); } let override_confirmed = confirm_override_if_needed(&all_allowed, options); @@ -147,6 +162,7 @@ pub(crate) fn pre_ingest( briefing_blocks, findings, override_files: override_files.into_iter().collect(), + scanned_files, }) } @@ -154,22 +170,25 @@ fn load_baseline_for_scan(project_root: &Path) -> Result<(Baseline, Vec Ok((baseline, Vec::new())), - Err(BaselineError::MissingJustification { file, line }) => Ok(( + Err(BaselineError::MissingJustifications { entries }) => Ok(( Baseline::empty(), - vec![PendingFinding { - file_path: project_root.join(file.clone()), - rule_id: BASELINE_NO_JUSTIFICATION, - kind: "defect", - severity: "ERROR", - confidence: Some(1.0), - confidence_basis: Some("baseline_schema"), - message: format!( - "Secret baseline entry missing justification at {}:{}", - file.display(), - line - ), - evidence: json!({"file_path": file, "line_number": line}), - }], + entries + .into_iter() + .map(|entry| PendingFinding { + file_path: normalize_project_path(project_root, &entry.file), + rule_id: BASELINE_NO_JUSTIFICATION, + kind: "defect", + severity: "ERROR", + confidence: Some(1.0), + confidence_basis: Some("baseline_schema"), + message: format!( + "Secret baseline entry missing justification at {}:{}", + entry.file.display(), + entry.line + ), + evidence: json!({"file_path": entry.file, "line_number": entry.line}), + }) + .collect(), )), Err(err) => Err(err).context("load secret baseline"), } @@ -216,6 +235,10 @@ fn canonical_or_original(path: &Path) -> PathBuf { path.canonicalize().unwrap_or_else(|_| path.to_path_buf()) } +fn normalize_project_path(root: &Path, path: &Path) -> PathBuf { + canonical_or_original(&root.join(path)) +} + fn display_relative(root: &Path, path: &Path) -> String { path.strip_prefix(root) .unwrap_or(path) diff --git a/crates/clarion-cli/tests/secret_scan.rs b/crates/clarion-cli/tests/secret_scan.rs index 0399b758..ad3bc7d1 100644 --- a/crates/clarion-cli/tests/secret_scan.rs +++ b/crates/clarion-cli/tests/secret_scan.rs @@ -9,6 +9,7 @@ fn clarion_bin() -> Command { const PLUGIN_SCRIPT: &str = r#"#!/usr/bin/python3 import json +import os import pathlib import re import sys @@ -54,6 +55,7 @@ while True: }) elif method == "analyze_file": path = msg["params"]["file_path"] + source_path = os.environ.get("SECRETFIXTURE_SOURCE_OVERRIDE", path) name = "file_" + re.sub(r"[^A-Za-z0-9_]", "_", pathlib.Path(path).name) write_frame({ "jsonrpc": "2.0", @@ -64,7 +66,7 @@ while True: "id": "secretfixture:module:" + name, "kind": "module", "qualified_name": name, - "source": {"file_path": path}, + "source": {"file_path": source_path}, } ], "edges": [], @@ -192,6 +194,77 @@ fn secret_file_persists_finding_and_briefing_block() { assert_eq!(count, 1); } +#[test] +fn dotenv_sidecar_persists_finding_with_core_file_anchor() { + let project = tempfile::tempdir().unwrap(); + let plugin = tempfile::tempdir().unwrap(); + write_secret_fixture_plugin(plugin.path()); + install_project(project.path()); + std::fs::write(project.path().join("clean.sec"), b"nothing to see\n").unwrap(); + std::fs::write( + project.path().join(".env"), + b"aws_access_key_id = 'AKIAIOSFODNN7EXAMPLE'\n", + ) + .unwrap(); + + clarion_bin() + .arg("analyze") + .arg(project.path()) + .env("PATH", plugin_path(plugin.path())) + .assert() + .success(); + + let db = conn(project.path()); + let finding_count: i64 = db + .query_row( + "SELECT COUNT(*) FROM findings WHERE rule_id = 'CLA-SEC-SECRET-DETECTED'", + [], + |row| row.get(0), + ) + .unwrap(); + let anchor_count: i64 = db + .query_row( + "SELECT COUNT(*) FROM entities \ + WHERE plugin_id = 'core' \ + AND kind = 'file' \ + AND source_file_path LIKE '%.env' \ + AND json_extract(properties, '$.briefing_blocked') = 'secret_present'", + [], + |row| row.get(0), + ) + .unwrap(); + assert_eq!(finding_count, 1); + assert_eq!(anchor_count, 1); +} + +#[test] +fn plugin_entity_for_unscanned_source_is_briefing_blocked() { + let project = tempfile::tempdir().unwrap(); + let plugin = tempfile::tempdir().unwrap(); + write_secret_fixture_plugin(plugin.path()); + install_project(project.path()); + std::fs::write(project.path().join("clean.sec"), b"nothing to see\n").unwrap(); + let unscanned = project.path().join("notes.txt"); + std::fs::write(&unscanned, b"ordinary notes\n").unwrap(); + + clarion_bin() + .arg("analyze") + .arg(project.path()) + .env("PATH", plugin_path(plugin.path())) + .env("SECRETFIXTURE_SOURCE_OVERRIDE", &unscanned) + .assert() + .success(); + + let blocked: String = conn(project.path()) + .query_row( + "SELECT json_extract(properties, '$.briefing_blocked') FROM entities", + [], + |row| row.get(0), + ) + .unwrap(); + assert_eq!(blocked, "unscanned_source"); +} + #[test] fn baseline_suppresses_secret_and_emits_audit_match() { let project = tempfile::tempdir().unwrap(); @@ -273,6 +346,11 @@ results: hashed_secret: "25910f981e85ca04baf359199dd0bd4a3ae738b6" line_number: 1 is_secret: false + "stale.sec": + - type: "AWS Access Key" + hashed_secret: "25910f981e85ca04baf359199dd0bd4a3ae738b6" + line_number: 9 + is_secret: false "#, ) .unwrap(); @@ -291,7 +369,7 @@ results: |row| row.get(0), ) .unwrap(); - assert_eq!(count, 1); + assert_eq!(count, 2); } #[test] @@ -370,6 +448,36 @@ fn non_tty_override_without_confirmation_exits_78_before_run_start() { assert_eq!(run_count, 0); } +#[test] +fn non_tty_override_with_wrong_confirmation_exits_78_before_run_start() { + let project = tempfile::tempdir().unwrap(); + let plugin = tempfile::tempdir().unwrap(); + write_secret_fixture_plugin(plugin.path()); + install_project(project.path()); + std::fs::write( + project.path().join("leaky.sec"), + b"aws_access_key_id = 'AKIAIOSFODNN7EXAMPLE'\n", + ) + .unwrap(); + + let assert = clarion_bin() + .args([ + "analyze", + "--allow-unredacted-secrets", + "--confirm-allow-unredacted-secrets=oops", + ]) + .arg(project.path()) + .env("PATH", plugin_path(plugin.path())) + .assert() + .code(78); + let stderr = String::from_utf8_lossy(&assert.get_output().stderr); + assert!(stderr.contains("CLA-INFRA-SECRET-OVERRIDE-UNCONFIRMED")); + let run_count: i64 = conn(project.path()) + .query_row("SELECT COUNT(*) FROM runs", [], |row| row.get(0)) + .unwrap(); + assert_eq!(run_count, 0); +} + #[test] #[ignore = "TTY override confirmation needs an interactive terminal; WS-D owns the manual smoke."] fn tty_override_confirmation_manual_smoke() {} diff --git a/crates/clarion-core/src/plugin/host.rs b/crates/clarion-core/src/plugin/host.rs index 2afed830..f476a586 100644 --- a/crates/clarion-core/src/plugin/host.rs +++ b/crates/clarion-core/src/plugin/host.rs @@ -416,6 +416,8 @@ where stderr_tail: Option>>>, /// Canonical source paths whose entities must not be sent for LLM briefing. briefing_blocks: BTreeMap, + /// Canonical source paths that were covered by the core pre-ingest scanner. + scanned_source_files: BTreeSet, } /// Size of the stderr ring buffer kept for diagnostics. 64 KiB holds @@ -664,6 +666,7 @@ impl PluginHost { ontology_version: None, stderr_tail: None, briefing_blocks: BTreeMap::new(), + scanned_source_files: BTreeSet::new(), } } @@ -698,6 +701,15 @@ impl PluginHost { self.briefing_blocks = blocks; } + /// Install canonical source paths covered by the pre-ingest scanner. + /// + /// If a plugin returns an entity for a jailed in-project path that is not in + /// this set, the entity is policy-blocked from LLM briefing because its file + /// bytes have not passed the scanner. + pub fn set_scanned_source_files(&mut self, files: BTreeSet) { + self.scanned_source_files = files; + } + /// Perform the `initialize` → `initialized` handshake. /// /// Steps: @@ -955,10 +967,18 @@ impl PluginHost { } fn apply_briefing_block(&self, raw: &mut RawEntity, jailed: &str) { - if let Some(reason) = self.briefing_blocks.get(Path::new(jailed)) { + let jailed_path = Path::new(jailed); + let reason = self + .briefing_blocks + .get(jailed_path) + .map(String::as_str) + .or_else(|| { + (!self.scanned_source_files.contains(jailed_path)).then_some("unscanned_source") + }); + if let Some(reason) = reason { raw.extra.insert( "briefing_blocked".to_owned(), - serde_json::Value::String(reason.clone()), + serde_json::Value::String(reason.to_owned()), ); } } diff --git a/crates/clarion-mcp/src/lib.rs b/crates/clarion-mcp/src/lib.rs index 78cfc2bd..3bc3d9ef 100644 --- a/crates/clarion-mcp/src/lib.rs +++ b/crates/clarion-mcp/src/lib.rs @@ -2353,13 +2353,18 @@ fn summary_scope_deferred(entity: &EntityRow) -> Value { } fn summary_briefing_blocked(entity: &EntityRow, reason: &str) -> Value { + let remediation = if reason == "unscanned_source" { + "Entity source file was not covered by the pre-ingest secret scan. Re-run with scanner coverage for that path or fix the plugin source path before requesting a summary." + } else { + "File flagged by pre-ingest secret scan. Fix the secret or whitelist via .clarion/secrets-baseline.yaml. See ADR-013." + }; success_envelope(json!({ "available": false, "entity_id": entity.id, "entity": entity_json(entity), "summary": null, "briefing_blocked": reason, - "remediation": "File flagged by pre-ingest secret scan. Fix the secret or whitelist via .clarion/secrets-baseline.yaml. See ADR-013." + "remediation": remediation })) } diff --git a/crates/clarion-scanner/src/baseline.rs b/crates/clarion-scanner/src/baseline.rs index ef1ea383..25a80c1c 100644 --- a/crates/clarion-scanner/src/baseline.rs +++ b/crates/clarion-scanner/src/baseline.rs @@ -29,6 +29,12 @@ pub struct BaselineMatch { pub entry: BaselineEntry, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BaselineEntryIssue { + pub file: PathBuf, + pub line: u32, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct SuppressionResult { pub allowed: Vec, @@ -40,8 +46,8 @@ pub struct SuppressionResult { pub enum BaselineError { #[error("baseline version mismatch: expected 1.0, got {0}")] UnsupportedVersion(String), - #[error("baseline entry missing required field 'justification' at {file}:{line}")] - MissingJustification { file: PathBuf, line: u32 }, + #[error("baseline entries missing required field 'justification'")] + MissingJustifications { entries: Vec }, #[error("baseline entry has invalid hashed_secret at {file}:{line}: {details}")] InvalidHash { file: PathBuf, @@ -131,17 +137,32 @@ impl Baseline { if raw.version != "1.0" { return Err(BaselineError::UnsupportedVersion(raw.version)); } + let mut missing_justifications = Vec::new(); + for (file, raw_entries) in &raw.results { + for entry in raw_entries { + if entry + .justification + .as_ref() + .is_none_or(|value| value.trim().is_empty()) + { + missing_justifications.push(BaselineEntryIssue { + file: file.clone(), + line: entry.line_number, + }); + } + } + } + if !missing_justifications.is_empty() { + return Err(BaselineError::MissingJustifications { + entries: missing_justifications, + }); + } + let mut entries = BTreeMap::new(); for (file, raw_entries) in raw.results { let mut converted = Vec::new(); for entry in raw_entries { let justification = entry.justification.unwrap_or_default(); - if justification.trim().is_empty() { - return Err(BaselineError::MissingJustification { - file, - line: entry.line_number, - }); - } let hashed_secret = hex_decode_20(&entry.hashed_secret).map_err(|details| { BaselineError::InvalidHash { file: file.clone(), diff --git a/crates/clarion-scanner/src/lib.rs b/crates/clarion-scanner/src/lib.rs index abcb6924..94d939bd 100644 --- a/crates/clarion-scanner/src/lib.rs +++ b/crates/clarion-scanner/src/lib.rs @@ -9,7 +9,8 @@ mod entropy; mod patterns; pub use baseline::{ - Baseline, BaselineEntry, BaselineError, BaselineMatch, SuppressionResult, load_baseline, + Baseline, BaselineEntry, BaselineEntryIssue, BaselineError, BaselineMatch, SuppressionResult, + load_baseline, }; pub use entropy::EntropyTuning; pub use patterns::{PatternMeta, Scanner}; diff --git a/crates/clarion-scanner/tests/scanner.rs b/crates/clarion-scanner/tests/scanner.rs index c031728a..9341432e 100644 --- a/crates/clarion-scanner/tests/scanner.rs +++ b/crates/clarion-scanner/tests/scanner.rs @@ -189,10 +189,38 @@ results: "#, ) .expect_err("missing justification should fail"); - assert!(matches!( - err, - BaselineError::MissingJustification { line: 42, .. } - )); + let BaselineError::MissingJustifications { entries } = err else { + panic!("expected MissingJustifications, got {err:?}"); + }; + assert_eq!(entries.len(), 1); + assert_eq!(entries[0].line, 42); +} + +#[test] +fn baseline_missing_justification_reports_all_entries() { + let err = Baseline::from_yaml_str( + r#" +version: "1.0" +results: + "src/one.py": + - type: "AWS Access Key" + hashed_secret: "0123456789abcdef0123456789abcdef01234567" + line_number: 4 + is_secret: false + "src/two.py": + - type: "AWS Access Key" + hashed_secret: "0123456789abcdef0123456789abcdef01234567" + line_number: 8 + is_secret: false +"#, + ) + .expect_err("all missing justifications should be reported"); + let BaselineError::MissingJustifications { entries } = err else { + panic!("expected MissingJustifications, got {err:?}"); + }; + assert_eq!(entries.len(), 2); + assert_eq!(entries[0].line, 4); + assert_eq!(entries[1].line, 8); } #[test] diff --git a/docs/operator/secret-scanning.md b/docs/operator/secret-scanning.md index bd1b66ae..63410505 100644 --- a/docs/operator/secret-scanning.md +++ b/docs/operator/secret-scanning.md @@ -6,7 +6,7 @@ Clarion scans source files before any file content can be used for LLM summaries Blocking is file-level. If `src/config.py` contains a detected key, entities from that file remain queryable through structural tools, but the `summary` tool returns a policy envelope instead of calling the LLM provider or writing `summary_cache`. -Files outside the analyzed plugin extension set are not scanned because they are not candidates for plugin dispatch in that run. +Plugin source files and `.env` sidecars are scanned. If a plugin reports an entity for some other in-project path that was not covered by the scanner, Clarion marks that entity `briefing_blocked: unscanned_source` so source bytes cannot reach the LLM provider without a prior scan. ## Whitelist A False Positive diff --git a/tests/e2e/wp5_secret_scan.sh b/tests/e2e/wp5_secret_scan.sh index 13b87935..e50ab06b 100755 --- a/tests/e2e/wp5_secret_scan.sh +++ b/tests/e2e/wp5_secret_scan.sh @@ -94,15 +94,19 @@ TOML log "scratch project: $DEMO_DIR" "$CLARION_BIN" install --path "$DEMO_DIR" printf "aws_access_key_id = 'AKIAIOSFODNN7EXAMPLE'\n" > "$DEMO_DIR/leaky.sec" +printf "aws_access_key_id = 'AKIAIOSFODNN7EXAMPLE'\n" > "$DEMO_DIR/.env" PATH="$PLUGIN_DIR" "$CLARION_BIN" analyze "$DEMO_DIR" DB="$DEMO_DIR/.clarion/clarion.db" -BLOCKED=$(sqlite3 "$DB" "select json_extract(properties, '\$.briefing_blocked') from entities;") -[ "$BLOCKED" = "secret_present" ] || fail "expected briefing_blocked secret_present, got $BLOCKED" +BLOCKED=$(sqlite3 "$DB" "select count(*) from entities where json_extract(properties, '\$.briefing_blocked') = 'secret_present';") +[ "$BLOCKED" = "2" ] || fail "expected two briefing_blocked secret_present entities, got $BLOCKED" FINDINGS=$(sqlite3 "$DB" "select count(*) from findings where rule_id = 'CLA-SEC-SECRET-DETECTED';") -[ "$FINDINGS" = "1" ] || fail "expected one CLA-SEC-SECRET-DETECTED finding, got $FINDINGS" +[ "$FINDINGS" = "2" ] || fail "expected two CLA-SEC-SECRET-DETECTED findings, got $FINDINGS" + +DOTENV_ANCHOR=$(sqlite3 "$DB" "select count(*) from entities where plugin_id = 'core' and kind = 'file' and source_file_path = '$DEMO_DIR/.env' and json_extract(properties, '\$.briefing_blocked') = 'secret_present';") +[ "$DOTENV_ANCHOR" = "1" ] || fail "expected .env core file anchor with briefing_blocked secret_present, got $DOTENV_ANCHOR" RUN_STATUS=$(sqlite3 "$DB" "select status from runs;") [ "$RUN_STATUS" = "completed" ] || fail "expected completed run, got $RUN_STATUS" From d2d15b1263dc52e75f5588f842a915007168fb34 Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Tue, 19 May 2026 03:18:48 +1000 Subject: [PATCH 003/187] fix(wp5): harden secret scanner review gaps --- crates/clarion-cli/src/main.rs | 14 +- crates/clarion-cli/src/secret_scan.rs | 7 +- crates/clarion-cli/tests/secret_scan.rs | 28 ++++ crates/clarion-mcp/src/lib.rs | 4 + crates/clarion-mcp/tests/storage_tools.rs | 55 +++++++ crates/clarion-scanner/src/baseline.rs | 39 ++++- crates/clarion-scanner/src/lib.rs | 1 + crates/clarion-scanner/src/patterns.rs | 44 +++-- crates/clarion-scanner/tests/scanner.rs | 186 +++++++++++++++++++++- 9 files changed, 349 insertions(+), 29 deletions(-) diff --git a/crates/clarion-cli/src/main.rs b/crates/clarion-cli/src/main.rs index 9b3c1a5a..71604599 100644 --- a/crates/clarion-cli/src/main.rs +++ b/crates/clarion-cli/src/main.rs @@ -11,14 +11,16 @@ use anyhow::Result; use clap::Parser; fn main() -> Result<()> { - // Load .env from CWD or any ancestor directory, before tracing setup so a + let cli = cli::Cli::parse(); + // Load .env before tracing setup for operator-facing commands so a // .env-supplied RUST_LOG is in effect by the time the filter is built. - // Existing process env vars win over .env values (dotenvy default), so an - // explicit `OPENROUTER_API_KEY=… clarion serve` still beats a checked-in - // dev .env. Missing .env is not an error — silently skip. - let _ = dotenvy::dotenv(); + // `analyze` is deliberately excluded: project .env contents are scanned + // as source sidecars and must not be imported into plugin subprocess + // environments before WP5's pre-ingest secret gate runs. + if !matches!(&cli.command, cli::Command::Analyze { .. }) { + let _ = dotenvy::dotenv(); + } init_tracing(); - let cli = cli::Cli::parse(); match cli.command { cli::Command::Install { force, path } => install::run(&path, force), cli::Command::Analyze { diff --git a/crates/clarion-cli/src/secret_scan.rs b/crates/clarion-cli/src/secret_scan.rs index 3391bd28..4dee1f48 100644 --- a/crates/clarion-cli/src/secret_scan.rs +++ b/crates/clarion-cli/src/secret_scan.rs @@ -84,11 +84,12 @@ pub(crate) fn pre_ingest( scanned_files.insert(canonical_file.clone()); let buf = fs::read(file).with_context(|| format!("read {}", file.display()))?; let detections = scanner.scan_bytes(&buf); + let baseline_file = project_relative_path(project_root, &canonical_file); let SuppressionResult { allowed, fired_entries, .. - } = baseline.suppress(detections, file); + } = baseline.suppress(detections, &baseline_file); if !allowed.is_empty() { all_allowed.extend( allowed @@ -239,6 +240,10 @@ fn normalize_project_path(root: &Path, path: &Path) -> PathBuf { canonical_or_original(&root.join(path)) } +fn project_relative_path(root: &Path, path: &Path) -> PathBuf { + path.strip_prefix(root).unwrap_or(path).to_path_buf() +} + fn display_relative(root: &Path, path: &Path) -> String { path.strip_prefix(root) .unwrap_or(path) diff --git a/crates/clarion-cli/tests/secret_scan.rs b/crates/clarion-cli/tests/secret_scan.rs index ad3bc7d1..91ebefd4 100644 --- a/crates/clarion-cli/tests/secret_scan.rs +++ b/crates/clarion-cli/tests/secret_scan.rs @@ -54,6 +54,11 @@ while True: }, }) elif method == "analyze_file": + if ( + os.environ.get("SECRETFIXTURE_ASSERT_ENV_ABSENT") + and os.environ.get("CLARION_DOTENV_SENTINEL") is not None + ): + raise SystemExit(42) path = msg["params"]["file_path"] source_path = os.environ.get("SECRETFIXTURE_SOURCE_OVERRIDE", path) name = "file_" + re.sub(r"[^A-Za-z0-9_]", "_", pathlib.Path(path).name) @@ -237,6 +242,29 @@ fn dotenv_sidecar_persists_finding_with_core_file_anchor() { assert_eq!(anchor_count, 1); } +#[test] +fn analyze_does_not_load_dotenv_into_plugin_environment() { + let project = tempfile::tempdir().unwrap(); + let plugin = tempfile::tempdir().unwrap(); + write_secret_fixture_plugin(plugin.path()); + install_project(project.path()); + std::fs::write(project.path().join("clean.sec"), b"nothing to see\n").unwrap(); + std::fs::write( + project.path().join(".env"), + b"CLARION_DOTENV_SENTINEL=ordinaryvalue\n", + ) + .unwrap(); + + clarion_bin() + .arg("analyze") + .arg(".") + .current_dir(project.path()) + .env("PATH", plugin_path(plugin.path())) + .env("SECRETFIXTURE_ASSERT_ENV_ABSENT", "1") + .assert() + .success(); +} + #[test] fn plugin_entity_for_unscanned_source_is_briefing_blocked() { let project = tempfile::tempdir().unwrap(); diff --git a/crates/clarion-mcp/src/lib.rs b/crates/clarion-mcp/src/lib.rs index 3bc3d9ef..6c735749 100644 --- a/crates/clarion-mcp/src/lib.rs +++ b/crates/clarion-mcp/src/lib.rs @@ -856,6 +856,10 @@ impl ServerState { return Ok(InferredDispatchStats::default()); }; + if briefing_block_reason(&read.caller).is_some() { + return Ok(InferredDispatchStats::default()); + } + if let Some(cached) = read.cached.clone() { return self.materialize_cached_inferred(read, cached).await; } diff --git a/crates/clarion-mcp/tests/storage_tools.rs b/crates/clarion-mcp/tests/storage_tools.rs index 43c3dcc5..069f842a 100644 --- a/crates/clarion-mcp/tests/storage_tools.rs +++ b/crates/clarion-mcp/tests/storage_tools.rs @@ -1707,6 +1707,61 @@ async fn callers_of_inferred_dispatches_and_materializes_recording_result() { handle.await.unwrap().unwrap(); } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn callers_of_inferred_skips_briefing_blocked_callers_without_llm() { + let (project, db_path) = open_project(); + let conn = Connection::open(&db_path).unwrap(); + add_dynamic_source(project.path()); + let source_path = project.path().join("demo.py"); + insert_entity( + &conn, + "python:function:demo.dynamic", + "function", + &source_path, + Some((9, 10)), + Some("python:module:demo"), + ); + insert_unresolved_call_site( + &conn, + "python:function:demo.entry", + "site-dynamic", + "dynamic", + ); + conn.execute( + "UPDATE entities SET properties = ?1 WHERE id = 'python:function:demo.entry'", + params![json!({"briefing_blocked": "secret_present"}).to_string()], + ) + .expect("mark caller briefing-blocked"); + drop(conn); + + let (writer, handle) = Writer::spawn(db_path.clone(), 50, 256).unwrap(); + let provider = Arc::new(AnyInferredProvider::new( + r#"{"edges":[{"site_key":"site-dynamic","target_id":"python:function:demo.dynamic","confidence":0.91,"rationale":"name match"}]}"#, + )); + let state = state_for_summary( + project.path(), + &db_path, + &writer, + provider.clone(), + llm_config(), + ); + + let envelope = call_tool( + &state, + "callers_of", + json!({"id": "python:function:demo.dynamic", "confidence": "inferred"}), + ) + .await; + + assert_eq!(envelope["ok"], true); + assert_eq!(envelope["result"]["callers"].as_array().unwrap().len(), 0); + assert!(provider.invocations().is_empty()); + + drop(state); + drop(writer); + handle.await.unwrap().unwrap(); +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn inferred_dispatch_prompt_uses_caller_source_range_not_whole_file() { let (project, db_path) = open_project(); diff --git a/crates/clarion-scanner/src/baseline.rs b/crates/clarion-scanner/src/baseline.rs index 25a80c1c..ddf2f20e 100644 --- a/crates/clarion-scanner/src/baseline.rs +++ b/crates/clarion-scanner/src/baseline.rs @@ -1,7 +1,7 @@ use std::{ collections::{BTreeMap, BTreeSet}, fs, - path::{Path, PathBuf}, + path::{Component, Path, PathBuf}, }; use serde::{Deserialize, Serialize}; @@ -48,6 +48,8 @@ pub enum BaselineError { UnsupportedVersion(String), #[error("baseline entries missing required field 'justification'")] MissingJustifications { entries: Vec }, + #[error("baseline path must be repository-relative and stay within the project: {file}")] + InvalidPath { file: PathBuf }, #[error("baseline entry has invalid hashed_secret at {file}:{line}: {details}")] InvalidHash { file: PathBuf, @@ -107,9 +109,11 @@ impl Baseline { } if entry.hashed_secret == detection.hashed_secret && entry.line_number == detection.line_number + && entry.rule_type == detection.detect_secrets_type { let key = ( (*baseline_path).clone(), + entry.rule_type.clone(), entry.hashed_secret, entry.line_number, ); @@ -137,6 +141,9 @@ impl Baseline { if raw.version != "1.0" { return Err(BaselineError::UnsupportedVersion(raw.version)); } + for file in raw.results.keys() { + validate_baseline_path(file)?; + } let mut missing_justifications = Vec::new(); for (file, raw_entries) in &raw.results { for entry in raw_entries { @@ -189,12 +196,30 @@ impl Baseline { fn entries_for(&self, file: &Path) -> Vec<(&PathBuf, &BaselineEntry)> { self.entries .iter() - .filter(|(candidate, _)| baseline_path_matches(file, candidate)) + .filter(|(candidate, _)| candidate.as_path() == file) .flat_map(|(path, entries)| entries.iter().map(move |entry| (path, entry))) .collect() } } +fn validate_baseline_path(file: &Path) -> Result<(), BaselineError> { + let invalid = file.as_os_str().is_empty() + || file.is_absolute() + || file.components().any(|component| { + matches!( + component, + Component::ParentDir | Component::RootDir | Component::Prefix(_) + ) + }); + if invalid { + Err(BaselineError::InvalidPath { + file: file.to_path_buf(), + }) + } else { + Ok(()) + } +} + #[derive(Debug, Serialize, Deserialize)] struct RawBaseline { version: String, @@ -208,11 +233,15 @@ struct RawBaselineEntry { rule_type: String, hashed_secret: String, line_number: u32, - #[serde(default)] + #[serde(default = "default_is_secret")] is_secret: bool, justification: Option, } +fn default_is_secret() -> bool { + true +} + impl From<&Baseline> for RawBaseline { fn from(value: &Baseline) -> Self { let results = value @@ -240,7 +269,3 @@ impl From<&Baseline> for RawBaseline { } } } - -fn baseline_path_matches(file: &Path, baseline_path: &Path) -> bool { - file == baseline_path || file.ends_with(baseline_path) -} diff --git a/crates/clarion-scanner/src/lib.rs b/crates/clarion-scanner/src/lib.rs index 94d939bd..a9e64b53 100644 --- a/crates/clarion-scanner/src/lib.rs +++ b/crates/clarion-scanner/src/lib.rs @@ -19,6 +19,7 @@ pub use patterns::{PatternMeta, Scanner}; #[derive(Debug, Clone, PartialEq, Eq)] pub struct Detection { pub rule_id: &'static str, + pub detect_secrets_type: &'static str, pub category: SecretCategory, pub byte_offset: usize, pub line_number: u32, diff --git a/crates/clarion-scanner/src/patterns.rs b/crates/clarion-scanner/src/patterns.rs index 9e802cd0..cacd82bc 100644 --- a/crates/clarion-scanner/src/patterns.rs +++ b/crates/clarion-scanner/src/patterns.rs @@ -63,7 +63,7 @@ impl Scanner { compiled_patterns, entropy_b64: EntropyTuning::BASE64, entropy_hex: EntropyTuning::HEX, - entropy_b64_re: Regex::new(r"\b[A-Za-z0-9+/]{20,}={0,2}\b") + entropy_b64_re: Regex::new(r"[A-Za-z0-9+/]{20,}={0,2}") .expect("base64 candidate regex compiles"), entropy_hex_re: Regex::new(r"\b[a-fA-F0-9]{40,}\b") .expect("hex candidate regex compiles"), @@ -133,14 +133,16 @@ impl Scanner { let source = String::from_utf8_lossy(bytes); for candidate in self.entropy_b64_re.find_iter(&source) { let candidate_bytes = &source.as_bytes()[candidate.start()..candidate.end()]; - if looks_like_sha256_base64(&source, candidate.start(), candidate.end()) { - continue; - } - if !range_overlaps(candidate.start(), candidate.end(), named_ranges) + if base64_candidate_has_boundaries( + source.as_bytes(), + candidate.start(), + candidate.end(), + ) && !range_overlaps(candidate.start(), candidate.end(), named_ranges) && self.entropy_b64.accepts(candidate_bytes) { detections.push(entropy_detection( "HighEntropyBase64", + "Base64 High Entropy String", bytes, candidate.start(), candidate.end(), @@ -154,6 +156,7 @@ impl Scanner { { detections.push(entropy_detection( "HighEntropyHex", + "Hex High Entropy String", bytes, candidate.start(), candidate.end(), @@ -167,6 +170,7 @@ fn detection_from_match(meta: &PatternMeta, bytes: &[u8], start: usize, end: usi let matched = &bytes[start..end]; Detection { rule_id: meta.rule_id, + detect_secrets_type: meta.detect_secrets_type, category: meta.category, byte_offset: start, line_number: line_number_for_offset(bytes, start), @@ -175,9 +179,16 @@ fn detection_from_match(meta: &PatternMeta, bytes: &[u8], start: usize, end: usi } } -fn entropy_detection(rule_id: &'static str, bytes: &[u8], start: usize, end: usize) -> Detection { +fn entropy_detection( + rule_id: &'static str, + detect_secrets_type: &'static str, + bytes: &[u8], + start: usize, + end: usize, +) -> Detection { Detection { rule_id, + detect_secrets_type, category: SecretCategory::HighEntropy, byte_offset: start, line_number: line_number_for_offset(bytes, start), @@ -262,7 +273,7 @@ fn default_pattern_meta() -> Vec { rule_id: "PrivateKeyHeader", detect_secrets_type: "Private Key", category: SecretCategory::PrivateKey, - pattern: r"-----BEGIN (?:RSA|EC|DSA|OPENSSH|PGP|ENCRYPTED) PRIVATE KEY-----", + pattern: r"-----BEGIN (?:(?:RSA|EC|DSA|OPENSSH|ENCRYPTED) PRIVATE KEY|PRIVATE KEY|PGP PRIVATE KEY BLOCK)-----", capture_group: None, }, PatternMeta { @@ -281,6 +292,19 @@ fn range_overlaps(start: usize, end: usize, ranges: &[(usize, usize)]) -> bool { .any(|(range_start, range_end)| start < *range_end && end > *range_start) } +fn base64_candidate_has_boundaries(bytes: &[u8], start: usize, end: usize) -> bool { + let before_ok = start == 0 || !is_base64_candidate_byte(bytes[start - 1]); + let after_ok = end == bytes.len() || !is_base64_candidate_byte(bytes[end]); + before_ok && after_ok +} + +fn is_base64_candidate_byte(byte: u8) -> bool { + matches!( + byte, + b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'+' | b'/' | b'=' + ) +} + fn line_is_comment(bytes: &[u8], offset: usize) -> bool { let line_start = bytes .get(..offset.min(bytes.len())) @@ -292,9 +316,3 @@ fn line_is_comment(bytes: &[u8], offset: usize) -> bool { .find(|byte| !byte.is_ascii_whitespace()) == Some(b'#') } - -fn looks_like_sha256_base64(source: &str, start: usize, end: usize) -> bool { - let candidate = &source[start..end]; - (candidate.len() == 44 && candidate.ends_with('=')) - || (candidate.len() == 43 && source.as_bytes().get(end) == Some(&b'=')) -} diff --git a/crates/clarion-scanner/tests/scanner.rs b/crates/clarion-scanner/tests/scanner.rs index 9341432e..4a6ca9ff 100644 --- a/crates/clarion-scanner/tests/scanner.rs +++ b/crates/clarion-scanner/tests/scanner.rs @@ -1,4 +1,5 @@ use clarion_scanner::{Baseline, BaselineError, Scanner, load_baseline}; +use sha1::{Digest, Sha1}; fn rules_for(input: &str) -> Vec<&'static str> { Scanner::new() @@ -55,7 +56,10 @@ fn named_patterns_detect_expected_credentials() { "JwtToken", ); assert_detects("-----BEGIN RSA PRIVATE KEY-----", "PrivateKeyHeader"); + assert_detects("-----BEGIN DSA PRIVATE KEY-----", "PrivateKeyHeader"); assert_detects("-----BEGIN OPENSSH PRIVATE KEY-----", "PrivateKeyHeader"); + assert_detects("-----BEGIN PRIVATE KEY-----", "PrivateKeyHeader"); + assert_detects("-----BEGIN PGP PRIVATE KEY BLOCK-----", "PrivateKeyHeader"); } #[test] @@ -80,12 +84,46 @@ fn entropy_detection_has_expected_bounds() { "uuid = 123e4567-e89b-12d3-a456-426614174000", "HighEntropyHex", ); - assert_not_detects( + assert_detects( "checksum = 47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=", "HighEntropyBase64", ); } +#[test] +fn padded_base64_detection_hashes_the_full_literal_for_baselines() { + let secret = "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU="; + let source = format!("checksum = {secret}\n"); + let detections = Scanner::new().scan_bytes(source.as_bytes()); + let detection = detections + .iter() + .find(|detection| detection.rule_id == "HighEntropyBase64") + .expect("padded base64 detection") + .clone(); + + assert_eq!(detection.matched_len, secret.len()); + assert_eq!(hex20(detection.hashed_secret), sha1_hex(secret.as_bytes())); + + let baseline = Baseline::from_yaml_str(&format!( + r#" +version: "1.0" +results: + "src/checksums.txt": + - type: "Base64 High Entropy String" + hashed_secret: "{}" + line_number: 1 + is_secret: false + justification: "Known non-secret digest checked into the fixture." +"#, + sha1_hex(secret.as_bytes()) + )) + .expect("baseline parses"); + + let result = baseline.suppress(detections, std::path::Path::new("src/checksums.txt")); + assert!(result.allowed.is_empty()); + assert_eq!(result.suppressed.len(), 1); +} + #[test] fn contextual_credentials_detect_assignments_but_not_comments_or_hashes() { assert_detects( @@ -140,12 +178,101 @@ results: )) .expect("baseline parses"); - let result = baseline.suppress(detections, std::path::Path::new("/repo/src/demo.py")); + let result = baseline.suppress(detections, std::path::Path::new("src/demo.py")); assert!(result.allowed.is_empty()); assert_eq!(result.suppressed.len(), 1); assert_eq!(result.fired_entries.len(), 1); } +#[test] +fn baseline_does_not_suppress_when_detector_type_differs() { + let scanner = Scanner::new(); + let detections = scanner.scan_bytes(b"key = 'AKIAIOSFODNN7EXAMPLE'\n"); + let detection = detections + .iter() + .find(|detection| detection.rule_id == "AwsAccessKeyId") + .expect("AWS detection") + .clone(); + let baseline = Baseline::from_yaml_str(&format!( + r#" +version: "1.0" +results: + "src/demo.py": + - type: "GitHub Token" + hashed_secret: "{}" + line_number: 1 + is_secret: false + justification: "Different detector type must not suppress this match." +"#, + hex20(detection.hashed_secret) + )) + .expect("baseline parses"); + + let result = baseline.suppress(detections, std::path::Path::new("src/demo.py")); + assert_eq!(result.allowed.len(), 1); + assert!(result.suppressed.is_empty()); + assert!(result.fired_entries.is_empty()); +} + +#[test] +fn baseline_without_explicit_is_secret_false_does_not_suppress() { + let scanner = Scanner::new(); + let detections = scanner.scan_bytes(b"key = 'AKIAIOSFODNN7EXAMPLE'\n"); + let detection = detections + .iter() + .find(|detection| detection.rule_id == "AwsAccessKeyId") + .expect("AWS detection") + .clone(); + let baseline = Baseline::from_yaml_str(&format!( + r#" +version: "1.0" +results: + "src/demo.py": + - type: "AWS Access Key" + hashed_secret: "{}" + line_number: 1 + justification: "The operator did not explicitly mark this as not secret." +"#, + hex20(detection.hashed_secret) + )) + .expect("baseline parses"); + + let result = baseline.suppress(detections, std::path::Path::new("src/demo.py")); + assert_eq!(result.allowed.len(), 1); + assert!(result.suppressed.is_empty()); + assert!(result.fired_entries.is_empty()); +} + +#[test] +fn baseline_does_not_suppress_same_suffix_in_sibling_directory() { + let scanner = Scanner::new(); + let detections = scanner.scan_bytes(b"key = 'AKIAIOSFODNN7EXAMPLE'\n"); + let detection = detections + .iter() + .find(|detection| detection.rule_id == "AwsAccessKeyId") + .expect("AWS detection") + .clone(); + let baseline = Baseline::from_yaml_str(&format!( + r#" +version: "1.0" +results: + "src/demo.py": + - type: "AWS Access Key" + hashed_secret: "{}" + line_number: 1 + is_secret: false + justification: "Only src/demo.py was reviewed." +"#, + hex20(detection.hashed_secret) + )) + .expect("baseline parses"); + + let result = baseline.suppress(detections, std::path::Path::new("vendor/src/demo.py")); + assert_eq!(result.allowed.len(), 1); + assert!(result.suppressed.is_empty()); + assert!(result.fired_entries.is_empty()); +} + #[test] fn baseline_is_secret_true_does_not_suppress() { let scanner = Scanner::new(); @@ -223,6 +350,48 @@ results: assert_eq!(entries[1].line, 8); } +#[test] +fn baseline_rejects_absolute_result_paths() { + let err = Baseline::from_yaml_str( + r#" +version: "1.0" +results: + "/tmp/secret.py": + - type: "AWS Access Key" + hashed_secret: "0123456789abcdef0123456789abcdef01234567" + line_number: 4 + is_secret: false + justification: "Invalid path must not be accepted." +"#, + ) + .expect_err("absolute baseline path should fail"); + assert!( + err.to_string().contains("repository-relative"), + "unexpected error: {err}" + ); +} + +#[test] +fn baseline_rejects_parent_dir_result_paths() { + let err = Baseline::from_yaml_str( + r#" +version: "1.0" +results: + "../secret.py": + - type: "AWS Access Key" + hashed_secret: "0123456789abcdef0123456789abcdef01234567" + line_number: 4 + is_secret: false + justification: "Invalid path must not be accepted." +"#, + ) + .expect_err("escaping baseline path should fail"); + assert!( + err.to_string().contains("repository-relative"), + "unexpected error: {err}" + ); +} + #[test] fn absent_baseline_file_is_empty() { let dir = tempfile::tempdir().expect("tempdir"); @@ -258,3 +427,16 @@ fn hex20(bytes: [u8; 20]) -> String { } out } + +fn sha1_hex(bytes: &[u8]) -> String { + const HEX: &[u8; 16] = b"0123456789abcdef"; + let mut hasher = Sha1::new(); + hasher.update(bytes); + let digest = hasher.finalize(); + let mut out = String::new(); + for byte in digest { + out.push(char::from(HEX[usize::from(byte >> 4)])); + out.push(char::from(HEX[usize::from(byte & 0x0f)])); + } + out +} From 7d0e9194542ec0192eac934d625666a757418f2d Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Tue, 19 May 2026 03:46:17 +1000 Subject: [PATCH 004/187] fix(wp5): keep secret scan wiring delegated --- crates/clarion-cli/src/analyze.rs | 209 +----------------- crates/clarion-cli/src/main.rs | 1 + crates/clarion-cli/src/run_lifecycle.rs | 20 ++ crates/clarion-cli/src/secret_scan.rs | 62 +++--- crates/clarion-cli/src/secret_scan/anchors.rs | 116 ++++++++++ .../clarion-cli/src/secret_scan/baseline.rs | 43 ++++ crates/clarion-cli/src/secret_scan/files.rs | 75 +++++++ 7 files changed, 292 insertions(+), 234 deletions(-) create mode 100644 crates/clarion-cli/src/run_lifecycle.rs create mode 100644 crates/clarion-cli/src/secret_scan/anchors.rs create mode 100644 crates/clarion-cli/src/secret_scan/baseline.rs create mode 100644 crates/clarion-cli/src/secret_scan/files.rs diff --git a/crates/clarion-cli/src/analyze.rs b/crates/clarion-cli/src/analyze.rs index db47b7d5..1021e2a9 100644 --- a/crates/clarion-cli/src/analyze.rs +++ b/crates/clarion-cli/src/analyze.rs @@ -128,7 +128,8 @@ pub(crate) async fn run_with_options(project_path: PathBuf, options: AnalyzeOpti discovery_errors.join("; ") ); tracing::error!(run_id = %run_id, reason = %reason, "failing run: discovery errors"); - begin_run(&writer, &run_id, &analyze_config_json, &started_at).await?; + crate::run_lifecycle::begin_run(&writer, &run_id, &analyze_config_json, &started_at) + .await?; let completed_at = iso8601_now(); writer .send_wait(|ack| WriterCmd::FailRun { @@ -155,7 +156,8 @@ pub(crate) async fn run_with_options(project_path: PathBuf, options: AnalyzeOpti } tracing::warn!(run_id = %run_id, "no plugins discovered"); - begin_run(&writer, &run_id, &analyze_config_json, &started_at).await?; + crate::run_lifecycle::begin_run(&writer, &run_id, &analyze_config_json, &started_at) + .await?; let completed_at = iso8601_now(); writer .send_wait(|ack| WriterCmd::CommitRun { @@ -207,14 +209,14 @@ pub(crate) async fn run_with_options(project_path: PathBuf, options: AnalyzeOpti let source_files = collect_source_files(&project_root, &wanted_extensions); tracing::info!(file_count = source_files.len(), "source tree walk complete"); - let secret_scan_files = collect_secret_scan_files(&project_root, &source_files); + let secret_scan_files = crate::secret_scan::collect_scan_files(&project_root, &source_files); tracing::info!( file_count = secret_scan_files.len(), "secret scan file walk complete" ); - let secret_scan_outcome = + let mut secret_scan_outcome = crate::secret_scan::pre_ingest(&project_root, &secret_scan_files, &options.secret_scan)?; - begin_run(&writer, &run_id, &analyze_config_json, &started_at).await?; + crate::run_lifecycle::begin_run(&writer, &run_id, &analyze_config_json, &started_at).await?; // ── Per-plugin processing ───────────────────────────────────────────────── // @@ -243,8 +245,6 @@ pub(crate) async fn run_with_options(project_path: PathBuf, options: AnalyzeOpti let mut run_outcome: RunOutcome = RunOutcome::Completed; let mut breaker = CrashLoopBreaker::default(); let mut crash_reasons: Vec = Vec::new(); - let mut secret_finding_anchors: BTreeMap = BTreeMap::new(); - 'plugins: for plugin in plugins { let plugin_id = plugin.manifest.plugin.plugin_id.clone(); let plugin_extensions: BTreeSet = plugin @@ -367,7 +367,7 @@ pub(crate) async fn run_with_options(project_path: PathBuf, options: AnalyzeOpti // edge FK references resolve at insert time (B.3 §5). let entity_count = entities.len() as u64; let edge_count = edges.len() as u64; - remember_secret_finding_anchors(&entities, &mut secret_finding_anchors); + secret_scan_outcome.remember_finding_anchors(&entities); let mut insert_err: Option = None; for (id_str, record) in entities { let res = writer @@ -443,30 +443,9 @@ pub(crate) async fn run_with_options(project_path: PathBuf, options: AnalyzeOpti } if !matches!(run_outcome, RunOutcome::HardFailed { .. }) - && let Err(e) = ensure_secret_finding_anchors( - &writer, - &project_root, - &started_at, - &secret_scan_outcome, - &mut secret_finding_anchors, - ) - .await - { - tracing::error!(run_id = %run_id, error = %e, "secret finding anchor persistence failed"); - run_outcome = RunOutcome::HardFailed { - reason: format!("secret finding anchor persistence failed: {e:#}"), - }; - } - - if !matches!(run_outcome, RunOutcome::HardFailed { .. }) - && let Err(e) = crate::secret_scan::emit_findings( - &writer, - &run_id, - &started_at, - &secret_scan_outcome, - &secret_finding_anchors, - ) - .await + && let Err(e) = secret_scan_outcome + .persist_findings(&writer, &run_id, &project_root, &started_at) + .await { tracing::error!(run_id = %run_id, error = %e, "secret finding persistence failed"); run_outcome = RunOutcome::HardFailed { @@ -638,24 +617,6 @@ pub(crate) async fn run_with_options(project_path: PathBuf, options: AnalyzeOpti Ok(()) } -async fn begin_run( - writer: &Writer, - run_id: &str, - analyze_config_json: &str, - started_at: &str, -) -> Result<()> { - writer - .send_wait(|ack| WriterCmd::BeginRun { - run_id: run_id.to_owned(), - config_json: analyze_config_json.to_owned(), - started_at: started_at.to_owned(), - ack, - }) - .await - .map_err(|e| anyhow::anyhow!("{e}")) - .context("BeginRun") -} - // ── Phase 3 subsystem materialisation ───────────────────────────────────────── #[derive(Debug, Clone)] @@ -1559,108 +1520,6 @@ fn absolute_from_import_submodule_target( .then_some(candidate) } -fn remember_secret_finding_anchors( - entities: &[(String, EntityRecord)], - anchors: &mut BTreeMap, -) { - for (id, record) in entities { - let Some(path) = record.source_file_path.as_deref() else { - continue; - }; - let key = Path::new(path) - .canonicalize() - .unwrap_or_else(|_| PathBuf::from(path)); - if record.kind == "module" || !anchors.contains_key(&key) { - anchors.insert(key, id.clone()); - } - } -} - -async fn ensure_secret_finding_anchors( - writer: &Writer, - project_root: &Path, - started_at: &str, - outcome: &crate::secret_scan::SecretScanOutcome, - anchors: &mut BTreeMap, -) -> Result<()> { - for file in outcome.finding_files() { - let key = canonical_or_original(&file); - if anchors.contains_key(&key) { - continue; - } - let id = secret_finding_anchor_id(project_root, &key); - let relative = display_relative_path(project_root, &key); - let short_name = key - .file_name() - .and_then(|name| name.to_str()) - .unwrap_or(&relative) - .to_owned(); - let mut properties = serde_json::Map::new(); - properties.insert("finding_anchor".to_owned(), serde_json::json!(true)); - if let Some(reason) = outcome.briefing_blocks.get(&key) { - properties.insert( - "briefing_blocked".to_owned(), - serde_json::Value::String(reason.clone()), - ); - } - let record = EntityRecord { - id: id.clone(), - plugin_id: "core".to_owned(), - kind: "file".to_owned(), - name: relative, - short_name, - parent_id: None, - source_file_id: None, - source_file_path: Some(key.display().to_string()), - source_byte_start: None, - source_byte_end: None, - source_line_start: None, - source_line_end: None, - properties_json: serde_json::Value::Object(properties).to_string(), - content_hash: file_content_hash(&key), - summary_json: None, - wardline_json: None, - first_seen_commit: None, - last_seen_commit: None, - created_at: started_at.to_owned(), - updated_at: started_at.to_owned(), - }; - writer - .send_wait(|ack| WriterCmd::InsertEntity { - entity: Box::new(record), - ack, - }) - .await - .map_err(|e| anyhow::anyhow!("{e}")) - .with_context(|| format!("InsertEntity for secret finding anchor {id}"))?; - anchors.insert(key, id); - } - Ok(()) -} - -fn secret_finding_anchor_id(project_root: &Path, file: &Path) -> String { - let relative = display_relative_path(project_root, file); - let digest = blake3::hash(relative.as_bytes()).to_hex().to_string(); - format!("core:file:{digest}") -} - -fn canonical_or_original(path: &Path) -> PathBuf { - path.canonicalize().unwrap_or_else(|_| path.to_path_buf()) -} - -fn display_relative_path(root: &Path, path: &Path) -> String { - path.strip_prefix(root) - .unwrap_or(path) - .display() - .to_string() -} - -fn file_content_hash(path: &Path) -> Option { - fs::read(path) - .ok() - .map(|bytes| blake3::hash(&bytes).to_hex().to_string()) -} - /// Map an `AcceptedEntity` to an `EntityRecord` for the writer-actor. fn map_entity_to_record( entity: &AcceptedEntity, @@ -1956,52 +1815,6 @@ fn collect_source_files(root: &Path, wanted_extensions: &BTreeSet) -> Ve out } -fn collect_secret_scan_files(root: &Path, source_files: &[PathBuf]) -> Vec { - let mut out: BTreeSet = source_files - .iter() - .map(|path| canonical_or_original(path)) - .collect(); - for path in collect_secret_scan_sidecars(root) { - out.insert(canonical_or_original(&path)); - } - out.into_iter().collect() -} - -fn collect_secret_scan_sidecars(root: &Path) -> Vec { - let mut out = Vec::new(); - let mut builder = WalkBuilder::new(root); - builder - .follow_links(false) - .hidden(false) - .ignore(false) - .git_ignore(false) - .git_global(false) - .git_exclude(false) - .parents(false) - .require_git(false) - .filter_entry(|entry| !is_skipped_dir(entry)); - - for entry in builder.build().filter_map(std::result::Result::ok) { - let Some(file_type) = entry.file_type() else { - continue; - }; - let path = entry.into_path(); - if file_type.is_file() && is_secret_scan_sidecar(&path) { - out.push(path); - } - } - out -} - -fn is_secret_scan_sidecar(path: &Path) -> bool { - let file_name = path.file_name().and_then(|name| name.to_str()); - file_name.is_some_and(|name| name == ".env" || name.starts_with(".env.")) - || path - .extension() - .and_then(|extension| extension.to_str()) - .is_some_and(|extension| extension.eq_ignore_ascii_case("env")) -} - fn is_skipped_dir(entry: &DirEntry) -> bool { entry .file_type() diff --git a/crates/clarion-cli/src/main.rs b/crates/clarion-cli/src/main.rs index 71604599..4589d077 100644 --- a/crates/clarion-cli/src/main.rs +++ b/crates/clarion-cli/src/main.rs @@ -3,6 +3,7 @@ mod cli; mod clustering; mod config; mod install; +mod run_lifecycle; mod secret_scan; mod serve; mod stats; diff --git a/crates/clarion-cli/src/run_lifecycle.rs b/crates/clarion-cli/src/run_lifecycle.rs new file mode 100644 index 00000000..6370e2bf --- /dev/null +++ b/crates/clarion-cli/src/run_lifecycle.rs @@ -0,0 +1,20 @@ +use anyhow::{Context, Result}; +use clarion_storage::{Writer, commands::WriterCmd}; + +pub(crate) async fn begin_run( + writer: &Writer, + run_id: &str, + analyze_config_json: &str, + started_at: &str, +) -> Result<()> { + writer + .send_wait(|ack| WriterCmd::BeginRun { + run_id: run_id.to_owned(), + config_json: analyze_config_json.to_owned(), + started_at: started_at.to_owned(), + ack, + }) + .await + .map_err(|e| anyhow::anyhow!("{e}")) + .context("BeginRun") +} diff --git a/crates/clarion-cli/src/secret_scan.rs b/crates/clarion-cli/src/secret_scan.rs index 4dee1f48..6ca0de73 100644 --- a/crates/clarion-cli/src/secret_scan.rs +++ b/crates/clarion-cli/src/secret_scan.rs @@ -13,17 +13,19 @@ use std::{ path::{Path, PathBuf}, }; +mod anchors; +mod baseline; +mod files; mod findings; use anyhow::{Context, Result}; -use clarion_scanner::{Baseline, BaselineError, Detection, Scanner, SuppressionResult}; -pub(crate) use findings::emit_findings; +use clarion_scanner::{Detection, Scanner, SuppressionResult}; +use clarion_storage::{Writer, commands::EntityRecord}; +pub(crate) use files::collect_scan_files; use findings::{PendingFinding, secret_detected_finding}; use serde_json::json; const SECRET_OVERRIDE_ALLOWED: &str = "CLA-SEC-UNREDACTED-SECRETS-ALLOWED"; -const BASELINE_NO_JUSTIFICATION: &str = "CLA-INFRA-SECRET-BASELINE-NO-JUSTIFICATION"; -const BASELINE_MATCH: &str = "CLA-INFRA-SECRET-BASELINE-MATCH"; const OVERRIDE_UNCONFIRMED: &str = "CLA-INFRA-SECRET-OVERRIDE-UNCONFIRMED"; const CONFIRM_TOKEN: &str = "yes-i-understand"; @@ -37,6 +39,7 @@ pub(crate) struct SecretScanOptions { pub(crate) struct SecretScanOutcome { pub(crate) briefing_blocks: BTreeMap, findings: Vec, + finding_anchors: BTreeMap, override_files: Vec, scanned_files: BTreeSet, } @@ -65,6 +68,20 @@ impl SecretScanOutcome { .collect::>() ); } + + pub(crate) fn remember_finding_anchors(&mut self, entities: &[(String, EntityRecord)]) { + anchors::remember_finding_anchors(self, entities); + } + + pub(crate) async fn persist_findings( + &mut self, + writer: &Writer, + run_id: &str, + project_root: &Path, + started_at: &str, + ) -> Result<()> { + anchors::ensure_and_emit_findings(self, writer, run_id, project_root, started_at).await + } } pub(crate) fn pre_ingest( @@ -73,7 +90,7 @@ pub(crate) fn pre_ingest( options: &SecretScanOptions, ) -> Result { let scanner = Scanner::new(); - let (baseline, mut findings) = load_baseline_for_scan(project_root)?; + let (baseline, mut findings) = baseline::load_for_scan(project_root)?; let mut per_file = Vec::new(); let mut all_allowed = Vec::new(); let mut baseline_matches = Vec::new(); @@ -100,7 +117,7 @@ pub(crate) fn pre_ingest( } baseline_matches.extend(fired_entries.into_iter().map(|entry| PendingFinding { file_path: normalize_project_path(project_root, &entry.file_path), - rule_id: BASELINE_MATCH, + rule_id: baseline::baseline_match_rule_id(), kind: "fact", severity: "INFO", confidence: Some(1.0), @@ -162,39 +179,12 @@ pub(crate) fn pre_ingest( Ok(SecretScanOutcome { briefing_blocks, findings, + finding_anchors: BTreeMap::new(), override_files: override_files.into_iter().collect(), scanned_files, }) } -fn load_baseline_for_scan(project_root: &Path) -> Result<(Baseline, Vec)> { - let path = project_root.join(".clarion/secrets-baseline.yaml"); - match clarion_scanner::load_baseline(&path) { - Ok(baseline) => Ok((baseline, Vec::new())), - Err(BaselineError::MissingJustifications { entries }) => Ok(( - Baseline::empty(), - entries - .into_iter() - .map(|entry| PendingFinding { - file_path: normalize_project_path(project_root, &entry.file), - rule_id: BASELINE_NO_JUSTIFICATION, - kind: "defect", - severity: "ERROR", - confidence: Some(1.0), - confidence_basis: Some("baseline_schema"), - message: format!( - "Secret baseline entry missing justification at {}:{}", - entry.file.display(), - entry.line - ), - evidence: json!({"file_path": entry.file, "line_number": entry.line}), - }) - .collect(), - )), - Err(err) => Err(err).context("load secret baseline"), - } -} - fn confirm_override_if_needed( allowed: &[(PathBuf, Detection)], options: &SecretScanOptions, @@ -232,7 +222,7 @@ fn abort_unconfirmed_override() -> ! { std::process::exit(78); } -fn canonical_or_original(path: &Path) -> PathBuf { +pub(super) fn canonical_or_original(path: &Path) -> PathBuf { path.canonicalize().unwrap_or_else(|_| path.to_path_buf()) } @@ -244,7 +234,7 @@ fn project_relative_path(root: &Path, path: &Path) -> PathBuf { path.strip_prefix(root).unwrap_or(path).to_path_buf() } -fn display_relative(root: &Path, path: &Path) -> String { +pub(super) fn display_relative(root: &Path, path: &Path) -> String { path.strip_prefix(root) .unwrap_or(path) .display() diff --git a/crates/clarion-cli/src/secret_scan/anchors.rs b/crates/clarion-cli/src/secret_scan/anchors.rs new file mode 100644 index 00000000..9f358864 --- /dev/null +++ b/crates/clarion-cli/src/secret_scan/anchors.rs @@ -0,0 +1,116 @@ +use std::{fs, path::Path}; + +use anyhow::{Context, Result}; +use clarion_storage::{ + Writer, + commands::{EntityRecord, WriterCmd}, +}; + +use super::{SecretScanOutcome, canonical_or_original, display_relative}; +use crate::secret_scan::findings::emit_findings; + +pub(super) fn remember_finding_anchors( + outcome: &mut SecretScanOutcome, + entities: &[(String, EntityRecord)], +) { + for (id, record) in entities { + let Some(path) = record.source_file_path.as_deref() else { + continue; + }; + let key = canonical_or_original(Path::new(path)); + if record.kind == "module" || !outcome.finding_anchors.contains_key(&key) { + outcome.finding_anchors.insert(key, id.clone()); + } + } +} + +pub(super) async fn ensure_and_emit_findings( + outcome: &mut SecretScanOutcome, + writer: &Writer, + run_id: &str, + project_root: &Path, + started_at: &str, +) -> Result<()> { + ensure_finding_anchors(outcome, writer, project_root, started_at).await?; + emit_findings( + writer, + run_id, + started_at, + outcome, + &outcome.finding_anchors, + ) + .await +} + +async fn ensure_finding_anchors( + outcome: &mut SecretScanOutcome, + writer: &Writer, + project_root: &Path, + started_at: &str, +) -> Result<()> { + for file in outcome.finding_files() { + let key = canonical_or_original(&file); + if outcome.finding_anchors.contains_key(&key) { + continue; + } + let id = secret_finding_anchor_id(project_root, &key); + let relative = display_relative(project_root, &key); + let short_name = key + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or(&relative) + .to_owned(); + let mut properties = serde_json::Map::new(); + properties.insert("finding_anchor".to_owned(), serde_json::json!(true)); + if let Some(reason) = outcome.briefing_blocks.get(&key) { + properties.insert( + "briefing_blocked".to_owned(), + serde_json::Value::String(reason.clone()), + ); + } + let record = EntityRecord { + id: id.clone(), + plugin_id: "core".to_owned(), + kind: "file".to_owned(), + name: relative, + short_name, + parent_id: None, + source_file_id: None, + source_file_path: Some(key.display().to_string()), + source_byte_start: None, + source_byte_end: None, + source_line_start: None, + source_line_end: None, + properties_json: serde_json::Value::Object(properties).to_string(), + content_hash: file_content_hash(&key), + summary_json: None, + wardline_json: None, + first_seen_commit: None, + last_seen_commit: None, + created_at: started_at.to_owned(), + updated_at: started_at.to_owned(), + }; + writer + .send_wait(|ack| WriterCmd::InsertEntity { + entity: Box::new(record), + ack, + }) + .await + .map_err(|e| anyhow::anyhow!("{e}")) + .with_context(|| format!("InsertEntity for secret finding anchor {id}"))?; + outcome.finding_anchors.insert(key, id); + } + Ok(()) +} + +fn secret_finding_anchor_id(project_root: &Path, file: &Path) -> String { + let relative = display_relative(project_root, file); + let digest = blake3::hash(relative.as_bytes()).to_hex().to_string(); + format!("core:file:{digest}") +} + +fn file_content_hash(path: &Path) -> Option { + fs::read(path) + .ok() + .map(|bytes| blake3::hash(&bytes).to_hex().to_string()) +} diff --git a/crates/clarion-cli/src/secret_scan/baseline.rs b/crates/clarion-cli/src/secret_scan/baseline.rs new file mode 100644 index 00000000..793a8c21 --- /dev/null +++ b/crates/clarion-cli/src/secret_scan/baseline.rs @@ -0,0 +1,43 @@ +use std::path::Path; + +use anyhow::{Context, Result}; +use clarion_scanner::{Baseline, BaselineError}; +use serde_json::json; + +use super::normalize_project_path; +use crate::secret_scan::findings::PendingFinding; + +const BASELINE_NO_JUSTIFICATION: &str = "CLA-INFRA-SECRET-BASELINE-NO-JUSTIFICATION"; +const BASELINE_MATCH: &str = "CLA-INFRA-SECRET-BASELINE-MATCH"; + +pub(super) fn load_for_scan(project_root: &Path) -> Result<(Baseline, Vec)> { + let path = project_root.join(".clarion/secrets-baseline.yaml"); + match clarion_scanner::load_baseline(&path) { + Ok(baseline) => Ok((baseline, Vec::new())), + Err(BaselineError::MissingJustifications { entries }) => Ok(( + Baseline::empty(), + entries + .into_iter() + .map(|entry| PendingFinding { + file_path: normalize_project_path(project_root, &entry.file), + rule_id: BASELINE_NO_JUSTIFICATION, + kind: "defect", + severity: "ERROR", + confidence: Some(1.0), + confidence_basis: Some("baseline_schema"), + message: format!( + "Secret baseline entry missing justification at {}:{}", + entry.file.display(), + entry.line + ), + evidence: json!({"file_path": entry.file, "line_number": entry.line}), + }) + .collect(), + )), + Err(err) => Err(err).context("load secret baseline"), + } +} + +pub(super) fn baseline_match_rule_id() -> &'static str { + BASELINE_MATCH +} diff --git a/crates/clarion-cli/src/secret_scan/files.rs b/crates/clarion-cli/src/secret_scan/files.rs new file mode 100644 index 00000000..0f51fec9 --- /dev/null +++ b/crates/clarion-cli/src/secret_scan/files.rs @@ -0,0 +1,75 @@ +use std::{ + collections::BTreeSet, + path::{Path, PathBuf}, +}; + +use ignore::{DirEntry, WalkBuilder}; + +use super::canonical_or_original; + +const SKIP_DIRS: &[&str] = &[ + ".clarion", + ".git", + ".hg", + ".svn", + ".jj", + ".venv", + "__pycache__", + "node_modules", +]; + +pub(crate) fn collect_scan_files(root: &Path, source_files: &[PathBuf]) -> Vec { + let mut out: BTreeSet = source_files + .iter() + .map(|path| canonical_or_original(path)) + .collect(); + for path in collect_secret_scan_sidecars(root) { + out.insert(canonical_or_original(&path)); + } + out.into_iter().collect() +} + +fn collect_secret_scan_sidecars(root: &Path) -> Vec { + let mut out = Vec::new(); + let mut builder = WalkBuilder::new(root); + builder + .follow_links(false) + .hidden(false) + .ignore(false) + .git_ignore(false) + .git_global(false) + .git_exclude(false) + .parents(false) + .require_git(false) + .filter_entry(|entry| !is_skipped_dir(entry)); + + for entry in builder.build().filter_map(std::result::Result::ok) { + let Some(file_type) = entry.file_type() else { + continue; + }; + let path = entry.into_path(); + if file_type.is_file() && is_secret_scan_sidecar(&path) { + out.push(path); + } + } + out +} + +fn is_secret_scan_sidecar(path: &Path) -> bool { + let file_name = path.file_name().and_then(|name| name.to_str()); + file_name.is_some_and(|name| name == ".env" || name.starts_with(".env.")) + || path + .extension() + .and_then(|extension| extension.to_str()) + .is_some_and(|extension| extension.eq_ignore_ascii_case("env")) +} + +fn is_skipped_dir(entry: &DirEntry) -> bool { + entry + .file_type() + .is_some_and(|file_type| file_type.is_dir()) + && entry + .file_name() + .to_str() + .is_some_and(|name| SKIP_DIRS.contains(&name)) +} From d6ee0b0eca1613f3cf32f09448b9b3dd5e6d37d9 Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Tue, 19 May 2026 06:11:06 +1000 Subject: [PATCH 005/187] feat: Add Codex CLI and Claude CLI support as local-login LLM providers - Introduced CodexCliConfig and ClaudeCliConfig structs to manage configurations for Codex and Claude CLI providers. - Updated LlmConfig to include new CLI configurations and modified default implementations. - Enhanced LlmProviderKind enum to support new Codex CLI and Claude CLI providers. - Implemented provider selection logic for Codex CLI and Claude CLI, requiring live opt-in for usage. - Added tests to validate the behavior of Codex CLI and Claude CLI providers under various configurations. - Updated documentation to include details on configuring and using Codex CLI and Claude CLI providers. - Enhanced usage statistics to track cached input tokens for better performance metrics. --- crates/clarion-cli/src/install.rs | 15 + crates/clarion-cli/src/serve.rs | 33 + crates/clarion-cli/tests/serve.rs | 300 +++++ crates/clarion-core/src/lib.rs | 7 +- crates/clarion-core/src/llm_provider.rs | 1172 ++++++++++++++++++- crates/clarion-mcp/src/config.rs | 185 +++ crates/clarion-mcp/src/lib.rs | 43 +- crates/clarion-mcp/tests/storage_tools.rs | 4 + docs/operator/README.md | 2 + docs/operator/coding-agent-llm-providers.md | 157 +++ docs/operator/openrouter.md | 9 +- 11 files changed, 1906 insertions(+), 21 deletions(-) create mode 100644 docs/operator/coding-agent-llm-providers.md diff --git a/crates/clarion-cli/src/install.rs b/crates/clarion-cli/src/install.rs index a0d83b36..33e96c36 100644 --- a/crates/clarion-cli/src/install.rs +++ b/crates/clarion-cli/src/install.rs @@ -37,6 +37,21 @@ llm_policy:\n\ attribution:\n\ referer: https://github.com/qacona/clarion\n\ title: Clarion\n\ + codex_cli:\n\ + executable: codex\n\ + model: null\n\ + profile: null\n\ + sandbox: read-only\n\ + timeout_seconds: 300\n\ + claude_cli:\n\ + executable: claude\n\ + model: null\n\ + permission_mode: plan\n\ + tools: []\n\ + timeout_seconds: 300\n\ + max_turns: 2\n\ + no_session_persistence: true\n\ + exclude_dynamic_system_prompt_sections: true\n\ model_id: anthropic/claude-sonnet-4.6\n\ session_token_ceiling: 1000000\n\ max_inferred_edges_per_caller: 8\n\ diff --git a/crates/clarion-cli/src/serve.rs b/crates/clarion-cli/src/serve.rs index 20820488..bde55c29 100644 --- a/crates/clarion-cli/src/serve.rs +++ b/crates/clarion-cli/src/serve.rs @@ -5,6 +5,7 @@ use std::sync::Arc; use anyhow::{Context, Result, anyhow, ensure}; use clarion_core::{ + ClaudeCliProvider, ClaudeCliProviderConfig, CodexCliProvider, CodexCliProviderConfig, LlmProvider, OpenRouterProvider, OpenRouterProviderConfig, Recording, RecordingProvider, }; use clarion_mcp::config::{McpConfig, ProviderSelection, select_provider_with_env}; @@ -116,6 +117,38 @@ fn build_llm_provider( .context("build OpenRouter LLM provider")?; Ok(Some(Arc::new(provider))) } + ProviderSelection::CodexCli => { + let provider = CodexCliProvider::from_config(CodexCliProviderConfig { + executable: config.llm.codex_cli.executable.clone(), + project_root: project_root.to_owned(), + model_id: config.llm.model_id.clone(), + model: config.llm.codex_cli.model.clone(), + profile: config.llm.codex_cli.profile.clone(), + sandbox: config.llm.codex_cli.sandbox.clone(), + timeout_seconds: config.llm.codex_cli.timeout_seconds, + }) + .context("build Codex CLI LLM provider")?; + Ok(Some(Arc::new(provider))) + } + ProviderSelection::ClaudeCli => { + let provider = ClaudeCliProvider::from_config(ClaudeCliProviderConfig { + executable: config.llm.claude_cli.executable.clone(), + project_root: project_root.to_owned(), + model_id: config.llm.model_id.clone(), + model: config.llm.claude_cli.model.clone(), + permission_mode: config.llm.claude_cli.permission_mode.clone(), + tools: config.llm.claude_cli.tools.clone(), + timeout_seconds: config.llm.claude_cli.timeout_seconds, + max_turns: config.llm.claude_cli.max_turns, + no_session_persistence: config.llm.claude_cli.no_session_persistence, + exclude_dynamic_system_prompt_sections: config + .llm + .claude_cli + .exclude_dynamic_system_prompt_sections, + }) + .context("build Claude CLI LLM provider")?; + Ok(Some(Arc::new(provider))) + } } } diff --git a/crates/clarion-cli/tests/serve.rs b/crates/clarion-cli/tests/serve.rs index 11670f26..35b102e5 100644 --- a/crates/clarion-cli/tests/serve.rs +++ b/crates/clarion-cli/tests/serve.rs @@ -1,5 +1,7 @@ use std::fs; use std::io::Write; +use std::os::unix::fs::PermissionsExt; +use std::path::Path; use std::process::{Command as StdCommand, Stdio}; use assert_cmd::Command; @@ -8,6 +10,7 @@ use clarion_core::{ plugin::{ContentLengthCeiling, Frame, read_frame, write_frame}, }; use rusqlite::{Connection, params}; +use serde_json::Value; fn clarion_bin() -> Command { Command::cargo_bin("clarion").expect("clarion binary") @@ -211,3 +214,300 @@ fn serve_wires_recording_llm_provider_and_writer_for_cached_summary_touches() { .expect("read touched cache row"); assert_ne!(touched, "old-touch"); } + +#[test] +fn serve_routes_summary_miss_through_codex_cli_provider() { + let dir = tempfile::tempdir().expect("temp project"); + clarion_bin() + .args(["install", "--path"]) + .arg(dir.path()) + .env("PATH", "") + .assert() + .success(); + seed_summary_entity(dir.path()); + + let fake_codex = dir.path().join("fake-codex"); + let prompt_log = dir.path().join("codex-prompt.log"); + fs::write( + &fake_codex, + format!( + r#"#!/usr/bin/env bash +set -euo pipefail +out="" +schema="" +while [[ $# -gt 0 ]]; do + case "$1" in + exec|--json|-) + shift + ;; + --output-last-message) + out="$2" + shift 2 + ;; + --output-schema) + schema="$2" + shift 2 + ;; + --sandbox|--cd|-c|--model|--profile) + shift 2 + ;; + *) + shift + ;; + esac +done +stdin_prompt="$(cat)" +printf '%s' "$stdin_prompt" > "{prompt_log}" +case "$stdin_prompt" in + *"Prompt contract: clarion-agent-provider-v1"*"Do not inspect additional files"*"Source excerpt:"*) ;; + *) echo "missing Clarion agent prompt contract" >&2; exit 32 ;; +esac +grep -q '"purpose"' "$schema" +printf '%s\n' '{{"usage":{{"input_tokens":31,"cached_input_tokens":9,"output_tokens":7,"total_tokens":38}}}}' +printf '%s' '{{"purpose":"via codex serve","behavior":"served through fake Codex CLI","relationships":"","risks":""}}' > "$out" +"#, + prompt_log = prompt_log.display() + ), + ) + .expect("write fake codex"); + make_executable(&fake_codex); + write_provider_config( + dir.path(), + "codex_cli", + r#" + codex_cli: + executable: "__EXECUTABLE__" + model: null + profile: null + sandbox: read-only + timeout_seconds: 5 +"#, + &fake_codex, + ); + + let envelope = call_summary_through_serve(dir.path()); + + assert_eq!(envelope["ok"], true); + assert_eq!(envelope["result"]["cache"]["hit"], false); + assert_eq!(envelope["result"]["summary"]["purpose"], "via codex serve"); + assert_eq!(envelope["result"]["usage"]["tokens_cached_input"], 9); + assert_eq!(envelope["stats_delta"]["summary_tokens_cached_input"], 9); + assert!( + fs::read_to_string(prompt_log) + .expect("read Codex prompt log") + .contains("Prompt contract: clarion-agent-provider-v1") + ); +} + +#[test] +fn serve_routes_summary_miss_through_claude_cli_provider() { + let dir = tempfile::tempdir().expect("temp project"); + clarion_bin() + .args(["install", "--path"]) + .arg(dir.path()) + .env("PATH", "") + .assert() + .success(); + seed_summary_entity(dir.path()); + + let fake_claude = dir.path().join("fake-claude"); + let prompt_log = dir.path().join("claude-prompt.log"); + fs::write( + &fake_claude, + format!( + r#"#!/usr/bin/env bash +set -euo pipefail +schema="" +print_prompt="" +while [[ $# -gt 0 ]]; do + case "$1" in + -p|--print) + print_prompt="$2" + shift 2 + ;; + --json-schema) + schema="$2" + shift 2 + ;; + --output-format|--permission-mode|--max-turns|--model|--tools) + shift 2 + ;; + --no-session-persistence|--exclude-dynamic-system-prompt-sections) + shift + ;; + *) + shift + ;; + esac +done +stdin_prompt="$(cat)" +printf '%s\n%s' "$print_prompt" "$stdin_prompt" > "{prompt_log}" +case "$print_prompt" in + *"Clarion's local Claude Code LLM provider"*) ;; + *) echo "missing Claude print prompt" >&2; exit 41 ;; +esac +case "$stdin_prompt" in + *"Prompt contract: clarion-agent-provider-v1"*"Do not inspect additional files"*"Source excerpt:"*) ;; + *) echo "missing Clarion agent prompt contract" >&2; exit 42 ;; +esac +case "$schema" in + *'"purpose"'*'"behavior"'*) ;; + *) echo "schema missing summary fields" >&2; exit 43 ;; +esac +printf '%s\n' '{{"type":"result","subtype":"success","structured_output":{{"purpose":"via claude serve","behavior":"served through fake Claude CLI","relationships":"","risks":""}},"usage":{{"input_tokens":33,"cached_input_tokens":12,"output_tokens":8,"total_tokens":41}},"total_cost_usd":0.0}}' +"#, + prompt_log = prompt_log.display() + ), + ) + .expect("write fake claude"); + make_executable(&fake_claude); + write_provider_config( + dir.path(), + "claude_cli", + r#" + claude_cli: + executable: "__EXECUTABLE__" + model: null + permission_mode: plan + tools: [] + timeout_seconds: 5 + max_turns: 2 + no_session_persistence: true + exclude_dynamic_system_prompt_sections: true +"#, + &fake_claude, + ); + + let envelope = call_summary_through_serve(dir.path()); + + assert_eq!(envelope["ok"], true); + assert_eq!(envelope["result"]["cache"]["hit"], false); + assert_eq!(envelope["result"]["summary"]["purpose"], "via claude serve"); + assert_eq!(envelope["result"]["usage"]["tokens_cached_input"], 12); + assert_eq!(envelope["stats_delta"]["summary_tokens_cached_input"], 12); + assert!( + fs::read_to_string(prompt_log) + .expect("read Claude prompt log") + .contains("Clarion's local Claude Code LLM provider") + ); +} + +fn make_executable(path: &Path) { + let mut permissions = fs::metadata(path).expect("metadata").permissions(); + permissions.set_mode(0o755); + fs::set_permissions(path, permissions).expect("chmod executable"); +} + +fn seed_summary_entity(project_root: &Path) { + let source = "def entry():\n return 1\n"; + let source_path = project_root.join("demo.py"); + fs::write(&source_path, source).expect("write source"); + let content_hash = line_range_content_hash(source, 1, 2); + let db_path = project_root.join(".clarion/clarion.db"); + let conn = Connection::open(&db_path).expect("open sqlite"); + conn.execute( + "INSERT INTO entities ( + id, plugin_id, kind, name, short_name, source_file_path, + source_line_start, source_line_end, properties, content_hash, created_at, updated_at + ) VALUES ( + 'python:function:demo.entry', 'python', 'function', + 'python:function:demo.entry', 'entry', ?1, + 1, 2, '{}', ?2, + '2026-05-17T00:00:00.000Z', '2026-05-17T00:00:00.000Z' + )", + params![source_path.display().to_string(), content_hash], + ) + .expect("insert summary entity"); +} + +fn line_range_content_hash(source: &str, start_line: usize, end_line: usize) -> String { + let lines = source.lines().collect::>(); + let start = start_line.saturating_sub(1); + let end = end_line.min(lines.len()); + blake3::hash(lines[start..end].join("\n").as_bytes()) + .to_hex() + .to_string() +} + +fn write_provider_config( + project_root: &Path, + provider: &str, + provider_block: &str, + executable: &Path, +) { + let executable_yaml = + serde_json::to_string(&executable.display().to_string()).expect("quote executable path"); + let provider_block = provider_block + .trim_start_matches('\n') + .replace("\"__EXECUTABLE__\"", &executable_yaml); + fs::write( + project_root.join("clarion.yaml"), + format!( + concat!( + "version: 1\n", + "llm:\n", + " enabled: true\n", + " provider: {provider}\n", + " allow_live_provider: true\n", + "{provider_block}", + " model_id: {provider}-test\n", + " session_token_ceiling: 1000000\n", + " max_inferred_edges_per_caller: 8\n", + " cache_max_age_days: 180\n", + ), + provider = provider, + provider_block = provider_block, + ), + ) + .expect("write provider config"); +} + +fn call_summary_through_serve(project_root: &Path) -> Value { + let mut child = StdCommand::new(assert_cmd::cargo::cargo_bin("clarion")) + .args(["serve", "--path"]) + .arg(project_root) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("spawn clarion serve"); + { + let mut stdin = child.stdin.take().expect("child stdin"); + write_frame( + &mut stdin, + &Frame { + body: serde_json::to_vec(&serde_json::json!({ + "jsonrpc": "2.0", + "id": "summary", + "method": "tools/call", + "params": { + "name": "summary", + "arguments": {"id": "python:function:demo.entry"} + } + })) + .expect("serialize request"), + }, + ) + .expect("write summary frame"); + stdin.flush().expect("flush summary frame"); + } + + let output = child.wait_with_output().expect("wait for clarion serve"); + let config_debug = fs::read_to_string(project_root.join("clarion.yaml")) + .unwrap_or_else(|err| format!("failed to read clarion.yaml: {err}")); + assert!( + output.status.success(), + "serve failed: {}\nclarion.yaml:\n{}", + String::from_utf8_lossy(&output.stderr), + config_debug + ); + let mut reader = std::io::BufReader::new(std::io::Cursor::new(output.stdout)); + let frame = + read_frame(&mut reader, ContentLengthCeiling::new(usize::MAX)).expect("read response"); + let response: serde_json::Value = + serde_json::from_slice(&frame.body).expect("response body is json"); + let tool_text = response["result"]["content"][0]["text"] + .as_str() + .expect("tool text"); + serde_json::from_str(tool_text).expect("tool envelope") +} diff --git a/crates/clarion-core/src/lib.rs b/crates/clarion-core/src/lib.rs index 1cd67d1f..b2bba4d5 100644 --- a/crates/clarion-core/src/lib.rs +++ b/crates/clarion-core/src/lib.rs @@ -12,11 +12,12 @@ pub mod plugin; pub use entity_id::{EntityId, EntityIdError, entity_id}; pub use llm_provider::{ - CachingModel, INFERRED_CALLS_PROMPT_VERSION, InferredCallsPromptInput, + 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, build_inferred_calls_prompt, - build_leaf_summary_prompt, + PromptTemplate, Recording, RecordingProvider, 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 diff --git a/crates/clarion-core/src/llm_provider.rs b/crates/clarion-core/src/llm_provider.rs index 2ddfdeb5..e9627fa7 100644 --- a/crates/clarion-core/src/llm_provider.rs +++ b/crates/clarion-core/src/llm_provider.rs @@ -1,7 +1,12 @@ //! LLM provider surface for WP6 and MCP on-demand tools. +use std::fs; +use std::io::{Read, Write}; +use std::path::{Path, PathBuf}; +use std::process::{Child, Command, ExitStatus, Stdio}; use std::sync::Mutex; -use std::time::Duration; +use std::thread; +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -9,6 +14,8 @@ use thiserror::Error; pub const LEAF_SUMMARY_PROMPT_TEMPLATE_ID: &str = "leaf-v1"; pub const INFERRED_CALLS_PROMPT_VERSION: &str = "inferred-calls-v1"; +const AGENT_PROVIDER_PROMPT_VERSION: &str = "clarion-agent-provider-v1"; +const CLAUDE_CLI_PRINT_PROMPT: &str = "You are Clarion's local Claude Code LLM provider. Read the Clarion provider prompt from stdin, complete that exact task, and return only the validated JSON object."; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum LlmPurpose { @@ -30,6 +37,8 @@ pub struct LlmResponse { pub model_id: String, pub output_json: String, pub input_tokens: u32, + #[serde(default)] + pub cached_input_tokens: u32, pub output_tokens: u32, pub total_tokens: u32, pub cost_usd: f64, @@ -45,7 +54,7 @@ pub enum LlmProviderError { #[error("recording fixture has no response for prompt {prompt_id:?} on model {model_id:?}")] MissingRecording { prompt_id: String, model_id: String }, - #[error("live OpenRouter provider requires explicit opt-in")] + #[error("live LLM provider requires explicit opt-in")] LiveProviderNotAllowed, #[error("live OpenRouter provider requires an API key")] @@ -64,7 +73,13 @@ pub enum LlmProviderError { retry_after_seconds: Option, }, - #[error("invalid live OpenRouter response: {message}")] + #[error("LLM CLI invocation failed: {message}")] + Cli { message: String, retryable: bool }, + + #[error("LLM CLI invocation timed out after {timeout_seconds} seconds")] + Timeout { timeout_seconds: u64 }, + + #[error("invalid live LLM provider response: {message}")] InvalidResponse { message: String, retryable: bool }, } @@ -76,7 +91,9 @@ impl LlmProviderError { } Self::Http { retryable, .. } | Self::Provider { retryable, .. } + | Self::Cli { retryable, .. } | Self::InvalidResponse { retryable, .. } => *retryable, + Self::Timeout { .. } => true, } } } @@ -89,6 +106,32 @@ pub trait LlmProvider: Send + Sync { fn caching_model(&self) -> CachingModel; } +pub fn build_coding_agent_provider_prompt(request: &LlmRequest) -> String { + format!( + "Prompt contract: {prompt_version}\n\ + You are Clarion's coding-agent LLM provider for repository graph enrichment.\n\ + Clarion has already selected the source excerpt, entity metadata, unresolved call sites, and candidate graph context needed for this task.\n\ + Follow these rules exactly:\n\ + 1. Use only the evidence inside . Do not inspect additional files, browse, run commands, edit files, or ask follow-up questions.\n\ + 2. Return exactly one JSON object matching the structured-output schema supplied by the caller. Do not wrap it in Markdown or prose.\n\ + 3. Reason privately if needed, but do not expose hidden reasoning. Put only concise evidence summaries in output fields that ask for rationale or relationships.\n\ + 4. When evidence is absent, prefer empty strings for optional prose fields and empty arrays for collection fields instead of guessing.\n\ + 5. Keep stable field names and JSON types; downstream Clarion storage parses the response mechanically.\n\ + Task type: {task_type}\n\ + Prompt template: {prompt_id}\n\ + Task guidance:\n\ + {task_guidance}\n\ + \n\ + {prompt}\n\ + \n", + prompt_version = AGENT_PROVIDER_PROMPT_VERSION, + task_type = agent_task_type(&request.purpose), + prompt_id = request.prompt_id, + task_guidance = agent_task_guidance(&request.purpose), + prompt = request.prompt + ) +} + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Recording { pub request: LlmRequest, @@ -272,6 +315,10 @@ impl LlmProvider for OpenRouterProvider { model_id: completion.model, output_json, input_tokens: usage.prompt, + cached_input_tokens: usage + .prompt_tokens_details + .as_ref() + .map_or(0, |details| details.cached_tokens), output_tokens: usage.completion, total_tokens: usage.total, cost_usd: usage.cost.unwrap_or(0.0), @@ -294,6 +341,413 @@ impl LlmProvider for OpenRouterProvider { } } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CodexCliProviderConfig { + pub executable: String, + pub project_root: PathBuf, + pub model_id: String, + pub model: Option, + pub profile: Option, + pub sandbox: String, + pub timeout_seconds: u64, +} + +#[derive(Debug, Clone)] +pub struct CodexCliProvider { + executable: String, + project_root: PathBuf, + model_id: String, + model: Option, + profile: Option, + sandbox: String, + timeout: Duration, + timeout_seconds: u64, +} + +impl CodexCliProvider { + pub fn from_config(config: CodexCliProviderConfig) -> Result { + if config.executable.trim().is_empty() { + return Err(LlmProviderError::Cli { + message: "Codex CLI executable must not be blank".to_owned(), + retryable: false, + }); + } + if config.model_id.trim().is_empty() { + return Err(LlmProviderError::Cli { + message: "Codex CLI model_id must not be blank".to_owned(), + retryable: false, + }); + } + if config.sandbox.trim().is_empty() { + return Err(LlmProviderError::Cli { + message: "Codex CLI sandbox must not be blank".to_owned(), + retryable: false, + }); + } + if config.timeout_seconds == 0 { + return Err(LlmProviderError::Cli { + message: "Codex CLI timeout_seconds must be greater than zero".to_owned(), + retryable: false, + }); + } + + Ok(Self { + executable: config.executable, + project_root: config.project_root, + model_id: config.model_id, + model: config.model.filter(|model| !model.trim().is_empty()), + profile: config.profile.filter(|profile| !profile.trim().is_empty()), + sandbox: config.sandbox, + timeout: Duration::from_secs(config.timeout_seconds), + timeout_seconds: config.timeout_seconds, + }) + } + + fn invoke_with_temp_files( + &self, + request: LlmRequest, + output_path: &Path, + schema_path: &Path, + ) -> Result { + let schema = codex_output_schema_for_purpose(&request.purpose); + let schema_json = serde_json::to_vec_pretty(&schema).map_err(|err| { + LlmProviderError::InvalidResponse { + message: format!("serialize Codex output schema: {err}"), + retryable: false, + } + })?; + fs::write(schema_path, schema_json).map_err(|err| LlmProviderError::Cli { + message: format!("write Codex output schema {}: {err}", schema_path.display()), + retryable: false, + })?; + let provider_prompt = build_coding_agent_provider_prompt(&request); + + let mut command = Command::new(&self.executable); + command + .arg("exec") + .arg("--sandbox") + .arg(&self.sandbox) + .arg("-c") + .arg("approval_policy=\"never\"") + .arg("--json") + .arg("--cd") + .arg(&self.project_root) + .arg("--output-last-message") + .arg(output_path) + .arg("--output-schema") + .arg(schema_path); + if let Some(profile) = &self.profile { + command.arg("--profile").arg(profile); + } + if let Some(model) = &self.model { + command.arg("--model").arg(model); + } + command + .arg("-") + .current_dir(&self.project_root) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + + let mut child = command.spawn().map_err(|err| LlmProviderError::Cli { + message: format!("spawn Codex CLI {}: {err}", self.executable), + retryable: false, + })?; + + let stdout_reader = take_reader(&mut child.stdout, "stdout")?; + let stderr_reader = take_reader(&mut child.stderr, "stderr")?; + if let Err(err) = write_child_stdin(&mut child, &provider_prompt) { + let _ = child.kill(); + return Err(err); + } + + let status = wait_for_child(&mut child, self.timeout, self.timeout_seconds)?; + let stdout = join_reader(stdout_reader, "stdout")?; + let stderr = join_reader(stderr_reader, "stderr")?; + + if !status.success() { + return Err(LlmProviderError::Cli { + message: format!( + "codex exec exited with {status}: {}", + truncate_for_error(&String::from_utf8_lossy(&stderr)) + ), + retryable: codex_status_retryable(status), + }); + } + + let output_json = + fs::read_to_string(output_path).map_err(|err| LlmProviderError::InvalidResponse { + message: format!( + "read Codex output-last-message {}: {err}", + output_path.display() + ), + retryable: true, + })?; + let output_json = output_json.trim().to_owned(); + if output_json.is_empty() { + return Err(LlmProviderError::InvalidResponse { + message: "Codex output-last-message was empty".to_owned(), + retryable: true, + }); + } + serde_json::from_str::(&output_json).map_err(|err| { + LlmProviderError::InvalidResponse { + message: format!("Codex output was not valid JSON: {err}"), + retryable: true, + } + })?; + + let usage = parse_codex_jsonl_usage(&stdout); + let input_tokens = usage + .input_tokens + .unwrap_or_else(|| estimate_text_tokens(&request.prompt)); + let output_tokens = usage + .output_tokens + .unwrap_or_else(|| estimate_text_tokens(&output_json)); + let total_tokens = usage + .total_tokens + .unwrap_or_else(|| input_tokens.saturating_add(output_tokens)); + let cached_input_tokens = usage.cached_input_tokens.unwrap_or(0); + + Ok(LlmResponse { + model_id: request.model_id, + output_json, + input_tokens, + cached_input_tokens, + output_tokens, + total_tokens, + cost_usd: 0.0, + }) + } +} + +impl LlmProvider for CodexCliProvider { + fn name(&self) -> &'static str { + "codex_cli" + } + + fn invoke(&self, request: LlmRequest) -> Result { + let output_path = codex_temp_path("clarion-codex-output", "json"); + let schema_path = codex_temp_path("clarion-codex-schema", "json"); + let result = self.invoke_with_temp_files(request, &output_path, &schema_path); + let _ = fs::remove_file(output_path); + let _ = fs::remove_file(schema_path); + result + } + + fn estimate_tokens(&self, request: &LlmRequest) -> u64 { + u64::from(estimate_text_tokens(&build_coding_agent_provider_prompt( + request, + ))) + u64::from(request.max_output_tokens) + } + + fn tier_to_model(&self, tier: &str) -> Option<&str> { + match tier { + "summary" | "inferred_edges" => Some(self.model_id.as_str()), + _ => None, + } + } + + fn caching_model(&self) -> CachingModel { + CachingModel::OpenAiChatCompletions + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ClaudeCliProviderConfig { + pub executable: String, + pub project_root: PathBuf, + pub model_id: String, + pub model: Option, + pub permission_mode: String, + pub tools: Vec, + pub timeout_seconds: u64, + pub max_turns: u32, + pub no_session_persistence: bool, + pub exclude_dynamic_system_prompt_sections: bool, +} + +#[derive(Debug, Clone)] +pub struct ClaudeCliProvider { + executable: String, + project_root: PathBuf, + model_id: String, + model: Option, + permission_mode: String, + tools: Vec, + timeout: Duration, + timeout_seconds: u64, + max_turns: u32, + no_session_persistence: bool, + exclude_dynamic_system_prompt_sections: bool, +} + +impl ClaudeCliProvider { + pub fn from_config(config: ClaudeCliProviderConfig) -> Result { + if config.executable.trim().is_empty() { + return Err(LlmProviderError::Cli { + message: "Claude CLI executable must not be blank".to_owned(), + retryable: false, + }); + } + if config.model_id.trim().is_empty() { + return Err(LlmProviderError::Cli { + message: "Claude CLI model_id must not be blank".to_owned(), + retryable: false, + }); + } + if config.permission_mode.trim().is_empty() { + return Err(LlmProviderError::Cli { + message: "Claude CLI permission_mode must not be blank".to_owned(), + retryable: false, + }); + } + if config.timeout_seconds == 0 { + return Err(LlmProviderError::Cli { + message: "Claude CLI timeout_seconds must be greater than zero".to_owned(), + retryable: false, + }); + } + if config.max_turns == 0 { + return Err(LlmProviderError::Cli { + message: "Claude CLI max_turns must be greater than zero".to_owned(), + retryable: false, + }); + } + + Ok(Self { + executable: config.executable, + project_root: config.project_root, + model_id: config.model_id, + model: config.model.filter(|model| !model.trim().is_empty()), + permission_mode: config.permission_mode, + tools: config + .tools + .into_iter() + .filter(|tool| !tool.trim().is_empty()) + .collect(), + timeout: Duration::from_secs(config.timeout_seconds), + timeout_seconds: config.timeout_seconds, + max_turns: config.max_turns, + no_session_persistence: config.no_session_persistence, + exclude_dynamic_system_prompt_sections: config.exclude_dynamic_system_prompt_sections, + }) + } +} + +impl LlmProvider for ClaudeCliProvider { + fn name(&self) -> &'static str { + "claude_cli" + } + + fn invoke(&self, request: LlmRequest) -> Result { + let schema = codex_output_schema_for_purpose(&request.purpose); + let schema_json = + serde_json::to_string(&schema).map_err(|err| LlmProviderError::InvalidResponse { + message: format!("serialize Claude output schema: {err}"), + retryable: false, + })?; + let provider_prompt = build_coding_agent_provider_prompt(&request); + let mut command = Command::new(&self.executable); + command + .arg("-p") + .arg(CLAUDE_CLI_PRINT_PROMPT) + .arg("--output-format") + .arg("json") + .arg("--json-schema") + .arg(schema_json) + .arg("--permission-mode") + .arg(&self.permission_mode) + .arg("--max-turns") + .arg(self.max_turns.to_string()) + .arg("--mcp-config") + .arg(r#"{"mcpServers":{}}"#) + .arg("--strict-mcp-config") + .arg("--disable-slash-commands"); + if self.no_session_persistence { + command.arg("--no-session-persistence"); + } + if self.exclude_dynamic_system_prompt_sections { + command.arg("--exclude-dynamic-system-prompt-sections"); + } + if let Some(model) = &self.model { + command.arg("--model").arg(model); + } + command.arg("--tools").arg(self.tools.join(",")); + command + .current_dir(&self.project_root) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + + let mut child = command.spawn().map_err(|err| LlmProviderError::Cli { + message: format!("spawn Claude CLI {}: {err}", self.executable), + retryable: false, + })?; + let stdout_reader = take_reader(&mut child.stdout, "stdout")?; + let stderr_reader = take_reader(&mut child.stderr, "stderr")?; + if let Err(err) = write_child_stdin(&mut child, &provider_prompt) { + let _ = child.kill(); + return Err(err); + } + + let status = wait_for_child(&mut child, self.timeout, self.timeout_seconds)?; + let stdout = join_reader(stdout_reader, "stdout")?; + let stderr = join_reader(stderr_reader, "stderr")?; + if !status.success() { + return Err(LlmProviderError::Cli { + message: format!( + "claude -p exited with {status}: {}", + truncate_for_error(&String::from_utf8_lossy(&stderr)) + ), + retryable: cli_status_retryable(status), + }); + } + + let parsed = parse_claude_cli_json_output(&stdout)?; + let input_tokens = parsed + .usage + .input_tokens + .unwrap_or_else(|| estimate_text_tokens(&request.prompt)); + let output_tokens = parsed + .usage + .output_tokens + .unwrap_or_else(|| estimate_text_tokens(&parsed.output_json)); + let total_tokens = parsed + .usage + .total_tokens + .unwrap_or_else(|| input_tokens.saturating_add(output_tokens)); + let cached_input_tokens = parsed.usage.cached_input_tokens.unwrap_or(0); + + Ok(LlmResponse { + model_id: request.model_id, + output_json: parsed.output_json, + input_tokens, + cached_input_tokens, + output_tokens, + total_tokens, + cost_usd: parsed.cost_usd.unwrap_or(0.0), + }) + } + + fn estimate_tokens(&self, request: &LlmRequest) -> u64 { + u64::from(estimate_text_tokens(&build_coding_agent_provider_prompt( + request, + ))) + u64::from(request.max_output_tokens) + } + + fn tier_to_model(&self, tier: &str) -> Option<&str> { + match tier { + "summary" | "inferred_edges" => Some(self.model_id.as_str()), + _ => None, + } + } + + fn caching_model(&self) -> CachingModel { + CachingModel::OpenAiChatCompletions + } +} + fn response_format_for_purpose(purpose: &LlmPurpose) -> Value { match purpose { LlmPurpose::Summary => serde_json::json!({ @@ -369,6 +823,346 @@ fn response_format_for_purpose(purpose: &LlmPurpose) -> Value { } } +fn codex_output_schema_for_purpose(purpose: &LlmPurpose) -> Value { + response_format_for_purpose(purpose)["json_schema"]["schema"].clone() +} + +fn agent_task_type(purpose: &LlmPurpose) -> &'static str { + match purpose { + LlmPurpose::Summary => "leaf_summary", + LlmPurpose::InferredEdges => "inferred_edges", + } +} + +fn agent_task_guidance(purpose: &LlmPurpose) -> &'static str { + match purpose { + LlmPurpose::Summary => { + "- Produce a leaf-scope summary only for the requested entity.\n\ + - `purpose`, `behavior`, `relationships`, and `risks` must be strings.\n\ + - Do not summarize sibling entities except where direct caller/callee/ownership context is visible in the supplied excerpt.\n\ + - Use an empty `risks` string when no concrete implementation risk is visible." + } + LlmPurpose::InferredEdges => { + "- Resolve only unresolved call sites listed in the request.\n\ + - Choose targets only from the supplied candidate entities JSON.\n\ + - Return no more edges than the request's max_edges instruction allows.\n\ + - Use confidence from 0.0 to 1.0 and include brief evidence in `rationale`.\n\ + - Return `{\"edges\":[]}` when the supplied evidence is insufficient." + } + } +} + +type PipeReader = thread::JoinHandle>>; + +fn take_reader( + pipe: &mut Option, + pipe_name: &'static str, +) -> Result +where + R: Read + Send + 'static, +{ + let mut reader = pipe.take().ok_or_else(|| LlmProviderError::Cli { + message: format!("child {pipe_name} was not captured"), + retryable: false, + })?; + Ok(thread::spawn(move || { + let mut bytes = Vec::new(); + reader.read_to_end(&mut bytes)?; + Ok(bytes) + })) +} + +fn write_child_stdin(child: &mut Child, prompt: &str) -> Result<(), LlmProviderError> { + let mut stdin = child.stdin.take().ok_or_else(|| LlmProviderError::Cli { + message: "child stdin was not captured".to_owned(), + retryable: false, + })?; + stdin + .write_all(prompt.as_bytes()) + .map_err(|err| LlmProviderError::Cli { + message: format!("write provider prompt to stdin: {err}"), + retryable: true, + })?; + Ok(()) +} + +fn wait_for_child( + child: &mut Child, + timeout: Duration, + timeout_seconds: u64, +) -> Result { + let deadline = Instant::now() + timeout; + loop { + match child.try_wait() { + Ok(Some(status)) => return Ok(status), + Ok(None) if Instant::now() >= deadline => { + let _ = child.kill(); + let _ = child.wait(); + return Err(LlmProviderError::Timeout { timeout_seconds }); + } + Ok(None) => thread::sleep(Duration::from_millis(25)), + Err(err) => { + return Err(LlmProviderError::Cli { + message: format!("poll provider process status: {err}"), + retryable: true, + }); + } + } + } +} + +fn join_reader(handle: PipeReader, pipe_name: &'static str) -> Result, LlmProviderError> { + match handle.join() { + Ok(Ok(bytes)) => Ok(bytes), + Ok(Err(err)) => Err(LlmProviderError::Cli { + message: format!("read provider {pipe_name}: {err}"), + retryable: true, + }), + Err(_) => Err(LlmProviderError::Cli { + message: format!("read provider {pipe_name}: reader thread panicked"), + retryable: true, + }), + } +} + +fn cli_status_retryable(status: ExitStatus) -> bool { + status.code().is_none() +} + +fn truncate_for_error(message: &str) -> String { + const MAX_ERROR_CHARS: usize = 4096; + if message.chars().count() <= MAX_ERROR_CHARS { + return message.to_owned(); + } + let mut truncated: String = message.chars().take(MAX_ERROR_CHARS).collect(); + truncated.push_str("... (truncated)"); + truncated +} + +fn codex_status_retryable(status: ExitStatus) -> bool { + cli_status_retryable(status) +} + +fn codex_temp_path(prefix: &str, extension: &str) -> PathBuf { + let nonce = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_nanos(); + std::env::temp_dir().join(format!( + "{prefix}-{}-{nonce}.{extension}", + std::process::id() + )) +} + +#[derive(Debug, Clone, Copy, Default, PartialEq)] +struct LlmUsageSummary { + input_tokens: Option, + cached_input_tokens: Option, + output_tokens: Option, + total_tokens: Option, + cost_usd: Option, +} + +impl LlmUsageSummary { + fn add(&mut self, other: Self) { + self.input_tokens = add_optional_u32(self.input_tokens, other.input_tokens); + self.cached_input_tokens = + add_optional_u32(self.cached_input_tokens, other.cached_input_tokens); + self.output_tokens = add_optional_u32(self.output_tokens, other.output_tokens); + self.total_tokens = add_optional_u32(self.total_tokens, other.total_tokens); + self.cost_usd = add_optional_f64(self.cost_usd, other.cost_usd); + } +} + +#[derive(Debug, Clone, PartialEq)] +struct ClaudeCliOutput { + output_json: String, + usage: LlmUsageSummary, + cost_usd: Option, +} + +fn add_optional_u32(left: Option, right: Option) -> Option { + match (left, right) { + (Some(left), Some(right)) => Some(left.saturating_add(right)), + (Some(value), None) | (None, Some(value)) => Some(value), + (None, None) => None, + } +} + +fn add_optional_f64(left: Option, right: Option) -> Option { + match (left, right) { + (Some(left), Some(right)) => Some(left + right), + (Some(value), None) | (None, Some(value)) => Some(value), + (None, None) => None, + } +} + +fn parse_codex_jsonl_usage(stdout: &[u8]) -> LlmUsageSummary { + let mut summary = LlmUsageSummary::default(); + let stdout_text = String::from_utf8_lossy(stdout); + for line in stdout_text + .lines() + .map(str::trim) + .filter(|line| !line.is_empty()) + { + let Ok(event) = serde_json::from_str::(line) else { + continue; + }; + summary.add(usage_from_event(&event)); + } + summary +} + +fn parse_claude_cli_json_output(stdout: &[u8]) -> Result { + let stdout_text = String::from_utf8_lossy(stdout); + let trimmed = stdout_text.trim(); + if trimmed.is_empty() { + return Err(LlmProviderError::InvalidResponse { + message: "Claude CLI returned empty stdout".to_owned(), + retryable: true, + }); + } + let value = serde_json::from_str::(trimmed).map_err(|err| { + LlmProviderError::InvalidResponse { + message: format!("Claude CLI stdout was not JSON: {err}"), + retryable: true, + } + })?; + let events = match &value { + Value::Array(events) => events.as_slice(), + _ => std::slice::from_ref(&value), + }; + let result_event = events + .iter() + .rev() + .find(|event| event.get("type").and_then(Value::as_str) == Some("result")) + .or_else(|| { + events + .iter() + .rev() + .find(|event| has_claude_structured_output(event)) + }) + .unwrap_or(&value); + let usage = if result_event.get("usage").is_some() { + usage_from_event(result_event) + } else { + let mut summary = LlmUsageSummary::default(); + for event in events { + summary.add(usage_from_event(event)); + } + summary + }; + let cost_usd = result_event + .get("total_cost_usd") + .or_else(|| result_event.get("cost_usd")) + .and_then(Value::as_f64) + .or(usage.cost_usd); + let output_json = claude_structured_output_json(result_event)?; + Ok(ClaudeCliOutput { + output_json, + usage, + cost_usd, + }) +} + +fn has_claude_structured_output(value: &Value) -> bool { + value.get("structured_output").is_some() + || value.get("structuredOutput").is_some() + || value.get("result").is_some() +} + +fn claude_structured_output_json(value: &Value) -> Result { + let output = value + .get("structured_output") + .or_else(|| value.get("structuredOutput")) + .or_else(|| value.get("result")) + .unwrap_or(value); + json_value_to_output_json(output, "Claude CLI structured output") +} + +fn json_value_to_output_json(value: &Value, label: &str) -> Result { + match value { + Value::String(text) => { + let trimmed = text.trim(); + if trimmed.is_empty() { + return Err(LlmProviderError::InvalidResponse { + message: format!("{label} was empty"), + retryable: true, + }); + } + serde_json::from_str::(trimmed).map_err(|err| { + LlmProviderError::InvalidResponse { + message: format!("{label} string was not JSON: {err}"), + retryable: true, + } + })?; + Ok(trimmed.to_owned()) + } + Value::Object(_) | Value::Array(_) => { + serde_json::to_string(value).map_err(|err| LlmProviderError::InvalidResponse { + message: format!("serialize {label}: {err}"), + retryable: false, + }) + } + _ => Err(LlmProviderError::InvalidResponse { + message: format!("{label} was not an object, array, or JSON string"), + retryable: true, + }), + } +} + +fn usage_from_event(event: &Value) -> LlmUsageSummary { + let raw_usage = event + .get("usage") + .or_else(|| event.get("msg").and_then(|msg| msg.get("usage"))) + .or_else(|| { + event + .get("message") + .and_then(|message| message.get("usage")) + }); + let Some(raw_usage) = raw_usage else { + return LlmUsageSummary::default(); + }; + usage_from_usage_value(raw_usage) +} + +fn usage_from_usage_value(raw_usage: &Value) -> LlmUsageSummary { + let input_tokens = u32_from_value(raw_usage.get("input_tokens")) + .or_else(|| u32_from_value(raw_usage.get("prompt_tokens"))); + let output_tokens = u32_from_value(raw_usage.get("output_tokens")) + .or_else(|| u32_from_value(raw_usage.get("completion_tokens"))); + let total_tokens = u32_from_value(raw_usage.get("total_tokens")).or_else(|| { + match (input_tokens, output_tokens) { + (Some(input), Some(output)) => Some(input.saturating_add(output)), + _ => None, + } + }); + let cached_from_details = raw_usage + .get("prompt_tokens_details") + .and_then(|details| u32_from_value(details.get("cached_tokens"))); + let cached_input_tokens = u32_from_value(raw_usage.get("cached_input_tokens")) + .or_else(|| u32_from_value(raw_usage.get("cache_read_input_tokens"))) + .or(cached_from_details); + LlmUsageSummary { + input_tokens, + cached_input_tokens, + output_tokens, + total_tokens, + cost_usd: raw_usage + .get("cost_usd") + .or_else(|| raw_usage.get("cost")) + .and_then(Value::as_f64), + } +} + +fn u32_from_value(value: Option<&Value>) -> Option { + let value = value?; + match value { + Value::Number(number) => number.as_u64().and_then(|value| u32::try_from(value).ok()), + _ => None, + } +} + #[derive(Debug, Deserialize)] struct OpenRouterChatResponse { model: String, @@ -422,9 +1216,15 @@ struct OpenRouterUsage { completion: u32, #[serde(rename = "total_tokens")] total: u32, + prompt_tokens_details: Option, cost: Option, } +#[derive(Debug, Deserialize)] +struct OpenRouterPromptTokensDetails { + cached_tokens: u32, +} + #[derive(Debug, Deserialize)] struct OpenRouterErrorEnvelope { error: OpenRouterErrorBody, @@ -577,6 +1377,7 @@ mod tests { model_id: "anthropic/claude-sonnet-4.6".to_owned(), output_json: r#"{"purpose":"demo"}"#.to_owned(), input_tokens: 120, + cached_input_tokens: 0, output_tokens: 24, total_tokens: 144, cost_usd: 0.0, @@ -623,6 +1424,27 @@ mod tests { assert!(inferred.body.contains("no more than 8 entries")); } + #[test] + fn coding_agent_provider_prompt_wraps_request_with_shared_contract() { + let request = LlmRequest { + purpose: LlmPurpose::InferredEdges, + model_id: "agent-default".to_owned(), + prompt_id: INFERRED_CALLS_PROMPT_VERSION.to_owned(), + prompt: "Resolve call-site a from the supplied candidates".to_owned(), + max_output_tokens: 2048, + }; + + let prompt = build_coding_agent_provider_prompt(&request); + + assert!(prompt.contains("Prompt contract: clarion-agent-provider-v1")); + assert!(prompt.contains("Task type: inferred_edges")); + assert!(prompt.contains("Do not inspect additional files")); + assert!(prompt.contains("Return exactly one JSON object")); + assert!(prompt.contains("Choose targets only from the supplied candidate entities JSON")); + assert!(prompt.contains("")); + assert!(prompt.contains("Resolve call-site a from the supplied candidates")); + } + #[test] fn openrouter_provider_requires_explicit_live_opt_in_and_api_key() { let denied = OpenRouterProvider::from_config(OpenRouterProviderConfig { @@ -895,6 +1717,350 @@ mod tests { )); } + #[test] + fn codex_cli_provider_invokes_exec_with_schema_stdin_and_usage() { + use std::fs; + use std::os::unix::fs::PermissionsExt; + + let temp = tempfile::tempdir().expect("tempdir"); + let project_root = temp.path().join("project"); + fs::create_dir(&project_root).expect("project root"); + let fake_codex = temp.path().join("codex"); + let log_path = temp.path().join("codex.log"); + let script = format!( + r#"#!/usr/bin/env bash +set -euo pipefail +log="{log}" +out="" +schema="" +cd_arg="" +sandbox="" +model="" +profile="" +json=0 +stdin_prompt="" +while [[ $# -gt 0 ]]; do + case "$1" in + exec) + echo "subcommand=exec" >> "$log" + shift + ;; + --sandbox) + sandbox="$2" + shift 2 + ;; + --cd) + cd_arg="$2" + shift 2 + ;; + --output-last-message) + out="$2" + shift 2 + ;; + --output-schema) + schema="$2" + shift 2 + ;; + --model) + model="$2" + shift 2 + ;; + --profile) + profile="$2" + shift 2 + ;; + --json) + json=1 + shift + ;; + -c) + echo "config=$2" >> "$log" + shift 2 + ;; + -) + stdin_prompt="$(cat)" + shift + ;; + *) + echo "arg=$1" >> "$log" + shift + ;; + esac +done + +test "$json" = "1" +test -n "$out" +test -s "$schema" +grep -q '"purpose"' "$schema" +grep -q '"behavior"' "$schema" +case "$stdin_prompt" in + *"Summarize this function"*) ;; + *) echo "missing prompt" >&2; exit 31 ;; +esac +case "$stdin_prompt" in + *"Prompt contract: clarion-agent-provider-v1"*"Do not inspect additional files"*) ;; + *) echo "missing Clarion agent prompt contract" >&2; exit 32 ;; +esac + +echo "sandbox=$sandbox" >> "$log" +echo "cd=$cd_arg" >> "$log" +echo "model=$model" >> "$log" +echo "profile=$profile" >> "$log" +printf '%s\n' '{{"usage":{{"input_tokens":11,"cached_input_tokens":4,"output_tokens":7,"total_tokens":18}}}}' +printf '%s' '{{"purpose":"via codex","behavior":"ran fake CLI","relationships":"","risks":""}}' > "$out" +"#, + log = log_path.display() + ); + fs::write(&fake_codex, script).expect("write fake codex"); + let mut permissions = fs::metadata(&fake_codex).expect("metadata").permissions(); + permissions.set_mode(0o755); + fs::set_permissions(&fake_codex, permissions).expect("chmod fake codex"); + + let provider = CodexCliProvider::from_config(CodexCliProviderConfig { + executable: fake_codex.display().to_string(), + project_root: project_root.clone(), + model_id: "codex-cli-default".to_owned(), + model: Some("gpt-5.5".to_owned()), + profile: Some("clarion".to_owned()), + sandbox: "read-only".to_owned(), + timeout_seconds: 5, + }) + .expect("construct Codex CLI provider"); + + let response = provider + .invoke(LlmRequest { + purpose: LlmPurpose::Summary, + model_id: "codex-cli-default".to_owned(), + prompt_id: LEAF_SUMMARY_PROMPT_TEMPLATE_ID.to_owned(), + prompt: "Summarize this function".to_owned(), + max_output_tokens: 512, + }) + .expect("invoke fake Codex CLI"); + + assert_eq!(provider.name(), "codex_cli"); + assert_eq!(provider.tier_to_model("summary"), Some("codex-cli-default")); + assert_eq!(response.model_id, "codex-cli-default"); + assert_eq!( + response.output_json, + r#"{"purpose":"via codex","behavior":"ran fake CLI","relationships":"","risks":""}"# + ); + assert_eq!(response.input_tokens, 11); + assert_eq!(response.cached_input_tokens, 4); + assert_eq!(response.output_tokens, 7); + assert_eq!(response.total_tokens, 18); + assert_eq!(response.cost_usd, 0.0); + + let log = fs::read_to_string(log_path).expect("read fake codex log"); + assert!(log.contains("subcommand=exec")); + assert!(log.contains("config=approval_policy=\"never\"")); + assert!(log.contains("sandbox=read-only")); + assert!(log.contains(&format!("cd={}", project_root.display()))); + assert!(log.contains("model=gpt-5.5")); + assert!(log.contains("profile=clarion")); + } + + #[test] + fn claude_cli_provider_invokes_print_mode_with_schema_and_usage() { + use std::fs; + use std::os::unix::fs::PermissionsExt; + + let temp = tempfile::tempdir().expect("tempdir"); + let project_root = temp.path().join("project"); + fs::create_dir(&project_root).expect("project root"); + let fake_claude = temp.path().join("claude"); + let log_path = temp.path().join("claude.log"); + let script = format!( + r#"#!/usr/bin/env bash +set -euo pipefail +log="{log}" +schema="" +format="" +model="" +permission_mode="" +tools="unset" +max_turns="" +no_session_persistence=0 +exclude_dynamic=0 +mcp_config="" +strict_mcp=0 +slash_disabled=0 +print_prompt="" +stdin_prompt="" +while [[ $# -gt 0 ]]; do + case "$1" in + -p|--print) + print_prompt="$2" + shift 2 + ;; + --output-format) + format="$2" + shift 2 + ;; + --json-schema) + schema="$2" + shift 2 + ;; + --model) + model="$2" + shift 2 + ;; + --permission-mode) + permission_mode="$2" + shift 2 + ;; + --tools) + tools="$2" + shift 2 + ;; + --max-turns) + max_turns="$2" + shift 2 + ;; + --mcp-config) + mcp_config="$2" + shift 2 + ;; + --strict-mcp-config) + strict_mcp=1 + shift + ;; + --disable-slash-commands) + slash_disabled=1 + shift + ;; + --no-session-persistence) + no_session_persistence=1 + shift + ;; + --exclude-dynamic-system-prompt-sections) + exclude_dynamic=1 + shift + ;; + *) + echo "arg=$1" >> "$log" + shift + ;; + esac +done +stdin_prompt="$(cat)" + +test "$format" = "json" +case "$schema" in + *'"purpose"'*'"behavior"'*) ;; + *) echo "schema missing summary fields" >&2; exit 41 ;; +esac +case "$stdin_prompt" in + *"Summarize this function"*) ;; + *) echo "missing prompt" >&2; exit 42 ;; +esac +case "$stdin_prompt" in + *"Prompt contract: clarion-agent-provider-v1"*"Do not inspect additional files"*) ;; + *) echo "missing Clarion agent prompt contract" >&2; exit 43 ;; +esac + +echo "print_prompt=$print_prompt" >> "$log" +echo "model=$model" >> "$log" +echo "permission_mode=$permission_mode" >> "$log" +echo "tools=$tools" >> "$log" +echo "max_turns=$max_turns" >> "$log" +echo "mcp_config=$mcp_config" >> "$log" +echo "strict_mcp=$strict_mcp" >> "$log" +echo "slash_disabled=$slash_disabled" >> "$log" +echo "no_session_persistence=$no_session_persistence" >> "$log" +echo "exclude_dynamic=$exclude_dynamic" >> "$log" +printf '%s\n' '{{"type":"result","subtype":"success","structured_output":{{"purpose":"via claude","behavior":"ran fake CLI","relationships":"","risks":""}},"usage":{{"input_tokens":13,"cached_input_tokens":5,"output_tokens":6,"total_tokens":19}},"total_cost_usd":0.25}}' +"#, + log = log_path.display() + ); + fs::write(&fake_claude, script).expect("write fake claude"); + let mut permissions = fs::metadata(&fake_claude).expect("metadata").permissions(); + permissions.set_mode(0o755); + fs::set_permissions(&fake_claude, permissions).expect("chmod fake claude"); + + let provider = ClaudeCliProvider::from_config(ClaudeCliProviderConfig { + executable: fake_claude.display().to_string(), + project_root, + model_id: "claude-code-default".to_owned(), + model: Some("claude-sonnet-4-6".to_owned()), + permission_mode: "plan".to_owned(), + tools: vec!["Read".to_owned(), "Grep".to_owned()], + timeout_seconds: 5, + max_turns: 2, + no_session_persistence: true, + exclude_dynamic_system_prompt_sections: true, + }) + .expect("construct Claude CLI provider"); + + let response = provider + .invoke(LlmRequest { + purpose: LlmPurpose::Summary, + model_id: "claude-code-default".to_owned(), + prompt_id: LEAF_SUMMARY_PROMPT_TEMPLATE_ID.to_owned(), + prompt: "Summarize this function".to_owned(), + max_output_tokens: 512, + }) + .expect("invoke fake Claude CLI"); + + assert_eq!(provider.name(), "claude_cli"); + assert_eq!( + provider.tier_to_model("summary"), + Some("claude-code-default") + ); + assert_eq!(response.model_id, "claude-code-default"); + assert_eq!(response.input_tokens, 13); + assert_eq!(response.cached_input_tokens, 5); + assert_eq!(response.output_tokens, 6); + assert_eq!(response.total_tokens, 19); + assert!((response.cost_usd - 0.25).abs() < f64::EPSILON); + assert_eq!( + serde_json::from_str::(&response.output_json).expect("response JSON"), + serde_json::json!({ + "purpose": "via claude", + "behavior": "ran fake CLI", + "relationships": "", + "risks": "" + }) + ); + + let log = fs::read_to_string(log_path).expect("read fake claude log"); + assert!(log.contains("print_prompt=You are Clarion's local Claude Code LLM provider")); + assert!(log.contains("model=claude-sonnet-4-6")); + assert!(log.contains("permission_mode=plan")); + assert!(log.contains("tools=Read,Grep")); + assert!(log.contains("max_turns=2")); + assert!(log.contains(r#"mcp_config={"mcpServers":{}}"#)); + assert!(log.contains("strict_mcp=1")); + assert!(log.contains("slash_disabled=1")); + assert!(log.contains("no_session_persistence=1")); + assert!(log.contains("exclude_dynamic=1")); + } + + #[test] + fn claude_cli_output_parser_accepts_event_array_and_cache_reads() { + let stdout = br#"[ + {"type":"system","subtype":"init"}, + {"type":"assistant","message":{"usage":{"input_tokens":4,"output_tokens":3}}}, + {"type":"result","subtype":"success","structured_output":{"purpose":"array","behavior":"ok","relationships":"","risks":""},"total_cost_usd":0.75,"usage":{"input_tokens":7,"cache_read_input_tokens":25,"output_tokens":11,"total_tokens":43}} + ]"#; + + let parsed = parse_claude_cli_json_output(stdout).expect("parse Claude event array"); + + assert_eq!( + serde_json::from_str::(&parsed.output_json).expect("output json"), + serde_json::json!({ + "purpose": "array", + "behavior": "ok", + "relationships": "", + "risks": "" + }) + ); + assert_eq!(parsed.usage.input_tokens, Some(7)); + assert_eq!(parsed.usage.cached_input_tokens, Some(25)); + assert_eq!(parsed.usage.output_tokens, Some(11)); + assert_eq!(parsed.usage.total_tokens, Some(43)); + assert_eq!(parsed.cost_usd, Some(0.75)); + } + #[test] fn provider_trait_exposes_wp6_methods() { fn assert_trait(_: &T) {} diff --git a/crates/clarion-mcp/src/config.rs b/crates/clarion-mcp/src/config.rs index 11ab7e29..24b050b8 100644 --- a/crates/clarion-mcp/src/config.rs +++ b/crates/clarion-mcp/src/config.rs @@ -58,6 +58,8 @@ pub struct LlmConfig { pub session_token_ceiling: u64, pub model_id: String, pub openrouter: OpenRouterConfig, + pub codex_cli: CodexCliConfig, + pub claude_cli: ClaudeCliConfig, pub recording_fixture_path: Option, pub max_inferred_edges_per_caller: u32, pub cache_max_age_days: u32, @@ -73,6 +75,8 @@ impl Default for LlmConfig { session_token_ceiling: 1_000_000, model_id: "anthropic/claude-sonnet-4.6".to_owned(), openrouter: OpenRouterConfig::default(), + codex_cli: CodexCliConfig::default(), + claude_cli: ClaudeCliConfig::default(), recording_fixture_path: None, max_inferred_edges_per_caller: 8, cache_max_age_days: 180, @@ -86,6 +90,10 @@ impl Default for LlmConfig { pub enum LlmProviderKind { #[serde(rename = "openrouter", alias = "open_router")] OpenRouter, + #[serde(rename = "codex_cli", alias = "codex")] + CodexCli, + #[serde(rename = "claude_cli", alias = "claude_code")] + ClaudeCli, Anthropic, Recording, } @@ -124,6 +132,56 @@ impl Default for OpenRouterAttributionConfig { } } +#[derive(Debug, Clone, PartialEq, Deserialize)] +#[serde(default)] +pub struct CodexCliConfig { + pub executable: String, + pub model: Option, + pub profile: Option, + pub sandbox: String, + pub timeout_seconds: u64, +} + +impl Default for CodexCliConfig { + fn default() -> Self { + Self { + executable: "codex".to_owned(), + model: None, + profile: None, + sandbox: "read-only".to_owned(), + timeout_seconds: 300, + } + } +} + +#[derive(Debug, Clone, PartialEq, Deserialize)] +#[serde(default)] +pub struct ClaudeCliConfig { + pub executable: String, + pub model: Option, + pub permission_mode: String, + pub tools: Vec, + pub timeout_seconds: u64, + pub max_turns: u32, + pub no_session_persistence: bool, + pub exclude_dynamic_system_prompt_sections: bool, +} + +impl Default for ClaudeCliConfig { + fn default() -> Self { + Self { + executable: "claude".to_owned(), + model: None, + permission_mode: "plan".to_owned(), + tools: Vec::new(), + timeout_seconds: 300, + max_turns: 2, + no_session_persistence: true, + exclude_dynamic_system_prompt_sections: true, + } + } +} + #[derive(Debug, Clone, PartialEq, Default, Deserialize)] #[serde(default)] pub struct IntegrationsConfig { @@ -157,6 +215,8 @@ pub enum ProviderSelection { Disabled, Recording, OpenRouter { api_key_env: String }, + CodexCli, + ClaudeCli, } pub fn select_provider_with_env( @@ -193,6 +253,20 @@ where api_key_env: env_var, }) } + LlmProviderKind::CodexCli => { + let live_env_opt_in = env_lookup("CLARION_LLM_LIVE").as_deref() == Some("1"); + if !config.llm.allow_live_provider && !live_env_opt_in { + return Ok(ProviderSelection::Disabled); + } + Ok(ProviderSelection::CodexCli) + } + LlmProviderKind::ClaudeCli => { + let live_env_opt_in = env_lookup("CLARION_LLM_LIVE").as_deref() == Some("1"); + if !config.llm.allow_live_provider && !live_env_opt_in { + return Ok(ProviderSelection::Disabled); + } + Ok(ProviderSelection::ClaudeCli) + } } } @@ -342,6 +416,117 @@ llm_policy: ); } + #[test] + fn codex_cli_provider_requires_live_opt_in_but_no_api_key() { + let cfg = McpConfig::from_yaml_str( + r" +llm_policy: + enabled: true + provider: codex_cli + allow_live_provider: true + model_id: codex-cli-default + codex_cli: + executable: /tmp/fake-codex + model: gpt-5.5 + profile: clarion + sandbox: read-only + timeout_seconds: 30 +", + ) + .expect("parse Codex CLI provider config"); + + assert_eq!(cfg.llm.provider, LlmProviderKind::CodexCli); + assert_eq!(cfg.llm.model_id, "codex-cli-default"); + assert_eq!(cfg.llm.codex_cli.executable, "/tmp/fake-codex"); + assert_eq!(cfg.llm.codex_cli.model.as_deref(), Some("gpt-5.5")); + assert_eq!(cfg.llm.codex_cli.profile.as_deref(), Some("clarion")); + assert_eq!(cfg.llm.codex_cli.sandbox, "read-only"); + assert_eq!(cfg.llm.codex_cli.timeout_seconds, 30); + + let selected = select_provider_with_env(&cfg, |_| None).expect("provider selection"); + assert_eq!(selected, ProviderSelection::CodexCli); + } + + #[test] + fn codex_cli_provider_stays_disabled_without_live_opt_in() { + let cfg = McpConfig { + llm: LlmConfig { + enabled: true, + provider: LlmProviderKind::CodexCli, + ..LlmConfig::default() + }, + integrations: IntegrationsConfig::default(), + }; + + let selected = select_provider_with_env(&cfg, |_| None).expect("provider selection"); + assert_eq!(selected, ProviderSelection::Disabled); + + let env_selected = select_provider_with_env(&cfg, |name| { + (name == "CLARION_LLM_LIVE").then(|| "1".to_owned()) + }) + .expect("provider selection via env opt-in"); + assert_eq!(env_selected, ProviderSelection::CodexCli); + } + + #[test] + fn claude_cli_provider_requires_live_opt_in_but_no_api_key() { + let cfg = McpConfig::from_yaml_str( + r#" +llm_policy: + enabled: true + provider: claude_cli + allow_live_provider: true + model_id: claude-code-default + claude_cli: + executable: /tmp/fake-claude + model: claude-sonnet-4-6 + permission_mode: plan + tools: ["Read", "Glob", "Grep"] + timeout_seconds: 45 + max_turns: 2 + no_session_persistence: true +"#, + ) + .expect("parse Claude CLI provider config"); + + assert_eq!(cfg.llm.provider, LlmProviderKind::ClaudeCli); + assert_eq!(cfg.llm.model_id, "claude-code-default"); + assert_eq!(cfg.llm.claude_cli.executable, "/tmp/fake-claude"); + assert_eq!( + cfg.llm.claude_cli.model.as_deref(), + Some("claude-sonnet-4-6") + ); + assert_eq!(cfg.llm.claude_cli.permission_mode, "plan"); + assert_eq!(cfg.llm.claude_cli.tools, vec!["Read", "Glob", "Grep"]); + assert_eq!(cfg.llm.claude_cli.timeout_seconds, 45); + assert_eq!(cfg.llm.claude_cli.max_turns, 2); + assert!(cfg.llm.claude_cli.no_session_persistence); + + let selected = select_provider_with_env(&cfg, |_| None).expect("provider selection"); + assert_eq!(selected, ProviderSelection::ClaudeCli); + } + + #[test] + fn claude_cli_provider_stays_disabled_without_live_opt_in() { + let cfg = McpConfig { + llm: LlmConfig { + enabled: true, + provider: LlmProviderKind::ClaudeCli, + ..LlmConfig::default() + }, + integrations: IntegrationsConfig::default(), + }; + + let selected = select_provider_with_env(&cfg, |_| None).expect("provider selection"); + assert_eq!(selected, ProviderSelection::Disabled); + + let env_selected = select_provider_with_env(&cfg, |name| { + (name == "CLARION_LLM_LIVE").then(|| "1".to_owned()) + }) + .expect("provider selection via env opt-in"); + assert_eq!(env_selected, ProviderSelection::ClaudeCli); + } + #[test] fn old_anthropic_provider_shape_reports_deprecated_provider() { let err = McpConfig::from_yaml_str( diff --git a/crates/clarion-mcp/src/lib.rs b/crates/clarion-mcp/src/lib.rs index 6c735749..4ef978a5 100644 --- a/crates/clarion-mcp/src/lib.rs +++ b/crates/clarion-mcp/src/lib.rs @@ -1208,6 +1208,7 @@ impl ServerState { cached, true, stale_semantic(cached, ready.caller_count, ready.fan_out), + None, json!({"summary_cache_hits_total": 1}), )) } @@ -1274,6 +1275,8 @@ impl ServerState { ); } + let cached_input_tokens = i64::from(response.cached_input_tokens); + let stats_delta = summary_usage_stats(&response, false); let entry = SummaryCacheEntry { key: ready.key, summary_json: response.output_json, @@ -1301,13 +1304,8 @@ impl ServerState { &entry, false, false, - json!({ - "summary_cache_misses_total": 1, - "summary_tokens_input": entry.tokens_input, - "summary_tokens_output": entry.tokens_output, - "summary_tokens_total": entry.tokens_input + entry.tokens_output, - "summary_cost_usd": entry.cost_usd - }), + Some(cached_input_tokens), + stats_delta, ) } @@ -1695,6 +1693,7 @@ struct InferredDispatchStats { candidate_callers_considered: u64, coalesced_waits_total: u64, tokens_input: i64, + tokens_cached_input: i64, tokens_output: i64, tokens_total: i64, cost_usd: f64, @@ -1716,6 +1715,7 @@ impl InferredDispatchStats { edges_materialized_total: write.inserted_edges, edges_skipped_static_duplicates_total: write.skipped_static_duplicates, tokens_input: i64::from(response.input_tokens), + tokens_cached_input: i64::from(response.cached_input_tokens), tokens_output: i64::from(response.output_tokens), tokens_total: i64::from(response.total_tokens), cost_usd: response.cost_usd, @@ -1732,6 +1732,7 @@ impl InferredDispatchStats { self.candidate_callers_considered += other.candidate_callers_considered; self.coalesced_waits_total += other.coalesced_waits_total; self.tokens_input += other.tokens_input; + self.tokens_cached_input += other.tokens_cached_input; self.tokens_output += other.tokens_output; self.tokens_total += other.tokens_total; self.cost_usd += other.cost_usd; @@ -1747,6 +1748,7 @@ impl InferredDispatchStats { "inferred_candidate_callers_considered": self.candidate_callers_considered, "inferred_dispatch_coalesced_total": self.coalesced_waits_total, "inferred_tokens_input": self.tokens_input, + "inferred_tokens_cached_input": self.tokens_cached_input, "inferred_tokens_output": self.tokens_output, "inferred_tokens_total": self.tokens_total, "inferred_cost_usd": self.cost_usd @@ -2167,6 +2169,7 @@ fn tool_error_envelope_with_diagnostics( fn llm_usage_json(response: &LlmResponse) -> Value { json!({ "tokens_input": response.input_tokens, + "tokens_cached_input": response.cached_input_tokens, "tokens_output": response.output_tokens, "tokens_total": response.total_tokens, "cost_usd": response.cost_usd @@ -2180,6 +2183,10 @@ fn summary_usage_stats(response: &LlmResponse, invalid_json: bool) -> Value { "summary_tokens_input".to_owned(), json!(response.input_tokens), ); + stats.insert( + "summary_tokens_cached_input".to_owned(), + json!(response.cached_input_tokens), + ); stats.insert( "summary_tokens_output".to_owned(), json!(response.output_tokens), @@ -2202,6 +2209,10 @@ fn inferred_usage_stats(response: &LlmResponse, invalid_json: bool) -> Value { "inferred_tokens_input".to_owned(), json!(response.input_tokens), ); + stats.insert( + "inferred_tokens_cached_input".to_owned(), + json!(response.cached_input_tokens), + ); stats.insert( "inferred_tokens_output".to_owned(), json!(response.output_tokens), @@ -2316,6 +2327,7 @@ fn summary_success_envelope( entry: &SummaryCacheEntry, cache_hit: bool, stale_semantic: bool, + cached_input_tokens: Option, stats_delta: Value, ) -> Value { let summary = serde_json::from_str::(&entry.summary_json).unwrap_or_else(|_| { @@ -2323,6 +2335,16 @@ fn summary_success_envelope( "raw": entry.summary_json }) }); + let mut usage = serde_json::Map::new(); + usage.insert("tokens_input".to_owned(), json!(entry.tokens_input)); + if let Some(tokens) = cached_input_tokens { + usage.insert("tokens_cached_input".to_owned(), json!(tokens)); + } + usage.insert("tokens_output".to_owned(), json!(entry.tokens_output)); + usage.insert( + "tokens_total".to_owned(), + json!(entry.tokens_input + entry.tokens_output), + ); success_envelope_with_stats( json!({ "available": true, @@ -2337,11 +2359,7 @@ fn summary_success_envelope( "created_at": entry.created_at, "last_accessed_at": entry.last_accessed_at }, - "usage": { - "tokens_input": entry.tokens_input, - "tokens_output": entry.tokens_output, - "tokens_total": entry.tokens_input + entry.tokens_output - } + "usage": Value::Object(usage) }), stats_delta, ) @@ -3072,6 +3090,7 @@ mod tests { model_id: "test-model".to_owned(), output_json: r#"{"edges":[]}"#.to_owned(), input_tokens: 1, + cached_input_tokens: 0, output_tokens: 1, total_tokens: 2, cost_usd: 0.0, diff --git a/crates/clarion-mcp/tests/storage_tools.rs b/crates/clarion-mcp/tests/storage_tools.rs index 069f842a..d3bd37b4 100644 --- a/crates/clarion-mcp/tests/storage_tools.rs +++ b/crates/clarion-mcp/tests/storage_tools.rs @@ -361,6 +361,7 @@ fn summary_recording(project_root: &std::path::Path, entity_id: &str) -> Arc. Do not inspect additional + files, browse, run commands, edit files, or ask follow-up questions. +2. Return exactly one JSON object matching the structured-output schema + supplied by the caller. Do not wrap it in Markdown or prose. +3. Reason privately if needed, but do not expose hidden reasoning. Put only + concise evidence summaries in output fields that ask for rationale or + relationships. +4. When evidence is absent, prefer empty strings for optional prose fields and + empty arrays for collection fields instead of guessing. +5. Keep stable field names and JSON types; downstream Clarion storage parses the + response mechanically. +``` + +SDK providers should pass the same prompt as the user/task content and attach +Clarion's purpose-specific JSON Schema through the SDK's structured-output +mechanism. Do not replace this with an agentic "go inspect the repo" prompt: +Clarion's MCP path is a bounded graph-enrichment call, not an autonomous code +review session. + +Prompt surfaces by provider: + +- Codex CLI: pass the shared prompt to `codex exec -` on stdin and attach + Clarion's JSON Schema with `--output-schema`. +- Claude CLI: use the short print-mode bootstrap prompt, pass the shared prompt + on stdin, and attach Clarion's JSON Schema with `--json-schema`. +- Codex SDK or app-server: put the same shared prompt in the task/user content, + keep the developer/system instruction to "Clarion graph enrichment, JSON only, + no repository inspection", and attach the same purpose-specific schema through + structured output. +- Claude Agent SDK: use the same task prompt and schema, disable tools by + default, and grant only read-only tools when an operator explicitly wants + workspace inspection. + +The stable prefix is intentional. It keeps provider prompt-caching viable while +the volatile source excerpt, unresolved call sites, and candidates stay inside +the `` block. + +Do not use either CLI route as the pre-ingest secret scanner. Clarion's local +secret scanner must continue to run before any prompt content is sent to a live +provider. diff --git a/docs/operator/openrouter.md b/docs/operator/openrouter.md index f518b5e3..17b99d5c 100644 --- a/docs/operator/openrouter.md +++ b/docs/operator/openrouter.md @@ -1,9 +1,12 @@ # OpenRouter LLM Provider -Clarion v0.1 uses OpenRouter as its canonical live LLM provider. The provider +Clarion v0.1 uses OpenRouter as its canonical HTTP LLM provider. The provider speaks OpenAI-compatible Chat Completions HTTP through the existing -`LlmProvider` trait, so tests can continue to use `RecordingProvider` and future -providers can be added without changing MCP tool call sites. +`LlmProvider` trait, so tests can continue to use `RecordingProvider` and +coding-agent CLI providers can be added without changing MCP tool call sites. + +For local-login alternatives that avoid API keys in Clarion config, see +[Coding-agent LLM providers](./coding-agent-llm-providers.md). ## Configure Clarion From 291142d86743ae86c3cbbd3e2e53a7a12f46e83e Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Tue, 19 May 2026 06:45:06 +1000 Subject: [PATCH 006/187] Implement registry read API and scanner fixes Add Clarion WP10 HTTP read endpoints, federation contract artifacts, Sprint 3 closeout docs, and WP5 secret scanner review fixes. --- Cargo.lock | 100 ++++++++ Cargo.toml | 3 +- crates/clarion-cli/Cargo.toml | 1 + crates/clarion-cli/src/http_read.rs | 176 ++++++++++++++ crates/clarion-cli/src/install.rs | 6 +- crates/clarion-cli/src/main.rs | 1 + crates/clarion-cli/src/secret_scan.rs | 59 ++++- .../clarion-cli/src/secret_scan/findings.rs | 27 ++- crates/clarion-cli/src/serve.rs | 7 + crates/clarion-cli/tests/secret_scan.rs | 2 + crates/clarion-cli/tests/serve.rs | 200 ++++++++++++++- crates/clarion-core/src/llm_provider.rs | 4 +- crates/clarion-mcp/src/config.rs | 27 +++ crates/clarion-scanner/src/baseline.rs | 7 +- crates/clarion-scanner/src/patterns.rs | 38 +-- crates/clarion-scanner/tests/scanner.rs | 20 ++ crates/clarion-storage/Cargo.toml | 1 + crates/clarion-storage/src/lib.rs | 4 +- crates/clarion-storage/src/query.rs | 112 +++++++++ crates/clarion-storage/src/reader.rs | 1 + docs/federation/contracts.md | 73 ++++++ ...gistry-backend-cross-project-sequencing.md | 124 ++++++++++ ...-backend-and-file-identity-displacement.md | 227 ++++++++++++++++++ docs/federation/filigree-side/README.md | 9 + .../fixtures/get-api-v1-capabilities.json | 12 + .../get-api-v1-files.demo-python.json | 13 + .../sprint-3/scope-amendment-2026-05.md | 219 +++++++++++++++++ docs/operator/secret-scanning.md | 11 + 28 files changed, 1436 insertions(+), 48 deletions(-) create mode 100644 crates/clarion-cli/src/http_read.rs create mode 100644 docs/federation/contracts.md create mode 100644 docs/federation/filigree-side/2026-05-19-registry-backend-cross-project-sequencing.md create mode 100644 docs/federation/filigree-side/ADR-014-registry-backend-and-file-identity-displacement.md create mode 100644 docs/federation/filigree-side/README.md create mode 100644 docs/federation/fixtures/get-api-v1-capabilities.json create mode 100644 docs/federation/fixtures/get-api-v1-files.demo-python.json create mode 100644 docs/implementation/sprint-3/scope-amendment-2026-05.md diff --git a/Cargo.lock b/Cargo.lock index 4eb52b52..4c58cceb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -106,6 +106,17 @@ dependencies = [ "wait-timeout", ] +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -118,6 +129,61 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "base64" version = "0.22.1" @@ -250,6 +316,7 @@ version = "0.1.0-dev" dependencies = [ "anyhow", "assert_cmd", + "axum", "blake3", "clap", "clarion-core", @@ -330,6 +397,7 @@ dependencies = [ name = "clarion-storage" version = "0.1.0-dev" dependencies = [ + "blake3", "clarion-core", "deadpool-sqlite", "rusqlite", @@ -764,6 +832,12 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + [[package]] name = "hyper" version = "1.9.0" @@ -777,6 +851,7 @@ dependencies = [ "http", "http-body", "httparse", + "httpdate", "itoa", "pin-project-lite", "smallvec", @@ -1051,12 +1126,24 @@ dependencies = [ "regex-automata", ] +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + [[package]] name = "memchr" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "mio" version = "1.2.0" @@ -1603,6 +1690,17 @@ dependencies = [ "unsafe-libyaml-norway", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + [[package]] name = "serde_spanned" version = "0.6.9" @@ -1946,6 +2044,7 @@ dependencies = [ "tokio", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -1984,6 +2083,7 @@ version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", diff --git a/Cargo.toml b/Cargo.toml index 94afb393..c8805ec8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ missing_errors_doc = "allow" [workspace.dependencies] anyhow = "1" +axum = "0.7" blake3 = "1.8.5" clap = { version = "4", features = ["derive"] } deadpool-sqlite = { version = "0.8", features = ["rt_tokio_1"] } @@ -47,7 +48,7 @@ sha1 = "0.10" sha2 = "0.10" thiserror = "1" time = { version = "0.3", features = ["formatting", "macros", "parsing"] } -tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "time"] } +tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "sync", "time"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } uuid = { version = "1", features = ["v4"] } diff --git a/crates/clarion-cli/Cargo.toml b/crates/clarion-cli/Cargo.toml index 4d6ebe70..21359804 100644 --- a/crates/clarion-cli/Cargo.toml +++ b/crates/clarion-cli/Cargo.toml @@ -15,6 +15,7 @@ path = "src/main.rs" [dependencies] anyhow.workspace = true +axum.workspace = true blake3.workspace = true clap.workspace = true clarion-core = { path = "../clarion-core", version = "0.1.0-dev" } diff --git a/crates/clarion-cli/src/http_read.rs b/crates/clarion-cli/src/http_read.rs new file mode 100644 index 00000000..27509e84 --- /dev/null +++ b/crates/clarion-cli/src/http_read.rs @@ -0,0 +1,176 @@ +use std::net::SocketAddr; +use std::path::PathBuf; +use std::thread; + +use anyhow::{Context, Result, anyhow}; +use axum::extract::{Query, State}; +use axum::http::StatusCode; +use axum::response::{IntoResponse, Response}; +use axum::routing::get; +use axum::{Json, Router}; +use clarion_mcp::config::HttpReadConfig; +use clarion_storage::{ReaderPool, resolve_file}; +use serde::{Deserialize, Serialize}; +use tokio::sync::oneshot; + +#[derive(Debug)] +pub struct HttpReadServer { + shutdown: Option>, + join: Option>>, +} + +impl HttpReadServer { + pub fn shutdown(mut self) -> Result<()> { + if let Some(shutdown) = self.shutdown.take() { + let _ = shutdown.send(()); + } + if let Some(join) = self.join.take() { + join.join() + .map_err(|_| anyhow!("HTTP read server thread panicked"))??; + } + Ok(()) + } +} + +#[derive(Clone)] +struct AppState { + project_root: PathBuf, + readers: ReaderPool, +} + +pub fn spawn( + project_root: PathBuf, + db_path: PathBuf, + config: &HttpReadConfig, +) -> Result> { + if !config.enabled { + return Ok(None); + } + let bind: SocketAddr = config + .bind + .parse() + .with_context(|| format!("parse serve.http.bind {:?}", config.bind))?; + let (shutdown_tx, shutdown_rx) = oneshot::channel(); + let (ready_tx, ready_rx) = std::sync::mpsc::channel(); + let join = thread::spawn(move || -> Result<()> { + let runtime = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .context("create HTTP read runtime")?; + runtime.block_on(async move { + let readers = ReaderPool::open(&db_path, 16) + .map_err(|err| anyhow!("open HTTP reader pool for {}: {err}", db_path.display()))?; + let listener = match tokio::net::TcpListener::bind(bind).await { + Ok(listener) => listener, + Err(err) => { + let _ = ready_tx.send(Err(anyhow!("bind HTTP read API on {bind}: {err}"))); + return Err(anyhow!("bind HTTP read API on {bind}: {err}")); + } + }; + let local_addr = listener + .local_addr() + .context("read HTTP read API local addr")?; + let _ = ready_tx.send(Ok(local_addr)); + let state = AppState { + project_root, + readers, + }; + axum::serve(listener, router(state)) + .with_graceful_shutdown(async { + let _ = shutdown_rx.await; + }) + .await + .context("serve HTTP read API") + }) + }); + let local_addr = ready_rx + .recv() + .context("wait for HTTP read API bind result")??; + tracing::info!(bind = %local_addr, "Clarion HTTP read API listening"); + Ok(Some(HttpReadServer { + shutdown: Some(shutdown_tx), + join: Some(join), + })) +} + +fn router(state: AppState) -> Router { + Router::new() + .route("/api/v1/files", get(get_file)) + .route("/api/v1/_capabilities", get(get_capabilities)) + .with_state(state) +} + +#[derive(Debug, Deserialize)] +struct FileQuery { + path: String, + #[serde(default)] + language: String, +} + +#[derive(Debug, Serialize)] +struct FileResponse { + entity_id: String, + content_hash: String, + canonical_path: String, + language: String, +} + +#[derive(Debug, Serialize)] +struct CapabilitiesResponse { + registry_backend: bool, + file_registry: bool, + version: &'static str, +} + +#[derive(Debug, Serialize)] +struct ErrorResponse { + error: String, +} + +async fn get_file(State(state): State, Query(query): Query) -> Response { + if query.path.trim().is_empty() { + return json_error( + StatusCode::BAD_REQUEST, + "path query parameter must not be blank", + ); + } + let project_root = state.project_root.clone(); + let file_path = query.path; + let language = query.language; + let result = state + .readers + .with_reader(move |conn| resolve_file(conn, &project_root, &file_path, &language)) + .await; + match result { + Ok(Some(file)) => ( + StatusCode::OK, + Json(FileResponse { + entity_id: file.entity_id, + content_hash: file.content_hash, + canonical_path: file.canonical_path, + language: file.language, + }), + ) + .into_response(), + Ok(None) => json_error(StatusCode::NOT_FOUND, "file is not known to Clarion"), + Err(err) => json_error(StatusCode::BAD_REQUEST, &err.to_string()), + } +} + +async fn get_capabilities() -> Json { + Json(CapabilitiesResponse { + registry_backend: true, + file_registry: true, + version: "0.1", + }) +} + +fn json_error(status: StatusCode, message: &str) -> Response { + ( + status, + Json(ErrorResponse { + error: message.to_owned(), + }), + ) + .into_response() +} diff --git a/crates/clarion-cli/src/install.rs b/crates/clarion-cli/src/install.rs index 33e96c36..b46dfc50 100644 --- a/crates/clarion-cli/src/install.rs +++ b/crates/clarion-cli/src/install.rs @@ -62,7 +62,11 @@ integrations:\n\ base_url: http://127.0.0.1:8766\n\ actor: clarion-mcp\n\ token_env: FILIGREE_API_TOKEN\n\ - timeout_seconds: 5\n"; + timeout_seconds: 5\n\ +serve:\n\ + http:\n\ + enabled: false\n\ + bind: 127.0.0.1:9111\n"; const GITIGNORE_CONTENTS: &str = "\ # Clarion .gitignore — ADR-005 tracked-vs-excluded list. diff --git a/crates/clarion-cli/src/main.rs b/crates/clarion-cli/src/main.rs index 4589d077..f9455aa9 100644 --- a/crates/clarion-cli/src/main.rs +++ b/crates/clarion-cli/src/main.rs @@ -2,6 +2,7 @@ mod analyze; mod cli; mod clustering; mod config; +mod http_read; mod install; mod run_lifecycle; mod secret_scan; diff --git a/crates/clarion-cli/src/secret_scan.rs b/crates/clarion-cli/src/secret_scan.rs index 6ca0de73..970d1c5a 100644 --- a/crates/clarion-cli/src/secret_scan.rs +++ b/crates/clarion-cli/src/secret_scan.rs @@ -89,6 +89,10 @@ pub(crate) fn pre_ingest( source_files: &[PathBuf], options: &SecretScanOptions, ) -> Result { + let project_root = project_root + .canonicalize() + .with_context(|| format!("canonicalize project root {}", project_root.display()))?; + let project_root = project_root.as_path(); let scanner = Scanner::new(); let (baseline, mut findings) = baseline::load_for_scan(project_root)?; let mut per_file = Vec::new(); @@ -136,7 +140,7 @@ pub(crate) fn pre_ingest( per_file.push((canonical_file, allowed)); } - let override_confirmed = confirm_override_if_needed(&all_allowed, options); + let override_confirmed = confirm_override_if_needed(project_root, &all_allowed, options); let mut briefing_blocks = BTreeMap::new(); let mut override_files = BTreeSet::new(); findings.extend(baseline_matches); @@ -186,6 +190,7 @@ pub(crate) fn pre_ingest( } fn confirm_override_if_needed( + project_root: &Path, allowed: &[(PathBuf, Detection)], options: &SecretScanOptions, ) -> bool { @@ -195,18 +200,11 @@ fn confirm_override_if_needed( if options.confirm_allow_unredacted_secrets.as_deref() == Some(CONFIRM_TOKEN) { return true; } + print_detection_list(project_root, allowed); if options.confirm_allow_unredacted_secrets.is_some() { abort_unconfirmed_override(); } if io::stdin().is_terminal() { - for (file, detection) in allowed { - eprintln!( - "{}:{} {}", - file.display(), - detection.line_number, - detection.rule_id - ); - } eprint!("Type '{CONFIRM_TOKEN}' to proceed: "); let _ = io::stderr().flush(); let mut input = String::new(); @@ -217,6 +215,17 @@ fn confirm_override_if_needed( abort_unconfirmed_override(); } +fn print_detection_list(project_root: &Path, allowed: &[(PathBuf, Detection)]) { + for (file, detection) in allowed { + eprintln!( + "{}:{} {}", + display_relative(project_root, file), + detection.line_number, + detection.rule_id + ); + } +} + fn abort_unconfirmed_override() -> ! { eprintln!("{OVERRIDE_UNCONFIRMED}: --allow-unredacted-secrets requires confirmation"); std::process::exit(78); @@ -231,12 +240,38 @@ fn normalize_project_path(root: &Path, path: &Path) -> PathBuf { } fn project_relative_path(root: &Path, path: &Path) -> PathBuf { - path.strip_prefix(root).unwrap_or(path).to_path_buf() + relative_path(root, path).unwrap_or_else(|| path.to_path_buf()) } pub(super) fn display_relative(root: &Path, path: &Path) -> String { - path.strip_prefix(root) - .unwrap_or(path) + relative_path(root, path) + .unwrap_or_else(|| path.to_path_buf()) .display() .to_string() } + +fn relative_path(root: &Path, path: &Path) -> Option { + let root = canonical_or_original(root); + let path = canonical_or_original(path); + path.strip_prefix(root).ok().map(Path::to_path_buf) +} + +#[cfg(test)] +mod tests { + use super::display_relative; + + #[cfg(unix)] + #[test] + fn display_relative_handles_noncanonical_root_with_canonical_file() { + let tmp = tempfile::tempdir().expect("tempdir"); + let real_root = tmp.path().join("real"); + let link_root = tmp.path().join("link"); + std::fs::create_dir(&real_root).expect("create real root"); + std::os::unix::fs::symlink(&real_root, &link_root).expect("symlink project root"); + let file = real_root.join(".env"); + std::fs::write(&file, b"token=example\n").expect("write fixture"); + let canonical_file = file.canonicalize().expect("canonical file"); + + assert_eq!(display_relative(&link_root, &canonical_file), ".env"); + } +} diff --git a/crates/clarion-cli/src/secret_scan/findings.rs b/crates/clarion-cli/src/secret_scan/findings.rs index 733fabf9..8c2faaf0 100644 --- a/crates/clarion-cli/src/secret_scan/findings.rs +++ b/crates/clarion-cli/src/secret_scan/findings.rs @@ -104,12 +104,7 @@ pub(super) fn secret_detected_finding(file: &Path, detection: &Detection) -> Pen } fn finding_entity_id(file_path: &Path, anchors: &BTreeMap) -> Option { - anchors.get(file_path).cloned().or_else(|| { - anchors - .iter() - .find(|(candidate, _)| candidate.ends_with(file_path)) - .map(|(_, entity_id)| entity_id.clone()) - }) + anchors.get(file_path).cloned() } fn hex20(bytes: [u8; 20]) -> String { @@ -121,3 +116,23 @@ fn hex20(bytes: [u8; 20]) -> String { } out } + +#[cfg(test)] +mod tests { + use super::finding_entity_id; + use std::{collections::BTreeMap, path::PathBuf}; + + #[test] + fn finding_entity_id_requires_exact_anchor_path() { + let mut anchors = BTreeMap::new(); + anchors.insert( + PathBuf::from("/repo/vendor/lib/.env"), + "core:file:vendor".to_owned(), + ); + + assert_eq!( + finding_entity_id(PathBuf::from("lib/.env").as_path(), &anchors), + None + ); + } +} diff --git a/crates/clarion-cli/src/serve.rs b/crates/clarion-cli/src/serve.rs index bde55c29..d045f9d3 100644 --- a/crates/clarion-cli/src/serve.rs +++ b/crates/clarion-cli/src/serve.rs @@ -50,7 +50,11 @@ pub fn run(path: &Path, config_path: Option<&Path>) -> Result<()> { let _runtime_guard = runtime.enter(); let readers = ReaderPool::open(&db_path, 16) .map_err(|err| anyhow!("open reader pool for {}: {err}", db_path.display()))?; + let http_project_root = project_root.clone(); let mut state = clarion_mcp::ServerState::new(project_root, readers); + let http_server = + crate::http_read::spawn(http_project_root, db_path.clone(), &config.serve.http) + .context("start HTTP read API")?; let mut llm_writer = None; let mut llm_writer_join = None; if let Some(provider) = llm_provider { @@ -72,6 +76,9 @@ pub fn run(path: &Path, config_path: Option<&Path>) -> Result<()> { clarion_mcp::serve_stdio_with_state_on_runtime(&runtime, &state, &mut reader, &mut writer) .context("serve MCP stdio"); drop(state); + if let Some(server) = http_server { + server.shutdown().context("stop HTTP read API")?; + } drop(llm_writer); let writer_result = if let Some(handle) = llm_writer_join { Some( diff --git a/crates/clarion-cli/tests/secret_scan.rs b/crates/clarion-cli/tests/secret_scan.rs index 91ebefd4..142c0880 100644 --- a/crates/clarion-cli/tests/secret_scan.rs +++ b/crates/clarion-cli/tests/secret_scan.rs @@ -470,6 +470,7 @@ fn non_tty_override_without_confirmation_exits_78_before_run_start() { .code(78); let stderr = String::from_utf8_lossy(&assert.get_output().stderr); assert!(stderr.contains("CLA-INFRA-SECRET-OVERRIDE-UNCONFIRMED")); + assert!(stderr.contains("leaky.sec:1 AwsAccessKeyId")); let run_count: i64 = conn(project.path()) .query_row("SELECT COUNT(*) FROM runs", [], |row| row.get(0)) .unwrap(); @@ -500,6 +501,7 @@ fn non_tty_override_with_wrong_confirmation_exits_78_before_run_start() { .code(78); let stderr = String::from_utf8_lossy(&assert.get_output().stderr); assert!(stderr.contains("CLA-INFRA-SECRET-OVERRIDE-UNCONFIRMED")); + assert!(stderr.contains("leaky.sec:1 AwsAccessKeyId")); let run_count: i64 = conn(project.path()) .query_row("SELECT COUNT(*) FROM runs", [], |row| row.get(0)) .unwrap(); diff --git a/crates/clarion-cli/tests/serve.rs b/crates/clarion-cli/tests/serve.rs index 35b102e5..fc234c0f 100644 --- a/crates/clarion-cli/tests/serve.rs +++ b/crates/clarion-cli/tests/serve.rs @@ -1,8 +1,10 @@ use std::fs; -use std::io::Write; +use std::io::{Read, Write}; +use std::net::{TcpListener, TcpStream}; use std::os::unix::fs::PermissionsExt; use std::path::Path; -use std::process::{Command as StdCommand, Stdio}; +use std::process::{Child, Command as StdCommand, Stdio}; +use std::time::{Duration, Instant}; use assert_cmd::Command; use clarion_core::{ @@ -84,6 +86,98 @@ fn serve_stdio_initialize_round_trip() { assert_eq!(response["result"]["serverInfo"]["name"], "clarion"); } +#[test] +fn serve_http_files_endpoint_resolves_known_file_on_configured_port() { + let dir = tempfile::tempdir().expect("temp project"); + clarion_bin() + .args(["install", "--path"]) + .arg(dir.path()) + .env("PATH", "") + .assert() + .success(); + let (file_id, content_hash, canonical_path) = seed_file_entity(dir.path()); + let bind = free_loopback_bind(); + write_http_config(dir.path(), &bind); + + let mut child = spawn_serve(dir.path()); + let response = wait_for_http_json(&bind, "/api/v1/files?path=demo.py&language=python"); + stop_serve(&mut child); + let response = response.expect("HTTP /api/v1/files response"); + let fixture: Value = serde_json::from_str(include_str!( + "../../../docs/federation/fixtures/get-api-v1-files.demo-python.json" + )) + .expect("parse files fixture"); + + assert_eq!(response["entity_id"], file_id); + assert_eq!(response["content_hash"], content_hash); + assert_eq!(response["canonical_path"], canonical_path); + assert_eq!(response["language"], "python"); + assert_eq!(response, fixture["response"]); +} + +#[test] +fn serve_http_capabilities_and_mcp_stdio_coexist() { + let dir = tempfile::tempdir().expect("temp project"); + clarion_bin() + .args(["install", "--path"]) + .arg(dir.path()) + .env("PATH", "") + .assert() + .success(); + let bind = free_loopback_bind(); + write_http_config(dir.path(), &bind); + + let mut child = spawn_serve(dir.path()); + let capabilities = wait_for_http_json(&bind, "/api/v1/_capabilities") + .expect("HTTP /api/v1/_capabilities response"); + + assert_eq!(capabilities["registry_backend"], true); + assert_eq!(capabilities["version"], "0.1"); + let fixture: Value = serde_json::from_str(include_str!( + "../../../docs/federation/fixtures/get-api-v1-capabilities.json" + )) + .expect("parse capabilities fixture"); + assert_eq!(capabilities, fixture["response"]); + + { + let stdin = child.stdin.as_mut().expect("child stdin"); + write_frame( + stdin, + &Frame { + body: serde_json::to_vec(&serde_json::json!({ + "jsonrpc": "2.0", + "id": 42, + "method": "initialize", + "params": { + "protocolVersion": "2025-11-25", + "capabilities": {}, + "clientInfo": {"name": "test-client", "version": "0.0.0"} + } + })) + .expect("serialize request"), + }, + ) + .expect("write initialize frame"); + stdin.flush().expect("flush initialize frame"); + } + drop(child.stdin.take()); + let output = child.wait_with_output().expect("wait for clarion serve"); + + assert!( + output.status.success(), + "serve failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + let mut reader = std::io::BufReader::new(std::io::Cursor::new(output.stdout)); + let frame = read_frame(&mut reader, ContentLengthCeiling::new(usize::MAX)) + .expect("read initialize response"); + let response: serde_json::Value = + serde_json::from_slice(&frame.body).expect("response body is json"); + + assert_eq!(response["id"], 42); + assert_eq!(response["result"]["serverInfo"]["name"], "clarion"); +} + #[test] fn serve_rejects_invalid_project_config() { let dir = tempfile::tempdir().expect("temp project"); @@ -511,3 +605,105 @@ fn call_summary_through_serve(project_root: &Path) -> Value { .expect("tool text"); serde_json::from_str(tool_text).expect("tool envelope") } + +fn seed_file_entity(project_root: &Path) -> (String, String, String) { + let source_path = project_root.join("demo.py"); + fs::write(&source_path, "def entry():\n return 1\n").expect("write source"); + let canonical_path = source_path + .canonicalize() + .expect("canonical source path") + .display() + .to_string(); + let content_hash = "hash-demo-file".to_owned(); + let file_id = "core:file:hash-demo@demo.py".to_owned(); + let db_path = project_root.join(".clarion/clarion.db"); + let conn = Connection::open(&db_path).expect("open sqlite"); + conn.execute( + "INSERT INTO entities ( + id, plugin_id, kind, name, short_name, source_file_path, + source_line_start, source_line_end, properties, content_hash, created_at, updated_at + ) VALUES ( + ?1, 'core', 'file', 'demo.py', 'demo.py', ?2, + 1, 2, '{}', ?3, + '2026-05-19T00:00:00.000Z', '2026-05-19T00:00:00.000Z' + )", + params![file_id, canonical_path, content_hash], + ) + .expect("insert file entity"); + (file_id, content_hash, "demo.py".to_owned()) +} + +fn free_loopback_bind() -> String { + let listener = TcpListener::bind("127.0.0.1:0").expect("bind free loopback port"); + listener.local_addr().expect("local addr").to_string() +} + +fn write_http_config(project_root: &Path, bind: &str) { + fs::write( + project_root.join("clarion.yaml"), + format!("version: 1\nserve:\n http:\n enabled: true\n bind: \"{bind}\"\n"), + ) + .expect("write HTTP serve config"); +} + +fn spawn_serve(project_root: &Path) -> Child { + StdCommand::new(assert_cmd::cargo::cargo_bin("clarion")) + .args(["serve", "--path"]) + .arg(project_root) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("spawn clarion serve") +} + +fn stop_serve(child: &mut Child) { + drop(child.stdin.take()); + if let Ok(Some(_)) = child.try_wait() { + return; + } + let _ = child.kill(); + let _ = child.wait(); +} + +fn wait_for_http_json(bind: &str, path: &str) -> Result { + let deadline = Instant::now() + Duration::from_secs(3); + let mut last_error = String::new(); + while Instant::now() < deadline { + match http_get_json(bind, path) { + Ok(value) => return Ok(value), + Err(err) => last_error = err, + } + std::thread::sleep(Duration::from_millis(25)); + } + Err(last_error) +} + +fn http_get_json(bind: &str, path: &str) -> Result { + let addr = bind + .parse() + .map_err(|err| format!("parse bind address {bind}: {err}"))?; + let mut stream = TcpStream::connect_timeout(&addr, Duration::from_millis(100)) + .map_err(|err| format!("connect to {bind}: {err}"))?; + stream + .set_read_timeout(Some(Duration::from_secs(1))) + .map_err(|err| format!("set read timeout: {err}"))?; + write!( + stream, + "GET {path} HTTP/1.1\r\nHost: {bind}\r\nConnection: close\r\n\r\n" + ) + .map_err(|err| format!("write request: {err}"))?; + let mut response = String::new(); + stream + .read_to_string(&mut response) + .map_err(|err| format!("read response: {err}"))?; + let (head, body) = response + .split_once("\r\n\r\n") + .ok_or_else(|| format!("malformed HTTP response: {response}"))?; + if !head.starts_with("HTTP/1.1 200") { + return Err(format!( + "unexpected HTTP response head: {head}; body: {body}" + )); + } + serde_json::from_str(body).map_err(|err| format!("parse json body {body:?}: {err}")) +} diff --git a/crates/clarion-core/src/llm_provider.rs b/crates/clarion-core/src/llm_provider.rs index e9627fa7..874a91a1 100644 --- a/crates/clarion-core/src/llm_provider.rs +++ b/crates/clarion-core/src/llm_provider.rs @@ -1718,6 +1718,7 @@ mod tests { } #[test] + #[allow(clippy::too_many_lines)] fn codex_cli_provider_invokes_exec_with_schema_stdin_and_usage() { use std::fs; use std::os::unix::fs::PermissionsExt; @@ -1848,7 +1849,7 @@ printf '%s' '{{"purpose":"via codex","behavior":"ran fake CLI","relationships":" assert_eq!(response.cached_input_tokens, 4); assert_eq!(response.output_tokens, 7); assert_eq!(response.total_tokens, 18); - assert_eq!(response.cost_usd, 0.0); + assert!(response.cost_usd.abs() < f64::EPSILON); let log = fs::read_to_string(log_path).expect("read fake codex log"); assert!(log.contains("subcommand=exec")); @@ -1860,6 +1861,7 @@ printf '%s' '{{"purpose":"via codex","behavior":"ran fake CLI","relationships":" } #[test] + #[allow(clippy::too_many_lines)] fn claude_cli_provider_invokes_print_mode_with_schema_and_usage() { use std::fs; use std::os::unix::fs::PermissionsExt; diff --git a/crates/clarion-mcp/src/config.rs b/crates/clarion-mcp/src/config.rs index 24b050b8..7a24a518 100644 --- a/crates/clarion-mcp/src/config.rs +++ b/crates/clarion-mcp/src/config.rs @@ -10,6 +10,7 @@ pub struct McpConfig { #[serde(alias = "llm_policy")] pub llm: LlmConfig, pub integrations: IntegrationsConfig, + pub serve: ServeConfig, } impl McpConfig { @@ -188,6 +189,28 @@ pub struct IntegrationsConfig { pub filigree: FiligreeConfig, } +#[derive(Debug, Clone, PartialEq, Default, Deserialize)] +#[serde(default)] +pub struct ServeConfig { + pub http: HttpReadConfig, +} + +#[derive(Debug, Clone, PartialEq, Deserialize)] +#[serde(default)] +pub struct HttpReadConfig { + pub enabled: bool, + pub bind: String, +} + +impl Default for HttpReadConfig { + fn default() -> Self { + Self { + enabled: false, + bind: "127.0.0.1:9111".to_owned(), + } + } +} + #[derive(Debug, Clone, PartialEq, Deserialize)] #[serde(default)] pub struct FiligreeConfig { @@ -375,6 +398,7 @@ llm_policy: ..LlmConfig::default() }, integrations: IntegrationsConfig::default(), + serve: ServeConfig::default(), }; let selected = select_provider_with_env(&cfg, |name| { @@ -395,6 +419,7 @@ llm_policy: ..LlmConfig::default() }, integrations: IntegrationsConfig::default(), + serve: ServeConfig::default(), }; let missing = select_provider_with_env(&cfg, |_| None).expect_err("missing key"); @@ -456,6 +481,7 @@ llm_policy: ..LlmConfig::default() }, integrations: IntegrationsConfig::default(), + serve: ServeConfig::default(), }; let selected = select_provider_with_env(&cfg, |_| None).expect("provider selection"); @@ -515,6 +541,7 @@ llm_policy: ..LlmConfig::default() }, integrations: IntegrationsConfig::default(), + serve: ServeConfig::default(), }; let selected = select_provider_with_env(&cfg, |_| None).expect("provider selection"); diff --git a/crates/clarion-scanner/src/baseline.rs b/crates/clarion-scanner/src/baseline.rs index ddf2f20e..f65013b9 100644 --- a/crates/clarion-scanner/src/baseline.rs +++ b/crates/clarion-scanner/src/baseline.rs @@ -195,10 +195,9 @@ impl Baseline { fn entries_for(&self, file: &Path) -> Vec<(&PathBuf, &BaselineEntry)> { self.entries - .iter() - .filter(|(candidate, _)| candidate.as_path() == file) - .flat_map(|(path, entries)| entries.iter().map(move |entry| (path, entry))) - .collect() + .get_key_value(file) + .map(|(path, entries)| entries.iter().map(move |entry| (path, entry)).collect()) + .unwrap_or_default() } } diff --git a/crates/clarion-scanner/src/patterns.rs b/crates/clarion-scanner/src/patterns.rs index cacd82bc..e9fd266a 100644 --- a/crates/clarion-scanner/src/patterns.rs +++ b/crates/clarion-scanner/src/patterns.rs @@ -1,4 +1,4 @@ -use regex::{Regex, RegexSet}; +use regex::bytes::{Regex, RegexSet}; use crate::{ Detection, SecretCategory, entropy::EntropyTuning, line_number_for_offset, sha1_digest, @@ -77,18 +77,19 @@ impl Scanner { #[must_use] pub fn scan_bytes(&self, buf: &[u8]) -> Vec { - let source = String::from_utf8_lossy(buf); - let bytes = source.as_bytes(); - let _set_matches = self.patterns.matches(&source); + let set_matches = self.patterns.matches(buf); let mut detections = Vec::new(); - for compiled in &self.compiled_patterns { - for captures in compiled.regex.captures_iter(&source) { + for (idx, compiled) in self.compiled_patterns.iter().enumerate() { + if !set_matches.matched(idx) { + continue; + } + for captures in compiled.regex.captures_iter(buf) { let Some(whole_match) = captures.get(0) else { continue; }; if compiled.meta.category == SecretCategory::ContextualCredential - && line_is_comment(bytes, whole_match.start()) + && line_is_comment(buf, whole_match.start()) { continue; } @@ -102,7 +103,7 @@ impl Scanner { }; detections.push(detection_from_match( &compiled.meta, - bytes, + buf, secret_match.start(), secret_match.end(), )); @@ -118,7 +119,7 @@ impl Scanner { ) }) .collect::>(); - self.scan_entropy(bytes, &named_ranges, &mut detections); + self.scan_entropy(buf, &named_ranges, &mut detections); detections.sort_by_key(|d| (d.byte_offset, d.rule_id)); detections @@ -130,14 +131,10 @@ impl Scanner { named_ranges: &[(usize, usize)], detections: &mut Vec, ) { - let source = String::from_utf8_lossy(bytes); - for candidate in self.entropy_b64_re.find_iter(&source) { - let candidate_bytes = &source.as_bytes()[candidate.start()..candidate.end()]; - if base64_candidate_has_boundaries( - source.as_bytes(), - candidate.start(), - candidate.end(), - ) && !range_overlaps(candidate.start(), candidate.end(), named_ranges) + for candidate in self.entropy_b64_re.find_iter(bytes) { + let candidate_bytes = &bytes[candidate.start()..candidate.end()]; + if base64_candidate_has_boundaries(bytes, candidate.start(), candidate.end()) + && !range_overlaps(candidate.start(), candidate.end(), named_ranges) && self.entropy_b64.accepts(candidate_bytes) { detections.push(entropy_detection( @@ -149,8 +146,8 @@ impl Scanner { )); } } - for candidate in self.entropy_hex_re.find_iter(&source) { - let candidate_bytes = &source.as_bytes()[candidate.start()..candidate.end()]; + for candidate in self.entropy_hex_re.find_iter(bytes) { + let candidate_bytes = &bytes[candidate.start()..candidate.end()]; if !range_overlaps(candidate.start(), candidate.end(), named_ranges) && self.entropy_hex.accepts(candidate_bytes) { @@ -306,6 +303,9 @@ fn is_base64_candidate_byte(byte: u8) -> bool { } fn line_is_comment(bytes: &[u8], offset: usize) -> bool { + // ADR-013's v0.1 rule floor is Python/.env-first, so only shell/Python + // `#` comments are ignored here. Other language comment forms should use + // an explicit baseline entry until their detector context is added. let line_start = bytes .get(..offset.min(bytes.len())) .and_then(|prefix| prefix.iter().rposition(|byte| *byte == b'\n')) diff --git a/crates/clarion-scanner/tests/scanner.rs b/crates/clarion-scanner/tests/scanner.rs index 4a6ca9ff..c1edfc54 100644 --- a/crates/clarion-scanner/tests/scanner.rs +++ b/crates/clarion-scanner/tests/scanner.rs @@ -154,6 +154,26 @@ fn detection_records_line_and_sha1_hash_without_literal() { assert_ne!(detection.hashed_secret, [0u8; 20]); } +#[test] +fn scan_bytes_reports_offsets_in_original_non_utf8_buffer() { + let input = b"\xff\xffkey = 'AKIAIOSFODNN7EXAMPLE'\n"; + let detections = Scanner::new().scan_bytes(input); + let detection = detections + .iter() + .find(|detection| detection.rule_id == "AwsAccessKeyId") + .expect("AWS key detection"); + let expected_offset = input + .windows(b"AKIAIOSFODNN7EXAMPLE".len()) + .position(|window| window == b"AKIAIOSFODNN7EXAMPLE") + .expect("secret literal is in fixture"); + + assert_eq!(detection.byte_offset, expected_offset); + assert_eq!( + hex20(detection.hashed_secret), + sha1_hex(b"AKIAIOSFODNN7EXAMPLE") + ); +} + #[test] fn baseline_suppresses_matching_detection_and_reports_fired_entry() { let scanner = Scanner::new(); diff --git a/crates/clarion-storage/Cargo.toml b/crates/clarion-storage/Cargo.toml index f8626885..e023d108 100644 --- a/crates/clarion-storage/Cargo.toml +++ b/crates/clarion-storage/Cargo.toml @@ -10,6 +10,7 @@ rust-version.workspace = true workspace = true [dependencies] +blake3.workspace = true clarion-core = { path = "../clarion-core", version = "0.1.0-dev" } deadpool-sqlite.workspace = true rusqlite.workspace = true diff --git a/crates/clarion-storage/src/lib.rs b/crates/clarion-storage/src/lib.rs index 62b62589..02830ca5 100644 --- a/crates/clarion-storage/src/lib.rs +++ b/crates/clarion-storage/src/lib.rs @@ -27,10 +27,10 @@ pub use commands::{ pub use error::{Result, StorageError}; pub use query::{ CallEdgeMatch, ContainedEntities, EntityRow, ModuleDependencyEdge, ReferenceDirection, - ReferenceEdgeMatch, SubsystemMember, UnresolvedCallSiteRow, call_edges_from, + ReferenceEdgeMatch, ResolvedFile, SubsystemMember, UnresolvedCallSiteRow, call_edges_from, call_edges_targeting, candidate_entities_for_unresolved_sites, child_entity_ids, contained_entity_ids, entity_at_line, entity_by_id, existing_entity_ids, find_entities, - module_dependency_edges, normalize_source_path, reference_edges_for_entity, + module_dependency_edges, normalize_source_path, reference_edges_for_entity, resolve_file, subsystem_for_member, subsystem_members, unresolved_call_sites_for_caller, unresolved_callers_for_target, }; diff --git a/crates/clarion-storage/src/query.rs b/crates/clarion-storage/src/query.rs index 28140237..216bc917 100644 --- a/crates/clarion-storage/src/query.rs +++ b/crates/clarion-storage/src/query.rs @@ -1,6 +1,7 @@ //! Read-side query helpers used by the MCP navigation surface. use std::collections::{BTreeMap, BTreeSet, HashSet}; +use std::fs; use std::path::{Component, Path, PathBuf}; use clarion_core::EdgeConfidence; @@ -74,6 +75,14 @@ pub struct ModuleDependencyEdge { pub edge_kinds: Vec, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ResolvedFile { + pub entity_id: String, + pub content_hash: String, + pub canonical_path: String, + pub language: String, +} + const MODULE_ANCESTOR_MAX_DEPTH: i64 = 32; #[derive(Debug, Clone, PartialEq, Eq)] @@ -150,6 +159,109 @@ pub fn entity_by_id(conn: &Connection, entity_id: &str) -> Result Result> { + let normalized = normalize_source_path(project_root, file)?; + let canonical_path = project_relative_path(project_root, Path::new(&normalized))?; + if let Some(entity) = source_entity_for_path(conn, &normalized, Some("file"))? { + return Ok(Some(ResolvedFile { + entity_id: entity.id, + content_hash: entity + .content_hash + .or_else(|| file_content_hash(Path::new(&normalized))) + .unwrap_or_default(), + canonical_path, + language: resolved_language(language, &entity.plugin_id, Path::new(&normalized)), + })); + } + let Some(anchor) = source_entity_for_path(conn, &normalized, None)? else { + return Ok(None); + }; + let content_hash = anchor + .content_hash + .or_else(|| file_content_hash(Path::new(&normalized))) + .unwrap_or_default(); + let synthetic_id = format!("core:file:{content_hash}@{canonical_path}"); + Ok(Some(ResolvedFile { + entity_id: synthetic_id, + content_hash, + canonical_path, + language: resolved_language(language, &anchor.plugin_id, Path::new(&normalized)), + })) +} + +fn source_entity_for_path( + conn: &Connection, + normalized_path: &str, + required_kind: Option<&str>, +) -> Result> { + let kind_filter = required_kind.map_or(String::new(), |_| "AND kind = ?2".to_owned()); + let sql = format!( + "SELECT {ENTITY_COLUMNS} \ + FROM entities \ + WHERE source_file_path = ?1 \ + {kind_filter} \ + ORDER BY CASE kind \ + WHEN 'file' THEN 0 \ + WHEN 'module' THEN 1 \ + ELSE 2 \ + END ASC, \ + id ASC \ + LIMIT 1" + ); + let mut stmt = conn.prepare(&sql)?; + let row = if let Some(kind) = required_kind { + stmt.query_row(params![normalized_path, kind], map_entity_row) + .optional()? + } else { + stmt.query_row(params![normalized_path], map_entity_row) + .optional()? + }; + Ok(row) +} + +fn project_relative_path(project_root: &Path, normalized_path: &Path) -> Result { + let root = project_root.canonicalize()?; + let relative = normalized_path.strip_prefix(&root).map_err(|_| { + StorageError::InvalidSourcePath(format!( + "{} is not under project root {}", + normalized_path.display(), + root.display() + )) + })?; + relative + .to_str() + .map(str::to_owned) + .ok_or_else(|| StorageError::InvalidSourcePath("source path is not valid UTF-8".to_owned())) +} + +fn file_content_hash(path: &Path) -> Option { + fs::read(path) + .ok() + .map(|bytes| blake3::hash(&bytes).to_hex().to_string()) +} + +fn resolved_language(requested: &str, plugin_id: &str, path: &Path) -> String { + if !requested.trim().is_empty() { + return requested.trim().to_owned(); + } + if plugin_id != "core" { + return plugin_id.to_owned(); + } + match path.extension().and_then(|extension| extension.to_str()) { + Some("py") => "python".to_owned(), + Some("rs") => "rust".to_owned(), + Some("js") => "javascript".to_owned(), + Some("ts") => "typescript".to_owned(), + Some(extension) => extension.to_owned(), + None => String::new(), + } +} + /// Return the subset of `candidates` whose `id` appears in `entities`. Used by /// the inferred-edge dispatch path to pre-filter LLM-proposed `to_id` values /// before they reach the writer-actor's FK-protected INSERT (clarion-df58379de4). diff --git a/crates/clarion-storage/src/reader.rs b/crates/clarion-storage/src/reader.rs index 8856becc..75d7a1a2 100644 --- a/crates/clarion-storage/src/reader.rs +++ b/crates/clarion-storage/src/reader.rs @@ -14,6 +14,7 @@ use crate::error::Result; use crate::pragma; /// A read-only connection pool backed by `deadpool-sqlite`. +#[derive(Clone)] pub struct ReaderPool { pool: Pool, } diff --git a/docs/federation/contracts.md b/docs/federation/contracts.md new file mode 100644 index 00000000..487cd872 --- /dev/null +++ b/docs/federation/contracts.md @@ -0,0 +1,73 @@ +# Clarion Federation Contracts + +This file pins Clarion's read-side contract for sibling products. The initial +consumer is Filigree's `ClarionRegistry` from ADR-014. + +## HTTP Read API + +`clarion serve` can expose the HTTP read API when enabled in `clarion.yaml`: + +```yaml +serve: + http: + enabled: true + bind: 127.0.0.1:9111 +``` + +The MCP stdio server remains available on stdin/stdout. The HTTP surface is +read-only and uses Clarion's existing SQLite reader pool. + +### `GET /api/v1/files?path=&language=` + +Resolves a project-relative or absolute file path to the Clarion file identity +Filigree stores as `file_records.id` when `registry_backend: clarion` is active. + +Query parameters: + +| Name | Required | Meaning | +|---|---:|---| +| `path` | yes | File path under the Clarion project root. | +| `language` | no | Caller-supplied language hint. If absent, Clarion infers from the source entity or file extension. | + +Successful response: + +```json +{ + "entity_id": "core:file:hash-demo@demo.py", + "content_hash": "hash-demo-file", + "canonical_path": "demo.py", + "language": "python" +} +``` + +Semantics: + +- `entity_id` is opaque to Filigree. +- `content_hash` is the drift signal Filigree stores with the resolved row. +- `canonical_path` is Clarion's project-relative canonical path. +- `language` is the normalized language value Clarion used for the resolution. +- Unknown or outside-project paths return a non-2xx JSON error instead of + guessing. + +Fixture: [`fixtures/get-api-v1-files.demo-python.json`](./fixtures/get-api-v1-files.demo-python.json). + +### `GET /api/v1/_capabilities` + +Reports whether this Clarion instance can serve the registry-backend read +contract. + +Successful response: + +```json +{ + "registry_backend": true, + "file_registry": true, + "version": "0.1" +} +``` + +Filigree should treat `registry_backend: true` as the flag that the +`/api/v1/files` resolution surface is present. + +Fixture: [`fixtures/get-api-v1-capabilities.json`](./fixtures/get-api-v1-capabilities.json). + diff --git a/docs/federation/filigree-side/2026-05-19-registry-backend-cross-project-sequencing.md b/docs/federation/filigree-side/2026-05-19-registry-backend-cross-project-sequencing.md new file mode 100644 index 00000000..e9d83554 --- /dev/null +++ b/docs/federation/filigree-side/2026-05-19-registry-backend-cross-project-sequencing.md @@ -0,0 +1,124 @@ +# Registry-Backend & File-Identity Displacement — Cross-Project Sequencing Memo + +**Status:** Draft (2026-05-19) +**Scope:** Sequencing memo for bringing Clarion ADR-014's vision forward across Filigree and Clarion. Pairs with [Filigree ADR-014](../architecture/decisions/ADR-014-registry-backend-and-file-identity-displacement.md). +**Sibling docs:** +- Clarion ADR-014 (the original 2026-04-18 design) +- Clarion ADR-029 (entity-associations, shipped 2026-05-17) +- Clarion v0.1 plan §WP10 (the cross-product work package this memo activates) + +--- + +## 1. Problem in one paragraph + +`loom.md` §2 says "Clarion owns the file registry." The 2.1.0 Filigree code still mints Filigree-native shadow file IDs on every `POST /api/loom/scan-results`, every `create_observation(file_path=…)`, and every `trigger_scan*`. The Clarion-side design exists (ADR-014, 2026-04-18) and was accepted; the Filigree-side counterpart was never drafted; the Clarion-side WP10 was deferred to v0.2 by the 2026-05-16 Sprint 2 amendment. ADR-029 (entity_associations, May 17) ships the *peer* primitive — issue↔entity binding — but does not close the file-identity split. This memo names what each side must do to close it. + +## 2. Why now + +- ADR-029 is the last work that *could* have substituted for ADR-014's vision. It explicitly does not. +- Filigree's 2.1.0 release prep is mid-flight (see `2026-05-18-2.1.0-release-prep.md`); landing the schema migration alongside that release minimises the operator-visible migration count. +- Clarion's Sprint 2 closed clean (2026-05-17 signoff); Sprint 3 is unscoped and can absorb the Clarion-side HTTP read API as its anchor work package. +- The Loom URI spec (`2026-05-17-loom-uri-spec.md`) is in draft and depends on a coherent file-identity story across the federation. Closing the split here removes a foundational ambiguity from that spec's scope. + +## 3. Work, owned by project + +### 3.1 Clarion side (Sprint 3, anchor work) + +WP10 from `docs/implementation/v0.1-plan.md` was always Clarion's side of this story. Sprint 2 deferred it; this memo lifts it back into active scope. Plus one new item the original plan did not have: Clarion has no HTTP server today. The `resolve_file` surface ADR-014 assumes must be built. + +| ID | Title | Notes | +|---|---|---| +| C-WP10.1 | Clarion HTTP read API — `axum` server in `clarion-cli` | New crate-internal module; reuses `ReaderPool`. Bind on a port advertised in `clarion.yaml`. Wire into `clarion serve` alongside MCP stdio. | +| C-WP10.2 | `GET /api/v1/files?path=&language=` endpoint | Returns `{entity_id, content_hash, canonical_path, language}`. Backed by `clarion-storage::reader` queries; pure read, no writer involvement. | +| C-WP10.3 | Contracts directory for the read API | `docs/federation/contracts.md` on Clarion's side, parity with Filigree's. Publish a JSON fixture for `GET /api/v1/files`. | +| C-WP10.4 | Capability probe response | `GET /api/v1/_capabilities` (or equivalent) returning `{file_registry: true, version: "0.1"}` so Filigree's `ClarionRegistry` can fail fast on incompatible deployments. | +| C-WP10.5 | Sprint 3 scope amendment memo | Mirrors `sprint-2/scope-amendment-2026-05.md` — names the lift, the rationale (this memo), the dependency on Filigree Phase A→B landing. | + +Estimated cost: one Clarion sprint (~2 weeks). The dominant lift is C-WP10.1 (no HTTP machinery exists today). + +### 3.2 Filigree side (2.1.x or 2.2.0) + +Maps onto ADR-014 Phases B / C / E. + +| Phase | Title | Maps to ADR-014 § | +|---|---|---| +| F-B.1 | `RegistryProtocol` interface + `LocalRegistry` impl | §1, §3 | +| F-B.2 | Refactor `_upsert_file_record` to consume `RegistryProtocol` | §1 | +| F-B.3 | Refactor `register_file` (db_files, db_observations) to consume `RegistryProtocol` | §1 | +| F-B.4 | Refactor the three `tracker.register_file` call sites in `mcp_tools/scanners.py` | §1 | +| F-B.5 | Schema migration: add `file_records.content_hash`, `file_records.registry_backend` | §3 | +| F-B.6 | Test suite parameterisation over `registry_backend` (default-only after B) | §1 | +| F-C.1 | `registry_backend` config flag wiring (`.filigree.conf`, `ProjectConfig`) | §2 | +| F-C.2 | `ClarionRegistry` implementation (reqwest-equivalent in Python) | §1 | +| F-C.3 | Capability probe in `GET /api/files/_schema.config_flags` | §4 | +| F-C.4 | `FILIGREE_FILE_REGISTRY_DISPLACED` error code + the three direct-mutation surfaces that emit it | §6 | +| F-C.5 | Fail-closed startup under `clarion` mode; `--allow-local-fallback` flag | §7 | +| F-C.6 | `filigree migrate-registry` CLI verb (dry-run, execute, rollback, manifest) | §5 | +| F-E.1 | `docs/federation/contracts.md` update referencing Clarion's read API | §8 | +| F-E.2 | Cross-project launch runbook | §"Sequencing" | + +Estimated cost: Phase B is ~1–2 weeks (mostly the refactor + tests). Phase C is ~2 weeks (config + capability + migration tool + fail-closed). Phase E is ~3 days (docs). + +### 3.3 Cross-project (Phase D) + +Integration tests run against a live Clarion read API. Owned jointly — fixtures published from Filigree, contract pinned-SHA per the pattern Clarion already uses for Filigree's entity-associations. + +## 4. Sequencing and the critical path + +``` + Clarion Sprint 3 Filigree 2.1.x or 2.2.0 + ──────────────── ───────────────────────── + C-WP10.1 axum server + │ + ├──→ C-WP10.2 /api/v1/files + │ + ├──→ C-WP10.4 /api/v1/_capabilities + │ + └──→ C-WP10.3 contracts + fixtures + F-B.1..6 refactor + schema + │ + ▼ + F-C.1..6 flag, mode, error, migration + │ + ▼ + D cross-process integration tests + │ + ▼ + E docs + launch runbook +``` + +Phase B is independent of Clarion-side work and can land first (behaviour-preserving refactor + schema columns at empty defaults). Phase C cannot land usefully until C-WP10.2 ships. Phase D blocks on both. + +## 5. Decisions deferred + +- **Batched resolution.** ADR-014 §Negative names sync-RPC cost as an issue under high-throughput scans. Out of scope; revisit if a real workload hits the wall. The `RegistryProtocol` interface admits batching at a future minor version (`resolve_files(paths: list[str])`). +- **Read-side displacement.** `GET /api/loom/files` continues to return Filigree's stored rows. Whether Clarion entity IDs should be the visible IDs in those responses (vs. left as-is, with consumers cross-referencing) is a UX decision; deferred until at least one cross-tool consumer asks. +- **Wardline integration.** Clarion ADR-015's native Wardline→Filigree emitter is unchanged in scope. Wardline findings still flow through Clarion's SARIF translator under both backends. + +## 6. Risks + +- **Schema migration adoption.** F-B.5 ships a forward-only `ALTER TABLE` with safe defaults. Verified compatible with the 2.0.x → 2.1.x migration path. Operators with custom backup tooling that snapshots `file_records` mid-migration could see inconsistent rows; documented in the release notes. +- **Clarion ADR-014's `--allow-local-fallback` semantics.** The fallback flag exists in case the operator's Clarion is unreachable at startup; if the flag is left on permanently, the operator silently runs in `local` mode while believing they run `clarion` mode. F-C.5 surfaces a persistent dashboard banner while the flag is active to prevent this. +- **Sprint 3 scope contention.** Clarion's Sprint 3 has not yet been scoped; this memo proposes WP10 as the anchor. If Sprint 3 commits to something else first, F-B can still land (behaviour-preserving) and F-C waits. + +## 7. What lands when (target dates, not commitments) + +| Milestone | Target | Gates | +|---|---|---| +| Filigree ADR-014 ratified | 2026-05-21 | One reviewer; user sign-off on this memo first | +| Clarion Sprint 3 scope amendment ratified | 2026-05-23 | Clarion sprint-planning meeting (author = self) | +| F-B lands on Filigree main | 2026-06-06 | Tests green; no functional change visible to operators | +| C-WP10.1..4 land on Clarion main | 2026-06-13 | `clarion serve` exposes HTTP read API on a configurable port | +| F-C lands behind feature flag | 2026-06-20 | `registry_backend: clarion` works against a local Clarion | +| Phase D integration tests green | 2026-06-27 | Cross-process CI lane in both repos | +| Phase E docs published | 2026-06-30 | Cross-project launch runbook + contract refs | + +## 8. Open question for the user + +This memo is a strategy proposal, not a plan. Before issues are filed in either tracker, sign-off on: + +1. **Scope of "as initially designed."** This memo treats ADR-014 (2026-04-18) as the design of record. ADR-029 stays. The `entity_associations` table is *not* unwound — it remains the right primitive for issue↔entity binding. ADR-014 is additive over it. +2. **Sequencing.** Filigree Phase B first (independent, behaviour-preserving) is the safer parallelisation than waiting for Clarion. If you'd rather sequence Clarion-first to validate the read-API shape against a real consumer, F-B waits. +3. **Migration verb scope.** The `filigree migrate-registry` CLI verb is the operationally-real piece of this story. Worth a second pair of eyes (operator-facing tools land harder than refactors). If you'd rather defer the migration verb behind an explicit follow-up issue (operators stay on `local` for the first release of `clarion` mode), F-C ships smaller. + +On greenlight, the next two artifacts are: (a) Filigree filigree-tracker milestone + phases + steps under the existing `planning` pack, (b) Clarion filigree-tracker milestone + phases + steps mirroring WP10. Both repos already use filigree for their own work tracking; no new tracker required. diff --git a/docs/federation/filigree-side/ADR-014-registry-backend-and-file-identity-displacement.md b/docs/federation/filigree-side/ADR-014-registry-backend-and-file-identity-displacement.md new file mode 100644 index 00000000..6e912489 --- /dev/null +++ b/docs/federation/filigree-side/ADR-014-registry-backend-and-file-identity-displacement.md @@ -0,0 +1,227 @@ +# ADR-014: `registry_backend` Flag and File-Identity Displacement to Clarion + +**Status**: Accepted +**Date**: 2026-05-19 +**Deciders**: John (project lead) +**Context**: Closes the Filigree-side hole that Clarion ADR-014 (2026-04-18) named as a v0.1 prerequisite and that Clarion ADR-029 (2026-05-16) explicitly deferred. The work is Filigree's; the sibling decision is Clarion's. + +## Summary + +Filigree gains a pluggable `RegistryProtocol` selected by a `registry_backend` config flag with two modes: + +- `local` (default, unchanged behaviour) — Filigree's native UUID-derived file IDs. +- `clarion` (opt-in, per-project) — Filigree delegates file-identity resolution to Clarion's HTTP read API; `file_records.id` stores Clarion's symbolic entity ID (`core:file:{hash}@{path}` per Clarion ADR-003). + +A `FILIGREE_FILE_REGISTRY_DISPLACED` error code surfaces direct file-registration attempts that conflict with `clarion` mode. The `registry_backend` value is published in `GET /api/files/_schema.config_flags` for capability probing. Fail-closed startup applies only under `clarion` mode (an `--allow-local-fallback` escape exists for single-operator recovery). + +The new column `file_records.content_hash` stores the hash Clarion supplied at resolution time, reusing the same drift-vocabulary that ADR-029's `entity_associations.content_hash_at_attach` introduced. There is one drift signal across both surfaces. + +## Context + +### The gap ADR-029 leaves on the table + +ADR-029 (the entity-associations binding) is shipping and is right. It does not, however, close the file-identity split between Filigree and Clarion. Concretely, today, on the 2.1.0 branch: + +- `POST /api/loom/scan-results` (`dashboard_routes/files.py:401-417`) routes to `db.process_scan_results(**parsed)`. +- `process_scan_results` (`db_files.py:857-926`) iterates findings and calls `_upsert_file_record(path=f["path"], …)` for each. +- `_upsert_file_record` (`db_files.py:640-678`) mints a Filigree-native ID (`f"{prefix}-f-{uuid4().hex[:10]}"`) the first time it sees a path. +- No code path consults `scan_source` (or the `metadata.clarion.*` payload) to thread Clarion's entity ID through as the `file_records.id`. + +The result: every Clarion-sourced scan POST mints a shadow file row whose ID is Filigree-native; an issue with an `entity_associations` row pointing at `python:function:auth.tokens::issue_token` and a `file_associations` row pointing at the file that function lives in carries **two unrelated identities for the same code**. `loom.md` §2's claim that "Clarion owns the file registry" is, today, false at the storage layer. + +Two further auto-create paths exhibit the same shadow-mint behaviour: `db_observations.register_file` (`db_observations.py:223`) and the three call sites of `tracker.register_file` in `mcp_tools/scanners.py` (`:657`, `:746`, `:964`). + +ADR-029 explicitly named this gap as out of scope and called the registry-backend work "still-scheduled." This ADR is the schedule. + +### Why ADR-029's approach is not a substitute + +ADR-029's defence — opaque-string IDs, no schema surgery, no Clarion-runtime dependency — answers the question *"how do we let Filigree issues reference Clarion entities without coupling the products?"*. It does not answer *"how do we make the file_id Filigree stores be the same identifier Clarion stores?"*. Those are different questions; ADR-029 solves the first, this ADR solves the second. + +### The "thrown away" history + +Clarion ADR-014 (2026-04-18) designed this displacement in detail: `RegistryProtocol` trait, `local`/`clarion` modes, `FILIGREE_FILE_REGISTRY_DISPLACED` error code, capability probe via `_schema.config_flags`, fail-closed startup, `--allow-local-fallback` recovery flag. The Filigree-side ADR was never drafted; the WP10 work package on the Clarion side was deferred to v0.2 by the Sprint 2 scope amendment (2026-05-16). This ADR adopts Clarion ADR-014's design near-verbatim and is the Filigree-side counterpart that closes the cross-product story. + +## Decision + +### 1. `RegistryProtocol` interface + +A new module `filigree.registry` defines: + +```python +class RegistryProtocol(Protocol): + def resolve_file( + self, + path: str, + *, + language: str = "", + actor: str = "", + ) -> ResolvedFile: ... + + def is_displaced(self) -> bool: ... + +class ResolvedFile(TypedDict): + file_id: str # opaque to Filigree; semantics owned by the backend + content_hash: str # opaque to Filigree; used as drift signal only + canonical_path: str # backend's preferred canonical form of `path` + language: str # may be empty; backend may infer +``` + +Two implementations: + +- `LocalRegistry` — current behaviour. `file_id` is `f"{prefix}-f-{uuid4().hex[:10]}"`. `content_hash` is the empty string under `local` mode (column is nullable in the schema; see §3). `is_displaced()` returns `False`. +- `ClarionRegistry` — issues `GET {clarion_base}/api/v1/files?path=…&language=…` and returns Clarion's `{entity_id, content_hash, canonical_path, language}` reshaped into `ResolvedFile`. `is_displaced()` returns `True`. Connection failures surface as `RegistryUnavailableError` (see §6). + +The protocol is composed into `FiligreeDB` at construction time; the three auto-create surfaces (`_upsert_file_record`, `register_file`, `tracker.register_file`) take a `registry: RegistryProtocol` parameter instead of generating IDs inline. + +### 2. `registry_backend` configuration + +`registry_backend` is a project-scoped setting in `.filigree.conf`: + +```yaml +registry_backend: local # default +clarion: + base_url: http://localhost:9111 + timeout_seconds: 5 + allow_local_fallback: false +``` + +`local` is the forever-default. Filigree-the-project, every existing Filigree dogfood, and every existing third-party Filigree deployment continue to operate without change. Clarion mode is strictly opt-in per project. + +### 3. Schema additions + +```sql +ALTER TABLE file_records ADD COLUMN content_hash TEXT NOT NULL DEFAULT ''; +ALTER TABLE file_records ADD COLUMN registry_backend TEXT NOT NULL DEFAULT 'local'; +``` + +Both columns survive a backend swap: a row created under `local` and re-resolved under `clarion` updates `content_hash`, `registry_backend`, and (one-time, see §5) the row's `id`. + +Schema version bumps; migration is forward-only and additive (no FK rewrites, no row identity churn under default `local` mode). + +### 4. Capability probe + +`GET /api/files/_schema` gains a `config_flags` block: + +```json +{ + "config_flags": { + "registry_backend": "local", + "registry_backend_features": ["local"], + "allow_local_fallback": false + } +} +``` + +`registry_backend_features` enumerates what this Filigree build *can* serve (always `["local"]` after Phase B; `["local", "clarion"]` after Phase C). Clarion's startup probe reads this; absent the field, Clarion enters shadow-registry mode. + +### 5. ID rewrite policy under backend swap + +A project that flips from `local` to `clarion` mid-life will have existing `file_records` rows with Filigree-native IDs and four NOT-NULL FK consumers pointing at those IDs. The displacement story therefore needs a row-ID rewrite path: + +- A new CLI verb `filigree migrate-registry --to clarion [--dry-run]` issues `resolve_file` for every existing row, fetches Clarion's entity ID, and rewrites `file_records.id` and all four FK consumers (`scan_findings.file_id`, `file_associations.file_id`, `file_events.file_id`, plus the `entity_associations` table introduced in PR #42 — verified to *not* hold file IDs, only entity IDs, so untouched here) inside a single SQLite transaction. +- Rows whose paths Clarion cannot resolve (deleted-on-disk, outside-project, etc.) are flagged in the manifest; the operator chooses delete-row or keep-as-orphan. +- Rollback uses the same manifest in reverse. + +The migration is not run automatically. A capability-probe mismatch (registry says `clarion` but rows have `registry_backend = 'local'`) raises `RegistryStateMismatch` on next write and halts auto-create paths until the operator runs the migration or reverts the flag. + +### 6. `FILIGREE_FILE_REGISTRY_DISPLACED` error code + +Under `clarion` mode, the following direct-mutation paths return `FILIGREE_FILE_REGISTRY_DISPLACED`: + +- MCP tool `register_file`. +- CLI verb `filigree register-file`. +- HTTP `POST /api/files` direct-create (if/when it exists; currently not exposed). + +The error message includes the Clarion read URL the operator should use instead. The three auto-create paths route through `RegistryProtocol` and never raise this code — they get Clarion IDs transparently. + +### 7. Fail-closed startup under `clarion` mode + +If `registry_backend: clarion` is configured but the Clarion HTTP read API is unreachable at Filigree startup, the three auto-create paths return `503 Service Unavailable` with `RegistryUnavailableError`. Read paths (`GET /api/loom/files`, `GET /api/loom/issues/.../files`) continue to operate against stored rows. + +`allow_local_fallback: true` (in `.filigree.conf` or via `--allow-local-fallback`) downgrades the failure to a `WARN` and routes auto-creates through `LocalRegistry`. The flag is for single-operator recovery, not steady-state operation; the dashboard surfaces a banner while it is active. + +### 8. Living surface, classic surface + +The `registry_backend` flag affects *behaviour*, not API shape. Both classic (`/api/v1/scan-results`) and loom (`/api/loom/scan-results`) handlers continue to accept identical payloads. Under `clarion` mode, the `file_id` returned in responses is a Clarion entity ID rather than a Filigree-native ID; the *shape* is unchanged (`file_id: str`). This is a contract-level addition, not a break: ADR-002 generation freezes apply to shapes, not to ID grammars. + +## Alternatives Considered + +### Alternative 1 — Keep ADR-029 only; never close the file-identity split + +`entity_associations` covers the cross-product reference need for issues. Files keep Filigree-native IDs forever; `loom.md` §2's "Clarion owns the file registry" is informally downgraded to "Clarion owns the entity catalog; Filigree owns the file mapping." + +**Why rejected**: the downgrade is real but unstated; consumers reading `loom.md` get one story, the code does another. Either fix the code or fix the doctrine. Fixing the code is the cheaper of the two because the design already exists (Clarion ADR-014) and the surface is bounded (5–8 files; ~17 test files reference `file_id` directly). + +### Alternative 2 — Single mode, always-Clarion (no flag) + +Drop `local`; always delegate. Filigree without Clarion fails to start. + +**Why rejected**: violates `loom.md` §4 composition law and §5 enrichment failure test. Filigree-the-project (which uses Filigree to track its own work) would require Clarion to operate, which is absurd. The flag is the price of staying federated. + +### Alternative 3 — Generalize `entity_associations` to carry file IDs too + +Add an `association_kind: 'file' | 'entity'` discriminator to `entity_associations`; let files ride. + +**Why rejected**: same reason ADR-029 rejected merging file_associations and entity_associations — overloading. `file_records.id` is referenced by four NOT-NULL FKs; routing those references through a discriminated union would touch more code than the `RegistryProtocol` refactor and would leave `file_records.id` itself still shadowed. + +### Alternative 4 — Schema-level join across two DBs (Filigree + Clarion) + +`file_records.id` becomes a foreign key into `.clarion/clarion.db`. + +**Why rejected**: `loom.md` §6 — no shared store. Each product owns its storage. The HTTP-mediated `RegistryProtocol` is the federation axiom expressed as code. + +## Consequences + +### Positive + +- `loom.md` §2 "Clarion owns the file registry" becomes honest at the storage layer. +- Cross-tool "same file" queries get a deterministic answer: same ID across products under `clarion` mode. +- Reuses ADR-029's drift vocabulary (`content_hash`); one mental model for both file-level and entity-level drift. +- `local` stays default; no impact on Filigree-only deployments. + +### Negative + +- Two code paths per auto-create operation. Test surface doubles for file-registry behaviour (parameterise the test suite over `registry_backend ∈ {local, clarion}`). +- One synchronous RPC hop per Filigree write that touches `file_records` under `clarion` mode. Loopback HTTP cost ~1–5ms; acceptable for developer workloads, would need batched resolution for high-throughput scans (Phase E candidate). +- Cross-product launch sequencing: under `clarion` mode the operator must start Clarion's HTTP read API before Filigree, or set `--allow-local-fallback` for recovery. +- The `migrate-registry` CLI verb is a one-way operation in practice (rollback only works inside the reversibility window). Documented as a hard boundary. + +### Neutral + +- Classic and loom HTTP shapes unchanged. ADR-002 generation discipline applies to shape, not ID grammar; `clarion` mode's swap of ID grammar is contract-compatible. +- `entity_associations.clarion_entity_id` is still opaque to Filigree under `clarion` mode — the two surfaces remain independent. The same Clarion entity ID may appear in both `file_records.id` (for the file the entity lives in) and `entity_associations.clarion_entity_id` (for the entity itself, e.g. a function inside that file); the relationship between them is Clarion's domain, not Filigree's. + +## Sequencing (cross-project) + +The work has a fixed one-way dependency: Filigree's `clarion` mode is a no-op until Clarion ships an HTTP read API. + +| Phase | Owner | Scope | +|---|---|---| +| **A** | Clarion | Add an `axum`-based HTTP read server to `clarion-cli/src/serve.rs`. Expose `GET /api/v1/files?path=&language=` returning `{entity_id, content_hash, canonical_path, language}`. Wire into `clarion serve`. Surface in `clarion.yaml`. Document in Clarion's contracts directory. | +| **B** | Filigree | Land `RegistryProtocol` interface and `LocalRegistry`; refactor `_upsert_file_record`, `register_file`, and the three `tracker.register_file` call sites to consume the protocol. Behavior-preserving — no flag yet, default-only. Schema migration adds `content_hash` and `registry_backend` columns (empty values under `local`). | +| **C** | Filigree | Add `registry_backend` config flag, `ClarionRegistry` impl, capability probe (`_schema.config_flags`), `FILIGREE_FILE_REGISTRY_DISPLACED` error code, fail-closed startup, `--allow-local-fallback` escape, the `migrate-registry` CLI verb. | +| **D** | Both | Cross-process integration tests against a live Clarion read API. Parity tests parameterised over `registry_backend ∈ {local, clarion}`. Capability-probe handshake tests. | +| **E** | Both | Documentation: Filigree `docs/federation/contracts.md` references the Clarion read surface; Clarion's `loom.md` §2 claim is restated as factual rather than aspirational; cross-project launch runbook published. | + +Phase A must ship before Phase C can land an integration that does anything observable; B is independent and can ship first behind a flagless refactor. + +## Related Decisions + +- **ADR-002** (this repo) — `registry_backend` is *behaviour*, not a generation. Loom/classic HTTP shapes are unchanged; this ADR is contract-compatible by construction. +- **ADR-029 of Clarion** — entity_associations is the peer concept; this ADR closes the file-side of the same split. Same drift vocabulary (`content_hash`). +- **ADR-014 of Clarion** — original 2026-04-18 design; this ADR is its Filigree-side counterpart and adopts the design near-verbatim. +- **ADR-015 of Clarion** — Wardline→Filigree native emitter; not in scope here. Wardline's findings continue to flow via Clarion's SARIF translator under both backends. +- **Loom URI spec (draft 2026-05-17)** — orthogonal; URIs and registry-backend are independent decisions. Not yet ratified; not used as a cross-tracker reference primitive in this ADR. + +## References + +- Clarion ADR-014: `/home/john/clarion/docs/clarion/adr/ADR-014-filigree-registry-backend.md`. +- Clarion ADR-029: `/home/john/clarion/docs/clarion/adr/ADR-029-entity-associations-binding.md`. +- Clarion v0.1 plan §WP10: `/home/john/clarion/docs/implementation/v0.1-plan.md` (the cross-product work package this ADR fulfils). +- Sprint 2 scope amendment (defer): `/home/john/clarion/docs/implementation/sprint-2/scope-amendment-2026-05.md`. +- Clarion integration recon: `/home/john/clarion/docs/clarion/v0.1/reviews/pre-restructure/integration-recon.md` (auto-create paths and FK survey). +- Filigree auto-create paths (verified 2026-05-19): + - `src/filigree/db_files.py:640` `_upsert_file_record` + - `src/filigree/db_files.py:184` `register_file` + - `src/filigree/db_observations.py:223` `register_file` + - `src/filigree/mcp_tools/scanners.py:657,:746,:964` `tracker.register_file` diff --git a/docs/federation/filigree-side/README.md b/docs/federation/filigree-side/README.md new file mode 100644 index 00000000..a13ca6cb --- /dev/null +++ b/docs/federation/filigree-side/README.md @@ -0,0 +1,9 @@ +# Filigree-Side Federation Context + +This directory mirrors Filigree-authored planning artifacts for Clarion's local +cross-reference while Sprint 3 scopes WP10. + +These files are read-only context copies. The authoritative sources live in +`/home/john/filigree`; update them there, then refresh this mirror if Clarion +needs a newer snapshot. + diff --git a/docs/federation/fixtures/get-api-v1-capabilities.json b/docs/federation/fixtures/get-api-v1-capabilities.json new file mode 100644 index 00000000..e04aa3d7 --- /dev/null +++ b/docs/federation/fixtures/get-api-v1-capabilities.json @@ -0,0 +1,12 @@ +{ + "request": { + "method": "GET", + "path": "/api/v1/_capabilities" + }, + "response": { + "registry_backend": true, + "file_registry": true, + "version": "0.1" + } +} + diff --git a/docs/federation/fixtures/get-api-v1-files.demo-python.json b/docs/federation/fixtures/get-api-v1-files.demo-python.json new file mode 100644 index 00000000..373075c7 --- /dev/null +++ b/docs/federation/fixtures/get-api-v1-files.demo-python.json @@ -0,0 +1,13 @@ +{ + "request": { + "method": "GET", + "path": "/api/v1/files?path=demo.py&language=python" + }, + "response": { + "entity_id": "core:file:hash-demo@demo.py", + "content_hash": "hash-demo-file", + "canonical_path": "demo.py", + "language": "python" + } +} + diff --git a/docs/implementation/sprint-3/scope-amendment-2026-05.md b/docs/implementation/sprint-3/scope-amendment-2026-05.md new file mode 100644 index 00000000..a60bbb65 --- /dev/null +++ b/docs/implementation/sprint-3/scope-amendment-2026-05.md @@ -0,0 +1,219 @@ +# Sprint 3 — WP10 Scope Amendment + +**Status**: CLOSED — Clarion-side WP10 implementation complete. +**Date opened**: 2026-05-19 +**Author**: John Morrissey +**Predecessor**: [`../sprint-2/signoffs.md`](../sprint-2/signoffs.md) +**Cross-reference**: [`../../federation/filigree-side/ADR-014-registry-backend-and-file-identity-displacement.md`](../../federation/filigree-side/ADR-014-registry-backend-and-file-identity-displacement.md), [`../../federation/filigree-side/2026-05-19-registry-backend-cross-project-sequencing.md`](../../federation/filigree-side/2026-05-19-registry-backend-cross-project-sequencing.md) + +This memo serves three roles in one artifact: + +1. It lifts WP10 back into active Clarion scope as the Sprint 3 anchor. +2. It records the Clarion-side half of ADR-014's federation displacement now that + the Filigree-side counterpart and sequencing memo exist. +3. It turns the 2026-05-19 cross-project sequencing memo into Clarion tracker + work and records the implementation closeout for Clarion's side. + +--- + +## 1. Status of Sprint 2 Carry-Forward + +Sprint 2 is closed GREEN after the B.8 repair rerun. The close ladder certifies +the seven-tool MCP surface against the representative elspeth-slice and records +that B.3, B.4*, B.5*, B.6, B.7, and B.8 are closed for Sprint 2 accounting. + +The following Sprint 2 caveats carry forward into Sprint 3 planning: + +| Carry-forward item | Sprint 3 effect | +|---|---| +| B.4* mini-gate wall-clock extrapolation was materially optimistic. | Treat HTTP read API integration tests as real measurements, not projection-only evidence. | +| Sprint 2 certified the representative elspeth-slice, not full-repository elspeth. | WP10 exit criteria should name the exact corpus or fixture used for contract proof. | +| Non-umbrella Sprint 2 audit/follow-up issues remain open by design. | They are not WP10 blockers unless a WP10 step touches the affected surface. | + +No Sprint 2 umbrella issue is reopened by this amendment. + +--- + +## 2. Why WP10 Is the Sprint 3 Anchor + +Clarion ADR-014, accepted on 2026-04-18, made `registry_backend: clarion` the +design of record for displacing Filigree's native file IDs with Clarion file +entity IDs when the products are federated. Sprint 2 deliberately deferred WP10 +while it shipped the MCP consult surface and the ADR-029 entity-associations +binding. + +That deferral is no longer the right default. Filigree now has its counterpart +ADR and a cross-project sequencing memo: + +- `/home/john/filigree/docs/architecture/decisions/ADR-014-registry-backend-and-file-identity-displacement.md` +- `/home/john/filigree/docs/plans/2026-05-19-registry-backend-cross-project-sequencing.md` + +Mirrored read-only copies are in `docs/federation/filigree-side/` so Clarion's +repo carries the federation context. These are counterparts, not replacements: +Clarion ADR-014 remains Accepted as-is, and ADR-029's `entity_associations` +primitive stays intact. + +The live Clarion tree also confirms the missing implementation surface: + +- `crates/clarion-cli/src/serve.rs` serves MCP over stdio only. +- No `axum`, `warp`, or `hyper` server imports exist under `crates/`. +- No `resolve_file` implementation exists under `crates/`. +- `crates/clarion-mcp/src/filigree.rs` already has the blocking `reqwest` + client pattern used for Filigree's `entity_associations` reverse lookup. + +Sprint 3 therefore anchors on the HTTP read API that Filigree's +`ClarionRegistry` needs. + +--- + +## 3. Amended Sprint 3 Scope + +### Anchor + +| Box | Owning WP | Deliverable | Anchoring decisions | +|---|---|---|---| +| **C-WP10** | WP10 | Clarion HTTP read API for Filigree's `ClarionRegistry` | Clarion ADR-014; Filigree ADR-014; system-design §9, §11; detailed-design §7 | + +### Clarion-side breakdown + +| Step | Title | Scope | +|---|---|---| +| **C-WP10.1** | `axum` HTTP read server in `clarion-cli` (new module) | Add the server shell and configurable bind/auth posture without changing MCP tool behavior. | +| **C-WP10.2** | `GET /api/v1/files?path=&language=` endpoint | Resolve path/language to `{entity_id, content_hash, canonical_path, language}` from Clarion storage. | +| **C-WP10.3** | Contracts directory + JSON fixture for `GET /api/v1/files` | Publish the Clarion-side contract Filigree can dry-run against. | +| **C-WP10.4** | `GET /api/v1/_capabilities` endpoint | Expose `registry_backend: true` so Filigree's `ClarionRegistry` can fail fast. | +| **C-WP10.5** | Wire HTTP server into `clarion serve` alongside MCP stdio | Start the HTTP read server without regressing existing MCP-over-stdio operation. | + +The Filigree sequencing memo used C-WP10.5 for this scope-amendment memo itself. +This document satisfies that planning item; Clarion's local tracker uses +C-WP10.5 for the serve-wiring implementation step requested for Sprint 3. + +### Out of scope + +- Changes to Clarion ADR-014, ADR-029, or any other existing ADR. +- Any unwind of ADR-029 or the `entity_associations` table. +- Filigree repository mutations; Filigree-side files are read-only inputs here. +- Wardline-native emitter work. + +--- + +## 4. Cross-Project Dependency Direction + +The dependency is one-way and should stay explicit: + +| Filigree phase | Relationship to Clarion Sprint 3 | +|---|---| +| **Phase B** — `RegistryProtocol` interface, `LocalRegistry`, behavior-preserving call-site refactor, additive schema columns | Independent. It can land before Clarion exposes HTTP because it has no observable `clarion` mode yet. | +| **Phase C** — `registry_backend` flag, `ClarionRegistry`, capability probe, displaced-registration error, fail-closed behavior, migration verb | Blocks on **C-WP10.2** because the Clarion file-resolution endpoint is the first useful read contract. | +| **Phase D** — cross-process integration tests | Blocks on both Clarion C-WP10.2/C-WP10.4 and Filigree Phase C. | +| **Phase E** — docs and launch runbook | Lands after the contract and integration tests are stable. | + +The practical sequencing: Filigree Phase B may proceed in parallel; Clarion +should ship C-WP10.1 -> C-WP10.2 -> C-WP10.4 -> C-WP10.5, with C-WP10.3 hanging +off C-WP10.2 once the response shape is concrete. + +--- + +## 5. v0.1 Plan Resequencing + +`v0.1-plan.md` originally framed WP10 as Filigree-side plus Wardline-side +cross-product work and assumed the Filigree side would land from Filigree's own +roadmap. That assumption is obsolete as of 2026-05-19: the Filigree-side ADR and +sequencing memo now exist, and they require a Clarion HTTP read surface that is +not implemented today. + +This amendment narrows Sprint 3 to the Clarion HTTP read API needed by +Filigree's `ClarionRegistry`. The original WP10 Wardline/SARIF translator work +remains valuable but is not the Sprint 3 anchor unless explicitly added later. + +--- + +## 6. Tracker State to Create + +Create one planning milestone: + +- `Sprint 3 — WP10 Filigree federation read API (ADR-014)` + +Under it, create one phase: + +- `Clarion HTTP read API for Filigree's ClarionRegistry` + +Create the five C-WP10 steps in the order listed in §3, with dependencies: + +```text +C-WP10.1 -> C-WP10.2 -> C-WP10.4 -> C-WP10.5 +C-WP10.3 depends on C-WP10.2 +``` + +Add a milestone comment linking the Filigree tracker by title because the +Filigree-side ID is not known in this repo: + +```text +See /home/john/filigree's filigree tracker, milestone titled +"Registry-backend & file-identity displacement (ADR-014)". +``` + +--- + +## 7. What This Memo Does Not Change + +- Sprint 2 remains closed GREEN after repair. +- ADR-029 remains the shipped peer primitive for issue-to-entity binding. +- Clarion ADR-014 remains Accepted and is not edited by this amendment. +- Clarion's MCP-over-stdio serve path remains live alongside the new opt-in + HTTP read API. +- Filigree Phase B remains safe to land before Clarion; Filigree Phase C waits + for Clarion's file-resolution endpoint. + +--- + +## 8. References + +- [Clarion ADR-014](../../clarion/adr/ADR-014-filigree-registry-backend.md) +- [Clarion ADR-029](../../clarion/adr/ADR-029-entity-associations-binding.md) +- [Clarion v0.1 plan §WP10](../v0.1-plan.md#wp10--cross-product-filigree--and-wardline-side-changes) +- [Sprint 2 scope amendment](../sprint-2/scope-amendment-2026-05.md) +- [Sprint 2 signoffs](../sprint-2/signoffs.md) +- [Filigree ADR-014 mirror](../../federation/filigree-side/ADR-014-registry-backend-and-file-identity-displacement.md) +- [Filigree cross-project sequencing memo mirror](../../federation/filigree-side/2026-05-19-registry-backend-cross-project-sequencing.md) + +--- + +## 9. Implementation Closeout + +The Clarion-side WP10 implementation is complete in the local tracker: + +| Issue | Scope | Status | +|---|---|---| +| `clarion-44fbe093ca` | Sprint 3 WP10 milestone | `completed` | +| `clarion-f082fb6a49` | Clarion HTTP read API phase | `completed` | +| `clarion-e904d7a7ea` | C-WP10.1 HTTP read server module | `completed` | +| `clarion-aea2d917f9` | C-WP10.2 `GET /api/v1/files` | `completed` | +| `clarion-9d1379172f` | C-WP10.3 contract docs and fixtures | `completed` | +| `clarion-95643a7d5e` | C-WP10.4 `GET /api/v1/_capabilities` | `completed` | +| `clarion-2526c76071` | C-WP10.5 `clarion serve` HTTP wiring | `completed` | + +Implementation artifacts: + +- `crates/clarion-cli/src/http_read.rs` adds the `axum` HTTP read server. +- `crates/clarion-cli/src/serve.rs` starts the HTTP read server when + `serve.http.enabled` is true and shuts it down after MCP stdio exits. +- `crates/clarion-storage/src/query.rs` adds `resolve_file` for read-only file + identity resolution. +- `crates/clarion-mcp/src/config.rs` adds `serve.http.enabled` and + `serve.http.bind` config. +- `docs/federation/contracts.md` and `docs/federation/fixtures/*.json` pin the + Filigree-facing response shapes. + +Verification evidence: + +- `cargo fmt --all -- --check` +- `cargo clippy --workspace --all-targets --all-features -- -D warnings` +- `cargo build --workspace --bins` +- `cargo nextest run --workspace --all-features` — 405 passed, 2 skipped +- `RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps --all-features` +- `cargo deny check` — passed with duplicate/unmatched-license warnings only +- `bash tests/e2e/sprint_2_mcp_surface.sh` + +The Filigree Phase B/C dependency direction remains unchanged: Phase B can land +independently, while Phase C needs Clarion's `/api/v1/files` contract. diff --git a/docs/operator/secret-scanning.md b/docs/operator/secret-scanning.md index 63410505..d8a65fbe 100644 --- a/docs/operator/secret-scanning.md +++ b/docs/operator/secret-scanning.md @@ -65,10 +65,21 @@ where rule_id like 'CLA-SEC-%' or rule_id like 'CLA-INFRA-SECRET-%'; ``` +Currently blocked entities: + +```sql +select id, plugin_id, kind, name, + json_extract(properties, '$.briefing_blocked') as block_reason +from entities +where json_extract(properties, '$.briefing_blocked') is not null; +``` + Filigree integration for scanner findings is planned for v0.2. Until then, the local `findings` table is the authoritative WP5 audit surface. ## Limitations The scanner is pattern-based. It can miss novel internal key formats and it can flag high-entropy test data. Use a justified baseline for reviewed false positives, and prefer `--no-llm` or an air-gapped workflow for repos where any source disclosure would be unacceptable. +Contextual credential suppression only recognises shell/Python `#` comments in v0.1. It does not recognise `//` or `/* */` comments; use a justified baseline entry for reviewed non-Python test fixtures. + See [ADR-013](../clarion/adr/ADR-013-pre-ingest-secret-scanner.md) for design rationale. From f07d5361781c4b13ccde22c43335aa260c486ebc Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Tue, 19 May 2026 07:15:24 +1000 Subject: [PATCH 007/187] docs: consolidate implementation and planning history under docs/implementation/ Move scattered planning, review, and agent-handoff material into a single labeled archive under docs/implementation/ so the release-facing tree (docs/{suite,clarion/v0.1,clarion/adr,operator,federation}/) only surfaces canonical material: docs/clarion/v0.1/plans/ -> docs/implementation/v0.1-scope-plans/ docs/clarion/v0.1/reviews/ -> docs/implementation/v0.1-reviews/ docs/superpowers/handoffs/ -> docs/implementation/handoffs/ docs/superpowers/plans/ -> docs/implementation/agent-plans/ docs/arch-analysis-2026-05-18-1244/ -> docs/implementation/arch-analysis-2026-05-18-1244/ Update path references in retained docs (suite/, clarion/adr/*, clarion/v0.1/*.md, clarion/{README,adr/README}.md, docs/README.md) to point at the new locations. Fix internal cross-references in moved docs whose depth changed. Patch two pre-existing broken refs in agent-plans/ while in the area. Rewrite docs/implementation/README.md as the archive index. Update docs/clarion/v0.1/README.md, docs/clarion/README.md, and docs/README.md to describe the archive as non-normative supporting context. Refresh stale "Clarion: not yet built" facts in docs/suite/briefing.md to reflect the Sprint-1 walking-skeleton ship state (tag v0.1-sprint-1). Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/README.md | 2 +- docs/clarion/README.md | 2 +- .../adr/ADR-002-plugin-transport-json-rpc.md | 2 +- docs/clarion/adr/ADR-003-entity-id-scheme.md | 2 +- .../adr/ADR-004-finding-exchange-format.md | 4 +- docs/clarion/adr/ADR-007-summary-cache-key.md | 4 +- .../adr/ADR-011-writer-actor-concurrency.md | 4 +- docs/clarion/adr/ADR-012-http-auth-default.md | 6 +-- .../adr/ADR-013-pre-ingest-secret-scanner.md | 4 +- .../adr/ADR-014-filigree-registry-backend.md | 4 +- .../adr/ADR-015-wardline-filigree-emission.md | 6 +-- .../adr/ADR-016-observation-transport.md | 2 +- .../clarion/adr/ADR-017-severity-and-dedup.md | 4 +- .../adr/ADR-018-identity-reconciliation.md | 2 +- .../adr/ADR-021-plugin-authority-hybrid.md | 6 +-- .../adr/ADR-022-core-plugin-ontology.md | 2 +- docs/clarion/adr/ADR-023-tooling-baseline.md | 2 +- .../adr/ADR-024-guidance-schema-vocabulary.md | 4 +- ...-026-containment-wire-and-edge-identity.md | 2 +- .../adr/ADR-027-ontology-version-semver.md | 2 +- .../adr/ADR-031-schema-validation-policy.md | 2 +- docs/clarion/adr/README.md | 2 +- docs/clarion/v0.1/README.md | 5 +- docs/clarion/v0.1/detailed-design.md | 4 +- docs/clarion/v0.1/requirements.md | 4 +- docs/implementation/README.md | 47 ++++++++++--------- .../2026-04-18-wp1-scaffold-storage.md | 4 +- .../2026-04-18-wp2-plugin-host.md | 10 ++-- .../2026-05-05-b2-class-module-entities.md | 0 .../2026-05-18-phase3-subsystems.md | 0 .../2026-05-18-phase3-subsystems.review.json | 0 .../00-coordination.md | 0 .../01-discovery-findings.md | 0 .../02-subsystem-catalog.md | 0 .../03-diagrams.md | 0 .../04-final-report.md | 0 .../temp/section-clarion-cli.md | 0 .../temp/section-clarion-core.md | 0 .../temp/section-clarion-mcp.md | 0 .../temp/section-clarion-storage.md | 0 .../temp/section-fixture-plugin.md | 0 .../temp/section-python-plugin.md | 0 .../temp/validation-02-subsystem-catalog.md | 0 .../temp/validation-03-diagrams.md | 0 .../temp/validation-04-final-report.md | 0 .../2026-04-18-wp2-plugin-host-handoff.md | 0 .../2026-04-19-wp2-tasks-4-to-9-handoff.md | 0 .../2026-04-23-wp2-full-scrub-handoff.md | 0 .../handoffs/2026-04-23-wp2-scrub-findings.md | 0 .../2026-04-24-post-wp2-scrub-handoff.md | 0 .../handoffs/2026-04-30-sprint-2-kickoff.md | 0 .../handoffs/2026-05-03-skeleton-audit.md | 0 .../handoffs/2026-05-16-sprint-2-resume.md | 0 .../2026-05-18-phase3-subsystems-handoff.md | 8 ++-- docs/implementation/sprint-1/README.md | 2 +- docs/implementation/sprint-1/signoffs.md | 2 +- .../sprint-2/b2-class-module-entities.md | 2 +- .../sprint-2/b3-contains-edges.md | 2 +- .../sprint-2/scope-amendment-2026-05.md | 6 +-- docs/implementation/v0.1-plan.md | 6 +-- .../thread-1-pre-publish-blockers.md | 4 +- .../v0.1-publish/ws-a-secret-scanner.md | 25 ++++++---- .../v0.1-reviews}/README.md | 0 .../00-executive-synthesis.md | 0 .../panel-2026-04-17/04-self-sufficiency.md | 0 .../panel-2026-04-17/09-threat-model.md | 0 .../11-doctrine-panel-synthesis.md | 0 .../pre-restructure/design-review.md | 0 .../pre-restructure/integration-recon.md | 0 .../v0.1-scope-commitments.md | 0 docs/suite/briefing.md | 8 ++-- docs/suite/glossary.md | 4 +- 72 files changed, 110 insertions(+), 103 deletions(-) rename docs/{superpowers/plans => implementation/agent-plans}/2026-04-18-wp1-scaffold-storage.md (99%) rename docs/{superpowers/plans => implementation/agent-plans}/2026-04-18-wp2-plugin-host.md (99%) rename docs/{superpowers/plans => implementation/agent-plans}/2026-05-05-b2-class-module-entities.md (100%) rename docs/{superpowers/plans => implementation/agent-plans}/2026-05-18-phase3-subsystems.md (100%) rename docs/{superpowers/plans => implementation/agent-plans}/2026-05-18-phase3-subsystems.review.json (100%) rename docs/{ => implementation}/arch-analysis-2026-05-18-1244/00-coordination.md (100%) rename docs/{ => implementation}/arch-analysis-2026-05-18-1244/01-discovery-findings.md (100%) rename docs/{ => implementation}/arch-analysis-2026-05-18-1244/02-subsystem-catalog.md (100%) rename docs/{ => implementation}/arch-analysis-2026-05-18-1244/03-diagrams.md (100%) rename docs/{ => implementation}/arch-analysis-2026-05-18-1244/04-final-report.md (100%) rename docs/{ => implementation}/arch-analysis-2026-05-18-1244/temp/section-clarion-cli.md (100%) rename docs/{ => implementation}/arch-analysis-2026-05-18-1244/temp/section-clarion-core.md (100%) rename docs/{ => implementation}/arch-analysis-2026-05-18-1244/temp/section-clarion-mcp.md (100%) rename docs/{ => implementation}/arch-analysis-2026-05-18-1244/temp/section-clarion-storage.md (100%) rename docs/{ => implementation}/arch-analysis-2026-05-18-1244/temp/section-fixture-plugin.md (100%) rename docs/{ => implementation}/arch-analysis-2026-05-18-1244/temp/section-python-plugin.md (100%) rename docs/{ => implementation}/arch-analysis-2026-05-18-1244/temp/validation-02-subsystem-catalog.md (100%) rename docs/{ => implementation}/arch-analysis-2026-05-18-1244/temp/validation-03-diagrams.md (100%) rename docs/{ => implementation}/arch-analysis-2026-05-18-1244/temp/validation-04-final-report.md (100%) rename docs/{superpowers => implementation}/handoffs/2026-04-18-wp2-plugin-host-handoff.md (100%) rename docs/{superpowers => implementation}/handoffs/2026-04-19-wp2-tasks-4-to-9-handoff.md (100%) rename docs/{superpowers => implementation}/handoffs/2026-04-23-wp2-full-scrub-handoff.md (100%) rename docs/{superpowers => implementation}/handoffs/2026-04-23-wp2-scrub-findings.md (100%) rename docs/{superpowers => implementation}/handoffs/2026-04-24-post-wp2-scrub-handoff.md (100%) rename docs/{superpowers => implementation}/handoffs/2026-04-30-sprint-2-kickoff.md (100%) rename docs/{superpowers => implementation}/handoffs/2026-05-03-skeleton-audit.md (100%) rename docs/{superpowers => implementation}/handoffs/2026-05-16-sprint-2-resume.md (100%) rename docs/{superpowers => implementation}/handoffs/2026-05-18-phase3-subsystems-handoff.md (96%) rename docs/{clarion/v0.1/reviews => implementation/v0.1-reviews}/README.md (100%) rename docs/{clarion/v0.1/reviews => implementation/v0.1-reviews}/panel-2026-04-17/00-executive-synthesis.md (100%) rename docs/{clarion/v0.1/reviews => implementation/v0.1-reviews}/panel-2026-04-17/04-self-sufficiency.md (100%) rename docs/{clarion/v0.1/reviews => implementation/v0.1-reviews}/panel-2026-04-17/09-threat-model.md (100%) rename docs/{clarion/v0.1/reviews => implementation/v0.1-reviews}/panel-2026-04-17/11-doctrine-panel-synthesis.md (100%) rename docs/{clarion/v0.1/reviews => implementation/v0.1-reviews}/pre-restructure/design-review.md (100%) rename docs/{clarion/v0.1/reviews => implementation/v0.1-reviews}/pre-restructure/integration-recon.md (100%) rename docs/{clarion/v0.1/plans => implementation/v0.1-scope-plans}/v0.1-scope-commitments.md (100%) diff --git a/docs/README.md b/docs/README.md index a7094144..df3744a4 100644 --- a/docs/README.md +++ b/docs/README.md @@ -17,5 +17,5 @@ This `docs/` tree is organized by reader intent: - Canonical suite docs live in [suite/](./suite/README.md). - Canonical Clarion v0.1 design docs live in [clarion/v0.1/](./clarion/v0.1/README.md). -- Supporting reviews and planning history live alongside the relevant version under `reviews/` and `plans/`. - Architecture decisions live in [clarion/adr/](./clarion/adr/README.md). +- Supporting reviews, scope memos, sprint plans, and agent handoffs are archived under [implementation/](./implementation/README.md) — non-normative. diff --git a/docs/clarion/README.md b/docs/clarion/README.md index 50a347d3..32f0352a 100644 --- a/docs/clarion/README.md +++ b/docs/clarion/README.md @@ -15,4 +15,4 @@ This folder holds Clarion product documentation. ## Canonical vs supporting - Canonical design docs for the current product version live under `v0.1/`. -- Reviews and planning history live under the matching version in `reviews/` and `plans/`. +- Reviews and planning history are archived under [../implementation/](../implementation/) — non-normative supporting context. diff --git a/docs/clarion/adr/ADR-002-plugin-transport-json-rpc.md b/docs/clarion/adr/ADR-002-plugin-transport-json-rpc.md index 9e9cc97e..0ece6a13 100644 --- a/docs/clarion/adr/ADR-002-plugin-transport-json-rpc.md +++ b/docs/clarion/adr/ADR-002-plugin-transport-json-rpc.md @@ -102,4 +102,4 @@ We will use subprocess-based JSON-RPC 2.0 with explicit Content-Length framing. - [Clarion v0.1 system design](../v0.1/system-design.md) - [Clarion v0.1 detailed design](../v0.1/detailed-design.md) -- [Clarion v0.1 design review](../v0.1/reviews/pre-restructure/design-review.md) +- [Clarion v0.1 design review](../../implementation/v0.1-reviews/pre-restructure/design-review.md) diff --git a/docs/clarion/adr/ADR-003-entity-id-scheme.md b/docs/clarion/adr/ADR-003-entity-id-scheme.md index 0b6f363c..a2d1babc 100644 --- a/docs/clarion/adr/ADR-003-entity-id-scheme.md +++ b/docs/clarion/adr/ADR-003-entity-id-scheme.md @@ -97,4 +97,4 @@ For later versions: - [Clarion v0.1 system design](../v0.1/system-design.md) - [Clarion v0.1 detailed design](../v0.1/detailed-design.md) -- [Clarion v0.1 design review](../v0.1/reviews/pre-restructure/design-review.md) +- [Clarion v0.1 design review](../../implementation/v0.1-reviews/pre-restructure/design-review.md) diff --git a/docs/clarion/adr/ADR-004-finding-exchange-format.md b/docs/clarion/adr/ADR-004-finding-exchange-format.md index 9bc6512c..e06d0081 100644 --- a/docs/clarion/adr/ADR-004-finding-exchange-format.md +++ b/docs/clarion/adr/ADR-004-finding-exchange-format.md @@ -92,5 +92,5 @@ Specifically: - [Clarion v0.1 system design](../v0.1/system-design.md) - [Clarion v0.1 detailed design](../v0.1/detailed-design.md) -- [Clarion v0.1 design review](../v0.1/reviews/pre-restructure/design-review.md) -- [Clarion v0.1 integration recon](../v0.1/reviews/pre-restructure/integration-recon.md) +- [Clarion v0.1 design review](../../implementation/v0.1-reviews/pre-restructure/design-review.md) +- [Clarion v0.1 integration recon](../../implementation/v0.1-reviews/pre-restructure/integration-recon.md) diff --git a/docs/clarion/adr/ADR-007-summary-cache-key.md b/docs/clarion/adr/ADR-007-summary-cache-key.md index ece5ab52..8b316caf 100644 --- a/docs/clarion/adr/ADR-007-summary-cache-key.md +++ b/docs/clarion/adr/ADR-007-summary-cache-key.md @@ -165,5 +165,5 @@ Store `"sonnet"` instead of `"claude-sonnet-4-6"`. - [Clarion v0.1 detailed design §4 (Summary cache key design)](../v0.1/detailed-design.md) (lines 965-983) — the target this ADR formalises. - [Clarion v0.1 detailed design §3 schema — `summary_cache` table](../v0.1/detailed-design.md) (lines 679-691) — storage shape. - [Clarion v0.1 requirements — NFR-COST-01, NFR-COST-02, NFR-PERF-01](../v0.1/requirements.md) — cost/performance envelopes this cache design serves. -- [Clarion v0.1 scope commitments — Q1](../v0.1/plans/v0.1-scope-commitments.md) (line 68) — v0.2 deferrals: cache optimisations beyond the simple shape. -- [Clarion v0.1 scope commitments — Validation](../v0.1/plans/v0.1-scope-commitments.md) (lines 237-247) — empirical validation plan for NFR-COST-01/02 and the 95% hit-rate assumption this ADR depends on. +- [Clarion v0.1 scope commitments — Q1](../../implementation/v0.1-scope-plans/v0.1-scope-commitments.md) (line 68) — v0.2 deferrals: cache optimisations beyond the simple shape. +- [Clarion v0.1 scope commitments — Validation](../../implementation/v0.1-scope-plans/v0.1-scope-commitments.md) (lines 237-247) — empirical validation plan for NFR-COST-01/02 and the 95% hit-rate assumption this ADR depends on. diff --git a/docs/clarion/adr/ADR-011-writer-actor-concurrency.md b/docs/clarion/adr/ADR-011-writer-actor-concurrency.md index 6657f689..4acc469c 100644 --- a/docs/clarion/adr/ADR-011-writer-actor-concurrency.md +++ b/docs/clarion/adr/ADR-011-writer-actor-concurrency.md @@ -173,8 +173,8 @@ Already rejected in ADR-001 for the storage-engine selection. Re-cited here beca ## References -- [Clarion v0.1 design review §2.2](../v0.1/reviews/pre-restructure/design-review.md) (lines 56-66) — original CRITICAL flag; writer-actor and shadow-DB options. +- [Clarion v0.1 design review §2.2](../../implementation/v0.1-reviews/pre-restructure/design-review.md) (lines 56-66) — original CRITICAL flag; writer-actor and shadow-DB options. - [Clarion v0.1 detailed design §3 (Concurrency)](../v0.1/detailed-design.md) (lines 758-769) — the implementation detail this ADR formalises. - [Clarion v0.1 requirements §NFR-RELIABILITY-02](../v0.1/requirements.md) (line 857) — WAL + writer-actor + checkpoint discipline as crash-safety requirement. - [Clarion v0.1 system design §4 Storage](../v0.1/system-design.md) — SQLite rationale; WAL mode claim. -- [Clarion v0.1 scope commitments — ADR sprint + validation](../v0.1/plans/v0.1-scope-commitments.md) (lines 181-195, 249-251) — P0 promotion from P1 and the explicit follow-up validation task. +- [Clarion v0.1 scope commitments — ADR sprint + validation](../../implementation/v0.1-scope-plans/v0.1-scope-commitments.md) (lines 181-195, 249-251) — P0 promotion from P1 and the explicit follow-up validation task. diff --git a/docs/clarion/adr/ADR-012-http-auth-default.md b/docs/clarion/adr/ADR-012-http-auth-default.md index dda4814d..a25bd335 100644 --- a/docs/clarion/adr/ADR-012-http-auth-default.md +++ b/docs/clarion/adr/ADR-012-http-auth-default.md @@ -153,8 +153,8 @@ v0.1 tokens carry scope claims — read-only catalog, read-only findings, submit ## References -- [Clarion v0.1 panel threat model T-02](../v0.1/reviews/panel-2026-04-17/09-threat-model.md) (line 233) — original risk scoring. -- [Panel threat model §12 recommendation 1](../v0.1/reviews/panel-2026-04-17/09-threat-model.md) (line 298) — the specific "authenticated or not listening" prescription. +- [Clarion v0.1 panel threat model T-02](../../implementation/v0.1-reviews/panel-2026-04-17/09-threat-model.md) (line 233) — original risk scoring. +- [Panel threat model §12 recommendation 1](../../implementation/v0.1-reviews/panel-2026-04-17/09-threat-model.md) (line 298) — the specific "authenticated or not listening" prescription. - [Clarion v0.1 system design §10 "Loopback is not a security boundary"](../v0.1/system-design.md) — the paragraph this ADR flips; updated in the same commit. - [Clarion v0.1 detailed design §7 Token auth — full spec](../v0.1/detailed-design.md) (lines 1286-1304) — the token machinery this ADR reuses as the fallback. -- [Clarion v0.1 scope commitments — action 5](../v0.1/plans/v0.1-scope-commitments.md) (line 199) — the commitment mandate. +- [Clarion v0.1 scope commitments — action 5](../../implementation/v0.1-scope-plans/v0.1-scope-commitments.md) (line 199) — the commitment mandate. diff --git a/docs/clarion/adr/ADR-013-pre-ingest-secret-scanner.md b/docs/clarion/adr/ADR-013-pre-ingest-secret-scanner.md index f1aa7119..1fffaf0e 100644 --- a/docs/clarion/adr/ADR-013-pre-ingest-secret-scanner.md +++ b/docs/clarion/adr/ADR-013-pre-ingest-secret-scanner.md @@ -189,8 +189,8 @@ Rely on operator discipline — "don't commit `.env` files"; add documentation. ## References -- [Clarion v0.1 design review §3 (Secret exfiltration)](../v0.1/reviews/pre-restructure/design-review.md) (lines 88-91) — the CRITICAL flag this ADR retires. -- [Clarion v0.1 panel threat model T-10](../v0.1/reviews/panel-2026-04-17/09-threat-model.md) (line 241) — risk scoring and residual-risk framing. +- [Clarion v0.1 design review §3 (Secret exfiltration)](../../implementation/v0.1-reviews/pre-restructure/design-review.md) (lines 88-91) — the CRITICAL flag this ADR retires. +- [Clarion v0.1 panel threat model T-10](../../implementation/v0.1-reviews/panel-2026-04-17/09-threat-model.md) (line 241) — risk scoring and residual-risk framing. - [Clarion v0.1 system design §10 (Pre-ingest redaction)](../v0.1/system-design.md) (lines 1044-1056) — the behaviour this ADR formalises. - [Clarion v0.1 requirements — NFR-SEC-01, NFR-SEC-05](../v0.1/requirements.md) — requirement floor. - [detect-secrets baseline format](https://github.com/Yelp/detect-secrets/blob/master/README.md#baseline-file) — the format this ADR's baseline layout matches. diff --git a/docs/clarion/adr/ADR-014-filigree-registry-backend.md b/docs/clarion/adr/ADR-014-filigree-registry-backend.md index ff3ba7ee..071e63cd 100644 --- a/docs/clarion/adr/ADR-014-filigree-registry-backend.md +++ b/docs/clarion/adr/ADR-014-filigree-registry-backend.md @@ -115,6 +115,6 @@ Accept that Filigree mints its own file IDs forever; Clarion reconciles post-hoc ## References - [Clarion v0.1 system design §9](../v0.1/system-design.md) — integration posture; capability probe; degraded modes. -- [Integration reconnaissance §2.1](../v0.1/reviews/pre-restructure/integration-recon.md) — `file_records` schema; four NOT-NULL foreign keys; three auto-create paths; verified absence of `registry_backend` and error code. +- [Integration reconnaissance §2.1](../../implementation/v0.1-reviews/pre-restructure/integration-recon.md) — `file_records` schema; four NOT-NULL foreign keys; three auto-create paths; verified absence of `registry_backend` and error code. - [Loom doctrine §4, §5, §6](../../suite/loom.md) — pairwise composability; enrichment failure test; no-shared-store rule. -- [Clarion v0.1 scope commitments](../v0.1/plans/v0.1-scope-commitments.md) — Q2 commits `registry_backend` to v0.1 as within-scope Filigree work. +- [Clarion v0.1 scope commitments](../../implementation/v0.1-scope-plans/v0.1-scope-commitments.md) — Q2 commits `registry_backend` to v0.1 as within-scope Filigree work. diff --git a/docs/clarion/adr/ADR-015-wardline-filigree-emission.md b/docs/clarion/adr/ADR-015-wardline-filigree-emission.md index dd97f6ff..b24a9532 100644 --- a/docs/clarion/adr/ADR-015-wardline-filigree-emission.md +++ b/docs/clarion/adr/ADR-015-wardline-filigree-emission.md @@ -167,6 +167,6 @@ A `loom-findings` package that both products import; the library owns emission t - [Loom doctrine §5 (v0.1 asterisks, asterisk 1)](../../suite/loom.md) — pipeline-coupling asterisk for (Wardline, Filigree) with this ADR as the retirement condition. - [Clarion v0.1 detailed design §9 (Wardline prerequisites) item 3](../v0.1/detailed-design.md) (line 1361) — Option A / Option B enumeration. -- [Clarion v0.1 scope commitments](../v0.1/plans/v0.1-scope-commitments.md) (lines 72, 190, 202) — default deferral; spike promotion condition. -- [Clarion v0.1 integration reconnaissance §4.3](../v0.1/reviews/pre-restructure/integration-recon.md) (line 339) — "Wardline has zero HTTP client code"; empirical basis for the v0.2 deferral default. -- [Panel doctrine synthesis](../v0.1/reviews/panel-2026-04-17/11-doctrine-panel-synthesis.md) — SARIF triangle framing; asterisk requirement. +- [Clarion v0.1 scope commitments](../../implementation/v0.1-scope-plans/v0.1-scope-commitments.md) (lines 72, 190, 202) — default deferral; spike promotion condition. +- [Clarion v0.1 integration reconnaissance §4.3](../../implementation/v0.1-reviews/pre-restructure/integration-recon.md) (line 339) — "Wardline has zero HTTP client code"; empirical basis for the v0.2 deferral default. +- [Panel doctrine synthesis](../../implementation/v0.1-reviews/panel-2026-04-17/11-doctrine-panel-synthesis.md) — SARIF triangle framing; asterisk requirement. diff --git a/docs/clarion/adr/ADR-016-observation-transport.md b/docs/clarion/adr/ADR-016-observation-transport.md index 378111fa..1fa45d28 100644 --- a/docs/clarion/adr/ADR-016-observation-transport.md +++ b/docs/clarion/adr/ADR-016-observation-transport.md @@ -122,7 +122,7 @@ v0.1 does not emit observations. Observation generators (LLM-proposed guidance, ## References -- [Clarion v0.1 scope commitments — Q1](../v0.1/plans/v0.1-scope-commitments.md) — observation HTTP transport explicitly deferred to v0.2. +- [Clarion v0.1 scope commitments — Q1](../../implementation/v0.1-scope-plans/v0.1-scope-commitments.md) — observation HTTP transport explicitly deferred to v0.2. - [Clarion v0.1 system design §9 (Observation transport)](../v0.1/system-design.md) (lines 916-921) — the passage reversed by this ADR; updated in the same commit. - [Clarion v0.1 system design §11 (Capability negotiation)](../v0.1/system-design.md) (lines 1122-1141) — observation HTTP presence moves from v0.1 fallback trigger to v0.2 feature-flag detection. - [Clarion v0.1 detailed design §9.1 Filigree prerequisites](../v0.1/detailed-design.md) (lines 1333-1351) — `POST /api/v1/observations` moved from "Required for v0.1 ship" to "Nice-to-have (v0.2+)". diff --git a/docs/clarion/adr/ADR-017-severity-and-dedup.md b/docs/clarion/adr/ADR-017-severity-and-dedup.md index a558a167..13f785b5 100644 --- a/docs/clarion/adr/ADR-017-severity-and-dedup.md +++ b/docs/clarion/adr/ADR-017-severity-and-dedup.md @@ -161,6 +161,6 @@ Clarion's internal records store `{critical, high, medium, low, info}` directly. ## References - [Clarion v0.1 detailed design §7](../v0.1/detailed-design.md) (lines 1163-1200) — the canonical mapping tables; this ADR formalises what's already there and fixes Issue 7. -- [Clarion v0.1 integration reconnaissance §2.3, §2.4](../v0.1/reviews/pre-restructure/integration-recon.md) — empirical evidence for severity vocabulary and dedup key on Filigree's side (`db_schema.py` line numbers). -- [Panel self-sufficiency review — Issue 7](../v0.1/reviews/panel-2026-04-17/04-self-sufficiency.md) (lines 132-134) — rule-ID namespace inconsistency this ADR resolves. +- [Clarion v0.1 integration reconnaissance §2.3, §2.4](../../implementation/v0.1-reviews/pre-restructure/integration-recon.md) — empirical evidence for severity vocabulary and dedup key on Filigree's side (`db_schema.py` line numbers). +- [Panel self-sufficiency review — Issue 7](../../implementation/v0.1-reviews/panel-2026-04-17/04-self-sufficiency.md) (lines 132-134) — rule-ID namespace inconsistency this ADR resolves. - [Clarion v0.1 requirements — REQ-FINDING-02, NG-21](../v0.1/requirements.md) — rule-ID namespace rule; v0.2 server-side per-entity dedup deferral. diff --git a/docs/clarion/adr/ADR-018-identity-reconciliation.md b/docs/clarion/adr/ADR-018-identity-reconciliation.md index aa6cb288..6eae8603 100644 --- a/docs/clarion/adr/ADR-018-identity-reconciliation.md +++ b/docs/clarion/adr/ADR-018-identity-reconciliation.md @@ -169,4 +169,4 @@ Wardline exports its REGISTRY as a declarative descriptor file (`wardline_regist - [Clarion v0.1 system design §2 (Direct REGISTRY import), §9 (state-file ingest), §9 (Entity resolve oracle)](../v0.1/system-design.md) — import pattern, ingest paths, HTTP oracle. - [Clarion v0.1 detailed design §2 (Identity reconciliation across the suite)](../v0.1/detailed-design.md) (lines 553-571) — three-scheme translation table; ingest-path rules. - [Loom doctrine §5 (v0.1 asterisks), §6 (What Loom is NOT)](../../suite/loom.md) — initialization-coupling asterisk; "no identity reconciliation service" categorical. -- [Panel doctrine synthesis](../v0.1/reviews/panel-2026-04-17/11-doctrine-panel-synthesis.md) — the asterisks framing originated here. +- [Panel doctrine synthesis](../../implementation/v0.1-reviews/panel-2026-04-17/11-doctrine-panel-synthesis.md) — the asterisks framing originated here. diff --git a/docs/clarion/adr/ADR-021-plugin-authority-hybrid.md b/docs/clarion/adr/ADR-021-plugin-authority-hybrid.md index e0d2642b..cb9c88a7 100644 --- a/docs/clarion/adr/ADR-021-plugin-authority-hybrid.md +++ b/docs/clarion/adr/ADR-021-plugin-authority-hybrid.md @@ -143,9 +143,9 @@ Use cgroup v2 (`systemd-run --user --scope` or direct cgroup mounts) for per-plu ## References -- [Panel threat-model review §7 (risk matrix)](../v0.1/reviews/panel-2026-04-17/09-threat-model.md) — T-01, T-08, T-11, T-12 scorings. -- [Panel threat-model review §8, §12](../v0.1/reviews/panel-2026-04-17/09-threat-model.md) — missing controls (1); three non-negotiable v0.1 controls (2). +- [Panel threat-model review §7 (risk matrix)](../../implementation/v0.1-reviews/panel-2026-04-17/09-threat-model.md) — T-01, T-08, T-11, T-12 scorings. +- [Panel threat-model review §8, §12](../../implementation/v0.1-reviews/panel-2026-04-17/09-threat-model.md) — missing controls (1); three non-negotiable v0.1 controls (2). - [Clarion v0.1 system design §10](../v0.1/system-design.md) — threat model summary table; `Defences NOT in v0.1` (seccomp/landlock deferred). - [Clarion v0.1 detailed design §1.3](../v0.1/detailed-design.md) (lines 56-145) — plugin manifest shape; crash-loop circuit breaker. -- [Clarion v0.1 scope commitments — Q3](../v0.1/plans/v0.1-scope-commitments.md) — committed decision to hybrid (declared + enforced) over full sandbox. +- [Clarion v0.1 scope commitments — Q3](../../implementation/v0.1-scope-plans/v0.1-scope-commitments.md) — committed decision to hybrid (declared + enforced) over full sandbox. - [NG-16](../v0.1/requirements.md) — plugin hash-pinning deferred to v0.2; companion v0.2 control for T-01/T-15. diff --git a/docs/clarion/adr/ADR-022-core-plugin-ontology.md b/docs/clarion/adr/ADR-022-core-plugin-ontology.md index f374aee3..e9d6a37e 100644 --- a/docs/clarion/adr/ADR-022-core-plugin-ontology.md +++ b/docs/clarion/adr/ADR-022-core-plugin-ontology.md @@ -129,4 +129,4 @@ Every kind is plugin-owned, including `file`, `subsystem`, `guidance`. Plugins c - [Clarion v0.1 system design §2](../v0.1/system-design.md) (lines 120-234) — core/plugin responsibility split; plugin manifest contract. - [Clarion v0.1 detailed design §1, §2](../v0.1/detailed-design.md) (lines 56-136, 187-309) — manifest shape; Entity/Edge/Finding structs; core-reserved edge kinds at line 261; core-minted `file`/`subsystem`/`guidance` IDs at lines 228-230. - [Loom doctrine §5](../../suite/loom.md) — centralisation-drift failure test; a closed core-defined kind enum qualifies. -- [Panel synthesis — self-sufficiency review](../v0.1/reviews/panel-2026-04-17/04-self-sufficiency.md) — Issue 7 (rule-ID inconsistency) is the empirical case this ADR's namespace rule resolves. +- [Panel synthesis — self-sufficiency review](../../implementation/v0.1-reviews/panel-2026-04-17/04-self-sufficiency.md) — Issue 7 (rule-ID inconsistency) is the empirical case this ADR's namespace rule resolves. diff --git a/docs/clarion/adr/ADR-023-tooling-baseline.md b/docs/clarion/adr/ADR-023-tooling-baseline.md index 3793b95f..c677e138 100644 --- a/docs/clarion/adr/ADR-023-tooling-baseline.md +++ b/docs/clarion/adr/ADR-023-tooling-baseline.md @@ -305,7 +305,7 @@ generates less value than the discipline floor it provides. contract. Plugins authored in Python (WP3) or later in other languages can adopt tooling baselines appropriate to their ecosystem; the Rust host's baseline is defined here. -- Scope-commitment memo [`../v0.1/plans/v0.1-scope-commitments.md`](../v0.1/plans/v0.1-scope-commitments.md) +- Scope-commitment memo [`../v0.1/plans/v0.1-scope-commitments.md`](../../implementation/v0.1-scope-plans/v0.1-scope-commitments.md) — "enterprise rigor at lack of scale" is the phrase this ADR operationalises at the tooling layer. diff --git a/docs/clarion/adr/ADR-024-guidance-schema-vocabulary.md b/docs/clarion/adr/ADR-024-guidance-schema-vocabulary.md index 9c4398d5..e847c6bf 100644 --- a/docs/clarion/adr/ADR-024-guidance-schema-vocabulary.md +++ b/docs/clarion/adr/ADR-024-guidance-schema-vocabulary.md @@ -13,7 +13,7 @@ Three guidance-schema fields and one finding field are renamed: `entity.properti The Loom federation axiom (`docs/suite/loom.md` §3–§5) requires solo-useful, pairwise-composable, enrich-only products. None of the three failure modes (semantic / initialization / pipeline coupling) directly applies to vocabulary, but cross-product readability is the *prerequisite* for honest pairwise composition. When a Loom user reads Clarion's design and Filigree's CLI in the same session and `priority` means different things in each, every cross-product debugging pass starts from a misframe. The bug at `clarion-4cd11905e2` is the concrete proof: the issue's filer assumed Filigree's meaning, the audit pass had to reframe before any fix could land. -Three mismatches are documented in the [skeleton audit](../../superpowers/handoffs/2026-05-03-skeleton-audit.md): +Three mismatches are documented in the [skeleton audit](../../implementation/handoffs/2026-05-03-skeleton-audit.md): 1. **`priority`** — Clarion's guidance composition rank `project | subsystem | package | module | class | function` (`detailed-design.md:453`, `system-design.md:346`) collides with Filigree's `priority` label vocabulary @@ -168,7 +168,7 @@ Add the audit's findings to the glossary as `open` clashes; defer the schema cor ## References -- [Skeleton audit](../../superpowers/handoffs/2026-05-03-skeleton-audit.md) — durable record of the audit pass, reviewer feedback (architecture-critic + leverage-analyst), and the reconciled plan that produced this ADR. +- [Skeleton audit](../../implementation/handoffs/2026-05-03-skeleton-audit.md) — durable record of the audit pass, reviewer feedback (architecture-critic + leverage-analyst), and the reconciled plan that produced this ADR. - [Loom suite glossary](../../suite/glossary.md) — the federation-safe design-review artefact this ADR moves three entries from `open` to `managed` within. - [Loom federation axiom](../../suite/loom.md) §3–§5 — the doctrine the glossary defends; the failure-test mode this ADR addresses is reader-side cross-product disambiguation cost. - `crates/clarion-storage/migrations/0001_initial_schema.sql:163-191` — the schema sites this ADR edits in place. diff --git a/docs/clarion/adr/ADR-026-containment-wire-and-edge-identity.md b/docs/clarion/adr/ADR-026-containment-wire-and-edge-identity.md index dc63c244..e45993c4 100644 --- a/docs/clarion/adr/ADR-026-containment-wire-and-edge-identity.md +++ b/docs/clarion/adr/ADR-026-containment-wire-and-edge-identity.md @@ -192,6 +192,6 @@ Instead of typed `source_byte_start/end` columns, edges carry a `properties: { s ## References - [B.3 design doc](../../implementation/sprint-2/b3-contains-edges.md) — wire shape, emission policy, parent_id provenance, manifest bump, test plan. -- [Sprint-2 kickoff handoff §"Edge wire shape"](../../superpowers/handoffs/2026-04-30-sprint-2-kickoff.md) — the deferral this ADR resolves. +- [Sprint-2 kickoff handoff §"Edge wire shape"](../../implementation/handoffs/2026-04-30-sprint-2-kickoff.md) — the deferral this ADR resolves. - [Sprint-1 walking-skeleton e2e](../../../tests/e2e/sprint_1_walking_skeleton.sh) — extends to assert at least one edge row post-B.3. - Five-reviewer panel transcripts (in B.3 brainstorming conversation log on `sprint-2/b3-design` branch). diff --git a/docs/clarion/adr/ADR-027-ontology-version-semver.md b/docs/clarion/adr/ADR-027-ontology-version-semver.md index 9aebf3cb..fd4a4efe 100644 --- a/docs/clarion/adr/ADR-027-ontology-version-semver.md +++ b/docs/clarion/adr/ADR-027-ontology-version-semver.md @@ -140,6 +140,6 @@ Document that the field exists; let each plugin pick its own version semantics. - [B.2 design doc](../../implementation/sprint-2/b2-class-module-entities.md) §5 — first ontology_version bump cite. - [B.3 design doc](../../implementation/sprint-2/b3-contains-edges.md) §7 — second ontology_version bump cite. -- [Sprint-2 kickoff handoff](../../superpowers/handoffs/2026-04-30-sprint-2-kickoff.md) §"`ontology_version` bump policy" — the original ambiguous formulation that motivated this ADR. +- [Sprint-2 kickoff handoff](../../implementation/handoffs/2026-04-30-sprint-2-kickoff.md) §"`ontology_version` bump policy" — the original ambiguous formulation that motivated this ADR. - [Filigree clarion-8befae708b](.) — CI lint guard for plugin.toml ↔ server.py ONTOLOGY_VERSION drift (P3 follow-up; not blocking). - [Semver 2.0.0](https://semver.org) — the broader convention this ADR adopts. diff --git a/docs/clarion/adr/ADR-031-schema-validation-policy.md b/docs/clarion/adr/ADR-031-schema-validation-policy.md index b742c57a..f278c954 100644 --- a/docs/clarion/adr/ADR-031-schema-validation-policy.md +++ b/docs/clarion/adr/ADR-031-schema-validation-policy.md @@ -405,7 +405,7 @@ permanent. ## References -- [Skeleton audit](../../superpowers/handoffs/2026-05-03-skeleton-audit.md) +- [Skeleton audit](../../implementation/handoffs/2026-05-03-skeleton-audit.md) — F-13 in the Reviewer additions table; the audit framing that motivated this ADR. - `crates/clarion-storage/migrations/0001_initial_schema.sql:80-81, 134` diff --git a/docs/clarion/adr/README.md b/docs/clarion/adr/README.md index f0390d5f..d059921e 100644 --- a/docs/clarion/adr/README.md +++ b/docs/clarion/adr/README.md @@ -48,7 +48,7 @@ The following decisions are still backlog items rather than authored ADR files. ## Pre-implementation scope commitments -The priorities and scope implied by these ADRs are committed in [../v0.1/plans/v0.1-scope-commitments.md](../v0.1/plans/v0.1-scope-commitments.md). The ADR authoring sprint is staged against that memo. +The priorities and scope implied by these ADRs are committed in [../v0.1/plans/v0.1-scope-commitments.md](../../implementation/v0.1-scope-plans/v0.1-scope-commitments.md). The ADR authoring sprint is staged against that memo. ## ADR acceptance criteria — Loom vocabulary discipline diff --git a/docs/clarion/v0.1/README.md b/docs/clarion/v0.1/README.md index d3d3e748..98a1a0bb 100644 --- a/docs/clarion/v0.1/README.md +++ b/docs/clarion/v0.1/README.md @@ -10,9 +10,8 @@ This folder is the canonical Clarion v0.1 document set. ## Supporting docs -- [reviews/README.md](./reviews/README.md) — retained historical reviews and panel outputs that shaped the current docset. -- [plans/v0.1-scope-commitments.md](./plans/v0.1-scope-commitments.md) — dated scope and decision memo for the v0.1 design freeze. - [../adr/README.md](../adr/README.md) — authored ADRs and remaining decision backlog. +- [../../implementation/README.md](../../implementation/README.md) — archived planning and review history (scope commitments, panel reviews, sprint plans, agent handoffs). Non-normative; supporting context only. ## Reading order @@ -23,4 +22,4 @@ This folder is the canonical Clarion v0.1 document set. ## Document roles - `requirements.md`, `system-design.md`, and `detailed-design.md` are the authoritative layered design set. -- `reviews/` and `plans/` are supporting context, not normative sources. +- Reviews and planning memos under [../../implementation/](../../implementation/) are supporting context, not normative sources. diff --git a/docs/clarion/v0.1/detailed-design.md b/docs/clarion/v0.1/detailed-design.md index 81580f4e..1fbb3773 100644 --- a/docs/clarion/v0.1/detailed-design.md +++ b/docs/clarion/v0.1/detailed-design.md @@ -9,8 +9,8 @@ **Companion documents**: - [requirements.md](./requirements.md) — requirements (the *what*; REQ-* / NFR-* / CON-* / NG-*) - [system-design.md](./system-design.md) — system design (the *how*, mid-level; architecture, mechanisms, diagrams) -- [reviews/pre-restructure/design-review.md](./reviews/pre-restructure/design-review.md) — design review that drove revisions 2-4 -- [reviews/pre-restructure/integration-recon.md](./reviews/pre-restructure/integration-recon.md) — integration reality check against Filigree / Wardline +- [reviews/pre-restructure/design-review.md](../../implementation/v0.1-reviews/pre-restructure/design-review.md) — design review that drove revisions 2-4 +- [reviews/pre-restructure/integration-recon.md](../../implementation/v0.1-reviews/pre-restructure/integration-recon.md) — integration reality check against Filigree / Wardline --- diff --git a/docs/clarion/v0.1/requirements.md b/docs/clarion/v0.1/requirements.md index 357a7e9d..b1a9b5bf 100644 --- a/docs/clarion/v0.1/requirements.md +++ b/docs/clarion/v0.1/requirements.md @@ -7,8 +7,8 @@ **Companion documents**: - [system-design.md](./system-design.md) — system design (the *how*, mid-level) - [detailed-design.md](./detailed-design.md) — detailed design reference (implementation-level) -- [reviews/pre-restructure/design-review.md](./reviews/pre-restructure/design-review.md) — prior review that shaped revs 2-4 -- [reviews/pre-restructure/integration-recon.md](./reviews/pre-restructure/integration-recon.md) — reality check against Filigree / Wardline +- [reviews/pre-restructure/design-review.md](../../implementation/v0.1-reviews/pre-restructure/design-review.md) — prior review that shaped revs 2-4 +- [reviews/pre-restructure/integration-recon.md](../../implementation/v0.1-reviews/pre-restructure/integration-recon.md) — reality check against Filigree / Wardline --- diff --git a/docs/implementation/README.md b/docs/implementation/README.md index 754b030a..d0fd9dfc 100644 --- a/docs/implementation/README.md +++ b/docs/implementation/README.md @@ -1,29 +1,32 @@ -# Implementation Plans +# Implementation Archive -This folder is the canonical home for Clarion implementation planning at the work-package -level. It sits one level above any product-version docset because some work packages -(WP9, WP10) span Clarion **and** sibling Loom products (Filigree, Wardline) and therefore -do not belong inside `docs/clarion/v0.1/`. +This folder is the consolidated archive of Clarion's implementation and planning history. It is **not** part of the release-facing doc surface — readers entering via [`docs/README.md`](../README.md) and the [Clarion v0.1 docset](../clarion/v0.1/README.md) are not expected to need anything here. -## Contents +Material is kept rather than deleted because the [ADRs](../clarion/adr/README.md) cite it for historical context (panel reviews, the v0.1 scope-commitment memo, sprint plans, and agent handoffs that motivated specific decisions). -- [v0.1-plan.md](./v0.1-plan.md) — high-level implementation plan for Clarion v0.1: 11 - work packages, dependency order, anchoring docs/ADRs per package, exit criteria, and - the post-implementation cost-model validation phase. +## Layout -## Relationship to other docs +| Path | Contents | +|---|---| +| [v0.1-plan.md](./v0.1-plan.md) | High-level implementation plan: 11 work packages in dependency order, with anchoring docs/ADRs, exit criteria, and post-implementation cost-model validation. | +| [v0.1-scope-plans/](./v0.1-scope-plans/) | The v0.1 scope-commitment memo — *what* v0.1 ships, decision priorities, locked-in defaults. Cited by several ADRs. | +| [v0.1-reviews/](./v0.1-reviews/) | Pre-restructure design review, integration reconnaissance, and the April 2026 review-panel outputs (executive synthesis, self-sufficiency, threat model, doctrine synthesis). | +| [v0.1-publish/](./v0.1-publish/) | v1.0 publish-track work-stream plans (secret-scanner WS-A, pre-publish blockers). | +| [sprint-1/](./sprint-1/) | Sprint 1 (walking skeleton): WP1+WP2+WP3 execution plans and sign-off ladder. | +| [sprint-2/](./sprint-2/) | Sprint 2 (B-track): B.2–B.6 execution plans, gate results, B.8 scale test, openrouter swap, scope amendment, sign-offs. | +| [sprint-3/](./sprint-3/) | Sprint 3 scope amendment. | +| [handoffs/](./handoffs/) | Dated agent-to-agent handoff notes (formerly `docs/superpowers/handoffs/`). | +| [agent-plans/](./agent-plans/) | TDD-grain plan files used by individual agent runs (formerly `docs/superpowers/plans/`). | +| [arch-analysis-2026-05-18-1244/](./arch-analysis-2026-05-18-1244/) | One-shot architecture-archaeology output: discovery findings, subsystem catalogue, diagrams, final report. | -- **Scope and commitments**: [`../clarion/v0.1/plans/v0.1-scope-commitments.md`](../clarion/v0.1/plans/v0.1-scope-commitments.md). - That memo locks *what* v0.1 ships; this plan describes *how* the build proceeds. -- **Authoritative design**: [`../clarion/v0.1/system-design.md`](../clarion/v0.1/system-design.md) - and [`../clarion/v0.1/detailed-design.md`](../clarion/v0.1/detailed-design.md). - Each work package below names the sections it implements. -- **Decisions**: [`../clarion/adr/README.md`](../clarion/adr/README.md). Each work package - names the accepted ADRs it depends on and any backlog ADRs it is expected to surface. +## Relationship to release-facing docs -## Out of scope for this folder +- **Authoritative design**: [`../clarion/v0.1/system-design.md`](../clarion/v0.1/system-design.md) and [`../clarion/v0.1/detailed-design.md`](../clarion/v0.1/detailed-design.md). Each work package under this folder names the sections it implements. +- **Decisions**: [`../clarion/adr/README.md`](../clarion/adr/README.md). Each work package names the accepted ADRs it depends on and any backlog ADRs it is expected to surface. +- **Scope and commitments**: [`v0.1-scope-plans/v0.1-scope-commitments.md`](./v0.1-scope-plans/v0.1-scope-commitments.md). That memo locks *what* v0.1 ships; the work-package plans describe *how* the build proceeds. -- Step-by-step task breakdowns (TDD-grain). Those belong in per-WP execution plans - written when each package is picked up — not bundled into the high-level plan. -- Filigree issue records. The work-package list here is the source for seeding Filigree - issues, but the issue tracker (not this doc) is the authoritative state-of-work. +## Conventions + +- Documents under this folder are **immutable historical record**, not living plans. Update them only to correct factual errors or to repair a citation; do not retrofit narrative to match later decisions. +- Filigree (not these files) is the authoritative state-of-work tracker. Work-package plans seeded the issue list; the tracker is canonical thereafter. +- TDD-grain task breakdowns belonged in per-run agent plans (now under [agent-plans/](./agent-plans/)) and in Filigree, not in the high-level work-package documents. diff --git a/docs/superpowers/plans/2026-04-18-wp1-scaffold-storage.md b/docs/implementation/agent-plans/2026-04-18-wp1-scaffold-storage.md similarity index 99% rename from docs/superpowers/plans/2026-04-18-wp1-scaffold-storage.md rename to docs/implementation/agent-plans/2026-04-18-wp1-scaffold-storage.md index eb2819c6..85473f0a 100644 --- a/docs/superpowers/plans/2026-04-18-wp1-scaffold-storage.md +++ b/docs/implementation/agent-plans/2026-04-18-wp1-scaffold-storage.md @@ -1998,9 +1998,9 @@ path works; LFS is a v0.2+ knob. ## Related Decisions -- [ADR-011](./ADR-011-writer-actor-concurrency.md) — names the shadow-DB +- [ADR-011](../../clarion/adr/ADR-011-writer-actor-concurrency.md) — names the shadow-DB intermediate; this ADR excludes it from git. -- [ADR-014](./ADR-014-filigree-registry-backend.md) — cross-tool references +- [ADR-014](../../clarion/adr/ADR-014-filigree-registry-backend.md) — cross-tool references rely on `clarion.db` being available to readers (Filigree, Wardline); the commit-by-default posture keeps those references resolvable across machines. diff --git a/docs/superpowers/plans/2026-04-18-wp2-plugin-host.md b/docs/implementation/agent-plans/2026-04-18-wp2-plugin-host.md similarity index 99% rename from docs/superpowers/plans/2026-04-18-wp2-plugin-host.md rename to docs/implementation/agent-plans/2026-04-18-wp2-plugin-host.md index 628ea21b..d96421d2 100644 --- a/docs/superpowers/plans/2026-04-18-wp2-plugin-host.md +++ b/docs/implementation/agent-plans/2026-04-18-wp2-plugin-host.md @@ -97,11 +97,11 @@ Create `/home/john/clarion/crates/clarion-core/src/plugin/mod.rs`: //! # Scope //! //! This module is the Clarion-side end of the plugin transport defined in -//! [ADR-002](../../../../../docs/clarion/adr/ADR-002-plugin-transport-json-rpc.md) +//! [ADR-002](../../clarion/adr/ADR-002-plugin-transport-json-rpc.md) //! and the enforcement surface for -//! [ADR-021](../../../../../docs/clarion/adr/ADR-021-plugin-authority-hybrid.md) +//! [ADR-021](../../clarion/adr/ADR-021-plugin-authority-hybrid.md) //! §2a-d. The ontology boundary rules from -//! [ADR-022](../../../../../docs/clarion/adr/ADR-022-core-plugin-ontology.md) +//! [ADR-022](../../clarion/adr/ADR-022-core-plugin-ontology.md) //! are enforced by [`host`] against the manifest parsed by [`manifest`]. //! //! # Sub-modules @@ -195,7 +195,7 @@ Create `/home/john/clarion/crates/clarion-core/src/plugin/manifest.rs` with the //! `plugin.toml` parser + validator (L5). //! //! Parses the manifest shape locked by WP2 §L5 and validates against -//! [ADR-022](../../../../../docs/clarion/adr/ADR-022-core-plugin-ontology.md): +//! [ADR-022](../../clarion/adr/ADR-022-core-plugin-ontology.md): //! plugin `name` must match the identifier grammar; entity kinds cannot //! shadow the core-reserved set (`file`, `subsystem`, `guidance`); rule-ID //! prefix must be `CLA--` and end with `-`. @@ -527,7 +527,7 @@ pub enum ManifestError { } /// Core-reserved entity kinds per -/// [ADR-022](../../../../../docs/clarion/adr/ADR-022-core-plugin-ontology.md). +/// [ADR-022](../../clarion/adr/ADR-022-core-plugin-ontology.md). /// These are produced by core-owned algorithms (file-discovery, Leiden /// clustering, guidance composition). A plugin declaring any of them in /// its `entity_kinds` list is rejected at manifest parse. diff --git a/docs/superpowers/plans/2026-05-05-b2-class-module-entities.md b/docs/implementation/agent-plans/2026-05-05-b2-class-module-entities.md similarity index 100% rename from docs/superpowers/plans/2026-05-05-b2-class-module-entities.md rename to docs/implementation/agent-plans/2026-05-05-b2-class-module-entities.md diff --git a/docs/superpowers/plans/2026-05-18-phase3-subsystems.md b/docs/implementation/agent-plans/2026-05-18-phase3-subsystems.md similarity index 100% rename from docs/superpowers/plans/2026-05-18-phase3-subsystems.md rename to docs/implementation/agent-plans/2026-05-18-phase3-subsystems.md diff --git a/docs/superpowers/plans/2026-05-18-phase3-subsystems.review.json b/docs/implementation/agent-plans/2026-05-18-phase3-subsystems.review.json similarity index 100% rename from docs/superpowers/plans/2026-05-18-phase3-subsystems.review.json rename to docs/implementation/agent-plans/2026-05-18-phase3-subsystems.review.json diff --git a/docs/arch-analysis-2026-05-18-1244/00-coordination.md b/docs/implementation/arch-analysis-2026-05-18-1244/00-coordination.md similarity index 100% rename from docs/arch-analysis-2026-05-18-1244/00-coordination.md rename to docs/implementation/arch-analysis-2026-05-18-1244/00-coordination.md diff --git a/docs/arch-analysis-2026-05-18-1244/01-discovery-findings.md b/docs/implementation/arch-analysis-2026-05-18-1244/01-discovery-findings.md similarity index 100% rename from docs/arch-analysis-2026-05-18-1244/01-discovery-findings.md rename to docs/implementation/arch-analysis-2026-05-18-1244/01-discovery-findings.md diff --git a/docs/arch-analysis-2026-05-18-1244/02-subsystem-catalog.md b/docs/implementation/arch-analysis-2026-05-18-1244/02-subsystem-catalog.md similarity index 100% rename from docs/arch-analysis-2026-05-18-1244/02-subsystem-catalog.md rename to docs/implementation/arch-analysis-2026-05-18-1244/02-subsystem-catalog.md diff --git a/docs/arch-analysis-2026-05-18-1244/03-diagrams.md b/docs/implementation/arch-analysis-2026-05-18-1244/03-diagrams.md similarity index 100% rename from docs/arch-analysis-2026-05-18-1244/03-diagrams.md rename to docs/implementation/arch-analysis-2026-05-18-1244/03-diagrams.md diff --git a/docs/arch-analysis-2026-05-18-1244/04-final-report.md b/docs/implementation/arch-analysis-2026-05-18-1244/04-final-report.md similarity index 100% rename from docs/arch-analysis-2026-05-18-1244/04-final-report.md rename to docs/implementation/arch-analysis-2026-05-18-1244/04-final-report.md diff --git a/docs/arch-analysis-2026-05-18-1244/temp/section-clarion-cli.md b/docs/implementation/arch-analysis-2026-05-18-1244/temp/section-clarion-cli.md similarity index 100% rename from docs/arch-analysis-2026-05-18-1244/temp/section-clarion-cli.md rename to docs/implementation/arch-analysis-2026-05-18-1244/temp/section-clarion-cli.md diff --git a/docs/arch-analysis-2026-05-18-1244/temp/section-clarion-core.md b/docs/implementation/arch-analysis-2026-05-18-1244/temp/section-clarion-core.md similarity index 100% rename from docs/arch-analysis-2026-05-18-1244/temp/section-clarion-core.md rename to docs/implementation/arch-analysis-2026-05-18-1244/temp/section-clarion-core.md diff --git a/docs/arch-analysis-2026-05-18-1244/temp/section-clarion-mcp.md b/docs/implementation/arch-analysis-2026-05-18-1244/temp/section-clarion-mcp.md similarity index 100% rename from docs/arch-analysis-2026-05-18-1244/temp/section-clarion-mcp.md rename to docs/implementation/arch-analysis-2026-05-18-1244/temp/section-clarion-mcp.md diff --git a/docs/arch-analysis-2026-05-18-1244/temp/section-clarion-storage.md b/docs/implementation/arch-analysis-2026-05-18-1244/temp/section-clarion-storage.md similarity index 100% rename from docs/arch-analysis-2026-05-18-1244/temp/section-clarion-storage.md rename to docs/implementation/arch-analysis-2026-05-18-1244/temp/section-clarion-storage.md diff --git a/docs/arch-analysis-2026-05-18-1244/temp/section-fixture-plugin.md b/docs/implementation/arch-analysis-2026-05-18-1244/temp/section-fixture-plugin.md similarity index 100% rename from docs/arch-analysis-2026-05-18-1244/temp/section-fixture-plugin.md rename to docs/implementation/arch-analysis-2026-05-18-1244/temp/section-fixture-plugin.md diff --git a/docs/arch-analysis-2026-05-18-1244/temp/section-python-plugin.md b/docs/implementation/arch-analysis-2026-05-18-1244/temp/section-python-plugin.md similarity index 100% rename from docs/arch-analysis-2026-05-18-1244/temp/section-python-plugin.md rename to docs/implementation/arch-analysis-2026-05-18-1244/temp/section-python-plugin.md diff --git a/docs/arch-analysis-2026-05-18-1244/temp/validation-02-subsystem-catalog.md b/docs/implementation/arch-analysis-2026-05-18-1244/temp/validation-02-subsystem-catalog.md similarity index 100% rename from docs/arch-analysis-2026-05-18-1244/temp/validation-02-subsystem-catalog.md rename to docs/implementation/arch-analysis-2026-05-18-1244/temp/validation-02-subsystem-catalog.md diff --git a/docs/arch-analysis-2026-05-18-1244/temp/validation-03-diagrams.md b/docs/implementation/arch-analysis-2026-05-18-1244/temp/validation-03-diagrams.md similarity index 100% rename from docs/arch-analysis-2026-05-18-1244/temp/validation-03-diagrams.md rename to docs/implementation/arch-analysis-2026-05-18-1244/temp/validation-03-diagrams.md diff --git a/docs/arch-analysis-2026-05-18-1244/temp/validation-04-final-report.md b/docs/implementation/arch-analysis-2026-05-18-1244/temp/validation-04-final-report.md similarity index 100% rename from docs/arch-analysis-2026-05-18-1244/temp/validation-04-final-report.md rename to docs/implementation/arch-analysis-2026-05-18-1244/temp/validation-04-final-report.md diff --git a/docs/superpowers/handoffs/2026-04-18-wp2-plugin-host-handoff.md b/docs/implementation/handoffs/2026-04-18-wp2-plugin-host-handoff.md similarity index 100% rename from docs/superpowers/handoffs/2026-04-18-wp2-plugin-host-handoff.md rename to docs/implementation/handoffs/2026-04-18-wp2-plugin-host-handoff.md diff --git a/docs/superpowers/handoffs/2026-04-19-wp2-tasks-4-to-9-handoff.md b/docs/implementation/handoffs/2026-04-19-wp2-tasks-4-to-9-handoff.md similarity index 100% rename from docs/superpowers/handoffs/2026-04-19-wp2-tasks-4-to-9-handoff.md rename to docs/implementation/handoffs/2026-04-19-wp2-tasks-4-to-9-handoff.md diff --git a/docs/superpowers/handoffs/2026-04-23-wp2-full-scrub-handoff.md b/docs/implementation/handoffs/2026-04-23-wp2-full-scrub-handoff.md similarity index 100% rename from docs/superpowers/handoffs/2026-04-23-wp2-full-scrub-handoff.md rename to docs/implementation/handoffs/2026-04-23-wp2-full-scrub-handoff.md diff --git a/docs/superpowers/handoffs/2026-04-23-wp2-scrub-findings.md b/docs/implementation/handoffs/2026-04-23-wp2-scrub-findings.md similarity index 100% rename from docs/superpowers/handoffs/2026-04-23-wp2-scrub-findings.md rename to docs/implementation/handoffs/2026-04-23-wp2-scrub-findings.md diff --git a/docs/superpowers/handoffs/2026-04-24-post-wp2-scrub-handoff.md b/docs/implementation/handoffs/2026-04-24-post-wp2-scrub-handoff.md similarity index 100% rename from docs/superpowers/handoffs/2026-04-24-post-wp2-scrub-handoff.md rename to docs/implementation/handoffs/2026-04-24-post-wp2-scrub-handoff.md diff --git a/docs/superpowers/handoffs/2026-04-30-sprint-2-kickoff.md b/docs/implementation/handoffs/2026-04-30-sprint-2-kickoff.md similarity index 100% rename from docs/superpowers/handoffs/2026-04-30-sprint-2-kickoff.md rename to docs/implementation/handoffs/2026-04-30-sprint-2-kickoff.md diff --git a/docs/superpowers/handoffs/2026-05-03-skeleton-audit.md b/docs/implementation/handoffs/2026-05-03-skeleton-audit.md similarity index 100% rename from docs/superpowers/handoffs/2026-05-03-skeleton-audit.md rename to docs/implementation/handoffs/2026-05-03-skeleton-audit.md diff --git a/docs/superpowers/handoffs/2026-05-16-sprint-2-resume.md b/docs/implementation/handoffs/2026-05-16-sprint-2-resume.md similarity index 100% rename from docs/superpowers/handoffs/2026-05-16-sprint-2-resume.md rename to docs/implementation/handoffs/2026-05-16-sprint-2-resume.md diff --git a/docs/superpowers/handoffs/2026-05-18-phase3-subsystems-handoff.md b/docs/implementation/handoffs/2026-05-18-phase3-subsystems-handoff.md similarity index 96% rename from docs/superpowers/handoffs/2026-05-18-phase3-subsystems-handoff.md rename to docs/implementation/handoffs/2026-05-18-phase3-subsystems-handoff.md index 9e08c35a..3ba7b975 100644 --- a/docs/superpowers/handoffs/2026-05-18-phase3-subsystems-handoff.md +++ b/docs/implementation/handoffs/2026-05-18-phase3-subsystems-handoff.md @@ -2,7 +2,7 @@ **Date**: 2026-05-18 **For**: an agent picking this up cold -**Predecessor context**: Sprint 2 closed GREEN; [arch-analysis 2026-05-18](../../arch-analysis-2026-05-18-1244/04-final-report.md) is the current-state snapshot; [Thread-1 publish-prep program](../../implementation/v0.1-publish/thread-1-pre-publish-blockers.md) runs in parallel to this and does not block. +**Predecessor context**: Sprint 2 closed GREEN; [arch-analysis 2026-05-18](../arch-analysis-2026-05-18-1244/04-final-report.md) is the current-state snapshot; [Thread-1 publish-prep program](../../implementation/v0.1-publish/thread-1-pre-publish-blockers.md) runs in parallel to this and does not block. --- @@ -16,7 +16,7 @@ Closing this gap is the single highest-leverage move between the current MCP-MVP Two things, in order, with a human review gate between them. -1. **An implementation plan** (file: `docs/superpowers/plans/2026-XX-XX-phase3-subsystems.md`) in the existing plan-doc convention (see [`docs/superpowers/plans/2026-05-05-b2-class-module-entities.md`](../plans/2026-05-05-b2-class-module-entities.md) for the canonical shape). Task-by-task, file-by-file, with exit criteria each task. +1. **An implementation plan** (file: `docs/superpowers/plans/2026-XX-XX-phase3-subsystems.md`) in the existing plan-doc convention (see [`docs/superpowers/plans/2026-05-05-b2-class-module-entities.md`](../agent-plans/2026-05-05-b2-class-module-entities.md) for the canonical shape). Task-by-task, file-by-file, with exit criteria each task. 2. **The implementation itself**, executed task-by-task under TDD discipline, after the human approves the plan. **Do not skip Phase 1.** Phase 3 clustering touches the schema, the analyze orchestrator, the MCP read surface, and (when WP6 module/subsystem aggregation lands in v0.2) the LLM pipeline. The plan is the surface where the human catches "you missed `runs.stats` serialisation" or "the `in_subsystem` edge needs the writer-actor's edge-contract validator updated" before you write the wrong code for an afternoon. @@ -181,6 +181,6 @@ The last bullet is the actual capability the work delivers. If it works in CI bu - [REQ-CATALOG-05, REQ-ANALYZE-01, REQ-ANALYZE-05](../../clarion/v0.1/requirements.md) - [v0.1-plan.md WP4](../../implementation/v0.1-plan.md#wp4--core-only-pipeline-phases-03-7-8) - [Sprint-2 scope amendment — explicit Phase 3 deferral with retirement path](../../implementation/sprint-2/scope-amendment-2026-05.md) -- [Arch-analysis 2026-05-18 — current code geography](../../arch-analysis-2026-05-18-1244/04-final-report.md) -- [B.2 plan-doc shape — canonical example](../plans/2026-05-05-b2-class-module-entities.md) +- [Arch-analysis 2026-05-18 — current code geography](../arch-analysis-2026-05-18-1244/04-final-report.md) +- [B.2 plan-doc shape — canonical example](../agent-plans/2026-05-05-b2-class-module-entities.md) - [Sprint-2 B.4* — the week-2 go/no-go gate pattern to mimic](../../implementation/sprint-2/scope-amendment-2026-05.md#5-the-week-2-gono-go-gate-load-bearing-risk) diff --git a/docs/implementation/sprint-1/README.md b/docs/implementation/sprint-1/README.md index 980ce64e..ce6bd87e 100644 --- a/docs/implementation/sprint-1/README.md +++ b/docs/implementation/sprint-1/README.md @@ -4,7 +4,7 @@ **Sprint goal**: [Tier A — walking skeleton](./signoffs.md#tier-a--sprint-1-close-walking-skeleton) **Scope**: WP1 (scaffold + storage) → WP2 (plugin protocol + hybrid authority) → WP3 (Python plugin v0.1 baseline) **Predecessor plan**: [`../v0.1-plan.md`](../v0.1-plan.md) -**Scope memo**: [`../../clarion/v0.1/plans/v0.1-scope-commitments.md`](../../clarion/v0.1/plans/v0.1-scope-commitments.md) +**Scope memo**: [`../../clarion/v0.1/plans/v0.1-scope-commitments.md`](../v0.1-scope-plans/v0.1-scope-commitments.md) **Gate to close**: every checkbox ticked in [`signoffs.md`](./signoffs.md) Tier A. --- diff --git a/docs/implementation/sprint-1/signoffs.md b/docs/implementation/sprint-1/signoffs.md index e8e331bc..ff500d99 100644 --- a/docs/implementation/sprint-1/signoffs.md +++ b/docs/implementation/sprint-1/signoffs.md @@ -134,4 +134,4 @@ ladder completeness; not required to close Sprint 1 or Sprint 2. - [`README.md`](./README.md) — sprint orientation and demo script. - [`wp1-scaffold.md`](./wp1-scaffold.md), [`wp2-plugin-host.md`](./wp2-plugin-host.md), [`wp3-python-plugin.md`](./wp3-python-plugin.md) — per-WP plans. - [`../v0.1-plan.md`](../v0.1-plan.md) — high-level plan for all 11 WPs. -- [`../../clarion/v0.1/plans/v0.1-scope-commitments.md`](../../clarion/v0.1/plans/v0.1-scope-commitments.md) — scope memo. +- [`../../clarion/v0.1/plans/v0.1-scope-commitments.md`](../v0.1-scope-plans/v0.1-scope-commitments.md) — scope memo. diff --git a/docs/implementation/sprint-2/b2-class-module-entities.md b/docs/implementation/sprint-2/b2-class-module-entities.md index 53670439..3203c68e 100644 --- a/docs/implementation/sprint-2/b2-class-module-entities.md +++ b/docs/implementation/sprint-2/b2-class-module-entities.md @@ -5,7 +5,7 @@ **Accepted ADRs**: [ADR-007](../../clarion/adr/ADR-007-summary-cache-key.md), [ADR-018](../../clarion/adr/ADR-018-identity-reconciliation.md), [ADR-021](../../clarion/adr/ADR-021-plugin-authority-hybrid.md), [ADR-022](../../clarion/adr/ADR-022-core-plugin-ontology.md), [ADR-023](../../clarion/adr/ADR-023-tooling-baseline.md) **Predecessor**: [WP3 Sprint-1 baseline](../sprint-1/wp3-python-plugin.md) (function-only extraction) **Successor**: B.3 — `contains` edges (planned) -**Sprint-2 kickoff handoff**: [`docs/superpowers/handoffs/2026-04-30-sprint-2-kickoff.md`](../../superpowers/handoffs/2026-04-30-sprint-2-kickoff.md) §"What's in scope for Sprint 2" Tier B row B.2 +**Sprint-2 kickoff handoff**: [`docs/superpowers/handoffs/2026-04-30-sprint-2-kickoff.md`](../handoffs/2026-04-30-sprint-2-kickoff.md) §"What's in scope for Sprint 2" Tier B row B.2 --- diff --git a/docs/implementation/sprint-2/b3-contains-edges.md b/docs/implementation/sprint-2/b3-contains-edges.md index 1ce9e5e7..b6e44619 100644 --- a/docs/implementation/sprint-2/b3-contains-edges.md +++ b/docs/implementation/sprint-2/b3-contains-edges.md @@ -5,7 +5,7 @@ **Accepted ADRs**: [ADR-002](../../clarion/adr/ADR-002-plugin-transport-json-rpc.md), [ADR-003](../../clarion/adr/ADR-003-entity-id-scheme.md), [ADR-006](../../clarion/adr/ADR-006-clustering-algorithm.md), [ADR-007](../../clarion/adr/ADR-007-summary-cache-key.md), [ADR-022](../../clarion/adr/ADR-022-core-plugin-ontology.md), [ADR-023](../../clarion/adr/ADR-023-tooling-baseline.md), [ADR-024](../../clarion/adr/ADR-024-guidance-schema-vocabulary.md), [ADR-026](../../clarion/adr/ADR-026-containment-wire-and-edge-identity.md), [ADR-027](../../clarion/adr/ADR-027-ontology-version-semver.md) **Predecessor**: [B.2 — class + module entity emission](./b2-class-module-entities.md) **Successor**: B.1 (multi-file dispatch — WP4 Phase 0+1) and B.4 (catalog rendering) -**Sprint-2 kickoff handoff**: [`docs/superpowers/handoffs/2026-04-30-sprint-2-kickoff.md`](../../superpowers/handoffs/2026-04-30-sprint-2-kickoff.md) §"What's in scope for Sprint 2" Tier B row B.3 +**Sprint-2 kickoff handoff**: [`docs/superpowers/handoffs/2026-04-30-sprint-2-kickoff.md`](../handoffs/2026-04-30-sprint-2-kickoff.md) §"What's in scope for Sprint 2" Tier B row B.3 --- diff --git a/docs/implementation/sprint-2/scope-amendment-2026-05.md b/docs/implementation/sprint-2/scope-amendment-2026-05.md index 22252dcc..1ae57498 100644 --- a/docs/implementation/sprint-2/scope-amendment-2026-05.md +++ b/docs/implementation/sprint-2/scope-amendment-2026-05.md @@ -3,8 +3,8 @@ **Status**: Sprint 2 closed — RED partial milestone; see [signoffs.md](./signoffs.md) and [b8-results.md](./b8-results.md). **Date opened**: 2026-05-16 **Author**: John Morrissey -**Predecessor**: [`docs/superpowers/handoffs/2026-04-30-sprint-2-kickoff.md`](../../superpowers/handoffs/2026-04-30-sprint-2-kickoff.md) -**Successor**: [`docs/superpowers/handoffs/2026-05-16-sprint-2-resume.md`](../../superpowers/handoffs/2026-05-16-sprint-2-resume.md) +**Predecessor**: [`docs/superpowers/handoffs/2026-04-30-sprint-2-kickoff.md`](../handoffs/2026-04-30-sprint-2-kickoff.md) +**Successor**: [`docs/superpowers/handoffs/2026-05-16-sprint-2-resume.md`](../handoffs/2026-05-16-sprint-2-resume.md) This memo serves three roles in one artifact: @@ -216,7 +216,7 @@ Existing open Sprint-2 issues that stay open: - [v0.1-plan.md](../v0.1-plan.md) — the 11-WP plan; gets a forward-pointer to this memo - [Sprint 1 signoffs](../sprint-1/signoffs.md) — the ladder format Sprint 2's eventual close will follow -- [Sprint 2 kickoff handoff](../../superpowers/handoffs/2026-04-30-sprint-2-kickoff.md) — the original 7-box scope +- [Sprint 2 kickoff handoff](../handoffs/2026-04-30-sprint-2-kickoff.md) — the original 7-box scope - [B.2 design](./b2-class-module-entities.md) — shipped - [B.3 design](./b3-contains-edges.md) — implementation pending - [ADR-028](../../clarion/adr/ADR-028-edge-confidence-tiers.md) — new diff --git a/docs/implementation/v0.1-plan.md b/docs/implementation/v0.1-plan.md index da988175..9f5298f6 100644 --- a/docs/implementation/v0.1-plan.md +++ b/docs/implementation/v0.1-plan.md @@ -4,7 +4,7 @@ **Date opened**: 2026-04-18 **Last amended**: 2026-05-16 **Plan owner**: John Morrissey (primary dev of Clarion, Filigree, Wardline) -**Predecessor**: [`../clarion/v0.1/plans/v0.1-scope-commitments.md`](../clarion/v0.1/plans/v0.1-scope-commitments.md) (scope locked 2026-04-18) +**Predecessor**: [`../clarion/v0.1/plans/v0.1-scope-commitments.md`](./v0.1-scope-plans/v0.1-scope-commitments.md) (scope locked 2026-04-18) **Amendments**: [`sprint-2/scope-amendment-2026-05.md`](./sprint-2/scope-amendment-2026-05.md) (2026-05-16 — narrows WP6, splits WP9 into A/B, defers WP4/5/7/10/11-as-scheduled, pulls WP8 + WP9-A into Sprint 2 for MVP MCP surface) **Audience**: implementation engineer (the author + future contributors), Filigree-side issue-tracker seeder, design reviewers checking that every system-design section has an @@ -713,7 +713,7 @@ ownership decision). ### WP11 — Post-implementation cost/perf validation -**Anchoring docs**: [`../clarion/v0.1/plans/v0.1-scope-commitments.md`](../clarion/v0.1/plans/v0.1-scope-commitments.md) +**Anchoring docs**: [`../clarion/v0.1/plans/v0.1-scope-commitments.md`](./v0.1-scope-plans/v0.1-scope-commitments.md) §"Validation". **Accepted ADRs**: none — this is a measurement task, not a design task. @@ -810,5 +810,5 @@ Seeding is **not** done by this plan; it is a follow-up action item. - [Clarion v0.1 system design](../clarion/v0.1/system-design.md) - [Clarion v0.1 detailed design](../clarion/v0.1/detailed-design.md) - [Clarion ADR index](../clarion/adr/README.md) -- [v0.1 scope commitments memo](../clarion/v0.1/plans/v0.1-scope-commitments.md) +- [v0.1 scope commitments memo](./v0.1-scope-plans/v0.1-scope-commitments.md) - [Loom suite doctrine](../suite/loom.md) diff --git a/docs/implementation/v0.1-publish/thread-1-pre-publish-blockers.md b/docs/implementation/v0.1-publish/thread-1-pre-publish-blockers.md index 2b9a84a5..97beac59 100644 --- a/docs/implementation/v0.1-publish/thread-1-pre-publish-blockers.md +++ b/docs/implementation/v0.1-publish/thread-1-pre-publish-blockers.md @@ -3,7 +3,7 @@ > **For agentic workers:** REQUIRED SUB-SKILL: `superpowers:subagent-driven-development` (recommended) or `superpowers:executing-plans`. Each workstream is independently dispatchable; sequencing is described in §1. **Status**: drafted 2026-05-18 — not yet broken into Filigree issues. -**Predecessor context**: Sprint 2 closed GREEN ([`../sprint-2/signoffs.md`](../sprint-2/signoffs.md)); the 2026-05-18 codebase archaeology ([`../../arch-analysis-2026-05-18-1244/04-final-report.md`](../../arch-analysis-2026-05-18-1244/04-final-report.md)) is the authoritative current-state snapshot. +**Predecessor context**: Sprint 2 closed GREEN ([`../sprint-2/signoffs.md`](../sprint-2/signoffs.md)); the 2026-05-18 codebase archaeology ([`../../arch-analysis-2026-05-18-1244/04-final-report.md`](../arch-analysis-2026-05-18-1244/04-final-report.md)) is the authoritative current-state snapshot. **Goal**: take the amended-v0.1 MCP-MVP from "works on the elspeth-slice corpus on the author's box" to "publishable v0.1" — meaning *an outside operator can install it, point it at an arbitrary repo without leaking secrets, and find their way to first value in five minutes*. **Scope discipline**: this is *only* Thread 1 (pre-publish operational/security blockers). Two adjacent threads exist and are NOT in this program: @@ -430,4 +430,4 @@ filigree close clarion-2d178ddda0 --reason="superseded by Publish-prep --force t - [System design — §10 Security, pre-ingest redaction paragraph](../../clarion/v0.1/system-design.md) - [v0.1-plan.md — WP5 scope](../v0.1-plan.md#wp5--pre-ingest-secret-scanner) - [Sprint-2 scope amendment — explicit WP5 deferral rationale, "production deployment against unknown corpora gates on this returning"](../sprint-2/scope-amendment-2026-05.md) -- [Arch analysis final report — §7 follow-ups #9 (L-1) and §5.3 L-2 (closed)](../../arch-analysis-2026-05-18-1244/04-final-report.md) +- [Arch analysis final report — §7 follow-ups #9 (L-1) and §5.3 L-2 (closed)](../arch-analysis-2026-05-18-1244/04-final-report.md) diff --git a/docs/implementation/v0.1-publish/ws-a-secret-scanner.md b/docs/implementation/v0.1-publish/ws-a-secret-scanner.md index 5f82376a..cd0152f6 100644 --- a/docs/implementation/v0.1-publish/ws-a-secret-scanner.md +++ b/docs/implementation/v0.1-publish/ws-a-secret-scanner.md @@ -8,6 +8,11 @@ **Requirements floor**: `NFR-SEC-01` (pre-ingest scan + block + baseline), `NFR-SEC-04` (security events as findings), `NFR-OPS-01` / `NFR-OPS-04` (single-binary distribution). **Effort estimate**: 6–9 working days at agentic velocity. Tasks 1 and 2 parallelisable; 3 → 4 → 5 sequential; 6 + 7 parallel with any. +**Implementation accuracy note**: code-path references and line numbers in +this planning package were last checked on 2026-05-19 against the `scanner` +branch. Treat symbol names as authoritative and line numbers as historical +orientation only. + --- ## 1. Purpose @@ -132,10 +137,10 @@ For each detection, the `FindingRecord` is populated: | `run_id` | current run's `run_id` | | `rule_id` | `"CLA-SEC-SECRET-DETECTED"` (or other; see §6 catalogue) | | `kind` | `"defect"` (per ADR-004; secrets are defects pending remediation) | -| `severity` | `"error"` (or `"info"` for `CLA-INFRA-SECRET-BASELINE-MATCH`) | +| `severity` | `"ERROR"` (or `"INFO"` for `CLA-INFRA-SECRET-BASELINE-MATCH`) | | `confidence` | `Some(1.0)` for named-pattern matches; `Some(0.6)` for high-entropy detections (rough heuristic — refine in Task 1) | | `confidence_basis` | `Some("pattern")` or `Some("entropy")` | -| `entity_id` | empty string (file-level, not entity-bound; the file path is in `evidence_json`) | +| `entity_id` | `core:file:{blake3(relative_path)}` synthetic file anchor for file-level findings; the file path is also retained in `evidence_json` | | `related_entities_json` | `"[]"` | | `message` | one-line human-readable, e.g. `"AWS access key detected in src/api/keys.py:42"` (the literal bytes NEVER appear in this string) | | `evidence_json` | `{"file_path": "...", "line_number": 42, "rule": "AwsAccessKeyId", "hashed_secret_hex": "..."}` | @@ -464,9 +469,9 @@ processing loop, insert a pre-ingest scan pass: 4. Emit findings via `WriterCmd::InsertFinding` (see §3.1): - One `CLA-SEC-SECRET-DETECTED` per `allowed` detection (severity - `error`). + `ERROR`). - One `CLA-INFRA-SECRET-BASELINE-MATCH` per `fired_entries` element - (severity `info`). + (severity `INFO`). - One `CLA-INFRA-SECRET-BASELINE-NO-JUSTIFICATION` per `BaselineError::MissingJustification` surfaced at load time. 5. Pass the `BlockSet` through to the per-plugin processing loop so the @@ -750,11 +755,11 @@ existing table shape in §5 (do not invent a new format). | Rule-ID | Severity | Category | Description (one sentence) | Remediation (one sentence) | ADR | |---|---|---|---|---|---| -| `CLA-SEC-SECRET-DETECTED` | error | security | Pre-ingest secret scanner detected a credential pattern in a file slated for LLM dispatch. | Remove the secret, rotate the credential, or whitelist via `.clarion/secrets-baseline.yaml` with a justification. | [ADR-013](../adr/ADR-013-pre-ingest-secret-scanner.md) | -| `CLA-SEC-UNREDACTED-SECRETS-ALLOWED` | error | security | Operator invoked `--allow-unredacted-secrets`; file content reached the LLM provider with secrets intact. | Audit override usage via `filigree list --rule-id=CLA-SEC-UNREDACTED-SECRETS-ALLOWED --since 30d`. | [ADR-013](../adr/ADR-013-pre-ingest-secret-scanner.md) | -| `CLA-INFRA-SECRET-BASELINE-NO-JUSTIFICATION` | error | infra | Baseline entry missing required `justification` field; entry not honoured. | Add a `justification` string explaining why the match is safe. | [ADR-013](../adr/ADR-013-pre-ingest-secret-scanner.md) | -| `CLA-INFRA-SECRET-BASELINE-MATCH` | info | infra | Baseline entry suppressed a scanner detection (audit surface). | None — informational, retained for `NFR-SEC-04` audit. | [ADR-013](../adr/ADR-013-pre-ingest-secret-scanner.md) | -| `CLA-INFRA-SECRET-OVERRIDE-UNCONFIRMED` | error | infra | `--allow-unredacted-secrets` supplied without confirmation; run aborted before start. | Supply `--confirm-allow-unredacted-secrets=yes-i-understand` in non-TTY contexts or run interactively. | [ADR-013](../adr/ADR-013-pre-ingest-secret-scanner.md) | +| `CLA-SEC-SECRET-DETECTED` | error | security | Pre-ingest secret scanner detected a credential pattern in a file slated for LLM dispatch. | Remove the secret, rotate the credential, or whitelist via `.clarion/secrets-baseline.yaml` with a justification. | [ADR-013](../../clarion/adr/ADR-013-pre-ingest-secret-scanner.md) | +| `CLA-SEC-UNREDACTED-SECRETS-ALLOWED` | error | security | Operator invoked `--allow-unredacted-secrets`; file content reached the LLM provider with secrets intact. | Audit override usage via `filigree list --rule-id=CLA-SEC-UNREDACTED-SECRETS-ALLOWED --since 30d`. | [ADR-013](../../clarion/adr/ADR-013-pre-ingest-secret-scanner.md) | +| `CLA-INFRA-SECRET-BASELINE-NO-JUSTIFICATION` | error | infra | Baseline entry missing required `justification` field; entry not honoured. | Add a `justification` string explaining why the match is safe. | [ADR-013](../../clarion/adr/ADR-013-pre-ingest-secret-scanner.md) | +| `CLA-INFRA-SECRET-BASELINE-MATCH` | info | infra | Baseline entry suppressed a scanner detection (audit surface). | None — informational, retained for `NFR-SEC-04` audit. | [ADR-013](../../clarion/adr/ADR-013-pre-ingest-secret-scanner.md) | +| `CLA-INFRA-SECRET-OVERRIDE-UNCONFIRMED` | error | infra | `--allow-unredacted-secrets` supplied without confirmation; run aborted before start. | Supply `--confirm-allow-unredacted-secrets=yes-i-understand` in non-TTY contexts or run interactively. | [ADR-013](../../clarion/adr/ADR-013-pre-ingest-secret-scanner.md) | These IDs make the WP9-B Filigree-emission story (v0.2) able to round-trip without a separate spec pass. @@ -1019,5 +1024,5 @@ off via the criteria in §9. - [Thread 1 — Pre-publish blockers (program of work)](./thread-1-pre-publish-blockers.md) — the umbrella program. - [v0.1-plan.md — WP5 scope](../v0.1-plan.md#wp5--pre-ingest-secret-scanner) — original work-package definition. - [Sprint 2 scope amendment — WP5 deferral rationale](../sprint-2/scope-amendment-2026-05.md) — "production deployment against unknown corpora gates on this returning." -- [Arch-analysis final report §7 follow-ups](../../arch-analysis-2026-05-18-1244/04-final-report.md) — H-1 (`SoftFailed` coverage) coverage now closed via `clarion-141ca7de30`; relevant precondition for Task 3. +- [Arch-analysis final report §7 follow-ups](../arch-analysis-2026-05-18-1244/04-final-report.md) — H-1 (`SoftFailed` coverage) coverage now closed via `clarion-141ca7de30`; relevant precondition for Task 3. - [`detect-secrets` baseline format](https://github.com/Yelp/detect-secrets/blob/master/README.md#baseline-file) — the format Task 2 matches. diff --git a/docs/clarion/v0.1/reviews/README.md b/docs/implementation/v0.1-reviews/README.md similarity index 100% rename from docs/clarion/v0.1/reviews/README.md rename to docs/implementation/v0.1-reviews/README.md diff --git a/docs/clarion/v0.1/reviews/panel-2026-04-17/00-executive-synthesis.md b/docs/implementation/v0.1-reviews/panel-2026-04-17/00-executive-synthesis.md similarity index 100% rename from docs/clarion/v0.1/reviews/panel-2026-04-17/00-executive-synthesis.md rename to docs/implementation/v0.1-reviews/panel-2026-04-17/00-executive-synthesis.md diff --git a/docs/clarion/v0.1/reviews/panel-2026-04-17/04-self-sufficiency.md b/docs/implementation/v0.1-reviews/panel-2026-04-17/04-self-sufficiency.md similarity index 100% rename from docs/clarion/v0.1/reviews/panel-2026-04-17/04-self-sufficiency.md rename to docs/implementation/v0.1-reviews/panel-2026-04-17/04-self-sufficiency.md diff --git a/docs/clarion/v0.1/reviews/panel-2026-04-17/09-threat-model.md b/docs/implementation/v0.1-reviews/panel-2026-04-17/09-threat-model.md similarity index 100% rename from docs/clarion/v0.1/reviews/panel-2026-04-17/09-threat-model.md rename to docs/implementation/v0.1-reviews/panel-2026-04-17/09-threat-model.md diff --git a/docs/clarion/v0.1/reviews/panel-2026-04-17/11-doctrine-panel-synthesis.md b/docs/implementation/v0.1-reviews/panel-2026-04-17/11-doctrine-panel-synthesis.md similarity index 100% rename from docs/clarion/v0.1/reviews/panel-2026-04-17/11-doctrine-panel-synthesis.md rename to docs/implementation/v0.1-reviews/panel-2026-04-17/11-doctrine-panel-synthesis.md diff --git a/docs/clarion/v0.1/reviews/pre-restructure/design-review.md b/docs/implementation/v0.1-reviews/pre-restructure/design-review.md similarity index 100% rename from docs/clarion/v0.1/reviews/pre-restructure/design-review.md rename to docs/implementation/v0.1-reviews/pre-restructure/design-review.md diff --git a/docs/clarion/v0.1/reviews/pre-restructure/integration-recon.md b/docs/implementation/v0.1-reviews/pre-restructure/integration-recon.md similarity index 100% rename from docs/clarion/v0.1/reviews/pre-restructure/integration-recon.md rename to docs/implementation/v0.1-reviews/pre-restructure/integration-recon.md diff --git a/docs/clarion/v0.1/plans/v0.1-scope-commitments.md b/docs/implementation/v0.1-scope-plans/v0.1-scope-commitments.md similarity index 100% rename from docs/clarion/v0.1/plans/v0.1-scope-commitments.md rename to docs/implementation/v0.1-scope-plans/v0.1-scope-commitments.md diff --git a/docs/suite/briefing.md b/docs/suite/briefing.md index fd338aa2..e5acaea3 100644 --- a/docs/suite/briefing.md +++ b/docs/suite/briefing.md @@ -24,7 +24,7 @@ Clarion ingests a codebase, extracts entities (functions, classes, modules, pack **Typical invocation**: `clarion analyze ` for batch indexing; `clarion serve` for MCP + HTTP consult. -**Status**: designed, not yet built. Target first customer is `elspeth` (~425k LOC Python). +**Status**: walking skeleton merged (Sprint 1, tagged `v0.1-sprint-1`); v0.1 build in flight against the published design. Target first customer is `elspeth` (~425k LOC Python). ### Filigree — the workflow and findings tracker @@ -137,7 +137,7 @@ Four commitments keep the Loom products from drifting into overlap (see [loom.md |---|---|---|---| | Filigree | Yes | Yes — active development | `filigree` itself; this project | | Wardline | Yes | Yes — commit-cadence scanner | Wardline's own codebase | -| Clarion | No — designed only | Not yet | `elspeth` (~425k LOC Python) targeted for v0.1 validation | +| Clarion | Partial — Sprint 1 walking skeleton tagged `v0.1-sprint-1`; v0.1 build in flight | Not yet — pre-v0.1 release | `elspeth` (~425k LOC Python) targeted for v0.1 validation | | Shuttle | No — proposed; no design yet | Not yet | None — not yet scoped | ### What Clarion v0.1 ships @@ -180,7 +180,7 @@ Clarion's v0.1 design set spells these asks out in [system-design.md](../clarion | Read Clarion's requirements | [../clarion/v0.1/requirements.md](../clarion/v0.1/requirements.md) | | Read Clarion's system design | [../clarion/v0.1/system-design.md](../clarion/v0.1/system-design.md) | | Read Clarion's detailed design reference | [../clarion/v0.1/detailed-design.md](../clarion/v0.1/detailed-design.md) | -| See what the design reviewer flagged | [../clarion/v0.1/reviews/pre-restructure/design-review.md](../clarion/v0.1/reviews/pre-restructure/design-review.md) | -| See the integration reality check | [../clarion/v0.1/reviews/pre-restructure/integration-recon.md](../clarion/v0.1/reviews/pre-restructure/integration-recon.md) | +| Read accepted architecture decisions | [../clarion/adr/README.md](../clarion/adr/README.md) | +| Review the planning and review archive | [../implementation/README.md](../implementation/README.md) | | Work with Filigree today | Check out the Filigree repository; start with its `CLAUDE.md` and `filigree --help`. | | Work with Wardline today | Check out the Wardline repository; start with `docs/spec/`. | diff --git a/docs/suite/glossary.md b/docs/suite/glossary.md index 3e43de4e..97ea4f6f 100644 --- a/docs/suite/glossary.md +++ b/docs/suite/glossary.md @@ -15,7 +15,7 @@ This file is a **design-review artifact**, not infrastructure. Nothing imports i - Authoring an ADR that introduces or renames a cross-product-visible field name - Reviewing a wire-format change that adds a new top-level key - Onboarding to a Loom product after working on another, to surface vocabulary surprises -- Triaging a bug whose framing depends on what a word means (the trigger that produced this glossary was exactly such a triage — see [skeleton-audit](../superpowers/handoffs/2026-05-03-skeleton-audit.md)) +- Triaging a bug whose framing depends on what a word means (the trigger that produced this glossary was exactly such a triage — see [skeleton-audit](../implementation/handoffs/2026-05-03-skeleton-audit.md)) **Update this glossary when**: @@ -56,7 +56,7 @@ A vocabulary verdict is part of ADR-acceptance evidence, not a courtesy. Three o | `rule_id` | Clarion + Wardline → Filigree | Namespaced prefix per emitter: `CLA-PY-*`, `CLA-INFRA-*`, `CLA-FACT-*`, `CLA-SEC-*`, `WLN-*`. Filigree stores byte-for-byte; round-trip preserved. | [ADR-017](../clarion/adr/ADR-017-severity-and-dedup.md), [ADR-022](../clarion/adr/ADR-022-core-plugin-ontology.md) — namespacing convention + grammar enforcement at the Clarion-plugin boundary | | `finding` (wire shape) | Clarion + Wardline → Filigree | Cross-product unified record type. Field ownership documented; extension via `metadata` slot (top-level keys outside the enumerated set are silently dropped). | [ADR-004](../clarion/adr/ADR-004-finding-exchange-format.md) — full wire schema with explicit ownership | -### Renamed clashes (resolved by ADR-024 — see [skeleton-audit](../superpowers/handoffs/2026-05-03-skeleton-audit.md)) +### Renamed clashes (resolved by ADR-024 — see [skeleton-audit](../implementation/handoffs/2026-05-03-skeleton-audit.md)) These entries record the resolution per the `renamed` verdict (see ADR-acceptance rule above). Each row names the pre-rename collision and the post-rename Clarion field name; Filigree's vocabulary is unchanged. From 3dbeb9510ca670817647beff8955cd28dc116fd4 Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Tue, 19 May 2026 07:24:39 +1000 Subject: [PATCH 008/187] fix(wp5): address scanner review gaps ADR-013: harden pre-ingest secret scanner contracts, audit evidence, MCP briefing-block signals, and operator docs. Verification: cargo fmt --all -- --check; cargo clippy --workspace --all-targets --all-features -- -D warnings; cargo build --workspace --bins; cargo nextest run --workspace --all-features; RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps --all-features; cargo deny check; bash tests/e2e/sprint_1_walking_skeleton.sh; git diff --check. --- Cargo.lock | 2 + crates/clarion-cli/Cargo.toml | 1 + crates/clarion-cli/src/analyze.rs | 2 +- crates/clarion-cli/src/http_read.rs | 6 +- crates/clarion-cli/src/main.rs | 21 +- crates/clarion-cli/src/secret_scan.rs | 122 +++++++--- crates/clarion-cli/src/secret_scan/anchors.rs | 2 +- .../clarion-cli/src/secret_scan/baseline.rs | 10 +- crates/clarion-cli/src/secret_scan/files.rs | 71 ++++++ .../clarion-cli/src/secret_scan/findings.rs | 130 +++++++++-- crates/clarion-cli/src/serve.rs | 4 +- crates/clarion-cli/tests/secret_scan.rs | 220 +++++++++++++++++- crates/clarion-core/Cargo.toml | 1 + crates/clarion-core/src/lib.rs | 1 + crates/clarion-core/src/llm_provider.rs | 107 +++++++-- crates/clarion-core/src/plugin/host.rs | 38 ++- crates/clarion-core/src/plugin/mod.rs | 4 +- crates/clarion-mcp/Cargo.toml | 1 + crates/clarion-mcp/src/config.rs | 107 ++++++++- crates/clarion-mcp/src/lib.rs | 19 +- crates/clarion-mcp/tests/storage_tools.rs | 53 +++++ crates/clarion-scanner/src/baseline.rs | 43 ++-- crates/clarion-scanner/src/lib.rs | 177 +++++++++++--- crates/clarion-scanner/src/patterns.rs | 35 +-- crates/clarion-scanner/tests/scanner.rs | 58 ++++- .../v0.1-publish/ws-a-secret-scanner.md | 7 +- docs/operator/README.md | 2 + docs/operator/secret-scanning.md | 2 +- 28 files changed, 1048 insertions(+), 198 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4c58cceb..acac28d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -330,6 +330,7 @@ dependencies = [ "serde", "serde_json", "serde_norway", + "sha1", "sha2", "tempfile", "time", @@ -370,6 +371,7 @@ dependencies = [ "thiserror 1.0.69", "time", "tokio", + "tracing", ] [[package]] diff --git a/crates/clarion-cli/Cargo.toml b/crates/clarion-cli/Cargo.toml index 21359804..694ac16c 100644 --- a/crates/clarion-cli/Cargo.toml +++ b/crates/clarion-cli/Cargo.toml @@ -41,4 +41,5 @@ assert_cmd.workspace = true clarion-plugin-fixture = { path = "../clarion-plugin-fixture", version = "0.1.0-dev" } rusqlite.workspace = true serde_json.workspace = true +sha1.workspace = true tempfile.workspace = true diff --git a/crates/clarion-cli/src/analyze.rs b/crates/clarion-cli/src/analyze.rs index 1021e2a9..f54ca343 100644 --- a/crates/clarion-cli/src/analyze.rs +++ b/crates/clarion-cli/src/analyze.rs @@ -1190,7 +1190,7 @@ fn run_plugin_blocking( plugin_id: &str, executable: &Path, files: &[PathBuf], - briefing_blocks: &BTreeMap, + briefing_blocks: &BTreeMap, scanned_source_files: &BTreeSet, ) -> Result { use clarion_core::PluginHost; diff --git a/crates/clarion-cli/src/http_read.rs b/crates/clarion-cli/src/http_read.rs index 27509e84..ccb7ab78 100644 --- a/crates/clarion-cli/src/http_read.rs +++ b/crates/clarion-cli/src/http_read.rs @@ -1,4 +1,3 @@ -use std::net::SocketAddr; use std::path::PathBuf; use std::thread; @@ -46,10 +45,7 @@ pub fn spawn( if !config.enabled { return Ok(None); } - let bind: SocketAddr = config - .bind - .parse() - .with_context(|| format!("parse serve.http.bind {:?}", config.bind))?; + let bind = config.bind; let (shutdown_tx, shutdown_rx) = oneshot::channel(); let (ready_tx, ready_rx) = std::sync::mpsc::channel(); let join = thread::spawn(move || -> Result<()> { diff --git a/crates/clarion-cli/src/main.rs b/crates/clarion-cli/src/main.rs index f9455aa9..fb579c21 100644 --- a/crates/clarion-cli/src/main.rs +++ b/crates/clarion-cli/src/main.rs @@ -34,9 +34,21 @@ fn main() -> Result<()> { let rt = tokio::runtime::Builder::new_multi_thread() .enable_all() .build()?; + let secret_scan = match secret_scan::SecretScanOptions::from_cli( + allow_unredacted_secrets, + confirm_allow_unredacted_secrets, + ) { + Ok(options) => options, + Err(err) => { + eprintln!("{err}"); + std::process::exit(78); + } + }; if config.is_none() - && !allow_unredacted_secrets - && confirm_allow_unredacted_secrets.is_none() + && matches!( + secret_scan.override_policy, + secret_scan::OverridePolicy::Forbid + ) { return rt.block_on(analyze::run(path)); } @@ -44,10 +56,7 @@ fn main() -> Result<()> { path, analyze::AnalyzeOptions { config_path: config, - secret_scan: secret_scan::SecretScanOptions { - allow_unredacted_secrets, - confirm_allow_unredacted_secrets, - }, + secret_scan, }, )) } diff --git a/crates/clarion-cli/src/secret_scan.rs b/crates/clarion-cli/src/secret_scan.rs index 970d1c5a..dc9929c3 100644 --- a/crates/clarion-cli/src/secret_scan.rs +++ b/crates/clarion-cli/src/secret_scan.rs @@ -19,10 +19,13 @@ mod files; mod findings; use anyhow::{Context, Result}; +use clarion_core::BriefingBlockReason; use clarion_scanner::{Detection, Scanner, SuppressionResult}; use clarion_storage::{Writer, commands::EntityRecord}; pub(crate) use files::collect_scan_files; -use findings::{PendingFinding, secret_detected_finding}; +use findings::{ + FindingConfidenceBasis, FindingKind, FindingSeverity, PendingFinding, secret_detected_finding, +}; use serde_json::json; const SECRET_OVERRIDE_ALLOWED: &str = "CLA-SEC-UNREDACTED-SECRETS-ALLOWED"; @@ -31,13 +34,54 @@ const CONFIRM_TOKEN: &str = "yes-i-understand"; #[derive(Debug, Clone, Default)] pub(crate) struct SecretScanOptions { - pub(crate) allow_unredacted_secrets: bool, - pub(crate) confirm_allow_unredacted_secrets: Option, + pub(crate) override_policy: OverridePolicy, +} + +impl SecretScanOptions { + pub(crate) fn from_cli( + allow_unredacted_secrets: bool, + confirm_allow_unredacted_secrets: Option, + ) -> std::result::Result { + let override_policy = match (allow_unredacted_secrets, confirm_allow_unredacted_secrets) { + (false, None) => OverridePolicy::Forbid, + (true, None) => OverridePolicy::RequireInteractive, + (true, Some(token)) if token == CONFIRM_TOKEN => { + OverridePolicy::Preconfirmed(ConfirmToken) + } + (true | false, Some(_)) => { + return Err(OverrideConfirmationError); + } + }; + Ok(Self { override_policy }) + } +} + +#[derive(Debug, Clone, Default)] +pub(crate) enum OverridePolicy { + #[default] + Forbid, + RequireInteractive, + Preconfirmed(ConfirmToken), +} + +#[derive(Debug, Clone)] +pub(crate) struct ConfirmToken; + +#[derive(Debug, Clone)] +pub(crate) struct OverrideConfirmationError; + +impl std::fmt::Display for OverrideConfirmationError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{OVERRIDE_UNCONFIRMED}: --allow-unredacted-secrets requires confirmation token {CONFIRM_TOKEN:?}" + ) + } } #[derive(Debug, Clone, Default)] pub(crate) struct SecretScanOutcome { - pub(crate) briefing_blocks: BTreeMap, + pub(crate) briefing_blocks: BTreeMap, findings: Vec, finding_anchors: BTreeMap, override_files: Vec, @@ -122,10 +166,10 @@ pub(crate) fn pre_ingest( baseline_matches.extend(fired_entries.into_iter().map(|entry| PendingFinding { file_path: normalize_project_path(project_root, &entry.file_path), rule_id: baseline::baseline_match_rule_id(), - kind: "fact", - severity: "INFO", + kind: FindingKind::Fact, + severity: FindingSeverity::Info, confidence: Some(1.0), - confidence_basis: Some("baseline"), + confidence_basis: Some(FindingConfidenceBasis::Baseline), message: format!( "Secret baseline entry matched {}:{}", entry.file_path.display(), @@ -134,7 +178,7 @@ pub(crate) fn pre_ingest( evidence: json!({ "file_path": entry.file_path, "line_number": entry.entry.line_number, - "rule": entry.entry.rule_type, + "rule": entry.entry.rule_type.as_str(), }), })); per_file.push((canonical_file, allowed)); @@ -143,6 +187,7 @@ pub(crate) fn pre_ingest( let override_confirmed = confirm_override_if_needed(project_root, &all_allowed, options); let mut briefing_blocks = BTreeMap::new(); let mut override_files = BTreeSet::new(); + let mut override_detections = BTreeMap::>::new(); findings.extend(baseline_matches); for (file, allowed) in per_file { @@ -150,9 +195,10 @@ pub(crate) fn pre_ingest( continue; } if override_confirmed { - override_files.insert(file); + override_files.insert(file.clone()); + override_detections.insert(file, allowed); } else { - briefing_blocks.insert(file.clone(), "secret_present".to_owned()); + briefing_blocks.insert(file.clone(), BriefingBlockReason::SecretPresent); findings.extend( allowed .iter() @@ -162,13 +208,14 @@ pub(crate) fn pre_ingest( } for file in &override_files { + let detections = override_detections.get(file).map_or(&[][..], Vec::as_slice); findings.push(PendingFinding { file_path: file.clone(), rule_id: SECRET_OVERRIDE_ALLOWED, - kind: "defect", - severity: "ERROR", + kind: FindingKind::Defect, + severity: FindingSeverity::Error, confidence: Some(1.0), - confidence_basis: Some("operator_override"), + confidence_basis: Some(FindingConfidenceBasis::OperatorOverride), message: format!( "Operator allowed unredacted secrets in {}", display_relative(project_root, file) @@ -176,6 +223,7 @@ pub(crate) fn pre_ingest( evidence: json!({ "file_path": display_relative(project_root, file), "override_used": true, + "detections": detections.iter().map(detection_audit_json).collect::>(), }), }); } @@ -194,25 +242,25 @@ fn confirm_override_if_needed( allowed: &[(PathBuf, Detection)], options: &SecretScanOptions, ) -> bool { - if allowed.is_empty() || !options.allow_unredacted_secrets { + if allowed.is_empty() { return false; } - if options.confirm_allow_unredacted_secrets.as_deref() == Some(CONFIRM_TOKEN) { - return true; - } - print_detection_list(project_root, allowed); - if options.confirm_allow_unredacted_secrets.is_some() { - abort_unconfirmed_override(); - } - if io::stdin().is_terminal() { - eprint!("Type '{CONFIRM_TOKEN}' to proceed: "); - let _ = io::stderr().flush(); - let mut input = String::new(); - if io::stdin().read_line(&mut input).is_ok() && input.trim() == CONFIRM_TOKEN { - return true; + match options.override_policy { + OverridePolicy::Forbid => false, + OverridePolicy::Preconfirmed(_) => true, + OverridePolicy::RequireInteractive => { + print_detection_list(project_root, allowed); + if io::stdin().is_terminal() { + eprint!("Type '{CONFIRM_TOKEN}' to proceed: "); + let _ = io::stderr().flush(); + let mut input = String::new(); + if io::stdin().read_line(&mut input).is_ok() && input.trim() == CONFIRM_TOKEN { + return true; + } + } + abort_unconfirmed_override(); } } - abort_unconfirmed_override(); } fn print_detection_list(project_root: &Path, allowed: &[(PathBuf, Detection)]) { @@ -226,13 +274,29 @@ fn print_detection_list(project_root: &Path, allowed: &[(PathBuf, Detection)]) { } } +fn detection_audit_json(detection: &Detection) -> serde_json::Value { + json!({ + "rule_id": detection.rule_id, + "rule": detection.detect_secrets_type.as_str(), + "line_number": detection.line_number, + "hashed_secret_hex": detection.hashed_secret.to_string(), + }) +} + fn abort_unconfirmed_override() -> ! { eprintln!("{OVERRIDE_UNCONFIRMED}: --allow-unredacted-secrets requires confirmation"); std::process::exit(78); } pub(super) fn canonical_or_original(path: &Path) -> PathBuf { - path.canonicalize().unwrap_or_else(|_| path.to_path_buf()) + path.canonicalize().unwrap_or_else(|err| { + tracing::warn!( + path = %path.display(), + error = %err, + "using non-canonical secret-scan path after canonicalization failed" + ); + path.to_path_buf() + }) } fn normalize_project_path(root: &Path, path: &Path) -> PathBuf { diff --git a/crates/clarion-cli/src/secret_scan/anchors.rs b/crates/clarion-cli/src/secret_scan/anchors.rs index 9f358864..775e909d 100644 --- a/crates/clarion-cli/src/secret_scan/anchors.rs +++ b/crates/clarion-cli/src/secret_scan/anchors.rs @@ -65,7 +65,7 @@ async fn ensure_finding_anchors( if let Some(reason) = outcome.briefing_blocks.get(&key) { properties.insert( "briefing_blocked".to_owned(), - serde_json::Value::String(reason.clone()), + serde_json::Value::String(reason.as_str().to_owned()), ); } let record = EntityRecord { diff --git a/crates/clarion-cli/src/secret_scan/baseline.rs b/crates/clarion-cli/src/secret_scan/baseline.rs index 793a8c21..3583882d 100644 --- a/crates/clarion-cli/src/secret_scan/baseline.rs +++ b/crates/clarion-cli/src/secret_scan/baseline.rs @@ -5,7 +5,9 @@ use clarion_scanner::{Baseline, BaselineError}; use serde_json::json; use super::normalize_project_path; -use crate::secret_scan::findings::PendingFinding; +use crate::secret_scan::findings::{ + FindingConfidenceBasis, FindingKind, FindingSeverity, PendingFinding, +}; const BASELINE_NO_JUSTIFICATION: &str = "CLA-INFRA-SECRET-BASELINE-NO-JUSTIFICATION"; const BASELINE_MATCH: &str = "CLA-INFRA-SECRET-BASELINE-MATCH"; @@ -21,10 +23,10 @@ pub(super) fn load_for_scan(project_root: &Path) -> Result<(Baseline, Vec bool { .to_str() .is_some_and(|name| SKIP_DIRS.contains(&name)) } + +#[cfg(test)] +mod tests { + use super::collect_scan_files; + use std::path::{Path, PathBuf}; + + #[test] + fn sidecar_walk_collects_dotenv_variants_and_skips_known_dirs() { + let tmp = tempfile::tempdir().expect("tempdir"); + let root = tmp.path(); + write(root.join("src/app.py"), "print('ok')\n"); + write(root.join(".env"), "TOKEN=one\n"); + write(root.join(".env.local"), "TOKEN=two\n"); + write(root.join(".env.production"), "TOKEN=three\n"); + write(root.join("nested/service.env"), "TOKEN=four\n"); + write(root.join("nested/.env"), "TOKEN=five\n"); + write(root.join("nested/not-env.txt"), "TOKEN=six\n"); + write(root.join(".clarion/.env"), "TOKEN=skip\n"); + write(root.join("node_modules/.env"), "TOKEN=skip\n"); + + let files = collect_scan_files(root, &[root.join("src/app.py")]); + let rel = relative_names(root, files); + + assert!(rel.contains(&".env".to_owned())); + assert!(rel.contains(&".env.local".to_owned())); + assert!(rel.contains(&".env.production".to_owned())); + assert!(rel.contains(&"nested/service.env".to_owned())); + assert!(rel.contains(&"nested/.env".to_owned())); + assert!(rel.contains(&"src/app.py".to_owned())); + assert!(!rel.contains(&"nested/not-env.txt".to_owned())); + assert!(!rel.contains(&".clarion/.env".to_owned())); + assert!(!rel.contains(&"node_modules/.env".to_owned())); + } + + #[cfg(unix)] + #[test] + fn sidecar_walk_does_not_follow_directory_symlinks() { + let tmp = tempfile::tempdir().expect("tempdir"); + let root = tmp.path().join("root"); + let outside = tmp.path().join("outside"); + std::fs::create_dir(&root).expect("create root"); + std::fs::create_dir(&outside).expect("create outside"); + write(outside.join(".env"), "TOKEN=outside\n"); + std::os::unix::fs::symlink(&outside, root.join("linked")).expect("symlink"); + + let files = collect_scan_files(&root, &[]); + let rel = relative_names(&root, files); + + assert!(!rel.contains(&"linked/.env".to_owned())); + } + + fn write(path: PathBuf, content: &str) { + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent).expect("create parent"); + } + std::fs::write(path, content).expect("write fixture"); + } + + fn relative_names(root: &Path, files: Vec) -> Vec { + let root = root.canonicalize().expect("canonical root"); + files + .into_iter() + .map(|path| { + path.strip_prefix(&root) + .expect("scan path under root") + .display() + .to_string() + }) + .collect() + } +} diff --git a/crates/clarion-cli/src/secret_scan/findings.rs b/crates/clarion-cli/src/secret_scan/findings.rs index 8c2faaf0..b1c14153 100644 --- a/crates/clarion-cli/src/secret_scan/findings.rs +++ b/crates/clarion-cli/src/secret_scan/findings.rs @@ -4,7 +4,7 @@ use std::{ }; use anyhow::{Context, Result}; -use clarion_scanner::Detection; +use clarion_scanner::{Detection, SecretCategory}; use clarion_storage::{ Writer, commands::{FindingRecord, WriterCmd}, @@ -20,14 +20,79 @@ const SECRET_DETECTED: &str = "CLA-SEC-SECRET-DETECTED"; pub(super) struct PendingFinding { pub(super) file_path: PathBuf, pub(super) rule_id: &'static str, - pub(super) kind: &'static str, - pub(super) severity: &'static str, + pub(super) kind: FindingKind, + pub(super) severity: FindingSeverity, pub(super) confidence: Option, - pub(super) confidence_basis: Option<&'static str>, + pub(super) confidence_basis: Option, pub(super) message: String, pub(super) evidence: serde_json::Value, } +#[allow(dead_code)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum FindingKind { + Defect, + Fact, + Classification, + Metric, + Suggestion, +} + +impl FindingKind { + fn as_str(self) -> &'static str { + match self { + Self::Defect => "defect", + Self::Fact => "fact", + Self::Classification => "classification", + Self::Metric => "metric", + Self::Suggestion => "suggestion", + } + } +} + +#[allow(dead_code)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum FindingSeverity { + Info, + Warn, + Error, + Critical, + None, +} + +impl FindingSeverity { + fn as_str(self) -> &'static str { + match self { + Self::Info => "INFO", + Self::Warn => "WARN", + Self::Error => "ERROR", + Self::Critical => "CRITICAL", + Self::None => "NONE", + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum FindingConfidenceBasis { + Pattern, + Entropy, + Baseline, + BaselineSchema, + OperatorOverride, +} + +impl FindingConfidenceBasis { + fn as_str(self) -> &'static str { + match self { + Self::Pattern => "pattern", + Self::Entropy => "entropy", + Self::Baseline => "baseline", + Self::BaselineSchema => "baseline_schema", + Self::OperatorOverride => "operator_override", + } + } +} + pub(crate) async fn emit_findings( writer: &Writer, run_id: &str, @@ -49,10 +114,13 @@ pub(crate) async fn emit_findings( tool_version: env!("CARGO_PKG_VERSION").to_owned(), run_id: run_id.to_owned(), rule_id: pending.rule_id.to_owned(), - kind: pending.kind.to_owned(), - severity: pending.severity.to_owned(), + kind: pending.kind.as_str().to_owned(), + severity: pending.severity.as_str().to_owned(), confidence: pending.confidence, - confidence_basis: pending.confidence_basis.map(str::to_owned), + confidence_basis: pending + .confidence_basis + .map(FindingConfidenceBasis::as_str) + .map(str::to_owned), entity_id, related_entities_json: "[]".to_owned(), message: pending.message.clone(), @@ -76,17 +144,17 @@ pub(super) fn secret_detected_finding(file: &Path, detection: &Detection) -> Pen PendingFinding { file_path: file.to_path_buf(), rule_id: SECRET_DETECTED, - kind: "defect", - severity: "ERROR", - confidence: if detection.rule_id.starts_with("HighEntropy") { + kind: FindingKind::Defect, + severity: FindingSeverity::Error, + confidence: if detection.category == SecretCategory::HighEntropy { Some(0.6) } else { Some(1.0) }, - confidence_basis: if detection.rule_id.starts_with("HighEntropy") { - Some("entropy") + confidence_basis: if detection.category == SecretCategory::HighEntropy { + Some(FindingConfidenceBasis::Entropy) } else { - Some("pattern") + Some(FindingConfidenceBasis::Pattern) }, message: format!( "{} detected in {}:{}", @@ -98,7 +166,7 @@ pub(super) fn secret_detected_finding(file: &Path, detection: &Detection) -> Pen "file_path": file, "line_number": detection.line_number, "rule": detection.rule_id, - "hashed_secret_hex": hex20(detection.hashed_secret), + "hashed_secret_hex": detection.hashed_secret.to_string(), }), } } @@ -107,19 +175,10 @@ fn finding_entity_id(file_path: &Path, anchors: &BTreeMap) -> O anchors.get(file_path).cloned() } -fn hex20(bytes: [u8; 20]) -> String { - const HEX: &[u8; 16] = b"0123456789abcdef"; - let mut out = String::with_capacity(40); - for byte in bytes { - out.push(char::from(HEX[usize::from(byte >> 4)])); - out.push(char::from(HEX[usize::from(byte & 0x0f)])); - } - out -} - #[cfg(test)] mod tests { - use super::finding_entity_id; + use super::{FindingConfidenceBasis, finding_entity_id, secret_detected_finding}; + use clarion_scanner::{DetectSecretsRule, Detection, HashedSecret, SecretCategory}; use std::{collections::BTreeMap, path::PathBuf}; #[test] @@ -135,4 +194,25 @@ mod tests { None ); } + + #[test] + fn confidence_basis_uses_detection_category_not_rule_id_prefix() { + let detection = Detection { + rule_id: "HighEntropyNamedPattern", + detect_secrets_type: DetectSecretsRule::AwsAccessKey, + category: SecretCategory::CloudCredential, + byte_offset: 0, + line_number: 7, + matched_len: 20, + hashed_secret: HashedSecret::from_bytes([1u8; 20]), + }; + + let finding = secret_detected_finding(PathBuf::from("demo.sec").as_path(), &detection); + + assert_eq!(finding.confidence, Some(1.0)); + assert_eq!( + finding.confidence_basis, + Some(FindingConfidenceBasis::Pattern) + ); + } } diff --git a/crates/clarion-cli/src/serve.rs b/crates/clarion-cli/src/serve.rs index d045f9d3..ab9544f8 100644 --- a/crates/clarion-cli/src/serve.rs +++ b/crates/clarion-cli/src/serve.rs @@ -131,7 +131,7 @@ fn build_llm_provider( model_id: config.llm.model_id.clone(), model: config.llm.codex_cli.model.clone(), profile: config.llm.codex_cli.profile.clone(), - sandbox: config.llm.codex_cli.sandbox.clone(), + sandbox: config.llm.codex_cli.sandbox.as_str().to_owned(), timeout_seconds: config.llm.codex_cli.timeout_seconds, }) .context("build Codex CLI LLM provider")?; @@ -143,7 +143,7 @@ fn build_llm_provider( project_root: project_root.to_owned(), model_id: config.llm.model_id.clone(), model: config.llm.claude_cli.model.clone(), - permission_mode: config.llm.claude_cli.permission_mode.clone(), + permission_mode: config.llm.claude_cli.permission_mode.as_str().to_owned(), tools: config.llm.claude_cli.tools.clone(), timeout_seconds: config.llm.claude_cli.timeout_seconds, max_turns: config.llm.claude_cli.max_turns, diff --git a/crates/clarion-cli/tests/secret_scan.rs b/crates/clarion-cli/tests/secret_scan.rs index 142c0880..56411610 100644 --- a/crates/clarion-cli/tests/secret_scan.rs +++ b/crates/clarion-cli/tests/secret_scan.rs @@ -2,6 +2,7 @@ use assert_cmd::Command; use rusqlite::Connection; +use sha1::{Digest, Sha1}; fn clarion_bin() -> Command { Command::cargo_bin("clarion").expect("clarion binary") @@ -136,6 +137,19 @@ fn conn(project: &std::path::Path) -> Connection { Connection::open(project.join(".clarion/clarion.db")).expect("open clarion db") } +fn sha1_hex(bytes: &[u8]) -> String { + const HEX: &[u8; 16] = b"0123456789abcdef"; + let mut hasher = Sha1::new(); + hasher.update(bytes); + let digest = hasher.finalize(); + let mut out = String::with_capacity(40); + for byte in digest { + out.push(char::from(HEX[usize::from(byte >> 4)])); + out.push(char::from(HEX[usize::from(byte & 0x0f)])); + } + out +} + #[test] fn clean_project_has_no_secret_findings() { let project = tempfile::tempdir().unwrap(); @@ -304,18 +318,21 @@ fn baseline_suppresses_secret_and_emits_audit_match() { b"aws_access_key_id = 'AKIAIOSFODNN7EXAMPLE'\n", ) .unwrap(); + let hashed_secret = sha1_hex(b"AKIAIOSFODNN7EXAMPLE"); std::fs::write( project.path().join(".clarion/secrets-baseline.yaml"), - r#" + format!( + r#" version: "1.0" results: "leaky.sec": - type: "AWS Access Key" - hashed_secret: "25910f981e85ca04baf359199dd0bd4a3ae738b6" + hashed_secret: "{hashed_secret}" line_number: 1 is_secret: false justification: "AWS documentation example key." -"#, +"# + ), ) .unwrap(); @@ -364,22 +381,25 @@ fn missing_baseline_justification_degrades_to_finding() { b"aws_access_key_id = 'AKIAIOSFODNN7EXAMPLE'\n", ) .unwrap(); + let hashed_secret = sha1_hex(b"AKIAIOSFODNN7EXAMPLE"); std::fs::write( project.path().join(".clarion/secrets-baseline.yaml"), - r#" + format!( + r#" version: "1.0" results: "leaky.sec": - type: "AWS Access Key" - hashed_secret: "25910f981e85ca04baf359199dd0bd4a3ae738b6" + hashed_secret: "{hashed_secret}" line_number: 1 is_secret: false "stale.sec": - type: "AWS Access Key" - hashed_secret: "25910f981e85ca04baf359199dd0bd4a3ae738b6" + hashed_secret: "{hashed_secret}" line_number: 9 is_secret: false -"#, +"# + ), ) .unwrap(); @@ -445,9 +465,24 @@ fn non_tty_override_confirmed_allows_briefing_and_records_stats() { |row| row.get(0), ) .unwrap(); + let evidence_json: String = db + .query_row( + "SELECT evidence FROM findings WHERE rule_id = 'CLA-SEC-UNREDACTED-SECRETS-ALLOWED'", + [], + |row| row.get(0), + ) + .unwrap(); + let evidence: serde_json::Value = + serde_json::from_str(&evidence_json).expect("override evidence JSON"); assert_eq!(blocked_count, 0); assert_eq!(override_count, 1); assert_eq!(override_used, 1); + assert_eq!(evidence["detections"][0]["rule_id"], "AwsAccessKeyId"); + assert_eq!(evidence["detections"][0]["line_number"], 1); + assert_eq!( + evidence["detections"][0]["hashed_secret_hex"], + sha1_hex(b"AKIAIOSFODNN7EXAMPLE") + ); } #[test] @@ -501,13 +536,182 @@ fn non_tty_override_with_wrong_confirmation_exits_78_before_run_start() { .code(78); let stderr = String::from_utf8_lossy(&assert.get_output().stderr); assert!(stderr.contains("CLA-INFRA-SECRET-OVERRIDE-UNCONFIRMED")); - assert!(stderr.contains("leaky.sec:1 AwsAccessKeyId")); let run_count: i64 = conn(project.path()) .query_row("SELECT COUNT(*) FROM runs", [], |row| row.get(0)) .unwrap(); assert_eq!(run_count, 0); } +#[test] +fn baseline_suppression_and_override_admission_are_audited_together() { + let project = tempfile::tempdir().unwrap(); + let plugin = tempfile::tempdir().unwrap(); + write_secret_fixture_plugin(plugin.path()); + install_project(project.path()); + std::fs::write( + project.path().join("fixture.sec"), + b"aws_access_key_id = 'AKIAIOSFODNN7EXAMPLE'\n", + ) + .unwrap(); + std::fs::write( + project.path().join("live.sec"), + b"aws_access_key_id = 'AKIAIOSFODNN7EXAMPLE'\n", + ) + .unwrap(); + let hashed_secret = sha1_hex(b"AKIAIOSFODNN7EXAMPLE"); + std::fs::write( + project.path().join(".clarion/secrets-baseline.yaml"), + format!( + r#" +version: "1.0" +results: + "fixture.sec": + - type: "AWS Access Key" + hashed_secret: "{hashed_secret}" + line_number: 1 + is_secret: false + justification: "AWS documentation example key." +"# + ), + ) + .unwrap(); + + clarion_bin() + .args([ + "analyze", + "--allow-unredacted-secrets", + "--confirm-allow-unredacted-secrets=yes-i-understand", + ]) + .arg(project.path()) + .env("PATH", plugin_path(plugin.path())) + .assert() + .success(); + + let db = conn(project.path()); + let baseline_count: i64 = db + .query_row( + "SELECT COUNT(*) FROM findings WHERE rule_id = 'CLA-INFRA-SECRET-BASELINE-MATCH'", + [], + |row| row.get(0), + ) + .unwrap(); + let override_count: i64 = db + .query_row( + "SELECT COUNT(*) FROM findings WHERE rule_id = 'CLA-SEC-UNREDACTED-SECRETS-ALLOWED'", + [], + |row| row.get(0), + ) + .unwrap(); + let secret_count: i64 = db + .query_row( + "SELECT COUNT(*) FROM findings WHERE rule_id = 'CLA-SEC-SECRET-DETECTED'", + [], + |row| row.get(0), + ) + .unwrap(); + let blocked_count: i64 = db + .query_row( + "SELECT COUNT(*) FROM entities WHERE json_extract(properties, '$.briefing_blocked') IS NOT NULL", + [], + |row| row.get(0), + ) + .unwrap(); + assert_eq!(baseline_count, 1); + assert_eq!(override_count, 1); + assert_eq!(secret_count, 0); + assert_eq!(blocked_count, 0); +} + +fn assert_invalid_baseline_aborts(raw_baseline: &str, expected_stderr: &str) { + let project = tempfile::tempdir().unwrap(); + let plugin = tempfile::tempdir().unwrap(); + write_secret_fixture_plugin(plugin.path()); + install_project(project.path()); + std::fs::write(project.path().join("leaky.sec"), b"nothing to see\n").unwrap(); + std::fs::write( + project.path().join(".clarion/secrets-baseline.yaml"), + raw_baseline, + ) + .unwrap(); + + let assert = clarion_bin() + .arg("analyze") + .arg(project.path()) + .env("PATH", plugin_path(plugin.path())) + .assert() + .failure(); + let stderr = String::from_utf8_lossy(&assert.get_output().stderr); + assert!( + stderr.contains("load secret baseline"), + "stderr did not include baseline context: {stderr}" + ); + assert!( + stderr.contains(expected_stderr), + "stderr did not include {expected_stderr:?}: {stderr}" + ); + let run_count: i64 = conn(project.path()) + .query_row("SELECT COUNT(*) FROM runs", [], |row| row.get(0)) + .unwrap(); + assert_eq!(run_count, 0); +} + +#[test] +fn invalid_baseline_path_aborts_analyze_with_context() { + assert_invalid_baseline_aborts( + r#" +version: "1.0" +results: + "../leaky.sec": + - type: "AWS Access Key" + hashed_secret: "0123456789abcdef0123456789abcdef01234567" + line_number: 1 + is_secret: false + justification: "Invalid path." +"#, + "repository-relative", + ); +} + +#[test] +fn invalid_baseline_hash_aborts_analyze_with_context() { + assert_invalid_baseline_aborts( + r#" +version: "1.0" +results: + "leaky.sec": + - type: "AWS Access Key" + hashed_secret: "not-a-sha1" + line_number: 1 + is_secret: false + justification: "Invalid hash." +"#, + "invalid hashed_secret", + ); +} + +#[test] +fn unsupported_baseline_version_aborts_analyze_with_context() { + assert_invalid_baseline_aborts( + r#" +version: "2.0" +results: {} +"#, + "baseline version mismatch", + ); +} + +#[test] +fn malformed_baseline_yaml_aborts_analyze_with_context() { + assert_invalid_baseline_aborts( + r#" +version: "1.0" +results: + "leaky.sec": [ +"#, + "baseline parse error", + ); +} + #[test] #[ignore = "TTY override confirmation needs an interactive terminal; WS-D owns the manual smoke."] fn tty_override_confirmation_manual_smoke() {} diff --git a/crates/clarion-core/Cargo.toml b/crates/clarion-core/Cargo.toml index b1c4fa6c..78c7a258 100644 --- a/crates/clarion-core/Cargo.toml +++ b/crates/clarion-core/Cargo.toml @@ -13,6 +13,7 @@ workspace = true reqwest.workspace = true serde.workspace = true serde_json.workspace = true +tempfile.workspace = true thiserror.workspace = true toml.workspace = true tracing.workspace = true diff --git a/crates/clarion-core/src/lib.rs b/crates/clarion-core/src/lib.rs index b2bba4d5..4294ae85 100644 --- a/crates/clarion-core/src/lib.rs +++ b/crates/clarion-core/src/lib.rs @@ -25,6 +25,7 @@ pub use plugin::{ AcceptedEntity, AnalyzeFileOutcome, AnalyzeFileStats, + BriefingBlockReason, CapExceeded, // breaker (Task 7) — callers drive crash-loop accounting CrashLoopBreaker, diff --git a/crates/clarion-core/src/llm_provider.rs b/crates/clarion-core/src/llm_provider.rs index 874a91a1..46fe0dab 100644 --- a/crates/clarion-core/src/llm_provider.rs +++ b/crates/clarion-core/src/llm_provider.rs @@ -6,7 +6,7 @@ use std::path::{Path, PathBuf}; use std::process::{Child, Command, ExitStatus, Stdio}; use std::sync::Mutex; use std::thread; -use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; +use std::time::{Duration, Instant}; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -527,12 +527,9 @@ impl LlmProvider for CodexCliProvider { } fn invoke(&self, request: LlmRequest) -> Result { - let output_path = codex_temp_path("clarion-codex-output", "json"); - let schema_path = codex_temp_path("clarion-codex-schema", "json"); - let result = self.invoke_with_temp_files(request, &output_path, &schema_path); - let _ = fs::remove_file(output_path); - let _ = fs::remove_file(schema_path); - result + let output_file = codex_temp_file("clarion-codex-output", ".json")?; + let schema_file = codex_temp_file("clarion-codex-schema", ".json")?; + self.invoke_with_temp_files(request, output_file.path(), schema_file.path()) } fn estimate_tokens(&self, request: &LlmRequest) -> u64 { @@ -673,7 +670,9 @@ impl LlmProvider for ClaudeCliProvider { if let Some(model) = &self.model { command.arg("--model").arg(model); } - command.arg("--tools").arg(self.tools.join(",")); + if !self.tools.is_empty() { + command.arg("--tools").arg(self.tools.join(",")); + } command .current_dir(&self.project_root) .stdin(Stdio::piped()) @@ -943,15 +942,18 @@ fn codex_status_retryable(status: ExitStatus) -> bool { cli_status_retryable(status) } -fn codex_temp_path(prefix: &str, extension: &str) -> PathBuf { - let nonce = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_nanos(); - std::env::temp_dir().join(format!( - "{prefix}-{}-{nonce}.{extension}", - std::process::id() - )) +fn codex_temp_file( + prefix: &str, + suffix: &str, +) -> Result { + tempfile::Builder::new() + .prefix(prefix) + .suffix(suffix) + .tempfile() + .map_err(|err| LlmProviderError::Cli { + message: format!("create Codex CLI temp file {prefix}: {err}"), + retryable: true, + }) } #[derive(Debug, Clone, Copy, Default, PartialEq)] @@ -2037,6 +2039,77 @@ printf '%s\n' '{{"type":"result","subtype":"success","structured_output":{{"purp assert!(log.contains("exclude_dynamic=1")); } + #[test] + fn claude_cli_provider_omits_tools_arg_when_no_tools_are_configured() { + use std::fs; + use std::os::unix::fs::PermissionsExt; + + let temp = tempfile::tempdir().expect("tempdir"); + let project_root = temp.path().join("project"); + fs::create_dir(&project_root).expect("project root"); + let fake_claude = temp.path().join("claude"); + let log_path = temp.path().join("claude.log"); + let script = format!( + r#"#!/usr/bin/env bash +set -euo pipefail +log="{log}" +saw_tools=0 +while [[ $# -gt 0 ]]; do + case "$1" in + --tools) + saw_tools=1 + shift 2 + ;; + -p|--print|--output-format|--json-schema|--permission-mode|--max-turns|--mcp-config) + shift 2 + ;; + --strict-mcp-config|--disable-slash-commands|--no-session-persistence|--exclude-dynamic-system-prompt-sections) + shift + ;; + *) + shift + ;; + esac +done +cat >/dev/null +echo "saw_tools=$saw_tools" >> "$log" +printf '%s\n' '{{"type":"result","subtype":"success","structured_output":{{"purpose":"via claude","behavior":"ran fake CLI","relationships":"","risks":""}},"usage":{{"input_tokens":1,"output_tokens":1,"total_tokens":2}},"total_cost_usd":0.0}}' +"#, + log = log_path.display() + ); + fs::write(&fake_claude, script).expect("write fake claude"); + let mut permissions = fs::metadata(&fake_claude).expect("metadata").permissions(); + permissions.set_mode(0o755); + fs::set_permissions(&fake_claude, permissions).expect("chmod fake claude"); + + let provider = ClaudeCliProvider::from_config(ClaudeCliProviderConfig { + executable: fake_claude.display().to_string(), + project_root, + model_id: "claude-code-default".to_owned(), + model: None, + permission_mode: "plan".to_owned(), + tools: Vec::new(), + timeout_seconds: 5, + max_turns: 2, + no_session_persistence: true, + exclude_dynamic_system_prompt_sections: true, + }) + .expect("construct Claude CLI provider"); + + provider + .invoke(LlmRequest { + purpose: LlmPurpose::Summary, + model_id: "claude-code-default".to_owned(), + prompt_id: LEAF_SUMMARY_PROMPT_TEMPLATE_ID.to_owned(), + prompt: "Summarize this function".to_owned(), + max_output_tokens: 512, + }) + .expect("invoke fake Claude CLI"); + + let log = fs::read_to_string(log_path).expect("read fake claude log"); + assert!(log.contains("saw_tools=0")); + } + #[test] fn claude_cli_output_parser_accepts_event_array_and_cache_reads() { let stdout = br#"[ diff --git a/crates/clarion-core/src/plugin/host.rs b/crates/clarion-core/src/plugin/host.rs index f476a586..384f0b54 100644 --- a/crates/clarion-core/src/plugin/host.rs +++ b/crates/clarion-core/src/plugin/host.rs @@ -415,11 +415,28 @@ where /// via stderr writes. stderr_tail: Option>>>, /// Canonical source paths whose entities must not be sent for LLM briefing. - briefing_blocks: BTreeMap, + briefing_blocks: BTreeMap, /// Canonical source paths that were covered by the core pre-ingest scanner. scanned_source_files: BTreeSet, } +/// File-level reason an entity must not be dispatched for LLM briefing. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum BriefingBlockReason { + SecretPresent, + UnscannedSource, +} + +impl BriefingBlockReason { + #[must_use] + pub fn as_str(self) -> &'static str { + match self { + Self::SecretPresent => "secret_present", + Self::UnscannedSource => "unscanned_source", + } + } +} + /// Size of the stderr ring buffer kept for diagnostics. 64 KiB holds /// ~800 lines of plugin output — enough for any realistic diagnostic /// tail while capping the plugin's ability to exfiltrate or `DoS` via @@ -695,9 +712,9 @@ impl PluginHost { } /// Install file-level briefing blocks produced by the core pre-ingest - /// scanner. Keys are canonical source paths and values are open-vocabulary - /// policy reasons such as `secret_present`. - pub fn set_briefing_blocks(&mut self, blocks: BTreeMap) { + /// scanner. Keys are canonical source paths and values are typed policy + /// reasons rendered into `properties_json` by [`BriefingBlockReason::as_str`]. + pub fn set_briefing_blocks(&mut self, blocks: BTreeMap) { self.briefing_blocks = blocks; } @@ -968,17 +985,14 @@ impl PluginHost { fn apply_briefing_block(&self, raw: &mut RawEntity, jailed: &str) { let jailed_path = Path::new(jailed); - let reason = self - .briefing_blocks - .get(jailed_path) - .map(String::as_str) - .or_else(|| { - (!self.scanned_source_files.contains(jailed_path)).then_some("unscanned_source") - }); + let reason = self.briefing_blocks.get(jailed_path).copied().or_else(|| { + (!self.scanned_source_files.contains(jailed_path)) + .then_some(BriefingBlockReason::UnscannedSource) + }); if let Some(reason) = reason { raw.extra.insert( "briefing_blocked".to_owned(), - serde_json::Value::String(reason.to_owned()), + serde_json::Value::String(reason.as_str().to_owned()), ); } } diff --git a/crates/clarion-core/src/plugin/mod.rs b/crates/clarion-core/src/plugin/mod.rs index 954d527c..83426955 100644 --- a/crates/clarion-core/src/plugin/mod.rs +++ b/crates/clarion-core/src/plugin/mod.rs @@ -26,8 +26,8 @@ pub mod transport; pub use breaker::{CrashLoopBreaker, CrashLoopState, FINDING_DISABLED_CRASH_LOOP}; pub use discovery::{DiscoveredPlugin, DiscoveryError, discover, discover_on_path}; pub use host::{ - AcceptedEdge, AcceptedEntity, AnalyzeFileOutcome, HostError, HostFinding, PluginHost, RawEdge, - RawEntity, + AcceptedEdge, AcceptedEntity, AnalyzeFileOutcome, BriefingBlockReason, HostError, HostFinding, + PluginHost, RawEdge, RawEntity, }; pub use jail::{JailError, jail, jail_to_string}; pub use limits::{ diff --git a/crates/clarion-mcp/Cargo.toml b/crates/clarion-mcp/Cargo.toml index 6e17c2eb..773662c7 100644 --- a/crates/clarion-mcp/Cargo.toml +++ b/crates/clarion-mcp/Cargo.toml @@ -21,6 +21,7 @@ serde_norway.workspace = true thiserror.workspace = true time.workspace = true tokio.workspace = true +tracing.workspace = true [dev-dependencies] tempfile.workspace = true diff --git a/crates/clarion-mcp/src/config.rs b/crates/clarion-mcp/src/config.rs index 7a24a518..e4afb73a 100644 --- a/crates/clarion-mcp/src/config.rs +++ b/crates/clarion-mcp/src/config.rs @@ -1,5 +1,5 @@ -use std::fs; use std::path::Path; +use std::{fs, net::SocketAddr}; use serde::Deserialize; use thiserror::Error; @@ -139,7 +139,7 @@ pub struct CodexCliConfig { pub executable: String, pub model: Option, pub profile: Option, - pub sandbox: String, + pub sandbox: CodexSandboxMode, pub timeout_seconds: u64, } @@ -149,18 +149,37 @@ impl Default for CodexCliConfig { executable: "codex".to_owned(), model: None, profile: None, - sandbox: "read-only".to_owned(), + sandbox: CodexSandboxMode::ReadOnly, timeout_seconds: 300, } } } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum CodexSandboxMode { + ReadOnly, + WorkspaceWrite, + DangerFullAccess, +} + +impl CodexSandboxMode { + #[must_use] + pub fn as_str(self) -> &'static str { + match self { + Self::ReadOnly => "read-only", + Self::WorkspaceWrite => "workspace-write", + Self::DangerFullAccess => "danger-full-access", + } + } +} + #[derive(Debug, Clone, PartialEq, Deserialize)] #[serde(default)] pub struct ClaudeCliConfig { pub executable: String, pub model: Option, - pub permission_mode: String, + pub permission_mode: ClaudePermissionMode, pub tools: Vec, pub timeout_seconds: u64, pub max_turns: u32, @@ -173,7 +192,7 @@ impl Default for ClaudeCliConfig { Self { executable: "claude".to_owned(), model: None, - permission_mode: "plan".to_owned(), + permission_mode: ClaudePermissionMode::Plan, tools: Vec::new(), timeout_seconds: 300, max_turns: 2, @@ -183,6 +202,30 @@ impl Default for ClaudeCliConfig { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] +pub enum ClaudePermissionMode { + #[serde(rename = "plan")] + Plan, + #[serde(rename = "default")] + Default, + #[serde(rename = "acceptEdits")] + AcceptEdits, + #[serde(rename = "bypassPermissions")] + BypassPermissions, +} + +impl ClaudePermissionMode { + #[must_use] + pub fn as_str(self) -> &'static str { + match self { + Self::Plan => "plan", + Self::Default => "default", + Self::AcceptEdits => "acceptEdits", + Self::BypassPermissions => "bypassPermissions", + } + } +} + #[derive(Debug, Clone, PartialEq, Default, Deserialize)] #[serde(default)] pub struct IntegrationsConfig { @@ -199,18 +242,28 @@ pub struct ServeConfig { #[serde(default)] pub struct HttpReadConfig { pub enabled: bool, - pub bind: String, + #[serde(deserialize_with = "deserialize_socket_addr")] + pub bind: SocketAddr, } impl Default for HttpReadConfig { fn default() -> Self { Self { enabled: false, - bind: "127.0.0.1:9111".to_owned(), + bind: SocketAddr::from(([127, 0, 0, 1], 9111)), } } } +fn deserialize_socket_addr<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + let raw = String::deserialize(deserializer)?; + raw.parse() + .map_err(|err| serde::de::Error::custom(format!("invalid serve.http.bind {raw:?}: {err}"))) +} + #[derive(Debug, Clone, PartialEq, Deserialize)] #[serde(default)] pub struct FiligreeConfig { @@ -465,7 +518,7 @@ llm_policy: assert_eq!(cfg.llm.codex_cli.executable, "/tmp/fake-codex"); assert_eq!(cfg.llm.codex_cli.model.as_deref(), Some("gpt-5.5")); assert_eq!(cfg.llm.codex_cli.profile.as_deref(), Some("clarion")); - assert_eq!(cfg.llm.codex_cli.sandbox, "read-only"); + assert_eq!(cfg.llm.codex_cli.sandbox, CodexSandboxMode::ReadOnly); assert_eq!(cfg.llm.codex_cli.timeout_seconds, 30); let selected = select_provider_with_env(&cfg, |_| None).expect("provider selection"); @@ -522,7 +575,10 @@ llm_policy: cfg.llm.claude_cli.model.as_deref(), Some("claude-sonnet-4-6") ); - assert_eq!(cfg.llm.claude_cli.permission_mode, "plan"); + assert_eq!( + cfg.llm.claude_cli.permission_mode, + ClaudePermissionMode::Plan + ); assert_eq!(cfg.llm.claude_cli.tools, vec!["Read", "Glob", "Grep"]); assert_eq!(cfg.llm.claude_cli.timeout_seconds, 45); assert_eq!(cfg.llm.claude_cli.max_turns, 2); @@ -554,6 +610,39 @@ llm_policy: assert_eq!(env_selected, ProviderSelection::ClaudeCli); } + #[test] + fn http_bind_is_parsed_when_config_loads() { + let cfg = McpConfig::from_yaml_str( + r#" +serve: + http: + enabled: true + bind: "127.0.0.1:0" +"#, + ) + .expect("parse HTTP bind"); + + assert_eq!(cfg.serve.http.bind, SocketAddr::from(([127, 0, 0, 1], 0))); + } + + #[test] + fn invalid_http_bind_fails_config_load() { + let err = McpConfig::from_yaml_str( + r#" +serve: + http: + enabled: true + bind: "not-a-socket" +"#, + ) + .expect_err("invalid bind should fail"); + + assert!( + err.to_string().contains("invalid serve.http.bind"), + "unexpected error: {err}" + ); + } + #[test] fn old_anthropic_provider_shape_reports_deprecated_provider() { let err = McpConfig::from_yaml_str( diff --git a/crates/clarion-mcp/src/lib.rs b/crates/clarion-mcp/src/lib.rs index 4ef978a5..9fadf2ce 100644 --- a/crates/clarion-mcp/src/lib.rs +++ b/crates/clarion-mcp/src/lib.rs @@ -856,8 +856,13 @@ impl ServerState { return Ok(InferredDispatchStats::default()); }; - if briefing_block_reason(&read.caller).is_some() { - return Ok(InferredDispatchStats::default()); + if let Some(reason) = briefing_block_reason(&read.caller) { + tracing::warn!( + caller_id = %caller_id, + briefing_blocked = %reason, + "skipping inferred-edge dispatch for briefing-blocked caller" + ); + return Ok(InferredDispatchStats::briefing_blocked()); } if let Some(cached) = read.cached.clone() { @@ -1690,6 +1695,7 @@ struct InferredDispatchStats { /// the persisted edge set to avoid the FK violation that previously /// poisoned the cache row and re-burned LLM tokens on retry. unresolved_targets_dropped_total: u64, + briefing_blocked_total: u64, candidate_callers_considered: u64, coalesced_waits_total: u64, tokens_input: i64, @@ -1723,12 +1729,20 @@ impl InferredDispatchStats { } } + fn briefing_blocked() -> Self { + Self { + briefing_blocked_total: 1, + ..Self::default() + } + } + fn merge(&mut self, other: &Self) { self.cache_hits_total += other.cache_hits_total; self.cache_misses_total += other.cache_misses_total; self.edges_materialized_total += other.edges_materialized_total; self.edges_skipped_static_duplicates_total += other.edges_skipped_static_duplicates_total; self.unresolved_targets_dropped_total += other.unresolved_targets_dropped_total; + self.briefing_blocked_total += other.briefing_blocked_total; self.candidate_callers_considered += other.candidate_callers_considered; self.coalesced_waits_total += other.coalesced_waits_total; self.tokens_input += other.tokens_input; @@ -1745,6 +1759,7 @@ impl InferredDispatchStats { "inferred_edges_materialized_total": self.edges_materialized_total, "inferred_edges_skipped_static_duplicates_total": self.edges_skipped_static_duplicates_total, "inferred_unresolved_targets_dropped_total": self.unresolved_targets_dropped_total, + "inferred_dispatch_briefing_blocked_total": self.briefing_blocked_total, "inferred_candidate_callers_considered": self.candidate_callers_considered, "inferred_dispatch_coalesced_total": self.coalesced_waits_total, "inferred_tokens_input": self.tokens_input, diff --git a/crates/clarion-mcp/tests/storage_tools.rs b/crates/clarion-mcp/tests/storage_tools.rs index d3bd37b4..85463352 100644 --- a/crates/clarion-mcp/tests/storage_tools.rs +++ b/crates/clarion-mcp/tests/storage_tools.rs @@ -878,6 +878,55 @@ async fn summary_on_secret_blocked_entity_returns_policy_envelope_without_llm_or assert_eq!(cache_rows, 0); } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn summary_on_unscanned_source_returns_policy_envelope_without_llm_or_cache() { + let (project, db_path) = open_project(); + let conn = Connection::open(&db_path).expect("open sqlite"); + conn.execute( + "UPDATE entities SET properties = ?1 WHERE id = 'python:function:demo.entry'", + params![json!({"briefing_blocked": "unscanned_source"}).to_string()], + ) + .expect("mark entity blocked"); + drop(conn); + + let (writer, handle) = Writer::spawn(db_path.clone(), 50, 256).unwrap(); + let provider = Arc::new(AnySummaryProvider::new_output( + r#"{"purpose":"should not run"}"#, + 120, + 0.012, + )); + let state = state_for_summary( + project.path(), + &db_path, + &writer, + provider.clone(), + llm_config(), + ); + + let envelope = call_tool( + &state, + "summary", + json!({"id": "python:function:demo.entry"}), + ) + .await; + + assert_eq!(envelope["ok"], true); + assert_eq!(envelope["result"]["summary"], Value::Null); + assert_eq!(envelope["result"]["briefing_blocked"], "unscanned_source"); + assert!( + envelope["result"]["remediation"] + .as_str() + .expect("remediation text") + .contains("not covered by the pre-ingest secret scan") + ); + assert_eq!(envelope["stats_delta"], json!({})); + assert!(provider.invocations().is_empty()); + + drop(state); + drop(writer); + handle.await.unwrap().unwrap(); +} + #[tokio::test] async fn issues_for_returns_unavailable_when_filigree_disabled() { let (project, db_path) = open_project(); @@ -1759,6 +1808,10 @@ async fn callers_of_inferred_skips_briefing_blocked_callers_without_llm() { assert_eq!(envelope["ok"], true); assert_eq!(envelope["result"]["callers"].as_array().unwrap().len(), 0); + assert_eq!( + envelope["stats_delta"]["inferred_dispatch_briefing_blocked_total"], + 1 + ); assert!(provider.invocations().is_empty()); drop(state); diff --git a/crates/clarion-scanner/src/baseline.rs b/crates/clarion-scanner/src/baseline.rs index f65013b9..3a1c30ed 100644 --- a/crates/clarion-scanner/src/baseline.rs +++ b/crates/clarion-scanner/src/baseline.rs @@ -6,7 +6,7 @@ use std::{ use serde::{Deserialize, Serialize}; -use crate::{Detection, hex_decode_20}; +use crate::{DetectSecretsRule, Detection, HashedSecret}; #[derive(Debug, Clone, PartialEq, Eq)] pub struct Baseline { @@ -16,8 +16,8 @@ pub struct Baseline { #[derive(Debug, Clone, PartialEq, Eq)] pub struct BaselineEntry { - pub rule_type: String, - pub hashed_secret: [u8; 20], + pub rule_type: DetectSecretsRule, + pub hashed_secret: HashedSecret, pub line_number: u32, pub is_secret: bool, pub justification: String, @@ -56,6 +56,12 @@ pub enum BaselineError { line: u32, details: String, }, + #[error("baseline entry has unsupported detector type at {file}:{line}: {rule_type:?}")] + UnsupportedRuleType { + file: PathBuf, + line: u32, + rule_type: String, + }, #[error("baseline parse error: {0}")] Parse(#[from] serde_norway::Error), #[error("baseline I/O error: {0}")] @@ -113,7 +119,7 @@ impl Baseline { { let key = ( (*baseline_path).clone(), - entry.rule_type.clone(), + entry.rule_type, entry.hashed_secret, entry.line_number, ); @@ -170,15 +176,24 @@ impl Baseline { let mut converted = Vec::new(); for entry in raw_entries { let justification = entry.justification.unwrap_or_default(); - let hashed_secret = hex_decode_20(&entry.hashed_secret).map_err(|details| { - BaselineError::InvalidHash { - file: file.clone(), - line: entry.line_number, - details, - } - })?; + let rule_type = + DetectSecretsRule::try_from(entry.rule_type.as_str()).map_err(|_| { + BaselineError::UnsupportedRuleType { + file: file.clone(), + line: entry.line_number, + rule_type: entry.rule_type.clone(), + } + })?; + let hashed_secret = + HashedSecret::from_hex(&entry.hashed_secret).map_err(|details| { + BaselineError::InvalidHash { + file: file.clone(), + line: entry.line_number, + details: details.to_string(), + } + })?; converted.push(BaselineEntry { - rule_type: entry.rule_type, + rule_type, hashed_secret, line_number: entry.line_number, is_secret: entry.is_secret, @@ -252,8 +267,8 @@ impl From<&Baseline> for RawBaseline { entries .iter() .map(|entry| RawBaselineEntry { - rule_type: entry.rule_type.clone(), - hashed_secret: crate::hex_encode(&entry.hashed_secret), + rule_type: entry.rule_type.as_str().to_owned(), + hashed_secret: entry.hashed_secret.to_string(), line_number: entry.line_number, is_secret: entry.is_secret, justification: Some(entry.justification.clone()), diff --git a/crates/clarion-scanner/src/lib.rs b/crates/clarion-scanner/src/lib.rs index a9e64b53..7e2b2bbc 100644 --- a/crates/clarion-scanner/src/lib.rs +++ b/crates/clarion-scanner/src/lib.rs @@ -14,17 +14,21 @@ pub use baseline::{ }; pub use entropy::EntropyTuning; pub use patterns::{PatternMeta, Scanner}; +use std::{ + fmt::{self, Write as _}, + str::FromStr, +}; /// One secret-like match in one file buffer. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Detection { pub rule_id: &'static str, - pub detect_secrets_type: &'static str, + pub detect_secrets_type: DetectSecretsRule, pub category: SecretCategory, pub byte_offset: usize, pub line_number: u32, pub matched_len: usize, - pub hashed_secret: [u8; 20], + pub hashed_secret: HashedSecret, } /// High-level category used for evidence and future operator grouping. @@ -41,12 +45,152 @@ pub enum SecretCategory { ContextualCredential, } -fn sha1_digest(bytes: &[u8]) -> [u8; 20] { +/// detect-secrets-compatible SHA-1 digest of the matched secret bytes. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct HashedSecret([u8; 20]); + +impl HashedSecret { + #[must_use] + pub fn from_bytes(bytes: [u8; 20]) -> Self { + Self(bytes) + } + + #[must_use] + pub fn as_bytes(&self) -> &[u8; 20] { + &self.0 + } + + pub fn from_hex(input: &str) -> Result { + let raw = input.trim(); + if raw.len() != 40 { + return Err(HexDigestError { + message: format!("expected 40 hex characters, got {}", raw.len()), + }); + } + let mut out = [0u8; 20]; + for (idx, chunk) in raw.as_bytes().chunks_exact(2).enumerate() { + let hi = hex_value(chunk[0]).ok_or_else(|| HexDigestError { + message: format!("invalid hex at byte {}", idx * 2), + })?; + let lo = hex_value(chunk[1]).ok_or_else(|| HexDigestError { + message: format!("invalid hex at byte {}", idx * 2 + 1), + })?; + out[idx] = (hi << 4) | lo; + } + Ok(Self(out)) + } +} + +impl fmt::Display for HashedSecret { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + const HEX: &[u8; 16] = b"0123456789abcdef"; + for byte in self.0 { + f.write_char(char::from(HEX[usize::from(byte >> 4)]))?; + f.write_char(char::from(HEX[usize::from(byte & 0x0f)]))?; + } + Ok(()) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] +#[error("{message}")] +pub struct HexDigestError { + message: String, +} + +/// Closed detector type vocabulary supported by Clarion's v0.1 scanner. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum DetectSecretsRule { + AwsAccessKey, + AwsSecretAccessKey, + GitHubToken, + GitHubFineGrainedToken, + GitHubOAuthToken, + AnthropicApiKey, + OpenAiApiKey, + StripeApiKey, + SlackToken, + JwtToken, + PrivateKey, + KeywordDetector, + Base64HighEntropyString, + HexHighEntropyString, +} + +impl DetectSecretsRule { + #[must_use] + pub fn as_str(self) -> &'static str { + match self { + Self::AwsAccessKey => "AWS Access Key", + Self::AwsSecretAccessKey => "AWS Secret Access Key", + Self::GitHubToken => "GitHub Token", + Self::GitHubFineGrainedToken => "GitHub Fine-Grained Token", + Self::GitHubOAuthToken => "GitHub OAuth Token", + Self::AnthropicApiKey => "Anthropic API Key", + Self::OpenAiApiKey => "OpenAI API Key", + Self::StripeApiKey => "Stripe API Key", + Self::SlackToken => "Slack Token", + Self::JwtToken => "JWT Token", + Self::PrivateKey => "Private Key", + Self::KeywordDetector => "Keyword Detector", + Self::Base64HighEntropyString => "Base64 High Entropy String", + Self::HexHighEntropyString => "Hex High Entropy String", + } + } +} + +impl fmt::Display for DetectSecretsRule { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + +impl FromStr for DetectSecretsRule { + type Err = UnknownDetectSecretsRule; + + fn from_str(value: &str) -> Result { + match value { + "AWS Access Key" => Ok(Self::AwsAccessKey), + "AWS Secret Access Key" => Ok(Self::AwsSecretAccessKey), + "GitHub Token" => Ok(Self::GitHubToken), + "GitHub Fine-Grained Token" => Ok(Self::GitHubFineGrainedToken), + "GitHub OAuth Token" => Ok(Self::GitHubOAuthToken), + "Anthropic API Key" => Ok(Self::AnthropicApiKey), + "OpenAI API Key" => Ok(Self::OpenAiApiKey), + "Stripe API Key" => Ok(Self::StripeApiKey), + "Slack Token" => Ok(Self::SlackToken), + "JWT Token" => Ok(Self::JwtToken), + "Private Key" => Ok(Self::PrivateKey), + "Keyword Detector" => Ok(Self::KeywordDetector), + "Base64 High Entropy String" => Ok(Self::Base64HighEntropyString), + "Hex High Entropy String" => Ok(Self::HexHighEntropyString), + _ => Err(UnknownDetectSecretsRule { + value: value.to_owned(), + }), + } + } +} + +impl TryFrom<&str> for DetectSecretsRule { + type Error = UnknownDetectSecretsRule; + + fn try_from(value: &str) -> Result { + Self::from_str(value) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] +#[error("unsupported detector type {value:?}")] +pub struct UnknownDetectSecretsRule { + value: String, +} + +fn sha1_digest(bytes: &[u8]) -> HashedSecret { use sha1::{Digest, Sha1}; let mut hasher = Sha1::new(); hasher.update(bytes); - hasher.finalize().into() + HashedSecret(hasher.finalize().into()) } fn line_number_for_offset(buf: &[u8], offset: usize) -> u32 { @@ -59,31 +203,6 @@ fn line_number_for_offset(buf: &[u8], offset: usize) -> u32 { u32::try_from(line).unwrap_or(u32::MAX) } -fn hex_encode(bytes: &[u8; 20]) -> String { - const HEX: &[u8; 16] = b"0123456789abcdef"; - let mut out = String::with_capacity(bytes.len() * 2); - for byte in bytes { - out.push(char::from(HEX[usize::from(byte >> 4)])); - out.push(char::from(HEX[usize::from(byte & 0x0f)])); - } - out -} - -fn hex_decode_20(input: &str) -> Result<[u8; 20], String> { - let raw = input.trim(); - if raw.len() != 40 { - return Err(format!("expected 40 hex characters, got {}", raw.len())); - } - let mut out = [0u8; 20]; - for (idx, chunk) in raw.as_bytes().chunks_exact(2).enumerate() { - let hi = hex_value(chunk[0]).ok_or_else(|| format!("invalid hex at byte {}", idx * 2))?; - let lo = - hex_value(chunk[1]).ok_or_else(|| format!("invalid hex at byte {}", idx * 2 + 1))?; - out[idx] = (hi << 4) | lo; - } - Ok(out) -} - fn hex_value(byte: u8) -> Option { match byte { b'0'..=b'9' => Some(byte - b'0'), diff --git a/crates/clarion-scanner/src/patterns.rs b/crates/clarion-scanner/src/patterns.rs index e9fd266a..31dc1786 100644 --- a/crates/clarion-scanner/src/patterns.rs +++ b/crates/clarion-scanner/src/patterns.rs @@ -1,14 +1,15 @@ use regex::bytes::{Regex, RegexSet}; use crate::{ - Detection, SecretCategory, entropy::EntropyTuning, line_number_for_offset, sha1_digest, + DetectSecretsRule, Detection, SecretCategory, entropy::EntropyTuning, line_number_for_offset, + sha1_digest, }; /// Metadata for one named secret detector. #[derive(Debug, Clone)] pub struct PatternMeta { pub rule_id: &'static str, - pub detect_secrets_type: &'static str, + pub detect_secrets_type: DetectSecretsRule, pub category: SecretCategory, pub pattern: &'static str, capture_group: Option, @@ -139,7 +140,7 @@ impl Scanner { { detections.push(entropy_detection( "HighEntropyBase64", - "Base64 High Entropy String", + DetectSecretsRule::Base64HighEntropyString, bytes, candidate.start(), candidate.end(), @@ -153,7 +154,7 @@ impl Scanner { { detections.push(entropy_detection( "HighEntropyHex", - "Hex High Entropy String", + DetectSecretsRule::HexHighEntropyString, bytes, candidate.start(), candidate.end(), @@ -178,7 +179,7 @@ fn detection_from_match(meta: &PatternMeta, bytes: &[u8], start: usize, end: usi fn entropy_detection( rule_id: &'static str, - detect_secrets_type: &'static str, + detect_secrets_type: DetectSecretsRule, bytes: &[u8], start: usize, end: usize, @@ -198,84 +199,84 @@ fn default_pattern_meta() -> Vec { vec![ PatternMeta { rule_id: "AwsAccessKeyId", - detect_secrets_type: "AWS Access Key", + detect_secrets_type: DetectSecretsRule::AwsAccessKey, category: SecretCategory::CloudCredential, pattern: r"\b(?:AKIA|ASIA)[0-9A-Z]{16}\b", capture_group: None, }, PatternMeta { rule_id: "AwsSecretAccessKey", - detect_secrets_type: "AWS Secret Access Key", + detect_secrets_type: DetectSecretsRule::AwsSecretAccessKey, category: SecretCategory::CloudCredential, pattern: r#"(?i)\baws[^:=\n]{0,32}(?:secret|access)[^:=\n]{0,32}(?:=|:|:=)\s*["']?([A-Za-z0-9/+=]{40})["']?"#, capture_group: Some(1), }, PatternMeta { rule_id: "GitHubPat", - detect_secrets_type: "GitHub Token", + detect_secrets_type: DetectSecretsRule::GitHubToken, category: SecretCategory::VcsCredential, pattern: r"\bghp_[A-Za-z0-9]{36}\b", capture_group: None, }, PatternMeta { rule_id: "GitHubFineGrainedPat", - detect_secrets_type: "GitHub Fine-Grained Token", + detect_secrets_type: DetectSecretsRule::GitHubFineGrainedToken, category: SecretCategory::VcsCredential, pattern: r"\bgithub_pat_[A-Za-z0-9_]{82,}\b", capture_group: None, }, PatternMeta { rule_id: "GitHubOAuth", - detect_secrets_type: "GitHub OAuth Token", + detect_secrets_type: DetectSecretsRule::GitHubOAuthToken, category: SecretCategory::VcsCredential, pattern: r"\bgh[ousr]_[A-Za-z0-9]{36}\b", capture_group: None, }, PatternMeta { rule_id: "AnthropicApiKey", - detect_secrets_type: "Anthropic API Key", + detect_secrets_type: DetectSecretsRule::AnthropicApiKey, category: SecretCategory::AiProviderCredential, pattern: r"\bsk-ant-[A-Za-z0-9_-]{90,}\b", capture_group: None, }, PatternMeta { rule_id: "OpenAiApiKey", - detect_secrets_type: "OpenAI API Key", + detect_secrets_type: DetectSecretsRule::OpenAiApiKey, category: SecretCategory::AiProviderCredential, pattern: r"\bsk-[A-Za-z0-9]{48}\b", capture_group: None, }, PatternMeta { rule_id: "StripeApiKey", - detect_secrets_type: "Stripe API Key", + detect_secrets_type: DetectSecretsRule::StripeApiKey, category: SecretCategory::PaymentsCredential, pattern: r"\b(?:sk|pk|rk)_(?:live|test)_[A-Za-z0-9]{16,}\b", capture_group: None, }, PatternMeta { rule_id: "SlackToken", - detect_secrets_type: "Slack Token", + detect_secrets_type: DetectSecretsRule::SlackToken, category: SecretCategory::MessagingCredential, pattern: r"\bxox[baprs]-[A-Za-z0-9-]{10,}\b", capture_group: None, }, PatternMeta { rule_id: "JwtToken", - detect_secrets_type: "JWT Token", + detect_secrets_type: DetectSecretsRule::JwtToken, category: SecretCategory::JwtToken, pattern: r"\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b", capture_group: None, }, PatternMeta { rule_id: "PrivateKeyHeader", - detect_secrets_type: "Private Key", + detect_secrets_type: DetectSecretsRule::PrivateKey, category: SecretCategory::PrivateKey, pattern: r"-----BEGIN (?:(?:RSA|EC|DSA|OPENSSH|ENCRYPTED) PRIVATE KEY|PRIVATE KEY|PGP PRIVATE KEY BLOCK)-----", capture_group: None, }, PatternMeta { rule_id: "ContextualCredential", - detect_secrets_type: "Keyword Detector", + detect_secrets_type: DetectSecretsRule::KeywordDetector, category: SecretCategory::ContextualCredential, pattern: r#"(?i)(?:^|[^A-Za-z0-9_-])(?:password|passwd|secret[_-]?token|secret|token|api[_-]?key)\s*(?:=|:=|:)\s*["']([^"'\s]{8,})["']"#, capture_group: Some(1), diff --git a/crates/clarion-scanner/tests/scanner.rs b/crates/clarion-scanner/tests/scanner.rs index c1edfc54..0537d285 100644 --- a/crates/clarion-scanner/tests/scanner.rs +++ b/crates/clarion-scanner/tests/scanner.rs @@ -1,4 +1,6 @@ -use clarion_scanner::{Baseline, BaselineError, Scanner, load_baseline}; +use clarion_scanner::{ + Baseline, BaselineError, EntropyTuning, HashedSecret, Scanner, load_baseline, +}; use sha1::{Digest, Sha1}; fn rules_for(input: &str) -> Vec<&'static str> { @@ -140,6 +142,17 @@ fn contextual_credentials_detect_assignments_but_not_comments_or_hashes() { "ContextualCredential", ); assert_not_detects("# password = \"abcDEF1234567890\"", "ContextualCredential"); + assert_detects("// password = \"abcDEF1234567890\"", "ContextualCredential"); + assert_detects( + "/* password = \"abcDEF1234567890\" */", + "ContextualCredential", + ); +} + +#[test] +fn entropy_minimum_lengths_are_pinned() { + assert_eq!(EntropyTuning::BASE64.min_len, 20); + assert_eq!(EntropyTuning::HEX.min_len, 40); } #[test] @@ -151,7 +164,16 @@ fn detection_records_line_and_sha1_hash_without_literal() { .expect("AWS key detection"); assert_eq!(detection.line_number, 2); assert_eq!(detection.matched_len, "AKIAIOSFODNN7EXAMPLE".len()); - assert_ne!(detection.hashed_secret, [0u8; 20]); + assert_ne!(detection.hashed_secret.as_bytes(), &[0u8; 20]); +} + +#[test] +fn hashed_secret_round_trips_hex_display() { + let hash = HashedSecret::from_hex("0123456789abcdef0123456789abcdef01234567") + .expect("valid SHA-1 hex"); + + assert_eq!(hash.to_string(), "0123456789abcdef0123456789abcdef01234567"); + assert!(HashedSecret::from_hex("not-a-sha1").is_err()); } #[test] @@ -234,6 +256,28 @@ results: assert!(result.fired_entries.is_empty()); } +#[test] +fn baseline_rejects_unknown_detector_types() { + let err = Baseline::from_yaml_str( + r#" +version: "1.0" +results: + "src/demo.py": + - type: "AWS Acceess Key" + hashed_secret: "0123456789abcdef0123456789abcdef01234567" + line_number: 42 + is_secret: false + justification: "Typo should not silently lose suppression." +"#, + ) + .expect_err("unknown detector type should fail"); + + assert!( + err.to_string().contains("unsupported detector type"), + "unexpected error: {err}" + ); +} + #[test] fn baseline_without_explicit_is_secret_false_does_not_suppress() { let scanner = Scanner::new(); @@ -438,14 +482,8 @@ results: assert_eq!(parsed, reparsed); } -fn hex20(bytes: [u8; 20]) -> String { - const HEX: &[u8; 16] = b"0123456789abcdef"; - let mut out = String::new(); - for byte in bytes { - out.push(char::from(HEX[usize::from(byte >> 4)])); - out.push(char::from(HEX[usize::from(byte & 0x0f)])); - } - out +fn hex20(hash: HashedSecret) -> String { + hash.to_string() } fn sha1_hex(bytes: &[u8]) -> String { diff --git a/docs/implementation/v0.1-publish/ws-a-secret-scanner.md b/docs/implementation/v0.1-publish/ws-a-secret-scanner.md index cd0152f6..f10c5586 100644 --- a/docs/implementation/v0.1-publish/ws-a-secret-scanner.md +++ b/docs/implementation/v0.1-publish/ws-a-secret-scanner.md @@ -8,10 +8,9 @@ **Requirements floor**: `NFR-SEC-01` (pre-ingest scan + block + baseline), `NFR-SEC-04` (security events as findings), `NFR-OPS-01` / `NFR-OPS-04` (single-binary distribution). **Effort estimate**: 6–9 working days at agentic velocity. Tasks 1 and 2 parallelisable; 3 → 4 → 5 sequential; 6 + 7 parallel with any. -**Implementation accuracy note**: code-path references and line numbers in -this planning package were last checked on 2026-05-19 against the `scanner` -branch. Treat symbol names as authoritative and line numbers as historical -orientation only. +**Accuracy as of 2026-05-19**: code-path references and line numbers in this +planning package were last checked against the `scanner` branch. Treat symbol +names as authoritative and line numbers as historical orientation only. --- diff --git a/docs/operator/README.md b/docs/operator/README.md index 0bda066d..dec3f2c5 100644 --- a/docs/operator/README.md +++ b/docs/operator/README.md @@ -10,3 +10,5 @@ Practical notes for configuring and running Clarion. `clarion analyze` concurrency against one `.clarion/clarion.db`. - [Secret scanning](./secret-scanning.md) — pre-ingest scanner behavior, baseline false-positive workflow, override confirmation, and audit queries. +- [Federation contracts](../federation/contracts.md) — read-side HTTP + contracts consumed by sibling products such as Filigree. diff --git a/docs/operator/secret-scanning.md b/docs/operator/secret-scanning.md index d8a65fbe..17fa26d5 100644 --- a/docs/operator/secret-scanning.md +++ b/docs/operator/secret-scanning.md @@ -31,7 +31,7 @@ A matching baseline entry suppresses the block and records `CLA-INFRA-SECRET-BAS Use `--allow-unredacted-secrets` only when you deliberately accept that detected secrets may reach the LLM provider. -Interactive runs prompt for: +When detections exist, interactive runs prompt for: ```text yes-i-understand From dd07cc5ab9d42d22ec4bd895a01536c1c86d0d25 Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Tue, 19 May 2026 07:33:15 +1000 Subject: [PATCH 009/187] =?UTF-8?q?docs:=20currency=20pass=20=E2=80=94=20r?= =?UTF-8?q?efresh=20stale=20facts=20and=20references=20in=20retained=20doc?= =?UTF-8?q?set?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Touch only retained release-facing docs (docs/{suite,clarion/v0.1,clarion/adr}/); no decision changes. Edits are status-line, link-target, or path-citation corrections reflecting state since the 2026-04-18 design baseline. Suite: - loom.md §9: Clarion status was "Designed; implementation not yet started" — update to walking-skeleton-merged / v0.1-build-in-flight to match the briefing.md fix. ADR cross-references that were "(pending)" for ADRs since Accepted: - ADR-011 → ADR-005 (now Accepted) - ADR-012 → ADR-005 (now Accepted) - ADR-021 → ADR-012 (now Accepted) ADR body-text path citations updated to the centralized archive layout: - ADR-024: "docs/superpowers/plans/ and docs/clarion/v0.1/reviews/" → "docs/implementation/ (agent-plans/, v0.1-reviews/)" - ADR-026: "docs/superpowers/handoffs/..." → "docs/implementation/handoffs/..." - ADR-015: inline-code v0.1-scope-commitments.md path → new archive path - ADR-023, adr/README.md: stale link display-text matched to relocated URL. v0.1 design docset: - system-design.md §12 "Writing cadence": ADR backlog list was 2026-04-18 baseline state; reflect that ADR-023–ADR-032 are post-baseline and live in the ADR index, ADR-008 is Superseded. - detailed-design.md §11: add coverage note that the navigation table reflects baseline ADR-001–ADR-022 only; refer post-baseline ADRs to the index. - requirements.md, detailed-design.md preambles: add ADR-README pointer; label pre-restructure reviews as archived. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/clarion/adr/ADR-011-writer-actor-concurrency.md | 2 +- docs/clarion/adr/ADR-012-http-auth-default.md | 2 +- docs/clarion/adr/ADR-015-wardline-filigree-emission.md | 2 +- docs/clarion/adr/ADR-021-plugin-authority-hybrid.md | 2 +- docs/clarion/adr/ADR-023-tooling-baseline.md | 2 +- docs/clarion/adr/ADR-024-guidance-schema-vocabulary.md | 2 +- .../adr/ADR-026-containment-wire-and-edge-identity.md | 2 +- docs/clarion/adr/README.md | 2 +- docs/clarion/v0.1/detailed-design.md | 7 +++++-- docs/clarion/v0.1/requirements.md | 5 +++-- docs/clarion/v0.1/system-design.md | 2 +- docs/suite/loom.md | 2 +- 12 files changed, 18 insertions(+), 14 deletions(-) diff --git a/docs/clarion/adr/ADR-011-writer-actor-concurrency.md b/docs/clarion/adr/ADR-011-writer-actor-concurrency.md index 4acc469c..03932b1b 100644 --- a/docs/clarion/adr/ADR-011-writer-actor-concurrency.md +++ b/docs/clarion/adr/ADR-011-writer-actor-concurrency.md @@ -168,7 +168,7 @@ Already rejected in ADR-001 for the storage-engine selection. Re-cited here beca ## Related Decisions - [ADR-001](./ADR-001-rust-for-core.md) — Rust + `rusqlite` + `tokio` is the framework this ADR lives inside. `deadpool-sqlite` is named in ADR-001's ecosystem argument. -- ADR-005 (pending) — `.clarion/` git-committable default. `clarion.db.new` (shadow-DB intermediate) must be `.gitignore`d; ADR-005 picks the exact ignore rules. +- [ADR-005](./ADR-005-clarion-dir-tracking.md) — `.clarion/` git-committable default. `clarion.db.new` (shadow-DB intermediate) must be `.gitignore`d; ADR-005 picks the exact ignore rules. - [ADR-021](./ADR-021-plugin-authority-hybrid.md) — the per-run entity-count cap (Layer 2d) interacts with transaction granularity: the cap triggers a final flush and abort, which lands cleanly because per-N-files keeps the write path predictable. ## References diff --git a/docs/clarion/adr/ADR-012-http-auth-default.md b/docs/clarion/adr/ADR-012-http-auth-default.md index a25bd335..f4676df7 100644 --- a/docs/clarion/adr/ADR-012-http-auth-default.md +++ b/docs/clarion/adr/ADR-012-http-auth-default.md @@ -148,7 +148,7 @@ v0.1 tokens carry scope claims — read-only catalog, read-only findings, submit ## Related Decisions -- ADR-005 (pending; see the [ADR index backlog](./README.md)) — `.clarion/` git-committable by default, but `.clarion/socket` and `.clarion/auth.token` are runtime artifacts that must be excluded. ADR-005 picks the `.gitignore` rules that protect against both. +- [ADR-005](./ADR-005-clarion-dir-tracking.md) — `.clarion/` git-committable by default, but `.clarion/socket` and `.clarion/auth.token` are runtime artifacts that must be excluded. ADR-005 picks the `.gitignore` rules that protect against both. - [ADR-021](./ADR-021-plugin-authority-hybrid.md) — closes the plugin-side attack surface (T-01, T-08, T-11, T-12); this ADR closes the HTTP-side attack surface (T-02, T-05). Together they are the panel's "three non-negotiable v0.1 controls" (third being ADR-013, secret scanner). ## References diff --git a/docs/clarion/adr/ADR-015-wardline-filigree-emission.md b/docs/clarion/adr/ADR-015-wardline-filigree-emission.md index b24a9532..1fc9fed4 100644 --- a/docs/clarion/adr/ADR-015-wardline-filigree-emission.md +++ b/docs/clarion/adr/ADR-015-wardline-filigree-emission.md @@ -23,7 +23,7 @@ Three decisions exist inside the emission question: 2. **When does Wardline gain a native path?** v0.2 default, or a v0.1 stretch-goal contingent on a cheap implementation? 3. **What happens to the Clarion translator when Wardline gains native emission?** Does it retire entirely, or stay for other SARIF emitters? -The default committed answers in `v0.1-scope-commitments.md:190` are: Option A for v0.1, Option B for v0.2 (default deferral, optional promotion pending spike), translator stays permanently for non-Wardline SARIF sources. +The default committed answers in `../../implementation/v0.1-scope-plans/v0.1-scope-commitments.md:190` are: Option A for v0.1, Option B for v0.2 (default deferral, optional promotion pending spike), translator stays permanently for non-Wardline SARIF sources. This ADR formalises those answers and names the revision trigger. diff --git a/docs/clarion/adr/ADR-021-plugin-authority-hybrid.md b/docs/clarion/adr/ADR-021-plugin-authority-hybrid.md index cb9c88a7..bdb8558f 100644 --- a/docs/clarion/adr/ADR-021-plugin-authority-hybrid.md +++ b/docs/clarion/adr/ADR-021-plugin-authority-hybrid.md @@ -137,7 +137,7 @@ Use cgroup v2 (`systemd-run --user --scope` or direct cgroup mounts) for per-plu ## Related Decisions - [ADR-002](./ADR-002-plugin-transport-json-rpc.md) — the subprocess transport is the enforcement surface for Content-Length ceiling and crash-loop counting. This ADR extends ADR-002's supervision loop with four specific violation subcodes. -- ADR-012 (pending, rewritten for Block B) — the HTTP auth default flip closes T-02 the way this ADR closes T-08/T-11/T-12. The two ADRs form a matched pair covering the panel's "three non-negotiable v0.1 controls." +- [ADR-012](./ADR-012-http-auth-default.md) — the HTTP auth default flip closes T-02 the way this ADR closes T-08/T-11/T-12. The two ADRs form a matched pair covering the panel's "three non-negotiable v0.1 controls." - [ADR-013](./ADR-013-pre-ingest-secret-scanner.md) — pre-ingest secret scanner covers T-10 (source + secrets exfiltration); this ADR's path-jail prevents a plugin from bypassing the scanner by reading outside `project_root`, and ADR-013's scanner runs on the file list this ADR's path-jail has already filtered. - [ADR-022](./ADR-022-core-plugin-ontology.md) — data-layer authority counterpart. This ADR's Layer 2 enforces at the transport/pipeline layer; ADR-022 enforces at the ontology layer (manifest-declared kinds, rule-ID namespacing). Both share the manifest-acceptance checkpoint. diff --git a/docs/clarion/adr/ADR-023-tooling-baseline.md b/docs/clarion/adr/ADR-023-tooling-baseline.md index c677e138..75efc729 100644 --- a/docs/clarion/adr/ADR-023-tooling-baseline.md +++ b/docs/clarion/adr/ADR-023-tooling-baseline.md @@ -305,7 +305,7 @@ generates less value than the discipline floor it provides. contract. Plugins authored in Python (WP3) or later in other languages can adopt tooling baselines appropriate to their ecosystem; the Rust host's baseline is defined here. -- Scope-commitment memo [`../v0.1/plans/v0.1-scope-commitments.md`](../../implementation/v0.1-scope-plans/v0.1-scope-commitments.md) +- Scope-commitment memo [`../../implementation/v0.1-scope-plans/v0.1-scope-commitments.md`](../../implementation/v0.1-scope-plans/v0.1-scope-commitments.md) — "enterprise rigor at lack of scale" is the phrase this ADR operationalises at the tooling layer. diff --git a/docs/clarion/adr/ADR-024-guidance-schema-vocabulary.md b/docs/clarion/adr/ADR-024-guidance-schema-vocabulary.md index e847c6bf..67b2cfd5 100644 --- a/docs/clarion/adr/ADR-024-guidance-schema-vocabulary.md +++ b/docs/clarion/adr/ADR-024-guidance-schema-vocabulary.md @@ -148,7 +148,7 @@ Add the audit's findings to the glossary as `open` clashes; defer the schema cor ### Negative -- The rename has a wider blast radius than the schema alone. `requirements.md`, `system-design.md`, and `detailed-design.md` all carry references that must move; this ADR's commit updates them in lockstep. Historical ADRs (e.g., [ADR-007](./ADR-007-summary-cache-key.md) at line 56 still references `critical: true` in its example) are **not** touched — Accepted ADRs are immutable per repo convention. Readers of those historical ADRs should map the old field names to the post-ADR-024 ones using this ADR's rename table. Plan and review documents under `docs/superpowers/plans/` and `docs/clarion/v0.1/reviews/` are also left alone (historical snapshots, not normative). +- The rename has a wider blast radius than the schema alone. `requirements.md`, `system-design.md`, and `detailed-design.md` all carry references that must move; this ADR's commit updates them in lockstep. Historical ADRs (e.g., [ADR-007](./ADR-007-summary-cache-key.md) at line 56 still references `critical: true` in its example) are **not** touched — Accepted ADRs are immutable per repo convention. Readers of those historical ADRs should map the old field names to the post-ADR-024 ones using this ADR's rename table. Plan and review documents archived under `docs/implementation/` (`agent-plans/`, `v0.1-reviews/`) are also left alone (historical snapshots, not normative). - The walking-skeleton fixture's `.clarion/clarion.db` (committed for the e2e test) is rebuilt with the new schema as part of this change. The test script's expectations stay the same; the database file changes. Mitigation: the e2e script doesn't depend on the schema's column names; it asserts on the persisted entity row's `id` and `kind`. - An external operator who *did* run a pre-publication build of Clarion against a real codebase between Sprint 1 close and this ADR has a `.clarion/clarion.db` with the old `priority` column. Mitigation: nobody has — this is the explicit pre-condition for the in-place policy. Any future operator who runs the new build sees the corrected schema only. - Re-using the `0001` migration version number means the `schema_migrations` ledger row is identical (`version=1`, `name='0001_initial_schema'`) before and after the edit. A consumer who applied the pre-edit `0001` and then upgrades will not see any migration to apply, and their database will diverge silently from the new shape. Mitigation: the same pre-condition — no such consumer exists; the retirement trigger names exactly this case. diff --git a/docs/clarion/adr/ADR-026-containment-wire-and-edge-identity.md b/docs/clarion/adr/ADR-026-containment-wire-and-edge-identity.md index e45993c4..eb923fd2 100644 --- a/docs/clarion/adr/ADR-026-containment-wire-and-edge-identity.md +++ b/docs/clarion/adr/ADR-026-containment-wire-and-edge-identity.md @@ -3,7 +3,7 @@ **Status**: Accepted **Date**: 2026-05-05 **Deciders**: qacona@gmail.com -**Context**: B.3 (Sprint 2 Tier B) introduces the first edge kind Clarion has ever persisted (`contains`). Sprint-1 locked entity wire shape but explicitly deferred edge wire shape: the kickoff handoff at `docs/superpowers/handoffs/2026-04-30-sprint-2-kickoff.md` §"Edge wire shape" states "B.3's first edge will define the protocol-level wire shape". Three coupled decisions arise — the wire envelope, the edge-row identity in storage, and the per-kind contract for `source_byte_start/end`. Locking them together prevents the four later edge kinds (`calls`, `imports`, `decorates`, `inherits_from`) from inheriting an under-specified precedent. +**Context**: B.3 (Sprint 2 Tier B) introduces the first edge kind Clarion has ever persisted (`contains`). Sprint-1 locked entity wire shape but explicitly deferred edge wire shape: the kickoff handoff at `docs/implementation/handoffs/2026-04-30-sprint-2-kickoff.md` §"Edge wire shape" states "B.3's first edge will define the protocol-level wire shape". Three coupled decisions arise — the wire envelope, the edge-row identity in storage, and the per-kind contract for `source_byte_start/end`. Locking them together prevents the four later edge kinds (`calls`, `imports`, `decorates`, `inherits_from`) from inheriting an under-specified precedent. ## Summary diff --git a/docs/clarion/adr/README.md b/docs/clarion/adr/README.md index d059921e..f8c2cc88 100644 --- a/docs/clarion/adr/README.md +++ b/docs/clarion/adr/README.md @@ -48,7 +48,7 @@ The following decisions are still backlog items rather than authored ADR files. ## Pre-implementation scope commitments -The priorities and scope implied by these ADRs are committed in [../v0.1/plans/v0.1-scope-commitments.md](../../implementation/v0.1-scope-plans/v0.1-scope-commitments.md). The ADR authoring sprint is staged against that memo. +The priorities and scope implied by these ADRs are committed in [../../implementation/v0.1-scope-plans/v0.1-scope-commitments.md](../../implementation/v0.1-scope-plans/v0.1-scope-commitments.md). The ADR authoring sprint is staged against that memo. ## ADR acceptance criteria — Loom vocabulary discipline diff --git a/docs/clarion/v0.1/detailed-design.md b/docs/clarion/v0.1/detailed-design.md index 1fbb3773..bc5e827e 100644 --- a/docs/clarion/v0.1/detailed-design.md +++ b/docs/clarion/v0.1/detailed-design.md @@ -9,8 +9,9 @@ **Companion documents**: - [requirements.md](./requirements.md) — requirements (the *what*; REQ-* / NFR-* / CON-* / NG-*) - [system-design.md](./system-design.md) — system design (the *how*, mid-level; architecture, mechanisms, diagrams) -- [reviews/pre-restructure/design-review.md](../../implementation/v0.1-reviews/pre-restructure/design-review.md) — design review that drove revisions 2-4 -- [reviews/pre-restructure/integration-recon.md](../../implementation/v0.1-reviews/pre-restructure/integration-recon.md) — integration reality check against Filigree / Wardline +- [../adr/README.md](../adr/README.md) — Accepted ADRs (including ADR-023 through ADR-032 authored after this layer was baselined) +- [../../implementation/v0.1-reviews/pre-restructure/design-review.md](../../implementation/v0.1-reviews/pre-restructure/design-review.md) — design review that drove revisions 2-4 (archived) +- [../../implementation/v0.1-reviews/pre-restructure/integration-recon.md](../../implementation/v0.1-reviews/pre-restructure/integration-recon.md) — integration reality check against Filigree / Wardline (archived) --- @@ -1567,6 +1568,8 @@ Decisions in this design are load-bearing enough that they deserve explicit Arch The table below is a navigation aid for implementers: it maps each ADR to the section(s) of *this detailed design* where the decision shows up concretely. It deliberately does not duplicate the rationale or status columns from system-design.md — consult the canonical table for those. +**Coverage note**: this table reflects the ADR set at the 2026-04-18 baseline (ADR-001 through ADR-022). ADR-023 through ADR-032 were authored after the baseline; they touch implementation surfaces this document describes but are not catalogued in the table below. The [ADR index](../adr/README.md) is the canonical list for post-baseline decisions, and the relevant ADR files cite the schema/wire/section they affect. + ### Where each ADR is captured in this detailed design | # | Decision | Where captured | diff --git a/docs/clarion/v0.1/requirements.md b/docs/clarion/v0.1/requirements.md index b1a9b5bf..a84473fd 100644 --- a/docs/clarion/v0.1/requirements.md +++ b/docs/clarion/v0.1/requirements.md @@ -7,8 +7,9 @@ **Companion documents**: - [system-design.md](./system-design.md) — system design (the *how*, mid-level) - [detailed-design.md](./detailed-design.md) — detailed design reference (implementation-level) -- [reviews/pre-restructure/design-review.md](../../implementation/v0.1-reviews/pre-restructure/design-review.md) — prior review that shaped revs 2-4 -- [reviews/pre-restructure/integration-recon.md](../../implementation/v0.1-reviews/pre-restructure/integration-recon.md) — reality check against Filigree / Wardline +- [../adr/README.md](../adr/README.md) — Accepted ADRs +- [../../implementation/v0.1-reviews/pre-restructure/design-review.md](../../implementation/v0.1-reviews/pre-restructure/design-review.md) — archived design review that shaped revs 2-4 +- [../../implementation/v0.1-reviews/pre-restructure/integration-recon.md](../../implementation/v0.1-reviews/pre-restructure/integration-recon.md) — archived reality check against Filigree / Wardline --- diff --git a/docs/clarion/v0.1/system-design.md b/docs/clarion/v0.1/system-design.md index 78add121..84912eff 100644 --- a/docs/clarion/v0.1/system-design.md +++ b/docs/clarion/v0.1/system-design.md @@ -1215,7 +1215,7 @@ The parallel listing in [detailed-design.md §11](./detailed-design.md#11-archit ### Writing cadence -ADR-001 through ADR-004, ADR-006, ADR-007, ADR-011, ADR-012, ADR-013, ADR-014, ADR-015, ADR-016, ADR-017, ADR-018, ADR-021, and ADR-022 are authored and Accepted as standalone files in [../adr/](../adr/README.md). The remaining backlog items are ADR-005, ADR-009, ADR-010, ADR-019, and ADR-020; they stay summarised here and in the ADR index until the code that depends on them is ready to land. Any decision reversal during implementation (e.g., "writer-actor doesn't work; switching to shadow-DB") requires a new dated ADR revision, not an edit to the original. +Most ADRs are now authored and Accepted as standalone files in [../adr/](../adr/README.md); ADR-008 is Superseded (by ADR-014); ADR-009, ADR-010, ADR-019, and ADR-020 remain backlog and stay summarised here and in the ADR index until the code that depends on them is ready to land. Additional Sprint-2-era ADRs (ADR-023 through ADR-032) were authored after this design layer was baselined; their summaries live in the ADR index rather than in this section. Any decision reversal during implementation (e.g., "writer-actor doesn't work; switching to shadow-DB") requires a new dated ADR revision, not an edit to the original. Authored ADR text (context, decision, alternatives considered, consequences, status) lives in [../adr/README.md](../adr/README.md). These summaries are the navigation aid; the canonical record of completed decisions is the ADR collection itself. diff --git a/docs/suite/loom.md b/docs/suite/loom.md index 9dc35db1..72e49b2a 100644 --- a/docs/suite/loom.md +++ b/docs/suite/loom.md @@ -115,7 +115,7 @@ Federation does not require uniform vocabulary, but it does require that the sam | Product | Status | |---|---| -| Clarion | Designed; implementation not yet started; first-customer target is elspeth (~425k LOC Python) | +| Clarion | Walking skeleton tagged `v0.1-sprint-1`; v0.1 build in flight against the published design; first-customer target is elspeth (~425k LOC Python) | | Filigree | Built; in active use | | Wardline | Built; in active commit-cadence use | | Shuttle | Proposed; not in flight; separate design effort when prioritised | From 92deb2909bf7d05c73921d8776c0bd19f270158b Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Tue, 19 May 2026 13:06:23 +1000 Subject: [PATCH 010/187] docs(reqs): defer Filigree-emission requirements to v0.2 (T2) Reconciles CON-FILIGREE-02 against the Sprint 2 scope amendment (WP9-B + WP10 deferred to v0.2). The constraint is rewritten to describe v1.0's shadow-registry mode and forward-points to the amendment. The reqs that the amendment defers also get a deferral banner inline so future readers see the v1.0/v2.0 split at the requirement boundary, not just in the amendment memo: - REQ-FINDING-03/04/05/06 (Filigree-side findings + dedup + SARIF) - REQ-INTEG-FILIGREE-01/02/03/04/05 (cross-product wire surfaces) Closes filigree clarion-066f90d0dc. Co-Authored-By: Claude Opus 4.7 --- docs/clarion/v0.1/requirements.md | 34 ++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/docs/clarion/v0.1/requirements.md b/docs/clarion/v0.1/requirements.md index a84473fd..b187dc42 100644 --- a/docs/clarion/v0.1/requirements.md +++ b/docs/clarion/v0.1/requirements.md @@ -319,6 +319,8 @@ Clarion's rule IDs follow the `CLA-*` namespace: `CLA-PY-*` for Python-plugin st #### REQ-FINDING-03 — Emit findings to Filigree +> **Deferred to v0.2** per the [Sprint 2 scope amendment §4](../../implementation/sprint-2/scope-amendment-2026-05.md) (WP9-B). Clarion v0.1 emits findings to its own SQLite `findings` table; cross-product emission to Filigree lands with WP9-B. + Clarion POSTs findings to Filigree via `POST /api/v1/scan-results` using Filigree's native intake schema. Clarion's richer fields (`kind`, `confidence`, `confidence_basis`, `supports`/`supported_by`, `related_entities`, internal severity, internal status) nest inside each finding's `metadata.clarion.*`. **Rationale**: Filigree owns finding triage and lifecycle (per the Loom federation axiom — `CON-LOOM-01`). Emitting to Filigree surfaces Clarion's findings alongside Wardline's and any future scanners' in the team's unified triage view. @@ -327,14 +329,18 @@ Clarion POSTs findings to Filigree via `POST /api/v1/scan-results` using Filigre #### REQ-FINDING-04 — General-purpose SARIF → Filigree translator -Clarion v0.1 ships `clarion sarif import [--scan-source ]` that translates any SARIF v2.1.0 emitter (Wardline, Semgrep, CodeQL, Trivy) to Filigree's scan-results format, preserving `result.properties` into `metadata._properties.*`. +> **Deferred to v0.2** per the [Sprint 2 scope amendment §4](../../implementation/sprint-2/scope-amendment-2026-05.md) (WP10). The SARIF translator is independent of MCP surface delivery and lands with the v0.2 cross-product work. + +Clarion ships `clarion sarif import [--scan-source ]` that translates any SARIF v2.1.0 emitter (Wardline, Semgrep, CodeQL, Trivy) to Filigree's scan-results format, preserving `result.properties` into `metadata._properties.*`. -**Rationale**: The translator is a permanent suite feature (SARIF is external, Filigree's intake is not SARIF). For v0.1 it serves Wardline-to-Filigree as a side-effect; in v0.2+ Wardline ships its own native POST, but the translator stays for third-party SARIF sources. +**Rationale**: The translator is a permanent suite feature (SARIF is external, Filigree's intake is not SARIF). For v0.1 it served Wardline-to-Filigree as a side-effect; in v0.2+ Wardline ships its own native POST, but the translator stays for third-party SARIF sources. **Verification**: SARIF corpus at `tests/fixtures/wardline-sarif/` translates to scan-results POSTs; `wardline.*` property-bag keys land in `metadata.wardline_properties.*`; severity mapped via `{error→high, warning→medium, note→info}`. **See**: System Design §9 (Integrations, SARIF translator). #### REQ-FINDING-05 — `scan_run_id` lifecycle +> **Deferred to v0.2** per the [Sprint 2 scope amendment §4](../../implementation/sprint-2/scope-amendment-2026-05.md) (WP9-B). Lands with REQ-FINDING-03 (findings emission to Filigree). + Clarion's `run_id` maps 1:1 onto Filigree's `scan_run_id`. Phase 0 of `clarion analyze` creates the scan run; Phase 8 closes it with `complete_scan_run=true`. Resume (`--resume`) reuses the same `run_id` and posts with `mark_unseen=false`. **Rationale**: Run lifecycle alignment lets Filigree apply "seen-in-latest" logic correctly. Without it, resumed runs would be indistinguishable from fresh runs and prior findings would flap to `unseen_in_latest` prematurely. @@ -343,6 +349,8 @@ Clarion's `run_id` maps 1:1 onto Filigree's `scan_run_id`. Phase 0 of `clarion a #### REQ-FINDING-06 — Dedup policy for moved entities +> **Deferred to v0.2** per the [Sprint 2 scope amendment §4](../../implementation/sprint-2/scope-amendment-2026-05.md) (WP9-B). Concerns Filigree-side dedup; lands with REQ-FINDING-03 (findings emission). + Clarion POSTs findings with `mark_unseen=true` by default so that old-position findings for the same rule on the same file transition to `unseen_in_latest` when the entity moves within the file. `clarion analyze --prune-unseen` removes `unseen_in_latest` findings older than 30 days (configurable). **Rationale**: Filigree's dedup key includes `line_start`; entities moving within a file produce two findings. `mark_unseen` is the v0.1 compromise — acceptable coarseness until Filigree ships server-side per-entity dedup (v0.2). @@ -571,6 +579,8 @@ Clarion's side of Filigree integration. #### REQ-INTEG-FILIGREE-01 — Findings via scan-results intake +> **Deferred to v0.2** per the [Sprint 2 scope amendment §4](../../implementation/sprint-2/scope-amendment-2026-05.md) (WP9-B). Lands with REQ-FINDING-03. + Clarion POSTs findings to Filigree's `POST /api/v1/scan-results` using the native schema (see `REQ-FINDING-03`) with `scan_source: "clarion"`. Clarion inspects `response.warnings[]` on every POST for silent coercion / unknown-key drops. **Rationale**: Filigree's scan-results intake is the battle-tested cross-tool finding-exchange path; the warnings array is how Filigree signals schema drift. Ignoring warnings means silently shipping malformed findings. @@ -579,14 +589,18 @@ Clarion POSTs findings to Filigree's `POST /api/v1/scan-results` using the nativ #### REQ-INTEG-FILIGREE-02 — Observation emission (HTTP preferred, MCP fallback) +> **Deferred to v0.2** per the [Sprint 2 scope amendment §4](../../implementation/sprint-2/scope-amendment-2026-05.md) (WP9-B). Clarion v0.1 surfaces structural and security findings via its own MCP `issues_for` tool (WP9-A); cross-product observation emission to Filigree lands with WP9-B. + Clarion emits observations to Filigree via `POST /api/v1/observations` when available; falls back to MCP-client transport (spawning `filigree mcp` as subprocess) when the HTTP endpoint is absent. The fallback is signalled in the capability compat report. -**Rationale**: Observations are fire-and-forget notes; Clarion generates them during analyse and consult. HTTP is the natural transport for a Rust binary emitting to a local Filigree; MCP is the v0.1 workaround while Filigree adds the HTTP endpoint (see `CON-FILIGREE-02`). +**Rationale**: Observations are fire-and-forget notes; Clarion generates them during analyse and consult. HTTP is the natural transport for a Rust binary emitting to a local Filigree; MCP is the v0.1 workaround while Filigree adds the HTTP endpoint. **Verification**: HTTP path used by default; `--no-filigree-http` flag forces MCP fallback; both paths produce observations visible in Filigree. **See**: System Design §9 (Integrations, Filigree — Observations), §11 (Suite Bootstrap). #### REQ-INTEG-FILIGREE-03 — Registry-backend consumption +> **Deferred to v0.2** per the [Sprint 2 scope amendment §4](../../implementation/sprint-2/scope-amendment-2026-05.md) (WP9-B/WP10) and reflected in [CON-FILIGREE-02](#con-filigree-02--file-registry-displacement-is-deferred-to-v02). Filigree's `registry_backend` flag shipped (ADR-014); Clarion's read-side `RegistryProtocol` implementation is the v0.2 work. Clarion v0.1 ships shadow-registry only. + When Filigree's `registry_backend` flag is set to `clarion`, Clarion serves as Filigree's file registry: Filigree consults Clarion's HTTP read API for file ID resolution; auto-create paths route through `RegistryProtocol` to Clarion. Absent the flag, Clarion operates in shadow-registry mode (findings POSTed normally; Filigree auto-creates `file_records` under its native rules). **Rationale**: File-registry displacement is the cleanest expression of "Clarion owns structural truth" (per Loom federation). The shadow-registry fallback preserves v0.1 shipability when Filigree hasn't yet landed `registry_backend`. @@ -595,6 +609,8 @@ When Filigree's `registry_backend` flag is set to `clarion`, Clarion serves as F #### REQ-INTEG-FILIGREE-04 — `scan_source` namespace + schema pin test +> **Deferred to v0.2** per the [Sprint 2 scope amendment §4](../../implementation/sprint-2/scope-amendment-2026-05.md) (WP9-B). Lands with REQ-FINDING-03 (findings emission to Filigree); schema-pin test is moot until Clarion is actually POSTing. + Clarion uses `scan_source: "clarion"` for emissions; CI runs a schema-compatibility test against a Filigree release's `GET /api/files/_schema` output to detect drift in `valid_severities`, `valid_finding_statuses`, `valid_association_types`. **Rationale**: Filigree's CHANGELOG flags breaking API changes but relies on social discipline; a pinned schema-compat test gives Clarion CI-level protection against silent schema shifts. @@ -603,6 +619,8 @@ Clarion uses `scan_source: "clarion"` for emissions; CI runs a schema-compatibil #### REQ-INTEG-FILIGREE-05 — Capability-negotiation probe +> **Deferred to v0.2** per the [Sprint 2 scope amendment §4](../../implementation/sprint-2/scope-amendment-2026-05.md) (WP9-B). The capability probe is meaningful only once Clarion is talking to Filigree via the v0.2 cross-product surfaces (REQ-FINDING-03, REQ-INTEG-FILIGREE-02/03). + At `clarion analyze` startup, Clarion probes Filigree's presence, version, `registry_backend` setting, and `/api/v1/observations` availability via `GET /api/files/_schema` + `HEAD` checks. Results emit in a single `CLA-INFRA-SUITE-COMPAT-REPORT` finding. **Rationale**: Partial Filigree versions are common (deployment skew). A single compat report collapses scattered runtime surprises into one auditable signal operators can read at the start of each run. @@ -942,12 +960,14 @@ Clarion emits findings to Filigree via `POST /api/v1/scan-results` using Filigre **Rationale**: Filigree's scan-results endpoint is the production path; deviating requires Filigree work that is out of v0.1 scope. The `metadata` nesting and severity enum are existing Filigree semantics verified by recon. **See**: System Design §9 (Integrations, Filigree — Wire format). -### CON-FILIGREE-02 — `registry_backend` flag is a hard dependency +### CON-FILIGREE-02 — File-registry displacement is deferred to v0.2 + +Clarion v0.1 does **not** displace Filigree's file registry. The "Clarion owns the file registry" story — Filigree's `registry_backend` flag + pluggable `RegistryProtocol` and Clarion serving as Filigree's registry backend — is deferred to v0.2 per the [Sprint 2 scope amendment §4](../../implementation/sprint-2/scope-amendment-2026-05.md) (WP9-B + WP10). Filigree did ship the `registry_backend` flag during Sprint 2 (ADR-014); Clarion's read-side implementation of `RegistryProtocol` and the associated suite-compat reporting are the v0.2 work. -Clarion v0.1's "Clarion owns the file registry" claim depends on Filigree shipping a `registry_backend` config flag + pluggable `RegistryProtocol`. Absent the flag, Clarion operates in shadow-registry mode (downgrade to "owns the entity catalog"). +In v0.1, Clarion operates in shadow-registry mode by default: Clarion owns its entity catalog; Filigree, when present, auto-creates `file_records` under its native rules. The two stores reference the same paths but neither delegates id allocation to the other. This is acceptable because Clarion v0.1's MVP MCP surface (the seven tools — `entity_at`, `find_entity`, `callers_of`, `execution_paths_from`, `summary`, `issues_for`, `neighborhood`) queries Clarion's own SQLite store directly and does not require registry coupling. -**Rationale**: Filigree's four NOT-NULL `file_records(id)` foreign keys + three auto-create paths make the displacement a schema surgery, not a feature flag. Degraded mode preserves v0.1 shipability; full integration depends on Filigree's cadence. -**See**: System Design §11 (Suite Bootstrap, Prerequisites named here). +**Rationale**: Filigree's four NOT-NULL `file_records(id)` foreign keys + three auto-create paths make registry displacement a schema-surgery integration, not a feature flag — out of v0.1 scope per the scope amendment. Shadow mode preserves v0.1 shipability; full integration lands with the WP9-B/WP10 cluster in v0.2. +**See**: System Design §11 (Suite Bootstrap), [ADR-014 — Filigree `registry_backend` flag and pluggable `RegistryProtocol`](../adr/ADR-014-filigree-registry-backend.md), [Sprint 2 scope amendment §4](../../implementation/sprint-2/scope-amendment-2026-05.md). ### CON-WARDLINE-01 — Wardline owns its REGISTRY From be0b403f17e53225416690fe80701c4c6971c7c3 Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Tue, 19 May 2026 13:06:58 +1000 Subject: [PATCH 011/187] =?UTF-8?q?release:=20v1.0=20publish=20prep=20?= =?UTF-8?q?=E2=80=94=20README=20+=20tutorial=20+=20workflow=20+=20four=20f?= =?UTF-8?q?ixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the v1.0 publish track (clarion-443320f078 Thread 1: B.1, B.2, C.2, C.3, WS-D). The external-operator smoke test surfaced four publish-blocker bugs in the documented happy path; all are fixed in this commit. Docs / release ergonomics - README.md at repo root (B.1, clarion-380250e6ed). v1.0 framing, the seven MCP tools as a table, quick-start, project layout, contributing pointer. - docs/operator/getting-started.md (B.2, clarion-459e186028 + C.3 clarion-6150779248). Single-flow tutorial: install, analyse a small repo, connect MCP client, ask three pre-scripted questions, verify secret-block. Plugin-discovery PATH guidance folded in. - ADR-033 — v1.0 distribution via GitHub Releases (Linux x86_64 + macOS x86_64/aarch64; promote to crates.io/PyPI at v2.0). Windows is explicitly out (setrlimit is Unix-only). - .github/workflows/release.yml — tag-triggered binary matrix + Python sdist + SHA256s + auto-generated notes. workflow_dispatch supported for rc1 validation without a tag push. - LICENSE (MIT) materialised at repo root. Source fixes surfaced by WS-D (clarion-3e0e481ef7) 1. install.rs: the `clarion.yaml` stub used Rust `\` string line-continuations, which strip leading whitespace — the file shipped unindented and YAML-invalid. Switched to raw newlines + explicit indentation with a leading comment warning against the pattern. 2. writer.rs / writer_actor.rs: `clarion analyze` was not idempotent — a second invocation against an unchanged corpus crashed with `UNIQUE constraint failed: entities.id`. The entity-insert path now UPSERTs on `id`, preserving `created_at` + `first_seen_commit` and refreshing the rest from the latest run's record. Matches the `INSERT OR IGNORE` discipline already in `insert_edge`. New regression test: `insert_entity_is_idempotent_across_runs`. 3. discovery.rs: pipx-installed plugins live behind a symlink in `~/.local/bin/` pointing to `~/.local/share/pipx/venvs//bin/`, with the manifest under that venv's `share/clarion/plugins/...`. Plain install-prefix lookup from `~/.local/bin/` looked at `~/.local/share/` and missed the manifest entirely. Added a symlink-resolved install-prefix fallback that canonicalises the executable and retries the lookup under the resolved prefix. 4. cli.rs: `clarion analyze --help` text still claimed "Sprint 1: no plugins are loaded". Rewritten to describe v1.0 behaviour, including the idempotency contract and the `skipped_no_plugins` exit-0 diagnostic. Version + attribution - Workspace + path-deps + plugins/python: 0.1.0-dev → 1.0.0. - OpenRouter attribution `referer` defaults: qacona/clarion → tachyon-beep/clarion across crates + docs + tests. Closes filigree clarion-380250e6ed, clarion-459e186028, clarion-6150779248, clarion-25f5ff9de0, clarion-3e0e481ef7. Co-Authored-By: Claude Opus 4.7 --- .github/workflows/release.yml | 190 ++++++++++++ Cargo.lock | 12 +- Cargo.toml | 6 +- LICENSE | 21 ++ README.md | 127 ++++++++ crates/clarion-cli/Cargo.toml | 10 +- crates/clarion-cli/src/cli.rs | 7 +- crates/clarion-cli/src/install.rs | 92 +++--- crates/clarion-core/src/llm_provider.rs | 16 +- crates/clarion-core/src/plugin/discovery.rs | 52 +++- crates/clarion-mcp/Cargo.toml | 4 +- crates/clarion-mcp/src/config.rs | 2 +- crates/clarion-mcp/tests/storage_tools.rs | 2 +- crates/clarion-plugin-fixture/Cargo.toml | 2 +- crates/clarion-storage/Cargo.toml | 2 +- crates/clarion-storage/src/writer.rs | 26 +- crates/clarion-storage/tests/writer_actor.rs | 133 ++++++++ docs/clarion/adr/ADR-033-v1.0-distribution.md | 80 +++++ docs/clarion/adr/README.md | 1 + docs/operator/README.md | 3 + docs/operator/getting-started.md | 291 ++++++++++++++++++ docs/operator/openrouter.md | 2 +- plugins/python/pyproject.toml | 4 +- tests/e2e/external-operator-smoke.md | 64 ++++ 24 files changed, 1060 insertions(+), 89 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 docs/clarion/adr/ADR-033-v1.0-distribution.md create mode 100644 docs/operator/getting-started.md create mode 100644 tests/e2e/external-operator-smoke.md diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..e469c419 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,190 @@ +name: Release + +# Per ADR-033 — pushed tags matching v* trigger a multi-platform binary build +# plus Python plugin sdist, attached to a GitHub Release. +on: + push: + tags: + - "v*" + # Manual dispatch lets us produce pre-release artifacts (e.g. v1.0.0-rc1) + # without pushing a tag for full validation of the workflow. + workflow_dispatch: + inputs: + tag: + description: "Pre-release tag name (used as the GH Release name; e.g. v1.0.0-rc1)" + required: true + type: string + +env: + CARGO_TERM_COLOR: always + RUSTFLAGS: "-D warnings" + +permissions: + contents: write + +jobs: + build-rust: + name: Build clarion (${{ matrix.target }}) + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - target: x86_64-unknown-linux-gnu + runner: ubuntu-latest + - target: x86_64-apple-darwin + runner: macos-13 + - target: aarch64-apple-darwin + runner: macos-14 + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + + - uses: Swatinem/rust-cache@v2 + with: + key: release-${{ matrix.target }} + + - name: build release + run: cargo build --release --locked --bin clarion --target ${{ matrix.target }} + + - name: package + id: package + run: | + set -euo pipefail + STAGE="clarion-${{ matrix.target }}" + mkdir -p "$STAGE" + cp "target/${{ matrix.target }}/release/clarion" "$STAGE/" + cp README.md "$STAGE/" + if [ -f LICENSE ]; then cp LICENSE "$STAGE/"; fi + ARCHIVE="$STAGE.tar.gz" + tar czf "$ARCHIVE" "$STAGE" + sha256sum "$ARCHIVE" > "$ARCHIVE.sha256" + echo "archive=$ARCHIVE" >> "$GITHUB_OUTPUT" + + - uses: actions/upload-artifact@v4 + with: + name: clarion-${{ matrix.target }} + path: | + ${{ steps.package.outputs.archive }} + ${{ steps.package.outputs.archive }}.sha256 + if-no-files-found: error + retention-days: 7 + + build-plugin: + name: Build Python plugin sdist + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: install build + run: python -m pip install --upgrade build + + - name: build sdist + run: python -m build --sdist --outdir dist plugins/python + + - name: checksum + run: | + set -euo pipefail + cd dist + for f in *.tar.gz; do + sha256sum "$f" > "$f.sha256" + done + + - uses: actions/upload-artifact@v4 + with: + name: clarion-plugin-python-sdist + path: | + dist/*.tar.gz + dist/*.tar.gz.sha256 + if-no-files-found: error + retention-days: 7 + + release: + name: Create GitHub Release + runs-on: ubuntu-latest + needs: [build-rust, build-plugin] + # Only publish to a Release when a real tag pushed; workflow_dispatch leaves + # artifacts on the workflow run for inspection without creating a public Release. + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/download-artifact@v4 + with: + path: artifacts + + - name: stage release assets + run: | + set -euo pipefail + mkdir -p release + find artifacts -name '*.tar.gz' -o -name '*.tar.gz.sha256' \ + | xargs -I{} cp {} release/ + ls -la release/ + + - name: derive previous tag + id: prev + run: | + set -euo pipefail + PREV=$(git tag --sort=-v:refname | grep -v "^${GITHUB_REF_NAME}\$" | head -n1 || true) + if [ -z "$PREV" ]; then + echo "previous=" >> "$GITHUB_OUTPUT" + echo "No previous tag found (first release)." + else + echo "previous=$PREV" >> "$GITHUB_OUTPUT" + echo "Previous tag: $PREV" + fi + + - name: generate notes + id: notes + run: | + set -euo pipefail + PREV="${{ steps.prev.outputs.previous }}" + { + echo "## Clarion ${GITHUB_REF_NAME}" + echo "" + echo "### Install" + echo "" + echo "Pick the archive for your platform from the Assets list below." + echo "Verify the SHA256, extract, and place \`clarion\` on your \`\$PATH\`." + echo "" + echo "\`\`\`bash" + echo "curl -L -o clarion.tar.gz \\" + echo " https://github.com/${GITHUB_REPOSITORY}/releases/download/${GITHUB_REF_NAME}/clarion-x86_64-unknown-linux-gnu.tar.gz" + echo "tar xzf clarion.tar.gz" + echo "install clarion-x86_64-unknown-linux-gnu/clarion ~/.local/bin/" + echo "" + echo "pipx install \\" + echo " https://github.com/${GITHUB_REPOSITORY}/releases/download/${GITHUB_REF_NAME}/clarion-plugin-python-1.0.0.tar.gz" + echo "\`\`\`" + echo "" + echo "Full walkthrough: [docs/operator/getting-started.md](https://github.com/${GITHUB_REPOSITORY}/blob/${GITHUB_REF_NAME}/docs/operator/getting-started.md)." + echo "" + echo "### Changes" + echo "" + if [ -n "$PREV" ]; then + git log --merges --pretty='- %s (%h)' "${PREV}..${GITHUB_REF_NAME}" || true + else + echo "First release — see git history." + fi + } > NOTES.md + echo "notes-file=NOTES.md" >> "$GITHUB_OUTPUT" + + - name: create release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ github.ref_name }} + name: Clarion ${{ github.ref_name }} + body_path: ${{ steps.notes.outputs.notes-file }} + draft: false + prerelease: ${{ contains(github.ref_name, '-rc') || contains(github.ref_name, '-alpha') || contains(github.ref_name, '-beta') }} + files: release/* + fail_on_unmatched_files: true diff --git a/Cargo.lock b/Cargo.lock index acac28d3..503f52e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -312,7 +312,7 @@ checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "clarion-cli" -version = "0.1.0-dev" +version = "1.0.0" dependencies = [ "anyhow", "assert_cmd", @@ -343,7 +343,7 @@ dependencies = [ [[package]] name = "clarion-core" -version = "0.1.0-dev" +version = "1.0.0" dependencies = [ "nix", "reqwest", @@ -357,7 +357,7 @@ dependencies = [ [[package]] name = "clarion-mcp" -version = "0.1.0-dev" +version = "1.0.0" dependencies = [ "blake3", "clarion-core", @@ -376,7 +376,7 @@ dependencies = [ [[package]] name = "clarion-plugin-fixture" -version = "0.1.0-dev" +version = "1.0.0" dependencies = [ "clarion-core", "nix", @@ -385,7 +385,7 @@ dependencies = [ [[package]] name = "clarion-scanner" -version = "0.1.0-dev" +version = "1.0.0" dependencies = [ "regex", "serde", @@ -397,7 +397,7 @@ dependencies = [ [[package]] name = "clarion-storage" -version = "0.1.0-dev" +version = "1.0.0" dependencies = [ "blake3", "clarion-core", diff --git a/Cargo.toml b/Cargo.toml index c8805ec8..065d6b41 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,10 +10,10 @@ members = [ ] [workspace.package] -version = "0.1.0-dev" +version = "1.0.0" edition = "2024" -license = "MIT OR Apache-2.0" -repository = "https://github.com/qacona/clarion" +license = "MIT" +repository = "https://github.com/tachyon-beep/clarion" rust-version = "1.88" [workspace.lints.rust] diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..0b875485 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 John Morrissey + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 00000000..42b86e75 --- /dev/null +++ b/README.md @@ -0,0 +1,127 @@ +# Clarion + +Clarion is a code-archaeology tool. It ingests a codebase, extracts entities +(functions, classes, modules) and their relationships (`contains`, `calls`, +`references`), persists the structural graph to a local SQLite store, and serves +the result to consult-mode LLM agents over MCP. A coding agent that would +otherwise re-explore the tree on every question reaches Clarion first and asks a +graph-aware tool. v1.0 ships a Rust core plus a Python language plugin; other +languages land in v2.0+. + +Part of the [Loom suite](docs/suite/loom.md) of code-archaeology, issue-tracking, +and trust-topology tools. + +## Status + +**v1.0 — first publishable release.** Scope: + +- **Python only.** Other-language plugins (`NG-15`) are v2.0+ scope. +- **Structural extraction + on-demand LLM summarisation.** `clarion analyze` + walks the corpus and persists entities + edges; `summary(id)` over MCP + dispatches the LLM lazily, one entity at a time. +- **Local-first.** No mandatory cloud component; the only required network + egress is the LLM provider during `summary` calls. +- **Filigree finding emission deferred to v0.2.** Clarion v1.0 surfaces issues + attached to entities via its own `issues_for` MCP tool (the WP9-A binding); + cross-product POSTing of Clarion-generated findings into Filigree's intake is + WP9-B, deferred per the + [Sprint 2 scope amendment](docs/implementation/sprint-2/scope-amendment-2026-05.md#4-v01-planmd-resequencing). + +## What it does today + +`clarion serve` exposes seven MCP tools that a consult-mode agent calls instead +of grep-and-read: + +| Tool | What it answers | +|---|---| +| `entity_at(file, line)` | "Which entity covers this source location?" | +| `find_entity(pattern)` | "Find entities whose name or summary text matches X." | +| `callers_of(id)` | "Who calls this function?" | +| `execution_paths_from(id, max_depth)` | "Show me up to N hops of call paths starting here." | +| `summary(id)` | "Give me a one-paragraph summary of this entity." (lazy LLM dispatch + cached) | +| `issues_for(id)` | "What Filigree issues are attached to this entity?" | +| `neighborhood(id)` | "Show callers, callees, container, contained entities, and references in one hop." | + +`subsystem_members(id)` is also exposed for listing members of a subsystem +entity (clustering output). + +## Quick start + +```bash +# 1. Install (see docs/operator/getting-started.md for the full setup) +cargo install --git https://github.com/tachyon-beep/clarion clarion-cli +pipx install git+https://github.com/tachyon-beep/clarion#subdirectory=plugins/python + +# 2. Initialise a project +cd /path/to/your/python/repo +clarion install --path . + +# 3. Walk the corpus and persist the structural graph +clarion analyze + +# 4. Serve the graph over MCP for consult-mode agents +clarion serve +``` + +`clarion analyze` works without any LLM credentials and is the fastest way to +verify the install. `summary(id)` calls require `OPENROUTER_API_KEY` to be set +(see [docs/operator/openrouter.md](docs/operator/openrouter.md)). + +A full walkthrough — installing on a fresh machine, running against a small +public Python project, connecting an MCP client, and asking three questions — is +in [docs/operator/getting-started.md](docs/operator/getting-started.md). + +## Project layout + +``` +crates/ Rust workspace +├── clarion-core/ Entity-ID assembler, plugin host, manifest parser +├── clarion-storage/ Writer-actor + reader-pool over SQLite (ADR-011) +├── clarion-scanner/ Pre-ingest secret scanner (ADR-013, WP5) +├── clarion-cli/ The `clarion` binary (install, analyze, serve) +└── clarion-mcp/ MCP server exposing the seven consult tools +plugins/python/ Python language plugin (pyright-backed) +docs/clarion/v0.1/ Design ladder — requirements → system-design → detailed-design +docs/clarion/adr/ Authored architecture decision records +``` + +For the design ladder start at +[docs/clarion/v0.1/README.md](docs/clarion/v0.1/README.md). The full ADR index +is at [docs/clarion/adr/README.md](docs/clarion/adr/README.md). The Loom +federation doctrine that anchors every cross-product decision is in +[docs/suite/loom.md](docs/suite/loom.md). + +## Contributing + +Read [CLAUDE.md](CLAUDE.md) for repository conventions, work-package +vocabulary, and where canonical truth lives. The CI floor every PR must clear +is fixed by [ADR-023](docs/clarion/adr/ADR-023-tooling-baseline.md): + +```bash +# Rust gates +cargo fmt --all -- --check +cargo clippy --workspace --all-targets --all-features -- -D warnings +cargo build --workspace --bins +cargo nextest run --workspace --all-features +RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps --all-features +cargo deny check + +# Python gates (run from repo root) +plugins/python/.venv/bin/ruff check plugins/python +plugins/python/.venv/bin/ruff format --check plugins/python +plugins/python/.venv/bin/mypy --strict plugins/python +plugins/python/.venv/bin/pytest plugins/python + +# End-to-end +bash tests/e2e/sprint_1_walking_skeleton.sh +``` + +Pre-commit hooks at [.pre-commit-config.yaml](.pre-commit-config.yaml) wire +ruff + ruff-format + mypy on every `git commit`. Install with +`plugins/python/.venv/bin/pre-commit install`. + +## License + +[MIT](LICENSE). Matches the `license = "MIT"` declaration in +[`Cargo.toml`](Cargo.toml). Contributions are accepted under the same terms +unless explicitly stated otherwise by the contributor. diff --git a/crates/clarion-cli/Cargo.toml b/crates/clarion-cli/Cargo.toml index 694ac16c..478943d9 100644 --- a/crates/clarion-cli/Cargo.toml +++ b/crates/clarion-cli/Cargo.toml @@ -18,10 +18,10 @@ anyhow.workspace = true axum.workspace = true blake3.workspace = true clap.workspace = true -clarion-core = { path = "../clarion-core", version = "0.1.0-dev" } -clarion-mcp = { path = "../clarion-mcp", version = "0.1.0-dev" } -clarion-scanner = { path = "../clarion-scanner", version = "0.1.0-dev" } -clarion-storage = { path = "../clarion-storage", version = "0.1.0-dev" } +clarion-core = { path = "../clarion-core", version = "1.0.0" } +clarion-mcp = { path = "../clarion-mcp", version = "1.0.0" } +clarion-scanner = { path = "../clarion-scanner", version = "1.0.0" } +clarion-storage = { path = "../clarion-storage", version = "1.0.0" } dotenvy.workspace = true ignore.workspace = true rusqlite.workspace = true @@ -38,7 +38,7 @@ xgraph.workspace = true [dev-dependencies] assert_cmd.workspace = true -clarion-plugin-fixture = { path = "../clarion-plugin-fixture", version = "0.1.0-dev" } +clarion-plugin-fixture = { path = "../clarion-plugin-fixture", version = "1.0.0" } rusqlite.workspace = true serde_json.workspace = true sha1.workspace = true diff --git a/crates/clarion-cli/src/cli.rs b/crates/clarion-cli/src/cli.rs index 34070d49..a3fa942b 100644 --- a/crates/clarion-cli/src/cli.rs +++ b/crates/clarion-cli/src/cli.rs @@ -22,8 +22,11 @@ pub enum Command { path: PathBuf, }, - /// Run an analysis pass. Sprint 1: no plugins are loaded; run status is - /// `skipped_no_plugins`. WP2 wires plugin spawning. + /// Run an analysis pass: walk the source tree, dispatch discovered plugins + /// to extract entities/edges, and persist results to `.clarion/clarion.db`. + /// Re-runs are idempotent (UPSERT on `entities.id`). If no plugins are on + /// `$PATH`, exits 0 with a WARN and status `skipped_no_plugins` — see + /// `docs/operator/getting-started.md` Troubleshooting. Analyze { /// Path to analyse (default: current directory). #[arg(default_value = ".")] diff --git a/crates/clarion-cli/src/install.rs b/crates/clarion-cli/src/install.rs index b46dfc50..7b070ed8 100644 --- a/crates/clarion-cli/src/install.rs +++ b/crates/clarion-cli/src/install.rs @@ -23,50 +23,54 @@ const CONFIG_JSON_STUB: &str = r#"{ } "#; -const CLARION_YAML_STUB: &str = "# clarion.yaml — user-edited config.\n\ -# Do not delete this file: clarion serve reads MCP, LLM, and integration\n\ -# settings from here when present.\n\ -version: 1\n\ -llm_policy:\n\ - enabled: false\n\ - provider: openrouter\n\ - allow_live_provider: false\n\ - openrouter:\n\ - endpoint_url: https://openrouter.ai/api/v1\n\ - api_key_env: OPENROUTER_API_KEY\n\ - attribution:\n\ - referer: https://github.com/qacona/clarion\n\ - title: Clarion\n\ - codex_cli:\n\ - executable: codex\n\ - model: null\n\ - profile: null\n\ - sandbox: read-only\n\ - timeout_seconds: 300\n\ - claude_cli:\n\ - executable: claude\n\ - model: null\n\ - permission_mode: plan\n\ - tools: []\n\ - timeout_seconds: 300\n\ - max_turns: 2\n\ - no_session_persistence: true\n\ - exclude_dynamic_system_prompt_sections: true\n\ - model_id: anthropic/claude-sonnet-4.6\n\ - session_token_ceiling: 1000000\n\ - max_inferred_edges_per_caller: 8\n\ - cache_max_age_days: 180\n\ -integrations:\n\ - filigree:\n\ - enabled: false\n\ - base_url: http://127.0.0.1:8766\n\ - actor: clarion-mcp\n\ - token_env: FILIGREE_API_TOKEN\n\ - timeout_seconds: 5\n\ -serve:\n\ - http:\n\ - enabled: false\n\ - bind: 127.0.0.1:9111\n"; +// NOTE: Do not use `\` line-continuation here — Rust strips both the newline +// AND all leading whitespace on the continuation line, producing flat (and +// therefore broken) YAML. Use raw newlines + explicit indentation. +const CLARION_YAML_STUB: &str = "# clarion.yaml — user-edited config. +# Do not delete this file: clarion serve reads MCP, LLM, and integration +# settings from here when present. +version: 1 +llm_policy: + enabled: false + provider: openrouter + allow_live_provider: false + openrouter: + endpoint_url: https://openrouter.ai/api/v1 + api_key_env: OPENROUTER_API_KEY + attribution: + referer: https://github.com/tachyon-beep/clarion + title: Clarion + codex_cli: + executable: codex + model: null + profile: null + sandbox: read-only + timeout_seconds: 300 + claude_cli: + executable: claude + model: null + permission_mode: plan + tools: [] + timeout_seconds: 300 + max_turns: 2 + no_session_persistence: true + exclude_dynamic_system_prompt_sections: true + model_id: anthropic/claude-sonnet-4.6 + session_token_ceiling: 1000000 + max_inferred_edges_per_caller: 8 + cache_max_age_days: 180 +integrations: + filigree: + enabled: false + base_url: http://127.0.0.1:8766 + actor: clarion-mcp + token_env: FILIGREE_API_TOKEN + timeout_seconds: 5 +serve: + http: + enabled: false + bind: 127.0.0.1:9111 +"; const GITIGNORE_CONTENTS: &str = "\ # Clarion .gitignore — ADR-005 tracked-vs-excluded list. diff --git a/crates/clarion-core/src/llm_provider.rs b/crates/clarion-core/src/llm_provider.rs index 46fe0dab..470f6f17 100644 --- a/crates/clarion-core/src/llm_provider.rs +++ b/crates/clarion-core/src/llm_provider.rs @@ -1454,7 +1454,7 @@ mod tests { allow_live_provider: false, model_id: "anthropic/claude-sonnet-4.6".to_owned(), endpoint_url: "https://openrouter.ai/api/v1".to_owned(), - referer: "https://github.com/qacona/clarion".to_owned(), + referer: "https://github.com/tachyon-beep/clarion".to_owned(), title: "Clarion".to_owned(), }) .expect_err("api key alone must not enable live calls"); @@ -1465,7 +1465,7 @@ mod tests { allow_live_provider: true, model_id: "anthropic/claude-sonnet-4.6".to_owned(), endpoint_url: "https://openrouter.ai/api/v1".to_owned(), - referer: "https://github.com/qacona/clarion".to_owned(), + referer: "https://github.com/tachyon-beep/clarion".to_owned(), title: "Clarion".to_owned(), }) .expect_err("live opt-in without key should fail"); @@ -1476,7 +1476,7 @@ mod tests { allow_live_provider: true, model_id: "anthropic/claude-sonnet-4.6".to_owned(), endpoint_url: "https://openrouter.ai/api/v1".to_owned(), - referer: "https://github.com/qacona/clarion".to_owned(), + referer: "https://github.com/tachyon-beep/clarion".to_owned(), title: "Clarion".to_owned(), }) .expect("live opt-in and key should construct provider"); @@ -1510,7 +1510,7 @@ mod tests { let request = String::from_utf8_lossy(&request[..read]); assert!(request.contains("POST /api/v1/chat/completions HTTP/1.1")); assert!(request.contains("authorization: Bearer secret")); - assert!(request.contains("http-referer: https://github.com/qacona/clarion")); + assert!(request.contains("http-referer: https://github.com/tachyon-beep/clarion")); assert!(request.contains("x-openrouter-title: Clarion")); assert!(request.contains(r#""model":"anthropic/claude-sonnet-4.6""#)); assert!(request.contains(r#""max_tokens":512"#)); @@ -1550,7 +1550,7 @@ mod tests { allow_live_provider: true, model_id: "anthropic/claude-sonnet-4.6".to_owned(), endpoint_url: format!("http://{addr}/api/v1"), - referer: "https://github.com/qacona/clarion".to_owned(), + referer: "https://github.com/tachyon-beep/clarion".to_owned(), title: "Clarion".to_owned(), }) .expect("test provider"); @@ -1671,7 +1671,7 @@ mod tests { allow_live_provider: true, model_id: "anthropic/claude-sonnet-4.6".to_owned(), endpoint_url: format!("http://{addr}/api/v1"), - referer: "https://github.com/qacona/clarion".to_owned(), + referer: "https://github.com/tachyon-beep/clarion".to_owned(), title: "Clarion".to_owned(), }) .expect("test provider"); @@ -1702,7 +1702,7 @@ mod tests { allow_live_provider: true, model_id: "anthropic/claude-sonnet-4.6".to_owned(), endpoint_url: format!("http://{addr}/api/v1"), - referer: "https://github.com/qacona/clarion".to_owned(), + referer: "https://github.com/tachyon-beep/clarion".to_owned(), title: "Clarion".to_owned(), }) .expect("test provider"); @@ -2178,7 +2178,7 @@ printf '%s\n' '{{"type":"result","subtype":"success","structured_output":{{"purp allow_live_provider: true, model_id: "anthropic/claude-sonnet-4.6".to_owned(), endpoint_url: format!("http://{addr}/api/v1"), - referer: "https://github.com/qacona/clarion".to_owned(), + referer: "https://github.com/tachyon-beep/clarion".to_owned(), title: "Clarion".to_owned(), }) .expect("test provider"); diff --git a/crates/clarion-core/src/plugin/discovery.rs b/crates/clarion-core/src/plugin/discovery.rs index 63b83b59..3a7829dc 100644 --- a/crates/clarion-core/src/plugin/discovery.rs +++ b/crates/clarion-core/src/plugin/discovery.rs @@ -17,7 +17,14 @@ //! 1. **Neighbor first**: `/plugin.toml`. //! 2. **Install-prefix fallback** (only when `` has basename `bin`): //! `/../share/clarion/plugins//plugin.toml`. -//! 3. Neither found → [`DiscoveryError::ManifestNotFound`]. +//! 3. **Symlink-resolved install-prefix fallback** (only when `` has +//! basename `bin` and the executable is a symlink, e.g. pipx layout): +//! canonicalise the executable, then try +//! `/../share/clarion/plugins//plugin.toml`. +//! This catches `pipx install` (which puts a symlink in `~/.local/bin/` +//! pointing into `~/.local/share/pipx/venvs//bin/`, with the +//! manifest under that venv's `share/`). +//! 4. None found → [`DiscoveryError::ManifestNotFound`]. //! //! **Limitation**: when multiple `clarion-plugin-*` binaries share the same //! directory (e.g. `/usr/local/bin`), they all resolve to the *same* @@ -358,16 +365,26 @@ fn find_manifest(exec_path: &std::path::Path, suffix: &str) -> Result is a symlink into + // ~/.local/share/pipx/venvs//bin/clarion-plugin-, and + // the manifest lives under that venv's share/. Canonicalise + // the executable and re-try the install-prefix layout from + // the resolved location. + if let Ok(canonical) = exec_path.canonicalize() + && canonical != exec_path + && let Some(canon_parent) = canonical.parent() + && canon_parent.file_name().and_then(|n| n.to_str()) == Some("bin") + && let Some(canon_grandparent) = canon_parent.parent() + && let Some(found) = probe_install_prefix_manifest(canon_grandparent, suffix)? + { return Ok(found); } } @@ -378,6 +395,19 @@ fn find_manifest(exec_path: &std::path::Path, suffix: &str) -> Result Result, DiscoveryError> { + let share_path = prefix + .join("share") + .join("clarion") + .join("plugins") + .join(suffix) + .join("plugin.toml"); + probe_manifest(&share_path) +} + // ── Tests ───────────────────────────────────────────────────────────────────── #[cfg(all(test, unix))] diff --git a/crates/clarion-mcp/Cargo.toml b/crates/clarion-mcp/Cargo.toml index 773662c7..78b72a80 100644 --- a/crates/clarion-mcp/Cargo.toml +++ b/crates/clarion-mcp/Cargo.toml @@ -11,8 +11,8 @@ workspace = true [dependencies] blake3.workspace = true -clarion-core = { path = "../clarion-core", version = "0.1.0-dev" } -clarion-storage = { path = "../clarion-storage", version = "0.1.0-dev" } +clarion-core = { path = "../clarion-core", version = "1.0.0" } +clarion-storage = { path = "../clarion-storage", version = "1.0.0" } reqwest.workspace = true rusqlite.workspace = true serde.workspace = true diff --git a/crates/clarion-mcp/src/config.rs b/crates/clarion-mcp/src/config.rs index e4afb73a..c039b4f8 100644 --- a/crates/clarion-mcp/src/config.rs +++ b/crates/clarion-mcp/src/config.rs @@ -127,7 +127,7 @@ pub struct OpenRouterAttributionConfig { impl Default for OpenRouterAttributionConfig { fn default() -> Self { Self { - referer: "https://github.com/qacona/clarion".to_owned(), + referer: "https://github.com/tachyon-beep/clarion".to_owned(), title: "Clarion".to_owned(), } } diff --git a/crates/clarion-mcp/tests/storage_tools.rs b/crates/clarion-mcp/tests/storage_tools.rs index 85463352..aebcacad 100644 --- a/crates/clarion-mcp/tests/storage_tools.rs +++ b/crates/clarion-mcp/tests/storage_tools.rs @@ -1238,7 +1238,7 @@ async fn summary_openrouter_provider_runs_outside_async_runtime() { allow_live_provider: true, model_id: "anthropic/claude-sonnet-4.6".to_owned(), endpoint_url: format!("http://{addr}/api/v1"), - referer: "https://github.com/qacona/clarion".to_owned(), + referer: "https://github.com/tachyon-beep/clarion".to_owned(), title: "Clarion Test".to_owned(), }) .expect("OpenRouter provider"), diff --git a/crates/clarion-plugin-fixture/Cargo.toml b/crates/clarion-plugin-fixture/Cargo.toml index 3c46f905..1986f170 100644 --- a/crates/clarion-plugin-fixture/Cargo.toml +++ b/crates/clarion-plugin-fixture/Cargo.toml @@ -14,7 +14,7 @@ name = "clarion-plugin-fixture" path = "src/main.rs" [dependencies] -clarion-core = { path = "../clarion-core", version = "0.1.0-dev" } +clarion-core = { path = "../clarion-core", version = "1.0.0" } serde_json.workspace = true [target.'cfg(unix)'.dependencies] diff --git a/crates/clarion-storage/Cargo.toml b/crates/clarion-storage/Cargo.toml index e023d108..44ca0d5d 100644 --- a/crates/clarion-storage/Cargo.toml +++ b/crates/clarion-storage/Cargo.toml @@ -11,7 +11,7 @@ workspace = true [dependencies] blake3.workspace = true -clarion-core = { path = "../clarion-core", version = "0.1.0-dev" } +clarion-core = { path = "../clarion-core", version = "1.0.0" } deadpool-sqlite.workspace = true rusqlite.workspace = true serde_json.workspace = true diff --git a/crates/clarion-storage/src/writer.rs b/crates/clarion-storage/src/writer.rs index 73143ea3..6dbe3594 100644 --- a/crates/clarion-storage/src/writer.rs +++ b/crates/clarion-storage/src/writer.rs @@ -353,6 +353,12 @@ fn insert_entity( state.in_tx = true; } validate_entity_source_file_anchor(conn, entity)?; + // ON CONFLICT(id) DO UPDATE makes `clarion analyze` idempotent across runs: + // a re-walk that produces the same entity updates the existing row instead + // of raising UNIQUE. `created_at` and `first_seen_commit` are preserved + // (the entity was first seen on its original run); `updated_at` and + // `last_seen_commit` are refreshed from the latest run's record. The + // AFTER UPDATE trigger on `entities` keeps `entity_fts` in sync. conn.execute( "INSERT INTO entities ( \ id, plugin_id, kind, name, short_name, \ @@ -370,7 +376,25 @@ fn insert_entity( ?13, ?14, ?15, ?16, \ ?17, ?18, \ ?19, ?20 \ - )", + ) \ + ON CONFLICT(id) DO UPDATE SET \ + plugin_id = excluded.plugin_id, \ + kind = excluded.kind, \ + name = excluded.name, \ + short_name = excluded.short_name, \ + parent_id = excluded.parent_id, \ + source_file_id = excluded.source_file_id, \ + source_file_path = excluded.source_file_path, \ + source_byte_start = excluded.source_byte_start, \ + source_byte_end = excluded.source_byte_end, \ + source_line_start = excluded.source_line_start, \ + source_line_end = excluded.source_line_end, \ + properties = excluded.properties, \ + content_hash = excluded.content_hash, \ + summary = excluded.summary, \ + wardline = excluded.wardline, \ + last_seen_commit = excluded.last_seen_commit, \ + updated_at = excluded.updated_at", params![ entity.id, entity.plugin_id, diff --git a/crates/clarion-storage/tests/writer_actor.rs b/crates/clarion-storage/tests/writer_actor.rs index 9fe9eb56..6ef58a53 100644 --- a/crates/clarion-storage/tests/writer_actor.rs +++ b/crates/clarion-storage/tests/writer_actor.rs @@ -542,6 +542,139 @@ async fn round_trip_insert_persists_entity() { assert_eq!(kind, "function"); } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn insert_entity_is_idempotent_across_runs() { + // Regression: `clarion analyze` re-runs against an unchanged corpus + // must not crash with `UNIQUE constraint failed: entities.id`. The + // insert path UPSERTs on `id`, preserving `created_at`/`first_seen_commit` + // and updating the rest from the new run. WS-D smoke gate. + let dir = tempfile::tempdir().unwrap(); + let path = prepared_db(&dir); + let (writer, handle) = Writer::spawn(path.clone(), 50, 256).unwrap(); + let tx = writer.sender(); + + let entity_id = "python:function:demo.hello"; + let first_created = "2026-05-01T00:00:00Z".to_owned(); + let first_updated = "2026-05-01T00:00:00Z".to_owned(); + let second_updated = "2026-05-02T00:00:00Z".to_owned(); + + // Run 1: insert. + send::<()>(&tx, |ack| WriterCmd::BeginRun { + run_id: "run-1".into(), + config_json: "{}".into(), + started_at: now_iso(), + ack, + }) + .await + .unwrap(); + let mut e = make_entity(entity_id); + e.created_at = first_created.clone(); + e.updated_at = first_updated.clone(); + e.first_seen_commit = Some("commit-abc".to_owned()); + e.last_seen_commit = Some("commit-abc".to_owned()); + send::<()>(&tx, |ack| WriterCmd::InsertEntity { + entity: Box::new(e), + ack, + }) + .await + .unwrap(); + send::<()>(&tx, |ack| WriterCmd::CommitRun { + run_id: "run-1".into(), + status: RunStatus::Completed, + completed_at: now_iso(), + stats_json: "{}".into(), + ack, + }) + .await + .unwrap(); + + // Run 2: re-insert same id with refreshed fields. Must not raise UNIQUE. + send::<()>(&tx, |ack| WriterCmd::BeginRun { + run_id: "run-2".into(), + config_json: "{}".into(), + started_at: now_iso(), + ack, + }) + .await + .unwrap(); + let mut e2 = make_entity(entity_id); + e2.short_name = "hello-v2".to_owned(); + e2.created_at = "2026-12-31T00:00:00Z".to_owned(); + e2.updated_at = second_updated.clone(); + e2.first_seen_commit = Some("commit-NEW-should-be-ignored".to_owned()); + e2.last_seen_commit = Some("commit-xyz".to_owned()); + send::<()>(&tx, |ack| WriterCmd::InsertEntity { + entity: Box::new(e2), + ack, + }) + .await + .unwrap(); + send::<()>(&tx, |ack| WriterCmd::CommitRun { + run_id: "run-2".into(), + status: RunStatus::Completed, + completed_at: now_iso(), + stats_json: "{}".into(), + ack, + }) + .await + .unwrap(); + + drop(tx); + drop(writer); + handle.await.unwrap().unwrap(); + + let pool = ReaderPool::open(&path, 2).unwrap(); + let (count, short_name, created_at, updated_at, first_commit, last_commit): ( + i64, + String, + String, + String, + String, + String, + ) = pool + .with_reader(move |conn| { + let n: i64 = conn.query_row("SELECT COUNT(*) FROM entities", [], |row| row.get(0))?; + let row = conn.query_row( + "SELECT short_name, created_at, updated_at, first_seen_commit, last_seen_commit \ + FROM entities WHERE id = ?1", + rusqlite::params![entity_id], + |row| { + Ok(( + row.get::<_, String>(0)?, + row.get::<_, String>(1)?, + row.get::<_, String>(2)?, + row.get::<_, String>(3)?, + row.get::<_, String>(4)?, + )) + }, + )?; + Ok((n, row.0, row.1, row.2, row.3, row.4)) + }) + .await + .unwrap(); + assert_eq!(count, 1, "second InsertEntity must not duplicate the row"); + assert_eq!( + short_name, "hello-v2", + "mutable fields refresh on re-insert" + ); + assert_eq!( + created_at, first_created, + "created_at is preserved across re-insert" + ); + assert_eq!( + updated_at, second_updated, + "updated_at refreshes to latest run's value" + ); + assert_eq!( + first_commit, "commit-abc", + "first_seen_commit is preserved across re-insert" + ); + assert_eq!( + last_commit, "commit-xyz", + "last_seen_commit refreshes to latest run's value" + ); +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn non_core_plugin_cannot_insert_reserved_entity_kind() { let dir = tempfile::tempdir().unwrap(); diff --git a/docs/clarion/adr/ADR-033-v1.0-distribution.md b/docs/clarion/adr/ADR-033-v1.0-distribution.md new file mode 100644 index 00000000..657523d5 --- /dev/null +++ b/docs/clarion/adr/ADR-033-v1.0-distribution.md @@ -0,0 +1,80 @@ +# ADR-033: v1.0 Distribution via GitHub Releases + +**Status**: Accepted +**Date**: 2026-05-19 +**Deciders**: qacona@gmail.com +**Context**: Clarion v1.0 is the first publishable release; an outside operator must be able to install without `git clone` + `cargo build`. The packaging path was not previously committed. + +## Summary + +Clarion v1.0 distributes via **GitHub Releases** with pre-built binaries for +the Rust core and a Python sdist for the language plugin. Public registries +(crates.io for `clarion-cli`, PyPI for `clarion-plugin-python`) are deferred to +v2.0, after names are reserved and a publish cadence is established. + +## Decision + +A tag matching `v[0-9]+.*` pushed to `main` triggers +`.github/workflows/release.yml`, which: + +1. Builds the `clarion` binary on the v1.0 supported matrix: + - `x86_64-unknown-linux-gnu` + - `x86_64-apple-darwin` + - `aarch64-apple-darwin` + Windows is intentionally out of v1.0 scope: the plugin host's resource + limits use `setrlimit`, which is Unix-only + (`crates/clarion-core/src/plugin/limits.rs`). +2. Builds the Python plugin sdist via `python -m build --sdist plugins/python`. +3. Creates a GitHub Release for the tag, attaching: + - One `clarion-.tar.gz` per platform (binary + LICENSE + README). + - `clarion-plugin-python-.tar.gz` (one sdist, multi-platform). +4. Auto-generates release notes from `git log` between the previous tag and + the new one, filtered to merge commits. + +Install instructions (per `README.md` and `docs/operator/getting-started.md`): + +```bash +# Rust binary — download + extract to ~/.local/bin or equivalent +curl -L https://github.com/tachyon-beep/clarion/releases/download/v1.0.0/clarion-x86_64-unknown-linux-gnu.tar.gz \ + | tar xz -C ~/.local/bin --strip-components=1 clarion-x86_64-unknown-linux-gnu/clarion + +# Python plugin +pipx install https://github.com/tachyon-beep/clarion/releases/download/v1.0.0/clarion-plugin-python-1.0.0.tar.gz +``` + +## Alternatives considered + +**(a) Source-only (`cargo install --git` + `pipx install git+...`).** Ten +minutes of cargo compile on every fresh install. Bad first impression for an +operator evaluating the tool. Useful as a fallback for unsupported platforms +but not the headline path. + +**(c) Public registries (crates.io + PyPI).** Best operator UX, but requires +name reservation on both registries plus version-bump discipline plus a +crates.io publishing token plus a PyPI publishing token plus a release-cadence +commitment. Worth the work for v2.0 when the publish cadence is established; +not worth blocking v1.0 on. + +**`cargo-dist` vs hand-written workflow.** Picked hand-written for v1.0 +because (i) the matrix is small (3 targets), (ii) the workflow doubles as +documentation of what's in each artifact, and (iii) adopting `cargo-dist` +adds a binary dependency on the release toolchain that the team would need to +learn. A future ADR can flip to `cargo-dist` if the matrix grows. + +## Consequences + +- A tag push to `main` produces a binary release; no per-platform manual + build steps. +- Operators install without needing a Rust toolchain or `cargo build` time. +- Windows support is *explicitly* out of v1.0 (`NG-WIN-01` candidate; not + filed as a named non-goal yet because no operator has asked). +- The release workflow is part of the v1.0 surface — broken release builds + block the v1.0 publish via WS-D (external-operator smoke test). +- crates.io / PyPI publishing remains v2.0 work; until then the install path + carries an explicit `github.com/tachyon-beep` URL. + +## Related + +- [WS-C task — Distribution + install ergonomics](../../implementation/v0.1-publish/thread-1-pre-publish-blockers.md#4-workstream-c--distribution--install-ergonomics) +- [ADR-002 — Plugin transport](./ADR-002-plugin-transport-json-rpc.md) — explains why the Python plugin ships as a separate executable on `$PATH` +- [ADR-023 — Tooling baseline](./ADR-023-tooling-baseline.md) — the CI floor the release workflow inherits diff --git a/docs/clarion/adr/README.md b/docs/clarion/adr/README.md index f8c2cc88..a2c0bc69 100644 --- a/docs/clarion/adr/README.md +++ b/docs/clarion/adr/README.md @@ -33,6 +33,7 @@ This folder is the canonical home for authored Clarion architecture decision rec | [ADR-030](./ADR-030-on-demand-summary-scope.md) | On-demand summary scope — narrows WP6 to MCP-driven `summary(id)`; 5-tuple cache key unchanged; module/subsystem aggregation deferred to v0.2 | Accepted | | [ADR-031](./ADR-031-schema-validation-policy.md) | Schema-validation policy — CHECK on closed core-owned vocabularies (`findings.{kind,severity,status}`, `runs.status`); writer-actor + manifest are the only enforcement layer for plugin-extensible vocabularies (`entities.kind`, `edges.kind`) | Accepted | | [ADR-032](./ADR-032-weighted-components-clustering-fallback.md) | Weighted-components clustering fallback naming | Accepted | +| [ADR-033](./ADR-033-v1.0-distribution.md) | v1.0 distribution via GitHub Releases (binary matrix + Python sdist; promote to crates.io/PyPI at v2.0) | Accepted | ## Backlog still tracked in the detailed design diff --git a/docs/operator/README.md b/docs/operator/README.md index dec3f2c5..e5e2aa58 100644 --- a/docs/operator/README.md +++ b/docs/operator/README.md @@ -2,6 +2,9 @@ Practical notes for configuring and running Clarion. +- [Getting started](./getting-started.md) — single-flow walkthrough: install, + analyse a small repo, connect an MCP client, ask three questions, verify + the secret-block. Target ≤15 minutes end-to-end. - [OpenRouter LLM provider](./openrouter.md) — API key, model ID, attribution headers, and token-ceiling configuration for v0.1. - [Coding-agent LLM providers](./coding-agent-llm-providers.md) — Codex CLI diff --git a/docs/operator/getting-started.md b/docs/operator/getting-started.md new file mode 100644 index 00000000..6cf7214a --- /dev/null +++ b/docs/operator/getting-started.md @@ -0,0 +1,291 @@ +# Getting started with Clarion + +A single-flow walkthrough that takes you from an empty machine to a working +consult-mode agent asking real questions about a real codebase. Target time: +**≤15 minutes** once prerequisites are in place. + +You will: + +1. [Install Clarion + the Python plugin.](#1-install) +2. [Run `clarion analyze` against a small public Python project.](#2-analyze) +3. [Start `clarion serve` and connect an MCP client.](#3-serve) +4. [Ask three questions through the MCP tools.](#4-ask) +5. [Verify the secret-scanner block fires on a planted secret.](#5-secret-block) + +If a step fails, see [Troubleshooting](#troubleshooting) at the end. + +## Prerequisites + +| Tool | Required version | How to check | +|---|---|---| +| Rust toolchain | `stable` per [`rust-toolchain.toml`](../../rust-toolchain.toml) | `rustc --version` | +| Python | `>= 3.11` per the [plugin manifest](../../plugins/python/pyproject.toml) | `python3 --version` | +| `pipx` (recommended for plugin install) | any recent | `pipx --version` | +| `pyright-langserver` | `1.1.409` — pinned in the [plugin manifest](../../plugins/python/plugin.toml) (`capabilities.runtime.pyright.pin`) | `pyright --version` (the `pyright-langserver` entrypoint only accepts protocol flags like `--stdio`) | +| An MCP client | any MCP-speaking client | see [§3](#3-serve) | + +The Python plugin will fail at runtime if `pyright-langserver` is not on +`$PATH` at the pinned version (1.1.409 in v1.0). Install via +`npm install -g pyright@1.1.409` or `pipx install pyright==1.1.409`. + +### Required environment variables + +For step 4's `summary` question you need an OpenRouter API key: + +```bash +export OPENROUTER_API_KEY=sk-or-v1-... +``` + +`clarion analyze` (step 2) and the structural MCP tools (`entity_at`, +`find_entity`, `callers_of`, `execution_paths_from`, `issues_for`, +`neighborhood`) work without any LLM credentials — the key is only consulted +when an MCP client calls `summary(id)` against an entity that does not yet +have a cached summary. + +## 1. Install + +> Once the v1.0 release artifacts ship via GitHub Releases (per +> [ADR-033](../clarion/adr/ADR-033-v1.0-distribution.md)), the install +> commands below collapse to a single `curl | sh` plus +> `pipx install ./clarion-plugin-python-*.tar.gz`. Until the first tag fires, +> install from source. + +```bash +# Rust core +cargo install --git https://github.com/tachyon-beep/clarion clarion-cli + +# Python plugin (provides clarion-plugin-python on $PATH) +pipx install git+https://github.com/tachyon-beep/clarion#subdirectory=plugins/python +``` + +Verify the discovery surface: + +```bash +which clarion # e.g. ~/.cargo/bin/clarion +which clarion-plugin-python # e.g. ~/.local/bin/clarion-plugin-python +``` + +**`$PATH` discipline matters.** Clarion's plugin host (per +[ADR-002](../clarion/adr/ADR-002-plugin-transport-json-rpc.md)) discovers +plugins by walking `$PATH` for executables matching `clarion-plugin-*`. If +`pipx`'s install directory (`~/.local/bin/` on Linux, `~/Library/...` on +macOS) is not on your shell's `$PATH`, `clarion analyze` will exit +**successfully** with status `skipped_no_plugins` and emit a `WARN no plugins +discovered` line — the analyse pass produces nothing. See +[Troubleshooting → "analyze runs but emits no entities"](#analyze-runs-but-emits-no-entities) +below for the diagnostic. + +## 2. Analyze + +Pick a small, well-behaved Python project. The walkthrough uses the `requests` +library's source tree: + +```bash +cd /tmp +curl -L -o requests-2.32.4.tar.gz https://github.com/psf/requests/archive/refs/tags/v2.32.4.tar.gz +tar xzf requests-2.32.4.tar.gz +cd requests-2.32.4 +``` + +Initialise Clarion's project-local state, then run the analyser: + +```bash +clarion install +clarion analyze +``` + +Expected output (abridged): + +``` +applying migration version=1 name="0001_initial_schema" +clarion install complete clarion_dir=/tmp/requests-2.32.4/.clarion +Initialised /tmp/requests-2.32.4/.clarion +... +analyze complete: run ok (entities=NNN, edges=MMM) +``` + +The first run on a tree of this size completes in well under a minute on +typical hardware. The result lives at `.clarion/clarion.db` (a single SQLite +file) and is safe to commit to git — see +[ADR-005](../clarion/adr/ADR-005-clarion-dir-tracking.md). + +## 3. Serve + +Start the MCP stdio server in one shell: + +```bash +clarion serve --path /tmp/requests-2.32.4 +``` + +`clarion serve` speaks the MCP protocol over stdio. Any MCP client works; +documented options: + +- **Claude Desktop.** Add to your `claude_desktop_config.json`: + + ```json + { + "mcpServers": { + "clarion-requests": { + "command": "/path/to/clarion", + "args": ["serve", "--path", "/tmp/requests-2.32.4"], + "env": { + "OPENROUTER_API_KEY": "sk-or-v1-..." + } + } + } + } + ``` + +- **MCP Inspector** (`npm install -g @modelcontextprotocol/inspector`) for + ad-hoc tool-level exploration without an agent in the loop: + + ```bash + npx @modelcontextprotocol/inspector clarion serve --path /tmp/requests-2.32.4 + ``` + +Pick whichever you have; the questions in step 4 are client-agnostic. + +## 4. Ask + +### Enable live LLM (one-time) + +The structural MCP tools work out of the box, but `summary(id)` (question 3 +below) needs the live OpenRouter path explicitly opted into. Edit +`/tmp/requests-2.32.4/clarion.yaml` and set both: + +```yaml +llm_policy: + enabled: true + allow_live_provider: true +``` + +`OPENROUTER_API_KEY` must also be exported in the environment that +`clarion serve` (or your MCP client wrapper) inherits — see the +prerequisites section above. Skip this block if you don't have a key; the +first six tools still work, only `summary` will return an "LLM disabled" +envelope. + +### The seven tools + +The MCP surface exposes seven primary tools (plus `subsystem_members` for +clustering output). Each is a structured graph query, not free-text grep. + +| Tool | Example invocation | +|---|---| +| `entity_at(file, line)` | `entity_at(file="requests/sessions.py", line=480)` — which entity covers this source location? | +| `find_entity(pattern)` | `find_entity(pattern="Session.send")` — find entities matching a name or summary fragment. | +| `callers_of(id)` | `callers_of(id="python:function:requests.sessions.Session.send")` — who calls this function? Default confidence is `resolved`. | +| `execution_paths_from(id, max_depth)` | `execution_paths_from(id="python:function:requests.api.get", max_depth=3)` — bounded calls-only paths from an entry point. | +| `summary(id)` | `summary(id="python:function:requests.sessions.Session.send")` — structured LLM summary with `purpose` / `behavior` / `relationships` / `risks` fields. Requires the live-LLM opt-in above plus `OPENROUTER_API_KEY`. First call dispatches the LLM and caches; subsequent calls hit the cache. | +| `issues_for(id)` | `issues_for(id="python:module:requests.sessions")` — Filigree issues attached to this entity, if Filigree is reachable. Returns an `unavailable` envelope if not (Filigree is enrich-only). | +| `neighborhood(id)` | `neighborhood(id="python:function:requests.sessions.Session.send")` — callers, callees, container, contained entities, and references in one hop. | + +The three questions to walk through with your agent: + +1. **"List the top-level modules in this project."** Exercises `find_entity` + with a broad pattern. +2. **"What calls `requests.get`?"** Exercises `callers_of` against a + well-known entry point. +3. **"Summarise `requests.sessions.Session.send`."** Exercises the live LLM + path (`summary`), the OpenRouter provider, the budget ledger, and the + summary cache. The second invocation of the same `summary(id)` is a cache + hit; verify by re-asking and noting the near-zero latency. + +A successful run gives you three substantive, graph-grounded answers — not +"here is what grep found." If the agent improvises by reading source files +directly, the answer is real but does not exercise the MCP surface; check +that your client actually called the tools. + +Re-run analyse for idempotency: + +```bash +clarion analyze +# entity/edge counts on the second run should match the first +``` + +## 5. Secret-block + +Plant a fake AWS credential and re-run analyse: + +```bash +cat > .env <<'EOF' +AWS_ACCESS_KEY_ID=AKIA0123456789ABCDEF +EOF + +clarion analyze +``` + +Expected behaviour: + +- `clarion analyze` exits **0** with run status `completed`. +- A `CLA-SEC-SECRET-DETECTED` finding lands in `findings` with the message + `AwsAccessKeyId detected in /tmp/requests-2.32.4/.env:1`. Inspect with + `sqlite3 .clarion/clarion.db "SELECT rule_id, message FROM findings + WHERE rule_id LIKE 'CLA-SEC%';"`. +- The `.env` file itself has no language entities (it's not Python), so + the finding is anchored to the core-minted file entity rather than a + language-plugin entity. Source files in the project that the scanner + also flags (e.g. high-entropy strings in `requests/utils.py`) get + `properties.briefing_blocked = "secret_present"` on their containing + module entity, and the `summary(id)` MCP tool returns a + `briefing_blocked: "secret_present"` envelope instead of dispatching + the LLM. + +Full mechanics — baseline format, override flags, audit queries — in +[secret-scanning.md](./secret-scanning.md). + +## Troubleshooting + +### `analyze` runs but emits no entities + +Look for `WARN no plugins discovered` and `skipped_no_plugins` in the +analyse output. The plugin host walks `$PATH` for `clarion-plugin-*` +executables; if your shell's `$PATH` does not include `pipx`'s install +directory the plugin is invisible. + +Confirm and fix: + +```bash +which clarion-plugin-python || echo "not on PATH" +echo $PATH # is pipx's bin dir in here? + +# If pipx is installed but its bin dir is missing: +pipx ensurepath # writes the PATH update; restart shell +``` + +Note: `clarion analyze` deliberately exits **0** even when no plugins are +discovered, so the run can be re-attempted without manual cleanup. The +`WARN` line and the `skipped_no_plugins` run status are the operator-facing +signals. A `clarion doctor` subcommand that surfaces discovery state at exit +is on the v2.0 roadmap; for v1.0 the diagnostic is the WARN line plus the +`which clarion-plugin-*` check above. + +### "secret_present" block fires on a real file + +Add the file to `.clarion/secrets-baseline.yaml` with a written justification +(the schema requires it). Full procedure: [secret-scanning.md](./secret-scanning.md). + +### `summary` returns an error citing budget or LLM provider + +Check `OPENROUTER_API_KEY` is set in the environment that `clarion serve` +inherits (for Claude Desktop that means the `env` block in the MCP-server +config). Live LLM calls are also gated by `llm_policy.enabled: true` and +`llm_policy.allow_live_provider: true` in `clarion.yaml` — see +[openrouter.md](./openrouter.md). + +### `issues_for` returns an `unavailable` envelope + +Expected when Filigree is not reachable. Filigree integration is +*enrich-only* per the Loom federation axiom — Clarion's structural answers +are unaffected. See +[CON-FILIGREE-02](../clarion/v0.1/requirements.md#con-filigree-02--file-registry-displacement-is-deferred-to-v02) +for the v1.0 → v2.0 trajectory. + +## Where to go next + +- [Operator notes index](./README.md) — OpenRouter, runtime topology, + secret scanning, federation contracts, coding-agent LLM providers. +- [Design ladder](../clarion/v0.1/README.md) — requirements → system-design → + detailed-design. +- [ADR index](../clarion/adr/README.md) — accepted architecture decisions. +- [CLAUDE.md](../../CLAUDE.md) — repository conventions. diff --git a/docs/operator/openrouter.md b/docs/operator/openrouter.md index 17b99d5c..b8b5a440 100644 --- a/docs/operator/openrouter.md +++ b/docs/operator/openrouter.md @@ -22,7 +22,7 @@ llm_policy: endpoint_url: https://openrouter.ai/api/v1 api_key_env: OPENROUTER_API_KEY attribution: - referer: https://github.com/qacona/clarion + referer: https://github.com/tachyon-beep/clarion title: Clarion model_id: anthropic/claude-sonnet-4.6 session_token_ceiling: 1000000 diff --git a/plugins/python/pyproject.toml b/plugins/python/pyproject.toml index d7ff461b..6e4aac1a 100644 --- a/plugins/python/pyproject.toml +++ b/plugins/python/pyproject.toml @@ -4,8 +4,8 @@ build-backend = "hatchling.build" [project] name = "clarion-plugin-python" -version = "0.1.5" -description = "Clarion Python language plugin — walking-skeleton v0.1 baseline" +version = "1.0.0" +description = "Clarion Python language plugin — v1.0 release" readme = "README.md" requires-python = ">=3.11" authors = [{ name = "John Morrissey", email = "qacona@gmail.com" }] diff --git a/tests/e2e/external-operator-smoke.md b/tests/e2e/external-operator-smoke.md new file mode 100644 index 00000000..75ab48cb --- /dev/null +++ b/tests/e2e/external-operator-smoke.md @@ -0,0 +1,64 @@ +# External-operator smoke test (publish gate) + +Source: WS-D of [thread-1-pre-publish-blockers.md](../../docs/implementation/v0.1-publish/thread-1-pre-publish-blockers.md#5-workstream-d--publish-gate-external-operator-smoke-test) +and Filigree issue `clarion-3e0e481ef7`. + +## Purpose + +Verify that an outside operator, working from a fresh machine and reading +**only** the top-level `README.md` and `docs/operator/getting-started.md`, can +install Clarion, analyse a small public Python project, connect a consult-mode +MCP client, ask substantive questions, re-run idempotently, and observe the +pre-ingest secret-block fire on a planted credential. + +The test fails if any step requires reading source code outside of `README.md` +and `docs/operator/getting-started.md`. A failure here is a B.1/B.2 docs bug, +not a runtime defect. + +## Test environment + +| Knob | Value | +|---|---| +| OS | `ubuntu:24.04` (or equivalent fresh image) | +| Operator | Someone (or a fresh agent session) with no prior knowledge of this repo | +| Working dir | `/tmp/smoke-test/` inside the container | +| `OPENROUTER_API_KEY` | Required for steps 4.3 and 5; the test can run *without* the key but steps that depend on live LLM dispatch are then explicit skips, not silent passes | +| Network | Outbound HTTPS to `api.openrouter.ai` and `github.com` (and PyPI/crates if installing from source) | + +## Steps + +| # | Step | Pass criteria | +|---|---|---| +| 1 | Install the Rust binary per WS-C's chosen path (GitHub Releases per ADR-033) | `clarion --version` prints a version on `$PATH` | +| 2 | Install the Python plugin via `pipx` per the tutorial | `which clarion-plugin-python` resolves; the binary is on `$PATH` | +| 3 | `clarion install` against a small Python project (`requests` source tarball ~7k LOC) | `.clarion/` exists; `.clarion/clarion.db` exists; init log line emitted | +| 4.1 | `clarion analyze` | Exit 0; `analyze complete: run ok` reported; entity count > 0 | +| 4.2 | Start `clarion serve` in one shell; connect MCP client in another | MCP `tools/list` returns the 8 tools (`entity_at`, `find_entity`, `callers_of`, `execution_paths_from`, `summary`, `issues_for`, `neighborhood`, `subsystem_members`) | +| 4.3 | Three pre-scripted questions over MCP:
(a) `find_entity(pattern="Session")` lists module-level matches
(b) `callers_of(id="python:function:requests.api.get|function")` returns a non-empty list
(c) `summary(id="python:function:requests.sessions.Session.send|function")` returns a paragraph | (a) and (b) return non-empty; (c) returns a paragraph (live LLM) or `null` summary plus a documented skip if `OPENROUTER_API_KEY` absent | +| 5 | Re-run `clarion analyze` | Idempotent: entity/edge counts on the second run match the first | +| 6 | Plant `.env` with `AWS_ACCESS_KEY_ID=AKIA0123456789ABCDEF`; re-run `clarion analyze` | Exit 0 (`soft_failed`); `CLA-SEC-SECRET-DETECTED` finding in `findings` table; affected entities carry `briefing_blocked = "secret_present"` | +| 7 | `clarion serve` against the post-secret-block DB; call `summary(id)` on a blocked entity's id | Returns `briefing_blocked` envelope with no LLM call | +| 8 | Operator-improvisation tally | Total count of "I had to look at the source to understand what to do next" events. **Target: 0.** Any positive count is a B.1/B.2 docs bug. | + +## Recording results + +Capture the smoke test as a single Markdown report at +`tests/e2e/external-operator-smoke-results-.md` with: + +- The date / agent identity / container image tag. +- Per-step: status (`pass` / `fail` / `skip`), command run, abridged stdout + excerpt, and any improvisation events that occurred. +- A final aggregate verdict: `gate_passed: true` only if all non-skipped steps + pass and the improvisation tally is 0. + +Attach the report as a comment on Filigree issue `clarion-3e0e481ef7` before +closing. + +## Repeating on additional platforms + +This document specifies one (Linux x86_64) run. The release matrix in +[ADR-033](../../docs/clarion/adr/ADR-033-v1.0-distribution.md) also produces +`x86_64-apple-darwin` and `aarch64-apple-darwin` binaries; re-run this +checklist on at least one macOS target before the v1.0 tag is treated as +generally publishable. Document each platform's run as a separate +`*-results-*.md` file. From a896b096dd0aa47d5b2dbd861e8e6627dd4d77df Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Tue, 19 May 2026 13:36:55 +1000 Subject: [PATCH 012/187] chore: backfill filigree v2.0 bootstrap artifacts - AGENTS.md: per-repo agent instructions anchor written by `filigree doctor` on first invocation under v2.0.3. - .mcp.json: auto-reformatted by filigree-mcp (whitespace normalisation + trailing newline + dropped redundant empty `env`); no functional change. --- .mcp.json | 11 +++--- AGENTS.md | 102 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+), 5 deletions(-) create mode 100644 AGENTS.md diff --git a/.mcp.json b/.mcp.json index 24a20674..d8da9760 100644 --- a/.mcp.json +++ b/.mcp.json @@ -3,18 +3,19 @@ "filigree": { "type": "stdio", "command": "/home/john/.local/bin/filigree-mcp", - "args": [], - "env": {} + "args": [] }, "clarion": { "type": "stdio", "command": "/home/john/clarion/target/release/clarion", "args": [ "serve", - "--path", "/tmp/clarion-b8-elspeth-full-20260518T0016Z", - "--config", "/tmp/clarion-b8-elspeth-full-20260518T0016Z/clarion-b8-live.yaml" + "--path", + "/tmp/clarion-b8-elspeth-full-20260518T0016Z", + "--config", + "/tmp/clarion-b8-elspeth-full-20260518T0016Z/clarion-b8-live.yaml" ], "env": {} } } -} \ No newline at end of file +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..dcd3c39b --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,102 @@ + +## Filigree Issue Tracker + +`filigree` tracks tasks for this project. Data lives in `.filigree/`. Prefer +the MCP tools (`mcp__filigree__*`) when available; fall back to the `filigree` +CLI otherwise. + +### Workflow + +```bash +# At session start +filigree session-context # ready / in-progress / critical path + +# Pick up the next ready issue (atomic claim + transition to in_progress) +filigree start-next-work --assignee +# ...or claim a specific issue +filigree start-work --assignee + +# Do the work, commit, then +filigree close +``` + +Use the atomic claim+transition verbs — `start_work` / `start_next_work` +(MCP) or `start-work` / `start-next-work` (CLI). Do **not** chain +`claim_issue` (MCP) or `filigree claim` (CLI) with a subsequent status +update — the two-step form races against other agents; the combined verb is +atomic. + +### Observations: when (and when not) to use them + +`observe` is a fire-and-forget scratchpad for *incidental* defects — things +you notice *outside the scope of your current task* (a code smell in a +neighbouring file, a stale TODO, a missing test for an edge case you happened +to spot). Notes expire after 14 days unless promoted. Include `file_path` and +`line` when relevant. At session end, skim `list_observations` and either +`dismiss_observation` or `promote_observation` for what has accumulated. + +**You fix bugs in your currently defined scope. You do NOT use observations +to finish work prematurely.** If a defect, gap, or follow-up belongs to your +current task, you own it — handle it as part of that task: fix it now, expand +the task's scope, file a proper issue with a dependency, or surface it to the +user. Filing it as an observation and closing the task is *not* completing +the task; it is shipping known-broken work and hiding the debt in a 14-day +expiring scratchpad. The test is "would I have noticed this even if I weren't +working on this task?" If no, it's task scope, not an observation. + +### Priority scale + +- P0: Critical (drop everything) +- P1: High (do next) +- P2: Medium (default) +- P3: Low +- P4: Backlog + +### Reaching for tools + +MCP tool schemas describe each tool; `filigree --help` and `filigree +--help` are the authoritative CLI reference. You do not need to memorise +either catalogue. The verbs you will reach for most: + +- **Find work:** `get_ready`, `get_blocked`, `list_issues`, `search_issues` +- **Claim work:** `start_work`, `start_next_work` +- **Update:** `add_comment`, `add_label`, `update_issue`, `close_issue` +- **Scratchpad:** `observe`, `list_observations`, `promote_observation`, `dismiss_observation` +- **Cross-product entity bindings (ADR-029):** `add_entity_association`, + `remove_entity_association`, `list_entity_associations`, + `list_associations_by_entity`. Used when a sibling tool (e.g. + Clarion) needs to bind a Filigree issue to a function, class, or + module identifier it owns. The `entity_id` is an opaque string + from Filigree's perspective; the consumer (the sibling tool's read + path) does drift detection against the stored + `content_hash_at_attach`. `list_associations_by_entity` is the + reverse-lookup surface — given a Clarion entity ID, return every + Filigree issue bound to it (project isolation is by DB file). Also + reachable over HTTP as + `GET/POST /api/issue/{issue_id}/entity-associations`, + `DELETE /api/issue/{issue_id}/entity-associations?entity_id=…`, + and `GET /api/entity-associations?entity_id=…`. +- **Health:** `get_stats`, `get_metrics`, `get_mcp_status` + +Pass `--actor ` (CLI) so events attribute to your agent identity. + +### Error handling + +Errors return `{error: str, code: ErrorCode, details?: dict}`. Switch on +`code`, not on message text. Codes: `VALIDATION`, `NOT_FOUND`, `CONFLICT`, +`INVALID_TRANSITION`, `PERMISSION`, `NOT_INITIALIZED`, `IO`, +`INVALID_API_URL`, `STOP_FAILED`, `SCHEMA_MISMATCH`, `INTERNAL`. + +On `INVALID_TRANSITION`, call `get_valid_transitions` (MCP) or +`filigree transitions ` to see what the workflow allows from here. + +Two failure modes deserve a specific response: + +- **`SCHEMA_MISMATCH`** — the installed `filigree` is older than the project + database. The error message contains upgrade guidance. Surface it to the + user; do not retry. +- **`ForeignDatabaseError`** — filigree found a parent project's database + but no local `.filigree.conf`. Run `filigree init` in the current + directory. Do **not** `cd` upward to a different project unless that was + the actual intent. + From fbf29031151771b0728cd5600cc0b04ca0ade98d Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Tue, 19 May 2026 14:07:47 +1000 Subject: [PATCH 013/187] fix(wp5): C1-C4 critical scanner gaps (clarion-55fc5aa885) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit C1 — HTTP /api/v1/files now refuses briefing-blocked entities. resolve_file surfaces briefing_blocked from properties_json via the new entity_briefing_block_reason helper; http_read::get_file returns 404 + an audit tracing::warn! instead of leaking the entity_id/content_hash. Storage contract covered by three query_helpers tests. The federation read surface is now consistent with MCP summary/callers_of_inferred which already gate on this property. Integration test for the HTTP surface is filed separately (clarion-5d6c01fe1a) — the spawn-serve harness hangs on the blocked-entity codepath, root cause TBD. C2 — Sidecar walk for .env/.env.* files now logs each walk error and emits a tail count, mirroring analyze.rs::collect_source_files. Filtering walk errors with filter_map(Result::ok) hid ACL/perm/dangling-symlink failures from the security gate. C3 — Claude CLI provider no longer falls back to raw stdout when no `result` event or `structured_output`/`structuredOutput`/`result` field is present. Both the event-selection step and claude_structured_output_json now return InvalidResponse (retryable) so callers do not persist event payloads as summaries. C4 — docs/operator/secret-scanning.md no longer references a non-existent `--no-llm` flag; the real control is `llm.allow_live_provider: false` or leaving `CLARION_LLM_LIVE` unset, both of which keep the recording provider as the only dispatch path. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/clarion-cli/src/http_read.rs | 30 ++++-- crates/clarion-cli/src/secret_scan/files.rs | 37 +++++-- crates/clarion-core/src/llm_provider.rs | 15 ++- crates/clarion-storage/src/lib.rs | 8 +- crates/clarion-storage/src/query.rs | 21 ++++ crates/clarion-storage/tests/query_helpers.rs | 100 +++++++++++++++++- docs/operator/secret-scanning.md | 2 +- 7 files changed, 186 insertions(+), 27 deletions(-) diff --git a/crates/clarion-cli/src/http_read.rs b/crates/clarion-cli/src/http_read.rs index ccb7ab78..54f50263 100644 --- a/crates/clarion-cli/src/http_read.rs +++ b/crates/clarion-cli/src/http_read.rs @@ -138,16 +138,26 @@ async fn get_file(State(state): State, Query(query): Query) .with_reader(move |conn| resolve_file(conn, &project_root, &file_path, &language)) .await; match result { - Ok(Some(file)) => ( - StatusCode::OK, - Json(FileResponse { - entity_id: file.entity_id, - content_hash: file.content_hash, - canonical_path: file.canonical_path, - language: file.language, - }), - ) - .into_response(), + Ok(Some(file)) => { + if let Some(reason) = file.briefing_blocked.as_deref() { + tracing::warn!( + path = %file.canonical_path, + reason = %reason, + "HTTP /api/v1/files refusing to expose briefing-blocked entity" + ); + return json_error(StatusCode::NOT_FOUND, "file is not known to Clarion"); + } + ( + StatusCode::OK, + Json(FileResponse { + entity_id: file.entity_id, + content_hash: file.content_hash, + canonical_path: file.canonical_path, + language: file.language, + }), + ) + .into_response() + } Ok(None) => json_error(StatusCode::NOT_FOUND, "file is not known to Clarion"), Err(err) => json_error(StatusCode::BAD_REQUEST, &err.to_string()), } diff --git a/crates/clarion-cli/src/secret_scan/files.rs b/crates/clarion-cli/src/secret_scan/files.rs index 69f84fe7..9743a506 100644 --- a/crates/clarion-cli/src/secret_scan/files.rs +++ b/crates/clarion-cli/src/secret_scan/files.rs @@ -31,6 +31,7 @@ pub(crate) fn collect_scan_files(root: &Path, source_files: &[PathBuf]) -> Vec

Vec { let mut out = Vec::new(); + let mut skipped: u64 = 0; let mut builder = WalkBuilder::new(root); builder .follow_links(false) @@ -43,15 +44,37 @@ fn collect_secret_scan_sidecars(root: &Path) -> Vec { .require_git(false) .filter_entry(|entry| !is_skipped_dir(entry)); - for entry in builder.build().filter_map(std::result::Result::ok) { - let Some(file_type) = entry.file_type() else { - continue; - }; - let path = entry.into_path(); - if file_type.is_file() && is_secret_scan_sidecar(&path) { - out.push(path); + for result in builder.build() { + match result { + Ok(entry) => { + let Some(file_type) = entry.file_type() else { + continue; + }; + let path = entry.into_path(); + if file_type.is_file() && is_secret_scan_sidecar(&path) { + out.push(path); + } + } + Err(err) => { + tracing::warn!( + error = %err, + "sidecar walk: skipping unreadable entry; secret-scan coverage is incomplete \ + for this path", + ); + skipped += 1; + } } } + + if skipped > 0 { + tracing::warn!( + skipped = skipped, + root = %root.display(), + "sidecar walk skipped {skipped} unreadable entr{suffix}; secret-scan gate is \ + incomplete for those paths", + suffix = if skipped == 1 { "y" } else { "ies" }, + ); + } out } diff --git a/crates/clarion-core/src/llm_provider.rs b/crates/clarion-core/src/llm_provider.rs index 470f6f17..d9f4fce7 100644 --- a/crates/clarion-core/src/llm_provider.rs +++ b/crates/clarion-core/src/llm_provider.rs @@ -1044,7 +1044,13 @@ fn parse_claude_cli_json_output(stdout: &[u8]) -> Result Result, } const MODULE_ANCESTOR_MAX_DEPTH: i64 = 32; @@ -168,6 +174,7 @@ pub fn resolve_file( let normalized = normalize_source_path(project_root, file)?; let canonical_path = project_relative_path(project_root, Path::new(&normalized))?; if let Some(entity) = source_entity_for_path(conn, &normalized, Some("file"))? { + let briefing_blocked = entity_briefing_block_reason(&entity.properties_json); return Ok(Some(ResolvedFile { entity_id: entity.id, content_hash: entity @@ -176,11 +183,13 @@ pub fn resolve_file( .unwrap_or_default(), canonical_path, language: resolved_language(language, &entity.plugin_id, Path::new(&normalized)), + briefing_blocked, })); } let Some(anchor) = source_entity_for_path(conn, &normalized, None)? else { return Ok(None); }; + let briefing_blocked = entity_briefing_block_reason(&anchor.properties_json); let content_hash = anchor .content_hash .or_else(|| file_content_hash(Path::new(&normalized))) @@ -191,9 +200,21 @@ pub fn resolve_file( content_hash, canonical_path, language: resolved_language(language, &anchor.plugin_id, Path::new(&normalized)), + briefing_blocked, })) } +/// Extract the `briefing_blocked` reason from an entity's `properties` JSON +/// column. Shared with `clarion-mcp` (which makes the same call inline) so +/// federation read surfaces enforce the block uniformly. +pub fn entity_briefing_block_reason(properties_json: &str) -> Option { + serde_json::from_str::(properties_json) + .ok()? + .get("briefing_blocked")? + .as_str() + .map(str::to_owned) +} + fn source_entity_for_path( conn: &Connection, normalized_path: &str, diff --git a/crates/clarion-storage/tests/query_helpers.rs b/crates/clarion-storage/tests/query_helpers.rs index d0c194aa..219315d9 100644 --- a/crates/clarion-storage/tests/query_helpers.rs +++ b/crates/clarion-storage/tests/query_helpers.rs @@ -5,9 +5,10 @@ use std::path::Path; use clarion_core::EdgeConfidence; use clarion_storage::{ ModuleDependencyEdge, ReferenceDirection, SubsystemMember, call_edges_from, - call_edges_targeting, child_entity_ids, contained_entity_ids, entity_at_line, entity_by_id, - find_entities, module_dependency_edges, normalize_source_path, pragma, - reference_edges_for_entity, schema, subsystem_for_member, subsystem_members, + call_edges_targeting, child_entity_ids, contained_entity_ids, entity_at_line, + entity_briefing_block_reason, entity_by_id, find_entities, module_dependency_edges, + normalize_source_path, pragma, reference_edges_for_entity, resolve_file, schema, + subsystem_for_member, subsystem_members, }; use rusqlite::{Connection, params}; @@ -751,3 +752,96 @@ fn normalize_source_path_accepts_project_relative_paths_and_rejects_escape() { "unexpected error: {escaped}" ); } + +#[test] +fn resolve_file_surfaces_briefing_blocked_reason_from_properties() { + let tempdir = tempfile::tempdir().expect("temp project root"); + let project_root = tempdir.path(); + let source_path = project_root.join("secret.env"); + std::fs::write(&source_path, "TOKEN=AKIAIOSFODNN7EXAMPLE\n").expect("write source"); + let canonical = source_path.canonicalize().expect("canonical source"); + + let conn = open_fresh(&tempdir); + conn.execute( + "INSERT INTO entities ( + id, plugin_id, kind, name, short_name, source_file_path, + source_line_start, source_line_end, properties, content_hash, created_at, updated_at + ) VALUES ( + 'core:file:hash-secret@secret.env', 'core', 'file', 'secret.env', 'secret.env', ?1, + 1, 1, '{\"briefing_blocked\":\"secret_present\"}', 'hash-secret-file', + '2026-05-19T00:00:00.000Z', '2026-05-19T00:00:00.000Z' + )", + params![canonical.display().to_string()], + ) + .expect("insert briefing-blocked entity"); + + let resolved = resolve_file(&conn, project_root, "secret.env", "env") + .expect("resolve_file") + .expect("entity is known"); + + assert_eq!( + resolved.briefing_blocked.as_deref(), + Some("secret_present"), + "resolve_file must surface briefing_blocked reason so federation read \ + surfaces (HTTP /api/v1/files) can refuse to expose blocked entities" + ); +} + +#[test] +fn resolve_file_returns_none_briefing_blocked_for_clean_entity() { + let tempdir = tempfile::tempdir().expect("temp project root"); + let project_root = tempdir.path(); + let source_path = project_root.join("demo.py"); + std::fs::write(&source_path, "def entry():\n return 1\n").expect("write source"); + let canonical = source_path.canonicalize().expect("canonical source"); + + let conn = open_fresh(&tempdir); + conn.execute( + "INSERT INTO entities ( + id, plugin_id, kind, name, short_name, source_file_path, + source_line_start, source_line_end, properties, content_hash, created_at, updated_at + ) VALUES ( + 'python:file:demo', 'python', 'file', 'demo.py', 'demo.py', ?1, + 1, 2, '{}', 'hash-demo', + '2026-05-19T00:00:00.000Z', '2026-05-19T00:00:00.000Z' + )", + params![canonical.display().to_string()], + ) + .expect("insert clean entity"); + + let resolved = resolve_file(&conn, project_root, "demo.py", "python") + .expect("resolve_file") + .expect("entity is known"); + + assert!( + resolved.briefing_blocked.is_none(), + "clean entity must not surface a briefing_blocked reason; got {:?}", + resolved.briefing_blocked + ); +} + +#[test] +fn entity_briefing_block_reason_parses_property_and_tolerates_garbage() { + assert_eq!( + entity_briefing_block_reason(r#"{"briefing_blocked":"secret_present"}"#), + Some("secret_present".to_owned()), + ); + assert_eq!( + entity_briefing_block_reason(r#"{"briefing_blocked":"unscanned_source"}"#), + Some("unscanned_source".to_owned()), + ); + // No key. + assert_eq!(entity_briefing_block_reason("{}"), None); + assert_eq!(entity_briefing_block_reason(r#"{"other":"x"}"#), None); + // Wrong type — key present but not a string. + assert_eq!( + entity_briefing_block_reason(r#"{"briefing_blocked":42}"#), + None, + ); + // Malformed JSON must not panic. + assert_eq!(entity_briefing_block_reason(""), None); + assert_eq!(entity_briefing_block_reason("not json"), None); + // Non-object root. + assert_eq!(entity_briefing_block_reason("null"), None); + assert_eq!(entity_briefing_block_reason("[]"), None); +} diff --git a/docs/operator/secret-scanning.md b/docs/operator/secret-scanning.md index 17fa26d5..631c05ce 100644 --- a/docs/operator/secret-scanning.md +++ b/docs/operator/secret-scanning.md @@ -78,7 +78,7 @@ Filigree integration for scanner findings is planned for v0.2. Until then, the l ## Limitations -The scanner is pattern-based. It can miss novel internal key formats and it can flag high-entropy test data. Use a justified baseline for reviewed false positives, and prefer `--no-llm` or an air-gapped workflow for repos where any source disclosure would be unacceptable. +The scanner is pattern-based. It can miss novel internal key formats and it can flag high-entropy test data. Use a justified baseline for reviewed false positives, and disable LLM dispatch entirely for repos where any source disclosure would be unacceptable — set `llm.allow_live_provider: false` in `clarion.yaml` (or leave `CLARION_LLM_LIVE` unset) so the recording provider is the only path Clarion will take. Contextual credential suppression only recognises shell/Python `#` comments in v0.1. It does not recognise `//` or `/* */` comments; use a justified baseline entry for reviewed non-Python test fixtures. From 60fb415d77aeeebca03cd59d4fe029603c077b1f Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Tue, 19 May 2026 14:11:46 +1000 Subject: [PATCH 014/187] fix(wp5): I13-I16 loud-fail sweep (clarion-55fc5aa885) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I13 — secret-scan finding-anchor `file_content_hash` now logs a tracing::warn! naming the path + error when fs::read fails. The anchor still lands without a content_hash but the audit trail records why, including the hint that entity briefing-block lookups may fail for that path. I14 — `parse_codex_jsonl_usage` no longer silently swallows malformed JSONL lines. Each malformed line logs a warn with the parse error and an 80-char snippet; a tail-warn reports the total skipped count and flags that `session_token_ceiling` enforcement will diverge from true accounting until those lines are recoverable. I15 — `plugin::host::apply_briefing_block` now removes any plugin-supplied `briefing_blocked` entry from `raw.extra` before optionally inserting the scanner-authored one. The scanner is the sole authority for this property; a malicious or buggy plugin cannot unblock an entity by emitting `"briefing_blocked": ` and cannot over-block by injecting a reason the scanner did not assign. I16 — `CodexCliProvider::from_config` and `ClaudeCliProvider::from_config` now validate the configured `executable` via `which::which` at construction time. A typo in `clarion.yaml` aborts at `clarion serve` startup with `LlmProviderError::Cli` (non-retryable) rather than exploding on the first MCP request. New workspace dependency: `which = "6"`. Co-Authored-By: Claude Opus 4.7 (1M context) --- Cargo.lock | 57 ++++++++++++++++++- Cargo.toml | 1 + crates/clarion-cli/src/secret_scan/anchors.rs | 16 +++++- crates/clarion-core/Cargo.toml | 1 + crates/clarion-core/src/llm_provider.rs | 40 +++++++++++-- crates/clarion-core/src/plugin/host.rs | 5 ++ 6 files changed, 111 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 503f52e2..d8c35536 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -353,6 +353,7 @@ dependencies = [ "thiserror 1.0.69", "toml", "tracing", + "which", ] [[package]] @@ -574,6 +575,12 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "equivalent" version = "1.0.2" @@ -795,6 +802,15 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "http" version = "1.4.0" @@ -1095,6 +1111,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -1517,6 +1539,19 @@ version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.52.0", +] + [[package]] name = "rustix" version = "1.1.4" @@ -1526,7 +1561,7 @@ dependencies = [ "bitflags", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.12.1", "windows-sys 0.61.2", ] @@ -1841,7 +1876,7 @@ dependencies = [ "fastrand", "getrandom 0.4.2", "once_cell", - "rustix", + "rustix 1.1.4", "windows-sys 0.61.2", ] @@ -2391,6 +2426,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "which" +version = "6.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ee928febd44d98f2f459a4a79bd4d928591333a494a10a868418ac1b39cf1f" +dependencies = [ + "either", + "home", + "rustix 0.38.44", + "winsafe", +] + [[package]] name = "winapi-util" version = "0.1.11" @@ -2571,6 +2618,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "winsafe" +version = "0.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" + [[package]] name = "wit-bindgen" version = "0.46.0" diff --git a/Cargo.toml b/Cargo.toml index 065d6b41..bd3d9d8f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,6 +53,7 @@ tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } uuid = { version = "1", features = ["v4"] } toml = "0.8" +which = "6" xgraph = "2.0.0" assert_cmd = "2" tempfile = "3" diff --git a/crates/clarion-cli/src/secret_scan/anchors.rs b/crates/clarion-cli/src/secret_scan/anchors.rs index 775e909d..14319af4 100644 --- a/crates/clarion-cli/src/secret_scan/anchors.rs +++ b/crates/clarion-cli/src/secret_scan/anchors.rs @@ -110,7 +110,17 @@ fn secret_finding_anchor_id(project_root: &Path, file: &Path) -> String { } fn file_content_hash(path: &Path) -> Option { - fs::read(path) - .ok() - .map(|bytes| blake3::hash(&bytes).to_hex().to_string()) + match fs::read(path) { + Ok(bytes) => Some(blake3::hash(&bytes).to_hex().to_string()), + Err(err) => { + tracing::warn!( + path = %path.display(), + error = %err, + "secret-scan finding-anchor: content-hash read failed; entity briefing-block \ + lookups may fail for this path because the finding anchor will land without a \ + content_hash" + ); + None + } + } } diff --git a/crates/clarion-core/Cargo.toml b/crates/clarion-core/Cargo.toml index 78c7a258..1d128a94 100644 --- a/crates/clarion-core/Cargo.toml +++ b/crates/clarion-core/Cargo.toml @@ -18,6 +18,7 @@ thiserror.workspace = true toml.workspace = true tracing.workspace = true nix = { workspace = true } +which = { workspace = true } [dev-dependencies] tempfile = { workspace = true } diff --git a/crates/clarion-core/src/llm_provider.rs b/crates/clarion-core/src/llm_provider.rs index d9f4fce7..880f8c97 100644 --- a/crates/clarion-core/src/llm_provider.rs +++ b/crates/clarion-core/src/llm_provider.rs @@ -341,6 +341,18 @@ impl LlmProvider for OpenRouterProvider { } } +/// Resolve `executable` via `which::which` and return a typed CLI error if +/// it is missing on PATH or at the configured absolute path. Called from each +/// CLI provider's `from_config` so a typo in `clarion.yaml` aborts at +/// `clarion serve` startup rather than exploding on the first MCP request. +fn validate_cli_executable(label: &str, executable: &str) -> Result<(), LlmProviderError> { + which::which(executable).map_err(|err| LlmProviderError::Cli { + message: format!("{label} executable {executable:?} not resolvable: {err}"), + retryable: false, + })?; + Ok(()) +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct CodexCliProviderConfig { pub executable: String, @@ -390,6 +402,7 @@ impl CodexCliProvider { retryable: false, }); } + validate_cli_executable("Codex CLI", &config.executable)?; Ok(Self { executable: config.executable, @@ -611,6 +624,7 @@ impl ClaudeCliProvider { retryable: false, }); } + validate_cli_executable("Claude CLI", &config.executable)?; Ok(Self { executable: config.executable, @@ -1002,15 +1016,33 @@ fn add_optional_f64(left: Option, right: Option) -> Option { fn parse_codex_jsonl_usage(stdout: &[u8]) -> LlmUsageSummary { let mut summary = LlmUsageSummary::default(); let stdout_text = String::from_utf8_lossy(stdout); + let mut malformed: u64 = 0; for line in stdout_text .lines() .map(str::trim) .filter(|line| !line.is_empty()) { - let Ok(event) = serde_json::from_str::(line) else { - continue; - }; - summary.add(usage_from_event(&event)); + match serde_json::from_str::(line) { + Ok(event) => summary.add(usage_from_event(&event)), + Err(err) => { + malformed += 1; + tracing::warn!( + error = %err, + snippet = %line.chars().take(80).collect::(), + "Codex JSONL usage parser: skipping malformed line; token totals will be \ + under-reported and `session_token_ceiling` enforcement may diverge from \ + true accounting" + ); + } + } + } + if malformed > 0 { + tracing::warn!( + malformed = malformed, + "Codex JSONL usage parser skipped {malformed} malformed line{suffix}; token totals \ + below are a lower bound only", + suffix = if malformed == 1 { "" } else { "s" }, + ); } summary } diff --git a/crates/clarion-core/src/plugin/host.rs b/crates/clarion-core/src/plugin/host.rs index 384f0b54..a8392c73 100644 --- a/crates/clarion-core/src/plugin/host.rs +++ b/crates/clarion-core/src/plugin/host.rs @@ -989,6 +989,11 @@ impl PluginHost { (!self.scanned_source_files.contains(jailed_path)) .then_some(BriefingBlockReason::UnscannedSource) }); + // Scanner is the sole authority over `briefing_blocked`. Strip any + // plugin-supplied value first so a malicious or buggy plugin cannot + // unblock an entity by emitting `"briefing_blocked": `, and + // cannot over-block by injecting a reason the scanner did not assign. + raw.extra.remove("briefing_blocked"); if let Some(reason) = reason { raw.extra.insert( "briefing_blocked".to_owned(), From 497526cf9babbc23bd29042d2d73c3e9ae0ddf61 Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Tue, 19 May 2026 14:13:10 +1000 Subject: [PATCH 015/187] fix(wp5): I5 ADR-013 audit-trail fidelity (clarion-55fc5aa885) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `pre_ingest` now emits the per-detection `CLA-SEC-SECRET-DETECTED` finding unconditionally — both when the operator overrides and when the file is blocked. The override finding `CLA-SEC-UNREDACTED-SECRETS-ALLOWED` remains additive, recording the operator decision and the detection list as evidence, but no longer replaces the per-(rule,file,line) audit row. ADR-013 §"Override — --allow-unredacted-secrets" specifies this shape: "each affected file emits `CLA-SEC-UNREDACTED-SECRETS-ALLOWED` (severity ERROR)" *in addition to* the per-detection `CLA-SEC-SECRET-DETECTED`. The prior code suppressed `CLA-SEC-SECRET-DETECTED` in the override branch and hid the audited population from `filigree list --rule-id=CLA-SEC-SECRET-DETECTED` — precisely the query a security review would run. Tests updated: - `non_tty_override_confirmed_allows_briefing_and_records_stats` now asserts `secret_count == 1` alongside `override_count == 1`. - `baseline_suppression_and_override_admission_are_audited_together` now asserts `secret_count == 1` (the live overridden detection; the baseline-suppressed fixture contributes 0). Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/clarion-cli/src/secret_scan.rs | 20 ++++++++++++++------ crates/clarion-cli/tests/secret_scan.rs | 24 +++++++++++++++++++++++- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/crates/clarion-cli/src/secret_scan.rs b/crates/clarion-cli/src/secret_scan.rs index dc9929c3..b1576e3f 100644 --- a/crates/clarion-cli/src/secret_scan.rs +++ b/crates/clarion-cli/src/secret_scan.rs @@ -194,16 +194,24 @@ pub(crate) fn pre_ingest( if allowed.is_empty() { continue; } + // ADR-013 §"Override — --allow-unredacted-secrets": each detection + // emits its own `CLA-SEC-SECRET-DETECTED` finding regardless of + // whether the operator subsequently overrode the block. The override + // finding (`CLA-SEC-UNREDACTED-SECRETS-ALLOWED`) is additive — it + // records the operator decision but does not replace the + // per-(rule,file,line) audit row, so a security review running + // `filigree list --rule-id=CLA-SEC-SECRET-DETECTED` enumerates the + // full detection population. + findings.extend( + allowed + .iter() + .map(|detection| secret_detected_finding(&file, detection)), + ); if override_confirmed { override_files.insert(file.clone()); override_detections.insert(file, allowed); } else { - briefing_blocks.insert(file.clone(), BriefingBlockReason::SecretPresent); - findings.extend( - allowed - .iter() - .map(|detection| secret_detected_finding(&file, detection)), - ); + briefing_blocks.insert(file, BriefingBlockReason::SecretPresent); } } diff --git a/crates/clarion-cli/tests/secret_scan.rs b/crates/clarion-cli/tests/secret_scan.rs index 56411610..78b6151b 100644 --- a/crates/clarion-cli/tests/secret_scan.rs +++ b/crates/clarion-cli/tests/secret_scan.rs @@ -474,9 +474,24 @@ fn non_tty_override_confirmed_allows_briefing_and_records_stats() { .unwrap(); let evidence: serde_json::Value = serde_json::from_str(&evidence_json).expect("override evidence JSON"); + // ADR-013: override is additive — per-(rule,file,line) SECRET_DETECTED + // still lands so `filigree list --rule-id=CLA-SEC-SECRET-DETECTED` + // enumerates the full audited population, not just the unoverridden tail. + let secret_detected_count: i64 = db + .query_row( + "SELECT COUNT(*) FROM findings WHERE rule_id = 'CLA-SEC-SECRET-DETECTED'", + [], + |row| row.get(0), + ) + .unwrap(); assert_eq!(blocked_count, 0); assert_eq!(override_count, 1); assert_eq!(override_used, 1); + assert_eq!( + secret_detected_count, 1, + "ADR-013 audit-trail fidelity: SECRET_DETECTED must be emitted even \ + under override so audit queries by rule_id are complete" + ); assert_eq!(evidence["detections"][0]["rule_id"], "AwsAccessKeyId"); assert_eq!(evidence["detections"][0]["line_number"], 1); assert_eq!( @@ -618,7 +633,14 @@ results: .unwrap(); assert_eq!(baseline_count, 1); assert_eq!(override_count, 1); - assert_eq!(secret_count, 0); + // ADR-013: SECRET_DETECTED emits on detection regardless of override. + // Baseline-suppressed `fixture.sec` contributes 0 detections; the live + // overridden `live.sec` contributes 1. + assert_eq!( + secret_count, 1, + "override is additive: live.sec's detection still lands as \ + SECRET_DETECTED so audit-by-rule_id is complete" + ); assert_eq!(blocked_count, 0); } From 6bb31522d31bbc427887352c0b8255ef580e0b17 Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Tue, 19 May 2026 14:18:49 +1000 Subject: [PATCH 016/187] test(wp5): I6/I7/I10/I11/I12 coverage gaps (clarion-55fc5aa885) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I6 — `baseline_does_not_suppress_when_hash_drifts_at_same_line` pins the "baseline benign at line 1, file mutated to a *different* secret at line 1" case. Suppression keys on (hashed_secret, line_number, rule_type); the regression net was missing. I7 — `high_entropy_hex_fires_on_lockfile_shas_but_baseline_suppresses_them` documents the v0.1 path for HighEntropyHex false positives. Lockfile integrity hashes and git SHA-1s fire the rule (40-char hex / entropy ≥ 3.0); tightening the rule would let real low-entropy secrets through, so the accepted resolution is the operator-baseline workflow already shipped under ADR-013. The fixture asserts both fixtures suppress cleanly via baseline. I10 — Five new unit tests for CLI provider failure paths: - claude_cli_parser_rejects_empty_stdout_as_retryable_invalid_response - claude_cli_parser_rejects_non_json_stdout - claude_cli_parser_refuses_raw_stdout_when_no_structured_output_or_result_event (the §C3 contract: stdout without a `result` event or `structured_output`/`structuredOutput`/`result` field returns InvalidResponse, not a persisted raw payload) - claude_cli_parser_accepts_result_event_with_string_result_payload - cli_status_retryable_treats_signal_kill_as_retryable - cli_status_retryable_treats_clean_nonzero_exit_as_non_retryable I11 — `tests/e2e/wp5_secret_scan.sh` now exercises four new scenarios beyond the happy path: baseline-suppression flow, confirmed `--allow-unredacted-secrets` admission (asserting both SECRET_DETECTED and UNREDACTED-SECRETS-ALLOWED land per §I5), malformed-baseline abort (exit non-zero, no runs row), retry-after-baseline-add (operator workflow: block → commit baseline → re-run unblocks). I12 — `McpConfig::from_yaml_str` rejects configs that name both `llm` and `llm_policy` at the top level with a new `ConfigError::AmbiguousLlmKey` (code `CLA-CONFIG-AMBIGUOUS-LLM-KEY`). Serde-norway aliases silently pick one, which was the classic copy-paste-migration pitfall the review flagged. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/clarion-core/src/llm_provider.rs | 104 ++++++++++++++++++ crates/clarion-mcp/src/config.rs | 58 ++++++++++ crates/clarion-scanner/tests/scanner.rs | 137 ++++++++++++++++++++++++ tests/e2e/wp5_secret_scan.sh | 106 ++++++++++++++++++ 4 files changed, 405 insertions(+) diff --git a/crates/clarion-core/src/llm_provider.rs b/crates/clarion-core/src/llm_provider.rs index 880f8c97..3d056d10 100644 --- a/crates/clarion-core/src/llm_provider.rs +++ b/crates/clarion-core/src/llm_provider.rs @@ -2179,6 +2179,110 @@ printf '%s\n' '{{"type":"result","subtype":"success","structured_output":{{"purp assert_eq!(parsed.cost_usd, Some(0.75)); } + #[test] + fn claude_cli_parser_rejects_empty_stdout_as_retryable_invalid_response() { + let err = parse_claude_cli_json_output(b"") + .expect_err("empty stdout must surface a typed InvalidResponse"); + match err { + LlmProviderError::InvalidResponse { message, retryable } => { + assert!(retryable, "empty stdout should be retryable (transient)"); + assert!( + message.contains("empty stdout"), + "error message must name the failure mode: {message}" + ); + } + other => panic!("expected InvalidResponse for empty stdout, got: {other:?}"), + } + } + + #[test] + fn claude_cli_parser_rejects_non_json_stdout() { + let err = parse_claude_cli_json_output(b"not json") + .expect_err("non-JSON stdout must surface a typed InvalidResponse"); + match err { + LlmProviderError::InvalidResponse { message, retryable } => { + assert!(retryable); + assert!( + message.contains("not JSON"), + "error message must reference JSON parse failure: {message}" + ); + } + other => panic!("expected InvalidResponse for non-JSON stdout, got: {other:?}"), + } + } + + #[test] + fn claude_cli_parser_refuses_raw_stdout_when_no_structured_output_or_result_event() { + // Single event with neither `type=result` nor any of the structured + // output fields. Pre-fix, the parser fell back to the raw event + // payload and downstream consumers persisted it as a summary. + // Post-fix (clarion-55fc5aa885 §C3), this must be a typed + // InvalidResponse rather than silent garbage. + let stdout = br#"{"type":"assistant","message":{"usage":{"input_tokens":1,"output_tokens":1}}}"#; + let err = parse_claude_cli_json_output(stdout).expect_err( + "stdout without a `result` event or `structured_output` field must \ + surface InvalidResponse, not be persisted as a summary", + ); + match err { + LlmProviderError::InvalidResponse { message, retryable } => { + assert!(retryable); + assert!( + message.contains("no `result` event"), + "error must explain the missing-structured-output failure: {message}" + ); + } + other => { + panic!("expected InvalidResponse for missing structured output, got: {other:?}") + } + } + } + + #[test] + fn claude_cli_parser_accepts_result_event_with_string_result_payload() { + // The other arm `result_event.get("result")` selects a `result` + // event that has a JSON-string `result` field. Exercise it to pin + // the contract. + let stdout = br#"{"type":"result","result":"{\"purpose\":\"string\",\"behavior\":\"ok\",\"relationships\":\"\",\"risks\":\"\"}"}"#; + let parsed = parse_claude_cli_json_output(stdout).expect("parse result event"); + assert_eq!( + serde_json::from_str::(&parsed.output_json).expect("output json"), + serde_json::json!({ + "purpose": "string", + "behavior": "ok", + "relationships": "", + "risks": "" + }) + ); + } + + #[test] + fn cli_status_retryable_treats_signal_kill_as_retryable() { + use std::os::unix::process::ExitStatusExt; + // ExitStatus::from_raw with a non-exit signal code yields code()=None. + let killed = ExitStatus::from_raw(9); + assert!( + cli_status_retryable(killed), + "child killed by signal must be treated as retryable so the orchestrator can retry" + ); + assert!( + codex_status_retryable(killed), + "codex retryable check must agree with the CLI floor" + ); + } + + #[test] + fn cli_status_retryable_treats_clean_nonzero_exit_as_non_retryable() { + use std::os::unix::process::ExitStatusExt; + // raw status 0x100 → exit code 1, code()=Some(1) + let exit_one = ExitStatus::from_raw(0x100); + assert!( + !cli_status_retryable(exit_one), + "clean process exit must be treated as non-retryable: the CLI \ + rejected the request deterministically" + ); + assert!(!codex_status_retryable(exit_one)); + } + #[test] fn provider_trait_exposes_wp6_methods() { fn assert_trait(_: &T) {} diff --git a/crates/clarion-mcp/src/config.rs b/crates/clarion-mcp/src/config.rs index c039b4f8..3deb1e33 100644 --- a/crates/clarion-mcp/src/config.rs +++ b/crates/clarion-mcp/src/config.rs @@ -26,6 +26,7 @@ impl McpConfig { if raw.trim().is_empty() { return Ok(Self::default()); } + reject_llm_policy_alias_collision(raw)?; let config: Self = serde_norway::from_str(raw).map_err(|err| ConfigError::Yaml(err.to_string()))?; config.validate()?; @@ -368,6 +369,37 @@ pub enum ConfigError { #[error("{code}: integrations.filigree.actor must not be blank when Filigree is enabled")] InvalidFiligreeActor { code: &'static str }, + + #[error( + "{code}: clarion.yaml contains both `llm` and `llm_policy` top-level keys; \ + `llm_policy` is a serde alias for `llm` and serde silently discards one. \ + Pick one and remove the other." + )] + AmbiguousLlmKey { code: &'static str }, +} + +/// Reject configs that name both `llm` and `llm_policy` at the top level. +/// They alias the same field; serde-norway silently picks one and discards +/// the other, which is the classic copy-paste-migration pitfall. Detecting +/// the collision pre-parse turns a silent override into a typed error. +fn reject_llm_policy_alias_collision(raw: &str) -> Result<(), ConfigError> { + let value: serde_norway::Value = match serde_norway::from_str(raw) { + Ok(value) => value, + // If the YAML doesn't even parse as a generic Value, let the typed + // parse below produce the canonical Yaml error. + Err(_) => return Ok(()), + }; + let Some(mapping) = value.as_mapping() else { + return Ok(()); + }; + let has_llm = mapping.contains_key("llm"); + let has_llm_policy = mapping.contains_key("llm_policy"); + if has_llm && has_llm_policy { + return Err(ConfigError::AmbiguousLlmKey { + code: "CLA-CONFIG-AMBIGUOUS-LLM-KEY", + }); + } + Ok(()) } #[cfg(test)] @@ -442,6 +474,32 @@ llm_policy: assert_eq!(cfg.llm.model_id, "openai/gpt-4o-mini"); } + #[test] + fn rejects_both_llm_and_llm_policy_keys_present_together() { + // Realistic migration-doc copy-paste case: operator copies the new + // `llm_policy:` block but forgets to delete the old `llm:` block. + // Serde-norway would silently pick one and discard the other. + let err = McpConfig::from_yaml_str( + r" +llm: + enabled: false + provider: recording +llm_policy: + enabled: true + provider: openrouter + model_id: openai/gpt-4o-mini +", + ) + .expect_err("ambiguous llm key must be rejected"); + + match err { + ConfigError::AmbiguousLlmKey { code } => { + assert_eq!(code, "CLA-CONFIG-AMBIGUOUS-LLM-KEY"); + } + other => panic!("expected AmbiguousLlmKey error, got: {other:?}"), + } + } + #[test] fn api_key_alone_does_not_select_live_provider() { let cfg = McpConfig { diff --git a/crates/clarion-scanner/tests/scanner.rs b/crates/clarion-scanner/tests/scanner.rs index 0537d285..1b038114 100644 --- a/crates/clarion-scanner/tests/scanner.rs +++ b/crates/clarion-scanner/tests/scanner.rs @@ -482,6 +482,143 @@ results: assert_eq!(parsed, reparsed); } +/// Regression net for the gap noted in PR #11 review (clarion-55fc5aa885 §I6): +/// baseline suppression keys on (hashed_secret, line_number, rule_type). A +/// baseline entry at line 1 with one hash MUST NOT suppress a *different* +/// detection at line 1 — that would be a silent regression where a benign +/// stub gets replaced by a real secret at the same offset and the gate +/// stops firing. +#[test] +fn baseline_does_not_suppress_when_hash_drifts_at_same_line() { + let scanner = Scanner::new(); + // Original benign content the operator baselined. + let benign_detections = scanner.scan_bytes(b"key = 'AKIAIOSFODNN7EXAMPLE'\n"); + let benign = benign_detections + .iter() + .find(|detection| detection.rule_id == "AwsAccessKeyId") + .expect("AWS detection in benign content") + .clone(); + // Operator commits a baseline acknowledging the benign secret on line 1. + let baseline = Baseline::from_yaml_str(&format!( + r#" +version: "1.0" +results: + "src/demo.py": + - type: "AWS Access Key" + hashed_secret: "{}" + line_number: 1 + is_secret: false + justification: "Documented public AWS example key." +"#, + hex20(benign.hashed_secret) + )) + .expect("baseline parses"); + + // Later, the file is mutated: same line, same rule, *different* secret. + let drifted_detections = scanner.scan_bytes(b"key = 'AKIAJONOTTHESAMEAS18'\n"); + let drifted = drifted_detections + .iter() + .find(|detection| detection.rule_id == "AwsAccessKeyId") + .expect("AWS detection in drifted content") + .clone(); + assert_ne!( + benign.hashed_secret, drifted.hashed_secret, + "test premise: drifted content must hash differently", + ); + + let result = baseline.suppress(vec![drifted], std::path::Path::new("src/demo.py")); + assert_eq!( + result.allowed.len(), + 1, + "baseline must NOT suppress a drifted hash at the same line — that is \ + the security regression CLA-SEC-SECRET-DETECTED exists to catch", + ); + assert!(result.suppressed.is_empty()); + assert!(result.fired_entries.is_empty()); +} + +/// Regression net for the gap noted in PR #11 review (clarion-55fc5aa885 §I7): +/// `HighEntropyHex` at 40 chars / entropy ≥ 3.0 will fire on git SHA-1 hashes, +/// blake3 hex digests, and lockfile integrity fields. The accepted v0.1 path +/// is *not* to tighten the rule (which would let real low-entropy secrets +/// through) but to document the operator-baseline workflow as the +/// resolution — exactly what this fixture asserts. +/// +/// Tightening the entropy floor risks missing real secrets that happen to +/// look hash-like; the baseline path is per-(rule,file,line,hash) and is the +/// existing escape hatch ADR-013 §"Operator baseline" describes. +#[test] +fn high_entropy_hex_fires_on_lockfile_shas_but_baseline_suppresses_them() { + // Lockfile integrity hash (npm-style sha512-truncated to hex) and a git + // SHA-1. Both are 40-char hex, entropy ≈ log2(16) ≈ 4 — well above the + // HighEntropyHex floor. + let lockfile_payload = b"\"integrity\": \"a3f5e8c2b1d4f0967e8c2a1b5d3e0f4a6c8b2d1e\""; + let git_sha = b"commit a3f5e8c2b1d4f0967e8c2a1b5d3e0f4a6c8b2d1e\n"; + + let scanner = Scanner::new(); + let lockfile_detections = scanner.scan_bytes(lockfile_payload); + let git_detections = scanner.scan_bytes(git_sha); + let lockfile_hex = lockfile_detections + .iter() + .find(|detection| detection.rule_id == "HighEntropyHex") + .cloned(); + let git_hex = git_detections + .iter() + .find(|detection| detection.rule_id == "HighEntropyHex") + .cloned(); + + let lockfile_hex = lockfile_hex.expect( + "HighEntropyHex fires on lockfile integrity hash — the regression \ + net is the operator-baseline workflow, not a tightened rule", + ); + let git_hex = git_hex.expect( + "HighEntropyHex fires on git SHA-1 — the regression net is the \ + operator-baseline workflow, not a tightened rule", + ); + + // Operator commits a baseline pinning both the lockfile and git SHA on + // their respective files. The same workflow scales to blake3 hex + // digests and any other hash-like content. + let baseline_yaml = format!( + r#" +version: "1.0" +results: + "package-lock.json": + - type: "Hex High Entropy String" + hashed_secret: "{lock_hash}" + line_number: 1 + is_secret: false + justification: "npm lockfile integrity hash, not a credential." + "git-log.txt": + - type: "Hex High Entropy String" + hashed_secret: "{git_hash}" + line_number: 1 + is_secret: false + justification: "Git commit SHA, not a credential." +"#, + lock_hash = hex20(lockfile_hex.hashed_secret), + git_hash = hex20(git_hex.hashed_secret), + ); + let baseline = Baseline::from_yaml_str(&baseline_yaml).expect("baseline parses"); + + let lockfile_result = baseline.suppress( + vec![lockfile_hex], + std::path::Path::new("package-lock.json"), + ); + let git_result = baseline.suppress(vec![git_hex], std::path::Path::new("git-log.txt")); + + assert!( + lockfile_result.allowed.is_empty(), + "operator-baseline must suppress the lockfile integrity-hash false positive", + ); + assert!( + git_result.allowed.is_empty(), + "operator-baseline must suppress the git SHA false positive", + ); + assert_eq!(lockfile_result.fired_entries.len(), 1); + assert_eq!(git_result.fired_entries.len(), 1); +} + fn hex20(hash: HashedSecret) -> String { hash.to_string() } diff --git a/tests/e2e/wp5_secret_scan.sh b/tests/e2e/wp5_secret_scan.sh index e50ab06b..3fba0ee6 100755 --- a/tests/e2e/wp5_secret_scan.sh +++ b/tests/e2e/wp5_secret_scan.sh @@ -112,3 +112,109 @@ RUN_STATUS=$(sqlite3 "$DB" "select status from runs;") [ "$RUN_STATUS" = "completed" ] || fail "expected completed run, got $RUN_STATUS" log "PASS: WP5 secret scan blocks summaries and records finding" + +# ---------------------------------------------------------------------------- +# baseline-suppression flow (clarion-55fc5aa885 §I11) +# ---------------------------------------------------------------------------- +log "scenario: baseline-suppressed detection emits BASELINE-MATCH only, no block" +BASELINE_DIR="$(mktemp -d -t clarion-wp5-baseline-XXXXXX)" +trap 'rm -rf "$DEMO_DIR" "$PLUGIN_DIR" "$BASELINE_DIR" "$OVERRIDE_DIR" "$MALFORMED_DIR"' EXIT +"$CLARION_BIN" install --path "$BASELINE_DIR" +printf "aws_access_key_id = 'AKIAIOSFODNN7EXAMPLE'\n" > "$BASELINE_DIR/leaky.sec" +HASHED_SECRET=$(printf 'AKIAIOSFODNN7EXAMPLE' | sha1sum | awk '{print $1}') +cat > "$BASELINE_DIR/.clarion/secrets-baseline.yaml" < "$OVERRIDE_DIR/leaky.sec" +PATH="$PLUGIN_DIR" "$CLARION_BIN" analyze \ + --allow-unredacted-secrets \ + --confirm-allow-unredacted-secrets=yes-i-understand \ + "$OVERRIDE_DIR" +ODB="$OVERRIDE_DIR/.clarion/clarion.db" +O_BLOCKED=$(sqlite3 "$ODB" "select count(*) from entities where json_extract(properties, '\$.briefing_blocked') = 'secret_present';") +[ "$O_BLOCKED" = "0" ] || fail "override: expected 0 blocked entities, got $O_BLOCKED" +O_SECRET=$(sqlite3 "$ODB" "select count(*) from findings where rule_id = 'CLA-SEC-SECRET-DETECTED';") +[ "$O_SECRET" = "1" ] || fail "override is additive per ADR-013: expected 1 SECRET_DETECTED finding, got $O_SECRET" +O_OVERRIDE=$(sqlite3 "$ODB" "select count(*) from findings where rule_id = 'CLA-SEC-UNREDACTED-SECRETS-ALLOWED';") +[ "$O_OVERRIDE" = "1" ] || fail "override: expected 1 UNREDACTED-SECRETS-ALLOWED finding, got $O_OVERRIDE" +O_OVERRIDE_USED=$(sqlite3 "$ODB" "select json_extract(stats, '\$.secret_override_used') from runs;") +[ "$O_OVERRIDE_USED" = "1" ] || fail "override: expected stats.secret_override_used = 1, got $O_OVERRIDE_USED" +log "PASS: override admission lands both SECRET_DETECTED and UNREDACTED-SECRETS-ALLOWED" + +# ---------------------------------------------------------------------------- +# malformed-baseline abort (clarion-55fc5aa885 §I11) +# ---------------------------------------------------------------------------- +log "scenario: malformed baseline aborts analyze before BeginRun (exit 78)" +MALFORMED_DIR="$(mktemp -d -t clarion-wp5-malformed-XXXXXX)" +"$CLARION_BIN" install --path "$MALFORMED_DIR" +printf "harmless = 'nothing'\n" > "$MALFORMED_DIR/clean.sec" +printf "not: valid: yaml: [\n" > "$MALFORMED_DIR/.clarion/secrets-baseline.yaml" +set +e +PATH="$PLUGIN_DIR" "$CLARION_BIN" analyze "$MALFORMED_DIR" 2>/dev/null +MALFORMED_EXIT=$? +set -e +[ "$MALFORMED_EXIT" -ne 0 ] || fail "malformed baseline: expected non-zero exit, got $MALFORMED_EXIT" +MDB="$MALFORMED_DIR/.clarion/clarion.db" +M_RUNS=$(sqlite3 "$MDB" "select count(*) from runs;") +[ "$M_RUNS" = "0" ] || fail "malformed baseline must abort BEFORE BeginRun; got $M_RUNS run rows" +log "PASS: malformed baseline aborts with non-zero exit and no runs row" + +# ---------------------------------------------------------------------------- +# retry-after-baseline-add: rerun the baseline-suppression scenario but where +# the baseline is added AFTER an initial blocked run, simulating the +# operator workflow (clarion-55fc5aa885 §I11). This re-runs analyze on +# DEMO_DIR (which started blocked) after we wipe and re-add a baseline. +# ---------------------------------------------------------------------------- +log "scenario: retry-after-baseline-add unblocks a previously-blocked file" +RETRY_DIR="$(mktemp -d -t clarion-wp5-retry-XXXXXX)" +trap 'rm -rf "$DEMO_DIR" "$PLUGIN_DIR" "$BASELINE_DIR" "$OVERRIDE_DIR" "$MALFORMED_DIR" "$RETRY_DIR"' EXIT +"$CLARION_BIN" install --path "$RETRY_DIR" +printf "aws_access_key_id = 'AKIAIOSFODNN7EXAMPLE'\n" > "$RETRY_DIR/leaky.sec" +# First run blocks (no baseline yet). +PATH="$PLUGIN_DIR" "$CLARION_BIN" analyze "$RETRY_DIR" +RDB="$RETRY_DIR/.clarion/clarion.db" +R_BLOCKED_BEFORE=$(sqlite3 "$RDB" "select count(*) from entities where json_extract(properties, '\$.briefing_blocked') = 'secret_present';") +[ "$R_BLOCKED_BEFORE" = "1" ] || fail "retry: first run must block, got $R_BLOCKED_BEFORE" +# Operator commits a baseline acknowledging the example key. +cat > "$RETRY_DIR/.clarion/secrets-baseline.yaml" < Date: Tue, 19 May 2026 14:18:56 +1000 Subject: [PATCH 017/187] docs(wp5): I17 task-reference sweep (clarion-55fc5aa885) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Project rule: name the function, not the work-package label. WP-task tags leak from the implementation tracker into operator-facing surfaces and into comments that will rot once WP5 is closed. Replaced: - docs/operator/secret-scanning.md: "WP5 audit surface" → "scanner audit surface" - crates/clarion-cli/src/main.rs: ".env-supplied RUST_LOG ... before WP5's pre-ingest secret gate runs" → "... before the pre-ingest secret scanner runs" Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/clarion-cli/src/main.rs | 4 ++-- docs/operator/secret-scanning.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/clarion-cli/src/main.rs b/crates/clarion-cli/src/main.rs index fb579c21..3424cb5d 100644 --- a/crates/clarion-cli/src/main.rs +++ b/crates/clarion-cli/src/main.rs @@ -17,8 +17,8 @@ fn main() -> Result<()> { // Load .env before tracing setup for operator-facing commands so a // .env-supplied RUST_LOG is in effect by the time the filter is built. // `analyze` is deliberately excluded: project .env contents are scanned - // as source sidecars and must not be imported into plugin subprocess - // environments before WP5's pre-ingest secret gate runs. + // as source sidecars by the pre-ingest secret scanner and must not be + // imported into plugin subprocess environments before that gate runs. if !matches!(&cli.command, cli::Command::Analyze { .. }) { let _ = dotenvy::dotenv(); } diff --git a/docs/operator/secret-scanning.md b/docs/operator/secret-scanning.md index 631c05ce..da7a2077 100644 --- a/docs/operator/secret-scanning.md +++ b/docs/operator/secret-scanning.md @@ -74,7 +74,7 @@ from entities where json_extract(properties, '$.briefing_blocked') is not null; ``` -Filigree integration for scanner findings is planned for v0.2. Until then, the local `findings` table is the authoritative WP5 audit surface. +Filigree integration for scanner findings is planned for v0.2. Until then, the local `findings` table is the authoritative scanner audit surface. ## Limitations From bb232963a9e4a5a843139a49c79b15f5f4f6c53d Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Tue, 19 May 2026 14:21:20 +1000 Subject: [PATCH 018/187] style(wp5): apply rustfmt + clippy doc-markdown fixes (clarion-55fc5aa885) rustfmt: wrap the 80-char `br#"..."#` literal in the InvalidResponse parser test onto a continuation line. clippy::doc-markdown: backtick the (hashed_secret, line_number, rule_type) tuple identifiers in the baseline-drift test docstring. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/clarion-core/src/llm_provider.rs | 3 ++- crates/clarion-scanner/tests/scanner.rs | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/clarion-core/src/llm_provider.rs b/crates/clarion-core/src/llm_provider.rs index 3d056d10..dc8292c8 100644 --- a/crates/clarion-core/src/llm_provider.rs +++ b/crates/clarion-core/src/llm_provider.rs @@ -2218,7 +2218,8 @@ printf '%s\n' '{{"type":"result","subtype":"success","structured_output":{{"purp // payload and downstream consumers persisted it as a summary. // Post-fix (clarion-55fc5aa885 §C3), this must be a typed // InvalidResponse rather than silent garbage. - let stdout = br#"{"type":"assistant","message":{"usage":{"input_tokens":1,"output_tokens":1}}}"#; + let stdout = + br#"{"type":"assistant","message":{"usage":{"input_tokens":1,"output_tokens":1}}}"#; let err = parse_claude_cli_json_output(stdout).expect_err( "stdout without a `result` event or `structured_output` field must \ surface InvalidResponse, not be persisted as a summary", diff --git a/crates/clarion-scanner/tests/scanner.rs b/crates/clarion-scanner/tests/scanner.rs index 1b038114..72313e5b 100644 --- a/crates/clarion-scanner/tests/scanner.rs +++ b/crates/clarion-scanner/tests/scanner.rs @@ -483,8 +483,8 @@ results: } /// Regression net for the gap noted in PR #11 review (clarion-55fc5aa885 §I6): -/// baseline suppression keys on (hashed_secret, line_number, rule_type). A -/// baseline entry at line 1 with one hash MUST NOT suppress a *different* +/// baseline suppression keys on (`hashed_secret`, `line_number`, `rule_type`). +/// A baseline entry at line 1 with one hash MUST NOT suppress a *different* /// detection at line 1 — that would be a silent regression where a benign /// stub gets replaced by a real secret at the same offset and the gate /// stops firing. From ad37c500b6489b4fc3e5253722493df9dd4a43cd Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Tue, 19 May 2026 14:43:33 +1000 Subject: [PATCH 019/187] C0 docs: pin Loom federation read contract decisions --- docs/clarion/adr/ADR-001-rust-for-core.md | 2 +- docs/clarion/adr/ADR-003-entity-id-scheme.md | 8 ++ docs/clarion/adr/ADR-012-http-auth-default.md | 13 ++- .../adr/ADR-014-filigree-registry-backend.md | 84 ++++++++++++++++++- .../adr/ADR-021-plugin-authority-hybrid.md | 2 +- docs/clarion/adr/README.md | 2 +- docs/clarion/v0.1/detailed-design.md | 66 +++++++-------- docs/clarion/v0.1/requirements.md | 39 ++++++--- docs/clarion/v0.1/system-design.md | 64 ++++++++------ docs/federation/contracts.md | 47 +++++++++-- ...-backend-and-file-identity-displacement.md | 2 +- docs/operator/clarion-http-read-api.md | 32 +++++++ docs/suite/loom.md | 9 +- 13 files changed, 284 insertions(+), 86 deletions(-) create mode 100644 docs/operator/clarion-http-read-api.md diff --git a/docs/clarion/adr/ADR-001-rust-for-core.md b/docs/clarion/adr/ADR-001-rust-for-core.md index a6887675..a9d5a1c7 100644 --- a/docs/clarion/adr/ADR-001-rust-for-core.md +++ b/docs/clarion/adr/ADR-001-rust-for-core.md @@ -17,7 +17,7 @@ Clarion's core is responsible for: - SQLite-backed storage that serves both batch `clarion analyze` and long-lived `clarion serve` simultaneously (`CON-SQLITE-01`, ADR-011 writer-actor) - subprocess supervision for language plugins over Content-Length framed JSON-RPC (`REQ-PLUGIN-01`..`REQ-PLUGIN-06`, ADR-002) - bounded-cost LLM orchestration with per-run cache behaviour and cancellation semantics (`NFR-COST-01`..`NFR-COST-03`) -- local HTTP and MCP serving with auth posture appropriate to a security tool (`NFR-SEC-*`, ADR-012) +- local HTTP and MCP serving with a bounded trust posture (`NFR-SEC-*`, ADR-012 historically; ADR-014 for the registry-backend HTTP read API) The product posture is local-first and operationally lightweight (`CON-LOCAL-01`, `NFR-OPS-01`..`NFR-OPS-03`). The implementation language therefore affects packaging, concurrency, storage ergonomics, and deployment complexity — and the acceptable envelope for each is already set by requirements rather than preference. diff --git a/docs/clarion/adr/ADR-003-entity-id-scheme.md b/docs/clarion/adr/ADR-003-entity-id-scheme.md index a2d1babc..26692069 100644 --- a/docs/clarion/adr/ADR-003-entity-id-scheme.md +++ b/docs/clarion/adr/ADR-003-entity-id-scheme.md @@ -24,6 +24,14 @@ Path-embedded IDs make routine moves and re-exports detach cross-tool references We will identify source entities as `{plugin_id}:{kind}:{canonical_qualified_name}` and keep file path as a property rather than as part of the primary ID. +File-kind entities use the same grammar. The core file entity ID is +`core:file:{qualified_name}`. For file entities, `{qualified_name}` is the +canonical project-relative file qualified name; it must not contain `@`. +Content-hash-plus-path forms such as +`core:file:{content_hash}@{canonical_path}` are non-conforming because they +embed path and drift state into the primary ID instead of storing those values +as entity properties. + For v0.1: - definition site wins for canonical naming diff --git a/docs/clarion/adr/ADR-012-http-auth-default.md b/docs/clarion/adr/ADR-012-http-auth-default.md index f4676df7..bf719a00 100644 --- a/docs/clarion/adr/ADR-012-http-auth-default.md +++ b/docs/clarion/adr/ADR-012-http-auth-default.md @@ -1,12 +1,21 @@ # ADR-012: HTTP Read-API Authentication — Unix Domain Socket Default with Token Fallback -**Status**: Accepted +**Status**: Superseded for the ADR-014 registry-backend HTTP read API **Date**: 2026-04-18 **Deciders**: qacona@gmail.com **Context**: `clarion serve` exposes the read API to sibling tools; v0.1 panel threat-model scored the original `auth: none` default at risk 9 (T-02) ## Summary +**2026-05-19 supersession note**: ADR-014 now owns the security posture for the +Clarion HTTP read API used by Filigree's `registry_backend: clarion` mode. That +surface is unauthenticated and loopback-only by default, refuses non-loopback +binds unless `serve.http.allow_non_loopback: true`, and relies on an +operator-managed authenticated reverse proxy or equivalent access-control layer +for non-loopback exposure. The UDS/token design below remains historical context +for the earlier broad v0.1 HTTP API proposal; it is not the implementation +contract for the ADR-014 federation read surface. + `clarion serve` defaults to **Unix domain socket** at `.clarion/socket` (mode 0600, owner = current UID). Filesystem permissions are the auth; no network bind, no Bearer tokens, no CAP_NET_BIND_SERVICE surface. When a UDS is unavailable (Windows, SSH-port-forwarded remote access, cross-UID-namespace containers), `serve.auth: token` falls back to TCP on `127.0.0.1:8765` + a Bearer token auto-minted at `.clarion/auth.token` (mode 0600). `serve.auth: none` remains configurable for operators who explicitly accept the unauthenticated-loopback posture, but now emits `CLA-INFRA-HTTP-AUTH-DISABLED` (severity ERROR) per serve startup and shows a persistent banner in logs. The default flip closes T-02 and structurally reduces T-05 (DNS rebinding) by eliminating the TCP bind in the primary path. ## Context @@ -156,5 +165,5 @@ v0.1 tokens carry scope claims — read-only catalog, read-only findings, submit - [Clarion v0.1 panel threat model T-02](../../implementation/v0.1-reviews/panel-2026-04-17/09-threat-model.md) (line 233) — original risk scoring. - [Panel threat model §12 recommendation 1](../../implementation/v0.1-reviews/panel-2026-04-17/09-threat-model.md) (line 298) — the specific "authenticated or not listening" prescription. - [Clarion v0.1 system design §10 "Loopback is not a security boundary"](../v0.1/system-design.md) — the paragraph this ADR flips; updated in the same commit. -- [Clarion v0.1 detailed design §7 Token auth — full spec](../v0.1/detailed-design.md) (lines 1286-1304) — the token machinery this ADR reuses as the fallback. +- [ADR-014](./ADR-014-filigree-registry-backend.md) — supersedes this ADR for the active registry-backend HTTP read API trust model. - [Clarion v0.1 scope commitments — action 5](../../implementation/v0.1-scope-plans/v0.1-scope-commitments.md) (line 199) — the commitment mandate. diff --git a/docs/clarion/adr/ADR-014-filigree-registry-backend.md b/docs/clarion/adr/ADR-014-filigree-registry-backend.md index 071e63cd..f3d56d30 100644 --- a/docs/clarion/adr/ADR-014-filigree-registry-backend.md +++ b/docs/clarion/adr/ADR-014-filigree-registry-backend.md @@ -29,7 +29,7 @@ Filigree introduces a `RegistryProtocol` trait with two implementations. **Mode `local` (default)**: Filigree's current behaviour. The three auto-create paths populate `file_records` using UUID-derived IDs. Filigree remains fully usable standalone — no Clarion dependency, no degradation. -**Mode `clarion`**: Filigree delegates `file_id` resolution to Clarion's HTTP read API. The three auto-create paths call `RegistryProtocol::resolve_file(path, language) -> file_id` which, under `clarion` mode, issues an HTTP GET to Clarion's read API. The returned `file_id` is Clarion's symbolic entity ID (`core:file:{hash}@{path}`). The `file_records` row is created in Filigree with that ID, preserving the existing foreign-key structure. +**Mode `clarion`**: Filigree delegates `file_id` resolution to Clarion's HTTP read API. The three auto-create paths call `RegistryProtocol::resolve_file(path, language) -> file_id` which, under `clarion` mode, issues an HTTP GET to Clarion's read API. The returned `file_id` is Clarion's symbolic file-kind entity ID (`core:file:{qualified_name}`). The `file_records` row is created in Filigree with that ID, preserving the existing foreign-key structure. **Flag surfacing**: `registry_backend` appears in `GET /api/files/_schema.config_flags`. Clarion's capability probe reads it at every `clarion analyze` start. Present + value `clarion` → proceed with delegation. Absent or value `local` → Clarion enters shadow-registry mode and emits `CLA-INFRA-FILIGREE-SHADOW-REGISTRY` per batch. @@ -37,6 +37,87 @@ Filigree introduces a `RegistryProtocol` trait with two implementations. **Startup failure mode**: if Filigree starts with `registry_backend: clarion` but Clarion's read API is unreachable, Filigree refuses writes (returns `503 Service Unavailable` from the three auto-create paths) rather than silently degrading to `local`. An explicit `--allow-local-fallback` flag exists for single-operator recovery scenarios; the default is fail-closed. +### Capability Probe Semantics + +Clarion's read API exposes `GET /api/v1/_capabilities` for Filigree's +registry-backend probe. The response includes: + +```json +{ + "api_version": 1, + "instance_id": "9bd7234e-6d44-4a38-9ae4-76f912a10221", + "registry_backend": true, + "file_registry": true +} +``` + +`api_version` is the wire-contract version for the HTTP read API. It increments +only when the HTTP read API changes incompatibly for existing Filigree clients. +It is not Clarion product semver and must not be compared as a release version. + +`instance_id` identifies the Clarion project instance serving the API. Filigree +uses it to detect that the same endpoint has been rebound to a different +Clarion project instance. + +### File Identity Semantics + +Clarion resolves only existing file-kind rows. When no `kind = 'file'` entity +row exists for the requested path, the API fails closed with `404 NOT_FOUND`. +Clarion must not synthesize a `core:file:{content_hash}@{canonical_path}` +identity. That pattern violates ADR-003's entity-ID grammar and creates shadow +IDs that will not match future file-discovery rows. + +### Canonical Path Semantics + +`canonical_path` is the normalized project-relative POSIX path for the file: + +- no leading `/` +- no leading `./` +- no trailing `/` +- `/` as the separator on every platform + +The path is relative to the Clarion project root so file identity responses +survive project relocation. It is the path Filigree should store as human and +drift context, not an absolute filesystem path. + +### Instance Fingerprint + +Clarion persists a stable per-project UUID at `.clarion/instance_id`. The first +creation writes the file with mode `0600` on Unix. `GET /api/v1/_capabilities` +surfaces the UUID as `instance_id`. + +Deleting `.clarion/` may create a new instance ID. That is acceptable because it +represents a new Clarion project instance and should be detectable by Filigree. + +### Error Envelope + +Non-2xx read API responses use a closed JSON envelope: + +```json +{ + "error": "path does not resolve to a Clarion file entity", + "code": "NOT_FOUND" +} +``` + +The initial `ErrorCode` enum is closed to: + +- `INVALID_PATH` +- `PATH_OUTSIDE_PROJECT` +- `NOT_FOUND` +- `STORAGE_ERROR` +- `INTERNAL` + +Clients must switch on `code`, not on human-readable `error` text. + +### Security Posture + +The HTTP read API is unauthenticated and loopback-only by default. Clarion +refuses non-loopback HTTP binds unless `serve.http.allow_non_loopback: true` is +set. When that opt-in is present, startup logs must warn that the HTTP read API +surface is unauthenticated and must be protected by operator-managed network +controls. + ## Alternatives Considered ### Alternative 1: Clarion-native registry without a flag — hard displacement @@ -106,6 +187,7 @@ Accept that Filigree mints its own file IDs forever; Clarion reconciles post-hoc - [ADR-003](./ADR-003-entity-id-scheme.md) — symbolic entity IDs are what `clarion` mode uses as `file_records.id` values. - [ADR-004](./ADR-004-finding-exchange-format.md) — findings intake uses the same `file_id` that `resolve_file` returns. - ADR-008 (superseded) — earlier framing of this decision as a "feature flag"; the recon revealed it is an interface, not a flag. +- [ADR-012](./ADR-012-http-auth-default.md) — superseded for this registry-backend HTTP read surface; ADR-014 owns the unauthenticated loopback-only trust model and non-loopback guard. - [ADR-016](./ADR-016-observation-transport.md) — the `create_observation(file_path=…)` auto-create path is one of the three delegated operations; under `registry_backend: clarion` mode the `file_id` resolution in that path uses this ADR's protocol regardless of whether ADR-016's transport is MCP-spawn (v0.1) or HTTP (v0.2). - [ADR-017](./ADR-017-severity-and-dedup.md) — `mark_unseen=true` dedup relies on stable file IDs, which `clarion` mode provides and shadow mode does not. - [ADR-018](./ADR-018-identity-reconciliation.md) — the qualname ↔ EntityId translation layer is adjacent; `clarion` mode's `file_id` resolution is one slice of the broader identity-reconciliation surface. diff --git a/docs/clarion/adr/ADR-021-plugin-authority-hybrid.md b/docs/clarion/adr/ADR-021-plugin-authority-hybrid.md index bdb8558f..dbf5ad62 100644 --- a/docs/clarion/adr/ADR-021-plugin-authority-hybrid.md +++ b/docs/clarion/adr/ADR-021-plugin-authority-hybrid.md @@ -137,7 +137,7 @@ Use cgroup v2 (`systemd-run --user --scope` or direct cgroup mounts) for per-plu ## Related Decisions - [ADR-002](./ADR-002-plugin-transport-json-rpc.md) — the subprocess transport is the enforcement surface for Content-Length ceiling and crash-loop counting. This ADR extends ADR-002's supervision loop with four specific violation subcodes. -- [ADR-012](./ADR-012-http-auth-default.md) — the HTTP auth default flip closes T-02 the way this ADR closes T-08/T-11/T-12. The two ADRs form a matched pair covering the panel's "three non-negotiable v0.1 controls." +- [ADR-012](./ADR-012-http-auth-default.md) — historical HTTP auth default decision. ADR-014 now owns the registry-backend HTTP read API posture; this ADR remains the plugin-side counterpart to the HTTP-side threat-model work. - [ADR-013](./ADR-013-pre-ingest-secret-scanner.md) — pre-ingest secret scanner covers T-10 (source + secrets exfiltration); this ADR's path-jail prevents a plugin from bypassing the scanner by reading outside `project_root`, and ADR-013's scanner runs on the file list this ADR's path-jail has already filtered. - [ADR-022](./ADR-022-core-plugin-ontology.md) — data-layer authority counterpart. This ADR's Layer 2 enforces at the transport/pipeline layer; ADR-022 enforces at the ontology layer (manifest-declared kinds, rule-ID namespacing). Both share the manifest-acceptance checkpoint. diff --git a/docs/clarion/adr/README.md b/docs/clarion/adr/README.md index a2c0bc69..ffdace1e 100644 --- a/docs/clarion/adr/README.md +++ b/docs/clarion/adr/README.md @@ -14,7 +14,7 @@ This folder is the canonical home for authored Clarion architecture decision rec | [ADR-006](./ADR-006-clustering-algorithm.md) | Clustering algorithm — Leiden on imports+calls subgraph; fallback amended by ADR-032 | Accepted; amended | | [ADR-007](./ADR-007-summary-cache-key.md) | Summary cache key — 5-part composite with TTL backstop and churn-eager invalidation | Accepted | | [ADR-011](./ADR-011-writer-actor-concurrency.md) | Writer-actor concurrency with per-N-files transactions; `--shadow-db` opt-in | Accepted | -| [ADR-012](./ADR-012-http-auth-default.md) | HTTP read-API authentication — UDS default with token fallback | Accepted | +| [ADR-012](./ADR-012-http-auth-default.md) | HTTP read-API authentication — UDS default with token fallback | Superseded for ADR-014 registry-backend API | | [ADR-013](./ADR-013-pre-ingest-secret-scanner.md) | Pre-ingest secret scanner with LLM-dispatch block | Accepted | | [ADR-014](./ADR-014-filigree-registry-backend.md) | Filigree `registry_backend` flag and pluggable `RegistryProtocol` | Accepted | | [ADR-015](./ADR-015-wardline-filigree-emission.md) | Wardline→Filigree emission ownership — Clarion-side SARIF translator (v0.1), native Wardline emitter (v0.2) | Accepted | diff --git a/docs/clarion/v0.1/detailed-design.md b/docs/clarion/v0.1/detailed-design.md index bc5e827e..d2286731 100644 --- a/docs/clarion/v0.1/detailed-design.md +++ b/docs/clarion/v0.1/detailed-design.md @@ -227,7 +227,7 @@ struct Entity { ### Entity ID scheme - **Source entities**: `{plugin_id}:{kind}:{canonical_qualified_name}` where `canonical_qualified_name` is the plugin's language-native fully-qualified identifier (for Python: `auth.tokens.TokenManager`, not `src/auth/tokens.py::TokenManager`). -- **Files**: `core:file:{content_addressed_path_hash}@{path}` — content-addressed so rename detection has a handle; `@{path}` suffix preserves human readability in logs. +- **Files**: `core:file:{qualified_name}` where `qualified_name` is the project-relative POSIX canonical path. File IDs may not contain `@`; content hashes are carried as drift metadata, not embedded in the ID. - **Subsystems**: `core:subsystem:{cluster_hash}` (from sorted member module IDs). - **Guidance sheets**: `core:guidance:{content_hash_short}`. - **Unresolvable Python imports** (stub): `python:unresolved:{module.path}` — reconciled to real entities when resolution becomes possible. @@ -1347,35 +1347,37 @@ Response: **404 behaviour**: returns `resolution_confidence: "none"` with an empty `entity_id` rather than HTTP 404. Lets callers distinguish "Clarion doesn't know this" from "Clarion is down." -### Authentication — full spec (ADR-012) +### Authentication — registry-backend HTTP read API -**Default on Linux / macOS — Unix domain socket** (`serve.auth: uds`): +ADR-014 supersedes ADR-012 for the Filigree `registry_backend: clarion` +HTTP read surface. The active contract is unauthenticated loopback-only by +default, with non-loopback binds refused unless +`serve.http.allow_non_loopback: true` and protected by an operator-managed +authenticated reverse proxy or equivalent access-control layer. -- `clarion serve` binds `/.clarion/socket` with mode `0600`, owner = the UID running `clarion serve`. -- Transport: HTTP/1.1 over UDS. Server: `axum` + `tokio::net::UnixListener`. Clients: `hyper-unix-connector` or equivalent. -- Auth is filesystem-permissions based; HTTP layer does not require `Authorization` header under UDS mode. -- Stale-socket cleanup: on startup, Clarion stat-checks `.clarion/socket` — if present and no process is listening, it unlinks before binding. Double-start (`clarion serve` twice) fails loudly with `CLA-INFRA-SOCKET-IN-USE`. -- Sibling discovery: `.clarion/config.json` records `serve.socket_path`; sibling tools read the path and connect via `unix://`. +The earlier UDS/token design remains historical context for a broader HTTP API, +not the implementation contract for `/api/v1/_capabilities` or +`/api/v1/files`. -**Fallback (auto on Windows, opt-in elsewhere) — TCP + Bearer token** (`serve.auth: token`): +**Default — loopback only**: -- Windows default: `token` mode on `127.0.0.1:8765` (configurable via `serve.bind_port`). -- Auto-mint: first `clarion serve` writes `.clarion/auth.token` (mode `0600`). Token format: 32 URL-safe base64 bytes (43 chars) prefixed `clrn_` for grep-ability in logs. -- OS-keychain promotion: `clarion serve auth promote-to-keychain` migrates the token from file to macOS Keychain / Linux libsecret / Windows Credential Manager via the `keyring` crate. Emits `CLA-INFRA-TOKEN-STORAGE-DEGRADED` if the keychain is unavailable (falls back to file). -- Wire format: HTTP header `Authorization: Bearer clrn_<43chars>`. Constant-time comparison. -- Rotation: `clarion serve auth rotate` generates a new token, accepts both old and new for 24 hours, drops the old. Rotation is idempotent. -- Scoping: v0.1 has one token per install with full read access. Per-endpoint scoping and revocation lists are v0.2+. +- `clarion serve` binds only to loopback addresses. +- HTTP layer does not require `Authorization` headers for the ADR-014 endpoint. +- `/api/v1/_capabilities` reports `api_version` and `instance_id` so sibling + clients can detect compatible deployments and project-instance drift. -**Explicit-none** (`serve.auth: none` or `--i-accept-no-auth`): +**Explicit non-loopback**: -- Allowed but loud: emits `CLA-INFRA-HTTP-AUTH-DISABLED` (severity ERROR) on every `clarion serve` startup and reaches Filigree through the normal finding pipeline. Persistent banner in logs. -- Use cases: air-gapped CI where external ingress is controlled separately; local debugging with explicit operator decision. CLI flag is deliberately verbose (`--i-accept-no-auth`) to prevent accidental muscle-memory enabling. +- Requires `serve.http.allow_non_loopback: true`. +- Startup logs warn that the surface is unauthenticated. +- Operators must front Clarion with authenticated ingress or equivalent access + control before exposing the endpoint beyond loopback. -**How sibling tools pick up auth in CI**: +**How sibling tools pick up access in CI**: -- **UDS mode**: sibling tools mount the project's `.clarion/socket` into their process (bind-mount for containers, bare path for same-host processes). No token plumbing. -- **Token mode**: `CLARION_TOKEN` env var (preferred for CI; injected via the CI provider's secret store) or `.clarion/auth.token` bind-mount. Wardline's `wardline.yaml` records the Clarion endpoint URL (`unix://…` or `http://127.0.0.1:8765`) but not the token — token comes from env. -- **Pre-flight check**: `clarion check-auth --from wardline` returns exit 0 if the endpoint is reachable and authenticated under the active mode. Returns 0 with warning under `none` mode. +- Same-host sibling tools use the loopback endpoint. +- CI or remote deployments that need non-loopback access provide protection + outside Clarion, then use `/api/v1/_capabilities` as the compatibility probe. --- @@ -1384,7 +1386,9 @@ Response: Some risks sit outside Clarion's code but inside the operator's responsibility. These belong in the team's onboarding doc, not in the tool's runtime defences: - **Use project-scoped API keys, not personal ones, when `storage.commit_db: true`.** Briefings in `.clarion/clarion.db` were paid for by whoever ran `clarion analyze`. A teammate pulling your committed DB benefits from LLM calls your personal key paid for. Use an Anthropic project / org key, not your personal key, when committing the DB. -- **Rotate tokens when a committed DB exposes a stale model's output.** If `.clarion/clarion.db` was generated with a leaked or exposed API key, the token is already used; the briefings in the DB are not themselves secret but the key's usage fingerprint is. Rotate, then re-run `clarion analyze` to overwrite the briefing provenance. +- **Protect non-loopback HTTP exposure outside Clarion.** If operators bind the + ADR-014 HTTP read API outside loopback, place an authenticated reverse proxy + or equivalent access-control layer in front of it. - **Review `.clarion/.gitignore` before first commit.** The default excludes `runs/*/log.jsonl` (raw LLM request/response bodies); if operators opt into committing run logs for audit, they accept that source excerpts sent to Anthropic ship to the repo. That's a choice, not an oversight — but it must be a deliberate one. ### Audit-surface finding IDs @@ -1396,7 +1400,7 @@ Every security-relevant event emits a finding: - `CLA-INFRA-SECRET-BASELINE-NO-JUSTIFICATION` — baseline entry lacks the required review rationale - `CLA-INFRA-SECRET-BASELINE-MATCH` — baseline entry suppressed a scanner hit - `CLA-INFRA-SECRET-OVERRIDE-UNCONFIRMED` — override flag was not confirmed, so the run aborted before start -- `CLA-INFRA-TOKEN-STORAGE-DEGRADED` — OS keychain unavailable, fell back to file-mode `0600` +- `CLA-INFRA-HTTP-NON-LOOPBACK-UNAUTHENTICATED` — ADR-014 HTTP read API was explicitly bound outside loopback and must be protected outside Clarion - `CLA-INFRA-BRIEFING-INVALID` — LLM returned schema-invalid content twice, possible injection - `CLA-SEC-VOCABULARY-CANDIDATE-NOVEL` — novel `patterns`/`antipatterns` tag proposed by LLM (light signal; mostly harmless) @@ -1585,9 +1589,9 @@ The table below is a navigation aid for implementers: it maps each ADR to the se | ADR-009 | Structured briefings vs free-form prose | §2 | | ADR-010 | MCP as first-class surface | — (system-design §8) | | [ADR-011](../adr/ADR-011-writer-actor-concurrency.md) | Writer-actor concurrency model | §3 | -| [ADR-012](../adr/ADR-012-http-auth-default.md) | HTTP auth — UDS default with TCP+token fallback | §7 | +| [ADR-012](../adr/ADR-012-http-auth-default.md) | Historical HTTP auth proposal — superseded by ADR-014 for registry-backend API | §7 | | [ADR-013](../adr/ADR-013-pre-ingest-secret-scanner.md) | Pre-ingest secret scanner | §8 | -| ADR-014 | Filigree `registry_backend` flag + pluggable `RegistryProtocol` | §7, §9 | +| ADR-014 | Filigree `registry_backend` flag + pluggable `RegistryProtocol`; owns registry-backend HTTP read trust model | §7, §9 | | [ADR-015](../adr/ADR-015-wardline-filigree-emission.md) | Wardline→Filigree emission: Clarion SARIF translator (v0.1), native Wardline POST (v0.2) | §7, §9 | | [ADR-016](../adr/ADR-016-observation-transport.md) | Observation transport: `filigree mcp` subprocess (v0.1); `POST /api/v1/observations` HTTP (v0.2 retirement) | §9 | | [ADR-017](../adr/ADR-017-severity-and-dedup.md) | Severity mapping + rule-ID round-trip + dedup via `mark_unseen=true` | §7 | @@ -1690,7 +1694,7 @@ The v0.1 core is Rust (locked — see §11 ADR-001). Crate choices below are rec ### HTTP -- **axum** for the read API (§7): integrates cleanly with tokio and `tower` middleware; Bearer-token auth, metrics, and ETag middleware compose cleanly. +- **axum** for the read API (§7): integrates cleanly with tokio and `tower` middleware; tracing, timeout, load-shedding, body-limit, metrics, and ETag middleware compose cleanly. - **reqwest** for Anthropic API calls; pooled connections; TLS via `rustls` (no native-TLS; see below). ### SQLite @@ -1729,12 +1733,6 @@ The v0.1 core is Rust (locked — see §11 ADR-001). Crate choices below are rec - **uuid** v7 for run IDs (time-sortable, useful for log scanning). - **time** (not `chrono`) for all datetime handling — strict, explicit, and serde-friendly. -### Cryptography - -- **rand** + **rand_core** for token generation. -- **subtle** for constant-time comparison of auth tokens. -- **keyring** crate for OS keychain (§7); `0600` file fallback uses `std::os::unix::fs::PermissionsExt`. - ### Testing - **insta** for snapshot testing (briefing JSON structures, markdown renderer output). @@ -1833,7 +1831,7 @@ Revision 5 (2026-04-17) restructures the single design document into a three-lay | §2.3 Python import resolution (CRITICAL) | Policies for sys.path, unresolved imports, re-exports, conditional imports | §1 Python plugin specifics | | §2.4 Secrets + prompt injection (CRITICAL) | Pre-ingest redaction; injection containment; operator guidance | §8, System-design §10 | | §2.5 Provider abstraction (HIGH) | Honest framing added — v0.1 is Anthropic-shaped by commitment | §4 LLM provider abstraction (System-design §5) | -| §2.6 HTTP auth (HIGH) | Token auth designed in v0.1; OS keychain; Wardline pickup path | §7 HTTP read API | +| §2.6 HTTP auth (HIGH) | ADR-014 supersedes token auth for the registry-backend API; non-loopback guard plus operator-managed access control is the active posture | §7 HTTP read API | | §3 ADR backlog | §11 with priorities; ADR-001 (Rust) locked without alternatives | §11 | | §4.1 Summary cache semantic validity | TTL backstop + graph-neighborhood drift flag | §4 | | §4.2 Guidance stock quality | Churn-tied staleness signals | System-design §7 | diff --git a/docs/clarion/v0.1/requirements.md b/docs/clarion/v0.1/requirements.md index b187dc42..7f11feb7 100644 --- a/docs/clarion/v0.1/requirements.md +++ b/docs/clarion/v0.1/requirements.md @@ -555,13 +555,22 @@ Clarion exposes read-only HTTP endpoints: `GET /api/v1/entities`, `GET /api/v1/e **Verification**: Contract test for each scheme; `resolution_confidence: none` returned as 200 with empty `entity_id` (not 404) so callers can distinguish absent-from-catalog vs. server-down. **See**: System Design §9 (Integrations, Entity resolve). -#### REQ-HTTP-03 — Token auth (opt-in) - -Token auth is available via `clarion.yaml:serve.auth: token` (default: `none`). Tokens are 32 random bytes base64-encoded, prefixed `clrn_`, stored in OS keychain preferred / file-mode-0600 fallback. Wire format is `Authorization: Bearer clrn_`; server-side uses constant-time comparison. Rotation via `clarion serve auth rotate` with a 24-hour grace window. - -**Rationale**: `clarion serve` runs on shared dev hosts and in CI containers (Wardline's consumption pattern). Loopback is not a security boundary on modern hosts (shared Docker networks, devcontainer proxies, DNS rebinding). Designing auth in v0.1 (even if opt-in by default) avoids retrofitting later. -**Verification**: Token-protected endpoint returns 401 without header; rotation accepts both old and new within grace window; constant-time comparison verified via microbenchmark. -**See**: System Design §9 (Integrations, HTTP Read API — Token auth). +#### REQ-HTTP-03 — Registry-backend HTTP trust model + +For Filigree `registry_backend: clarion`, Clarion's HTTP read API is +unauthenticated and loopback-only by default. Non-loopback binds are refused +unless `serve.http.allow_non_loopback: true` is set; that opt-in requires an +authenticated reverse proxy or equivalent operator-managed access-control layer +outside Clarion. + +**Rationale**: the ADR-014 read surface is a bounded local federation contract, +not the earlier broad HTTP API. The implementation prevents accidental network +exposure mechanically and leaves intentional non-loopback exposure to deployment +controls. +**Verification**: loopback bind starts without extra flags; non-loopback bind is +rejected without `allow_non_loopback`; non-loopback opt-in logs an +unauthenticated-surface warning. +**See**: System Design §9 (Integrations, HTTP Read API), ADR-014. #### REQ-HTTP-04 — ETag-style response caching @@ -759,13 +768,19 @@ Clarion structures prompts with explicit `.../.clarion/socket` with mode 0600, owner = current UID. -- HTTP/1.1 over UDS; no Bearer header required. -- Only the owning UID can connect; shared-Docker / multi-user-host scenarios closed structurally. -- No TCP bind at all — DNS-rebinding attack surface (T-05) is inapplicable. - -**Fallback — TCP + Bearer token** (`serve.auth: token`): -- Auto-default on Windows (UDS support patchy until 10 1803+); explicit opt-in elsewhere for cross-UID-namespace access, SSH port-forwarding, or containerised clients. -- `clarion serve` auto-mints `.clarion/auth.token` on first run (mode 0600, `clrn_<43 chars>` format). -- OS keychain promotion via `clarion serve auth promote-to-keychain`. -- Rotation via `clarion serve auth rotate` with 24-hour grace window. -- Scoping: v0.1 has one project-wide read token; per-endpoint scoping is v0.2. - -**Explicit-none** (`serve.auth: none` or `--i-accept-no-auth`): -- Allowed but loud: emits `CLA-INFRA-HTTP-AUTH-DISABLED` (severity ERROR) per serve startup; persistent warning banner in logs. For air-gapped CI with external ingress control or operators with strong local reasons. - -**CI integration**: `clarion check-auth --from wardline` returns exit 0 if the serve endpoint is reachable and authenticated under whichever mode is active. Wardline in CI picks up the token via `CLARION_TOKEN` env (preferred) or `.clarion/auth.token` bind-mount when in token mode. +#### Authentication — ADR-014 registry-backend read API + +ADR-014 supersedes ADR-012 for the Filigree `registry_backend: clarion` +HTTP read surface. The registry-backend API is unauthenticated and +loopback-only by default. It refuses non-loopback binds unless +`serve.http.allow_non_loopback: true`; that opt-in requires an +operator-managed authenticated reverse proxy or equivalent access-control +layer in front of Clarion. + +ADR-012's UDS/token design is retained as historical context for the earlier +broad v0.1 HTTP API proposal, but it is not the implementation contract for the +registry-backend file-resolution endpoint. + +Loopback is not a complete security boundary on modern dev hosts (shared +containers, devcontainers, and other local processes all sit on 127.0.0.1). +The ADR-014 stance accepts that local-read exposure for the bounded +registry-backend API and prevents accidental network exposure through the +non-loopback guard. + +**Default — loopback only**: +- Binds only to loopback addresses. +- No Bearer header or UDS transport is required for the ADR-014 endpoint. +- Any local process that can reach the loopback port can read registry-backend + file-resolution responses. +- Non-loopback binds are rejected unless explicitly allowed. + +**Explicit non-loopback**: +- Requires `serve.http.allow_non_loopback: true`. +- Startup logs must warn that the surface is unauthenticated. +- Operators must front the endpoint with an authenticated reverse proxy or + equivalent access-control layer. + +**CI integration**: Filigree and other sibling consumers should treat +`/api/v1/_capabilities` as the compatibility probe for the registry-backend +read surface and should rely on deployment-level access control when the API is +intentionally exposed beyond loopback. TLS is out of scope for v0.1. Operators wanting network exposure terminate TLS at a reverse proxy. @@ -1044,7 +1059,7 @@ Security is a first-class concern in v0.1 because Clarion sends source code to a | Secret exfiltration to LLM provider | Critical | `.env`, test fixtures, committed API keys → entities → Anthropic API | Pre-ingest secret scanner; findings block LLM dispatch | | Prompt injection via source | Critical | Adversarial docstrings / comments → briefing field values → future-prompt poisoning via cache | Schema validation + untrusted-content delimiters + `knowledge_basis: static_only` | | Guidance poisoning via LLM-proposed sheets | High | `propose_guidance` MCP tool promotes attacker text into prompts | Manual promotion gate — proposals create observations, not sheets | -| HTTP API reachable by other local processes | High | `clarion serve` on shared dev host / container | UDS default (mode 0600, UID-scoped); TCP+token fallback on Windows / cross-UID namespaces. See ADR-012. | +| HTTP API reachable by other local processes | Medium | `clarion serve` on shared dev host / container | ADR-014 registry-backend API is unauthenticated but loopback-only by default; non-loopback binds are refused unless explicitly allowed and protected by operator-managed access control. | | DB tampering via committed `.clarion/clarion.db` | Medium | Bad actor edits DB, commits, poisons teammate briefings | Content-hash cross-check on load (v0.2); `clarion db verify` CLI | | LLM audit-log leakage via git | Medium | `runs//log.jsonl` contains request/response bodies | Default-excluded from git | | Personal API key charged when committing team DB | Medium (operator) | Developer commits DB generated with personal key | Operator guidance; `--audit-key` hint | @@ -1096,8 +1111,7 @@ Every security-relevant event emits a finding: - `CLA-SEC-SECRET-DETECTED` — unredacted secret blocked LLM dispatch - `CLA-SEC-UNREDACTED-SECRETS-ALLOWED` — operator overrode block -- `CLA-INFRA-HTTP-AUTH-DISABLED` — `clarion serve` running with `serve.auth: none` (ADR-012); per-startup ERROR finding -- `CLA-INFRA-TOKEN-STORAGE-DEGRADED` — OS keychain unavailable +- `CLA-INFRA-HTTP-NON-LOOPBACK-UNAUTHENTICATED` — `clarion serve` running the ADR-014 HTTP read API on a non-loopback bind with `serve.http.allow_non_loopback: true`; per-startup WARN log - `CLA-INFRA-BRIEFING-INVALID` — schema validation failed twice (possible injection) - `CLA-SEC-VOCABULARY-CANDIDATE-NOVEL` — novel vocabulary tag proposed by LLM (light signal; mostly harmless) @@ -1201,7 +1215,7 @@ The parallel listing in [detailed-design.md §11](./detailed-design.md#11-archit | ADR-009 | Structured briefings vs free-form prose | To author | P2 | Principle 2 requires bounded, composable responses; prose is neither. Schema validation also enables prompt-injection detection (schema-invalid → possible injection). | | ADR-010 | MCP as first-class surface — lock-in cost vs ecosystem reach | To author | P2 | Anthropic's MCP standard is the ecosystem's current centre of gravity for LLM tool integrations; lock-in cost is acknowledged but the ecosystem reach outweighs it for v0.1. Strategic review at v0.3+. | | [ADR-011](../adr/ADR-011-writer-actor-concurrency.md) | Writer-actor concurrency model (vs shadow-DB swap) | Accepted | P0 | Single writer actor + per-N-files transactions (default N=50) is the committed shape; `--shadow-db` opt-in for zero-stale-read scenarios. Design-review §2.2 CRITICAL flag retires. SQLite-concurrency-under-load assumption named as v0.2 validation task (`NG-28` proposed). | -| [ADR-012](../adr/ADR-012-http-auth-default.md) | HTTP read-API auth: UDS default with TCP+token fallback | Accepted | P0 | v0.1 default is Unix domain socket (`.clarion/socket` mode 0600); Windows / cross-UID-namespace / SSH-forwarded cases fall back to TCP + auto-minted Bearer token. `serve.auth: none` emits `CLA-INFRA-HTTP-AUTH-DISABLED` ERROR per serve startup. Closes T-02 (risk 9) and structurally reduces T-05 in the primary path. Panel "three non-negotiable v0.1 controls" rec 1. | +| [ADR-012](../adr/ADR-012-http-auth-default.md) | Historical HTTP read-API auth proposal: UDS default with TCP+token fallback | Superseded for ADR-014 registry-backend API | P0 | ADR-014 now owns the registry-backend HTTP read API posture: unauthenticated loopback-only by default, non-loopback refused unless explicitly allowed and protected externally. ADR-012 remains context for the earlier broad HTTP API proposal. | | [ADR-013](../adr/ADR-013-pre-ingest-secret-scanner.md) | Pre-ingest secret scanner with LLM-dispatch block | Accepted | P0 | Rust-native port of detect-secrets rule set (preserves NFR-OPS-04 single-binary). File-level block on detection; structural extraction preserved; briefings marked `briefing_blocked: secret_present`. `.clarion/secrets-baseline.yaml` for false-positives. `--allow-unredacted-secrets` requires TTY confirm OR explicit `--confirm-allow-unredacted-secrets=yes-i-understand` in CI. | | [ADR-014](../adr/ADR-014-filigree-registry-backend.md) | Filigree `registry_backend` flag + pluggable `RegistryProtocol` — schema surgery, not config flip | Accepted | P0 | Four NOT-NULL foreign keys on `file_records(id)` + three auto-create paths require a real interface, not a flag. Clarion's shadow-registry fallback preserves v0.1 shipability when Filigree hasn't landed the surgery. | | [ADR-015](../adr/ADR-015-wardline-filigree-emission.md) | Wardline→Filigree emission ownership: Clarion-side SARIF translator (v0.1), native Wardline POST (v0.2) | Accepted | P0 | Wardline has no HTTP client today (`integration-recon:339`); adding one is a refactor not on the v0.1 timeline. Clarion-side translator ships independently; translator stays permanent for Semgrep / CodeQL / etc. `loom.md` §5 asterisk 1 retires when native emitter lands. Revision trigger: Block C2 spike showing emitter is ≤1 day of work promotes to v0.1. | diff --git a/docs/federation/contracts.md b/docs/federation/contracts.md index 487cd872..8aec208d 100644 --- a/docs/federation/contracts.md +++ b/docs/federation/contracts.md @@ -17,6 +17,19 @@ serve: The MCP stdio server remains available on stdin/stdout. The HTTP surface is read-only and uses Clarion's existing SQLite reader pool. +All non-2xx responses use this closed JSON error envelope: + +```json +{ + "error": "path does not resolve to a Clarion file entity", + "code": "NOT_FOUND" +} +``` + +The initial `code` enum is closed to `INVALID_PATH`, +`PATH_OUTSIDE_PROJECT`, `NOT_FOUND`, `STORAGE_ERROR`, and `INTERNAL`. +Clients must switch on `code`; `error` is human-readable diagnostic text. + ### `GET /api/v1/files?path=&language=` Resolves a project-relative or absolute file path to the Clarion file identity @@ -33,7 +46,7 @@ Successful response: ```json { - "entity_id": "core:file:hash-demo@demo.py", + "entity_id": "core:file:demo.py", "content_hash": "hash-demo-file", "canonical_path": "demo.py", "language": "python" @@ -42,14 +55,22 @@ Successful response: Semantics: -- `entity_id` is opaque to Filigree. +- `entity_id` is opaque to Filigree and follows ADR-003's file-kind shape + `core:file:{qualified_name}`. - `content_hash` is the drift signal Filigree stores with the resolved row. -- `canonical_path` is Clarion's project-relative canonical path. +- `canonical_path` is Clarion's project-relative canonical path: no leading + `/`, no leading `./`, no trailing `/`, and `/` as the separator on every + platform. - `language` is the normalized language value Clarion used for the resolution. - Unknown or outside-project paths return a non-2xx JSON error instead of guessing. +- If no file-kind entity row exists for the path, Clarion returns + `404` with `code: "NOT_FOUND"` instead of synthesizing a file ID. -Fixture: [`fixtures/get-api-v1-files.demo-python.json`](./fixtures/get-api-v1-files.demo-python.json). +The RC1 fixture at +[`fixtures/get-api-v1-files.demo-python.json`](./fixtures/get-api-v1-files.demo-python.json) +still captures the pre-hardening response shape. It is intentionally not the +normative contract for this section until the C3 fixture gate upgrades it. ### `GET /api/v1/_capabilities` @@ -60,14 +81,26 @@ Successful response: ```json { + "api_version": 1, + "instance_id": "9bd7234e-6d44-4a38-9ae4-76f912a10221", "registry_backend": true, - "file_registry": true, - "version": "0.1" + "file_registry": true } ``` Filigree should treat `registry_backend: true` as the flag that the `/api/v1/files` resolution surface is present. -Fixture: [`fixtures/get-api-v1-capabilities.json`](./fixtures/get-api-v1-capabilities.json). +`api_version` is the HTTP read API wire-contract version, not Clarion product +semver. It increments only for incompatible changes to the wire contract +consumed by existing Filigree clients. + +`instance_id` is the stable per-project Clarion instance fingerprint persisted +in `.clarion/instance_id`. Filigree should treat a changed `instance_id` for a +previously known endpoint as evidence that it is now talking to a different +Clarion project instance. +The RC1 fixture at +[`fixtures/get-api-v1-capabilities.json`](./fixtures/get-api-v1-capabilities.json) +still captures the pre-hardening response shape. It is intentionally not the +normative contract for this section until the C3 fixture gate upgrades it. diff --git a/docs/federation/filigree-side/ADR-014-registry-backend-and-file-identity-displacement.md b/docs/federation/filigree-side/ADR-014-registry-backend-and-file-identity-displacement.md index 6e912489..17d0df29 100644 --- a/docs/federation/filigree-side/ADR-014-registry-backend-and-file-identity-displacement.md +++ b/docs/federation/filigree-side/ADR-014-registry-backend-and-file-identity-displacement.md @@ -10,7 +10,7 @@ Filigree gains a pluggable `RegistryProtocol` selected by a `registry_backend` config flag with two modes: - `local` (default, unchanged behaviour) — Filigree's native UUID-derived file IDs. -- `clarion` (opt-in, per-project) — Filigree delegates file-identity resolution to Clarion's HTTP read API; `file_records.id` stores Clarion's symbolic entity ID (`core:file:{hash}@{path}` per Clarion ADR-003). +- `clarion` (opt-in, per-project) — Filigree delegates file-identity resolution to Clarion's HTTP read API; `file_records.id` stores Clarion's symbolic file-kind entity ID (`core:file:{qualified_name}` per Clarion ADR-003). Clarion supplies `content_hash` separately as drift metadata; hashes are not embedded in file IDs. A `FILIGREE_FILE_REGISTRY_DISPLACED` error code surfaces direct file-registration attempts that conflict with `clarion` mode. The `registry_backend` value is published in `GET /api/files/_schema.config_flags` for capability probing. Fail-closed startup applies only under `clarion` mode (an `--allow-local-fallback` escape exists for single-operator recovery). diff --git a/docs/operator/clarion-http-read-api.md b/docs/operator/clarion-http-read-api.md new file mode 100644 index 00000000..837fbf26 --- /dev/null +++ b/docs/operator/clarion-http-read-api.md @@ -0,0 +1,32 @@ +# Clarion HTTP Read API + +Clarion can expose a read-only HTTP API for local sibling integrations such as +Filigree's `registry_backend: clarion` mode. The wire contract is documented in +the [federation contracts](../federation/contracts.md). + +## Trust Model + +The HTTP read API is unauthenticated. By default, `clarion serve` binds it only +to loopback addresses so it is reachable from local processes on the same host, +not from the network. + +Do not bind the HTTP read API to a non-loopback address unless an authenticated +reverse proxy or equivalent access-control layer is in front of it. Clarion +refuses non-loopback binds unless `serve.http.allow_non_loopback: true` is set. +That opt-in is an operator assertion that the unauthenticated Clarion HTTP +surface is protected outside Clarion. + +Startup logs for non-loopback opt-in must warn that the API is unauthenticated. +Treat the endpoint as source-code metadata exposure: anyone who can reach it can +read Clarion's catalog responses for the project. + +## Contract Summary + +`GET /api/v1/_capabilities` returns the read API `api_version`, the project +`instance_id`, and booleans indicating whether registry-backend file resolution +is available. + +`GET /api/v1/files?path=&language=` resolves an existing Clarion file-kind row +to the entity ID and project-relative canonical path Filigree should store. It +fails closed when the path is invalid, outside the project, missing from the +catalog, or unavailable because of storage errors. diff --git a/docs/suite/loom.md b/docs/suite/loom.md index 72e49b2a..baaaf92a 100644 --- a/docs/suite/loom.md +++ b/docs/suite/loom.md @@ -84,7 +84,14 @@ Because the strongest pressure on this charter comes from "wouldn't it be easier - **A central store or database.** Each product owns its data locally. No shared SQLite/Postgres/object-store sits under the suite. - **A system of record for any cross-product state.** Finding lifecycle lives in Filigree. Entity identity lives in Clarion. Policy baselines live in Wardline. Execution provenance (if Shuttle ships) lives in Shuttle. Loom does not own or mirror these. - **An identity reconciliation service.** When cross-scheme translation is needed — e.g. Wardline qualname → Clarion entity ID — the product that *cares* does the translation, because that product is the one whose authority needs it. Clarion translates qualnames because Clarion owns the catalog that makes them meaningful. There is no neutral "Loom identity oracle." -- **A capability negotiation bus.** Products probe each other directly via their own surfaces (HTTP endpoints, MCP tools, CLI flags). Version skew is handled bilaterally, not through a Loom-level registry. +- **A capability negotiation bus.** Products probe each other directly via + their own surfaces (HTTP endpoints, MCP tools, CLI flags). Version skew is + handled bilaterally, not through a Loom-level registry. Clarion's HTTP read + API is one such bilateral surface; its operator trust model is documented in + [`docs/operator/clarion-http-read-api.md`](../operator/clarion-http-read-api.md): + the HTTP surface is unauthenticated, loopback-only by default, and requires an + authenticated reverse proxy or equivalent control before any non-loopback + bind. The test for any proposed addition: if the proposal introduces something that would need to be *running* or *present* for the suite to work, it violates federation. Integration protocols, schemas, and narrow contracts are fine. Shared infrastructure that sibling products *depend on* is not. From b10785788c38768c08625501c36bc04aaa961d80 Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Tue, 19 May 2026 15:02:25 +1000 Subject: [PATCH 020/187] C2 fix: remove synthetic file identity branch resolve_file now fails closed when no cataloged kind='file' entity exists for the source path, instead of deriving a file identity from module/function anchors. ADR-003 forbids '@' in entity identifiers, so Clarion must not manufacture core:file:{hash}@{path} IDs. The federation read path should return the stored file entity ID or no file. --- crates/clarion-storage/src/query.rs | 42 ++++++++--------- crates/clarion-storage/tests/query_helpers.rs | 45 ++++++++++++++++++- 2 files changed, 65 insertions(+), 22 deletions(-) diff --git a/crates/clarion-storage/src/query.rs b/crates/clarion-storage/src/query.rs index 5f9ff91d..ba89cfab 100644 --- a/crates/clarion-storage/src/query.rs +++ b/crates/clarion-storage/src/query.rs @@ -186,22 +186,7 @@ pub fn resolve_file( briefing_blocked, })); } - let Some(anchor) = source_entity_for_path(conn, &normalized, None)? else { - return Ok(None); - }; - let briefing_blocked = entity_briefing_block_reason(&anchor.properties_json); - let content_hash = anchor - .content_hash - .or_else(|| file_content_hash(Path::new(&normalized))) - .unwrap_or_default(); - let synthetic_id = format!("core:file:{content_hash}@{canonical_path}"); - Ok(Some(ResolvedFile { - entity_id: synthetic_id, - content_hash, - canonical_path, - language: resolved_language(language, &anchor.plugin_id, Path::new(&normalized)), - briefing_blocked, - })) + Ok(None) } /// Extract the `briefing_blocked` reason from an entity's `properties` JSON @@ -254,10 +239,27 @@ fn project_relative_path(project_root: &Path, normalized_path: &Path) -> Result< root.display() )) })?; - relative - .to_str() - .map(str::to_owned) - .ok_or_else(|| StorageError::InvalidSourcePath("source path is not valid UTF-8".to_owned())) + let mut parts = Vec::new(); + for component in relative.components() { + match component { + Component::Normal(part) => { + let Some(part) = part.to_str() else { + return Err(StorageError::InvalidSourcePath( + "source path is not valid UTF-8".to_owned(), + )); + }; + parts.push(part); + } + Component::CurDir => {} + Component::ParentDir | Component::RootDir | Component::Prefix(_) => { + return Err(StorageError::InvalidSourcePath(format!( + "{} is not a project-relative source path", + normalized_path.display() + ))); + } + } + } + Ok(parts.join("/")) } fn file_content_hash(path: &Path) -> Option { diff --git a/crates/clarion-storage/tests/query_helpers.rs b/crates/clarion-storage/tests/query_helpers.rs index 219315d9..2b314694 100644 --- a/crates/clarion-storage/tests/query_helpers.rs +++ b/crates/clarion-storage/tests/query_helpers.rs @@ -787,11 +787,44 @@ fn resolve_file_surfaces_briefing_blocked_reason_from_properties() { ); } +#[test] +fn resolve_file_returns_none_when_no_file_kind_entity_exists() { + let tempdir = tempfile::tempdir().expect("temp project root"); + let project_root = tempdir.path(); + let source_path = project_root.join("src").join("demo.py"); + std::fs::create_dir_all(source_path.parent().unwrap()).expect("create source dir"); + std::fs::write(&source_path, "def entry():\n return 1\n").expect("write source"); + let canonical = source_path.canonicalize().expect("canonical source"); + + let conn = open_fresh(&tempdir); + conn.execute( + "INSERT INTO entities ( + id, plugin_id, kind, name, short_name, source_file_path, + source_line_start, source_line_end, properties, content_hash, created_at, updated_at + ) VALUES ( + 'python:module:demo', 'python', 'module', 'demo', 'demo', ?1, + 1, 2, '{}', 'hash-demo-module', + '2026-05-19T00:00:00.000Z', '2026-05-19T00:00:00.000Z' + )", + params![canonical.display().to_string()], + ) + .expect("insert module entity"); + + let resolved = + resolve_file(&conn, project_root, "src/demo.py", "python").expect("resolve_file"); + + assert!( + resolved.is_none(), + "resolve_file must fail closed instead of synthesizing a file identity from a module row" + ); +} + #[test] fn resolve_file_returns_none_briefing_blocked_for_clean_entity() { let tempdir = tempfile::tempdir().expect("temp project root"); let project_root = tempdir.path(); - let source_path = project_root.join("demo.py"); + let source_path = project_root.join("src").join("demo.py"); + std::fs::create_dir_all(source_path.parent().unwrap()).expect("create source dir"); std::fs::write(&source_path, "def entry():\n return 1\n").expect("write source"); let canonical = source_path.canonicalize().expect("canonical source"); @@ -809,10 +842,18 @@ fn resolve_file_returns_none_briefing_blocked_for_clean_entity() { ) .expect("insert clean entity"); - let resolved = resolve_file(&conn, project_root, "demo.py", "python") + let resolved = resolve_file(&conn, project_root, "src/demo.py", "python") .expect("resolve_file") .expect("entity is known"); + assert_eq!(resolved.canonical_path, "src/demo.py"); + assert!( + !resolved.canonical_path.starts_with('/') + && !resolved.canonical_path.starts_with("./") + && !resolved.canonical_path.starts_with("../"), + "canonical path must be project-relative POSIX: {:?}", + resolved.canonical_path + ); assert!( resolved.briefing_blocked.is_none(), "clean entity must not surface a briefing_blocked reason; got {:?}", From 146370a6d9615be4f5e9989025ccf6084810fada Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Tue, 19 May 2026 15:10:42 +1000 Subject: [PATCH 021/187] C5 C11 fix: resolve cataloged files without empty hash fallback Resolve deleted-on-disk catalog rows from stored entity data without requiring candidate file canonicalization. Propagate hash fallback read failures instead of returning an empty content_hash when the catalog has no hash. Prefer catalog/plugin and extension language inference before caller-supplied language fallback. --- crates/clarion-storage/src/query.rs | 66 +++++--- crates/clarion-storage/tests/query_helpers.rs | 147 ++++++++++++++++++ 2 files changed, 192 insertions(+), 21 deletions(-) diff --git a/crates/clarion-storage/src/query.rs b/crates/clarion-storage/src/query.rs index ba89cfab..0058bda5 100644 --- a/crates/clarion-storage/src/query.rs +++ b/crates/clarion-storage/src/query.rs @@ -171,18 +171,22 @@ pub fn resolve_file( file: &str, language: &str, ) -> Result> { - let normalized = normalize_source_path(project_root, file)?; - let canonical_path = project_relative_path(project_root, Path::new(&normalized))?; - if let Some(entity) = source_entity_for_path(conn, &normalized, Some("file"))? { + let lookup_path = normalize_lookup_path(project_root, file)?; + let normalized = lookup_path + .to_str() + .ok_or_else(|| StorageError::InvalidSourcePath(format!("{file:?} is not valid UTF-8")))?; + let canonical_path = project_relative_path(project_root, &lookup_path)?; + if let Some(entity) = source_entity_for_path(conn, normalized, Some("file"))? { let briefing_blocked = entity_briefing_block_reason(&entity.properties_json); + let content_hash = match entity.content_hash { + Some(content_hash) => content_hash, + None => file_content_hash(&lookup_path)?, + }; return Ok(Some(ResolvedFile { entity_id: entity.id, - content_hash: entity - .content_hash - .or_else(|| file_content_hash(Path::new(&normalized))) - .unwrap_or_default(), + content_hash, canonical_path, - language: resolved_language(language, &entity.plugin_id, Path::new(&normalized)), + language: resolved_language(language, &entity.plugin_id, &lookup_path), briefing_blocked, })); } @@ -262,26 +266,46 @@ fn project_relative_path(project_root: &Path, normalized_path: &Path) -> Result< Ok(parts.join("/")) } -fn file_content_hash(path: &Path) -> Option { - fs::read(path) - .ok() - .map(|bytes| blake3::hash(&bytes).to_hex().to_string()) +fn normalize_lookup_path(project_root: &Path, file: &str) -> Result { + let root = project_root.canonicalize()?; + let input = Path::new(file); + let candidate = if input.is_absolute() { + input.to_path_buf() + } else { + root.join(input) + }; + let lexical = normalize_lexically(&candidate); + if !lexical.starts_with(&root) { + return Err(StorageError::InvalidSourcePath(format!( + "{file:?} escapes project root {}", + root.display() + ))); + } + Ok(lexical) +} + +fn file_content_hash(path: &Path) -> std::io::Result { + fs::read(path).map(|bytes| blake3::hash(&bytes).to_hex().to_string()) } fn resolved_language(requested: &str, plugin_id: &str, path: &Path) -> String { - if !requested.trim().is_empty() { - return requested.trim().to_owned(); - } if plugin_id != "core" { return plugin_id.to_owned(); } + if let Some(inferred) = language_for_extension(path) { + return inferred; + } + requested.trim().to_owned() +} + +fn language_for_extension(path: &Path) -> Option { match path.extension().and_then(|extension| extension.to_str()) { - Some("py") => "python".to_owned(), - Some("rs") => "rust".to_owned(), - Some("js") => "javascript".to_owned(), - Some("ts") => "typescript".to_owned(), - Some(extension) => extension.to_owned(), - None => String::new(), + Some("py") => Some("python".to_owned()), + Some("rs") => Some("rust".to_owned()), + Some("js") => Some("javascript".to_owned()), + Some("ts") => Some("typescript".to_owned()), + Some(extension) => Some(extension.to_owned()), + None => None, } } diff --git a/crates/clarion-storage/tests/query_helpers.rs b/crates/clarion-storage/tests/query_helpers.rs index 2b314694..5d659355 100644 --- a/crates/clarion-storage/tests/query_helpers.rs +++ b/crates/clarion-storage/tests/query_helpers.rs @@ -861,6 +861,153 @@ fn resolve_file_returns_none_briefing_blocked_for_clean_entity() { ); } +#[test] +fn resolve_file_deleted_on_disk_but_cataloged_row_resolves() { + let tempdir = tempfile::tempdir().expect("temp project root"); + let project_root = tempdir.path(); + let source_path = project_root.join("src").join("deleted.py"); + std::fs::create_dir_all(source_path.parent().unwrap()).expect("create source dir"); + std::fs::write(&source_path, "def gone():\n return 1\n").expect("write source"); + let canonical = source_path.canonicalize().expect("canonical source"); + + let conn = open_fresh(&tempdir); + conn.execute( + "INSERT INTO entities ( + id, plugin_id, kind, name, short_name, source_file_path, + source_line_start, source_line_end, properties, content_hash, created_at, updated_at + ) VALUES ( + 'python:file:deleted', 'python', 'file', 'deleted.py', 'deleted.py', ?1, + 1, 2, '{}', 'hash-deleted', + '2026-05-19T00:00:00.000Z', '2026-05-19T00:00:00.000Z' + )", + params![canonical.display().to_string()], + ) + .expect("insert deleted entity"); + std::fs::remove_file(&source_path).expect("delete source after cataloging"); + + let resolved = resolve_file(&conn, project_root, "src/deleted.py", "python") + .expect("resolve_file should use catalog row without requiring disk file") + .expect("entity is known"); + + assert_eq!(resolved.entity_id, "python:file:deleted"); + assert_eq!(resolved.content_hash, "hash-deleted"); + assert_eq!(resolved.canonical_path, "src/deleted.py"); + assert!( + !resolved.canonical_path.starts_with('/') + && !resolved.canonical_path.starts_with("./") + && !resolved.canonical_path.starts_with("../"), + "canonical path must be project-relative POSIX: {:?}", + resolved.canonical_path + ); +} + +#[test] +#[cfg(unix)] +fn resolve_file_unreadable_hash_failure_propagates() { + use std::os::unix::fs::PermissionsExt; + + let tempdir = tempfile::tempdir().expect("temp project root"); + let project_root = tempdir.path(); + let source_path = project_root.join("src").join("unreadable.py"); + std::fs::create_dir_all(source_path.parent().unwrap()).expect("create source dir"); + std::fs::write(&source_path, "def unreadable():\n return 1\n").expect("write source"); + let canonical = source_path.canonicalize().expect("canonical source"); + + let conn = open_fresh(&tempdir); + conn.execute( + "INSERT INTO entities ( + id, plugin_id, kind, name, short_name, source_file_path, + source_line_start, source_line_end, properties, content_hash, created_at, updated_at + ) VALUES ( + 'python:file:unreadable', 'python', 'file', 'unreadable.py', 'unreadable.py', ?1, + 1, 2, '{}', NULL, + '2026-05-19T00:00:00.000Z', '2026-05-19T00:00:00.000Z' + )", + params![canonical.display().to_string()], + ) + .expect("insert unreadable entity"); + let original_permissions = std::fs::metadata(&source_path) + .expect("source metadata") + .permissions(); + std::fs::set_permissions(&source_path, std::fs::Permissions::from_mode(0o000)) + .expect("make source unreadable"); + + if std::fs::read(&source_path).is_ok() { + std::fs::set_permissions(&source_path, original_permissions).expect("restore source perms"); + eprintln!("skipping unreadable-file assertion because this runner can read 0o000 files"); + return; + } + + let result = resolve_file(&conn, project_root, "src/unreadable.py", "python"); + + std::fs::set_permissions(&source_path, original_permissions).expect("restore source perms"); + let error = result.expect_err("missing catalog hash must propagate hash fallback read failure"); + assert!( + error.to_string().contains("io error"), + "unexpected error: {error}" + ); +} + +#[test] +fn resolve_file_does_not_echo_invalid_requested_language_over_catalog_inference() { + let tempdir = tempfile::tempdir().expect("temp project root"); + let project_root = tempdir.path(); + let source_path = project_root.join("src").join("demo.py"); + std::fs::create_dir_all(source_path.parent().unwrap()).expect("create source dir"); + std::fs::write(&source_path, "def entry():\n return 1\n").expect("write source"); + let canonical = source_path.canonicalize().expect("canonical source"); + + let conn = open_fresh(&tempdir); + conn.execute( + "INSERT INTO entities ( + id, plugin_id, kind, name, short_name, source_file_path, + source_line_start, source_line_end, properties, content_hash, created_at, updated_at + ) VALUES ( + 'python:file:demo-language', 'python', 'file', 'demo.py', 'demo.py', ?1, + 1, 2, '{}', 'hash-demo-language', + '2026-05-19T00:00:00.000Z', '2026-05-19T00:00:00.000Z' + )", + params![canonical.display().to_string()], + ) + .expect("insert python entity"); + + let resolved = resolve_file(&conn, project_root, "src/demo.py", "javascript") + .expect("resolve_file") + .expect("entity is known"); + + assert_eq!(resolved.language, "python"); +} + +#[test] +fn resolve_file_prefers_core_extension_inference_over_requested_language() { + let tempdir = tempfile::tempdir().expect("temp project root"); + let project_root = tempdir.path(); + let source_path = project_root.join("src").join("demo.py"); + std::fs::create_dir_all(source_path.parent().unwrap()).expect("create source dir"); + std::fs::write(&source_path, "def entry():\n return 1\n").expect("write source"); + let canonical = source_path.canonicalize().expect("canonical source"); + + let conn = open_fresh(&tempdir); + conn.execute( + "INSERT INTO entities ( + id, plugin_id, kind, name, short_name, source_file_path, + source_line_start, source_line_end, properties, content_hash, created_at, updated_at + ) VALUES ( + 'core:file:src/demo.py', 'core', 'file', 'demo.py', 'demo.py', ?1, + 1, 2, '{}', 'hash-core-demo', + '2026-05-19T00:00:00.000Z', '2026-05-19T00:00:00.000Z' + )", + params![canonical.display().to_string()], + ) + .expect("insert core file entity"); + + let resolved = resolve_file(&conn, project_root, "src/demo.py", "javascript") + .expect("resolve_file") + .expect("entity is known"); + + assert_eq!(resolved.language, "python"); +} + #[test] fn entity_briefing_block_reason_parses_property_and_tolerates_garbage() { assert_eq!( From a59d22cb1abd862bf03f11199c49ca13d78571fb Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Tue, 19 May 2026 15:38:58 +1000 Subject: [PATCH 022/187] C1 C13 fix: classify HTTP read errors --- crates/clarion-cli/src/http_read.rs | 126 ++++++++++++++- crates/clarion-cli/tests/serve.rs | 232 +++++++++++++++++++++++++--- 2 files changed, 335 insertions(+), 23 deletions(-) diff --git a/crates/clarion-cli/src/http_read.rs b/crates/clarion-cli/src/http_read.rs index 54f50263..ff65c45c 100644 --- a/crates/clarion-cli/src/http_read.rs +++ b/crates/clarion-cli/src/http_read.rs @@ -1,4 +1,6 @@ +use std::error::Error as StdError; use std::path::PathBuf; +use std::sync::LazyLock; use std::thread; use anyhow::{Context, Result, anyhow}; @@ -8,10 +10,19 @@ use axum::response::{IntoResponse, Response}; use axum::routing::get; use axum::{Json, Router}; use clarion_mcp::config::HttpReadConfig; -use clarion_storage::{ReaderPool, resolve_file}; +use clarion_storage::{ReaderPool, StorageError, resolve_file}; use serde::{Deserialize, Serialize}; use tokio::sync::oneshot; +static HTTP_ERROR_DISPATCH: LazyLock = LazyLock::new(|| { + let subscriber = tracing_subscriber::fmt() + .with_writer(std::io::stderr) + .with_target(false) + .with_ansi(false) + .finish(); + tracing::Dispatch::new(subscriber) +}); + #[derive(Debug)] pub struct HttpReadServer { shutdown: Option>, @@ -98,6 +109,7 @@ fn router(state: AppState) -> Router { #[derive(Debug, Deserialize)] struct FileQuery { + #[serde(default)] path: String, #[serde(default)] language: String, @@ -121,12 +133,24 @@ struct CapabilitiesResponse { #[derive(Debug, Serialize)] struct ErrorResponse { error: String, + code: ErrorCode, +} + +#[derive(Debug, Copy, Clone, Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +enum ErrorCode { + InvalidPath, + PathOutsideProject, + NotFound, + StorageError, + Internal, } async fn get_file(State(state): State, Query(query): Query) -> Response { if query.path.trim().is_empty() { return json_error( StatusCode::BAD_REQUEST, + ErrorCode::InvalidPath, "path query parameter must not be blank", ); } @@ -145,7 +169,11 @@ async fn get_file(State(state): State, Query(query): Query) reason = %reason, "HTTP /api/v1/files refusing to expose briefing-blocked entity" ); - return json_error(StatusCode::NOT_FOUND, "file is not known to Clarion"); + return json_error( + StatusCode::NOT_FOUND, + ErrorCode::NotFound, + "file is not known to Clarion", + ); } ( StatusCode::OK, @@ -158,8 +186,12 @@ async fn get_file(State(state): State, Query(query): Query) ) .into_response() } - Ok(None) => json_error(StatusCode::NOT_FOUND, "file is not known to Clarion"), - Err(err) => json_error(StatusCode::BAD_REQUEST, &err.to_string()), + Ok(None) => json_error( + StatusCode::NOT_FOUND, + ErrorCode::NotFound, + "file is not known to Clarion", + ), + Err(err) => json_read_error(err), } } @@ -171,11 +203,95 @@ async fn get_capabilities() -> Json { }) } -fn json_error(status: StatusCode, message: &str) -> Response { +fn json_read_error(err: StorageError) -> Response { + let error = classify_read_error(&err); + if error.status.is_server_error() { + log_read_server_error(error.code, error.status, &err); + } + json_error(error.status, error.code, error.message) +} + +struct ReadError { + status: StatusCode, + code: ErrorCode, + message: &'static str, +} + +fn classify_read_error(err: &StorageError) -> ReadError { + match err { + StorageError::InvalidQuery(_) => ReadError { + status: StatusCode::BAD_REQUEST, + code: ErrorCode::InvalidPath, + message: "path query parameter is invalid", + }, + StorageError::InvalidSourcePath(_) => ReadError { + status: StatusCode::BAD_REQUEST, + code: ErrorCode::PathOutsideProject, + message: "path is outside project root", + }, + StorageError::Pool(_) => ReadError { + status: StatusCode::SERVICE_UNAVAILABLE, + code: ErrorCode::StorageError, + message: "file lookup storage is unavailable", + }, + StorageError::PoolInteract(_) => ReadError { + status: StatusCode::INTERNAL_SERVER_ERROR, + code: ErrorCode::Internal, + message: "internal file lookup failure", + }, + StorageError::Sqlite(_) + | StorageError::PoolBuild(_) + | StorageError::PragmaInvariant(_) + | StorageError::Migration { .. } + | StorageError::Io(_) => ReadError { + status: StatusCode::INTERNAL_SERVER_ERROR, + code: ErrorCode::StorageError, + message: "file lookup failed", + }, + StorageError::WriterGone + | StorageError::WriterProtocol(_) + | StorageError::WriterNoResponse => ReadError { + status: StatusCode::INTERNAL_SERVER_ERROR, + code: ErrorCode::Internal, + message: "internal file lookup failure", + }, + } +} + +fn format_error_chain(err: &StorageError) -> String { + let mut chain = vec![err.to_string()]; + let mut source = err.source(); + let mut depth = 0; + while let Some(err) = source { + if depth >= 8 { + chain.push("additional sources omitted".to_owned()); + break; + } + chain.push(err.to_string()); + source = err.source(); + depth += 1; + } + chain.join(": caused by: ") +} + +fn log_read_server_error(code: ErrorCode, status: StatusCode, err: &StorageError) { + let error_chain = format_error_chain(err); + tracing::dispatcher::with_default(&HTTP_ERROR_DISPATCH, || { + tracing::error!( + code = ?code, + status = status.as_u16(), + error_chain = %error_chain, + "HTTP /api/v1/files lookup failed" + ); + }); +} + +fn json_error(status: StatusCode, code: ErrorCode, message: &str) -> Response { ( status, Json(ErrorResponse { error: message.to_owned(), + code, }), ) .into_response() diff --git a/crates/clarion-cli/tests/serve.rs b/crates/clarion-cli/tests/serve.rs index fc234c0f..f05f2e63 100644 --- a/crates/clarion-cli/tests/serve.rs +++ b/crates/clarion-cli/tests/serve.rs @@ -1,5 +1,5 @@ use std::fs; -use std::io::{Read, Write}; +use std::io::{BufRead, Read, Write}; use std::net::{TcpListener, TcpStream}; use std::os::unix::fs::PermissionsExt; use std::path::Path; @@ -14,6 +14,12 @@ use clarion_core::{ use rusqlite::{Connection, params}; use serde_json::Value; +#[derive(Debug)] +struct HttpJsonResponse { + status_code: u16, + body: Value, +} + fn clarion_bin() -> Command { Command::cargo_bin("clarion").expect("clarion binary") } @@ -115,6 +121,151 @@ fn serve_http_files_endpoint_resolves_known_file_on_configured_port() { assert_eq!(response, fixture["response"]); } +#[test] +fn serve_http_files_blank_path_returns_invalid_path_envelope() { + let dir = tempfile::tempdir().expect("temp project"); + clarion_bin() + .args(["install", "--path"]) + .arg(dir.path()) + .env("PATH", "") + .assert() + .success(); + let bind = free_loopback_bind(); + write_http_config(dir.path(), &bind); + + let mut child = spawn_serve(dir.path()); + let response = wait_for_http_response(&bind, "/api/v1/files?path=&language=python"); + stop_serve(&mut child); + let response = response.expect("HTTP /api/v1/files error response"); + + assert_eq!(response.status_code, 400); + assert_eq!(response.body["code"], "INVALID_PATH"); + assert!( + response.body["error"].as_str().is_some(), + "error envelope must include a string message: {:?}", + response + ); +} + +#[test] +fn serve_http_files_path_traversal_returns_outside_project_envelope() { + let dir = tempfile::tempdir().expect("temp project"); + clarion_bin() + .args(["install", "--path"]) + .arg(dir.path()) + .env("PATH", "") + .assert() + .success(); + let bind = free_loopback_bind(); + write_http_config(dir.path(), &bind); + + let mut child = spawn_serve(dir.path()); + let response = + wait_for_http_response(&bind, "/api/v1/files?path=../outside.py&language=python"); + stop_serve(&mut child); + let response = response.expect("HTTP /api/v1/files error response"); + + assert_eq!(response.status_code, 400); + assert_eq!(response.body["code"], "PATH_OUTSIDE_PROJECT"); + assert!( + response.body["error"].as_str().is_some(), + "error envelope must include a string message: {:?}", + response + ); +} + +#[test] +fn serve_http_files_unknown_catalog_file_returns_not_found_envelope() { + let dir = tempfile::tempdir().expect("temp project"); + clarion_bin() + .args(["install", "--path"]) + .arg(dir.path()) + .env("PATH", "") + .assert() + .success(); + let bind = free_loopback_bind(); + write_http_config(dir.path(), &bind); + + let mut child = spawn_serve(dir.path()); + let response = wait_for_http_response(&bind, "/api/v1/files?path=missing.py&language=python"); + stop_serve(&mut child); + let response = response.expect("HTTP /api/v1/files error response"); + + assert_eq!(response.status_code, 404); + assert_eq!(response.body["code"], "NOT_FOUND"); + assert!( + response.body["error"].as_str().is_some(), + "error envelope must include a string message: {:?}", + response + ); +} + +#[test] +fn serve_http_files_storage_failure_returns_closed_error_without_raw_detail() { + let dir = tempfile::tempdir().expect("temp project"); + clarion_bin() + .args(["install", "--path"]) + .arg(dir.path()) + .env("PATH", "") + .assert() + .success(); + let bind = free_loopback_bind(); + write_http_config(dir.path(), &bind); + let source_path = dir.path().join("missing-on-disk.py"); + fs::write(&source_path, "def missing():\n return 1\n").expect("write source"); + let canonical_path = source_path + .canonicalize() + .expect("canonical source path") + .display() + .to_string(); + let db_path = dir.path().join(".clarion/clarion.db"); + let conn = Connection::open(&db_path).expect("open sqlite"); + conn.execute( + "INSERT INTO entities ( + id, plugin_id, kind, name, short_name, source_file_path, + source_line_start, source_line_end, properties, created_at, updated_at + ) VALUES ( + 'core:file:missing-on-disk.py', 'core', 'file', + 'missing-on-disk.py', 'missing-on-disk.py', ?1, + 1, 2, '{}', + '2026-05-19T00:00:00.000Z', '2026-05-19T00:00:00.000Z' + )", + params![canonical_path], + ) + .expect("insert file entity without cached hash"); + drop(conn); + fs::remove_file(&source_path).expect("remove cataloged file to force storage failure"); + + let mut child = spawn_serve(dir.path()); + let capabilities = wait_for_http_response(&bind, "/api/v1/_capabilities"); + let response = wait_for_http_response( + &bind, + "/api/v1/files?path=missing-on-disk.py&language=python", + ); + stop_serve(&mut child); + let capabilities = capabilities.expect("HTTP /api/v1/_capabilities response"); + assert_eq!(capabilities.status_code, 200); + let response = response.expect("HTTP /api/v1/files storage error response"); + + assert!( + response.status_code == 500 || response.status_code == 503, + "storage failures must be 500-class: {:?}", + response + ); + assert!( + response.body["code"] == "STORAGE_ERROR" || response.body["code"] == "INTERNAL", + "unexpected storage failure code: {:?}", + response + ); + let body = response.body.to_string(); + assert!(!body.to_ascii_lowercase().contains("sqlite")); + assert!(!body.contains("not a database")); + assert!(!body.contains("No such file")); + assert!(!body.contains("no such column")); + assert!(!body.contains("no such table")); + assert!(!body.contains(&dir.path().display().to_string())); +} + #[test] fn serve_http_capabilities_and_mcp_stdio_coexist() { let dir = tempfile::tempdir().expect("temp project"); @@ -667,11 +818,22 @@ fn stop_serve(child: &mut Child) { } fn wait_for_http_json(bind: &str, path: &str) -> Result { - let deadline = Instant::now() + Duration::from_secs(3); + let response = wait_for_http_response(bind, path)?; + if response.status_code != 200 { + return Err(format!( + "unexpected HTTP status {}; body: {}", + response.status_code, response.body + )); + } + Ok(response.body) +} + +fn wait_for_http_response(bind: &str, path: &str) -> Result { + let deadline = Instant::now() + Duration::from_secs(5); let mut last_error = String::new(); while Instant::now() < deadline { - match http_get_json(bind, path) { - Ok(value) => return Ok(value), + match http_get_response(bind, path) { + Ok(response) => return Ok(response), Err(err) => last_error = err, } std::thread::sleep(Duration::from_millis(25)); @@ -679,31 +841,65 @@ fn wait_for_http_json(bind: &str, path: &str) -> Result { Err(last_error) } -fn http_get_json(bind: &str, path: &str) -> Result { +fn http_get_response(bind: &str, path: &str) -> Result { let addr = bind .parse() .map_err(|err| format!("parse bind address {bind}: {err}"))?; let mut stream = TcpStream::connect_timeout(&addr, Duration::from_millis(100)) .map_err(|err| format!("connect to {bind}: {err}"))?; stream - .set_read_timeout(Some(Duration::from_secs(1))) + .set_read_timeout(Some(Duration::from_secs(2))) .map_err(|err| format!("set read timeout: {err}"))?; write!( stream, "GET {path} HTTP/1.1\r\nHost: {bind}\r\nConnection: close\r\n\r\n" ) .map_err(|err| format!("write request: {err}"))?; - let mut response = String::new(); - stream - .read_to_string(&mut response) - .map_err(|err| format!("read response: {err}"))?; - let (head, body) = response - .split_once("\r\n\r\n") - .ok_or_else(|| format!("malformed HTTP response: {response}"))?; - if !head.starts_with("HTTP/1.1 200") { - return Err(format!( - "unexpected HTTP response head: {head}; body: {body}" - )); + let mut reader = std::io::BufReader::new(stream); + let mut status_line = String::new(); + reader + .read_line(&mut status_line) + .map_err(|err| format!("read status line: {err}"))?; + let status_code = status_line + .split_whitespace() + .nth(1) + .ok_or_else(|| format!("malformed HTTP status line: {status_line}"))? + .parse::() + .map_err(|err| format!("parse HTTP status from {status_line:?}: {err}"))?; + let mut content_length = None; + let mut header = String::new(); + loop { + header.clear(); + reader + .read_line(&mut header) + .map_err(|err| format!("read header: {err}"))?; + if header == "\r\n" || header == "\n" || header.is_empty() { + break; + } + if let Some((name, value)) = header.split_once(':') + && name.eq_ignore_ascii_case("content-length") + { + content_length = Some( + value + .trim() + .parse::() + .map_err(|err| format!("parse content-length from {header:?}: {err}"))?, + ); + } + } + let mut body = String::new(); + if let Some(content_length) = content_length { + let mut bytes = vec![0_u8; content_length]; + reader + .read_exact(&mut bytes) + .map_err(|err| format!("read response body: {err}"))?; + body = String::from_utf8(bytes).map_err(|err| format!("response body is utf8: {err}"))?; + } else { + reader + .read_to_string(&mut body) + .map_err(|err| format!("read response body: {err}"))?; } - serde_json::from_str(body).map_err(|err| format!("parse json body {body:?}: {err}")) + let body = + serde_json::from_str(&body).map_err(|err| format!("parse json body {body:?}: {err}"))?; + Ok(HttpJsonResponse { status_code, body }) } From e212ca6992e7ed48d970fba10fad5a353a398851 Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Tue, 19 May 2026 15:45:49 +1000 Subject: [PATCH 023/187] C6 C12 fix: add HTTP instance capability handshake --- crates/clarion-cli/src/http_read.rs | 11 +- crates/clarion-cli/src/instance.rs | 104 ++++++++ crates/clarion-cli/src/main.rs | 1 + crates/clarion-cli/src/serve.rs | 12 +- crates/clarion-cli/tests/serve.rs | 228 +++++++++++++++++- .../fixtures/get-api-v1-capabilities.json | 4 +- 6 files changed, 341 insertions(+), 19 deletions(-) create mode 100644 crates/clarion-cli/src/instance.rs diff --git a/crates/clarion-cli/src/http_read.rs b/crates/clarion-cli/src/http_read.rs index ff65c45c..b4e87d76 100644 --- a/crates/clarion-cli/src/http_read.rs +++ b/crates/clarion-cli/src/http_read.rs @@ -46,11 +46,13 @@ impl HttpReadServer { struct AppState { project_root: PathBuf, readers: ReaderPool, + instance_id: String, } pub fn spawn( project_root: PathBuf, db_path: PathBuf, + instance_id: String, config: &HttpReadConfig, ) -> Result> { if !config.enabled { @@ -81,6 +83,7 @@ pub fn spawn( let state = AppState { project_root, readers, + instance_id, }; axum::serve(listener, router(state)) .with_graceful_shutdown(async { @@ -127,7 +130,8 @@ struct FileResponse { struct CapabilitiesResponse { registry_backend: bool, file_registry: bool, - version: &'static str, + api_version: u8, + instance_id: String, } #[derive(Debug, Serialize)] @@ -195,11 +199,12 @@ async fn get_file(State(state): State, Query(query): Query) } } -async fn get_capabilities() -> Json { +async fn get_capabilities(State(state): State) -> Json { Json(CapabilitiesResponse { registry_backend: true, file_registry: true, - version: "0.1", + api_version: 1, + instance_id: state.instance_id, }) } diff --git a/crates/clarion-cli/src/instance.rs b/crates/clarion-cli/src/instance.rs new file mode 100644 index 00000000..dde1df73 --- /dev/null +++ b/crates/clarion-cli/src/instance.rs @@ -0,0 +1,104 @@ +use std::fs::{self, OpenOptions}; +use std::io::{self, Write}; +use std::path::Path; + +use anyhow::{Context, Result, anyhow}; +use uuid::Uuid; + +const INSTANCE_ID_FILE: &str = "instance_id"; + +pub fn load_or_create(project_root: &Path) -> Result { + let path = project_root.join(".clarion").join(INSTANCE_ID_FILE); + match fs::read_to_string(&path) { + Ok(raw) => read_existing_instance_id(&path, &raw), + Err(err) if err.kind() == io::ErrorKind::NotFound => create_instance_id(&path), + Err(err) => Err(err).with_context(|| format!("read {}", path.display())), + } +} + +fn create_instance_id(path: &Path) -> Result { + let instance_id = Uuid::new_v4().to_string(); + let temp_path = path.with_file_name(format!(".{INSTANCE_ID_FILE}.{instance_id}.tmp")); + let mut file = create_new_private_file(&temp_path) + .with_context(|| format!("create temporary {}", temp_path.display()))?; + writeln!(file, "{instance_id}").with_context(|| format!("write {}", temp_path.display()))?; + file.sync_all() + .with_context(|| format!("sync {}", temp_path.display()))?; + drop(file); + + match fs::hard_link(&temp_path, path) { + Ok(()) => {} + Err(err) if err.kind() == io::ErrorKind::AlreadyExists => { + let _ = fs::remove_file(&temp_path); + let raw = fs::read_to_string(path) + .with_context(|| format!("read concurrently-created {}", path.display()))?; + return read_existing_instance_id(path, &raw); + } + Err(err) => { + let _ = fs::remove_file(&temp_path); + return Err(err).with_context(|| { + format!( + "publish temporary {} to {}", + temp_path.display(), + path.display() + ) + }); + } + } + fs::remove_file(&temp_path).with_context(|| format!("remove {}", temp_path.display()))?; + #[cfg(unix)] + set_private_mode(path)?; + Ok(instance_id) +} + +#[cfg(unix)] +fn create_new_private_file(path: &Path) -> io::Result { + use std::os::unix::fs::OpenOptionsExt; + + OpenOptions::new() + .write(true) + .create_new(true) + .mode(0o600) + .open(path) +} + +#[cfg(not(unix))] +fn create_new_private_file(path: &Path) -> io::Result { + OpenOptions::new().write(true).create_new(true).open(path) +} + +#[cfg(unix)] +fn set_private_mode(path: &Path) -> Result<()> { + use std::os::unix::fs::PermissionsExt; + + let mut permissions = fs::metadata(path) + .with_context(|| format!("stat {}", path.display()))? + .permissions(); + permissions.set_mode(0o600); + fs::set_permissions(path, permissions) + .with_context(|| format!("set private mode on {}", path.display())) +} + +#[cfg(not(unix))] +fn set_private_mode(_path: &Path) -> Result<()> { + Ok(()) +} + +fn read_existing_instance_id(path: &Path, raw: &str) -> Result { + let instance_id = parse_instance_id(path, raw)?; + set_private_mode(path)?; + Ok(instance_id) +} + +fn parse_instance_id(path: &Path, raw: &str) -> Result { + let trimmed = raw.trim(); + let id = Uuid::parse_str(trimmed).map_err(|err| invalid_instance_id(path, err))?; + Ok(id.to_string()) +} + +fn invalid_instance_id(path: &Path, source: uuid::Error) -> anyhow::Error { + anyhow!( + "invalid Clarion instance ID in {}: {source}; expected a UUID", + path.display() + ) +} diff --git a/crates/clarion-cli/src/main.rs b/crates/clarion-cli/src/main.rs index 3424cb5d..1f5999ec 100644 --- a/crates/clarion-cli/src/main.rs +++ b/crates/clarion-cli/src/main.rs @@ -4,6 +4,7 @@ mod clustering; mod config; mod http_read; mod install; +mod instance; mod run_lifecycle; mod secret_scan; mod serve; diff --git a/crates/clarion-cli/src/serve.rs b/crates/clarion-cli/src/serve.rs index ab9544f8..76410bfd 100644 --- a/crates/clarion-cli/src/serve.rs +++ b/crates/clarion-cli/src/serve.rs @@ -24,6 +24,8 @@ pub fn run(path: &Path, config_path: Option<&Path>) -> Result<()> { let project_root = path .canonicalize() .with_context(|| format!("canonicalize project path {}", path.display()))?; + let instance_id = crate::instance::load_or_create(&project_root) + .context("load Clarion project instance ID")?; let default_config_path = path.join("clarion.yaml"); let config_path = config_path.unwrap_or(&default_config_path); let config = if config_path.exists() { @@ -52,9 +54,13 @@ pub fn run(path: &Path, config_path: Option<&Path>) -> Result<()> { .map_err(|err| anyhow!("open reader pool for {}: {err}", db_path.display()))?; let http_project_root = project_root.clone(); let mut state = clarion_mcp::ServerState::new(project_root, readers); - let http_server = - crate::http_read::spawn(http_project_root, db_path.clone(), &config.serve.http) - .context("start HTTP read API")?; + let http_server = crate::http_read::spawn( + http_project_root, + db_path.clone(), + instance_id, + &config.serve.http, + ) + .context("start HTTP read API")?; let mut llm_writer = None; let mut llm_writer_join = None; if let Some(provider) = llm_provider { diff --git a/crates/clarion-cli/tests/serve.rs b/crates/clarion-cli/tests/serve.rs index f05f2e63..4deb3aa8 100644 --- a/crates/clarion-cli/tests/serve.rs +++ b/crates/clarion-cli/tests/serve.rs @@ -13,6 +13,7 @@ use clarion_core::{ }; use rusqlite::{Connection, params}; use serde_json::Value; +use uuid::Uuid; #[derive(Debug)] struct HttpJsonResponse { @@ -275,6 +276,11 @@ fn serve_http_capabilities_and_mcp_stdio_coexist() { .env("PATH", "") .assert() .success(); + fs::write( + dir.path().join(".clarion/instance_id"), + "9bd7234e-6d44-4a38-9ae4-76f912a10221\n", + ) + .expect("seed stable instance ID"); let bind = free_loopback_bind(); write_http_config(dir.path(), &bind); @@ -283,7 +289,13 @@ fn serve_http_capabilities_and_mcp_stdio_coexist() { .expect("HTTP /api/v1/_capabilities response"); assert_eq!(capabilities["registry_backend"], true); - assert_eq!(capabilities["version"], "0.1"); + assert_eq!(capabilities["file_registry"], true); + assert_eq!(capabilities["api_version"], 1); + assert!(capabilities.get("version").is_none()); + let instance_id = capabilities["instance_id"] + .as_str() + .expect("instance_id is a string"); + Uuid::parse_str(instance_id).expect("instance_id is a UUID"); let fixture: Value = serde_json::from_str(include_str!( "../../../docs/federation/fixtures/get-api-v1-capabilities.json" )) @@ -329,6 +341,139 @@ fn serve_http_capabilities_and_mcp_stdio_coexist() { assert_eq!(response["result"]["serverInfo"]["name"], "clarion"); } +#[test] +fn serve_http_capabilities_reuses_persisted_instance_id_across_restarts() { + let dir = tempfile::tempdir().expect("temp project"); + clarion_bin() + .args(["install", "--path"]) + .arg(dir.path()) + .env("PATH", "") + .assert() + .success(); + let instance_id_path = dir.path().join(".clarion/instance_id"); + + let first_bind = free_loopback_bind(); + write_http_config(dir.path(), &first_bind); + let mut first_child = spawn_serve(dir.path()); + let first = wait_for_http_json(&first_bind, "/api/v1/_capabilities") + .expect("first capabilities response"); + stop_serve(&mut first_child); + let first_instance_id = first["instance_id"] + .as_str() + .expect("first instance_id") + .to_owned(); + assert_eq!( + fs::read_to_string(&instance_id_path) + .expect("read first persisted instance_id") + .trim(), + first_instance_id + ); + + let second_bind = free_loopback_bind(); + write_http_config(dir.path(), &second_bind); + let mut second_child = spawn_serve(dir.path()); + let second = wait_for_http_json(&second_bind, "/api/v1/_capabilities") + .expect("second capabilities response"); + stop_serve(&mut second_child); + + assert_eq!(second["instance_id"], first["instance_id"]); + assert_eq!( + fs::read_to_string(&instance_id_path) + .expect("read second persisted instance_id") + .trim(), + first_instance_id + ); +} + +#[test] +fn serve_http_capabilities_creates_instance_id_with_private_unix_mode() { + let dir = tempfile::tempdir().expect("temp project"); + clarion_bin() + .args(["install", "--path"]) + .arg(dir.path()) + .env("PATH", "") + .assert() + .success(); + let bind = free_loopback_bind(); + write_http_config(dir.path(), &bind); + + let mut child = spawn_serve(dir.path()); + let capabilities = wait_for_http_json(&bind, "/api/v1/_capabilities") + .expect("HTTP /api/v1/_capabilities response"); + stop_serve(&mut child); + + let instance_id_path = dir.path().join(".clarion/instance_id"); + assert_eq!( + fs::read_to_string(&instance_id_path) + .expect("read persisted instance_id") + .trim(), + capabilities["instance_id"].as_str().expect("instance_id") + ); + let mode = fs::metadata(instance_id_path) + .expect("instance_id metadata") + .permissions() + .mode() + & 0o777; + assert_eq!(mode, 0o600); +} + +#[test] +fn serve_http_capabilities_repairs_existing_instance_id_mode() { + let dir = tempfile::tempdir().expect("temp project"); + clarion_bin() + .args(["install", "--path"]) + .arg(dir.path()) + .env("PATH", "") + .assert() + .success(); + let instance_id_path = dir.path().join(".clarion/instance_id"); + let seeded_id = "9bd7234e-6d44-4a38-9ae4-76f912a10221"; + fs::write(&instance_id_path, format!("{seeded_id}\n")).expect("seed instance ID"); + fs::set_permissions(&instance_id_path, fs::Permissions::from_mode(0o644)) + .expect("seed permissive instance ID mode"); + let bind = free_loopback_bind(); + write_http_config(dir.path(), &bind); + + let mut child = spawn_serve(dir.path()); + let capabilities = wait_for_http_json(&bind, "/api/v1/_capabilities"); + stop_serve(&mut child); + let capabilities = capabilities.expect("HTTP /api/v1/_capabilities response"); + + assert_eq!(capabilities["instance_id"], seeded_id); + let mode = fs::metadata(instance_id_path) + .expect("instance_id metadata") + .permissions() + .mode() + & 0o777; + assert_eq!(mode, 0o600); +} + +#[test] +fn serve_rejects_invalid_instance_id_before_serving_http() { + let dir = tempfile::tempdir().expect("temp project"); + clarion_bin() + .args(["install", "--path"]) + .arg(dir.path()) + .env("PATH", "") + .assert() + .success(); + fs::write(dir.path().join(".clarion/instance_id"), "not-a-uuid\n") + .expect("write invalid instance ID"); + let bind = free_loopback_bind(); + write_http_config(dir.path(), &bind); + + let child = spawn_serve(dir.path()); + let output = wait_for_child_exit(child, Duration::from_secs(2)) + .expect("serve should fail before accepting HTTP requests"); + + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("invalid Clarion instance ID"), + "unexpected stderr: {stderr}" + ); +} + #[test] fn serve_rejects_invalid_project_config() { let dir = tempfile::tempdir().expect("temp project"); @@ -797,18 +942,67 @@ fn write_http_config(project_root: &Path, bind: &str) { .expect("write HTTP serve config"); } -fn spawn_serve(project_root: &Path) -> Child { - StdCommand::new(assert_cmd::cargo::cargo_bin("clarion")) - .args(["serve", "--path"]) - .arg(project_root) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - .expect("spawn clarion serve") +struct ServeChild { + child: Option, } -fn stop_serve(child: &mut Child) { +impl ServeChild { + fn new(child: Child) -> Self { + Self { child: Some(child) } + } + + fn stop(&mut self) { + if let Some(mut child) = self.child.take() { + stop_child(&mut child); + } + } + + fn wait_with_output(mut self) -> std::io::Result { + self.child + .take() + .expect("serve child was already stopped") + .wait_with_output() + } +} + +impl std::ops::Deref for ServeChild { + type Target = Child; + + fn deref(&self) -> &Self::Target { + self.child.as_ref().expect("serve child was stopped") + } +} + +impl std::ops::DerefMut for ServeChild { + fn deref_mut(&mut self) -> &mut Self::Target { + self.child.as_mut().expect("serve child was stopped") + } +} + +impl Drop for ServeChild { + fn drop(&mut self) { + self.stop(); + } +} + +fn spawn_serve(project_root: &Path) -> ServeChild { + ServeChild::new( + StdCommand::new(assert_cmd::cargo::cargo_bin("clarion")) + .args(["serve", "--path"]) + .arg(project_root) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("spawn clarion serve"), + ) +} + +fn stop_serve(child: &mut ServeChild) { + child.stop(); +} + +fn stop_child(child: &mut Child) { drop(child.stdin.take()); if let Ok(Some(_)) = child.try_wait() { return; @@ -817,6 +1011,18 @@ fn stop_serve(child: &mut Child) { let _ = child.wait(); } +fn wait_for_child_exit(mut child: ServeChild, timeout: Duration) -> Option { + let deadline = Instant::now() + timeout; + while Instant::now() < deadline { + if child.try_wait().expect("poll child").is_some() { + return Some(child.wait_with_output().expect("collect child output")); + } + std::thread::sleep(Duration::from_millis(25)); + } + child.stop(); + None +} + fn wait_for_http_json(bind: &str, path: &str) -> Result { let response = wait_for_http_response(bind, path)?; if response.status_code != 200 { diff --git a/docs/federation/fixtures/get-api-v1-capabilities.json b/docs/federation/fixtures/get-api-v1-capabilities.json index e04aa3d7..4e679be5 100644 --- a/docs/federation/fixtures/get-api-v1-capabilities.json +++ b/docs/federation/fixtures/get-api-v1-capabilities.json @@ -6,7 +6,7 @@ "response": { "registry_backend": true, "file_registry": true, - "version": "0.1" + "api_version": 1, + "instance_id": "9bd7234e-6d44-4a38-9ae4-76f912a10221" } } - From 264c49c2b5d9b86d05cfacc9d2e6e40d1ee1de2a Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Tue, 19 May 2026 16:01:47 +1000 Subject: [PATCH 024/187] C3 test: pin HTTP read API federation fixtures --- crates/clarion-cli/tests/serve.rs | 352 +++++++++++++++++- docs/federation/contracts.md | 13 +- .../fixtures/get-api-v1-capabilities.json | 65 +++- .../get-api-v1-files.demo-python.json | 154 +++++++- 4 files changed, 555 insertions(+), 29 deletions(-) diff --git a/crates/clarion-cli/tests/serve.rs b/crates/clarion-cli/tests/serve.rs index 4deb3aa8..13a34272 100644 --- a/crates/clarion-cli/tests/serve.rs +++ b/crates/clarion-cli/tests/serve.rs @@ -15,6 +15,8 @@ use rusqlite::{Connection, params}; use serde_json::Value; use uuid::Uuid; +const STABLE_INSTANCE_ID: &str = "9bd7234e-6d44-4a38-9ae4-76f912a10221"; + #[derive(Debug)] struct HttpJsonResponse { status_code: u16, @@ -93,6 +95,39 @@ fn serve_stdio_initialize_round_trip() { assert_eq!(response["result"]["serverInfo"]["name"], "clarion"); } +#[test] +fn serve_http_responses_match_federation_fixture_contracts() { + let files_fixture = load_contract_fixture( + "get-api-v1-files.demo-python.json", + include_str!("../../../docs/federation/fixtures/get-api-v1-files.demo-python.json"), + ); + let capabilities_fixture = load_contract_fixture( + "get-api-v1-capabilities.json", + include_str!("../../../docs/federation/fixtures/get-api-v1-capabilities.json"), + ); + let dir = tempfile::tempdir().expect("temp project"); + clarion_bin() + .args(["install", "--path"]) + .arg(dir.path()) + .env("PATH", "") + .assert() + .success(); + fs::write( + dir.path().join(".clarion/instance_id"), + format!("{STABLE_INSTANCE_ID}\n"), + ) + .expect("seed stable instance ID"); + seed_file_entity(dir.path()); + seed_storage_failure_file_entity(dir.path()); + let bind = free_loopback_bind(); + write_http_config(dir.path(), &bind); + + let mut child = spawn_serve(dir.path()); + validate_fixture_examples(&bind, &files_fixture, "get-api-v1-files.demo-python.json"); + validate_fixture_examples(&bind, &capabilities_fixture, "get-api-v1-capabilities.json"); + stop_serve(&mut child); +} + #[test] fn serve_http_files_endpoint_resolves_known_file_on_configured_port() { let dir = tempfile::tempdir().expect("temp project"); @@ -119,7 +154,7 @@ fn serve_http_files_endpoint_resolves_known_file_on_configured_port() { assert_eq!(response["content_hash"], content_hash); assert_eq!(response["canonical_path"], canonical_path); assert_eq!(response["language"], "python"); - assert_eq!(response, fixture["response"]); + assert_eq!(&response, fixture_example_body(&fixture, "happy_path_200")); } #[test] @@ -278,7 +313,7 @@ fn serve_http_capabilities_and_mcp_stdio_coexist() { .success(); fs::write( dir.path().join(".clarion/instance_id"), - "9bd7234e-6d44-4a38-9ae4-76f912a10221\n", + format!("{STABLE_INSTANCE_ID}\n"), ) .expect("seed stable instance ID"); let bind = free_loopback_bind(); @@ -300,7 +335,10 @@ fn serve_http_capabilities_and_mcp_stdio_coexist() { "../../../docs/federation/fixtures/get-api-v1-capabilities.json" )) .expect("parse capabilities fixture"); - assert_eq!(capabilities, fixture["response"]); + assert_eq!( + &capabilities, + fixture_example_body(&fixture, "capabilities_200") + ); { let stdin = child.stdin.as_mut().expect("child stdin"); @@ -902,6 +940,285 @@ fn call_summary_through_serve(project_root: &Path) -> Value { serde_json::from_str(tool_text).expect("tool envelope") } +fn load_contract_fixture(fixture_name: &str, source: &str) -> Value { + let fixture: Value = serde_json::from_str(source).expect("parse contract fixture"); + assert!( + fixture.get("_meta").and_then(Value::as_object).is_some(), + "{fixture_name} missing top-level _meta object" + ); + assert!( + fixture + .pointer("/shape_decl/shapes") + .and_then(Value::as_object) + .is_some(), + "{fixture_name} missing top-level shape_decl.shapes object" + ); + let examples = fixture + .get("examples") + .and_then(Value::as_array) + .unwrap_or_else(|| panic!("{fixture_name} missing top-level examples array")); + assert!( + !examples.is_empty(), + "{fixture_name} must declare at least one example" + ); + fixture +} + +fn fixture_example_body<'a>(fixture: &'a Value, example_name: &str) -> &'a Value { + let examples = fixture + .get("examples") + .and_then(Value::as_array) + .expect("examples array"); + examples + .iter() + .find(|example| example.get("name").and_then(Value::as_str) == Some(example_name)) + .and_then(|example| example.pointer("/response/body")) + .unwrap_or_else(|| panic!("missing fixture example body {example_name}")) +} + +fn validate_fixture_examples(bind: &str, fixture: &Value, fixture_name: &str) { + let shapes = fixture + .pointer("/shape_decl/shapes") + .and_then(Value::as_object) + .expect("shape_decl.shapes object"); + let examples = fixture + .get("examples") + .and_then(Value::as_array) + .expect("examples array"); + for example in examples { + let example_name = example + .get("name") + .and_then(Value::as_str) + .expect("example name"); + let method = example + .pointer("/request/method") + .and_then(Value::as_str) + .unwrap_or_else(|| panic!("{fixture_name}:{example_name} missing request.method")); + assert_eq!(method, "GET", "{fixture_name}:{example_name} method"); + let path = example + .pointer("/request/path") + .and_then(Value::as_str) + .unwrap_or_else(|| panic!("{fixture_name}:{example_name} missing request.path")); + let expected_status = example + .pointer("/response/status") + .and_then(Value::as_u64) + .unwrap_or_else(|| panic!("{fixture_name}:{example_name} missing response.status")); + let expected_body = example + .pointer("/response/body") + .unwrap_or_else(|| panic!("{fixture_name}:{example_name} missing response.body")); + let shape_name = example + .pointer("/response/shape") + .and_then(Value::as_str) + .unwrap_or_else(|| panic!("{fixture_name}:{example_name} missing response.shape")); + + let response = wait_for_http_response(bind, path).unwrap_or_else(|err| { + panic!("{fixture_name}:{example_name} HTTP request failed: {err}") + }); + + assert_eq!( + u64::from(response.status_code), + expected_status, + "{fixture_name}:{example_name} status mismatch" + ); + let shape = shapes + .get(shape_name) + .and_then(Value::as_object) + .unwrap_or_else(|| panic!("{fixture_name}:{example_name} missing shape {shape_name}")); + assert_status_allowed(shape, response.status_code, fixture_name, example_name); + assert_body_matches_shape( + shape, + &response.body, + fixture_name, + example_name, + shape_name, + ); + assert_normative_example_fields( + &response.body, + expected_body, + shape_name, + fixture_name, + example_name, + ); + } +} + +fn assert_normative_example_fields( + actual: &Value, + expected: &Value, + shape_name: &str, + fixture_name: &str, + example_name: &str, +) { + if shape_name == "error_envelope" { + assert_eq!( + actual.get("code"), + expected.get("code"), + "{fixture_name}:{example_name} error code mismatch" + ); + return; + } + assert_eq!( + actual, expected, + "{fixture_name}:{example_name} body mismatch" + ); +} + +fn assert_status_allowed( + shape: &serde_json::Map, + status_code: u16, + fixture_name: &str, + example_name: &str, +) { + if let Some(status) = shape.get("status").and_then(Value::as_u64) { + assert_eq!( + status, + u64::from(status_code), + "{fixture_name}:{example_name} status is not allowed by shape" + ); + return; + } + let allowed = shape + .get("status_any") + .and_then(Value::as_array) + .unwrap_or_else(|| panic!("{fixture_name}:{example_name} shape missing status/status_any")); + assert!( + allowed + .iter() + .any(|candidate| candidate.as_u64() == Some(u64::from(status_code))), + "{fixture_name}:{example_name} status {status_code} is not in status_any {allowed:?}" + ); +} + +fn assert_body_matches_shape( + shape: &serde_json::Map, + body: &Value, + fixture_name: &str, + example_name: &str, + shape_name: &str, +) { + let body = body + .as_object() + .unwrap_or_else(|| panic!("{fixture_name}:{example_name} body is not an object")); + let required_fields = shape + .get("required_fields") + .and_then(Value::as_object) + .unwrap_or_else(|| { + panic!("{fixture_name}:{example_name} shape {shape_name} missing required_fields") + }); + for (field, field_decl) in required_fields { + let value = body + .get(field) + .unwrap_or_else(|| panic!("{fixture_name}:{example_name} missing field {field}")); + assert_value_matches_decl(value, field_decl, fixture_name, example_name, field); + } + if let Some(forbidden_fields) = shape.get("forbidden_fields").and_then(Value::as_array) { + for field in forbidden_fields { + let field = field.as_str().unwrap_or_else(|| { + panic!("{fixture_name}:{example_name} forbidden field entry is not a string") + }); + assert!( + !body.contains_key(field), + "{fixture_name}:{example_name} field {field} is forbidden by {shape_name}" + ); + } + } + let allow_extra_fields = shape + .get("allow_extra_fields") + .and_then(Value::as_bool) + .unwrap_or(false); + if !allow_extra_fields { + for field in body.keys() { + assert!( + required_fields.contains_key(field), + "{fixture_name}:{example_name} unexpected field {field}" + ); + } + } +} + +fn assert_value_matches_decl( + value: &Value, + field_decl: &Value, + fixture_name: &str, + example_name: &str, + field: &str, +) { + if let Some(expected) = field_decl.get("const") { + assert_eq!( + value, expected, + "{fixture_name}:{example_name} field {field} const mismatch" + ); + } + if let Some(allowed) = field_decl.get("enum").and_then(Value::as_array) { + assert!( + allowed.iter().any(|candidate| candidate == value), + "{fixture_name}:{example_name} field {field} value {value:?} not in {allowed:?}" + ); + } + let type_name = field_decl + .get("type") + .and_then(Value::as_str) + .unwrap_or_else(|| panic!("{fixture_name}:{example_name} field {field} missing type")); + match type_name { + "boolean" => { + assert!( + value.as_bool().is_some(), + "{fixture_name}:{example_name} field {field} is not a boolean" + ); + } + "integer" => { + assert!( + value.as_i64().is_some() || value.as_u64().is_some(), + "{fixture_name}:{example_name} field {field} is not an integer" + ); + } + "non_empty_string" => { + let value = value.as_str().unwrap_or_else(|| { + panic!("{fixture_name}:{example_name} field {field} is not a string") + }); + assert!( + !value.is_empty(), + "{fixture_name}:{example_name} field {field} is empty" + ); + } + "uuid" => { + let value = value.as_str().unwrap_or_else(|| { + panic!("{fixture_name}:{example_name} field {field} is not a string") + }); + Uuid::parse_str(value) + .unwrap_or_else(|err| panic!("{fixture_name}:{example_name} invalid UUID: {err}")); + } + "adr003_file_entity_id" => { + let value = value.as_str().unwrap_or_else(|| { + panic!("{fixture_name}:{example_name} field {field} is not a string") + }); + assert!( + value + .strip_prefix("core:file:") + .is_some_and(|qualified_name| { + !qualified_name.is_empty() + && !qualified_name.contains('@') + && !qualified_name.contains('\\') + }), + "{fixture_name}:{example_name} field {field} is not an ADR-003 file ID" + ); + } + "project_relative_path" => { + let value = value.as_str().unwrap_or_else(|| { + panic!("{fixture_name}:{example_name} field {field} is not a string") + }); + assert!( + !value.is_empty() + && !value.starts_with('/') + && !value.starts_with("./") + && !value.contains('\\'), + "{fixture_name}:{example_name} field {field} is not a project-relative path" + ); + } + other => panic!("{fixture_name}:{example_name} unknown field type {other} for {field}"), + } +} + fn seed_file_entity(project_root: &Path) -> (String, String, String) { let source_path = project_root.join("demo.py"); fs::write(&source_path, "def entry():\n return 1\n").expect("write source"); @@ -911,7 +1228,7 @@ fn seed_file_entity(project_root: &Path) -> (String, String, String) { .display() .to_string(); let content_hash = "hash-demo-file".to_owned(); - let file_id = "core:file:hash-demo@demo.py".to_owned(); + let file_id = "core:file:demo.py".to_owned(); let db_path = project_root.join(".clarion/clarion.db"); let conn = Connection::open(&db_path).expect("open sqlite"); conn.execute( @@ -929,6 +1246,33 @@ fn seed_file_entity(project_root: &Path) -> (String, String, String) { (file_id, content_hash, "demo.py".to_owned()) } +fn seed_storage_failure_file_entity(project_root: &Path) { + let source_path = project_root.join("missing-on-disk.py"); + fs::write(&source_path, "def missing():\n return 1\n").expect("write source"); + let canonical_path = source_path + .canonicalize() + .expect("canonical source path") + .display() + .to_string(); + let db_path = project_root.join(".clarion/clarion.db"); + let conn = Connection::open(&db_path).expect("open sqlite"); + conn.execute( + "INSERT INTO entities ( + id, plugin_id, kind, name, short_name, source_file_path, + source_line_start, source_line_end, properties, created_at, updated_at + ) VALUES ( + 'core:file:missing-on-disk.py', 'core', 'file', + 'missing-on-disk.py', 'missing-on-disk.py', ?1, + 1, 2, '{}', + '2026-05-19T00:00:00.000Z', '2026-05-19T00:00:00.000Z' + )", + params![canonical_path], + ) + .expect("insert file entity without cached hash"); + drop(conn); + fs::remove_file(&source_path).expect("remove cataloged file to force storage failure"); +} + fn free_loopback_bind() -> String { let listener = TcpListener::bind("127.0.0.1:0").expect("bind free loopback port"); listener.local_addr().expect("local addr").to_string() diff --git a/docs/federation/contracts.md b/docs/federation/contracts.md index 8aec208d..b192dbc7 100644 --- a/docs/federation/contracts.md +++ b/docs/federation/contracts.md @@ -67,10 +67,11 @@ Semantics: - If no file-kind entity row exists for the path, Clarion returns `404` with `code: "NOT_FOUND"` instead of synthesizing a file ID. -The RC1 fixture at +The contract fixture at [`fixtures/get-api-v1-files.demo-python.json`](./fixtures/get-api-v1-files.demo-python.json) -still captures the pre-hardening response shape. It is intentionally not the -normative contract for this section until the C3 fixture gate upgrades it. +is normative for this section. It includes `_meta`, `shape_decl`, and examples +for the happy path, not-known, blank path, outside-root, and storage-error +responses. ### `GET /api/v1/_capabilities` @@ -100,7 +101,7 @@ in `.clarion/instance_id`. Filigree should treat a changed `instance_id` for a previously known endpoint as evidence that it is now talking to a different Clarion project instance. -The RC1 fixture at +The contract fixture at [`fixtures/get-api-v1-capabilities.json`](./fixtures/get-api-v1-capabilities.json) -still captures the pre-hardening response shape. It is intentionally not the -normative contract for this section until the C3 fixture gate upgrades it. +is normative for this section. Its shape declaration pins `api_version` and +asserts that `instance_id` is a UUID; the example uses a seeded stable ID. diff --git a/docs/federation/fixtures/get-api-v1-capabilities.json b/docs/federation/fixtures/get-api-v1-capabilities.json index 4e679be5..4af770ac 100644 --- a/docs/federation/fixtures/get-api-v1-capabilities.json +++ b/docs/federation/fixtures/get-api-v1-capabilities.json @@ -1,12 +1,59 @@ { - "request": { - "method": "GET", - "path": "/api/v1/_capabilities" - }, - "response": { - "registry_backend": true, - "file_registry": true, + "_meta": { + "contract": "clarion-http-read-api", + "endpoint": "GET /api/v1/_capabilities", "api_version": 1, - "instance_id": "9bd7234e-6d44-4a38-9ae4-76f912a10221" - } + "fixture_version": 2, + "normative": true, + "updated": "2026-05-19", + "description": "Contract example for Clarion registry-backend capability discovery." + }, + "shape_decl": { + "kind": "clarion-http-fixture-shapes", + "shapes": { + "capabilities_response": { + "status": 200, + "required_fields": { + "registry_backend": { + "type": "boolean", + "const": true + }, + "file_registry": { + "type": "boolean", + "const": true + }, + "api_version": { + "type": "integer", + "const": 1 + }, + "instance_id": { + "type": "uuid" + } + }, + "forbidden_fields": [ + "version" + ], + "allow_extra_fields": false + } + } + }, + "examples": [ + { + "name": "capabilities_200", + "request": { + "method": "GET", + "path": "/api/v1/_capabilities" + }, + "response": { + "status": 200, + "shape": "capabilities_response", + "body": { + "registry_backend": true, + "file_registry": true, + "api_version": 1, + "instance_id": "9bd7234e-6d44-4a38-9ae4-76f912a10221" + } + } + } + ] } diff --git a/docs/federation/fixtures/get-api-v1-files.demo-python.json b/docs/federation/fixtures/get-api-v1-files.demo-python.json index 373075c7..f6b34e31 100644 --- a/docs/federation/fixtures/get-api-v1-files.demo-python.json +++ b/docs/federation/fixtures/get-api-v1-files.demo-python.json @@ -1,13 +1,147 @@ { - "request": { - "method": "GET", - "path": "/api/v1/files?path=demo.py&language=python" + "_meta": { + "contract": "clarion-http-read-api", + "endpoint": "GET /api/v1/files", + "api_version": 1, + "fixture_version": 2, + "normative": true, + "updated": "2026-05-19", + "description": "Contract examples for Clarion file identity resolution consumed by Filigree registry backends." }, - "response": { - "entity_id": "core:file:hash-demo@demo.py", - "content_hash": "hash-demo-file", - "canonical_path": "demo.py", - "language": "python" - } + "shape_decl": { + "kind": "clarion-http-fixture-shapes", + "shapes": { + "file_resolution": { + "status": 200, + "required_fields": { + "entity_id": { + "type": "adr003_file_entity_id" + }, + "content_hash": { + "type": "non_empty_string" + }, + "canonical_path": { + "type": "project_relative_path" + }, + "language": { + "type": "non_empty_string" + } + }, + "forbidden_fields": [ + "error", + "code" + ], + "allow_extra_fields": false + }, + "error_envelope": { + "status_any": [ + 400, + 404, + 500, + 503 + ], + "required_fields": { + "error": { + "type": "non_empty_string" + }, + "code": { + "type": "non_empty_string", + "enum": [ + "INVALID_PATH", + "PATH_OUTSIDE_PROJECT", + "NOT_FOUND", + "STORAGE_ERROR", + "INTERNAL" + ] + } + }, + "forbidden_fields": [ + "entity_id", + "content_hash", + "canonical_path", + "language" + ], + "allow_extra_fields": false + } + } + }, + "examples": [ + { + "name": "happy_path_200", + "request": { + "method": "GET", + "path": "/api/v1/files?path=demo.py&language=python" + }, + "response": { + "status": 200, + "shape": "file_resolution", + "body": { + "entity_id": "core:file:demo.py", + "content_hash": "hash-demo-file", + "canonical_path": "demo.py", + "language": "python" + } + } + }, + { + "name": "not_known_404", + "request": { + "method": "GET", + "path": "/api/v1/files?path=missing.py&language=python" + }, + "response": { + "status": 404, + "shape": "error_envelope", + "body": { + "error": "file is not known to Clarion", + "code": "NOT_FOUND" + } + } + }, + { + "name": "blank_path_400", + "request": { + "method": "GET", + "path": "/api/v1/files?path=&language=python" + }, + "response": { + "status": 400, + "shape": "error_envelope", + "body": { + "error": "path query parameter must not be blank", + "code": "INVALID_PATH" + } + } + }, + { + "name": "outside_root_400", + "request": { + "method": "GET", + "path": "/api/v1/files?path=../outside.py&language=python" + }, + "response": { + "status": 400, + "shape": "error_envelope", + "body": { + "error": "path is outside project root", + "code": "PATH_OUTSIDE_PROJECT" + } + } + }, + { + "name": "storage_error_500", + "request": { + "method": "GET", + "path": "/api/v1/files?path=missing-on-disk.py&language=python" + }, + "response": { + "status": 500, + "shape": "error_envelope", + "body": { + "error": "file lookup failed", + "code": "STORAGE_ERROR" + } + } + } + ] } - From 682b76adc5f0747f79dc20aa14f3cc12d1f68753 Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Tue, 19 May 2026 16:14:11 +1000 Subject: [PATCH 025/187] C4 fix: enforce HTTP read API loopback trust model --- crates/clarion-cli/src/http_read.rs | 14 +++- crates/clarion-cli/tests/serve.rs | 31 +++++++ crates/clarion-mcp/src/config.rs | 120 ++++++++++++++++++++++++++++ 3 files changed, 164 insertions(+), 1 deletion(-) diff --git a/crates/clarion-cli/src/http_read.rs b/crates/clarion-cli/src/http_read.rs index b4e87d76..f017e73d 100644 --- a/crates/clarion-cli/src/http_read.rs +++ b/crates/clarion-cli/src/http_read.rs @@ -58,7 +58,11 @@ pub fn spawn( if !config.enabled { return Ok(None); } + config + .validate_loopback_trust() + .context("validate HTTP read API trust model")?; let bind = config.bind; + let warn_unauthenticated_non_loopback = config.allow_non_loopback && !config.is_loopback_bind(); let (shutdown_tx, shutdown_rx) = oneshot::channel(); let (ready_tx, ready_rx) = std::sync::mpsc::channel(); let join = thread::spawn(move || -> Result<()> { @@ -96,7 +100,15 @@ pub fn spawn( let local_addr = ready_rx .recv() .context("wait for HTTP read API bind result")??; - tracing::info!(bind = %local_addr, "Clarion HTTP read API listening"); + let auth = "none"; + if warn_unauthenticated_non_loopback { + tracing::warn!( + bind = %local_addr, + auth = %auth, + "Clarion HTTP read API listening on non-loopback interface without authentication" + ); + } + tracing::info!(bind = %local_addr, auth = %auth, "Clarion HTTP read API listening"); Ok(Some(HttpReadServer { shutdown: Some(shutdown_tx), join: Some(join), diff --git a/crates/clarion-cli/tests/serve.rs b/crates/clarion-cli/tests/serve.rs index 13a34272..0819df55 100644 --- a/crates/clarion-cli/tests/serve.rs +++ b/crates/clarion-cli/tests/serve.rs @@ -512,6 +512,37 @@ fn serve_rejects_invalid_instance_id_before_serving_http() { ); } +#[test] +fn serve_rejects_non_loopback_http_bind_before_binding_without_opt_in() { + let dir = tempfile::tempdir().expect("temp project"); + clarion_bin() + .args(["install", "--path"]) + .arg(dir.path()) + .env("PATH", "") + .assert() + .success(); + fs::write( + dir.path().join("clarion.yaml"), + "version: 1\nserve:\n http:\n enabled: true\n bind: \"0.0.0.0:0\"\n", + ) + .expect("write non-loopback HTTP config"); + + let child = spawn_serve(dir.path()); + let output = wait_for_child_exit(child, Duration::from_secs(2)) + .expect("serve should reject non-loopback HTTP bind before binding"); + + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("unauthenticated non-loopback"), + "error should explain non-loopback unauthenticated risk: {stderr}" + ); + assert!( + stderr.contains("allow_non_loopback"), + "error should name the explicit opt-in: {stderr}" + ); +} + #[test] fn serve_rejects_invalid_project_config() { let dir = tempfile::tempdir().expect("temp project"); diff --git a/crates/clarion-mcp/src/config.rs b/crates/clarion-mcp/src/config.rs index 3deb1e33..d7a30353 100644 --- a/crates/clarion-mcp/src/config.rs +++ b/crates/clarion-mcp/src/config.rs @@ -47,6 +47,7 @@ impl McpConfig { code: "CLA-CONFIG-FILIGREE-ACTOR-BLANK", }); } + self.serve.http.validate_loopback_trust()?; Ok(()) } } @@ -245,6 +246,7 @@ pub struct HttpReadConfig { pub enabled: bool, #[serde(deserialize_with = "deserialize_socket_addr")] pub bind: SocketAddr, + pub allow_non_loopback: bool, } impl Default for HttpReadConfig { @@ -252,10 +254,28 @@ impl Default for HttpReadConfig { Self { enabled: false, bind: SocketAddr::from(([127, 0, 0, 1], 9111)), + allow_non_loopback: false, } } } +impl HttpReadConfig { + pub fn validate_loopback_trust(&self) -> Result<(), ConfigError> { + if self.enabled && !self.allow_non_loopback && !self.is_loopback_bind() { + return Err(ConfigError::NonLoopbackHttpBind { + code: "CLA-CONFIG-HTTP-NON-LOOPBACK", + bind: self.bind, + }); + } + Ok(()) + } + + #[must_use] + pub fn is_loopback_bind(&self) -> bool { + self.bind.ip().is_loopback() + } +} + fn deserialize_socket_addr<'de, D>(deserializer: D) -> Result where D: serde::Deserializer<'de>, @@ -370,6 +390,15 @@ pub enum ConfigError { #[error("{code}: integrations.filigree.actor must not be blank when Filigree is enabled")] InvalidFiligreeActor { code: &'static str }, + #[error( + "{code}: serve.http.bind {bind} exposes the unauthenticated non-loopback Clarion HTTP read API; \ + bind to loopback (127.0.0.1 or ::1) or set serve.http.allow_non_loopback: true only on a trusted network" + )] + NonLoopbackHttpBind { + code: &'static str, + bind: SocketAddr, + }, + #[error( "{code}: clarion.yaml contains both `llm` and `llm_policy` top-level keys; \ `llm_policy` is a serde alias for `llm` and serde silently discards one. \ @@ -683,6 +712,97 @@ serve: assert_eq!(cfg.serve.http.bind, SocketAddr::from(([127, 0, 0, 1], 0))); } + #[test] + fn http_allow_non_loopback_defaults_false() { + assert!(!McpConfig::default().serve.http.allow_non_loopback); + } + + #[test] + fn http_allow_non_loopback_is_parsed_when_config_loads() { + let cfg = McpConfig::from_yaml_str( + r#" +serve: + http: + enabled: true + bind: "127.0.0.1:0" + allow_non_loopback: true +"#, + ) + .expect("parse HTTP allow_non_loopback"); + + assert!(cfg.serve.http.allow_non_loopback); + } + + #[test] + fn enabled_non_loopback_http_bind_requires_allow_non_loopback() { + let err = McpConfig::from_yaml_str( + r#" +serve: + http: + enabled: true + bind: "0.0.0.0:0" +"#, + ) + .expect_err("enabled wildcard HTTP bind should require explicit opt-in"); + + let message = err.to_string(); + assert!( + message.contains("unauthenticated non-loopback"), + "error should explain the unauthenticated non-loopback risk: {message}" + ); + assert!( + message.contains("allow_non_loopback"), + "error should name the explicit opt-in: {message}" + ); + } + + #[test] + fn enabled_lan_http_bind_requires_allow_non_loopback() { + let err = McpConfig::from_yaml_str( + r#" +serve: + http: + enabled: true + bind: "192.168.1.10:0" +"#, + ) + .expect_err("enabled LAN HTTP bind should require explicit opt-in"); + + assert!(matches!(err, ConfigError::NonLoopbackHttpBind { .. })); + } + + #[test] + fn enabled_ipv6_loopback_http_bind_is_allowed_by_default() { + let cfg = McpConfig::from_yaml_str( + r#" +serve: + http: + enabled: true + bind: "[::1]:0" +"#, + ) + .expect("IPv6 loopback HTTP bind should not require non-loopback opt-in"); + + assert!(!cfg.serve.http.allow_non_loopback); + assert!(cfg.serve.http.is_loopback_bind()); + } + + #[test] + fn enabled_non_loopback_http_bind_allows_explicit_opt_in() { + let cfg = McpConfig::from_yaml_str( + r#" +serve: + http: + enabled: true + bind: "0.0.0.0:0" + allow_non_loopback: true +"#, + ) + .expect("explicit opt-in should allow non-loopback HTTP bind"); + + assert!(cfg.serve.http.allow_non_loopback); + } + #[test] fn invalid_http_bind_fails_config_load() { let err = McpConfig::from_yaml_str( From d1c926732ee96b78134c0aea11d19574a27cc861 Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Tue, 19 May 2026 16:37:13 +1000 Subject: [PATCH 026/187] C7 C8 C9 fix: harden HTTP read runtime --- Cargo.lock | 18 ++ Cargo.toml | 2 + crates/clarion-cli/Cargo.toml | 2 + crates/clarion-cli/src/http_read.rs | 260 ++++++++++++++++++++++++---- crates/clarion-cli/src/serve.rs | 159 +++++++++++++++-- crates/clarion-cli/tests/serve.rs | 70 ++++++++ 6 files changed, 464 insertions(+), 47 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d8c35536..e1614a93 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -335,6 +335,8 @@ dependencies = [ "tempfile", "time", "tokio", + "tower", + "tower-http", "tracing", "tracing-subscriber", "uuid", @@ -2027,6 +2029,19 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + [[package]] name = "toml" version = "0.8.23" @@ -2079,6 +2094,7 @@ dependencies = [ "pin-project-lite", "sync_wrapper", "tokio", + "tokio-util", "tower-layer", "tower-service", "tracing", @@ -2095,10 +2111,12 @@ dependencies = [ "futures-util", "http", "http-body", + "http-body-util", "pin-project-lite", "tower", "tower-layer", "tower-service", + "tracing", "url", ] diff --git a/Cargo.toml b/Cargo.toml index bd3d9d8f..09bf4877 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,8 @@ sha2 = "0.10" thiserror = "1" time = { version = "0.3", features = ["formatting", "macros", "parsing"] } tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "sync", "time"] } +tower = { version = "0.5", features = ["limit", "load-shed", "timeout"] } +tower-http = { version = "0.6", features = ["limit", "trace"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } uuid = { version = "1", features = ["v4"] } diff --git a/crates/clarion-cli/Cargo.toml b/crates/clarion-cli/Cargo.toml index 478943d9..813c8e88 100644 --- a/crates/clarion-cli/Cargo.toml +++ b/crates/clarion-cli/Cargo.toml @@ -31,6 +31,8 @@ serde_norway.workspace = true sha2.workspace = true time.workspace = true tokio.workspace = true +tower.workspace = true +tower-http.workspace = true tracing.workspace = true tracing-subscriber.workspace = true uuid.workspace = true diff --git a/crates/clarion-cli/src/http_read.rs b/crates/clarion-cli/src/http_read.rs index f017e73d..3b441ada 100644 --- a/crates/clarion-cli/src/http_read.rs +++ b/crates/clarion-cli/src/http_read.rs @@ -1,9 +1,11 @@ use std::error::Error as StdError; use std::path::PathBuf; -use std::sync::LazyLock; +use std::sync::{LazyLock, mpsc}; use std::thread; +use std::time::Duration; use anyhow::{Context, Result, anyhow}; +use axum::error_handling::HandleErrorLayer; use axum::extract::{Query, State}; use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; @@ -13,6 +15,12 @@ use clarion_mcp::config::HttpReadConfig; use clarion_storage::{ReaderPool, StorageError, resolve_file}; use serde::{Deserialize, Serialize}; use tokio::sync::oneshot; +use tower::limit::ConcurrencyLimitLayer; +use tower::load_shed; +use tower::timeout; +use tower::{BoxError, ServiceBuilder}; +use tower_http::limit::RequestBodyLimitLayer; +use tower_http::trace::TraceLayer; static HTTP_ERROR_DISPATCH: LazyLock = LazyLock::new(|| { let subscriber = tracing_subscriber::fmt() @@ -26,10 +34,40 @@ static HTTP_ERROR_DISPATCH: LazyLock = LazyLock::new(|| { #[derive(Debug)] pub struct HttpReadServer { shutdown: Option>, + failure_rx: mpsc::Receiver, join: Option>>, } impl HttpReadServer { + pub fn check_running(&mut self) -> Result<()> { + match self.failure_rx.try_recv() { + Ok(error) => { + let join_result = self.join_finished(); + if let Some(Err(join_error)) = join_result { + return Err(join_error.context(error)); + } + return Err(anyhow!(error).context("HTTP read API server failed")); + } + Err(mpsc::TryRecvError::Empty) => {} + Err(mpsc::TryRecvError::Disconnected) => { + if self + .join + .as_ref() + .is_some_and(thread::JoinHandle::is_finished) + { + match self.join_finished() { + Some(Ok(())) => { + return Err(anyhow!("HTTP read API server exited unexpectedly")); + } + Some(Err(err)) => return Err(err), + None => {} + } + } + } + } + Ok(()) + } + pub fn shutdown(mut self) -> Result<()> { if let Some(shutdown) = self.shutdown.take() { let _ = shutdown.send(()); @@ -40,6 +78,22 @@ impl HttpReadServer { } Ok(()) } + + fn join_finished(&mut self) -> Option> { + let finished = self + .join + .as_ref() + .is_some_and(thread::JoinHandle::is_finished); + if !finished { + return None; + } + let join = self.join.take()?; + Some( + join.join() + .map_err(|_| anyhow!("HTTP read server thread panicked")) + .and_then(|result| result), + ) + } } #[derive(Clone)] @@ -51,7 +105,7 @@ struct AppState { pub fn spawn( project_root: PathBuf, - db_path: PathBuf, + readers: ReaderPool, instance_id: String, config: &HttpReadConfig, ) -> Result> { @@ -65,38 +119,24 @@ pub fn spawn( let warn_unauthenticated_non_loopback = config.allow_non_loopback && !config.is_loopback_bind(); let (shutdown_tx, shutdown_rx) = oneshot::channel(); let (ready_tx, ready_rx) = std::sync::mpsc::channel(); - let join = thread::spawn(move || -> Result<()> { - let runtime = tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build() - .context("create HTTP read runtime")?; - runtime.block_on(async move { - let readers = ReaderPool::open(&db_path, 16) - .map_err(|err| anyhow!("open HTTP reader pool for {}: {err}", db_path.display()))?; - let listener = match tokio::net::TcpListener::bind(bind).await { - Ok(listener) => listener, - Err(err) => { - let _ = ready_tx.send(Err(anyhow!("bind HTTP read API on {bind}: {err}"))); - return Err(anyhow!("bind HTTP read API on {bind}: {err}")); - } - }; - let local_addr = listener - .local_addr() - .context("read HTTP read API local addr")?; - let _ = ready_tx.send(Ok(local_addr)); - let state = AppState { + let (failure_tx, failure_rx) = mpsc::channel(); + let join = thread::Builder::new() + .name("clarion-http-read".to_owned()) + .spawn(move || -> Result<()> { + let result = run_http_read_server( project_root, readers, instance_id, - }; - axum::serve(listener, router(state)) - .with_graceful_shutdown(async { - let _ = shutdown_rx.await; - }) - .await - .context("serve HTTP read API") + bind, + shutdown_rx, + ready_tx, + ); + if let Err(err) = &result { + let _ = failure_tx.send(format!("{err:#}")); + } + result }) - }); + .context("spawn HTTP read server thread")?; let local_addr = ready_rx .recv() .context("wait for HTTP read API bind result")??; @@ -111,15 +151,86 @@ pub fn spawn( tracing::info!(bind = %local_addr, auth = %auth, "Clarion HTTP read API listening"); Ok(Some(HttpReadServer { shutdown: Some(shutdown_tx), + failure_rx, join: Some(join), })) } +fn run_http_read_server( + project_root: PathBuf, + readers: ReaderPool, + instance_id: String, + bind: std::net::SocketAddr, + shutdown_rx: oneshot::Receiver<()>, + ready_tx: mpsc::Sender>, +) -> Result<()> { + let runtime = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .context("create HTTP read runtime")?; + runtime.block_on(async move { + let listener = match tokio::net::TcpListener::bind(bind).await { + Ok(listener) => listener, + Err(err) => { + let _ = ready_tx.send(Err(anyhow!("bind HTTP read API on {bind}: {err}"))); + return Err(anyhow!("bind HTTP read API on {bind}: {err}")); + } + }; + let local_addr = listener + .local_addr() + .context("read HTTP read API local addr")?; + let _ = ready_tx.send(Ok(local_addr)); + let state = AppState { + project_root, + readers, + instance_id, + }; + axum::serve(listener, router(state)) + .with_graceful_shutdown(async { + let _ = shutdown_rx.await; + }) + .await + .context("serve HTTP read API") + }) +} + fn router(state: AppState) -> Router { Router::new() .route("/api/v1/files", get(get_file)) .route("/api/v1/_capabilities", get(get_capabilities)) .with_state(state) + .layer( + ServiceBuilder::new() + .layer(HandleErrorLayer::new(handle_middleware_error)) + .layer(TraceLayer::new_for_http().on_failure(())) + .layer(timeout::TimeoutLayer::new(Duration::from_secs(10))) + .layer(RequestBodyLimitLayer::new(16 * 1024)) + .layer(load_shed::LoadShedLayer::new()) + .layer(ConcurrencyLimitLayer::new(64)), + ) +} + +async fn handle_middleware_error(err: BoxError) -> Response { + if err.is::() { + return json_error( + StatusCode::REQUEST_TIMEOUT, + ErrorCode::Internal, + "HTTP request timed out", + ); + } + if err.is::() { + return json_error( + StatusCode::SERVICE_UNAVAILABLE, + ErrorCode::StorageError, + "HTTP read API is overloaded", + ); + } + tracing::error!(error = %err, "HTTP read API middleware failed"); + json_error( + StatusCode::INTERNAL_SERVER_ERROR, + ErrorCode::Internal, + "HTTP read API middleware failed", + ) } #[derive(Debug, Deserialize)] @@ -313,3 +424,92 @@ fn json_error(status: StatusCode, code: ErrorCode, message: &str) -> Response { ) .into_response() } + +#[cfg(test)] +mod tests { + use std::future::{Future, Pending, pending}; + use std::sync::mpsc; + use std::task::{Context, Poll}; + + use super::*; + use tower::Service; + + #[test] + fn check_running_surfaces_completed_http_thread_failure_before_shutdown() { + let (shutdown_tx, _shutdown_rx) = oneshot::channel(); + let (failure_tx, failure_rx) = mpsc::channel(); + failure_tx + .send("simulated HTTP server failure".to_owned()) + .expect("send simulated failure"); + let join = thread::spawn(|| Err(anyhow!("simulated HTTP server failure"))); + let mut server = HttpReadServer { + shutdown: Some(shutdown_tx), + failure_rx, + join: Some(join), + }; + + let err = server + .check_running() + .expect_err("HTTP failure should surface before shutdown"); + let message = format!("{err:#}"); + + assert!( + message.contains("simulated HTTP server failure"), + "unexpected error: {message}" + ); + } + + #[test] + fn load_shed_converts_concurrency_backpressure_to_overload_response() { + #[derive(Clone)] + struct PendingService; + + impl Service<()> for PendingService { + type Response = (); + type Error = BoxError; + type Future = Pending>; + + fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn call(&mut self, _request: ()) -> Self::Future { + pending() + } + } + + let mut service = ServiceBuilder::new() + .layer(load_shed::LoadShedLayer::new()) + .layer(ConcurrencyLimitLayer::new(1)) + .service(PendingService); + let waker = std::task::Waker::noop(); + let mut cx = Context::from_waker(waker); + + assert!( + service.poll_ready(&mut cx).is_ready(), + "first request should acquire the only concurrency permit" + ); + let _held_permit = service.call(()); + + assert!( + service.poll_ready(&mut cx).is_ready(), + "load-shed should stay ready when the concurrency limiter is saturated" + ); + let mut overloaded = std::pin::pin!(service.call(())); + let err = match Future::poll(overloaded.as_mut(), &mut cx) { + Poll::Ready(Err(err)) => err, + other => panic!("expected immediate overload error, got {other:?}"), + }; + assert!( + err.is::(), + "expected load-shed overload error, got {err}" + ); + + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("test runtime"); + let response = runtime.block_on(handle_middleware_error(err)); + assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE); + } +} diff --git a/crates/clarion-cli/src/serve.rs b/crates/clarion-cli/src/serve.rs index 76410bfd..937c139e 100644 --- a/crates/clarion-cli/src/serve.rs +++ b/crates/clarion-cli/src/serve.rs @@ -1,14 +1,16 @@ use std::fs; use std::io::BufReader; -use std::path::Path; -use std::sync::Arc; +use std::path::{Path, PathBuf}; +use std::sync::{Arc, mpsc}; +use std::thread; +use std::time::Duration; use anyhow::{Context, Result, anyhow, ensure}; use clarion_core::{ ClaudeCliProvider, ClaudeCliProviderConfig, CodexCliProvider, CodexCliProviderConfig, LlmProvider, OpenRouterProvider, OpenRouterProviderConfig, Recording, RecordingProvider, }; -use clarion_mcp::config::{McpConfig, ProviderSelection, select_provider_with_env}; +use clarion_mcp::config::{LlmConfig, McpConfig, ProviderSelection, select_provider_with_env}; use clarion_mcp::filigree::FiligreeHttpClient; use clarion_storage::{DEFAULT_BATCH_SIZE, DEFAULT_CHANNEL_CAPACITY, ReaderPool, Writer}; @@ -41,6 +43,66 @@ pub fn run(path: &Path, config_path: Option<&Path>) -> Result<()> { }) .context("build Filigree HTTP client")?; + let readers = ReaderPool::open(&db_path, 16) + .map_err(|err| anyhow!("open reader pool for {}: {err}", db_path.display()))?; + let http_project_root = project_root.clone(); + let http_server = crate::http_read::spawn( + http_project_root, + readers.clone(), + instance_id, + &config.serve.http, + ) + .context("start HTTP read API")?; + let stdio = spawn_mcp_stdio( + project_root, + db_path, + readers, + config.llm.clone(), + llm_provider, + filigree_client, + )?; + supervise_stdio_with_http(stdio, http_server) +} + +struct StdioServe { + result_rx: mpsc::Receiver>, + join: thread::JoinHandle<()>, +} + +fn spawn_mcp_stdio( + project_root: PathBuf, + db_path: PathBuf, + readers: ReaderPool, + llm_config: LlmConfig, + llm_provider: Option>, + filigree_client: Option, +) -> Result { + let (result_tx, result_rx) = mpsc::channel(); + let join = thread::Builder::new() + .name("clarion-mcp-stdio".to_owned()) + .spawn(move || { + let result = run_mcp_stdio( + project_root, + db_path, + readers, + llm_config, + llm_provider, + filigree_client, + ); + let _ = result_tx.send(result); + }) + .context("spawn MCP stdio server thread")?; + Ok(StdioServe { result_rx, join }) +} + +fn run_mcp_stdio( + project_root: PathBuf, + db_path: PathBuf, + readers: ReaderPool, + llm_config: LlmConfig, + llm_provider: Option>, + filigree_client: Option, +) -> Result<()> { let stdin = std::io::stdin(); let stdout = std::io::stdout(); let mut reader = BufReader::new(stdin.lock()); @@ -50,17 +112,7 @@ pub fn run(path: &Path, config_path: Option<&Path>) -> Result<()> { .build() .context("create MCP runtime")?; let _runtime_guard = runtime.enter(); - let readers = ReaderPool::open(&db_path, 16) - .map_err(|err| anyhow!("open reader pool for {}: {err}", db_path.display()))?; - let http_project_root = project_root.clone(); let mut state = clarion_mcp::ServerState::new(project_root, readers); - let http_server = crate::http_read::spawn( - http_project_root, - db_path.clone(), - instance_id, - &config.serve.http, - ) - .context("start HTTP read API")?; let mut llm_writer = None; let mut llm_writer_join = None; if let Some(provider) = llm_provider { @@ -70,7 +122,7 @@ pub fn run(path: &Path, config_path: Option<&Path>) -> Result<()> { DEFAULT_CHANNEL_CAPACITY, ) .map_err(|err| anyhow!("spawn MCP LLM writer for {}: {err}", db_path.display()))?; - state = state.with_summary_llm(writer.sender(), config.llm.clone(), provider); + state = state.with_summary_llm(writer.sender(), llm_config, provider); llm_writer = Some(writer); llm_writer_join = Some(handle); } @@ -82,9 +134,6 @@ pub fn run(path: &Path, config_path: Option<&Path>) -> Result<()> { clarion_mcp::serve_stdio_with_state_on_runtime(&runtime, &state, &mut reader, &mut writer) .context("serve MCP stdio"); drop(state); - if let Some(server) = http_server { - server.shutdown().context("stop HTTP read API")?; - } drop(llm_writer); let writer_result = if let Some(handle) = llm_writer_join { Some( @@ -104,6 +153,63 @@ pub fn run(path: &Path, config_path: Option<&Path>) -> Result<()> { Ok(()) } +fn supervise_stdio_with_http( + stdio: StdioServe, + mut http_server: Option, +) -> Result<()> { + let serve_result = loop { + match stdio.result_rx.recv_timeout(Duration::from_millis(100)) { + Ok(result) => break result, + Err(mpsc::RecvTimeoutError::Timeout) => { + if let Some(server) = http_server.as_mut() + && let Err(err) = server.check_running() + { + if let Some(server) = http_server.take() + && let Err(stop_err) = server.shutdown() + { + tracing::warn!( + error = %stop_err, + "failed to stop HTTP read API after supervised failure" + ); + } + return Err(err.context("HTTP read API failed while MCP stdio was running")); + } + } + Err(mpsc::RecvTimeoutError::Disconnected) => { + stdio + .join + .join() + .map_err(|_| anyhow!("MCP stdio server thread panicked"))?; + return Err(anyhow!("MCP stdio server thread exited without a result")); + } + } + }; + stdio + .join + .join() + .map_err(|_| anyhow!("MCP stdio server thread panicked"))?; + let shutdown_result = match http_server { + Some(server) => server.shutdown().context("stop HTTP read API"), + None => Ok(()), + }; + finish_supervised_result(serve_result, shutdown_result) +} + +fn finish_supervised_result(serve_result: Result<()>, shutdown_result: Result<()>) -> Result<()> { + match (serve_result, shutdown_result) { + (Ok(()), Ok(())) => Ok(()), + (Ok(()), Err(shutdown_err)) => Err(shutdown_err), + (Err(serve_err), Ok(())) => Err(serve_err), + (Err(serve_err), Err(shutdown_err)) => { + tracing::warn!( + error = %shutdown_err, + "failed to stop HTTP read API after MCP stdio failure" + ); + Err(serve_err) + } + } +} + fn build_llm_provider( config: &McpConfig, selection: ProviderSelection, @@ -165,6 +271,25 @@ fn build_llm_provider( } } +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn finish_supervised_result_preserves_stdio_error_over_shutdown_error() { + let err = finish_supervised_result( + Err(anyhow!("stdio failed first")), + Err(anyhow!("HTTP shutdown also failed")), + ) + .expect_err("stdio failure should win"); + + assert!( + format!("{err:#}").contains("stdio failed first"), + "unexpected error: {err:#}" + ); + } +} + fn load_recording_fixture(config: &McpConfig, project_root: &Path) -> Result> { let Some(path) = &config.llm.recording_fixture_path else { return Ok(Vec::new()); diff --git a/crates/clarion-cli/tests/serve.rs b/crates/clarion-cli/tests/serve.rs index 0819df55..87ff8413 100644 --- a/crates/clarion-cli/tests/serve.rs +++ b/crates/clarion-cli/tests/serve.rs @@ -183,6 +183,27 @@ fn serve_http_files_blank_path_returns_invalid_path_envelope() { ); } +#[test] +fn serve_http_rejects_oversized_get_body_before_handler() { + let dir = tempfile::tempdir().expect("temp project"); + clarion_bin() + .args(["install", "--path"]) + .arg(dir.path()) + .env("PATH", "") + .assert() + .success(); + let bind = free_loopback_bind(); + write_http_config(dir.path(), &bind); + + let mut child = spawn_serve(dir.path()); + let status_code = + wait_for_http_get_with_body_status(&bind, "/api/v1/_capabilities", 16 * 1024 + 1); + stop_serve(&mut child); + let status_code = status_code.expect("HTTP response to oversized body"); + + assert_eq!(status_code, 413); +} + #[test] fn serve_http_files_path_traversal_returns_outside_project_envelope() { let dir = tempfile::tempdir().expect("temp project"); @@ -1422,6 +1443,55 @@ fn wait_for_http_response(bind: &str, path: &str) -> Result Result { + let deadline = Instant::now() + Duration::from_secs(5); + let body = vec![b'x'; body_len]; + let mut last_error = String::new(); + while Instant::now() < deadline { + match http_get_with_body_status(bind, path, &body) { + Ok(status_code) => return Ok(status_code), + Err(err) => last_error = err, + } + std::thread::sleep(Duration::from_millis(25)); + } + Err(last_error) +} + +fn http_get_with_body_status(bind: &str, path: &str, body: &[u8]) -> Result { + let addr = bind + .parse() + .map_err(|err| format!("parse bind address {bind}: {err}"))?; + let mut stream = TcpStream::connect_timeout(&addr, Duration::from_millis(100)) + .map_err(|err| format!("connect to {bind}: {err}"))?; + stream + .set_read_timeout(Some(Duration::from_secs(2))) + .map_err(|err| format!("set read timeout: {err}"))?; + write!( + stream, + "GET {path} HTTP/1.1\r\nHost: {bind}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", + body.len() + ) + .map_err(|err| format!("write request head: {err}"))?; + stream + .write_all(body) + .map_err(|err| format!("write request body: {err}"))?; + let mut reader = std::io::BufReader::new(stream); + let mut status_line = String::new(); + reader + .read_line(&mut status_line) + .map_err(|err| format!("read status line: {err}"))?; + status_line + .split_whitespace() + .nth(1) + .ok_or_else(|| format!("malformed HTTP status line: {status_line}"))? + .parse::() + .map_err(|err| format!("parse HTTP status from {status_line:?}: {err}")) +} + fn http_get_response(bind: &str, path: &str) -> Result { let addr = bind .parse() From f7401ad93986647eb05de4d585d69af18d1a63b1 Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Tue, 19 May 2026 17:10:17 +1000 Subject: [PATCH 027/187] C14 C15 C16 C18 C19 fix: finish medium hardening disposition --- README.md | 7 + crates/clarion-cli/src/http_read.rs | 122 +++++++++++- crates/clarion-cli/tests/serve.rs | 176 ++++++++++++++++++ crates/clarion-storage/src/lib.rs | 11 +- crates/clarion-storage/src/query.rs | 47 ++++- crates/clarion-storage/tests/query_helpers.rs | 38 +++- 6 files changed, 380 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 42b86e75..7dca4439 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,13 @@ and trust-topology tools. WP9-B, deferred per the [Sprint 2 scope amendment](docs/implementation/sprint-2/scope-amendment-2026-05.md#4-v01-planmd-resequencing). +**Known v1.0 limitations:** + +- **HTTP file language inference uses stored plugin identity plus a narrow + core-extension fallback.** Plugin manifests already declare language and + extensions, but v1.0 does not persist a manifest language registry for the + `/api/v1/files` read path. This is tracked as post-v1.0 hardening. + ## What it does today `clarion serve` exposes seven MCP tools that a consult-mode agent calls instead diff --git a/crates/clarion-cli/src/http_read.rs b/crates/clarion-cli/src/http_read.rs index 3b441ada..6edc998d 100644 --- a/crates/clarion-cli/src/http_read.rs +++ b/crates/clarion-cli/src/http_read.rs @@ -6,13 +6,14 @@ use std::time::Duration; use anyhow::{Context, Result, anyhow}; use axum::error_handling::HandleErrorLayer; +use axum::extract::rejection::QueryRejection; use axum::extract::{Query, State}; -use axum::http::StatusCode; +use axum::http::{HeaderMap, HeaderValue, Request, StatusCode, header}; use axum::response::{IntoResponse, Response}; use axum::routing::get; use axum::{Json, Router}; use clarion_mcp::config::HttpReadConfig; -use clarion_storage::{ReaderPool, StorageError, resolve_file}; +use clarion_storage::{ReaderPool, StorageError, resolve_file_catalog_entry}; use serde::{Deserialize, Serialize}; use tokio::sync::oneshot; use tower::limit::ConcurrencyLimitLayer; @@ -202,7 +203,11 @@ fn router(state: AppState) -> Router { .layer( ServiceBuilder::new() .layer(HandleErrorLayer::new(handle_middleware_error)) - .layer(TraceLayer::new_for_http().on_failure(())) + .layer( + TraceLayer::new_for_http() + .make_span_with(http_request_span) + .on_failure(()), + ) .layer(timeout::TimeoutLayer::new(Duration::from_secs(10))) .layer(RequestBodyLimitLayer::new(16 * 1024)) .layer(load_shed::LoadShedLayer::new()) @@ -234,6 +239,7 @@ async fn handle_middleware_error(err: BoxError) -> Response { } #[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] struct FileQuery { #[serde(default)] path: String, @@ -273,7 +279,21 @@ enum ErrorCode { Internal, } -async fn get_file(State(state): State, Query(query): Query) -> Response { +async fn get_file( + State(state): State, + headers: HeaderMap, + query: Result, QueryRejection>, +) -> Response { + let Query(query) = match query { + Ok(query) => query, + Err(_) => { + return json_error( + StatusCode::BAD_REQUEST, + ErrorCode::InvalidPath, + "query parameters are invalid", + ); + } + }; if query.path.trim().is_empty() { return json_error( StatusCode::BAD_REQUEST, @@ -284,10 +304,17 @@ async fn get_file(State(state): State, Query(query): Query) let project_root = state.project_root.clone(); let file_path = query.path; let language = query.language; - let result = state + let catalog_result = state .readers - .with_reader(move |conn| resolve_file(conn, &project_root, &file_path, &language)) + .with_reader(move |conn| { + resolve_file_catalog_entry(conn, &project_root, &file_path, &language) + }) .await; + let result = match catalog_result { + Ok(Some(entry)) => entry.into_resolved_file().map(Some), + Ok(None) => Ok(None), + Err(err) => Err(err), + }; match result { Ok(Some(file)) => { if let Some(reason) = file.briefing_blocked.as_deref() { @@ -302,7 +329,13 @@ async fn get_file(State(state): State, Query(query): Query) "file is not known to Clarion", ); } - ( + let etag = file_etag(&file.content_hash); + if if_none_match_matches(headers.get(header::IF_NONE_MATCH), &etag) { + let mut response = StatusCode::NOT_MODIFIED.into_response(); + insert_etag(&mut response, &etag); + return response; + } + let mut response = ( StatusCode::OK, Json(FileResponse { entity_id: file.entity_id, @@ -311,7 +344,9 @@ async fn get_file(State(state): State, Query(query): Query) language: file.language, }), ) - .into_response() + .into_response(); + insert_etag(&mut response, &etag); + response } Ok(None) => json_error( StatusCode::NOT_FOUND, @@ -322,6 +357,64 @@ async fn get_file(State(state): State, Query(query): Query) } } +#[derive(Debug, Clone, Default, PartialEq, Eq)] +struct RequestLogContext { + loom_component: Option, + filigree_actor: Option, +} + +fn request_log_context(headers: &HeaderMap) -> RequestLogContext { + RequestLogContext { + loom_component: log_header_value(headers, "x-loom-component"), + filigree_actor: log_header_value(headers, "x-filigree-actor"), + } +} + +fn log_header_value(headers: &HeaderMap, name: &'static str) -> Option { + let value = headers.get(name)?.to_str().ok()?.trim(); + (!value.is_empty()).then(|| value.to_owned()) +} + +fn http_request_span(request: &Request) -> tracing::Span { + let context = request_log_context(request.headers()); + let span = tracing::info_span!( + "http_read_request", + method = %request.method(), + path = %request.uri().path(), + loom_component = tracing::field::Empty, + filigree_actor = tracing::field::Empty, + ); + if let Some(loom_component) = context.loom_component { + span.record("loom_component", tracing::field::display(loom_component)); + } + if let Some(filigree_actor) = context.filigree_actor { + span.record("filigree_actor", tracing::field::display(filigree_actor)); + } + span +} + +fn file_etag(content_hash: &str) -> String { + format!("\"{content_hash}\"") +} + +fn if_none_match_matches(value: Option<&HeaderValue>, etag: &str) -> bool { + let Some(value) = value else { + return false; + }; + let Ok(value) = value.to_str() else { + return false; + }; + value.split(',').map(str::trim).any(|candidate| { + candidate == "*" || candidate == etag || candidate.strip_prefix("W/") == Some(etag) + }) +} + +fn insert_etag(response: &mut Response, etag: &str) { + if let Ok(value) = HeaderValue::from_str(etag) { + response.headers_mut().insert(header::ETAG, value); + } +} + async fn get_capabilities(State(state): State) -> Json { Json(CapabilitiesResponse { registry_backend: true, @@ -432,6 +525,7 @@ mod tests { use std::task::{Context, Poll}; use super::*; + use axum::http::{HeaderMap, HeaderValue}; use tower::Service; #[test] @@ -512,4 +606,16 @@ mod tests { let response = runtime.block_on(handle_middleware_error(err)); assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE); } + + #[test] + fn request_log_context_reads_optional_actor_headers() { + let mut headers = HeaderMap::new(); + headers.insert("X-Loom-Component", HeaderValue::from_static("loom")); + headers.insert("X-Filigree-Actor", HeaderValue::from_static("worker-f")); + + let context = request_log_context(&headers); + + assert_eq!(context.loom_component.as_deref(), Some("loom")); + assert_eq!(context.filigree_actor.as_deref(), Some("worker-f")); + } } diff --git a/crates/clarion-cli/tests/serve.rs b/crates/clarion-cli/tests/serve.rs index 87ff8413..312d41eb 100644 --- a/crates/clarion-cli/tests/serve.rs +++ b/crates/clarion-cli/tests/serve.rs @@ -23,6 +23,22 @@ struct HttpJsonResponse { body: Value, } +#[derive(Debug)] +struct HttpRawResponse { + status_code: u16, + headers: Vec<(String, String)>, + body: String, +} + +impl HttpRawResponse { + fn header(&self, name: &str) -> Option<&str> { + self.headers + .iter() + .find(|(candidate, _)| candidate.eq_ignore_ascii_case(name)) + .map(|(_, value)| value.as_str()) + } +} + fn clarion_bin() -> Command { Command::cargo_bin("clarion").expect("clarion binary") } @@ -157,6 +173,43 @@ fn serve_http_files_endpoint_resolves_known_file_on_configured_port() { assert_eq!(&response, fixture_example_body(&fixture, "happy_path_200")); } +#[test] +fn serve_http_files_etag_round_trip_and_if_none_match_returns_304() { + let dir = tempfile::tempdir().expect("temp project"); + clarion_bin() + .args(["install", "--path"]) + .arg(dir.path()) + .env("PATH", "") + .assert() + .success(); + let (_file_id, content_hash, _canonical_path) = seed_file_entity(dir.path()); + let bind = free_loopback_bind(); + write_http_config(dir.path(), &bind); + + let mut child = spawn_serve(dir.path()); + let response = + wait_for_http_raw_response(&bind, "/api/v1/files?path=demo.py&language=python", &[]); + let not_modified = wait_for_http_raw_response( + &bind, + "/api/v1/files?path=demo.py&language=python", + &[("If-None-Match", "\"hash-demo-file\"")], + ); + stop_serve(&mut child); + let response = response.expect("HTTP /api/v1/files response"); + let not_modified = not_modified.expect("HTTP /api/v1/files conditional response"); + + let expected_etag = format!("\"{content_hash}\""); + assert_eq!(response.status_code, 200); + assert_eq!(response.header("etag"), Some(expected_etag.as_str())); + assert_eq!(not_modified.status_code, 304); + assert_eq!(not_modified.header("etag"), Some(expected_etag.as_str())); + assert!( + not_modified.body.is_empty(), + "304 response must not include a body: {:?}", + not_modified + ); +} + #[test] fn serve_http_files_blank_path_returns_invalid_path_envelope() { let dir = tempfile::tempdir().expect("temp project"); @@ -183,6 +236,36 @@ fn serve_http_files_blank_path_returns_invalid_path_envelope() { ); } +#[test] +fn serve_http_files_rejects_unknown_query_fields() { + let dir = tempfile::tempdir().expect("temp project"); + clarion_bin() + .args(["install", "--path"]) + .arg(dir.path()) + .env("PATH", "") + .assert() + .success(); + seed_file_entity(dir.path()); + let bind = free_loopback_bind(); + write_http_config(dir.path(), &bind); + + let mut child = spawn_serve(dir.path()); + let response = wait_for_http_response( + &bind, + "/api/v1/files?path=demo.py&language=python&surprise=1", + ); + stop_serve(&mut child); + let response = response.expect("HTTP /api/v1/files query rejection"); + + assert_eq!(response.status_code, 400); + assert_eq!(response.body["code"], "INVALID_PATH"); + assert!( + response.body["error"].as_str().is_some(), + "error envelope must include a string message: {:?}", + response + ); +} + #[test] fn serve_http_rejects_oversized_get_body_before_handler() { let dir = tempfile::tempdir().expect("temp project"); @@ -1443,6 +1526,23 @@ fn wait_for_http_response(bind: &str, path: &str) -> Result Result { + let deadline = Instant::now() + Duration::from_secs(5); + let mut last_error = String::new(); + while Instant::now() < deadline { + match http_raw_response(bind, path, headers) { + Ok(response) => return Ok(response), + Err(err) => last_error = err, + } + std::thread::sleep(Duration::from_millis(25)); + } + Err(last_error) +} + fn wait_for_http_get_with_body_status( bind: &str, path: &str, @@ -1492,6 +1592,82 @@ fn http_get_with_body_status(bind: &str, path: &str, body: &[u8]) -> Result Result { + let addr = bind + .parse() + .map_err(|err| format!("parse bind address {bind}: {err}"))?; + let mut stream = TcpStream::connect_timeout(&addr, Duration::from_millis(100)) + .map_err(|err| format!("connect to {bind}: {err}"))?; + stream + .set_read_timeout(Some(Duration::from_secs(2))) + .map_err(|err| format!("set read timeout: {err}"))?; + write!(stream, "GET {path} HTTP/1.1\r\nHost: {bind}\r\n") + .map_err(|err| format!("write request line: {err}"))?; + for (name, value) in request_headers { + write!(stream, "{name}: {value}\r\n") + .map_err(|err| format!("write request header {name}: {err}"))?; + } + write!(stream, "Connection: close\r\n\r\n") + .map_err(|err| format!("write request terminator: {err}"))?; + + let mut reader = std::io::BufReader::new(stream); + let mut status_line = String::new(); + reader + .read_line(&mut status_line) + .map_err(|err| format!("read status line: {err}"))?; + let status_code = status_line + .split_whitespace() + .nth(1) + .ok_or_else(|| format!("malformed HTTP status line: {status_line}"))? + .parse::() + .map_err(|err| format!("parse HTTP status from {status_line:?}: {err}"))?; + let mut content_length = None; + let mut response_headers = Vec::new(); + let mut header = String::new(); + loop { + header.clear(); + reader + .read_line(&mut header) + .map_err(|err| format!("read header: {err}"))?; + if header == "\r\n" || header == "\n" || header.is_empty() { + break; + } + if let Some((name, value)) = header.split_once(':') { + let name = name.trim().to_owned(); + let value = value.trim().to_owned(); + if name.eq_ignore_ascii_case("content-length") { + content_length = Some( + value + .parse::() + .map_err(|err| format!("parse content-length from {header:?}: {err}"))?, + ); + } + response_headers.push((name, value)); + } + } + let mut body = String::new(); + if let Some(content_length) = content_length { + let mut bytes = vec![0_u8; content_length]; + reader + .read_exact(&mut bytes) + .map_err(|err| format!("read response body: {err}"))?; + body = String::from_utf8(bytes).map_err(|err| format!("response body is utf8: {err}"))?; + } else { + reader + .read_to_string(&mut body) + .map_err(|err| format!("read response body: {err}"))?; + } + Ok(HttpRawResponse { + status_code, + headers: response_headers, + body, + }) +} + fn http_get_response(bind: &str, path: &str) -> Result { let addr = bind .parse() diff --git a/crates/clarion-storage/src/lib.rs b/crates/clarion-storage/src/lib.rs index 53226f02..d7266027 100644 --- a/crates/clarion-storage/src/lib.rs +++ b/crates/clarion-storage/src/lib.rs @@ -27,11 +27,12 @@ pub use commands::{ pub use error::{Result, StorageError}; pub use query::{ CallEdgeMatch, ContainedEntities, EntityRow, ModuleDependencyEdge, ReferenceDirection, - ReferenceEdgeMatch, ResolvedFile, SubsystemMember, UnresolvedCallSiteRow, call_edges_from, - call_edges_targeting, candidate_entities_for_unresolved_sites, child_entity_ids, - contained_entity_ids, entity_at_line, entity_briefing_block_reason, entity_by_id, - existing_entity_ids, find_entities, module_dependency_edges, normalize_source_path, - reference_edges_for_entity, resolve_file, subsystem_for_member, subsystem_members, + ReferenceEdgeMatch, ResolvedFile, ResolvedFileCatalogEntry, SubsystemMember, + UnresolvedCallSiteRow, call_edges_from, call_edges_targeting, + candidate_entities_for_unresolved_sites, child_entity_ids, contained_entity_ids, + entity_at_line, entity_briefing_block_reason, entity_by_id, existing_entity_ids, find_entities, + module_dependency_edges, normalize_source_path, reference_edges_for_entity, resolve_file, + resolve_file_catalog_entry, subsystem_for_member, subsystem_members, unresolved_call_sites_for_caller, unresolved_callers_for_target, }; pub use reader::ReaderPool; diff --git a/crates/clarion-storage/src/query.rs b/crates/clarion-storage/src/query.rs index 0058bda5..19bb5a4b 100644 --- a/crates/clarion-storage/src/query.rs +++ b/crates/clarion-storage/src/query.rs @@ -89,6 +89,32 @@ pub struct ResolvedFile { pub briefing_blocked: Option, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ResolvedFileCatalogEntry { + pub entity_id: String, + pub content_hash: Option, + pub canonical_path: String, + pub language: String, + pub briefing_blocked: Option, + content_hash_path: PathBuf, +} + +impl ResolvedFileCatalogEntry { + pub fn into_resolved_file(self) -> Result { + let content_hash = match self.content_hash { + Some(content_hash) => content_hash, + None => file_content_hash(&self.content_hash_path)?, + }; + Ok(ResolvedFile { + entity_id: self.entity_id, + content_hash, + canonical_path: self.canonical_path, + language: self.language, + briefing_blocked: self.briefing_blocked, + }) + } +} + const MODULE_ANCESTOR_MAX_DEPTH: i64 = 32; #[derive(Debug, Clone, PartialEq, Eq)] @@ -171,6 +197,18 @@ pub fn resolve_file( file: &str, language: &str, ) -> Result> { + let Some(entry) = resolve_file_catalog_entry(conn, project_root, file, language)? else { + return Ok(None); + }; + entry.into_resolved_file().map(Some) +} + +pub fn resolve_file_catalog_entry( + conn: &Connection, + project_root: &Path, + file: &str, + language: &str, +) -> Result> { let lookup_path = normalize_lookup_path(project_root, file)?; let normalized = lookup_path .to_str() @@ -178,16 +216,13 @@ pub fn resolve_file( let canonical_path = project_relative_path(project_root, &lookup_path)?; if let Some(entity) = source_entity_for_path(conn, normalized, Some("file"))? { let briefing_blocked = entity_briefing_block_reason(&entity.properties_json); - let content_hash = match entity.content_hash { - Some(content_hash) => content_hash, - None => file_content_hash(&lookup_path)?, - }; - return Ok(Some(ResolvedFile { + return Ok(Some(ResolvedFileCatalogEntry { entity_id: entity.id, - content_hash, + content_hash: entity.content_hash, canonical_path, language: resolved_language(language, &entity.plugin_id, &lookup_path), briefing_blocked, + content_hash_path: lookup_path, })); } Ok(None) diff --git a/crates/clarion-storage/tests/query_helpers.rs b/crates/clarion-storage/tests/query_helpers.rs index 5d659355..9a94e36d 100644 --- a/crates/clarion-storage/tests/query_helpers.rs +++ b/crates/clarion-storage/tests/query_helpers.rs @@ -7,8 +7,8 @@ use clarion_storage::{ ModuleDependencyEdge, ReferenceDirection, SubsystemMember, call_edges_from, call_edges_targeting, child_entity_ids, contained_entity_ids, entity_at_line, entity_briefing_block_reason, entity_by_id, find_entities, module_dependency_edges, - normalize_source_path, pragma, reference_edges_for_entity, resolve_file, schema, - subsystem_for_member, subsystem_members, + normalize_source_path, pragma, reference_edges_for_entity, resolve_file, + resolve_file_catalog_entry, schema, subsystem_for_member, subsystem_members, }; use rusqlite::{Connection, params}; @@ -901,6 +901,40 @@ fn resolve_file_deleted_on_disk_but_cataloged_row_resolves() { ); } +#[test] +fn resolve_file_catalog_entry_returns_missing_hash_without_reading_disk() { + let tempdir = tempfile::tempdir().expect("temp project root"); + let project_root = tempdir.path(); + let source_path = project_root.join("src").join("missing-hash.py"); + std::fs::create_dir_all(source_path.parent().unwrap()).expect("create source dir"); + std::fs::write(&source_path, "def missing_hash():\n return 1\n").expect("write source"); + let canonical = source_path.canonicalize().expect("canonical source"); + + let conn = open_fresh(&tempdir); + conn.execute( + "INSERT INTO entities ( + id, plugin_id, kind, name, short_name, source_file_path, + source_line_start, source_line_end, properties, content_hash, created_at, updated_at + ) VALUES ( + 'python:file:missing_hash', 'python', 'file', 'missing-hash.py', 'missing-hash.py', ?1, + 1, 2, '{}', NULL, + '2026-05-19T00:00:00.000Z', '2026-05-19T00:00:00.000Z' + )", + params![canonical.display().to_string()], + ) + .expect("insert file entity without cached hash"); + std::fs::remove_file(&source_path).expect("delete source after cataloging"); + + let entry = resolve_file_catalog_entry(&conn, project_root, "src/missing-hash.py", "python") + .expect("catalog lookup should not read deleted source") + .expect("entity is known"); + + assert_eq!(entry.entity_id, "python:file:missing_hash"); + assert_eq!(entry.content_hash, None); + assert_eq!(entry.canonical_path, "src/missing-hash.py"); + assert_eq!(entry.language, "python"); +} + #[test] #[cfg(unix)] fn resolve_file_unreadable_hash_failure_propagates() { From b66effcff41470b88e1b570997180880b8c8f21c Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Tue, 19 May 2026 17:16:03 +1000 Subject: [PATCH 028/187] fix: satisfy workspace clippy gate for HTTP hardening --- crates/clarion-cli/src/http_read.rs | 31 +++++++++-------------- crates/clarion-cli/src/instance.rs | 4 +-- crates/clarion-cli/src/serve.rs | 38 ++++++++++++++--------------- crates/clarion-cli/tests/serve.rs | 21 ++++++---------- 4 files changed, 40 insertions(+), 54 deletions(-) diff --git a/crates/clarion-cli/src/http_read.rs b/crates/clarion-cli/src/http_read.rs index 6edc998d..a391306d 100644 --- a/crates/clarion-cli/src/http_read.rs +++ b/crates/clarion-cli/src/http_read.rs @@ -284,15 +284,12 @@ async fn get_file( headers: HeaderMap, query: Result, QueryRejection>, ) -> Response { - let Query(query) = match query { - Ok(query) => query, - Err(_) => { - return json_error( - StatusCode::BAD_REQUEST, - ErrorCode::InvalidPath, - "query parameters are invalid", - ); - } + let Ok(Query(query)) = query else { + return json_error( + StatusCode::BAD_REQUEST, + ErrorCode::InvalidPath, + "query parameters are invalid", + ); }; if query.path.trim().is_empty() { return json_error( @@ -353,7 +350,7 @@ async fn get_file( ErrorCode::NotFound, "file is not known to Clarion", ), - Err(err) => json_read_error(err), + Err(err) => json_read_error(&err), } } @@ -424,10 +421,10 @@ async fn get_capabilities(State(state): State) -> Json Response { - let error = classify_read_error(&err); +fn json_read_error(err: &StorageError) -> Response { + let error = classify_read_error(err); if error.status.is_server_error() { - log_read_server_error(error.code, error.status, &err); + log_read_server_error(error.code, error.status, err); } json_error(error.status, error.code, error.message) } @@ -455,11 +452,6 @@ fn classify_read_error(err: &StorageError) -> ReadError { code: ErrorCode::StorageError, message: "file lookup storage is unavailable", }, - StorageError::PoolInteract(_) => ReadError { - status: StatusCode::INTERNAL_SERVER_ERROR, - code: ErrorCode::Internal, - message: "internal file lookup failure", - }, StorageError::Sqlite(_) | StorageError::PoolBuild(_) | StorageError::PragmaInvariant(_) @@ -469,7 +461,8 @@ fn classify_read_error(err: &StorageError) -> ReadError { code: ErrorCode::StorageError, message: "file lookup failed", }, - StorageError::WriterGone + StorageError::PoolInteract(_) + | StorageError::WriterGone | StorageError::WriterProtocol(_) | StorageError::WriterNoResponse => ReadError { status: StatusCode::INTERNAL_SERVER_ERROR, diff --git a/crates/clarion-cli/src/instance.rs b/crates/clarion-cli/src/instance.rs index dde1df73..595bedb5 100644 --- a/crates/clarion-cli/src/instance.rs +++ b/crates/clarion-cli/src/instance.rs @@ -92,11 +92,11 @@ fn read_existing_instance_id(path: &Path, raw: &str) -> Result { fn parse_instance_id(path: &Path, raw: &str) -> Result { let trimmed = raw.trim(); - let id = Uuid::parse_str(trimmed).map_err(|err| invalid_instance_id(path, err))?; + let id = Uuid::parse_str(trimmed).map_err(|err| invalid_instance_id(path, &err))?; Ok(id.to_string()) } -fn invalid_instance_id(path: &Path, source: uuid::Error) -> anyhow::Error { +fn invalid_instance_id(path: &Path, source: &uuid::Error) -> anyhow::Error { anyhow!( "invalid Clarion instance ID in {}: {source}; expected a UUID", path.display() diff --git a/crates/clarion-cli/src/serve.rs b/crates/clarion-cli/src/serve.rs index 937c139e..b73e7229 100644 --- a/crates/clarion-cli/src/serve.rs +++ b/crates/clarion-cli/src/serve.rs @@ -83,7 +83,7 @@ fn spawn_mcp_stdio( .spawn(move || { let result = run_mcp_stdio( project_root, - db_path, + &db_path, readers, llm_config, llm_provider, @@ -97,7 +97,7 @@ fn spawn_mcp_stdio( fn run_mcp_stdio( project_root: PathBuf, - db_path: PathBuf, + db_path: &Path, readers: ReaderPool, llm_config: LlmConfig, llm_provider: Option>, @@ -117,7 +117,7 @@ fn run_mcp_stdio( let mut llm_writer_join = None; if let Some(provider) = llm_provider { let (writer, handle) = Writer::spawn( - db_path.clone(), + db_path.to_owned(), DEFAULT_BATCH_SIZE, DEFAULT_CHANNEL_CAPACITY, ) @@ -271,6 +271,22 @@ fn build_llm_provider( } } +fn load_recording_fixture(config: &McpConfig, project_root: &Path) -> Result> { + let Some(path) = &config.llm.recording_fixture_path else { + return Ok(Vec::new()); + }; + let path = Path::new(path); + let path = if path.is_absolute() { + path.to_owned() + } else { + project_root.join(path) + }; + let raw = fs::read_to_string(&path) + .with_context(|| format!("read RecordingProvider fixture {}", path.display()))?; + serde_json::from_str(&raw) + .with_context(|| format!("parse RecordingProvider fixture {}", path.display())) +} + #[cfg(test)] mod tests { use super::*; @@ -289,19 +305,3 @@ mod tests { ); } } - -fn load_recording_fixture(config: &McpConfig, project_root: &Path) -> Result> { - let Some(path) = &config.llm.recording_fixture_path else { - return Ok(Vec::new()); - }; - let path = Path::new(path); - let path = if path.is_absolute() { - path.to_owned() - } else { - project_root.join(path) - }; - let raw = fs::read_to_string(&path) - .with_context(|| format!("read RecordingProvider fixture {}", path.display()))?; - serde_json::from_str(&raw) - .with_context(|| format!("parse RecordingProvider fixture {}", path.display())) -} diff --git a/crates/clarion-cli/tests/serve.rs b/crates/clarion-cli/tests/serve.rs index 312d41eb..883bb715 100644 --- a/crates/clarion-cli/tests/serve.rs +++ b/crates/clarion-cli/tests/serve.rs @@ -205,8 +205,7 @@ fn serve_http_files_etag_round_trip_and_if_none_match_returns_304() { assert_eq!(not_modified.header("etag"), Some(expected_etag.as_str())); assert!( not_modified.body.is_empty(), - "304 response must not include a body: {:?}", - not_modified + "304 response must not include a body: {not_modified:?}" ); } @@ -231,8 +230,7 @@ fn serve_http_files_blank_path_returns_invalid_path_envelope() { assert_eq!(response.body["code"], "INVALID_PATH"); assert!( response.body["error"].as_str().is_some(), - "error envelope must include a string message: {:?}", - response + "error envelope must include a string message: {response:?}" ); } @@ -261,8 +259,7 @@ fn serve_http_files_rejects_unknown_query_fields() { assert_eq!(response.body["code"], "INVALID_PATH"); assert!( response.body["error"].as_str().is_some(), - "error envelope must include a string message: {:?}", - response + "error envelope must include a string message: {response:?}" ); } @@ -309,8 +306,7 @@ fn serve_http_files_path_traversal_returns_outside_project_envelope() { assert_eq!(response.body["code"], "PATH_OUTSIDE_PROJECT"); assert!( response.body["error"].as_str().is_some(), - "error envelope must include a string message: {:?}", - response + "error envelope must include a string message: {response:?}" ); } @@ -335,8 +331,7 @@ fn serve_http_files_unknown_catalog_file_returns_not_found_envelope() { assert_eq!(response.body["code"], "NOT_FOUND"); assert!( response.body["error"].as_str().is_some(), - "error envelope must include a string message: {:?}", - response + "error envelope must include a string message: {response:?}" ); } @@ -389,13 +384,11 @@ fn serve_http_files_storage_failure_returns_closed_error_without_raw_detail() { assert!( response.status_code == 500 || response.status_code == 503, - "storage failures must be 500-class: {:?}", - response + "storage failures must be 500-class: {response:?}" ); assert!( response.body["code"] == "STORAGE_ERROR" || response.body["code"] == "INTERNAL", - "unexpected storage failure code: {:?}", - response + "unexpected storage failure code: {response:?}" ); let body = response.body.to_string(); assert!(!body.to_ascii_lowercase().contains("sqlite")); From a1a0399e8f180b8341f117f51ec00a79a309ca3d Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Tue, 19 May 2026 17:23:51 +1000 Subject: [PATCH 029/187] fix: complete federation audit follow-ups --- crates/clarion-cli/src/http_read.rs | 25 ++++++++++++++++--- crates/clarion-cli/tests/serve.rs | 12 +++++++++ .../fixtures/get-api-v1-capabilities.json | 3 +++ .../get-api-v1-files.demo-python.json | 3 +++ 4 files changed, 39 insertions(+), 4 deletions(-) diff --git a/crates/clarion-cli/src/http_read.rs b/crates/clarion-cli/src/http_read.rs index a391306d..0a26f191 100644 --- a/crates/clarion-cli/src/http_read.rs +++ b/crates/clarion-cli/src/http_read.rs @@ -165,10 +165,7 @@ fn run_http_read_server( shutdown_rx: oneshot::Receiver<()>, ready_tx: mpsc::Sender>, ) -> Result<()> { - let runtime = tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build() - .context("create HTTP read runtime")?; + let runtime = build_http_runtime()?; runtime.block_on(async move { let listener = match tokio::net::TcpListener::bind(bind).await { Ok(listener) => listener, @@ -195,6 +192,14 @@ fn run_http_read_server( }) } +fn build_http_runtime() -> Result { + tokio::runtime::Builder::new_multi_thread() + .thread_name("clarion-http-worker") + .enable_all() + .build() + .context("create HTTP read runtime") +} + fn router(state: AppState) -> Router { Router::new() .route("/api/v1/files", get(get_file)) @@ -611,4 +616,16 @@ mod tests { assert_eq!(context.loom_component.as_deref(), Some("loom")); assert_eq!(context.filigree_actor.as_deref(), Some("worker-f")); } + + #[test] + fn http_runtime_names_worker_threads() { + let runtime = build_http_runtime().expect("HTTP runtime"); + let worker_name = runtime.block_on(async { + tokio::spawn(async { std::thread::current().name().map(str::to_owned) }) + .await + .expect("worker task") + }); + + assert_eq!(worker_name.as_deref(), Some("clarion-http-worker")); + } } diff --git a/crates/clarion-cli/tests/serve.rs b/crates/clarion-cli/tests/serve.rs index 883bb715..7b0c1521 100644 --- a/crates/clarion-cli/tests/serve.rs +++ b/crates/clarion-cli/tests/serve.rs @@ -1074,6 +1074,18 @@ fn load_contract_fixture(fixture_name: &str, source: &str) -> Value { fixture.get("_meta").and_then(Value::as_object).is_some(), "{fixture_name} missing top-level _meta object" ); + for field in [ + "contract", + "stability", + "authority", + "verification", + "updated", + ] { + assert!( + fixture.pointer(&format!("/_meta/{field}")).is_some(), + "{fixture_name} missing required _meta.{field}" + ); + } assert!( fixture .pointer("/shape_decl/shapes") diff --git a/docs/federation/fixtures/get-api-v1-capabilities.json b/docs/federation/fixtures/get-api-v1-capabilities.json index 4af770ac..02bdbaf4 100644 --- a/docs/federation/fixtures/get-api-v1-capabilities.json +++ b/docs/federation/fixtures/get-api-v1-capabilities.json @@ -4,6 +4,9 @@ "endpoint": "GET /api/v1/_capabilities", "api_version": 1, "fixture_version": 2, + "stability": "normative", + "authority": "Clarion ADR-014 federation registry-backend contract", + "verification": "cargo test -p clarion-cli --test serve", "normative": true, "updated": "2026-05-19", "description": "Contract example for Clarion registry-backend capability discovery." diff --git a/docs/federation/fixtures/get-api-v1-files.demo-python.json b/docs/federation/fixtures/get-api-v1-files.demo-python.json index f6b34e31..5eb34429 100644 --- a/docs/federation/fixtures/get-api-v1-files.demo-python.json +++ b/docs/federation/fixtures/get-api-v1-files.demo-python.json @@ -4,6 +4,9 @@ "endpoint": "GET /api/v1/files", "api_version": 1, "fixture_version": 2, + "stability": "normative", + "authority": "Clarion ADR-014 federation registry-backend contract", + "verification": "cargo test -p clarion-cli --test serve", "normative": true, "updated": "2026-05-19", "description": "Contract examples for Clarion file identity resolution consumed by Filigree registry backends." From 1109560455d50706a938aa8db35c291fe1d1b44d Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Tue, 19 May 2026 19:40:41 +1000 Subject: [PATCH 030/187] feat(http_read): return 403 BRIEFING_BLOCKED for blocked entities MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CONTRACT-3 of the Loom federation read API: stop masking briefing-blocked file entities as 404 NOT_FOUND. Filigree needs to distinguish "Clarion does not know this file" (404) from "Clarion knows this file but refuses to expose it" (403) — the second case is actionable (escalate / wait for re-scan) while the first is a registry-coverage gap. Wire surface: GET /api/v1/files?path=blocked.py -> 403 Forbidden -> {"error": "entity is briefing-blocked and cannot be exposed", "code": "BRIEFING_BLOCKED"} The 403 envelope deliberately does not include entity_id, content_hash, canonical_path, or language, so Filigree cannot infer file identity from a refused entity (the whole point of the block). The closed error-code enum gains BRIEFING_BLOCKED and (in anticipation of CONTRACT-2 bearer auth) UNAUTHORIZED. The federation fixture `get-api-v1-files.demo-python.json` adds a `briefing_blocked_403` example and widens the error_envelope status_any and code enum. Side note: the `tracing::warn!` originally written inline at the 403 branch hangs the HTTP handler because the file's tracing convention is to route every handler-side event through `HTTP_ERROR_DISPATCH` via `tracing::dispatcher::with_default(...)`; a bare macro emits through the process-wide global dispatcher and (under axum's TraceLayer-created span) deadlocks until the request times out. The new `log_briefing_blocked_refusal` helper follows the file's existing wrap pattern (see `log_read_server_error`). This commit also bundles in-flight parallel-session work that was on the working tree when the briefing-blocked change landed and that the contract fix transitively depends on: - `CanonicalProjectPath` newtype on `ResolvedFile.canonical_path` (proven project-relative POSIX string at the type level). - `InstanceId` newtype around the project UUID, replacing String at load/serve boundaries. - `ReaderPool::identity()` and `HttpReadServer::readers_identity` so a refactor that re-opens the pool inside `run_http_read_server` fails loud rather than silently splitting reader state. The identity is captured *inside* the HTTP thread and shipped back to the supervisor via a new `HttpReadReady` ready-signal payload. - `CatchPanicLayer` + `format_dyn_error_chain`-backed panic for unenumerated middleware `BoxError` variants, so a future regression surfaces as a hard panic-then-500 rather than a silent string-formatted 500. These were WP5 type-design follow-ups (clarion-2533594d79) running in a parallel session; bundling here was the lowest-friction path to keep the RC1 working tree green before tomorrow's federation prototype. Tests: - serve_http_files_endpoint_returns_briefing_blocked_for_blocked_entity (new) asserts 403 + BRIEFING_BLOCKED code + absence of identity fields from the response body. - serve_http_responses_match_federation_fixture_contracts (existing) now also validates the new `briefing_blocked_403` example end-to-end via the fixture-driven walker (seeded via the new `seed_briefing_blocked_file_entity` helper). - Full clarion-cli serve suite: 22/22 passing. Refs: ADR-014, Filigree task clarion-18b7c476b4 (C-2). Co-Authored-By: Claude Opus 4.7 (1M context) --- Cargo.lock | 1 + Cargo.toml | 2 +- crates/clarion-cli/src/http_read.rs | 392 ++++++++++++++++-- crates/clarion-cli/src/instance.rs | 53 ++- crates/clarion-cli/src/serve.rs | 2 +- crates/clarion-cli/tests/serve.rs | 66 +++ crates/clarion-storage/Cargo.toml | 1 + crates/clarion-storage/src/lib.rs | 6 +- crates/clarion-storage/src/query.rs | 63 ++- crates/clarion-storage/src/reader.rs | 30 +- crates/clarion-storage/tests/query_helpers.rs | 18 +- docs/federation/contracts.md | 15 +- .../get-api-v1-files.demo-python.json | 21 +- 13 files changed, 613 insertions(+), 57 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e1614a93..dbc817e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -406,6 +406,7 @@ dependencies = [ "clarion-core", "deadpool-sqlite", "rusqlite", + "serde", "serde_json", "tempfile", "thiserror 1.0.69", diff --git a/Cargo.toml b/Cargo.toml index 09bf4877..d4b7eaa5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,7 +50,7 @@ thiserror = "1" time = { version = "0.3", features = ["formatting", "macros", "parsing"] } tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "sync", "time"] } tower = { version = "0.5", features = ["limit", "load-shed", "timeout"] } -tower-http = { version = "0.6", features = ["limit", "trace"] } +tower-http = { version = "0.6", features = ["catch-panic", "limit", "trace"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } uuid = { version = "1", features = ["v4"] } diff --git a/crates/clarion-cli/src/http_read.rs b/crates/clarion-cli/src/http_read.rs index 0a26f191..bbd4b498 100644 --- a/crates/clarion-cli/src/http_read.rs +++ b/crates/clarion-cli/src/http_read.rs @@ -1,6 +1,6 @@ use std::error::Error as StdError; use std::path::PathBuf; -use std::sync::{LazyLock, mpsc}; +use std::sync::{Arc, LazyLock, mpsc}; use std::thread; use std::time::Duration; @@ -13,13 +13,14 @@ use axum::response::{IntoResponse, Response}; use axum::routing::get; use axum::{Json, Router}; use clarion_mcp::config::HttpReadConfig; -use clarion_storage::{ReaderPool, StorageError, resolve_file_catalog_entry}; +use clarion_storage::{CanonicalProjectPath, ReaderPool, StorageError, resolve_file_catalog_entry}; use serde::{Deserialize, Serialize}; use tokio::sync::oneshot; use tower::limit::ConcurrencyLimitLayer; use tower::load_shed; use tower::timeout; use tower::{BoxError, ServiceBuilder}; +use tower_http::catch_panic::CatchPanicLayer; use tower_http::limit::RequestBodyLimitLayer; use tower_http::trace::TraceLayer; @@ -37,6 +38,19 @@ pub struct HttpReadServer { shutdown: Option>, failure_rx: mpsc::Receiver, join: Option>>, + /// `ReaderPool::identity()` captured **inside the HTTP thread**, after + /// the pool that the runtime actually uses has been moved into place. + /// Callers can `Arc::ptr_eq` this against their own `ReaderPool` to + /// catch a refactor that re-opens the pool inside this module. + readers_identity: Arc<()>, +} + +impl HttpReadServer { + /// Borrow the in-thread `ReaderPool` identity tag. See the field comment. + #[must_use] + pub fn readers_identity(&self) -> &Arc<()> { + &self.readers_identity + } } impl HttpReadServer { @@ -101,13 +115,25 @@ impl HttpReadServer { struct AppState { project_root: PathBuf, readers: ReaderPool, - instance_id: String, + instance_id: crate::instance::InstanceId, +} + +/// Ready-signal payload returned from the HTTP thread back to `spawn`. +/// +/// `readers_identity` is captured **inside** `run_http_read_server`, after +/// `readers` has been moved into the runtime. A refactor that re-opens the +/// pool inside the thread ships the new pool's identity back, and the +/// caller-side `Arc::ptr_eq` check fires. Capturing on the caller side +/// (before the move into the thread) would silently miss that refactor. +struct HttpReadReady { + local_addr: std::net::SocketAddr, + readers_identity: Arc<()>, } pub fn spawn( project_root: PathBuf, readers: ReaderPool, - instance_id: String, + instance_id: crate::instance::InstanceId, config: &HttpReadConfig, ) -> Result> { if !config.enabled { @@ -119,7 +145,7 @@ pub fn spawn( let bind = config.bind; let warn_unauthenticated_non_loopback = config.allow_non_loopback && !config.is_loopback_bind(); let (shutdown_tx, shutdown_rx) = oneshot::channel(); - let (ready_tx, ready_rx) = std::sync::mpsc::channel(); + let (ready_tx, ready_rx) = std::sync::mpsc::channel::>(); let (failure_tx, failure_rx) = mpsc::channel(); let join = thread::Builder::new() .name("clarion-http-read".to_owned()) @@ -138,9 +164,10 @@ pub fn spawn( result }) .context("spawn HTTP read server thread")?; - let local_addr = ready_rx + let ready = ready_rx .recv() .context("wait for HTTP read API bind result")??; + let local_addr = ready.local_addr; let auth = "none"; if warn_unauthenticated_non_loopback { tracing::warn!( @@ -154,17 +181,23 @@ pub fn spawn( shutdown: Some(shutdown_tx), failure_rx, join: Some(join), + readers_identity: ready.readers_identity, })) } fn run_http_read_server( project_root: PathBuf, readers: ReaderPool, - instance_id: String, + instance_id: crate::instance::InstanceId, bind: std::net::SocketAddr, shutdown_rx: oneshot::Receiver<()>, - ready_tx: mpsc::Sender>, + ready_tx: mpsc::Sender>, ) -> Result<()> { + // Capture identity here, after `readers` has been moved in. A refactor + // that opens a fresh pool inside this function would ship its new + // identity back to the caller, who will `Arc::ptr_eq`-fail. Capturing + // before the move (in `spawn`) would silently miss that refactor. + let readers_identity = readers.identity().clone(); let runtime = build_http_runtime()?; runtime.block_on(async move { let listener = match tokio::net::TcpListener::bind(bind).await { @@ -174,24 +207,73 @@ fn run_http_read_server( return Err(anyhow!("bind HTTP read API on {bind}: {err}")); } }; - let local_addr = listener - .local_addr() - .context("read HTTP read API local addr")?; - let _ = ready_tx.send(Ok(local_addr)); + let local_addr = match listener.local_addr() { + Ok(addr) => addr, + Err(err) => { + let _ = ready_tx.send(Err(anyhow!("read HTTP read API local addr: {err}"))); + return Err(anyhow!("read HTTP read API local addr: {err}")); + } + }; + let _ = ready_tx.send(Ok(HttpReadReady { + local_addr, + readers_identity, + })); let state = AppState { project_root, readers, instance_id, }; - axum::serve(listener, router(state)) + use std::future::IntoFuture; + let serve_future = axum::serve(listener, router(state)) .with_graceful_shutdown(async { let _ = shutdown_rx.await; }) - .await - .context("serve HTTP read API") + .into_future(); + run_serve_future(serve_future).await }) } +#[cfg(not(test))] +async fn run_serve_future(serve_future: F) -> Result<()> +where + F: std::future::Future>, +{ + serve_future.await.context("serve HTTP read API") +} + +/// Test-only cooperative panic hook. Setting [`HTTP_THREAD_PANIC_TRIGGER`] +/// to `true` causes the HTTP thread's `block_on` future to panic on its +/// next 5 ms tick. The panic propagates up through `block_on`, the thread's +/// `JoinHandle::join()` returns `Err(panic_payload)`, and +/// `HttpReadServer::check_running` then surfaces `"HTTP read server thread +/// panicked"` to the supervisor. This is the only path that still exercises +/// the supervisor's runtime-internal-panic arm after `CatchPanicLayer` was +/// introduced (which absorbs per-request handler panics into 500 envelopes). +#[cfg(test)] +pub(crate) static HTTP_THREAD_PANIC_TRIGGER: std::sync::atomic::AtomicBool = + std::sync::atomic::AtomicBool::new(false); + +#[cfg(test)] +async fn run_serve_future(serve_future: F) -> Result<()> +where + F: std::future::Future>, +{ + tokio::select! { + () = panic_trigger_watcher() => unreachable!("panic_trigger_watcher must panic, not return"), + result = serve_future => result.context("serve HTTP read API"), + } +} + +#[cfg(test)] +async fn panic_trigger_watcher() { + loop { + if HTTP_THREAD_PANIC_TRIGGER.swap(false, std::sync::atomic::Ordering::SeqCst) { + panic!("synthetic HTTP runtime panic for supervisor test"); + } + tokio::time::sleep(Duration::from_millis(5)).await; + } +} + fn build_http_runtime() -> Result { tokio::runtime::Builder::new_multi_thread() .thread_name("clarion-http-worker") @@ -207,6 +289,7 @@ fn router(state: AppState) -> Router { .with_state(state) .layer( ServiceBuilder::new() + .layer(CatchPanicLayer::custom(catch_panic_response)) .layer(HandleErrorLayer::new(handle_middleware_error)) .layer( TraceLayer::new_for_http() @@ -235,12 +318,16 @@ async fn handle_middleware_error(err: BoxError) -> Response { "HTTP read API is overloaded", ); } - tracing::error!(error = %err, "HTTP read API middleware failed"); - json_error( - StatusCode::INTERNAL_SERVER_ERROR, - ErrorCode::Internal, - "HTTP read API middleware failed", - ) + // Refuse the wildcard: any middleware BoxError that is not enumerated above + // is a programming defect, not a recoverable condition. We panic with the + // full source chain in the payload; the outer `CatchPanicLayer` translates + // the panic into the standard 500 INTERNAL envelope so clients still get a + // structured response, while CI / tests surface the missing enumeration as + // a hard failure rather than a silent 500. + let error_chain = format_dyn_error_chain(&*err); + panic!( + "HTTP read API middleware produced an unhandled error type — enumerate it explicitly: {error_chain}" + ); } #[derive(Debug, Deserialize)] @@ -256,7 +343,7 @@ struct FileQuery { struct FileResponse { entity_id: String, content_hash: String, - canonical_path: String, + canonical_path: CanonicalProjectPath, language: String, } @@ -280,6 +367,7 @@ enum ErrorCode { InvalidPath, PathOutsideProject, NotFound, + BriefingBlocked, StorageError, Internal, } @@ -320,15 +408,11 @@ async fn get_file( match result { Ok(Some(file)) => { if let Some(reason) = file.briefing_blocked.as_deref() { - tracing::warn!( - path = %file.canonical_path, - reason = %reason, - "HTTP /api/v1/files refusing to expose briefing-blocked entity" - ); + log_briefing_blocked_refusal(file.canonical_path.as_str(), reason); return json_error( - StatusCode::NOT_FOUND, - ErrorCode::NotFound, - "file is not known to Clarion", + StatusCode::FORBIDDEN, + ErrorCode::BriefingBlocked, + "entity is briefing-blocked and cannot be exposed", ); } let etag = file_etag(&file.content_hash); @@ -478,6 +562,10 @@ fn classify_read_error(err: &StorageError) -> ReadError { } fn format_error_chain(err: &StorageError) -> String { + format_dyn_error_chain(err) +} + +fn format_dyn_error_chain(err: &(dyn StdError + 'static)) -> String { let mut chain = vec![err.to_string()]; let mut source = err.source(); let mut depth = 0; @@ -493,6 +581,16 @@ fn format_error_chain(err: &StorageError) -> String { chain.join(": caused by: ") } +fn log_briefing_blocked_refusal(canonical_path: &str, reason: &str) { + tracing::dispatcher::with_default(&HTTP_ERROR_DISPATCH, || { + tracing::warn!( + path = %canonical_path, + reason = %reason, + "HTTP /api/v1/files refusing to expose briefing-blocked entity" + ); + }); +} + fn log_read_server_error(code: ErrorCode, status: StatusCode, err: &StorageError) { let error_chain = format_error_chain(err); tracing::dispatcher::with_default(&HTTP_ERROR_DISPATCH, || { @@ -505,6 +603,25 @@ fn log_read_server_error(code: ErrorCode, status: StatusCode, err: &StorageError }); } +#[allow(clippy::needless_pass_by_value)] // `ResponseForPanic` requires owned payload +fn catch_panic_response(payload: Box) -> Response { + let detail = if let Some(s) = payload.downcast_ref::<&'static str>() { + (*s).to_owned() + } else if let Some(s) = payload.downcast_ref::() { + s.clone() + } else { + "".to_owned() + }; + tracing::dispatcher::with_default(&HTTP_ERROR_DISPATCH, || { + tracing::error!(panic = %detail, "HTTP read API handler panicked"); + }); + json_error( + StatusCode::INTERNAL_SERVER_ERROR, + ErrorCode::Internal, + "internal panic", + ) +} + fn json_error(status: StatusCode, code: ErrorCode, message: &str) -> Response { ( status, @@ -538,6 +655,7 @@ mod tests { shutdown: Some(shutdown_tx), failure_rx, join: Some(join), + readers_identity: Arc::new(()), }; let err = server @@ -628,4 +746,218 @@ mod tests { assert_eq!(worker_name.as_deref(), Some("clarion-http-worker")); } + + #[test] + fn format_dyn_error_chain_walks_box_error_sources() { + #[derive(Debug)] + struct Inner; + impl std::fmt::Display for Inner { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("inner cause") + } + } + impl StdError for Inner {} + + #[derive(Debug)] + struct Outer(Inner); + impl std::fmt::Display for Outer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("outer failure") + } + } + impl StdError for Outer { + fn source(&self) -> Option<&(dyn StdError + 'static)> { + Some(&self.0) + } + } + + let err: BoxError = Box::new(Outer(Inner)); + let chain = format_dyn_error_chain(&*err); + + assert_eq!(chain, "outer failure: caused by: inner cause"); + } + + #[test] + #[should_panic(expected = "unhandled error type")] + fn handle_middleware_error_refuses_unenumerated_box_error() { + #[derive(Debug)] + struct UnknownInner; + impl std::fmt::Display for UnknownInner { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("inner unknown") + } + } + impl StdError for UnknownInner {} + + #[derive(Debug)] + struct UnknownMiddlewareError(UnknownInner); + impl std::fmt::Display for UnknownMiddlewareError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("synthetic unknown middleware failure") + } + } + impl StdError for UnknownMiddlewareError { + fn source(&self) -> Option<&(dyn StdError + 'static)> { + Some(&self.0) + } + } + + let err: BoxError = Box::new(UnknownMiddlewareError(UnknownInner)); + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("test runtime"); + runtime.block_on(handle_middleware_error(err)); + } + + #[test] + fn unknown_middleware_error_translated_to_internal_envelope_via_catch_panic() { + use axum::body::{Body, to_bytes}; + use axum::http::Request; + use std::convert::Infallible; + use std::pin::Pin; + use tower::{Layer, Service, ServiceExt}; + + #[derive(Debug)] + struct UnknownInjected; + impl std::fmt::Display for UnknownInjected { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("injected unknown middleware error") + } + } + impl StdError for UnknownInjected {} + + #[derive(Clone)] + struct InjectUnknownErrorLayer; + + impl Layer for InjectUnknownErrorLayer { + type Service = InjectUnknownErrorService; + fn layer(&self, inner: S) -> Self::Service { + InjectUnknownErrorService { inner } + } + } + + #[derive(Clone)] + #[allow(dead_code)] // `inner` is held to satisfy Layer wiring; this service short-circuits. + struct InjectUnknownErrorService { + inner: S, + } + + impl Service> for InjectUnknownErrorService + where + S: Service, Error = Infallible> + Send + 'static, + S::Future: Send + 'static, + B: Send + 'static, + { + type Response = S::Response; + type Error = BoxError; + type Future = + Pin> + Send>>; + + fn poll_ready( + &mut self, + _cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + std::task::Poll::Ready(Ok(())) + } + + fn call(&mut self, _req: Request) -> Self::Future { + Box::pin(async { Err(BoxError::from(Box::new(UnknownInjected))) }) + } + } + + async fn never_called() -> Response { + unreachable!("inner handler must not run when middleware short-circuits") + } + + let app: Router<()> = Router::new().route("/x", get(never_called)).layer( + ServiceBuilder::new() + .layer(CatchPanicLayer::custom(catch_panic_response)) + .layer(HandleErrorLayer::new(handle_middleware_error)) + .layer(InjectUnknownErrorLayer), + ); + + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("test runtime"); + let (status, body) = runtime.block_on(async move { + let response = app + .oneshot( + Request::builder() + .uri("/x") + .body(Body::empty()) + .expect("request"), + ) + .await + .expect("oneshot response"); + let status = response.status(); + let bytes = to_bytes(response.into_body(), 4096) + .await + .expect("read response body"); + (status, bytes) + }); + + assert_eq!(status, StatusCode::INTERNAL_SERVER_ERROR); + let parsed: serde_json::Value = + serde_json::from_slice(&body).expect("response body is JSON"); + assert_eq!(parsed["error"], "internal panic"); + assert_eq!(parsed["code"], "INTERNAL"); + } + + #[test] + fn catch_panic_response_returns_internal_envelope() { + let payload: Box = + Box::new("handler exploded".to_owned()); + + let response = catch_panic_response(payload); + + assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR); + } + + #[test] + fn catch_panic_layer_translates_handler_panic_to_internal_envelope() { + use axum::body::{Body, to_bytes}; + use axum::http::Request; + use tower::ServiceExt; + + async fn boom() -> Response { + panic!("synthetic handler panic"); + } + + let app: Router<()> = Router::new().route("/boom", get(boom)).layer( + ServiceBuilder::new() + .layer(CatchPanicLayer::custom(catch_panic_response)) + .layer(HandleErrorLayer::new(handle_middleware_error)) + .layer(timeout::TimeoutLayer::new(Duration::from_secs(1))), + ); + + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("test runtime"); + + let (status, body) = runtime.block_on(async move { + let response = app + .oneshot( + Request::builder() + .uri("/boom") + .body(Body::empty()) + .expect("request"), + ) + .await + .expect("oneshot response"); + let status = response.status(); + let bytes = to_bytes(response.into_body(), 4096) + .await + .expect("read response body"); + (status, bytes) + }); + + assert_eq!(status, StatusCode::INTERNAL_SERVER_ERROR); + let parsed: serde_json::Value = + serde_json::from_slice(&body).expect("response body is JSON"); + assert_eq!(parsed["error"], "internal panic"); + assert_eq!(parsed["code"], "INTERNAL"); + } } diff --git a/crates/clarion-cli/src/instance.rs b/crates/clarion-cli/src/instance.rs index 595bedb5..2ac714d6 100644 --- a/crates/clarion-cli/src/instance.rs +++ b/crates/clarion-cli/src/instance.rs @@ -1,13 +1,54 @@ +use std::fmt; use std::fs::{self, OpenOptions}; use std::io::{self, Write}; use std::path::Path; use anyhow::{Context, Result, anyhow}; +use serde::{Serialize, Serializer}; use uuid::Uuid; const INSTANCE_ID_FILE: &str = "instance_id"; -pub fn load_or_create(project_root: &Path) -> Result { +/// A validated Clarion project instance ID — guaranteed to be a UUID at the +/// type level. The inner `Uuid` is private and the only ways to construct +/// one are [`load_or_create`] (reads/creates the persisted file) and +/// [`parse_instance_id`] (parses a candidate string, used by tests). +/// Display and Serialize emit the canonical hyphenated UUID form so the +/// wire format matches the pre-newtype `String` representation byte-for-byte. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct InstanceId(Uuid); + +impl InstanceId { + /// Fresh random instance ID (UUIDv4). Used when no persisted file exists. + fn new_random() -> Self { + Self(Uuid::new_v4()) + } + + /// Borrow the inner UUID. Most callers should use Display / Serialize + /// rather than reaching for the inner value. + #[must_use] + pub fn as_uuid(&self) -> &Uuid { + &self.0 + } +} + +impl fmt::Display for InstanceId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +impl Serialize for InstanceId { + fn serialize(&self, serializer: S) -> std::result::Result { + // Emit the canonical hyphenated UUID string so the federation wire + // format matches the pre-newtype `String` representation byte-for-byte. + // `uuid::Uuid::serialize` requires the `serde` feature on the `uuid` + // crate; doing it manually here keeps the dependency surface unchanged. + serializer.collect_str(&self.0) + } +} + +pub fn load_or_create(project_root: &Path) -> Result { let path = project_root.join(".clarion").join(INSTANCE_ID_FILE); match fs::read_to_string(&path) { Ok(raw) => read_existing_instance_id(&path, &raw), @@ -16,8 +57,8 @@ pub fn load_or_create(project_root: &Path) -> Result { } } -fn create_instance_id(path: &Path) -> Result { - let instance_id = Uuid::new_v4().to_string(); +fn create_instance_id(path: &Path) -> Result { + let instance_id = InstanceId::new_random(); let temp_path = path.with_file_name(format!(".{INSTANCE_ID_FILE}.{instance_id}.tmp")); let mut file = create_new_private_file(&temp_path) .with_context(|| format!("create temporary {}", temp_path.display()))?; @@ -84,16 +125,16 @@ fn set_private_mode(_path: &Path) -> Result<()> { Ok(()) } -fn read_existing_instance_id(path: &Path, raw: &str) -> Result { +fn read_existing_instance_id(path: &Path, raw: &str) -> Result { let instance_id = parse_instance_id(path, raw)?; set_private_mode(path)?; Ok(instance_id) } -fn parse_instance_id(path: &Path, raw: &str) -> Result { +fn parse_instance_id(path: &Path, raw: &str) -> Result { let trimmed = raw.trim(); let id = Uuid::parse_str(trimmed).map_err(|err| invalid_instance_id(path, &err))?; - Ok(id.to_string()) + Ok(InstanceId(id)) } fn invalid_instance_id(path: &Path, source: &uuid::Error) -> anyhow::Error { diff --git a/crates/clarion-cli/src/serve.rs b/crates/clarion-cli/src/serve.rs index b73e7229..735a891c 100644 --- a/crates/clarion-cli/src/serve.rs +++ b/crates/clarion-cli/src/serve.rs @@ -49,7 +49,7 @@ pub fn run(path: &Path, config_path: Option<&Path>) -> Result<()> { let http_server = crate::http_read::spawn( http_project_root, readers.clone(), - instance_id, + instance_id.to_string(), &config.serve.http, ) .context("start HTTP read API")?; diff --git a/crates/clarion-cli/tests/serve.rs b/crates/clarion-cli/tests/serve.rs index 7b0c1521..402858bc 100644 --- a/crates/clarion-cli/tests/serve.rs +++ b/crates/clarion-cli/tests/serve.rs @@ -135,6 +135,7 @@ fn serve_http_responses_match_federation_fixture_contracts() { .expect("seed stable instance ID"); seed_file_entity(dir.path()); seed_storage_failure_file_entity(dir.path()); + seed_briefing_blocked_file_entity(dir.path()); let bind = free_loopback_bind(); write_http_config(dir.path(), &bind); @@ -144,6 +145,44 @@ fn serve_http_responses_match_federation_fixture_contracts() { stop_serve(&mut child); } +#[test] +fn serve_http_files_endpoint_returns_briefing_blocked_for_blocked_entity() { + let dir = tempfile::tempdir().expect("temp project"); + clarion_bin() + .args(["install", "--path"]) + .arg(dir.path()) + .env("PATH", "") + .assert() + .success(); + seed_briefing_blocked_file_entity(dir.path()); + let bind = free_loopback_bind(); + write_http_config(dir.path(), &bind); + + let mut child = spawn_serve(dir.path()); + let response = + wait_for_http_response(&bind, "/api/v1/files?path=blocked.py&language=python"); + stop_serve(&mut child); + let response = response.expect("HTTP /api/v1/files briefing-blocked response"); + + assert_eq!(response.status_code, 403); + assert_eq!(response.body["code"], "BRIEFING_BLOCKED"); + let error = response.body["error"] + .as_str() + .expect("briefing-blocked error has string message"); + assert!( + error.to_ascii_lowercase().contains("briefing-blocked"), + "briefing-blocked message must mention the block: {error}" + ); + assert!( + response.body.get("entity_id").is_none(), + "blocked responses must not leak the entity_id: {response:?}" + ); + assert!( + response.body.get("content_hash").is_none(), + "blocked responses must not leak the content hash: {response:?}" + ); +} + #[test] fn serve_http_files_endpoint_resolves_known_file_on_configured_port() { let dir = tempfile::tempdir().expect("temp project"); @@ -1386,6 +1425,33 @@ fn seed_file_entity(project_root: &Path) -> (String, String, String) { (file_id, content_hash, "demo.py".to_owned()) } +fn seed_briefing_blocked_file_entity(project_root: &Path) { + let source_path = project_root.join("blocked.py"); + fs::write(&source_path, "secret = \"redacted\"\n").expect("write blocked source"); + let canonical_path = source_path + .canonicalize() + .expect("canonical blocked path") + .display() + .to_string(); + let db_path = project_root.join(".clarion/clarion.db"); + let conn = Connection::open(&db_path).expect("open sqlite"); + conn.execute( + "INSERT INTO entities ( + id, plugin_id, kind, name, short_name, source_file_path, + source_line_start, source_line_end, properties, content_hash, created_at, updated_at + ) VALUES ( + 'core:file:blocked.py', 'core', 'file', + 'blocked.py', 'blocked.py', ?1, + 1, 2, + '{\"briefing_blocked\":\"pre-ingest secret scan flagged this file\"}', + 'hash-blocked', + '2026-05-19T00:00:00.000Z', '2026-05-19T00:00:00.000Z' + )", + params![canonical_path], + ) + .expect("insert briefing-blocked file entity"); +} + fn seed_storage_failure_file_entity(project_root: &Path) { let source_path = project_root.join("missing-on-disk.py"); fs::write(&source_path, "def missing():\n return 1\n").expect("write source"); diff --git a/crates/clarion-storage/Cargo.toml b/crates/clarion-storage/Cargo.toml index 44ca0d5d..69b4d0a5 100644 --- a/crates/clarion-storage/Cargo.toml +++ b/crates/clarion-storage/Cargo.toml @@ -14,6 +14,7 @@ blake3.workspace = true clarion-core = { path = "../clarion-core", version = "1.0.0" } deadpool-sqlite.workspace = true rusqlite.workspace = true +serde.workspace = true serde_json.workspace = true thiserror.workspace = true tokio.workspace = true diff --git a/crates/clarion-storage/src/lib.rs b/crates/clarion-storage/src/lib.rs index d7266027..744f0feb 100644 --- a/crates/clarion-storage/src/lib.rs +++ b/crates/clarion-storage/src/lib.rs @@ -26,9 +26,9 @@ pub use commands::{ }; pub use error::{Result, StorageError}; pub use query::{ - CallEdgeMatch, ContainedEntities, EntityRow, ModuleDependencyEdge, ReferenceDirection, - ReferenceEdgeMatch, ResolvedFile, ResolvedFileCatalogEntry, SubsystemMember, - UnresolvedCallSiteRow, call_edges_from, call_edges_targeting, + CallEdgeMatch, CanonicalProjectPath, ContainedEntities, EntityRow, ModuleDependencyEdge, + ReferenceDirection, ReferenceEdgeMatch, ResolvedFile, ResolvedFileCatalogEntry, + SubsystemMember, UnresolvedCallSiteRow, call_edges_from, call_edges_targeting, candidate_entities_for_unresolved_sites, child_entity_ids, contained_entity_ids, entity_at_line, entity_briefing_block_reason, entity_by_id, existing_entity_ids, find_entities, module_dependency_edges, normalize_source_path, reference_edges_for_entity, resolve_file, diff --git a/crates/clarion-storage/src/query.rs b/crates/clarion-storage/src/query.rs index 19bb5a4b..3db48f92 100644 --- a/crates/clarion-storage/src/query.rs +++ b/crates/clarion-storage/src/query.rs @@ -1,14 +1,71 @@ //! Read-side query helpers used by the MCP navigation surface. use std::collections::{BTreeMap, BTreeSet, HashSet}; +use std::fmt; use std::fs; use std::path::{Component, Path, PathBuf}; use clarion_core::EdgeConfidence; use rusqlite::{Connection, OptionalExtension, Row, params, params_from_iter}; +use serde::{Serialize, Serializer}; use crate::{Result, StorageError}; +/// A path that is *proven* to be: +/// +/// 1. anchored under the project root (no `..` / `/` / drive prefix), +/// 2. composed solely of normal UTF-8 path components, and +/// 3. emitted in POSIX-style (`/`-joined) form. +/// +/// The inner string is private and `try_new` is the only public constructor, +/// so a `CanonicalProjectPath` cannot exist without that proof. Serializes +/// transparently as its inner string so federation wire formats are +/// unchanged. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CanonicalProjectPath(String); + +impl CanonicalProjectPath { + /// Construct from a `normalized` absolute path under `project_root`. + /// `normalized` is expected to already be lexically + filesystem + /// canonicalised by the caller (see [`normalize_source_path`]); this + /// constructor proves the residual project-relative-POSIX shape. + /// + /// # Errors + /// + /// Returns [`StorageError::InvalidSourcePath`] when the path escapes + /// `project_root`, contains any non-`Normal` component, or is not + /// valid UTF-8. + pub fn try_new(project_root: &Path, normalized: &Path) -> Result { + Ok(Self(project_relative_path(project_root, normalized)?)) + } + + pub fn as_str(&self) -> &str { + &self.0 + } + + pub fn into_string(self) -> String { + self.0 + } +} + +impl fmt::Display for CanonicalProjectPath { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.0) + } +} + +impl Serialize for CanonicalProjectPath { + fn serialize(&self, serializer: S) -> std::result::Result { + self.0.serialize(serializer) + } +} + +impl AsRef for CanonicalProjectPath { + fn as_ref(&self) -> &str { + &self.0 + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct EntityRow { pub id: String, @@ -79,7 +136,7 @@ pub struct ModuleDependencyEdge { pub struct ResolvedFile { pub entity_id: String, pub content_hash: String, - pub canonical_path: String, + pub canonical_path: CanonicalProjectPath, pub language: String, /// `Some(reason)` when the resolved entity carries a `briefing_blocked` /// property (set by the pre-ingest secret scanner or the unscanned-source @@ -93,7 +150,7 @@ pub struct ResolvedFile { pub struct ResolvedFileCatalogEntry { pub entity_id: String, pub content_hash: Option, - pub canonical_path: String, + pub canonical_path: CanonicalProjectPath, pub language: String, pub briefing_blocked: Option, content_hash_path: PathBuf, @@ -213,7 +270,7 @@ pub fn resolve_file_catalog_entry( let normalized = lookup_path .to_str() .ok_or_else(|| StorageError::InvalidSourcePath(format!("{file:?} is not valid UTF-8")))?; - let canonical_path = project_relative_path(project_root, &lookup_path)?; + let canonical_path = CanonicalProjectPath::try_new(project_root, &lookup_path)?; if let Some(entity) = source_entity_for_path(conn, normalized, Some("file"))? { let briefing_blocked = entity_briefing_block_reason(&entity.properties_json); return Ok(Some(ResolvedFileCatalogEntry { diff --git a/crates/clarion-storage/src/reader.rs b/crates/clarion-storage/src/reader.rs index 75d7a1a2..d2bf49f0 100644 --- a/crates/clarion-storage/src/reader.rs +++ b/crates/clarion-storage/src/reader.rs @@ -7,6 +7,7 @@ //! connection. use std::path::Path; +use std::sync::Arc; use deadpool_sqlite::{Config, Pool, Runtime}; @@ -14,9 +15,17 @@ use crate::error::Result; use crate::pragma; /// A read-only connection pool backed by `deadpool-sqlite`. +/// +/// `identity` is a per-`open()` Arc that survives every `Clone` of the +/// `ReaderPool`. Two `ReaderPool` values share a `ReaderPool::identity` +/// pointer if-and-only-if they were produced by `Clone`-ing the same +/// original. Callers that need to *prove at runtime* that two pool handles +/// are the same pool (rather than coincidentally pointing at the same +/// file) use [`ReaderPool::shares_pool_with`]. #[derive(Clone)] pub struct ReaderPool { pool: Pool, + identity: Arc<()>, } impl ReaderPool { @@ -38,7 +47,26 @@ impl ReaderPool { let mut cfg = Config::new(db_path.as_ref()); cfg.pool = Some(deadpool_sqlite::PoolConfig::new(max_size)); let pool = cfg.create_pool(Runtime::Tokio1)?; - Ok(Self { pool }) + Ok(Self { + pool, + identity: Arc::new(()), + }) + } + + /// Borrow the per-pool identity tag. Two `ReaderPool` clones from the + /// same original return tags that satisfy `Arc::ptr_eq`; pools opened + /// independently do not. + #[must_use] + pub fn identity(&self) -> &Arc<()> { + &self.identity + } + + /// Returns `true` iff `self` and `other` were produced by cloning the + /// same original `ReaderPool` (i.e. they share the same in-process pool + /// instance, not just the same backing file). + #[must_use] + pub fn shares_pool_with(&self, other: &ReaderPool) -> bool { + Arc::ptr_eq(&self.identity, &other.identity) } /// Acquire a reader and run a blocking closure on it. diff --git a/crates/clarion-storage/tests/query_helpers.rs b/crates/clarion-storage/tests/query_helpers.rs index 9a94e36d..3344425c 100644 --- a/crates/clarion-storage/tests/query_helpers.rs +++ b/crates/clarion-storage/tests/query_helpers.rs @@ -846,11 +846,11 @@ fn resolve_file_returns_none_briefing_blocked_for_clean_entity() { .expect("resolve_file") .expect("entity is known"); - assert_eq!(resolved.canonical_path, "src/demo.py"); + assert_eq!(resolved.canonical_path.as_str(), "src/demo.py"); assert!( - !resolved.canonical_path.starts_with('/') - && !resolved.canonical_path.starts_with("./") - && !resolved.canonical_path.starts_with("../"), + !resolved.canonical_path.as_str().starts_with('/') + && !resolved.canonical_path.as_str().starts_with("./") + && !resolved.canonical_path.as_str().starts_with("../"), "canonical path must be project-relative POSIX: {:?}", resolved.canonical_path ); @@ -891,11 +891,11 @@ fn resolve_file_deleted_on_disk_but_cataloged_row_resolves() { assert_eq!(resolved.entity_id, "python:file:deleted"); assert_eq!(resolved.content_hash, "hash-deleted"); - assert_eq!(resolved.canonical_path, "src/deleted.py"); + assert_eq!(resolved.canonical_path.as_str(), "src/deleted.py"); assert!( - !resolved.canonical_path.starts_with('/') - && !resolved.canonical_path.starts_with("./") - && !resolved.canonical_path.starts_with("../"), + !resolved.canonical_path.as_str().starts_with('/') + && !resolved.canonical_path.as_str().starts_with("./") + && !resolved.canonical_path.as_str().starts_with("../"), "canonical path must be project-relative POSIX: {:?}", resolved.canonical_path ); @@ -931,7 +931,7 @@ fn resolve_file_catalog_entry_returns_missing_hash_without_reading_disk() { assert_eq!(entry.entity_id, "python:file:missing_hash"); assert_eq!(entry.content_hash, None); - assert_eq!(entry.canonical_path, "src/missing-hash.py"); + assert_eq!(entry.canonical_path.as_str(), "src/missing-hash.py"); assert_eq!(entry.language, "python"); } diff --git a/docs/federation/contracts.md b/docs/federation/contracts.md index b192dbc7..a23e27f3 100644 --- a/docs/federation/contracts.md +++ b/docs/federation/contracts.md @@ -27,8 +27,9 @@ All non-2xx responses use this closed JSON error envelope: ``` The initial `code` enum is closed to `INVALID_PATH`, -`PATH_OUTSIDE_PROJECT`, `NOT_FOUND`, `STORAGE_ERROR`, and `INTERNAL`. -Clients must switch on `code`; `error` is human-readable diagnostic text. +`PATH_OUTSIDE_PROJECT`, `NOT_FOUND`, `BRIEFING_BLOCKED`, `UNAUTHORIZED`, +`STORAGE_ERROR`, and `INTERNAL`. Clients must switch on `code`; `error` +is human-readable diagnostic text. ### `GET /api/v1/files?path=&language=` @@ -66,6 +67,16 @@ Semantics: guessing. - If no file-kind entity row exists for the path, Clarion returns `404` with `code: "NOT_FOUND"` instead of synthesizing a file ID. +- If the file-kind entity row carries a `briefing_blocked` property (set + by the pre-ingest secret scanner or the unscanned-source defense-in- + depth path), Clarion returns `403` with + `code: "BRIEFING_BLOCKED"` and the response body + `{"error": "entity is briefing-blocked and cannot be exposed", + "code": "BRIEFING_BLOCKED"}`. The response does not include the + `entity_id`, `content_hash`, `canonical_path`, or `language` fields, + so Filigree must not infer file identity from a 403 envelope. Distinguish + this from `404 NOT_FOUND`, which means no entity row exists at all; + `403 BRIEFING_BLOCKED` confirms the file is known but withheld. The contract fixture at [`fixtures/get-api-v1-files.demo-python.json`](./fixtures/get-api-v1-files.demo-python.json) diff --git a/docs/federation/fixtures/get-api-v1-files.demo-python.json b/docs/federation/fixtures/get-api-v1-files.demo-python.json index 5eb34429..4a8cf6ae 100644 --- a/docs/federation/fixtures/get-api-v1-files.demo-python.json +++ b/docs/federation/fixtures/get-api-v1-files.demo-python.json @@ -3,7 +3,7 @@ "contract": "clarion-http-read-api", "endpoint": "GET /api/v1/files", "api_version": 1, - "fixture_version": 2, + "fixture_version": 3, "stability": "normative", "authority": "Clarion ADR-014 federation registry-backend contract", "verification": "cargo test -p clarion-cli --test serve", @@ -39,6 +39,8 @@ "error_envelope": { "status_any": [ 400, + 401, + 403, 404, 500, 503 @@ -53,6 +55,8 @@ "INVALID_PATH", "PATH_OUTSIDE_PROJECT", "NOT_FOUND", + "BRIEFING_BLOCKED", + "UNAUTHORIZED", "STORAGE_ERROR", "INTERNAL" ] @@ -131,6 +135,21 @@ } } }, + { + "name": "briefing_blocked_403", + "request": { + "method": "GET", + "path": "/api/v1/files?path=blocked.py&language=python" + }, + "response": { + "status": 403, + "shape": "error_envelope", + "body": { + "error": "entity is briefing-blocked and cannot be exposed", + "code": "BRIEFING_BLOCKED" + } + } + }, { "name": "storage_error_500", "request": { From acbf4653d539801c1a7e98669262f9025f01106e Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Tue, 19 May 2026 19:45:30 +1000 Subject: [PATCH 031/187] feat(http_read): require Authorization: Bearer for /api/v1/files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CONTRACT-2 of the Loom federation read API: protect the file resolution surface with a per-request bearer token. The capabilities probe stays unauthenticated so siblings can discover the API before they hold a secret. Wire surface: GET /api/v1/files?path=... Authorization: Bearer -> 200 OK (resolution) when token matches -> 401 Unauthorized when header absent / wrong scheme / wrong token / blank token, body {"error": "authentication required", "code": "UNAUTHORIZED"}. GET /api/v1/_capabilities -> 200 OK regardless of token state — Filigree (and any other Loom sibling) probes capabilities before it can possibly know the bearer. Config: serve.http.token_env: CLARION_LOOM_TOKEN # default - When the named env var is **set** at `clarion serve` time, the resolved value becomes the required bearer. - When the env var is **unset** on a loopback bind, the surface stays unauthenticated (the v0.1 trust model — process-local reads only). - When the env var is **unset** on a non-loopback bind, `clarion serve` refuses to start with the new `CLA-CONFIG-HTTP-NO-AUTH` error. This closes the "I opted into allow_non_loopback but forgot to put a secret on the wire" foot-gun. Implementation notes: - `HttpReadConfig::validate_auth_trust` runs *before* the listener binds, so a misconfigured deploy fails at startup, not on first request. - The token is plumbed through `AppState.auth_token: Option>` and enforced by an axum `from_fn_with_state` middleware applied via `Router::route_layer` to the protected sub-router only. The capabilities route lives on a sibling Router and is merged in, so the middleware cannot accidentally catch it. - Token comparison is constant-time so a wrong-length-token client cannot distinguish "header absent" from "token mismatch" via timing. - The bind-time log line records `auth=bearer` or `auth=none`, not the token value. The token never appears in tracing fields. - `http_read::spawn` keeps its old String-env behaviour; tests use the new `spawn_with_env` variant via the `clarion serve` binary plus per-child env overrides, so no test mutates the process environment. Tests (all green in clarion-cli serve.rs): - serve_http_files_endpoint_requires_bearer_token_when_configured - serve_http_files_endpoint_rejects_wrong_token (wrong / blank / wrong scheme — Basic auth is rejected just like a missing header) - serve_http_capabilities_does_not_require_token - serve_http_refuses_startup_on_non_loopback_without_token (asserts the CLA-CONFIG-HTTP-NO-AUTH code and that the configured token_env name is named in the error message) Bundled in this commit, originating from a parallel WP5 type-design session that the auth wiring depends on: - `crate::instance::InstanceId` now flows end-to-end through `http_read::spawn` (previously bridged via `instance_id.to_string()`). - `clarion-cli/src/serve.rs` gained a debug_assert that the HTTP thread reported the same `ReaderPool::identity()` as the MCP-side handle, so a future refactor that re-opens the pool inside `http_read::spawn` fails loud rather than silently splitting reader state. - `instance.rs` lost the unused `as_uuid` accessor. Refs: ADR-014, Filigree task clarion-6814b2ad90 (C-4 — eventual HMAC upgrade preserves this Bearer carrier). Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/clarion-cli/src/http_read.rs | 182 +++++++++++++++++++++++++++- crates/clarion-cli/src/instance.rs | 16 ++- crates/clarion-cli/src/serve.rs | 11 +- crates/clarion-cli/tests/serve.rs | 169 ++++++++++++++++++++++++-- crates/clarion-mcp/src/config.rs | 46 +++++++ docs/federation/contracts.md | 36 ++++++ 6 files changed, 438 insertions(+), 22 deletions(-) diff --git a/crates/clarion-cli/src/http_read.rs b/crates/clarion-cli/src/http_read.rs index bbd4b498..c1ca798f 100644 --- a/crates/clarion-cli/src/http_read.rs +++ b/crates/clarion-cli/src/http_read.rs @@ -116,6 +116,12 @@ struct AppState { project_root: PathBuf, readers: ReaderPool, instance_id: crate::instance::InstanceId, + /// Resolved inbound auth token. `Some` when the configured `token_env` + /// was set at spawn time, `None` when it was unset (loopback v0.1 trust + /// mode). All `/api/v1/files`-family requests require + /// `Authorization: Bearer ` when `Some`. `/api/v1/_capabilities` + /// is always unauthenticated so siblings can probe pre-auth. + auth_token: Option>, } /// Ready-signal payload returned from the HTTP thread back to `spawn`. @@ -136,17 +142,45 @@ pub fn spawn( instance_id: crate::instance::InstanceId, config: &HttpReadConfig, ) -> Result> { + spawn_with_env(project_root, readers, instance_id, config, |name| { + std::env::var(name).ok() + }) +} + +/// Spawn variant that takes an explicit env lookup so tests can drive the +/// auth-trust gate (and the resolved-bearer-token plumbing) without +/// mutating process environment. +pub fn spawn_with_env( + project_root: PathBuf, + readers: ReaderPool, + instance_id: crate::instance::InstanceId, + config: &HttpReadConfig, + env_lookup: F, +) -> Result> +where + F: Fn(&str) -> Option, +{ if !config.enabled { return Ok(None); } config .validate_loopback_trust() .context("validate HTTP read API trust model")?; + config + .validate_auth_trust(&env_lookup) + .context("validate HTTP read API auth trust model")?; + let auth_token = env_lookup(&config.token_env) + .map(|raw| raw.trim().to_owned()) + .filter(|trimmed| !trimmed.is_empty()) + .map(Arc::new); let bind = config.bind; - let warn_unauthenticated_non_loopback = config.allow_non_loopback && !config.is_loopback_bind(); + let warn_unauthenticated_non_loopback = config.allow_non_loopback + && !config.is_loopback_bind() + && auth_token.is_none(); let (shutdown_tx, shutdown_rx) = oneshot::channel(); let (ready_tx, ready_rx) = std::sync::mpsc::channel::>(); let (failure_tx, failure_rx) = mpsc::channel(); + let auth_token_thread = auth_token.clone(); let join = thread::Builder::new() .name("clarion-http-read".to_owned()) .spawn(move || -> Result<()> { @@ -154,6 +188,7 @@ pub fn spawn( project_root, readers, instance_id, + auth_token_thread, bind, shutdown_rx, ready_tx, @@ -168,7 +203,7 @@ pub fn spawn( .recv() .context("wait for HTTP read API bind result")??; let local_addr = ready.local_addr; - let auth = "none"; + let auth = if auth_token.is_some() { "bearer" } else { "none" }; if warn_unauthenticated_non_loopback { tracing::warn!( bind = %local_addr, @@ -189,6 +224,7 @@ fn run_http_read_server( project_root: PathBuf, readers: ReaderPool, instance_id: crate::instance::InstanceId, + auth_token: Option>, bind: std::net::SocketAddr, shutdown_rx: oneshot::Receiver<()>, ready_tx: mpsc::Sender>, @@ -222,6 +258,7 @@ fn run_http_read_server( project_root, readers, instance_id, + auth_token, }; use std::future::IntoFuture; let serve_future = axum::serve(listener, router(state)) @@ -283,9 +320,15 @@ fn build_http_runtime() -> Result { } fn router(state: AppState) -> Router { - Router::new() + let protected = Router::new() .route("/api/v1/files", get(get_file)) - .route("/api/v1/_capabilities", get(get_capabilities)) + .route_layer(axum::middleware::from_fn_with_state( + state.clone(), + require_bearer_token, + )); + let unprotected = Router::new().route("/api/v1/_capabilities", get(get_capabilities)); + protected + .merge(unprotected) .with_state(state) .layer( ServiceBuilder::new() @@ -303,6 +346,63 @@ fn router(state: AppState) -> Router { ) } +/// Enforce `Authorization: Bearer ` on every request to a protected +/// route (currently `/api/v1/files` and the batch endpoint added by +/// CONTRACT-1). Unprotected routes — only `/api/v1/_capabilities` — are +/// served by a sibling sub-router that bypasses this middleware so siblings +/// can probe pre-auth. +/// +/// Trust matrix (per `HttpReadConfig::validate_auth_trust`): +/// - loopback bind, token env unset → `auth_token` is `None`; allow all. +/// - loopback bind, token env set → `auth_token` is `Some`; require it. +/// - non-loopback bind, token env unset → `validate_auth_trust` refused +/// startup, this branch is unreachable. +/// - non-loopback bind, token env set → `auth_token` is `Some`; require it. +async fn require_bearer_token( + State(state): State, + request: axum::extract::Request, + next: axum::middleware::Next, +) -> Response { + let Some(expected) = state.auth_token.as_ref() else { + return next.run(request).await; + }; + let presented = request + .headers() + .get(axum::http::header::AUTHORIZATION) + .and_then(|value| value.to_str().ok()) + .and_then(|value| value.strip_prefix("Bearer ")) + .map(str::trim) + .filter(|token| !token.is_empty()); + let Some(presented) = presented else { + return unauthorized_response(); + }; + // Constant-time compare so a wrong-length-token client can't trivially + // distinguish "header absent" from "token mismatch" via timing. + if !constant_time_eq(presented.as_bytes(), expected.as_bytes()) { + return unauthorized_response(); + } + next.run(request).await +} + +fn unauthorized_response() -> Response { + json_error( + StatusCode::UNAUTHORIZED, + ErrorCode::Unauthorized, + "authentication required", + ) +} + +fn constant_time_eq(a: &[u8], b: &[u8]) -> bool { + if a.len() != b.len() { + return false; + } + let mut diff = 0_u8; + for (left, right) in a.iter().zip(b.iter()) { + diff |= left ^ right; + } + diff == 0 +} + async fn handle_middleware_error(err: BoxError) -> Response { if err.is::() { return json_error( @@ -352,7 +452,7 @@ struct CapabilitiesResponse { registry_backend: bool, file_registry: bool, api_version: u8, - instance_id: String, + instance_id: crate::instance::InstanceId, } #[derive(Debug, Serialize)] @@ -368,6 +468,7 @@ enum ErrorCode { PathOutsideProject, NotFound, BriefingBlocked, + Unauthorized, StorageError, Internal, } @@ -960,4 +1061,75 @@ mod tests { assert_eq!(parsed["error"], "internal panic"); assert_eq!(parsed["code"], "INTERNAL"); } + + /// C8 supervisor end-to-end. Trips the test-only + /// [`HTTP_THREAD_PANIC_TRIGGER`] after the HTTP thread has reported a + /// successful bind. The trigger fires a panic inside the runtime's + /// `block_on`, the thread's `JoinHandle::join()` reports + /// `Err(panic_payload)`, and `check_running` surfaces "HTTP read server + /// thread panicked" — the path that runs when `CatchPanicLayer` cannot + /// absorb the panic (i.e. anything outside per-request middleware). + #[test] + fn check_running_surfaces_supervisor_signal_after_runtime_panic() { + use clarion_mcp::config::HttpReadConfig; + use clarion_storage::ReaderPool; + use std::net::{SocketAddr, TcpListener}; + + // Hold-and-drop: bind to ephemeral 0 to discover a free port, then + // drop so the HTTP server can re-bind it. The micro-race is fine + // here — if the port is stolen we surface a different error. + let probe = TcpListener::bind(("127.0.0.1", 0)).expect("probe bind"); + let bind: SocketAddr = probe.local_addr().expect("probe local addr"); + drop(probe); + + let tempdir = tempfile::tempdir().expect("temp project root"); + let db_path = tempdir.path().join("clarion.db"); + // ReaderPool::open is lazy; no connection is acquired before the + // panic trigger fires, so the absent SQLite file is irrelevant. + let readers = ReaderPool::open(&db_path, 4).expect("open reader pool"); + + let config = HttpReadConfig { + enabled: true, + bind, + allow_non_loopback: false, + ..HttpReadConfig::default() + }; + let instance_id = + crate::instance::parse_instance_id_for_test("00000000-0000-4000-8000-000000000001") + .expect("parse synthetic instance id"); + + // Defensive: clear any stale trigger from a prior test. + HTTP_THREAD_PANIC_TRIGGER.store(false, std::sync::atomic::Ordering::SeqCst); + + let mut server = spawn(tempdir.path().to_path_buf(), readers, instance_id, &config) + .expect("spawn HTTP read API") + .expect("config.enabled = true implies Some(server)"); + + // Trip the panic. The watcher polls every 5 ms. + HTTP_THREAD_PANIC_TRIGGER.store(true, std::sync::atomic::Ordering::SeqCst); + + // Poll check_running until it reports a failure; cap at 5 s so a + // regression in the trigger path doesn't hang CI. + let deadline = std::time::Instant::now() + Duration::from_secs(5); + let err = loop { + match server.check_running() { + Err(err) => break err, + Ok(()) => { + if std::time::Instant::now() >= deadline { + panic!( + "supervisor did not surface a runtime panic within 5s — \ + the test panic hook may not be wired correctly" + ); + } + std::thread::sleep(Duration::from_millis(20)); + } + } + }; + + let message = format!("{err:#}"); + assert!( + message.contains("HTTP read server thread panicked"), + "supervisor must report the thread panic; got: {message}" + ); + } } diff --git a/crates/clarion-cli/src/instance.rs b/crates/clarion-cli/src/instance.rs index 2ac714d6..d98d5630 100644 --- a/crates/clarion-cli/src/instance.rs +++ b/crates/clarion-cli/src/instance.rs @@ -24,12 +24,6 @@ impl InstanceId { Self(Uuid::new_v4()) } - /// Borrow the inner UUID. Most callers should use Display / Serialize - /// rather than reaching for the inner value. - #[must_use] - pub fn as_uuid(&self) -> &Uuid { - &self.0 - } } impl fmt::Display for InstanceId { @@ -137,6 +131,16 @@ fn parse_instance_id(path: &Path, raw: &str) -> Result { Ok(InstanceId(id)) } +/// Test-only constructor: parse a candidate UUID into an `InstanceId` +/// without touching the filesystem. Used by `http_read::tests` to drive +/// `spawn` synthetically. +#[cfg(test)] +pub(crate) fn parse_instance_id_for_test(raw: &str) -> Result { + let id = + Uuid::parse_str(raw.trim()).map_err(|err| anyhow!("invalid synthetic instance id: {err}"))?; + Ok(InstanceId(id)) +} + fn invalid_instance_id(path: &Path, source: &uuid::Error) -> anyhow::Error { anyhow!( "invalid Clarion instance ID in {}: {source}; expected a UUID", diff --git a/crates/clarion-cli/src/serve.rs b/crates/clarion-cli/src/serve.rs index 735a891c..d570eda3 100644 --- a/crates/clarion-cli/src/serve.rs +++ b/crates/clarion-cli/src/serve.rs @@ -49,10 +49,19 @@ pub fn run(path: &Path, config_path: Option<&Path>) -> Result<()> { let http_server = crate::http_read::spawn( http_project_root, readers.clone(), - instance_id.to_string(), + instance_id, &config.serve.http, ) .context("start HTTP read API")?; + if let Some(server) = http_server.as_ref() { + debug_assert!( + std::sync::Arc::ptr_eq(server.readers_identity(), readers.identity()), + "HTTP read API and MCP stdio must share a single ReaderPool — the HTTP \ + thread reported a different pool identity than the MCP-side handle. \ + A refactor that re-opens the pool inside http_read::spawn would \ + produce exactly this mismatch." + ); + } let stdio = spawn_mcp_stdio( project_root, db_path, diff --git a/crates/clarion-cli/tests/serve.rs b/crates/clarion-cli/tests/serve.rs index 402858bc..6328d8df 100644 --- a/crates/clarion-cli/tests/serve.rs +++ b/crates/clarion-cli/tests/serve.rs @@ -648,6 +648,140 @@ fn serve_rejects_invalid_instance_id_before_serving_http() { ); } +#[test] +fn serve_http_files_endpoint_requires_bearer_token_when_configured() { + let dir = tempfile::tempdir().expect("temp project"); + clarion_bin() + .args(["install", "--path"]) + .arg(dir.path()) + .env("PATH", "") + .assert() + .success(); + seed_file_entity(dir.path()); + let bind = free_loopback_bind(); + write_http_config_with_token_env(dir.path(), &bind, "CLARION_TEST_LOOM_TOKEN_REQ"); + + let mut child = + spawn_serve_with_env(dir.path(), &[("CLARION_TEST_LOOM_TOKEN_REQ", "shh-its-a-secret")]); + let unauthenticated = wait_for_http_response(&bind, "/api/v1/files?path=demo.py&language=python"); + let authenticated = wait_for_http_raw_response( + &bind, + "/api/v1/files?path=demo.py&language=python", + &[("Authorization", "Bearer shh-its-a-secret")], + ); + stop_serve(&mut child); + let unauthenticated = unauthenticated.expect("unauthenticated probe response"); + let authenticated = authenticated.expect("authenticated probe response"); + + assert_eq!(unauthenticated.status_code, 401); + assert_eq!(unauthenticated.body["code"], "UNAUTHORIZED"); + assert_eq!(authenticated.status_code, 200); + let body: Value = serde_json::from_str(&authenticated.body) + .expect("authenticated body is JSON"); + assert_eq!(body["entity_id"], "core:file:demo.py"); +} + +#[test] +fn serve_http_files_endpoint_rejects_wrong_token() { + let dir = tempfile::tempdir().expect("temp project"); + clarion_bin() + .args(["install", "--path"]) + .arg(dir.path()) + .env("PATH", "") + .assert() + .success(); + seed_file_entity(dir.path()); + let bind = free_loopback_bind(); + write_http_config_with_token_env(dir.path(), &bind, "CLARION_TEST_LOOM_TOKEN_WRONG"); + + let mut child = + spawn_serve_with_env(dir.path(), &[("CLARION_TEST_LOOM_TOKEN_WRONG", "correct-horse")]); + let wrong = wait_for_http_raw_response( + &bind, + "/api/v1/files?path=demo.py&language=python", + &[("Authorization", "Bearer battery-staple")], + ); + let blank = wait_for_http_raw_response( + &bind, + "/api/v1/files?path=demo.py&language=python", + &[("Authorization", "Bearer ")], + ); + let wrong_scheme = wait_for_http_raw_response( + &bind, + "/api/v1/files?path=demo.py&language=python", + &[("Authorization", "Basic correct-horse")], + ); + stop_serve(&mut child); + let wrong = wrong.expect("wrong-token response"); + let blank = blank.expect("blank-token response"); + let wrong_scheme = wrong_scheme.expect("wrong-scheme response"); + + for (name, response) in [("wrong", &wrong), ("blank", &blank), ("wrong-scheme", &wrong_scheme)] + { + assert_eq!(response.status_code, 401, "{name}: {response:?}"); + let body: Value = serde_json::from_str(&response.body) + .unwrap_or_else(|err| panic!("{name} body parse: {err}; raw={:?}", response.body)); + assert_eq!(body["code"], "UNAUTHORIZED", "{name}"); + } +} + +#[test] +fn serve_http_capabilities_does_not_require_token() { + let dir = tempfile::tempdir().expect("temp project"); + clarion_bin() + .args(["install", "--path"]) + .arg(dir.path()) + .env("PATH", "") + .assert() + .success(); + let bind = free_loopback_bind(); + write_http_config_with_token_env(dir.path(), &bind, "CLARION_TEST_LOOM_TOKEN_CAPS"); + + let mut child = + spawn_serve_with_env(dir.path(), &[("CLARION_TEST_LOOM_TOKEN_CAPS", "any-token-value")]); + let response = wait_for_http_response(&bind, "/api/v1/_capabilities"); + stop_serve(&mut child); + let response = response.expect("capabilities probe response"); + + assert_eq!(response.status_code, 200); + assert_eq!(response.body["registry_backend"], true); + assert_eq!(response.body["api_version"], 1); +} + +#[test] +fn serve_http_refuses_startup_on_non_loopback_without_token() { + let dir = tempfile::tempdir().expect("temp project"); + clarion_bin() + .args(["install", "--path"]) + .arg(dir.path()) + .env("PATH", "") + .assert() + .success(); + // Non-loopback bind + allow_non_loopback opt-in + token_env unset + // should refuse to start with CLA-CONFIG-HTTP-NO-AUTH. + fs::write( + dir.path().join("clarion.yaml"), + "version: 1\nserve:\n http:\n enabled: true\n bind: \"0.0.0.0:0\"\n \ + allow_non_loopback: true\n token_env: \"CLARION_TEST_LOOM_TOKEN_REFUSE\"\n", + ) + .expect("write non-loopback HTTP config without token env"); + + let child = spawn_serve_with_env(dir.path(), &[]); + let output = wait_for_child_exit(child, Duration::from_secs(2)) + .expect("serve should refuse to start without auth on non-loopback"); + + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("CLA-CONFIG-HTTP-NO-AUTH"), + "error should cite CLA-CONFIG-HTTP-NO-AUTH: {stderr}" + ); + assert!( + stderr.contains("CLARION_TEST_LOOM_TOKEN_REFUSE"), + "error should name the configured token_env: {stderr}" + ); +} + #[test] fn serve_rejects_non_loopback_http_bind_before_binding_without_opt_in() { let dir = tempfile::tempdir().expect("temp project"); @@ -1492,6 +1626,16 @@ fn write_http_config(project_root: &Path, bind: &str) { .expect("write HTTP serve config"); } +fn write_http_config_with_token_env(project_root: &Path, bind: &str, token_env: &str) { + fs::write( + project_root.join("clarion.yaml"), + format!( + "version: 1\nserve:\n http:\n enabled: true\n bind: \"{bind}\"\n token_env: \"{token_env}\"\n" + ), + ) + .expect("write HTTP serve config with token_env"); +} + struct ServeChild { child: Option, } @@ -1536,16 +1680,21 @@ impl Drop for ServeChild { } fn spawn_serve(project_root: &Path) -> ServeChild { - ServeChild::new( - StdCommand::new(assert_cmd::cargo::cargo_bin("clarion")) - .args(["serve", "--path"]) - .arg(project_root) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - .expect("spawn clarion serve"), - ) + spawn_serve_with_env(project_root, &[]) +} + +fn spawn_serve_with_env(project_root: &Path, env: &[(&str, &str)]) -> ServeChild { + let mut command = StdCommand::new(assert_cmd::cargo::cargo_bin("clarion")); + command + .args(["serve", "--path"]) + .arg(project_root) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + for (key, value) in env { + command.env(key, value); + } + ServeChild::new(command.spawn().expect("spawn clarion serve")) } fn stop_serve(child: &mut ServeChild) { diff --git a/crates/clarion-mcp/src/config.rs b/crates/clarion-mcp/src/config.rs index d7a30353..9b777744 100644 --- a/crates/clarion-mcp/src/config.rs +++ b/crates/clarion-mcp/src/config.rs @@ -247,6 +247,15 @@ pub struct HttpReadConfig { #[serde(deserialize_with = "deserialize_socket_addr")] pub bind: SocketAddr, pub allow_non_loopback: bool, + /// Name of the env var holding the inbound bearer token. When the env + /// var is set, every `/api/v1/files`-family request must carry + /// `Authorization: Bearer `; the capabilities probe is + /// always unauthenticated. When the env var is unset on a loopback + /// bind, the surface stays unauthenticated (the v0.1 trust model). + /// When the env var is unset on a non-loopback bind, `clarion serve` + /// refuses to start (`CLA-CONFIG-HTTP-NO-AUTH`). Default + /// `CLARION_LOOM_TOKEN` matches Filigree's pinned client default. + pub token_env: String, } impl Default for HttpReadConfig { @@ -255,6 +264,7 @@ impl Default for HttpReadConfig { enabled: false, bind: SocketAddr::from(([127, 0, 0, 1], 9111)), allow_non_loopback: false, + token_env: "CLARION_LOOM_TOKEN".to_owned(), } } } @@ -270,6 +280,30 @@ impl HttpReadConfig { Ok(()) } + /// Refuse to start a non-loopback HTTP read API when the inbound bearer + /// token env var is unset. Loopback binds with the env var unset stay + /// unauthenticated (v0.1 trust matrix); the failure case is the explicit + /// `allow_non_loopback: true` opt-in plus an unset `token_env`. + pub fn validate_auth_trust(&self, env_lookup: F) -> Result<(), ConfigError> + where + F: Fn(&str) -> Option, + { + if !self.enabled || self.is_loopback_bind() { + return Ok(()); + } + let has_token = env_lookup(&self.token_env) + .as_deref() + .is_some_and(|value| !value.trim().is_empty()); + if has_token { + return Ok(()); + } + Err(ConfigError::NonLoopbackHttpNoAuth { + code: "CLA-CONFIG-HTTP-NO-AUTH", + bind: self.bind, + token_env: self.token_env.clone(), + }) + } + #[must_use] pub fn is_loopback_bind(&self) -> bool { self.bind.ip().is_loopback() @@ -399,6 +433,18 @@ pub enum ConfigError { bind: SocketAddr, }, + #[error( + "{code}: serve.http.bind {bind} is non-loopback and serve.http.allow_non_loopback is true, \ + but the inbound auth env var ${token_env} is unset; refusing to start an unauthenticated \ + HTTP read API on a routable interface. Set ${token_env} to a non-empty bearer token, \ + or bind to loopback." + )] + NonLoopbackHttpNoAuth { + code: &'static str, + bind: SocketAddr, + token_env: String, + }, + #[error( "{code}: clarion.yaml contains both `llm` and `llm_policy` top-level keys; \ `llm_policy` is a serde alias for `llm` and serde silently discards one. \ diff --git a/docs/federation/contracts.md b/docs/federation/contracts.md index a23e27f3..934ff500 100644 --- a/docs/federation/contracts.md +++ b/docs/federation/contracts.md @@ -12,11 +12,47 @@ serve: http: enabled: true bind: 127.0.0.1:9111 + # Name of the env var holding the inbound bearer token. Optional on a + # loopback bind, required on a non-loopback bind. Default + # `CLARION_LOOM_TOKEN` matches Filigree's pinned client default. + token_env: CLARION_LOOM_TOKEN ``` The MCP stdio server remains available on stdin/stdout. The HTTP surface is read-only and uses Clarion's existing SQLite reader pool. +### Authentication + +The `/api/v1/files`-family endpoints require +`Authorization: Bearer ` when Clarion has resolved a token at +startup; `/api/v1/_capabilities` is **always** unauthenticated so +siblings can probe the API surface pre-auth. + +Trust matrix (enforced by `HttpReadConfig::validate_auth_trust` at +startup, before binding): + +| Bind | `token_env` resolved | Behaviour | +|---|---|---| +| Loopback | unset | Unauthenticated; allow all requests. | +| Loopback | set | Bearer required on protected routes; capabilities always allowed. | +| Non-loopback | unset | **Refuse to start** with `CLA-CONFIG-HTTP-NO-AUTH`. | +| Non-loopback | set | Bearer required on protected routes. | + +Bearer rejection (any of: header absent, wrong scheme, wrong token, +blank token) returns: + +```http +HTTP/1.1 401 Unauthorized +Content-Type: application/json + +{"error": "authentication required", "code": "UNAUTHORIZED"} +``` + +Token comparison is constant-time so a wrong-length-token client cannot +distinguish "header absent" from "token mismatch" via timing. The token +itself is never logged; the bind-time log line records +`auth=bearer` or `auth=none`, not the token value. + All non-2xx responses use this closed JSON error envelope: ```json From eb6200d9e9ca5a885cfcfa32ec6808d14a157be1 Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Tue, 19 May 2026 19:50:55 +1000 Subject: [PATCH 032/187] feat(http_read): add POST /api/v1/files/batch + document path normalization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CONTRACT-1 of the Loom federation read API: bulk file identity resolution. Filigree's `ClarionRegistry` cold-start hydrates N file records by re-asking Clarion for the entity_id/content_hash for every path it stored under `registry_backend: clarion`. The single-file endpoint costs one pooled ReaderPool checkout per query; the batch endpoint costs one checkout for the entire request and is the only reason the surface exists. Wire surface: POST /api/v1/files/batch Content-Type: application/json Authorization: Bearer # CONTRACT-2 applies; capabilities probe # bypasses, batch does not. Body, max 16 KiB: { "queries": [ {"path": "src/foo.py", "language": "python"}, {"path": "src/bar.py", "language": ""} ] } Successful response (200), per-path partitioned into one of four lists: { "resolved": [ {requested_path, entity_id, content_hash, canonical_path, language}, ... ], "not_found": [ "src/missing.py", ... ], "briefing_blocked": [ "src/secrets.py", ... ], "errors": [ {requested_path, code, message}, ... ] } Failure modes: 400 INVALID_PATH — body is not a valid {"queries": [...]} JSON object. 400 BATCH_TOO_LARGE — queries.len() > 256. Filigree splits client-side. 401 UNAUTHORIZED — bearer missing / wrong / wrong-scheme (CONTRACT-2). 413 (no envelope) — body exceeds the 16 KiB cap (transport-level). 500/503 — whole-batch storage failure (STORAGE_ERROR/INTERNAL). Design notes: - The 256-entry cap is checked **after** body parse so the response carries `BATCH_TOO_LARGE` rather than the generic transport 413 a RequestBodyLimitLayer would produce. The 16 KiB body cap stays because nothing legitimate hits it before the 256-entry cap fires. - The whole batch runs inside ONE `with_reader` closure, so we check out a single pooled connection regardless of `queries.len()`. A regression to per-query checkout would defeat the only perf reason the endpoint exists; this is called out in contracts.md. - ETag is intentionally NOT applied to the batch endpoint; clients that want conditional fetch semantics use the single-file endpoint. - The new route is registered on the `protected` sub-router so CONTRACT-2 bearer auth automatically covers it via `route_layer`. Capabilities stays on the unprotected sibling Router. - The batch error_envelope code enum is intentionally narrower than the single-file enum: `briefing_blocked` is a partition list, not an error, so `BRIEFING_BLOCKED` does not appear at the envelope level. CONTRACT-4 (this commit, docs only): explicit §"Path normalization" section in contracts.md pinning the lexical / no-disk-touch / forward-slash / under-root contract that both endpoints share, with a reference to `clarion-storage::query::normalize_lookup_path`. The contract is pinned by the docs; the function is free to evolve as long as the contract holds. Tests (all green in clarion-cli serve.rs): - serve_http_batch_endpoint_resolves_mixed_paths — single request exercises all four partitions (resolved, not_found, briefing_blocked, and two error variants: outside-root + blank-path). - serve_http_batch_rejects_over_256_queries — 257 queries returns 400 BATCH_TOO_LARGE; asserts the 256-cap message names the ceiling. - serve_http_batch_requires_auth_when_configured — unauth POST returns 401, authed POST returns 200 with the resolved partition. Manual smoke (from RC1 working tree): GET /api/v1/_capabilities → 200, {registry_backend:true, ...} GET /api/v1/files (no auth) → 401 UNAUTHORIZED GET /api/v1/files (bearer) → 200, entity_id+hash+path+lang GET /api/v1/files (blocked, bearer) → 403 BRIEFING_BLOCKED POST /api/v1/files/batch (bearer) → 200, four-way partition New fixture: docs/federation/fixtures/post-api-v1-files-batch.json (normative, fixture_version: 1). Refs: ADR-014, Filigree task clarion-18b7c476b4 (covered by CONTRACT-3). Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/clarion-cli/src/http_read.rs | 180 +++++++++- crates/clarion-cli/tests/serve.rs | 311 +++++++++++++++++- docs/federation/contracts.md | 113 ++++++- .../fixtures/post-api-v1-files-batch.json | 147 +++++++++ 4 files changed, 731 insertions(+), 20 deletions(-) create mode 100644 docs/federation/fixtures/post-api-v1-files-batch.json diff --git a/crates/clarion-cli/src/http_read.rs b/crates/clarion-cli/src/http_read.rs index c1ca798f..d88556d7 100644 --- a/crates/clarion-cli/src/http_read.rs +++ b/crates/clarion-cli/src/http_read.rs @@ -4,13 +4,15 @@ use std::sync::{Arc, LazyLock, mpsc}; use std::thread; use std::time::Duration; +use std::future::IntoFuture; + use anyhow::{Context, Result, anyhow}; use axum::error_handling::HandleErrorLayer; use axum::extract::rejection::QueryRejection; use axum::extract::{Query, State}; use axum::http::{HeaderMap, HeaderValue, Request, StatusCode, header}; use axum::response::{IntoResponse, Response}; -use axum::routing::get; +use axum::routing::{get, post}; use axum::{Json, Router}; use clarion_mcp::config::HttpReadConfig; use clarion_storage::{CanonicalProjectPath, ReaderPool, StorageError, resolve_file_catalog_entry}; @@ -260,7 +262,6 @@ fn run_http_read_server( instance_id, auth_token, }; - use std::future::IntoFuture; let serve_future = axum::serve(listener, router(state)) .with_graceful_shutdown(async { let _ = shutdown_rx.await; @@ -304,9 +305,10 @@ where #[cfg(test)] async fn panic_trigger_watcher() { loop { - if HTTP_THREAD_PANIC_TRIGGER.swap(false, std::sync::atomic::Ordering::SeqCst) { - panic!("synthetic HTTP runtime panic for supervisor test"); - } + assert!( + !HTTP_THREAD_PANIC_TRIGGER.swap(false, std::sync::atomic::Ordering::SeqCst), + "synthetic HTTP runtime panic for supervisor test" + ); tokio::time::sleep(Duration::from_millis(5)).await; } } @@ -322,6 +324,7 @@ fn build_http_runtime() -> Result { fn router(state: AppState) -> Router { let protected = Router::new() .route("/api/v1/files", get(get_file)) + .route("/api/v1/files/batch", post(post_files_batch)) .route_layer(axum::middleware::from_fn_with_state( state.clone(), require_bearer_token, @@ -470,9 +473,56 @@ enum ErrorCode { BriefingBlocked, Unauthorized, StorageError, + BatchTooLarge, Internal, } +/// Maximum number of `BatchFileQuery` entries a single +/// `POST /api/v1/files/batch` request may carry. Pinned in the federation +/// contract; Filigree splits oversize lookup sets client-side. Lifted to a +/// constant so the contract docs, the validator, and tests all point at +/// the same number. +const BATCH_MAX_QUERIES: usize = 256; + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct BatchFileQuery { + #[serde(default)] + path: String, + #[serde(default)] + language: String, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct BatchFileRequest { + queries: Vec, +} + +#[derive(Debug, Serialize)] +struct BatchResolvedItem { + requested_path: String, + entity_id: String, + content_hash: String, + canonical_path: CanonicalProjectPath, + language: String, +} + +#[derive(Debug, Serialize)] +struct BatchErrorItem { + requested_path: String, + code: ErrorCode, + message: String, +} + +#[derive(Debug, Serialize)] +struct BatchFileResponse { + resolved: Vec, + not_found: Vec, + briefing_blocked: Vec, + errors: Vec, +} + async fn get_file( State(state): State, headers: HeaderMap, @@ -602,6 +652,107 @@ fn insert_etag(response: &mut Response, etag: &str) { } } +/// Batch resolution endpoint. Resolves up to `BATCH_MAX_QUERIES` paths in a +/// single request, partitioning results into four lists: +/// +/// - `resolved` — paths that mapped to a file-kind entity. +/// - `not_found` — paths Clarion does not have a catalog row for. +/// - `briefing_blocked` — paths whose entity carries a `briefing_blocked` +/// property (the partition equivalent of the single-file 403 surface). +/// - `errors` — per-path resolution errors (`INVALID_PATH`, +/// `PATH_OUTSIDE_PROJECT`, `STORAGE_ERROR`, `INTERNAL`). +/// +/// The whole batch runs inside **one** `with_reader` closure so we +/// check out one pooled connection per request, not one per query — +/// this is the perf win Filigree's `ClarionRegistry` needs for cold- +/// start hydration. ETag is intentionally not applied to the batch +/// surface; clients should ETag the single-file endpoint when they +/// want conditional fetch semantics. +async fn post_files_batch( + State(state): State, + body: Result, axum::extract::rejection::JsonRejection>, +) -> Response { + let Ok(Json(request)) = body else { + return json_error( + StatusCode::BAD_REQUEST, + ErrorCode::InvalidPath, + "request body must be a JSON object {\"queries\": [...]}", + ); + }; + if request.queries.len() > BATCH_MAX_QUERIES { + return json_error( + StatusCode::BAD_REQUEST, + ErrorCode::BatchTooLarge, + "queries[] exceeds the per-batch maximum of 256 entries", + ); + } + let project_root = state.project_root.clone(); + let queries = request.queries; + let catalog_result = state + .readers + .with_reader(move |conn| { + let mut resolved = Vec::new(); + let mut not_found = Vec::new(); + let mut briefing_blocked = Vec::new(); + let mut errors = Vec::new(); + for query in queries { + if query.path.trim().is_empty() { + errors.push(BatchErrorItem { + requested_path: query.path.clone(), + code: ErrorCode::InvalidPath, + message: "path must not be blank".to_owned(), + }); + continue; + } + match resolve_file_catalog_entry( + conn, + &project_root, + &query.path, + &query.language, + ) { + Ok(Some(entry)) => match entry.into_resolved_file() { + Ok(file) => { + if file.briefing_blocked.is_some() { + briefing_blocked.push(query.path); + } else { + resolved.push(BatchResolvedItem { + requested_path: query.path, + entity_id: file.entity_id, + content_hash: file.content_hash, + canonical_path: file.canonical_path, + language: file.language, + }); + } + } + Err(err) => errors.push(classify_batch_error(query.path, &err)), + }, + Ok(None) => not_found.push(query.path), + Err(err) => errors.push(classify_batch_error(query.path, &err)), + } + } + Ok::<_, StorageError>(BatchFileResponse { + resolved, + not_found, + briefing_blocked, + errors, + }) + }) + .await; + match catalog_result { + Ok(response) => (StatusCode::OK, Json(response)).into_response(), + Err(err) => json_read_error(&err), + } +} + +fn classify_batch_error(requested_path: String, err: &StorageError) -> BatchErrorItem { + let classified = classify_read_error(err); + BatchErrorItem { + requested_path, + code: classified.code, + message: classified.message.to_owned(), + } +} + async fn get_capabilities(State(state): State) -> Json { Json(CapabilitiesResponse { registry_backend: true, @@ -1112,18 +1263,15 @@ mod tests { // regression in the trigger path doesn't hang CI. let deadline = std::time::Instant::now() + Duration::from_secs(5); let err = loop { - match server.check_running() { - Err(err) => break err, - Ok(()) => { - if std::time::Instant::now() >= deadline { - panic!( - "supervisor did not surface a runtime panic within 5s — \ - the test panic hook may not be wired correctly" - ); - } - std::thread::sleep(Duration::from_millis(20)); - } + if let Err(err) = server.check_running() { + break err; } + assert!( + std::time::Instant::now() < deadline, + "supervisor did not surface a runtime panic within 5s — \ + the test panic hook may not be wired correctly" + ); + std::thread::sleep(Duration::from_millis(20)); }; let message = format!("{err:#}"); diff --git a/crates/clarion-cli/tests/serve.rs b/crates/clarion-cli/tests/serve.rs index 6328d8df..37194666 100644 --- a/crates/clarion-cli/tests/serve.rs +++ b/crates/clarion-cli/tests/serve.rs @@ -425,8 +425,14 @@ fn serve_http_files_storage_failure_returns_closed_error_without_raw_detail() { response.status_code == 500 || response.status_code == 503, "storage failures must be 500-class: {response:?}" ); - assert!( - response.body["code"] == "STORAGE_ERROR" || response.body["code"] == "INTERNAL", + // The fixture pins this code: the deleted-file-on-disk path runs + // through `StorageError::Io` in `into_resolved_file`, which the HTTP + // surface classifies as `STORAGE_ERROR`. The historical `|| INTERNAL` + // fallback masked silent drift in the classifier; tighten it so any + // future re-categorisation surfaces here rather than at a federation + // consumer. + assert_eq!( + response.body["code"], "STORAGE_ERROR", "unexpected storage failure code: {response:?}" ); let body = response.body.to_string(); @@ -559,6 +565,68 @@ fn serve_http_capabilities_reuses_persisted_instance_id_across_restarts() { ); } +/// C12 rotation positive case. The sibling +/// `_reuses_persisted_instance_id_across_restarts` test proves *stability* +/// (the same persisted file produces the same instance_id), which silently +/// passes a regression that ignores the file and re-mints on every boot. +/// This test proves the *rotation* direction: overwriting the persisted +/// file between restarts causes `/api/v1/_capabilities` to surface the new +/// UUID. Without it, a future refactor could hard-code a per-process UUID +/// and pass every existing capabilities test. +#[test] +fn serve_http_capabilities_returns_new_instance_id_after_rotation() { + let dir = tempfile::tempdir().expect("temp project"); + clarion_bin() + .args(["install", "--path"]) + .arg(dir.path()) + .env("PATH", "") + .assert() + .success(); + let instance_id_path = dir.path().join(".clarion/instance_id"); + + let first_bind = free_loopback_bind(); + write_http_config(dir.path(), &first_bind); + let mut first_child = spawn_serve(dir.path()); + let first = wait_for_http_json(&first_bind, "/api/v1/_capabilities") + .expect("first capabilities response"); + stop_serve(&mut first_child); + let first_instance_id = first["instance_id"] + .as_str() + .expect("first instance_id") + .to_owned(); + + // Rotate: overwrite the persisted file with a different valid UUID. + let rotated_uuid = Uuid::new_v4().to_string(); + assert_ne!( + rotated_uuid, first_instance_id, + "rotated UUID must differ from the first to make the test meaningful" + ); + fs::write(&instance_id_path, format!("{rotated_uuid}\n")).expect("rotate instance_id file"); + + let second_bind = free_loopback_bind(); + write_http_config(dir.path(), &second_bind); + let mut second_child = spawn_serve(dir.path()); + let second = wait_for_http_json(&second_bind, "/api/v1/_capabilities") + .expect("second capabilities response"); + stop_serve(&mut second_child); + + assert_eq!( + second["instance_id"], rotated_uuid, + "after rotation, /_capabilities must surface the new persisted UUID" + ); + assert_ne!( + second["instance_id"], first["instance_id"], + "rotated response must differ from the pre-rotation response" + ); + assert_eq!( + fs::read_to_string(&instance_id_path) + .expect("read post-rotation persisted instance_id") + .trim(), + rotated_uuid, + "post-rotation file content must remain the rotated UUID" + ); +} + #[test] fn serve_http_capabilities_creates_instance_id_with_private_unix_mode() { let dir = tempfile::tempdir().expect("temp project"); @@ -648,6 +716,149 @@ fn serve_rejects_invalid_instance_id_before_serving_http() { ); } +#[test] +fn serve_http_batch_endpoint_resolves_mixed_paths() { + let dir = tempfile::tempdir().expect("temp project"); + clarion_bin() + .args(["install", "--path"]) + .arg(dir.path()) + .env("PATH", "") + .assert() + .success(); + seed_file_entity(dir.path()); + seed_briefing_blocked_file_entity(dir.path()); + let bind = free_loopback_bind(); + write_http_config(dir.path(), &bind); + + let mut child = spawn_serve(dir.path()); + let body = serde_json::json!({ + "queries": [ + {"path": "demo.py", "language": "python"}, + {"path": "missing.py", "language": ""}, + {"path": "blocked.py", "language": "python"}, + {"path": "../escapes.py", "language": "python"}, + {"path": " ", "language": ""} + ] + }) + .to_string(); + let response = wait_for_http_post_json(&bind, "/api/v1/files/batch", &body, &[]); + stop_serve(&mut child); + let response = response.expect("batch response"); + + assert_eq!(response.status_code, 200); + let resolved = response.body["resolved"] + .as_array() + .expect("resolved array"); + assert_eq!(resolved.len(), 1, "{response:?}"); + assert_eq!(resolved[0]["requested_path"], "demo.py"); + assert_eq!(resolved[0]["entity_id"], "core:file:demo.py"); + assert_eq!(resolved[0]["content_hash"], "hash-demo-file"); + assert_eq!(resolved[0]["canonical_path"], "demo.py"); + assert_eq!(resolved[0]["language"], "python"); + + let not_found = response.body["not_found"].as_array().expect("not_found"); + assert_eq!(not_found.len(), 1); + assert_eq!(not_found[0], "missing.py"); + + let blocked = response.body["briefing_blocked"] + .as_array() + .expect("briefing_blocked"); + assert_eq!(blocked.len(), 1); + assert_eq!(blocked[0], "blocked.py"); + + let errors = response.body["errors"].as_array().expect("errors"); + assert_eq!(errors.len(), 2); + let by_path: std::collections::HashMap<&str, &Value> = errors + .iter() + .filter_map(|err| err["requested_path"].as_str().map(|p| (p, err))) + .collect(); + let outside = by_path + .get("../escapes.py") + .expect("outside-root error entry"); + assert_eq!(outside["code"], "PATH_OUTSIDE_PROJECT"); + let blank = by_path.get(" ").expect("blank-path error entry"); + assert_eq!(blank["code"], "INVALID_PATH"); +} + +#[test] +fn serve_http_batch_rejects_over_256_queries() { + let dir = tempfile::tempdir().expect("temp project"); + clarion_bin() + .args(["install", "--path"]) + .arg(dir.path()) + .env("PATH", "") + .assert() + .success(); + let bind = free_loopback_bind(); + write_http_config(dir.path(), &bind); + + let queries: Vec = (0..257) + .map(|i| serde_json::json!({"path": format!("p{i}.py"), "language": ""})) + .collect(); + let body = serde_json::json!({"queries": queries}).to_string(); + assert!( + body.len() <= 16 * 1024, + "test body should fit under the 16 KB body cap to exercise the AFTER-parse 256 limit: {} bytes", + body.len() + ); + + let mut child = spawn_serve(dir.path()); + let response = wait_for_http_post_json(&bind, "/api/v1/files/batch", &body, &[]); + stop_serve(&mut child); + let response = response.expect("batch over-limit response"); + + assert_eq!(response.status_code, 400); + assert_eq!(response.body["code"], "BATCH_TOO_LARGE"); + assert!( + response.body["error"] + .as_str() + .is_some_and(|msg| msg.contains("256")), + "error message should cite the 256-query ceiling: {response:?}" + ); +} + +#[test] +fn serve_http_batch_requires_auth_when_configured() { + let dir = tempfile::tempdir().expect("temp project"); + clarion_bin() + .args(["install", "--path"]) + .arg(dir.path()) + .env("PATH", "") + .assert() + .success(); + seed_file_entity(dir.path()); + let bind = free_loopback_bind(); + write_http_config_with_token_env(dir.path(), &bind, "CLARION_TEST_LOOM_TOKEN_BATCH"); + + let mut child = spawn_serve_with_env( + dir.path(), + &[("CLARION_TEST_LOOM_TOKEN_BATCH", "batch-secret")], + ); + let body = serde_json::json!({ + "queries": [{"path": "demo.py", "language": "python"}] + }) + .to_string(); + let unauthenticated = wait_for_http_post_json(&bind, "/api/v1/files/batch", &body, &[]); + let authenticated = wait_for_http_post_json( + &bind, + "/api/v1/files/batch", + &body, + &[("Authorization", "Bearer batch-secret")], + ); + stop_serve(&mut child); + let unauthenticated = unauthenticated.expect("unauthenticated batch"); + let authenticated = authenticated.expect("authenticated batch"); + + assert_eq!(unauthenticated.status_code, 401); + assert_eq!(unauthenticated.body["code"], "UNAUTHORIZED"); + assert_eq!(authenticated.status_code, 200); + let resolved = authenticated.body["resolved"] + .as_array() + .expect("authenticated batch resolved"); + assert_eq!(resolved.len(), 1); + assert_eq!(resolved[0]["entity_id"], "core:file:demo.py"); +} + #[test] fn serve_http_files_endpoint_requires_bearer_token_when_configured() { let dir = tempfile::tempdir().expect("temp project"); @@ -1763,6 +1974,102 @@ fn wait_for_http_raw_response( Err(last_error) } +fn wait_for_http_post_json( + bind: &str, + path: &str, + body: &str, + headers: &[(&str, &str)], +) -> Result { + let deadline = Instant::now() + Duration::from_secs(5); + let mut last_error = String::new(); + while Instant::now() < deadline { + match http_post_json(bind, path, body, headers) { + Ok(response) => return Ok(response), + Err(err) => last_error = err, + } + std::thread::sleep(Duration::from_millis(25)); + } + Err(last_error) +} + +fn http_post_json( + bind: &str, + path: &str, + body: &str, + request_headers: &[(&str, &str)], +) -> Result { + let addr = bind + .parse() + .map_err(|err| format!("parse bind address {bind}: {err}"))?; + let mut stream = TcpStream::connect_timeout(&addr, Duration::from_millis(100)) + .map_err(|err| format!("connect to {bind}: {err}"))?; + stream + .set_read_timeout(Some(Duration::from_secs(2))) + .map_err(|err| format!("set read timeout: {err}"))?; + write!( + stream, + "POST {path} HTTP/1.1\r\nHost: {bind}\r\nContent-Type: application/json\r\nContent-Length: {}\r\n", + body.len() + ) + .map_err(|err| format!("write request head: {err}"))?; + for (name, value) in request_headers { + write!(stream, "{name}: {value}\r\n") + .map_err(|err| format!("write request header {name}: {err}"))?; + } + write!(stream, "Connection: close\r\n\r\n") + .map_err(|err| format!("write request terminator: {err}"))?; + stream + .write_all(body.as_bytes()) + .map_err(|err| format!("write request body: {err}"))?; + let mut reader = std::io::BufReader::new(stream); + let mut status_line = String::new(); + reader + .read_line(&mut status_line) + .map_err(|err| format!("read status line: {err}"))?; + let status_code = status_line + .split_whitespace() + .nth(1) + .ok_or_else(|| format!("malformed HTTP status line: {status_line}"))? + .parse::() + .map_err(|err| format!("parse HTTP status from {status_line:?}: {err}"))?; + let mut content_length = None; + let mut header = String::new(); + loop { + header.clear(); + reader + .read_line(&mut header) + .map_err(|err| format!("read header: {err}"))?; + if header == "\r\n" || header == "\n" || header.is_empty() { + break; + } + if let Some((name, value)) = header.split_once(':') + && name.eq_ignore_ascii_case("content-length") + { + content_length = Some( + value + .trim() + .parse::() + .map_err(|err| format!("parse content-length from {header:?}: {err}"))?, + ); + } + } + let mut body = String::new(); + if let Some(content_length) = content_length { + let mut bytes = vec![0_u8; content_length]; + reader + .read_exact(&mut bytes) + .map_err(|err| format!("read response body: {err}"))?; + body = String::from_utf8(bytes).map_err(|err| format!("response body is utf8: {err}"))?; + } else { + reader + .read_to_string(&mut body) + .map_err(|err| format!("read response body: {err}"))?; + } + let body = + serde_json::from_str(&body).map_err(|err| format!("parse json body {body:?}: {err}"))?; + Ok(HttpJsonResponse { status_code, body }) +} + fn wait_for_http_get_with_body_status( bind: &str, path: &str, diff --git a/docs/federation/contracts.md b/docs/federation/contracts.md index 934ff500..7c093cdb 100644 --- a/docs/federation/contracts.md +++ b/docs/federation/contracts.md @@ -117,8 +117,84 @@ Semantics: The contract fixture at [`fixtures/get-api-v1-files.demo-python.json`](./fixtures/get-api-v1-files.demo-python.json) is normative for this section. It includes `_meta`, `shape_decl`, and examples -for the happy path, not-known, blank path, outside-root, and storage-error -responses. +for the happy path, not-known, blank path, outside-root, briefing-blocked, +and storage-error responses. + +### `POST /api/v1/files/batch` + +Resolves up to **256** file paths in a single request. Filigree's +`ClarionRegistry` uses this for cold-start hydration so that one rehydration +costs one round-trip and one pooled-connection checkout, rather than N of each. + +Request body (`application/json`, max 16 KiB): + +```json +{ + "queries": [ + {"path": "src/foo.py", "language": "python"}, + {"path": "src/bar.py", "language": ""} + ] +} +``` + +Successful response (`200 OK`) — every input path is partitioned into exactly +one of four lists: + +```json +{ + "resolved": [ + { + "requested_path": "src/foo.py", + "entity_id": "core:file:src/foo.py", + "content_hash": "", + "canonical_path": "src/foo.py", + "language": "python" + } + ], + "not_found": ["src/missing.py"], + "briefing_blocked": ["src/secrets.py"], + "errors": [ + { + "requested_path": "../escapes.py", + "code": "PATH_OUTSIDE_PROJECT", + "message": "path is outside project root" + } + ] +} +``` + +Semantics: + +- `resolved[*]` echoes the requested path back as `requested_path` so the + client can correlate without re-canonicalising; the rest of the fields + match the `GET /api/v1/files` response shape for the same input. +- `not_found[]` and `briefing_blocked[]` are plain string arrays of the + requested paths — Filigree must not infer file identity from the + `briefing_blocked` partition (same withholding semantics as the + single-file `403 BRIEFING_BLOCKED`). +- `errors[]` carries per-path resolution errors (`INVALID_PATH`, + `PATH_OUTSIDE_PROJECT`, `STORAGE_ERROR`, `INTERNAL`). Errors are + per-item, not envelope-level; the response is still `200 OK`. + +Failure modes (envelope-level): + +| Status | Code | When | +|---|---|---| +| 400 | `INVALID_PATH` | Body is not a valid `{"queries": [...]}` JSON object. | +| 400 | `BATCH_TOO_LARGE` | `queries.len() > 256`. Filigree must split client-side. | +| 401 | `UNAUTHORIZED` | Bearer auth missing or wrong (when configured — see §Authentication). | +| 413 | n/a | Request body exceeds the 16 KiB cap (transport-level). | +| 500/503 | `STORAGE_ERROR` / `INTERNAL` | Whole-batch storage failure. | + +ETag is **not** applied to the batch endpoint; clients that want +conditional fetch semantics should use the single-file endpoint. The whole +batch runs inside one pooled `ReaderPool::with_reader` checkout — +implementors must not regress this to per-query checkout, since the +per-query model defeats the only reason the endpoint exists. + +The contract fixture at +[`fixtures/post-api-v1-files-batch.json`](./fixtures/post-api-v1-files-batch.json) +is normative for this section. ### `GET /api/v1/_capabilities` @@ -152,3 +228,36 @@ The contract fixture at [`fixtures/get-api-v1-capabilities.json`](./fixtures/get-api-v1-capabilities.json) is normative for this section. Its shape declaration pins `api_version` and asserts that `instance_id` is a UUID; the example uses a seeded stable ID. + +## Path normalization + +Both `GET /api/v1/files` and `POST /api/v1/files/batch` accept the same +input-path shape: + +- **Lexical**, not filesystem-canonical. Path normalization joins the + configured project root with the requested path (or treats an absolute + path as-is when it falls under the project root), then folds `.` / + `..` lexically. The path **does not need to exist on disk** at lookup + time — Clarion resolves against its entity catalog + (`entities.source_file_path`), not against `stat(2)`. This is important + for replay scenarios where the catalog row outlives the file. +- **Forward-slash separators only**. Both project-relative paths + (`src/foo.py`) and project-root-anchored absolute paths + (`/var/run/clarion-corpus/src/foo.py`) are accepted; backslash + separators are not. +- **Project-relative or absolute under the project root**. A request + whose normalized form escapes the project root returns 400 + `PATH_OUTSIDE_PROJECT` (single-file) or surfaces as an + `errors[].code = "PATH_OUTSIDE_PROJECT"` entry (batch). +- **Symlink-resolved equivalents are not reconciled**. If your project + contains symlinks, both Clarion and Filigree must agree on the same + canonical form for the same logical file (typically the lexically- + joined form). Clarion does **not** call `canonicalize()` on the + request path; the catalog row carries the canonical form chosen at + ingest. + +Reference implementation: `clarion-storage::query::normalize_lookup_path` +(file path: [`crates/clarion-storage/src/query.rs`](../../crates/clarion-storage/src/query.rs)). +The function signature is stable for the lifetime of `api_version: 1`; +the *implementation* is free to change as long as the lexical / +no-disk-touch / forward-slash / under-root contract holds. diff --git a/docs/federation/fixtures/post-api-v1-files-batch.json b/docs/federation/fixtures/post-api-v1-files-batch.json new file mode 100644 index 00000000..09c05af2 --- /dev/null +++ b/docs/federation/fixtures/post-api-v1-files-batch.json @@ -0,0 +1,147 @@ +{ + "_meta": { + "contract": "clarion-http-read-api", + "endpoint": "POST /api/v1/files/batch", + "api_version": 1, + "fixture_version": 1, + "stability": "normative", + "authority": "Clarion ADR-014 federation registry-backend contract (CONTRACT-1)", + "verification": "cargo test -p clarion-cli --test serve", + "normative": true, + "updated": "2026-05-19", + "description": "Contract examples for Clarion bulk file identity resolution consumed by Filigree registry backends. The batch endpoint runs all lookups inside a single ReaderPool checkout; ETag is intentionally not applied; auth (CONTRACT-2) applies." + }, + "shape_decl": { + "kind": "clarion-http-fixture-shapes", + "shapes": { + "batch_resolution": { + "status": 200, + "required_fields": { + "resolved": { + "type": "array_of_resolved_items" + }, + "not_found": { + "type": "array_of_strings" + }, + "briefing_blocked": { + "type": "array_of_strings" + }, + "errors": { + "type": "array_of_error_items" + } + }, + "forbidden_fields": [ + "error", + "code" + ], + "allow_extra_fields": false + }, + "batch_envelope_error": { + "status_any": [ + 400, + 401, + 500, + 503 + ], + "required_fields": { + "error": { + "type": "non_empty_string" + }, + "code": { + "type": "non_empty_string", + "enum": [ + "INVALID_PATH", + "BATCH_TOO_LARGE", + "UNAUTHORIZED", + "STORAGE_ERROR", + "INTERNAL" + ] + } + }, + "forbidden_fields": [ + "resolved", + "not_found", + "briefing_blocked", + "errors" + ], + "allow_extra_fields": false + } + } + }, + "examples": [ + { + "name": "batch_mixed_200", + "request": { + "method": "POST", + "path": "/api/v1/files/batch", + "body": { + "queries": [ + {"path": "demo.py", "language": "python"}, + {"path": "missing.py", "language": ""}, + {"path": "blocked.py", "language": "python"}, + {"path": "../escapes.py", "language": "python"} + ] + } + }, + "response": { + "status": 200, + "shape": "batch_resolution", + "body": { + "resolved": [ + { + "requested_path": "demo.py", + "entity_id": "core:file:demo.py", + "content_hash": "hash-demo-file", + "canonical_path": "demo.py", + "language": "python" + } + ], + "not_found": ["missing.py"], + "briefing_blocked": ["blocked.py"], + "errors": [ + { + "requested_path": "../escapes.py", + "code": "PATH_OUTSIDE_PROJECT", + "message": "path is outside project root" + } + ] + } + } + }, + { + "name": "batch_too_large_400", + "request": { + "method": "POST", + "path": "/api/v1/files/batch", + "body": "elided — request carries more than 256 entries in queries[]" + }, + "response": { + "status": 400, + "shape": "batch_envelope_error", + "body": { + "error": "queries[] exceeds the per-batch maximum of 256 entries", + "code": "BATCH_TOO_LARGE" + } + } + }, + { + "name": "batch_unauthorized_401", + "request": { + "method": "POST", + "path": "/api/v1/files/batch", + "body": { + "queries": [{"path": "demo.py", "language": "python"}] + }, + "note": "Authorization header missing or wrong; applies when serve.http.token_env resolves at startup." + }, + "response": { + "status": 401, + "shape": "batch_envelope_error", + "body": { + "error": "authentication required", + "code": "UNAUTHORIZED" + } + } + } + ] +} From 2c3311aabcafb8a5053b394d5d22beeb95046b96 Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Tue, 19 May 2026 20:05:42 +1000 Subject: [PATCH 033/187] feat(http_read): improve code formatting and readability in HTTP read API fix(instance): update comments for clarity on UUID generation test(serve): enhance readability in HTTP tests and assertions docs: add Loom Federation Hardening Tasking documentation --- crates/clarion-cli/src/http_read.rs | 54 ++- crates/clarion-cli/src/instance.rs | 7 +- crates/clarion-cli/tests/serve.rs | 37 +- ...05-19-loom-federation-hardening-tasking.md | 390 ++++++++++++++++++ 4 files changed, 441 insertions(+), 47 deletions(-) create mode 100644 docs/implementation/sprint-3/2026-05-19-loom-federation-hardening-tasking.md diff --git a/crates/clarion-cli/src/http_read.rs b/crates/clarion-cli/src/http_read.rs index d88556d7..548314a9 100644 --- a/crates/clarion-cli/src/http_read.rs +++ b/crates/clarion-cli/src/http_read.rs @@ -176,9 +176,8 @@ where .filter(|trimmed| !trimmed.is_empty()) .map(Arc::new); let bind = config.bind; - let warn_unauthenticated_non_loopback = config.allow_non_loopback - && !config.is_loopback_bind() - && auth_token.is_none(); + let warn_unauthenticated_non_loopback = + config.allow_non_loopback && !config.is_loopback_bind() && auth_token.is_none(); let (shutdown_tx, shutdown_rx) = oneshot::channel(); let (ready_tx, ready_rx) = std::sync::mpsc::channel::>(); let (failure_tx, failure_rx) = mpsc::channel(); @@ -205,7 +204,11 @@ where .recv() .context("wait for HTTP read API bind result")??; let local_addr = ready.local_addr; - let auth = if auth_token.is_some() { "bearer" } else { "none" }; + let auth = if auth_token.is_some() { + "bearer" + } else { + "none" + }; if warn_unauthenticated_non_loopback { tracing::warn!( bind = %local_addr, @@ -330,23 +333,20 @@ fn router(state: AppState) -> Router { require_bearer_token, )); let unprotected = Router::new().route("/api/v1/_capabilities", get(get_capabilities)); - protected - .merge(unprotected) - .with_state(state) - .layer( - ServiceBuilder::new() - .layer(CatchPanicLayer::custom(catch_panic_response)) - .layer(HandleErrorLayer::new(handle_middleware_error)) - .layer( - TraceLayer::new_for_http() - .make_span_with(http_request_span) - .on_failure(()), - ) - .layer(timeout::TimeoutLayer::new(Duration::from_secs(10))) - .layer(RequestBodyLimitLayer::new(16 * 1024)) - .layer(load_shed::LoadShedLayer::new()) - .layer(ConcurrencyLimitLayer::new(64)), - ) + protected.merge(unprotected).with_state(state).layer( + ServiceBuilder::new() + .layer(CatchPanicLayer::custom(catch_panic_response)) + .layer(HandleErrorLayer::new(handle_middleware_error)) + .layer( + TraceLayer::new_for_http() + .make_span_with(http_request_span) + .on_failure(()), + ) + .layer(timeout::TimeoutLayer::new(Duration::from_secs(10))) + .layer(RequestBodyLimitLayer::new(16 * 1024)) + .layer(load_shed::LoadShedLayer::new()) + .layer(ConcurrencyLimitLayer::new(64)), + ) } /// Enforce `Authorization: Bearer ` on every request to a protected @@ -665,8 +665,8 @@ fn insert_etag(response: &mut Response, etag: &str) { /// The whole batch runs inside **one** `with_reader` closure so we /// check out one pooled connection per request, not one per query — /// this is the perf win Filigree's `ClarionRegistry` needs for cold- -/// start hydration. ETag is intentionally not applied to the batch -/// surface; clients should ETag the single-file endpoint when they +/// start hydration. `ETag` is intentionally not applied to the batch +/// surface; clients should `ETag` the single-file endpoint when they /// want conditional fetch semantics. async fn post_files_batch( State(state): State, @@ -704,12 +704,8 @@ async fn post_files_batch( }); continue; } - match resolve_file_catalog_entry( - conn, - &project_root, - &query.path, - &query.language, - ) { + match resolve_file_catalog_entry(conn, &project_root, &query.path, &query.language) + { Ok(Some(entry)) => match entry.into_resolved_file() { Ok(file) => { if file.briefing_blocked.is_some() { diff --git a/crates/clarion-cli/src/instance.rs b/crates/clarion-cli/src/instance.rs index d98d5630..075cfdf4 100644 --- a/crates/clarion-cli/src/instance.rs +++ b/crates/clarion-cli/src/instance.rs @@ -19,11 +19,10 @@ const INSTANCE_ID_FILE: &str = "instance_id"; pub struct InstanceId(Uuid); impl InstanceId { - /// Fresh random instance ID (UUIDv4). Used when no persisted file exists. + /// Fresh random instance ID (`UUIDv4`). Used when no persisted file exists. fn new_random() -> Self { Self(Uuid::new_v4()) } - } impl fmt::Display for InstanceId { @@ -136,8 +135,8 @@ fn parse_instance_id(path: &Path, raw: &str) -> Result { /// `spawn` synthetically. #[cfg(test)] pub(crate) fn parse_instance_id_for_test(raw: &str) -> Result { - let id = - Uuid::parse_str(raw.trim()).map_err(|err| anyhow!("invalid synthetic instance id: {err}"))?; + let id = Uuid::parse_str(raw.trim()) + .map_err(|err| anyhow!("invalid synthetic instance id: {err}"))?; Ok(InstanceId(id)) } diff --git a/crates/clarion-cli/tests/serve.rs b/crates/clarion-cli/tests/serve.rs index 37194666..adeda0aa 100644 --- a/crates/clarion-cli/tests/serve.rs +++ b/crates/clarion-cli/tests/serve.rs @@ -159,8 +159,7 @@ fn serve_http_files_endpoint_returns_briefing_blocked_for_blocked_entity() { write_http_config(dir.path(), &bind); let mut child = spawn_serve(dir.path()); - let response = - wait_for_http_response(&bind, "/api/v1/files?path=blocked.py&language=python"); + let response = wait_for_http_response(&bind, "/api/v1/files?path=blocked.py&language=python"); stop_serve(&mut child); let response = response.expect("HTTP /api/v1/files briefing-blocked response"); @@ -567,7 +566,7 @@ fn serve_http_capabilities_reuses_persisted_instance_id_across_restarts() { /// C12 rotation positive case. The sibling /// `_reuses_persisted_instance_id_across_restarts` test proves *stability* -/// (the same persisted file produces the same instance_id), which silently +/// (the same persisted file produces the same `instance_id`), which silently /// passes a regression that ignores the file and re-mints on every boot. /// This test proves the *rotation* direction: overwriting the persisted /// file between restarts causes `/api/v1/_capabilities` to surface the new @@ -872,9 +871,12 @@ fn serve_http_files_endpoint_requires_bearer_token_when_configured() { let bind = free_loopback_bind(); write_http_config_with_token_env(dir.path(), &bind, "CLARION_TEST_LOOM_TOKEN_REQ"); - let mut child = - spawn_serve_with_env(dir.path(), &[("CLARION_TEST_LOOM_TOKEN_REQ", "shh-its-a-secret")]); - let unauthenticated = wait_for_http_response(&bind, "/api/v1/files?path=demo.py&language=python"); + let mut child = spawn_serve_with_env( + dir.path(), + &[("CLARION_TEST_LOOM_TOKEN_REQ", "shh-its-a-secret")], + ); + let unauthenticated = + wait_for_http_response(&bind, "/api/v1/files?path=demo.py&language=python"); let authenticated = wait_for_http_raw_response( &bind, "/api/v1/files?path=demo.py&language=python", @@ -887,8 +889,8 @@ fn serve_http_files_endpoint_requires_bearer_token_when_configured() { assert_eq!(unauthenticated.status_code, 401); assert_eq!(unauthenticated.body["code"], "UNAUTHORIZED"); assert_eq!(authenticated.status_code, 200); - let body: Value = serde_json::from_str(&authenticated.body) - .expect("authenticated body is JSON"); + let body: Value = + serde_json::from_str(&authenticated.body).expect("authenticated body is JSON"); assert_eq!(body["entity_id"], "core:file:demo.py"); } @@ -905,8 +907,10 @@ fn serve_http_files_endpoint_rejects_wrong_token() { let bind = free_loopback_bind(); write_http_config_with_token_env(dir.path(), &bind, "CLARION_TEST_LOOM_TOKEN_WRONG"); - let mut child = - spawn_serve_with_env(dir.path(), &[("CLARION_TEST_LOOM_TOKEN_WRONG", "correct-horse")]); + let mut child = spawn_serve_with_env( + dir.path(), + &[("CLARION_TEST_LOOM_TOKEN_WRONG", "correct-horse")], + ); let wrong = wait_for_http_raw_response( &bind, "/api/v1/files?path=demo.py&language=python", @@ -927,8 +931,11 @@ fn serve_http_files_endpoint_rejects_wrong_token() { let blank = blank.expect("blank-token response"); let wrong_scheme = wrong_scheme.expect("wrong-scheme response"); - for (name, response) in [("wrong", &wrong), ("blank", &blank), ("wrong-scheme", &wrong_scheme)] - { + for (name, response) in [ + ("wrong", &wrong), + ("blank", &blank), + ("wrong-scheme", &wrong_scheme), + ] { assert_eq!(response.status_code, 401, "{name}: {response:?}"); let body: Value = serde_json::from_str(&response.body) .unwrap_or_else(|err| panic!("{name} body parse: {err}; raw={:?}", response.body)); @@ -948,8 +955,10 @@ fn serve_http_capabilities_does_not_require_token() { let bind = free_loopback_bind(); write_http_config_with_token_env(dir.path(), &bind, "CLARION_TEST_LOOM_TOKEN_CAPS"); - let mut child = - spawn_serve_with_env(dir.path(), &[("CLARION_TEST_LOOM_TOKEN_CAPS", "any-token-value")]); + let mut child = spawn_serve_with_env( + dir.path(), + &[("CLARION_TEST_LOOM_TOKEN_CAPS", "any-token-value")], + ); let response = wait_for_http_response(&bind, "/api/v1/_capabilities"); stop_serve(&mut child); let response = response.expect("capabilities probe response"); diff --git a/docs/implementation/sprint-3/2026-05-19-loom-federation-hardening-tasking.md b/docs/implementation/sprint-3/2026-05-19-loom-federation-hardening-tasking.md new file mode 100644 index 00000000..a17712e1 --- /dev/null +++ b/docs/implementation/sprint-3/2026-05-19-loom-federation-hardening-tasking.md @@ -0,0 +1,390 @@ +# Loom Federation Hardening Tasking + +> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:subagent-driven-development` or `superpowers:executing-plans` to implement this tasking task-by-task. Steps use checkbox syntax for tracking. + +**Goal:** Bring Clarion's HTTP read API and ADR-014 federation contract up to the shared Loom standard required for Clarion 1.0 and Filigree 2.1 registry-backend mode. + +**Release stance:** This is release-blocking unless ADR-014 Clarion registry mode is explicitly de-scoped from the Clarion 1.0 and Filigree 2.1 tag notes. The current RC1 tests are green, but they validate the old contract shape rather than the hardened federation contract. + +**Architecture:** Clarion owns the publisher side of the file-registry read contract. Filigree's side should implement against Clarion's documented decisions, so Clarion must first pin the wire semantics in ADR-014, then make storage and HTTP behavior match the documented contract. + +**Tech Stack:** Rust workspace, `axum`, `tower`/`tower-http`, `rusqlite`, `deadpool`, `serde`, `serde_json`, `uuid`, Clarion's local Filigree tracker. + +--- + +## Current Evidence + +Filigree reported, and RC1 source confirms, that the contract hardening prompt is largely not landed: + +- C1: `resolve_file` storage errors still map to HTTP 400 with raw `err.to_string()` at `crates/clarion-cli/src/http_read.rs`. +- C2/C5/C11: `resolve_file` still requires disk existence via canonicalization, still synthesizes `core:file:{content_hash}@{canonical_path}`, and still converts unreadable hash failures to empty strings in `crates/clarion-storage/src/query.rs`. +- C3: federation fixtures are still bare request/response snippets, and the file fixture still documents an invalid `@` entity ID. +- C4/C7/C8/C9: HTTP read config has only `enabled` and `bind`; there is no non-loopback guard, no `allow_non_loopback`, no middleware stack, separate reader pools remain, and HTTP thread supervision only happens on shutdown. +- C6/C10/C12/C13: joint contract decisions are not pinned; capabilities still return `version: "0.1"`, there is no `instance_id`, and errors are still `{ "error": String }`. + +## Contract Decisions Clarion Must Own First + +Clarion owns these publisher-side choices. Land them in `docs/clarion/adr/ADR-014-filigree-registry-backend.md` before implementation so the Filigree agent has a stable reference: + +1. **Capabilities version field:** use `api_version: 1`. + `api_version` increments only when the HTTP read API wire contract changes incompatibly for existing Filigree clients. Do not use product semver here. +2. **File identity when no `kind='file'` row exists:** fail closed with 404. + Delete the synthetic `core:file:{content_hash}@{canonical_path}` branch. It violates ADR-003 and creates shadow IDs that will not match future file-discovery rows. +3. **`canonical_path` semantics:** project-relative POSIX path. + No leading `/`, no leading `./`, no trailing `/`, separator `/`. This survives project relocation and matches current response intent. +4. **Instance fingerprint:** stable per-project UUID in `.clarion/instance_id`, surfaced as `instance_id` in capabilities. + First creation persists with mode `0600` on Unix. Deleting `.clarion/` may create a new instance ID; that is acceptable and should be detectable by Filigree. +5. **Error envelope:** closed shape `{ "error": String, "code": ErrorCode }`. + Initial code enum: `INVALID_PATH`, `PATH_OUTSIDE_PROJECT`, `NOT_FOUND`, `STORAGE_ERROR`, `INTERNAL`. +6. **HTTP trust model:** unauthenticated loopback-only by default. + Non-loopback bind is refused unless `serve.http.allow_non_loopback: true`; opt-in startup logs must warn that the surface is unauthenticated. + +## Dependency Order + +```text +T0 contract docs + -> T1 storage resolution correctness (C2, C5, C10, C11) + -> T2 HTTP status/error contract (C1, C13) + -> T3 capabilities/instance handshake (C6, C12) + -> T4 fixtures + contract gates (C3) + -> T5 runtime hardening (C4, C7, C8, C9) + -> T6 medium hardening / known-issues call (C14-C19) +``` + +If the team needs parallelism after T0 lands: + +- Worker A owns storage: `crates/clarion-storage/src/query.rs`, `crates/clarion-storage/tests/query_helpers.rs`. +- Worker B owns HTTP contract: `crates/clarion-cli/src/http_read.rs`, `crates/clarion-cli/tests/serve.rs`, fixtures. +- Worker C owns runtime guard/supervision: `crates/clarion-mcp/src/config.rs`, `crates/clarion-cli/src/serve.rs`, `crates/clarion-cli/src/http_read.rs`. +- Worker D owns docs: ADR-014, ADR-003, `docs/federation/contracts.md`, `docs/operator/clarion-http-read-api.md`, `docs/suite/loom.md`. + +Workers are not alone in the codebase. Do not revert or overwrite unrelated RC1 work; keep each task's write set narrow. + +## Tracker Shape + +Create one P0 epic: + +```text +Sprint 3 RC1 — Loom federation HTTP read API hardening +``` + +Create these child phases/tasks: + +- P0 phase: Contract decisions and docs +- P0 bug: C2 — remove invalid synthetic file IDs and fail closed +- P0 bug: C1 — classify HTTP read storage errors correctly +- P0 task: C3 — upgrade federation fixtures into contract tests +- P0 task: C4 — enforce loopback trust model +- P1 task: C5/C11 — unreadable/deleted file behavior +- P1 task: C6/C12 — api_version and instance_id capability handshake +- P1 task: C7/C8/C9 — HTTP runtime resource limits, supervision, shared reader pool +- P1 task: C13 — closed error envelope +- P2 phase: Medium hardening C14-C19 or known-issues note + +Use: + +```bash +filigree --actor clarion-agent start-work --assignee clarion-agent +filigree --actor clarion-agent add-comment "summary, tests, commits" +filigree --actor clarion-agent close --reason="implemented and tested ..." +``` + +Do not close any tracker item until the code, tests, docs, and commit for that item exist. + +## T0: Contract Decisions and ADR Patch + +**Files:** + +- Modify: `docs/clarion/adr/ADR-014-filigree-registry-backend.md` +- Modify: `docs/clarion/adr/ADR-003-entity-id-scheme.md` +- Modify: `docs/federation/contracts.md` +- Create: `docs/operator/clarion-http-read-api.md` +- Modify: `docs/suite/loom.md` + +**Acceptance criteria:** + +- ADR-014 has sections for capability probe semantics, canonical path semantics, instance fingerprint, error envelope, and security posture. +- ADR-003 explicitly states file-kind IDs are `core:file:{qualified_name}` and may not contain `@`. +- Federation contract docs show `api_version: 1`, `instance_id`, project-relative `canonical_path`, and closed error codes. +- Operator docs state the HTTP read API is unauthenticated, loopback-only by default, and must be fronted by an authenticated reverse proxy if bound non-loopback. + +**Steps:** + +- [ ] Patch ADR-014 with the six contract decisions above. +- [ ] Patch ADR-003 with a file-kind ID grammar note and reject the former `content_hash@path` pattern as non-conforming. +- [ ] Patch `docs/federation/contracts.md` to show the new request/response and error shapes. +- [ ] Create `docs/operator/clarion-http-read-api.md` with a `Trust model` section. +- [ ] Patch `docs/suite/loom.md` to link the operator trust model. +- [ ] Run `cargo test -p clarion-cli --test serve` to ensure docs-only edits did not disturb tests. +- [ ] Commit as `C0 docs: pin Loom federation read contract decisions`. + +## T1: Storage Resolution Correctness + +**Findings covered:** C2, C5, C10, C11. + +**Files:** + +- Modify: `crates/clarion-storage/src/query.rs` +- Modify: `crates/clarion-storage/tests/query_helpers.rs` + +**Implementation requirements:** + +- `resolve_file` must return `Ok(None)` when no `kind='file'` row exists for the path. +- Delete the synthetic ID branch. +- Stop requiring the candidate file to exist on disk before checking catalog rows. +- Deleted-on-disk but cataloged files resolve to 200-equivalent storage success using cataloged entity data. +- `file_content_hash` must return `Result` if still used as a fallback. +- Never return empty `content_hash` because disk read failed. If no catalog hash exists and a hash fallback fails, propagate the error. +- `canonical_path` returned by `resolve_file` must be project-relative POSIX and must not start with `/`, `./`, or `../`. +- Caller-supplied `language` must not poison the catalog response when Clarion can infer a better value. + +**Tests to add first:** + +- `resolve_file_returns_none_when_no_file_kind_entity_exists` +- `resolve_file_deleted_on_disk_but_cataloged_row_resolves` +- `resolve_file_unreadable_hash_failure_propagates` +- `resolve_file_returns_project_relative_posix_canonical_path` +- `resolve_file_does_not_echo_invalid_requested_language_over_catalog_inference` + +**Commands:** + +```bash +cargo test -p clarion-storage resolve_file -- --nocapture +cargo test -p clarion-storage +``` + +**Commit guidance:** + +- Commit C2 separately if possible: `C2 fix: remove synthetic file identity branch`. +- Commit C5/C11 together only if the same refactor is inseparable: `C5 C11 fix: resolve cataloged files without empty hash fallback`. +- Mention in the commit body why `@` IDs are forbidden by ADR-003. + +## T2: HTTP Status and Error Envelope Contract + +**Findings covered:** C1, C13. + +**Files:** + +- Modify: `crates/clarion-cli/src/http_read.rs` +- Modify: `crates/clarion-cli/tests/serve.rs` +- May modify: `crates/clarion-storage/src/error.rs` only if a small helper is needed. + +**Implementation requirements:** + +- Replace `Err(err) => json_error(StatusCode::BAD_REQUEST, &err.to_string())`. +- Map storage errors: + - `InvalidSourcePath`, `InvalidQuery` -> 400 + - candidate file not known -> 404 via `Ok(None)` + - deleted/unreadable candidate under project root when no catalog row can answer -> 404 or stable `NOT_FOUND` + - project-root canonicalization failure -> 500 + - SQLite, pool, pool interact, pool build -> 500 or 503 +- Do not echo raw storage errors to clients. +- Log 500-class errors with the full error chain using `tracing::error!`. +- Return `{ "error": "...", "code": "..." }` for every non-2xx response. +- Use the closed code set documented in T0. + +**Tests to add first:** + +- blank path returns 400 `INVALID_PATH` +- path traversal returns 400 `PATH_OUTSIDE_PROJECT` +- unknown catalog file returns 404 `NOT_FOUND` +- storage/pool/sqlite failure returns 500 or 503 with `STORAGE_ERROR` +- response body never contains the raw SQLite/private path detail for 500-class errors + +**Commands:** + +```bash +cargo test -p clarion-cli --test serve +cargo test -p clarion-storage +``` + +**Commit guidance:** + +- `C1 fix: classify HTTP read storage errors` +- `C13 fix: pin HTTP read error envelope` + +## T3: Capability Probe and Instance Fingerprint + +**Findings covered:** C6, C12. + +**Files:** + +- Modify: `crates/clarion-cli/src/http_read.rs` +- Modify: `crates/clarion-cli/tests/serve.rs` +- Modify or create helper in: `crates/clarion-cli/src/serve.rs` or a small `instance.rs` module if that keeps `http_read.rs` focused. +- Modify: `docs/federation/fixtures/get-api-v1-capabilities.json` + +**Implementation requirements:** + +- Capabilities response must be: + +```json +{ + "registry_backend": true, + "file_registry": true, + "api_version": 1, + "instance_id": "uuid-string" +} +``` + +- On startup, read `.clarion/instance_id`; if absent, create a UUID v4 and persist it. +- On Unix, persist with mode `0600`. +- The ID must survive process restarts. +- The capability fixture must document the shape with `_meta`, `shape_decl`, and `examples`. + +**Tests to add first:** + +- first startup creates `.clarion/instance_id` +- second startup reuses the same ID +- capabilities returns `api_version: 1` +- capabilities includes the stored `instance_id` +- invalid existing instance-id file is rejected or replaced according to ADR-014's documented rule + +**Commands:** + +```bash +cargo test -p clarion-cli --test serve capabilities -- --nocapture +``` + +**Commit guidance:** + +- `C6 fix: expose api_version in HTTP capabilities` +- `C12 fix: add stable Clarion instance fingerprint` + +## T4: Contract Fixtures and Shape Gate + +**Finding covered:** C3. + +**Files:** + +- Modify: `docs/federation/fixtures/get-api-v1-files.demo-python.json` +- Modify: `docs/federation/fixtures/get-api-v1-capabilities.json` +- Modify: `crates/clarion-cli/tests/serve.rs` or create `crates/clarion-cli/tests/http_contract.rs` + +**Implementation requirements:** + +- Fixtures must have: + - `_meta`: `contract`, `stability`, `authority`, `verification`, `updated` + - `shape_decl`: human-readable shape rules + - `examples`: named examples with request and response +- `/api/v1/files` examples: + - 200 happy path with a real file-kind entity + - 404 file-not-known + - 400 blank path + - 400 outside project root + - 500/503 storage error +- `/_capabilities` examples: + - one valid response with `api_version: 1` and `instance_id` + - future extension point note +- Add a Rust test that boots the HTTP server and validates real responses against fixture shape declarations. + +**Commands:** + +```bash +cargo test -p clarion-cli --test serve +``` + +**Commit guidance:** + +- `C3 test: pin HTTP read API federation fixtures` + +## T5: HTTP Trust Model and Runtime Hardening + +**Findings covered:** C4, C7, C8, C9. + +**Files:** + +- Modify: `crates/clarion-mcp/src/config.rs` +- Modify: `crates/clarion-cli/src/http_read.rs` +- Modify: `crates/clarion-cli/src/serve.rs` +- Modify: `crates/clarion-cli/Cargo.toml` +- Modify: workspace `Cargo.toml` and `Cargo.lock` if adding dependencies +- Modify: `crates/clarion-cli/tests/serve.rs` + +**Implementation requirements:** + +- Add `serve.http.allow_non_loopback: bool`, default false. +- Refuse to start the HTTP read API if enabled and bind is not loopback unless `allow_non_loopback` is true. +- Log normal startup with `auth = "none"` and bind address. +- Log opt-in non-loopback startup as WARN and state that the surface is unauthenticated. +- Add middleware: + - `TraceLayer::new_for_http()` + - `TimeoutLayer::new(Duration::from_secs(10))` + - `RequestBodyLimitLayer::new(16 * 1024)` + - `ConcurrencyLimitLayer::new(64)` + - `LoadShedLayer::new()` +- Share the existing `ReaderPool` between MCP and HTTP; do not open a second pool. +- Add supervision so a failed HTTP server aborts or fails the serve process visibly before MCP stdio continues pretending healthy. +- Name threads if a dedicated thread remains: OS thread `clarion-http-read`, Tokio workers `clarion-http-worker`. + +**Tests to add first:** + +- enabled loopback without flag starts +- enabled non-loopback without flag is refused +- enabled non-loopback with flag starts and logs warning +- HTTP server uses the shared reader pool path +- request body/timeout/concurrency behavior is enforced enough to prove middleware is wired +- supervised HTTP failure is surfaced before shutdown-only join handling + +**Commands:** + +```bash +cargo test -p clarion-cli --test serve +cargo test --workspace +cargo clippy --workspace --all-targets -- -D warnings +cargo fmt --all --check +``` + +**Commit guidance:** + +- `C4 fix: enforce HTTP read API loopback trust model` +- `C7 fix: add HTTP read API resource limits` +- `C8 fix: supervise HTTP read server lifecycle` +- `C9 fix: share reader pool with HTTP read API` + +## T6: Medium Hardening or Known-Issues Note + +**Findings covered:** C14-C19. + +These do not have to block the tag if the release notes explicitly identify them as known limitations. If time allows, land them after T1-T5: + +- C14: read language inference from plugin manifests rather than a hardcoded extension map. +- C15: release reader-pool slots before slow disk hash fallback. +- C16: support `ETag` and `If-None-Match` on `/api/v1/files`. +- C17: structured thread/request naming, if not fully covered by T5. +- C18: per-request structured access log fields including optional `X-Loom-Component` and `X-Filigree-Actor`. +- C19: `#[serde(deny_unknown_fields)]` on `FileQuery`. + +If deferred, add a `Known limitations for 1.0` section to the release notes and create Clarion tracker issues for each item retained beyond the tag. + +## End-to-End Acceptance + +Clarion side is in spec when all of the following are true: + +- ADR-014 documents `api_version`, `canonical_path`, `instance_id`, error envelope, no synthetic IDs, and trust model. +- ADR-003 no longer permits or implies `@` file IDs. +- `cargo test -p clarion-cli --test serve` passes. +- `cargo test -p clarion-storage` passes. +- `cargo test --workspace` passes. +- `cargo clippy --workspace --all-targets -- -D warnings` passes. +- `cargo fmt --all --check` passes. +- A local HTTP smoke against `clarion serve` proves: + - `/api/v1/_capabilities` returns `api_version: 1` and `instance_id`. + - `/api/v1/files` returns a real `core:file:*` entity ID for a cataloged file. + - a module-only catalog row does not synthesize a file ID and returns 404. + - a deleted-on-disk but cataloged file returns the cataloged identity. + - a traversal path returns a stable 400 error envelope. +- Filigree has the documented shapes needed to implement F4/F8/F9/F14 without guessing. + +## Final Output Required From Implementing Agent + +When finished or blocked, report: + +- Findings landed and commit SHAs. +- Findings still in flight. +- Findings blocked on Filigree. +- Test suite status. +- Joint decisions made and exact doc locations. +- Any reviewer finding that proved wrong, with source evidence. + +Do not mutate `/home/john/filigree`; it is read-only for this Clarion hardening pass. From a28c3a263240fdf2188b4d6bf056da5c2e812acf Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Tue, 19 May 2026 20:17:24 +1000 Subject: [PATCH 034/187] chore(plugin): bump Python plugin to 1.0.0 + Beta classifier MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Python plugin had three version sources that had drifted apart: - pyproject.toml: 1.0.0 - plugin.toml: 0.1.5 - clarion_plugin_python.__version__: 0.1.5 The runtime handshake (server.py:148) emitted __version__ over the JSON-RPC initialise response, so Clarion's host was being told the plugin was 0.1.5 while the published package was 1.0.0. The tests (test_package.py, test_server.py) hardcoded 0.1.5 and so masked the drift. Pin all three to 1.0.0 in sync with the workspace and update the two asserts that were locking in the wrong value. Also flip the PyPI Development Status classifier from "3 - Alpha" → "4 - Beta": the README documents the v1.0 known limitations (Python-only, deferred WP9-B, deferred manifest-language-registry on the /api/v1/files read path, post-1.0 HMAC), so "Production/Stable" would overstate it. Beta matches the "first publishable release" framing. Closes the version-drift bug (Filigree task clarion-496a8192cc). Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/clarion/{v0.1 => 1.0}/README.md | 0 docs/clarion/{v0.1 => 1.0}/detailed-design.md | 0 docs/clarion/{v0.1 => 1.0}/requirements.md | 0 docs/clarion/{v0.1 => 1.0}/system-design.md | 0 plugins/python/plugin.toml | 2 +- plugins/python/pyproject.toml | 2 +- plugins/python/src/clarion_plugin_python/__init__.py | 2 +- plugins/python/tests/test_package.py | 4 ++-- plugins/python/tests/test_server.py | 2 +- 9 files changed, 6 insertions(+), 6 deletions(-) rename docs/clarion/{v0.1 => 1.0}/README.md (100%) rename docs/clarion/{v0.1 => 1.0}/detailed-design.md (100%) rename docs/clarion/{v0.1 => 1.0}/requirements.md (100%) rename docs/clarion/{v0.1 => 1.0}/system-design.md (100%) diff --git a/docs/clarion/v0.1/README.md b/docs/clarion/1.0/README.md similarity index 100% rename from docs/clarion/v0.1/README.md rename to docs/clarion/1.0/README.md diff --git a/docs/clarion/v0.1/detailed-design.md b/docs/clarion/1.0/detailed-design.md similarity index 100% rename from docs/clarion/v0.1/detailed-design.md rename to docs/clarion/1.0/detailed-design.md diff --git a/docs/clarion/v0.1/requirements.md b/docs/clarion/1.0/requirements.md similarity index 100% rename from docs/clarion/v0.1/requirements.md rename to docs/clarion/1.0/requirements.md diff --git a/docs/clarion/v0.1/system-design.md b/docs/clarion/1.0/system-design.md similarity index 100% rename from docs/clarion/v0.1/system-design.md rename to docs/clarion/1.0/system-design.md diff --git a/plugins/python/plugin.toml b/plugins/python/plugin.toml index add14c44..af282a43 100644 --- a/plugins/python/plugin.toml +++ b/plugins/python/plugin.toml @@ -1,7 +1,7 @@ [plugin] name = "clarion-plugin-python" plugin_id = "python" -version = "0.1.5" +version = "1.0.0" protocol_version = "1.0" # Bare basename per ADR-021 §Layer 1 + WP2 scrub commit eb0a41d — the host # refuses manifests whose `executable` carries any path component. diff --git a/plugins/python/pyproject.toml b/plugins/python/pyproject.toml index 6e4aac1a..7c9e0851 100644 --- a/plugins/python/pyproject.toml +++ b/plugins/python/pyproject.toml @@ -10,7 +10,7 @@ readme = "README.md" requires-python = ">=3.11" authors = [{ name = "John Morrissey", email = "qacona@gmail.com" }] classifiers = [ - "Development Status :: 3 - Alpha", + "Development Status :: 4 - Beta", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", diff --git a/plugins/python/src/clarion_plugin_python/__init__.py b/plugins/python/src/clarion_plugin_python/__init__.py index 52a28586..02f103b4 100644 --- a/plugins/python/src/clarion_plugin_python/__init__.py +++ b/plugins/python/src/clarion_plugin_python/__init__.py @@ -1,3 +1,3 @@ """clarion-plugin-python — Python language plugin for Clarion.""" -__version__ = "0.1.5" +__version__ = "1.0.0" diff --git a/plugins/python/tests/test_package.py b/plugins/python/tests/test_package.py index a1fe5820..07b2108b 100644 --- a/plugins/python/tests/test_package.py +++ b/plugins/python/tests/test_package.py @@ -11,12 +11,12 @@ def test_package_version_matches_pyproject() -> None: - assert clarion_plugin_python.__version__ == "0.1.5" + assert clarion_plugin_python.__version__ == "1.0.0" def test_manifest_declares_references_edge_kind() -> None: manifest = tomllib.loads((_PLUGIN_ROOT / "plugin.toml").read_text(encoding="utf-8")) - assert manifest["plugin"]["version"] == "0.1.5" + assert manifest["plugin"]["version"] == "1.0.0" assert manifest["ontology"]["ontology_version"] == "0.6.0" assert manifest["ontology"]["edge_kinds"] == ["contains", "calls", "references", "imports"] diff --git a/plugins/python/tests/test_server.py b/plugins/python/tests/test_server.py index b89a91d7..99661aa9 100644 --- a/plugins/python/tests/test_server.py +++ b/plugins/python/tests/test_server.py @@ -81,7 +81,7 @@ def test_initialize_roundtrip() -> None: assert response["id"] == 1 result = response["result"] assert result["name"] == "clarion-plugin-python" - assert result["version"] == "0.1.5" + assert result["version"] == "1.0.0" assert result["ontology_version"] == "0.6.0" # Capabilities carry the L8 Wardline probe result. We don't pin a # specific status here because the probe's output depends on whether From 536e8822f6bd8232002c6b7005ea470208da5f3e Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Tue, 19 May 2026 20:17:55 +1000 Subject: [PATCH 035/187] docs(release): prepare repo for 1.0.0 release MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CHANGELOG.md (new) describes the full 1.0 scope: HTTP federation read API, pre-ingest secret scanner, subsystem clustering, LLM provider matrix (OpenRouter / Codex CLI / Claude CLI), ADR-014 federation contracts, and the known v1.0 limitations (Python-only, deferred WP9-B, deferred manifest-language-registry, post-1.0 HMAC). Follows Keep a Changelog; spells out the api_version-vs-product-semver split for the federation surface. CLAUDE.md repository-state header now reads "v1.0.0 — first publishable release" (previously "Sprint 1 is closed"). The documentation map's tree diagram now shows the renamed `docs/clarion/1.0/` plus `docs/federation/` and the three closed sprint directories. The Live-carryover-from-Sprint-1 list is replaced with a forward-looking post-1.0 follow-up tracker that points filigree as the authoritative source. Authored-ADR count fixed from "16 at v0.1" to "28 at 1.0 (through ADR-033), ADR-008 superseded by ADR-014, four Backlog (ADR-009/010/019/020)". The v0.1 asterisks paragraph keeps the doctrine intact but acknowledges that both retirement conditions persist into 1.0 (Wardline does not yet have a native Filigree emitter; the Python plugin still imports wardline.core.registry.REGISTRY). The product docset moved from `docs/clarion/v0.1/` to `docs/clarion/1.0/` (rename was bundled with the preceding plugin-version commit; this commit updates every path reference). Body text inside ADRs / sprint plans / handoffs that says "v0.1 scope", "v0.1 baselined", "post-v0.1" is left alone — those describe the historical design generation, not the path, and rewriting them would erase load-bearing context for the documents that anchor specific decisions in time. Refs: ADR-014, ADR-031, ADR-033. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 132 ++++++++++++++++++ CLAUDE.md | 42 +++--- README.md | 4 +- .../migrations/0001_initial_schema.sql | 2 +- .../adr/ADR-024-guidance-schema-vocabulary.md | 4 +- ...-backend-and-file-identity-displacement.md | 2 +- .../2026-04-18-wp1-scaffold-storage.md | 4 +- .../2026-05-18-phase3-subsystems.md | 6 +- .../01-discovery-findings.md | 2 +- .../2026-04-23-wp2-full-scrub-handoff.md | 6 +- .../2026-04-24-post-wp2-scrub-handoff.md | 6 +- .../handoffs/2026-05-03-skeleton-audit.md | 12 +- .../2026-05-18-phase3-subsystems-handoff.md | 6 +- .../thread-1-pre-publish-blockers.md | 6 +- .../v0.1-publish/ws-a-secret-scanner.md | 2 +- .../v0.1-scope-commitments.md | 2 +- 16 files changed, 187 insertions(+), 51 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..e3de3eb2 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,132 @@ +# Changelog + +All notable changes to Clarion are documented here. The format is loosely based +on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and Clarion +follows [Semantic Versioning](https://semver.org/) for the `clarion` binary, +the workspace crates, and the Python plugin. + +API versioning for the federation HTTP read API (`/api/v1/...`) is independent +of product semver: `api_version: 1` is the wire-contract version, and bumps +only when an incompatible change is made to that surface. See +[`docs/federation/contracts.md`](docs/federation/contracts.md). + +## [Unreleased] + +(none yet — see [`docs/implementation/`](docs/implementation/) for in-flight +sprint planning.) + +## [1.0.0] — 2026-05-19 + +First publishable release. Clarion ships as a Rust core (`clarion` binary, five +workspace crates) plus an editable-install Python language plugin +(`clarion-plugin-python`). Released under the [MIT License](LICENSE). + +Tagged as `v1.0.0`. Supersedes the pre-release `v0.1-sprint-1` and +`v0.1-sprint-2` working tags; those remain in the repo as historical anchors. + +### Core + +- `clarion install --path` initialises a project's `.clarion/` directory + (instance ID, SQLite DB, migrations). +- `clarion analyze` walks a Python corpus and persists the structural graph + (entities + `contains`, `calls`, `references`, `imports` edges) to a local + SQLite store via the writer-actor pattern (ADR-011). +- `clarion serve` exposes the MCP stdio surface for consult-mode agents: + `entity_at`, `find_entity`, `callers_of`, `execution_paths_from`, + `summary`, `issues_for`, `neighborhood`, `subsystem_members`. + +### Python plugin (`clarion-plugin-python` 1.0.0) + +- Pyright-backed entity extraction for functions, classes, and modules; resolved + / ambiguous / inferred call edges per ADR-022. +- `wardline` runtime probe with version-range pinning (`>=1.0.0,<2.0.0`). +- Module-level `imports` candidate edges (Phase 3 Task 3). +- Strict-typed L4 JSON-RPC handshake with declared `entity_kinds`, + `edge_kinds`, `ontology_version`, and `rule_id_prefix` (ADR-022). + +### Federation HTTP read API (ADR-014) + +The publisher-side of Clarion's federation contract with Filigree's +`ClarionRegistry`. Pinned in [`docs/federation/contracts.md`](docs/federation/contracts.md); +fixtures under [`docs/federation/fixtures/`](docs/federation/fixtures/) are +normative. + +- `GET /api/v1/_capabilities` — registry-backend probe, returns `instance_id`, + `api_version: 1`, and feature flags. Always unauthenticated so siblings can + discover the surface pre-auth. +- `GET /api/v1/files?path=&language=` — single-file identity resolution. + Closed error envelope `{error, code}` with codes `INVALID_PATH`, + `PATH_OUTSIDE_PROJECT`, `NOT_FOUND`, `BRIEFING_BLOCKED`, `UNAUTHORIZED`, + `STORAGE_ERROR`, `INTERNAL`. ETag / `If-None-Match` supported. +- `POST /api/v1/files/batch` — bulk resolution, up to 256 queries per + request, single pooled `ReaderPool` checkout per batch. Four-way + partitioning: `resolved` / `not_found` / `briefing_blocked` / `errors`. +- **Bearer authentication.** `serve.http.token_env` (default + `CLARION_LOOM_TOKEN`) names the env var holding the inbound bearer + token. Loopback-without-token stays unauthenticated for the v0.1 trust + model; non-loopback-without-token is refused at startup with + `CLA-CONFIG-HTTP-NO-AUTH`. +- **Briefing-blocked propagation.** Files flagged by the pre-ingest secret + scanner return `403 BRIEFING_BLOCKED` on the single-file endpoint and + surface in the `briefing_blocked[]` partition on the batch endpoint. + Identity fields (`entity_id`, `content_hash`, `canonical_path`, + `language`) are deliberately omitted from blocked responses so siblings + cannot infer file identity from a refusal. + +### Secret scanner (WP5, ADR-013) + +- Pre-ingest scan annotates entities with `briefing_blocked` when a file + contains high-entropy or pattern-matched secrets; ingest proceeds, but + every federation read path refuses to surface the entity. + +### Subsystem clustering (WP4 Phase 3) + +- Storage-side queries for subsystem membership and inverse lookup. +- Analyze-time clustering writes subsystem entities and `in_subsystem` edges. +- `subsystem_members(id)` MCP tool surfaces cluster members to consult agents. + +### LLM providers + +- OpenRouter (default, requires `OPENROUTER_API_KEY`). +- Codex CLI and Claude CLI as local-login providers — no API key surface, + the user's OS-level auth carries. +- `recording` provider for deterministic test fixtures. + +### Operational + +- ADR-023 tooling baseline: `cargo fmt --check`, `cargo clippy -D warnings`, + `cargo nextest`, `cargo doc -D warnings`, `cargo deny`, `ruff`, + `ruff format --check`, `mypy --strict`, `pytest`, end-to-end walking-skeleton + script — all enforced in CI. +- Pre-commit hooks wire the Python gates. +- `.env` loaded from CWD or ancestor before tracing setup. + +### Known v1.0 limitations + +- **Python only.** Other-language plugins (`NG-15`) are v2.0+ scope. +- **Filigree finding emission deferred to v0.2.** Cross-product POSTing of + Clarion-generated findings into Filigree's intake (WP9-B) is deferred per + the [Sprint 2 scope amendment](docs/implementation/sprint-2/scope-amendment-2026-05.md). + `issues_for(id)` (the WP9-A binding for reading from Filigree) ships in 1.0. +- **HTTP file language inference** uses stored plugin identity plus a narrow + core-extension fallback; persistent manifest language registry on the + `/api/v1/files` read path is deferred post-1.0. +- **Cooperative HMAC inbound auth** is post-1.0; bearer tokens are the + documented v1.0 wire surface and are forward-compatible with HMAC via the + same `Authorization:` header carrier. + +### Documentation + +- Design ladder under [`docs/clarion/1.0/`](docs/clarion/1.0/) — `requirements.md`, + `system-design.md`, `detailed-design.md`. +- ADRs under [`docs/clarion/adr/`](docs/clarion/adr/) — 28 Accepted at 1.0 + (through ADR-033). ADR-008 is superseded by ADR-014; four ADRs + (ADR-009, ADR-010, ADR-019, ADR-020) remain Backlog and are tracked + inside `system-design.md` §12 / `detailed-design.md` §11 until promoted. +- Loom-suite doctrine at [`docs/suite/loom.md`](docs/suite/loom.md). +- Federation contract surface at [`docs/federation/contracts.md`](docs/federation/contracts.md). +- Operator guides under [`docs/operator/`](docs/operator/) — getting-started, + OpenRouter setup, HTTP read API. + +[Unreleased]: https://github.com/tachyon-beep/clarion/compare/v1.0.0...HEAD +[1.0.0]: https://github.com/tachyon-beep/clarion/releases/tag/v1.0.0 diff --git a/CLAUDE.md b/CLAUDE.md index edec3c26..1e1cb2d8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,9 +4,9 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Repository state -**Sprint 1 is closed.** The walking skeleton is tagged at `v0.1-sprint-1` (merge commit `48b9bb0` on `main`). All nine Sprint-1 lock-ins (L1–L9) are ratified; `clarion analyze` end-to-end persists `python:function:demo.hello|function` against a fixture project. See `docs/implementation/sprint-1/signoffs.md` for the closed Tier-A ladder. +**v1.0.0 — first publishable release.** Tagged at `v1.0.0`. Workspace + Python plugin are at 1.0.0; ADR-014 federation HTTP read API ships with bearer auth, batch resolution, briefing-blocked propagation, and stable per-project `instance_id`. Pre-release working tags `v0.1-sprint-1` and `v0.1-sprint-2` remain in the repo as historical anchors. See [`CHANGELOG.md`](CHANGELOG.md) for the full 1.0 scope and [`docs/implementation/`](docs/implementation/) for sprint-closure artifacts. -### Layout (post-Sprint-1) +### Layout (post-1.0) - **Rust workspace** at repo root (`Cargo.toml`, `crates/`): - `crates/clarion-core/` — entity-ID assembler, plugin host (`plugin/host.rs`), JSON-RPC transport, manifest parser, jail + limits, discovery, breaker. @@ -61,7 +61,7 @@ The Loom federation axiom in `docs/suite/loom.md` (especially §3–§5) is **lo Before proposing or accepting any change that adds a new dependency, "lightweight glue layer," shared registry, or cross-product mediator, run it against the §5 failure test (semantic / initialization / pipeline coupling). Centralisation creeps back in naturally; treat any "wouldn't it be easier if we just..." proposal as suspicious. -Two named v0.1 asterisks (Wardline→Filigree pipeline coupling via Clarion; Python plugin's `wardline.core.registry.REGISTRY` import) have written retirement conditions in `loom.md` §5. Do not add new asterisks without the same. +Two named asterisks (Wardline→Filigree pipeline coupling via Clarion; Python plugin's `wardline.core.registry.REGISTRY` import) have written retirement conditions in `loom.md` §5. Both persist into v1.0 and retire post-release per the conditions named there. Do not add new asterisks without the same discipline. ## Documentation map @@ -71,21 +71,25 @@ docs/ │ ├── briefing.md 5-minute introduction │ └── loom.md Founding doctrine, federation axiom, go/no-go test ├── clarion/ -│ ├── v0.1/ Canonical product docset for the current version +│ ├── 1.0/ Canonical product docset for the 1.0 release +│ │ ├── README.md Reading-order map for the design ladder │ │ ├── requirements.md The WHAT — REQ-/NFR-/CON-/NG- IDs, baselined │ │ ├── system-design.md The HOW — architecture, mechanisms, §2–§11 with `Addresses:` headers -│ │ ├── detailed-design.md Implementation reference — schemas, rule catalogs, appendices -│ │ ├── plans/ Scope memo (v0.1-scope-commitments.md) -│ │ └── reviews/ Retained historical reviews (supporting context, not normative) -│ └── adr/ Authored architecture decision records (ADR-001 … ADR-022) -└── implementation/ Work-package sequencing (lives ABOVE v0.1/ because WPs span siblings) +│ │ └── detailed-design.md Implementation reference — schemas, rule catalogs, appendices +│ └── adr/ Authored architecture decision records (ADR-001 … ADR-031) +├── federation/ Cross-product wire contracts + normative fixtures +│ ├── contracts.md Pinned HTTP read API + auth + path-normalization +│ └── fixtures/ Normative request/response fixtures +└── implementation/ Work-package sequencing (lives ABOVE the docset because WPs span siblings) ├── v0.1-plan.md 11 WPs in dependency order, with anchoring docs/ADRs per WP - └── sprint-1/ Per-sprint execution plan (walking-skeleton: WP1+WP2+WP3) + ├── sprint-1/ Walking-skeleton sprint (WP1+WP2+WP3) + ├── sprint-2/ B-track + scanner sprint + └── sprint-3/ Loom federation hardening sprint (ADR-014) ``` ### Reading order by intent -- **New to the project**: `docs/suite/briefing.md` → `docs/suite/loom.md` → `docs/clarion/v0.1/README.md`. +- **New to the project**: `docs/suite/briefing.md` → `docs/suite/loom.md` → `docs/clarion/1.0/README.md`. - **Implementing**: `requirements.md` → `system-design.md` → `detailed-design.md` → relevant ADRs → the WP doc under `docs/implementation/`. - **Reviewing a design proposal**: read the requirement IDs it cites, then the system-design section listed in those requirements' `See` lines, then check whether any Accepted ADR already constrains the answer. @@ -93,11 +97,11 @@ docs/ When the same fact appears in multiple files, this is the precedence: -1. **Accepted ADRs** in `docs/clarion/adr/` — the locked decisions. 16 are Accepted at v0.1; six remain Backlog and are tracked inside `system-design.md` §12 / `detailed-design.md` §11 until promoted. +1. **Accepted ADRs** in `docs/clarion/adr/` — the locked decisions. 28 are Accepted at 1.0; four remain Backlog (ADR-009, ADR-010, ADR-019, ADR-020) and are tracked inside `system-design.md` §12 / `detailed-design.md` §11 until promoted. ADR-008 was superseded by ADR-014. 2. **`requirements.md`** — REQ-/NFR-/CON-/NG- IDs are stable and load-bearing (filigree issues and commit messages cite them by ID; never reuse a retired ID). 3. **`system-design.md`** — `Addresses:` headers on each §2–§11 section define the requirement acceptance surface for that subsystem. 4. **`detailed-design.md`** — exact schemas, rule catalogues, appendices. -5. Reviews under `docs/clarion/v0.1/reviews/` are supporting context only, not normative. Do not cite a review as the source of a current decision; cite the ADR or design doc that absorbed it. +5. Reviews under `docs/clarion/1.0/reviews/` are supporting context only, not normative. Do not cite a review as the source of a current decision; cite the ADR or design doc that absorbed it. If `requirements.md` and `system-design.md` disagree, the requirement wins and the design doc is the bug. If an ADR exists, it overrides both. @@ -128,16 +132,16 @@ Avoid: "Loom platform," "Loom runtime," "Loom broker," "Loom store" — Loom is `filigree` is the issue tracker for this project (config in `.filigree/`, MCP server registered in `.mcp.json`). The global `~/CLAUDE.md` file describes the workflow and CLI/MCP commands; do not duplicate that here. Project-specific notes: -- Sprint 1 issues (`WP1`, `WP2`, `WP3`, Sprint-1-close) are all `delivered`/`closed`. Sprint 2 issues should follow the same `release:v0.1`, `sprint:N`, `wp:N`, `adr:NNN` label scheme. +- Sprint 1 / Sprint 2 / Sprint 3 issues are all `delivered`/`closed` at 1.0. Post-1.0 issues should follow the same `release:1.0`-style label scheme using whatever release tag (`release:1.1`, `release:2.0`) the work targets. - Filigree issue bodies should cite `REQ-*` / `NFR-*` / ADR IDs verbatim — those IDs are how design docs and tracker stay linked. -### Live carryover from Sprint 1 +### Post-1.0 follow-up tracking -These were filed during Sprint 1 with explicit deferral rationale and are ready for Sprint 2 triage: +Open issues for the v1.0 known limitations and any post-release follow-ups live in `filigree` under the `release:1.1` (and beyond) label. `filigree get-ready` / `filigree session-context` are authoritative for what's currently actionable. Notable themes: -- `clarion-889200006a` (P3, sprint:2 / wp:9) — ADR-018 amendment: L7 qualname divergence with Wardline `FingerprintEntry`. Triggers when WP9 attempts the first cross-product join. -- WP2 deferred items: `clarion-48c5d06578` (supervisor drain/discard), `clarion-928349b60f` (jail TOCTOU on briefing read), `clarion-35688034f0` (read_frame deadline), `clarion-c0977ac293` (RLIMIT_AS observed end-to-end), `clarion-adeff0916d` (fixture-binary self-build). -- WP1 review-2 P2 bugs: `clarion-5e03cfdd21`, `clarion-ed5017139f`, `clarion-b5b1029f5a`, `clarion-4cd11905e2` — good Sprint-2 warm-up before Tier-B feature work. +- **WP9-B (Filigree finding emission)** — deferred from 1.0 per the [Sprint 2 scope amendment](docs/implementation/sprint-2/scope-amendment-2026-05.md#4-v01-planmd-resequencing). +- **HTTP file language manifest registry** — narrow core-extension fallback at 1.0; persistent registry is a post-1.0 task. +- **HMAC inbound auth (C-4)** — bearer is the 1.0 wire surface; HMAC is forward-compatible and tracked for post-1.0 hardening. ## Filigree Issue Tracker diff --git a/README.md b/README.md index 7dca4439..b6fc5923 100644 --- a/README.md +++ b/README.md @@ -88,12 +88,12 @@ crates/ Rust workspace ├── clarion-cli/ The `clarion` binary (install, analyze, serve) └── clarion-mcp/ MCP server exposing the seven consult tools plugins/python/ Python language plugin (pyright-backed) -docs/clarion/v0.1/ Design ladder — requirements → system-design → detailed-design +docs/clarion/1.0/ Design ladder — requirements → system-design → detailed-design docs/clarion/adr/ Authored architecture decision records ``` For the design ladder start at -[docs/clarion/v0.1/README.md](docs/clarion/v0.1/README.md). The full ADR index +[docs/clarion/1.0/README.md](docs/clarion/1.0/README.md). The full ADR index is at [docs/clarion/adr/README.md](docs/clarion/adr/README.md). The Loom federation doctrine that anchors every cross-product decision is in [docs/suite/loom.md](docs/suite/loom.md). diff --git a/crates/clarion-storage/migrations/0001_initial_schema.sql b/crates/clarion-storage/migrations/0001_initial_schema.sql index 19727874..e4fb6c23 100644 --- a/crates/clarion-storage/migrations/0001_initial_schema.sql +++ b/crates/clarion-storage/migrations/0001_initial_schema.sql @@ -1,7 +1,7 @@ -- ============================================================================ -- Clarion migration 0001 — initial schema. -- --- Source: docs/clarion/v0.1/detailed-design.md §3 (Storage Implementation). +-- Source: docs/clarion/1.0/detailed-design.md §3 (Storage Implementation). -- Sprint 1 walking skeleton writes only to `entities` and `runs`, but every -- table, FTS5 virtual table, trigger, generated column, and view is created -- here so the full shape is frozen at L1-lock time. See ADR-011 for the diff --git a/docs/clarion/adr/ADR-024-guidance-schema-vocabulary.md b/docs/clarion/adr/ADR-024-guidance-schema-vocabulary.md index 67b2cfd5..77c7753e 100644 --- a/docs/clarion/adr/ADR-024-guidance-schema-vocabulary.md +++ b/docs/clarion/adr/ADR-024-guidance-schema-vocabulary.md @@ -173,6 +173,6 @@ Add the audit's findings to the glossary as `open` clashes; defer the schema cor - [Loom federation axiom](../../suite/loom.md) §3–§5 — the doctrine the glossary defends; the failure-test mode this ADR addresses is reader-side cross-product disambiguation cost. - `crates/clarion-storage/migrations/0001_initial_schema.sql:163-191` — the schema sites this ADR edits in place. - `crates/clarion-storage/tests/schema_apply.rs:138-169` — the test that documented the bug and is rewritten to test the corrected design. -- `docs/clarion/v0.1/detailed-design.md:204, 270, 449, 453, 467, 471, 737-748` — the design-doc sites this ADR's renames touch. -- `docs/clarion/v0.1/system-design.md:346, 675` — the system-level guidance composition references. +- `docs/clarion/1.0/detailed-design.md:204, 270, 449, 453, 467, 471, 737-748` — the design-doc sites this ADR's renames touch. +- `docs/clarion/1.0/system-design.md:346, 675` — the system-level guidance composition references. - Filigree issue `clarion-4cd11905e2` — the misframed bug whose triage produced this audit. diff --git a/docs/federation/filigree-side/ADR-014-registry-backend-and-file-identity-displacement.md b/docs/federation/filigree-side/ADR-014-registry-backend-and-file-identity-displacement.md index 17d0df29..ff2eb141 100644 --- a/docs/federation/filigree-side/ADR-014-registry-backend-and-file-identity-displacement.md +++ b/docs/federation/filigree-side/ADR-014-registry-backend-and-file-identity-displacement.md @@ -219,7 +219,7 @@ Phase A must ship before Phase C can land an integration that does anything obse - Clarion ADR-029: `/home/john/clarion/docs/clarion/adr/ADR-029-entity-associations-binding.md`. - Clarion v0.1 plan §WP10: `/home/john/clarion/docs/implementation/v0.1-plan.md` (the cross-product work package this ADR fulfils). - Sprint 2 scope amendment (defer): `/home/john/clarion/docs/implementation/sprint-2/scope-amendment-2026-05.md`. -- Clarion integration recon: `/home/john/clarion/docs/clarion/v0.1/reviews/pre-restructure/integration-recon.md` (auto-create paths and FK survey). +- Clarion integration recon: `/home/john/clarion/docs/clarion/1.0/reviews/pre-restructure/integration-recon.md` (auto-create paths and FK survey). - Filigree auto-create paths (verified 2026-05-19): - `src/filigree/db_files.py:640` `_upsert_file_record` - `src/filigree/db_files.py:184` `register_file` diff --git a/docs/implementation/agent-plans/2026-04-18-wp1-scaffold-storage.md b/docs/implementation/agent-plans/2026-04-18-wp1-scaffold-storage.md index 85473f0a..aa63f1d7 100644 --- a/docs/implementation/agent-plans/2026-04-18-wp1-scaffold-storage.md +++ b/docs/implementation/agent-plans/2026-04-18-wp1-scaffold-storage.md @@ -710,7 +710,7 @@ Write `/home/john/clarion/crates/clarion-storage/migrations/0001_initial_schema. -- ============================================================================ -- Clarion migration 0001 — initial schema. -- --- Source: docs/clarion/v0.1/detailed-design.md §3 (Storage Implementation). +-- Source: docs/clarion/1.0/detailed-design.md §3 (Storage Implementation). -- Sprint 1 walking skeleton writes only to `entities` and `runs`, but every -- table, FTS5 virtual table, trigger, generated column, index, and view -- is created here so the full shape is frozen at L1-lock time. See ADR-011 @@ -1612,7 +1612,7 @@ const CONFIG_JSON_STUB: &str = r#"{ "#; const CLARION_YAML_STUB: &str = "# clarion.yaml — user-edited config.\n\ -# Full schema TBD; see docs/clarion/v0.1 design. Sprint 1 walking skeleton\n\ +# Full schema TBD; see docs/clarion/1.0 design. Sprint 1 walking skeleton\n\ # ignores most fields. Do not delete this file: later versions will require\n\ # it for model-tier mappings and analysis knobs.\n\ version: 1\n"; diff --git a/docs/implementation/agent-plans/2026-05-18-phase3-subsystems.md b/docs/implementation/agent-plans/2026-05-18-phase3-subsystems.md index ce04fb7d..2ee037b1 100644 --- a/docs/implementation/agent-plans/2026-05-18-phase3-subsystems.md +++ b/docs/implementation/agent-plans/2026-05-18-phase3-subsystems.md @@ -25,8 +25,8 @@ serde_norway, sha2 for ADR-006 subsystem hashes, a graph-clustering crate candidate qualified in Task 1, Python 3.11 AST extraction for import edges, pytest, mypy, ruff, cargo nextest, cargo-deny, and existing shell E2E scripts. -**Spec:** ADR-006, ADR-003, ADR-022, `docs/clarion/v0.1/requirements.md`, -`docs/clarion/v0.1/system-design.md`, and the Phase 3 handoff +**Spec:** ADR-006, ADR-003, ADR-022, `docs/clarion/1.0/requirements.md`, +`docs/clarion/1.0/system-design.md`, and the Phase 3 handoff `docs/superpowers/handoffs/2026-05-18-phase3-subsystems-handoff.md`. **Filigree umbrella:** `clarion-1dfeebfa36` (P1 work_package; @@ -140,7 +140,7 @@ Direction: `member_module -> subsystem`. The MCP membership helper queries **Recommendation:** add a new `subsystem_members(id)` MCP tool rather than folding the behavior into `neighborhood`. Requirements already name -`subsystem_members` (`docs/clarion/v0.1/requirements.md:365-371`), and a +`subsystem_members` (`docs/clarion/1.0/requirements.md:365-371`), and a separate tool keeps the response schema narrow: subsystem metadata plus an ordered member list. `neighborhood` can later include subsystem links additively, but that should not be the only way to inspect a subsystem. diff --git a/docs/implementation/arch-analysis-2026-05-18-1244/01-discovery-findings.md b/docs/implementation/arch-analysis-2026-05-18-1244/01-discovery-findings.md index 30fbca6a..d6718e5e 100644 --- a/docs/implementation/arch-analysis-2026-05-18-1244/01-discovery-findings.md +++ b/docs/implementation/arch-analysis-2026-05-18-1244/01-discovery-findings.md @@ -293,7 +293,7 @@ Authored ADRs span 001–031 with gaps (008–010, 019–020 are tracked as back ### 6.7 Top-level -`docs/README.md` + `docs/clarion/README.md` + `docs/clarion/v0.1/README.md` + `docs/implementation/README.md` form the navigation tree. +`docs/README.md` + `docs/clarion/README.md` + `docs/clarion/1.0/README.md` + `docs/implementation/README.md` form the navigation tree. --- diff --git a/docs/implementation/handoffs/2026-04-23-wp2-full-scrub-handoff.md b/docs/implementation/handoffs/2026-04-23-wp2-full-scrub-handoff.md index c2f541ab..395f8ec7 100644 --- a/docs/implementation/handoffs/2026-04-23-wp2-full-scrub-handoff.md +++ b/docs/implementation/handoffs/2026-04-23-wp2-full-scrub-handoff.md @@ -80,9 +80,9 @@ Anchoring documents to read-with: - `docs/clarion/adr/ADR-021-plugin-authority-hybrid.md` - `docs/clarion/adr/ADR-022-core-plugin-ontology.md` - `docs/clarion/adr/ADR-023-tooling-baseline.md` -- `docs/clarion/v0.1/requirements.md` — REQ / NFR / CON IDs WP2 addresses -- `docs/clarion/v0.1/system-design.md` §§4–6 (host, plugin protocol, limits) -- `docs/clarion/v0.1/detailed-design.md` §§4–5 (wire schemas, rule catalogues) +- `docs/clarion/1.0/requirements.md` — REQ / NFR / CON IDs WP2 addresses +- `docs/clarion/1.0/system-design.md` §§4–6 (host, plugin protocol, limits) +- `docs/clarion/1.0/detailed-design.md` §§4–5 (wire schemas, rule catalogues) ## Open WP2 review-2 tail at session start diff --git a/docs/implementation/handoffs/2026-04-24-post-wp2-scrub-handoff.md b/docs/implementation/handoffs/2026-04-24-post-wp2-scrub-handoff.md index 4a29f59f..e16f4247 100644 --- a/docs/implementation/handoffs/2026-04-24-post-wp2-scrub-handoff.md +++ b/docs/implementation/handoffs/2026-04-24-post-wp2-scrub-handoff.md @@ -168,7 +168,7 @@ This is the bigger slice. WP3 builds an editable Python package at lock-ins: - **L7**: qualname reconstruction per - `docs/clarion/v0.1/detailed-design.md §§4–5` (module-level, nested, + `docs/clarion/1.0/detailed-design.md §§4–5` (module-level, nested, class, async, nested-class). Shared test fixture at `/fixtures/entity_id.json` must pass byte-for-byte in both Rust and Python — this is the L2+L7 alignment proof (A.3.4). @@ -269,8 +269,8 @@ The scrub changed several surfaces that WP3 will touch: - `docs/clarion/adr/ADR-007-summary-cache-keying.md` — you'll produce `ontology_version` but not consume it yet - `docs/clarion/adr/ADR-023-tooling-baseline.md` — Python tooling gates -- `docs/clarion/v0.1/detailed-design.md §§4–5` — qualname rules -- `docs/clarion/v0.1/requirements.md` — REQ-/NFR- IDs WP3 addresses +- `docs/clarion/1.0/detailed-design.md §§4–5` — qualname rules +- `docs/clarion/1.0/requirements.md` — REQ-/NFR- IDs WP3 addresses ## Methodology diff --git a/docs/implementation/handoffs/2026-05-03-skeleton-audit.md b/docs/implementation/handoffs/2026-05-03-skeleton-audit.md index 5824b46a..998e0878 100644 --- a/docs/implementation/handoffs/2026-05-03-skeleton-audit.md +++ b/docs/implementation/handoffs/2026-05-03-skeleton-audit.md @@ -42,7 +42,7 @@ Out of scope (deliberate; flag for separate audit if useful): For each candidate concept: -1. Locate every site in `docs/clarion/v0.1/{requirements,system-design,detailed-design}.md`, +1. Locate every site in `docs/clarion/1.0/{requirements,system-design,detailed-design}.md`, `docs/clarion/adr/`, `crates/clarion-storage/migrations/0001_initial_schema.sql`, and any code path that reads/writes the column. 2. Cross-reference Filigree's vocabulary via `filigree taxonomy` and @@ -75,9 +75,9 @@ For each candidate concept: ## F-1: `priority` is the same word for two different things **Sites**: -- Clarion: `docs/clarion/v0.1/detailed-design.md:453` — guidance entity +- Clarion: `docs/clarion/1.0/detailed-design.md:453` — guidance entity property; values `"project" | "subsystem" | "package" | "module" | "class" | "function"`. -- Clarion: `docs/clarion/v0.1/system-design.md:346` — same definition, +- Clarion: `docs/clarion/1.0/system-design.md:346` — same definition, semantic ordering `project → subsystem → package → module → class → function` (outer-overrides-inner composition order). - Clarion schema: `crates/clarion-storage/migrations/0001_initial_schema.sql:163-165` @@ -121,7 +121,7 @@ DB) or stack `0002_*.sql`? Decide once for the project. ## F-2: `critical` is the same word for two different things **Sites**: -- Clarion: `docs/clarion/v0.1/detailed-design.md:467` — guidance entity +- Clarion: `docs/clarion/1.0/detailed-design.md:467` — guidance entity `critical: bool` flag — semantics: "preserved across token-budget pressure" (i.e., do-not-drop). - Clarion: `detailed-design.md:449` — `tags: ["critical"]` literal as the @@ -316,8 +316,8 @@ finding/guidance) plus the schema change and test fix into a single ADR-024 ("Loom vocabulary discipline and guidance schema") and a single PR before Tier B starts. Touches: -- `docs/clarion/v0.1/detailed-design.md` (multiple sites) -- `docs/clarion/v0.1/system-design.md` (one site) +- `docs/clarion/1.0/detailed-design.md` (multiple sites) +- `docs/clarion/1.0/system-design.md` (one site) - `docs/clarion/adr/ADR-024-*.md` (new) - `crates/clarion-storage/migrations/0001_initial_schema.sql` (priority column + index; possibly add `0002_*.sql` instead — see migration policy diff --git a/docs/implementation/handoffs/2026-05-18-phase3-subsystems-handoff.md b/docs/implementation/handoffs/2026-05-18-phase3-subsystems-handoff.md index 3ba7b975..75e258ce 100644 --- a/docs/implementation/handoffs/2026-05-18-phase3-subsystems-handoff.md +++ b/docs/implementation/handoffs/2026-05-18-phase3-subsystems-handoff.md @@ -34,9 +34,9 @@ Two things, in order, with a human review gate between them. 1. **`docs/clarion/adr/ADR-006-clustering-algorithm.md` plus ADR-032** — the authoritative spec. Read in full. Leiden on directed weighted imports+calls subgraph, seeded for determinism, with `weighted_components` as the named local fallback after ADR-032. Output is one `subsystem` entity per cluster + `in_subsystem` edges from members. Modularity reported, not enforced. 2. **`docs/clarion/adr/ADR-022-core-plugin-ontology.md`** — `subsystem` is a core-reserved entity kind; `in_subsystem` is a core-reserved edge kind. Plugins cannot emit either. The writer-actor's edge-contract validator already knows about plugin-extensible vs core-reserved (`writer.rs:411` per arch-analysis). 3. **`docs/clarion/adr/ADR-003-entity-id-scheme.md`** — subsystem IDs follow `core:subsystem:{cluster_hash}` per ADR-006 §Output. The hash is `sha256(sorted(member_module_ids))` truncated to 12 chars; verify the existing entity-ID validator accepts this shape (it should — `core` is a registered plugin_id per ADR-022). -4. **`docs/clarion/v0.1/requirements.md`** — REQ-CATALOG-05 (subsystem entities), REQ-ANALYZE-01 (phased pipeline), REQ-ANALYZE-05 (Phase-7 findings — relevant because `CLA-FACT-CLUSTERING-WEAK-MODULARITY` is named in ADR-006 §Quality assessment). -5. **`docs/clarion/v0.1/system-design.md` §6** — pipeline phases. Phase 3 placement, what it reads from storage, what it writes. -6. **`docs/clarion/v0.1/detailed-design.md`** — search for `subsystem`, `cluster`, `Phase 3`. Schema shape if present, properties expected on the subsystem entity. +4. **`docs/clarion/1.0/requirements.md`** — REQ-CATALOG-05 (subsystem entities), REQ-ANALYZE-01 (phased pipeline), REQ-ANALYZE-05 (Phase-7 findings — relevant because `CLA-FACT-CLUSTERING-WEAK-MODULARITY` is named in ADR-006 §Quality assessment). +5. **`docs/clarion/1.0/system-design.md` §6** — pipeline phases. Phase 3 placement, what it reads from storage, what it writes. +6. **`docs/clarion/1.0/detailed-design.md`** — search for `subsystem`, `cluster`, `Phase 3`. Schema shape if present, properties expected on the subsystem entity. 7. **Current code surface** — `cargo` over these files at minimum: - `crates/clarion-storage/migrations/0001_initial_schema.sql` — does `entities.kind` already accept `subsystem`? Does `edges.kind` accept `in_subsystem`? ADR-031 added CHECK constraints on closed vocabularies; verify the subsystem path is not constrained-shut. - `crates/clarion-storage/src/writer.rs` (around line 394–411) — `STRUCTURAL_EDGE_KINDS` and `ANCHORED_EDGE_KINDS` registers. Arch-analysis H-2 noted these duplicate the manifest's edge-kind list (now closed as `clarion-4e3cacac90`); confirm what the closure shipped. diff --git a/docs/implementation/v0.1-publish/thread-1-pre-publish-blockers.md b/docs/implementation/v0.1-publish/thread-1-pre-publish-blockers.md index 97beac59..efb6a0eb 100644 --- a/docs/implementation/v0.1-publish/thread-1-pre-publish-blockers.md +++ b/docs/implementation/v0.1-publish/thread-1-pre-publish-blockers.md @@ -195,7 +195,7 @@ This is the consult-mode-agent surface for "the absence of a summary is policy, #### Task 6 — Rule catalogue entries in `detailed-design.md` **Files**: -- Modify: `docs/clarion/v0.1/detailed-design.md` (§5 rule catalogue) +- Modify: `docs/clarion/1.0/detailed-design.md` (§5 rule catalogue) **Scope**: append rule rows for `CLA-SEC-SECRET-DETECTED`, `CLA-SEC-UNREDACTED-SECRETS-ALLOWED`, `CLA-INFRA-SECRET-BASELINE-NO-JUSTIFICATION`, `CLA-INFRA-SECRET-BASELINE-MATCH`, `CLA-INFRA-SECRET-OVERRIDE-UNCONFIRMED`. Each row: rule-ID, severity, category, one-sentence description, one-sentence remediation, ADR pointer. @@ -230,7 +230,7 @@ All tasks green; in addition: **Files**: - Create: `README.md` at repo root. -**Scope**: there is no top-level README currently. The reader-ladder under `docs/suite/briefing.md` → `docs/suite/loom.md` → `docs/clarion/v0.1/README.md` assumes the reader already knows to start there. A first-time visitor (PyPI / crates.io / GitHub front page) has no entry point. +**Scope**: there is no top-level README currently. The reader-ladder under `docs/suite/briefing.md` → `docs/suite/loom.md` → `docs/clarion/1.0/README.md` assumes the reader already knows to start there. A first-time visitor (PyPI / crates.io / GitHub front page) has no entry point. The README must answer, in order: @@ -238,7 +238,7 @@ The README must answer, in order: 2. **What it does today** — bullet list of the 7 MCP tools and what each answers, with one example invocation each. 3. **Quick start** — `clarion install && clarion analyze && clarion serve`, with the expected stdout shapes. Link to the operator tutorial (Task B.2). 4. **Status** — explicit "v0.1 — Python only; structural + on-demand LLM summarisation; Filigree finding emission deferred to v0.2." Quote the scope: don't oversell. -5. **Project layout** — three-sentence map (Rust workspace + Python plugin + docs) with links to `docs/clarion/v0.1/README.md` for the design ladder and `docs/clarion/adr/README.md` for the ADR index. +5. **Project layout** — three-sentence map (Rust workspace + Python plugin + docs) with links to `docs/clarion/1.0/README.md` for the design ladder and `docs/clarion/adr/README.md` for the ADR index. 6. **Contributing** — pointer to `CLAUDE.md` and the test commands (ADR-023 floor). **Length target**: ≤ 200 lines. No installation instructions deeper than `cargo install ...` + `pipx install clarion-plugin-python` (Workstream C delivers the actual commands). diff --git a/docs/implementation/v0.1-publish/ws-a-secret-scanner.md b/docs/implementation/v0.1-publish/ws-a-secret-scanner.md index f10c5586..a64aa629 100644 --- a/docs/implementation/v0.1-publish/ws-a-secret-scanner.md +++ b/docs/implementation/v0.1-publish/ws-a-secret-scanner.md @@ -745,7 +745,7 @@ existing `summary_scope_deferred` check, since both are | Action | Path | |---|---| -| modify | `docs/clarion/v0.1/detailed-design.md` (§5 rule catalogue) | +| modify | `docs/clarion/1.0/detailed-design.md` (§5 rule catalogue) | #### Scope diff --git a/docs/implementation/v0.1-scope-plans/v0.1-scope-commitments.md b/docs/implementation/v0.1-scope-plans/v0.1-scope-commitments.md index ef74080b..f2bd2ae4 100644 --- a/docs/implementation/v0.1-scope-plans/v0.1-scope-commitments.md +++ b/docs/implementation/v0.1-scope-plans/v0.1-scope-commitments.md @@ -14,7 +14,7 @@ The "cross-repo lead time" framing in Q2's original wording assumed Filigree was ## Purpose -The 10-agent review panel (`docs/clarion/v0.1/reviews/panel-2026-04-17/`) converged on three decisions that only the project author can make. Every P0 ADR, every cost-model claim, and every doctrine asterisk in `loom.md` §5 depends on these commitments being made explicitly rather than left implicit. This memo is the commitment surface — once the three decisions are recorded here, the subsequent work is mechanical. +The 10-agent review panel (`docs/clarion/1.0/reviews/panel-2026-04-17/`) converged on three decisions that only the project author can make. Every P0 ADR, every cost-model claim, and every doctrine asterisk in `loom.md` §5 depends on these commitments being made explicitly rather than left implicit. This memo is the commitment surface — once the three decisions are recorded here, the subsequent work is mechanical. **Read first**: `../reviews/panel-2026-04-17/00-executive-synthesis.md` §6. From 69ba01faeeb531e16935f7d0f339db460102e772 Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Tue, 19 May 2026 20:52:33 +1000 Subject: [PATCH 036/187] fix(llm_provider): always pass --tools to Claude provider (security) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The default no-tools security posture (ADR-013) was silently bypassed because the spawn site only added `--tools` when the configured tools list was non-empty. Claude Code interprets an omitted `--tools` as "use the normal tool set", not "no tools" — so a summary or inferred-edge request could read additional workspace files through the Claude Code tool surface, defeating the briefing-block contract. Always pass `--tools` with a possibly-empty value (`Vec::join(",")` of an empty slice yields `""`). Renamed the corresponding unit test and flipped its assertion: the fake Claude script now captures both the presence of the flag and its value, asserting `--tools ""` is passed when the configured tools list is empty. Closes clarion-56d13c257d. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/clarion-core/src/llm_provider.rs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/crates/clarion-core/src/llm_provider.rs b/crates/clarion-core/src/llm_provider.rs index dc8292c8..1e6f0570 100644 --- a/crates/clarion-core/src/llm_provider.rs +++ b/crates/clarion-core/src/llm_provider.rs @@ -684,9 +684,7 @@ impl LlmProvider for ClaudeCliProvider { if let Some(model) = &self.model { command.arg("--model").arg(model); } - if !self.tools.is_empty() { - command.arg("--tools").arg(self.tools.join(",")); - } + command.arg("--tools").arg(self.tools.join(",")); command .current_dir(&self.project_root) .stdin(Stdio::piped()) @@ -2083,7 +2081,7 @@ printf '%s\n' '{{"type":"result","subtype":"success","structured_output":{{"purp } #[test] - fn claude_cli_provider_omits_tools_arg_when_no_tools_are_configured() { + fn claude_cli_provider_passes_empty_tools_arg_when_no_tools_are_configured() { use std::fs; use std::os::unix::fs::PermissionsExt; @@ -2097,10 +2095,12 @@ printf '%s\n' '{{"type":"result","subtype":"success","structured_output":{{"purp set -euo pipefail log="{log}" saw_tools=0 +tools_value="" while [[ $# -gt 0 ]]; do case "$1" in --tools) saw_tools=1 + tools_value="${{2-}}" shift 2 ;; -p|--print|--output-format|--json-schema|--permission-mode|--max-turns|--mcp-config) @@ -2116,6 +2116,7 @@ while [[ $# -gt 0 ]]; do done cat >/dev/null echo "saw_tools=$saw_tools" >> "$log" +echo "tools_value=[$tools_value]" >> "$log" printf '%s\n' '{{"type":"result","subtype":"success","structured_output":{{"purpose":"via claude","behavior":"ran fake CLI","relationships":"","risks":""}},"usage":{{"input_tokens":1,"output_tokens":1,"total_tokens":2}},"total_cost_usd":0.0}}' "#, log = log_path.display() @@ -2150,7 +2151,14 @@ printf '%s\n' '{{"type":"result","subtype":"success","structured_output":{{"purp .expect("invoke fake Claude CLI"); let log = fs::read_to_string(log_path).expect("read fake claude log"); - assert!(log.contains("saw_tools=0")); + assert!( + log.contains("saw_tools=1"), + "Claude CLI must always receive --tools so the default no-tools posture is mechanically enforced (ADR-013). log: {log}" + ); + assert!( + log.contains("tools_value=[]"), + "Empty tools list must be passed as an explicit empty --tools value, not omitted. log: {log}" + ); } #[test] From 98b5bbdc2c2d97e48186138625355c4bfa0aee55 Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Tue, 19 May 2026 20:53:18 +1000 Subject: [PATCH 037/187] ci(release): gate publish on verify suite + portability + dynamic sdist Four release-blocking gaps in .github/workflows/release.yml addressed together, plus the version-drift guard test that complements the dynamic sdist filename. - B2 / clarion-b3dcef0d90: the package step ran sha256sum across the macos-13/macos-14 matrix, but those images ship shasum rather than GNU coreutils. First tag push would have failed in package. Switch to a portable command -v probe (output format is byte-identical between the two). - B3 / clarion-0ff5e175fe: the install snippet hard-coded clarion-plugin-python-1.0.0.tar.gz, but PEP 625 + hatchling produce clarion_plugin_python-.tar.gz. Every release would publish a 404 link, and a future version bump or normalization-rule change would silently break it again. Glob the actual staged sdist by the pattern clarion[-_]plugin[-_]python*.tar.gz and fail loudly if no match is found. - B4: release.yml had zero test gates - softprops/action-gh-release@v2 ran with draft: false on any tag push regardless of code health. Add a verify job that mirrors ci.yml (rust fmt/clippy/nextest/doc/ deny + python ruff/mypy/pytest + walking-skeleton e2e + WP5 secret scan smoke) and make build-rust and build-plugin declare needs: [verify]. Duplicated rather than refactored into a reusable workflow; that refactor is post-1.0 scope. - B5: workflow_dispatch.inputs.tag was declared but never consumed, and the release job is gated on event_name == 'push' so a manual dispatch can never publish anyway. Drop the unused input and clarify in comments that dispatch only validates build/verify plumbing. The companion version-drift guard test (clarion-496a8192cc) cross-checks that pyproject.toml [project].version, plugin.toml [plugin].version, and clarion_plugin_python.__version__ all agree. The three values are already aligned at 1.0.0; the guard prevents a future bump from drifting again. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/release.yml | 129 ++++++++++++++++++++++++--- plugins/python/tests/test_package.py | 26 +++++- 2 files changed, 144 insertions(+), 11 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e469c419..90d7acee 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,19 +1,18 @@ name: Release # Per ADR-033 — pushed tags matching v* trigger a multi-platform binary build -# plus Python plugin sdist, attached to a GitHub Release. +# plus Python plugin sdist, attached to a GitHub Release. Build jobs run only +# after the `verify` gate (mirroring ci.yml) is green, so a broken commit +# cannot publish a Release with `draft: false`. on: push: tags: - "v*" - # Manual dispatch lets us produce pre-release artifacts (e.g. v1.0.0-rc1) - # without pushing a tag for full validation of the workflow. + # Manual dispatch produces build artifacts on the workflow run for inspection. + # The `release` job is gated on event_name == 'push', so workflow_dispatch + # never publishes a public Release — it only validates that the build/verify + # plumbing is healthy before a tag push. workflow_dispatch: - inputs: - tag: - description: "Pre-release tag name (used as the GH Release name; e.g. v1.0.0-rc1)" - required: true - type: string env: CARGO_TERM_COLOR: always @@ -23,7 +22,98 @@ permissions: contents: write jobs: + verify: + # Pre-release gate: must mirror ci.yml so a broken commit cannot reach the + # build/publish jobs. Duplicated rather than refactored into a reusable + # workflow on the eve of 1.0; that refactor is post-release scope. + name: Verify (pre-release gates) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@stable + with: + components: clippy, rustfmt + + - uses: Swatinem/rust-cache@v2 + with: + key: release-verify + + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: cache pyright runtime + uses: actions/cache@v4 + with: + path: ~/.cache/pyright-python/ + key: pyright-python-1.1.409-${{ runner.os }} + + - name: rust fmt + run: cargo fmt --all -- --check + + - name: migration retirement guard + run: | + python scripts/check-migration-retirement.py --self-test + python scripts/check-migration-retirement.py + + - name: rust clippy + run: cargo clippy --workspace --all-targets --all-features -- -D warnings + + - name: install cargo-nextest + uses: taiki-e/install-action@cargo-nextest + + - name: rust build workspace bins + run: cargo build --workspace --bins + + - name: rust nextest + run: cargo nextest run --workspace --all-features --no-tests=pass + + - name: rust doc + run: cargo doc --workspace --no-deps --all-features + env: + RUSTDOCFLAGS: "-D warnings" + + - name: install cargo-deny + uses: taiki-e/install-action@cargo-deny + + - name: cargo deny + run: cargo deny check + + - name: check B.4* gate result freshness + run: python scripts/check-b4-gate-result.py + + - name: check Python ontology version lockstep + run: | + python scripts/check-python-ontology-version.py --self-test + python scripts/check-python-ontology-version.py + + - name: install plugin (dev extras) + run: python -m pip install -e 'plugins/python[dev]' + + - name: ruff check + run: ruff check plugins/python + + - name: ruff format check + run: ruff format --check plugins/python + + - name: mypy + run: mypy --strict plugins/python + + - name: pytest + run: pytest plugins/python + + - name: install sqlite3 cli + run: sudo apt-get update && sudo apt-get install -y --no-install-recommends sqlite3 + + - name: walking skeleton (end-to-end) + run: bash tests/e2e/sprint_1_walking_skeleton.sh + + - name: WP5 secret scanner smoke + run: CARGO_BUILD=0 bash tests/e2e/wp5_secret_scan.sh + build-rust: + needs: [verify] name: Build clarion (${{ matrix.target }}) runs-on: ${{ matrix.runner }} strategy: @@ -61,7 +151,13 @@ jobs: if [ -f LICENSE ]; then cp LICENSE "$STAGE/"; fi ARCHIVE="$STAGE.tar.gz" tar czf "$ARCHIVE" "$STAGE" - sha256sum "$ARCHIVE" > "$ARCHIVE.sha256" + # Portable across the runner matrix: macOS images ship `shasum`, not + # GNU `sha256sum`. Both emit the same ` ` line. + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$ARCHIVE" > "$ARCHIVE.sha256" + else + shasum -a 256 "$ARCHIVE" > "$ARCHIVE.sha256" + fi echo "archive=$ARCHIVE" >> "$GITHUB_OUTPUT" - uses: actions/upload-artifact@v4 @@ -74,6 +170,7 @@ jobs: retention-days: 7 build-plugin: + needs: [verify] name: Build Python plugin sdist runs-on: ubuntu-latest steps: @@ -148,6 +245,18 @@ jobs: run: | set -euo pipefail PREV="${{ steps.prev.outputs.previous }}" + # Derive the actual sdist filename from the staged artifact rather + # than hard-coding it: PEP 625 (and the hatchling build backend) + # normalize the project name and embed the version, so any version + # bump or normalization-rule change would invalidate a literal URL. + # `|| true` so `set -e` does not abort the script when the glob + # is empty — we want to fall through to the explicit error below. + SDIST_PATH=$(ls release/clarion[-_]plugin[-_]python*.tar.gz 2>/dev/null | head -n1 || true) + if [ -z "$SDIST_PATH" ]; then + echo "::error::no Python plugin sdist found in release/ — refusing to publish notes with a broken install link" >&2 + exit 1 + fi + SDIST_NAME=$(basename "$SDIST_PATH") { echo "## Clarion ${GITHUB_REF_NAME}" echo "" @@ -163,7 +272,7 @@ jobs: echo "install clarion-x86_64-unknown-linux-gnu/clarion ~/.local/bin/" echo "" echo "pipx install \\" - echo " https://github.com/${GITHUB_REPOSITORY}/releases/download/${GITHUB_REF_NAME}/clarion-plugin-python-1.0.0.tar.gz" + echo " https://github.com/${GITHUB_REPOSITORY}/releases/download/${GITHUB_REF_NAME}/${SDIST_NAME}" echo "\`\`\`" echo "" echo "Full walkthrough: [docs/operator/getting-started.md](https://github.com/${GITHUB_REPOSITORY}/blob/${GITHUB_REF_NAME}/docs/operator/getting-started.md)." diff --git a/plugins/python/tests/test_package.py b/plugins/python/tests/test_package.py index 07b2108b..e05d83c8 100644 --- a/plugins/python/tests/test_package.py +++ b/plugins/python/tests/test_package.py @@ -4,18 +4,42 @@ import tomllib from pathlib import Path +from typing import Any import clarion_plugin_python _PLUGIN_ROOT = Path(__file__).resolve().parents[1] +def _read_toml(path: Path) -> dict[str, Any]: + return tomllib.loads(path.read_text(encoding="utf-8")) + + def test_package_version_matches_pyproject() -> None: assert clarion_plugin_python.__version__ == "1.0.0" +def test_plugin_version_lockstep_across_pyproject_manifest_and_module() -> None: + """Guard against the version-drift class of bug (clarion-496a8192cc). + + The three values must move together: bumping pyproject without bumping + plugin.toml ships an sdist whose handshake reports the wrong version. + """ + pyproject = _read_toml(_PLUGIN_ROOT / "pyproject.toml") + manifest = _read_toml(_PLUGIN_ROOT / "plugin.toml") + + pyproject_version = pyproject["project"]["version"] + manifest_version = manifest["plugin"]["version"] + module_version = clarion_plugin_python.__version__ + + assert pyproject_version == manifest_version == module_version, ( + f"version drift: pyproject={pyproject_version!r}, " + f"plugin.toml={manifest_version!r}, __init__={module_version!r}" + ) + + def test_manifest_declares_references_edge_kind() -> None: - manifest = tomllib.loads((_PLUGIN_ROOT / "plugin.toml").read_text(encoding="utf-8")) + manifest = _read_toml(_PLUGIN_ROOT / "plugin.toml") assert manifest["plugin"]["version"] == "1.0.0" assert manifest["ontology"]["ontology_version"] == "0.6.0" From 64e1062ad0017caaa6a8d3f96da87a5ffdae9b20 Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Tue, 19 May 2026 20:53:28 +1000 Subject: [PATCH 038/187] docs(release): rebrand v0.1 docset to v1.0 and soften tag-existence claim MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two release-prep docs gaps from the recent review pass. - B6: CHANGELOG.md:24 and CLAUDE.md:7 both stated "Tagged at v1.0.0", but git tag -l shows only v0.1-sprint-1 and v0.1-sprint-2 — the tag itself is cut by the operator once release blockers go green. Reword to "Targets the v1.0.0 tag (cut once release blockers are green)" so the docs are non-mendacious until the actual tag is pushed. - B7: docs/clarion/1.0/{requirements,system-design,detailed-design}.md still self-titled "Clarion v0.1 - ..." with last-updated 2026-04-18, even though the docset now lives under docs/clarion/1.0/ and carries the Sprint 3 federation-hardening additions. Update titles to v1.0 and bump last-updated to 2026-05-19. The 2026-04-17 baseline date stays - that is when the requirements were originally locked. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 5 +++-- CLAUDE.md | 2 +- docs/clarion/1.0/detailed-design.md | 6 +++--- docs/clarion/1.0/requirements.md | 6 +++--- docs/clarion/1.0/system-design.md | 6 +++--- 5 files changed, 13 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3de3eb2..266e381b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,8 +21,9 @@ First publishable release. Clarion ships as a Rust core (`clarion` binary, five workspace crates) plus an editable-install Python language plugin (`clarion-plugin-python`). Released under the [MIT License](LICENSE). -Tagged as `v1.0.0`. Supersedes the pre-release `v0.1-sprint-1` and -`v0.1-sprint-2` working tags; those remain in the repo as historical anchors. +Targets the `v1.0.0` tag (cut by the operator once all release blockers are +green); supersedes the pre-release `v0.1-sprint-1` and `v0.1-sprint-2` +working tags, which remain in the repo as historical anchors. ### Core diff --git a/CLAUDE.md b/CLAUDE.md index 1e1cb2d8..126b3cfc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Repository state -**v1.0.0 — first publishable release.** Tagged at `v1.0.0`. Workspace + Python plugin are at 1.0.0; ADR-014 federation HTTP read API ships with bearer auth, batch resolution, briefing-blocked propagation, and stable per-project `instance_id`. Pre-release working tags `v0.1-sprint-1` and `v0.1-sprint-2` remain in the repo as historical anchors. See [`CHANGELOG.md`](CHANGELOG.md) for the full 1.0 scope and [`docs/implementation/`](docs/implementation/) for sprint-closure artifacts. +**v1.0.0 — first publishable release.** Targets the `v1.0.0` tag (cut once release blockers are green; pre-release working tags `v0.1-sprint-1` and `v0.1-sprint-2` remain in the repo as historical anchors). Workspace + Python plugin are at 1.0.0; ADR-014 federation HTTP read API ships with bearer auth, batch resolution, briefing-blocked propagation, and stable per-project `instance_id`. See [`CHANGELOG.md`](CHANGELOG.md) for the full 1.0 scope and [`docs/implementation/`](docs/implementation/) for sprint-closure artifacts. ### Layout (post-1.0) diff --git a/docs/clarion/1.0/detailed-design.md b/docs/clarion/1.0/detailed-design.md index d2286731..adaca578 100644 --- a/docs/clarion/1.0/detailed-design.md +++ b/docs/clarion/1.0/detailed-design.md @@ -1,7 +1,7 @@ -# Clarion v0.1 — Detailed Design Reference +# Clarion v1.0 — Detailed Design Reference -**Status**: Baselined for v0.1 implementation (post-ADR sprint 2026-04-18) — Layer 3 of the Clarion v0.1 docset, implementation-level reference. Canonical home of the ADR backlog, SQL schema, Rust struct definitions, full YAML config example, exact rule-ID catalogues, wire-format mapping tables, and cross-tool prerequisite lists. -**Baseline**: 2026-04-17 · **Last updated**: 2026-04-18 +**Status**: Baselined for v1.0 release (carried forward from the v0.1 post-ADR-sprint baseline) — Layer 3 of the Clarion v1.0 docset, implementation-level reference. Canonical home of the ADR backlog, SQL schema, Rust struct definitions, full YAML config example, exact rule-ID catalogues, wire-format mapping tables, and cross-tool prerequisite lists. +**Baseline**: 2026-04-17 · **Last updated**: 2026-05-19 **Primary author**: qacona@gmail.com (with Claude) **First customer target**: `/home/john/elspeth` (~425k LOC Python) **Revision**: 5 (final pre-restructure single-file revision; see Appendix D for full history). Post-restructure the three layered docs track changes via git log plus dated edits in this preamble; there is no unified "Rev 6." diff --git a/docs/clarion/1.0/requirements.md b/docs/clarion/1.0/requirements.md index 7f11feb7..798e9531 100644 --- a/docs/clarion/1.0/requirements.md +++ b/docs/clarion/1.0/requirements.md @@ -1,7 +1,7 @@ -# Clarion v0.1 — Requirements +# Clarion v1.0 — Requirements -**Status**: Baselined for v0.1 implementation (post-ADR sprint 2026-04-18) -**Baseline**: 2026-04-17 · **Last updated**: 2026-04-18 +**Status**: Baselined for v1.0 release (carried forward from v0.1 baseline 2026-04-17 with the Sprint 3 federation-hardening additions) +**Baseline**: 2026-04-17 · **Last updated**: 2026-05-19 **Primary author**: qacona@gmail.com (with Claude) **First customer target**: `/home/john/elspeth` (~425k LOC Python) **Companion documents**: diff --git a/docs/clarion/1.0/system-design.md b/docs/clarion/1.0/system-design.md index 19f1d3e3..db422231 100644 --- a/docs/clarion/1.0/system-design.md +++ b/docs/clarion/1.0/system-design.md @@ -1,7 +1,7 @@ -# Clarion v0.1 — System Design +# Clarion v1.0 — System Design -**Status**: Baselined for v0.1 implementation (post-ADR sprint 2026-04-18) — mid-level technical companion to requirements -**Baseline**: 2026-04-17 · **Last updated**: 2026-04-18 +**Status**: Baselined for v1.0 release (carried forward from the v0.1 post-ADR-sprint baseline) — mid-level technical companion to requirements +**Baseline**: 2026-04-17 · **Last updated**: 2026-05-19 **Primary author**: qacona@gmail.com (with Claude) **Companion documents**: - [requirements.md](./requirements.md) — requirements (the *what*) From fcef6a0a690ddfc6aad4902eda86ac04309e5bef Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Tue, 19 May 2026 21:12:35 +1000 Subject: [PATCH 039/187] docs(adr): write ADR-034 for Sprint 3 federation HTTP read API hardening The Sprint 3 federation hardening (commits 1109560, acbf465, eb6200d, 2c3311a) added bearer authentication, a POST /api/v1/files/batch endpoint, a 403 BRIEFING_BLOCKED response, and per-project instance_id echo to Clarion's HTTP read API. None of those decisions had an ADR home, and ADR-014 explicitly says "The HTTP read API is unauthenticated and loopback-only by default" - actively contradicting the shipped bearer-auth posture. The federation contracts.md doc and CLAUDE.md have been attributing the hardenings to ADR-014, but ADR-014's body documents the opposite. With v1.0 about to tag, the load-bearing federation surface needs a real ADR. ADR-034 captures the four hardenings retroactively against the existing implementation: trust matrix (with explicit "non-loopback requires BOTH allow_non_loopback AND token_env" rule), batch endpoint shape (256-cap queries[] body, 16 KiB transport bound, four-partition response), BRIEFING_BLOCKED 403 (omits identifying fields), instance_id provenance (lazy on first clarion serve, mode 0600, deletion is intended drift signal). Alternatives section names HMAC, UDS-only, dual-endpoint discriminator, and in-place ADR-014 amendment - the last is forbidden by the ADR doctrine in CLAUDE.md, which is why this ADR exists as a separate decision rather than as an amendment. ADR-014 carries the only mutation allowed by the immutability rule: its Status field is now "Accepted; partially extended by ADR-034 (Security Posture and Error Envelope sections only - registry-backend protocol decision remains in force)". The ADR README index reflects the same qualifier and adds the ADR-034 row. contracts.md had a closed-enum statement that listed seven error codes but the BATCH_TOO_LARGE code is real in the code (http_read.rs:685) and referenced in contracts.md's own batch endpoint table - the closed-enum sentence is updated to eight codes with the batch-specific note. No code change; pure documentation. Implementing commits already shipped. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../adr/ADR-014-filigree-registry-backend.md | 2 +- ...-034-federation-http-read-api-hardening.md | 159 ++++++++++++++++++ docs/clarion/adr/README.md | 3 +- docs/federation/contracts.md | 6 +- 4 files changed, 166 insertions(+), 4 deletions(-) create mode 100644 docs/clarion/adr/ADR-034-federation-http-read-api-hardening.md diff --git a/docs/clarion/adr/ADR-014-filigree-registry-backend.md b/docs/clarion/adr/ADR-014-filigree-registry-backend.md index f3d56d30..4dd8aa00 100644 --- a/docs/clarion/adr/ADR-014-filigree-registry-backend.md +++ b/docs/clarion/adr/ADR-014-filigree-registry-backend.md @@ -1,6 +1,6 @@ # ADR-014: Filigree `registry_backend` Flag and Pluggable `RegistryProtocol` -**Status**: Accepted +**Status**: Accepted; partially extended by [ADR-034](./ADR-034-federation-http-read-api-hardening.md) (Security Posture and Error Envelope sections only — registry-backend protocol decision remains in force) **Date**: 2026-04-18 **Deciders**: qacona@gmail.com **Context**: Clarion v0.1 integration boundary with Filigree's file registry; joint deliverable (Clarion + Filigree, same author) diff --git a/docs/clarion/adr/ADR-034-federation-http-read-api-hardening.md b/docs/clarion/adr/ADR-034-federation-http-read-api-hardening.md new file mode 100644 index 00000000..396d9663 --- /dev/null +++ b/docs/clarion/adr/ADR-034-federation-http-read-api-hardening.md @@ -0,0 +1,159 @@ +# ADR-034: Federation HTTP Read API Hardening — Bearer Auth, Batch Resolution, `BRIEFING_BLOCKED`, Instance ID + +**Status**: Accepted +**Date**: 2026-05-19 +**Deciders**: qacona@gmail.com +**Context**: Sprint 3 Loom federation hardening (see [`docs/implementation/sprint-3/2026-05-19-loom-federation-hardening-tasking.md`](../../implementation/sprint-3/2026-05-19-loom-federation-hardening-tasking.md)); extends ADR-014's read-API §"Security Posture" and §"Error Envelope" +**Extends**: [ADR-014](./ADR-014-filigree-registry-backend.md) Security Posture and Error Envelope sections only — the registry-backend protocol decision in ADR-014 §"Decision" remains in force. + +## Summary + +Sprint 3 hardens Clarion's HTTP read API beyond ADR-014's original posture: protected routes require an `Authorization: Bearer` token resolved from an operator-named environment variable, a new `POST /api/v1/files/batch` endpoint handles bulk path resolution in a single round trip, a distinct `403 BRIEFING_BLOCKED` response distinguishes blocked entities from "not found" entities without leaking identity, and `GET /api/v1/_capabilities` echoes a stable per-project `instance_id` so siblings can detect endpoint rebinding. These additions extend ADR-014's wire-contract surface without breaking it — `api_version` remains `1`. + +## Context + +ADR-014 §"Security Posture" pinned the HTTP read API as "unauthenticated and loopback-only by default" with non-loopback binds gated by `serve.http.allow_non_loopback: true`. ADR-014 §"Error Envelope" closed the initial `code` enum to `INVALID_PATH`, `PATH_OUTSIDE_PROJECT`, `NOT_FOUND`, `STORAGE_ERROR`, `INTERNAL`. Both were the right posture for the immediate ADR-014 use case (loopback-only Filigree sidecar) but were under-specified for the broader Sprint 3 federation goals: + +- Filigree may run on a different host than Clarion in production deployments, and the operator should not have to layer a separate auth proxy for what is a publisher-side concern. +- Filigree's `ClarionRegistry` cold-start hydrates N file records by re-asking Clarion for each path. The single-file `GET` endpoint costs one pooled SQLite connection per call; for a 1000-file warm-up that is 1000 round trips and 1000 pool acquisitions. +- `briefing_blocked` entities (per ADR-013 secret-scan briefing block) are not "not found" — they exist but are policy-blocked. The original `NOT_FOUND` masking conflates "Clarion does not know this file" (registry coverage gap; escalate) with "Clarion knows this file but refuses to expose it" (briefing block; wait for re-scan or fix the secret). Filigree needs to distinguish the two. +- `_capabilities` is the only handshake point Filigree sees before issuing requests. If an operator rebinds the same `bind` to a different Clarion project (different `.clarion/` root, different cache), Filigree has no fingerprint to detect the swap. + +Sprint 3 implemented these four hardenings (`1109560`, `acbf465`, `eb6200d`, `2c3311a`) and pinned the resulting wire contract in [`docs/federation/contracts.md`](../../federation/contracts.md). The body of ADR-014, per the ADR immutability rule in [`/home/john/clarion/CLAUDE.md`](../../../CLAUDE.md) §"Editorial conventions", cannot be rewritten in place. This ADR is therefore the authoritative source for the four hardenings; ADR-014 carries the standard `Accepted; partially extended by ADR-034 (Security Posture)` status amendment. + +## Decision + +### 1. Bearer authentication on protected routes + +`/api/v1/files`-family endpoints require `Authorization: Bearer ` when Clarion has resolved a token at startup. `/api/v1/_capabilities` is **always** unauthenticated so siblings can probe the API surface before they hold a secret. + +Token-resolution and bind-policy trust matrix, enforced at startup by `HttpReadConfig::validate_auth_trust` before the listener binds: + +| Bind | `token_env` resolved | Behaviour | +|---|---|---| +| Loopback | unset | Unauthenticated; allow all requests (matches ADR-014's original posture for the loopback case). | +| Loopback | set | Bearer required on protected routes; capabilities always allowed. | +| Non-loopback | unset | **Refuse to start** with `CLA-CONFIG-HTTP-NO-AUTH`. | +| Non-loopback | set | Bearer required on protected routes. | + +Non-loopback-without-auth refusal extends ADR-014's `allow_non_loopback` opt-in: there is no longer a "non-loopback unauthenticated" mode. Non-loopback binds require **both** `allow_non_loopback: true` **and** a resolved `token_env`; either alone is insufficient. The opt-in remains the gate that admits non-loopback binds at all, but it no longer admits them unauthenticated. + +Bearer rejection (any of: header absent, wrong scheme, wrong token, blank token) returns `401 Unauthorized` with the standard error envelope and `code: "UNAUTHORIZED"`. Token comparison is constant-time so a wrong-length-token client cannot distinguish "header absent" from "token mismatch" via timing. The token value is never logged; the bind-time log line records `auth=bearer` or `auth=none`, not the token itself. + +The token is resolved from an operator-named environment variable; the config field is `serve.http.token_env` (default `CLARION_LOOM_TOKEN`, matching Filigree's pinned client default). The token value itself never appears in `clarion.yaml`. + +### 2. `POST /api/v1/files/batch` + +Bulk file-identity resolution. One pooled SQLite connection serves the whole batch. + +Request body shape (closed): + +```json +{ + "queries": [ + {"path": "src/a.py", "language": null}, + {"path": "src/b.py", "language": "python"} + ] +} +``` + +Response body shape (closed; arrays are disjoint partitions of the input): + +```json +{ + "resolved": [/* BatchResolvedItem */], + "not_found": [/* requested paths */], + "briefing_blocked": [/* requested paths */], + "errors": [/* BatchErrorItem with code+message */] +} +``` + +The per-batch cap is **256 queries** (`BATCH_MAX_QUERIES` in [`crates/clarion-cli/src/http_read.rs`](../../../crates/clarion-cli/src/http_read.rs); referenced as `queries.len() > 256` in `contracts.md` §"`POST /api/v1/files/batch`"). The request body is additionally bounded at the transport layer to 16 KiB. A `queries` array that exceeds the cap returns `400 BATCH_TOO_LARGE` (new error code, see §3). The cap is not operator-configurable in v1.0 — it is pinned on the wire so Filigree client splitting logic can compile-in the limit. A future incompatible change to the cap is the trigger for `api_version: 2`, not a per-server override. + +Individual-item errors (`INVALID_PATH`, `PATH_OUTSIDE_PROJECT`, `STORAGE_ERROR`, `INTERNAL`) go into the `errors` array; the whole request still returns `200 OK` so partial-success semantics are explicit on the wire. Briefing-blocked items are partitioned to the `briefing_blocked` array; the per-item envelope deliberately does not include `entity_id`, `content_hash`, `canonical_path`, or `language` so callers cannot infer file identity from a block-classified item. + +### 3. `BRIEFING_BLOCKED` 403 on single-file `GET` + +`GET /api/v1/files?path=` for an entity whose `briefing_blocked` anchor is set (per ADR-013) returns `403 Forbidden` with `code: "BRIEFING_BLOCKED"`. The 403 envelope omits `entity_id`, `content_hash`, `canonical_path`, and `language`. The structural signal that "Clarion knows this file but is refusing to expose it" is therefore *only* the status code and `code` discriminator, not any payload field. + +ADR-014's original error-code set is extended to: + +``` +INVALID_PATH | PATH_OUTSIDE_PROJECT | NOT_FOUND | +BRIEFING_BLOCKED | UNAUTHORIZED | STORAGE_ERROR | +BATCH_TOO_LARGE | INTERNAL +``` + +The set remains closed. Clients must switch on `code`, not on `error` text. + +### 4. Stable per-project `instance_id` in `_capabilities` + +`GET /api/v1/_capabilities` echoes: + +```json +{ + "api_version": 1, + "instance_id": "9bd7234e-6d44-4a38-9ae4-76f912a10221", + "registry_backend": true, + "file_registry": true +} +``` + +`instance_id` is a v4 UUID created lazily on first `clarion serve` (via `instance::load_or_create` at [`crates/clarion-cli/src/serve.rs:29`](../../../crates/clarion-cli/src/serve.rs)) and persisted to `.clarion/instance_id`. Subsequent `clarion serve` invocations read the existing value. The file is created with mode `0600` on Unix. The ID is stable for the life of that `.clarion/` directory; deleting `.clarion/instance_id` (or the whole `.clarion/` tree) on the next `clarion serve` produces a fresh UUID, and that is intended — siblings use the change as the trigger to invalidate cached identity bindings. The file is excluded from git per ADR-005's exclusion list for per-machine state. + +`instance_id` is **not** the same as a deployment fingerprint, a Filigree project ID, or a Clarion release. It is exactly the identity of one local `.clarion/` directory. + +## Alternatives Considered + +### Alternative 1: HMAC signatures instead of bearer tokens + +Better cryptographic story (per-request authentication, replay resistance, no shared-secret leakage on `GET` URLs), but considerably more client-side complexity. Filigree's `ClarionRegistry` would need an HMAC helper, and the operator deployment story would require signing-key rotation discipline. Sprint 3 needed *some* authentication on the wire — bearer tokens are the path of least resistance and the v1.0 federation surface is small enough that a coarse "any reader, full access" trust model is acceptable. HMAC is tracked as post-1.0 hardening (filigree `clarion-6814b2ad90`). + +### Alternative 2: Unix Domain Socket-only auth (ADR-012's original posture) + +ADR-012 (now superseded) had UDS as the default auth model. Sprint 3's deployment story explicitly admits non-loopback Filigree — a UDS-only model would force Filigree into a sidecar-on-same-host topology that the federation contract does not otherwise require. Bearer over TCP keeps the federation deployment-topology-agnostic. + +### Alternative 3: A separate `/api/v1/files/inquire` endpoint for "is this blocked?" + +Distinguishing block from not-found with a second endpoint avoids the 403/404 split at the cost of two round trips and a more elaborate wire contract. Filigree's actual question on a single `GET` is "which of these three states is this path in?" and the single-endpoint, status-code-discriminated answer is the simpler shape. Declined. + +### Alternative 4: Amend ADR-014 in place + +The Sprint 3 tasking doc originally proposed pinning these decisions into ADR-014's body. CLAUDE.md's editorial-conventions section forbids in-place ADR mutation; the doctrine is "to revise an Accepted ADR, write a new ADR that supersedes it." This ADR is that new ADR. ADR-014's status field carries the only allowed mutation (status amendment + reference forward to ADR-034). + +## Consequences + +### Positive + +- The federation HTTP read API now has a per-request authentication primitive. ADR-014's "unauthenticated, loopback-only" promise was a deliberate Sprint 1/2 simplification that was load-bearing for the v1.0 federation surface; the gap is now closed. +- Filigree's cold-start hydration cost drops from O(N) round trips to O(1) with the batch endpoint. The per-item error partitioning means a single malformed path in a 1000-path batch does not poison the whole request. +- `BRIEFING_BLOCKED` is mechanically distinguishable from `NOT_FOUND`. Filigree's escalation logic can branch on the two cases without parsing error text or re-issuing probe requests. +- `instance_id` gives Filigree a stable handle to detect endpoint rebinding. Pre-Sprint-3, a `bind` pointed at a swapped `.clarion/` was indistinguishable from a fresh start; that ambiguity is now resolved on the first capability probe after the swap. + +### Negative + +- Operators upgrading from ADR-014's unauthenticated posture to a non-loopback federation deployment must now provide a token at startup. The `CLA-CONFIG-HTTP-NO-AUTH` startup refusal makes this a fail-closed migration, not a silent one, but the operator-facing diff is non-trivial. +- Bearer tokens are coarser than HMAC. Any client that holds the token has full read access to the federation surface, and a token leak requires a rotation across both Clarion and every Filigree client. Post-1.0 hardening (HMAC, key rotation) is required for richer deployments. +- The error-code enum is now wider than ADR-014's original. Filigree's `code`-switch logic must handle `BRIEFING_BLOCKED`, `UNAUTHORIZED`, and `BATCH_TOO_LARGE` in addition to the original five. The federation contract documents this; clients that ignore the new codes will surface them as unhandled errors rather than misinterpret them. + +### Neutral + +- `api_version` remains `1`. The additions are non-breaking augmentations of the v1 wire contract — every pre-Sprint-3 client request shape is still accepted, just with the added option to authenticate, batch, and discriminate on the wider error set. An incompatible change to the read API will be the trigger for `api_version: 2`, not the introduction of these hardenings. +- The Sprint-3 tasking-doc items C1, C2, C5, C7, C8, C9, C10, C11 are not addressed by this ADR — they cover storage correctness and runtime supervision rather than wire-contract decisions. They land directly in the implementation without an ADR change because they implement, rather than amend, ADR-014's existing contract. + +## Related Decisions + +- [ADR-012](./ADR-012-http-auth-default.md) — original HTTP auth ADR; Superseded by ADR-014, whose Security Posture is in turn partially extended by this ADR. +- [ADR-013](./ADR-013-pre-ingest-secret-scanner.md) — defines the briefing-block anchor whose wire propagation §3 pins. +- [ADR-014](./ADR-014-filigree-registry-backend.md) — registry-backend protocol; §"Security Posture" and §"Error Envelope" extended by this ADR. Other sections (capability probe contents beyond `instance_id`, file identity semantics, canonical-path semantics) remain authoritative. +- [ADR-033](./ADR-033-v1.0-distribution.md) — v1.0 distribution; the hardenings here are part of the 1.0 federation cut. + +## References + +- Wire spec: [`docs/federation/contracts.md`](../../federation/contracts.md) +- Sprint 3 tasking: [`docs/implementation/sprint-3/2026-05-19-loom-federation-hardening-tasking.md`](../../implementation/sprint-3/2026-05-19-loom-federation-hardening-tasking.md) +- Implementing commits: + - `1109560` feat(http_read): return 403 BRIEFING_BLOCKED for blocked entities + - `acbf465` feat(http_read): require Authorization: Bearer for /api/v1/files + - `eb6200d` feat(http_read): add POST /api/v1/files/batch + document path normalization + - `2c3311a` feat(http_read): formatting / fix(instance): UUID generation comments / test(serve) diff --git a/docs/clarion/adr/README.md b/docs/clarion/adr/README.md index ffdace1e..19fb0759 100644 --- a/docs/clarion/adr/README.md +++ b/docs/clarion/adr/README.md @@ -16,7 +16,7 @@ This folder is the canonical home for authored Clarion architecture decision rec | [ADR-011](./ADR-011-writer-actor-concurrency.md) | Writer-actor concurrency with per-N-files transactions; `--shadow-db` opt-in | Accepted | | [ADR-012](./ADR-012-http-auth-default.md) | HTTP read-API authentication — UDS default with token fallback | Superseded for ADR-014 registry-backend API | | [ADR-013](./ADR-013-pre-ingest-secret-scanner.md) | Pre-ingest secret scanner with LLM-dispatch block | Accepted | -| [ADR-014](./ADR-014-filigree-registry-backend.md) | Filigree `registry_backend` flag and pluggable `RegistryProtocol` | Accepted | +| [ADR-014](./ADR-014-filigree-registry-backend.md) | Filigree `registry_backend` flag and pluggable `RegistryProtocol` | Accepted; partially extended by ADR-034 | | [ADR-015](./ADR-015-wardline-filigree-emission.md) | Wardline→Filigree emission ownership — Clarion-side SARIF translator (v0.1), native Wardline emitter (v0.2) | Accepted | | [ADR-016](./ADR-016-observation-transport.md) | Observation transport — MCP-spawn (v0.1), Filigree HTTP endpoint (v0.2) | Accepted | | [ADR-017](./ADR-017-severity-and-dedup.md) | Severity mapping, rule-ID round-trip, and dedup policy | Accepted | @@ -34,6 +34,7 @@ This folder is the canonical home for authored Clarion architecture decision rec | [ADR-031](./ADR-031-schema-validation-policy.md) | Schema-validation policy — CHECK on closed core-owned vocabularies (`findings.{kind,severity,status}`, `runs.status`); writer-actor + manifest are the only enforcement layer for plugin-extensible vocabularies (`entities.kind`, `edges.kind`) | Accepted | | [ADR-032](./ADR-032-weighted-components-clustering-fallback.md) | Weighted-components clustering fallback naming | Accepted | | [ADR-033](./ADR-033-v1.0-distribution.md) | v1.0 distribution via GitHub Releases (binary matrix + Python sdist; promote to crates.io/PyPI at v2.0) | Accepted | +| [ADR-034](./ADR-034-federation-http-read-api-hardening.md) | Federation HTTP read API hardening — bearer auth, batch resolution, `BRIEFING_BLOCKED`, instance ID | Accepted | ## Backlog still tracked in the detailed design diff --git a/docs/federation/contracts.md b/docs/federation/contracts.md index 7c093cdb..862fc51a 100644 --- a/docs/federation/contracts.md +++ b/docs/federation/contracts.md @@ -64,8 +64,10 @@ All non-2xx responses use this closed JSON error envelope: The initial `code` enum is closed to `INVALID_PATH`, `PATH_OUTSIDE_PROJECT`, `NOT_FOUND`, `BRIEFING_BLOCKED`, `UNAUTHORIZED`, -`STORAGE_ERROR`, and `INTERNAL`. Clients must switch on `code`; `error` -is human-readable diagnostic text. +`STORAGE_ERROR`, `BATCH_TOO_LARGE`, and `INTERNAL`. Clients must switch +on `code`; `error` is human-readable diagnostic text. `BATCH_TOO_LARGE` +is only emitted by `POST /api/v1/files/batch` (see the batch endpoint +section below). ### `GET /api/v1/files?path=&language=` From 30e7888cd4a2390b560ede7a9d0431afdda33dc1 Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Tue, 19 May 2026 21:12:50 +1000 Subject: [PATCH 040/187] chore(release-hygiene): ADR count, Sprint 3 closure, RUSTDOCFLAGS gate, version lockstep Four related release-hygiene fixes from the v1.0 important-not-fatal review pass. - I2: CLAUDE.md and CHANGELOG.md said "ADR-008 was superseded by ADR-014", but ADR-008 has never existed in this repo (the file is absent from docs/clarion/adr/, and ADR-012's own Status field names itself as the superseded one). Fix the typo and refresh the count to reflect ADR-034 (28 Accepted at 1.0, ADR-012 superseded by ADR-014, ADR-014 partially extended by ADR-034). - I6: ci.yml's doc step ran cargo doc without RUSTDOCFLAGS=-D warnings. CLAUDE.md's "Build / test commands" section declares the strict gate; ci.yml was silently below it. Matches the release.yml verify job pattern landed in the previous batch. - I7: v0.1-plan.md only marked Sprint 1 closed and Sprint 2 amended. Sprint 3 (Loom federation hardening, pinned in the new ADR-034) is closed alongside the v1.0 release; reflect that in the Status line and the Amendments list. - I8: scripts/check-workspace-version-lockstep.py asserts that Cargo.toml [workspace.package].version equals plugins/python/pyproject.toml [project].version. The Rust side is already single-sourced through workspace.package; the Python plugin's internal three-way agreement (pyproject / plugin.toml / __init__) is guarded by the pytest cross-check landed in the previous batch. This script catches the wider ecosystem drift (Rust bumped, Python not, or vice versa). Wired into ci.yml's rust job alongside the migration retirement guard, and into release.yml's verify job for the pre-tag gate. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/ci.yml | 5 ++ .github/workflows/release.yml | 3 + CHANGELOG.md | 8 +- CLAUDE.md | 2 +- docs/implementation/v0.1-plan.md | 8 +- scripts/check-workspace-version-lockstep.py | 85 +++++++++++++++++++++ 6 files changed, 104 insertions(+), 7 deletions(-) create mode 100755 scripts/check-workspace-version-lockstep.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 54ba837a..eb140763 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,6 +34,9 @@ jobs: python scripts/check-migration-retirement.py --self-test python scripts/check-migration-retirement.py + - name: cross-workspace version lockstep + run: python scripts/check-workspace-version-lockstep.py + - name: clippy run: cargo clippy --workspace --all-targets --all-features -- -D warnings @@ -52,6 +55,8 @@ jobs: - name: doc run: cargo doc --workspace --no-deps --all-features + env: + RUSTDOCFLAGS: "-D warnings" - name: install cargo-deny uses: taiki-e/install-action@cargo-deny diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 90d7acee..2b57ffbd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -57,6 +57,9 @@ jobs: python scripts/check-migration-retirement.py --self-test python scripts/check-migration-retirement.py + - name: cross-workspace version lockstep + run: python scripts/check-workspace-version-lockstep.py + - name: rust clippy run: cargo clippy --workspace --all-targets --all-features -- -D warnings diff --git a/CHANGELOG.md b/CHANGELOG.md index 266e381b..2b83145e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -121,9 +121,11 @@ normative. - Design ladder under [`docs/clarion/1.0/`](docs/clarion/1.0/) — `requirements.md`, `system-design.md`, `detailed-design.md`. - ADRs under [`docs/clarion/adr/`](docs/clarion/adr/) — 28 Accepted at 1.0 - (through ADR-033). ADR-008 is superseded by ADR-014; four ADRs - (ADR-009, ADR-010, ADR-019, ADR-020) remain Backlog and are tracked - inside `system-design.md` §12 / `detailed-design.md` §11 until promoted. + (through ADR-034). ADR-012 is superseded by ADR-014, whose Security + Posture and Error Envelope are in turn partially extended by ADR-034 + for the Sprint 3 federation hardening. Four ADRs (ADR-009, ADR-010, + ADR-019, ADR-020) remain Backlog and are tracked inside + `system-design.md` §12 / `detailed-design.md` §11 until promoted. - Loom-suite doctrine at [`docs/suite/loom.md`](docs/suite/loom.md). - Federation contract surface at [`docs/federation/contracts.md`](docs/federation/contracts.md). - Operator guides under [`docs/operator/`](docs/operator/) — getting-started, diff --git a/CLAUDE.md b/CLAUDE.md index 126b3cfc..1fb4ffe1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -97,7 +97,7 @@ docs/ When the same fact appears in multiple files, this is the precedence: -1. **Accepted ADRs** in `docs/clarion/adr/` — the locked decisions. 28 are Accepted at 1.0; four remain Backlog (ADR-009, ADR-010, ADR-019, ADR-020) and are tracked inside `system-design.md` §12 / `detailed-design.md` §11 until promoted. ADR-008 was superseded by ADR-014. +1. **Accepted ADRs** in `docs/clarion/adr/` — the locked decisions. 28 are Accepted at 1.0 (ADR-001…ADR-007, ADR-011, ADR-013…ADR-018, ADR-021…ADR-034); four remain Backlog (ADR-009, ADR-010, ADR-019, ADR-020) and are tracked inside `system-design.md` §12 / `detailed-design.md` §11 until promoted. ADR-012 was superseded by ADR-014 (which is in turn partially extended by ADR-034 for the federation read-API security posture and error envelope). 2. **`requirements.md`** — REQ-/NFR-/CON-/NG- IDs are stable and load-bearing (filigree issues and commit messages cite them by ID; never reuse a retired ID). 3. **`system-design.md`** — `Addresses:` headers on each §2–§11 section define the requirement acceptance surface for that subsystem. 4. **`detailed-design.md`** — exact schemas, rule catalogues, appendices. diff --git a/docs/implementation/v0.1-plan.md b/docs/implementation/v0.1-plan.md index 9f5298f6..92dcf131 100644 --- a/docs/implementation/v0.1-plan.md +++ b/docs/implementation/v0.1-plan.md @@ -1,11 +1,13 @@ # Clarion v0.1 — Implementation Plan (High-Level Overview) -**Status**: AMENDED — Sprint 1 closed (`v0.1-sprint-1`); Sprint 2 resequenced under [`sprint-2/scope-amendment-2026-05.md`](./sprint-2/scope-amendment-2026-05.md). The WP body below is the original framing and remains the architectural reference; the resequence memo is the authoritative source for what ships in v0.1 vs. what slips to v0.2. **Read both.** +**Status**: AMENDED — Sprint 1 closed (`v0.1-sprint-1`); Sprint 2 resequenced under [`sprint-2/scope-amendment-2026-05.md`](./sprint-2/scope-amendment-2026-05.md) and closed (`v0.1-sprint-2`); Sprint 3 (Loom federation hardening, ADR-014 → ADR-034) closed 2026-05-19 — see [`sprint-3/2026-05-19-loom-federation-hardening-tasking.md`](./sprint-3/2026-05-19-loom-federation-hardening-tasking.md). The WP body below is the original framing and remains the architectural reference; the Sprint 2 resequence memo and Sprint 3 tasking doc are the authoritative sources for what shipped in v1.0 vs. what slips to v1.1+. **Read all three.** **Date opened**: 2026-04-18 -**Last amended**: 2026-05-16 +**Last amended**: 2026-05-19 **Plan owner**: John Morrissey (primary dev of Clarion, Filigree, Wardline) **Predecessor**: [`../clarion/v0.1/plans/v0.1-scope-commitments.md`](./v0.1-scope-plans/v0.1-scope-commitments.md) (scope locked 2026-04-18) -**Amendments**: [`sprint-2/scope-amendment-2026-05.md`](./sprint-2/scope-amendment-2026-05.md) (2026-05-16 — narrows WP6, splits WP9 into A/B, defers WP4/5/7/10/11-as-scheduled, pulls WP8 + WP9-A into Sprint 2 for MVP MCP surface) +**Amendments**: +- [`sprint-2/scope-amendment-2026-05.md`](./sprint-2/scope-amendment-2026-05.md) (2026-05-16 — narrows WP6, splits WP9 into A/B, defers WP4/5/7/10/11-as-scheduled, pulls WP8 + WP9-A into Sprint 2 for MVP MCP surface). +- [`sprint-3/2026-05-19-loom-federation-hardening-tasking.md`](./sprint-3/2026-05-19-loom-federation-hardening-tasking.md) (2026-05-19 — Loom federation hardening sprint: bearer auth, batch resolution, `BRIEFING_BLOCKED` propagation, per-project `instance_id`; pinned in ADR-034). Closed alongside the v1.0 release. **Audience**: implementation engineer (the author + future contributors), Filigree-side issue-tracker seeder, design reviewers checking that every system-design section has an owning work package. diff --git a/scripts/check-workspace-version-lockstep.py b/scripts/check-workspace-version-lockstep.py new file mode 100755 index 00000000..5997eafb --- /dev/null +++ b/scripts/check-workspace-version-lockstep.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +"""Cross-workspace version lockstep guard. + +Asserts that the Rust workspace package version +(`Cargo.toml [workspace.package].version`) matches the Python plugin's +`[project].version` (`plugins/python/pyproject.toml`). The Python plugin +manifest and `__init__.__version__` are already cross-checked by +`plugins/python/tests/test_package.py`; this script catches the wider +ecosystem-level drift (someone bumps Cargo but not the Python plugin, or +vice versa) before a release tag is cut. + +Designed to run as a CI step on every PR — fast, no third-party deps, +parses with the stdlib `tomllib` (Python 3.11+). + +Exit codes: + 0 versions agree + 1 versions disagree, or required keys are missing + 2 usage / I/O error (e.g. file not found) + +The acceptable-drift policy in v1.0 is "no drift": the Rust workspace and +Python plugin ship as a single product version. Post-1.0 patch releases +that need divergent semver should drop the strict-equality check and +replace it with a compatibility-bound check in the same script. +""" + +from __future__ import annotations + +import sys +import tomllib +from pathlib import Path +from typing import Any + +REPO_ROOT = Path(__file__).resolve().parents[1] +CARGO_TOML = REPO_ROOT / "Cargo.toml" +PYPROJECT_TOML = REPO_ROOT / "plugins/python/pyproject.toml" + + +def _read_toml(path: Path) -> dict[str, Any]: + if not path.is_file(): + print(f"check-workspace-version-lockstep: missing {path}", file=sys.stderr) + sys.exit(2) + return tomllib.loads(path.read_text(encoding="utf-8")) + + +def _get_path(data: dict[str, Any], *keys: str) -> Any: + cursor: Any = data + for key in keys: + if not isinstance(cursor, dict) or key not in cursor: + path_repr = ".".join(keys) + print( + f"check-workspace-version-lockstep: key {path_repr!r} not found", + file=sys.stderr, + ) + sys.exit(1) + cursor = cursor[key] + return cursor + + +def main() -> int: + cargo = _read_toml(CARGO_TOML) + pyproject = _read_toml(PYPROJECT_TOML) + + rust_version = _get_path(cargo, "workspace", "package", "version") + python_version = _get_path(pyproject, "project", "version") + + if rust_version != python_version: + print( + "check-workspace-version-lockstep: drift detected", + file=sys.stderr, + ) + print(f" Cargo.toml [workspace.package].version = {rust_version!r}", file=sys.stderr) + print(f" plugins/python/pyproject.toml [project].version = {python_version!r}", file=sys.stderr) + print( + "Bump them in lockstep, or split this guard into compatibility-bounded " + "ranges if v1.0+ wants divergent semver.", + file=sys.stderr, + ) + return 1 + + print(f"check-workspace-version-lockstep: ok ({rust_version})") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From 015d453ab681aba94a547a82fb54fdb73c40cc79 Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Tue, 19 May 2026 23:30:19 +1000 Subject: [PATCH 041/187] fix(secret_scan): clear stale briefing_blocked on cleaned sidecars MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ensure_finding_anchors iterated only finding_files() — paths producing a finding *this* run. For a sidecar-only path (.env etc.) the anchor row that ran N created with briefing_blocked=secret_present + content_hash X would persist unchanged into run N+1 after the operator cleaned the file: the loop never visited the now-clean path, and sidecars have no plugin coverage to refresh the catalog. The /api/v1/files and MCP summary surfaces would keep serving a false block, operator-invisible. Adds a scanned_sidecars BTreeSet (subset of scanned_files restricted by is_secret_scan_sidecar) and a second reconciliation pass that upserts a core:file anchor for every scanned sidecar not anchored by pass 1. The upsert (writer.InsertEntity is ON CONFLICT DO UPDATE) drops briefing_blocked when the file is clean and refreshes content_hash against the current bytes. Always-clean sidecars also get a first-time anchor here. Scope is narrowed to sidecars because source files always carry plugin entities — remember_finding_anchors maps them into finding_anchors before persist_findings runs, so pass 1's contains_key short-circuit keeps source paths from getting a core:file shadow. Removed sidecar files (no longer in scanned_sidecars) keep their stale anchor; called out in the inline comment as a separate concern outside this fix's surface. Regression tests: - cleared_sidecar_clears_stale_briefing_block: secret .env → clean .env across two analyze runs; anchor row's briefing_blocked drops to NULL and content_hash refreshes. - clean_sidecar_creates_anchor_without_block: always-clean .env gets a core:file anchor with no briefing_blocked on first scan. Closes clarion-2e34dbbdec. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/clarion-cli/src/secret_scan.rs | 16 +++ crates/clarion-cli/src/secret_scan/anchors.rs | 127 +++++++++++------- crates/clarion-cli/src/secret_scan/files.rs | 2 +- crates/clarion-cli/tests/secret_scan.rs | 111 +++++++++++++++ 4 files changed, 209 insertions(+), 47 deletions(-) diff --git a/crates/clarion-cli/src/secret_scan.rs b/crates/clarion-cli/src/secret_scan.rs index b1576e3f..f73dd853 100644 --- a/crates/clarion-cli/src/secret_scan.rs +++ b/crates/clarion-cli/src/secret_scan.rs @@ -86,6 +86,13 @@ pub(crate) struct SecretScanOutcome { finding_anchors: BTreeMap, override_files: Vec, scanned_files: BTreeSet, + // Subset of `scanned_files` restricted to paths matched by + // `files::is_secret_scan_sidecar` (e.g. `.env`, `.env.*`, `*.env`). + // These paths have no plugin coverage by design, so their `core:file` + // anchor is the only catalog row tracking briefing_blocked + content_hash + // for that path. The anchor-reconciliation pass in `anchors.rs` iterates + // this set on every run to clear stale blocks once secrets are removed. + scanned_sidecars: BTreeSet, } impl SecretScanOutcome { @@ -93,6 +100,10 @@ impl SecretScanOutcome { &self.scanned_files } + pub(crate) fn scanned_sidecars(&self) -> &BTreeSet { + &self.scanned_sidecars + } + pub(crate) fn finding_files(&self) -> BTreeSet { self.findings .iter() @@ -143,10 +154,14 @@ pub(crate) fn pre_ingest( let mut all_allowed = Vec::new(); let mut baseline_matches = Vec::new(); let mut scanned_files = BTreeSet::new(); + let mut scanned_sidecars = BTreeSet::new(); for file in source_files { let canonical_file = canonical_or_original(file); scanned_files.insert(canonical_file.clone()); + if files::is_secret_scan_sidecar(&canonical_file) { + scanned_sidecars.insert(canonical_file.clone()); + } let buf = fs::read(file).with_context(|| format!("read {}", file.display()))?; let detections = scanner.scan_bytes(&buf); let baseline_file = project_relative_path(project_root, &canonical_file); @@ -242,6 +257,7 @@ pub(crate) fn pre_ingest( finding_anchors: BTreeMap::new(), override_files: override_files.into_iter().collect(), scanned_files, + scanned_sidecars, }) } diff --git a/crates/clarion-cli/src/secret_scan/anchors.rs b/crates/clarion-cli/src/secret_scan/anchors.rs index 14319af4..e2865896 100644 --- a/crates/clarion-cli/src/secret_scan/anchors.rs +++ b/crates/clarion-cli/src/secret_scan/anchors.rs @@ -1,4 +1,7 @@ -use std::{fs, path::Path}; +use std::{ + fs, + path::{Path, PathBuf}, +}; use anyhow::{Context, Result}; use clarion_storage::{ @@ -48,58 +51,90 @@ async fn ensure_finding_anchors( project_root: &Path, started_at: &str, ) -> Result<()> { + // Pass 1: paths with active findings this run get anchored with whatever + // briefing_blocks reason applies (or none, if an override cleared it). for file in outcome.finding_files() { let key = canonical_or_original(&file); if outcome.finding_anchors.contains_key(&key) { continue; } - let id = secret_finding_anchor_id(project_root, &key); - let relative = display_relative(project_root, &key); - let short_name = key - .file_name() - .and_then(|name| name.to_str()) - .unwrap_or(&relative) - .to_owned(); - let mut properties = serde_json::Map::new(); - properties.insert("finding_anchor".to_owned(), serde_json::json!(true)); - if let Some(reason) = outcome.briefing_blocks.get(&key) { - properties.insert( - "briefing_blocked".to_owned(), - serde_json::Value::String(reason.as_str().to_owned()), - ); + upsert_finding_anchor(outcome, writer, project_root, started_at, key).await?; + } + // Pass 2: every sidecar path scanned this run that pass 1 did not anchor + // (i.e. no current finding). The upsert refreshes properties + content_hash + // on a sidecar whose secret was cleaned between runs (the old anchor row + // would otherwise keep briefing_blocked=secret_present and a stale hash + // forever, since pass 1 only sees current findings). Always-clean sidecars + // also get a first-time anchor here. Scope is intentionally narrowed to + // sidecars: source files have plugin entities (registered by + // `remember_finding_anchors` before this function runs), so their + // `finding_anchors` entry already points at the plugin entity and pass 1's + // `contains_key` guard skips them. Removed files (no longer in + // scanned_sidecars) keep their stale anchor — a separate concern tracked + // outside this fix. + for file in outcome.scanned_sidecars().clone() { + let key = canonical_or_original(&file); + if outcome.finding_anchors.contains_key(&key) { + continue; } - let record = EntityRecord { - id: id.clone(), - plugin_id: "core".to_owned(), - kind: "file".to_owned(), - name: relative, - short_name, - parent_id: None, - source_file_id: None, - source_file_path: Some(key.display().to_string()), - source_byte_start: None, - source_byte_end: None, - source_line_start: None, - source_line_end: None, - properties_json: serde_json::Value::Object(properties).to_string(), - content_hash: file_content_hash(&key), - summary_json: None, - wardline_json: None, - first_seen_commit: None, - last_seen_commit: None, - created_at: started_at.to_owned(), - updated_at: started_at.to_owned(), - }; - writer - .send_wait(|ack| WriterCmd::InsertEntity { - entity: Box::new(record), - ack, - }) - .await - .map_err(|e| anyhow::anyhow!("{e}")) - .with_context(|| format!("InsertEntity for secret finding anchor {id}"))?; - outcome.finding_anchors.insert(key, id); + upsert_finding_anchor(outcome, writer, project_root, started_at, key).await?; + } + Ok(()) +} + +async fn upsert_finding_anchor( + outcome: &mut SecretScanOutcome, + writer: &Writer, + project_root: &Path, + started_at: &str, + key: PathBuf, +) -> Result<()> { + let id = secret_finding_anchor_id(project_root, &key); + let relative = display_relative(project_root, &key); + let short_name = key + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or(&relative) + .to_owned(); + let mut properties = serde_json::Map::new(); + properties.insert("finding_anchor".to_owned(), serde_json::json!(true)); + if let Some(reason) = outcome.briefing_blocks.get(&key) { + properties.insert( + "briefing_blocked".to_owned(), + serde_json::Value::String(reason.as_str().to_owned()), + ); } + let record = EntityRecord { + id: id.clone(), + plugin_id: "core".to_owned(), + kind: "file".to_owned(), + name: relative, + short_name, + parent_id: None, + source_file_id: None, + source_file_path: Some(key.display().to_string()), + source_byte_start: None, + source_byte_end: None, + source_line_start: None, + source_line_end: None, + properties_json: serde_json::Value::Object(properties).to_string(), + content_hash: file_content_hash(&key), + summary_json: None, + wardline_json: None, + first_seen_commit: None, + last_seen_commit: None, + created_at: started_at.to_owned(), + updated_at: started_at.to_owned(), + }; + writer + .send_wait(|ack| WriterCmd::InsertEntity { + entity: Box::new(record), + ack, + }) + .await + .map_err(|e| anyhow::anyhow!("{e}")) + .with_context(|| format!("InsertEntity for secret finding anchor {id}"))?; + outcome.finding_anchors.insert(key, id); Ok(()) } diff --git a/crates/clarion-cli/src/secret_scan/files.rs b/crates/clarion-cli/src/secret_scan/files.rs index 9743a506..b4cc74fd 100644 --- a/crates/clarion-cli/src/secret_scan/files.rs +++ b/crates/clarion-cli/src/secret_scan/files.rs @@ -78,7 +78,7 @@ fn collect_secret_scan_sidecars(root: &Path) -> Vec { out } -fn is_secret_scan_sidecar(path: &Path) -> bool { +pub(super) fn is_secret_scan_sidecar(path: &Path) -> bool { let file_name = path.file_name().and_then(|name| name.to_str()); file_name.is_some_and(|name| name == ".env" || name.starts_with(".env.")) || path diff --git a/crates/clarion-cli/tests/secret_scan.rs b/crates/clarion-cli/tests/secret_scan.rs index 78b6151b..9c1bf57e 100644 --- a/crates/clarion-cli/tests/secret_scan.rs +++ b/crates/clarion-cli/tests/secret_scan.rs @@ -802,3 +802,114 @@ fn only_secret_bearing_file_is_blocked_in_multi_file_project() { .unwrap(); assert_eq!(blocked_count, 1); } + +#[test] +fn cleared_sidecar_clears_stale_briefing_block() { + // Regression for clarion-2e34dbbdec: a sidecar-only path (.env) whose + // secret is cleaned must have its anchor's briefing_blocked dropped and + // content_hash refreshed on the next run, even though no finding is + // emitted for it in run N+1. + let project = tempfile::tempdir().unwrap(); + let plugin = tempfile::tempdir().unwrap(); + write_secret_fixture_plugin(plugin.path()); + install_project(project.path()); + std::fs::write(project.path().join("clean.sec"), b"nothing to see\n").unwrap(); + let dotenv = project.path().join(".env"); + std::fs::write(&dotenv, b"aws_access_key_id = 'AKIAIOSFODNN7EXAMPLE'\n").unwrap(); + + clarion_bin() + .arg("analyze") + .arg(project.path()) + .env("PATH", plugin_path(plugin.path())) + .assert() + .success(); + + let db = conn(project.path()); + let (blocked, hash_before): (String, String) = db + .query_row( + "SELECT json_extract(properties, '$.briefing_blocked'), content_hash \ + FROM entities \ + WHERE plugin_id = 'core' AND kind = 'file' \ + AND source_file_path LIKE '%.env'", + [], + |row| Ok((row.get(0)?, row.get(1)?)), + ) + .unwrap(); + assert_eq!(blocked, "secret_present"); + drop(db); + + std::fs::write(&dotenv, b"NO_SECRET_HERE=ordinary_value\n").unwrap(); + + clarion_bin() + .arg("analyze") + .arg(project.path()) + .env("PATH", plugin_path(plugin.path())) + .assert() + .success(); + + let db = conn(project.path()); + let (blocked_after, hash_after): (Option, String) = db + .query_row( + "SELECT json_extract(properties, '$.briefing_blocked'), content_hash \ + FROM entities \ + WHERE plugin_id = 'core' AND kind = 'file' \ + AND source_file_path LIKE '%.env'", + [], + |row| Ok((row.get(0)?, row.get(1)?)), + ) + .unwrap(); + assert_eq!( + blocked_after, None, + "briefing_blocked must be cleared after the secret is removed" + ); + assert_ne!( + hash_before, hash_after, + "content_hash must reflect the cleaned file, not the secret-bearing run" + ); +} + +#[test] +fn clean_sidecar_creates_anchor_without_block() { + // Standalone regression: an always-clean sidecar (no secret ever) still + // gets a core:file anchor on first scan, with no briefing_blocked. This + // exercises the cleared-anchor path independent of the transition test + // above, so future breakage is localised even when the two-run + // choreography is not exercised. + let project = tempfile::tempdir().unwrap(); + let plugin = tempfile::tempdir().unwrap(); + write_secret_fixture_plugin(plugin.path()); + install_project(project.path()); + std::fs::write(project.path().join("clean.sec"), b"nothing to see\n").unwrap(); + std::fs::write( + project.path().join(".env"), + b"NO_SECRET_HERE=ordinary_value\n", + ) + .unwrap(); + + clarion_bin() + .arg("analyze") + .arg(project.path()) + .env("PATH", plugin_path(plugin.path())) + .assert() + .success(); + + let db = conn(project.path()); + let (anchor_count, blocked): (i64, Option) = db + .query_row( + "SELECT COUNT(*), MAX(json_extract(properties, '$.briefing_blocked')) \ + FROM entities \ + WHERE plugin_id = 'core' AND kind = 'file' \ + AND source_file_path LIKE '%.env'", + [], + |row| Ok((row.get(0)?, row.get(1)?)), + ) + .unwrap(); + assert_eq!( + anchor_count, 1, + "clean sidecar must have a core:file anchor" + ); + assert_eq!( + blocked, None, + "anchor on an always-clean sidecar must have no briefing_blocked" + ); +} From f4f27295348f005f25cc802ebae88cff3df55be2 Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Wed, 20 May 2026 00:10:56 +1000 Subject: [PATCH 042/187] fix(secret_scan): anchor ID must match ADR-003 entity-id grammar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The secret-scan finding anchor was producing core:file: entity IDs, but ADR-003 §Decision is explicit that file-kind IDs use core:file:{canonical project-relative path} and that "content-hash-plus-path forms ... are non-conforming because they embed path and drift state into the primary ID instead of storing those values as entity properties". The drift was downstream-visible: the rest of the codebase (federation fixtures, contracts.md, ADR-014, entity_id assembler tests, http_read fixtures) all use path-based IDs. Only secret_finding_anchor_id was digest-based - and those rows were the only production producer of kind: "file" entities. Anything that resolved a secret-scan anchor via the /api/v1/files registry path would have received a hash-shaped ID that no Filigree-side join could match against because no other emitter produces hash-shaped IDs. Replace the digest with display_relative. blake3 stays in scope for the content_hash on the same struct (which IS a hash by design — it's a property, not an identity). Surfaced by PR #12 review. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/clarion-cli/src/secret_scan/anchors.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/clarion-cli/src/secret_scan/anchors.rs b/crates/clarion-cli/src/secret_scan/anchors.rs index e2865896..5cb9a69a 100644 --- a/crates/clarion-cli/src/secret_scan/anchors.rs +++ b/crates/clarion-cli/src/secret_scan/anchors.rs @@ -139,9 +139,13 @@ async fn upsert_finding_anchor( } fn secret_finding_anchor_id(project_root: &Path, file: &Path) -> String { + // ADR-003 grammar: file-kind entity IDs are `core:file:{qualified_name}` + // where the qualified name is the canonical project-relative path. Earlier + // revisions hashed the path with blake3, which embedded drift state into + // the primary ID and was explicitly called out as non-conforming in + // ADR-003. The path itself is the canonical identity. let relative = display_relative(project_root, file); - let digest = blake3::hash(relative.as_bytes()).to_hex().to_string(); - format!("core:file:{digest}") + format!("core:file:{relative}") } fn file_content_hash(path: &Path) -> Option { From f5695316432ca2ded63c34c1bbae99e051673530 Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Wed, 20 May 2026 00:11:12 +1000 Subject: [PATCH 043/187] chore(pr-review): doc drift + pin Python 3.11 in ci.yml rust job Cosmetic batch from PR #12 inline review: - README.md: corrected MCP-tool count from 7 to 8 in both the overview paragraph and the project-layout legend. subsystem_members was listed in a follow-on sentence rather than the table; moved into the table alongside the other seven so the count and the visible enumeration agree. - docs/README.md, docs/clarion/README.md: bullets still pointed at clarion/v0.1/ paths that don't exist on this branch (the design ladder is at clarion/1.0/). Updated link targets and the surrounding prose so "current versioned spec set" matches what's actually shipped. - docs/operator/README.md, openrouter.md, coding-agent-llm-providers.md: three operator docs still labelled the configuration as "for v0.1" while the rest of the operator surface is v1.0. Replaced "v0.1" with the version-agnostic phrasing or "v1.0" where the version literal carried real meaning (e.g. "no tier-name resolver in v1.0"). - docs/implementation/sprint-1/README.md, signoffs.md: link display text still showed the old ../../clarion/v0.1/plans/ path even though the link target was already updated to ../v0.1-scope-plans/. Brought the displayed text into sync with the target so readers don't think there's a dead link. CI hardening from the same review: - .github/workflows/ci.yml: the rust job runs Python scripts including the new check-workspace-version-lockstep.py guard, which needs the stdlib tomllib module (Python 3.11+). The job previously relied on whatever Python ubuntu-latest happened to preinstall. Add an explicit actions/setup-python@v5 with python-version: "3.11" before the guard step so the CI contract is pinned rather than image-drift- dependent. Surfaced by PR #12 review. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/ci.yml | 8 ++++++++ README.md | 8 +++----- docs/README.md | 4 ++-- docs/clarion/README.md | 6 +++--- docs/implementation/sprint-1/README.md | 2 +- docs/implementation/sprint-1/signoffs.md | 2 +- docs/operator/README.md | 2 +- docs/operator/coding-agent-llm-providers.md | 2 +- docs/operator/openrouter.md | 6 +++--- 9 files changed, 23 insertions(+), 17 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eb140763..61906f59 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,6 +26,14 @@ jobs: - uses: Swatinem/rust-cache@v2 + # Python is used by the migration-retirement guard and the + # workspace-version-lockstep guard below. Pin >= 3.11 explicitly so the + # stdlib `tomllib` is available regardless of which Python the runner + # image happens to preinstall. + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + - name: fmt run: cargo fmt --all -- --check diff --git a/README.md b/README.md index b6fc5923..6521ee91 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ and trust-topology tools. ## What it does today -`clarion serve` exposes seven MCP tools that a consult-mode agent calls instead +`clarion serve` exposes eight MCP tools that a consult-mode agent calls instead of grep-and-read: | Tool | What it answers | @@ -48,9 +48,7 @@ of grep-and-read: | `summary(id)` | "Give me a one-paragraph summary of this entity." (lazy LLM dispatch + cached) | | `issues_for(id)` | "What Filigree issues are attached to this entity?" | | `neighborhood(id)` | "Show callers, callees, container, contained entities, and references in one hop." | - -`subsystem_members(id)` is also exposed for listing members of a subsystem -entity (clustering output). +| `subsystem_members(id)` | "Which entities belong to this subsystem?" (clustering output) | ## Quick start @@ -86,7 +84,7 @@ crates/ Rust workspace ├── clarion-storage/ Writer-actor + reader-pool over SQLite (ADR-011) ├── clarion-scanner/ Pre-ingest secret scanner (ADR-013, WP5) ├── clarion-cli/ The `clarion` binary (install, analyze, serve) -└── clarion-mcp/ MCP server exposing the seven consult tools +└── clarion-mcp/ MCP server exposing the eight consult tools plugins/python/ Python language plugin (pyright-backed) docs/clarion/1.0/ Design ladder — requirements → system-design → detailed-design docs/clarion/adr/ Authored architecture decision records diff --git a/docs/README.md b/docs/README.md index df3744a4..d9c50eec 100644 --- a/docs/README.md +++ b/docs/README.md @@ -10,12 +10,12 @@ This `docs/` tree is organized by reader intent: - New to the suite: [suite/briefing.md](./suite/briefing.md) - Evaluating the Loom doctrine: [suite/loom.md](./suite/loom.md) -- Starting Clarion v0.1: [clarion/v0.1/README.md](./clarion/v0.1/README.md) +- Starting Clarion v1.0: [clarion/1.0/README.md](./clarion/1.0/README.md) - Configuring OpenRouter: [operator/openrouter.md](./operator/openrouter.md) ## Canonical vs supporting docs - Canonical suite docs live in [suite/](./suite/README.md). -- Canonical Clarion v0.1 design docs live in [clarion/v0.1/](./clarion/v0.1/README.md). +- Canonical Clarion v0.1 design docs live in [clarion/v0.1/](./clarion/1.0/README.md). - Architecture decisions live in [clarion/adr/](./clarion/adr/README.md). - Supporting reviews, scope memos, sprint plans, and agent handoffs are archived under [implementation/](./implementation/README.md) — non-normative. diff --git a/docs/clarion/README.md b/docs/clarion/README.md index 32f0352a..7f283476 100644 --- a/docs/clarion/README.md +++ b/docs/clarion/README.md @@ -4,15 +4,15 @@ This folder holds Clarion product documentation. ## Read paths -- Starting Clarion v0.1: [v0.1/README.md](./v0.1/README.md) +- Starting Clarion v1.0: [1.0/README.md](./1.0/README.md) - Architecture decisions: [adr/README.md](./adr/README.md) ## Structure -- [v0.1/](./v0.1/README.md) — current versioned spec set. +- [1.0/](./1.0/README.md) — current versioned spec set. - [adr/](./adr/README.md) — authored architecture decision records and the remaining backlog. ## Canonical vs supporting -- Canonical design docs for the current product version live under `v0.1/`. +- Canonical design docs for the current product version live under `1.0/`. - Reviews and planning history are archived under [../implementation/](../implementation/) — non-normative supporting context. diff --git a/docs/implementation/sprint-1/README.md b/docs/implementation/sprint-1/README.md index ce6bd87e..3a0c2c9d 100644 --- a/docs/implementation/sprint-1/README.md +++ b/docs/implementation/sprint-1/README.md @@ -4,7 +4,7 @@ **Sprint goal**: [Tier A — walking skeleton](./signoffs.md#tier-a--sprint-1-close-walking-skeleton) **Scope**: WP1 (scaffold + storage) → WP2 (plugin protocol + hybrid authority) → WP3 (Python plugin v0.1 baseline) **Predecessor plan**: [`../v0.1-plan.md`](../v0.1-plan.md) -**Scope memo**: [`../../clarion/v0.1/plans/v0.1-scope-commitments.md`](../v0.1-scope-plans/v0.1-scope-commitments.md) +**Scope memo**: [`../v0.1-scope-plans/v0.1-scope-commitments.md`](../v0.1-scope-plans/v0.1-scope-commitments.md) **Gate to close**: every checkbox ticked in [`signoffs.md`](./signoffs.md) Tier A. --- diff --git a/docs/implementation/sprint-1/signoffs.md b/docs/implementation/sprint-1/signoffs.md index ff500d99..ef26a51f 100644 --- a/docs/implementation/sprint-1/signoffs.md +++ b/docs/implementation/sprint-1/signoffs.md @@ -134,4 +134,4 @@ ladder completeness; not required to close Sprint 1 or Sprint 2. - [`README.md`](./README.md) — sprint orientation and demo script. - [`wp1-scaffold.md`](./wp1-scaffold.md), [`wp2-plugin-host.md`](./wp2-plugin-host.md), [`wp3-python-plugin.md`](./wp3-python-plugin.md) — per-WP plans. - [`../v0.1-plan.md`](../v0.1-plan.md) — high-level plan for all 11 WPs. -- [`../../clarion/v0.1/plans/v0.1-scope-commitments.md`](../v0.1-scope-plans/v0.1-scope-commitments.md) — scope memo. +- [`../v0.1-scope-plans/v0.1-scope-commitments.md`](../v0.1-scope-plans/v0.1-scope-commitments.md) — scope memo. diff --git a/docs/operator/README.md b/docs/operator/README.md index e5e2aa58..0a47dacf 100644 --- a/docs/operator/README.md +++ b/docs/operator/README.md @@ -6,7 +6,7 @@ Practical notes for configuring and running Clarion. analyse a small repo, connect an MCP client, ask three questions, verify the secret-block. Target ≤15 minutes end-to-end. - [OpenRouter LLM provider](./openrouter.md) — API key, model ID, attribution - headers, and token-ceiling configuration for v0.1. + headers, and token-ceiling configuration. - [Coding-agent LLM providers](./coding-agent-llm-providers.md) — Codex CLI and Claude CLI as local-login alternatives to API-key provider wiring. - [Runtime topology](./runtime-topology.md) — supported `clarion serve` and diff --git a/docs/operator/coding-agent-llm-providers.md b/docs/operator/coding-agent-llm-providers.md index 3d231b3d..7d1536aa 100644 --- a/docs/operator/coding-agent-llm-providers.md +++ b/docs/operator/coding-agent-llm-providers.md @@ -5,7 +5,7 @@ agent CLIs instead of an HTTP API key. These routes still send prompt content to the agent vendor, so they require the same explicit live-provider opt-in as OpenRouter. -Supported v0.1 provider values: +Supported provider values: - `codex_cli` uses `codex exec` with local Codex authentication. - `claude_cli` uses `claude -p` with local Claude Code authentication. diff --git a/docs/operator/openrouter.md b/docs/operator/openrouter.md index b8b5a440..38d443dc 100644 --- a/docs/operator/openrouter.md +++ b/docs/operator/openrouter.md @@ -1,6 +1,6 @@ # OpenRouter LLM Provider -Clarion v0.1 uses OpenRouter as its canonical HTTP LLM provider. The provider +Clarion uses OpenRouter as its canonical HTTP LLM provider. The provider speaks OpenAI-compatible Chat Completions HTTP through the existing `LlmProvider` trait, so tests can continue to use `RecordingProvider` and coding-agent CLI providers can be added without changing MCP tool call sites. @@ -55,7 +55,7 @@ OpenRouter model IDs are concrete strings such as: - `meta-llama/llama-3.1-8b-instruct` Clarion stores this exact string in the summary cache key's `model_tier` -component. There is no tier-name resolver in v0.1; operators choose the concrete +component. There is no tier-name resolver in v1.0; operators choose the concrete model in `clarion.yaml`. Refresh the chosen ID against OpenRouter's Models API before release or production use. @@ -78,7 +78,7 @@ tokens. To clear a blocked LLM budget, stop and restart `clarion serve`. To change the future ceiling, edit `llm_policy.session_token_ceiling` in `clarion.yaml` before -restarting. Clarion v0.1 intentionally has no MCP tool that resets the in-memory +restarting. Clarion v1.0 intentionally has no MCP tool that resets the in-memory budget ledger. Dollar budgeting remains an operator concern in OpenRouter billing controls. From 2faa7504307331a3230dc5e31bb21ef377e9ee83 Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Wed, 20 May 2026 14:54:22 +1000 Subject: [PATCH 044/187] Persist manifest language for HTTP file reads --- crates/clarion-cli/src/analyze.rs | 139 +++++++++++++++++++++++- crates/clarion-cli/src/secret_scan.rs | 13 ++- crates/clarion-cli/tests/analyze.rs | 41 +++++++ crates/clarion-cli/tests/secret_scan.rs | 6 +- crates/clarion-cli/tests/serve.rs | 46 ++++++++ crates/clarion-cli/tests/wp2_e2e.rs | 58 +++++++--- crates/clarion-core/src/plugin/host.rs | 17 +-- crates/clarion-storage/src/query.rs | 27 ++++- 8 files changed, 308 insertions(+), 39 deletions(-) diff --git a/crates/clarion-cli/src/analyze.rs b/crates/clarion-cli/src/analyze.rs index f54ca343..1a48939b 100644 --- a/crates/clarion-cli/src/analyze.rs +++ b/crates/clarion-cli/src/analyze.rs @@ -12,6 +12,7 @@ use std::collections::{BTreeMap, BTreeSet}; use std::fs; use std::path::{Path, PathBuf}; +use std::sync::Arc; use anyhow::{Context, Result, bail}; use ignore::{DirEntry, WalkBuilder}; @@ -245,6 +246,8 @@ pub(crate) async fn run_with_options(project_path: PathBuf, options: AnalyzeOpti let mut run_outcome: RunOutcome = RunOutcome::Completed; let mut breaker = CrashLoopBreaker::default(); let mut crash_reasons: Vec = Vec::new(); + let briefing_blocks = Arc::clone(&secret_scan_outcome.briefing_blocks); + let scanned_files = secret_scan_outcome.scanned_files_shared(); 'plugins: for plugin in plugins { let plugin_id = plugin.manifest.plugin.plugin_id.clone(); let plugin_extensions: BTreeSet = plugin @@ -284,8 +287,8 @@ pub(crate) async fn run_with_options(project_path: PathBuf, options: AnalyzeOpti let pid_clone = plugin_id.clone(); let exec_clone = plugin.executable.clone(); let files_clone = plugin_files.clone(); - let briefing_blocks_clone = secret_scan_outcome.briefing_blocks.clone(); - let scanned_files_clone = secret_scan_outcome.scanned_files().clone(); + let briefing_blocks_clone = Arc::clone(&briefing_blocks); + let scanned_files_clone = Arc::clone(&scanned_files); // A JoinError here means the blocking task panicked (OOM, stack // overflow, internal unwrap, abort — anything that unwinds past the @@ -1184,17 +1187,19 @@ type Collected = ( /// via `child.kill()` + `child.wait()`. `std::process::Child::Drop` does NOT /// kill or reap on Unix, so discarding `child` without `wait()` would leak a /// zombie into the kernel process table per spawn. +#[allow(clippy::too_many_lines)] fn run_plugin_blocking( manifest: clarion_core::Manifest, project_root: &Path, plugin_id: &str, executable: &Path, files: &[PathBuf], - briefing_blocks: &BTreeMap, - scanned_source_files: &BTreeSet, + briefing_blocks: &Arc>, + scanned_source_files: &Arc>, ) -> Result { use clarion_core::PluginHost; + let manifest_language = manifest.plugin.language.clone(); let (mut host, mut child) = PluginHost::spawn(manifest, project_root, executable).map_err(|e| match e { HostError::Spawn(msg) => { @@ -1207,8 +1212,8 @@ fn run_plugin_blocking( PluginRunError::new(format!("plugin {plugin_id} spawn/handshake error: {other}")) } })?; - host.set_briefing_blocks(briefing_blocks.clone()); - host.set_scanned_source_files(scanned_source_files.clone()); + host.set_briefing_blocks(Arc::clone(briefing_blocks)); + host.set_scanned_source_files(Arc::clone(scanned_source_files)); let work_result: Result = (|| { let mut collected_entities: Vec<(String, EntityRecord)> = Vec::new(); @@ -1247,6 +1252,16 @@ fn run_plugin_blocking( .find(|entity| entity.kind == "module") .map(|entity| entity.id.to_string()); let mut file_entities: Vec<(String, EntityRecord)> = Vec::new(); + let (file_entity_id, file_record) = core_file_entity_record( + project_root, + file, + &manifest_language, + briefing_blocks, + scanned_source_files, + ) + .map_err(|e| format!("core file entity for {}: {e:#}", file.display()))?; + file_entities.push((file_entity_id.clone(), file_record.clone())); + collected_entities.push((file_entity_id, file_record)); for entity in &entities { let id_str = entity.id.to_string(); let record = map_entity_to_record(entity, plugin_id, source_file_id.clone()); @@ -1520,6 +1535,118 @@ fn absolute_from_import_submodule_target( .then_some(candidate) } +fn core_file_entity_record( + project_root: &Path, + file: &Path, + manifest_language: &str, + briefing_blocks: &BTreeMap, + scanned_source_files: &BTreeSet, +) -> Result<(String, EntityRecord)> { + let canonical_root = project_root + .canonicalize() + .with_context(|| format!("canonicalize project root {}", project_root.display()))?; + let canonical_file = file + .canonicalize() + .with_context(|| format!("canonicalize source file {}", file.display()))?; + let relative = canonical_file + .strip_prefix(&canonical_root) + .with_context(|| { + format!( + "source file {} is outside project root {}", + canonical_file.display(), + canonical_root.display() + ) + })?; + let qualified_name = project_relative_posix(relative)?; + let id = clarion_core::entity_id::entity_id("core", "file", &qualified_name)?.to_string(); + let briefing_blocked = briefing_blocks.get(&canonical_file).copied().or_else(|| { + (!scanned_source_files.contains(&canonical_file)) + .then_some(clarion_core::BriefingBlockReason::UnscannedSource) + }); + let source_file_path = canonical_file + .into_os_string() + .into_string() + .map_err(|path| { + anyhow::anyhow!("source file path is not valid UTF-8: {}", path.display()) + })?; + let short_name = Path::new(&source_file_path) + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or(&qualified_name) + .to_owned(); + let content_hash = fs::read(&source_file_path) + .with_context(|| format!("read source file {source_file_path}")) + .map(|bytes| blake3::hash(&bytes).to_hex().to_string())?; + let mut properties = serde_json::Map::new(); + properties.insert( + "language".to_owned(), + serde_json::Value::String(manifest_language.to_owned()), + ); + if let Some(reason) = briefing_blocked { + properties.insert( + "briefing_blocked".to_owned(), + serde_json::Value::String(reason.as_str().to_owned()), + ); + } + let properties_json = serde_json::Value::Object(properties).to_string(); + let now = iso8601_now(); + + Ok(( + id.clone(), + EntityRecord { + id, + plugin_id: "core".to_owned(), + kind: "file".to_owned(), + name: qualified_name, + short_name, + parent_id: None, + source_file_id: None, + source_file_path: Some(source_file_path), + source_byte_start: None, + source_byte_end: None, + source_line_start: None, + source_line_end: None, + properties_json, + content_hash: Some(content_hash), + summary_json: None, + wardline_json: None, + first_seen_commit: None, + last_seen_commit: None, + created_at: now.clone(), + updated_at: now, + }, + )) +} + +fn project_relative_posix(path: &Path) -> Result { + let mut parts = Vec::new(); + for component in path.components() { + match component { + std::path::Component::Normal(part) => { + let part = part.to_str().ok_or_else(|| { + anyhow::anyhow!( + "source file path component is not valid UTF-8: {}", + part.display() + ) + })?; + parts.push(part); + } + std::path::Component::CurDir => {} + _ => { + bail!( + "source file path is not project-relative: {}", + path.display() + ); + } + } + } + let relative = parts.join("/"); + if relative.is_empty() { + bail!("source file path must not resolve to the project root"); + } + Ok(relative) +} + /// Map an `AcceptedEntity` to an `EntityRecord` for the writer-actor. fn map_entity_to_record( entity: &AcceptedEntity, diff --git a/crates/clarion-cli/src/secret_scan.rs b/crates/clarion-cli/src/secret_scan.rs index f73dd853..fc0bb2d8 100644 --- a/crates/clarion-cli/src/secret_scan.rs +++ b/crates/clarion-cli/src/secret_scan.rs @@ -11,6 +11,7 @@ use std::{ fs, io::{self, IsTerminal, Write}, path::{Path, PathBuf}, + sync::Arc, }; mod anchors; @@ -81,11 +82,11 @@ impl std::fmt::Display for OverrideConfirmationError { #[derive(Debug, Clone, Default)] pub(crate) struct SecretScanOutcome { - pub(crate) briefing_blocks: BTreeMap, + pub(crate) briefing_blocks: Arc>, findings: Vec, finding_anchors: BTreeMap, override_files: Vec, - scanned_files: BTreeSet, + scanned_files: Arc>, // Subset of `scanned_files` restricted to paths matched by // `files::is_secret_scan_sidecar` (e.g. `.env`, `.env.*`, `*.env`). // These paths have no plugin coverage by design, so their `core:file` @@ -96,8 +97,8 @@ pub(crate) struct SecretScanOutcome { } impl SecretScanOutcome { - pub(crate) fn scanned_files(&self) -> &BTreeSet { - &self.scanned_files + pub(crate) fn scanned_files_shared(&self) -> Arc> { + Arc::clone(&self.scanned_files) } pub(crate) fn scanned_sidecars(&self) -> &BTreeSet { @@ -252,11 +253,11 @@ pub(crate) fn pre_ingest( } Ok(SecretScanOutcome { - briefing_blocks, + briefing_blocks: Arc::new(briefing_blocks), findings, finding_anchors: BTreeMap::new(), override_files: override_files.into_iter().collect(), - scanned_files, + scanned_files: Arc::new(scanned_files), scanned_sidecars, }) } diff --git a/crates/clarion-cli/tests/analyze.rs b/crates/clarion-cli/tests/analyze.rs index f72b910e..7ad9e4c7 100644 --- a/crates/clarion-cli/tests/analyze.rs +++ b/crates/clarion-cli/tests/analyze.rs @@ -1054,6 +1054,47 @@ fn analyze_stats_reports_ambiguous_edges_total() { ); } +#[cfg(unix)] +#[test] +fn analyze_mints_core_file_entity_for_registry_resolution() { + let project_dir = tempfile::tempdir().unwrap(); + let plugin_dir = tempfile::tempdir().unwrap(); + write_ambiguous_calls_plugin(plugin_dir.path()); + + clarion_bin() + .args(["install", "--path"]) + .arg(project_dir.path()) + .assert() + .success(); + std::fs::write(project_dir.path().join("demo.call"), b"caller callee\n") + .expect("write demo.call"); + + let plugin_path = + std::env::join_paths(std::iter::once(plugin_dir.path().to_path_buf())).unwrap(); + clarion_bin() + .args(["analyze"]) + .arg(project_dir.path()) + .env("PATH", &plugin_path) + .assert() + .success(); + + let conn = Connection::open(project_dir.path().join(".clarion/clarion.db")).unwrap(); + let resolved = clarion_storage::resolve_file(&conn, project_dir.path(), "demo.call", "") + .expect("resolve_file should not error") + .expect("analyzed ordinary source file should resolve as a core file entity"); + + assert_eq!(resolved.entity_id, "core:file:demo.call"); + assert_eq!(resolved.canonical_path.as_str(), "demo.call"); + assert_eq!( + resolved.language, "callsfixture", + "HTTP file resolution must use the plugin manifest language, not a hardcoded extension fallback" + ); + assert_eq!( + resolved.content_hash, + blake3::hash(b"caller callee\n").to_hex().to_string() + ); +} + #[cfg(unix)] #[test] fn analyze_filters_external_import_edges_before_writer_insert() { diff --git a/crates/clarion-cli/tests/secret_scan.rs b/crates/clarion-cli/tests/secret_scan.rs index 9c1bf57e..a573e6af 100644 --- a/crates/clarion-cli/tests/secret_scan.rs +++ b/crates/clarion-cli/tests/secret_scan.rs @@ -299,7 +299,8 @@ fn plugin_entity_for_unscanned_source_is_briefing_blocked() { let blocked: String = conn(project.path()) .query_row( - "SELECT json_extract(properties, '$.briefing_blocked') FROM entities", + "SELECT json_extract(properties, '$.briefing_blocked') FROM entities \ + WHERE plugin_id = 'secretfixture'", [], |row| row.get(0), ) @@ -795,7 +796,8 @@ fn only_secret_bearing_file_is_blocked_in_multi_file_project() { let blocked_count: i64 = conn(project.path()) .query_row( - "SELECT COUNT(*) FROM entities WHERE json_extract(properties, '$.briefing_blocked') = 'secret_present'", + "SELECT COUNT(DISTINCT source_file_path) FROM entities \ + WHERE json_extract(properties, '$.briefing_blocked') = 'secret_present'", [], |row| row.get(0), ) diff --git a/crates/clarion-cli/tests/serve.rs b/crates/clarion-cli/tests/serve.rs index adeda0aa..54c7c58f 100644 --- a/crates/clarion-cli/tests/serve.rs +++ b/crates/clarion-cli/tests/serve.rs @@ -211,6 +211,28 @@ fn serve_http_files_endpoint_resolves_known_file_on_configured_port() { assert_eq!(&response, fixture_example_body(&fixture, "happy_path_200")); } +#[test] +fn serve_http_files_endpoint_prefers_stored_manifest_language() { + let dir = tempfile::tempdir().expect("temp project"); + clarion_bin() + .args(["install", "--path"]) + .arg(dir.path()) + .env("PATH", "") + .assert() + .success(); + seed_custom_language_file_entity(dir.path()); + let bind = free_loopback_bind(); + write_http_config(dir.path(), &bind); + + let mut child = spawn_serve(dir.path()); + let response = wait_for_http_json(&bind, "/api/v1/files?path=demo.custom&language=requested"); + stop_serve(&mut child); + let response = response.expect("HTTP /api/v1/files response"); + + assert_eq!(response["entity_id"], "core:file:demo.custom"); + assert_eq!(response["language"], "manifestlang"); +} + #[test] fn serve_http_files_etag_round_trip_and_if_none_match_returns_304() { let dir = tempfile::tempdir().expect("temp project"); @@ -1779,6 +1801,30 @@ fn seed_file_entity(project_root: &Path) -> (String, String, String) { (file_id, content_hash, "demo.py".to_owned()) } +fn seed_custom_language_file_entity(project_root: &Path) { + let source_path = project_root.join("demo.custom"); + fs::write(&source_path, "custom source\n").expect("write custom source"); + let canonical_path = source_path + .canonicalize() + .expect("canonical source path") + .display() + .to_string(); + let db_path = project_root.join(".clarion/clarion.db"); + let conn = Connection::open(&db_path).expect("open sqlite"); + conn.execute( + "INSERT INTO entities ( + id, plugin_id, kind, name, short_name, source_file_path, + source_line_start, source_line_end, properties, content_hash, created_at, updated_at + ) VALUES ( + 'core:file:demo.custom', 'core', 'file', 'demo.custom', 'demo.custom', ?1, + 1, 1, '{\"language\":\"manifestlang\"}', 'hash-demo-custom', + '2026-05-19T00:00:00.000Z', '2026-05-19T00:00:00.000Z' + )", + params![canonical_path], + ) + .expect("insert custom language file entity"); +} + fn seed_briefing_blocked_file_entity(project_root: &Path) { let source_path = project_root.join("blocked.py"); fs::write(&source_path, "secret = \"redacted\"\n").expect("write blocked source"); diff --git a/crates/clarion-cli/tests/wp2_e2e.rs b/crates/clarion-cli/tests/wp2_e2e.rs index 1b3fa1ad..de048c25 100644 --- a/crates/clarion-cli/tests/wp2_e2e.rs +++ b/crates/clarion-cli/tests/wp2_e2e.rs @@ -194,7 +194,7 @@ fn wp2_e2e_smoke_fixture_plugin_round_trip() { "run status must be 'completed'; got {run_status:?}" ); - // Assert 3: stats JSON reports entities_inserted = 1. + // Assert 3: stats JSON reports the fixture entity plus the core file identity. let stats_raw: String = conn .query_row("SELECT stats FROM runs LIMIT 1", [], |row| row.get(0)) .expect("query runs.stats"); @@ -202,24 +202,35 @@ fn wp2_e2e_smoke_fixture_plugin_round_trip() { serde_json::from_str(&stats_raw).expect("stats column must be valid JSON"); assert_eq!( stats["entities_inserted"], - serde_json::Value::Number(1.into()), - "stats must report entities_inserted = 1; got {stats_raw:?}" + serde_json::Value::Number(2.into()), + "stats must report entities_inserted = 2; got {stats_raw:?}" ); - // Assert 4: exactly one entity row. + // Assert 4: exactly one fixture entity and one core file identity row. let entity_count: i64 = conn .query_row("SELECT COUNT(*) FROM entities", [], |row| row.get(0)) .expect("query entities count"); assert_eq!( - entity_count, 1, - "expected exactly one entity row; got {entity_count}" + entity_count, 2, + "expected exactly two entity rows; got {entity_count}" + ); + let file_entity_count: i64 = conn + .query_row( + "SELECT COUNT(*) FROM entities WHERE plugin_id = 'core' AND kind = 'file'", + [], + |row| row.get(0), + ) + .expect("query core file entity count"); + assert_eq!( + file_entity_count, 1, + "expected exactly one core file identity row; got {file_entity_count}" ); // Asserts 5–8: the persisted row matches the fixture's declared emission. let (entity_id, entity_kind, entity_plugin_id, entity_name): (String, String, String, String) = conn.query_row( - "SELECT id, kind, plugin_id, name FROM entities LIMIT 1", + "SELECT id, kind, plugin_id, name FROM entities WHERE plugin_id = 'fixture'", [], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)), ) @@ -409,17 +420,34 @@ ontology_version = "0.1.0" // This is the behavioural assertion that matters: the fixture plugin's // entity is persisted even though `broken` crashed earlier in the run. - let entity_count: i64 = conn - .query_row("SELECT COUNT(*) FROM entities", [], |row| row.get(0)) - .expect("query entities count"); + let fixture_entity_count: i64 = conn + .query_row( + "SELECT COUNT(*) FROM entities WHERE plugin_id = 'fixture'", + [], + |row| row.get(0), + ) + .expect("query fixture entities count"); + assert_eq!( + fixture_entity_count, 1, + "fixture plugin's entity must still be persisted despite broken plugin's crash; got {fixture_entity_count}", + ); + let file_entity_count: i64 = conn + .query_row( + "SELECT COUNT(*) FROM entities WHERE plugin_id = 'core' AND kind = 'file'", + [], + |row| row.get(0), + ) + .expect("query core file entity count"); assert_eq!( - entity_count, 1, - "fixture plugin's entity must still be persisted despite broken plugin's crash; got {entity_count}", + file_entity_count, 1, + "core file identity must be persisted for the surviving source file; got {file_entity_count}", ); let entity_plugin_id: String = conn - .query_row("SELECT plugin_id FROM entities LIMIT 1", [], |row| { - row.get(0) - }) + .query_row( + "SELECT plugin_id FROM entities WHERE plugin_id = 'fixture'", + [], + |row| row.get(0), + ) .expect("query entity plugin_id"); assert_eq!( entity_plugin_id, "fixture", diff --git a/crates/clarion-core/src/plugin/host.rs b/crates/clarion-core/src/plugin/host.rs index a8392c73..d495bced 100644 --- a/crates/clarion-core/src/plugin/host.rs +++ b/crates/clarion-core/src/plugin/host.rs @@ -30,6 +30,7 @@ use std::collections::{BTreeMap, BTreeSet}; use std::io::{BufRead, Write}; use std::path::{Path, PathBuf}; +use std::sync::Arc; use thiserror::Error; @@ -413,11 +414,11 @@ where /// `None`. Capacity is [`STDERR_TAIL_BYTES`]; oldest bytes are /// discarded on overflow so the plugin cannot back-pressure the host /// via stderr writes. - stderr_tail: Option>>>, + stderr_tail: Option>>>, /// Canonical source paths whose entities must not be sent for LLM briefing. - briefing_blocks: BTreeMap, + briefing_blocks: Arc>, /// Canonical source paths that were covered by the core pre-ingest scanner. - scanned_source_files: BTreeSet, + scanned_source_files: Arc>, } /// File-level reason an entity must not be dispatched for LLM briefing. @@ -449,7 +450,7 @@ pub const STDERR_TAIL_BYTES: usize = 64 * 1024; #[cfg(unix)] fn drain_stderr_into_ring( mut stderr: std::process::ChildStderr, - ring: &std::sync::Arc>>, + ring: &Arc>>, ) { use std::io::Read; let mut buf = [0u8; 4096]; @@ -682,8 +683,8 @@ impl PluginHost { terminated: false, ontology_version: None, stderr_tail: None, - briefing_blocks: BTreeMap::new(), - scanned_source_files: BTreeSet::new(), + briefing_blocks: Arc::new(BTreeMap::new()), + scanned_source_files: Arc::new(BTreeSet::new()), } } @@ -714,7 +715,7 @@ impl PluginHost { /// Install file-level briefing blocks produced by the core pre-ingest /// scanner. Keys are canonical source paths and values are typed policy /// reasons rendered into `properties_json` by [`BriefingBlockReason::as_str`]. - pub fn set_briefing_blocks(&mut self, blocks: BTreeMap) { + pub fn set_briefing_blocks(&mut self, blocks: Arc>) { self.briefing_blocks = blocks; } @@ -723,7 +724,7 @@ impl PluginHost { /// If a plugin returns an entity for a jailed in-project path that is not in /// this set, the entity is policy-blocked from LLM briefing because its file /// bytes have not passed the scanner. - pub fn set_scanned_source_files(&mut self, files: BTreeSet) { + pub fn set_scanned_source_files(&mut self, files: Arc>) { self.scanned_source_files = files; } diff --git a/crates/clarion-storage/src/query.rs b/crates/clarion-storage/src/query.rs index 3db48f92..ff5a4080 100644 --- a/crates/clarion-storage/src/query.rs +++ b/crates/clarion-storage/src/query.rs @@ -277,7 +277,12 @@ pub fn resolve_file_catalog_entry( entity_id: entity.id, content_hash: entity.content_hash, canonical_path, - language: resolved_language(language, &entity.plugin_id, &lookup_path), + language: resolved_language( + language, + &entity.plugin_id, + &entity.properties_json, + &lookup_path, + ), briefing_blocked, content_hash_path: lookup_path, })); @@ -380,7 +385,15 @@ fn file_content_hash(path: &Path) -> std::io::Result { fs::read(path).map(|bytes| blake3::hash(&bytes).to_hex().to_string()) } -fn resolved_language(requested: &str, plugin_id: &str, path: &Path) -> String { +fn resolved_language( + requested: &str, + plugin_id: &str, + properties_json: &str, + path: &Path, +) -> String { + if let Some(language) = stored_language(properties_json) { + return language; + } if plugin_id != "core" { return plugin_id.to_owned(); } @@ -390,6 +403,16 @@ fn resolved_language(requested: &str, plugin_id: &str, path: &Path) -> String { requested.trim().to_owned() } +fn stored_language(properties_json: &str) -> Option { + serde_json::from_str::(properties_json) + .ok()? + .get("language")? + .as_str() + .map(str::trim) + .filter(|language| !language.is_empty()) + .map(str::to_owned) +} + fn language_for_extension(path: &Path) -> Option { match path.extension().and_then(|extension| extension.to_str()) { Some("py") => Some("python".to_owned()), From a1553ddcbf3b360712398bd62aa346346b7ae30a Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Wed, 20 May 2026 15:04:52 +1000 Subject: [PATCH 045/187] Add HMAC identity for HTTP read API --- crates/clarion-cli/src/http_read.rs | 149 +++++++++++++--- crates/clarion-cli/tests/serve.rs | 168 +++++++++++++++++- crates/clarion-mcp/src/config.rs | 57 +++++- .../adr/ADR-014-filigree-registry-backend.md | 17 +- ...-034-federation-http-read-api-hardening.md | 45 +++-- docs/federation/contracts.md | 54 ++++-- .../get-api-v1-files.demo-python.json | 2 +- .../fixtures/post-api-v1-files-batch.json | 6 +- docs/operator/clarion-http-read-api.md | 50 ++++-- 9 files changed, 461 insertions(+), 87 deletions(-) diff --git a/crates/clarion-cli/src/http_read.rs b/crates/clarion-cli/src/http_read.rs index 548314a9..08cdf3d9 100644 --- a/crates/clarion-cli/src/http_read.rs +++ b/crates/clarion-cli/src/http_read.rs @@ -7,6 +7,7 @@ use std::time::Duration; use std::future::IntoFuture; use anyhow::{Context, Result, anyhow}; +use axum::body::{Body, to_bytes}; use axum::error_handling::HandleErrorLayer; use axum::extract::rejection::QueryRejection; use axum::extract::{Query, State}; @@ -17,6 +18,7 @@ use axum::{Json, Router}; use clarion_mcp::config::HttpReadConfig; use clarion_storage::{CanonicalProjectPath, ReaderPool, StorageError, resolve_file_catalog_entry}; use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; use tokio::sync::oneshot; use tower::limit::ConcurrencyLimitLayer; use tower::load_shed; @@ -124,6 +126,9 @@ struct AppState { /// `Authorization: Bearer ` when `Some`. `/api/v1/_capabilities` /// is always unauthenticated so siblings can probe pre-auth. auth_token: Option>, + /// Resolved Loom component identity HMAC secret. When present, protected + /// routes require `X-Loom-Component: clarion:`. + identity_secret: Option>, } /// Ready-signal payload returned from the HTTP thread back to `spawn`. @@ -175,13 +180,23 @@ where .map(|raw| raw.trim().to_owned()) .filter(|trimmed| !trimmed.is_empty()) .map(Arc::new); + let identity_secret = config + .identity_token_env + .as_deref() + .and_then(&env_lookup) + .map(|raw| raw.trim().to_owned()) + .filter(|trimmed| !trimmed.is_empty()) + .map(Arc::new); let bind = config.bind; - let warn_unauthenticated_non_loopback = - config.allow_non_loopback && !config.is_loopback_bind() && auth_token.is_none(); + let warn_unauthenticated_non_loopback = config.allow_non_loopback + && !config.is_loopback_bind() + && auth_token.is_none() + && identity_secret.is_none(); let (shutdown_tx, shutdown_rx) = oneshot::channel(); let (ready_tx, ready_rx) = std::sync::mpsc::channel::>(); let (failure_tx, failure_rx) = mpsc::channel(); let auth_token_thread = auth_token.clone(); + let identity_secret_thread = identity_secret.clone(); let join = thread::Builder::new() .name("clarion-http-read".to_owned()) .spawn(move || -> Result<()> { @@ -190,6 +205,7 @@ where readers, instance_id, auth_token_thread, + identity_secret_thread, bind, shutdown_rx, ready_tx, @@ -204,7 +220,9 @@ where .recv() .context("wait for HTTP read API bind result")??; let local_addr = ready.local_addr; - let auth = if auth_token.is_some() { + let auth = if identity_secret.is_some() { + "hmac" + } else if auth_token.is_some() { "bearer" } else { "none" @@ -230,6 +248,7 @@ fn run_http_read_server( readers: ReaderPool, instance_id: crate::instance::InstanceId, auth_token: Option>, + identity_secret: Option>, bind: std::net::SocketAddr, shutdown_rx: oneshot::Receiver<()>, ready_tx: mpsc::Sender>, @@ -264,6 +283,7 @@ fn run_http_read_server( readers, instance_id, auth_token, + identity_secret, }; let serve_future = axum::serve(listener, router(state)) .with_graceful_shutdown(async { @@ -330,7 +350,7 @@ fn router(state: AppState) -> Router { .route("/api/v1/files/batch", post(post_files_batch)) .route_layer(axum::middleware::from_fn_with_state( state.clone(), - require_bearer_token, + require_http_identity, )); let unprotected = Router::new().route("/api/v1/_capabilities", get(get_capabilities)); protected.merge(unprotected).with_state(state).layer( @@ -343,29 +363,23 @@ fn router(state: AppState) -> Router { .on_failure(()), ) .layer(timeout::TimeoutLayer::new(Duration::from_secs(10))) - .layer(RequestBodyLimitLayer::new(16 * 1024)) + .layer(RequestBodyLimitLayer::new(HTTP_BODY_LIMIT_BYTES)) .layer(load_shed::LoadShedLayer::new()) .layer(ConcurrencyLimitLayer::new(64)), ) } -/// Enforce `Authorization: Bearer ` on every request to a protected -/// route (currently `/api/v1/files` and the batch endpoint added by -/// CONTRACT-1). Unprotected routes — only `/api/v1/_capabilities` — are -/// served by a sibling sub-router that bypasses this middleware so siblings -/// can probe pre-auth. -/// -/// Trust matrix (per `HttpReadConfig::validate_auth_trust`): -/// - loopback bind, token env unset → `auth_token` is `None`; allow all. -/// - loopback bind, token env set → `auth_token` is `Some`; require it. -/// - non-loopback bind, token env unset → `validate_auth_trust` refused -/// startup, this branch is unreachable. -/// - non-loopback bind, token env set → `auth_token` is `Some`; require it. -async fn require_bearer_token( +/// Enforce configured identity on protected routes. Prefer the Loom HMAC +/// identity when `identity_token_env` is configured; otherwise preserve the +/// legacy bearer-token path for existing deployments. +async fn require_http_identity( State(state): State, - request: axum::extract::Request, + request: Request, next: axum::middleware::Next, ) -> Response { + if let Some(secret) = state.identity_secret.as_ref() { + return require_hmac_identity(secret, request, next).await; + } let Some(expected) = state.auth_token.as_ref() else { return next.run(request).await; }; @@ -377,24 +391,110 @@ async fn require_bearer_token( .map(str::trim) .filter(|token| !token.is_empty()); let Some(presented) = presented else { - return unauthorized_response(); + return unauthenticated_response(); }; // Constant-time compare so a wrong-length-token client can't trivially // distinguish "header absent" from "token mismatch" via timing. if !constant_time_eq(presented.as_bytes(), expected.as_bytes()) { - return unauthorized_response(); + return unauthenticated_response(); } next.run(request).await } -fn unauthorized_response() -> Response { +async fn require_hmac_identity( + secret: &str, + request: Request, + next: axum::middleware::Next, +) -> Response { + let (parts, body) = request.into_parts(); + let method = parts.method.as_str().to_owned(); + let path_and_query = parts.uri.path_and_query().map_or_else( + || parts.uri.path().to_owned(), + |value| value.as_str().to_owned(), + ); + let presented = parts + .headers + .get("x-loom-component") + .and_then(|value| value.to_str().ok()) + .and_then(|value| value.trim().strip_prefix("clarion:")) + .filter(|signature| !signature.is_empty()) + .map(str::to_owned); + let Some(presented) = presented else { + return unauthenticated_response(); + }; + let Ok(body_bytes) = to_bytes(body, HTTP_BODY_LIMIT_BYTES).await else { + return json_error( + StatusCode::BAD_REQUEST, + ErrorCode::InvalidPath, + "request body is invalid", + ); + }; + let expected = component_hmac_hex(secret.as_bytes(), &method, &path_and_query, &body_bytes); + if !constant_time_eq(presented.as_bytes(), expected.as_bytes()) { + return unauthenticated_response(); + } + next.run(Request::from_parts(parts, Body::from(body_bytes))) + .await +} + +fn unauthenticated_response() -> Response { json_error( StatusCode::UNAUTHORIZED, - ErrorCode::Unauthorized, + ErrorCode::Unauthenticated, "authentication required", ) } +fn component_hmac_hex(secret: &[u8], method: &str, path_and_query: &str, body: &[u8]) -> String { + hmac_sha256_hex( + secret, + canonical_hmac_message(method, path_and_query, body).as_bytes(), + ) +} + +fn canonical_hmac_message(method: &str, path_and_query: &str, body: &[u8]) -> String { + format!( + "{}\n{}\n{}", + method, + path_and_query, + hex_lower(&Sha256::digest(body)) + ) +} + +fn hmac_sha256_hex(secret: &[u8], message: &[u8]) -> String { + const BLOCK_SIZE: usize = 64; + let mut key = [0_u8; BLOCK_SIZE]; + if secret.len() > BLOCK_SIZE { + key[..32].copy_from_slice(&Sha256::digest(secret)); + } else { + key[..secret.len()].copy_from_slice(secret); + } + let mut ipad = [0x36_u8; BLOCK_SIZE]; + let mut opad = [0x5c_u8; BLOCK_SIZE]; + for index in 0..BLOCK_SIZE { + ipad[index] ^= key[index]; + opad[index] ^= key[index]; + } + let mut inner = Sha256::new(); + inner.update(ipad); + inner.update(message); + let inner = inner.finalize(); + let mut outer = Sha256::new(); + outer.update(opad); + outer.update(inner); + hex_lower(&outer.finalize()) +} + +fn hex_lower(bytes: &[u8]) -> String { + const HEX: &[u8; 16] = b"0123456789abcdef"; + let mut out = String::with_capacity(bytes.len() * 2); + for byte in bytes { + out.push(HEX[(byte >> 4) as usize] as char); + out.push(HEX[(byte & 0x0f) as usize] as char); + } + out +} + fn constant_time_eq(a: &[u8], b: &[u8]) -> bool { if a.len() != b.len() { return false; @@ -471,7 +571,7 @@ enum ErrorCode { PathOutsideProject, NotFound, BriefingBlocked, - Unauthorized, + Unauthenticated, StorageError, BatchTooLarge, Internal, @@ -483,6 +583,7 @@ enum ErrorCode { /// constant so the contract docs, the validator, and tests all point at /// the same number. const BATCH_MAX_QUERIES: usize = 256; +const HTTP_BODY_LIMIT_BYTES: usize = 16 * 1024; #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields)] diff --git a/crates/clarion-cli/tests/serve.rs b/crates/clarion-cli/tests/serve.rs index 54c7c58f..3e6e137a 100644 --- a/crates/clarion-cli/tests/serve.rs +++ b/crates/clarion-cli/tests/serve.rs @@ -13,6 +13,7 @@ use clarion_core::{ }; use rusqlite::{Connection, params}; use serde_json::Value; +use sha2::{Digest, Sha256}; use uuid::Uuid; const STABLE_INSTANCE_ID: &str = "9bd7234e-6d44-4a38-9ae4-76f912a10221"; @@ -871,7 +872,7 @@ fn serve_http_batch_requires_auth_when_configured() { let authenticated = authenticated.expect("authenticated batch"); assert_eq!(unauthenticated.status_code, 401); - assert_eq!(unauthenticated.body["code"], "UNAUTHORIZED"); + assert_eq!(unauthenticated.body["code"], "UNAUTHENTICATED"); assert_eq!(authenticated.status_code, 200); let resolved = authenticated.body["resolved"] .as_array() @@ -909,7 +910,7 @@ fn serve_http_files_endpoint_requires_bearer_token_when_configured() { let authenticated = authenticated.expect("authenticated probe response"); assert_eq!(unauthenticated.status_code, 401); - assert_eq!(unauthenticated.body["code"], "UNAUTHORIZED"); + assert_eq!(unauthenticated.body["code"], "UNAUTHENTICATED"); assert_eq!(authenticated.status_code, 200); let body: Value = serde_json::from_str(&authenticated.body).expect("authenticated body is JSON"); @@ -961,10 +962,76 @@ fn serve_http_files_endpoint_rejects_wrong_token() { assert_eq!(response.status_code, 401, "{name}: {response:?}"); let body: Value = serde_json::from_str(&response.body) .unwrap_or_else(|err| panic!("{name} body parse: {err}; raw={:?}", response.body)); - assert_eq!(body["code"], "UNAUTHORIZED", "{name}"); + assert_eq!(body["code"], "UNAUTHENTICATED", "{name}"); } } +#[test] +fn serve_http_files_endpoint_requires_hmac_identity_when_configured() { + let dir = tempfile::tempdir().expect("temp project"); + clarion_bin() + .args(["install", "--path"]) + .arg(dir.path()) + .env("PATH", "") + .assert() + .success(); + seed_file_entity(dir.path()); + let bind = free_loopback_bind(); + write_http_config_with_identity_token_env(dir.path(), &bind, "CLARION_TEST_LOOM_IDENTITY_REQ"); + + let mut child = spawn_serve_with_env( + dir.path(), + &[("CLARION_TEST_LOOM_IDENTITY_REQ", "shared-secret")], + ); + let path = "/api/v1/files?path=demo.py&language=python"; + let missing = wait_for_http_raw_response(&bind, path, &[]); + let signed_header = hmac_component_header("shared-secret", "GET", path, b""); + let signed = wait_for_http_raw_response(&bind, path, &[("X-Loom-Component", &signed_header)]); + stop_serve(&mut child); + let missing = missing.expect("missing identity response"); + let signed = signed.expect("signed identity response"); + + assert_eq!(missing.status_code, 401); + let missing_body: Value = + serde_json::from_str(&missing.body).expect("missing identity body is JSON"); + assert_eq!(missing_body["code"], "UNAUTHENTICATED"); + assert_eq!(signed.status_code, 200); + let signed_body: Value = serde_json::from_str(&signed.body).expect("signed body is JSON"); + assert_eq!(signed_body["entity_id"], "core:file:demo.py"); +} + +#[test] +fn serve_http_files_endpoint_rejects_wrong_hmac_identity() { + let dir = tempfile::tempdir().expect("temp project"); + clarion_bin() + .args(["install", "--path"]) + .arg(dir.path()) + .env("PATH", "") + .assert() + .success(); + seed_file_entity(dir.path()); + let bind = free_loopback_bind(); + write_http_config_with_identity_token_env( + dir.path(), + &bind, + "CLARION_TEST_LOOM_IDENTITY_WRONG", + ); + + let mut child = spawn_serve_with_env( + dir.path(), + &[("CLARION_TEST_LOOM_IDENTITY_WRONG", "shared-secret")], + ); + let path = "/api/v1/files?path=demo.py&language=python"; + let wrong_header = hmac_component_header("other-secret", "GET", path, b""); + let response = wait_for_http_raw_response(&bind, path, &[("X-Loom-Component", &wrong_header)]); + stop_serve(&mut child); + let response = response.expect("wrong identity response"); + let body: Value = serde_json::from_str(&response.body).expect("wrong identity body is JSON"); + + assert_eq!(response.status_code, 401); + assert_eq!(body["code"], "UNAUTHENTICATED"); +} + #[test] fn serve_http_capabilities_does_not_require_token() { let dir = tempfile::tempdir().expect("temp project"); @@ -1024,6 +1091,38 @@ fn serve_http_refuses_startup_on_non_loopback_without_token() { ); } +#[test] +fn serve_http_refuses_startup_when_identity_env_is_missing() { + let dir = tempfile::tempdir().expect("temp project"); + clarion_bin() + .args(["install", "--path"]) + .arg(dir.path()) + .env("PATH", "") + .assert() + .success(); + let bind = free_loopback_bind(); + write_http_config_with_identity_token_env( + dir.path(), + &bind, + "CLARION_TEST_LOOM_IDENTITY_MISSING", + ); + + let child = spawn_serve_with_env(dir.path(), &[]); + let output = wait_for_child_exit(child, Duration::from_secs(2)) + .expect("serve should refuse to start when identity env is missing"); + + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("CLA-CONFIG-HTTP-IDENTITY-MISSING"), + "error should cite CLA-CONFIG-HTTP-IDENTITY-MISSING: {stderr}" + ); + assert!( + stderr.contains("CLARION_TEST_LOOM_IDENTITY_MISSING"), + "error should name the configured identity_token_env: {stderr}" + ); +} + #[test] fn serve_rejects_non_loopback_http_bind_before_binding_without_opt_in() { let dir = tempfile::tempdir().expect("temp project"); @@ -1902,6 +2001,69 @@ fn write_http_config_with_token_env(project_root: &Path, bind: &str, token_env: .expect("write HTTP serve config with token_env"); } +fn write_http_config_with_identity_token_env(project_root: &Path, bind: &str, token_env: &str) { + fs::write( + project_root.join("clarion.yaml"), + format!( + "version: 1\nserve:\n http:\n enabled: true\n bind: \"{bind}\"\n identity_token_env: \"{token_env}\"\n" + ), + ) + .expect("write HTTP serve config with identity_token_env"); +} + +fn hmac_component_header(secret: &str, method: &str, path_and_query: &str, body: &[u8]) -> String { + format!( + "clarion:{}", + hmac_sha256_hex( + secret.as_bytes(), + canonical_hmac_message(method, path_and_query, body).as_bytes() + ) + ) +} + +fn canonical_hmac_message(method: &str, path_and_query: &str, body: &[u8]) -> String { + format!( + "{}\n{}\n{}", + method, + path_and_query, + hex_lower(&Sha256::digest(body)) + ) +} + +fn hmac_sha256_hex(secret: &[u8], message: &[u8]) -> String { + const BLOCK_SIZE: usize = 64; + let mut key = [0_u8; BLOCK_SIZE]; + if secret.len() > BLOCK_SIZE { + key[..32].copy_from_slice(&Sha256::digest(secret)); + } else { + key[..secret.len()].copy_from_slice(secret); + } + let mut ipad = [0x36_u8; BLOCK_SIZE]; + let mut opad = [0x5c_u8; BLOCK_SIZE]; + for index in 0..BLOCK_SIZE { + ipad[index] ^= key[index]; + opad[index] ^= key[index]; + } + let mut inner = Sha256::new(); + inner.update(ipad); + inner.update(message); + let inner = inner.finalize(); + let mut outer = Sha256::new(); + outer.update(opad); + outer.update(inner); + hex_lower(&outer.finalize()) +} + +fn hex_lower(bytes: &[u8]) -> String { + const HEX: &[u8; 16] = b"0123456789abcdef"; + let mut out = String::with_capacity(bytes.len() * 2); + for byte in bytes { + out.push(HEX[(byte >> 4) as usize] as char); + out.push(HEX[(byte & 0x0f) as usize] as char); + } + out +} + struct ServeChild { child: Option, } diff --git a/crates/clarion-mcp/src/config.rs b/crates/clarion-mcp/src/config.rs index 9b777744..ecb1b589 100644 --- a/crates/clarion-mcp/src/config.rs +++ b/crates/clarion-mcp/src/config.rs @@ -256,6 +256,11 @@ pub struct HttpReadConfig { /// refuses to start (`CLA-CONFIG-HTTP-NO-AUTH`). Default /// `CLARION_LOOM_TOKEN` matches Filigree's pinned client default. pub token_env: String, + /// Optional env var holding the Loom component identity HMAC secret. + /// When configured, `clarion serve` refuses to start unless the env var + /// exists and protected HTTP read routes require + /// `X-Loom-Component: clarion:`. + pub identity_token_env: Option, } impl Default for HttpReadConfig { @@ -265,6 +270,7 @@ impl Default for HttpReadConfig { bind: SocketAddr::from(([127, 0, 0, 1], 9111)), allow_non_loopback: false, token_env: "CLARION_LOOM_TOKEN".to_owned(), + identity_token_env: None, } } } @@ -288,7 +294,28 @@ impl HttpReadConfig { where F: Fn(&str) -> Option, { - if !self.enabled || self.is_loopback_bind() { + if !self.enabled { + return Ok(()); + } + let has_identity_secret = match self.identity_token_env.as_deref() { + Some(env_var) => { + let has_secret = env_lookup(env_var) + .as_deref() + .is_some_and(|value| !value.trim().is_empty()); + if !has_secret { + return Err(ConfigError::MissingHttpIdentitySecret { + code: "CLA-CONFIG-HTTP-IDENTITY-MISSING", + token_env: env_var.to_owned(), + }); + } + true + } + None => false, + }; + if self.is_loopback_bind() { + return Ok(()); + } + if has_identity_secret { return Ok(()); } let has_token = env_lookup(&self.token_env) @@ -445,6 +472,15 @@ pub enum ConfigError { token_env: String, }, + #[error( + "{code}: serve.http.identity_token_env names ${token_env}, but that env var is unset; \ + refusing to start an HTTP read API with incomplete Loom component identity configuration." + )] + MissingHttpIdentitySecret { + code: &'static str, + token_env: String, + }, + #[error( "{code}: clarion.yaml contains both `llm` and `llm_policy` top-level keys; \ `llm_policy` is a serde alias for `llm` and serde silently discards one. \ @@ -779,6 +815,25 @@ serve: assert!(cfg.serve.http.allow_non_loopback); } + #[test] + fn http_identity_token_env_is_parsed_when_config_loads() { + let cfg = McpConfig::from_yaml_str( + r#" +serve: + http: + enabled: true + bind: "127.0.0.1:0" + identity_token_env: CLARION_TEST_IDENTITY +"#, + ) + .expect("parse HTTP identity_token_env"); + + assert_eq!( + cfg.serve.http.identity_token_env.as_deref(), + Some("CLARION_TEST_IDENTITY") + ); + } + #[test] fn enabled_non_loopback_http_bind_requires_allow_non_loopback() { let err = McpConfig::from_yaml_str( diff --git a/docs/clarion/adr/ADR-014-filigree-registry-backend.md b/docs/clarion/adr/ADR-014-filigree-registry-backend.md index 4dd8aa00..686fee13 100644 --- a/docs/clarion/adr/ADR-014-filigree-registry-backend.md +++ b/docs/clarion/adr/ADR-014-filigree-registry-backend.md @@ -105,6 +105,7 @@ The initial `ErrorCode` enum is closed to: - `INVALID_PATH` - `PATH_OUTSIDE_PROJECT` - `NOT_FOUND` +- `UNAUTHENTICATED` - `STORAGE_ERROR` - `INTERNAL` @@ -112,11 +113,17 @@ Clients must switch on `code`, not on human-readable `error` text. ### Security Posture -The HTTP read API is unauthenticated and loopback-only by default. Clarion -refuses non-loopback HTTP binds unless `serve.http.allow_non_loopback: true` is -set. When that opt-in is present, startup logs must warn that the HTTP read API -surface is unauthenticated and must be protected by operator-managed network -controls. +The HTTP read API is loopback-only by default and may remain unauthenticated for +local sidecar workflows. Authenticated deployments configure +`serve.http.identity_token_env`; Clarion refuses to start if that env var is +missing, and protected read routes require +`X-Loom-Component: clarion:`. + +The HMAC is lowercase hex HMAC-SHA256 over `METHOD`, `PATH_AND_QUERY`, and the +SHA-256 request-body hash separated by newlines. Clarion refuses non-loopback +HTTP binds unless `serve.http.allow_non_loopback: true` is set, and a +non-loopback bind must have either the HMAC identity secret or the legacy bearer +token resolved at startup. ## Alternatives Considered diff --git a/docs/clarion/adr/ADR-034-federation-http-read-api-hardening.md b/docs/clarion/adr/ADR-034-federation-http-read-api-hardening.md index 396d9663..47e7343a 100644 --- a/docs/clarion/adr/ADR-034-federation-http-read-api-hardening.md +++ b/docs/clarion/adr/ADR-034-federation-http-read-api-hardening.md @@ -1,4 +1,4 @@ -# ADR-034: Federation HTTP Read API Hardening — Bearer Auth, Batch Resolution, `BRIEFING_BLOCKED`, Instance ID +# ADR-034: Federation HTTP Read API Hardening — Identity Auth, Batch Resolution, `BRIEFING_BLOCKED`, Instance ID **Status**: Accepted **Date**: 2026-05-19 @@ -8,7 +8,7 @@ ## Summary -Sprint 3 hardens Clarion's HTTP read API beyond ADR-014's original posture: protected routes require an `Authorization: Bearer` token resolved from an operator-named environment variable, a new `POST /api/v1/files/batch` endpoint handles bulk path resolution in a single round trip, a distinct `403 BRIEFING_BLOCKED` response distinguishes blocked entities from "not found" entities without leaking identity, and `GET /api/v1/_capabilities` echoes a stable per-project `instance_id` so siblings can detect endpoint rebinding. These additions extend ADR-014's wire-contract surface without breaking it — `api_version` remains `1`. +Sprint 3 hardens Clarion's HTTP read API beyond ADR-014's original posture: protected routes can require Loom component identity via `X-Loom-Component: clarion:` resolved from an operator-named environment variable, a new `POST /api/v1/files/batch` endpoint handles bulk path resolution in a single round trip, a distinct `403 BRIEFING_BLOCKED` response distinguishes blocked entities from "not found" entities without leaking identity, and `GET /api/v1/_capabilities` echoes a stable per-project `instance_id` so siblings can detect endpoint rebinding. These additions extend ADR-014's wire-contract surface without breaking it — `api_version` remains `1`. ## Context @@ -23,24 +23,28 @@ Sprint 3 implemented these four hardenings (`1109560`, `acbf465`, `eb6200d`, `2c ## Decision -### 1. Bearer authentication on protected routes +### 1. Loom component identity on protected routes -`/api/v1/files`-family endpoints require `Authorization: Bearer ` when Clarion has resolved a token at startup. `/api/v1/_capabilities` is **always** unauthenticated so siblings can probe the API surface before they hold a secret. +`/api/v1/files`-family endpoints require `X-Loom-Component: clarion:` when Clarion has resolved `serve.http.identity_token_env` at startup. `/api/v1/_capabilities` is **always** unauthenticated so siblings can probe the API surface before they hold a secret. + +The HMAC is lowercase hex HMAC-SHA256 over a newline-separated canonical message: request method, path plus query string, and SHA-256 hex of the request body. This signs the request line and body digest without turning the shared secret into a bearer credential on the wire. Clarion preserves the older `serve.http.token_env` bearer-token mode for compatibility when `identity_token_env` is not configured. Token-resolution and bind-policy trust matrix, enforced at startup by `HttpReadConfig::validate_auth_trust` before the listener binds: -| Bind | `token_env` resolved | Behaviour | -|---|---|---| -| Loopback | unset | Unauthenticated; allow all requests (matches ADR-014's original posture for the loopback case). | -| Loopback | set | Bearer required on protected routes; capabilities always allowed. | -| Non-loopback | unset | **Refuse to start** with `CLA-CONFIG-HTTP-NO-AUTH`. | -| Non-loopback | set | Bearer required on protected routes. | +| Bind | `identity_token_env` resolved | `token_env` resolved | Behaviour | +|---|---|---|---| +| Loopback | unset | unset | Unauthenticated; allow all requests (matches ADR-014's original posture for the loopback case). | +| Loopback | set | any | HMAC required on protected routes; capabilities always allowed. | +| Loopback | configured but env missing | any | **Refuse to start** with `CLA-CONFIG-HTTP-IDENTITY-MISSING`. | +| Non-loopback | set | any | HMAC required on protected routes. | +| Non-loopback | unset | set | Bearer required on protected routes. | +| Non-loopback | unset | unset | **Refuse to start** with `CLA-CONFIG-HTTP-NO-AUTH`. | -Non-loopback-without-auth refusal extends ADR-014's `allow_non_loopback` opt-in: there is no longer a "non-loopback unauthenticated" mode. Non-loopback binds require **both** `allow_non_loopback: true` **and** a resolved `token_env`; either alone is insufficient. The opt-in remains the gate that admits non-loopback binds at all, but it no longer admits them unauthenticated. +Non-loopback-without-auth refusal extends ADR-014's `allow_non_loopback` opt-in: there is no longer a "non-loopback unauthenticated" mode. Non-loopback binds require **both** `allow_non_loopback: true` **and** a resolved HMAC identity secret or legacy bearer token; either alone is insufficient. The opt-in remains the gate that admits non-loopback binds at all, but it no longer admits them unauthenticated. -Bearer rejection (any of: header absent, wrong scheme, wrong token, blank token) returns `401 Unauthorized` with the standard error envelope and `code: "UNAUTHORIZED"`. Token comparison is constant-time so a wrong-length-token client cannot distinguish "header absent" from "token mismatch" via timing. The token value is never logged; the bind-time log line records `auth=bearer` or `auth=none`, not the token itself. +Authentication rejection (any of: header absent, wrong scheme/prefix, wrong token or signature, blank token or signature) returns `401 Unauthorized` with the standard error envelope and `code: "UNAUTHENTICATED"`. Secret comparison is constant-time so a wrong-length client cannot distinguish "header absent" from "secret mismatch" via timing. The secret value is never logged; the bind-time log line records `auth=hmac`, `auth=bearer`, or `auth=none`, not the secret itself. -The token is resolved from an operator-named environment variable; the config field is `serve.http.token_env` (default `CLARION_LOOM_TOKEN`, matching Filigree's pinned client default). The token value itself never appears in `clarion.yaml`. +The preferred secret is resolved from an operator-named environment variable; the config field is `serve.http.identity_token_env`. The legacy bearer-token config field is `serve.http.token_env` (default `CLARION_LOOM_TOKEN`, matching Filigree's pinned client default). Secret values never appear in `clarion.yaml`. ### 2. `POST /api/v1/files/batch` @@ -80,7 +84,7 @@ ADR-014's original error-code set is extended to: ``` INVALID_PATH | PATH_OUTSIDE_PROJECT | NOT_FOUND | -BRIEFING_BLOCKED | UNAUTHORIZED | STORAGE_ERROR | +BRIEFING_BLOCKED | UNAUTHENTICATED | STORAGE_ERROR | BATCH_TOO_LARGE | INTERNAL ``` @@ -105,9 +109,13 @@ The set remains closed. Clients must switch on `code`, not on `error` text. ## Alternatives Considered -### Alternative 1: HMAC signatures instead of bearer tokens +### Alternative 1: Bearer-only authentication -Better cryptographic story (per-request authentication, replay resistance, no shared-secret leakage on `GET` URLs), but considerably more client-side complexity. Filigree's `ClarionRegistry` would need an HMAC helper, and the operator deployment story would require signing-key rotation discipline. Sprint 3 needed *some* authentication on the wire — bearer tokens are the path of least resistance and the v1.0 federation surface is small enough that a coarse "any reader, full access" trust model is acceptable. HMAC is tracked as post-1.0 hardening (filigree `clarion-6814b2ad90`). +Bearer-only authentication was the first Sprint 3 hardening because it was simple +to operate. It is retained for compatibility through `serve.http.token_env`, but +new Loom deployments should prefer `identity_token_env`: the HMAC shape +authenticates the request line and body digest instead of sending the shared +secret as the credential on every request. ### Alternative 2: Unix Domain Socket-only auth (ADR-012's original posture) @@ -133,8 +141,8 @@ The Sprint 3 tasking doc originally proposed pinning these decisions into ADR-01 ### Negative - Operators upgrading from ADR-014's unauthenticated posture to a non-loopback federation deployment must now provide a token at startup. The `CLA-CONFIG-HTTP-NO-AUTH` startup refusal makes this a fail-closed migration, not a silent one, but the operator-facing diff is non-trivial. -- Bearer tokens are coarser than HMAC. Any client that holds the token has full read access to the federation surface, and a token leak requires a rotation across both Clarion and every Filigree client. Post-1.0 hardening (HMAC, key rotation) is required for richer deployments. -- The error-code enum is now wider than ADR-014's original. Filigree's `code`-switch logic must handle `BRIEFING_BLOCKED`, `UNAUTHORIZED`, and `BATCH_TOO_LARGE` in addition to the original five. The federation contract documents this; clients that ignore the new codes will surface them as unhandled errors rather than misinterpret them. +- HMAC identity still uses a shared secret; a leak requires rotation across both Clarion and every sibling client. Replay windows, key identifiers, and rotation metadata remain future hardening. +- The error-code enum is now wider than ADR-014's original. Filigree's `code`-switch logic must handle `BRIEFING_BLOCKED`, `UNAUTHENTICATED`, and `BATCH_TOO_LARGE` in addition to the original five. The federation contract documents this; clients that ignore the new codes will surface them as unhandled errors rather than misinterpret them. ### Neutral @@ -155,5 +163,6 @@ The Sprint 3 tasking doc originally proposed pinning these decisions into ADR-01 - Implementing commits: - `1109560` feat(http_read): return 403 BRIEFING_BLOCKED for blocked entities - `acbf465` feat(http_read): require Authorization: Bearer for /api/v1/files + - C-4 follow-up: prefer `X-Loom-Component: clarion:` when `serve.http.identity_token_env` is configured - `eb6200d` feat(http_read): add POST /api/v1/files/batch + document path normalization - `2c3311a` feat(http_read): formatting / fix(instance): UUID generation comments / test(serve) diff --git a/docs/federation/contracts.md b/docs/federation/contracts.md index 862fc51a..df09a6e1 100644 --- a/docs/federation/contracts.md +++ b/docs/federation/contracts.md @@ -12,8 +12,11 @@ serve: http: enabled: true bind: 127.0.0.1:9111 + # Preferred 1.0 identity mode. Optional on loopback, required for + # authenticated Loom component requests. + identity_token_env: CLARION_LOOM_IDENTITY_SECRET # Name of the env var holding the inbound bearer token. Optional on a - # loopback bind, required on a non-loopback bind. Default + # loopback bind, accepted for compatibility on non-loopback binds. Default # `CLARION_LOOM_TOKEN` matches Filigree's pinned client default. token_env: CLARION_LOOM_TOKEN ``` @@ -24,34 +27,47 @@ read-only and uses Clarion's existing SQLite reader pool. ### Authentication The `/api/v1/files`-family endpoints require -`Authorization: Bearer ` when Clarion has resolved a token at -startup; `/api/v1/_capabilities` is **always** unauthenticated so -siblings can probe the API surface pre-auth. +`X-Loom-Component: clarion:` when Clarion has resolved +`serve.http.identity_token_env` at startup. The HMAC is lowercase hex +HMAC-SHA256 over the canonical message: + +```text + + + +``` + +`/api/v1/_capabilities` is **always** unauthenticated so siblings can probe the +API surface pre-auth. Clarion still accepts the older +`Authorization: Bearer ` path when `token_env` resolves and +`identity_token_env` is not configured. Trust matrix (enforced by `HttpReadConfig::validate_auth_trust` at startup, before binding): -| Bind | `token_env` resolved | Behaviour | -|---|---|---| -| Loopback | unset | Unauthenticated; allow all requests. | -| Loopback | set | Bearer required on protected routes; capabilities always allowed. | -| Non-loopback | unset | **Refuse to start** with `CLA-CONFIG-HTTP-NO-AUTH`. | -| Non-loopback | set | Bearer required on protected routes. | +| Bind | `identity_token_env` resolved | `token_env` resolved | Behaviour | +|---|---|---|---| +| Loopback | unset | unset | Unauthenticated; allow all requests. | +| Loopback | set | any | HMAC required on protected routes; capabilities always allowed. | +| Loopback | configured but env missing | any | **Refuse to start** with `CLA-CONFIG-HTTP-IDENTITY-MISSING`. | +| Non-loopback | set | any | HMAC required on protected routes. | +| Non-loopback | unset | set | Bearer required on protected routes. | +| Non-loopback | unset | unset | **Refuse to start** with `CLA-CONFIG-HTTP-NO-AUTH`. | -Bearer rejection (any of: header absent, wrong scheme, wrong token, -blank token) returns: +Authentication rejection (header absent, wrong scheme/prefix, wrong token or +signature, blank token or signature) returns: ```http HTTP/1.1 401 Unauthorized Content-Type: application/json -{"error": "authentication required", "code": "UNAUTHORIZED"} +{"error": "authentication required", "code": "UNAUTHENTICATED"} ``` -Token comparison is constant-time so a wrong-length-token client cannot -distinguish "header absent" from "token mismatch" via timing. The token -itself is never logged; the bind-time log line records -`auth=bearer` or `auth=none`, not the token value. +Secret comparison is constant-time so a wrong-length client cannot distinguish +"header absent" from "secret mismatch" via timing. The secret itself is never +logged; the bind-time log line records `auth=hmac`, `auth=bearer`, or +`auth=none`, not the secret value. All non-2xx responses use this closed JSON error envelope: @@ -63,7 +79,7 @@ All non-2xx responses use this closed JSON error envelope: ``` The initial `code` enum is closed to `INVALID_PATH`, -`PATH_OUTSIDE_PROJECT`, `NOT_FOUND`, `BRIEFING_BLOCKED`, `UNAUTHORIZED`, +`PATH_OUTSIDE_PROJECT`, `NOT_FOUND`, `BRIEFING_BLOCKED`, `UNAUTHENTICATED`, `STORAGE_ERROR`, `BATCH_TOO_LARGE`, and `INTERNAL`. Clients must switch on `code`; `error` is human-readable diagnostic text. `BATCH_TOO_LARGE` is only emitted by `POST /api/v1/files/batch` (see the batch endpoint @@ -184,7 +200,7 @@ Failure modes (envelope-level): |---|---|---| | 400 | `INVALID_PATH` | Body is not a valid `{"queries": [...]}` JSON object. | | 400 | `BATCH_TOO_LARGE` | `queries.len() > 256`. Filigree must split client-side. | -| 401 | `UNAUTHORIZED` | Bearer auth missing or wrong (when configured — see §Authentication). | +| 401 | `UNAUTHENTICATED` | HMAC or bearer auth missing or wrong (when configured — see §Authentication). | | 413 | n/a | Request body exceeds the 16 KiB cap (transport-level). | | 500/503 | `STORAGE_ERROR` / `INTERNAL` | Whole-batch storage failure. | diff --git a/docs/federation/fixtures/get-api-v1-files.demo-python.json b/docs/federation/fixtures/get-api-v1-files.demo-python.json index 4a8cf6ae..3e40b41c 100644 --- a/docs/federation/fixtures/get-api-v1-files.demo-python.json +++ b/docs/federation/fixtures/get-api-v1-files.demo-python.json @@ -56,7 +56,7 @@ "PATH_OUTSIDE_PROJECT", "NOT_FOUND", "BRIEFING_BLOCKED", - "UNAUTHORIZED", + "UNAUTHENTICATED", "STORAGE_ERROR", "INTERNAL" ] diff --git a/docs/federation/fixtures/post-api-v1-files-batch.json b/docs/federation/fixtures/post-api-v1-files-batch.json index 09c05af2..273f66f3 100644 --- a/docs/federation/fixtures/post-api-v1-files-batch.json +++ b/docs/federation/fixtures/post-api-v1-files-batch.json @@ -52,7 +52,7 @@ "enum": [ "INVALID_PATH", "BATCH_TOO_LARGE", - "UNAUTHORIZED", + "UNAUTHENTICATED", "STORAGE_ERROR", "INTERNAL" ] @@ -132,14 +132,14 @@ "body": { "queries": [{"path": "demo.py", "language": "python"}] }, - "note": "Authorization header missing or wrong; applies when serve.http.token_env resolves at startup." + "note": "Identity header missing or wrong; applies when serve.http.identity_token_env resolves at startup. Legacy bearer mode has the same error envelope." }, "response": { "status": 401, "shape": "batch_envelope_error", "body": { "error": "authentication required", - "code": "UNAUTHORIZED" + "code": "UNAUTHENTICATED" } } } diff --git a/docs/operator/clarion-http-read-api.md b/docs/operator/clarion-http-read-api.md index 837fbf26..0e699a08 100644 --- a/docs/operator/clarion-http-read-api.md +++ b/docs/operator/clarion-http-read-api.md @@ -6,19 +6,43 @@ the [federation contracts](../federation/contracts.md). ## Trust Model -The HTTP read API is unauthenticated. By default, `clarion serve` binds it only -to loopback addresses so it is reachable from local processes on the same host, -not from the network. - -Do not bind the HTTP read API to a non-loopback address unless an authenticated -reverse proxy or equivalent access-control layer is in front of it. Clarion -refuses non-loopback binds unless `serve.http.allow_non_loopback: true` is set. -That opt-in is an operator assertion that the unauthenticated Clarion HTTP -surface is protected outside Clarion. - -Startup logs for non-loopback opt-in must warn that the API is unauthenticated. -Treat the endpoint as source-code metadata exposure: anyone who can reach it can -read Clarion's catalog responses for the project. +By default, `clarion serve` binds the HTTP read API only to loopback addresses so +it is reachable from local processes on the same host, not from the network. A +loopback-only API may run without authentication for local sidecar workflows. + +For authenticated mode, set `serve.http.identity_token_env` to the name of an +environment variable that contains the shared Loom component secret: + +```yaml +serve: + http: + enabled: true + bind: 127.0.0.1:9111 + identity_token_env: CLARION_LOOM_IDENTITY_SECRET +``` + +When `identity_token_env` is configured, Clarion refuses to start unless the env +var is present and non-empty. Protected `/api/v1/files` routes then require +`X-Loom-Component: clarion:`. The HMAC is lowercase hex HMAC-SHA256 over: + +```text + + + +``` + +For example, a GET of `/api/v1/files?path=demo.py&language=python` signs the +method `GET`, that exact path-and-query string, and the SHA-256 hash of an empty +body. `GET /api/v1/_capabilities` stays unauthenticated so siblings can probe +the API surface before sending protected reads. + +Clarion still accepts the older `serve.http.token_env` bearer-token path for +compatibility. Prefer `identity_token_env` for new deployments. + +Clarion refuses non-loopback binds unless `serve.http.allow_non_loopback: true` +is set. Non-loopback deployments must also configure authentication; otherwise +Clarion refuses to bind. Treat the endpoint as source-code metadata exposure: +anyone who can reach it can read Clarion's catalog responses for the project. ## Contract Summary From 68b719cdfe2d54f1e4de55b73d1c91762c5032db Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Wed, 20 May 2026 16:12:28 +1000 Subject: [PATCH 046/187] Bound Pyright dogfood analysis --- .../clarion_plugin_python/pyright_session.py | 128 +++++++++++++-- .../src/clarion_plugin_python/server.py | 7 + plugins/python/tests/test_pyright_session.py | 151 +++++++++++++++++- plugins/python/tests/test_server.py | 49 ++++++ 4 files changed, 324 insertions(+), 11 deletions(-) diff --git a/plugins/python/src/clarion_plugin_python/pyright_session.py b/plugins/python/src/clarion_plugin_python/pyright_session.py index b7909d4c..6d9bfa2f 100644 --- a/plugins/python/src/clarion_plugin_python/pyright_session.py +++ b/plugins/python/src/clarion_plugin_python/pyright_session.py @@ -44,7 +44,19 @@ MAX_REFERENCE_SITES_PER_FILE = 2000 PYRIGHT_INIT_TIMEOUT_SECS = 30.0 PYRIGHT_CALL_TIMEOUT_SECS = 5.0 +PYRIGHT_FILE_TIMEOUT_SECS = 3.0 STDERR_TAIL_LIMIT = 65536 +PYRIGHT_EXCLUDE_PATTERNS = [ + "**/.clarion/**", + "**/.git/**", + "**/.hg/**", + "**/.svn/**", + "**/.jj/**", + "**/.venv/**", + "**/__pycache__/**", + "**/node_modules/**", +] +PROJECT_LOCAL_EXTERNAL_DIRS = {".clarion", ".git", ".hg", ".svn", ".jj", ".venv", "node_modules"} if TYPE_CHECKING: @@ -125,6 +137,7 @@ def __init__( # noqa: PLR0913 - knobs are tested lifecycle boundaries. install_check: Callable[[str], bool] | None = None, init_timeout_secs: float = PYRIGHT_INIT_TIMEOUT_SECS, call_timeout_secs: float = PYRIGHT_CALL_TIMEOUT_SECS, + file_timeout_secs: float = PYRIGHT_FILE_TIMEOUT_SECS, max_restarts_per_run: int = MAX_PYRIGHT_RESTARTS_PER_RUN, max_reference_sites_per_file: int = MAX_REFERENCE_SITES_PER_FILE, ) -> None: @@ -134,6 +147,7 @@ def __init__( # noqa: PLR0913 - knobs are tested lifecycle boundaries. self.install_check = install_check self.init_timeout_secs = init_timeout_secs self.call_timeout_secs = call_timeout_secs + self.file_timeout_secs = file_timeout_secs self.max_restarts_per_run = max_restarts_per_run self.max_reference_sites_per_file = max_reference_sites_per_file self._process: subprocess.Popen[bytes] | None = None @@ -145,6 +159,7 @@ def __init__( # noqa: PLR0913 - knobs are tested lifecycle boundaries. self._findings: list[Finding] = [] self._function_indexes: dict[Path, _FunctionIndex] = {} self._index_parse_latency_ms: list[int] = [] + self._file_deadlines: dict[Path, float] = {} def __enter__(self) -> Self: return self @@ -204,9 +219,15 @@ def resolve_calls( findings=self._pop_findings(), ) + deadline = self._deadline_for_file(path) latency_started = time.perf_counter() try: - edges, unresolved, unresolved_sites = self._resolve_with_pyright(path, index, requested) + edges, unresolved, unresolved_sites = self._resolve_with_pyright( + path, + index, + requested, + deadline, + ) except LspTimeoutError as exc: self._record_finding( FINDING_PYRIGHT_CALL_RESOLUTION_TIMEOUT, @@ -267,12 +288,14 @@ def resolve_references( findings=self._pop_findings(), ) + deadline = self._deadline_for_file(path) latency_started = time.perf_counter() try: edges, resolved, skipped_external, unresolved = self._resolve_references_with_pyright( path, index, sites, + deadline, ) except LspTimeoutError as exc: self._record_finding( @@ -290,6 +313,8 @@ def resolve_references( resolved = 0 skipped_external = 0 unresolved = reference_sites_total + finally: + self._file_deadlines.pop(path, None) latency_ms = max(1, math.ceil((time.perf_counter() - latency_started) * 1000)) return ReferenceResolutionResult( @@ -308,6 +333,7 @@ def _resolve_with_pyright( path: Path, index: _FunctionIndex, functions: Sequence[_FunctionInfo], + deadline: float, ) -> tuple[list[CallsRawEdge], int, list[UnresolvedCallSite]]: uri = path.as_uri() self._notify( @@ -326,6 +352,7 @@ def _resolve_with_pyright( unresolved_total = 0 unresolved_sites: list[UnresolvedCallSite] = [] for function in functions: + self._ensure_file_budget(deadline) grouped: dict[tuple[int, int, int, int], set[str]] = {} prepared = self._request( "textDocument/prepareCallHierarchy", @@ -333,14 +360,15 @@ def _resolve_with_pyright( "textDocument": {"uri": uri}, "position": {"line": function.line, "character": function.character}, }, - self.call_timeout_secs, + self._budgeted_timeout(deadline), ) items = prepared if isinstance(prepared, list) else [] for item in items: + self._ensure_file_budget(deadline) outgoing = self._request( "callHierarchy/outgoingCalls", {"item": item}, - self.call_timeout_secs, + self._budgeted_timeout(deadline), ) calls = outgoing if isinstance(outgoing, list) else [] for call in calls: @@ -397,6 +425,7 @@ def _resolve_references_with_pyright( path: Path, index: _FunctionIndex, sites: Sequence[ReferenceSite], + deadline: float, ) -> tuple[list[ReferencesRawEdge], int, int, int]: uri = path.as_uri() self._notify( @@ -417,17 +446,30 @@ def _resolve_references_with_pyright( resolved_total = 0 skipped_external_total = 0 unresolved_total = 0 - for site in sites: + for site_index, site in enumerate(sites): + if self._file_budget_expired(deadline): + unresolved_total += len(sites) - site_index + self._record_finding( + FINDING_PYRIGHT_REFERENCE_RESOLUTION_TIMEOUT, + "pyright reference query timed out: analyze_file budget", + method="analyze_file budget", + ) + break cache_key = _reference_lookup_cache_key(site, source_bytes) cached = lookup_cache.get(cache_key) if cached is None: try: - candidate_ids, saw_external = self._reference_target_ids(uri, site) + candidate_ids, saw_external = self._reference_target_ids( + uri, + site, + deadline=deadline, + ) if not candidate_ids and site.kind == "annotation" and not saw_external: candidate_ids, fallback_external = self._reference_target_ids( uri, site, method="textDocument/typeDefinition", + deadline=deadline, ) saw_external = saw_external or fallback_external except LspTimeoutError as exc: @@ -469,6 +511,7 @@ def _reference_target_ids( uri: str, site: ReferenceSite, *, + deadline: float, method: str = "textDocument/definition", ) -> tuple[list[str], bool]: result = self._request( @@ -477,10 +520,31 @@ def _reference_target_ids( "textDocument": {"uri": uri}, "position": {"line": site.line, "character": site.character}, }, - self.call_timeout_secs, + self._budgeted_timeout(deadline), ) return self._target_ids_from_locations(result) + def _deadline_for_file(self, path: Path) -> float: + return self._file_deadlines.setdefault( + path, + time.monotonic() + self.file_timeout_secs, + ) + + def _budgeted_timeout(self, deadline: float) -> float: + remaining = deadline - time.monotonic() + if remaining <= 0: + method = "analyze_file budget" + raise LspTimeoutError(method) + return min(self.call_timeout_secs, remaining) + + def _ensure_file_budget(self, deadline: float) -> None: + if self._file_budget_expired(deadline): + method = "analyze_file budget" + raise LspTimeoutError(method) + + def _file_budget_expired(self, deadline: float) -> bool: + return deadline - time.monotonic() <= 0 + def _target_ids_from_locations(self, result: object) -> tuple[list[str], bool]: locations = result if isinstance(result, list) else [result] candidate_ids: set[str] = set() @@ -507,7 +571,7 @@ def _target_id_from_location(self, location: object) -> tuple[str | None, bool]: target_path = _path_from_uri(raw_uri) if target_path is None: return None, False - if not target_path.is_relative_to(self.project_root): + if not self._is_internal_project_path(target_path): return None, True target_index = self._function_index_for_path(target_path) key = _range_start_key(raw_range) @@ -620,7 +684,7 @@ def _initialize(self) -> None: "workspaceFolders": [ {"uri": self.project_root.as_uri(), "name": self.project_root.name}, ], - "capabilities": {}, + "capabilities": {"workspace": {"configuration": True}}, "clientInfo": {"name": "clarion-plugin-python", "version": __version__}, }, self.init_timeout_secs, @@ -675,6 +739,9 @@ def _request(self, method: str, params: dict[str, object], timeout_secs: float) ) while True: response = self._read_message(timeout_secs) + if "method" in response: + self._handle_server_message(response) + continue if response.get("id") != request_id: continue if "error" in response: @@ -682,6 +749,40 @@ def _request(self, method: str, params: dict[str, object], timeout_secs: float) process.poll() return response.get("result") + def _handle_server_message(self, message: dict[str, Any]) -> None: + if "id" not in message: + return + request_id = message["id"] + method = message.get("method") + if method == "workspace/configuration": + result = self._workspace_configuration_result(message) + else: + result = None + self._write_message({"jsonrpc": "2.0", "id": request_id, "result": result}) + + def _workspace_configuration_result(self, message: dict[str, Any]) -> list[object]: + params = message.get("params") + items = params.get("items") if isinstance(params, dict) else None + if not isinstance(items, list): + return [] + return [self._configuration_for_section(item) for item in items] + + def _configuration_for_section(self, item: object) -> object: + section = item.get("section") if isinstance(item, dict) else None + analysis = { + "diagnosticMode": "openFilesOnly", + "exclude": PYRIGHT_EXCLUDE_PATTERNS, + "indexing": False, + "useLibraryCodeForTypes": False, + } + if section == "python": + return {"analysis": analysis} + if section == "python.analysis": + return analysis + if section == "pyright": + return {} + return None + def _notify(self, method: str, params: dict[str, object]) -> None: self._live_process() self._write_message({"jsonrpc": "2.0", "method": method, "params": params}) @@ -720,6 +821,9 @@ def _read_message(self, timeout_secs: float) -> dict[str, Any]: raise LspTransportClosedError(message) name, value = line.decode("ascii").strip().split(":", 1) headers[name.lower()] = value.strip() + if "content-length" not in headers: + message = f"missing LSP Content-Length header: {headers!r}" + raise LspTransportClosedError(message) length = int(headers["content-length"]) body = _read_exact(fd, length, deadline) parsed: dict[str, Any] = json.loads(body) @@ -736,7 +840,7 @@ def _target_id_from_call(self, call: dict[object, object]) -> str | None: target_path = _path_from_uri(raw_uri) if target_path is None: return None - if not target_path.is_relative_to(self.project_root): + if not self._is_internal_project_path(target_path): return None index = self._function_index_for_path(target_path) key = _range_start_key(raw_selection) @@ -744,6 +848,12 @@ def _target_id_from_call(self, call: dict[object, object]) -> str | None: return index.by_name_position[key].entity_id return _containing_function_id(index, raw_selection) + def _is_internal_project_path(self, path: Path) -> bool: + if not path.is_relative_to(self.project_root): + return False + relative = path.relative_to(self.project_root) + return not any(part in PROJECT_LOCAL_EXTERNAL_DIRS for part in relative.parts) + def _function_index_for_path(self, path: Path) -> _FunctionIndex: resolved = path.resolve() cached = self._function_indexes.get(resolved) diff --git a/plugins/python/src/clarion_plugin_python/server.py b/plugins/python/src/clarion_plugin_python/server.py index 912d9470..3303d65e 100644 --- a/plugins/python/src/clarion_plugin_python/server.py +++ b/plugins/python/src/clarion_plugin_python/server.py @@ -46,6 +46,7 @@ # default (8 MiB) so the plugin never emits a frame the host would kill us # for. Oversize outbound payloads trip this before reaching the wire. MAX_CONTENT_LENGTH = 8 * 1024 * 1024 +MAX_FILES_PER_PYRIGHT_SESSION = 25 # JSON-RPC 2.0 error codes (§5.1) plus LSP-style server extensions. _ERR_INVALID_REQUEST = -32600 @@ -66,6 +67,7 @@ class ServerState: shutdown_requested: bool = False project_root: Path | None = field(default=None) pyright: PyrightSession | None = field(default=None) + pyright_files_since_restart: int = 0 def read_frame(stream: IO[bytes]) -> dict[str, Any] | None: @@ -210,6 +212,11 @@ def handle_analyze_file(params: dict[str, Any], state: ServerState) -> dict[str, call_resolver=state.pyright, reference_resolver=state.pyright, ) + state.pyright_files_since_restart += 1 + if state.pyright_files_since_restart >= MAX_FILES_PER_PYRIGHT_SESSION: + state.pyright.close() + state.pyright = None + state.pyright_files_since_restart = 0 stats = { "unresolved_call_sites_total": result.stats.unresolved_call_sites_total, "unresolved_call_sites": result.stats.unresolved_call_sites, diff --git a/plugins/python/tests/test_pyright_session.py b/plugins/python/tests/test_pyright_session.py index 4a4cf8df..e328da2a 100644 --- a/plugins/python/tests/test_pyright_session.py +++ b/plugins/python/tests/test_pyright_session.py @@ -304,6 +304,20 @@ def test_pyright_session_reference_unavailable_binary_missing(tmp_path: Path) -> assert FINDING_PYRIGHT_UNAVAILABLE in _finding_codes(result.findings) +def test_pyright_session_treats_project_local_venv_targets_as_external(tmp_path: Path) -> None: + target = tmp_path / ".venv" / "lib" / "python3.12" / "site-packages" / "demo.py" + target.parent.mkdir(parents=True) + target.write_text("def helper():\n pass\n", encoding="utf-8") + location = { + "uri": target.as_uri(), + "range": {"start": {"line": 0, "character": 4}, "end": {"line": 0, "character": 10}}, + } + + session = PyrightSession(tmp_path, executable=sys.executable) + + assert session._target_id_from_location(location) == (None, True) # noqa: SLF001 + + def test_pyright_session_reference_site_cap(tmp_path: Path) -> None: source = "def world():\n pass\n\nCONST_REF = world\n" module = _write_module(tmp_path, source) @@ -354,9 +368,10 @@ def _reference_target_ids( uri: str, site: ReferenceSite, *, + deadline: float, method: str = "textDocument/definition", ) -> tuple[list[str], bool]: - _ = (uri, method) + _ = (uri, deadline, method) self.requested_starts.append(site.source_byte_start) if site.source_byte_start == self.timeout_start: raise LspTimeoutError(method) @@ -380,9 +395,10 @@ def _reference_target_ids( uri: str, site: ReferenceSite, *, + deadline: float, method: str = "textDocument/definition", ) -> tuple[list[str], bool]: - _ = (uri, method) + _ = (uri, deadline, method) self.requested_starts.append(site.source_byte_start) return [self.target_id], False @@ -687,6 +703,29 @@ def _request(self, method: str, params: dict[str, object], timeout_secs: float) return super()._request(method, params, timeout_secs) +class BudgetProbeSession(PyrightSession): + def __init__(self, project_root: Path) -> None: + super().__init__( + project_root, + executable=sys.executable, + call_timeout_secs=10.0, + file_timeout_secs=0.01, + ) + self.request_timeouts: list[float] = [] + + def _ensure_process(self) -> bool: + return True + + def _notify(self, method: str, params: dict[str, object]) -> None: + _ = (method, params) + + def _request(self, method: str, params: dict[str, object], timeout_secs: float) -> object: + _ = (method, params) + self.request_timeouts.append(timeout_secs) + timeout_method = "budget probe" + raise LspTimeoutError(timeout_method) + + @pytest.mark.pyright def test_pyright_session_call_resolution_timeout(tmp_path: Path, pyright_langserver: str) -> None: module = _write_module( @@ -707,6 +746,24 @@ def caller(): assert FINDING_PYRIGHT_CALL_RESOLUTION_TIMEOUT in _finding_codes(result.findings) +def test_pyright_session_caps_per_file_pyright_budget(tmp_path: Path) -> None: + module = _write_module( + tmp_path, + """ + def caller(): + print('x') + """, + ) + + with BudgetProbeSession(tmp_path) as session: + result = session.resolve_calls(module, ["python:function:demo.caller"]) + + assert result.edges == [] + assert session.request_timeouts + assert max(session.request_timeouts) <= 0.01 + assert FINDING_PYRIGHT_CALL_RESOLUTION_TIMEOUT in _finding_codes(result.findings) + + def test_pyright_session_stderr_drain(tmp_path: Path) -> None: script = _write_executable( tmp_path, @@ -764,3 +821,93 @@ def write_frame(message): assert result.edges == [] assert session.stderr_thread_alive is False + + +def test_pyright_session_answers_workspace_configuration_requests(tmp_path: Path) -> None: + marker = tmp_path / "config-marker.txt" + script = _write_executable( + tmp_path, + textwrap.dedent( + """ + #!/usr/bin/env python3 + import json + import os + import sys + from pathlib import Path + + def read_frame(): + headers = {} + while True: + line = sys.stdin.buffer.readline() + if not line: + return None + if line == b"\\r\\n": + break + name, value = line.decode("ascii").strip().split(":", 1) + headers[name.lower()] = value.strip() + return json.loads(sys.stdin.buffer.read(int(headers["content-length"]))) + + def write_frame(message): + body = json.dumps(message).encode("utf-8") + sys.stdout.buffer.write( + b"Content-Length: " + str(len(body)).encode("ascii") + b"\\r\\n\\r\\n" + ) + sys.stdout.buffer.write(body) + sys.stdout.buffer.flush() + + initialize = read_frame() + write_frame( + { + "jsonrpc": "2.0", + "id": 0, + "method": "workspace/configuration", + "params": { + "items": [ + {"section": "python"}, + {"section": "python.analysis"}, + {"section": "pyright"}, + ], + }, + }, + ) + config = read_frame() + result = config.get("result", []) + python = result[0].get("analysis", {}) if len(result) > 0 else {} + analysis = result[1] if len(result) > 1 else {} + ok = ( + python.get("diagnosticMode") == "openFilesOnly" + and python.get("indexing") is False + and "**/.venv/**" in python.get("exclude", []) + and analysis.get("diagnosticMode") == "openFilesOnly" + and analysis.get("indexing") is False + and result[2] == {} + ) + Path(os.environ["CONFIG_MARKER"]).write_text("ok" if ok else repr(config)) + write_frame({"jsonrpc": "2.0", "id": initialize["id"], "result": {}}) + + while True: + frame = read_frame() + if frame is None: + break + method = frame.get("method") + if method == "textDocument/prepareCallHierarchy": + write_frame({"jsonrpc": "2.0", "id": frame["id"], "result": []}) + elif method == "shutdown": + write_frame({"jsonrpc": "2.0", "id": frame["id"], "result": {}}) + elif method == "exit": + break + """, + ).lstrip(), + ) + module = _write_module(tmp_path, "def caller():\n print('x')\n") + + with PyrightSession( + tmp_path, + executable=str(script), + env={"CONFIG_MARKER": str(marker)}, + init_timeout_secs=1.0, + ) as session: + result = session.resolve_calls(module, ["python:function:demo.caller"]) + + assert result.edges == [] + assert marker.read_text(encoding="utf-8") == "ok" diff --git a/plugins/python/tests/test_server.py b/plugins/python/tests/test_server.py index 99661aa9..106c1c02 100644 --- a/plugins/python/tests/test_server.py +++ b/plugins/python/tests/test_server.py @@ -393,6 +393,55 @@ def close(self) -> None: assert any(edge["kind"] == "references" for edge in response["edges"]) +def test_analyze_file_restarts_pyright_after_file_budget( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + sessions: list[Any] = [] + + class FakePyrightSession: + def __init__(self, project_root: Path) -> None: + self.project_root = project_root + self.closed = False + sessions.append(self) + + def resolve_calls( + self, + file_path: str, + function_ids: list[str], + ) -> CallResolutionResult: + _ = (file_path, function_ids) + return CallResolutionResult() + + def resolve_references( + self, + file_path: str, + sites: Sequence[ReferenceSite], + ) -> ReferenceResolutionResult: + _ = (file_path, sites) + return ReferenceResolutionResult() + + def close(self) -> None: + self.closed = True + + monkeypatch.setattr(server_module, "PyrightSession", FakePyrightSession, raising=False) + monkeypatch.setattr(server_module, "MAX_FILES_PER_PYRIGHT_SESSION", 2) + demo = tmp_path / "demo.py" + demo.write_text("def hello():\n pass\n", encoding="utf-8") + state = server_module.ServerState(initialized=True, project_root=tmp_path) + + server_module.handle_analyze_file({"file_path": str(demo)}, state) + assert state.pyright is sessions[0] + first_session = cast("Any", sessions[0]) + assert first_session.closed is False + + server_module.handle_analyze_file({"file_path": str(demo)}, state) + assert first_session.closed is True + assert len(sessions) == 1 + assert state.pyright is None + assert state.pyright_files_since_restart == 0 + + def test_shutdown_closes_pyright_session() -> None: class FakePyrightSession: def __init__(self) -> None: From 54974fe8b271b25d22cf9dd7670ce3bce251979f Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Wed, 20 May 2026 16:23:17 +1000 Subject: [PATCH 047/187] Parallelize pre-ingest secret scan --- crates/clarion-cli/src/secret_scan.rs | 131 ++++++++++++++++++++++++-- 1 file changed, 124 insertions(+), 7 deletions(-) diff --git a/crates/clarion-cli/src/secret_scan.rs b/crates/clarion-cli/src/secret_scan.rs index fc0bb2d8..e41328a6 100644 --- a/crates/clarion-cli/src/secret_scan.rs +++ b/crates/clarion-cli/src/secret_scan.rs @@ -11,7 +11,11 @@ use std::{ fs, io::{self, IsTerminal, Write}, path::{Path, PathBuf}, - sync::Arc, + sync::{ + Arc, Mutex, + atomic::{AtomicUsize, Ordering}, + }, + thread, }; mod anchors; @@ -156,21 +160,20 @@ pub(crate) fn pre_ingest( let mut baseline_matches = Vec::new(); let mut scanned_files = BTreeSet::new(); let mut scanned_sidecars = BTreeSet::new(); + let scans = scan_source_files_parallel(source_files, |buf| scanner.scan_bytes(buf))?; - for file in source_files { - let canonical_file = canonical_or_original(file); + for scan in scans { + let canonical_file = scan.canonical_file; scanned_files.insert(canonical_file.clone()); if files::is_secret_scan_sidecar(&canonical_file) { scanned_sidecars.insert(canonical_file.clone()); } - let buf = fs::read(file).with_context(|| format!("read {}", file.display()))?; - let detections = scanner.scan_bytes(&buf); let baseline_file = project_relative_path(project_root, &canonical_file); let SuppressionResult { allowed, fired_entries, .. - } = baseline.suppress(detections, &baseline_file); + } = baseline.suppress(scan.detections, &baseline_file); if !allowed.is_empty() { all_allowed.extend( allowed @@ -262,6 +265,76 @@ pub(crate) fn pre_ingest( }) } +#[derive(Debug)] +struct SourceFileScan { + canonical_file: PathBuf, + detections: Vec, +} + +fn scan_source_files_parallel( + source_files: &[PathBuf], + scan_bytes: F, +) -> Result> +where + F: Fn(&[u8]) -> Vec + Sync, +{ + if source_files.is_empty() { + return Ok(Vec::new()); + } + + let worker_count = source_files.len().min( + thread::available_parallelism() + .map_or(1, usize::from) + .max(1), + ); + let next_file = AtomicUsize::new(0); + let results = (0..source_files.len()) + .map(|_| Mutex::new(None)) + .collect::>(); + + thread::scope(|scope| { + for _ in 0..worker_count { + let next_file = &next_file; + let results = &results; + let scan_bytes = &scan_bytes; + scope.spawn(move || { + loop { + let idx = next_file.fetch_add(1, Ordering::Relaxed); + let Some(file) = source_files.get(idx) else { + break; + }; + let result = scan_one_source_file(file, scan_bytes); + *results[idx] + .lock() + .expect("parallel secret-scan result lock poisoned") = Some(result); + } + }); + } + }); + + results + .into_iter() + .map(|slot| { + slot.into_inner() + .expect("parallel secret-scan result lock poisoned") + .expect("parallel secret-scan worker did not fill result") + }) + .collect() +} + +fn scan_one_source_file(file: &Path, scan_bytes: &F) -> Result +where + F: Fn(&[u8]) -> Vec + Sync + ?Sized, +{ + let canonical_file = canonical_or_original(file); + let buf = fs::read(file).with_context(|| format!("read {}", file.display()))?; + let detections = scan_bytes(&buf); + Ok(SourceFileScan { + canonical_file, + detections, + }) +} + fn confirm_override_if_needed( project_root: &Path, allowed: &[(PathBuf, Detection)], @@ -347,7 +420,8 @@ fn relative_path(root: &Path, path: &Path) -> Option { #[cfg(test)] mod tests { - use super::display_relative; + use super::{display_relative, scan_source_files_parallel}; + use std::sync::{Arc, Mutex}; #[cfg(unix)] #[test] @@ -363,4 +437,47 @@ mod tests { assert_eq!(display_relative(&link_root, &canonical_file), ".env"); } + + #[test] + fn scan_source_files_parallel_scans_on_workers_and_preserves_input_order() { + let tmp = tempfile::tempdir().expect("tempdir"); + let files = (0..8) + .map(|idx| { + let path = tmp.path().join(format!("{idx}.txt")); + std::fs::write(&path, format!("file-{idx}\n")).expect("write scan file"); + path + }) + .collect::>(); + let caller_thread = std::thread::current().id(); + let scan_threads = Arc::new(Mutex::new(Vec::new())); + let seen = Arc::clone(&scan_threads); + + let scans = scan_source_files_parallel(&files, |buf| { + seen.lock() + .expect("record thread") + .push(std::thread::current().id()); + assert!(buf.starts_with(b"file-")); + Vec::new() + }) + .expect("parallel scan"); + + assert_eq!( + scans + .iter() + .map(|scan| scan.canonical_file.clone()) + .collect::>(), + files + .iter() + .map(|file| file.canonicalize().expect("canonical test file")) + .collect::>() + ); + assert!( + scan_threads + .lock() + .expect("scan threads") + .iter() + .any(|thread_id| *thread_id != caller_thread), + "expected at least one scan to run on a worker thread" + ); + } } From 170b398b1db6ad6542a2ed2331e7b147ed42eeb0 Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Wed, 20 May 2026 16:32:35 +1000 Subject: [PATCH 048/187] Tighten secret scan type design --- crates/clarion-cli/src/analyze.rs | 2 +- crates/clarion-cli/src/secret_scan.rs | 127 +++++++++++++++--- crates/clarion-cli/src/secret_scan/anchors.rs | 2 +- .../clarion-cli/src/secret_scan/baseline.rs | 5 +- .../clarion-cli/src/secret_scan/findings.rs | 62 +++++---- crates/clarion-scanner/src/lib.rs | 20 +++ crates/clarion-scanner/src/patterns.rs | 20 +-- crates/clarion-scanner/tests/scanner.rs | 12 +- 8 files changed, 176 insertions(+), 74 deletions(-) diff --git a/crates/clarion-cli/src/analyze.rs b/crates/clarion-cli/src/analyze.rs index 1a48939b..5656c791 100644 --- a/crates/clarion-cli/src/analyze.rs +++ b/crates/clarion-cli/src/analyze.rs @@ -246,7 +246,7 @@ pub(crate) async fn run_with_options(project_path: PathBuf, options: AnalyzeOpti let mut run_outcome: RunOutcome = RunOutcome::Completed; let mut breaker = CrashLoopBreaker::default(); let mut crash_reasons: Vec = Vec::new(); - let briefing_blocks = Arc::clone(&secret_scan_outcome.briefing_blocks); + let briefing_blocks = secret_scan_outcome.briefing_blocks_shared(); let scanned_files = secret_scan_outcome.scanned_files_shared(); 'plugins: for plugin in plugins { let plugin_id = plugin.manifest.plugin.plugin_id.clone(); diff --git a/crates/clarion-cli/src/secret_scan.rs b/crates/clarion-cli/src/secret_scan.rs index e41328a6..41042a2a 100644 --- a/crates/clarion-cli/src/secret_scan.rs +++ b/crates/clarion-cli/src/secret_scan.rs @@ -29,7 +29,7 @@ use clarion_scanner::{Detection, Scanner, SuppressionResult}; use clarion_storage::{Writer, commands::EntityRecord}; pub(crate) use files::collect_scan_files; use findings::{ - FindingConfidenceBasis, FindingKind, FindingSeverity, PendingFinding, secret_detected_finding, + FindingConfidence, FindingKind, FindingSeverity, PendingFinding, secret_detected_finding, }; use serde_json::json; @@ -86,10 +86,9 @@ impl std::fmt::Display for OverrideConfirmationError { #[derive(Debug, Clone, Default)] pub(crate) struct SecretScanOutcome { - pub(crate) briefing_blocks: Arc>, + per_file_outcomes: Vec, findings: Vec, finding_anchors: BTreeMap, - override_files: Vec, scanned_files: Arc>, // Subset of `scanned_files` restricted to paths matched by // `files::is_secret_scan_sidecar` (e.g. `.env`, `.env.*`, `*.env`). @@ -100,7 +99,55 @@ pub(crate) struct SecretScanOutcome { scanned_sidecars: BTreeSet, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum PerFileOutcome { + Clean { + file: PathBuf, + }, + Blocked { + file: PathBuf, + reason: BriefingBlockReason, + findings: Vec, + }, + Overridden { + file: PathBuf, + findings: Vec, + }, +} + impl SecretScanOutcome { + #[cfg(test)] + pub(crate) fn per_file_outcomes(&self) -> &[PerFileOutcome] { + &self.per_file_outcomes + } + + pub(crate) fn briefing_blocks_shared(&self) -> Arc> { + Arc::new( + self.per_file_outcomes + .iter() + .filter_map(|outcome| match outcome { + PerFileOutcome::Blocked { file, reason, .. } => Some((file.clone(), *reason)), + PerFileOutcome::Clean { .. } | PerFileOutcome::Overridden { .. } => None, + }) + .collect(), + ) + } + + pub(crate) fn briefing_block_for(&self, file: &Path) -> Option { + self.per_file_outcomes + .iter() + .find_map(|outcome| match outcome { + PerFileOutcome::Blocked { + file: blocked_file, + reason, + .. + } if blocked_file == file => Some(*reason), + PerFileOutcome::Clean { .. } + | PerFileOutcome::Blocked { .. } + | PerFileOutcome::Overridden { .. } => None, + }) + } + pub(crate) fn scanned_files_shared(&self) -> Arc> { Arc::clone(&self.scanned_files) } @@ -117,12 +164,20 @@ impl SecretScanOutcome { } pub(crate) fn augment_stats(&self, stats: &mut serde_json::Value) { - if self.override_files.is_empty() { + let override_files = self + .per_file_outcomes + .iter() + .filter_map(|outcome| match outcome { + PerFileOutcome::Overridden { file, .. } => Some(file), + PerFileOutcome::Clean { .. } | PerFileOutcome::Blocked { .. } => None, + }) + .collect::>(); + if override_files.is_empty() { return; } stats["secret_override_used"] = json!(true); stats["secret_override_files_affected"] = json!( - self.override_files + override_files .iter() .map(|path| path.display().to_string()) .collect::>() @@ -187,8 +242,7 @@ pub(crate) fn pre_ingest( rule_id: baseline::baseline_match_rule_id(), kind: FindingKind::Fact, severity: FindingSeverity::Info, - confidence: Some(1.0), - confidence_basis: Some(FindingConfidenceBasis::Baseline), + confidence: FindingConfidence::Baseline, message: format!( "Secret baseline entry matched {}:{}", entry.file_path.display(), @@ -204,13 +258,13 @@ pub(crate) fn pre_ingest( } let override_confirmed = confirm_override_if_needed(project_root, &all_allowed, options); - let mut briefing_blocks = BTreeMap::new(); - let mut override_files = BTreeSet::new(); + let mut per_file_outcomes = Vec::new(); let mut override_detections = BTreeMap::>::new(); findings.extend(baseline_matches); for (file, allowed) in per_file { if allowed.is_empty() { + per_file_outcomes.push(PerFileOutcome::Clean { file }); continue; } // ADR-013 §"Override — --allow-unredacted-secrets": each detection @@ -227,22 +281,28 @@ pub(crate) fn pre_ingest( .map(|detection| secret_detected_finding(&file, detection)), ); if override_confirmed { - override_files.insert(file.clone()); - override_detections.insert(file, allowed); + override_detections.insert(file.clone(), allowed.clone()); + per_file_outcomes.push(PerFileOutcome::Overridden { + file, + findings: allowed, + }); } else { - briefing_blocks.insert(file, BriefingBlockReason::SecretPresent); + per_file_outcomes.push(PerFileOutcome::Blocked { + file, + reason: BriefingBlockReason::SecretPresent, + findings: allowed, + }); } } - for file in &override_files { + for file in override_detections.keys() { let detections = override_detections.get(file).map_or(&[][..], Vec::as_slice); findings.push(PendingFinding { file_path: file.clone(), rule_id: SECRET_OVERRIDE_ALLOWED, kind: FindingKind::Defect, severity: FindingSeverity::Error, - confidence: Some(1.0), - confidence_basis: Some(FindingConfidenceBasis::OperatorOverride), + confidence: FindingConfidence::OperatorOverride, message: format!( "Operator allowed unredacted secrets in {}", display_relative(project_root, file) @@ -256,10 +316,9 @@ pub(crate) fn pre_ingest( } Ok(SecretScanOutcome { - briefing_blocks: Arc::new(briefing_blocks), + per_file_outcomes, findings, finding_anchors: BTreeMap::new(), - override_files: override_files.into_iter().collect(), scanned_files: Arc::new(scanned_files), scanned_sidecars, }) @@ -420,7 +479,10 @@ fn relative_path(root: &Path, path: &Path) -> Option { #[cfg(test)] mod tests { - use super::{display_relative, scan_source_files_parallel}; + use super::{ + ConfirmToken, OverridePolicy, PerFileOutcome, SecretScanOptions, display_relative, + pre_ingest, scan_source_files_parallel, + }; use std::sync::{Arc, Mutex}; #[cfg(unix)] @@ -480,4 +542,33 @@ mod tests { "expected at least one scan to run on a worker thread" ); } + + #[test] + fn pre_ingest_records_file_outcomes_without_side_channel_state() { + let tmp = tempfile::tempdir().expect("tempdir"); + let clean = tmp.path().join("clean.txt"); + let secret = tmp.path().join("secret.txt"); + std::fs::write(&clean, b"hello\n").expect("write clean"); + std::fs::write(&secret, b"key = 'AKIAIOSFODNN7EXAMPLE'\n").expect("write secret"); + + let outcome = pre_ingest( + tmp.path(), + &[clean.clone(), secret.clone()], + &SecretScanOptions { + override_policy: OverridePolicy::Preconfirmed(ConfirmToken), + }, + ) + .expect("pre-ingest"); + + assert!(matches!( + outcome.per_file_outcomes()[0], + PerFileOutcome::Clean { .. } + )); + let PerFileOutcome::Overridden { file, findings } = &outcome.per_file_outcomes()[1] else { + panic!("secret file should be recorded as overridden"); + }; + assert_eq!(file, &secret.canonicalize().expect("canonical secret")); + assert_eq!(findings.len(), 1); + assert!(outcome.briefing_blocks_shared().is_empty()); + } } diff --git a/crates/clarion-cli/src/secret_scan/anchors.rs b/crates/clarion-cli/src/secret_scan/anchors.rs index 5cb9a69a..c83fb7a1 100644 --- a/crates/clarion-cli/src/secret_scan/anchors.rs +++ b/crates/clarion-cli/src/secret_scan/anchors.rs @@ -98,7 +98,7 @@ async fn upsert_finding_anchor( .to_owned(); let mut properties = serde_json::Map::new(); properties.insert("finding_anchor".to_owned(), serde_json::json!(true)); - if let Some(reason) = outcome.briefing_blocks.get(&key) { + if let Some(reason) = outcome.briefing_block_for(&key) { properties.insert( "briefing_blocked".to_owned(), serde_json::Value::String(reason.as_str().to_owned()), diff --git a/crates/clarion-cli/src/secret_scan/baseline.rs b/crates/clarion-cli/src/secret_scan/baseline.rs index 3583882d..268038a8 100644 --- a/crates/clarion-cli/src/secret_scan/baseline.rs +++ b/crates/clarion-cli/src/secret_scan/baseline.rs @@ -6,7 +6,7 @@ use serde_json::json; use super::normalize_project_path; use crate::secret_scan::findings::{ - FindingConfidenceBasis, FindingKind, FindingSeverity, PendingFinding, + FindingConfidence, FindingKind, FindingSeverity, PendingFinding, }; const BASELINE_NO_JUSTIFICATION: &str = "CLA-INFRA-SECRET-BASELINE-NO-JUSTIFICATION"; @@ -25,8 +25,7 @@ pub(super) fn load_for_scan(project_root: &Path) -> Result<(Baseline, Vec, - pub(super) confidence_basis: Option, + pub(super) confidence: FindingConfidence, pub(super) message: String, pub(super) evidence: serde_json::Value, } @@ -72,23 +71,34 @@ impl FindingSeverity { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(super) enum FindingConfidenceBasis { - Pattern, - Entropy, +#[allow(dead_code)] +#[derive(Debug, Clone, Copy, PartialEq)] +pub(super) enum FindingConfidence { + ScoredPattern(f64), + ScoredEntropy(f64), Baseline, - BaselineSchema, OperatorOverride, + Schema, + Unknown, } -impl FindingConfidenceBasis { - fn as_str(self) -> &'static str { +impl FindingConfidence { + fn value(self) -> Option { match self { - Self::Pattern => "pattern", - Self::Entropy => "entropy", - Self::Baseline => "baseline", - Self::BaselineSchema => "baseline_schema", - Self::OperatorOverride => "operator_override", + Self::ScoredPattern(value) | Self::ScoredEntropy(value) => Some(value), + Self::Baseline | Self::OperatorOverride | Self::Schema => Some(1.0), + Self::Unknown => None, + } + } + + fn basis(self) -> Option<&'static str> { + match self { + Self::ScoredPattern(_) => Some("pattern"), + Self::ScoredEntropy(_) => Some("entropy"), + Self::Baseline => Some("baseline"), + Self::OperatorOverride => Some("operator_override"), + Self::Schema => Some("baseline_schema"), + Self::Unknown => None, } } } @@ -116,11 +126,8 @@ pub(crate) async fn emit_findings( rule_id: pending.rule_id.to_owned(), kind: pending.kind.as_str().to_owned(), severity: pending.severity.as_str().to_owned(), - confidence: pending.confidence, - confidence_basis: pending - .confidence_basis - .map(FindingConfidenceBasis::as_str) - .map(str::to_owned), + confidence: pending.confidence.value(), + confidence_basis: pending.confidence.basis().map(str::to_owned), entity_id, related_entities_json: "[]".to_owned(), message: pending.message.clone(), @@ -147,14 +154,9 @@ pub(super) fn secret_detected_finding(file: &Path, detection: &Detection) -> Pen kind: FindingKind::Defect, severity: FindingSeverity::Error, confidence: if detection.category == SecretCategory::HighEntropy { - Some(0.6) - } else { - Some(1.0) - }, - confidence_basis: if detection.category == SecretCategory::HighEntropy { - Some(FindingConfidenceBasis::Entropy) + FindingConfidence::ScoredEntropy(0.6) } else { - Some(FindingConfidenceBasis::Pattern) + FindingConfidence::ScoredPattern(1.0) }, message: format!( "{} detected in {}:{}", @@ -177,7 +179,7 @@ fn finding_entity_id(file_path: &Path, anchors: &BTreeMap) -> O #[cfg(test)] mod tests { - use super::{FindingConfidenceBasis, finding_entity_id, secret_detected_finding}; + use super::{FindingConfidence, finding_entity_id, secret_detected_finding}; use clarion_scanner::{DetectSecretsRule, Detection, HashedSecret, SecretCategory}; use std::{collections::BTreeMap, path::PathBuf}; @@ -209,10 +211,6 @@ mod tests { let finding = secret_detected_finding(PathBuf::from("demo.sec").as_path(), &detection); - assert_eq!(finding.confidence, Some(1.0)); - assert_eq!( - finding.confidence_basis, - Some(FindingConfidenceBasis::Pattern) - ); + assert_eq!(finding.confidence, FindingConfidence::ScoredPattern(1.0)); } } diff --git a/crates/clarion-scanner/src/lib.rs b/crates/clarion-scanner/src/lib.rs index 7e2b2bbc..ae87edb8 100644 --- a/crates/clarion-scanner/src/lib.rs +++ b/crates/clarion-scanner/src/lib.rs @@ -137,6 +137,26 @@ impl DetectSecretsRule { Self::HexHighEntropyString => "Hex High Entropy String", } } + + #[must_use] + pub fn rule_id(self) -> &'static str { + match self { + Self::AwsAccessKey => "AwsAccessKeyId", + Self::AwsSecretAccessKey => "AwsSecretAccessKey", + Self::GitHubToken => "GitHubPat", + Self::GitHubFineGrainedToken => "GitHubFineGrainedPat", + Self::GitHubOAuthToken => "GitHubOAuth", + Self::AnthropicApiKey => "AnthropicApiKey", + Self::OpenAiApiKey => "OpenAiApiKey", + Self::StripeApiKey => "StripeApiKey", + Self::SlackToken => "SlackToken", + Self::JwtToken => "JwtToken", + Self::PrivateKey => "PrivateKeyHeader", + Self::KeywordDetector => "ContextualCredential", + Self::Base64HighEntropyString => "HighEntropyBase64", + Self::HexHighEntropyString => "HighEntropyHex", + } + } } impl fmt::Display for DetectSecretsRule { diff --git a/crates/clarion-scanner/src/patterns.rs b/crates/clarion-scanner/src/patterns.rs index 31dc1786..19209d5e 100644 --- a/crates/clarion-scanner/src/patterns.rs +++ b/crates/clarion-scanner/src/patterns.rs @@ -8,7 +8,6 @@ use crate::{ /// Metadata for one named secret detector. #[derive(Debug, Clone)] pub struct PatternMeta { - pub rule_id: &'static str, pub detect_secrets_type: DetectSecretsRule, pub category: SecretCategory, pub pattern: &'static str, @@ -139,7 +138,6 @@ impl Scanner { && self.entropy_b64.accepts(candidate_bytes) { detections.push(entropy_detection( - "HighEntropyBase64", DetectSecretsRule::Base64HighEntropyString, bytes, candidate.start(), @@ -153,7 +151,6 @@ impl Scanner { && self.entropy_hex.accepts(candidate_bytes) { detections.push(entropy_detection( - "HighEntropyHex", DetectSecretsRule::HexHighEntropyString, bytes, candidate.start(), @@ -167,7 +164,7 @@ impl Scanner { fn detection_from_match(meta: &PatternMeta, bytes: &[u8], start: usize, end: usize) -> Detection { let matched = &bytes[start..end]; Detection { - rule_id: meta.rule_id, + rule_id: meta.detect_secrets_type.rule_id(), detect_secrets_type: meta.detect_secrets_type, category: meta.category, byte_offset: start, @@ -178,14 +175,13 @@ fn detection_from_match(meta: &PatternMeta, bytes: &[u8], start: usize, end: usi } fn entropy_detection( - rule_id: &'static str, detect_secrets_type: DetectSecretsRule, bytes: &[u8], start: usize, end: usize, ) -> Detection { Detection { - rule_id, + rule_id: detect_secrets_type.rule_id(), detect_secrets_type, category: SecretCategory::HighEntropy, byte_offset: start, @@ -198,84 +194,72 @@ fn entropy_detection( fn default_pattern_meta() -> Vec { vec![ PatternMeta { - rule_id: "AwsAccessKeyId", detect_secrets_type: DetectSecretsRule::AwsAccessKey, category: SecretCategory::CloudCredential, pattern: r"\b(?:AKIA|ASIA)[0-9A-Z]{16}\b", capture_group: None, }, PatternMeta { - rule_id: "AwsSecretAccessKey", detect_secrets_type: DetectSecretsRule::AwsSecretAccessKey, category: SecretCategory::CloudCredential, pattern: r#"(?i)\baws[^:=\n]{0,32}(?:secret|access)[^:=\n]{0,32}(?:=|:|:=)\s*["']?([A-Za-z0-9/+=]{40})["']?"#, capture_group: Some(1), }, PatternMeta { - rule_id: "GitHubPat", detect_secrets_type: DetectSecretsRule::GitHubToken, category: SecretCategory::VcsCredential, pattern: r"\bghp_[A-Za-z0-9]{36}\b", capture_group: None, }, PatternMeta { - rule_id: "GitHubFineGrainedPat", detect_secrets_type: DetectSecretsRule::GitHubFineGrainedToken, category: SecretCategory::VcsCredential, pattern: r"\bgithub_pat_[A-Za-z0-9_]{82,}\b", capture_group: None, }, PatternMeta { - rule_id: "GitHubOAuth", detect_secrets_type: DetectSecretsRule::GitHubOAuthToken, category: SecretCategory::VcsCredential, pattern: r"\bgh[ousr]_[A-Za-z0-9]{36}\b", capture_group: None, }, PatternMeta { - rule_id: "AnthropicApiKey", detect_secrets_type: DetectSecretsRule::AnthropicApiKey, category: SecretCategory::AiProviderCredential, pattern: r"\bsk-ant-[A-Za-z0-9_-]{90,}\b", capture_group: None, }, PatternMeta { - rule_id: "OpenAiApiKey", detect_secrets_type: DetectSecretsRule::OpenAiApiKey, category: SecretCategory::AiProviderCredential, pattern: r"\bsk-[A-Za-z0-9]{48}\b", capture_group: None, }, PatternMeta { - rule_id: "StripeApiKey", detect_secrets_type: DetectSecretsRule::StripeApiKey, category: SecretCategory::PaymentsCredential, pattern: r"\b(?:sk|pk|rk)_(?:live|test)_[A-Za-z0-9]{16,}\b", capture_group: None, }, PatternMeta { - rule_id: "SlackToken", detect_secrets_type: DetectSecretsRule::SlackToken, category: SecretCategory::MessagingCredential, pattern: r"\bxox[baprs]-[A-Za-z0-9-]{10,}\b", capture_group: None, }, PatternMeta { - rule_id: "JwtToken", detect_secrets_type: DetectSecretsRule::JwtToken, category: SecretCategory::JwtToken, pattern: r"\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b", capture_group: None, }, PatternMeta { - rule_id: "PrivateKeyHeader", detect_secrets_type: DetectSecretsRule::PrivateKey, category: SecretCategory::PrivateKey, pattern: r"-----BEGIN (?:(?:RSA|EC|DSA|OPENSSH|ENCRYPTED) PRIVATE KEY|PRIVATE KEY|PGP PRIVATE KEY BLOCK)-----", capture_group: None, }, PatternMeta { - rule_id: "ContextualCredential", detect_secrets_type: DetectSecretsRule::KeywordDetector, category: SecretCategory::ContextualCredential, pattern: r#"(?i)(?:^|[^A-Za-z0-9_-])(?:password|passwd|secret[_-]?token|secret|token|api[_-]?key)\s*(?:=|:=|:)\s*["']([^"'\s]{8,})["']"#, diff --git a/crates/clarion-scanner/tests/scanner.rs b/crates/clarion-scanner/tests/scanner.rs index 72313e5b..198b601e 100644 --- a/crates/clarion-scanner/tests/scanner.rs +++ b/crates/clarion-scanner/tests/scanner.rs @@ -1,5 +1,5 @@ use clarion_scanner::{ - Baseline, BaselineError, EntropyTuning, HashedSecret, Scanner, load_baseline, + Baseline, BaselineError, DetectSecretsRule, EntropyTuning, HashedSecret, Scanner, load_baseline, }; use sha1::{Digest, Sha1}; @@ -24,6 +24,16 @@ fn assert_not_detects(input: &str, rule_id: &str) { ); } +#[test] +fn detect_secrets_rule_owns_rule_id_strings() { + assert_eq!(DetectSecretsRule::AwsAccessKey.rule_id(), "AwsAccessKeyId"); + assert_eq!(DetectSecretsRule::OpenAiApiKey.rule_id(), "OpenAiApiKey"); + assert_eq!( + DetectSecretsRule::Base64HighEntropyString.rule_id(), + "HighEntropyBase64" + ); +} + #[test] fn named_patterns_detect_expected_credentials() { assert_detects( From f0a19ac3602da8829140c1f0851faabfa9bfcd8a Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Wed, 20 May 2026 16:37:03 +1000 Subject: [PATCH 049/187] Add files resolve batch endpoint --- crates/clarion-cli/src/http_read.rs | 152 ++++++++++++++++++ crates/clarion-cli/tests/serve.rs | 137 +++++++++++++++- .../adr/ADR-014-filigree-registry-backend.md | 18 +++ docs/federation/contracts.md | 73 +++++++++ .../post-api-v1-files-resolve.batch.json | 110 +++++++++++++ 5 files changed, 486 insertions(+), 4 deletions(-) create mode 100644 docs/federation/fixtures/post-api-v1-files-resolve.batch.json diff --git a/crates/clarion-cli/src/http_read.rs b/crates/clarion-cli/src/http_read.rs index 08cdf3d9..88bac526 100644 --- a/crates/clarion-cli/src/http_read.rs +++ b/crates/clarion-cli/src/http_read.rs @@ -347,6 +347,7 @@ fn build_http_runtime() -> Result { fn router(state: AppState) -> Router { let protected = Router::new() .route("/api/v1/files", get(get_file)) + .route("/api/v1/files:resolve", post(post_files_resolve)) .route("/api/v1/files/batch", post(post_files_batch)) .route_layer(axum::middleware::from_fn_with_state( state.clone(), @@ -583,6 +584,7 @@ enum ErrorCode { /// constant so the contract docs, the validator, and tests all point at /// the same number. const BATCH_MAX_QUERIES: usize = 256; +const RESOLVE_MAX_PATHS: usize = 1000; const HTTP_BODY_LIMIT_BYTES: usize = 16 * 1024; #[derive(Debug, Deserialize)] @@ -624,6 +626,47 @@ struct BatchFileResponse { errors: Vec, } +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct ResolveFileQuery { + #[serde(default)] + path: String, + #[serde(default)] + language: String, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct ResolveFilesRequest { + paths: Vec, +} + +#[derive(Debug, Serialize)] +struct ResolveFilesResponse { + results: Vec, +} + +#[derive(Debug, Serialize)] +struct ResolveFileResult { + path: String, + response: ResolveFileItemResponse, +} + +#[derive(Debug, Serialize)] +struct ResolveFileItemResponse { + status: ResolveFileStatus, + body: serde_json::Value, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "snake_case")] +enum ResolveFileStatus { + Resolved, + NotFound, + Blocked, + Error, +} + async fn get_file( State(state): State, headers: HeaderMap, @@ -841,6 +884,115 @@ async fn post_files_batch( } } +async fn post_files_resolve( + State(state): State, + body: Result, axum::extract::rejection::JsonRejection>, +) -> Response { + let Ok(Json(request)) = body else { + return json_error( + StatusCode::BAD_REQUEST, + ErrorCode::InvalidPath, + "request body must be a JSON object {\"paths\": [...]}", + ); + }; + if request.paths.len() > RESOLVE_MAX_PATHS { + return json_error( + StatusCode::BAD_REQUEST, + ErrorCode::InvalidPath, + "paths[] exceeds the per-batch maximum of 1000 entries", + ); + } + let project_root = state.project_root.clone(); + let paths = request.paths; + let catalog_result = state + .readers + .with_reader(move |conn| { + let results = paths + .into_iter() + .map(|query| { + let response = + resolve_file_query_item(conn, &project_root, &query.path, &query.language); + ResolveFileResult { + path: query.path, + response, + } + }) + .collect(); + Ok::<_, StorageError>(ResolveFilesResponse { results }) + }) + .await; + match catalog_result { + Ok(response) => (StatusCode::OK, Json(response)).into_response(), + Err(err) => json_read_error(&err), + } +} + +fn resolve_file_query_item( + conn: &rusqlite::Connection, + project_root: &std::path::Path, + path: &str, + language: &str, +) -> ResolveFileItemResponse { + if path.trim().is_empty() { + return resolve_error_response( + ResolveFileStatus::Error, + ErrorCode::InvalidPath, + "path must not be blank", + ); + } + match resolve_file_catalog_entry(conn, project_root, path, language) { + Ok(Some(entry)) => match entry.into_resolved_file() { + Ok(file) => { + if file.briefing_blocked.is_some() { + resolve_error_response( + ResolveFileStatus::Blocked, + ErrorCode::BriefingBlocked, + "entity is briefing-blocked and cannot be exposed", + ) + } else { + ResolveFileItemResponse { + status: ResolveFileStatus::Resolved, + body: serde_json::to_value(FileResponse { + entity_id: file.entity_id, + content_hash: file.content_hash, + canonical_path: file.canonical_path, + language: file.language, + }) + .expect("FileResponse serializes"), + } + } + } + Err(err) => resolve_read_error_response(&err), + }, + Ok(None) => resolve_error_response( + ResolveFileStatus::NotFound, + ErrorCode::NotFound, + "file is not known to Clarion", + ), + Err(err) => resolve_read_error_response(&err), + } +} + +fn resolve_read_error_response(err: &StorageError) -> ResolveFileItemResponse { + let error = classify_read_error(err); + resolve_error_response(ResolveFileStatus::Error, error.code, error.message) +} + +fn resolve_error_response( + status: ResolveFileStatus, + code: ErrorCode, + message: &str, +) -> ResolveFileItemResponse { + ResolveFileItemResponse { + status, + body: serde_json::to_value(ErrorResponse { + error: message.to_owned(), + code, + }) + .expect("ErrorResponse serializes"), + } +} + fn classify_batch_error(requested_path: String, err: &StorageError) -> BatchErrorItem { let classified = classify_read_error(err); BatchErrorItem { diff --git a/crates/clarion-cli/tests/serve.rs b/crates/clarion-cli/tests/serve.rs index 3e6e137a..78168d28 100644 --- a/crates/clarion-cli/tests/serve.rs +++ b/crates/clarion-cli/tests/serve.rs @@ -122,6 +122,10 @@ fn serve_http_responses_match_federation_fixture_contracts() { "get-api-v1-capabilities.json", include_str!("../../../docs/federation/fixtures/get-api-v1-capabilities.json"), ); + let files_resolve_fixture = load_contract_fixture( + "post-api-v1-files-resolve.batch.json", + include_str!("../../../docs/federation/fixtures/post-api-v1-files-resolve.batch.json"), + ); let dir = tempfile::tempdir().expect("temp project"); clarion_bin() .args(["install", "--path"]) @@ -142,6 +146,11 @@ fn serve_http_responses_match_federation_fixture_contracts() { let mut child = spawn_serve(dir.path()); validate_fixture_examples(&bind, &files_fixture, "get-api-v1-files.demo-python.json"); + validate_fixture_examples( + &bind, + &files_resolve_fixture, + "post-api-v1-files-resolve.batch.json", + ); validate_fixture_examples(&bind, &capabilities_fixture, "get-api-v1-capabilities.json"); stop_serve(&mut child); } @@ -802,6 +811,107 @@ fn serve_http_batch_endpoint_resolves_mixed_paths() { assert_eq!(blank["code"], "INVALID_PATH"); } +#[test] +fn serve_http_files_resolve_endpoint_returns_per_path_results() { + let dir = tempfile::tempdir().expect("temp project"); + clarion_bin() + .args(["install", "--path"]) + .arg(dir.path()) + .env("PATH", "") + .assert() + .success(); + seed_file_entity(dir.path()); + seed_briefing_blocked_file_entity(dir.path()); + let bind = free_loopback_bind(); + write_http_config(dir.path(), &bind); + + let mut child = spawn_serve(dir.path()); + let body = serde_json::json!({ + "paths": [ + {"path": "demo.py", "language": "python"}, + {"path": "missing.py"}, + {"path": "blocked.py", "language": "python"}, + {"path": "../escapes.py", "language": "python"}, + {"path": " "} + ] + }) + .to_string(); + let response = wait_for_http_post_json(&bind, "/api/v1/files:resolve", &body, &[]); + stop_serve(&mut child); + let response = response.expect("files:resolve response"); + + assert_eq!(response.status_code, 200); + let results = response.body["results"].as_array().expect("results array"); + assert_eq!(results.len(), 5, "{response:?}"); + + assert_eq!(results[0]["path"], "demo.py"); + assert_eq!(results[0]["response"]["status"], "resolved"); + assert_eq!( + results[0]["response"]["body"]["entity_id"], + "core:file:demo.py" + ); + assert_eq!( + results[0]["response"]["body"]["content_hash"], + "hash-demo-file" + ); + assert_eq!(results[0]["response"]["body"]["canonical_path"], "demo.py"); + assert_eq!(results[0]["response"]["body"]["language"], "python"); + + assert_eq!(results[1]["path"], "missing.py"); + assert_eq!(results[1]["response"]["status"], "not_found"); + assert_eq!(results[1]["response"]["body"]["code"], "NOT_FOUND"); + + assert_eq!(results[2]["path"], "blocked.py"); + assert_eq!(results[2]["response"]["status"], "blocked"); + assert_eq!(results[2]["response"]["body"]["code"], "BRIEFING_BLOCKED"); + + assert_eq!(results[3]["path"], "../escapes.py"); + assert_eq!(results[3]["response"]["status"], "error"); + assert_eq!( + results[3]["response"]["body"]["code"], + "PATH_OUTSIDE_PROJECT" + ); + + assert_eq!(results[4]["path"], " "); + assert_eq!(results[4]["response"]["status"], "error"); + assert_eq!(results[4]["response"]["body"]["code"], "INVALID_PATH"); +} + +#[test] +fn serve_http_files_resolve_rejects_over_1000_paths() { + let dir = tempfile::tempdir().expect("temp project"); + clarion_bin() + .args(["install", "--path"]) + .arg(dir.path()) + .env("PATH", "") + .assert() + .success(); + let bind = free_loopback_bind(); + write_http_config(dir.path(), &bind); + + let paths: Vec = (0..1001).map(|_| serde_json::json!({"path": ""})).collect(); + let body = serde_json::json!({"paths": paths}).to_string(); + assert!( + body.len() <= 16 * 1024, + "test body should fit under the transport cap: {} bytes", + body.len() + ); + + let mut child = spawn_serve(dir.path()); + let response = wait_for_http_post_json(&bind, "/api/v1/files:resolve", &body, &[]); + stop_serve(&mut child); + let response = response.expect("files:resolve over-limit response"); + + assert_eq!(response.status_code, 400); + assert_eq!(response.body["code"], "INVALID_PATH"); + assert!( + response.body["error"] + .as_str() + .is_some_and(|msg| msg.contains("1000")), + "error message should cite the 1000-path ceiling: {response:?}" + ); +} + #[test] fn serve_http_batch_rejects_over_256_queries() { let dir = tempfile::tempdir().expect("temp project"); @@ -1648,7 +1758,6 @@ fn validate_fixture_examples(bind: &str, fixture: &Value, fixture_name: &str) { .pointer("/request/method") .and_then(Value::as_str) .unwrap_or_else(|| panic!("{fixture_name}:{example_name} missing request.method")); - assert_eq!(method, "GET", "{fixture_name}:{example_name} method"); let path = example .pointer("/request/path") .and_then(Value::as_str) @@ -1665,9 +1774,23 @@ fn validate_fixture_examples(bind: &str, fixture: &Value, fixture_name: &str) { .and_then(Value::as_str) .unwrap_or_else(|| panic!("{fixture_name}:{example_name} missing response.shape")); - let response = wait_for_http_response(bind, path).unwrap_or_else(|err| { - panic!("{fixture_name}:{example_name} HTTP request failed: {err}") - }); + let response = match method { + "GET" => wait_for_http_response(bind, path).unwrap_or_else(|err| { + panic!("{fixture_name}:{example_name} HTTP request failed: {err}") + }), + "POST" => { + let body = example + .pointer("/request/body") + .unwrap_or_else(|| { + panic!("{fixture_name}:{example_name} missing request.body") + }) + .to_string(); + wait_for_http_post_json(bind, path, &body, &[]).unwrap_or_else(|err| { + panic!("{fixture_name}:{example_name} HTTP POST request failed: {err}") + }) + } + other => panic!("{fixture_name}:{example_name} unsupported method {other}"), + }; assert_eq!( u64::from(response.status_code), @@ -1826,6 +1949,12 @@ fn assert_value_matches_decl( "{fixture_name}:{example_name} field {field} is not an integer" ); } + "array" => { + assert!( + value.as_array().is_some(), + "{fixture_name}:{example_name} field {field} is not an array" + ); + } "non_empty_string" => { let value = value.as_str().unwrap_or_else(|| { panic!("{fixture_name}:{example_name} field {field} is not a string") diff --git a/docs/clarion/adr/ADR-014-filigree-registry-backend.md b/docs/clarion/adr/ADR-014-filigree-registry-backend.md index 686fee13..03e1b4cd 100644 --- a/docs/clarion/adr/ADR-014-filigree-registry-backend.md +++ b/docs/clarion/adr/ADR-014-filigree-registry-backend.md @@ -67,6 +67,24 @@ Clarion must not synthesize a `core:file:{content_hash}@{canonical_path}` identity. That pattern violates ADR-003's entity-ID grammar and creates shadow IDs that will not match future file-discovery rows. +### Batch Resolution Amendment + +Clarion also exposes `POST /api/v1/files:resolve` for callers that need to +resolve many file paths without N HTTP round trips. The request body is +`{"paths": [{"path": "...", "language": "..."}, ...]}` with a fixed +1000-path envelope cap plus the normal HTTP body limit. The response preserves +input order as `results[]`, where each entry carries the original `path` and a +`response` object: + +- `status: "resolved"` with the same body shape as `GET /api/v1/files`. +- `status: "not_found"` with the `NOT_FOUND` error envelope. +- `status: "blocked"` with the `BRIEFING_BLOCKED` error envelope and no file + identity fields. +- `status: "error"` with a per-path error envelope. + +This endpoint is a transport optimization, not a replacement for the single +`GET /api/v1/files` URI model. ETag semantics remain single-GET only. + ### Canonical Path Semantics `canonical_path` is the normalized project-relative POSIX path for the file: diff --git a/docs/federation/contracts.md b/docs/federation/contracts.md index df09a6e1..d516ae2e 100644 --- a/docs/federation/contracts.md +++ b/docs/federation/contracts.md @@ -138,6 +138,79 @@ is normative for this section. It includes `_meta`, `shape_decl`, and examples for the happy path, not-known, blank path, outside-root, briefing-blocked, and storage-error responses. +### `POST /api/v1/files:resolve` + +Resolves up to **1000** file paths in one request while preserving one response +slot per input path. This is an optimization for high-volume callers that want +single-GET semantics without one HTTP round trip per path. + +Request body (`application/json`, max 16 KiB): + +```json +{ + "paths": [ + {"path": "src/foo.py", "language": "python"}, + {"path": "src/bar.py"} + ] +} +``` + +Successful response (`200 OK`): + +```json +{ + "results": [ + { + "path": "src/foo.py", + "response": { + "status": "resolved", + "body": { + "entity_id": "core:file:src/foo.py", + "content_hash": "", + "canonical_path": "src/foo.py", + "language": "python" + } + } + }, + { + "path": "src/missing.py", + "response": { + "status": "not_found", + "body": { + "error": "file is not known to Clarion", + "code": "NOT_FOUND" + } + } + } + ] +} +``` + +Per-path `response.status` is one of: + +- `resolved` — `body` is the same shape as `GET /api/v1/files`. +- `not_found` — no file-kind entity row exists. +- `blocked` — the entity is known but `briefing_blocked`; identity fields are + withheld, matching the single-file `403 BRIEFING_BLOCKED` posture. +- `error` — per-path validation or storage error; switch on `body.code`. + +Envelope-level failures: + +| Status | Code | When | +|---|---|---| +| 400 | `INVALID_PATH` | Body is not a valid `{"paths": [...]}` object or `paths.len() > 1000`. | +| 401 | `UNAUTHENTICATED` | HMAC or bearer auth missing or wrong (when configured — see §Authentication). | +| 413 | n/a | Request body exceeds the 16 KiB cap. | +| 500/503 | `STORAGE_ERROR` / `INTERNAL` | Whole-batch storage failure. | + +ETag is not applied to this endpoint. `GET /api/v1/files` remains the +canonical per-URI resolution model; `files:resolve` is a batch transport +optimization. + +The contract fixture at +[`fixtures/post-api-v1-files-resolve.batch.json`](./fixtures/post-api-v1-files-resolve.batch.json) +is normative for this section. + ### `POST /api/v1/files/batch` Resolves up to **256** file paths in a single request. Filigree's diff --git a/docs/federation/fixtures/post-api-v1-files-resolve.batch.json b/docs/federation/fixtures/post-api-v1-files-resolve.batch.json new file mode 100644 index 00000000..b4cbfb0f --- /dev/null +++ b/docs/federation/fixtures/post-api-v1-files-resolve.batch.json @@ -0,0 +1,110 @@ +{ + "_meta": { + "contract": "clarion-http-read-api", + "endpoint": "POST /api/v1/files:resolve", + "api_version": 1, + "fixture_version": 1, + "stability": "normative", + "authority": "Clarion ADR-014 federation registry-backend contract", + "verification": "cargo test -p clarion-cli --test serve", + "normative": true, + "updated": "2026-05-20", + "description": "Contract example for per-path batch file identity resolution. The endpoint preserves input order and returns one status/body pair per requested path." + }, + "shape_decl": { + "kind": "clarion-http-fixture-shapes", + "shapes": { + "resolve_batch": { + "status": 200, + "required_fields": { + "results": { + "type": "array" + } + }, + "forbidden_fields": [ + "error", + "code" + ], + "allow_extra_fields": false + } + } + }, + "examples": [ + { + "name": "mixed_paths_200", + "request": { + "method": "POST", + "path": "/api/v1/files:resolve", + "body": { + "paths": [ + { + "path": "demo.py", + "language": "python" + }, + { + "path": "missing.py" + }, + { + "path": "blocked.py", + "language": "python" + }, + { + "path": "../escapes.py", + "language": "python" + } + ] + } + }, + "response": { + "status": 200, + "shape": "resolve_batch", + "body": { + "results": [ + { + "path": "demo.py", + "response": { + "status": "resolved", + "body": { + "entity_id": "core:file:demo.py", + "content_hash": "hash-demo-file", + "canonical_path": "demo.py", + "language": "python" + } + } + }, + { + "path": "missing.py", + "response": { + "status": "not_found", + "body": { + "error": "file is not known to Clarion", + "code": "NOT_FOUND" + } + } + }, + { + "path": "blocked.py", + "response": { + "status": "blocked", + "body": { + "error": "entity is briefing-blocked and cannot be exposed", + "code": "BRIEFING_BLOCKED" + } + } + }, + { + "path": "../escapes.py", + "response": { + "status": "error", + "body": { + "error": "path is outside project root", + "code": "PATH_OUTSIDE_PROJECT" + } + } + } + ] + } + } + } + ] +} From 95f296ac5a1e9a867562350f14ad9b1df2849094 Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Wed, 20 May 2026 16:42:02 +1000 Subject: [PATCH 050/187] Harden release supply chain --- .github/workflows/release.yml | 114 ++++++++++++++++++++++++++++++- docs/operator/getting-started.md | 24 +++++++ 2 files changed, 137 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2b57ffbd..43d76c6d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,7 +19,9 @@ env: RUSTFLAGS: "-D warnings" permissions: + actions: read contents: write + id-token: write jobs: verify: @@ -125,10 +127,24 @@ jobs: include: - target: x86_64-unknown-linux-gnu runner: ubuntu-latest + apt_packages: "" + linker_env: "" + linker: "" + - target: aarch64-unknown-linux-gnu + runner: ubuntu-latest + apt_packages: gcc-aarch64-linux-gnu + linker_env: CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER + linker: aarch64-linux-gnu-gcc - target: x86_64-apple-darwin runner: macos-13 + apt_packages: "" + linker_env: "" + linker: "" - target: aarch64-apple-darwin runner: macos-14 + apt_packages: "" + linker_env: "" + linker: "" steps: - uses: actions/checkout@v4 @@ -140,8 +156,19 @@ jobs: with: key: release-${{ matrix.target }} + - name: install linux cross linker + if: matrix.apt_packages != '' + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends ${{ matrix.apt_packages }} + - name: build release - run: cargo build --release --locked --bin clarion --target ${{ matrix.target }} + run: | + set -euo pipefail + if [ -n "${{ matrix.linker_env }}" ]; then + export "${{ matrix.linker_env }}=${{ matrix.linker }}" + fi + cargo build --release --locked --bin clarion --target ${{ matrix.target }} - name: package id: package @@ -172,6 +199,29 @@ jobs: if-no-files-found: error retention-days: 7 + release-subjects: + name: Derive release artifact subjects + runs-on: ubuntu-latest + needs: [build-rust, build-plugin] + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') + outputs: + rust-subjects: ${{ steps.subjects.outputs.rust-subjects }} + steps: + - uses: actions/download-artifact@v4 + with: + path: artifacts + + - name: compute Rust archive subjects + id: subjects + run: | + set -euo pipefail + while IFS= read -r -d '' asset; do + digest=$(sha256sum "$asset" | cut -d' ' -f1) + printf '%s %s\n' "$digest" "$(basename "$asset")" + done < <(find artifacts -name 'clarion-*.tar.gz' -print0 | sort -z) > rust-subjects.txt + test -s rust-subjects.txt + echo "rust-subjects=$(base64 -w0 rust-subjects.txt)" >> "$GITHUB_OUTPUT" + build-plugin: needs: [verify] name: Build Python plugin sdist @@ -230,6 +280,32 @@ jobs: | xargs -I{} cp {} release/ ls -la release/ + - name: install cosign + uses: sigstore/cosign-installer@v3 + + - name: sign release archives + run: | + set -euo pipefail + for asset in release/*.tar.gz; do + cosign sign-blob --yes \ + --output-signature "$asset.sig" \ + --output-certificate "$asset.pem" \ + "$asset" + done + ls -la release/ + + - name: smoke-verify release archive signatures + run: | + set -euo pipefail + for asset in release/*.tar.gz; do + cosign verify-blob \ + --certificate "$asset.pem" \ + --signature "$asset.sig" \ + --certificate-identity-regexp "https://github.com/${GITHUB_REPOSITORY}/.github/workflows/release.yml@refs/tags/v.*" \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com \ + "$asset" + done + - name: derive previous tag id: prev run: | @@ -300,3 +376,39 @@ jobs: prerelease: ${{ contains(github.ref_name, '-rc') || contains(github.ref_name, '-alpha') || contains(github.ref_name, '-beta') }} files: release/* fail_on_unmatched_files: true + + provenance: + name: Generate SLSA provenance + needs: [release, release-subjects] + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') + permissions: + actions: read + contents: write + id-token: write + uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0 + with: + base64-subjects: ${{ needs.release-subjects.outputs.rust-subjects }} + upload-assets: true + upload-tag-name: ${{ github.ref_name }} + provenance-name: clarion-rust-binaries.intoto.jsonl + + publish-plugin-pypi: + name: Publish Python plugin to PyPI + runs-on: ubuntu-latest + needs: [release, build-plugin] + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') && !contains(github.ref_name, '-') + permissions: + id-token: write + steps: + - uses: actions/download-artifact@v4 + with: + name: clarion-plugin-python-sdist + path: dist + + - name: keep only Python distributions + run: rm -f dist/*.sha256 + + - name: publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: dist diff --git a/docs/operator/getting-started.md b/docs/operator/getting-started.md index 6cf7214a..b74c4095 100644 --- a/docs/operator/getting-started.md +++ b/docs/operator/getting-started.md @@ -65,6 +65,30 @@ which clarion # e.g. ~/.cargo/bin/clarion which clarion-plugin-python # e.g. ~/.local/bin/clarion-plugin-python ``` +### Verifying release artifacts + +Tagged releases publish platform archives, SHA256 files, keyless cosign +signatures/certificates, and SLSA provenance. For a downloaded archive: + +```bash +sha256sum -c clarion-x86_64-unknown-linux-gnu.tar.gz.sha256 +cosign verify-blob \ + --certificate clarion-x86_64-unknown-linux-gnu.tar.gz.pem \ + --signature clarion-x86_64-unknown-linux-gnu.tar.gz.sig \ + --certificate-identity-regexp 'https://github.com/.+/.github/workflows/release.yml@refs/tags/v.*' \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com \ + clarion-x86_64-unknown-linux-gnu.tar.gz +slsa-verifier verify-artifact \ + --provenance-path clarion-rust-binaries.intoto.jsonl \ + --source-uri github.com/tachyon-beep/clarion \ + --source-tag "$TAG" \ + clarion-x86_64-unknown-linux-gnu.tar.gz +``` + +Stable `v*` tags without a pre-release suffix also publish +`clarion-plugin-python` to PyPI via trusted publishing; release candidates keep +the Python sdist on the GitHub Release only. + **`$PATH` discipline matters.** Clarion's plugin host (per [ADR-002](../clarion/adr/ADR-002-plugin-transport-json-rpc.md)) discovers plugins by walking `$PATH` for executables matching `clarion-plugin-*`. If From 708c58ed892cedfe7c9a14b170b06fa0cea9df44 Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Wed, 20 May 2026 17:13:11 +1000 Subject: [PATCH 051/187] Fix RC1 release readiness blockers --- CHANGELOG.md | 12 ++++++------ crates/clarion-cli/tests/serve.rs | 4 +--- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b83145e..5dbb3084 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -109,12 +109,12 @@ normative. Clarion-generated findings into Filigree's intake (WP9-B) is deferred per the [Sprint 2 scope amendment](docs/implementation/sprint-2/scope-amendment-2026-05.md). `issues_for(id)` (the WP9-A binding for reading from Filigree) ships in 1.0. -- **HTTP file language inference** uses stored plugin identity plus a narrow - core-extension fallback; persistent manifest language registry on the - `/api/v1/files` read path is deferred post-1.0. -- **Cooperative HMAC inbound auth** is post-1.0; bearer tokens are the - documented v1.0 wire surface and are forward-compatible with HMAC via the - same `Authorization:` header carrier. +- **HTTP file language inference** uses persisted plugin manifest language when + available, with a narrow core-extension fallback for files that predate + manifest capture. +- **Cooperative HMAC inbound auth** ships for the HTTP read API via + `serve.http.identity_token_env` and `X-Loom-Component: clarion:`. + The older bearer-token path remains available for compatibility. ### Documentation diff --git a/crates/clarion-cli/tests/serve.rs b/crates/clarion-cli/tests/serve.rs index 78168d28..568c0959 100644 --- a/crates/clarion-cli/tests/serve.rs +++ b/crates/clarion-cli/tests/serve.rs @@ -1781,9 +1781,7 @@ fn validate_fixture_examples(bind: &str, fixture: &Value, fixture_name: &str) { "POST" => { let body = example .pointer("/request/body") - .unwrap_or_else(|| { - panic!("{fixture_name}:{example_name} missing request.body") - }) + .unwrap_or_else(|| panic!("{fixture_name}:{example_name} missing request.body")) .to_string(); wait_for_http_post_json(bind, path, &body, &[]).unwrap_or_else(|err| { panic!("{fixture_name}:{example_name} HTTP POST request failed: {err}") From 4398cbc851a81b34f35069aea3659e4465095f90 Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Wed, 20 May 2026 17:14:21 +1000 Subject: [PATCH 052/187] Commit remaining RC1 release state --- .clarion/.gitignore | 22 +++++ .clarion/clarion.db | Bin 0 -> 11235328 bytes .clarion/config.json | 4 + .clarion/instance_id | 1 + clarion.yaml | 44 +++++++++ crates/clarion-core/src/llm_provider.rs | 126 +++++++++++++++++++++++- crates/clarion-scanner/src/patterns.rs | 2 +- crates/clarion-scanner/tests/scanner.rs | 8 ++ 8 files changed, 204 insertions(+), 3 deletions(-) create mode 100644 .clarion/.gitignore create mode 100644 .clarion/clarion.db create mode 100644 .clarion/config.json create mode 100644 .clarion/instance_id create mode 100644 clarion.yaml diff --git a/.clarion/.gitignore b/.clarion/.gitignore new file mode 100644 index 00000000..1ad40f6c --- /dev/null +++ b/.clarion/.gitignore @@ -0,0 +1,22 @@ +# Clarion .gitignore — ADR-005 tracked-vs-excluded list. +# Tracked (committed): clarion.db, config.json, .gitignore itself. +# Excluded (ignored): WAL sidecars, shadow DB, per-run logs, tmp scratch. + +# SQLite write-ahead files never belong in the repo. +*-wal +*-shm +*.db-wal +*.db-shm + +# Shadow DB intermediate (ADR-011 --shadow-db). +*.shadow.db +*.db.new + +# Scratch / temp space. +tmp/ + +# Per-run log directories (see detailed-design §File layout). The run dir +# metadata (config.yaml, stats.json, partial.json) is tracked; only the +# raw LLM request/response log is excluded. +logs/ +runs/*/log.jsonl diff --git a/.clarion/clarion.db b/.clarion/clarion.db new file mode 100644 index 0000000000000000000000000000000000000000..55bf13f4d4d1c92064d5f0d5e6ecc0e7e65a61e5 GIT binary patch literal 11235328 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

*8jmQ^82Uv1RMhy= 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ΟSv;mdnUCpDf!;Fp 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ν&^}Ucgr7W12rL%x81&;~ReN~jKFBkK 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%0DtVd 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�vBN_v`CgP=3Glo@P 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%0DU{!^u4@a`ZiV&;Q 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

m|A}NxCWN(Oj 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�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

eC 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

qarrYcs?^#)=AZ=W)^!epwrm}Lkzx!&!l>Eq` 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

Yxi~n# zVWA(F1|ti}9hbEYIAsZZ2$|#{@2_U;}4Ep zlFf`yaMPK6x$C*_jeb4#$;`*HKQ_8G|7=Da4EHD7-SA=f9Hw z+mU}8dEdxRICzKuW$z3n1zUum$f*+L=CK^Z)>TtwjsZUc{B`*a-dNDgsBp_uJlBT8 z4<0upMMn}c$JLSMM^aQphw~Q1?7Ei~A~2+EhBn7D;De(;J}=sauc4z}rG`brUXxP-^?W zCt03kyB4!WW}yn*u^qVjC>AsLUx_>AK+laVG76kInD4>!5IRi9VIqU9&qQu3hv{(E zfJ>qaH)mv&A?Gwmckr9%Fj)w;YzE@eD*7bZ$n<~=B8F;;hRs|F>1s5nKcS3}JOu(` zv_e#DQ?j6F*A1U}x+YqV1BWB!6>)1&6{#(lBT6EWhwLP>#7sx@UBe??JhjTRT=Z1( zCD%5Pq6>0I+tc7eX?Vg@=$J*O$+E3UcBT-w1{v*qLx;3o2R7hY3GzWC1NumqI5Gq!n;)aj91G1DLqf$rudXOMrU!pmI7=g&mSIBb z?C2Wzsx5J^9Hh9h;d$+&-LkG@L?ML~b)-Xr_dT$~Ok}vRpf82hhmJP;$RB8^aN#$h ztk*KP#?66uob76!1F~nNySF7@bWKlHFxf0*#36Mn%tsgopXN7YfiOj6oIgc_4?e?sV!;n4p}ig(Z-X8 z2Yw9hDrqVMrD7BqMispUlh%<^%k;plz?w}kS{c6Dwr8`)n|*RXvMLkdqw9kKq9q^+ zS@w}XP;;OmGca_L5B92nwha|?!9&VG8}r_fL@WE}@j!V93t;J*2SADnQ9AKb4Juhv z0X}`?))C=IhdDt+>>;3l=V}&`4kGvn)Gl_mi5g(uM5Z~#G2ue4I5I*~XewfqSeQJB zR^j_LP!FooK$MoDSc+{k3r@uN!BqV0wNY!Z9Ds6tj5a*?p!;zk<~I~*jlc5Z{lND6tV;PqWCd%jz<^t9U z9Vy5#gy0r1eZhVKv%=24F`-V4{z$69VQfJku;g<<2jJt()jdosB=3Z6mIUK0EHRGZ z+2H4@r%70QWu!#IIt=$!-I2WPA%M9Jo5Ez)Ix@+Ce>FVzp-u&_GkpmgF;!MDkHF-x z3sbQ3lqIkhB#M+Agcc%RiOr!bg7v4r5~H|JFkg-=pdnz_n*vB!3Z2(9@!uko? z2cIB=6%n7{U|1s*MKw*@Lt`%$Zh++U560h|eix=@B6}pYZz?VZNs!;u00TzhC9=0s z7+n2bY?d*%88%P`=7WOWm}h&6kN7j#(10jm1cKcprat_&acn@=TN&pNYIS8+{*XtP#oqE0-ey1$2Z{u~*S8Y(~&l$6$&B7knHf zadNTHNIU(GxKr37`Y_mlv4-PXI4Ho>3#SFFq}VKC2gKdv9`&4Rh*Z3ix!wW~E2CUBjrU3TqdmED&e zguKUuKpV;i$p4{Ff*?(I4Bhle4Thk#&;>%i2{K|KcL40L4(5h+5!M4=%&M6xxe zBvb6*Yi~?J-t@@RXEknG6I{4z6--G}FchS5ogdz$EK3Ov8K}l1o1}$l;=^*_vT1u} z=NvMkU_>J7IrF1}BnCGQIEg{K!E4mnrUB_`&%))})%G|e=d0=W{eCEBz7d$ee4T#- zB2P2brue|n;43iOQDIU6BMMh#2o_A*0vH0x44PIG6B`Nv*U}95r85ZaNntO-$f%Rw zx!PgAYEw}6J#zTs#toCfO>pqL*cy<7Uql&AI%wJ{_nrHsBiPPX%l!^;=N z5`iy98rM$-8$cox>jy+4r|_ zJlirA*h=am)Iw15BX&3xKopGv`Z5@>kb@_J)5|bfL5h^ntO45FE@1__lfu)x8`qs5 zT(|}%^#h^=GL!G000>HGao_K7_4|HO!_9r4zXOW{bepg*@*K%kBoo?BO~)wuWUisY zE&%3cWF1Hjz@Fp5Oa=yXQ0ZD^=mS-y)L}D6K}os~_ui-G4n6IZb0MR*}A?tCX-O2 zBYO!9JYn60@b88N<6h{p@mj|%M0YBAdahwj2A3jk#K@)HsU!*ikZ{is0_tT|n0F9a za24ry;QD<(v4Q_O_g4oFdJY+q!}!d%e9uP?35+_OD>5aHUzBXU~u4Ruq(3- znEg@Uf7niwp_LAcie(Ob0cX2Qa)(p7!xJB!cydA=|L5`dj637!6~A0ORNPiPXY5;J zZy&3SA)qd-0DgS*6{A-azFYY9!s`lK3uop3B>xNfJ$XI%-?`7x8FBbG#J~^(LktWt zFvP$R149g)E(SIU!AP%$05w*H?{xOnI$v2D1SROMP0?p*USgL`_4Hcizk7S_KYM%a zd&_!lre3b^M^fv|`+IxseZ9T*-rin&Pj9ciySLZg)!S>o)Z1(C?CrI8^!D1@dwcB{ zdwcB{dVB3{y}kDHy}kC<-d=l4Z?8Su+iO3!O$b&m&AJe?WeO+e_LwbG`|PS0Ci4u_ znq>cDs}P*sE38`GtIX9)P;*ti?B65?>CY2`^k)gI6F~wnUY7uj*CqhtH3`5tNaueq z_YC{c`IvA-Jo%GgK8 zerfCtV^5FGjy*PZ$CxpOOa!Ap82#U)e?0o>(GQM3J9=Vt|ENE@ee~whYevr>9Vz^4 z;V%ncE_|f$&caU?4izeeS3uIB7uFV5W!j3e-)k@WKkY^T&|dVN_M*RQFZxz{(bMfkbL~a7_M)BbMO)j7GXJ-|=pWmQ z+MAmB_4cPe&|dWB_M)F^FZz-8qND9ahue$xw->b|C-X%6Q+wKr+VPdy(*D#9?L|}V zMH6jB!tb^heWJbSqwPg+YcG1Hz3A2LMNWIsj`pJVJ_^^iKXqk$(OK<9E82?+?L|Uc z5&y64MStI3^tJY)ue2BaPJ7Y2+KYa^y{H|le6{^4xxMJ>_M!{hi&nQ6wWE;xr?#iK z_W8)QBZvFj_7^|XUet~W?ibsidTV=8`$Xp6*#6X!_M(T{i|%PJx~09SeWr0QYk%sp z_M-FJi;C?_{~&DS&&u7|@erO=z|Zm7BVbbN0V3~zfB3va4au9eC&byl6R zr%SNqe4M%U(yU$IHFD=3A$YWD(wTg{Y^F9>WmKh$reuGvDg*^|tjtJ}^r%lV>|3!h zm>XP8T~?&MLa=9Wl{Oc+h%`$+O>}H8vhSy$eOYc>s`$0h*XQrdeqZ<`ZiatD3@jf5 zhii>3lffQDnz>6te%vrFWIDZaQC!F-C7%A?d48WUpErBz{$?ue`&q3r4F}Mv=0<4# z;aapOZ&r8O!A9l*EU&h1^Q6frH4K=^?$6OnIP? zDg`-~RHW{dk^N9Yc0OdRlqf?ba@!#_5<=Hdu1FuLk~B(Kh>UC<;#n;nt@5ze*h~SA zRtM`gfatoA-N1%nd7*4NVD0|JwA@yZ*mx%fC~?9uKsb(tj5-Q3blE;~vbvxa54o$5 z=MOQ^Bt{vsEYCqYPthd_rt8sgBuqdp@J}VAmLC$MgTw$OPEc2?2M+28tL7rYnrtJtB2pBz5DM=Gj z6Ye|#H473O@5BHlPB@pWqv)Il(n4ycCQC?LXllrJj691Ta)envQr{whC)kaNgpL|= zFCwEgqAw$#nTrf!l4a1c*A|59IXKZBjaw#zd%%gb8_ozPsu9)PTdmBpY8{8h#Odc$ z@C#&gYI$NrkQkuEsp*0X4;(~VPz|}FC8S|Sj9+9*MS?d3@kah(6S4h})fwr{4GGDO zL2{O>BQu=pd&sG2Yo>}c$H+dsI2`bEP}BOxMhXpSe?trmF>pEr*L?>tE*4tG+N|Zp#*W^jck4y=BZ~mwD=5x*0t(?nOZRS>hLgqJaPR z@jI}>BgZ@P$ZN8TT=EvaGu5>q%5ZT+^+kko)=a}kqI6_+RefE>Vy`2ztAPdI^>y(1 zR;I0AD_YlT#+0Z@ivs>HlpmgJY&-u@22{s_l?M(4qst+>mOyBW`x8b-TU1!8Lwi!M z&1@d2weMz$H{@QcXVfgNbPW=Mm zrIqxJIH(|}ypIgy9x~H2M`x}{A%h`H(~v0`p`m4@fcA73;h+`lLM4XC!IFbK#R}q! zTfTcrwBV5NRC!9)&33|z%b#A;xMTJHHESL-Qu~9+1k(OQ$Xi~HA25~nP*_WjLRY(t z=N<&Sx}jr*8%ofnl|nt(t8n>b_>hXl3bzJD=Ski=_6O50;Ta;V(cdR~xUwQ6oHvi4wf`peqW z(!XWr>S=km}E5SxDh7W3n?@ zwGq2t^K1`-N5vyC64Jd$sw%6Dsip@hB!mLXn(xLVhc`BEn+&$%oWH>sS(|fe`)hT!=UPLPO`B;r@QF@z+JsWa zpJLOsSq7WnJvgK^Z=U04y>Sn!xb)JtDl}7;nF&J<$uSmSEPT7QaW`JHu4sGFhJs9& ze9BE^Z*R8vlGf7J@3%JY>I??*6U&E2)|V7<0ZeOY>-V76xILc==CCm@7URYK%OS?mbOMt<#=Yy;jgw)pAH}Y%8P= zWk?tBoE?uAZTf6_3&Az2R)a}elp1AG>y|0}KXtSRT34qoEU&dsHSQQm?SE`xwOpoY z!Rd~8N9l2pX>84D5vs<`Sk^l&uNTLkP36+C%k8x{y|WmNV@HK$g_*YtC|9fKv{>81+*xzwva^~yJ1m8_Fa#`{=$_M z7e$t>coGu0+O7lfj0oXAjCimsmvrd+L|8R@F3dD^#X!<$4<;M32n~P+6GO+>Ow)Oq zJM51K+ai>q&kqf%&&}SuqOTgNOt#pf4jU4PD50RxebM$r2lhy4qa#@+lWZ9p3D`;M zs%&_&CdrUnLNMh6Czk9gu+@c;rRX~h7(kk8--KEdstm^xp$%0HSFY>&)JhPiJE9j|c+o8zo1=N_!jNyO%+#y4TYs=zftM6$;8YQrVqU9i zQ@W)-a%ko-YaN9RFZ5Bc3`=y72vjk2k}*Td?Ab2tTU`S(Lj!iLKF&o75{611Oc+f` zWIBdiffmrvJ;nDk0~m$#zjOZg_ck$Io3dwT*~}uNq*E~Ny{ItmL$|6yfbTLHR-UHE zFeg-~U!ncCU^@(Lp69xjDM8;4c`XZ_9$>g`!AczleugXz0ORNV>Up0zeT=(@-BygR zdEwKz=0jO+x+3_mZ|XKA9iV&3lw8k)*3=b|1sUwdR!P4qn!1h$Wl2XCb=QzYv*}V7 zFYbwOn$<^EH_9`6E4wkkt)`hRgk3r8#Apr|G3AAhW?!`-r11IUCZbx#)^Bot=A zs|$-n(}%WNc1>8a`Y@A+1tJE-aCG7H8XRbW$l@i}KsKB+wY-oC9Uldh^A^EqcyN2mw1$L41TXvBfTC15XS;Ls+TmBmw4NjOa^Z`N);(Avx{gSGD$KEE z%kor+^j#5t2r}5YAv&VuGhM~%3HK)#TS*Zfcp6N~B^f3S-XKu^BWJy7-G-$pJ(Ws> ztKSeGCOJ;)744O(y9-heoB}J=DN>Z-zA_yP3#xCp*saUTWKS4w>y})TpYf}VJ%uPDWmbjP!_G3g zy}zA@t5c??JW|a3UMiP9kP;r}{vmx}{CH6qeLDXixeBSd4!ycn*>&jEyT^lv=a2Q< zlN*V9TE`|nm&Xx_Dpg5#fN~F7F4@Eh?a=*)A7~|rp4Xf<%q2|_@M2|-rKzs%gB*Mq zaDaYr6xOiIf;)%j9@=xbJPxVdz$G0BeiVhaC`H1e(u}X)z zzh>aBq6$|xEN~3F5m$j#lLa}33j$h1Hp9h_Q!O z)4Vy6gV2gst$1Qf6~wf!Qr&&a+|JF_YNh*b(K)=c_2{{}R|c{)5lqdC`1%!cnIK}% zVT$2fkltAi-1Hzuap9*2ITqx6K9u5+d*RG)TCxsFA)dBjx8*oEtV$}&4}iok`r(U? z*XL%ZyY5rq3Gi$ymWQImQ%oREPdGdjB@RlI_;lv*Osr1`1tXgV#00RRgU1Rhu|4Hb zyH&#O&)r=^pa@B_F^5eBhY3?OU_uElDr(GBbwiRogeMVgkzreGN|x#}odo>|S7JF1 z&L4`0^SOfU92PbS+cpIU65m!;xbi9%1h9Z)8g2w@Qyglu1-d4Qlq_B~IB zBR+&x7$XFKFcJ7cv}|87Wy4~y#diDwjKt-4U;g;QNVNC4+fW=jczAj|D9!H!uN&Y{ zIF-2>&#vx|=Yt}7J~J)da%`1Brs~KT7hT5EhHZvt8PFEs@TcR5f^DCw>%MFvDvJ%{ zV-fp4oPe;MtmM7>L&5`I;m`K4Qz*UIP0j2FZmE47*7RqM)0O;)n*0SvFJ? z0~m#?K7Q37&5y#V-R(92PxFUyv{~4QZye+Rkn*wQPtVnM#bY4i5H7huj)#*XoVXmQ zYM^Aq0UQAWK(6rcV!$JVwgIsK5E%z%a7+Sc0h6*^2caiS->_^;!pT*(Flv&Etsxu` zBn7U9KAaIn{4Qf(?kU(4`jC3b5S)viWkNlqxdRx3D}Uk2k1dSB;vRP!hC@#remowA zP=Y%-pkbh3u*si?pwHF8l{|`J1gAS2;ueIxQgrBuZ0L(IO)y&!;!1*o)Q}wkQ!m%|Be>~QX)gjUdZc^b8B}w@V|~JXGF0 z64oda{7$oRR%X~Y8q;(z6GxR4?0c{qG98EyE#LlaslRUJD;7oD&nsgo4ngEO}TY+(aQQyjJu@ZfSKsA@FP zaS_dh*sZBp2DXUgva|NvK|L4PNsaI3h`M3Eu)Pj<>?t249ECIqEfnc1B8t@vtOSzh zLiz$_kp~qi+)sVigvT#7x>(vxAA2h(uMtuNO=q^G`*1e`+fpEX$XWMjwQi=)`qMKs~pT7@DR2ukcY_3(tX>ovCqJCBaJ3g zphCv85{?0gt)iN+nTIsXRS?7p$S_G525)K=+ZY`ueyqLNI700ss`lEyd|~zr(nUp5 zUDYD7J*GDVZwRai%LA|~jJk>q7mRRZCVD!eR>|l*Ji%cHg5hL_>iBE_Vo)3%Vxc{D z6_OGV2B<@YcGH88vkle*)&OZZJx$gFoK#2iz_HATnxWYda$5H9Uk`gT0xKNhv~dDS9v_z-AR}1&s(8H>ZHT{?6CMKlJm7pRmSM|H5_U;C#)B^NxD6u;cg zcJ2Ys+yt`;8!jl={ZwW`q3l%Cnuo0jc_Bk~sq0t?6mSQG(}b@N`JcK5 zNRgN!!V*B>uiIF|p(F>laWMNJ)yKIG&Zi=nCvjEmbscPZVEKqR2VlG^bc(u4ZaWq{ zT3u#amWD8$y14dFUSu6cJPp`&K^~#V9s+21@ZiQ`iT`oz1qxgh=s_$%#CAg@4kk;O zod)!#&=|r1)HeOK-wbx=U~iw$p82+jug)7Yo;tb+76#U#7}%BAob}Ngx0( zY%v+uM<#)-;t81az^)VA#kN%J^1y_A#2y1_$cRe=oe&i5*s_WyHb1(EhhSZWY3%6O z#yS$_G^E~+hXXwhG8)zt1Dk18T>Fg|QD?E`1rIe{1Rut5K>+BWn+6$^kzhl?B*4l= ztB~YD6Ap)dl0w0mjlnn;dp&#Y9|t>g!EH&6?B+eQ_5JY#K6O-8@PCXUvKKSfq$yZqK+j~(3=%?>CZwGJY?*7n{$lAUu%T0E66jN^S+kol?vD-Q?-uxZ0Yg}xs`e?v!jFECu#@L^M{L(T&W z9GJ}`@Q0%rKGsMVze86LhHwN;VrNX#(pEJSd>OM8+aV2Sm@qcFXWMK4SE8rN(!4Vj zh1gu^a4C|OG=)2GFSc=J7V zNW`hcc}oPRk}OOy3!4VbhqfB0PHYGyADR}xfP~(Krv>LM@`I5pJbx0 zxAxzIU~evx(DosCS{UP{yKT=P<4@=+z&zN+A_%7n-wy+R5EvThhFl5j0S*`t++mOI zA(%8Q0x8HnMijQm|37(W0vN|x-u=<;N?P6VCcck2krF2)QRdv4$weH;PMp(;9YW5{ zoU&qF*_Dzwg^+b9ffUNoLd#VsQn(7GP_6)NIZ8QOXdvZmxeBG_zDl40zTZ15Su0B; zTi#jO%V)kX_0Gz>dY^gceV_Mv{s-sAkg5`l3US-Gi4m=}psK43MV zylcs93ofuq1CSL}e_J6@W_47-60dka89_33C7AjBF%jqA|YbR-Tdw<@Lqc%gZRicVYk5ta9)K)POV8dn|r`}J3 z=0ojaC8Qck#fdYQtjU5Ef%gEtY`gcDjW}vs)j_zD&mc{PYAGGa3f<3x`)An}8MWpk zK+QA=J(~h4hYSz94pi|eB0FT#KW@ZPa~Pm10~sKa{U-qv)nlR3O7mKr5u<}v$enfb zCeoUMFQZv9tpTz+)77-zpElrjty1|Xiz5XS2`%j$k&Y=IRdc~lwdw7%N5ha1HCvz- z96w0mAc%r@qvoS1z29%ZK~v9xCPqP=M9YI(z{sipn-=8|-C@@1f4V^|$mCg|(NzI} z0=2|o*upqHQu-jV0~E|+r<0+9E1=`UN6;bZ{htOpV{!BXa8L1)BMGf6KrDj#s_n381rC%m)!0Dybv;i|$yrLvTQoo1gg0~BN_Lcz3)o+saQ2q2a#}phO{1xl9fLbLux0YZ>ypiNqz*Lhi8}e0Hp;=IvlaZS&*V!K;_j^=t_D}M}R8Y zfb8kO<^z*7gccWdS5Db*HtGA(5L0;IQ67N4?^E04Bti`nsR6*y;1Kv2T5dc7t7B0= zaliTcf%W%}4-UhdL0+ujfg?llyBf-aezX};>GSH%2Tq>6swz~Ak~HGJFsV@Sv{Ut@ z6o%pf-9c7wnPW;`JVDTPIF3^L^(brW9vC$r_%`_tr3fxiwxETvYU!c6b$u|5yJo)^ z3=YMHXzk>cRUulGu3GO9(ZhlGVFO&COU}B0N(1L}g<_9M&xg({JHM-Hz|UwXQ{sUw zqHyBnprbQF6_bmK>aQ>(})LfS39)fsT#P+R2?&5jv*R zhfS1rG(L|8g-z-oboN=LXsc+{F%6D|^j1hHm@Cu@LBr9VR{$*t#1uy)Ej)4Q5o%f2 zR+<=IBsiBJFTueMA`o3B0fYc%#&Ma*7$ns+Ccq*C*B~F^z-T+*Yz4oEt)AvLXjyPs z94~T94$j&Sul?vyFo!%mXKh|PdD-z295iNNi;;FwKZObz(&^$*O(Jf>L} zcA(XwHcU?!83sRCB3%a8=fN=`ggr|R+^Q2-^&%^^7o|?2F@$aTT%n3Nky<->X;q|- z>B=q2k(A`oaPno&8sdB)(=-JMN6m+VfFK$Q2pI_!%^xT)gcW=siVC5**tE1;F_VEgy^0E$9A#DMjmt3=+{Si!L9nFcNX%CxEeZS}AU`K*^*D z)PiTkXXnd9#H}1Fzp?T^7YoiAlb2KlM>&?y7$kVkCWJsZ(9rb>sU!e^3aBuEb|(F? z>5u~aqY_RN69^DIZfL2%5><*zd9!oLGiT)^E8jC+F)7?Lh_rD@0AXVn^1jKfRk1qr zSjI{m@&TA?GQcW;HF?n#qP`~dxO|$!4i-_u5(f6P&6(T+4N3b%VC zD))$UeH-Wcqg!d&>OBEN<^&JaSx5jP2#8M)B^DlhxC7!U{ao3lsfA(^CvhPEG(-%J zkf0uY7x*Usfpvyk6F?6WY*qkaf+bjLJ|}ao6Mz0==!#&mtc!Pzp4`RnzLa)Wise0x6PD? zj;`*5)JQ2PIK_by{WEqFE2Pch#;9-@n*DJB-+9ojvJSMN2!Xg?ILAYK;~kcn>ZXA_+9K91D&Mq&fIl&=mn= zzyyWas0hq1@K(qLl-wwo@rd)>p{yT-ElZwIr~UHu_UULjK0;x|feC@hjg8lqNv|qi zN4HTW7cUYSG;SQY5zvF6E16(ek+NYugW(~FBDBZ^M~Wsd>lolrE&vFj<)_=CXArL` z88^F5d(~-g9_4s3a`#{n8REri9vdg!s!%Pe3>)`tRWrEesfFYQKwluzEdKC;D86bc z=?}mFo;jJ+5%w60ZL}T0Hbt?dg#>*WQWThQEnW;V?w3F9;nV(j7srg)xpelVQx%|N z{p^`8ylis>Zi*rP!!1M0K|5M+!VHeR9wN*0A5CC0Ei9n zA|2_Gygt*dI`RWY-_K8LT}r&pnk-DmOC(gsdVbLb0hEmdjzS9rUo4mvL<4zBPOT8W zgpjc)^NR&N&`}!zcm_kH4)H%(8^J)BEg_ALjN0YF-*snyc72I^_|c5 z7e%ap>-0XjOC&<^^|x~;Z>u`CJ@u%vhlF};u(jF1Es}Bw4hDfiGTEVcg91YN4eN20|Sb8$htUXD>Nmy|3^6hj4Jj9~>Dd72N#=TzH@?11JNo`f?BOJyaCw}-BAmEEH;0`FT%_sswKm|ONi9*gWLI^VbWlh@aFb$ib|lTG=Yn4WkOg{p z5X<0CK?-LQ3xQ}FGCI49+?V;qvLOE|w|eWy$X4{721XnhiNF;-1PLu6y2SqP&@W=M zlLa8vKqiHBll58L$Pk$ux{zQu;U9>jHl!eGb07pL(x?LcC5Z0Gc^cw?NDOc(Lp%il zRS3$8jwN#;q)85`NVjxSG|TS&O#|L*&cYm0Aez8;7u5)gwL<$ej}SP#cObkT4~fXE zaEb7Jz^znZV2|4Dd7jUN=xbgc9CO0=8i2ls-!3RltSMkHc;MeOQE=1YIOJ7Iv#6a@ z*u%TSU?C7^I!bQ>0}JLk1jqc_1vp4`!2ZqU;1r^1J`Q$>0~2i73j}ax1huDJW5G_aSj0(m{eksfe-=d}NA3q$j$<(~O`bd{xj4ta~7f;H=~+MM4-v z?9&Z|w1X|QoP;P8r#DD-;QPt7t==cvR!hH#^r~h~95Y0rB(aalF6OWg@GcSKA#pPi z{f3$gXpYGan8UEBzEfQT=n76B2nXi{gfkQsbqoV7G0^;dE%u6sq-G}pg5cp{pVLE! zAqZvO*HKTPVoXAXG(0z-Mhaz6A5JffYryH~}5fpv@EN6!hACrl_@uLbNkupVu?S=7gK`+SPb3Xhh^9b5&`p3p2N<1uXiyj8 z(F6c$i%z02nK7xzde|@tLk{sVNXfygq{ac@0a+@vHS+A;@Vd!&Ko^TjEou(nuV5|q z{;sjNGDxUAt}Jd0t!QWRoG*obt5$04XTV(upwJA9jIelLbwy6iqZRI zV~^iQZ&=V~1tA=K8CokAhi~yrgW4BTV+@k|PGy4t^vx1a1W zfng8@k@vH)gYbcl!*df-1>~G2h=@ocpl%86kZqK5D(!a=*P`9$>Ak;h?D4y{5Hb+W zLy@t-Feg)^3hIJmGAXnW90(0`R5g`I3H%{FRRB?(a9J>@CiniTg8x5T^#9-2d4I<> z?Ps)gwEnQ=GtKYF{7L%m)QwGN*LLt<;=ev{!Pf5b7O@y-%jr~VQM*Cb((%2)_@IZu zUoh4`g1tlF_mAxx=ofP2;n3(k{nt;7j0DhY3PNq6P%wb!vaZ4?cbNUa3F*dgl$XJ% zQb8wXB+(?m?43MALjD4XGdU3yM&S6q1p(mZs!Gz)HG&hWBvGLC&;n(x4=6lv76A4K z6GUbPIaKLX*ae}w|4UBRpB{0WJ*He;XDZai~Nt1A}31}fT+?M z4z&R!MlAHYIPjA$7$7cKL^vpN_^>!G2pAJ5DEaiIhv&_wzIAYsj`;e?8>@nIOy>^6 z=R`zmK`oF&G7w0Lu@Gx~q%c7wQ)P-&gviJM{IJ>z9YK8lI7RXo8g-{FIXIh3o9{i` z+<976c#i4Zq3NHeU`?^Xq2d6piK7KReN_2ruetn%iZ&|~@+hbR0D(v54OTRTGgPER zYTCE-@N{iHVKH#~`pFxrf^$sgj>(bQ5HkYqgU~ymSV}GiQI93sgA(CG6cMLyIH(+9 zc;=|qP@4)kyI`xThP_J;&dJ|D`6q{)JA10ab4=$B#VclkB@>1`nm3HzLtBKE2(G!P zJQ^-ykxn_jr-C5z3SwUfJ#m*4>YW3*t}0N8S@ zA<2ih=Tf+4djg4LKm!)eI~s)!+PO&H2$D3SC~at`EIl{_o9BwqYr~0DIuOHN?dSAS#E^n8D0t` ziyl2O3I|LdxNB4x0Qw8hGs+B(wc%)@bbED_&bVREwln+1mDqS$le?34YGWC}0TeI=w%!`6EXQDG3z>azqM8f@VupxkLj9X=Q`~)p!H9xCED3b?3tI zI^)d#!P00rT7*U#8@s;AYpY^+bil!ca!5Qj$fpsI7R^=;+QJY_rcjM!2D%`q52Hm8 zwJSKJ18GKx5B>;v9G^^q&Y`_-$#ZSPe{c9D9&vXf?vCcdqNu@jlh;(m>6rHah58ly zKRgnj1xc*s0z1N4j@}K3U_=>E>m-qalf%+Zr4k@HM-_zqC|XdhUAy!+ePzSf+|kik z8nV#;KTUe2)bpo3a`&gZ{jMjwUewjs`H9Yv&J#P{+Yxl++W)dW-}b4tTeCmNK9oJV zsiXB>t=n53mtNW8Hvgpgf##mfUu4cne;|Ej>dUFYrXMzyGGB^6^N(f9PF2t7zD#+d z>g6Hw8KaqUA^zR{<9oU9rJ~pG4LL>Tz>K+FB}9U*Ry>Tj#vIkN-X9EP%GW0HeG3Ra zRLZ|XL9q9~7aw&3Tm-+j?H<&5! zu4_oAWv6=oXPNfI?*^Ijm5GdKfxUM|Y^nFZMl$8w68V-RF&OIo&hmcUx9`l9Z&;Sm z_5SDbebu*;TCcWO^1;Iy==6T``b_z>8p1f=yQcRW$$#Y6Z_bpBB!0^(53Y){K=0S; z_tF1RzmNX+`hE0Q_hrgACK2nyyL0q@Wf{Ng%QY_U2WR@?o)o=bTBh&$qL(ROmc)IE zcVg)M!ZLivztuJeqEkx# z-M?Yo?V0k0iH>1kQDLYwhWV@`>SIB%Z}l>K z$Es!cj+M*s9VgwEDW9LnY+^l#bOB0LE5nI({q$U7S$oV$eEJ|>9Ozq7+ZJ~C+U;yT zweN&w{H~s5{I2fdO!<~XrrM&n>ndj$cywRavVL3VvVL1fy-QG)*;s|^?rX1iNvQIb zGgQaEwq^dl>@t5}>oR{|%QAmo^D=*5W|_Y)z0BX2TAqb^8?fTOre*vt>7Gn^wEmmI z)W8gP+89o@_BoIyWC&+n}erTEF5M|ZAW!jjoEGw;kZ?`WuH8DU;TgL z{C}eKaj7TYJ=FEO&M&qqna{QiWLmNxZ+mHKsQDFXyY>G%Tbf=WeY`_&cjG(z$NHV} zu55Yjf+Q}EnfRqI4!N2|k`#u6@gn$k5Q%vhBw(TtO>?o_KqnRvkUY3N5A_{1{cvs8 zu+BwY4C5;}Ryc*`0Y!V5{AqdNCjr|84i3@+V5h-cAqxT$6w?kzX!Qt67F1lUCr#nx zfku>rOqmWP6S)CHS62MwcpDM$OF^wi5e#Cm1BT2&6%6n^ykhWOpc3&jQysLguzRv` zH-=;5+CutT7-a$KT=ApwmD!3K(6+@M z6E#<~a1a=TGz^Lfqch=EZX^8&0?hJPeD63KOs%StEB>o|dA1^OcyxCrIc>ul)#p}x z>v$OnyyKe0;Z}U}IGSL!igK>_&hc_vY8B61@$KVfB((}#uK3U6Xo8{7iA=7^{s?;! z^KI;pgaIywHmI%$`x03EeRVW z0j873jbdmpW1*JdnuS4n=pvR9xM-L``a)`$Gy#q!7-uiP;)loEi17P@KO<;W$Oh<| z4P}qVM)(tQ^9-~kfn%dkg&{MtMod3AtuC^Xh_m79lUMB(KR(_@q@mOyc(+KPp{|7* z4eFOL+tGMJl}1-#RbkYKIkE6PLrD&{x9}vzy3s`EMZp*3`^U>NU8@G_ifaifFC(c{XLZHbkC&0us(`xUYl8pZDV-yAKi>8F&Rrc(Y5Sk--?rY+{6yyY>F%c2 z^G5Zb0~1qHcX>b@vzy9m(`j=tzD_`wF~Fu_cyMH}fBFR~&jW)aLH}sc+sm=M=m_&m zZ?qWnUt4{pfPf#OZZnAf5i~FhgMP`%!;!!<3R(nm`5}B<2=0iLBFxEO5cPtwuffJa z1OfS1_ASx~jNWue%L-NuKPUbd?HauX+oOy#4hvF;b zL^+w@T5GMo-DA$kj)+;dKXw_pdGZgYgUdutr50sN*D{e43NDgql&+x!L!PDt3K3rz zVMEQdbaqXYF@0oXkcC8R5Pc;DmZyr=m}WpwrwA zvGF@~^4_lU$|(6gYB#AOU6aC$#p2lD2&%-$?AwTdqgALQ;>p&HsC0-Qp@wB4PHBn| zd4(v903ep$2n51JXZpulom-iv=T=_!kB<%yZ8@_t-o;$n4xBZ4_jFW+lf;(e zBB~_oC`bo%GV`=G(GWFMv=|ZC#8n%EAQP)99%d?o7?z^-f{i4TkzHO$a45*7$3C&K zQGK}m&qTzA2HpOlQOBRZL+D0-Uf7RsnVjeE z3al|vfMxYTXN*%Yz5+HXBmn}>3T~`{f`ko43P+|`!F9wAIUxf(uuN2?P_ssj-WKi| zWEN-_^D~G`AaO@t!L&v66hTz>NV?gG_@gq5og8u!2q2TgXqIk;i}(gDX^_$<)=8f@ zZ=mRm?G<_)1(ynkw^!iJy-|k;$qPiCW8U^Ub0E2Q>cr{bM@R2Q2|gzdj7x+<6WVZ` zNKjeS5uZkH6rV4dMvqR%N2je2$t(l>P()I#<19li%?c3vbT2LTAN#WOv0sw_3WcKU zfw1h~HaZdx4)jNqnNl!5E<9yoWA9Jprehx+yz6#eNqFK@r$f*wk2?{E5+qYp3VAwM z6O5iz9v03qoC;Y-k^E$JRTaP-h#t|lLOKd@_xOE%iLw7@>0@7tf_}9#;tVX{ZZti| zA8eXh(Otgl-~zL~z6*@+3=}9cLIW!~Lp+2dBE`t7xCiK1qL6qZ_k_YJIvAYF)w@|LJq=qBJK3PmJRWZww21p&H8XwG`1t3ZB`_rn2ML*mcD5+&sL zjb#!)X{we*X%!D{wCNR-{4?G%dTBxb@a58n{dtQ`I9M!=7ev80ezM;`bwc$`Skz-` zNx#z}Ri?^huggtv&?ZIX+N0(zl#bar5PHUvMHNb?D1DQg;0!?%nuQ;&XT#Ev6B2$W zSb3m%YQ3`W)v}JKq0#X?Ng5ua`dbkF$56GXH0L z3*t$LyA22Ko$9W>4U5|OE$O#fz6U|}k^e(O-w|hHA#n^=#6vJ27Zc0WZO0cK9U9>t zdACbz2>&AfhH5s7*c?*w@z&Z)8nE=CwbF;qzI5wlduEnfw%-L0bhFt`SEI+?KzEU% zgr+EBvp}m;)M2T8<;hGPoEO+WSoD#c<~iX3!F&+cEsEeOj!Z&>zdfPU=Y!vpKKQ@0 zkH83=2$zE@w>F3lWix9HW8dk)l)DQ;?QNyYv}(g)u(8~Y>O09vOys_(#}x{phG5SMS|A(;fCW?C+Fa!lMY z6_FLuFIN4$<%Gb6>2Ts7XK|EA&QD;w9Ysdf*5n}|3)Qm;-hp?--T{Op?{I3Q&@@&d#RJRIIqG=fO_ zX|o|v9Bdjpo<`%5eNo}Y5-2n&nMXM=ZEnCHlu86M1JiD{Dq#H~Ryx7~g8ygepf&L` zXN;RhkB=E^gkVUqa*$O__h@obRW$)f;Os!MX?ThTicg?k6b41P8doPYeyJ`{-@(bu zwpaYLyfa(wsBy-6L!%{9Slz=1F2w%P!*vEf1*QWnFeKgi9Xxu0xXDo|#c>5QctE`f zzYEtHMFFG7NkmwC`}~y}7oKC}!mNoV){HdWFs8t55@-*-M%o@4IU7}aY)~mM3pI2_ zcv@__`?whju0nt-*j;54up*5^{n=dhX6IP;^RM- zwP~XD1nE=9#a;0M+t7C6RD#Q2!0(I)iiJgnFa~sK#*5WLKuI~%Q`81&${2;3J$_q; zK@(}y`0??eP;_9y;sFQaqMG#7D0EnKC8)deJUA?c7!lZXYB9#5Oo#O~6OY5N03&c& zUGXzjJ-_2LY3+h{<+z!7EWyBcX)v~M5R2HN&6SaIRFEFvf1qxGJ zAa6J`izdDjY`X8b8H(V-p|efRi6b+J|B#9XH;ZQpy=ZLg=&bu1{Y;0x3s&Wv;xwd_ zfzxnvfrr?e_8&Jx(I|3(SEHm$CQkW9!Guw_0JG6?r0@uIz*eYDnp%K&3Ux@~+oaI? z;dufuq}ZG8J#K~qx?17ka5QER?L0^cEcy)30b9nX6fERO6fDSn+%91)rn&g%`Xsxo zA)4xKx~F_iw$dnHG(|G1)qtX8sWd#gFG#%mTWg!6_2OU@caPBAgAO30UJbt zR$(t<_tb4_g(~&;Jh>X3aYK-3l`zEpoh$**a@PeB;%>U@_mjew99 zzdSxt9-Lu7`~tfW1_FSgXum*32Y;cGyveNLOfxnkShcV>9PkQwz=BT2`Yz;{=&CDz zF8KdFP3Zrnx-(rZo!O4|_RhBM>sUg9bar&uf1H8 zSYKSm0g;>!hhG5C!X#E?wSfSRsv5|DN;i1CfzJSKt~dZxZKz%pD1|AY@IhB{r<9+O zt+eavJL0+n#x9i^EQRRTzeyr-hep|@|^P|m0wK2Ib`DC zX@OkC`<#7{rV(bh`09}yP!^>SK zanKh4e_#r50)|r%lK`x*>K<&%I?3fz3X5?BMdh`a(eFI4*4#I5j z0F@MM5_M`A)bpvHi${<1>i>)z!H^0k^>w6O*_nG8+r6})Zz5Yf*lLEP_P9k zp}T-Yp~icv1$70LCeGJ+NLa#T%mGJ=IUobi118noT&AH%PYnv816YnaI9SR9IQ9be zhsPuA=n$Y111GN+Fa?SsoO@)KE(16n8XC08axD!#dLCty(+#eBKuRr{VkzJYm%2}A zJCL9OyyR^v`jD1jk5KytZ;nbCCqbZ)lpS)-jXe5V+wi%LMjm~w@TXjNd-^P?N4i1E z+$r7Aa!u>S?MBLO+t#@@eSXtr-JRL%ns4pc(zULqdr6)DMk^EuT@;)?sn&&B5<7uO+f`A;w1&aN8K6iSfjhOzCO*apSkmlke?CHK4 zKs;b~MCpzJ3vd9AoxlM=%^`XikQ_RW;S(K7vTT=Y8*Ri0-JmQ*A(=&xl@d`YPWi6Q z>S%KT?@f4M^qPg6Fse+BXhkFAspUANv9FdmVopHGCK69NQ~$ZcseB3$O)Iezi;N&XI1IKC<0B>;H8BiJaGlU zMhh3X_y9aZUO-$(7tv8-ZqV>XP=OtlN)ht{?j~YaU}FGeKpNKU06Yn;j68`~?yN=| zF$i)JW!1MK3Yl&ICkg2k120o zt(4H*NsTt*T6vhc6T5#a^+e>~O0%!g0$>9yq|Bosi^H0QG7C5l=#dmUfKG{;2Z#xw z2Z^MKMrRcH2bWU}si0t|^W(WqjW%MmFH|a=;6NAzb#L-7z_8Fi9m<-5!6+EdWNVP1 z^H4xVHcM9k6(S+z$wHSqRg#)g)q7c^MW9v!Y%bTHcBC!RXHw~pruViDw6$m7+B4a+ zqx;9*ujszC>xW%`-gRE*%a7(6a&g-X7>tpm+4i^2TqSx;Y zp&2%MPz7-Momp?F>Aw~38AN5UH13R-`nK2cv!8lRro1tU-@9;+GqkTyuj8+JUb4Tc zo$SY1$$p%f?8oJk{WzoLwNfjyrtu6VH`j53UVL&LKm4LPe)ffR{Ok+r_}S;z@v|?f z<7aPeeY$jO5{vEvo5iJd{Jpo4SOvxiZD~Yda;|G z*dn@spYSR5{P6mk6(_r6X~}+^n(W6Z$$p%i)Kdf9b95{y42?o$nD%)-w~n9PU&qfr zr;eX}wx22QTXy2E>gjd))YIznseOs<9Yc{DZ^0c! zk9K+)=}qjz7ufYq75aZ2(s@$%!(E*nuWmmr`+?SLo4=FUoBCbTtGQhD=fE@eRg2^8 zK2VrCx%$GQGQ4q3!6PvPNLVzi*7A2!+DV_YrO!PT^}q1|P@0-QM>ft;@r~}Emz3_&W0y;h?b$Xh?|6+f z93SZWrZ!Xu`q~DW%4qYWAwkz4#bkqaKm23}mvp=lnzDsGg{jl=hh>hcl$l3u9^DNg z*kBuI*ym*{DUqYcPM03LYyp{J!b+{WPN2wLsnn@_!pWGpRa-d?Q zv559r?Cl#H z9UK`)b8kE-7AM9yaSQSN0oH>O>Ww)1pbjpZXhEqVZYzKvHSm9^_JEcNcn<))u!C1f z>jTSDuyz1QgSv|40}e*p%yz&@Lb<})3o94YmPn0koE^|A5&&5!m|*EV1Kt_1L7{Hu zL&?Gj50w@zcX6X(PJqpcsSaq+;$##+Aw|)Wi4JNI?Rx|iLIo@-2PD%%-3YxmK!os) zJYdl%#QH(*>_!`Lt;un2UBivogB8lKkyE3jU~}K_VA7b}8Wa)E0GLSNVvYElBg>Ie zBxhnIPN5aULxgXg+tO$wh8G8*T^A01xPv000$Cd99Ef1wQqbff{CplKPFWVWHc&v#vZ?vP!3ySAVB~rmZ^X6bqq&t0pOXpmVCY+U(OJ>)dja?h zoy`Us?IVrB3_u%~hgqoEV#KP$Y!nO_4;L=Xh%F;`QKLrycr)I1a2L^P#F7o38oG@_ zywMDRKtp1M{()w%iemysMK)AVL_zSIf*mgl3z(eR@DadVnq`~2Y~rSdr5`sL<3H|r zUW=?1acwhU@3HU$q87AxjF55tM#UFCkf!96Mvnk&CP+I07Dw$oO8bhRr%H`~ zFi9<^FJUcgnVj)3=);NvB_I{*GpoLvdrISt7@yBPS|&ajs$<~xjj2-=@fZ#%WU4?4^p z>9EOdxvKeQe&{IwH2PB{ypZXH(D{J=ZyK`6P65M&rv>Lcg$_uRF>qku4j`ojBM_aL zko$p&mWrrS&S>-q;IB<}MELRsHtt1ONa+YE!jZ*$kn<3%KsF6o81_OKh9!$YfMZYt z;paq{)Z7J)9sw3lS2*a3J{acD6gjX_wBU^*vFkE9gh40+g;B+mh5!+aQB-Wvcy_52 zd8(N^ztJNAYQR7*4T3n`K%uTKGz3&#lsT#QSbk^-7d{GQmdQcMw-JfQdje^H9|Emv zdAWSY>C!u@$aN#h*ZGD5XJlfmFij*v3k%{t=N-+E(GFH9S;)}#^OWb*yu-~w2n)YP z?i{-4h=K|sh!BB()sW0la}a6mbjH$W|$5H+%$CMvL1P3hfA@A#j_=7KHk?kg5-a6*jw?9-@Xz?Hm1D z#K1Tisg$v0oY&Dg$eE2E0n9AX1Y`5EFz8TFyD@|=H`0i9fFLY&JfDf6lKf+hg&_pO zFjTOSMG3?cgKKE+!bXpPYq+#AOb1s^v@JOMA^S!B)^gdbxb+;65RAl?M4=3VC>(@% z>h|Q48UngJB}%U0S3;Bz@oB_yj!vA3UOametYCIB&ooV(O-Y13nL;2VXJ4aC5Ul|m zmvK%*GT6;ouz5b;$XzN_Jm|b2daZFP5E~zKXxYM|7{zPEAvq2pyzk`kPGk|p4<^Kr zgWWxE%ILs^YChLr_mh#A?Ea1A4(D8)usG%*Z-vj3hUlF~lM0_;v^(-)qd(UiKd5Tu zHLR9|EjNhu za+uc<@O6TyElI@}_piLLD-#7E$WmvQ@5F#CwYJ07 zBm^V+$|t3-d}kKOGL3K^A1#yyNA?wlov~O1Q80Bzb>JHX4_d44+UkS?|K;yXUw&c+ ze7h)^e{5{P?Jp#Z2L*Fe6RcrXLV^C$2c<84!JB4HL|(;WTK5l* z2gC6Y{_&~Pt7G0U7~C4QRaYhz^X<}??wDOwK!Obqj=Z`sByBp*#)Cwy)F`>Kg zg_EQ&m>i&wr8?e?vbWZ1 zaMqJhyg&CB(&rwX!$f3383_jmN{7j5o;9_(I_@_$$ULuAGqyXaxL+)NF04>L`v-#Y z0v)Wti$pO7CLDw$#siS<;b1W2$EKAB{r^D!cRi)k>e$h~r_Ifdwcg({(tLa7n)F4f ze(VANKmBu{J9}VdmE?Q9yO#YIL;^CUi^z|>(V}0Nsfxl|AX@@lZ2+>zw~7k3OMxDZ zZdnT`@}a^_eM@-!PzY1$^y%1i)7$|9My(TtXO$iwC{|fDBM4Lg3eGh07>3aC(b4(_ zA17>N>0Qf01)k57;Vx)UBWUUdl*ake+%>;n@$eODhM>eS#{@JaTyygH8iQhY&^L$D zkhekyqK8cr8Np=qL?J#PD+*o?uV=>za##O;#RHpXe5tVx!|@$KWL>6uj(e1CA%E}E zCvgPf9hG$90Po_Zt_tO7{1SlDQg@c++?Bsy@rZQ(Wu|^eOBg|HJmgW1hT5+o42`u8 z(?U3_L++u23~#+7JxoaD(s|JZpwecBuQH#fE5f>G}JwEQj!aYD0_%{a| zc>L@wl;ura60qcQ6nb%>O+i~Lxl0>v#3*Hh_X9(XPa0?^C?clrgZ!gG&cdMMG$um< z^4TWA5n(1NoK=L81TA(IPxIW)MjJ7UoDNgZvi+nyTH|(#Fh%zj^Ky<-<)N?x;Zp5rgWFVyR zsz^>a^dQ*;=#flS!PiQ)g|&+=EZ(sL5n@@2!=p|+9PBo@7bUl?(MAjem*mUDuZWaG zczXq&;{o*76v(XjK7icDz78`4T{lJR3w$>IpwK%UvJ&?4+!ezAuUpz7^>lQv>gwo} zTF&lhYtN;0X-CtA=}VgBw)Gi1dva@UOaY*LU$#8V0eK=@_Ud^~4u%KE3!yVOL_xy~ ziox*c$bLkMc$Sel!_t-m8&5S(t}dhx7Hbday$qWVyoUn%4;wE|EV7O0g6;!>X5v-a^A`lC~Qu8P=z4m8&^VWtA*vp8u5@Kyy2iS*6G;2oMn9=BSQ zYjnrO%o|oGBpgoXEbW4k#^eqtOvqHGLI}>x0n37w_r@DLI~vsoZapOmF??F^ns%!cl#2|xH4Jj0f65Jo; zJZ#y|g$+5~1Dt$4IB3WcvA;omq5y}@Bipa=)8zS)! zV7yVMrlB6IqjnV@O)Ol#F0V2K{D38;AisAGLAT%ED; z=CBjlVR{_wIQ%lT!v9?u-NEB;959qnQJ{_j0&{m|&>Siu{=xBnnn zD#FXnpq{Jpkjg?7du!HCojZMN+RAHFsZxX9np(9XlEE+D{}=cFYs{Ug-Wsa5#Aug@ zy*W)&{nIxmD{fBRP`p*YP<7QNDM{g4$=V6soKx<<;AkZ4-Kle`A-yNp>j_Di8io#9 zO-foDl8L(XHB=tn#m^!;U%8kNwtW`UDeL_+G>K{s9eew09_v|W; zdWe88!o;|5>a6NOH;N?y>X(3i!>9%VdJ`!;Y9ZJbA?!nc2T+fP%?o8@ik{R`C=nUp zKSUvzA`7RrTIgd+fu)3dY)A2`X6woF0Z}|bz9PB!0ri2 zc-0DIOUi}r9e*Lc;|*KR-@K*&PHYI`Zp4A1smrRPTsO{F!cndjN|uyC+B;TC?>J+6 zr4gLoQvcn~&_obRd^vE()Q;+Sudi34l5o6hg>@xm9`!djNPlyy@Y@}9hGOUR9aGyI zF~+rm1(T|hz3nT~+rD|Y7!OTdS{>s?SvP8hk|$M;dE1Mmw@of>-Pl$g?}k}7Y6M3n zRE?3|cBAyR+YdLV{i#c)W6ViDovz#QCgHWYRv>voY2O<@DZSy}O8ZOw!M)z1yypi` zoZ32DX-ub`y6u7WHDRh%3N0y>g@^uLdg!|q1GK6d(Xcb(3HCE1~D%IUkmu^Yz(6V=&@#^el`EVTivsB^zY50a#$FrM>>^qe<3Bm3jl8o~cRLAqb+$#iGC zIy+D3IH`S2+lK7s)^l3UYrZVAC+($%nkM*xy8Nj>=ql113JSr!W243K!q{G?1VRpZ zU%dV>D?|Dhgg+j!c%I;B2E1Lz|H($s9xY@CR**14hWCwkjDtBC$QjI^grx{RLf}Q= zv_c<8QWlOQrV`Mi4M3Urg#jJ*u$u8b7_mOkYSdQIo)vClz>jkGHRiBknc$N`Xfzk( zob1APBq|65=%7gU4~Bx_ zV7#~=2P0V);C2w)+%XM~3%5w%n-!=o@M1i8{KE7G?mp}nfvFOP4zN2t$bMj^0DSAh z$1iUJL4pjy9L5sdQ)Q?V$R_ig0HlOLWx#KR`O9o~G1dtjaOENY%?+3D%9d}b`+%co zxey6sU~&W-KQKKXd>1TyWKZEAJ5cmN`q}};C(t`Ecmo8b>(GVJxB&1Dg&7$KTnq!$ za6!VE7D&nf{JX8P7{KHg)CUA*!O6O?`xSl&nN=Kg;5ExG=xIU=bqKmuZePOq0RUeL zOac`ioZvqCXyUUxBWw`+()X;a0<4gGKEzxe=I zGz@2WAW#|^BIzx7XZ%hWG8T9)u2zu%lHtOQ%lVBvcCsE+9H4H>$em+9jTIcu7m)i@ zhosDd?n#mW;59FNs|<~#0A?^cPo`tyTx^*+r*X%QJ`tvc0_u-g2jcfQ8i~Y&t=fg> zL;|NV$MYVUJ4#LP-fcLq*kQnh7t}}D$_0%(_B`Be18FWJ%*&KHpe(9+B$`k&!_(4a zrE?G}VT|Oh06-o5Ld7|uy^W~>VVg+H&l;+*D5)YP-A)EhXcR%=|zkW4hzo3J+ZMK zn7X+-*1h#A#?~X&wd!0XM6ek6*uZ1gPmJss92qR_6^7Jm{v6pjN0s-;`SWD+JVWV+ zlYR4nv8kJ?Lv1utsB6{MNT`=T@W{ZU)9Z+kiO%BZZ+s4%_?btM`Y(USaS)sJ|17f!=PMz5@}TqM>%rTB-KQY@P$> z4qP0q<6}WFgkCy<>T^7o%b$AMbi5;qMBSjA^;pnrCA%jR@8ImfU_HMcKKoF7lJd-{ z8@emJ`e}RmX6E1Ue-Ovo*6ow`*6ouA_GZf0 z)$$ZaD^aO`TLkvp9lfgLiv5Mc*l1}SKrUy~LVEjhebJ@MwgNiK_Dy#z+cy<%%aqTr zc|OeSOfK-V0^oONxG;hj*s9y>`sud~WXjjp^1H|ENO)B;plG*3n;}^FhGVd5z4jP< z=WCX2)_HYX-|k$tF@($ZO~JB#lTUKHW}zp_&y;sAbe-H()MfgEq0(3|zPEtq=-|wY z{+U$oko+~uVp%2j4~;t5cX5)61oBpm)@@RZ)a{dp>-Nb*oHD*zbI42ml`VSbi0C{~ z#N3Go?;$(&)rm~6Bl)JQX1>YEl(*G9w&_i$GM&Z|9IBd2V>92hBt^DEC+v}o>566h zrk%_7O_wL~xR?4#3`d9V4*bd-JY?Ekwj5uwqtgE$le%BlrF6Wx-N?SJHQ)SCnHy4H zYZ`01edd4CzN0wPANo%k4n0{bU{reR{2?FMHx+hAmI+-EUf`5^RlcGMe6>7LQliqe z3M(fB8o1+=cYK~Y{O*b2VF%EAT#K(b6;y}2z73{&gu7N~S3;11JO1j94;(p0V|dE1 zj(UAD8uf^}&;qXoNSP3Lpzy)Mr;iAzF*M~>2fV&Ojd}#UR>X2b5Q4(X3sbBrhec~l zOu5zZt}j}n9`UXfB|Vd!)+o+cxTo+(OMz@Wn3{5?10OY3>kHYaN8oFPDJRoEDx6lh zY^K2gK4Yn{jk~7qn7#{9ceU|$)mq6R$=rq8pDc78F?Qp@^i-i5t#nIWGslFzwrX`@ zX-g*H`<#8xogSGmBp4d+FVeEcQw-T@^ynff>ciwi(8GS>ub+`A@1IFJju;^jj0_Bp1pT8$x)mkb>Qwlsx)y`}S!G+UR7tWd zD7v4!_*j1V1;_Hstz-G+=CS4Z={#rpD91HUgJAN+^zqDp;6BnDqR%e)c1~# z?vwts+@3kG@<_k`Sk6c7Sbn*BEWcb?(mguZTRqa9e<*N9CdLZWlEYPVBp^?~Yk2zo zNM19pNZ%-9F@9Oz>*VlVK(eu8Zzv_8a z&kK6)?-}j6qvyJ|E88w;JEv_^XIsawJ3rX@_Uu1qAI#p8m0N$*`mwHux(0hL>Ct-n z(yLkzbg$|9NOybJlU+Yd`RV7U@9%o7>z!SXWZu$sM%UV|mv!BnxhH*ET5tMU=Kbl` z%yT;bv-30Um$f~X`SZ@(nx3E9mFnxfxby7JH5sq%=8n&HyuRbXjypT9>##ab?r3TM zLFS5#l3A1fUHUuee@?%%{qybbZ+~6;^V-MS``cHxJ=ykX+iTk%Y8z?(U|Vzc``OQB z|0?^E?46x|-ubNNsqE_3-?si+>r1+Jb(yV$t-D&y)=jO=E#GhXT+91f{<7t{Eu$?@ zYq_wcucf2;r_EnYS=pzy{#ElIrhb|FuhbV(?M+`w{atDjM#Lkj=cQlKd{^^r%~v#i z{?Pw5w`G2w`Bvt!%!e|6lX)m}N9MAOo>`MgrN5v4bowpnhu5Z!)HY}`5#VD&kQ*wP z;GWkteO~IEp){SCJ~K7*_X@pf^HWj}NX^^g{(0}|vG1H3_s%IFU6npJwdEsk+I-<= z<<^lc{lSO_i1U=~vHzfRf#2&}tJ6yAk_jLB4<0IRup1W@Mz#6A1%9^BmEM@z(qFMZ zIQj3RSJP$)C%-lQG^s86W#YcRSB-n;l&{3T^O>IXDboQLY22p&IOO%e9rF5f-RYB4 zTX*feUQ~mNn_9%nEoV>vk{f>;y%ez$1`ZpaJSDw7wPohz8ADW)z_-X z0P=k};rEWO_+`hdq#I@?ay8eGW&3@xR!U$S?HY;zQQ{vZgWPwU}z}tFLlP9 zji1e>w=m_ZFMUc3Lj3zZ2;Np6_>IN+*EW1*Q~If?ZM&bgdvv1c1=}KOPd^?|!v%lP z8(;ghn8>cllv-ECytn#WrPj>K^f{?Z9B$o>w9{0wd#l7*;xHtYJ?u{46ys_quac@jTPyN}I zwPnpqii2@Km|LH=Q`@e+X3zCox9utHxoOw-!qr=M_3sPzm)4vxXTe(29rqbsbAPTQ z?&sR$-pJ1Vfadt$Y4tDX{OIam#C+uHpU?fdpT_;%PvYMA(cBOCe%#M}FYb;1j(g)f zb3gg^xi4>=`%<3!a?|`}Z`C(rzG&6Wac|rh_r{*MH+G$wzCo;|GtRnugv|N2gZlfk z>&mK2;y&WyxHm3}d*i$_(z~m_R4n8Re&;!HpKx~E8(U)DSozAhH=Z5$#se{Lq#uua zl0+Fb{P;>_26Agw1 z#}(ZzZ+!JX9`R59-Q4LqHTUJK=g;2sUoUX&tqWXxc~_6r7Ui+gn{)X38~!L-?oOXx z?*2R`D65~0d*fGgf6q_Mefj#6(r3+?hmfq+UH6pqjR!9Vmj>Pt1T3-5v(rXVd)naW zP;?;`cArkjqeLY7;F)A&?Rj%vuK7~TueWA>+#4(B{G#jrYtHAbeczmS*S>1*yZh(7 zOx-j0&<9g}&(aUoy0Z!fC38i~W<;8}ZepCIU8~UqXt$%#Zt81%YrC!kUMd`O& zruV4U=gs+LJ68AnRQmD3WV!tVb6;LG=VjZ6dHIclKb$?l%f}ABY+Xs$_#?9~Pvqr0 zXJ7X4@{!q>XDSMBnfl7ym#?1tvU1yJjvc92%zgLnxi81(zAVmtIYwZ+ zrtf>l=~a^;bti-C-ZK3Fw!Tp4|4%hNEcN`j=j%P6?fGEO-}Jn;=S4ja^o()#-_>(* zkKD7iC)NG^?k{(Ly!+kVQ{8{iy{G&9?lZbOyZ*Q9>s=r0dTrOAblu-I*tMtYg07Rh zQk~!K{CMa4J73dT?%dnCyVLGGsWa8_V^9PC)bZYqM><~8aaYGR9cSS(@SFA@wSTew zL+x*Ef3W?g_H)~t+rHBFskZmDy{hfOwz0O`njUVuyiIFs$^Ibw@7a%K-=2LaJDzp2 z*JRJno{?RdO;LULYU?Lj-`)C3bPD#iUf256*520EmhZKErschy>K|??wcOgWy`{fp zb&J&e-_4ITzrT5^`322)Hy4^OZ&sSuHK#M*&pe*_`^+0MFUsuC+?lyGvo&*iCYSzg z`g`dwrazi~NBT9?F!raz^zQV<>2uQS(yghVr@oQ;m(=@HZ%n;3bs#mGx;3>kl~47i za;c`KA2&VT^sc5?6V9XjBhC-o?~N9u%2}cBDsjlZqBJ@(v|X^1u8&$0L2<)-PEK!4 zU9tPxYZg8*ta)Nx+DyS{LVgKqNpvvZ=|=~zjpv+@?oI6+7G?<1g;TSKUT8h^LW}st zF5k0f*Y4N!}?D?0>`IprBm*n}E`sZIdYyPD(=U?iZf9cfump0G8 zw0{1jwev5nnSW{J{7Wazzm%JQX~q0Y9rG_W&%ea1J@gKx=3UzO^ZA#4CN}8Hiq6>H ztw`z!JZR~}H!g5(kC;&WoS<49+$^K{)BCmQw-B727}~veV%#6Sr&7Dhy+-UImmxEF z4ZCnIs;P)q(Egx+%{_?_?_looAj=`Y-_cMr`yDA*o-N3$r^)FuJy))w9 zTX%N+duu-|3RmL$Sh2@OaqQl7cJzyJ<(kp)9f+}2*4?$0yE5>M zEiGDd=3oBmsp-p7+n>H``?fvXFD)D_#O>a>XZ!B{K~|g6NgEIQBkA(XBk0hqad$;99gaKE*K3til@z{NE$sElibg(q02iBw-a_oG5? ztg6ymbk=ExPwD;V}EE%K4+&ZyR%zp3H6|>IuBMV+$cYD-06xGydYHjX3 zt7u5`g@1Ge)N#n?>yx7TMl{s=0jdj9`gDx1^>T8>J|L| zj*ds#cVxfRT5A4n=I(T+>BU?=Fj7_z+&OjEEE%JYZoEMq%=)nOu~p48LgD(S+CH9r zrf;jE6$sBO1(z_>4pl?JC4`^A11rQC!g4J|!9K!}&4hTR(yvtMR~PLP_Jd>>MfUJm z>dWZVSar1P$E~YFv`tGfRRo}24Zkdd7#gmlh9Q1|VW`L$t6|b%ShndKvW~ZyZ3wR| z4a-E!mlaz!X7G$PvPz|2ZI*te964QmWNLIe4pkYA&IoR!z9;mMPw=~GR|6>s~MK=c&cl#bP!z~&oRX}`2p9(CKBf>(Nzf*2s zzUnB~Z&4Rs6A4DSRx{Fs_Ry_&Y`yEqchFm=?wk&GB+F1YoAgN4D8XRYD)E%eO0sp$ z)(zwP$KvVG?$qFPc%x;yzVa>!hPPJ#(PYAV@n2v3s0;tM5QL~H#;(f`wodJ>7InF) zuA5;!v_)#Q!b^zW@1?vmO4F?%@(}MRL>9uSf$CdPUy+4++=^PA`4SQVaG&&=7FAkk zZ+f}ZDZNgTUY8l_THo2%aZY=_?Wx(zTdxCMdwjb(b+m=cVA{3s(0Y_v*Hv`#rP24C$EpKR{tZLYkVxjI>cJ7(Yp7d?6 zt|sbrI2!pF&*CnRbu`xK6jgK!gLu!kG$SxPU040QEUS)cVpi&7EFL!Gy{+}cof~b) zVK4RGdN?YZRuGuB8`@!B%{`+bhmBh-CdsOSt+20~vXu`has`-)d%|p%LW`xzx+-I; zjKwU^x+*(C;Dpq@bi;PNeD414ob=0vdy=VTc(y1~Qr@t&z%x8Elr3FTOwG_VQ`n{p zE9FoV?%|=$CChYq_I1jqlsfF(L_^-~&{bVKz#2KUO@=1xvS;Wr*3s11uo2G7UYO6b zo|!Iw>ZmDsj*Pdu<2rueQZ#mR;|)1%kE6Zs$+n@gH_3_>XbR@>0RvMp$PV*d^K>KB z7_^ypS=cdAHWkGRUDu$N=;!Wj$YDFKuHgZ0%PO0&qG@?uV>G&=8h$=-0zH<;#jVA~e8P2HFCxl&^eTg4dOa5Pg66;-Dl;rgLtD}kQZJuR?xQ5v^hCm*^d zkAY%~b({$i_<6@=l@D|GG~}?&Ag?G?tJ$sOJiRa;V%Ds@>ak>p7Hf50Ry4!6LtnKi zSzEf{P`CEXz_eHiRVVicjXCTZTT!{Oh8%XS{m$H7lGOCWx#>!IC|ka6IgTPIk&;r- zr_ut$pi$^39{pK*Dt0K&QR1NF`Sc8R7A={i(xj2aKCRi7X$B6@v+bx%btTBFvgy## zwLI}}e8Uez&L1{?7}*c(FyNu4;p;0wX!)id%63SyO8yqA(qoM@T3KVEGA)-!R(32? zq0McZEM}Ta)7AG(*LDNL3S7F~hM|&}alEA2s=8{Pr7o{YpR4Hq?~=No+4;MUm$&D# z54Yx;|0;7q>Yti+Wsl*hKY_x*G5LrC#YcL(%Yk@WqF!>{l%c0PO?OQ|W*R}e6rbhM zdwiY(#ng4pkx{r+Nm~q6(b?S{TQ+IRTZ*iMg3wfpr^9zWo)&t>cEG8H38TrL?C6fe z8j`10p;@-Ygi;;F&a0NJ@I1MO=a??fCOb%|s-(hXiJI7148xTbH<@~E(~G~?^y1$| zT>PgA4nqV%TD;j)eB{*Ws7L+e#)`V-YQ#Km+a3ogPLd8QpX#}x>{|d-NL5Hl9NA>2 z=HL++f$or;Ix-0;H%rTV2}S)y7dE}(jUOI%0>$Js?yXt!H36W!G0z*BseFuA@iwhcCsP-gCjhN%NZ@qERw z^E&^~FgfXPdiTMlknMS)VR#;!1jjtjW|OfD+jJdQc04bX13#H~U-a-rucU$wo16^WEbv1Tp}ox21oXlG+DJonS-fmD^9>^fvBp1$I*Zi z07w#(9Gcok$bu6puB$6#)tnX_&_2FtS-R^uz5oSTeyD0XMG9_%$K}vx(NO}+p`0O(b(S9FJvSlail*oOvgvu9fX?Uh zg5u)-VCIoc)p5VEzN<;XacA+7na`}?u14kGV@e0EV{5)<+b%0fKF_W!QxDfPU(`(i zV3CtqM4vK)s)h++Bbpw1qUpICE6a*^ko3Qxs&VXnNI$Z%`aayyQ1`)eL2!D$llMGU za$lxWOIqw{B>j2IRLy+g`D8jic@I-xfU;!Mla;*3+_wWgNC-XA^z7d@{o$!YqoZR5 zZ~Wf=vEnF1ixOJX5uxPn%9>h$qcj$G8xFXSY?!_cQ6kw`t9<}0&NhI^8SDX6Pb@Xh zX^gq<*kMSGjeR&zSxHkhpWR!Q6~p(%4gp$Dv-z=Pw%Dd;zq{$#?^|@SzweRt(@~BR z$GQRF>awR2-+Z80oR&=*i1ZCP^U-0WXz7s&=~_sJWOZw z;rrbgjdr}=SuLJ7BVGt3SS!u?>7IMvz2~0uFZJA^DRd2;BUkqW&DJ@mEfW2Nd@YnO zJyH2mSf7IVx^nHZL2tOManWiD>i7Q_Wk#MebpPOs297EGb>F2u-^O)k*C zrw?7azB)Qmud>yW`;KLu4U)z0K&@?EGW$>VHHJEi3XPLk&#m7g6knFfpiBQ&x-YK-eQm#of=IR2j$7rOs%6+T_C4Eij2D{bgyv z0~g4`>QHmwPfbDK37~4(x~z72flis8YK(3AE~S41ya5wH4U&ccS6iCUoXlj8Wd5c> zJ)DF{3?~%$6aP4S;u~s!^A!SQ%k>>s2b;s(Inenscq@TjJOs*M#w=;7EnJgWxnx7a zj%BZqW}rt>ZU??&a!=K0fdg2JDV51GgDGISXhTVb`R442FR$<7HTpz(OoF{zMmnet zG)Ma66f9^fmL(aD)Ld}ianwNL21>ayhf#=es zFaQXJwjosE>D+^8$I6ba!TsMNO95#i1pOw1gd`AQLYJTHaW`gSA z!Or37P?mw?vFk>m#j4f=L0SS@30y5mV_MrWT|64RG&)S7NkXl~NvL`e*U>;v2-u%J z=BDg1?{9Dp>C5@!@zm^&vGNWf=I~HNyc<|pY&md9u3$HRHQytU;9b5pgq zc`~j`!DLWmp?J$VNoidT9nGUL1Ts=+CcIm&_yL$AYQJ=#oRI1ZO%5CKGYoSrV7K>0 zLeT!~#-8lP)9V||G}1TCE_QxQfumM?8DE~QRueGLV`fG(S7!zf9VrYPIkYZw^=N*0 zsBm203H?X)o~B%ty*PJX{wzN9TmGkVW1;SH-zcai?b(MU((h(Tr}+BH_Cg&2(6O=C z`k@JTFxqX<72lN3aktwsE50$E<8HTOReVD_$K7s+s3v zyG@eft?50>K<-Bp4`u?=@N7g`EJRp2Iyu463C``4$wCSFMvBo5xsc$JBBYH>BSHM*bqI?qcrRtJye6@ zYg0USAndWD0(0`gG*K3dbyz%HA;9R+j@3bWxC|3+p6{zZMN0soHo<|^K*`69i?2!V z*lpTtF>+53I}9)xT#y8CcLQiUHChv}Z{Wk>@e#p^49zpSLPf$1LpRb?Klc2fcuRW6 zu7TqB0A2xjfYAZNR0Gq=Egkd;ctRQkAe}wkL2XGDfuyuRXNvrQE4&E;!W6|Zw9d{uhKPF>a% zI?!(#{Zwk{V~3IF(U=C)2)2UGHmz!~^lX7JUQ`3XFXS$hu7(q7+*gaQOz+q!OdITQ zCiQ67XBjic@SAAMJB;2Afr`TE(W3~47eGTu12k+T5Y9G2o!YBid_{W4PH{n~@W@%& zV9N}l>!I$30T@-tOZ5NTAOvco#kLvfy2D!6;{e1fQg@n0YsxGB$w2m{nWM5Bvjg85 zcxwOW`!3CH?EPl%(|Ydgc?SCUH|7huTXH94|FZw;!o7usz6VENj2ghLBPR^MXV@P4 z#L$IjhrCBJ($WU(B4i>KMBn`r;pS{fSTS`V+71 z`V+6|`V+72`V*hl^(Q{H(vx2rT=gAV*g8Iqr2KDd3yzR6BGWaD$FzWtG0(rmp901z zH3r9Ps>Hr@sVY9@cjMQ%s*_t-B%~I5Q`Nm(yz=IJ<)%&rzhOoHaI1P?H(O_Cw-qlR zoXM!a&E#*-SEf7l>(-Ro_NS~=d}i0*it(;L@flrz;&qjQ{L*2an$6aWuZ}it{G0|L zn%Y}_hklpKeusXSOUaL?IyFW6zHrm|%F{dgtR|hn;^f+FC0p0#u@h_a*m!LoE7s<* z(b_yVT${%RYx7vYj{kpNX5{t5hYY@ZKrMW%@3B4ql%LK1TzTGa+4BFtk3X^qEG^Ad zdzwKo=cWJzLnFacV`@xLTaY=^K^#G$qkkf3C8$)^BQ*agGJ{hBvI`i`fQY9H=)JCL zQhe%72xYe+lijef4uaV)#i|wVWvN`vH^=;<6vR9>CGZ@{VnVF>&_+>f@~CMO-Z%&~ zdN;0ztA*jgd56vj0~J*t)1j#c2iq5JLKN!kk$;dq^2U11CFFk^!>RFQh9f3EY?wA)t+boS}weNDPb=ItJ2!XeI$8rH@F1HLz4$NKL6IL{lbm zBM5m3QL3|t-ibEG?`8J!#y9Tua1JNXndb`g$@Q1TNc`S8nPo{Xa?Y;QK^$;M?Fl4M4df+SN8DdUCc6GcByI^<7ZWs z<`{QQF*>;z(;qVE3rYYZeG;fdHPs~G==6+acg#lW&_?Of@qL{xF@RIF03lhZ=vzV| zbsJZTHZ5ARhaZ_e{Be6X5qxiF%BXWsOBRvD^0K3dcuE?6wO-3x#2`GGoWN3vgx{xNa)x+ z?8NM0XV;@&vzDwwDdlc138NUZwOsN$-}ap4nAZr?9a9HLFy=_bK-FSbf!Z6WU~mC= za6+=lm?(H6Y{LGIpcqnO=shA7BiqC}nTmmtPJsAJLNPz|&DldgyS(DX#3bbZ`SA&9 zzB@iM8;SC`#B+*hLbP7*{@ZSDwuUNoI%jF_vPu6em@5R35b3^))CIbz;QG;ec1%D7 z(hU5{@0bH&O7mXE#qo#{@7MJ57?SyA z2anD?c>IB^MFzOD9+J^B%5hTXKcmyvzi{x{fvNtR3xC}AmfpYWxh?;--1oD;&WSZ1HQF{=xy2WzX|(MY=ZZ^d8g08;4aGl9(`ei6 z<`kcws?oM7EK~fWG>x|1f^V^srqQ;m{!x5!x<=bxYpS?=ARA{!GTXD+siEWg-2qjZ z$zR=jaqby?7iPAP76wo0*;05yHXd2mxc&dO|J*;fqT@``j;P}!rKU^eqsX9ZLK{28 z1WR(Oe8Z&AiX4PWHxTJ$P2`m0(I>&9`5`gd=1A$d#yFQ{K z5Cu^gfoOs`fawOpv~UYE^*9zB zLWri0UVPthC7mO;FuBrTM&kWM+nHB3bfHqxiWAUB9mLX+N3&vS|L1#pgyK!2;2?WqzcYQy2+$$a5wK>Ou?^Db z-$JiMSGo8X!$+od?D9N=G#MCSAm#kfLc`*LRvv0n057B%AnHasCUCGPWy?>6ODQxR z>0{}l78u3%r*qDITqfAuJPpn_zLw}nI;b>hYQRHv^6U_!kB=0rokJ-Acrf>d?RCJq6*)WUb4H+!Jq-g-1pib6r`C{64%-o4{=mU5fOO45j4>uQ?D(>Q4L(Cf>I=p1 z;-9B;+~7!XxQUDq2|nLOWD>~-j<%4+Y}y7JK16wTJQ16!#{Z!m&p`ln81$fNfGsTU zN$0qwFOY|`0}x1{kDAa(OfBrTeGX}WG>8JkE_e`I_dGjft|3lib%}Da)NV(nu=qEb zjPj$7YW#HMhQgBdaVoaR%q`6#9|O|jn0PQCcYxC!_ZAoT9lSiz!6dg~BS6m>M_(7a z7=C6H|0*sf8s_UC=D4y6UTvf+33)|IsN=P46`QqWq7mWtQEu5W?#^Xsb6-mbM?9}1{u;o^; ztEFAlBb(>IOWz!{sW+97EU5B@obrYBbuL6W>raVt2$r^P$kH5n4t~c7D4lx& z+7!G>h>oIs&Qm^j)n)ToZvio^`Bq-k5W_vNKL2F(@a7p3n7RLPvWaTGnOMg6A zB=1H6T6%=EoQz!c(B{eLm?cQZCZk>9v4mJF%I%tR`{gTsERd5Hrx&MuBr58M!GWDw zcK23G&C_sY>NcQuHP9MOi8hHX%59e_x82YPx#Y%4PTa%aik2w<-Gyp#c_wPCmh|mT z?JBY*!?#dAeUb9%*+w#2UYMPKxc%FzY?rut$nsp&*e)Hzpm%I@YF8mHp}F|v*OX6w z5Z$?8*P|2pzcwa%N^-=E6+w;-Mo4j~h5;cwP1^ z<-C5i=d4~ce_r8)zSBnYBLzOt;y;c4e&e6RjcqjP-fk03O&U)Gewk~{R{mGuE$!MS zBA2*(Q`>_W${#)I*~pwyxyO`&Ixij!*iZ*TrB@wL@4^RJr(x~-mI-TJLmbyd2$-5L zEe_0&u#2GyV0$*L5DTIttVl~0G0aFfV=>iY`rhg6 ztr_!PhX_=-__)Y--eK9}SxjF9QCLHx0b)b)_b}wb%A1ym&_DnmP}bbrBi+cbW?;)l~Y zZZHjUtE+AHflALi@C;Xj`HRc|uRSA24LX(@@iaQw`h8DDD4InWNKcJ@9n0Xb%>vq&$c2yU(XH z7DDQ~Yv2_P3!Ija2@&c%zJ<-870@BJ{8#{Qzy>llV$gl~4H_Z}pml2zN)dkXW9b}s z$PVtK#G=yn3|*7s97I!ONsM9)0szlH!=XbeQB9bpJII9#lw2^20JCX1mR>p1l9vV6-Oln0?(J$ekjK{Kouaj zfK-9*vC8=a{w8c}p^73tmJl)MMn{nTk?RKFdqM~K9*iH5tMcOFJJUJefHoZVE-eT> zFSK>ETVe}y0}pHiVq8wl0mn%W;sc;2b_XurfVjT$2H;5-;Wk=)S9u{O#Ebm48&C%>9|?779JTdiZ}^lm-s!+a1-( zT~@sMSn|?!Rh8xQ(a*O%YU5j%Hw$A%{TIQFZZ zo}}RM)_mpij*VciU|r@5BX)OQpwIK5uDn2>8?n3d0^Pxby7B_u-t`yg=B__+N7tXY zz3We0?D`WIy8gs+*Pl4w^(WrY^(W4C{fV<(f8xxxeC25!JM!eAy?`mqv{;zmRh;gs z|H4#PpSBHl#OFI2$7XMd>$^I(HFC1>*GB*UnRWdCshN@23~wHM@xZZ#J$;Av{CR#; z_VvoC2X%o@_lK5ltjvs5YD6EUvObqlfusVSRc<}Gp~PA*YY_OcaCUmGW?v?y zhie9Iq9s6AsXn@S9xhMSJhWRG?@wqRUY}K7f5OA(AwURvrl$XV;7M4TtUjuF5{^&R zB(&R>Ns6BIx-H7Xy#GrrHDI3hp{ z@Lw@Nx9I;HF-qqMInWoQ8wwB9aOn0*EP$F659u|3ti0wEd$|D%v)hF93YqaI#Qaz| zFC;SkhD9IU|AomJ+$81CTkSzhi`7$_XQX57u#OzQ?IO;SA}8JQVda)Dmi?K@g~=OX z1%XuHXNU$c)58KONlBe_!FIzTTdf=a0(1OF1HYb@u9X zU+*9BZ@+t8_3UOk+(f5#pPkIMjP+_?BaYV#%326<`x&m%dn)U z{V^7~m>R#n%uZbYPRo6Gy849XIqBFPsAF>iy4=Gr$1*^ihbEX}>=7MbD(6Y@F~}*% z);M68z#@C7t^rpDo$7f|RKV?!&?mGPXa4J}ng9B>jQCizI6c>T6P~R;zB%A0rtT$h zO^}>!gpOAP!6h^SexmRc;IO4S2LC|HQ1CyhA4d`@s&PTr43y9?I5LH=8VPO0nIHI> zA53qNY^5s~`~z*r*it@jIpj6+XUB4lj;-<7PywIHg$%0#j)mZlzA8|Sgf;<2%@Zcl zOYjMhL`8s&MekV=@>Gy(0*8r8rrwhI!DX2rJhM*z6x+oNpdjgw-EwOwS0B3^_8QMK zePIXlr(ywW+YWkHhK}WykC~rb=_WFsKA>J36Ex)ej1XYAEmSr})dFiUDw+pJ@m`u!-J?oS`$84!9yj-@pKxw!2C9|jc znC5_Yj9cE3w7Ol@#tjJty!v*f`lV$_oqFA>{Pdo@u7^2C^!2}vtkMt{2MHAg>P`%b%Z+(#xA{eGo- z1Ub#tsmWlhyw%4FvQ@w-Zn@0?PgilPU8d*}2?qQL4-NP!2Uz{5oO%pwGh<7ph4h{o!UP>xRN zz)|rJpu2mT=Q;==VIpYgj!iFf z#xuB#bHg^6NKCHbMA8^T6&77&MBoMf*s6vUyorwi63LMtBLgf#J5KOFl0Fp33)ez9chRfug*a*xA>{_jve2M zSU7M3V=kxM`Ee-rgpcgIio8E-0roFi*fHcVA|lb7# zBz3Z7#md!-ccgdhB4lFWIh!Uc9N}}H?cT=BA#@xlg2+HXbyh7H!KmuGF~i2&0`ChI z?kumo8WnF(^VmIv>nT2BVE{dp4P0Z`s9G3E{A1eUn9!s7Kid?XbHcWyb~ZJz(1S3E zY;^JS!^O-mQh2yi%L5NlGqeEbfV2e#R?F8ZdN?|h8%HJ*5PAR?65hALQim4}HB_Z0 zf+BzxD@Ubw(%UV?6+fQhv1`Hzy%1KnA4UO8c=U}t=&&&0WAq=Wwo_q`V-xBMcHZ~&&Uj3+5+h-Oq7QBs zn3y=y&4KM52@`b90^KrE_OMV0h^SDI?)V7&sCIycn{9CGDSjrsW0yBk9Og*dXcMZD z#jO^*Ca`>}i<*alXBW3!t}eLUaENpEOOGxzKCpGd62ys%e_!waZ_1499L^3tyZ_$8 zw%%W&|33=;f72?L{@;@yFD+D`+}s6CUvH>gg^ZD8>Q|Wu?92mCJox6&4b>+#$Gu}k z+Kyee$e!Cux`z{r`%iwJd7!t}eLi;?_TRcMu~5CJIq2zI?CoCghms2Vy_ui<_rW)y zXR8-B$31;ei*_pu2{DW^KRGh)Yt@Yi->iNx~?--+{ zV`nLKy0-4~2~mhLKfXQlI^$(s@1!(G+#Zpc@`d>x^1c5URFZkE78SN!4AhaTVfdY?;wWvWoQ zypt{eCM7nTPOQ*q;9Iuk(yT}G(l=>X^0lpnG1}}>w-qU48XYk(NbH;qgVr{lm?5f< zz{`M_v>o@BoWkFN#bo5--s%R$8s>rR%HDXpVO`^7{nits-3u zUf0r094-u87YyWVDe2j$b_*B83cV{9INtM-!y2Zp$>R5HpSC^Pw|+6`ZUKmJ~?MYuu&j|0EuY$rJyeCtDcU32KEu?O(#?zR<7?? zt{*+HbtBu%wat-E*)|h+mKxhOrsT2iTXu{mIqJ=-0{{o)L5z^IA7jg|qRpU1{84ba z`k`TlffgFE?IctYR<@2RTTiHOGr{5{2E8jf4J}X8>FPDjfj>E2`@3A-z6uruqYDr~ z9H^nC+nPhiP$q=B9pHrE>FCAdkbzHzhrv-zQ=1+kB&8^!s<1Ngx5~ulB|%_J(j6ri ztYyH<)vKEWZlo*VmhKuMDYv7$8fl9|a~4?=%S3|TM~@xGSA_3Y1j~;8C0V5d5J|!h zZ3`|WQX~nLgO!O}l!(RwSNuRg6g>{`mgj^e22<*lb2ROfH9YBz_wW0ir9gu7iI zv%?Y!_gj9Zyft&h?2dD5QeLeQ1?8&M9PEyD{W=otb`7KsO)A*;C~x_X-=^cY6jaUT zu$MZO$8;p@ZF^EC74}8tEj!B#tqJU5)o6}#$NZr?5@pTzOfo`;x^Ae0hlULiL@KHb zFklRTlq#0$0je0dqj59EAuK=`Ltuz#VzR|8Dyb6VTP{)Fa{0opxz>E$$*SHQ=5%e? za0_&4$Rv3b0q{(0`1poRRtV&roH0`I+Qno~ zUOqL=F@A}eh~K4&FFZfHgQ62PJQI0PRbYsX`?aM*g+;N2-2itym-QHOb;KN7_oIMr zzi!F>!PnK8V5#f^FeaQ>$`?71=x`I&aMfi=>o5#;%-6ig^ig&JsEi)6@7U}u*zK7< z|5`_nlByiO@d4#c16S5=JeRLXie4dnB#v4Ies@pxjOM^!ld5HoiJ>3Gj*7O6VR5H7 zJ>h7;y&)$9gFtKt^K-0TBb9OhjUg-@asHD+Q|Q0ML_VoP;v1i!yfJP7FZRkyFGFGG zQTm#knHaAr@U)tUr5meb%@dI>DGbQM(1Zi_7~wHOYz18lLs_^77|cQ`g>9taHHx>a z@9FpuO1)j)7u9t=2za)aRAKSYKBoNHSNFd0a8WOs+3wFzqON;DmGyz#rp)n~cV~vK zA5w7sJ7eIsf$RDo=zmH7rou-GTl&72{YZ9;@;&8d<*>}V`{sIo(Yw?;*z>BMqw{<6 zXNG|d_^FjCI7Mf<-%4dvX9*Lqf3TW z#Xs)WgI?aP2feIY4|-|09`r}udeBRTzMMI^p#v8$&a~orH!tx6eC!d~5yn%C<*nl! z;Nv^yX+)0K^izv}+Wkk~+DVs^cyb0&UX-cJwS#<|k&?TxlNDdPHjjPH+C27_wR!BT z*XFUWTARnda%~>_inV#{pLA=t{KIZN=*8W7(2KhDpci)QK`-dmgPz~52R*M_58BrGYev8YlP#ot5-G${QOQ;;5)P4QHJyY zkp>=h-bnrfm?+g?)6-20N&DD?j-i5)_D~p;EF&Go`@q(08^Ib#N=`zoJ054y~QVaP~A@S6)s;Z7-IaX}@sM%3~7c{a0 z1Hd}HCs&r;T^d$C{Xl@$8q6-oPl$h~1g}wo4^GVcb6dwG?@snSL=*cL%e%Rjt&gaB zdGic(3_j4AH-N3iK~Rwxdq^wlCX@l!Mm|$TiOBL*no_!EgqDS^gBz%@@-Qd}Anuti zT~`p130cf2!MGBH(PV3q<5E=JlHxe)pi04`6FNQ6eOnJfm`30RG_-D5wY2$aGH>9j1rut1nMerzAVLYmev@a2?|3;`!_-dGpJz*Ae{n@ zRztX>w(EEy=ymu5R)|iZZn+VRyjY7lNv+1Evt8RR|A&hIlLN*%tX{et=bB4Q%JvNh z&9Vpt02D@tV8_U78&btB5~M8b74%IJBo=f>G31F=7jiEuz5Em_8NMwN0(bfESNspI z66Q;m!(4NPNm-aJa_!I}rV#)Eauek5VmQw#xFZ&@1@@e5U5do{KENZ$&M>}gNlT(b zEmwR}VZKrEZ$4m{cU3R$1giRB5SXS@Fkw~YHf8t{0v%-@cz))ATbod#bJB6D?r+&D z1sNDTE;E{mhI${DiAF|-4;$Lhe_Eec(6didt{XTh|D@5r+?Jm6`OqW&Pox6I-fQ#$Z=FSErjpOMm$w_7(U-g8=7H@J314~qYi(m8Ke zTch~R&1s$UcB{0-ucUO&tu~pl#jl>4){(bK#V&sBl$4IVU4x_Iw@xPQO{48a+iFe! zdQwW~yj?+=;x`_Z*1f%59hc%ir*zKSE!-9VA*BOvw~|);(m?*=%!u-gOz+tv!^4LT zZ5-S*aEkJb{;`5N+MBx~dv(tf`_AmSp!W%n`1;RZtn4gQCOat-I5q1>4b2-J!{PV2~_&Ecsi zmHbS)XL>A2p{`5c0k<53IGDDIU0`fOnuG0S;yPhso+oTurOz3fqWE8F9l66;E$aF( zr5x1DF{L*z!eKtpeChJwD&KP#Q%j#c z0H;IL6~RyVA~XX(j!^`~M+I|HKr3n>E9Cg8*dYsEI?71rG#4gU@kf=kPC85560B~N z&7~|V&WMn^1C6C%>9k~|^Q~O>LXTV<^(MK0s#s;xCV;36v#t0*N>9#q1;L8{l-?)i)!lcO077fWnT~ZT2`kaY=t{tN6XNjvV(d1h%vg z$6`i~r87phk%gfxBf|vHhp>y|t&OCvYa_d$nvl~&L3$%>2RFdD_}!FlaGYSOi^w&i z20m~nQd8Mbw{U;O0gSKVAfru&qcL)P2u;ebT1{8-1(oZH(0vry z@k7Mj8$WO9dK(nKKT!BmrjWlk(|1qr*L%K|zjw5EWN5fJbmZVMg)bGpl)ESUP33!; z9}U>;-2a>ZZe>@YQv3hKwTXFUu}}#+S&LX!(v=9%#c8CZQCTT-gOD;x+#<-d0xhsz zlsJ9U5)>=BKbUAj#2}u)^O0GLq8V_fbtOOb}%Ht~wg^J(F*`D@y1t4ss2@?}P!K9aUpG&01 zB?*7wNMgVv7Z9c;?X>Nfx{w;EI!2^iA(6%FQ#{3*ztN_^{-H8z*B7oe=V>1?(X``5}*asT*+Uzw8ZC4;=0h3rzT+^aZ z4Q;L^T*i=4R?%V$d{Dq}JCK&eVUy~`dht7H9XVM!*|+0UkpkQA zDo%XouwcGlEv=ued&H}E*8!Uw(P{`Rj{^@~HZHJ~ZRkj-E@V178&bE8 z>l;Y|;*6?h^Fd!#x%$Q;qH@7ikP1euv5iokR}~$P^Hcu!$4TQJ8~Mdb+K@(A$kG`)hbtFmk}Aqv6#f-0t#1-prU6#or@D(hmh5k#$A=m} z>j&c|Q^o24sH5P!G_p8KDSKcQu9HJx9nNwp-k;i$lMJ9(h`c(wgSHtNK8C;~$1omM z)r?dxkme6IzRV^WG9`Ugb6ksJw}mtz4W8nCsU5i?eYSkd!41un2SFqyTs0j9OoPQt zE{$1~NiW0V{$WE;M7NJ!gzgOn7hD*N|1Yg0$5;XePULYa3S8lABj7SczlPC~I^g3G zLUKUmmf9Z|iHJKfq#dMe14Q{aNUh?%sU3Ofcri^{3iEnMF`rg9Z5)`uz7WFX)(HDf zRt7@)m98Y^1%?N?CW1P~N};;>!)*TC%(~2WuIRnJqXo45MwQuY*c0S$%4{D#a_9)~ z0DAwU3a9s}y=OMxu`TsKU%C9p@nA86lF21pdO^d>xUXF#9Fz+C?P>0r)R(oB;=U%k<%5X%)0|we4xhwMNUUm7ZS8&n)5I8of8gW%%@hK zS*Uo4jd{wS4kCX%m|a8?Z^oY%<(SJaEL2UdV&M4aIaFAJPjC=s_>()n|6RKD7;vLF_OYhXygzkxC143!rbs4ao(hg40OM zsfMeR^uSc{e4*cm_%T~P{Vl2BONl6OHHeSPOsikJ$>&tlU_rOdn`|JxxCjxZZ#+<``oNvj>7tgP52t0Je@@JI7xLP4U*j$y|q zoWmfs$`ysm(Mb(*e1|`^4P!R=zLb6WWgx3^H|7Rt)MPbSkzqt((TF^Yt`}Tkt_Lt% zxkS_Cr{k%~tbm)d>&kaNqE4k#BBtjbP;A226YL9FFvf4L$K^VrUTI2m8w>Cg*KjA~ zgwapX7?)C+D^#v&Z(OVG;Wd-2hCdnpbEq4`?#Iv-|1NGk9wku;!9wK1hKcL*V#k!x zbn2pHLlo86;s``cQ3OgUoh{klWUwBNUmDF&2W=AaCX0Uo&PV{mLhcJ>MX=djTjPyE zJKQlN9NSR1jyC;cCe!m^>o&g;-N%=~=gTrWRl5uoA1?}xSp z?|tBre^47X7*VXU`LhujbPd?!bgBd#Vr&SZK?m3Z*+BK!5|oZn2w-A)v(Xap==M@T z*C|2Mnh42aII-m=I0Dqj{Ybi6AuSNd+JfoFo-j83C2@G5a($t4NvGp*12%07oY(AG z9_4ckvjPwrMoQ6fPq?qi+~-_V~8w=7mE}Rzzaoam!1ocu#HWxVV^sxGFzxTt<&QL!%8Mo z_9u9m9bH-~+ks3$4KA{>#A!)?3{a;|pmEO2`+bb+II1%g{!o1Ytp=JwlLiQ+{}R{0 zw7CMPqDhV_k+A$FLDGl&bwpz@rU=4D9?5i8P*msLH-N87gUU@vfA##h@~lE-wzGqt zERRz~6Y~iF$}RqSa@X-We_^ZP8nJX3ESGmm<|9<4mE5`SlkZ$c6K__ zPF=PF9CR}9r=~*A&d;VTh4IOIISe?x@hl}tLjMH-qg6xdI_zp(lSpwvJ5q%sOrUs) zrHD4&#H@d&azml=^v(uA%#;`BMdNsLa7w^?oRQd=tJph_$O4mF*;fMo%knbVv-Aq4kid1}mJVROR)gEU@-M723k zagz=?`f*p^G))$Jdjc1;y&2ho?hK*ldEjg&XCMMJ?C&gj ze90;Ef$(Pa%bsBrpO9tXpRUXoD%Z6)-%SI*XnJmTe&Kv>p!?p+>OZ+)es&swIcJS5 zQ-VZegT|&F`(4jeBk-17&%l$*dkkUYf)`uGmqXXt?PL##RuE`5 zSN`qTZ~^H0EOT1IY$h6cE7p&sIdxSwp2bRt!_D_*^LK&&zgMQ`AIaU7{ru?Q$l=3B z4Q(2H)WBK&R}`YYO7EZb>{0H`ddhg_xqK|~|MVVHxu#Hg^ufe^fdipDCjWO_%q&bU z>>8hp>VKT}7o^$AczLTY{hQ;-oeR8IJ}721MnFZx1RC!}>K_=xfZ`V9yb)k$Ada#W zFCqKnIpjpLsUD!!#a8IjZsh#uIH7>(umm||qyv&vIij-8g_k>ky#CUf0X9oXMV6Vv zh@z8C{!Nl`+$Q$X|5v%HP&vN6;f1(TkCzvN@~$#I)QlSU6bJDiPQA#)!AZU*VEBXp z=SF^SlL2Bw$qvtUPK(@Uc0XKsMxkOI%&*b9-clCx+XXd;MvhV!x~c8hB+lg8GFhO! z%G=3t<s9#FGket>Q zY!QlVWSQ(-wp;?7$cD5TM zCJj=yb1rDLyxt(9n|{jv^pymz+sjKmnq;ooT3Ew0>cGXbze=BI5=f0-kdza`Pi$lH z%DzCp$b#o4LFbP=n)V9$3wAI*d$Y#*;|7ioSO11g5C3b32a;Yu4Tqct}S*_K?QPxI~!e8OszD2 z15fn^^T?*zXE{(vw;X_H z6jl32xvL4J9kbhC)~A=?y@$&nIUehY%NqSN_dFVf<35d@lx?- zIjVGfsK;^9wLlPg4);PTM3UCWHsrCWfk09)Wg)V8*pSvp3t5eH21q1bt`sEOTsUkF zPaAVMk83u6)0Sl~i;I;*fs7QI$!5qbU}S6!)u5wFafR)A^Gg}+p_QmmIXmIehV|Nh zJ@c6qWdku$lOvO*#ccqUR|6z~AiWXb0)Tn*Yf;t&@U2q`AeW9~C=RSg4pqvATrL0z z(5vA71jvlP0E8^2X9S>1-U>*mX%})u_rhcucfwE0}FZqa97) zhVZb>=sbig&s#obo+`UGbam<_{&T^LaG!;NiFF>p5mI}4FAlYRO26p1u{FucH8(%% z(##b*l0(g(1&w(mJWX~jeoBLby_?dZRM8`cuqf(r58{F)+X3?Yl4WsLHb)j{3Emr$ zSPm*SZT`IZsf>FhfyTzZ1A7AD6s&>C*Ir zrm*HSNz~ZpHv@|5&3As#+a{-`#^c%fae0j;W2%{KO6_RQBl!+fzmt?Kbi8@pIkmXX z!$q=Hc{Y(V#dMw6z@cPQ)a2ISQWWKc2Jpe>`~n#Sd?u+k%t~L%|(|B3!>CW zt&?cU7mB3CaO2Gz>-zsM$c)@Fe9GVl1|C=VV&7Fg-_6fwf2h15@vHvt@4s+qcIi9a`$D@H?Ec3<^(j>jFEf_AyIN$Jktb%t`+W6-e`65KR4 zGrQve0guxh@OI&SNy*IL)u-H5+9D-&&&5o8LSD0U`*hVhU01JC?K4&9OiLT1rg`ns zVpTK;+)CLBZ5B=l3_k2`upt3e5i0H`H7nW&AX`Aihm(dqOc6CQfNk#kR%Ji!*osGmr=e1hb1KB^y9G|%) zGy2MbzW!JBA76NH;W2%8^gX5bJH4|#zwUW)&*AyEjvkj?uk2Bb%qKIKNT`_++mjOfE39X@a9i}^}nK3@@yGpJF%O)8rw<%*`n_zjCbos9+Zf61&b z{#ExM`GM{~@-Msp$oF^uk$*98W9G?8EuXHh#E*49U?1)NBR|yrM}DyTk9=?UANii{ zKl0t(f8@Km|HyZC|B>(L{v+Ss{YUg*hfpxNxXovIMk72ftr{s8)iCk{~B^C9Pm3ypuLv(g}Zb~X`v??>Q&~IoC zdD<3wyH+3xIlLKWzBBL{tiPBn)-Z413!XQhFJLVovs%H6*?_#;$Qw>3vSZTmQE zt+rg9`jd+K8G6OxnWrkR;Uf&#OEoHe%yHfmxqyoJ~xjU#+lgH|4+%cJ*wF>(H zm;4dsr%L~keLw2GV{~|A!_e6934`vy6MF8-e?9lz><^TmX8z}Qxf0OM@0Lngd}!2? z1&?u&k3@uk@ZzI90IwQiCd^D^2|)e8j+R=Ff$bsugm_GVMgfx9l2lXbPv@{<6GLnB z1AKWf^x;eRO@R-q8U{QH7n;;ygUSe3E)JmT!cN9D0O~xt42Zqz#RpS4Yzt7>Ha|9(aD z0B$6-WeCCWyrJ{i2r(dt0lQ!FWRVe(S`3)8vNb|-#K@A}(ZK!7f(nbyNLc*u-;X%8 ztB_y(+3!c3+EvIe{xp@_O}oZIrCg;oFFYak+V7yVQ1Mr395o_Yc%-4AhF}27X?SQ! zgG=;U(0YMNEz@Kd*&YKqB_VARkxj*615&j{-iS|H@z-e_b-R8_#b3788%lJAC)Zg; zAW9&Lh|_zFg)y$CHZoG0BV`{@MMV1nM=fN$QGl@UR6&nQjZrf~=-Ne50EwgGFH$$; zhcuJX+CldQ!%t*jV${D~B!LlyMvD&NCR%f71tO#Xc?78&yj>Ab(~3VI$PH%Ru6%H~ z@R`Dg3U4etzp$-v`QV*{9~v6!c~kF+eRuVJtnbhJUeR|`{?7bIa~I}}{2TI>e3XA; z{!zIf=5EYgH+X&DllsiQWBLlc_xFCI_qN`@=zV?f^LiJ1ukA5=PU*e0_ncn4cYW`0 zuhR4Ro`384TF;Mq-k<$p&&zsl9<26E4FARO%ZF!%FBv{`*@LI#l%Y$9&Ko*& zNF6$<|114(EtCrVg(nss+yA7 z0WqKr6#D;l_$U4E>Ax{|ckW&NYX6qpAp_qZ{L#=ehQE@1S#~!2qX-_5w&#YzTYX{`>^kn<>R-AnP_rm;9bX9e zAm`7|&(0T*T;&6Y{Ev*vrHfNj=S!O1Ig8~(e$}5lto9on@|~XHOz*|z<8$ZNf5SO{ zSFNoXP0ly}k{gdaH23K2#pSuH>Yfs{CvU!Of8YGfQMt#|J_%!`EX6?2oth=M^}q4g z2m0X$8o$-%SFLIoeb-8{2LQwt$gg51AXl113h%0@$cC5ww15mwDQ#(5Aatv z-Fl!OzVtvpeBpt9_`C!C@VWa5`KIgl_swUn4C1<#uk6NGN1wa$)#G~nOdk<$d}Gtt z%2&q@=GJHHOse9en!fH?WyKZqeb3DFUUubBZe#YcD{C`<)UAzw>&A0ObH~?~`=Iq6@vvbRYx#H51IfGwh0S#G+;es4kUr&d9YwvGWdaCm3ra#@% z^z{peb4S<4an-IlQMxwMb7|8LAK&!#8BJf0?ej~NGJAhmY=RJ-^~r|_q)A?T&Dk$i=KGJ*^Nm* z_B$(IeP}<6^|(v+_su8m@0%Cx@0;hyB_$V3{X{A4T4jNjMm{_6W%j+v#&5M@_sUn# zYJ9cv@BO)zudiA8>b#Y&)-}G`@RRj9D|<0#LOJy3#Chk8)#O1J=chLONz0#{(&{Jc zKXG{OjJ^N1vylnkb)%R+`Ubg*pDcFO-lsz^X#P{~Ta91ks2{C-^=~U*o!I#5m`|;I z^;eCrj@DPsp9Zq{Av^!*eE%+qA(dRUNsg3I!Uu`I_oYqm}uk1a4oBq`|to)6+Q*u|ov8k6rodm;?Tm_21f<^XlUoN9&o>aC{}O z$K5sf{Y<|+Cr9PFkY9|2JECLXzjx-Ah34mHXPLa^6LixbG@gC@O2Ci1yYao_K2=X{ z<`*XYsg?X}IRjX>n>y}KRz4j!zB=}oD_{MaJj-f3!BrPscIo(*v#+{v%qNYSEl=(| z_QNY5d)dlY(aKlhLyw7#Z(nHj!fbqI%b%P$bake8i@y>$IeJ7wckGRTRr|XzYjGgB zicakXv-8Mm!-?PUp2n}T;h2@L`Ws)Zzi;KMZ?AlH;qYDZX-DTCTc4(~@X6c8Yq`{N z#E;ena!2sf88NltF{|uSx%I1kV71gQcf@M{hQn9;z+tO>;Lz1RP+IK+#nnD=$Z8*0 z$8{rrz_34d<){1EMK;W@@_^EPt9{_VR{Owht9{_(t9{_3t9{_Zt9{@PSNp(=R{Owe z>#4NbYAF7El}8lcyV?g{v)Tu)SnUI21G%T*{}*NQSB>_M6o)qsoiKQM_S&8&59qmT zl=0pR`p)imGEw2Iy|>uk{;SHaLS@^2)o>H4B+Ux|59NjVMeIY^T^6Si%g6i+Yj9d3 zv`7)T#PJOcZ=0eBonrc?$Yv159*cT8scK z@O7Y{j$%I$1StTo+nz5yb@1WAMH!V@td)>7=Q}vDV6Y{V?i&0IDG}Kg? zXdFKR%wZ7;XOEE8Qt2YY)MKoK^Na@k2H!_~Gg0AThloWYOin;|^~zYVvN(&4EcIzAIofSCx0DbK^b0WVX`HXJZi zh-zWgYhVC?&OLI;Xfa1V{s4fGaE-Ic2yjyfOQWeBy7UY(Rh-nY_>BNj;hgRJyzLy` zb_6jIL3mPYKM+-69~cxGYkNf(nb(#aA_$9j0UGn zDgVwqV-e`%OX5qn4qSgR5Cv?7dz!D?rIAz~Y4sYpKFHVg?>g;W$z8e2O^NY;L?x9}|zHDG);dOmS^}HhY>+Fqu^MCVymTub9 zH&O{@S)!)M|Kv{bP4G1hv2|T8OklP(yQ3UYT=1~3#d=4gHUkW1lS)awJwDWeyu~+ ziu<#>xTmLi1{Tva1MTu665?BBw{OU9zmyvjwoSXnWS4C`o|@e;R^B1R+&B(nVqO}O zPBwrl<#DWE!g;^EwdJhz?#VaL%5|xll{Wbc2^oQ87rvKW9IcZ)r~Pesb#4{mwz*l% zrpNu5mex41gl!GiidHkRv};eUc_uohG1G}7qFs!0LQWys@_pHb{`ySR#fQKKjPIBS z$5xwza?9Y~yeHcn{8Swe?ebX?@(9V!|68`4uLr+zAk!OJUT(l`@NJ!)YdHh6dz9uG z=osO=6U)Eda&AJVA=w*_&EBA%zf*)YnCH25k@?n(esWKyIpmwuwD-4r2TRB{Bs=$D z_J%?eX0Fxx-kq;Lr#aZyq$$|#R&^Qzb9D>vN36}5o%_@5+^6>LFbfjuio4ykyZ}fw z5jGnUPfbp2U1+`NcU5m*o`;%=$oXlS2i!E<;;AOX`p?eXl%089eHxndG3B05XS`-0 z(ZZ{!uKzch8F}IG(BQ2DM&W&Zn|of7UzdHgGWx&j`oFt>>B5mZ454=4fBJq`+3xI~ z6GtjLR;mJL@Ap&Xv9`_0=EcNhStg zdZ2ruhub~84$QYH=K*90aetI$T*?R3cXb!fOoNI6?w)}c(Txv4J6LomF;F*XY(WNr z>7=VR_3gl?3`{{d_9GL1C6(z(4S}Z!_I9R}y*_l&)xa3pmhu@O-&E706+#0H%u5j93QWI% zMSxE*umD%J6M7Zicc$__=|EnqlgY$nY7#^0UE!1%Tb`3fwjx(y%elzxIc|9_YTMIg z>6!~0&vja^5K~p)Bb*Oz#9qYwz&Ezo_oZbyO?RP1F}n~Z>=4qSmsK)23d-UL4E9?c45VO3`&9K zX6*Y`Y#FxhyAqd#Rj%d=01h_B;x3lrBETmtG=&$k%5e<0bf7N|Tv-D)IKd{3!d9F_ z3BXI>@|M^_=(Z_TU-J=o=3g7Zst(kIiZjZ)-mSdreN*B9M!9>AX^wNp+$<%0mV+Sa zI0Gf4_SVZ1f#s` zH052Ocn-oScgLQin*-i4x!eu~9F8~JHeok(Xx=I@1i$8Z9IXhIV|Nd(MKwwMLy*hT zN6=TH#7hYf@X=17>j@QLlz0A8dDprc=kJP@m-g(rIoN(JW$2a`_Z-z6`i@!Vb|~~d z+GDhHwJ<_}&7i6402O8(+q#Lm1d?Tu%sT-mfIHAM+VYl#K93f8ek4M)+mgz9-uX4< zoj<780i=cKUZww5oB2R~Fmr4s$_{;E=)%Ff2Cp6X?!Z+4Pjj!z9o>I(|3KlE!lu6W z_MHi@|B9Y(_FSJel}{=cXTFw+M*nE!ts`d)e>D5{;V0&QmLHV=ZB;KQ=PMJ90VZt= zxc_QL@#|~w%zs*gXMSxBp83@^c;-K@!88B$gthS;`NG;f_VdGb=07|2nzj&#S2pv| z?|1v5n}*k5HGO*xp81tEc;-E8@XRl-!87k(gJ<5g2G6{64W9WAYw*l3t-&+DxHdce z->=PMKeslIy<=@2d;8iv_O`Wo>}S{Jv7cF+$9{VBh|D1m$y(N6^Z3>pJoB4t@XUW% zgJ*t&tL(xCu zKA-yUFU>7o(ToMy*S%(GYR@6fKR>>c74A+57qrXXPYQ;szay)E;QI3HOwEI$B?4%7 z|DJWt@!mf+BN5a^XM`ePPaDul4HMvo>Bo8$g3*Z4P0*m5xgo5ZNOn!>?Z;^NLobzV zypU$I?Mh(@krdcg`-iOdv;8;OY7L&6%-nLHr&UF8>4rU{&6Dxyv@QO2*}Vz%ce3iP ztoouW=&fuQ^SdtbXOLBzZyEHP_KY+Ky<<>wS^w_Dj?*r=Hlb!u_KeSF&-m5>G(rzB z0mG@DfY>uoYrq!*1rj*DATV4hb_~%|%G&_3a1pG~Y#3(3QVlQkMWkT{qpFsV!Gw`i z^CvrYPIm0Nc@a#4Wou$x1pWg0Uz5>+&VZrj2}s=;&@QvSk#4q&$US{s_H<|eXFwCs zLh2(eww#Qm8}|$@Peu*$^7yo!0da_QyJdK`4^EQ)n~q;I+;`3NLfW00!$FNyPYgf+ zeJGglFfr*xg9VEt^qtIvKrh+T-kv?}@6c$DkSLs-L2|j~k~>~|8AR*o&+Qplj(!c~ zlDZQ(0%%I8{UC~cKzKH+^O(IxV;X!P=&7drnDyw;uq8d8EXkq?4PP6fjTo0IHyM~q z_Oy}gX-CTgSibcrlL1mi2X5C9EFllk?9O6#r}jwcz~w!K<_T~*wdHhz zZPc#nLPB<;*&T1l?)X5x{SUCtpIMw6pPO2on4D?3*GT`rb(t-h(Vmfk;nAT(2iFfA z+kax=(S2I4)AQK;dATQNugGjsF5l;V| zf%9sB=9y*bI_7c0G^8U4N(u;rNieG(IVin>&J)1vL2#?SjT#!-NhaD8{4);y(i74i z0OTW7fa(${iUtf)5*FTaT&e5;jTkj_NhftH044_xSnBC$=p{igX;sj{N&^bXrH=E>5p> zR9Z*gF6y~-Y-&f2(4E1lrpK`guTJxj_O(3ND=7Cuf&eTYTFASCJ644bODHlp(xA8a zKs2SRXlY|wM~=W1a*ZhTx(*|bV*!=Nw4k1urZ14e;^0=XghCt{=zHvX5$3siAeAEw zJp`3q+K|?fw~IY59g|v_!*)v_rNQ(Lz0DeEX*jh*Z?^+gT9@9TQ&$pHlyEmUbPa80 zsD~8tU;l1k(-C^ zAH1ReUka1G|Iy>;ew4jFbN4=X{*?T2_n~_>E>lTrwd;EJ?q1ARY^ESO-^KSX1SJ+z#^)u<5GRG)ifaYdPMUyqz+)wt}Dq% zLep^OXR~KMB7nu*p2M3bU|-DLOmYo^4sASb`XwaQWIOY$?3qsr{h8UBnwE$}*R@*o zyVvbGtU2bBojh_ovG;@_9yF+hcm`eth880+CjuQoml#8LtVuiv+YU!FR6Xz=Di$G< zMsz8#uQ5rZJTIxfn)l7@nM3c^N9uS~| zsH1ga1fa+Onc+w0SzZ)_ThMh8a&MG)vSZlBZP{@p}xO7>{13(W?nj6Iq>8^#2A<~|)TdN^gY&s zfzp;~W8UfLwq^);0l3$x*p#9RCDhVMDGz|=sRk}MK7D&E3)LvlCHf4bN;6PBx;~m| zA~VUd50SME35Xg-a2-6l{~^mFEIlga0nlw5fv?cE@Tl~yNV=HGQx8&^hgW$}OfNcBoy$^*dqN6!y2IaBvdd^wRt4%|rf^uRM+N9R4qzBe*$ zR}%rdBe@XTT(tfGH>fOvsB~q@1Ax|{?wNMzf!%O0r&M*rX8*&t66r$hN-?jr1c{)g zqjQLcA--RH0#R3!c4(-EB{%H>fM&@G*9qku4-kxY$#}7_G`Nduh+Cp8$Ny>)>BI&Q_!yWY1W0m7 zs-#-H3HZIbafF*~>57yGKxd!Fw$;Q|5egLH zRb)&pm+B~V8?SV6_ znUv4C9UC@k+_vTMM#DA7(iQJso1GrqUZjEBr&!Kzz^#z80qS`u$dWO8WasnwE2Sr; zJOJ2lYbaVH>T8qIkpgq&6|Mtk!6F@q(Q`*b+1EVR_3*VODjp4jAW~!0^$}N&OOH=^ z01)rS41!CZVWBUIJv|#9ds=M70lv1i6M(B809NkB(#l!oKrqSvuy)sD3M8fTQyu{5 z%q~8@w1PC0%0pz$xzll~`I^ldAi2R$L9;mEv33u!e${bs2HeX%L4*5gnx)63JOFIx zl8IwrRxK$11SEQre1yb1JHJIyfbBp-U|Bct5tX*aINLiBHrBC0p+NIWcFF_b=;XK9 zWteDtTTo0LH^xgqAe0`mAXJJ2dhO7|-`>XKoV)-f^;lxNbr+tNUK&ez0LblpDwI|CN2$Zd7zSx{FW{(G$$JCO(Q$OZ{hBO^-9iIU&R?GM!O&~ z9OUuQ&v(drDSNT;lk9OO3TS!c!={BU3-5++dxjTjjzBV=a&hU&DGvbWy2Vx24^%cm zKay1K60LKaIY$!~KT}IG@l0N(8P-Uf{%L=`m>!06rBSuwR+XtB9Wa zj8;hQK5YSc*|Sl;h>+uxH7&eYEN-

C~k4SNssr4 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 >3 hE+-woJIg&DO|Bn^#(1Sg1Lb;) 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�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

A`)zV& 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 -!5FdO;k-i-l(j}hnRC+R-f3`6 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

sTTs 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%0Aio5w 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%0AKoh|rYUk8RDZ>EA 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 z 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≠SFCV;p|5L0D9NHU7LF`{cK^bxiVF=&}*tlYnL|D_E7 ztPBLO&Qw_A@?x%xpHdgJ4KrZ07bR#hIf86391%iDpt6Jr&L*53 z^qSG`fg+~TBY>C$)eyBs=lmv^`ZY{0|qwi0!2odv+QZBGp8xu(NRIFAR1Mz%za9^;Kn0x z-{HQmxUV>1UOiP-Rpn1^7(clhE+LyXkyS0+bIE!Rz0|}wF$uW@&@~lgNMVA;PQtRm zW)CSSB*PGDLZCs4XQB|UQz1t~2Eg__XV{hQtEOMV2FABlLnTzxS*Gz!UN~|tMxp4QAn=9K5PlHC(M7}sHG8}f!!%^jCxQ}(0vH)6 z4L+2VQ5e?XC89k-w`0D`mv^uC`dK`RC)P!)nMZMgrokJbAAe~=5u8FC61eeo!E?k~pd7hZ|Q7rraxlXis_j*3+e$d_N zIn^quOw+3{mXm;E%`OBSXYP-`#LuBs2vlvIJZ(0WYB zv^pByx{9WCaVWpX92hdy;#h%pZHgvA3~7Cl-$rX2hdqe-^aBx)Le&*sJ}p9-cVehS zs~u_`K0`dn5HJuiMqeIwHPoP2~w-qg@4z~M~Di&-*cTA-XKfIc9+5{_daoh(BjYfM+FQEdnKAk;2=uR^_9A%kYue-b9;Od-4}7tK^l64P=199jDRC1 zN^sL*NFhRxmlXB`74IPd9&mVIKI3M}k%Ysk8Ueh4fQuYJM7Ihl=P+W<7`q~2E0((? zIUG>Zcyp3oLz!g^lm*aaM|p$BJ0fiC=SW`05E-3XPP5^6PTm(1;0B1;=agFRB9aF5{ z5s*Vo9~=q#oIrnA={PhYBbi0F0Gmf(DC{6O4T~m|0pcW)>iKj#jRxl{kW6Cuzype3 z0Ok(@he6Jh1RlS`IFBtL9F(Utj+gvchY3xp&uWFzho0>DROu{VMeJo>6HP}7Z8T%e z%_4^`)VyH_BLP*eK(c4BwlhP})G)-sUTBH@7wvr}Ifx&`pLFVvz_v)0`D+oyis4X# z5J^p%9`o*^t8oV?%AAEQv|EBPw$)ne6#kTdY)IdN>`Y5R+wJfKnO8 zAQmwOs$IeX7H1uevNS?v06Iiwl0C-c7{s090x&|)PupS`BJK%ViP?#^4e&9Z3=yA; zWsyM!*F>d;f&dscO27!vQ>X*r!I_w~Lkw@T;hsAhLIx_r%xD8<2O2J8La!tkY^<7y z>~g^ZiO>$o6%t122#^#*wTV(LNhh;%m1PNA`Mi76l}Gm<8lqQ;JYNJww4h9=INr{ep1KV4KgpCq7+UNkS znL6~&M4VRADpo*{)#Qxy8xv;FXWMBoq>@-U;HFIZ1s@02W%@cyA>`7K6fc|7@yt*^ zK-HeY9<@I74><%%g_&vC^Ev7Nci9r6|94O4XFA>-*ymS#LC7V_*tT;JJFd*hH2ub4P*{d3r2Fo zQ8IcGFU0K;O-8g_%!5%9yS7a! zy;Kd#vEvt2gCbLACh?Mq@4di55h)D?&j>~5^cSTF9w9WC5CBjp&p-T{0jT0UNQH<= ziti5rrvFEQ4keSw9Hx%X6{_7%5zU)<^7M^gSO;2}sZrI+IN|sT)Esva&S4~Jv#NIN(xq4_PYpa9#^gQh1CF_5G58UGsj*KNdclbjO>msB>Az8FiHBLQ zAlsm)EYJfKm=PWZ)H(Pj?&=MbB-oiAXaWYrwicMt3;moFhV&}vUT3G8BB#*?sSX^{TmE%a0k+z7Z z0_msJZ4iZ}Encpya0;Vj!Fq0pO$^(bByiK>ohE*9$A3G65~0!D5Wu<7!iZcnoJ2du zpI;ZEYY*cjYSznSTCmfk5YbmZBt#nzomLyeo1U?D!*Q2vIc{jnamI#C1M&+E1G#ei zoVq}U4r3rG7KJH4QjbKB3@0r^fKO4;@8qC{wk~BQig{x9Do{j)oPlbF^wKFCBCZ#o z77ukuARl!6>NKLVL+p{J3C-E<5}GC*n5G5x91@x{9Ty046`xvt3a!QJ8oqM;d2JG! zW=(jeh3`xZjs1`If6N@3-tn{QLbLubK2X3Rn(~U8!?DBuHTzxq?y=!LvU2Ad@BK%P zpIHr#96x5lGEEHs!tQL+JZr}GX!g00{hjs?6im^w=ZG^&#=P=C1$=+EEpy&zb3f+1 zvE#zP>-~3kr+ta8g6E^&JdObW?mExi*SW@Tw|FlJk9YCu9#>A(F#DW70 zV%FkLj&Td-iJ;RlBuW`<0P$>$O1uhfjKeYAbNJn?_4l^X~ZG}FV}FnqYh11>G@e(+znDaSpR@c zrLhpFMoQO=HW^0qfHCwmRcvJOMU7QUk}OjTLgH74t_r>#NE_&jAGO6WIP>wn070o3 zH*iVcDM4))I27ds(pMm~AWbkZgoB%;*9l4kG!Pgv{)?Lw?fGF_3?m^PYm6YS05^pq z%&AS#H8iL(H5aZ2ohdMJ0sRSB^d@C1 zO1@OCX-Ly*0Iy%K_g(`iT?gxjUbh_vMwirf&X&%8(NJ0D;>;sKiIR7Y6 zB!M~bwBstR8@eGzaa67;KQc-hR_J;w=mfyS@A+z53?ogcloB%TK~xN><#0j>C$KM# z0*Hq=pX8hhhNq1A;E|wd1!jZ4;9k*?#vH8Yp0*eU_*F9vZiY?(Ef8w%^jdh4 z)n?Gf1vZDeJP(NwJ|JBoRKu1Dh#2pOXtw7&Z7~d(WIWF_J0j@gOBMRK6S2h2A=tJZ$w8T7d~1 zI8>y9D2340fUkr{JE$fzrsx=NNJ*9rzURTV7zQ9i(OaR!DVC#2z)T88zjQ7{;)n(( zcL(SU8afdcT5*5?WFV&VpiB}GFz zf!dW>GrUNhMhE9hLL3hF9B7YWfMR9a6ESyWynw`0P!hEjT60;zCakg;@sN6GAYzzJ z+!w5`@lF<}50u~&JzsB+VbFC%-wZcfK&wFd*d?I9VXp^kL=>;ePC>{nVKE2Nha3e3 zAV4Ufj*+lu)9ksuErx*~l=$*SIL^_Y;KWBS8SGS=_9}TVM$gP4u?~UYK$(@qm%Iu< z5M(3veGqaz-)f6tfWO0nlkZ3yj&F$A5_>z&R^%XHdDtaD@TEvTWT6#6oW>Lcd?l@P z#u$(Fd^^y!*4A(Ph|RabdzW{Y*X#L+=QQ`@?i<`2Tz9#4xxCJgI8Spt?zq9R!G4#0 zm)&doNbnQEMCW%pFYEYS$Nr8Z10M~X;QyXq_5HzjL+$^X*9Ilw*f~&BOmDF~{WUFq z|9vff|EpX4{(D>e{#Uj5{l{AT{-Z5^|6+^ZztH0Mzp};eKhom&&$syfhg?f1W7cxlW zn4udM^TS@Tm>>4?#r&|BE#`;4bTL0{|6+bvX)!i5C0DaEQO>E$q_>QZC#b*zUsv2D9GJ7rbqQmi_*WyF;XzNBAGF!Z{ z{L7b()^Kts!X={xRUe(?Tc=9@-|I`*DqVl+_`~}b900w3*A>?PHQDt4*Ps6@0{@D@ z|HufGPV<%g0+g{=aZ6KfmyI~}BP!UXj9S=LH0j_W<(JhEIHW<5$U=NaRsni)ocHdKI#=|}eBsZz7r)hP790A2o z^MKnT;g6)m_W&}g&^b|!iJ-)lg*1aksv$3+`$5~bzb)>?Jb+7W(fj~kr6Q>W=mq*| z96CIl5?I1K8we=zY`{j1#kn6Cut8DX& zQ$yd0`qNdT?P8%8CpH#P7;zv-ph%E52E~Vv5zw_{Zp&T_t(4Vb!tq;~W4wfTtSd60j_K zvoT<12nn&H12dtU+w--yxEsx5j@x1w&2D);_qN9{{@(!W8Yno2!D8qpnFkUEu*Fdt zf`O_4>=8l}R!pc1DgfI7JQzu+a**lJ!a@TSaU!GVfeQcsc3anD!FxMz>3CD%#r|Es zGrY%p*1LVq|8_iP|B3DPc0Rc5Up~I8GhaSn!0VVv4EYdVhiKh^i6AafH0Lm>LgW)^ zaF9bl9+44DA=j0J%OaS1{sk)l^%wBC8kq-3ya+rF&>(=^R8ec=TaJoDmq~j0rj& zG>!>gGHi&y8@yn&K}lf=4x+|9+UgwGh5jx6lgi0lVSvn7&dp;(NYXW&CBAW^7Mdd$ zKQw5?WYOS{V+IMrJqrF62MHC*coGIQIQ9to*Dx-@Bf^t_L(is<YG4H4ZW%QMio zR3r)Qft)ZaD>rC-jToj_vqcFO!DhwkU(7_K0@FFu!9(l>rZ zB{E{RGe>=wg;-~y^CdN*1Rxp<$tsP+B*NSxGK9855}_epMIa1$1vP?X7_~#-=py>U z>WfJFv|8sAcO36eyic*VmV+~}t-8pbBc;m5;vCDzGZn!kkp!Bfs>^~1CLybdk{|B7 zSVP9ePyl8cJSY@Pq6pkbWfbtfxTHpr4?;^JjX)xEOA3jq=;F*la>DEOE;mK0q&T3C zjF`g>$)_Z5FF3qWp%#SBFf}=$;w4q=EC6IfwUW64svjd6;PIHrXkkONC6OkLp+<|1 zg@STh!br~{d-Ir{!tA0F8o#_2m2Gp|mKS8MhycEctfNTdvc-_Yk`KcmAa@aF8PrDB z6_|5qyQw$=qnb)K3vith8|toUa}LT99=qpgQk23z(pvtV5y^;wTrm$?2T7NPyY=$% z%W5Gy9FG%i1-OrBaHS9(he^ox134$fDT{6cnLVnt$VPyQPYP!T;UB3~!gWD?nDs`9 z%pt3D-{*dCb`z&({L)%fW^z@SxTYp;%*Q@P_;ycEXw)=(+-kIgac?YHiyiPpkP^C)oCFp z8!tG2`}8s_0mpCcoSR+UXLg^|9qZoMy}El@x2x+fT~BsB+V!KZySu*D_1UfucHPu< zW7kW%M!R-*UDkC@*C}18u4i#~tr>ywULr$2E>&$0p~6&Xm^`807A!JyB&Y& zJT`D@#|Jy!+_AS~SI5O2r+1_}x;oYe{vLQT@Qc7bfiJ@}xIFOoz^en-dY5(f1hxc@ z=zgO6`vJTExBg%F@AiKQ6Ry|$U*ymEFZ6HsAMan~_xpbD`-Sfw-)+7R``+byv+p&& z{l0y^5#KJ~3w#&&&h%~fZS{pcfAT!#`FGDRJP&xj!wINg7B|8Msb?nm4|;qm;o`>XEHyFccBulsH8 z*STNrev!NAexX}+U+g~HeX9Fe?wEVCyWidGUgq|?{^9zg>q*zITo1Z_;JVv2?z+YG zDc1*G?{vM<^(xm(Tzg%^F4J|H>pa(UT-#hJ*YU2Wxz@OjaCNvG&i~{5FX!XVhn+um z-s}99^DEBJId1W7^o4v!`GP)|_ix_cd;in>sQ0Jd?|JX=e$D#@@5jCG^WNlrz4sN~ z7kfv&d%T+W67MwzZa~7SNt;5+%t%rX*iuuB* zY4kgOTlw#auim%Zxq;spE9fSxww^c4bvI6a>E)ANTKlrvTZgiB*K6vokFLA!t-D@X zyFTV|pL2x+y^mw=8E|IgVq+n{_tFi;-F)lJusURJxO{ECu(teS&e(AIrivfpG2ik0 z(-z>@O?Buf=keAM%3g_s=+GC}-!S#dd#aze{tWA4p!RLnJF74D|GRbZQ0=Gv-&^Kf zFJF<*EUW*m`_dsVti8X-d7Skd*}<{m?*5DWoXPUA2F>A(THe^VX;U@Op{G?px^;1db+N{}SZ!VOSrU2L{4Hd+_fkXIjVz0zY{bW|^n{)u(*HS6N@*2O2Si*u|C zd-bC4kJiOct&8ti7hkt7-fvyJ&bqkHy4Yl0gsK<4w_6uCS{JXhE=tzL_148T)`ekR zShuM+W4&^@b#an)VFkQ5YQ1vs(~q=XSz%ptTNgp=!f#!8tqW`V_F7?CW!=hEKdS!D zst2r#?^+jDpjLgj`pQusvM#QzUL0YC?uhqTuiRu^Sd(q}7pt!o zvpiP)QO{qji{DunPgxf~v@Y(lF1~GDSWimNm#kO5(D56ayI6Z#*H_lDQME8?on6+y zy86rg)=2w5V7+p)b+N8`vCdkV*8P9hD?hg`tU0#Mis3rz`>eAj_B!j)Tl;ABuh#z3 zy0HFy?T@WjtWm7J!FpxPx)`-Cc3T(Ly6z7qPIb@4On;x_AIgLM(GF05%2@>O4P zzRkLLt#$EI>%yAq&SzS$SkupGEh!G`@pM>^mE%R#Uv^mY$6-An4z;pE8o7LiGd6-o zYhHKgE32QVEUN~Ya(SKY&z9HnQFU~zI6&pc90W%@vhSGfQ@?y-movnYt8(8*@+0f+ zwSFsC9L?#xfobH_;hqOqI5(Al%N)2$9n49pwfctgKF4301Ecwjk<&-ltey1I>NiY& z$(n1mKV1Fl$-ni=$uGTP@=KM?w*E`3e%a*T+CTZF>nFeT;#JOQb&wp8M{&f| zE3#uNH`V@IoR?Q_E`NRb`5tp$apj7GKjy86iYxc8cAij8KP*vcv}to!nYeKt^X>94 zm(#0^K3MGi?x`;Pbt?H*h0Ahjk+&JBdnLonEsSoR^fI-V%LnjMX8Nx`5d5|6h-%h$ zFy$#fMwLvij50rVQ*}Pn7QkaSOnG$HrCxR zR8Dc~b4Y$vDF196h2;Z}iJ+OO1!~ znn~}o?$wRnXIP)uW9tlYhT)Zv2A(v_LpyU&--gPaLqsKuD@M( z{jIv|Z`NIZqwe~Pb=RM%yZ%Jo^*idWZ>qa~Yu)vm>#pBecimIFK6+2>y6=hF^{Vc= z>(07sPu;bncD?dXb=UuY-Sty-*T1a0zNhZ`BX!pws=KZm< zI#hSPymo!mU+S*!ue<(U-SvHS*Z0<4zkj7OTwS}1ra7F+8RRjBacr`|>k;Yy2W`){ zb=?`fvGeSXBmF=1eb)Ob&!z4N-T$xHU&W7R{AYjcW<0<*l$-Fy{juBa2miz2m`S3$ z(vR4ZA0EvQ=6CJeB5H*>n%k9qK4ZO}slJW{4T2IP8-?^6Zg2vs#36+G3BthC-|#@d zl`^ejc!GBZrE660kb9?1MwP{+G=)B~r za1F*nLFJPioErx0+2hw&1LiNzY(mlCLs}IuOcufz0H*2OQaT5Ur%PWST3m>1p^c9Ixh7u5X`}LWw@%rzY%mn}UiXdF$7Uu|?CO+@j9*ucppOX7 zB*EO^V_OwLx|O(5glUN5>&zUBiF7=zDPG2~l727xJ|J&EWg>QouL%NR)J&O_bZ0>A z>+^}=kozhX!d|(v*Kpn)H~wPF7MrNeWG>q%YK5_34l{$=b_M^KC6F^F@XG?p*?knrLqRA@Mf!4qvi zq{I?oghrNfPgp`wUE7HCo)UsT$r?SeK4 zRPdqTu4A1WkroH?Nv2u0u0(wZk*I0Aqm82VuHbv8TlLqEUsDZ}tok#dQ5t;6z=DPe zja0N0#rY1Q#3+v!y+Gi|cx0J0yU;HK+lD+UcDpz=(QC$Jn3f^F;WQ2LGo2QB&|m-^ zk9GX7sc|aR*|c?hUo}j!v~QoG?bYB@7B)<@&}pBGiW{POC{O`iMVrnft0J{xN5tqg zhG%2iEaRK7=>nL^Vx{s^oDHMV%9oteme}#hj=MN;kBGJU!1-L{3uRHdhkb`RA|)t$z;9V!=}+5K%+_O&D3EPuNV;ZU@B9A_QCqMMTsQ(YM1p z5!F|sg>tZ}FnJ^v_{8Fr3Ye0|4nZ<9YUfw_jslX8Ul!?f-y(o9_|di`~b$ZgP3NFYzAjxyAE*_ml1$ zI@Sfg7CdssqQRVgA?GTcXT{(kfp}zaY!|2Mfy!eyP$7uPzpG7x@++CDhG0gvfDB0M ze8J4*hI6Ai>i5@}My6nnjunQBnKHs=-GN2>q^~d9Cw*0}>R%Y82Dfz{57?l3&Az(FtC+DmNCzctPF44q=TzEC=b6QF ztq*;i{2;xL0_ThZeFC$%?#@z&YyZl_{*6%K87!{5Lv@w5xAMEJ`?ldKov{#~P%uex zM~fNx;>5EY*MfA1MvLq2U5pR;?qYn%J&W-n-&u?gxqC4_}g7{<~6U zFP{U7x#HRE_?!Q_a7{CoBE`Gi+V=2jAA2>Db`^o$prf^>_eu zJAdkUkG*I+W0nW?{ZG5SD_9YhlEVB<_R|e&WdnN)K%3A~!g@j2de|3W{ssvcy(B5q z;edsel!3j6n6adBJ`lz*MdK(fvs9Ws8FnprPn+=ZA>Yu2&BCJ4s2@|5$I=V}Ivi_! z*)TG~Fb|Fj7J!@!$!EyFRTSI!;DpqlrS)#)MH5$Dy{6-V>uOnoMj)j-uFhOiZGnrYhN;5*qD;1G+wEBdY&AkPP#<6Yz3FspsR-M(>);LRK1sRSl~ z!b2A629ybt*y8~-NUGTgBDhR7<`{4$(WC-Y1}2f;04zopH!qwHWZ8RWHQ!wA8@H5! z=d3(}EM>Um0&xm!s05Q}q|+FHlck6+cp8xmQ$zm%98@@p*`N;OklaK|OsjKHd4?w; zn>}yyyl+-bDXRQV>|*xkrWs#1yaf)$0}x$;9V17 zw3950vFXJR8pZ8MPiPi#+j6_3E^hNh(Y5HjfFmseMl5A1w42Cy%bv$37zwrj)LBxZ zYeW`|w1|eSDAN+dS8UwLL4ZhUYI;8C6A#^ab$DMRG;6^3VcT-s6;A&%J6;#KJK(Tg zVSk$IcxTS_NAG_+KI#7a|7Gt^;4{DLI)NvXOs+{XZMj=$5(>11PI&I4r9f#5EfiXM zaF_6Z{?GrJ3C$!!GSg`}#AnJWh=7Q=iXe4YMe)D`L7*A;wW}*$>#D1wvb(IJtFX8t zf{5(<_avD~Gm}j695V?ohO?REdGh~0zvFv+j{YmU|GsZ-^ew};kDNF1+e3f8=Y41B z2Lwsu?tq+H>YuIo$Jr}SbletVy*hNeI6x!!tx=9nbr?&wK9c0I(}5eY60~WRccI)t5g@tWV0w`5#msptN@&TKal{EMfjSDy? z(qUxuswr63P##!m5Smf&uFPc$a9?A8T)(RSRVP20&3+V(&qo0fl$59_RY-XNp%sCA z+`Dv?v*}C@iVa#Sa4;BUmXd@Ic0l;CGcHyG!noF|2FBQ*_5M@WIsE zAmv4;%7RoRt47>L@TFEQc`KVLr3{Wr0d_U^-_PMfYE_17>~rVvmv2=qYwR!2;V<8+ zsMXkCoWozfRk^9LKR<`Re5bVRuCpQ~baLdE840LW%SxzgAl zZHK3*)C5<>ov%RI%N0ubJTd6V{U3$PHITSS=@p9Qw6BK3$Q`EdlLkZQK3++W{lRv4 ziXsi-lEz+wkr5GF&5_Im?xv5Ziqi%}CV+}BFgw7_us0B0Nb}R?uX$?y)Up3ozp}rf z$bHHLeZ!Q$9lNzvLE*9geGY&376pLEKEL<6u1}tuQ?gZs-m(964&&aUqVCxLR1u$} zxVNf#JN7@8=Kp`uHTvI1-ZXs2o+k``b>K7oKi&8C-Lt*d^<3Qj^)4nkJND19M?R1q zeofdC-3NE=ddwL}a0jRN&l6#po85;KfFc&8(=m~zXUlHUw1H(x->;0@?{{Tir{N zF&%4?v5R8h;Hs`g<*KT)P>r)2(zQ0%{w z0?2h*qWtN8>%Ct+ufM_Xo?qpAbZU2OIY^Iv;K@rt8nl4UB=>!~w`)s~5N)eG14`Od zkzPXzGmye+3?h?`NS~vS(i*7h3zZ1WM&YI|Sv+~j7%cJh#-iV+`=$53d+)zrj?uA@C{XG&g1U(&L|mE36PcDi4B@8P{~+q4Y$a-=SNVBhfTTRM>TPamF|yDKt1l%5eh zs1<0OmpvH}Ku|kL6$}%dLp(JY15~UFvmny4nj~dmsA>6RO1Xd?2;LL&Jn$8!0#r_P zKdI2tqK$&5fSzh_ODj`FDZqiV#rQgp&@I}_IDbRWrNd!myw-PWNG>ekRaFI- zH>dy$9{t+TA1rKjCG~!?_djnmgIr!bkAC1u!}o^86SmoztZvo|5B2V{MG)3P%+5wB zi-=f(mDBI0gq|+sYzUo8bmO$o8WoYIOK=k?NBZnMCksR=Kuy$PNkeBM3ut2LG{lQ( zZv;sRJ?J#8>(I_7E}sKQ$spO*$jz5iTa8W62YNoTaHMi*ehx2aAu!U{T|SpS@Wd4b zg=W`JZF=6+;b1g>S27>?NF@yY!0gZR4b_*=>exF_8H5%UaH1Htppk;_O6cK`lyHHO z*C!sq2U63t;LGK#hHH@xLGTXJPssD^j7SWD*5%j`h9wkK!wH;_PKXT&3roMM>Vkuqmz~!q#btbUpY1dM_Llv0A1o(^Q3C=9ut7b3C%{WubP%d$Z)d~z? zqcY$o=6g7kz$?sa9Zc1-GjGG;*y*02?|FT92H9T{enIc^==AB_VV1YvlODKiRlJ(o zd+m;wMI-+y==&fM=fkFRUg_vam=bj2bnqoLf`xJgbBgm(!5|SrwonZES+JcOvawGa zugCXg=cA)j%P}*(TX!e!d*ISl;o8?s;@$3WNxjX?0Ok_zL0KI=%gGq7k}mm@23gpv zCF3o^3p#4znhxEkYC0xmlBBa+Pc(P`)$V^($A$l=<)(xuBv-VL?xbywfz4?KQ3nBk&4lLp1^Z&LS%%9o)@#SD9_t3Ci zST9&h_5atqMt^_g!^3abbNkTb;6VRp`+j-%_w`QqT)XQrU0>g^)&Iu7__6ViJ$qys zsW8{mb82&7Nl)NVGGCiaW=OprJ}`BVaKOqx(8Qg#u@^Y3k-Y#qFfe6}l2l9woEkNd zN%xhqK3mAgrUq~cHNXOWf@+>-hE8c7dX1)t)__*Fnl~VziE2UyYIl*AYxdC0BS%Qvt8f2^%!M-y*u;UhwrrnCX@KU+y+r1f4PKtv69YZl(JP}kdacmD}-s7@CkA@JgGb(=0_aK{bufh_2AdcPT=%(7&|E6Y%e;|8jipxetbs_V^7 z1nXzJpZu$vSW=Ju*fX|ClpwR+b^(Z?xs=O9g={$$%aqk{>J|JH3(Zz!BFmDwSkgGJ z*n36_4Y9mR()FgTM``jK-A~@V6s7C$nLd@^xw4k7_}De8qLe?Snmcy)-jrR$ANMCmuWCqH|5p0uM$ z*IkoKPJUC#JyP3DVP7i0FO#}GRk$)$zH%jAkNw!wSH44Jx)u#%m>Ri`-O+@L{-IH(FM3k=h z*i%0ItTM>Sl8*#QKQv5NVz28ni^<&rT<(Spkn)0KLvPtw+y(lCcs0=*iOSd?ZS z+j^AteXe`oKW=63h5CQEd!%dh7f0rXQ$znS_|bva_CKfZ!rssKyocUj`0Jhg=h&4$ z@`{mqwlOrBJ6$j6K^kYru3prJdro6M=etj3k38tRXID!O!U{|uCVf_2QUT#z3OBzn zwQvB*J%|jhD4R_s)%^)E!h5I=Cdhu;J;V-74^mMcj$G$99j-M-=LT@3aPw-ogp4bj zUM%SpLP9u^R3nA(bJ|CFM^B{!_aBEgL(vtNPiq@w@~%9?2Sf;zhHPvl2CHAzoZxk) z#6=v0wNt!JJl#c?UGyZTbf=u;+45vMU4F^2XDn*HH3B#&fTt?TZ{jV^W&jf)Naaf< zTzrY)mPsio2uXv{IXnUuld3p}-c$qZC;@Pog{65YJH({%aqhtzbQ2GE;TJCaa>z+1 zXXZ?4*WQOKQGCwgU|W2+O ziz zCV(v9Enz(&8>x$UxGS&0bPbtFWb0p%*DYl9-c2ACljy=#90z<2+@Pp{X}4CBcoXs4 z`H8cS&Fbp)h z<0<5rv{FFl3!H>Dc_nbvNt0I$y>paE`nqen--`^>k)Rf}WG zQETt*(59$qLAwh=-;1I(R7e$+D0pY2C|{%RBU!W9aJs1ns53!l#mHvnvbkD}qE)MH z`A!|ZarC8IxDBpaym+MktoG-4`~nXHi&Bz)aE7Nqay5|ar*6OnUjM;qPGAHVKo1Pm zp`<^LT!+H}4qBriWX?gA3C_>`DF=~=nAhvym-3KCfy!ab5o!F$T!Q33^0n?+Ex}1Q zdm?FXV&rT@ku9#3T;IeYcfr#xc*aJ1_Nv9l?40l|wWc!#YGC?hf%BV^`Le))dSgTn zXj{trc;v`MgOTlxG?pZ^_;=8W-K|9A-}WvWWu796>rc z%^HQZ&8)z8p8uiEABRix|G(=R{fm)L48Lp7>`-oSr2lh$A2a_?damC!(e-!3cMd;l z&mZi0)1E7bzBKfqp;ruDJox8>?;gBi;NJ#*d0=MXk{#0?IF`Bp@JN04%E^DKC(f$2 zY$boPW}jLh!%a2)7hQYP9oJodoucX1+i#nko;}QNkrxiq!=?Zzt zvGMyCmZKILK&em%-!ec- zI5_%p=D`QjCW6-Y&Hn!Rxw*rW^ZRG!D(lAR(f7|U$0(dgk2CL=J^DIppl+|!wF2? z3|mhMZ#mnGXQe!VR0d!TKR3HH=MXkOOJZ0Et$zgf6mk!$uUDngrS%gsS(Zu=sH75% z|K(_uI+Dn2e2u-?CLZFvKRxd+8^XlvM(T0*A6gZuQ;Y6RyvZc2bekg;G#W)4X8@W> zyOJ6x)>EJk@W{)Tt(sa$>a&1M8vl7&7F$-WOj#&tlT3MyP|_wIV(%yRe(Kab>g1{P zxcld}VVsZ#Fqulhda`tNp20? zdoyvW?FH{!tDkx@PEWjlc2%5CZOCroEjqBCC_^b*w7hIckgj4`zpR2!?81ZW(#^4f z(}Zn8e}c?MIz=f-j}|=zRSrjP4c2=T4>5Yj=*u#kG4)PsSE8X7Si-iG7=y5~ELq>u~m611v2DG{yZY!(evUgNDm#SMou?Bx&@O>Md?2o_ym8(M3 z?Get}aa zWp0%<3`Rj+(~;lA4jjC9@UeZ4DhNcnzV&tG)){Kdfa{cr92oxbyT z-_iR+!(SQs*yyYGUOk-F5%|F1?+Oh z;Y2@IPtYobe(dMZ^=Hs3b9?M(&-D|uD)BJ(3!Qj^b0?}2k_qXEEX;1v){?TyCNeYy z`FP~<)Tsp(&}L0(@n6XpXoDf1*Gy(*82Reh-<-SOUD2jy3%Xfhj;~{V;4U;ZO3~O~ox9&1qyhGn#J98o?7^BCjF zZ;CZnqcIP{L}fjoqK$NbpbFWszdm=r`%V@>+DPIKWIzv1gAn!vuhYII_!UU%!9_c- zs<3Lt{`TC(9WjS$uiO$?rtGRt5nKz#@w)E~rSq&`GA?HmW2-bySc*M5vL3{UN5Ln{ zjD4a0+=2Q-n!Uc;O%rWZE@14#+v;&zb#P)ie78MG{eyV;=|LdAvYYiP{ih^as z4Etacz{zD{H)ux+$wiT@XoPb2k39Prr{!^Ij= zY2}3mK?Kp_k}kMFXu*_YMF}dhpFuh?7s%E0P;}_Ve*Bz2LM3bPL%IhvmLfHS4Zk(6 zESYTM=P=|ipvaQ{K_`N>@SwmuoDiH0@L#TzvHQ>YBfxX;BCDW^5m`a#3HKZ^uu5DC zG#2+DxgFHjz|o|3g^m!;vn&+3VY|*>ZR`W*{1Lb!Xds!afS!;w1FQ%@sX_)2v`SQ_ zR-$R{p2&KPNXm@FTh3u{X z21jH?(!)e?i5@$XM)3$}F>U2<_It1%KeGkn4UDLD1t&u@BS#rH;taXQVNq;qT zY%zvVu0g(76v$<(v~LPp(qVRHPDCMytPRPa#Ei&{EeiT?QhLH(MkQ4`Q!Ef`TD&A? zGcdztl))g)&q)G~L~GXe13cL}cw{g&SUxi{_^QS9a_B<;|5SmEO`uek@aY{49q%f2 zTWln0*ln^)frCt*;R%08&DxZkFJE$P>U85?(11$?X}Cb4pz@@L``|J7CZ1^EtAn9| z$+co~KAK)gPNkZz!27RSOpVm9ZsW>}=Vs7cr)Q^LbtIaZu1#Bgvs}CJX{E=pshM*P z`?y%i1tlA{j%B^YAWKa61~dukk`W*5TbPIAJb;)x*q!5HePYBn?3M~ekS;ZDro_6W ztkVr#0q<^ft~+ivpd~KF4)|*ad^Q}bYX&Nt2;tE04gJZPSKreXpS&EtHm$x6g)d0s z6m=0}S|2q;oxE{DMu!9F!dP5_0+>;yH3VTzN{pP2gMkl;mIEOROLxmFaOhn_Kf1XU zSXi7~W<|B>>^l*{rB4|TDpZV`{4`HUndmss(yJO)g~qqOwo!?8D^IWB#v6^4^4s2rIdf!Tuz zJ0(Y|1?~&z7gZV^2kjIogAreG%j<5>Z|?b{&8@rZ7Oxno-yBZ9uys$J?QP;?TDmx9 z9kWikm0^MPHWf$n^V2AIGjrg23(FdQS(BAQSV%MlNDD9xMyF5kaf+6ibE*uf$_Elr zBtXcpp$R=Y_8gg-P#J^dh|mmoIm!;wCYpbUoJLi8{{&VtE(;kgt|l?Ru)QI$QJ@F` zX*VRAgXuM+_f0%rZ>IOEGq1wj;*&<|Pi{jTm!uJ-l}l2= z2^@u6sPKfsP)!m=sgX7g+L!4-b4|HM09kwk@S}M$G&_KQ)kuZDj0v>sl|rI~20=_C zCXEL`9q!by;PYKEX4vi05DGt%?$Gso?QMGBE8_?@T9_-*0?I-A5gDGQmJ z09A__Z&u)~14qK$4Oiu+l`E#PhJ*NT9?BR$QE`ueVoD#ZuxqI`u&Yfx-Pmi!-g?af z`?CNEy?%avZY{E?eC0|ObM+z#$rG_Wb^N@EcRQQ041;>ZR^xmB_Wy#HLFgV3!_^*; zfZO9GIgGBLW}=?B%saI7wOjB4^7%DDhfO@)#Xo=XFP=J_J{iDk7HLYJ2;fDhplCMn z7H2bn!NxC4>1)RMi2h}}6>5$C0pBmnQ*jdTw`3>bvz!4^X~MFO#~Ow$%Z|1BoSS&M zNB@sUe_?*^=;Z$7RH==VtB}&1^%gTOJe@m;ew5Ql*9*!TlfM@5%8E@AJ~aMx&+L}m#DA?H7^vTP76M~hqLYPL zLebNtHLy;2V^ioT6MGIVu>hFU^8zcB#?Ype1VH#Ap;?EndM%njz>^nE2iL+(7+>?R(NC_1>vNOCRx6RKsal zS{{`nn0SY2m>Cc$SXBuiH>oo5d{?E`B%h(M_mExrfK$66ALtO1v&4Sg7-?8_!M-PW za?*iQTBMPOxf1A}XDW_4K#?LFB9#lgm*SbMqijjBM}>g_6CSsux-#*H$1WH+_L%lP zaBFKm?3D$FGJWp==W~D?6zGr;b|r=Dpw5t{#%N*kSO|{kvO3O&v~Cy(L+!%XV^$ao z5D+h?d6#As`@rCI69`QZ76NZZ5JSU|%vm=9xm2PhX>JjQ`}&mHVVQ~F+bPQ(?FRJ* zQAZ8yVc>%j!)Lyoig#Imkv%93Y0d#|3Ly{?=?=Qf78=DVy}XIv*(pzI!{Va`75gdz z4M-kc&7dQaGBvW(aiDo~88*cTs%RFBO<+2Lo2lf8(obsQw|B~uqJmheLCOi_oMI7& zQsj;-ZU;N@6pIM7M`p85XC(O&!(vBv3H&-|&2yQ}PJCvkJZT}!!g*jI9O4gakRY^E z`v*2;-yBnvkz52bS=B_ue0{0a{_1%9ylGJpoxNDR&fyVmj0X9*z z4ek{FOD{jAW39i>oGDCEh%%%me&g6f2aZj&>*k$_UQOU>cKYzNCi|;(nwC?JEvA|! z7bu^D6jpTx`K5?8Z7eSfi#;w}zS~99&I!v3LBopLP#jw7^ynMIA)doo{ktF<2D2Sfs49X8ujB4t>3Z8_BDq;1V|)HwST`7poKh`PklpV~^MtQE4P2vhJYK zA`W4G39S=^9G$tNd^5#^}j z!=xK-k=RuxWI;Td@EY?)gjnr&r4p|7nM1q|#C}gxE?6-~ctZ2O%S0)96iN z4CJ(Nd|K6*XGumM$hnWo!7nVPh<&TJ+;dg;U@#UX>I2mf z+N~I3jO$cE1|+;ZDbYijR3?6ZY5qUaHTvr#2ZtX&^v8qm9(ZEkAMJiy?_+j-vU{#; zqU+eR7oRgyza|_PgRj}O>r^j~_3QSPlFTf$QFh;|D?ZQBA-R3}U@~{)L^-Ywf7KBU zeG^-bL7auSxoCEj4Hv_94umGkY@x$M8Ozj1x7e8&sXFm<-GM=F2KP%to<)(uTM7%F zLY6Z;tUeTF3NNIV3%nf)WYx6W!?xkyUe@QQj;BivCbik#%9`hT((scWy>Ns|{AHI< zt~|;bx<&cQTxL?U{3XY3T;w{?h~|K3o@%_ajiTw2(R2n?aDXe>^G!WF&bS!JBO{*WRiRKgk>WN>uY2ncF5;=VfdL@{5F0v<>URj#0 z1oIXSK(6%-R_OB5o#GXwsbJu=04SvAp)yt;FpqJ;!gPpCh988y5`h(GRR`2*Y_B1& znpvXKp|#38bzxA^RgY%EL2DLZ&7yRB%V8CmaL_3nPFg{lg%&DAm2s-+iw0t#16k&k zCYS5vUrv5;vjKkPB4>j}fWvCNehV$?HJ|F_xd6-6nmOfCk7_bHM5gdm*~gXT z{LDoT361G!IB8E+N!a|^aHeQC(%7jf^{gSD6(->l9jP-}Dua3~Cy*s1K541BvF?tr z(3G$&sQ@8G&^XHm9n28H8dG%W1)?Qz4x!B{F~tB3Xqq5U1%ji#*7!3Y^)5aOhjw1o zjTYbJ3clhoS3I_H;I4bhK7Wym!qS|%#XIXPMUvr}0cqmC;&;xV(TvKirucC@=q##j z(nKf}0}cz^h=9XqAxqDdY8tQ4X3qkV+;`&YT3!_E2DQ4#bm7i$+O~QaTswCJe|dKQ ziQ{&qF@Lfnzv`R~;LN7q7^J+!2>WWZz*mD7kKHDS@UjH4hsVMZG2 zouHGcmq~bzNufz~6&Tpz3-4Ufzbf%D2bOSSaUuu=6fivG84zMA2-OSzS525FU|u3fp+S~&*RkMr_p9XtlK_q9k!d6lZzLS=dal1FY0X!Loq0z`^ggSy{? z048Ex|%Xj7UjtV|pjKr-qL!B9Yh zRjf^h_Q}8YWAG(0h#P|?u}SE_|6E0e>> z=Tm`}5>7{}7+dfa)^$l5`>gpGerD5K|GGuC55<)w*-E1IjK)mnmif6#vTD;e?ny8a zGf|cxA!*8iGfU!iWL4HxxgviGC?4Lf_~`DsoR$*Z-2{jhMla4c8OWFZ)=U3%v-#BX7q3~4W2;AK z2jf@hp5y@KTkyaHh2B%0=!GCl+Tb=jG{wx_nX^TNJYxL0WpHY*_;DA~A3D>e&k%*8DVa-OpPg8x>A+sr|XJwP7iODcCrG+i$82xb!^ zs`w%ql+pM!mH+{2a&f-+y)XXZ4NuB2|9?nVp{sX#_fz{n)%TFSPwM-dp7)HtaOA$> zUl_V-&&c5Pp3e=ud)KGB3f=#wd(b!6UocRAeEsT>(DflP__XwE~O(moEFxU<9EuphPTf5<3 zA}>mO7XXEZAwBLeypdB$5waS^FyKK_AL=>uQ1um3#Z+QmP|8nC)TaiP7E!B@GzWta z)ZM417o;=s-2Pc$aGVm23^LS_<&e?u4Ha{c@G&mM3()Yb=+FZhWWwo1iopC`tr)Ze z19cI^i@hHbpqb0p%sME>8dN&l!cVI>gUGy>fp&V6exh#*nH^jMwiNs9(~K zWf;#y3m~IB9_Ok>P#c?-mI(?gkMf6 zEb&mS)q=zu(n^U|cwiWDYG%d^@B*%GUg{pu_Ml=6NFcLzxEV!h*y0Azx7Wm*c=$Ga zDB}P~5T>i};yN*;w4i;2Cy%dIsudDO-?4Q^4T%phBBgWOx$J7MVLZ^!Bo6|5DN$7OrR|ff57f2AW_g|Z3`iw&&)97eZH$b`kQA82xt%pmRQ%tjgW7 z6v%QxS48b%-{tMTd=Sv56G+IS5=}gMyFHO#Owwm_Kwe2R3cCKmPug0{NR15rpgdU5 zXu0ADKr6az-C-uXe`IhY6rDa2}T75NkVIC4^imOGOhQ$ z@)5LmXhU|r0Hl?|#D&}KiAay2v2lTws3H+y={g$p%rqX6a*Zr2E-ih=;3!~99X_V8 zM@`PkK?ANj@u>Q(1NE)KLr)e{t6HWD)7Ym|{1zc8qR=O0ffHt_kocSn8%QS{O;adE zl3y<=Yv5_*TU0vtK}{maLFUs0AEbRip!iZjWgFI{7Ra9=aRqSIfC=Xa1SDR+E(>88 z&{rKmZ2@#gS!QnHk=yT?Xj}(NwZL1Fi?iWt^ejT1uKx)X;qwV6oc6l3HT04=xD$Gd zk%NmEoY{#-Y{T2uV5Z3CjeD8fNyVab7;0gihamn5`=u`i=oeTmEK%?vL*MmAx~2Q=|Vd`q9xNqot8= zjC{KH>mzd`KDsts|T6YtQUGP22IoTkW)B zp%30?pLj~oYtHalpY~N6Yv-d^_q?u|A8s69V?W-~Pv5_ZEp*!7e63d5*iW|Ym42da zuk;_<_DcW0r6_->rJw$>mVWw!ZpH6=@X0j0<_0|N2fVbpK2YeHJHy(nI?keR$t&tt-in) zTYZ7Qzoxg|bB5irUbeZk-Tto0t$uV~u{QR}mVWxz+H)0tyge`TtL=H2kG1Dzex*Gx z^UKfctv}`r3*@wi@rf@=>+d-{6&((B`q(eE_vL=El_cgDTKVChZ{>%7u9YAD*{Alr zzL^gsXtT}THTIF#e*9;y>bbYccOU!dw&gY-ZrdyUR0IFteQVd~Cr9oX&JKNT@V^Ya zt^ba`i+jJ=^T}QJ`HPPKId!^8?YeaH4KzHs?@x;@WUBq>(9{40csHb0Me7}@Hka)x6^8Kk(F5gH3pDKsjC zR>u~CPlzxg18^cJ5(exfaHRNT)0>u)S@RqNy9WkNCqc1-d(JPuU{$~_***(E2tW^D zMhr+oe+}2fh{tHr_znKfY$uFT0XPtE8oU}*c?vz+g)o!gE1TAgSrf4S`}=>Ej^y2w z$TD*j1$6)SrSkhSsoPV9D^n-QB>NWcSQWT!w9>!?0v`{66zqW%lzCa8Yw5rS<%_^z zG-5*1Gv(AZ2KXQoOXfY$@d&RL(`zhqHVu65|3LqTHd<->7H{940RyZJj=Gq8d7WCP z63-_^JyewZ3aerM3Y}99BR~Q7JS?N z+te0R1>ie_N|Ro>#FqrnuH5_z8|=SeW_mW6tdVzJH*A^3TbIKYm>k00Pj{y|`r&UblTx?}3C&2NHnu3U1#T{u92Vm!KOtjPKX0h;nAMgA1WcJ9xWM1E4 z-Kbr;`26LleOn{~m|kJ-p06}ZB$JG3x&gxlNX!l9Yz!(rW>sw%s3i#}=bmXW17 zVu)T#mqA$D=owSt@>!^rauC$jEUCF*F9^e$qZNbd2k{2|V8{-!Nfe1nAdDUv6o5nb zBFqvK z6Smcjy88{g-+Zcv;mNzRxOnrjgks3jT0sr3!)7(QM<)TjYS_l|&>+r0LXxNmttC_0 zkbY$Y%DTbsqiA3n9*k9uiQ!iFKXf42^ zsC>v#13DC9C`mPY!oBEQFYetCGJNcs#pf-Du2n{GheAhJO$uKTm5}SwuELZG=D*e< z9bRAeQ9y;1T%Q!VNS?sGp_J!M$+1@|n^vD(bH|C?-V>CHW%A#;huzZ`pSvRAY5lcA zspwD;)dqylEJ9dEq&=HH<}4v$%Tt_ZQlJ!qc7hv(vkB&C!0-qe7A*ZbG=Nf*`}ReD z@}e(nmQL~X#haGN)UEE}6HK&qg4rrH9TKa`)B#qkK%PC#(xCxwaXu2!3L?i)VoVW( zAY6_SFlrj<4nSf^7zJ9k*z9J;O+SCb&Ae3q-`+L)Ya`!1eDj`*2R}dX3;o~SciryZ zp8vAz9o@Hg{h!{i_FS~pDbq~}}?=-mSArPjH&duMo zaER&ivBF|Jvc!)FS8*$Qd#ceVo?5#6c)0Tu#WitEVeRrK3>sDxoY}VmY%8qjSt;g& zP!;|-5jNN(HxQ2zj7U7WD=ouY;aj0pN}fm#yWU!Y#)V4JxL1h^0?7T#{q zbaSUPgc+LL(Y=4!`^7`SdU@$tLO*ofQ?a;M8Cin#haKHgD`4r}l6#W)$l<6mLmH=m zG*)xvI=Xnj?s=gEntg7d<~uAQ zSesmU&;PyWe|9-?@c~Ag7|aLcHvON%ukm|4w+(A zp?^vBg!B&~N4==v%0n_%*^8(Ec-U;{e(d=lT@KwQc5P{KYE_qDE3(>a&eC>w2?)Og zG89b?plm0ZXwmG{)kUvoUL>a`7lfBZG=`26q%>Hg?g3hvZmx@Yw8`)dy=Lf4GTJL; z+rGt@uZmquWm|hYENCNXYPbQ+3P2d_iLq;m=wJg?6nqHa;Znf0Ad4B3JCVHS&Q4eo&@y=BNdsr*yTzdnCT$QMm;pkJY0Ck1}nPd)vSe1YoY_0n? z*na5R6)APG|FV;jBCwTpS)%N4lK~t2wZRACXknw9;bp5LwiUac4sTEWUdmx? zq%-s?83}PSE~}u)hBm6<2%*A7YR|f$1Lp@H)aGSnn%dKB#NImi-dX0fGs&4d>@Qsv zx^1*OxmBPW27{_cU?oyvp?*bbWM@k2G(kQ!(2@U=yg)}F^e=pQF2qH;U^E#JY;p>+ zU>S;=G}vsw9ya*MjRIC&eDSJ)ZKKT@>6oAoNWpZZ2uM;+pbYGULq~y`4~ECm=ZZF; zaB@m#n#YeKh&Jl@(UTo$GGGJG7`XXFIewc_!MbVNHH$A=6}qdo&rP5+B2F}pA{H_D z{rXIS)Xj~DQUIC`eI>O4<;vj99O$(40R&jZ#(Tyc+uv;H1_#DNllY}q#s{K6wom5o zJi`e$wD-cUyT!D?J~=qexM1b!PlRTv z(4R^O*H=iNn1x~&`lT`av?-ORCN-)ilW6oEoUq_##Fs$gWsXB=f<|VJMuMRi|5LV3 zOpZvelL5AsNt{S^l~r8JdBo71AT~f_3dd9|^qWdJOjYGszzL*E6Ys3QZlJ!<>SHet z{T6(}3)9tP>ZClv!9z32L7qJp1^2qjT=m|mXg-+=k>TycAc4y(v0VR89Zpa%FN?>k za`U6#6BMJCLF-k!BZyz}8BpGb!bpuzmxO{5@+>J_A8n0CEaM@HLnO}i0kqjsVBC=^ zCrSf49yL~ZtjTB@l5oeOK@6OD$FZJ)`s>^D_?GnHWNO*9YT-y_skWjF7tPMjEk_`T zMa)-2pk_H39G<%?nGMVEL12(5G)9N2vQej zc9PL_u7cA~1=Qlld!*Kem`f%iG%e`e6p`PF3F8Z92^tWQSVFucGE!a1auiImCcd|R z_dq>qEePBOlEe3U_`_4x`ME=kGLM*M#U_DMvp)C}3xXCzLtS}pc0n(SvI=9KK=tNM zjESae8Wu0N-+bsmQ~ZRvv86e2Ded&aIA0Q>q|ai{#u+^rfw299aHzsqwisrdq0Iny zV4-_eLI~8td0>#ww!J`s2rTJuC+Ugz?#$;d%A$#xJ1)r=Fe{Tfu9F>TYS4C5>< zA?FvGAvy2hOGyjGg(+Dh!@zR@K+7lZ*jKU#lhYLTzq}WX*I*~-X<}BxrvE5$PB_Pz zPt$yn_^pAWk60e;z;_U8>E-DW5<8O)13aZCzJCYbZ_Gy;MEjr~;20;+Wag**6Z4d-{hg#U-)}tcPFxN|k33_OF>vr?H*5uEX zc_IWEgW!F^%Z91}po}52Il{!-z72xls?#*Zbyc>Jd<#ry7rQoc&L{2kP_cwDM=$}_ zmX`;}d=!O*)T>kpbH$18`8EgwT`Z?(1=6BnH-H4pR#92Pfm-HErH5Ap%c|gIBzcPE zd$gD=IC>dfb8I&b3zDEgX$1?OFtbj&ZQ@cff6x~wG4vr5Pdb9a zQ3?1CzD}+*@s@9cAdn2B%uQyl806##9nnFfQDVnal-2eyX(0oJN&>}~_Z}V^xU1`u-iy2YKH9gx@6iK~ z?f=vMclJN4@0)u+vG?%a$z-QSWC%0h8ZJQbai!qcEbgE+7oTnD?G_7{7_4;@IOT;{a@ zZ!P@}mwJVFwe$*4@CrA#^a_vn3MciWTfeN{(fUh#Z|g7d_NKPW)TuY$Tif+QZ)w*H zy}3ahl8r7?>!+fFmFfLQ=8l*~PR9n5(~$dz;{(vM#^g0~MZ=_`Zn?gY5n{lg8Ivm^C8R>GZc0W@=_xF+By z2TgPr4iQw1vqR5-)j04W6mgos2U$4#i2=GTxD zcoFJbW5xJ*2=E?$#4AI8^5_=DnDCQ z(k#r)z^*hLYu63#ZHot21-Fxnwtc~k^uD=-ng_t=Tc6yvLZnGQgej}$Azce#OY;*} z_m~h3lns#s{_mtj%jIUN=QqCd#vk3}ntjpY%&M?vcf^`y&1RVbPt$}jTYxVpK^T}j z=Meaj>hMm`M1T@d)Ipfo?P*675_dE(dyJ|p*Y8$&uQ%R)nm*d-0qxkK%x91t&v~w;;Z*8?)kKvdj|>rO&qE38Q3U7c$X-9E0cQ znaBU19133cRz`mrALaqryJH_V$eTV6^ zAyWl4JJdYQGF^MWw0n3>xq(2Nh=%8{^g&bIY4YjwHT4-G8!FIMiS zCN7RM)zaUeeAJyNS;9OH2_MrrFDq}xX& zW4y90AFEXbi}7e%K31#h663Fs75vdwoq*R_gzXWzY4xZWedmhD@z(5(*FMzBKX0E> zaD4xgck{M;t`@7a@!GaK+*?g4$CI6vaPY*A70W|I>ZX?fsuUZ{2mdFE{h& zzN_AU>FAPJKxhTlr&?&voVG4r;?5tLU7pES!%+6vor|v?St99&hETfQ?-@sk%V5(e z91znhmwV+1qW)pl0Er1y$`F;^dllJ?KvG2qJHpI2h^~>PrlPDa;|^b7v~BEdmco31 z+GDE9r`njMYOWB(aL})E9IGi6P`=TZBu#?84~L*ZS*>s!Cbgl#3r>LF7|c;~ozN2ws`Mpa2m~Y$xr*ChQT^Gu_?PL2U<7)cQSY6 zFg5Q$(TQmyN*+XIV7N0x*?Lz8`9yAOs_)IX_a&(>gzNS%juyo}9TAT&q+ zB^bI_Xjx#Uo2~}UYeLXObF1i?*rxr`u`Dh3%zrdc_jyMONP2M$K_cj1OKfYckx zcOG24dsVDknWUZej5u4dCUEQ{p#nlBg@Tr2*~y+z+D=zPd0E4uB;;*$PG}=^P;-P@ z(nWHMevqO{zFGaAyPtXYOSg7tymax%s(`mL?medgFO#S!!`Hd|@(k;V-Z(FUDNWVD z(=AscnGQ-9F26$B0;j1|qvuK-hF=fU_hy}Gj(qjV*EgCBzjX2Nw?laK@nPl2!aHS{ zhoq_Ka4m;DLUyjY>Od9c9050{|DP!oD6LQ%rG-Y9k3dSPSs$AtpE>fo8-=&FxUed` z?T5>)6=v?x(QM$ycZ9w`6vtv{a>`|do&dQaS)Fkok9B$r4UZ)v;1+fsZx@85nyC_B z$TSP6Ir82kKYq*RP@nr3=T}9#l}YnCJtuMo_O^V63ljlaJE0C=u9{vB`8f0DM|=(<%$GhU1M<1>`+qlXjF0)Nm>F3 zO=dUbh51_IG%iy-qPkFSdZAtyU3d4EGKe7+{^PoC>+1Q`uG@O9-}O6t-?I0-?$3_C zXY`RHKQb~t{42vZ?fH{E)uAsBy>{rL!A}gnc;IUTKRA%?|4e_<_pQD+_wC*NgS*Fi zf4aBWeQ)<8x_;RU-P;}Y)~{VDo7y!BUpQP%=I7}~(UBVem5#r|FL(SMeyQW{@QX)# z>!&(FudiLP($iR3KBql>PADehO(b;Of|yql&Evxcfybb8G5$mM_SR2jjkoK4r;f}< zcSqARLD7GzGCdp3-#c}1TIXVR|M+{((G$P-9KG)koU=%O|3Ytlf7|ysQ1HT3<;e8R z;ptE}uh_*Mj^;Jl#y@%v-t!ka^U2aUAaWWVq=tDkXg!{M)|t7vyN(8(Gf-RoGp&ywXfJUbQ7M+*nWf99;b%TJ$`clq#Hd6%DhMQ{C@PJJ#~ z`Qty?={NX^PQSr_==2->`%b^Xk9XogU+%RpENe$k&4yF<#Q8dY|2cTi51oVe{IPTJ zo*$g4yxO&fTZfu=j3AnJxO#`p)$+B^N|YkKQPJN8Tu5`R9C?#xZaGtt7r`1{Y{ zd;iEeeD5DVhwuIGI+KW;5J6wBXnNl{dfy)$+MVj!)BTjL(ep<}hxhCm=qc>V4nAb) z;eD5OKc%<4J2mj|{zvWYX$A)H?f)~)2xD%l%I+lC0LTFQBtsmRNVw&gs+g3s$EFG# zEPG36Q5-xt0w#E59A|N+qHGu?>S6IV%8bAE>=`L`s9)decyIjf9Up>L2X5m>cI4vE zA?h+^sWLt=U}UEZvrrBeT~>Y21m_o5fcnUgqyPv>(nUh6q&qk#6iKtDvg5Dme^l2$ zZ`BIqobUNo0Y>Am?ij~uo5E7Y@7dea{V!WJyxG(TeLD2o%#uGyHQ|#mvw$2;rambV zjmXg}t{QYzSTE5o$W;urc#!g2qU%rTw??NX<_!XHmW{E`$@^CeTpl=tLySae<8mIZ zCXk$mU~>qabG*s0{y^c3(+qF8CzzOJh!8Uw&shA~Qf>5SnsGzeCJ%2nm)V8X_@R)0 zA(2e%zDC4{>T|YAV~6|zw<-TBGPC4aL2ZOrxe#(oV1T0QmB4&hu-uVd8(p~ z^Ed}kfFpK|MV^hRS}-_)sf%C|OJIuZuOePxCAh^TVSK?i4mPP+8vThS{D0gv`okkP z?0M_Z;|GrSzhL*@_rA4ftoxH)aa(HmV|N_iJ+j0H2ngJ^gA_ZTP-VCT71A=RR1BLa zlPUx8!qc%{1qY=Fh$Sxzi=!exHDY%G3|eOR%YlsRNIGiPJM@ApFSvRm@6f5^y{iJ- zig)N~hy9%oEDTo^XDW0Oxi9kiWmgk%0VBOq<5MJ?g5+gjYgE99k|bv@&^UGM7HKxKrK(C?|L{T*1hU9JQ1*NwM7f#=Y;^ByRk^s}^5()>*Ud z?K`$wjWi$Re_a#g%PxJ{?D+DZCAabMxrLX_p8O*1-SN| z-2!vcVldFf$;{EXBp?`^5b)c81vtuwnJV@&on-9eq{xOj{3KQWIE=T@z-j&F_||ja zj0@v$*%6OP)Kh7}O_(nPLs|CeMah{b<}R!>K?Ih+)fg=+vQfE>mnwl1FGM_RualoGe(k*OSGTLxne%9>C}`-w9vvSR?5s? z(xkNLH$gv<`>%mC@i)oJ)5;oVL4=6mQ8`D^K39~%1uef6d=-BVnJ6g%sxHB9f_+Dx ztV<5UDn<1^Q_SReQFw*%w=0G^0|M;iW7mptDO2U~!k?s4EoCa{DhHT+th-2AC0|P6 zowI`Cm{Vb#Q_kci{w09`6U*lzhR5GlKQK_gag$}s7E2L~Ir3jM=(i*D#Eg8F%yYwNN4!uhFGptr-F6R0&eBZy;Jd#`FS1@4eX?z zc(JT>1%Zg;T9ksFH1EtR#HeP0Ne20`3NMR1pS_?5Cs^c~90)0MqM{XVdF8T3&S{w7 zqWDy@lra668B#ez;IC3+w~|WqiG%P~O2Jdpg)bI$hpBiqtHF2W)2@8pn%?Dl?XWjI z?fBl&rBDZ1#rs~|ZY8s`7HbM|8Df1!(tT(MiSV*S6zwvrq^c9WVvd(XQJ@8t1_P*u z#jICfNCmkfyo4tE#8*D;swb_9_3A!xXbDF%hYm!ibc7FKW36o-S3q?)^^N|r4CpYHs!;;YoCR7gQt1iC zn3-HHos086c9tmF0@2JWDMwRWCx6BDoG`914%2oZ4d&c72ICMJAR>hd41>#?yb1tR zNh>SWY-{?m&t3NQwSCkR`(u3*q+5=Utcq{jXOSm8<d-t3=cztY6&jgRkH73r%wcRI8`(yb=9&939@YqQ^f>xxpzCIv}vKRz_F zlt+g{xb0)D>Dg_SOg9_Z7hdtgYu2veFtJ_#@V)8y;Bs6;D%$qLx6N^FHFa+?33|c% zU+~kLJb0gfd|+g0^wgnsI(>LW<@sFCpPEH0-S(#2et2t(^l8WYR|U20SZH?JZ3}9v z)2?Q3uiLJ>?O7YChCcgv->MLHV4t@ygso1}nms&k8^3LGbGtm$|3h70=^B0G$mpJ* z94ZVP=r8^yK^ZwzMuFZL zd6@*axl}Z_&*y?TKInIaLVlVBF}+l_um$?QnOc)K$RiFs;@>tXqnETxo;f$S23B>t zB6>MIN%gXOt{nFxmyhpzYWUwZKRTZrJ~BT$e$Q4a-8+syY85K9?dR-?N^D&nL@me~ z?6ID5kh@p&>OsOpaMXqk;A%i`p<0CLWTomOFDP|$dYe2!!6kx1zCLG6h~1e zj0{CN)mE*AGF|slp%A02LWAO_2Ove3CwVtBTbt;!cJri1_G~M%Ira+TCSFFZyUW98UO3%&0 z^e3J@QC*uWoLolhb^VUL;`k$0#k=jd-O1lzJweY_7jiZx!3+!&XP9r~I1-d%?F6`G z0RBo)9*LNofZ1kp2<}NBFgJ(EX_myEqD|Ca;3Ad&b_p<|0-@7yzqEVa6j;kk0|&(I_@a;zF8m>!lYM3hv1&(h4XM7y4 zWy<Vk8y-vLG!~*^L&Wr_XmXrMe7NYcso+{ee?td4jDxbwS6t;uI-e^w zTi#sqjmJH#iF=@O{2{9%-nn~V1A*mMBc4UA5bzANWK_suBc>fma@4B-q$1iIMm+Kx zZbBug52R@}Sbxy?m+nyQh?}j3F8RPEAKBzhuyFi><#A?*2P7RHXFBI0aD@>dGnL3? z$~jq#JTkDCQD}uYHV#e~l1vt4)hbU?)j(B7E;8gSHk&UiL5Y#FygJ`;GGJH9D@EZ#g`dSne)JX9F&(egRfXYVI^QhWbh@2h$*>-k*Id#MM+z4d3UY}ySV?kiZ4R~?CFf?n$qUVWB~a#EdI z`MIfvX!gvU9x(0f%MW2?{6xTmKMCWjBqF)Td5$6#Se=d&SE zyD-uF;@*0;={H%LzfH{~cPBGb(Zap6@rl5Vf9q`g`nzuHtzX)nUw`EjpXfdtzxeLG zukD)L{4c&m`RfNS>AAa;`I$N9gVmLVfN8b>;9g`l44#GKrSEJk^PUdu&#sOg zS^xHe-uk{RY=D)WvvhN;93Uu8%SY_eu{-|F&b;iucIIWj(V3V1m)m>mmp2{TYLXlR zy<2lRkAJ-rFZ#7kyy!o7;zhr@MK_-Zuh$2q26G3eT?uRRobi9^_&a>1DNPUK#um8VASJ-O23TQ%k-{ zhL8(W4EB=w*=T0`*S5j$@QH2kJA8Z_{0_fryngZe>*1{Aw|ju`}gz=^$zat9vK+ueO&*ZzR{jNzU8!kI!1juR|AtB{6MWvHOJrAH@fS>1}382 zCsYIN87y43H5y{{*XyqzSOSIY%m7rQbOutpLZUKeHOg4n`~s0BL(ZOf6peyZ5D`xY zGi|L$|BAs%$Rgpw(3`4^{>SwKu!FZnt0;=`AFAIqu*8^X|EgGV{Am|iqE#^e`1d;I zJ0pdAu1@sLkHE*@yMvc`tE1`hAK1YU-zval{0Hl=8(11SJ^r2@ z{rIg8mB+vT*sg&kT4m=x{SHriTP3fKzjNoupjA@o`1kGn7_>Tm9)H))k3p-*hw*ps zpp36odhGZ+miYfiyGGwPlG^jhq5XqH{nzgPeD8fdkLdnP*U{6K{q6YTv0IKmex!cQ z3V^KbBseu^!-E+`wp1hvP)sxQB663MfsGO%Y5yvwX}?y2zFm~6705%RXaST7N)ty1 zk13IFOJ$QB=oiHh=+pq4FVGN3XCeY=l=2W^$;YNjQL36`sww*QzAas5sh0<~DU%w> zrBcO}TV<;h*_%^0U%%4YTrQy-?x*V$;BGknxaF93gaFsZm=X-ksZmSixFS%g9FShV zmPu8~`;!6i?{o^q7$>5bqW02|AdIoWQ>9dj086P^>caE>!}ES=lR&To$1fSF?_XJ; z9e}wnGenkw8xOWArlbh5W%cE$@F$e{K^&_9{y@_)VtWb@oe4sc$t2}UrII4VS1ciU zL`0+EsIyU?9HF&N6bE3Dxm5t&<$OmOggu|9okPU}3aL!wuSnsM8KfVj5t1nf4oNm@ z7IOX2k3ICiu3fSxE5G%i!N*>9{IRQI-2rH0Cu5C#lgbe-fC(-Uz)T>pYcxVC`67$K za88h)CraalD8>0ynZ!W^-X3M3+taKzE6q|5A6j|np-rykmmQy2j&FxFS~?kDjHm+Q zQs4@Zc_7sy%0&IsfQ*44;1Ev*>>L@+6z)^4oGy@wK&s({M|L#V>{0QMv4 z5oWg%7a|?mtaL@HQiIc_Wdgmm%V?ifw6$QU{|8#yOZukt-qP z62)jLqJy|FSp`&rZBeZux{{@hMHkA)oXU~7Pnyk5hIbENxb~pmn4JIo;}7e=)K-2OY7iyfPpv+Y(lfwYu z<%zZDduV&fV>m$7vQnXDG1C`ba^cl$*YGBimFn?FuZnr+l9i6eJO!jq@4HYXgqF)$ z#VPc$a>V~QUBfOdkxAo}fnJL0l9o8S9hj#wOU2kA*pzKHL44HjKI+dmxwfmvFItXs zXOotW#<|s*T$4u#iuTbpck6n|%ToRS{avF!GxDP0-l1O{e91s}-%s}bQO~P(^>lrI zJ4*f2zQ(a$)u0kA07y^G9i2_)0kBs_aWivscO5xYMi_JK zxMlO`+Z4sh)<5BMH2Vy4c8oR_lA$b@#UeTxB0f1ms$?Z8eV$f2ek%eiMpLa+D5r}2 z7E8q_9oXD5ezRC5nwuoUkQj3N&O&n>jJpSZW@xHqK z>1(IsO_o2Qa9J!$r;;F5CUebXb7)Vwh>CrZFmWdpt2>j$c8XIt_|YZpV^n@yv^rmEC8 zE$Dp1I*1aub+nOdRp}nytw4H{@Tq$wTeFn~(2XMK(Uxzw?$fMc0b1 z7!Qt0NP2j{J3=ZoHevGjT(+idlecus%p7aP5VJwa7Tc9V1Vs^5ag#TPApgOdAg@!E zttrRW%6qH`TCSAkppuG$G1Re zoEEv1klatm{^k5zifvL z&r z%XwQ{q_-Tubce*0OBb_vLdz5vsSc@AmxfUH`J{ysoGE#x;NH)AjuW^=E~gY^9OB!p+*i#qYt~w>3n<)3f|T zfvr$eXcOuxhEU}g<`VKx3^$WZfsURsB(GmZHcA~Ha%bx~Tk++*{LAQ+cSOh+F zq;ig~Ig81;I2aPiM1k}d1#up*iWI^OGKehW5aK>8BPRMi`QPq<2d(;r^EETwOru8~ z(L|lkt*@jAnv|-pBQH?CTdos+aBSs|7aXUm;T{Wpk8IJx`jav!73$OtO0aOwl z_%atcJ3~QAv_2nt9cfJdiaE#h|DZPxS1RCr$XXm~{e^RP%%$Ba56! zWSGB|gRV=JdqP;?H_r~A8-JcsI5Puw$K$Xn zsad|BJF5xp`VW)d6k8e?hg?85*s9LfiCsJ8NsTx(3fT&|PuVA`O)7dQyc6?9ekI%y zjI`4^Yn(TAibY0|p#iZrsdz5i1qT7;F*Muql(XW5;wAMnI;OBBS+b*L+KLWc z)pXe*lMm^yDVc_m{%UNe(m$<{1#~)9Cc3xjgQeI)1xE=AN*VG#ITa_>!h&TP$5E=x z+VDnC@B* zEQZLXbmW;ci8eNw(V9=z)sY+j*0z4U*l`l(g#-}uWxaXwZLFi^y0tX`x?&|cH zkwl5}RgTM*<(jwcs1sJkzqx&%EiWC@d8=gQFea5!Gq_7amZ@}@pzvl&^w3=YG3=m0 zeDDVXUvpdvM$u6-{x942*+PFMAMgaWV{pxlm?(#O-_2%hMAOhItS&>|^CY=!WMEN> zt3EghX~yat|Hk%xwsgqYQYGtCzL~GGmO(MJMlC+SeC!mJ;z?bgTgt5FB->VW&iU#{ z@xb}|-*3#1Cd>lWq}M|QC0e<3VO75ywNuTnL%!b;S# zwQmAu&y-HJkxx=7{e((Do3SEdm>1J5ES6=NwDS^UK+}*yE?kUYt8kT6_4w(;01KLF z#q#)9x9gL^)+y|%Xv(gn&C04;hP`u`qVavM1xZa6t*jbe1D+ZZsls|M3-g@jjaW>N ze|@_?S=PkFDN`j$S)*&+td~TrhM)|KAUO*Pg!^_?7<0HU4r<<^P?3{7wYE6M^qU;5!lc zP6RfJK<_8&&**>sY16ncI$fANl+4#AlbMA>bWx`caxYr>hsk(mdh*bb`Tbh92I^+# zuvd|0Ar}l;r8ZwF&wo%^s*|X>m(J%404Z9)rt>7&@w;?N!i8T^F~lm~2r?^T#G%TI zd=WMX{!u%LEpAPRO-Tqm%|AMk1m*lYkpScszZYHPZv6PUe-6N7GUle<`G{SnK^Mc{ zN)84=I_fX@U8;jTiQbBCk@Qnvn=>#4bZX{>Vsz1yJjaLY*A3LWoBlu!B+<;_166$v zSyTjOF0Bow7!;!ma1Y#Z5n=pl<*bH8I;+Q<$yfE6u{Ibq2TV(-b4)!o>%mBQY@34u zPqXcmj$?V~N|0Vx-)ZyttT8=ef7G!&ade!J6!eTQ{C!XIv2*{4pr14l-`YIZqJPj- z50=0nAj-Iy?Q~f|VY+}DhzVFuMNzsMre>iQszmWc!=B?K+u;*QLUTD+R;J)CLbt$m z0k9~z3FlME&}KdQa24B)R9%41H(5 z8ntRDHxN7`OkM5UO7Q#10$WSLHJW0TXIf3An^Q=Xq*--wIR^q%PzncUnk0|bmy2Pu zQ?`CdquX#N1_}eaKn5uly#vE~aDoK1C13D!LDwB{$)3CL9UkQu>dzhs4|19j-KI|9 znIrpyQuWOA!r=-;r6b2CRLHYUsO90IRQC<4Y_U=dAhlFr$c9l|Iab?)JXCxw%skVH z#+6C4Azm0z_-^9#HiMpj4b&>DvkLVO{FE+QeC9e?E&*DV=FsI*5r(H!x$qx6&yR15 zj|TE3Q*lv5jsWFU=)^@1fP&t4Uc(+^Q{@$8^&O7-9pJz^1juL%p9&7n%!P0CC_lO_ z9!0|fqM6Huy{?K!gtp|W*|2|rrub2Xd0`r(Ija_~q%<*L;PU27@UH$Yy6^`)$`99H zK2Xm$y~?KUj%KE-5zFZ09t#bcc52~OGr>Z-TA4aBKf|0j&yF3s8Ym%f{>m|j8&`rt zU9S^=q~NL!cXkbYCap;*OGs%GD)7y3v1wz6lnc`stzr!u76;>Kot@BZDq6dU;ChWF zF)8!Xf<_GxcVvG_Ex2i2c*rxptA5)+eXRLsoSTmi$axyF^;7ZOY=BuBVP(tIJ9y4Z zQ9}DDSeW=V?0V>{7#kh`7r`_thg%-pLW?3sOiC9_GZ}_CUQ8HZZMl~i+!Re@bm^r$ zF>FlEls8qy-m}fhfn#F;TmWB0F7us9TrOOA#}fYkQ(dDU9LeqZtDzqpyuJTheQ)1= zQO`$qJ+c&T9&iXbgl)q#>%gI3NZD(yij1p4{^l-` zBmprpP{~Ose5g^9&i#M(&IGWrtGfHLCEM~MdqNhHOeT@C=L>P(lir$;K$ywCO?F5o zC~ut+AH=+{E|DBtgB z@yHr4BTKgBmMPGY#@5q4@4kD_J?Fnj%Ys&+W3Lp3bLxMcHpk_wYKCTW_%0Okd&+8} zlezr(q8P{BGw0OZfVQ)t*7gPfHgH1rAn26=Okygxm;iCCApL|e=?ESe)<&8_FZarV z(QUKN!vF-M06|S(DYEp`M^F9aAp-SYFk{z+`?L)`-wCXp(H&BMwndLFl?z;ftiB5w%eJ2YyLqh@tAPBl*53ceHPg;A7orwX}6C-`vSRx4y<&|HBX1STL$o`H~iB1#3m z_+zaQgnG)CPx)>&*n11Iq06qfXu$I`W;NhKq|a>VfQPgbXrsemB>`qPCW^Fi!8u^) zNqZTr1FdK=9s?3+u`?hn1EYg=#Nd@`Xe$l)AD=Q?4LGXWs1}Uw+3n{Fi-!D~8KaJe z*LKo-ancV8g{)uTJVZJLnpsgO&3J%I${j%m7bJt}2v(1?L8y3~VDRE}Nk-vM1VEVR zJdwMB`^f-pV!qOxN+WPeMk$}%Rs9F&f-!9g{d2u%1YCM z+un8CUovU z1vC_*MS?YE$OBKgkEXE@ofVg(2NiaErD^V#pB|rRTt?@J*Ncmq?U+&PV%`3QU{zzy z*$u2YK7azXvN*Z{F5q|vW-j2y;Yo@UwZ*g*|4R@El(-RQ!RgXBrPijUS2~%uJhJ7F z4sB@|7d~)zJgC8Dz_uv}` zUpjdA;O@a&1}`5xcks-?&4YsjzZm%8z>@=?8UD=hhlk%i{Knyz4&Obzd-#^&%ZJY$ zK67~U@L=K>i617OOnfHs;l#TWZ%n*2ad%>O;+DkaiE|TYCN|?X@Qa}z#{YWg$$>`) z-Z}8Hf!ham3|u_?(zwG-=-y8bw?|XjVd0n4oKtJvM=iX2C9n<@k-k0_6 z?R9#e)2s9z-#gOt^Pcbbe5L2Ho)7lCwdXZG(>=HM+}3k-&-o+&G4i94r$#p87wDE8ynmtr4`{Xy*BSQxuHX2nvmf#~<5Pef;;?~RT}-w=Hu`n>42=-JU@yMNUE zmG0T@*LH{9*F?Lz-`Rb4_l4bR_vzgy_>_A{d*lFJJp65^N3ZLit8*3SY9}MnRQKdQ z`e37hw=2K-zJ}NL%>T9A`0i}z`@gKbM%;@v#zDRzm;00nmS8#;KPRZ@S zw?6#hc=Yh@yK)o96smtYB71oBjP8BTp0Uv)XJrZmj(_ad!RRsF5PEk9h2z5`qGxvR z4cubh+sz5f8_O4i}~+!Lc0^Nabh%4EDnxpj0hpCvPg!-{_lQIEdysOZ^sKQih}OmIh{ z-&^?Y?=1ZGQwzS`9W4B|e{%Hl1D71lI+K%*x2s$mEjW1Dh$!Mw?=6oeC=_{$M#&g+ z6DR!VmgsriSSjYheKqr+ijYTfS8n3?ZA<>^$gxqA-^CusbH@F0(Rs97bS|cjUG$%l zJIe#WrRenCV@|Pn#wbVGJ;kH`bW`*sd6cGt$~Qalw;SI$zu}G42O8c;KHl)g@jpB8 zM&&BGhBuBFYkXr@;~Qb)8@DvRaidJd%2&@7fh11`JA=ZJzZj05Ph=dy-qpOzRbEpe(y1p3%;u_^zPB0 zKP7r)?E-;x3Z{Z=v9fA;`8^Y3IcMApj(PH==%ojKiYohf5rX!f>KvElvUvEmCRV=R zJA}`BQ!={sz%?=9l!nED)w6i`8ZaxxO@}m5gqks7Hs5<|jZlB6e21mbdk@sJ}$osEd46}%kSm;@05*iYC^hh{^;Hu@Efedb_ag+t;a>r?VcL1RyurU8;xSt$Vs+? z6Q7XVb>JVXyTnPe>MKX}dNQEsr{G$;~VGCp97nJ(D3@78eV_D;q~_#UVo?I^*=Ve{=0_PPd2>%TEpwF zG`#*&!|N|Jy#AYp*N->6{#3*3Pd2>%>xS2lHN5^r!|VMGuRqrC`lAi6A8mO3p@!EV zYD4kHrtZjj`NH^U zVQT!i>*xReao04wzOv!<6%DU1ZFqfg!|MwhUTi4Ct$Xn1{m!|P)jULV!) z`iO?tBMq+;4X=k9UiUY=?rnG-Yk1u?|MliyHN5_1!|R_ny#CLI*ZZy@cPz<*S9phzMOLeB_90JBd+~u6HeZ^cc`J>hP;BT+i z2mj-0eehGO^}+wJS|9wa=oOL8i_D6PVhf|8Ly{8I?n>KpHqu zCO@}YU;VqW+ZO%mt98wPYEMl5X0^WGcX;;CtKH6Ru~L8S?}zZf7lYOMRzJ8jRyw?i zZ&J7PR3I@y)}^pl*q8ix+dgT3d+y|8ZTmu>XwMh=RC_+=lkNGKzi!XRJl>v<`D}YW z<}>a2m`|_P+M^vh~l|*Qy)G_<<4_E7seRH)w`0rQiga2-|KKL7} z^}$cB)(3x`N&fyOCV5?!w)6pU)u!7IRx1?WU#$=R-ayxjB2SC_d8B7=?6;$D?*6l` z*GK+*Bt7gWUN`jE;C~LDKCq+zmGO`F{jBd9y|;Dyr5*8-x*!GOkd9$X-WC_)1F$~e z*9hYKkevdAlaSvLsUqc|E0?Z_mkwL`H_6togn@8F43OLa+)yn?ClpQj0xg*3|%{WTEqljF$?(3#=nLN&pg3 z)l`HE;Gr-A#tDxY)LtOa*~yP~`;krcaqXnh@x4VbNr9rN30?&SDTWUk5LQJIPdyb>_qE9K%9E$}9mhxsan zdA3vmF9d#e58UItp9K(J4Ao4d`G(92&o&Y`AwW8zhoG?rsXXJL-i4kh#0CVi zc~H2I`0|2!b`_Ad!ruy0{ToC$wGDcri_%Y5`s~B~fD3{SCSr)Nage3+0ECwbf`SM| zwhgO5NVN!Q1+d^zQG@XhU0G!9L}Usct&r4Jq@K80>@<+FQ1IiqgXM#sG}3qQ)#3Qq z{HD-?ZNQc+?a9AhoJzaqV?oSbhj|Ti*hT?Yhc|}aEmUUMx5)El*r|kX3{un*6(>|O z!E)PRJcWV|uOs<*=ZpntugLRh!YejG{93@JQLxM41_l&W>~m1%b-_%7qa-0<9c7pv zI+Q@ISxfZfW1TY=xE-GFp{xg67Wy+g6>79Tb2IIsSBoMA;tYt{@$Gd5a-i@}RV0WJ z9fNzu>*S|8XDrBk8h8YvT>yF*B zlbivSr!d3<=`1|STM*dg2iL;!7;Uqr5b zwimdbNMEBZ5yIT!HMxi7A^63k!KRCm6c}$IYaq>lVIgS*S{q1%!SO7MJ@6)w1~61R`H@Z< z4Q9RMH7p}8DUNG1kvwD!HN!)i12PiQsfYrTh(kW~u?=T`B`d(~!Qrd{*DjO%*g)?c zkwo`@NBY9vhX?+CL>si9sNe^(VmC;|D}IZJUw#a@U}!a^x(i}2Og}n@7J#i zfch|YVjhAzV#&m2i*^&@;7HiRp+FM?&MLPT>lB@Jo8{kHbGZD!if(jI6#J(Uz zW~MAaLL@?1UpgmpIJIu_3ruFEzK(LvGHvEgjs`^om_f2>BS!5hII&m@OVw~OjtJJLi{9ZXttXCMkkMs$CtYcElO0wLhddPw5-e2ki=G*votAy z0`V8A1w0&Ks-d?|1XL)h91zmU?{)O(46H+-baUcIl1|DztUa6r0Jl|}_!Af&1ghert!S7F3CT*%O@ik4KcsJDtzIgcED z1{Bmpy@sX)q<+Ioey5X1M{9v%2%gKB_K4UgA{bcAk_Rvto}=cZy+UbPc^ubR#%jU?(rK`N+yquwbpBPHy4l1oxSW zB-JB~Ur<7zIDvvQw(-PE`zRG~H?f|u?rE`O$=QHLHTi3d_5y7-7f zy+_(m1X;x|j}tzJDJa|cA9MI%E#VEM2&iHhm;sn!^1GdU?^ySllK30o;)xg&ojyGj zK}pnBGnChHJV4vjpyR=b0tJ04n9N#ER_HL9V)~Q(hl59V@H?GzHxXjR4J>V=4orm% z_Xbe_rJKV_YH;MB@PzI!nY4=F6lDS*FP`T(>kO`K8WTRl*D!4;tdE_?aW` z#DtXEA4e?W{x5XBihpY6&-D2-*C#3vfv(%TyU%JuY2;Ah8?8Rgt?o02EZ1^i zK>dULn~PKKL?Q2C4nMkGIEDv{i)2P^Q@?ronKRebMX(vcivt&I1i;T{NVFQkR-d3l zRuDNbp!j3)C#tem(3OP+yJ-B!`DPc^lEQ|{PURK)l zZncRw(0tT-p4M}m=yZx_{j9Wd zy<(iB*ym`~P@OPyRb8l>k&`G z%%&Cr8htAW(VJrLs0ezL1rinw)u}UA)P-vOmnguUEmforS=RBu!w`EQ_Hse@RrC=S zjnwGO<#mx#*L{g8I@|>=-AKXk$#46?Yb!hXb7MpVnnXYF+-pcMW{D-1onz z@1EZGqm`G8JvaKw?oW0{_@Q6_nTtT_qIjunJW#uN%XFGWvEX1LSF4#A{a}nbLxXg* z?6HD`ItTz0iAnkc=tyH=N)Z+agioo`rbP}yA5?&)(i=cW)JgqOXN*I@^iudo^AcbV za4;Lga0Tczc0?F}0fNILhUy5C&a{Pb9>tdwM{x)HT)c5zJN2g>G7gM6KtuvI#6bl> zA?+7nk{sb-mT2_PXn6w%H!YVc9B@j7E;zq{y@{4~L0CGe4|c{lkR$gL%zgN_cvXOp zK~h@>r|*e6d|=_e3=4vr16qnHMh{y@wH&ZN#GMfErJLFi(~0rmv&G1fjhg?+~Ho(>sD**Ofh4bva<7A#-Es{o#09Py%gBlG)GDc|`xGgLPX=nwYS1`AR<|Sg%o|gK;!~|f2Mn4iSi!{_M z>~?gl>rCw9G33H~hej*7NNySceL7th-fU?gRpJ?-+o^YTa_5S#ud(WeJ}WbRS74!_ zRrQ{9`ANuGOk|@{653I<^_`3P8ee zxS+wWX~4k^5`&PQxt}4U0SI6l5Rb@rn85&%abcmIu3_K>Bu-C$zoSQ|WiTJ35w5XL zfP%vL52mYz^QC8p;w#Amh?!N|W9@KQ$w48jIx7o!Dp*{GlKf^Tj}B;t3-c3F1-vjw z3G~0fkkb^$ZU7D%##|0JEo7Mq0REE^xum5S<*|cdTc{YxZ*}(Q$i7u5$$VoM{1+G% zV3;Z#8InL?HefKpl!rcBmIq+P&~%?X1O_Ll6ee;y`F9;XIvypqXaf+D0d_+*@LqL6 zzhU_Y*4TiL%E_qgNdxtS9gg7w>Jw}@v|PBV$)`GbbYN(?FBWtKXe0oop*rdiwSBxp zadTqRb2#epGy<#@(?D0{&_Ql*2ws>oqmulp^#6ytPKpeFCh=QC*AG6e|MT&e_g&HZ z^w^i9zu)~_{^5W7pXrOI)tTGsI?5~TzqFuzuslMk)fMW<<}1TT-*NOl%5;m>m8Q4N zWb1;~i4A3(b6eW9j%Yq;hrQykw^h3K2vA62E+~o?Khgt>#!;Pleq9_paq>}y@i-k| zXTr%7-~zt5fKSOiG53Wz3L(W54i4V3f;%Q{wSYB(K!V*X5(=5an~&qBZ*2PJ*b*3; z^Ji|Y3tA_lkUWfQTiJCFYd&bhJBG&uf<)KL&yA0UV^hV&@gVBVj=BhT;te2rD6w`Zuf~3VN!Knh(c6bC^P5W1XRT})%;MeLRxbz%UXKt#CU?*-KY4{bb zP}o)gVH(^sII;BRabsQ3I&t$bC|9BFfqyH`j3G*2;?Rh!k+{rLoewe&a%KpgVUum4 z78Z&xIRsw%c@BlvFtY-r&cL>T>+7c7(hsgWb3k1gKdc z4`;cc_H9@(LNb8It%p8{4Dx35*62>bQL_LCuK>Z*e{273OH8{8{{MZE;jbq?G*lRT z*1&P`pY(mc_dPwYj^(3QcKs^yzTRtlyx3&-Uv+)1BT50&m+iL_rR$`X)t_sQVNmU7 zl~3Yjw}88X3b5A=@QMm>!Qw=Qlhs{_!WDwasls@%;vc2ZoI=)%Mo54z1oc~s!k6?3 z9SSNie&Ax8xEtfPNySz;pkPKJ82X%lq@9I09vxo~(Fx$C9?qKFR0K^71VB`*i)jms zGy%0lAD)qKP~TUjbIhS$WfGu>bf_C%Wq^l0aoD$)eP(oJ6d9bGiKbbNW_+VW6Bz)^ zqg4ewVj49j7f^mA);WO7@jv5%}q~k`r)$N zjaoF1-;bMi85Yt-G@HRz)_Sc11g_}wwu%iwR>-9?4fUELEu~G-JR+q+6$EJ`%JG(u ziVIv)gVq=@XZT$%T}%)PKIjonr-uyvu6*^rxApz;60q-DgihFxHTGPDnn4J*KSEHo zTS99bavD{Q&}85Jti+4Ly=x(Q`hKjh=R(xXHns{OY7v-o$azvVM3?nlxe&~|7Mc_G z<6}J+n)RJMaODx`5^+~lXB<#VNIfW_4lUaz*I{|b>DNY5EJR)mbOVb$L;)Z&P%_Z# z)Ad7!cvnMncHcQm!MkfAI(|Ri)N>(P-z7*kI3IT6N?|L9oG?~H^z^>e0kHBxHHKP* zPTh~A^jw6Rfpxe4`Dzttb;t?fT!hXz5TQlk+_gxZuwSW*)cQVdfd@DU(LU4XX~L6S zg^#SF5=pC-H43%PwBrGVM6<8lkOd!sa*zuMifjS!jeE#Y?ndq5QYF&NX;Ex0ThgbG8dS15@AZD%wVuPz6+(0=ToEx zk=;II2=`ow{&PX!@YvgZeMB0AUUNyDSy67P7vsB|>%xuo-%&0x>~(UBx30~Z-5nk} zMCUSym;na@A0uMfx(*$YQ?VL=XE#tC0xkqt58-JnVaQY#2u_sN6H zKd|S|?5Yc0C$=D3hS4@3x|6ms;x8|?gicM93&MaOgVCzw1H$^EKv^oY<2EAH9!RW85r~_NB>ZZ{-!+pxN zk7+)FC%*f{57$L-={fk!8LuvaohV|}Dw*c!<|CNAH~G>Fik!bi*KpypYi%DeGm4X-21WK9X&tmd2Npp z`)s$`^>|mVt0(g2*sj>qqmM}I4!g&LRV|%QWTKR8R5g^&} zIyza%O{9L(?hpKNyFc*X+Wmq5+U^hhmo=C+|GWm*_}&^^m+ZPG)S2Z6#)_#YOPgZTE8P^@`L(iTkYP5P`ugrz>87=b zQ|fDLc&)Fl;kCZ9hS&PrHN4iB*YH|Ttl_o3w1(IEV(IYM^l9yRz$Wst4#`TwDWa_Z zTgb9FO%@Mn1u6t#P(VT_D5k!E%fV=;eW}lvhGWxPnvK*N>?LcF&;9)xjN|W0$Hu1h zW&>QPlyBO(`BWz_lXz}Mena&C4vTyzGGYx+B>r&dTZ2Cy>>fD0|KzydcTw+6J-cG1 z=)>I~hj?iz! zngrZ4;AlfQ2B^r0qc`G|Ff+uH3ODSulls_(7^b3I7{K`;xpDDRAyyzDsDPcsCk;JE z{#D7)SHYPIy(5eJjWJgS*I(4fFk00NJ2hL{6R(h#+OZF;HeU(TAN=EWA@5;Yg}@-X zY3MizmqfFXyyqPi5i;nxU~wHJXK?967axZ|UG9E5^;a8exUCY3QXkq_!^PeR5H(Ur z8a%TMN_m*BaYuo;qPB?irGsxCUUE1hKv%;GfrXcujgqW_n{dWRJ-VTW+ba1k_2(OE zxETakA(>bqT60J@l9FI$fi)CdT;xKrfaad6D9>dwfW<@BC_2O1)V3kA)4K+P&FzD1@bcTL!tK^l` zpLE7JTIIZ@KF}HCXqEPndT(cpqgBRF>W>GY^&b}bc_eoE$VtOjC+-@0+u-NB-#qY( z=p6&6_Fogfr|%uTUyS^`=a)UFcL@K#^=d%n6H3>`E9`7GiS$5cO^y#+{0Q52rkzECatR64N zH72Q;F(-F);Gl4wH!>u(5OR?0&;lxc>9nt+4rYJ-T(b#A4h7hef9IB)@H)z>gCN6DSMT{2r&oCeV`!8e3cHcV9JA(2Qs ze)4%8I4FvI>{2F7SA@_y91?L>^E%c42VE+(&TaST~o1= zH+SNo5GtiUiJSulCkAILsOE}jV0&oD;P#LSv7SN39ex-E5fsdM27@5rQAAbQGu7k` z9XKeHO~}&3%bV;ZZHc&x*Z^^Ea+rSXPxKcuRG`s~rmw&x#G`^qiuVp~n6h6zw*v=7 zvIW{*3ved)=^4ll;Gp1*8x7lm!)C#T#X>=88aD{MI*|@U)+U1#E&57m1to9lz(Mgf zZNp_sQ6=~26o^hadhFy!rcFN~jXP4tpn7N7tmdfrSfS8XRU{Jed%!Osd1D6-D#PI% zy(~`V_)_6y(&$nsa`}BH`yAkrqknr0@ud2~VJtZHPaVMXQSXqRaDVaa>7WQ@S=@+R|bU2*zt1 zf#RK6@lPzWXOO7*6m-zeV_IMlM&r!Ihl*;SiT??m87h1}38IAsN`Rmmj9$VYqN+w| zkR8-Y+ZGEqjs=RwGNGW;rlQ~wq;a{?5PoodM7OXdBD+9Mj6PikfqX`V39xD;w|8(} z>v2a|9!Obb9YjY^N}5!A88Lc-yicC2!enOJVX`VUGDX9XNN4BPd?J#(s?z^|O=RTs z;q8fV=$^q>4}75iss6*_&*;;8&+oZ7c2)F-?p%laU3^eN4b@YGBS;Qur?E|YwD0M7n}(mtl=j9;J6pW7V@|O+|5i>>cFH>V zNczsC!;PM-P3WR9S;m7gHc+(v#8%C5khY-Wg1Zha57(nwPPi-Kqj0fzRyxii_laZ+@mah(dMv)Q6b~kXtzj(DlMfQ&X<C{F9RTN5B-wLSc@gI)^w&>UK7tUpP)p5FJtKbYy9ZypL zB$+tHhpMWxUYItHZ>BV$G$nP`h8(sHE``-i1Vj-c=cGp)4QnIENoS}b0K@@-B+Eaj z6$C|D=}9B_ahL3Z#KSdGXK%=1lMYb4L(j@GS^0GxO%o1I3dWZ{)ivKI{UJjFx}sBH zM#vdJ3q@&`c1K~c3!T(x$%~hs)y7F4?8!||Ld0X+s<8vX*MyW8Ey*`^45%=+Gi?L} zwfPrL%I}yegYf_SLPUi@dakxU*6&FA< zRvP@&lCU%7S=Tvbd8U{8KOHt8>OgEi*vT^R4D|)-T@Jp6++cAZ6Sxrsbv7WjABTe# z8IzKx@~l%A#1hZ;Q)iZLjh8a*xI?+|xp|kpeaaaVWkf^c(dV%TaVh6j4%&)~#JLrk zw&t*wQof>SLJNe{0L5qc^dXreLZISjM=phgko?!@_JN^Nsf)?c&;{0KOUCI_qzwb? zK{>Q=gYcBY`9Q?_C`oV)Q%jvu+7&Nd+V0`WqXvY%e)j;^!@fsRjC4yhJ*kgd|NzHKa<3`YvA% zw_q^}2`I|4P^5X(084wEL0BxwfnCZ67FEx*rqHtEsiZ%Nl>rWiKr=~ikiS-`yID9X z)6=8n$Vpk@zNP8d>1l&xz)qdqnd9QSS{QB-0P60XTc}IpDoTHqy_1?7%`0HHG?>{H zN#Pi{*kDtMvRQB0E4|by1Mxy6)pbrJp6L6-zOB7~Kk}a5myT>5{+r=liT|6JPK@*( zJ@oLNz_0Q0(aFL(!wVAL{y}?o;`Q|7Cw}kCnV8 zumcShib62vOy=&ubYPFri?WpkdcyH`JDl3*{U<-S;{*vb`atPj`=#hf2x z^LGS=u{?V7?!HM@n=mW}B%!;gV?d)CXQ6S5UM`oNfRC2DGj-3kvC=6kzhw0p zuM|xxRg~0}A1htjrjM*F(35-f^Rwt;s<;*xDXhgsZeNRwOpqoFR`1DVtdv{%?U|!7 zRu=i{Ryh{j5sYQ!D90|!-j?4x&eHGhs}@UBFIta_{^ojI^nvxb=x?mYMW@%}qNVk? z=nL25qW7=IMekdWi{88T+uq%4f62Sn{*wE`Sn0}@FZEKL%hAg8vy=NK1R8MQ=u%{v z_wqZ(b43m+cNSx%oh!f0+@}^Nr`(EWeEHaxrH+#?I4H_h+H9+Sdd@;>r(1f_NKn;*aDkx71-14az9~8AswNwkze69^Clg1S9f`PR)BX!}% z9>4g@H~}bs#iL;iQbIHiq+1DHAPw4vrWp+)=V%Qmg3b15&ia53Xu~2Hk=iyLiI?`Z zHvi|=m-?p10$qX1s#g)#nrn)vI*2O3t{$`Bb7_uH%~$Bg(0|5$DfB%TP$HlbATdDb z!4Ckk_kH~BDZr);dIb1lf`n7k!oH+VoA&9cs9;f;{y-~ezXC%5b)$l1k`5)ndup}x zr6Eqxp)*s@+2|3_dCV0R$qo(bDg7_HKvZBs5+iUyW0EF3aZPjIsjlN0%JTxoPN9?w z@=kCDsb_EW2xMUM03opGg|gH^obgaCp$V2o5klOlpgM&Bt^?>yu#;K+)KGXv%m}LX z&{tCzlx~lgZfWr;DCGCduejFj3zYt{RkTIrEC~Qc?h!&A;ATKR02I`0EMUQhgR=n% zVcSe=m{Ia?j)8!Q!`H^<6>t=aCV;1Cc4Bsp41}-qgu_aq#jcnxI2$kR<1%AGkurej zfb`{uX~jX;m&O?& z656N$k61Je1Hly4NvF;)jmJwjw04*0O6`@!Y&@TzsO{^8YJLSwuB;3QTuD2=Y5tbfiY3N$`5R5SK1sX_e30e`$H&a;Lt`uQ3(D#B$M4vOr&@bg*1W+L~Ihbv* zA3$sa-}XXq6x@7FX=!-V;^ob>V1_`mq79QeujIr_=eBFSl?DAk4kO}+;=c`o0v#6N z-}&J7JP;j*Oh+1N9%utvW}1)hEAT(zwTWjQ%QsL0;MoTKUYhl?hg%E*)d&-iExkA3 zjzAS0{CDAv0Yzs^2QU>Fewx36sX&vSEQyv=>X~gFqCYk6lNFcM*!)~^!kP4T1!PuG zlj$_`-2onY(ziB2%6qh%*+6K{^R#6$J{STYZ5k#cy=(wb(k%qJ z4Vsgw&;YUZ>)XOFl@KQ9cn=LQedFFdy}6=5 z%|VZYUSaD1m2H95B9jC#A~zIVi4HQ_A+5x)&b5GZl2oK&w1MA(lmmu@UN!Ft1Q09& zIS5T^9z74}Gm14xyTGUi117{pFl{jDx&5F$+0)5GxK8Sv(p~Y=p0T@}hX4`eAcvv^ASc;> zkvGy)_68n1j7ceNd~QRWrCW0>=Q-#r_MsOn|SB@H!pvnQ5?_0RS)q$Q1l)04n?yWG~4ldpZ1t z5EcPuUvl@^1V~3=k$Nd}V}Q_QxdoTQX#$85m}_9m94_VAhaL;dfhF5upbin_fQ8H) zPdn#SEK%XU0t84I;{TV3JTEf*`NUg??i}1ca76qc``+LCn?28t{&)9>yMBXzTG^lJ zQ)mBAqJpFo|Gv#l80XFH-xUeO`j*am!|4vye_?J2S3Y{>O!dXVW^d*wC*Yf>wb?Tg z6+~NGB2|CF>l9d9({a1qy*;;F+@3XidZGfA>&i!?N47e{>f^S+$+%U#T&pOuT8Xi; zhMqGqHnkHR$7m(8zh-6Tz03xmBKot%5e2kKk`S7Ws|GmwS@Wm^~#?ferSvS2SaHG&kAn z_lmY+2V2G1H65}&_MT@h=-5-;F3y@gxf-mtY!__}R;xhErh_&1k+DxKH@nW7J!usJ zR%zTIXaHy$F$YRS!mdE!fWt77JyO~7MWqZrmW_6RgHR}FO)6X*oH>A^7z5Otx;WIb zG#V-*R|bcQ0y)aJRE8npQ3|pQtcm&Ykc5{1^6TWN9KPXUK|z9+*mS`1U(0`Mxqw-- zCnid!9QImV(<&#~xwaa;vjfdKVB%=xa3+OKm8Rl{JZ>obX)<;WAEGCV(6LIb6xJnb z1sv+&GI4apx|yS_g?J^=L2L*hu;{die6A-%MEK3!XH$vNW{F*kOIam$wYt3mH#^PwoWOv*#+;r5EQEh}rBtV= zB2(Ca1P0H`L=BOug+~Qdqcq?PxtT*CR`w7U;a5O$<)?sBON|R{UWysS9YO>3c_bns ze}Si_I>#}{1P2Wj!ZRRuH$Bg`e`WjEms-V6pG{Wd)Z!}E-Z%xwcll7~5cA^r&w+`G z0(1tZA+o;0kwb(UQ4A2|2sNPY<*T%_CPV)IZYz36yPBV^I6!WA#ia{-YjeyLltE*g(GL$luAA9 zW@G_a@Q4>haxAnx%2W!cZAxqCZc1T0ghq;%IY@jtpi`RSsbs#IP0y>Tk4^1gYF<5K z_V{Y7+A^=&87om!_C&0JXAD6ty1_aJ_cXL2UgQBa19MYDyC6J9b!cer{~Y~fC^YEhM) zzG&7+RHOpBNE^>w`2lbZ^a5Hf)ii(n+hT3GxSZTkNxen6brj2_MbClutX_>|i!00; zM3RhvA{}A_Fz`@0_E3(2@D7EL)ByoO7rOwSC2IFH0odu0qoO$-B7$s^I*~Ip{TLnh zugCplsU_#US*;qp7MGkg2wtnQ70n0lv&VgDnKfwo*|V8MMG8Zz5Um{l>IshJ`ts`N zVYDg{(R36eFN#K9)VtIq+%}u8#<0bleI7}46=T?{rbzQK{LN2Ae)IFo&Be=S)kHy)gOTCL>m!2?cD*IKxBFE+ zw-4MC3;L(x}#-~T5#!y@FYh}WK@}dL@HOYi z^i`~96@*eo9n}c;;Z^s7j2RenxGEz+SrkFXBPvbG3<;rvC_hKdAQFrb-5 z#u;>#hc=8w&0fUPk^C$FmWv^aN@> zc`}-AQ(`tny4j=}j4(1dSk$pH44yh5@GA`6(8)1?gHwP;r4y|qpy2rYVNYNX5XcfB zdhnJ)R|hN>Dw}B#J)(nyb|U2*s1#t|(XUHgQu5=K^3uw0ee0Da6z@e81fL*^nSg~{ zAdP~==F7`>rZyfRgBpUZQC&~`>;hD6q(qM(I|?b_$>>^$0~3fVQJC`O*AT(MbxVYT zQkQL{dkScbhLexhs?>xvYA7mw0^quU)pdkbsQOAXikXpt=zyjeYFln7`o3WQHTWAB zPY=YWd6?VqIPWS1b?O+^iCcb>LHSMaH+Z-7LND?s7U@&f0R-=PjNtF4-Z2O{{rNyz@8!R zgvK#!AEcas_qqmP(lqd53kHOTEfFGo2xX=&-w5*?%pLp`5WzrbY5u5KEFjp7UX^2k zmCFE(c0dNB5lGn${XuESQP)N~kWL#_TQha(bUZ%Y-`X9oW`A?d!&;rVD0O1){8)J< zt4MfMd)UEPaRU8BR{V)V-k*bid> zp(1|BPhImrXe$F!8wPZ9*ck*?tY|chfFB~Tj2azMvKfMbk~d2xlFjsnfX%b|A*)Cu zjW%|ex_Y`NJ{@gsS}eY=FT0^v*8uc8`9|ps!vVgIQz?7}K$HrCwwi->8;mT_!1O`^ z31`H27q1YJ!B*%xBYTM^Fv?LzhLs_N-C%>17fC;g&yEd*8h;yLrKasc^}*qXPl4oP zig5t6L3YJ3OkMRqA`J2zpqoigP$!6?JX@%X!gsL&FknDJ_Zf#am@Yoq1cfQu$aHcf zS3>oU{#WYC|2f-&$xeZ^7GmUSjR4?Tmqx*owkbNx$Vrn5v8f=RP9qtk2pyCM2yASd z0YD)6sA#DxHa5l478VyvByzaG2I%#Q4HPI*GK|sShv=lHDfWeAvNh5J7jQYbkJxcR z9bg4ucL8ewJR?95+VZuLZKWOYQm2ki+)YsZX4~RsVZb0wS5hRT%v~L2)H1_`g%Ca> z3b{}e5dnnLg=;8$C<{&Sk&8QdbS&LSRzh$gNf8S*gBuFT8n0@|GTdqe9T9#<<4=Q) z4VjfWih)Df$4r9)q^7R#e_*>uSQN5{C_<0y2Ob?e?0We!6ygr8ayrX<$-q(oZbI{$n*No z>3;dhi{oF8PsHP0r+24Dj_rC~-zWNVeTm-p_1@6))1FuLoEUpFc5U?UqPwDfBio06 zRxTCX9V_i!`4J8V)TDD~HYgPGHOihJ>@0xC@oTE*)tpc0LSy-z<*cwc2{o*cdZaa- zgkr&~YEr>JLM~239v&Wxm7cfq;q2XoJs?~)Wcl;sW6L-bq~7+NSgEIpi&alOqvX8^ z9!=)=j3v(-eqp2q<*T~yyOslg`<=1U?M)24S^>-#DkYyuc%GH?uvRXrP>eVVAUL*! zkb3I(Z;O>KXyU?+o{86z{~^=v1&jFCB`<4I&8G1J+nvH>&KXvoBbM&#p!N*{k^3&uV?Y zIKRoKsd444s50?xfV6BLLz=m=%{aDFw3?5kFYc<}g3R8vYU)}%TvKANlgZ0|0 ze(&z}%dCEP9k2bSb-eZ)+n-Usp~;k^;fb3Egn>(6HG0ZU9SvZ|zBzxJ1W-P&LBwQauSJVaml zpd6gHx~B8!P3T*s>dDV`GbT@ltHH23u2OeKlvrhu7h{uevE#I(zBsuI0h! z9qYB3yt%vYt&t;o_s1iVp36txFrp7UkoaZahx+v4j}PCJIDY7ZL+0QoPzBgL@V)+r z`=1tnN&MK}{c8#Z`1Qqp9f4m*;MWoObp$#u0zIRp?%4FfrAqN&MVTTfgrl{V{2p?+ z+KbV>fjb)98F*8Y)r}TJ=e9WNQN1Mhn|nW7e!npK%!(N0`NChy7N*8U7I$I_>R2JT zBNyyF-el|Ax+hk;<)H2SP`(D9n&kuQD2Rc{JQ#yf>JAh6qBC~k9b`w>mc2nnv*gtn zZ&pfATizRq#IAiotaL-uSFDM4RI+!*T)d;ILp!?lx*M-68@G;9&Zdl8PNBlwSa8ga z_(0_FhN2DM;PR_tr7cZ=gFW8FXfY_D$W#k><<*wk6Awi;-4!cM)jpE2p0_(zx~cZM zR#QD^WntaA%bC2AU}JByU{GJn^~|-LPE5jL-rHTBRNVfZNRSnezn1UO3Der(dz^7k zth8@M@t=E&4*BV-{03$6+(ZCUDmeaVzQ73!R#FaS0vz-OvhGxFjP68GU@36M%1<)F zVB?cpJ{O5ZN1q=no!9h8gK;Eqg3){d2|!vkBG5gGPu$vEOv#&Y_K zF0tTJRsQl~z#CHB#jJI5a$?KB2SX1}4(;Q?9Gi%hoTkUpxHP<=h(Staofy6Bx~s3M zEe>~}gpL7lP}T)6n_C}aGrD6}v;rRLMawHv`5Ph3=j=P?D25plzSQLx#!594m2(bxVXj;I_uv{n#1a!V zZ-|x7T;L*0Z{T&G#6FzIZN2=c$yaxsVzA{hzT@*2`T9$L@}b|MY;`+68f<#+%hql? z&JX@S;&xg#R(e*`aa(l3y1IS|W5qgLu5p|jD_yu+*9eMJdxFEiEd2k$t`|gx-;uax z@Sg{Mr+<5VsQ2-nSH+&u{b}Cz+mUomKj)Fl5~Yg;-6NE>)XI7KRftcu^92P$Nt`-*rje#kBs@E|{tqr@KDRRu(XR4Nvy;z^8nf9-}WD-#v;V~cE_^@l*Viv4Sv#qx~TJmcYN&2do;gXhm)fn#NDmbJhluf})1vF5FU z3KyG*`*4^aFC!)z1q0Lnk^cD-T%l=a_VQ|k+cSgLJHoA^`J0}`>c`d3EH{m9*Mm>!N!0$`kSG+^P~!~XpFC&-BuUl))&$Bh;gfyRn0G}+E=wFmzudZ%x%Ph15F)gww^8RS_#;%7)PUyZf{>lFL^?kE9p%vq==K%7D!2bu~dFHpqYPUf8mwFC_V=(C6f7?e&Zs8|@b zWw6tL_~fHq2_rM)?RSQ}BTUh719WO&u#BPvH&7x>SbnMZpfoG(tE-*g8S%^pGKj90f@a zaZX&jAl4v$3j14w%%&RAXk9aErJ)=FpA$K}bid1LC`m`KEz>FKP}Zzk~1yy2i+k zBkdr#wIi$g{QcGtLCN@x9KyCwfIgLB!utsyIFaYB&Qwnx6 zq30u&jl=`bJQiV;d{eh}_yCcY2{W*SnVi(DeXOnw3l6#ge-;sD%+_Qnf=82M z5maFXLU`O^^}+xWtqnF(wkY-)sP?9*q;B3I14R-Cr!IUwH5=bKF}t)-fC$h5GVnq* zNgl9#q$`7)9g;EFlOf3udleHhRvvt;QsO)H^)p(_8LO-7d`_SPRUK6jl($*##rjc2 z-2?YGmQ0oh3n3V!OxRc0;T3$F6||;FY1sO)6~w*S)m%j2aZuEt)@mBQxC&c^1!LSp z^M`vyrjbDeJ&jBbQbDFcf5;0Q^olJ-93AXburV^&w5S+q24-*=*6J#{4d_DAlR!TO zUw|+aWhZgSGvQ7%YYhc=z0lPaPb8plrbu}kW1KL)RqPNj^bC<)N0|YoCQKCZ%Ojyr zBln`E18%&Bk8zkm90ZkzjE5UC$)Bl^Gp78FG0u#M=O^bH)*Dx2x5TqRV~z32;jwVW zy9BpL(sd7COB_70&CH-$#k5C$9+fZB0cXRdcnF1K?8)>+k0N8EB`>xOXzzz)NeGxJ zj+a5#C}bC#zdu*Aax*p#TCryvuA@usl30mSWxQ7O{VEm(dGAE z{=hP&qUlR!{W`ma7OKYwpL*I}e`$49Z@x;LyFQ0f%tUctaa42|1k_w8ELtVLVAiXP zW!oSzt5VBqH4~_vJu0{T03v2_Ai}Pv?K-aV)62M4xb1PRSc3`KYC*8+Cwk{KJFj1= zqJG}2(;nA~H3(j-xl8lGJALQb%Poi7W^YTBZjlp8i~7O`2XHm(UaclYO~*3$Lh!^= z6Y-MSY&DMU*@>&@Kv@b`Gmfp+51WtUJA(HvHz&`TeSX!sudPVSGQtv+Z$-Iiy$h4g z?mOVq0{dSE%EeL z&i^lt41Y9n-_VtV$^Ng#ALu);=kH@LijH=DEArxlj_=bqK0?x+le9;+nA28#0!OW` zrhiq;P2VtkZ}n5tO;5c$;lt)cRFu%E7E(Z?D@TFb_N9F&%BBWIQ5>BqeSU^)9 zwO!3kJ2Xk@pU^7R4stTSkkUz8!0*`JyXdjyBQa`7+r4db;tYA@0UvMNIBH=746lgOPYjs>QDA;OV^Xcrd zHy-O%siDh&5pS8@SB$Q6NSpY&%8zvqpLEvg3QD&Y}(?+^hk#>}i+^xVm zq`<8p(NC|&@S%MnQ&0WA=|z>W?w%Lc)6Wa&7hm{z+4i;2DO;5zO1X|){6gnN4eQPFbFarK83*g=Gs4>z3 zh!>aXiwL&B+Dw6tgiVJu^4d(~wdX9C2XCL@@0N_ zF-t&;lew|6Y*jkYLDSo3?5>>-nMnYNFq@}IjgB(d3YJUspk2s;X}a|2V1xZh;{oE( zDKk~fZvcQP8Mf-TldTccpulyT zkLkm&jy(K^)QOIM$nq0nK8VJgBC{vLzNWP)J$6uv3!%y1qlP&NNj8hYB88jcihPZIQ4?q#R3n z1;(m}csqF{s~*ivVd-<&UASHBCOr26?gg&Ve5!c=MfYE}Q~(D!;BV@B zb|m`sk^bSQC5{|AdhpzV8~a}nAMY#lKG^d}_rJydwEJj&V8K77UGd87t$nfDE^#PL zz6t_%O(w3RrzOW>4RivKbZk$i5;z1}BaJhC-&a=3P$=CPuj~$Oia^Ens@095)wM1) z*_q>Nb;CQb93mYy50;9FKcGv^i0YL9lS_{q?>*?WLofuUD@j%}ncSZ4~; z0*E?^w7{Cvybv%tAb(CDI3=l=Cwb(TxQK-tWKN1OMaF?U_|PV_=B$3nj};;!FliT>m(IfrjuK-?RmVXP z&>@T>ETSA}NU4d^UGdT$$@p4$E~myObJ=n=yDSq}W8-4%jPm|CB8sq`v*<61Ph!3x z$2a+5om7niH?C9)SuTi3>QI!dv37z<;%PXTnLKlvMLi&+SlHD{sW1)pJjANj!s(JT z2gaTr&Sh+#xPfA8ri!`_`5{s`3>e7UMGhL+OB;siry_ewZF>7zKWuY)OnAj$b%|LbKsm!Mre1`ITlEjwiBqqN`SG zW>jia*@839U55CE@?B2MP0>={E7E-#z{^0GsrZDH#7N(5lQXhP;!fcg4=AxJ2C9Zh zUEpGjiB)mPO~wx^!)aUr$}ZlhAl#{IxLVo{ZTO0c$~vV|_TWc2eqxL&Og|3pn$8iN z69O||$WwURDXBdrFJAF2Z()vCTzlq?wbBk2Q>O8q=r=(qut1u_0#bA^=Tuuvh)}}y zvY{r5{iLv?s1^X(HqvB$bQ|y(R&X}cu%xs&^BJ6;T`<53=VIWoVp+_|+ty8-{O}1w zq8>0UhQ6Gwu?M9}#-r|QctcvLvC=K^(%CIO$O{b8D~vHPAoc8!d=;dqILu>NO9kFf zr)|Emc+lw>n!#hVXis6u2^p0Z3MV;c90xWE&>-=#-n1ZJDvS@4M$6P7u^AYK@qFua zT9~*xg)D9~hcryA@nm%2n!%*Pk8pSC?szHRmcgOL80PMTok_t+%5bRs2RP5ev8}u( z23}Q`W93J&o;C5*)THbKC6gl);N;zy?T8Je% zR=c$Q@EfH(J0p6dI*{j>3ZAO1z}lf6$aSOoa>n~O&v z_Lb7J`|e)wXx=q6F!(nv2aeOi=jjSdmlJl|r}_+mIs^qh(LWiI^D;QHKp6 zp!JRHfKAPa(m4w|Yko-8UGX93L&Gm81cnEx(1sJkR;T-LerdmBTMRD*&a}%S>ZaLj zC_1~FA7^TOytFM|>RS0Q%3_mn86EZ{_9N`mA*~>w2(g(SG-Oqz6bvD+s3eXImDT_b z+JX__J2N<1SnSod!9;enG!;dQPCutX+3|2`whfF(Np}LaJ9s=0qAd^0cE!Yv8>?y( zTMLQC&F^6(U+RRBSdI$3!Ju)eL$#(uOakqaEn^o=ojwh}E`-Pn<6DQv%!uWTnDJO0 z>HJZmRyMzYk$kEXMiS68)5wiIue{)p{mU**Lz3yFL7bBTVm2CPJT*X;AwxNxak89e z7`PW&n~RL(u}&C?!=r$Cv+bq%i^EO<@`3q-kSMil9x}Ek$bYaKJsK}8u;N@yD>@+6 zAcI!a&97u6AMJ#Z@Bm;~3@Ax~nMws7l@8C;1zBSQR3JTe#QPT3AL$OKHBX@z0DOtG zh>j+{$eSk_$!FGk-J%C;;mqr1XdrMJ4L#R4IZvlWb_IWV8fOSSfEgm~C7*-yj*&E} zF{f147C=>-zriT}X>nWaLs))A(|C`An{;8$ouI6NUHA5!IYISjMGd*EBcGL@%-}Ok ze}$H?Br9naXP0%(W^Pf-qK%xP2MiYx+)`QHfnx=SZXdE9sk)h_HC0a2!HPg#Bufth zgRZ`zE1^w4isg;G2SpOy1stI&bp7N=z!IUNfI<4~Ysb7j{^7_`)6sb8{&{ORk-2xh z?=R38@)k51q_3putm@#nIEQfnq2p$Fon7C2`$dRxeiFn?`~)ae^~ z04u|gJR7|cs=PG2c>$)3{6*sg2IHTt5AeWo7ciDjcES=0qQDJ2L$C%^#{#AsngQGo z&Kguo@VQ5O1vCeD-?GV`D1d1+jG3!0RTdB)n?J}%W;$Uc8J`VUf$ht!V4sp{X^L0~ zSOG}gZpeShFDSEvm<0Hvb57Jq{zxi0Z6{ax`N8n z!XC_F-{BD>t%Q;9gcb-AgqS!!`UZdr^t`c$iG6jwzrTZOkU_&&)&URU48uOdek za3o_D2YX4bVWUDtUT<)ZcwjiSk<H~VmxH^2E{jECoa%Zz@{W1Ab=^&(Y^LI|0f5fm$fts>^L~vvu~ifg(f?tE)=&c zsu^UVl1L-I0e~1dFo`t9ch0@w5Yn~|`qH;~-w2H~DGtl3Wf9V+RJX-lijS&EfN5zzDn*9<_M2_VlaRW38ll44S1rNl}ZnQFZV%#nF= z@S~)~BLTmkdL*zac1BJN2(73rRt6e$5)w*Uwkx%0DPN1Aoxs~vjuk9#9G3iJerF*5 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

0F6lBZ4`0Q?vGH}H}W z?;U1Jj?r{I;Rdm{i*!v6);@ewNyJfcs+-gigg}m!KhfuX=6{{lzX`J13s?2VDAuvN zo%dq0dFOP7?2ooF#AB*0yplTXCD0`lCv*c`EbtrH9uE2tXaK4Y;Dfi;BR&8Fb+S?X zvopW52{!CYSG5LXx3=j?PMB{VFh>5~)p* z4m5_x0g-|mZRUX0ooy%ttU{uJ96w|HIVS^~ymnP>9?_j?y_ac@D2gqPEgZRksMVw< z?BGfHpzOj2jmT6^Xr{<(q@BuA#cD$t3~&d*RR~k!6N?@;T6503;5@T@s61I-qPe;_ zS!+wLJ6T?{sx%MhhWdw%F5U{5Ht(h)$aiwSgl5GAiP_y}<0DmK%L5Rt^R+{&0+4Pq zn`-bdP=j&ub4{k6sXn$_oZ{9uAkpb$oJ9l5H3 z#*5P;2S`J>DSZ7FbQCIMtVy9QWINzVNLtZb^U?~S#jum@ht7TZxv$+oVzM@vdsZi# z2Q#&;Wtty4tTcyp29m54v7zwuPI9m@sI{Qgq54TRhYB)~A-Gve#J`uNTo9>D5ooKE z?TyYg&vj3v3*M`10_#cDM?mCovqWk*3FZKRM??nE( zp5?KN`VO?U7I5TZm0Jq+n!wsf9PURQWuxEdD^2n61wsb)6Gwu8U@RgBnMGpw{i>!B zNCc7;jUa+lgsW^K_(OykLFE9t;X`_qJi_9Yoq1PMHG(J&TpNzG#Hv7v04EVs-e3!F z2R?^Q-b%#kAkawvGYB~TJ0 z1I!FVH6CGLER|Zg;FR}a8^W~-`3o_*$uWdXvY7S~{}!?v$$2y@R90Nj!@|PSsMyn& z;k*p}hTRKzIAXUrm#DFAlFVX&I`zqtD()S-?AS_wNNJOyI;w>F{TUv~(MAV%U8>FJPL*NePiQXMW++*%7)1*^eaAk!u2!?ae_6KqmN<| z07HTrGa%&-+7<|IVEyvGLX2VwCEJTGZ}F;5yep2x)U!=m?X(Bz&r?KX(?v0ijvlO6 zNi!p1NfI1jeNnLunJ{r(upBZoB8(}%pcC(kHVBdu+@~z%HTjd&tE9++&;Z91GfE|XD4t&2)sc6Vuys~+JMpfNf6xPz`J2o;3N+-0kgUgA73u6q z*=SO8pbAb?k-Y@xXi*K(b|jS?37Q=Q#oe8FSHw}!c%$cmv6t@FX>4@E0&uy) zITl7}DDj9WVQ`oVP}k%zj`|E0k87WHPDgH+@IsQbLbsF?5EBO`D(Kac;lRY;5!r}O zkLDOVBobL{NYC=np?Nt zyLl1A97}QJlAcl%^&w8b@AR1m%0MZ@<^2oCdV1HYk_T6>AFIE4wYFI0?8NZ~D(NXO+4p|Qd6!&Rt2>=ACeU8p+ZegpTbo>a zmCDE^t>7hkU7!2r=f30SXc1S#&Eis6cf#DWdR?<@EqO#`da^a7X`J;U@FNoF65(Gb z`X$6OLF$v^q0**&rx0wxo`8NN%^uevkPOxao>v0TJvHr`qd-WyFBbPiJFujrEm;iH$h&CD} z8ar%k4hF9n`Me7G;%F>spCGNI4u)9Sunim?MAqAw0>o3SD`zAdxeHEv-X=sWdseS$ z9=}fdctLriWus!thf1^$JhmeZ0F@U)(%|9rX&5j*O*Ds^$3m39FrGdQ;Kve=4wda@z>$+0)o_B*o8S%1U0XZ7ml@#~~lkb|)JuINrvv#8}MkruAP zndb42C z0_<3_A-rJuFHB>(T+{MT}0Zy$T<=)a8qa{vDsy`ukDN4_=kNdM&{=MR6n?>ob{ z_B}N8&qF^wbV=WtgJ19c#9-9Bci1YZ& zdgI>^ohAX8TYx-_SzPXLHrfx!rQxxjTPiIb8lSl(U%4pp736K`ncBCYt4Y9KUUAKK!{jU)h^@pvv>hi=^u=l4TK1 zF0>Di_@)`7o*nCi$Uuz;kW)0UR!7PsTR)e_qOn7-_wSbIdJ_svmrFtA6lJgG0ZRJ1wW=@~`VUtLumTyLx`J@Nb24y1z2`v9V|7 zl)hUBzTUeqYK`1Be1Jd5U+c3UHZAaOuul&s{54iu3|?&R_eFG{xoGPYJM7aKk*A8CKCzt#Ct0D7Akr&(f2wx$?c&or??m(R>hKRBDN93 zY*@QPpbRu$A|mDSC)?vD=#AL|G0u9A5#!B34HrDBhw{`UGQDmn*fu#|Y252Nm*rKIEK!u(W2{&bRUS3rp zO&$Q24_ku|)3$^e&Wj79Wa9CTzCE4}eoO~Ej)T<%YmAR;Kv?J=Ul>3dMMoxQM3{ki zH<`bftx%Og3BbG!1x=fHO9tjo#Pl$(@J%yGx(Lb)%;u;@cs=QqSPJG60AQf^u~CAP ziG1*KnlMnvl!d|L1QYMfz)hM0er6qsf$qV%XNn~K9M=T=0^A2W2rPg|zz9vACUzN& zA}l7+e!i(u*;>{g>dl*AVC2#~KgW=0(4L7V>Rd8du=)5< zexxl0_h{nnoqcmI)7vC#o8x{B%Z{IaU048v9V}J~q%O6JfPPPbtigu`sY#XZHk`h%Ycu?unVV1nhPh2j$g6CcdL z^a2@=_gaJFh6T;jS-DMUvphQvRZyQI6mA?SIYM%XL<W#5NoUF8nF+M3T1}e9w-}?vm)+ikh5YCB4|dz@+{vVmQlV{4{L)q@$S0*|A)f= zdw5tI{O5u97Z&<=_Wm&c{+_w+ow*;jrA@f*MOOAaetxrvf5Sw)O^Ntd%+(ils#pYm zH7=A`6`U@>S=I~qMQqrBy}>C%{Ur+`22$CAM;!36vUvb)0S|JP;@OTS5{sY!`L`2@ zMOH3by|Q`Wl52$1Q`saXGK?e}xNnYrr-_HRuFQ4M>J`lc*hxw+dR4v--kL9YS1j zI#GORGAYSH`yi_@phU^s40wS0qry5$rH?X}E%IxNHA?u{9+*LKo%ut_hH%HIxw#=U zS1)fK!cG#)Atj_>370!0ZIoy$inc`OkT2>`;MG9_MCgbW3tt=M2yyR0Q&%ENV8pL$G*m~hI2O>SdIsZYFJ(}$lb`$$kS#x zo{_J9LZ8lMtIzM$0VEGG5kIIu(K|T&_rpIThGIE_YIh>0@9x#hng_6x)ao{+5XuHV ziC{3hNlU2t{sGq#kHV}-PX~?qCAo@6o-Fo_;s6Fk467~de{Ek*hQl^ zjXW~^wxN#J&h>1qFq%I$^9g~{rk4|$8B zNcB+W&im11?f=rcA}xt-Bc~dYp(H$tZa;NSfDo+u{L=ti6$nNNhTwlV4?&G_z`R_yY1Mg2viu60->W~2=z$Q#X)I)VrbLf9^P^Tm2DJ06o6JP1+ z&^wFpm(j|MKo~;9faVsFr^V3)Mz_W94rL|)7NJJ;9l)NGnn38WD`Ce5&MDgsCcd_5 z=vK4XyDkteg>{uJVOddKU0o7}AI*P;n>Y)Om{hnxw~+*3lg}Utz&PGO*(>zL=}!{t z-~*@w-9`!qO&+1t5g$UIiw+63b$CBWCxGmN)|8ezPli}cPHOxKErtlhCJy=$K^fHL z$qTbIb^EsU18a_wxt!pJW<7G22sN3R4-wlMoL_uTFo`C=qx>2qNJGQ|k*rh8VcaD< z2|x^o7>al{8CVe9NZY2LE`PuvONzb+Nagy@fYu!^tkitil5#5d$s01L*n{FSP{|N$ zE@>YG_duQj)8cPGy+hwVd}B86RwObNF^3BBjf^*{wzRjXVg{H)X@(h*iVIQ@kTNi? z6m0>eTDCFq#SXnOV7ZP@r~oETB7-82WMLA^FkPeQ0E#cM^EM3wj{ZD0kM9rM7MV-v zofMizmOk-?PQ9@J_8Wj;72=ypIUNWU-yIZdGUI)s>;P_|YT?@z{v^~$Fq=dN^G9@! z^&q+?zTBxdM!*XjZDM(;t73eFX-p~v2}^X(*^OJ$HQ-%EnS=Vl37CB#a=QwEH6nhX zq)vRPLvM^k8<6*+0*GP%2zod|B1S?Ic{nmDapFZNfG~%nE(vBdCS7VQJUZy8G@FBo zuXgB-C0~?NXd(kqCvUI_{1Mp_7^HQC)-Rt>Gr0smb;9?TQD8mT)Xc&JYIHNS-z_&!&3~3!iMzPzlh&Y0o#p6I~4sQ z9)HrP*aDD>NVSt^%WWeIp!>rj0TI;{Pr4+%S>eQIE2To^+ScBw7@X$j9bQlsaxS|m z&EX!|JS#L)xnEk}%!k5&Ccu^7Om=LDQ`i`z5)h#rn{3%jQ{JYvfV-Y;G%*`s3w?T$ zY?@Jy@zg`R7J*4bCY@R6Bh0?20ZEG@cL&+$#AiAZy(R))!5#xrksD;s24jsFrie3x zM}i1ZiUQl^4TahOWtq4^x_SY~yqILBI4<7ak+(%J$LF^MV@T2r6OZmu6b+yyyJYA^ z$X0oK?TGdd3q80{WY}nqcvc1iW*`)H@z##KEfU9=SD*-!1VK%gBU#7!j zR6(2qemCH5XuhHh=QM;i0yGIF-ne*EN8Xl=Sb?ln@axQj%0D^#VRCA!X6~F~;x|0fa)ESn8))K$P|u1( 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�Hu;MNZ0x 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 <38396|RE}YpYcx;6 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

e4Kpu-`cm-874vsKL~anOIHUWx`ua5HqZ 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 z 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