@volter/world-core 3.0.38 → 3.0.40

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/app-route.cjs +26 -25
  2. package/app-route.d.cts +1 -0
  3. package/dist/app-route.cjs +26 -25
  4. package/dist/app-route.d.cts +1 -0
  5. package/dist/generated/pack-facts.json +409 -352
  6. package/dist/inject.cjs +474 -95
  7. package/dist/network-policy.cjs +110 -13
  8. package/dist/network-policy.d.cts +8 -3
  9. package/dist/src/actions.js +2 -1
  10. package/dist/src/anthropic-wire.d.ts +2 -0
  11. package/dist/src/anthropic-wire.js +5 -0
  12. package/dist/src/attestation.d.ts +6 -0
  13. package/dist/src/attestation.js +27 -0
  14. package/dist/src/bytes.d.ts +3 -0
  15. package/dist/src/bytes.js +17 -0
  16. package/dist/src/changeset.js +2 -1
  17. package/dist/src/clickhouse/sql.js +73 -14
  18. package/dist/src/derived-core.d.ts +210 -32
  19. package/dist/src/derived-core.js +510 -244
  20. package/dist/src/derived-real.d.ts +1 -1
  21. package/dist/src/derived-real.js +704 -93
  22. package/dist/src/derived.d.ts +18 -2
  23. package/dist/src/derived.js +21 -2
  24. package/dist/src/events.d.ts +28 -45
  25. package/dist/src/events.js +40 -58
  26. package/dist/src/exact-json.d.ts +10 -0
  27. package/dist/src/exact-json.js +98 -0
  28. package/dist/src/executor.d.ts +17 -0
  29. package/dist/src/executor.js +91 -30
  30. package/dist/src/file-response.js +1 -1
  31. package/dist/src/form.d.ts +8 -0
  32. package/dist/src/form.js +76 -0
  33. package/dist/src/git/format.d.ts +16 -0
  34. package/dist/src/git/format.js +110 -0
  35. package/dist/src/git/index.d.ts +1 -0
  36. package/dist/src/git/index.js +1 -0
  37. package/dist/src/git/lfs.d.ts +1 -1
  38. package/dist/src/git/lfs.js +4 -3
  39. package/dist/src/graphql-wire.d.ts +6 -0
  40. package/dist/src/graphql-wire.js +6 -3
  41. package/dist/src/grpc-wire.js +2 -1
  42. package/dist/src/head.d.ts +1 -0
  43. package/dist/src/head.js +15 -4
  44. package/dist/src/held-reads.d.ts +6 -0
  45. package/dist/src/held-reads.js +60 -0
  46. package/dist/src/history.js +5 -4
  47. package/dist/src/host-port.d.ts +5 -0
  48. package/dist/src/host-port.js +14 -0
  49. package/dist/src/index.d.ts +11 -3
  50. package/dist/src/index.js +9 -4
  51. package/dist/src/log.js +12 -7
  52. package/dist/src/machines.d.ts +81 -5
  53. package/dist/src/machines.js +125 -9
  54. package/dist/src/managed-database.d.ts +15 -11
  55. package/dist/src/managed-database.js +38 -37
  56. package/dist/src/multipart.d.ts +21 -3
  57. package/dist/src/multipart.js +112 -38
  58. package/dist/src/observe.d.ts +3 -0
  59. package/dist/src/observe.js +6 -2
  60. package/dist/src/openai-wire.d.ts +16 -0
  61. package/dist/src/openai-wire.js +43 -12
  62. package/dist/src/pack-fetch.d.ts +4 -6
  63. package/dist/src/pack-fetch.js +189 -57
  64. package/dist/src/packRegistry.d.ts +7 -1
  65. package/dist/src/packRegistry.js +20 -3
  66. package/dist/src/private-transport.d.ts +1 -0
  67. package/dist/src/private-transport.js +11 -0
  68. package/dist/src/protobuf.js +10 -4
  69. package/dist/src/rateBudget.d.ts +8 -1
  70. package/dist/src/rateBudget.js +1 -0
  71. package/dist/src/redis/engine.d.ts +14 -5
  72. package/dist/src/redis/engine.js +271 -72
  73. package/dist/src/redis/frames.d.ts +17 -0
  74. package/dist/src/redis/frames.js +123 -0
  75. package/dist/src/redis/index.d.ts +6 -0
  76. package/dist/src/redis/index.js +6 -0
  77. package/dist/src/redis/json.d.ts +2 -0
  78. package/dist/src/redis/json.js +14 -0
  79. package/dist/src/redis/lua-libs.d.ts +4 -0
  80. package/dist/src/redis/lua-libs.js +173 -0
  81. package/dist/src/redis/lua.d.ts +1 -0
  82. package/dist/src/redis/lua.js +36 -0
  83. package/dist/src/redis/member-storage.d.ts +5 -0
  84. package/dist/src/redis/member-storage.js +75 -0
  85. package/dist/src/redis/resp2.d.ts +4 -0
  86. package/dist/src/redis/resp2.js +29 -0
  87. package/dist/src/redis/stream.d.ts +14 -0
  88. package/dist/src/redis/stream.js +335 -0
  89. package/dist/src/remote-execute.d.ts +2 -2
  90. package/dist/src/request-body.d.ts +11 -0
  91. package/dist/src/request-body.js +11 -0
  92. package/dist/src/runtime.d.ts +6 -5
  93. package/dist/src/runtime.js +5 -5
  94. package/dist/src/s3/wire.d.ts +4 -4
  95. package/dist/src/s3/wire.js +5 -3
  96. package/dist/src/scenario.d.ts +22 -0
  97. package/dist/src/scenario.js +35 -31
  98. package/dist/src/serve-http.d.ts +2 -1
  99. package/dist/src/serve-http.js +10 -1
  100. package/dist/src/serve.js +7 -4
  101. package/dist/src/signing.d.ts +23 -5
  102. package/dist/src/signing.js +74 -11
  103. package/dist/src/sigv4.d.ts +3 -1
  104. package/dist/src/sigv4.js +4 -2
  105. package/dist/src/smtp.js +2 -1
  106. package/dist/src/sockets.js +8 -3
  107. package/dist/src/state-system.d.ts +3 -0
  108. package/dist/src/storage.js +5 -2
  109. package/dist/src/twin-fetch.d.ts +3 -1
  110. package/dist/src/twin-fetch.js +3 -2
  111. package/dist/src/vendor-call.d.ts +10 -3
  112. package/dist/src/vendor-call.js +58 -14
  113. package/dist/test-fixtures/attestation.SOURCE.md +1 -0
  114. package/dist/test-fixtures/attestation.json +50 -0
  115. package/generated/pack-facts.json +409 -352
  116. package/inject.cjs +474 -95
  117. package/network-policy.cjs +110 -13
  118. package/network-policy.d.cts +8 -3
  119. package/package.json +11 -4
  120. package/src/actions.ts +2 -1
  121. package/src/anthropic-wire.ts +6 -0
  122. package/src/attestation.ts +23 -0
  123. package/src/bytes.ts +21 -0
  124. package/src/changeset.ts +2 -1
  125. package/src/clickhouse/sql.ts +50 -16
  126. package/src/derived-core.ts +573 -249
  127. package/src/derived-real.ts +552 -42
  128. package/src/derived.ts +23 -4
  129. package/src/events.ts +54 -85
  130. package/src/exact-json.ts +71 -0
  131. package/src/executor.ts +86 -27
  132. package/src/file-response.ts +1 -1
  133. package/src/form.ts +66 -0
  134. package/src/git/format.ts +89 -0
  135. package/src/git/index.ts +1 -0
  136. package/src/git/lfs.ts +5 -4
  137. package/src/graphql-wire.ts +8 -3
  138. package/src/grpc-wire.ts +2 -1
  139. package/src/head.ts +15 -5
  140. package/src/held-reads.ts +54 -0
  141. package/src/history.ts +5 -4
  142. package/src/host-port.ts +9 -0
  143. package/src/index.ts +10 -4
  144. package/src/log.ts +11 -7
  145. package/src/machines.ts +135 -13
  146. package/src/managed-database.ts +48 -40
  147. package/src/multipart.ts +110 -35
  148. package/src/observe.ts +6 -3
  149. package/src/openai-wire.ts +45 -14
  150. package/src/pack-fetch.ts +173 -50
  151. package/src/packRegistry.ts +27 -4
  152. package/src/private-transport.ts +8 -0
  153. package/src/protobuf.ts +8 -4
  154. package/src/rateBudget.ts +6 -1
  155. package/src/redis/engine.ts +168 -63
  156. package/src/redis/frames.ts +53 -0
  157. package/src/redis/index.ts +6 -0
  158. package/src/redis/json.ts +13 -0
  159. package/src/redis/lua-libs.ts +62 -0
  160. package/src/redis/lua.ts +20 -0
  161. package/src/redis/member-storage.ts +58 -0
  162. package/src/redis/resp2.ts +25 -0
  163. package/src/redis/stream.ts +102 -0
  164. package/src/remote-execute.ts +2 -2
  165. package/src/request-body.ts +12 -0
  166. package/src/runtime.ts +6 -5
  167. package/src/s3/wire.ts +8 -5
  168. package/src/scenario.ts +34 -13
  169. package/src/serve-http.ts +11 -2
  170. package/src/serve.ts +4 -3
  171. package/src/signing.ts +71 -17
  172. package/src/sigv4.ts +5 -3
  173. package/src/smtp.ts +3 -2
  174. package/src/sockets.ts +8 -3
  175. package/src/state-system.ts +3 -0
  176. package/src/storage.ts +5 -2
  177. package/src/twin-fetch.ts +4 -3
  178. package/src/vendor-call.ts +59 -12
  179. package/test-fixtures/attestation.json +50 -0
