Skip to main content
Glama
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.*