Skip to content

Toolkits

A toolkit is a named, reusable bundle of tools — and it is the only way an agent gets tools. Tool resolution is fail-closed: an agent with no attached toolkit has no tools at all, not even send_email. That inversion is deliberate. Writing a tool class puts it in the registry; who may use it, and on whose behalf, is an operator decision made in the dashboard, where it can be changed without a deploy.

Three records carry the model:

RecordWhat it is
ToolkitThe bundle: a name, a description, and a checklist of member tools. Global — defined once, attachable to many agents.
Attachment (AgentToolkit)The grant binding one toolkit to one agent. This is where permission lives: the sender gate and the scheduled-runs flag sit on the attachment, so two agents can hold the same toolkit on different terms.
Gate rule (AgentToolkitRule)One allow/deny address pattern on an attachment's sender gate.

Creating a toolkit

Toolkits → New in the dashboard. Give it a name and a description, then check off members from the tool manifest — every registered tool is listed with its display name and its summary (the short human-facing line each tool declares; see Tools). Membership is stored as tool ids, so a member whose class was later deleted from the code is simply skipped — a stale id can never conjure or break a tool.

Attaching it to an agent

On the agent's page, the Agent Toolkits section lists what's attached and offers the attach form. Each attachment's Configure Access Settings page is where its terms live:

  • Sender gate — allow/deny rules deciding which correspondents unlock these tools.
  • Allow on scheduled runs — whether these tools are available on the agent's scheduled runs, where there is no sender to check. Off by default.

The sender gate

Every inference run resolves the agent's tools fresh: for each attached toolkit, the gate is checked against the sender of the message being handled, and the run is offered the union of the members of every toolkit whose gate that sender passes.

Rules are address patterns — an exact address (mara@thescoop.com) or a single-* wildcard (*@thescoop.com) — matched against the sender's tag-stripped, lowercased routing key, so Mara+billing@TheScoop.com still matches a mara@thescoop.com rule. Evaluation follows the same fixed precedence as access rules:

  1. A deny match always wins.
  2. Otherwise an allow match admits.
  3. Otherwise the default settles it — and the default is derived: the presence of any allow rule flips the gate into allow-list mode (everyone else denied), while a gate with only deny rules behaves as a block-list. An attachment with no rules admits every sender.

Because grants are checked independently and their admitted members unioned, gates only ever add access across toolkits: one toolkit's deny rule doesn't subtract a tool another admitted toolkit also carries. Put a tool that needs a tighter audience in its own toolkit.

Scheduled runs

A responsibility run has no inbound sender, so sender gates can't apply. Instead each attachment opts in with Allow on scheduled runs (allow_proactive) — off by default, so a scheduled run is tool-less until you flip it on the toolkits it should draw from. If your agent's duty ends by emailing a summary, the toolkit carrying send_email needs the flag too.

Enforcement

The gate is enforced at dispatch, not just by hiding tools: the catalog offered to the model and the backstop that validates each tool call both read the same per-run resolution (available_tools_for). A tool the current sender isn't cleared for can't be called — not even by a model echoing a tool name it saw earlier in the thread's history.

System toolkits

The engine maintains two toolkits you don't create by hand (both undeletable):

  • All Tools — always holds every registered tool; its membership is rewritten to the live manifest on each sync. Attach it where you want the full catalogue (a dev workspace, a trusted internal agent).
  • Default Toolsattached automatically to every new agent on create with an open gate, so a fresh agent starts functional. The engine seeds it with every engine built-in tool (send_email, search_emails, the web and file tools — host-defined tools stay out); its membership is yours to curate into whatever baseline every new agent should get.

Both are maintained by:

bash
bin/rails protege:toolkits:sync   # idempotent — run on every deploy, after db:migrate

The task eager-loads the app first so All Tools captures every tool. Calling Protege::SystemToolkits.sync! from db/seeds.rb works too, but rake-invoked code doesn't eager-load — call Rails.application.eager_load! first, and run the sync before seeding agents so Default Tools exists when they're created.

Run the sync before creating agents

Default Tools auto-attaches on agent create. An agent created before the first sync starts with no toolkits — and therefore no tools, including reply. (Attach a toolkit by hand, or recreate the agent, to recover.)

Scenario: one agent, different access per caller

The Scoop's support agent, Sundae, serves two audiences: customers, and the shop itself. Give her two toolkits —

  • Supportlookup_order, check_flavor_stock. No gate rules: any sender who reaches Sundae at all may use them.
  • Financeprocess_refund. One allow rule: *@thescoop.com.

Now watch a single thread:

  1. jane@acme.com writes: "Order SCP-1042 arrived melted. I'd like a refund." Jane's run passes the Support gate only — Sundae can look the order up and reply, but process_refund isn't even in her catalog. She replies with the order status and says she'll check with the shop, adding mara@thescoop.com (the owner) to the thread.
  2. Mara replies on the same thread: "Approved — process it." This run's sender is mara@thescoop.com, which passes the Finance gate — process_refund appears in Sundae's catalog for this run, and she issues the refund.
  3. If Jane later writes back "process another one", her run still can't touch process_refund — the gate is checked per message, not per thread.

Same agent, same conversation, different tools per correspondent — the gate follows whoever is talking. And because enforcement is at dispatch, a model that saw process_refund succeed in the thread history still can't call it on Jane's behalf.

Two variations worth knowing:

  • Same toolkit, different terms. Attach that Finance toolkit to Scout (the internal ops agent) too, with no rules — everyone who can reach Scout at all is already gated to *@thescoop.com by her access rules, so the attachment can stay open.
  • Proactive duties. Scout's Operations toolkit has Allow on scheduled runs enabled, so her morning stock-report responsibility can call check_flavor_stock and send_email with no sender in sight.

From Ruby

Everything the dashboard does is ordinary Active Record, useful in seeds and tests:

ruby
finance = Protege::Toolkit.create!(name: "Finance", member_tool_ids: %w[process_refund])
grant   = sundae.agent_toolkits.create!(toolkit: finance)
grant.agent_toolkit_rules.create!(kind: :allow, pattern: "*@thescoop.com")

grant.permits?(sender: "mara@thescoop.com") # => true

sundae.available_tools                      # union across attachments (the dashboard catalogue view)
sundae.available_tools_for(context:)        # the per-run, gate-checked set the harness offers

Protege::Toolkit.all_tools                  # the system toolkits, by stable key
Protege::Toolkit.default_tools
  • Agents — the records toolkits attach to.
  • Tools — writing the tool classes toolkits bundle.
  • Security — the message-level guardrail that runs before tool gating.
  • A second agent & scoping — toolkit scoping, step by step.