@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
package/dist/host/app.js CHANGED
@@ -45,7 +45,10 @@ export function createHostApp(options) {
45
45
  });
46
46
  app.use(async (c, next) => {
47
47
  const { incoming } = c.env;
48
- if (requestHasBody(incoming) && !isJsonContentType(headerValue(incoming, "content-type")))
48
+ if (requestHasBody(incoming) &&
49
+ !isJsonContentType(headerValue(incoming, "content-type")) &&
50
+ // An artifact upload's body is the file itself, of any type.
51
+ tenantRouteOf(incoming)?.bytes !== true)
49
52
  return rejectedResponse(415, "unsupported_media_type", "Request bodies must use application/json");
50
53
  await next();
51
54
  });
@@ -100,7 +103,10 @@ export function createHostApp(options) {
100
103
  return rejectedResponse(400, "invalid_request", `${PUBLISHABLE_KEY_HEADER} header is malformed`);
101
104
  const tenantHeader = headerValue(incoming, TENANT_HEADER);
102
105
  const named = tenantHeader === undefined || tenantHeader.trim() === "" ? undefined : tenantHeader.trim();
103
- if (!protocolAccepted(headerValue(incoming, PROTOCOL_HEADER)))
106
+ const protocol = headerValue(incoming, PROTOCOL_HEADER);
107
+ // A capability link is opened without the header; when one is sent, it is checked.
108
+ if (!protocolAccepted(protocol) &&
109
+ !(protocol === undefined && tenantRouteOf(incoming)?.unversioned === true))
104
110
  return protocolRejectedResponse();
105
111
  const resolution = await module.resolve();
106
112
  if (resolution.kind !== "open") {
@@ -152,6 +158,21 @@ export function createHostApp(options) {
152
158
  function browserRoute(method, segments) {
153
159
  return findTenantRoute(method, segments)?.browser === true;
154
160
  }
161
+ /** The Tenant route a request names, if any; a malformed path names none. */
162
+ function tenantRouteOf(incoming) {
163
+ const segments = [];
164
+ for (const segment of pathnameOf(incoming).split("/").filter(Boolean)) {
165
+ try {
166
+ segments.push(decodeURIComponent(segment));
167
+ }
168
+ catch {
169
+ return undefined;
170
+ }
171
+ }
172
+ if (segments[0] !== "v1" || segments[1] === "admin")
173
+ return undefined;
174
+ return findTenantRoute(incoming.method ?? "GET", segments);
175
+ }
155
176
  /** The answer's status: a Response's, or the Node response's when it was written there. */
156
177
  function statusOf(c) {
157
178
  const logged = c.get("status");
@@ -3,7 +3,8 @@
3
3
  * the Tool Gate's routes (`api/gate/routes.ts`), behind the same `Host` check as the Runtime's
4
4
  * listeners.
5
5
  * It opens no Tenant runtime, no Restate endpoint and no stream relay, and runs no migration;
6
- * it needs only the Postgres pool and the Host's tenant directory (read-only).
6
+ * it needs only the Postgres pool and the Host's tenant directory (read-only). Run tokens (F5)
7
+ * verify against the Tenant's public keys and session rows in that database.
7
8
  *
8
9
  * Nothing is written while a model call runs, so the server's request timeout sits above the
9
10
  * gate's longest call. Closing stops accepting calls, lets running ones finish for up to
@@ -13,11 +14,13 @@ import { type Server } from "node:http";
13
14
  import { type InflightCallsOptions } from "../gates/inflight.js";
14
15
  import type { openMcpServer } from "../mcp/connect.js";
15
16
  import type { OutboundPolicy } from "../tenant/outbound.js";
17
+ import { type BlobStore } from "../blob/index.js";
16
18
  import type { ModelCallSettings } from "../gates/model-gate.js";
17
19
  import { type TenantVaults } from "../gates/tenant-vaults.js";
18
20
  import type { PostgresClient } from "../store/postgres/connect.js";
19
21
  import type { Logger } from "../tenant/types.js";
20
- import type { GatesConfig } from "./stack-config.js";
22
+ import type { EgressConfig, GatesConfig } from "./stack-config.js";
23
+ import { type EgressGate, type StartEgressGateOptions } from "../gates/egress.js";
21
24
  /** Above the gate's 600 s provider request timeout and the loop's 630 s client timeout. */
22
25
  export declare const GATES_REQUEST_TIMEOUT_MS = 660000;
23
26
  export interface StartGatesOptions {
@@ -48,6 +51,11 @@ export interface StartGatesOptions {
48
51
  * with the Tenant's vault key. The gateway is then not ready until the key file is there.
49
52
  */
50
53
  readonly keys?: boolean;
54
+ /**
55
+ * The Object store model-gate reads the files a prompt names from (protocol 6): the stack's
56
+ * `s3` store. Without one, the Tenant's `fs` store under the Host root, when there is one.
57
+ */
58
+ readonly blobs?: BlobStore;
51
59
  }
52
60
  export interface GatesServer {
53
61
  /** Where the gate listens, e.g. `http://0.0.0.0:4100`. */
@@ -56,3 +64,20 @@ export interface GatesServer {
56
64
  close(): Promise<void>;
57
65
  }
58
66
  export declare function startGates(options: StartGatesOptions): Promise<GatesServer>;
67
+ export interface StartEgressOptions {
68
+ readonly egress: EgressConfig;
69
+ readonly logger: Logger;
70
+ /** The pool for the Tenant's keys and sandboxes. Required unless `vaults` is given. */
71
+ readonly database?: PostgresClient;
72
+ /** The Host root. Required unless `vaults` is given. */
73
+ readonly hostRoot?: string;
74
+ /** Replaces the Postgres-backed Tenant (tests). */
75
+ readonly vaults?: TenantVaults;
76
+ /** Replaces how sandboxes are read, and the gate's limits and hooks (tests). */
77
+ readonly gate?: Partial<Omit<StartEgressGateOptions, "listen" | "logger" | "verify">>;
78
+ }
79
+ /**
80
+ * egress-gate (`--service egress`, F7.2): pods' CONNECT proxy, verifying egress tokens against the
81
+ * Tenant's public keys and reading its sandboxes from the same database as the gates.
82
+ */
83
+ export declare function startEgress(options: StartEgressOptions): Promise<EgressGate>;
@@ -3,7 +3,8 @@
3
3
  * the Tool Gate's routes (`api/gate/routes.ts`), behind the same `Host` check as the Runtime's
4
4
  * listeners.
5
5
  * It opens no Tenant runtime, no Restate endpoint and no stream relay, and runs no migration;
6
- * it needs only the Postgres pool and the Host's tenant directory (read-only).
6
+ * it needs only the Postgres pool and the Host's tenant directory (read-only). Run tokens (F5)
7
+ * verify against the Tenant's public keys and session rows in that database.
7
8
  *
8
9
  * Nothing is written while a model call runs, so the server's request timeout sits above the
9
10
  * gate's longest call. Closing stops accepting calls, lets running ones finish for up to
@@ -18,9 +19,13 @@ import { createMcpHandler, GATE_MCP_IDLE_MS } from "../gates/mcp-handler.js";
18
19
  import { createToolCalls } from "../gates/tool-calls.js";
19
20
  import { existsSync } from "node:fs";
20
21
  import { tenantPaths } from "../tenant/paths.js";
22
+ import { staleRun, verifyRunToken } from "../tenant/run-token.js";
23
+ import { createFsBlobStore } from "../blob/index.js";
21
24
  import { createTenantVaults } from "../gates/tenant-vaults.js";
22
25
  import { probeDatabase } from "../infra/database.js";
23
26
  import { bindListener, headerValue, isAllowedRequestHost, sendRejected } from "./http.js";
27
+ import { startEgressGate, storeEgressSandboxes } from "../gates/egress.js";
28
+ import { verifyEgressToken } from "../sandbox/egress-token.js";
24
29
  /** Above the gate's 600 s provider request timeout and the loop's 630 s client timeout. */
25
30
  export const GATES_REQUEST_TIMEOUT_MS = 660_000;
26
31
  export async function startGates(options) {
@@ -31,6 +36,10 @@ export async function startGates(options) {
31
36
  throw new Error("startGates needs the Postgres pool and the Host root");
32
37
  vaults = createTenantVaults({ sql: database, hostRoot: options.hostRoot });
33
38
  }
39
+ const blobs = options.blobs ??
40
+ (options.hostRoot !== undefined
41
+ ? createFsBlobStore({ root: tenantPaths(options.hostRoot).blobs })
42
+ : undefined);
34
43
  const inflight = createInflightCalls(options.inflight);
35
44
  const mcpIdleMs = options.mcpIdleMs ?? GATE_MCP_IDLE_MS;
36
45
  const mcp = createMcpHandler({
@@ -49,13 +58,24 @@ export async function startGates(options) {
49
58
  sweep.unref();
50
59
  const prune = setInterval(() => void toolCalls.prune(), 60 * 60_000);
51
60
  prune.unref();
61
+ // Run tokens verify against the Tenant's public keys and session rows (F5): the same
62
+ // database the ledger and `tool_crossings` use.
63
+ const publicKeys = new Map();
52
64
  const app = createGatesApp({
53
65
  token: gates.token,
66
+ runs: {
67
+ async verify(raw) {
68
+ const vault = await vaults.open();
69
+ return verifyRunToken(vault.store, vault.tenantId, raw, publicKeys);
70
+ },
71
+ stale: async (claims) => staleRun((await vaults.open()).store, claims),
72
+ },
54
73
  inflight,
55
74
  modelGate: createModelCallHandler({
56
75
  vaults,
57
76
  logger,
58
77
  ...(options.settings ? { settings: options.settings } : {}),
78
+ ...(blobs ? { blobs } : {}),
59
79
  }),
60
80
  ready: async () => {
61
81
  // The keys service needs the vault key, which `nylorun start` writes; it never creates one.
@@ -125,3 +145,28 @@ export async function startGates(options) {
125
145
  },
126
146
  };
127
147
  }
148
+ /**
149
+ * egress-gate (`--service egress`, F7.2): pods' CONNECT proxy, verifying egress tokens against the
150
+ * Tenant's public keys and reading its sandboxes from the same database as the gates.
151
+ */
152
+ export async function startEgress(options) {
153
+ const { database } = options;
154
+ let vaults = options.vaults;
155
+ if (!vaults) {
156
+ if (!database || options.hostRoot === undefined)
157
+ throw new Error("startEgress needs the Postgres pool and the Host root");
158
+ vaults = createTenantVaults({ sql: database, hostRoot: options.hostRoot });
159
+ }
160
+ const tenant = vaults;
161
+ const publicKeys = new Map();
162
+ return startEgressGate({
163
+ listen: options.egress.listen,
164
+ logger: options.logger,
165
+ async verify(raw) {
166
+ const vault = await tenant.open();
167
+ return verifyEgressToken(vault.store, vault.tenantId, raw, publicKeys);
168
+ },
169
+ sandboxes: { live: async (sandboxId) => storeEgressSandboxes((await tenant.open()).store).live(sandboxId) },
170
+ ...options.gate,
171
+ });
172
+ }
@@ -43,7 +43,10 @@ export declare function isJsonContentType(value: string | undefined): boolean;
43
43
  export declare function requestHasBody(request: IncomingMessage): boolean;
44
44
  /** `/health` and `/ready` are polled; the request log leaves them out. */
45
45
  export declare function pathnameIsLogged(rawUrl: string | undefined): boolean;
46
- /** Redact path parameters after `/v1/sessions/` for Host logs. */
46
+ /**
47
+ * Redact path parameters after `/v1/sessions/` for Host logs, and a capability link's token
48
+ * (a credential) after `/v1/artifact-links/`.
49
+ */
47
50
  export declare function redactRoutePath(pathname: string): string;
48
51
  /** Constant-time comparison of SHA-256 digests of the presented and stored admin keys. */
49
52
  export declare function adminKeyMatches(presented: string | undefined, adminKey: string): boolean;
package/dist/host/http.js CHANGED
@@ -135,13 +135,18 @@ export function pathnameIsLogged(rawUrl) {
135
135
  const pathname = new URL(rawUrl, "http://runtime.local").pathname;
136
136
  return pathname !== "/health" && pathname !== "/ready";
137
137
  }
138
- /** Redact path parameters after `/v1/sessions/` for Host logs. */
138
+ /**
139
+ * Redact path parameters after `/v1/sessions/` for Host logs, and a capability link's token
140
+ * (a credential) after `/v1/artifact-links/`.
141
+ */
139
142
  export function redactRoutePath(pathname) {
140
143
  const parts = pathname.split("/");
141
144
  // ["", "v1", "sessions", "<id>", ...]
142
145
  if (parts[1] === "v1" && parts[2] === "sessions" && parts.length > 3) {
143
146
  return `/v1/sessions/:id${parts.length > 4 ? "/…" : ""}`;
144
147
  }
148
+ if (parts[1] === "v1" && parts[2] === "artifact-links" && parts.length > 3)
149
+ return "/v1/artifact-links/:token";
145
150
  return pathname;
146
151
  }
147
152
  /** Constant-time comparison of SHA-256 digests of the presented and stored admin keys. */
package/dist/host/main.js CHANGED
@@ -13,10 +13,15 @@
13
13
  * `NYLORUN_DERIVED_PRINCIPALS`; tenancy.md §4); with S2, a process running core
14
14
  * runs the stream relay, which feeds the Tenant's streams from the record over
15
15
  * logical replication (one process at a time holds the slot); `/ready` reports
16
- * the Tenant and the infrastructure checks. A process running the gates service (the local
17
- * stack's `gateway` container) starts only the gate (`runGates`): it needs
18
- * neither host.json nor host-credentials.json. See the startup order in `main()`. Tests compose a Host without this
19
- * entry, with `createHost` and an injected Tenant module.
16
+ * the Tenant and the infrastructure checks. With an Object store
17
+ * (`NYLORUN_OBJECT_STORE_*`) the Tenant's blobs go to it through the `s3` BlobStore, and a
18
+ * process running core creates its bucket; without one they stay on disk (`fs`). A process
19
+ * running the gates service (the local stack's `gateway` container) starts only the gate, and egress-gate with `egress` (`runGates`): it needs
20
+ * neither host.json nor host-credentials.json, and reads the files a prompt names from the Object store. A process running the harness service (`--service harness`, F6.2)
21
+ * starts only the harness (`harness/main.ts`), which connects to core's Harness API. With `NYLORUN_HARNESS=remote`
22
+ * the Tenant runs no harness of its own: core starts the Harness API listener (`NYLORUN_HARNESS_LISTEN_*`) and
23
+ * harnesses that present `NYLORUN_HARNESS_TOKEN` run its segments and hold its workspaces. See the startup order
24
+ * in `main()`. Tests compose a Host without this entry, with `createHost` and an injected Tenant module.
20
25
  */
21
26
  import { readFileSync, mkdirSync, existsSync } from "node:fs";
22
27
  import { createRequire } from "node:module";
@@ -40,10 +45,15 @@ import { createHostLogger } from "./logger.js";
40
45
  import { describeEndpoints, describeServices, parseStackConfig, } from "./stack-config.js";
41
46
  import { createExecution, createInfra } from "../infra/index.js";
42
47
  import { createDatabase } from "../infra/database.js";
43
- import { startGates } from "./gates.js";
48
+ import { startEgress, startGates } from "./gates.js";
44
49
  import { httpToolGate } from "../gates/tool-client.js";
45
50
  import { httpKeys } from "../keys/client.js";
46
51
  import { httpModelGate } from "../gates/http-client.js";
52
+ import { createS3BlobStore } from "../blob/index.js";
53
+ import { createRunGrants } from "../tenant/run-grants.js";
54
+ import { runHarness } from "../harness/main.js";
55
+ import { startHarnessListener } from "../harness-api/ws-server.js";
56
+ import { httpSandboxesClient } from "../sandbox/pods/client.js";
47
57
  const nodeRequire = createRequire(import.meta.url);
48
58
  function coreVersion() {
49
59
  try {
@@ -61,38 +71,85 @@ function resolveHostRoot() {
61
71
  const root = fromEnv && fromEnv.length > 0 ? fromEnv : resolve(homedir(), ".nylorun");
62
72
  return resolve(root);
63
73
  }
74
+ /** The Object store's `s3` BlobStore (D35), from `StackConfig.objectStore`. */
75
+ function objectStore(config) {
76
+ return createS3BlobStore({
77
+ endpoint: config.endpoint,
78
+ bucket: config.bucket,
79
+ region: config.region,
80
+ accessKeyId: config.accessKeyId,
81
+ secretAccessKey: config.secretAccessKey,
82
+ });
83
+ }
84
+ /**
85
+ * Creates the Object store's bucket unless it exists, retrying while the store starts. In the
86
+ * background: only artifacts use the store, so a store that stays down is logged (artifact
87
+ * uploads and downloads fail meanwhile) and does not hold up the Host.
88
+ */
89
+ async function ensureBucket(store, bucket, logger) {
90
+ for (let attempt = 1;; attempt += 1) {
91
+ try {
92
+ await store.ensureBucket();
93
+ logger.info("object_store_ready", { bucket });
94
+ return;
95
+ }
96
+ catch (error) {
97
+ if (attempt >= 10) {
98
+ logger.error("object_store_unavailable", {
99
+ bucket,
100
+ message: error instanceof Error ? error.message : String(error),
101
+ });
102
+ return;
103
+ }
104
+ await new Promise((resolve) => setTimeout(resolve, Math.min(1000 * attempt, 5000)));
105
+ }
106
+ }
107
+ }
64
108
  /** The slot's lag in WAL bytes, when the source can tell. */
65
109
  async function lagOf(source) {
66
110
  const lagBytes = await source.lag?.().catch(() => undefined);
67
111
  return lagBytes === undefined ? {} : { lagBytes };
68
112
  }
69
113
  /**
70
- * The gates service: the Model Gate's listener over the Postgres pool and the Host's tenant
71
- * directory. Writes nothing to the Host root (the local stack mounts it read-only).
114
+ * The gateway's services: the Model Gate's listener (gates, keys) and egress-gate's (egress), over
115
+ * the Postgres pool and the Host's tenant directory. Writes nothing to the Host root (the local
116
+ * stack mounts it read-only).
72
117
  */
73
118
  async function runGates(stack) {
74
- const gates = stack.gates;
75
119
  if (!stack.endpoints.databaseUrl)
76
- throw new Error("NYLORUN_DATABASE_URL is required for --service gates: the gate reads Tenant vaults from Postgres (`nylorun start` sets it)");
120
+ throw new Error(`NYLORUN_DATABASE_URL is required for --service ${describeServices(stack.services)}: the gateway reads the Tenant from Postgres (\`nylorun start\` sets it)`);
77
121
  const logger = createHostLogger();
78
122
  logger.info("host_stack_config", {
79
123
  services: [...stack.services],
80
124
  ...(stack.packing ? { packing: stack.packing } : {}),
81
125
  endpoints: describeEndpoints(stack.endpoints),
126
+ objectStore: stack.objectStore ? "s3" : "none",
82
127
  });
83
128
  const database = createDatabase(stack);
84
- let server;
129
+ const servers = [];
85
130
  try {
86
- server = await startGates({
87
- gates,
88
- database,
89
- hostRoot: resolveHostRoot(),
90
- logger,
91
- ...(stack.delivery ? { delivery: stack.delivery } : {}),
92
- ...(stack.services.has("keys") ? { keys: true } : {}),
93
- });
131
+ if (stack.gates) {
132
+ const server = await startGates({
133
+ gates: stack.gates,
134
+ database,
135
+ hostRoot: resolveHostRoot(),
136
+ logger,
137
+ ...(stack.delivery ? { delivery: stack.delivery } : {}),
138
+ ...(stack.services.has("keys") ? { keys: true } : {}),
139
+ // Model-gate reads the files a prompt names from the Object store (protocol 6).
140
+ ...(stack.objectStore ? { blobs: objectStore(stack.objectStore) } : {}),
141
+ });
142
+ servers.push(server);
143
+ logger.info("gates_ready", { url: server.url });
144
+ }
145
+ if (stack.egress) {
146
+ const egress = await startEgress({ egress: stack.egress, database, hostRoot: resolveHostRoot(), logger });
147
+ servers.push(egress);
148
+ logger.info("egress_ready", { url: egress.url });
149
+ }
94
150
  }
95
151
  catch (error) {
152
+ await Promise.all(servers.map((server) => server.close()));
96
153
  await database.end({ timeout: 5 });
97
154
  if (error instanceof HostListenError) {
98
155
  logger.error("listen_failed", { message: error.message, exitCode: error.exitCode });
@@ -101,23 +158,23 @@ async function runGates(stack) {
101
158
  }
102
159
  throw error;
103
160
  }
104
- logger.info("gates_ready", { url: server.url });
105
161
  let stopping = false;
106
162
  for (const signal of ["SIGINT", "SIGTERM"])
107
163
  process.on(signal, () => {
108
164
  if (stopping)
109
165
  return;
110
166
  stopping = true;
111
- void server
112
- .close()
167
+ void Promise.all(servers.map((server) => server.close()))
113
168
  .then(() => database.end({ timeout: 5 }))
114
169
  .finally(() => process.exit(0));
115
170
  });
116
171
  }
117
172
  export async function main() {
118
173
  const stack = parseStackConfig(process.env, process.argv.slice(2));
119
- if (stack.services.has("gates") || stack.services.has("keys"))
174
+ if (stack.services.has("gates") || stack.services.has("keys") || stack.services.has("egress"))
120
175
  return runGates(stack);
176
+ if (stack.services.has("harness"))
177
+ return runHarness(stack, baselineEnvironment(process.env));
121
178
  const hostRoot = resolveHostRoot();
122
179
  const paths = hostPaths(hostRoot);
123
180
  mkdirSync(paths.home, { recursive: true });
@@ -150,8 +207,10 @@ export async function main() {
150
207
  ...(stack.services.has("loop")
151
208
  ? { modelGate: stack.modelGate ? stack.modelGate.url : "in-process" }
152
209
  : {}),
210
+ harness: stack.harnessMode ?? "in-process",
153
211
  mode: stack.listen ? "container" : "local",
154
212
  endpoints: describeEndpoints(stack.endpoints),
213
+ objectStore: stack.objectStore ? "s3" : "fs",
155
214
  });
156
215
  const infra = createInfra(stack, { logger });
157
216
  const database = infra.database;
@@ -206,15 +265,31 @@ export async function main() {
206
265
  };
207
266
  // With the gates service the Tenant's vault-backed model calls, remote MCP calls and Action
208
267
  // deliveries cross it, and this process never reads a model or MCP credential.
209
- const modelGate = stack.modelGate
210
- ? httpModelGate({ url: stack.modelGate.url, token: stack.modelGate.token })
268
+ // Their credentials (F5): each advance's run token for its session's model and MCP calls,
269
+ // core's credential (NYLORUN_GATES_TOKEN) for deliveries and MCP requests outside a run.
270
+ const runGrants = stack.modelGate ? createRunGrants() : undefined;
271
+ const modelGate = stack.modelGate && runGrants
272
+ ? httpModelGate({ url: stack.modelGate.url, runTokens: runGrants })
211
273
  : undefined;
212
- const toolGate = stack.modelGate
213
- ? httpToolGate({ url: stack.modelGate.url, token: stack.modelGate.token })
274
+ const toolGate = stack.modelGate && runGrants
275
+ ? httpToolGate({ url: stack.modelGate.url, token: stack.modelGate.token, runTokens: runGrants })
214
276
  : undefined;
215
277
  // With the keys service, vault writes and token signing cross it, and this process never
216
278
  // reads the vault key (F4.2).
217
279
  const keys = stack.keys ? httpKeys({ url: stack.keys.url, token: stack.keys.token }) : undefined;
280
+ // The Object store (D35): the stack's S3 server through the BlobStore seam, handed to the
281
+ // Tenant (`TenantContext.blobs`). Without one the Tenant keeps blobs on disk. core creates
282
+ // the bucket.
283
+ const blobs = stack.objectStore ? objectStore(stack.objectStore) : undefined;
284
+ if (blobs && stack.services.has("core"))
285
+ void ensureBucket(blobs, stack.objectStore.bucket, logger);
286
+ // Sandbox pods (F7.2): the sandboxes service, which alone holds the cluster's credentials.
287
+ const pods = stack.sandboxes
288
+ ? {
289
+ client: httpSandboxesClient({ url: stack.sandboxes.url, token: stack.sandboxes.token }),
290
+ harnessImage: stack.sandboxes.harnessImage,
291
+ }
292
+ : undefined;
218
293
  const tenantSettings = stack.tenant ?? { name: "default", derivedPrincipals: ["project"] };
219
294
  const module = createTenantModule({
220
295
  open: createPostgresTenantOpener({
@@ -234,8 +309,12 @@ export async function main() {
234
309
  execution: hostExecution.tenantExecution,
235
310
  ...(modelGate ? { modelGate } : {}),
236
311
  ...(toolGate ? { toolGate } : {}),
312
+ ...(runGrants ? { runGrants } : {}),
237
313
  ...(keys ? { keys } : {}),
238
314
  ...(streams ? { streams, hostRelay: true } : {}),
315
+ ...(blobs ? { blobs } : {}),
316
+ ...(stack.harnessMode === "remote" ? { harness: "remote" } : {}),
317
+ ...(pods ? { pods } : {}),
239
318
  ...opened,
240
319
  }),
241
320
  }),
@@ -273,8 +352,12 @@ export async function main() {
273
352
  beforeTenants: async () => {
274
353
  await hostExecution.stop();
275
354
  await relay?.stop();
355
+ // The harnesses' runs stop with the Tenant's advances; their connections close after.
356
+ },
357
+ afterTenants: async () => {
358
+ await harnessListener?.close();
359
+ await infra.close();
276
360
  },
277
- afterTenants: () => infra.close(),
278
361
  },
279
362
  };
280
363
  const host = createHost(options);
@@ -289,11 +372,35 @@ export async function main() {
289
372
  // Restate's state (§14.8), and starts the stream relay. A Tenant that cannot
290
373
  // be opened leaves the Host listening but not ready, with the cause in
291
374
  // `/v1/admin/status`.
375
+ // The Harness API listener (NYLORUN_HARNESS=remote): only harnesses connect here, with the
376
+ // harness credential, and only while the Tenant is open.
377
+ let harnessListener;
292
378
  try {
293
379
  await hostExecution.start();
380
+ if (stack.harnessListener)
381
+ harnessListener = await startHarnessListener({
382
+ ...stack.harnessListener.listen,
383
+ ...(stack.harnessListener.token ? { token: stack.harnessListener.token } : {}),
384
+ // Sandbox pods join here and connect with host tokens (F7.2).
385
+ ...(pods
386
+ ? {
387
+ hosts: async () => {
388
+ const resolved = await module.resolve();
389
+ return resolved.kind === "open" ? resolved.handle.hostAuthority?.() : undefined;
390
+ },
391
+ }
392
+ : {}),
393
+ attach: async () => {
394
+ const resolved = await module.resolve();
395
+ const handle = resolved.kind === "open" ? resolved.handle : undefined;
396
+ return handle?.attachHarness ? (channel, peer) => handle.attachHarness(channel, peer) : undefined;
397
+ },
398
+ logger,
399
+ });
294
400
  await host.listen();
295
401
  }
296
402
  catch (error) {
403
+ await harnessListener?.close().catch(() => undefined);
297
404
  await hostExecution.stop().catch(() => undefined);
298
405
  await module.close().catch(() => undefined);
299
406
  await relay?.stop().catch(() => undefined);
@@ -311,6 +418,7 @@ export async function main() {
311
418
  const tenant = module.tenant();
312
419
  logger.info("host_ready", {
313
420
  url: host.url,
421
+ ...(harnessListener ? { harness: harnessListener.url } : {}),
314
422
  hostId: config.hostId,
315
423
  tenantId: tenant.id,
316
424
  tenant: tenant.state,
@@ -1,9 +1,16 @@
1
+ import { type BlockedAddress } from "../sandbox/pods/network-gate.js";
1
2
  /** A Runtime service this release has. */
2
- export type RuntimeService = "core" | "loop" | "gates" | "keys";
3
+ export type RuntimeService = "core" | "loop" | "gates" | "keys" | "egress" | "harness";
3
4
  export type RuntimeServices = ReadonlySet<RuntimeService>;
4
5
  export declare const RUNTIME_SERVICES: readonly RuntimeService[];
5
6
  /** Where the gates service listens by default. */
6
7
  export declare const DEFAULT_GATES_LISTEN_PORT = 4100;
8
+ /** Where egress-gate listens by default. */
9
+ export declare const DEFAULT_EGRESS_LISTEN_PORT = 4200;
10
+ /** Where core's Harness API listener listens by default. */
11
+ export declare const DEFAULT_HARNESS_LISTEN_PORT = 4200;
12
+ /** Where a harness process answers `/health` (loopback only). */
13
+ export declare const DEFAULT_HARNESS_HEALTH_PORT = 4300;
7
14
  /** What a process runs without `--service`: core and loop, as `--role all` did. */
8
15
  export declare const DEFAULT_SERVICES: RuntimeServices;
9
16
  /** The deprecated `--role` values, and the services each stands for. */
@@ -48,16 +55,90 @@ export interface GatesConfig {
48
55
  /** `NYLORUN_GATES_TOKEN`: the bearer the loop presents; at least 32 bytes as hex. */
49
56
  token: string;
50
57
  }
58
+ /** egress-gate's listener (`NYLORUN_EGRESS_LISTEN_HOST`, `NYLORUN_EGRESS_LISTEN_PORT`). */
59
+ export interface EgressConfig {
60
+ listen: {
61
+ host: string;
62
+ port: number;
63
+ };
64
+ }
51
65
  /** Where the loop reaches the gates service (`NYLORUN_GATES_URL`, `NYLORUN_GATES_TOKEN`). */
52
66
  export interface ModelGateEndpoint {
53
67
  url: string;
54
68
  token: string;
55
69
  }
70
+ /**
71
+ * Core's Harness API listener (`NYLORUN_HARNESS=remote`, or sandbox pods): where harnesses
72
+ * connect, and the credential they present (`NYLORUN_HARNESS_TOKEN`), which nothing else
73
+ * accepts. With sandbox pods and no harness container there is no token: only pods' host tokens
74
+ * are accepted (F7.2).
75
+ */
76
+ export interface HarnessListenConfig {
77
+ listen: ContainerListen;
78
+ token?: string;
79
+ }
80
+ /**
81
+ * Sandbox pods (`NYLORUN_SANDBOXES_URL`, F7.2): the sandboxes service core drives them with, its
82
+ * token, and the Runtime image pods copy the engine from.
83
+ */
84
+ export interface SandboxesConfig {
85
+ url: string;
86
+ token: string;
87
+ /** `NYLORUN_SANDBOX_HARNESS_IMAGE` (the stack sets it to the Runtime image). */
88
+ harnessImage: string;
89
+ }
90
+ /**
91
+ * The engine in a pod sandbox (`--service harness` with `NYLORUN_SANDBOX_KIND=pod`, F7.2): no
92
+ * harness token; it joins with the join token mounted in the pod.
93
+ */
94
+ export interface PodHostConfig {
95
+ /** `NYLORUN_SANDBOX_ID`. */
96
+ sandboxId: string;
97
+ /** `NYLORUN_POD_UID` (the downward API's `metadata.uid`). */
98
+ podUid: string;
99
+ /** `NYLORUN_SANDBOX_JOIN_FILE`. */
100
+ joinFile: string;
101
+ /** The Harness API listener's HTTP base, for `host/join` and `host/renew`. */
102
+ httpUrl: string;
103
+ /** `NYLORUN_EGRESS_PROXY`: egress-gate's address, or undefined while it is not published. */
104
+ egressProxy?: string;
105
+ /** Addresses the pod's NetworkPolicy must block, probed before the engine starts. */
106
+ blocked: BlockedAddress[];
107
+ }
108
+ /** A harness process (`--service harness`). */
109
+ export interface HarnessServiceConfig {
110
+ /** `NYLORUN_HARNESS_URL`: core's Harness API, e.g. `ws://runtime:4200/nylorun/harness/v1`. */
111
+ url: string;
112
+ /** `NYLORUN_HARNESS_TOKEN`; absent for a pod's engine, which joins (`pod`). */
113
+ token?: string;
114
+ /** Present for the engine in a pod sandbox. */
115
+ pod?: PodHostConfig;
116
+ /** `NYLORUN_GATES_URL`: the gates service its model and MCP calls cross, with run tokens. */
117
+ gatesUrl: string;
118
+ /** `NYLORUN_HARNESS_ROOT`: its sandboxes, plugin data, home and tmp. Default `/harness`. */
119
+ root: string;
120
+ /** `/health` on `127.0.0.1:<NYLORUN_HARNESS_HEALTH_PORT>` (default 4300). */
121
+ healthPort: number;
122
+ }
56
123
  export interface StackConfig {
57
124
  /** The Runtime services this process runs. */
58
125
  services: RuntimeServices;
59
126
  /** Present when the process runs gates. */
60
127
  gates?: GatesConfig;
128
+ /** Present when the process runs egress. */
129
+ egress?: EgressConfig;
130
+ /** Present when the process runs the harness service (`--service harness`). */
131
+ harness?: HarnessServiceConfig;
132
+ /**
133
+ * `NYLORUN_HARNESS` (`in-process` or `remote`; absent means `in-process`), for a process running
134
+ * core or loop: whether the Tenant runs its own harness, or harnesses connect to
135
+ * `harnessListener`.
136
+ */
137
+ harnessMode?: "in-process" | "remote";
138
+ /** Present when `harnessMode` is `remote`, or with `sandboxes`: core's Harness API listener. */
139
+ harnessListener?: HarnessListenConfig;
140
+ /** Present when the process runs core or loop and `NYLORUN_SANDBOXES_URL` is set (F7.2). */
141
+ sandboxes?: SandboxesConfig;
61
142
  /**
62
143
  * Present when the process runs core or loop and `NYLORUN_GATES_URL` is set: its model calls,
63
144
  * remote MCP calls and Action deliveries cross the gates service. Required for loop in
@@ -110,6 +191,25 @@ export interface StackConfig {
110
191
  };
111
192
  /** Present when the process runs core or loop: who its Tenant is on first start. */
112
193
  tenant?: TenantSettings;
194
+ /**
195
+ * The Object store (blueprint D35) behind the `BlobStore` seam's `s3` adapter, from
196
+ * `NYLORUN_OBJECT_STORE_*`; the local stack's RustFS. Absent: the Tenant keeps blobs on disk
197
+ * (the `fs` adapter, `TenantPaths.blobs`).
198
+ */
199
+ objectStore?: ObjectStoreConfig;
200
+ }
201
+ /** Where the Object store is and the credential for it (`NYLORUN_OBJECT_STORE_*`). */
202
+ export interface ObjectStoreConfig {
203
+ /** `NYLORUN_OBJECT_STORE_ENDPOINT`: the S3 endpoint, e.g. `http://rustfs:9000`. */
204
+ endpoint: string;
205
+ /** `NYLORUN_OBJECT_STORE_BUCKET`. Default `nylorun`. */
206
+ bucket: string;
207
+ /** `NYLORUN_OBJECT_STORE_REGION`. Default `us-east-1`. */
208
+ region: string;
209
+ /** `NYLORUN_OBJECT_STORE_ACCESS_KEY`. */
210
+ accessKeyId: string;
211
+ /** `NYLORUN_OBJECT_STORE_SECRET_KEY`. */
212
+ secretAccessKey: string;
113
213
  }
114
214
  /**
115
215
  * The Tenant a Host creates when its database holds none (tenancy.md §4). Later starts open the