@@ -14,7 +14,7 @@ const obj = (v: unknown): Row => (v && typeof v === 'object' && !Array.isArray(v
14
14
  const arr = (v: unknown): Row[] => (Array.isArray(v) ? v as Row[] : []);
15
15
 
16
16
  /** A turn: the text, or tool calls, the reasoning when there is some, and why it finished. */
17
- export type WireTurn = { text?: string | null; toolCalls?: Array<{ name: string; arguments: Row }>; reasoning?: string | null; finishReason?: string };
17
+ export type WireTurn = { text?: string | null; toolCalls?: Array<{ name: string; arguments: Row }>; reasoning?: string | null; reasoningDetails?: Row[]; finishReason?: string };
18
18
 
19
19
  /** Tokens, estimated at about four characters a token: a twin's count, not a tokenizer's. */
20
20
  export const estimateTokens = (text: string): number => Math.max(1, Math.ceil(text.length / 4));
@@ -91,7 +91,7 @@ function chatParts(s: ChatShape): { text: string | null; finish: string; calls:
91
91
  export function chatCompletion(s: ChatShape): Row {
92
92
  const p = chatParts(s);
93
93
  const field = s.reasoningField ?? 'reasoning';
94
- const message = { role: 'assistant', content: p.text, refusal: null, ...(p.reasoning ? { [field]: p.reasoning } : {}), ...(p.calls.length ? { tool_calls: p.calls } : {}) };
94
+ const message = { role: 'assistant', content: p.text, refusal: null, ...(p.reasoning ? { [field]: p.reasoning } : {}), ...(s.turn.reasoningDetails ? { reasoning_details: s.turn.reasoningDetails } : {}), ...(p.calls.length ? { tool_calls: p.calls } : {}) };
95
95
  return {
96
96
  id: s.id, ...(s.extra ?? {}), model: s.model, object: 'chat.completion', created: s.created,
97
97
  choices: [{ index: 0, message, logprobs: null, finish_reason: p.finish, ...(s.nativeFinish ? { native_finish_reason: p.finish } : {}) }], usage: p.usage,
@@ -101,21 +101,22 @@ export function chatCompletion(s: ChatShape): Row {
101
101
  /** A chat completion streamed: its chunks (`object: chat.completion.chunk`) as Server-Sent Events, the text in two
102
102
  * deltas, the last chunk carrying the finish reason and the usage, then `data: [DONE]`. `comment` is a keep-alive a
103
103
  * vendor sends first (OpenRouter's `: OPENROUTER PROCESSING`). */
104
- export function chatCompletionSSE(s: ChatShape & { comment?: string }): string {
104
+ export function chatCompletionSSE(s: ChatShape & { comment?: string; separateUsageChunk?: boolean }): string {
105
105
  const p = chatParts(s);
106
106
  const field = s.reasoningField ?? 'reasoning';
107
- const chunk = (delta: Row, last: boolean): string => `data: ${JSON.stringify({
107
+ const chunk = (delta: Row, last: boolean, usage = last): string => `data: ${JSON.stringify({
108
108
  id: s.id, ...((last ? s.extra : s.interimExtra ?? s.extra) ?? {}), model: s.model, object: 'chat.completion.chunk', created: s.created,
109
109
  choices: [{ index: 0, delta, logprobs: null, finish_reason: last ? p.finish : null, ...(s.nativeFinish ? { native_finish_reason: last ? p.finish : null } : {}) }],
110
- ...(last ? { usage: p.usage } : {}),
110
+ ...(usage ? { usage: p.usage } : {}),
111
111
  })}\n\n`;
112
112
  const half = p.text ? Math.ceil(p.text.length / 2) : 0;
113
113
  return [
114
114
  ...(s.comment ? [`: ${s.comment}\n\n`] : []),
115
- ...(p.reasoning ? [chunk({ role: 'assistant', content: '', [field]: p.reasoning }, false)] : []),
115
+ ...(p.reasoning || s.turn.reasoningDetails ? [chunk({ role: 'assistant', content: '', ...(p.reasoning ? { [field]: p.reasoning } : {}), ...(s.turn.reasoningDetails ? { reasoning_details: s.turn.reasoningDetails } : {}) }, false)] : []),
116
116
  ...(p.text ? [chunk({ role: 'assistant', content: p.text.slice(0, half) }, false), ...(p.text.length > half ? [chunk({ content: p.text.slice(half) }, false)] : [])] : []),
117
117
  ...(p.calls.length ? [chunk({ role: 'assistant', content: null, tool_calls: p.calls.map((c, i) => ({ index: i, ...c })) }, false)] : []),
118
- chunk({}, true),
118
+ chunk(s.separateUsageChunk ? { role: 'assistant', content: '' } : {}, true, !s.separateUsageChunk),
119
+ ...(s.separateUsageChunk ? [chunk({ role: 'assistant', content: '' }, true, true)] : []),
119
120
  'data: [DONE]\n\n',
120
121
  ].join('');
121
122
  }
@@ -229,12 +230,15 @@ export type ResponsesShape = {
229
230
  /** ids for the output items: the message, the reasoning, each function call and its call id */
230
231
  itemId: (kind: 'msg' | 'rs' | 'fc' | 'call', i: number) => string;
231
232
  extra?: Row;
233
+ /** Vendor defaults; explicit request values win, including nullable gateway defaults. */
234
+ defaults?: Row;
232
235
  };
233
236
 
234
237
  /** A Responses object (`object: response`, `status: completed`): its output items (the reasoning when there is some,
235
238
  * the message with its output text, or function calls) and its usage. */
236
239
  export function responsesAnswer(s: ResponsesShape): Row {
237
240
  const b = s.body;
241
+ const defaultOf = (field: string, fallback: unknown): unknown => s.defaults && Object.hasOwn(s.defaults, field) ? s.defaults[field] : fallback;
238
242
  let text = s.turn.text ?? null;
239
243
  const max = typeof b.max_output_tokens === 'number' ? b.max_output_tokens : undefined;
240
244
  let incomplete: Row | null = null;
@@ -251,10 +255,10 @@ export function responsesAnswer(s: ResponsesShape): Row {
251
255
  id: s.id, object: 'response', created_at: s.createdAt, status: incomplete ? 'incomplete' : 'completed', ...(s.extra ?? {}),
252
256
  error: null, incomplete_details: incomplete, instructions: b.instructions ?? null, max_output_tokens: max ?? null, model: s.model, output,
253
257
  parallel_tool_calls: b.parallel_tool_calls ?? true, previous_response_id: b.previous_response_id ?? null,
254
- reasoning: { effort: obj(b.reasoning).effort ?? null, summary: obj(b.reasoning).summary ?? null }, store: b.store ?? true,
255
- temperature: b.temperature ?? 1, text: b.text ?? { format: { type: 'text' } }, tool_choice: b.tool_choice ?? 'auto', tools: b.tools ?? [], top_p: b.top_p ?? 1,
258
+ reasoning: { effort: obj(b.reasoning).effort ?? null, summary: obj(b.reasoning).summary ?? null }, store: b.store ?? defaultOf('store', true),
259
+ temperature: b.temperature ?? defaultOf('temperature', 1), text: b.text ?? { format: { type: 'text' } }, tool_choice: b.tool_choice ?? 'auto', tools: b.tools ?? [], top_p: b.top_p ?? defaultOf('top_p', 1),
256
260
  usage: { input_tokens: input, input_tokens_details: { cached_tokens: 0 }, output_tokens: out + reasoningTokens, output_tokens_details: { reasoning_tokens: reasoningTokens }, total_tokens: input + out + reasoningTokens },
257
- metadata: b.metadata ?? {},
261
+ metadata: b.metadata ?? defaultOf('metadata', {}),
258
262
  };
259
263
  }
260
264
 
@@ -278,7 +282,14 @@ export function responsesSSE(s: ResponsesShape): string {
278
282
  events.push({ type: 'response.function_call_arguments.delta', item_id: item.id, output_index: i, delta: item.arguments });
279
283
  events.push({ type: 'response.function_call_arguments.done', item_id: item.id, output_index: i, arguments: item.arguments });
280
284
  } else {
281
- events.push({ type: 'response.output_item.added', output_index: i, item });
285
+ // announced empty, as a message item is: an SDK's accumulator appends each summary part the events below add
286
+ events.push({ type: 'response.output_item.added', output_index: i, item: { ...item, summary: [], status: 'in_progress' } });
287
+ for (const [n, summary] of arr(item.summary).entries()) {
288
+ events.push({ type: 'response.reasoning_summary_part.added', item_id: item.id, output_index: i, summary_index: n, part: { type: 'summary_text', text: '' } });
289
+ events.push({ type: 'response.reasoning_summary_text.delta', item_id: item.id, output_index: i, summary_index: n, delta: summary.text });
290
+ events.push({ type: 'response.reasoning_summary_text.done', item_id: item.id, output_index: i, summary_index: n, text: summary.text });
291
+ events.push({ type: 'response.reasoning_summary_part.done', item_id: item.id, output_index: i, summary_index: n, part: summary });
292
+ }
282
293
  }
283
294
  events.push({ type: 'response.output_item.done', output_index: i, item });
284
295
  }
@@ -298,18 +309,19 @@ export const forbidsTools = (choice: unknown): boolean => choice === 'none' || o
298
309
 
299
310
  /** The turn a scenario's decision scripts (`respond`: text, toolCalls, reasoning, finishReason), or undefined. */
300
311
  export function scenarioTurn(decision: ScenarioDecision | undefined): WireTurn | undefined {
301
- if (!decision || decision.kind !== 'handler') return undefined;
312
+ if (!decision || decision.kind !== 'handler' || decision.fault?.kind === 'generation') return undefined;
302
313
  const r = obj(decision.respond);
303
314
  return {
304
315
  ...(typeof r.text === 'string' ? { text: r.text } : {}),
305
316
  ...(r.toolCalls !== undefined ? { toolCalls: (Array.isArray(r.toolCalls) ? r.toolCalls : [r.toolCalls]) as WireTurn['toolCalls'] } : {}),
306
317
  ...(typeof r.reasoning === 'string' ? { reasoning: r.reasoning } : {}),
318
+ ...(Array.isArray(r.reasoningDetails) ? { reasoningDetails: r.reasoningDetails as Row[] } : {}),
307
319
  ...(typeof r.finishReason === 'string' ? { finishReason: r.finishReason } : {}),
308
320
  };
309
321
  }
310
322
 
311
323
  const nonEmpty = (v: unknown): v is string => typeof v === 'string' && v.length > 0;
312
- const RESPOND_KEYS = new Set(['text', 'toolCalls', 'reasoning', 'finishReason']);
324
+ const RESPOND_KEYS = new Set(['text', 'toolCalls', 'reasoning', 'reasoningDetails', 'finishReason']);
313
325
 
314
326
  /** A request as the scenario reads it: its model, its text, its last user text, the tools it offers and the tool
315
327
  * results it ends with, from a chat completion's messages or a Responses input. */
@@ -347,10 +359,11 @@ export function scenario(vendor: string, operations: string[], faultBody: (statu
347
359
  return null;
348
360
  },
349
361
  validateRespond: (respond) => {
350
- if (typeof respond !== 'object' || respond === null || Array.isArray(respond)) return 'respond is an object { text?, toolCalls?, reasoning?, finishReason? }';
362
+ if (typeof respond !== 'object' || respond === null || Array.isArray(respond)) return 'respond is an object { text?, toolCalls?, reasoning?, reasoningDetails?, finishReason? }';
351
363
  const r = respond as Row;
352
364
  for (const k of Object.keys(r)) if (!RESPOND_KEYS.has(k)) return `respond: unknown key "${k}" (valid: ${[...RESPOND_KEYS].join(', ')})`;
353
365
  if (r.text !== undefined && typeof r.text !== 'string') return 'respond.text is a string';
366
+ if (r.reasoningDetails !== undefined && (!Array.isArray(r.reasoningDetails) || r.reasoningDetails.some((d) => !d || typeof d !== 'object' || Array.isArray(d)))) return 'respond.reasoningDetails is an array of JSON objects';
354
367
  if (r.reasoning !== undefined && typeof r.reasoning !== 'string') return 'respond.reasoning is a string';
355
368
  for (const c of r.toolCalls === undefined ? [] : Array.isArray(r.toolCalls) ? r.toolCalls : [r.toolCalls]) {
356
369
  const u = c as Row;
@@ -369,3 +382,21 @@ export function scenario(vendor: string, operations: string[], faultBody: (statu
369
382
  };
370
383
  return { adapter, operations, request };
371
384
  }
385
+
386
+ /** An admitted generation that fails: partial JSON or actual Chat Completions error frames.
387
+ * The pack supplies its error envelope/type; the shared wire owns the SDK framing. */
388
+ export function chatCompletionFailure(s: { id: string; model: string; created: number; error: Row; text?: string; extra?: Row }, stream: boolean): Row | string {
389
+ const common = { id: s.id, model: s.model, created: s.created, ...s.extra };
390
+ if (!stream) return { ...common, object: 'chat.completion', choices: [{ index: 0, message: { role: 'assistant', content: s.text ?? '' }, finish_reason: 'error', error: s.error }] };
391
+ const frames: Row[] = [];
392
+ if (s.text) frames.push({ ...common, object: 'chat.completion.chunk', choices: [{ index: 0, delta: { role: 'assistant', content: s.text }, finish_reason: null }] });
393
+ frames.push({ ...common, object: 'chat.completion.chunk', choices: [{ index: 0, delta: { content: '' }, finish_reason: 'error' }], error: s.error });
394
+ return frames.map((f) => `data: ${JSON.stringify(f)}\n\n`).join('') + 'data: [DONE]\n\n';
395
+ }
396
+
397
+ /** Responses generation failures keep the response's canonical fields and native error code. */
398
+ export function responsesFailure(response: Row, stream: boolean, event = 'response.failed'): Row | string {
399
+ if (!stream) return response;
400
+ const data = event === 'response.failed' ? { type: event, response } : { type: event, error: response.error };
401
+ return `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
402
+ }
package/src/pack-fetch.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { parseExactJson } from './exact-json.ts';
2
+ import { readHostPort } from './host-port.ts';
1
3
  // A Protocol 3 pack's whole HTTP surface, served by the kernel from the pack's declarations (docs/contributing/
2
4
  // architecture.md, "The pack contract: the kernel's root"): the discovery door (`GET /twin`, the manifest's
3
5
  // `discovery`), the vendor's own moves as time passes (`semantics/clock.ts`), the World's doors (the manifest's `doors`,
@@ -10,7 +12,7 @@ import { placedCookies, TWIN_PREFIX_HEADER } from './twin-fetch.ts';
10
12
  import { PREFIX_PARAMS_HEADER } from './derived.ts';
11
13
  import { withHeaders } from './with-headers.ts';
12
14
  import { authRefusal, bindSemantics, contextFor, LANE_HEADER, SCENARIO_DECISION_HEADER, coreFor, crossCutting, derivedRequestScopes, vendorError, type CoreScope, type DerivedManifest, type ErrorSpec, type Handler, type HandlerContext, type LanesDecl, type VendorManifest } from './derived-core.ts';
13
- import { compileSurface, createDerivedFetch, matchOperation, type DerivedFetch, type DerivedOperation, type DerivedOwner, type DerivedSurface, type Workspace, type BoardFrame } from './derived.ts';
15
+ import { compileSurface, createDerivedFetch, matchOperation, pathParams, type DerivedFetch, type DerivedOperation, type DerivedOwner, type DerivedSurface, type Workspace, type BoardFrame } from './derived.ts';
14
16
  import { isReadOnlyRequest, READ_ONLY_REQUEST_HEADER, runAsVendorMove } from './request-scope.ts';
15
17
  import { parseScenarioDocument, ScenarioEngine, ScenarioError, statefulTwinManifest, twinManifest, type PackScenarioAdapter } from './scenario.ts';
16
18
  import { getActiveWorldStore } from './world-store.ts';
@@ -25,10 +27,13 @@ import { hostRules } from '../host-routing.cjs';
25
27
  /** A descriptor's host rules, as data. */
26
28
  type PackDescriptorHosts = ReadonlyArray<HostRule> | undefined;
27
29
  import { socketUpgrade, type SocketEngine } from './sockets.ts';
28
- import { MACHINE_POOL, POOL_KINDS } from './machines.ts';
30
+ import type { WebSocketUpgrade, WebSocketPeer } from './serve-http.ts';
31
+ import { MACHINE_POOL, POOL_KINDS, resetFakePool } from './machines.ts';
29
32
  import { applyTwinWrite } from './serve.ts';
30
33
  import { graphqlEndpoint, type GraphqlPart } from './graphql-wire.ts';
31
- import { brotliDecompressSync, gunzipSync, gzipSync, inflateSync, zstdDecompressSync } from 'node:zlib';
34
+ import { gzipSync } from 'node:zlib';
35
+ import { decompress } from './bytes.ts';
36
+ import { decodedBodyLimit, oversizedBody, RequestBodyTooLargeError } from './request-body.ts';
32
37
 
33
38
  /** What a pack declares and writes, and nothing it assembles itself. */
34
39
  export type PackParts = {
@@ -80,6 +85,7 @@ export type LanePart = {
80
85
  serves?: (method: string, path: string) => boolean; boards?: boolean; draws?: (url: string) => boolean;
81
86
  /** the hosts the lane's built content screens name: its vendor's router sends `<base>/@<host>/` there */
82
87
  contentHosts?: ReadonlyArray<string>;
88
+ upgrade?: WebSocketUpgrade;
83
89
  };
84
90
  manifest: { doors?: ReadonlyArray<{ method: string; path: string }> };
85
91
  };
@@ -112,6 +118,8 @@ function pathMatcher(pattern: string, prefix: boolean): (path: string) => Match
112
118
  }
113
119
  /** A declared host (`dash.cloudflare.com`, `<domain>.daily.co`) as a matcher of a request's host. */
114
120
  function hostMatcher(pattern: string): (host: string) => Match | undefined {
121
+ const whole = /^(?:\{([^}]+)\}|<([^>]+)>)$/.exec(pattern);
122
+ if (whole) return (host) => host ? { params: { [whole[1] ?? whole[2]!]: host } } : undefined;
115
123
  const names: string[] = [];
116
124
  const re = new RegExp(`^${pattern.split('.').map((label) => {
117
125
  const m = /^(?:\{([^}]+)\}|<([^>]+)>)$/.exec(label);
@@ -125,7 +133,7 @@ function hostMatcher(pattern: string): (host: string) => Match | undefined {
125
133
  };
126
134
  }
127
135
  /** The host a request is for: the vendor host the World's injector names, else its own. */
128
- const hostOf = (request: Request): string => (request.headers.get('x-volter-twin-original-host') ?? request.headers.get('host') ?? new URL(request.url).host).split(':')[0]!.toLowerCase();
136
+ const hostOf = (request: Request): string => readHostPort(request.headers.get('x-volter-twin-original-host') ?? request.headers.get('host') ?? new URL(request.url).host)?.host ?? '';
129
137
 
130
138
  /** The answer for a path no operation, door or screen serves, and for an operation nothing models: the manifest's
131
139
  * `gap`, its `{method}` and `{path}` filled, else the manifest's not-found refusal. */
@@ -137,25 +145,29 @@ function gapAnswer(m: DerivedManifest, request: Request, otherMethod = false): R
137
145
  }
138
146
 
139
147
  const METHODS = ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE'];
140
- const DECODE = { gzip: gunzipSync, zstd: zstdDecompressSync, deflate: inflateSync, br: brotliDecompressSync } as const;
148
+ type ContentCoding = 'gzip' | 'zstd' | 'deflate' | 'br';
141
149
 
142
150
  /** A request with its body decoded as its Content-Encoding says, when the manifest's `encodings` reads that coding;
143
151
  * undefined when the bytes are not what the header names. */
144
- async function decoded(m: DerivedManifest, request: Request): Promise<Request | undefined> {
152
+ async function decoded(m: DerivedManifest, request: Request): Promise<Request | Response | undefined> {
153
+ const codings = (request.headers.get('content-encoding') ?? '').split(',').map(c => c.trim().toLowerCase()).filter(Boolean);
154
+ if (codings.length > 1) return new Response('Unsupported Content-Encoding', { status: 415 });
145
155
  if (request.method === 'GET' || request.method === 'HEAD' || request.body === null) return request;
146
156
  // the body read once, here, and every part of the pack given a request made again from its bytes: a served request's
147
157
  // body read through a clone and then read again (an `around` that checks a body before its handler does) tees a stream
148
158
  // whose second branch a Bun server never finishes, and the request hung
149
159
  const raw = new Uint8Array(await request.arrayBuffer());
150
- const coding = (request.headers.get('content-encoding') ?? '').trim().toLowerCase() as keyof typeof DECODE;
160
+ const coding = (codings[0] ?? '') as ContentCoding;
151
161
  if (!m.encodings?.request?.includes(coding)) return new Request(request.url, { method: request.method, headers: request.headers, body: raw });
152
162
  let body: Uint8Array;
153
- try { body = DECODE[coding](raw); } catch { return undefined; }
163
+ try { body = await decompress(raw, coding, decodedBodyLimit(m.descriptor?.maxRequestBodySize)); }
164
+ catch (error) { return error instanceof RequestBodyTooLargeError ? oversizedBody() : undefined; }
154
165
  const headers = new Headers(request.headers);
155
166
  headers.delete('content-encoding');
156
167
  headers.delete('content-length');
157
- // a copy over an ArrayBuffer of its own: zlib's result may view a shared buffer, which a request body does not take
158
- return new Request(request.url, { method: request.method, headers, body: new Uint8Array(body) });
168
+ // A finite single-chunk stream holds the decoder's buffer; BufferSource extraction would copy the entire body.
169
+ const buffered = new ReadableStream<Uint8Array>({ start(controller) { controller.enqueue(body); controller.close(); } });
170
+ return new Request(request.url, { method: request.method, headers, body: buffered, duplex: 'half' } as RequestInit);
159
171
  }
160
172
 
161
173
  /** An answer in the coding the request accepts, when the manifest's `encodings` answers in it. */
@@ -171,6 +183,9 @@ async function encoded(m: DerivedManifest, request: Request, res: Response): Pro
171
183
  }
172
184
 
173
185
  /** A pack's HTTP surface, as the kernel serves it from the pack's parts. */
186
+ /** A door's path as it is served and listed: under `/_twin/`, whether the manifest writes the prefix or not. */
187
+ const doorPath = (path: string): string => (path.startsWith('/_twin/') ? path : `/_twin${path.startsWith('/') ? '' : '/'}${path}`);
188
+
174
189
  export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}): DerivedFetch {
175
190
  const m = parts.manifest;
176
191
  const scope: CoreScope = {
@@ -195,7 +210,7 @@ export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}
195
210
  if (parts.gap) {
196
211
  const url = new URL(request.url);
197
212
  const matched = operation ? matchOperation(routes, request.method, url.pathname, url.searchParams, request.headers) : undefined;
198
- return parts.gap(await contextFor(m, { request, operation: operation ?? ({ id: 'unmatched', method: request.method, path: url.pathname, class: request.method === 'GET' ? 'retrieve' : 'action' } as DerivedOperation), params: matched?.params ?? {} }, scope));
213
+ return parts.gap(await contextFor(m, { request, operation: operation ?? ({ id: 'unmatched', method: request.method, path: url.pathname, class: request.method === 'GET' ? 'retrieve' : 'action' } as DerivedOperation), params: matched?.params ?? pathParams(routes, url.pathname) }, scope));
199
214
  }
200
215
  const url = new URL(request.url);
201
216
  const other = !matchOperation(routes, request.method, url.pathname, url.searchParams, request.headers)
@@ -224,7 +239,7 @@ export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}
224
239
  const raw = getActiveWorldStore().read(options.scenarioPath);
225
240
  if (raw === null) throw new ScenarioError(`${m.vendor} scenario: no file at ${options.scenarioPath}`);
226
241
  if (!engine || raw !== engineRaw) {
227
- engine = new ScenarioEngine(parts.scenario.adapter, parseScenarioDocument(JSON.parse(raw), parts.scenario.adapter));
242
+ engine = new ScenarioEngine(parts.scenario.adapter, parseScenarioDocument(parseExactJson(raw), parts.scenario.adapter));
228
243
  engineRaw = raw;
229
244
  }
230
245
  return engine;
@@ -232,11 +247,13 @@ export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}
232
247
  /** The API, a turn the scenario decides handed its decision (ctx.scenario), or answered as the fault it names. */
233
248
  const decided = async (arrived: Request): Promise<Response> => {
234
249
  if (!parts.scenario) return api(arrived);
235
- // the body is read once and the request made again from its bytes: a served request's body cloned (for the turn's
236
- // context) and then forwarded tees a stream whose second branch a Bun server never finishes, and every turn hung
237
- // (measured on merge served with --scenario: no answer in 8 s, matched or not; t_c0279b69)
250
+ // the body is read once and the request made again from its bytes, so the turn's context and the API each read their
251
+ // own. Merge served with --scenario once answered no turn in 8 s (t_c0279b69), and this change came with the cure;
252
+ // putting the earlier read back (the served request cloned for the context, then forwarded) does not reproduce the
253
+ // hang on its own (2026-10-01: merge's CLI with --scenario, and its life walked over HTTP: every turn answered), so
254
+ // what hung is UNEXPLAINED, and this is no proof of its cause
238
255
  const bytes = arrived.method === 'GET' || arrived.method === 'HEAD' ? undefined : await arrived.arrayBuffer();
239
- const request = new Request(arrived.url, { method: arrived.method, headers: arrived.headers, ...(bytes ? { body: bytes } : {}) });
256
+ const request = new Request(arrived.url, { method: arrived.method, headers: arrived.headers, signal: arrived.signal, ...(bytes ? { body: bytes } : {}) });
240
257
  const url = new URL(request.url);
241
258
  const matched = matchOperation(routes, request.method, url.pathname, url.searchParams, request.headers);
242
259
  if (!matched || !parts.scenario.operations.includes(matched.operation.id)) return api(request);
@@ -262,7 +279,7 @@ export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}
262
279
  const doors = (m.doors ?? []).map((d) => {
263
280
  const answer = parts.doors?.[d.id];
264
281
  if (!answer) throw new Error(`${m.vendor}: the manifest declares the door ${d.id}, and semantics/doors.ts exports no ${d.id}`);
265
- return { decl: d, path: pathMatcher(d.path.startsWith('/_twin/') ? d.path : `/_twin${d.path.startsWith('/') ? '' : '/'}${d.path}`, false), answer };
282
+ return { decl: d, path: pathMatcher(doorPath(d.path), false), answer };
266
283
  });
267
284
  const screens = (m.screens ?? []).filter((s) => s.status === 'done' && parts.screens?.[s.id]).map((s) => {
268
285
  const hosts = [s.host, ...(s.hosts ?? [])].map(hostMatcher);
@@ -287,22 +304,49 @@ export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}
287
304
  // a context the kernel opens for what no surface operation names: a door, a screen, the clock, the board
288
305
  const open = (request: Request, id: string, method: string, path: string, params: Record<string, string>): Promise<HandlerContext> =>
289
306
  contextFor(m, { request, operation: { id, method, path, class: method === 'GET' ? 'retrieve' : 'action' } as DerivedOperation, params }, scope);
307
+ const advanceClock = async (request: Request): Promise<void> => {
308
+ if (parts.clock) await runAsVendorMove(async () => parts.clock!(await open(new Request(request.url, m.tenant ? { headers: request.headers } : {}), 'clock', 'POST', '/_twin/clock', {})));
309
+ };
290
310
  const refusedReadOnly = (): Response => vendorError(m, m.readOnly);
291
311
 
292
312
  const prefix = m.pathPrefix ? new RegExp(`^(?:${m.pathPrefix})(?=/)`) : undefined;
313
+ const base = surface.basePath ?? '';
314
+ const baseAlias = m.basePathAlias && base ? new RegExp(`^(?:${m.basePathAlias})(?=/)`) : undefined;
293
315
  const hostPath = m.pathFromHost ? new RegExp(m.pathFromHost, 'i') : undefined;
294
316
  const hostParams = m.hostParams ? new RegExp(m.hostParams, 'i') : undefined;
295
- /** The request as its vendor routes it: duplicate slashes collapsed, the manifest's path prefix taken off, a host's
317
+ // each operation's spanning parameters, keyed by the operation and the parameter as the route compiles them (a whole
318
+ // segment's label whose name the surface or the manifest declares spanning), so one operation's spanning name never
319
+ // keeps another's slashes, nor a label that only part of a segment holds
320
+ const spanningNames = new Set(surface.spanning ?? []);
321
+ const spanningOf = new Set(surface.operations.flatMap((op) => ((op.basePath ?? base) + op.path).split('/').flatMap((seg) => {
322
+ const label = /^\{([^}]+)\}$/.exec(seg)?.[1];
323
+ return label !== undefined && spanningNames.has(label) ? [`${op.id}\u0000${label}`] : [];
324
+ })));
325
+ /** A path as the vendor routes it: the manifest's path prefix taken off, the spec's base in an alias's place, a host's
296
326
  * path label (pathFromHost) put in front. */
327
+ const rewritten = (pathname: string, host: string): string => {
328
+ let path = pathname;
329
+ if (prefix) path = path.replace(prefix, '') || '/';
330
+ // a base the vendor serves as its own: the spec's base in its place (a path already under the spec's base stays)
331
+ if (baseAlias && !(path === base || path.startsWith(`${base}/`))) path = path.replace(baseAlias, base);
332
+ const label = hostPath?.exec(host)?.[1];
333
+ if (label) path = `/${label}${path === '/' ? '' : path}`;
334
+ return path;
335
+ };
336
+ /** The request as its vendor routes it: duplicate slashes collapsed, unless the rewritten path matches an operation
337
+ * through one of that operation's spanning parameters (the surface's and the manifest's), whose value keeps its
338
+ * slashes; then rewritten. */
297
339
  const routed = (request: Request): Request => {
298
340
  const url = new URL(request.url);
299
- let path = url.pathname.replace(/\/{2,}/g, '/');
341
+ const host = hostOf(request);
342
+ const kept = rewritten(url.pathname, host);
343
+ const matched = matchOperation(routes, request.method, kept);
344
+ const spanning = matched !== undefined && Object.keys(matched.params).some((name) => spanningOf.has(`${matched.operation.id}\u0000${name}`));
345
+ const source = spanning ? url.pathname : url.pathname.replace(/\/{2,}/g, '/');
300
346
  // a prefix's named groups (Jira's `/ex/jira/(?<cloudId>[^/]+)`) and the host's (E2B's envd at
301
347
  // `49983-(?<sandboxID>…).e2b.app`) join the operation's parameters
302
- const groups = { ...(prefix?.exec(path)?.groups ?? {}), ...(hostParams?.exec(hostOf(request))?.groups ?? {}) };
303
- if (prefix) path = path.replace(prefix, '') || '/';
304
- const label = hostPath?.exec(hostOf(request))?.[1];
305
- if (label) path = `/${label}${path === '/' ? '' : path}`;
348
+ const groups = { ...(prefix?.exec(source)?.groups ?? {}), ...(hostParams?.exec(host)?.groups ?? {}), ...Object.fromEntries(Object.entries(m.headerParams ?? {}).flatMap(([param, header]) => { const value = request.headers.get(header); return value === null ? [] : [[param, value]]; })) };
349
+ const path = spanning ? kept : rewritten(source, host);
306
350
  if (path === url.pathname && !Object.keys(groups).length) return request;
307
351
  // the path the client sent, which a signature over the request (SigV4's canonical URI) covers
308
352
  const headers = new Headers(request.headers);
@@ -314,6 +358,8 @@ export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}
314
358
  };
315
359
  const rootServes = (method: string, path: string): boolean => Boolean(matchOperation(routes, method, path));
316
360
  const lane = m.lanes ? laneRouter(m.vendor, m.lanes, parts.lanes ?? {}, rootServes) : undefined;
361
+ // Inspect lane-owned World doors directly, without routing them back through the vendor gateway.
362
+ const laneDoor = (request: Request, path: string): Promise<Record<string, unknown>> => laneDoors(parts.lanes ?? {}, request, path);
317
363
  // at a World's place, a page's asset or upload on another of the pack's content hosts is `<base>/@<host>/<path>`
318
364
  // (architecture, "A page at a World's place"): answered by the built content screen that names that host exactly,
319
365
  // routed from it, and by nothing else; a request naming no such host is general routing's
@@ -358,7 +404,7 @@ export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}
358
404
  };
359
405
  /** A request as the kernel reads it: the headers only the kernel names taken off, its body decoded (undefined when it
360
406
  * does not decode as its Content-Encoding says). */
361
- const prepared = async (arrived: Request): Promise<Request | undefined> => {
407
+ const prepared = async (arrived: Request): Promise<Request | Response | undefined> => {
362
408
  if (arrived.headers.has(SCENARIO_DECISION_HEADER) || arrived.headers.has(PREFIX_PARAMS_HEADER)) {
363
409
  const headers = new Headers(arrived.headers);
364
410
  headers.delete(SCENARIO_DECISION_HEADER);
@@ -374,7 +420,7 @@ export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}
374
420
  const address = arrived.headers.has(TWIN_PREFIX_HEADER) ? contentAddressOf(new URL(arrived.url).pathname) : undefined;
375
421
  if (!address || !contentHosts.includes(address.host)) return undefined;
376
422
  const readable = await prepared(arrived);
377
- return readable ? atContentHost(readable) : undecodable(arrived);
423
+ return readable instanceof Response ? readable : readable ? atContentHost(readable) : undecodable(arrived);
378
424
  };
379
425
  const fetch = async (arrived: Request): Promise<Response> => {
380
426
  // the discovery door is the pack's own, whichever lane would take its path
@@ -393,9 +439,12 @@ export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}
393
439
  // one a lane's names, the lane router sends it to that lane's own fetch, wrapped as any request to it is
394
440
  const own = await answerContent(arrived);
395
441
  if (own) return own;
396
- const toLane = discovery ? undefined : lane?.(arrived);
442
+ // Root World doors belong to the pack at every routed host, including a lane's host.
443
+ const rootDoor = doors.some((d) => d.decl.method.toUpperCase() === arrived.method && d.path(asked));
444
+ const toLane = discovery || rootDoor ? undefined : lane?.(arrived);
397
445
  if (toLane) return toLane(arrived);
398
446
  const readable = await prepared(arrived);
447
+ if (readable instanceof Response) return readable;
399
448
  if (!readable) return undecodable(arrived);
400
449
  const incoming = routed(readable);
401
450
  // a twin started read-only marks every request so, so every context it opens knows (isReadOnlyRequest)
@@ -405,31 +454,41 @@ export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}
405
454
  const readOnly = (options.readOnly ?? false) || isReadOnlyRequest(request);
406
455
  if (request.method === 'GET' && path === '/twin' && m.discovery) {
407
456
  const d = m.discovery;
457
+ const discoveryDoors = [...(m.doors ?? []), ...(m.machines ? [{ id: 'machine-pool', method: 'POST', path: '/_twin/machine-pool', note: 'Enroll the World execution pool: { kind: light | docker | none | fake, images?: { image: { context, command?, runtime? } } }.' }] : [])];
408
458
  // the workspaces a person can be shown (Viewing a World): each built workspace screen's id and path
409
- const shown = workspaces().length ? { screens: workspaces() } : {};
459
+ const laneDiscovery = await laneDoor(request, '/twin');
460
+ const shown = { ...(workspaces().length ? { screens: workspaces() } : {}), ...(Object.keys(laneDiscovery).length ? { lanes: laneDiscovery } : {}) };
461
+ const identity = d.identity ?? (m.auth?.held ? 'Authenticate as the vendor does, using a credential issued in this World; unknown credentials are refused.' : undefined);
410
462
  // a model vendor's: its state, and what its scenario scripts, with the scenario's live counts
411
463
  if (parts.scenario && d.behavior) {
412
- return Response.json({ ...shown, ...twinManifest({
464
+ const body = twinManifest({
413
465
  vendor: m.vendor, twinOf: d.twinOf,
414
- stateSentence: `Stateful: it stores ${d.stores}. Create state through the vendor's OWN API with the real SDK or plain fetch pointed here.`,
466
+ stateSentence: `Stateful: it stores ${d.stores}. Create state through the vendor's OWN API with the real SDK or plain fetch pointed here${discoveryDoors.length ? "; what the vendor does on its own pages (not its API) is a door of the World, listed under doors" : ''}.`,
415
467
  behaviorSentence: d.behavior, exampleHandler: d.exampleHandler ?? null, ...(engineOf() ? { engine: engineOf() as never } : {}),
416
- }) });
468
+ });
469
+ const declared = Object.fromEntries(discoveryDoors.map((door) => [`${door.method} ${doorPath(door.path)}`, door.note ?? 'a door of the World']));
470
+ return Response.json({ ...shown, ...body, program: { ...(body.program as Record<string, unknown>), ...(identity ? { identity } : {}) }, doors: { ...(body.doors as Record<string, string>), ...declared } });
417
471
  }
