Skip to content

A second agent & scoping

Some of The Scoop's work isn't customer-facing: reordering stock, chasing suppliers, internal reports. Running that under Sundae's public support identity would be wrong — different audience, different powers, different trust boundary. So we hire a second agent: Scout, on the internal ops desk.

This chapter introduces a new role (the class), a new agent (the record), and the two ways Protege bounds what an agent can do.

A new role and agent

Same split as before — the class is the role, the record is the agent. Scaffold the Operations role:

bash
bin/rails g protege:agent operations
ruby
# app/agents/operations_agent.rb
class OperationsAgent < Protege::Agent
  self.display_name = "Operations"

  message_resolvers do |chain|
    chain.use(Protege::LoadTextResolver, role: :system) { |ctx| ctx.agent.instructions }
    chain.use Protege::ThreadHistoryResolver
  end

  responsibility_resolvers do |chain|
    chain.use(Protege::LoadTextResolver, role: :system) { |ctx| ctx.agent.instructions }
    chain.use(Protege::LoadRecordResolver) { Flavor.all }   # the stock list her duties report on
    chain.use(Protege::LoadTextResolver, role: :user)   { |ctx| ctx.responsibility.instructions }
  end
end

Create Scout as a record on The Scoop's already-registered domain (Sundae is a CustomerServiceAgent; Scout is an OperationsAgent — same domain, different role):

ruby
OperationsAgent.create!(
  name:          "Scout",
  email_address: "ops@thescoop.com",
  instructions:  "You are Scout, The Scoop's internal ops agent. Keep the shop stocked and " \
                 "flag anything that needs a human."
)

Because email_address is unique across all agents, Scout is a singleton on ops@thescoop.com — one agent per inbox.

Scoping tools: toolkits

An agent's tools come entirely from its attached toolkits — fail-closed, so what you don't attach is structurally out of reach, and every new agent starts with only Default Tools (the engine built-ins). That makes scoping Scout simple: give her exactly her trade and nothing else.

Toolkits → New — create Operations with Check Flavor Stock, Create Order, and Send Email checked (a tool may live in many toolkits; bundling the send tool here will make Scout's scheduled work self-contained), then attach it on Scout's page. process_refund and lookup_order aren't members of anything she holds, so they're never even offered to her model. An ops agent has no business issuing customer refunds, and now she structurally can't. (The same bundles are reusable across agents — Sundae could hold Operations too, on her own terms.)

The attachment, not the toolkit, decides who may reach those tools, so one toolkit given to two agents can be gated differently:

  • Sender rules on the attachment gate its tools by the inbound sender (same allow/deny patterns as the access rules below). With no rules, the attachment admits everyone.
  • Allow on scheduled runs is a per-attachment flag: whether these tools may run on Scout's scheduled duties, which have no sender. Off by default — we'll flip it in the next chapter.

Gate Sundae's refunds while we're here

Chapter 4 left process_refund in Sundae's open Support toolkit — any customer could talk her into trying it. The gate is checked per message, so the fix is to split the sensitive tool into its own toolkit with a sender rule:

  1. Toolkits → Support → Edit — uncheck Process Refund.
  2. Toolkits → New — create Finance with just Process Refund, and attach it to Sundae.
  3. On that attachment's Configure Access Settings page, add an allow rule with pattern *@thescoop.com.

Now a customer's message runs with lookup and stock tools only, while a message from the shop — even on the same thread, mid-conversation — unlocks the refund tool. One agent, different powers per correspondent; the worked version of this scenario is in Toolkits.

Scoping senders: access rules

Sundae should hear from anyone — she's support. Scout should only be reachable from inside the company. That's the inbound access guardrail, which is built from two layers that only ever narrow:

  • The global ceiling (config.inbound_access) — the org-wide boundary.
  • Per-agent rules — editable on each agent's record in the dashboard.

Leave the ceiling permissive (so customers reach Sundae), and add an allow-rule on Scout for *@thescoop.com:

ruby
# config/initializers/protege.rb — a permissive ceiling; customers must reach Sundae
config.inbound_access = Protege::Gateway.build_access_policy   # permit-all

Then on Scout's record, in the dashboard, add an allow access rule with pattern *@thescoop.com. That single rule flips Scout into allow-list mode: only company senders get through, everyone else is bounced with an access-denied notice — while Sundae, with no rules of her own, stays open to the public.

One honest caveat: the check reads the mail's From header, so it's only as strong as your transport's sender authentication. A provider ingress verifies SPF/DKIM/DMARC for you (as does the bundled self-host MTA), and behind that a forged From doesn't get through — but don't treat an allow rule alone as cryptographic proof of identity. See Security.

Next

Scout exists and is locked down — but so far she only acts when emailed. Her real value is proactive work.

Scheduled work — give Scout a standing duty that runs on its own.