@sigrun/sigrun 0.1.4 → 0.1.5

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 (76) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +24 -2
  3. package/dist/blocks/builders.js +125 -0
  4. package/dist/blocks/images.js +57 -0
  5. package/dist/blocks/index.js +20 -0
  6. package/dist/blocks/limits.js +26 -0
  7. package/dist/blocks/placements.js +99 -0
  8. package/dist/blocks/schema.js +559 -0
  9. package/dist/blocks/validate.js +602 -0
  10. package/dist/config.js +31 -0
  11. package/dist/extension.d.ts +476 -9
  12. package/dist/harness/boot.js +8 -1
  13. package/dist/harness/event-bus.js +33 -0
  14. package/dist/harness/identity/users.js +3 -2
  15. package/dist/harness/loader.js +369 -13
  16. package/dist/harness/packages.js +71 -2
  17. package/dist/harness/seam-host.js +69 -2
  18. package/dist/harness/settings.js +5 -0
  19. package/dist/jobs/fire.js +11 -0
  20. package/dist/jobs/spec.js +19 -0
  21. package/dist/serve.js +29 -39
  22. package/dist/server/auth/pipeline.js +16 -1
  23. package/dist/server/conversation-ops.js +482 -10
  24. package/dist/server/gateway.js +122 -0
  25. package/dist/server/helpers.js +26 -0
  26. package/dist/server/host-bridge.js +141 -4
  27. package/dist/server/index.js +27 -0
  28. package/dist/server/notifications.js +6 -2
  29. package/dist/server/routes/conversations.js +37 -93
  30. package/dist/server/routes/events.js +39 -0
  31. package/dist/server/routes/ext-assets.js +84 -0
  32. package/dist/server/routes/extensions.js +31 -1
  33. package/dist/server/routes/files.js +38 -83
  34. package/dist/server/routes/meta.js +35 -1
  35. package/dist/server/routes/providers.js +5 -21
  36. package/dist/server/routes/submissions.js +36 -111
  37. package/dist/server/routes/surfaces.js +387 -0
  38. package/dist/server/routes/users.js +6 -1
  39. package/dist/server/session-meta.js +12 -6
  40. package/dist/server/shutdown.js +85 -0
  41. package/dist/server/surfaces/access-cache.js +43 -0
  42. package/dist/server/surfaces/dedup.js +67 -0
  43. package/dist/server/surfaces/instance.js +42 -0
  44. package/dist/server/surfaces/mime.js +73 -0
  45. package/dist/server/surfaces/rate-limit.js +63 -0
  46. package/dist/server/surfaces/service.js +493 -0
  47. package/dist/server/surfaces/watchers.js +94 -0
  48. package/node_modules/@sigrun/shared/dist/blocks/format.d.ts +28 -0
  49. package/node_modules/@sigrun/shared/dist/blocks/format.js +199 -0
  50. package/node_modules/@sigrun/shared/dist/blocks/icons.d.ts +12 -0
  51. package/node_modules/@sigrun/shared/dist/blocks/icons.js +64 -0
  52. package/node_modules/@sigrun/shared/dist/blocks/index.d.ts +25 -0
  53. package/node_modules/@sigrun/shared/dist/blocks/index.js +21 -0
  54. package/node_modules/@sigrun/shared/dist/blocks/instance.d.ts +29 -0
  55. package/node_modules/@sigrun/shared/dist/blocks/instance.js +51 -0
  56. package/node_modules/@sigrun/shared/dist/blocks/lttb.d.ts +16 -0
  57. package/node_modules/@sigrun/shared/dist/blocks/lttb.js +67 -0
  58. package/node_modules/@sigrun/shared/dist/blocks/ops.d.ts +44 -0
  59. package/node_modules/@sigrun/shared/dist/blocks/ops.js +294 -0
  60. package/node_modules/@sigrun/shared/dist/blocks/text.d.ts +43 -0
  61. package/node_modules/@sigrun/shared/dist/blocks/text.js +790 -0
  62. package/node_modules/@sigrun/shared/dist/blocks/transport.d.ts +77 -0
  63. package/node_modules/@sigrun/shared/dist/blocks/transport.js +11 -0
  64. package/node_modules/@sigrun/shared/dist/blocks/types.d.ts +541 -0
  65. package/node_modules/@sigrun/shared/dist/blocks/types.js +17 -0
  66. package/node_modules/@sigrun/shared/dist/jobs.d.ts +9 -0
  67. package/node_modules/@sigrun/shared/dist/live.d.ts +71 -0
  68. package/node_modules/@sigrun/shared/dist/live.js +82 -0
  69. package/node_modules/@sigrun/shared/dist/session-meta.d.ts +19 -3
  70. package/node_modules/@sigrun/shared/dist/settings.d.ts +7 -0
  71. package/node_modules/@sigrun/shared/dist/views.d.ts +81 -3
  72. package/node_modules/@sigrun/shared/package.json +10 -2
  73. package/package.json +9 -3
  74. package/webui/dist/assets/index-B4-GYqEb.js +50 -0
  75. package/webui/dist/index.html +1 -1
  76. package/webui/dist/assets/index-hVLvsizV.js +0 -50
