lean-ctx 3.9.13

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
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
//! Opt-in OS sandbox for the stdio MCP servers an addon spawns (#865).
//!
//! A stdio addon is a child process with the user's full privileges. When
//! `addons.sandbox` is enabled, lean-ctx wraps that child in an OS-native
//! sandbox launcher before spawning it (the single spawn point is
//! [`crate::core::mcp_catalog::client`]):
//!
//! - **macOS** → `sandbox-exec` with a generated SBPL profile,
//! - **Linux** → `bwrap` (bubblewrap) with a read-only root + network unshare.
//!
//! Local stdio tools rarely need the network, so the highest-value, lowest-
//! breakage control is **outbound-network isolation** (`auto`); `strict` also
//! makes the filesystem read-only except a scratch tmp and **refuses to spawn**
//! if no launcher is available (fail-closed). Default is [`SandboxMode::Off`]
//! → zero behavioural change. The argv-building is pure + unit-tested; the
//! enforcement is delegated to the OS launcher.
//!
//! The OS sandbox enforces two dimensions — outbound network and filesystem
//! writes — and child processes **inherit** the profile, so any subprocess an
//! addon spawns is bound by the same network/filesystem restrictions. The
//! declared `exec` capability is therefore *not* an OS control here: it is
//! disclosed, audited and surfaced for consent (see [`super::capabilities`] /
//! [`super::audit`]), while the data-safety guarantees come from the inherited
//! network/filesystem profile. Path-allowlisting `execve` is also not portable
//! (`bwrap`/seccomp cannot do it) and breaks interpreted servers (the
//! interpreter chain is itself a `process-exec`), so lean-ctx does not attempt
//! it.

use std::path::Path;

use super::capabilities::AddonCapabilities;

/// The two enforceable dimensions of an OS sandbox profile. Both the legacy
/// global [`SandboxMode`] and a per-addon [`AddonCapabilities`] declaration are
/// projected onto these, so one set of pure profile builders serves both paths.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Dims {
    /// Allow outbound network when `true`; otherwise the sandbox blocks egress.
    pub network_allowed: bool,
    /// Allow filesystem writes when `true`; otherwise read-only (+ scratch tmp).
    pub fs_writable: bool,
}

impl Dims {
    /// Nothing left to enforce at the OS level (everything is permitted).
    #[must_use]
    fn is_noop(self) -> bool {
        self.network_allowed && self.fs_writable
    }
}

/// Project a legacy [`SandboxMode`] onto sandbox [`Dims`]. `Off` is permissive
/// (callers short-circuit before wrapping); `Auto` blocks network; `Strict`
/// also makes the filesystem read-only.
#[must_use]
fn dims_for_mode(mode: SandboxMode) -> Dims {
    match mode {
        SandboxMode::Off => Dims {
            network_allowed: true,
            fs_writable: true,
        },
        SandboxMode::Auto => Dims {
            network_allowed: false,
            fs_writable: true,
        },
        SandboxMode::Strict => Dims {
            network_allowed: false,
            fs_writable: false,
        },
    }
}

/// Project declared [`AddonCapabilities`] onto sandbox [`Dims`].
#[must_use]
fn dims_for_caps(caps: &AddonCapabilities) -> Dims {
    Dims {
        network_allowed: caps.network_allowed(),
        fs_writable: caps.filesystem_writable(),
    }
}

/// How aggressively to sandbox a spawned stdio server.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SandboxMode {
    /// No sandbox — spawn the command directly (default).
    #[default]
    Off,
    /// Best-effort: wrap if a launcher exists, else run directly with a warning.
    /// Blocks outbound network.
    Auto,
    /// Network blocked + read-only filesystem; **refuses** to spawn if no
    /// launcher is available.
    Strict,
}

impl SandboxMode {
    #[must_use]
    pub fn as_str(self) -> &'static str {
        match self {
            Self::Off => "off",
            Self::Auto => "auto",
            Self::Strict => "strict",
        }
    }

    /// Parse from config text; unknown / empty → [`Self::Off`].
    #[must_use]
    pub fn parse(s: &str) -> Self {
        match s.trim().to_ascii_lowercase().as_str() {
            "auto" => Self::Auto,
            "strict" => Self::Strict,
            _ => Self::Off,
        }
    }
}