418
- return Response.json({ ...statefulTwinManifest({ vendor: m.vendor, twinOf: d.twinOf, stores: d.stores, ...(d.identity ? { identity: d.identity } : {}), ...(d.notes ? { notes: d.notes } : {}) }), ...shown });
472
+ return Response.json({ ...statefulTwinManifest({ vendor: m.vendor, twinOf: d.twinOf, stores: d.stores, ...(identity ? { identity } : {}), ...(d.behavior ? { behavior: d.behavior } : {}), ...(d.notes ? { notes: d.notes } : {}), ...(discoveryDoors.length ? { doors: discoveryDoors.map((door) => ({ ...door, path: doorPath(door.path) })) } : {}) }), ...shown });
419
473
  }
420
474
  // the scenario's handlers with their match counts, and the recent misses
421
- if (request.method === 'GET' && path === '/twin/scenario' && parts.scenario) return Response.json(engineOf()?.status() ?? { vendor: m.vendor, handlers: [], misses: 0, recentMisses: [] });
475
+ if (request.method === 'GET' && path === '/twin/scenario') {
476
+ const lanes = await laneDoor(request, '/twin/scenario');
477
+ if (parts.scenario || Object.keys(lanes).length) return Response.json({ ...(engineOf()?.status() ?? { vendor: m.vendor, handlers: [], misses: 0, recentMisses: [] }), ...(Object.keys(lanes).length ? { lanes } : {}) });
478
+ }
422
479
  // the vendor's own moves, not the caller's: its context carries none of the request's headers (a Stripe-Account
