Skip to content

Commit 9c174c8

Browse files
hyperpolymathclaude
andcommitted
feat(gateway): add static policy, catalog-integration design, roadmap
Static verb-governance policy for boj-server's 5-route surface (container/gateway-policy.yaml, DSL v1 for http-capability-gateway). Phase 1: all invoke calls public. Phase 2: catalog-mode replaces it. Design doc (docs/gateway-catalog-integration.adoc) specifies the PolicyLoader.load_from_boj_catalog/1 addition to http-capability-gateway: reads BOJ_CARTRIDGES_ROOT/*/cartridge.json at boot, infers per-cartridge invoke exposure from auth.method (none→public, api-key→authenticated). No cartridge.json schema changes required. ROADMAP.adoc: mark mod.js/cartridge.json parity complete (2026-04-25), add v1.1.0 milestone for JS dispatch + gateway sidecar. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 61bd0e9 commit 9c174c8

3 files changed

Lines changed: 239 additions & 2 deletions

File tree

ROADMAP.adoc

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,27 @@ metadata parity before Grade C dogfooding.
7373
=== v1.0.1 — Adapter Migration Recovery (P0, In Progress)
7474

7575
* [ ] Remove remaining V-lang adapters across `cartridges/` (56 `.v` files as of 2026-04-12)
76-
* [ ] Ensure every cartridge has `cartridge.json` metadata (currently 65 files)
77-
* [ ] Ensure every cartridge has `mod.js` tool handlers (currently 64 files)
76+
* [x] Ensure every cartridge has `cartridge.json` metadata (all 106 cartridges covered 2026-04-25)
77+
* [x] Ensure every cartridge has `mod.js` tool handlers (all 106 cartridges covered 2026-04-25)
7878
* [ ] Re-run cartridge matrix build/tests and update `TEST-NEEDS.md` with current evidence
7979
* [ ] Refresh repo documentation claims after migration closure (counts, completion text, handover docs)
8080

81+
=== v1.1.0 — JS Dispatch + Gateway Sidecar
82+
83+
JS cartridge dispatch (the large remaining gap — 99 of 106 cartridges use `mod.js`, not Zig `.so`):
84+
85+
* [ ] `BojRest.JsInvoker` — shells out to `deno run mod.js` per invocation (Phase 1: fork-per-call, ~50 ms overhead acceptable for initial traffic)
86+
* [ ] Router dispatch branch: if `cart["ffi"]` key present → `Invoker` (Zig path); else → `JsInvoker` (Deno path)
87+
* [ ] Fix `cartridge_so_path/1` to read `cart["ffi"]["so_path"]` from the manifest instead of hardcoding the derivation
88+
* [ ] End-to-end test: `curl POST /cartridge/boj-health/invoke` (Zig) + `curl POST /cartridge/local-memory-mcp/invoke` (JS)
89+
90+
Gateway sidecar (`http-capability-gateway` in front of boj-server):
91+
92+
* [ ] Add `gateway-policy.yaml` (done — `container/gateway-policy.yaml`) to dev compose as optional sidecar
93+
* [ ] Implement `PolicyLoader.load_from_boj_catalog/1` in `http-capability-gateway` — reads `BOJ_CARTRIDGES_ROOT/*/cartridge.json` at boot, infers per-cartridge invoke exposure from `auth.method` (`none` → `public`, `api-key`/`bearer` → `authenticated`). Zero manual policy maintenance thereafter. See `docs/gateway-catalog-integration.adoc`.
94+
* [ ] Update `compose.dev.yaml` with gateway sidecar entry (port 7800 → boj-rest:7700)
95+
* [ ] Retire static `gateway-policy.yaml` once catalog mode is live
96+
8197
== Future Directions
8298

8399
* **oDNS relay**: Distributed Oblivious DNS as a network service (research problem)

