@sigrun/sigrun 0.1.6 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/CHANGELOG.md +192 -0
  2. package/README.md +445 -42
  3. package/dist/blocks/build-stamp.json +13 -0
  4. package/dist/blocks/builders.d.ts +57 -0
  5. package/dist/blocks/images.d.ts +26 -0
  6. package/dist/blocks/index.d.ts +20 -0
  7. package/dist/blocks/limits.d.ts +25 -0
  8. package/dist/blocks/placements.d.ts +26 -0
  9. package/dist/blocks/schema.d.ts +38 -0
  10. package/dist/blocks/schema.js +9 -2
  11. package/dist/blocks/validate.d.ts +73 -0
  12. package/dist/blocks/validate.js +57 -8
  13. package/dist/cli.js +26 -2
  14. package/dist/config.js +67 -1
  15. package/dist/extension.d.ts +230 -22
  16. package/dist/harness/ambient.js +39 -185
  17. package/dist/harness/blocks/entries.js +47 -0
  18. package/dist/harness/blocks/state.js +58 -0
  19. package/dist/harness/blocks/tools.js +274 -0
  20. package/dist/harness/boot.js +259 -20
  21. package/dist/harness/builtins.js +15 -11
  22. package/dist/harness/event-bus.js +105 -10
  23. package/dist/harness/identity/accounts.js +186 -0
  24. package/dist/harness/identity/groups.js +14 -0
  25. package/dist/harness/identity/index.js +13 -6
  26. package/dist/harness/identity/json-store.js +15 -4
  27. package/dist/harness/identity/setup-code-check.js +9 -0
  28. package/dist/harness/identity/tokens.js +28 -5
  29. package/dist/harness/identity/users.js +111 -12
  30. package/dist/harness/interactions.js +292 -136
  31. package/dist/harness/loader.js +271 -17
  32. package/dist/harness/packages.js +1 -1
  33. package/dist/harness/reserved-sections.js +21 -0
  34. package/dist/harness/seam-host.js +92 -6
  35. package/dist/harness/sections/context-files.js +1 -1
  36. package/dist/harness/settings.js +63 -2
  37. package/dist/harness/stores.js +241 -4
  38. package/dist/harness/user-services.js +391 -0
  39. package/dist/harness/user-tools.js +187 -0
  40. package/dist/harness/workspace.js +61 -57
  41. package/dist/jobs/fire.js +10 -16
  42. package/dist/sandbox/availability.js +211 -0
  43. package/dist/sandbox/command-observer.js +59 -0
  44. package/dist/sandbox/grants.js +159 -0
  45. package/dist/sandbox/helper.js +29 -0
  46. package/dist/sandbox/isolation.js +29 -0
  47. package/dist/sandbox/launch.js +31 -0
  48. package/dist/sandbox/os-account.js +49 -0
  49. package/dist/sandbox/policy.js +274 -0
  50. package/dist/sandbox/pool.js +548 -0
  51. package/dist/sandbox/protocol.js +167 -0
  52. package/dist/sandbox/sandboxed-env.js +245 -0
  53. package/dist/sandbox/unavailable-env.js +49 -0
  54. package/dist/sandbox/worker.js +261 -0
  55. package/dist/serve.js +213 -25
  56. package/dist/server/auth/access-principal.js +16 -0
  57. package/dist/server/auth/pipeline.js +99 -36
  58. package/dist/server/blocks/answers.js +94 -0
  59. package/dist/server/blocks/chat-surfaces.js +393 -0
  60. package/dist/server/blocks/notes.js +216 -0
  61. package/dist/server/blocks/summaries.js +328 -0
  62. package/dist/server/commands.js +5 -0
  63. package/dist/server/content-parts-lazy.js +13 -0
  64. package/dist/server/conversation-handler.js +2 -5
  65. package/dist/server/conversation-ops.js +138 -37
  66. package/dist/server/helpers.js +18 -6
  67. package/dist/server/host-bridge.js +40 -20
  68. package/dist/server/images/cache.js +213 -0
  69. package/dist/server/images/convert.js +13 -0
  70. package/dist/server/images/fetch.js +302 -0
  71. package/dist/server/images/ranges.js +42 -0
  72. package/dist/server/images/rewrite.js +94 -0
  73. package/dist/server/images/sign.js +84 -0
  74. package/dist/server/images/sniff.js +62 -0
  75. package/dist/server/index.js +152 -4
  76. package/dist/server/login/flows.js +126 -0
  77. package/dist/server/login/http.js +31 -0
  78. package/dist/server/login/resolve.js +133 -0
  79. package/dist/server/login/setup-claim.js +55 -0
  80. package/dist/server/login/usernames.js +15 -0
  81. package/dist/server/notifications.js +21 -4
  82. package/dist/server/ownership.js +13 -0
  83. package/dist/server/routes/admin.js +58 -0
  84. package/dist/server/routes/auth.js +88 -2
  85. package/dist/server/routes/conversations.js +3 -6
  86. package/dist/server/routes/events.js +28 -3
  87. package/dist/server/routes/extensions.js +37 -5
  88. package/dist/server/routes/images.js +274 -0
  89. package/dist/server/routes/interactions.js +57 -68
  90. package/dist/server/routes/jobs.js +2 -8
  91. package/dist/server/routes/login.js +223 -0
  92. package/dist/server/routes/me.js +90 -0
  93. package/dist/server/routes/meta.js +19 -0
  94. package/dist/server/routes/onboarding.js +94 -84
  95. package/dist/server/routes/settings-sections.js +190 -0
  96. package/dist/server/routes/settings.js +16 -4
  97. package/dist/server/routes/submissions.js +1 -1
  98. package/dist/server/routes/surfaces.js +175 -23
  99. package/dist/server/routes/users.js +116 -20
  100. package/dist/server/schema-fixture.js +213 -0
  101. package/dist/server/session-meta.js +3 -0
  102. package/dist/server/shutdown.js +11 -4
  103. package/dist/server/static.js +18 -0
  104. package/dist/server/surfaces/dedup.js +9 -2
  105. package/dist/server/surfaces/service.js +181 -13
  106. package/dist/server/surfaces/settings-surfaces.js +597 -0
  107. package/dist/server/turns/submit-turn.js +26 -0
  108. package/dist/server/views.js +16 -0
  109. package/dist/settings/core-sections.js +532 -0
  110. package/dist/settings/pending.js +181 -0
  111. package/dist/settings/registry.js +453 -0
  112. package/dist/skills/index.js +6 -0
  113. package/node_modules/@sigrun/shared/CHANGELOG.md +11 -0
  114. package/node_modules/@sigrun/shared/dist/access.d.ts +4 -2
  115. package/node_modules/@sigrun/shared/dist/access.js +11 -4
  116. package/node_modules/@sigrun/shared/dist/ambient.d.ts +17 -74
  117. package/node_modules/@sigrun/shared/dist/ambient.js +8 -6
  118. package/node_modules/@sigrun/shared/dist/blocks/text.js +16 -2
  119. package/node_modules/@sigrun/shared/dist/blocks/transport.d.ts +4 -0
  120. package/node_modules/@sigrun/shared/dist/blocks/types.d.ts +21 -4
  121. package/node_modules/@sigrun/shared/dist/interactions.d.ts +22 -48
  122. package/node_modules/@sigrun/shared/dist/interactions.js +9 -7
  123. package/node_modules/@sigrun/shared/dist/settings.d.ts +27 -2
  124. package/node_modules/@sigrun/shared/dist/themes.d.ts +2 -3
  125. package/node_modules/@sigrun/shared/dist/themes.js +2 -3
  126. package/node_modules/@sigrun/shared/dist/turns.d.ts +14 -0
  127. package/node_modules/@sigrun/shared/dist/turns.js +53 -3
  128. package/node_modules/@sigrun/shared/dist/views.d.ts +202 -12
  129. package/node_modules/@sigrun/shared/dist/views.js +16 -0
  130. package/node_modules/@sigrun/shared/package.json +1 -1
  131. package/package.json +13 -9
  132. package/webui/dist/assets/index-SO2W4jjk.js +55 -0
  133. package/webui/dist/assets/index-yqkL6BSS.css +1 -0
  134. package/webui/dist/index.html +2 -2
  135. package/CONTEXT.md +0 -163
  136. package/webui/dist/assets/index-CiYInlLx.css +0 -1
  137. package/webui/dist/assets/index-JDfOQV9z.js +0 -52
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Per-resource sharing and content access (core hardening W1 Task 5, spec
3
- * §5.9 ruling 6/10: per-resource sharing; groups cut).
3
+ * §5.9 ruling 6/10: per-resource sharing; multi-user M3b adds groups).
4
4
  *
