@sigrun/sigrun 0.1.6 → 0.2.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.
Files changed (137) hide show
  1. package/CHANGELOG.md +192 -0
  2. package/README.md +445 -42
  3. package/dist/blocks/build-stamp.json +13 -0
  4. package/dist/blocks/builders.d.ts +57 -0
  5. package/dist/blocks/images.d.ts +26 -0
  6. package/dist/blocks/index.d.ts +20 -0
  7. package/dist/blocks/limits.d.ts +25 -0
  8. package/dist/blocks/placements.d.ts +26 -0
  9. package/dist/blocks/schema.d.ts +38 -0
  10. package/dist/blocks/schema.js +9 -2
  11. package/dist/blocks/validate.d.ts +73 -0
  12. package/dist/blocks/validate.js +57 -8
  13. package/dist/cli.js +26 -2
  14. package/dist/config.js +67 -1
  15. package/dist/extension.d.ts +230 -22
  16. package/dist/harness/ambient.js +39 -185
  17. package/dist/harness/blocks/entries.js +47 -0
  18. package/dist/harness/blocks/state.js +58 -0
  19. package/dist/harness/blocks/tools.js +274 -0
  20. package/dist/harness/boot.js +259 -20
  21. package/dist/harness/builtins.js +15 -11
  22. package/dist/harness/event-bus.js +105 -10
  23. package/dist/harness/identity/accounts.js +186 -0
  24. package/dist/harness/identity/groups.js +14 -0
  25. package/dist/harness/identity/index.js +13 -6
  26. package/dist/harness/identity/json-store.js +15 -4
  27. package/dist/harness/identity/setup-code-check.js +9 -0
  28. package/dist/harness/identity/tokens.js +28 -5
  29. package/dist/harness/identity/users.js +111 -12
  30. package/dist/harness/interactions.js +292 -136
  31. package/dist/harness/loader.js +271 -17
  32. package/dist/harness/packages.js +1 -1
  33. package/dist/harness/reserved-sections.js +21 -0
  34. package/dist/harness/seam-host.js +92 -6
  35. package/dist/harness/sections/context-files.js +1 -1
  36. package/dist/harness/settings.js +63 -2
  37. package/dist/harness/stores.js +241 -4
  38. package/dist/harness/user-services.js +391 -0
  39. package/dist/harness/user-tools.js +187 -0
  40. package/dist/harness/workspace.js +61 -57
  41. package/dist/jobs/fire.js +10 -16
  42. package/dist/sandbox/availability.js +211 -0
  43. package/dist/sandbox/command-observer.js +59 -0
  44. package/dist/sandbox/grants.js +159 -0
  45. package/dist/sandbox/helper.js +29 -0
  46. package/dist/sandbox/isolation.js +29 -0
  47. package/dist/sandbox/launch.js +31 -0
  48. package/dist/sandbox/os-account.js +49 -0
  49. package/dist/sandbox/policy.js +274 -0
  50. package/dist/sandbox/pool.js +548 -0
  51. package/dist/sandbox/protocol.js +167 -0
  52. package/dist/sandbox/sandboxed-env.js +245 -0
  53. package/dist/sandbox/unavailable-env.js +49 -0
  54. package/dist/sandbox/worker.js +261 -0
  55. package/dist/serve.js +213 -25
  56. package/dist/server/auth/access-principal.js +16 -0
  57. package/dist/server/auth/pipeline.js +99 -36
  58. package/dist/server/blocks/answers.js +94 -0
  59. package/dist/server/blocks/chat-surfaces.js +393 -0
  60. package/dist/server/blocks/notes.js +216 -0
  61. package/dist/server/blocks/summaries.js +328 -0
  62. package/dist/server/commands.js +5 -0
  63. package/dist/server/content-parts-lazy.js +13 -0
  64. package/dist/server/conversation-handler.js +2 -5
  65. package/dist/server/conversation-ops.js +138 -37
  66. package/dist/server/helpers.js +18 -6
  67. package/dist/server/host-bridge.js +40 -20
  68. package/dist/server/images/cache.js +213 -0
  69. package/dist/server/images/convert.js +13 -0
  70. package/dist/server/images/fetch.js +302 -0
  71. package/dist/server/images/ranges.js +42 -0
  72. package/dist/server/images/rewrite.js +94 -0
  73. package/dist/server/images/sign.js +84 -0
  74. package/dist/server/images/sniff.js +62 -0
  75. package/dist/server/index.js +152 -4
  76. package/dist/server/login/flows.js +126 -0
  77. package/dist/server/login/http.js +31 -0
  78. package/dist/server/login/resolve.js +133 -0
  79. package/dist/server/login/setup-claim.js +55 -0
  80. package/dist/server/login/usernames.js +15 -0
  81. package/dist/server/notifications.js +21 -4
  82. package/dist/server/ownership.js +13 -0
  83. package/dist/server/routes/admin.js +58 -0
  84. package/dist/server/routes/auth.js +88 -2
  85. package/dist/server/routes/conversations.js +3 -6
  86. package/dist/server/routes/events.js +28 -3
  87. package/dist/server/routes/extensions.js +37 -5
  88. package/dist/server/routes/images.js +274 -0
  89. package/dist/server/routes/interactions.js +57 -68
  90. package/dist/server/routes/jobs.js +2 -8
  91. package/dist/server/routes/login.js +223 -0
  92. package/dist/server/routes/me.js +90 -0
  93. package/dist/server/routes/meta.js +19 -0
  94. package/dist/server/routes/onboarding.js +94 -84
  95. package/dist/server/routes/settings-sections.js +190 -0
  96. package/dist/server/routes/settings.js +16 -4
  97. package/dist/server/routes/submissions.js +1 -1
  98. package/dist/server/routes/surfaces.js +175 -23
  99. package/dist/server/routes/users.js +116 -20
  100. package/dist/server/schema-fixture.js +213 -0
  101. package/dist/server/session-meta.js +3 -0
  102. package/dist/server/shutdown.js +11 -4
  103. package/dist/server/static.js +18 -0
  104. package/dist/server/surfaces/dedup.js +9 -2
  105. package/dist/server/surfaces/service.js +181 -13
  106. package/dist/server/surfaces/settings-surfaces.js +597 -0
  107. package/dist/server/turns/submit-turn.js +26 -0
  108. package/dist/server/views.js +16 -0
  109. package/dist/settings/core-sections.js +532 -0
  110. package/dist/settings/pending.js +181 -0
  111. package/dist/settings/registry.js +453 -0
  112. package/dist/skills/index.js +6 -0
  113. package/node_modules/@sigrun/shared/CHANGELOG.md +11 -0
  114. package/node_modules/@sigrun/shared/dist/access.d.ts +4 -2
  115. package/node_modules/@sigrun/shared/dist/access.js +11 -4
  116. package/node_modules/@sigrun/shared/dist/ambient.d.ts +17 -74
  117. package/node_modules/@sigrun/shared/dist/ambient.js +8 -6
  118. package/node_modules/@sigrun/shared/dist/blocks/text.js +16 -2
  119. package/node_modules/@sigrun/shared/dist/blocks/transport.d.ts +4 -0
  120. package/node_modules/@sigrun/shared/dist/blocks/types.d.ts +21 -4
  121. package/node_modules/@sigrun/shared/dist/interactions.d.ts +22 -48
  122. package/node_modules/@sigrun/shared/dist/interactions.js +9 -7
  123. package/node_modules/@sigrun/shared/dist/settings.d.ts +27 -2
  124. package/node_modules/@sigrun/shared/dist/themes.d.ts +2 -3
  125. package/node_modules/@sigrun/shared/dist/themes.js +2 -3
  126. package/node_modules/@sigrun/shared/dist/turns.d.ts +14 -0
  127. package/node_modules/@sigrun/shared/dist/turns.js +53 -3
  128. package/node_modules/@sigrun/shared/dist/views.d.ts +202 -12
  129. package/node_modules/@sigrun/shared/dist/views.js +16 -0
  130. package/node_modules/@sigrun/shared/package.json +1 -1
  131. package/package.json +13 -9
  132. package/webui/dist/assets/index-SO2W4jjk.js +55 -0
  133. package/webui/dist/assets/index-yqkL6BSS.css +1 -0
  134. package/webui/dist/index.html +2 -2
  135. package/CONTEXT.md +0 -163
  136. package/webui/dist/assets/index-CiYInlLx.css +0 -1
  137. package/webui/dist/assets/index-JDfOQV9z.js +0 -52
