lean-ctx 3.9.16

Context Runtime for AI Agents with CCP. 71 MCP tools, 10 read modes, 95+ compression patterns, cross-session memory (CCP), persistent AI knowledge with temporal facts + contradiction detection, multi-agent context sharing, LITM-aware positioning, AAAK compact format, adaptive compression with Thompson Sampling bandits. Supports 24+ AI tools. Reduces LLM token consumption by up to 99%.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
//! Declared capability model for addons (P1 — platform keystone).
//!
//! An addon's optional `[capabilities]` block tells lean-ctx exactly what the
//! addon needs: outbound network, filesystem writes, and which host environment
//! variables it may receive. The declaration is **secure-by-default** — an
//! addon that declares a `[capabilities]` block but omits a field gets the most
//! restrictive value (no network, read-only filesystem, scrubbed environment).
//!
//! A declared block drives two real, *enforced* controls at the single gateway
//! spawn point ([`crate::core::mcp_catalog::client`]):
//!
//! 1. the per-addon OS sandbox profile ([`super::sandbox`]) — network egress and
//!    filesystem writes are wrapped via `sandbox-exec` (macOS) / `bwrap` (Linux),
//! 2. the environment allowlist — host secrets never reach the child unless the
//!    addon explicitly lists the variable name,
//!
//! and is surfaced to the user for explicit consent at install time
//! ([`crate::cli::addon_cmd`]). Child processes inherit the OS sandbox, so a
//! subprocess an addon spawns is bound by the same network/filesystem limits;
//! the declared `exec` capability is therefore disclosed + audited rather than
//! OS-enforced (see [`super::sandbox`]).
//!
//! Unlike the legacy blanket `addons.sandbox` mode, this is *per addon* and
//! bound to the manifest, so a marketplace addon is granted exactly what it
//! asked for — no more. Addons **without** a `[capabilities]` block keep the
//! legacy behaviour (governed by `addons.sandbox`) so existing installs do not
//! change.

use serde::{Deserialize, Serialize};

/// Host environment variables a scrubbed child is always allowed to see, on top
/// of whatever the addon declares. Chosen to let normal programs start (binary
/// resolution, locale, temp dir) without exposing ambient secrets. Reuses the
/// plugin allowlist so the two sandboxes converge on one list (P0).
pub use crate::core::plugins::sandbox::ENV_ALLOWLIST as BASE_ENV_ALLOWLIST;

/// Outbound-network capability a stdio addon declares.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum NetworkAccess {
    /// No outbound network. The default — most local tools never need it, and
    /// blocking egress is the single highest-value sandbox control.
    #[default]
    None,
    /// Full outbound network (the addon talks to the internet / remote APIs).
    Full,
}

impl NetworkAccess {
    #[must_use]
    pub fn as_str(self) -> &'static str {
        match self {
            Self::None => "none",
            Self::Full => "full",
        }
    }

    /// Whether the OS sandbox should permit outbound network.
    #[must_use]
    pub fn allowed(self) -> bool {
        matches!(self, Self::Full)
    }
}

/// Filesystem capability a stdio addon declares.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum FilesystemAccess {
    /// Read-only filesystem; writes restricted to a scratch tmp. The default.
    #[default]
    ReadOnly,
    /// Read-write filesystem (the addon needs to write outside tmp).
    ReadWrite,
}

impl FilesystemAccess {
    #[must_use]
    pub fn as_str(self) -> &'static str {
        match self {
            Self::ReadOnly => "read_only",
            Self::ReadWrite => "read_write",
        }
    }

    /// Whether the OS sandbox should permit filesystem writes.
    #[must_use]
    pub fn writable(self) -> bool {
        matches!(self, Self::ReadWrite)
    }
}

/// Subprocess-execution capability a stdio addon declares.
///
/// Modeled as an untagged enum so the manifest can write either a mode string
/// or a binary allowlist:
///
/// ```toml
/// exec = "none"              # block all child process execution (default)
/// exec = "full"             # may execute any binary
/// exec = ["lean-ctx", "git"] # may execute exactly these binaries (by name/path)
/// ```
///
/// `exec` is a **declared, audited and consented** capability — it is *not*
/// OS-enforced (see [`super::sandbox`]): path-allowlisting `execve` is not
/// portable (`bwrap`/seccomp cannot do it) and breaks interpreted servers,
/// whose own interpreter chain is itself a `process-exec`. The real data-safety
/// guarantees come from the network/filesystem sandbox, which child processes
/// inherit — so a subprocess an addon spawns still cannot exfiltrate or tamper.
/// Declaring `exec` keeps the audit honest (an addon that shells out must say
/// so) and is surfaced for consent at install on every platform.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ExecAccess {
    /// A bare mode: `"none"` (block all child exec) or `"full"` (unrestricted).
    Mode(ExecMode),
    /// An explicit allowlist of binary names / absolute paths the addon may
    /// `execve`. An empty list is equivalent to [`ExecMode::None`].
    Allowlist(Vec<String>),
}

