Class: Silas::Configuration

Inherits:
Object
  • Object
show all
Defined in:
lib/silas/configuration.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeConfiguration

Returns a new instance of Configuration.



64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/silas/configuration.rb', line 64

def initialize
  @engine = :ruby_llm
  @auth = :api_key
  # Must be resolvable by the installed ruby_llm's model registry — newer
  # Claude models may need `RubyLLM.models.refresh!` before they resolve.
  @default_model = "claude-opus-4-8"
  @queue_name = :default
  @around_model_call = nil
  @approval_ttl = 7.days
  @max_steps = 25
  @isolate_steps = true
  @tool_resolver = nil
  @tool_definitions = nil
  @definitions_digest = nil
  @skills = nil
  @schedules = nil
  @subagent_index = nil
  @subagent_scopes = nil
  @agent_override = nil
  @instructions_dir = nil
  @channel_resolver = nil
  @slack_signing_secret = nil
  @slack_bot_token = nil
  @agent_sdk_claude_bin = "claude"
  @agent_sdk_model = nil # falls back to the agent model; must be a CLI-accepted id
  @agent_sdk_mcp_host = "127.0.0.1"
  @agent_sdk_mcp_timeout_ms = 15_000
  @agent_sdk_cli_version_range = ">= 2.1.150, < 3"
  @eval_dir = "test/agent_evals"
  @eval_grader = nil
  @sandbox = :none
  @sandbox_image = nil
  @sandbox_network = "none"
  @sandbox_memory = "512m"
  @sandbox_cpus = "1"
  @sandbox_pids = 256
  @sandbox_workdir = "/workspace"
  @sandbox_docker_bin = "docker"
  @sandbox_timeout = 30
  @inbox_auth = ->(controller) { controller.head :not_found } # deny by default
  @inbox_public_read = false
  @inbox_actor = ->(controller) { controller.try(:current_user)&.try(:email) || "inbox" }
  # Current Claude rates as cost-units per 1k tokens (1e6 units = $1):
  # Opus 4.8 $5/$25, Sonnet $3/$15, Haiku $1/$5 per million tokens.
  @model_prices = {
    "claude-opus-4-8" => { in: 5000, out: 25_000 },
    "claude-sonnet-5" => { in: 3000, out: 15_000 },
    "claude-sonnet-4-6" => { in: 3000, out: 15_000 },
    "claude-haiku-4-5" => { in: 1000, out: 5000 },
    "claude-haiku-4-5-20251001" => { in: 1000, out: 5000 }
  }
end

Instance Attribute Details

#agent_overrideObject

Subagents: the roster (name+description) and per-name scope builders, plus the active-agent overrides swapped in during a nested run.



28
29
30
# File 'lib/silas/configuration.rb', line 28

def agent_override
  @agent_override
end

#agent_sdk_claude_binObject

:agent_sdk engine knobs.



34
35
36
# File 'lib/silas/configuration.rb', line 34

def agent_sdk_claude_bin
  @agent_sdk_claude_bin
end

#agent_sdk_cli_version_rangeObject

:agent_sdk engine knobs.



34
35
36
# File 'lib/silas/configuration.rb', line 34

def agent_sdk_cli_version_range
  @agent_sdk_cli_version_range
end

#agent_sdk_mcp_hostObject

:agent_sdk engine knobs.



34
35
36
# File 'lib/silas/configuration.rb', line 34

def agent_sdk_mcp_host
  @agent_sdk_mcp_host
end

#agent_sdk_mcp_timeout_msObject

:agent_sdk engine knobs.



34
35
36
# File 'lib/silas/configuration.rb', line 34

def agent_sdk_mcp_timeout_ms
  @agent_sdk_mcp_timeout_ms
end

#agent_sdk_modelObject

:agent_sdk engine knobs.



34
35
36
# File 'lib/silas/configuration.rb', line 34

def agent_sdk_model
  @agent_sdk_model
end

#approval_ttlObject

How long a parked approval (or in-doubt invocation) waits before expiring. eve parks forever; Silas expires (parked-forever ghosts are a bug, not a feature).



16
17
18
# File 'lib/silas/configuration.rb', line 16

def approval_ttl
  @approval_ttl
end

#around_model_callObject