package/CHANGELOG.md CHANGED
@@ -7,6 +7,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.1.5] - 2026-10-23
11
+ ### Added
12
+ - The image + chart release pipeline: multi-arch (amd64/arm64) container
13
+ image on `ghcr.io/sigrun-ai/sigrun` and the Helm chart on GHCR's OCI
14
+ registry (`charts/sigrun/sigrun`), built and published by GitHub Actions
15
+ on `v*` tags. CI builds and lints both on every PR and main push.
16
+ - The chart manages the extension fleet declaratively: a `sigrun:` values
17
+ block seeds `settings.json` (fleet list + chart-owned keys re-asserted
18
+ every boot, per-slug extension settings seed-once), passes secret
19
+ references through envFrom (never values), and installs deploy-time
20
+ tools (kubectl/helm/talosctl/flux/k9s) hash-cached onto persistent
21
+ storage via an initContainer.
22
+ - Deploy hardening: non-root image with a privilege-drop entrypoint (uid
23
+ 1000 default, overridable), agent userland baked in (ripgrep, fd, jq,
24
+ git, sqlite3), hardened compose defaults (read-only rootfs, caps
25
+ dropped, init, no-new-privileges).
26
+ - Block-set foundation (`@sigrun/shared/blocks`): the canonical block
27
+ tree wire types, ops, text rendering and transport shared by server and
28
+ clients.
29
+ - Gateway core seams: extension `channels` contribution field, the
30
+ gateway engine field with lifecycle + shutdown ordering, host-surface
31
+ conversation ops, upload/readFile/models/commands, interaction answer
32
+ attribution (`interactionSettled`), group-derived admin persisted for
33
+ host-side checks, and deliver-targets on prompt jobs
34
+ (`@sigrun/shared/live` narrows the live-doc wire).
35
+
36
+ ### Changed
37
+ - `@sigrun/shared` 0.1.1: new `./live` and `./blocks` export subpaths
38
+ (all 0.1.0 subpaths unchanged).
10
39
  ## [0.1.4] - 2026-10-22
11
40
  ### Added
12
41
  - The version is visible where operators look: the boot log line (`sigrun
package/README.md CHANGED
@@ -286,6 +286,20 @@ configurable) when the request comes from a peer inside
286
286
  proxy and nothing else: every peer in `trustedProxies` can forge any
287
287
  identity, including an admin.
288
288
 
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.
302
+
289
303
  **Onboarding (the first claim).** An unclaimed server serves
290
304
  `GET /v1/onboarding` + `POST /v1/onboarding/claim`. On a loopback bind
291
305
  or through a trusted proxy identity the claim needs no code; otherwise
@@ -640,8 +654,16 @@ is read-only for them (`PATCH`/`DELETE` return `409 job_declared`).
640
654
  A `prompt` action submits its prompt as a conversation — `"new"`
641
655
  creates one per fire (ownerless, `source: "cron"`), a fixed id submits
642
656
  into that conversation (the job's `model`/`thinking` apply only to
643
- `"new"`). A `task` action creates a pi-durable task owned by the
644
- schedule task (deleting the job cascades): `{"kind": "task",
657
+ `"new"`). A `prompt` action may also carry `deliver`: an array of
658
+ 1–8 gateway delivery targets (each 1–256 chars), valid only with
659
+ `conversation: "new"` — the run's answer is delivered to those chats
660
+ by the loaded gateway engine. The targets are stamped onto the run
661
+ conversation's meta alongside the owning `jobId` (both server-owned,
662
+ never PATCHable); with no engine loaded each fire logs one
663
+ `deliver targets ignored: no gateway` warning, and editing the targets
664
+ is a job revision like any other spec change. A `task` action creates
665
+ a pi-durable task owned by the schedule task (deleting the job
666
+ cascades): `{"kind": "task",
645
667
  "task": "<registered task name>", "input": …}`.