423
480
  // header would make a payout time makes the caller's), but a tenant's, when the vendor keeps tenants apart
424
- if (parts.clock) await runAsVendorMove(async () => parts.clock!(await open(new Request(request.url, m.tenant ? { headers: request.headers } : {}), 'clock', 'POST', '/_twin/clock', {})));
481
+ await advanceClock(request);
425
482
  if (path.startsWith('/_twin/')) {
426
483
  // the World's machine pool, enrolled by the kernel for a vendor that runs images (machines.ts)
427
484
  if (m.machines && path === '/_twin/machine-pool' && request.method === 'POST') {
428
485
  if (readOnly) return refusedReadOnly();
429
- let kind: unknown;
430
- try { kind = (JSON.parse(await request.clone().text()) as { kind?: unknown }).kind; } catch { kind = undefined; }
486
+ let enrollment: Record<string, unknown> = {};
487
+ try { enrollment = parseExactJson(await request.clone().text()); } catch {}
488
+ const kind = enrollment.kind;
431
489
  if (!POOL_KINDS.includes(kind as never)) return Response.json({ message: `kind must be one of ${POOL_KINDS.join(', ')}` }, { status: 400 });
432
- await applyTwinWrite(m.service, { operation: 'machine_pool.enroll', subjectType: MACHINE_POOL, subjectId: 'pool', fields: { kind }, occurredAt: (await open(request, 'machine-pool', 'POST', '/_twin/machine-pool', {})).occurredAt, actor: { kind: 'system' } }, options.root);
490
+ await applyTwinWrite(m.service, { operation: 'machine_pool.enroll', subjectType: MACHINE_POOL, subjectId: 'pool', fields: { kind, ...(kind === 'fake' && Array.isArray(enrollment.httpReplies) ? { httpReplies: enrollment.httpReplies } : {}), ...(kind === 'light' ? { images: enrollment.images ?? {} } : {}) }, occurredAt: (await open(request, 'machine-pool', 'POST', '/_twin/machine-pool', {})).occurredAt, actor: { kind: 'system' } }, options.root);
491
+ if (kind === 'fake') resetFakePool(m.service, options.root);
433
492
  return Response.json({ kind }, { status: 201 });
434
493
  }
435
494
  for (const d of doors) {
@@ -471,11 +530,27 @@ export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}
471
530
  if (readOnly && request.method !== 'GET' && request.method !== 'HEAD') return refusedReadOnly();
472
531
  return byPath.s.answer(await open(request, byPath.s.decl.id, request.method, byPath.s.decl.path, { ...(byPath.onHost?.params ?? {}), ...byPath.hit!.params }));
473
532
  }
