Skip to content

A second persona & 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:persona operations
ruby
# app/personas/operations_persona.rb
class OperationsPersona < Protege::Persona
  self.display_name = "Operations"

  # Scout's grant — deliberately narrower than Customer Service (see below).
  tools :check_flavor_stock, :create_order, :send_email

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

  responsibility_resolvers do |chain|
    chain.use(Protege::LoadTextResolver, role: :system) { |ctx| ctx.persona.instructions }
    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 CustomerServicePersona; Scout is an OperationsPersona — same domain, different role):

ruby
OperationsPersona.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 personas, Scout is a singleton on ops@thescoop.com — one agent per inbox.

Scoping tools: the grant

Notice the tools line in OperationsPersona. By default a persona may use every registered tool — that's what Sundae does. Scout shouldn't; an ops agent has no business issuing customer refunds. The tools macro declares a grant — the code-side ceiling of what the role may use:

ruby
tools :check_flavor_stock, :create_order, :send_email

Scout can check stock, create orders, and send mail — and nothing else. ProcessRefund and LookupOrder aren't in her grant, so they're never even offered to her model.

Two things about the grant worth remembering:

  • It inherits down the STI tree. Unlike the resolver chains (which are per-class), a base role's grant is inherited by subclasses — so you can set a family-wide ceiling and let subclasses tighten it.
  • Omitting it means "all tools." That's why Sundae, with no grant, has the full catalog.

An operator can narrow the grant further at runtime by disabling specific tools on the agent's record in the dashboard — but they can never widen past the grant. See Personas.

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-persona 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. An external sender can never reach Scout, no matter what they put in From.

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.