@nylorun/runtime 0.15.2-beta → 0.17.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 (291) hide show
  1. package/CHANGELOG.md +134 -0
  2. package/README.md +68 -29
  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 +341 -7
  9. package/dist/api/a2a/tasks.js +6 -1
  10. package/dist/api/ag-ui/endpoint.js +1 -2
  11. package/dist/api/ag-ui/history.js +6 -1
  12. package/dist/api/ag-ui/routes.d.ts +1 -1
  13. package/dist/api/ag-ui/routes.js +15 -13
  14. package/dist/api/components.d.ts +2667 -711
  15. package/dist/api/components.js +24 -11
  16. package/dist/api/gate/keys.d.ts +4 -3
  17. package/dist/api/gate/routes.d.ts +19 -3
  18. package/dist/api/gate/routes.js +162 -53
  19. package/dist/api/http/app.js +6 -0
  20. package/dist/api/http/define.d.ts +19 -12
  21. package/dist/api/http/define.js +13 -27
  22. package/dist/api/http/routes/access.d.ts +3 -19
  23. package/dist/api/http/routes/access.js +12 -172
  24. package/dist/api/http/routes/actions.js +0 -1
  25. package/dist/api/http/routes/artifacts.d.ts +11 -0
  26. package/dist/api/http/routes/artifacts.js +516 -0
  27. package/dist/api/http/routes/me.d.ts +13 -0
  28. package/dist/api/http/routes/me.js +45 -0
  29. package/dist/api/http/routes/sandboxes.d.ts +9 -0
  30. package/dist/api/http/routes/sandboxes.js +150 -0
  31. package/dist/api/http/routes/sessions.js +9 -13
  32. package/dist/api/http/routes/tenant.js +36 -2
  33. package/dist/api/http/routes/vaults.d.ts +12 -3
  34. package/dist/api/http/routes/vaults.js +122 -54
  35. package/dist/api/openapi.js +4 -10
  36. package/dist/artifacts/config.d.ts +23 -0
  37. package/dist/artifacts/config.js +27 -0
  38. package/dist/artifacts/export.d.ts +8 -0
  39. package/dist/artifacts/export.js +205 -0
  40. package/dist/artifacts/files.d.ts +36 -0
  41. package/dist/artifacts/files.js +45 -0
  42. package/dist/artifacts/folders.d.ts +60 -0
  43. package/dist/artifacts/folders.js +209 -0
  44. package/dist/artifacts/links.d.ts +28 -0
  45. package/dist/artifacts/links.js +92 -0
  46. package/dist/artifacts/media-types.d.ts +13 -0
  47. package/dist/artifacts/media-types.js +80 -0
  48. package/dist/artifacts/parts.d.ts +34 -0
  49. package/dist/artifacts/parts.js +38 -0
  50. package/dist/artifacts/service.d.ts +74 -0
  51. package/dist/artifacts/service.js +301 -0
  52. package/dist/artifacts/workspace.d.ts +60 -0
  53. package/dist/artifacts/workspace.js +57 -0
  54. package/dist/artifacts/zip.d.ts +10 -0
  55. package/dist/artifacts/zip.js +126 -0
  56. package/dist/blob/fs.d.ts +13 -0
  57. package/dist/blob/fs.js +194 -0
  58. package/dist/blob/index.d.ts +9 -0
  59. package/dist/blob/index.js +9 -0
  60. package/dist/blob/s3.d.ts +42 -0
  61. package/dist/blob/s3.js +366 -0
  62. package/dist/blob/types.d.ts +108 -0
  63. package/dist/blob/types.js +85 -0
  64. package/dist/core/sandbox-routes.d.ts +2 -2
  65. package/dist/core/sandbox-routes.js +4 -3
  66. package/dist/execution/memory.d.ts +11 -1
  67. package/dist/execution/memory.js +77 -3
  68. package/dist/execution/types.d.ts +54 -1
  69. package/dist/execution/types.js +31 -0
  70. package/dist/gates/contract.d.ts +14 -13
  71. package/dist/gates/contract.js +13 -12
  72. package/dist/gates/egress.d.ts +72 -0
  73. package/dist/gates/egress.js +319 -0
  74. package/dist/gates/handler.d.ts +3 -0
  75. package/dist/gates/handler.js +3 -2
  76. package/dist/gates/http-client.d.ts +3 -2
  77. package/dist/gates/http-client.js +17 -6
  78. package/dist/gates/in-process.d.ts +13 -1
  79. package/dist/gates/in-process.js +11 -0
  80. package/dist/gates/inflight.d.ts +23 -4
  81. package/dist/gates/inflight.js +18 -4
  82. package/dist/gates/mcp-handler.js +5 -1
  83. package/dist/gates/model-gate.d.ts +6 -2
  84. package/dist/gates/tenant-vaults.d.ts +18 -9
  85. package/dist/gates/tenant-vaults.js +17 -12
  86. package/dist/gates/tool-calls.d.ts +9 -5
  87. package/dist/gates/tool-calls.js +6 -6
  88. package/dist/gates/tool-client.d.ts +9 -2
  89. package/dist/gates/tool-client.js +56 -22
  90. package/dist/gates/tool-contract.d.ts +12 -7
  91. package/dist/gates/tool-contract.js +10 -4
  92. package/dist/gates/tool-gate.d.ts +1 -0
  93. package/dist/harness/calls.d.ts +52 -0
  94. package/dist/harness/calls.js +113 -0
  95. package/dist/harness/executors.d.ts +22 -0
  96. package/dist/harness/executors.js +96 -0
  97. package/dist/harness/main.d.ts +6 -0
  98. package/dist/harness/main.js +103 -0
  99. package/dist/harness/pod.d.ts +21 -0
  100. package/dist/harness/pod.js +119 -0
  101. package/dist/harness/run-tokens.d.ts +18 -0
  102. package/dist/harness/run-tokens.js +15 -0
  103. package/dist/harness/service.d.ts +46 -0
  104. package/dist/harness/service.js +179 -0
  105. package/dist/harness/ws-client.d.ts +43 -0
  106. package/dist/harness/ws-client.js +122 -0
  107. package/dist/harness/ws-port.d.ts +22 -0
  108. package/dist/harness/ws-port.js +78 -0
  109. package/dist/harness-api/in-process.d.ts +24 -0
  110. package/dist/harness-api/in-process.js +119 -0
  111. package/dist/harness-api/record.d.ts +44 -0
  112. package/dist/harness-api/record.js +250 -0
  113. package/dist/harness-api/renew.d.ts +37 -0
  114. package/dist/harness-api/renew.js +77 -0
  115. package/dist/harness-api/server.d.ts +104 -0
  116. package/dist/harness-api/server.js +408 -0
  117. package/dist/harness-api/start.d.ts +36 -0
  118. package/dist/harness-api/start.js +118 -0
  119. package/dist/harness-api/workspace.d.ts +53 -0
  120. package/dist/harness-api/workspace.js +201 -0
  121. package/dist/harness-api/ws-server.d.ts +34 -0
  122. package/dist/harness-api/ws-server.js +227 -0
  123. package/dist/host/admin-api.d.ts +12 -5
  124. package/dist/host/admin-api.js +72 -6
  125. package/dist/host/app.d.ts +0 -1
  126. package/dist/host/app.js +35 -24
  127. package/dist/host/config-for.d.ts +4 -0
  128. package/dist/host/config-for.js +2 -0
  129. package/dist/host/config.d.ts +0 -6
  130. package/dist/host/create-host.d.ts +3 -10
  131. package/dist/host/create-host.js +4 -1
  132. package/dist/host/gates.d.ts +27 -2
  133. package/dist/host/gates.js +52 -2
  134. package/dist/host/http.d.ts +4 -1
  135. package/dist/host/http.js +6 -1
  136. package/dist/host/main.js +167 -37
  137. package/dist/host/stack-config.d.ts +112 -9
  138. package/dist/host/stack-config.js +222 -20
  139. package/dist/index.d.ts +3 -0
  140. package/dist/index.js +2 -0
  141. package/dist/keys/client.js +9 -1
  142. package/dist/keys/keys.d.ts +12 -2
  143. package/dist/keys/keys.js +8 -0
  144. package/dist/mcp/connect.js +2 -1
  145. package/dist/mcp/pool.d.ts +1 -0
  146. package/dist/mcp/pool.js +1 -0
  147. package/dist/mcp/snapshot.d.ts +8 -1
  148. package/dist/mcp/snapshot.js +28 -0
  149. package/dist/media.d.ts +5 -24
  150. package/dist/media.js +5 -0
  151. package/dist/model/pi-model.d.ts +6 -2
  152. package/dist/model/pi-model.js +39 -22
  153. package/dist/node/index.d.ts +1 -1
  154. package/dist/node/index.js +1 -1
  155. package/dist/openapi.json +14084 -9426
  156. package/dist/record/index.d.ts +1 -0
  157. package/dist/record/index.js +1 -0
  158. package/dist/record/sandbox.d.ts +25 -0
  159. package/dist/record/sandbox.js +43 -0
  160. package/dist/sandbox/capability.d.ts +3 -0
  161. package/dist/sandbox/capability.js +10 -0
  162. package/dist/sandbox/egress-token.d.ts +26 -0
  163. package/dist/sandbox/egress-token.js +8 -0
  164. package/dist/sandbox/join.d.ts +37 -0
  165. package/dist/sandbox/join.js +165 -0
  166. package/dist/sandbox/manager.d.ts +99 -34
  167. package/dist/sandbox/manager.js +216 -84
  168. package/dist/sandbox/placement.d.ts +27 -0
  169. package/dist/sandbox/placement.js +44 -0
  170. package/dist/sandbox/pods/client.d.ts +90 -0
  171. package/dist/sandbox/pods/client.js +79 -0
  172. package/dist/sandbox/pods/lifecycle.d.ts +91 -0
  173. package/dist/sandbox/pods/lifecycle.js +178 -0
  174. package/dist/sandbox/pods/name.d.ts +1 -0
  175. package/dist/sandbox/pods/name.js +28 -0
  176. package/dist/sandbox/pods/network-gate.d.ts +25 -0
  177. package/dist/sandbox/pods/network-gate.js +71 -0
  178. package/dist/sandbox/pods/reconcile.d.ts +11 -0
  179. package/dist/sandbox/pods/reconcile.js +152 -0
  180. package/dist/sandbox/pods/spec.d.ts +45 -0
  181. package/dist/sandbox/pods/spec.js +62 -0
  182. package/dist/sandbox/records.d.ts +29 -0
  183. package/dist/sandbox/records.js +79 -0
  184. package/dist/sandbox/resolve.d.ts +5 -0
  185. package/dist/sandbox/resolve.js +3 -2
  186. package/dist/sandbox/session-sandbox.js +7 -5
  187. package/dist/sandbox/share.d.ts +11 -0
  188. package/dist/sandbox/share.js +10 -0
  189. package/dist/sandbox/tenant-config.d.ts +16 -1
  190. package/dist/sandbox/tenant-config.js +9 -0
  191. package/dist/sandbox/types.d.ts +29 -1
  192. package/dist/sandbox/types.js +13 -1
  193. package/dist/store/postgres/drizzle/0004_sandbox_resources.sql +20 -0
  194. package/dist/store/postgres/drizzle/0005_artifacts.sql +27 -0
  195. package/dist/store/postgres/drizzle/0006_folder_artifacts.sql +12 -0
  196. package/dist/store/postgres/drizzle/0007_sandbox_pods.sql +14 -0
  197. package/dist/store/postgres/drizzle/0008_installation_vaults.sql +2 -0
  198. package/dist/store/postgres/drizzle/0009_oauth_pending.sql +19 -0
  199. package/dist/store/postgres/drizzle/0010_oss_auth_removals.sql +3 -0
  200. package/dist/store/postgres/drizzle/meta/_journal.json +49 -0
  201. package/dist/store/postgres/record-writer.d.ts +6 -1
  202. package/dist/store/postgres/record-writer.js +20 -2
  203. package/dist/store/postgres/schema.d.ts +1168 -250
  204. package/dist/store/postgres/schema.js +174 -28
  205. package/dist/store/postgres/store.js +339 -103
  206. package/dist/store/postgres/tenant.d.ts +1 -1
  207. package/dist/store/postgres/tenant.js +2 -2
  208. package/dist/store/types.d.ts +163 -32
  209. package/dist/streams/control.d.ts +2 -5
  210. package/dist/streams/control.js +5 -8
  211. package/dist/streams/types.d.ts +8 -8
  212. package/dist/streams/types.js +1 -1
  213. package/dist/tenant/advance.js +218 -160
  214. package/dist/tenant/artifact-tool.d.ts +5 -0
  215. package/dist/tenant/artifact-tool.js +119 -0
  216. package/dist/tenant/auth.d.ts +13 -9
  217. package/dist/tenant/auth.js +25 -32
  218. package/dist/tenant/commands.js +31 -10
  219. package/dist/tenant/context.d.ts +83 -26
  220. package/dist/tenant/delivery-token.js +4 -4
  221. package/dist/tenant/effects.d.ts +24 -14
  222. package/dist/tenant/effects.js +10 -398
  223. package/dist/tenant/ephemeral.d.ts +14 -2
  224. package/dist/tenant/ephemeral.js +8 -1
  225. package/dist/tenant/history.d.ts +13 -23
  226. package/dist/tenant/history.js +14 -62
  227. package/dist/tenant/host-token.d.ts +92 -0
  228. package/dist/tenant/host-token.js +179 -0
  229. package/dist/tenant/identity-file.d.ts +75 -0
  230. package/dist/tenant/identity-file.js +202 -0
  231. package/dist/tenant/issuers.d.ts +48 -0
  232. package/dist/tenant/issuers.js +311 -0
  233. package/dist/tenant/jwt.js +3 -3
  234. package/dist/tenant/module.js +1 -0
  235. package/dist/tenant/operator-keys.d.ts +18 -0
  236. package/dist/tenant/operator-keys.js +55 -0
  237. package/dist/tenant/outbound.d.ts +30 -0
  238. package/dist/tenant/outbound.js +142 -10
  239. package/dist/tenant/paths.d.ts +2 -1
  240. package/dist/tenant/paths.js +3 -1
  241. package/dist/tenant/principals.d.ts +2 -11
  242. package/dist/tenant/principals.js +10 -30
  243. package/dist/tenant/reset.d.ts +5 -2
  244. package/dist/tenant/reset.js +22 -2
  245. package/dist/tenant/run-grants.d.ts +49 -0
  246. package/dist/tenant/run-grants.js +62 -0
  247. package/dist/tenant/run-token.d.ts +70 -0
  248. package/dist/tenant/run-token.js +138 -0
  249. package/dist/tenant/runtime.d.ts +42 -0
  250. package/dist/tenant/runtime.js +134 -33
  251. package/dist/tenant/sandboxes.d.ts +119 -0
  252. package/dist/tenant/sandboxes.js +551 -0
  253. package/dist/tenant/session-streams.d.ts +6 -10
  254. package/dist/tenant/session-streams.js +9 -27
  255. package/dist/tenant/session.d.ts +2 -13
  256. package/dist/tenant/session.js +4 -28
  257. package/dist/tenant/sessions.d.ts +6 -1
  258. package/dist/tenant/sessions.js +29 -2
  259. package/dist/tenant/signing-keys.d.ts +2 -1
  260. package/dist/tenant/signing-keys.js +2 -2
  261. package/dist/tenant/status.d.ts +8 -3
  262. package/dist/tenant/status.js +5 -0
  263. package/dist/tenant/streams.d.ts +8 -2
  264. package/dist/tenant/streams.js +25 -6
  265. package/dist/tenant/sweep.js +37 -4
  266. package/dist/tenant/types.d.ts +39 -1
  267. package/dist/tenant/worker.d.ts +11 -5
  268. package/dist/tenant/worker.js +12 -6
  269. package/dist/vault/crypto.d.ts +7 -0
  270. package/dist/vault/crypto.js +16 -0
  271. package/dist/vault/oauth.d.ts +55 -0
  272. package/dist/vault/oauth.js +186 -0
  273. package/dist/vault/service.d.ts +54 -7
  274. package/dist/vault/service.js +261 -17
  275. package/dist/vault/sources.d.ts +54 -0
  276. package/dist/vault/sources.js +185 -0
  277. package/dist/version.d.ts +1 -1
  278. package/dist/version.js +1 -1
  279. package/package.json +14 -10
  280. package/dist/adapters/media.d.ts +0 -24
  281. package/dist/adapters/media.js +0 -111
  282. package/dist/host/cors.d.ts +0 -27
  283. package/dist/host/cors.js +0 -64
  284. package/dist/tenant/access-policy.d.ts +0 -30
  285. package/dist/tenant/access-policy.js +0 -52
  286. package/dist/tenant/browser.d.ts +0 -15
  287. package/dist/tenant/browser.js +0 -39
  288. package/dist/tenant/subject-limits.d.ts +0 -16
  289. package/dist/tenant/subject-limits.js +0 -45
  290. package/dist/tenant/tokens.d.ts +0 -14
  291. package/dist/tenant/tokens.js +0 -180
