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
43 changes: 43 additions & 0 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,49 @@ Adding to this list requires explicit user approval and an unblock condition. Au

---

## ABI / FFI / Adapter Baseline (verified 2026-07-17)

Every cartridge's interface stack is three layers, in these languages, no
exceptions:

| Layer | Language | Role |
|---|---|---|
| **ABI** | **Idris2** | Formally verified contract — dependent-type proofs, state machine or exposure-gate invariants, `%default total`, zero `believe_me`/`postulate`/`assert_total` in the trusted core. |
| **FFI** | **Zig** | C-ABI implementation (ADR-0006 five-symbol pattern: `boj_cartridge_{init,deinit,name,version,invoke}`). |
| **Adapter** | **Zig** | The base-level API/service surface — see below. |

### The "unified adapter"

The current canonical shape (see `cartridges/k9iser-mcp/adapter/` for the
reference implementation, or `cartridges/templates/gossamer-mcp/adapter/` in
`boj-server-cartridges` for the minting template): **one loopback listener**,
protocol-classified (REST `/invoke`, SSE `/sse`, GraphQL `/graphql`, gRPC-compat
`/grpc/<Svc>/<Method>`), that funnels every request through a **transaction
gate** (`exposureSatisfied`, mirroring the cartridge's own Idris2 exposure
contract when it has one) before a single dispatch call into the one Zig ABI
(`ffi.boj_cartridge_invoke`). Invariants (from the adapter READMEs, verbatim):

- **Stateless** — all state lives behind the FFI, never in the adapter.
- **Response passthrough** — whatever the FFI returns goes back to the wire
unmodified (no embellishment, no silent recovery).
- **`cartridge.json` is source of truth** for the tool catalogue; drift between
adapter and manifest is a CI failure.
- **Internal-only, never a public ingress.** Per ADR-0004 the only governed
public surface is the `http-capability-gateway` (tier-2) in front of the
unified Zig core; adapters bind loopback and sit behind it.

Naming lineage: the fuller 16-protocol-surface pattern is called the
**Hexadeca-Connector** elsewhere in the estate (`hyperpolymath/hypatia`,
`hyperpolymath/proven-servers`) — same Idris2-ABI + Zig-FFI substrate, more
protocol surfaces. It descends from a now-**retired** V-lang reference
(`developer-ecosystem/v-ecosystem/v_api_interfaces/v_api_interfaces.v`),
replaced across the estate by Zig+Idris2(+Rust-client where relevant) per the
V-lang ban (2026-04-10). Cartridge `adapter/` dirs keep a `SIDELINED-*.v.adoc`
note documenting what was replaced — never resurrect the V version, and never
substitute anything but Zig for a new adapter.

---

## PR Workflow

This repo squash-merges PRs. Two consequences worth knowing before pushing follow-ups:
Expand Down
44 changes: 44 additions & 0 deletions cartridges/bug-filing-mcp/abi/BugFilingMcp/SafeBugFiling.idr
Original file line number Diff line number Diff line change
Expand Up @@ -156,3 +156,47 @@ export
bf_can_transition : Int -> Int -> Int
bf_can_transition from to =
if canTransition (intToFilingState from) (intToFilingState to) then 1 else 0

-- ═══════════════════════════════════════════════════════════════════════════
-- Exposure / transaction-gating contract (BoJ interface-safety policy)
-- ═══════════════════════════════════════════════════════════════════════════
-- A port boundary must never be a gatekeeperless gateway: every adapter
-- exposes the unified ABI ONLY behind this transaction gate. Mirrors
-- BojRest.TrustPolicy and K9iserMcp.SafeK9iser's identically-named contract:
-- caller exposure derived from the cartridge's auth.method, loopback callers
-- locally trusted, X-Trust-Level enforced otherwise.

||| Caller trust the gateway/sidecar has established.
public export
data Exposure = Public | Authenticated | Internal