/// An OS sandbox launcher available on this host.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Launcher {
    /// macOS `sandbox-exec` (SBPL profile via `-p`).
    SandboxExec,
    /// Linux `bwrap` (bubblewrap).
    Bwrap,
}

/// What to do for a given (mode, launcher) pair — pure, so it is fully tested.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Plan {
    /// Spawn the command unchanged.
    Direct,
    /// Wrap the command with `launcher`.
    Wrap(Launcher),
    /// Refuse to spawn (strict mode, no launcher). Carries the reason.
    Refuse(String),
}

/// Decide the plan for `mode` given whether a launcher was detected. Pure.
#[must_use]
pub fn plan(mode: SandboxMode, launcher: Option<Launcher>) -> Plan {
    match (mode, launcher) {
        (SandboxMode::Off, _) | (SandboxMode::Auto, None) => Plan::Direct,
        (_, Some(l)) => Plan::Wrap(l),
        (SandboxMode::Strict, None) => Plan::Refuse(
            "addons.sandbox = strict but no OS sandbox launcher (sandbox-exec / bwrap) is available"
                .to_string(),
        ),
    }
}

/// Detect an available launcher for the current OS, or `None`.
#[must_use]
pub fn detect_launcher() -> Option<Launcher> {
    if cfg!(target_os = "macos") && which("sandbox-exec") {
        Some(Launcher::SandboxExec)
    } else if cfg!(target_os = "linux") && which("bwrap") {
        Some(Launcher::Bwrap)
    } else {
        None
    }
}

/// Build the final `(command, args)` for a [`Plan::Wrap`], prefixing the
/// original invocation with the launcher + a profile derived from `mode`. Pure.
#[must_use]
pub fn wrap_argv(
    launcher: Launcher,
    mode: SandboxMode,
    command: &str,
    args: &[String],
) -> (String, Vec<String>) {
    wrap_argv_dims(launcher, dims_for_mode(mode), command, args)
}

/// Build the final `(command, args)` for a [`Plan::Wrap`] from explicit
/// [`Dims`] (network + filesystem). The OS sandbox enforces exactly these two
/// dimensions; child processes inherit the profile, so a subprocess the addon
/// spawns is bound by the same network/filesystem restrictions. Pure.
#[must_use]
fn wrap_argv_dims(
    launcher: Launcher,
    dims: Dims,
    command: &str,
    args: &[String],
) -> (String, Vec<String>) {
    match launcher {
        Launcher::SandboxExec => {
            let profile = sbpl_profile_dims(dims);
            let mut v = vec!["-p".to_string(), profile, command.to_string()];
            v.extend(args.iter().cloned());
            ("sandbox-exec".to_string(), v)
        }
        Launcher::Bwrap => {
            let mut v = bwrap_flags_dims(dims);
            v.push(command.to_string());
            v.extend(args.iter().cloned());
            ("bwrap".to_string(), v)
        }
    }
}

/// macOS SBPL profile for `mode` (test-only wrapper over [`sbpl_profile_dims`];
/// the runtime path goes through [`wrap_argv`] → [`wrap_argv_dims`]).
#[cfg(test)]
fn sbpl_profile(mode: SandboxMode) -> String {
    sbpl_profile_dims(dims_for_mode(mode))
}

/// macOS SBPL profile for explicit [`Dims`]. `allow default` keeps the tool
/// working; the denies are the security wins. Last-match-wins, so the tmp
/// re-allow follows the deny.
fn sbpl_profile_dims(dims: Dims) -> String {
    let mut p = String::from("(version 1)\n(allow default)\n");
    if !dims.network_allowed {
        p.push_str("(deny network*)\n");
    }
    if !dims.fs_writable {
        p.push_str("(deny file-write*)\n");
        p.push_str("(allow file-write* (subpath \"/tmp\") (subpath \"/private/tmp\") (subpath \"/var/folders\"))\n");
    }
    p
}

/// bubblewrap flags for `mode` (test-only wrapper over [`bwrap_flags_dims`];
/// the runtime path goes through [`wrap_argv`] → [`wrap_argv_dims`]).
#[cfg(test)]
fn bwrap_flags(mode: SandboxMode) -> Vec<String> {
    bwrap_flags_dims(dims_for_mode(mode))
}

