@nylorun/runtime 0.23.0-beta → 0.25.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 (301) hide show
  1. package/CHANGELOG.md +252 -0
  2. package/README.md +87 -28
  3. package/dist/adapters/execution/restate.js +1 -1
  4. package/dist/adapters/replication/pgoutput.d.ts +2 -0
  5. package/dist/adapters/replication/pgoutput.js +6 -3
  6. package/dist/adapters/sandbox/local.d.ts +9 -1
  7. package/dist/adapters/sandbox/local.js +16 -4
  8. package/dist/adapters/sandbox/virtual.js +4 -1
  9. package/dist/api/a2a/card.d.ts +11 -1
  10. package/dist/api/a2a/card.js +26 -2
  11. package/dist/api/a2a/endpoint.js +9 -3
  12. package/dist/api/a2a/protocol.d.ts +25 -2
  13. package/dist/api/a2a/protocol.js +20 -3
  14. package/dist/api/a2a/routes.d.ts +6 -1
  15. package/dist/api/a2a/routes.js +67 -9
  16. package/dist/api/a2a/tasks.js +2 -1
  17. package/dist/api/ag-ui/endpoint.js +8 -3
  18. package/dist/api/ag-ui/history.d.ts +6 -1
  19. package/dist/api/ag-ui/history.js +39 -3
  20. package/dist/api/ag-ui/routes.d.ts +4 -1
  21. package/dist/api/ag-ui/routes.js +63 -20
  22. package/dist/api/ag-ui/translate.js +3 -2
  23. package/dist/api/components.d.ts +5327 -1091
  24. package/dist/api/components.js +40 -16
  25. package/dist/api/gate/keys.d.ts +2 -1
  26. package/dist/api/gate/keys.js +14 -1
  27. package/dist/api/gate/routes.d.ts +6 -0
  28. package/dist/api/gate/routes.js +52 -3
  29. package/dist/api/http/app.js +17 -6
  30. package/dist/api/http/body.d.ts +3 -3
  31. package/dist/api/http/body.js +7 -7
  32. package/dist/api/http/define.d.ts +6 -0
  33. package/dist/api/http/define.js +13 -1
  34. package/dist/api/http/respond.js +1 -1
  35. package/dist/api/http/routes/artifacts.js +35 -8
  36. package/dist/api/http/routes/coverage.d.ts +9 -0
  37. package/dist/api/http/routes/coverage.js +34 -0
  38. package/dist/api/http/routes/environment-manifests.d.ts +9 -0
  39. package/dist/api/http/routes/environment-manifests.js +118 -0
  40. package/dist/api/http/routes/environments.d.ts +11 -0
  41. package/dist/api/http/routes/environments.js +332 -0
  42. package/dist/api/http/routes/mcp.d.ts +1 -1
  43. package/dist/api/http/routes/mcp.js +2 -2
  44. package/dist/api/http/routes/me.js +1 -1
  45. package/dist/api/http/routes/reads.d.ts +94 -9
  46. package/dist/api/http/routes/reads.js +54 -5
  47. package/dist/api/http/routes/sessions.js +24 -9
  48. package/dist/api/http/routes/tenant.js +65 -15
  49. package/dist/api/inbound-files.d.ts +65 -0
  50. package/dist/api/inbound-files.js +115 -0
  51. package/dist/api/openapi.js +48 -28
  52. package/dist/artifacts/disk.d.ts +59 -0
  53. package/dist/artifacts/disk.js +569 -0
  54. package/dist/artifacts/export.d.ts +1 -1
  55. package/dist/artifacts/export.js +2 -3
  56. package/dist/artifacts/folders.d.ts +6 -3
  57. package/dist/artifacts/folders.js +17 -12
  58. package/dist/artifacts/media-types.d.ts +15 -0
  59. package/dist/artifacts/media-types.js +56 -4
  60. package/dist/artifacts/parts.d.ts +12 -3
  61. package/dist/artifacts/parts.js +15 -4
  62. package/dist/artifacts/service.d.ts +2 -1
  63. package/dist/artifacts/service.js +6 -1
  64. package/dist/artifacts/workspace.d.ts +5 -5
  65. package/dist/artifacts/workspace.js +7 -19
  66. package/dist/core/flow-host.d.ts +3 -4
  67. package/dist/core/flow-host.js +5 -2
  68. package/dist/core/provider.d.ts +3 -2
  69. package/dist/core/provider.js +5 -37
  70. package/dist/core/sandbox-routes.d.ts +2 -21
  71. package/dist/core/sandbox-routes.js +11 -44
  72. package/dist/core/turn-manifest.d.ts +3 -2
  73. package/dist/core/turn-manifest.js +29 -5
  74. package/dist/execution/types.d.ts +1 -1
  75. package/dist/gates/contract.d.ts +3 -24
  76. package/dist/gates/contract.js +1 -36
  77. package/dist/gates/egress-credentials.d.ts +9 -0
  78. package/dist/gates/egress-credentials.js +11 -4
  79. package/dist/gates/egress.d.ts +22 -1
  80. package/dist/gates/egress.js +116 -10
  81. package/dist/gates/fixture-model.d.ts +28 -0
  82. package/dist/gates/fixture-model.js +92 -0
  83. package/dist/gates/handler.js +4 -4
  84. package/dist/gates/http-tool.d.ts +9 -6
  85. package/dist/gates/http-tool.js +11 -17
  86. package/dist/gates/in-process.d.ts +5 -0
  87. package/dist/gates/in-process.js +12 -1
  88. package/dist/gates/journal.d.ts +30 -0
  89. package/dist/gates/journal.js +51 -0
  90. package/dist/gates/mcp-handler.d.ts +4 -0
  91. package/dist/gates/mcp-handler.js +7 -4
  92. package/dist/gates/meter.d.ts +10 -2
  93. package/dist/gates/meter.js +20 -5
  94. package/dist/gates/policy.d.ts +49 -0
  95. package/dist/gates/policy.js +132 -0
  96. package/dist/gates/tenant-vaults.d.ts +7 -7
  97. package/dist/gates/tenant-vaults.js +5 -5
  98. package/dist/gates/tool-calls.d.ts +10 -5
  99. package/dist/gates/tool-calls.js +28 -10
  100. package/dist/gates/tool-client.d.ts +8 -0
  101. package/dist/gates/tool-client.js +27 -11
  102. package/dist/gates/tool-contract.d.ts +11 -69
  103. package/dist/gates/tool-contract.js +1 -85
  104. package/dist/gates/tool-gate.d.ts +1 -1
  105. package/dist/harness/calls.d.ts +14 -7
  106. package/dist/harness/calls.js +30 -24
  107. package/dist/harness/executors.js +12 -9
  108. package/dist/harness/main.js +24 -5
  109. package/dist/harness/pod.d.ts +39 -0
  110. package/dist/harness/pod.js +46 -0
  111. package/dist/harness/service.d.ts +0 -1
  112. package/dist/harness/service.js +17 -9
  113. package/dist/harness/setup.d.ts +29 -0
  114. package/dist/harness/setup.js +174 -0
  115. package/dist/harness/ws-client.js +3 -2
  116. package/dist/harness/ws-port.d.ts +1 -5
  117. package/dist/harness/ws-port.js +1 -5
  118. package/dist/harness-api/in-process.js +3 -3
  119. package/dist/harness-api/record.d.ts +15 -1
  120. package/dist/harness-api/record.js +180 -50
  121. package/dist/harness-api/redact.d.ts +3 -0
  122. package/dist/harness-api/redact.js +45 -0
  123. package/dist/harness-api/server.d.ts +35 -4
  124. package/dist/harness-api/server.js +130 -17
  125. package/dist/harness-api/start.d.ts +10 -3
  126. package/dist/harness-api/start.js +40 -10
  127. package/dist/harness-api/tool-journal.d.ts +105 -0
  128. package/dist/harness-api/tool-journal.js +132 -0
  129. package/dist/harness-api/tool-status.d.ts +13 -0
  130. package/dist/harness-api/tool-status.js +26 -0
  131. package/dist/harness-api/workspace.d.ts +12 -7
  132. package/dist/harness-api/workspace.js +47 -25
  133. package/dist/harness-api/ws-server.d.ts +11 -3
  134. package/dist/harness-api/ws-server.js +94 -15
  135. package/dist/host/gates.d.ts +2 -1
  136. package/dist/host/gates.js +10 -2
  137. package/dist/host/main.js +54 -5
  138. package/dist/host/operate.js +67 -5
  139. package/dist/host/stack-config.d.ts +64 -0
  140. package/dist/host/stack-config.js +62 -2
  141. package/dist/keys/client.js +4 -1
  142. package/dist/keys/keys.d.ts +13 -6
  143. package/dist/keys/keys.js +9 -8
  144. package/dist/management-openapi.json +1264 -2171
  145. package/dist/mcp/connect.d.ts +26 -22
  146. package/dist/mcp/connect.js +30 -34
  147. package/dist/mcp/pool.d.ts +3 -6
  148. package/dist/mcp/pool.js +14 -28
  149. package/dist/mcp/preview.d.ts +3 -3
  150. package/dist/mcp/preview.js +10 -21
  151. package/dist/mcp/snapshot.d.ts +16 -24
  152. package/dist/mcp/snapshot.js +33 -45
  153. package/dist/model/capabilities.d.ts +26 -0
  154. package/dist/model/capabilities.js +44 -0
  155. package/dist/model/catalog.d.ts +34 -3
  156. package/dist/model/catalog.js +38 -2
  157. package/dist/model/models.d.ts +2 -0
  158. package/dist/model/models.js +1 -1
  159. package/dist/model/native-blocks.d.ts +35 -0
  160. package/dist/model/native-blocks.js +170 -0
  161. package/dist/model/pi-model.js +114 -94
  162. package/dist/openapi.json +12822 -3943
  163. package/dist/reads/types.d.ts +16 -3
  164. package/dist/record/append.d.ts +3 -1
  165. package/dist/record/append.js +1 -0
  166. package/dist/record/envelope.d.ts +5 -1
  167. package/dist/record/envelope.js +7 -3
  168. package/dist/record/environment.d.ts +25 -0
  169. package/dist/record/environment.js +44 -0
  170. package/dist/record/index.d.ts +1 -1
  171. package/dist/record/index.js +1 -1
  172. package/dist/sandbox/disk-fs.d.ts +95 -0
  173. package/dist/sandbox/disk-fs.js +206 -0
  174. package/dist/sandbox/join.d.ts +17 -18
  175. package/dist/sandbox/join.js +46 -15
  176. package/dist/sandbox/manager.d.ts +47 -21
  177. package/dist/sandbox/manager.js +162 -107
  178. package/dist/sandbox/placement.d.ts +14 -13
  179. package/dist/sandbox/placement.js +31 -14
  180. package/dist/sandbox/pods/client.d.ts +9 -2
  181. package/dist/sandbox/pods/lifecycle.d.ts +36 -27
  182. package/dist/sandbox/pods/lifecycle.js +29 -26
  183. package/dist/sandbox/pods/reconcile.js +18 -12
  184. package/dist/sandbox/pods/spec.d.ts +15 -13
  185. package/dist/sandbox/pods/spec.js +17 -20
  186. package/dist/sandbox/records.d.ts +2 -2
  187. package/dist/sandbox/records.js +3 -3
  188. package/dist/sandbox/resolve.d.ts +37 -27
  189. package/dist/sandbox/resolve.js +73 -41
  190. package/dist/sandbox/runtime-tools.d.ts +9 -0
  191. package/dist/sandbox/runtime-tools.js +16 -0
  192. package/dist/sandbox/session-sandbox.d.ts +4 -6
  193. package/dist/sandbox/session-sandbox.js +47 -94
  194. package/dist/sandbox/setup.d.ts +64 -0
  195. package/dist/sandbox/setup.js +160 -0
  196. package/dist/sandbox/share.d.ts +21 -26
  197. package/dist/sandbox/share.js +36 -63
  198. package/dist/sandbox/skills.d.ts +8 -6
  199. package/dist/sandbox/skills.js +24 -23
  200. package/dist/sandbox/tenant-config.d.ts +12 -14
  201. package/dist/sandbox/tenant-config.js +12 -17
  202. package/dist/sandbox/types.d.ts +6 -0
  203. package/dist/store/postgres/drizzle/0020_environments.sql +63 -0
  204. package/dist/store/postgres/drizzle/0021_disk_versions.sql +7 -0
  205. package/dist/store/postgres/drizzle/0022_model_usage_timing.sql +5 -0
  206. package/dist/store/postgres/drizzle/0023_artifact_pages.sql +1 -0
  207. package/dist/store/postgres/drizzle/0024_flat_manifest.sql +7 -0
  208. package/dist/store/postgres/drizzle/0025_environment_manifests.sql +14 -0
  209. package/dist/store/postgres/drizzle/0026_environment_forks.sql +1 -0
  210. package/dist/store/postgres/drizzle/0027_environment_image_digest.sql +1 -0
  211. package/dist/store/postgres/drizzle/meta/_journal.json +56 -0
  212. package/dist/store/postgres/reads.js +123 -17
  213. package/dist/store/postgres/record-writer.d.ts +4 -4
  214. package/dist/store/postgres/record-writer.js +9 -9
  215. package/dist/store/postgres/record.d.ts +7 -0
  216. package/dist/store/postgres/record.js +23 -2
  217. package/dist/store/postgres/schema.d.ts +1126 -736
  218. package/dist/store/postgres/schema.js +136 -73
  219. package/dist/store/postgres/store.js +195 -67
  220. package/dist/store/types.d.ts +165 -50
  221. package/dist/tenant/advance.d.ts +7 -6
  222. package/dist/tenant/advance.js +118 -42
  223. package/dist/tenant/artifact-tool.d.ts +1 -1
  224. package/dist/tenant/artifact-tool.js +46 -26
  225. package/dist/tenant/commands.d.ts +15 -2
  226. package/dist/tenant/commands.js +115 -14
  227. package/dist/tenant/context.d.ts +56 -24
  228. package/dist/tenant/context.js +4 -15
  229. package/dist/tenant/effects.d.ts +1 -1
  230. package/dist/tenant/effects.js +44 -29
  231. package/dist/tenant/environment-manifests.d.ts +57 -0
  232. package/dist/tenant/environment-manifests.js +200 -0
  233. package/dist/tenant/environments.d.ts +174 -0
  234. package/dist/tenant/environments.js +1044 -0
  235. package/dist/tenant/harness-tokens.d.ts +21 -0
  236. package/dist/tenant/harness-tokens.js +61 -0
  237. package/dist/tenant/host-token.d.ts +1 -1
  238. package/dist/tenant/host-token.js +2 -4
  239. package/dist/tenant/identity-file.d.ts +4 -4
  240. package/dist/tenant/identity-file.js +17 -8
  241. package/dist/tenant/issuers.js +3 -3
  242. package/dist/tenant/model-setting.d.ts +14 -0
  243. package/dist/tenant/model-setting.js +51 -0
  244. package/dist/tenant/principals.js +1 -1
  245. package/dist/tenant/reset.js +3 -3
  246. package/dist/tenant/runtime.d.ts +6 -1
  247. package/dist/tenant/runtime.js +7 -1
  248. package/dist/tenant/session.d.ts +3 -3
  249. package/dist/tenant/session.js +0 -14
  250. package/dist/tenant/sessions.d.ts +18 -14
  251. package/dist/tenant/sessions.js +99 -143
  252. package/dist/tenant/signing-keys.d.ts +5 -1
  253. package/dist/tenant/skill-tool.d.ts +3 -3
  254. package/dist/tenant/skill-tool.js +6 -11
  255. package/dist/tenant/status.js +3 -3
  256. package/dist/tenant/sweep.js +13 -7
  257. package/dist/tenant/tool-results.js +1 -1
  258. package/dist/tenant/tool-search.d.ts +7 -11
  259. package/dist/tenant/tool-search.js +8 -5
  260. package/dist/tenant/transcript.d.ts +245 -36
  261. package/dist/tenant/transcript.js +64 -12
  262. package/dist/tenant/turn-timing.d.ts +43 -0
  263. package/dist/tenant/turn-timing.js +39 -0
  264. package/dist/tenant/types.d.ts +2 -0
  265. package/dist/traces/ids.d.ts +41 -0
  266. package/dist/traces/ids.js +81 -0
  267. package/dist/traces/key.d.ts +6 -0
  268. package/dist/traces/key.js +37 -0
  269. package/dist/traces/map.d.ts +113 -0
  270. package/dist/traces/map.js +289 -0
  271. package/dist/traces/options.d.ts +7 -0
  272. package/dist/traces/options.js +1 -0
  273. package/dist/traces/otlp.d.ts +42 -0
  274. package/dist/traces/otlp.js +84 -0
  275. package/dist/traces/projector.d.ts +58 -0
  276. package/dist/traces/projector.js +200 -0
  277. package/dist/traces/read.d.ts +23 -0
  278. package/dist/traces/read.js +16 -0
  279. package/dist/vault/coverage.d.ts +26 -0
  280. package/dist/vault/coverage.js +62 -0
  281. package/dist/vault/error.d.ts +11 -1
  282. package/dist/vault/error.js +5 -1
  283. package/dist/vault/host-model.d.ts +4 -1
  284. package/dist/vault/host-model.js +10 -2
  285. package/dist/vault/service.d.ts +92 -48
  286. package/dist/vault/service.js +462 -376
  287. package/dist/vault/sources.d.ts +5 -16
  288. package/dist/vault/sources.js +6 -8
  289. package/dist/version.d.ts +1 -1
  290. package/dist/version.js +2 -2
  291. package/package.json +13 -13
  292. package/dist/api/http/routes/sandboxes.d.ts +0 -9
  293. package/dist/api/http/routes/sandboxes.js +0 -153
  294. package/dist/api/http/routes/vaults.d.ts +0 -10
  295. package/dist/api/http/routes/vaults.js +0 -139
  296. package/dist/record/sandbox.d.ts +0 -25
  297. package/dist/record/sandbox.js +0 -43
  298. package/dist/sandbox/capability.d.ts +0 -3
  299. package/dist/sandbox/capability.js +0 -10
  300. package/dist/tenant/sandboxes.d.ts +0 -123
  301. package/dist/tenant/sandboxes.js +0 -566
