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.
# 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")
}
}
endprovider_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.
config.provider_id = :openrouterproviders
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).
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.
config.max_tool_turns = 8console_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.
config.console_address = "console@protege.local"nav_title
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.
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.
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.
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.
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.
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.
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.
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:
| Variable | Used for |
|---|---|
OPENROUTER_API_KEY | The bundled OpenRouter provider's key |
OPENROUTER_MODEL | The model OpenRouter targets (e.g. anthropic/claude-sonnet-4-5) |
OPENROUTER_BASE_URL | Override the OpenRouter endpoint (rarely needed) |
RAILS_INBOUND_EMAIL_PASSWORD | Action 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.
Related
- Providers — what a
providersslice feeds. - Security — the access guardrail in depth.
- Generators & CLI — the initializer this documents is generated.

