diff --git a/.github/workflows/actions.lock b/.github/workflows/actions.lock index e141b7c..3e107d3 100644 --- a/.github/workflows/actions.lock +++ b/.github/workflows/actions.lock @@ -7,7 +7,7 @@ workflows: - 'actions/cache@v6.1.0' - 'actions/checkout@v7.0.1' - 'actions/configure-pages@v6.0.0' - - 'actions/deploy-pages@v5.0.0' + - 'actions/deploy-pages@v5.0.1' - 'actions/upload-pages-artifact@v5.0.0' - 'haskell-actions/setup@v2.12.0' '.github/workflows/ci.yml': @@ -84,9 +84,9 @@ dependencies: commit: 'sha1-45bfe0192ca1faeb007ade9deae92b16b8254a0d' owner_id: 44036562 repo_id: 513659658 - 'actions/deploy-pages@v5.0.0': - ref: 'v5.0.0' - commit: 'sha1-cd2ce8fcbc39b97be8ca5fce6e763baed58fa128' + 'actions/deploy-pages@v5.0.1': + ref: 'v5.0.1' + commit: 'sha1-368f82528645a54fb793d4d04e342629a3f51346' owner_id: 44036562 repo_id: 438112499 'actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f': diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c01f791..647d70b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -68,6 +68,8 @@ jobs: run: cargo build --workspace --locked - name: Test (standalone workspace) run: cargo test --workspace --locked + - name: Eclexia adapter feature and rejection checks + run: bash tests/test-eclexia-adapter.sh - name: Clippy (informational) run: cargo clippy --workspace continue-on-error: true diff --git a/.github/workflows/governance.yml b/.github/workflows/governance.yml index f47b7ef..a05f4a5 100644 --- a/.github/workflows/governance.yml +++ b/.github/workflows/governance.yml @@ -15,5 +15,6 @@ permissions: jobs: governance: - uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@4d104d325b96c348d685956c6678139640c15c4b - secrets: inherit + uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@469605210e767ee94d1c7a9c13cb6a1d0a78cad1 + secrets: + HYPATIA_SCAN_PAT: ${{ secrets.HYPATIA_SCAN_PAT }} diff --git a/.github/workflows/hypatia-scan.yml b/.github/workflows/hypatia-scan.yml index 436a2f3..c139558 100644 --- a/.github/workflows/hypatia-scan.yml +++ b/.github/workflows/hypatia-scan.yml @@ -18,4 +18,6 @@ permissions: jobs: scan: - uses: hyperpolymath/standards/.github/workflows/hypatia-scan-reusable.yml@bd0df9ead7faf0cdfe0e13e7966d91e28d0101d4 + uses: hyperpolymath/standards/.github/workflows/hypatia-scan-reusable.yml@469605210e767ee94d1c7a9c13cb6a1d0a78cad1 + with: + block-on-high: true diff --git a/.machine_readable/descriptiles/STATE.a2ml b/.machine_readable/descriptiles/STATE.a2ml index 62f81ae..3901710 100644 --- a/.machine_readable/descriptiles/STATE.a2ml +++ b/.machine_readable/descriptiles/STATE.a2ml @@ -76,3 +76,8 @@ facts = [ last-run-utc = "2026-07-28T18:30:00Z" last-result = "pass" # unknown | pass | warn | fail notes = "main green across CI, CodeQL, Governance, Hypatia, Language Policy, Scorecard, Secret Scanner, Workflow Security Linter, Publish Image and GitHub Pages. Zero open PRs." + +[language-portfolio-audit-20260907] +policy-adapter = "Malformed binary verdicts now error; native feature explicitly unavailable; placeholder evaluation measurements removed" +validation = "Default and all-features adapter regression tests; CI feature check added" +remaining = "Real Eclexia input/result protocol, bound execution, source-policy evaluation and measured evaluation cost remain unimplemented" diff --git a/crates/oikosbot-eclexia/Cargo.toml b/crates/oikosbot-eclexia/Cargo.toml index 24d41ee..2aa55c1 100644 --- a/crates/oikosbot-eclexia/Cargo.toml +++ b/crates/oikosbot-eclexia/Cargo.toml @@ -12,11 +12,10 @@ description = "Eclexia policy engine integration for oikosbot" [features] default = [] -# Native Eclexia compiler integration. The upstream `eclexia` compiler crates -# (a separate hyperpolymath repo) were decoupled when OikosBot was extracted -# from gitbot-fleet so this crate builds standalone; the feature is retained as -# a no-op seam to be re-wired (path or git dep) when native evaluation is added. -# Until then the crate ships its lightweight built-in `.ecl` policy evaluator. +# Reserved native integration feature. Enabling it builds successfully but +# evaluation returns an explicit unavailable error until real dependencies, +# input binding, budgets, and the result protocol are integrated. +# The default builtin fallback does not parse or execute .ecl source. eclexia-native = [] [dependencies] diff --git a/crates/oikosbot-eclexia/src/lib.rs b/crates/oikosbot-eclexia/src/lib.rs index 8df04fc..0c3c32b 100644 --- a/crates/oikosbot-eclexia/src/lib.rs +++ b/crates/oikosbot-eclexia/src/lib.rs @@ -3,11 +3,10 @@ //! # OikosBot-Eclexia Integration //! -//! Policy engine integration with two backends: -//! - **Default**: shells out to `eclexia` binary (works without eclexia repo on disk) -//! - **Native** (`eclexia-native` feature): direct library integration via eclexia-interp -//! -//! Both backends implement the same `PolicyEngine` trait. +//! Experimental policy adapter. The binary protocol still requires upstream +//! integration; the builtin fallback uses hardcoded rules, not `.ecl` source. +//! The reserved `eclexia-native` feature reports an explicit unavailable error. +//! No backend currently provides measured policy-evaluation resource costs. #![forbid(unsafe_code)] use anyhow::{Context, Result}; @@ -58,8 +57,10 @@ pub fn evaluate_policies( let entries = std::fs::read_dir(policy_dir) .with_context(|| format!("Failed to read policy dir: {}", policy_dir.display()))?; - for entry in entries.flatten() { - let path = entry.path(); + for entry in entries { + let path = entry + .context("Failed to enumerate policy directory entry")? + .path(); if path.extension().and_then(|e| e.to_str()) == Some("ecl") { let policy_name = path .file_stem() @@ -145,7 +146,7 @@ fn evaluate_binary( if output.status.success() { let stdout = String::from_utf8_lossy(&output.stdout); - let result: bool = serde_json::from_str(stdout.trim()).unwrap_or(false); + let result = parse_binary_verdict(stdout.trim())?; Ok(PolicyDecision { outcome: if result { @@ -155,7 +156,7 @@ fn evaluate_binary( }, message: format!("Policy '{}' evaluated via eclexia binary", policy_name), suggestion: None, - evaluation_cost: Some(placeholder_cost()), + evaluation_cost: None, policy_name: policy_name.to_string(), }) } else { @@ -170,63 +171,22 @@ fn evaluate_binary( } } -/// Native backend: direct eclexia-interp library integration +/// Decode only the documented boolean protocol. Empty output, diagnostics, +/// structured JSON, and trailing data are evaluation errors, never passes. +#[cfg(any(test, not(feature = "eclexia-native")))] +fn parse_binary_verdict(stdout: &str) -> Result { + serde_json::from_str(stdout).context("Invalid Eclexia policy output: expected a JSON boolean") +} + +/// Reserved feature: no native parser/interpreter dependencies are linked yet. +/// Failing explicitly prevents a successful build from claiming integration. #[cfg(feature = "eclexia-native")] fn evaluate_native( - policy_path: &Path, - results: &[AnalysisResult], - policy_name: &str, + _policy_path: &Path, + _results: &[AnalysisResult], + _policy_name: &str, ) -> Result { - let source = std::fs::read_to_string(policy_path) - .with_context(|| format!("Failed to read policy: {}", policy_path.display()))?; - - let (ast, errors) = eclexia_parser::parse(&source); - - if !errors.is_empty() { - let error_msgs: Vec = errors.iter().map(|e| format!("{:?}", e)).collect(); - anyhow::bail!( - "Policy parse errors in {}: {}", - policy_name, - error_msgs.join("; ") - ); - } - - // Run the policy through the interpreter - let mut interp = eclexia_interp::Interpreter::new(); - // Set a tight budget for policy evaluation itself (dogfooding!) - interp.set_energy_budget(1.0); // 1 Joule max for policy eval - interp.set_carbon_budget(0.001); // 0.001g CO2e max - - match eclexia_interp::run(&ast) { - Ok(value) => { - let should_warn = value.is_truthy(); - - Ok(PolicyDecision { - outcome: if should_warn { - PolicyOutcome::Warn - } else { - PolicyOutcome::Pass - }, - message: format!( - "Policy '{}' evaluated natively: result={:?}", - policy_name, value - ), - suggestion: None, - evaluation_cost: Some(placeholder_cost()), - policy_name: policy_name.to_string(), - }) - } - Err(e) => { - // Runtime error — treat as warning - Ok(PolicyDecision { - outcome: PolicyOutcome::Warn, - message: format!("Policy '{}' runtime error: {:?}", policy_name, e), - suggestion: Some("Check policy syntax and logic".to_string()), - evaluation_cost: None, - policy_name: policy_name.to_string(), - }) - } - } + anyhow::bail!("Eclexia native evaluation is unavailable: input binding, interpreter budgets, and result protocol are not integrated") } /// The builtin backend matches policies by FILE STEM and applies hardcoded @@ -338,7 +298,7 @@ fn evaluate_builtin(results: &[AnalysisResult], policy_name: &str) -> Result String { path.to_string() } -fn placeholder_cost() -> ResourceProfile { - ResourceProfile { - energy: oikosbot_metrics::Energy::joules(0.05), - duration: oikosbot_metrics::Duration::milliseconds(1.0), - carbon: oikosbot_metrics::Carbon::grams_co2e(0.000007), - memory: oikosbot_metrics::Memory::kilobytes(50), - } -} - /// Example policy in Eclexia (to be written to policies/ directory) pub const EXAMPLE_POLICY: &str = r#" // SPDX-License-Identifier: MPL-2.0 @@ -467,6 +418,39 @@ pub fn decisions_to_results(decisions: &[PolicyDecision]) -> Vec mod tests { use super::*; + #[test] + fn binary_protocol_rejects_invalid_output() { + for output in [ + "", + "null", + "0", + "{}", + "[]", + "\"false\"", + "false true", + "diagnostic\nfalse", + ] { + assert!(parse_binary_verdict(output).is_err(), "accepted {output:?}"); + } + assert!(!parse_binary_verdict("false").unwrap()); + assert!(parse_binary_verdict("true\n").unwrap()); + } + + #[cfg(feature = "eclexia-native")] + #[test] + fn native_feature_cannot_claim_policy_execution() { + let error = evaluate_native(Path::new("unused.ecl"), &[], "unused").unwrap_err(); + assert!(error.to_string().contains("unavailable")); + } + + #[test] + fn builtin_does_not_invent_evaluation_measurements() { + assert!(evaluate_builtin(&[], "energy_threshold") + .unwrap() + .evaluation_cost + .is_none()); + } + #[test] fn test_example_policy_syntax() { assert!(EXAMPLE_POLICY.contains("def evaluate_policy")); diff --git a/tests/test-eclexia-adapter.sh b/tests/test-eclexia-adapter.sh new file mode 100644 index 0000000..087b0ae --- /dev/null +++ b/tests/test-eclexia-adapter.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +# Run the real adapter suite, including malformed-output rejection and feature gates. +set -euo pipefail +cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." +cargo test --locked -p oikosbot-eclexia --all-features