This file provides AI coding agents with the essential project context needed to implement any feature autonomously. Read this once before starting work. For per-feature details, see the Feature Implementation Brief in the issue body.
Fairvisor Edge is an inline HTTP enforcement layer built on OpenResty / LuaJIT 2.1+. It makes allow/reject decisions on every request with sub-millisecond latency. It is NOT a proxy, NOT a gateway, NOT a WAF — it is a policy enforcement point.
- Only runtime language: Lua (LuaJIT 2.1+ on OpenResty)
- Runs as an nginx module inside nginx phases:
init_worker,access,header_filter,body_filter,log - Performance-critical paths MAY use LuaJIT FFI for C libraries (only when profiling proves necessity)
- 2-space indentation
snake_casefor variables, functions, file namesUPPER_CASEfor module-level constantslocaleverything — no globals exceptngx- One
returnat module bottom exporting the public API _prefix for private/internal functions (MUST NOT be called from outside the module)- Comments: explain why, not what. No commented-out code.
- Return
nil, error_messagefrom functions — nevererror()in production code - Log errors with context: include
module,function, and relevant IDs - Reserve
error()for programming bugs / assertions during development only
bin/
fairvisor -- CLI entrypoint (requires OpenResty resty in PATH)
cli/
main.lua -- command dispatch
commands/ -- init, validate, test, connect, status, logs, version, help
lib/ -- args, output
templates/ -- api.json, llm.json, webhook.json
src/
fairvisor/
descriptor.lua -- 001
bundle_loader.lua -- 002
...
spec/
unit/
<module>_spec.lua -- one per module
cli/ -- help_spec, version_spec, init_spec, ...
integration/
helpers/
mock_ngx.lua
tests/
e2e/
docker-compose.test.yml
CLI runs as Lua via OpenResty's resty; it reuses fairvisor.* modules for validate/test (no second language).
These MUST be respected in every module:
- Zero allocations on hot path — no
string.format, no table creation, no closures, no JSON per request - No I/O in the decision path — no disk, no network, no DNS
- No regex on hot path — use radix trie for routes, direct table lookups for claims
shared_dictbudget: at most1 get + 1 setper enforced token bucket per request- Prepare at load, execute at request — build data structures during
init_workeror hot-reload
| Component | p50 | p99 max |
|---|---|---|
| Route/policy selection (LRU hit) | < 5 µs | — |
| Route/policy selection (LRU miss) | < 50 µs | — |
| Descriptor extraction (≤3 keys) | < 20 µs | — |
| Token bucket (low contention) | < 50 µs | < 500 µs |
| Full rule evaluation (≤10 policies × 10 rules) | < 100 µs | < 500 µs |
| Total per-request | < 100 µs | < 1 ms |
bundle_loader → route_index, policy_store
decision_api → rule_engine
rule_engine → descriptor_extraction
→ route_matching
→ token_bucket / cost_budget / llm_limiter
→ loop_detection
→ circuit_breaker
→ kill_switch
→ shadow_mode
- One module, one responsibility — each feature spec (001–018) maps to one Lua module
- No circular dependencies — if A requires B, B MUST NOT require A
- No relative imports — all modules loaded via
require("fairvisor.<module>") - Interfaces over internals — expose public API per spec;
_prefix for private functions
- "All must pass" — a request must pass ALL rules in ALL matching policies. First REJECT stops evaluation.
- Deterministic — same input → same output. No randomness, no jitter.
- Fail-open for missing descriptors — allow request, log
reason=descriptor_missing, increment metric. - Kill-switch before rules — always runs first; if matched, REJECT with
reason: "kill_switch". - Shadow mode is transparent — logs decision but always returns ALLOW.
- busted for Lua tests (BDD style)
- All tests MUST be Gherkin-first: express scenarios in plain English using
Feature/Rule/Scenario/Given/When/Then/And - pytest for e2e tests; E2E scenarios MUST also be Gherkin-first in plain English
- Test-to-code ratio: > 2:1 (lines of test ≥ 2× lines of production code)
| Layer | Location | What | Runs without nginx? |
|---|---|---|---|
| Unit | spec/unit/ |
Every module in isolation, mock ngx APIs | Yes |
| Integration | spec/integration/ |
Module interactions, realistic bundles | Yes |
| E2E | tests/e2e/ |
Real OpenResty in Docker, real HTTP | No |
Use natural English. Gherkin blocks via the local wrapper are mandatory for test scenarios, including tests/e2e.
Feature files vs spec: Scenarios live in spec/<layer>/features/<module>.feature; step definitions and harness in spec/<layer>/<module>_spec.lua. The spec loads the feature with runner:feature_file_relative("features/<module>.feature"). See spec/README.md for the full layout convention.
Feature: Token bucket enforcement
Rule: Requests cannot exceed burst capacity
Scenario: Request is rejected when bucket is empty
Given the nginx mock environment is reset
When I run one request with key "tb:rule:user-1" and default cost
Then the request is rejected-- GOOD
describe("token bucket rate limiter", function()
context("when the bucket is full", function()
it("allows the request and decrements tokens", function()
-- BAD
describe("tb", function()
it("test1", function()Golden test scenarios (RE-001 through RE-014) are sacred — they MUST ALL pass at every commit.
Critical: agents MUST use these patterns to mock nginx APIs in unit tests.
local function mock_shared_dict()
local data = {}
return {
get = function(_, key)
return data[key]
end,
set = function(_, key, value)
data[key] = value
return true
end,
incr = function(_, key, value, init, init_ttl)
local current = data[key]
if current == nil then
if init then
data[key] = init + value
return data[key], nil, true -- value, err, forcible
end
return nil, "not found"
end
data[key] = current + value
return data[key], nil, false
end,
delete = function(_, key)
data[key] = nil
end,
flush_all = function(_)
data = {}
end,
}
endlocal _mock_time = 1000.000
_G.ngx = _G.ngx or {}
ngx.now = function() return _mock_time end
ngx.update_time = function() end
-- Advance time in tests:
local function advance_time(seconds)
_mock_time = _mock_time + seconds
endngx.shared = ngx.shared or {}
ngx.shared.fairvisor_counters = mock_shared_dict()When implementing a feature whose dependencies are not yet implemented:
- Create a stub module with the interface defined in the dependency's spec
- Place stubs in
src/fairvisor/with the standard module name - The stub MUST implement the public API contract (correct signatures)
- The stub SHOULD return sensible defaults (e.g., extracted descriptors = empty table)
- Mark stub functions with a comment:
-- STUB: replace when <feature> is implemented
Features should be implemented in this order:
| Wave | Features | Dependencies |
|---|---|---|
| 1 | 001, 003, 004, 005, 007, 008, 009, 010, 012 | None (leaf modules) |
| 2 | 002, 013 | 001 |
| 3 | 006 | 001, 003, 004, 005, 007, 008, 009, 010 |
| 4 | 011, 014, 015 | 006 |
| 5 | 016, 017, 018 | 002, 011, 012 |
Within a wave, features can be implemented in parallel.