@nylorun/runtime 0.15.1-beta → 0.16.0-beta

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 (220) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/README.md +33 -6
  3. package/dist/adapters/execution/restate.d.ts +9 -1
  4. package/dist/adapters/execution/restate.js +64 -5
  5. package/dist/adapters/replication/pgoutput.js +3 -1
  6. package/dist/adapters/sandbox/local.d.ts +10 -0
  7. package/dist/adapters/sandbox/local.js +195 -0
  8. package/dist/adapters/sandbox/virtual.js +36 -1
  9. package/dist/admin-openapi.json +37 -3
  10. package/dist/api/a2a/tasks.js +6 -1
  11. package/dist/api/ag-ui/history.js +6 -1
  12. package/dist/api/ag-ui/routes.js +5 -1
  13. package/dist/api/components.d.ts +2673 -655
  14. package/dist/api/components.js +17 -1
  15. package/dist/api/gate/keys.d.ts +4 -3
  16. package/dist/api/gate/routes.d.ts +19 -3
  17. package/dist/api/gate/routes.js +162 -53
  18. package/dist/api/http/app.js +4 -0
  19. package/dist/api/http/define.d.ts +10 -0
  20. package/dist/api/http/define.js +2 -1
  21. package/dist/api/http/routes/artifacts.d.ts +11 -0
  22. package/dist/api/http/routes/artifacts.js +518 -0
  23. package/dist/api/http/routes/sandboxes.d.ts +9 -0
  24. package/dist/api/http/routes/sandboxes.js +152 -0
  25. package/dist/api/http/routes/sessions.js +3 -1
  26. package/dist/api/http/routes/tenant.js +36 -2
  27. package/dist/artifacts/config.d.ts +23 -0
  28. package/dist/artifacts/config.js +27 -0
  29. package/dist/artifacts/export.d.ts +8 -0
  30. package/dist/artifacts/export.js +205 -0
  31. package/dist/artifacts/files.d.ts +36 -0
  32. package/dist/artifacts/files.js +45 -0
  33. package/dist/artifacts/folders.d.ts +60 -0
  34. package/dist/artifacts/folders.js +209 -0
  35. package/dist/artifacts/links.d.ts +28 -0
  36. package/dist/artifacts/links.js +92 -0
  37. package/dist/artifacts/media-types.d.ts +13 -0
  38. package/dist/artifacts/media-types.js +80 -0
  39. package/dist/artifacts/parts.d.ts +34 -0
  40. package/dist/artifacts/parts.js +38 -0
  41. package/dist/artifacts/service.d.ts +74 -0
  42. package/dist/artifacts/service.js +301 -0
  43. package/dist/artifacts/workspace.d.ts +60 -0
  44. package/dist/artifacts/workspace.js +57 -0
  45. package/dist/artifacts/zip.d.ts +10 -0
  46. package/dist/artifacts/zip.js +126 -0
  47. package/dist/blob/fs.d.ts +13 -0
  48. package/dist/blob/fs.js +194 -0
  49. package/dist/blob/index.d.ts +9 -0
  50. package/dist/blob/index.js +9 -0
  51. package/dist/blob/s3.d.ts +42 -0
  52. package/dist/blob/s3.js +366 -0
  53. package/dist/blob/types.d.ts +108 -0
  54. package/dist/blob/types.js +85 -0
  55. package/dist/core/sandbox-routes.d.ts +2 -2
  56. package/dist/core/sandbox-routes.js +4 -3
  57. package/dist/execution/memory.d.ts +11 -1
  58. package/dist/execution/memory.js +77 -3
  59. package/dist/execution/types.d.ts +54 -1
  60. package/dist/execution/types.js +31 -0
  61. package/dist/gates/contract.d.ts +14 -13
  62. package/dist/gates/contract.js +13 -12
  63. package/dist/gates/egress.d.ts +72 -0
  64. package/dist/gates/egress.js +319 -0
  65. package/dist/gates/handler.d.ts +3 -0
  66. package/dist/gates/handler.js +3 -2
  67. package/dist/gates/http-client.d.ts +3 -2
  68. package/dist/gates/http-client.js +17 -6
  69. package/dist/gates/in-process.d.ts +13 -1
  70. package/dist/gates/in-process.js +11 -0
  71. package/dist/gates/inflight.d.ts +23 -4
  72. package/dist/gates/inflight.js +18 -4
  73. package/dist/gates/model-gate.d.ts +6 -2
  74. package/dist/gates/tool-calls.d.ts +9 -5
  75. package/dist/gates/tool-calls.js +6 -6
  76. package/dist/gates/tool-client.d.ts +9 -2
  77. package/dist/gates/tool-client.js +56 -22
  78. package/dist/gates/tool-contract.d.ts +12 -7
  79. package/dist/gates/tool-contract.js +10 -4
  80. package/dist/gates/tool-gate.d.ts +1 -0
  81. package/dist/harness/calls.d.ts +52 -0
  82. package/dist/harness/calls.js +113 -0
  83. package/dist/harness/executors.d.ts +22 -0
  84. package/dist/harness/executors.js +96 -0
  85. package/dist/harness/main.d.ts +6 -0
  86. package/dist/harness/main.js +103 -0
  87. package/dist/harness/pod.d.ts +21 -0
  88. package/dist/harness/pod.js +119 -0
  89. package/dist/harness/run-tokens.d.ts +18 -0
  90. package/dist/harness/run-tokens.js +15 -0
  91. package/dist/harness/service.d.ts +46 -0
  92. package/dist/harness/service.js +179 -0
  93. package/dist/harness/ws-client.d.ts +43 -0
  94. package/dist/harness/ws-client.js +122 -0
  95. package/dist/harness/ws-port.d.ts +22 -0
  96. package/dist/harness/ws-port.js +78 -0
  97. package/dist/harness-api/in-process.d.ts +24 -0
  98. package/dist/harness-api/in-process.js +119 -0
  99. package/dist/harness-api/record.d.ts +44 -0
  100. package/dist/harness-api/record.js +250 -0
  101. package/dist/harness-api/renew.d.ts +37 -0
  102. package/dist/harness-api/renew.js +77 -0
  103. package/dist/harness-api/server.d.ts +104 -0
  104. package/dist/harness-api/server.js +408 -0
  105. package/dist/harness-api/start.d.ts +36 -0
  106. package/dist/harness-api/start.js +118 -0
  107. package/dist/harness-api/workspace.d.ts +53 -0
  108. package/dist/harness-api/workspace.js +201 -0
  109. package/dist/harness-api/ws-server.d.ts +34 -0
  110. package/dist/harness-api/ws-server.js +227 -0
  111. package/dist/host/app.js +23 -2
  112. package/dist/host/gates.d.ts +27 -2
  113. package/dist/host/gates.js +46 -1
  114. package/dist/host/http.d.ts +4 -1
  115. package/dist/host/http.js +6 -1
  116. package/dist/host/main.js +135 -27
  117. package/dist/host/stack-config.d.ts +101 -1
  118. package/dist/host/stack-config.js +199 -8
  119. package/dist/mcp/snapshot.d.ts +8 -1
  120. package/dist/mcp/snapshot.js +28 -0
  121. package/dist/media.d.ts +5 -24
  122. package/dist/media.js +5 -0
  123. package/dist/model/pi-model.d.ts +6 -2
  124. package/dist/model/pi-model.js +39 -22
  125. package/dist/node/index.d.ts +1 -1
  126. package/dist/node/index.js +1 -1
  127. package/dist/openapi.json +15245 -8850
  128. package/dist/record/index.d.ts +1 -0
  129. package/dist/record/index.js +1 -0
  130. package/dist/record/sandbox.d.ts +25 -0
  131. package/dist/record/sandbox.js +43 -0
  132. package/dist/sandbox/capability.d.ts +3 -0
  133. package/dist/sandbox/capability.js +10 -0
  134. package/dist/sandbox/egress-token.d.ts +26 -0
  135. package/dist/sandbox/egress-token.js +8 -0
  136. package/dist/sandbox/join.d.ts +37 -0
  137. package/dist/sandbox/join.js +165 -0
  138. package/dist/sandbox/manager.d.ts +99 -34
  139. package/dist/sandbox/manager.js +216 -84
  140. package/dist/sandbox/placement.d.ts +27 -0
  141. package/dist/sandbox/placement.js +44 -0
  142. package/dist/sandbox/pods/client.d.ts +90 -0
  143. package/dist/sandbox/pods/client.js +79 -0
  144. package/dist/sandbox/pods/lifecycle.d.ts +91 -0
  145. package/dist/sandbox/pods/lifecycle.js +178 -0
  146. package/dist/sandbox/pods/name.d.ts +1 -0
  147. package/dist/sandbox/pods/name.js +28 -0
  148. package/dist/sandbox/pods/network-gate.d.ts +25 -0
  149. package/dist/sandbox/pods/network-gate.js +71 -0
  150. package/dist/sandbox/pods/reconcile.d.ts +11 -0
  151. package/dist/sandbox/pods/reconcile.js +152 -0
  152. package/dist/sandbox/pods/spec.d.ts +45 -0
  153. package/dist/sandbox/pods/spec.js +62 -0
  154. package/dist/sandbox/records.d.ts +29 -0
  155. package/dist/sandbox/records.js +79 -0
  156. package/dist/sandbox/resolve.d.ts +5 -0
  157. package/dist/sandbox/resolve.js +3 -2
  158. package/dist/sandbox/session-sandbox.js +7 -5
  159. package/dist/sandbox/share.d.ts +11 -0
  160. package/dist/sandbox/share.js +10 -0
  161. package/dist/sandbox/tenant-config.d.ts +16 -1
  162. package/dist/sandbox/tenant-config.js +9 -0
  163. package/dist/sandbox/types.d.ts +29 -1
  164. package/dist/sandbox/types.js +13 -1
  165. package/dist/store/postgres/drizzle/0004_sandbox_resources.sql +20 -0
  166. package/dist/store/postgres/drizzle/0005_artifacts.sql +27 -0
  167. package/dist/store/postgres/drizzle/0006_folder_artifacts.sql +12 -0
  168. package/dist/store/postgres/drizzle/0007_sandbox_pods.sql +14 -0
  169. package/dist/store/postgres/drizzle/meta/_journal.json +28 -0
  170. package/dist/store/postgres/record-writer.d.ts +6 -1
  171. package/dist/store/postgres/record-writer.js +20 -2
  172. package/dist/store/postgres/schema.d.ts +894 -0
  173. package/dist/store/postgres/schema.js +130 -2
  174. package/dist/store/postgres/store.js +289 -4
  175. package/dist/store/types.d.ts +143 -6
  176. package/dist/streams/control.d.ts +2 -0
  177. package/dist/streams/control.js +5 -0
  178. package/dist/streams/types.d.ts +10 -1
  179. package/dist/tenant/advance.js +218 -160
  180. package/dist/tenant/artifact-tool.d.ts +5 -0
  181. package/dist/tenant/artifact-tool.js +119 -0
  182. package/dist/tenant/commands.js +30 -5
  183. package/dist/tenant/context.d.ts +64 -5
  184. package/dist/tenant/effects.d.ts +19 -13
  185. package/dist/tenant/effects.js +5 -382
  186. package/dist/tenant/history.d.ts +13 -23
  187. package/dist/tenant/history.js +14 -62
  188. package/dist/tenant/host-token.d.ts +92 -0
  189. package/dist/tenant/host-token.js +179 -0
  190. package/dist/tenant/module.js +1 -0
  191. package/dist/tenant/paths.d.ts +2 -1
  192. package/dist/tenant/paths.js +3 -1
  193. package/dist/tenant/reset.d.ts +5 -2
  194. package/dist/tenant/reset.js +22 -2
  195. package/dist/tenant/run-grants.d.ts +49 -0
  196. package/dist/tenant/run-grants.js +62 -0
  197. package/dist/tenant/run-token.d.ts +70 -0
  198. package/dist/tenant/run-token.js +138 -0
  199. package/dist/tenant/runtime.d.ts +38 -0
  200. package/dist/tenant/runtime.js +120 -32
  201. package/dist/tenant/sandboxes.d.ts +119 -0
  202. package/dist/tenant/sandboxes.js +551 -0
  203. package/dist/tenant/session.d.ts +2 -13
  204. package/dist/tenant/session.js +4 -28
  205. package/dist/tenant/sessions.d.ts +5 -0
  206. package/dist/tenant/sessions.js +28 -1
  207. package/dist/tenant/status.d.ts +8 -3
  208. package/dist/tenant/status.js +5 -0
  209. package/dist/tenant/streams.d.ts +7 -0
  210. package/dist/tenant/streams.js +24 -1
  211. package/dist/tenant/sweep.js +37 -4
  212. package/dist/tenant/tokens.js +9 -2
  213. package/dist/tenant/types.d.ts +18 -1
  214. package/dist/tenant/worker.d.ts +11 -5
  215. package/dist/tenant/worker.js +12 -6
  216. package/dist/version.d.ts +1 -1
  217. package/dist/version.js +1 -1
  218. package/package.json +13 -10
  219. package/dist/adapters/media.d.ts +0 -24
  220. package/dist/adapters/media.js +0 -111
