@sigrun/sigrun 0.1.5 → 0.2.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.
Files changed (128) hide show
  1. package/CHANGELOG.md +136 -0
  2. package/README.md +352 -36
  3. package/THIRD_PARTY_NOTICES.md +23 -0
  4. package/dist/blocks/schema.js +9 -2
  5. package/dist/blocks/validate.js +57 -8
  6. package/dist/cli.js +26 -2
  7. package/dist/config.js +67 -1
  8. package/dist/extension.d.ts +189 -15
  9. package/dist/harness/agent-env.js +62 -0
  10. package/dist/harness/ambient.js +39 -185
  11. package/dist/harness/blocks/entries.js +47 -0
  12. package/dist/harness/blocks/state.js +58 -0
  13. package/dist/harness/blocks/tools.js +274 -0
  14. package/dist/harness/boot.js +253 -20
  15. package/dist/harness/builtins.js +15 -11
  16. package/dist/harness/event-bus.js +105 -10
  17. package/dist/harness/identity/accounts.js +186 -0
  18. package/dist/harness/identity/groups.js +14 -0
  19. package/dist/harness/identity/index.js +13 -6
  20. package/dist/harness/identity/json-store.js +15 -4
  21. package/dist/harness/identity/setup-code-check.js +9 -0
  22. package/dist/harness/identity/tokens.js +28 -5
  23. package/dist/harness/identity/users.js +111 -12
  24. package/dist/harness/interactions.js +292 -136
  25. package/dist/harness/loader.js +271 -17
  26. package/dist/harness/packages.js +1 -1
  27. package/dist/harness/reserved-sections.js +21 -0
  28. package/dist/harness/seam-host.js +80 -6
  29. package/dist/harness/settings.js +63 -2
  30. package/dist/harness/stores.js +241 -4
  31. package/dist/harness/user-services.js +391 -0
  32. package/dist/harness/user-tools.js +187 -0
  33. package/dist/harness/workspace.js +61 -55
  34. package/dist/jobs/fire.js +10 -16
  35. package/dist/sandbox/availability.js +162 -0
  36. package/dist/sandbox/command-observer.js +59 -0
  37. package/dist/sandbox/grants.js +159 -0
  38. package/dist/sandbox/helper.js +29 -0
  39. package/dist/sandbox/isolation.js +29 -0
  40. package/dist/sandbox/launch.js +20 -0
  41. package/dist/sandbox/os-account.js +49 -0
  42. package/dist/sandbox/policy.js +253 -0
  43. package/dist/sandbox/pool.js +548 -0
  44. package/dist/sandbox/protocol.js +167 -0
  45. package/dist/sandbox/sandboxed-env.js +245 -0
  46. package/dist/sandbox/unavailable-env.js +49 -0
  47. package/dist/sandbox/worker.js +281 -0
  48. package/dist/serve.js +82 -7
  49. package/dist/server/auth/access-principal.js +16 -0
  50. package/dist/server/auth/pipeline.js +47 -33
  51. package/dist/server/blocks/answers.js +94 -0
  52. package/dist/server/blocks/chat-surfaces.js +393 -0
  53. package/dist/server/blocks/notes.js +216 -0
  54. package/dist/server/blocks/summaries.js +328 -0
  55. package/dist/server/commands.js +5 -0
  56. package/dist/server/content-parts-lazy.js +13 -0
  57. package/dist/server/conversation-handler.js +2 -5
  58. package/dist/server/conversation-ops.js +138 -37
  59. package/dist/server/helpers.js +18 -6
  60. package/dist/server/host-bridge.js +40 -20
  61. package/dist/server/images/cache.js +205 -0
  62. package/dist/server/images/convert.js +13 -0
  63. package/dist/server/images/fetch.js +302 -0
  64. package/dist/server/images/ranges.js +42 -0
  65. package/dist/server/images/rewrite.js +94 -0
  66. package/dist/server/images/sign.js +84 -0
  67. package/dist/server/images/sniff.js +62 -0
  68. package/dist/server/index.js +144 -4
  69. package/dist/server/login/flows.js +126 -0
  70. package/dist/server/login/http.js +31 -0
  71. package/dist/server/login/resolve.js +133 -0
  72. package/dist/server/login/setup-claim.js +55 -0
  73. package/dist/server/login/usernames.js +15 -0
  74. package/dist/server/ownership.js +13 -0
  75. package/dist/server/routes/admin.js +58 -0
  76. package/dist/server/routes/auth.js +88 -2
  77. package/dist/server/routes/conversations.js +3 -6
  78. package/dist/server/routes/events.js +28 -3
  79. package/dist/server/routes/extensions.js +37 -5
  80. package/dist/server/routes/images.js +267 -0
  81. package/dist/server/routes/interactions.js +57 -68
  82. package/dist/server/routes/jobs.js +2 -8
  83. package/dist/server/routes/login.js +223 -0
  84. package/dist/server/routes/me.js +90 -0
  85. package/dist/server/routes/meta.js +15 -0
  86. package/dist/server/routes/onboarding.js +94 -84
  87. package/dist/server/routes/settings-sections.js +190 -0
  88. package/dist/server/routes/settings.js +16 -4
  89. package/dist/server/routes/submissions.js +1 -1
  90. package/dist/server/routes/surfaces.js +175 -23
  91. package/dist/server/routes/users.js +116 -20
  92. package/dist/server/schema-fixture.js +213 -0
  93. package/dist/server/session-meta.js +3 -0
  94. package/dist/server/shutdown.js +11 -4
  95. package/dist/server/static.js +18 -0
  96. package/dist/server/surfaces/dedup.js +9 -2
  97. package/dist/server/surfaces/service.js +181 -13
  98. package/dist/server/surfaces/settings-surfaces.js +597 -0
  99. package/dist/server/turns/submit-turn.js +26 -0
  100. package/dist/server/views.js +16 -0
  101. package/dist/settings/core-sections.js +532 -0
  102. package/dist/settings/pending.js +181 -0
  103. package/dist/settings/registry.js +453 -0
  104. package/dist/skills/index.js +6 -0
  105. package/node_modules/@sigrun/shared/CHANGELOG.md +15 -0
  106. package/node_modules/@sigrun/shared/dist/access.d.ts +4 -2
  107. package/node_modules/@sigrun/shared/dist/access.js +11 -4
  108. package/node_modules/@sigrun/shared/dist/ambient.d.ts +17 -74
  109. package/node_modules/@sigrun/shared/dist/ambient.js +8 -6
  110. package/node_modules/@sigrun/shared/dist/blocks/text.js +16 -2
  111. package/node_modules/@sigrun/shared/dist/blocks/transport.d.ts +4 -0
  112. package/node_modules/@sigrun/shared/dist/blocks/types.d.ts +21 -4
  113. package/node_modules/@sigrun/shared/dist/interactions.d.ts +22 -48
  114. package/node_modules/@sigrun/shared/dist/interactions.js +9 -7
  115. package/node_modules/@sigrun/shared/dist/settings.d.ts +27 -2
  116. package/node_modules/@sigrun/shared/dist/themes.d.ts +2 -3
  117. package/node_modules/@sigrun/shared/dist/themes.js +2 -3
  118. package/node_modules/@sigrun/shared/dist/turns.d.ts +14 -0
  119. package/node_modules/@sigrun/shared/dist/turns.js +53 -3
  120. package/node_modules/@sigrun/shared/dist/views.d.ts +175 -12
  121. package/node_modules/@sigrun/shared/dist/views.js +16 -0
  122. package/node_modules/@sigrun/shared/package.json +1 -1
  123. package/package.json +13 -8
  124. package/webui/dist/assets/index-CxB969qp.js +55 -0
  125. package/webui/dist/assets/index-yqkL6BSS.css +1 -0
  126. package/webui/dist/index.html +2 -2
  127. package/webui/dist/assets/index-B4-GYqEb.js +0 -50
  128. package/webui/dist/assets/index-BiSMljer.css +0 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,142 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.0] - 2026-10-27
