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:
199
Agentic Bridle MCP Plan.md
Normal file
199
Agentic Bridle MCP Plan.md
Normal 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
78
CLAUDE.md
Normal 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.**
|
||||
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