Files
yvgude--lean-ctx/docs/reference/13-security-and-governance.md
T
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:35:30 +08:00

383 lines
18 KiB
Markdown

# Journey 13 — Security & Governance
> You're putting lean-ctx in front of real code — possibly on a shared machine,
> in CI, or under a security policy. This journey covers every guardrail lean-ctx
> applies by default and every dial you can tighten: filesystem jail, shell
> allowlist, secret redaction, OS sandboxing, harden mode, and role policies.
Source files:
- `rust/src/core/pathjail.rs` — filesystem boundary (PathJail)
- `rust/src/core/shell_allowlist.rs` — command allowlist
- `rust/src/core/secret_detection.rs` — secret scanning & redaction
- `rust/src/core/sandbox.rs`, `sandbox_seatbelt.rs`, `sandbox_landlock.rs` — OS sandbox
- `rust/src/core/tcc_guard_sandbox.rs` — macOS launchd seatbelt wrapper (deny `~/Documents`, #356)
- `rust/src/cli/harden.rs``harden` (soft/hard/undo)
- `rust/src/core/context_policies.rs` — role policies
- `rust/src/core/owasp_alignment.rs`, `audit_trail.rs` — alignment & audit
---
## 0. The defense-in-depth model
Every file read and shell command flows through layered guardrails, **on by
default** — you don't opt in:
```mermaid
flowchart LR
R[ctx_read / ctx_shell] --> PJ[PathJail<br/>stay in project root]
PJ --> AL[Shell allowlist<br/>~200 known binaries]
AL --> SB[OS sandbox<br/>Seatbelt / Landlock]
SB --> SD[Secret redaction<br/>before output leaves]
SD --> OUT[result to the agent]
```
You can tighten each layer (stricter shell parsing, harden mode, role policies)
but you cannot accidentally turn off the baseline.
---
## 0.1 See and flip the whole posture — `lean-ctx security`
lean-ctx's guardrails fall into **two independent planes**. Keeping them separate
is deliberate: a usability-first user can let the agent do anything on a trusted
machine **without** ever leaking secrets to the model provider.
| Plane | Protects | Controls |
|-------|----------|----------|
| **Containment** | your *machine* from the agent | PathJail + shell gating |
| **Secret defense** | your *secrets* from the LLM provider | `.env` / credential redaction |
One command shows the live posture and how to change it:
```bash
lean-ctx security status # posture board: jail, shell, secret redaction
```
Two master switches flip **containment** in one step — secret redaction is never
touched by them:
```bash
lean-ctx yolo # OPEN: any path + any command (containment off)
lean-ctx secure # STRICT: restore the secure defaults
```
`yolo` writes `path_jail = false` and `shell_security = "off"` to your global
config and takes effect immediately (no restart). It is fully reversible, and you
re-enable pieces **granularly** afterwards — the array/blanket dichotomy works in
both directions:
```bash
lean-ctx config set shell_security warn # log violations, block nothing
lean-ctx config set path_jail true # re-enable the filesystem jail
lean-ctx allow acli # permit one extra command (additive)
```
Secret/`.env` redaction is a **separate** toggle — `yolo` leaves it on:
```bash
lean-ctx security secrets off # stop masking secrets (NOT recommended)
lean-ctx security secrets on # re-enable masking
```
`lean-ctx security status` (and `lean-ctx doctor`) print a coarse label —
**STRICT** (both planes enforced), **RELAXED** (partially loosened), or **OPEN**
(containment fully off) — so the active posture can never hide.
> Mental model: **containment** is "what the agent may touch on this machine";
> **secret defense** is "what may leave this machine for the provider". `yolo`
> only drops the first.
---
## 1. PathJail — stay inside the project
**What it does:** confines file access to the resolved project root. Absolute
paths outside the jail are rejected, so a stray `read /etc/passwd` or a
path-traversal `../../` cannot escape the workspace.
- Re-rooting to a different project root is **off by default**
(`allow_auto_reroot = false`) — lean-ctx will not silently follow an absolute
path into another tree.
- Multi-root setups (`lean-ctx serve --root a:A --root b:B`) jail each root
independently (`server/multi_path.rs`).
- **Widen it (array):** add trusted roots with `allow_paths` / `extra_roots`
(or `LEAN_CTX_ALLOW_PATH`) — reads/writes resolve under those prefixes too.
- **Disable it (blanket):** set `path_jail = false` to allow *any* path — the
blanket equivalent of `allow_paths = ["/"]`, for containers/sandboxes where the
boundary is already external. `lean-ctx yolo` sets this for you, and
`lean-ctx doctor` flags it loudly while it is active.
This is the foundation every other layer assumes.
---
## 1.5 Workspace trust — gate untrusted project-local config
**What it does:** a cloned repo ships its own `.lean-ctx.toml`, which is merged
over your global config. That merge can raise **security-sensitive** settings —
replace the `shell_allowlist`, widen the jail via `allow_paths` / `extra_roots`,
repoint a `proxy.*_upstream`, define `custom_aliases`, or flip
`rules_scope` / `rules_injection`. lean-ctx treats those as *trusted-only*.
For a workspace you have **not** trusted, the sensitive overrides are **withheld**
(comfort knobs like `compression_level` / `theme` still apply) and a `[SECURITY]`
warning is logged. Grant trust explicitly:
```bash
lean-ctx trust # trust the current project root
lean-ctx trust status # show trust state + which overrides are gated
lean-ctx trust --list # list all trusted workspaces
lean-ctx untrust # revoke trust for the current root
```
Trust is pinned to **both** the workspace path **and** a content hash of
`.lean-ctx.toml` — editing the file after trust re-gates it, so a "trust once,
modify later" change can't take effect silently. `lean-ctx doctor` shows the
state. Headless / fleet environments can opt in without a prompt via
`LEAN_CTX_TRUST_WORKSPACE=1` or a `LEAN_CTX_TRUSTED_ROOTS=/path/a,/path/b` list.
> Review a clone's `.lean-ctx.toml` before you `lean-ctx trust` it — trusting is
> what lets its security-sensitive settings widen lean-ctx's own boundaries.
---
## 2. Shell allowlist & strict mode
**What it does:** the shell hook only compresses/executes commands whose binary
is on the allowlist (~200 common dev tools: `git`, `cargo`, `npm`, `node`,
`python`, …). Anything else passes through untouched rather than being wrapped.
**Pipelines & chains (`|`, `&&`, `||`, `;`):** a compound command is wrapped as a
*single* `lean-ctx -c "<whole>"` only when **every** stage is gate-clean — then
the pipe/chain runs natively inside one profile-free shell and just the *final*
output is compressed (so `git log | wc -l` counts the raw log, not a digest). If
any stage is not gate-clean (an interpreter `-c`, a non-allowlisted sink such as
`kubectl`, …) the **whole** command is left raw for the agent's shell. lean-ctx
never wraps only part of a pipe (which would compress mid-pipe data the next stage
still needs) and never relaxes the gate to wrap a tricky sink — the rewrite can
only ever route commands the same chokepoint would already allow.
```toml
# config.toml
shell_allowlist = ["git", "cargo", "npm", "…"] # REPLACE the default set
shell_allowlist_extra = ["just", "task"] # ADD to the default set
shell_strict_mode = false # set true to block $() and backticks
excluded_commands = [] # never intercept these
```
- **`docker` / `podman` are deliberately *not* in the default allowlist** —
mount flags can bypass PathJail. Add them explicitly only if you accept that.
- `shell_strict_mode = true` blocks command substitution (`$(…)`, backticks) for
environments that must forbid dynamic command construction.
- Replace the whole list via `LEAN_CTX_SHELL_ALLOWLIST` (comma-separated), or
just **add** a few extras with `shell_allowlist_extra`. The
`lean-ctx allow <cmd>` CLI edits `shell_allowlist_extra` for you, and
`lean-ctx allow --list` prints the effective allowlist plus any parse errors so
a typo can never silently drop your overrides.
### 2.1 Shell-security mode (`enforce` | `warn` | `off`)
One switch governs **all** command gating — the allowlist *and* the hard blocks
(`eval`/`exec`/`source`, `$()`/backticks at command position, interpreter `-c`).
It is applied at a single chokepoint, so MCP `ctx_shell` and the CLI
(`lean-ctx -c` / `-t`) behave identically.
```toml
# config.toml
shell_security = "enforce" # default — secure
# shell_security = "warn" # run the checks, log violations via tracing, never block
# shell_security = "off" # skip command gating entirely — compression stays active
```
| Mode | Allowlist | Hard blocks (`eval`, `$()`, …) | Compression |
|---|---|---|---|
| `enforce` (default) | enforced | blocked | on |
| `warn` | logged only | logged only | on |
| `off` | skipped | skipped | **on** |
- **Resolution:** `LEAN_CTX_SHELL_SECURITY` env → `shell_security` in config →
default `enforce`. An unknown value falls back to `enforce` (never fails open).
- **Why `enforce` is the default, even for beginners:** lean-ctx mediates the
agent's shell, so a weaker default would silently downgrade security for every
install on upgrade. In practice the default allowlist already covers normal
workflows (`git`/`cargo`/`npm`/`node`/`python`/…), so beginners rarely hit it —
the friction is for power users running exotic tools, who opt into `off`.
- **`off` is the "YOLO" escape hatch.** It disables *command gating* only; it does
**not** lift the read-only-output doctrine in `ctx_shell` (no `>`/`tee`/heredoc
file writes — use the native write tool). Compression is unaffected in every mode.
Set it together with `path_jail = false` in one step via `lean-ctx yolo`
(and revert both with `lean-ctx secure`).
- `lean-ctx doctor` surfaces the active mode whenever it is not `enforce`, so a
relaxed posture can never hide.
- This supersedes the CLI-only `LEAN_CTX_ALLOWLIST_WARN_ONLY` (which still works
for backward compatibility); `shell_security` is the canonical, global switch.
---
## 3. OS sandboxing for executed code
**What it does:** when lean-ctx executes code (`ctx_execute`), it runs under the
OS sandbox — **Seatbelt** on macOS and **Landlock** on Linux — so the executed
process gets a restricted filesystem/network view, not your full user
privileges. On platforms without a supported sandbox, execution is gated rather
than run unconfined.
This is separate from PathJail (which guards lean-ctx's *own* reads); the sandbox
guards *child processes* lean-ctx spawns on your behalf.
### 3.1 launchd seatbelt — no macOS "Documents" prompt (#356)
On macOS the daemon, proxy and auto-updater run as LaunchAgents — i.e. as their
own TCC identity (`ppid 1`), which does **not** inherit your terminal's or
editor's privacy grants. Any access they make under `~/Documents`, `~/Desktop`
or `~/Downloads` would pop the "lean-ctx would like to access files in your
Documents folder" prompt, and because every release re-signs the binary a
granted permission is voided on the next update — so the prompt returns forever.
lean-ctx therefore bakes a **deny-`~/Documents` Seatbelt profile** into the
LaunchAgent invocation itself (`rust/src/core/tcc_guard_sandbox.rs`): the plist
runs `sandbox-exec -p '(allow default) (deny file-read* file-write* …)' lean-ctx
…`. The kernel refuses any access to the three protected directories *silently*
with `EPERM` — the TCC subsystem is never consulted, so the prompt can never
appear and no "Allow" is ever required. Everything outside those directories
stays permitted, so the processes keep full functionality. The path guards
(`pathutil::may_probe_path`) and the optional stable code-signing identity
(`lean-ctx codesign-setup`) remain underneath as defense-in-depth. Verified by
`rust/tests/tcc_sandbox.sh`, which boots the daemon under the production wrapper.
---
## 4. Secret redaction — nothing leaks to the model
**What it does:** before any shell output or file content is returned, lean-ctx
scans it for credentials (AWS keys, tokens, etc.) and replaces matches with
`[REDACTED:<kind>]`. It is **on by default**.
```toml
[secret_detection]
enabled = true
redact = true
custom_patterns = ["MYCORP_[A-Z0-9]{20}"] # add org-specific secret shapes
exclude_patterns = ["LCTX_PUBLIC_\\w+"] # carve out known-safe matches (subtractive)
```
Built-in patterns cover common cloud/credential formats; `custom_patterns` lets
you redact organization-specific secret shapes. Matches are reported with a safe
preview (e.g. `AKIA…`) so you know redaction fired without seeing the secret.
`exclude_patterns` is the subtractive counterpart (#718): a detected match
covered by any of these regexes is neither reported nor redacted — carve out a
repo's own naming conventions without disabling secret detection wholesale.
Since v3.9.2 the detector also skips benign non-values on its own: unquoted
identifier/property references (`serverEnv.getStripeSecretKey`), camelCase
subwords (`superuserPassword:` as a key), and obvious placeholders
(`ghp_change_me`, `your_key_here`). Quoted string literals and digit-bearing
values remain protected.
### 4.1 Sensitivity policy floor — per-item levels
Where `[secret_detection]` masks *known credential shapes*, the **sensitivity
floor** classifies every item by a level and enforces one uniform **policy
floor** just before content reaches the model. It is **off by default** (fully
no-op) and covers both tool outputs and injected knowledge facts.
```toml
[sensitivity]
enabled = true # off by default → no-op
policy_floor = "secret" # public < internal < confidential < secret
action = "redact" # "redact" masks the spans, "drop" withholds the item
```
| Level | Raised by (high-precision signals only) |
|-------|------------------------------------------|
| `secret` | secret-like paths (`.env`, `.ssh/…`, `*.pem`) or detected credentials |
| `confidential` | Luhn-validated card numbers, mod-97-validated IBANs |
| `internal` | reserved for explicit tagging |
| `public` | default |
Anything classified **at or above** `policy_floor` is dropped or redacted: with
`action = "drop"` the whole item is replaced by a short notice; with `redact`
only the offending spans are masked. Knowledge facts store their level at
creation (`KnowledgeFact.sensitivity`) and are re-checked at injection time, so a
floor change takes effect immediately. The classifier uses **only high-precision
signals** (no speculative heuristics) to avoid false positives; `LEAN_CTX_SENSITIVITY=0|1`
toggles enforcement for a single run. This section lives in the **global**
`~/.lean-ctx/config.toml` only — an untrusted project file cannot lower the floor.
---
## 5. Harden mode — force the compressed path
**What it does:** `harden` makes the agent use lean-ctx's compressed `ctx_*`
tools by **denying native Read/Grep** (except immediately after an Edit). Two
levels:
```bash
lean-ctx harden # soft: sets LEAN_CTX_HARDEN=1 in MCP configs
lean-ctx harden --hard # also adds "Bash" to ~/.claude/settings.json deny
lean-ctx harden --undo # remove both — native tools allowed again
```
Real output:
```text
lean-ctx harden (level: soft)
[OK] /Users/you/.cursor/mcp.json
[OK] /Users/you/.claude.json
[OK] Set LEAN_CTX_HARDEN=1 in MCP configs
Harden active. Native Read/Grep will be denied (except after Edit).
Undo with: lean-ctx harden --undo
```
Soft harden is reversible and config-only; `--hard` additionally blocks the
Claude Bash tool. Both are fully undone by `--undo` (see also
[Journey 12 → §4](12-troubleshooting.md)).
---
## 6. Role policies — least privilege per agent
**What it does:** role policies (`context_policies.rs`, set via
`ctx_session action=role` / `lean-ctx session role`) scope what a session may do
— e.g. a `reviewer` role that cannot write, or limiting privileged search
(`ctx_search ignore_gitignore=true` requires an admin-class role). Use this to
give a sub-agent or teammate a least-privilege surface.
---
## 7. Audit & alignment
lean-ctx maintains an **audit trail** (`audit_trail.rs`) of security-relevant
actions and ships an **OWASP alignment** map (`owasp_alignment.rs`) documenting
which controls address which risks — useful when answering a security review.
For a structured external yardstick, lean-ctx publishes a self-assessment
against the 32-control [Context Governance Benchmark](https://github.com/yvgude/context-governance-benchmark)
(v1.0-draft): graded **C2 — Managed** with declared gaps — see
[docs/compliance/cgb-self-assessment.md](../compliance/cgb-self-assessment.md).
---
## Governance checklist
| Goal | Control |
|------|---------|
| See / flip the whole posture | `lean-ctx security status` · `yolo` · `secure` |
| Disable containment for a trusted machine | `lean-ctx yolo` (keeps secret redaction on) |
| Keep file access in-project | PathJail (on by default) |
| Gate a cloned repo's `.lean-ctx.toml` | Workspace trust (`lean-ctx trust`) |
| Restrict which commands run | `shell_allowlist` + `shell_strict_mode` |
| Never wrap docker mount-escapes | docker/podman off the default allowlist |
| Confine executed code | Seatbelt (macOS) / Landlock (Linux) |
| Stop secrets reaching the model | `[secret_detection]` (on by default) |
| Block whole sensitivity levels (PII/secret) pre-prompt | `[sensitivity]` policy floor (off by default) |
| Force compressed reads | `lean-ctx harden [--hard]` |
| Least-privilege agents | role policies |
| Answer a security review | audit trail + OWASP alignment |
> Tuning *how much* lean-ctx compresses or *which tools* it exposes lives in
> [Journey 10 — Customization & Governance](10-customization-and-governance.md);
> this journey is specifically the **security** surface.