Skip to content
All projects
LiveSecurityAI

Portalis

A security layer between an AI agent and the tools it can reach.

Portalis screenshot

Overview

Portalis protects people who have connected a language model to real capabilities without quite realising it. An agent reads a web page, a PDF or an email, and that content can carry instructions aimed at the model rather than the user who never sees them. Portalis inspects what enters the agent's context and constrains what leaves it as an action. Two adapters over one engine: an MCP proxy that sits in front of a user's existing tool servers and can actually refuse, and a hosted page for others who just paste into a chatbot.

Problem

LLMs are rampant nowadays, gaining more capabilities per day. While this introduces exciting opportunities, it also introduces potential attack surfaces. This aims to protect the general user who interacts with LLMs unaware of potential threats.

Solution

A gateway for the user to go through before talking to the LLM. Either as a MCP proxy, or self-checked by the user in their browser, their input is checked based on detection rules and known injection patterns to flag out and remove PII, secrets and potential attacks.

Architecture

A pure synchronous core with no I/O, clocks, or randomness ensures deterministic decisions: the same input always produces the same result, and every block decision can be replayed with an explanation of why it occurred. Detectors include RE2 patterns, a hand-written linear model for names, and a character n-gram model for injection, which all only report findings. The system combines detector results conservatively, with a declarative TOML policy layer making the final decision. Policies are designed so that their order cannot accidentally weaken a security decision. Adapters handle different interfaces without changing the core logic, and tests verify that all interfaces produce consistent results.

Tech Stack

PythonFastAPIpydanticgoogle-re2pytestHypothesisDocker

Challenges

  • The first big surprise was discovering that a gateway could enforce absolutely nothing while every test still passed. The core decision logic was working correctly, but the transport layer was simply throwing the result away. I found it almost immediately when I tested the system in a real environment. The tests had only ever exercised the logic directly.
  • I learned that 'cleaning' suspicious input can actually make things worse. Removing the obvious parts of an injection left behind something that looked harmless, but still contained the original instruction. That made me much more cautious about modifying user input.
  • There is a difficult balance between catching more threats and keeping people from losing trust in the tool. When I loosened a rule to catch a subtle credential leak, it also started flagging perfectly normal CI variables. A security tool with too many false positives eventually gets ignored, and that can become a security problem of its own.

Lessons Learned

  • You cannot grade your own homework. Nine different times, a test or benchmark I had written alongside the feature told me everything was working when real usage said otherwise. Independent testing needs to be a part of the design, not something added at the end.
  • The failures are just as important as the successes. After finishing the system, I deliberately attacked it knowing exactly how it worked. Ten of fifteen attacks worked immediately. I documented the ones I couldn't stop rather than hiding them, because knowing where the system breaks is far more useful than pretending it doesn't.
  • The most important lesson was to design around what I can actually prove. An attacker can keep changing the way an attack is written, so endlessly adding patterns isn't a reliable strategy. The strongest rules in the system don't try to decide whether something 'looks malicious', rather they check whether data is being sent somewhere it was never supposed to go.

Future Improvements

  • One attack still gets through every layer: a destination that is assembled dynamically at render time, leaving nothing for the system to inspect until it is already resolved.
  • The current injection detection is limited. Reworded attacks and attacks written in other languages remain open problems rather than being covered with increasingly fragile pattern matching.
  • The system can identify situations where a user should be asked for confirmation, but the integrations I've built so far don't provide a way to actually ask them. For now, those cases fall back to a refusal.