5
5
  * Part of the shared leaf package (`@sigrun/shared`): the server computes
6
6
  * access through `accessOf`, the clients render the `sharing` state — one
@@ -13,7 +13,7 @@
13
13
  * 2. ownerless resource → household write (everyone);
14
14
  * 3. user === owner → write;
15
15
  * 4. household sharing with a defined user → the shared level;
16
- * 5. people sharing including the user → the shared level;
16
+ * 5. people sharing including the user or one of their groups → the shared level;
17
17
  * 6. otherwise none (hidden: 404 on every :id route, absent from lists).
18
18
  *
19
19
  * Anonymous callers (a principal with no user) get nothing from a shared
@@ -40,8 +40,12 @@ export function accessOf(p, r) {
40
40
  if (p.user !== undefined && r.sharing !== undefined) {
41
41
  if (r.sharing.scope === "household")
42
42
  return r.sharing.level;
43
- if (r.sharing.scope === "people" && r.sharing.users.includes(p.user)) {
44
- return r.sharing.level;
43
+ if (r.sharing.scope === "people") {
44
+ const sharing = r.sharing;
45
+ if (sharing.users.includes(p.user) ||
46
+ p.groups?.some((group) => sharing.groups?.includes(group)) === true) {
47
+ return sharing.level;
48
+ }
45
49
  }
46
50
  }
47
51
  // 6. Hidden.
@@ -65,6 +69,9 @@ export function isSharing(value) {
65
69
  case "people":
66
70
  return (Array.isArray(record.users) &&
67
71
  record.users.every((user) => typeof user === "string") &&
72
+ (record.groups === undefined ||
73
+ (Array.isArray(record.groups) &&
74
+ record.groups.every((group) => typeof group === "string"))) &&
68
75
  (record.level === "read" || record.level === "write"));
69
76
  default:
70
77
  return false;
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * The ambient seam TYPES (spec: docs/approval-ux-seams.md, "Ambient
3
- * surfaces"): the channel-neutral record shapes for persistent
4
- * read-mostly state — statusline slots and panel widgets — written by
5
- * extensions through the host context and rendered by channels
6
- * generically.
3
+ * surfaces"; the block mapping: UI-blocks design §9.2): the channel-neutral record
4
+ * shapes for persistent read-mostly state — statusline slots and panel
5
+ * widgets — written by extensions through the host context and rendered
6
+ * by channels generically.
7
7
  *
8
8
  * The CANONICAL type declarations live here (the shared leaf package);
9
9
  * `src/harness/ambient.ts` imports them (type-only) for the doc
@@ -17,79 +17,16 @@
17
17
  * not silent composition — the pi-zentui chrome-skin lesson).
18
18
  *
19
19
  * Layering mirrors the interaction seam: strict envelope (key, place,
20
- * budget, refresh, expires) around a body composed only from the closed
21
- * ambient primitives (line, rows, gauge, ledger), with opaque `details`.
20
+ * budget, refresh, expires) around a block-tree body (placement
21
+ * `ambient`; `status`/`footer` allow one line), with opaque `details`.
22
+ * The legacy `line`/`rows`/`gauge`/`ledger` primitives are REJECTED
23
+ * (ruling 16: no compatibility layer).
22
24
  */
25
+ import type { Tree } from "./blocks/index.js";
23
26
  import type { JsonObject } from "./json.js";
24
27
  /** Where a channel places the surface. */
25
28
  export type AmbientPlace = "status" | "footer" | "panel";
26
- /** One widget row (primitive B). */
27
- export type AmbientRow = {
28
- readonly id: string;
29
- readonly label: string;
30
- /** Channel-schemed glyph state (todo statuses, agent run states). */
31
- readonly status?: string;
32
- /** Severity ordering/color: 0 = severe .. 2 = quiet. */
33
- readonly severity?: 0 | 1 | 2;
34
- /** Folded away by default (warden's quiet verdicts). */
35
- readonly quiet?: boolean;
36
- readonly detail?: string;
37
- /** Nesting depth for tree rendering (pi-goal-x task tree). */
38
- readonly depth?: number;
39
- /** Dependency ids for marker rendering (rpiv-todo blockedBy). */
40
- readonly deps?: string[];
41
- /** Present-continuous spinner label while the row is active. */
42
- readonly spinnerLabel?: string;
43
- readonly metrics?: AmbientMetrics;
44
- };
45
- /** Numeric metrics with an optional derived-format hint. */
46
- export type AmbientMetrics = {
47
- readonly tokens?: number;
48
- readonly cost?: number;
49
- readonly current?: number;
50
- readonly total?: number;
51
- readonly completed?: number;
52
- /** Derived display hint for numeric fields ("2.5k", currency) — the
53
- * host-reuse lesson from every footer extension. */
54
- readonly format?: "compact" | "usd" | "percent";
55
- };
56
- /** The `line` primitive: one keyed statusline/footer line, optionally segmented. */
57
- export type AmbientLine = {
58
- readonly text: string;
59
- /** Segmented composition for powerline-style slots (pi-powerline,
60
- * pi-zentui): sub-segments with placement zones. */
61
- readonly segments?: {
62
- readonly id: string;
63
- readonly text: string;
64
- readonly place?: "left" | "middle" | "right";
65
- }[];
66
- };
67
- /** The `gauge` primitive: live progress with optional check rows. */
68
- export type AmbientGauge = {
69
- readonly label: string;
70
- readonly percent?: number;
71
- readonly phase?: string;
72
- readonly elapsedMs?: number;
73
- readonly recentOutput?: string[];
74
- /** Check rows (pi-goal-x auditor): row-shaped, same schema as rows[]. */
75
- readonly checks?: AmbientRow[];
76
- };
77
- /** One ledger entry (primitive D): append-style feed with per-entry amend. */
78
- export type AmbientLedgerEntry = {
79
- readonly id: string;
80
- readonly at: number;
81
- readonly kind: string;
82
- readonly summary: string;
83
- readonly detail?: string[];
84
- };
85
- /** The ambient body: ONLY these keys, each from the closed primitives. */
86
- export type AmbientBody = {
87
- readonly line?: AmbientLine;
88
- readonly rows?: AmbientRow[];
89
- readonly gauge?: AmbientGauge;
90
- readonly ledger?: AmbientLedgerEntry[];
91
- };
92
- /** The strict envelope + closed-primitive body of one ambient surface. */
29
+ /** The strict envelope + block-tree body of one ambient surface. */
93
30
  export type AmbientRecord = {
94
31
  /** The keyed slot id ("goal", "rpiv-todos", "fabric"). */
95
32
  readonly key: string;
@@ -105,7 +42,13 @@ export type AmbientRecord = {
105
42
  /** Auto-clear rule: "never" stays until replaced with an empty body. */
106
43
  readonly expires: "never" | "idle";
107
44
  readonly updatedAt: number;
108
- readonly body?: AmbientBody;
45
+ /**
46
+ * The ambient body: a block tree (placement `ambient`). `status` and
47
+ * `footer` allow one line — a single line-level block or one
48
+ * `columns` of one-line blocks. Undefined = the surface is cleared.
49
+ * Legacy-shaped bodies fail validation (ruling 16).
50
+ */
51
+ readonly body?: Tree;
109
52
  /** Opaque extension-owned payload; rendered collapsed, never interpreted. */
110
53
  readonly details?: JsonObject;
111
54
  };
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * The ambient seam TYPES (spec: docs/approval-ux-seams.md, "Ambient
3
- * surfaces"): the channel-neutral record shapes for persistent
4
- * read-mostly state — statusline slots and panel widgets — written by
5
- * extensions through the host context and rendered by channels
6
- * generically.
3
+ * surfaces"; the block mapping: UI-blocks design §9.2): the channel-neutral record
4
+ * shapes for persistent read-mostly state — statusline slots and panel
5
+ * widgets — written by extensions through the host context and rendered
6
+ * by channels generically.
7
7
  *
8
8
  * The CANONICAL type declarations live here (the shared leaf package);
9
9
  * `src/harness/ambient.ts` imports them (type-only) for the doc
@@ -17,7 +17,9 @@
17
17
  * not silent composition — the pi-zentui chrome-skin lesson).
18
18
  *
19
19
  * Layering mirrors the interaction seam: strict envelope (key, place,
20
- * budget, refresh, expires) around a body composed only from the closed
21
- * ambient primitives (line, rows, gauge, ledger), with opaque `details`.
20
+ * budget, refresh, expires) around a block-tree body (placement
21
+ * `ambient`; `status`/`footer` allow one line), with opaque `details`.
22
+ * The legacy `line`/`rows`/`gauge`/`ledger` primitives are REJECTED
23
+ * (ruling 16: no compatibility layer).
22
24
  */
23
25
  export {};
@@ -287,7 +287,14 @@ function renderPage(page, ctx) {
287
287
  let main;
288
288
  const collection = firstCollection(page.main);
289
289
  if (page.empty !== undefined && collection !== undefined) {
290
- const items = collection.items ? resolvePointer(ctx.data, collection.items.$) : undefined;
290
+ // Literal rows (ambient's ruling shape) count as present for the
291
+ // `empty` gate; a binding resolves against the data model.
292
+ const bound = collection.items;
293
+ const items = Array.isArray(collection.items)
294
+ ? collection.items
295
+ : bound === undefined
296
+ ? undefined
297
+ : resolvePointer(ctx.data, bound.$);
291
298
  if (!Array.isArray(items) || items.length === 0) {
292
299
  main = [page.empty.title];
293
300
  if (page.empty.hint !== undefined && page.empty.hint !== "")
@@ -475,7 +482,14 @@ function renderDiff(block) {
475
482
  // ─── Collection ───────────────────────────────────────────────────────────
476
483
  /** Render a `collection` in its display's text shape. */
477
484
  function renderCollection(block, ctx) {
478
- const raw = block.items ? resolvePointer(ctx.data, block.items.$) : undefined;
485
+ // Literal rows (ambient) render as-is; a binding resolves against
486
+ // the data model.
487
+ const bound = block.items;
488
+ const raw = Array.isArray(block.items)
489
+ ? block.items
490
+ : bound === undefined
491
+ ? undefined
492
+ : resolvePointer(ctx.data, bound.$);
479
493
  const items = (Array.isArray(raw) ? raw : []).filter(isRecord);
480
494
  const ordered = sortedItems(items, block.sort);
481
495
  if (block.display === "table" && block.columns !== undefined && block.columns.length > 0) {
@@ -65,6 +65,10 @@ export interface SurfaceTransport {
65
65
  fetchData(since?: number): Promise<SurfaceFetch>;
66
66
  act(body: {
67
67
  action: string;
68
+ /** The action's wire type (submit/edit/move/…), carried alongside
69
+ * the resolved name so a transport can gate on KIND (an edit
70
+ * named like the submit action must never read as a submit). */
71
+ type: string;
68
72
  payload: JsonValue;
69
73
  actionId: string;
70
74
  }): Promise<SurfaceActResult>;
@@ -250,7 +250,10 @@ export interface LogLine {
250
250
  }
251
251
  export interface LogBlock extends BlockBase {
252
252
  readonly type: "log";
253
- readonly lines: Binding;
253
+ /** Literal lines are legal (ambient placement requires them — the
254
+ * ambient envelope carries no data document); placements that
255
+ * resolve bindings keep the binding form. */
256
+ readonly lines: Bindable<readonly JsonValue[]>;
254
257
  readonly follow?: boolean;
255
258
  readonly wrap?: boolean;
256
259
  /** Per-level tones; default `error`→bad, `warn`→warn, `info`→neutral, `debug`→subtle. */
@@ -373,8 +376,10 @@ export interface CollectionTime {
373
376
  }
374
377
  export interface CollectionBlock extends BlockBase {
375
378
  readonly type: "collection";
376
- /** Binding to an array; each item has a unique `id`. */
377
- readonly items: Binding;
379
+ /** Binding to an array (each item has a unique `id`) — or the array
380
+ * itself as literal rows, the only legal shape in ambient placement
381
+ * (the ambient envelope carries no data document). */
382
+ readonly items: Binding | readonly JsonValue[];
378
383
  /** The card template. */
379
384
  readonly item: Card;
380
385
  readonly display: "list" | "table" | "lanes" | "grid" | "calendar";
@@ -425,6 +430,18 @@ export interface FieldBase {
425
430
  readonly maxLength?: number;
426
431
  readonly pattern?: string;
427
432
  }
433
+ export interface ToggleFieldProps {
434
+ /**
435
+ * The value an ABSENT bound value stands for (default false). Set it
436
+ * on toggles whose settings key defaults ON (absent = on): the
437
+ * channel initializes an untouched toggle to the default, and the
438
+ * settings surface's save skips a submitted value EQUAL to the
439
+ * default, so a full-form save never writes the key while the value
440
+ * sits at its default (explicitly flipping OFF the default still
441
+ * writes the key — the write is how the non-default state persists).
442
+ */
443
+ readonly default?: boolean;
444
+ }
428
445
  export interface TextField extends FieldBase {
429
446
  readonly type: "text";
430
447
  readonly kind?: "plain" | "search" | "email" | "url";
@@ -442,7 +459,7 @@ export interface NumberField extends FieldBase {
442
459
  readonly step?: number;
443
460
  readonly format?: Format;
444
461
  }
445
- export interface ToggleField extends FieldBase {
462
+ export interface ToggleField extends FieldBase, ToggleFieldProps {
446
463
  readonly type: "toggle";
447
464
  }
448
465
  export interface ChoiceOption {
@@ -1,8 +1,8 @@
1
1
  /**
2
- * The interaction seam TYPES (spec: docs/approval-ux-seams.md): the
3
- * channel-neutral record shapes for "this conversation is waiting on a
4
- * human decision", written by extensions through the host context and
5
- * answered by channels through one generic route.
2
+ * The interaction seam TYPES (spec: docs/approval-ux-seams.md; the block
3
+ * mapping: UI-blocks design §9.1): the channel-neutral record shapes for "this conversation is
4
+ * waiting on a human decision", written by extensions through the host
5
+ * context and answered by channels through one generic route.
6
6
  *
7
7
  * The CANONICAL type declarations live here (the shared leaf package);
8
8
  * `src/harness/interactions.ts` imports them (type-only) for the doc
@@ -10,60 +10,29 @@
10
10
  * AS-IS (envelope + body + details — never projected), so these shapes
11
11
  * are the channel contract too (see `@sigrun/shared/views`).
12
12
  *
13
- * Layering (strict envelope / closed primitives / opaque details):
13
+ * Layering (strict envelope / block-tree body / opaque details):
14
14
  *
15
15
  * - envelope: kind, callId, status, timestamps, answer — load-bearing for
16
16
  * joining tool slots and lifecycle arbitration. Strictly validated.
17
- * - body: composed ONLY from the closed primitives (text, options,
18
- * fields, diff). Structurally validated; unknown body keys quarantine.
17
+ * - body: a block tree (placement `interaction` — a root `form` whose
18
+ * submit is the answer). Validated by `validateTree`; the legacy
19
+ * `text`/`options`/`fields`/`diff` primitives are REJECTED (ruling 16:
20
+ * no compatibility layer).
19
21
  * - details: opaque extension-owned JSON. Never interpreted.
20
22
  */
21
- import type { JsonObject } from "./json.js";
23
+ import type { Tree } from "./blocks/index.js";
24
+ import type { JsonObject, JsonValue } from "./json.js";
22
25
  /** Lifecycle of one interaction record (spec statuses). */
23
26
  export type InteractionStatus = "pending" | "answered" | "skipped" | "expired" | "cancelled";
24
- /** One choice in the `options` primitive. */
25
- export type InteractionOption = {
26
- readonly id: string;
27
- readonly label: string;
28
- /** Second line under the label (pi-advisor presets, rpiv options). */
29
- readonly description?: string;
30
- /** Channel-schemed color/severity hint: "danger" | "warn" | "ok" | "neutral". */
31
- readonly tone?: "danger" | "warn" | "ok" | "neutral";
32
- /** Pre-selected by the extension's recommendation (pi-interview conviction). */
33
- readonly recommended?: boolean;
34
- };
35
- /** One light-form field in the `fields` primitive. */
36
- export type InteractionField = {
37
- readonly name: string;
38
- readonly type: "string" | "number" | "boolean" | "secret";
39
- readonly label?: string;
40
- readonly required?: boolean;
41
- };
42
- /** The `diff` primitive: before/after content, optionally path-scoped. */
43
- export type InteractionDiff = {
44
- readonly before: string;
45
- readonly after: string;
46
- readonly path?: string;
47
- readonly lang?: string;
48
- };
49
27
  /**
50
- * The interaction body: ONLY these four keys, each built only from the
51
- * closed primitives. Everything else belongs in `details`.
28
+ * What an answer carries (ruling 20): every field's value under its
29
+ * name — a `choice` value is its option id (or ids when `multi`), a
30
+ * `choice` with `allowOther` carries `{other: "…"}`.
52
31
  */
53
- export type InteractionBody = {
54
- /** Prose prompt body (markdown). */
55
- readonly text?: string;
56
- readonly options?: InteractionOption[];
57
- readonly fields?: InteractionField[];
58
- readonly diff?: InteractionDiff;
59
- };
60
- /** What an answer may carry: chosen ids, form values, and/or free text. */
61
32
  export type InteractionAnswer = {
62
- readonly ids?: string[];
63
- readonly values?: Readonly<Record<string, string | number | boolean>>;
64
- readonly text?: string;
33
+ readonly values: Readonly<Record<string, JsonValue>>;
65
34
  };
66
- /** The strict envelope + closed-primitive body of one interaction. */
35
+ /** The strict envelope + block-tree body of one interaction. */
67
36
  export type InteractionRecord = {
68
37
  /** Extension-declared kind id (e.g. "gating.confirm", "questions.form"). */
69
38
  readonly kind: string;
@@ -85,7 +54,12 @@ export type InteractionRecord = {
85
54
  * model-asserted. Absent on the naive single-operator path (no user).
86
55
  */
87
56
  readonly answeredByUser?: string;
88
- readonly body?: InteractionBody;
57
+ /**
58
+ * The interaction body: a block tree (placement `interaction` — a
59
+ * root `form` whose `submit` is the answer). Legacy-shaped bodies
60
+ * fail validation (ruling 16).
61
+ */
62
+ readonly body?: Tree;
89
63
  /** Opaque extension-owned payload; rendered collapsed, never interpreted. */
90
64
  readonly details?: JsonObject;
91
65
  };
@@ -1,8 +1,8 @@
1
1
  /**
2
- * The interaction seam TYPES (spec: docs/approval-ux-seams.md): the
3
- * channel-neutral record shapes for "this conversation is waiting on a
4
- * human decision", written by extensions through the host context and
5
- * answered by channels through one generic route.
2
+ * The interaction seam TYPES (spec: docs/approval-ux-seams.md; the block
3
+ * mapping: UI-blocks design §9.1): the channel-neutral record shapes for "this conversation is
4
+ * waiting on a human decision", written by extensions through the host
5
+ * context and answered by channels through one generic route.
6
6
  *
7
7
  * The CANONICAL type declarations live here (the shared leaf package);
8
8
  * `src/harness/interactions.ts` imports them (type-only) for the doc
@@ -10,12 +10,14 @@
10
10
  * AS-IS (envelope + body + details — never projected), so these shapes
11
11
  * are the channel contract too (see `@sigrun/shared/views`).
12
12
  *
13
- * Layering (strict envelope / closed primitives / opaque details):
13
+ * Layering (strict envelope / block-tree body / opaque details):
14
14
  *
15
15
  * - envelope: kind, callId, status, timestamps, answer — load-bearing for
16
16
  * joining tool slots and lifecycle arbitration. Strictly validated.
17
- * - body: composed ONLY from the closed primitives (text, options,
18
- * fields, diff). Structurally validated; unknown body keys quarantine.
17
+ * - body: a block tree (placement `interaction` — a root `form` whose
18
+ * submit is the answer). Validated by `validateTree`; the legacy
19
+ * `text`/`options`/`fields`/`diff` primitives are REJECTED (ruling 16:
20
+ * no compatibility layer).
19
21
  * - details: opaque extension-owned JSON. Never interpreted.
20
22
  */
21
23
  export {};
@@ -149,6 +149,15 @@ export interface ServerSettings {
149
149
  readonly workerDeadlineMs?: number;
150
150
  readonly queueMax?: number;
151
151
  };
152
+ /** The multi-user execution jail (M1). `mode` and `osUsers` are restart-required. */
153
+ readonly isolation?: {
154
+ /** `SIGRUN_ISOLATION` — auto, always, or off; default auto. */
155
+ readonly mode?: "auto" | "always" | "off";
156
+ /** Per-user uid/gid isolation; default false. */
157
+ readonly osUsers?: boolean;
158
+ /** Idle worker lifetime in minutes; default 10. */
159
+ readonly workerIdleMinutes?: number;
160
+ };
152
161
  /**
153
162
  * The provider plugins' runtime knobs (provider plugins Task 5):
154
163
  * `SIGRUN_PROVIDER_REFRESH_MINUTES` — the interval refresh cadence
@@ -174,6 +183,8 @@ export interface ServerSettings {
174
183
  readonly auth?: {
175
184
  /** `SIGRUN_AUTH` — csv of `token`/`proxy`; absent = derive from the listen host. */
176
185
  readonly methods?: readonly ("token" | "proxy")[];
186
+ /** Browser-session lifetime in days. Positive integer; default 30; LIVE. */
187
+ readonly sessionDays?: number;
177
188
  readonly proxy?: {
178
189
  readonly trustedProxies?: readonly string[];
179
190
  readonly secretHeader?: string;
@@ -202,6 +213,16 @@ export interface ServerSettings {
202
213
  * U1 blocks gallery, U3's development error bodies, and U4.
203
214
  */
204
215
  readonly dev?: boolean;
216
+ /**
217
+ * The supervised flag (UI-blocks U4 Task 2, spec §13.3):
218
+ * `SIGRUN_SUPERVISED` (strict true/false, `1`/`0` accepted) over this
219
+ * key, default `false`. `true` promises a supervisor restarts the
220
+ * process after an intentional `exit 0` — the Helm chart and docker
221
+ * compose set the env (both restart the container); the Dockerfile
222
+ * deliberately does NOT. Gates `POST /v1/admin/restart`; `/v1/meta`
223
+ * reports it (`supervised`).
224
+ */
225
+ readonly supervised?: boolean;
205
226
  /**
206
227
  * The backup target — exactly one of the two shapes (mirroring the
207
228
  * env rule: `SIGRUN_BACKUP_PATH` XOR `SIGRUN_BACKUP_S3_BUCKET`), plus
@@ -268,8 +289,8 @@ export interface NotificationsSettings {
268
289
  readonly runSettled?: "unwatched" | "cron" | "always" | "never";
269
290
  }
270
291
  /**
271
- * The built-in extensions toggles (Task 2, pi parity): codemode, MCP and
272
- * tool search ship WITH core. Each key defaults to `true`; `false`
292
+ * The built-in extensions toggles: codemode, MCP, tool search and OAuth
293
+ * ship WITH core. Each key defaults to `true`; `false`
273
294
  * removes the built-in entirely (absent from `/v1/extensions`, its config
274
295
  * ignored with one log line). RESTART-REQUIRED: the built-in set resolves
275
296
  * at boot, like `extensions`.
@@ -278,6 +299,10 @@ export interface BuiltinExtensionsSettings {
278
299
  readonly codemode?: boolean;
279
300
  readonly mcp?: boolean;
280
301
  readonly toolSearch?: boolean;
302
+ /** Gateway (gateway C3): the built-in engine, default true. */
303
+ readonly gateway?: boolean;
304
+ /** OAuth login providers (multi-user M3b), default true. */
305
+ readonly oauth?: boolean;
281
306
  }
282
307
  /**
283
308
  * The persisted shape: HarnessSettings pass-throughs + sigrun fields.
@@ -7,9 +7,8 @@
7
7
  * consume this file from a single source; any import would drag a
8
8
  * dependency only one client can load or resolve.
9
9
  *
10
- * Colour values are copied verbatim from the palette mockup
11
- * (docs/superpowers/specs/assets/2026-10-17-ui-refresh/palettes.html),
12
- * with the mockup ids renamed to the plan ids: palette `iron` →
10
+ * Colour values are copied verbatim from the UI-refresh palette mockup
11
+ * (palettes.html), with the mockup ids renamed to the plan ids: palette `iron` →
13
12
  * `iron-ember`, `aurora` → `polar-night`, `birch` → `birch-moss`;
14
13
  * accent `amber` → `baltic-amber`; token key `surface-2` → `surface2`.
15
14
  */
@@ -7,9 +7,8 @@
7
7
  * consume this file from a single source; any import would drag a
8
8
  * dependency only one client can load or resolve.
9
9
  *
10
- * Colour values are copied verbatim from the palette mockup
11
- * (docs/superpowers/specs/assets/2026-10-17-ui-refresh/palettes.html),
12
- * with the mockup ids renamed to the plan ids: palette `iron` →
10
+ * Colour values are copied verbatim from the UI-refresh palette mockup
11
+ * (palettes.html), with the mockup ids renamed to the plan ids: palette `iron` →
13
12
  * `iron-ember`, `aurora` → `polar-night`, `birch` → `birch-moss`;
14
13
  * accent `amber` → `baltic-amber`; token key `surface-2` → `surface2`.
15
14
  */
@@ -24,6 +24,14 @@
24
24
  /** The user-entry kind an expanded `/skill:` or template input is placed
25
25
  * as (see the module doc); grouping treats it exactly like `pi.user`. */
26
26
  export declare const EXPANDED_USER_KIND = "sigrun.user.expanded";
27
+ /** The chat-blocks entry kinds (UI-blocks U5, spec §8.2/§8.3/§8.4) the
28
+ * grouping recognises. The server-side twins live in
29
+ * `src/harness/blocks/entries.ts` (this module imports nothing, so the
30
+ * tokens duplicate by design — one spelling per kind, pinned by the
31
+ * grouping tests). */
32
+ export declare const BLOCKS_KIND = "sigrun.blocks";
33
+ export declare const BLOCKS_NOTE_KIND = "sigrun.blocks.note";
34
+ export declare const BLOCKS_ANSWER_KIND = "sigrun.blocks.answer";
27
35
  export interface TurnBlock {
28
36
  readonly type: string;
29
37
  readonly text?: string;
@@ -98,6 +106,12 @@ export type TimelineEvent = {
98
106
  } | {
99
107
  readonly kind: "text";
100
108
  readonly text: string;
109
+ } | {
110
+ readonly kind: "blocks";
111
+ readonly entryId: number;
112
+ } | {
113
+ readonly kind: "note";
114
+ readonly entryId: number;
101
115
  };
102
116
  export declare function groupTurns<E extends TurnEntry>(entries: readonly E[]): Turn<E>[];
103
117
  export declare function toolPreview(name: string, args: unknown): string;
@@ -24,6 +24,22 @@
24
24
  /** The user-entry kind an expanded `/skill:` or template input is placed
25
25
  * as (see the module doc); grouping treats it exactly like `pi.user`. */
26
26
  export const EXPANDED_USER_KIND = "sigrun.user.expanded";
27
+ /** The chat-blocks entry kinds (UI-blocks U5, spec §8.2/§8.3/§8.4) the
28
+ * grouping recognises. The server-side twins live in
29
+ * `src/harness/blocks/entries.ts` (this module imports nothing, so the
30
+ * tokens duplicate by design — one spelling per kind, pinned by the
31
+ * grouping tests). */
32
+ export const BLOCKS_KIND = "sigrun.blocks";
33
+ export const BLOCKS_NOTE_KIND = "sigrun.blocks.note";
34
+ export const BLOCKS_ANSWER_KIND = "sigrun.blocks.answer";
35
+ /** The `show_blocks`/`update_blocks` tool names: a call to one of these
36
+ * never joins the turn's `tools` or the timeline — the block surface it
37
+ * created IS the visible artifact (the activity line would otherwise
38
+ * count its own rendering machinery as activity). */
39
+ const BLOCKS_TOOLS = new Set([
40
+ "show_blocks",
41
+ "update_blocks",
42
+ ]);
27
43
  /** Narrow one entry to its expanded-user `data` — defined exactly when
28
44
  * the kind is {@link EXPANDED_USER_KIND} and `typed` is a non-empty
29
45
  * string; undefined for every other shape (a plain `pi.user`, missing
@@ -105,10 +121,17 @@ export function groupTurns(entries) {
105
121
  });
106
122
  }
107
123
  else if (block.type === "toolCall") {
124
+ const name = block.name ?? "";
125
+ // A blocks tool's call is invisible in the transcript: the
126
+ // `sigrun.blocks` entry it appends is the artifact. The
127
+ // RESULT entry still joins by callId below (matching only
128
+ // pending tools, so its join is a harmless no-op).
129
+ if (BLOCKS_TOOLS.has(name))
130
+ continue;
108
131
  const call = {
109
132
  callId: block.id ?? "",
110
- name: block.name ?? "",
111
- preview: toolPreview(block.name ?? "", block.arguments),
133
+ name,
134
+ preview: toolPreview(name, block.arguments),
112
135
  isError: false,
113
136
  };
114
137
  pending.tools.push(call);
@@ -131,8 +154,35 @@ export function groupTurns(entries) {
131
154
  }
132
155
  continue;
133
156
  }
157
+ // U5 Task 4: the chat-blocks kinds are RUN ARTIFACTS, never
158
+ // standalone records (spec §8.2/§8.3). With a pending assistant
159
+ // turn they ride it — a boundary note mid-run appends its event
160
+ // and does NOT flush. With NONE (auto-compaction's pi.compaction
161
+ // flushed the run; the trailing note/blocks entry lands after the
162
+ // flush) they START one: an assistant turn with no text whose
163
+ // timeline still renders the block/note rows — falling through to
164
+ // `other` would print "unsupported entry kind".
165
+ // `sigrun.blocks.answer` is a user turn (§8.4: the answer submits
166
+ // through the normal path).
167
+ if (entry.kind === BLOCKS_KIND || entry.kind === BLOCKS_NOTE_KIND) {
168
+ pending ??= {
169
+ entries: [],
170
+ thinking: [],
171
+ tools: [],
172
+ texts: [],
173
+ timeline: [],
174
+ };
175
+ pending.entries.push(entry);
176
+ pending.timeline.push({
177
+ kind: entry.kind === BLOCKS_KIND ? "blocks" : "note",
178
+ entryId: entry.id,
179
+ });
180
+ continue;
181
+ }
134
182
  flush();
135
- if (entry.kind === "pi.user" || entry.kind === EXPANDED_USER_KIND) {
183
+ if (entry.kind === "pi.user" ||
184
+ entry.kind === EXPANDED_USER_KIND ||
185
+ entry.kind === BLOCKS_ANSWER_KIND) {
136
186
  turns.push({ kind: "user", entry });
137
187
  }
138
188
  else {