Merge pull request 'docs: phased implementation plan and architecture decision records' (#1) from docs/implementation-plan into main

Reviewed-on: #1
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
This commit is contained in:
2026-08-08 19:37:23 +00:00
15 changed files with 833 additions and 0 deletions

199
Agentic Bridle MCP Plan.md Normal file
View File

@@ -0,0 +1,199 @@
# Agentic Bridle MCP
## Purpose
A self-hosted MCP server that acts as the single point of contact between coding agents (Claude Code, Codex, Kimi, DeepSeek, and others) and a project's tools, memory, and rules. The goal: switching which coding agent you're using should not change the behavior, context, or constraints that agent operates under. The intelligence and consistency live in this server, not in each agent's local config file.
**Design principle:** Bridle is project-agnostic infrastructure — a control plane other projects plug into, not something built around or dependent on any single project. RunicGateway (or any other project) is an early adopter/consumer of Bridle, not a dependency of it.
Deployed self-hosted via Docker Compose. Designed to be usable by others, not just a single homelab: Gitea is assumed to already exist (it's the one thing wired in as a pure external integration), but every other supporting service — Kanboard, Qdrant, LM Studio-equivalent embedding, etc. — ships as an *optional installable* component in the compose file. Someone who already runs their own Kanboard or vector DB points the gateway at their existing instance via env vars; someone who doesn't can spin the bundled version up alongside everything else with one `docker compose up`.
---
## Architecture
```
┌─────────────────────┐
Agents ───────► │ Gateway (Rust) │ ← single MCP entrypoint
(Claude Code, │ - auth/session │
Codex, Kimi, │ - routing │
DeepSeek, ...) │ - token vault │
└──────────┬───────────┘
┌──────────────┬───────┴────────┬───────────────┬────────────────┐
▼ ▼ ▼ ▼ ▼
┌───────────┐ ┌────────────┐ ┌─────────────┐ ┌─────────────┐ ┌──────────────┐
│ Rules │ │ Memory │ │ Gitea MCP │ │ Kanboard MCP │ │ Web Panel │
│ Server │ │ Service │ │ (existing) │ │ (custom) │ │ (admin UI) │
│ (custom) │ │ (custom) │ │ │ │ │ │ │
└─────┬──────┘ └──────┬──────┘ └──────────────┘ └──────────────┘ └──────┬───────┘
│ │ │
└────────► Qdrant (vector store) ◄───────────────────────────────────┘
```
All admin actions — from any surface — go through the Gateway's admin API. The Web Panel is a thin client against that API, not a separate integration point per service.
---
## Components
### 1. Gateway (custom, Rust)
- Single MCP endpoint all agents connect to.
- Identifies connecting agent (via MCP handshake client info or explicit config) and tags requests with agent type.
- Routes requests to backend services, aggregates responses into a consistent shape.
- **Owns all secrets/tokens centrally.** Backend services never hold their own credentials — the gateway injects them per-request. Agents and sub-services never see raw tokens.
- Token storage: Docker secrets for v1 (file-mounted, not env vars); consider SOPS if secrets need to be version-controlled in Gitea; Vault only if this scales beyond single-host/single-user.
- Exposes an admin API that the Web Panel consumes for full control of every subsystem.
- Maintains an audit log distinguishing the source of every change: user-edited (via panel), agent-proposed (via MCP tool, requires approval), or vendor-seeded (via guardrail suggestion feed, requires approval).
### 2. Rules Server (custom)
Stores one canonical ruleset per project — the source of truth that gets rendered into each agent's native format.
**Canonical document structure:**
```yaml
project:
overview: ...
stack: ...
conventions: ...
guardrails:
- id: no-unrequested-refactors
statement: "Do not perform unrelated refactoring outside the requested task."
severity: guidance | hard-instruction | enforcement
applies_to: [all_agents]
provenance:
source: user | agent | vendor
agent_session: ... # if agent-proposed
approved_by: ...
approved_at: ...
patterns:
standard: [MVC, MVVM, hexagonal, ...]
custom:
- name: "modified-mvc"
description: ...
example_repos:
- url: ...
reference_tag: "modified-mvc-example-1"
arch_tag: "modified-mvc"
last_indexed: <timestamp>
```
Guardrail severity tiers:
- **guidance** — model can deviate, shouldn't
- **hard-instruction** — agent is required to follow it. For scope-expansion rules specifically, this means the agent evaluates its own next action against the rule and calls a `request_scope_approval` MCP tool to check in before proceeding — not the gateway intercepting tool calls in the live path.
- **enforcement** — the gateway itself gates the action (e.g. rules-approval, secret access) rather than relying on the agent to self-report.
**Guardrails vs. agent profiles — kept separate:** project guardrails (above) are project policy, with their own provenance and lifecycle. This is deliberately distinct from vendor-published best-practice guidance, tracked separately per agent:
```yaml
agent_profiles:
claude:
capabilities: [...]
instruction_format: "CLAUDE.md conventions"
vendor_guardrail_suggestions: [...] # sourced from Anthropic docs, pending review
codex:
capabilities: [...]
instruction_format: "AGENTS.md"
vendor_guardrail_suggestions: [...]
kimi: {...}
deepseek: {...}
```
A vendor suggestion only becomes project policy once explicitly approved via the Web Panel — at which point it's promoted into the canonical `guardrails:` list with its own provenance record. This keeps vendor documentation from silently becoming project policy.
**Permissions:** each agent's authority over Bridle itself is explicit, not implied — e.g. an agent may be permitted to *propose* rule changes without being permitted to *approve* them:
```yaml
permissions:
claude:
gitea: {read: true, write: true}
kanboard: {read: true, write: true}
rules: {propose: true, approve: false}
secrets: {read: false}
```
**Per-model rendering:** `get_rules(project_id, agent_type)` renders the canonical doc into the target format:
- Claude → `CLAUDE.md`, native heading conventions
- Codex → `AGENTS.md`
- Kimi / DeepSeek → `AGENTS.md`-style, but formatted with more explicit, step-by-step phrasing to compensate for weaker instruction-following at the guardrail level
**Full document delivered, not a dynamic subset.** The rendered rules doc is always sent in full — no path-based or task-based filtering of what an agent receives. Some models (e.g. Opus-tier) need a fuller, more complete leash to reliably stay on task than others; how much a given model needs is itself a per-model concern handled by the renderer, not a universal token-saving optimization applied to everyone. Dynamic subsetting was considered and rejected for this reason.
**Guardrail sourcing:** a periodic or manually-triggered job pulls each vendor's published agent-guidance docs and proposes guardrail entries into a review queue. Nothing auto-merges — proposals require approval via the Web Panel before becoming part of the canonical doc.
**Editing paths:**
- Web Panel — primary surface for reviewing vendor suggestions, editing guardrails/patterns directly, managing example repos.
- Agent-initiated — MCP tools (`propose_guardrail`, `add_pattern`, `update_rules`) let an agent suggest changes mid-session. These are gated behind a confirmation step (surfaced in the Web Panel's approval queue) rather than auto-applied.
### 3. Memory Service (custom)
- Wraps Qdrant (self-hosted) for vector storage.
- Embeddings generated locally via LM Studio (RTX 3060).
- Two primary use cases sharing the same Qdrant instance in separate collections/namespaces:
- **Session memory** — decisions, context, and history from agent sessions.
- **Pattern example RAG** — indexed code from example repos linked to architecture patterns.
**Pattern example indexing:**
- Triggered once when a user adds an example repo to a pattern entry (clone → chunk → embed → upsert).
- Payload metadata per chunk: `reference_tag`, `arch_tag`, `source_repo`, `file_path`, `chunk_type`, `last_indexed`.
- **No automatic re-fetching.** Re-indexing only happens on explicit trigger — via the Web Panel or by telling an agent to refresh (`refresh_pattern_example(reference_tag)`), which re-runs the fetch/chunk/embed/upsert flow and replaces prior vectors for that tag.
- Exposed as an MCP tool: `search_pattern_examples(query, arch_tag?)` — semantic search optionally filtered to a specific named pattern.
### 4. Kanboard MCP (custom wrapper)
- Wraps Kanboard's existing REST API as MCP tools for task/plan management.
- Kanboard itself is not replaced — this is a thin translation layer.
### 5. Gitea MCP (existing)
- Uses an existing Gitea MCP server implementation rather than a custom-built wrapper.
- Exposes issues, PRs, commits, diffs as tools, authenticated via a gateway-issued token.
### 6. Web Panel (custom)
- Full admin surface — this project is user/developer-first, and the panel is not an add-on but the primary control interface.
- Talks only to the Gateway's admin API (not directly to individual backend services), mirroring the centralized-token principle.
- Controls:
- Gateway: connected agents/sessions, routing, agent-identity mapping
- Secrets: add/rotate/revoke (raw values never displayed after storage)
- Rules: edit canonical doc, approve/reject agent-proposed and vendor-seeded guardrails, manage patterns and example repos, trigger re-indexing
- Memory: browse/search indexed content, manual purge/re-index
- Integrations: Gitea/Kanboard connection status and project mappings
- Audit log: full change history with source attribution
---
## Deployment
- Docker Compose, self-hosted (dev instance on Darrow/Proxmox or Lucifer/Unraid).
- WireGuard for secure agent access when off local network.
- Pangolin if external reachability is needed.
**Optional/bundled services:** everything except Gitea is toggleable in the compose file — bundled via Compose profiles (e.g. `--profile with-kanboard`, `--profile with-qdrant`) so each service is either:
- **Bundled**: spun up as part of the stack for someone with no existing instance, or
- **External**: pointed at an existing self-hosted instance via env vars (`KANBOARD_URL`, `QDRANT_URL`, etc.), skipping the bundled container entirely.
Gitea is the exception — treated as a required external integration (via the existing Gitea MCP server) rather than something bundled, since it's assumed the user already runs one.
---
## Build Phases
1. Gateway skeleton + Rules Server (prove single-source-of-truth rendering across at least two agent formats)
2. Qdrant integration for session memory
3. Kanboard MCP wrapper
4. Gitea MCP integration (existing server, wired through gateway auth)
5. Pattern-example RAG pipeline (fetch/chunk/embed/tag/search)
6. Web Panel — full admin surface across all of the above
7. Guardrail vendor-sourcing feed + approval queue
8. Audit log and multi-project support
---
## Open Questions
- Exact schema/versioning strategy for the canonical rules document as it evolves (e.g. schema migrations when new sections are added).
- Whether Web Panel auth is single-user or needs multi-user roles (currently designed single-user/developer-first).
- Long-term: whether to integrate with agentglass for session observability once the gateway is routing all agent traffic.

78
CLAUDE.md Normal file
View File

@@ -0,0 +1,78 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Subagents
Use subagents (the Agent tool) when a task genuinely benefits from one — and match the model to the
work:
- **Reach for a subagent** for broad, read-heavy fan-out that would otherwise flood the main context:
sweeping many files across services, tracing a change through multiple components, or researching
where/how something is wired. Prefer `Explore` (read-only search) or `general-purpose` for these.
Use `Plan` to design a multi-step implementation before editing. Keep narrow, single-file edits and
quick lookups on the main thread — spawning a cold agent for those costs more than it saves.
- **Assign the model to the task.** Route heavy reasoning — architecture decisions, contract/protocol
changes, security-sensitive work (auth, secrets, credential handling) — to a stronger model (Opus).
Route routine, well-scoped mechanical work — bulk search, boilerplate, simple refactors,
running/reporting tests — to a faster, cheaper model (Sonnet or Haiku). State the model explicitly
when spawning rather than defaulting.
## Git & Gitea access
- **All commits and pushes use the `wtclaude` bot identity, authenticated with the token at
`C:\Users\colby\.gitea_token_claude`.** Do not use the human user's git credentials. Configure the
remote (or a one-off push) to carry the token and set the author/committer to `wtclaude`, e.g.:
```bash
TOKEN=$(cat /c/Users/colby/.gitea_token_claude)
git -c http.extraHeader="Authorization: token $TOKEN" \
-c user.name=wtclaude -c user.email=claude@whitlocktech.net \
push origin <branch>
```
Keep the token out of committed files, remote URLs, and command output/logs (pass it via
`http.extraHeader` or a credential helper, never inline in the `origin` URL).
- **Use the Gitea MCP tools (`mcp__gitea__*`) for all server-side Gitea operations** — opening and
reviewing pull requests, issues, releases, branches, labels, and reading repo contents. Prefer the
MCP over shelling out to `git`/`gh`/`tea` or the raw REST API for these. Reserve local `git` for
working-tree operations (commit, push, branch checkout) using the token above.
- **Sync `main` before you start coding.** Before creating any new branch, switch to `main` and pull
so the local copy matches the remote (branches are always cut from an up-to-date `main`, never a
stale one):
```bash
TOKEN=$(cat /c/Users/colby/.gitea_token_claude)
git checkout main
git -c http.extraHeader="Authorization: token $TOKEN" pull --ff-only origin main
git checkout -b <type>/<branch>
```
If the working tree has uncommitted changes that block the checkout/pull, stop and surface it
rather than discarding them.
## SonarQube (code quality / security scanning)
- **SonarQube is at `https://sonar.whitlocktech.com`.** The API token lives in a local file in
`SONAR_TOKEN=squ_…` (KEY=value) format — extract the value after `=`. Read it at call time, and
keep it out of committed files, remote URLs, and command output/logs. **Authenticate with HTTP
Basic auth (token as username, empty password) — `-u "$TOKEN:"`; the `Authorization: Bearer` header
returns 401 on this instance.** Example:
```bash
TOKEN=$(grep -oP '(?<=SONAR_TOKEN=).*' /path/to/sonar_token.txt)
curl -s -u "$TOKEN:" \
"https://sonar.whitlocktech.com/api/issues/search?componentKeys=<projectKey>&types=VULNERABILITY"
```
- **Project key(s):** _fill in once the Sonar project(s) for this repo are created._
## Conventions
- **Conventional Commits** (`type(scope): summary`, e.g. `feat(gateway): add token vault`).
- **AI-assisted contributions must be disclosed** (org policy): tick the PR-template box naming the
tool, and mark AI-authored commits with a trailer such as `Co-Authored-By: Claude <noreply@anthropic.com>`.
Undisclosed AI-generated contributions may be closed. See `CONTRIBUTING.md`.
- Branch from `main` (`feature/…`, `fix/…`, `docs/…`, `chore/…`).
- **All architectural and design decisions must be asked and approved by the org lead (Colby
Whitlock) before implementation. You must ask before writing code.**

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.

204
docs/implementation-plan.md Normal file
View 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 16, 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 16 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 18 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.