/// bubblewrap flags for explicit [`Dims`]: unshare the network unless allowed;
/// bind the root read-only (with a writable tmpfs at `/tmp`) unless writable.
fn bwrap_flags_dims(dims: Dims) -> Vec<String> {
    let mut f: Vec<String> = vec!["--die-with-parent".into()];
    if !dims.network_allowed {
        f.push("--unshare-net".into());
    }
    if dims.fs_writable {
        f.extend(
            ["--bind", "/", "/", "--dev", "/dev", "--proc", "/proc"]
                .iter()
                .map(|s| (*s).to_string()),
        );
    } else {
        f.extend(
            [
                "--ro-bind",
                "/",
                "/",
                "--dev",
                "/dev",
                "--proc",
                "/proc",
                "--tmpfs",
                "/tmp",
            ]
            .iter()
            .map(|s| (*s).to_string()),
        );
    }
    f
}

/// Resolve the configured sandbox mode and rewrite `(command, args)` for the
/// gateway spawn point. Returns the original invocation when sandboxing is off
/// or unavailable in `auto`; an `Err` when `strict` cannot be honoured (the
/// caller must then refuse to spawn). Reads the global-only `[addons]` config.
pub fn apply(command: &str, args: &[String]) -> Result<(String, Vec<String>), String> {
    let mode = crate::core::config::Config::load().addons.sandbox_mode();
    if mode == SandboxMode::Off {
        return Ok((command.to_string(), args.to_vec()));
    }
    match plan(mode, detect_launcher()) {
        Plan::Direct => {
            if mode != SandboxMode::Off {
                tracing::warn!(
                    "addons.sandbox = {} but no OS sandbox launcher is available — \
                     spawning `{command}` UNSANDBOXED",
                    mode.as_str()
                );
            }
            Ok((command.to_string(), args.to_vec()))
        }
        Plan::Wrap(launcher) => {
            tracing::debug!(
                "sandboxing `{command}` via {:?} ({} mode)",
                launcher,
                mode.as_str()
            );
            Ok(wrap_argv(launcher, mode, command, args))
        }
        Plan::Refuse(reason) => Err(reason),
    }
}

/// Resolve the sandbox for a spawn, preferring per-addon declared
/// [`AddonCapabilities`] over the legacy global `addons.sandbox` mode.
///
/// - `Some(caps)` → enforce exactly the declared network + filesystem profile
///   (secure-by-default for the platform/marketplace path); child processes
///   inherit it. `exec` is disclosed/audited, not OS-enforced (see module docs).
///   If the profile restricts anything but no OS launcher is available, fail
///   closed when `addons.enforce_capabilities` is set, otherwise run unsandboxed.
/// - `None` → fall back to [`apply`] (the legacy `addons.sandbox` behaviour), so
///   addons that predate the capability model keep working unchanged.
pub fn apply_for(
    command: &str,
    args: &[String],
    capabilities: Option<&AddonCapabilities>,
) -> Result<(String, Vec<String>), String> {
    match capabilities {
        Some(caps) => apply_caps(command, args, caps),
        None => apply(command, args),
    }
}

/// Enforce a per-addon capability profile at the spawn point. The OS sandbox
/// enforces the network + filesystem dimensions (and child processes inherit
/// them); `exec` is a declared + audited + consented capability, not an OS
/// control — see the module docs for why path-allowlisting `execve` is neither
/// portable nor compatible with interpreted servers. Pure decision +
/// OS-launcher detection; the wrapping argv is unit-tested.
fn apply_caps(
    command: &str,
    args: &[String],
    caps: &AddonCapabilities,
) -> Result<(String, Vec<String>), String> {
    let dims = dims_for_caps(caps);

    // Network + filesystem unrestricted → nothing for the OS sandbox to add
    // (env scrubbing still happens at the spawn point).
    if dims.is_noop() {
        return Ok((command.to_string(), args.to_vec()));
    }

    let enforce = crate::core::config::Config::load()
        .addons
        .enforce_capabilities;

    let Some(launcher) = detect_launcher() else {
        // No OS launcher: fail closed only when the org opted in, else warn.
        if enforce {
            return Err(format!(
                "addons.enforce_capabilities = true but no OS sandbox launcher \
                 (sandbox-exec / bwrap) is available to honour `{command}`'s declared \
                 restricted capabilities"
            ));
        }
        tracing::warn!(
            "addon `{command}` declares restricted capabilities but no OS sandbox \
             launcher is available — running UNSANDBOXED (set \
             addons.enforce_capabilities = true to fail closed)"
        );
        return Ok((command.to_string(), args.to_vec()));
    };

    tracing::debug!(
        "sandboxing `{command}` via {:?} (net={}, fs_write={})",
        launcher,
        dims.network_allowed,
        dims.fs_writable
    );
    Ok(wrap_argv_dims(launcher, dims, command, args))
}

