@sigrun/sigrun 0.1.4 → 0.1.6

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 (82) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/README.md +24 -2
  3. package/THIRD_PARTY_NOTICES.md +23 -0
  4. package/dist/blocks/builders.js +125 -0
  5. package/dist/blocks/images.js +57 -0
  6. package/dist/blocks/index.js +20 -0
  7. package/dist/blocks/limits.js +26 -0
  8. package/dist/blocks/placements.js +99 -0
  9. package/dist/blocks/schema.js +559 -0
  10. package/dist/blocks/validate.js +602 -0
  11. package/dist/config.js +31 -0
  12. package/dist/extension.d.ts +476 -9
  13. package/dist/harness/agent-env.js +62 -0
  14. package/dist/harness/boot.js +8 -1
  15. package/dist/harness/event-bus.js +33 -0
  16. package/dist/harness/identity/users.js +3 -2
  17. package/dist/harness/loader.js +369 -13
  18. package/dist/harness/packages.js +71 -2
  19. package/dist/harness/seam-host.js +69 -2
  20. package/dist/harness/settings.js +5 -0
  21. package/dist/harness/workspace.js +6 -4
  22. package/dist/jobs/fire.js +11 -0
  23. package/dist/jobs/spec.js +19 -0
  24. package/dist/serve.js +29 -39
  25. package/dist/server/auth/pipeline.js +16 -1
  26. package/dist/server/conversation-ops.js +482 -10
  27. package/dist/server/gateway.js +122 -0
  28. package/dist/server/helpers.js +26 -0
  29. package/dist/server/host-bridge.js +141 -4
  30. package/dist/server/index.js +27 -0
  31. package/dist/server/notifications.js +6 -2
  32. package/dist/server/routes/conversations.js +37 -93
  33. package/dist/server/routes/events.js +39 -0
  34. package/dist/server/routes/ext-assets.js +84 -0
  35. package/dist/server/routes/extensions.js +31 -1
  36. package/dist/server/routes/files.js +38 -83
  37. package/dist/server/routes/meta.js +35 -1
  38. package/dist/server/routes/providers.js +5 -21
  39. package/dist/server/routes/submissions.js +36 -111
  40. package/dist/server/routes/surfaces.js +387 -0
  41. package/dist/server/routes/users.js +6 -1
  42. package/dist/server/session-meta.js +12 -6
  43. package/dist/server/shutdown.js +85 -0
  44. package/dist/server/surfaces/access-cache.js +43 -0
  45. package/dist/server/surfaces/dedup.js +67 -0
  46. package/dist/server/surfaces/instance.js +42 -0
  47. package/dist/server/surfaces/mime.js +73 -0
  48. package/dist/server/surfaces/rate-limit.js +63 -0
  49. package/dist/server/surfaces/service.js +493 -0
  50. package/dist/server/surfaces/watchers.js +94 -0
  51. package/node_modules/@sigrun/shared/CHANGELOG.md +9 -0
  52. package/node_modules/@sigrun/shared/dist/blocks/format.d.ts +28 -0
  53. package/node_modules/@sigrun/shared/dist/blocks/format.js +199 -0
  54. package/node_modules/@sigrun/shared/dist/blocks/icons.d.ts +12 -0
  55. package/node_modules/@sigrun/shared/dist/blocks/icons.js +64 -0
  56. package/node_modules/@sigrun/shared/dist/blocks/index.d.ts +25 -0
  57. package/node_modules/@sigrun/shared/dist/blocks/index.js +21 -0
  58. package/node_modules/@sigrun/shared/dist/blocks/instance.d.ts +29 -0
  59. package/node_modules/@sigrun/shared/dist/blocks/instance.js +51 -0
  60. package/node_modules/@sigrun/shared/dist/blocks/lttb.d.ts +16 -0
  61. package/node_modules/@sigrun/shared/dist/blocks/lttb.js +67 -0
  62. package/node_modules/@sigrun/shared/dist/blocks/ops.d.ts +44 -0
  63. package/node_modules/@sigrun/shared/dist/blocks/ops.js +294 -0
  64. package/node_modules/@sigrun/shared/dist/blocks/text.d.ts +43 -0
  65. package/node_modules/@sigrun/shared/dist/blocks/text.js +790 -0
  66. package/node_modules/@sigrun/shared/dist/blocks/transport.d.ts +77 -0
  67. package/node_modules/@sigrun/shared/dist/blocks/transport.js +11 -0
  68. package/node_modules/@sigrun/shared/dist/blocks/types.d.ts +541 -0
  69. package/node_modules/@sigrun/shared/dist/blocks/types.js +17 -0
  70. package/node_modules/@sigrun/shared/dist/jobs.d.ts +9 -0
  71. package/node_modules/@sigrun/shared/dist/live.d.ts +71 -0
  72. package/node_modules/@sigrun/shared/dist/live.js +82 -0
  73. package/node_modules/@sigrun/shared/dist/session-meta.d.ts +19 -3
  74. package/node_modules/@sigrun/shared/dist/settings.d.ts +7 -0
  75. package/node_modules/@sigrun/shared/dist/views.d.ts +81 -3
  76. package/node_modules/@sigrun/shared/package.json +10 -2
  77. package/package.json +11 -5
  78. package/webui/dist/assets/index-CiYInlLx.css +1 -0
  79. package/webui/dist/assets/index-JDfOQV9z.js +52 -0
  80. package/webui/dist/index.html +2 -2
  81. package/webui/dist/assets/index-BiSMljer.css +0 -1
  82. package/webui/dist/assets/index-hVLvsizV.js +0 -50
