@loomwork/ui-hydration 0.0.0-stage → 0.1.0

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 (179) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +8 -0
  3. package/README.md +398 -2
  4. package/dist/cache.d.ts +71 -0
  5. package/dist/cache.d.ts.map +1 -0
  6. package/dist/cache.js +157 -0
  7. package/dist/cache.js.map +1 -0
  8. package/dist/chatSource.d.ts +105 -0
  9. package/dist/chatSource.d.ts.map +1 -0
  10. package/dist/chatSource.js +259 -0
  11. package/dist/chatSource.js.map +1 -0
  12. package/dist/client.d.ts +17 -0
  13. package/dist/client.d.ts.map +1 -0
  14. package/dist/client.js +36 -0
  15. package/dist/client.js.map +1 -0
  16. package/dist/contentRefs.d.ts +78 -0
  17. package/dist/contentRefs.d.ts.map +1 -0
  18. package/dist/contentRefs.js +129 -0
  19. package/dist/contentRefs.js.map +1 -0
  20. package/dist/controller.d.ts +228 -0
  21. package/dist/controller.d.ts.map +1 -0
  22. package/dist/controller.js +454 -0
  23. package/dist/controller.js.map +1 -0
  24. package/dist/dataProvider.d.ts +53 -0
  25. package/dist/dataProvider.d.ts.map +1 -0
  26. package/dist/dataProvider.js +77 -0
  27. package/dist/dataProvider.js.map +1 -0
  28. package/dist/esm/cache.js +151 -0
  29. package/dist/esm/cache.js.map +1 -0
  30. package/dist/esm/chatSource.js +254 -0
  31. package/dist/esm/chatSource.js.map +1 -0
  32. package/dist/esm/client.js +15 -0
  33. package/dist/esm/client.js.map +1 -0
  34. package/dist/esm/contentRefs.js +119 -0
  35. package/dist/esm/contentRefs.js.map +1 -0
  36. package/dist/esm/controller.js +449 -0
  37. package/dist/esm/controller.js.map +1 -0
  38. package/dist/esm/dataProvider.js +72 -0
  39. package/dist/esm/dataProvider.js.map +1 -0
  40. package/dist/esm/index.js +15 -0
  41. package/dist/esm/index.js.map +1 -0
  42. package/dist/esm/invoker.js +2 -0
  43. package/dist/esm/invoker.js.map +1 -0
  44. package/dist/esm/invokers.js +310 -0
  45. package/dist/esm/invokers.js.map +1 -0
  46. package/dist/esm/loomEffectProvider.js +96 -0
  47. package/dist/esm/loomEffectProvider.js.map +1 -0
  48. package/dist/esm/params.js +207 -0
  49. package/dist/esm/params.js.map +1 -0
  50. package/dist/esm/path.js +24 -0
  51. package/dist/esm/path.js.map +1 -0
  52. package/dist/esm/plan.js +190 -0
  53. package/dist/esm/plan.js.map +1 -0
  54. package/dist/esm/projection.js +230 -0
  55. package/dist/esm/projection.js.map +1 -0
  56. package/dist/esm/provider.js +416 -0
  57. package/dist/esm/provider.js.map +1 -0
  58. package/dist/esm/repair.js +53 -0
  59. package/dist/esm/repair.js.map +1 -0
  60. package/dist/esm/reshape.js +156 -0
  61. package/dist/esm/reshape.js.map +1 -0
  62. package/dist/esm/restricted.js +54 -0
  63. package/dist/esm/restricted.js.map +1 -0
  64. package/dist/esm/scheduler.js +247 -0
  65. package/dist/esm/scheduler.js.map +1 -0
  66. package/dist/esm/select.js +18 -0
  67. package/dist/esm/select.js.map +1 -0
  68. package/dist/esm/shape.js +34 -0
  69. package/dist/esm/shape.js.map +1 -0
  70. package/dist/esm/split/client.js +309 -0
  71. package/dist/esm/split/client.js.map +1 -0
  72. package/dist/esm/split/index.js +3 -0
  73. package/dist/esm/split/index.js.map +1 -0
  74. package/dist/esm/split/server.js +488 -0
  75. package/dist/esm/split/server.js.map +1 -0
  76. package/dist/esm/split/shared.js +13 -0
  77. package/dist/esm/split/shared.js.map +1 -0
  78. package/dist/index.d.ts +29 -0
  79. package/dist/index.d.ts.map +1 -0
  80. package/dist/index.js +63 -0
  81. package/dist/index.js.map +1 -0
  82. package/dist/invoker.d.ts +10 -0
  83. package/dist/invoker.d.ts.map +1 -0
  84. package/dist/invoker.js +3 -0
  85. package/dist/invoker.js.map +1 -0
  86. package/dist/invokers.d.ts +36 -0
  87. package/dist/invokers.d.ts.map +1 -0
  88. package/dist/invokers.js +315 -0
  89. package/dist/invokers.js.map +1 -0
  90. package/dist/loomEffectProvider.d.ts +67 -0
  91. package/dist/loomEffectProvider.d.ts.map +1 -0
  92. package/dist/loomEffectProvider.js +101 -0
  93. package/dist/loomEffectProvider.js.map +1 -0
  94. package/dist/params.d.ts +103 -0
  95. package/dist/params.d.ts.map +1 -0
  96. package/dist/params.js +221 -0
  97. package/dist/params.js.map +1 -0
  98. package/dist/path.d.ts +12 -0
  99. package/dist/path.d.ts.map +1 -0
  100. package/dist/path.js +28 -0
  101. package/dist/path.js.map +1 -0
  102. package/dist/plan.d.ts +56 -0
  103. package/dist/plan.d.ts.map +1 -0
  104. package/dist/plan.js +199 -0
  105. package/dist/plan.js.map +1 -0
  106. package/dist/projection.d.ts +122 -0
  107. package/dist/projection.d.ts.map +1 -0
  108. package/dist/projection.js +238 -0
  109. package/dist/projection.js.map +1 -0
  110. package/dist/provider.d.ts +152 -0
  111. package/dist/provider.d.ts.map +1 -0
  112. package/dist/provider.js +421 -0
  113. package/dist/provider.js.map +1 -0
  114. package/dist/repair.d.ts +18 -0
  115. package/dist/repair.d.ts.map +1 -0
  116. package/dist/repair.js +57 -0
  117. package/dist/repair.js.map +1 -0
  118. package/dist/reshape.d.ts +32 -0
  119. package/dist/reshape.d.ts.map +1 -0
  120. package/dist/reshape.js +164 -0
  121. package/dist/reshape.js.map +1 -0
  122. package/dist/restricted.d.ts +21 -0
  123. package/dist/restricted.d.ts.map +1 -0
  124. package/dist/restricted.js +61 -0
  125. package/dist/restricted.js.map +1 -0
  126. package/dist/scheduler.d.ts +82 -0
  127. package/dist/scheduler.d.ts.map +1 -0
  128. package/dist/scheduler.js +254 -0
  129. package/dist/scheduler.js.map +1 -0
  130. package/dist/select.d.ts +34 -0
  131. package/dist/select.d.ts.map +1 -0
  132. package/dist/select.js +21 -0
  133. package/dist/select.js.map +1 -0
  134. package/dist/shape.d.ts +3 -0
  135. package/dist/shape.d.ts.map +1 -0
  136. package/dist/shape.js +37 -0
  137. package/dist/shape.js.map +1 -0
  138. package/dist/split/client.d.ts +132 -0
  139. package/dist/split/client.d.ts.map +1 -0
  140. package/dist/split/client.js +313 -0
  141. package/dist/split/client.js.map +1 -0
  142. package/dist/split/index.d.ts +5 -0
  143. package/dist/split/index.d.ts.map +1 -0
  144. package/dist/split/index.js +14 -0
  145. package/dist/split/index.js.map +1 -0
  146. package/dist/split/server.d.ts +160 -0
  147. package/dist/split/server.d.ts.map +1 -0
  148. package/dist/split/server.js +496 -0
  149. package/dist/split/server.js.map +1 -0
  150. package/dist/split/shared.d.ts +6 -0
  151. package/dist/split/shared.d.ts.map +1 -0
  152. package/dist/split/shared.js +19 -0
  153. package/dist/split/shared.js.map +1 -0
  154. package/package.json +77 -3
  155. package/src/cache.ts +211 -0
  156. package/src/chatSource.ts +374 -0
  157. package/src/client.ts +46 -0
  158. package/src/contentRefs.ts +195 -0
  159. package/src/controller.ts +708 -0
  160. package/src/dataProvider.ts +141 -0
  161. package/src/index.ts +86 -0
  162. package/src/invoker.ts +14 -0
  163. package/src/invokers.ts +348 -0
  164. package/src/loomEffectProvider.ts +136 -0
  165. package/src/params.ts +314 -0
  166. package/src/path.ts +26 -0
  167. package/src/plan.ts +197 -0
  168. package/src/projection.ts +297 -0
  169. package/src/provider.ts +657 -0
  170. package/src/repair.ts +70 -0
  171. package/src/reshape.ts +193 -0
  172. package/src/restricted.ts +69 -0
  173. package/src/scheduler.ts +284 -0
  174. package/src/select.ts +59 -0
  175. package/src/shape.ts +39 -0
  176. package/src/split/client.ts +449 -0
  177. package/src/split/index.ts +18 -0
  178. package/src/split/server.ts +746 -0
  179. package/src/split/shared.ts +18 -0
