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/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