docs: add phased implementation plan and architecture decision records

Turns the design doc into an ordered, dependency-correct build plan (phases
0-9) with an exit criterion per phase, and records the twelve architectural
decisions it rests on as ADRs.

Decisions: single Rust binary with routed compose services; Streamable HTTP
only; per-agent bearer tokens with clientInfo as a display hint only; SQLite;
get_rules delivered via session gating; full-document rule delivery; project_id
from day one; enforcement tier scoped to Bridle-mediated actions; stable tool
list on upstream failure; OpenAI-compatible embeddings with model/dim guarding;
pattern RAG gated behind a spike; CLAUDE.md + AGENTS.md as v1 renderer targets.

Deviates from the design doc's original 1-8 ordering by moving the audit log
and multi-project schema into phase 1, building the admin API incrementally
rather than all at the Web Panel phase, downgrading vendor guardrail sourcing
to manual-first, and gating the pattern-example RAG behind a validation spike.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019RZerbsHGF9Ka3bKhCjJ9m
This commit is contained in:
2026-08-08 14:33:27 -05:00
parent 7fdde56560
commit 06917d43e2
13 changed files with 556 additions and 0 deletions

View File

@@ -0,0 +1,32 @@
# ADR-0001: Single Rust binary with routed compose services
**Status:** Accepted (2026-08-08)
## Context
The design doc's architecture diagram shows Rules, Memory, the Kanboard wrapper, and the Web Panel
as separate boxes, which reads as four independent deployables communicating over HTTP. Bridle
targets single-host, single-user deployment. Splitting hand-written components across processes at
that scale buys nothing: it costs four ports, four health checks, four failure modes, and turns
what would be function calls into network hops.
## Decision
All hand-written components compile into **one Rust binary** with internal modules: `mcp` (server
and session handling), `rules`, `memory`, `proxy`, `admin`, and the Web Panel's served assets.
Module boundaries stay clean enough that any module could be split into its own service later
without redesign.
Third-party and off-the-shelf services remain **separate Docker Compose containers** — Qdrant,
Kanboard, the embedding server, and the existing Gitea MCP server — with all agent traffic to them
routed through the binary.
## Consequences
- One deployable, one health check, one log stream, one thing to restart.
- No Python sidecar: the memory module is Rust talking to Qdrant and to an OpenAI-compatible
embeddings endpoint over HTTP.
- Compose profiles for bundled-versus-external services are unaffected — see the deployment section
of the design doc.
- If Bridle ever outgrows single-host, the module boundaries are the seams to split along. This is
a deliberate deferral, not an oversight.

View File

@@ -0,0 +1,24 @@
# ADR-0002: Streamable HTTP as the sole MCP transport
**Status:** Accepted (2026-08-08)
## Context
MCP defines stdio and Streamable HTTP transports; an older HTTP+SSE transport is deprecated in
favor of Streamable HTTP. Bridle is a networked gateway intended to be reachable from agents over
WireGuard, which rules out stdio as the primary transport. A local stdio shim bridging to the HTTP
endpoint was considered for clients with weak Streamable HTTP support, as was continuing to serve
the deprecated SSE transport.
## Decision
Serve **Streamable HTTP only**, at `/mcp`. No stdio shim and no deprecated SSE endpoint at v1.
## Consequences
- Matches the per-agent bearer token model (ADR-0003) directly — the token rides the
`Authorization` header.
- Works over WireGuard without extra components.
- One transport to implement, test, and secure.
- If a target agent turns out to have unusable Streamable HTTP support, a `bridle-stdio` bridging
binary is the fallback. It is not in scope until a specific agent demonstrably needs it.

View File

