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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions .github/CICD-CHANGES-2026-06-04.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
<!--
SPDX-License-Identifier: MPL-2.0
Copyright (c) Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
-->

# CI/CD Changes — 2026-06-04

**Date:** 2026-06-04
Expand All @@ -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 |
Expand Down Expand Up @@ -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.

---

Expand Down
6 changes: 4 additions & 2 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/dogfood-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

2 changes: 1 addition & 1 deletion cartridges/claude-ai-mcp/src/server.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env node
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
#!/usr/bin/env node
//
// claude-ai-mcp — Anthropic Messages API cartridge for the BoJ
//
Expand Down
2 changes: 1 addition & 1 deletion cartridges/model-router-mcp/src/router.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env node
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
#!/usr/bin/env node
//
// Model Router — Intelligent model switching for Claude Code
//
Expand Down
8 changes: 4 additions & 4 deletions docs/integration/hcg-tier2-rollout-runbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ Copyright (c) Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>

# 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)
Expand Down Expand Up @@ -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 <staging-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 <staging-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).

---

Expand Down
2 changes: 1 addition & 1 deletion mcp-bridge/lib/generate-offline-menu.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env -S deno run --allow-read
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
#!/usr/bin/env -S deno run --allow-read
//
// Generate offline-menu.js from the cartridges/ directory structure.
//
Expand Down
6 changes: 4 additions & 2 deletions mcp-bridge/lib/runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 }} */
Expand All @@ -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);
}
}
};
Expand All @@ -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);
}
}
};
Expand Down
27 changes: 22 additions & 5 deletions mcp-bridge/main.js
Original file line number Diff line number Diff line change
@@ -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 <j.d.a.jewell@open.ac.uk>
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read
//
// BoJ Server — MCP transport bridge (stdio + HTTP per ADR-0013)
//
Expand All @@ -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();

Expand All @@ -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 {
Expand All @@ -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);
}
Expand All @@ -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);
}
Expand All @@ -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 () => {
Expand Down
9 changes: 6 additions & 3 deletions mcp-bridge/tests/boot_smoke.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
// Copyright (c) 2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
//
// BoJ Server — bridge boot smoke (runtime portability gate)
//
Expand Down Expand Up @@ -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);
Expand Down
42 changes: 42 additions & 0 deletions scripts/check-shebang-first.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: MPL-2.0
# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
#
# 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"
2 changes: 1 addition & 1 deletion scripts/datasets/process-datasets.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env node
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
#!/usr/bin/env node

// Process and Integrate Datasets

Expand Down
14 changes: 14 additions & 0 deletions scripts/hcg-policy-smoke.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
Expand Down
2 changes: 1 addition & 1 deletion tools/cartridge-configurator/configurator.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env node
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
#!/usr/bin/env node

// Cartridge Configurator — Apply runtime configuration to cartridges dynamically

Expand Down
2 changes: 1 addition & 1 deletion tools/cartridge-provisioner/provisioner.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env node
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
#!/usr/bin/env node

// Cartridge Provisioner — Deploy cartridges to BoJ Server, BoJ Server + Elixir Multiplier, or panll

Expand Down
2 changes: 1 addition & 1 deletion tools/panel-harness/harness.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env node
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
#!/usr/bin/env node

// Panel Harness — Bridge cartridges to BoJ Server and panll

Expand Down
Loading