@@ -13,7 +13,15 @@
13
13
  * listener (`NYLORUN_GATES_LISTEN_*`), and only one that runs loop parses
14
14
  * where to reach the gate (`NYLORUN_GATES_URL`). Both read
15
15
  * `NYLORUN_GATES_TOKEN`. In a container, loop requires the gate: the loop
16
- * process must never hold a model credential.
16
+ * process must never hold a model credential. `egress` (egress-gate, F7.2) joins gates and keys
17
+ * in the gateway and parses its listener (`NYLORUN_EGRESS_LISTEN_*`, default `0.0.0.0:4200`).
18
+ *
19
+ * `harness` (F6.2) runs alone: it holds the harness credential and nothing else. It connects to
20
+ * core's Harness API listener (`NYLORUN_HARNESS_URL`, `NYLORUN_HARNESS_TOKEN`), calls models and
21
+ * remote MCP servers through the gates service (`NYLORUN_GATES_URL`, with run tokens only), and
22
+ * keeps its workspaces under `NYLORUN_HARNESS_ROOT`. It refuses to start with a database, the
23
+ * gates' or keys' credential, or Restate settings. A process running core starts the Harness API
24
+ * listener when `NYLORUN_HARNESS=remote` (`NYLORUN_HARNESS_LISTEN_*`, `NYLORUN_HARNESS_TOKEN`).
17
25
  *