Optional hook wrapping every model call — e.g. ruby_llm-resilience:

config.around_model_call = ->(ctx, &call) { RubyLLM::Resilience.chain(:anthropic) { call.() } }


13
14
15
# File 'lib/silas/configuration.rb', line 13

def around_model_call
  @around_model_call
end

#authObject

Auth mode for :agent_sdk — :api_key or :oauth (subscription).



6
7
8
# File 'lib/silas/configuration.rb', line 6

def auth
  @auth
end

#channel_resolverObject

Channels: name -> Channel subclass (wired by the Registry). Slack creds default to credentials.dig(:silas, :slack, ...); nil disables Slack.



31
32
33
# File 'lib/silas/configuration.rb', line 31

def channel_resolver
  @channel_resolver
end

#default_modelObject

Default model when agent.yml doesn't specify one.



8
9
10
# File 'lib/silas/configuration.rb', line 8

def default_model
  @default_model
end

#definitions_digestObject

Injectable seams until/beyond the Registry: name -> tool object, the tool schema list for the model, and the definitions digest for the nondeterminism guard. The Registry wires real defaults at boot.



25
26
27
# File 'lib/silas/configuration.rb', line 25

def definitions_digest
  @definitions_digest
end

#engineObject

Inference engine seam: :ruby_llm (Phase 1) or :agent_sdk (stub).



4
5
6
# File 'lib/silas/configuration.rb', line 4

def engine
  @engine
end

#eval_dirObject

Evals: where *_eval.rb scenarios live, and an optional custom LLM grader.



48
49
50
# File 'lib/silas/configuration.rb', line 48

def eval_dir
  @eval_dir
end

#eval_graderObject

Evals: where *_eval.rb scenarios live, and an optional custom LLM grader.



48
49
50
# File 'lib/silas/configuration.rb', line 48

def eval_grader
  @eval_grader
end

#inbox_actorObject

Inbox (mountable UI at /silas/inbox). inbox_auth — deny-by-default lambda; the host renders/head-404s to DENY and passes by NOT rendering (resilience pattern). inbox_public_read — reads render for anyone; approve/decline still gated. inbox_actor — controller -> identity string (approved_by/decline by:). model_prices — model id -> out: cost-units per 1k tokens, where 1_000_000 units = $1 (so a $3/M-token input rate is 3000 units/1k). Cost.format divides by 1e6.



44
45
46
# File 'lib/silas/configuration.rb', line 44

def inbox_actor
  @inbox_actor
end

#inbox_authObject

Inbox (mountable UI at /silas/inbox). inbox_auth — deny-by-default lambda; the host renders/head-404s to DENY and passes by NOT rendering (resilience pattern). inbox_public_read — reads render for anyone; approve/decline still gated. inbox_actor — controller -> identity string (approved_by/decline by:). model_prices — model id -> out: cost-units per 1k tokens, where 1_000_000 units = $1 (so a $3/M-token input rate is 3000 units/1k). Cost.format divides by 1e6.



44
45
46
# File 'lib/silas/configuration.rb', line 44

def inbox_auth
  @inbox_auth
end

#inbox_public_readObject

Inbox (mountable UI at /silas/inbox). inbox_auth — deny-by-default lambda; the host renders/head-404s to DENY and passes by NOT rendering (resilience pattern). inbox_public_read — reads render for anyone; approve/decline still gated. inbox_actor — controller -> identity string (approved_by/decline by:). model_prices — model id -> out: cost-units per 1k tokens, where 1_000_000 units = $1 (so a $3/M-token input rate is 3000 units/1k). Cost.format divides by 1e6.



44
45
46
# File 'lib/silas/configuration.rb', line 44

def inbox_public_read
  @inbox_public_read
end

#inbox_streamingObject

Force-disable live broadcasting even when turbo-rails is present (nil = auto).



46
47
48
# File 'lib/silas/configuration.rb', line 46

def inbox_streaming
  @inbox_streaming
end

#instructions_dirObject

Subagents: the roster (name+description) and per-name scope builders, plus the active-agent overrides swapped in during a nested run.



28
29
30
# File 'lib/silas/configuration.rb', line 28

def instructions_dir
  @instructions_dir
end

#isolate_stepsObject