@@ -0,0 +1,26 @@
1
+ /**
2
+ * How a tool row of the effect journal reads (R5.1, R5.2), from its body alone: shared by the
3
+ * journal (`tool-journal.ts`) and the session reads (`/calls/tool`).
4
+ */
5
+ import { durationBetween } from "../tenant/transcript.js";
6
+ /** The codes of a call the Runtime ended rather than the tool: its status is `failed`. */
7
+ const RUNTIME_ENDED = new Set(["tool.disabled", "tool.unavailable", "tool.interrupted", "tool.denied"]);
8
+ /** How a tool row ended, as `tool.completed` and `/calls/tool` say it. */
9
+ export function toolCallStatus(row) {
10
+ if (row.status === "cancelled")
11
+ return "cancelled";
12
+ if (row.status === "held")
13
+ return "held";
14
+ if (row.status === "uncertain")
15
+ return "uncertain";
16
+ if (row.status !== "completed")
17
+ return "invoking";
18
+ const value = row.outcome?.value;
19
+ return value?.kind === "failed" && typeof value.code === "string" && RUNTIME_ENDED.has(value.code)
20
+ ? "failed"
21
+ : "completed";
22
+ }
23
+ /** From the first send to the outcome, or undefined when the call never ran or has not ended. */
24
+ export function toolCallDuration(row) {
25
+ return durationBetween(row.startedAt, row.settledAt);
26
+ }
@@ -1,5 +1,6 @@
1
- import type { CapabilityManifest, SandboxToolName } from "@nylorun/core/define";
1
+ import type { SandboxManifest, SandboxToolName } from "@nylorun/core/define";
2
2
  import type { SandboxManager, SandboxSessionRef } from "../sandbox/manager.js";