@@ -0,0 +1,96 @@
1
+ /**
2
+ * A harness's executors (F6.1, F6.2): the model route, the Tool Gate, an MCP pool and a
3
+ * SandboxManager, injected. They reach no store: a run's routing comes with its `turn.start`,
4
+ * and the session's MCP discovery is recorded through core (`session.mcp`, `prepare`).
5
+ *
6
+ * The in-process harness gets the Tenant's own pool and SandboxManager; a harness process
7
+ * (`--service harness`, `main.ts`) its own, whose sandbox events it claims from core.
8
+ */
9
+ import { hashManifest } from "@nylorun/core/define";
10
+ import { toolFixtureModel } from "../core/provider.js";
11
+ import { serversOf } from "../mcp/pool.js";
12
+ import { mcpToolOf } from "../mcp/snapshot.js";
13
+ import { abortOn, callMcpTool, callSandboxTool, invokeModel, isRemoteMcpCall, isSaveArtifactCall, } from "./calls.js";
14
+ /** The Tenant's model of the fixture-model setting. Stateless. */
15
+ const fixture = toolFixtureModel();
16
+ export function harnessExecutors(options) {
17
+ const recoversMcp = options.toolGate.recovers === true;
18
+ const remote = (effect, run) => recoversMcp && isRemoteMcpCall(routingOf(run), effect);
19
+ return {
20
+ model: (effect, signal, run) => invokeModel(options.model, effect, signal, run.start.options.fixtureModel ? fixture : undefined),
21
+ tool(effect, signal, run) {
22
+ const routing = routingOf(run);
23
+ if (!mcpToolOf(routing.mcpSnapshot, effect))
24
+ return callSandboxTool(options.sandbox(), routing, effect, signal);
25
+ // A call at the Tool Gate outlives a shutdown there; a local one finishes and is recorded.
26
+ const kinds = remote(effect, run) ? ["cancel", "shutdown"] : ["cancel"];
27
+ return callMcpTool(options.mcp, routing, effect, abortOn(signal, kinds));
28
+ },
29
+ recovers: {
30
+ get model() {
31
+ return options.model.useVaultModel && options.model.modelGate.recovers === true;
32
+ },
33
+ remoteMcp: remote,
34
+ },
35
+ async cancelAtGate(effect) {
36
+ await options.toolGate.cancel?.({
37
+ tenantId: options.model.tenantId,
38
+ sessionId: effect.sessionId,
39
+ effectId: effect.effectId,
40
+ });
41
+ },
42
+ prepare: (run, record) => prepareMcp(options.mcp, run, record),
43
+ };
44
+ }
45
+ /**
46
+ * Readies the run's MCP servers (§10.5 step 3): the first segment of a session discovers their
47
+ * tools and records the snapshot (the first recorded wins); later ones reconnect them and record
48
+ * what failed. Nothing for a session without MCP servers.
49
+ */
50
+ export async function prepareMcp(pool, run, record) {
51
+ const routing = routingOf(run);
52
+ const manifest = routing.rootManifest;
53
+ if (serversOf(manifest).length === 0)
54
+ return undefined;
55
+ const sessionId = run.grant.sessionId;
56
+ let answer;
57
+ if (!routing.mcpSnapshot) {
58
+ const found = await pool.discover({
59
+ sessionId,
60
+ manifest,
61
+ manifestHash: hashManifest(manifest),
62
+ pluginRoots: routing.pluginRoots,
63
+ signal: run.signal,
64
+ });
65
+ answer = await record({ snapshot: found.snapshot, diagnostics: found.diagnostics });
66
+ }
67
+ else {
68
+ const diagnostics = await pool.reconnect({
69
+ sessionId,
70
+ manifest,
71
+ pluginRoots: routing.pluginRoots,
72
+ tools: routing.mcpSnapshot.mcpTools,
73
+ signal: run.signal,
74
+ });
75
+ if (diagnostics.length === 0)
76
+ return undefined;
77
+ answer = await record({ diagnostics });
78
+ }
79
+ return {
80
+ sessionTools: answer.sessionTools,
81
+ ...(answer.snapshot === undefined ? {} : { mcpSnapshot: answer.snapshot }),
82
+ };
83
+ }
84
+ /** A run's routing, as the call helpers take it. Its sandbox events are claimed for the run. */
85
+ export function routingOf(run) {
86
+ const { routing } = run.start;
87
+ return {
88
+ rootManifest: routing.rootManifest,
89
+ pluginRoots: routing.pluginRoots,
90
+ ...(routing.mcpSnapshot ? { mcpSnapshot: routing.mcpSnapshot } : {}),
91
+ sandboxOwnerId: routing.sandbox?.ownerId ?? run.grant.sessionId,
92
+ ...(routing.sandbox?.sandboxId === undefined ? {} : { sandboxId: routing.sandbox.sandboxId }),
93
+ activeTurnId: run.grant.turnId,
94
+ claim: run.runId,
95
+ };
96
+ }
@@ -0,0 +1,6 @@
1
+ import type { StackConfig } from "../host/stack-config.js";
2
+ /**
3
+ * `baseline` is the allowlisted environment of MCP stdio servers (`baselineEnvironment`), read
4
+ * by `host/main.ts`: nothing here reads the process environment.
5
+ */
6
+ export declare function runHarness(stack: StackConfig, baseline: Readonly<Record<string, string>>): Promise<void>;
@@ -0,0 +1,103 @@
1
+ /**
2
+ * The harness service (`--service harness`, F6.2): the process that runs the Tenant's agent
3
+ * segments, MCP servers and sandboxes, apart from core. It holds one credential, the harness
4
+ * token, for core's Harness API; its model and MCP calls present the run token of the run they
5
+ * belong to (from the `lease` answer and each renewal), never the gates' credential. It opens no
6
+ * database, no Restate, no keys.
7
+ *
8
+ * Its files live under `NYLORUN_HARNESS_ROOT` (`/harness`): `sandboxes/` (workspaces and their
9
+ * records), `plugin-data/`, and `home/` and `tmp/` for MCP stdio servers. `/health` answers on
10
+ * 127.0.0.1 only.
11
+ *
12
+ * In a pod sandbox (`NYLORUN_SANDBOX_KIND=pod`, F7.2) it is that sandbox's engine: it first waits
13
+ * until the pod's NetworkPolicy is in force (`awaitNetworkPolicy`), then joins with the
14
+ * pod's join token instead of a harness token (`pod.ts`), and runs its sandbox's tools in the
15
+ * pod itself (the `local` backend), with egress through egress-gate. SIGTERM (tini forwards
16
+ * it; the pod's grace is 10 s by default) gives its runs back and exits.
17
+ */
18
+ import { mkdirSync } from "node:fs";
19
+ import { createServer } from "node:http";
20
+ import { join } from "node:path";
21
+ import { httpModelGate } from "../gates/http-client.js";
22
+ import { httpToolGate } from "../gates/tool-client.js";
23
+ import { createHostLogger } from "../host/logger.js";
24
+ import { RUNTIME_VERSION } from "../version.js";
25
+ import { localBackend } from "../adapters/sandbox/local.js";
26
+ import { awaitNetworkPolicy } from "../sandbox/pods/network-gate.js";
27
+ import { podHost } from "./pod.js";
28
+ import { harnessRunTokens } from "./run-tokens.js";
29
+ import { startHarnessService } from "./service.js";
30
+ /**
31
+ * `baseline` is the allowlisted environment of MCP stdio servers (`baselineEnvironment`), read
32
+ * by `host/main.ts`: nothing here reads the process environment.
33
+ */
34
+ export async function runHarness(stack, baseline) {
35
+ const config = stack.harness;
36
+ const logger = createHostLogger();
37
+ const paths = {
38
+ sandboxes: join(config.root, "sandboxes"),
39
+ pluginData: join(config.root, "plugin-data"),
40
+ home: join(config.root, "home"),
41
+ tmp: join(config.root, "tmp"),
42
+ };
43
+ for (const dir of Object.values(paths))
44
+ mkdirSync(dir, { recursive: true });
45
+ logger.info("host_stack_config", { services: [...stack.services], harness: config.url, gates: config.gatesUrl });
46
+ // A pod's engine trusts the network only once its NetworkPolicy blocks what it must.
47
+ let pod;
48
+ if (config.pod) {
49
+ const { waitedMs } = await awaitNetworkPolicy({
50
+ addresses: config.pod.blocked,
51
+ log: (message, fields) => logger.info(message, fields),
52
+ });
53
+ logger.info("sandbox_network_policy_in_force", { sandboxId: config.pod.sandboxId, waitedMs });
54
+ pod = podHost(config.pod, logger, { volumeFile: join(config.root, "volume-id") });
55
+ }
56
+ const token = pod ? () => pod.token() : config.token;
57
+ if (token === undefined)
58
+ throw new Error("--service harness needs NYLORUN_HARNESS_TOKEN");
59
+ const runTokens = harnessRunTokens();
60
+ const service = startHarnessService({
61
+ url: config.url,
62
+ token,
63
+ ...(pod ? { sandboxBackends: [localBackend({ env: baseline, proxyEnv: () => pod.proxyEnv() })] } : {}),
64
+ paths,
65
+ childEnv: { ...baseline, HOME: paths.home, TMPDIR: paths.tmp },
66
+ modelGate: httpModelGate({ url: config.gatesUrl, runTokens }),
67
+ useVaultModel: true,
68
+ toolGate: httpToolGate({ url: config.gatesUrl, runTokens }),
69
+ logger,
70
+ name: pod ? `sandbox ${config.pod.sandboxId}` : "harness",
71
+ version: RUNTIME_VERSION,
72
+ onGrant: (grant) => runTokens.grant(grant),
73
+ });
74
+ const health = createServer((request, response) => {
75
+ if (request.url !== "/health") {
76
+ response.writeHead(404).end();
77
+ return;
78
+ }
79
+ response.writeHead(200, { "content-type": "application/json" });
80
+ response.end(JSON.stringify({ status: "ok", service: "nylorun-harness", connected: service.client.connected }));
81
+ });
82
+ await new Promise((resolve, reject) => {
83
+ health.once("error", reject);
84
+ health.listen(config.healthPort, "127.0.0.1", () => resolve());
85
+ });
86
+ logger.info("harness_ready", { health: `http://127.0.0.1:${config.healthPort}/health` });
87
+ let stopping = false;
88
+ for (const signal of ["SIGINT", "SIGTERM"])
89
+ process.on(signal, () => {
90
+ if (stopping)
91
+ return;
92
+ stopping = true;
93
+ // Runs are given back (core resumes them in its next advance), then MCP and sandboxes stop.
94
+ pod?.stop();
95
+ void service
96
+ .stop(pod ? 7_000 : 10_000)
97
+ .catch((error) => logger.warn("harness_stop_failed", { message: error instanceof Error ? error.message : String(error) }))
98
+ .finally(() => {
99
+ health.close();
100
+ process.exit(0);
101
+ });
102
+ });
103
+ }
@@ -0,0 +1,21 @@
1
+ import type { PodHostConfig } from "../host/stack-config.js";
2
+ /** How often the host and egress tokens are renewed. */
3
+ export declare const RENEW_MS: number;
4
+ export interface PodHost {
5
+ /** A current host token, for the next connection. Throws when neither renewal nor join works. */
6
+ token(): Promise<string>;
7
+ /** `HTTPS_PROXY` and friends with the current egress token; none before the first join. */
8
+ proxyEnv(): Record<string, string>;
9
+ /** The host epoch of the tokens held. */
10
+ readonly epoch: number | undefined;
11
+ stop(): void;
12
+ }
13
+ export declare function podHost(config: PodHostConfig, logger: {
14
+ info(message: string, fields?: Record<string, unknown>): void;
15
+ warn(message: string, fields?: Record<string, unknown>): void;
16
+ }, options?: {
17
+ fetch?: typeof fetch;
18
+ renewMs?: number;
19
+ /** Where the engine keeps the volume's id (on the volume): a new volume gets a new one. */
20
+ volumeFile?: string;
21
+ }): PodHost;
@@ -0,0 +1,119 @@
1
+ /**
2
+ * The engine in a pod sandbox (F7.2, D42): its credentials. It holds no harness token. Before
3
+ * each connection to the Harness API it renews its host token (`host/renew`), or, with none or
4
+ * one refused (its epoch moved), exchanges the join token mounted in the pod (`host/join`,
5
+ * read afresh each time: a reset writes a new one). The egress token minted with the host token
6
+ * is the password of the proxy every command gets (`proxyEnv`); both are renewed every
7
+ * `RENEW_MS`, well inside their 15-minute life.
8
+ */
9
+ import { randomUUID } from "node:crypto";
10
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
11
+ import { dirname } from "node:path";
12
+ /** How often the host and egress tokens are renewed. */
13
+ export const RENEW_MS = 5 * 60_000;
14
+ class Refused extends Error {
15
+ }
16
+ export function podHost(config, logger, options = {}) {
17
+ const call = options.fetch ?? fetch;
18
+ let held;
19
+ const post = async (path, init) => {
20
+ const response = await call(`${config.httpUrl}/nylorun/harness/v1/host/${path}`, {
21
+ method: "POST",
22
+ headers: {
23
+ ...(init.body === undefined ? {} : { "content-type": "application/json" }),
24
+ ...(init.bearer ? { authorization: `Bearer ${init.bearer}` } : {}),
25
+ },
26
+ ...(init.body === undefined ? {} : { body: JSON.stringify(init.body) }),
27
+ signal: AbortSignal.timeout(15_000),
28
+ });
29
+ const text = await response.text();
30
+ if (response.status === 401 || response.status === 400)
31
+ throw new Refused(`host/${path} refused: ${response.status}`);
32
+ if (!response.ok)
33
+ throw new Error(`host/${path} answered ${response.status}`);
34
+ const body = JSON.parse(text);
35
+ return {
36
+ hostToken: body.hostToken,
37
+ egressToken: body.egressToken,
38
+ epoch: body.epoch,
39
+ expiresAt: Date.parse(body.expiresAt),
40
+ };
41
+ };
42
+ /** The volume's id, created on a volume that has none. */
43
+ const volumeId = async () => {
44
+ const file = options.volumeFile;
45
+ if (!file)
46
+ return undefined;
47
+ try {
48
+ return (await readFile(file, "utf8")).trim();
49
+ }
50
+ catch {
51
+ const id = randomUUID();
52
+ await mkdir(dirname(file), { recursive: true });
53
+ await writeFile(file, `${id}\n`, { mode: 0o600 });
54
+ return id;
55
+ }
56
+ };
57
+ const join = async () => {
58
+ const joinToken = (await readFile(config.joinFile, "utf8")).trim();
59
+ const volume = await volumeId();
60
+ held = await post("join", {
61
+ body: {
62
+ sandboxId: config.sandboxId,
63
+ podUid: config.podUid,
64
+ joinToken,
65
+ ...(volume === undefined ? {} : { volumeId: volume }),
66
+ },
67
+ });
68
+ logger.info("sandbox_host_joined", { sandboxId: config.sandboxId, epoch: held.epoch });
69
+ return held;
70
+ };
71
+ const renew = async () => {
72
+ if (!held)
73
+ return undefined;
74
+ try {
75
+ held = await post("renew", { bearer: held.hostToken });
76
+ return held;
77
+ }
78
+ catch (error) {
79
+ if (error instanceof Refused) {
80
+ logger.warn("sandbox_host_renew_refused", { sandboxId: config.sandboxId, epoch: held.epoch });
81
+ held = undefined;
82
+ return undefined;
83
+ }
84
+ throw error;
85
+ }
86
+ };
87
+ const timer = setInterval(() => {
88
+ void renew().catch((error) => logger.warn("sandbox_host_renew_failed", { message: error instanceof Error ? error.message : String(error) }));
89
+ }, options.renewMs ?? RENEW_MS);
90
+ timer.unref();
91
+ return {
92
+ async token() {
93
+ const current = (await renew()) ?? (await join());
94
+ return current.hostToken;
95
+ },
96
+ proxyEnv() {
97
+ if (!config.egressProxy || !held)
98
+ return {};
99
+ const url = new URL(config.egressProxy);
100
+ url.username = "nylorun";
101
+ url.password = held.egressToken;
102
+ const proxy = url.toString().replace(/\/$/, "");
103
+ return {
104
+ HTTPS_PROXY: proxy,
105
+ HTTP_PROXY: proxy,
106
+ https_proxy: proxy,
107
+ http_proxy: proxy,
108
+ NO_PROXY: "localhost,127.0.0.1",
109
+ no_proxy: "localhost,127.0.0.1",
110
+ };
111
+ },
112
+ get epoch() {
113
+ return held?.epoch;
114
+ },
115
+ stop() {
116
+ clearInterval(timer);
117
+ },
118
+ };
119
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The run tokens of a harness process (F5, F6.2): the credential of its model and MCP calls at
3
+ * the gates. Each comes with a run (the `lease` answer's grant) or a renewal (`lease.renew`);
4
+ * the HTTP gate clients read a session's token on every request (`RunTokens`).
5
+ *
6
+ * A session's last token is kept after its run ends, so a pooled MCP connection closed later
7
+ * (idle, or at shutdown) still presents one: a close the gate refuses is ignored, and the
8
+ * gateway's own idle timeout closes the rest. A harness never holds `NYLORUN_GATES_TOKEN`.
9
+ */
10
+ import type { RunGrant } from "@nylorun/core/harness-api";
11
+ export interface HarnessRunTokens {
12
+ /** The latest run token of `sessionId`, if a run of it was ever leased here. */
13
+ token(sessionId: string): string | undefined;
14
+ /** Keeps a run's grant: `createHarness({ onGrant })`. */
15
+ grant(grant: RunGrant): void;
16
+ }
17
+ /** Remembers the last token of at most `limit` sessions (least recently granted dropped first). */
18
+ export declare function harnessRunTokens(limit?: number): HarnessRunTokens;
@@ -0,0 +1,15 @@
1
+ /** Remembers the last token of at most `limit` sessions (least recently granted dropped first). */
2
+ export function harnessRunTokens(limit = 10_000) {
3
+ const tokens = new Map();
4
+ return {
5
+ token: (sessionId) => tokens.get(sessionId),
6
+ grant(grant) {
7
+ if (!grant.token)
8
+ return;
9
+ tokens.delete(grant.sessionId);
10
+ tokens.set(grant.sessionId, grant.token);
11
+ if (tokens.size > limit)
12
+ tokens.delete(tokens.keys().next().value);
13
+ },
14
+ };
15
+ }
@@ -0,0 +1,46 @@
1
+ import type { HarnessOptions } from "@nylorun/harness/api";
2
+ import type { ModelProvider } from "../core/provider.js";
3
+ import type { ModelGate } from "../gates/model-gate.js";
4
+ import type { ToolGate } from "../gates/tool-gate.js";
5
+ import { McpPool } from "../mcp/pool.js";
6
+ import type { SandboxBackend } from "../sandbox/types.js";
7
+ import { type HarnessClient, type HarnessClientOptions } from "./ws-client.js";
8
+ export interface HarnessServiceOptions {
9
+ readonly url: string;
10
+ readonly token: HarnessClientOptions["token"];
11
+ /** Where workspaces (and their records) live, and the plugins' data. */
12
+ readonly paths: {
13
+ readonly sandboxes: string;
14
+ readonly pluginData: string;
15
+ };
16
+ /** The environment of MCP stdio servers: the allowlisted base, with HOME and TMPDIR. */
17
+ readonly childEnv: Readonly<Record<string, string>>;
18
+ /** Vault-backed model calls (the gates service's client). */
19
+ readonly modelGate: ModelGate;
20
+ /** Models served without the vault; a harness process has none (`useVaultModel`). */
21
+ readonly modelProvider?: ModelProvider;
22
+ readonly useVaultModel: boolean;
23
+ /** Remote MCP servers (`openMcp`) and keyed cancels; the gates service's client. */
24
+ readonly toolGate: Pick<ToolGate, "recovers" | "cancel" | "openMcp">;
25
+ /**
26
+ * Authorizes a remote MCP server opened in this process. A harness process has no vault: it
27
+ * opens remote servers through the Tool Gate, and refuses them without one.
28
+ */
29
+ readonly authorize?: ConstructorParameters<typeof McpPool>[0]["authorize"];
30
+ /** Default: the virtual backend under `paths.sandboxes`. */
31
+ readonly sandboxBackends?: readonly SandboxBackend[];
32
+ /** Delete the workspaces on stop (an ephemeral Runtime's loopback harness). */
33
+ readonly ephemeral?: boolean;
34
+ readonly logger: HarnessClientOptions["logger"];
35
+ readonly name?: string;
36
+ readonly version?: string;
37
+ readonly onGrant?: HarnessOptions["onGrant"];
38
+ readonly backoff?: HarnessClientOptions["backoff"];
39
+ readonly liveness?: HarnessClientOptions["liveness"];
40
+ }
41
+ export interface HarnessService {
42
+ readonly client: HarnessClient;
43
+ /** Stops leasing and gives runs back (at most `waitMs`), then closes MCP and sandboxes. */
44
+ stop(waitMs?: number): Promise<void>;
45
+ }
46
+ export declare function startHarnessService(options: HarnessServiceOptions): HarnessService;
@@ -0,0 +1,179 @@
1
+ /**
2
+ * A harness service (F6.2): a harness that runs the Tenant's segments and serves its workspaces
3
+ * over the Harness API, with its own MCP pool and SandboxManager, and no store. `main.ts` runs
4
+ * one as `--service harness`; a Tenant opened with `harness: "ws"` (tests) runs one in its own
5
+ * process over a loopback WebSocket.
6
+ *
7
+ * It learns its Tenant and the sandbox backend preference from core's answer to `hello`, and
8
+ * only then creates its SandboxManager: workspace keys are scoped by the Tenant. The manager
9
+ * keeps its compute records in `<sandboxes>/records.json` and claims its events from core
10
+ * (`event`): for the run that made the call, or the workspace request core sent.
11
+ */
12
+ import { join } from "node:path";
13
+ import { HarnessApiError, } from "@nylorun/core/harness-api";
14
+ import { McpPool } from "../mcp/pool.js";
15
+ import { SandboxManager } from "../sandbox/manager.js";
16
+ import { fileSandboxRecords, workspacePrefix } from "../sandbox/records.js";
17
+ import { defaultSandboxBackends } from "../sandbox/select.js";
18
+ import { harnessExecutors } from "./executors.js";
19
+ import { connectHarness } from "./ws-client.js";
20
+ /** Which request runs each sandbox tool. */
21
+ const METHODS = {
22
+ bash: "workspace.exec",
23
+ read: "workspace.read",
24
+ grep: "workspace.read",
25
+ glob: "workspace.read",
26
+ write: "workspace.write",
27
+ edit: "workspace.write",
28
+ };
29
+ /** How often the harness closes MCP connections idle past their timeout. */
30
+ const MCP_SWEEP_MS = 60_000;
31
+ /** The SandboxManager's view of the session a workspace request acts for. */
32
+ function refOf(session) {
33
+ return {
34
+ id: session.ownerId,
35
+ activeTurnId: session.activeTurnId,
36
+ ...(session.sandboxId === undefined ? {} : { sandboxId: session.sandboxId }),
37
+ };
38
+ }
39
+ const noModel = async () => {
40
+ throw new Error("This harness calls models only through the gates service");
41
+ };
42
+ export function startHarnessService(options) {
43
+ const { logger } = options;
44
+ const backends = options.sandboxBackends ?? defaultSandboxBackends({ root: options.paths.sandboxes });
45
+ let tenantId = "";
46
+ let manager;
47
+ let client;
48
+ const sandbox = () => {
49
+ if (!manager)
50
+ throw new HarnessApiError("unavailable", "The harness has not said hello yet");
51
+ return manager;
52
+ };
53
+ const pool = new McpPool({
54
+ pluginData: options.paths.pluginData,
55
+ childEnv: options.childEnv,
56
+ authorize: options.authorize ??
57
+ (async () => {
58
+ throw new Error("This harness opens remote MCP servers only through the gates service");
59
+ }),
60
+ ...(options.toolGate.openMcp ? { openRemote: (server) => options.toolGate.openMcp(server) } : {}),
61
+ });
62
+ const sweep = setInterval(() => void pool.sweep().catch(() => undefined), MCP_SWEEP_MS);
63
+ sweep.unref();
64
+ const executors = harnessExecutors({
65
+ model: {
66
+ get tenantId() {
67
+ return tenantId;
68
+ },
69
+ modelGate: options.modelGate,
70
+ modelProvider: options.modelProvider ?? noModel,
71
+ useVaultModel: options.useVaultModel,
72
+ },
73
+ toolGate: options.toolGate,
74
+ mcp: pool,
75
+ sandbox,
76
+ });
77
+ const serve = async (method, params, signal) => {
78
+ switch (method) {
79
+ case "workspace.read":
80
+ case "workspace.write":
81
+ case "workspace.exec": {
82
+ if (method === "workspace.read" && "list" in params) {
83
+ const call = params;
84
+ return sandbox().listFiles(refOf(call.session), { id: "sandbox", type: "agent", sandbox: call.spec, tools: [] }, call.list.dir, call.list.maxEntries, signal);
85
+ }
86
+ if (method === "workspace.read" && "bytes" in params) {
87
+ const call = params;
88
+ const read = await sandbox().readBytes(refOf(call.session), { id: "sandbox", type: "agent", sandbox: call.spec, tools: [] }, call.bytes.path, call.bytes.maxBytes, signal);
89
+ return read.kind === "read"
90
+ ? { kind: "read", path: read.path, base64: Buffer.from(read.bytes).toString("base64") }
91
+ : read;
92
+ }
93
+ const call = params;
94
+ if (METHODS[call.tool] !== method)
95
+ throw new HarnessApiError("invalid", `${method} does not run ${call.tool}`);
96
+ return sandbox().run(refOf(call.session), { id: "sandbox", type: "agent", sandbox: call.spec, tools: [] }, call.tool, call.input, signal);
97
+ }
98
+ case "workspace.report":
99
+ return sandbox().report();
100
+ case "workspace.sweep": {
101
+ const { now } = params;
102
+ await sandbox().stopIdle(now);
103
+ return { workspaces: await sandbox().workspaces() };
104
+ }
105
+ case "workspace.remove": {
106
+ const { keys, sandboxIds, all } = params;
107
+ const workspaces = sandbox();
108
+ if (all) {
109
+ await workspaces.reconcile(() => false, () => false);
110
+ for (const record of await workspaces.workspaces())
111
+ await workspaces.remove(record.key);
112
+ }
113
+ for (const key of keys ?? []) {
114
+ if (!key.startsWith(workspacePrefix(tenantId)))
115
+ throw new HarnessApiError("invalid", `${key} is not a workspace of this Tenant`);
116
+ await workspaces.remove(key);
117
+ }
118
+ for (const id of sandboxIds ?? [])
119
+ await workspaces.removeSandbox(id);
120
+ return {};
121
+ }
122
+ default:
123
+ throw new HarnessApiError("invalid", `Unknown request ${method}`);
124
+ }
125
+ };
126
+ client = connectHarness({
127
+ url: options.url,
128
+ token: options.token,
129
+ logger,
130
+ executors,
131
+ serve,
132
+ name: options.name ?? "harness",
133
+ ...(options.version ? { version: options.version } : {}),
134
+ capabilities: { workspace: { backends: backends.map((backend) => backend.name) } },
135
+ onHello(answer) {
136
+ tenantId = answer.tenantId;
137
+ manager ??= new SandboxManager({
138
+ scope: answer.tenantId,
139
+ records: fileSandboxRecords(join(options.paths.sandboxes, "records.json")),
140
+ backends,
141
+ preference: answer.sandbox.backend ?? "auto",
142
+ ephemeral: options.ephemeral === true,
143
+ // A claim core refuses (its run ended, the session is gone) is dropped, as core's own
144
+ // SandboxManager drops an event it cannot write.
145
+ emit: async (sessionId, turnId, type, payload, meta) => {
146
+ const channel = client.channel();
147
+ if (!channel)
148
+ return;
149
+ await channel
150
+ .request("event", {
151
+ ...(meta.claim === undefined ? {} : { runId: meta.claim }),
152
+ sessionId,
153
+ turnId,
154
+ type,
155
+ payload,
156
+ ...(meta.record ? { record: meta.record } : {}),
157
+ })
158
+ .catch((error) => logger.warn("harness sandbox event not claimed", {
159
+ sessionId,
160
+ type,
161
+ message: error instanceof Error ? error.message : String(error),
162
+ }));
163
+ },
164
+ });
165
+ },
166
+ ...(options.onGrant ? { onGrant: options.onGrant } : {}),
167
+ ...(options.backoff ? { backoff: options.backoff } : {}),
168
+ ...(options.liveness ? { liveness: options.liveness } : {}),
169
+ });
170
+ return {
171
+ client,
172
+ async stop(waitMs) {
173
+ clearInterval(sweep);
174
+ await client.stop(waitMs);
175
+ await pool.close();
176
+ await manager?.close();
177
+ },
178
+ };
179
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * A harness's connection to core (F6.2): a WebSocket to the Harness API listener, with the
3
+ * harness credential (`Authorization: Bearer`, `NYLORUN_HARNESS_TOKEN`) and the API version
4
+ * header. Each connection runs one `createHarness`: it says `hello`, leases runs, and answers
5
+ * core's `workspace.*` requests (`serve`). When the connection is lost its runs stop (core gives
6
+ * their segments to the next advance, which resumes them by replay) and the client connects
7
+ * again after a jittered backoff of 0.5 to 10 s.
8
+ */
9
+ import { type HarnessChannel, type RequestHandler } from "@nylorun/core/harness-api";
10
+ import { type HarnessOptions } from "@nylorun/harness/api";
11
+ import { type WsPortOptions } from "./ws-port.js";
12
+ export interface HarnessClientOptions extends Omit<HarnessOptions, "channel" | "logger"> {
13
+ /** The Harness API, e.g. `ws://runtime:4200/nylorun/harness/v1` (`NYLORUN_HARNESS_URL`). */
14
+ readonly url: string;
15
+ /**
16
+ * `NYLORUN_HARNESS_TOKEN`, or, for a pod sandbox's engine (F7.2), a function that answers
17
+ * a current host token before each connection (it renews, or joins again).
18
+ */
19
+ readonly token: string | (() => Promise<string>);
20
+ readonly logger: {
21
+ info(message: string, fields?: Record<string, unknown>): void;
22
+ warn(message: string, fields?: Record<string, unknown>): void;
23
+ };
24
+ /** Answers core's requests (`workspace.*`). */
25
+ readonly serve?: RequestHandler;
26
+ /** Reconnect backoff bounds. Default 500 ms to 10 s. */
27
+ readonly backoff?: {
28
+ readonly minMs: number;
29
+ readonly maxMs: number;
30
+ };
31
+ readonly liveness?: Omit<WsPortOptions, "ping">;
32
+ }
33
+ export interface HarnessClient {
34
+ /** Resolves once the first connection's `hello` is answered. */
35
+ readonly ready: Promise<void>;
36
+ /** Connected, and `hello` answered. */
37
+ readonly connected: boolean;
38
+ /** The current connection's channel, while there is one. */
39
+ channel(): HarnessChannel | undefined;
40
+ /** Stops leasing, gives its runs back (at most `waitMs`), and closes the connection. */
41
+ stop(waitMs?: number): Promise<void>;
42
+ }
43
+ export declare function connectHarness(options: HarnessClientOptions): HarnessClient;