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:
32
docs/adr/0001-single-binary-with-routed-services.md
Normal file
32
docs/adr/0001-single-binary-with-routed-services.md
Normal 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.
|
||||||
24
docs/adr/0002-streamable-http-only.md
Normal file
24
docs/adr/0002-streamable-http-only.md
Normal 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.
|
||||||
32
docs/adr/0003-per-agent-bearer-tokens.md
Normal file
32
docs/adr/0003-per-agent-bearer-tokens.md
Normal 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.
|
||||||
29
docs/adr/0004-sqlite-datastore.md
Normal file
29
docs/adr/0004-sqlite-datastore.md
Normal 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.
|
||||||
36
docs/adr/0005-get-rules-tool-with-session-gating.md
Normal file
36
docs/adr/0005-get-rules-tool-with-session-gating.md
Normal 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.
|
||||||
26
docs/adr/0006-full-document-rule-delivery.md
Normal file
26
docs/adr/0006-full-document-rule-delivery.md
Normal 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).
|
||||||
23
docs/adr/0007-project-id-from-day-one.md
Normal file
23
docs/adr/0007-project-id-from-day-one.md
Normal 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.
|
||||||
35
docs/adr/0008-enforcement-scoped-to-mediated-actions.md
Normal file
35
docs/adr/0008-enforcement-scoped-to-mediated-actions.md
Normal 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.
|
||||||
30
docs/adr/0009-stable-tool-list-on-upstream-failure.md
Normal file
30
docs/adr/0009-stable-tool-list-on-upstream-failure.md
Normal 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.
|
||||||
29
docs/adr/0010-openai-compatible-embeddings.md
Normal file
29
docs/adr/0010-openai-compatible-embeddings.md
Normal 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.
|
||||||
28
docs/adr/0011-pattern-rag-behind-spike.md
Normal file
28
docs/adr/0011-pattern-rag-behind-spike.md
Normal 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.
|
||||||
28
docs/adr/0012-v1-renderer-targets.md
Normal file
28
docs/adr/0012-v1-renderer-targets.md
Normal 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.
|
||||||
204
docs/implementation-plan.md
Normal file
204
docs/implementation-plan.md
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
# Agentic Bridle — Phased Implementation Plan
|
||||||
|
|
||||||
|
**Status:** approved 2026-08-08 by Colby Whitlock (org lead)
|
||||||
|
**Source design:** [`Agentic Bridle MCP Plan.md`](../Agentic%20Bridle%20MCP%20Plan.md)
|
||||||
|
**Decision records:** [`docs/adr/`](adr/)
|
||||||
|
|
||||||
|
This document turns the design doc into an ordered, dependency-correct build plan. Every
|
||||||
|
architectural decision it depends on is recorded as an ADR; this plan is the schedule, the ADRs are
|
||||||
|
the reasoning.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Locked decisions
|
||||||
|
|
||||||
|
| Area | Decision | ADR |
|
||||||
|
|---|---|---|
|
||||||
|
| Deployment | One Rust binary (gateway, rules, memory, admin API, panel); Qdrant / Kanboard / Gitea MCP / embeddings as separate compose services, all traffic routed through the binary | [0001](adr/0001-single-binary-with-routed-services.md) |
|
||||||
|
| Transport | Streamable HTTP only (`/mcp`) | [0002](adr/0002-streamable-http-only.md) |
|
||||||
|
| Auth | Bridle-issued per-agent bearer tokens; `clientInfo` is a display hint, never a permission source | [0003](adr/0003-per-agent-bearer-tokens.md) |
|
||||||
|
| Storage | SQLite (`bridle.db`) | [0004](adr/0004-sqlite-datastore.md) |
|
||||||
|
| Rule delivery | `get_rules` tool, with hard gating — every other tool errors until `get_rules` succeeds in that session | [0005](adr/0005-get-rules-tool-with-session-gating.md) |
|
||||||
|
| Rule scope | Full rendered document, never a dynamic subset | [0006](adr/0006-full-document-rule-delivery.md) |
|
||||||
|
| Multi-project | `project_id` in schema and tool signatures from day one; single-project UX at 1.0 | [0007](adr/0007-project-id-from-day-one.md) |
|
||||||
|
| `enforcement` tier | Scoped to Bridle-mediated actions; bypass documented as a known limit | [0008](adr/0008-enforcement-scoped-to-mediated-actions.md) |
|
||||||
|
| Degradation | Stable tool list always; downed upstreams return structured errors | [0009](adr/0009-stable-tool-list-on-upstream-failure.md) |
|
||||||
|
| Embeddings | Generic OpenAI-compatible endpoint; model + dimension recorded in collection metadata, mismatched writes refused | [0010](adr/0010-openai-compatible-embeddings.md) |
|
||||||
|
| Pattern RAG | Gated behind a throwaway validation spike | [0011](adr/0011-pattern-rag-behind-spike.md) |
|
||||||
|
| Renderers v1 | `CLAUDE.md` + `AGENTS.md` (Codex); Kimi/DeepSeek dialects post-1.0 | [0012](adr/0012-v1-renderer-targets.md) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 0 — Foundations
|
||||||
|
|
||||||
|
**Size:** small
|
||||||
|
|
||||||
|
Cargo workspace, `docs/adr/` populated, compose skeleton (bridle plus profiles for `with-qdrant`,
|
||||||
|
`with-kanboard`, `with-embeddings`), Gitea CI on pull request, `.env.example`, Docker secrets
|
||||||
|
wiring.
|
||||||
|
|
||||||
|
**Exit criterion:** `docker compose up` yields a health-checked binary that completes an MCP
|
||||||
|
`initialize` over Streamable HTTP and advertises zero tools.
|
||||||
|
|
||||||
|
## Phase 1 — Gateway core
|
||||||
|
|
||||||
|
**Size:** large
|
||||||
|
|
||||||
|
The security and accountability spine. Everything later assumes it exists.
|
||||||
|
|
||||||
|
- Streamable HTTP MCP server; session lifecycle and per-session state, including the
|
||||||
|
*rules-fetched* flag that phase 2's gate depends on.
|
||||||
|
- Token auth → agent identity → permissions lookup. Token issuance, rotation, and revocation
|
||||||
|
through the admin API.
|
||||||
|
- SQLite schema carrying `project_id` throughout: `projects`, `agents`, `tokens`, `permissions`,
|
||||||
|
`audit_log`.
|
||||||
|
- Append-only audit log written from the first request, with source attribution
|
||||||
|
(user / agent / vendor).
|
||||||
|
- Docker-secrets loading, structured logging, per-request timeouts.
|
||||||
|
- Admin API skeleton — the Web Panel's contract starts here so it is not written twice.
|
||||||
|
|
||||||
|
**Exit criterion:** two agents connect under distinct authenticated identities with different
|
||||||
|
permission sets; a forged `clientInfo` changes nothing about what either may do; every call appears
|
||||||
|
in the audit log correctly attributed.
|
||||||
|
|
||||||
|
## Phase 2 — Rules server, renderers, gating
|
||||||
|
|
||||||
|
**Size:** large
|
||||||
|
|
||||||
|
The core thesis of the project.
|
||||||
|
|
||||||
|
- Canonical schema v1 with `schema_version` and JSON Schema validation: guardrails, patterns,
|
||||||
|
provenance, agent profiles, permissions.
|
||||||
|
- Storage and full CRUD admin API.
|
||||||
|
- `get_rules(project_id, agent_type)` plus the gate: any other tool call before it returns a
|
||||||
|
structured error naming the required call.
|
||||||
|
- `CLAUDE.md` and `AGENTS.md` renderers under golden-file tests. Renderer correctness is the
|
||||||
|
deliverable of this phase, so it carries real test coverage.
|
||||||
|
- Agent-proposal tools (`propose_guardrail`, `add_pattern`) writing to an approval queue. Nothing
|
||||||
|
auto-applies.
|
||||||
|
|
||||||
|
**Exit criterion:** one edit in Bridle demonstrably changes behavior in a live Claude Code session
|
||||||
|
*and* a live Codex session; a session that skips `get_rules` cannot use any tool.
|
||||||
|
|
||||||
|
## Phase 3 — Upstream proxy and Gitea
|
||||||
|
|
||||||
|
**Size:** medium
|
||||||
|
|
||||||
|
- Generic upstream-MCP client with connection pooling and health tracking.
|
||||||
|
- Tool namespacing, stable across restarts (see open item 1).
|
||||||
|
- Per-agent tool filtering driven by the phase-1 permissions table.
|
||||||
|
- Credential injection per request; upstreams never hold their own tokens.
|
||||||
|
- Failure isolation per [ADR-0009](adr/0009-stable-tool-list-on-upstream-failure.md).
|
||||||
|
|
||||||
|
**Exit criterion:** an agent completes a full Gitea PR workflow through Bridle while holding no
|
||||||
|
Gitea token itself; killing the Gitea MCP container degrades only Gitea tools.
|
||||||
|
|
||||||
|
## Phase 4 — Kanboard wrapper
|
||||||
|
|
||||||
|
**Size:** medium
|
||||||
|
|
||||||
|
Thin REST-to-MCP translation inside the binary. Validates both the author-our-own-upstream path and
|
||||||
|
the bundled-versus-external compose story.
|
||||||
|
|
||||||
|
**Exit criterion:** task CRUD from an agent against a bundled Kanboard and against an external one,
|
||||||
|
switched by environment variable alone.
|
||||||
|
|
||||||
|
## Phase 5 — Session memory
|
||||||
|
|
||||||
|
**Size:** medium
|
||||||
|
|
||||||
|
Qdrant integration, pluggable embeddings with model/dimension guarding, `<project>_sessions`
|
||||||
|
collections, `remember` and `recall` tools, retention and purge.
|
||||||
|
|
||||||
|
**Exit criterion:** a decision recorded by Claude Code is recalled by Codex in a later session;
|
||||||
|
swapping embedding models is detected and refused rather than silently corrupting the collection.
|
||||||
|
|
||||||
|
## Phase 5.5 — Pattern RAG spike
|
||||||
|
|
||||||
|
**Size:** small, throwaway
|
||||||
|
|
||||||
|
Index one real example repo with naive chunking, run roughly ten realistic queries, and judge
|
||||||
|
whether the retrieved chunks would actually improve an agent's output. This is an explicit go/no-go
|
||||||
|
gate on phase 6 — the spike code is discarded either way.
|
||||||
|
|
||||||
|
**Exit criterion:** a written go/no-go recommendation with the queries and retrieved chunks as
|
||||||
|
evidence.
|
||||||
|
|
||||||
|
## Phase 6 — Pattern-example RAG
|
||||||
|
|
||||||
|
**Size:** large — **conditional on phase 5.5 passing**
|
||||||
|
|
||||||
|
Repo fetch → tree-sitter AST-aware chunking → tagged upsert; `search_pattern_examples(query,
|
||||||
|
arch_tag?)`; `refresh_pattern_example(reference_tag)` with clean replacement of prior vectors.
|
||||||
|
|
||||||
|
**Exit criterion:** a named pattern's example repo is indexed and returns relevant code for a
|
||||||
|
natural-language query; a refresh fully replaces prior vectors for that tag.
|
||||||
|
|
||||||
|
## Phase 7 — Web Panel
|
||||||
|
|
||||||
|
**Size:** large
|
||||||
|
|
||||||
|
Served by the same binary. The admin API already exists from phases 1–6, so this phase is a client
|
||||||
|
against it: approval queue, rules editor, secrets (write, rotate, revoke — never display), audit
|
||||||
|
browser, memory browser, integration status.
|
||||||
|
|
||||||
|
**Exit criterion:** every phase 1–6 operation is achievable through the UI with no CLI or database
|
||||||
|
access.
|
||||||
|
|
||||||
|
## Phase 8 — Guardrail sourcing
|
||||||
|
|
||||||
|
**Size:** small
|
||||||
|
|
||||||
|
Manual paste-a-document flow producing proposed guardrails into the existing approval queue.
|
||||||
|
Automated vendor-doc fetching only if the manual workflow proves its worth.
|
||||||
|
|
||||||
|
**Exit criterion:** a vendor guidance document becomes a reviewable proposal that requires explicit
|
||||||
|
approval before entering the canonical ruleset.
|
||||||
|
|
||||||
|
## Phase 9 — Multi-project, hardening, 1.0
|
||||||
|
|
||||||
|
**Size:** medium
|
||||||
|
|
||||||
|
Project switcher on the day-one schema, backup and restore, install documentation, packaging,
|
||||||
|
release.
|
||||||
|
|
||||||
|
**Exit criterion:** a second person can stand up Bridle from the README against their own Gitea and
|
||||||
|
reach a working agent session.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cross-cutting
|
||||||
|
|
||||||
|
- Security review on every phase that touches authentication or secrets.
|
||||||
|
- Documentation written as features land, not retrofitted.
|
||||||
|
- One ADR per architectural decision.
|
||||||
|
- Golden-file and integration tests from phase 2 onward.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deviations from the design doc's original 1–8 ordering
|
||||||
|
|
||||||
|
- **Audit log and multi-project schema moved to phase 1.** Both are cheap to add upfront and
|
||||||
|
expensive to retrofit across five subsystems.
|
||||||
|
- **Web Panel moved after memory,** but its admin API is built incrementally from phase 1 rather
|
||||||
|
than all at the end.
|
||||||
|
- **Vendor guardrail sourcing downgraded to manual-first.** Scraping vendor documentation is
|
||||||
|
fragile; the manual path captures most of the value at a fraction of the cost.
|
||||||
|
- **Pattern-example RAG gated behind a spike** rather than committed to outright.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Open items
|
||||||
|
|
||||||
|
Not blocking phase 0. Sensible defaults noted where they exist.
|
||||||
|
|
||||||
|
1. **Tool namespacing convention.** Proposed default: `gitea__create_pr` (double underscore).
|
||||||
|
Effectively permanent once agents start depending on it.
|
||||||
|
2. **Audit log integrity.** Plain append-only table, or hash-chained and tamper-evident?
|
||||||
|
3. **Approval queue notifications.** Panel-only, or out-of-band webhook/email when an agent
|
||||||
|
proposes a change?
|
||||||
|
4. **Web Panel stack.** Server-rendered Rust templates versus an embedded SPA. Not blocking until
|
||||||
|
phase 7.
|
||||||
|
5. **Kanboard instance and auth model.** API user versus application token.
|
||||||
|
6. **CI.** Gitea runner availability, and whether PR-gated CI starts at phase 0.
|
||||||
|
7. **Repository visibility** and whether GPLv3 remains the license.
|
||||||
Reference in New Issue
Block a user