3
+ import { type DiskOp } from "../sandbox/disk-fs.js";
3
4
  import type { SandboxSelectionReport } from "../sandbox/select.js";
4
5
  import type { SandboxToolOutcome } from "../sandbox/tools.js";
5
6
  import type { SessionStore } from "../store/types.js";
@@ -12,11 +13,13 @@ export type WorkspaceReport = SandboxSelectionReport & {
12
13
  type Exists = (id: string) => boolean | Promise<boolean>;
13
14
  export interface WorkspacePort {
14
15
  /** Runs one sandbox tool in the workspace of `session`. */
15
- run(session: SandboxSessionRef, capability: CapabilityManifest, toolName: SandboxToolName, input: unknown, signal: AbortSignal): Promise<SandboxToolOutcome>;
16
+ run(session: SandboxSessionRef, sandbox: SandboxManifest | undefined, toolName: SandboxToolName, input: unknown, signal: AbortSignal): Promise<SandboxToolOutcome>;
16
17
  /** Lists the files under `dir` of the workspace of `session` (the turn-end export, F8.2). */
17
- listFiles(session: SandboxSessionRef, capability: CapabilityManifest, dir: string, maxEntries: number, signal: AbortSignal): ReturnType<SandboxManager["listFiles"]>;
18
+ listFiles(session: SandboxSessionRef, sandbox: SandboxManifest | undefined, dir: string, maxEntries: number, signal: AbortSignal): ReturnType<SandboxManager["listFiles"]>;
18
19
  /** Reads a file of the workspace of `session` as bytes, at most `maxBytes` (`save_artifact`). */
19
- readBytes(session: SandboxSessionRef, capability: CapabilityManifest, path: string, maxBytes: number, signal: AbortSignal): ReturnType<SandboxManager["readBytes"]>;
20
+ readBytes(session: SandboxSessionRef, sandbox: SandboxManifest | undefined, path: string, maxBytes: number, signal: AbortSignal): ReturnType<SandboxManager["readBytes"]>;
21
+ /** One operation on the disk of the workspace of `session` (R2d step 3). */
22
+ disk(session: SandboxSessionRef, sandbox: SandboxManifest | undefined, op: DiskOp, signal: AbortSignal): ReturnType<SandboxManager["disk"]>;
20
23
  report(): Promise<WorkspaceReport>;
21
24
  /** One Tenant sweep pass: stop idle workspaces, remove those whose owner is gone. */
22
25
  sweep(options: {
@@ -24,8 +27,8 @@ export interface WorkspacePort {
24
27
  sessionExists: Exists;
25
28
  sandboxExists: Exists;
26
29
  }): Promise<void>;
27
- /** Deletes sandbox resource `sandboxId`'s workspace (the resource was deleted). */
28
- removeSandbox(sandboxId: string): Promise<void>;
30
+ /** Deletes sandbox resource `environmentId`'s workspace (the resource was deleted). */
31
+ removeSandbox(environmentId: string): Promise<void>;
29
32
  /** Deletes every workspace with its files (a sandboxes reset). */
30
33
  removeAll(): Promise<void>;
31
34
  close(): Promise<void>;
@@ -40,6 +43,8 @@ export interface RemoteWorkspaceOptions {
40
43
  readonly preference: string;
41
44
  /** The pod sandbox whose host serves these workspaces (F7.2). */
42
45
  readonly pod?: string;
46
+ /** With `pod`: the oldest host epoch that may serve them (`SandboxSessionRef.hostEpoch`). */
47
+ readonly minEpoch?: number;
43
48
  }
44
49
  /** The workspaces of the harness that serves them. */
45
50
  export declare function remoteWorkspace(options: RemoteWorkspaceOptions): WorkspacePort;
@@ -48,6 +53,6 @@ export declare function remoteWorkspace(options: RemoteWorkspaceOptions): Worksp
48
53
  * everything else goes to `base`. `podOf` says whether a sandbox resource is a pod sandbox.
49
54
  */
50
55
  export declare function withPodWorkspaces(base: WorkspacePort, options: Omit<RemoteWorkspaceOptions, "pod"> & {
51
- readonly isPod: (sandboxId: string) => Promise<boolean>;
56
+ readonly isPod: (environmentId: string) => Promise<boolean>;
52
57
  }): WorkspacePort;
53
58
  export {};
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The workspace capability (F6.2): what core does with the Tenant's sandbox workspaces outside a
2
+ * The workspace feature (F6.2): what core does with the Tenant's sandbox workspaces outside a
3
3
  * run. `ctx.sandbox` is a `WorkspacePort`: the sandbox tool route (`POST
4
4
  * /v1/sessions/:id/sandbox/:tool`), Tenant status, the Tenant sweep, a sandbox resource's deletion and a sandboxes reset.
5
5
  *
@@ -15,21 +15,23 @@
15
15
  * - **Pod sandboxes** (F7.2, `withPodWorkspaces`): a session attached to a pod sandbox has its
16
16
  * workspace in the pod, served by the sandbox's engine (its host connection). Its tool calls,
17
17
  * listings and reads go there whatever serves the other workspaces; with the pod stopped or
18
- * starting they are `409 sandbox_unavailable`.
18
+ * starting they are `409 environment_unavailable`.
19
19
  */
20
20
  import { HarnessApiError } from "@nylorun/core/harness-api";
21
+ import { fromWireAnswer, toWireOp } from "../sandbox/disk-fs.js";
21
22
  import { DEFAULT_SANDBOX_IMAGE } from "../sandbox/policy.js";
22
23
  import { fail } from "../tenant/http.js";
23
24
  import { NoWorkspaceHarness } from "./server.js";
24
25
  /** The Tenant's own SandboxManager. */
25
26
  export function localWorkspace(manager) {
26
27
  return {
27
- run: (session, capability, toolName, input, signal) => manager.run(session, capability, toolName, input, signal),
28
- listFiles: (session, capability, dir, maxEntries, signal) => manager.listFiles(session, capability, dir, maxEntries, signal),
29
- readBytes: (session, capability, path, maxBytes, signal) => manager.readBytes(session, capability, path, maxBytes, signal),
28
+ run: (session, sandbox, toolName, input, signal) => manager.run(session, sandbox, toolName, input, signal),
29
+ listFiles: (session, sandbox, dir, maxEntries, signal) => manager.listFiles(session, sandbox, dir, maxEntries, signal),
30
+ readBytes: (session, sandbox, path, maxBytes, signal) => manager.readBytes(session, sandbox, path, maxBytes, signal),
31
+ disk: (session, sandbox, op, signal) => manager.disk(session, sandbox, op, signal),
30
32
  report: () => manager.report(),
31
33
  sweep: (options) => manager.sweep(options),
32
- removeSandbox: (sandboxId) => manager.removeSandbox(sandboxId),
34
+ removeSandbox: (environmentId) => manager.removeSandbox(environmentId),
33
35
  removeAll: () => manager.reconcile(() => false, () => false),
34
36
  close: () => manager.close(),
35
37
  };
@@ -45,17 +47,19 @@ const METHODS = {
45
47
  function sessionOf(session) {
46
48
  return {
47
49
  ownerId: session.id,
48
- ...(session.sandboxId === undefined ? {} : { sandboxId: session.sandboxId }),
50
+ ...(session.environmentId === undefined ? {} : { environmentId: session.environmentId }),
49
51
  activeTurnId: session.activeTurnId,
50
52
  };
51
53
  }
52
54
  /** The workspaces of the harness that serves them. */
53
55
  export function remoteWorkspace(options) {
54
56
  const { store } = options;
55
- const target = options.pod === undefined ? undefined : { pod: options.pod };
57
+ const target = options.pod === undefined
58
+ ? undefined
59
+ : { pod: options.pod, ...(options.minEpoch === undefined ? {} : { minEpoch: options.minEpoch }) };
56
60
  const unavailable = (error) => error.pod === undefined
57
61
  ? fail(503, error.message, { code: "request_rejected" })
58
- : fail(409, error.message, { code: "sandbox_unavailable" });
62
+ : fail(409, error.message, { code: "environment_unavailable" });
59
63
  /** The harness's answer, or `undefined` when none serves workspaces. */
60
64
  const ask = async (method, params, signal) => {
61
65
  try {
@@ -74,9 +78,9 @@ export function remoteWorkspace(options) {
74
78
  const { workspaces } = answer;
75
79
  const gone = [];
76
80
  for (const record of workspaces) {
77
- const kept = record.sandboxId === undefined
81
+ const kept = record.environmentId === undefined
78
82
  ? await sessionExists(record.sessionId)
79
- : await sandboxExists(record.sandboxId);
83
+ : await sandboxExists(record.environmentId);
80
84
  if (!kept)
81
85
  gone.push(record.key);
82
86
  }
@@ -96,11 +100,11 @@ export function remoteWorkspace(options) {
96
100
  });
97
101
  };
98
102
  return {
99
- async run(session, capability, toolName, input, signal) {
103
+ async run(session, sandbox, toolName, input, signal) {
100
104
  try {
101
105
  return (await options.server.workspace(METHODS[toolName], {
102
106
  session: sessionOf(session),
103
- spec: capability.sandbox ?? {},
107
+ spec: sandbox ?? {},
104
108
  tool: toolName,
105
109
  input: input ?? {},
106
110
  }, signal, target));
@@ -115,9 +119,9 @@ export function remoteWorkspace(options) {
115
119
  throw error;
116
120
  }
117
121
  },
118
- async listFiles(session, capability, dir, maxEntries, signal) {
122
+ async listFiles(session, sandbox, dir, maxEntries, signal) {
119
123
  try {
120
- const answer = (await options.server.workspace("workspace.read", { session: sessionOf(session), spec: capability.sandbox ?? {}, list: { dir, maxEntries } }, signal, target));
124
+ const answer = (await options.server.workspace("workspace.read", { session: sessionOf(session), spec: sandbox ?? {}, list: { dir, maxEntries } }, signal, target));
121
125
  if (answer.kind === "listed")
122
126
  return {
123
127
  kind: "listed",
@@ -135,11 +139,11 @@ export function remoteWorkspace(options) {
135
139
  throw error;
136
140
  }
137
141
  },
138
- async readBytes(session, capability, path, maxBytes, signal) {
142
+ async readBytes(session, sandbox, path, maxBytes, signal) {
139
143
  try {
140
144
  const answer = (await options.server.workspace("workspace.read", {
141
145
  session: sessionOf(session),
142
- spec: capability.sandbox ?? {},
146
+ spec: sandbox ?? {},
143
147
  bytes: { path, maxBytes },
144
148
  }, signal, target));
145
149
  if (answer.kind === "read")
@@ -154,6 +158,19 @@ export function remoteWorkspace(options) {
154
158
  throw error;
155
159
  }
156
160
  },
161
+ async disk(session, sandbox, op, signal) {
162
+ try {
163
+ const answer = (await options.server.workspace("list" in op || "read" in op ? "workspace.read" : "workspace.write", { session: sessionOf(session), spec: sandbox ?? {}, disk: toWireOp(op) }, signal, target));
164
+ return answer.kind === "failed"
165
+ ? { kind: "failed", code: String(answer.code), message: String(answer.message) }
166
+ : fromWireAnswer(answer);
167
+ }
168
+ catch (error) {
169
+ if (error instanceof NoWorkspaceHarness)
170
+ return unavailable(error);
171
+ throw error;
172
+ }
173
+ },
157
174
  async report() {
158
175
  const answer = await ask("workspace.report", {}).catch(() => undefined);
159
176
  if (answer)
@@ -168,12 +185,12 @@ export function remoteWorkspace(options) {
168
185
  };
169
186
  },
170
187
  sweep: (pass) => sweep(pass.sessionExists, pass.sandboxExists, pass.now),
171
- async removeSandbox(sandboxId) {
188
+ async removeSandbox(environmentId) {
172
189
  // With no harness connected, the next sweep removes it: its resource is gone.
173
- await ask("workspace.remove", { sandboxIds: [sandboxId] });
190
+ await ask("workspace.remove", { environmentIds: [environmentId] });
174
191
  await store.tx(async (t) => {
175
192
  for (const row of await t.listSandboxes())
176
- if (row.sandboxId === sandboxId)
193
+ if (row.environmentId === environmentId)
177
194
  await t.delete("sandboxes", row.key);
178
195
  });
179
196
  },
@@ -188,13 +205,18 @@ export function remoteWorkspace(options) {
188
205
  * everything else goes to `base`. `podOf` says whether a sandbox resource is a pod sandbox.
189
206
  */
190
207
  export function withPodWorkspaces(base, options) {
191
- const routed = async (session) => session.sandboxId !== undefined && (await options.isPod(session.sandboxId))
192
- ? remoteWorkspace({ ...options, pod: session.sandboxId })
208
+ const routed = async (session) => session.environmentId !== undefined && (await options.isPod(session.environmentId))
209
+ ? remoteWorkspace({
210
+ ...options,
211
+ pod: session.environmentId,
212
+ ...(session.hostEpoch === undefined ? {} : { minEpoch: session.hostEpoch }),
213
+ })
193
214
  : base;
194
215
  return {
195
216
  ...base,
196
- run: async (session, capability, toolName, input, signal) => (await routed(session)).run(session, capability, toolName, input, signal),
197
- listFiles: async (session, capability, dir, maxEntries, signal) => (await routed(session)).listFiles(session, capability, dir, maxEntries, signal),
198
- readBytes: async (session, capability, path, maxBytes, signal) => (await routed(session)).readBytes(session, capability, path, maxBytes, signal),
217
+ run: async (session, sandbox, toolName, input, signal) => (await routed(session)).run(session, sandbox, toolName, input, signal),
218
+ listFiles: async (session, sandbox, dir, maxEntries, signal) => (await routed(session)).listFiles(session, sandbox, dir, maxEntries, signal),
219
+ readBytes: async (session, sandbox, path, maxBytes, signal) => (await routed(session)).readBytes(session, sandbox, path, maxBytes, signal),
220
+ disk: async (session, sandbox, op, signal) => (await routed(session)).disk(session, sandbox, op, signal),
199
221
  };
200
222
  }
@@ -1,11 +1,14 @@
1
1
  import { type Server } from "node:http";
2
2
  import { type HarnessChannel } from "@nylorun/core/harness-api";
3
+ import { HOST_JOIN_PATH, HOST_RENEW_PATH } from "@nylorun/core/harness-api";
3
4
  import { type WsPortOptions } from "../harness/ws-port.js";
4
5
  import { type HostAuthority } from "../sandbox/join.js";
5
6
  import type { HarnessPeer } from "./server.js";
6
- /** Where a pod's engine exchanges its join token, and renews its host token. */
7
- export declare const HOST_JOIN_PATH = "/nylorun/harness/v1/host/join";
8
- export declare const HOST_RENEW_PATH = "/nylorun/harness/v1/host/renew";
7
+ export { HOST_JOIN_PATH, HOST_RENEW_PATH };
8
+ /** Where a pod's engine reads its setup and reports it (R2d step 2, `sandbox/setup.ts`). */
9
+ export declare const HOST_SETUP_PATH = "/nylorun/harness/v1/host/setup";
10
+ /** Where it reads its input files: `/{index}`, and a folder's file `/{index}/{path}`. */
11
+ export declare const HOST_FILES_PATH = "/nylorun/harness/v1/host/files";
9
12
  /** Attaches a harness's channel to the open Tenant; returns a function that detaches it. */
10
13
  export type HarnessAttach = (channel: HarnessChannel, peer: HarnessPeer) => () => void;
11
14
  export interface HarnessListenerOptions {
@@ -17,6 +20,11 @@ export interface HarnessListenerOptions {
17
20
  readonly token?: string;
18
21
  /** The open Tenant's `attachHarness`, or undefined while there is none. */
19
22
  readonly attach: () => Promise<HarnessAttach | undefined>;
23
+ /**
24
+ * The custom harness a per-harness token is for (R4.1), or undefined. Absent, only the harness
25
+ * token and host tokens are accepted.
26
+ */
27
+ readonly harnessOfToken?: (token: string) => Promise<string | undefined>;
20
28
  /** The open Tenant's host authority (sandbox pods, F7.2), or undefined without one. */
21
29
  readonly hosts?: () => Promise<HostAuthority | undefined>;
22
30
  readonly logger: {
@@ -14,22 +14,31 @@
14
14
  * /nylorun/harness/v1/host/join` (`{ sandboxId, podUid, joinToken }`), renews it with `POST
15
15
  * .../host/renew` (`Authorization: Bearer <host token>`), and upgrades with the host token as
16
16
  * its bearer: that connection hosts its sandbox alone (`HarnessPeer.host`). Both answer
17
- * `{ hostToken, egressToken, sandboxId, epoch, expiresAt, caCertificate }` (the egress CA, R2c);
18
- * every refusal is the same 401. The harness token is never accepted as a host token, nor a host token anywhere but here.
17
+ * `{ hostToken, egressToken, sandboxId, epoch, expiresAt, caCertificate }` (the egress CA, R2c),
18
+ * and a join adds the disk's `setup` (R2d step 2). With its host token the engine also reads and
19
+ * reports its setup (`GET|POST .../host/setup`) and reads its input files (`GET
20
+ * .../host/files/{index}[/{path}]`). Every refusal is the same 401. The harness token is never
21
+ * accepted as a host token, nor a host token anywhere but here.
19
22
  */
20
23
  import { createHash, timingSafeEqual } from "node:crypto";
21
24
  import { createServer } from "node:http";
22
- import { createChannel } from "@nylorun/core/harness-api";
25
+ import { DEFAULT_HARNESS, createChannel } from "@nylorun/core/harness-api";
23
26
  import { WebSocketServer } from "ws";
24
- import { HARNESS_API_HEADER, HARNESS_API_PATH, HARNESS_MAX_PAYLOAD, wsPort, } from "../harness/ws-port.js";
27
+ import { HARNESS_API_HEADER, HARNESS_API_PATH, HARNESS_API_TRANSPORT, HARNESS_MAX_PAYLOAD, HOST_JOIN_PATH, HOST_RENEW_PATH, } from "@nylorun/core/harness-api";
28
+ import { wsPort } from "../harness/ws-port.js";
25
29
  import { tokenType } from "../tenant/jwt.js";
26
30
  import { HOST_TOKEN_TYP } from "../tenant/host-token.js";
27
31
  import { HostAuthError } from "../sandbox/join.js";
28
- /** Where a pod's engine exchanges its join token, and renews its host token. */
29
- export const HOST_JOIN_PATH = `${HARNESS_API_PATH}/host/join`;
30
- export const HOST_RENEW_PATH = `${HARNESS_API_PATH}/host/renew`;
32
+ import { parseSetupReport } from "../sandbox/setup.js";
33
+ export { HOST_JOIN_PATH, HOST_RENEW_PATH };
34
+ /** Where a pod's engine reads its setup and reports it (R2d step 2, `sandbox/setup.ts`). */
35
+ export const HOST_SETUP_PATH = `${HARNESS_API_PATH}/host/setup`;
36
+ /** Where it reads its input files: `/{index}`, and a folder's file `/{index}/{path}`. */
37
+ export const HOST_FILES_PATH = `${HARNESS_API_PATH}/host/files`;
31
38
  /** A join's body is small. */
32
39
  const MAX_JOIN_BODY = 8 * 1024;
40
+ /** A setup report carries the end of a command's output. */
41
+ const MAX_REPORT_BODY = 64 * 1024;
33
42
  function refuse(socket, status, text, headers = {}) {
34
43
  const lines = [
35
44
  `HTTP/1.1 ${status} ${text}`,
@@ -76,12 +85,12 @@ export async function startHarnessListener(options) {
76
85
  });
77
86
  response.end(text);
78
87
  };
79
- const readBody = (request) => new Promise((resolve, reject) => {
88
+ const readBody = (request, max = MAX_JOIN_BODY) => new Promise((resolve, reject) => {
80
89
  let size = 0;
81
90
  const chunks = [];
82
91
  request.on("data", (chunk) => {
83
92
  size += chunk.length;
84
- if (size > MAX_JOIN_BODY) {
93
+ if (size > max) {
85
94
  reject(new HostAuthError(400, "The body is too large"));
86
95
  request.destroy();
87
96
  }
@@ -116,7 +125,61 @@ export async function startHarnessListener(options) {
116
125
  return reply(response, 503, { error: { code: "unavailable", message: "Try again" } });
117
126
  }
118
127
  };
119
- // Plain requests get nothing but the pods' join and renewal: the listener speaks the Harness API.
128
+ /** A pod's setup and input files (R2d step 2), with its host token. */
129
+ const serveSetup = async (request, response, path) => {
130
+ if (!allowed.has(String(request.headers.host ?? "").toLowerCase()))
131
+ return reply(response, 421);
132
+ const authority = await options.hosts?.().catch(() => undefined);
133
+ if (!authority || closing)
134
+ return reply(response, 503, { error: { code: "unavailable", message: "Try again" } });
135
+ const token = bearerOf(request.headers.authorization) ?? "";
136
+ try {
137
+ if (path === HOST_SETUP_PATH) {
138
+ if (request.method === "GET")
139
+ return reply(response, 200, await authority.setup(token));
140
+ if (request.method !== "POST")
141
+ return reply(response, 405, undefined, { allow: "GET, POST" });
142
+ const report = parseSetupReport(JSON.parse(await readBody(request, MAX_REPORT_BODY)));
143
+ if (!report)
144
+ throw new HostAuthError(400, "The report is malformed");
145
+ await authority.reportSetup(token, report);
146
+ return reply(response, 200, {});
147
+ }
148
+ if (request.method !== "GET")
149
+ return reply(response, 405, undefined, { allow: "GET" });
150
+ const [index, ...rest] = path.slice(HOST_FILES_PATH.length + 1).split("/");
151
+ if (!/^\d{1,2}$/.test(index ?? ""))
152
+ return reply(response, 404);
153
+ const file = await authority.setupFile(token, Number(index), rest.length === 0 ? undefined : rest.map((part) => decodeURIComponent(part)).join("/"));
154
+ if (!file)
155
+ return reply(response, 404);
156
+ if (file.kind === "folder")
157
+ return reply(response, 200, { entries: file.entries }, { "x-nylorun-artifact-kind": "folder" });
158
+ response.writeHead(200, {
159
+ connection: "close",
160
+ "cache-control": "no-store",
161
+ "content-type": file.contentType,
162
+ "x-nylorun-artifact-kind": "file",
163
+ });
164
+ for await (const chunk of file.body)
165
+ response.write(chunk);
166
+ response.end();
167
+ }
168
+ catch (error) {
169
+ if (error instanceof HostAuthError) {
170
+ if (error.reason)
171
+ logger.warn("sandbox_host_refused", { path, reason: error.reason });
172
+ return reply(response, error.status, { error: { code: error.status === 401 ? "unauthorized" : "invalid", message: error.message } }, error.status === 401 ? { "www-authenticate": "Bearer" } : {});
173
+ }
174
+ if (error instanceof SyntaxError)
175
+ return reply(response, 400, { error: { code: "invalid", message: "Not JSON" } });
176
+ logger.warn("sandbox_host_failed", { path, message: error instanceof Error ? error.message : String(error) });
177
+ if (response.headersSent)
178
+ return void response.destroy();
179
+ return reply(response, 503, { error: { code: "unavailable", message: "Try again" } });
180
+ }
181
+ };
182
+ // Plain requests get nothing but the pods' join, renewal and setup: the listener speaks the Harness API.
120
183
  const server = createServer((request, response) => {
121
184
  const path = new URL(request.url ?? "/", "http://harness.local").pathname;
122
185
  if (closing)
@@ -125,6 +188,10 @@ export async function startHarnessListener(options) {
125
188
  void serveHost(request, response, path);
126
189
  return;
127
190
  }
191
+ if ((path === HOST_SETUP_PATH || path.startsWith(`${HOST_FILES_PATH}/`)) && options.hosts) {
192
+ void serveSetup(request, response, path);
193
+ return;
194
+ }
128
195
  response.writeHead(426, { connection: "close", upgrade: "websocket" });
129
196
  response.end();
130
197
  });
@@ -140,10 +207,12 @@ export async function startHarnessListener(options) {
140
207
  const bearer = bearerOf(request.headers.authorization);
141
208
  // A pod's host token (F7.2): checked against its sandbox before the upgrade.
142
209
  const hostToken = bearer !== undefined && options.hosts && tokenType(bearer) === HOST_TOKEN_TYP;
143
- if (!hostToken && !bearerMatches(request.headers.authorization, options.token))
210
+ // The harness token is our harness's; any other is a custom harness's own, or refused (R4.1).
211
+ const ours = !hostToken && bearerMatches(request.headers.authorization, options.token);
212
+ if (!hostToken && !ours && (bearer === undefined || !options.harnessOfToken))
144
213
  return refuse(socket, 401, "Unauthorized", { "WWW-Authenticate": "Bearer" });
145
- if (request.headers[HARNESS_API_HEADER] !== "1")
146
- return refuse(socket, 426, "Upgrade Required", { [HARNESS_API_HEADER]: "1" });
214
+ if (request.headers[HARNESS_API_HEADER] !== HARNESS_API_TRANSPORT)
215
+ return refuse(socket, 426, "Upgrade Required", { [HARNESS_API_HEADER]: HARNESS_API_TRANSPORT });
147
216
  const host = hostToken
148
217
  ? options.hosts().then(async (authority) => {
149
218
  if (!authority)
@@ -152,15 +221,25 @@ export async function startHarnessListener(options) {
152
221
  return { sandboxId: claims.sandboxId, epoch: claims.epoch };
153
222
  })
154
223
  : Promise.resolve(undefined);
155
- void Promise.all([options.attach(), host]).then(([attach, hosted]) => {
224
+ const harness = hostToken
225
+ ? Promise.resolve(undefined)
226
+ : ours
227
+ ? Promise.resolve(DEFAULT_HARNESS)
228
+ : options.harnessOfToken(bearer).then((id) => {
229
+ if (id === undefined)
230
+ throw new HostAuthError(401, "Unauthorized");
231
+ return id;
232
+ });
233
+ void Promise.all([options.attach(), host, harness]).then(([attach, hosted, harnessId]) => {
156
234
  if (!attach || closing || (hostToken && !hosted))
157
235
  return refuse(socket, 503, "Service Unavailable");
158
236
  wss.handleUpgrade(request, socket, head, (ws) => {
159
237
  const peer = {
160
238
  name: hosted
161
239
  ? `sandbox ${hosted.sandboxId}@${request.socket.remoteAddress ?? "unknown"}`
162
- : `harness@${request.socket.remoteAddress ?? "unknown"}`,
240
+ : `harness ${harnessId}@${request.socket.remoteAddress ?? "unknown"}`,
163
241
  ...(hosted ? { host: hosted } : {}),
242
+ ...(harnessId === undefined ? {} : { harness: harnessId }),
164
243
  };
165
244
  const channel = createChannel(wsPort(ws, { ...options.liveness, ping: true }), { validate: true });
166
245
  const detach = attach(channel, peer);
@@ -81,6 +81,7 @@ export interface StartEgressOptions {
81
81
  }
82
82
  /**
83
83
  * egress-gate (`--service egress`, F7.2): pods' CONNECT proxy, verifying egress tokens against the
84
- * Tenant's public keys and reading its sandboxes from the same database as the gates.
84
+ * Tenant's public keys and reading its sandboxes from the same database as the gates, where it
85
+ * also records each connection (R7.3).
85
86
  */
86
87
  export declare function startEgress(options: StartEgressOptions): Promise<EgressGate>;
@@ -23,9 +23,10 @@ import { tenantPaths } from "../tenant/paths.js";
23
23
  import { staleRun, verifyRunToken } from "../tenant/run-token.js";
24
24
  import { createFsBlobStore } from "../blob/index.js";
25
25
  import { createTenantVaults } from "../gates/tenant-vaults.js";
26
+ import { journalCheck } from "../gates/journal.js";
26
27
  import { probeDatabase } from "../infra/database.js";
27
28
  import { bindListener, headerValue, isAllowedRequestHost, sendRejected } from "./http.js";
28
- import { startEgressGate, storeEgressSandboxes } from "../gates/egress.js";
29
+ import { startEgressGate, storeEgressRecord, storeEgressSandboxes, } from "../gates/egress.js";
29
30
  import { verifyEgressToken } from "../sandbox/egress-token.js";
30
31
  import { leafKeyPair } from "../keys/x509.js";
31
32
  /** Above the gate's 600 s provider request timeout and the loop's 630 s client timeout. */
@@ -101,6 +102,7 @@ export async function startGates(options) {
101
102
  mcp,
102
103
  http: gateHttpTools(vaults, options.delivery ?? {}, logger),
103
104
  toolCalls,
105
+ journal: journalCheck(vaults),
104
106
  logger,
105
107
  ...(options.keys ? { keys: async () => (await vaults.open()).keys() } : {}),
106
108
  });
@@ -153,7 +155,8 @@ export async function startGates(options) {
153
155
  }
154
156
  /**
155
157
  * egress-gate (`--service egress`, F7.2): pods' CONNECT proxy, verifying egress tokens against the
156
- * Tenant's public keys and reading its sandboxes from the same database as the gates.
158
+ * Tenant's public keys and reading its sandboxes from the same database as the gates, where it
159
+ * also records each connection (R7.3).
157
160
  */
158
161
  export async function startEgress(options) {
159
162
  const { database } = options;
@@ -173,6 +176,11 @@ export async function startEgress(options) {
173
176
  return verifyEgressToken(vault.store, vault.tenantId, raw, publicKeys);
174
177
  },
175
178
  sandboxes: { live: async (sandboxId) => storeEgressSandboxes((await tenant.open()).store).live(sandboxId) },
179
+ // Each connection on the Tenant's record (R7.3): the gates' one write into it.
180
+ record: {
181
+ runningTurns: async (environmentId) => storeEgressRecord((await tenant.open()).store).runningTurns(environmentId),
182
+ write: async (connection) => storeEgressRecord((await tenant.open()).store).write(connection),
183
+ },
176
184
  credentials: {
177
185
  bound: async (sandboxId, host) => (await tenant.open()).environmentSecretBound(sandboxId, host),
178
186
  release: async (sandboxId, host) => (await tenant.open()).releaseEnvironmentSecret(sandboxId, host),
package/dist/host/main.js CHANGED
@@ -12,7 +12,9 @@
12
12
  * first start (`NYLORUN_TENANT_ID`, `NYLORUN_TENANT_NAME`; tenancy.md §4), trusting the issuers of the identity file
13
13
  * `NYLORUN_IDENTITY_FILE` names (Host feature `trusted-issuers`); with S2, a process running core
14
14
  * runs the stream relay, which feeds the Tenant's streams from the record over
15
- * logical replication (one process at a time holds the slot); `/ready` reports
15
+ * logical replication (one process at a time holds the slot); with `OTEL_EXPORTER_OTLP_ENDPOINT`
16
+ * it also runs the trace projector, which posts the record's spans over OTLP from a slot of its
17
+ * own (R7.2); `/ready` reports
16
18
  * the Tenant and the infrastructure checks. With an Object store
17
19
  * (`NYLORUN_OBJECT_STORE_*`) the Tenant's blobs go to it through the `s3` BlobStore, and a
18
20
  * process running core creates its bucket; without one they stay on disk (`fs`). A process
@@ -36,8 +38,12 @@ import { createPostgresTenantOpener } from "../tenant/store-pg.js";
36
38
  import { openTenantRuntime } from "../tenant/runtime.js";
37
39
  import { createPgoutputSource } from "../adapters/replication/pgoutput.js";
38
40
  import { assertLogicalReplication } from "../store/postgres/connect.js";
39
- import { createPostgresRecordReader } from "../store/postgres/record.js";
40
41
  import { createStreamRelay } from "../streams/relay/core.js";
42
+ import { createPostgresRecordReader, createPostgresTurnMessages } from "../store/postgres/record.js";
43
+ import { createPostgresSessionStore } from "../store/postgres/store.js";
44
+ import { createTraceProjector, TRACE_SLOT } from "../traces/projector.js";
45
+ import { createOtlpPoster } from "../traces/otlp.js";
46
+ import { traceKey } from "../traces/key.js";
41
47
  import { configForFactory } from "./config-for.js";
42
48
  import { createHost } from "./create-host.js";
43
49
  import { createHostExecution } from "./execution.js";
@@ -141,7 +147,7 @@ function warnRemovedSettings(stack, logger) {
141
147
  if (stack.removedResolverVariables)
142
148
  logger.warn("resolver_removed", {
143
149
  variables: stack.removedResolverVariables,
144
- message: "The credential resolver is gone (protocol 10) and these variables are ignored: keep a person's keys in their user vault, or reach their accounts through a gateway",
150
+ message: "The credential resolver is gone (protocol 10) and these variables are ignored: keep a person's keys in their Environment's vault, or reach their accounts through a gateway",
145
151
  });
146
152
  }
147
153
  /**
@@ -254,6 +260,7 @@ export async function main() {
254
260
  mode: stack.listen ? "container" : "local",
255
261
  endpoints: describeEndpoints(stack.endpoints),
256
262
  objectStore: stack.objectStore ? "s3" : "fs",
263
+ traces: stack.traces?.export ? "otlp" : "off",
257
264
  });
258
265
  const infra = createInfra(stack, { logger });
259
266
  const database = infra.database;
@@ -283,7 +290,11 @@ export async function main() {
283
290
  // running core runs one, once the Tenant is open (the relay reads its id); the replication
284
291
  // slot lets exactly one be active. Without S2, the Tenant relays its own commits.
285
292
  let relay;
286
- if (streams && stack.services.has("core"))
293
+ // With an OTLP endpoint (R7.2, Q5), the trace projector reads the record on its own slot: one
294
+ // process running core at a time, as for the relay. Without one there is no slot.
295
+ const traceExport = stack.services.has("core") ? stack.traces?.export : undefined;
296
+ let projector;
297
+ if ((streams || traceExport) && stack.services.has("core"))
287
298
  await assertLogicalReplication(database);
288
299
  const startRelay = (tenantId) => {
289
300
  if (!streams || !stack.services.has("core") || relay)
@@ -301,6 +312,32 @@ export async function main() {
301
312
  });
302
313
  relay.start();
303
314
  };
315
+ const startProjector = (tenantId) => {
316
+ if (!traceExport || projector)
317
+ return;
318
+ const store = createPostgresSessionStore({ sql: database, tenantId });
319
+ const log = (message, fields) => logger.info(message, fields);
320
+ projector = createTraceProjector({
321
+ source: createPgoutputSource({
322
+ connectionString: stack.endpoints.databaseUrl,
323
+ tenantId,
324
+ slot: TRACE_SLOT,
325
+ applicationName: "nylorun-trace-export",
326
+ log,
327
+ }),
328
+ post: createOtlpPoster({
329
+ url: traceExport.url,
330
+ headers: traceExport.headers,
331
+ resource: stack.traces.resource,
332
+ log: (message, fields) => logger.warn(message, fields),
333
+ }),
334
+ key: () => traceKey(store),
335
+ turnMessage: createPostgresTurnMessages(database),
336
+ content: new Set(stack.traces.content),
337
+ log,
338
+ });
339
+ projector.start();
340
+ };
304
341
  // With the gates service the Tenant's vault-backed model calls, remote MCP calls and HTTP
305
342
  // tool calls cross it, and this process never reads a model or MCP credential.
306
343
  // Their credentials (F5): each advance's run token for its session's model and tool calls,
@@ -355,11 +392,17 @@ export async function main() {
355
392
  ...(blobs ? { blobs } : {}),
356
393
  ...(stack.harnessMode === "remote" ? { harness: "remote" } : {}),
357
394
  ...(pods ? { pods } : {}),
395
+ ...(stack.traces
396
+ ? { traces: { resource: stack.traces.resource, content: new Set(stack.traces.content) } }
397
+ : {}),
358
398
  ...opened,
359
399
  }),
360
400
  }),
361
401
  logger,
362
- onOpen: (handle) => startRelay(handle.envelope.id),
402
+ onOpen: (handle) => {
403
+ startRelay(handle.envelope.id);
404
+ startProjector(handle.envelope.id);
405
+ },
363
406
  });
364
407
  const options = {
365
408
  hostRoot,
@@ -378,6 +421,7 @@ export async function main() {
378
421
  beforeTenants: async () => {
379
422
  await hostExecution.stop();
380
423
  await relay?.stop();
424
+ await projector?.stop();
381
425
  // The harnesses' runs stop with the Tenant's advances; their connections close after.
382
426
  },
383
427
  afterTenants: async () => {
@@ -421,6 +465,11 @@ export async function main() {
421
465
  const handle = resolved.kind === "open" ? resolved.handle : undefined;
422
466
  return handle?.attachHarness ? (channel, peer) => handle.attachHarness(channel, peer) : undefined;
423
467
  },
468
+ // Custom harnesses connect with their own tokens (R4.1).
469
+ harnessOfToken: async (token) => {
470
+ const resolved = await module.resolve();
471
+ return resolved.kind === "open" ? resolved.handle.harnessOfToken?.(token) : undefined;
472
+ },
424
473
  logger,
425
474
  });
426
475
  await host.listen();