package/README.md CHANGED
@@ -1,11 +1,28 @@
1
1
  # Sigrun
2
2
 
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.
3
+ Sigrun is a **durable, multi-user personal-assistant server** built on
4
+ [pi-durable](https://www.npmjs.com/package/@earendil-works/pi-durable).
5
+ One agent loop serves every surface — a web UI, a terminal UI, an HTTP
6
+ API, and (via the gateway extension) chat channels — over long-lived
7
+ conversations stored in SQLite.
8
+ An extension model installs extra tools, providers, prompt sections,
9
+ memory systems, and login providers at boot.
10
+
11
+ **Why use it?** Conversations survive restarts and resume where they
12
+ stopped; memory and integrations are extension-owned and individually
13
+ replaceable; multi-user boundaries are explicit (real authentication plus
14
+ a per-user execution jail); and it deploys as an npm package, a Docker
15
+ compose reference, or a Helm chart. [`docs/why.md`](docs/why.md) is the
16
+ longer rationale.
17
+
18
+ **Status:** 0.x — useful and exercised, but extension and settings
19
+ contracts can still evolve between minor releases. See
20
+ [`SECURITY.md`](SECURITY.md) for the support, trust, and verification
21
+ posture, and [`CONTRIBUTING.md`](CONTRIBUTING.md) to build and test.
22
+
23
+ **Try it:** `npm i -g @sigrun/sigrun && sigrun run` — or jump to
24
+ [Install](#install) and [Quickstart](#quickstart) for the model/credential
25
+ setup every install needs.
9
26
 
10
27
  - **Durable** — conversations, submissions, and schedules live in one
11
28
  SQLite database (`SIGRUN_STORAGE_PATH`); a restart resumes where it
@@ -104,7 +121,7 @@ entrypoints:
104
121
  | Command | Behavior |
105
122
  |---|---|
106
123
  | `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. |
124
+ | `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 (`::`, the dual-stack wildcard — accepts IPv4 and IPv6; a v4-only `0.0.0.0` bind is unreachable on a dual-stack pod's IPv6 endpoint) — the container default; workstation `sigrun` (run) binds loopback only. |
108
125
  | `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
126
 
110
127
  Resolution chains for `attach` (and for the workstation-mode user):
@@ -156,6 +173,7 @@ still override it per field.
156
173
  | `OPENAI_API_KEY` | string | — | `auth.json` (see below) | Credential for the env-driven OpenAI-compatible provider; the `auth.json` fallback after file entries. |
157
174
  | `OPENAI_BASE_URL` | URL | OpenAI | `defaultProvider.baseUrl` | Endpoint for the env-driven provider (any compatible host). |
158
175
  | `SIGRUN_PORT` | positive int | `3000` | `server.port` | HTTP listen port. |
176
+ | `SIGRUN_LISTEN_HOST` | host | `::` | — | HTTP listen host — the environment equivalent of the programmatic `serve({host})` option (the option wins when both are set). One resolved value drives the bind AND the auth defaults: a loopback bind derives the no-auth workstation posture, any other bind derives `token` auth. `GET /v1/meta` reports the effective listener (`listener.host`/`port`/`families`). |
159
177
  | `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
178
  | `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
179
  | `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. |
@@ -265,8 +283,10 @@ list explicitly — explicit always wins, including `[]` on a non-
265
283
  loopback bind, which boots with a warning that any reachable client is
266
284
  trusted. The related env knobs: `SIGRUN_AUTH_TRUSTED_PROXIES` (CIDR
267
285
  csv), `SIGRUN_PROXY_SECRET` and `SIGRUN_API_TOKEN` (credentials —
268
- env-only, never in `settings.json`). Every auth setting is
269
- restart-required.
286
+ env-only, never in `settings.json`). Every auth setting except
287
+ `server.auth.sessionDays` is restart-required. `sessionDays` applies live to
288
+ new sessions and has no core settings-section field; change it in the
289
+ settings file or with `PATCH /v1/settings`.
270
290
 
271
291
  **Tokens.** API tokens are `sgr_`-prefixed secrets shown once at
272
292
  creation (the store keeps only SHA-256 digests) and are self-service:
@@ -275,7 +295,8 @@ an authenticated user lists and mints their own via
275
295
  admins may revoke anyone's). A token whose user was deleted is dead.
276
296
  A browser exchanges a token for a `sigrun_session` cookie at
277
297
  `POST /v1/auth/login` — invalid attempts are delayed and rate-limited —
278
- and `GET /v1/auth/whoami` returns the resolved principal.
298
+ and `GET /v1/auth/whoami` returns the resolved principal. An IdP-signed-in
299
+ user can mint their own API token for the TUI through the same token routes.
279
300
 
280
301
  **Proxy method (forward auth).** A trusted reverse proxy vouches for
281
302
  the caller through headers (Authelia's `Remote-*` defaults,
@@ -286,19 +307,166 @@ configurable) when the request comes from a peer inside
286
307
  proxy and nothing else: every peer in `trustedProxies` can forge any
287
308
  identity, including an admin.
288
309
 
289
- **Group-derived admin (persisted).** `server.auth.adminGroups` lists
290
- the proxy groups (Authelia's `Remote-Groups`) whose members act as
291
- admins. The request pipeline computes the grant per request and
292
- writes it through to the user's directory record as `groupAdmin`
293
- whenever it differs — so host-side surfaces with no request headers
294
- (chat commands, channel adapters) see effective admin (`admin ||
295
- groupAdmin`), while `/v1/users` keeps `admin` as the stored flag the
296
- users-admin UI toggles and shows `groupAdmin` alongside. The grant
297
- moves only on proxy-authenticated requests while `adminGroups` is
298
- non-empty: removal from the admin group takes effect at that user's
299
- next web request. Group-derived admin never counts toward the
300
- last-admin guard — demoting or deleting the only stored admin is
301
- still refused.
310
+ **Stored groups and group-derived admin.** `server.auth.adminGroups`
311
+ lists groups (Authelia's `Remote-Groups`, for example) whose members act as
312
+ admins. Core stores groups per authenticating source: `proxy` for validated
313
+ forward-auth headers and `login:<slug>:<id>` for an external login. Effective
314
+ admin is the stored `admin` flag OR any stored group in `adminGroups`, so
315
+ web, chat, and host-side checks agree. `/v1/users` keeps `admin` as the
316
+ stored flag the users-admin UI toggles; its `groupAdmin` compatibility field
317
+ is derived, never persisted, and is retained for one release. Group-derived
318
+ admin never counts toward the last-admin guard — demoting or deleting the
319
+ only stored admin is still refused.
320
+
321
+ Login-provider groups participate in `server.auth.adminGroups`, so configure
322
+ only providers whose group claims are administrator-controlled. A linked
323
+ account's groups come from the provider at each sign-in; removing admin
324
+ therefore requires changing the provider's claim or removing the link/user.
325
+
326
+ Groups refresh only at that user's next login or proxy request. This
327
+ staleness is deliberate: a group removal at the IdP does not take effect
328
+ until then. Token and session requests only read the stored record; they
329
+ never accept groups from the request. People-scoped conversation shares may
330
+ also name groups, matched against the same stored groups.
331
+
332
+ ### External sign-in (OIDC and OAuth 2)
333
+
334
+ The web sign-in screen lists configured external providers above the token
335
+ form when the effective auth methods include `token`; `/v1/auth/providers`
336
+ returns an empty list on proxy-only deployments because their completed
337
+ sign-ins could not authenticate the resulting session. The TUI continues to
338
+ use tokens. Login providers come from extensions.
339
+ The reference implementation is **`@sigrun/oauth`, the fourth built-in
340
+ extension**. It is bundled and loads by default, so no `extensions` entry is
341
+ needed. Set `"builtinExtensions": {"oauth": false}` to disable it; like the
342
+ other built-in toggles, this is restart-required. A configured extension
343
+ whose package name is also `@sigrun/oauth` replaces the built-in. If the
344
+ built-in cannot be resolved or loaded, it is quarantined without aborting
345
+ boot. Standalone installation outside the built-in is not a supported use
346
+ case in 0.2; the package's type-only core dependency posture is accepted
347
+ for this release. It is inert when its settings contain no valid providers.
348
+
349
+ Create an OAuth/OIDC client at the identity service, with the redirect URI
350
+ shown below. Configure providers in the extension's
351
+ `<config-dir>/settings/oauth.json`; an administrator can edit that JSON in
352
+ Settings → Extensions → oauth. Set `server.publicUrl` (or
353
+ `SIGRUN_PUBLIC_URL`) to sigrun's externally reachable origin so callback
354
+ URLs are stable; otherwise core derives one from the request.
355
+
356
+ For a generic OIDC service — Authelia, Authentik, and Keycloak are examples
357
+ of products this form fits, not service-specific presets — configure its
358
+ issuer and client:
359
+
360
+ ```json
361
+ {
362
+ "providers": [
363
+ {
364
+ "id": "company",
365
+ "label": "Company SSO",
366
+ "kind": "oidc",
367
+ "issuer": "https://idp.example.invalid",
368
+ "clientId": "sigrun",
369
+ "scopes": "openid profile email",
370
+ "usernameClaim": "preferred_username",
371
+ "groupsClaim": "groups",
372
+ "autoProvision": false,
373
+ "allowedGroups": ["staff"],
374
+ "allowedDomains": ["example.invalid"]
375
+ }
376
+ ]
377
+ }
378
+ ```
379
+
380
+ The extension uses OIDC discovery, authorization code with PKCE S256 and a
381
+ nonce, and validates the ID token. `scopes`, `usernameClaim`, and
382
+ `groupsClaim` shown above are the defaults. For this example, register
383
+ `https://sigrun.example.invalid/v1/auth/callback/login:oauth:company` as
384
+ the redirect URI (the provider id is qualified, not merely `company`).
385
+ Production identity endpoints must use HTTPS; plain HTTP is restricted to
386
+ loopback development and tests.
387
+
388
+ Every provider needs a client secret named `<id>.clientSecret` in the
389
+ **oauth extension secret store** — `company.clientSecret` here. Set it in
390
+ Settings → Extensions → oauth (write-only), or as an administrator with
391
+ `PUT /v1/extensions/oauth/secrets/company.clientSecret` and body
392
+ `{"value":"<client-secret>"}`. Never put a client secret in
393
+ `settings/oauth.json`. Extension settings are read at boot; restart after
394
+ changing them. Secrets are re-read per use, so a rotated client secret
395
+ applies to the next login without a restart.
396
+
397
+ The GitHub and Discord presets replace issuer/endpoint discovery with their
398
+ protocol endpoints and user field mappings:
399
+
400
+ ```json
401
+ {
402
+ "providers": [
403
+ {
404
+ "id": "github",
405
+ "label": "GitHub",
406
+ "kind": "oauth2",
407
+ "preset": "github",
408
+ "clientId": "acme-github-client-id"
409
+ },
410
+ {
411
+ "id": "discord",
412
+ "label": "Discord",
413
+ "kind": "oauth2",
414
+ "preset": "discord",
415
+ "clientId": "acme-discord-client-id"
416
+ }
417
+ ]
418
+ }
419
+ ```
420
+
421
+ **Both presets require `clientId` in settings and a client secret in the
422
+ extension secret store** (`github.clientSecret` and
423
+ `discord.clientSecret`). Their redirect URIs end in
424
+ `/v1/auth/callback/login:oauth:github` and
425
+ `/v1/auth/callback/login:oauth:discord` on the same sigrun origin. GitHub
426
+ uses only a primary, verified email; Discord uses its verified flag.
427
+ Neither preset supplies groups. A malformed provider entry is skipped with
428
+ a warning naming it; other valid entries still load.
429
+
430
+ For another OAuth 2 service, use `kind: "oauth2"` with explicit
431
+ `authorizeUrl`, `tokenUrl`, and `userinfoUrl`, plus `fields` mapping a
432
+ required `subject` and optional `username`, `name`, `email`,
433
+ `emailVerified`, and `groups` dot-separated response paths. `clientId`, its
434
+ `<id>.clientSecret` secret, and the same optional policy fields apply.
435
+
436
+ **First login never auto-links by username or email.** An unlinked subject
437
+ on an unclaimed server needs the one-shot setup code and becomes the first
438
+ admin. On a claimed server, `autoProvision: false` (the default) requires an
439
+ invite code; `true` creates a non-admin user, optionally restricted by
440
+ `allowedGroups` and `allowedDomains`. **These filters apply only when
441
+ creating a new user by auto-provisioning** — not to an already-linked
442
+ account's logins or to invite-based provisioning. A linked account keeps
443
+ working; remove its link (or the user) to remove access. A domain match
444
+ additionally requires `emailVerified === true` from the provider. Core
445
+ enforces the M1 admission prerequisites: adding a second user is refused
446
+ without available execution isolation and real `token` and/or `proxy`
447
+ authentication. An external IdP is not itself a trusted proxy.
448
+
449
+ ### Browser sessions and account links
450
+
451
+ A successful external sign-in creates a `sigrun_session` cookie backed by a
452
+ revocable session record in the token store. `server.auth.sessionDays`
453
+ controls its lifetime (default **30 days**, live for newly created
454
+ sessions). An expired session is rejected as unknown (401) and its record
455
+ is pruned on the next token-store write. Logout revokes the current
456
+ session; users can also see and revoke sessions in their token list.
457
+
458
+ A session has the user's normal rights, including creating that user's own
459
+ API tokens for the TUI. **Revoking a session does not revoke API tokens
460
+ created with it**; revoke those separately. Core does not store provider
461
+ access or refresh tokens.
462
+
463
+ Settings → Account links is the self-service panel for external identities.
464
+ Connect starts the provider flow with `?link=1` while signed in; the
465
+ identity is linked to the caller instead of creating another sign-in.
466
+ Users can remove their own links and administrators can manage others'
467
+ links. One `(provider, subject)` pair belongs to one user: a link is never
468
+ re-pointed to a different user. The holder or an administrator must remove
469
+ a conflicting old link first.
302
470
 
303
471
  **Onboarding (the first claim).** An unclaimed server serves
304
472
  `GET /v1/onboarding` + `POST /v1/onboarding/claim`. On a loopback bind
@@ -313,11 +481,195 @@ users (`PATCH`/`DELETE /v1/users/{user}` — deletion transfers their
313
481
  conversations).
314
482
 
315
483
  **Sharing.** Conversations are private to their owner by default. The
316
- owner can share one — household-wide or with named users, `read` or
317
- `write` — through the web UI's ⋯ menu or the TUI's `/share`; `write`
484
+ owner can share one — household-wide or with named users and groups,
485
+ `read` or `write` — through the web UI's ⋯ menu; `write`
318
486
  lets a recipient submit, answer, and fork, but only the owner can
319
487
  re-share. Household sharing defaults to `read`.
320
488
 
489
+ ### Per-user extensions and deletion
490
+
491
+ Extensions can offer personal tools, settings, credentials, and services
492
+ instead of only server-wide resources. Each conversation selects its
493
+ owner's per-user tools, and core also checks the committed owner every
494
+ time one executes: selecting another user's entry does not permit its
495
+ use. Per-user services run as that user in their own service jail,
496
+ sibling to the agent's worker. A credential supplied to a service never
497
+ belongs in the agent workspace. See
498
+ [`docs/extending.md`](docs/extending.md#per-user-stores) for the extension
499
+ contract and `/v1/me` self-service routes.
500
+
501
+ A user's extension secrets live in
502
+ `<configDir>/user-secrets/<user>.json`, mode `0600`, with one section
503
+ per extension slug. Their settings live under
504
+ `<configDir>/user-settings/<user>/`; extension data, state, and cache
505
+ have separate per-user paths. Login, sessions, and account linking are
506
+ documented in [External sign-in](#external-sign-in-oidc-and-oauth-2) and
507
+ [Browser sessions and account links](#browser-sessions-and-account-links).
508
+
509
+ `DELETE /v1/users/{user}?transferTo={recipient}` requires an admin and
510
+ another directory user as recipient. Core transfers conversations and
511
+ owned resources, then revokes the departing user's tokens and account
512
+ links, stops their worker and services, and removes their per-user tool
513
+ entries. Extensions then receive `userDeleted` while the old record and
514
+ secrets remain readable, so they can revoke external provider
515
+ credentials. Finally core removes the directory record, per-user secrets,
516
+ user-settings directory, and per-user cache directories.
517
+
518
+ **Credentials belong to a person and never transfer.** Data and state
519
+ stay for the extension to retain, move, or delete according to its own
520
+ rules; core does not make that choice.
521
+
522
+ ## Multi-user execution isolation
523
+
524
+ `server.isolation.mode` (or `SIGRUN_ISOLATION`) controls the execution jail:
525
+
526
+ | Mode | Behavior |
527
+ |---|---|
528
+ | `auto` (default) | Turns isolation on when the directory has more than one user. Once active, it stays active until restart. |
529
+ | `always` | Confines every user, including a lone admin. |
530
+ | `off` | No jail; permitted only with at most one directory user. |
531
+
532
+ A second user is refused when isolation is unavailable or `off`, and a
533
+ multi-user server never falls back to unconfined execution. Multi-user also
534
+ requires real authentication (`token` and/or `proxy`).
535
+
536
+ Isolation requires Linux with Landlock ABI 6 or newer (Linux 6.12+) and the
537
+ `sigrun-sandbox` helper. Debian 12's default 6.1 kernel does not qualify;
538
+ bookworm-backports' 6.12 kernel does. Ubuntu 24.04's GA 6.8 kernel does not
539
+ qualify; its HWE 7.0 kernel does. Check the running host with
540
+ `sigrun-sandbox probe`. The container needs no extra capabilities: uid 1000,
541
+ no capabilities, and `RuntimeDefault` seccomp are sufficient.
542
+
543
+ **When isolation is on**, a user's agent can reach only their workspace,
544
+ their grants, their home and temp directories, and read-only system paths.
545
+ It cannot read sigrun's files or environment, other users' files, or signal
546
+ processes outside its jail.
547
+
548
+ The kernel boundary suite has not run yet; CI is its first execution.
549
+ Isolation must therefore not be treated as kernel-verified from the unit
550
+ suite alone.
551
+
552
+ **Not covered:** extensions run inside sigrun with full access, like any
553
+ installed package — install only extensions you trust. One user's agent can
554
+ still exhaust CPU, memory, or disk. Outbound network is not restricted.
555
+ macOS and Windows hosts run single-user only.
556
+
557
+ **Shared MCP servers** hand every permitted user's agent the admin's
558
+ credential for that service.
559
+
560
+ **File modes are not the boundary.** Workers that share sigrun's uid are
561
+ confined by Landlock alone; `0700` directories are hygiene.
562
+
563
+ **Single-user servers** run the agent with full host access.
564
+
565
+ **Every peer in `trustedProxies` can forge any identity**; with isolation
566
+ on, the shared secret is required for that reason. Configure
567
+ `server.auth.proxy.secretHeader` and the env-only `SIGRUN_PROXY_SECRET`.
568
+
569
+ For bare-metal OS-account mappings, enable `server.isolation.osUsers` and
570
+ set a directory record's `osUser`. Grant the installed helper only the
571
+ identity-switch capabilities once (replace `<helper>` with its path):
572
+
573
+ ```sh
574
+ sudo setcap cap_setuid,cap_setgid+ep <helper>
575
+ ```
576
+
577
+ The helper's boot self-test verifies the switch. `SIGRUN_SANDBOX_HELPER`
578
+ points sigrun at a helper binary at a non-default absolute path (it falls back
579
+ to the installed location with one warning if the path does not exist), which is
580
+ how a staged copy gets used.
581
+
582
+ File capabilities only take effect when the capabilities they grant are
583
+ already in the **caller's** bounding set: executing the capped helper needs
584
+ `CAP_SETUID` and `CAP_SETGID` present in the process that launches it —
585
+ `CAP_SETFCAP` alone only permits writing the xattr and is not enough. A root
586
+ shell, or a host that runs sigrun as root and maps users to real OS accounts,
587
+ normally satisfies this.
588
+
589
+ This mapping is bare-metal only. The container entrypoint starts as the image's
590
+ non-root `USER` and execs straight through (one uid for the whole container), so
591
+ it holds no identity-switch capabilities to inherit — containers use the
592
+ Landlock jail alone, which is where container multi-user isolation comes from.
593
+ A root container start (`SIGRUN_UID=0`) does exist, but it is a mount-point
594
+ ownership trampoline, not an `osUsers` enabler: it needs
595
+ `CAP_CHOWN`/`CAP_SETUID`/`CAP_SETGID` for the chown-and-drop and still leaves
596
+ you with a single container uid.
597
+
598
+ #### If you insist on `osUsers` in a container — and why you shouldn't
599
+
600
+ It is possible, and two of the three pieces need no image change. A capped
601
+ helper can be staged at runtime on any writable path (a named volume, a
602
+ `tmpfs`, a bind mount), and the caps come from the container's own flags:
603
+
604
+ ```yaml
605
+ # the runtime half — no derived image required
606
+ services:
607
+ sigrun:
608
+ user: "1000:1000"
609
+ cap_add: [SETUID, SETGID] # bounding set for the helper's file caps
610
+ volumes:
611
+ - sigrun-data:/data
612
+ - helper:/opt/helper # a writable path for the capped copy
613
+ # then, once per volume: copy the helper in, chmod 755, setcap
614
+ # cap_setuid,cap_setgid+ep under --cap-add SETFCAP, and point
615
+ # SIGRUN_SANDBOX_HELPER at it
616
+ security_opt:
617
+ - no-new-privileges:false # REQUIRED — see below
618
+ ```
619
+
620
+ The blocker is the third piece: **`osUser` maps to a login name**, resolved from
621
+ `/etc/passwd`, so every user you want needs an account. The image ships exactly
622
+ two non-root accounts (`node` and `nobody`). That is what forces either a
623
+ derived image (`RUN useradd …` per user) or mounting a `passwd` file over
624
+ `/etc/passwd` through a `subPath`, which is fragile and surprises tooling.
625
+
626
+ And the default posture actively prevents it, three ways: `read_only: true`
627
+ makes `setcap` on the image layer fail with "Read-only file system",
628
+ `cap_drop: ALL` grants nothing, and `no-new-privileges: true` suppresses the
629
+ file-capability gain on `execve` even when `SETUID`/`SETGID` are in the bounding
630
+ set (the helper dies at its first privileged call). Enabling this therefore
631
+ means adding capabilities **and** permitting privilege escalation — a
632
+ deliberate threat-model relaxation, not a configuration tweak.
633
+
634
+ The reason not to is structural rather than configurational: inside one
635
+ container every `osUser` shares **one filesystem and one mount namespace**. You
636
+ would gain per-process uids and Landlock's per-process confinement, but not the
637
+ kernel-level user separation a host provides — because there is only one
638
+ container boundary to separate. You would be paying a real security cost (an
639
+ identity-switch capability held by the main process, privilege escalation
640
+ permitted) for isolation the container boundary already substantially provides.
641
+ Use the Landlock jail alone in containers; use `osUsers` on bare metal, where
642
+ the accounts and the separation are real.
643
+
644
+ ### Kernel integration runner controls
645
+
646
+ The opt-in suite is `SIGRUN_SANDBOX_IT=1 npx vitest run test/sandbox-it`.
647
+ The CI `sandbox-it` job requires Landlock ABI 6 before running it. Its
648
+ `/proc` and ptrace cases also perform the same operation as the same uid
649
+ from outside any jail. The run log states, per case, either **Landlock
650
+ discriminating** (the outside operation succeeds) or **Yama/host-policy
651
+ satisfied, not Landlock-discriminating** (the outside operation is denied
652
+ too). The latter still checks the denial but does not claim Landlock caused
653
+ it. The abstract-socket case requires a successful outside-jail connection.
654
+ The `pkill -x node` case needs unprivileged user/PID namespaces and `nsenter`:
655
+ its outside control kills only PID 1 of a disposable namespace, never the
656
+ test runner or another host Node process. If namespaces are unavailable,
657
+ the suite skips with that named reason.
658
+
659
+ The `osUser` success case and the in-jail uid-switch discrimination require
660
+ a **privileged runner** with `CAP_SETFCAP` and
661
+ `SIGRUN_SANDBOX_IT_PRIVILEGED=1`. The suite grants only
662
+ `cap_setuid,cap_setgid+ep` to a temporary helper copy and verifies an
663
+ unjailed uid/gid switch before testing the jailed denial. Without that
664
+ successful outside control, both tests skip with a named reason; they never
665
+ claim to prove confinement. An ordinary CI runner without file-capability
666
+ permission therefore leaves these two cases unproven. The suite never runs
667
+ `sudo` itself.
668
+
669
+ The per-user-service process tests use a separate real-helper-launched
670
+ service fixture. They cover the sibling-jail `/proc` and ptrace boundary;
671
+ the per-user service API and lifecycle are documented above.
672
+
321
673
  ## Backup
322
674
 
323
675
  Set a target (`SIGRUN_BACKUP_PATH` or `SIGRUN_BACKUP_S3_BUCKET`+`SIGRUN_BACKUP_S3_PREFIX`,
@@ -509,18 +861,31 @@ wins, env bootstraps). The author-facing contract — the store taxonomy,
509
861
  the directory map, the slug rule, the `host.stores` handle, uninstall
510
862
  and backup posture — is [`docs/extending.md`](docs/extending.md).
511
863
 
864
+ **Building surfaces:** an extension can also contribute UI — views,
865
+ panels, settings sections, chat and interaction cards, ambient status —
866
+ as **block trees** rendered identically by the webui and the TUI. The
867
+ typed builders come from `@sigrun/sigrun/blocks`; the placement table,
868
+ block catalog, action kinds, access and `sharedData`, uploads, images,
869
+ and three complete worked examples (a lanes board, a settings section,
870
+ a dashboard) are in
871
+ [`docs/extending.md`](docs/extending.md#building-surfaces).
872
+
512
873
  ## Built-in extensions
513
874
 
514
- Three extensions are built into core — **codemode**, **MCP**, and
515
- **tool search** — mirroring what pi 1.3.0 ships built-in. They load by
516
- default on every boot, with no settings entry, and resolve from core's
517
- own `node_modules` (they are core dependencies, never installed into
518
- the packages dir — `extensions` has nothing to install for them, unless
519
- a same-named package is deliberately installed to replace one; see
520
- below).
875
+ Five extensions are built into core — **codemode**, **MCP**, **tool
876
+ search**, **gateway**, and **OAuth login**. The first three mirror
877
+ what pi 1.3.0 ships built-in; the gateway is sigrun's own channel-adapter
878
+ engine; OAuth provides OIDC, GitHub, and Discord login providers. They
879
+ load by default on every boot, with no settings entry, and resolve from
880
+ core's own `node_modules` (they are core dependencies, never installed
881
+ into the packages dir — `extensions` has nothing to install for them,
882
+ unless a same-named package is deliberately installed to replace one;
883
+ see below).
521
884
  A default install therefore has pi's tool surface without configuring
522
885
  anything. On `GET /v1/extensions` they report under the names
523
- `codemode`, `sigrun-mcp`, and `tool-search`.
886
+ `codemode`, `sigrun-mcp`, `tool-search`, `gateway`, and `oauth`.
887
+ Configure OAuth providers in `settings/oauth.json`; see the
888
+ `@sigrun/oauth` package README for provider settings and secrets.
524
889
 
525
890
  **The tools stay off until needed** (pi parity): `codemode` and
526
891
  `tool_search` are not in a new conversation's model-offered tool set by
@@ -534,10 +899,10 @@ default. They join the offered set when an enabled MCP server's
534
899
  automatic decision (auto-enable only narrows the default set, never a
535
900
  named list).
536
901
  - **Turning one off** — `"builtinExtensions": {"codemode": false,
537
- "mcp": false, "toolSearch": false}`. Each defaults to `true`; each is
538
- restart-required. A disabled built-in disappears from
539
- `GET /v1/extensions` entirely — no row, not even a skipped one — and
540
- its config is then ignored.
902
+ "mcp": false, "toolSearch": false, "gateway": false, "oauth": false}`.
903
+ Each defaults to `true`; each is restart-required. A disabled built-in
904
+ disappears from `GET /v1/extensions` entirely — no row, not even a
905
+ skipped one — and its config is then ignored.
541
906
  - **Replacing one** — installing a package that resolves to the same
542
907
  package name as a built-in replaces it: the installed copy loads, the
543
908
  built-in is skipped, boot logs one warning naming both, and
@@ -550,16 +915,23 @@ default. They join the offered set when an enabled MCP server's
550
915
  The MCP servers themselves live in the MCP config file — the
551
916
  `@sigrun/ext-mcp` package's README documents its format, including each
552
917
  server's `exposure` and the `SIGRUN_MCP_CONFIG` override.
918
+ **User-supplied MCP servers are remote-only in 0.2** (URL transport);
919
+ a user-supplied stdio server fails loudly with a config error. Dynamic
920
+ jailed per-user stdio services are a post-release feature — per-user
921
+ services are declared statically by extensions (see
922
+ docs/extending.md).
553
923
 
554
924
  ```jsonc
555
925
  {
556
926
  "extensions": [ // configured extensions install + load as above
557
927
  "@acme/sigrun-ext-example@^0.1.0"
558
928
  ],
559
- "builtinExtensions": { // all three default to true; restart required
929
+ "builtinExtensions": { // all five default to true; restart required
560
930
  "codemode": true,
561
931
  "mcp": true,
562
- "toolSearch": false
932
+ "toolSearch": false,
933
+ "gateway": true,
934
+ "oauth": true
563
935
  }
564
936
  }
565
937
  ```
@@ -680,11 +1052,38 @@ In the web UI the Jobs panel (the clock icon in the header) lists jobs
680
1052
  with a “Run now” button; in the TUI `/jobs` prints the table (name,
681
1053
  next run, last outcome).
682
1054
 
1055
+ ## Restarting
1056
+
1057
+ Restart-only settings (the registry marks every key `live` or
1058
+ `restart` — a `restart` key's stored write applies at the next boot)
1059
+ surface as `pendingRestart` on `GET /v1/settings`, and the settings
1060
+ panel lists them with a Restart button for admins. The button calls:
1061
+
1062
+ - `POST /v1/admin/restart` — admin-only; runs the normal graceful
1063
+ shutdown (SSE drains, in-flight runs reach a durable boundary and
1064
+ resume after the restart — pi-durable's recovery) and exits 0, so the
1065
+ deployment's supervisor brings the process back. Both channels show
1066
+ "Restarting…" and reconnect when `/readyz` answers.
1067
+
1068
+ The route exists ONLY when the server knows something restarts it:
1069
+ `server.supervised` (env `SIGRUN_SUPERVISED=1`, strict true/false)
1070
+ must be true, else 409 `not_supervised`. The Helm chart and
1071
+ `docker/docker-compose.yaml` set the env (both restart the container);
1072
+ the Dockerfile deliberately does NOT — a bare `docker run` without a
1073
+ restart policy would exit and stay down. `sigrun run` (the local TUI
1074
+ launcher) is not supervised either; there the callout says "Restart
1075
+ sigrun to apply" with no button. `GET /v1/meta` reports the posture
1076
+ (`supervised`).
1077
+
683
1078
  ## API sketch
684
1079
 
685
1080
  - `GET /v1/meta` — version, extensions, health, auth posture
686
1081
  (`methods`, `claimed`), capabilities.
687
- - `GET`/`PATCH /v1/settings` — the settings file (see above).
1082
+ - `GET`/`PATCH /v1/settings` — the settings file (see above);
1083
+ responses carry `pendingRestart` (restart keys whose stored value
1084
+ differs from boot) and PATCH reports `{applied, pending}`.
1085
+ - `POST /v1/admin/restart` — graceful restart (admin, supervised
1086
+ deploys only; see Restarting).
688
1087
  - `GET /v1/jobs`, `POST /v1/jobs`, `PATCH`/`DELETE /v1/jobs/{id}` —
689
1088
  the jobs surface (see [Jobs](#jobs)); `POST /v1/jobs/{id}/run` fires
690
1089
  now; `GET /v1/jobs/{id}/runs` and `GET /v1/jobs/{id}/revisions` are
@@ -697,9 +1096,13 @@ next run, last outcome).
697
1096
  - `GET /v1/onboarding`, `POST /v1/onboarding/claim` — the unclaimed
698
1097
  state and the first-user claim.
699
1098
  - `POST /v1/auth/login`, `POST /v1/auth/logout`, `GET /v1/auth/whoami` —
700
- the browser session and the resolved principal.
1099
+ token sign-in, logout, and the resolved principal.
1100
+ - `GET /v1/auth/providers`, `GET /v1/auth/login/{provider}`,
1101
+ `GET`/`POST /v1/auth/callback/{provider}` — external sign-in.
701
1102
  - `GET`/`POST /v1/auth/tokens`, `DELETE /v1/auth/tokens/{id}` —
702
- self-service API tokens.
1103
+ self-service API tokens and browser sessions.
1104
+ - `GET /v1/auth/accounts`, `DELETE /v1/auth/accounts` — self-service
1105
+ external-account links (administrators may manage another user's).
703
1106
  - `GET /v1/users`, `PATCH`/`DELETE /v1/users/{user}`,
704
1107
  `POST /v1/users/invites` — the user directory and invite codes
705
1108
  (admin-gated).
@@ -0,0 +1,13 @@
1
+ {
2
+ "algorithm": "sha256",
3
+ "sources": {
4
+ "shared/src/blocks/index.ts": "ca37dbb56a4a1a05a40a0125a06d864b9a0671c70817c83a44dd0e3962999e30",
5
+ "src/blocks/builders.ts": "a0c9835aaeab871bc2548d4b8d4426849c60cdb0cb1b20142d3bbe11e8377005",
6
+ "src/blocks/images.ts": "2e1bd54546c9c1c5bf0cfe9b488aaffd7c37979ef84bae9789168275e63e202f",
7
+ "src/blocks/index.ts": "8878a2e2269572ebdecca2cf3b95fa67f144232d8a8fe4258b120948064d0520",
8
+ "src/blocks/limits.ts": "1e913f29167114e26bed6f8c1fd04c64b119ee1db3f4e9e2ae161ddf70578683",
9
+ "src/blocks/placements.ts": "fccda42669713d91dc78d025268ee127f926891cf383727c1e235a39b9f15c6f",
10
+ "src/blocks/schema.ts": "27dbc5cdf1152a16a75bfb1a70383f1a456999a41d393308c05079b41f087a83",
11
+ "src/blocks/validate.ts": "31b9fc78f569cceff229bd1f28bfc62e2f8da437684100e051104f059a1ca19f"
12
+ }
13
+ }