/// The two bare exec modes (the non-allowlist forms of [`ExecAccess`]).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ExecMode {
    /// No child process execution. The default — most addons never spawn
    /// subprocesses, and arbitrary `execve` is the highest-impact escape.
    #[default]
    None,
    /// May execute any binary (the legacy, unrestricted behaviour).
    Full,
}

impl Default for ExecAccess {
    fn default() -> Self {
        Self::Mode(ExecMode::None)
    }
}

impl ExecAccess {
    /// Whether the addon is permitted to execute *any* child process at all.
    /// `full` and a non-empty allowlist are permissive; `none` / empty list are
    /// not.
    #[must_use]
    pub fn allowed(&self) -> bool {
        match self {
            Self::Mode(ExecMode::Full) => true,
            Self::Mode(ExecMode::None) => false,
            Self::Allowlist(list) => !list.is_empty(),
        }
    }

    /// Whether this is a restricted declaration (`none` or an allowlist) vs.
    /// blanket `full`. Drives the audit/consent disclosure, not OS enforcement.
    #[must_use]
    pub fn is_restricted(&self) -> bool {
        !matches!(self, Self::Mode(ExecMode::Full))
    }

    /// The declared allowlist of binaries, if any (`full`/`none` have none).
    #[must_use]
    pub fn allowlist(&self) -> &[String] {
        match self {
            Self::Allowlist(list) => list,
            Self::Mode(_) => &[],
        }
    }

    /// Short human label for the consent preview.
    #[must_use]
    fn label(&self) -> String {
        match self {
            Self::Mode(ExecMode::None) => "none (no subprocesses)".to_string(),
            Self::Mode(ExecMode::Full) => "full (any binary)".to_string(),
            Self::Allowlist(list) if list.is_empty() => "none (empty allowlist)".to_string(),
            Self::Allowlist(list) => format!("only {}", list.join(", ")),
        }
    }
}

/// `[capabilities]` — what an addon is permitted to do. A present-but-empty
/// block resolves to the strictest profile (see module docs). Secure-by-default.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct AddonCapabilities {
    /// Outbound network access.
    pub network: NetworkAccess,
    /// Filesystem access.
    pub filesystem: FilesystemAccess,
    /// Host environment variable names the addon may receive, in addition to
    /// [`BASE_ENV_ALLOWLIST`]. Anything not listed is scrubbed before spawn, so
    /// ambient secrets never reach the child process.
    pub env: Vec<String>,
    /// Subprocess-execution permission. Defaults to [`ExecMode::None`] inside a
    /// declared block (secure-by-default): an addon that spawns child processes
    /// (e.g. shells out, or calls back into `lean-ctx call`) must declare it.
    pub exec: ExecAccess,
}

impl AddonCapabilities {
    /// True when the addon declares no elevated capability — the strictest,
    /// default profile and the safest to run.
    #[must_use]
    pub fn is_minimal(&self) -> bool {
        self.network == NetworkAccess::None
            && self.filesystem == FilesystemAccess::ReadOnly
            && self.env.is_empty()
            && !self.exec.allowed()
    }

    /// Whether the addon may execute child processes at all.
    #[must_use]
    pub fn exec_allowed(&self) -> bool {
        self.exec.allowed()
    }

    /// Whether the addon declares a restricted exec profile (`none` or an
    /// allowlist, vs. blanket `full`). Used by the audit + consent surface;
    /// `exec` is not OS-enforced (see [`super::sandbox`]).
    #[must_use]
    pub fn exec_restricted(&self) -> bool {
        self.exec.is_restricted()
    }

    /// Whether exec is a blanket `full` grant (vs. an allowlist or `none`). Used
    /// by the audit to nudge blanket grants toward least privilege.
    #[must_use]
    pub fn exec_is_blanket(&self) -> bool {
        matches!(self.exec, ExecAccess::Mode(ExecMode::Full))
    }

    /// Whether the OS sandbox should permit outbound network for this addon.
    #[must_use]
    pub fn network_allowed(&self) -> bool {
        self.network.allowed()
    }

    /// Whether the OS sandbox should permit filesystem writes for this addon.
    #[must_use]
    pub fn filesystem_writable(&self) -> bool {
        self.filesystem.writable()
    }

