Skip to content

Security and Risk Model

An agent with an email address and access to your app is a real attack surface. Protege is explicit about where the line sits: some protections the engine enforces for you, and some are yours to own. This page draws that line, then details the one guardrail the engine gives you the most control over — inbound access control.

What the engine enforces

  • Domain-scoped routing. The engine claims only inbound mail for domains you've registered as an EmailDomain. It never intercepts the host app's other mail, and mail to an unknown address is bounced rather than processed.
  • Inbound access control. Every sender is checked against a layered guardrail before a message reaches inference (detailed below).
  • A recursion guard. Outbound mail carries a hop budget; an inbound message whose budget is spent is dropped, so two agents can't reply to each other indefinitely.
  • Reply identity. The send_email tool forces From to the sending agent's own address — an agent can't impersonate another sender — and takes a reply's subject and threading from the inbound message rather than letting the model invent them.
  • Bounded loops. A single run is capped at max_tool_turns tool-calling rounds, so a confused agent can't spin forever.
  • Attachment limits. Oversized or too-numerous attachments are bounced at the edge, before storage.

What the host owns

  • Dashboard authentication. The engine ships none. The /protege mount exposes personas, threads, and traces to anyone who can reach it — you must wrap the mount in your app's auth (e.g. authenticate :user do ... end). The install generator leaves a loud TODO to this effect.
  • Mail authentication. Whether a sender's From can be trusted at all is a transport concern — SPF/DKIM/DMARC. A provider handles it for you; if you self-host, the bundled MTA enforces it, but you own the DNS. Without it, the access guardrail is only as trustworthy as the From header.
  • Tool blast radius. A tool is ordinary Ruby with your app's full power. What a tool can do — refund any order, or only the sender's; read one record, or the table — is your design. Scope aggressively and treat tool input as untrusted.
  • Prompt injection. A customer's email is untrusted input that the model reads. Assume it may try to talk the agent into misusing a tool, and defend at the tool boundary (authorization checks in the tool), not by trusting the prompt.

Inbound access control

This is the guardrail you tune most, so it's worth understanding in full. It decides which senders may reach an agent, and it's built from two layers that only ever narrow:

  • The global ceilingconfig.inbound_access, a committed, org-wide policy. This is the outer boundary; nothing can widen past it.
  • Per-persona rulesAccessRule records, editable in the dashboard. These can restrict an individual agent further, but never admit a sender the ceiling blocks.

The effective decision is the intersection: a sender must pass both layers. A persona with no rules of its own imposes no extra constraint and inherits the ceiling as-is.

How The Scoop uses it

Sundae (customer-facing) should hear from anyone, while Scout (internal ops) should only be reachable from inside the company. Set the ceiling wide and restrict Scout at the persona layer:

ruby
# config/initializers/protege.rb — the global ceiling: no outright bans
config.inbound_access = Protege::Gateway.build_access_policy   # permit-all ceiling

Then, on the Operations persona's record in the dashboard, add an allow rule for *@thescoop.com. That rule flips Scout into allow-list mode, so only company senders get through — while Sundae, with no rules, stays open to customers. Neither can be reached from a domain the ceiling would have blocked, had you set one.

Pattern matching

Patterns are an exact address or a single * wildcard (*@thescoop.com). They're matched against the sender's routing key — tag-stripped and lowercased — so Ceo+finance@Thescoop.com matches a ceo@thescoop.com rule. Precedence within a layer is fixed: an explicit deny match always wins, then an allow match admits, then the layer's default settles everyone else. Supplying any allow rule makes the layer default-deny ("only these may enter"); a layer with only deny rules is a block-list.

A sender that fails the guardrail is bounced with an access-denied notice and never reaches inference.