Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/actions.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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':
Expand Down Expand Up @@ -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':
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/governance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
4 changes: 3 additions & 1 deletion .github/workflows/hypatia-scan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
5 changes: 5 additions & 0 deletions .machine_readable/descriptiles/STATE.a2ml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
9 changes: 4 additions & 5 deletions crates/oikosbot-eclexia/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
130 changes: 57 additions & 73 deletions crates/oikosbot-eclexia/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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<bool> {
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<PolicyDecision> {
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<String> = 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
Expand Down Expand Up @@ -338,7 +298,7 @@ fn evaluate_builtin(results: &[AnalysisResult], policy_name: &str) -> Result<Pol
outcome,
message,
suggestion,
evaluation_cost: Some(placeholder_cost()),
evaluation_cost: None,
policy_name: format!("{} (builtin)", policy_name),
})
}
Expand Down Expand Up @@ -381,15 +341,6 @@ fn shellexpand(path: &str) -> 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
Expand Down Expand Up @@ -467,6 +418,39 @@ pub fn decisions_to_results(decisions: &[PolicyDecision]) -> Vec<AnalysisResult>
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"));
Expand Down
7 changes: 7 additions & 0 deletions tests/test-eclexia-adapter.sh
Original file line number Diff line number Diff line change
@@ -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
Loading