container/gateway-policy.yaml

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# SPDX-License-Identifier: PMPL-1.0-or-later
2+
# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
3+
#
4+
# BoJ Server — http-capability-gateway verb governance policy (DSL v1)
5+
#
6+
# Sits in front of boj-server (port 7700). Gateway listens on port 7800
7+
# (override PORT env var). Backend: BACKEND_URL=http://127.0.0.1:7700
8+
#
9+
# Route surface (all 5 boj-server routes are covered):
10+
# GET /health — public, liveness probe
11+
# GET /menu — public, cartridge menu
12+
# GET /cartridges — public, cartridge name list
13+
# GET /cartridge/:name — public, cartridge metadata
14+
# POST /cartridge/:name/invoke — auth-required for keyed cartridges
15+
# (public fallback until catalog-mode lands)
16+
#
17+
# Phase 1 (this file): static policy, all invoke calls public.
18+
# Phase 2 (catalog-mode): gateway reads BOJ_CARTRIDGES_ROOT/*/cartridge.json
19+
# at boot and auto-derives per-cartridge exposure from auth.method:
20+
# "none" → "public"
21+
# "api-key" → "authenticated"
22+
# Replaces this file entirely; zero ongoing maintenance.
23+
# See ROADMAP.adoc §v1.1.0 and docs/gateway-catalog-integration.adoc.
24+
25+
dsl_version: "1"
26+
27+
governance:
28+
# Global default: only GET is allowed on any path not listed below.
29+
# Prevents stray PUT / PATCH / DELETE / OPTIONS from reaching boj-server.
30+
global_verbs:
31+
- GET
32+
33+
routes:
34+
# Liveness + readiness probe
35+
- path: "^/health$"
36+
verbs: [GET]
37+
exposure: public
38+
narrative: "Liveness probe — no auth required"
39+
40+
# Cartridge menu (human-readable catalogue with tiers)
41+
- path: "^/menu$"
42+
verbs: [GET]
43+
exposure: public
44+
narrative: "Cartridge menu — public read"
45+
46+
# Cartridge name list
47+
- path: "^/cartridges$"
48+
verbs: [GET]
49+
exposure: public
50+
narrative: "Cartridge inventory — public read"
51+
52+
# Per-cartridge metadata (cartridge.json contents)
53+
- path: "^/cartridge/[^/]+$"
54+
verbs: [GET]
55+
exposure: public
56+
narrative: "Cartridge metadata — public read"
57+
58+
# Tool invocation — POST only, no other verbs.
59+
# Phase 1: public exposure (no auth wired yet in boj-server).
60+
# Phase 2: replaced by per-cartridge rules generated from auth.method.
61+
- path: "^/cartridge/[^/]+/invoke$"
62+
verbs: [POST]
63+
exposure: public
64+
narrative: "Tool invocation — Phase 1 public; Phase 2 auto-derived from cartridge auth.method"
65+
66+
stealth:
67+
# Return 404 (not 403) for denied requests so the route surface does
68+
# not advertise which paths exist to unauthenticated callers.
69+
enabled: true
70+
status_code: 404
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
// SPDX-License-Identifier: PMPL-1.0-or-later
2+
// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
3+
= BoJ Gateway — Catalog-Driven Auto-Policy (Design)
4+
:toc:
5+
:toc-placement: preamble
6+
7+
Describes how `http-capability-gateway` will auto-derive its verb-governance
8+
policy from the BoJ cartridge catalog so that adding a cartridge never
9+
requires a manual policy update.
10+
11+
== The Drift Problem
12+
13+
`container/gateway-policy.yaml` handles boj-server's five routes. The four
14+
read-only routes (`/health`, `/menu`, `/cartridges`, `/cartridge/:name`) are
15+
permanently public and never change.
16+
17+
The fifth route, `POST /cartridge/:name/invoke`, is different: some cartridges
18+
require credentials (`api-key`, `bearer`) while others are fully public.
19+
Without auto-sync, operators must remember to update the gateway policy every
20+
time a keyed cartridge is added. In an estate with 100+ cartridges this is an
21+
inevitable maintenance failure mode.
22+
23+
== The Fix: `PolicyLoader.load_from_boj_catalog/1`
24+
25+
A new loader mode in `http-capability-gateway` reads
26+
`$BOJ_CARTRIDGES_ROOT/*/cartridge.json` at startup and synthesises per-cartridge
27+
invoke rules. The source of truth is already present in every
28+
`cartridge.json`:
29+
30+
[source,json]
31+
----
32+
"auth": {
33+
"method": "none" // → exposure: public
34+
}
35+
----
36+
37+
[source,json]
38+
----
39+
"auth": {
40+
"method": "api-key", // → exposure: authenticated
41+
"env_var": "GITHUB_TOKEN"
42+
}
43+
----
44+
45+
=== Exposure Inference Table
46+
47+
[cols="2,2"]
48+
|===
49+
| `auth.method` | Gateway exposure
50+
51+
| `none` | `public`
52+
| `api-key` | `authenticated`
53+
| `bearer` | `authenticated`
54+
| `oauth` | `authenticated`
55+
| _(anything else)_ | `authenticated` (fail-safe)
56+
|===
57+
58+
=== Generated Policy Shape
59+
60+
For each cartridge the loader emits two rules:
61+
62+
[source,yaml]
63+
----
64+
# metadata read — always public
65+
- path: "^/cartridge/boj-health$"
66+
verbs: [GET]
67+
exposure: public
68+
69+
# tool invocation — derived from auth.method
70+
- path: "^/cartridge/boj-health/invoke$"
71+
verbs: [POST]
72+
exposure: public # because auth.method = "none"
73+
----
74+
75+
[source,yaml]
76+
----
77+
- path: "^/cartridge/notifyhub-mcp/invoke$"
78+
verbs: [POST]
79+
exposure: authenticated # because auth.method = "api-key"
80+
----
81+
82+
These are merged with the static base rules for `/health`, `/menu`,
83+
`/cartridges`.
84+
85+
No changes to `cartridge.json` schema are needed. The inference is fully
86+
deterministic from existing fields.
87+
88+
== Gateway Changes Required
89+
90+
Two changes to `http-capability-gateway`:
91+
92+
=== 1. `PolicyLoader.load_from_boj_catalog/1`
93+
94+
[source,elixir]
95+
----
96+
@spec load_from_boj_catalog(catalog_root :: String.t()) :: {:ok, map()} | {:error, String.t()}
97+
def load_from_boj_catalog(catalog_root) do
98+
# Scan cartridges/*/cartridge.json, infer per-cartridge rules,
99+
# merge with static base policy for /health /menu /cartridges.
100+
end
101+
----
102+
103+
=== 2. Config trigger
104+
105+
[source,elixir]
106+
----
107+
config :http_capability_gateway,
108+
catalog_integration: [
109+
enabled: true,
110+
root: System.get_env("BOJ_CARTRIDGES_ROOT", "/app/cartridges")
111+
]
112+
----
113+
114+
When `enabled: true`, `Application.start/2` calls `load_from_boj_catalog/1`
115+
instead of `load_from_file/1`. The static `gateway-policy.yaml` is retained
116+
only as a fallback if the catalog root is unreachable.
117+
118+
== Compose / Container Integration
119+
120+
[source,yaml]
121+
----
122+
services:
123+
boj-gateway:
124+
image: http-capability-gateway:latest
125+
ports: ["7800:4000"]
126+
environment:
127+
BACKEND_URL: http://boj-rest:7700
128+
BOJ_CARTRIDGES_ROOT: /workspace/cartridges
129+
CATALOG_INTEGRATION_ENABLED: "true"
130+
volumes:
131+
- ..:/workspace:ro,z # same mount as boj-rest dev compose
132+
----
133+
134+
The gateway shares the read-only workspace mount and reads the same
135+
`cartridge.json` files boj-server uses. No extra file sync needed.
136+
137+
== Rollout Plan
138+
139+
. *Phase 1 (done)*: static `container/gateway-policy.yaml`, all invoke calls
140+
public. Ships with boj-server; usable immediately when gateway is deployed.
141+
. *Phase 2*: implement `PolicyLoader.load_from_boj_catalog/1` in
142+
`http-capability-gateway`. Update `compose.dev.yaml` to add gateway
143+
sidecar. Retire the static file.
144+
. *Phase 3*: wire `:authenticated` trust check in boj-server's Invoker so
145+
keyed-cartridge invoke calls that lack the credential are rejected at the
146+
Elixir layer too (defence-in-depth behind the gateway).
147+
148+
== Non-Changes
149+
150+
`cartridge.json` schema is **not** extended. The inference table above is
151+
the complete spec. No new `"gateway"` section, no new fields.

0 commit comments

Comments
 (0)