||| Required exposure inferred from cartridge auth.method.
||| "none"/absent -> Public; any credential-bearing method -> Authenticated.
||| bug-filing-mcp's cartridge.json declares auth.method = "none", so this
||| cartridge's required exposure is Public.
public export
requiredExposure : (authMethodIsNone : Bool) -> Exposure
requiredExposure True = Public
requiredExposure False = Authenticated

||| The transaction gate. Loopback callers are locally trusted (mcp-bridge,
||| local curl). Otherwise the presented X-Trust-Level must meet the
||| required exposure. This is the total relation the Zig transaction layer
||| mirrors; no dispatch may occur unless it returns True.
public export
exposureSatisfied : (required : Exposure) -> (presented : Exposure) -> (isLocal : Bool) -> Bool
exposureSatisfied _ _ True = True
exposureSatisfied Public _ _ = True
exposureSatisfied Authenticated Authenticated _ = True
exposureSatisfied Authenticated Internal _ = True
exposureSatisfied Internal Internal _ = True
exposureSatisfied _ _ _ = False

||| FFI: 1 if dispatch is permitted, 0 if the gate rejects.
||| req/pres encoding: 0=Public 1=Authenticated 2=Internal; isLocal: 1/0.
export
bf_exposure_satisfied : Int -> Int -> Int -> Int
bf_exposure_satisfied req pres isLocal =
let r = case req of { 0 => Public; 1 => Authenticated; _ => Internal }
p = case pres of { 0 => Public; 1 => Authenticated; _ => Internal }
in if exposureSatisfied r p (isLocal /= 0) then 1 else 0
51 changes: 51 additions & 0 deletions cartridges/bug-filing-mcp/adapter/README.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// SPDX-License-Identifier: CC-BY-SA-4.0
// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
= bug-filing-mcp / adapter — protocol bridge
:orientation: shallow

== Purpose

Exposes this cartridge's FFI over the wire protocols declared in
`cartridge.json`. Dispatches tool invocations to the C-ABI exports at
`../ffi/` and formats responses per protocol.

INTERNAL-ONLY unified adapter — not a public ingress. Per ADR-0004 the only
governed public surface is the `http-capability-gateway` (tier-2); this
adapter binds loopback (`127.0.0.1:9391`) and sits behind it.

== Files

[cols="1,4"]
|===
| File | Role

| `build.zig` | Zig build graph — depends on the sibling `ffi` target.
| `bug_filing_mcp_adapter.zig` | Protocol dispatch. Tool catalogue matches `cartridge.json` (`research_feedback`, `synthesize_feedback`, `submit_feedback`).
|===

== Invariants

* **Stateless** — all state lives behind the FFI, never in the adapter.
* **Response passthrough** — whatever the FFI returns goes back to the wire
unmodified (no embellishment, no silent recovery). Since the FFI itself is
an honest delegation stub (see `../ffi/README.adoc` / `../ABI-FFI-README.md`
equivalent), every protocol surface here answers with that same delegation
notice — this adapter does not paper over what the FFI does not do.
* **Cartridge.json is source of truth** for the tool catalogue; drift between
adapter and manifest is a CI failure.
* **Transaction-gated** — every request passes `exposureSatisfied` (mirroring
`BugFilingMcp.SafeBugFiling.exposureSatisfied` in `../abi/`) before dispatch.
`auth.method: "none"` in `cartridge.json` means `requiredExposure = Public`.

== Test/proof surface

Inline Zig tests cover protocol classification, tool extraction per protocol,
dispatch-to-ABI funneling, and the exposure-gate truth table (cross-checked
against the Idris2 contract by hand — same truth table, same encoding).

== Read-first

. `bug_filing_mcp_adapter.zig` — dispatch function and per-protocol routers.
. `../cartridge.json` — the tool manifest the dispatcher mirrors.
. `../abi/BugFilingMcp/SafeBugFiling.idr` — the Idris2 contract this adapter's
gate mirrors.
Loading
Loading