@nylorun/runtime 0.15.2-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 (219) hide show
  1. package/CHANGELOG.md +76 -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/sandbox/local.d.ts +10 -0
  6. package/dist/adapters/sandbox/local.js +195 -0
  7. package/dist/adapters/sandbox/virtual.js +36 -1
  8. package/dist/admin-openapi.json +37 -3
  9. package/dist/api/a2a/tasks.js +6 -1
  10. package/dist/api/ag-ui/history.js +6 -1
  11. package/dist/api/ag-ui/routes.js +5 -1
  12. package/dist/api/components.d.ts +2673 -655
  13. package/dist/api/components.js +17 -1
  14. package/dist/api/gate/keys.d.ts +4 -3
  15. package/dist/api/gate/routes.d.ts +19 -3
  16. package/dist/api/gate/routes.js +162 -53
  17. package/dist/api/http/app.js +4 -0
  18. package/dist/api/http/define.d.ts +10 -0
  19. package/dist/api/http/define.js +2 -1
  20. package/dist/api/http/routes/artifacts.d.ts +11 -0
  21. package/dist/api/http/routes/artifacts.js +518 -0
  22. package/dist/api/http/routes/sandboxes.d.ts +9 -0
  23. package/dist/api/http/routes/sandboxes.js +152 -0
  24. package/dist/api/http/routes/sessions.js +3 -1
  25. package/dist/api/http/routes/tenant.js +36 -2
  26. package/dist/artifacts/config.d.ts +23 -0
  27. package/dist/artifacts/config.js +27 -0
  28. package/dist/artifacts/export.d.ts +8 -0
  29. package/dist/artifacts/export.js +205 -0
  30. package/dist/artifacts/files.d.ts +36 -0
  31. package/dist/artifacts/files.js +45 -0
  32. package/dist/artifacts/folders.d.ts +60 -0
  33. package/dist/artifacts/folders.js +209 -0
  34. package/dist/artifacts/links.d.ts +28 -0
  35. package/dist/artifacts/links.js +92 -0
  36. package/dist/artifacts/media-types.d.ts +13 -0
  37. package/dist/artifacts/media-types.js +80 -0
  38. package/dist/artifacts/parts.d.ts +34 -0
  39. package/dist/artifacts/parts.js +38 -0
  40. package/dist/artifacts/service.d.ts +74 -0
  41. package/dist/artifacts/service.js +301 -0
  42. package/dist/artifacts/workspace.d.ts +60 -0
  43. package/dist/artifacts/workspace.js +57 -0
  44. package/dist/artifacts/zip.d.ts +10 -0
  45. package/dist/artifacts/zip.js +126 -0
  46. package/dist/blob/fs.d.ts +13 -0
  47. package/dist/blob/fs.js +194 -0
  48. package/dist/blob/index.d.ts +9 -0
  49. package/dist/blob/index.js +9 -0
  50. package/dist/blob/s3.d.ts +42 -0
  51. package/dist/blob/s3.js +366 -0
  52. package/dist/blob/types.d.ts +108 -0
  53. package/dist/blob/types.js +85 -0
  54. package/dist/core/sandbox-routes.d.ts +2 -2
  55. package/dist/core/sandbox-routes.js +4 -3
  56. package/dist/execution/memory.d.ts +11 -1
  57. package/dist/execution/memory.js +77 -3
  58. package/dist/execution/types.d.ts +54 -1
  59. package/dist/execution/types.js +31 -0
  60. package/dist/gates/contract.d.ts +14 -13
  61. package/dist/gates/contract.js +13 -12
  62. package/dist/gates/egress.d.ts +72 -0
  63. package/dist/gates/egress.js +319 -0
  64. package/dist/gates/handler.d.ts +3 -0
  65. package/dist/gates/handler.js +3 -2
  66. package/dist/gates/http-client.d.ts +3 -2
  67. package/dist/gates/http-client.js +17 -6
  68. package/dist/gates/in-process.d.ts +13 -1
  69. package/dist/gates/in-process.js +11 -0
  70. package/dist/gates/inflight.d.ts +23 -4
  71. package/dist/gates/inflight.js +18 -4
  72. package/dist/gates/model-gate.d.ts +6 -2
  73. package/dist/gates/tool-calls.d.ts +9 -5
  74. package/dist/gates/tool-calls.js +6 -6
  75. package/dist/gates/tool-client.d.ts +9 -2
  76. package/dist/gates/tool-client.js +56 -22
  77. package/dist/gates/tool-contract.d.ts +12 -7
  78. package/dist/gates/tool-contract.js +10 -4
  79. package/dist/gates/tool-gate.d.ts +1 -0
  80. package/dist/harness/calls.d.ts +52 -0
  81. package/dist/harness/calls.js +113 -0
  82. package/dist/harness/executors.d.ts +22 -0
  83. package/dist/harness/executors.js +96 -0
  84. package/dist/harness/main.d.ts +6 -0
  85. package/dist/harness/main.js +103 -0
  86. package/dist/harness/pod.d.ts +21 -0
  87. package/dist/harness/pod.js +119 -0
  88. package/dist/harness/run-tokens.d.ts +18 -0
  89. package/dist/harness/run-tokens.js +15 -0
  90. package/dist/harness/service.d.ts +46 -0
  91. package/dist/harness/service.js +179 -0
  92. package/dist/harness/ws-client.d.ts +43 -0
  93. package/dist/harness/ws-client.js +122 -0
  94. package/dist/harness/ws-port.d.ts +22 -0
  95. package/dist/harness/ws-port.js +78 -0
  96. package/dist/harness-api/in-process.d.ts +24 -0
  97. package/dist/harness-api/in-process.js +119 -0
  98. package/dist/harness-api/record.d.ts +44 -0
  99. package/dist/harness-api/record.js +250 -0
  100. package/dist/harness-api/renew.d.ts +37 -0
  101. package/dist/harness-api/renew.js +77 -0
  102. package/dist/harness-api/server.d.ts +104 -0
  103. package/dist/harness-api/server.js +408 -0
  104. package/dist/harness-api/start.d.ts +36 -0
  105. package/dist/harness-api/start.js +118 -0
  106. package/dist/harness-api/workspace.d.ts +53 -0
  107. package/dist/harness-api/workspace.js +201 -0
  108. package/dist/harness-api/ws-server.d.ts +34 -0
  109. package/dist/harness-api/ws-server.js +227 -0
  110. package/dist/host/app.js +23 -2
  111. package/dist/host/gates.d.ts +27 -2
  112. package/dist/host/gates.js +46 -1
  113. package/dist/host/http.d.ts +4 -1
  114. package/dist/host/http.js +6 -1
  115. package/dist/host/main.js +135 -27
  116. package/dist/host/stack-config.d.ts +101 -1
  117. package/dist/host/stack-config.js +199 -8
  118. package/dist/mcp/snapshot.d.ts +8 -1
  119. package/dist/mcp/snapshot.js +28 -0
  120. package/dist/media.d.ts +5 -24
  121. package/dist/media.js +5 -0
  122. package/dist/model/pi-model.d.ts +6 -2
  123. package/dist/model/pi-model.js +39 -22
  124. package/dist/node/index.d.ts +1 -1
  125. package/dist/node/index.js +1 -1
  126. package/dist/openapi.json +15245 -8850
  127. package/dist/record/index.d.ts +1 -0
  128. package/dist/record/index.js +1 -0
  129. package/dist/record/sandbox.d.ts +25 -0
  130. package/dist/record/sandbox.js +43 -0
  131. package/dist/sandbox/capability.d.ts +3 -0
  132. package/dist/sandbox/capability.js +10 -0
  133. package/dist/sandbox/egress-token.d.ts +26 -0
  134. package/dist/sandbox/egress-token.js +8 -0
  135. package/dist/sandbox/join.d.ts +37 -0
  136. package/dist/sandbox/join.js +165 -0
  137. package/dist/sandbox/manager.d.ts +99 -34
  138. package/dist/sandbox/manager.js +216 -84
  139. package/dist/sandbox/placement.d.ts +27 -0
  140. package/dist/sandbox/placement.js +44 -0
  141. package/dist/sandbox/pods/client.d.ts +90 -0
  142. package/dist/sandbox/pods/client.js +79 -0
  143. package/dist/sandbox/pods/lifecycle.d.ts +91 -0
  144. package/dist/sandbox/pods/lifecycle.js +178 -0
  145. package/dist/sandbox/pods/name.d.ts +1 -0
  146. package/dist/sandbox/pods/name.js +28 -0
  147. package/dist/sandbox/pods/network-gate.d.ts +25 -0
  148. package/dist/sandbox/pods/network-gate.js +71 -0
  149. package/dist/sandbox/pods/reconcile.d.ts +11 -0
  150. package/dist/sandbox/pods/reconcile.js +152 -0
  151. package/dist/sandbox/pods/spec.d.ts +45 -0
  152. package/dist/sandbox/pods/spec.js +62 -0
  153. package/dist/sandbox/records.d.ts +29 -0
  154. package/dist/sandbox/records.js +79 -0
  155. package/dist/sandbox/resolve.d.ts +5 -0
  156. package/dist/sandbox/resolve.js +3 -2
  157. package/dist/sandbox/session-sandbox.js +7 -5
  158. package/dist/sandbox/share.d.ts +11 -0
  159. package/dist/sandbox/share.js +10 -0
  160. package/dist/sandbox/tenant-config.d.ts +16 -1
  161. package/dist/sandbox/tenant-config.js +9 -0
  162. package/dist/sandbox/types.d.ts +29 -1
  163. package/dist/sandbox/types.js +13 -1
  164. package/dist/store/postgres/drizzle/0004_sandbox_resources.sql +20 -0
  165. package/dist/store/postgres/drizzle/0005_artifacts.sql +27 -0
  166. package/dist/store/postgres/drizzle/0006_folder_artifacts.sql +12 -0
  167. package/dist/store/postgres/drizzle/0007_sandbox_pods.sql +14 -0
  168. package/dist/store/postgres/drizzle/meta/_journal.json +28 -0
  169. package/dist/store/postgres/record-writer.d.ts +6 -1
  170. package/dist/store/postgres/record-writer.js +20 -2
  171. package/dist/store/postgres/schema.d.ts +894 -0
  172. package/dist/store/postgres/schema.js +130 -2
  173. package/dist/store/postgres/store.js +289 -4
  174. package/dist/store/types.d.ts +143 -6
  175. package/dist/streams/control.d.ts +2 -0
  176. package/dist/streams/control.js +5 -0
  177. package/dist/streams/types.d.ts +10 -1
  178. package/dist/tenant/advance.js +218 -160
  179. package/dist/tenant/artifact-tool.d.ts +5 -0
  180. package/dist/tenant/artifact-tool.js +119 -0
  181. package/dist/tenant/commands.js +30 -5
  182. package/dist/tenant/context.d.ts +64 -5
  183. package/dist/tenant/effects.d.ts +19 -13
  184. package/dist/tenant/effects.js +5 -382
  185. package/dist/tenant/history.d.ts +13 -23
  186. package/dist/tenant/history.js +14 -62
  187. package/dist/tenant/host-token.d.ts +92 -0
  188. package/dist/tenant/host-token.js +179 -0
  189. package/dist/tenant/module.js +1 -0
  190. package/dist/tenant/paths.d.ts +2 -1
  191. package/dist/tenant/paths.js +3 -1
  192. package/dist/tenant/reset.d.ts +5 -2
  193. package/dist/tenant/reset.js +22 -2
  194. package/dist/tenant/run-grants.d.ts +49 -0
  195. package/dist/tenant/run-grants.js +62 -0
  196. package/dist/tenant/run-token.d.ts +70 -0
  197. package/dist/tenant/run-token.js +138 -0
  198. package/dist/tenant/runtime.d.ts +38 -0
  199. package/dist/tenant/runtime.js +120 -32
  200. package/dist/tenant/sandboxes.d.ts +119 -0
  201. package/dist/tenant/sandboxes.js +551 -0
  202. package/dist/tenant/session.d.ts +2 -13
  203. package/dist/tenant/session.js +4 -28
  204. package/dist/tenant/sessions.d.ts +5 -0
  205. package/dist/tenant/sessions.js +28 -1
  206. package/dist/tenant/status.d.ts +8 -3
  207. package/dist/tenant/status.js +5 -0
  208. package/dist/tenant/streams.d.ts +7 -0
  209. package/dist/tenant/streams.js +24 -1
  210. package/dist/tenant/sweep.js +37 -4
  211. package/dist/tenant/tokens.js +9 -2
  212. package/dist/tenant/types.d.ts +18 -1
  213. package/dist/tenant/worker.d.ts +11 -5
  214. package/dist/tenant/worker.js +12 -6
  215. package/dist/version.d.ts +1 -1
  216. package/dist/version.js +1 -1
  217. package/package.json +13 -10
  218. package/dist/adapters/media.d.ts +0 -24
  219. package/dist/adapters/media.js +0 -111
