@nylorun/runtime 0.17.0-beta → 0.19.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 (223) hide show
  1. package/CHANGELOG.md +149 -0
  2. package/README.md +88 -58
  3. package/dist/adapters/execution/restate.d.ts +1 -3
  4. package/dist/adapters/execution/restate.js +4 -31
  5. package/dist/adapters/replication/pgoutput.js +1 -1
  6. package/dist/adapters/sandbox/local.js +1 -1
  7. package/dist/adapters/sandbox/virtual.js +1 -1
  8. package/dist/api/ag-ui/routes.js +1 -1
  9. package/dist/api/ag-ui/translate.d.ts +1 -3
  10. package/dist/api/ag-ui/translate.js +2 -17
  11. package/dist/api/components.d.ts +4871 -2614
  12. package/dist/api/components.js +12 -11
  13. package/dist/api/gate/routes.d.ts +7 -8
  14. package/dist/api/gate/routes.js +58 -52
  15. package/dist/api/http/app.d.ts +6 -0
  16. package/dist/api/http/app.js +18 -14
  17. package/dist/api/http/define.d.ts +14 -2
  18. package/dist/api/http/define.js +34 -15
  19. package/dist/api/http/routes/access.d.ts +3 -3
  20. package/dist/api/http/routes/access.js +18 -17
  21. package/dist/api/http/routes/artifacts.js +2 -4
  22. package/dist/api/http/routes/files.d.ts +8 -0
  23. package/dist/api/http/routes/files.js +77 -0
  24. package/dist/api/http/routes/keys.d.ts +10 -0
  25. package/dist/api/http/routes/keys.js +64 -0
  26. package/dist/api/http/routes/me.js +5 -2
  27. package/dist/api/http/routes/reads.d.ts +66 -0
  28. package/dist/api/http/routes/reads.js +127 -0
  29. package/dist/api/http/routes/sandboxes.js +7 -4
  30. package/dist/api/http/routes/sessions.js +25 -8
  31. package/dist/api/http/routes/tenant.js +15 -18
  32. package/dist/api/http/routes/vaults.d.ts +10 -9
  33. package/dist/api/http/routes/vaults.js +27 -23
  34. package/dist/api/openapi.d.ts +10 -11
  35. package/dist/api/openapi.js +291 -61
  36. package/dist/artifacts/links.js +1 -1
  37. package/dist/core/flow-host.d.ts +44 -23
  38. package/dist/core/flow-host.js +88 -106
  39. package/dist/core/provider.d.ts +4 -1
  40. package/dist/core/provider.js +6 -1
  41. package/dist/core/sandbox-routes.d.ts +0 -9
  42. package/dist/core/sandbox-routes.js +2 -21
  43. package/dist/execution/memory.d.ts +4 -5
  44. package/dist/execution/memory.js +11 -25
  45. package/dist/execution/types.d.ts +3 -33
  46. package/dist/execution/types.js +2 -12
  47. package/dist/gates/http-tool.d.ts +86 -0
  48. package/dist/gates/http-tool.js +197 -0
  49. package/dist/gates/mcp-handler.d.ts +3 -0
  50. package/dist/gates/mcp-handler.js +1 -3
  51. package/dist/gates/meter.d.ts +2 -2
  52. package/dist/gates/meter.js +6 -0
  53. package/dist/gates/tenant-vaults.d.ts +5 -9
  54. package/dist/gates/tool-calls.d.ts +7 -15
  55. package/dist/gates/tool-calls.js +4 -4
  56. package/dist/gates/tool-client.d.ts +1 -1
  57. package/dist/gates/tool-client.js +23 -47
  58. package/dist/gates/tool-contract.d.ts +27 -39
  59. package/dist/gates/tool-contract.js +30 -21
  60. package/dist/gates/tool-gate.d.ts +25 -24
  61. package/dist/gates/tool-gate.js +11 -7
  62. package/dist/harness/calls.d.ts +10 -4
  63. package/dist/harness/calls.js +15 -6
  64. package/dist/harness/executors.d.ts +1 -1
  65. package/dist/harness/executors.js +17 -12
  66. package/dist/harness/main.d.ts +2 -2
  67. package/dist/harness/main.js +5 -13
  68. package/dist/harness/service.d.ts +4 -5
  69. package/dist/harness/service.js +9 -2
  70. package/dist/harness-api/in-process.js +2 -2
  71. package/dist/harness-api/record.d.ts +6 -3
  72. package/dist/harness-api/record.js +65 -72
  73. package/dist/harness-api/server.d.ts +1 -9
  74. package/dist/harness-api/server.js +19 -17
  75. package/dist/harness-api/start.js +1 -5
  76. package/dist/harness-api/workspace.js +2 -3
  77. package/dist/harness-api/ws-server.js +1 -1
  78. package/dist/host/app.d.ts +2 -9
  79. package/dist/host/app.js +15 -25
  80. package/dist/host/config-for.d.ts +1 -5
  81. package/dist/host/config-for.js +0 -3
  82. package/dist/host/config.d.ts +0 -5
  83. package/dist/host/create-host.d.ts +3 -33
  84. package/dist/host/create-host.js +18 -94
  85. package/dist/host/environment.d.ts +0 -3
  86. package/dist/host/environment.js +0 -9
  87. package/dist/host/gates.d.ts +4 -1
  88. package/dist/host/gates.js +3 -2
  89. package/dist/host/http.d.ts +1 -2
  90. package/dist/host/http.js +1 -7
  91. package/dist/host/index.d.ts +2 -2
  92. package/dist/host/index.js +2 -2
  93. package/dist/host/main.js +27 -34
  94. package/dist/host/operate.d.ts +29 -0
  95. package/dist/host/operate.js +223 -0
  96. package/dist/host/stack-config.d.ts +12 -12
  97. package/dist/host/stack-config.js +5 -32
  98. package/dist/index.d.ts +1 -1
  99. package/dist/index.js +1 -1
  100. package/dist/infra/workers.d.ts +1 -1
  101. package/dist/keys/keys.js +2 -2
  102. package/dist/management-openapi.json +6491 -0
  103. package/dist/mcp/connect.d.ts +8 -4
  104. package/dist/mcp/connect.js +6 -26
  105. package/dist/mcp/pool.d.ts +5 -9
  106. package/dist/mcp/pool.js +8 -19
  107. package/dist/mcp/snapshot.d.ts +8 -2
  108. package/dist/mcp/snapshot.js +29 -2
  109. package/dist/media.d.ts +1 -1
  110. package/dist/media.js +1 -1
  111. package/dist/model/pi-model.js +3 -0
  112. package/dist/openapi.json +4700 -12350
  113. package/dist/reads/access.d.ts +4 -0
  114. package/dist/reads/access.js +12 -0
  115. package/dist/reads/cursor.d.ts +11 -0
  116. package/dist/reads/cursor.js +80 -0
  117. package/dist/reads/types.d.ts +25 -0
  118. package/dist/reads/types.js +1 -0
  119. package/dist/sandbox/manager.d.ts +16 -0
  120. package/dist/sandbox/manager.js +40 -0
  121. package/dist/sandbox/session-sandbox.js +13 -3
  122. package/dist/sandbox/skills.d.ts +13 -0
  123. package/dist/sandbox/skills.js +46 -0
  124. package/dist/sandbox/types.d.ts +2 -2
  125. package/dist/store/ownership.d.ts +9 -0
  126. package/dist/store/ownership.js +13 -0
  127. package/dist/store/postgres/connect.d.ts +2 -0
  128. package/dist/store/postgres/connect.js +29 -0
  129. package/dist/store/postgres/drizzle/0011_key_roles.sql +3 -0
  130. package/dist/store/postgres/drizzle/0012_session_reads.sql +8 -0
  131. package/dist/store/postgres/drizzle/0013_definition_files.sql +16 -0
  132. package/dist/store/postgres/drizzle/0014_action_endpoints_removed.sql +2 -0
  133. package/dist/store/postgres/drizzle/meta/_journal.json +28 -0
  134. package/dist/store/postgres/reads.d.ts +3 -0
  135. package/dist/store/postgres/reads.js +272 -0
  136. package/dist/store/postgres/schema.d.ts +294 -489
  137. package/dist/store/postgres/schema.js +41 -76
  138. package/dist/store/postgres/store.js +51 -154
  139. package/dist/store/postgres/tenant.d.ts +34 -2
  140. package/dist/store/postgres/tenant.js +16 -32
  141. package/dist/store/types.d.ts +27 -96
  142. package/dist/store/types.js +0 -1
  143. package/dist/streams/control.d.ts +0 -2
  144. package/dist/streams/control.js +0 -5
  145. package/dist/streams/types.d.ts +1 -10
  146. package/dist/tenant/advance.d.ts +3 -3
  147. package/dist/tenant/advance.js +10 -7
  148. package/dist/tenant/auth.d.ts +14 -9
  149. package/dist/tenant/auth.js +24 -28
  150. package/dist/tenant/cause.d.ts +3 -3
  151. package/dist/tenant/cause.js +1 -1
  152. package/dist/tenant/commands.d.ts +2 -18
  153. package/dist/tenant/commands.js +35 -172
  154. package/dist/tenant/context.d.ts +14 -24
  155. package/dist/tenant/definition-files.d.ts +24 -0
  156. package/dist/tenant/definition-files.js +103 -0
  157. package/dist/tenant/effects.d.ts +18 -20
  158. package/dist/tenant/effects.js +56 -119
  159. package/dist/tenant/ephemeral.d.ts +7 -6
  160. package/dist/tenant/ephemeral.js +3 -11
  161. package/dist/tenant/host-token.js +1 -1
  162. package/dist/tenant/jwt.js +1 -1
  163. package/dist/tenant/module.d.ts +1 -4
  164. package/dist/tenant/module.js +3 -12
  165. package/dist/tenant/operator-keys.d.ts +34 -8
  166. package/dist/tenant/operator-keys.js +50 -18
  167. package/dist/tenant/outbound.d.ts +10 -1
  168. package/dist/tenant/outbound.js +44 -27
  169. package/dist/tenant/paths.d.ts +1 -1
  170. package/dist/tenant/paths.js +1 -3
  171. package/dist/tenant/principals.d.ts +11 -2
  172. package/dist/tenant/principals.js +27 -8
  173. package/dist/tenant/run-token.js +3 -3
  174. package/dist/tenant/runtime.d.ts +4 -6
  175. package/dist/tenant/runtime.js +19 -18
  176. package/dist/tenant/sandboxes.d.ts +4 -1
  177. package/dist/tenant/sandboxes.js +11 -0
  178. package/dist/tenant/scheduler.d.ts +1 -5
  179. package/dist/tenant/scheduler.js +2 -5
  180. package/dist/tenant/session-streams.d.ts +2 -1
  181. package/dist/tenant/session-streams.js +39 -5
  182. package/dist/tenant/session.d.ts +8 -41
  183. package/dist/tenant/session.js +0 -37
  184. package/dist/tenant/sessions.js +16 -8
  185. package/dist/tenant/signing-keys.d.ts +3 -3
  186. package/dist/tenant/signing-keys.js +5 -5
  187. package/dist/tenant/skill-tool.d.ts +20 -0
  188. package/dist/tenant/skill-tool.js +72 -0
  189. package/dist/tenant/slim.d.ts +1 -1
  190. package/dist/tenant/slim.js +1 -1
  191. package/dist/tenant/status.js +7 -12
  192. package/dist/tenant/store-pg.d.ts +15 -0
  193. package/dist/tenant/store-pg.js +7 -3
  194. package/dist/tenant/streams.d.ts +3 -10
  195. package/dist/tenant/streams.js +1 -24
  196. package/dist/tenant/sweep.js +0 -5
  197. package/dist/tenant/transcript.d.ts +2 -2
  198. package/dist/tenant/transcript.js +2 -2
  199. package/dist/tenant/types.d.ts +15 -22
  200. package/dist/tenant/worker.d.ts +1 -3
  201. package/dist/tenant/worker.js +3 -8
  202. package/dist/vault/sources.d.ts +7 -1
  203. package/dist/vault/sources.js +22 -11
  204. package/dist/version.d.ts +1 -1
  205. package/dist/version.js +1 -1
  206. package/package.json +4 -4
  207. package/dist/admin-openapi.json +0 -904
  208. package/dist/api/http/routes/actions.d.ts +0 -8
  209. package/dist/api/http/routes/actions.js +0 -75
  210. package/dist/api/http/routes/endpoints.d.ts +0 -7
  211. package/dist/api/http/routes/endpoints.js +0 -100
  212. package/dist/host/admin-api.d.ts +0 -30
  213. package/dist/host/admin-api.js +0 -140
  214. package/dist/mcp/stdio.d.ts +0 -17
  215. package/dist/mcp/stdio.js +0 -82
  216. package/dist/tenant/actions.d.ts +0 -14
  217. package/dist/tenant/actions.js +0 -29
  218. package/dist/tenant/delivery-token.d.ts +0 -41
  219. package/dist/tenant/delivery-token.js +0 -104
  220. package/dist/tenant/delivery.d.ts +0 -62
  221. package/dist/tenant/delivery.js +0 -481
  222. package/dist/tenant/endpoints.d.ts +0 -67
  223. package/dist/tenant/endpoints.js +0 -90