11
+ ### Added
12
+ - Per-user extension surface (multi-user M2): lifecycle events carry the
13
+ conversation owner; `stores.user(user)` provides per-user secrets,
14
+ settings, and data/state/cache paths; `/v1/me` exposes self-service
15
+ settings, secret-key management, and service status.
16
+ - Per-user tool publications (`perUserTools` and
17
+ `host.tools.setUserTools`) with `<slug>@<user>` registry entries,
18
+ owner-driven selection on create/fork/transfer and boot reconciliation.
19
+ Every tool is wrapped with a committed-owner execution check: selection
20
+ is convenience, not the authorization boundary.
21
+ - Per-user service processes (`userServices` and `services.connect`),
22
+ launched under the user's own sibling jail policy with idle stopping,
23
+ crash fencing, and lifecycle cleanup on deletion, quarantine, and
24
+ shutdown. Settings sections can use `scope: "user"` for personal
25
+ configuration.
26
+ - The §10.6 deletion sequence: transfer ownership and remove external
27
+ identity access, stop per-user execution and tool entries, emit
28
+ `userDeleted` while credential revocation remains possible, remove the
29
+ record, then delete per-user credentials, settings, and cache. Data and
30
+ state remain extension-owned.
31
+ - Multi-user login (M3b): extension-contributed `loginProviders` with
32
+ core-owned, single-use state and callback routes; first-login setup and
33
+ invite resolution with optional policy-gated auto-provisioning; webui
34
+ provider sign-in; and self-service account links that are never silently
35
+ re-pointed to another user.
36
+ - Gateway built-in: `@sigrun/ext-gateway` supervises channel adapters,
37
+ with pairing and self-linking, group conversations, live responses,
38
+ durable outbound delivery, agent tools, home notifications, and job
39
+ delivery. Enabled by default; operators can disable it with
40
+ `builtinExtensions.gateway: false`.
41
+ - Expiring, revocable browser sessions (`server.auth.sessionDays`, default
42
+ 30 days). A session can mint the user's API tokens; revoking it leaves
43
+ those tokens independent.
44
+ - Stored groups per authenticating source, effective group-derived admin,
45
+ and people-scoped group sharing. Groups refresh at the next login or
46
+ validated proxy request. The separately shipped `@sigrun/oauth`
47
+ package provides OIDC, GitHub, and Discord login providers.
48
+ - `@sigrun/oauth` is the fourth built-in extension. It loads by default,
49
+ resolves from core's dependencies, and supports the same disable,
50
+ replacement, and quarantine behavior as the other built-ins. The
51
+ `builtinExtensions.oauth` toggle defaults to true and is restart-required.
52
+ - Chat blocks (U5): an entry and state family plus `show`/`update` tools,
53
+ surface routes with authored change records, change notes before every
54
+ turn admission, block answers as `InputEntry` turns, transcript
55
+ rendering, and the webui and TUI transports.
56
+ - Interactions and ambient surfaces on block trees (U6): `{values}` answers
57
+ with write-only secrets, channel renderers, and the ambient
58
+ literal-content ruling.
59
+ - Images (U7): an HMAC-signed proxy, a rebinding-safe fetcher, an LRU cache
60
+ with `sharp` conversion, and CSP headers.
61
+ - Core settings sections rebuilt on blocks (U4): one settings renderer for
62
+ webui and TUI, extension-declared sections, a `/v1/settings` sections
63
+ route, `pendingRestart` with an admin restart route, live/restart
64
+ metadata for every core key, label search, and
65
+ `settings.changed`/`onSettingsChanged`.
66
+
67
+ ### Changed
68
+ - Family versions move together to 0.2.0: `@sigrun/shared` 0.2.0
69
+ (surface, block, interaction, ambient, and settings-section wire types)
70
+ and `@sigrun/tui` 0.2.0 (chat-block transcripts, interaction and ambient
71
+ rendering, settings forms).
72
+ - Retired persisted `groupAdmin` in favor of stored groups; its user-wire
73
+ compatibility field is now derived and retained for one release.
74
+ - The execution jail (multi-user M1): per-user workers are Landlock-confined
75
+ and multi-user fails closed when the boundary is unavailable.
76
+
77
+ ### Fixed
78
+ - Kept the extension inventory available to non-admin clients while limiting
79
+ service status projections to admins; quarantine now removes per-user
80
+ tools as well as services.
81
+ - Preserved service crash fences across explicit stops and skipped the
82
+ per-user selection sweep when no per-user extensions are configured.
83
+
84
+ ### Security
85
+ - Added the opt-in Landlock kernel boundary suite for two-user execution
86
+ isolation, with a hard ABI 6+ CI gate and explicit local skip reasons.
87
+ Documented the isolation boundary, trusted-extension and resource limits,
88
+ kernel requirements, OS-account setup, and proxy trust posture.
89
+ The suite also covers abstract sockets, ptrace, worker lifecycle, IPC
90
+ descriptors, and privileged-runner osUser switching; outside-jail controls
91
+ distinguish Landlock denials from Yama or missing-capability conditions.
92
+
93
+ ## [0.1.7] - 2026-10-24
94
+ ### Fixed
95
+ - The deploy entry binds the dual-stack wildcard (`::`) instead of the
96
+ IPv4-only `0.0.0.0`. On a dual-stack pod whose Service is IPv6-only
97
+ (`ipFamilies: ["IPv6"]` — the default shape on a cluster with an
98
+ `fd00::/8` ClusterIP range) the EndpointSlice advertises only the
99
+ pod's IPv6 address; the gateway dialed it, got connection-refused,
100
+ and answered `upstream connect error or disconnect/reset before
101
+ headers. reset reason: remote connection failure` while the
102
+ IPv4-loopback probes kept the pod Ready. `::` accepts both families
103
+ (`net.ipv6.bindv6only` is 0 on Linux), matching every other workload
104
+ in a dual-stack cluster. The reported listen line brackets the host
105
+ (`http://[::]:3000`).
106
+
107
+ ### Changed
108
+ - The chart's main Service is `PreferDualStack` over `[IPv6, IPv4]`,
109
+ so it advertises both pod addresses regardless of the cluster's
110
+ default family. Converting an existing `SingleStack` Service is a
111
+ recreate (`spec.clusterIPs` is immutable).
112
+ - `sigrun run` (workstation mode) still binds `127.0.0.1` only.
113
+
114
+ ## [0.1.6] - 2026-10-24
115
+ ### Added
116
+ - Extension surfaces: typed block rendering, actions and uploads, live
117
+ transports, per-principal access control, and web/TUI surface views.
118
+ - Web and TUI block renderers: content, charts, collections, forms,
119
+ navigation, live updates, and extension-provided views.
120
+
121
+ ### Fixed
122
+ - The container image preserves the core, webui, shared, and TUI build
123
+ output paths and compiles the TUI workspace. The published 0.1.5 image
124
+ flattened core `dist/` into `/app`, so `node dist/index.js` could not
125
+ start. CI now smoke-checks the built image's runtime layout.
126
+
127
+ ### Changed
128
+ - `@sigrun/shared` 0.1.2: surface and block-instance wire types.
129
+ - `@sigrun/tui` 0.1.1: extension surfaces and the full block renderer.
130
+ - Fastify 5.12.7 and `@fastify/static` 10.1.7 address the server's
131
+ header-validation and static-file path/authorization advisories; the
132
+ patched `fast-uri` transitive releases address its URI-normalization
133
+ advisories. `npm audit` reports zero vulnerabilities.
134
+
135
+ ### Security
136
+ - Agent shells no longer inherit sigrun's environment. pi-durable's bash
137
+ tool ran every command with the server's full environment, so an
138
+ agent's `env` printed `SIGRUN_API_TOKEN`, `SIGRUN_PROXY_SECRET` and
139
+ provider keys. Every command now gets an allowlist (`PATH`, `HOME`,
140
+ `TMPDIR`, `LANG`, `LC_*`, `TZ`, `TERM`, `USER`, `LOGNAME`, `SHELL`,
141
+ `NPM_CONFIG_CACHE`, `SSH_AUTH_SOCK`) plus whatever the calling tool
142
+ sets. `NODE_ENV=production` from the image no longer reaches agent
143
+ shells, so an agent's `npm install` installs devDependencies again.
144
+ The bundled MCP extension's stdio servers get the same allowlist.
145
+
10
146
  ## [0.1.5] - 2026-10-23
