From 06917d43e218a7cf1fa90e514490101389a912f0 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Sat, 8 Aug 2026 14:33:27 -0500 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_019RZerbsHGF9Ka3bKhCjJ9m --- ...0001-single-binary-with-routed-services.md | 32 +++ docs/adr/0002-streamable-http-only.md | 24 +++ docs/adr/0003-per-agent-bearer-tokens.md | 32 +++ docs/adr/0004-sqlite-datastore.md | 29 +++ ...0005-get-rules-tool-with-session-gating.md | 36 ++++ docs/adr/0006-full-document-rule-delivery.md | 26 +++ docs/adr/0007-project-id-from-day-one.md | 23 ++ ...-enforcement-scoped-to-mediated-actions.md | 35 +++ ...09-stable-tool-list-on-upstream-failure.md | 30 +++ docs/adr/0010-openai-compatible-embeddings.md | 29 +++ docs/adr/0011-pattern-rag-behind-spike.md | 28 +++ docs/adr/0012-v1-renderer-targets.md | 28 +++ docs/implementation-plan.md | 204 ++++++++++++++++++ 13 files changed, 556 insertions(+) create mode 100644 docs/adr/0001-single-binary-with-routed-services.md create mode 100644 docs/adr/0002-streamable-http-only.md create mode 100644 docs/adr/0003-per-agent-bearer-tokens.md create mode 100644 docs/adr/0004-sqlite-datastore.md create mode 100644 docs/adr/0005-get-rules-tool-with-session-gating.md create mode 100644 docs/adr/0006-full-document-rule-delivery.md create mode 100644 docs/adr/0007-project-id-from-day-one.md create mode 100644 docs/adr/0008-enforcement-scoped-to-mediated-actions.md create mode 100644 docs/adr/0009-stable-tool-list-on-upstream-failure.md create mode 100644 docs/adr/0010-openai-compatible-embeddings.md create mode 100644 docs/adr/0011-pattern-rag-behind-spike.md create mode 100644 docs/adr/0012-v1-renderer-targets.md create mode 100644 docs/implementation-plan.md diff --git a/docs/adr/0001-single-binary-with-routed-services.md b/docs/adr/0001-single-binary-with-routed-services.md new file mode 100644 index 0000000..31d4a7d --- /dev/null +++ b/docs/adr/0001-single-binary-with-routed-services.md @@ -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. diff --git a/docs/adr/0002-streamable-http-only.md b/docs/adr/0002-streamable-http-only.md new file mode 100644 index 0000000..7e029d0 --- /dev/null +++ b/docs/adr/0002-streamable-http-only.md @@ -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. diff --git a/docs/adr/0003-per-agent-bearer-tokens.md b/docs/adr/0003-per-agent-bearer-tokens.md new file mode 100644 index 0000000..ed26b9b --- /dev/null +++ b/docs/adr/0003-per-agent-bearer-tokens.md @@ -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. diff --git a/docs/adr/0004-sqlite-datastore.md b/docs/adr/0004-sqlite-datastore.md new file mode 100644 index 0000000..27655a3 --- /dev/null +++ b/docs/adr/0004-sqlite-datastore.md @@ -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. diff --git a/docs/adr/0005-get-rules-tool-with-session-gating.md b/docs/adr/0005-get-rules-tool-with-session-gating.md new file mode 100644 index 0000000..8f3ea09 --- /dev/null +++ b/docs/adr/0005-get-rules-tool-with-session-gating.md @@ -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. diff --git a/docs/adr/0006-full-document-rule-delivery.md b/docs/adr/0006-full-document-rule-delivery.md new file mode 100644 index 0000000..42e980a --- /dev/null +++ b/docs/adr/0006-full-document-rule-delivery.md @@ -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). diff --git a/docs/adr/0007-project-id-from-day-one.md b/docs/adr/0007-project-id-from-day-one.md new file mode 100644 index 0000000..4963bb9 --- /dev/null +++ b/docs/adr/0007-project-id-from-day-one.md @@ -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 (`_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. diff --git a/docs/adr/0008-enforcement-scoped-to-mediated-actions.md b/docs/adr/0008-enforcement-scoped-to-mediated-actions.md new file mode 100644 index 0000000..dc3ed36 --- /dev/null +++ b/docs/adr/0008-enforcement-scoped-to-mediated-actions.md @@ -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. diff --git a/docs/adr/0009-stable-tool-list-on-upstream-failure.md b/docs/adr/0009-stable-tool-list-on-upstream-failure.md new file mode 100644 index 0000000..75c7e28 --- /dev/null +++ b/docs/adr/0009-stable-tool-list-on-upstream-failure.md @@ -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. diff --git a/docs/adr/0010-openai-compatible-embeddings.md b/docs/adr/0010-openai-compatible-embeddings.md new file mode 100644 index 0000000..48885e5 --- /dev/null +++ b/docs/adr/0010-openai-compatible-embeddings.md @@ -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. diff --git a/docs/adr/0011-pattern-rag-behind-spike.md b/docs/adr/0011-pattern-rag-behind-spike.md new file mode 100644 index 0000000..4d35262 --- /dev/null +++ b/docs/adr/0011-pattern-rag-behind-spike.md @@ -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. diff --git a/docs/adr/0012-v1-renderer-targets.md b/docs/adr/0012-v1-renderer-targets.md new file mode 100644 index 0000000..0f358b8 --- /dev/null +++ b/docs/adr/0012-v1-renderer-targets.md @@ -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. diff --git a/docs/implementation-plan.md b/docs/implementation-plan.md new file mode 100644 index 0000000..a5a950a --- /dev/null +++ b/docs/implementation-plan.md @@ -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, `_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.