@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
@@ -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;
@@ -307,7 +307,10 @@ const MeterBlock = Type.Object({
307
307
  const LogBlock = Type.Object({
308
308
  ...BlockBase,
309
309
  type: Type.Literal("log"),
310
- lines: Type.Ref("Binding"),
310
+ // Literal lines are legal (ambient placement requires them — the
311
+ // ambient envelope carries no data document); placements that resolve
312
+ // bindings keep the binding form.
313
+ lines: Type.Union([Type.Ref("Binding"), Type.Array(Type.Ref("JsonValue"))]),
311
314
  follow: Type.Optional(Type.Boolean()),
312
315
  wrap: Type.Optional(Type.Boolean()),
313
316
  tones: Type.Optional(Type.Record(Type.String(), Type.Ref("Tone"))),
@@ -374,7 +377,10 @@ const BulkAction = Type.Object({
374
377
  const CollectionBlock = Type.Object({
375
378
  ...BlockBase,
376
379
  type: Type.Literal("collection"),
377
- items: Type.Ref("Binding"),
380
+ // Literal rows are legal (ambient placement requires them — the
381
+ // ambient envelope carries no data document); placements that resolve
382
+ // bindings keep the binding form.
383
+ items: Type.Union([Type.Ref("Binding"), Type.Array(Type.Ref("JsonValue"))]),
378
384
  item: AnyObject,
379
385
  display: Type.Unsafe({
380
386
  type: "string",
@@ -445,6 +451,7 @@ const NumberField = Type.Object({
445
451
  const ToggleField = Type.Object({
446
452
  ...FieldBase,
447
453
  type: Type.Literal("toggle"),
454
+ default: Type.Optional(Type.Boolean()),
448
455
  });
449
456
  const ChoiceField = Type.Object({
450
457
  ...FieldBase,
@@ -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;
@@ -142,9 +142,22 @@ function checkActionButtons(ctx, buttons, path) {
142
142
  checkAction(ctx, button.action, joinPath(bPath, "action"));
143
143
  });
144
144
  }
145
- /** Relative `./` bindings are legal only inside a card template (§4.2). */
145
+ /**
146
+ * Bindings outside a card template (§4.2): relative `./` bindings are
147
+ * legal only inside an item template. In the AMBIENT placement, every
148
+ * binding (absolute or relative) outside an item template is rejected —
149
+ * the ambient envelope carries no data document, so a bound prop has
150
+ * nothing to resolve against and renders blank (the 2026-10-24 ruling's
151
+ * items rule, extended to every bindable prop).
152
+ */
146
153
  function checkBinding(ctx, binding, path) {
147
- if (!ctx.itemTemplate && binding.$.startsWith("./")) {
154
+ if (ctx.itemTemplate)
155
+ return;
156
+ if (ctx.placement === "ambient") {
157
+ push(ctx, joinPath(path, "$"), `ambient surfaces carry literal content only (binding "${binding.$}" outside a collection item template)`);
158
+ return;
159
+ }
160
+ if (binding.$.startsWith("./")) {
148
161
  push(ctx, joinPath(path, "$"), `relative binding "${binding.$}" is only valid inside a collection item template`);
149
162
  }
150
163
  }
@@ -152,10 +165,12 @@ function checkBinding(ctx, binding, path) {
152
165
  * Any relative `./` binding on this node (§4.2): every prop value is
153
166
  * scanned — the binding-only props (`items`, `lines`, `nodes`, `rows`,
154
167
  * `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).
168
+ * bindable literals, so a hardcoded prop list would miss them. Outside
169
+ * ambient, absolute `/` bindings stay legal everywhere and relative
170
+ * ones only inside item templates; in ambient, every binding outside an
171
+ * item template fails (checkBinding). `skip` names props whose own walk
172
+ * already reported (the structural literals above carry their more
173
+ * precise message).
159
174
  */
160
175
  function checkNodeRelativeBindings(ctx, node, path, skip) {
161
176
  if (ctx.itemTemplate)
@@ -282,6 +297,35 @@ function checkBlock(ctx, node, path, rootDepth) {
282
297
  checkSchema(ctx, validator, node, path);
283
298
  // collection-specific rules
284
299
  if (type === "collection") {
300
+ // Controller ruling (2026-10-24): bindings are OUT of ambient
301
+ // placement — the ambient envelope carries no data document, so a
302
+ // bound `items` has nothing to resolve against. Literal rows only.
303
+ // (Reversible: extending the envelope with a data slot remains open.)
304
+ if (ctx.placement === "ambient" && isBinding(node.items)) {
305
+ // checkBinding's ambient branch fires for this prop too; the
306
+ // more specific collection wording wins, so skip `items` below.
307
+ push(ctx, joinPath(path, "items"), "ambient collections carry literal rows (items must not be a binding)");
308
+ }
309
+ // Ambient: bindables nested inside structural props are still
310
+ // bound nowhere (the no-data-document ruling). `groups`/`columns`
311
+ // sit in the structural skip set, so the generic binding walk
312
+ // never reaches their `title`/`label` bindables — walk them here.
313
+ if (ctx.placement === "ambient") {
314
+ const nestedBindables = [
315
+ ["groups", "title"],
316
+ ["columns", "label"],
317
+ ];
318
+ for (const [listProp, leafProp] of nestedBindables) {
319
+ const list = node[listProp];
320
+ if (!Array.isArray(list))
321
+ continue;
322
+ list.forEach((entry, i) => {
323
+ if (isRec(entry) && isBinding(entry[leafProp])) {
324
+ checkBinding(ctx, entry[leafProp], joinPath(joinPath(joinPath(path, listProp), String(i)), leafProp));
325
+ }
326
+ });
327
+ }
328
+ }
285
329
  const display = node.display;
286
330
  if (typeof display === "string" &&
287
331
  rules.collectionDisplays &&
@@ -350,9 +394,14 @@ function checkBlock(ctx, node, path, rootDepth) {
350
394
  if (!check.ok)
351
395
  push(ctx, joinPath(path, "src"), check.message);
352
396
  }
353
- // bindable props: every relative `./` binding on the node (§4.2)
397
+ // bindable props: every relative `./` binding on the node (§4.2).
398
+ // `items` joins the skip set in ambient so the collection-specific
399
+ // message above stays the single error at its path.
354
400
  const structural = STRUCTURAL_LITERALS[type] ?? [];
355
- checkNodeRelativeBindings(ctx, node, path, new Set(structural));
401
+ const skip = new Set(structural);
402
+ if (type === "collection" && ctx.placement === "ambient")
403
+ skip.add("items");
404
+ checkNodeRelativeBindings(ctx, node, path, skip);
356
405
  // onActivate carries an Action (§6.1): same rules as button actions
357
406
  if (node.onActivate !== undefined) {
358
407
  checkAction(ctx, node.onActivate, joinPath(path, "onActivate"));
package/dist/cli.js CHANGED
@@ -49,6 +49,12 @@ import { Writable } from "node:stream";
49
49
  import { resolveStoreRoots, writeFileAtomic } from "./harness/stores.js";
50
50
  import { openIdentityStores } from "./harness/identity/index.js";
51
51
  import { pathToFileURL } from "node:url";
52
+ import { effectiveMethods, loadConfig } from "./config.js";
53
+ import { probeIsolation, runHelperProbe, selfTestHelper } from "./sandbox/availability.js";
54
+ import { SettingsStore } from "./harness/settings.js";
55
+ import { resolveHelper } from "./sandbox/helper.js";
56
+ import { selfPackageRoot } from "./harness/packages.js";
57
+ import { createAdmissionGuard } from "./harness/identity/users.js";
52
58
  import { serve } from "./serve.js";
53
59
  import { ACCENT_NAMES, PALETTE_NAMES, isAccentName, isPaletteName, } from "@sigrun/shared/themes";
54
60
  export const USAGE_TEXT = `usage: sigrun [--user <name>]
@@ -643,7 +649,7 @@ async function runProviderCommand(parsed, env, io, fetchImpl, readSecret) {
643
649
  * secret exists only at create time);
644
650
  * - `token revoke` fails loud on an unknown id.
645
651
  */
646
- async function runIdentityCommand(parsed, env, io) {
652
+ async function runIdentityCommand(parsed, env, io, isolationProbe) {
647
653
  const stores = openIdentityStores(resolveStoreRoots(env));
648
654
  if (parsed.command === "token") {
649
655
  switch (parsed.sub) {
@@ -679,6 +685,24 @@ async function runIdentityCommand(parsed, env, io) {
679
685
  }
680
686
  switch (parsed.sub) {
681
687
  case "add": {
688
+ const config = loadConfig(env, new SettingsStore({ dir: defaultConfigDir(env) }).read());
689
+ const coreRoot = selfPackageRoot();
690
+ const isolation = await isolationProbe({
691
+ platform: process.platform,
692
+ helper: coreRoot === undefined ? undefined : resolveHelper(env, coreRoot),
693
+ runProbe: runHelperProbe,
694
+ selfTest: selfTestHelper,
695
+ });
696
+ const methods = effectiveMethods(config.auth, "127.0.0.1");
697
+ stores.users.configureAdmission({
698
+ firstWorkspace: config.workspace.path,
699
+ admit: createAdmissionGuard({
700
+ isolation,
701
+ mode: config.isolation.mode,
702
+ methods,
703
+ proxySecretHeader: config.auth.proxy.secretHeader,
704
+ }),
705
+ });
682
706
  await stores.users.add({
683
707
  user: parsed.user,
684
708
  admin: parsed.admin ?? false,
@@ -850,7 +874,7 @@ export async function cliMain(argv, env = process.env, deps, io = {
850
874
  case "token":
851
875
  case "user": {
852
876
  // The identity commands (Task 7): stores directly, server or not.
853
- return await runIdentityCommand(parsed, env, io);
877
+ return await runIdentityCommand(parsed, env, io, realDeps.probeIsolation ?? probeIsolation);
854
878
  }
855
879
  case "provider": {
856
880
  // The provider commands (release 0.1 Task 2b): API clients over
package/dist/config.js CHANGED
@@ -148,6 +148,7 @@ export const DEFAULT_PROVIDER_REFRESH_MINUTES = 15;
148
148
  * defaulted — they exist only when env provides them.
149
149
  */
150
150
  export const DEFAULT_AUTH = {
151
+ sessionDays: 30,
151
152
  methods: undefined,
152
153
  proxy: {
153
154
  trustedProxies: [],
@@ -323,6 +324,40 @@ function parseDev(env) {
323
324
  return false;
324
325
  throw new Error(`SIGRUN_DEV must be "true" or "false" (or 1/0), got ${JSON.stringify(raw)}`);
325
326
  }
327
+ /** Parse `SIGRUN_ISOLATION`; unset/empty lets the file/default tier apply. */
328
+ function parseIsolationMode(env) {
329
+ const raw = env.SIGRUN_ISOLATION;
330
+ if (raw === undefined || raw === "")
331
+ return undefined;
332
+ const mode = raw.trim().toLowerCase();
333
+ if (mode === "auto" || mode === "always" || mode === "off")
334
+ return mode;
335
+ throw new Error(`SIGRUN_ISOLATION must be "auto", "always", or "off", got ${JSON.stringify(raw)}`);
336
+ }
337
+ function parseIsolation(env, server) {
338
+ return {
339
+ mode: parseIsolationMode(env) ?? server?.mode ?? "auto",
340
+ osUsers: server?.osUsers ?? false,
341
+ workerIdleMinutes: server?.workerIdleMinutes ?? 10,
342
+ };
343
+ }
344
+ /**
345
+ * Parse `SIGRUN_SUPERVISED` (UI-blocks U4 Task 2, spec §13.3): the
346
+ * `parseDev` grammar exactly ("true"/"false"/1/0, strict otherwise —
347
+ * the env contract is the deploy's promise that a restart policy exists,
348
+ * so a fuzzy parse would lie about the failure mode).
349
+ */
350
+ function parseSupervised(env) {
351
+ const raw = env.SIGRUN_SUPERVISED;
352
+ if (raw === undefined || raw === "")
353
+ return undefined;
354
+ const lowered = raw.trim().toLowerCase();
355
+ if (lowered === "true" || lowered === "1")
356
+ return true;
357
+ if (lowered === "false" || lowered === "0")
358
+ return false;
359
+ throw new Error(`SIGRUN_SUPERVISED must be "true" or "false" (or 1/0), got ${JSON.stringify(raw)}`);
360
+ }
326
361
  /**
327
362
  * Parse `SIGRUN_AUTH`: a csv of `token`/`proxy` (order preserved,
328
363
  * duplicates dropped), `none` ALONE as the empty list (authentication
@@ -398,6 +433,7 @@ function parseAuth(env, server) {
398
433
  const token = env.SIGRUN_API_TOKEN?.trim() || undefined;
399
434
  const secret = env.SIGRUN_PROXY_SECRET?.trim() || undefined;
400
435
  return {
436
+ sessionDays: server?.sessionDays ?? DEFAULT_AUTH.sessionDays,
401
437
  methods,
402
438
  proxy: {
403
439
  trustedProxies,
@@ -730,11 +766,15 @@ export function resolvedServerSection(config) {
730
766
  proxySection !== undefined ||
731
767
  config.auth.adminGroups.length > 0 ||
732
768
  config.auth.allowedHosts !== undefined ||
733
- config.auth.allowedOrigins !== undefined
769
+ config.auth.allowedOrigins !== undefined ||
770
+ config.auth.sessionDays !== DEFAULT_AUTH.sessionDays
734
771
  ? {
735
772
  ...(config.auth.methods === undefined
736
773
  ? {}
737
774
  : { methods: [...config.auth.methods] }),
775
+ ...(config.auth.sessionDays === DEFAULT_AUTH.sessionDays
776
+ ? {}
777
+ : { sessionDays: config.auth.sessionDays }),
738
778
  ...(proxySection === undefined ? {} : { proxy: proxySection }),
739
779
  ...(config.auth.adminGroups.length > 0
740
780
  ? { adminGroups: [...config.auth.adminGroups] }
@@ -765,6 +805,7 @@ export function resolvedServerSection(config) {
765
805
  ...(webui === undefined ? {} : { webui }),
766
806
  compaction: { ...config.compaction },
767
807
  memorySeam: { ...config.memorySeam },
808
+ isolation: { ...config.isolation },
768
809
  providers: { ...config.providers },
769
810
  ...(auth === undefined ? {} : { auth }),
770
811
  ...(metrics === undefined ? {} : { metrics }),
@@ -803,6 +844,7 @@ export function resolvedServerSection(config) {
803
844
  * | `SIGRUN_IDLE_THRESHOLD` | `memorySeam.idleThresholdMs` |
804
845
  * | `SIGRUN_WORKER_DEADLINE` | `memorySeam.workerDeadlineMs` |
805
846
  * | `SIGRUN_QUEUE_MAX` | `memorySeam.queueMax` |
847
+ * | `SIGRUN_ISOLATION` | `isolation.mode` |
806
848
  * | `SIGRUN_PROVIDER_REFRESH_MINUTES` | `providers.refreshIntervalMinutes` |
807
849
  * | `SIGRUN_BACKUP_*` | `backup` |
808
850
  * | `SIGRUN_MAX_UPLOAD_BYTES` | `maxUploadBytes` |
@@ -859,6 +901,9 @@ export function envServerSection(env) {
859
901
  });
860
902
  if (memorySeam !== undefined)
861
903
  section.memorySeam = memorySeam;
904
+ const isolationMode = parseIsolationMode(env);
905
+ if (isolationMode !== undefined)
906
+ section.isolation = { mode: isolationMode };
862
907
  const refreshIntervalMinutes = parseNonNegativeIntEnv(env, PROVIDER_REFRESH_MINUTES_ENV);
863
908
  if (refreshIntervalMinutes !== undefined) {
864
909
  section.providers = { refreshIntervalMinutes };
@@ -877,6 +922,12 @@ export function envServerSection(env) {
877
922
  const dev = parseDev(env);
878
923
  if (dev !== undefined)
879
924
  section.dev = dev;
925
+ // The supervised flag (§13.3) — the same bootstrap rule as dev:
926
+ // projected only when the env var is SET (a defaulted flag is never
927
+ // persisted; the deploy's restart posture is env intent, not state).
928
+ const supervised = parseSupervised(env);
929
+ if (supervised !== undefined)
930
+ section.supervised = supervised;
880
931
  return section;
881
932
  }
882
933
  /**
@@ -950,8 +1001,18 @@ export function effectiveServerSection(env, fileServer, config) {
950
1001
  "memorySeam",
951
1002
  "queueMax",
952
1003
  ]);
1004
+ emit("auth.sessionDays", config.auth.sessionDays, ["auth", "sessionDays"]);
1005
+ emit("isolation.mode", config.isolation.mode, ["isolation", "mode"]);
1006
+ emit("isolation.osUsers", config.isolation.osUsers, ["isolation", "osUsers"]);
1007
+ emit("isolation.workerIdleMinutes", config.isolation.workerIdleMinutes, [
1008
+ "isolation",
1009
+ "workerIdleMinutes",
1010
+ ]);
953
1011
  emit("providers.refreshIntervalMinutes", config.providers.refreshIntervalMinutes, ["providers", "refreshIntervalMinutes"]);
954
1012
  emit("dev", config.dev, ["dev"]);
1013
+ // U4 §13.3: the supervised posture rides the effective block too —
1014
+ // the settings surface's restart affordance reads one place.
1015
+ emit("supervised", config.supervised, ["supervised"]);
955
1016
  if (config.backup?.target === "path") {
956
1017
  emit("backup.path", config.backup.path, ["backup", "path"]);
957
1018
  emit("backup.staleAfterHours", config.backupStaleAfterHours, ["backup", "staleAfterHours"]);
@@ -1093,6 +1154,7 @@ export function loadConfig(env, settingsFile) {
1093
1154
  backupStaleAfterHours: server?.backup?.staleAfterHours ?? DEFAULT_BACKUP_STALE_AFTER_HOURS,
1094
1155
  compaction: parseCompaction(env, server?.compaction),
1095
1156
  memorySeam: parseMemorySeam(env, server?.memorySeam),
1157
+ isolation: parseIsolation(env, server?.isolation),
1096
1158
  providers: {
1097
1159
  refreshIntervalMinutes: parseRefreshIntervalMinutes(env, server?.providers?.refreshIntervalMinutes),
1098
1160
  },
@@ -1101,6 +1163,10 @@ export function loadConfig(env, settingsFile) {
1101
1163
  // The development flag (§14.2/§14.4): strict env parse over the
1102
1164
  // file's `server.dev`, default false.
1103
1165
  dev: parseDev(env) ?? server?.dev ?? false,
1166
+ // The supervised posture (§13.3): env over file over false. Only
1167
+ // the chart and compose set the env; a file-set flag serves a
1168
+ // hand-rolled systemd/supervisord deploy (same restart promise).
1169
+ supervised: parseSupervised(env) ?? server?.supervised ?? false,
1104
1170
  // The jobs default timezone (§8.2): env over file over the process
1105
1171
  // TZ, validated loud (a bad name would otherwise crash the scheduler
1106
1172
  // at evaluation time instead of failing boot).