11
147
  ### Added
12
148
  - The image + chart release pipeline: multi-arch (amd64/arm64) container
package/README.md CHANGED
@@ -104,7 +104,7 @@ entrypoints:
104
104
  | Command | Behavior |
105
105
  |---|---|
106
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. |
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 (`::`, 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
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
109
 
110
110
  Resolution chains for `attach` (and for the workstation-mode user):
@@ -265,8 +265,10 @@ list explicitly — explicit always wins, including `[]` on a non-
265
265
  loopback bind, which boots with a warning that any reachable client is
266
266
  trusted. The related env knobs: `SIGRUN_AUTH_TRUSTED_PROXIES` (CIDR
267
267
  csv), `SIGRUN_PROXY_SECRET` and `SIGRUN_API_TOKEN` (credentials —
268
- env-only, never in `settings.json`). Every auth setting is
269
- restart-required.
268
+ env-only, never in `settings.json`). Every auth setting except
269
+ `server.auth.sessionDays` is restart-required. `sessionDays` applies live to
270
+ new sessions and has no core settings-section field; change it in the
271
+ settings file or with `PATCH /v1/settings`.
270
272
 
271
273
  **Tokens.** API tokens are `sgr_`-prefixed secrets shown once at
272
274
  creation (the store keeps only SHA-256 digests) and are self-service:
