Inspiration

Teams want autonomous agents to do real GitLab work — bump dependencies, open MRs, triage issues, cut releases — but they won't hand an LLM write credentials, because anything the agent reads can hijack it. A poisoned changelog, a crafted issue body, a hidden comment in a README: any of these can steer an agent toward create_pipeline (CI/CD secret exfiltration), delete_branch, fork_repository, or a direct push to main. Indirect prompt injection is OWASP's #1 LLM risk, and the GitLab MCP exposes 196 tools to the agent — a huge blast radius. We wanted the missing piece that makes "leave the agent unattended on a real repo" actually safe.

What it does

Airlock is a deterministic, LLM-free guard that sits on the MCP wire between an autonomous ADK + Gemini 3 agent and the GitLab MCP server. Before the agent reads any untrusted content, the operator's intent is sealed (hashed, frozen). Every tools/call is then checked against that sealed intent at the transport layer:

  • Default-deny: only the handful of tools in the sealed intent pass; the other 190+ — including tools nobody remembered to forbid — are blocked at the wire, and the downstream GitLab MCP never receives them.
  • Field-level enforcement: path traversal (package.json/../.env), nested-arg smuggling (args.options.commit.branch=main), and protected-branch writes are caught by diffing tool-call fields, at any nesting depth.
  • Time-boundary: intent is sealed before untrusted content is read; committing a permissive intent after the first tool call throws — the attacker can't redefine the task post-hoc.

Try it live: open the demo, give the agent a task, paste your own injection (disguise it as a CI/CD convention), and run it WITHOUT vs WITH Airlock. Without, the injected destructive call goes through (dry-run); with, it dies at the wire in ~8 µs.

How we built it

  • Agent: Google Agent Builder (ADK, TypeScript) drives an autonomous repo-maintenance agent on Gemini 3 via Vertex AI. The agent instruction is neutral — it is never told to obey embedded instructions, so the hijack we demonstrate is emergent, not scripted.
  • Guard: a synchronous field-diff (src/guard/policy.ts) — no model in the trust path. It intercepts MCP tools/call and tools/list frames at the transport, so it's framework- and model-agnostic (ADK, LangGraph, or a raw loop).
  • Upstream: the real GitLab MCP (@zereight/mcp-gitlab, 196 tools) against a real GitLab repo (leejk2061/airlock-demo).
  • Deploy: containerized and shipped to Cloud Run (scale-to-zero) so judges can poke a live instance.

Airlock is built on Google Agent Builder: the agent is an ADK app (ADK ships under Google's official agent-builder/ documentation), running Gemini 3 through Vertex AI and deployed on Cloud Run — the hackathon's explicitly recommended runtime. The Airlock guard is a transport-layer add-on around that ADK agent, not a replacement for it.

Challenges we ran into

  • Proving the hijack is real, not staged. If we told the agent to obey the injection, the demo would be theater. So the agent instruction stays neutral and the hijack has to emerge from Gemini 3 reading poisoned content — which means the attack has to be genuinely persuasive (a plausible "trunk-based-dev convention"), not a loud SYSTEM INSTRUCTION.
  • Catching smuggled destinations. Attackers hide a protected-branch write or a traversal path inside nested argument objects. The guard had to walk args recursively and match keys exactly (branch is a write destination; target_branch is a legit MR review destination and must not trip the guard).
  • Not over-claiming. A deterministic guard cannot inspect the content of an allowed write. We chose to surface that limitation explicitly rather than paper over it.

Accomplishments that we're proud of

  • Default-deny across GitLab's entire 196-tool surface — the guard blocks tools we never even enumerated, because the base policy is deny-by-default.
  • Externally-grounded validation. Beyond our own 17 adversarial scenarios (17/17 blocked, 0/10 false-blocks across 27 vectors), we ran a separate battery whose attack classes come from public taxonomies we did not author — OWASP LLM Top 10, CWE-22 path traversal, AgentDojo tool-use-injection, CI/CD secret-exfil TTPs: 18/18 blocked, 0/4 false-blocks (npm run bench).
  • An honest threat model. We document exactly what Airlock does not catch (content-poisoning of an in-scope file — reported as KNOWN_LIMITATION, not caught, by design) instead of hiding it. The guard gates which calls run, not the content of allowed writes.
  • Sub-10-microsecond, model-free enforcement (~8 µs/call measured) vs an LLM-judge guardrail's hundreds-of-ms round-trip that can itself be argued with by the same injection.
  • 69 tests green, live on Cloud Run, with a reproducible end-to-end run in DEMO-RUN.md.

What we learned

The strongest guard for agentic ops is the one that doesn't reason. Putting an LLM judge in front of an agent just gives the injection a second model to social-engineer. Moving enforcement to the MCP transport, as a deterministic allow-list sealed before untrusted input, turns "can we trust the model?" into "the model's authority is bounded regardless of what it's told" — which is the property you actually need to leave an agent unattended.

What's next for Airlock

  • Content/SAST layer for the documented in-scope limitation (dependency allow-list, scanning the content of allowed writes).
  • ref-parameter coverage for pipeline/tag tools (today the branch guard keys on branch; create_pipeline uses ref).
  • Beyond GitLab: the guard core is MCP-protocol-level, so the same sealed-intent enforcement generalizes to any MCP tool surface (GitHub, filesystem, cloud).

Built With

Share this project:

Updates