Continuation isolation for loop steps. true in production (persistence per step — the durability contract); specs may disable for inline runs.



21
22
23
# File 'lib/silas/configuration.rb', line 21

def isolate_steps
  @isolate_steps
end

#max_stepsObject

Hard cap on model calls per turn (agent.yml can lower it per-agent).



18
19
20
# File 'lib/silas/configuration.rb', line 18

def max_steps
  @max_steps
end

#mcp_client_factoryObject

Connections: a test seam for injecting a fake MCP client ->(connection){ client }.



50
51
52
# File 'lib/silas/configuration.rb', line 50

def mcp_client_factory
  @mcp_client_factory
end

#model_pricesObject

Inbox (mountable UI at /silas/inbox). inbox_auth — deny-by-default lambda; the host renders/head-404s to DENY and passes by NOT rendering (resilience pattern). inbox_public_read — reads render for anyone; approve/decline still gated. inbox_actor — controller -> identity string (approved_by/decline by:). model_prices — model id -> out: cost-units per 1k tokens, where 1_000_000 units = $1 (so a $3/M-token input rate is 3000 units/1k). Cost.format divides by 1e6.



44
45
46
# File 'lib/silas/configuration.rb', line 44

def model_prices
  @model_prices
end

#queue_nameObject

Active Job queue for agent turns.



10
11
12
# File 'lib/silas/configuration.rb', line 10

def queue_name
  @queue_name
end

#sandboxObject

Sandbox: :none (default, code exec off) | :docker | any object responding to #run. Docker knobs are inert unless :docker.



53
54
55
# File 'lib/silas/configuration.rb', line 53

def sandbox
  @sandbox
end

#sandbox_cpusObject

Sandbox: :none (default, code exec off) | :docker | any object responding to #run. Docker knobs are inert unless :docker.



53
54
55
# File 'lib/silas/configuration.rb', line 53

def sandbox_cpus
  @sandbox_cpus
end

#sandbox_docker_binObject

Sandbox: :none (default, code exec off) | :docker | any object responding to #run. Docker knobs are inert unless :docker.



53
54
55
# File 'lib/silas/configuration.rb', line 53

def sandbox_docker_bin
  @sandbox_docker_bin
end

#sandbox_imageObject

Sandbox: :none (default, code exec off) | :docker | any object responding to #run. Docker knobs are inert unless :docker.



53
54
55
# File 'lib/silas/configuration.rb', line 53

def sandbox_image
  @sandbox_image
end

#sandbox_memoryObject

Sandbox: :none (default, code exec off) | :docker | any object responding to #run. Docker knobs are inert unless :docker.



53
54
55
# File 'lib/silas/configuration.rb', line 53

def sandbox_memory
  @sandbox_memory
end

#sandbox_networkObject

Sandbox: :none (default, code exec off) | :docker | any object responding to #run. Docker knobs are inert unless :docker.



53
54
55
# File 'lib/silas/configuration.rb', line 53

def sandbox_network
  @sandbox_network
end

#sandbox_pidsObject

Sandbox: :none (default, code exec off) | :docker | any object responding to #run. Docker knobs are inert unless :docker.



53
54
55
# File 'lib/silas/configuration.rb', line 53

def sandbox_pids
  @sandbox_pids
end

#sandbox_timeoutObject

Sandbox: :none (default, code exec off) | :docker | any object responding to #run. Docker knobs are inert unless :docker.



53
54
55
# File 'lib/silas/configuration.rb', line 53

def sandbox_timeout
  @sandbox_timeout
end

#sandbox_workdirObject

Sandbox: :none (default, code exec off) | :docker | any object responding to #run. Docker knobs are inert unless :docker.



53
54
55
# File 'lib/silas/configuration.rb', line 53

def sandbox_workdir
  @sandbox_workdir
end

#schedulesObject

Injectable seams until/beyond the Registry: name -> tool object, the tool schema list for the model, and the definitions digest for the nondeterminism guard. The Registry wires real defaults at boot.



25
26
27
# File 'lib/silas/configuration.rb', line 25

def schedules
  @schedules
end

#skillsObject

Injectable seams until/beyond the Registry: name -> tool object, the tool schema list for the model, and the definitions digest for the nondeterminism guard. The Registry wires real defaults at boot.



