26382a7ac6
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
JetBrains Plugin / Actionlint (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
CodeQL / Analyze (rust) (push) Waiting to run
JetBrains Plugin / Validation (push) Waiting to run
JetBrains Plugin / Build (push) Waiting to run
JetBrains Plugin / Test (push) Blocked by required conditions
Security Check / Security Scan (push) Waiting to run
CI / Clippy (push) Failing after 15m13s
CI / Test (ubuntu-latest) (push) Failing after 16m1s
CI / Test (macos-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Build (no embeddings / no ORT) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Cookbook (Node) (push) Has been cancelled
CI / Pi Extension (Node) (push) Has been cancelled
CI / Rust SDK (lean-ctx-client) (push) Has been cancelled
CI / Embed SDK (lean-ctx-sdk) (push) Has been cancelled
CI / Python SDK (leanctx) (push) Has been cancelled
CI / Hermes Plugin (Python) (push) Has been cancelled
CI / SDK Conformance Matrix (push) Has been cancelled
CI / Coverage (push) Has been cancelled
CI / cargo-deny (push) Has been cancelled
CI / Adversarial Safety (push) Has been cancelled
CI / Benchmarks (push) Has been cancelled
CI / Output-Quality Gate (eval A/B) (push) Has been cancelled
CI / Documentation (push) Has been cancelled
CI / CI Green (push) Has been cancelled
193 lines
11 KiB
Markdown
193 lines
11 KiB
Markdown
# Addon Bootstrap Engine — Phase 2 (implemented)
|
||
|
||
> Status: **implemented** in `rust/src/core/addons/bootstrap.rs` (+ manifest,
|
||
> policy, store, CLI wiring). This document is the design of record for the
|
||
> GitLab epic *Addon Bootstrap Engine* (`root/lean-ctx#1105`, subtasks
|
||
> `#1106`–`#1110`). Where the shipped behaviour refines the original plan it is
|
||
> noted inline.
|
||
|
||
## Problem
|
||
|
||
Today `lean-ctx addon add` is **declarative**: it appends a
|
||
`[[gateway.servers]]` entry to the global config and records the install — it
|
||
never fetches or installs a package. That is sufficient for **ephemeral
|
||
runners** (`npx`, `uvx`), which download and execute their package lazily on the
|
||
first tool call. So `repomix` and `serena` already install on add.
|
||
|
||
It is **not** sufficient for tools that need a real, one-time bootstrap before a
|
||
runnable command exists:
|
||
|
||
| Tool | Why a runner can't do it |
|
||
|-----------|----------------------------------------------------------------------|
|
||
| Headroom | Ships as `headroom-ai[all]`; `uvx --from` breaks on its entry points — needs `uv tool install`. |
|
||
| Graphify | `uv tool install "graphifyy[mcp]"` **and** a pre-built `graph.json`. |
|
||
| Cognee | Clone + `uv sync`; no single-command runner. |
|
||
| Letta | `npm i -g` plus a long-running server instance. |
|
||
|
||
For these the registry stays **listed** (homepage + manual instructions). The
|
||
bootstrap engine closes that gap: a declarative `[install]` block that lean-ctx
|
||
can execute idempotently, with a clean uninstall path and the same security bar
|
||
as the rest of the addon system.
|
||
|
||
## Goals / non-goals
|
||
|
||
**Goals**
|
||
- One declarative `[install]` block per addon, pinned and auditable.
|
||
- Idempotent install + reliable uninstall (no orphaned global packages).
|
||
- Reuse the existing trust/audit pipeline — no new ad-hoc shell-outs.
|
||
- Keep Phase-1 behaviour unchanged when no `[install]` block is present.
|
||
|
||
**Non-goals**
|
||
- No arbitrary script execution (no `curl | sh`, no inline shell).
|
||
- No secret provisioning (Mem0 / Claude-Context keys stay the user's job; the
|
||
engine only documents and validates the required env names).
|
||
- No new network fetch of the registry itself (still compiled-in / signed override).
|
||
|
||
## The `[install]` manifest block
|
||
|
||
```toml
|
||
[install]
|
||
manager = "uv" # one of: pip | uv | cargo | npm | brew | dotnet
|
||
package = "headroom-ai[mcp]" # the package spec the manager understands
|
||
version = "0.27.0" # MANDATORY exact pin (no ranges, no "latest")
|
||
bin = "headroom" # binary the [mcp] command expects (PATH idempotency)
|
||
# verify = ["headroom", "--version"]# optional argv probe; exit 0 ⇒ already installed
|
||
```
|
||
|
||
Rules (enforced by `AddonInstall::validate()`, called from `manifest.validate()`):
|
||
- `manager` ∈ a fixed allowlist (`uv`/`pip`/`cargo`/`npm`/`brew`/`dotnet`). Each manager
|
||
maps to a **fixed argv template** the engine owns — the manifest never supplies
|
||
raw shell.
|
||
- `version` is required and must be an exact pin (empty / `latest` / `*` are
|
||
rejected).
|
||
- `package`, `version`, `bin` and every `verify` element are rejected if they
|
||
contain shell metacharacters (`| ; & $ \` > <`, newlines) — defence-in-depth,
|
||
since the engine never uses a shell.
|
||
- **Idempotency check** (shipped refinement): `verify` is *optional*. With no
|
||
`verify`, the engine checks whether `bin` resolves on `PATH`; `verify` is an
|
||
escape hatch (argv, exit 0 ⇒ installed) for tools whose presence needs a
|
||
deeper probe.
|
||
- The block is only meaningful together with a runnable `[mcp]` block whose
|
||
`command` is produced by the install (e.g. `headroom`).
|
||
|
||
### Manager → argv templates (engine-owned)
|
||
|
||
| `manager` | install argv | uninstall argv |
|
||
|-----------|--------------------------------------------------|----------------------------------|
|
||
| `uv` | `uv tool install {package}=={version}` | `uv tool uninstall {base}` |
|
||
| `pip` | `pip install --user {package}=={version}` | `pip uninstall -y {base}` |
|
||
| `cargo` | `cargo install {base} --version {version}` | `cargo uninstall {base}` |
|
||
| `npm` | `npm install -g {package}@{version}` | `npm rm -g {base}` |
|
||
| `brew` | `brew install {package}` (formula carries the pin, e.g. `node@22`) | `brew uninstall {base}` |
|
||
| `dotnet` | `dotnet tool install --global {package} --version {version}` | `dotnet tool uninstall --global {base}` |
|
||
|
||
`{base}` is `{package}` with extras and any inline version stripped
|
||
(`headroom-ai[mcp]` → `headroom-ai`), keeping an npm scope intact
|
||
(`@scope/pkg`). The manifest chooses a manager + package + pin; it **cannot**
|
||
influence the flags or inject extra argv. Every value is passed as a *discrete*
|
||
argv element via `std::process::Command` — no shell, no interpolation.
|
||
|
||
## Install lifecycle
|
||
|
||
The executor lives in `addons/bootstrap.rs` (`ensure_installed` / `uninstall`)
|
||
and is orchestrated by the CLI (`cli/addon_cmd.rs`) *after* consent and *before*
|
||
the health probe. The core `addons/install.rs` stays pure — it only persists the
|
||
receipt — so its unit tests never spawn a process.
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
add["addon add <name>"] --> has{has [install]?}
|
||
has -- no --> wire["wire [[gateway.servers]] (Phase 1)"]
|
||
has -- yes --> gate["bootstrap gate: validate + consent"]
|
||
gate --> present{already present? (verify argv)}
|
||
present -- yes --> wire
|
||
present -- no --> run["run engine-owned install argv (pinned)"]
|
||
run --> verify["run verify argv → must exit 0"]
|
||
verify -- ok --> record["record install receipt (manager, package, version, bin)"]
|
||
record --> wire
|
||
verify -- fail --> rollback["best-effort uninstall + abort, no wiring"]
|
||
```
|
||
|
||
- **Idempotency**: check presence *first* (`verify` argv, else `bin` on `PATH`);
|
||
if already satisfied, skip the manager entirely and just wire. Re-running `add`
|
||
is safe and reports `Already installed — skipped`.
|
||
- **Pre-flight**: when an install *is* needed, verify the manager itself exists
|
||
(`LEANCTX_BOOTSTRAP_<MGR>` override path, else the bare name on `PATH`) before
|
||
spawning it. A missing manager fails with an install hint
|
||
(`install uv → https://docs.astral.sh/uv/…`) instead of a raw spawn error, and
|
||
the `add` disclosure shows a `requires: <manager> on PATH — ✓/✗` line up front
|
||
so the gap is visible *before* consent.
|
||
- **Receipt**: `<data_dir>/addons/installed.json` carries an `install` record
|
||
(manager, package, version, bin) — content-only, no timestamps, so it stays
|
||
determinism-friendly (#498). `remove` reads it to uninstall.
|
||
- **Uninstall**: `addon remove` runs the manager's uninstall argv for packages
|
||
*this engine installed* (tracked by receipt) — never something the user had
|
||
already. It is best-effort: a failed uninstall logs a note but never blocks the
|
||
unwire that already succeeded.
|
||
- **Failure**: a non-zero manager exit aborts `add` before anything is wired. A
|
||
clean install whose `bin` is not yet on `PATH` is a non-fatal warning (a PATH
|
||
setup issue, not a failed install), and the subsequent health probe still
|
||
guards a truly broken wiring.
|
||
|
||
## Security gates
|
||
|
||
The bootstrap surface is gated at four layers (shipped):
|
||
|
||
- **Structural validation** (`AddonInstall::validate()`, hard error): unknown
|
||
manager, missing/floating version, or shell metacharacters in
|
||
`package`/`version`/`bin`/`verify` reject the manifest. Because it runs from
|
||
`manifest.validate()`, every path is covered — `addon add`, `addon audit`,
|
||
`from_path`, and the registry validator (so a bad block fails CI's
|
||
`bundled_registry_passes_security_validator`).
|
||
- **Capability coherence**: a declared `[install]` block makes
|
||
`trust::wiring_uses_network` return `true`, so an addon that *also* declares
|
||
`[capabilities] network = "none"` trips the existing `cap_net_underdeclared`
|
||
audit — same gate as the `npx`/`uvx` runner case.
|
||
- **Consent**: the `add` preview prints the **exact** install + uninstall argv,
|
||
the manager, the package and the pin *before anything runs*, then requires the
|
||
standard yes/no (`--yes` to skip in CI). `add` itself is the user's explicit,
|
||
consented action.
|
||
- **Policy floor**: `addons.allow_bootstrap` (global-only). Default **on** — the
|
||
whole point is that `add` installs — but a team that forbids local
|
||
package-manager execution sets it to `false`, and `policy::gate` refuses any
|
||
`[install]` addon before a single command runs.
|
||
|
||
## What this unlocks — and the honest migration status
|
||
|
||
The engine is generic across all five managers. Registry entries flip to
|
||
install-on-add **only when the tool actually ships a clean, pinned,
|
||
runnable-out-of-the-box MCP server** — never with fabricated wiring.
|
||
|
||
| Tool | Status | Why |
|
||
|-----------|---------------|-----|
|
||
| **Headroom** | ✅ migrated | `uv tool install "headroom-ai[mcp]"` (pinned `0.27.0`) → `headroom mcp serve`; a local, secret-free stdio MCP server. The flagship install-on-add. |
|
||
| Graphify | listed | Package installs cleanly (`graphifyy[mcp]`), but its MCP server needs a **pre-built `graph.json`** (`python -m graphify.serve graph.json`) — no out-of-the-box server to probe. |
|
||
| Cognee | listed | MCP server needs a **repo clone + `uv sync`** (upstream issue #1815); no working pinned one-liner yet. |
|
||
| Letta | listed | A pinned `letta-mcp-server` package exists, but the server needs `LETTA_API_KEY` + a Letta backend to start — key-gated, not one-click. |
|
||
|
||
Mem0 and Claude-Context likewise remain key-gated: the engine *could* install
|
||
their package, but cannot provision `MEM0_API_KEY` / `OPENAI_API_KEY` + Milvus —
|
||
those stay documented prerequisites. Each tool above flips to installable with a
|
||
**one-line registry change** (an `[install]` + `[mcp]` block) the moment upstream
|
||
ships a clean server — no further engine work.
|
||
|
||
## Rollout — done
|
||
|
||
1. ✅ `[install]` parsing + validator gates.
|
||
2. ✅ Install/uninstall executor (`bootstrap.rs`), gated by `addons.allow_bootstrap`.
|
||
3. ✅ Headroom migrated; the bundled registry stays green on
|
||
`bundled_registry_passes_security_validator`. Graphify/Cognee/Letta wait on a
|
||
clean upstream MCP server (see table) rather than shipping broken wiring.
|
||
|
||
## Operational notes & open questions
|
||
|
||
- **Manager path override** (shipped): set `LEANCTX_BOOTSTRAP_<MANAGER>` (e.g.
|
||
`LEANCTX_BOOTSTRAP_UV=/opt/uv`) to pin the exact manager binary for locked-down
|
||
environments; otherwise the manager is resolved from `PATH`.
|
||
- Open: per-manager cache/location detection for richer "already present" checks
|
||
(today: `verify` argv, else `bin` on `PATH`).
|
||
- Partly shipped: `add` now pre-flights the manager's existence (with an install
|
||
hint); a full `doctor` sweep over *already-installed* addons is still open.
|
||
- Open: Windows support for the manager templates (the executable probe already
|
||
falls back to "is a file" off-unix; argv templates assume POSIX managers).
|