@sigrun/sigrun 0.2.0 → 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.
- package/CHANGELOG.md +95 -7
- package/README.md +99 -12
- package/dist/blocks/build-stamp.json +13 -0
- package/dist/blocks/builders.d.ts +57 -0
- package/dist/blocks/images.d.ts +26 -0
- package/dist/blocks/index.d.ts +20 -0
- package/dist/blocks/limits.d.ts +25 -0
- package/dist/blocks/placements.d.ts +26 -0
- package/dist/blocks/schema.d.ts +38 -0
- package/dist/blocks/validate.d.ts +73 -0
- package/dist/extension.d.ts +41 -7
- package/dist/harness/boot.js +6 -0
- package/dist/harness/builtins.js +1 -1
- package/dist/harness/seam-host.js +12 -0
- package/dist/harness/sections/context-files.js +1 -1
- package/dist/sandbox/availability.js +69 -20
- package/dist/sandbox/launch.js +11 -0
- package/dist/sandbox/policy.js +24 -3
- package/dist/sandbox/worker.js +4 -24
- package/dist/serve.js +139 -26
- package/dist/server/auth/pipeline.js +52 -3
- package/dist/server/images/cache.js +13 -5
- package/dist/server/index.js +10 -2
- package/dist/server/notifications.js +21 -4
- package/dist/server/routes/images.js +16 -9
- package/dist/server/routes/me.js +1 -1
- package/dist/server/routes/meta.js +4 -0
- package/node_modules/@sigrun/shared/CHANGELOG.md +5 -0
- package/node_modules/@sigrun/shared/dist/views.d.ts +27 -0
- package/node_modules/@sigrun/shared/package.json +1 -1
- package/package.json +8 -9
- package/webui/dist/assets/{index-CxB969qp.js → index-SO2W4jjk.js} +1 -1
- package/webui/dist/index.html +1 -1
- package/CONTEXT.md +0 -163
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,75 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.1] - 2026-10-28
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `GET /v1/meta` now reports the effective listener: `listener.host`,
|
|
14
|
+
`listener.port`, and `listener.families` — the address families the
|
|
15
|
+
live listener accepts, measured once at listen (the `::` wildcard
|
|
16
|
+
probes both loopbacks and reports exactly the families that connect;
|
|
17
|
+
a hostname bind reports the bound socket's family, never a concrete
|
|
18
|
+
peer address). Additive wire field: pre-upgrade servers omit
|
|
19
|
+
`listener`; clients consuming it must tolerate its absence.
|
|
20
|
+
- `SIGRUN_LISTEN_HOST` is now honored as the environment equivalent of
|
|
21
|
+
the programmatic `serve({host})` option. One host value is resolved
|
|
22
|
+
at the serving boundary (`options.host` → `SIGRUN_LISTEN_HOST` → `::`)
|
|
23
|
+
and drives the bind AND both auth derivations — the previously split
|
|
24
|
+
defaults (the boot auth derivation's independent env read, and the
|
|
25
|
+
harness-side `0.0.0.0` fallback) are gone.
|
|
26
|
+
- Proxy-auth rejections (401) now log one sanitized `warn` per rejected
|
|
27
|
+
request: the socket peer, the method, a short reason (`untrusted
|
|
28
|
+
proxy`, `missing or wrong shared secret`, `missing identity header`,
|
|
29
|
+
`untrusted identity source`), and the resolved route template — never
|
|
30
|
+
tokens, cookies, identity-header values, query strings, or the full
|
|
31
|
+
URL. Token-auth failures and successful proxy authentications stay
|
|
32
|
+
silent.
|
|
33
|
+
- The normal core build now emits real `dist/blocks/**/*.d.ts`
|
|
34
|
+
declarations, including the `dist/blocks/index.d.ts` named by the
|
|
35
|
+
`@sigrun/sigrun/blocks` export. Pack/type-resolution coverage compiles
|
|
36
|
+
an external consumer against those declarations; extension consumers
|
|
37
|
+
no longer need an ambient block-module shim.
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
- Successful `PUT /v1/me/extensions/:slug/settings` writes now publish
|
|
41
|
+
through the same post-commit settings-change seam as section saves:
|
|
42
|
+
one per-user failure-fence clear, one observing-extension reconciliation,
|
|
43
|
+
and the extension slug plus changed body keys. Invalid, unknown, and
|
|
44
|
+
otherwise rejected writes publish nothing; repeated successful writes
|
|
45
|
+
retain the existing notification semantics.
|
|
46
|
+
- `host.peekInteraction(conversationId, callId, context)`: a
|
|
47
|
+
non-consuming interaction inspection read beside
|
|
48
|
+
`snapshotInteraction`. It returns the same stored record projection
|
|
49
|
+
(secret values stay masked) but NEVER touches the one-shot raw-secret
|
|
50
|
+
delivery — safe to repeat for notifiers, outcome edits, and answer
|
|
51
|
+
correlation. `snapshotInteraction` remains the ONLY read that merges
|
|
52
|
+
and consumes an unread delivery, reserved for the interaction's
|
|
53
|
+
owning extension; its first-read/second-read semantics are unchanged.
|
|
54
|
+
- `NotificationEvent` is now a discriminated union (shared base plus
|
|
55
|
+
`interaction.pending` / `job.failed` / `run.settled` variants), and
|
|
56
|
+
the `run.settled` variant carries a structured
|
|
57
|
+
`outcome: { status: "answered" } | { status: "unanswered"; reason: string }`
|
|
58
|
+
translated directly from the host event's existing `outcome`/`reason`
|
|
59
|
+
(no second answer read, no inferred status). The other variants do
|
|
60
|
+
not gain the field; exhaustive narrowing on `type` now typechecks.
|
|
61
|
+
|
|
62
|
+
### Fixed (continued)
|
|
63
|
+
- Restored per-user worker startup by placing the selected device paths
|
|
64
|
+
under read-only Landlock rules. Their ordinary I/O works, while
|
|
65
|
+
`/dev/shm` and writes outside workspace/home/tmp remain denied.
|
|
66
|
+
- Preserved composer text and attachments when a slow conversation-meta
|
|
67
|
+
restore resolves after a local edit or clear. The interaction visual
|
|
68
|
+
drain now covers required secrets and multi-step forms, and the visual
|
|
69
|
+
e2e gate passes all 163 cases.
|
|
70
|
+
|
|
71
|
+
### Changed
|
|
72
|
+
- Settled-run notifications are truthful: an answered run keeps the
|
|
73
|
+
`"Reply ready"` title, while an unanswered run (model error, abort,
|
|
74
|
+
or a missing answer) is titled `"Run ended without a reply"` with the
|
|
75
|
+
settle reason appended to the body — a failed run no longer
|
|
76
|
+
masquerades as a ready reply. Recipient, dedup, and
|
|
77
|
+
`notifications.runSettled` gating are unchanged.
|
|
78
|
+
|
|
10
79
|
## [0.2.0] - 2026-10-27
|
|
11
80
|
### Added
|
|
12
81
|
- Per-user extension surface (multi-user M2): lifecycle events carry the
|
|
@@ -58,11 +127,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
58
127
|
literal-content ruling.
|
|
59
128
|
- Images (U7): an HMAC-signed proxy, a rebinding-safe fetcher, an LRU cache
|
|
60
129
|
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
130
|
|
|
67
131
|
### Changed
|
|
68
132
|
- Family versions move together to 0.2.0: `@sigrun/shared` 0.2.0
|
|
@@ -91,6 +155,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
91
155
|
distinguish Landlock denials from Yama or missing-capability conditions.
|
|
92
156
|
|
|
93
157
|
## [0.1.7] - 2026-10-24
|
|
158
|
+
|
|
159
|
+
*Correction (2026-10-28): the original entry omitted substantial scope
|
|
160
|
+
that shipped in this release — the U4 settings rebuild, the M3a
|
|
161
|
+
account-link store and routes, and the accompanying `@sigrun/shared` /
|
|
162
|
+
`@sigrun/tui` settings-surface work. Those entries are restored below;
|
|
163
|
+
the Service-migration claim is also corrected (adding a secondary
|
|
164
|
+
family while retaining the primary is an in-place patch, not a
|
|
165
|
+
recreate).*
|
|
166
|
+
|
|
167
|
+
### Added
|
|
168
|
+
- Core settings sections rebuilt on blocks (U4): one settings renderer
|
|
169
|
+
for webui and TUI, extension-declared sections, a `/v1/settings`
|
|
170
|
+
sections route, `pendingRestart` with an admin restart route,
|
|
171
|
+
live/restart metadata for every core key, label search, and
|
|
172
|
+
`settings.changed`/`onSettingsChanged`. The shared and TUI packages
|
|
173
|
+
carry the matching settings-surface and settings-form work (live
|
|
174
|
+
surface revs, default-on toggle handling, roster-driven tool
|
|
175
|
+
defaults, walk-based search).
|
|
176
|
+
- Account links (multi-user M3a): the core account-link store plus
|
|
177
|
+
routes to list and remove an account's linked external identities.
|
|
178
|
+
|
|
94
179
|
### Fixed
|
|
95
180
|
- The deploy entry binds the dual-stack wildcard (`::`) instead of the
|
|
96
181
|
IPv4-only `0.0.0.0`. On a dual-stack pod whose Service is IPv6-only
|
|
@@ -107,8 +192,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
107
192
|
### Changed
|
|
108
193
|
- The chart's main Service is `PreferDualStack` over `[IPv6, IPv4]`,
|
|
109
194
|
so it advertises both pod addresses regardless of the cluster's
|
|
110
|
-
default family.
|
|
111
|
-
|
|
195
|
+
default family. Adding a secondary family while retaining the
|
|
196
|
+
primary is an in-place patch (`spec.ipFamilies` gains the new
|
|
197
|
+
family); only changing the PRIMARY family requires recreation
|
|
198
|
+
(`spec.clusterIPs` is immutable). *Corrected 2026-10-28: the
|
|
199
|
+
original entry claimed every conversion is a recreate.*
|
|
112
200
|
- `sigrun run` (workstation mode) still binds `127.0.0.1` only.
|
|
113
201
|
|
|
114
202
|
## [0.1.6] - 2026-10-24
|
package/README.md
CHANGED
|
@@ -1,11 +1,28 @@
|
|
|
1
1
|
# Sigrun
|
|
2
2
|
|
|
3
|
-
Sigrun is a durable personal-assistant
|
|
4
|
-
[pi-durable](https://www.npmjs.com/package/@earendil-works/pi-durable)
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
@@ -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. |
|
|
@@ -324,9 +342,9 @@ needed. Set `"builtinExtensions": {"oauth": false}` to disable it; like the
|
|
|
324
342
|
other built-in toggles, this is restart-required. A configured extension
|
|
325
343
|
whose package name is also `@sigrun/oauth` replaces the built-in. If the
|
|
326
344
|
built-in cannot be resolved or loaded, it is quarantined without aborting
|
|
327
|
-
boot.
|
|
328
|
-
|
|
329
|
-
|
|
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.
|
|
330
348
|
|
|
331
349
|
Create an OAuth/OIDC client at the identity service, with the redirect URI
|
|
332
350
|
shown below. Configure providers in the extension's
|
|
@@ -556,8 +574,72 @@ identity-switch capabilities once (replace `<helper>` with its path):
|
|
|
556
574
|
sudo setcap cap_setuid,cap_setgid+ep <helper>
|
|
557
575
|
```
|
|
558
576
|
|
|
559
|
-
The helper's boot self-test verifies the switch.
|
|
560
|
-
|
|
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.
|
|
561
643
|
|
|
562
644
|
### Kernel integration runner controls
|
|
563
645
|
|
|
@@ -790,7 +872,7 @@ a dashboard) are in
|
|
|
790
872
|
|
|
791
873
|
## Built-in extensions
|
|
792
874
|
|
|
793
|
-
|
|
875
|
+
Five extensions are built into core — **codemode**, **MCP**, **tool
|
|
794
876
|
search**, **gateway**, and **OAuth login**. The first three mirror
|
|
795
877
|
what pi 1.3.0 ships built-in; the gateway is sigrun's own channel-adapter
|
|
796
878
|
engine; OAuth provides OIDC, GitHub, and Discord login providers. They
|
|
@@ -833,6 +915,11 @@ default. They join the offered set when an enabled MCP server's
|
|
|
833
915
|
The MCP servers themselves live in the MCP config file — the
|
|
834
916
|
`@sigrun/ext-mcp` package's README documents its format, including each
|
|
835
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).
|
|
836
923
|
|
|
837
924
|
```jsonc
|
|
838
925
|
{
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The typed builders (spec §14.1): one function per block and field,
|
|
3
|
+
* plus `bind(pointer)` and `tree(root)`. Builders return plain JSON —
|
|
4
|
+
* there is no runtime besides them.
|
|
5
|
+
*
|
|
6
|
+
* Each builder takes its block's props object and returns the block
|
|
7
|
+
* with the `type` literal stamped in. The literal is spread LAST, so a
|
|
8
|
+
* stray `type` inside a JavaScript caller's props object can never
|
|
9
|
+
* override it; TypeScript callers cannot pass one at all (`Props` omits
|
|
10
|
+
* it). The tree BLOCK builder is exported as `treeBlock`, keeping the
|
|
11
|
+
* `tree` name for the version-stamping root wrapper.
|
|
12
|
+
*
|
|
13
|
+
* `tree(root)` stamps {@link BLOCK_SET_VERSION} from the shared wire
|
|
14
|
+
* package — the version the SERVER validates against, which is why
|
|
15
|
+
* extensions import these builders from `@sigrun/sigrun/blocks` (the
|
|
16
|
+
* host-linked copy, spec §14.1) instead of bundling their own.
|
|
17
|
+
*/
|
|
18
|
+
import type { Binding, Block, CalloutBlock, Card, ChartBlock, ChoiceField, CollectionBlock, ColumnsBlock, DateField, DaterangeField, DatetimeField, DetailsBlock, DiffBlock, FieldsBlock, FileField, FormBlock, GridBlock, ImageBlock, ListField, LogBlock, LongtextField, MarkdownBlock, MeterBlock, NumberField, PageBlock, SectionBlock, SecretField, StackBlock, StatBlock, TextField, ToggleField, Tree, TreeBlock } from "@sigrun/shared/blocks";
|
|
19
|
+
/** One block/field's props: the wire shape minus the `type` literal. */
|
|
20
|
+
type Props<T> = Omit<T, "type">;
|
|
21
|
+
/** `bind("/cards")` → `{ $: "/cards" }` — a pointer into the surface's data model (§4.2). */
|
|
22
|
+
export declare function bind(pointer: string): Binding;
|
|
23
|
+
/** Wrap a root block with the server's block-set version (§4.4). */
|
|
24
|
+
export declare function tree(root: Block): Tree;
|
|
25
|
+
export declare function page(props: Props<PageBlock>): PageBlock;
|
|
26
|
+
export declare function stack(props: Props<StackBlock>): StackBlock;
|
|
27
|
+
export declare function columns(props: Props<ColumnsBlock>): ColumnsBlock;
|
|
28
|
+
export declare function grid(props: Props<GridBlock>): GridBlock;
|
|
29
|
+
export declare function section(props: Props<SectionBlock>): SectionBlock;
|
|
30
|
+
export declare function markdown(props: Props<MarkdownBlock>): MarkdownBlock;
|
|
31
|
+
export declare function fields(props: Props<FieldsBlock>): FieldsBlock;
|
|
32
|
+
export declare function stat(props: Props<StatBlock>): StatBlock;
|
|
33
|
+
export declare function callout(props: Props<CalloutBlock>): CalloutBlock;
|
|
34
|
+
export declare function image(props: Props<ImageBlock>): ImageBlock;
|
|
35
|
+
export declare function chart(props: Props<ChartBlock>): ChartBlock;
|
|
36
|
+
export declare function meter(props: Props<MeterBlock>): MeterBlock;
|
|
37
|
+
export declare function log(props: Props<LogBlock>): LogBlock;
|
|
38
|
+
/** The tree BLOCK builder (§5.3) — `tree` is the root wrapper. */
|
|
39
|
+
export declare function treeBlock(props: Props<TreeBlock>): TreeBlock;
|
|
40
|
+
export declare function diff(props: Props<DiffBlock>): DiffBlock;
|
|
41
|
+
export declare function details(props: Props<DetailsBlock>): DetailsBlock;
|
|
42
|
+
export declare function collection(props: Props<CollectionBlock>): CollectionBlock;
|
|
43
|
+
/** The collection item template (§5.5.1) — a slot bundle, not a block. */
|
|
44
|
+
export declare function card(props: Card): Card;
|
|
45
|
+
export declare function form(props: Props<FormBlock>): FormBlock;
|
|
46
|
+
export declare function text(props: Props<TextField>): TextField;
|
|
47
|
+
export declare function longtext(props: Props<LongtextField>): LongtextField;
|
|
48
|
+
export declare function number(props: Props<NumberField>): NumberField;
|
|
49
|
+
export declare function toggle(props: Props<ToggleField>): ToggleField;
|
|
50
|
+
export declare function choice(props: Props<ChoiceField>): ChoiceField;
|
|
51
|
+
export declare function date(props: Props<DateField>): DateField;
|
|
52
|
+
export declare function datetime(props: Props<DatetimeField>): DatetimeField;
|
|
53
|
+
export declare function daterange(props: Props<DaterangeField>): DaterangeField;
|
|
54
|
+
export declare function secret(props: Props<SecretField>): SecretField;
|
|
55
|
+
export declare function file(props: Props<FileField>): FileField;
|
|
56
|
+
export declare function list(props: Props<ListField>): ListField;
|
|
57
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image source rules (spec §11.1): `asset:` paths, `data:` URLs of the
|
|
3
|
+
* four accepted MIME types (capped at 192 KiB of base64 text), and
|
|
4
|
+
* `https:` URLs. SVG and AVIF are never accepted (script payload; no
|
|
5
|
+
* TUI decoder), and `http:` URLs are rejected. The proxy that rewrites
|
|
6
|
+
* and fetches is U7; this module only validates.
|
|
7
|
+
*/
|
|
8
|
+
/** A source that passed the §11.1 rules. */
|
|
9
|
+
export type ImageSourceOk = {
|
|
10
|
+
ok: true;
|
|
11
|
+
kind: "asset" | "data" | "https";
|
|
12
|
+
};
|
|
13
|
+
/** A source the §11.1 rules reject. */
|
|
14
|
+
export type ImageSourceErr = {
|
|
15
|
+
ok: false;
|
|
16
|
+
message: string;
|
|
17
|
+
};
|
|
18
|
+
/** The result of checking one `src` value. */
|
|
19
|
+
export type ImageSourceResult = ImageSourceOk | ImageSourceErr;
|
|
20
|
+
/**
|
|
21
|
+
* Check one image `src` against the §11.1 source rules. Bindings
|
|
22
|
+
* (`{$: …}` objects) are not checked here — their resolved string is,
|
|
23
|
+
* where the data model holds one (the data-size limit already covers
|
|
24
|
+
* the byte cost).
|
|
25
|
+
*/
|
|
26
|
+
export declare function checkImageSource(src: string): ImageSourceResult;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@sigrun/sigrun/blocks` — the extension authoring surface for the
|
|
3
|
+
* block set (spec §14.1): the typed builders, the wire types, and
|
|
4
|
+
* `validateTree` (§14.2) so an extension can unit-test its trees
|
|
5
|
+
* against the same validation the server runs.
|
|
6
|
+
*
|
|
7
|
+
* Extensions import this subpath, never a bundled copy: the package is
|
|
8
|
+
* a `peerDependency: "*"` (see docs/extending.md) and
|
|
9
|
+
* `@sigrun/sigrun` is a HOST RUNTIME MODULE (src/harness/packages.ts)
|
|
10
|
+
* — the host links its own running copy into every extension packages
|
|
11
|
+
* dir, so `tree(...)` always stamps the server's
|
|
12
|
+
* {@link BLOCK_SET_VERSION}. The `exports` map points here at
|
|
13
|
+
* `dist/blocks/index.js` (never `src/`), which is what the built
|
|
14
|
+
* server ships.
|
|
15
|
+
*/
|
|
16
|
+
export * from "./builders.js";
|
|
17
|
+
export { validateTree, validateData, VALID_PLACEMENTS, PLACEMENT_RULES, LIMITS, } from "./validate.js";
|
|
18
|
+
export type { PlacementLimits, PlacementRule, ValidateDataResult, ValidateErr, ValidateOk, ValidateOptions, ValidateResult, ValidationError, } from "./validate.js";
|
|
19
|
+
export { BLOCK_SET_VERSION } from "@sigrun/shared/blocks";
|
|
20
|
+
export type * from "@sigrun/shared/blocks";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The per-placement size limits of spec §4.5: max blocks per tree, and
|
|
3
|
+
* max UTF-8 bytes of the serialized JSON for the tree and the data.
|
|
4
|
+
*
|
|
5
|
+
* Over a limit the server rejects with `413 surface_too_large` naming
|
|
6
|
+
* the limit; `validateTree`/`validateData` surface the same error
|
|
7
|
+
* strings. A collection counts as one block however many items it
|
|
8
|
+
* binds, and a `data:` image URL counts toward whichever of the tree or
|
|
9
|
+
* the data holds it (the sizes are measured over the serialized JSON,
|
|
10
|
+
* so both hold by construction).
|
|
11
|
+
*/
|
|
12
|
+
import type { Placement } from "@sigrun/shared/blocks";
|
|
13
|
+
/** One row of the §4.5 limits table. */
|
|
14
|
+
export interface PlacementLimits {
|
|
15
|
+
/** Max blocks in one tree (a collection counts as one). */
|
|
16
|
+
readonly blocks: number;
|
|
17
|
+
/** Max UTF-8 bytes of the serialized tree JSON. */
|
|
18
|
+
readonly treeBytes: number;
|
|
19
|
+
/** Max UTF-8 bytes of the serialized data JSON. */
|
|
20
|
+
readonly dataBytes: number;
|
|
21
|
+
}
|
|
22
|
+
/** Spec §4.5, one row per placement. */
|
|
23
|
+
export declare const LIMITS: Readonly<Record<Placement, PlacementLimits>>;
|
|
24
|
+
/** UTF-8 byte length of a value's serialized JSON (spec §4.5 sizing). */
|
|
25
|
+
export declare function jsonBytes(value: unknown): number;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The placement rules of spec §5.7, as data: which blocks, which
|
|
3
|
+
* collection displays and which form fields each placement allows,
|
|
4
|
+
* plus the root-block and `page.sidebar` constraints the table carries.
|
|
5
|
+
*
|
|
6
|
+
* `validateTree` walks a tree against these rules; the tests pin the
|
|
7
|
+
* table's literal cells so this data cannot drift from the spec.
|
|
8
|
+
*/
|
|
9
|
+
/** One row of the §5.7 table. */
|
|
10
|
+
export interface PlacementRule {
|
|
11
|
+
/** Blocks allowed anywhere in the tree. */
|
|
12
|
+
readonly blocks: ReadonlySet<string>;
|
|
13
|
+
/** The only legal root block type, when the placement pins it. */
|
|
14
|
+
readonly rootBlock?: "page" | "form";
|
|
15
|
+
/** Whether a root `page` may carry `sidebar` (panel: no). */
|
|
16
|
+
readonly pageSidebar: boolean;
|
|
17
|
+
/** Collection displays allowed (`undefined`: all five). */
|
|
18
|
+
readonly collectionDisplays?: readonly string[];
|
|
19
|
+
/** Field types allowed in `form` blocks. */
|
|
20
|
+
readonly fields: ReadonlySet<string>;
|
|
21
|
+
}
|
|
22
|
+
/** The placement names, kept local so this module has no imports. */
|
|
23
|
+
type PlacementName = "view" | "panel" | "settings" | "chat" | "interaction" | "ambient";
|
|
24
|
+
/** Spec §5.7, one row per placement. */
|
|
25
|
+
export declare const PLACEMENT_RULES: Readonly<Record<PlacementName, PlacementRule>>;
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The typebox wire schemas for every block and field (spec §5/§6.1),
|
|
3
|
+
* each compiled once into its own `Validator` (spec §14.2 validation).
|
|
4
|
+
*
|
|
5
|
+
* Design notes:
|
|
6
|
+
* - Schemas are the wire contract, so they deliberately do NOT set
|
|
7
|
+
* `additionalProperties: false`: unknown props pass through and
|
|
8
|
+
* channels ignore them (the block-set version governs what exists).
|
|
9
|
+
* - `Tone`, `IconName` and `Format` are enums/patterns built from the
|
|
10
|
+
* shared constants; an unknown icon name fails here, at the icon
|
|
11
|
+
* prop's own path.
|
|
12
|
+
* - Bindable props are `Union([literal, Binding])` throughout; the
|
|
13
|
+
* literal-ness of STRUCTURAL props (`display`, `groups`, `reorder`,
|
|
14
|
+
* `mode`, field `name`s, `columns`, handler names) is enforced by the
|
|
15
|
+
* second walk in `validate.ts`, not the schema, so the error can say
|
|
16
|
+
* "must be a literal" instead of an anyOf mismatch.
|
|
17
|
+
* - Child slots (`children`, `sidebar`, `main`, `detail`,
|
|
18
|
+
* `columns[].block`, `collection.item`, `form` children, a field's
|
|
19
|
+
* `list.item`, `page.switcher`, a bulk action's `form.root`) are
|
|
20
|
+
* deliberately loose here (any object): `validate.ts` walks the tree
|
|
21
|
+
* and validates every child node against ITS compiled validator, so
|
|
22
|
+
* one broken block is one precise error instead of a union mismatch
|
|
23
|
+
* per sibling branch.
|
|
24
|
+
* - `Compile` (typebox/compile) builds each validator; its errors are
|
|
25
|
+
* read from `instancePath`, relative to the node the validator ran
|
|
26
|
+
* against.
|
|
27
|
+
*/
|
|
28
|
+
import type { Validator } from "typebox/compile";
|
|
29
|
+
/** One compiled validator per block type, keyed by its `type` literal. */
|
|
30
|
+
export declare const BLOCK_VALIDATORS: ReadonlyMap<string, Validator>;
|
|
31
|
+
/** One compiled validator per field type, keyed by its `type` literal. */
|
|
32
|
+
export declare const FIELD_VALIDATORS: ReadonlyMap<string, Validator>;
|
|
33
|
+
/** The compiled validator for `Action` objects (spec §6.1). */
|
|
34
|
+
export declare const ACTION_VALIDATOR: Validator;
|
|
35
|
+
/** The compiled validator for action buttons (spec §6.1). */
|
|
36
|
+
export declare const ACTION_BUTTON_VALIDATOR: Validator;
|
|
37
|
+
/** The compiled validator for a collection's card template (spec §5.5.1). */
|
|
38
|
+
export declare const CARD_VALIDATOR: Validator;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `validateTree` (spec §14.2): every error with its JSON path and a
|
|
3
|
+
* plain message.
|
|
4
|
+
*
|
|
5
|
+
* Two passes:
|
|
6
|
+
* 1. The compiled typebox validators (`schema.ts`) check each node's
|
|
7
|
+
* shape: the walk descends the tree, finds every block, field,
|
|
8
|
+
* action, button and card template, and runs the matching compiled
|
|
9
|
+
* validator against it. Instance paths (relative to the node) join
|
|
10
|
+
* the walk's path, so one broken block is one precise error
|
|
11
|
+
* (`root.main[2].text`), never a union mismatch per sibling.
|
|
12
|
+
* 2. The same walk checks what schemas cannot express: placements
|
|
13
|
+
* (§5.7), the literal-ness of structural props (§4.2), handler names
|
|
14
|
+
* (§6.1), relative bindings outside item templates (§4.2), ids
|
|
15
|
+
* (§4.1), `turn` outside chat (§6.2), `open` schemes (§6.1), image
|
|
16
|
+
* sources (§11.1), limits (§4.5), and the ambient one-line rule
|
|
17
|
+
* (§9.2).
|
|
18
|
+
*/
|
|
19
|
+
import { type Placement, type Tree } from "@sigrun/shared/blocks";
|
|
20
|
+
export { PLACEMENT_RULES } from "./placements.js";
|
|
21
|
+
export { LIMITS } from "./limits.js";
|
|
22
|
+
export type { PlacementRule } from "./placements.js";
|
|
23
|
+
export type { PlacementLimits } from "./limits.js";
|
|
24
|
+
/** The placements the block set defines (spec §3). */
|
|
25
|
+
export declare const VALID_PLACEMENTS: readonly Placement[];
|
|
26
|
+
/** One validation error: where and what (spec §14.2). */
|
|
27
|
+
export interface ValidationError {
|
|
28
|
+
path: string;
|
|
29
|
+
message: string;
|
|
30
|
+
}
|
|
31
|
+
/** Options for `validateTree`. */
|
|
32
|
+
export interface ValidateOptions {
|
|
33
|
+
placement: Placement;
|
|
34
|
+
/**
|
|
35
|
+
* The surface's `actions` map keys (spec §6.1); when given, every
|
|
36
|
+
* handler-name prop must be in it. When omitted (chat), handler
|
|
37
|
+
* names are free labels.
|
|
38
|
+
*/
|
|
39
|
+
handlers?: ReadonlySet<string>;
|
|
40
|
+
/** The server's block-set version; a mismatched tree is rejected (§4.4). */
|
|
41
|
+
serverV?: number;
|
|
42
|
+
/** For ambient trees: which `place` renders this body (§9.2 one-line rule). */
|
|
43
|
+
ambientPlace?: "status" | "footer" | "panel";
|
|
44
|
+
}
|
|
45
|
+
/** The ok result of `validateTree`: the tree, narrowed to the wire type. */
|
|
46
|
+
export interface ValidateOk {
|
|
47
|
+
ok: true;
|
|
48
|
+
tree: Tree;
|
|
49
|
+
}
|
|
50
|
+
/** The failure result: every error found, each with its path. */
|
|
51
|
+
export interface ValidateErr {
|
|
52
|
+
ok: false;
|
|
53
|
+
errors: ValidationError[];
|
|
54
|
+
}
|
|
55
|
+
/** `validateTree`'s result. */
|
|
56
|
+
export type ValidateResult = ValidateOk | ValidateErr;
|
|
57
|
+
/** The result of `validateData`. */
|
|
58
|
+
export type ValidateDataResult = {
|
|
59
|
+
ok: true;
|
|
60
|
+
} | {
|
|
61
|
+
ok: false;
|
|
62
|
+
errors: ValidationError[];
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Validate a block tree against a placement (spec §14.2). Returns every
|
|
66
|
+
* error with its JSON path; on success, the narrowed tree.
|
|
67
|
+
*/
|
|
68
|
+
export declare function validateTree(tree: unknown, opts: ValidateOptions): ValidateResult;
|
|
69
|
+
/**
|
|
70
|
+
* Validate a surface's data object: a JSON object within the placement's
|
|
71
|
+
* data-size limit (spec §4.5).
|
|
72
|
+
*/
|
|
73
|
+
export declare function validateData(data: unknown, placement: Placement): ValidateDataResult;
|
package/dist/extension.d.ts
CHANGED
|
@@ -349,14 +349,18 @@ export interface ExtensionCommandContribution {
|
|
|
349
349
|
readonly notice?: string;
|
|
350
350
|
} | void>;
|
|
351
351
|
}
|
|
352
|
-
/**
|
|
353
|
-
|
|
354
|
-
|
|
352
|
+
/** The settled-run outcome: the notification-safe projection of the host
|
|
353
|
+
* event's `outcome`/`reason` (status and reason only — no answer text). */
|
|
354
|
+
export type RunSettledOutcome = {
|
|
355
|
+
readonly status: "answered";
|
|
356
|
+
} | {
|
|
357
|
+
readonly status: "unanswered";
|
|
358
|
+
readonly reason: string;
|
|
359
|
+
};
|
|
360
|
+
/** Fields shared by every notification variant. */
|
|
361
|
+
interface NotificationEventBase {
|
|
355
362
|
readonly conversationId?: number;
|
|
356
363
|
readonly jobId?: string;
|
|
357
|
-
/** Gateway C6: the pending record's id — set on `interaction.pending` only
|
|
358
|
-
* (read the record via `host.snapshotInteraction`). */
|
|
359
|
-
readonly callId?: string;
|
|
360
364
|
/** The recipient. */
|
|
361
365
|
readonly user: string | undefined;
|
|
362
366
|
readonly title: string;
|
|
@@ -364,6 +368,21 @@ export interface NotificationEvent {
|
|
|
364
368
|
/** `${server.publicUrl}/#/c/<conversationId>` when `server.publicUrl` is set; absent otherwise. */
|
|
365
369
|
readonly url?: string;
|
|
366
370
|
}
|
|
371
|
+
/** One notification core delivers to every registered notifier (best-effort). */
|
|
372
|
+
export type NotificationEvent = NotificationEventBase & ({
|
|
373
|
+
readonly type: "interaction.pending";
|
|
374
|
+
/** Gateway C6: the pending record's id — set on `interaction.pending` only
|
|
375
|
+
* (inspect it via `host.peekInteraction`; the OWNING extension's
|
|
376
|
+
* delivery read is `host.snapshotInteraction`). */
|
|
377
|
+
readonly callId?: string;
|
|
378
|
+
} | {
|
|
379
|
+
readonly type: "job.failed";
|
|
380
|
+
} | {
|
|
381
|
+
readonly type: "run.settled";
|
|
382
|
+
/** Whether the run answered or not (model error, abort, …), with
|
|
383
|
+
* the wire reason verbatim. Never infer success from the body. */
|
|
384
|
+
readonly outcome: RunSettledOutcome;
|
|
385
|
+
});
|
|
367
386
|
/** One notifier transport (ntfy, Signal, web push — core ships none). */
|
|
368
387
|
export interface NotifierContribution {
|
|
369
388
|
readonly name: string;
|
|
@@ -800,7 +819,22 @@ export interface SigrunHost {
|
|
|
800
819
|
}): Promise<boolean>;
|
|
801
820
|
/** Mark a pending interaction expired/cancelled/skipped (extension-owned semantics). */
|
|
802
821
|
settleInteraction(conversationId: ConversationId, callId: string, status: "skipped" | "expired" | "cancelled", context: HandlerContext): Promise<boolean>;
|
|
803
|
-
/**
|
|
822
|
+
/**
|
|
823
|
+
* Inspect one interaction record WITHOUT consuming anything: returns
|
|
824
|
+
* the same stored projection as {@link SigrunHost.snapshotInteraction}
|
|
825
|
+
* (secret values stay `SECRET_MASK`), but never touches the one-shot
|
|
826
|
+
* secret-delivery `take` path. Safe to repeat; neither extends nor
|
|
827
|
+
* shortens secret TTL; creates, settles, and mutates nothing. Use for
|
|
828
|
+
* inspection/rendering (notifiers, outcome edits, correlation).
|
|
829
|
+
*/
|
|
830
|
+
peekInteraction(conversationId: ConversationId, callId: string, context: HandlerContext): Promise<Readonly<InteractionRecord> | undefined>;
|
|
831
|
+
/**
|
|
832
|
+
* The OWNING extension's delivery read: the first applicable read of
|
|
833
|
+
* an answered secret record merges its raw secret values back ONCE;
|
|
834
|
+
* subsequent reads see the stored mask. Inspection-only callers must
|
|
835
|
+
* use {@link SigrunHost.peekInteraction} instead — a consuming read
|
|
836
|
+
* by a non-owner silently destroys the owner's one-shot delivery.
|
|
837
|
+
*/
|
|
804
838
|
snapshotInteraction(conversationId: ConversationId, callId: string, context: HandlerContext): Promise<Readonly<InteractionRecord> | undefined>;
|
|
805
839
|
/** Fully replace one ambient surface's state (write = replacement); the key must be unowned or yours. */
|
|
806
840
|
setAmbient(conversationId: ConversationId, record: {
|