26382a7ac6
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
JetBrains Plugin / Actionlint (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (rust) (push) Has been cancelled
JetBrains Plugin / Validation (push) Has been cancelled
JetBrains Plugin / Build (push) Has been cancelled
JetBrains Plugin / Test (push) Has been cancelled
Security Check / Security Scan (push) Has been cancelled
546 lines
24 KiB
Markdown
546 lines
24 KiB
Markdown
# Addons — community extensions for lean-ctx
|
||
|
||
Addons let anyone extend lean-ctx with an **external MCP server** and have it
|
||
show up through the gateway with one command — no fork, no recompile. This guide
|
||
covers using addons and **building & publishing your own**.
|
||
|
||
> Not sure an Addon is the right mechanism? See
|
||
> [Extending lean-ctx](extensions.md) for the one-decision guide (Addon vs
|
||
> Plugin vs Provider vs Pack vs SDK).
|
||
|
||
Contract: [`addon-manifest-v1`](../contracts/addon-manifest-v1.md).
|
||
|
||
## Why an addon goes deeper than a passthrough
|
||
|
||
Most "MCP aggregators" stop at proxying: they forward a downstream tool's output
|
||
to the model verbatim. lean-ctx can do that too (a **governed passthrough** —
|
||
secrets redacted, output audit-tagged as untrusted), but it can also do something
|
||
no aggregator does: run the addon's output through **its own context engine**, so
|
||
the result is retrieved, searched, graphed and remembered through the *same* paths
|
||
as your own code. One `ctx_expand`, one `ctx_search`, one `ctx_callgraph`, one
|
||
`ctx_knowledge` — regardless of which addon produced the data.
|
||
|
||
This is opt-in and **off by default** (pure passthrough until you enable it).
|
||
Turn it on globally and/or per server:
|
||
|
||
```toml
|
||
[gateway]
|
||
enabled = true
|
||
compress_output = true # L1: format-aware compression (deterministic)
|
||
handle_spill = true # L2: oversized output → ctx_expand retrieval handle
|
||
index_output = true # L3: consolidate into BM25 + graph + knowledge
|
||
output_budget_tokens = 2000 # L1 target / L2 spill threshold
|
||
|
||
[[gateway.servers]]
|
||
name = "repomix"
|
||
# … command/args …
|
||
integration = "codebase-pack" # L4 typed adapter (usually auto-derived; see below)
|
||
```
|
||
|
||
…or via the CLI: `lean-ctx config set gateway.index_output true`.
|
||
|
||
### The four levels
|
||
|
||
| Level | Flag | What happens to addon output |
|
||
|---|---|---|
|
||
| **L1 compress** | `compress_output` | Format-aware compression to `output_budget_tokens` — a deterministic function of (content, budget), so it never breaks provider prompt-caching ([#498](../reference/README.md)). |
|
||
| **L2 handle/spill** | `handle_spill` | Output over budget is stored verbatim in the content-addressed archive; the model gets a summary + a `ctx_expand` handle instead of the blob. Generalizes Repomix's `outputId` and Headroom's CCR to **every** addon, through one retrieval path. |
|
||
| **L3 consolidate** | `index_output` | A background side-channel feeds the output into the BM25 index (`ctx_search` / `ctx_semantic_search`), links file references as property-graph edges (`ctx_callgraph`), and remembers facts (`ctx_knowledge`). Never alters the returned text. |
|
||
| **L4 typed adapters** | per-server `integration` | A category-aware adapter folds a known payload into the matching store (below). |
|
||
|
||
Security and determinism are preserved at every level: post-processing runs
|
||
**after** `scrub_output` (secrets already gone), L1/L2 are deterministic
|
||
functions of the content, and L3 is a pure side-channel (like usage metering).
|
||
|
||
### Typed adapters (L4) — competitors as first-class citizens
|
||
|
||
When an addon belongs to a known category, a typed adapter understands its output
|
||
and routes it into lean-ctx's native store. The `integration` slug is normally
|
||
**auto-derived** from the addon's `categories`; set it explicitly only to force or
|
||
disable an adapter (`none`).
|
||
|
||
| `integration` | Example addons | What the adapter does |
|
||
|---|---|---|
|
||
| `codebase-pack` | Repomix | `pack_codebase` → archive + `ctx_expand` handle (keeps the repomix `outputId` for grep) |
|
||
| `code-graph` | Graphify | nodes/edges → property graph → `ctx_callgraph` |
|
||
| `code-symbols` | Serena | LSP-precise `find_referencing_symbols` → property-graph call edges (complements tree-sitter) |
|
||
| `memory` | Mem0 / OpenMemory / Cognee / Letta | `search_memories` → `ctx_knowledge` facts |
|
||
| `compression` | Headroom / RTK | registered as a named lean-ctx `Compressor` (selectable like the built-ins) |
|
||
|
||
The positioning is deliberate **counter-lock-in**: a competing tool plugs in as
|
||
one interchangeable component among many, while lean-ctx stays the unifying
|
||
retrieval / search / graph / memory substrate. You can integrate the competition
|
||
instead of being encapsulated by it.
|
||
|
||
## Use an addon
|
||
|
||
```bash
|
||
lean-ctx addon list # installed addons + the registry
|
||
lean-ctx addon search markdown # search the registry (empty = list all)
|
||
lean-ctx addon info <name> # details + the MCP wiring it would add
|
||
lean-ctx addon add <name> # install from the curated registry
|
||
lean-ctx addon add acme/tool # install a hosted pack from ctxpkg.com
|
||
lean-ctx addon remove <name> # uninstall
|
||
```
|
||
|
||
`add` prints the exact server it will run (transport, command, args, env) and
|
||
asks before changing anything. Pass `--yes` / `-y` to skip the prompt in
|
||
scripts. Installing an addon enables the MCP gateway (`gateway.enabled = true`);
|
||
its tools become reachable via `ctx_tools` (find/call) — restart your MCP client
|
||
to pick them up.
|
||
|
||
A `<namespace>/<name>` target resolves against the hosted ctxpkg registry
|
||
(GH #726): lean-ctx downloads the signed `kind=addon` pack, verifies the
|
||
artifact hash against the registry index, the pack's integrity hashes, its
|
||
**mandatory** ed25519 signature and the kind ↔ payload coherence — then the
|
||
embedded manifest walks the exact same consent → preflight → health-probe
|
||
pipeline as a local or curated install. `@version` pins a release;
|
||
`addon update` re-resolves from wherever the addon was installed.
|
||
|
||
### Install on add — artifacts, ephemeral runners & the `[install]` block
|
||
|
||
There are three ways `add` makes a tool runnable, all pinned and disclosed —
|
||
resolved in this order:
|
||
|
||
1. **`[artifacts]` block** (GH #725) — the manifest declares prebuilt binaries
|
||
per platform (Rust target triple). `add` downloads the one for your
|
||
platform into the **managed bin dir**
|
||
(`<data_dir>/addons/bin/<name>/<version>/`, never `PATH`), verifies its
|
||
SHA-256 before the atomic install, pins that hash as the spawn-time
|
||
binhash, and wires the gateway to the absolute path. A tampered binary is
|
||
refused at spawn; `lean-ctx addon update <name>` installs the next version
|
||
side-by-side, health-checks it, then prunes the old one.
|
||
2. **Ephemeral runner** — when the `[mcp]` command is `npx` (Node) or `uvx`
|
||
(uv/Python), the package is downloaded and run **lazily on the first tool
|
||
call**, then cached. `add` only writes the `[[gateway.servers]]` entry;
|
||
*adding is installing*, provided the runner is on your `PATH`.
|
||
3. **`[install]` block** (#1105, Phase 2) — for tools that need a one-time
|
||
bootstrap before a runnable command exists, the manifest declares a pinned
|
||
package-manager install. On `add`, lean-ctx runs it (idempotently); on
|
||
`remove`, it uninstalls it. The exact commands are shown before anything runs.
|
||
|
||
```toml
|
||
[artifacts.aarch64-apple-darwin]
|
||
filename = "my-addon-aarch64-apple-darwin"
|
||
url = "https://github.com/you/my-addon/releases/download/v1.2.0/my-addon-aarch64-apple-darwin"
|
||
sha256 = "…" # mandatory pin — unpinned artifacts are rejected
|
||
|
||
[artifacts.x86_64-unknown-linux-gnu]
|
||
filename = "my-addon-x86_64-unknown-linux-gnu"
|
||
url = "https://github.com/you/my-addon/releases/download/v1.2.0/my-addon-x86_64-unknown-linux-gnu"
|
||
sha256 = "…"
|
||
```
|
||
|
||
Platforms without an `[artifacts]` entry fall through to the runner /
|
||
`[install]` path, so one manifest can serve prebuilt binaries where you build
|
||
them and a `cargo install` bootstrap everywhere else.
|
||
|
||
```toml
|
||
[install]
|
||
manager = "uv" # uv | pip | cargo | npm | brew | dotnet
|
||
package = "headroom-ai[mcp]" # the package spec the manager understands
|
||
version = "0.27.0" # mandatory exact pin (no ranges / latest)
|
||
bin = "headroom" # binary the [mcp] command needs (PATH idempotency)
|
||
```
|
||
|
||
The engine never uses a shell: each manager has a fixed argv template, and
|
||
`package`/`version`/`bin` are passed as discrete arguments (and rejected if they
|
||
contain shell metacharacters). A team can forbid all bootstrap execution with
|
||
`lean-ctx config set addons.allow_bootstrap false`. Every installable entry pins
|
||
an exact version; an unpinned runner or `[install]` block is rejected by the
|
||
registry validator, so upstream can't change under you silently.
|
||
|
||
| Tool | Add = install? | Wiring / bootstrap | Secrets |
|
||
|---|---|---|---|
|
||
| `repomix` | **yes** (runner) | `npx -y repomix@1.15.0 --mcp` | — |
|
||
| `serena` | **yes** (runner) | `uvx --from serena-agent==1.5.3 serena start-mcp-server` | — |
|
||
| `qmd` | **yes** (runner) | `npx -y @tobilu/qmd@2.5.3 mcp` — register folders first (`qmd collection add`) | — |
|
||
| `memgraph-ingester` | **yes** (runner) | `uvx --from memgraph-ingester-mcp==0.6.6 memgraph-ingester-mcp` | needs a running Memgraph (`MEMGRAPH_INGESTER_MCP_BOLT_URI`) |
|
||
| `sequential-thinking` | **yes** (runner) | `npx -y @modelcontextprotocol/server-sequential-thinking@…` | — |
|
||
| `everything` | **yes** (runner) | `npx -y @modelcontextprotocol/server-everything@…` | — |
|
||
| `headroom` | **yes** (`[install]`) | `uv tool install headroom-ai[mcp]==0.27.0` → `headroom mcp serve` | — |
|
||
| `graphify` | listed | `uv tool install "graphifyy[mcp]"` **+ a built `graph.json`** (no out-of-the-box server) | — |
|
||
| `cognee` | listed | clone + `uv sync` (upstream #1815); no pinned one-liner | — |
|
||
| `letta` | listed | `npm i -g letta-mcp-server` + a running Letta backend | `LETTA_API_KEY` |
|
||
| `mem0` | listed | official MCP server (hosted) | `MEM0_API_KEY` |
|
||
| `claude-context` | listed | `npx @zilliz/claude-context-mcp` | `OPENAI_API_KEY` + Milvus |
|
||
| `rtk` | listed | shell-output hook; MCP via the `rtk-mcp` bridge | — |
|
||
| `lean-md` | listed | Directive-driven Markdown for agent plans (`@dasTholo/lean-md`) | — |
|
||
|
||
*Listed* tools either need secrets/a backend or don't ship a clean, pinned,
|
||
out-of-the-box MCP server yet. Each flips to install-on-add with a one-line
|
||
registry change (an `[install]` + `[mcp]` block) the moment upstream ships one —
|
||
see the [bootstrap-engine design](../dev/addon-bootstrap-engine.md).
|
||
|
||
## Build your own addon
|
||
|
||
An addon is just an MCP server plus a manifest. Four steps:
|
||
|
||
### 1. Expose your tool as an MCP server
|
||
|
||
Ship a `stdio` server (an executable that speaks MCP over stdin/stdout) or an
|
||
`http` server (a streamable-HTTP endpoint). This is what lean-ctx will run or
|
||
connect to. If your project is currently a library or a fork, wrap its
|
||
capabilities behind a thin MCP server binary — that is what makes it a runtime
|
||
addon instead of a build-time fork.
|
||
|
||
### 2. Add `lean-ctx-addon.toml` to your repo
|
||
|
||
Scaffold one in seconds — `lean-ctx addon init` writes a valid,
|
||
secure-by-default manifest (slug taken from the directory name) you then edit:
|
||
|
||
```bash
|
||
lean-ctx addon init # stdio addon in ./lean-ctx-addon.toml
|
||
lean-ctx addon init my-addon --http # or name it + use an HTTP endpoint
|
||
```
|
||
|
||
…or write it by hand:
|
||
|
||
```toml
|
||
[addon]
|
||
name = "my-addon" # slug: [a-z0-9-]
|
||
display_name = "My Addon"
|
||
version = "0.1.0"
|
||
description = "What it does, in one line."
|
||
author = "you"
|
||
homepage = "https://github.com/you/my-addon"
|
||
license = "Apache-2.0"
|
||
categories = ["workflow"]
|
||
keywords = ["plans", "macros"]
|
||
min_lean_ctx = "3.8.0"
|
||
|
||
[mcp]
|
||
transport = "stdio" # or "http"
|
||
command = "my-addon-mcp" # stdio: executable to spawn
|
||
args = ["serve"]
|
||
# env = { MY_TOKEN = "..." } # optional child-process env
|
||
|
||
# For an HTTP server instead of stdio:
|
||
# [mcp]
|
||
# transport = "http"
|
||
# url = "https://my-addon.example.com/mcp"
|
||
# headers = { Authorization = "Bearer ..." }
|
||
|
||
# Context packages this addon needs at runtime (depth-1, installed first):
|
||
[[dependencies]]
|
||
name = "@dasTholo/lean-md-skills"
|
||
version_req = "^0.2"
|
||
optional = false
|
||
|
||
[mcp.env]
|
||
LEAN_MD_SKILLS_DIR = "{pack_dir:@dasTholo/lean-md-skills}"
|
||
```
|
||
|
||
See the [contract](../contracts/addon-manifest-v1.md) for every field.
|
||
|
||
### Declare what your addon needs — `[capabilities]`
|
||
|
||
Add a `[capabilities]` block to opt your stdio addon into a **per-addon,
|
||
secure-by-default sandbox**. lean-ctx enforces the `network`/`filesystem` profile
|
||
you declare at the spawn point (`sandbox-exec` on macOS, `bwrap` on Linux — and
|
||
child processes inherit it), scrubs the environment to your `env` allowlist, and
|
||
shows the user the full list before they install:
|
||
|
||
```toml
|
||
[capabilities]
|
||
network = "full" # "none" (default) blocks all outbound network
|
||
filesystem = "read_only" # "read_write" if you write outside a scratch tmp
|
||
env = ["GITHUB_TOKEN"] # only these host env vars reach your process
|
||
```
|
||
|
||
Declaring nothing is the safest: no network, read-only filesystem, and a
|
||
scrubbed environment (host secrets never leak to your child process). Omit the
|
||
block entirely to keep the legacy global `addons.sandbox` behaviour. Declaring
|
||
the minimum you need is what makes your addon trustworthy in the marketplace.
|
||
|
||
### 3. Test it live — locally, before publishing
|
||
|
||
```bash
|
||
lean-ctx addon audit ./lean-ctx-addon.toml # the publish/list gate (#403)
|
||
lean-ctx addon add ./lean-ctx-addon.toml
|
||
lean-ctx addon list # your addon, installed (source: local)
|
||
# … exercise it via ctx_tools …
|
||
lean-ctx addon remove my-addon
|
||
```
|
||
|
||
`addon add <path>` wires a local manifest exactly like a registry entry, so you
|
||
get the full install flow without touching the registry. `addon audit` runs the
|
||
same gate the registry validator does — wiring risk, **capability coherence**
|
||
(do your `[capabilities]` match what the wiring actually does?) and **malware
|
||
heuristics** — and exits non-zero on a `fail` verdict, so you can run it in CI.
|
||
|
||
#### Pin your binary (stdio) — `sha256`
|
||
|
||
For a `stdio` addon, pin the binary so a swapped executable can never run under
|
||
your addon's name:
|
||
|
||
```bash
|
||
shasum -a 256 my-addon-mcp # → copy the hex digest
|
||
```
|
||
|
||
```toml
|
||
[mcp]
|
||
transport = "stdio"
|
||
command = "my-addon-mcp"
|
||
sha256 = "…the digest…" # the gateway refuses a mismatch, fail-closed
|
||
```
|
||
|
||
A pinned binary is one of the requirements for the verified/paid tier (see the
|
||
audit gate below).
|
||
|
||
### 4. Publish it
|
||
|
||
Two distribution channels, one trust chain:
|
||
|
||
**Self-service — `addon publish` (GH #726).** Ship without waiting for a
|
||
review cycle: your `lean-ctx-addon.toml` is wrapped verbatim into a signed
|
||
`kind=addon` context package and uploaded to the hosted ctxpkg registry.
|
||
|
||
```bash
|
||
lean-ctx addon publish --namespace <your-handle> --check # every gate, no upload
|
||
CTXPKG_TOKEN=ctxp_… lean-ctx addon publish --namespace <your-handle>
|
||
```
|
||
|
||
`publish` refuses locally what the registry would refuse remotely — schema
|
||
errors, a missing runnable `[mcp]` endpoint, an empty description, and every
|
||
blocking audit finding (shell-exec wiring, non-HTTPS endpoints, malware
|
||
heuristics, under-declared capabilities). A `review`-level audit publishes
|
||
with the findings disclosed. After that, anyone installs it with:
|
||
|
||
```bash
|
||
lean-ctx addon add <your-handle>/my-addon
|
||
```
|
||
|
||
**Curated default catalog.** For the addons every lean-ctx binary should know
|
||
about offline, open a merge request adding your manifest as an entry to
|
||
`rust/data/addon_registry.json`:
|
||
|
||
```json
|
||
{
|
||
"addon": {
|
||
"name": "my-addon",
|
||
"display_name": "My Addon",
|
||
"description": "What it does, in one line.",
|
||
"author": "you",
|
||
"homepage": "https://github.com/you/my-addon",
|
||
"license": "Apache-2.0",
|
||
"categories": ["workflow"],
|
||
"keywords": ["plans", "macros"],
|
||
"min_lean_ctx": "3.8.0"
|
||
},
|
||
"mcp": {
|
||
"transport": "stdio",
|
||
"command": "my-addon-mcp",
|
||
"args": ["serve"]
|
||
}
|
||
}
|
||
```
|
||
|
||
Before opening the merge request, validate and canonicalize the registry
|
||
locally — the same bar CI enforces. The registry files are **generated
|
||
snapshots**: `gen_registry` sorts entries by name and writes one canonical
|
||
form, and CI fails on any byte drift, so hand-edits can't diverge:
|
||
|
||
```bash
|
||
lean-ctx addon registry validate rust/data/addon_registry.json
|
||
cargo run --example gen_registry --features dev-tools # canonicalize in place
|
||
```
|
||
|
||
Once merged, everyone can run `lean-ctx addon add my-addon`, and your addon
|
||
appears on the website's Addons page.
|
||
|
||
> **Not ready to publish an endpoint yet?** Submit a *listed* entry — the
|
||
> `[addon]` table without an `[mcp]` block. It shows up in the registry and on
|
||
> the website and links to your homepage; `addon add` points users there until
|
||
> you ship the endpoint, then adding the `mcp` block flips it to one-click
|
||
> installable.
|
||
|
||
### 5. Sell your addon (optional)
|
||
|
||
Add a `[pricing]` block to make your addon a paid artifact — the same commerce
|
||
rails that already sell context packs:
|
||
|
||
```toml
|
||
[pricing]
|
||
price_cents = 1900 # $19.00 one-time
|
||
currency = "usd"
|
||
# or usage-metered, billed per tool call:
|
||
# model = "usage"
|
||
# usage_price_per_1k_cents = 200 # $2.00 per 1,000 calls
|
||
```
|
||
|
||
A paid addon must clear the **paid-listing gate** before it can be sold — this is
|
||
deliberate: buyers of third-party code get App-Store-level assurance. The gate
|
||
requires:
|
||
|
||
- a **pass** audit that is **paid-eligible** (declared + coherent
|
||
`[capabilities]`, and a pinned `sha256` for stdio addons),
|
||
- a **verified-publisher** entry, and
|
||
- well-formed pricing.
|
||
|
||
Check exactly where you stand any time:
|
||
|
||
```bash
|
||
lean-ctx addon audit ./lean-ctx-addon.toml # shows pricing + paid-listing gate
|
||
```
|
||
|
||
If blocked, the audit lists the precise remaining steps (pin your binary, apply
|
||
for verification, declare capabilities). Free addons are unaffected — the gate
|
||
only governs paid artifacts.
|
||
|
||
## Build *on* lean-ctx from inside your addon (`lean-ctx call`)
|
||
|
||
Your addon can call lean-ctx's own tools — read, search, symbol/outline, refactor
|
||
and the rest — by shelling out to `lean-ctx call`. This is the simplest, most
|
||
robust integration path and works from **any language**:
|
||
|
||
```bash
|
||
lean-ctx call <tool> --project-root <root> --json '<args>'
|
||
```
|
||
|
||
- **Stateless** — each call is a fresh, short-lived process; one error = one exit
|
||
code, trivially retryable. No server, no warm connection, no endpoint
|
||
discovery — it only needs `lean-ctx` on `PATH`.
|
||
- **No `tool_profile` precondition** — `call` builds the tool registry itself and
|
||
dispatches to *any* tool, independent of any running server's profile (unlike
|
||
the MCP path, where the code-intel `ctx_*` tools require `tool_profile = power`).
|
||
- **Always pass `--project-root`** — `call` resolves a `path` argument against it
|
||
(and pins `"."`/`""` to the root), so tools operate on your project, never the
|
||
process CWD.
|
||
|
||
```jsonc
|
||
// example: ask lean-ctx to read a file, compressed
|
||
lean-ctx call ctx_read --project-root /repo --json '{"path":"src/main.rs","mode":"signatures"}'
|
||
```
|
||
|
||
### Declare it: the callback capability block
|
||
|
||
Spawning `lean-ctx` is subprocess execution, so a callback addon should declare
|
||
`exec` — it's how the audit and the install consent reflect what the addon does.
|
||
Recommended block:
|
||
|
||
```toml
|
||
[capabilities]
|
||
network = "none" # local code-intel needs no internet
|
||
filesystem = "read_write" # the lean-ctx child writes its session cache
|
||
exec = ["lean-ctx"] # may spawn exactly lean-ctx
|
||
```
|
||
|
||
Two gotchas, because the spawned `lean-ctx call` **inherits your addon's
|
||
sandbox**:
|
||
|
||
- **Cache writes.** Under `filesystem = "read_only"`, the child's writes to its
|
||
data dir are blocked (only a scratch tmp is writable) — output still returns,
|
||
but caching degrades. Either declare `filesystem = "read_write"` **or** point
|
||
the child at a writable tmp with `LEANCTX_DATA_DIR=/tmp/lean-ctx-<addon>`.
|
||
- **Write tools.** `ctx_refactor` and friends modify files; if your addon
|
||
applies (not just previews) them, it needs `filesystem = "read_write"`.
|
||
|
||
`exec` is a **declared + audited** capability — not OS-enforced on any platform.
|
||
What's enforced is the network/filesystem sandbox, which the spawned `lean-ctx`
|
||
**inherits** (so the callback can't exfiltrate or tamper either). Declaring
|
||
`exec = ["lean-ctx"]` keeps the audit honest and shows the user exactly what the
|
||
addon does (see
|
||
[`addon-manifest-v1`](../contracts/addon-manifest-v1.md)).
|
||
|
||
## How it works
|
||
|
||
- Installing writes a `[[gateway.servers]]` entry to your global `config.toml`
|
||
and records the addon in `<data_dir>/addons/installed.json`. The gateway is
|
||
**global-only** and opt-in — an untrusted project can never wire a server.
|
||
- `remove` drops exactly the gateway server the addon installed. It leaves the
|
||
gateway enabled; turn it off with `lean-ctx config set gateway.enabled false`.
|
||
- Everything is local and deterministic: no network calls or telemetry in the
|
||
add/list/search/info/remove paths.
|
||
- **Output pipeline (opt-in).** Once a call returns, the gateway redacts secrets,
|
||
then — if the deep-integration flags are set — runs the output through L1–L4
|
||
(see [Why an addon goes deeper](#why-an-addon-goes-deeper-than-a-passthrough)).
|
||
Installing a categorized addon records its `integration` slug in the
|
||
`[[gateway.servers]]` entry, so routing needs no catalog lookup on the hot path.
|
||
|
||
### Discover & measure
|
||
|
||
```bash
|
||
lean-ctx addon search plans # full-text search; [verified] addons are badged
|
||
lean-ctx addon categories # browse by category, with live counts
|
||
lean-ctx addon usage # per-addon / per-tool call counters (local meter)
|
||
```
|
||
|
||
`addon usage` reads the local meter (`<data_dir>/addons/usage.json`): every
|
||
gateway tool call is attributed to its addon + tool, so you can see what you
|
||
actually rely on. It is local-only and a pure side-channel — it never changes a
|
||
tool's output. Turn it off with `lean-ctx config set addons.metering false`.
|
||
|
||
## Security & trust
|
||
|
||
An addon runs real code with your privileges (stdio) or sends context to a remote
|
||
endpoint (http), so lean-ctx makes installing one a disclosed, policy-gated
|
||
action. Full model: the [contract](../contracts/addon-manifest-v1.md#security-model).
|
||
|
||
- **Trust tier.** Catalog entries are **verified** (maintainer-audited) or
|
||
**community** (installable, unaudited). The tier shows in `addon list`,
|
||
`addon info` and the install preview.
|
||
- **Risk review.** Before install, lean-ctx prints a security review of the
|
||
wiring — remote endpoints, shelling out, unpinned upstreams, secret-bearing env
|
||
— so you see what an addon can do before you say yes.
|
||
- **Capabilities.** An addon that declares `[capabilities]` runs under a
|
||
per-addon OS sandbox + environment allowlist derived from exactly those
|
||
permissions — secure-by-default, shown to you before install.
|
||
- **Audit gate.** `lean-ctx addon audit` (and the registry validator) flags any
|
||
addon whose declared capabilities don't match its wiring, and scans for malware
|
||
patterns (pipe-to-shell, base64-decode→exec, persistence writes). A `fail`
|
||
verdict bars a listing; verified/paid entries must pass cleanly, declare
|
||
coherent capabilities, and pin their binary.
|
||
- **Binary pin.** A stdio addon can pin its binary's `sha256`; the gateway hashes
|
||
the resolved executable before spawn and refuses a swap (fail-closed).
|
||
- **Untrusted output.** An addon's tool output is redacted for secrets and
|
||
audit-tagged as untrusted before it reaches the model.
|
||
- **Kill-switch.** `lean-ctx addon revoke <name>` blocks an addon from running
|
||
everywhere — install, the gateway catalog, and every call — without waiting for
|
||
an uninstall. `unrevoke` lifts it; `revocations` lists active blocks.
|
||
- **Integrity lock.** Install pins a hash of the exact wiring. `lean-ctx addon
|
||
verify` re-checks it against your live config and flags drift — a swapped
|
||
command, an extra arg, or a widened capability after install.
|
||
|
||
### Lock it down (teams / enterprise)
|
||
|
||
The global-only `[addons]` block sets a floor an untrusted repo can't loosen:
|
||
|
||
```bash
|
||
# only install maintainer-verified addons
|
||
lean-ctx config set addons.policy verified_only
|
||
|
||
# or restrict to an explicit allowlist
|
||
lean-ctx config set addons.policy allowlist
|
||
lean-ctx config set addons.allowlist my-addon,other-addon
|
||
|
||
# refuse anything with a high-risk capability
|
||
lean-ctx config set addons.block_risky true
|
||
|
||
# sandbox spawned addon servers without a [capabilities] block
|
||
# (macOS sandbox-exec / Linux bwrap)
|
||
lean-ctx config set addons.sandbox strict
|
||
|
||
# fail closed if a declared-capability addon can't be sandboxed
|
||
lean-ctx config set addons.enforce_capabilities true
|
||
|
||
# require a signed user-override registry (trusted org key)
|
||
lean-ctx config set addons.require_signature true
|
||
|
||
lean-ctx config schema addons # inspect every key
|
||
```
|
||
|
||
Distribute these via MDM / config-management, or pin them through the signed
|
||
org-policy floor (`policy org`) to make them un-bypassable.
|
||
|
||
## Troubleshooting
|
||
|
||
```bash
|
||
lean-ctx addon list # is it installed? which gateway server?
|
||
lean-ctx config schema gateway # inspect gateway config keys
|
||
lean-ctx status # MCP server / gateway status
|
||
```
|
||
|
||
If a freshly installed addon's tools do not appear, restart your MCP client so
|
||
it re-reads the gateway catalog.
|