brackenedge
by J-X0
README.md
# brackenedge
Edge inference for pharmaceutical supply disposition, delivered as an MCP
server. Given the sensor and handling data for a shipment leg, it returns one of
three dispositions -- `release`, `review`, or `quarantine` -- and records every
decision in a tamper-evident audit trail.
Two design constraints shape everything here:
- **Graceful degradation.** Decisions run against an edge model when it is
reachable and confident. When the model is down, errors out, or answers with
low confidence, the engine falls back to a documented rule set instead of
failing the decision. The provenance of every decision (`model` vs
`heuristic`) and the reason for any fallback are recorded.
- **A reviewable audit trail.** Every decision writes exactly one record to a
hash-chained log before the result is returned. Editing or removing any past
record breaks the chain, so an auditor can prove the trail is complete and
unaltered. See `brackenedge/audit.py`.
## The problem
A pharmaceutical distributor receiving shipments at warehouse docks needs a
disposition (release, hold for review, or quarantine) for each arriving leg,
computed at the dock rather than in a cloud round trip. Two things make this
harder than a model call. The edge box's model may be down, slow, or
uninstalled, and a stalled dock is a business problem — so the decision must
still be made. And because these are release decisions for regulated product,
every one has to be defensible to an auditor months later: what was decided,
why, and whether a human or a model made the call.
## Architecture
The decision path is deliberately layered so the two constraints are structural,
not bolted on:
```
features ──> InferenceEngine.decide ──> Decision
│
├─ Provider.available()? no ─┐
├─ Provider.infer() error ──┤
├─ confidence < threshold ───┤
│ ▼
│ heuristic.evaluate (Provenance.HEURISTIC)
│ otherwise │
└─ model disposition ─────────┤ (Provenance.MODEL)
▼
AuditLog.append (hash-chained, one per decision)
```
- The engine talks only to the `Provider` interface. The model backend
(`providers/real.py`, HTTP) and the deterministic test stub
(`providers/stub.py`) are interchangeable, which is what lets the whole suite
run offline and makes degradation a property of the interface rather than a
special case.
- The engine, not the provider, owns the degradation policy and the audit write.
There is no path to a `Decision` that skips the audit record — that is
enforced in one place, `InferenceEngine.decide`.
- The audit log is a SHA-256 hash chain: each record hashes its own contents
plus the previous record's hash, so a removed or edited record is detectable.
The contested design calls (why a hash chain, why degrade instead of fail
closed, why a provider interface, where the confidence policy lives) are
recorded in `docs/adr/`.
## Layout
```
brackenedge/
domain.py # ShipmentFeatures, Decision, enums (validated, inert)
heuristic.py # rule-based fallback with documented thresholds
engine.py # InferenceEngine: the degradation algorithm + audit write
audit.py # hash-chained, tamper-evident AuditLog
config.py # env-driven EngineConfig + engine factory (validated)
logging_setup.py # structured JSON logging
cli.py # batch-scoring / audit-review command line
server.py # MCP server exposing the engine as tools
providers/
base.py # Provider interface + output/error types
stub.py # deterministic, offline provider used by tests
real.py # HTTP provider for a real edge endpoint (stdlib only)
tests/
```
## Install and test
```
make venv
make install
make test
```
`make venv` needs the `python3.12-venv` package present. If you cannot create a
venv, the core and its tests depend only on the standard library plus pytest, so
this also works:
```
python3.12 -m pip install pytest
python3.12 -m pytest
```
The test suite runs offline with no API key: it uses `StubProvider`, and
`tests/test_offline.py` asserts that a decision opens no socket. Override the
interpreter with `make test PY=/path/to/python` if you are not using `.venv`.
## Running the server
```
make serve
```
`serve` runs `python -m brackenedge.server`. With no configuration it uses the
deterministic stub provider so the server starts and degrades sensibly even
with no model deployed. Configuration is via environment variables:
| Variable | Effect |
| --- | --- |
| `BRACKENEDGE_MODEL_ENDPOINT` | Base URL of an HTTP edge model. When set, the real provider is used; otherwise the stub. |
| `BRACKENEDGE_MODEL_NAME` | Name recorded in the audit trail for the model. |
| `BRACKENEDGE_AUDIT_PATH` | File to append audit records to as JSONL. Reloaded and re-verified on startup. |
Two more variables tune behaviour: `BRACKENEDGE_CONFIDENCE_THRESHOLD` (default
0.5), `BRACKENEDGE_MODEL_TIMEOUT_S` (default 2.0), `BRACKENEDGE_MAX_BATCH`
(default 1000), and `BRACKENEDGE_LOG_LEVEL` (default INFO). Invalid values fail
at startup with a clear message.
The server exposes three MCP tools: `decide_shipment`, `verify_audit`, and
`audit_tail`.
## Command line
The same engine is available as a CLI for batch scoring and audit review:
```
# score one shipment from stdin
echo '{"shipment_id":"S1","product_class":"cold_chain","max_temp_excursion_c":1.0,"excursion_minutes":60}' \
| python -m brackenedge.cli decide
# score a batch from a file
python -m brackenedge.cli decide --input shipments.json
# review the audit trail (requires BRACKENEDGE_AUDIT_PATH)
python -m brackenedge.cli verify
python -m brackenedge.cli tail --limit 20
```
Input is a JSON object or an array of objects using `ShipmentFeatures` keys.
Exit codes: `0` success, `2` configuration error, `3` input error (bad file,
bad JSON, invalid features, batch over `BRACKENEDGE_MAX_BATCH`), `4` audit
verification failed. Decisions and errors are logged as structured JSON to
stderr, including measured per-decision latency.
### Expected model endpoint contract
When `BRACKENEDGE_MODEL_ENDPOINT` is set, the real provider expects:
- `GET {endpoint}/health` -> 2xx when the model is ready.
- `POST {endpoint}/infer` with `{"features": {...}}` -> JSON
`{"disposition": "...", "risk_score": 0..1, "confidence": 0..1, "rationale": "..."}`.
Anything else (connection refused, timeout, non-2xx, unparseable body) causes a
graceful fallback to the heuristic, recorded with its reason.
## The heuristic
The fallback is intentionally conservative -- in a pharma release decision, when
in doubt you hold product for a human rather than release it. Rule precedence
(first match decides; every triggered concern is still recorded):
1. Broken seal -> `quarantine`
2. Sustained cold-chain temperature excursion -> `quarantine`
3. Large ambient excursion -> `review`
4. Excessive transit delay -> `review`
5. Otherwise -> `release`
## Known limitations
- The heuristic thresholds (`brackenedge/heuristic.py`) are defensible defaults,
not validated SOP values. Wiring in a real, versioned SOP table is deferred;
the constants are grouped at the top of the module for that reason.
- The confidence threshold (default 0.5) is a starting policy value, not tuned
against labelled outcomes.
- `real.py` targets a simple HTTP endpoint; batching and streaming are out of
scope for this milestone.
---
*Glasshouse Data is an illustrative client; this repository is a self-directed reference implementation built to work end to end.*
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessNo issues