646
668
 
647
669
  **Runs:** a cron run is an ORDINARY CONTINUABLE CONVERSATION — open the
@@ -0,0 +1,125 @@
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 { BLOCK_SET_VERSION } from "@sigrun/shared/blocks";
19
+ /** `bind("/cards")` → `{ $: "/cards" }` — a pointer into the surface's data model (§4.2). */
20
+ export function bind(pointer) {
21
+ return { $: pointer };
22
+ }
23
+ /** Wrap a root block with the server's block-set version (§4.4). */
24
+ export function tree(root) {
25
+ return { v: BLOCK_SET_VERSION, root };
26
+ }
27
+ // ─── Shell (§5.1) ─────────────────────────────────────────────────────────
28
+ export function page(props) {
29
+ return { ...props, type: "page" };
30
+ }
31
+ // ─── Layout (§5.2) ────────────────────────────────────────────────────────
32
+ export function stack(props) {
33
+ return { ...props, type: "stack" };
34
+ }
35
+ export function columns(props) {
36
+ return { ...props, type: "columns" };
37
+ }
38
+ export function grid(props) {
39
+ return { ...props, type: "grid" };
40
+ }
41
+ export function section(props) {
42
+ return { ...props, type: "section" };
43
+ }
44
+ // ─── Content (§5.3) ───────────────────────────────────────────────────────
45
+ export function markdown(props) {
46
+ return { ...props, type: "markdown" };
47
+ }
48
+ export function fields(props) {
49
+ return { ...props, type: "fields" };
50
+ }
51
+ export function stat(props) {
52
+ return { ...props, type: "stat" };
53
+ }
54
+ export function callout(props) {
55
+ return { ...props, type: "callout" };
56
+ }
57
+ export function image(props) {
58
+ return { ...props, type: "image" };
59
+ }
60
+ // ─── Chart (§5.4) ─────────────────────────────────────────────────────────
61
+ export function chart(props) {
62
+ return { ...props, type: "chart" };
63
+ }
64
+ // ─── Meter / log / tree / diff / details (§5.3) ───────────────────────────
65
+ export function meter(props) {
66
+ return { ...props, type: "meter" };
67
+ }
68
+ export function log(props) {
69
+ return { ...props, type: "log" };
70
+ }
71
+ /** The tree BLOCK builder (§5.3) — `tree` is the root wrapper. */
72
+ export function treeBlock(props) {
73
+ return { ...props, type: "tree" };
74
+ }
75
+ export function diff(props) {
76
+ return { ...props, type: "diff" };
77
+ }
78
+ export function details(props) {
79
+ return { ...props, type: "details" };
80
+ }
81
+ // ─── Collection (§5.5) and the card template (§5.5.1) ────────────────────
82
+ export function collection(props) {
83
+ return { ...props, type: "collection" };
84
+ }
85
+ /** The collection item template (§5.5.1) — a slot bundle, not a block. */
86
+ export function card(props) {
87
+ return { ...props };
88
+ }
89
+ // ─── Form and fields (§5.6) ───────────────────────────────────────────────
90
+ export function form(props) {
91
+ return { ...props, type: "form" };
92
+ }
93
+ export function text(props) {
94
+ return { ...props, type: "text" };
95
+ }
96
+ export function longtext(props) {
97
+ return { ...props, type: "longtext" };
98
+ }
99
+ export function number(props) {
100
+ return { ...props, type: "number" };
101
+ }
102
+ export function toggle(props) {
103
+ return { ...props, type: "toggle" };
104
+ }
105
+ export function choice(props) {
106
+ return { ...props, type: "choice" };
107
+ }
108
+ export function date(props) {
109
+ return { ...props, type: "date" };
110
+ }
111
+ export function datetime(props) {
112
+ return { ...props, type: "datetime" };
113
+ }
114
+ export function daterange(props) {
115
+ return { ...props, type: "daterange" };
116
+ }
117
+ export function secret(props) {
118
+ return { ...props, type: "secret" };
119
+ }
120
+ export function file(props) {
121
+ return { ...props, type: "file" };
122
+ }
123
+ export function list(props) {
124
+ return { ...props, type: "list" };
125
+ }
@@ -0,0 +1,57 @@
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
+ /** Max base64 characters in a `data:` image URL (spec §11.1: 192 KiB). */
9
+ const MAX_DATA_URL_CHARS = 192 * 1024;
10
+ /** MIME types a `data:` URL may carry. */
11
+ const DATA_MIME_TYPES = new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
12
+ /**
13
+ * Check one image `src` against the §11.1 source rules. Bindings
14
+ * (`{$: …}` objects) are not checked here — their resolved string is,
15
+ * where the data model holds one (the data-size limit already covers
16
+ * the byte cost).
17
+ */
18
+ export function checkImageSource(src) {
19
+ if (src.startsWith("asset:")) {
20
+ const path = src.slice("asset:".length);
21
+ if (path.length === 0)
22
+ return { ok: false, message: "asset: path is empty" };
23
+ return { ok: true, kind: "asset" };
24
+ }
25
+ if (src.startsWith("data:")) {
26
+ const comma = src.indexOf(",");
27
+ if (comma === -1)
28
+ return { ok: false, message: "data: URL has no payload" };
29
+ const meta = src.slice("data:".length, comma);
30
+ const payload = src.slice(comma + 1);
31
+ const [type, ...params] = meta.split(";");
32
+ const normalized = type.toLowerCase();
33
+ if (!DATA_MIME_TYPES.has(normalized)) {
34
+ return {
35
+ ok: false,
36
+ message: `data: URL type ${type || "(empty)"} is not one of png, jpeg, gif, webp (svg and avif are never accepted)`,
37
+ };
38
+ }
39
+ if (!params.includes("base64")) {
40
+ return { ok: false, message: "data: URL must be base64" };
41
+ }
42
+ if (payload.length > MAX_DATA_URL_CHARS) {
43
+ return {
44
+ ok: false,
45
+ message: `data: URL payload is ${payload.length} base64 characters, over the 192 KiB cap`,
46
+ };
47
+ }
48
+ return { ok: true, kind: "data" };
49
+ }
50
+ if (/^https:\/\//.test(src)) {
51
+ return { ok: true, kind: "https" };
52
+ }
53
+ if (/^http:\/\//.test(src)) {
54
+ return { ok: false, message: "http: image URLs are rejected; use https" };
55
+ }
56
+ return { ok: false, message: "image src must be asset:, data: (png/jpeg/gif/webp) or https:" };
57
+ }
@@ -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
+ // The wire contract (§4/§5/§6.1) and the block-set version constant,
19
+ // verbatim from the shared leaf — one import site for extension code.
20
+ export { BLOCK_SET_VERSION } from "@sigrun/shared/blocks";
@@ -0,0 +1,26 @@
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
+ const KiB = 1024;
13
+ const MiB = 1024 * 1024;
14
+ /** Spec §4.5, one row per placement. */
15
+ export const LIMITS = {
16
+ chat: { blocks: 200, treeBytes: 256 * KiB, dataBytes: 256 * KiB },
17
+ interaction: { blocks: 100, treeBytes: 64 * KiB, dataBytes: 64 * KiB },
18
+ ambient: { blocks: 100, treeBytes: 64 * KiB, dataBytes: 64 * KiB },
19
+ view: { blocks: 2000, treeBytes: MiB, dataBytes: 4 * MiB },
20
+ panel: { blocks: 2000, treeBytes: MiB, dataBytes: 4 * MiB },
21
+ settings: { blocks: 2000, treeBytes: MiB, dataBytes: 4 * MiB },
22
+ };
23
+ /** UTF-8 byte length of a value's serialized JSON (spec §4.5 sizing). */
24
+ export function jsonBytes(value) {
25
+ return Buffer.byteLength(JSON.stringify(value), "utf8");
26
+ }
@@ -0,0 +1,99 @@
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
+ const ALL_BLOCKS = [
10
+ "page",
11
+ "stack",
12
+ "columns",
13
+ "grid",
14
+ "section",
15
+ "markdown",
16
+ "fields",
17
+ "stat",
18
+ "callout",
19
+ "image",
20
+ "chart",
21
+ "meter",
22
+ "log",
23
+ "tree",
24
+ "diff",
25
+ "details",
26
+ "collection",
27
+ "form",
28
+ ];
29
+ const ALL_FIELDS = [
30
+ "text",
31
+ "longtext",
32
+ "number",
33
+ "toggle",
34
+ "choice",
35
+ "date",
36
+ "datetime",
37
+ "daterange",
38
+ "secret",
39
+ "file",
40
+ "list",
41
+ ];
42
+ function rule(init) {
43
+ return {
44
+ blocks: new Set(init.blocks),
45
+ rootBlock: init.rootBlock,
46
+ pageSidebar: init.pageSidebar ?? true,
47
+ collectionDisplays: init.collectionDisplays,
48
+ fields: new Set(init.fields),
49
+ };
50
+ }
51
+ const CONTENT = ["markdown", "fields", "callout", "diff", "details"];
52
+ const STAT_METER_LOG = ["stat", "meter", "log"];
53
+ const CHART_IMAGE_TREE = ["chart", "image", "tree"];
54
+ const ALL_COLLECTION_DISPLAYS = ["list", "table", "lanes", "grid", "calendar"];
55
+ /** Spec §5.7, one row per placement. */
56
+ export const PLACEMENT_RULES = {
57
+ // view: everything; a required page root.
58
+ view: rule({
59
+ blocks: ALL_BLOCKS,
60
+ rootBlock: "page",
61
+ pageSidebar: true,
62
+ fields: ALL_FIELDS,
63
+ }),
64
+ // panel: everything but a sidebar on the page root.
65
+ panel: rule({
66
+ blocks: ALL_BLOCKS,
67
+ rootBlock: "page",
68
+ pageSidebar: false,
69
+ fields: ALL_FIELDS,
70
+ }),
71
+ // settings: section + content + stat-free; forms only at the root;
72
+ // collections limited to list/table; all fields but file.
73
+ settings: rule({
74
+ blocks: ["section", ...CONTENT, "collection", "form"],
75
+ rootBlock: "form",
76
+ collectionDisplays: ["list", "table"],
77
+ fields: ALL_FIELDS.filter((f) => f !== "file"),
78
+ }),
79
+ // chat: everything but page; all fields but secret.
80
+ chat: rule({
81
+ blocks: ALL_BLOCKS.filter((b) => b !== "page"),
82
+ fields: ALL_FIELDS.filter((f) => f !== "secret"),
83
+ }),
84
+ // interaction: stack/section layout, content, list collections, and a
85
+ // required form root with the ask-user subset of fields.
86
+ interaction: rule({
87
+ blocks: ["stack", "section", ...CONTENT, "collection", "form"],
88
+ rootBlock: "form",
89
+ collectionDisplays: ["list"],
90
+ fields: ["text", "longtext", "number", "toggle", "choice", "secret"],
91
+ }),
92
+ // ambient: stack/columns layout, content, stat/meter/log, list
93
+ // collections; no forms (§9.2 records render read-only state).
94
+ ambient: rule({
95
+ blocks: ["stack", "columns", ...CONTENT, ...STAT_METER_LOG, "collection"],
96
+ collectionDisplays: ["list"],
97
+ fields: [],
98
+ }),
99
+ };