@stratusagent/agents 0.13.7 → 0.14.1

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.
@@ -72,19 +72,21 @@ This is the question you will be asked most. A secret reaches Stratus in one of
72
72
  - **Provider sign-ins** (the model's own key or subscription): `stratus setup` → Providers, or the dashboard. There is no `stratus login`.
73
73
  - **One agent on its own Claude subscription**: `stratus signin set anthropic --agent <id>` at the machine, with a token from `claude setup-token` typed at the hidden prompt or piped on stdin. It replaces the shared Anthropic sign-in for that agent only, from its next turn, no restart. `stratus signin remove anthropic --agent <id>` goes back to the shared one, and `stratus signins` lists who has one. It is not a named credential: you cannot request, read, or resolve it, and `credential.request` is the wrong tool for it. If its token is refused, the turn fails with an error naming that agent's sign-in rather than moving onto the shared account.
74
74
  - **Channel tokens** (a Slack app's tokens): `stratus setup` → Channels, or the dashboard. No agent can read them, yours included.
75
- - **Named credentials**, such as `search.apiKey` or `github.token`, are for tools whose plugin declares that name, and for shell commands that `credentialsEnv` maps one into (next). A web search backend is the common one: its key is always `search.apiKey`, whatever the vendor. These are the only secrets you can ask for yourself.
76
- - **A shell command's token** reaches `shell.run` as an environment variable. Your operator sets that up in the trusted config at the machine, and a change there needs a daemon restart:
77
- - from the credential store, with `@stratusagent/tool-shell`'s `credentialsEnv`: `{ "GH_TOKEN": "github.token" }` gives each of your commands your stored `github.token` in `GH_TOKEN`, so a CLI that reads its token from a variable just works. It is a named credential like any other: your soul must list it, you can ask for it with `credential.request`, and a replaced value reaches the next command with no restart. Fleet-wide, or under `agents.<id>` for one agent;
78
- - or as a value written into tool-shell's `env`, under `agents.<id>` to keep it to one agent. An agent's `env` is added to the shared one rather than replacing it, its own names win, and a name set to `null` there is withheld from that agent.
75
+ - **Named credentials**, such as `search.apiKey` or `github.token`, are for tools whose plugin declares that name, and for shell commands that read one from an environment variable (next). A web search backend is the common one: its key is always `search.apiKey`, whatever the vendor. These are the only secrets you can ask for yourself.
76
+ - **A shell command's token** reaches `shell.run` as an environment variable, so a CLI or script that reads its token from one just works. It is a named credential like any other (your soul must list it), bound to the variable one of these ways:
77
+ - **Ask for it with `env`**: `credential.request` with the `name` and `env: "TWILIO_AUTH_TOKEN"`. The form tells the approver your shell will be able to read it as that variable, and answering it is their approval for that. Once they add it, your commands have `$TWILIO_AUTH_TOKEN` from your next reply, with no config edit and no restart. `env` must be uppercase and end in `_TOKEN`, `_KEY`, `_SECRET`, `_PASSWORD`, `_SID` or `_ID`; any other name is refused before anyone is asked. For a key you already hold, it is refused with the one command your operator runs instead.
78
+ - **Your operator binds it at the machine**: `stratus credential env <name> <VAR> --agent <id>`, or `stratus credential set <name> --agent <id> --env <VAR>` while storing it. Any variable except ones that change how commands run (`PATH`, `HOME`, `BASH_ENV`, `GIT_DIR` and the like), and it applies from the next command with no restart. `--remove` undoes it, and removing the key removes its bindings.
79
+ - **The trusted config**, with `@stratusagent/tool-shell`'s `credentialsEnv`: `{ "GH_TOKEN": "github.token" }`, fleet-wide or under `agents.<id>` for one agent. It is the operator's pin: a variable it maps is never taken from a binding. A config edit needs a daemon restart;
80
+ - or as a value written into tool-shell's `env`, under `agents.<id>` to keep it to one agent. An agent's `env` is added to the shared one rather than replacing it, its own names win, and a name set to `null` there is withheld from that agent. A config edit, so a restart.
79
81
 
80
82
  Either way your commands can read the value (`echo $GH_TOKEN` prints it), so only narrowly scoped tokens belong there.
81
83
  - **An MCP server's token** is not a named credential: MCP servers never read the credential store. Your operator puts it in `servers.<name>.headers` (HTTP) or `servers.<name>.env` (stdio) in the trusted config at the machine, which every agent granted `mcp.<name>.*` uses, and the daemon needs a restart. Its replies are labelled `external`, and so is everything you remember after reading them, unless the operator sets `servers.<name>.outputTrust` to `agent` (or `unknown`) for a server they run; never `user`.
82
84
 
83
85
  `passEnv` beside tool-shell's or a server's `env` forwards variables, values included, from the daemon's own environment, and only ones that are not secret belong on it.
84
86
 
85
- For a named credential to reach you, three things must all be true. It is **stored**, your soul's `credentials:` **lists its name**, and a tool you have **declares it** (tool-shell declares the names its `credentialsEnv` maps). Storing grants nothing by itself. Tools use the value on your behalf, and there is no file for you to look in. The one place a value shows up is a `shell.run` command's environment, in the variable `credentialsEnv` maps it to. Stored entries are read before an environment variable of the same name, and your own entry before a shared one.
87
+ For a named credential to reach you, three things must all be true. It is **stored**, your soul's `credentials:` **lists its name**, and a tool you have **declares it**, or it is **bound to a variable** for your shell (tool-shell reads bindings and the names its `credentialsEnv` maps). Storing grants nothing by itself. Tools use the value on your behalf, and there is no file for you to look in. The one place a value shows up is a `shell.run` command's environment, in the variable it is bound or mapped to. Stored entries are read before an environment variable of the same name, and your own entry before a shared one.
86
88
 
87
- When you need a named credential you do not hold, first make sure a tool you have uses that name. A key no tool reads does nothing once stored; if what you need is a tool, say which plugin or tool is missing instead. Then:
89
+ When you need a named credential you do not hold, first make sure a tool you have uses that name, or that a command you run reads it from an environment variable (then ask with `env`). A key nothing reads does nothing once stored; if what you need is a tool, say which plugin or tool is missing instead. Then:
88
90
 
89
91
  **In Slack, the form comes first.** When someone in a Slack conversation asks how to give you a key, or you need one there, put up the form with `credential.request` instead of explaining terminal steps: the value never touches the conversation, and nobody has to be at the machine. Give the `stratus credential set` command only when they ask for it, when the tool says no form can go up here, or when they say they are at the machine. For example, asked in a Slack thread "how do I give you the GitHub token?", call `credential.request` with `name: "github.token"` and a reason, then reply: "I put a secure form in this thread. An approver enters the token there, it goes straight to the store, and I can use it from my next reply." Not a paragraph of `read -rs KEY` steps.
90
92
 
@@ -92,15 +94,16 @@ When you need a named credential you do not hold, first make sure a tool you hav
92
94
  - **A form**, in Slack, goes up in this conversation when one of `approvals.slackApprovers` can see it: a public channel, a private one with an approver in it, or a direct message with an approver.
93
95
  - **A link** comes back otherwise, or when you pass `via: "link"`, but only if your soul's `tools:` lists `credential.request` (or you have no `tools:` key); without that, it is a form or nothing. It needs the control API, and without `api.publicUrl` it opens only on the daemon's machine. Anyone who has the link can set the key, once, within 30 minutes, with no approver involved, and opening it spends nothing. So give it only to your operator. In a shared channel, or a direct message with anyone else, do not post it: ask your operator to message you directly, and ask again there. Pass `via: "form"` to get a form or nothing.
94
96
  - Either way the key goes straight to the store and the name into your soul, and you can use it from your next reply. A restart cancels a request still waiting.
97
+ - **A key that spans lines**, such as a GitHub App's PEM private key: the link's field keeps line breaks and the Slack form's one-line field does not, so ask for a new one with `via: "link"`. Replacing one is form-only, so for a multi-line replacement ask your operator to run `stratus credential set <name> --agent <your id> < key.pem` on the machine instead. A PEM key that arrives with its line breaks lost is refused, so nothing broken is stored.
95
98
  2. **Otherwise, ask your operator to run, on the machine:**
96
99
 
97
100
  ```
98
101
  read -rs KEY && printf %s "$KEY" | stratus credential set <name> --agent <your id>
99
102
  ```
100
103
 
101
- (leave out `--agent` for a shared one), and to add `<name>` to `credentials:` in your soul.
104
+ (leave out `--agent` for a shared one; for a key file, `stratus credential set <name> --agent <your id> < key.pem`), and to add `<name>` to `credentials:` in your soul.
102
105
 
103
- Away from the machine, the control API and a link only **add**: they store a name nobody has stored, and refuse one already stored or supplied by the daemon's environment. The Slack form adds too, and it is the one way to **replace** a key from chat. When one you hold is expiring or was revoked, call `credential.request` with `replace: true`. That is always a form an approver answers in this conversation, never a link. It is refused before anyone is asked when you don't hold the key, when it comes from the daemon's environment, or when your `scope` doesn't match where it is stored: a shared key needs `scope: "shared"`, because replacing it changes the key for every agent granted it. The new value is used from the next tool call or command. At the machine, `stratus credential set` replaces as well, and `stratus credential remove` is the only way to remove one. Each add or replace from a form or a link appends a line to `~/.stratus/credential-audit.log`, names only. `stratus credentials` lists the names stored, shared and per agent, never the values.
106
+ Away from the machine, the control API and a link only **add**: they store a name nobody has stored, and refuse one already stored or supplied by the daemon's environment. The Slack form adds too, and it is the one way to **replace** a key from chat. When one you hold is expiring or was revoked, call `credential.request` with `replace: true`. That is always a form an approver answers in this conversation, never a link. It is refused before anyone is asked when you don't hold the key, when it comes from the daemon's environment, or when your `scope` doesn't match where it is stored: a shared key needs `scope: "shared"`, because replacing it changes the key for every agent granted it. The new value is used from the next tool call or command. At the machine, `stratus credential set` replaces as well, and `stratus credential remove` is the only way to remove one. Each add or replace from a form or a link appends a line to `~/.stratus/credential-audit.log`, names only, with the variable when one was bound, and so does each `stratus credential env`. `stratus credentials` lists the names stored, shared and per agent, and what each agent's shell reads, never the values.
104
107
 
105
108
  A stored named credential or provider key takes effect on the next turn, with no restart. One that comes from an environment variable is fixed when the daemon starts, and the background service does not read a shell profile.
106
109
 
@@ -112,7 +115,7 @@ When a stored key "isn't found", check in this order:
112
115
 
113
116
  ## Tools, plugins, and approvals
114
117
 
115
- - **Built in everywhere:** `memory.*` (remember, recall, forget, pin) and `skill.read`, which every agent has because this skill counts. **Only under the daemon:** `schedule.*`, `message.send`, `message.read`, `channel.create`, `agent.delegate`, and `credential.request`, so `stratus run` and `stratus chat` cannot call them.
118
+ - **Built in everywhere:** `memory.*` (remember, recall, forget, pin) and `skill.read`, which every agent has because this skill counts. **Only under the daemon:** `schedule.*`, `message.send`, `message.read`, `channel.create`, `channel.join`, `channel.invite`, `channel.archive`, `agent.delegate`, and `credential.request`, so `stratus run` and `stratus chat` cannot call them.
116
119
  - **Plugins** add the rest, each installed with `npm install -g <package>` and enabled under `plugins` in the trusted config:
117
120
  - `@stratusagent/tool-fs` gives `fs.*`, inside configured roots only, so with no roots there is no filesystem. Whatever the roots, `fs.*` never reaches `~/.stratus` outside the agents' workspaces (credentials, config, logs, sessions, memories, souls, skills, grants), so a refusal there is by design, not a roots problem. Roots go in `"@stratusagent/tool-fs": { "enabled": true, "roots": ["~/notes"] }`, or under `"agents": { "<id>": { "roots": [...] } }` for one agent.
118
121
  - `@stratusagent/tool-shell` gives `shell.run`.
@@ -123,7 +126,7 @@ When a stored key "isn't found", check in this order:
123
126
  - Enabled is not granted: your soul's `tools:` must also cover a plugin's tools. `stratus plugins` walks the whole chain for each tool (installed, enabled, granted to which agents, and what approvals do with a call), so it answers "why can't you use X" in one command.
124
127
  - **Risk.** Every tool is `safe`, `gated`, or `dangerous`. The daemon runs `safe` tools unattended, among them `memory.*`, `skill.read`, `credential.request`, `agent.delegate`, `schedule.list`, `schedule.cancel`, and `fs.read`, `fs.list`, and `fs.search`. Gated tools include:
125
128
  - `web.fetch`, `fs.write`, `fs.move`, `shell.run`, `browser.*`, and `mcp.*`;
126
- - `schedule.every`, `schedule.at`, `message.send`, `message.read` (which reads a channel your Slack app is in, or the DM you're talking in, never any other DM, and labels what it returns `external`; a top-level read also returns the channel's bookmarks, which need the `bookmarks:read` scope; a DM read is refused before any approval prompt, since approving can't make it run), and `channel.create` (which makes a channel with your own app and invites people and agents; on Slack it needs the `channels:manage` or `groups:write` scope, which apps don't have by default, and a failed invite or topic is reported while the channel stays, so don't retry the create);
129
+ - `schedule.every`, `schedule.at`, `schedule.on` (an event trigger: your prompt runs when a signed delivery from a source the operator configured passes your filter, labeled `external`; it is refused for a source the daemon doesn't have), `message.send`, `message.read` (which reads a channel your Slack app is in, or the DM you're talking in, never any other DM, and labels what it returns `external`; a top-level read also returns the channel's bookmarks, which need the `bookmarks:read` scope; images on the messages read are shown to you after the result, at most 5 and 10 MB per read, with Slack's preview for one over 5 MB, and each message's `shownImages` and `imageNotes` say which you were shown and why any other wasn't, so read fewer messages to see the rest and never describe an image you weren't shown; a DM read is refused before any approval prompt, since approving can't make it run), and `channel.create` (which makes a channel with your own app and invites people and agents; on Slack it needs the `channels:manage` or `groups:write` scope, which apps don't have by default, and a failed invite or topic is reported while the channel stays, so don't retry the create), `channel.join` (which joins an existing public channel with your own app, by its `C…` id, never a name; on Slack it needs the `channels:join` scope, and a private channel can't be joined at all: a member has to invite your app) and `channel.invite` (which invites people and agents to a channel your app is already in; on Slack it needs `channels:write.invites` or `groups:write.invites`, or the `channel.create` scopes; it fails only when nobody was invited, otherwise `notInvited` says who wasn't and why), and `channel.archive` (which archives a channel your own app created, by its id; it refuses a channel anyone else made, a DM, a shared channel and the conversation you were asked from, that last one before any approval prompt; you can't undo it, but messages are kept and a person can unarchive it in Slack; on Slack it uses the `channel.create` scopes, and it refuses every channel when another agent here shares your Slack app);
127
130
  - every third-party tool, `web.search` included.
128
131
  - Under the default `approvals.posture: "open"` a gated call runs without asking; what needs approval is a dangerous call (and, under `externalContent: "gate"`, a gated one after external content). Under `"guarded"` every uncovered gated call does. What happens to a call that needs approval depends on the approvals mode. Under `headless` (the default) it is refused. Under `remote` the people in `approvals.slackApprovers` are asked in Slack (or through the control API's `/approvals`) with **Allow once**, **Always allow**, and **Deny**. Under `remote` with nobody to ask, it is refused too.
129
132
  - Either way, what was already approved still runs unattended. For `shell.run` that is a command scope, a command granted by name, plus a short built-in safe list (`cd`, `pwd`, `echo`, `true`, `false`, `whoami`, and `uname`; no `git` command, not even `git status`, since git runs programs a repository names), plus `grep`, `head`, `tail`, `wc`, `sort`, and `uniq` as filters that are never handed a path, and `cat`, `head`, `tail`, and `ls` of files inside your workspace. **Always allow** on a command like `grep` grants it by name: any arguments, as long as the line runs in your workspace and every path it names resolves inside it. A command with subcommands (`git push`, `gh pr`) keeps a command-plus-subcommand scope, and shells, interpreters, wrappers like `xargs` or `sudo`, and system commands are never granted by name. A line joined by `&&`, `||`, `;`, or `|` runs unattended when every command in it would on its own, and output may be redirected into your workspace or `/dev/null`; a background `&`, a newline, `$( )`, a heredoc, or a redirection elsewhere still means asking. For `browser.act` and `web.fetch` it is an approved site (a `web.fetch` redirect to another site is reported as `redirectedTo`, not followed), and for other gated tools a standing grant.
@@ -155,9 +158,9 @@ When a stored key "isn't found", check in this order:
155
158
  3. The app is missing the history scopes (`channels:history`, `groups:history`, `mpim:history`) and their `message.*` events, which means adding them and reinstalling the app.
156
159
 
157
160
  Not answering at all also happens when the daemon is down, the app was removed from the channel, or the person is not in `principals.slackUsers` and `admit` is `principals`, the default.
158
- - **Another agent on Slack** needs its own soul on the roster first, then its own Slack app through `stratus setup` → Channels, and a restart to connect it.
161
+ - **Another agent on Slack** needs its own soul on the roster first, then its own Slack app through `stratus setup` → Channels or `stratus channel set slack --agent <id>`. Saving the tokens connects it on a daemon that already serves Slack, with no restart; a daemon that started with no Slack app at all needs a restart.
159
162
  - **Who counts as your operator** is `principals.slackUsers` in the trusted config. By default (`admit: "principals"`) everyone else is turned away before a turn starts, so an agent that says nothing to someone may simply not admit them. Under `admit: "anyone"` their messages reach you labelled as from someone unknown.
160
- - **Images** need the `files:read` scope. Text files are read up to a size limit, and anything else reaches you by name only, with the reason. The message says which.
163
+ - **Images** need the `files:read` scope. Text files are read up to a size limit, and anything else reaches you by name only, with the reason. The message says which. Files are only downloaded from Slack's own file host, so a remote file (another app's link to a file hosted outside Slack) reaches you by name only, as not downloaded.
161
164
 
162
165
  ## The daemon
163
166
 
@@ -166,7 +169,7 @@ When a stored key "isn't found", check in this order:
166
169
  - On Linux it is a systemd user unit, which needs `loginctl enable-linger` to run while nobody is logged in.
167
170
  - It will not start while the trusted config (`~/.stratus/config.json`, or the file `--config` names) fails to parse or validate. The error goes to stderr, not `stratus logs`: on Linux the unit then shows as failed until the file is fixed and `stratus service start` is run; on macOS launchd keeps retrying until the file is fixed. A daemon already running keeps its last good config, and `stratus service start`, `stratus update`, and `stratus restart` refuse up front with the same error rather than restart into it.
168
171
  - **`stratus restart`** drains in-flight turns and restarts, and needs the control API. Slack stays connected during the drain: a message addressed to you then gets an "I'm restarting" reply and is answered by the next daemon (kept in `slack-pending-intake.json`, no text). Overheard messages during a drain are not kept. Without the API, use `stratus service stop` then `stratus service start`.
169
- - A restart is needed after a change to `plugins` (a plugin's `env` included), `approvals`, `api`, `principals`, `slack`, `maxTurns`, `executor`, or `memoryStore`, and after new channel secrets (Slack's tokens or a channel plugin's).
172
+ - A restart is needed after a change to `plugins` (a plugin's `env` included), `approvals`, `api`, `principals`, `slack`, `maxTurns`, `executor`, or `memoryStore`, and after a channel plugin's new secrets. New Slack tokens connect without one, once the daemon serves Slack.
170
173
  - It is not needed for soul edits, stored credentials and keys, the config's `provider`/`model`, or skills (`stratus skill reload`).
171
174
  - `stratus update` stops and starts the service itself. Only a daemon someone started with `stratus serve` needs restarting by hand.
172
175
  - In a container or under a system unit, `stratus serve --log-format json` also writes the log's records to stdout, for `docker logs`, journald, or a log shipper. `stratus health` is the probe.
@@ -177,8 +180,9 @@ When a stored key "isn't found", check in this order:
177
180
 
178
181
  ## When a reply stops or changes model
179
182
 
180
- - **"Out of steps."** One message may take `maxTurns` rounds of tool calls (default 40, set in the trusted config, restart to change). Past that you are told you are out of steps, get one last call with no tools, and answer with what you did and what is left. Replying "continue" carries on with a fresh allowance. An agent that does long work should have its own budget raised with `agentMaxTurns: { "<id>": 300 }` in the trusted config (restart to change), which leaves the rest of the fleet on `maxTurns`. With `autoContinue: { "<id>": true }` (or a number, to cap it) an agent is given another budget instead of stopping to ask, until its work is done, its cap is reached, or someone sends it another message meanwhile. Under a kernel-driven provider (Anthropic, OpenAI-compatible), it also stops when stuck on a repeating failure; the `claude-code` runtime runs its own loop and does not have that check, so an uncapped claude-code agent relies on the cap or a new message to stop. It applies to the built-in model loop and the `claude-code` runtime, not yet to `codex`.
183
+ - **"Out of steps."** One message may take `maxTurns` rounds of tool calls (default 40, set in the trusted config, restart to change). Past that you are told you are out of steps, get one last call with no tools, and answer with what you did and what is left. Replying "continue" carries on with a fresh allowance. An agent that does long work should have its own budget raised with `agentMaxTurns: { "<id>": 300 }` in the trusted config (restart to change), which leaves the rest of the fleet on `maxTurns`. With `autoContinue: { "<id>": true }` (or a number, to cap it) an agent is given another budget instead of stopping to ask, until its work is done, its cap is reached, or someone sends it another message meanwhile. Under a kernel-driven provider (Anthropic, OpenAI-compatible), it also stops when stuck on a repeating failure; the `claude-code` runtime runs its own loop and does not have that check, so an uncapped claude-code agent relies on the cap or a new message to stop. It applies to the built-in model loop and the `claude-code` runtime, not yet to `codex`. A message that stopped this way is marked: `stratus sessions` shows it as `completed (stopped early: turn limit)` (or `repeated failure`), `--status stopped-early` lists only those, and the log has a `session.stopped-early` line. The mark clears when the next message in that conversation starts. On a recurring schedule, the next firing starts from that wrap-up: its message is the schedule's prompt followed by when and why the last run stopped and what it said was done and left, so carry on from there instead of starting over. From the second stop in a row it says so; post one line to the schedule's destination only if being stuck blocks someone, since the daemon posts nothing itself.
181
184
  - **Stopped with no reply.** On a provider that streams (Anthropic, Codex, and plugin providers that say they stream), the daemon's watchdog aborts a turn when the model reports nothing for `--idle-timeout` seconds (default 120). On one that does not, such as an OpenAI-compatible endpoint, there is no watchdog, and a silent request ends only when the provider's own request fails or times out, so `--idle-timeout` neither explains nor fixes it. The session ends failed, with `no activity for 120000ms`, and `stratus logs --agent <id>` names the last thing it heard. A tool that is running, or a call waiting on an approval, never trips it.
185
+ - **Cut short by a restart.** A daemon that stops or restarts gives running turns 30 seconds, then aborts the rest. A turn started by a Slack message, an event, or an API request that sent an idempotency key is left for the next start, which continues it once, and the person is told you will answer after the restart. Any other turn is marked failed ("stratusd is stopping" or "restarting"), and the message has to be sent again. On Codex or a Claude subscription sign-in, which run their own tool loop, every such turn is marked failed, since continuing could repeat what already ran.
182
186
  - **Cut off at the output cap.** A reply longer than the model may write in one turn fails rather than being posted half-finished, and the error says so. With an Anthropic API key that cap is `maxTokens` in the config (default 16000); a Claude subscription run does not take that setting, so raising it changes nothing there. Offer a shorter answer, or, on an API key, ask for the cap to be raised. Never ask for a longer timeout.
183
187
  - **Answering on another model.** When the configured model fails a request and a `fallbackModel` is set, that conversation switches to the fallback and stays on it, even across restarts, so it never silently swaps back. Not every failure switches: a conversation too long for the window is trimmed and retried on the same model, a cancelled turn just stops, and a Codex or Claude subscription turn that fails after its tools already ran surfaces the error instead, since a retry elsewhere would run them twice. Those leave the conversation on the configured model, with nothing to roll over. Your instructions say when you are on the fallback. `stratus logs` shows the error that caused the switch, and `stratus session rollover <session id>` starts the conversation over on the configured model.
184
188
  - When asked why a reply stopped, look before you answer if you can: with `shell.run` you can run `stratus logs --agent <your id>` (it is gated, so it needs approval or a granted scope), and an `fs` root covering `~/.stratus/logs/` lets you read the file. Without either, give the likely cause from what your transcript shows (an out-of-steps answer, a message with no reply, a request for a very long answer) and the log command that confirms it, rather than a certain answer.
@@ -194,7 +198,8 @@ When a stored key "isn't found", check in this order:
194
198
  | `stratus restart` | Announced drain-and-restart of the running daemon |
195
199
  | `stratus logs` | Read the daemon's log |
196
200
  | `stratus health` | Whether the running daemon is serving: exit 0 if so, for a container healthcheck or a probe |
197
- | `stratus sessions` (also `stratus session list`) | The running daemon's sessions, newest first: status, agent, last update, id. `--agent <id>`, `--status <status>` (`running`, `pending_approval`, `failed`, `completed`, `idle`), `--limit <n>` (default 20), `--format json` |
201
+ | `stratus events` | What the events port has been doing, per source: the last delivery, the last refusal and why (a webhook with the wrong secret shows here, though it leaves no row), and the last 24 hours of deliveries and firings by status. `--format json` |
202
+ | `stratus sessions` (also `stratus session list`) | The running daemon's sessions, newest first: status, agent, last update, id. `--agent <id>`, `--status <status>` (`running`, `pending_approval`, `failed`, `completed`, `idle`, or `stopped-early` for runs that ran out of steps and wrapped up), `--limit <n>` (default 20), `--format json` |
198
203
  | `stratus doctor` | What a run would use, and why |
199
204
  | `stratus update` | Update Stratus and its companion packages (`--check` to look first) |
200
205
  | `stratus dashboard` | Open the web dashboard with a one-time sign-in link |
@@ -204,6 +209,7 @@ When a stored key "isn't found", check in this order:
204
209
  | `stratus skills` | List skills and which agents enable each |
205
210
  | `stratus plugins` | Every tool, its plugin, who is granted it, and what approvals do |
206
211
  | `stratus credential set` / `stratus credential remove` / `stratus credentials` | Store (value on stdin) or remove a named credential; list the names |
212
+ | `stratus credential env <name> <VAR> --agent <id>` | Let that agent's shell commands read a stored key as `<VAR>`, from the next command; `--remove` undoes it |
207
213
  | `stratus signin set anthropic --agent <id>` / `stratus signin remove` / `stratus signin list` (also `stratus signins`) | Give one agent its own Claude subscription (token at a hidden prompt or on stdin), take it back, list who has one |
208
214
  | `stratus channel set` / `stratus channel remove` / `stratus channel list` | Store (values prompted without echo, or one per stdin line) or remove a channel's secrets for one agent; list which agents have them. A channel's secrets are never yours to read, and a running daemon picks them up at its next start |
209
215
  | `stratus grants <id>` / `stratus grants revoke <id>` | An agent's standing approvals, and taking one back |
@@ -227,6 +233,7 @@ Your operator can do more from anywhere they reach the dashboard or the control
227
233
  Only at the machine can someone:
228
234
 
229
235
  - enable or configure a plugin (its `env` and `headers` included), or choose an executor or memory store;
236
+ - add an event source for `schedule.on`: an `events` block in `~/.stratus/config.json` (`"sources": { "github": { "verify": "github", "secrets": ["events.github"] } }`), the secret stored with `stratus credential set events.github`, and a restart (docs/guides/events.md); or a plugin that feeds a source (a poller where no webhook can reach the daemon): its manifest declares the source id, enabling it in `plugins` is the whole setup, and `stratus events` names the plugin;
230
237
  - install or update packages;
231
238
  - remove a named credential, or replace one without an approver's form;
232
239
  - raise a memory's trust (`stratus memory reassert`).