@@ -1,16 +1,18 @@
1
1
  /**
2
- * The Runtime's requests to Action endpoints (design: Action endpoints §8.3): one POST, no
3
- * redirects, a bounded answer, and the Host's address policy checked on the address actually
4
- * connected to, so a DNS answer cannot steer a delivery somewhere the Host forbids.
2
+ * The Runtime's requests to developer URLs: HTTP tools (`gates/http-tool.ts`) and, through
3
+ * `guardedFetch`, remote MCP servers and OAuth: one request, no redirects, a bounded answer, and
4
+ * the Host's address policy checked on the address actually connected to, so a DNS answer cannot
5
+ * steer a request somewhere the Host forbids.
5
6
  *
6
- * The result says whether anything reached the endpoint. A request that failed before its body
7
- * was flushed (refused connection, unknown host, TLS failure, a refused address) was `not_sent`
8
- * and is safe to send again, even for a tool; one that failed after (reset, timeout) is `lost`.
7
+ * The result says whether anything reached the URL. A request that failed before its body was
8
+ * flushed (refused connection, unknown host, TLS failure, a refused address) was `not_sent`;
9
+ * one that failed after (reset, timeout) is `lost`.
9
10
  */
10
11
  import { lookup as dnsLookup } from "node:dns";
11
12
  import { request as httpRequest } from "node:http";