533
+ for (const route of m.machineRoutes ?? []) {
534
+ const host = request.headers.get('x-volter-twin-original-host') ?? new URL(request.url).hostname;
535
+ const match = new RegExp(route.host).exec(host);
536
+ if (!match) continue;
537
+ const ctx = await open(request, 'machine.traffic', request.method, url.pathname, match.groups ?? {});
538
+ const field = (row: Record<string, unknown>, path: string): unknown => path.split('.').reduce<unknown>((value, key) => value && typeof value === 'object' ? (value as Record<string, unknown>)[key] : undefined, row);
539
+ const fill = (value: string, row: Record<string, unknown> = {}): string => value.replace(/\{([^}]+)\}/g, (_, key: string) => String(match.groups?.[key] ?? field(row, key) ?? ''));
540
+ const row = ctx.rowsRaw(route.resource).find(row => Object.entries(route.where).every(([key, value]) => String(field(row, key)) === fill(value)));
541
+ if (!row) return new Response('Guest is unavailable', { status: 503 });
542
+ try { return await ctx.machines.request(String(field(row, route.name)), Number(typeof route.port === 'string' ? fill(route.port, row) : route.port), request); }
543
+ catch { return new Response('Guest is unavailable', { status: 503 }); }
544
+ }
474
545
  if (!parts.around) return encoded(m, request, await decided(request));