@@ -2,6 +2,10 @@
2
2
  export class InflightConflict extends Error {
3
3
  name = "InflightConflict";
4
4
  }
5
+ /** A re-send under an older lease epoch than the call's: never joins (G4). */
6
+ export class InflightStale extends Error {
7
+ name = "InflightStale";
8
+ }
5
9
  export function createInflightCalls(options = {}) {
6
10
  const ttlMs = options.ttlMs ?? 30 * 60_000;
7
11
  const max = options.max ?? 10_000;
@@ -23,12 +27,19 @@ export function createInflightCalls(options = {}) {
23
27
  }
24
28
  }
25
29
  return {
26
- run(key, hash, start) {
30
+ run(key, hash, start, owner) {
27
31
  prune();
28
32
  const existing = entries.get(key);
29
33
  if (existing) {
30
34
  if (existing.hash !== hash)
31
35
  return Promise.reject(new InflightConflict(`A different request is already running under ${key}`));
36
+ if (owner && existing.owner) {
37
+ if (owner.sessionId !== existing.owner.sessionId)
38
+ return Promise.reject(new InflightConflict(`The call under ${key} is another session's`));
39
+ if (owner.epoch < existing.owner.epoch)
40
+ return Promise.reject(new InflightStale(`A newer owner of the session holds the call under ${key}`));
41
+ existing.owner = owner;
42
+ }
32
43
  return existing.promise;
33
44
  }
34
45
  const controller = new AbortController();
@@ -42,7 +53,7 @@ export function createInflightCalls(options = {}) {
42
53
  });
43
54
  // Nobody may be waiting (the client went away); a rejection is not an unhandled one.
44
55
  promise.catch(() => { });
45
- const entry = { hash, controller, promise };
56
+ const entry = { hash, controller, promise, ...(owner ? { owner } : {}) };
46
57
  entries.set(key, entry);
47
58
  return promise;
48
59
  },
@@ -50,12 +61,15 @@ export function createInflightCalls(options = {}) {
50
61
  prune();
51
62
  return entries.has(key);
52
63
  },
53
- cancel(key) {
64
+ cancel(key, sessionId) {
54
65
  const entry = entries.get(key);
55
66
  if (!entry)
56
- return;
67
+ return true;
68
+ if (sessionId !== undefined && entry.owner && entry.owner.sessionId !== sessionId)
69
+ return false;
57
70
  entries.delete(key);
58
71
  entry.controller.abort(new Error("The caller cancelled the call"));
72
+ return true;
59
73
  },
60
74
  close() {
61
75
  for (const entry of entries.values())
@@ -11,7 +11,11 @@
11
11
  */
12
12
  import type { RuntimeModelAdapter, RuntimeModelCall } from "../contracts.js";
13
13
  export type { ModelCallSettings } from "../model/pi-model.js";
14
- /** One model call, as the loop hands it to the gate. JSON-safe: it crosses the network. */
14
+ /**
15
+ * One model call, as the loop hands it to the gate. JSON-safe: it crosses the network. Over
16
+ * HTTP the session, turn and agent travel in the run token, not the body (F5): the gate's route
17
+ * fills them in from its claims.
18
+ */
15
19
  export interface ModelGateRequest {
16
20
  readonly tenantId: string;
17
21
  readonly sessionId: string;
@@ -41,5 +45,5 @@ export interface ModelGate {
41
45
  * Stops a call that outlives its caller (a user cancel). Best-effort: never rejects. Only a
42
46
  * gate that `recovers` needs it; for the others aborting `signal` already stops the call.
43
47
  */
44
- cancel?(request: Pick<ModelGateRequest, "tenantId" | "effectId">): Promise<void>;
48
+ cancel?(request: Pick<ModelGateRequest, "tenantId" | "sessionId" | "effectId">): Promise<void>;
45
49
  }
@@ -15,7 +15,7 @@
15
15
  */
16
16
  import type { McpServerRef } from "../mcp/pool.js";
17
17
  import type { Logger } from "../tenant/types.js";
18
- import { type InflightCallsOptions } from "./inflight.js";
18
+ import { type InflightCallsOptions, type InflightOwner } from "./inflight.js";
19
19
  import type { McpHandler } from "./mcp-handler.js";
20
20
  import type { TenantVaults } from "./tenant-vaults.js";
21
21
  import type { McpAnswer } from "./tool-contract.js";
@@ -31,11 +31,15 @@ export interface ToolCallRequest {
31
31
  export interface ToolCalls {
32
32
  /**
33
33
  * Runs, joins or answers the call under `key`. Rejects with `InflightConflict` when a
34
- * different request already ran under it.
34
+ * different request already ran under it, and as `InflightCalls.run` does for `owner`, the
35
+ * run that sends it (F5).
35
36
  */
36
- run(tenantId: string | undefined, key: string, hash: string, request: ToolCallRequest): Promise<ToolCallAnswer>;
37
- /** Aborts the call under `key`; it is never run again under that key. */
38
- cancel(key: string): void;
37
+ run(tenantId: string | undefined, key: string, hash: string, request: ToolCallRequest, owner?: InflightOwner): Promise<ToolCallAnswer>;
38
+ /**
39
+ * Aborts the call under `key`; it is never run again under that key. With `sessionId`, false
40
+ * when the running call is another session's (nothing is aborted).
41
+ */
42
+ cancel(key: string, sessionId?: string): boolean;
39
43
  /** Aborts every running call (gateway shutdown). */
40
44
  close(): void;
41
45
  /** Deletes crossings settled more than `TOOL_CROSSING_TTL_MS` ago. */
@@ -1,4 +1,4 @@
1
- import { createInflightCalls, InflightConflict } from "./inflight.js";
1
+ import { createInflightCalls, InflightConflict, } from "./inflight.js";
2
2
  /** How long a settled crossing is kept. */
3
3
  export const TOOL_CROSSING_TTL_MS = 24 * 60 * 60_000;
4
4
  const LOST = {
@@ -14,9 +14,9 @@ export function createToolCalls(options) {
14
14
  const inflight = createInflightCalls(options.inflight);
15
15
  const iso = () => new Date(now()).toISOString();
16
16
  return {
17
- async run(tenantId, key, hash, request) {
17
+ async run(tenantId, key, hash, request, owner) {
18
18
  if (inflight.has(key))
19
- return inflight.run(key, hash, () => Promise.resolve(LOST));
19
+ return inflight.run(key, hash, () => Promise.resolve(LOST), owner);
20
20
  const vault = await vaults.open(tenantId);
21
21
  const store = vault.store;
22
22
  const row = await store.tx((t) => t.toolCrossing(key));
@@ -27,7 +27,7 @@ export function createToolCalls(options) {
27
27
  return row.answer;
28
28
  // Started here a moment ago, or lost with an earlier gateway.
29
29
  if (inflight.has(key))
30
- return inflight.run(key, hash, () => Promise.resolve(LOST));
30
+ return inflight.run(key, hash, () => Promise.resolve(LOST), owner);
31
31
  return LOST;
32
32
  }
33
33
  return inflight.run(key, hash, async (signal) => {
@@ -46,9 +46,9 @@ export function createToolCalls(options) {
46
46
  message: error instanceof Error ? error.message : String(error),
47
47
  }));
48
48
  return answer;
49
- });
49
+ }, owner);
50
50
  },
51
- cancel: (key) => inflight.cancel(key),
51
+ cancel: (key, sessionId) => inflight.cancel(key, sessionId),
52
52
  close: () => inflight.close(),
53
53
  async prune() {
54
54
  try {
@@ -1,9 +1,16 @@
1
1
  import type { ToolGate } from "./tool-gate.js";
2
+ import type { RunTokens } from "../tenant/run-grants.js";
2
3
  export interface HttpToolGateOptions {
3
4
  /** The gates service, e.g. `http://gateway:4100` (`NYLORUN_GATES_URL`). */
4
5
  readonly url: string;
5
- /** `NYLORUN_GATES_TOKEN`. */
6
- readonly token: string;
6
+ /**
7
+ * `NYLORUN_GATES_TOKEN`: core's credential, for deliveries and MCP requests outside a run. A
8
+ * harness has none (F6.2): its requests outside a run carry the session's last run token, or
9
+ * nothing, and the gate refuses them.
10
+ */
11
+ readonly token?: string;
12
+ /** The run token of each session an advance of this process owns. Without it, core's only. */
13
+ readonly runTokens?: RunTokens;
7
14
  /** Sent as `Nylorun-Tenant`; the gate checks it against its database's Tenant. */
8
15
  readonly tenantId?: string;
9
16
  /** How long an MCP request may stay silent. Default `GATE_CLIENT_TIMEOUT_MS`. */
@@ -9,6 +9,12 @@
9
9
  * diagnostic and `resolveEffect` marks a call `uncertain`, as before.
10
10
  * - A delivery never throws. A hop that failed before the request was sent is `not_sent`
11
11
  * (the deliverer sends it again, even a tool); one that failed after is `lost`.
12
+ *
13
+ * Credentials (F5): a session's MCP requests and keyed cancels carry its run token, read per
14
+ * request from the advance's grant, and leave the session out of the body: the gate takes it
15
+ * from the token. Deliveries, pings, and MCP requests made outside a run (closing a pooled
16
+ * connection after the advance ended) carry core's credential, `NYLORUN_GATES_TOKEN`, and name
17
+ * the session in the body.
12
18
  */
13
19
  import { randomUUID } from "node:crypto";
14
20
  import { request as httpRequest } from "node:http";
@@ -57,7 +63,9 @@ export function httpToolGate(options) {
57
63
  const outgoing = send(url, {
58
64
  method: "POST",
59
65
  headers: {
60
- authorization: `Bearer ${options.token}`,
66
+ ...((request.bearer ?? options.token)
67
+ ? { authorization: `Bearer ${request.bearer ?? options.token}` }
68
+ : {}),
61
69
  "content-type": "application/json",
62
70
  "content-length": payload.byteLength,
63
71
  ...(options.tenantId ? { [TENANT_HEADER]: options.tenantId } : {}),
@@ -88,9 +96,24 @@ export function httpToolGate(options) {
88
96
  outgoing.end(payload);
89
97
  });
90
98
  }
99
+ /**
100
+ * The body and credential of a request for `server`'s session: its run token and no session
101
+ * in the body while an advance holds a grant, core's credential and the session otherwise.
102
+ */
103
+ function scoped(server, body) {
104
+ const token = options.runTokens?.token(server.sessionId);
105
+ if (!token)
106
+ return { body: body({ ...server }) };
107
+ const { sessionId: _, ...unnamed } = server;
108
+ return { body: body(unnamed), bearer: token };
109
+ }
91
110
  /** The result of an MCP request, or a throw the pool and `resolveEffect` read as before. */
92
- async function mcp(path, body, request) {
93
- const result = await exchange(path, body, { ...request, timeoutMs });
111
+ async function mcp(path, scope, request) {
112
+ const result = await exchange(path, scope.body, {
113
+ ...request,
114
+ timeoutMs,
115
+ ...(scope.bearer ? { bearer: scope.bearer } : {}),
116
+ });
94
117
  if (result.kind === "aborted")
95
118
  throw result.reason ?? new Error("aborted");
96
119
  if (result.kind !== "answer")
@@ -102,32 +125,43 @@ export function httpToolGate(options) {
102
125
  return parsed.result;
103
126
  throw mcpError(parsed.error);
104
127
  }
105
- async function fireAndForget(path, body) {
106
- await exchange(path, body, { timeoutMs: SHORT_TIMEOUT_MS });
128
+ async function fireAndForget(path, scope) {
129
+ await exchange(path, scope.body, {
130
+ timeoutMs: SHORT_TIMEOUT_MS,
131
+ ...(scope.bearer ? { bearer: scope.bearer } : {}),
132
+ });
107
133
  }
108
134
  return {
109
135
  recovers: true,
110
136
  async openMcp(server) {
111
- await mcp(MCP_CONNECT_PATH, { server }, {});
137
+ // The credential is read per request: the connection outlives the advance that opened it.
138
+ await mcp(MCP_CONNECT_PATH, scoped(server, (named) => ({ server: named })), {});
112
139
  const client = {
113
- listTools: (params, request) => mcp(MCP_LIST_PATH, { server, ...(params?.cursor ? { cursor: params.cursor } : {}) }, request?.signal ? { signal: request.signal } : {}),
114
- callTool: (params, request) => {
115
- const body = {
116
- server: { ...server },
117
- effectId: request?.key ?? randomUUID(),
118
- name: params.name,
119
- arguments: params.arguments,
120
- };
121
- return mcp(TOOL_CALLS_PATH, body, {
122
- ...(request?.signal ? { signal: request.signal } : {}),
123
- ...(request?.key ? { idempotencyKey: request.key } : {}),
124
- });
125
- },
140
+ listTools: (params, request) => mcp(MCP_LIST_PATH, scoped(server, (named) => ({
141
+ server: named,
142
+ ...(params?.cursor ? { cursor: params.cursor } : {}),
143
+ })), request?.signal ? { signal: request.signal } : {}),
144
+ callTool: (params, request) => mcp(TOOL_CALLS_PATH, scoped(server, (named) => ({
145
+ server: named,
146
+ effectId: request?.key ?? randomUUID(),
147
+ name: params.name,
148
+ arguments: params.arguments,
149
+ })), {
150
+ ...(request?.signal ? { signal: request.signal } : {}),
151
+ ...(request?.key ? { idempotencyKey: request.key } : {}),
152
+ }),
153
+ };
154
+ return {
155
+ client,
156
+ close: () => fireAndForget(MCP_CLOSE_PATH, scoped(server, (named) => ({ server: named }))),
126
157
  };
127
- return { client, close: () => fireAndForget(MCP_CLOSE_PATH, { server }) };
128
158
  },
129
159
  async cancel(request) {
130
- await fireAndForget(`${TOOL_CALLS_PATH}/${encodeURIComponent(request.effectId)}/cancel`, undefined);
160
+ const token = options.runTokens?.token(request.sessionId);
161
+ await fireAndForget(`${TOOL_CALLS_PATH}/${encodeURIComponent(request.effectId)}/cancel`, {
162
+ body: undefined,
163
+ ...(token ? { bearer: token } : {}),
164
+ });
131
165
  },
132
166
  async post(delivery, signal) {
133
167
  const result = await exchange(DELIVERIES_PATH, {
@@ -174,7 +208,7 @@ function mcpError(error) {
174
208
  function gateRefusal(status, parsed) {
175
209
  const message = parsed?.error?.message;
176
210
  if (status === 401)
177
- return "The tool gate refused this Runtime's token: the runtime and gateway containers must share NYLORUN_GATES_TOKEN";
211
+ return `The tool gate refused the credential${typeof message === "string" ? `: ${message}` : ""}`;
178
212
  return `Tool gate answered ${status}${typeof message === "string" ? `: ${message}` : ""}`;
179
213
  }
180
214
  function parse(text) {
@@ -1,8 +1,12 @@
1
1
  /**
2
2
  * The Tool Gate's wire format (blueprint §12, F4.1): the routes the loop calls on the gates
3
3
  * service for remote MCP servers and Action deliveries. Internal, like the model call route
4
- * (`contract.ts`): same token, same optional Tenant header, not in the published OpenAPI
5
- * documents.
4
+ * (`contract.ts`): same optional Tenant header, not in the published OpenAPI documents.
5
+ *
6
+ * Credentials (F5). A session's MCP requests carry its run token, and the gate takes the
7
+ * session from it: `server.sessionId` is then absent (or the token's). Core's credential
8
+ * (`NYLORUN_GATES_TOKEN`) is accepted too, for requests made outside a run, and then
9
+ * `server.sessionId` names the session. Deliveries accept core's credential only.
6
10
  *
7
11
  * Remote MCP. The gate holds the connection and the vault credential; the loop names the
8
12
  * server, never a URL or a credential, and the gate finds it in the session's pinned manifest.
@@ -12,7 +16,8 @@
12
16
  * - `POST /nylorun/v1/tool-calls` `{server, effectId, name, arguments}`: one
13
17
  * `tools/call`. With an `Idempotency-Key` (the effect id) the call outlives its client and runs
14
18
  * once (G3); a re-send joins it or gets its outcome.
15
- * - `POST /nylorun/v1/tool-calls/{key}/cancel`: aborts a keyed call; `204` either way.
19
+ * - `POST /nylorun/v1/tool-calls/{key}/cancel`: aborts a keyed call; `204` either way, `403
20
+ * gate_forbidden` for another session's call under a run token.
16
21
  * - `POST /nylorun/v1/mcp/close` `{server}`: closes the server's connection; `204`.
17
22
  * Each answers `200 {ok: true, result}` or `200 {ok: false, error}`: an MCP failure is an
18
23
  * answer, not a gate error. A keyed call whose earlier attempt was lost with the gateway answers
@@ -35,14 +40,14 @@ export declare const MAX_DELIVERY_TIMEOUT_MS: number;
35
40
  /** The only headers the gate forwards on a delivery, lower-cased. */
36
41
  export declare const DELIVERY_HEADERS: ReadonlySet<string>;
37
42
  export declare const McpServerRefSchema: z.ZodObject<{
38
- sessionId: z.ZodString;
43
+ sessionId: z.ZodOptional<z.ZodString>;
39
44
  agentId: z.ZodOptional<z.ZodString>;
40
45
  capabilityId: z.ZodString;
41
46
  serverName: z.ZodString;
42
47
  }, z.core.$strip>;
43
48
  export declare const McpServerBodySchema: z.ZodObject<{
44
49
  server: z.ZodObject<{
45
- sessionId: z.ZodString;
50
+ sessionId: z.ZodOptional<z.ZodString>;
46
51
  agentId: z.ZodOptional<z.ZodString>;
47
52
  capabilityId: z.ZodString;
48
53
  serverName: z.ZodString;
@@ -50,7 +55,7 @@ export declare const McpServerBodySchema: z.ZodObject<{
50
55
  }, z.core.$strip>;
51
56
  export declare const McpListBodySchema: z.ZodObject<{
52
57
  server: z.ZodObject<{
53
- sessionId: z.ZodString;
58
+ sessionId: z.ZodOptional<z.ZodString>;
54
59
  agentId: z.ZodOptional<z.ZodString>;
55
60
  capabilityId: z.ZodString;
56
61
  serverName: z.ZodString;
@@ -59,7 +64,7 @@ export declare const McpListBodySchema: z.ZodObject<{
59
64
  }, z.core.$strip>;
60
65
  export declare const ToolCallBodySchema: z.ZodObject<{
61
66
  server: z.ZodObject<{
62
- sessionId: z.ZodString;
67
+ sessionId: z.ZodOptional<z.ZodString>;
63
68
  agentId: z.ZodOptional<z.ZodString>;
64
69
  capabilityId: z.ZodString;
65
70
  serverName: z.ZodString;
@@ -1,8 +1,12 @@
1
1
  /**
2
2
  * The Tool Gate's wire format (blueprint §12, F4.1): the routes the loop calls on the gates
3
3
  * service for remote MCP servers and Action deliveries. Internal, like the model call route
4
- * (`contract.ts`): same token, same optional Tenant header, not in the published OpenAPI
5
- * documents.
4
+ * (`contract.ts`): same optional Tenant header, not in the published OpenAPI documents.
5
+ *
6
+ * Credentials (F5). A session's MCP requests carry its run token, and the gate takes the
7
+ * session from it: `server.sessionId` is then absent (or the token's). Core's credential
8
+ * (`NYLORUN_GATES_TOKEN`) is accepted too, for requests made outside a run, and then
9
+ * `server.sessionId` names the session. Deliveries accept core's credential only.
6
10
  *
7
11
  * Remote MCP. The gate holds the connection and the vault credential; the loop names the
8
12
  * server, never a URL or a credential, and the gate finds it in the session's pinned manifest.
@@ -12,7 +16,8 @@
12
16
  * - `POST /nylorun/v1/tool-calls` `{server, effectId, name, arguments}`: one
13
17
  * `tools/call`. With an `Idempotency-Key` (the effect id) the call outlives its client and runs
14
18
  * once (G3); a re-send joins it or gets its outcome.
15
- * - `POST /nylorun/v1/tool-calls/{key}/cancel`: aborts a keyed call; `204` either way.
19
+ * - `POST /nylorun/v1/tool-calls/{key}/cancel`: aborts a keyed call; `204` either way, `403
20
+ * gate_forbidden` for another session's call under a run token.
16
21
  * - `POST /nylorun/v1/mcp/close` `{server}`: closes the server's connection; `204`.
17
22
  * Each answers `200 {ok: true, result}` or `200 {ok: false, error}`: an MCP failure is an
18
23
  * answer, not a gate error. A keyed call whose earlier attempt was lost with the gateway answers
@@ -40,7 +45,8 @@ export const DELIVERY_HEADERS = new Set([
40
45
  "idempotency-key",
41
46
  ]);
42
47
  export const McpServerRefSchema = z.object({
43
- sessionId: z.string().min(1),
48
+ /** Required with core's credential; a run token names the session itself. */
49
+ sessionId: z.string().min(1).optional(),
44
50
  agentId: z.string().min(1).optional(),
45
51
  capabilityId: z.string().min(1),
46
52
  serverName: z.string().min(1),
@@ -38,6 +38,7 @@ export interface ToolGate {
38
38
  /** Stops a keyed MCP call that outlives its caller (a user cancel). Never rejects. */
39
39
  cancel?(request: {
40
40
  tenantId: string;
41
+ sessionId: string;
41
42
  effectId: string;
42
43
  }): Promise<void>;
43
44
  }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The calls a harness executes, as pure helpers over what routes them: the Model Gate (or the
3
+ * Tenant's provider), the MCP pool, the SandboxManager. No store, no record: the session's
4
+ * routing comes with the run (`TurnStart.routing`).
5
+ */
6
+ import { type AgentManifest } from "@nylorun/core/define";
7
+ import type { HostEffect } from "@nylorun/harness/run";
8
+ import type { AbortReason } from "@nylorun/core/harness-api";
9
+ import type { ModelProvider } from "../core/provider.js";
10
+ import type { ModelGate } from "../gates/model-gate.js";
11
+ import { type McpPool } from "../mcp/pool.js";
12
+ import { type McpSnapshot } from "../mcp/snapshot.js";
13
+ import type { SandboxManager } from "../sandbox/manager.js";
14
+ /** Where a Tenant's model calls go. */
15
+ export interface ModelRoute {
16
+ readonly tenantId: string;
17
+ readonly modelGate: ModelGate;
18
+ readonly modelProvider: ModelProvider;
19
+ /** True when the model comes from the Tenant vault selection, served by `modelGate`. */
20
+ readonly useVaultModel: boolean;
21
+ }
22
+ /** What a session's tool calls are routed by. */
23
+ export interface ToolRouting {
24
+ readonly rootManifest: AgentManifest;
25
+ readonly pluginRoots: Readonly<Record<string, string>>;
26
+ readonly mcpSnapshot?: McpSnapshot;
27
+ /** The session that owns the tree's sandbox, and the sandbox resource it is attached to. */
28
+ readonly sandboxOwnerId: string;
29
+ readonly sandboxId?: string;
30
+ readonly activeTurnId: string | null;
31
+ /** Who the sandbox call's events are claimed for: the run's id. */
32
+ readonly claim?: string;
33
+ }
34
+ /**
35
+ * Call the model for one effect. A provider failure comes back as a failure outcome
36
+ * (Model Calls §6), whichever provider serves the call; only an abort throws, and the
37
+ * advance decides what the abort means.
38
+ */
39
+ export declare function invokeModel(route: ModelRoute, request: HostEffect, signal: AbortSignal, model?: ModelProvider): Promise<unknown>;
40
+ /**
41
+ * A signal that follows `signal` only for the abort kinds in `kinds`. An MCP call stops on a
42
+ * cancel; at the Tool Gate it also stops waiting on a shutdown, since the gate keeps the call
43
+ * for the next owner. Otherwise a shutdown or a deadline lets the call finish and records it,
44
+ * so the next advance replays it.
45
+ */
46
+ export declare function abortOn(signal: AbortSignal, kinds: readonly AbortReason[]): AbortSignal;
47
+ /** True when `request` calls `save_artifact` of the `nylorun.artifacts` capability `manifest` has. */
48
+ export declare function isSaveArtifactCall(manifest: AgentManifest | undefined, request: HostEffect): boolean;
49
+ /** True when `request` calls a tool of a remote (`streamable-http` or `sse`) MCP server. */
50
+ export declare function isRemoteMcpCall(routing: Pick<ToolRouting, "rootManifest" | "mcpSnapshot">, request: HostEffect): boolean;
51
+ export declare function callMcpTool(mcp: McpPool, routing: ToolRouting, request: HostEffect, signal: AbortSignal): Promise<unknown>;
52
+ export declare function callSandboxTool(sandbox: SandboxManager, routing: ToolRouting, request: HostEffect, signal: AbortSignal): Promise<unknown>;
@@ -0,0 +1,113 @@
1
+ /**
2
+ * The calls a harness executes, as pure helpers over what routes them: the Model Gate (or the
3
+ * Tenant's provider), the MCP pool, the SandboxManager. No store, no record: the session's
4
+ * routing comes with the run (`TurnStart.routing`).
5
+ */
6
+ import { ARTIFACTS_CAPABILITY_ID, SAVE_ARTIFACT_TOOL } from "@nylorun/core/define";
7
+ import { runAbortKind } from "@nylorun/harness/api";
8
+ import { classifyThrown } from "../model/classify.js";
9
+ import { findServer } from "../mcp/pool.js";
10
+ import { manifestFor, mcpToolOf } from "../mcp/snapshot.js";
11
+ import { sandboxCapabilityOf } from "../sandbox/capability.js";
12
+ /**
13
+ * Call the model for one effect. A provider failure comes back as a failure outcome
14
+ * (Model Calls §6), whichever provider serves the call; only an abort throws, and the
15
+ * advance decides what the abort means.
16
+ */
17
+ export async function invokeModel(route, request, signal, model) {
18
+ try {
19
+ if (model)
20
+ return await model(request, signal);
21
+ if (!route.useVaultModel)
22
+ return await route.modelProvider(request, signal);
23
+ return await route.modelGate.call({
24
+ tenantId: route.tenantId,
25
+ sessionId: request.sessionId,
26
+ turnId: request.turnId,
27
+ agentId: request.agentId,
28
+ effectId: request.effectId,
29
+ invocationId: String(request.context.invocationId),
30
+ call: request.input,
31
+ }, signal);
32
+ }
33
+ catch (error) {
34
+ if (signal.aborted) {
35
+ // A call that outlives this process stops only when told to: on a user cancel. After a
36
+ // shutdown or a lost lease the next owner re-sends it and picks up its outcome (P1.2).
37
+ if (!model && route.useVaultModel && runAbortKind(signal) === "cancel" && route.modelGate.cancel)
38
+ await route.modelGate.cancel({
39
+ tenantId: route.tenantId,
40
+ sessionId: request.sessionId,
41
+ effectId: request.effectId,
42
+ });
43
+ throw error;
44
+ }
45
+ return classifyThrown(error);
46
+ }
47
+ }
48
+ /**
49
+ * A signal that follows `signal` only for the abort kinds in `kinds`. An MCP call stops on a
50
+ * cancel; at the Tool Gate it also stops waiting on a shutdown, since the gate keeps the call
51
+ * for the next owner. Otherwise a shutdown or a deadline lets the call finish and records it,
52
+ * so the next advance replays it.
53
+ */
54
+ export function abortOn(signal, kinds) {
55
+ const controller = new AbortController();
56
+ const follow = () => {
57
+ const kind = runAbortKind(signal);
58
+ if (kind && kinds.includes(kind))
59
+ controller.abort(signal.reason);
60
+ };
61
+ if (signal.aborted)
62
+ follow();
63
+ else
64
+ signal.addEventListener("abort", follow, { once: true });
65
+ return controller.signal;
66
+ }
67
+ /** True when `request` calls `save_artifact` of the `nylorun.artifacts` capability `manifest` has. */
68
+ export function isSaveArtifactCall(manifest, request) {
69
+ return (request.kind === "tool" &&
70
+ request.capabilityId === ARTIFACTS_CAPABILITY_ID &&
71
+ request.toolName === SAVE_ARTIFACT_TOOL &&
72
+ manifest?.capabilities.some((capability) => capability.id === ARTIFACTS_CAPABILITY_ID) === true);
73
+ }
74
+ /** True when `request` calls a tool of a remote (`streamable-http` or `sse`) MCP server. */
75
+ export function isRemoteMcpCall(routing, request) {
76
+ if (request.kind !== "tool")
77
+ return false;
78
+ const tool = mcpToolOf(routing.mcpSnapshot, request);
79
+ if (!tool)
80
+ return false;
81
+ const declared = findServer(routing.rootManifest, tool.agentId, tool.capabilityId, tool.serverName);
82
+ return declared !== undefined && declared.server.type !== "stdio";
83
+ }
84
+ export async function callMcpTool(mcp, routing, request, signal) {
85
+ const tool = mcpToolOf(routing.mcpSnapshot, request);
86
+ if (!tool)
87
+ throw new Error(`MCP tool '${request.toolName ?? ""}' is not in the session snapshot`);
88
+ return mcp.call({
89
+ sessionId: request.sessionId,
90
+ ...(tool.agentId === undefined ? {} : { agentId: tool.agentId }),
91
+ capabilityId: tool.capabilityId,
92
+ serverName: tool.serverName,
93
+ serverToolName: tool.serverToolName,
94
+ args: request.input,
95
+ manifest: routing.rootManifest,
96
+ pluginRoots: routing.pluginRoots,
97
+ effectId: request.effectId,
98
+ signal,
99
+ });
100
+ }
101
+ export async function callSandboxTool(sandbox, routing, request, signal) {
102
+ const capability = sandboxCapabilityOf(manifestFor(routing.rootManifest, request.agent), request.capabilityId, request.toolName);
103
+ if (!capability)
104
+ throw new Error(`'${request.toolName ?? ""}' is not a sandbox tool`);
105
+ // Agents used as tools share the session's sandbox; the tree declares one sandbox spec.
106
+ return sandbox.run({
107
+ id: routing.sandboxOwnerId,
108
+ activeTurnId: routing.activeTurnId,
109
+ manifest: routing.rootManifest,
110
+ ...(routing.sandboxId === undefined ? {} : { sandboxId: routing.sandboxId }),
111
+ ...(routing.claim === undefined ? {} : { claim: routing.claim }),
112
+ }, capability, request.toolName, request.input, signal);
113
+ }
@@ -0,0 +1,22 @@
1
+ import type { HarnessExecutors, HarnessRun, McpRecorder, PreparedRun } from "@nylorun/harness/api";
2
+ import type { ToolGate } from "../gates/tool-gate.js";
3
+ import { type McpPool } from "../mcp/pool.js";
4
+ import type { SandboxManager } from "../sandbox/manager.js";
5
+ import { type ModelRoute, type ToolRouting } from "./calls.js";
6
+ export interface HarnessExecutorsOptions {
7
+ /** Where model calls go. Read per call: a harness process learns its Tenant at `hello`. */
8
+ readonly model: ModelRoute;
9
+ readonly toolGate: Pick<ToolGate, "recovers" | "cancel">;
10
+ readonly mcp: McpPool;
11
+ /** The SandboxManager; a harness process creates it once `hello` names the Tenant. */
12
+ readonly sandbox: () => SandboxManager;
13
+ }
14
+ export declare function harnessExecutors(options: HarnessExecutorsOptions): HarnessExecutors;
15
+ /**
16
+ * Readies the run's MCP servers (§10.5 step 3): the first segment of a session discovers their
17
+ * tools and records the snapshot (the first recorded wins); later ones reconnect them and record
18
+ * what failed. Nothing for a session without MCP servers.
19
+ */
20
+ export declare function prepareMcp(pool: McpPool, run: HarnessRun, record: McpRecorder): Promise<PreparedRun | undefined>;
21
+ /** A run's routing, as the call helpers take it. Its sandbox events are claimed for the run. */
22
+ export declare function routingOf(run: HarnessRun): ToolRouting;