# Keel
**The execution layer under AI agents.**
Agents decide *what* to do. Keel decides *what is allowed to become real* — filesystem access, subprocesses, network egress, and credentials — without trusting the model’s story about what it “needs.”
```text
┌─────────────────────────────┐
│ Agent runtime │ Grok · Codex · Zene · custom hosts
│ (plans, tools, prompts) │
└──────────────┬──────────────┘
│ policy · spawn · SpaceFs · events
┌──────────────▼──────────────┐
│ Keel │ Policy · Enforce · Record · Lifecycle
│ Secure execution space │
└─────────────────────────────┘
```
Named after a ship’s **keel**: the structural spine underneath. The agent rides on top; Keel keeps real host access from tearing things apart.
> **One line:** Keel is the landing gate under the agent — bind reach first, execute under policy, audit what actually happened.
## What Keel does
| **Policy-bound commands** | `spawn` under a `Space`; configure stdin/stdout/stderr (including MCP stdio pipes). |
| **Process-tree lifecycle** | Timeout / cancel kills the **process group** (Unix) so shell grandchildren are less likely to leak; collect exit code, duration, and termination reason. |
| **Policy-bound file I/O** | **`space.fs()` (preferred)** — read / write / create / delete / rename / metadata + audit. `check_fs` is advisory UI only. |
| **Baseline secret denies** | Every policy (unless opted out) denies `~/.ssh`, `~/.aws`, `**/.env`, `**/*.pem`, … even when `default_read` is true. |
| **Egress control** | Allowlists, local CONNECT proxy, and kernel ProxyOnly on children (DenyAll → seccomp block). |
| **Credentials** | JIT inject at spawn; redact exec args in audit logs when needed (`audit_args: false`). |
| **Worktree isolation** | Optional git worktree or directory under `~/.keel/worktrees/`. |
| **Task narrowing** | Child policies can only **shrink** parent reach — the model cannot expand its own rights. |
| **Optional host confine** | `Space::create_confined` applies Landlock/Seatbelt to **this process** (irreversible; Grok-style). Default keeps the host clean and sandboxes children only. |
| **Audit integrity** | Default hash chain (`prev_hash` / `event_hash`); SpaceFs can attach `content_sha256` digests. Verify with `verify_chain`. |
### Four pillars
| **Policy** | Up front: what may be read, written, executed, dialed, and which credentials exist (plus TTL). |
| **Enforce** | At runtime: soft checks and/or kernel sandboxes (Landlock / Seatbelt) map policy to boundaries. |
| **Record** | Ground truth: events tagged with `space_id` / `policy_id` (optional `events.jsonl` under `~/.keel`). |
| **Lifecycle** | A **Space**: create → use → destroy. |
### What Keel is *not*
- Not a chat UI or planning framework (not a replacement for Grok / Codex / your agent host).
- Not a full desktop app.
- Soft checks alone are **not** VM-grade isolation — use kernel backends when you need stronger FS/net enforcement. `SpaceFs` is a **soft** host-side API (TOCTOU still exists); hosts should keep `O_NOFOLLOW` and path re-checks where they already have them.
## Status
**v0.0.15** on [crates.io](https://crates.io) as **`eero-keel-*`** (owner [EeroEternal](https://crates.io/users/EeroEternal)).
Windows **Job Objects** + **AppContainer** (path grants + CreateProcess); baseline denies;
SpaceFs-first; hash-chain audit; Unix Landlock/Seatbelt/seccomp as before.
See [CHANGELOG.md](CHANGELOG.md), [design](docs/design.md), and [host integration](docs/integration.md).
## Install
Requires **Rust 1.93+**.
### CLI
```bash
cargo install eero-keel-cli
keel info
```
### Library
```toml
[dependencies]
eero-keel-core = "0.0.15"
```
Rust imports use short crate names (`keel_core`, `keel_policy`, …), not the crates.io package prefix:
```rust
use keel_core::{
profile_workspace, ProcessGuardBackend, Space, SpawnRequest, StdioMode,
};
```
### crates.io package names
| [`eero-keel-policy`](https://crates.io/crates/eero-keel-policy) | `keel_policy` | Policy, presets, IDs, serde |
| [`eero-keel-record`](https://crates.io/crates/eero-keel-record) | `keel_record` | Events + sinks (memory, JSONL) |
| [`eero-keel-enforce`](https://crates.io/crates/eero-keel-enforce) | `keel_enforce` | Backends (null, process-guard, kernel, worktree) |
| [`eero-keel-core`](https://crates.io/crates/eero-keel-core) | `keel_core` | `Space` orchestration |
| [`eero-keel-cli`](https://crates.io/crates/eero-keel-cli) | binary `keel` | CLI |
Notes:
- Plain names `keel` / `keel-core` / `keel-cli` / `keel-enforce` are taken by unrelated crates.
- Historical `keel-exec-*` is **not** maintained from this repo.
- Source directories remain `crates/keel-*`; use package names with `cargo -p eero-keel-cli`.
## Quick start
### From source
```bash
cargo build -p eero-keel-cli --release
./target/release/keel info
cargo run -p eero-keel-cli -- policy --profile workspace
cargo run -p eero-keel-cli -- check --profile workspace --write ./README.md
cargo run -p eero-keel-cli -- run --profile workspace -- echo hello
```
### Library sketch
```rust
use std::sync::Arc;
use std::time::Duration;
use keel_core::{
profile_workspace, MemorySink, ProcessGuardBackend, Space, SpaceOptions, SpawnRequest,
};
# async fn demo() -> anyhow::Result<()> {
let policy = profile_workspace(std::env::current_dir()?.as_path())?;
let sink = Arc::new(MemorySink::new());
let backend = Arc::new(ProcessGuardBackend::new());
let space = Space::create_with_sink(
policy,
backend,
SpaceOptions {
persist_events: false,
memory_events: true,
..Default::default()
},
Some(sink),
)
.await?;
// Files under policy (prefer SpaceFs over raw std::fs + check_fs alone):
// space.fs().write("foo/bar.txt", b"ok").await?;
let exit = space
.spawn(SpawnRequest::new("echo").args(["keel"]))
.await?
.wait_timeout(Duration::from_secs(5))
.await?;
assert!(exit.success());
// Timeout + collect stdout/stderr:
// .wait_with_output_timeout(Duration::from_secs(30)).await?;
// MCP-style stdio:
// SpawnRequest::new("server").stdin(StdioMode::Piped).stdout(StdioMode::Piped)
space.destroy().await?;
# Ok(())
# }
```
Typical host flow:
```text
open Space (bind policy)
→ space.fs() for file tools / space.spawn() for commands
→ wait with timeout or CancellationToken
→ destroy → keep audit log if needed
```
## Backends
| `null` | Record only; accept policy | done |
| `process-guard` | Soft FS / exec checks | done |
| `local-process` | **Linux:** Landlock + bwrap + seccomp · **macOS:** Seatbelt · **Windows:** Job Objects + AppContainer (path grants) · children by default | done |
| `local-worktree` | Git worktree or directory under `~/.keel/worktrees/` | done |
| `remote-microvm` | Guest microVM | future — [docs](docs/future-remote-microvm.md) |
**Windows notes:** Jobs kill process trees on cancel/timeout/Drop. AppContainer profiles +
`icacls` grants enforce OS-level FS reach for children when `windows_appcontainer` is enabled
(default). Capability set is minimal (`internetClient` when network is not DenyAll).
```bash
# Kernel FS on children; events under ~/.keel/spaces/
cargo run -p eero-keel-cli -- run --backend local-process --profile workspace -- echo hello
# Egress allowlist (CONNECT proxy + ProxyOnly where supported)
cargo run -p eero-keel-cli -- check-egress evil.com --allow-host api.x.ai:443
cargo run -p eero-keel-cli -- run --backend local-process --allow-host example.com:80 -- \
curl -sI http://example.com/
# Worktree + credential injection
cargo run -p eero-keel-cli -- run --worktree --cred API_TOKEN=env:MY_TOKEN -- echo ok
```
On Linux, true read-deny for nested paths often needs **bubblewrap** (`bwrap` on `PATH` or `KEEL_BWRAP`).
## Policy profiles
| `workspace` | world (default) | workspace + `~/.keel` + temps | unrestricted |
| `read-only` | world | `~/.keel` + temps | deny-all |
| `strict` | system + workspace | workspace + `~/.keel` + temps | deny-all |
Custom profiles: builder API, JSON/TOML, or `sandbox.toml` (`~/.keel/sandbox.toml` and project `.keel/sandbox.toml`).
## Links
- Repository: [github.com/EeroEternal/keel](https://github.com/EeroEternal/keel)
- [Design](docs/design.md) · [Integration](docs/integration.md) · [Testing](docs/testing.md) · [Changelog](CHANGELOG.md)
## License
Apache-2.0