diff --git a/README.adoc b/README.adoc deleted file mode 100644 index 21e1a02..0000000 --- a/README.adoc +++ /dev/null @@ -1,467 +0,0 @@ -// SPDX-License-Identifier: CC-BY-SA-4.0 -// Copyright (c) Jonathan D.A. Jewell -// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell - -= http-capability-gateway -image:https://img.shields.io/badge/OpenSSF-Best_Practices-green?logo=openssourcesecurity[OpenSSF Best Practices,link="https://www.bestpractices.dev/en/projects/new?repo_url=https://github.com/hyperpolymath/http-capability-gateway"] - -Jonathan D.A. Jewell -:toc: preamble -:icons: font -:sectnums: - -image:https://img.shields.io/badge/License-MPL_2.0-blue.svg[License: MPL-2.0,link="https://opensource.org/licenses/MPL-2.0"] -image:https://img.shields.io/badge/Elixir-1.19+-purple.svg[Elixir 1.19+] -image:https://img.shields.io/badge/OTP-27+-red.svg[OTP 27+] - -== Overview - -http-capability-gateway is a lightweight, policy-driven HTTP governance layer -that enforces a declarative, auditable model of HTTP verb exposure in front of -existing services. - -This project introduces a minimal viable implementation of a new category: -a capability gateway for HTTP. It does not replace nginx or Apache. Instead, -it governs what they are allowed to do. - -The gateway loads a Verb Governance Spec (DSL v1), validates it, compiles it -into fast enforcement rules, and applies those rules to real HTTP traffic. -Every decision is logged in structured form for audit and introspection. - -=== Current Status - -This repository contains a real Elixir gateway implementation, but it should -currently be treated as a narrow, in-progress API governance layer rather than -a fully proven front door for an entire site. - -* The core policy pipeline exists: loader, validator, compiler, gateway, proxy, telemetry. -* The main remaining gaps are security depth, end-to-end verification, and benchmark evidence. -* Read `ROADMAP.adoc`, `TEST-NEEDS.md`, and `PROOFS_NEEDED.md` together when judging readiness. - -It provides: - -* *Declarative Verb Governance*: Define allowed HTTP verbs globally and per-route -* *Stealth Mode*: Return configurable status codes (404, 403, etc.) for unauthorized requests -* *Fast Policy Enforcement Architecture*: ETS-backed lookups and compiled policy rules; benchmark evidence still needs to be formalized -* *Trust Level Integration*: Current implementation is header-based, with mTLS-oriented direction documented but not the primary proved path yet -* *Comprehensive Logging*: Structured JSON logs with telemetry metrics -* *Backend Proxy*: Transparent proxying to backend services with header preservation - -Wondering how this works? See link:EXPLAINME.adoc[]. - -== Why This Exists - -Modern systems expose HTTP methods inconsistently and often accidentally. -DELETE, PUT, PATCH, OPTIONS, and even HEAD can leak capabilities or create -attack surface when left unmanaged. - -Traditional reverse proxies do not provide: - -* per-verb governance -* narrative or provenance -* reversible policy artefacts -* trust-aware verb exposure -* structured constraints -* intentional stealthing or deception - -http-capability-gateway introduces a principled, schema-driven approach to -HTTP method governance without disrupting existing infrastructure. - -== MVP Scope - -The MVP focuses on the smallest coherent loop: - -1. Load a Verb Governance Spec from disk -2. Validate it against a top-level schema -3. Compile it into fast, matchable rules -4. Enforce those rules on real HTTP traffic -5. Emit structured logs for every decision - -No trust engine, no dynamic scoring, no control plane, no VeriSimDB integration. -Those will grow around this core in later phases. - -== Quick Start - -=== Installation - -[source,bash] ----- -# Clone the repository -git clone https://github.com/hyperpolymath/http-capability-gateway.git -cd http-capability-gateway - -# Install dependencies -mix deps.get - -# Compile -mix compile ----- - -=== Basic Usage - -. *Create a policy file* (`config/policy.yaml`): -+ -[source,yaml] ----- -dsl_version: "1" -governance: - global_verbs: - - GET - - POST - routes: - - path: "/api/admin" - verbs: [GET] - - path: "/api/users/[0-9]+" - verbs: [GET, PUT, DELETE] -stealth: - enabled: true - status_code: 404 ----- - -. *Configure backend* (`config/dev.exs`): -+ -[source,elixir] ----- -config :http_capability_gateway, - policy_path: "examples/policy-dev.yaml", - backend_url: "http://localhost:4000", - port: 4000 ----- - -. *Start the gateway*: -+ -[source,bash] ----- -mix run --no-halt - -# Or with interactive shell -iex -S mix ----- - -. *Test requests*: -+ -[source,bash] ----- -# Allowed: GET on global route -curl http://localhost:4000/api/public -# Returns: proxied response from backend - -# Denied: DELETE not in global verbs -curl -X DELETE http://localhost:4000/api/public -# Returns: 404 (stealth mode) - -# Allowed: PUT on specific route -curl -X PUT http://localhost:4000/api/users/123 -# Returns: proxied response from backend ----- - -== Verb Governance Spec (DSL v1) - -=== Structure - -[source,yaml] ----- -dsl_version: "1" # Required: policy format version - -governance: - # Global verbs: allowed on all routes unless overridden - global_verbs: - - GET - - POST - - # Route-specific rules (optional) - routes: - - path: "/api/admin" - verbs: [GET] # Only GET allowed, overrides global - - path: "/api/users/[0-9]+" # Regex patterns supported - verbs: [GET, PUT, DELETE] - -stealth: # Optional stealth mode configuration - enabled: true - status_code: 404 # Status code for denied requests ----- - -Earlier-format DSL (v0) uses a richer `service`/`verbs`/`narrative` structure: - -[source,yaml] ----- -service: - name: ledger-api - version: 1 - environment: dev - -verbs: - GET: { exposure: public } - POST: { exposure: authenticated } - DELETE: { exposure: internal } - -routes: - - path: /accounts - verbs: - DELETE: - exposure: internal - narrative: "Account deletion requires internal trust." - -stealth: - profiles: - limited: - unauthenticated: 405 - untrusted: 404 - -narrative: - purpose: "Define safe verb exposure for ledger operations." ----- - -=== Supported HTTP Verbs - -`GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`, `OPTIONS` - -=== Path Matching - -* *Literal paths*: `/api/users` -* *Regex patterns*: `/api/users/[0-9]+` (numeric user IDs) -* *Wildcard patterns*: `/api/posts/.+` (any post path) - -=== Stealth Mode - -When a request is denied: - -* *Stealth enabled*: Returns configured status code (e.g., 404) with empty body -* *Stealth disabled*: Returns 403 Forbidden - -Valid stealth status codes: `200`, `301`, `302`, `403`, `404`, `410`, `500`, `503` - -== Configuration - -=== Environment Variables - -[source,bash] ----- -# Policy file path (default: config/policy.yaml) -export POLICY_PATH=/path/to/policy.yaml - -# Backend URL (required) -export BACKEND_URL=http://backend:4000 - -# Gateway port (default: 4000) -export PORT=4000 - -# Trust level header name (default: x-trust-level) -export TRUST_LEVEL_HEADER=x-trust-level ----- - -=== Elixir Config - -`config/config.exs` (shared config): - -[source,elixir] ----- -import Config - -config :http_capability_gateway, - backend_url: System.get_env("BACKEND_URL", "http://localhost:4000"), - port: String.to_integer(System.get_env("PORT", "4000")), - trust_level_header: System.get_env("TRUST_LEVEL_HEADER", "x-trust-level") ----- - -`config/dev.exs` (development): - -[source,elixir] ----- -import Config - -config :http_capability_gateway, - policy_path: "examples/policy-dev.yaml", - log_level: :debug ----- - -`config/prod.exs` (production): - -[source,elixir] ----- -import Config - -config :http_capability_gateway, - policy_path: System.get_env("POLICY_PATH"), - log_level: :info ----- - -== Trust Levels - -Extract trust levels from mTLS certificates or HTTP headers: - -[source,bash] ----- -# From header (current implementation) -curl -H "X-Trust-Level: high" http://localhost:4000/api/admin ----- - -Trust levels can be used for: - -* Audit logging -* Fine-grained access control (future feature) -* Rate limiting (future feature) - -== Logging - -Structured JSON logs with telemetry: - -[source,json] ----- -{ - "timestamp": "2026-01-22T23:00:00.000Z", - "level": "info", - "message": "request_handled", - "request_id": "req-abc123", - "method": "GET", - "path": "/api/users/123", - "trust_level": "high", - "verb_allowed": true, - "stealth_triggered": false, - "response_status": 200, - "duration_ms": 45 -} ----- - -== Testing - -[source,bash] ----- -# Run all tests -mix test - -# Run specific test file -mix test test/policy_loader_test.exs - -# Run property-based tests only -mix test --only property - -# Run performance tests -mix test --only performance - -# Run with coverage -mix test --cover ----- - -Current tests cover the policy pipeline and some gateway behavior, but the repo -still needs materially stronger evidence in the following areas: - -* security tests for token validation, request sanitization, and SSRF resistance -* end-to-end request lifecycle tests -* concurrency and reload testing -* formal benchmark runs - -Do not treat the current suite as sufficient proof for whole-site gateway deployment. - -== Architecture (MVP) - -[source,ascii] ----- -Policy File (DSL) - | - v -Policy Loader → Validator → Compiler - | - v -Gateway (Elixir) - | - v -HTTP Traffic → Enforcement → JSON Logs ----- - -See link:TOPOLOGY.md[TOPOLOGY.md] for a full visual architecture map and completion dashboard. - -== Performance Optimisations (v1.0.0) - -=== Tiered ETS Lookup - -Policy lookups use a three-tier strategy that eliminates full-table scans: - -* *Tier 1 — Exact Path (O(1))*: Literal route patterns (no regex metacharacters) are stored with `{:exact, path, verb}` ETS keys. A direct hash lookup resolves 90%+ of requests instantly. -* *Tier 2 — Regex Routes (O(r))*: Patterns containing regex metacharacters are tested only against other regex routes, not the entire table. -* *Tier 3 — Global Rules (O(1))*: If no route matches, a final `{:global, verb}` lookup catches global verb defaults. - -For a 1000-route policy, this reduces from ~1000 regex evaluations per request to -1 hash lookup (typical case) or ~50 regex evaluations (edge case). - -Performance-oriented design is present, but the benchmark story is not yet strong -enough to advertise hard numbers as release evidence. A performance test file exists; -benchmarking and concurrency validation are still tracked as open work in -`ROADMAP.adoc` and `TEST-NEEDS.md`. - -=== Security Headers - -All responses (including health/metrics endpoints) include OWASP-recommended security headers: - -* `X-Content-Type-Options: nosniff` — prevent MIME-type sniffing -* `X-Frame-Options: DENY` — prevent clickjacking -* `Referrer-Policy: strict-origin-when-cross-origin` — limit referrer leakage -* `Cache-Control: no-store, no-cache, must-revalidate` — prevent caching of policy decisions -* `Connection: close` — prevent connection reuse across trust boundaries - -== Project Structure - -[source] ----- -http-capability-gateway/ -├── lib/ -│ └── http_capability_gateway/ -│ ├── application.ex # OTP application -│ ├── gateway.ex # HTTP gateway (Plug.Router) -│ ├── proxy.ex # Backend proxy (Req) -│ ├── policy_loader.ex # YAML policy loading -│ ├── policy_validator.ex # DSL v1 validation -│ ├── policy_compiler.ex # ETS compilation -│ ├── logging.ex # Structured logging -│ └── log_formatter.ex # JSON log formatter -├── test/ # Current automated tests -├── config/ # Elixir config files and default policy -├── examples/ # Example policies -└── docs/ # API documentation ----- - -== Roadmap - -Use `ROADMAP.adoc` as the current roadmap. The short version: - -* core policy pipeline exists -* tests exist but are not yet strong enough for broad production claims -* the recommended near-term role is route-scoped API prefiltering, not full-site gateway responsibility -* benchmark, E2E, and security-hardening work remain open - -=== Phase 2 -* Rate limits -* Expanded stealth profiles -* Hot policy reloads - -=== Phase 3 -* Control plane -* VeriSimDB integration for provenance -* Distributed gateways - -=== Phase 4 -* Agent introspection -* Constraint engine -* Formal proofs - -== Contributing - -Contributions welcome! Please: - -. Fork the repository -. Create a feature branch -. Add tests for new features -. Ensure `mix test` passes -. Submit a pull request - -== Philosophy - -This project treats governance as a first-class engineering concern. -Policies are artefacts. Artefacts are reversible. Decisions have provenance. -HTTP verbs become capabilities, not accidents. - -== License - -This project is licensed under the Mozilla Public License, v. 2.0. See the `LICENSE` file for details. - -SPDX-License-Identifier: CC-BY-SA-4.0 \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..af26817 --- /dev/null +++ b/README.md @@ -0,0 +1,527 @@ + + +Jonathan D.A. Jewell \<[j.d.a.jewell@open.ac](j.d.a.jewell@open.ac).uk\> +:toc: preamble :icons: font :sectnums: + +[![License: MPL-2.0](https://img.shields.io/badge/License-MPL_2.0-blue.svg)](https://opensource.org/licenses/MPL-2.0) image:[Elixir +1.19+](https://img.shields.io/badge/Elixir-1.19+-purple.svg) image:[OTP +27+](https://img.shields.io/badge/OTP-27+-red.svg) + +# Overview + +http-capability-gateway is a lightweight, policy-driven HTTP governance +layer that enforces a declarative, auditable model of HTTP verb exposure +in front of existing services. + +This project introduces a minimal viable implementation of a new +category: a capability gateway for HTTP. It does not replace nginx or +Apache. Instead, it governs what they are allowed to do. + +The gateway loads a Verb Governance Spec (DSL v1), validates it, +compiles it into fast enforcement rules, and applies those rules to real +HTTP traffic. Every decision is logged in structured form for audit and +introspection. + +## Current Status + +This repository contains a real Elixir gateway implementation, but it +should currently be treated as a narrow, in-progress API governance +layer rather than a fully proven front door for an entire site. + +- The core policy pipeline exists: loader, validator, compiler, gateway, + proxy, telemetry. + +- The main remaining gaps are security depth, end-to-end verification, + and benchmark evidence. + +- Read `ROADMAP.adoc`, `TEST-NEEDS.md`, and `PROOFS_NEEDED.md` together + when judging readiness. + +It provides: + +- **Declarative Verb Governance**: Define allowed HTTP verbs globally + and per-route + +- **Stealth Mode**: Return configurable status codes (404, 403, etc.) + for unauthorized requests + +- **Fast Policy Enforcement Architecture**: ETS-backed lookups and + compiled policy rules; benchmark evidence still needs to be formalized + +- **Trust Level Integration**: Current implementation is header-based, + with mTLS-oriented direction documented but not the primary proved + path yet + +- **Comprehensive Logging**: Structured JSON logs with telemetry metrics + +- **Backend Proxy**: Transparent proxying to backend services with + header preservation + +Wondering how this works? See [EXPLAINME.adoc](EXPLAINME.adoc). + +# Why This Exists + +Modern systems expose HTTP methods inconsistently and often +accidentally. DELETE, PUT, PATCH, OPTIONS, and even HEAD can leak +capabilities or create attack surface when left unmanaged. + +Traditional reverse proxies do not provide: + +- per-verb governance + +- narrative or provenance + +- reversible policy artefacts + +- trust-aware verb exposure + +- structured constraints + +- intentional stealthing or deception + +http-capability-gateway introduces a principled, schema-driven approach +to HTTP method governance without disrupting existing infrastructure. + +# MVP Scope + +The MVP focuses on the smallest coherent loop: + +1. Load a Verb Governance Spec from disk + +2. Validate it against a top-level schema + +3. Compile it into fast, matchable rules + +4. Enforce those rules on real HTTP traffic + +5. Emit structured logs for every decision + +No trust engine, no dynamic scoring, no control plane, no VeriSimDB +integration. Those will grow around this core in later phases. + +# Quick Start + +## Installation + +```bash +# Clone the repository +git clone https://github.com/hyperpolymath/http-capability-gateway.git +cd http-capability-gateway + +# Install dependencies +mix deps.get + +# Compile +mix compile +``` + +## Basic Usage + +1. **Create a policy file** (`config/policy.yaml`): + + ``` yaml + dsl_version: "1" + governance: + global_verbs: + - GET + - POST + routes: + - path: "/api/admin" + verbs: [GET] + - path: "/api/users/[0-9]+" + verbs: [GET, PUT, DELETE] + stealth: + enabled: true + status_code: 404 + ``` + + + +1. **Configure backend** (`config/dev.exs`): + + ``` elixir + config :http_capability_gateway, + policy_path: "examples/policy-dev.yaml", + backend_url: "http://localhost:4000", + port: 4000 + ``` + + + +1. **Start the gateway**: + + ``` bash + mix run --no-halt + + # Or with interactive shell + iex -S mix + ``` + + + +1. **Test requests**: + + ``` bash + # Allowed: GET on global route + curl http://localhost:4000/api/public + # Returns: proxied response from backend + + # Denied: DELETE not in global verbs + curl -X DELETE http://localhost:4000/api/public + # Returns: 404 (stealth mode) + + # Allowed: PUT on specific route + curl -X PUT http://localhost:4000/api/users/123 + # Returns: proxied response from backend + ``` + +# Verb Governance Spec (DSL v1) + +## Structure + +```yaml +dsl_version: "1" # Required: policy format version + +governance: + # Global verbs: allowed on all routes unless overridden + global_verbs: + - GET + - POST + + # Route-specific rules (optional) + routes: + - path: "/api/admin" + verbs: [GET] # Only GET allowed, overrides global + - path: "/api/users/[0-9]+" # Regex patterns supported + verbs: [GET, PUT, DELETE] + +stealth: # Optional stealth mode configuration + enabled: true + status_code: 404 # Status code for denied requests +``` + +Earlier-format DSL (v0) uses a richer `service`/`verbs`/`narrative` +structure: + +```yaml +service: + name: ledger-api + version: 1 + environment: dev + +verbs: + GET: { exposure: public } + POST: { exposure: authenticated } + DELETE: { exposure: internal } + +routes: + - path: /accounts + verbs: + DELETE: + exposure: internal + narrative: "Account deletion requires internal trust." + +stealth: + profiles: + limited: + unauthenticated: 405 + untrusted: 404 + +narrative: + purpose: "Define safe verb exposure for ledger operations." +``` + +## Supported HTTP Verbs + +`GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`, `OPTIONS` + +## Path Matching + +- **Literal paths**: `/api/users` + +- **Regex patterns**: `/api/users/[0-9]+` (numeric user IDs) + +- **Wildcard patterns**: `/api/posts/.+` (any post path) + +## Stealth Mode + +When a request is denied: + +- **Stealth enabled**: Returns configured status code (e.g., 404) with + empty body + +- **Stealth disabled**: Returns 403 Forbidden + +Valid stealth status codes: `200`, `301`, `302`, `403`, `404`, `410`, +`500`, `503` + +# Configuration + +## Environment Variables + +```bash +# Policy file path (default: config/policy.yaml) +export POLICY_PATH=/path/to/policy.yaml + +# Backend URL (required) +export BACKEND_URL=http://backend:4000 + +# Gateway port (default: 4000) +export PORT=4000 + +# Trust level header name (default: x-trust-level) +export TRUST_LEVEL_HEADER=x-trust-level +``` + +## Elixir Config + +`config/config.exs` (shared config): + +```elixir +import Config + +config :http_capability_gateway, + backend_url: System.get_env("BACKEND_URL", "http://localhost:4000"), + port: String.to_integer(System.get_env("PORT", "4000")), + trust_level_header: System.get_env("TRUST_LEVEL_HEADER", "x-trust-level") +``` + +`config/dev.exs` (development): + +```elixir +import Config + +config :http_capability_gateway, + policy_path: "examples/policy-dev.yaml", + log_level: :debug +``` + +`config/prod.exs` (production): + +```elixir +import Config + +config :http_capability_gateway, + policy_path: System.get_env("POLICY_PATH"), + log_level: :info +``` + +# Trust Levels + +Extract trust levels from mTLS certificates or HTTP headers: + +```bash +# From header (current implementation) +curl -H "X-Trust-Level: high" http://localhost:4000/api/admin +``` + +Trust levels can be used for: + +- Audit logging + +- Fine-grained access control (future feature) + +- Rate limiting (future feature) + +# Logging + +Structured JSON logs with telemetry: + +```json +{ + "timestamp": "2026-01-22T23:00:00.000Z", + "level": "info", + "message": "request_handled", + "request_id": "req-abc123", + "method": "GET", + "path": "/api/users/123", + "trust_level": "high", + "verb_allowed": true, + "stealth_triggered": false, + "response_status": 200, + "duration_ms": 45 +} +``` + +# Testing + +```bash +# Run all tests +mix test + +# Run specific test file +mix test test/policy_loader_test.exs + +# Run property-based tests only +mix test --only property + +# Run performance tests +mix test --only performance + +# Run with coverage +mix test --cover +``` + +Current tests cover the policy pipeline and some gateway behavior, but +the repo still needs materially stronger evidence in the following +areas: + +- security tests for token validation, request sanitization, and SSRF + resistance + +- end-to-end request lifecycle tests + +- concurrency and reload testing + +- formal benchmark runs + +Do not treat the current suite as sufficient proof for whole-site +gateway deployment. + +# Architecture (MVP) + +```ascii +Policy File (DSL) + | + v +Policy Loader → Validator → Compiler + | + v +Gateway (Elixir) + | + v +HTTP Traffic → Enforcement → JSON Logs +``` + +See TOPOLOGY for a full visual +architecture map and completion dashboard. + +# Performance Optimisations (v1.0.0) + +## Tiered ETS Lookup + +Policy lookups use a three-tier strategy that eliminates full-table +scans: + +- **Tier 1 — Exact Path (O(1))**: Literal route patterns (no regex + metacharacters) are stored with `{:exact,` `path,` `verb}` ETS keys. A + direct hash lookup resolves 90%+ of requests instantly. + +- **Tier 2 — Regex Routes (O(r))**: Patterns containing regex + metacharacters are tested only against other regex routes, not the + entire table. + +- **Tier 3 — Global Rules (O(1))**: If no route matches, a final + `{:global,` `verb}` lookup catches global verb defaults. + +For a 1000-route policy, this reduces from ~1000 regex evaluations per +request to 1 hash lookup (typical case) or ~50 regex evaluations (edge +case). + +Performance-oriented design is present, but the benchmark story is not +yet strong enough to advertise hard numbers as release evidence. A +performance test file exists; benchmarking and concurrency validation +are still tracked as open work in `ROADMAP.adoc` and `TEST-NEEDS.md`. + +## Security Headers + +All responses (including health/metrics endpoints) include +OWASP-recommended security headers: + +- `X-Content-Type-Options:` `nosniff` — prevent MIME-type sniffing + +- `X-Frame-Options:` `DENY` — prevent clickjacking + +- `Referrer-Policy:` `strict-origin-when-cross-origin` — limit referrer + leakage + +- `Cache-Control:` `no-store,` `no-cache,` `must-revalidate` — prevent + caching of policy decisions + +- `Connection:` `close` — prevent connection reuse across trust + boundaries + +# Project Structure + + http-capability-gateway/ + ├── lib/ + │ └── http_capability_gateway/ + │ ├── application.ex # OTP application + │ ├── gateway.ex # HTTP gateway (Plug.Router) + │ ├── proxy.ex # Backend proxy (Req) + │ ├── policy_loader.ex # YAML policy loading + │ ├── policy_validator.ex # DSL v1 validation + │ ├── policy_compiler.ex # ETS compilation + │ ├── logging.ex # Structured logging + │ └── log_formatter.ex # JSON log formatter + ├── test/ # Current automated tests + ├── config/ # Elixir config files and default policy + ├── examples/ # Example policies + └── docs/ # API documentation + +# Roadmap + +Use `ROADMAP.adoc` as the current roadmap. The short version: + +- core policy pipeline exists + +- tests exist but are not yet strong enough for broad production claims + +- the recommended near-term role is route-scoped API prefiltering, not + full-site gateway responsibility + +- benchmark, E2E, and security-hardening work remain open + +## Phase 2 + +- Rate limits + +- Expanded stealth profiles + +- Hot policy reloads + +## Phase 3 + +- Control plane + +- VeriSimDB integration for provenance + +- Distributed gateways + +## Phase 4 + +- Agent introspection + +- Constraint engine + +- Formal proofs + +# Contributing + +Contributions welcome! Please: + +1. Fork the repository + +2. Create a feature branch + +3. Add tests for new features + +4. Ensure `mix` `test` passes + +5. Submit a pull request + +# Philosophy + +This project treats governance as a first-class engineering concern. +Policies are artefacts. Artefacts are reversible. Decisions have +provenance. HTTP verbs become capabilities, not accidents. + +# License + +This project is licensed under the Mozilla Public License, v. 2.0. See +the `LICENSE` file for details. + +SPDX-License-Identifier: CC-BY-SA-4.0