@@ -0,0 +1,602 @@
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 { BLOCK_SET_VERSION } from "@sigrun/shared/blocks";
20
+ import { ACTION_BUTTON_VALIDATOR, ACTION_VALIDATOR, BLOCK_VALIDATORS, CARD_VALIDATOR, FIELD_VALIDATORS, } from "./schema.js";
21
+ import { PLACEMENT_RULES } from "./placements.js";
22
+ import { LIMITS, jsonBytes } from "./limits.js";
23
+ import { checkImageSource } from "./images.js";
24
+ export { PLACEMENT_RULES } from "./placements.js";
25
+ export { LIMITS } from "./limits.js";
26
+ /** The placements the block set defines (spec §3). */
27
+ export const VALID_PLACEMENTS = [
28
+ "view",
29
+ "panel",
30
+ "settings",
31
+ "chat",
32
+ "interaction",
33
+ "ambient",
34
+ ];
35
+ function isRec(value) {
36
+ return typeof value === "object" && value !== null && !Array.isArray(value);
37
+ }
38
+ function isBinding(value) {
39
+ return isRec(value) && typeof value.$ === "string";
40
+ }
41
+ /** Path segment join: `root` + `main` → `root.main`, + `2` → `root.main[2]`. */
42
+ function joinPath(base, key) {
43
+ return /^\d+$/.test(key) ? `${base}[${key}]` : `${base}.${key}`;
44
+ }
45
+ /** Handler-name props across the block set (spec §6.1). */
46
+ const HANDLER_PROPS = ["onMove", "onCheck", "onExpand", "onResize", "onCreate", "onSubmit", "onUpload"];
47
+ /** Block types the ambient one-line rule treats as line-level (§9.2). */
48
+ const AMBIENT_LINE_BLOCKS = new Set(["markdown", "stat", "meter", "log"]);
49
+ /** Structural props that must be literals, per block type (spec §4.2). */
50
+ /** Structural props that must be literals, per block type (spec §4.2). */
51
+ const STRUCTURAL_LITERALS = {
52
+ page: [],
53
+ stack: [],
54
+ columns: [],
55
+ grid: [],
56
+ section: [],
57
+ markdown: [],
58
+ fields: [],
59
+ stat: [],
60
+ callout: [],
61
+ image: [],
62
+ chart: [],
63
+ meter: [],
64
+ log: [],
65
+ tree: [],
66
+ diff: [],
67
+ details: [],
68
+ collection: ["display", "groupBy", "groups", "reorder", "select", "columns", "onMove", "onCheck"],
69
+ form: ["mode", "onSubmit"],
70
+ };
71
+ /** A field's `name` is always structural (§4.2). */
72
+ const FIELD_STRUCTURAL_LITERALS = ["name"];
73
+ function push(ctx, path, message) {
74
+ ctx.errors.push({ path, message });
75
+ }
76
+ /** Convert a JSON-pointer instance path to the spec's dot shape ("/text" → ".text"). */
77
+ function relPath(instancePath) {
78
+ return instancePath
79
+ .split("/")
80
+ .filter((s) => s.length > 0)
81
+ .map((s) => (/^\d+$/.test(s) ? `[${s}]` : `.${s}`))
82
+ .join("");
83
+ }
84
+ /** Append a node's compiled-validator errors, prefixed with its walk path. */
85
+ function checkSchema(ctx, validator, node, path) {
86
+ for (const raw of validator.Errors(node)) {
87
+ const rel = relPath(raw.instancePath);
88
+ const message = raw.message ?? "invalid value";
89
+ // Name the prop the error sits on ("icon: must be equal to one of
90
+ // the allowed values") so the message reads at the path it lands at.
91
+ const segments = raw.instancePath.split("/").filter((s) => s.length > 0);
92
+ const lastProp = [...segments].reverse().find((s) => !/^\d+$/.test(s));
93
+ push(ctx, `${path}${rel}`, lastProp ? `${lastProp}: ${message}` : message);
94
+ }
95
+ }
96
+ /** Check a handler-name prop: literal, and in the handlers set when given. */
97
+ function checkHandler(ctx, path, name, prop) {
98
+ if (name === undefined || name === null)
99
+ return;
100
+ if (isRec(name)) {
101
+ push(ctx, path, `${prop} must be a literal (a handler name), not a binding`);
102
+ return;
103
+ }
104
+ if (typeof name !== "string")
105
+ return; // the schema already flagged it
106
+ if (ctx.handlers && !ctx.handlers.has(name)) {
107
+ push(ctx, path, `no handler named ${name}`);
108
+ }
109
+ }
110
+ /** An `Action` object: shape via its validator, then name/turn/url rules. */
111
+ function checkAction(ctx, action, path) {
112
+ if (!isRec(action))
113
+ return;
114
+ checkSchema(ctx, ACTION_VALIDATOR, action, path);
115
+ const type = action.type;
116
+ if (type === "submit" || type === "move" || type === "edit") {
117
+ if (isRec(action.name)) {
118
+ push(ctx, joinPath(path, "name"), "action name must be a literal");
119
+ }
120
+ else if (typeof action.name === "string" && ctx.handlers && !ctx.handlers.has(action.name)) {
121
+ push(ctx, joinPath(path, "name"), `no handler named ${action.name}`);
122
+ }
123
+ }
124
+ if (ctx.placement !== "chat" && action.turn === true) {
125
+ push(ctx, joinPath(path, "turn"), "`turn` is valid only in the chat placement");
126
+ }
127
+ if (type === "open" && typeof action.url === "string" && !/^(https?:|mailto:)/.test(action.url)) {
128
+ push(ctx, joinPath(path, "url"), `open url must be https:, http: or mailto: (got "${action.url.slice(0, 40)}")`);
129
+ }
130
+ }
131
+ /** An action-button list (`page.actions`, `callout.actions`, card `actions`). */
132
+ function checkActionButtons(ctx, buttons, path) {
133
+ if (!Array.isArray(buttons))
134
+ return;
135
+ buttons.forEach((button, i) => {
136
+ if (!isRec(button))
137
+ return;
138
+ const bPath = `${path}[${i}]`;
139
+ checkSchema(ctx, ACTION_BUTTON_VALIDATOR, button, bPath);
140
+ if (isBinding(button.label))
141
+ checkBinding(ctx, button.label, joinPath(bPath, "label"));
142
+ checkAction(ctx, button.action, joinPath(bPath, "action"));
143
+ });
144
+ }
145
+ /** Relative `./` bindings are legal only inside a card template (§4.2). */
146
+ function checkBinding(ctx, binding, path) {
147
+ if (!ctx.itemTemplate && binding.$.startsWith("./")) {
148
+ push(ctx, joinPath(path, "$"), `relative binding "${binding.$}" is only valid inside a collection item template`);
149
+ }
150
+ }
151
+ /**
152
+ * Any relative `./` binding on this node (§4.2): every prop value is
153
+ * scanned — the binding-only props (`items`, `lines`, `nodes`, `rows`,
154
+ * `series[].data`, `submitLabel`, `copy.text`) bind just like the
155
+ * bindable literals, so a hardcoded prop list would miss them. Absolute
156
+ * `/` bindings stay legal everywhere; relative ones only inside item
157
+ * templates. `skip` names props whose own walk already reported (the
158
+ * structural literals above carry their more precise message).
159
+ */
160
+ function checkNodeRelativeBindings(ctx, node, path, skip) {
161
+ if (ctx.itemTemplate)
162
+ return;
163
+ for (const [prop, value] of Object.entries(node)) {
164
+ if (skip.has(prop))
165
+ continue;
166
+ if (isBinding(value)) {
167
+ checkBinding(ctx, value, joinPath(path, prop));
168
+ }
169
+ else if (Array.isArray(value)) {
170
+ // rows, series, meta, badges: bindings nested one level in
171
+ // literal arrays (e.g. `series[].data`)
172
+ value.forEach((element, i) => {
173
+ if (isBinding(element)) {
174
+ checkBinding(ctx, element, joinPath(joinPath(path, prop), String(i)));
175
+ }
176
+ else if (isRec(element)) {
177
+ for (const [k, v] of Object.entries(element)) {
178
+ if (isBinding(v)) {
179
+ checkBinding(ctx, v, joinPath(joinPath(joinPath(path, prop), String(i)), k));
180
+ }
181
+ }
182
+ }
183
+ });
184
+ }
185
+ }
186
+ }
187
+ /** One field node (spec §5.6). `path` is the field's own path. */
188
+ function checkField(ctx, node, path) {
189
+ const type = node.type;
190
+ if (typeof type !== "string") {
191
+ // `type` is a structural literal (§4.2); a missing or bound type is
192
+ // a soundness hole, never a silent pass
193
+ push(ctx, joinPath(path, "type"), `field type must be a literal string`);
194
+ return;
195
+ }
196
+ const rules = PLACEMENT_RULES[ctx.placement];
197
+ // placement: allowed field types (forms only exist where forms exist)
198
+ if (!rules.fields.has(type)) {
199
+ push(ctx, path, `${type} field is not allowed in ${ctx.placement}`);
200
+ return;
201
+ }
202
+ // schema
203
+ const validator = FIELD_VALIDATORS.get(type);
204
+ if (validator)
205
+ checkSchema(ctx, validator, node, path);
206
+ // structural literals
207
+ for (const prop of FIELD_STRUCTURAL_LITERALS) {
208
+ if (isBinding(node[prop])) {
209
+ push(ctx, joinPath(path, prop), `field name must be a literal, not a binding`);
210
+ }
211
+ }
212
+ // bindable props: every relative `./` binding on the field (§4.2);
213
+ // the structural-literal props above already carry their message
214
+ checkNodeRelativeBindings(ctx, node, path, new Set([...FIELD_STRUCTURAL_LITERALS, "type"]));
215
+ // handler names
216
+ if (node.onUpload !== undefined) {
217
+ checkHandler(ctx, joinPath(path, "onUpload"), node.onUpload, "onUpload");
218
+ }
219
+ // list fields recurse into one entry's field
220
+ if (type === "list" && isRec(node.item)) {
221
+ checkField(ctx, node.item, joinPath(path, "item"));
222
+ }
223
+ }
224
+ /** A card template (spec §5.5.1): relative bindings are legal throughout. */
225
+ function checkCard(ctx, card, path) {
226
+ if (!isRec(card))
227
+ return;
228
+ checkSchema(ctx, CARD_VALIDATOR, card, path);
229
+ if (Array.isArray(card.actions)) {
230
+ checkActionButtons(ctx, card.actions, joinPath(path, "actions"));
231
+ }
232
+ }
233
+ /**
234
+ * One block node. `path` is the node's own path; `rootDepth` marks the
235
+ * tree's root block (root constraints apply there only).
236
+ */
237
+ function checkBlock(ctx, node, path, rootDepth) {
238
+ const type = node.type;
239
+ if (typeof type !== "string") {
240
+ // `type` is a structural literal (§4.2); a missing or bound type is
241
+ // a soundness hole, never a silent pass
242
+ push(ctx, joinPath(path, "type"), `block type must be a literal string`);
243
+ return;
244
+ }
245
+ const rules = PLACEMENT_RULES[ctx.placement];
246
+ // unknown block types (§4.4) first: `carousel` is not a block of this
247
+ // set at all, so its error names the type, at the type prop's path
248
+ if (!BLOCK_VALIDATORS.has(type)) {
249
+ if (FIELD_VALIDATORS.has(type)) {
250
+ push(ctx, joinPath(path, "type"), `a ${type} field is not a block; fields belong inside a form`);
251
+ }
252
+ else {
253
+ push(ctx, joinPath(path, "type"), `unknown block type "${type}"`);
254
+ }
255
+ return;
256
+ }
257
+ // placement: allowed blocks
258
+ if (!rules.blocks.has(type)) {
259
+ push(ctx, path, `${type} block is not allowed in ${ctx.placement}`);
260
+ return; // an illegal block's children are noise
261
+ }
262
+ // root constraints
263
+ if (rootDepth && rules.rootBlock && type !== rules.rootBlock) {
264
+ push(ctx, path, `the root block of a ${ctx.placement} surface must be ${rules.rootBlock}`);
265
+ }
266
+ if (type === "page" && !rules.pageSidebar && node.sidebar !== undefined) {
267
+ push(ctx, joinPath(path, "sidebar"), `page.sidebar is not allowed in ${ctx.placement}`);
268
+ }
269
+ // structural literals (§4.2): checked before the schema so the
270
+ // precise "must be a literal" message is the first error at its path
271
+ for (const prop of STRUCTURAL_LITERALS[type] ?? []) {
272
+ if (isBinding(node[prop])) {
273
+ const isHandler = HANDLER_PROPS.includes(prop);
274
+ push(ctx, joinPath(path, prop), isHandler
275
+ ? `${prop} must be a literal (a handler name), not a binding`
276
+ : `${prop} must be a literal, not a binding`);
277
+ }
278
+ }
279
+ // schema
280
+ const validator = BLOCK_VALIDATORS.get(type);
281
+ if (validator)
282
+ checkSchema(ctx, validator, node, path);
283
+ // collection-specific rules
284
+ if (type === "collection") {
285
+ const display = node.display;
286
+ if (typeof display === "string" &&
287
+ rules.collectionDisplays &&
288
+ !rules.collectionDisplays.includes(display)) {
289
+ push(ctx, joinPath(path, "display"), `collection display ${display} is not allowed in ${ctx.placement} (allowed: ${rules.collectionDisplays.join(", ")})`);
290
+ }
291
+ // reorder requires onMove (spec §5.5)
292
+ if (node.reorder !== undefined && node.onMove === undefined) {
293
+ push(ctx, joinPath(path, "reorder"), "`reorder` requires `onMove`");
294
+ }
295
+ // the card template: relative bindings legal inside it
296
+ if (isRec(node.item)) {
297
+ checkCard({ ...ctx, itemTemplate: true }, node.item, joinPath(path, "item"));
298
+ }
299
+ // bulk actions carry small forms (§5.5); their subtrees are NOT item
300
+ // templates, so relative bindings inside them fail — correct.
301
+ if (Array.isArray(node.bulkActions)) {
302
+ node.bulkActions.forEach((bulk, i) => {
303
+ if (!isRec(bulk))
304
+ return;
305
+ const bPath = joinPath(joinPath(path, "bulkActions"), String(i));
306
+ if (bulk.onSubmit !== undefined) {
307
+ checkHandler(ctx, joinPath(bPath, "onSubmit"), bulk.onSubmit, "onSubmit");
308
+ }
309
+ if (isRec(bulk.form)) {
310
+ const formPath = joinPath(joinPath(bPath, "form"), "root");
311
+ if (isRec(bulk.form.root)) {
312
+ // a nested form is never the surface's root: the placement's
313
+ // root-block constraint must not re-apply to it (§5.7)
314
+ checkBlock(ctx, bulk.form.root, formPath, false);
315
+ }
316
+ }
317
+ });
318
+ }
319
+ }
320
+ // handler names
321
+ for (const prop of HANDLER_PROPS) {
322
+ if (node[prop] !== undefined) {
323
+ checkHandler(ctx, joinPath(path, prop), node[prop], prop);
324
+ }
325
+ }
326
+ if (type === "collection" && isRec(node.time)) {
327
+ const timePath = joinPath(path, "time");
328
+ for (const prop of ["onResize", "onCreate"]) {
329
+ if (node.time[prop] !== undefined) {
330
+ checkHandler(ctx, joinPath(timePath, prop), node.time[prop], prop);
331
+ }
332
+ }
333
+ }
334
+ // ids (§4.1): unique, and required on blocks carrying actions/inputs
335
+ const id = node.id;
336
+ if (typeof id === "string") {
337
+ if (ctx.ids.has(id)) {
338
+ push(ctx, path, `duplicate id "${id}" (first seen at ${ctx.ids.get(id) ?? "root"})`);
339
+ }
340
+ else {
341
+ ctx.ids.set(id, path);
342
+ }
343
+ }
344
+ else if (id === undefined && carriesActionsOrInputs(type, node)) {
345
+ push(ctx, path, `an id is required on ${type} blocks that carry actions or inputs`);
346
+ }
347
+ // image sources (§11.1)
348
+ if (type === "image" && typeof node.src === "string") {
349
+ const check = checkImageSource(node.src);
350
+ if (!check.ok)
351
+ push(ctx, joinPath(path, "src"), check.message);
352
+ }
353
+ // bindable props: every relative `./` binding on the node (§4.2)
354
+ const structural = STRUCTURAL_LITERALS[type] ?? [];
355
+ checkNodeRelativeBindings(ctx, node, path, new Set(structural));
356
+ // onActivate carries an Action (§6.1): same rules as button actions
357
+ if (node.onActivate !== undefined) {
358
+ checkAction(ctx, node.onActivate, joinPath(path, "onActivate"));
359
+ }
360
+ // actions lists
361
+ if (Array.isArray(node.actions)) {
362
+ checkActionButtons(ctx, node.actions, joinPath(path, "actions"));
363
+ }
364
+ // form: turn is chat-only (§6.2)
365
+ if (type === "form" && ctx.placement !== "chat" && node.turn === true) {
366
+ push(ctx, joinPath(path, "turn"), "`turn` is valid only in the chat placement");
367
+ }
368
+ // children: blocks and (inside forms/sections) fields
369
+ if (Array.isArray(node.children)) {
370
+ const wrapperParent = type === "columns" || type === "grid";
371
+ node.children.forEach((child, i) => {
372
+ if (!isRec(child))
373
+ return;
374
+ // columns/grid children are `{weight?, block}` wrappers with no
375
+ // `type` of their own; the wrapper walk below validates `block`.
376
+ // Other parents (stack, section, form…) type their children as
377
+ // blocks/fields directly, so a stray record-valued `block` prop
378
+ // must NOT skip them.
379
+ if (wrapperParent && isRec(child.block))
380
+ return;
381
+ if (wrapperParent && !isRec(child.block)) {
382
+ // a wrapper child without `block` (e.g. {weight: 1}): the
383
+ // parent schema's required-property error is the precise one
384
+ return;
385
+ }
386
+ const cPath = joinPath(joinPath(path, "children"), String(i));
387
+ const childType = child.type;
388
+ if (typeof childType === "string" && FIELD_VALIDATORS.has(childType)) {
389
+ checkField(ctx, child, cPath);
390
+ }
391
+ else {
392
+ checkBlock(ctx, child, cPath, false);
393
+ }
394
+ });
395
+ }
396
+ // page columns
397
+ for (const prop of ["sidebar", "main", "detail"]) {
398
+ const blocks = node[prop];
399
+ if (Array.isArray(blocks)) {
400
+ blocks.forEach((child, i) => {
401
+ if (isRec(child)) {
402
+ checkBlock(ctx, child, joinPath(joinPath(path, prop), String(i)), false);
403
+ }
404
+ });
405
+ }
406
+ }
407
+ // columns/grid wrappers
408
+ if (type === "columns" || type === "grid") {
409
+ if (Array.isArray(node.children)) {
410
+ node.children.forEach((child, i) => {
411
+ if (!isRec(child) || !isRec(child.block))
412
+ return;
413
+ checkBlock(ctx, child.block, joinPath(joinPath(joinPath(path, "children"), String(i)), "block"), false);
414
+ });
415
+ }
416
+ }
417
+ // page.switcher is a choice field
418
+ if (type === "page" && isRec(node.switcher) && node.switcher.type === "choice") {
419
+ checkField(ctx, node.switcher, joinPath(path, "switcher"));
420
+ }
421
+ }
422
+ /** Whether a block of this type carries actions or inputs (§4.1 id rule). */
423
+ function carriesActionsOrInputs(type, node) {
424
+ switch (type) {
425
+ case "page":
426
+ return node.actions !== undefined;
427
+ case "callout":
428
+ return node.actions !== undefined;
429
+ case "tree":
430
+ return node.onExpand !== undefined || node.onActivate !== undefined;
431
+ case "form":
432
+ return true; // fields are inputs
433
+ case "collection":
434
+ return (node.onMove !== undefined ||
435
+ node.onCheck !== undefined ||
436
+ node.onActivate !== undefined ||
437
+ node.bulkActions !== undefined ||
438
+ node.reorder !== undefined ||
439
+ node.checkable !== undefined ||
440
+ (node.time !== undefined &&
441
+ isRec(node.time) &&
442
+ (node.time.onResize !== undefined || node.time.onCreate !== undefined)));
443
+ default:
444
+ return false;
445
+ }
446
+ }
447
+ /** Count blocks (§4.5: a collection counts as one). */
448
+ function countBlocks(node) {
449
+ if (!isRec(node))
450
+ return 0;
451
+ if (node.type === "collection")
452
+ return 1;
453
+ let count = 1;
454
+ if (isRec(node.form))
455
+ count += countBlocks(node.form.root); // bulk-action form
456
+ const arrayProps = [
457
+ ["children", true],
458
+ ["sidebar", true],
459
+ ["main", true],
460
+ ["detail", true],
461
+ ];
462
+ for (const [prop, recurse] of arrayProps) {
463
+ if (!recurse)
464
+ continue;
465
+ const value = node[prop];
466
+ if (Array.isArray(value)) {
467
+ for (const element of value) {
468
+ if (!isRec(element))
469
+ continue;
470
+ if (isRec(element.block)) {
471
+ count += countBlocks(element.block); // columns/grid wrapper
472
+ }
473
+ else {
474
+ count += countBlocks(element);
475
+ }
476
+ }
477
+ }
478
+ }
479
+ return count;
480
+ }
481
+ /** The ambient one-line rule for `status`/`footer` (spec §9.2). */
482
+ function checkAmbientLine(ctx, root) {
483
+ if (ctx.placement !== "ambient" || !ctx.ambientPlace || ctx.ambientPlace === "panel")
484
+ return;
485
+ const place = ctx.ambientPlace;
486
+ const rootType = root.type;
487
+ if (rootType === "markdown" || rootType === "stat" || rootType === "meter" || rootType === "log") {
488
+ return; // one line-level block
489
+ }
490
+ if (rootType === "stack") {
491
+ const children = root.children;
492
+ if (Array.isArray(children) && children.length > 1) {
493
+ push(ctx, "root.children", `a ${place} ambient surface allows one line-level block`);
494
+ }
495
+ return;
496
+ }
497
+ if (rootType === "columns") {
498
+ const children = root.children;
499
+ if (Array.isArray(children)) {
500
+ children.forEach((child, i) => {
501
+ if (!isRec(child) || !isRec(child.block))
502
+ return;
503
+ const innerType = child.block.type;
504
+ if (typeof innerType !== "string")
505
+ return;
506
+ if (!AMBIENT_LINE_BLOCKS.has(innerType)) {
507
+ push(ctx, `root.children[${i}].block`, `${innerType} is not a line-level block for a ${place} ambient surface`);
508
+ }
509
+ });
510
+ }
511
+ return;
512
+ }
513
+ // fields/callout/diff/details/details bindables are single-line roots
514
+ }
515
+ // ─── the public entry points ──────────────────────────────────────────────
516
+ /**
517
+ * Validate a block tree against a placement (spec §14.2). Returns every
518
+ * error with its JSON path; on success, the narrowed tree.
519
+ */
520
+ export function validateTree(tree, opts) {
521
+ // version first (§4.4): a tree from another block-set version is
522
+ // rejected before anything else is checked.
523
+ const serverV = opts.serverV ?? BLOCK_SET_VERSION;
524
+ if (isRec(tree) && typeof tree.v === "number" && tree.v !== serverV) {
525
+ return {
526
+ ok: false,
527
+ errors: [
528
+ {
529
+ path: "v",
530
+ message: `unsupported block-set version ${tree.v} (server supports ${serverV})`,
531
+ },
532
+ ],
533
+ };
534
+ }
535
+ if (!VALID_PLACEMENTS.includes(opts.placement)) {
536
+ return {
537
+ ok: false,
538
+ errors: [{ path: "root", message: `unknown placement "${String(opts.placement)}"` }],
539
+ };
540
+ }
541
+ const errors = [];
542
+ if (!isRec(tree)) {
543
+ return {
544
+ ok: false,
545
+ errors: [{ path: "", message: "a tree must be {v, root}" }],
546
+ };
547
+ }
548
+ if (!isRec(tree.root)) {
549
+ return {
550
+ ok: false,
551
+ errors: [{ path: "root", message: "a tree must be {v, root}" }],
552
+ };
553
+ }
554
+ const root = tree.root;
555
+ const ctx = {
556
+ placement: opts.placement,
557
+ handlers: opts.handlers,
558
+ itemTemplate: false,
559
+ ambientPlace: opts.ambientPlace,
560
+ errors,
561
+ ids: new Map(),
562
+ };
563
+ // limits (§4.5) — sizes are the UTF-8 length of the serialized JSON
564
+ const limit = LIMITS[opts.placement];
565
+ const blocks = countBlocks(root);
566
+ if (blocks > limit.blocks) {
567
+ push(ctx, "root", `surface_too_large: ${blocks} blocks exceeds the ${opts.placement} limit of ${limit.blocks} blocks`);
568
+ }
569
+ const bytes = jsonBytes(tree);
570
+ if (bytes > limit.treeBytes) {
571
+ push(ctx, "root", `surface_too_large: tree is ${bytes} bytes, over the ${opts.placement} limit of ${limit.treeBytes} treeBytes`);
572
+ }
573
+ checkBlock(ctx, root, "root", true);
574
+ checkAmbientLine(ctx, root);
575
+ return errors.length === 0 ? { ok: true, tree: tree } : { ok: false, errors };
576
+ }
577
+ /**
578
+ * Validate a surface's data object: a JSON object within the placement's
579
+ * data-size limit (spec §4.5).
580
+ */
581
+ export function validateData(data, placement) {
582
+ if (!isRec(data)) {
583
+ return {
584
+ ok: false,
585
+ errors: [{ path: "", message: "surface data must be a JSON object" }],
586
+ };
587
+ }
588
+ const bytes = jsonBytes(data);
589
+ const limit = LIMITS[placement];
590
+ if (bytes > limit.dataBytes) {
591
+ return {
592
+ ok: false,
593
+ errors: [
594
+ {
595
+ path: "",
596
+ message: `surface_too_large: data is ${bytes} bytes, over the ${placement} limit of ${limit.dataBytes} dataBytes`,
597
+ },
598
+ ],
599
+ };
600
+ }
601
+ return { ok: true };
602
+ }
package/dist/config.js CHANGED
@@ -307,6 +307,22 @@ function parseMetricsPublic(env) {
307
307
  return false;
308
308
  throw new Error(`SIGRUN_METRICS_PUBLIC must be "true" or "false", got ${JSON.stringify(raw)}`);
309
309
  }