475
- const matched = matchOperation(routes, request.method, url.pathname, url.searchParams, request.headers);
476
- const operation = matched?.operation ?? ({ id: 'unmatched', method: request.method, path: url.pathname, class: request.method === 'GET' ? 'retrieve' : 'action' } as DerivedOperation);
477
- const ctx = await contextFor(m, { request: request.clone(), operation, params: matched?.params ?? {} }, scope);
478
- return encoded(m, request, await parts.around(ctx, (rewritten) => decided(rewritten ?? request)));
546
+ // The front reads a context before forwarding (possibly with new headers). Materialize a transport body before
547
+ // those independent reads, rather than chaining tees of the incoming stream through the front and the scenario.
548
+ const bytes = request.method === 'GET' || request.method === 'HEAD' ? undefined : await request.arrayBuffer();
549
+ const buffered = new Request(request.url, { method: request.method, headers: request.headers, signal: request.signal, ...(bytes ? { body: bytes } : {}) });
550
+ const matched = matchOperation(routes, buffered.method, url.pathname, url.searchParams, buffered.headers);
551
+ const operation = matched?.operation ?? ({ id: 'unmatched', method: buffered.method, path: url.pathname, class: buffered.method === 'GET' ? 'retrieve' : 'action' } as DerivedOperation);
552
+ const ctx = await contextFor(m, { request: buffered.clone(), operation, params: matched?.params ?? pathParams(routes, url.pathname) }, scope);
553
+ return encoded(m, buffered, await parts.around(ctx, (rewritten) => decided(rewritten ?? buffered)));
479
554
  };