25
26
27
# File 'lib/silas/configuration.rb', line 25

def skills
  @skills
end

#slack_bot_tokenObject



60
61
62
# File 'lib/silas/configuration.rb', line 60

def slack_bot_token
  @slack_bot_token || Rails.application.credentials.dig(:silas, :slack, :bot_token)
end

#slack_signing_secretObject



56
57
58
# File 'lib/silas/configuration.rb', line 56

def slack_signing_secret
  @slack_signing_secret || Rails.application.credentials.dig(:silas, :slack, :signing_secret)
end

#subagent_indexObject

Subagents: the roster (name+description) and per-name scope builders, plus the active-agent overrides swapped in during a nested run.



28
29
30
# File 'lib/silas/configuration.rb', line 28

def subagent_index
  @subagent_index
end

#subagent_scopesObject

Subagents: the roster (name+description) and per-name scope builders, plus the active-agent overrides swapped in during a nested run.



28
29
30
# File 'lib/silas/configuration.rb', line 28

def subagent_scopes
  @subagent_scopes
end

#tool_definitionsObject

Injectable seams until/beyond the Registry: name -> tool object, the tool schema list for the model, and the definitions digest for the nondeterminism guard. The Registry wires real defaults at boot.



25
26
27
# File 'lib/silas/configuration.rb', line 25

def tool_definitions
  @tool_definitions
end

#tool_resolverObject

Injectable seams until/beyond the Registry: name -> tool object, the tool schema list for the model, and the definitions digest for the nondeterminism guard. The Registry wires real defaults at boot.



25
26
27
# File 'lib/silas/configuration.rb', line 25

def tool_resolver
  @tool_resolver
end

Instance Method Details

#boot_guard!Object

PLAN.md §1, non-negotiable: raise if subscription OAuth is configured while an API key is present — the key would silently win and bill credits. The inverse also holds: :agent_sdk runs --bare (API-key auth only), so api_key mode needs a key present.



126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/silas/configuration.rb', line 126

def boot_guard!
  if engine == :agent_sdk && auth == :oauth && ENV["ANTHROPIC_API_KEY"].present?
    raise BootGuardError,
          "engine :agent_sdk with auth: :oauth is configured, but ANTHROPIC_API_KEY exists " \
          "in the environment. The key would silently override subscription OAuth and drain " \
          "API credits. Unset ANTHROPIC_API_KEY or switch to auth: :api_key."
  end

  if engine == :agent_sdk && auth == :api_key && ENV["ANTHROPIC_API_KEY"].blank?
    raise BootGuardError,
          "engine :agent_sdk uses --bare (API-key auth only) but ANTHROPIC_API_KEY is not set."
  end

  warn_unsafe_queue_adapter!
end

#validate!Object



117
118
119
120
# File 'lib/silas/configuration.rb', line 117

def validate!
  boot_guard!
  self
end

#warn_unsafe_queue_adapter!Object

Silas's durability and exactly-once guarantees rest on the queue adapter: a turn's jobs must run one-at-a-time and continuation retries must run in a FRESH execution after the prior one committed. The in-process Async adapter (ActiveJob's dev default) breaks both — it runs a re-enqueued continuation on a thread pool concurrently with the original, which double-executes steps and violates the single-active-turn invariant (a re-run model call mints new tool_call ids the ledger cannot dedup). Solid Queue — or any durable, serializing, DB-backed adapter — is required to run agents; the synchronous :inline adapter is safe for scripts and demos (no durability).



151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/silas/configuration.rb', line 151

def warn_unsafe_queue_adapter!
  return unless defined?(::ActiveJob::Base)

  name = ::ActiveJob::Base.queue_adapter.class.name.to_s
  return unless name.include?("AsyncAdapter")

  message = "[Silas] ActiveJob is using the in-process Async queue adapter. " \
            "This runs continuation retries on a thread pool concurrently with " \
            "the original job, which double-executes agent steps and breaks " \
            "exactly-once tool effects. Use :solid_queue (production) or " \
            ":inline (scripts/demos). See Silas DEPLOY.md."
  (defined?(::Rails) && ::Rails.logger ? ::Rails.logger.warn(message) : nil) || Kernel.warn(message)
rescue StandardError
  # A diagnostic must never break boot.
  nil
end