12
13
  import { request as httpsRequest } from "node:https";
13
14
  import { BlockList, isIP } from "node:net";
15
+ import { Readable } from "node:stream";
14
16
  /** The largest answer read from an endpoint. */
15
17
  export const MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
16
18
  const PRIVATE = new BlockList();
@@ -73,7 +75,10 @@ function guardedLookup(policy) {
73
75
  return callback(null, allowed[0].address, allowed[0].family);
74
76
  });
75
77
  }
76
- /** POSTs `body` to `url` under `policy`. Never throws; `signal` aborts the request. */
78
+ /**
79
+ * POSTs (or, with `method`, PUTs or PATCHes) the JSON `body` to `url` under `policy`. Never
80
+ * throws; `signal` aborts the request.
81
+ */
77
82
  export async function post(target, body, headers, options) {
78
83
  const { policy, signal } = options;
79
84
  const limit = options.maxResponseBytes ?? MAX_RESPONSE_BYTES;
@@ -96,10 +101,10 @@ export async function post(target, body, headers, options) {
96
101
  const failed = (error) => finish({
97
102
  kind: sent ? "lost" : "not_sent",
98
103
  code: signal.aborted ? "ABORTED" : (error.code ?? error.name),
99
- message: signal.aborted ? "The delivery was aborted" : error.message,
104
+ message: signal.aborted ? "The request was aborted" : error.message,
100
105
  });
101
106
  const request = (url.protocol === "https:" ? httpsRequest : httpRequest)(url, {
102
- method: "POST",
107
+ method: options.method ?? "POST",
103
108
  headers: {
104
109
  ...headers,
105
110
  host: originalHost,
@@ -165,10 +170,15 @@ export const FETCH_TIMEOUT_MS = 30_000;
165
170
  * unless the caller passes a signal. The gateway calls OAuth discovery, registration, the code
166
171
  * exchange and refresh with it. A refused URL or address rejects with `OutboundRefused`, any
167
172
  * other failure with `OutboundFailed`.
173
+ *
174
+ * `stream`: the Response resolves once the headers arrive and its body streams, unbounded and
175
+ * with no timeout but the caller's signal: remote MCP servers, whose answers and event streams
176
+ * stay open (`mcp/connect.ts`).
168
177
  */
169
178
  export function guardedFetch(policy, options = {}) {
170
179
  const limit = options.maxResponseBytes ?? MAX_FETCH_RESPONSE_BYTES;
171
180
  const timeoutMs = options.timeoutMs ?? FETCH_TIMEOUT_MS;
181
+ const stream = options.stream === true;
172
182
  const lookup = guardedLookup(policy);
173
183
  const guarded = async (input, init = {}) => {
174
184
  const request = input instanceof Request ? input : undefined;
@@ -199,7 +209,7 @@ export function guardedFetch(policy, options = {}) {
199
209
  }
200
210
  const originalHost = url.host;
201
211
  dockerHost(url, policy);
202
- const signal = init.signal ?? request?.signal ?? AbortSignal.timeout(timeoutMs);
212
+ const signal = init.signal ?? request?.signal ?? (stream ? undefined : AbortSignal.timeout(timeoutMs));
203
213
  const outgoing = { host: originalHost };
204
214
  for (const [name, value] of headers)
205
215
  outgoing[name] = value;
@@ -215,13 +225,34 @@ export function guardedFetch(policy, options = {}) {
215
225
  };
216
226
  const failed = (error) => settle(() => reject(error instanceof RefusedAddress
217
227
  ? new OutboundRefused(error.message)
218
- : new OutboundFailed(signal.aborted ? `The request to ${originalHost} was aborted or timed out` : error.message, signal.aborted ? "ABORTED" : (error.code ?? error.name))));
219
- const sent = (url.protocol === "https:" ? httpsRequest : httpRequest)(url, { method, headers: outgoing, lookup: lookup, signal }, (response) => {
228
+ : new OutboundFailed(signal?.aborted ? `The request to ${originalHost} was aborted or timed out` : error.message, signal?.aborted ? "ABORTED" : (error.code ?? error.name))));
229
+ const sent = (url.protocol === "https:" ? httpsRequest : httpRequest)(url, { method, headers: outgoing, lookup: lookup, ...(signal ? { signal } : {}) }, (response) => {
220
230
  const status = response.statusCode ?? 0;
221
231
  if (status >= 300 && status < 400 && response.headers.location !== undefined) {
222
232
  response.resume();
223
233
  return settle(() => reject(new OutboundFailed(`${originalHost} answered a redirect, which this Runtime does not follow`, "REDIRECT")));
224
234
  }
235
+ const noBody = status === 204 || status === 304 || method === "HEAD";
236
+ const answer = (body) => {
237
+ const headers = new Headers();
238
+ for (const [name, value] of Object.entries(response.headers)) {
239
+ if (value === undefined)
240
+ continue;
241
+ for (const item of Array.isArray(value) ? value : [value])
242
+ headers.append(name, item);
243
+ }
244
+ return new Response(noBody ? null : body, {
245
+ status,
246
+ statusText: response.statusMessage ?? "",
247
+ headers,
248
+ });
249
+ };
250
+ if (stream) {
251
+ // A failure after this reaches the reader as the body's error.
252
+ if (noBody)
253
+ response.resume();
254
+ return settle(() => resolve(answer(noBody ? null : Readable.toWeb(response))));
255
+ }
225
256
  const chunks = [];
226
257
  let size = 0;
227
258
  response.on("data", (chunk) => {
@@ -233,21 +264,7 @@ export function guardedFetch(policy, options = {}) {
233
264
  }
234
265
  chunks.push(chunk);
235
266
  });
236
- response.on("end", () => settle(() => {
237
- const answer = new Headers();
238
- for (const [name, value] of Object.entries(response.headers)) {
239
- if (value === undefined)
240
- continue;
241
- for (const item of Array.isArray(value) ? value : [value])
242
- answer.append(name, item);
243
- }
244
- const noBody = status === 204 || status === 304 || method === "HEAD";
245
- resolve(new Response(noBody ? null : Buffer.concat(chunks), {
246
- status,
247
- statusText: response.statusMessage ?? "",
248
- headers: answer,
249
- }));
250
- }));
267
+ response.on("end", () => settle(() => resolve(answer(Buffer.concat(chunks)))));
251
268
  response.on("error", failed);
252
269
  response.on("aborted", () => failed(Object.assign(new Error("The answer was cut off"), { code: "ECONNRESET" })));
253
270
  });
@@ -16,7 +16,7 @@ export declare function hostPaths(hostRoot: string): HostPaths;
16
16
  * root after realpath.
17
17
  *
18
18
  * The directory holds what stays on the Host (the Tenant's data is its database): `home/`,
19
- * `tmp/`, `sandboxes/`, `plugin-data/`, `blobs/` (the `fs` BlobStore, when the Host has no
19
+ * `sandboxes/`, `blobs/` (the `fs` BlobStore, when the Host has no
20
20
  * Object store), `logs/tenant.log`. The vault key is the one exception:
21
21
  * it lives beside the Tenant directory, in `<hostRoot>/keys/vault-kek`, which the local stack
22
22
  * mounts only into the gateway (F4.2), never into the runtime container.
@@ -24,7 +24,7 @@ export function hostPaths(hostRoot) {
24
24
  * root after realpath.
25
25
  *
26
26
  * The directory holds what stays on the Host (the Tenant's data is its database): `home/`,
27
- * `tmp/`, `sandboxes/`, `plugin-data/`, `blobs/` (the `fs` BlobStore, when the Host has no
27
+ * `sandboxes/`, `blobs/` (the `fs` BlobStore, when the Host has no
28
28
  * Object store), `logs/tenant.log`. The vault key is the one exception:
29
29
  * it lives beside the Tenant directory, in `<hostRoot>/keys/vault-kek`, which the local stack
30
30
  * mounts only into the gateway (F4.2), never into the runtime container.
@@ -39,9 +39,7 @@ export function tenantPaths(hostRoot) {
39
39
  root,
40
40
  kek: join(host, "keys", "vault-kek"),
41
41
  home: join(root, "home"),
42
- tmp: join(root, "tmp"),
43
42
  sandboxes: join(root, "sandboxes"),
44
- pluginData: join(root, "plugin-data"),
45
43
  blobs: join(root, "blobs"),
46
44
  logs: join(root, "logs"),
47
45
  log: join(root, "logs", "tenant.log"),
@@ -2,8 +2,12 @@ import type { InitialPrincipal } from "../store/postgres/tenant.js";
2
2
  export type { PrincipalRow } from "../store/types.js";
3
3
  /** Principal id of the Studio key derived from the admin key. */
4
4
  export declare const STUDIO_PRINCIPAL_ID = "studio";
5
- /** The Studio key of `tenantId`: HMAC-SHA256 of the admin key. */
6
- export declare function deriveStudioKey(adminKey: string, tenantId: string): string;
5
+ /**
6
+ * The Studio key: HMAC-SHA256 of the admin key over `nylorun/studio/v2`. It names no Tenant
7
+ * (protocol 8): Studio derives it before it can learn the Tenant's id, and the admin key is
8
+ * already one per installation.
9
+ */
10
+ export declare function deriveStudioKey(adminKey: string): string;
7
11
  /**
8
12
  * The principals a Host's Tenant is created with: `studio`, and an application principal when
9
13
  * a key is given (the ephemeral Runtime). Keys are hashed here.
@@ -16,4 +20,9 @@ export declare function hostPrincipals(options: {
16
20
  };
17
21
  /** Registers `studio` with this hash instead of the admin key's derivation. */
18
22
  studioCredentialHash?: string;
23
+ /**
24
+ * The management key of `NYLORUN_MANAGEMENT_KEY_FILE`: registered as `bootstrap`, and
25
+ * replaced at the next start when the file changed.
26
+ */
27
+ bootstrapKey?: string;
19
28
  }): (tenantId: string) => InitialPrincipal[];
@@ -2,27 +2,42 @@
2
2
  * The principals the Host gives its Tenant when it creates it (tenancy.md §4): the Studio
3
3
  * principal, whose key the admin key derives, so Studio (and an app embedding it) stores none,
4
4
  * and, for the ephemeral Runtime, one application principal with a key of its own. The Tenant
5
- * keeps only their hashes. Every other application key is an operator key
6
- * (`PUT /v1/admin/keys/{id}`, `operator-keys.ts`); no other key is derived.
5
+ * keeps only their hashes. Every other key is put by name (`PUT /v1/tenant/keys/{keyId}`,
6
+ * `nylorun-operate keys`, `operator-keys.ts`); no other key is derived.
7
7
  *
8
8
  * The derivation is the one `@nylorun/admin` computes on the client side
9
9
  * (`admin/src/derived-credentials.ts`: `deriveStudioToken`); both sides must stay byte for byte
10
10
  * the same.
11
11
  */
12
12
  import { createHmac } from "node:crypto";
13
+ import { BOOTSTRAP_KEY_ID } from "@nylorun/core/compatibility";
13
14
  import { hashToken } from "../core/bearer.js";
14
15
  /** Principal id of the Studio key derived from the admin key. */
15
16
  export const STUDIO_PRINCIPAL_ID = "studio";
16
- /** The Studio key of `tenantId`: HMAC-SHA256 of the admin key. */
17
- export function deriveStudioKey(adminKey, tenantId) {
18
- return hmac(adminKey, ["nylorun/studio/v1", tenantId]);
17
+ /**
18
+ * The Studio key: HMAC-SHA256 of the admin key over `nylorun/studio/v2`. It names no Tenant
19
+ * (protocol 8): Studio derives it before it can learn the Tenant's id, and the admin key is
20
+ * already one per installation.
21
+ */
22
+ export function deriveStudioKey(adminKey) {
23
+ return hmac(adminKey, ["nylorun/studio/v2"]);
19
24
  }
20
25
  /**
21
26
  * The principals a Host's Tenant is created with: `studio`, and an application principal when
22
27
  * a key is given (the ephemeral Runtime). Keys are hashed here.
23
28
  */
24
29
  export function hostPrincipals(options) {
25
- return (tenantId) => [
30
+ return () => [
31
+ ...(options.bootstrapKey
32
+ ? [
33
+ {
34
+ id: BOOTSTRAP_KEY_ID,
35
+ role: "management",
36
+ credentialHash: hashToken(options.bootstrapKey),
37
+ replace: true,
38
+ },
39
+ ]
40
+ : []),
26
41
  ...(options.application
27
42
  ? [
28
43
  {
@@ -33,8 +48,12 @@ export function hostPrincipals(options) {
33
48
  : []),
34
49
  {
35
50
  id: STUDIO_PRINCIPAL_ID,
36
- credentialHash: options.studioCredentialHash ??
37
- hashToken(deriveStudioKey(options.adminKey, tenantId)),
51
+ // Studio runs sessions and edits the Tenant's settings: both APIs (protocol 8).
52
+ role: "studio",
53
+ credentialHash: options.studioCredentialHash ?? hashToken(deriveStudioKey(options.adminKey)),
54
+ // Its key derives from the admin key: a database holding an older derivation (v1, with
55
+ // the Tenant id) takes this one at the next start.
56
+ replace: true,
38
57
  },
39
58
  ];
40
59
  }
@@ -4,9 +4,9 @@
4
4
  * remote MCP calls. The gates take session, turn and agent from it, never from the body, and
5
5
  * check on every call that the lease it names is still the session's (`staleRun`).
6
6
  *
7
- * Signed by the keys service with the Tenant's current signing key, like subject and delivery
8
- * tokens, and told apart from them by `typ` and `aud`. Internal: not in `openapi.json`. A token
9
- * lives `RUN_TOKEN_TTL_SECONDS`, no longer than a delivery token, so a key rotation never
7
+ * Signed by the keys service with the Tenant's current signing key, like capability links,
8
+ * and told apart from them by `typ` and `aud`. Internal: not in `openapi.json`. A token lives
9
+ * `RUN_TOKEN_TTL_SECONDS`, no longer than `TOKEN_TTL_MAX_SECONDS`, so a key rotation never
10
10
  * revokes a live one (`signing-keys.ts`).
11
11
  *
12
12
  * ```text
@@ -1,4 +1,4 @@
1
- import type { TenantEnvelope } from "@nylorun/core/contracts";
1
+ import type { HarnessStatus, TenantEnvelope } from "@nylorun/core/contracts";
2
2
  import { type ModelProvider } from "../core/provider.js";
3
3
  import type { ModelGate } from "../gates/model-gate.js";
4
4
  import { type AuthorizeResult } from "../vault/service.js";
@@ -14,7 +14,6 @@ import type { HarnessChannel, MemoryPortsOptions } from "@nylorun/core/harness-a
14
14
  import { type HarnessPeer } from "../harness-api/server.js";
15
15
  import { type HostAuthority } from "../sandbox/join.js";
16
16
  import type { TenantPods } from "./context.js";
17
- import { type OperatorKeys } from "./operator-keys.js";
18
17
  /** TENANTS-CCR: test/injection hooks until TenantConfig gains them. */
19
18
  export type TenantOpenHooks = {
20
19
  modelProvider?: ModelProvider;
@@ -24,8 +23,8 @@ export type TenantOpenHooks = {
24
23
  */
25
24
  modelGate?: ModelGate;
26
25
  /**
27
- * Serves the Tenant's remote MCP servers and Action deliveries (the gates service's client).
28
- * Without one, the Tenant opens them and POSTs deliveries in this process.
26
+ * Serves the Tenant's remote MCP servers and HTTP tools (the gates service's client).
27
+ * Without one, the Tenant opens and calls them in this process.
29
28
  */
30
29
  toolGate?: ToolGate;
31
30
  /**
@@ -120,8 +119,7 @@ export declare class TenantRuntime implements TenantHandle {
120
119
  /** Adds a callback to the Tenant sweep. Returns a function that removes it. */
121
120
  onSweep(hook: () => Promise<void>): () => void;
122
121
  attachHarness(channel: HarnessChannel, peer: HarnessPeer): () => void;
123
- /** The Tenant's operator keys (F9 I1), over its store. */
124
- operatorKeys(): OperatorKeys;
122
+ harnessStatus(): HarnessStatus;
125
123
  close(): Promise<void>;
126
124
  }
127
125
  /**
@@ -18,7 +18,6 @@ import { closeStreams, streamsStatus, wireStreams, } from "./streams.js";
18
18
  import { abortAll, abortLocal, createWorkState, drain, waitForIdle, } from "./scheduler.js";
19
19
  import { advance } from "./advance.js";
20
20
  import { sweep } from "./sweep.js";
21
- import { deliverAction } from "./delivery.js";
22
21
  import { TenantWorkers, WORKER_ID, } from "./worker.js";
23
22
  import { authorize } from "./effects.js";
24
23
  import { inProcessToolGate } from "../gates/tool-gate.js";
@@ -33,7 +32,7 @@ import { localWorkspace, remoteWorkspace, withPodWorkspaces, } from "../harness-
33
32
  import { hostAuthority } from "../sandbox/join.js";
34
33
  import { reconcileSandbox } from "../sandbox/pods/reconcile.js";
35
34
  import { sandboxWorkspaceReader } from "../artifacts/workspace.js";
36
- import { operatorKeys } from "./operator-keys.js";
35
+ import { readDefinitionFile } from "./definition-files.js";
37
36
  /** Default ownership lease of an advance; the heartbeat renews it every third. */
38
37
  const DEFAULT_OWNER_LEASE_MS = 30_000;
39
38
  function validateConfig(config) {
@@ -82,9 +81,7 @@ export class TenantRuntime {
82
81
  const paths = config.paths;
83
82
  mkdirSync(paths.root, { recursive: true });
84
83
  mkdirSync(paths.home, { recursive: true });
85
- mkdirSync(paths.tmp, { recursive: true });
86
84
  mkdirSync(paths.sandboxes, { recursive: true });
87
- mkdirSync(paths.pluginData, { recursive: true });
88
85
  mkdirSync(paths.logs, { recursive: true });
89
86
  const store = hooks.store;
90
87
  let wired;
@@ -131,7 +128,12 @@ export class TenantRuntime {
131
128
  });
132
129
  // `ctx` is assigned below; these callbacks only run once the Tenant is open.
133
130
  let ctx;
134
- const toolGate = hooks.toolGate ?? inProcessToolGate(config.delivery ?? {});
131
+ const toolGate = hooks.toolGate ??
132
+ inProcessToolGate(config.delivery ?? {}, {
133
+ session: (sessionId) => opened.tx((t) => t.get("sessions", sessionId)),
134
+ authorize: (sessionId, request) => authorize(ctx, sessionId, request),
135
+ logger: config.logger,
136
+ });
135
137
  const signingKeys = new SigningKeys({ tenantId: config.tenantId, kek: ensureKek });
136
138
  const keys = hooks.keys ?? inProcessKeys({ store: opened, vault, signingKeys, kek: ensureKek });
137
139
  const harnessMode = hooks.harness ?? "memory";
@@ -141,9 +143,8 @@ export class TenantRuntime {
141
143
  // executors; otherwise the harness holds both (F6.2).
142
144
  const mcp = inProcess
143
145
  ? new McpPool({
144
- pluginData: paths.pluginData,
145
- childEnv: config.childEnv,
146
146
  authorize: (sessionId, request) => authorize(ctx, sessionId, request),
147
+ policy: config.delivery ?? {},
147
148
  ...(toolGate.openMcp ? { openRemote: (server) => toolGate.openMcp(server) } : {}),
148
149
  })
149
150
  : undefined;
@@ -156,6 +157,12 @@ export class TenantRuntime {
156
157
  defaultSandboxBackends({ root: paths.sandboxes }),
157
158
  preference,
158
159
  ephemeral,
160
+ definitionFile: async (sha256) => {
161
+ const bytes = await readDefinitionFile(blobs, sha256);
162
+ if (!bytes)
163
+ throw new Error(`The Object store has no bytes for ${sha256}`);
164
+ return bytes;
165
+ },
159
166
  emit: async (sessionId, turnId, type, payload) => {
160
167
  if (ctx.closed)
161
168
  return;
@@ -230,6 +237,7 @@ export class TenantRuntime {
230
237
  ctx = {
231
238
  config,
232
239
  envelope,
240
+ reads: hooks.reads,
233
241
  store: opened,
234
242
  vault,
235
243
  credentials,
@@ -260,11 +268,6 @@ export class TenantRuntime {
260
268
  await execution.wake(config.tenantId, sessionId, wake);
261
269
  },
262
270
  abortLocal: (sessionId, turnId) => abortLocal(ctx, sessionId, "cancel", turnId),
263
- deliver: async (actionId) => {
264
- if (ctx.closing || ctx.closed)
265
- return;
266
- await execution.deliver(config.tenantId, actionId);
267
- },
268
271
  ...(hooks.pods ? { pods: hooks.pods } : {}),
269
272
  sandboxSignal: async (sandboxId, signal) => {
270
273
  if (ctx.closing || ctx.closed || !execution.sandbox)
@@ -304,7 +307,6 @@ export class TenantRuntime {
304
307
  // sessions a previous process left runnable or running (takeover handles the rest).
305
308
  const worker = {
306
309
  advance: (sessionId, signal) => advance(ctx, sessionId, signal),
307
- deliver: (actionId, signal) => deliverAction(ctx, actionId, signal),
308
310
  sweep: () => sweep(ctx),
309
311
  sandbox: (sandboxId, trigger) => reconcileSandbox(ctx, sandboxId, trigger),
310
312
  };
@@ -322,6 +324,7 @@ export class TenantRuntime {
322
324
  await detach?.().catch(() => undefined);
323
325
  await harness?.stop().catch(() => undefined);
324
326
  await wired?.close().catch(() => undefined);
327
+ await hooks.reads?.close().catch(() => undefined);
325
328
  await store.close().catch(() => undefined);
326
329
  throw error;
327
330
  }
@@ -338,8 +341,6 @@ export class TenantRuntime {
338
341
  return {
339
342
  ready: !this.ctx.closing && !this.ctx.closed,
340
343
  runningSessions: counts.runningSessions,
341
- inFlightDeliveries: [...this.ctx.work.deliveries.values()].reduce((n, set) => n + set.size, 0),
342
- pendingActions: counts.pendingActions,
343
344
  uncertainEffects: counts.uncertainEffects,
344
345
  harness: harnessStatusOf(this.ctx),
345
346
  };
@@ -368,9 +369,8 @@ export class TenantRuntime {
368
369
  attachHarness(channel, peer) {
369
370
  return this.ctx.harness.attach(channel, peer);
370
371
  }
371
- /** The Tenant's operator keys (F9 I1), over its store. */
372
- operatorKeys() {
373
- return operatorKeys(this.ctx.store);
372
+ harnessStatus() {
373
+ return harnessStatusOf(this.ctx);
374
374
  }
375
375
  async close() {
376
376
  const ctx = this.ctx;
@@ -402,6 +402,7 @@ export class TenantRuntime {
402
402
  return closeStreams(ctx);
403
403
  },
404
404
  ],
405
+ ["reads", () => ctx.reads?.close()],
405
406
  ["store", () => ctx.store.close()],
406
407
  ];
407
408
  let failure;
@@ -30,7 +30,8 @@
30
30
  */
31
31
  import { type PutSandboxRequest, type SandboxView } from "@nylorun/core/contracts";
32
32
  import type { SandboxResource, Tx } from "../store/types.js";
33
- import type { AuthScope, Session, TenantContext } from "./context.js";
33
+ import type { SandboxRouteDeps } from "../core/sandbox-routes.js";
34
+ import { type AuthScope, type Session, type TenantContext } from "./context.js";
34
35
  /** The sandbox grants that limit `scope`, or undefined when it reaches every sandbox. */
35
36
  export declare function sandboxGrantsOf(scope: AuthScope): readonly string[] | undefined;
36
37
  /** A sandbox resource, unless it is gone or being deleted. */
@@ -117,3 +118,5 @@ export declare function touchPodSandbox(ctx: TenantContext, id: string): Promise
117
118
  * timer is set). Best effort.
118
119
  */
119
120
  export declare function rearmPodTimers(ctx: TenantContext): Promise<void>;
121
+ /** Session lookups for the sandbox route handlers (`core/sandbox-routes.ts`). */
122
+ export declare function sandboxRouteDeps(ctx: TenantContext): SandboxRouteDeps;
@@ -37,6 +37,7 @@ import { podName } from "../sandbox/pods/name.js";
37
37
  import { expiresAtOf, podLifecycleConfig } from "../sandbox/pods/spec.js";
38
38
  import { requirePods } from "../sandbox/placement.js";
39
39
  import { parseSandboxDuration } from "@nylorun/core/define";
40
+ import { sandboxLookup, sessionOf } from "./context.js";
40
41
  import { fail } from "./http.js";
41
42
  /** The sandbox grants that limit `scope`, or undefined when it reaches every sandbox. */
42
43
  export function sandboxGrantsOf(scope) {
@@ -549,3 +550,13 @@ export async function rearmPodTimers(ctx) {
549
550
  });
550
551
  }
551
552
  }
553
+ /** Session lookups for the sandbox route handlers (`core/sandbox-routes.ts`). */
554
+ export function sandboxRouteDeps(ctx) {
555
+ return {
556
+ sandbox: ctx.sandbox,
557
+ session: (id) => ctx.store.tx(async (t) => {
558
+ const session = await sessionOf(t, id);
559
+ return { session, lookup: await sandboxLookup(t, id) };
560
+ }),
561
+ };
562
+ }
@@ -5,14 +5,10 @@ export interface WorkState {
5
5
  readonly running: Map<string, AbortController>;
6
6
  /** The turn each running advance belongs to, by session id (`null`: none). */
7
7
  readonly runningTurns: Map<string, string | null>;
8
- /** Deliveries to Action endpoints in flight on this process, by their Action's session id. */
9
- readonly deliveries: Map<string, Set<AbortController>>;
10
- /** When each Action last reported a failed delivery (`action.delivery_failed` throttling). */
11
- readonly deliveryNotices: Map<string, number>;
12
8
  }
13
9
  export declare function createWorkState(): WorkState;
14
10
  /**
15
- * Abort the advance of `id` if it runs on this process, and its deliveries to Action endpoints.
11
+ * Abort the advance of `id` if it runs on this process.
16
12
  * Cancel calls it after committing `cancelled`, and so does the control stream for cancels made
17
13
  * elsewhere. With `turnId`, an advance of another turn keeps running: the signal came late.
18
14
  */
@@ -13,7 +13,7 @@ import { randomUUID } from "node:crypto";
13
13
  import { command } from "./commands.js";
14
14
  import { AdvanceAbort } from "./worker.js";
15
15
  export function createWorkState() {
16
- return { running: new Map(), runningTurns: new Map(), deliveries: new Map(), deliveryNotices: new Map() };
16
+ return { running: new Map(), runningTurns: new Map() };
17
17
  }
18
18
  const MESSAGES = {
19
19
  cancel: "Turn cancelled",
@@ -26,7 +26,7 @@ function abortWith(controller, kind) {
26
26
  controller.abort(new AdvanceAbort(kind, MESSAGES[kind]));
27
27
  }
28
28
  /**
29
- * Abort the advance of `id` if it runs on this process, and its deliveries to Action endpoints.
29
+ * Abort the advance of `id` if it runs on this process.
30
30
  * Cancel calls it after committing `cancelled`, and so does the control stream for cancels made
31
31
  * elsewhere. With `turnId`, an advance of another turn keeps running: the signal came late.
32
32
  */
@@ -34,9 +34,6 @@ export function abortLocal(ctx, id, kind = "cancel", turnId) {
34
34
  const controller = ctx.work.running.get(id);
35
35
  if (controller && (turnId === undefined || ctx.work.runningTurns.get(id) === turnId))
36
36
  abortWith(controller, kind);
37
- // A cancel also closes the session's deliveries, which aborts the tools' `ctx.signal`.
38
- for (const delivery of ctx.work.deliveries.get(id) ?? [])
39
- abortWith(delivery, kind);
40
37
  }
41
38
  /** Reset: abort every advance running on this process; their sessions are being cleared. */
42
39
  export function clearWork(ctx) {
@@ -79,9 +79,10 @@ export declare function currentBasin(ctx: TenantContext): string;
79
79
  * read starts. The cursor returned is the last record read (the request's cursor when none
80
80
  * was), whatever the `agent` filter kept.
81
81
  */
82
- export declare function readHistory(ctx: TenantContext, sessionId: string, cursor: string | undefined, agent: string | undefined): Promise<{
82
+ export declare function readHistory(ctx: TenantContext, sessionId: string, cursor: string | undefined, agent: string | undefined, limit?: number): Promise<{
83
83
  items: LiveEvent[];
84
84
  cursor: string | null;
85
+ tail?: boolean;
85
86
  }>;
86
87
  /**
87
88
  * `GET /v1/sessions/:id/events`: joins the session's shared read at the client's cursor and
@@ -1,3 +1,24 @@
1
+ /**
2
+ * Live delivery over Durable Streams (architecture §12.4, "Reading"): session history
3
+ * (`GET /v1/sessions/:id/items`) and session SSE (`GET /v1/sessions/:id/events`).
4
+ *
5
+ * - **Streams.** A session's events are in `sessions/<id>` in the Tenant's current basin
6
+ * generation (`currentBasin`); cursors carry only the session id and sequence.
7
+ * - **History** reads the session's stream from the cursor up to the tail seen when the read
8
+ * starts. The `agent` filter runs here, and the response cursor is the last record read. If
9
+ * the streams fail, history answers `503`.
10
+ * - **SSE** shares one stream read per observed session in this process (a `SessionStream`)
11
+ * among that session's observers. Each observer keeps the next sequence it needs and skips
12
+ * what it already has, so a client resuming from `Last-Event-ID` sees no gap and no
13
+ * duplicate. An observer behind the feed restarts the shared read from its own position.
14
+ * A feed follows one basin generation: it ends once its session is gone or the Tenant moved
15
+ * to a new generation (a reset, possibly on another node), checked when a client joins, when
16
+ * its read fails, on a `sessions.reset` signal, and periodically (`checkSessionStreams`).
17
+ *
18
+ * Business code never writes here: events reach streams only through the stream relay, from
19
+ * the record, after commit.
20
+ */
21
+ import { historyCursor, historyResume } from "../reads/cursor.js";
1
22
  import { decodeCursor, encodeCursor } from "../record/index.js";
2
23
  import { basinOf } from "../streams/basin.js";
3
24
  import { sessionStream, } from "../streams/types.js";
@@ -85,17 +106,27 @@ function streamsOf(ctx) {
85
106
  * read starts. The cursor returned is the last record read (the request's cursor when none
86
107
  * was), whatever the `agent` filter kept.
87
108
  */
88
- export async function readHistory(ctx, sessionId, cursor, agent) {
89
- const from = startSeq(sessionId, cursor);
109
+ export async function readHistory(ctx, sessionId, cursor, agent, limit) {
110
+ const codec = limit === undefined ? undefined : historyCursor(ctx.config.tenantId, sessionId, agent);
111
+ const from = startSeq(sessionId, codec ? codec.decode(cursor) : historyResume(ctx.config.tenantId, sessionId, cursor));
90
112
  const streams = streamsOf(ctx);
91
113
  const ref = (await currentStream(ctx, sessionId)) ?? fail(404, "Session not found");
92
114
  const items = [];
93
115
  let last;
116
+ let atTail = true;
117
+ let examined = 0;
94
118
  try {
119
+ const tail = limit === undefined ? undefined : await streams.tail(ref.basin, ref.stream);
95
120
  for await (const record of streams.read(ref.basin, ref.stream, from, { follow: false })) {
121
+ if (tail !== undefined && record.seq >= tail)
122
+ break;
96
123
  last = record.seq;
97
124
  if (served(record.body) && (agent === undefined || belongsTo(record.body, agent)))
98
125
  items.push(record.body);
126
+ if (limit !== undefined && ++examined >= limit) {
127
+ atTail = last >= tail - 1;
128
+ break;
129
+ }
99
130
  }
100
131
  }
101
132
  catch (error) {
@@ -107,7 +138,10 @@ export async function readHistory(ctx, sessionId, cursor, agent) {
107
138
  }
108
139
  return {
109
140
  items,
110
- cursor: last !== undefined ? encodeCursor(sessionId, last) : cursor ?? null,
141
+ ...(limit === undefined ? {} : { tail: atTail }),
142
+ cursor: last !== undefined
143
+ ? (codec ? codec.encode(encodeCursor(sessionId, last)) : encodeCursor(sessionId, last))
144
+ : cursor ?? null,
111
145
  };
112
146
  }
113
147
  /**
@@ -117,7 +151,7 @@ export async function readHistory(ctx, sessionId, cursor, agent) {
117
151
  export async function streamSessionEvents(ctx, request, response, sessionId, cursor, holder) {
118
152
  const observer = {
119
153
  sink: sseSink(response),
120
- next: startSeq(sessionId, cursor),
154
+ next: startSeq(sessionId, historyResume(ctx.config.tenantId, sessionId, cursor)),
121
155
  ...(holder ? { holder } : {}),
122
156
  };
123
157
  const joined = await join(ctx, sessionId, observer);
@@ -162,7 +196,7 @@ export async function* observeSession(ctx, sessionId, cursor, options) {
162
196
  notify();
163
197
  },
164
198
  },
165
- next: startSeq(sessionId, cursor),
199
+ next: startSeq(sessionId, historyResume(ctx.config.tenantId, sessionId, cursor)),
166
200
  ...(options.holder ? { holder: options.holder } : {}),
167
201
  };
168
202
  const joined = await join(ctx, sessionId, observer);