310
+ /**
311
+ * Parse `SIGRUN_DEV` (§14.2/§14.4): "true"/"false" case-insensitive, plus
312
+ * the spec's own `SIGRUN_DEV=1`/`=0` shell form. Strict otherwise — `yes`
313
+ * or any other value fails loud naming the variable (the pattern above).
314
+ */
315
+ function parseDev(env) {
316
+ const raw = env.SIGRUN_DEV;
317
+ if (raw === undefined || raw === "")
318
+ return undefined;
319
+ const lowered = raw.trim().toLowerCase();
320
+ if (lowered === "true" || lowered === "1")
321
+ return true;
322
+ if (lowered === "false" || lowered === "0")
323
+ return false;
324
+ throw new Error(`SIGRUN_DEV must be "true" or "false" (or 1/0), got ${JSON.stringify(raw)}`);
325
+ }
310
326
  /**
311
327
  * Parse `SIGRUN_AUTH`: a csv of `token`/`proxy` (order preserved,
312
328
  * duplicates dropped), `none` ALONE as the empty list (authentication
@@ -734,6 +750,10 @@ export function resolvedServerSection(config) {
734
750
  const metrics = config.metricsPublic
735
751
  ? { public: true }
736
752
  : undefined;
753
+ // The development flag (§14.2/§14.4) follows the metrics pattern: a
754
+ // DEFAULTED dev is omitted (persisting `dev: false` would be noise),
755
+ // a dev deployment records it (an operator-visible intent marker).
756
+ const dev = config.dev === true ? true : undefined;
737
757
  return {
738
758
  port: config.port,
739
759
  workspacePath: config.workspace.path,
@@ -748,6 +768,7 @@ export function resolvedServerSection(config) {
748
768
  providers: { ...config.providers },
749
769
  ...(auth === undefined ? {} : { auth }),
750
770
  ...(metrics === undefined ? {} : { metrics }),
771
+ ...(dev === undefined ? {} : { dev }),
751
772
  ...(backup === undefined ? {} : { backup }),
752
773
  };
753
774
  }
@@ -850,6 +871,12 @@ export function envServerSection(env) {
850
871
  const timezone = env.SIGRUN_TIMEZONE?.trim() || undefined;
851
872
  if (timezone !== undefined)
852
873
  section.timezone = timezone;
874
+ // The development flag (§14.2/§14.4) — a flat boolean, projected only
875
+ // when the env var is set (a defaulted dev is never persisted: it
876
+ // would read as intent on the next boot).
877
+ const dev = parseDev(env);
878
+ if (dev !== undefined)
879
+ section.dev = dev;
853
880
  return section;
854
881
  }
855
882
  /**
@@ -924,6 +951,7 @@ export function effectiveServerSection(env, fileServer, config) {
924
951
  "queueMax",
925
952
  ]);
926
953
  emit("providers.refreshIntervalMinutes", config.providers.refreshIntervalMinutes, ["providers", "refreshIntervalMinutes"]);
954
+ emit("dev", config.dev, ["dev"]);
927
955
  if (config.backup?.target === "path") {
928
956
  emit("backup.path", config.backup.path, ["backup", "path"]);
929
957
  emit("backup.staleAfterHours", config.backupStaleAfterHours, ["backup", "staleAfterHours"]);
@@ -1070,6 +1098,9 @@ export function loadConfig(env, settingsFile) {
1070
1098
  },
1071
1099
  auth: parseAuth(env, server?.auth),
1072
1100
  metricsPublic: parseMetricsPublic(env) ?? server?.metrics?.public ?? false,
1101
+ // The development flag (§14.2/§14.4): strict env parse over the
1102
+ // file's `server.dev`, default false.
1103
+ dev: parseDev(env) ?? server?.dev ?? false,
1073
1104
  // The jobs default timezone (§8.2): env over file over the process
1074
1105
  // TZ, validated loud (a bad name would otherwise crash the scheduler
1075
1106
  // at evaluation time instead of failing boot).