18
26
  * Two listen modes:
19
27
  * - **local** (no `NYLORUN_LISTEN_*` / `NYLORUN_ALLOWED_HOSTS`): the Host binds
@@ -24,21 +32,31 @@
24
32
  * probes from inside the container.
25
33
  *
26
34
  * The Postgres, Restate and S2 endpoints are parsed and validated here;
27
- * `infra/*` builds the clients from them. So is who the Host's Tenant is when its database
35
+ * `infra/*` builds the clients from them. So is the Object store (`NYLORUN_OBJECT_STORE_*`),
36
+ * which every service may read; `host/main.ts` builds the `BlobStore` from it. So is who the Host's Tenant is when its database
28
37
  * holds none yet (`NYLORUN_TENANT_ID`, `NYLORUN_TENANT_NAME`, `NYLORUN_DERIVED_PRINCIPALS`).
29
38
  */
30
39
  import { DERIVED_PRINCIPAL_ID_PATTERN, isTenantId } from "@nylorun/core/compatibility";
31
- export const RUNTIME_SERVICES = ["core", "loop", "gates", "keys"];
40
+ import { blockedAddresses } from "../sandbox/pods/network-gate.js";
41
+ export const RUNTIME_SERVICES = ["core", "loop", "gates", "keys", "egress", "harness"];
32
42
  /**
33
43
  * Services that may share a process (D12): they hold the same secrets and parse the same
34
- * trust class of input. keys joins gates (F4.2); egress joins them in a later release.
44
+ * trust class of input. keys joins gates (F4.2), and egress joins them (F7.2). The harness
45
+ * (F6.2) runs agent code's neighbours and holds only its own credential: it shares with nothing.
35
46
  */
36
47
  const SERVICE_GROUPS = [
37
48
  ["core", "loop"],
38
- ["gates", "keys"],
49
+ ["gates", "keys", "egress"],
50
+ ["harness"],
39
51
  ];
40
52
  /** Where the gates service listens by default. */
41
53
  export const DEFAULT_GATES_LISTEN_PORT = 4100;
54
+ /** Where egress-gate listens by default. */
55
+ export const DEFAULT_EGRESS_LISTEN_PORT = 4200;
56
+ /** Where core's Harness API listener listens by default. */
57
+ export const DEFAULT_HARNESS_LISTEN_PORT = 4200;
58
+ /** Where a harness process answers `/health` (loopback only). */
59
+ export const DEFAULT_HARNESS_HEALTH_PORT = 4300;
42
60
  /** What a process runs without `--service`: core and loop, as `--role all` did. */