@@ -275,7 +277,8 @@ an authenticated user lists and mints their own via
275
277
  admins may revoke anyone's). A token whose user was deleted is dead.
276
278
  A browser exchanges a token for a `sigrun_session` cookie at
277
279
  `POST /v1/auth/login` — invalid attempts are delayed and rate-limited —
278
- and `GET /v1/auth/whoami` returns the resolved principal.
280
+ and `GET /v1/auth/whoami` returns the resolved principal. An IdP-signed-in
281
+ user can mint their own API token for the TUI through the same token routes.
279
282
 
280
283
  **Proxy method (forward auth).** A trusted reverse proxy vouches for
281
284
  the caller through headers (Authelia's `Remote-*` defaults,
@@ -286,19 +289,166 @@ configurable) when the request comes from a peer inside
286
289
  proxy and nothing else: every peer in `trustedProxies` can forge any
287
290
  identity, including an admin.
288
291
 
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.
292
+ **Stored groups and group-derived admin.** `server.auth.adminGroups`
293
+ lists groups (Authelia's `Remote-Groups`, for example) whose members act as
294
+ admins. Core stores groups per authenticating source: `proxy` for validated
295
+ forward-auth headers and `login:<slug>:<id>` for an external login. Effective
296
+ admin is the stored `admin` flag OR any stored group in `adminGroups`, so
297
+ web, chat, and host-side checks agree. `/v1/users` keeps `admin` as the
298
+ stored flag the users-admin UI toggles; its `groupAdmin` compatibility field
299
+ is derived, never persisted, and is retained for one release. Group-derived
300
+ admin never counts toward the last-admin guard — demoting or deleting the
301
+ only stored admin is still refused.
302
+
303
+ Login-provider groups participate in `server.auth.adminGroups`, so configure
304
+ only providers whose group claims are administrator-controlled. A linked
305
+ account's groups come from the provider at each sign-in; removing admin
306
+ therefore requires changing the provider's claim or removing the link/user.
307
+
308
+ Groups refresh only at that user's next login or proxy request. This
309
+ staleness is deliberate: a group removal at the IdP does not take effect
310
+ until then. Token and session requests only read the stored record; they
311
+ never accept groups from the request. People-scoped conversation shares may
312
+ also name groups, matched against the same stored groups.
313
+
314
+ ### External sign-in (OIDC and OAuth 2)
315
+
316
+ The web sign-in screen lists configured external providers above the token
317
+ form when the effective auth methods include `token`; `/v1/auth/providers`
318
+ returns an empty list on proxy-only deployments because their completed
319
+ sign-ins could not authenticate the resulting session. The TUI continues to
320
+ use tokens. Login providers come from extensions.
321
+ The reference implementation is **`@sigrun/oauth`, the fourth built-in
322
+ extension**. It is bundled and loads by default, so no `extensions` entry is
323
+ needed. Set `"builtinExtensions": {"oauth": false}` to disable it; like the
324
+ other built-in toggles, this is restart-required. A configured extension
325
+ whose package name is also `@sigrun/oauth` replaces the built-in. If the
326
+ built-in cannot be resolved or loaded, it is quarantined without aborting
327
+ boot. It remains separately installable through the normal `extensions`
328
+ list (see [Extensions](#extensions)) for a deployment that wants it without
329
+ the built-in. It is inert when its settings contain no valid providers.
330
+
331
+ Create an OAuth/OIDC client at the identity service, with the redirect URI
332
+ shown below. Configure providers in the extension's
333
+ `<config-dir>/settings/oauth.json`; an administrator can edit that JSON in
334
+ Settings → Extensions → oauth. Set `server.publicUrl` (or
335
+ `SIGRUN_PUBLIC_URL`) to sigrun's externally reachable origin so callback
336
+ URLs are stable; otherwise core derives one from the request.
337
+
338
+ For a generic OIDC service — Authelia, Authentik, and Keycloak are examples
339
+ of products this form fits, not service-specific presets — configure its
340
+ issuer and client:
341
+
342
+ ```json
343
+ {
344
+ "providers": [
345
+ {
346
+ "id": "company",
347
+ "label": "Company SSO",
348
+ "kind": "oidc",
349
+ "issuer": "https://idp.example.invalid",
350
+ "clientId": "sigrun",
351
+ "scopes": "openid profile email",
352
+ "usernameClaim": "preferred_username",
353
+ "groupsClaim": "groups",
354
+ "autoProvision": false,
355
+ "allowedGroups": ["staff"],
356
+ "allowedDomains": ["example.invalid"]
357
+ }
358
+ ]
359
+ }
360
+ ```
361
+
362
+ The extension uses OIDC discovery, authorization code with PKCE S256 and a
363
+ nonce, and validates the ID token. `scopes`, `usernameClaim`, and
364
+ `groupsClaim` shown above are the defaults. For this example, register
365
+ `https://sigrun.example.invalid/v1/auth/callback/login:oauth:company` as
366
+ the redirect URI (the provider id is qualified, not merely `company`).
367
+ Production identity endpoints must use HTTPS; plain HTTP is restricted to
368
+ loopback development and tests.
369
+
370
+ Every provider needs a client secret named `<id>.clientSecret` in the
371
+ **oauth extension secret store** — `company.clientSecret` here. Set it in
372
+ Settings → Extensions → oauth (write-only), or as an administrator with
373
+ `PUT /v1/extensions/oauth/secrets/company.clientSecret` and body
374
+ `{"value":"<client-secret>"}`. Never put a client secret in
375
+ `settings/oauth.json`. Extension settings are read at boot; restart after
376
+ changing them. Secrets are re-read per use, so a rotated client secret
377
+ applies to the next login without a restart.
378
+
379
+ The GitHub and Discord presets replace issuer/endpoint discovery with their
380
+ protocol endpoints and user field mappings:
381
+
382
+ ```json
383
+ {
384
+ "providers": [
385
+ {
386
+ "id": "github",
387
+ "label": "GitHub",
388
+ "kind": "oauth2",
389
+ "preset": "github",
390
+ "clientId": "acme-github-client-id"
391
+ },
392
+ {
393
+ "id": "discord",
394
+ "label": "Discord",
395
+ "kind": "oauth2",
396
+ "preset": "discord",
397
+ "clientId": "acme-discord-client-id"
398
+ }
399
+ ]
400
+ }
401
+ ```
402
+
403
+ **Both presets require `clientId` in settings and a client secret in the
404
+ extension secret store** (`github.clientSecret` and
405
+ `discord.clientSecret`). Their redirect URIs end in
406
+ `/v1/auth/callback/login:oauth:github` and
407
+ `/v1/auth/callback/login:oauth:discord` on the same sigrun origin. GitHub
408
+ uses only a primary, verified email; Discord uses its verified flag.
409
+ Neither preset supplies groups. A malformed provider entry is skipped with
410
+ a warning naming it; other valid entries still load.
411
+
412
+ For another OAuth 2 service, use `kind: "oauth2"` with explicit
413
+ `authorizeUrl`, `tokenUrl`, and `userinfoUrl`, plus `fields` mapping a
414
+ required `subject` and optional `username`, `name`, `email`,
415
+ `emailVerified`, and `groups` dot-separated response paths. `clientId`, its
416
+ `<id>.clientSecret` secret, and the same optional policy fields apply.
417
+
418
+ **First login never auto-links by username or email.** An unlinked subject
419
+ on an unclaimed server needs the one-shot setup code and becomes the first
420
+ admin. On a claimed server, `autoProvision: false` (the default) requires an
421
+ invite code; `true` creates a non-admin user, optionally restricted by
422
+ `allowedGroups` and `allowedDomains`. **These filters apply only when
423
+ creating a new user by auto-provisioning** — not to an already-linked
424
+ account's logins or to invite-based provisioning. A linked account keeps
425
+ working; remove its link (or the user) to remove access. A domain match
426
+ additionally requires `emailVerified === true` from the provider. Core
427
+ enforces the M1 admission prerequisites: adding a second user is refused
428
+ without available execution isolation and real `token` and/or `proxy`
429
+ authentication. An external IdP is not itself a trusted proxy.
430
+
431
+ ### Browser sessions and account links
432
+
433
+ A successful external sign-in creates a `sigrun_session` cookie backed by a
434
+ revocable session record in the token store. `server.auth.sessionDays`
435
+ controls its lifetime (default **30 days**, live for newly created
436
+ sessions). An expired session is rejected as unknown (401) and its record
437
+ is pruned on the next token-store write. Logout revokes the current
438
+ session; users can also see and revoke sessions in their token list.
439
+
440
+ A session has the user's normal rights, including creating that user's own
441
+ API tokens for the TUI. **Revoking a session does not revoke API tokens
442
+ created with it**; revoke those separately. Core does not store provider
443
+ access or refresh tokens.
444
+
445
+ Settings → Account links is the self-service panel for external identities.
446
+ Connect starts the provider flow with `?link=1` while signed in; the
447
+ identity is linked to the caller instead of creating another sign-in.
448
+ Users can remove their own links and administrators can manage others'
449
+ links. One `(provider, subject)` pair belongs to one user: a link is never
450
+ re-pointed to a different user. The holder or an administrator must remove
451
+ a conflicting old link first.
302
452
 
303
453
  **Onboarding (the first claim).** An unclaimed server serves
304
454
  `GET /v1/onboarding` + `POST /v1/onboarding/claim`. On a loopback bind
@@ -313,11 +463,131 @@ users (`PATCH`/`DELETE /v1/users/{user}` — deletion transfers their
313
463
  conversations).
314
464
 
315
465
  **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`
466
+ owner can share one — household-wide or with named users and groups,
467
+ `read` or `write` — through the web UI's ⋯ menu; `write`
318
468
  lets a recipient submit, answer, and fork, but only the owner can
319
469
  re-share. Household sharing defaults to `read`.
320
470
 
471
+ ### Per-user extensions and deletion
472
+
473
+ Extensions can offer personal tools, settings, credentials, and services
474
+ instead of only server-wide resources. Each conversation selects its
475
+ owner's per-user tools, and core also checks the committed owner every
476
+ time one executes: selecting another user's entry does not permit its
477
+ use. Per-user services run as that user in their own service jail,
478
+ sibling to the agent's worker. A credential supplied to a service never
479
+ belongs in the agent workspace. See
480
+ [`docs/extending.md`](docs/extending.md#per-user-stores) for the extension
481
+ contract and `/v1/me` self-service routes.
482
+
483
+ A user's extension secrets live in
484
+ `<configDir>/user-secrets/<user>.json`, mode `0600`, with one section
485
+ per extension slug. Their settings live under
486
+ `<configDir>/user-settings/<user>/`; extension data, state, and cache
487
+ have separate per-user paths. Login, sessions, and account linking are
488
+ documented in [External sign-in](#external-sign-in-oidc-and-oauth-2) and
489
+ [Browser sessions and account links](#browser-sessions-and-account-links).
490
+
491
+ `DELETE /v1/users/{user}?transferTo={recipient}` requires an admin and
492
+ another directory user as recipient. Core transfers conversations and
493
+ owned resources, then revokes the departing user's tokens and account
494
+ links, stops their worker and services, and removes their per-user tool
495
+ entries. Extensions then receive `userDeleted` while the old record and
496
+ secrets remain readable, so they can revoke external provider
497
+ credentials. Finally core removes the directory record, per-user secrets,
498
+ user-settings directory, and per-user cache directories.
499
+
500
+ **Credentials belong to a person and never transfer.** Data and state
501
+ stay for the extension to retain, move, or delete according to its own
502
+ rules; core does not make that choice.
503
+
504
+ ## Multi-user execution isolation
505
+
506
+ `server.isolation.mode` (or `SIGRUN_ISOLATION`) controls the execution jail:
507
+
508
+ | Mode | Behavior |
509
+ |---|---|
510
+ | `auto` (default) | Turns isolation on when the directory has more than one user. Once active, it stays active until restart. |
511
+ | `always` | Confines every user, including a lone admin. |
512
+ | `off` | No jail; permitted only with at most one directory user. |
513
+
514
+ A second user is refused when isolation is unavailable or `off`, and a
515
+ multi-user server never falls back to unconfined execution. Multi-user also
516
+ requires real authentication (`token` and/or `proxy`).
517
+
518
+ Isolation requires Linux with Landlock ABI 6 or newer (Linux 6.12+) and the
519
+ `sigrun-sandbox` helper. Debian 12's default 6.1 kernel does not qualify;
520
+ bookworm-backports' 6.12 kernel does. Ubuntu 24.04's GA 6.8 kernel does not
521
+ qualify; its HWE 7.0 kernel does. Check the running host with
522
+ `sigrun-sandbox probe`. The container needs no extra capabilities: uid 1000,
523
+ no capabilities, and `RuntimeDefault` seccomp are sufficient.
524
+
525
+ **When isolation is on**, a user's agent can reach only their workspace,
526
+ their grants, their home and temp directories, and read-only system paths.
527
+ It cannot read sigrun's files or environment, other users' files, or signal
528
+ processes outside its jail.
529
+
530
+ The kernel boundary suite has not run yet; CI is its first execution.
531
+ Isolation must therefore not be treated as kernel-verified from the unit
532
+ suite alone.
533
+
534
+ **Not covered:** extensions run inside sigrun with full access, like any
535
+ installed package — install only extensions you trust. One user's agent can
536
+ still exhaust CPU, memory, or disk. Outbound network is not restricted.
537
+ macOS and Windows hosts run single-user only.
538
+
539
+ **Shared MCP servers** hand every permitted user's agent the admin's
540
+ credential for that service.
541
+
542
+ **File modes are not the boundary.** Workers that share sigrun's uid are
543
+ confined by Landlock alone; `0700` directories are hygiene.
544
+
545
+ **Single-user servers** run the agent with full host access.
546
+
547
+ **Every peer in `trustedProxies` can forge any identity**; with isolation
548
+ on, the shared secret is required for that reason. Configure
549
+ `server.auth.proxy.secretHeader` and the env-only `SIGRUN_PROXY_SECRET`.
550
+
551
+ For bare-metal OS-account mappings, enable `server.isolation.osUsers` and
552
+ set a directory record's `osUser`. Grant the installed helper only the
553
+ identity-switch capabilities once (replace `<helper>` with its path):
554
+
555
+ ```sh
556
+ sudo setcap cap_setuid,cap_setgid+ep <helper>
557
+ ```
558
+
559
+ The helper's boot self-test verifies the switch. This mapping is unavailable
560
+ in containers, whose entrypoint drops every capability.
561
+
562
+ ### Kernel integration runner controls
563
+
564
+ The opt-in suite is `SIGRUN_SANDBOX_IT=1 npx vitest run test/sandbox-it`.
565
+ The CI `sandbox-it` job requires Landlock ABI 6 before running it. Its
566
+ `/proc` and ptrace cases also perform the same operation as the same uid
567
+ from outside any jail. The run log states, per case, either **Landlock
568
+ discriminating** (the outside operation succeeds) or **Yama/host-policy
569
+ satisfied, not Landlock-discriminating** (the outside operation is denied
570
+ too). The latter still checks the denial but does not claim Landlock caused
571
+ it. The abstract-socket case requires a successful outside-jail connection.
572
+ The `pkill -x node` case needs unprivileged user/PID namespaces and `nsenter`:
573
+ its outside control kills only PID 1 of a disposable namespace, never the
574
+ test runner or another host Node process. If namespaces are unavailable,
575
+ the suite skips with that named reason.
576
+
577
+ The `osUser` success case and the in-jail uid-switch discrimination require
578
+ a **privileged runner** with `CAP_SETFCAP` and
579
+ `SIGRUN_SANDBOX_IT_PRIVILEGED=1`. The suite grants only
580
+ `cap_setuid,cap_setgid+ep` to a temporary helper copy and verifies an
581
+ unjailed uid/gid switch before testing the jailed denial. Without that
582
+ successful outside control, both tests skip with a named reason; they never
583
+ claim to prove confinement. An ordinary CI runner without file-capability
584
+ permission therefore leaves these two cases unproven. The suite never runs
585
+ `sudo` itself.
586
+
587
+ The per-user-service process tests use a separate real-helper-launched
588
+ service fixture. They cover the sibling-jail `/proc` and ptrace boundary;
589
+ the per-user service API and lifecycle are documented above.
590
+
321
591
  ## Backup
322
592
 
323
593
  Set a target (`SIGRUN_BACKUP_PATH` or `SIGRUN_BACKUP_S3_BUCKET`+`SIGRUN_BACKUP_S3_PREFIX`,
@@ -509,18 +779,31 @@ wins, env bootstraps). The author-facing contract — the store taxonomy,
509
779
  the directory map, the slug rule, the `host.stores` handle, uninstall
510
780
  and backup posture — is [`docs/extending.md`](docs/extending.md).
511
781
 
782
+ **Building surfaces:** an extension can also contribute UI — views,
783
+ panels, settings sections, chat and interaction cards, ambient status —
784
+ as **block trees** rendered identically by the webui and the TUI. The
785
+ typed builders come from `@sigrun/sigrun/blocks`; the placement table,
786
+ block catalog, action kinds, access and `sharedData`, uploads, images,
787
+ and three complete worked examples (a lanes board, a settings section,
788
+ a dashboard) are in
789
+ [`docs/extending.md`](docs/extending.md#building-surfaces).
790
+
512
791
  ## Built-in extensions
513
792
 
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).
793
+ Four extensions are built into core — **codemode**, **MCP**, **tool
794
+ search**, **gateway**, and **OAuth login**. The first three mirror
795
+ what pi 1.3.0 ships built-in; the gateway is sigrun's own channel-adapter
796
+ engine; OAuth provides OIDC, GitHub, and Discord login providers. They
797
+ load by default on every boot, with no settings entry, and resolve from
798
+ core's own `node_modules` (they are core dependencies, never installed
799
+ into the packages dir — `extensions` has nothing to install for them,
800
+ unless a same-named package is deliberately installed to replace one;
801
+ see below).
521
802
  A default install therefore has pi's tool surface without configuring
522
803
  anything. On `GET /v1/extensions` they report under the names
523
- `codemode`, `sigrun-mcp`, and `tool-search`.
804
+ `codemode`, `sigrun-mcp`, `tool-search`, `gateway`, and `oauth`.
805
+ Configure OAuth providers in `settings/oauth.json`; see the
806
+ `@sigrun/oauth` package README for provider settings and secrets.
524
807
 
525
808
  **The tools stay off until needed** (pi parity): `codemode` and
526
809
  `tool_search` are not in a new conversation's model-offered tool set by
@@ -534,10 +817,10 @@ default. They join the offered set when an enabled MCP server's
534
817
  automatic decision (auto-enable only narrows the default set, never a
535
818
  named list).
536
819
  - **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.
820
+ "mcp": false, "toolSearch": false, "gateway": false, "oauth": false}`.
821
+ Each defaults to `true`; each is restart-required. A disabled built-in
822
+ disappears from `GET /v1/extensions` entirely — no row, not even a
823
+ skipped one — and its config is then ignored.
541
824
  - **Replacing one** — installing a package that resolves to the same
542
825
  package name as a built-in replaces it: the installed copy loads, the
543
826
  built-in is skipped, boot logs one warning naming both, and
@@ -556,10 +839,12 @@ server's `exposure` and the `SIGRUN_MCP_CONFIG` override.
556
839
  "extensions": [ // configured extensions install + load as above
557
840
  "@acme/sigrun-ext-example@^0.1.0"
558
841
  ],
559
- "builtinExtensions": { // all three default to true; restart required
842
+ "builtinExtensions": { // all five default to true; restart required
560
843
  "codemode": true,
561
844
  "mcp": true,
562
- "toolSearch": false
845
+ "toolSearch": false,
846
+ "gateway": true,
847
+ "oauth": true
563
848
  }
564
849
  }
565
850
  ```
@@ -680,11 +965,38 @@ In the web UI the Jobs panel (the clock icon in the header) lists jobs
680
965
  with a “Run now” button; in the TUI `/jobs` prints the table (name,
681
966
  next run, last outcome).
682
967
 
968
+ ## Restarting
969
+
970
+ Restart-only settings (the registry marks every key `live` or
971
+ `restart` — a `restart` key's stored write applies at the next boot)
972
+ surface as `pendingRestart` on `GET /v1/settings`, and the settings
973
+ panel lists them with a Restart button for admins. The button calls:
974
+
975
+ - `POST /v1/admin/restart` — admin-only; runs the normal graceful
976
+ shutdown (SSE drains, in-flight runs reach a durable boundary and
977
+ resume after the restart — pi-durable's recovery) and exits 0, so the
978
+ deployment's supervisor brings the process back. Both channels show
979
+ "Restarting…" and reconnect when `/readyz` answers.
980
+
981
+ The route exists ONLY when the server knows something restarts it:
982
+ `server.supervised` (env `SIGRUN_SUPERVISED=1`, strict true/false)
983
+ must be true, else 409 `not_supervised`. The Helm chart and
984
+ `docker/docker-compose.yaml` set the env (both restart the container);
985
+ the Dockerfile deliberately does NOT — a bare `docker run` without a
986
+ restart policy would exit and stay down. `sigrun run` (the local TUI
987
+ launcher) is not supervised either; there the callout says "Restart
988
+ sigrun to apply" with no button. `GET /v1/meta` reports the posture
989
+ (`supervised`).
990
+
683
991
  ## API sketch
684
992
 
685
993
  - `GET /v1/meta` — version, extensions, health, auth posture
686
994
  (`methods`, `claimed`), capabilities.
687
- - `GET`/`PATCH /v1/settings` — the settings file (see above).
995
+ - `GET`/`PATCH /v1/settings` — the settings file (see above);
996
+ responses carry `pendingRestart` (restart keys whose stored value
997
+ differs from boot) and PATCH reports `{applied, pending}`.
998
+ - `POST /v1/admin/restart` — graceful restart (admin, supervised
999
+ deploys only; see Restarting).
688
1000
  - `GET /v1/jobs`, `POST /v1/jobs`, `PATCH`/`DELETE /v1/jobs/{id}` —
689
1001
  the jobs surface (see [Jobs](#jobs)); `POST /v1/jobs/{id}/run` fires
690
1002
  now; `GET /v1/jobs/{id}/runs` and `GET /v1/jobs/{id}/revisions` are
@@ -697,9 +1009,13 @@ next run, last outcome).
697
1009
  - `GET /v1/onboarding`, `POST /v1/onboarding/claim` — the unclaimed
698
1010
  state and the first-user claim.
699
1011
  - `POST /v1/auth/login`, `POST /v1/auth/logout`, `GET /v1/auth/whoami` —
700
- the browser session and the resolved principal.
1012
+ token sign-in, logout, and the resolved principal.
1013
+ - `GET /v1/auth/providers`, `GET /v1/auth/login/{provider}`,
1014
+ `GET`/`POST /v1/auth/callback/{provider}` — external sign-in.
701
1015
  - `GET`/`POST /v1/auth/tokens`, `DELETE /v1/auth/tokens/{id}` —
702
- self-service API tokens.
1016
+ self-service API tokens and browser sessions.
1017
+ - `GET /v1/auth/accounts`, `DELETE /v1/auth/accounts` — self-service
1018
+ external-account links (administrators may manage another user's).
703
1019
  - `GET /v1/users`, `PATCH`/`DELETE /v1/users/{user}`,
704
1020
  `POST /v1/users/invites` — the user directory and invite codes
705
1021
  (admin-gated).
@@ -1,5 +1,28 @@
1
1
  # Third-party notices
2
2
 
3
+ ## Lucide
4
+
5
+ - <https://lucide.dev>
6
+ - The webui's block-set icons (`webui/src/blocks/icons.tsx`) are inline
7
+ SVGs whose stroke geometry is copied from Lucide (v0.577.0), resized
8
+ and coloured by the block renderer.
9
+
10
+ ISC License
11
+
12
+ Copyright (c) 2020, Lucide Contributors
13
+
14
+ Permission to use, copy, modify, and/or distribute this software for any
15
+ purpose with or without fee is hereby granted, provided that the above
16
+ copyright notice and this permission notice appear in all copies.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
19
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
20
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
21
+ SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
22
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
23
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
24
+ IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
25
+
3
26
  ## hermes-webui
4
27
 
5
28
  - <https://github.com/nesquena/hermes-webui>