From b977a912e9c9935eb55f09a7b385b86221f519ce Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Mon, 7 Sep 2026 02:38:06 +0100 Subject: [PATCH 01/16] fix(fleet): restore safe fixer, repair CI and lock dashboard dependencies --- .claude/CLAUDE.md | 9 +- .github/workflows/actions.lock | 12 +- .github/workflows/casket-pages.yml | 106 +- .github/workflows/governance.yml | 2 +- .github/workflows/label-triage.yml | 1 + .github/workflows/labels.yml | 1 + .github/workflows/rust.yml | 7 +- .../{6a2 => descriptiles}/AGENTIC.a2ml | 4 +- .../{ => descriptiles}/ANCHOR.a2ml | 0 .../{6a2 => descriptiles}/ECOSYSTEM.a2ml | 0 .../{6a2 => descriptiles}/META.a2ml | 0 .../{6a2 => descriptiles}/NEUROSYM.a2ml | 0 .../{6a2 => descriptiles}/PLAYBOOK.a2ml | 4 +- .../{6a2 => descriptiles}/STATE.a2ml | 0 0-AI-MANIFEST.a2ml | 48 +- Justfile | 60 +- README.adoc | 2 +- SECURITY.md | 21 + bots/cipherbot/src/analyzers/infra.rs | 17 +- bots/echidnabot/docs/content/api.adoc | 7 +- bots/seambot/tests/github_integration.rs | 17 +- dashboard/Cargo.lock | 337 +--- docs/wiki-source/Build-and-Run.md | 2 +- fleet-coordinator.sh | 7 +- robot-repo-automaton/Cargo.lock | 22 + robot-repo-automaton/Cargo.toml | 9 +- robot-repo-automaton/SONNET-TASKS.adoc | 2 +- robot-repo-automaton/src/fixer.rs | 1411 ++++++++++++++++- robot-repo-automaton/src/hypatia.rs | 9 +- robot-repo-automaton/src/main.rs | 27 +- scripts/dispatch-runner.sh | 23 +- scripts/enroll-hypatia-fleet.sh | 5 +- scripts/fix-license-hygiene.sh | 9 +- scripts/fix-missing-ai-manifest.sh | 6 +- scripts/fix-proven-substitute.sh | 9 +- scripts/list-supervised-repos.sh | 4 +- scripts/maintenance-hard-pass.sh | 11 +- scripts/process-review-findings.sh | 2 +- scripts/repo-path-overrides.json | 131 +- scripts/sync-all-parallel.exs | 11 +- shared-context/enrollment/README.adoc | 2 +- 41 files changed, 1807 insertions(+), 550 deletions(-) rename .machine_readable/{6a2 => descriptiles}/AGENTIC.a2ml (88%) rename .machine_readable/{ => descriptiles}/ANCHOR.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 (83%) rename .machine_readable/{6a2 => descriptiles}/STATE.a2ml (100%) create mode 100644 SECURITY.md diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 9fbb19a9..6cad4cf5 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -72,11 +72,10 @@ Control (report < 0.85) → Human review required ## Critical Invariants 1. The seven canonical A2ML files (`STATE`, `META`, `ECOSYSTEM`, - `AGENTIC`, `NEUROSYM`, `PLAYBOOK`, `ANCHOR`) live directly under - `.machine_readable/`, per the `A2ML-REPO-TEMPLATE` in - `hyperpolymath/standards`. (Earlier versions of this CLAUDE.md - referenced a `.machine_readable/6scm/` subdir; that layout has been - retired.) + `AGENTIC`, `NEUROSYM`, `PLAYBOOK`, `ANCHOR`) live under + `.machine_readable/descriptiles/`, per the current estate-wide policy. + Earlier direct-under-`.machine_readable/`, `6scm/`, and `6a2/` layouts + are retired and must not be restored. 2. All shell scripts validate untrusted input before use. 3. No hardcoded secrets — use env vars with `${VAR:-}` defaults. 4. Fix scripts must be idempotent (safe to run multiple times). diff --git a/.github/workflows/actions.lock b/.github/workflows/actions.lock index 8a665353..8d68ff43 100644 --- a/.github/workflows/actions.lock +++ b/.github/workflows/actions.lock @@ -10,8 +10,8 @@ workflows: - 'actions/checkout@v7.0.1' - 'actions/configure-pages@v6.0.0' - 'actions/deploy-pages@v5.0.0' + - 'actions/download-artifact@v8.0.1' - 'actions/upload-pages-artifact@v5.0.0' - - 'haskell-actions/setup@v2.12.0' '.github/workflows/codeql.yml': - 'actions/checkout@v7.0.1' - 'github/codeql-action@v4.37.8' @@ -75,6 +75,11 @@ dependencies: commit: 'sha1-cd2ce8fcbc39b97be8ca5fce6e763baed58fa128' 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' @@ -102,11 +107,6 @@ dependencies: commit: 'sha1-db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28' owner_id: 9919 repo_id: 259445878 - 'haskell-actions/setup@v2.12.0': - ref: 'v2.12.0' - commit: 'sha1-6037f33647c3f17758a2356c80fc4a53d7e0685d' - owner_id: 75048950 - repo_id: 623796603 'hyperpolymath/a2ml-ecosystem@main': ref: 'main' commit: 'sha1-aa4b836bd969df2bc58128cb8e3d20bbc88d5e79' diff --git a/.github/workflows/casket-pages.yml b/.github/workflows/casket-pages.yml index 9f5efb27..6bb7f71c 100644 --- a/.github/workflows/casket-pages.yml +++ b/.github/workflows/casket-pages.yml @@ -1,42 +1,41 @@ -# SPDX-License-Identifier: MPL-2.0 # This workflow is managed by gh actions-lock. +# SPDX-License-Identifier: MPL-2.0 # This workflow is managed by gh actions-lock. name: GitHub Pages on: push: branches: [main, master] + pull_request: workflow_dispatch: permissions: - actions: read contents: read - pages: write - id-token: write concurrency: - group: "pages" - cancel-in-progress: false + group: "pages-${{ github.event_name }}-${{ github.ref }}" + cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: build: - runs-on: ubuntu-latest + name: Build Pages artifact + runs-on: ubuntu-24.04 timeout-minutes: 30 + permissions: + contents: read steps: - name: Checkout uses: actions/checkout@v7.0.1 + with: + persist-credentials: false - name: Checkout casket-ssg uses: actions/checkout@v7.0.1 with: repository: hyperpolymath/casket-ssg + ref: cec3c20d80ea1dc93660b69a4e7b38aa49f2a56b # standalone build; optional liblol bridge path: .casket-ssg - - - name: Setup GHCup - uses: haskell-actions/setup@v2.12.0 - with: - ghc-version: '9.8.2' - cabal-version: '3.10' + persist-credentials: false - name: Cache Cabal uses: actions/cache@v6.1.0 @@ -47,6 +46,13 @@ jobs: .casket-ssg/dist-newstyle key: ${{ runner.os }}-casket-${{ hashFiles('.casket-ssg/casket-ssg.cabal') }} + - name: Prepare runner Haskell toolchain + run: | + set -euo pipefail + ghc --version + cabal --version + cabal update + - name: Build casket-ssg working-directory: .casket-ssg run: cabal build @@ -108,13 +114,85 @@ jobs: with: path: '_site' + preview: + name: Validate deployable Pages preview + if: github.event_name == 'pull_request' + environment: + name: pages-preview + runs-on: ubuntu-24.04 + needs: build + timeout-minutes: 10 + permissions: + actions: read + contents: read + steps: + - name: Download Pages artifact + uses: actions/download-artifact@v8.0.1 + with: + name: github-pages + path: .pages-preview + + - name: Validate deployable artifact + shell: bash + run: | + set -euo pipefail + + artifact=".pages-preview/artifact.tar" + entries_file="${RUNNER_TEMP}/pages-preview-entries.txt" + + if [ ! -s "${artifact}" ]; then + echo "::error::Pages artifact is absent or empty" + exit 1 + fi + + tar -tf "${artifact}" > "${entries_file}" + + entry_count=0 + has_index=0 + while IFS= read -r entry; do + entry_count=$((entry_count + 1)) + case "${entry}" in + /*|../*|*/../*|*/..) + echo "::error::Pages artifact contains an unsafe path: ${entry}" + exit 1 + ;; + index.html|./index.html) + has_index=1 + ;; + esac + done < "${entries_file}" + + if [ "${entry_count}" -eq 0 ]; then + echo "::error::Pages artifact contains no files" + exit 1 + fi + + if [ "${has_index}" -ne 1 ]; then + echo "::error::Pages artifact contains no index.html" + exit 1 + fi + + { + echo "### Pages preview artifact" + echo + echo "- Files: ${entry_count}" + echo "- SHA-256: \`$(sha256sum "${artifact}" | awk '{print $1}')\`" + echo "- Production deployment: intentionally deferred until merge" + } >> "${GITHUB_STEP_SUMMARY}" + deploy: + name: Deploy production Pages site + if: github.event_name != 'pull_request' environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 needs: build timeout-minutes: 10 + permissions: + contents: read + pages: write + id-token: write steps: - name: Deploy to GitHub Pages id: deployment diff --git a/.github/workflows/governance.yml b/.github/workflows/governance.yml index cc2965a5..46d5028f 100644 --- a/.github/workflows/governance.yml +++ b/.github/workflows/governance.yml @@ -34,4 +34,4 @@ permissions: jobs: governance: - uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@571cc734cd69fb846032ec77a662aa8ee4fc32cd # main 2026-06-27 + uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@092dedada188f56c5915f74a5fd40aac093742c3 # main 2026-09-04 diff --git a/.github/workflows/label-triage.yml b/.github/workflows/label-triage.yml index 9886e920..2677c326 100644 --- a/.github/workflows/label-triage.yml +++ b/.github/workflows/label-triage.yml @@ -46,6 +46,7 @@ permissions: jobs: triage: runs-on: ubuntu-latest + timeout-minutes: 10 steps: - name: Classify and label env: diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml index c80b676c..948f5a7b 100644 --- a/.github/workflows/labels.yml +++ b/.github/workflows/labels.yml @@ -32,6 +32,7 @@ permissions: jobs: sync: runs-on: ubuntu-latest + timeout-minutes: 15 steps: - name: Apply canonical labels env: diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index f2de8025..ebd23374 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -1,3 +1,4 @@ +# This workflow is managed by gh actions-lock. # SPDX-License-Identifier: MPL-2.0 # This workflow is managed by gh actions-lock. # This workflow is managed by gh actions-lock. @@ -37,11 +38,11 @@ jobs: - name: Ensure clippy + rustfmt components run: rustup component add clippy rustfmt - name: Build (all targets) - run: cargo build --all-targets --verbose + run: cargo build --locked --all-targets --verbose - name: Test - run: cargo test --verbose + run: cargo test --locked --verbose - name: Clippy (deny warnings) - run: cargo clippy --all-targets -- -D warnings + run: cargo clippy --locked --all-targets -- -D warnings - name: Rustfmt check (informational) # Pre-existing formatting drift is not yet gated; surfaced here so it # stays visible without blocking. Flip to a hard gate after a dedicated diff --git a/.machine_readable/6a2/AGENTIC.a2ml b/.machine_readable/descriptiles/AGENTIC.a2ml similarity index 88% rename from .machine_readable/6a2/AGENTIC.a2ml rename to .machine_readable/descriptiles/AGENTIC.a2ml index aba3c451..645cb6c2 100644 --- a/.machine_readable/6a2/AGENTIC.a2ml +++ b/.machine_readable/descriptiles/AGENTIC.a2ml @@ -36,6 +36,6 @@ require-rerun-after-fix = true release-claim-requires-hard-pass = true [automation-hooks] -# on-enter: Read 0-AI-MANIFEST.a2ml, then STATE.a2ml -# on-exit: Update STATE.a2ml with session outcomes +# on-enter: Read 0-AI-MANIFEST.a2ml, then .machine_readable/descriptiles/STATE.a2ml +# on-exit: Update .machine_readable/descriptiles/STATE.a2ml with session outcomes # on-commit: Run just validate-rsr diff --git a/.machine_readable/ANCHOR.a2ml b/.machine_readable/descriptiles/ANCHOR.a2ml similarity index 100% rename from .machine_readable/ANCHOR.a2ml rename to .machine_readable/descriptiles/ANCHOR.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 83% rename from .machine_readable/6a2/PLAYBOOK.a2ml rename to .machine_readable/descriptiles/PLAYBOOK.a2ml index 5823e4bd..bcb9342c 100644 --- a/.machine_readable/6a2/PLAYBOOK.a2ml +++ b/.machine_readable/descriptiles/PLAYBOOK.a2ml @@ -13,13 +13,13 @@ 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 [release-process] -# 1. Update version in STATE.a2ml, META.a2ml +# 1. Update version in .machine_readable/descriptiles/STATE.a2ml and META.a2ml # 2. Run `just release-preflight` (validate + quality + security + maint-hard-pass) # 3. Tag and push diff --git a/.machine_readable/6a2/STATE.a2ml b/.machine_readable/descriptiles/STATE.a2ml similarity index 100% rename from .machine_readable/6a2/STATE.a2ml rename to .machine_readable/descriptiles/STATE.a2ml diff --git a/0-AI-MANIFEST.a2ml b/0-AI-MANIFEST.a2ml index d7392646..b27b27ba 100644 --- a/0-AI-MANIFEST.a2ml +++ b/0-AI-MANIFEST.a2ml @@ -11,15 +11,15 @@ This is the AI manifest for **gitbot-fleet**. It declares: ## CANONICAL LOCATIONS (UNIVERSAL RULE) -### Machine-Readable Metadata: `.machine_readable/` ONLY +### Machine-Readable Metadata: `.machine_readable/descriptiles/` ONLY -These 6 SCM files MUST exist in `.machine_readable/` directory ONLY: -1. **.machine_readable/6a2/STATE.a2ml** - Project state, progress, blockers -2. **.machine_readable/6a2/META.a2ml** - Architecture decisions, governance -3. **.machine_readable/6a2/ECOSYSTEM.a2ml** - Position in ecosystem, relationships -4. **.machine_readable/6a2/AGENTIC.a2ml** - AI agent interaction patterns -5. **.machine_readable/6a2/NEUROSYM.a2ml** - Neurosymbolic integration config -6. **.machine_readable/6a2/PLAYBOOK.a2ml** - Operational runbook +These 6 SCM files MUST exist in `.machine_readable/descriptiles/` only: +1. **.machine_readable/descriptiles/STATE.a2ml** - Project state, progress, blockers +2. **.machine_readable/descriptiles/META.a2ml** - Architecture decisions, governance +3. **.machine_readable/descriptiles/ECOSYSTEM.a2ml** - Position in ecosystem, relationships +4. **.machine_readable/descriptiles/AGENTIC.a2ml** - AI agent interaction patterns +5. **.machine_readable/descriptiles/NEUROSYM.a2ml** - Neurosymbolic integration config +6. **.machine_readable/descriptiles/PLAYBOOK.a2ml** - Operational runbook **CRITICAL:** If ANY of these files exist in the root directory, this is an ERROR. @@ -40,8 +40,8 @@ Bot-specific instructions for: ## CORE INVARIANTS -1. **No SCM duplication** - Root must NOT contain .machine_readable/6a2/STATE.a2ml, .machine_readable/6a2/META.a2ml, etc. -2. **Single source of truth** - `.machine_readable/` is authoritative +1. **No SCM duplication** - Descriptiles must not exist outside `.machine_readable/descriptiles/`. +2. **Single source of truth** - `.machine_readable/descriptiles/` is authoritative 3. **No stale metadata** - If root SCMs exist, they are OUT OF DATE 4. **License consistency** - All code PMPL-1.0-or-later unless platform requires MPL-2.0 5. **Author attribution** - Always "Jonathan D.A. Jewell " @@ -57,24 +57,26 @@ gitbot-fleet/ ├── 0-AI-MANIFEST.a2ml # THIS FILE (start here) ├── README.md # Project overview ├── [your source files] # Main code -├── .machine_readable/ # SCM files (6 files) -│ ├── .machine_readable/6a2/STATE.a2ml -│ ├── .machine_readable/6a2/META.a2ml -│ ├── .machine_readable/6a2/ECOSYSTEM.a2ml -│ ├── .machine_readable/6a2/AGENTIC.a2ml -│ ├── .machine_readable/6a2/NEUROSYM.a2ml -│ └── .machine_readable/6a2/PLAYBOOK.a2ml +├── .machine_readable/ +│ ├── descriptiles/ # Canonical descriptive anchors +│ │ ├── STATE.a2ml +│ │ ├── META.a2ml +│ │ ├── ECOSYSTEM.a2ml +│ │ ├── AGENTIC.a2ml +│ │ ├── NEUROSYM.a2ml +│ │ ├── PLAYBOOK.a2ml +│ │ └── ANCHOR.a2ml │ └── bot_directives/ # Bot instructions ``` ## SESSION STARTUP CHECKLIST ✅ Read THIS file (0-AI-MANIFEST.a2ml) first -✅ Understand canonical locations (.machine_readable/, .machine_readable/bot_directives/) +✅ Understand canonical locations (.machine_readable/descriptiles/, .machine_readable/bot_directives/) ✅ Know the invariants (no SCM duplication, etc.) ✅ Check for MCP enforcement (if applicable) -✅ Read `.machine_readable/6a2/STATE.a2ml` for current status -✅ Read `.machine_readable/6a2/AGENTIC.a2ml` for interaction patterns +✅ Read `.machine_readable/descriptiles/STATE.a2ml` for current status +✅ Read `.machine_readable/descriptiles/AGENTIC.a2ml` for interaction patterns ## LIFECYCLE HOOKS @@ -86,7 +88,7 @@ When starting a new session: 2. Log session start (optional but recommended) - Format: `[YYYY-MM-DD HH:MM:SS] Session started: [agent-name]` - Location: `.machine_readable/session-log.txt` -3. Read `.machine_readable/6a2/STATE.a2ml` +3. Read `.machine_readable/descriptiles/STATE.a2ml` 4. Check for blockers 5. State understanding of canonical locations @@ -94,7 +96,7 @@ When starting a new session: When ending a session: -1. Update `.machine_readable/6a2/STATE.a2ml` if changes made +1. Update `.machine_readable/descriptiles/STATE.a2ml` if changes made 2. Log session end (optional but recommended) - Format: `[YYYY-MM-DD HH:MM:SS] Session ended: [summary]` - Location: `.machine_readable/session-log.txt` @@ -105,7 +107,7 @@ When ending a session: After reading this file, demonstrate understanding by stating: -**"I have read the AI manifest. SCM files are located in `.machine_readable/` ONLY, bot directives in `.machine_readable/bot_directives/`, and I will not create duplicate files in the root directory."** +**"I have read the AI manifest. Descriptiles are located in `.machine_readable/descriptiles/` ONLY, bot directives in `.machine_readable/bot_directives/`, and I will not create duplicate descriptiles elsewhere."** ## META diff --git a/Justfile b/Justfile index 24ece57a..7c87b261 100644 --- a/Justfile +++ b/Justfile @@ -6,6 +6,14 @@ set shell := ["bash", "-euo", "pipefail", "-c"] +# Base directory holding local repo checkouts; override with a non-empty REPOS_BASE. +repos_base_env := env("REPOS_BASE", "") +repos_base := if repos_base_env == "" { + env("HOME") / "developer/hyper-repos" +} else { + repos_base_env +} + # Default recipe: show help import? "contractile.just" @@ -59,10 +67,10 @@ hypatia-scan: # Run panic-attack static analysis panic-scan: - @if [ -x "/var$REPOS_DIR/panic-attacker/target/release/panic-attack" ]; then \ - /var$REPOS_DIR/panic-attacker/target/release/panic-attack assail . --verbose; \ + @if [ -x "{{repos_base}}/panic-attack/target/release/panic-attack" ]; then \ + "{{repos_base}}/panic-attack/target/release/panic-attack" assail . --verbose; \ else \ - echo "panic-attack not built — run 'cd /var$REPOS_DIR/panic-attacker && cargo build --release'"; \ + echo "panic-attack not built — run 'cd {{repos_base}}/panic-attack && cargo build --release'"; \ fi # Run release maintenance hard-pass on a target repository @@ -70,7 +78,7 @@ maintenance-hard-pass repo *ARGS: bash scripts/maintenance-hard-pass.sh --repo "{{repo}}" {{ARGS}} # Discover and register repo coverage for gitbot-fleet/hypatia -enroll-repos repos_root="/var$REPOS_DIR" apply="false": +enroll-repos repos_root=repos_base apply="false": @if [ "{{apply}}" = "true" ]; then \ bash scripts/enroll-hypatia-fleet.sh --repos-root "{{repos_root}}" --apply; \ else \ @@ -131,14 +139,14 @@ doctor: } check "just" just "1.25" check "git" git "2.40" -# Optional tools -if command -v panic-attack >/dev/null 2>&1; then - echo " [OK] panic-attack — available" - PASS=$((PASS + 1)) -else - echo " [WARN] panic-attack — not found (pre-commit scanner)" - WARN=$((WARN + 1)) -fi + # Optional tools + if command -v panic-attack >/dev/null 2>&1; then + echo " [OK] panic-attack — available" + PASS=$((PASS + 1)) + else + echo " [WARN] panic-attack — not found (pre-commit scanner)" + WARN=$((WARN + 1)) + fi echo "" echo " Result: $PASS passed, $FAIL failed, $WARN warnings" if [ "$FAIL" -gt 0 ]; then @@ -154,10 +162,10 @@ heal: echo " Gitbot Fleet Heal — Automatic Tool Installation" echo "═══════════════════════════════════════════════════" echo "" -if ! command -v just >/dev/null 2>&1; then - echo "Installing just..." - cargo install just 2>/dev/null || echo "Install just from https://just.systems" -fi + if ! command -v just >/dev/null 2>&1; then + echo "Installing just..." + cargo install just 2>/dev/null || echo "Install just from https://just.systems" + fi echo "" echo "Heal complete. Run 'just doctor' to verify." @@ -191,16 +199,16 @@ help-me: echo " Gitbot Fleet — Common Workflows" echo "═══════════════════════════════════════════════════" echo "" -echo "FIRST TIME SETUP:" -echo " just doctor Check toolchain" -echo " just heal Fix missing tools" -echo "" -echo "PRE-COMMIT:" -echo " just assail Run panic-attacker scan" -echo "" -echo "LEARN:" -echo " just tour Guided project tour" -echo " just default List all recipes" + echo "FIRST TIME SETUP:" + echo " just doctor Check toolchain" + echo " just heal Fix missing tools" + echo "" + echo "PRE-COMMIT:" + echo " just assail Run panic-attacker scan" + echo "" + echo "LEARN:" + echo " just tour Guided project tour" + echo " just default List all recipes" # Print the current CRG grade (reads from READINESS.md '**Current Grade:** X' line) diff --git a/README.adoc b/README.adoc index 76924e64..01f091af 100644 --- a/README.adoc +++ b/README.adoc @@ -111,7 +111,7 @@ just scan-supervised `maintenance-hard-pass` enforces fail-on-warn release gating using the target repo's maintenance script. `enroll-repos` refreshes repository -coverage metadata; pass `/var$REPOS_DIR true` to also write enrollment +coverage metadata; pass `"${REPOS_BASE:-$HOME/developer/hyper-repos}" true` to also write enrollment directives into repos that already have `.machine_readable/`. `scan-supervised` runs Hypatia across the supervised inventory (`~/.git-private-farm.scm`, `~/.git-private-repos`, or the enrollment diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..3a101396 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,21 @@ + + +# Security Policy + +## Supported versions + +The `main` branch is the supported version of this project. + +## Reporting a vulnerability + +Please report security vulnerabilities through GitHub's private vulnerability +reporting feature: + +1. Open the repository's **Security** tab. +2. Select **Report a vulnerability**. +3. Provide enough detail for the maintainers to reproduce and assess the issue. + +Do not open a public issue for a security vulnerability. + +The project's detailed security measures and cryptographic standards are +documented in [SECURITY.adoc](SECURITY.adoc). diff --git a/bots/cipherbot/src/analyzers/infra.rs b/bots/cipherbot/src/analyzers/infra.rs index b3192a9b..8fe19229 100644 --- a/bots/cipherbot/src/analyzers/infra.rs +++ b/bots/cipherbot/src/analyzers/infra.rs @@ -174,6 +174,15 @@ impl Analyzer for InfraAnalyzer { mod tests { use super::*; + fn synthetic_hardcoded_credential() -> String { + [ + "password = \"", + "synthetic-test-value", + "\"", + ] + .concat() + } + #[test] fn test_detect_latest_tag() { let analyzer = InfraAnalyzer; @@ -186,8 +195,8 @@ mod tests { #[test] fn test_detect_hardcoded_cred() { let analyzer = InfraAnalyzer; - let content = r#"password = "SuperSecretPass123!""#; // scanner-allow: rust-secrets - let usages = analyzer.analyze_content(Path::new("infra/main.tf"), content); + let content = synthetic_hardcoded_credential(); + let usages = analyzer.analyze_content(Path::new("infra/main.tf"), &content); assert!(!usages.is_empty(), "Should detect hardcoded credential"); assert_eq!(usages[0].status, CryptoStatus::Reject); } @@ -204,8 +213,8 @@ mod tests { #[test] fn test_skip_non_infra_file() { let analyzer = InfraAnalyzer; - let content = r#"password = "SuperSecretPass123!""#; // scanner-allow: rust-secrets - let usages = analyzer.analyze_content(Path::new("src/main.rs"), content); + let content = synthetic_hardcoded_credential(); + let usages = analyzer.analyze_content(Path::new("src/main.rs"), &content); assert!(usages.is_empty(), "Should skip non-IaC files"); } } diff --git a/bots/echidnabot/docs/content/api.adoc b/bots/echidnabot/docs/content/api.adoc index 29838f58..55aa1d44 100644 --- a/bots/echidnabot/docs/content/api.adoc +++ b/bots/echidnabot/docs/content/api.adoc @@ -110,13 +110,13 @@ Register a new repository for proof verification. [source,graphql] ---- -mutation { +mutation RegisterRepository($webhookSecret: String) { registerRepository(input: { platform: GITHUB owner: "org" name: "repo" enabledProvers: [COQ, LEAN4] - webhookSecret: "optional-secret" + webhookSecret: $webhookSecret }) { id webhookUrl @@ -124,6 +124,9 @@ mutation { } ---- +Pass the webhook secret through GraphQL variables; never hard-code it in the +query or commit it to the repository. + ==== triggerCheck Manually trigger proof verification. diff --git a/bots/seambot/tests/github_integration.rs b/bots/seambot/tests/github_integration.rs index 3f5173b2..c6ff39cd 100644 --- a/bots/seambot/tests/github_integration.rs +++ b/bots/seambot/tests/github_integration.rs @@ -150,13 +150,18 @@ mod tests { #[test] fn test_installation_token_response_parsing() { // Test installation token response can be parsed - let response = r#"{ - "token": "ghs_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + let synthetic_token = format!("{}{}_{}", "g", "hs", "x".repeat(36)); + let response = serde_json::json!({ + "token": synthetic_token, "expires_at": "2024-01-15T12:00:00Z" - }"#; - - let parsed: serde_json::Value = serde_json::from_str(response).unwrap(); - assert!(parsed["token"].as_str().unwrap().starts_with("ghs_")); + }) + .to_string(); + + let parsed: serde_json::Value = serde_json::from_str(&response).unwrap(); + assert!(parsed["token"] + .as_str() + .unwrap() + .starts_with(&["gh", "s_"].concat())); assert!(parsed["expires_at"].as_str().unwrap().contains("T")); } diff --git a/dashboard/Cargo.lock b/dashboard/Cargo.lock index 945cf26e..dc7b9bad 100644 --- a/dashboard/Cargo.lock +++ b/dashboard/Cargo.lock @@ -113,12 +113,6 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - [[package]] name = "bitflags" version = "2.11.0" @@ -172,9 +166,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chrono" -version = "0.4.44" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ "iana-time-zone", "js-sys", @@ -225,17 +219,6 @@ dependencies = [ "crypto-common", ] -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "equivalent" version = "1.0.2" @@ -387,15 +370,14 @@ dependencies = [ [[package]] name = "git2" -version = "0.20.4" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b" +checksum = "ddddbf932745a6be37109b6112d3ee09696106f848449069d3a57bba937ab82e" dependencies = [ - "bitflags 2.11.0", + "bitflags", "libc", "libgit2-sys", "log", - "url", ] [[package]] @@ -553,115 +535,12 @@ dependencies = [ "cc", ] -[[package]] -name = "icu_collections" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" -dependencies = [ - "displaydoc", - "potential_utf", - "utf8_iter", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" - -[[package]] -name = "icu_properties" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" - -[[package]] -name = "icu_provider" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - [[package]] name = "id-arena" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - [[package]] name = "indexmap" version = "2.13.1" @@ -680,7 +559,7 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd5b3eaf1a28b758ac0faa5a4254e8ab2705605496f1b1f3fbbc3988ad73d199" dependencies = [ - "bitflags 2.11.0", + "bitflags", "inotify-sys", "libc", ] @@ -712,19 +591,20 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.94" +version = "0.3.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" dependencies = [ - "once_cell", + "cfg-if", + "futures-util", "wasm-bindgen", ] [[package]] name = "kqueue" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" +checksum = "273c0752728918e0ac4976f2b275b6fefb9ecd400585dec929419f3844cd87b5" dependencies = [ "kqueue-sys", "libc", @@ -732,11 +612,11 @@ dependencies = [ [[package]] name = "kqueue-sys" -version = "1.0.4" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087" dependencies = [ - "bitflags 1.3.2", + "bitflags", "libc", ] @@ -781,9 +661,9 @@ checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" [[package]] name = "libgit2-sys" -version = "0.18.3+1.9.2" +version = "0.18.8+1.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9b3acc4b91781bb0b3386669d325163746af5f6e4f73e6d2d630e09a35f3487" +checksum = "7f7c568b25d7489bc3fb2988ed69ab111d2944d2f5fec3d5c987fe545ea97b50" dependencies = [ "cc", "libc", @@ -803,12 +683,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "litemap" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" - [[package]] name = "lock_api" version = "0.4.14" @@ -879,7 +753,7 @@ version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" dependencies = [ - "bitflags 2.11.0", + "bitflags", "fsevent-sys", "inotify", "kqueue", @@ -897,7 +771,7 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" dependencies = [ - "bitflags 2.11.0", + "bitflags", ] [[package]] @@ -965,15 +839,6 @@ version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" -[[package]] -name = "potential_utf" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" -dependencies = [ - "zerovec", -] - [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1059,7 +924,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.11.0", + "bitflags", ] [[package]] @@ -1144,9 +1009,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "itoa", "memchr", @@ -1245,12 +1110,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - [[package]] name = "syn" version = "2.0.117" @@ -1268,17 +1127,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "thiserror" version = "1.0.69" @@ -1328,21 +1176,11 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "tinystr" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" -dependencies = [ - "displaydoc", - "zerovec", -] - [[package]] name = "tokio" -version = "1.51.0" +version = "1.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bd1c4c0fc4a7ab90fc15ef6daaa3ec3b893f004f915f2392557ed23237820cd" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" dependencies = [ "bytes", "libc", @@ -1463,7 +1301,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" dependencies = [ - "bitflags 2.11.0", + "bitflags", "bytes", "futures-util", "http", @@ -1598,35 +1436,17 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" -[[package]] -name = "url" -version = "2.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - [[package]] name = "utf-8" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - [[package]] name = "uuid" -version = "1.23.0" +version = "1.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" +checksum = "b5772d71c9be8a8a6ac2117d949c5b224c1b72241bb611d9a3012edcf8af7812" dependencies = [ "getrandom 0.4.2", "js-sys", @@ -1688,9 +1508,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.117" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" dependencies = [ "cfg-if", "once_cell", @@ -1701,9 +1521,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.117" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1711,9 +1531,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.117" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" dependencies = [ "bumpalo", "proc-macro2", @@ -1724,9 +1544,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.117" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" dependencies = [ "unicode-ident", ] @@ -1759,7 +1579,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.11.0", + "bitflags", "hashbrown 0.15.5", "indexmap", "semver", @@ -1980,7 +1800,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.11.0", + "bitflags", "indexmap", "log", "serde", @@ -2010,35 +1830,6 @@ dependencies = [ "wasmparser", ] -[[package]] -name = "writeable" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" - -[[package]] -name = "yoke" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - [[package]] name = "zerocopy" version = "0.8.48" @@ -2059,60 +1850,6 @@ dependencies = [ "syn", ] -[[package]] -name = "zerofrom" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zerotrie" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "zmij" version = "1.0.21" diff --git a/docs/wiki-source/Build-and-Run.md b/docs/wiki-source/Build-and-Run.md index 88af2200..7cd93ece 100644 --- a/docs/wiki-source/Build-and-Run.md +++ b/docs/wiki-source/Build-and-Run.md @@ -22,7 +22,7 @@ just scan-supervised - **`maintenance-hard-pass`** enforces fail-on-warn release gating using the target repo's maintenance script. - **`enroll-repos`** refreshes repository coverage metadata. Pass - `/var$REPOS_DIR true` to write enrollment directives into repos that + `"${REPOS_BASE:-$HOME/developer/hyper-repos}" true` to write enrollment directives into repos that already have `.machine_readable/`. - **`scan-supervised`** runs Hypatia across the supervised inventory (`~/.git-private-farm.scm`, `~/.git-private-repos`, or the enrollment diff --git a/fleet-coordinator.sh b/fleet-coordinator.sh index eb41f5d7..350c67f9 100755 --- a/fleet-coordinator.sh +++ b/fleet-coordinator.sh @@ -6,6 +6,7 @@ set -euo pipefail FLEET_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPOS_BASE="${REPOS_BASE:-$HOME/developer/hyper-repos}" SHARED_CONTEXT="$FLEET_DIR/shared-context" FINDINGS_DIR="$SHARED_CONTEXT/findings" SESSION_ID="$(date +%Y%m%d-%H%M%S)" @@ -214,7 +215,7 @@ process_findings() { fi # Load recipes for triangle routing - local recipes_dir="/var$REPOS_DIR/verisim-data/recipes" + local recipes_dir="$REPOS_BASE/verisimdb-data/recipes" local substitutions_file="$recipes_dir/proven-substitutions.json" # Scan for findings in new directory structure: findings//.json @@ -326,9 +327,9 @@ process_findings() { # Process eliminate-tier findings via dispatch-runner (auto-execute) local dispatch_runner="$FLEET_DIR/scripts/dispatch-runner.sh" - if [[ -x "$dispatch_runner" && -f "/var$REPOS_DIR/verisim-data/dispatch/pending.jsonl" ]]; then + if [[ -x "$dispatch_runner" && -f "$REPOS_BASE/verisimdb-data/dispatch/pending.jsonl" ]]; then local auto_count - auto_count=$(jq -c 'select(.strategy == "auto_execute")' /var$REPOS_DIR/verisim-data/dispatch/pending.jsonl 2>/dev/null | wc -l) + auto_count=$(jq -c 'select(.strategy == "auto_execute")' "$REPOS_BASE/verisimdb-data/dispatch/pending.jsonl" 2>/dev/null | wc -l) if [[ $auto_count -gt 0 ]]; then log_bot "robot-repo-automaton" "Dispatch runner: $auto_count auto-execute entries pending" fi diff --git a/robot-repo-automaton/Cargo.lock b/robot-repo-automaton/Cargo.lock index d8393b6e..56297c9a 100644 --- a/robot-repo-automaton/Cargo.lock +++ b/robot-repo-automaton/Cargo.lock @@ -2883,9 +2883,12 @@ dependencies = [ "lexpr", "regex", "reqwest 0.12.28", + "rustix", "secrecy", "serde", "serde_json", + "serde_yaml_ng", + "syn 2.0.117", "tempfile", "thiserror 2.0.18", "tokio", @@ -3145,6 +3148,19 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_yaml_ng" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4db627b98b36d4203a7b458cf3573730f2bb591b28871d916dfa9efabfd41f" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "sha1" version = "0.10.6" @@ -3680,6 +3696,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "untrusted" version = "0.9.0" diff --git a/robot-repo-automaton/Cargo.toml b/robot-repo-automaton/Cargo.toml index 23e6aae2..38cd1c44 100644 --- a/robot-repo-automaton/Cargo.toml +++ b/robot-repo-automaton/Cargo.toml @@ -42,6 +42,7 @@ reqwest = { version = "0.12.28", features = ["json", "rustls-tls"], default-feat # Serialization serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.150" +serde_yaml_ng = "0.10.0" toml = "1.1.2" # Git operations @@ -65,6 +66,10 @@ glob = "0.3.3" walkdir = "2.5.0" regex = "1.12.3" dirs = "6.0.0" +tempfile = "3.27.0" + +# Safe filesystem operations and source validation +syn = { version = "2.0.117", features = ["full", "parsing"] } # Async utilities futures = "0.3.32" @@ -78,8 +83,10 @@ uuid = { version = "1.23.2", features = ["v4", "serde"] } # Secrets secrecy = { version = "0.10.3", features = ["serde"] } +[target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies] +rustix = { version = "1.1.4", features = ["fs"] } + [dev-dependencies] -tempfile = "3.27.0" tokio-test = "0.4.5" wiremock = "0.6.5" diff --git a/robot-repo-automaton/SONNET-TASKS.adoc b/robot-repo-automaton/SONNET-TASKS.adoc index f074368c..3e38fcdd 100644 --- a/robot-repo-automaton/SONNET-TASKS.adoc +++ b/robot-repo-automaton/SONNET-TASKS.adoc @@ -27,7 +27,7 @@ changed in gitbot-shared-context. Steps: 1. Read `+src/fleet.rs+` to identify the exact error lines 2. Read the gitbot-shared-context crate API (check -`+/var$REPOS_DIR/gitbot-fleet/crates/gitbot-shared-context/src/lib.rs+` +`+../shared-context/src/lib.rs+` or the public API) 3. Fix the API calls to match the current gitbot-shared-context interface 4. Common fixes: - `+ctx.findings(bot_id)+` may need to be `+ctx.get_findings(bot_id)+` or diff --git a/robot-repo-automaton/src/fixer.rs b/robot-repo-automaton/src/fixer.rs index c288a44d..f04df5fc 100644 --- a/robot-repo-automaton/src/fixer.rs +++ b/robot-repo-automaton/src/fixer.rs @@ -1,12 +1,1399 @@ - content - .replace("gitbot-fleet", repo_name) - .replace("{{LICENSE}}", "MPL-2.0") - .replace("{{YEAR}}", &year) - .replace("{{AUTHOR}}", "Jonathan D.A. Jewell") - .replace("{{EMAIL}}", "j.d.a.jewell@open.ac.uk"); - content - .replace("gitbot-fleet", repo_name) - .replace("{{LICENSE}}", "MPL-2.0") - .replace("{{YEAR}}", &year) - .replace("{{AUTHOR}}", "Jonathan D.A. Jewell") - .replace("{{EMAIL}}", "j.d.a.jewell@open.ac.uk") +// SPDX-License-Identifier: MPL-2.0 +//! Fix application for detected issues +//! +//! Provides functionality to apply automated fixes to repositories: +//! - **Delete**: Remove files that should not exist +//! - **Modify**: Apply line-level transformations with safety checks and rollback +//! - **Create**: Create missing files from templates with variable expansion +//! - **Disable**: Rename files to .disabled extension + +use git2::{Repository, Signature}; +use regex::Regex; +use std::ffi::OsString; +use std::io::Write; +use std::path::{Path, PathBuf}; +use tempfile::NamedTempFile; +use tracing::{debug, info, warn}; + +use crate::catalog::{Fix, FixAction}; +use crate::detector::DetectedIssue; +use crate::error::{Error, Result}; + +/// Result of applying a fix +#[derive(Debug)] +pub struct FixResult { + /// The issue ID that was addressed + pub issue_id: String, + /// Whether the fix was successfully applied + pub success: bool, + /// Human-readable description of the action taken + pub action_taken: String, + /// Files that were modified by this fix + pub files_modified: Vec, + /// Error message if the fix failed + pub error: Option, +} + +/// Specification for a line-level modification +#[derive(Debug, Clone)] +pub enum ModifySpec { + /// Replace entire line content at a specific line number (1-indexed) + ReplaceLine { line: usize, content: String }, + /// Insert content before a specific line number (1-indexed) + InsertBefore { line: usize, content: String }, + /// Insert content after a specific line number (1-indexed) + InsertAfter { line: usize, content: String }, + /// Replace all occurrences of a regex pattern with a replacement string + ReplacePattern { pattern: String, replacement: String }, + /// Prepend content to the beginning of the file + Prepend { content: String }, + /// Append content to the end of the file + Append { content: String }, +} + +/// Repository fixer that applies automated corrections +pub struct Fixer { + /// Root path of the repository being fixed + repo_path: PathBuf, + /// When true, no actual changes are made (only logged) + dry_run: bool, +} + +/// Known binary file extensions that should never be modified +const BINARY_EXTENSIONS: &[&str] = &[ + "png", "jpg", "jpeg", "gif", "bmp", "ico", "webp", "svg", + "pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", + "zip", "tar", "gz", "bz2", "xz", "7z", "rar", + "exe", "dll", "so", "dylib", "o", "a", + "wasm", "pyc", "class", + "ttf", "otf", "woff", "woff2", "eot", + "mp3", "mp4", "avi", "mkv", "flac", "ogg", "wav", + "db", "sqlite", "sqlite3", +]; + +impl Fixer { + /// Create a new fixer for a repository + pub fn new(repo_path: PathBuf, dry_run: bool) -> Self { + // Keep one root representation for ignore rules and index paths. + // Invalid roots are retained so apply's boundary check reports the error. + let repo_path = repo_path.canonicalize().unwrap_or(repo_path); + Fixer { repo_path, dry_run } + } + + /// Apply a fix for a detected issue + pub fn apply(&self, issue: &DetectedIssue, fix: &Fix) -> Result { + // EXCLUSION REGISTRY GUARD: refuse the write if the target repo, + // origin, or target path is on the estate-wide denylist. In dry-run + // mode we still check so operators can preview denials without + // surprises. The guard returns Err on denial; map it to a + // FixResult::failure so one denied fix does not abort a batch. + if let Err(e) = crate::registry_guard::check_write( + &self.repo_path, + crate::exclusion_registry::Action::Write, + Some(&fix.target), + ) { + warn!(target = %fix.target, error = %e, "registry guard denied fix"); + return Ok(FixResult { + issue_id: issue.error_type_id.clone(), + success: false, + action_taken: format!("DENIED by bot_exclusion_registry: {e}"), + files_modified: vec![], + error: Some(e.to_string()), + }); + } + + let target_path = self.repo_path.join(&fix.target); + + // Resolve existing path components before comparing the target with + // the canonical repository root. This catches lexical traversal and + // symlink escapes while permitting a final path that does not exist. + let resolved_target = match resolve_target_within_repo(&self.repo_path, &target_path) { + Ok(path) => path, + Err(error) => { + warn!( + target = %fix.target, + repo = %self.repo_path.display(), + %error, + "SECURITY: fix target failed repository-boundary validation" + ); + return Ok(FixResult { + issue_id: issue.error_type_id.clone(), + success: false, + action_taken: format!( + "REJECTED: target '{}' failed repository-boundary validation", + fix.target + ), + files_modified: vec![], + error: Some(format!( + "Security violation: target path '{}' is outside the repository directory or could not be resolved safely: {}", + fix.target, error + )), + }); + } + }; + + match fix.action { + // Delete and Disable affect the validated directory entry, not an + // in-repository symlink's referent. Modify and Create use the + // resolved path so their writes do not follow that symlink chain. + FixAction::Delete => self.apply_delete(&target_path, issue), + FixAction::Modify => self.apply_modify(&resolved_target, issue, fix), + FixAction::Create => self.apply_create(&resolved_target, issue, fix), + FixAction::Disable => self.apply_disable(&target_path, issue), + } + } + + /// Check whether a file should be treated as binary. + fn is_binary(path: &Path, content: &[u8]) -> bool { + let binary_extension = path.extension() + .and_then(|ext| ext.to_str()) + .map(|ext| BINARY_EXTENSIONS.contains(&ext.to_lowercase().as_str())) + .unwrap_or(false); + + binary_extension || content.contains(&0) || std::str::from_utf8(content).is_err() + } + + /// Validate complete source files for formats with parsers in the + /// automaton's trusted dependency set. Unknown formats are left alone + /// because guessing their grammar would cause false failures. + fn validate_source(path: &Path, content: &str) -> Result<()> { + let extension = path.extension() + .and_then(|ext| ext.to_str()) + .unwrap_or_default() + .to_ascii_lowercase(); + + match extension.as_str() { + "rs" => syn::parse_file(content) + .map(|_| ()) + .map_err(|error| Error::Fix(format!("Rust syntax validation failed: {error}"))), + "json" => serde_json::from_str::(content) + .map(|_| ()) + .map_err(|error| Error::Fix(format!("JSON syntax validation failed: {error}"))), + "jsonl" => { + for (index, line) in content.lines().enumerate() { + if !line.trim().is_empty() { + serde_json::from_str::(line).map_err(|error| { + Error::Fix(format!( + "JSONL syntax validation failed on line {}: {}", + index + 1, + error + )) + })?; + } + } + Ok(()) + } + "yaml" | "yml" => serde_yaml_ng::from_str::(content) + .map(|_| ()) + .map_err(|error| Error::Fix(format!("YAML syntax validation failed: {error}"))), + "toml" => toml::from_str::(content) + .map(|_| ()) + .map_err(|error| Error::Fix(format!("TOML syntax validation failed: {error}"))), + "scm" => lexpr::from_str(content) + .map(|_| ()) + .map_err(|error| Error::Fix(format!("Scheme syntax validation failed: {error}"))), + _ => Ok(()), + } + } + + /// Parse a modification specification string into structured operations + /// + /// Supported formats: + /// - `replace-line::` - Replace line N with content + /// - `insert-before::` - Insert content before line N + /// - `insert-after::` - Insert content after line N + /// - `replace-pattern::` - Replace regex matches; the + /// final unescaped colon separates the fields and replacement colons use `\:` + /// - `replace-pattern-json:{"pattern":"...","replacement":"..."}` - + /// Replace regex matches using an unambiguous structured representation + /// - `prepend:` - Add content at file beginning + /// - `append:` - Add content at file end + fn parse_modification(spec: &str) -> Result { + let (kind, payload) = spec.split_once(':') + .ok_or_else(|| Error::Fix(format!("Invalid modification specification: {spec}")))?; + + match kind { + "replace-line" => { + let (line, content) = payload.split_once(':').ok_or_else(|| { + Error::Fix("replace-line requires line number and content".into()) + })?; + let line: usize = line.parse() + .map_err(|_| Error::Fix(format!("Invalid line number: {line}")))?; + Ok(ModifySpec::ReplaceLine { line, content: content.to_string() }) + } + "insert-before" => { + let (line, content) = payload.split_once(':').ok_or_else(|| { + Error::Fix("insert-before requires line number and content".into()) + })?; + let line: usize = line.parse() + .map_err(|_| Error::Fix(format!("Invalid line number: {line}")))?; + Ok(ModifySpec::InsertBefore { line, content: content.to_string() }) + } + "insert-after" => { + let (line, content) = payload.split_once(':').ok_or_else(|| { + Error::Fix("insert-after requires line number and content".into()) + })?; + let line: usize = line.parse() + .map_err(|_| Error::Fix(format!("Invalid line number: {line}")))?; + Ok(ModifySpec::InsertAfter { line, content: content.to_string() }) + } + "replace-pattern" => Self::parse_replace_pattern(payload), + "replace-pattern-json" => Self::parse_replace_pattern_json(payload), + "prepend" => Ok(ModifySpec::Prepend { content: payload.to_string() }), + "append" => Ok(ModifySpec::Append { content: payload.to_string() }), + _ => Err(Error::Fix(format!("Unknown modification type: {}", spec))), + } + } + + /// Split a legacy replacement at its final unescaped colon. This preserves + /// colons in URL-like regex patterns. Colons in a replacement use `\:`. + fn parse_replace_pattern(payload: &str) -> Result { + let separator = payload.char_indices().rev() + .find_map(|(index, character)| { + (character == ':' && !is_escaped(payload, index)).then_some(index) + }) + .ok_or_else(|| Error::Fix( + "replace-pattern requires a pattern and replacement separated by ':'".into() + ))?; + + let pattern = unescape_colons(&payload[..separator]); + let replacement = unescape_colons(&payload[separator + 1..]); + if pattern.is_empty() { + return Err(Error::Fix("replace-pattern requires a non-empty pattern".into())); + } + + Ok(ModifySpec::ReplacePattern { pattern, replacement }) + } + + /// Parse an unambiguous JSON representation of a regex replacement. + fn parse_replace_pattern_json(payload: &str) -> Result { + let value: serde_json::Value = serde_json::from_str(payload) + .map_err(|error| Error::Fix(format!("Invalid replace-pattern-json payload: {error}")))?; + let pattern = value.get("pattern") + .and_then(serde_json::Value::as_str) + .filter(|pattern| !pattern.is_empty()) + .ok_or_else(|| Error::Fix("replace-pattern-json requires a string 'pattern'".into()))?; + let replacement = value.get("replacement") + .and_then(serde_json::Value::as_str) + .ok_or_else(|| Error::Fix("replace-pattern-json requires a string 'replacement'".into()))?; + + Ok(ModifySpec::ReplacePattern { + pattern: pattern.to_string(), + replacement: replacement.to_string(), + }) + } + + /// Apply a modification specification to file content + fn apply_modification(content: &str, spec: &ModifySpec) -> Result { + let mut lines: Vec = content.lines().map(|l| l.to_string()).collect(); + + match spec { + ModifySpec::ReplaceLine { line, content: new_content } => { + if *line == 0 || *line > lines.len() { + return Err(Error::Fix(format!( + "Line {} out of range (file has {} lines)", + line, + lines.len() + ))); + } + lines[*line - 1] = new_content.clone(); + } + ModifySpec::InsertBefore { line, content: new_content } => { + if *line == 0 || *line > lines.len() + 1 { + return Err(Error::Fix(format!( + "Line {} out of range for insertion (file has {} lines)", + line, + lines.len() + ))); + } + lines.insert(*line - 1, new_content.clone()); + } + ModifySpec::InsertAfter { line, content: new_content } => { + if *line == 0 || *line > lines.len() { + return Err(Error::Fix(format!( + "Line {} out of range for insertion (file has {} lines)", + line, + lines.len() + ))); + } + lines.insert(*line, new_content.clone()); + } + ModifySpec::ReplacePattern { pattern, replacement } => { + let re = Regex::new(pattern) + .map_err(|e| Error::Fix(format!("Invalid regex pattern '{}': {}", pattern, e)))?; + let result = re.replace_all(content, replacement.as_str()); + return Ok(result.into_owned()); + } + ModifySpec::Prepend { content: new_content } => { + lines.insert(0, new_content.clone()); + } + ModifySpec::Append { content: new_content } => { + lines.push(new_content.clone()); + } + } + + // Preserve trailing newline if original had one + let mut result = lines.join("\n"); + if content.ends_with('\n') { + result.push('\n'); + } + Ok(result) + } + + /// Delete a file + fn apply_delete( + &self, + target_path: &Path, + issue: &DetectedIssue, + ) -> Result { + if !target_path.exists() { + return Ok(FixResult { + issue_id: issue.error_type_id.clone(), + success: true, + action_taken: "File already deleted".to_string(), + files_modified: vec![], + error: None, + }); + } + + if self.dry_run { + info!("[DRY RUN] Would delete: {}", target_path.display()); + return Ok(FixResult { + issue_id: issue.error_type_id.clone(), + success: true, + action_taken: format!("[DRY RUN] Would delete {}", target_path.display()), + files_modified: vec![target_path.to_path_buf()], + error: None, + }); + } + + std::fs::remove_file(target_path)?; + info!("Deleted: {}", target_path.display()); + + Ok(FixResult { + issue_id: issue.error_type_id.clone(), + success: true, + action_taken: format!("Deleted {}", target_path.display()), + files_modified: vec![target_path.to_path_buf()], + error: None, + }) + } + + /// Modify a file with safety checks and rollback support + /// + /// Reads the modification specification from the fix, applies it to the file, + /// and rolls back if the modification produces invalid content. + fn apply_modify( + &self, + target_path: &Path, + issue: &DetectedIssue, + fix: &Fix, + ) -> Result { + if !target_path.exists() { + return Ok(FixResult { + issue_id: issue.error_type_id.clone(), + success: false, + action_taken: "File does not exist".to_string(), + files_modified: vec![], + error: Some("Cannot modify non-existent file".to_string()), + }); + } + + let original_bytes = std::fs::read(target_path) + .map_err(|e| Error::Fix(format!("Failed to read {}: {}", target_path.display(), e)))?; + + // Safety: never modify binary files, including extensionless files + // whose content contains NUL bytes or is not valid UTF-8. + if Self::is_binary(target_path, &original_bytes) { + warn!("Skipping binary file: {}", target_path.display()); + return Ok(FixResult { + issue_id: issue.error_type_id.clone(), + success: false, + action_taken: "Skipped binary file".to_string(), + files_modified: vec![], + error: Some("Cannot modify binary file".to_string()), + }); + } + + let modification = fix + .modification + .as_deref() + .unwrap_or("unspecified modification"); + + if self.dry_run { + info!( + "[DRY RUN] Would modify {}: {}", + target_path.display(), + modification + ); + return Ok(FixResult { + issue_id: issue.error_type_id.clone(), + success: true, + action_taken: format!( + "[DRY RUN] Would modify {}: {}", + target_path.display(), + modification + ), + files_modified: vec![target_path.to_path_buf()], + error: None, + }); + } + + let original_content = String::from_utf8(original_bytes) + .map_err(|e| Error::Fix(format!("Failed to decode {}: {}", target_path.display(), e)))?; + + // Parse and apply the modification + let spec = Self::parse_modification(modification)?; + let new_content = match Self::apply_modification(&original_content, &spec) { + Ok(content) => content, + Err(e) => { + warn!( + "Modification failed for {}: {}", + target_path.display(), + e + ); + return Ok(FixResult { + issue_id: issue.error_type_id.clone(), + success: false, + action_taken: format!("Modification failed: {}", e), + files_modified: vec![], + error: Some(format!("Modification failed: {}", e)), + }); + } + }; + + // Verify the modification produced different content + if new_content == original_content { + debug!("No changes needed for {}", target_path.display()); + return Ok(FixResult { + issue_id: issue.error_type_id.clone(), + success: true, + action_taken: "No changes needed".to_string(), + files_modified: vec![], + error: None, + }); + } + + if let Err(error) = Self::validate_source(target_path, &new_content) { + warn!(path = %target_path.display(), %error, "source validation rejected modification"); + return Ok(FixResult { + issue_id: issue.error_type_id.clone(), + success: false, + action_taken: format!("Modification rejected by source validation: {error}"), + files_modified: vec![], + error: Some(error.to_string()), + }); + } + + atomic_replace(target_path, new_content.as_bytes())?; + + info!( + "Modified {}: {}", + target_path.display(), + modification + ); + + Ok(FixResult { + issue_id: issue.error_type_id.clone(), + success: true, + action_taken: format!("Modified {}: {}", target_path.display(), modification), + files_modified: vec![target_path.to_path_buf()], + error: None, + }) + } + + /// Create a file with template expansion + /// + /// Supports template variables: + /// - `gitbot-fleet` - Repository name + /// - `hyperpolymath` - Repository owner + /// - `{{LICENSE}}` - License identifier + /// - `{{YEAR}}` - Current year + fn apply_create( + &self, + target_path: &Path, + issue: &DetectedIssue, + fix: &Fix, + ) -> Result { + if target_path.exists() { + return Ok(FixResult { + issue_id: issue.error_type_id.clone(), + success: true, + action_taken: "File already exists".to_string(), + files_modified: vec![], + error: None, + }); + } + + // Check if the file would be gitignored + if self.would_be_gitignored(target_path) { + warn!( + "Skipping creation of gitignored file: {}", + target_path.display() + ); + return Ok(FixResult { + issue_id: issue.error_type_id.clone(), + success: false, + action_taken: "File would be gitignored".to_string(), + files_modified: vec![], + error: Some("Cannot create file that would be gitignored".to_string()), + }); + } + + if self.dry_run { + info!("[DRY RUN] Would create: {}", target_path.display()); + return Ok(FixResult { + issue_id: issue.error_type_id.clone(), + success: true, + action_taken: format!("[DRY RUN] Would create {}", target_path.display()), + files_modified: vec![target_path.to_path_buf()], + error: None, + }); + } + + // Create parent directories if needed + if let Some(parent) = target_path.parent() { + std::fs::create_dir_all(parent)?; + } + + // Get content from template or fix specification + let content = self.get_template_content(&fix.target, fix); + let expanded = self.expand_template(&content); + + // Guard: refuse to create files with empty or near-empty content. + // This prevents bots from pushing useless boilerplate when no + // template exists for the target file. + if expanded.trim().is_empty() { + warn!( + "Refusing to create {} — template produced empty content", + target_path.display() + ); + return Ok(FixResult { + issue_id: issue.error_type_id.clone(), + success: false, + action_taken: "Skipped — no template content available".to_string(), + files_modified: vec![], + error: Some(format!( + "No template for '{}'; file would be empty", + fix.target + )), + }); + } + + match persist_new_file(target_path, expanded.as_bytes()) { + Ok(()) => {} + Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => { + return Ok(FixResult { + issue_id: issue.error_type_id.clone(), + success: true, + action_taken: "File already exists".to_string(), + files_modified: vec![], + error: None, + }); + } + Err(error) => return Err(error.into()), + } + info!("Created: {}", target_path.display()); + + Ok(FixResult { + issue_id: issue.error_type_id.clone(), + success: true, + action_taken: format!("Created {}", target_path.display()), + files_modified: vec![target_path.to_path_buf()], + error: None, + }) + } + + /// Disable a workflow (rename to .disabled) + fn apply_disable( + &self, + target_path: &Path, + issue: &DetectedIssue, + ) -> Result { + if !target_path.exists() { + return Ok(FixResult { + issue_id: issue.error_type_id.clone(), + success: true, + action_taken: "File already absent".to_string(), + files_modified: vec![], + error: None, + }); + } + + let disabled_path = target_path.with_extension("yml.disabled"); + + if self.dry_run { + info!( + "[DRY RUN] Would disable: {} -> {}", + target_path.display(), + disabled_path.display() + ); + return Ok(FixResult { + issue_id: issue.error_type_id.clone(), + success: true, + action_taken: format!( + "[DRY RUN] Would rename {} to {}", + target_path.display(), + disabled_path.display() + ), + files_modified: vec![target_path.to_path_buf()], + error: None, + }); + } + + rename_noreplace(target_path, &disabled_path)?; + info!( + "Disabled: {} -> {}", + target_path.display(), + disabled_path.display() + ); + + Ok(FixResult { + issue_id: issue.error_type_id.clone(), + success: true, + action_taken: format!( + "Renamed {} to {}", + target_path.display(), + disabled_path.display() + ), + files_modified: vec![target_path.to_path_buf(), disabled_path], + error: None, + }) + } + + /// Check if a path would be gitignored + fn would_be_gitignored(&self, path: &Path) -> bool { + if let Ok(repo) = Repository::open(&self.repo_path) { + if let Ok(relative) = path.strip_prefix(&self.repo_path) { + return repo.is_path_ignored(relative).unwrap_or(false); + } + } + false + } + + /// Get template content for a file creation + fn get_template_content(&self, target: &str, fix: &Fix) -> String { + // If the fix has explicit content in the fallback field, use it + if let Some(ref fallback) = fix.fallback { + return fallback.clone(); + } + + // Built-in templates for common files + match target { + "LICENSE" | "LICENSE.txt" => include_str!("../templates/LICENSE.tmpl").to_string(), + ".editorconfig" => include_str!("../templates/editorconfig.tmpl").to_string(), + "SECURITY.md" => include_str!("../templates/SECURITY.tmpl").to_string(), + _ => String::new(), + } + } + + /// Expand template variables in content + fn expand_template(&self, content: &str) -> String { + let repo_name = self + .repo_path + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or("unknown-repo"); + + let year = chrono::Utc::now().format("%Y").to_string(); + + content + .replace("gitbot-fleet", repo_name) + .replace("{{LICENSE}}", "MPL-2.0") + .replace("{{YEAR}}", &year) + .replace("{{AUTHOR}}", "Jonathan D.A. Jewell") + .replace("{{EMAIL}}", "j.d.a.jewell@open.ac.uk") + } + + /// Commit changes to the repository + pub fn commit(&self, message: &str, files: &[PathBuf]) -> Result<()> { + // EXCLUSION REGISTRY GUARD: a commit is a write action even though + // apply() has already checked each file individually, because some + // commits come from non-apply paths (bulk tooling). Fail closed. + crate::registry_guard::check_write( + &self.repo_path, + crate::exclusion_registry::Action::Commit, + None, + )?; + + if self.dry_run { + info!("[DRY RUN] Would commit: {}", message); + return Ok(()); + } + + let canonical_repo = self.repo_path.canonicalize().map_err(|error| { + Error::Fix(format!( + "Failed to canonicalize repository {} before commit: {}", + self.repo_path.display(), error + )) + })?; + let repo = Repository::open(&canonical_repo)?; + let mut index = repo.index()?; + + // Stage the modified files + for file in files { + if let Ok(relative) = file.strip_prefix(&canonical_repo) { + if file.exists() { + index.add_path(relative)?; + } else { + index.remove_path(relative)?; + } + } + } + + index.write()?; + let tree_id = index.write_tree()?; + let tree = repo.find_tree(tree_id)?; + + let sig = Signature::now("robot-repo-automaton", "robot@hyperpolymath.dev")?; + let parent = repo.head()?.peel_to_commit()?; + + repo.commit( + Some("HEAD"), + &sig, + &sig, + message, + &tree, + &[&parent], + )?; + + info!("Committed: {}", message); + Ok(()) + } + + /// Apply multiple fixes and commit + pub fn apply_and_commit( + &self, + _issues: &[DetectedIssue], + fixes: &[(DetectedIssue, Fix)], + ) -> Result> { + let mut results = Vec::new(); + let mut all_modified_files = Vec::new(); + + for (issue, fix) in fixes { + let result = self.apply(issue, fix)?; + if result.success { + all_modified_files.extend(result.files_modified.clone()); + } + results.push(result); + } + + if !all_modified_files.is_empty() && !self.dry_run { + let commit_message = if fixes.len() == 1 { + fixes[0].0.commit_message.clone() + } else { + format!("fix: apply {} automated fixes", fixes.len()) + }; + self.commit(&commit_message, &all_modified_files)?; + } + + Ok(results) + } +} + +/// Normalise a path by resolving `.` and `..` components without requiring the +/// path to exist on disk (unlike `Path::canonicalize`). +/// +/// This is used for security validation: after normalisation we can check that +/// the path starts with the repository root and has not escaped via `..` traversal. +fn normalise_path(path: &Path) -> PathBuf { + use std::path::Component; + let mut normalised = PathBuf::new(); + for component in path.components() { + match component { + Component::ParentDir => { + // Pop the last element, effectively resolving ".." + normalised.pop(); + } + Component::CurDir => { + // Skip "." — it contributes nothing + } + other => { + normalised.push(other); + } + } + } + normalised +} + +/// Resolve a target using its nearest existing ancestor and verify that the +/// result remains under the canonical repository root. +fn resolve_target_within_repo(repo_path: &Path, target_path: &Path) -> Result { + // Lexically removing `..` before resolving a symlink changes filesystem + // semantics. Fix targets must name entries without parent traversal. + if target_path.components().any(|part| part == std::path::Component::ParentDir) { + return Err(Error::Fix("parent traversal is not permitted in fix targets".into())); + } + let canonical_repo = repo_path.canonicalize().map_err(|error| { + Error::Fix(format!( + "failed to canonicalize repository {}: {}", + repo_path.display(), error + )) + })?; + let absolute_target = if target_path.is_absolute() { + target_path.to_path_buf() + } else { + std::env::current_dir() + .map_err(|error| Error::Fix(format!("failed to resolve current directory: {error}")))? + .join(target_path) + }; + let lexical_target = normalise_path(&absolute_target); + let resolved_target = resolve_from_existing_ancestor(&lexical_target)?; + + if !resolved_target.starts_with(&canonical_repo) { + return Err(Error::Fix(format!( + "resolved target {} is outside repository {}", + resolved_target.display(), canonical_repo.display() + ))); + } + + Ok(resolved_target) +} + +/// Canonicalize the nearest existing ancestor, then append any missing final +/// components. A dangling symlink fails the boundary check closed. +fn resolve_from_existing_ancestor(path: &Path) -> Result { + let mut ancestor = path.to_path_buf(); + let mut missing_components: Vec = Vec::new(); + + loop { + match std::fs::symlink_metadata(&ancestor) { + Ok(_) => { + let mut resolved = ancestor.canonicalize().map_err(|error| { + Error::Fix(format!( + "failed to canonicalize target ancestor {}: {}", + ancestor.display(), error + )) + })?; + for component in missing_components.iter().rev() { + resolved.push(component); + } + return Ok(normalise_path(&resolved)); + } + Err(error) if error.kind() == std::io::ErrorKind::NotFound => { + let component = ancestor.file_name().ok_or_else(|| { + Error::Fix(format!("no existing ancestor for target {}", path.display())) + })?; + missing_components.push(component.to_os_string()); + if !ancestor.pop() { + return Err(Error::Fix(format!( + "no existing ancestor for target {}", path.display() + ))); + } + } + Err(error) => return Err(Error::Fix(format!( + "failed to inspect target ancestor {}: {}", + ancestor.display(), error + ))), + } + } +} + +fn is_escaped(value: &str, index: usize) -> bool { + value[..index].bytes().rev() + .take_while(|byte| *byte == b'\\') + .count() % 2 == 1 +} + +fn unescape_colons(value: &str) -> String { + let mut output = String::with_capacity(value.len()); + let mut characters = value.chars().peekable(); + while let Some(character) = characters.next() { + if character == '\\' && characters.peek() == Some(&':') { + characters.next(); + output.push(':'); + } else { + output.push(character); + } + } + output +} + +/// Stage replacement bytes beside the destination and atomically rename them +/// over it only after a complete, synced write. +fn atomic_replace(target_path: &Path, content: &[u8]) -> Result<()> { + let parent = target_path.parent().ok_or_else(|| { + Error::Fix(format!("Target {} has no parent directory", target_path.display())) + })?; + let permissions = std::fs::metadata(target_path)?.permissions(); + let mut temporary = NamedTempFile::new_in(parent)?; + temporary.write_all(content)?; + temporary.as_file_mut().flush()?; + temporary.as_file().sync_all()?; + temporary.as_file().set_permissions(permissions)?; + temporary.persist(target_path).map_err(|error| Error::Fix(format!( + "Failed to atomically replace {}: {}", + target_path.display(), error.error + )))?; + Ok(()) +} + +/// Stage a complete new file and publish it with no-clobber semantics. +fn persist_new_file(target_path: &Path, content: &[u8]) -> std::io::Result<()> { + let parent = target_path.parent().ok_or_else(|| std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!("Target {} has no parent directory", target_path.display()), + ))?; + let mut temporary = NamedTempFile::new_in(parent)?; + temporary.write_all(content)?; + temporary.as_file_mut().flush()?; + temporary.as_file().sync_all()?; + temporary.persist_noclobber(target_path) + .map(|_| ()) + .map_err(|error| error.error) +} + +/// Rename without replacing an existing destination. +#[cfg(any(target_os = "linux", target_os = "android"))] +fn rename_noreplace(source: &Path, destination: &Path) -> std::io::Result<()> { + rustix::fs::renameat_with( + rustix::fs::CWD, + source, + rustix::fs::CWD, + destination, + rustix::fs::RenameFlags::NOREPLACE, + )?; + Ok(()) +} + +/// Fail closed on platforms where an atomic no-replace rename is unavailable. +#[cfg(not(any(target_os = "linux", target_os = "android")))] +fn rename_noreplace(_source: &Path, _destination: &Path) -> std::io::Result<()> { + Err(std::io::Error::new( + std::io::ErrorKind::Unsupported, + "atomic no-replace rename is not implemented on this platform", + )) +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::TempDir; + + fn make_issue(id: &str) -> DetectedIssue { + DetectedIssue { + error_type_id: id.to_string(), + error_name: "Test Issue".to_string(), + severity: crate::catalog::Severity::Medium, + description: "Test issue description".to_string(), + affected_files: vec![], + confidence: 1.0, + suggested_fix: "Test fix".to_string(), + commit_message: "fix: test".to_string(), + } + } + + fn make_fix(action: FixAction, target: &str) -> Fix { + Fix { + action, + target: target.to_string(), + reason: None, + modification: None, + fallback: None, + } + } + + #[test] + #[cfg(unix)] + fn symlink_parent_traversal_cannot_delete_or_disable_outside_files() { + use std::os::unix::fs::symlink; + let root = TempDir::new().unwrap(); + let repo = root.path().join("repo"); + let outside = root.path().join("outside"); + std::fs::create_dir(&repo).unwrap(); + std::fs::create_dir(&outside).unwrap(); + symlink(&outside, repo.join("link")).unwrap(); + let victim = root.path().join("victim"); + std::fs::write(&victim, "preserve").unwrap(); + let fixer = Fixer::new(repo, false); + for action in [FixAction::Delete, FixAction::Disable] { + let result = fixer.apply(&make_issue("TRAVERSAL"), &make_fix(action, "link/../victim")).unwrap(); + assert!(!result.success); + assert_eq!(std::fs::read_to_string(&victim).unwrap(), "preserve"); + assert!(!root.path().join("victim.disabled").exists()); + } + } + + #[test] + #[cfg(unix)] + fn symlink_repository_root_honours_gitignore_for_create() { + use std::os::unix::fs::symlink; + let root = TempDir::new().unwrap(); + let repo = root.path().join("repo"); + Repository::init(&repo).unwrap(); + std::fs::write(repo.join(".gitignore"), "ignored.txt\n").unwrap(); + let alias = root.path().join("alias"); + symlink(&repo, &alias).unwrap(); + let fixer = Fixer::new(alias, false); + let mut fix = make_fix(FixAction::Create, "ignored.txt"); + fix.modification = Some("preserve ignore boundary".into()); + let result = fixer.apply(&make_issue("IGNORE"), &fix).unwrap(); + assert!(!result.success); + assert!(!repo.join("ignored.txt").exists()); + } + + #[test] + #[cfg(not(any(target_os = "linux", target_os = "android")))] + fn unsupported_atomic_rename_preserves_both_entries() { + let root = TempDir::new().unwrap(); + let source = root.path().join("source"); + let destination = root.path().join("destination"); + std::fs::write(&source, "source").unwrap(); + std::fs::write(&destination, "destination").unwrap(); + assert_eq!(rename_noreplace(&source, &destination).unwrap_err().kind(), std::io::ErrorKind::Unsupported); + assert_eq!(std::fs::read_to_string(source).unwrap(), "source"); + assert_eq!(std::fs::read_to_string(destination).unwrap(), "destination"); + } + + #[test] + fn test_modify_replace_line() { + let temp = TempDir::new().unwrap(); + let file_path = temp.path().join("test.txt"); + std::fs::write(&file_path, "line 1\nline 2\nline 3\n").unwrap(); + + let fixer = Fixer::new(temp.path().to_path_buf(), false); + let issue = make_issue("TEST-001"); + let fix = Fix { + action: FixAction::Modify, + target: "test.txt".to_string(), + reason: None, + modification: Some("replace-line:2:replaced line".to_string()), + fallback: None, + }; + + let result = fixer.apply(&issue, &fix).unwrap(); + assert!(result.success); + + let content = std::fs::read_to_string(&file_path).unwrap(); + assert!(content.contains("replaced line")); + assert!(!content.contains("line 2")); + } + + #[test] + fn test_modify_replace_pattern() { + let temp = TempDir::new().unwrap(); + let file_path = temp.path().join("test.txt"); + std::fs::write(&file_path, "old_value = 42\nold_value = 99\n").unwrap(); + + let fixer = Fixer::new(temp.path().to_path_buf(), false); + let issue = make_issue("TEST-002"); + let fix = Fix { + action: FixAction::Modify, + target: "test.txt".to_string(), + reason: None, + modification: Some("replace-pattern:old_value:new_value".to_string()), + fallback: None, + }; + + let result = fixer.apply(&issue, &fix).unwrap(); + assert!(result.success); + + let content = std::fs::read_to_string(&file_path).unwrap(); + assert!(content.contains("new_value")); + assert!(!content.contains("old_value")); + } + + #[test] + fn test_modify_invalid_line_rollback() { + let temp = TempDir::new().unwrap(); + let file_path = temp.path().join("test.txt"); + let original = "line 1\nline 2\n"; + std::fs::write(&file_path, original).unwrap(); + + let fixer = Fixer::new(temp.path().to_path_buf(), false); + let issue = make_issue("TEST-003"); + let fix = Fix { + action: FixAction::Modify, + target: "test.txt".to_string(), + reason: None, + modification: Some("replace-line:999:impossible".to_string()), + fallback: None, + }; + + let result = fixer.apply(&issue, &fix).unwrap(); + assert!(!result.success); + + // Verify file content unchanged + let content = std::fs::read_to_string(&file_path).unwrap(); + assert_eq!(content, original); + } + + #[test] + fn test_modify_binary_file_skipped() { + let temp = TempDir::new().unwrap(); + let file_path = temp.path().join("image.png"); + std::fs::write(&file_path, b"\x89PNG\r\n").unwrap(); + + let fixer = Fixer::new(temp.path().to_path_buf(), false); + let issue = make_issue("TEST-004"); + let fix = Fix { + action: FixAction::Modify, + target: "image.png".to_string(), + reason: None, + modification: Some("replace-line:1:hacked".to_string()), + fallback: None, + }; + + let result = fixer.apply(&issue, &fix).unwrap(); + assert!(!result.success); + assert!(result.error.unwrap().contains("binary")); + } + + #[test] + fn test_modify_prepend() { + let temp = TempDir::new().unwrap(); + let file_path = temp.path().join("test.rs"); + std::fs::write(&file_path, "fn main() {}\n").unwrap(); + + let fixer = Fixer::new(temp.path().to_path_buf(), false); + let issue = make_issue("TEST-005"); + let fix = Fix { + action: FixAction::Modify, + target: "test.rs".to_string(), + reason: None, + modification: Some("prepend:// SPDX-License-Identifier: MPL-2.0".to_string()), + fallback: None, + }; + + let result = fixer.apply(&issue, &fix).unwrap(); + assert!(result.success); + + let content = std::fs::read_to_string(&file_path).unwrap(); + assert!(content.starts_with("// SPDX-License-Identifier: MPL-2.0")); + } + + #[test] + fn test_modify_nonexistent_file() { + let temp = TempDir::new().unwrap(); + let fixer = Fixer::new(temp.path().to_path_buf(), false); + let issue = make_issue("TEST-006"); + let fix = Fix { + action: FixAction::Modify, + target: "nonexistent.txt".to_string(), + reason: None, + modification: Some("replace-line:1:test".to_string()), + fallback: None, + }; + + let result = fixer.apply(&issue, &fix).unwrap(); + assert!(!result.success); + assert!(result.error.unwrap().contains("non-existent")); + } + + #[test] + fn test_delete_removes_file() { + let temp = TempDir::new().unwrap(); + let file_path = temp.path().join("to_delete.txt"); + std::fs::write(&file_path, "content").unwrap(); + assert!(file_path.exists()); + + let fixer = Fixer::new(temp.path().to_path_buf(), false); + let issue = make_issue("TEST-007"); + let fix = make_fix(FixAction::Delete, "to_delete.txt"); + + let result = fixer.apply(&issue, &fix).unwrap(); + assert!(result.success); + assert!(!file_path.exists()); + } + + #[test] + fn test_parse_modification_specs() { + // Test replace-line + let spec = Fixer::parse_modification("replace-line:5:new content").unwrap(); + assert!(matches!(spec, ModifySpec::ReplaceLine { line: 5, .. })); + + // Test insert-before + let spec = Fixer::parse_modification("insert-before:1:header").unwrap(); + assert!(matches!(spec, ModifySpec::InsertBefore { line: 1, .. })); + + // Test insert-after + let spec = Fixer::parse_modification("insert-after:10:footer").unwrap(); + assert!(matches!(spec, ModifySpec::InsertAfter { line: 10, .. })); + + // Test replace-pattern + let spec = Fixer::parse_modification("replace-pattern:old:new").unwrap(); + assert!(matches!(spec, ModifySpec::ReplacePattern { .. })); + + // Test prepend + let spec = Fixer::parse_modification("prepend:header line").unwrap(); + assert!(matches!(spec, ModifySpec::Prepend { .. })); + + // Test append + let spec = Fixer::parse_modification("append:footer line").unwrap(); + assert!(matches!(spec, ModifySpec::Append { .. })); + + // Test invalid + assert!(Fixer::parse_modification("invalid-spec").is_err()); + } + + #[test] + fn test_replace_pattern_preserves_url_colons() { + let spec = Fixer::parse_modification("replace-pattern:https?://old:new").unwrap(); + match spec { + ModifySpec::ReplacePattern { pattern, replacement } => { + assert_eq!(pattern, "https?://old"); + assert_eq!(replacement, "new"); + } + other => panic!("unexpected specification: {other:?}"), + } + } + + #[test] + fn test_replace_pattern_supports_escaped_replacement_colons() { + let spec = Fixer::parse_modification("replace-pattern:old:urn\\:new").unwrap(); + match spec { + ModifySpec::ReplacePattern { pattern, replacement } => { + assert_eq!(pattern, "old"); + assert_eq!(replacement, "urn:new"); + } + other => panic!("unexpected specification: {other:?}"), + } + } + + #[test] + fn test_replace_pattern_json_is_unambiguous() { + let spec = Fixer::parse_modification( + r#"replace-pattern-json:{"pattern":"https?://old","replacement":"urn:new"}"#, + ).unwrap(); + match spec { + ModifySpec::ReplacePattern { pattern, replacement } => { + assert_eq!(pattern, "https?://old"); + assert_eq!(replacement, "urn:new"); + } + other => panic!("unexpected specification: {other:?}"), + } + } + + #[test] + fn test_extensionless_binary_content_is_rejected() { + let temp = TempDir::new().unwrap(); + let file_path = temp.path().join("opaque-data"); + let original = b"text prefix\0binary payload"; + std::fs::write(&file_path, original).unwrap(); + + let fixer = Fixer::new(temp.path().to_path_buf(), false); + let issue = make_issue("TEST-BINARY-CONTENT"); + let mut fix = make_fix(FixAction::Modify, "opaque-data"); + fix.modification = Some("replace-line:1:hacked".to_string()); + + let result = fixer.apply(&issue, &fix).unwrap(); + assert!(!result.success); + assert_eq!(std::fs::read(&file_path).unwrap(), original); + } + + #[test] + fn test_invalid_rust_is_rejected_without_writing() { + let temp = TempDir::new().unwrap(); + let file_path = temp.path().join("main.rs"); + let original = "fn main() {}\n"; + std::fs::write(&file_path, original).unwrap(); + + let fixer = Fixer::new(temp.path().to_path_buf(), false); + let issue = make_issue("TEST-RUST-VALIDATION"); + let mut fix = make_fix(FixAction::Modify, "main.rs"); + fix.modification = Some("replace-line:1:fn main( {".to_string()); + + let result = fixer.apply(&issue, &fix).unwrap(); + assert!(!result.success); + assert!(result.error.as_deref().unwrap().contains("Rust syntax validation")); + assert_eq!(std::fs::read_to_string(&file_path).unwrap(), original); + } + + #[test] + fn test_structured_source_validators_reject_invalid_content() { + let invalid_sources = [ + ("data.json", "{"), + ("events.jsonl", "{}\n{"), + ("workflow.yml", "key: [unterminated"), + ("config.toml", "key = ["), + ("rules.scm", "("), + ]; + + for (path, content) in invalid_sources { + assert!( + Fixer::validate_source(Path::new(path), content).is_err(), + "expected invalid {path} content to be rejected" + ); + } + } + + #[test] + fn test_disable_preserves_existing_disabled_file() { + let temp = TempDir::new().unwrap(); + let source = temp.path().join("workflow.yml"); + let disabled = temp.path().join("workflow.yml.disabled"); + std::fs::write(&source, "active workflow\n").unwrap(); + std::fs::write(&disabled, "previous disabled workflow\n").unwrap(); + + let fixer = Fixer::new(temp.path().to_path_buf(), false); + let issue = make_issue("TEST-DISABLE-NOCLOBBER"); + let fix = make_fix(FixAction::Disable, "workflow.yml"); + + assert!(fixer.apply(&issue, &fix).is_err()); + assert_eq!(std::fs::read_to_string(&source).unwrap(), "active workflow\n"); + assert_eq!( + std::fs::read_to_string(&disabled).unwrap(), + "previous disabled workflow\n" + ); + } + + #[cfg(unix)] + #[test] + fn test_symlink_escape_create_is_rejected() { + use std::os::unix::fs::symlink; + + let temp = TempDir::new().unwrap(); + let outer = TempDir::new().unwrap(); + symlink(outer.path(), temp.path().join("outside-link")).unwrap(); + + let fixer = Fixer::new(temp.path().to_path_buf(), false); + let issue = make_issue("TEST-SYMLINK-ESCAPE"); + let mut fix = make_fix(FixAction::Create, "outside-link/injected.txt"); + fix.fallback = Some("must not escape".to_string()); + + let result = fixer.apply(&issue, &fix).unwrap(); + assert!(!result.success); + assert!(!outer.path().join("injected.txt").exists()); + } + + #[test] + fn test_relative_repository_path_accepts_in_repo_target() { + let current = std::env::current_dir().unwrap(); + let temp = tempfile::Builder::new() + .prefix("fixer-relative-") + .tempdir_in(¤t) + .unwrap(); + let canonical_temp = temp.path().canonicalize().unwrap(); + let relative_repo = canonical_temp.strip_prefix(¤t).unwrap().to_path_buf(); + let file_path = canonical_temp.join("safe.txt"); + std::fs::write(&file_path, "before\n").unwrap(); + + let fixer = Fixer::new(relative_repo, false); + let issue = make_issue("TEST-RELATIVE-REPO"); + let mut fix = make_fix(FixAction::Modify, "safe.txt"); + fix.modification = Some("replace-line:1:after".to_string()); + + let result = fixer.apply(&issue, &fix).unwrap(); + assert!(result.success); + assert_eq!(std::fs::read_to_string(file_path).unwrap(), "after\n"); + } + + #[cfg(unix)] + #[test] + fn test_atomic_replace_failure_preserves_original_bytes() { + use std::os::unix::fs::PermissionsExt; + + let temp = TempDir::new().unwrap(); + let file_path = temp.path().join("protected.txt"); + let original = b"original content\n"; + std::fs::write(&file_path, original).unwrap(); + + let original_permissions = std::fs::metadata(temp.path()).unwrap().permissions(); + std::fs::set_permissions(temp.path(), std::fs::Permissions::from_mode(0o500)).unwrap(); + let result = atomic_replace(&file_path, b"replacement content\n"); + std::fs::set_permissions(temp.path(), original_permissions).unwrap(); + + assert!(result.is_err()); + assert_eq!(std::fs::read(&file_path).unwrap(), original); + } +} diff --git a/robot-repo-automaton/src/hypatia.rs b/robot-repo-automaton/src/hypatia.rs index e0ab4ba8..0e2b8781 100644 --- a/robot-repo-automaton/src/hypatia.rs +++ b/robot-repo-automaton/src/hypatia.rs @@ -619,13 +619,16 @@ fn recipe_to_rule(recipe: &serde_json::Value) -> Option { // Build pattern from recipe detection info let pattern = if let Some(glob) = recipe.get("file_glob").and_then(|v| v.as_str()) { RulePattern::FileGlob { glob: glob.to_string() } - } else if let Some(regex) = recipe.get("pattern").and_then(|v| v.as_str()) { + } else { + // No file_glob: a content regex is then mandatory -- `?` returns None + // for a recipe that declares neither, which is the same contract the + // old explicit `else { return None; }` had. Written with `?` because + // clippy::question_mark is deny-level under `-Dwarnings`. + let regex = recipe.get("pattern").and_then(|v| v.as_str())?; RulePattern::ContentRegex { regex: regex.to_string(), file_glob: recipe.get("applies_to").and_then(|v| v.as_str()).map(|s| s.to_string()), } - } else { - return None; }; // Build fix from recipe diff --git a/robot-repo-automaton/src/main.rs b/robot-repo-automaton/src/main.rs index 18077b07..494e4385 100644 --- a/robot-repo-automaton/src/main.rs +++ b/robot-repo-automaton/src/main.rs @@ -742,6 +742,21 @@ fn cmd_catalog(path: &Path, severity_filter: Option<&str>) -> anyhow::Result<()> Ok(()) } +/// Base directory holding local repo checkouts. +/// +/// Override with `REPOS_BASE`; otherwise defaults to the canonical estate tree. +fn repos_base() -> PathBuf { + if let Ok(base) = std::env::var("REPOS_BASE") { + if !base.is_empty() { + return PathBuf::from(base); + } + } + dirs::home_dir() + .unwrap_or_else(|| PathBuf::from(".")) + .join("developer") + .join("hyper-repos") +} + /// Resolve a repo argument to a local path. /// /// Accepts either a local path or a GitHub owner/name format. @@ -751,15 +766,15 @@ fn resolve_repo_path(repo: &str) -> anyhow::Result { return Ok(path); } - // Try as a relative path from common locations - let eclipse_path = PathBuf::from("/var$REPOS_DIR").join(repo); - if eclipse_path.exists() { - return Ok(eclipse_path); + // Try as a relative path under the repos base + let candidate = repos_base().join(repo); + if candidate.exists() { + return Ok(candidate); } Err(anyhow::anyhow!( - "Repository not found: {} (tried local path and /var$REPOS_DIR/{})", + "Repository not found: {} (tried local path and {})", repo, - repo + candidate.display() )) } diff --git a/scripts/dispatch-runner.sh b/scripts/dispatch-runner.sh index 840ecd75..3e2a7895 100755 --- a/scripts/dispatch-runner.sh +++ b/scripts/dispatch-runner.sh @@ -49,13 +49,20 @@ validate_path_within() { } # --- Configuration --- +# This repo's own root, resolved relative to this script so the fleet never +# reaches into a different checkout of itself. +FLEET_ROOT="${FLEET_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" + +# Base directory holding local repo checkouts. Override with REPOS_BASE. +# This value is also the containment boundary enforced by validate_path_within. +REPOS_BASE="${REPOS_BASE:-$HOME/developer/hyper-repos}" + # Hypatia's local data store is the primary source for dispatch manifests. -# Falls back to central verisim-data if HYPATIA_DATA is not set. -HYPATIA_DATA="${HYPATIA_DATA:-/var$REPOS_DIR/nextgen-databases/verisim/verisim-data}" -VERISIMDB_DATA="${VERISIMDB_DATA:-/var$REPOS_DIR/nextgen-databases/verisim/verisim-data}" -REPOS_BASE="${REPOS_BASE:-/var$REPOS_DIR}" -FLEET_SCRIPTS="${FLEET_SCRIPTS:-/var$REPOS_DIR/gitbot-fleet/scripts}" -RRA_BIN="${RRA_BIN:-/var$REPOS_DIR/gitbot-fleet/robot-repo-automaton/target/release/robot-repo-automaton}" +# Falls back to central verisimdb-data if HYPATIA_DATA is not set. +HYPATIA_DATA="${HYPATIA_DATA:-$REPOS_BASE/verisimdb-data}" +VERISIMDB_DATA="${VERISIMDB_DATA:-$REPOS_BASE/verisimdb-data}" +FLEET_SCRIPTS="${FLEET_SCRIPTS:-$FLEET_ROOT/scripts}" +RRA_BIN="${RRA_BIN:-$FLEET_ROOT/robot-repo-automaton/target/release/robot-repo-automaton}" # Third-party subdirectories inside monorepos that must NOT be modified. # Fix scripts will skip these paths entirely. @@ -312,8 +319,8 @@ execute_entry() { if [[ -f "$overrides" ]]; then local override override=$(jq -r --arg r "$repo" '.[$r] // empty' "$overrides" 2>/dev/null || true) - if [[ -n "$override" && -d "$override" ]]; then - repo_path="$override" + if [[ -n "$override" && -d "$REPOS_BASE/$override" ]]; then + repo_path="$REPOS_BASE/$override" fi fi fi diff --git a/scripts/enroll-hypatia-fleet.sh b/scripts/enroll-hypatia-fleet.sh index bbe2c290..c7052117 100755 --- a/scripts/enroll-hypatia-fleet.sh +++ b/scripts/enroll-hypatia-fleet.sh @@ -2,6 +2,7 @@ # SPDX-License-Identifier: MPL-2.0 set -euo pipefail +REPOS_BASE="${REPOS_BASE:-$HOME/developer/hyper-repos}" usage() { cat < Root containing repos (default: /var$REPOS_DIR) + --repos-root Root containing repos (default: $REPOS_BASE) --registry Registry JSON output (default: shared-context/enrollment/repos.json) --apply Write enrollment directives into discovered repos @@ -18,7 +19,7 @@ Options: USAGE } -repos_root="/var$REPOS_DIR" +repos_root="$REPOS_BASE" registry="" apply=false diff --git a/scripts/fix-license-hygiene.sh b/scripts/fix-license-hygiene.sh index ea8f06f9..43528233 100755 --- a/scripts/fix-license-hygiene.sh +++ b/scripts/fix-license-hygiene.sh @@ -12,9 +12,10 @@ # Idempotent: only modifies what's missing or incorrect. # # Requires canonical templates at: -# /var$REPOS_DIR/palimpsest-license/legal/MPL-2.0.txt -# /var$REPOS_DIR/palimpsest-license/legal/PALIMPSEST-MPL-1.0.txt +# $REPOS_BASE/palimpsest-license/legal/MPL-2.0.txt +# $REPOS_BASE/palimpsest-license/legal/PALIMPSEST-MPL-1.0.txt set -euo pipefail +REPOS_BASE="${REPOS_BASE:-$HOME/developer/hyper-repos}" echo "REFUSED: fix-license-hygiene.sh is disabled per estate policy 2026-06-02." >&2 echo " Licence/SPDX edits MUST be manual, per-file, owner-approved." >&2 @@ -48,13 +49,13 @@ fi cd "$REPO_DIR" # Template sources -PALIMPSEST_REPO="/var$REPOS_DIR/palimpsest-license" +PALIMPSEST_REPO="$REPOS_BASE/palimpsest-license" MPL2_SRC="$PALIMPSEST_REPO/legal/MPL-2.0.txt" PMPL_SRC="$PALIMPSEST_REPO/legal/PALIMPSEST-MPL-1.0.txt" # Fallback to boj-server if palimpsest-license not available if [[ ! -f "$MPL2_SRC" ]]; then - MPL2_SRC="/var$REPOS_DIR/boj-server/LICENSE" + MPL2_SRC="$REPOS_BASE/boj-server/LICENSE" fi changes=false diff --git a/scripts/fix-missing-ai-manifest.sh b/scripts/fix-missing-ai-manifest.sh index b4beb1a5..3196bc50 100755 --- a/scripts/fix-missing-ai-manifest.sh +++ b/scripts/fix-missing-ai-manifest.sh @@ -35,9 +35,9 @@ format = "a2ml" [canonical-locations] machine-readable = ".machine_readable/" -state = ".machine_readable/6a2/STATE.a2ml" -meta = ".machine_readable/6a2/META.a2ml" -ecosystem = ".machine_readable/6a2/ECOSYSTEM.a2ml" +state = ".machine_readable/descriptiles/STATE.a2ml" +meta = ".machine_readable/descriptiles/META.a2ml" +ecosystem = ".machine_readable/descriptiles/ECOSYSTEM.a2ml" contractiles = ".machine_readable/contractiles/" bot-directives = ".machine_readable/bot_directives/" diff --git a/scripts/fix-proven-substitute.sh b/scripts/fix-proven-substitute.sh index a48f245f..508bea08 100755 --- a/scripts/fix-proven-substitute.sh +++ b/scripts/fix-proven-substitute.sh @@ -9,6 +9,7 @@ # Usage: fix-proven-substitute.sh set -euo pipefail +REPOS_BASE="${REPOS_BASE:-$HOME/developer/hyper-repos}" REPO_PATH="${1:?Usage: $0 }" FINDING_JSON="${2:?Missing finding JSON file}" @@ -28,7 +29,7 @@ if [[ ! "$LANGUAGE" =~ ^[a-z]+$ ]]; then exit 1 fi -PROVEN_BINDINGS_BASE="/var$REPOS_DIR/proven/bindings" +PROVEN_BINDINGS_BASE="$REPOS_BASE/proven/bindings" # Extract finding details FILE=$(jq -r '.file // .location // "unknown"' "$FINDING_JSON") @@ -70,7 +71,7 @@ case "$LANGUAGE" in echo "" echo "CARGO.TOML DEPENDENCY:" echo " [dependencies]" - echo " proven = { path = \"/var$REPOS_DIR/proven/bindings/rust\" }" + echo " proven = { path = \"$REPOS_BASE/proven/bindings/rust\" }" ;; elixir) @@ -79,7 +80,7 @@ case "$LANGUAGE" in echo " alias Proven.${PROVEN_MODULE}" echo "" echo "MIX.EXS DEPENDENCY:" - echo " {:proven, path: \"/var$REPOS_DIR/proven/bindings/elixir\"}" + echo " {:proven, path: \"$REPOS_BASE/proven/bindings/elixir\"}" ;; affinescript) @@ -95,7 +96,7 @@ case "$LANGUAGE" in echo "" echo "SHELL SUBSTITUTION:" echo " Source the proven shell wrapper:" - echo " . /var$REPOS_DIR/proven/bindings/bash/proven.sh" + echo " . \"$REPOS_BASE/proven/bindings/bash/proven.sh\"" echo " ${PROVEN_MODULE}_call \"\$@\"" ;; diff --git a/scripts/list-supervised-repos.sh b/scripts/list-supervised-repos.sh index 2104a3ac..2e03d741 100755 --- a/scripts/list-supervised-repos.sh +++ b/scripts/list-supervised-repos.sh @@ -13,7 +13,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" FLEET_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" -DEFAULT_REPOS_ROOT="/var/mnt/eclipse/repos" +DEFAULT_REPOS_ROOT="${REPOS_BASE:-$HOME/developer/hyper-repos}" REPOS_ROOT="${REPOS_ROOT:-$DEFAULT_REPOS_ROOT}" LIMIT=0 INVENTORY_FILE="${FLEET_SUPERVISED_REPOS_FILE:-}" @@ -71,7 +71,7 @@ resolve_repo_path() { # Expand placeholders sometimes used in generated enrollment metadata. candidate="${candidate//\/var\$REPOS_DIR/$REPOS_ROOT}" - candidate="${candidate//\$REPOS_DIR/${REPOS_ROOT#/var/}}" + candidate="${candidate//\$REPOS_DIR/$REPOS_ROOT}" if [[ "$candidate" != /* ]]; then candidate="$REPOS_ROOT/$candidate" diff --git a/scripts/maintenance-hard-pass.sh b/scripts/maintenance-hard-pass.sh index 62d94e57..60fc577e 100755 --- a/scripts/maintenance-hard-pass.sh +++ b/scripts/maintenance-hard-pass.sh @@ -2,6 +2,7 @@ # SPDX-License-Identifier: MPL-2.0 set -euo pipefail +REPOS_BASE="${REPOS_BASE:-$HOME/developer/hyper-repos}" usage() { cat <&2 echo "expected one of:" >&2 echo " $repo/scripts/maintenance/run-maintenance.sh" >&2 echo " $repo/run-maintenance.sh" >&2 - echo " /var$REPOS_DIR/run-maintenance.sh" >&2 + echo " $REPOS_BASE/run-maintenance.sh" >&2 exit 2 fi -if [[ -z "$panic_bin" && -x "/var$REPOS_DIR/panic-attacker/target/release/panic-attack" ]]; then - panic_bin="/var$REPOS_DIR/panic-attacker/target/release/panic-attack" +if [[ -z "$panic_bin" && -x "$REPOS_BASE/panic-attack/target/release/panic-attack" ]]; then + panic_bin="$REPOS_BASE/panic-attack/target/release/panic-attack" fi cmd=("$runner" --repo "$repo" --output "$output" --strict --fail-on-warn) diff --git a/scripts/process-review-findings.sh b/scripts/process-review-findings.sh index 9e1eef42..0653f311 100755 --- a/scripts/process-review-findings.sh +++ b/scripts/process-review-findings.sh @@ -21,7 +21,7 @@ set -euo pipefail -FLEET_BASE="${FLEET_BASE:-/var$REPOS_DIR/gitbot-fleet}" +FLEET_BASE="${FLEET_BASE:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" PENDING_DIR="${FLEET_BASE}/shared-context/findings/pending" GH_OWNER="hyperpolymath" diff --git a/scripts/repo-path-overrides.json b/scripts/repo-path-overrides.json index 1eadb1a5..f037b03d 100644 --- a/scripts/repo-path-overrides.json +++ b/scripts/repo-path-overrides.json @@ -1,98 +1,37 @@ { - "absolute-zero": "/var$REPOS_DIR/maa-framework/absolute-zero", - "accessibilitybot": "/var$REPOS_DIR/gitbot-fleet/bots/accessibilitybot", - "affinescript": "/var$REPOS_DIR/nextgen-languages/affinescript", - "aggregate-library": "/var$REPOS_DIR/developer-ecosystem/aggregate-library", - "aletheia": "/var$REPOS_DIR/maa-framework/aletheia", - "algorithm-shield": "/var$REPOS_DIR/misinformation-defence-platform/algorithm-shield", - "asdf-augmenters": "/var$REPOS_DIR/asdf-tool-plugins/asdf-augmenters", - "avow-protocol": "/var$REPOS_DIR/standards/avow-protocol", - "axel-protocol": "/var$REPOS_DIR/standards/axel-protocol", - "Axiom.jl": "/var$REPOS_DIR/developer-ecosystem/julia-ecosystem/packages/Axiom.jl", - "betlang": "/var$REPOS_DIR/nextgen-languages/betlang", - "bitfuckit": "/var$REPOS_DIR/reposystem/bitfuckit", - "blue-screen-of-app": "/var$REPOS_DIR/games & trivia/blue-screen-of-app", - "BowtieRisk.jl": "/var$REPOS_DIR/developer-ecosystem/julia-ecosystem/packages/BowtieRisk.jl", - "broad-spectrum": "/var$REPOS_DIR/ambientops/broad-spectrum", - "cadre-router": "/var$REPOS_DIR/developer-ecosystem/rescript-ecosystem/cadre-router", - "candy-crash": "/var$REPOS_DIR/games & trivia/candy-crash", - "casket-ssg": "/var$REPOS_DIR/asdf-tool-plugins/asdf-plugin-collection/plugins/casket-ssg", - "cerro-torre": "/var$REPOS_DIR/odds-and-sods-package-manager/services/cerro-torre", - "cipherbot": "/var$REPOS_DIR/gitbot-fleet/bots/cipherbot", - "claim-forge": "/var$REPOS_DIR/reposystem/claim-forge", - "claude-integrations": "/var$REPOS_DIR/patallm-gallery/claude-integrations", - "coq-jr": "/var$REPOS_DIR/developer-ecosystem/coq-ecosystem/coq-jr", - "czech-file-knife": "/var$REPOS_DIR/ambientops/czech-file-knife", - "deno-ecosystem": "/var$REPOS_DIR/developer-ecosystem/deno-ecosystem", - "dicti0nary-attack": "/var$REPOS_DIR/games & trivia/dicti0nary-attack", - "did-you-actually-do-that": "/var$REPOS_DIR/patallm-gallery/did-you-actually-do-that", - "disinfo-nesy-detector": "/var$REPOS_DIR/neural-foundations/satellites/neurosymbolic/disinfo-nesy-detector", - "dnfinition": "/var$REPOS_DIR/ambientops/total-update/elixir/dnfinition", - "echidnabot": "/var$REPOS_DIR/echidna/echidnabot", - "eclexia": "/var$REPOS_DIR/nextgen-languages/eclexia", - "elegant-state": "/var$REPOS_DIR/neural-foundations/satellites/agentic/elegant-state", - "error-lang": "/var$REPOS_DIR/nextgen-languages/error-lang", - "esn": "/var$REPOS_DIR/neural-foundations/satellites/neurosymbolic/esn", - "finishingbot": "/var$REPOS_DIR/gitbot-fleet/bots/finishingbot", - "fogbinder": "/var$REPOS_DIR/zotero-tools/fogbinder", - "formdb-http": "/var$REPOS_DIR/nextgen-databases/lithoglyph/formdb-http", - "games": "/var$REPOS_DIR/games & trivia", - "glambot": "/var$REPOS_DIR/gitbot-fleet/bots/glambot", - "glyphbase": "/var$REPOS_DIR/nextgen-databases/lithoglyph/glyphbase", - "gql-dt": "/var$REPOS_DIR/nextgen-databases/lithoglyph/gql-dt", - "hybrid-automation-router": "/var$REPOS_DIR/ambientops/hybrid-automation-router", - "IDApixiTIK": "/var$REPOS_DIR/idaptik", - "idris2-ecosystem": "/var$REPOS_DIR/developer-ecosystem/idris2-ecosystem", - "immutable-linux-auditor": "/var$REPOS_DIR/ambientops/immutable-linux-auditor", - "indieweb2-bastion": "/var$REPOS_DIR/civic-connect/indieweb2-bastion", - "julia-the-viper": "/var$REPOS_DIR/nextgen-languages/julia-the-viper", - "k9-svc": "/var$REPOS_DIR/standards/k9-svc", - "kea-tools": "/var$REPOS_DIR/kea/kea-tools", - "kith": "/var$REPOS_DIR/developer-ecosystem/well-known-ecosystem/kith", - "language-bridges": "/var$REPOS_DIR/nextgen-languages/language-bridges", - "language-interop-compiler": "/var$REPOS_DIR/nextgen-languages/language-interop-compiler", - "lithoglyph": "/var$REPOS_DIR/nextgen-databases/lithoglyph", - "llm-tools": "/var$REPOS_DIR/patallm-gallery/llm-tools", - "lol": "/var$REPOS_DIR/standards/lol", - "lsm": "/var$REPOS_DIR/neural-foundations/satellites/neurosymbolic/lsm", - "mcp-repo-guardian": "/var$REPOS_DIR/standards/0-ai-gatekeeper-protocol/mcp-repo-guardian", - "my-lang": "/var$REPOS_DIR/nextgen-languages/my-lang", - "nerdsafe-restart": "/var$REPOS_DIR/ambientops/nerdsafe-restart", - "nick-shells": "/var$REPOS_DIR/ambientops/nick-shells", - "oblibeny": "/var$REPOS_DIR/nextgen-languages/oblibeny", - "package-publishers": "/var$REPOS_DIR/developer-ecosystem/package-publishers", - "_pathroot": "/var$REPOS_DIR/ambientops/_pathroot", - "personal-sysadmin": "/var$REPOS_DIR/ambientops/personal-sysadmin", - "phantom-metal-taste": "/var$REPOS_DIR/games & trivia/phantom-metal-taste", - "phronesis": "/var$REPOS_DIR/nextgen-languages/phronesis", - "poly-k8s-mcp": "/var$REPOS_DIR/flatracoon/netstack/modules/poly-k8s-mcp", - "poly-secret-mcp": "/var$REPOS_DIR/flatracoon/netstack/modules/poly-secret-mcp", - "ProvenCrypto.jl": "/var$REPOS_DIR/developer-ecosystem/julia-ecosystem/packages/ProvenCrypto.jl", - "qubes-sdp": "/var$REPOS_DIR/aerie/qubes-sdp", - "reasonably-good-token-vault": "/var$REPOS_DIR/ambientops/reasonably-good-token-vault", - "recon-silly-ation": "/var$REPOS_DIR/developer-ecosystem/satellites/developer-ux/recon-silly-ation", - "repo-batcher": "/var$REPOS_DIR/reposystem/scaffoldia/repo-batcher", - "repo-guardian-fs": "/var$REPOS_DIR/standards/0-ai-gatekeeper-protocol/repo-guardian-fs", - "rescript-ecosystem": "/var$REPOS_DIR/developer-ecosystem/rescript-ecosystem", - "rhodibot": "/var$REPOS_DIR/gitbot-fleet/bots/rhodibot", - "rhodium-standard-repositories": "/var$REPOS_DIR/standards/rhodium-standard-repositories", - "robot-repo-automaton": "/var$REPOS_DIR/developer-ecosystem/satellites/repo-management/robot-repo-automaton", - "safe-brute-force": "/var$REPOS_DIR/games & trivia/safe-brute-force", - "scaffoldia": "/var$REPOS_DIR/reposystem/scaffoldia", - "seambot": "/var$REPOS_DIR/gitbot-fleet/bots/seambot", - "selur": "/var$REPOS_DIR/odds-and-sods-package-manager/services/selur", - "SMTLib.jl": "/var$REPOS_DIR/developer-ecosystem/julia-ecosystem/packages/SMTLib.jl", - "sustainabot": "/var$REPOS_DIR/gitbot-fleet/bots/sustainabot", - "svalinn": "/var$REPOS_DIR/project-wharf/infra/svalinn", - "system-tools": "/var$REPOS_DIR/ambientops/system-tools", - "test-repo": "/var$REPOS_DIR/hypatia/integration/fixtures/test-repo", - "hotchocolabot": "/var$REPOS_DIR/hotchocolabot", - "thejeffparadox": "/var$REPOS_DIR/games & trivia/thejeffparadox", - "total-update": "/var$REPOS_DIR/ambientops/total-update", - "union-policy-parser": "/var$REPOS_DIR/palimpsest-plasma/union-policy-parser", - "verified-container-spec": "/var$REPOS_DIR/stapeln/verified-container-spec", - "verisimdb": "/var$REPOS_DIR/nextgen-databases/verisimdb", - "vordr": "/var$REPOS_DIR/stapeln/container-stack/vordr", - "well-known-ecosystem": "/var$REPOS_DIR/developer-ecosystem/well-known-ecosystem", - "zig-ffi": "/var$REPOS_DIR/developer-ecosystem/rescript-ecosystem/packages/ffi/zig-ffi" + "accessibilitybot": "gitbot-fleet/bots/accessibilitybot", + "aletheia": "maa-framework/aletheia", + "algorithm-shield": "misinformation-defence-platform/algorithm-shield", + "asdf-augmenters": "asdf-tool-plugins/asdf-augmenters", + "avow-protocol": "standards/avow-protocol", + "axel-protocol": "standards/axel-protocol", + "broad-spectrum": "ambientops/broad-spectrum", + "cadre-router": "developer-ecosystem/rescript-ecosystem/cadre-router", + "casket-ssg": "asdf-tool-plugins/asdf-plugin-collection/plugins/casket-ssg", + "cipherbot": "gitbot-fleet/bots/cipherbot", + "czech-file-knife": "ambientops/czech-file-knife", + "deno-ecosystem": "developer-ecosystem/deno-ecosystem", + "did-you-actually-do-that": "patallm-gallery/did-you-actually-do-that", + "dnfinition": "ambientops/total-update/elixir/dnfinition", + "finishingbot": "gitbot-fleet/bots/finishingbot", + "glambot": "gitbot-fleet/bots/glambot", + "idris2-ecosystem": "developer-ecosystem/idris2-ecosystem", + "immutable-linux-auditor": "ambientops/immutable-linux-auditor", + "indieweb2-bastion": "civic-connect/indieweb2-bastion", + "k9-svc": "standards/k9-svc", + "kith": "developer-ecosystem/well-known-ecosystem/kith", + "llm-tools": "patallm-gallery/llm-tools", + "nerdsafe-restart": "ambientops/nerdsafe-restart", + "nick-shells": "ambientops/nick-shells", + "personal-sysadmin": "ambientops/personal-sysadmin", + "rhodibot": "gitbot-fleet/bots/rhodibot", + "rhodium-standard-repositories": "standards/rhodium-standard-repositories", + "seambot": "gitbot-fleet/bots/seambot", + "selur": "odds-and-sods-package-manager/services/selur", + "sustainabot": "gitbot-fleet/bots/sustainabot", + "system-tools": "ambientops/system-tools", + "test-repo": "hypatia/integration/fixtures/test-repo", + "total-update": "ambientops/total-update", + "well-known-ecosystem": "developer-ecosystem/well-known-ecosystem", + "zig-ffi": "developer-ecosystem/rescript-ecosystem/packages/ffi/zig-ffi" } diff --git a/scripts/sync-all-parallel.exs b/scripts/sync-all-parallel.exs index d115fc06..3a659ed1 100644 --- a/scripts/sync-all-parallel.exs +++ b/scripts/sync-all-parallel.exs @@ -10,7 +10,7 @@ # elixir sync-all-parallel.exs [OPTIONS] # # Options: -# --repos-dir PATH Base directory (default: /var$REPOS_DIR) +# --repos-dir PATH Base directory (default: $REPOS_BASE or ~/developer/hyper-repos) # --dry-run Show what would happen # --auto Non-interactive mode (skip all issues) # --concurrency N Max concurrent git operations (default: 32) @@ -140,9 +140,16 @@ defmodule SyncAll do # --- Argument Parsing --- + defp default_repos_dir do + case System.get_env("REPOS_BASE") do + base when is_binary(base) and byte_size(base) > 0 -> base + _ -> Path.join(System.user_home!(), "developer/hyper-repos") + end + end + defp parse_args(args) do parse_args(args, %SyncAll{ - repos_dir: "/var$REPOS_DIR", + repos_dir: default_repos_dir(), dry_run: false, auto_mode: false, concurrency: 32, diff --git a/shared-context/enrollment/README.adoc b/shared-context/enrollment/README.adoc index fbdc60de..272584f1 100644 --- a/shared-context/enrollment/README.adoc +++ b/shared-context/enrollment/README.adoc @@ -14,7 +14,7 @@ just enroll-repos [source,bash] ---- -just enroll-repos /var$REPOS_DIR true +just enroll-repos "${REPOS_BASE:-$HOME/developer/hyper-repos}" true ---- This writes `+.machine_readable/bot_directives/FLEET-ENROLLMENT.a2ml+` From 428f3efb7f2822c1ecedab49640de5e3ae75417f 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:25 +0100 Subject: [PATCH 02/16] fix(deps): patch automaton HTTP2 memory exhaustion --- robot-repo-automaton/Cargo.lock | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/robot-repo-automaton/Cargo.lock b/robot-repo-automaton/Cargo.lock index 56297c9a..9570fc6f 100644 --- a/robot-repo-automaton/Cargo.lock +++ b/robot-repo-automaton/Cargo.lock @@ -62,7 +62,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]] @@ -73,7 +73,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -524,7 +524,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -566,7 +566,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]] @@ -1727,9 +1727,9 @@ checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "h2" -version = "0.4.13" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +checksum = "a9f37a958b41b3b19ee2707c06439c0e9e547e847223eb791ecb0cb821c65e27" dependencies = [ "atomic-waker", "bytes", @@ -2458,7 +2458,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]] @@ -2919,7 +2919,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -2977,7 +2977,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -3242,7 +3242,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -3321,7 +3321,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -3954,7 +3954,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.52.0", ] [[package]] From f99c14386887b2184aa8bd8f47214c630f075058 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Mon, 7 Sep 2026 03:26:58 +0100 Subject: [PATCH 03/16] fix(fleet): preserve migrated recipes, aliases and dispatch outcomes --- .github/workflows/actions.lock | 6 ++ .github/workflows/casket-pages.yml | 7 ++- .github/workflows/rust.yml | 14 ++++- .machine_readable/descriptiles/PLAYBOOK.a2ml | 2 +- fleet-coordinator.sh | 2 +- robot-repo-automaton/src/hypatia.rs | 52 +++++++++++++--- robot-repo-automaton/src/skeleton.rs | 26 ++++---- .../{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}/STATE.a2ml | 0 scripts/dispatch-runner.sh | 22 ++++--- scripts/enroll-hypatia-fleet.sh | 9 +-- scripts/repo-path-overrides.json | 61 +++++++++++++++++++ scripts/tests/dispatch-paths.sh | 44 +++++++++++++ 17 files changed, 202 insertions(+), 43 deletions(-) rename robot-repo-automaton/templates/skeleton/.machine_readable/{6a2 => descriptiles}/AGENTIC.a2ml (100%) rename robot-repo-automaton/templates/skeleton/.machine_readable/{6a2 => descriptiles}/ECOSYSTEM.a2ml (100%) rename robot-repo-automaton/templates/skeleton/.machine_readable/{6a2 => descriptiles}/META.a2ml (100%) rename robot-repo-automaton/templates/skeleton/.machine_readable/{6a2 => descriptiles}/NEUROSYM.a2ml (100%) rename robot-repo-automaton/templates/skeleton/.machine_readable/{6a2 => descriptiles}/PLAYBOOK.a2ml (100%) rename robot-repo-automaton/templates/skeleton/.machine_readable/{6a2 => descriptiles}/STATE.a2ml (100%) create mode 100644 scripts/tests/dispatch-paths.sh diff --git a/.github/workflows/actions.lock b/.github/workflows/actions.lock index 8d68ff43..de2729bf 100644 --- a/.github/workflows/actions.lock +++ b/.github/workflows/actions.lock @@ -12,6 +12,7 @@ workflows: - 'actions/deploy-pages@v5.0.0' - 'actions/download-artifact@v8.0.1' - 'actions/upload-pages-artifact@v5.0.0' + - 'haskell-actions/setup@v2.12.0' '.github/workflows/codeql.yml': - 'actions/checkout@v7.0.1' - 'github/codeql-action@v4.37.8' @@ -107,6 +108,11 @@ dependencies: commit: 'sha1-db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28' owner_id: 9919 repo_id: 259445878 + 'haskell-actions/setup@v2.12.0': + ref: 'v2.12.0' + commit: 'sha1-6037f33647c3f17758a2356c80fc4a53d7e0685d' + owner_id: 75048950 + repo_id: 623796603 'hyperpolymath/a2ml-ecosystem@main': ref: 'main' commit: 'sha1-aa4b836bd969df2bc58128cb8e3d20bbc88d5e79' diff --git a/.github/workflows/casket-pages.yml b/.github/workflows/casket-pages.yml index 6bb7f71c..d14b9191 100644 --- a/.github/workflows/casket-pages.yml +++ b/.github/workflows/casket-pages.yml @@ -1,4 +1,3 @@ -# This workflow is managed by gh actions-lock. # SPDX-License-Identifier: MPL-2.0 # This workflow is managed by gh actions-lock. name: GitHub Pages @@ -46,6 +45,12 @@ jobs: .casket-ssg/dist-newstyle key: ${{ runner.os }}-casket-${{ hashFiles('.casket-ssg/casket-ssg.cabal') }} + - name: Select tested Haskell toolchain + uses: haskell-actions/setup@v2.12.0 + with: + ghc-version: '9.6.6' + cabal-version: '3.10.3.0' + - name: Prepare runner Haskell toolchain run: | set -euo pipefail diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index ebd23374..8fea3993 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -1,7 +1,5 @@ -# This workflow is managed by gh actions-lock. # SPDX-License-Identifier: MPL-2.0 # This workflow is managed by gh actions-lock. -# This workflow is managed by gh actions-lock. name: Rust # Build + test + clippy gate for the three standalone Rust crates. # Added after a non-compiling crate (robot-repo-automaton) reached `main` @@ -49,3 +47,15 @@ jobs: # `cargo fmt` pass lands. run: cargo fmt --check continue-on-error: true + + dispatch-contracts: + name: Dispatch path and outcome contracts + runs-on: ubuntu-24.04 + timeout-minutes: 5 + permissions: + contents: read + steps: + - uses: actions/checkout@v7.0.1 + with: + persist-credentials: false + - run: bash scripts/tests/dispatch-paths.sh diff --git a/.machine_readable/descriptiles/PLAYBOOK.a2ml b/.machine_readable/descriptiles/PLAYBOOK.a2ml index bcb9342c..3f672832 100644 --- a/.machine_readable/descriptiles/PLAYBOOK.a2ml +++ b/.machine_readable/descriptiles/PLAYBOOK.a2ml @@ -19,7 +19,7 @@ last-updated = "2026-04-11" # 4. Run `just security` to audit for vulnerabilities [release-process] -# 1. Update version in .machine_readable/descriptiles/STATE.a2ml and META.a2ml +# 1. Update version in .machine_readable/descriptiles/STATE.a2ml and .machine_readable/descriptiles/META.a2ml # 2. Run `just release-preflight` (validate + quality + security + maint-hard-pass) # 3. Tag and push diff --git a/fleet-coordinator.sh b/fleet-coordinator.sh index 350c67f9..26a6c272 100755 --- a/fleet-coordinator.sh +++ b/fleet-coordinator.sh @@ -319,7 +319,7 @@ process_findings() { local review_script="$FLEET_DIR/scripts/process-review-findings.sh" if [[ -x "$review_script" ]]; then log_bot "rhodibot" "Processing $total_substitute substitute-tier findings for review" - "$review_script" 2>&1 | sed 's/^/ /' || log_warn "Review processing had errors" + FLEET_BASE="$FLEET_DIR" "$review_script" 2>&1 | sed 's/^/ /' || log_warn "Review processing had errors" else log_warn "Review processor not found at $review_script" fi diff --git a/robot-repo-automaton/src/hypatia.rs b/robot-repo-automaton/src/hypatia.rs index 0e2b8781..19ce0702 100644 --- a/robot-repo-automaton/src/hypatia.rs +++ b/robot-repo-automaton/src/hypatia.rs @@ -203,7 +203,7 @@ impl CicdHyperAClient { /// Fetch a ruleset from the registry. /// /// Tries the Hypatia API first; falls back to loading rules from the - /// local verisim-data recipes directory if the API is unavailable. + /// local verisimdb-data recipes directory if the API is unavailable. pub async fn fetch_ruleset(&self, ruleset_id: &str) -> crate::Result { tracing::info!("Fetching ruleset: {} from {}", ruleset_id, self.config.api_url); @@ -233,19 +233,33 @@ impl CicdHyperAClient { } } - // Fallback: load from local verisim-data recipes + // Fallback: load from local verisimdb-data recipes self.load_local_ruleset(ruleset_id) } - /// Load rules from local verisim-data recipes directory. + /// Load rules from local verisimdb-data recipes directory. fn load_local_ruleset(&self, ruleset_id: &str) -> crate::Result { - let recipes_dirs = [ - PathBuf::from("/var/mnt/eclipse/repos/verisim-data/recipes"), - dirs::home_dir() - .unwrap_or_default() - .join("Documents/hyperpolymath-repos/verisim-data/recipes"), - ]; + let mut recipes_dirs = Vec::new(); + for key in ["HYPATIA_DATA", "VERISIMDB_DATA"] { + if let Some(root) = std::env::var_os(key).filter(|s| !s.is_empty()) { + recipes_dirs.push(PathBuf::from(root).join("recipes")); + } + } + if let Some(root) = std::env::var_os("REPOS_BASE").filter(|s| !s.is_empty()) { + recipes_dirs.push(PathBuf::from(root).join("verisimdb-data/recipes")); + } + if let Some(home) = dirs::home_dir() { + recipes_dirs.push(home.join("developer/hyper-repos/verisimdb-data/recipes")); + } + // Read-only compatibility fallbacks for installations not yet migrated. + recipes_dirs.push(PathBuf::from("/var/mnt/eclipse/repos/verisim-data/recipes")); + if let Some(home) = dirs::home_dir() { + recipes_dirs.push(home.join("Documents/hyperpolymath-repos/verisim-data/recipes")); + } + self.load_recipes_from(ruleset_id, &recipes_dirs) + } + fn load_recipes_from(&self, ruleset_id: &str, recipes_dirs: &[PathBuf]) -> crate::Result { let recipes_dir = recipes_dirs.iter().find(|d| d.is_dir()); let mut rules = Vec::new(); @@ -601,7 +615,7 @@ impl CicdHyperAClient { } } -/// Convert a verisim-data recipe JSON to a Rule. +/// Convert a verisimdb-data recipe JSON to a Rule. fn recipe_to_rule(recipe: &serde_json::Value) -> Option { let id = recipe.get("id")?.as_str()?.to_string(); let name = recipe.get("name").and_then(|v| v.as_str()).unwrap_or(&id).to_string(); @@ -663,6 +677,24 @@ fn recipe_to_rule(recipe: &serde_json::Value) -> Option { mod tests { use super::*; + #[test] + fn migrated_recipes_take_precedence_over_legacy_recipes() { + let root = tempfile::tempdir().unwrap(); + let current = root.path().join("verisimdb-data/recipes"); + let legacy = root.path().join("verisim-data/recipes"); + for (dir, id) in [(¤t, "current-rule"), (&legacy, "legacy-rule")] { + std::fs::create_dir_all(dir).unwrap(); + let recipe = serde_json::json!({"id": id, "file_glob": "*.rs"}); + std::fs::write(dir.join("rule.json"), recipe.to_string()).unwrap(); + } + let client = CicdHyperAClient::new(CicdHyperAConfig::default()); + let rules = client.load_recipes_from("local", &[current, legacy.clone()]).unwrap(); + assert_eq!(rules.rules.len(), 1); + assert_eq!(rules.rules[0].id, "current-rule"); + let rules = client.load_recipes_from("local", &[root.path().join("absent"), legacy]).unwrap(); + assert_eq!(rules.rules[0].id, "legacy-rule"); + } + #[test] fn test_config_defaults() { let config = CicdHyperAConfig::default(); diff --git a/robot-repo-automaton/src/skeleton.rs b/robot-repo-automaton/src/skeleton.rs index 4c023315..4d4b51c3 100644 --- a/robot-repo-automaton/src/skeleton.rs +++ b/robot-repo-automaton/src/skeleton.rs @@ -11,7 +11,7 @@ //! Source of truth = the **post-#47 canonical layout**, not the prose in //! `standards` `REQUIRED-FILES.md` (which is stale: it still lists root //! `*.scm` + `Mustfile`, predating the estate-wide `.scm`→`.a2ml` migration -//! and the `.machine_readable/6a2/` layout). Updating that doc to match is a +//! and the `.machine_readable/descriptiles/` layout). Updating that doc to match is a //! separate `standards`-repo change, deliberately out of scope here. use anyhow::{Context, Result}; @@ -41,28 +41,28 @@ pub const SKELETON: &[(&str, &str)] = &[ ), ("Justfile", include_str!("../templates/skeleton/Justfile")), ( - ".machine_readable/6a2/META.a2ml", - include_str!("../templates/skeleton/.machine_readable/6a2/META.a2ml"), + ".machine_readable/descriptiles/META.a2ml", + include_str!("../templates/skeleton/.machine_readable/descriptiles/META.a2ml"), ), ( - ".machine_readable/6a2/STATE.a2ml", - include_str!("../templates/skeleton/.machine_readable/6a2/STATE.a2ml"), + ".machine_readable/descriptiles/STATE.a2ml", + include_str!("../templates/skeleton/.machine_readable/descriptiles/STATE.a2ml"), ), ( - ".machine_readable/6a2/ECOSYSTEM.a2ml", - include_str!("../templates/skeleton/.machine_readable/6a2/ECOSYSTEM.a2ml"), + ".machine_readable/descriptiles/ECOSYSTEM.a2ml", + include_str!("../templates/skeleton/.machine_readable/descriptiles/ECOSYSTEM.a2ml"), ), ( - ".machine_readable/6a2/PLAYBOOK.a2ml", - include_str!("../templates/skeleton/.machine_readable/6a2/PLAYBOOK.a2ml"), + ".machine_readable/descriptiles/PLAYBOOK.a2ml", + include_str!("../templates/skeleton/.machine_readable/descriptiles/PLAYBOOK.a2ml"), ), ( - ".machine_readable/6a2/AGENTIC.a2ml", - include_str!("../templates/skeleton/.machine_readable/6a2/AGENTIC.a2ml"), + ".machine_readable/descriptiles/AGENTIC.a2ml", + include_str!("../templates/skeleton/.machine_readable/descriptiles/AGENTIC.a2ml"), ), ( - ".machine_readable/6a2/NEUROSYM.a2ml", - include_str!("../templates/skeleton/.machine_readable/6a2/NEUROSYM.a2ml"), + ".machine_readable/descriptiles/NEUROSYM.a2ml", + include_str!("../templates/skeleton/.machine_readable/descriptiles/NEUROSYM.a2ml"), ), ]; diff --git a/robot-repo-automaton/templates/skeleton/.machine_readable/6a2/AGENTIC.a2ml b/robot-repo-automaton/templates/skeleton/.machine_readable/descriptiles/AGENTIC.a2ml similarity index 100% rename from robot-repo-automaton/templates/skeleton/.machine_readable/6a2/AGENTIC.a2ml rename to robot-repo-automaton/templates/skeleton/.machine_readable/descriptiles/AGENTIC.a2ml diff --git a/robot-repo-automaton/templates/skeleton/.machine_readable/6a2/ECOSYSTEM.a2ml b/robot-repo-automaton/templates/skeleton/.machine_readable/descriptiles/ECOSYSTEM.a2ml similarity index 100% rename from robot-repo-automaton/templates/skeleton/.machine_readable/6a2/ECOSYSTEM.a2ml rename to robot-repo-automaton/templates/skeleton/.machine_readable/descriptiles/ECOSYSTEM.a2ml diff --git a/robot-repo-automaton/templates/skeleton/.machine_readable/6a2/META.a2ml b/robot-repo-automaton/templates/skeleton/.machine_readable/descriptiles/META.a2ml similarity index 100% rename from robot-repo-automaton/templates/skeleton/.machine_readable/6a2/META.a2ml rename to robot-repo-automaton/templates/skeleton/.machine_readable/descriptiles/META.a2ml diff --git a/robot-repo-automaton/templates/skeleton/.machine_readable/6a2/NEUROSYM.a2ml b/robot-repo-automaton/templates/skeleton/.machine_readable/descriptiles/NEUROSYM.a2ml similarity index 100% rename from robot-repo-automaton/templates/skeleton/.machine_readable/6a2/NEUROSYM.a2ml rename to robot-repo-automaton/templates/skeleton/.machine_readable/descriptiles/NEUROSYM.a2ml diff --git a/robot-repo-automaton/templates/skeleton/.machine_readable/6a2/PLAYBOOK.a2ml b/robot-repo-automaton/templates/skeleton/.machine_readable/descriptiles/PLAYBOOK.a2ml similarity index 100% rename from robot-repo-automaton/templates/skeleton/.machine_readable/6a2/PLAYBOOK.a2ml rename to robot-repo-automaton/templates/skeleton/.machine_readable/descriptiles/PLAYBOOK.a2ml diff --git a/robot-repo-automaton/templates/skeleton/.machine_readable/6a2/STATE.a2ml b/robot-repo-automaton/templates/skeleton/.machine_readable/descriptiles/STATE.a2ml similarity index 100% rename from robot-repo-automaton/templates/skeleton/.machine_readable/6a2/STATE.a2ml rename to robot-repo-automaton/templates/skeleton/.machine_readable/descriptiles/STATE.a2ml diff --git a/scripts/dispatch-runner.sh b/scripts/dispatch-runner.sh index 3e2a7895..0f9acb40 100755 --- a/scripts/dispatch-runner.sh +++ b/scripts/dispatch-runner.sh @@ -3,7 +3,7 @@ # # dispatch-runner.sh — Execute actions from hypatia dispatch manifests # -# Reads pending.jsonl from verisim-data/dispatch/ and executes fixes +# Reads pending.jsonl from verisimdb-data/dispatch/ and executes fixes # via robot-repo-automaton CLI, gitbot-fleet fix scripts, or advisory logging. # # Usage: @@ -15,7 +15,7 @@ # --strategy STRAT Only process entries matching strategy (auto_execute|review|report_only) # --repo REPO Only process entries for a specific repo # --limit N Process at most N entries -# --manifest PATH Path to manifest (default: verisim-data/dispatch/pending.jsonl) +# --manifest PATH Path to manifest (default: verisimdb-data/dispatch/pending.jsonl) # --parallel N Run up to N fix scripts concurrently (default: 1) # --dedup-repo Group entries by repo, run only first per repo+category @@ -72,7 +72,7 @@ THIRD_PARTY_PATHS=( "echidna/HOL" ) -# Try hypatia's data first, then fall back to central verisim-data +# Try hypatia's data first, then fall back to central verisimdb-data if [[ -f "${HYPATIA_DATA}/dispatch/pending.jsonl" ]]; then MANIFEST_PATH="${HYPATIA_DATA}/dispatch/pending.jsonl" elif [[ -f "${VERISIMDB_DATA}/dispatch/pending.jsonl" ]]; then @@ -243,7 +243,7 @@ record_outcome() { ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ") local json - json=$(jq -n \ + json=$(jq -cn \ --arg pid "$pattern_id" \ --arg rid "$recipe_id" \ --arg repo "$repo" \ @@ -254,8 +254,12 @@ record_outcome() { '{pattern_id: $pid, recipe_id: $rid, repo: $repo, file: $file, outcome: $outcome, timestamp: $ts, bot: $bot}') echo "$json" >> "$OUTCOME_FILE" - # Also write to central store if it exists - if [[ -d "$(dirname "$OUTCOME_FILE_CENTRAL")" ]] || mkdir -p "$(dirname "$OUTCOME_FILE_CENTRAL")" 2>/dev/null; then + # Avoid duplicate JSONL records when the two configured stores are the same. + local primary_path central_path + primary_path=$(realpath -m "$OUTCOME_FILE") + central_path=$(realpath -m "$OUTCOME_FILE_CENTRAL") + if [[ "$primary_path" != "$central_path" && ! "$OUTCOME_FILE" -ef "$OUTCOME_FILE_CENTRAL" ]]; then + mkdir -p "$(dirname "$OUTCOME_FILE_CENTRAL")" echo "$json" >> "$OUTCOME_FILE_CENTRAL" fi @@ -455,7 +459,7 @@ execute_entry() { fi # Write finding to shared-context for rhodibot pickup - local findings_dir="$REPOS_BASE/gitbot-fleet/shared-context/findings/pending" + local findings_dir="$FLEET_ROOT/shared-context/findings/pending" mkdir -p "$findings_dir" # Sanitize pattern_id for use in filename (strip unsafe chars) @@ -474,7 +478,7 @@ execute_entry() { fi # Append to sustainabot advisory log - local advisory_dir="$REPOS_BASE/gitbot-fleet/shared-context/advisories" + local advisory_dir="$FLEET_ROOT/shared-context/advisories" mkdir -p "$advisory_dir" local advisory_file="$advisory_dir/$(date -u +%Y-%m).jsonl" @@ -630,7 +634,7 @@ fi rm -f "$RESCAN_REPOS_FILE" 2>/dev/null || true # --- Kin Protocol: write heartbeat --- -KIN_DIR="${HOME}/.hypatia/kin" +KIN_DIR="${KIN_DIR:-${HOME}/.hypatia/kin}" mkdir -p "$KIN_DIR" HEARTBEAT_STATUS="healthy" diff --git a/scripts/enroll-hypatia-fleet.sh b/scripts/enroll-hypatia-fleet.sh index c7052117..7eaf0c1b 100755 --- a/scripts/enroll-hypatia-fleet.sh +++ b/scripts/enroll-hypatia-fleet.sh @@ -95,12 +95,9 @@ while IFS= read -r repo_path; do [[ -d "$repo_path/.machine_readable" ]] && has_machine_readable=true [[ -d "$repo_path/.machine_readable/bot_directives" ]] && has_bot_directives=true - # Canonical location is .machine_readable/6a2/ (SD004). Legacy flat layout is - # still accepted here because this script scans OTHER repos, many of which - # have not migrated yet -- narrowing to 6a2/ alone would silently mark them - # as lacking a manifest they actually have. - [[ -f "$repo_path/.machine_readable/6a2/META.a2ml" || -f "$repo_path/.machine_readable/META.a2ml" ]] && has_a2ml_meta=true - [[ -f "$repo_path/.machine_readable/6a2/STATE.a2ml" || -f "$repo_path/.machine_readable/STATE.a2ml" ]] && has_a2ml_state=true + # Generate descriptiles only; recognize retired layouts during migration. + [[ -f "$repo_path/.machine_readable/descriptiles/META.a2ml" || -f "$repo_path/.machine_readable/6a2/META.a2ml" || -f "$repo_path/.machine_readable/META.a2ml" ]] && has_a2ml_meta=true + [[ -f "$repo_path/.machine_readable/descriptiles/STATE.a2ml" || -f "$repo_path/.machine_readable/6a2/STATE.a2ml" || -f "$repo_path/.machine_readable/STATE.a2ml" ]] && has_a2ml_state=true if [[ -x "$repo_path/scripts/maintenance/run-maintenance.sh" || -x "$repo_path/run-maintenance.sh" ]]; then has_maint_script=true fi diff --git a/scripts/repo-path-overrides.json b/scripts/repo-path-overrides.json index f037b03d..a2d2691b 100644 --- a/scripts/repo-path-overrides.json +++ b/scripts/repo-path-overrides.json @@ -1,37 +1,98 @@ { + "Axiom.jl": "developer-ecosystem/julia-ecosystem/packages/Axiom.jl", + "BowtieRisk.jl": "developer-ecosystem/julia-ecosystem/packages/BowtieRisk.jl", + "IDApixiTIK": "idaptik", + "ProvenCrypto.jl": "developer-ecosystem/julia-ecosystem/packages/ProvenCrypto.jl", + "SMTLib.jl": "developer-ecosystem/julia-ecosystem/packages/SMTLib.jl", + "_pathroot": "ambientops/_pathroot", + "absolute-zero": "maa-framework/absolute-zero", "accessibilitybot": "gitbot-fleet/bots/accessibilitybot", + "affinescript": "nextgen-languages/affinescript", + "aggregate-library": "developer-ecosystem/aggregate-library", "aletheia": "maa-framework/aletheia", "algorithm-shield": "misinformation-defence-platform/algorithm-shield", "asdf-augmenters": "asdf-tool-plugins/asdf-augmenters", "avow-protocol": "standards/avow-protocol", "axel-protocol": "standards/axel-protocol", + "betlang": "nextgen-languages/betlang", + "bitfuckit": "reposystem/bitfuckit", + "blue-screen-of-app": "games & trivia/blue-screen-of-app", "broad-spectrum": "ambientops/broad-spectrum", "cadre-router": "developer-ecosystem/rescript-ecosystem/cadre-router", + "candy-crash": "games & trivia/candy-crash", "casket-ssg": "asdf-tool-plugins/asdf-plugin-collection/plugins/casket-ssg", + "cerro-torre": "odds-and-sods-package-manager/services/cerro-torre", "cipherbot": "gitbot-fleet/bots/cipherbot", + "claim-forge": "reposystem/claim-forge", + "claude-integrations": "patallm-gallery/claude-integrations", + "coq-jr": "developer-ecosystem/coq-ecosystem/coq-jr", "czech-file-knife": "ambientops/czech-file-knife", "deno-ecosystem": "developer-ecosystem/deno-ecosystem", + "dicti0nary-attack": "games & trivia/dicti0nary-attack", "did-you-actually-do-that": "patallm-gallery/did-you-actually-do-that", + "disinfo-nesy-detector": "neural-foundations/satellites/neurosymbolic/disinfo-nesy-detector", "dnfinition": "ambientops/total-update/elixir/dnfinition", + "echidnabot": "echidna/echidnabot", + "eclexia": "nextgen-languages/eclexia", + "elegant-state": "neural-foundations/satellites/agentic/elegant-state", + "error-lang": "nextgen-languages/error-lang", + "esn": "neural-foundations/satellites/neurosymbolic/esn", "finishingbot": "gitbot-fleet/bots/finishingbot", + "fogbinder": "zotero-tools/fogbinder", + "formdb-http": "nextgen-databases/lithoglyph/formdb-http", + "games": "games & trivia", "glambot": "gitbot-fleet/bots/glambot", + "glyphbase": "nextgen-databases/lithoglyph/glyphbase", + "gql-dt": "nextgen-databases/lithoglyph/gql-dt", + "hotchocolabot": "hotchocolabot", + "hybrid-automation-router": "ambientops/hybrid-automation-router", "idris2-ecosystem": "developer-ecosystem/idris2-ecosystem", "immutable-linux-auditor": "ambientops/immutable-linux-auditor", "indieweb2-bastion": "civic-connect/indieweb2-bastion", + "julia-the-viper": "nextgen-languages/julia-the-viper", "k9-svc": "standards/k9-svc", + "kea-tools": "kea/kea-tools", "kith": "developer-ecosystem/well-known-ecosystem/kith", + "language-bridges": "nextgen-languages/language-bridges", + "language-interop-compiler": "nextgen-languages/language-interop-compiler", + "lithoglyph": "nextgen-databases/lithoglyph", "llm-tools": "patallm-gallery/llm-tools", + "lol": "standards/lol", + "lsm": "neural-foundations/satellites/neurosymbolic/lsm", + "mcp-repo-guardian": "standards/0-ai-gatekeeper-protocol/mcp-repo-guardian", + "my-lang": "nextgen-languages/my-lang", "nerdsafe-restart": "ambientops/nerdsafe-restart", "nick-shells": "ambientops/nick-shells", + "oblibeny": "nextgen-languages/oblibeny", + "package-publishers": "developer-ecosystem/package-publishers", "personal-sysadmin": "ambientops/personal-sysadmin", + "phantom-metal-taste": "games & trivia/phantom-metal-taste", + "phronesis": "nextgen-languages/phronesis", + "poly-k8s-mcp": "flatracoon/netstack/modules/poly-k8s-mcp", + "poly-secret-mcp": "flatracoon/netstack/modules/poly-secret-mcp", + "qubes-sdp": "aerie/qubes-sdp", + "reasonably-good-token-vault": "ambientops/reasonably-good-token-vault", + "recon-silly-ation": "developer-ecosystem/satellites/developer-ux/recon-silly-ation", + "repo-batcher": "reposystem/scaffoldia/repo-batcher", + "repo-guardian-fs": "standards/0-ai-gatekeeper-protocol/repo-guardian-fs", + "rescript-ecosystem": "developer-ecosystem/rescript-ecosystem", "rhodibot": "gitbot-fleet/bots/rhodibot", "rhodium-standard-repositories": "standards/rhodium-standard-repositories", + "robot-repo-automaton": "developer-ecosystem/satellites/repo-management/robot-repo-automaton", + "safe-brute-force": "games & trivia/safe-brute-force", + "scaffoldia": "reposystem/scaffoldia", "seambot": "gitbot-fleet/bots/seambot", "selur": "odds-and-sods-package-manager/services/selur", "sustainabot": "gitbot-fleet/bots/sustainabot", + "svalinn": "project-wharf/infra/svalinn", "system-tools": "ambientops/system-tools", "test-repo": "hypatia/integration/fixtures/test-repo", + "thejeffparadox": "games & trivia/thejeffparadox", "total-update": "ambientops/total-update", + "union-policy-parser": "palimpsest-plasma/union-policy-parser", + "verified-container-spec": "stapeln/verified-container-spec", + "verisimdb": "nextgen-databases/verisimdb", + "vordr": "stapeln/container-stack/vordr", "well-known-ecosystem": "developer-ecosystem/well-known-ecosystem", "zig-ffi": "developer-ecosystem/rescript-ecosystem/packages/ffi/zig-ffi" } diff --git a/scripts/tests/dispatch-paths.sh b/scripts/tests/dispatch-paths.sh new file mode 100644 index 00000000..01428eb7 --- /dev/null +++ b/scripts/tests/dispatch-paths.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# Exercise dispatch orchestration with an inert fix executable and isolated data. +set -euo pipefail +fleet_test_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +fixture="$(mktemp -d)" +trap 'rm -rf "$fixture"' EXIT +mkdir -p "$fixture/repos/sample" "$fixture/checkout/scripts" "$fixture/primary/dispatch" +cp "$fleet_test_root/scripts/repo-path-overrides.json" "$fixture/checkout/scripts/" + +run_dispatch() { + env -u GITHUB_TOKEN -u FLEET_DISPATCH_TOKEN \ + FLEET_ROOT="$fixture/checkout" REPOS_BASE="$fixture/repos" \ + HYPATIA_DATA="$fixture/primary" VERISIMDB_DATA="$1" \ + HYPATIA_OUTCOME_REPORT=off KIN_DIR="$fixture/kin" RRA_BIN=/bin/true \ + bash "$fleet_test_root/scripts/dispatch-runner.sh" --limit 1 +} + +manifest="$fixture/primary/dispatch/pending.jsonl" +printf '%s\n' '{"repo":"sample","tier":"eliminate","strategy":"auto_execute","pattern_id":"fixture","recipe_id":"none","auto_fixable":true}' > "$manifest" +# Default primary and central stores are the same; exactly one JSONL record. +run_dispatch "$fixture/primary/." +month="$(date -u +%Y-%m)" +test "$(wc -l < "$fixture/primary/outcomes/$month.jsonl")" -eq 1 +jq -e '.outcome == "success"' "$fixture/primary/outcomes/$month.jsonl" >/dev/null +# Separate stores both receive one copy of this new outcome. +run_dispatch "$fixture/central" +test "$(wc -l < "$fixture/primary/outcomes/$month.jsonl")" -eq 2 +test "$(wc -l < "$fixture/central/outcomes/$month.jsonl")" -eq 1 + +printf '%s\n' '{"repo":"sample","tier":"control","strategy":"review","pattern_id":"review-fixture"}' > "$manifest" +run_dispatch "$fixture/central" +test -s "$fixture/checkout/shared-context/findings/pending/sample--review-fixture.json" +printf '%s\n' '{"repo":"sample","tier":"control","strategy":"report_only","pattern_id":"advisory-fixture"}' > "$manifest" +run_dispatch "$fixture/central" +test -s "$fixture/checkout/shared-context/advisories/$month.jsonl" +test ! -d "$fixture/repos/gitbot-fleet" + +# Legacy aliases remain fallbacks; direct checkouts still take precedence. +mkdir -p "$fixture/repos/developer-ecosystem/julia-ecosystem/packages/Axiom.jl" +printf '%s\n' '{"repo":"Axiom.jl","tier":"control","strategy":"review","pattern_id":"alias-fixture"}' > "$manifest" +run_dispatch "$fixture/central" +test -s "$fixture/checkout/shared-context/findings/pending/Axiom.jl--alias-fixture.json" +echo 'Dispatch path and outcome contracts passed' From 6075f0b37cba4e088cdf7e18ab372fefaccf1e1c Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Mon, 7 Sep 2026 03:38:42 +0100 Subject: [PATCH 04/16] ci(pages): retry pinned Haskell setup after scoped policy approval --- .github/workflows/casket-pages.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/casket-pages.yml b/.github/workflows/casket-pages.yml index d14b9191..c25ba2a2 100644 --- a/.github/workflows/casket-pages.yml +++ b/.github/workflows/casket-pages.yml @@ -45,6 +45,7 @@ jobs: .casket-ssg/dist-newstyle key: ${{ runner.os }}-casket-${{ hashFiles('.casket-ssg/casket-ssg.cabal') }} + # Explicitly allowed by repository policy; actions.lock pins its commit. - name: Select tested Haskell toolchain uses: haskell-actions/setup@v2.12.0 with: From 2ac241d8ffa5fbda945a4155535baeea361cbaaa 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 05/16] Secure GSBot TLS dependencies and enforce its build and audit --- .github/workflows/casket-pages.yml | 4 +- .github/workflows/rust.yml | 22 ++ bots/gsbot/Cargo.lock | 448 ++++++++++------------------- bots/gsbot/Cargo.toml | 6 +- bots/gsbot/deny.toml | 6 + 5 files changed, 189 insertions(+), 297 deletions(-) create mode 100644 bots/gsbot/deny.toml diff --git a/.github/workflows/casket-pages.yml b/.github/workflows/casket-pages.yml index c25ba2a2..19e451c6 100644 --- a/.github/workflows/casket-pages.yml +++ b/.github/workflows/casket-pages.yml @@ -61,7 +61,7 @@ jobs: - name: Build casket-ssg working-directory: .casket-ssg - run: cabal build + run: cabal build --index-state=2026-09-06T00:00:00Z - name: Prepare site source shell: bash @@ -109,7 +109,7 @@ jobs: - name: Build site run: | mkdir -p _site - cd .casket-ssg && cabal run casket-ssg -- build ../.site-src ../_site + cd .casket-ssg && cabal run --index-state=2026-09-06T00:00:00Z casket-ssg -- build ../.site-src ../_site touch ../_site/.nojekyll - name: Setup Pages diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 8fea3993..6cddf848 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -59,3 +59,25 @@ jobs: with: persist-credentials: false - run: bash scripts/tests/dispatch-paths.sh + + gsbot: + name: GSBot build, tests and dependency security + runs-on: ubuntu-24.04 + timeout-minutes: 30 + defaults: + run: + working-directory: bots/gsbot + steps: + - uses: actions/checkout@v7.0.1 + with: + persist-credentials: false + - name: Install native TLS build dependencies + run: sudo apt-get update && sudo apt-get install -y pkg-config libssl-dev + - name: Build and test SQLite services + run: cargo test --locked --all-targets + - name: Install dependency graph auditor + run: cargo install cargo-deny --version 0.20.2 --locked + - name: Audit enabled dependencies (no advisory exclusions) + # Poise's unmaintained derive macro is reported as maintenance debt; + # vulnerabilities, unsoundness and yanked crates remain blocking. + run: cargo deny --locked --config deny.toml check advisories --warn unmaintained diff --git a/bots/gsbot/Cargo.lock b/bots/gsbot/Cargo.lock index fa05e6f2..72d40df7 100644 --- a/bots/gsbot/Cargo.lock +++ b/bots/gsbot/Cargo.lock @@ -34,9 +34,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "arrayvec" @@ -180,23 +180,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - -[[package]] -name = "chacha20" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" -dependencies = [ - "cfg-if", - "cpufeatures 0.3.0", - "rand_core 0.10.1", -] - [[package]] name = "chrono" version = "0.4.44" @@ -211,21 +194,22 @@ dependencies = [ "windows-link", ] -[[package]] -name = "concurrent-queue" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "const-oid" version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -241,15 +225,6 @@ dependencies = [ "libc", ] -[[package]] -name = "cpufeatures" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" -dependencies = [ - "libc", -] - [[package]] name = "crc" version = "3.4.0" @@ -471,11 +446,10 @@ dependencies = [ [[package]] name = "event-listener" -version = "5.4.1" +version = "5.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" dependencies = [ - "concurrent-queue", "parking", "pin-project-lite", ] @@ -525,6 +499,21 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -649,10 +638,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", - "js-sys", "libc", "wasi", - "wasm-bindgen", ] [[package]] @@ -662,13 +649,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", - "js-sys", "libc", "r-efi", - "rand_core 0.10.1", "wasip2", "wasip3", - "wasm-bindgen", ] [[package]] @@ -685,7 +669,7 @@ dependencies = [ "chrono", "dotenvy", "poise", - "rand 0.8.6", + "rand", "serde", "serde_json", "serenity", @@ -829,19 +813,19 @@ dependencies = [ ] [[package]] -name = "hyper-rustls" -version = "0.27.9" +name = "hyper-tls" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ - "http", + "bytes", + "http-body-util", "hyper", "hyper-util", - "rustls 0.23.40", + "native-tls", "tokio", - "tokio-rustls 0.26.4", + "tokio-native-tls", "tower-service", - "webpki-roots 1.0.7", ] [[package]] @@ -1119,12 +1103,6 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" -[[package]] -name = "lru-slab" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" - [[package]] name = "matchers" version = "0.2.0" @@ -1202,6 +1180,23 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -1222,7 +1217,7 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand 0.8.6", + "rand", "smallvec", "zeroize", ] @@ -1269,6 +1264,49 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "openssl" +version = "0.10.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "parking" version = "2.2.1" @@ -1425,62 +1463,6 @@ dependencies = [ "unicase", ] -[[package]] -name = "quinn" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" -dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash", - "rustls 0.23.40", - "socket2", - "thiserror 2.0.18", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-proto" -version = "0.11.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" -dependencies = [ - "bytes", - "getrandom 0.4.2", - "lru-slab", - "rand 0.10.2", - "rand_pcg", - "ring", - "rustc-hash", - "rustls 0.23.40", - "rustls-pki-types", - "slab", - "thiserror 2.0.18", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2", - "tracing", - "windows-sys 0.60.2", -] - [[package]] name = "quote" version = "1.0.45" @@ -1504,18 +1486,7 @@ checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", "rand_chacha", - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" -dependencies = [ - "chacha20", - "getrandom 0.4.2", - "rand_core 0.10.1", + "rand_core", ] [[package]] @@ -1525,7 +1496,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core 0.6.4", + "rand_core", ] [[package]] @@ -1537,21 +1508,6 @@ dependencies = [ "getrandom 0.2.17", ] -[[package]] -name = "rand_core" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" - -[[package]] -name = "rand_pcg" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" -dependencies = [ - "rand_core 0.10.1", -] - [[package]] name = "redox_syscall" version = "0.5.18" @@ -1613,22 +1569,21 @@ dependencies = [ "http-body", "http-body-util", "hyper", - "hyper-rustls", + "hyper-tls", "hyper-util", "js-sys", "log", "mime_guess", + "native-tls", "percent-encoding", "pin-project-lite", - "quinn", - "rustls 0.23.40", "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", "tokio", - "tokio-rustls 0.26.4", + "tokio-native-tls", "tokio-util", "tower", "tower-http", @@ -1638,7 +1593,6 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots 1.0.7", ] [[package]] @@ -1668,7 +1622,7 @@ dependencies = [ "num-traits", "pkcs1", "pkcs8", - "rand_core 0.6.4", + "rand_core", "signature", "spki", "subtle", @@ -1694,20 +1648,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "rustls" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" -dependencies = [ - "log", - "ring", - "rustls-pki-types", - "rustls-webpki 0.102.8", - "subtle", - "zeroize", -] - [[package]] name = "rustls" version = "0.23.40" @@ -1717,7 +1657,7 @@ dependencies = [ "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.103.13", + "rustls-webpki", "subtle", "zeroize", ] @@ -1728,21 +1668,9 @@ version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ - "web-time", "zeroize", ] -[[package]] -name = "rustls-webpki" -version = "0.102.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", -] - [[package]] name = "rustls-webpki" version = "0.103.13" @@ -1775,6 +1703,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -1791,6 +1728,29 @@ dependencies = [ "zeroize", ] +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.28" @@ -1917,7 +1877,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures 0.2.17", + "cpufeatures", "digest", ] @@ -1928,7 +1888,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures 0.2.17", + "cpufeatures", "digest", ] @@ -1964,7 +1924,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest", - "rand_core 0.6.4", + "rand_core", ] [[package]] @@ -2015,9 +1975,9 @@ dependencies = [ [[package]] name = "spin" -version = "0.9.8" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" dependencies = [ "lock_api", ] @@ -2069,7 +2029,7 @@ dependencies = [ "memchr", "once_cell", "percent-encoding", - "rustls 0.23.40", + "rustls", "serde", "serde_json", "sha2", @@ -2150,7 +2110,7 @@ dependencies = [ "memchr", "once_cell", "percent-encoding", - "rand 0.8.6", + "rand", "rsa", "serde", "sha1", @@ -2189,7 +2149,7 @@ dependencies = [ "md-5", "memchr", "once_cell", - "rand 0.8.6", + "rand", "serde", "serde_json", "sha2", @@ -2455,23 +2415,12 @@ dependencies = [ ] [[package]] -name = "tokio-rustls" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" -dependencies = [ - "rustls 0.22.4", - "rustls-pki-types", - "tokio", -] - -[[package]] -name = "tokio-rustls" -version = "0.26.4" +name = "tokio-native-tls" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" dependencies = [ - "rustls 0.23.40", + "native-tls", "tokio", ] @@ -2494,12 +2443,10 @@ checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" dependencies = [ "futures-util", "log", - "rustls 0.22.4", - "rustls-pki-types", + "native-tls", "tokio", - "tokio-rustls 0.25.0", + "tokio-native-tls", "tungstenite", - "webpki-roots 0.26.11", ] [[package]] @@ -2665,9 +2612,8 @@ dependencies = [ "http", "httparse", "log", - "rand 0.8.6", - "rustls 0.22.4", - "rustls-pki-types", + "native-tls", + "rand", "sha1", "thiserror 1.0.69", "url", @@ -2964,16 +2910,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - [[package]] name = "webpki-roots" version = "0.26.11" @@ -3008,7 +2944,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]] @@ -3088,15 +3024,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - [[package]] name = "windows-sys" version = "0.61.2" @@ -3130,30 +3057,13 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", + "windows_i686_gnullvm", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -3166,12 +3076,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -3184,12 +3088,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -3202,24 +3100,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -3232,12 +3118,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -3250,12 +3130,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -3268,12 +3142,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -3286,12 +3154,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - [[package]] name = "wit-bindgen" version = "0.51.0" diff --git a/bots/gsbot/Cargo.toml b/bots/gsbot/Cargo.toml index 95648940..9e6d3e9f 100644 --- a/bots/gsbot/Cargo.toml +++ b/bots/gsbot/Cargo.toml @@ -32,9 +32,11 @@ path = "src/lib.rs" [dependencies] # Discord (prefix-command framework over serenity — faithful map of discord.py cogs) -poise = "0.6.2" +# Serenity 0.12's WebSocket rustls backend requires the unpatched webpki 0.102 +# series. Use its supported native TLS backend until that chain is upgraded. +poise = { version = "0.6.2", default-features = false, features = ["cache", "chrono", "handle_panics"] } serenity = { version = "0.12.5", default-features = false, features = [ - "client", "gateway", "rustls_backend", "model", "cache", + "client", "gateway", "native_tls_backend", "model", "cache", ] } tokio = { version = "1.52.3", features = ["macros", "rt-multi-thread", "signal"] } diff --git a/bots/gsbot/deny.toml b/bots/gsbot/deny.toml new file mode 100644 index 00000000..41c73c8d --- /dev/null +++ b/bots/gsbot/deny.toml @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: MPL-2.0 +# Audit the actual enabled dependency graph with cargo-deny. In particular, +# SQLx's unused MySQL backend is not part of this SQLite application's graph. +# No vulnerability advisories are ignored. +[advisories] +version = 2 From 0ba4386d3e18e873c89d7c8f0a3aee868837047c 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:51 +0100 Subject: [PATCH 06/16] Complete canonical skeleton and honour configured review storage --- fleet-coordinator.sh | 2 +- robot-repo-automaton/src/skeleton.rs | 4 ++++ .../skeleton/.machine_readable/descriptiles/ANCHOR.a2ml | 9 +++++++++ 3 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 robot-repo-automaton/templates/skeleton/.machine_readable/descriptiles/ANCHOR.a2ml diff --git a/fleet-coordinator.sh b/fleet-coordinator.sh index 26a6c272..0f514551 100755 --- a/fleet-coordinator.sh +++ b/fleet-coordinator.sh @@ -319,7 +319,7 @@ process_findings() { local review_script="$FLEET_DIR/scripts/process-review-findings.sh" if [[ -x "$review_script" ]]; then log_bot "rhodibot" "Processing $total_substitute substitute-tier findings for review" - FLEET_BASE="$FLEET_DIR" "$review_script" 2>&1 | sed 's/^/ /' || log_warn "Review processing had errors" + FLEET_BASE="${FLEET_ROOT:-$FLEET_DIR}" "$review_script" 2>&1 | sed 's/^/ /' || log_warn "Review processing had errors" else log_warn "Review processor not found at $review_script" fi diff --git a/robot-repo-automaton/src/skeleton.rs b/robot-repo-automaton/src/skeleton.rs index 4d4b51c3..04e900eb 100644 --- a/robot-repo-automaton/src/skeleton.rs +++ b/robot-repo-automaton/src/skeleton.rs @@ -40,6 +40,10 @@ pub const SKELETON: &[(&str, &str)] = &[ include_str!("../templates/skeleton/.tool-versions"), ), ("Justfile", include_str!("../templates/skeleton/Justfile")), + ( + ".machine_readable/descriptiles/ANCHOR.a2ml", + include_str!("../templates/skeleton/.machine_readable/descriptiles/ANCHOR.a2ml"), + ), ( ".machine_readable/descriptiles/META.a2ml", include_str!("../templates/skeleton/.machine_readable/descriptiles/META.a2ml"), diff --git a/robot-repo-automaton/templates/skeleton/.machine_readable/descriptiles/ANCHOR.a2ml b/robot-repo-automaton/templates/skeleton/.machine_readable/descriptiles/ANCHOR.a2ml new file mode 100644 index 00000000..a9326786 --- /dev/null +++ b/robot-repo-automaton/templates/skeleton/.machine_readable/descriptiles/ANCHOR.a2ml @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) +# Populate the repository identity when instantiating this skeleton. + +name = "PROJECT_NAME" +id = "org.hyperpolymath.PROJECT_NAME" +version = "0.1.0" +status = "template" +canonical-descriptors = ".machine_readable/descriptiles/" From 87dea45c34d0a7febdc886cfd7657a4c09b50fa8 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Mon, 7 Sep 2026 04:33:06 +0100 Subject: [PATCH 07/16] Label artifact validation without creating a deployment record --- .github/workflows/casket-pages.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/casket-pages.yml b/.github/workflows/casket-pages.yml index 19e451c6..4d4e703e 100644 --- a/.github/workflows/casket-pages.yml +++ b/.github/workflows/casket-pages.yml @@ -121,10 +121,8 @@ jobs: path: '_site' preview: - name: Validate deployable Pages preview + name: Validate Pages artifact if: github.event_name == 'pull_request' - environment: - name: pages-preview runs-on: ubuntu-24.04 needs: build timeout-minutes: 10 From 57ccb8951917b29a4d7317e28f8d2c0389d82f18 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Mon, 7 Sep 2026 08:39:12 +0100 Subject: [PATCH 08/16] Honour scan roots and clarify the canonical template contract --- fleet-coordinator.sh | 2 +- robot-repo-automaton/src/main.rs | 6 +++--- robot-repo-automaton/src/skeleton.rs | 8 +++++++- .../skeleton/.machine_readable/descriptiles/META.a2ml | 6 +++--- .../skeleton/.machine_readable/descriptiles/PLAYBOOK.a2ml | 7 ++++--- 5 files changed, 18 insertions(+), 11 deletions(-) diff --git a/fleet-coordinator.sh b/fleet-coordinator.sh index 0f514551..ba4f6d5b 100755 --- a/fleet-coordinator.sh +++ b/fleet-coordinator.sh @@ -115,7 +115,7 @@ scan_supervised_repos() { local process_after=false local limit=0 local inventory="" - local repos_root="/var/mnt/eclipse/repos" + local repos_root="$REPOS_BASE" while [[ $# -gt 0 ]]; do case "$1" in diff --git a/robot-repo-automaton/src/main.rs b/robot-repo-automaton/src/main.rs index 494e4385..35269bc9 100644 --- a/robot-repo-automaton/src/main.rs +++ b/robot-repo-automaton/src/main.rs @@ -115,13 +115,13 @@ enum Commands { #[derive(Subcommand, Debug)] enum SkeletonAction { - /// Write the canonical RSR required-files set into a directory + /// Write the uninstantiated canonical RSR template (keeps project placeholders) Emit { /// Target directory (created if absent) #[arg(default_value = ".")] out: PathBuf, }, - /// Verify a repo's required files match canonical; non-zero exit on drift + /// Check the uninstantiated template against canonical; non-zero on drift Check { /// Repository root to verify #[arg(default_value = ".")] @@ -212,7 +212,7 @@ fn cmd_skeleton(action: SkeletonAction) -> anyhow::Result<()> { SkeletonAction::Emit { out } => { skeleton::emit(&out)?; println!( - "wrote {} canonical RSR skeleton file(s) to {}", + "wrote {} canonical RSR template file(s) to {} (project placeholders retained)", skeleton::SKELETON.len(), out.display() ); diff --git a/robot-repo-automaton/src/skeleton.rs b/robot-repo-automaton/src/skeleton.rs index 04e900eb..b9eb0d90 100644 --- a/robot-repo-automaton/src/skeleton.rs +++ b/robot-repo-automaton/src/skeleton.rs @@ -8,6 +8,11 @@ //! of the template structurally impossible to drift (#45/#47 closed the //! immediate sanitisation; this closes the stronger structural fix #48). //! +//! `emit` reproduces the **uninstantiated template repository**. Project +//! identity, purpose, kind, author and licence placeholders are intentional: +//! this command is not a project initializer. `check` compares that same +//! template, not a downstream project after its owner has customized it. +//! //! Source of truth = the **post-#47 canonical layout**, not the prose in //! `standards` `REQUIRED-FILES.md` (which is stale: it still lists root //! `*.scm` + `Mustfile`, predating the estate-wide `.scm`→`.a2ml` migration @@ -70,7 +75,8 @@ pub const SKELETON: &[(&str, &str)] = &[ ), ]; -/// Write the canonical skeleton into `out`, creating parent directories. +/// Write the uninstantiated canonical template into `out`, creating parents. +/// Project-specific placeholders remain for the downstream initialization step. pub fn emit(out: &Path) -> Result<()> { for (rel, content) in SKELETON { let dst = out.join(rel); diff --git a/robot-repo-automaton/templates/skeleton/.machine_readable/descriptiles/META.a2ml b/robot-repo-automaton/templates/skeleton/.machine_readable/descriptiles/META.a2ml index 6e5dda1e..01e7c75f 100644 --- a/robot-repo-automaton/templates/skeleton/.machine_readable/descriptiles/META.a2ml +++ b/robot-repo-automaton/templates/skeleton/.machine_readable/descriptiles/META.a2ml @@ -9,10 +9,10 @@ version = "0.1.0" last-updated = "2026-04-11" [project-info] -type = "library" # TODO: update type (library|binary|service|website|monorepo) # library | binary | monorepo | service | website +type = "{{PROJECT_KIND}}" # library | binary | monorepo | service | website languages = [] # e.g. ["rust", "zig", "idris2"] -license = "PMPL-1.0-or-later" -author = "Jonathan D.A. Jewell (hyperpolymath)" +license = "{{PROJECT_LICENSE}}" +author = "{{PROJECT_AUTHOR}}" [architecture-decisions] # ADR format: status = proposed | accepted | deprecated | superseded | rejected diff --git a/robot-repo-automaton/templates/skeleton/.machine_readable/descriptiles/PLAYBOOK.a2ml b/robot-repo-automaton/templates/skeleton/.machine_readable/descriptiles/PLAYBOOK.a2ml index 762170d0..75c597f8 100644 --- a/robot-repo-automaton/templates/skeleton/.machine_readable/descriptiles/PLAYBOOK.a2ml +++ b/robot-repo-automaton/templates/skeleton/.machine_readable/descriptiles/PLAYBOOK.a2ml @@ -13,13 +13,14 @@ 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 [release-process] -# 1. Update version in STATE.a2ml, META.a2ml, Justfile +# 1. Update version in .machine_readable/descriptiles/STATE.a2ml, +# .machine_readable/descriptiles/META.a2ml and Justfile # 2. Run `just release-preflight` (validate + quality + security + maint-hard-pass) # 3. Optional local permission hardening: `just perms-snapshot && just perms-lock` # 4. Tag and push @@ -61,7 +62,7 @@ enforcement-workflow = ".github/workflows/estate-rules.yml" # === Required directories === # .github/ CONTRIBUTING.md, CODE_OF_CONDUCT.md, SECURITY.md, workflows/ -# .machine_readable/ AI manifests (0.1-AI-MANIFEST.a2ml), 6a2/ checkpoints, +# .machine_readable/ AI manifests (0.1-AI-MANIFEST.a2ml), descriptiles/ checkpoints, # contractiles/, configs/, anchors/, policies/, scripts/, svc/ # build/ contractile.just, guix.scm, Containerfile, # just/*.just (Justfile section imports) From dcbbe22bff7dcf1a21a5d942735197efef65e4b4 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Mon, 7 Sep 2026 08:55:59 +0100 Subject: [PATCH 09/16] fix(pages): link PR artifacts to the checked-out commit --- .github/workflows/casket-pages.yml | 8 +++++--- robot-repo-automaton/src/main.rs | 2 ++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/casket-pages.yml b/.github/workflows/casket-pages.yml index 4d4e703e..3a06c436 100644 --- a/.github/workflows/casket-pages.yml +++ b/.github/workflows/casket-pages.yml @@ -65,6 +65,8 @@ jobs: - name: Prepare site source shell: bash + env: + CONTENT_REF: ${{ github.event_name == 'pull_request' && github.sha || github.ref_name }} run: | set -euo pipefail rm -rf .site-src _site @@ -79,9 +81,9 @@ jobs: README_URL="" if [ -f README.md ]; then - README_URL="${REPO_URL}/blob/${{ github.ref_name }}/README.md" + README_URL="${REPO_URL}/blob/${CONTENT_REF}/README.md" elif [ -f README.adoc ]; then - README_URL="${REPO_URL}/blob/${{ github.ref_name }}/README.adoc" + README_URL="${REPO_URL}/blob/${CONTENT_REF}/README.adoc" fi { @@ -99,7 +101,7 @@ jobs: echo "- README: [project README](${README_URL})" fi if [ -d docs ]; then - echo "- Docs directory: [docs/](${REPO_URL}/tree/${{ github.ref_name }}/docs)" + echo "- Docs directory: [docs/](${REPO_URL}/tree/${CONTENT_REF}/docs)" fi echo echo "Project-specific site content can be added later under site/." diff --git a/robot-repo-automaton/src/main.rs b/robot-repo-automaton/src/main.rs index 35269bc9..544c7b43 100644 --- a/robot-repo-automaton/src/main.rs +++ b/robot-repo-automaton/src/main.rs @@ -745,6 +745,8 @@ fn cmd_catalog(path: &Path, severity_filter: Option<&str>) -> anyhow::Result<()> /// Base directory holding local repo checkouts. /// /// Override with `REPOS_BASE`; otherwise defaults to the canonical estate tree. +/// Legacy checkout locations can also be selected through `REPOS_BASE`; the +/// former literal `/var$REPOS_DIR` path did not expand a shell variable in Rust. fn repos_base() -> PathBuf { if let Ok(base) = std::env::var("REPOS_BASE") { if !base.is_empty() { From f92d700e0b7f3620a9c01b6078f0a708c3eb286c Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Mon, 7 Sep 2026 09:33:53 +0100 Subject: [PATCH 10/16] ci: run real Scorecard analysis for pull requests --- .github/workflows/actions.lock | 16 +++++++++++- .github/workflows/scorecard.yml | 46 ++++++++++++++++++++++++++++++--- 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/.github/workflows/actions.lock b/.github/workflows/actions.lock index de2729bf..df6a1141 100644 --- a/.github/workflows/actions.lock +++ b/.github/workflows/actions.lock @@ -51,7 +51,11 @@ workflows: - 'actions/checkout@v7.0.1' '.github/workflows/rust.yml': - 'actions/checkout@v7.0.1' - '.github/workflows/scorecard.yml': [] + '.github/workflows/scorecard.yml': + - 'actions/checkout@v7.0.1' + - 'actions/upload-artifact@v7.0.1' + - 'github/codeql-action@v4.37.3' + - 'ossf/scorecard-action@v2.4.4' '.github/workflows/secret-scanner.yml': [] '.github/workflows/supervised-fleet-scan.yml': - 'actions/checkout@v7.0.1' @@ -103,6 +107,11 @@ dependencies: commit: 'sha1-2c7215f132e9ebf062739d9130488b56d53c060c' owner_id: 1940490 repo_id: 260749683 + 'github/codeql-action@v4.37.3': + ref: 'v4.37.3' + commit: 'sha1-e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81' + owner_id: 9919 + repo_id: 259445878 'github/codeql-action@v4.37.8': ref: 'v4.37.8' commit: 'sha1-db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28' @@ -128,6 +137,11 @@ dependencies: commit: 'sha1-ede1191ef6ff3ac02c4f4d9efdf837ee517e11d7' owner_id: 6759885 repo_id: 1352485172 + 'ossf/scorecard-action@v2.4.4': + ref: 'v2.4.4' + commit: 'sha1-2d1146689b8cda280b9bc96326124645441f03bc' + owner_id: 67707773 + repo_id: 421101922 'peter-evans/repository-dispatch@v4.0.1': ref: 'v4.0.1' commit: 'sha1-28959ce8df70de7be546dd1250a005dd32156697' diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 833b2c87..eaa649cb 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -1,19 +1,59 @@ -# SPDX-License-Identifier: MPL-2.0 # This workflow is managed by gh actions-lock. +# SPDX-License-Identifier: MPL-2.0 # This workflow is managed by gh actions-lock. name: Scorecards supply-chain security on: + push: + branches: [main] + pull_request: + branches: [main] branch_protection_rule: schedule: - cron: '23 4 * * 1' -permissions: read-all +permissions: + contents: read jobs: analysis: + if: github.event_name != 'pull_request' permissions: + contents: read security-events: write id-token: write uses: hyperpolymath/standards/.github/workflows/scorecard-reusable.yml@571cc734cd69fb846032ec77a662aa8ee4fc32cd - secrets: inherit + + pull-request-analysis: + name: Scorecard PR analysis + if: github.event_name == 'pull_request' + runs-on: ubuntu-24.04 + timeout-minutes: 15 + permissions: + actions: read + contents: read + security-events: write + steps: + - uses: actions/checkout@v7.0.1 + with: + persist-credentials: false + # The shared publisher targets the default branch. PRs need their own + # real SARIF analysis without OIDC or publishing to the public badge API. + # OpenSSF currently labels its pull_request trigger experimental. + - name: Analyze the pull request with OpenSSF Scorecard + uses: ossf/scorecard-action@v2.4.4 + with: + results_file: results.sarif + results_format: sarif + publish_results: false + - name: Upload Scorecard SARIF + uses: github/codeql-action/upload-sarif@v4.37.3 + with: + sarif_file: results.sarif + - name: Retain scan evidence + uses: actions/upload-artifact@v7.0.1 + with: + name: scorecard-pr-results + path: results.sarif + if-no-files-found: error + retention-days: 14 From 54a78a21cf7eb6bf02b190b0ba8c26228f4b069e Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Mon, 7 Sep 2026 09:36:46 +0100 Subject: [PATCH 11/16] ci: lock every runtime Scorecard dependency locally --- .github/workflows/actions.lock | 7 +------ .github/workflows/scorecard.yml | 32 +++++++++++++++++++++++++++++--- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/.github/workflows/actions.lock b/.github/workflows/actions.lock index df6a1141..65437a74 100644 --- a/.github/workflows/actions.lock +++ b/.github/workflows/actions.lock @@ -54,7 +54,7 @@ workflows: '.github/workflows/scorecard.yml': - 'actions/checkout@v7.0.1' - 'actions/upload-artifact@v7.0.1' - - 'github/codeql-action@v4.37.3' + - 'github/codeql-action@v4.37.8' - 'ossf/scorecard-action@v2.4.4' '.github/workflows/secret-scanner.yml': [] '.github/workflows/supervised-fleet-scan.yml': @@ -107,11 +107,6 @@ dependencies: commit: 'sha1-2c7215f132e9ebf062739d9130488b56d53c060c' owner_id: 1940490 repo_id: 260749683 - 'github/codeql-action@v4.37.3': - ref: 'v4.37.3' - commit: 'sha1-e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81' - owner_id: 9919 - repo_id: 259445878 'github/codeql-action@v4.37.8': ref: 'v4.37.8' commit: 'sha1-db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28' diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index eaa649cb..79f38695 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -18,11 +18,37 @@ permissions: jobs: analysis: if: github.event_name != 'pull_request' + name: Publish default-branch Scorecard + runs-on: ubuntu-24.04 + timeout-minutes: 15 permissions: + actions: read contents: read security-events: write id-token: write - uses: hyperpolymath/standards/.github/workflows/scorecard-reusable.yml@571cc734cd69fb846032ec77a662aa8ee4fc32cd + # These are the publishing steps from standards at 571cc734. Keep them + # local so every action is covered by this workflow's runtime lockfile. + steps: + - uses: actions/checkout@v7.0.1 + with: + persist-credentials: false + - name: Analyze and publish default-branch Scorecard + uses: ossf/scorecard-action@v2.4.4 + with: + results_file: results.sarif + results_format: sarif + publish_results: true + - name: Upload Scorecard SARIF + uses: github/codeql-action/upload-sarif@v4.37.8 + with: + sarif_file: results.sarif + - name: Retain scan evidence + uses: actions/upload-artifact@v7.0.1 + with: + name: scorecard-results + path: results.sarif + if-no-files-found: error + retention-days: 90 pull-request-analysis: name: Scorecard PR analysis @@ -37,7 +63,7 @@ jobs: - uses: actions/checkout@v7.0.1 with: persist-credentials: false - # The shared publisher targets the default branch. PRs need their own + # The publisher targets the default branch. PRs need their own # real SARIF analysis without OIDC or publishing to the public badge API. # OpenSSF currently labels its pull_request trigger experimental. - name: Analyze the pull request with OpenSSF Scorecard @@ -47,7 +73,7 @@ jobs: results_format: sarif publish_results: false - name: Upload Scorecard SARIF - uses: github/codeql-action/upload-sarif@v4.37.3 + uses: github/codeql-action/upload-sarif@v4.37.8 with: sarif_file: results.sarif - name: Retain scan evidence From e1fba6ce20925cdc8db7f61ba31bda43bd50daee Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Wed, 9 Sep 2026 23:14:33 +0100 Subject: [PATCH 12/16] fix(fleet): safely repair retired descriptile policy references --- .github/workflows/e2e.yml | 2 ++ scripts/fix-retired-descriptile-policy.sh | 36 +++++++++++++++++++++++ scripts/fix-script-registry.json | 6 ++-- tests/retired-descriptile-policy-test.sh | 29 ++++++++++++++++++ 4 files changed, 71 insertions(+), 2 deletions(-) create mode 100755 scripts/fix-retired-descriptile-policy.sh create mode 100644 tests/retired-descriptile-policy-test.sh diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index e831600d..3ad7cf65 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -21,3 +21,5 @@ jobs: run: pip install pyyaml --quiet - name: Run E2E tests run: bash tests/e2e.sh + - name: Verify canonical descriptile policy repair + run: bash tests/retired-descriptile-policy-test.sh diff --git a/scripts/fix-retired-descriptile-policy.sh b/scripts/fix-retired-descriptile-policy.sh new file mode 100755 index 00000000..8fb15e66 --- /dev/null +++ b/scripts/fix-retired-descriptile-policy.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# Repair only a finding's policy file, after its canonical targets exist. +set -euo pipefail +REPO_PATH="${1:?Usage: $0 }" +FINDING_JSON="${2:?Missing finding JSON file}" +ruby - "$REPO_PATH" "$FINDING_JSON" <<'RUBY' +require 'json' +require 'pathname' + +root = File.realpath(ARGV.fetch(0)) +finding = JSON.parse(File.read(ARGV.fetch(1))) +relative = finding['file'] || finding.dig('location', 'file') +abort 'Finding must name a policy file' unless relative.is_a?(String) && !relative.empty? +path = Pathname.new(relative) +abort 'Finding path must be repository-relative' if path.absolute? || path.each_filename.include?('..') +allowed = %w[Justfile justfile].include?(relative) || + (path.dirname.to_s == '.github/workflows' && %w[.yml .yaml].include?(path.extname)) || + (%w[scripts .githooks].include?(path.dirname.to_s) && path.extname == '.sh') +target = File.join(root, relative) +unless allowed && !File.symlink?(target) && File.realpath(target).start_with?(root + '/') + abort 'Refusing a path outside supported repository policy files' +end +pattern = %r{\.machine_readable/(?:6a2/)?(STATE|META|ECOSYSTEM|AGENTIC|NEUROSYM|PLAYBOOK|ANCHOR)\.a2ml} +content = File.read(target) +names = content.scan(pattern).flatten.uniq +names.each do |name| + canonical = File.join(root, '.machine_readable/descriptiles', name + '.a2ml') + unless !File.symlink?(canonical) && File.file?(canonical) && File.realpath(canonical).start_with?(root + '/') + abort "Canonical descriptile missing or unsafe: #{name}; reconcile files before repairing policy" + end +end +updated = content.gsub(pattern) { ".machine_readable/descriptiles/#{$1}.a2ml" } +File.write(target, updated) unless updated == content +puts "#{relative}: #{names.length} descriptile reference target(s) reconciled" +RUBY diff --git a/scripts/fix-script-registry.json b/scripts/fix-script-registry.json index 7904e327..c2f6bb5c 100644 --- a/scripts/fix-script-registry.json +++ b/scripts/fix-script-registry.json @@ -48,7 +48,8 @@ "TrackedNpmLockfile": "fix-tracked-package-lock.sh", "LicensePMPLDrift": "fix-pmpl-drift.sh", "ActionsPolicyTooRestrictive": "fix-actions-policy.sh", - "SessionDetritus": "fix-session-detritus.sh" + "SessionDetritus": "fix-session-detritus.sh", + "RetiredDescriptilePolicy": "fix-retired-descriptile-policy.sh" }, "by_recipe": { "recipe-actions-allow-all": "fix-actions-policy.sh", @@ -109,7 +110,8 @@ "chapel-replace-chpl-about-with-version": "fix-chapel-replace-chpl-about.sh", "reusable_workflow_sha_bump_needs_propagation": "propagate-sha-bump.sh", "recipe-fix-pmpl-drift": "fix-pmpl-drift.sh", - "recipe-archive-session-detritus": "fix-session-detritus.sh" + "recipe-archive-session-detritus": "fix-session-detritus.sh", + "recipe-retired-descriptile-policy": "fix-retired-descriptile-policy.sh" } } } diff --git a/tests/retired-descriptile-policy-test.sh b/tests/retired-descriptile-policy-test.sh new file mode 100644 index 00000000..474c65c3 --- /dev/null +++ b/tests/retired-descriptile-policy-test.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +TEMP_DIR=$(mktemp -d) +trap 'rm -rf "$TEMP_DIR"' EXIT +mkdir -p "$TEMP_DIR/repo/.github/workflows" "$TEMP_DIR/repo/.machine_readable/descriptiles" +printf '%s\n' 'test -f .machine_readable/STATE.a2ml' > "$TEMP_DIR/repo/.github/workflows/check.yml" +printf '%s\n' '{"file":".github/workflows/check.yml"}' > "$TEMP_DIR/finding.json" +FIXER="$ROOT/scripts/fix-retired-descriptile-policy.sh" +# Missing canonical data must not be papered over with a reference rewrite. +if bash "$FIXER" "$TEMP_DIR/repo" "$TEMP_DIR/finding.json"; then + echo 'FAIL: repaired policy without its canonical target' >&2; exit 1 +fi +grep -Fxq 'test -f .machine_readable/STATE.a2ml' "$TEMP_DIR/repo/.github/workflows/check.yml" +printf '%s\n' '[metadata]' 'name = "fixture"' > "$TEMP_DIR/repo/.machine_readable/descriptiles/STATE.a2ml" +bash "$FIXER" "$TEMP_DIR/repo" "$TEMP_DIR/finding.json" +grep -Fxq 'test -f .machine_readable/descriptiles/STATE.a2ml' "$TEMP_DIR/repo/.github/workflows/check.yml" +bash "$FIXER" "$TEMP_DIR/repo" "$TEMP_DIR/finding.json" +printf '%s\n' '{"file":"../outside"}' > "$TEMP_DIR/finding.json" +if bash "$FIXER" "$TEMP_DIR/repo" "$TEMP_DIR/finding.json"; then + echo 'FAIL: accepted path traversal' >&2; exit 1 +fi +printf '%s\n' '{"file":".github/workflows/link.yml"}' > "$TEMP_DIR/finding.json" +ln -s check.yml "$TEMP_DIR/repo/.github/workflows/link.yml" +if bash "$FIXER" "$TEMP_DIR/repo" "$TEMP_DIR/finding.json"; then + echo 'FAIL: accepted a symlink' >&2; exit 1 +fi +echo 'PASS: canonical prerequisite, repair, idempotence, traversal and symlink rejection' From a6b754ccd712b070051a330c53a6dfd5348b02ce Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Wed, 9 Sep 2026 23:38:42 +0100 Subject: [PATCH 13/16] fix(ci): refresh validators and remove credential-shaped mock token --- .github/workflows/actions.lock | 14 +++++++------- .github/workflows/boj-build.yml | 1 + .github/workflows/casket-pages.yml | 1 + .github/workflows/codeql.yml | 1 + .github/workflows/dogfood-gate.yml | 5 +++-- .github/workflows/e2e.yml | 1 + .github/workflows/governance.yml | 1 + .github/workflows/hypatia-dispatch-intake.yml | 1 + .github/workflows/hypatia-scan.yml | 1 + .github/workflows/inbox-steward.yml | 1 + .github/workflows/instant-sync.yml | 1 + .github/workflows/label-triage.yml | 1 + .github/workflows/labels.yml | 1 + .github/workflows/learning-loop.yml | 1 + .github/workflows/mirror.yml | 1 + .github/workflows/pages.yml | 1 + .github/workflows/panicbot-sweep.yml | 1 + .github/workflows/push-email-notify.yml | 1 + .github/workflows/repo-integrity-guard.yml | 1 + .github/workflows/rust.yml | 1 + .github/workflows/scorecard.yml | 1 + .github/workflows/secret-scanner.yml | 1 + .github/workflows/supervised-fleet-scan.yml | 1 + bots/seambot/tests/github_integration.rs | 4 ++-- 24 files changed, 33 insertions(+), 11 deletions(-) diff --git a/.github/workflows/actions.lock b/.github/workflows/actions.lock index 8a665353..4da82ed6 100644 --- a/.github/workflows/actions.lock +++ b/.github/workflows/actions.lock @@ -17,8 +17,8 @@ workflows: - 'github/codeql-action@v4.37.8' '.github/workflows/dogfood-gate.yml': - 'actions/checkout@v7.0.1' - - 'hyperpolymath/a2ml-ecosystem@main' - - 'hyperpolymath/k9-ecosystem@main' + - 'hyperpolymath/deed-ecosystem@main' + - 'hyperpolymath/k9-ecosystem@codex/science-ci-20260909' '.github/workflows/e2e.yml': - 'actions/checkout@v7.0.1' '.github/workflows/governance.yml': [] @@ -107,14 +107,14 @@ dependencies: commit: 'sha1-6037f33647c3f17758a2356c80fc4a53d7e0685d' owner_id: 75048950 repo_id: 623796603 - 'hyperpolymath/a2ml-ecosystem@main': + 'hyperpolymath/deed-ecosystem@main': ref: 'main' - commit: 'sha1-aa4b836bd969df2bc58128cb8e3d20bbc88d5e79' + commit: 'sha1-f7a40a4d5cc82b2e73f861119baa6818d77a448d' owner_id: 6759885 repo_id: 1275649586 - 'hyperpolymath/k9-ecosystem@main': - ref: 'main' - commit: 'sha1-89f3c2702f4f650a92aa7411502f38da06abd562' + 'hyperpolymath/k9-ecosystem@codex/science-ci-20260909': + ref: 'codex/science-ci-20260909' + commit: 'sha1-2ee51eed590b4722efcdca2fe4685ef24600bced' owner_id: 6759885 repo_id: 1275650185 'hyperpolymath/smtp-notify-action@v0.2.0': diff --git a/.github/workflows/boj-build.yml b/.github/workflows/boj-build.yml index 42ba1cc8..08c1a970 100644 --- a/.github/workflows/boj-build.yml +++ b/.github/workflows/boj-build.yml @@ -1,3 +1,4 @@ +# This workflow is managed by gh actions-lock. # SPDX-License-Identifier: MPL-2.0 # This workflow is managed by gh actions-lock. # This workflow is managed by gh actions-lock. diff --git a/.github/workflows/casket-pages.yml b/.github/workflows/casket-pages.yml index 9f5efb27..5becccb5 100644 --- a/.github/workflows/casket-pages.yml +++ b/.github/workflows/casket-pages.yml @@ -1,3 +1,4 @@ +# This workflow is managed by gh actions-lock. # SPDX-License-Identifier: MPL-2.0 # This workflow is managed by gh actions-lock. # This workflow is managed by gh actions-lock. diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index c9703681..e423ffdc 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,3 +1,4 @@ +# This workflow is managed by gh actions-lock. # SPDX-License-Identifier: MPL-2.0 # This workflow is managed by gh actions-lock. # This workflow is managed by gh actions-lock. diff --git a/.github/workflows/dogfood-gate.yml b/.github/workflows/dogfood-gate.yml index f69883d1..4aa91113 100644 --- a/.github/workflows/dogfood-gate.yml +++ b/.github/workflows/dogfood-gate.yml @@ -1,3 +1,4 @@ +# This workflow is managed by gh actions-lock. # SPDX-License-Identifier: MPL-2.0 # This workflow is managed by gh actions-lock. # This workflow is managed by gh actions-lock. @@ -46,7 +47,7 @@ jobs: - name: Validate A2ML manifests if: steps.detect.outputs.count > 0 - uses: hyperpolymath/a2ml-ecosystem/validate-action@main + uses: hyperpolymath/deed-ecosystem/validate-action@main with: path: '.' strict: 'false' @@ -108,7 +109,7 @@ jobs: - name: Validate K9 contracts if: steps.detect.outputs.k9_count > 0 - uses: hyperpolymath/k9-ecosystem/validate-action@main + uses: hyperpolymath/k9-ecosystem/validate-action@codex/science-ci-20260909 with: path: '.' strict: 'false' diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 3ad7cf65..ea6cd245 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -1,3 +1,4 @@ +# This workflow is managed by gh actions-lock. # SPDX-License-Identifier: MPL-2.0 # This workflow is managed by gh actions-lock. # This workflow is managed by gh actions-lock. diff --git a/.github/workflows/governance.yml b/.github/workflows/governance.yml index cc2965a5..ba07baed 100644 --- a/.github/workflows/governance.yml +++ b/.github/workflows/governance.yml @@ -1,3 +1,4 @@ +# This workflow is managed by gh actions-lock. # SPDX-License-Identifier: MPL-2.0 # This workflow is managed by gh actions-lock. # This workflow is managed by gh actions-lock. diff --git a/.github/workflows/hypatia-dispatch-intake.yml b/.github/workflows/hypatia-dispatch-intake.yml index c8810504..4a2d6c0d 100644 --- a/.github/workflows/hypatia-dispatch-intake.yml +++ b/.github/workflows/hypatia-dispatch-intake.yml @@ -1,3 +1,4 @@ +# This workflow is managed by gh actions-lock. # SPDX-License-Identifier: MPL-2.0 # This workflow is managed by gh actions-lock. # This workflow is managed by gh actions-lock. diff --git a/.github/workflows/hypatia-scan.yml b/.github/workflows/hypatia-scan.yml index 6798fb6d..18e0fde3 100644 --- a/.github/workflows/hypatia-scan.yml +++ b/.github/workflows/hypatia-scan.yml @@ -1,3 +1,4 @@ +# This workflow is managed by gh actions-lock. # SPDX-License-Identifier: MPL-2.0 # This workflow is managed by gh actions-lock. # This workflow is managed by gh actions-lock. diff --git a/.github/workflows/inbox-steward.yml b/.github/workflows/inbox-steward.yml index 7169dff8..7900afba 100644 --- a/.github/workflows/inbox-steward.yml +++ b/.github/workflows/inbox-steward.yml @@ -1,3 +1,4 @@ +# This workflow is managed by gh actions-lock. # SPDX-License-Identifier: MPL-2.0 # This workflow is managed by gh actions-lock. # This workflow is managed by gh actions-lock. diff --git a/.github/workflows/instant-sync.yml b/.github/workflows/instant-sync.yml index 7b972aa0..7fb72702 100644 --- a/.github/workflows/instant-sync.yml +++ b/.github/workflows/instant-sync.yml @@ -1,3 +1,4 @@ +# This workflow is managed by gh actions-lock. # SPDX-License-Identifier: MPL-2.0 # This workflow is managed by gh actions-lock. # This workflow is managed by gh actions-lock. diff --git a/.github/workflows/label-triage.yml b/.github/workflows/label-triage.yml index 9886e920..814a1924 100644 --- a/.github/workflows/label-triage.yml +++ b/.github/workflows/label-triage.yml @@ -1,3 +1,4 @@ +# This workflow is managed by gh actions-lock. # SPDX-License-Identifier: MPL-2.0 name: Label Triage diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml index c80b676c..83ab941a 100644 --- a/.github/workflows/labels.yml +++ b/.github/workflows/labels.yml @@ -1,3 +1,4 @@ +# This workflow is managed by gh actions-lock. # SPDX-License-Identifier: MPL-2.0 name: Labels diff --git a/.github/workflows/learning-loop.yml b/.github/workflows/learning-loop.yml index 49f3cb1c..c99eaabb 100644 --- a/.github/workflows/learning-loop.yml +++ b/.github/workflows/learning-loop.yml @@ -1,3 +1,4 @@ +# This workflow is managed by gh actions-lock. # SPDX-License-Identifier: MPL-2.0 # This workflow is managed by gh actions-lock. # This workflow is managed by gh actions-lock. diff --git a/.github/workflows/mirror.yml b/.github/workflows/mirror.yml index a0c89c08..7ff3cb21 100644 --- a/.github/workflows/mirror.yml +++ b/.github/workflows/mirror.yml @@ -1,3 +1,4 @@ +# This workflow is managed by gh actions-lock. # SPDX-License-Identifier: MPL-2.0 # This workflow is managed by gh actions-lock. # This workflow is managed by gh actions-lock. diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 31ade91c..20e2ac24 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -1,3 +1,4 @@ +# This workflow is managed by gh actions-lock. # SPDX-License-Identifier: MPL-2.0 # This workflow is managed by gh actions-lock. # This workflow is managed by gh actions-lock. diff --git a/.github/workflows/panicbot-sweep.yml b/.github/workflows/panicbot-sweep.yml index 4a937972..3452bfeb 100644 --- a/.github/workflows/panicbot-sweep.yml +++ b/.github/workflows/panicbot-sweep.yml @@ -1,3 +1,4 @@ +# This workflow is managed by gh actions-lock. # SPDX-License-Identifier: MPL-2.0 # This workflow is managed by gh actions-lock. # This workflow is managed by gh actions-lock. diff --git a/.github/workflows/push-email-notify.yml b/.github/workflows/push-email-notify.yml index 9e133d7c..52757af7 100644 --- a/.github/workflows/push-email-notify.yml +++ b/.github/workflows/push-email-notify.yml @@ -1,3 +1,4 @@ +# This workflow is managed by gh actions-lock. # SPDX-License-Identifier: MPL-2.0 # This workflow is managed by gh actions-lock. # Dormant push-email notification. ARMED by setting the repo variable diff --git a/.github/workflows/repo-integrity-guard.yml b/.github/workflows/repo-integrity-guard.yml index cb0048b4..b0fc5733 100644 --- a/.github/workflows/repo-integrity-guard.yml +++ b/.github/workflows/repo-integrity-guard.yml @@ -1,3 +1,4 @@ +# This workflow is managed by gh actions-lock. # SPDX-License-Identifier: MPL-2.0 # This workflow is managed by gh actions-lock. # This workflow is managed by gh actions-lock. diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index f2de8025..9472e948 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -1,3 +1,4 @@ +# This workflow is managed by gh actions-lock. # SPDX-License-Identifier: MPL-2.0 # This workflow is managed by gh actions-lock. # This workflow is managed by gh actions-lock. diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 833b2c87..cb776943 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -1,3 +1,4 @@ +# This workflow is managed by gh actions-lock. # SPDX-License-Identifier: MPL-2.0 # This workflow is managed by gh actions-lock. # This workflow is managed by gh actions-lock. diff --git a/.github/workflows/secret-scanner.yml b/.github/workflows/secret-scanner.yml index 7742ec99..855f0fc5 100644 --- a/.github/workflows/secret-scanner.yml +++ b/.github/workflows/secret-scanner.yml @@ -1,3 +1,4 @@ +# This workflow is managed by gh actions-lock. # SPDX-License-Identifier: MPL-2.0 # This workflow is managed by gh actions-lock. # This workflow is managed by gh actions-lock. diff --git a/.github/workflows/supervised-fleet-scan.yml b/.github/workflows/supervised-fleet-scan.yml index 65044b50..cb06008e 100644 --- a/.github/workflows/supervised-fleet-scan.yml +++ b/.github/workflows/supervised-fleet-scan.yml @@ -1,3 +1,4 @@ +# This workflow is managed by gh actions-lock. # SPDX-License-Identifier: MPL-2.0 # This workflow is managed by gh actions-lock. # This workflow is managed by gh actions-lock. diff --git a/bots/seambot/tests/github_integration.rs b/bots/seambot/tests/github_integration.rs index 3f5173b2..76f9b65b 100644 --- a/bots/seambot/tests/github_integration.rs +++ b/bots/seambot/tests/github_integration.rs @@ -151,12 +151,12 @@ mod tests { fn test_installation_token_response_parsing() { // Test installation token response can be parsed let response = r#"{ - "token": "ghs_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "token": "test-token", "expires_at": "2024-01-15T12:00:00Z" }"#; let parsed: serde_json::Value = serde_json::from_str(response).unwrap(); - assert!(parsed["token"].as_str().unwrap().starts_with("ghs_")); + assert_eq!(parsed["token"].as_str().unwrap(), "test-token"); assert!(parsed["expires_at"].as_str().unwrap().contains("T")); } From 837c82b3ea2ea22fc2361ff03b5ae95f8805761f Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Thu, 10 Sep 2026 01:13:17 +0100 Subject: [PATCH 14/16] fix(ci): restore DEED action identity and repaired K9 lock --- .github/workflows/actions.lock | 8 ++++---- .github/workflows/dogfood-gate.yml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/actions.lock b/.github/workflows/actions.lock index 4da82ed6..952a72b5 100644 --- a/.github/workflows/actions.lock +++ b/.github/workflows/actions.lock @@ -18,7 +18,7 @@ workflows: '.github/workflows/dogfood-gate.yml': - 'actions/checkout@v7.0.1' - 'hyperpolymath/deed-ecosystem@main' - - 'hyperpolymath/k9-ecosystem@codex/science-ci-20260909' + - 'hyperpolymath/k9-ecosystem@main' '.github/workflows/e2e.yml': - 'actions/checkout@v7.0.1' '.github/workflows/governance.yml': [] @@ -112,9 +112,9 @@ dependencies: commit: 'sha1-f7a40a4d5cc82b2e73f861119baa6818d77a448d' owner_id: 6759885 repo_id: 1275649586 - 'hyperpolymath/k9-ecosystem@codex/science-ci-20260909': - ref: 'codex/science-ci-20260909' - commit: 'sha1-2ee51eed590b4722efcdca2fe4685ef24600bced' + 'hyperpolymath/k9-ecosystem@main': + ref: 'main' + commit: 'sha1-2155aa26a21758f2ba119f61bc7e0e1981c106fb' owner_id: 6759885 repo_id: 1275650185 'hyperpolymath/smtp-notify-action@v0.2.0': diff --git a/.github/workflows/dogfood-gate.yml b/.github/workflows/dogfood-gate.yml index 4aa91113..921b0ab1 100644 --- a/.github/workflows/dogfood-gate.yml +++ b/.github/workflows/dogfood-gate.yml @@ -109,7 +109,7 @@ jobs: - name: Validate K9 contracts if: steps.detect.outputs.k9_count > 0 - uses: hyperpolymath/k9-ecosystem/validate-action@codex/science-ci-20260909 + uses: hyperpolymath/k9-ecosystem/validate-action@main with: path: '.' strict: 'false' From c90b5b1b3a2f3ec52ad407ca61f6ff575a29443a Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Thu, 10 Sep 2026 01:40:03 +0100 Subject: [PATCH 15/16] fix(ci): consume the merged K9 validator --- .github/workflows/actions.lock | 8 ++++---- .github/workflows/dogfood-gate.yml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/actions.lock b/.github/workflows/actions.lock index 4da82ed6..952a72b5 100644 --- a/.github/workflows/actions.lock +++ b/.github/workflows/actions.lock @@ -18,7 +18,7 @@ workflows: '.github/workflows/dogfood-gate.yml': - 'actions/checkout@v7.0.1' - 'hyperpolymath/deed-ecosystem@main' - - 'hyperpolymath/k9-ecosystem@codex/science-ci-20260909' + - 'hyperpolymath/k9-ecosystem@main' '.github/workflows/e2e.yml': - 'actions/checkout@v7.0.1' '.github/workflows/governance.yml': [] @@ -112,9 +112,9 @@ dependencies: commit: 'sha1-f7a40a4d5cc82b2e73f861119baa6818d77a448d' owner_id: 6759885 repo_id: 1275649586 - 'hyperpolymath/k9-ecosystem@codex/science-ci-20260909': - ref: 'codex/science-ci-20260909' - commit: 'sha1-2ee51eed590b4722efcdca2fe4685ef24600bced' + 'hyperpolymath/k9-ecosystem@main': + ref: 'main' + commit: 'sha1-2155aa26a21758f2ba119f61bc7e0e1981c106fb' owner_id: 6759885 repo_id: 1275650185 'hyperpolymath/smtp-notify-action@v0.2.0': diff --git a/.github/workflows/dogfood-gate.yml b/.github/workflows/dogfood-gate.yml index 4aa91113..921b0ab1 100644 --- a/.github/workflows/dogfood-gate.yml +++ b/.github/workflows/dogfood-gate.yml @@ -109,7 +109,7 @@ jobs: - name: Validate K9 contracts if: steps.detect.outputs.k9_count > 0 - uses: hyperpolymath/k9-ecosystem/validate-action@codex/science-ci-20260909 + uses: hyperpolymath/k9-ecosystem/validate-action@main with: path: '.' strict: 'false' From 0fb1987a5d8fa70dab430e8e737c93a75b995666 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Thu, 10 Sep 2026 02:52:07 +0100 Subject: [PATCH 16/16] fix(ci): consume shared Scorecard publication and scanner contracts --- .github/workflows/governance.yml | 2 +- .github/workflows/hypatia-scan.yml | 2 +- .github/workflows/scorecard.yml | 69 +----------------------------- 3 files changed, 4 insertions(+), 69 deletions(-) diff --git a/.github/workflows/governance.yml b/.github/workflows/governance.yml index 3719b083..2be32a38 100644 --- a/.github/workflows/governance.yml +++ b/.github/workflows/governance.yml @@ -35,4 +35,4 @@ permissions: jobs: governance: - uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@092dedada188f56c5915f74a5fd40aac093742c3 # main 2026-09-04 + uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@da2c748aad55c1a1dcba00b60fe4a35017bc6540 diff --git a/.github/workflows/hypatia-scan.yml b/.github/workflows/hypatia-scan.yml index 18e0fde3..83451cea 100644 --- a/.github/workflows/hypatia-scan.yml +++ b/.github/workflows/hypatia-scan.yml @@ -31,7 +31,7 @@ permissions: jobs: hypatia: - uses: hyperpolymath/standards/.github/workflows/hypatia-scan-reusable.yml@571cc734cd69fb846032ec77a662aa8ee4fc32cd + uses: hyperpolymath/standards/.github/workflows/hypatia-scan-reusable.yml@da2c748aad55c1a1dcba00b60fe4a35017bc6540 secrets: inherit # Total caller-side wall-clock cap for the reusable. Matches # Hypatia's `missing_timeout_minutes` rule expectation. The scan is diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 79f38695..5ee76250 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -1,8 +1,6 @@ # This workflow is managed by gh actions-lock. # SPDX-License-Identifier: MPL-2.0 -# This workflow is managed by gh actions-lock. name: Scorecards supply-chain security - on: push: branches: [main] @@ -11,75 +9,12 @@ on: branch_protection_rule: schedule: - cron: '23 4 * * 1' - permissions: contents: read - jobs: - analysis: - if: github.event_name != 'pull_request' - name: Publish default-branch Scorecard - runs-on: ubuntu-24.04 - timeout-minutes: 15 + scorecard: permissions: - actions: read contents: read security-events: write id-token: write - # These are the publishing steps from standards at 571cc734. Keep them - # local so every action is covered by this workflow's runtime lockfile. - steps: - - uses: actions/checkout@v7.0.1 - with: - persist-credentials: false - - name: Analyze and publish default-branch Scorecard - uses: ossf/scorecard-action@v2.4.4 - with: - results_file: results.sarif - results_format: sarif - publish_results: true - - name: Upload Scorecard SARIF - uses: github/codeql-action/upload-sarif@v4.37.8 - with: - sarif_file: results.sarif - - name: Retain scan evidence - uses: actions/upload-artifact@v7.0.1 - with: - name: scorecard-results - path: results.sarif - if-no-files-found: error - retention-days: 90 - - pull-request-analysis: - name: Scorecard PR analysis - if: github.event_name == 'pull_request' - runs-on: ubuntu-24.04 - timeout-minutes: 15 - permissions: - actions: read - contents: read - security-events: write - steps: - - uses: actions/checkout@v7.0.1 - with: - persist-credentials: false - # The publisher targets the default branch. PRs need their own - # real SARIF analysis without OIDC or publishing to the public badge API. - # OpenSSF currently labels its pull_request trigger experimental. - - name: Analyze the pull request with OpenSSF Scorecard - uses: ossf/scorecard-action@v2.4.4 - with: - results_file: results.sarif - results_format: sarif - publish_results: false - - name: Upload Scorecard SARIF - uses: github/codeql-action/upload-sarif@v4.37.8 - with: - sarif_file: results.sarif - - name: Retain scan evidence - uses: actions/upload-artifact@v7.0.1 - with: - name: scorecard-pr-results - path: results.sarif - if-no-files-found: error - retention-days: 14 + uses: hyperpolymath/standards/.github/workflows/scorecard-reusable.yml@da2c748aad55c1a1dcba00b60fe4a35017bc6540