silas 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +211 -0
- data/DEPLOY.md +111 -0
- data/README.md +87 -250
- data/app/controllers/silas/api/v1/approvals_controller.rb +10 -0
- data/app/controllers/silas/inbox/invocations_controller.rb +8 -0
- data/app/helpers/silas/inbox/trace_helper.rb +20 -5
- data/app/jobs/silas/channel_delivery_job.rb +15 -0
- data/app/models/concerns/silas/inbox/broadcastable.rb +12 -0
- data/app/models/silas/compaction.rb +32 -0
- data/app/models/silas/tool_invocation.rb +29 -3
- data/app/views/layouts/silas/inbox.html.erb +89 -30
- data/app/views/silas/inbox/invocations/_approval_card.html.erb +36 -13
- data/app/views/silas/inbox/invocations/_invocation.html.erb +17 -5
- data/app/views/silas/inbox/sessions/_row.html.erb +14 -0
- data/app/views/silas/inbox/sessions/index.html.erb +16 -15
- data/app/views/silas/inbox/sessions/show.html.erb +9 -0
- data/config/routes.rb +2 -0
- data/db/migrate/20260725000002_create_silas_compactions.rb +26 -0
- data/docs/agents.md +81 -0
- data/docs/budgets.md +67 -0
- data/docs/cancellation.md +41 -0
- data/docs/channels.md +290 -0
- data/docs/configuration.md +106 -0
- data/docs/connections.md +55 -0
- data/docs/conventions.md +161 -0
- data/docs/evals.md +95 -0
- data/docs/guarantees.md +76 -0
- data/docs/inbox-and-api.md +84 -0
- data/docs/memory.md +35 -0
- data/docs/sandbox.md +44 -0
- data/docs/tools.md +77 -0
- data/docs/tutorial.md +268 -0
- data/docs/vs-eve.md +93 -0
- data/docs/why-silas.md +87 -0
- data/lib/generators/silas/channel/channel_generator.rb +72 -0
- data/lib/generators/silas/channel/templates/channel.rb.tt +48 -0
- data/lib/generators/silas/channel/templates/controller.rb.tt +66 -0
- data/lib/generators/silas/install/install_generator.rb +11 -1
- data/lib/generators/silas/install/templates/claude_skill.md +136 -0
- data/lib/generators/silas/install/templates/ruby_llm.rb +4 -1
- data/lib/silas/adapters/ruby_llm.rb +102 -46
- data/lib/silas/channel.rb +35 -0
- data/lib/silas/compactor.rb +178 -0
- data/lib/silas/configuration.rb +16 -0
- data/lib/silas/eval/dsl.rb +7 -2
- data/lib/silas/instrumentation.rb +7 -3
- data/lib/silas/ledger.rb +2 -2
- data/lib/silas/log_subscriber.rb +5 -0
- data/lib/silas/message_builder.rb +19 -0
- data/lib/silas/registry.rb +5 -2
- data/lib/silas/schedule.rb +44 -15
- data/lib/silas/slack.rb +8 -5
- data/lib/silas/step_runner.rb +5 -0
- data/lib/silas/tools/ask_question.rb +26 -0
- data/lib/silas/version.rb +1 -1
- data/lib/silas/webhook.rb +47 -0
- data/lib/silas.rb +3 -0
- metadata +30 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fd68f6d368466b20fbb35f49628f166ac61dd60ace3a8cbd20bbc6e316c1d346
|
|
4
|
+
data.tar.gz: 94d2a902cd7f364aa33200b1ea1f98446dabee38d9e9f7431df9cd96d1fc7f41
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c386608b076c2c3dfb23e369c819a7d7b1c4fc6122f0011ea80880a46b158b25549a75388a17de250b200a60e0ada311a73df28b47c67e4007cc9e2ebef8bf3f
|
|
7
|
+
data.tar.gz: 4216b83c6925605076cb3742286699673e2a18dfd9eab3a3cc6e21479ee64e17de714737e5764acce0f147ca0023ced60ed0864b0836850c771dbab88ea84100
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,216 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.0 (2026-07-26)
|
|
4
|
+
|
|
5
|
+
The brand release: the Signals inbox, the docs surface (site + gem-shipped
|
|
6
|
+
guides + tutorial), the templates family (desk + analyst) with its anti-rot
|
|
7
|
+
CI gate, community files, and the framework-first README.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Docs for the whole surface.** New guides, all shipped in the gem:
|
|
12
|
+
`tutorial` (build the refund desk outward, one primitive per chapter),
|
|
13
|
+
`guarantees`, `tools`, `agents`, `memory`, `inbox-and-api`, `sandbox`,
|
|
14
|
+
`evals`, `budgets`, `cancellation`, `connections`, and a full
|
|
15
|
+
`configuration` reference. A docs site (Jekyll + just-the-docs, dark-first,
|
|
16
|
+
Archivo/Space Mono, `early · 0.x` chip) builds from `docs/` in CI and
|
|
17
|
+
deploys to Pages, with an `llms.txt` for coding agents. Brand assets
|
|
18
|
+
(`brand/`, hero at `docs/img/`) now live in the repo; the hero image is
|
|
19
|
+
excluded from the packaged gem.
|
|
20
|
+
- **Rails application templates, as a family** — `templates/desk.rb` and
|
|
21
|
+
`templates/analyst.rb`, each a single CI-gated `rails new -m` script.
|
|
22
|
+
`rails new desk -m
|
|
23
|
+
https://raw.githubusercontent.com/danielstpaul/silas/main/templates/desk.rb`
|
|
24
|
+
builds a deployable agent app from nothing: a refund desk with one tool per effect
|
|
25
|
+
mode (`lookup_order` idempotent, `issue_refund` transactional behind a
|
|
26
|
+
£25 approval gate, `notify_customer` at-most-once), Solid Queue **and** Solid
|
|
27
|
+
Cable wired in development (the durability contract needs a real worker; live
|
|
28
|
+
deltas need a cross-process cable), a keyless scripted stand-in so the first
|
|
29
|
+
`bin/dev` works with zero secrets, three deterministic agent evals asserting
|
|
30
|
+
the hold and the exactly-once execution, and a Signals-branded signal-board
|
|
31
|
+
landing page. The template only runs the generators the gem already tests,
|
|
32
|
+
and the `templates_smoke` workflow regenerates and tests an app from every
|
|
33
|
+
template on each push — starters that structurally cannot rot. (eve needs
|
|
34
|
+
template repos because it has no generator story; Rails has application
|
|
35
|
+
templates.) **The analyst** is the second template: a scheduled reporting
|
|
36
|
+
agent — `query_metrics` reads, `flag_anomaly` lands rows exactly once,
|
|
37
|
+
`publish_report` holds at the signal (`approval :always`), a Monday-07:00
|
|
38
|
+
schedule, and a schema-checked `final_answer` (`Turn#answer_data`). Both
|
|
39
|
+
templates also wire `ANTHROPIC_API_KEY` into Kamal's secrets so the
|
|
40
|
+
generated app deploys without a scavenger hunt.
|
|
41
|
+
- **Community files** — `CONTRIBUTING.md` (the deliberate scope no-list, the
|
|
42
|
+
chaos-gate requirement, how templates are contributed), `SECURITY.md`
|
|
43
|
+
(private vulnerability reporting; deny-by-default surfaces; contract
|
|
44
|
+
violations count), and GitHub issue forms.
|
|
45
|
+
- **The gem ships its own docs, and the installer ships a coding-agent skill.**
|
|
46
|
+
`docs/**` and `DEPLOY.md` are in the packaged gem, so `bundle show silas`
|
|
47
|
+
gives a coding agent (or an offline human) the real reference — and the
|
|
48
|
+
README's links stop 404ing for gem-only readers. `rails g silas:install` now
|
|
49
|
+
also writes `.claude/skills/silas/SKILL.md`: the `app/agent/` conventions,
|
|
50
|
+
the effect-mode and approval decision rules, and the ledger rules an agent
|
|
51
|
+
must never violate — so a Claude Code/Codex session building on this app
|
|
52
|
+
gets the framework's judgment without the human learning it first. Gemspec
|
|
53
|
+
gains `documentation_uri` and `bug_tracker_uri`.
|
|
54
|
+
|
|
55
|
+
### Changed
|
|
56
|
+
|
|
57
|
+
- **The inbox wears the brand** (direction "Signals"). Dark-first — the tokens'
|
|
58
|
+
base is the night palette and light is the `prefers-color-scheme` override —
|
|
59
|
+
with a position-light mark, a lowercase wordmark, and one white "lamp" accent
|
|
60
|
+
that never means state. The seven run states each get their aspect:
|
|
61
|
+
`in_doubt` its own violet (it is neither waiting-by-design nor failed),
|
|
62
|
+
`canceled` a dashed quiet (a lamp going out, not turning red), and two
|
|
63
|
+
UI-only relabels — `waiting` reads **held**, `completed` reads **clear**. The
|
|
64
|
+
database strings and the JSON API are untouched (`docs/conventions.md`).
|
|
65
|
+
- **Approvals and questions are hoisted to the top of the session** — the
|
|
66
|
+
operator never scrolls a long trace hunting for the card; the trace keeps a
|
|
67
|
+
one-line "held at the signal" stub in place. Live parks append their card via
|
|
68
|
+
the same Turbo broadcasts; settled cards remove themselves. Tool arguments
|
|
69
|
+
render as key/value rows and results collapse behind a disclosure; the
|
|
70
|
+
session index groups its rail by who's blocked: **Held / Working / Filed**.
|
|
71
|
+
- The playground's chat page consumes the engine's colour tokens instead of
|
|
72
|
+
re-hardcoding a palette, and hosts the hoisted approval/question cards above
|
|
73
|
+
its transcript.
|
|
74
|
+
|
|
75
|
+
- **README reframed, framework-first** — it now mirrors eve's shape exactly:
|
|
76
|
+
the mark as the logo (dark/light `<picture>`), badges, a one-paragraph
|
|
77
|
+
thesis, "The filesystem is the authoring interface" with the tree, quick
|
|
78
|
+
start, one minimal example, status/community/license. The hero illustration
|
|
79
|
+
moved to the repo's social-preview role (its inbox panel is a designed
|
|
80
|
+
idealization, not a screenshot — it shouldn't sit where users compare it to
|
|
81
|
+
the real `/silas/inbox`). The docs site nav is grouped eve-style — Tutorial
|
|
82
|
+
and Guarantees up top, then **Core / Advanced / Reference / About**
|
|
83
|
+
sections — via build-time frontmatter injection (`site/assemble.rb`, which
|
|
84
|
+
fails the build if a docs page lacks a nav entry), keeping the gem-shipped
|
|
85
|
+
markdown frontmatter-free. `docs/vs-eve.md` was **factually rewritten**
|
|
86
|
+
(2026-07-26): the previous revision described eve as a managed-cloud
|
|
87
|
+
platform; eve is fully self-hostable, and the comparison now rests on what's
|
|
88
|
+
actually different — exactly-once vs documented at-least-once, a shipped
|
|
89
|
+
production inbox vs a dev TUI, memory vs none, and the transaction-boundary
|
|
90
|
+
argument, date-stamped against eve 0.27.6 and framed as siblings (pick by
|
|
91
|
+
stack). `docs/why-silas.md` rewritten in the same voice — you build the same
|
|
92
|
+
things with Silas you'd build with any modern agent framework; the
|
|
93
|
+
guarantees are where it goes further — and DEPLOY.md dropped its stale "eve
|
|
94
|
+
without the bill" subtitle.
|
|
95
|
+
|
|
96
|
+
### Fixed
|
|
97
|
+
|
|
98
|
+
- The generated `config/initializers/ruby_llm.rb` no longer guards its whole
|
|
99
|
+
configure block behind `ANTHROPIC_API_KEY` — keyless boots skipped the
|
|
100
|
+
`use_new_acts_as` opt-in, so every demo-mode boot printed RubyLLM's legacy
|
|
101
|
+
acts_as deprecation warning. A nil key assignment is inert; the block now
|
|
102
|
+
always runs.
|
|
103
|
+
|
|
104
|
+
## 0.5.0 (2026-07-26)
|
|
105
|
+
|
|
106
|
+
Two new loop primitives (replay-safe compaction, ask_question), a whole-channel
|
|
107
|
+
generator, the adapter rebound onto RubyLLM's public single-turn seam, and
|
|
108
|
+
per-agent schedules. Chaos-gated: **295 kill/deploy cycles across both stores —
|
|
109
|
+
zero duplicate side effects, byte-identical replay** — including a new compact
|
|
110
|
+
mode that kills mid-summarisation and asserts the compaction claim is
|
|
111
|
+
exactly-once and the rebuilt provider messages are byte-identical
|
|
112
|
+
(`chaos_host/RESULTS.md`).
|
|
113
|
+
|
|
114
|
+
### Added
|
|
115
|
+
|
|
116
|
+
- **Per-agent schedules.** Named agents own their cron the way they own tools
|
|
117
|
+
and skills: `app/agents/analyst/schedules/monday_kpis.md` is discovered as
|
|
118
|
+
`agents/analyst/monday_kpis`, compiled by `silas:schedules` under a
|
|
119
|
+
collision-free recurring key, and its ticks start **the analyst** — a staff
|
|
120
|
+
member's schedule never wakes the root agent. `.rb` handlers resolve under
|
|
121
|
+
the agent's namespace (`Agents::Analyst::Schedules::MondayKpis`).
|
|
122
|
+
|
|
123
|
+
- **`ask_question` — the agent can park to ask a human something.**
|
|
124
|
+
Information, not permission: the model calls the new builtin with a
|
|
125
|
+
question, the turn parks at zero compute through the same machinery as
|
|
126
|
+
approvals (TTL, channel ping, resume gate), and the operator's free-text
|
|
127
|
+
reply becomes the tool result the model resumes with
|
|
128
|
+
(`{"answer" => "..."}`). Answer from the inbox (a question card with a text
|
|
129
|
+
box replaces approve/decline) or the API
|
|
130
|
+
(`POST /silas/api/v1/approvals/:id/answer {text:}`); `decline!` remains the
|
|
131
|
+
refusal path, and an unanswered question expires as
|
|
132
|
+
`{"answer" => nil, "note" => "question expired unanswered"}`. Channels are
|
|
133
|
+
pinged only if they implement `deliver_question` — buttons are the wrong UI
|
|
134
|
+
for free text, so transports without it simply leave the question in the
|
|
135
|
+
inbox. Disable with `config.ask_question = false`.
|
|
136
|
+
|
|
137
|
+
**Upgrade note:** adding a builtin changes the definitions digest, so turns
|
|
138
|
+
parked across the upgrade fail loudly on resume (the nondeterminism guard
|
|
139
|
+
working as designed). Settle parked turns before upgrading, or set
|
|
140
|
+
`config.ask_question = false` to keep the old digest.
|
|
141
|
+
|
|
142
|
+
- **Context compaction that survives replay.** Long sessions used to grow
|
|
143
|
+
until the provider rejected the prompt and the turn failed. Now, when the
|
|
144
|
+
measured context passes `config.compact_at` (default 0.9 of the model's
|
|
145
|
+
registry context window; set an Integer for an absolute token threshold, or
|
|
146
|
+
nil to disable), Silas summarises all prior turns into a `silas_compactions`
|
|
147
|
+
row and the conversation continues — the current turn is never compacted.
|
|
148
|
+
|
|
149
|
+
The design constraint is the durability contract: replayed executions must
|
|
150
|
+
see byte-identical message arrays, so a summary can never be computed at
|
|
151
|
+
build time. Compaction is an *effect*, made exactly-once the way tool
|
|
152
|
+
effects are — claimed compare-and-swap (unique index per session + span),
|
|
153
|
+
generated once, then read deterministically from the row forever. A crash
|
|
154
|
+
mid-summary leaves a pending row the resume finishes; a crash mid-step
|
|
155
|
+
replays against the identical compacted history. New `compact.silas`
|
|
156
|
+
instrumentation event (duration = the summarisation call). Chaos-gated with
|
|
157
|
+
a dedicated mode: kill -9 during the compacting turn, including
|
|
158
|
+
mid-summarisation.
|
|
159
|
+
|
|
160
|
+
### Changed
|
|
161
|
+
|
|
162
|
+
- **The `:ruby_llm` adapter no longer fights the library.** `Chat#complete`
|
|
163
|
+
runs RubyLLM's whole agentic loop — model, execute tools, feed results back,
|
|
164
|
+
model again — but Silas needs a single move, because the step boundary *is*
|
|
165
|
+
the durability boundary. It used to get one by registering tool proxies that
|
|
166
|
+
threw `RubyLLM::Tool::Halt` to abort the loop from the inside.
|
|
167
|
+
|
|
168
|
+
Chat is now used as the builder it is (it owns model resolution, schema
|
|
169
|
+
normalisation, system instructions and message construction) and execution
|
|
170
|
+
drops one layer to `RubyLLM::Provider#complete` — the same call Chat makes
|
|
171
|
+
internally for a single turn. Entirely public API, and the adapter got
|
|
172
|
+
smaller: no `Tool::Halt`, no hunting back through `chat.messages` for the
|
|
173
|
+
assistant reply, and the `before_message` streaming-timing oddity is gone in
|
|
174
|
+
favour of an event Silas emits itself.
|
|
175
|
+
|
|
176
|
+
**This removes Silas's exposure to the largest RubyLLM 2.0 breaking change.**
|
|
177
|
+
2.0 deletes `Tool::Halt` precisely because the loop became caller-controlled;
|
|
178
|
+
Silas no longer needs it either way. The adapter also now calls `with_tools`
|
|
179
|
+
(2.0 drops the singular `with_tool`) and its schema proxy answers to both
|
|
180
|
+
`params_schema` and `parameters_schema` (2.0 renames it), so the tool path is
|
|
181
|
+
version-agnostic today. No behaviour change for users.
|
|
182
|
+
|
|
183
|
+
### Added
|
|
184
|
+
|
|
185
|
+
- **`rails g silas:channel <name>`** — scaffolds a whole channel, not half of
|
|
186
|
+
one. Channels were reachable before (`Channel.dispatch` is a ~50-line seam)
|
|
187
|
+
but the engine ships webhook routes for Slack only, so any other transport
|
|
188
|
+
meant hand-rolling a controller, a route, and signature verification with no
|
|
189
|
+
documented contract. The generator writes the outbound `Channel` subclass,
|
|
190
|
+
a signature-verifying inbound controller, and the route that joins them —
|
|
191
|
+
with the security decisions already made: verify before anything else, sign
|
|
192
|
+
over the raw body, fail closed on a missing secret, and send approvals to an
|
|
193
|
+
operator rather than to whoever started the session.
|
|
194
|
+
- **`Silas::Webhook.verify_hmac`** — the parts of webhook verification that are
|
|
195
|
+
identical for every vendor (constant-time comparison, replay window,
|
|
196
|
+
fail-closed on a missing secret), with the vendor's shape (`payload`,
|
|
197
|
+
`prefix`, `digest`) supplied by the caller. `Silas::Slack.verify_signature`
|
|
198
|
+
now delegates to it and keeps its exact v0 scheme.
|
|
199
|
+
- **`Silas::Channel.approval_url(invocation, action)`** — a signed, expiring
|
|
200
|
+
one-click approve/decline link for *any* transport, built from the engine's
|
|
201
|
+
route set and the discovered mount point, so it works from a delivery job
|
|
202
|
+
with no routing scope. Raises with the fix when no host is configured rather
|
|
203
|
+
than minting a dead link.
|
|
204
|
+
- `docs/channels.md`: the inbound/outbound contract, a per-vendor signature
|
|
205
|
+
table, and a worked WhatsApp Cloud API example.
|
|
206
|
+
|
|
207
|
+
### Removed
|
|
208
|
+
|
|
209
|
+
- `demo/refund-desk` and `demo/churn-desk`. Both were copy-paste kits whose
|
|
210
|
+
READMEs instructed deleting a file the generated eval still asserted on —
|
|
211
|
+
broken on arrival. `examples/playground` is the example; `docs/why-silas.md`
|
|
212
|
+
and `docs/vs-eve.md` now point at it.
|
|
213
|
+
|
|
3
214
|
## 0.4.0
|
|
4
215
|
|
|
5
216
|
The architecture-and-hardening release: one shipped feature that had never
|
data/DEPLOY.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Deploying Silas
|
|
2
|
+
|
|
3
|
+
A Silas app is an ordinary Rails app, so it deploys the way your Rails apps
|
|
4
|
+
already deploy — a single cheap VPS with Kamal works fine; there is no second
|
|
5
|
+
runtime or control plane to stand up. The one thing you MUST get right: the
|
|
6
|
+
**Solid Queue worker has to run**, because the durability contract (a turn
|
|
7
|
+
surviving a crash / deploy) depends on it. A web-only deploy will accept turns
|
|
8
|
+
and never run them.
|
|
9
|
+
|
|
10
|
+
> Status: this is the reference configuration. It is correct by construction
|
|
11
|
+
> (worker wired, rescuer active, secrets injected) but has not yet been run
|
|
12
|
+
> against a live VPS end-to-end — that's the final launch step.
|
|
13
|
+
|
|
14
|
+
## 1. The worker must run
|
|
15
|
+
|
|
16
|
+
> **Queue adapter: use Solid Queue.** Silas's durability and exactly-once
|
|
17
|
+
> guarantees require a durable, serializing, DB-backed adapter. **Do not run on
|
|
18
|
+
> the in-process `:async` adapter** (Rails' development default) — it runs
|
|
19
|
+
> continuation retries on a thread pool concurrently with the original job,
|
|
20
|
+
> which double-executes steps and breaks exactly-once. Silas warns at boot if it
|
|
21
|
+
> detects the Async adapter. For scripts and single-process demos, the
|
|
22
|
+
> synchronous `:inline` adapter is safe (but has no durability).
|
|
23
|
+
|
|
24
|
+
`rails g silas:install` already appends the durability rescuer to
|
|
25
|
+
`config/recurring.yml`:
|
|
26
|
+
|
|
27
|
+
```yaml
|
|
28
|
+
production:
|
|
29
|
+
silas_dead_job_rescuer: # retries jobs failed by dead-worker reaping/pruning
|
|
30
|
+
class: Silas::DeadJobRescuerJob
|
|
31
|
+
schedule: every 30 seconds
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Pick one way to run Solid Queue:
|
|
35
|
+
|
|
36
|
+
- **Simple (one container):** run the worker inside Puma —
|
|
37
|
+
set `SOLID_QUEUE_IN_PUMA=true` (Rails 8 default plugin). Fine for a demo box.
|
|
38
|
+
- **Recommended (durable):** a dedicated `bin/jobs` process, so a `kill -9` of the
|
|
39
|
+
worker is isolated from the web tier and recovery is observable.
|
|
40
|
+
|
|
41
|
+
## 2. Kamal config (`config/deploy.yml`)
|
|
42
|
+
|
|
43
|
+
A standard `rails new` ships a Dockerfile and `config/deploy.yml`. Add a **job
|
|
44
|
+
role** so the worker is its own container:
|
|
45
|
+
|
|
46
|
+
```yaml
|
|
47
|
+
service: my-agent
|
|
48
|
+
image: you/my-agent
|
|
49
|
+
servers:
|
|
50
|
+
web:
|
|
51
|
+
- 1.2.3.4 # your £5/month VPS
|
|
52
|
+
job:
|
|
53
|
+
hosts:
|
|
54
|
+
- 1.2.3.4
|
|
55
|
+
cmd: bin/jobs # the Solid Queue worker (+ dispatcher + scheduler)
|
|
56
|
+
|
|
57
|
+
registry:
|
|
58
|
+
username: you
|
|
59
|
+
password:
|
|
60
|
+
- KAMAL_REGISTRY_PASSWORD
|
|
61
|
+
|
|
62
|
+
env:
|
|
63
|
+
secret:
|
|
64
|
+
- RAILS_MASTER_KEY
|
|
65
|
+
- ANTHROPIC_API_KEY # the :ruby_llm engine authenticates with this
|
|
66
|
+
clear:
|
|
67
|
+
SOLID_QUEUE_IN_PUMA: false # the job role runs the worker instead
|
|
68
|
+
|
|
69
|
+
# Persist SQLite + Solid Queue on a volume if you're using SQLite (or point at Postgres).
|
|
70
|
+
volumes:
|
|
71
|
+
- "my_agent_storage:/rails/storage"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Secrets (Slack signing secret / bot token, model keys) go in `.kamal/secrets`
|
|
75
|
+
(pulled from your env or a vault), never in the image.
|
|
76
|
+
|
|
77
|
+
## 3. Deploy
|
|
78
|
+
|
|
79
|
+
```sh
|
|
80
|
+
kamal setup # first time
|
|
81
|
+
kamal deploy # subsequent
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Mid-deploy safety is built in: a turn interrupted by the SIGTERM of a rolling
|
|
85
|
+
deploy re-enqueues and resumes (chaos-gated). Recovery latency after a hard
|
|
86
|
+
crash ≈ `SolidQueue.process_alive_threshold` + the rescuer's 30s cadence.
|
|
87
|
+
|
|
88
|
+
## 3½. Template apps deploy stock
|
|
89
|
+
|
|
90
|
+
An app generated from `templates/*.rb` needs no deploy surgery: Rails 8.1's
|
|
91
|
+
generated `config/deploy.yml` already runs Solid Queue inside Puma for the
|
|
92
|
+
single-server SQLite shape (`SOLID_QUEUE_IN_PUMA: true` — that satisfies "the
|
|
93
|
+
worker must run"), the template adds `ANTHROPIC_API_KEY` to Kamal's secret
|
|
94
|
+
list and `.kamal/secrets`, and `config/recurring.yml` already carries the
|
|
95
|
+
rescuer. Set the two secrets in your shell, point `deploy.yml` at your server
|
|
96
|
+
and registry, `kamal setup`. Scale beyond one box by moving to the dedicated
|
|
97
|
+
worker role in §1 and a real Postgres in §2.
|
|
98
|
+
|
|
99
|
+
## 4. Cost
|
|
100
|
+
|
|
101
|
+
The whole thing runs on one VPS + your model spend. The engine authenticates
|
|
102
|
+
with an API key for whatever provider you configure through RubyLLM. Your data
|
|
103
|
+
and the ledger never leave your own Postgres, and there is no Silas platform
|
|
104
|
+
bill or per-run metering.
|
|
105
|
+
|
|
106
|
+
## 5. Local development note (macOS + Postgres)
|
|
107
|
+
|
|
108
|
+
Solid Queue forks workers, and on macOS the pg gem needs
|
|
109
|
+
`PGGSSENCMODE=disable OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES` in the
|
|
110
|
+
environment or forked workers crash inside libpq. SQLite development needs
|
|
111
|
+
nothing.
|