fn which(bin: &str) -> bool {
    let Ok(path) = std::env::var("PATH") else {
        return false;
    };
    std::env::split_paths(&path).any(|dir| {
        let p = dir.join(bin);
        p.is_file() && is_executable(&p)
    })
}

#[cfg(unix)]
fn is_executable(p: &Path) -> bool {
    use std::os::unix::fs::PermissionsExt;
    std::fs::metadata(p).is_ok_and(|m| m.permissions().mode() & 0o111 != 0)
}

#[cfg(not(unix))]
fn is_executable(_p: &Path) -> bool {
    true
}

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

    #[test]
    fn mode_parse_roundtrip() {
        assert_eq!(SandboxMode::parse("auto"), SandboxMode::Auto);
        assert_eq!(SandboxMode::parse("STRICT"), SandboxMode::Strict);
        assert_eq!(SandboxMode::parse(""), SandboxMode::Off);
        assert_eq!(SandboxMode::parse("nonsense"), SandboxMode::Off);
        assert_eq!(SandboxMode::Strict.as_str(), "strict");
    }

    #[test]
    fn plan_off_is_always_direct() {
        assert_eq!(plan(SandboxMode::Off, Some(Launcher::Bwrap)), Plan::Direct);
        assert_eq!(plan(SandboxMode::Off, None), Plan::Direct);
    }

    #[test]
    fn plan_auto_without_launcher_runs_direct() {
        assert_eq!(plan(SandboxMode::Auto, None), Plan::Direct);
    }

    #[test]
    fn plan_strict_without_launcher_refuses() {
        assert!(matches!(plan(SandboxMode::Strict, None), Plan::Refuse(_)));
    }

    #[test]
    fn plan_wraps_when_launcher_present() {
        assert_eq!(
            plan(SandboxMode::Auto, Some(Launcher::SandboxExec)),
            Plan::Wrap(Launcher::SandboxExec)
        );
    }

    #[test]
    fn sandbox_exec_argv_prepends_profile_and_command() {
        let (cmd, args) = wrap_argv(
            Launcher::SandboxExec,
            SandboxMode::Auto,
            "my-mcp",
            &["serve".into()],
        );
        assert_eq!(cmd, "sandbox-exec");
        assert_eq!(args[0], "-p");
        assert!(args[1].contains("(deny network*)"));
        assert_eq!(args[2], "my-mcp");
        assert_eq!(args[3], "serve");
    }

    #[test]
    fn strict_sbpl_restricts_writes() {
        let p = sbpl_profile(SandboxMode::Strict);
        assert!(p.contains("(deny file-write*)"));
        assert!(p.contains("/tmp"));
        let auto = sbpl_profile(SandboxMode::Auto);
        assert!(!auto.contains("(deny file-write*)"));
    }

    #[test]
    fn bwrap_argv_unshares_network() {
        let (cmd, args) = wrap_argv(Launcher::Bwrap, SandboxMode::Auto, "my-mcp", &["x".into()]);
        assert_eq!(cmd, "bwrap");
        assert!(args.iter().any(|a| a == "--unshare-net"));
        assert!(args.iter().any(|a| a == "my-mcp"));
        assert!(args.iter().any(|a| a == "x"));
    }

    #[test]
    fn bwrap_strict_is_readonly_root() {
        let (_c, args) = wrap_argv(Launcher::Bwrap, SandboxMode::Strict, "m", &[]);
        assert!(args.iter().any(|a| a == "--ro-bind"));
        assert!(args.iter().any(|a| a == "--tmpfs"));
    }

    // --- capability-derived profiles (P1) ---

    use super::super::capabilities::{
        AddonCapabilities, ExecAccess, FilesystemAccess, NetworkAccess,
    };

    #[test]
    fn minimal_caps_block_network_and_writes() {
        let dims = dims_for_caps(&AddonCapabilities::default());
        assert!(!dims.network_allowed);
        assert!(!dims.fs_writable);
        let sbpl = sbpl_profile_dims(dims);
        assert!(sbpl.contains("(deny network*)"));
        assert!(sbpl.contains("(deny file-write*)"));
    }

    #[test]
    fn full_network_caps_omit_network_deny() {
        let caps = AddonCapabilities {
            network: NetworkAccess::Full,
            filesystem: FilesystemAccess::ReadOnly,
            env: vec![],
            exec: ExecAccess::default(),
        };
        let dims = dims_for_caps(&caps);
        assert!(dims.network_allowed);
        let sbpl = sbpl_profile_dims(dims);
        assert!(!sbpl.contains("(deny network*)"));
        assert!(sbpl.contains("(deny file-write*)"));
        // bwrap must NOT unshare the network when egress is allowed.
        let flags = bwrap_flags_dims(dims);
        assert!(!flags.iter().any(|f| f == "--unshare-net"));
        assert!(flags.iter().any(|f| f == "--ro-bind"));
    }

    #[test]
    fn permissive_net_fs_is_a_noop_regardless_of_exec() {
        // exec is not an OS-sandbox dimension: an unrestricted network +
        // filesystem profile is a true no-op even when the exec declaration is
        // restricted (the addon — and its interpreter chain — must start).
        let caps = AddonCapabilities {
            network: NetworkAccess::Full,
            filesystem: FilesystemAccess::ReadWrite,
            env: vec![],
            exec: ExecAccess::default(), // `none` — a restricted declaration
        };
        assert!(caps.exec_restricted());
        assert!(dims_for_caps(&caps).is_noop());
        // apply_for returns the command unchanged — exec is never OS-enforced.
        let (cmd, args) = apply_for("my-mcp", &["serve".into()], Some(&caps)).expect("noop");
        assert_eq!(cmd, "my-mcp");
        assert_eq!(args, vec!["serve".to_string()]);
    }

    #[test]
    fn caps_wrap_argv_prepends_launcher() {
        let dims = dims_for_caps(&AddonCapabilities::default());
        let (cmd, args) = wrap_argv_dims(Launcher::SandboxExec, dims, "my-mcp", &["x".into()]);
        assert_eq!(cmd, "sandbox-exec");
        assert_eq!(args[0], "-p");
        assert!(args[1].contains("(deny network*)"));
        assert_eq!(args[2], "my-mcp");
        assert_eq!(args[3], "x");
    }

    // --- exec is declared/audited, NOT OS-enforced (see module docs) ---

    #[test]
    fn sandbox_profile_never_emits_process_exec() {
        // Whatever the exec declaration, the generated SBPL profile only ever
        // governs network + filesystem — never `process-exec`. Path-allowlisting
        // execve is not portable (bwrap/seccomp can't) and breaks interpreted
        // servers (the interpreter chain is itself a process-exec).
        let dims = dims_for_caps(&AddonCapabilities::default());
        let (_cmd, args) = wrap_argv_dims(Launcher::SandboxExec, dims, "my-mcp", &[]);
        assert!(args[1].contains("(deny network*)"));
        assert!(args[1].contains("(deny file-write*)"));
        assert!(!args[1].contains("process-exec"));
    }

    #[test]
    fn mode_path_unchanged_via_dims() {
        // Back-compat: the mode wrappers still produce the historical profiles.
        assert!(sbpl_profile(SandboxMode::Auto).contains("(deny network*)"));
        assert!(!sbpl_profile(SandboxMode::Auto).contains("(deny file-write*)"));
        assert!(sbpl_profile(SandboxMode::Strict).contains("(deny file-write*)"));
        assert!(
            bwrap_flags(SandboxMode::Auto)
                .iter()
                .any(|f| f == "--unshare-net")
        );
    }
}