@sigrun/sigrun 0.0.0-stage → 0.1.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/CHANGELOG.md +204 -0
- package/CONTEXT.md +163 -0
- package/LICENSE +21 -0
- package/README.md +691 -2
- package/THIRD_PARTY_NOTICES.md +28 -0
- package/dist/backup/marker.js +80 -0
- package/dist/backup/run-backup.js +339 -0
- package/dist/cli.js +952 -0
- package/dist/commands/expand.js +245 -0
- package/dist/commands/templates.js +272 -0
- package/dist/config.js +1146 -0
- package/dist/extension.d.ts +431 -0
- package/dist/harness/activity.js +42 -0
- package/dist/harness/ambient.js +261 -0
- package/dist/harness/auth.js +130 -0
- package/dist/harness/boot.js +865 -0
- package/dist/harness/builtins.js +191 -0
- package/dist/harness/event-bus.js +1203 -0
- package/dist/harness/health.js +30 -0
- package/dist/harness/identity/index.js +37 -0
- package/dist/harness/identity/invites.js +94 -0
- package/dist/harness/identity/json-store.js +108 -0
- package/dist/harness/identity/setup-code.js +81 -0
- package/dist/harness/identity/tokens.js +101 -0
- package/dist/harness/identity/users.js +93 -0
- package/dist/harness/interactions.js +283 -0
- package/dist/harness/loader.js +1236 -0
- package/dist/harness/memory-default.js +35 -0
- package/dist/harness/naming.js +193 -0
- package/dist/harness/packages.js +1002 -0
- package/dist/harness/providers/auth-bridge.js +60 -0
- package/dist/harness/providers/index.js +277 -0
- package/dist/harness/providers/models-store.js +105 -0
- package/dist/harness/providers/refresh.js +161 -0
- package/dist/harness/providers/scheduler.js +89 -0
- package/dist/harness/registry.js +9 -0
- package/dist/harness/reserved-sections.js +24 -0
- package/dist/harness/seam-host.js +239 -0
- package/dist/harness/sections/context-files.js +78 -0
- package/dist/harness/sections/model-guidance.js +77 -0
- package/dist/harness/settings.js +1053 -0
- package/dist/harness/stores.js +450 -0
- package/dist/harness/tools.js +500 -0
- package/dist/harness/workspace.js +73 -0
- package/dist/index.js +39 -0
- package/dist/jobs/docs.js +257 -0
- package/dist/jobs/fire.js +342 -0
- package/dist/jobs/index.js +228 -0
- package/dist/jobs/observe.js +165 -0
- package/dist/jobs/reconcile.js +245 -0
- package/dist/jobs/schedule.js +77 -0
- package/dist/jobs/spec.js +248 -0
- package/dist/jobs/task.js +190 -0
- package/dist/jobs/transfer.js +53 -0
- package/dist/serve.js +164 -0
- package/dist/server/auth/claim-lock.js +28 -0
- package/dist/server/auth/ip.js +51 -0
- package/dist/server/auth/login-limiter.js +52 -0
- package/dist/server/auth/loopback.js +47 -0
- package/dist/server/auth/pipeline.js +330 -0
- package/dist/server/auth/principal.js +26 -0
- package/dist/server/commands.js +198 -0
- package/dist/server/content-parts.js +142 -0
- package/dist/server/context.js +13 -0
- package/dist/server/conversation-handler.js +61 -0
- package/dist/server/conversation-ops.js +310 -0
- package/dist/server/events/hub.js +39 -0
- package/dist/server/events/types.js +8 -0
- package/dist/server/helpers.js +351 -0
- package/dist/server/host-bridge.js +131 -0
- package/dist/server/index.js +232 -0
- package/dist/server/notifications.js +185 -0
- package/dist/server/ownership.js +67 -0
- package/dist/server/readiness.js +36 -0
- package/dist/server/routes/auth.js +172 -0
- package/dist/server/routes/conversations.js +341 -0
- package/dist/server/routes/events.js +161 -0
- package/dist/server/routes/extension-routes.js +127 -0
- package/dist/server/routes/extensions.js +168 -0
- package/dist/server/routes/files.js +308 -0
- package/dist/server/routes/interactions.js +260 -0
- package/dist/server/routes/jobs.js +271 -0
- package/dist/server/routes/meta.js +146 -0
- package/dist/server/routes/metrics.js +219 -0
- package/dist/server/routes/onboarding.js +268 -0
- package/dist/server/routes/probes.js +53 -0
- package/dist/server/routes/providers.js +198 -0
- package/dist/server/routes/settings.js +48 -0
- package/dist/server/routes/stream.js +131 -0
- package/dist/server/routes/submissions.js +317 -0
- package/dist/server/routes/users.js +169 -0
- package/dist/server/schema-fixture.js +950 -0
- package/dist/server/session-meta.js +247 -0
- package/dist/server/static.js +169 -0
- package/dist/server/views.js +71 -0
- package/dist/skills/index.js +418 -0
- package/node_modules/@sigrun/shared/CHANGELOG.md +16 -0
- package/node_modules/@sigrun/shared/LICENSE +21 -0
- package/node_modules/@sigrun/shared/README.md +10 -0
- package/node_modules/@sigrun/shared/dist/access.d.ts +55 -0
- package/node_modules/@sigrun/shared/dist/access.js +72 -0
- package/node_modules/@sigrun/shared/dist/ambient.d.ts +111 -0
- package/node_modules/@sigrun/shared/dist/ambient.js +23 -0
- package/node_modules/@sigrun/shared/dist/commands.d.ts +75 -0
- package/node_modules/@sigrun/shared/dist/commands.js +25 -0
- package/node_modules/@sigrun/shared/dist/conversations.d.ts +31 -0
- package/node_modules/@sigrun/shared/dist/conversations.js +94 -0
- package/node_modules/@sigrun/shared/dist/interactions.d.ts +91 -0
- package/node_modules/@sigrun/shared/dist/interactions.js +21 -0
- package/node_modules/@sigrun/shared/dist/jobs.d.ts +100 -0
- package/node_modules/@sigrun/shared/dist/jobs.js +19 -0
- package/node_modules/@sigrun/shared/dist/json.d.ts +20 -0
- package/node_modules/@sigrun/shared/dist/json.js +13 -0
- package/node_modules/@sigrun/shared/dist/session-meta.d.ts +103 -0
- package/node_modules/@sigrun/shared/dist/session-meta.js +13 -0
- package/node_modules/@sigrun/shared/dist/settings.d.ts +401 -0
- package/node_modules/@sigrun/shared/dist/settings.js +19 -0
- package/node_modules/@sigrun/shared/dist/themes.d.ts +50 -0
- package/node_modules/@sigrun/shared/dist/themes.js +156 -0
- package/node_modules/@sigrun/shared/dist/turns.d.ts +102 -0
- package/node_modules/@sigrun/shared/dist/turns.js +166 -0
- package/node_modules/@sigrun/shared/dist/view-delta.d.ts +54 -0
- package/node_modules/@sigrun/shared/dist/view-delta.js +135 -0
- package/node_modules/@sigrun/shared/dist/views.d.ts +742 -0
- package/node_modules/@sigrun/shared/dist/views.js +38 -0
- package/node_modules/@sigrun/shared/package.json +83 -0
- package/package.json +90 -5
- package/webui/dist/assets/index-Rl0KhCLa.css +1 -0
- package/webui/dist/assets/index-rOiARZLF.js +50 -0
- package/webui/dist/index.html +37 -0
package/README.md
CHANGED
|
@@ -1,3 +1,692 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Sigrun
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Sigrun is a durable personal-assistant harness built on
|
|
4
|
+
[pi-durable](https://www.npmjs.com/package/@earendil-works/pi-durable):
|
|
5
|
+
long-lived conversations over a SQLite store (surviving restarts, with
|
|
6
|
+
resumable generation), an HTTP API plus web and terminal UIs, and an
|
|
7
|
+
extension system that installs extra tools, providers, prompt sections,
|
|
8
|
+
and capabilities at boot.
|
|
9
|
+
|
|
10
|
+
- **Durable** — conversations, submissions, and schedules live in one
|
|
11
|
+
SQLite database (`SIGRUN_STORAGE_PATH`); a restart resumes where it
|
|
12
|
+
stopped.
|
|
13
|
+
- **pi-durable core** — the harness primitives (conversations, inbox
|
|
14
|
+
steering/follow-up modes, retries, streaming, compaction) come from
|
|
15
|
+
pi-durable; sigrun is the deployable shell around them.
|
|
16
|
+
- **Extensions** — separate npm packages loaded from `SIGRUN_EXTENSIONS` (or the settings file's
|
|
17
|
+
top-level `extensions` list); a failing extension is quarantined and
|
|
18
|
+
reported, never a boot abort.
|
|
19
|
+
- **Web UI** — a static bundle served from `SIGRUN_WEBUI_BUNDLE` (or the
|
|
20
|
+
settings file's `server.webuiBundle`; default `webui/dist`); API-only
|
|
21
|
+
deploys simply omit it. The web UI's visual design is adapted from
|
|
22
|
+
[hermes-webui](https://github.com/nesquena/hermes-webui) by nesquena and
|
|
23
|
+
contributors — see THIRD_PARTY_NOTICES.md.
|
|
24
|
+
- **Terminal UI** — a pi-idiom terminal client over the same HTTP + SSE
|
|
25
|
+
API the web UI consumes (feature parity by design): conversation
|
|
26
|
+
sidebar, streaming conversation pane, fork, the interaction and
|
|
27
|
+
ambient seams, slash commands. See [Launch modes](#launch-modes).
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
The published package is `@sigrun/sigrun` (the CLI command stays
|
|
32
|
+
`sigrun`):
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm i -g @sigrun/sigrun
|
|
36
|
+
sigrun run # server + terminal client on loopback
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Node `>=22.19.0`. The web UI ships inside the package; the terminal
|
|
40
|
+
client is the optional companion package (`npm i -g @sigrun/tui` —
|
|
41
|
+
`sigrun run` offers an actionable install message when absent).
|
|
42
|
+
|
|
43
|
+
- **Docker** — `docker/` holds a compose reference:
|
|
44
|
+
`cd docker && cp .env.example .env && docker compose up --build`.
|
|
45
|
+
- **Helm** — `charts/sigrun` is the reference Kubernetes deployment
|
|
46
|
+
(a durable PVC, the config dir, and the image).
|
|
47
|
+
|
|
48
|
+
## Quickstart
|
|
49
|
+
|
|
50
|
+
**A model and a credential come first.** `sigrun serve` refuses to boot
|
|
51
|
+
without a default model — `SIGRUN_DEFAULT_MODEL` (or `defaultModel` in
|
|
52
|
+
`settings.json`) — and every model needs a provider credential before it
|
|
53
|
+
can answer. So the real first run is: pick a provider, set the model,
|
|
54
|
+
then start the server.
|
|
55
|
+
|
|
56
|
+
The smallest configuration that boots, using any [OI]-compatible
|
|
57
|
+
endpoint:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
export SIGRUN_DEFAULT_MODEL=openai/gpt-4o-mini # provider/model-id
|
|
61
|
+
export OPENAI_API_KEY=sk-...
|
|
62
|
+
export OPENAI_BASE_URL=https://api.openai.com/v1 # any compatible endpoint
|
|
63
|
+
|
|
64
|
+
sigrun serve # listens on :3000
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
(`SIGRUN_MODEL` still works as a deprecated alias for
|
|
68
|
+
`SIGRUN_DEFAULT_MODEL` — one release cycle, with a boot warning.)
|
|
69
|
+
|
|
70
|
+
Or install a provider plugin and let it supply the catalog — then the
|
|
71
|
+
only configuration is which of its models to default to:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
# settings.json: { "extensions": ["@sigrun/provider-ollama@^0.1.0"] }
|
|
75
|
+
export SIGRUN_DEFAULT_MODEL=ollama/glm-5.3-flash
|
|
76
|
+
sigrun serve
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**Then claim the install.** The first request opens an onboarding gate:
|
|
80
|
+
the server prints a setup code to its log, and the web UI (or
|
|
81
|
+
`sigrun token create`) uses it to claim the first admin account — every
|
|
82
|
+
later user is an invite. Nothing else is required; the first message in
|
|
83
|
+
a new conversation creates it.
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
grep 'setup code' <the server log> # sigrun setup code: N42E-HB2G
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Open `http://localhost:3000/` (web UI) or drive the API:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
curl localhost:3000/v1/meta
|
|
93
|
+
curl -X POST localhost:3000/v1/conversations -H 'content-type: application/json' -d '{}'
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
From a source checkout the dev shape is `npm install && npm start`
|
|
97
|
+
(`tsx src/index.ts`, same server).
|
|
98
|
+
|
|
99
|
+
## Launch modes
|
|
100
|
+
|
|
101
|
+
The `sigrun` binary (`package.json` `bin`, run via tsx) has three
|
|
102
|
+
entrypoints:
|
|
103
|
+
|
|
104
|
+
| Command | Behavior |
|
|
105
|
+
|---|---|
|
|
106
|
+
| `sigrun [--user <name>]` | Workstation mode: boot the harness + server **in-process** and launch the terminal UI against the loopback port — the server binds **loopback (`127.0.0.1`) only**, so the naive-identity API never answers the LAN. The agent dies with the TUI — when the TUI exits, server and harness shut down with it. |
|
|
107
|
+
| `sigrun serve` | Headless: boot the API + web UI statics, no TUI. This is exactly the `npm start` / `src/index.ts` composition the Docker image and Helm chart run. Binds all interfaces (`0.0.0.0`) — the container default; workstation `sigrun` (run) binds loopback only. |
|
|
108
|
+
| `sigrun attach [--server <url>] [--user <name>]` | TUI only, against a harness it can reach — the remote-client mode (e.g. attaching to a cluster or tailnet deployment). |
|
|
109
|
+
|
|
110
|
+
Resolution chains for `attach` (and for the workstation-mode user):
|
|
111
|
+
|
|
112
|
+
- **server**: `--server` → `tui.json`'s `server` →
|
|
113
|
+
`http://localhost:$SIGRUN_PORT` (default `3000`)
|
|
114
|
+
- **user**: `--user` → `SIGRUN_USER` → `tui.json`'s `user`
|
|
115
|
+
|
|
116
|
+
`tui.json` lives in the config directory (`SIGRUN_CONFIG_DIR`, default
|
|
117
|
+
`~/.config/sigrun`) next to `settings.json`, and holds the TUI client's
|
|
118
|
+
own small config: `{ "user": "...", "server": "..." }` (both optional;
|
|
119
|
+
a malformed file is a boot error). The configured `user` rides every
|
|
120
|
+
request as the `X-Sigrun-User` header and is stamped as the
|
|
121
|
+
conversation's `owner` on create/fork/import (the identity seam — naive
|
|
122
|
+
mode, no auth flow yet; omitting the user omits the header and behavior
|
|
123
|
+
is unchanged).
|
|
124
|
+
|
|
125
|
+
## Configuration
|
|
126
|
+
|
|
127
|
+
Sigrun is configured by environment variables and a settings file at
|
|
128
|
+
`~/.config/sigrun/settings.json` (`SIGRUN_CONFIG_DIR` relocates the
|
|
129
|
+
directory — `SIGRUN_CONFIG_DIR` itself stays env-only, since it locates
|
|
130
|
+
the file). Precedence, per setting:
|
|
131
|
+
|
|
132
|
+
1. **Environment variable** (always wins — and an *invalid* env value is
|
|
133
|
+
a boot error even if the file has a valid one).
|
|
134
|
+
2. **`settings.json`** field (applies only where the env var is absent).
|
|
135
|
+
3. **Code default** (listed in the table below).
|
|
136
|
+
|
|
137
|
+
When an env var and the settings file both set a knob, the env var wins
|
|
138
|
+
— this is documented behavior, not enforced: sigrun never warns about or
|
|
139
|
+
detects env/file conflicts.
|
|
140
|
+
|
|
141
|
+
On first boot (settings file absent), sigrun writes only what the
|
|
142
|
+
environment explicitly set — the env-sourced `server` fields plus
|
|
143
|
+
`extensions` and `defaultModel` when env supplied them — into
|
|
144
|
+
`settings.json`; defaults are never persisted, so later releases'
|
|
145
|
+
default changes reach existing installs (the file records intent, not
|
|
146
|
+
reality). The full resolved server section is visible instead in
|
|
147
|
+
`GET /v1/settings`'s `effective` block: every flattened server key
|
|
148
|
+
(`server.port`, `server.compaction.reserveTokens`, …) with its resolved
|
|
149
|
+
value and its source — `"env"`, `"file"`, or `"default"`. On later
|
|
150
|
+
boots the file is never rewritten by bootstrap: edit it, and env vars
|
|
151
|
+
still override it per field.
|
|
152
|
+
|
|
153
|
+
| Variable | Type | Default | File key | Purpose |
|
|
154
|
+
|---|---|---|---|---|
|
|
155
|
+
| `SIGRUN_DEFAULT_MODEL` | `provider/model-id` | — (**required**: env or file) | `defaultModel` | Boot default model; bare `model-id` implies the OpenAI-compat provider. `SIGRUN_MODEL` is a deprecated alias (boot warning; one release cycle). |
|
|
156
|
+
| `OPENAI_API_KEY` | string | — | `auth.json` (see below) | Credential for the env-driven OpenAI-compatible provider; the `auth.json` fallback after file entries. |
|
|
157
|
+
| `OPENAI_BASE_URL` | URL | OpenAI | `defaultProvider.baseUrl` | Endpoint for the env-driven provider (any compatible host). |
|
|
158
|
+
| `SIGRUN_PORT` | positive int | `3000` | `server.port` | HTTP listen port. |
|
|
159
|
+
| `SIGRUN_STORAGE_PATH` | path | `<dataDir>/sigrun.db` | `server.storagePath` | SQLite database location. Relative values resolve against the data root (absolute values verbatim; the cwd-relative `data/sigrun.db` default is retired). |
|
|
160
|
+
| `SIGRUN_WORKSPACE_PATH` | path | `<dataDir>/workspace` | `server.workspacePath` | Root of the static local workspace every conversation's tools run against. The default rides the data root (chart/compose deploys set the path explicitly); it is never persisted on first boot. |
|
|
161
|
+
| `SIGRUN_DATA_DIR` | path | `$XDG_DATA_HOME/sigrun` or `~/.local/share/sigrun` | `server.dataDir` | Data root (the DB default and extension data dirs). Env/file values win verbatim. |
|
|
162
|
+
| `SIGRUN_CACHE_DIR` | path | `$XDG_CACHE_HOME/sigrun` or `~/.cache/sigrun` | `server.cacheDir` | Cache root (regenerable; never ensured at boot). Env/file values win verbatim. |
|
|
163
|
+
| `SIGRUN_STATE_DIR` | path | `$XDG_STATE_HOME/sigrun` or `~/.local/state/sigrun` | `server.stateDir` | State root (operational bookkeeping). Env/file values win verbatim. |
|
|
164
|
+
| `SIGRUN_EXTENSIONS` | csv | *(none)* | `extensions` (top-level list) | Deploy-time extension list (string entries; env wins over the file). |
|
|
165
|
+
| — | — | *(none)* | `gitDir` is always `<configDir>/git` | Git extension checkouts clone here (pi layout). Not env-configurable. |
|
|
166
|
+
| `SIGRUN_WEBUI_BUNDLE` | path | `webui/dist` | `server.webuiBundle` | Web UI bundle directory; explicit-but-missing = boot error, default-missing = API-only. |
|
|
167
|
+
| `SIGRUN_CONFIG_DIR` | path | `~/.config/sigrun` | *env-only* | The settings file's own directory. |
|
|
168
|
+
| `SIGRUN_COMPACTION_ENABLED` | `true`/`false` | `true` | `server.compaction.enabled` | Context compaction on/off. |
|
|
169
|
+
| `SIGRUN_COMPACTION_RESERVE_TOKENS` | positive int | `16384` | `server.compaction.reserveTokens` | Headroom reserved for the model's reply. |
|
|
170
|
+
| `SIGRUN_COMPACTION_KEEP_RECENT_TOKENS` | positive int | `20000` | `server.compaction.keepRecentTokens` | Recent context retained verbatim through a compaction. |
|
|
171
|
+
| `SIGRUN_COMPACTION_BACKGROUND_TOKENS` | non-negative int | `32768` | `server.compaction.backgroundTokens` | Background compaction threshold; `0` disables background compaction. |
|
|
172
|
+
| `SIGRUN_BACKUP_PATH` | path | *(unset)* | `server.backup.path` | Path backup target (mutually exclusive with the S3 target). |
|
|
173
|
+
| `SIGRUN_BACKUP_S3_BUCKET` | string | *(unset)* | `server.backup.s3.bucket` | S3 backup bucket. |
|
|
174
|
+
| `SIGRUN_BACKUP_S3_PREFIX` | string | *(unset)* | `server.backup.s3.prefix` | Key prefix within the bucket. |
|
|
175
|
+
| `SIGRUN_BACKUP_S3_ENDPOINT` | URL | AWS default | `server.backup.s3.endpoint` | Custom S3 endpoint (S3-compatible stores). |
|
|
176
|
+
|
|
177
|
+
### settings.json schema
|
|
178
|
+
|
|
179
|
+
```jsonc
|
|
180
|
+
{
|
|
181
|
+
"defaultModel": "openai/gpt-4o-mini", // non-empty provider/model-id; bare id = openai provider
|
|
182
|
+
"extensions": [ // the desired install+load state — see Extensions
|
|
183
|
+
"@acme/sigrun-ext-example@^0.1.0", // npm specifier
|
|
184
|
+
"git:github.com/obra/superpowers@main", // git skills-only package: its SKILL.md
|
|
185
|
+
// dirs load; its .ts pi extension is skipped
|
|
186
|
+
{ "source": "./dev-checkout", "enabled": false } // object form
|
|
187
|
+
],
|
|
188
|
+
"defaultProvider": {
|
|
189
|
+
"baseUrl": "https://api.openai.com/v1" // non-empty URL
|
|
190
|
+
},
|
|
191
|
+
"server": {
|
|
192
|
+
"port": 3000, // positive integer
|
|
193
|
+
"workspacePath": "/home/sigrun", // non-empty path
|
|
194
|
+
"storagePath": "data/sigrun.db", // non-empty path; relative resolves against the data root
|
|
195
|
+
"dataDir": "/var/lib/sigrun", // non-empty path (SIGRUN_DATA_DIR; verbatim)
|
|
196
|
+
"cacheDir": "/var/cache/sigrun", // non-empty path (SIGRUN_CACHE_DIR; verbatim)
|
|
197
|
+
"stateDir": "/var/lib/sigrun/state", // non-empty path (SIGRUN_STATE_DIR; verbatim)
|
|
198
|
+
"webuiBundle": "webui/dist", // non-empty path
|
|
199
|
+
"compaction": {
|
|
200
|
+
"enabled": true,
|
|
201
|
+
"reserveTokens": 16384, // positive integer
|
|
202
|
+
"keepRecentTokens": 20000, // positive integer
|
|
203
|
+
"backgroundTokens": 32768 // non-negative integer (0 = off)
|
|
204
|
+
},
|
|
205
|
+
"backup": { // target: { "path": "/backups" } XOR { "s3": { "bucket": "...", "prefix": "...", "endpoint": "..." } }
|
|
206
|
+
"staleAfterHours": 36 // positive number: health.backup degrades past this marker age
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
The file also carries the live harness settings (`steeringMode`,
|
|
213
|
+
`followUpMode`, `retry`, `stream`, `defaultTools`, `perModelThinking`)
|
|
214
|
+
and is read fresh at every resolution.
|
|
215
|
+
|
|
216
|
+
### auth.json
|
|
217
|
+
|
|
218
|
+
API keys live in `<config-dir>/auth.json` — the same directory as
|
|
219
|
+
`settings.json` — keyed by provider name, never in `settings.json`:
|
|
220
|
+
|
|
221
|
+
```jsonc
|
|
222
|
+
{
|
|
223
|
+
"default": { "apiKey": "sk-..." }, // the resolved default provider's key
|
|
224
|
+
"openai": { "apiKey": "sk-..." } // a provider's own entry
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
The env-driven OpenAI-compatible provider resolves its key in this
|
|
229
|
+
order: `auth.json`'s `"default"` entry → the resolved provider's own
|
|
230
|
+
`auth.json` entry → the `OPENAI_API_KEY` environment variable (compat
|
|
231
|
+
fallback, still honored). `"default"` is used only by the provider of
|
|
232
|
+
the resolved default model; every other provider reads its own entry.
|
|
233
|
+
With no key anywhere the provider registers unconfigured — requests
|
|
234
|
+
fail loud, boot does not. The file is created
|
|
235
|
+
with mode `0600` (owner-only) whenever sigrun writes it. Key material is
|
|
236
|
+
never logged, echoed, or served by the API (`GET /v1/providers` reports
|
|
237
|
+
key *presence* only).
|
|
238
|
+
|
|
239
|
+
**Restart requirement:** `server.*`, `extensions`,
|
|
240
|
+
`defaultModel`, and `defaultProvider` changes — whether by hand edit or
|
|
241
|
+
`PATCH /v1/settings` — apply on the **next boot** only; the live
|
|
242
|
+
settings (`steeringMode`, `retry`, `stream`, `defaultTools`,
|
|
243
|
+
`perModelThinking`, `skills.sectionMaxBytes`) take effect immediately on
|
|
244
|
+
the next submission.
|
|
245
|
+
|
|
246
|
+
## Authentication
|
|
247
|
+
|
|
248
|
+
One pipeline authenticates every request into a *principal* (user,
|
|
249
|
+
admin flag, method) before any route runs. `/healthz` and `/readyz` are
|
|
250
|
+
always public; the browser surface, `/v1/onboarding*`, and login/logout
|
|
251
|
+
are public too; everything else needs a resolved principal. An
|
|
252
|
+
unclaimed server (empty user directory) answers 403 `setup_required` on
|
|
253
|
+
every gated route — onboarding is the only usable surface until the
|
|
254
|
+
first claim.
|
|
255
|
+
|
|
256
|
+
**Defaults.** The effective method list derives from the listen host:
|
|
257
|
+
a loopback bind (`127.0.0.1` / `[::1]` — `sigrun` workstation mode and
|
|
258
|
+
loopback `serve`) requires nothing (the naive single-operator path,
|
|
259
|
+
where an optional `X-Sigrun-User` header names the acting user); any
|
|
260
|
+
other bind requires `token`.
|
|
261
|
+
|
|
262
|
+
**The override.** `SIGRUN_AUTH` (csv of `token` and/or `proxy`; `none`
|
|
263
|
+
= the empty list) or the settings file's `server.auth.methods` sets the
|
|
264
|
+
list explicitly — explicit always wins, including `[]` on a non-
|
|
265
|
+
loopback bind, which boots with a warning that any reachable client is
|
|
266
|
+
trusted. The related env knobs: `SIGRUN_AUTH_TRUSTED_PROXIES` (CIDR
|
|
267
|
+
csv), `SIGRUN_PROXY_SECRET` and `SIGRUN_API_TOKEN` (credentials —
|
|
268
|
+
env-only, never in `settings.json`). Every auth setting is
|
|
269
|
+
restart-required.
|
|
270
|
+
|
|
271
|
+
**Tokens.** API tokens are `sgr_`-prefixed secrets shown once at
|
|
272
|
+
creation (the store keeps only SHA-256 digests) and are self-service:
|
|
273
|
+
an authenticated user lists and mints their own via
|
|
274
|
+
`GET`/`POST /v1/auth/tokens` (`DELETE /v1/auth/tokens/{id}` revokes;
|
|
275
|
+
admins may revoke anyone's). A token whose user was deleted is dead.
|
|
276
|
+
A browser exchanges a token for a `sigrun_session` cookie at
|
|
277
|
+
`POST /v1/auth/login` — invalid attempts are delayed and rate-limited —
|
|
278
|
+
and `GET /v1/auth/whoami` returns the resolved principal.
|
|
279
|
+
|
|
280
|
+
**Proxy method (forward auth).** A trusted reverse proxy vouches for
|
|
281
|
+
the caller through headers (Authelia's `Remote-*` defaults,
|
|
282
|
+
configurable) when the request comes from a peer inside
|
|
283
|
+
`server.auth.proxy.trustedProxies` (CIDRs). Setting
|
|
284
|
+
`SIGRUN_PROXY_SECRET` additionally requires that shared secret in the
|
|
285
|
+
`secretHeader` on every request. Scope the CIDR list to exactly the
|
|
286
|
+
proxy and nothing else: every peer in `trustedProxies` can forge any
|
|
287
|
+
identity, including an admin.
|
|
288
|
+
|
|
289
|
+
**Onboarding (the first claim).** An unclaimed server serves
|
|
290
|
+
`GET /v1/onboarding` + `POST /v1/onboarding/claim`. On a loopback bind
|
|
291
|
+
or through a trusted proxy identity the claim needs no code; otherwise
|
|
292
|
+
the one-shot setup code is required — printed once in the log at boot
|
|
293
|
+
and written to `<cacheDir>/setup-code` (in the Helm chart: the pod log
|
|
294
|
+
and `/cache/setup-code`; `SIGRUN_SETUP_CODE` pins it instead). The
|
|
295
|
+
claimant becomes the first admin and owns the reserved root
|
|
296
|
+
conversation. Later users join by invite code (`POST /v1/users/invites`,
|
|
297
|
+
admin) or by trusted-proxy auto-provision; admins can transfer or remove
|
|
298
|
+
users (`PATCH`/`DELETE /v1/users/{user}` — deletion transfers their
|
|
299
|
+
conversations).
|
|
300
|
+
|
|
301
|
+
**Sharing.** Conversations are private to their owner by default. The
|
|
302
|
+
owner can share one — household-wide or with named users, `read` or
|
|
303
|
+
`write` — through the web UI's ⋯ menu or the TUI's `/share`; `write`
|
|
304
|
+
lets a recipient submit, answer, and fork, but only the owner can
|
|
305
|
+
re-share. Household sharing defaults to `read`.
|
|
306
|
+
|
|
307
|
+
## Backup
|
|
308
|
+
|
|
309
|
+
Set a target (`SIGRUN_BACKUP_PATH` or `SIGRUN_BACKUP_S3_BUCKET`+`SIGRUN_BACKUP_S3_PREFIX`,
|
|
310
|
+
via env or `server.backup` in `settings.json`) and schedule the backup
|
|
311
|
+
script — the chart ships a nightly CronJob; bare deployments run
|
|
312
|
+
`npx tsx src/backup/run-backup.ts` (or the built `dist/backup/run-backup.js`)
|
|
313
|
+
with `SIGRUN_STORAGE_PATH` pointing at the database. Each run takes an
|
|
314
|
+
online-safe `sqlite3 .backup`, ships it to the target (`sigrun-<UTC
|
|
315
|
+
timestamp>.db`), and folds in any enabled `SIGRUN_BACKUP_SECTIONS`
|
|
316
|
+
tarballs (config/secrets/data/state — see
|
|
317
|
+
[`docs/extending.md`](docs/extending.md)).
|
|
318
|
+
|
|
319
|
+
**S3 credentials** come from the standard AWS chain — `AWS_ACCESS_KEY_ID`,
|
|
320
|
+
`AWS_SECRET_ACCESS_KEY` and `AWS_REGION` in the container env (or an AWS
|
|
321
|
+
config file); sigrun adds no credential settings of its own. Set
|
|
322
|
+
`SIGRUN_BACKUP_S3_ENDPOINT` for an S3-compatible store (MinIO, RustFS):
|
|
323
|
+
the client then uses path-style addressing, which those stores require.
|
|
324
|
+
|
|
325
|
+
**Retention differs by target kind:**
|
|
326
|
+
|
|
327
|
+
- **Path targets** retain in-process: each run prunes `sigrun-*.db`
|
|
328
|
+
files older than 30 days from the target directory (mtime-based;
|
|
329
|
+
non-matching files are never touched).
|
|
330
|
+
- **S3 targets are pruned by the BUCKET, not the script** — configure a
|
|
331
|
+
lifecycle rule expiring the backup objects after 30 days. The rule's
|
|
332
|
+
filter must match your key layout: object keys are
|
|
333
|
+
`<SIGRUN_BACKUP_S3_PREFIX>sigrun-….db` (plus `<section>-….tar.gz` when
|
|
334
|
+
sections are enabled — cover those too, or drop the filter on a
|
|
335
|
+
dedicated bucket):
|
|
336
|
+
|
|
337
|
+
```json
|
|
338
|
+
{
|
|
339
|
+
"Rules": [
|
|
340
|
+
{
|
|
341
|
+
"ID": "sigrun-backup-retention",
|
|
342
|
+
"Status": "Enabled",
|
|
343
|
+
"Filter": { "Prefix": "sigrun-" },
|
|
344
|
+
"Expiration": { "Days": 30 }
|
|
345
|
+
}
|
|
346
|
+
]
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
Applied with `aws s3api put-bucket-lifecycle-configuration --bucket
|
|
351
|
+
<bucket> --lifecycle-configuration file://rule.json` (adjust `Prefix`
|
|
352
|
+
to your `SIGRUN_BACKUP_S3_PREFIX`, or remove `Filter` on a dedicated
|
|
353
|
+
bucket). S3-compatible stores accept the same rule shape through
|
|
354
|
+
their own tooling, with `--endpoint-url` selecting yours.
|
|
355
|
+
|
|
356
|
+
**Backup honesty (`GET /v1/meta`):** a configured target reports
|
|
357
|
+
`health.backup: "configured"` only while backups actually SUCCEED — the
|
|
358
|
+
script writes `<stateDir>/backup/last-success.json` after each
|
|
359
|
+
successful run, and `/v1/meta` reads it per request: absent, corrupt,
|
|
360
|
+
or older than `server.backup.staleAfterHours` (default 36 h) reports
|
|
361
|
+
`"degraded"`, with the last success's timestamp in `backup.lastSuccessAt`.
|
|
362
|
+
This requires the server and the CronJob to resolve the SAME state root
|
|
363
|
+
— set `SIGRUN_STATE_DIR` on both (the chart does); with it unset on the
|
|
364
|
+
job, the script warns and skips the marker (the backup itself still
|
|
365
|
+
succeeds), and the badge honestly reads `"degraded"`.
|
|
366
|
+
|
|
367
|
+
## Monitoring
|
|
368
|
+
|
|
369
|
+
`GET /metrics` serves a Prometheus text exposition (scrape-compatible
|
|
370
|
+
plain text, one `# HELP`/`# TYPE` pair per family). Access follows the
|
|
371
|
+
auth pipeline: a principal is required unless `SIGRUN_METRICS_PUBLIC`
|
|
372
|
+
(`server.metrics.public`) is set — the Helm chart sets it so the
|
|
373
|
+
ServiceMonitor can scrape. `/healthz` and `/readyz` stay public probes
|
|
374
|
+
(`sigrun_readiness 1`, or 503 with `{ready: false, checks: {storage:
|
|
375
|
+
"failed"}}` — the same storage check as the `sigrun_ready` gauge
|
|
376
|
+
below, so the probe and the alert cannot disagree).
|
|
377
|
+
|
|
378
|
+
**Metrics** (families with nothing to report — no jobs, unconfigured
|
|
379
|
+
backup — are omitted rather than zeroed):
|
|
380
|
+
|
|
381
|
+
| family | meaning |
|
|
382
|
+
|---|---|
|
|
383
|
+
| `sigrun_up` | 1 when `harness.inspect()` answers; 0 on a closed or storage-unreachable session (the scrape itself still answers 200). |
|
|
384
|
+
| `sigrun_ready` | 1 when the readiness storage check (the root-conversation read `/readyz` gates on) succeeds; 0 when it fails — fires even while the process and the scrape stay up. |
|
|
385
|
+
| `sigrun_live_runs` | Conversations with a placed-but-unsettled input submission. |
|
|
386
|
+
| `sigrun_inflight_submission_oldest_seconds` | Age of the oldest in-flight submission's admission (0 when none; in-process clock — a pre-restart run is invisible until it settles). |
|
|
387
|
+
| `sigrun_tasks_live{status}` | Live tasks by scheduler state (running/ready/waiting/completing/blocked, 0s included). |
|
|
388
|
+
| `sigrun_provider_ready{provider,status}` | 1 per provider at its current auth-readiness status (`ready`, `degraded`, `invalid`, `not_ready`, `not_configured`). |
|
|
389
|
+
| `sigrun_extensions{status}` | Configured extensions by load outcome (`ok` / `quarantined`). |
|
|
390
|
+
| `sigrun_job_last_run_timestamp_seconds{job,outcome}` | Unix time of each job's last run, by outcome. |
|
|
391
|
+
| `sigrun_job_next_run_timestamp_seconds{job}` | Unix time of each job's next scheduled fire. |
|
|
392
|
+
| `sigrun_job_runs_total{outcome}` (counter) | Job runs settled by outcome since process start (resets on restart; the durable history lives in the run records). |
|
|
393
|
+
| `sigrun_backup_last_success_timestamp_seconds` | Unix time of the last successful backup marker; omitted when backup is unconfigured or no marker exists. |
|
|
394
|
+
|
|
395
|
+
**Alerts** (the chart's `monitoring.prometheusRule.enabled` toggle ships these):
|
|
396
|
+
|
|
397
|
+
| alert | fires when |
|
|
398
|
+
|---|---|
|
|
399
|
+
| `SigrunNotReady` | `up == 0 or sigrun_up == 0 or sigrun_ready == 0` for 10m — scrape death, harness inspection failing, or the readiness storage check failing (10m tolerates a restart). |
|
|
400
|
+
| `SigrunNoScrape` | The `up{job="sigrun"}` series is absent for 30m — the ServiceMonitor/Prometheus wiring itself broke. |
|
|
401
|
+
| `SigrunSubmissionWedged` | `sigrun_inflight_submission_oldest_seconds > 1800` for 5m — a live run is making no progress. |
|
|
402
|
+
| `SigrunProviderNotReady` | A provider sits at a non-ready auth status for 15m. |
|
|
403
|
+
| `SigrunJobFailed` | `increase(sigrun_job_runs_total{outcome="failed"}[1h]) > 0` — a scheduled job failed. |
|
|
404
|
+
| `SigrunBackupStale` | The backup marker is older than 36h while backup is enabled (the rule is absent on an unbacked deploy). |
|
|
405
|
+
|
|
406
|
+
## Extensions
|
|
407
|
+
|
|
408
|
+
One list in `settings.json` — **`extensions`** — is the desired
|
|
409
|
+
install-and-load state. Every entry is both installed (or fetched) at
|
|
410
|
+
boot and loaded into the registry; removing an entry uninstalls it.
|
|
411
|
+
Entry grammar:
|
|
412
|
+
|
|
413
|
+
| entry | what happens |
|
|
414
|
+
|---|---|
|
|
415
|
+
| `"name"` / `"name@range"` / `"@scope/name@range"` | npm install into the packages dir, then load |
|
|
416
|
+
| `"file:/abs/path"` | npm `file:` install (symlinked source — the dev-checkout shape), then load |
|
|
417
|
+
| `"./rel/path"` / `"/abs/path"` | load in place (nothing installed) |
|
|
418
|
+
| `"git:host/path"` / `"https://…git"`, optional `@ref` | clone into `<config-dir>/git/<…>`, reconcile to the ref, then load |
|
|
419
|
+
| `{ "source": …, "entry"?: "./x.js", "ref"?: "v1.2", "enabled"?: false }` | the explicit form: entry-point override, ref override, or install-but-don't-load |
|
|
420
|
+
|
|
421
|
+
```jsonc
|
|
422
|
+
{
|
|
423
|
+
"extensions": [
|
|
424
|
+
"@acme/sigrun-ext-example@^0.1.0",
|
|
425
|
+
"git:github.com/obra/superpowers@main",
|
|
426
|
+
{ "source": "./my-dev-checkout", "enabled": false }
|
|
427
|
+
]
|
|
428
|
+
}
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
- **Packages dir** — `<config-dir>/packages/`, a real npm root (a
|
|
432
|
+
synthetic private `package.json` + `node_modules/`, pi's
|
|
433
|
+
`~/.pi/agent/npm` shape). Boot diffs the declared npm/file entries
|
|
434
|
+
against the dir's own manifest: missing or range-changed → `npm
|
|
435
|
+
install`, removed → `npm uninstall`, no diff → no subprocess. Install
|
|
436
|
+
failures (or the 120s timeout) fail the boot, naming the specifier.
|
|
437
|
+
- **Git dir** — `<config-dir>/git/`, pi's host/path layout. Absent →
|
|
438
|
+
clone; present with a declared `ref` → `git fetch origin <ref>` +
|
|
439
|
+
checkout `FETCH_HEAD`; present without a ref → fetch only (the
|
|
440
|
+
checkout stays where you left it). Entry discovery: the object form's
|
|
441
|
+
`entry` → a `sigrun` manifest (`{"sigrun": {"extension": "./index.js"}}`)
|
|
442
|
+
→ a pi-compat manifest (the first non-`.ts` `pi.extensions` entry; each
|
|
443
|
+
`.ts` entry is skipped with one warning — it cannot load under plain
|
|
444
|
+
node) → `dist/index.js`.
|
|
445
|
+
- **Skills from packages** — a package may ship skills instead of (or
|
|
446
|
+
beside) extension code: manifest `sigrun.skills` or pi-compat
|
|
447
|
+
`pi.skills` (`["./skills"]`, the same SKILL.md format), or a
|
|
448
|
+
factory-result `skills` field (absolute dirs). A package that declares
|
|
449
|
+
skills and has no loadable entry is a valid **skills-only** package
|
|
450
|
+
(`git:github.com/obra/superpowers` is exactly this shape): outcome `ok`,
|
|
451
|
+
nothing imported. A declared entry that does not resolve still
|
|
452
|
+
quarantines. Discovery merges three roots — the workspace's `skills/`,
|
|
453
|
+
the user dir `<config-dir>/skills/`, and package roots in install order
|
|
454
|
+
— with workspace > user > packages precedence (a collision keeps the
|
|
455
|
+
higher root and logs). The section cap is `skills.sectionMaxBytes`
|
|
456
|
+
(default 4096, live).
|
|
457
|
+
- **Resolution** — npm/file names resolve against the packages dir
|
|
458
|
+
first (the deployment's declared version wins over anything the image
|
|
459
|
+
bundles); `git` entries resolve to their discovered checkout entry;
|
|
460
|
+
then bare import resolution (dev checkouts, image-bundled packages).
|
|
461
|
+
- **Disabled entries** — `{enabled: false}` keeps the directory state
|
|
462
|
+
(installed, cloned) but is never loaded. Re-enable by flipping the
|
|
463
|
+
boolean and restarting.
|
|
464
|
+
|
|
465
|
+
`SIGRUN_EXTENSIONS` overrides the list (env wins, per Configuration's
|
|
466
|
+
rule): comma-separated, and the same grammar per chunk — including
|
|
467
|
+
JSON-object entries.
|
|
468
|
+
|
|
469
|
+
**Restart to change:** everything extension-shaped is boot-time. Docker
|
|
470
|
+
compose (`SIGRUN_CONFIG_DIR=/data/config` on the `sigrun-data` volume —
|
|
471
|
+
see `docker/`): edit `settings.json` in the volume, `docker compose
|
|
472
|
+
restart`. Kubernetes (the chart defaults the config dir onto the same
|
|
473
|
+
PVC as the database): edit the file (`kubectl exec`, or your GitOps
|
|
474
|
+
seeding), `kubectl rollout restart`. Source: edit
|
|
475
|
+
`~/.config/sigrun/settings.json`, restart the process. No image rebuild,
|
|
476
|
+
no manual npm.
|
|
477
|
+
|
|
478
|
+
**Security notes:** `npm install` executes the installed packages'
|
|
479
|
+
lifecycle scripts — operator-configured specifiers carry the same trust
|
|
480
|
+
as the extension code they name (full tool authority in the harness).
|
|
481
|
+
Git sources execute nothing at install (clone/fetch/checkout): the
|
|
482
|
+
extension code loads through the upstream discovery, and git extensions
|
|
483
|
+
must be dependency-light — their repos are loaded directly, NOT
|
|
484
|
+
npm-installed, so a loadable entry (`.js` dist, a sigrun manifest, or a
|
|
485
|
+
ts-loadable source under the tsx-started process) is the contract.
|
|
486
|
+
Load failures quarantine in `GET /v1/meta` (`extensions[].status`) and
|
|
487
|
+
never abort the boot — install failures fail loud at boot.
|
|
488
|
+
|
|
489
|
+
**Writing an extension:** an extension is a plain npm package — a
|
|
490
|
+
default-export factory returning `{ extension }` — and every file it
|
|
491
|
+
touches has exactly one place to live: five stores (settings, secrets,
|
|
492
|
+
data, state, cache) under XDG-respected roots, scoped by the extension's
|
|
493
|
+
slug, with one resolution chain and a fixed secrets precedence (stored
|
|
494
|
+
wins, env bootstraps). The author-facing contract — the store taxonomy,
|
|
495
|
+
the directory map, the slug rule, the `host.stores` handle, uninstall
|
|
496
|
+
and backup posture — is [`docs/extending.md`](docs/extending.md).
|
|
497
|
+
|
|
498
|
+
## Built-in extensions
|
|
499
|
+
|
|
500
|
+
Three extensions are built into core — **codemode**, **MCP**, and
|
|
501
|
+
**tool search** — mirroring what pi 1.3.0 ships built-in. They load by
|
|
502
|
+
default on every boot, with no settings entry, and resolve from core's
|
|
503
|
+
own `node_modules` (they are core dependencies, never installed into
|
|
504
|
+
the packages dir — `extensions` has nothing to install for them, unless
|
|
505
|
+
a same-named package is deliberately installed to replace one; see
|
|
506
|
+
below).
|
|
507
|
+
A default install therefore has pi's tool surface without configuring
|
|
508
|
+
anything. On `GET /v1/extensions` they report under the names
|
|
509
|
+
`codemode`, `sigrun-mcp`, and `tool-search`.
|
|
510
|
+
|
|
511
|
+
**The tools stay off until needed** (pi parity): `codemode` and
|
|
512
|
+
`tool_search` are not in a new conversation's model-offered tool set by
|
|
513
|
+
default. They join the offered set when an enabled MCP server's
|
|
514
|
+
`exposure` needs them, or when `defaultTools` names them explicitly.
|
|
515
|
+
|
|
516
|
+
- **The auto-enable rule** — with an enabled MCP server whose `exposure`
|
|
517
|
+
is `codemode` (the default), the `codemode` tool is offered; with
|
|
518
|
+
`deferred`, `tool_search` is. `"autoEnableCodemode": false` turns
|
|
519
|
+
both arms off. An explicit `defaultTools` list always wins over the
|
|
520
|
+
automatic decision (auto-enable only narrows the default set, never a
|
|
521
|
+
named list).
|
|
522
|
+
- **Turning one off** — `"builtinExtensions": {"codemode": false,
|
|
523
|
+
"mcp": false, "toolSearch": false}`. Each defaults to `true`; each is
|
|
524
|
+
restart-required. A disabled built-in disappears from
|
|
525
|
+
`GET /v1/extensions` entirely — no row, not even a skipped one — and
|
|
526
|
+
its config is then ignored.
|
|
527
|
+
- **Replacing one** — installing a package that resolves to the same
|
|
528
|
+
package name as a built-in replaces it: the installed copy loads, the
|
|
529
|
+
built-in is skipped, boot logs one warning naming both, and
|
|
530
|
+
`GET /v1/extensions` shows the built-in's row with `"builtin": true`
|
|
531
|
+
and a `replacedBy` field naming the configured specifier.
|
|
532
|
+
- **Quarantine** — a built-in that fails to load is quarantined like
|
|
533
|
+
any other extension: boot continues, the outcome is reported (status
|
|
534
|
+
`quarantined`, in `GET /v1/meta` alongside the configured set).
|
|
535
|
+
|
|
536
|
+
The MCP servers themselves live in the MCP config file — the
|
|
537
|
+
`@sigrun/ext-mcp` package's README documents its format, including each
|
|
538
|
+
server's `exposure` and the `SIGRUN_MCP_CONFIG` override.
|
|
539
|
+
|
|
540
|
+
```jsonc
|
|
541
|
+
{
|
|
542
|
+
"extensions": [ // configured extensions install + load as above
|
|
543
|
+
"@acme/sigrun-ext-example@^0.1.0"
|
|
544
|
+
],
|
|
545
|
+
"builtinExtensions": { // all three default to true; restart required
|
|
546
|
+
"codemode": true,
|
|
547
|
+
"mcp": true,
|
|
548
|
+
"toolSearch": false
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
## Skills and prompt templates
|
|
554
|
+
|
|
555
|
+
Two text-expansion surfaces ship in core, pi parity: a **skill
|
|
556
|
+
invocation** and a **prompt template**, both expanded SERVER-SIDE at
|
|
557
|
+
admission (in `submitOp`, before content-part resolution) — so the
|
|
558
|
+
webui, the TUI, the HTTP API, `host.conversations.submit`, and job
|
|
559
|
+
prompts all behave identically. Expansion happens when the submitted
|
|
560
|
+
text starts with `/`; everything else is admitted verbatim.
|
|
561
|
+
|
|
562
|
+
**`/skill:<name> [args]`** expands to the skill's full body:
|
|
563
|
+
|
|
564
|
+
```text
|
|
565
|
+
<skill name="greet" location="skills/greet/SKILL.md">
|
|
566
|
+
References are relative to skills/greet.
|
|
567
|
+
|
|
568
|
+
Say hello to the user warmly.
|
|
569
|
+
</skill>
|
|
570
|
+
|
|
571
|
+
hej
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
(the blank line and the arguments only when arguments were given; the
|
|
575
|
+
body is the SKILL.md with frontmatter stripped). An unknown skill
|
|
576
|
+
passes through unchanged — never an error. A skill with
|
|
577
|
+
`disable-model-invocation: true` stays out of the model's skill list
|
|
578
|
+
but still expands here. Every skill appears in the `/` menu as
|
|
579
|
+
`skill:<name>` with its description.
|
|
580
|
+
|
|
581
|
+
**Prompt templates** are Markdown files whose filename is the command
|
|
582
|
+
(`review.md` → `/review`). Frontmatter `description` (else the first
|
|
583
|
+
non-empty line) feeds the `/` menu, optional `argument-hint` hints the
|
|
584
|
+
arguments, and the body substitutes shell-quoted arguments:
|
|
585
|
+
`$1`, `$2`, …; `$@` and `$ARGUMENTS` (all arguments, space-joined);
|
|
586
|
+
`${1:-default}`; `${@:-default}`; `${@:N}`; `${@:N:L}`.
|
|
587
|
+
`/review "API compatibility"` passes ONE argument. An unterminated
|
|
588
|
+
quote is a usage error, not a silent swallow.
|
|
589
|
+
|
|
590
|
+
Both are discovered from the same three roots, first-wins in this
|
|
591
|
+
order (the skills convention's precedence):
|
|
592
|
+
|
|
593
|
+
1. the workspace — `skills/` and `prompts/` under the conversation
|
|
594
|
+
workspace;
|
|
595
|
+
2. the user dir — `<config-dir>/skills/` and `<config-dir>/prompts/`;
|
|
596
|
+
3. packages — each installed package's `skills/` (manifest or
|
|
597
|
+
factory-declared) and `prompts/` directories.
|
|
598
|
+
|
|
599
|
+
Both lists are re-scanned live (each `/v1/commands` request and each
|
|
600
|
+
expansion) — drop a file in and it works, no restart. A template whose
|
|
601
|
+
name collides with a core or extension command is skipped with one
|
|
602
|
+
warning: commands keep their meaning. The transcript shows what you
|
|
603
|
+
typed — the submission's user entry carries
|
|
604
|
+
`{kind: "sigrun.user.expanded", data: {typed, command}}` while the
|
|
605
|
+
model receives the expanded text.
|
|
606
|
+
|
|
607
|
+
## Jobs
|
|
608
|
+
|
|
609
|
+
A job is a schedule plus an action, durable by construction — one
|
|
610
|
+
SQLite-backed record per job, one immutable revision per change, and
|
|
611
|
+
one record per run. Jobs are the only time-based scheduler in sigrun
|
|
612
|
+
(extensions that need a timer declare a job with a `task` action).
|
|
613
|
+
|
|
614
|
+
**Three sources:**
|
|
615
|
+
|
|
616
|
+
| source | where it is declared | id | mutable via the API |
|
|
617
|
+
|---|---|---|---|
|
|
618
|
+
| `api` | `POST /v1/jobs` | `api:<uuid>` | yes (owner or admin) |
|
|
619
|
+
| `file` | the settings file's `jobs[]` list | `file:<name>` | no — edit the file |
|
|
620
|
+
| `extension` | an installed extension's factory result | `ext:<slug>:<name>` | no |
|
|
621
|
+
|
|
622
|
+
`file` and `extension` declarations reconcile into job records at
|
|
623
|
+
boot and on change; the declaration is the source of truth, so the API
|
|
624
|
+
is read-only for them (`PATCH`/`DELETE` return `409 job_declared`).
|
|
625
|
+
|
|
626
|
+
**The shape** (spec §8.2):
|
|
627
|
+
|
|
628
|
+
```jsonc
|
|
629
|
+
{
|
|
630
|
+
"name": "morning-brief",
|
|
631
|
+
"schedule": { "cron": "0 7 * * *", "timezone": "America/Toronto" }, // or { "every": "15m" } or { "at": "<ISO 8601>" }
|
|
632
|
+
"action": { "kind": "prompt", "prompt": "Summarize …", "model": "bifrost/glm-5.3-flash",
|
|
633
|
+
"conversation": "new" }, // or a fixed conversation id
|
|
634
|
+
"overlap": "skip", // default; "queue" admits anyway (the conversation's inbox queues it)
|
|
635
|
+
"catchUp": "once", // default; "none" skips fires missed while the server was down
|
|
636
|
+
"enabled": true
|
|
637
|
+
}
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
A `prompt` action submits its prompt as a conversation — `"new"`
|
|
641
|
+
creates one per fire (ownerless, `source: "cron"`), a fixed id submits
|
|
642
|
+
into that conversation (the job's `model`/`thinking` apply only to
|
|
643
|
+
`"new"`). A `task` action creates a pi-durable task owned by the
|
|
644
|
+
schedule task (deleting the job cascades): `{"kind": "task",
|
|
645
|
+
"task": "<registered task name>", "input": …}`.
|
|
646
|
+
|
|
647
|
+
**Runs:** a cron run is an ORDINARY CONTINUABLE CONVERSATION — open the
|
|
648
|
+
run conversation in either UI and keep talking; it is never a
|
|
649
|
+
disposable side effect. `overlap: "skip"` (the default) records a
|
|
650
|
+
`skipped` run when the previous fire is still live; `catchUp: "once"`
|
|
651
|
+
(the default) fires once after downtime, `"none"` skips missed fires
|
|
652
|
+
entirely. Runs carry the job `version` that fired them, so an outcome
|
|
653
|
+
always maps to the exact definition it ran.
|
|
654
|
+
|
|
655
|
+
**Run now:** `POST /v1/jobs/{id}/run` (write access) fires immediately
|
|
656
|
+
— the same hook external triggers (CI, Alertmanager) use with a token.
|
|
657
|
+
In the web UI the Jobs panel (the clock icon in the header) lists jobs
|
|
658
|
+
with a “Run now” button; in the TUI `/jobs` prints the table (name,
|
|
659
|
+
next run, last outcome).
|
|
660
|
+
|
|
661
|
+
## API sketch
|
|
662
|
+
|
|
663
|
+
- `GET /v1/meta` — version, extensions, health, auth posture
|
|
664
|
+
(`methods`, `claimed`), capabilities.
|
|
665
|
+
- `GET`/`PATCH /v1/settings` — the settings file (see above).
|
|
666
|
+
- `GET /v1/jobs`, `POST /v1/jobs`, `PATCH`/`DELETE /v1/jobs/{id}` —
|
|
667
|
+
the jobs surface (see [Jobs](#jobs)); `POST /v1/jobs/{id}/run` fires
|
|
668
|
+
now; `GET /v1/jobs/{id}/runs` and `GET /v1/jobs/{id}/revisions` are
|
|
669
|
+
the history (newest first).
|
|
670
|
+
- `POST /v1/conversations`, `POST /v1/conversations/{id}/submissions`,
|
|
671
|
+
`GET /v1/conversations/{id}` — the durable conversation surface
|
|
672
|
+
(SSE streaming views included).
|
|
673
|
+
- `GET /v1/providers` — per-provider readiness, statuses only (never
|
|
674
|
+
credentials); `GET /v1/auth/check` is the deprecated alias.
|
|
675
|
+
- `GET /v1/onboarding`, `POST /v1/onboarding/claim` — the unclaimed
|
|
676
|
+
state and the first-user claim.
|
|
677
|
+
- `POST /v1/auth/login`, `POST /v1/auth/logout`, `GET /v1/auth/whoami` —
|
|
678
|
+
the browser session and the resolved principal.
|
|
679
|
+
- `GET`/`POST /v1/auth/tokens`, `DELETE /v1/auth/tokens/{id}` —
|
|
680
|
+
self-service API tokens.
|
|
681
|
+
- `GET /v1/users`, `PATCH`/`DELETE /v1/users/{user}`,
|
|
682
|
+
`POST /v1/users/invites` — the user directory and invite codes
|
|
683
|
+
(admin-gated).
|
|
684
|
+
- `GET /healthz`, `GET /readyz` — probes.
|
|
685
|
+
|
|
686
|
+
## Development
|
|
687
|
+
|
|
688
|
+
```bash
|
|
689
|
+
npm test # full suite (vitest)
|
|
690
|
+
npm run typecheck # tsc --noEmit
|
|
691
|
+
npm start # tsx src/index.ts
|
|
692
|
+
```
|