From 7094b8c6a1e4f148186a19de21d7467519074fe6 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Mon, 7 Sep 2026 02:30:22 +0100 Subject: [PATCH 01/10] fix(bot): preserve prover protocol and verify the requested forge revision --- .github/workflows/actions.lock | 19 +++++ .github/workflows/proof-safety.yml | 26 ++++++ src/dispatcher/echidna_client.rs | 14 ++- src/main.rs | 131 ++++++++++++++++------------- tests/protocol_contract.rs | 103 +++++++++++++++++++++++ 5 files changed, 230 insertions(+), 63 deletions(-) create mode 100644 .github/workflows/actions.lock create mode 100644 .github/workflows/proof-safety.yml create mode 100644 tests/protocol_contract.rs diff --git a/.github/workflows/actions.lock b/.github/workflows/actions.lock new file mode 100644 index 0000000..fa47800 --- /dev/null +++ b/.github/workflows/actions.lock @@ -0,0 +1,19 @@ +# This file is machine-generated by `gh actions-lock`. +# Do not edit by hand; run `gh actions-lock` to update. +# Docs: https://gh.io/actions-lockfile +version: 'v0.0.2' +workflows: + '.github/workflows/proof-safety.yml': + - 'actions/checkout@v7.0.1' + - 'swatinem/rust-cache@v2.9.2' +dependencies: + 'actions/checkout@v7.0.1': + ref: 'v7.0.1' + commit: 'sha1-3d3c42e5aac5ba805825da76410c181273ba90b1' + owner_id: 44036562 + repo_id: 197814629 + 'swatinem/rust-cache@v2.9.2': + ref: 'v2.9.2' + commit: 'sha1-6323deb102c322ba6fcbdcafc7e3dddab59af2b6' + owner_id: 580492 + repo_id: 298565987 diff --git a/.github/workflows/proof-safety.yml b/.github/workflows/proof-safety.yml new file mode 100644 index 0000000..cd9c272 --- /dev/null +++ b/.github/workflows/proof-safety.yml @@ -0,0 +1,26 @@ +# This workflow is managed by gh actions-lock. +# SPDX-License-Identifier: MPL-2.0 +name: Proof Safety +on: + pull_request: + push: + branches: [main] +permissions: + contents: read +concurrency: + group: proof-safety-${{ github.ref }} + cancel-in-progress: true +jobs: + protocol: + name: Proof protocol contracts + runs-on: ubuntu-24.04 + timeout-minutes: 25 + steps: + - uses: actions/checkout@v7.0.1 + with: + persist-credentials: false + - uses: Swatinem/rust-cache@v2.9.2 + - name: Test REST and GraphQL wire contracts + run: | + cargo test --locked --test protocol_contract + cargo test --locked --bin echidnabot clone_contract_tests diff --git a/src/dispatcher/echidna_client.rs b/src/dispatcher/echidna_client.rs index aa0b61f..414c395 100644 --- a/src/dispatcher/echidna_client.rs +++ b/src/dispatcher/echidna_client.rs @@ -160,7 +160,7 @@ impl EchidnaClient { "# .to_string(), variables: serde_json::json!({ - "prover": format!("{:?}", prover).to_lowercase(), + "prover": prover.as_str(), "content": content }), }; @@ -238,7 +238,7 @@ impl EchidnaClient { "# .to_string(), variables: serde_json::json!({ - "prover": format!("{:?}", prover).to_lowercase(), + "prover": prover.as_str(), "context": context, "goalState": goal_state }), @@ -320,7 +320,7 @@ impl EchidnaClient { "# .to_string(), variables: serde_json::json!({ - "prover": format!("{:?}", prover).to_lowercase() + "prover": prover.as_str() }), }; @@ -528,7 +528,13 @@ struct RestProverInfo { } fn prover_to_echidna_name(prover: &ProverKind) -> String { - prover.display_name().to_string() + // These are ECHIDNA's serde enum names, not presentation labels. + match prover.as_str() { + "lean" => "Lean", + "isabelle" => "Isabelle", + "hol-light" => "HOLLight", + _ => prover.display_name(), + }.to_string() } // ============================================================================= diff --git a/src/main.rs b/src/main.rs index 21e2c3b..a263d64 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1382,79 +1382,92 @@ async fn clone_repo(config: &Config, repo: &RepoId, commit: &str) -> Result clone_repo_via_git("https://codeberg.org", repo, commit).await, + Platform::Codeberg => { + let base_url = config.codeberg.as_ref() + .map(|c| c.url.as_str()).unwrap_or("https://codeberg.org"); + clone_repo_via_git(base_url, repo, commit).await + } } } async fn clone_repo_via_git(base_url: &str, repo: &RepoId, commit: &str) -> Result { let temp_dir = tempfile::tempdir()?; - let clone_path = temp_dir.keep(); let url = format!( "{}/{}/{}.git", base_url.trim_end_matches('/'), repo.owner, repo.name ); + let clone = tokio::process::Command::new("git") + .args(["clone", "--no-checkout", "--depth", "1", "--", &url]) + .arg(temp_dir.path()) + .status() + .await?; + if !clone.success() { + return Err(echidnabot::Error::Internal(format!("Failed to clone {}", repo.full_name()))); + } + let fetch = tokio::process::Command::new("git") + .current_dir(temp_dir.path()) + .args(["fetch", "--depth", "1", "--", "origin", commit]) + .status() + .await?; + if !fetch.success() { + return Err(echidnabot::Error::Internal(format!("Failed to fetch requested revision for {}", repo.full_name()))); + } + let checkout = tokio::process::Command::new("git") + .current_dir(temp_dir.path()) + .args(["checkout", "--detach", "FETCH_HEAD"]) + .status() + .await?; + if !checkout.success() { + return Err(echidnabot::Error::Internal(format!("Failed to check out requested revision for {}", repo.full_name()))); + } + Ok(temp_dir.keep()) +} - let status = if commit == "HEAD" { - tokio::process::Command::new("git") - .args([ - "clone", - "--depth", - "1", - &url, - &*clone_path.to_string_lossy(), - ]) - .status() - .await? - } else { - tokio::process::Command::new("git") - .args([ - "clone", - "--depth", - "1", - "--branch", - commit, - &url, - &*clone_path.to_string_lossy(), - ]) - .status() - .await? - }; - - if !status.success() && commit != "HEAD" { - let status = tokio::process::Command::new("git") - .args([ - "clone", - "--depth", - "1", - &url, - &*clone_path.to_string_lossy(), - ]) - .status() - .await?; - - if !status.success() { - return Err(echidnabot::Error::Internal(format!( - "Failed to clone {}", - repo.full_name() - ))); - } - - tokio::process::Command::new("git") - .current_dir(&clone_path) - .args(["fetch", "--depth", "1", "origin", commit]) - .status() - .await?; - - tokio::process::Command::new("git") - .current_dir(&clone_path) - .args(["checkout", commit]) - .status() - .await?; +#[cfg(test)] +mod clone_contract_tests { + use super::*; + use echidnabot::config::CodebergConfig; + + fn git(repo: &Path, args: &[&str]) -> String { + let output = std::process::Command::new("git") + .current_dir(repo) + .args(args) + .output().unwrap(); + assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr)); + String::from_utf8(output.stdout).unwrap().trim().to_string() } - Ok(clone_path) + #[tokio::test] + async fn configured_forge_clones_exact_revision_and_rejects_missing_revision() { + let forge = tempfile::tempdir().unwrap(); + let repo = forge.path().join("owner/proofs.git"); + std::fs::create_dir_all(&repo).unwrap(); + git(&repo, &["init", "--initial-branch=main"]); + let commit_args = ["-c", "user.name=Contract Test", "-c", "user.email=contract@example.invalid", + "-c", "commit.gpgsign=false", "commit", "--allow-empty", "-m", "fixture"]; + git(&repo, &commit_args); + let first = git(&repo, &["rev-parse", "HEAD"]); + git(&repo, &commit_args); + assert_ne!(first, git(&repo, &["rev-parse", "HEAD"])); + let config = Config { + codeberg: Some(CodebergConfig { + url: format!("file://{}", forge.path().display()), + token: None, + webhook_secret: None, + }), + ..Default::default() + }; + let id = RepoId::new(Platform::Codeberg, "owner", "proofs"); + let cloned = clone_repo(&config, &id, &first).await.unwrap(); + let actual = git(&cloned, &["rev-parse", "HEAD"]); + std::fs::remove_dir_all(cloned).unwrap(); + assert_eq!(actual, first); + assert!(clone_repo(&config, &id, "missing-contract-revision").await.is_err()); + let missing = RepoId::new(Platform::Codeberg, "owner", "missing"); + assert!(clone_repo(&config, &missing, "HEAD").await.is_err()); + } } const MAX_PROOF_FILES: usize = 10_000; diff --git a/tests/protocol_contract.rs b/tests/protocol_contract.rs new file mode 100644 index 0000000..3d0787b --- /dev/null +++ b/tests/protocol_contract.rs @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: MPL-2.0 +//! HTTP contract tests: the server here validates wire messages, not proofs. +use axum::{ + routing::{get, post}, + Json, Router, +}; +use echidnabot::config::{EchidnaApiMode, EchidnaConfig}; +use echidnabot::dispatcher::{EchidnaClient, ProofStatus, ProverKind}; +use echidnabot::dispatcher::echidna_client::ProverStatus; +use serde_json::{json, Value}; + +async fn endpoint(router: Router) -> (String, tokio::task::JoinHandle<()>) { + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let url = format!("http://{}", listener.local_addr().unwrap()); + let task = tokio::spawn(async move { axum::serve(listener, router).await.unwrap() }); + (url, task) +} + +#[tokio::test] +async fn rest_uses_core_identifiers_and_preserves_rejection() { + let router = Router::new() + .route("/api/provers", get(|| async { Json(json!({"provers":[ + {"name":"Lean","tier":1,"complexity":3}, + {"name":"Isabelle","tier":1,"complexity":4}, + {"name":"HOLLight","tier":2,"complexity":3} + ]})) })) + .route("/api/verify", post(|Json(body): Json| async move { + assert!(["Lean", "Isabelle", "HOLLight"].contains(&body["prover"].as_str().unwrap())); + Json(json!({"valid":body["content"] == "accepted-fixture", "goals_remaining":0, "tactics_used":0})) + })); + let (url, server) = endpoint(router).await; + let client = EchidnaClient::new(&EchidnaConfig { + rest_endpoint: url, + mode: EchidnaApiMode::Rest, + ..Default::default() + }); + for slug in ["lean", "isabelle", "hol-light"] { + let prover = ProverKind::new(slug); + assert_eq!( + client.prover_status(&prover).await.unwrap(), + ProverStatus::Available + ); + assert_eq!( + client + .verify_proof(&prover, "accepted-fixture") + .await + .unwrap() + .status, + ProofStatus::Verified + ); + assert_eq!( + client + .verify_proof(&prover, "rejected-fixture") + .await + .unwrap() + .status, + ProofStatus::Failed + ); + } + server.abort(); +} + +#[tokio::test] +async fn graphql_uses_slug_for_verification_suggestions_and_status() { + let router = Router::new().route("/graphql", post(|Json(body): Json| async move { + assert_eq!(body["variables"]["prover"], "isabelle"); + let query = body["query"].as_str().unwrap(); + let response = if query.contains("VerifyProof") { + json!({"verifyProof":{"status":"FAILED","message":"contract fixture", "proverOutput":"", "durationMs":1, "artifacts":[]}}) + } else if query.contains("SuggestTactics") { + json!({"suggestTactics":[]}) + } else { + assert!(query.contains("ProverStatus")); + json!({"proverStatus":{"available":true,"message":null}}) + }; + Json(json!({"data":response})) + })); + let (url, server) = endpoint(router).await; + let client = EchidnaClient::new(&EchidnaConfig { + endpoint: format!("{url}/graphql"), + mode: EchidnaApiMode::Graphql, + ..Default::default() + }); + let prover = ProverKind::new("isabelle"); + assert_eq!( + client + .verify_proof(&prover, "fixture") + .await + .unwrap() + .status, + ProofStatus::Failed + ); + assert!(client + .suggest_tactics(&prover, "", "fixture") + .await + .unwrap() + .is_empty()); + assert_eq!( + client.prover_status(&prover).await.unwrap(), + ProverStatus::Available + ); + server.abort(); +} From bcdce5c1d50b7f498407b50acf1d0f4b8832debb Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Mon, 7 Sep 2026 02:48:24 +0100 Subject: [PATCH 02/10] fix(ci): enforce dependency audits and classify tested proof-hole fixtures --- .claude/CLAUDE.md | 14 +- .github/workflows/actions.lock | 171 +++++++ .github/workflows/boj-build.yml | 3 +- .github/workflows/cargo-audit.yml | 5 +- .github/workflows/casket-pages.yml | 15 +- .github/workflows/cflite_batch.yml | 5 +- .github/workflows/cflite_pr.yml | 5 +- .github/workflows/codeql.yml | 1 + .github/workflows/container.yml | 13 +- .github/workflows/db-checks.yml | 7 +- .github/workflows/dependabot-automerge.yml | 3 +- .github/workflows/docs.yml | 13 +- .github/workflows/dogfood-gate.yml | 13 +- .github/workflows/e2e.yml | 5 +- .github/workflows/echidna-fuzz.yml | 17 +- .github/workflows/echidnabot.yml | 11 +- .github/workflows/governance.yml | 1 + .github/workflows/hypatia-scan.yml | 1 + .github/workflows/label-triage.yml | 1 + .github/workflows/labels.yml | 1 + .github/workflows/mirror.yml | 1 + .github/workflows/openssf-compliance.yml | 3 +- .github/workflows/proof-safety.yml | 2 +- .github/workflows/publish.yml | 11 +- .github/workflows/release.yml | 23 +- .github/workflows/rhodibot.yml | 3 +- .github/workflows/rust-ci.yml | 2 + .github/workflows/scorecard.yml | 1 + .github/workflows/secret-scanner.yml | 1 + .github/workflows/static-analysis-gate.yml | 23 +- .github/workflows/stress-test.yml | 5 +- .github/workflows/workflow-linter.yml | 3 +- .machine_readable/STATE.a2ml | 439 ------------------ .machine_readable/contractiles/Mustfile.a2ml | 4 +- .../{6a2 => descriptiles}/0-AI-MANIFEST.a2ml | 0 .../{6a2 => descriptiles}/AGENTIC.a2ml | 0 .../{6a2 => descriptiles}/ECOSYSTEM.a2ml | 0 .../{6a2 => descriptiles}/META.a2ml | 0 .../{6a2 => descriptiles}/NEUROSYM.a2ml | 0 .../{6a2 => descriptiles}/PLAYBOOK.a2ml | 0 .../{6a2 => descriptiles}/README.adoc | 0 .../{6a2 => descriptiles}/STATE.a2ml | 6 +- .../anchor/0-AI-MANIFEST.a2ml | 0 .../{6a2 => descriptiles}/anchor/ANCHOR.a2ml | 0 .../{6a2 => descriptiles}/anchor/README.adoc | 0 0-AI-MANIFEST.a2ml | 14 +- CONTRIBUTING.adoc | 4 +- Cargo.lock | 20 +- EXPLAINME.adoc | 2 +- MAINTAINERS.adoc | 2 +- ROADMAP.adoc | 2 +- RSR_COMPLIANCE.adoc | 4 +- RSR_OUTLINE.adoc | 4 +- docs/content/api.adoc | 4 +- echidnabot.example.toml | 6 +- echidnabot.toml | 6 +- proofs/coq/admitted_stub.v | 1 + proofs/lean/sorry_stub.lean | 1 + src/config.rs | 2 +- src/dispatcher/echidna_client.rs | 3 +- src/main.rs | 49 +- tests/protocol_contract.rs | 38 +- 62 files changed, 414 insertions(+), 580 deletions(-) delete mode 100644 .machine_readable/STATE.a2ml rename .machine_readable/{6a2 => descriptiles}/0-AI-MANIFEST.a2ml (100%) rename .machine_readable/{6a2 => descriptiles}/AGENTIC.a2ml (100%) rename .machine_readable/{6a2 => descriptiles}/ECOSYSTEM.a2ml (100%) rename .machine_readable/{6a2 => descriptiles}/META.a2ml (100%) rename .machine_readable/{6a2 => descriptiles}/NEUROSYM.a2ml (100%) rename .machine_readable/{6a2 => descriptiles}/PLAYBOOK.a2ml (100%) rename .machine_readable/{6a2 => descriptiles}/README.adoc (100%) rename .machine_readable/{6a2 => descriptiles}/STATE.a2ml (98%) rename .machine_readable/{6a2 => descriptiles}/anchor/0-AI-MANIFEST.a2ml (100%) rename .machine_readable/{6a2 => descriptiles}/anchor/ANCHOR.a2ml (100%) rename .machine_readable/{6a2 => descriptiles}/anchor/README.adoc (100%) diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 32adc18..ffd4d3b 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -4,7 +4,7 @@ Copyright (c) Jonathan D.A. Jewell --> ## Machine-Readable Artefacts -The following files in `.machine_readable/6a2/` contain structured project metadata +The following files in `.machine_readable/descriptiles/` contain structured project metadata (TOML-flavoured A2ML, migrated from Guile Scheme in `bdbc5a4`): - `STATE.a2ml` - Current project state and progress @@ -35,7 +35,7 @@ The following files in `.machine_readable/6a2/` contain structured project metad | **JavaScript** | Only where AffineScript cannot | MCP protocol glue, Bun APIs | | **Nickel** | Configuration language | For complex configs | | **Guile Scheme** | Guix packages | guix.scm, manifests/*.scm | -| **A2ML (TOML)** | State/meta files | .machine_readable/6a2/*.a2ml | +| **A2ML (TOML)** | State/meta files | .machine_readable/descriptiles/*.a2ml | | **Julia** | Batch scripts, data processing | Per RSR | | **OCaml** | AffineScript compiler | Language-specific | | **Ada** | Safety-critical systems | Where required | @@ -108,8 +108,8 @@ Both are FOSS with independent governance (no Big Tech). ### First-read order (when entering this repo) -1. `.machine_readable/6a2/STATE.a2ml` — blockers + completion-percentage + session notes -2. `.machine_readable/6a2/AGENTIC.a2ml` — tooling constraints; check `[exceptions.*]` +1. `.machine_readable/descriptiles/STATE.a2ml` — blockers + completion-percentage + session notes +2. `.machine_readable/descriptiles/AGENTIC.a2ml` — tooling constraints; check `[exceptions.*]` 3. `.machine_readable/bot_directives/echidnabot.a2ml` — self-mode + future-direction notes 4. `EXPLAINME.adoc` — caveats on README claims (test counts, mode wiring, prover surface drift) 5. `ROADMAP.adoc` — what's actually done vs aspirational @@ -141,7 +141,7 @@ and the eventual fix path (promote to real submodule or delete `list_outcome_history`, `corpus_refresh`) is a documented carve-out from the estate-wide "all MCP through BoJ" rule. Scope: this repo only. Sunset: when the BoJ cartridge supports the double-loop -feedback protocol. See `.machine_readable/6a2/AGENTIC.a2ml` +feedback protocol. See `.machine_readable/descriptiles/AGENTIC.a2ml` `[exceptions.boj-only-mcp]` for the canonical record. ### Upstream-drift warning (12 vs 113 provers) @@ -175,8 +175,8 @@ provers can land without touching the type. Use | New prover support | `wiki/Supported-Provers.md` + `src/dispatcher/` | | New bot-mode behaviour | `wiki/FAQ.md` + `.machine_readable/bot_directives/echidnabot.a2ml` | | ABI/FFI surface change | `src/abi/*.idr` + `ffi/zig/src/main.zig` + `ABI-FFI-README.md` | -| Roadmap item closed | `ROADMAP.adoc` + `.machine_readable/6a2/STATE.a2ml` | -| New external target / consumer | `.machine_readable/6a2/STATE.a2ml` `[external-targets]` | +| Roadmap item closed | `ROADMAP.adoc` + `.machine_readable/descriptiles/STATE.a2ml` | +| New external target / consumer | `.machine_readable/descriptiles/STATE.a2ml` `[external-targets]` | ### Tests — full suite is 184 diff --git a/.github/workflows/actions.lock b/.github/workflows/actions.lock index fa47800..9d4af02 100644 --- a/.github/workflows/actions.lock +++ b/.github/workflows/actions.lock @@ -3,15 +3,186 @@ # Docs: https://gh.io/actions-lockfile version: 'v0.0.2' workflows: + '.github/workflows/boj-build.yml': + - 'actions/checkout@v7.0.1' + '.github/workflows/cargo-audit.yml': + - 'actions/checkout@v7.0.1' + - 'dtolnay/rust-toolchain@master' + '.github/workflows/casket-pages.yml': + - 'actions/cache@v6.1.0' + - 'actions/checkout@v7.0.1' + - 'actions/configure-pages@v6.0.0' + - 'actions/deploy-pages@v5.0.1' + - 'actions/upload-pages-artifact@v5.0.0' + - 'haskell-actions/setup@v2.12.0' + '.github/workflows/cflite_batch.yml': + - 'google/clusterfuzzlite@v1' + '.github/workflows/cflite_pr.yml': + - 'google/clusterfuzzlite@v1' + '.github/workflows/container.yml': + - 'actions/attest-build-provenance@v4.2.2' + - 'actions/checkout@v7.0.1' + - 'docker/build-push-action@v7.3.0' + - 'docker/login-action@v4.6.0' + - 'docker/metadata-action@v6.2.0' + - 'docker/setup-buildx-action@v4.3.0' + '.github/workflows/db-checks.yml': + - 'actions/checkout@v7.0.1' + - 'dtolnay/rust-toolchain@master' + '.github/workflows/dependabot-automerge.yml': + - 'dependabot/fetch-metadata@v3.1.0' + '.github/workflows/docs.yml': + - 'actions/cache@v6.1.0' + - 'actions/checkout@v7.0.1' + - 'actions/deploy-pages@v5.0.1' + - 'actions/upload-pages-artifact@v5.0.0' + - 'haskell-actions/setup@v2.12.0' + '.github/workflows/dogfood-gate.yml': + - 'actions/checkout@v7.0.1' + '.github/workflows/e2e.yml': + - 'actions/checkout@v7.0.1' + - 'swatinem/rust-cache@v2.9.2' + '.github/workflows/echidna-fuzz.yml': + - 'actions/cache@v6.1.0' + - 'actions/checkout@v7.0.1' + - 'actions/upload-artifact@v7.0.1' + '.github/workflows/echidnabot.yml': + - 'actions/checkout@v7.0.1' + '.github/workflows/openssf-compliance.yml': + - 'actions/checkout@v7.0.1' '.github/workflows/proof-safety.yml': - 'actions/checkout@v7.0.1' - 'swatinem/rust-cache@v2.9.2' + '.github/workflows/publish.yml': + - 'actions/attest-build-provenance@v4.2.2' + - 'actions/checkout@v7.0.1' + - 'dtolnay/rust-toolchain@master' + - 'softprops/action-gh-release@v3.0.3' + - 'swatinem/rust-cache@v2.9.2' + '.github/workflows/release.yml': + - 'actions/attest-build-provenance@v4.2.2' + - 'actions/checkout@v7.0.1' + - 'actions/download-artifact@v8.0.1' + - 'actions/upload-artifact@v7.0.1' + - 'dtolnay/rust-toolchain@master' + - 'softprops/action-gh-release@v3.0.3' + '.github/workflows/rhodibot.yml': + - 'actions/checkout@v7.0.1' + '.github/workflows/static-analysis-gate.yml': + - 'actions/checkout@v7.0.1' + - 'actions/download-artifact@v8.0.1' + - 'actions/upload-artifact@v7.0.1' + - 'erlef/setup-beam@v1.24.1' + '.github/workflows/stress-test.yml': + - 'actions/checkout@v7.0.1' + - 'dtolnay/rust-toolchain@master' + '.github/workflows/workflow-linter.yml': + - 'actions/checkout@v7.0.1' dependencies: + 'actions/attest-build-provenance@v4.2.2': + ref: 'v4.2.2' + commit: 'sha1-4d101475d8b20a2381f78447822ac1eab6504dd8' + owner_id: 44036562 + repo_id: 760702757 + uses: + - 'actions/attest@508db95dd578ae2727ebd6217d5ba78e4fbda05d' + 'actions/attest@508db95dd578ae2727ebd6217d5ba78e4fbda05d': + ref: 'v4.2.1' + commit: 'sha1-508db95dd578ae2727ebd6217d5ba78e4fbda05d' + owner_id: 44036562 + repo_id: 760701061 + 'actions/cache@v6.1.0': + ref: 'v6.1.0' + commit: 'sha1-55cc8345863c7cc4c66a329aec7e433d2d1c52a9' + owner_id: 44036562 + repo_id: 215566462 'actions/checkout@v7.0.1': ref: 'v7.0.1' commit: 'sha1-3d3c42e5aac5ba805825da76410c181273ba90b1' owner_id: 44036562 repo_id: 197814629 + 'actions/configure-pages@v6.0.0': + ref: 'v6.0.0' + commit: 'sha1-45bfe0192ca1faeb007ade9deae92b16b8254a0d' + owner_id: 44036562 + repo_id: 513659658 + 'actions/deploy-pages@v5.0.1': + ref: 'v5.0.1' + commit: 'sha1-368f82528645a54fb793d4d04e342629a3f51346' + owner_id: 44036562 + repo_id: 438112499 + 'actions/download-artifact@v8.0.1': + ref: 'v8.0.1' + commit: 'sha1-3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c' + owner_id: 44036562 + repo_id: 192626254 + 'actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f': + ref: 'v7.0.0' + commit: 'sha1-bbbca2ddaa5d8feaa63e36b76fdaad77386f024f' + owner_id: 44036562 + repo_id: 192625955 + 'actions/upload-artifact@v7.0.1': + ref: 'v7.0.1' + commit: 'sha1-043fb46d1a93c77aae656e7c1c64a875d1fc6a0a' + owner_id: 44036562 + repo_id: 192625955 + 'actions/upload-pages-artifact@v5.0.0': + ref: 'v5.0.0' + commit: 'sha1-fc324d3547104276b827a68afc52ff2a11cc49c9' + owner_id: 44036562 + repo_id: 496012378 + uses: + - 'actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f' + 'dependabot/fetch-metadata@v3.1.0': + ref: 'v3.1.0' + commit: 'sha1-25dd0e34f4fe68f24cc83900b1fe3fe149efef98' + owner_id: 27347476 + repo_id: 371068214 + 'docker/build-push-action@v7.3.0': + ref: 'v7.3.0' + commit: 'sha1-53b7df96c91f9c12dcc8a07bcb9ccacbed38856a' + owner_id: 5429470 + repo_id: 241092383 + 'docker/login-action@v4.6.0': + ref: 'v4.6.0' + commit: 'sha1-dbcb813823bdd20940b903addbd779551569679f' + owner_id: 5429470 + repo_id: 287743349 + 'docker/metadata-action@v6.2.0': + ref: 'v6.2.0' + commit: 'sha1-dc802804100637a589fabce1cb79ff13a1411302' + owner_id: 5429470 + repo_id: 306769011 + 'docker/setup-buildx-action@v4.3.0': + ref: 'v4.3.0' + commit: 'sha1-37fe631027851001ddb9b187196cc803df7f5f0e' + owner_id: 5429470 + repo_id: 288485773 + 'dtolnay/rust-toolchain@master': + ref: 'master' + commit: 'sha1-b3b07ba8b418998c39fb20f53e8b695cdcc8de1b' + owner_id: 1940490 + repo_id: 260749683 + 'erlef/setup-beam@v1.24.1': + ref: 'v1.24.1' + commit: 'sha1-54075bcc5e249e4758d363f27d099f55d843f124' + owner_id: 47606891 + repo_id: 331103973 + 'google/clusterfuzzlite@v1': + ref: 'v1' + commit: 'sha1-884713a6c30a92e5e8544c39945cd7cb630abcd1' + owner_id: 1342004 + repo_id: 400046858 + 'haskell-actions/setup@v2.12.0': + ref: 'v2.12.0' + commit: 'sha1-6037f33647c3f17758a2356c80fc4a53d7e0685d' + owner_id: 75048950 + repo_id: 623796603 + 'softprops/action-gh-release@v3.0.3': + ref: 'v3.0.3' + commit: 'sha1-efb35369e0ad2afab669f228072c1b0d510eae64' + owner_id: 2242 + repo_id: 204253808 'swatinem/rust-cache@v2.9.2': ref: 'v2.9.2' commit: 'sha1-6323deb102c322ba6fcbdcafc7e3dddab59af2b6' diff --git a/.github/workflows/boj-build.yml b/.github/workflows/boj-build.yml index 5529dba..fd82d6a 100644 --- a/.github/workflows/boj-build.yml +++ b/.github/workflows/boj-build.yml @@ -1,4 +1,5 @@ # SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. # # OPTIONAL: BoJ Server Build Trigger # This workflow notifies a BoJ Server instance when code is pushed. @@ -23,7 +24,7 @@ jobs: if: ${{ vars.BOJ_SERVER_URL != '' }} steps: - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + uses: actions/checkout@v7.0.1 - name: Trigger BoJ Server (Casket/ssg-mcp) env: BOJ_URL: ${{ secrets.BOJ_SERVER_URL || vars.BOJ_SERVER_URL }} diff --git a/.github/workflows/cargo-audit.yml b/.github/workflows/cargo-audit.yml index 8c2886c..32f1991 100644 --- a/.github/workflows/cargo-audit.yml +++ b/.github/workflows/cargo-audit.yml @@ -1,4 +1,5 @@ # SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. # Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) # # cargo-audit.yml — Dependency vulnerability scanning for Rust projects. @@ -21,12 +22,12 @@ jobs: timeout-minutes: 15 steps: - name: Checkout repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4 + uses: actions/checkout@v7.0.1 - name: Install Rust toolchain # `toolchain:` is mandatory when this action is pinned to a SHA — see # the note in stress-test.yml. This SHA currently tolerates its absence; # stating it explicitly keeps the step working across future re-pins. - uses: dtolnay/rust-toolchain@4be9e76fd7c4901c61fb841f559994984270fce7 # stable + uses: dtolnay/rust-toolchain@master with: toolchain: stable - name: Install cargo-audit diff --git a/.github/workflows/casket-pages.yml b/.github/workflows/casket-pages.yml index 1044b66..f050793 100644 --- a/.github/workflows/casket-pages.yml +++ b/.github/workflows/casket-pages.yml @@ -1,4 +1,5 @@ # SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. name: GitHub Pages on: push: @@ -17,19 +18,19 @@ jobs: timeout-minutes: 15 steps: - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4 + uses: actions/checkout@v7.0.1 - name: Checkout casket-ssg - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4 + uses: actions/checkout@v7.0.1 with: repository: hyperpolymath/casket-ssg path: .casket-ssg - name: Setup GHCup - uses: haskell-actions/setup@6037f33647c3f17758a2356c80fc4a53d7e0685d # v2 + uses: haskell-actions/setup@v2.12.0 with: ghc-version: '9.8.2' cabal-version: '3.10' - name: Cache Cabal - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v4 + uses: actions/cache@v6.1.0 with: path: | ~/.cabal/packages @@ -70,9 +71,9 @@ jobs: fi cd .casket-ssg && cabal run casket-ssg -- build ../site ../_site - name: Setup Pages - uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0 + uses: actions/configure-pages@v6.0.0 - name: Upload artifact - uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v3 + uses: actions/upload-pages-artifact@v5.0.0 with: path: '_site' deploy: @@ -85,4 +86,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1 + uses: actions/deploy-pages@v5.0.1 diff --git a/.github/workflows/cflite_batch.yml b/.github/workflows/cflite_batch.yml index 5e2a470..91c4109 100644 --- a/.github/workflows/cflite_batch.yml +++ b/.github/workflows/cflite_batch.yml @@ -1,4 +1,5 @@ # SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. name: ClusterFuzzLite batch fuzzing on: schedule: @@ -16,13 +17,13 @@ jobs: steps: - name: Build Fuzzers (${{ matrix.sanitizer }}) id: build - uses: google/clusterfuzzlite/actions/build_fuzzers@884713a6c30a92e5e8544c39945cd7cb630abcd1 # v1 + uses: google/clusterfuzzlite/actions/build_fuzzers@v1 with: language: rust sanitizer: ${{ matrix.sanitizer }} - name: Run Fuzzers (${{ matrix.sanitizer }}) id: run - uses: google/clusterfuzzlite/actions/run_fuzzers@884713a6c30a92e5e8544c39945cd7cb630abcd1 # v1 + uses: google/clusterfuzzlite/actions/run_fuzzers@v1 with: github-token: ${{ secrets.GITHUB_TOKEN }} fuzz-seconds: 1800 diff --git a/.github/workflows/cflite_pr.yml b/.github/workflows/cflite_pr.yml index 34d3df5..61beb96 100644 --- a/.github/workflows/cflite_pr.yml +++ b/.github/workflows/cflite_pr.yml @@ -1,4 +1,5 @@ # SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. name: ClusterFuzzLite PR fuzzing on: pull_request: @@ -23,13 +24,13 @@ jobs: steps: - name: Build Fuzzers (${{ matrix.sanitizer }}) id: build - uses: google/clusterfuzzlite/actions/build_fuzzers@884713a6c30a92e5e8544c39945cd7cb630abcd1 # v1 + uses: google/clusterfuzzlite/actions/build_fuzzers@v1 with: language: rust sanitizer: ${{ matrix.sanitizer }} - name: Run Fuzzers (${{ matrix.sanitizer }}) id: run - uses: google/clusterfuzzlite/actions/run_fuzzers@884713a6c30a92e5e8544c39945cd7cb630abcd1 # v1 + uses: google/clusterfuzzlite/actions/run_fuzzers@v1 with: github-token: ${{ secrets.GITHUB_TOKEN }} fuzz-seconds: 300 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 25705db..489854f 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,4 +1,5 @@ # SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. # Thin wrapper around hyperpolymath/standards codeql-reusable.yml. # Matches the echidna sibling-repo pattern (estate-wide consolidation: # 263 deployments, 69 unique SHAs, 26% structural drift). Default diff --git a/.github/workflows/container.yml b/.github/workflows/container.yml index eeb57df..1e03327 100644 --- a/.github/workflows/container.yml +++ b/.github/workflows/container.yml @@ -1,4 +1,5 @@ # SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. name: Container Build on: push: @@ -20,18 +21,18 @@ jobs: attestations: write steps: - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4 + uses: actions/checkout@v7.0.1 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 + uses: docker/setup-buildx-action@v4.3.0 - name: Log in to GitHub Container Registry - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v3 + uses: docker/login-action@v4.6.0 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata id: meta - uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 + uses: docker/metadata-action@v6.2.0 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | @@ -40,7 +41,7 @@ jobs: type=sha - name: Build and push id: push - uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v5 + uses: docker/build-push-action@v7.3.0 with: context: . file: ./Containerfile @@ -50,7 +51,7 @@ jobs: cache-from: type=gha cache-to: type=gha,mode=max - name: Attest container provenance - uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 + uses: actions/attest-build-provenance@v4.2.2 with: subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} subject-digest: ${{ steps.push.outputs.digest }} diff --git a/.github/workflows/db-checks.yml b/.github/workflows/db-checks.yml index a4f70a2..417707c 100644 --- a/.github/workflows/db-checks.yml +++ b/.github/workflows/db-checks.yml @@ -1,4 +1,5 @@ # SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. # Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) # # db-checks.yml — Migration + schema integrity gate for echidnabot. @@ -36,18 +37,18 @@ jobs: steps: - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + uses: actions/checkout@v7.0.1 - name: Install Rust toolchain # `toolchain:` is mandatory when this action is pinned to a SHA — see # the note in stress-test.yml. This SHA currently tolerates its absence; # stating it explicitly keeps the step working across future re-pins. - uses: dtolnay/rust-toolchain@4be9e76fd7c4901c61fb841f559994984270fce7 # stable + uses: dtolnay/rust-toolchain@master with: toolchain: stable - name: Cache cargo - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 + uses: Swatinem/rust-cache@v2.9.2 - name: Install sqlite3 CLI run: sudo apt-get update && sudo apt-get install -y sqlite3 diff --git a/.github/workflows/dependabot-automerge.yml b/.github/workflows/dependabot-automerge.yml index cff4451..5564dae 100644 --- a/.github/workflows/dependabot-automerge.yml +++ b/.github/workflows/dependabot-automerge.yml @@ -1,4 +1,5 @@ # SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. # # dependabot-automerge.yml — enable GitHub's native auto-merge on # Dependabot pull requests that match a declared severity / ecosystem @@ -51,7 +52,7 @@ jobs: steps: - name: Fetch Dependabot metadata id: meta - uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.0 + uses: dependabot/fetch-metadata@v3.1.0 with: github-token: ${{ secrets.GITHUB_TOKEN }} # --- Policy gate ------------------------------------------------------- diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index b7ccd90..e877ee7 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,4 +1,5 @@ # SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. name: Documentation (casket-ssg) permissions: contents: read @@ -24,19 +25,19 @@ jobs: timeout-minutes: 15 steps: - name: Checkout repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + uses: actions/checkout@v7.0.1 - name: Checkout casket-ssg - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + uses: actions/checkout@v7.0.1 with: repository: hyperpolymath/casket-ssg path: casket-ssg - name: Set up GHC - uses: haskell-actions/setup@6037f33647c3f17758a2356c80fc4a53d7e0685d # v2 + uses: haskell-actions/setup@v2.12.0 with: ghc-version: '9.4' cabal-version: '3.10' - name: Cache Cabal - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + uses: actions/cache@v6.1.0 with: path: | ~/.cabal/packages @@ -122,7 +123,7 @@ jobs: echo "Built: $outfile" done - name: Upload artifact - uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 + uses: actions/upload-pages-artifact@v5.0.0 with: path: docs/_site deploy: @@ -136,4 +137,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1 + uses: actions/deploy-pages@v5.0.1 diff --git a/.github/workflows/dogfood-gate.yml b/.github/workflows/dogfood-gate.yml index 43cf758..3aae6c0 100644 --- a/.github/workflows/dogfood-gate.yml +++ b/.github/workflows/dogfood-gate.yml @@ -1,4 +1,5 @@ # SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. # Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) # # dogfood-gate.yml — Hyperpolymath Dogfooding Quality Gate @@ -26,7 +27,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4 + uses: actions/checkout@v7.0.1 - name: Check for A2ML files id: detect @@ -67,7 +68,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4 + uses: actions/checkout@v7.0.1 - name: Check for K9 files id: detect @@ -113,7 +114,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4 + uses: actions/checkout@v7.0.1 - name: Scan for invisible characters id: lint @@ -178,7 +179,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4 + uses: actions/checkout@v7.0.1 - name: Check for Groove manifest id: groove @@ -237,7 +238,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4 + uses: actions/checkout@v7.0.1 - name: Check and validate eclexiaiser manifest id: eclex @@ -303,7 +304,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4 + uses: actions/checkout@v7.0.1 - name: Generate dogfooding scorecard run: | diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 12fc9df..b52f90b 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -1,4 +1,5 @@ # SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. # E2E + Point-to-Point + Unit tests for echidnabot. # # Taxonomy: standards/testing-and-benchmarking/TESTING-TAXONOMY.adoc @@ -50,7 +51,7 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/checkout@v7.0.1 - name: Prove a standalone clone resolves before building run: | @@ -60,7 +61,7 @@ jobs: cargo metadata --format-version 1 >/dev/null echo "✅ dependency graph resolves from a standalone checkout" - - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + - uses: Swatinem/rust-cache@v2.9.2 - name: Unit + Point-to-Point + End-to-End run: | diff --git a/.github/workflows/echidna-fuzz.yml b/.github/workflows/echidna-fuzz.yml index 6b6d99f..d47415f 100644 --- a/.github/workflows/echidna-fuzz.yml +++ b/.github/workflows/echidna-fuzz.yml @@ -1,4 +1,5 @@ # SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. # SPDX-License-Identifier: MPL-2.0 name: Echidna Smart Contract Fuzzing permissions: @@ -111,7 +112,7 @@ jobs: contracts: ${{ steps.detect.outputs.contracts }} contract_count: ${{ steps.detect.outputs.contract_count }} steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/checkout@v7.0.1 - name: Detect contract files id: detect run: | @@ -151,9 +152,9 @@ jobs: matrix: contract: ${{ fromJson(format('["{0}"]', needs.detect-contracts.outputs.contracts)) }} steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/checkout@v7.0.1 - name: Restore Echidna corpus cache - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + uses: actions/cache@v6.1.0 with: path: echidna-corpus key: echidna-corpus-${{ matrix.contract }}-${{ hashFiles(format('contracts/{0}.sol', matrix.contract)) }} @@ -273,13 +274,13 @@ jobs: fi - name: Save corpus cache if: always() - uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + uses: actions/cache/save@v6.1.0 with: path: echidna-corpus key: echidna-corpus-${{ matrix.contract }}-${{ hashFiles(format('contracts/{0}.sol', matrix.contract)) }} - name: Upload Echidna results if: always() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@v7.0.1 with: name: echidna-property-results-${{ matrix.contract }} path: echidna-results/ @@ -299,9 +300,9 @@ jobs: matrix: contract: ${{ fromJson(format('["{0}"]', needs.detect-contracts.outputs.contracts)) }} steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/checkout@v7.0.1 - name: Restore Echidna corpus cache - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + uses: actions/cache@v6.1.0 with: path: echidna-corpus-assertion key: echidna-corpus-assertion-${{ matrix.contract }}-${{ hashFiles(format('contracts/{0}.sol', matrix.contract)) }} @@ -355,7 +356,7 @@ jobs: fi - name: Upload Echidna results if: always() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@v7.0.1 with: name: echidna-assertion-results-${{ matrix.contract }} path: echidna-results/ diff --git a/.github/workflows/echidnabot.yml b/.github/workflows/echidnabot.yml index 02c2464..0abeca4 100644 --- a/.github/workflows/echidnabot.yml +++ b/.github/workflows/echidnabot.yml @@ -1,4 +1,5 @@ # SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. name: echidnabot Proof Verification permissions: contents: read @@ -60,7 +61,7 @@ jobs: has_metamath: ${{ steps.detect.outputs.has_metamath }} any_proofs: ${{ steps.detect.outputs.any_proofs }} steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/checkout@v7.0.1 - name: Detect proof file types id: detect run: | @@ -89,7 +90,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 15 steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/checkout@v7.0.1 - name: Install Coq run: | sudo apt-get update @@ -109,7 +110,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 15 steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/checkout@v7.0.1 - name: Install elan (Lean version manager) run: | curl https://raw.githubusercontent.com/leanprover/elan/master/elan-init.sh -sSf | sh -s -- -y @@ -134,7 +135,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 15 steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/checkout@v7.0.1 - name: Install Z3 run: | sudo apt-get update @@ -154,7 +155,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 15 steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/checkout@v7.0.1 - name: Install Agda run: | sudo apt-get update diff --git a/.github/workflows/governance.yml b/.github/workflows/governance.yml index d8c88d3..be3d7df 100644 --- a/.github/workflows/governance.yml +++ b/.github/workflows/governance.yml @@ -1,4 +1,5 @@ # SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. name: Governance on: diff --git a/.github/workflows/hypatia-scan.yml b/.github/workflows/hypatia-scan.yml index 3d5cd00..da2e480 100644 --- a/.github/workflows/hypatia-scan.yml +++ b/.github/workflows/hypatia-scan.yml @@ -1,4 +1,5 @@ # SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. name: Hypatia Security Scan on: diff --git a/.github/workflows/label-triage.yml b/.github/workflows/label-triage.yml index 9886e92..fc79947 100644 --- a/.github/workflows/label-triage.yml +++ b/.github/workflows/label-triage.yml @@ -1,4 +1,5 @@ # SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. name: Label Triage # Classify newly-filed issues against the estate label taxonomy. diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml index c80b676..af34c6b 100644 --- a/.github/workflows/labels.yml +++ b/.github/workflows/labels.yml @@ -1,4 +1,5 @@ # SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. name: Labels # Applies the canonical estate label set from .github/labels.json. diff --git a/.github/workflows/mirror.yml b/.github/workflows/mirror.yml index e393d81..628ac11 100644 --- a/.github/workflows/mirror.yml +++ b/.github/workflows/mirror.yml @@ -1,4 +1,5 @@ # SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. name: Mirror to Git Forges on: push: diff --git a/.github/workflows/openssf-compliance.yml b/.github/workflows/openssf-compliance.yml index 9d5e636..eaf9981 100644 --- a/.github/workflows/openssf-compliance.yml +++ b/.github/workflows/openssf-compliance.yml @@ -1,4 +1,5 @@ # SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. # OpenSSF Best Practices compliance gate — blocks PRs and pushes that lack # required files or still contain unfilled placeholder tokens. name: OpenSSF Compliance @@ -17,7 +18,7 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4 + - uses: actions/checkout@v7.0.1 with: persist-credentials: false - name: Check SECURITY.md exists and has substance diff --git a/.github/workflows/proof-safety.yml b/.github/workflows/proof-safety.yml index cd9c272..e25a26e 100644 --- a/.github/workflows/proof-safety.yml +++ b/.github/workflows/proof-safety.yml @@ -1,5 +1,5 @@ -# This workflow is managed by gh actions-lock. # SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. name: Proof Safety on: pull_request: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 8b28af4..1ef47aa 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,4 +1,5 @@ # SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. # SPDX-FileCopyrightText: 2025 hyperpolymath # # Publish to crates.io on release tags @@ -31,13 +32,13 @@ jobs: attestations: write steps: - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4 + uses: actions/checkout@v7.0.1 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # stable + uses: dtolnay/rust-toolchain@master with: toolchain: stable - name: Cache cargo - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 + uses: Swatinem/rust-cache@v2.9.2 - name: Install dependencies run: | sudo apt-get update @@ -74,7 +75,7 @@ jobs: - name: Package crate run: cargo package - name: Attest crate provenance - uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 + uses: actions/attest-build-provenance@v4.2.2 with: subject-path: 'target/package/*.crate' - name: Publish to crates.io (dry run) @@ -87,7 +88,7 @@ jobs: run: cargo publish - name: Create GitHub Release if: startsWith(github.ref, 'refs/tags/') - uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v2 + uses: softprops/action-gh-release@v3.0.3 with: generate_release_notes: true draft: false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d4b5f5e..3c5682d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,5 @@ # SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. # SPDX-FileCopyrightText: 2025 hyperpolymath name: Release @@ -44,12 +45,12 @@ jobs: os: windows-latest binary: echidnabot.exe steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/checkout@v7.0.1 - name: Install Rust toolchain # `toolchain:` is mandatory when this action is pinned to a SHA — see # the note in stress-test.yml. Supplying `targets:` alone does not # satisfy it. - uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # stable + uses: dtolnay/rust-toolchain@master with: toolchain: stable targets: ${{ matrix.target }} @@ -86,7 +87,7 @@ jobs: Compress-Archive -Path ${{ matrix.binary }} -DestinationPath echidnabot-${{ env.RELEASE_TAG }}-${{ matrix.target }}.zip Get-FileHash echidnabot-${{ env.RELEASE_TAG }}-${{ matrix.target }}.zip -Algorithm SHA256 | ForEach-Object { "$($_.Hash.ToLower()) echidnabot-${{ env.RELEASE_TAG }}-${{ matrix.target }}.zip" } | Out-File -FilePath echidnabot-${{ env.RELEASE_TAG }}-${{ matrix.target }}.zip.sha256 - name: Upload artifact - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v4 + uses: actions/upload-artifact@v7.0.1 with: name: echidnabot-${{ matrix.target }} path: | @@ -99,13 +100,13 @@ jobs: contents: read needs: build-binaries steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/checkout@v7.0.1 - name: Install cargo-deb run: cargo install cargo-deb - name: Build .deb run: cargo deb - name: Upload .deb - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v4 + uses: actions/upload-artifact@v7.0.1 with: name: deb-package path: target/debian/*.deb @@ -117,7 +118,7 @@ jobs: contents: read needs: build-binaries steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/checkout@v7.0.1 - name: Install cargo-generate-rpm run: cargo install cargo-generate-rpm - name: Build binary @@ -127,7 +128,7 @@ jobs: - name: Build .rpm run: cargo generate-rpm - name: Upload .rpm - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v4 + uses: actions/upload-artifact@v7.0.1 with: name: rpm-package path: target/generate-rpm/*.rpm @@ -141,9 +142,9 @@ jobs: id-token: write attestations: write steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/checkout@v7.0.1 - name: Download all artifacts - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v4 + uses: actions/download-artifact@v8.0.1 with: path: artifacts - name: Collect release assets @@ -152,13 +153,13 @@ jobs: find artifacts -type f \( -name "*.tar.gz" -o -name "*.zip" -o -name "*.sha256" -o -name "*.deb" -o -name "*.rpm" \) -exec cp {} release/ \; ls -la release/ - name: Create Release - uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v2 + uses: softprops/action-gh-release@v3.0.3 with: tag_name: ${{ env.RELEASE_TAG }} files: release/* generate_release_notes: true draft: false - name: Attest build provenance - uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 + uses: actions/attest-build-provenance@v4.2.2 with: subject-path: 'release/*' diff --git a/.github/workflows/rhodibot.yml b/.github/workflows/rhodibot.yml index cfa8e13..687e30c 100644 --- a/.github/workflows/rhodibot.yml +++ b/.github/workflows/rhodibot.yml @@ -1,4 +1,5 @@ # SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. # rhodibot.yml — Automated RSR compliance enforcement # # Reads root-hygiene rules and auto-fixes what it can: @@ -27,7 +28,7 @@ jobs: timeout-minutes: 15 steps: - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4 + uses: actions/checkout@v7.0.1 with: fetch-depth: 1 - name: Rhodibot — Scan and Fix diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml index f96168d..817b9ee 100644 --- a/.github/workflows/rust-ci.yml +++ b/.github/workflows/rust-ci.yml @@ -1,4 +1,5 @@ # SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. # Rust CI — thin wrapper calling the shared estate reusable in # hyperpolymath/standards. Configure once, propagate everywhere. # See: docs/CI-REUSABLE-WORKFLOWS.adoc in standards. @@ -16,6 +17,7 @@ jobs: # ordinary PR gate aligned with publish.yml: run every named integration # suite explicitly while the library-test deadlock is tracked separately. with: + enable_audit: true test_args: >- --test integration_tests --test lifecycle diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 11d9b2a..1871ab9 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -1,4 +1,5 @@ # SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. name: OSSF Scorecard on: diff --git a/.github/workflows/secret-scanner.yml b/.github/workflows/secret-scanner.yml index bf2df40..aeec938 100644 --- a/.github/workflows/secret-scanner.yml +++ b/.github/workflows/secret-scanner.yml @@ -1,4 +1,5 @@ # SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. # Thin wrapper around hyperpolymath/standards secret-scanner-reusable.yml. # Matches the echidna sibling-repo pattern. The reusable contains the # canonical trufflehog + gitleaks + rust-secrets (+ shell-secrets) diff --git a/.github/workflows/static-analysis-gate.yml b/.github/workflows/static-analysis-gate.yml index 4398ef4..b7ddaa7 100644 --- a/.github/workflows/static-analysis-gate.yml +++ b/.github/workflows/static-analysis-gate.yml @@ -1,4 +1,5 @@ # SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. # Static Analysis Gate — Required by branch protection rules. # Runs panic-attack and hypatia, deposits findings for gitbot-fleet learning. name: Static Analysis Gate @@ -19,7 +20,7 @@ jobs: timeout-minutes: 15 steps: - name: Checkout repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + uses: actions/checkout@v7.0.1 with: fetch-depth: 0 - name: Install panic-attack (if available) @@ -116,7 +117,7 @@ jobs: echo "" >> "$GITHUB_STEP_SUMMARY" echo "Skipped: panic-attack not available in this environment." >> "$GITHUB_STEP_SUMMARY" - name: Upload panic-attack findings - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@v7.0.1 with: name: panic-attack-findings path: panic-attack-findings.json @@ -135,13 +136,13 @@ jobs: timeout-minutes: 15 steps: - name: Checkout repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + uses: actions/checkout@v7.0.1 with: fetch-depth: 0 - name: Setup Elixir for Hypatia scanner id: beam continue-on-error: true - uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.18.2 + uses: erlef/setup-beam@v1.24.1 with: elixir-version: '1.19.4' otp-version: '28.3' @@ -242,7 +243,7 @@ jobs: echo "" >> "$GITHUB_STEP_SUMMARY" echo "Skipped: Hypatia scanner not available in this environment." >> "$GITHUB_STEP_SUMMARY" - name: Upload hypatia findings - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@v7.0.1 with: name: hypatia-findings path: hypatia-findings.json @@ -261,7 +262,7 @@ jobs: timeout-minutes: 15 steps: - name: Checkout repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + uses: actions/checkout@v7.0.1 with: fetch-depth: 0 - name: Install panic-attack (if available) @@ -323,7 +324,7 @@ jobs: echo "" >> "$GITHUB_STEP_SUMMARY" echo "Skipped: panic-attack not available in this environment." >> "$GITHUB_STEP_SUMMARY" - name: Upload bridge report - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@v7.0.1 with: name: bridge-report path: bridge-report.json @@ -345,17 +346,17 @@ jobs: if: always() steps: - name: Download panic-attack findings - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v4 + uses: actions/download-artifact@v8.0.1 with: name: panic-attack-findings path: findings/ - name: Download hypatia findings - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v4 + uses: actions/download-artifact@v8.0.1 with: name: hypatia-findings path: findings/ - name: Download bridge report - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v4 + uses: actions/download-artifact@v8.0.1 with: name: bridge-report path: findings/ @@ -415,7 +416,7 @@ jobs: echo "medium=$MEDIUM" >> "$GITHUB_OUTPUT" echo "low=$LOW" >> "$GITHUB_OUTPUT" - name: Upload unified findings (fleet scanner picks these up) - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@v7.0.1 with: name: unified-findings path: findings/unified-findings.json diff --git a/.github/workflows/stress-test.yml b/.github/workflows/stress-test.yml index a3d8776..bb32cf8 100644 --- a/.github/workflows/stress-test.yml +++ b/.github/workflows/stress-test.yml @@ -1,4 +1,5 @@ # SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. name: Stress Testing on: schedule: @@ -10,13 +11,13 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 60 steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4 + - uses: actions/checkout@v7.0.1 - name: Install Rust toolchain # `toolchain:` is mandatory when this action is pinned to a SHA: it # normally infers the toolchain from the ref that called it (@stable -> # "stable"), and a SHA carries no such name. Without it the action fails # with `error: invalid toolchain name ''`. - uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b # stable 1.91 + uses: dtolnay/rust-toolchain@master with: toolchain: stable diff --git a/.github/workflows/workflow-linter.yml b/.github/workflows/workflow-linter.yml index bd1c3e9..0daf533 100644 --- a/.github/workflows/workflow-linter.yml +++ b/.github/workflows/workflow-linter.yml @@ -1,4 +1,5 @@ # SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. # Prevention workflow - validates all workflows have proper security config name: Workflow Security Linter on: @@ -14,7 +15,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 15 steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4 + - uses: actions/checkout@v7.0.1 - name: Check SPDX headers run: | errors=0 diff --git a/.machine_readable/STATE.a2ml b/.machine_readable/STATE.a2ml deleted file mode 100644 index c1daf9b..0000000 --- a/.machine_readable/STATE.a2ml +++ /dev/null @@ -1,439 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) -# -# STATE.a2ml — Echidnabot project state -[metadata] -project = "echidnabot" -version = "0.1.0" -last-updated = "2026-04-26" # seam test: 7a mode-routing + 7b corpus-bridge end-to-end -status = "active" -session = "seam test DONE (tests/seam_test.rs, 15 tests); smoke.rs mode_selector bug fixed; lifecycle.rs unused-import cleaned; total 184/184 tests (137 lib + 17 lifecycle + 32 integration + 12 property + 15 seam + 8 smoke)" - -[project-context] -name = "Echidnabot" -purpose = """ -Tier-1 Verifier bot in the gitbot-fleet. Bridges webhook events from -GitHub/GitLab/Bitbucket to the ECHIDNA proof engine, schedules verification -jobs with isolation, and reports findings back to the fleet. Exposes -double-loop feedback: reranks ECHIDNA's ML suggestions by local tactic -history, feeds successful proofs into echidna's retrain corpus. -""" -completion-percentage = 100 - -[position] -phase = "implementation" -maturity = "beta" - -# ═══════════════════════════════════════════════════════════════════════════════ -# CRG (Critical Reception Grade) advancement — D→C→B -# Per memory rule: B-grade requires [external-targets]≥6, [issues-fed-back], -# [field-signal] populated. -# ═══════════════════════════════════════════════════════════════════════════════ - -[external-targets] -[[external-targets.target]] -name = "gitbot-fleet" -relationship = "fleet-member" -contract = "Tier-1 Verifier per fleet topology; coordinates with rhodibot/panicbot/sustainabot via shared-context" -discovered = "ECOSYSTEM.a2ml + bot directives in target repos" - -[[external-targets.target]] -name = "echidna (upstream proof engine)" -relationship = "consumer-of" -contract = "REST/GraphQL via dispatcher/echidna_client.rs; 12 of 113 ProverKind variants currently surfaced" -discovered = "src/dispatcher/echidna_client.rs" - -[[external-targets.target]] -name = "AI assistants (Claude Desktop / Claude Code)" -relationship = "consumer-via-mcp" -contract = "JSON-RPC over stdio via echidnabot-mcp bin (4 tools: suggest_tactics, record_tactic_outcome, list_outcome_history, corpus_refresh)" -discovered = "Package 7b-4 — temporary BoJ-only-MCP exception (ADR-001)" - -[[external-targets.target]] -name = "GitHub / GitLab / Bitbucket" -relationship = "webhook-source" -contract = "PlatformAdapter trait per platform; HMAC-SHA256 signature verification when webhook_secret configured" -discovered = "src/adapters/{github,gitlab,bitbucket}.rs" - -[[external-targets.target]] -name = "boj-server / cartridges / echidna-llm-mcp" -relationship = "future-absorption" -contract = "When BoJ loader supports double-loop feedback protocol, echidnabot-mcp folds into the BoJ cartridge per ADR-001 sunset clause" -discovered = "AGENTIC.a2ml [exceptions.boj-only-mcp]" - -[[external-targets.target]] -name = "hypatia (estate-wide neurosym CI)" -relationship = "sibling-consumer" -contract = "Hypatia scanning workflow consumes echidnabot fleet status as part of CI signals" -discovered = ".github/workflows/hypatia-scan.yml" - -[[external-targets.target]] -name = "proof-bearing repos (registered)" -relationship = "monitored-targets" -contract = "Repos that `register` with echidnabot become subjects of webhook-driven proof verification; per-repo provers/modes stored in `repositories` table" -discovered = "src/main.rs `register` subcommand + repositories schema" - -[issues-fed-back] -[[issues-fed-back.issue]] -source = "SONNET-TASKS.md Task 1" -date = "2026-04 (pre-session)" -issue = "src/executor/container.rs is empty — proofs ran without isolation, malicious proof could execute arbitrary code" -status = "open — Sonnet's territory; tracked in SONNET-TASKS.md" - -[[issues-fed-back.issue]] -source = "SONNET-TASKS.md Task 2 + EXPLAINME.adoc caveat" -date = "2026-04-24" -issue = "Bot modes Advisor/Consultant/Regulator defined but not wired into webhook handlers; only Verifier path executes" -status = "resolved (T3 2026-04-26) — ModeSelector added to AppState; resolve_mode_with_daemon_default threads daemon-wide [bot] mode; Consultant posts inline review comments with line extraction; Advisor/Regulator post PR comments; check runs wired for all modes; 136/136 lib tests pass" - -[[issues-fed-back.issue]] -source = "echidnabot-mcp 7b-3 schema gap" -date = "2026-04-24" -issue = "CorpusDelta delta_*.jsonl format didn't match merge_corpus.jl PER_PROVER_FILES expectations" -status = "resolved upstream — echidna commit d16347d added glob ingest" - -[[issues-fed-back.issue]] -source = "e2e test 2026-04-24 — webhook_secret unset" -date = "2026-04-24" -issue = "Empty-body POST to /webhooks/github returned 200 without signature verification when no webhook_secret configured" -status = "resolved — startup warning added in commit fa692dd" - -[[issues-fed-back.issue]] -source = "e2e test 2026-04-24 — serve subcommand TOML override" -date = "2026-04-24" -issue = "`serve` subcommand had hardcoded clap defaults that silently overrode TOML [server] section" -status = "resolved — flags now Option-typed, fall back to config in commit e018ce8" - -[[issues-fed-back.issue]] -source = "DRIFT-001 dual-truth pattern" -date = "2026-04-24" -issue = "echidnabot tracked twice: as files in echidna parent + standalone repo. Edits diverged silently." -status = "resolved — echidna commit 9218f77 deleted the embedded copy; standalone canonical" - -[field-signal] -[[field-signal.signal]] -source = "cargo test --lib + --bin echidnabot-mcp" -metric = "test pass rate" -value = "119/119 lib + 6/6 mcp = 125 green" -collected = "2026-04-25 (post-reqwest 0.12 port 7f403e0)" - -[[field-signal.signal]] -source = "Dependabot scanner" -metric = "open vulnerability count" -value = "0 open / 9 fixed (post 1ff7647 + 7f403e0)" -collected = "2026-04-25" - -[[field-signal.signal]] -source = "e2e test 2026-04-24 (5:42 uptime, 18 jobs dispatched)" -metric = "process stability" -value = "RSS 30.9 MB stable (no leak); zero crashes; zero panics; no .unwrap() failures despite 100 sites" -collected = "2026-04-24 e2e simulation" - -[[field-signal.signal]] -source = "e2e test 2026-04-24 — webhook intake" -metric = "successful intake under load" -value = "9/9 webhooks accepted with HTTP 200 (1 init + 8 loop), all enqueued + processed" -collected = "2026-04-24 e2e simulation" - -[[field-signal.signal]] -source = "e2e test 2026-04-24 — graceful degradation" -metric = "ECHIDNA-unavailable handling" -value = "All 18 jobs correctly marked Failed with consistent error; no cascading failures; fleet skipped without exception" -collected = "2026-04-24 e2e simulation" - -[[field-signal.signal]] -source = "BoJ zig adapter status" -metric = "MCP cartridge fallback dependency" -value = "BoJ zig adapter removed (zig ban 2026-04-10); Zig replacement empty source dir; native echidnabot-mcp shipped as documented exception until BoJ re-enabled" -collected = "ADR-001 in META.a2ml" - -# ═══════════════════════════════════════════════════════════════════════════════ - -[route-to-mvp] -milestones = [ - "7a: Sonnet — wire bot modes (Advisor/Consultant/Regulator) into webhook handlers + container executor ✓", - "7b: Opus — double-loop actuation ✓", - "integration: 7a + 7b seam test via dogfood proofs/ ✓ (tests/seam_test.rs, 15 tests, 2026-04-26)", -] - -[blockers-and-issues] -issues = [ - "BoJ Zig adapter source dir empty — echidnabot-mcp routes through existing BoJ cartridges as a stopgap. Needs Zig shim before BoJ-only-MCP rule is fully satisfied.", - "GitLab + Bitbucket Discussions/Diff API for true inline review comments deferred — stubs fall back to MR note.", -] - -[known-drift] -# Standing structural issues that are not bugs but need eventual cleanup. -# Each entry MUST name the drift, its blast radius, and its resolution path. - -[[known-drift.entry]] -id = "DRIFT-001" -name = "dual-truth monorepo + standalone" -description = """ -echidnabot is registered in the parent echidna repo's .gitmodules but its -contents are checked in as regular files (not a live submodule). This creates -two sources of truth: changes to .machine_readable/, src/, etc. must land in -both this standalone repo and the monorepo copy at -echidna/echidnabot/ — otherwise the two diverge silently. - -Pattern catalogued in memory: project_gitbot_fleet_dual_truth_pattern.md. -""" -blast-radius = "all edits to .machine_readable/, src/, docs/ must be mirrored" -detection = "git diff against monorepo copy; CI could be added to fail on divergence" -resolution = "either (a) promote to real submodule with .gitmodules resolving, or (b) delete the .gitmodules entry and treat as pure monorepo subdir. Pick one, not both." -status = "acknowledged, no scheduled fix" -discovered = "2026-04-24 (by parallel Claude session during Package 7 sweep)" - -[critical-next-actions] -actions = [ - "7a DONE (T3 2026-04-26): ModeSelector, review-comment pipeline, daemon-wide cascade — 136/136 tests", - "7b-3 DONE (2026-04-26 commit 9887638): two-path write — delta_*.jsonl audit log + proof_states_echidnabot_*.jsonl corpus feed; canonical_prover_name() normalisation; 8 new tests; 137/137 total", - "Task-E DONE (T2 2026-04-26): 48 call-site & fixes + solver_integrity.rs prover_key() debug-repr fix; 129/129 standalone", - "container.rs DONE (T2 2026-04-26): 848 LoC, Podman+bwrap isolation, cgroups v2", - "SEAM TEST DONE (2026-04-26): tests/seam_test.rs — 15 tests covering 7a mode-routing + 7b corpus-bridge; smoke.rs mode_selector bug fixed; 184/184 total; MVP gates all closed", - "DEFERRED: BoJ Zig adapter (empty source dir)", - "DEFERRED: GitLab + Bitbucket Discussions/Diff API inline comments", -] - -[maintenance-status] -last-run-utc = "2026-04-26T00:00:00Z" -last-result = "pass" # 184/184 all tests: 137 lib + 17 lifecycle + 32 integration + 12 property + 15 seam + 8 smoke - -[components.double-loop-feedback] -status = "done" -completion = 100 -notes = "7b-1..5 DONE, 7b-3 schema bridge fixed (9887638), 7b-6 deferred (BoJ Zig adapter). MVP gate = seam test." - -[[components.double-loop-feedback.subtask]] -id = "7b-1" -title = "tactic_outcome table + fingerprint helper" -status = "done" -commit = "0cc4b4a" - -[[components.double-loop-feedback.subtask]] -id = "7b-2" -title = "history-weighted reranker (Laplace-smoothed)" -status = "done" -commit = "b6ef652" - -[[components.double-loop-feedback.subtask]] -id = "7b-3" -title = "CorpusDelta writer + trigger_refresh" -status = "done" -commit = "9887638" -notes = "Two-path write: delta_*.jsonl (audit, all rows) + proof_states_echidnabot_*.jsonl (corpus feed, successes only). canonical_prover_name() mapping. 8 tests." - -[[components.double-loop-feedback.subtask]] -id = "7b-4" -title = "native echidnabot-mcp binary (temporary BoJ exception)" -status = "retired" -commit = "b01cc07" # landed as part of the EXPLAINME commit scoop -adr = "ADR-001 (sunset 2026-04-25)" -retired-on = "2026-04-25" -retired-via = "0cfeb60" -retired-reason = "BoJ revival made the BoJ-only-MCP exception unnecessary; bin removed, traffic now routes through boj-server cartridges/echidna-llm-mcp (consultant_qa tool)" - -[[components.double-loop-feedback.subtask]] -id = "7b-5" -title = "dogfood proofs (2 Coq + 2 Lean) under proofs/" -status = "done" -commit = "ec25aae" - -[[components.double-loop-feedback.subtask]] -id = "7b-6" -title = "6a2 sweep + project memory for BoJ exception" -status = "done" -commit = "c0f30fd" -note = "Exception flipped to superseded with sunset record; memory file feedback_boj_mcp_exception_echidnabot.md flipped to historical; MEMORY.md critical-rules entry updated." - -# ─── Phase 6 BoJ-routed Q&A end-to-end validation ───────────────────────── -[[components.double-loop-feedback.subtask]] -id = "7b-7" -title = "BoJ-routed Consultant Q&A end-to-end validation" -status = "done" -commit = "post-0cfeb60" -date = "2026-04-25" -evidence = """ -Live test against revived BoJ on 127.0.0.1:7700: -- GET /health → 200 OK ✓ -- GET /cartridges → 106 cartridges listed including echidna-llm-mcp ✓ -- POST /cartridge/echidna-llm-mcp/invoke {tool: consultant_qa, ...} - routes correctly to the cartridge layer; cartridge's onward call to - echidna fails with exit_code 7 (cli_crashed) — expected because - echidna isn't running on this workstation. The failure surfaces at - the right layer, confirming the BoJ→cartridge hop is intact. -- echidnabot's query_boj_q_and_a (commit 4a7871c) targets the same - /cartridge//invoke endpoint; no client-side change was needed - for the sunset migration. -""" - -# ─── Panic-attack sweep 2026-04-25 ───────────────────────────────────── -[panic-attack-sweep] -date = "2026-04-25" -last-result = "pass-with-classified-false-positives" -findings = """ -echidnabot: - - 2 Critical (ProofDrift on proofs/coq/admitted_stub.v + proofs/lean/sorry_stub.lean) - → INTENTIONAL DOGFOOD FIXTURES from commit ec25aae. Designed to - exercise the bot's failure path. False positives — panic-attack - lacks fixture-exclusion; structural-first per memory rule but - no config option exists. Not chasing. - - 1 Low (MutationGap — no cargo-mutants config). Add later if mutation - testing becomes an estate-wide rule. - - Statistics: 17 unwrap_calls, 0 panic_sites, 15 unsafe_blocks (all - legitimate FFI), 14 threading constructs. - -vcl-ut: - - 2 Critical (UnboundedAllocation on src/interface/{fmt,lint}/src/main.rs) - → fs::read_to_string of operator-supplied file. CLI tools, trusted - input. False positive in this context (no untrusted-input boundary). - - 1 Medium (InsecureProtocol on echidna-client lib.rs) - → http://127.0.0.1:8000 as the default echidna URL. Localhost; HTTPS - isn't expected for default local-dev path. False positive. - - 1 Low (MutationGap). - -boj-server cartridges/echidna-llm-mcp: - - 1 High (UnsafeCode on ffi/cartridge_shim.zig) - → 1 unsafe @ptrCast in the FFI shim. Per memory rule "panic-attack - unsafe_blocks conflates banned partial casts with legitimate FFI - try/catch — classify each hit." This one is legitimate FFI surface. - - 1 Medium (InsecureProtocol on mod.js) - → localhost echidna URL. False positive (same as vcl-ut). - -echidna: - - SCAN TIMED OUT at 180s (repo too large for a single assail run). - Defer to a chunked/per-subdir invocation in a future sweep. - -Real critical findings: 0. All flagged criticals are classified -false-positives or operator-trusted-input patterns. -""" - -# ─── Task E: ProverKind Enum→Slug Migration (2026-04-25) ───────────────────────── -[session-2026-04-25-task-e] -title = "ProverKind enum→slug migration: 12-variant → dynamic 113-prover support" -phase = "implementation" -completion = 100 -status = "done — 0 compilation errors; 132 lib tests + 8 integration tests pass (2026-04-26)" -date = "2026-04-25" -session-key = "echidnabot-task-e" - -[session-2026-04-25-task-e.context] -objective = """ -Migrate echidnabot from 12-variant ProverKind enum to a ProverSlug(String) struct -that dynamically supports all 113 echidna provers at runtime. This unblocks -Advisor/Consultant/Regulator bot modes, which require access to the full prover -surface that echidna provides. -""" - -epic = "Echidna Reorganisation + Roadmap + A2ML Conformance (2026-04-25 plan)" -upstream = "ADR-CART-003 (2026-04-25) in dispatcher/mod.rs; ProverSlug replaces enum" - -[session-2026-04-25-task-e.migration-strategy] -approach = """ -1. Replace 12-variant enum ProverKind with struct ProverSlug(String) -2. Add type alias: pub type ProverKind = ProverSlug (backwards compatibility) -3. Update CLASSIC_PROVERS constant to map (slug, display_name) tuples -4. Implement methods: new(), as_str(), from_extension(), display_name(), tier(), - file_extensions(), classic_all(), all() -5. Convert 224+ enum-variant match patterns to .as_str() string comparisons -6. Update function signatures: owned ProverKind → &ProverKind references -7. Handle struct fields: avoid lifetime specifiers by owning ProverKind, use .clone() - at construction time (trade clone cost for simpler lifetime annotations) -""" - -affected-modules = [ - "src/dispatcher/mod.rs (core struct + methods)", - "src/store/sqlite.rs (parse_prover → dynamic ProverKind::new())", - "src/api/graphql.rs (identity map_prover_kind, reference passing)", - "src/dispatcher/echidna_client.rs (function signatures → &ProverKind)", - "src/executor/container.rs (prover_to_env_name, prover_extension, prover_command)", - "src/trust/axiom_tracker.rs (scan method signature)", - "src/trust/confidence.rs (struct field ownership, .clone() at construction)", - "src/trust/solver_integrity.rs (struct field: owned ProverKind)", - "src/webhooks.rs (estimated, parameter passing)", - "src/feedback/reranker.rs (estimated, parameter passing)", -] - -[session-2026-04-25-task-e.commits] -commit-1 = """ -7052195 feat(Task E): Begin ProverKind enum→slug migration for 113-prover support - - Replaced 12-variant ProverKind enum with struct ProverSlug(String) - - Added type alias pub type ProverKind = ProverSlug for backwards compatibility - - Updated CLASSIC_PROVERS constant (12 provers with display names and extensions) - - Implemented core methods: new(), as_str(), from_extension(), display_name() - - Implemented tier(), file_extensions(), classic_all(), all() - - Updated src/store/sqlite.rs parse_prover(): enum patterns → ProverKind::new() - - First pass error reduction: 136 → 136 errors (enum variant patterns remain) -""" - -commit-2 = """ -1b465ba refactor(Task E): Continue ProverKind→ProverSlug migration integration - - Converted match statements: enum patterns → .as_str() string comparisons - - Updated function signatures: owned → &ProverKind references throughout - - Replaced struct fields: &ProverKind → owned ProverKind with .clone() calls - - Changed .copied() → .cloned() for non-Copy iterator handling - - Updated src/api/graphql.rs: identity maps for map_prover_kind/map_prover_kind_to_core - - Updated src/dispatcher/echidna_client.rs: verify_proof, suggest_tactics, prover_status - - Updated src/executor/container.rs: prover_to_env_name, prover_extension, prover_command - - Updated src/trust/{axiom_tracker,confidence,solver_integrity}.rs - - Second pass error reduction: 136 → 26 errors (systematic parameter-type integration remains) -""" - -[session-2026-04-25-task-e.error-analysis] -initial-count = 136 -post-variant-replacement = 136 -post-match-conversion = 38 -post-signature-fixes = 31 -current-count = 26 -remaining-pattern = "Parameter type mismatches in executor/container.rs, feedback/reranker.rs, webhooks.rs" - -errors-by-category = [ - "E0308 (type mismatch): expecting &ProverKind at call site, parameter now reference", - "E0382 (moved value): secondary uses of prover in same scope require reference", - "E0507 (borrow of moved): dereferencing borrowed values to move them", -] - -key-fixes-applied = [ - "Match pattern conversion: ProverKind::new(\"coq\") → prover.as_str() == \"coq\"", - "Parameter passing: changed function signatures to take &ProverKind where needed", - "Struct ownership: changed fields from &ProverKind to owned ProverKind", - "Iterator operations: .copied() → .cloned() for String-based structs", - "Default match arms: added catch-all for unknown provers (pass slug through)", -] - -[session-2026-04-25-task-e.remaining-work] -blocked-by = "26 compilation errors in parameter-type integration" -estimated = "2–4 hours (systematic per-module integration)" -path-forward = """ -1. Run `cargo check` and classify remaining 26 errors -2. For each error, determine if function should take reference or owned value -3. Update function signatures in executor/container.rs, feedback/reranker.rs -4. Update all call sites to match new signatures (add & or remove &) -5. Verify all 113 provers accessible via ProverSlug::new(\"slug-name\") -6. Integration test: verify Advisor/Consultant/Regulator bot modes can access - beyond the classic 12 (spot-check 3–5 extended provers like linear-agda, etc.) -7. Commit final integration pass -8. Run full test suite: cargo test --lib + cargo test --bin echidnabot-mcp -""" - -blockers = [ - "No architectural blockers — design pattern (string-based slugs) is proven", - "No external API changes needed — echidna already supports all 113 provers", - "No dep updates needed — no new crates required", -] - -[session-2026-04-25-task-e.validation] -testing = "cargo check clean ✓ (minus 26 remaining type mismatches)" -dogfood = "proofs/ fixtures (admit_stub.v, sorry_stub.lean) exercise the new code paths" -integration = "Requires bot-mode wiring (Task 7a) before E2E validation possible" - -[session-2026-04-25-task-e.unblocks] -for-phase = "Enables Advisor/Consultant/Regulator bot modes full 113-prover surface" -for-3rd-party = "LLM agents calling echidnabot API will access all 113 provers" -for-roadmap = "Stage 3 (estate-wide service discovery) + Stage 4 (expert API)" - -[session-2026-04-25-task-e.documentation-landed] -file-1 = "Desktop/AI-WORK-todo.md — Task E entry at top with full progress summary" -file-2 = ".machine_readable/6a2/STATE.a2ml — this block (2026-04-25)" -file-3 = "src/dispatcher/mod.rs — ADR-CART-003 comment block + method documentation" -file-4 = "Two commits (7052195 + 1b465ba) on main branch with detailed commit messages" diff --git a/.machine_readable/contractiles/Mustfile.a2ml b/.machine_readable/contractiles/Mustfile.a2ml index 55f8ab4..611ef25 100644 --- a/.machine_readable/contractiles/Mustfile.a2ml +++ b/.machine_readable/contractiles/Mustfile.a2ml @@ -63,12 +63,12 @@ hooks fail if any check fails. ### six-directory-present - description: 6a2 directory exists with required files -- run: test -d .machine_readable/6a2 && test -f .machine_readable/6a2/META.a2ml && test -f .machine_readable/6a2/ECOSYSTEM.a2ml && test -f .machine_readable/6a2/STATE.a2ml && test -f .machine_readable/6a2/PLAYBOOK.a2ml && test -f .machine_readable/6a2/AGENTIC.a2ml && test -f .machine_readable/6a2/NEUROSYM.a2ml +- run: test -d .machine_readable/6a2 && test -f .machine_readable/descriptiles/META.a2ml && test -f .machine_readable/descriptiles/ECOSYSTEM.a2ml && test -f .machine_readable/descriptiles/STATE.a2ml && test -f .machine_readable/descriptiles/PLAYBOOK.a2ml && test -f .machine_readable/descriptiles/AGENTIC.a2ml && test -f .machine_readable/descriptiles/NEUROSYM.a2ml - severity: critical ### anchors-directory - description: anchors directory exists in 6a2 -- run: test -d .machine_readable/6a2/anchors +- run: test -d .machine_readable/descriptiles/anchors - severity: warning ### self-validating-structure diff --git a/.machine_readable/6a2/0-AI-MANIFEST.a2ml b/.machine_readable/descriptiles/0-AI-MANIFEST.a2ml similarity index 100% rename from .machine_readable/6a2/0-AI-MANIFEST.a2ml rename to .machine_readable/descriptiles/0-AI-MANIFEST.a2ml diff --git a/.machine_readable/6a2/AGENTIC.a2ml b/.machine_readable/descriptiles/AGENTIC.a2ml similarity index 100% rename from .machine_readable/6a2/AGENTIC.a2ml rename to .machine_readable/descriptiles/AGENTIC.a2ml diff --git a/.machine_readable/6a2/ECOSYSTEM.a2ml b/.machine_readable/descriptiles/ECOSYSTEM.a2ml similarity index 100% rename from .machine_readable/6a2/ECOSYSTEM.a2ml rename to .machine_readable/descriptiles/ECOSYSTEM.a2ml diff --git a/.machine_readable/6a2/META.a2ml b/.machine_readable/descriptiles/META.a2ml similarity index 100% rename from .machine_readable/6a2/META.a2ml rename to .machine_readable/descriptiles/META.a2ml diff --git a/.machine_readable/6a2/NEUROSYM.a2ml b/.machine_readable/descriptiles/NEUROSYM.a2ml similarity index 100% rename from .machine_readable/6a2/NEUROSYM.a2ml rename to .machine_readable/descriptiles/NEUROSYM.a2ml diff --git a/.machine_readable/6a2/PLAYBOOK.a2ml b/.machine_readable/descriptiles/PLAYBOOK.a2ml similarity index 100% rename from .machine_readable/6a2/PLAYBOOK.a2ml rename to .machine_readable/descriptiles/PLAYBOOK.a2ml diff --git a/.machine_readable/6a2/README.adoc b/.machine_readable/descriptiles/README.adoc similarity index 100% rename from .machine_readable/6a2/README.adoc rename to .machine_readable/descriptiles/README.adoc diff --git a/.machine_readable/6a2/STATE.a2ml b/.machine_readable/descriptiles/STATE.a2ml similarity index 98% rename from .machine_readable/6a2/STATE.a2ml rename to .machine_readable/descriptiles/STATE.a2ml index 04df37b..4ddd97d 100644 --- a/.machine_readable/6a2/STATE.a2ml +++ b/.machine_readable/descriptiles/STATE.a2ml @@ -434,6 +434,10 @@ for-roadmap = "Stage 3 (estate-wide service discovery) + Stage 4 (expert API)" [session-2026-04-25-task-e.documentation-landed] file-1 = "Desktop/AI-WORK-todo.md — Task E entry at top with full progress summary" -file-2 = ".machine_readable/6a2/STATE.a2ml — this block (2026-04-25)" +file-2 = ".machine_readable/descriptiles/STATE.a2ml — this block (2026-04-25)" file-3 = "src/dispatcher/mod.rs — ADR-CART-003 comment block + method documentation" file-4 = "Two commits (7052195 + 1b465ba) on main branch with detailed commit messages" + +[history.retired-root-state] +last-updated = "2026-04-26" # seam test: 7a mode-routing + 7b corpus-bridge end-to-end +session = "seam test DONE (tests/seam_test.rs, 15 tests); smoke.rs mode_selector bug fixed; lifecycle.rs unused-import cleaned; total 184/184 tests (137 lib + 17 lifecycle + 32 integration + 12 property + 15 seam + 8 smoke)" diff --git a/.machine_readable/6a2/anchor/0-AI-MANIFEST.a2ml b/.machine_readable/descriptiles/anchor/0-AI-MANIFEST.a2ml similarity index 100% rename from .machine_readable/6a2/anchor/0-AI-MANIFEST.a2ml rename to .machine_readable/descriptiles/anchor/0-AI-MANIFEST.a2ml diff --git a/.machine_readable/6a2/anchor/ANCHOR.a2ml b/.machine_readable/descriptiles/anchor/ANCHOR.a2ml similarity index 100% rename from .machine_readable/6a2/anchor/ANCHOR.a2ml rename to .machine_readable/descriptiles/anchor/ANCHOR.a2ml diff --git a/.machine_readable/6a2/anchor/README.adoc b/.machine_readable/descriptiles/anchor/README.adoc similarity index 100% rename from .machine_readable/6a2/anchor/README.adoc rename to .machine_readable/descriptiles/anchor/README.adoc diff --git a/0-AI-MANIFEST.a2ml b/0-AI-MANIFEST.a2ml index fe9caa7..a4fe2a2 100644 --- a/0-AI-MANIFEST.a2ml +++ b/0-AI-MANIFEST.a2ml @@ -16,18 +16,18 @@ fleet = "gitbot-fleet (Tier-1 Verifier role)" human-readable = ["README.adoc", "EXPLAINME.adoc", "ROADMAP.adoc"] ai-instructions = [".claude/CLAUDE.md"] ai-state = [ - ".machine_readable/6a2/STATE.a2ml", - ".machine_readable/6a2/META.a2ml", - ".machine_readable/6a2/AGENTIC.a2ml", - ".machine_readable/6a2/ECOSYSTEM.a2ml", + ".machine_readable/descriptiles/STATE.a2ml", + ".machine_readable/descriptiles/META.a2ml", + ".machine_readable/descriptiles/AGENTIC.a2ml", + ".machine_readable/descriptiles/ECOSYSTEM.a2ml", ] bot-directives = [".machine_readable/bot_directives/echidnabot.a2ml"] abi-ffi-surface = ["ABI-FFI-README.md", "src/abi/", "ffi/zig/"] # ─── Operating rules ──────────────────────────────────────────────────────── [workflow] -inspect-first = ".machine_readable/6a2/STATE.a2ml" # blockers + next actions -respect = ".machine_readable/6a2/AGENTIC.a2ml" # tooling constraints + [exceptions.*] +inspect-first = ".machine_readable/descriptiles/STATE.a2ml" # blockers + next actions +respect = ".machine_readable/descriptiles/AGENTIC.a2ml" # tooling constraints + [exceptions.*] self-directive = ".machine_readable/bot_directives/echidnabot.a2ml" # current mode = advisor # ─── Delivery promises ────────────────────────────────────────────────────── @@ -38,7 +38,7 @@ respect-claude-md = true # .claude/CLAUDE.md is binding for AI assistants # ─── Format migration history ─────────────────────────────────────────────── [history] -2026-04-17 = "migrated from Guile Scheme (`.machine_readable/6scm/*.scm`) to TOML-flavoured A2ML (`.machine_readable/6a2/*.a2ml`)" +2026-04-17 = "migrated from Guile Scheme (`.machine_readable/6scm/*.scm`) to TOML-flavoured A2ML (`.machine_readable/descriptiles/*.a2ml`)" 2026-04-25 = "bot_directives extended with [bot] mode-resolver block" 2026-04-26 = "test suite reached 184/184 passing" 2026-05-30 = ".claude/CLAUDE.md flipped AffineScript→AffineScript per estate policy" diff --git a/CONTRIBUTING.adoc b/CONTRIBUTING.adoc index 8bc5c2e..421e82c 100644 --- a/CONTRIBUTING.adoc +++ b/CONTRIBUTING.adoc @@ -274,7 +274,7 @@ ClusterFuzzLite runs continuous fuzzing in CI; see `+ABI-FFI-README.md+` |Roadmap item closed |`+ROADMAP.adoc+` + -`+.machine_readable/6a2/STATE.a2ml+` +`+.machine_readable/descriptiles/STATE.a2ml+` |=== ==== Wiki @@ -287,7 +287,7 @@ procedure. ==== Machine-readable state -`+.machine_readable/6a2/+` holds A2ML descriptors consumed by Hypatia +`+.machine_readable/descriptiles/+` holds A2ML descriptors consumed by Hypatia and sibling bots. When a contribution lands a substantive change (new external target, closed-issue feedback loop, completion-percentage shift), update `+STATE.a2ml+` in the same commit. diff --git a/Cargo.lock b/Cargo.lock index a8dabcb..5aff155 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -83,7 +83,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -94,7 +94,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -1235,7 +1235,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -1571,9 +1571,9 @@ checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "h2" -version = "0.4.14" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" +checksum = "a9f37a958b41b3b19ee2707c06439c0e9e547e847223eb791ecb0cb821c65e27" dependencies = [ "atomic-waker", "bytes", @@ -2390,7 +2390,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -3437,7 +3437,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -3788,7 +3788,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -4116,7 +4116,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -4973,7 +4973,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] diff --git a/EXPLAINME.adoc b/EXPLAINME.adoc index 868b997..f85d8a0 100644 --- a/EXPLAINME.adoc +++ b/EXPLAINME.adoc @@ -144,7 +144,7 @@ self-healing cascades. === BoJ-only-MCP exception A documented carve-out from the estate-wide "all MCP through BoJ" rule. -See `.machine_readable/6a2/AGENTIC.a2ml` `[exceptions.boj-only-mcp]`. +See `.machine_readable/descriptiles/AGENTIC.a2ml` `[exceptions.boj-only-mcp]`. Scope: this repo only. Sunset: when BoJ cartridge supports the double-loop feedback protocol. diff --git a/MAINTAINERS.adoc b/MAINTAINERS.adoc index cd09462..022fa9e 100644 --- a/MAINTAINERS.adoc +++ b/MAINTAINERS.adoc @@ -36,7 +36,7 @@ Maintainers are responsible for: link:CODE_OF_CONDUCT.md[Code of Conduct] * Coordinating with sibling bots in the https://github.com/hyperpolymath/gitbot-fleet[gitbot-fleet] -* Keeping `.machine_readable/6a2/STATE.a2ml` in sync with reality +* Keeping `.machine_readable/descriptiles/STATE.a2ml` in sync with reality == Becoming a Maintainer diff --git a/ROADMAP.adoc b/ROADMAP.adoc index 05d0181..411dc29 100644 --- a/ROADMAP.adoc +++ b/ROADMAP.adoc @@ -17,7 +17,7 @@ toc::[] NOTE: Older sections of this document (and the README) cite 129 tests. That figure was pre-Package-7b; the canonical current count is 184 per -`.machine_readable/6a2/STATE.a2ml` (last-updated `2026-04-26`). Treat 184 +`.machine_readable/descriptiles/STATE.a2ml` (last-updated `2026-04-26`). Treat 184 as authoritative. == Completed (v0.1.0 -- February 2026) diff --git a/RSR_COMPLIANCE.adoc b/RSR_COMPLIANCE.adoc index 8d723c8..6362cbd 100644 --- a/RSR_COMPLIANCE.adoc +++ b/RSR_COMPLIANCE.adoc @@ -74,7 +74,7 @@ packaging primary going forward. |`CODE_OF_CONDUCT.md` present |✓ | |`CONTRIBUTING.md` present |✓ | |`SECURITY.md` present |✓ | -|`.machine_readable/6a2/` present |✓ |TOML-flavoured A2ML (migrated from Guile in `bdbc5a4`) +|`.machine_readable/descriptiles/` present |✓ |TOML-flavoured A2ML (migrated from Guile in `bdbc5a4`) |`0-AI-MANIFEST.a2ml` present |✓ |AI first-read pointer |`.claude/CLAUDE.md` present |✓ |AI agent operating rules |Conventional commits enforced |✓ |CI lints via `standards/changelog-reusable.yml` @@ -107,7 +107,7 @@ packaging primary going forward. |Exemption |Justification |`echidnabot-mcp` outside BoJ -|Documented carve-out in `.machine_readable/6a2/AGENTIC.a2ml` +|Documented carve-out in `.machine_readable/descriptiles/AGENTIC.a2ml` `[exceptions.boj-only-mcp]`. Sunset: when BoJ cartridge supports the double-loop feedback protocol. diff --git a/RSR_OUTLINE.adoc b/RSR_OUTLINE.adoc index 8265d65..03b474c 100644 --- a/RSR_OUTLINE.adoc +++ b/RSR_OUTLINE.adoc @@ -175,13 +175,13 @@ Per the estate boundary convention: |`RSR_OUTLINE.adoc` |✓ |This file |`0-AI-MANIFEST.a2ml` |✓ |AI first-read pointer |`.claude/CLAUDE.md` |✓ |AI agent operating rules -|`.machine_readable/6a2/` |✓ |A2ML descriptors +|`.machine_readable/descriptiles/` |✓ |A2ML descriptors |=== == STATE.a2ml Shape The canonical project-state file -(`.machine_readable/6a2/STATE.a2ml`) carries: +(`.machine_readable/descriptiles/STATE.a2ml`) carries: * `[metadata]` — project / version / last-updated / session * `[project-context]` — purpose / completion-percentage diff --git a/docs/content/api.adoc b/docs/content/api.adoc index 29838f5..0aff6c7 100644 --- a/docs/content/api.adoc +++ b/docs/content/api.adoc @@ -110,13 +110,13 @@ Register a new repository for proof verification. [source,graphql] ---- -mutation { +mutation($webhookSecret: String!) { registerRepository(input: { platform: GITHUB owner: "org" name: "repo" enabledProvers: [COQ, LEAN4] - webhookSecret: "optional-secret" + webhookSecret: $webhookSecret }) { id webhookUrl diff --git a/echidnabot.example.toml b/echidnabot.example.toml index 65c8baa..8c416cc 100644 --- a/echidnabot.example.toml +++ b/echidnabot.example.toml @@ -41,13 +41,13 @@ queue_size = 100 # token = "ghp_xxxxxxxxxxxxxxxxxxxx" # # # Webhook secret for signature verification -# webhook_secret = "your-webhook-secret" +# webhook_secret = "" # Supply your webhook signing secret locally. # GitLab Integration (optional) # [gitlab] # url = "https://gitlab.com" -# token = "glpat-xxxxxxxxxxxxxxxxxxxx" -# webhook_secret = "your-webhook-secret" +# token = "" # Supply a token from your credential store locally. +# webhook_secret = "" # Supply your webhook signing secret locally. # Bot operating mode (daemon-wide default) # Per-repo directives (.machine_readable/bot_directives/echidnabot.a2ml) and diff --git a/echidnabot.toml b/echidnabot.toml index 3d3e6a3..d310d1f 100644 --- a/echidnabot.toml +++ b/echidnabot.toml @@ -41,13 +41,13 @@ queue_size = 100 # token = "ghp_xxxxxxxxxxxxxxxxxxxx" # # # Webhook secret for signature verification -# webhook_secret = "your-webhook-secret" +# webhook_secret = "" # Supply your webhook signing secret locally. # GitLab Integration (optional) # [gitlab] # url = "https://gitlab.com" -# token = "glpat-xxxxxxxxxxxxxxxxxxxx" -# webhook_secret = "your-webhook-secret" +# token = "" # Supply a token from your credential store locally. +# webhook_secret = "" # Supply your webhook signing secret locally. # Environment variables can override any setting: # ECHIDNABOT__SERVER__PORT=9090 diff --git a/proofs/coq/admitted_stub.v b/proofs/coq/admitted_stub.v index 23b1598..468fc60 100644 --- a/proofs/coq/admitted_stub.v +++ b/proofs/coq/admitted_stub.v @@ -1,4 +1,5 @@ (* SPDX-License-Identifier: MPL-2.0 *) +(* hypatia: allow code_safety/admitted -- Deliberate negative fixture checked by protocol_contract::checked_in_negative_proofs_are_detected. *) (* Dogfood fixture: deliberately admitted Coq proof. coqc exits 0 with an admit warning; echidnabot's axiom scanner (src/trust/axiom_tracker) detects the Admitted. token and — once Regulator mode is wired — will diff --git a/proofs/lean/sorry_stub.lean b/proofs/lean/sorry_stub.lean index b4f9e87..498616a 100644 --- a/proofs/lean/sorry_stub.lean +++ b/proofs/lean/sorry_stub.lean @@ -1,4 +1,5 @@ -- SPDX-License-Identifier: MPL-2.0 +-- hypatia: allow code_safety/sorry -- Deliberate negative fixture checked by protocol_contract::checked_in_negative_proofs_are_detected. -- Dogfood fixture: deliberately incomplete Lean4 proof using sorry. -- Lean compiles with a warning; Regulator-mode axiom scan will treat -- sorry as a blocking violation once wired. diff --git a/src/config.rs b/src/config.rs index 8fb1f56..33d57fe 100644 --- a/src/config.rs +++ b/src/config.rs @@ -427,7 +427,7 @@ pub struct GitLabConfig { /// [codeberg] /// url = "https://forgejo.example.org" /// token = "..." -/// webhook_secret = "..." +/// # Set webhook_secret from your credential store locally. /// ``` /// /// `token` is optional — read-only endpoints (file fetch, default diff --git a/src/dispatcher/echidna_client.rs b/src/dispatcher/echidna_client.rs index 414c395..096626e 100644 --- a/src/dispatcher/echidna_client.rs +++ b/src/dispatcher/echidna_client.rs @@ -534,7 +534,8 @@ fn prover_to_echidna_name(prover: &ProverKind) -> String { "isabelle" => "Isabelle", "hol-light" => "HOLLight", _ => prover.display_name(), - }.to_string() + } + .to_string() } // ============================================================================= diff --git a/src/main.rs b/src/main.rs index a263d64..cc27aa4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1383,8 +1383,11 @@ async fn clone_repo(config: &Config, repo: &RepoId, commit: &str) -> Result { - let base_url = config.codeberg.as_ref() - .map(|c| c.url.as_str()).unwrap_or("https://codeberg.org"); + let base_url = config + .codeberg + .as_ref() + .map(|c| c.url.as_str()) + .unwrap_or("https://codeberg.org"); clone_repo_via_git(base_url, repo, commit).await } } @@ -1404,7 +1407,10 @@ async fn clone_repo_via_git(base_url: &str, repo: &RepoId, commit: &str) -> Resu .status() .await?; if !clone.success() { - return Err(echidnabot::Error::Internal(format!("Failed to clone {}", repo.full_name()))); + return Err(echidnabot::Error::Internal(format!( + "Failed to clone {}", + repo.full_name() + ))); } let fetch = tokio::process::Command::new("git") .current_dir(temp_dir.path()) @@ -1412,7 +1418,10 @@ async fn clone_repo_via_git(base_url: &str, repo: &RepoId, commit: &str) -> Resu .status() .await?; if !fetch.success() { - return Err(echidnabot::Error::Internal(format!("Failed to fetch requested revision for {}", repo.full_name()))); + return Err(echidnabot::Error::Internal(format!( + "Failed to fetch requested revision for {}", + repo.full_name() + ))); } let checkout = tokio::process::Command::new("git") .current_dir(temp_dir.path()) @@ -1420,7 +1429,10 @@ async fn clone_repo_via_git(base_url: &str, repo: &RepoId, commit: &str) -> Resu .status() .await?; if !checkout.success() { - return Err(echidnabot::Error::Internal(format!("Failed to check out requested revision for {}", repo.full_name()))); + return Err(echidnabot::Error::Internal(format!( + "Failed to check out requested revision for {}", + repo.full_name() + ))); } Ok(temp_dir.keep()) } @@ -1434,8 +1446,13 @@ mod clone_contract_tests { let output = std::process::Command::new("git") .current_dir(repo) .args(args) - .output().unwrap(); - assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr)); + .output() + .unwrap(); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); String::from_utf8(output.stdout).unwrap().trim().to_string() } @@ -1445,8 +1462,18 @@ mod clone_contract_tests { let repo = forge.path().join("owner/proofs.git"); std::fs::create_dir_all(&repo).unwrap(); git(&repo, &["init", "--initial-branch=main"]); - let commit_args = ["-c", "user.name=Contract Test", "-c", "user.email=contract@example.invalid", - "-c", "commit.gpgsign=false", "commit", "--allow-empty", "-m", "fixture"]; + let commit_args = [ + "-c", + "user.name=Contract Test", + "-c", + "user.email=contract@example.invalid", + "-c", + "commit.gpgsign=false", + "commit", + "--allow-empty", + "-m", + "fixture", + ]; git(&repo, &commit_args); let first = git(&repo, &["rev-parse", "HEAD"]); git(&repo, &commit_args); @@ -1464,7 +1491,9 @@ mod clone_contract_tests { let actual = git(&cloned, &["rev-parse", "HEAD"]); std::fs::remove_dir_all(cloned).unwrap(); assert_eq!(actual, first); - assert!(clone_repo(&config, &id, "missing-contract-revision").await.is_err()); + assert!(clone_repo(&config, &id, "missing-contract-revision") + .await + .is_err()); let missing = RepoId::new(Platform::Codeberg, "owner", "missing"); assert!(clone_repo(&config, &missing, "HEAD").await.is_err()); } diff --git a/tests/protocol_contract.rs b/tests/protocol_contract.rs index 3d0787b..c61100b 100644 --- a/tests/protocol_contract.rs +++ b/tests/protocol_contract.rs @@ -5,10 +5,46 @@ use axum::{ Json, Router, }; use echidnabot::config::{EchidnaApiMode, EchidnaConfig}; -use echidnabot::dispatcher::{EchidnaClient, ProofStatus, ProverKind}; use echidnabot::dispatcher::echidna_client::ProverStatus; +use echidnabot::dispatcher::{EchidnaClient, ProofStatus, ProverKind}; use serde_json::{json, Value}; +#[test] +fn checked_in_negative_proofs_are_detected() { + use echidnabot::trust::axiom_tracker::{AxiomFlag, AxiomTracker}; + for (slug, source, expected) in [ + ( + "coq", + include_str!("../proofs/coq/admitted_stub.v"), + AxiomFlag::Admitted, + ), + ( + "lean4", + include_str!("../proofs/lean/sorry_stub.lean"), + AxiomFlag::Sorry, + ), + ] { + // Scan the declaration and body, excluding the explanatory header: + // removing the actual hole must fail this control even if its name + // remains in a comment. + let declaration = source + .lines() + .skip_while(|line| !line.starts_with("Theorem ") && !line.starts_with("theorem ")) + .collect::>() + .join("\n"); + assert!(!declaration.is_empty()); + let report = AxiomTracker::scan(&ProverKind::new(slug), &declaration); + assert!(report.has_unsound()); + assert!(report.flags.contains(&expected)); + } + for (slug, source) in [ + ("coq", include_str!("../proofs/coq/trivial_ok.v")), + ("lean4", include_str!("../proofs/lean/trivial_ok.lean")), + ] { + assert!(!AxiomTracker::scan(&ProverKind::new(slug), source).has_unsound()); + } +} + async fn endpoint(router: Router) -> (String, tokio::task::JoinHandle<()>) { let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); let url = format!("http://{}", listener.local_addr().unwrap()); From d0e7d0a0e58aec5a6a30a3f1fe373489ce2218cd Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Mon, 7 Sep 2026 03:17:34 +0100 Subject: [PATCH 03/10] fix(ci): preserve fuzzing failures and build docs with pinned Casket --- .github/workflows/casket-pages.yml | 10 +- .github/workflows/docs.yml | 96 +++++------------- .github/workflows/echidna-fuzz.yml | 61 ++++++----- .github/workflows/governance.yml | 2 +- .github/workflows/openssf-compliance.yml | 10 +- .github/workflows/workflow-linter.yml | 15 ++- src/main.rs | 124 +++++++++++------------ 7 files changed, 140 insertions(+), 178 deletions(-) diff --git a/.github/workflows/casket-pages.yml b/.github/workflows/casket-pages.yml index f050793..a478275 100644 --- a/.github/workflows/casket-pages.yml +++ b/.github/workflows/casket-pages.yml @@ -15,7 +15,7 @@ concurrency: jobs: build: runs-on: ubuntu-latest - timeout-minutes: 15 + timeout-minutes: 30 steps: - name: Checkout uses: actions/checkout@v7.0.1 @@ -23,7 +23,9 @@ jobs: uses: actions/checkout@v7.0.1 with: repository: hyperpolymath/casket-ssg + ref: cec3c20d80ea1dc93660b69a4e7b38aa49f2a56b path: .casket-ssg + persist-credentials: false - name: Setup GHCup uses: haskell-actions/setup@v2.12.0 with: @@ -39,7 +41,9 @@ jobs: key: ${{ runner.os }}-casket-${{ hashFiles('.casket-ssg/casket-ssg.cabal') }} - name: Build casket-ssg working-directory: .casket-ssg - run: cabal build + run: | + cabal update + cabal build - name: Build site run: | mkdir -p site _site @@ -81,7 +85,7 @@ jobs: name: github-pages url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest - timeout-minutes: 15 + timeout-minutes: 30 needs: build steps: - name: Deploy to GitHub Pages diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index e877ee7..39f8300 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -18,11 +18,12 @@ on: branches: [main] paths: - 'docs/**' + - '.github/workflows/docs.yml' workflow_dispatch: jobs: build: runs-on: ubuntu-latest - timeout-minutes: 15 + timeout-minutes: 30 steps: - name: Checkout repository uses: actions/checkout@v7.0.1 @@ -30,11 +31,13 @@ jobs: uses: actions/checkout@v7.0.1 with: repository: hyperpolymath/casket-ssg + ref: cec3c20d80ea1dc93660b69a4e7b38aa49f2a56b path: casket-ssg + persist-credentials: false - name: Set up GHC uses: haskell-actions/setup@v2.12.0 with: - ghc-version: '9.4' + ghc-version: '9.8.2' cabal-version: '3.10' - name: Cache Cabal uses: actions/cache@v6.1.0 @@ -49,79 +52,28 @@ jobs: - name: Build casket-ssg working-directory: casket-ssg run: | - ghc -O2 -o casket Casket.hs - - name: Build documentation - run: | - mkdir -p docs/_site - - # Process each markdown file with casket-ssg - for mdfile in docs/content/*.md; do - filename=$(basename "$mdfile" .md) - if [ "$filename" = "index" ]; then - outfile="docs/_site/index.html" - else - outfile="docs/_site/${filename}.html" - fi - - echo "Processing: $mdfile -> $outfile" - - # Use casket-ssg to convert markdown to HTML - # For now, use the test-full mode to validate the pipeline works - ./casket-ssg/casket test-full > /dev/null 2>&1 || true - - # Simple pandoc fallback if casket needs more setup - if command -v pandoc &> /dev/null; then - pandoc "$mdfile" \ - --template=docs/templates/default.html \ - --metadata title="$(grep -m1 '^title:' "$mdfile" | cut -d: -f2 | xargs)" \ - --metadata date="$(grep -m1 '^date:' "$mdfile" | cut -d: -f2 | xargs)" \ - -o "$outfile" 2>/dev/null || \ - pandoc "$mdfile" -s -o "$outfile" - else - # Minimal HTML generation if pandoc unavailable - echo "" > "$outfile" - cat "$mdfile" >> "$outfile" - echo "" >> "$outfile" - fi - done - - # Copy static assets - cp -r docs/templates/*.css docs/_site/ 2>/dev/null || true - - # Create .nojekyll for GitHub Pages - touch docs/_site/.nojekyll - - name: Install Pandoc (for enhanced builds) + cabal update + cabal build + - name: Install AsciiDoc conversion tools run: | sudo apt-get update - sudo apt-get install -y pandoc - - name: Rebuild with Pandoc + sudo apt-get install -y --no-install-recommends asciidoctor pandoc + - name: Convert documentation sources for Casket run: | - for mdfile in docs/content/*.md; do - filename=$(basename "$mdfile" .md) - if [ "$filename" = "index" ]; then - outfile="docs/_site/index.html" - else - outfile="docs/_site/${filename}.html" - fi - - # Extract frontmatter - title=$(sed -n '/^---$/,/^---$/p' "$mdfile" | grep '^title:' | cut -d: -f2- | xargs) - date=$(sed -n '/^---$/,/^---$/p' "$mdfile" | grep '^date:' | cut -d: -f2- | xargs) - - # Remove frontmatter for processing - content_start=$(grep -n '^---$' "$mdfile" | tail -1 | cut -d: -f1) - tail -n +$((content_start + 1)) "$mdfile" > /tmp/content.md - - pandoc /tmp/content.md \ - --standalone \ - --template=docs/templates/default.html \ - --metadata title="$title" \ - --metadata date="$date" \ - --highlight-style=kate \ - -o "$outfile" - - echo "Built: $outfile" + set -euo pipefail + mkdir -p .docs-source docs/_site + # Casket accepts Markdown. Preserve AsciiDoc semantics through DocBook. + for source in docs/content/*.adoc; do + name=$(basename "$source" .adoc) + asciidoctor -b docbook5 -o "${RUNNER_TEMP}/${name}.xml" "$source" + pandoc -f docbook -t markdown "${RUNNER_TEMP}/${name}.xml" -o ".docs-source/${name}.md" done + - name: Build documentation with Casket + working-directory: casket-ssg + run: | + cabal run casket-ssg -- build ../.docs-source ../docs/_site + test -s ../docs/_site/index.html + touch ../docs/_site/.nojekyll - name: Upload artifact uses: actions/upload-pages-artifact@v5.0.0 with: @@ -132,7 +84,7 @@ jobs: name: github-pages url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest - timeout-minutes: 15 + timeout-minutes: 30 needs: build steps: - name: Deploy to GitHub Pages diff --git a/.github/workflows/echidna-fuzz.yml b/.github/workflows/echidna-fuzz.yml index d47415f..4d1d907 100644 --- a/.github/workflows/echidna-fuzz.yml +++ b/.github/workflows/echidna-fuzz.yml @@ -1,6 +1,5 @@ # SPDX-License-Identifier: MPL-2.0 # This workflow is managed by gh actions-lock. -# SPDX-License-Identifier: MPL-2.0 name: Echidna Smart Contract Fuzzing permissions: contents: read @@ -133,7 +132,8 @@ jobs: echo "has_contracts=$([[ $contract_count -gt 0 ]] && echo true || echo false)" >> $GITHUB_OUTPUT echo "has_echidna_tests=$([[ -n "$echidna_tests" ]] && echo true || echo false)" >> $GITHUB_OUTPUT - echo "contracts=$echidna_tests" >> $GITHUB_OUTPUT + contracts_json=$(printf '%s' "$echidna_tests" | jq -Rc 'split(",") | map(select(length > 0))') + echo "contracts=$contracts_json" >> "$GITHUB_OUTPUT" echo "contract_count=$test_count" >> $GITHUB_OUTPUT echo "::notice::Found $contract_count Solidity files, Echidna tests: $echidna_tests (limited to ${{ env.MAX_PARALLEL_CONTRACTS }} max)" @@ -150,7 +150,7 @@ jobs: fail-fast: false max-parallel: 2 # Rate limit: max 2 concurrent test jobs matrix: - contract: ${{ fromJson(format('["{0}"]', needs.detect-contracts.outputs.contracts)) }} + contract: ${{ fromJson(needs.detect-contracts.outputs.contracts) }} steps: - uses: actions/checkout@v7.0.1 - name: Restore Echidna corpus cache @@ -160,18 +160,20 @@ jobs: key: echidna-corpus-${{ matrix.contract }}-${{ hashFiles(format('contracts/{0}.sol', matrix.contract)) }} restore-keys: | echidna-corpus-${{ matrix.contract }}- - - name: Install solc - run: | - wget -q "https://github.com/ethereum/solidity/releases/download/v${{ env.SOLC_VERSION }}/solc-static-linux" - chmod +x solc-static-linux - sudo mv solc-static-linux /usr/local/bin/solc - solc --version - - name: Install Echidna + - name: Install pinned Solidity and Echidna binaries run: | - wget -q "https://github.com/crytic/echidna/releases/download/v${{ env.ECHIDNA_VERSION }}/echidna-${{ env.ECHIDNA_VERSION }}-x86_64-linux.tar.gz" - tar -xzf "echidna-${{ env.ECHIDNA_VERSION }}-x86_64-linux.tar.gz" - sudo mv echidna /usr/local/bin/ - echidna --version + set -euo pipefail + install_dir="${RUNNER_TEMP}/solidity-fuzz-tools" + mkdir -p "$install_dir" + curl -fL --retry 3 -o "$install_dir/solc" "https://github.com/ethereum/solidity/releases/download/v${SOLC_VERSION}/solc-static-linux" + echo "7a5c1d3dc9a8eba62bb2ec37192c9178ae5fe8a54a56e5573fd3c9c17cd9eb48 $install_dir/solc" | sha256sum --check + curl -fL --retry 3 -o "$install_dir/echidna.tar.gz" "https://github.com/crytic/echidna/releases/download/v${ECHIDNA_VERSION}/echidna-${ECHIDNA_VERSION}-x86_64-linux.tar.gz" + echo "d33c0451bb3bb65c76445d2dbfcd1102dab7a27a8d450197dabd3f06bed76067 $install_dir/echidna.tar.gz" | sha256sum --check + tar -xzf "$install_dir/echidna.tar.gz" -C "$install_dir" echidna + chmod +x "$install_dir/solc" "$install_dir/echidna" + echo "$install_dir" >> "$GITHUB_PATH" + "$install_dir/solc" --version + "$install_dir/echidna" --version - name: Run Echidna property tests id: echidna run: | @@ -222,8 +224,10 @@ jobs: PASS_CMD="$ECHIDNA_CMD" fi - timeout ${{ env.TEST_TIMEOUT }} $PASS_CMD 2>&1 | tee "echidna-results/pass-$i.json" || true + set +e + timeout ${{ env.TEST_TIMEOUT }} $PASS_CMD 2>&1 | tee "echidna-results/pass-$i.json" PASS_EXIT=${PIPESTATUS[0]} + set -e if [ $PASS_EXIT -eq 0 ]; then PASS_COUNT=$((PASS_COUNT + 1)) @@ -298,7 +302,7 @@ jobs: fail-fast: false max-parallel: 2 matrix: - contract: ${{ fromJson(format('["{0}"]', needs.detect-contracts.outputs.contracts)) }} + contract: ${{ fromJson(needs.detect-contracts.outputs.contracts) }} steps: - uses: actions/checkout@v7.0.1 - name: Restore Echidna corpus cache @@ -308,16 +312,20 @@ jobs: key: echidna-corpus-assertion-${{ matrix.contract }}-${{ hashFiles(format('contracts/{0}.sol', matrix.contract)) }} restore-keys: | echidna-corpus-assertion-${{ matrix.contract }}- - - name: Install solc - run: | - wget -q "https://github.com/ethereum/solidity/releases/download/v${{ env.SOLC_VERSION }}/solc-static-linux" - chmod +x solc-static-linux - sudo mv solc-static-linux /usr/local/bin/solc - - name: Install Echidna + - name: Install pinned Solidity and Echidna binaries run: | - wget -q "https://github.com/crytic/echidna/releases/download/v${{ env.ECHIDNA_VERSION }}/echidna-${{ env.ECHIDNA_VERSION }}-x86_64-linux.tar.gz" - tar -xzf "echidna-${{ env.ECHIDNA_VERSION }}-x86_64-linux.tar.gz" - sudo mv echidna /usr/local/bin/ + set -euo pipefail + install_dir="${RUNNER_TEMP}/solidity-fuzz-tools" + mkdir -p "$install_dir" + curl -fL --retry 3 -o "$install_dir/solc" "https://github.com/ethereum/solidity/releases/download/v${SOLC_VERSION}/solc-static-linux" + echo "7a5c1d3dc9a8eba62bb2ec37192c9178ae5fe8a54a56e5573fd3c9c17cd9eb48 $install_dir/solc" | sha256sum --check + curl -fL --retry 3 -o "$install_dir/echidna.tar.gz" "https://github.com/crytic/echidna/releases/download/v${ECHIDNA_VERSION}/echidna-${ECHIDNA_VERSION}-x86_64-linux.tar.gz" + echo "d33c0451bb3bb65c76445d2dbfcd1102dab7a27a8d450197dabd3f06bed76067 $install_dir/echidna.tar.gz" | sha256sum --check + tar -xzf "$install_dir/echidna.tar.gz" -C "$install_dir" echidna + chmod +x "$install_dir/solc" "$install_dir/echidna" + echo "$install_dir" >> "$GITHUB_PATH" + "$install_dir/solc" --version + "$install_dir/echidna" --version - name: Run Echidna assertion tests id: echidna run: | @@ -352,7 +360,8 @@ jobs: elif [ $ECHIDNA_EXIT -eq 0 ]; then echo "::notice::All assertion tests passed!" else - echo "::warning::Echidna found assertion violations" + echo "::error::Echidna found assertion violations" + exit "$ECHIDNA_EXIT" fi - name: Upload Echidna results if: always() diff --git a/.github/workflows/governance.yml b/.github/workflows/governance.yml index be3d7df..415259b 100644 --- a/.github/workflows/governance.yml +++ b/.github/workflows/governance.yml @@ -14,4 +14,4 @@ permissions: jobs: governance: - uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@81dbf2dd854b1444fd6236fa2352474383b2c2b9 + uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@571cc734cd69fb846032ec77a662aa8ee4fc32cd diff --git a/.github/workflows/openssf-compliance.yml b/.github/workflows/openssf-compliance.yml index eaf9981..2cdf5f2 100644 --- a/.github/workflows/openssf-compliance.yml +++ b/.github/workflows/openssf-compliance.yml @@ -67,11 +67,11 @@ jobs: exit 1 fi - if [ ! -f ".machine_readable/STATE.a2ml" ]; then - echo "::error::.machine_readable/STATE.a2ml is required" + if [ ! -f ".machine_readable/descriptiles/STATE.a2ml" ]; then + echo "::error::.machine_readable/descriptiles/STATE.a2ml is required" exit 1 fi - echo ".machine_readable/STATE.a2ml: OK" + echo ".machine_readable/descriptiles/STATE.a2ml: OK" - name: Check CHANGELOG exists run: | if [ ! -f "CHANGELOG.md" ] && [ ! -f "CHANGELOG.adoc" ] && [ ! -f "CHANGES.md" ]; then @@ -87,8 +87,8 @@ jobs: # Collect all required files that exist for f in SECURITY.md SECURITY.adoc .github/SECURITY.md LICENSE LICENSE.txt \ CONTRIBUTING.md CONTRIBUTING.adoc README.md README.adoc \ - .machine_readable/STATE.a2ml .machine_readable/META.a2ml \ - .machine_readable/ECOSYSTEM.a2ml CHANGELOG.md CHANGELOG.adoc; do + .machine_readable/descriptiles/STATE.a2ml .machine_readable/descriptiles/META.a2ml \ + .machine_readable/descriptiles/ECOSYSTEM.a2ml CHANGELOG.md CHANGELOG.adoc; do [ -f "$f" ] && REQUIRED_FILES="$REQUIRED_FILES $f" done diff --git a/.github/workflows/workflow-linter.yml b/.github/workflows/workflow-linter.yml index 0daf533..5171312 100644 --- a/.github/workflows/workflow-linter.yml +++ b/.github/workflows/workflow-linter.yml @@ -38,13 +38,10 @@ jobs: fi done exit $errors - - name: Check pinned actions + - name: Verify immutable action lockfile with the authoritative tool + env: + GH_TOKEN: ${{ github.token }} run: | - errors=0 - for f in .github/workflows/*.yml .github/workflows/*.yaml; do - [ -f "$f" ] || continue - # Look for uses: without SHA - if grep -E "uses:.*@v[0-9]" "$f" | grep -v "#"; then - echo "WARNING: $f has unpinned actions (missing SHA comment)" - fi - done + gh extension install github/gh-actions-lock --pin v0.1.6 + gh actions-lock --rescan --no-fix + diff --git a/src/main.rs b/src/main.rs index cc27aa4..3697435 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1437,68 +1437,6 @@ async fn clone_repo_via_git(base_url: &str, repo: &RepoId, commit: &str) -> Resu Ok(temp_dir.keep()) } -#[cfg(test)] -mod clone_contract_tests { - use super::*; - use echidnabot::config::CodebergConfig; - - fn git(repo: &Path, args: &[&str]) -> String { - let output = std::process::Command::new("git") - .current_dir(repo) - .args(args) - .output() - .unwrap(); - assert!( - output.status.success(), - "{}", - String::from_utf8_lossy(&output.stderr) - ); - String::from_utf8(output.stdout).unwrap().trim().to_string() - } - - #[tokio::test] - async fn configured_forge_clones_exact_revision_and_rejects_missing_revision() { - let forge = tempfile::tempdir().unwrap(); - let repo = forge.path().join("owner/proofs.git"); - std::fs::create_dir_all(&repo).unwrap(); - git(&repo, &["init", "--initial-branch=main"]); - let commit_args = [ - "-c", - "user.name=Contract Test", - "-c", - "user.email=contract@example.invalid", - "-c", - "commit.gpgsign=false", - "commit", - "--allow-empty", - "-m", - "fixture", - ]; - git(&repo, &commit_args); - let first = git(&repo, &["rev-parse", "HEAD"]); - git(&repo, &commit_args); - assert_ne!(first, git(&repo, &["rev-parse", "HEAD"])); - let config = Config { - codeberg: Some(CodebergConfig { - url: format!("file://{}", forge.path().display()), - token: None, - webhook_secret: None, - }), - ..Default::default() - }; - let id = RepoId::new(Platform::Codeberg, "owner", "proofs"); - let cloned = clone_repo(&config, &id, &first).await.unwrap(); - let actual = git(&cloned, &["rev-parse", "HEAD"]); - std::fs::remove_dir_all(cloned).unwrap(); - assert_eq!(actual, first); - assert!(clone_repo(&config, &id, "missing-contract-revision") - .await - .is_err()); - let missing = RepoId::new(Platform::Codeberg, "owner", "missing"); - assert!(clone_repo(&config, &missing, "HEAD").await.is_err()); - } -} - const MAX_PROOF_FILES: usize = 10_000; fn collect_files_by_extension(root: &Path, extensions: &[String]) -> Vec { @@ -1572,3 +1510,65 @@ fn collect_files_inner(root: &Path, extensions: &[String], results: &mut Vec String { + let output = std::process::Command::new("git") + .current_dir(repo) + .args(args) + .output() + .unwrap(); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + String::from_utf8(output.stdout).unwrap().trim().to_string() + } + + #[tokio::test] + async fn configured_forge_clones_exact_revision_and_rejects_missing_revision() { + let forge = tempfile::tempdir().unwrap(); + let repo = forge.path().join("owner/proofs.git"); + std::fs::create_dir_all(&repo).unwrap(); + git(&repo, &["init", "--initial-branch=main"]); + let commit_args = [ + "-c", + "user.name=Contract Test", + "-c", + "user.email=contract@example.invalid", + "-c", + "commit.gpgsign=false", + "commit", + "--allow-empty", + "-m", + "fixture", + ]; + git(&repo, &commit_args); + let first = git(&repo, &["rev-parse", "HEAD"]); + git(&repo, &commit_args); + assert_ne!(first, git(&repo, &["rev-parse", "HEAD"])); + let config = Config { + codeberg: Some(CodebergConfig { + url: format!("file://{}", forge.path().display()), + token: None, + webhook_secret: None, + }), + ..Default::default() + }; + let id = RepoId::new(Platform::Codeberg, "owner", "proofs"); + let cloned = clone_repo(&config, &id, &first).await.unwrap(); + let actual = git(&cloned, &["rev-parse", "HEAD"]); + std::fs::remove_dir_all(cloned).unwrap(); + assert_eq!(actual, first); + assert!(clone_repo(&config, &id, "missing-contract-revision") + .await + .is_err()); + let missing = RepoId::new(Platform::Codeberg, "owner", "missing"); + assert!(clone_repo(&config, &missing, "HEAD").await.is_err()); + } +} From a0ffbb889322d45cb058c486e1779e9799a1a8ee Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Mon, 7 Sep 2026 03:27:36 +0100 Subject: [PATCH 04/10] fix(ci): isolate timeout environment tests and install crytic compiler adapter --- .github/workflows/actions.lock | 1 + .github/workflows/db-checks.yml | 2 +- .github/workflows/e2e.yml | 2 +- .github/workflows/echidna-fuzz.yml | 10 +++++++ .github/workflows/proof-safety.yml | 2 +- .github/workflows/publish.yml | 2 +- tests/lifecycle.rs | 43 +++++++++++++++++++----------- 7 files changed, 43 insertions(+), 19 deletions(-) diff --git a/.github/workflows/actions.lock b/.github/workflows/actions.lock index 9d4af02..36c488a 100644 --- a/.github/workflows/actions.lock +++ b/.github/workflows/actions.lock @@ -29,6 +29,7 @@ workflows: '.github/workflows/db-checks.yml': - 'actions/checkout@v7.0.1' - 'dtolnay/rust-toolchain@master' + - 'swatinem/rust-cache@v2.9.2' '.github/workflows/dependabot-automerge.yml': - 'dependabot/fetch-metadata@v3.1.0' '.github/workflows/docs.yml': diff --git a/.github/workflows/db-checks.yml b/.github/workflows/db-checks.yml index 417707c..f3aa666 100644 --- a/.github/workflows/db-checks.yml +++ b/.github/workflows/db-checks.yml @@ -48,7 +48,7 @@ jobs: toolchain: stable - name: Cache cargo - uses: Swatinem/rust-cache@v2.9.2 + uses: swatinem/rust-cache@v2.9.2 - name: Install sqlite3 CLI run: sudo apt-get update && sudo apt-get install -y sqlite3 diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index b52f90b..b717f87 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -61,7 +61,7 @@ jobs: cargo metadata --format-version 1 >/dev/null echo "✅ dependency graph resolves from a standalone checkout" - - uses: Swatinem/rust-cache@v2.9.2 + - uses: swatinem/rust-cache@v2.9.2 - name: Unit + Point-to-Point + End-to-End run: | diff --git a/.github/workflows/echidna-fuzz.yml b/.github/workflows/echidna-fuzz.yml index 4d1d907..d248858 100644 --- a/.github/workflows/echidna-fuzz.yml +++ b/.github/workflows/echidna-fuzz.yml @@ -174,6 +174,11 @@ jobs: echo "$install_dir" >> "$GITHUB_PATH" "$install_dir/solc" --version "$install_dir/echidna" --version + - name: Install pinned Solidity compiler adapter + run: | + python3 -m venv "${RUNNER_TEMP}/crytic-compile" + "${RUNNER_TEMP}/crytic-compile/bin/pip" install 'crytic-compile==0.4.2' + echo "${RUNNER_TEMP}/crytic-compile/bin" >> "$GITHUB_PATH" - name: Run Echidna property tests id: echidna run: | @@ -326,6 +331,11 @@ jobs: echo "$install_dir" >> "$GITHUB_PATH" "$install_dir/solc" --version "$install_dir/echidna" --version + - name: Install pinned Solidity compiler adapter + run: | + python3 -m venv "${RUNNER_TEMP}/crytic-compile" + "${RUNNER_TEMP}/crytic-compile/bin/pip" install 'crytic-compile==0.4.2' + echo "${RUNNER_TEMP}/crytic-compile/bin" >> "$GITHUB_PATH" - name: Run Echidna assertion tests id: echidna run: | diff --git a/.github/workflows/proof-safety.yml b/.github/workflows/proof-safety.yml index e25a26e..82bece7 100644 --- a/.github/workflows/proof-safety.yml +++ b/.github/workflows/proof-safety.yml @@ -19,7 +19,7 @@ jobs: - uses: actions/checkout@v7.0.1 with: persist-credentials: false - - uses: Swatinem/rust-cache@v2.9.2 + - uses: swatinem/rust-cache@v2.9.2 - name: Test REST and GraphQL wire contracts run: | cargo test --locked --test protocol_contract diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 1ef47aa..5dac02b 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -38,7 +38,7 @@ jobs: with: toolchain: stable - name: Cache cargo - uses: Swatinem/rust-cache@v2.9.2 + uses: swatinem/rust-cache@v2.9.2 - name: Install dependencies run: | sudo apt-get update diff --git a/tests/lifecycle.rs b/tests/lifecycle.rs index b99c399..23f2089 100644 --- a/tests/lifecycle.rs +++ b/tests/lifecycle.rs @@ -326,21 +326,34 @@ async fn lifecycle_store_close_is_idempotent() { // Graceful-shutdown lifecycle // ────────────────────────────────────────────────────────────────────────────── -#[tokio::test] -async fn lifecycle_shutdown_default_timeout_is_30s() { - assert_eq!(DEFAULT_SHUTDOWN_TIMEOUT_SECS, 30); - let t = resolve_shutdown_timeout(DEFAULT_SHUTDOWN_TIMEOUT_SECS); - assert_eq!(t, Duration::from_secs(30)); -} - -#[tokio::test] -async fn lifecycle_shutdown_env_var_overrides_config() { - // Use a unique value (47) that nothing else in the test set uses, - // so we can confirm the env source rather than coincidental default. - std::env::set_var(ENV_SHUTDOWN_TIMEOUT, "47"); - let t = resolve_shutdown_timeout(30); - assert_eq!(t, Duration::from_secs(47)); - std::env::remove_var(ENV_SHUTDOWN_TIMEOUT); +// Exercise environment overrides in child processes so concurrent tests never +// share process-global environment mutations. +#[test] +fn lifecycle_shutdown_timeout_environment_contract() { + const CHILD: &str = "ECHIDNABOT_TIMEOUT_CONTRACT_CHILD"; + if let Ok(expected) = std::env::var(CHILD) { + assert_eq!(DEFAULT_SHUTDOWN_TIMEOUT_SECS, 30); + assert_eq!( + resolve_shutdown_timeout(30), + Duration::from_secs(expected.parse().unwrap()) + ); + return; + } + for (value, expected) in [(None, "30"), (Some("47"), "47"), (Some("invalid"), "30")] { + let mut command = std::process::Command::new(std::env::current_exe().unwrap()); + command.args([ + "--exact", + "lifecycle_shutdown_timeout_environment_contract", + "--nocapture", + ]); + command + .env(CHILD, expected) + .env_remove(ENV_SHUTDOWN_TIMEOUT); + if let Some(value) = value { + command.env(ENV_SHUTDOWN_TIMEOUT, value); + } + assert!(command.status().unwrap().success()); + } } #[tokio::test] From 903ea87cb12c2f8c3e66f65913ad934a27b97dbc Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Mon, 7 Sep 2026 03:35:04 +0100 Subject: [PATCH 05/10] fix(test): exercise valid Solidity invariants and assertion mode --- .github/workflows/db-checks.yml | 2 +- .github/workflows/e2e.yml | 2 +- .github/workflows/proof-safety.yml | 2 +- .github/workflows/publish.yml | 2 +- contracts/TokenEchidnaTest.sol | 20 ++++++++++---------- echidna/echidna-assertion.yaml | 7 ++++--- echidna/echidna-ci.yaml | 2 +- echidna/echidna-config.yaml | 2 +- echidna/echidna-no-flaky-assertion.yaml | 4 ++-- echidna/echidna-no-flaky.yaml | 2 +- 10 files changed, 23 insertions(+), 22 deletions(-) diff --git a/.github/workflows/db-checks.yml b/.github/workflows/db-checks.yml index f3aa666..417707c 100644 --- a/.github/workflows/db-checks.yml +++ b/.github/workflows/db-checks.yml @@ -48,7 +48,7 @@ jobs: toolchain: stable - name: Cache cargo - uses: swatinem/rust-cache@v2.9.2 + uses: Swatinem/rust-cache@v2.9.2 - name: Install sqlite3 CLI run: sudo apt-get update && sudo apt-get install -y sqlite3 diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index b717f87..b52f90b 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -61,7 +61,7 @@ jobs: cargo metadata --format-version 1 >/dev/null echo "✅ dependency graph resolves from a standalone checkout" - - uses: swatinem/rust-cache@v2.9.2 + - uses: Swatinem/rust-cache@v2.9.2 - name: Unit + Point-to-Point + End-to-End run: | diff --git a/.github/workflows/proof-safety.yml b/.github/workflows/proof-safety.yml index 82bece7..e25a26e 100644 --- a/.github/workflows/proof-safety.yml +++ b/.github/workflows/proof-safety.yml @@ -19,7 +19,7 @@ jobs: - uses: actions/checkout@v7.0.1 with: persist-credentials: false - - uses: swatinem/rust-cache@v2.9.2 + - uses: Swatinem/rust-cache@v2.9.2 - name: Test REST and GraphQL wire contracts run: | cargo test --locked --test protocol_contract diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 5dac02b..1ef47aa 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -38,7 +38,7 @@ jobs: with: toolchain: stable - name: Cache cargo - uses: swatinem/rust-cache@v2.9.2 + uses: Swatinem/rust-cache@v2.9.2 - name: Install dependencies run: | sudo apt-get update diff --git a/contracts/TokenEchidnaTest.sol b/contracts/TokenEchidnaTest.sol index 1076b25..e15448d 100644 --- a/contracts/TokenEchidnaTest.sol +++ b/contracts/TokenEchidnaTest.sol @@ -40,9 +40,12 @@ contract TokenEchidnaTest is Token { /// @notice Transfer should not create tokens (conservation of value) /// @dev Sum of balances should remain constant after transfers - function echidna_transfer_preserves_total() public view returns (bool) { - // This is a simplified check - in production you'd track all addresses - return totalSupply >= 0; + function echidna_transfer_preserves_total() public returns (bool) { + if (paused) return true; + uint256 supplyBefore = totalSupply; + address recipient = msg.sender == ECHIDNA_RECEIVER ? ECHIDNA_SENDER : ECHIDNA_RECEIVER; + transfer(recipient, balanceOf[msg.sender] / 2); + return totalSupply == supplyBefore; } /// @notice Zero address should never have a balance @@ -56,11 +59,8 @@ contract TokenEchidnaTest is Token { function echidna_self_transfer_neutral() public returns (bool) { uint256 balanceBefore = balanceOf[msg.sender]; if (balanceBefore > 0 && !paused) { - try this.transfer(msg.sender, balanceBefore / 2) { - return balanceOf[msg.sender] == balanceBefore; - } catch { - return true; // Failed transfer is acceptable - } + transfer(msg.sender, balanceBefore / 2); + return balanceOf[msg.sender] == balanceBefore; } return true; } @@ -129,14 +129,14 @@ contract TokenEchidnaTest is Token { // ========== HELPER FUNCTIONS FOR ECHIDNA ========== /// @notice Allow Echidna to trigger pause (simulating owner action) - function echidna_try_pause() public { + function try_pause() public { if (msg.sender == owner) { pause(); } } /// @notice Allow Echidna to trigger unpause - function echidna_try_unpause() public { + function try_unpause() public { if (msg.sender == owner) { unpause(); } diff --git a/echidna/echidna-assertion.yaml b/echidna/echidna-assertion.yaml index 5853827..542e5e2 100644 --- a/echidna/echidna-assertion.yaml +++ b/echidna/echidna-assertion.yaml @@ -1,5 +1,5 @@ # SPDX-License-Identifier: MPL-2.0 -// Owner: Jonathan D.A. Jewell +# Owner: Jonathan D.A. Jewell # Echidna Assertion Mode Configuration # Tests functions with assert() statements @@ -23,8 +23,9 @@ sender: deployer: "0x30000" -# No prefix filter in assertion mode - tests all functions with assert() -prefix: "" +# Keep property names distinct from argument-taking transaction functions. +# Assertion mode still exercises assertions in all transaction functions. +prefix: "echidna_" # Enable coverage coverage: true diff --git a/echidna/echidna-ci.yaml b/echidna/echidna-ci.yaml index 8661489..9a2677e 100644 --- a/echidna/echidna-ci.yaml +++ b/echidna/echidna-ci.yaml @@ -1,5 +1,5 @@ # SPDX-License-Identifier: MPL-2.0 -// Owner: Jonathan D.A. Jewell +# Owner: Jonathan D.A. Jewell # Echidna CI Configuration - Faster settings for continuous integration # Property-based testing mode diff --git a/echidna/echidna-config.yaml b/echidna/echidna-config.yaml index 4590b5b..c0e4dc0 100644 --- a/echidna/echidna-config.yaml +++ b/echidna/echidna-config.yaml @@ -1,5 +1,5 @@ # SPDX-License-Identifier: MPL-2.0 -// Owner: Jonathan D.A. Jewell +# Owner: Jonathan D.A. Jewell # Echidna Configuration for Smart Contract Fuzzing # https://github.com/crytic/echidna diff --git a/echidna/echidna-no-flaky-assertion.yaml b/echidna/echidna-no-flaky-assertion.yaml index cd4dc4f..ab13343 100644 --- a/echidna/echidna-no-flaky-assertion.yaml +++ b/echidna/echidna-no-flaky-assertion.yaml @@ -1,5 +1,5 @@ # SPDX-License-Identifier: MPL-2.0 -// Owner: Jonathan D.A. Jewell +# Owner: Jonathan D.A. Jewell # Echidna No-Flaky Assertion Mode Configuration # Deterministic settings for stable assertion-based testing @@ -24,7 +24,7 @@ sender: deployer: "0x30000" # No prefix filter in assertion mode -prefix: "" +prefix: "echidna_" # Enable coverage coverage: true diff --git a/echidna/echidna-no-flaky.yaml b/echidna/echidna-no-flaky.yaml index 8f3d4e8..5988795 100644 --- a/echidna/echidna-no-flaky.yaml +++ b/echidna/echidna-no-flaky.yaml @@ -1,5 +1,5 @@ # SPDX-License-Identifier: MPL-2.0 -// Owner: Jonathan D.A. Jewell +# Owner: Jonathan D.A. Jewell # Echidna No-Flaky Mode Configuration # Deterministic settings for reproducible, stable test results # From 803a9c686f2e3dc989aa9ab6e94aade9b285630d Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Mon, 7 Sep 2026 03:39:48 +0100 Subject: [PATCH 06/10] fix(ci): prefer verified solc over the adapter selection shim --- .github/workflows/echidna-fuzz.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/echidna-fuzz.yml b/.github/workflows/echidna-fuzz.yml index d248858..a2eaf1d 100644 --- a/.github/workflows/echidna-fuzz.yml +++ b/.github/workflows/echidna-fuzz.yml @@ -182,6 +182,9 @@ jobs: - name: Run Echidna property tests id: echidna run: | + # The adapter installs a solc-select shim; prefer the checksum-pinned compiler. + export PATH="${RUNNER_TEMP}/solidity-fuzz-tools:$PATH" + test "$(command -v solc)" = "${RUNNER_TEMP}/solidity-fuzz-tools/solc" echo "::group::Echidna Property Testing - ${{ matrix.contract }}" CONTRACT_FILE="contracts/${{ matrix.contract }}.sol" @@ -277,7 +280,7 @@ jobs: echo "::notice::All property tests passed!" else echo "result=failure" >> $GITHUB_OUTPUT - echo "::error::Echidna found property violations" + echo "::error::Echidna run failed; inspect output for compiler errors or counterexamples" exit $ECHIDNA_EXIT fi fi @@ -339,6 +342,8 @@ jobs: - name: Run Echidna assertion tests id: echidna run: | + export PATH="${RUNNER_TEMP}/solidity-fuzz-tools:$PATH" + test "$(command -v solc)" = "${RUNNER_TEMP}/solidity-fuzz-tools/solc" echo "::group::Echidna Assertion Testing - ${{ matrix.contract }}" CONTRACT_FILE="contracts/${{ matrix.contract }}.sol" @@ -370,7 +375,7 @@ jobs: elif [ $ECHIDNA_EXIT -eq 0 ]; then echo "::notice::All assertion tests passed!" else - echo "::error::Echidna found assertion violations" + echo "::error::Echidna assertion run failed; inspect output for compiler errors or counterexamples" exit "$ECHIDNA_EXIT" fi - name: Upload Echidna results From c52e17a7bae27b044e16b821afbfffd0cb4b4b61 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Mon, 7 Sep 2026 04:13:42 +0100 Subject: [PATCH 07/10] Own cloned repositories through proof processing and resolve review findings --- .github/workflows/casket-pages.yml | 4 +- .github/workflows/docs.yml | 4 +- .github/workflows/e2e.yml | 2 + .github/workflows/echidnabot.yml | 10 +++ .github/workflows/workflow-linter.yml | 7 +- .machine_readable/contractiles/Justfile | 8 +-- .machine_readable/contractiles/Mustfile.a2ml | 8 +-- .machine_readable/descriptiles/STATE.a2ml | 2 +- EXPLAINME.adoc | 8 +-- docs/content/api.adoc | 7 +- echidnabot.example.toml | 2 +- echidnabot.toml | 2 +- src/adapters/bitbucket.rs | 72 +------------------ src/adapters/codeberg.rs | 75 +------------------ src/adapters/github.rs | 76 +------------------- src/adapters/gitlab.rs | 72 +------------------ src/adapters/mod.rs | 43 ++++++++++- src/main.rs | 56 +++++---------- 18 files changed, 104 insertions(+), 354 deletions(-) diff --git a/.github/workflows/casket-pages.yml b/.github/workflows/casket-pages.yml index a478275..0f33507 100644 --- a/.github/workflows/casket-pages.yml +++ b/.github/workflows/casket-pages.yml @@ -43,7 +43,7 @@ jobs: working-directory: .casket-ssg run: | cabal update - cabal build + cabal build --index-state=2026-09-06T00:00:00Z - name: Build site run: | mkdir -p site _site @@ -73,7 +73,7 @@ jobs: echo "Documentation coming soon." >> site/index.md fi fi - cd .casket-ssg && cabal run casket-ssg -- build ../site ../_site + cd .casket-ssg && cabal run --index-state=2026-09-06T00:00:00Z casket-ssg -- build ../site ../_site - name: Setup Pages uses: actions/configure-pages@v6.0.0 - name: Upload artifact diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 39f8300..2f73e73 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -53,7 +53,7 @@ jobs: working-directory: casket-ssg run: | cabal update - cabal build + cabal build --index-state=2026-09-06T00:00:00Z - name: Install AsciiDoc conversion tools run: | sudo apt-get update @@ -71,7 +71,7 @@ jobs: - name: Build documentation with Casket working-directory: casket-ssg run: | - cabal run casket-ssg -- build ../.docs-source ../docs/_site + cabal run --index-state=2026-09-06T00:00:00Z casket-ssg -- build ../.docs-source ../docs/_site test -s ../docs/_site/index.html touch ../docs/_site/.nojekyll - name: Upload artifact diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index b52f90b..d913780 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -52,6 +52,8 @@ jobs: contents: read steps: - uses: actions/checkout@v7.0.1 + with: + persist-credentials: false - name: Prove a standalone clone resolves before building run: | diff --git a/.github/workflows/echidnabot.yml b/.github/workflows/echidnabot.yml index 0abeca4..2ee4c60 100644 --- a/.github/workflows/echidnabot.yml +++ b/.github/workflows/echidnabot.yml @@ -62,6 +62,8 @@ jobs: any_proofs: ${{ steps.detect.outputs.any_proofs }} steps: - uses: actions/checkout@v7.0.1 + with: + persist-credentials: false - name: Detect proof file types id: detect run: | @@ -91,6 +93,8 @@ jobs: timeout-minutes: 15 steps: - uses: actions/checkout@v7.0.1 + with: + persist-credentials: false - name: Install Coq run: | sudo apt-get update @@ -111,6 +115,8 @@ jobs: timeout-minutes: 15 steps: - uses: actions/checkout@v7.0.1 + with: + persist-credentials: false - name: Install elan (Lean version manager) run: | curl https://raw.githubusercontent.com/leanprover/elan/master/elan-init.sh -sSf | sh -s -- -y @@ -136,6 +142,8 @@ jobs: timeout-minutes: 15 steps: - uses: actions/checkout@v7.0.1 + with: + persist-credentials: false - name: Install Z3 run: | sudo apt-get update @@ -156,6 +164,8 @@ jobs: timeout-minutes: 15 steps: - uses: actions/checkout@v7.0.1 + with: + persist-credentials: false - name: Install Agda run: | sudo apt-get update diff --git a/.github/workflows/workflow-linter.yml b/.github/workflows/workflow-linter.yml index 5171312..6597eee 100644 --- a/.github/workflows/workflow-linter.yml +++ b/.github/workflows/workflow-linter.yml @@ -42,6 +42,7 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: | - gh extension install github/gh-actions-lock --pin v0.1.6 - gh actions-lock --rescan --no-fix - + curl -fsSL --retry 3 -o "$RUNNER_TEMP/gh-actions-lock" https://github.com/github/gh-actions-lock/releases/download/v0.1.6/linux-amd64 + echo "4181ec1da5408b34b9a542a7ee5c6ce3a4d6ac815c7d0206a00ceca8a817f4e3 $RUNNER_TEMP/gh-actions-lock" | sha256sum --check + chmod u+x "$RUNNER_TEMP/gh-actions-lock" + "$RUNNER_TEMP/gh-actions-lock" --rescan --no-fix diff --git a/.machine_readable/contractiles/Justfile b/.machine_readable/contractiles/Justfile index a339fff..229fecd 100644 --- a/.machine_readable/contractiles/Justfile +++ b/.machine_readable/contractiles/Justfile @@ -52,7 +52,7 @@ info: @echo "Version: {{version}}" @echo "RSR Tier: {{tier}}" @echo "Recipes: $(just --summary | wc -w)" - @[ -f ".machine_readable/STATE.a2ml" ] && grep -oP 'phase\s*=\s*"\K[^"]+' .machine_readable/STATE.a2ml | head -1 | xargs -I{} echo "Phase: {}" || true + @[ -f ".machine_readable/descriptiles/STATE.a2ml" ] && grep -oP 'phase\s*=\s*"\K[^"]+' .machine_readable/descriptiles/STATE.a2ml | head -1 | xargs -I{} echo "Phase: {}" || true # Run Invariant Path overlay tools for this repository invariant-path *ARGS: @@ -549,14 +549,14 @@ import? "build/just/validate.just" # Update STATE.a2ml timestamp state-touch: - @if [ -f ".machine_readable/STATE.a2ml" ]; then \ - sed -i 's/last-updated = "[^"]*"/last-updated = "'"$(date +%Y-%m-%d)"'"/' .machine_readable/STATE.a2ml && \ + @if [ -f ".machine_readable/descriptiles/STATE.a2ml" ]; then \ + sed -i 's/last-updated = "[^"]*"/last-updated = "'"$(date +%Y-%m-%d)"'"/' .machine_readable/descriptiles/STATE.a2ml && \ echo "STATE.a2ml timestamp updated"; \ fi # Show current phase from STATE.a2ml state-phase: - @grep -oP 'phase\s*=\s*"\K[^"]+' .machine_readable/STATE.a2ml 2>/dev/null | head -1 || echo "unknown" + @grep -oP 'phase\s*=\s*"\K[^"]+' .machine_readable/descriptiles/STATE.a2ml 2>/dev/null | head -1 || echo "unknown" # ═══════════════════════════════════════════════════════════════════════════════ # GUIX & GUIX diff --git a/.machine_readable/contractiles/Mustfile.a2ml b/.machine_readable/contractiles/Mustfile.a2ml index 611ef25..b08509e 100644 --- a/.machine_readable/contractiles/Mustfile.a2ml +++ b/.machine_readable/contractiles/Mustfile.a2ml @@ -62,13 +62,13 @@ hooks fail if any check fails. - severity: critical ### six-directory-present -- description: 6a2 directory exists with required files -- run: test -d .machine_readable/6a2 && test -f .machine_readable/descriptiles/META.a2ml && test -f .machine_readable/descriptiles/ECOSYSTEM.a2ml && test -f .machine_readable/descriptiles/STATE.a2ml && test -f .machine_readable/descriptiles/PLAYBOOK.a2ml && test -f .machine_readable/descriptiles/AGENTIC.a2ml && test -f .machine_readable/descriptiles/NEUROSYM.a2ml +- description: descriptiles directory exists with required files +- run: test -d .machine_readable/descriptiles && test -f .machine_readable/descriptiles/META.a2ml && test -f .machine_readable/descriptiles/ECOSYSTEM.a2ml && test -f .machine_readable/descriptiles/STATE.a2ml && test -f .machine_readable/descriptiles/PLAYBOOK.a2ml && test -f .machine_readable/descriptiles/AGENTIC.a2ml && test -f .machine_readable/descriptiles/NEUROSYM.a2ml - severity: critical ### anchors-directory -- description: anchors directory exists in 6a2 -- run: test -d .machine_readable/descriptiles/anchors +- description: anchors directory exists in descriptiles +- run: test -d .machine_readable/descriptiles/anchor - severity: warning ### self-validating-structure diff --git a/.machine_readable/descriptiles/STATE.a2ml b/.machine_readable/descriptiles/STATE.a2ml index 4ddd97d..ec6586c 100644 --- a/.machine_readable/descriptiles/STATE.a2ml +++ b/.machine_readable/descriptiles/STATE.a2ml @@ -440,4 +440,4 @@ file-4 = "Two commits (7052195 + 1b465ba) on main branch with detailed commit me [history.retired-root-state] last-updated = "2026-04-26" # seam test: 7a mode-routing + 7b corpus-bridge end-to-end -session = "seam test DONE (tests/seam_test.rs, 15 tests); smoke.rs mode_selector bug fixed; lifecycle.rs unused-import cleaned; total 184/184 tests (137 lib + 17 lifecycle + 32 integration + 12 property + 15 seam + 8 smoke)" +session = "seam test DONE (tests/seam_test.rs, 15 tests); smoke.rs mode_selector bug fixed; lifecycle.rs unused-import cleaned; historical suite counts recorded as 137 lib + 17 lifecycle + 32 integration + 12 property + 15 seam + 8 smoke (sum 221; the retired 184 aggregate was inconsistent and is not a current test result)" diff --git a/EXPLAINME.adoc b/EXPLAINME.adoc index f85d8a0..60db538 100644 --- a/EXPLAINME.adoc +++ b/EXPLAINME.adoc @@ -143,10 +143,10 @@ self-healing cascades. === BoJ-only-MCP exception -A documented carve-out from the estate-wide "all MCP through BoJ" rule. -See `.machine_readable/descriptiles/AGENTIC.a2ml` `[exceptions.boj-only-mcp]`. -Scope: this repo only. Sunset: when BoJ cartridge supports the -double-loop feedback protocol. +This historical carve-out is inactive: it was superseded and its sunset +fulfilled on 2026-04-25. MCP integrations must now go through BoJ. +See `.machine_readable/descriptiles/AGENTIC.a2ml` `[exceptions.boj-only-mcp]` +for the retained history. === Dual-truth: monorepo + standalone diff --git a/docs/content/api.adoc b/docs/content/api.adoc index 0aff6c7..4dc224a 100644 --- a/docs/content/api.adoc +++ b/docs/content/api.adoc @@ -106,17 +106,18 @@ query { ==== registerRepository -Register a new repository for proof verification. +Register a new repository for proof verification. This example omits the +optional webhook secret; configure a secret from your credential store before +accepting authenticated webhook events. [source,graphql] ---- -mutation($webhookSecret: String!) { +mutation { registerRepository(input: { platform: GITHUB owner: "org" name: "repo" enabledProvers: [COQ, LEAN4] - webhookSecret: $webhookSecret }) { id webhookUrl diff --git a/echidnabot.example.toml b/echidnabot.example.toml index 8c416cc..774bbbb 100644 --- a/echidnabot.example.toml +++ b/echidnabot.example.toml @@ -38,7 +38,7 @@ queue_size = 100 # private_key_path = "/path/to/private-key.pem" # # # Option 2: Personal Access Token (simpler for testing) -# token = "ghp_xxxxxxxxxxxxxxxxxxxx" +# Load the GitHub token from your local credential store. # # # Webhook secret for signature verification # webhook_secret = "" # Supply your webhook signing secret locally. diff --git a/echidnabot.toml b/echidnabot.toml index d310d1f..4053fbd 100644 --- a/echidnabot.toml +++ b/echidnabot.toml @@ -38,7 +38,7 @@ queue_size = 100 # private_key_path = "/path/to/private-key.pem" # # # Option 2: Personal Access Token (simpler for testing) -# token = "ghp_xxxxxxxxxxxxxxxxxxxx" +# Load the GitHub token from your local credential store. # # # Webhook secret for signature verification # webhook_secret = "" # Supply your webhook signing secret locally. diff --git a/src/adapters/bitbucket.rs b/src/adapters/bitbucket.rs index 9e37954..b60d990 100644 --- a/src/adapters/bitbucket.rs +++ b/src/adapters/bitbucket.rs @@ -3,7 +3,6 @@ //! Bitbucket platform adapter (minimal clone support) use async_trait::async_trait; -use std::path::PathBuf; use super::{ CheckConclusion, CheckRun, CheckRunId, CheckStatus, CommentId, IssueId, NewIssue, @@ -43,76 +42,9 @@ impl BitbucketAdapter { #[async_trait] impl PlatformAdapter for BitbucketAdapter { - async fn clone_repo(&self, repo: &RepoId, commit: &str) -> Result { - let temp_dir = tempfile::tempdir().map_err(Error::Io)?; - let clone_path = temp_dir.keep(); - + async fn clone_repo(&self, repo: &RepoId, commit: &str) -> Result { let url = self.repo_url(repo); - - let status = if commit == "HEAD" { - tokio::process::Command::new("git") - .args([ - "clone", - "--depth", - "1", - &url, - &*clone_path.to_string_lossy(), - ]) - .status() - .await - .map_err(Error::Io)? - } else { - tokio::process::Command::new("git") - .args([ - "clone", - "--depth", - "1", - "--branch", - commit, - &url, - &*clone_path.to_string_lossy(), - ]) - .status() - .await - .map_err(Error::Io)? - }; - - if !status.success() && commit != "HEAD" { - let status = tokio::process::Command::new("git") - .args([ - "clone", - "--depth", - "1", - &url, - &*clone_path.to_string_lossy(), - ]) - .status() - .await - .map_err(Error::Io)?; - - if !status.success() { - return Err(Error::Unsupported(format!( - "Failed to clone {}", - repo.full_name() - ))); - } - - tokio::process::Command::new("git") - .current_dir(&clone_path) - .args(["fetch", "--depth", "1", "origin", commit]) - .status() - .await - .map_err(Error::Io)?; - - tokio::process::Command::new("git") - .current_dir(&clone_path) - .args(["checkout", commit]) - .status() - .await - .map_err(Error::Io)?; - } - - Ok(clone_path) + super::clone_revision(&url, commit).await } async fn create_check_run(&self, repo: &RepoId, check: CheckRun) -> Result { diff --git a/src/adapters/codeberg.rs b/src/adapters/codeberg.rs index e0a2b8d..76ceb66 100644 --- a/src/adapters/codeberg.rs +++ b/src/adapters/codeberg.rs @@ -55,7 +55,6 @@ //! - [ ] Smoke test against a real Codeberg instance (currently unit-only) use async_trait::async_trait; -use std::path::PathBuf; use super::{ CheckConclusion, CheckRun, CheckRunId, CheckStatus, CommentId, IssueId, NewIssue, @@ -117,79 +116,9 @@ impl CodebergAdapter { #[async_trait] impl PlatformAdapter for CodebergAdapter { - async fn clone_repo(&self, repo: &RepoId, commit: &str) -> Result { - // Mirrors github/gitlab/bitbucket — shallow clone, then fall - // back to fetch+checkout for a specific commit if the initial - // branch-targeted clone fails (e.g. SHA, not branch name). - let temp_dir = tempfile::tempdir().map_err(Error::Io)?; - let clone_path = temp_dir.keep(); - + async fn clone_repo(&self, repo: &RepoId, commit: &str) -> Result { let url = self.repo_url(repo); - - let status = if commit == "HEAD" { - tokio::process::Command::new("git") - .args([ - "clone", - "--depth", - "1", - &url, - &*clone_path.to_string_lossy(), - ]) - .status() - .await - .map_err(Error::Io)? - } else { - tokio::process::Command::new("git") - .args([ - "clone", - "--depth", - "1", - "--branch", - commit, - &url, - &*clone_path.to_string_lossy(), - ]) - .status() - .await - .map_err(Error::Io)? - }; - - if !status.success() && commit != "HEAD" { - let status = tokio::process::Command::new("git") - .args([ - "clone", - "--depth", - "1", - &url, - &*clone_path.to_string_lossy(), - ]) - .status() - .await - .map_err(Error::Io)?; - - if !status.success() { - return Err(Error::Unsupported(format!( - "Failed to clone {}", - repo.full_name() - ))); - } - - tokio::process::Command::new("git") - .current_dir(&clone_path) - .args(["fetch", "--depth", "1", "origin", commit]) - .status() - .await - .map_err(Error::Io)?; - - tokio::process::Command::new("git") - .current_dir(&clone_path) - .args(["checkout", commit]) - .status() - .await - .map_err(Error::Io)?; - } - - Ok(clone_path) + super::clone_revision(&url, commit).await } async fn create_check_run(&self, repo: &RepoId, check: CheckRun) -> Result { diff --git a/src/adapters/github.rs b/src/adapters/github.rs index eab1ed7..a0a800c 100644 --- a/src/adapters/github.rs +++ b/src/adapters/github.rs @@ -4,7 +4,6 @@ //! GitHub platform adapter using Octocrab use async_trait::async_trait; -use std::path::PathBuf; use super::{ CheckConclusion, CheckRun, CheckRunId, CheckStatus, CommentId, IssueId, NewIssue, @@ -50,80 +49,9 @@ impl GitHubAdapter { #[async_trait] impl PlatformAdapter for GitHubAdapter { - async fn clone_repo(&self, repo: &RepoId, commit: &str) -> Result { - // Create a temporary directory for the clone - let temp_dir = tempfile::tempdir().map_err(Error::Io)?; - let clone_path = temp_dir.keep(); - - // Use git to clone (shallow, specific commit) + async fn clone_repo(&self, repo: &RepoId, commit: &str) -> Result { let url = format!("https://github.com/{}/{}.git", repo.owner, repo.name); - - let status = if commit == "HEAD" { - tokio::process::Command::new("git") - .args([ - "clone", - "--depth", - "1", - &url, - &*clone_path.to_string_lossy(), - ]) - .status() - .await - .map_err(Error::Io)? - } else { - tokio::process::Command::new("git") - .args([ - "clone", - "--depth", - "1", - "--branch", - commit, - &url, - &*clone_path.to_string_lossy(), - ]) - .status() - .await - .map_err(Error::Io)? - }; - - if !status.success() && commit != "HEAD" { - // Try fetching the specific commit instead - let status = tokio::process::Command::new("git") - .args([ - "clone", - "--depth", - "1", - &url, - &*clone_path.to_string_lossy(), - ]) - .status() - .await - .map_err(Error::Io)?; - - if !status.success() { - return Err(Error::GitHub(format!( - "Failed to clone {}", - repo.full_name() - ))); - } - - // Fetch and checkout specific commit - tokio::process::Command::new("git") - .current_dir(&clone_path) - .args(["fetch", "--depth", "1", "origin", commit]) - .status() - .await - .map_err(Error::Io)?; - - tokio::process::Command::new("git") - .current_dir(&clone_path) - .args(["checkout", commit]) - .status() - .await - .map_err(Error::Io)?; - } - - Ok(clone_path) + super::clone_revision(&url, commit).await } async fn create_check_run(&self, repo: &RepoId, check: CheckRun) -> Result { diff --git a/src/adapters/gitlab.rs b/src/adapters/gitlab.rs index 7eab76c..8005229 100644 --- a/src/adapters/gitlab.rs +++ b/src/adapters/gitlab.rs @@ -3,7 +3,6 @@ //! GitLab platform adapter (minimal clone support) use async_trait::async_trait; -use std::path::PathBuf; use super::{ CheckConclusion, CheckRun, CheckRunId, CheckStatus, CommentId, IssueId, NewIssue, @@ -43,76 +42,9 @@ impl GitLabAdapter { #[async_trait] impl PlatformAdapter for GitLabAdapter { - async fn clone_repo(&self, repo: &RepoId, commit: &str) -> Result { - let temp_dir = tempfile::tempdir().map_err(Error::Io)?; - let clone_path = temp_dir.keep(); - + async fn clone_repo(&self, repo: &RepoId, commit: &str) -> Result { let url = self.repo_url(repo); - - let status = if commit == "HEAD" { - tokio::process::Command::new("git") - .args([ - "clone", - "--depth", - "1", - &url, - &*clone_path.to_string_lossy(), - ]) - .status() - .await - .map_err(Error::Io)? - } else { - tokio::process::Command::new("git") - .args([ - "clone", - "--depth", - "1", - "--branch", - commit, - &url, - &*clone_path.to_string_lossy(), - ]) - .status() - .await - .map_err(Error::Io)? - }; - - if !status.success() && commit != "HEAD" { - let status = tokio::process::Command::new("git") - .args([ - "clone", - "--depth", - "1", - &url, - &*clone_path.to_string_lossy(), - ]) - .status() - .await - .map_err(Error::Io)?; - - if !status.success() { - return Err(Error::GitHub(format!( - "Failed to clone {}", - repo.full_name() - ))); - } - - tokio::process::Command::new("git") - .current_dir(&clone_path) - .args(["fetch", "--depth", "1", "origin", commit]) - .status() - .await - .map_err(Error::Io)?; - - tokio::process::Command::new("git") - .current_dir(&clone_path) - .args(["checkout", commit]) - .status() - .await - .map_err(Error::Io)?; - } - - Ok(clone_path) + super::clone_revision(&url, commit).await } async fn create_check_run(&self, repo: &RepoId, check: CheckRun) -> Result { diff --git a/src/adapters/mod.rs b/src/adapters/mod.rs index bb7f984..e54a375 100644 --- a/src/adapters/mod.rs +++ b/src/adapters/mod.rs @@ -10,7 +10,6 @@ pub mod github; pub mod gitlab; use async_trait::async_trait; -use std::path::PathBuf; use crate::error::Result; @@ -159,7 +158,7 @@ pub fn build_adapter( #[async_trait] pub trait PlatformAdapter: Send + Sync { /// Clone a repository to a local path - async fn clone_repo(&self, repo: &RepoId, commit: &str) -> Result; + async fn clone_repo(&self, repo: &RepoId, commit: &str) -> Result; /// Create a check run (GitHub) or pipeline status (GitLab) async fn create_check_run(&self, repo: &RepoId, check: CheckRun) -> Result; @@ -205,3 +204,43 @@ pub trait PlatformAdapter: Send + Sync { location: ReviewCommentLocation, ) -> Result; } + +/// Clone and check out the requested revision. The returned owner removes the +/// checkout on drop, including cancellation and errors during proof processing. +pub async fn clone_revision(url: &str, commit: &str) -> Result { + let temp_dir = tempfile::tempdir()?; + let clone = tokio::process::Command::new("git") + .args(["clone", "--no-checkout", "--depth", "1", "--", url]) + .arg(temp_dir.path()) + .status() + .await?; + if !clone.success() { + return Err(crate::Error::Internal(format!( + "Failed to clone {}", + "requested repository" + ))); + } + let fetch = tokio::process::Command::new("git") + .current_dir(temp_dir.path()) + .args(["fetch", "--depth", "1", "--", "origin", commit]) + .status() + .await?; + if !fetch.success() { + return Err(crate::Error::Internal(format!( + "Failed to fetch requested revision for {}", + "requested repository" + ))); + } + let checkout = tokio::process::Command::new("git") + .current_dir(temp_dir.path()) + .args(["checkout", "--detach", "FETCH_HEAD"]) + .status() + .await?; + if !checkout.success() { + return Err(crate::Error::Internal(format!( + "Failed to check out requested revision for {}", + "requested repository" + ))); + } + Ok(temp_dir) +} diff --git a/src/main.rs b/src/main.rs index 3697435..e0f62eb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1206,7 +1206,8 @@ async fn process_job( .ok_or_else(|| echidnabot::Error::RepoNotFound(job.repo_id.to_string()))?; let repo_id = RepoId::new(repo.platform, repo.owner.clone(), repo.name.clone()); - let repo_path = clone_repo(config, &repo_id, &job.commit_sha).await?; + let checkout = clone_repo(config, &repo_id, &job.commit_sha).await?; + let repo_path = checkout.path().to_path_buf(); let mut file_paths = job.file_paths.clone(); if file_paths.is_empty() { @@ -1363,7 +1364,7 @@ async fn process_job( }) } -async fn clone_repo(config: &Config, repo: &RepoId, commit: &str) -> Result { +async fn clone_repo(config: &Config, repo: &RepoId, commit: &str) -> Result { match repo.platform { Platform::GitHub => { if let Some(ref gh) = config.github { @@ -1393,48 +1394,18 @@ async fn clone_repo(config: &Config, repo: &RepoId, commit: &str) -> Result Result { - let temp_dir = tempfile::tempdir()?; +async fn clone_repo_via_git( + base_url: &str, + repo: &RepoId, + commit: &str, +) -> Result { let url = format!( "{}/{}/{}.git", base_url.trim_end_matches('/'), repo.owner, repo.name ); - let clone = tokio::process::Command::new("git") - .args(["clone", "--no-checkout", "--depth", "1", "--", &url]) - .arg(temp_dir.path()) - .status() - .await?; - if !clone.success() { - return Err(echidnabot::Error::Internal(format!( - "Failed to clone {}", - repo.full_name() - ))); - } - let fetch = tokio::process::Command::new("git") - .current_dir(temp_dir.path()) - .args(["fetch", "--depth", "1", "--", "origin", commit]) - .status() - .await?; - if !fetch.success() { - return Err(echidnabot::Error::Internal(format!( - "Failed to fetch requested revision for {}", - repo.full_name() - ))); - } - let checkout = tokio::process::Command::new("git") - .current_dir(temp_dir.path()) - .args(["checkout", "--detach", "FETCH_HEAD"]) - .status() - .await?; - if !checkout.success() { - return Err(echidnabot::Error::Internal(format!( - "Failed to check out requested revision for {}", - repo.full_name() - ))); - } - Ok(temp_dir.keep()) + echidnabot::adapters::clone_revision(&url, commit).await } const MAX_PROOF_FILES: usize = 10_000; @@ -1562,8 +1533,13 @@ mod clone_contract_tests { }; let id = RepoId::new(Platform::Codeberg, "owner", "proofs"); let cloned = clone_repo(&config, &id, &first).await.unwrap(); - let actual = git(&cloned, &["rev-parse", "HEAD"]); - std::fs::remove_dir_all(cloned).unwrap(); + let path = cloned.path().to_path_buf(); + let actual = git(&path, &["rev-parse", "HEAD"]); + drop(cloned); + assert!( + !path.exists(), + "dropping the checkout must remove the repository" + ); assert_eq!(actual, first); assert!(clone_repo(&config, &id, "missing-contract-revision") .await From c9346be53884c6bf0ba7bdc59c3be89b2e1311af Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Mon, 7 Sep 2026 04:29:52 +0100 Subject: [PATCH 08/10] Document the canonical descriptor and anchor directories --- RSR_OUTLINE.adoc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/RSR_OUTLINE.adoc b/RSR_OUTLINE.adoc index 03b474c..52ffe2c 100644 --- a/RSR_OUTLINE.adoc +++ b/RSR_OUTLINE.adoc @@ -52,7 +52,8 @@ echidnabot/ │ ├── ai.txt │ └── humans.txt ├── .machine_readable/ # A2ML descriptors (canonical) -│ ├── 6a2/ # STATE / META / AGENTIC / ECOSYSTEM / etc. +│ ├── descriptiles/ # STATE / META / AGENTIC / ECOSYSTEM / etc. +│ │ └── anchor/ # Supporting anchor descriptors │ ├── bot_directives/ # Per-bot directive files │ ├── contractiles/ # intend / bust / adjust contractile verbs │ └── svc/ # Service descriptors (k9 templates) From 988fd047f87f6aa3488343a528396cf1e4938cf3 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Mon, 7 Sep 2026 04:41:27 +0100 Subject: [PATCH 09/10] Grant governance the read-only Actions permission it requires --- .github/workflows/governance.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/governance.yml b/.github/workflows/governance.yml index 415259b..fd42d2b 100644 --- a/.github/workflows/governance.yml +++ b/.github/workflows/governance.yml @@ -10,6 +10,7 @@ on: workflow_dispatch: permissions: + actions: read contents: read jobs: From 37abc9c6ea0f784b63e4c1ffc44b36973bb5ea6c Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Mon, 7 Sep 2026 08:29:45 +0100 Subject: [PATCH 10/10] Bound forge checkouts and terminate Git children on cancellation --- .github/workflows/proof-safety.yml | 1 + .machine_readable/descriptiles/PLAYBOOK.a2ml | 2 +- Justfile | 16 +-- src/adapters/mod.rs | 137 +++++++++++++++++-- 4 files changed, 130 insertions(+), 26 deletions(-) diff --git a/.github/workflows/proof-safety.yml b/.github/workflows/proof-safety.yml index e25a26e..4bc630a 100644 --- a/.github/workflows/proof-safety.yml +++ b/.github/workflows/proof-safety.yml @@ -24,3 +24,4 @@ jobs: run: | cargo test --locked --test protocol_contract cargo test --locked --bin echidnabot clone_contract_tests + cargo test --locked --lib git_process_tests diff --git a/.machine_readable/descriptiles/PLAYBOOK.a2ml b/.machine_readable/descriptiles/PLAYBOOK.a2ml index 5003fd0..b9215be 100644 --- a/.machine_readable/descriptiles/PLAYBOOK.a2ml +++ b/.machine_readable/descriptiles/PLAYBOOK.a2ml @@ -11,7 +11,7 @@ last-updated = "2026-04-11" # target = "container" # container | binary | library | wasm [incident-response] -# 1. Check .machine_readable/STATE.a2ml for current status +# 1. Check .machine_readable/descriptiles/STATE.a2ml for current status # 2. Review recent commits and CI results # 3. Run `just validate` to check compliance # 4. Run `just security` to audit for vulnerabilities diff --git a/Justfile b/Justfile index 9d376e2..68742bf 100644 --- a/Justfile +++ b/Justfile @@ -1,5 +1,5 @@ # SPDX-License-Identifier: MPL-2.0 -// Owner: Jonathan D.A. Jewell +# Owner: Jonathan D.A. Jewell # Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) # # RSR Standard Justfile Template @@ -53,7 +53,7 @@ info: @echo "Version: {{version}}" @echo "RSR Tier: {{tier}}" @echo "Recipes: $(just --summary | wc -w)" - @[ -f ".machine_readable/STATE.a2ml" ] && grep -oP 'phase\s*=\s*"\K[^"]+' .machine_readable/STATE.a2ml | head -1 | xargs -I{} echo "Phase: {}" || true + @[ -f ".machine_readable/descriptiles/STATE.a2ml" ] && grep -oP 'phase\s*=\s*"\K[^"]+' .machine_readable/descriptiles/STATE.a2ml | head -1 | xargs -I{} echo "Phase: {}" || true # Run Invariant Path overlay tools for this repository invariant-path *ARGS: @@ -550,17 +550,17 @@ import? "build/just/validate.just" # Update STATE.a2ml timestamp state-touch: - @if [ -f ".machine_readable/STATE.a2ml" ]; then \ - sed -i 's/last-updated = "[^"]*"/last-updated = "'"$(date +%Y-%m-%d)"'"/' .machine_readable/STATE.a2ml && \ + @if [ -f ".machine_readable/descriptiles/STATE.a2ml" ]; then \ + sed -i 's/last-updated = "[^"]*"/last-updated = "'"$(date +%Y-%m-%d)"'"/' .machine_readable/descriptiles/STATE.a2ml && \ echo "STATE.a2ml timestamp updated"; \ fi # Show current phase from STATE.a2ml state-phase: - @grep -oP 'phase\s*=\s*"\K[^"]+' .machine_readable/STATE.a2ml 2>/dev/null | head -1 || echo "unknown" + @grep -oP 'phase\s*=\s*"\K[^"]+' .machine_readable/descriptiles/STATE.a2ml 2>/dev/null | head -1 || echo "unknown" # ═══════════════════════════════════════════════════════════════════════════════ -# GUIX & GUIX +# GUIX # ═══════════════════════════════════════════════════════════════════════════════ # Enter Guix development shell (primary) @@ -571,10 +571,6 @@ guix-shell: guix-build: guix build -f guix.scm -# Enter Guix development shell (fallback) -guix-shell: - @if [ -f "flake.guix" ]; then guix develop; else echo "No flake.guix"; fi - # ═══════════════════════════════════════════════════════════════════════════════ # HYBRID AUTOMATION # ═══════════════════════════════════════════════════════════════════════════════ diff --git a/src/adapters/mod.rs b/src/adapters/mod.rs index e54a375..296b568 100644 --- a/src/adapters/mod.rs +++ b/src/adapters/mod.rs @@ -207,35 +207,47 @@ pub trait PlatformAdapter: Send + Sync { /// Clone and check out the requested revision. The returned owner removes the /// checkout on drop, including cancellation and errors during proof processing. +/// All Git operations share a two-minute deadline and run without terminal +/// prompts. Dropping an in-flight operation also terminates its Git child. pub async fn clone_revision(url: &str, commit: &str) -> Result { + let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(120); let temp_dir = tempfile::tempdir()?; - let clone = tokio::process::Command::new("git") - .args(["clone", "--no-checkout", "--depth", "1", "--", url]) - .arg(temp_dir.path()) - .status() - .await?; + let clone = git_status( + tokio::process::Command::new("git") + .args(["clone", "--no-checkout", "--depth", "1", "--", url]) + .arg(temp_dir.path()), + "clone", + deadline, + ) + .await?; if !clone.success() { return Err(crate::Error::Internal(format!( "Failed to clone {}", "requested repository" ))); } - let fetch = tokio::process::Command::new("git") - .current_dir(temp_dir.path()) - .args(["fetch", "--depth", "1", "--", "origin", commit]) - .status() - .await?; + let fetch = git_status( + tokio::process::Command::new("git") + .current_dir(temp_dir.path()) + .args(["fetch", "--depth", "1", "--", "origin", commit]), + "fetch", + deadline, + ) + .await?; if !fetch.success() { return Err(crate::Error::Internal(format!( "Failed to fetch requested revision for {}", "requested repository" ))); } - let checkout = tokio::process::Command::new("git") - .current_dir(temp_dir.path()) - .args(["checkout", "--detach", "FETCH_HEAD"]) - .status() - .await?; + let checkout = git_status( + tokio::process::Command::new("git") + .current_dir(temp_dir.path()) + .args(["checkout", "--detach", "FETCH_HEAD"]), + "checkout", + deadline, + ) + .await?; if !checkout.success() { return Err(crate::Error::Internal(format!( "Failed to check out requested revision for {}", @@ -244,3 +256,98 @@ pub async fn clone_revision(url: &str, commit: &str) -> Result Result { + let mut child = command + .env("GIT_TERMINAL_PROMPT", "0") + .stdin(std::process::Stdio::null()) + .kill_on_drop(true) + .spawn()?; + match tokio::time::timeout_at(deadline, child.wait()).await { + Ok(status) => Ok(status?), + Err(_) => { + child.kill().await?; + Err(crate::Error::Internal(format!("Git {operation} timed out"))) + } + } +} + +#[cfg(all(test, target_os = "linux"))] +mod git_process_tests { + use super::*; + use std::path::Path; + use std::time::Duration; + use tokio::time::{sleep, Instant}; + + async fn assert_child_reaped(pid_file: &Path) { + let pid: u32 = std::fs::read_to_string(pid_file) + .unwrap() + .trim() + .parse() + .unwrap(); + let proc_path = std::path::PathBuf::from(format!("/proc/{pid}")); + let deadline = Instant::now() + Duration::from_secs(2); + while proc_path.exists() && Instant::now() < deadline { + sleep(Duration::from_millis(10)).await; + } + assert!( + !proc_path.exists(), + "Git child {pid} survived its operation" + ); + } + + #[tokio::test] + async fn git_deadline_kills_and_reaps_the_child_without_prompts() { + let dir = tempfile::tempdir().unwrap(); + let pid = dir.path().join("pid"); + let prompt = dir.path().join("prompt"); + let result = git_status( + tokio::process::Command::new("/bin/sh") + .args([ + "-c", + "echo \"$GIT_TERMINAL_PROMPT\" > \"$1\"; echo $$ > \"$2\"; exec sleep 30", + "contract", + ]) + .arg(&prompt) + .arg(&pid), + "contract", + Instant::now() + Duration::from_millis(200), + ) + .await; + assert!( + matches!(result, Err(crate::Error::Internal(message)) if message.contains("timed out")) + ); + assert_eq!(std::fs::read_to_string(prompt).unwrap().trim(), "0"); + assert_child_reaped(&pid).await; + } + + #[tokio::test] + async fn cancelling_checkout_terminates_its_child() { + let dir = tempfile::tempdir().unwrap(); + let pid = dir.path().join("pid"); + let child_pid = pid.clone(); + let task = tokio::spawn(async move { + git_status( + tokio::process::Command::new("/bin/sh") + .args(["-c", "echo $$ > \"$1\"; exec sleep 30", "contract"]) + .arg(child_pid), + "contract", + Instant::now() + Duration::from_secs(10), + ) + .await + }); + let deadline = Instant::now() + Duration::from_secs(2); + while !pid.exists() && Instant::now() < deadline { + sleep(Duration::from_millis(10)).await; + } + task.abort(); + assert!(task.await.unwrap_err().is_cancelled()); + assert_child_reaped(&pid).await; + } +}