480
555
  // the request's id is the correlation of every entry it appends (runtime contract D3), minted here, always: it is the
481
556
  // group a perform sends as one call (the real-system adapters, "Perform"). Under `auto` the group is performed once
@@ -513,13 +588,21 @@ export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}
513
588
  // the vendor's sockets: a request to a declared socket's path opens a session of its engine, over a context of the
514
589
  // request that opened it (operation `socket:<id>`)
515
590
  for (const d of m.sockets ?? []) if (!parts.sockets?.[d.id]) throw new Error(`${m.vendor}: the manifest declares the socket ${d.id}, and semantics/sockets.ts exports no ${d.id}`);
516
- const upgrade = m.sockets?.length ? socketUpgrade(m.service, options.root, m.sockets, parts.sockets!,
591
+ const ownUpgrade = m.sockets?.length ? socketUpgrade(m.service, options.root, m.sockets, parts.sockets!,
517
592
  (request, d) => open(request, `socket:${d.id}`, 'GET', d.path, {}), hostOf, (request) => new URL(routed(request).url).pathname,
518
593
  // a turn a session speaks (a voice agent's reply) is the World's scenario's, as a turn over HTTP is
519
594
  async (asked) => engineOf()?.serve(asked)) : undefined;
520
- // what serves each operation: the pack's own, and each lane's
521
- const owners = (): Record<string, DerivedOwner> => ({ ...api.owners(), ...lanesOwners(parts.lanes ?? {}) });
522
- return derivedRequestScopes(m, Object.assign(served, { owners, serves, workspaces, boards: Boolean(parts.board), draws: drawsUrl, contentHosts, ...(upgrade ? { upgrade } : {}) }));
595
+ const upgrade = laneUpgrades(parts.lanes ?? {}, ownUpgrade);
596
+ // what serves each operation: the pack's own, and each lane's, each unit's under its own name
597
+ const ownersByUnit = (): Record<string, Record<string, DerivedOwner>> => ({
598
+ '': {
599
+ ...((surface.format === 'command-reply' || surface.format === 'command-table' && m.descriptor?.transport === 'raw-tcp') ? Object.fromEntries((surface.commands ?? []).map((c) => [c.id, parts.around && !m.unmodeled?.includes(c.id) ? 'handler' : 'gap'])) : api.owners()),
600
+ ...Object.fromEntries((parts.graphql?.operations ?? []).map((op) => [op.id, `${op.kind === 'query' ? 'Query' : 'Mutation'}.${op.field}` in (parts.graphql?.resolvers ?? {}) ? 'handler' : 'gap'] as const)),
601
+ },
602
+ ...lanesOwners(parts.lanes ?? {}),
603
+ });
604
+ const owners = (): Record<string, DerivedOwner> => flatOwners(ownersByUnit());
605
+ return derivedRequestScopes(m, Object.assign(served, { owners, ownersByUnit, serves, advanceClock, workspaces, boards: Boolean(parts.board), maxRequestBodySize: m.descriptor?.maxRequestBodySize, draws: drawsUrl, contentHosts, ...(upgrade ? { upgrade } : {}) }), options.readOnly);
523
606
  }