@@ -0,0 +1,160 @@
1
+ import { type Audience, type ContentRefTable, type DeclaredToolClasses, type Experience, type Finding, type ReadPolicy, type SemanticCatalog, type Surface, type UndeclaredToolPolicy, type Viewer } from "@loomwork/ui-core";
2
+ import { type HydrationCache } from "../cache";
3
+ import type { DriftReport } from "../controller";
4
+ import { type ExecuteOptions } from "../provider";
5
+ import { type HandleLoadResult } from "../scheduler";
6
+ import type { HandleInventory, HandleLoadRequest } from "./client";
7
+ /**
8
+ * Derives a handle's public revision from the provider's content fingerprint (`specFingerprint`).
9
+ * It must be KEYED: an unkeyed function of the fingerprint lets a client confirm a guessed provider
10
+ * (tool, arguments) offline. In production use an HMAC over a server secret, e.g.
11
+ * `createHmac("sha256", secret).update(handle + "\u0000" + fingerprint).digest("hex")`. It must be
12
+ * deterministic for as long as clients hold inventories (a rotated key re-asks every handle once),
13
+ * and it must move with the fingerprint: a per-handle constant cannot be detected here, and it
14
+ * defeats re-asking — a client never learns that the provider behind a handle changed.
15
+ */
16
+ export type RevisionFn = (handle: string, fingerprint: string) => string;
17
+ export interface InventoryOptions {
18
+ readonly revisionOf: RevisionFn;
19
+ /**
20
+ * Who the inventory is for. A handle classified above the viewer's clearance is OMITTED — not
21
+ * listed with a placeholder, so the client learns neither its name nor its revision. FAIL-CLOSED:
22
+ * absent = no clearance (one `viewer-not-supplied` warn is returned if anything is classified).
23
+ * Pass the same viewer to `serveHandles`, which refuses such a handle even if a client names it.
24
+ */
25
+ readonly viewer?: Viewer;
26
+ /**
27
+ * Who the surface is for (`@loomwork/ui-core` `Audience`, the value `compile` took). `"broadcast"`
28
+ * lists only unclassified handles, whoever the `viewer` is. Pass the same to `serveHandles`.
29
+ */
30
+ readonly audience?: Audience;
31
+ /**
32
+ * The host's DECLARED class per tool (`toolKey`), a read-time floor under every handle calling
33
+ * that tool (`@loomwork/ui-core` `DeclaredToolClasses`): a handle whose stored nodes carry no class
34
+ * is still gated as strictly as its tool is declared. Absent = only the experience's own classes.
35
+ */
36
+ readonly declaredToolClasses?: DeclaredToolClasses;
37
+ /**
38
+ * What a tool the `declaredToolClasses` table does not list reads as: `"allow"` (default) = as the
39
+ * experience itself says; `"deny"` = no viewer may read it (only with a table present).
40
+ */
41
+ readonly undeclared?: UndeclaredToolPolicy;
42
+ }
43
+ /**
44
+ * What a client may know about an experience's data plane: never a provider. Throws `TypeError`
45
+ * without a `revisionOf`, or when it returns a non-string, an empty string or the fingerprint.
46
+ */
47
+ export declare function handleInventory(exp: Experience, opts: InventoryOptions): {
48
+ inventory: HandleInventory;
49
+ findings: Finding[];
50
+ };
51
+ /** The most requests one `serveHandles` call serves by default (`ServeOptions.maxBatch`). */
52
+ export declare const DEFAULT_MAX_BATCH = 256;
53
+ /**
54
+ * The most transient calls one `serveHandles` batch has in flight by default
55
+ * (`ServeOptions.maxConcurrentCalls`): the batch is client-sized, so the server bounds its fan-out.
56
+ */
57
+ export declare const DEFAULT_MAX_CONCURRENT_CALLS = 32;
58
+ export interface ServeOptions extends Omit<ExecuteOptions, "params" | "kinds" | "contentRefArgs"> {
59
+ /**
60
+ * REQUIRED (U9b): the marked content-reference argument paths per tool, derived from the offered
61
+ * capabilities with the planner's `contentRefArgsFrom(capabilities)` (persist it with the capability
62
+ * set; `parseContentRefTable` re-reads it). `"none"` is the explicit opt-out: nothing is guarded,
63
+ * and serving a media-bound handle then reports `media-content-refs-unguarded` (warn).
64
+ */
65
+ readonly contentRefArgs: ContentRefTable | "none";
66
+ /** Kinds for `"contract"` providers: the nodes binding the handle, resolved on this surface. */
67
+ readonly outputKinds?: {
68
+ readonly catalog: SemanticCatalog;
69
+ readonly surface: Surface;
70
+ };
71
+ readonly cache?: HydrationCache;
72
+ /**
73
+ * Who the answers are for. A handle bound by any node classified above the viewer's clearance is
74
+ * refused BEFORE its provider is looked up, cached or invoked, and the client is told exactly
75
+ * what it is told for a handle the experience does not bind (`unknown-handle`) — no oracle for
76
+ * which handles exist but are restricted; the full `restricted` finding goes to `onFinding`.
77
+ * FAIL-CLOSED: absent = no clearance. Unclassified handles are unaffected.
78
+ */
79
+ readonly viewer?: Viewer;
80
+ /**
81
+ * Who the surface is for (the value `compile` took). `"broadcast"` refuses every classified
82
+ * handle exactly as `unknown-handle`, even a client naming one the requesting `viewer` may read.
83
+ */
84
+ readonly audience?: Audience;
85
+ /**
86
+ * The host's DECLARED class per tool (`toolKey`), a read-time floor under every handle calling
87
+ * that tool (`@loomwork/ui-core` `DeclaredToolClasses`): a handle whose stored nodes carry no class
88
+ * is still gated as strictly as its tool is declared. Absent = only the experience's own classes.
89
+ */
90
+ readonly declaredToolClasses?: DeclaredToolClasses;
91
+ /**
92
+ * What a tool the `declaredToolClasses` table does not list reads as: `"allow"` (default) = as the
93
+ * experience itself says; `"deny"` = no viewer may read it (only with a table present).
94
+ */
95
+ readonly undeclared?: UndeclaredToolPolicy;
96
+ /** Keys a `principal`-scoped cache entry; without it such a provider is never cached. */
97
+ readonly principalId?: string;
98
+ readonly onDrift?: (report: DriftReport) => void;
99
+ /**
100
+ * Every finding a load produced, IN FULL (tool, step, script, hashes, exception text), for the
101
+ * server's own logs. What the client receives is only its public form (`publicFinding`). A
102
+ * throw here, or a rejection of the promise it returns, is swallowed: a logging hook never
103
+ * changes what a client is answered.
104
+ */
105
+ readonly onFinding?: (handle: string, finding: Finding) => unknown;
106
+ /**
107
+ * At most this many transient calls in flight across the whole batch. Default
108
+ * `DEFAULT_MAX_CONCURRENT_CALLS` (32): a client decides how many handles a batch carries, so
109
+ * the server bounds the upstream fan-out unless it is told otherwise. It bounds ONE batch: for
110
+ * a server-wide bound, wrap the invoker once in the exported `limitInvoker` and share that
111
+ * wrapped invoker across every batch.
112
+ */
113
+ readonly maxConcurrentCalls?: number;
114
+ /**
115
+ * A batch longer than this is refused whole (`batch-too-large`). Default `DEFAULT_MAX_BATCH`.
116
+ * The refusal is still one result per request, so a transport should bound request bodies.
117
+ */
118
+ readonly maxBatch?: number;
119
+ }
120
+ /**
121
+ * The public form of a finding: `severity` kept; the code kept when it is one of the fixed codes
122
+ * or a loom-declared channel (otherwise the generic `tool-failed` / `reshape-failed` /
123
+ * `load-failed` / `load-warning`); the message a template keyed by that code, naming only the
124
+ * (client-supplied) handle — never a node, step, tool, script, hash, argument, channel or
125
+ * exception text.
126
+ */
127
+ export declare function publicFinding(handle: string | undefined, finding: Finding): Finding;
128
+ /**
129
+ * The server half of split hydration: answer a BATCH of handle loads for `exp` in one pass — one
130
+ * clock read (`opts.now`, else `Date.now()` once), one step memo in front of one call cap
131
+ * (`maxConcurrentCalls`, default 32), so identical steps across the batch's providers run once.
132
+ * The memo is keyed by principal too, and one call serves one principal (`opts.principal`), so
133
+ * two viewers never share a call. Every failure, a malformed request included, is a result, one per request
134
+ * in request order (a batch that is not a list gets one `invalid-request`) — and every finding a
135
+ * client receives is its PUBLIC form (`publicFinding`); the full one goes to `onFinding`. Rejects
136
+ * with `TypeError` only for a server misconfiguration (`maxBatch`, `maxConcurrentCalls`).
137
+ */
138
+ export declare function serveHandles(exp: Experience, requests: readonly HandleLoadRequest[], opts: ServeOptions): Promise<HandleLoadResult[]>;
139
+ /**
140
+ * Answer ONE handle load for `exp` — a batch of one (`serveHandles`). `opts.now` should be the
141
+ * server's clock: relative time is resolved here, never on the client.
142
+ */
143
+ export declare function serveHandle(exp: Experience, request: HandleLoadRequest, opts: ServeOptions): Promise<HandleLoadResult>;
144
+ /**
145
+ * `handleInventory`, `serveHandle` and `serveHandles` with the host's {@link ReadPolicy} bound
146
+ * ONCE. Every call applies it, so a host that wires the policy here cannot leak by forgetting it on
147
+ * one call (the inventory naming a handle the loader then refuses, or the reverse). A per-call
148
+ * `declaredToolClasses` / `undeclared` may only ADD to it (`mergeReadPolicy`): `{}` or `undefined`
149
+ * cannot lower what was bound. Prefer this to the free functions, which take the policy per call
150
+ * and are the low-level API.
151
+ */
152
+ export interface ServeSurface {
153
+ /** The bound policy (a copy; read-only). */
154
+ readonly policy: ReadPolicy;
155
+ readonly handleInventory: (exp: Experience, opts: InventoryOptions) => ReturnType<typeof handleInventory>;
156
+ readonly serveHandles: (exp: Experience, requests: readonly HandleLoadRequest[], opts: ServeOptions) => Promise<HandleLoadResult[]>;
157
+ readonly serveHandle: (exp: Experience, request: HandleLoadRequest, opts: ServeOptions) => Promise<HandleLoadResult>;
158
+ }
159
+ export declare function createServeSurface(policy?: ReadPolicy): ServeSurface;
160
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/split/server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,496 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_MAX_CONCURRENT_CALLS = exports.DEFAULT_MAX_BATCH = void 0;
4
+ exports.handleInventory = handleInventory;
5
+ exports.publicFinding = publicFinding;
6
+ exports.serveHandles = serveHandles;
7
+ exports.serveHandle = serveHandle;
8
+ exports.createServeSurface = createServeSurface;
9
+ const plugin_mcp_1 = require("@loomwork/plugin-mcp");
10
+ const plugin_sandbox_1 = require("@loomwork/plugin-sandbox");
11
+ const ui_core_1 = require("@loomwork/ui-core");
12
+ const cache_1 = require("../cache");
13
+ const contentRefs_1 = require("../contentRefs");
14
+ const invokers_1 = require("../invokers");
15
+ const params_1 = require("../params");
16
+ const projection_1 = require("../projection");
17
+ const provider_1 = require("../provider");
18
+ const restricted_1 = require("../restricted");
19
+ const scheduler_1 = require("../scheduler");
20
+ const shared_1 = require("./shared");
21
+ /**
22
+ * What a client may know about an experience's data plane: never a provider. Throws `TypeError`
23
+ * without a `revisionOf`, or when it returns a non-string, an empty string or the fingerprint.
24
+ */
25
+ function handleInventory(exp, opts) {
26
+ const revisionOf = opts?.revisionOf;
27
+ if (typeof revisionOf !== "function")
28
+ throw new TypeError("handleInventory: a revisionOf (a keyed hash) is required");
29
+ const findings = [];
30
+ const handles = [];
31
+ if ((0, ui_core_1.treeDepth)(exp.root) > ui_core_1.MAX_TREE_DEPTH) {
32
+ findings.push((0, scheduler_1.tooDeep)(exp.experienceId, ui_core_1.MAX_TREE_DEPTH));
33
+ }
34
+ else {
35
+ const split = (0, restricted_1.partitionByViewer)((0, ui_core_1.experienceHandles)(exp, opts.declaredToolClasses, opts.undeclared), (0, restricted_1.snapshotViewer)(opts.viewer, opts.audience));
36
+ findings.push(...split.advisory);
37
+ for (const use of split.allowed) {
38
+ if ((0, projection_1.isReservedHandle)(use.handle)) {
39
+ findings.push((0, shared_1.reservedFinding)(use.handle));
40
+ continue;
41
+ }
42
+ const spec = Object.hasOwn(exp.providers, use.handle) ? exp.providers[use.handle] : undefined;
43
+ if (spec === undefined) {
44
+ const f = {
45
+ code: "unknown-handle",
46
+ severity: "error",
47
+ message: `nodes ${use.nodeIds.join(", ")} bind "${use.handle}", which has no provider`,
48
+ };
49
+ findings.push(f);
50
+ handles.push({
51
+ handle: use.handle,
52
+ priority: use.priority,
53
+ params: [],
54
+ revision: "",
55
+ finding: f,
56
+ });
57
+ continue;
58
+ }
59
+ handles.push({
60
+ handle: use.handle,
61
+ priority: use.priority,
62
+ params: (0, params_1.providerParams)(spec),
63
+ revision: publicRevision(revisionOf, use.handle, spec),
64
+ });
65
+ }
66
+ }
67
+ return {
68
+ inventory: { experienceId: exp.experienceId, versionHash: exp.versionHash, handles },
69
+ findings,
70
+ };
71
+ }
72
+ function publicRevision(revisionOf, handle, spec) {
73
+ const fingerprint = (0, cache_1.specFingerprint)(spec);
74
+ const revision = revisionOf(handle, fingerprint);
75
+ if (typeof revision !== "string" || revision === "")
76
+ throw new TypeError(`handleInventory: revisionOf returned ${revision === "" ? "an empty string" : `a ${typeof revision}`} for "${handle}"`);
77
+ if (revision.includes(fingerprint))
78
+ throw new TypeError(`handleInventory: revisionOf returned the provider fingerprint for "${handle}"; it must be keyed`);
79
+ return revision;
80
+ }
81
+ /** The most requests one `serveHandles` call serves by default (`ServeOptions.maxBatch`). */
82
+ exports.DEFAULT_MAX_BATCH = 256;
83
+ /**
84
+ * The most transient calls one `serveHandles` batch has in flight by default
85
+ * (`ServeOptions.maxConcurrentCalls`): the batch is client-sized, so the server bounds its fan-out.
86
+ */
87
+ exports.DEFAULT_MAX_CONCURRENT_CALLS = 32;
88
+ /**
89
+ * The failure channels loom itself declares (ADR 0060) — `mcp.invoke`'s and the sandbox's, derived
90
+ * from loom's own exports so the list cannot drift. A `tool-<channel>` / `reshape-<channel>` code
91
+ * is built from whatever channel the (pluggable) invoker threw, which is arbitrary text; only
92
+ * these are served as they are.
93
+ */
94
+ const TOOL_CHANNELS = new Set(Object.keys(plugin_mcp_1.MCP_INVOKE_ERRORS));
95
+ const RESHAPE_CHANNELS = new Set(Object.keys(plugin_sandbox_1.SANDBOX_ERRORS));
96
+ /**
97
+ * Every other code a served finding may carry: the fixed codes hydration and the server mint
98
+ * themselves. Anything else is replaced (`load-failed` / `load-warning`) — the code is a fixed
99
+ * vocabulary, never text a provider or an invoker chose.
100
+ */
101
+ const FIXED_CODES = new Set([
102
+ "tool-failed",
103
+ "tool-targetUnregistered",
104
+ "provider-kind-unregistered",
105
+ "reshape-failed",
106
+ "reshape-unregistered",
107
+ "schema-drift",
108
+ "schema-drift-proceeded",
109
+ "output-shape-drift",
110
+ "output-shape-unhashable",
111
+ "output-shape-unpinned",
112
+ "output-contract-drift",
113
+ "output-contract-missing",
114
+ "output-contract-unbound",
115
+ "param-not-overridable",
116
+ "param-templated",
117
+ "param-template-refused",
118
+ "invalid-params",
119
+ "invalid-time-token",
120
+ "argument-hook-failed",
121
+ "unresolved-step-reference",
122
+ "upstream-empty",
123
+ "dependency-failed",
124
+ "provider-failed",
125
+ "content-ref-refused",
126
+ "empty-graph",
127
+ "graph-cycle",
128
+ "aborted",
129
+ "invalid-request",
130
+ "version-mismatch",
131
+ "invalid-handle",
132
+ "unknown-handle",
133
+ "batch-too-large",
134
+ "tree-too-deep",
135
+ "hydration-internal",
136
+ ]);
137
+ function publicCode(finding) {
138
+ const { code } = finding;
139
+ if (FIXED_CODES.has(code))
140
+ return code;
141
+ if (code.startsWith("tool-"))
142
+ return TOOL_CHANNELS.has(code.slice(5)) ? code : "tool-failed";
143
+ if (code.startsWith("reshape-"))
144
+ return RESHAPE_CHANNELS.has(code.slice(8)) ? code : "reshape-failed";
145
+ return finding.severity === "warn" ? "load-warning" : "load-failed";
146
+ }
147
+ /**
148
+ * What a client may be told about a finding: a fixed text keyed by its PUBLIC code. The fixed
149
+ * codes' own cases run FIRST, so one of them that happens to share a `tool-`/`reshape-` prefix
150
+ * with an allow-listed channel code (`reshape-unregistered`, a hydration-minted code, not a
151
+ * channel) gets its own text instead of the generic prefix fallback below.
152
+ */
153
+ function publicText(code, severity) {
154
+ switch (code) {
155
+ case "schema-drift-proceeded":
156
+ return "input schema changed; served with the observed schema";
157
+ case "schema-drift":
158
+ return "input schema changed; waiting for a repair";
159
+ case "output-shape-drift":
160
+ case "output-shape-unhashable":
161
+ case "output-contract-drift":
162
+ return "the output no longer matches what it declares; waiting for a repair";
163
+ case "output-shape-unpinned":
164
+ return "the output is served unchecked";
165
+ case "output-contract-missing":
166
+ case "output-contract-unbound":
167
+ return "the output cannot be checked";
168
+ case "provider-kind-unregistered":
169
+ return "no data provider is registered for this view's source";
170
+ case "tool-targetUnregistered":
171
+ return "this view's source is not available";
172
+ case "reshape-unregistered":
173
+ return "the output could not be reshaped: the named reshaper is not registered";
174
+ case "upstream-empty":
175
+ return "no data for this view";
176
+ case "param-not-overridable":
177
+ return "a param was ignored";
178
+ case "invalid-params":
179
+ case "invalid-time-token":
180
+ return "the params were refused";
181
+ case "content-ref-refused":
182
+ return "a content-reference argument was refused";
183
+ case "aborted":
184
+ return "the load was aborted";
185
+ case "invalid-request":
186
+ return "the request is malformed";
187
+ case "version-mismatch":
188
+ return "the request is for another version of the experience";
189
+ case "invalid-handle":
190
+ return "the handle is reserved";
191
+ case "unknown-handle":
192
+ return "not a handle this experience binds";
193
+ case "batch-too-large":
194
+ return "the batch is too large";
195
+ case "tree-too-deep":
196
+ return "the experience cannot be served";
197
+ case "hydration-internal":
198
+ return "an internal error occurred";
199
+ }
200
+ if (code.startsWith("tool-"))
201
+ return "a tool call failed";
202
+ if (code.startsWith("reshape-"))
203
+ return "the output could not be reshaped";
204
+ return severity === "warn" ? "loaded with a warning" : "could not be loaded";
205
+ }
206
+ /**
207
+ * The public form of a finding: `severity` kept; the code kept when it is one of the fixed codes
208
+ * or a loom-declared channel (otherwise the generic `tool-failed` / `reshape-failed` /
209
+ * `load-failed` / `load-warning`); the message a template keyed by that code, naming only the
210
+ * (client-supplied) handle — never a node, step, tool, script, hash, argument, channel or
211
+ * exception text.
212
+ */
213
+ function publicFinding(handle, finding) {
214
+ const code = publicCode(finding);
215
+ const text = publicText(code, finding.severity);
216
+ return {
217
+ code,
218
+ severity: finding.severity,
219
+ message: handle === undefined ? text : `handle ${JSON.stringify(handle.slice(0, 128))}: ${text}`,
220
+ };
221
+ }
222
+ /** Hands each finding to `onFinding` once, in full; a throwing hook is swallowed. */
223
+ function reporter(opts, handle) {
224
+ const seen = new Set();
225
+ const report = (finding) => {
226
+ if (seen.has(finding))
227
+ return;
228
+ seen.add(finding);
229
+ try {
230
+ const ret = opts.onFinding?.(handle, finding);
231
+ // An async hook's rejection is its own: never an unhandled rejection in the server.
232
+ if (ret !== null &&
233
+ typeof ret === "object" &&
234
+ typeof ret.then === "function")
235
+ Promise.resolve(ret).catch(() => { });
236
+ }
237
+ catch {
238
+ // A logging hook must never change what the client is answered (nor, if its `then`
239
+ // getter throws, what the server does).
240
+ }
241
+ };
242
+ return Object.assign(report, {
243
+ silence: (finding) => {
244
+ seen.add(finding);
245
+ },
246
+ });
247
+ }
248
+ /** Report the result's finding in full, and answer the client with its public form. */
249
+ function publicResult(handle, result, report) {
250
+ if (result.finding === undefined)
251
+ return result;
252
+ report(result.finding);
253
+ const finding = publicFinding(handle, result.finding);
254
+ return result.state === "ready"
255
+ ? { state: "ready", value: result.value, finding }
256
+ : { state: result.state, finding };
257
+ }
258
+ /**
259
+ * The server half of split hydration: answer a BATCH of handle loads for `exp` in one pass — one
260
+ * clock read (`opts.now`, else `Date.now()` once), one step memo in front of one call cap
261
+ * (`maxConcurrentCalls`, default 32), so identical steps across the batch's providers run once.
262
+ * The memo is keyed by principal too, and one call serves one principal (`opts.principal`), so
263
+ * two viewers never share a call. Every failure, a malformed request included, is a result, one per request
264
+ * in request order (a batch that is not a list gets one `invalid-request`) — and every finding a
265
+ * client receives is its PUBLIC form (`publicFinding`); the full one goes to `onFinding`. Rejects
266
+ * with `TypeError` only for a server misconfiguration (`maxBatch`, `maxConcurrentCalls`).
267
+ */
268
+ async function serveHandles(exp, requests, opts) {
269
+ // Server configuration, not client input: a bad bound is a programming error, and loud.
270
+ (0, contentRefs_1.assertContentRefConfig)(opts.contentRefArgs, "serveHandles");
271
+ const max = opts.maxBatch ?? exports.DEFAULT_MAX_BATCH;
272
+ if (!Number.isInteger(max) || max < 1)
273
+ throw new TypeError(`serveHandles: maxBatch must be a positive integer, got ${String(max)}`);
274
+ const cap = opts.maxConcurrentCalls ?? exports.DEFAULT_MAX_CONCURRENT_CALLS;
275
+ if (!Number.isInteger(cap) || cap < 1)
276
+ throw new TypeError(`serveHandles: maxConcurrentCalls must be a positive integer, got ${String(cap)}`);
277
+ const batch = reporter(opts, "");
278
+ if (!Array.isArray(requests))
279
+ return [
280
+ publicResult(undefined, (0, shared_1.refuse)("invalid-request", "a batch load is a list of load requests"), batch),
281
+ ];
282
+ if (requests.length > max) {
283
+ const tooMany = publicResult(undefined, (0, shared_1.refuse)("batch-too-large", `a batch of ${requests.length} requests exceeds the bound of ${max}`), batch);
284
+ return requests.map(() => tooMany);
285
+ }
286
+ // Checked before anything walks the tree for handles or kinds.
287
+ if ((0, ui_core_1.treeDepth)(exp.root) > ui_core_1.MAX_TREE_DEPTH) {
288
+ const deep = publicResult(undefined, { state: "error", finding: (0, scheduler_1.tooDeep)(exp.experienceId, ui_core_1.MAX_TREE_DEPTH) }, batch);
289
+ return requests.map(() => deep);
290
+ }
291
+ let pass;
292
+ try {
293
+ pass = openPass(exp, opts);
294
+ }
295
+ catch (e) {
296
+ const failed = publicResult(undefined, (0, shared_1.refuse)("hydration-internal", (0, shared_1.errorMessage)(e)), batch);
297
+ return requests.map(() => failed);
298
+ }
299
+ return Promise.all(requests.map(async (r) => {
300
+ // Snapshotted ONCE, inside a guard: a hostile getter or Proxy costs this request, not the
301
+ // batch. `experienceId`/`versionHash`/`handle` are primitives, captured by value right here.
302
+ // `params`, an object, is NOT safe by the same copy — a field holding a live reference can
303
+ // still answer a later read differently — so `serveOne` takes its own single deep-copy-and-
304
+ // validate pass over it (`snapshotParams`) before anything reads a key of it twice.
305
+ let snapshot;
306
+ try {
307
+ snapshot =
308
+ r !== null && typeof r === "object"
309
+ ? {
310
+ experienceId: r.experienceId,
311
+ versionHash: r.versionHash,
312
+ handle: r.handle,
313
+ params: r.params,
314
+ }
315
+ : null;
316
+ }
317
+ catch {
318
+ const report = reporter(opts, "");
319
+ return publicResult(undefined, (0, shared_1.refuse)("invalid-request", "the request cannot be read"), report);
320
+ }
321
+ const handle = typeof snapshot?.handle === "string" ? snapshot.handle : undefined;
322
+ const report = reporter(opts, handle ?? "");
323
+ return publicResult(handle, await serveOne(exp, snapshot, opts, pass, report), report);
324
+ }));
325
+ }
326
+ /**
327
+ * Answer ONE handle load for `exp` — a batch of one (`serveHandles`). `opts.now` should be the
328
+ * server's clock: relative time is resolved here, never on the client.
329
+ */
330
+ async function serveHandle(exp, request, opts) {
331
+ const [result] = await serveHandles(exp, [request], opts);
332
+ return result ?? (0, shared_1.refuse)("hydration-internal", "no result for the request");
333
+ }
334
+ function openPass(exp, opts) {
335
+ const { outputKinds, invoker, now, signal, principal } = opts;
336
+ const capped = (0, invokers_1.limitInvoker)(invoker, opts.maxConcurrentCalls ?? exports.DEFAULT_MAX_CONCURRENT_CALLS);
337
+ const exec = {
338
+ // One memo per batch, in front of the cap: a memo hit never takes a slot.
339
+ invoker: (0, invokers_1.memoInvoker)(capped),
340
+ now: now ?? Date.now(),
341
+ ...(signal !== undefined && { signal }),
342
+ ...(principal !== undefined && { principal }),
343
+ ...(opts.concurrency !== undefined && { concurrency: opts.concurrency }),
344
+ ...(opts.captureSample !== undefined && { captureSample: opts.captureSample }),
345
+ ...(opts.argumentHook !== undefined && { argumentHook: opts.argumentHook }),
346
+ ...(opts.contentRefArgs !== "none" && { contentRefArgs: opts.contentRefArgs }),
347
+ ...(opts.acceptContentRefParam !== undefined && {
348
+ acceptContentRefParam: opts.acceptContentRefParam,
349
+ }),
350
+ ...(opts.inputDrift !== undefined && { inputDrift: opts.inputDrift }),
351
+ ...(opts.reshapers !== undefined && { reshapers: opts.reshapers }),
352
+ ...(opts.providers !== undefined && { providers: opts.providers }),
353
+ ...(outputKinds?.catalog.valueContracts !== undefined && {
354
+ contracts: outputKinds.catalog.valueContracts,
355
+ }),
356
+ };
357
+ return {
358
+ exec,
359
+ // `bound` is handle-id collection only (no class read); `restricted` below takes the table.
360
+ bound: new Set((0, ui_core_1.experienceHandles)(exp).map((u) => u.handle)),
361
+ restricted: new Set((0, restricted_1.partitionByViewer)((0, ui_core_1.experienceHandles)(exp, opts.declaredToolClasses, opts.undeclared), (0, restricted_1.snapshotViewer)(opts.viewer, opts.audience)).restricted.map((u) => u.handle)),
362
+ // U9: media-typed nodes mark their handle as media even with NO `outputKinds` (fail closed).
363
+ kinds: (0, ui_core_1.handleKindsWithMedia)(exp, outputKinds?.catalog, outputKinds?.surface),
364
+ mediaWithoutKinds: outputKinds === undefined && (0, ui_core_1.hasMediaTypeNodes)(exp),
365
+ refsUnguarded: opts.contentRefArgs === "none" && (0, ui_core_1.hasMediaTypeNodes)(exp),
366
+ };
367
+ }
368
+ async function serveOne(exp, request, opts, pass, report) {
369
+ try {
370
+ if (request === null || typeof request !== "object" || typeof request.handle !== "string")
371
+ return (0, shared_1.refuse)("invalid-request", "a load request names a handle");
372
+ if (request.experienceId !== exp.experienceId || request.versionHash !== exp.versionHash)
373
+ return (0, shared_1.refuse)("version-mismatch", `the request is for ${String(request.experienceId)}@${String(request.versionHash)}, not ${exp.experienceId}@${exp.versionHash}`);
374
+ const handle = request.handle;
375
+ if ((0, projection_1.isReservedHandle)(handle))
376
+ return { state: "error", finding: (0, shared_1.reservedFinding)(handle) };
377
+ // Checked first, before the provider is even looked up: a restricted handle is never loaded,
378
+ // cached or invoked, for any request, whatever its params.
379
+ if (pass.restricted.has(handle)) {
380
+ // The server's log gets the true reason ONCE; the client's answer is the public
381
+ // `unknown-handle`, which is silenced so it is not logged as a second, contradicting finding.
382
+ report((0, restricted_1.restrictedFinding)());
383
+ const answer = (0, shared_1.refuse)("unknown-handle", `"${handle}" is not a handle this experience binds`);
384
+ if (answer.finding !== undefined)
385
+ report.silence(answer.finding);
386
+ return answer;
387
+ }
388
+ // Only what a node binds is served: a provider no node binds is not the client's to ask for.
389
+ const declared = pass.bound.has(handle) && Object.hasOwn(exp.providers, handle)
390
+ ? exp.providers[handle]
391
+ : undefined;
392
+ if (declared === undefined)
393
+ return (0, shared_1.refuse)("unknown-handle", `"${handle}" is not a handle this experience binds`);
394
+ // U9: a handle feeding a media widget is ALWAYS contract-checked, whatever the provider
395
+ // declared (a shape pin passes inline bytes or a locator into a client payload and the cache).
396
+ const mediaBound = (pass.kinds.get(handle) ?? []).some((k) => (0, ui_core_1.isMediaKind)(k) || (0, ui_core_1.isChatKind)(k));
397
+ const spec = mediaBound && declared.outputCheck !== "contract"
398
+ ? { ...declared, outputCheck: "contract" }
399
+ : declared;
400
+ if (mediaBound && pass.mediaWithoutKinds)
401
+ report({
402
+ code: "media-output-kinds-missing",
403
+ severity: "warn",
404
+ message: "the experience has media nodes and serve has no outputKinds: media handles are contract-checked by node type, but pass outputKinds so kinds resolve through the catalogue",
405
+ });
406
+ if (mediaBound && pass.refsUnguarded)
407
+ report({
408
+ code: "media-content-refs-unguarded",
409
+ severity: "warn",
410
+ message: 'the experience has media nodes and serve runs with contentRefArgs: "none": a content-reference tool argument is not checked; derive the table with contentRefArgsFrom(capabilities)',
411
+ });
412
+ // ONE deep-copy-and-validate pass over the request's params (`snapshotParams`): the result is
413
+ // plain, detached data with no accessor of its own, so `relevantParams`/`pickParams` below can
414
+ // never re-trigger a caller-supplied getter that answered differently than the check just did.
415
+ const snap = (0, params_1.snapshotParams)(request.params ?? {});
416
+ if (!snap.ok)
417
+ return { state: "error", finding: snap.finding };
418
+ const params = (0, params_1.relevantParams)(spec, snap.params);
419
+ // As in-process: only a contract provider's value is answerable to a kind.
420
+ const kinds = spec.outputCheck === "contract" ? (pass.kinds.get(handle) ?? []) : undefined;
421
+ const variant = kinds === undefined
422
+ ? { params }
423
+ : {
424
+ params,
425
+ kinds,
426
+ ...(pass.exec.contracts !== undefined && { contracts: pass.exec.contracts }),
427
+ };
428
+ // U9b: the content-reference guard (gate included) runs BEFORE the cache is consulted: a cached
429
+ // value was produced for an argument some principal's gate accepted once, and serving it to another
430
+ // principal, or after the host revoked it, would skip the gate. A refusal is not served from the
431
+ // cache (the load below refuses it); a param that fills a guarded argument is cached per principal.
432
+ const refs = pass.exec.contentRefArgs;
433
+ const verdict = refs === undefined
434
+ ? undefined
435
+ : (0, contentRefs_1.inspectContentRefs)(spec, params, (0, contentRefs_1.contentRefEnv)(refs, spec, pass.exec.acceptContentRefParam, {
436
+ principalId: opts.principalId,
437
+ principal: pass.exec.principal,
438
+ }));
439
+ const cacheSpec = verdict?.paramFilled ? (0, contentRefs_1.principalScoped)(spec) : spec;
440
+ const hit = verdict?.refusal === undefined
441
+ ? opts.cache?.get(cacheSpec, opts.principalId, variant)
442
+ : undefined;
443
+ if (hit)
444
+ return { state: "ready", value: hit.value };
445
+ const outcome = await (0, provider_1.executeProvider)(spec, {
446
+ ...pass.exec,
447
+ params,
448
+ ...(opts.principalId !== undefined && { principalId: opts.principalId }),
449
+ ...(kinds !== undefined && { kinds }),
450
+ });
451
+ for (const f of outcome.findings)
452
+ report(f);
453
+ if (!outcome.ok)
454
+ report(outcome.finding);
455
+ const drift = outcome.drift ?? [];
456
+ // Never cache a result that proceeded past an input drift (`report-and-proceed`): the key
457
+ // does not carry the drift mode, so a later request served under the default (`report`)
458
+ // would get it as a clean `ready` — no stale, no `onDrift`, no `onFinding`.
459
+ if (outcome.ok && drift.length === 0)
460
+ opts.cache?.set(cacheSpec, opts.principalId, outcome.value, variant);
461
+ if (drift.length > 0)
462
+ opts.onDrift?.({
463
+ experienceId: exp.experienceId,
464
+ versionHash: exp.versionHash,
465
+ handle,
466
+ spec,
467
+ drift,
468
+ findings: outcome.findings,
469
+ // Only a PROCEEDED input drift reaches here beside a delivered answer — a value, or an
470
+ // `empty` one (the retried upstream answered with no rows). `empty` itself is never drift.
471
+ proceeded: outcome.ok || outcome.finding.code === "upstream-empty",
472
+ });
473
+ return (0, provider_1.outcomeResult)(outcome);
474
+ }
475
+ catch (e) {
476
+ return (0, shared_1.refuse)("hydration-internal", (0, shared_1.errorMessage)(e));
477
+ }
478
+ }
479
+ function createServeSurface(policy = {}) {
480
+ const bound = (0, ui_core_1.mergeReadPolicy)(policy);
481
+ const joined = (opts) => {
482
+ const m = (0, ui_core_1.mergeReadPolicy)(bound, opts);
483
+ return {
484
+ ...opts,
485
+ ...(m.declaredToolClasses !== undefined && { declaredToolClasses: m.declaredToolClasses }),
486
+ ...(m.undeclared !== undefined && { undeclared: m.undeclared }),
487
+ };
488
+ };
489
+ return {
490
+ policy: bound,
491
+ handleInventory: (exp, opts) => handleInventory(exp, joined(opts)),
492
+ serveHandles: (exp, requests, opts) => serveHandles(exp, requests, joined(opts)),
493
+ serveHandle: (exp, request, opts) => serveHandle(exp, request, joined(opts)),
494
+ };
495
+ }
496
+ //# sourceMappingURL=server.js.map