Calibrated to what TangleBrain is: a published, single-operator local tool that handles credentials and can spend real money.
Each requirement below states how it is verified, because an NFR nobody can check is a wish.
These bind. Departing from one is a decision to record and justify.
-
The default install costs $0 and reaches nothing off-machine. A fresh install's packaged roster has one active entry — the free local tier.
Why: this is the product's premise, not a performance target. "Local-first" is the claim the README makes and the reason someone chooses this over a hosted router; a default that contacted a paid or remote service would make the claim false. Any change here is a breaking change to what TangleBrain is, not a tuning decision.
-
Degrade, don't fail. Every dependency is assumed unavailable sometimes; the design question is always what happens next, and the answer is never "raise and stop" while another path exists.
Why: a router's whole value is having somewhere else to send the work. Failing on the first backend error would make it strictly worse than calling the backend directly. This is what the failover ladder, the classifier's fail-to-frontier direction, and per-item batch status all implement.
The system degrades correctly but records nothing when it does, so failover is currently invisible. That is a missing signal, not a violation — #100.
-
A side-effect never breaks the main path. Measurement, logging, and state persistence are all subordinate to returning the answer.
Why: the answer has already been paid for — in local compute, in subscription quota, or in real money. Losing it to a logging bug destroys something valuable to record something incidental. This is the rationale that licenses the codebase's one deliberately broad exception handler (
measurement.py:376-378); the waiver is scoped to this rule and does not generalize.
| Requirement | Target | Verification |
|---|---|---|
| Routing overhead | Negligible against backend latency, which dominates by orders of magnitude | Not measured. Accepted on structural grounds: routing is a few dict lookups and a file read against a multi-second network call. |
delegate_many wall-clock |
Meaningfully better than sequential | ThreadPoolExecutor fan-out; the workload is network-wait, so threads are the right primitive. |
| Streaming latency | First token reaches the client as soon as the backend produces it | The serve endpoint writes one flushed SSE event per delta rather than buffering. |
| Startup | Fast enough to be invisible in a CLI | No heavy imports at module load; MCP is an optional extra so the base install stays thin. |
Non-goal: throughput. There is no request-per-second target because there is one operator. Optimizing for concurrency here would be optimizing for a user who does not exist.
Explicitly bounded, and the bounds are the design:
- One operator, one machine. No multi-tenancy, no shared instance, no horizontal scaling.
- The only concurrency dimension is sub-task fan-out, bounded by
settings.delegate_max_concurrencyor anos.cpu_count()-derived default. A per-call value may lower but never raise it — a model calling the tool cannot talk the system into more parallelism than the operator permitted. - Roster size is expected in the tens. Selection is a linear scan and that is appropriate; anything cleverer would be unjustified.
Known unbounded quantity: usage.jsonl grows forever. No rotation, no cap, no pruning.
Slow-moving for one operator, but it is the one place the system has no scaling story at all —
#101.
There is no uptime target — this is not a service. What replaces it is a specified degradation ladder:
| Failure | Behavior | Verification |
|---|---|---|
| Classifier errors or is ambiguous | Route to frontier | Fail-safe direction: a hard task is never trapped on the local tier. tests/test_classifier.py |
| A backend fails | Fail over to the next orchestrator | tests/test_router.py |
| All orchestrators fail, paid gate off | RouterError listing every failure, rate-limits annotated |
tests/test_router.py |
| All orchestrators fail, paid gate on | Fall through to enabled api entries in roster order; a paid success does not advance the rotation cursor |
tests/test_router.py |
| Measurement raises | Swallowed; answer still returns | measurement.py:376-378, tests/test_measurement.py |
| A usage record is corrupt | Skipped; rollup still produced | tests/test_measurement.py |
| Roster missing | Falls back to the packaged local-only example | tests/test_roster.py |
A delegate_many item fails |
Per-item status; batch completes |
tests/test_delegate.py |
| Rotation cursor lost | Rotation restarts from the beginning | Harmless — a fairness hint, not correctness. |
Hard constraint: the default configuration costs exactly $0 and contacts nothing off-machine.
- Paid backends require two independent gates, both defaulting false, both strictly
bool-validated so a stray
"yes"or1cannot enable billing. apiis never auto-selected by capability — paid is a last resort, never a preference.budget_usd_monthper entry is the operator's declared ceiling.- Verified by
tests/test_selector.pyandtests/test_settings.py.
Honest limit: budget_usd_month is a declaration, not an enforcement — nothing stops spend
when the figure is reached, and --stats reports cloud-equivalent (avoided) cost rather than
actual billed spend. An operator who reads it as a hard cap is misreading it. Worth either enforcing
or renaming; recorded, not decided.
Fully specified in security-model.md. The NFR-level statements:
- No secret is ever persisted, logged, or rendered — only
key_refreferences. - No prompt or response text is ever written to disk.
- No shell is ever invoked;
cmdislist[str]. - Nothing binds off-loopback.
- Python: as declared in
pyproject.tomland enforced by the CI matrix. - Dependencies: deliberately minimal — stdlib GUI and serve, no agent framework. Every dependency avoided is one that cannot break a published package.
- The
mcp < 2cap is an NFR, not an oversight. mcp 2.0.0 removedmcp.server.fastmcp, so an unboundedmcp >= 1.0shipped an extra that could not import.tests/test_packaging.pyencodes that constraint — when #90 lifts the cap, that test gets updated deliberately, never deleted to green a build. - Open risk:
httpx >= 0.27andPyYAML >= 6.0carry the same unbounded shape, and no scheduled CI run exists to catch an upstream break without a push (#92). The v0.20.1 incident is the proof that this class of risk is real here, not theoretical.
- Pure logic is separated from I/O so it is directly testable:
gui/views.pyandserve/views.pyare socket-free, withserver.pywrapping each in a puredispatch(method, path, body)plus aThreadingHTTPServer. - Adding a backend type is contained to one adapter file.
- Adding a backend is a config edit, not a code change — the product's central claim, and the thing to protect in review.
Baseline, proportionate to a localhost knob panel: the primary surface is a CLI (terminal accessibility inherited), and the GUI uses semantic HTML with real labelled form controls — achieved largely by not reaching for custom widgets.
No WCAG conformance is claimed, and claiming one without an audit would be dishonest. Gap: contrast ratios in the panel CSS have never been measured. Cheap to check; not yet checked.