524
607
 
525
608
  /** The vendor's gateway over its lanes: the lane a request goes to (a door to the lane that declares it, then the first
@@ -574,17 +657,52 @@ function laneRouter(vendor: string, decl: LanesDecl, lanes: Record<string, LaneP
574
657
 
575
658
  /** A vendor whose every API is a lane (Cloudflare's API v4 and R2, AWS's services), served from its manifest: the
576
659
  * discovery door, then its lanes as its gateway sends them; a request no lane takes is the vendor's not-found. */
577
- /** What serves each of a vendor's lanes' operations (a handler, the core, or the gap), as one map. */
578
- const lanesOwners = (lanes: Record<string, LanePart>): Record<string, DerivedOwner> =>
579
- Object.assign({}, ...Object.values(lanes).map((l) => (l.fetch as { owners?: () => Record<string, DerivedOwner> }).owners?.() ?? {}));
660
+ /** Each lane's answer at one of its World doors (`/twin`, `/twin/scenario`), by lane, read from the lane directly rather
661
+ * than routed back through the vendor's front: a vendor's discovery holds its lanes' (their doors, their scripts). */
662
+ async function laneDoors(lanes: Record<string, LanePart>, request: Request, path: string): Promise<Record<string, unknown>> {
663
+ const entries = await Promise.all(Object.entries(lanes).map(async ([name, part]) => {
664
+ const url = new URL(request.url); url.pathname = path; url.search = '';
665
+ const answer = await part.fetch(new Request(url, { headers: request.headers, signal: request.signal }));
666
+ return answer.ok ? [name, await answer.json()] as const : undefined;
667
+ }));
668
+ return Object.fromEntries(entries.filter((entry): entry is readonly [string, unknown] => entry !== undefined));
669
+ }
670
+
671
+ /** What serves each of a vendor's lanes' operations (a handler, the core, or the gap), by lane: a lane's operation is the
672
+ * lane's own, so an id it shares with its root or another lane (Clerk's Backend and Frontend APIs both name
673
+ * `ListOrganizationMemberships`) keeps the lane's own owner, and a lane's gap stays a gap whoever else serves the id. */
674
+ const lanesOwners = (lanes: Record<string, LanePart>): Record<string, Record<string, DerivedOwner>> =>
675
+ Object.fromEntries(Object.entries(lanes).map(([name, l]) => [name, (l.fetch as { owners?: () => Record<string, DerivedOwner> }).owners?.() ?? {}]));
676
+
677
+ /** A vendor's owners as one map, for its breadth: its own API's operations by id, a lane's as `<lane>:<id>`. */
678
+ const flatOwners = (byUnit: Record<string, Record<string, DerivedOwner>>): Record<string, DerivedOwner> =>
679
+ Object.fromEntries(Object.entries(byUnit).flatMap(([unit, map]) => Object.entries(map).map(([id, owner]) => [unit ? `${unit}:${id}` : id, owner] as const)));
580
680
 
581
681
  /** The workspaces a vendor's lanes draw, as one list. */
582
682
  const lanesWorkspaces = (lanes: Record<string, LanePart>): Workspace[] =>
583
683
  Object.values(lanes).flatMap((l) => (l.fetch as { workspaces?: () => Workspace[] }).workspaces?.() ?? []);
584
684
 
585
- export function createVendorFetch(manifest: VendorManifest, lanes: Record<string, LanePart>): ((request: Request) => Promise<Response>) & { owners: () => Record<string, DerivedOwner>; workspaces: () => Workspace[] } {
685
+ /** Keep each upgraded peer on the vendor or lane engine that accepted its opening request. */
686
+ function laneUpgrades(lanes: Record<string, LanePart>, own?: WebSocketUpgrade): WebSocketUpgrade | undefined {
687
+ const choices = [own, ...Object.values(lanes).map(lane => lane.fetch.upgrade)].filter((choice): choice is WebSocketUpgrade => choice !== undefined);
688
+ if (choices.length === 0) return undefined;
689
+ if (choices.length === 1) return choices[0];
690
+ const held = new WeakMap<WebSocketPeer, WebSocketUpgrade>();
691
+ const select = (request: Request): WebSocketUpgrade | undefined => choices.find(choice => choice.accepts(request));
692
+ return {
693
+ accepts: request => select(request) !== undefined,
694
+ protocol: request => select(request)?.protocol?.(request),
695
+ open(peer, request) { const selected = select(request); if (selected) { held.set(peer, selected); selected.open(peer, request); } },
696
+ message(peer, data) { return held.get(peer)?.message(peer, data); },
697
+ close(peer) { const selected = held.get(peer); held.delete(peer); selected?.close(peer); },
698
+ };
699
+ }
700
+
701
+ export function createVendorFetch(manifest: VendorManifest, lanes: Record<string, LanePart>): DerivedFetch {
702
+ const upgrade = laneUpgrades(lanes);
586
703
  const lane = laneRouter(manifest.vendor, manifest.lanes, lanes);
587
704
  return Object.assign(async (request: Request): Promise<Response> => {
705
+ for (const part of Object.values(lanes)) await (part.fetch as typeof part.fetch & { advanceClock?: (request: Request) => Promise<void> }).advanceClock?.(request);
588
706
  const path = new URL(request.url).pathname.replace(/\/+$/, '') || '/';
589
707
  // the vendor's board is the frames of the lanes that lay one out, each drawn when any lane's screen takes its url
590
708
  if (request.method === 'GET' && path === '/twin/board') {
@@ -602,9 +720,14 @@ export function createVendorFetch(manifest: VendorManifest, lanes: Record<string
602
720
  if (request.method === 'GET' && path === '/twin' && manifest.discovery) {
603
721
  // the vendor's screens are its lanes' (a dashboard drawn by one lane is the vendor's)
604
722
  const screens = lanesWorkspaces(lanes);
605
- return Response.json({ ...statefulTwinManifest({ vendor: manifest.vendor, ...manifest.discovery }), ...(screens.length ? { screens } : {}) });
723
+ const laneDiscovery = await laneDoors(lanes, request, '/twin');
724
+ return Response.json({ ...statefulTwinManifest({ vendor: manifest.vendor, ...manifest.discovery }), ...(screens.length ? { screens } : {}), ...(Object.keys(laneDiscovery).length ? { lanes: laneDiscovery } : {}) });
725
+ }
726
+ if (request.method === 'GET' && path === '/twin/scenario') {
727
+ const laneScenarios = await laneDoors(lanes, request, '/twin/scenario');
728
+ if (Object.keys(laneScenarios).length) return Response.json({ vendor: manifest.vendor, lanes: laneScenarios });
606
729
  }
607
730
  const to = lane(request);
608
731
  return to ? to(request) : Response.json({ error: `${manifest.vendor}: no lane serves ${request.method} ${path}` }, { status: 404 });
609
- }, { owners: () => lanesOwners(lanes), workspaces: () => lanesWorkspaces(lanes) });
732
+ }, { maxRequestBodySize: manifest.descriptor?.maxRequestBodySize, owners: () => flatOwners(lanesOwners(lanes)), ownersByUnit: () => lanesOwners(lanes), workspaces: () => lanesWorkspaces(lanes), ...(upgrade ? { upgrade } : {}) });
610
733
  }