    /// Validate the declaration (fail-closed: a malformed declaration is a
    /// manifest error, not a silent grant). Env names must be plausible
    /// `[A-Za-z0-9_]` identifiers.
    pub fn validate(&self) -> Result<(), String> {
        for name in &self.env {
            let n = name.trim();
            if n.is_empty() || !n.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
                return Err(format!(
                    "capabilities.env entry `{name}` is not a valid environment variable name \
                     (use [A-Za-z0-9_])"
                ));
            }
        }
        for bin in self.exec.allowlist() {
            let b = bin.trim();
            if b.is_empty() || b.contains(char::is_whitespace) {
                return Err(format!(
                    "capabilities.exec entry `{bin}` is not a valid binary name or path \
                     (no whitespace, non-empty)"
                ));
            }
        }
        Ok(())
    }

    /// Human-readable lines for the install-consent preview. Always returns the
    /// three dimensions so the user sees exactly what they are granting.
    #[must_use]
    pub fn summary(&self) -> Vec<String> {
        let network = if self.network_allowed() {
            "full (outbound internet)"
        } else {
            "none (egress blocked)"
        };
        let filesystem = if self.filesystem_writable() {
            "read-write"
        } else {
            "read-only (+ scratch tmp)"
        };
        let env = if self.env.is_empty() {
            "scrubbed (base allowlist only)".to_string()
        } else {
            format!("+ {}", self.env.join(", "))
        };
        vec![
            format!("network:    {network}"),
            format!("filesystem: {filesystem}"),
            format!("env:        {env}"),
            format!("exec:       {}", self.exec.label()),
        ]
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn default_is_minimal_and_locked_down() {
        let caps = AddonCapabilities::default();
        assert!(caps.is_minimal());
        assert!(!caps.network_allowed());
        assert!(!caps.filesystem_writable());
        assert!(caps.env.is_empty());
        assert!(!caps.exec_allowed());
        assert!(caps.exec_restricted());
    }

    #[test]
    fn parses_declared_block() {
        let caps: AddonCapabilities = toml::from_str(
            "network = \"full\"\nfilesystem = \"read_write\"\nenv = [\"GITHUB_TOKEN\"]\n",
        )
        .expect("parse");
        assert!(caps.network_allowed());
        assert!(caps.filesystem_writable());
        assert_eq!(caps.env, vec!["GITHUB_TOKEN".to_string()]);
        assert!(!caps.is_minimal());
        // exec was omitted → secure-by-default (none).
        assert!(!caps.exec_allowed());
    }

    #[test]
    fn parses_exec_modes_and_allowlist() {
        let full: AddonCapabilities = toml::from_str("exec = \"full\"\n").expect("parse full");
        assert!(full.exec_allowed());
        assert!(!full.exec_restricted());
        assert!(full.exec.allowlist().is_empty());

        let none: AddonCapabilities = toml::from_str("exec = \"none\"\n").expect("parse none");
        assert!(!none.exec_allowed());
        assert!(none.exec_restricted());

        let allow: AddonCapabilities =
            toml::from_str("exec = [\"lean-ctx\", \"git\"]\n").expect("parse allowlist");
        assert!(allow.exec_allowed());
        assert!(allow.exec_restricted());
        assert_eq!(allow.exec.allowlist(), &["lean-ctx", "git"]);
        assert!(!allow.is_minimal());

        let empty: AddonCapabilities = toml::from_str("exec = []\n").expect("parse empty");
        assert!(!empty.exec_allowed(), "empty allowlist == none");
        assert!(empty.exec_restricted());
    }

    #[test]
    fn exec_allowlist_rejects_whitespace_entries() {
        let bad = AddonCapabilities {
            exec: ExecAccess::Allowlist(vec!["ok-bin".into(), "bad bin".into()]),
            ..Default::default()
        };
        assert!(bad.validate().is_err());
        let good = AddonCapabilities {
            exec: ExecAccess::Allowlist(vec!["/usr/bin/git".into(), "lean-ctx".into()]),
            ..Default::default()
        };
        assert!(good.validate().is_ok());
    }

    #[test]
    fn empty_block_resolves_to_strictest() {
        let caps: AddonCapabilities = toml::from_str("").expect("parse");
        assert!(caps.is_minimal());
    }

    #[test]
    fn unknown_enum_value_is_rejected() {
        let err = toml::from_str::<AddonCapabilities>("network = \"halfway\"\n");
        assert!(err.is_err(), "unknown network value must fail-closed");
    }

    #[test]
    fn validate_rejects_bad_env_names() {
        let bad = AddonCapabilities {
            env: vec!["OK_NAME".into(), "bad name".into()],
            ..Default::default()
        };
        assert!(bad.validate().is_err());
        let good = AddonCapabilities {
            env: vec!["GITHUB_TOKEN".into(), "API_KEY_2".into()],
            ..Default::default()
        };
        assert!(good.validate().is_ok());
    }

    #[test]
    fn summary_always_lists_all_dimensions() {
        let s = AddonCapabilities::default().summary();
        assert_eq!(s.len(), 4);
        assert!(s[0].contains("none"));
        assert!(s[1].contains("read-only"));
        assert!(s[2].contains("scrubbed"));
        assert!(s[3].contains("exec") && s[3].contains("none"));

        let elevated = AddonCapabilities {
            network: NetworkAccess::Full,
            filesystem: FilesystemAccess::ReadWrite,
            env: vec!["TOKEN".into()],
            exec: ExecAccess::Allowlist(vec!["lean-ctx".into()]),
        };
        let s = elevated.summary();
        assert!(s[0].contains("full"));
        assert!(s[1].contains("read-write"));
        assert!(s[2].contains("TOKEN"));
        assert!(s[3].contains("lean-ctx"));
    }

    #[test]
    fn as_str_roundtrips() {
        assert_eq!(NetworkAccess::None.as_str(), "none");
        assert_eq!(NetworkAccess::Full.as_str(), "full");
        assert_eq!(FilesystemAccess::ReadOnly.as_str(), "read_only");
        assert_eq!(FilesystemAccess::ReadWrite.as_str(), "read_write");
    }
}