@@ -0,0 +1,32 @@
# ADR-0003: Per-agent bearer tokens for identity and authorization
**Status:** Accepted (2026-08-08)
## Context
The design doc proposed identifying the connecting agent via "MCP handshake client info". The
`clientInfo` object in the MCP `initialize` request is **self-reported by the client** and trivially
forged. Hanging the permissions model off it — including `secrets: {read: false}` — would make that
model decorative rather than enforcing. This matters more once WireGuard or Pangolin place the
endpoint on a network beyond the operator's full control.
Alternatives considered: mTLS client certificates (stronger binding, but meaningful setup friction
per agent and per machine) and full OAuth 2.1 per the MCP authorization spec (standards-correct and
the right answer for public exposure, but an entire authorization server to build and operate for a
single-user homelab).
## Decision
Each agent identity is issued a **Bridle bearer token**. Identity and permissions derive from the
token presented on the connection. `clientInfo` is retained as a **display hint only** — surfaced in
the audit log and Web Panel, never consulted for an authorization decision.
Tokens are issued, rotated, and revoked through the admin API and Web Panel.
## Consequences
- Simple, revocable, and sufficient behind WireGuard.
- Forging `clientInfo` gains an attacker nothing.
- Token issuance/rotation/revocation is phase-1 scope.
- If Bridle is ever exposed to clients the operator does not control, revisit in favor of OAuth 2.1
per the MCP authorization spec.

View File

@@ -0,0 +1,29 @@
# ADR-0004: SQLite as the primary datastore
**Status:** Accepted (2026-08-08)
## Context
The design doc specified no storage backend for the canonical rules document, permissions, audit
log, sessions, secret metadata, or the approval queue. Options considered were SQLite, a bundled
Postgres, and git-backed YAML in a Gitea repository with a database for everything else.
The git-backed option was genuinely attractive in a Gitea shop — free versioning, diffs, and
PR-based review of guardrail changes. It was rejected because Web Panel edits and direct git edits
create two write paths to the same canonical data that must then be reconciled.
## Decision
**SQLite**, one file (`bridle.db`), holding `projects`, `agents`, `tokens`, `permissions`,
`audit_log`, `rules_documents` (JSON plus `schema_version`), `guardrails` with provenance, and
`approval_queue`.
## Consequences
- Zero operational overhead; backup is copying one file.
- Correct default for the single-host, single-user target.
- Rules versioning must be built explicitly (document history in-table) rather than inherited from
git.
- Migration to Postgres remains straightforward if multi-host or a multi-user panel ever
materializes. Keep SQL portable and avoid SQLite-specific constructs where a standard equivalent
exists.

View File

@@ -0,0 +1,36 @@
# ADR-0005: Rules delivered via `get_rules`, enforced by session gating
**Status:** Accepted (2026-08-08)
## Context
Bridle's premise is that the canonical ruleset governs every agent session. But an MCP server can
only *offer* a `get_rules` tool — it cannot make a model call it. Meanwhile Claude Code auto-loads
`CLAUDE.md` from disk and Codex auto-loads `AGENTS.md` from disk, unprompted, every session. A tool
the model may skip is strictly weaker than a file the harness injects.
Three delivery mechanisms were considered:
1. A local sync client (`bridle sync` or a session-start hook) writing rendered files to disk —
reliable across agents today, but adds a client-side component the design doc does not have.
2. The `instructions` field of the MCP `initialize` result — no client install, but whether a given
client injects it into the system prompt varies and would need per-agent empirical testing.
3. The `get_rules` tool alone — matches the design doc, but leaves delivery best-effort.
## Decision
Deliver rules through the **`get_rules(project_id, agent_type)` tool**, and make it reliable by
**gating the session**: any other tool call before `get_rules` has succeeded returns a structured
error naming the required call. Per-session state tracks whether rules have been fetched.
## Consequences
- Delivery becomes effectively guaranteed using only the in-band MCP mechanism — no client-side
component to install or maintain per agent.
- Costs one extra round-trip at the start of each session.
- The error message is part of the product surface and must be unambiguous enough that an agent
recovers on the first try.
- Populating the `initialize` `instructions` field remains available later as belt-and-braces. It
reuses the same renderer, requires no client install, and does not change this architecture.
- Session state now matters: the gate flag must be scoped to the MCP session, and session loss
means re-fetching.

View File

