Skip to content

Configuration

Protege is configured once at boot, in an initializer, and treated as read-only thereafter — the Orchestrator snapshots a frozen copy of the config for each run. rails g protege:install writes a fully-documented config/initializers/protege.rb; this page explains every option in it.

ruby
# config/initializers/protege.rb
Protege.configure do |config|
  config.provider_id = :openrouter
  config.providers   = {
    openrouter: {
      model:    ENV.fetch("OPENROUTER_MODEL", "anthropic/claude-sonnet-4-5"),
      api_key:  ENV.fetch("OPENROUTER_API_KEY", nil),
      base_url: ENV.fetch("OPENROUTER_BASE_URL", "https://openrouter.ai/api/v1")
    }
  }
end

provider_id

Default: nil. The symbolic id of the provider Protege runs inference through. It must match a registered provider's protege_id:openrouter for the bundled adapter, or the id you gave a custom one. Until it's set, no inference can run, so this is effectively required.

ruby
config.provider_id = :openrouter

providers

Default: { openrouter: {} }. A plain hash keyed by provider id. Each provider reads its own slice for the model, credentials, and any sampling knobs — the engine treats the values as opaque, so a custom provider can put whatever keys it needs under its own id and read them back with Protege.configuration.provider_options(:its_id).

ruby
config.providers = {
  openrouter: {
    model:    ENV.fetch("OPENROUTER_MODEL", "anthropic/claude-sonnet-4-5"),
    api_key:  ENV.fetch("OPENROUTER_API_KEY", nil),
    base_url: "https://openrouter.ai/api/v1"
    # temperature: 0.7,
    # max_output_tokens: 4096
  },
  scoop_llm: { api_key: ENV["SCOOP_LLM_KEY"], region: "us" }   # a custom provider's slice
}

Assignment replaces the whole hash, so include every provider you configure — not just the active one. The bundled OpenRouter provider requires model, api_key, and base_url in its slice (they raise on first use if missing); temperature and max_output_tokens are optional.

max_tool_turns

Default: 8. How many tool-calling rounds the harness allows before it stops and returns whatever the model last produced. It's the guard against a runaway tool chain — an agent that keeps calling tools without ever answering. Raise it for genuinely multi-step work; lower it to cap cost and latency on simple agents.

ruby
config.max_tool_turns = 8

console_address

Default: "console@protege.local". The address that represents the dashboard user in conversations started from the console. It's how the engine recognizes an in-app message: replies addressed to it stay entirely in-app and skip SMTP delivery, so you can exercise an agent from the dashboard (and via Gateway.message) without sending real mail.

ruby
config.console_address = "console@protege.local"

Default: "🥚 Protege". The brand shown in the dashboard's nav header — the place to white-label the console for your own product. The Scoop might set it to its own name so the dashboard reads as an in-house tool.

ruby
config.nav_title = "The Scoop · Agents"

tracing

Default: { enabled: false }. Turns tracing on or off. When enabled, the harness snapshots every inference turn (the exact request, the response, and the model/settings) into a Protege::Trace row for later review and fine-tuning. It's off by default because nothing should be captured until you opt in — and because multimodal turns store attachment bytes inline, so rows can be large.

ruby
config.tracing = { enabled: true }

inbound_access

Default: permit-all. The global ceiling on which senders may reach any agent — the committed, org-wide layer of the access guardrail. Assign a policy built through Gateway.build_access_policy. Per-agent access rules can only narrow this further, never widen past it, so this is where you set the outer boundary once.

ruby
config.inbound_access = Protege::Gateway.build_access_policy(allow: ["*@thescoop.com"])
# or a block-list:
config.inbound_access = Protege::Gateway.build_access_policy(deny: ["*@spam.example"])

Patterns are an exact address or a single * wildcard, matched against the sender's tag-stripped, lowercased address. Supplying an allow list flips the policy into allow-list mode (default-deny).

attachment_policy

Default: enabled, 10 MB per attachment, 10 attachments per message, 25 MB total. The limits enforced at the mail edges — inbound mail that breaches any limit is bounced, and the send_email tool refuses to attach a breaching set. Assign a policy built through Gateway.build_attachment_policy.

ruby
config.attachment_policy = Protege::Gateway.build_attachment_policy(max_bytes: 5 * 1024 * 1024)
# or turn attachments off entirely:
config.attachment_policy = Protege::Gateway.build_attachment_policy(enabled: false)

http_user_agent

Default: a generic, unbranded Chrome string. The User-Agent the built-in web_search and web_fetch tools send on outbound HTTP. The default is deliberately unbranded because some origins reject or fingerprint unusual agents. Override it to present your host's own browser agent.

ruby
config.http_user_agent = ENV.fetch("HTTP_USER_AGENT", Protege::Configuration::DEFAULT_HTTP_USER_AGENT)

logger

Default: Rails.logger. The logger the engine writes to. Override it to route Protege's output — tool failures, swallowed hook errors, provisioning warnings — to a dedicated target.

ruby
config.logger = Logger.new(Rails.root.join("log/protege.log"))

Extension scaffold paths

Defaults: app/tools, app/resolvers, app/hooks, app/personas, app/providers. Where the rails g protege:{tool,resolver,hook,persona,provider} generators write their files. Override them if you group extensions elsewhere — but the target must stay under an autoloaded path, or Rails (and Protege's subclass discovery) won't load them.

ruby
config.tools_path     = "app/tools"
config.resolvers_path = "app/resolvers"
config.hooks_path     = "app/hooks"
config.personas_path  = "app/personas"
config.providers_path = "app/providers"

Secrets

Nothing secret is hard-coded; credentials come from the environment. The variables the generated defaults read:

VariableUsed for
OPENROUTER_API_KEYThe bundled OpenRouter provider's key
OPENROUTER_MODELThe model OpenRouter targets (e.g. anthropic/claude-sonnet-4-5)
OPENROUTER_BASE_URLOverride the OpenRouter endpoint (rarely needed)
RAILS_INBOUND_EMAIL_PASSWORDAction Mailbox ingress auth (and the self-hosted MTA push) — see Mail

A note on development

Extensions are discovered as loaded subclasses, so in development you must let Rails load them eagerly — add config.eager_load = true to config/environments/development.rb, or your tools, hooks, and providers won't register until first referenced.