43
61
  export const DEFAULT_SERVICES = new Set([
44
62
  "core",
@@ -46,8 +64,6 @@ export const DEFAULT_SERVICES = new Set([
46
64
  ]);
47
65
  /** Services of the blueprint this release doesn't have yet. */
48
66
  const LATER_SERVICES = [
49
- "egress",
50
- "harness",
51
67
  "sandboxd",
52
68
  ];
53
69
  const ROLE_SERVICES = {
@@ -74,7 +90,7 @@ function read(env, name) {
74
90
  const trimmed = value.trim();
75
91
  return trimmed === "" ? undefined : trimmed;
76
92
  }
77
- const USAGE = "Usage: main.js [--service core,loop|gates]";
93
+ const USAGE = "Usage: main.js [--service core,loop|gates,keys,egress|harness]";
78
94
  /** Parses `--service a,b` (or the deprecated `--role`); throws `StackConfigError`. */
79
95
  export function parseServices(argv) {
80
96
  let service;
@@ -248,11 +264,14 @@ function parseIdentityKeys(env) {
248
264
  /** Parse the stack configuration; throws `StackConfigError` naming the variable. */
249
265
  export function parseStackConfig(env, argv) {
250
266
  const { services, deprecatedRole } = parseServices(argv);
267
+ if (services.has("harness"))
268
+ return { services, harness: parseHarnessService(env), endpoints: {} };
251
269
  // The image sets NYLORUN_LISTEN_*: only the API's processes read them.
252
270
  const servesApi = services.has("core") || services.has("loop");
253
271
  const listen = servesApi ? parseListen(env) : undefined;
254
272
  const operator = servesApi ? parseAdminListen(env) : undefined;
255
273
  const gates = services.has("gates") || services.has("keys") ? parseGates(env) : undefined;
274
+ const egress = services.has("egress") ? parseEgress(env) : undefined;
256
275
  // The gates service's clients: the loop's model and tool calls, and core's endpoint pings.
257
276
  const modelGate = servesApi ? parseModelGate(env) : undefined;
258
277
  // The keys service is the gateway's unless NYLORUN_KEYS_URL names another listener.
@@ -302,12 +321,23 @@ export function parseStackConfig(env, argv) {
302
321
  const rawBrowser = read(env, "NYLORUN_BROWSER_ACCESS");
303
322
  if (rawBrowser !== undefined && rawBrowser !== "on" && rawBrowser !== "off")
304
323
  throw new StackConfigError(`NYLORUN_BROWSER_ACCESS must be on or off, not ${rawBrowser}`);
324
+ const harnessMode = servesApi ? parseHarnessMode(env) : undefined;
325
+ const sandboxes = servesApi ? parseSandboxes(env) : undefined;
326
+ // Pods connect to the Harness API listener even when the Tenant runs its own harness.
327
+ const harnessListener = harnessMode === "remote" || sandboxes
328
+ ? parseHarnessListener(env, harnessMode === "remote")
329
+ : undefined;
330
+ if (harnessListener &&
331
+ [listen?.port, operator?.port].includes(harnessListener.listen.port))
332
+ throw new StackConfigError("NYLORUN_HARNESS_LISTEN_PORT must differ from NYLORUN_LISTEN_PORT and NYLORUN_ADMIN_LISTEN_PORT");
305
333
  const delivery = parseDelivery(env);
306
334
  const tenant = servesApi ? parseTenant(env) : undefined;
335
+ const objectStore = parseObjectStore(env);
307
336
  return {
308
337
  services,
309
338
  ...(deprecatedRole ? { deprecatedRole } : {}),
310
339
  ...(gates ? { gates } : {}),
340
+ ...(egress ? { egress } : {}),
311
341
  ...(modelGate ? { modelGate } : {}),
312
342
  ...(keys ? { keys } : {}),
313
343
  ...(rawPacking ? { packing: rawPacking } : {}),
@@ -315,11 +345,37 @@ export function parseStackConfig(env, argv) {
315
345
  endpoints,
316
346
  ...(publicUrl ? { publicUrl } : {}),
317
347
  ...(rawBrowser === undefined ? {} : { browserAccess: rawBrowser === "on" }),
348
+ ...(harnessMode ? { harnessMode } : {}),
349
+ ...(harnessListener ? { harnessListener } : {}),
350
+ ...(sandboxes ? { sandboxes } : {}),
318
351
  ...(operator ? { operator } : {}),
319
352
  ...(delivery ? { delivery } : {}),
320
353
  ...(tenant ? { tenant } : {}),
354
+ ...(objectStore ? { objectStore } : {}),
321
355
  };
322
356
  }
357
+ /** S3 bucket names: 3 to 63 lowercase letters, digits, dots and hyphens. */
358
+ const BUCKET = /^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/;
359
+ /** `StackConfig.objectStore` from `NYLORUN_OBJECT_STORE_*`, or `undefined` without an endpoint. */
360
+ function parseObjectStore(env) {
361
+ const endpoint = parseUrl(env, "NYLORUN_OBJECT_STORE_ENDPOINT", ["http:", "https:"]);
362
+ const accessKeyId = read(env, "NYLORUN_OBJECT_STORE_ACCESS_KEY");
363
+ const secretAccessKey = read(env, "NYLORUN_OBJECT_STORE_SECRET_KEY");
364
+ if (endpoint === undefined) {
365
+ if (accessKeyId !== undefined || secretAccessKey !== undefined)
366
+ throw new StackConfigError("NYLORUN_OBJECT_STORE_ACCESS_KEY or _SECRET_KEY is set without NYLORUN_OBJECT_STORE_ENDPOINT: set the S3 endpoint, e.g. http://rustfs:9000");
367
+ return undefined;
368
+ }
369
+ if (accessKeyId === undefined || secretAccessKey === undefined)
370
+ throw new StackConfigError("NYLORUN_OBJECT_STORE_ACCESS_KEY and NYLORUN_OBJECT_STORE_SECRET_KEY are required with NYLORUN_OBJECT_STORE_ENDPOINT (`nylorun start` sets them)");
371
+ const bucket = read(env, "NYLORUN_OBJECT_STORE_BUCKET") ?? "nylorun";
372
+ if (!BUCKET.test(bucket))
373
+ throw new StackConfigError(`NYLORUN_OBJECT_STORE_BUCKET must be an S3 bucket name (3 to 63 of a-z 0-9 . -), not ${bucket}`);
374
+ const region = read(env, "NYLORUN_OBJECT_STORE_REGION") ?? "us-east-1";
375
+ if (!/^[a-z0-9-]+$/.test(region))
376
+ throw new StackConfigError(`NYLORUN_OBJECT_STORE_REGION is not a region: ${region}`);
377
+ return { endpoint: endpoint.replace(/\/+$/, ""), bucket, region, accessKeyId, secretAccessKey };
378
+ }
323
379
  function parseTenant(env) {
324
380
  const id = read(env, "NYLORUN_TENANT_ID");
325
381
  if (id !== undefined && !isTenantId(id))
@@ -336,6 +392,132 @@ function parseTenant(env) {
336
392
  };
337
393
  }
338
394
  const GATES_TOKEN = /^[0-9a-f]{64,}$/i;
395
+ /** `NYLORUN_HARNESS`: `in-process` (the default when unset) or `remote`. */
396
+ function parseHarnessMode(env) {
397
+ const raw = read(env, "NYLORUN_HARNESS");
398
+ if (raw === undefined)
399
+ return undefined;
400
+ if (raw !== "in-process" && raw !== "remote")
401
+ throw new StackConfigError(`NYLORUN_HARNESS must be in-process or remote, not ${raw}`);
402
+ return raw;
403
+ }
404
+ function parseHarnessToken(env, why) {
405
+ const token = read(env, "NYLORUN_HARNESS_TOKEN");
406
+ if (token === undefined)
407
+ throw new StackConfigError(`NYLORUN_HARNESS_TOKEN is required ${why} (\`nylorun start\` sets it)`);
408
+ if (!GATES_TOKEN.test(token))
409
+ throw new StackConfigError("NYLORUN_HARNESS_TOKEN must be at least 32 bytes as hex");
410
+ return token;
411
+ }
412
+ /** `StackConfig.sandboxes` from `NYLORUN_SANDBOXES_*`, or undefined without a URL. */
413
+ function parseSandboxes(env) {
414
+ const url = parseUrl(env, "NYLORUN_SANDBOXES_URL", ["http:", "https:"]);
415
+ if (url === undefined)
416
+ return undefined;
417
+ const token = read(env, "NYLORUN_SANDBOXES_TOKEN");
418
+ if (token === undefined || token.length < 32)
419
+ throw new StackConfigError("NYLORUN_SANDBOXES_TOKEN (at least 32 characters) is required with NYLORUN_SANDBOXES_URL (`nylorun sandbox enable` sets both)");
420
+ const harnessImage = read(env, "NYLORUN_SANDBOX_HARNESS_IMAGE");
421
+ if (harnessImage === undefined || /\s/.test(harnessImage))
422
+ throw new StackConfigError("NYLORUN_SANDBOX_HARNESS_IMAGE is required with NYLORUN_SANDBOXES_URL: the Runtime image sandbox pods copy the engine from (`nylorun start` sets it)");
423
+ return { url: url.replace(/\/+$/, ""), token, harnessImage };
424
+ }
425
+ /**
426
+ * `StackConfig.harnessListener` from `NYLORUN_HARNESS_LISTEN_*` and `NYLORUN_HARNESS_TOKEN`
427
+ * (required with `NYLORUN_HARNESS=remote`; optional when only sandbox pods connect).
428
+ */
429
+ function parseHarnessListener(env, tokenRequired = true) {
430
+ const host = read(env, "NYLORUN_HARNESS_LISTEN_HOST") ?? DEFAULT_CONTAINER_LISTEN_HOST;
431
+ if (/\s|\//.test(host))
432
+ throw new StackConfigError(`NYLORUN_HARNESS_LISTEN_HOST is not an address: ${host}`);
433
+ const rawPort = read(env, "NYLORUN_HARNESS_LISTEN_PORT");
434
+ const port = rawPort === undefined
435
+ ? DEFAULT_HARNESS_LISTEN_PORT
436
+ : parsePort("NYLORUN_HARNESS_LISTEN_PORT", rawPort);
437
+ const rawAllowed = read(env, "NYLORUN_HARNESS_ALLOWED_HOSTS");
438
+ const explicit = rawAllowed === undefined
439
+ ? []
440
+ : rawAllowed
441
+ .split(",")
442
+ .map((entry) => entry.trim())
443
+ .filter((entry) => entry !== "")
444
+ .map((entry) => normalizeAllowedHost("NYLORUN_HARNESS_ALLOWED_HOSTS", entry));
445
+ if (explicit.length === 0 && !isLoopbackAddress(host))
446
+ throw new StackConfigError(`NYLORUN_HARNESS_ALLOWED_HOSTS is required when NYLORUN_HARNESS_LISTEN_HOST is ${host}: list the Host headers harnesses send, e.g. runtime:${port}`);
447
+ const token = tokenRequired || read(env, "NYLORUN_HARNESS_TOKEN") !== undefined
448
+ ? parseHarnessToken(env, "with NYLORUN_HARNESS=remote: the credential harnesses present")
449
+ : undefined;
450
+ return {
451
+ listen: { host, port, allowedHosts: [...new Set([...explicit, ...loopbackForms(port)])] },
452
+ ...(token ? { token } : {}),
453
+ };
454
+ }
455
+ /** What a harness process must not hold: it runs next to agent code. */
456
+ const HARNESS_REFUSED = ["NYLORUN_DATABASE_URL", "NYLORUN_GATES_TOKEN", "NYLORUN_KEYS_URL"];
457
+ /** `StackConfig.harness` from `NYLORUN_HARNESS_*` and `NYLORUN_GATES_URL`. */
458
+ function parseHarnessService(env) {
459
+ const held = [
460
+ ...HARNESS_REFUSED.filter((name) => read(env, name) !== undefined),
461
+ ...Object.keys(env)
462
+ .filter((name) => name.startsWith("NYLORUN_RESTATE_") && read(env, name) !== undefined)
463
+ .sort(),
464
+ ];
465
+ if (held.length > 0)
466
+ throw new StackConfigError(`--service harness refuses to start with ${held.join(", ")} set: a harness reaches no database, no gate or keys credential and no Restate`);
467
+ const pod = read(env, "NYLORUN_SANDBOX_KIND") === "pod";
468
+ const url = parseUrl(env, "NYLORUN_HARNESS_URL", pod ? ["http:", "https:", "ws:", "wss:"] : ["ws:", "wss:"]);
469
+ if (url === undefined)
470
+ throw new StackConfigError("NYLORUN_HARNESS_URL is required for --service harness: core's Harness API, e.g. ws://runtime:4200/nylorun/harness/v1");
471
+ const gatesUrl = parseUrl(env, "NYLORUN_GATES_URL", ["http:", "https:"]);
472
+ if (gatesUrl === undefined)
473
+ throw new StackConfigError("NYLORUN_GATES_URL is required for --service harness: its model and MCP calls go through the gates service, e.g. http://gateway:4100");
474
+ const root = read(env, "NYLORUN_HARNESS_ROOT") ?? "/harness";
475
+ if (!root.startsWith("/"))
476
+ throw new StackConfigError(`NYLORUN_HARNESS_ROOT must be an absolute path, not ${root}`);
477
+ const rawHealth = read(env, "NYLORUN_HARNESS_HEALTH_PORT");
478
+ const podHost = pod ? parsePodHost(env, url) : undefined;
479
+ return {
480
+ url: podHost ? podHost.wsUrl : url,
481
+ ...(podHost
482
+ ? { pod: podHost.config }
483
+ : { token: parseHarnessToken(env, "for --service harness: the credential core's Harness API accepts") }),
484
+ gatesUrl: gatesUrl.replace(/\/+$/, ""),
485
+ root,
486
+ healthPort: rawHealth === undefined
487
+ ? DEFAULT_HARNESS_HEALTH_PORT
488
+ : parsePort("NYLORUN_HARNESS_HEALTH_PORT", rawHealth),
489
+ };
490
+ }
491
+ /**
492
+ * The engine in a pod sandbox: the pod's identity and its join file, from the variables the
493
+ * sandboxes service renders into the pod. `NYLORUN_HARNESS_URL` is the listener's address on the
494
+ * Docker host (`http://<host>:<port>`); the WebSocket URL adds the Harness API path.
495
+ */
496
+ function parsePodHost(env, url) {
497
+ const need = (name) => {
498
+ const value = read(env, name);
499
+ if (value === undefined)
500
+ throw new StackConfigError(`${name} is required for the engine in a sandbox pod (NYLORUN_SANDBOX_KIND=pod)`);
501
+ return value;
502
+ };
503
+ if (read(env, "NYLORUN_HARNESS_TOKEN") !== undefined)
504
+ throw new StackConfigError("A sandbox pod's engine holds no NYLORUN_HARNESS_TOKEN: it joins with its join token");
505
+ const base = new URL(url);
506
+ const secure = base.protocol === "https:" || base.protocol === "wss:";
507
+ const origin = `${secure ? "https" : "http"}://${base.host}`;
508
+ const egress = read(env, "NYLORUN_EGRESS_PROXY");
509
+ return {
510
+ wsUrl: `${secure ? "wss" : "ws"}://${base.host}/nylorun/harness/v1`,
511
+ config: {
512
+ sandboxId: need("NYLORUN_SANDBOX_ID"),
513
+ podUid: need("NYLORUN_POD_UID"),
514
+ joinFile: need("NYLORUN_SANDBOX_JOIN_FILE"),
515
+ httpUrl: origin,
516
+ ...(egress ? { egressProxy: egress.replace(/\/+$/, "") } : {}),
517
+ blocked: blockedAddresses(env),
518
+ },
519
+ };
520
+ }
339
521
  /** `StackConfig.gates` from `NYLORUN_GATES_*`. */
340
522
  function parseGates(env) {
341
523
  const host = read(env, "NYLORUN_GATES_LISTEN_HOST") ?? DEFAULT_CONTAINER_LISTEN_HOST;
@@ -365,6 +547,15 @@ function parseGates(env) {
365
547
  token,
366
548
  };
367
549
  }
550
+ /** `StackConfig.egress` from `NYLORUN_EGRESS_LISTEN_*`. CONNECT has no Host header to check. */
551
+ function parseEgress(env) {
552
+ const host = read(env, "NYLORUN_EGRESS_LISTEN_HOST") ?? DEFAULT_CONTAINER_LISTEN_HOST;
553
+ if (/\s|\//.test(host))
554
+ throw new StackConfigError(`NYLORUN_EGRESS_LISTEN_HOST is not an address: ${host}`);
555
+ const rawPort = read(env, "NYLORUN_EGRESS_LISTEN_PORT");
556
+ const port = rawPort === undefined ? DEFAULT_EGRESS_LISTEN_PORT : parsePort("NYLORUN_EGRESS_LISTEN_PORT", rawPort);
557
+ return { listen: { host, port } };
558
+ }
368
559
  /** `StackConfig.modelGate` from `NYLORUN_GATES_URL` and `NYLORUN_GATES_TOKEN`. */
369
560
  function parseModelGate(env) {
370
561
  const url = parseUrl(env, "NYLORUN_GATES_URL", ["http:", "https:"]);
@@ -1,5 +1,6 @@
1
1
  import type { JsonObject } from "@nylorun/core/define";
2
- import type { AgentManifest } from "@nylorun/core/define";
2
+ import { type AgentManifest } from "@nylorun/core/define";
3
+ import type { DurableSessionTool, HostEffect } from "@nylorun/harness/run";
3
4
  export interface McpToolRecord {
4
5
  /** The agent used as a tool that declares the server; absent for the session's root agent. */
5
6
  readonly agentId?: string;
@@ -27,3 +28,9 @@ export interface McpDiagnostic {
27
28
  }
28
29
  export declare function modelToolName(serverName: string, serverToolName: string): string;
29
30
  export declare function declaredToolNames(manifest: AgentManifest): Set<string>;
31
+ /** The tools of a snapshot, as the engine advertises them. */
32
+ export declare function sessionToolsOf(snapshot: McpSnapshot | undefined): readonly DurableSessionTool[] | undefined;
33
+ /** The snapshot's record of an MCP tool call, or undefined when the call is not an MCP tool. */
34
+ export declare function mcpToolOf(snapshot: McpSnapshot | undefined, request: Pick<HostEffect, "agent" | "capabilityId" | "toolName">): McpToolRecord | undefined;
35
+ /** The manifest of the agent an effect belongs to: the root, or an agent it uses as a tool. */
36
+ export declare function manifestFor(manifest: AgentManifest, agent: HostEffect["agent"]): AgentManifest | undefined;
@@ -1,3 +1,4 @@
1
+ import { delegateManifest } from "@nylorun/core/define";
1
2
  export function modelToolName(serverName, serverToolName) {
2
3
  return `${serverName}__${serverToolName}`;
3
4
  }
@@ -8,3 +9,30 @@ export function declaredToolNames(manifest) {
8
9
  names.add(tool.name);
9
10
  return names;
10
11
  }
12
+ /** The tools of a snapshot, as the engine advertises them. */
13
+ export function sessionToolsOf(snapshot) {
14
+ if (!snapshot?.mcpTools.length)
15
+ return undefined;
16
+ return snapshot.mcpTools.map((tool) => ({
17
+ ...(tool.agentId === undefined ? {} : { agentId: tool.agentId }),
18
+ capabilityId: tool.capabilityId,
19
+ name: tool.name,
20
+ ...(tool.description === undefined
21
+ ? {}
22
+ : { description: tool.description }),
23
+ inputSchema: tool.inputSchema,
24
+ ...(tool.outputSchema === undefined
25
+ ? {}
26
+ : { outputSchema: tool.outputSchema }),
27
+ }));
28
+ }
29
+ /** The snapshot's record of an MCP tool call, or undefined when the call is not an MCP tool. */
30
+ export function mcpToolOf(snapshot, request) {
31
+ return snapshot?.mcpTools.find((tool) => tool.agentId === request.agent?.id &&
32
+ tool.capabilityId === request.capabilityId &&
33
+ tool.name === request.toolName);
34
+ }
35
+ /** The manifest of the agent an effect belongs to: the root, or an agent it uses as a tool. */
36
+ export function manifestFor(manifest, agent) {
37
+ return agent ? delegateManifest(manifest, agent.id) : manifest;
38
+ }
package/dist/media.d.ts CHANGED
@@ -1,29 +1,10 @@
1
+ /**
2
+ * Image checks for code that handles image bytes itself (an image tool in an Action endpoint):
3
+ * the supported types, the size cap and a signature check. Storing images is the job of file
4
+ * artifacts (`artifacts/`, protocol 6), which replaced `MediaStore`.
5
+ */
1
6
  export declare const IMAGE_MEDIA_TYPES: readonly string[];
2
7
  export declare const MAX_IMAGE_BYTES: number;
3
- export interface MediaAsset {
4
- readonly id: string;
5
- readonly mediaType: string;
6
- readonly bytes: number;
7
- readonly kind: "input" | "generated";
8
- }
9
- /** Opaque reference retained by Harness and resolved by the configured media adapter. */
10
- export interface MediaReference {
11
- readonly agentId: string;
12
- readonly assetId: string;
13
- }
14
8
  export declare function decodeImageBase64(mediaType: string, base64: string): Uint8Array;
15
9
  /** Verify the asserted media type and lightweight file signature for a supported image. */
16
10
  export declare function validateImageBytes(mediaType: string, bytes: Uint8Array): void;
17
- export interface RuntimeMedia {
18
- saveInput(agentId: string, sessionId: string, mediaType: string, base64: string): Promise<MediaAsset>;
19
- saveGenerated(agentId: string, sessionId: string, mediaType: string, bytes: Uint8Array): Promise<MediaAsset>;
20
- dataUrl(reference: MediaReference, sessionId: string): Promise<{
21
- readonly asset: MediaAsset;
22
- readonly url: string;
23
- } | undefined>;
24
- latestInput(agentId: string, sessionId: string): Promise<MediaAsset | undefined>;
25
- read(agentId: string, sessionId: string, assetId: string): Promise<{
26
- asset: MediaAsset;
27
- bytes: Uint8Array;
28
- } | undefined>;
29
- }
package/dist/media.js CHANGED
@@ -1,3 +1,8 @@
1
+ /**
2
+ * Image checks for code that handles image bytes itself (an image tool in an Action endpoint):
3
+ * the supported types, the size cap and a signature check. Storing images is the job of file
4
+ * artifacts (`artifacts/`, protocol 6), which replaced `MediaStore`.
5
+ */
1
6
  export const IMAGE_MEDIA_TYPES = Object.freeze([
2
7
  "image/jpeg",
3
8
  "image/png",
@@ -1,6 +1,6 @@
1
1
  import { type Credential } from "@earendil-works/pi-ai";
2
2
  import type { RuntimeModelAdapter } from "../contracts.js";
3
- import type { RuntimeMedia } from "../adapters/media.js";
3
+ import { type FileResolver } from "../artifacts/files.js";
4
4
  import type { HostModelSecret } from "../vault/service.js";
5
5
  export interface ModelPreview {
6
6
  readonly invocationId: string;
@@ -20,7 +20,11 @@ export interface ModelCallSettings {
20
20
  export interface PiModelOptions {
21
21
  readonly root?: string;
22
22
  readonly onPreview?: (preview: ModelPreview) => void;
23
- readonly media?: Pick<RuntimeMedia, "dataUrl">;
23
+ /**
24
+ * Resolves the files a prompt names (media parts whose reference is an artifact version) to
25
+ * their bytes. Model-gate passes one over the Tenant's artifacts (`artifactFiles`).
26
+ */
27
+ readonly files?: FileResolver;
24
28
  readonly readHostModel?: () => HostModelSecret | undefined | Promise<HostModelSecret | undefined>;
25
29
  readonly writeHostCredential?: (credential: Credential) => void | Promise<void>;
26
30
  readonly settings?: ModelCallSettings;
@@ -1,4 +1,6 @@
1
1
  import { parseJsonWithRepair, retryAssistantCall, } from "@earendil-works/pi-ai";
2
+ import { FileUnavailableError, } from "../artifacts/files.js";
3
+ import { essence, isModelImage, isText } from "../artifacts/media-types.js";
2
4
  import { scrub } from "../redact.js";
3
5
  import { classifyAssistantError, failure } from "./classify.js";
4
6
  import { modelsFor } from "./models.js";
@@ -227,28 +229,43 @@ async function buildContext(call, selected, options) {
227
229
  if (part.type === "text")
228
230
  result.push({ type: "text", text: part.text });
229
231
  else if (part.type === "media") {
230
- if (!selected.input.includes("image"))
231
- throw new InvalidRequest("The configured model does not accept images.");
232
- const ref = part.reference;
233
- if (!ref ||
234
- typeof ref !== "object" ||
235
- Array.isArray(ref) ||
236
- !("agentId" in ref) ||
237
- !("assetId" in ref) ||
238
- typeof ref.agentId !== "string" ||
239
- typeof ref.assetId !== "string")
240
- throw new InvalidRequest("Expected a local media reference.");
241
- const asset = await options.media?.dataUrl({ agentId: ref.agentId, assetId: ref.assetId }, "sessionId" in ref && typeof ref.sessionId === "string"
242
- ? ref.sessionId
243
- : call.executionId);
244
- if (!asset)
245
- throw new InvalidRequest("Media is unavailable; pass the shared media adapter to piModel({ media }).");
246
- const comma = asset.url.indexOf(",");
247
- result.push({
248
- type: "image",
249
- data: asset.url.slice(comma + 1),
250
- mimeType: asset.asset.mediaType,
251
- });
232
+ // A file the message named by artifact (protocol 6): its bytes go only into the
233
+ // provider request built here, never into the transcript or the journal.
234
+ if (!options.files)
235
+ throw new InvalidRequest("Files are unavailable; pass a file resolver to piModel({ files }).");
236
+ let file;
237
+ try {
238
+ file = await options.files(part.reference);
239
+ }
240
+ catch (error) {
241
+ if (error instanceof FileUnavailableError)
242
+ throw new InvalidRequest(error.message);
243
+ throw error;
244
+ }
245
+ if (isModelImage(file.mediaType)) {
246
+ if (!selected.input.includes("image"))
247
+ throw new InvalidRequest("The configured model does not accept images.");
248
+ result.push({
249
+ type: "image",
250
+ data: Buffer.from(file.bytes).toString("base64"),
251
+ mimeType: essence(file.mediaType),
252
+ });
253
+ }
254
+ else if (isText(file.mediaType)) {
255
+ let text;
256
+ try {
257
+ text = new TextDecoder("utf-8", { fatal: true }).decode(file.bytes);
258
+ }
259
+ catch {
260
+ throw new InvalidRequest(`${file.name} is not UTF-8 text.`);
261
+ }
262
+ result.push({
263
+ type: "text",
264
+ text: `<file name=${JSON.stringify(file.name)} type=${JSON.stringify(essence(file.mediaType))}>\n${text}\n</file>`,
265
+ });
266
+ }
267
+ else
268
+ throw new InvalidRequest(`The model reads images and text files; ${file.name} is ${essence(file.mediaType)}.`);
252
269
  }
253
270
  }
254
271
  return result;
@@ -1,5 +1,5 @@
1
- export { localMedia, MediaStore } from "../adapters/media.js";
2
1
  export { jsonlObserver } from "../adapters/observe.js";
3
2
  export { piModel, type PiModelOptions } from "../model/pi-model.js";
3
+ export { FileUnavailableError, type FileResolver, type ResolvedFile, } from "../artifacts/files.js";
4
4
  export { projectAsset } from "../assets.js";
5
5
  export { probeSandboxBackends, type SandboxSelectionReport, } from "../sandbox/select.js";
@@ -1,5 +1,5 @@
1
- export { localMedia, MediaStore } from "../adapters/media.js";
2
1
  export { jsonlObserver } from "../adapters/observe.js";
3
2
  export { piModel } from "../model/pi-model.js";
3
+ export { FileUnavailableError, } from "../artifacts/files.js";
4
4
  export { projectAsset } from "../assets.js";
5
5
  export { probeSandboxBackends, } from "../sandbox/select.js";