@@ -0,0 +1,26 @@
# ADR-0006: Full-document rule delivery, no dynamic subsetting
**Status:** Accepted (2026-08-08) — carried forward from the original design doc
## Context
An obvious token-saving optimization is to send an agent only the slice of the ruleset relevant to
its current task or file path. This was considered and rejected during design.
## Decision
The rendered rules document is **always sent in full**. No path-based or task-based filtering of
what an agent receives.
How much leash a given model needs to reliably stay on task is a **per-model concern handled by the
renderer** (ADR-0012), not a universal token-saving optimization applied to everyone. Some
models need a fuller, more complete document than others.
## Consequences
- Predictable behavior: an agent's context either contains the whole policy or the session is
gated (ADR-0005).
- Higher token cost per session, accepted deliberately.
- Renderer verbosity becomes the tuning knob for per-model needs.
- Note that this reasoning applies to **rules**, not to **tools**. Tool schemas are a separate
context budget and *are* filtered per agent, driven by the permissions table (see phase 3).

View File

@@ -0,0 +1,23 @@
# ADR-0007: `project_id` in the data model from day one
**Status:** Accepted (2026-08-08)
## Context
The design doc placed multi-project support in the final build phase. But `get_rules(project_id,
agent_type)` already carries a project identifier in its phase-1 signature, and retrofitting
multi-tenancy after five subsystems exist means migrating rules, audit, memory collections,
permissions, and the panel simultaneously.
## Decision
Every table and every tool signature carries **`project_id` from phase 1**. Memory collections are
namespaced per project (`<project>_sessions`). The Web Panel and CLI default to a single project so
the v1 user experience is unchanged.
## Consequences
- Near-zero upfront cost — the identifier was already in the API design.
- Phase 9 becomes exposing a project switcher rather than performing a cross-subsystem migration.
- Every query must be project-scoped from the start; a missing `WHERE project_id = ?` is a
correctness bug, so this belongs in the review checklist.

View File

@@ -0,0 +1,35 @@
# ADR-0008: `enforcement` severity scoped to Bridle-mediated actions
**Status:** Accepted (2026-08-08)
## Context
The design doc defines an `enforcement` guardrail tier where "the gateway itself gates the action
rather than relying on the agent to self-report". As written this implies more than the
architecture can deliver: the gateway can only gate actions that route **through** the gateway. Any
agent with shell access can `git push` directly, `curl` the Kanboard API, or read a token off disk,
and Bridle will never see it.
Adding optional per-agent interceptors (for example Claude Code `PreToolUse` hooks) that consult
Bridle before the agent's own tools run would close the bypass, but is agent-specific, brittle
across vendor updates, and requires a client-side install per agent.
## Decision
Redefine the tier honestly: **`enforcement` means the gateway refuses the action when that action
is Bridle-mediated.** The bypass is documented as a known limit, in the user-facing documentation
and not only here.
Covered: Gitea, Kanboard, secrets, and rules approval accessed through Bridle.
Not covered: the agent's own shell, `curl`, or `git`.
Behavioral enforcement via agent-side hooks is explicitly out of scope for 1.0.
## Consequences
- The tier is accurate rather than aspirational, and ships in phase 1.
- No per-agent brittleness and no client-side component.
- Users must understand that Bridle constrains what flows through it, not what an agent can
physically do. Under-communicating this would be worse than not having the tier.
- Genuine behavioral enforcement, if ever wanted, needs agent-side hooks or network egress control
— a separate effort.

View File

@@ -0,0 +1,30 @@
# ADR-0009: Stable tool list, structured errors on upstream failure
**Status:** Accepted (2026-08-08)
## Context
Bridle proxies several upstreams — the Gitea MCP server, Kanboard, Qdrant — any of which can be
down while the others are healthy. The design doc did not specify what a connected agent sees in
that case. Options: keep the tool list stable and error on call; health-check upstreams and omit
their tools from `tools/list`, notifying via `list_changed`; or refuse to serve at all unless every
upstream is healthy.
Hiding tools avoids the agent calling something dead, but mutates the tool list mid-session, which
some MCP clients cache or otherwise handle badly. Failing the whole session lets one flaky
container block unrelated work.
## Decision
The **tool list stays stable** for the whole session and always reflects the agent's permissions,
not upstream liveness. A call to a downed upstream returns a **clear structured error** naming the
upstream and the failure mode.
## Consequences
- No mid-session tool-list churn, so no dependence on client `list_changed` handling.
- A Qdrant outage does not block Gitea or Kanboard work — failure is isolated per upstream.
- Agents will occasionally call a tool that cannot currently succeed. The error must be specific
enough that the agent reports the real cause rather than inventing a workaround.
- Upstream health is still tracked and surfaced in the Web Panel; it just does not alter the
advertised tool list.

