diff --git a/.github/CICD-CHANGES-2026-06-04.md b/.github/CICD-CHANGES-2026-06-04.md
index 672a8a74..8a21131e 100644
--- a/.github/CICD-CHANGES-2026-06-04.md
+++ b/.github/CICD-CHANGES-2026-06-04.md
@@ -1,3 +1,8 @@
+
+
# CI/CD Changes — 2026-06-04
**Date:** 2026-06-04
@@ -22,7 +27,7 @@ All 18 workflows in this repository have been updated to include `timeout-minute
| Workflow | timeout-minutes | Concurrency Added | Notes |
|----------|-----------------|------------------|-------|
| `abi-drift.yml` | 15 | | ABI manifest + FFI verification |
-| `codeql.yml` | 15 | ✓ | Includes C++ support (has C/C++ headers) |
+| `codeql.yml` | 15 | ✓ | JavaScript/TypeScript CodeQL only; Zig FFI is covered by Zig workflows |
| `container-publish.yml` | 30 | | Container build & push |
| `dogfood-gate.yml` | 5-15 | ✓ | 6 jobs: a2ml(5), k9(5), empty-lint(15), groove(5), eclexiaiser(5), summary(5) |
| `e2e.yml` | 15 | ✓ | MCP bridge input fuzz tests |
@@ -54,8 +59,13 @@ All 18 workflows in this repository have been updated to include `timeout-minute
## CodeQL Configuration
-**Languages:** `javascript-typescript` + `cpp`
-**Reason:** This repository contains C/C++ headers in the FFI layer.
+**Languages:** `javascript-typescript`
+
+**Reason:** The FFI implementation is Zig. The tracked C ABI file is a generated
+header-only surface (`generated/abi/boj_catalogue.h`), not a C/C++ translation
+unit; enabling CodeQL `cpp` for headers alone makes extraction fail before
+analysis. Re-add `cpp` only when tracked `.c`, `.cc`, `.cpp`, or `.cxx` sources
+exist.
---
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index 4cb86bb3..ee97c746 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -33,8 +33,10 @@ jobs:
include:
- language: javascript-typescript
build-mode: none
- - language: cpp
- build-mode: none
+ # C/C++ CodeQL is intentionally not enabled for the generated
+ # header-only ABI surface. The FFI implementation is Zig and is
+ # covered by the Zig workflows; re-add cpp only when tracked
+ # .c/.cc/.cpp/.cxx translation units exist.
steps:
- name: Checkout
diff --git a/.github/workflows/dogfood-gate.yml b/.github/workflows/dogfood-gate.yml
index 76d824db..d18476e8 100644
--- a/.github/workflows/dogfood-gate.yml
+++ b/.github/workflows/dogfood-gate.yml
@@ -165,6 +165,9 @@ jobs:
echo "::warning file=${REL_PATH}::Invisible Unicode characters detected (zero-width space, BOM, NBSP, etc.)"
done < /tmp/empty-lint-results.txt
+ - name: Check shebang placement
+ run: bash scripts/check-shebang-first.sh
+
- name: Write summary
run: |
if [ "${{ steps.lint.outputs.ready }}" = "true" ]; then
@@ -374,4 +377,3 @@ jobs:
*Generated by the [Dogfood Gate](https://github.com/hyperpolymath/rsr-template-repo) workflow.*
*Dogfooding is guinea pig fooding — we test our tools on ourselves.*
EOF
-
diff --git a/.github/workflows/governance.yml b/.github/workflows/governance.yml
index 1b4e269a..8161ec24 100644
--- a/.github/workflows/governance.yml
+++ b/.github/workflows/governance.yml
@@ -1,35 +1,16 @@
-# SPDX-License-Identifier: MPL-2.0
-# governance.yml — single wrapper calling the shared estate governance bundle
-# in hyperpolymath/standards instead of carrying per-repo copies.
-#
-# Replaces the per-repo governance scaffolding removed in the same commit:
-# quality.yml, guix-nix-policy.yml, npm-bun-blocker.yml, ts-blocker.yml,
-# security-policy.yml, rsr-antipattern.yml, wellknown-enforcement.yml,
-# workflow-linter.yml
-#
-# Load-bearing build/security workflows stay standalone in the repo
-# (rust-ci, codeql, dependabot, release, scan/mirror/pages plumbing).
-
+# SPDX-License-Identifier: PMPL-1.0-or-later
name: Governance
on:
push:
branches: [main, master]
pull_request:
+ branches: [main, master]
workflow_dispatch:
-# Estate guardrail: cancel superseded runs so re-pushes / rebased PR
-# updates do not pile up queued runs against the shared account-wide
-# Actions concurrency pool. Applied only to read-only check workflows
-# (no publish/mutation), so cancelling a superseded run is always safe.
-concurrency:
- group: ${{ github.workflow }}-${{ github.ref }}
- cancel-in-progress: true
-
permissions:
contents: read
jobs:
governance:
- uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@861b5e911d9e5dcfb3c0ab3dd2a9a3c8fd0a1613
- timeout-minutes: 10
+ uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@5a93d9d57cc04de4002d6d0ecd336fc7a8698910
diff --git a/.github/workflows/hypatia-scan.yml b/.github/workflows/hypatia-scan.yml
index 127905d2..e7158485 100644
--- a/.github/workflows/hypatia-scan.yml
+++ b/.github/workflows/hypatia-scan.yml
@@ -1,415 +1,19 @@
-# SPDX-License-Identifier: MPL-2.0
-# Hypatia Neurosymbolic CI/CD Security Scan
+# SPDX-License-Identifier: PMPL-1.0-or-later
name: Hypatia Security Scan
on:
push:
- branches: [ main, master, develop ]
+ branches: [main, master, develop]
pull_request:
- branches: [ main, master ]
+ branches: [main, master]
schedule:
- - cron: '0 0 * * 0' # Weekly on Sunday
+ - cron: '0 0 * * 0'
workflow_dispatch:
-# Estate guardrail: cancel superseded runs so re-pushes don't pile up
-# queued runs across the estate. Safe here because this workflow only
-# performs read-only checks/lint/test/scan with no publish or mutation.
-concurrency:
- group: ${{ github.workflow }}-${{ github.ref }}
- cancel-in-progress: true
permissions:
contents: read
- # security-events: write serves two purposes (write implies read):
- # 1. read — lets the built-in GITHUB_TOKEN query this repo's own
- # Dependabot alerts via the Hypatia DependabotAlerts rule
- # (DA001-DA004). Without read, `scan_from_path` gets HTTP 403
- # and the rule silently returns no findings.
- # See 007-lang/audits/audit-dependabot-automation-gap-2026-04-17.md.
- # 2. write — lets the "Upload SARIF to code scanning" step publish
- # Hypatia findings to the Security → Code scanning page so they
- # are triaged/deduplicated like CodeQL alerts instead of living
- # only in a build artifact nobody is required to look at.
- # See hyperpolymath/burble#35 (SARIF integration).
- # This is a single-job workflow, so job-level scoping would not
- # narrow the grant further; it stays workflow-level and documented.
- security-events: write
- # pull-requests: write lets the advisory "Comment on PR with findings"
- # step post its summary. Without it the built-in GITHUB_TOKEN gets
- # "Resource not accessible by integration" and (absent continue-on-error)
- # hard-fails the scan — exactly what the gate-decoupling design forbids.
- pull-requests: write
+ security-events: read
jobs:
scan:
- name: Hypatia Neurosymbolic Analysis
- runs-on: ubuntu-latest
- timeout-minutes: 15
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- with:
- fetch-depth: 0 # Full history for better pattern analysis
-
- - name: Setup Elixir for Hypatia scanner
- uses: erlef/setup-beam@fc68ffb90438ef2936bbb3251622353b3dcb2f93 # v1.18.2
- with:
- elixir-version: '1.18'
- otp-version: '27'
-
- - name: Clone Hypatia
- run: |
- if [ ! -d "$HOME/hypatia" ]; then
- git clone https://github.com/hyperpolymath/hypatia.git "$HOME/hypatia"
- fi
-
- - name: Build Hypatia scanner (if needed)
- run: |
- cd "$HOME/hypatia"
- if [ ! -f hypatia ]; then
- echo "Building hypatia scanner..."
- mix deps.get
- mix escript.build
- fi
-
- - name: Run Hypatia scan
- id: scan
- env:
- # Pass the built-in Actions token through to Hypatia so the
- # DependabotAlerts rule can query this repo's own alerts.
- # For cross-repo scanning (fleet-coordinator scan-supervised),
- # a PAT with `security_events` scope is required instead.
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: |
- echo "Scanning repository: ${{ github.repository }}"
-
- # Run scanner (exits non-zero when findings exist — suppress to continue)
- HYPATIA_FORMAT=json "$HOME/hypatia/hypatia-cli.sh" scan . --exit-zero > hypatia-findings.json || true
-
- # Count findings
- FINDING_COUNT=$(jq '. | length' hypatia-findings.json 2>/dev/null || echo 0)
- echo "findings_count=$FINDING_COUNT" >> $GITHUB_OUTPUT
-
- # Extract severity counts
- CRITICAL=$(jq '[.[] | select(.severity == "critical")] | length' hypatia-findings.json)
- HIGH=$(jq '[.[] | select(.severity == "high")] | length' hypatia-findings.json)
- MEDIUM=$(jq '[.[] | select(.severity == "medium")] | length' hypatia-findings.json)
-
- echo "critical=$CRITICAL" >> $GITHUB_OUTPUT
- echo "high=$HIGH" >> $GITHUB_OUTPUT
- echo "medium=$MEDIUM" >> $GITHUB_OUTPUT
-
- echo "## Hypatia Scan Results" >> $GITHUB_STEP_SUMMARY
- echo "- Total findings: $FINDING_COUNT" >> $GITHUB_STEP_SUMMARY
- echo "- Critical: $CRITICAL" >> $GITHUB_STEP_SUMMARY
- echo "- High: $HIGH" >> $GITHUB_STEP_SUMMARY
- echo "- Medium: $MEDIUM" >> $GITHUB_STEP_SUMMARY
-
- - name: Upload findings artifact
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
- with:
- name: hypatia-findings
- path: hypatia-findings.json
- retention-days: 90
-
- - name: Convert Hypatia findings to SARIF
- # Always runs (no findings_count guard): an EMPTY SARIF run is
- # valid and intentional — uploading it clears stale Hypatia
- # alerts from the code-scanning page when a repo goes clean.
- # The converter is dependency-free Node (Node ships on
- # ubuntu-latest; no npm install — estate npm ban respected) and
- # is hardened against the heterogeneous Hypatia JSON schema:
- # most findings are {rule_module,severity,type,file,reason,
- # action}; only some carry an integer `line`; `file` may be
- # empty or absolute. See lib/hypatia/cli.ex (collect_findings).
- run: |
- cat > "$RUNNER_TEMP/hypatia-sarif.cjs" <<'CJS'
- const fs = require('fs');
- const path = require('path');
- const crypto = require('crypto');
-
- const ws = process.env.GITHUB_WORKSPACE || process.cwd();
-
- let findings = [];
- try {
- const parsed = JSON.parse(fs.readFileSync('hypatia-findings.json', 'utf8'));
- if (Array.isArray(parsed)) findings = parsed;
- } catch (_) {
- // Scanner unavailable / empty / malformed -> empty SARIF.
- // Intentionally clears stale alerts rather than erroring.
- findings = [];
- }
-
- // Mirrors Hypatia's own "github" annotation mapping
- // (lib/hypatia/cli.ex output/2): critical|high -> error,
- // medium -> warning, everything else -> note.
- const levelFor = (sev) => {
- switch (String(sev || '').toLowerCase()) {
- case 'critical':
- case 'high': return 'error';
- case 'medium': return 'warning';
- default: return 'note';
- }
- };
-
- // SARIF artifactLocation.uri must be a repo-relative POSIX
- // path. Hypatia may emit absolute paths (scanned under
- // $GITHUB_WORKSPACE) or "" / "." for repo-level findings.
- const relUri = (file) => {
- if (!file) return '.';
- let f = String(file);
- if (path.isAbsolute(f)) {
- const rel = path.relative(ws, f);
- f = (rel && !rel.startsWith('..')) ? rel : path.basename(f);
- }
- f = f.replace(/\\/g, '/').replace(/^\.\//, '');
- return f || '.';
- };
-
- const rules = new Map();
- const results = findings.map((f) => {
- const mod = String(f.rule_module || 'hypatia');
- const type = String(f.type || 'finding');
- const ruleId = `hypatia/${mod}/${type}`;
- const level = levelFor(f.severity);
- if (!rules.has(ruleId)) {
- rules.set(ruleId, {
- id: ruleId,
- name: `${mod}.${type}`,
- shortDescription: { text: `Hypatia ${mod}: ${type}` },
- defaultConfiguration: { level }
- });
- }
- const uri = relUri(f.file);
- const msg = String(f.reason || f.type || 'Hypatia finding');
- const startLine =
- Number.isInteger(f.line) && f.line > 0 ? f.line : 1;
- // Stable cross-run fingerprint for dedupe (no line, so a
- // moved finding in the same file/rule stays one alert).
- const fp = crypto
- .createHash('sha256')
- .update([ruleId, uri, type, msg].join('|'))
- .digest('hex');
- return {
- ruleId,
- level,
- message: { text: msg },
- locations: [
- {
- physicalLocation: {
- artifactLocation: { uri },
- region: { startLine }
- }
- }
- ],
- partialFingerprints: { 'hypatiaFindingHash/v1': fp }
- };
- });
-
- const sarif = {
- $schema: 'https://json.schemastore.org/sarif-2.1.0.json',
- version: '2.1.0',
- runs: [
- {
- tool: {
- driver: {
- name: 'Hypatia',
- informationUri: 'https://github.com/hyperpolymath/hypatia',
- rules: Array.from(rules.values())
- }
- },
- results
- }
- ]
- };
-
- fs.writeFileSync('hypatia.sarif', JSON.stringify(sarif, null, 2));
- console.log(`hypatia.sarif written: ${results.length} result(s).`);
- CJS
- node "$RUNNER_TEMP/hypatia-sarif.cjs"
-
- - name: Upload SARIF to GitHub code scanning
- # Fork PRs get a read-only GITHUB_TOKEN, so security-events:write
- # is unavailable and upload-sarif cannot publish — skip there
- # rather than hard-fail (the push/schedule run on the default
- # branch is the authoritative upload). Same-repo PRs and pushes
- # do upload. This step is deliberately NOT continue-on-error:
- # if the security-surface integration breaks we want a loud red,
- # not a silently-ungated scanner (the exact failure mode #35
- # exists to end). The empty-SARIF "clear stale alerts" path is
- # handled in the converter above and does not error here.
- if: >-
- always() &&
- (github.event_name != 'pull_request' ||
- github.event.pull_request.head.repo.fork != true)
- uses: github/codeql-action/upload-sarif@0d579ffd059c29b07949a3cce3983f0780820c98 # v3.28.1
- with:
- sarif_file: hypatia.sarif
- # Distinct category so Hypatia results coexist with CodeQL's
- # (codeql.yml) instead of overwriting them on the same surface.
- category: hypatia
-
- - name: Submit findings to gitbot-fleet (Phase 2)
- if: steps.scan.outputs.findings_count > 0
- # Phase 2 is the collaborative LEARNING side-channel ("bots share
- # findings via gitbot-fleet"), not the security gate. The gate is
- # the baseline-aware "Check for critical or high-severity issues"
- # step below. A fleet-side regression (e.g. the submit script being
- # moved/removed) must NEVER hard-fail every consuming repo's scan.
- # Same reasoning as the "Comment on PR with findings" step.
- # See hyperpolymath/hypatia#213 (gate decoupling) and the exit-127
- # estate-wide breakage when gitbot-fleet/scripts/submit-finding.sh
- # no longer existed on the default branch.
- continue-on-error: true
- env:
- # All GitHub context values surface as env vars so the run
- # block never interpolates `${{ … }}` inline (closes the
- # workflow_audit/unsafe_curl_payload + actions_expression_injection
- # findings).
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- FLEET_PUSH_TOKEN: ${{ secrets.HYPATIA_DISPATCH_PAT }}
- FLEET_DISPATCH_TOKEN: ${{ secrets.HYPATIA_DISPATCH_PAT }}
- GITHUB_REPOSITORY: ${{ github.repository }}
- GITHUB_SHA: ${{ github.sha }}
- FINDINGS_COUNT: ${{ steps.scan.outputs.findings_count }}
- run: |
- echo "📤 Submitting $FINDINGS_COUNT findings to gitbot-fleet..."
-
- # Clone gitbot-fleet to temp directory. A clone failure (network,
- # repo gone) is non-fatal: learning submission is best-effort.
- FLEET_DIR="/tmp/gitbot-fleet-$$"
- if ! git clone --depth 1 https://github.com/hyperpolymath/gitbot-fleet.git "$FLEET_DIR"; then
- echo "::warning::Could not clone gitbot-fleet — skipping Phase 2 learning submission (non-fatal)."
- exit 0
- fi
-
- # The submission script's location in gitbot-fleet has drifted
- # before (it was absent from the default branch, which exit-127'd
- # every consuming repo's scan). Probe known locations rather than
- # hard-coding one path, and skip gracefully if none is present.
- SUBMIT_SCRIPT=""
- for cand in \
- "$FLEET_DIR/scripts/submit-finding.sh" \
- "$FLEET_DIR/scripts/submit_finding.sh" \
- "$FLEET_DIR/bin/submit-finding.sh" \
- "$FLEET_DIR/submit-finding.sh"; do
- if [ -f "$cand" ]; then
- SUBMIT_SCRIPT="$cand"
- break
- fi
- done
-
- if [ -z "$SUBMIT_SCRIPT" ]; then
- echo "::warning::gitbot-fleet submit-finding script not found at any known path — skipping Phase 2 learning submission (non-fatal). Findings are still uploaded as an artifact and gated below."
- rm -rf "$FLEET_DIR"
- exit 0
- fi
-
- # Run submission script. Pass the findings path as ABSOLUTE —
- # the script cd's into its own working dir before reading the
- # file, so a relative path would resolve to the wrong place.
- # A submission-script failure is logged but non-fatal.
- if bash "$SUBMIT_SCRIPT" "$GITHUB_WORKSPACE/hypatia-findings.json"; then
- echo "✅ Finding submission complete"
- else
- echo "::warning::gitbot-fleet submission script exited non-zero — Phase 2 learning submission skipped (non-fatal)."
- fi
-
- # Cleanup
- rm -rf "$FLEET_DIR"
-
- - name: Check for critical issues
- if: steps.scan.outputs.critical > 0
- # GATING POLICY (explicit, by design — not an oversight):
- # Hypatia is ADVISORY here. Critical findings are surfaced
- # (step annotation + SARIF alert on the code-scanning page +
- # PR comment) but do NOT fail this check. Enforcement is
- # delegated to the code-scanning surface: tighten by adding a
- # branch-protection "required" status on the `hypatia` SARIF
- # category, not by reintroducing an `exit 1` here. This keeps
- # the gate decision in one auditable place (hypatia#213 gate
- # decoupling) and lets a repo opt into fail-on-critical without
- # editing this canonical workflow. To change the policy, change
- # branch protection — deliberately no commented-out `exit 1`.
- run: |
- echo "::warning::Hypatia found critical security issue(s) — advisory."
- echo "See the Security → Code scanning page (category: hypatia)"
- echo "and the hypatia-findings.json artifact for details."
-
- - name: Generate scan report
- run: |
- cat << EOF > hypatia-report.md
- # Hypatia Security Scan Report
-
- **Repository:** ${{ github.repository }}
- **Scan Date:** $(date -u +"%Y-%m-%d %H:%M:%S UTC")
- **Commit:** ${{ github.sha }}
-
- ## Summary
-
- | Severity | Count |
- |----------|-------|
- | Critical | ${{ steps.scan.outputs.critical }} |
- | High | ${{ steps.scan.outputs.high }} |
- | Medium | ${{ steps.scan.outputs.medium }} |
- | **Total**| ${{ steps.scan.outputs.findings_count }} |
-
- ## Next Steps
-
- 1. Triage findings on the **Security → Code scanning** page
- (SARIF category \`hypatia\`) — dismiss/track them there like
- CodeQL alerts.
- 2. The full finding set is also attached as the
- \`hypatia-findings.json\` build artifact for offline review.
- 3. Findings are **advisory** today (surfaced, not gated); the
- gating policy is documented in the workflow's "Check for
- critical issues" step.
-
- ## Learning
-
- These findings feed Hypatia's learning engine to improve future rules.
-
- ---
- *Powered by [Hypatia](https://github.com/hyperpolymath/hypatia) - Neurosymbolic CI/CD Intelligence*
- EOF
-
- cat hypatia-report.md >> $GITHUB_STEP_SUMMARY
-
- - name: Comment on PR with findings
- if: github.event_name == 'pull_request' && steps.scan.outputs.findings_count > 0
- # Advisory only — posting findings as a PR comment must never gate
- # the scan (hypatia#213 gate decoupling). Belt-and-braces alongside
- # the pull-requests: write permission above: a token/API hiccup or
- # a fork PR (read-only token) skips the comment, not the check.
- continue-on-error: true
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v7
- with:
- script: |
- const fs = require('fs');
- const findings = JSON.parse(fs.readFileSync('hypatia-findings.json', 'utf8'));
-
- const critical = findings.filter(f => f.severity === 'critical').length;
- const high = findings.filter(f => f.severity === 'high').length;
-
- let comment = `## 🔍 Hypatia Security Scan\n\n`;
- comment += `**Findings:** ${findings.length} issues detected\n\n`;
- comment += `| Severity | Count |\n|----------|-------|\n`;
- comment += `| 🔴 Critical | ${critical} |\n`;
- comment += `| 🟠 High | ${high} |\n`;
- comment += `| 🟡 Medium | ${findings.length - critical - high} |\n\n`;
-
- if (critical > 0) {
- comment += `⚠️ **Action Required:** Critical security issues found!\n\n`;
- }
-
- comment += `View findings
\n\n`;
- comment += `\`\`\`json\n${JSON.stringify(findings.slice(0, 10), null, 2)}\n\`\`\`\n`;
- comment += ` \n\n`;
- comment += `*Powered by Hypatia Neurosymbolic CI/CD Intelligence*`;
-
- github.rest.issues.createComment({
- owner: context.repo.owner,
- repo: context.repo.repo,
- issue_number: context.issue.number,
- body: comment
- });
\ No newline at end of file
+ uses: hyperpolymath/standards/.github/workflows/hypatia-scan-reusable.yml@5a93d9d57cc04de4002d6d0ecd336fc7a8698910
diff --git a/.github/workflows/scorecard-enforcer.yml b/.github/workflows/scorecard-enforcer.yml
deleted file mode 100644
index 86d81735..00000000
--- a/.github/workflows/scorecard-enforcer.yml
+++ /dev/null
@@ -1,124 +0,0 @@
-# SPDX-License-Identifier: MPL-2.0
-# Copyright (c) 2026 Jonathan D.A. Jewell
-#
-# Prevention workflow - runs OpenSSF Scorecard and fails on low scores.
-#
-# Split into TWO jobs to satisfy the OSSF Scorecard webapp's workflow
-# restriction:
-# "scorecard job must only have steps with `uses`"
-# (https://github.com/ossf/scorecard-action#workflow-restrictions).
-#
-# When the publishing job contains a `run:` step, the webapp rejects
-# the signed-result POST with HTTP 400 "workflow verification failed".
-# That regression had this workflow `failure`-state ever since
-# `publish_results: true` was added — the score *was* computed, but
-# the webapp publish step failed and propagated to job-level failure.
-#
-# Job 1 (`scorecard`): uses-only steps. Computes the score, publishes
-# signed results to the OSSF webapp, uploads SARIF to GitHub code
-# scanning, and saves results.sarif as an artifact for Job 2.
-#
-# Job 2 (`enforce-min-score`): downloads the artifact and runs the
-# minimum-score gate. May contain `run:` steps freely — it does not
-# call scorecard-action.
-#
-# Sister fix landed in hyperpolymath/ephapax#264 (2026-06-01); this
-# PR mirrors it so the two repos stay in sync.
-
-name: OpenSSF Scorecard Enforcer
-
-on:
- push:
- branches: [main]
- schedule:
- - cron: '0 6 * * 1' # Weekly on Monday
- workflow_dispatch:
-
-# Estate guardrail: cancel superseded runs so re-pushes / rebased PR
-# updates do not pile up queued runs against the shared account-wide
-# Actions concurrency pool. Applied only to read-only check workflows
-# (no publish/mutation), so cancelling a superseded run is always safe.
-concurrency:
- group: ${{ github.workflow }}-${{ github.ref }}
- cancel-in-progress: true
-
-permissions:
- contents: read
-
-jobs:
- scorecard:
- runs-on: ubuntu-latest
- timeout-minutes: 15
- permissions:
- security-events: write
- id-token: write # For OIDC
- steps:
- - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- with:
- persist-credentials: false
-
- - name: Run Scorecard
- uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
- with:
- results_file: results.sarif
- results_format: sarif
- publish_results: true
-
- - name: Upload SARIF
- uses: github/codeql-action/upload-sarif@c6f931105cb2c34c8f901cc885ba1e2e259cf745 # v4
- with:
- sarif_file: results.sarif
-
- - name: Upload results artifact for min-score gate
- uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
- with:
- name: scorecard-results
- path: results.sarif
- retention-days: 7
-
- enforce-min-score:
- needs: scorecard
- runs-on: ubuntu-latest
- timeout-minutes: 5
- steps:
- - name: Download results artifact
- uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9
- with:
- name: scorecard-results
-
- - name: Check minimum score
- run: |
- # Parse score from SARIF results.
- SCORE=$(jq -r '.runs[0].tool.driver.properties.score // 0' results.sarif 2>/dev/null || echo "0")
-
- echo "OpenSSF Scorecard Score: $SCORE"
-
- # Minimum acceptable score (0-10 scale).
- MIN_SCORE=5
-
- if [ "$(echo "$SCORE < $MIN_SCORE" | bc -l)" = "1" ]; then
- echo "::error::Scorecard score $SCORE is below minimum $MIN_SCORE"
- exit 1
- fi
-
- # Check specific high-priority items
- check-critical:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
-
- - name: Check SECURITY.md exists
- run: |
- if [ ! -f "SECURITY.md" ]; then
- echo "::error::SECURITY.md is required"
- exit 1
- fi
-
- - name: Check for pinned dependencies
- run: |
- # Check workflows for unpinned actions
- unpinned=$(grep -r "uses:.*@v[0-9]" .github/workflows/*.yml 2>/dev/null | grep -v "#" | head -5 || true)
- if [ -n "$unpinned" ]; then
- echo "::warning::Found unpinned actions:"
- echo "$unpinned"
- fi
diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml
index 149b14c8..47acbb5f 100644
--- a/.github/workflows/scorecard.yml
+++ b/.github/workflows/scorecard.yml
@@ -1,37 +1,16 @@
-# SPDX-License-Identifier: MPL-2.0
-# Copyright (c) 2026 Jonathan D.A. Jewell
-#
-# Aligned with the documented caller pattern in
-# hyperpolymath/standards .github/workflows/scorecard-reusable.yml.
-# Previously this workflow was startup_failure-on-every-run because it
-# diverged from the reusable's documented caller shape:
-# - missing `concurrency:` group (re-pushes piled up superseded runs)
-# - `branch_protection_rule:` trigger (unused; complicates startup)
-# - `secrets: inherit` against a reusable with no `secrets:` block
-# - `permissions: read-all` instead of explicit `contents: read`
-# Pattern now matches the reusable's header example verbatim. Sister
-# fix landed in hyperpolymath/ephapax#264 (2026-06-01); this PR mirrors
-# it so the two repos stay in sync.
-name: Scorecards supply-chain security
+# SPDX-License-Identifier: PMPL-1.0-or-later
+name: OSSF Scorecard
on:
push:
- branches: [main]
+ branches: [main, master]
schedule:
- - cron: '23 4 * * 1'
+ - cron: '0 4 * * *'
workflow_dispatch:
-concurrency:
- group: ${{ github.workflow }}-${{ github.ref }}
- cancel-in-progress: true
-
permissions:
contents: read
jobs:
- analysis:
- permissions:
- security-events: write
- id-token: write
- uses: hyperpolymath/standards/.github/workflows/scorecard-reusable.yml@e0caf11508a3989574713c78f5f444f2ce5e33ef
- timeout-minutes: 10
+ scorecard:
+ uses: hyperpolymath/standards/.github/workflows/scorecard-reusable.yml@5a93d9d57cc04de4002d6d0ecd336fc7a8698910
diff --git a/cartridges/claude-ai-mcp/src/server.js b/cartridges/claude-ai-mcp/src/server.js
index 2fab17e1..b50fc756 100644
--- a/cartridges/claude-ai-mcp/src/server.js
+++ b/cartridges/claude-ai-mcp/src/server.js
@@ -1,6 +1,6 @@
+#!/usr/bin/env node
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) Jonathan D.A. Jewell
-#!/usr/bin/env node
//
// claude-ai-mcp — Anthropic Messages API cartridge for the BoJ
//
diff --git a/cartridges/model-router-mcp/src/router.js b/cartridges/model-router-mcp/src/router.js
index 5bad6d54..46abb2fa 100644
--- a/cartridges/model-router-mcp/src/router.js
+++ b/cartridges/model-router-mcp/src/router.js
@@ -1,6 +1,6 @@
+#!/usr/bin/env node
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) Jonathan D.A. Jewell
-#!/usr/bin/env node
//
// Model Router — Intelligent model switching for Claude Code
//
diff --git a/docs/integration/hcg-tier2-rollout-runbook.md b/docs/integration/hcg-tier2-rollout-runbook.md
index 3edc6b61..1946416d 100644
--- a/docs/integration/hcg-tier2-rollout-runbook.md
+++ b/docs/integration/hcg-tier2-rollout-runbook.md
@@ -6,9 +6,9 @@ Copyright (c) Jonathan D.A. Jewell
# HCG tier-2 — rollout & rollback runbook
-**Version:** 0.5 (smoke-script verb-canary expansion, Phase E in-progress)
-**Date:** 2026-06-13 (rev. from 2026-06-10)
-**Status:** Phase E deliverables E1 (deploy spec) + E5 (rollback runbook) drafted; live gateway policy (`config/gateway-policy-boj.yaml`) promoted from the worked example (§1.5); `scripts/hcg-policy-smoke.sh` lands as the checked-in §1.5 operator pre-check (deny-path covers gateway-alone; `--with-backend` adds allow-path coverage); §1.5 verb-canary block extended to cover OPTIONS, regex-route DELETE, and wrong-verb-on-listed-path so the operator pre-check fails closed against more verb-governance regression classes. Owner-input markers (`!OWNER:`) remain to be filled before any traffic-shift action is taken.
+**Version:** 0.6 (smoke-script unknown-path canary, Phase E in-progress)
+**Date:** 2026-06-14 (rev. from 2026-06-13)
+**Status:** Phase E deliverables E1 (deploy spec) + E5 (rollback runbook) drafted; live gateway policy (`config/gateway-policy-boj.yaml`) promoted from the worked example (§1.5); `scripts/hcg-policy-smoke.sh` lands as the checked-in §1.5 operator pre-check (deny-path covers gateway-alone; `--with-backend` adds allow-path coverage); §1.5 verb-canary block covers OPTIONS, regex-route DELETE, and wrong-verb-on-listed-path; a path-canary now exercises the no-match default-deny branch (synthetic unknown path with a `global_verbs` verb) so the operator pre-check fails closed against both unknown-method and unknown-path regression classes. Owner-input markers (`!OWNER:`) remain to be filled before any traffic-shift action is taken.
**ADR:** [`docs/decisions/0004-adopt-http-capability-gateway.md`](../decisions/0004-adopt-http-capability-gateway.md)
**Plan:** [`docs/integration/http-capability-gateway-plan.md`](http-capability-gateway-plan.md) (§ Phase E)
**Contract:** [`docs/integration/http-capability-gateway-boj-contract.md`](http-capability-gateway-boj-contract.md)
@@ -91,7 +91,7 @@ These cannot be inferred from the code/contract; the owner must fill them before
- [x] `container/gateway-deploy.k9.ncl` exists in the gateway repo (plan §E1) — http-capability-gateway#38 (2026-06-03). Five-level k9-svc pedigree (Snout / Scent / Leash / Gut / Muscle) modelled on `boj-server:container/deploy.k9.ncl`; per-environment `BACKEND_URL` (`http://127.0.0.1:7700` staging, `http://unix:/run/boj/gnosis.sock:/` production); trust source `"header"` staging → `"mtls"` production after §2.4 rehearsal; `max_unavailable = 0`; `failure_mode = "fail-closed"` matching the `[SEAMS] gateway-boj-gnosis` declaration.
- [x] Gateway policy file in place: `config/gateway-policy-boj-example.yaml`, covering all BoJ surface routes (`/.well-known/boj-node-pubkey`, `/health`, `/menu`, `/cartridges`, `/cartridge/:name`, `/cartridge/:name/invoke`, `/cartridge/:name/sse`, plus any added since contract v1.0). Re-verified 2026-05-28 against `BojRest.Router`; the `POST /cartridge/:name/sse` route (router.ex line 130, wired since the SSE landing — ADR-0013 §6, STATE entry 2026-05-18) was the only drift since contract v1.0 and is now governed by the `cartridge-sse-post` rule alongside `cartridge-invoke-post` (boj-server#165).
- [x] Live policy file (`config/gateway-policy-boj.yaml`) promoted from the example. Content-identical to the example at promotion time; future BoJ-surface evolution lands in the live file and the example remains as the worked-example artefact (Phase A A3). Both §2.1 staging and §3.1 production load the live file via `POLICY_PATH`.
-- [ ] Gateway has been smoke-tested in isolation with the policy, returning expected allow/deny on each route. Run `scripts/hcg-policy-smoke.sh --gateway-url ` against the gateway loaded with `config/gateway-policy-boj.yaml`; the script exercises a no-trust-header deny probe for every non-public route (25 in the live policy) plus six default-deny verb canaries — DELETE/PUT/PATCH on listed exact paths, OPTIONS on a listed path (no CORS-preflight bypass), DELETE on a regex-matched route (no per-verb regex regression), and GET on the POST-only `ssg-mcp-webhook` public route (the `{path, verb}` pairing must be enforced even when the path itself is in the policy) — and is fully gateway-internal — BoJ does **not** need to be reachable for this run. Once BoJ is up behind the gateway, re-run with `--with-backend` from a trusted-proxy IP (loopback by default) to also cover the allow path on authenticated/internal routes including the `POST /cartridge/:name/sse` authenticated/untrusted pair carried over from boj-server#165's test plan. Attach the script's PASS/FAIL summary to the cut-over ticket; a single FAIL is a stop-the-rollout condition (gateway loaded the policy but is not enforcing as declared, or BoJ is unreachable from the gateway, or the script is being run from a non-trusted-proxy IP and the trust header is being stripped).
+- [ ] Gateway has been smoke-tested in isolation with the policy, returning expected allow/deny on each route. Run `scripts/hcg-policy-smoke.sh --gateway-url ` against the gateway loaded with `config/gateway-policy-boj.yaml`; the script exercises a no-trust-header deny probe for every non-public route (25 in the live policy), six default-deny verb canaries — DELETE/PUT/PATCH on listed exact paths, OPTIONS on a listed path (no CORS-preflight bypass), DELETE on a regex-matched route (no per-verb regex regression), and GET on the POST-only `ssg-mcp-webhook` public route (the `{path, verb}` pairing must be enforced even when the path itself is in the policy) — and one path canary (GET on a synthetic `/__phase-e-canary-unknown-path__` URL that matches no exact rule, no regex rule, and no public exception) which isolates the no-match → default-deny branch of the gateway's three-tier lookup; the verb canaries cover the unknown-method path, the path canary covers the unknown-path path, and both must default-deny on independent code branches. The whole script is fully gateway-internal — BoJ does **not** need to be reachable for this run. Once BoJ is up behind the gateway, re-run with `--with-backend` from a trusted-proxy IP (loopback by default) to also cover the allow path on authenticated/internal routes including the `POST /cartridge/:name/sse` authenticated/untrusted pair carried over from boj-server#165's test plan. Attach the script's PASS/FAIL summary to the cut-over ticket; a single FAIL is a stop-the-rollout condition (gateway loaded the policy but is not enforcing as declared, or BoJ is unreachable from the gateway, or the script is being run from a non-trusted-proxy IP and the trust header is being stripped).
---
diff --git a/mcp-bridge/lib/generate-offline-menu.js b/mcp-bridge/lib/generate-offline-menu.js
index 6caa35e3..2ed1cd71 100644
--- a/mcp-bridge/lib/generate-offline-menu.js
+++ b/mcp-bridge/lib/generate-offline-menu.js
@@ -1,6 +1,6 @@
+#!/usr/bin/env -S deno run --allow-read
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) Jonathan D.A. Jewell
-#!/usr/bin/env -S deno run --allow-read
//
// Generate offline-menu.js from the cartridges/ directory structure.
//
diff --git a/mcp-bridge/lib/runtime.js b/mcp-bridge/lib/runtime.js
index 069438eb..af8ac8bc 100644
--- a/mcp-bridge/lib/runtime.js
+++ b/mcp-bridge/lib/runtime.js
@@ -7,6 +7,8 @@
// Supports the "Deno > Bun > NPM" hierarchy while maintaining
// compatibility with Node-only MCP clients like Glama.
+import { writeSync as writeFdSync } from "node:fs";
+
const isDeno = typeof globalThis.Deno !== "undefined";
/** @type {{ get: (name: string) => string|undefined }} */
@@ -32,7 +34,7 @@ export const stdout = {
if (isDeno) {
globalThis.Deno.stdout.writeSync(buf);
} else if (typeof process !== "undefined") {
- process.stdout.write(buf);
+ writeFdSync(process.stdout.fd, buf);
}
}
};
@@ -44,7 +46,7 @@ export const stderr = {
if (isDeno) {
globalThis.Deno.stderr.writeSync(buf);
} else if (typeof process !== "undefined") {
- process.stderr.write(buf);
+ writeFdSync(process.stderr.fd, buf);
}
}
};
diff --git a/mcp-bridge/main.js b/mcp-bridge/main.js
index 7ce8134d..dbaec208 100755
--- a/mcp-bridge/main.js
+++ b/mcp-bridge/main.js
@@ -1,6 +1,6 @@
+#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) Jonathan D.A. Jewell
-#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read
//
// BoJ Server — MCP transport bridge (stdio + HTTP per ADR-0013)
//
@@ -17,10 +17,8 @@
import { env, stdout } from "./lib/runtime.js";
import { sanitizeErrorMessage } from "./lib/security.js";
-import { dispatchMcpMessage } from "./lib/dispatcher.js";
import { info, error as logError } from "./lib/logger.js";
import * as otel from "./lib/otel.js";
-import { startHttpTransport } from "./lib/http-transport.js";
const TRANSPORT = (env.get("BOJ_TRANSPORT") ?? "stdio").toLowerCase();
@@ -43,6 +41,15 @@ function sendError(id, code, message) {
send({ jsonrpc: "2.0", id, error: { code, message: sanitizeErrorMessage(message) } });
}
+let dispatchMcpMessagePromise;
+
+async function getDispatchMcpMessage() {
+ if (!dispatchMcpMessagePromise) {
+ dispatchMcpMessagePromise = import("./lib/dispatcher.js").then((m) => m.dispatchMcpMessage);
+ }
+ return dispatchMcpMessagePromise;
+}
+
async function handleStdioLine(line) {
let msg;
try {
@@ -51,6 +58,7 @@ async function handleStdioLine(line) {
sendError(null, -32700, "Parse error");
return;
}
+ const dispatchMcpMessage = await getDispatchMcpMessage();
const response = await dispatchMcpMessage(msg, { transport: "stdio" });
if (response !== null) send(response);
}
@@ -77,8 +85,16 @@ async function runStdio() {
if (typeof Deno !== "undefined") {
for await (const chunk of Deno.stdin.readable) processChunk(chunk);
} else {
- // @ts-ignore: process is global in Node
- for await (const chunk of process.stdin) processChunk(chunk);
+ await new Promise((resolve, reject) => {
+ // @ts-ignore: process is global in Node/Bun
+ process.stdin.on("data", processChunk);
+ // @ts-ignore: process is global in Node/Bun
+ process.stdin.once("end", resolve);
+ // @ts-ignore: process is global in Node/Bun
+ process.stdin.once("error", reject);
+ // @ts-ignore: process is global in Node/Bun
+ process.stdin.resume();
+ });
}
await Promise.allSettled(pendingMessages);
}
@@ -88,6 +104,7 @@ async function runStdio() {
// ===================================================================
async function runHttp() {
+ const { startHttpTransport } = await import("./lib/http-transport.js");
const handle = await startHttpTransport();
await new Promise((resolve) => {
const stop = async () => {
diff --git a/mcp-bridge/tests/boot_smoke.js b/mcp-bridge/tests/boot_smoke.js
index f42487e0..33416619 100644
--- a/mcp-bridge/tests/boot_smoke.js
+++ b/mcp-bridge/tests/boot_smoke.js
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MPL-2.0
-// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath)
+// Copyright (c) 2026 Jonathan D.A. Jewell
//
// BoJ Server — bridge boot smoke (runtime portability gate)
//
@@ -58,14 +58,17 @@ let stdout = "";
let stderr = "";
child.stdout.on("data", (d) => (stdout += d));
child.stderr.on("data", (d) => (stderr += d));
+child.stdin.on("error", (e) => (stderr += `stdin error: ${e.message}\n`));
const killTimer = setTimeout(() => {
console.error(`FAIL: bridge did not exit within ${TIMEOUT_MS}ms`);
child.kill("SIGKILL");
}, TIMEOUT_MS);
-child.stdin.write(requests.map((r) => JSON.stringify(r)).join("\n") + "\n");
-child.stdin.end();
+const payload = requests.map((r) => JSON.stringify(r)).join("\n") + "\n";
+child.once("spawn", () => {
+ child.stdin.end(payload);
+});
child.on("close", (code) => {
clearTimeout(killTimer);
diff --git a/scripts/check-shebang-first.sh b/scripts/check-shebang-first.sh
new file mode 100755
index 00000000..e86fa7a9
--- /dev/null
+++ b/scripts/check-shebang-first.sh
@@ -0,0 +1,42 @@
+#!/usr/bin/env bash
+# SPDX-License-Identifier: MPL-2.0
+# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath)
+#
+# Shebangs are only interpreter directives when they are the first line.
+# A license header above "#!" makes Node, Deno, Bun, and POSIX shells parse it
+# as source text instead, so executable scripts must keep "#!" at line 1.
+
+set -euo pipefail
+
+fail=0
+
+while IFS= read -r -d '' path; do
+ case "$path" in
+ *.awk|*.bash|*.cjs|*.exs|*.fish|*.js|*.mjs|*.pl|*.py|*.rb|*.scm|*.sh|*.ts|*.zsh) ;;
+ *) continue ;;
+ esac
+
+ line_no=0
+ while IFS= read -r line || [ -n "$line" ]; do
+ line_no=$((line_no + 1))
+ case "$line" in
+ '#!'*)
+ if [ "$line_no" != "1" ]; then
+ printf 'ERROR: %s:%s has a shebang after line 1\n' "$path" "$line_no" >&2
+ fail=1
+ fi
+ ;;
+ esac
+ done < "$path"
+done < <(git ls-files -z)
+
+if [ "$fail" -ne 0 ]; then
+ cat >&2 <<'EOF'
+
+Shebangs must be the first line of executable scripts. Put SPDX and copyright
+comments immediately after the shebang.
+EOF
+ exit 1
+fi
+
+echo "OK: all tracked shebangs are on line 1"
diff --git a/scripts/datasets/process-datasets.js b/scripts/datasets/process-datasets.js
index 0265ff8a..88c2f81b 100644
--- a/scripts/datasets/process-datasets.js
+++ b/scripts/datasets/process-datasets.js
@@ -1,6 +1,6 @@
+#!/usr/bin/env node
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) Jonathan D.A. Jewell
-#!/usr/bin/env node
// Process and Integrate Datasets
diff --git a/scripts/hcg-policy-smoke.sh b/scripts/hcg-policy-smoke.sh
index aa4e278e..a87865b6 100755
--- a/scripts/hcg-policy-smoke.sh
+++ b/scripts/hcg-policy-smoke.sh
@@ -224,6 +224,20 @@ probe OPTIONS /cartridges deny "verb-canary:OPTIONS /cartr
probe DELETE /cartridge/probe/invoke deny "verb-canary:DELETE on regex route (cartridge-invoke-post)"
probe GET /cartridges/ssg-mcp/webhook deny "verb-canary:GET on POST-only public route (ssg-mcp-webhook-post)"
+# Unknown-path canary — a synthetic path that matches no exact rule,
+# no regex rule, and no public exception. The verb (GET) is in
+# `global_verbs`, so this probe isolates the no-match → default-deny
+# branch of the gateway's three-tier lookup (exact → regex → global)
+# in `lib/http_capability_gateway/gateway.ex` at the `{:error, :no_match}`
+# clause. The verb-canaries above exercise the unknown-method path
+# (a known path with a verb outside `global_verbs`); this canary
+# exercises the unknown-path path (a verb in `global_verbs` against
+# a path with no matching rule). Both must default-deny, but the code
+# paths are distinct — a regression in either is independently
+# possible. The synthetic prefix `__phase-e-canary-` is reserved for
+# this probe; it must never appear as a real route in the policy.
+probe GET /__phase-e-canary-unknown-path__ deny "path-canary:GET on synthetic unknown path (no-match default-deny)"
+
if [ "$WITH_BACKEND" = "1" ]; then
echo
echo "==> HCG policy allow smoke (--with-backend)"
diff --git a/tools/cartridge-configurator/configurator.js b/tools/cartridge-configurator/configurator.js
index 3a914b9f..5e036d34 100644
--- a/tools/cartridge-configurator/configurator.js
+++ b/tools/cartridge-configurator/configurator.js
@@ -1,6 +1,6 @@
+#!/usr/bin/env node
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) Jonathan D.A. Jewell
-#!/usr/bin/env node
// Cartridge Configurator — Apply runtime configuration to cartridges dynamically
diff --git a/tools/cartridge-provisioner/provisioner.js b/tools/cartridge-provisioner/provisioner.js
index 6cde7428..d5ae02ec 100644
--- a/tools/cartridge-provisioner/provisioner.js
+++ b/tools/cartridge-provisioner/provisioner.js
@@ -1,6 +1,6 @@
+#!/usr/bin/env node
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) Jonathan D.A. Jewell
-#!/usr/bin/env node
// Cartridge Provisioner — Deploy cartridges to BoJ Server, BoJ Server + Elixir Multiplier, or panll
diff --git a/tools/panel-harness/harness.js b/tools/panel-harness/harness.js
index 8682c50f..c333823f 100644
--- a/tools/panel-harness/harness.js
+++ b/tools/panel-harness/harness.js
@@ -1,6 +1,6 @@
+#!/usr/bin/env node
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) Jonathan D.A. Jewell
-#!/usr/bin/env node
// Panel Harness — Bridge cartridges to BoJ Server and panll