View File

@@ -0,0 +1,29 @@
# ADR-0010: Generic OpenAI-compatible embeddings with model/dimension guarding
**Status:** Accepted (2026-08-08)
## Context
The design doc specified embeddings generated locally via LM Studio on an RTX 3060. Hard-coding
that makes the entire memory subsystem depend on one service and one GPU box being up. Separately,
the choice of embedding model **pins the vector dimensionality** of a Qdrant collection — changing
models silently corrupts a collection or forces a full reindex, and neither should happen by
accident.
## Decision
Target a **generic OpenAI-compatible embeddings endpoint**, configured by `EMBEDDING_URL`,
`EMBEDDING_MODEL`, and `EMBEDDING_DIM`. LM Studio, Ollama, and text-embeddings-inference all
satisfy this.
Record `{model, dim, created_at}` in each Qdrant collection's metadata. On any write, compare the
configured model and dimension against the collection's recorded values; on mismatch, **refuse the
write** and flag that a reindex is required.
## Consequences
- The 3060 is no longer a hard single point of failure; users may point at whatever they run.
- Changing embedding models becomes a loud, explicit reindex rather than silent corruption.
- Slightly more configuration surface than hard-coding LM Studio.
- Bundling a small CPU embedding container as a compose profile default remains available later to
improve the out-of-box experience for new adopters.

View File

@@ -0,0 +1,28 @@
# ADR-0011: Pattern-example RAG gated behind a validation spike
**Status:** Accepted (2026-08-08)
## Context
The pattern-example RAG pipeline — clone, chunk, embed, tag, search, refresh — is the largest
single body of work in the design doc, and it rests on an unvalidated hypothesis: that semantically
retrieved chunks from an example repository measurably improve an agent's output. Naive chunking
of source code is known to produce poor retrieval, so doing it well requires AST-aware chunking via
tree-sitter, which is most of the cost.
## Decision
Insert **phase 5.5, a throwaway spike**, before committing to phase 6. Index one real example repo
with naive chunking, run roughly ten realistic queries, and judge whether the retrieved chunks
would actually have improved an agent's output. The spike code is discarded regardless of outcome.
Phase 6 proceeds only if the spike passes. Its deliverable is a written go/no-go recommendation
with the queries and retrieved chunks as evidence.
## Consequences
- A large speculative build is de-risked for a few days of throwaway work.
- If the spike fails, the `patterns:` section of the canonical document still exists — names,
descriptions, and repository URLs — just without indexed, searchable example code.
- If it passes, the spike's queries become the seed of phase 6's evaluation set.
- Phase 6's size estimate stays honest rather than being quietly discovered mid-build.

View File

@@ -0,0 +1,28 @@
# ADR-0012: `CLAUDE.md` and `AGENTS.md` as the v1 renderer targets
**Status:** Accepted (2026-08-08)
## Context
The design doc names four agent targets: Claude, Codex, Kimi, and DeepSeek. Phase 2 exists to prove
the single-source-of-truth thesis — that one canonical document renders correctly into genuinely
different agent formats. Shipping only one renderer would not prove it, because nothing would force
the canonical schema to stay format-neutral; it would quietly shape itself around the single target.
Shipping all four means four renderers and four golden-test suites before the thesis is validated
even once.
## Decision
Ship **`CLAUDE.md` (Claude conventions) and `AGENTS.md` (Codex)** at 1.0. Kimi and DeepSeek land
post-1.0 as `AGENTS.md` dialects carrying the more explicit, step-by-step phrasing the design doc
calls for on weaker instruction-followers.
## Consequences
- Two formats different enough to keep the canonical schema honestly format-neutral.
- Golden-file tests cover both from phase 2; renderer correctness is that phase's actual
deliverable.
- The renderer must be structured so a dialect (same base format, different phrasing verbosity) is
a cheap addition rather than a fork.
- `agent_profiles` entries for Kimi and DeepSeek may exist in the canonical schema before their
renderers do.