@nylorun/runtime 0.23.0-beta → 0.24.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 (252) hide show
  1. package/CHANGELOG.md +148 -0
  2. package/README.md +79 -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/endpoint.js +5 -1
  10. package/dist/api/a2a/routes.d.ts +2 -0
  11. package/dist/api/a2a/routes.js +2 -2
  12. package/dist/api/a2a/tasks.js +2 -1
  13. package/dist/api/ag-ui/endpoint.js +4 -2
  14. package/dist/api/ag-ui/history.js +4 -1
  15. package/dist/api/ag-ui/routes.d.ts +4 -1
  16. package/dist/api/ag-ui/routes.js +11 -10
  17. package/dist/api/ag-ui/translate.js +3 -2
  18. package/dist/api/components.d.ts +4953 -1204
  19. package/dist/api/components.js +25 -16
  20. package/dist/api/gate/keys.js +7 -1
  21. package/dist/api/gate/routes.d.ts +6 -0
  22. package/dist/api/gate/routes.js +39 -3
  23. package/dist/api/http/app.js +4 -4
  24. package/dist/api/http/define.d.ts +6 -0
  25. package/dist/api/http/define.js +13 -1
  26. package/dist/api/http/respond.js +1 -1
  27. package/dist/api/http/routes/artifacts.js +35 -8
  28. package/dist/api/http/routes/coverage.d.ts +9 -0
  29. package/dist/api/http/routes/coverage.js +34 -0
  30. package/dist/api/http/routes/environments.d.ts +11 -0
  31. package/dist/api/http/routes/environments.js +283 -0
  32. package/dist/api/http/routes/mcp.d.ts +1 -1
  33. package/dist/api/http/routes/mcp.js +2 -2
  34. package/dist/api/http/routes/me.js +1 -1
  35. package/dist/api/http/routes/reads.d.ts +45 -9
  36. package/dist/api/http/routes/reads.js +54 -5
  37. package/dist/api/http/routes/sessions.js +11 -6
  38. package/dist/api/http/routes/tenant.js +32 -10
  39. package/dist/api/openapi.js +31 -26
  40. package/dist/artifacts/disk.d.ts +59 -0
  41. package/dist/artifacts/disk.js +563 -0
  42. package/dist/artifacts/export.d.ts +1 -1
  43. package/dist/artifacts/export.js +2 -3
  44. package/dist/artifacts/folders.d.ts +6 -3
  45. package/dist/artifacts/folders.js +17 -12
  46. package/dist/artifacts/service.d.ts +2 -1
  47. package/dist/artifacts/service.js +6 -1
  48. package/dist/artifacts/workspace.d.ts +1 -1
  49. package/dist/artifacts/workspace.js +1 -1
  50. package/dist/core/flow-host.d.ts +2 -3
  51. package/dist/core/flow-host.js +3 -0
  52. package/dist/core/provider.d.ts +3 -2
  53. package/dist/core/provider.js +5 -37
  54. package/dist/core/sandbox-routes.d.ts +2 -21
  55. package/dist/core/sandbox-routes.js +10 -43
  56. package/dist/execution/types.d.ts +1 -1
  57. package/dist/gates/contract.d.ts +3 -24
  58. package/dist/gates/contract.js +1 -36
  59. package/dist/gates/egress-credentials.d.ts +9 -0
  60. package/dist/gates/egress-credentials.js +11 -4
  61. package/dist/gates/egress.d.ts +28 -1
  62. package/dist/gates/egress.js +115 -10
  63. package/dist/gates/fixture-model.d.ts +28 -0
  64. package/dist/gates/fixture-model.js +92 -0
  65. package/dist/gates/handler.js +4 -4
  66. package/dist/gates/http-tool.d.ts +6 -2
  67. package/dist/gates/http-tool.js +3 -5
  68. package/dist/gates/in-process.d.ts +5 -0
  69. package/dist/gates/in-process.js +12 -1
  70. package/dist/gates/journal.d.ts +30 -0
  71. package/dist/gates/journal.js +52 -0
  72. package/dist/gates/mcp-handler.d.ts +4 -0
  73. package/dist/gates/mcp-handler.js +5 -1
  74. package/dist/gates/meter.d.ts +10 -2
  75. package/dist/gates/meter.js +20 -5
  76. package/dist/gates/policy.d.ts +49 -0
  77. package/dist/gates/policy.js +134 -0
  78. package/dist/gates/tenant-vaults.d.ts +7 -7
  79. package/dist/gates/tenant-vaults.js +5 -5
  80. package/dist/gates/tool-calls.d.ts +10 -5
  81. package/dist/gates/tool-calls.js +28 -10
  82. package/dist/gates/tool-client.d.ts +8 -0
  83. package/dist/gates/tool-client.js +27 -11
  84. package/dist/gates/tool-contract.d.ts +10 -68
  85. package/dist/gates/tool-contract.js +1 -85
  86. package/dist/gates/tool-gate.d.ts +1 -1
  87. package/dist/harness/calls.d.ts +12 -5
  88. package/dist/harness/calls.js +21 -12
  89. package/dist/harness/executors.js +12 -9
  90. package/dist/harness/main.js +18 -5
  91. package/dist/harness/pod.d.ts +34 -0
  92. package/dist/harness/pod.js +41 -0
  93. package/dist/harness/service.d.ts +0 -1
  94. package/dist/harness/service.js +13 -5
  95. package/dist/harness/setup.d.ts +27 -0
  96. package/dist/harness/setup.js +150 -0
  97. package/dist/harness/ws-client.js +3 -2
  98. package/dist/harness/ws-port.d.ts +1 -5
  99. package/dist/harness/ws-port.js +1 -5
  100. package/dist/harness-api/in-process.js +3 -3
  101. package/dist/harness-api/record.d.ts +15 -1
  102. package/dist/harness-api/record.js +174 -44
  103. package/dist/harness-api/redact.d.ts +3 -0
  104. package/dist/harness-api/redact.js +45 -0
  105. package/dist/harness-api/server.d.ts +8 -0
  106. package/dist/harness-api/server.js +93 -13
  107. package/dist/harness-api/start.d.ts +10 -3
  108. package/dist/harness-api/start.js +40 -10
  109. package/dist/harness-api/tool-journal.d.ts +96 -0
  110. package/dist/harness-api/tool-journal.js +102 -0
  111. package/dist/harness-api/tool-status.d.ts +13 -0
  112. package/dist/harness-api/tool-status.js +26 -0
  113. package/dist/harness-api/workspace.d.ts +6 -3
  114. package/dist/harness-api/workspace.js +27 -11
  115. package/dist/harness-api/ws-server.d.ts +11 -3
  116. package/dist/harness-api/ws-server.js +94 -15
  117. package/dist/host/gates.d.ts +2 -1
  118. package/dist/host/gates.js +10 -2
  119. package/dist/host/main.js +54 -5
  120. package/dist/host/operate.js +67 -5
  121. package/dist/host/stack-config.d.ts +64 -0
  122. package/dist/host/stack-config.js +61 -1
  123. package/dist/keys/client.js +4 -1
  124. package/dist/keys/keys.d.ts +5 -5
  125. package/dist/keys/keys.js +5 -7
  126. package/dist/management-openapi.json +999 -2211
  127. package/dist/mcp/connect.d.ts +25 -20
  128. package/dist/mcp/connect.js +29 -30
  129. package/dist/mcp/pool.d.ts +2 -2
  130. package/dist/mcp/pool.js +1 -1
  131. package/dist/mcp/preview.d.ts +3 -3
  132. package/dist/mcp/preview.js +10 -20
  133. package/dist/mcp/snapshot.d.ts +5 -11
  134. package/dist/mcp/snapshot.js +6 -7
  135. package/dist/model/pi-model.js +8 -0
  136. package/dist/openapi.json +6955 -2526
  137. package/dist/reads/types.d.ts +16 -3
  138. package/dist/record/append.d.ts +3 -1
  139. package/dist/record/append.js +1 -0
  140. package/dist/record/envelope.d.ts +5 -1
  141. package/dist/record/envelope.js +7 -3
  142. package/dist/record/environment.d.ts +25 -0
  143. package/dist/record/environment.js +44 -0
  144. package/dist/record/index.d.ts +1 -1
  145. package/dist/record/index.js +1 -1
  146. package/dist/sandbox/disk-fs.d.ts +95 -0
  147. package/dist/sandbox/disk-fs.js +206 -0
  148. package/dist/sandbox/join.d.ts +17 -18
  149. package/dist/sandbox/join.js +42 -15
  150. package/dist/sandbox/manager.d.ts +16 -8
  151. package/dist/sandbox/manager.js +33 -17
  152. package/dist/sandbox/placement.d.ts +13 -12
  153. package/dist/sandbox/placement.js +29 -12
  154. package/dist/sandbox/pods/client.d.ts +6 -1
  155. package/dist/sandbox/pods/lifecycle.d.ts +28 -27
  156. package/dist/sandbox/pods/lifecycle.js +14 -26
  157. package/dist/sandbox/pods/reconcile.js +18 -12
  158. package/dist/sandbox/pods/spec.d.ts +15 -13
  159. package/dist/sandbox/pods/spec.js +16 -20
  160. package/dist/sandbox/records.d.ts +2 -2
  161. package/dist/sandbox/records.js +3 -3
  162. package/dist/sandbox/resolve.d.ts +35 -27
  163. package/dist/sandbox/resolve.js +66 -41
  164. package/dist/sandbox/session-sandbox.js +1 -1
  165. package/dist/sandbox/setup.d.ts +61 -0
  166. package/dist/sandbox/setup.js +123 -0
  167. package/dist/sandbox/share.d.ts +11 -21
  168. package/dist/sandbox/share.js +12 -41
  169. package/dist/sandbox/tenant-config.d.ts +12 -14
  170. package/dist/sandbox/tenant-config.js +12 -17
  171. package/dist/sandbox/types.d.ts +6 -0
  172. package/dist/store/postgres/drizzle/0020_environments.sql +63 -0
  173. package/dist/store/postgres/drizzle/0021_disk_versions.sql +7 -0
  174. package/dist/store/postgres/drizzle/0022_model_usage_timing.sql +5 -0
  175. package/dist/store/postgres/drizzle/0023_artifact_pages.sql +1 -0
  176. package/dist/store/postgres/drizzle/meta/_journal.json +28 -0
  177. package/dist/store/postgres/reads.js +117 -17
  178. package/dist/store/postgres/record-writer.d.ts +4 -4
  179. package/dist/store/postgres/record-writer.js +9 -9
  180. package/dist/store/postgres/record.d.ts +7 -0
  181. package/dist/store/postgres/record.js +23 -2
  182. package/dist/store/postgres/schema.d.ts +934 -761
  183. package/dist/store/postgres/schema.js +102 -73
  184. package/dist/store/postgres/store.js +119 -66
  185. package/dist/store/types.d.ts +109 -48
  186. package/dist/tenant/advance.d.ts +7 -6
  187. package/dist/tenant/advance.js +115 -42
  188. package/dist/tenant/artifact-tool.js +3 -4
  189. package/dist/tenant/commands.d.ts +15 -2
  190. package/dist/tenant/commands.js +93 -12
  191. package/dist/tenant/context.d.ts +48 -22
  192. package/dist/tenant/context.js +4 -15
  193. package/dist/tenant/effects.d.ts +1 -1
  194. package/dist/tenant/effects.js +37 -20
  195. package/dist/tenant/environments.d.ts +167 -0
  196. package/dist/tenant/environments.js +823 -0
  197. package/dist/tenant/harness-tokens.d.ts +21 -0
  198. package/dist/tenant/harness-tokens.js +61 -0
  199. package/dist/tenant/host-token.d.ts +1 -1
  200. package/dist/tenant/host-token.js +2 -4
  201. package/dist/tenant/identity-file.d.ts +4 -4
  202. package/dist/tenant/identity-file.js +17 -8
  203. package/dist/tenant/issuers.js +3 -3
  204. package/dist/tenant/principals.js +1 -1
  205. package/dist/tenant/reset.js +3 -3
  206. package/dist/tenant/runtime.d.ts +6 -1
  207. package/dist/tenant/runtime.js +7 -1
  208. package/dist/tenant/session.d.ts +3 -3
  209. package/dist/tenant/session.js +0 -14
  210. package/dist/tenant/sessions.d.ts +13 -13
  211. package/dist/tenant/sessions.js +61 -138
  212. package/dist/tenant/signing-keys.d.ts +5 -1
  213. package/dist/tenant/status.js +3 -3
  214. package/dist/tenant/sweep.js +12 -6
  215. package/dist/tenant/transcript.d.ts +210 -20
  216. package/dist/tenant/transcript.js +62 -11
  217. package/dist/tenant/turn-timing.d.ts +43 -0
  218. package/dist/tenant/turn-timing.js +39 -0
  219. package/dist/tenant/types.d.ts +2 -0
  220. package/dist/traces/ids.d.ts +42 -0
  221. package/dist/traces/ids.js +85 -0
  222. package/dist/traces/key.d.ts +6 -0
  223. package/dist/traces/key.js +37 -0
  224. package/dist/traces/map.d.ts +113 -0
  225. package/dist/traces/map.js +317 -0
  226. package/dist/traces/options.d.ts +7 -0
  227. package/dist/traces/options.js +1 -0
  228. package/dist/traces/otlp.d.ts +42 -0
  229. package/dist/traces/otlp.js +84 -0
  230. package/dist/traces/projector.d.ts +58 -0
  231. package/dist/traces/projector.js +200 -0
  232. package/dist/traces/read.d.ts +23 -0
  233. package/dist/traces/read.js +16 -0
  234. package/dist/vault/coverage.d.ts +26 -0
  235. package/dist/vault/coverage.js +64 -0
  236. package/dist/vault/error.d.ts +11 -1
  237. package/dist/vault/error.js +5 -1
  238. package/dist/vault/service.d.ts +47 -48
  239. package/dist/vault/service.js +321 -382
  240. package/dist/vault/sources.d.ts +5 -16
  241. package/dist/vault/sources.js +6 -8
  242. package/dist/version.d.ts +1 -1
  243. package/dist/version.js +2 -2
  244. package/package.json +13 -13
  245. package/dist/api/http/routes/sandboxes.d.ts +0 -9
  246. package/dist/api/http/routes/sandboxes.js +0 -153
  247. package/dist/api/http/routes/vaults.d.ts +0 -10
  248. package/dist/api/http/routes/vaults.js +0 -139
  249. package/dist/record/sandbox.d.ts +0 -25
  250. package/dist/record/sandbox.js +0 -43
  251. package/dist/tenant/sandboxes.d.ts +0 -123
  252. package/dist/tenant/sandboxes.js +0 -566
@@ -28,7 +28,7 @@ export interface ToolGate {
28
28
  /**
29
29
  * True when a keyed MCP or HTTP tool call outlives the caller's process (the gates service,
30
30
  * G3): after a takeover or a shutdown, re-sending it joins the call or returns its outcome,
31
- * and a call lost with the gateway answers `uncertain` instead of running again.
31
+ * and a call lost with the gateway runs again under the same key (R5.3).
32
32
  */
33
33
  readonly recovers?: boolean;
34
34
  /** Stops a keyed tool call that outlives its caller (a user cancel). Never rejects. */
@@ -26,7 +26,7 @@ export interface ToolRouting {
26
26
  readonly mcpSnapshot?: McpSnapshot;
27
27
  /** The session that owns the tree's sandbox, and the sandbox resource it is attached to. */
28
28
  readonly sandboxOwnerId: string;
29
- readonly sandboxId?: string;
29
+ readonly environmentId?: string;
30
30
  /** Variables every sandbox command gets (R2c): secret names set to the sentinel, plain values. */
31
31
  readonly environment?: Readonly<Record<string, string>>;
32
32
  readonly activeTurnId: string | null;
@@ -36,9 +36,12 @@ export interface ToolRouting {
36
36
  /**
37
37
  * Call the model for one effect. A provider failure comes back as a failure outcome
38
38
  * (Model Calls §6), whichever provider serves the call; only an abort throws, and the
39
- * advance decides what the abort means.
39
+ * advance decides what the abort means. `atGate` sends the call to the Model Gate even when the
40
+ * Tenant's model is not from the vault: the Tenant's fixture model is answered there (R4.3).
40
41
  */
41
- export declare function invokeModel(route: ModelRoute, request: HostEffect, signal: AbortSignal, model?: ModelProvider): Promise<unknown>;
42
+ export declare function invokeModel(route: ModelRoute, request: HostEffect, signal: AbortSignal, options?: {
43
+ readonly atGate?: boolean;
44
+ }): Promise<unknown>;
42
45
  /**
43
46
  * A signal that follows `signal` only for the abort kinds in `kinds`. An MCP call stops on a
44
47
  * cancel; at the Tool Gate it also stops waiting on a shutdown, since the gate keeps the call
@@ -52,11 +55,15 @@ export declare function isSaveArtifactCall(manifest: AgentManifest | undefined,
52
55
  export declare function isReadArtifactCall(manifest: AgentManifest | undefined, request: HostEffect): boolean;
53
56
  /** True when `request` calls a tool of a declared MCP server (every one is remote). */
54
57
  export declare function isRemoteMcpCall(routing: Pick<ToolRouting, "rootManifest" | "mcpSnapshot">, request: HostEffect): boolean;
55
- export declare function callMcpTool(mcp: McpPool, routing: ToolRouting, request: HostEffect, signal: AbortSignal): Promise<unknown>;
58
+ export declare function callMcpTool(mcp: McpPool, routing: ToolRouting, request: HostEffect, signal: AbortSignal,
59
+ /** The call's span context (R7.4), sent as `traceparent` where this process holds the connection. */
60
+ traceparent?: string): Promise<unknown>;
56
61
  /**
57
62
  * Calls an HTTP tool through the Tool Gate (R2 M3), keyed by the effect id: an agent's (the
58
63
  * effect's agent used as a tool, or the root), or a flow's HTTP stage or HTTP verifier. Its
59
64
  * outcome is the effect's value; a throw leaves the effect `uncertain`.
60
65
  */
61
- export declare function callHttpTool(gate: Pick<ToolGate, "callHttp">, request: HostEffect, signal: AbortSignal): Promise<unknown>;
66
+ export declare function callHttpTool(gate: Pick<ToolGate, "callHttp">, request: HostEffect, signal: AbortSignal,
67
+ /** The call's span context (R7.4); the gates service reads its own from the journal. */
68
+ traceparent?: string): Promise<unknown>;
62
69
  export declare function callSandboxTool(sandbox: SandboxManager, routing: ToolRouting, request: HostEffect, signal: AbortSignal): Promise<unknown>;
@@ -13,13 +13,13 @@ import { sandboxCapabilityOf } from "../sandbox/capability.js";
13
13
  /**
14
14
  * Call the model for one effect. A provider failure comes back as a failure outcome
15
15
  * (Model Calls §6), whichever provider serves the call; only an abort throws, and the
16
- * advance decides what the abort means.
16
+ * advance decides what the abort means. `atGate` sends the call to the Model Gate even when the
17
+ * Tenant's model is not from the vault: the Tenant's fixture model is answered there (R4.3).
17
18
  */
18
- export async function invokeModel(route, request, signal, model) {
19
+ export async function invokeModel(route, request, signal, options = {}) {
20
+ const atGate = route.useVaultModel || options.atGate === true;
19
21
  try {
20
- if (model)
21
- return await model(request, signal);
22
- if (!route.useVaultModel)
22
+ if (!atGate)
23
23
  return await route.modelProvider(request, signal);
24
24
  return await route.modelGate.call({
25
25
  tenantId: route.tenantId,
@@ -35,7 +35,7 @@ export async function invokeModel(route, request, signal, model) {
35
35
  if (signal.aborted) {
36
36
  // A call that outlives this process stops only when told to: on a user cancel. After a
37
37
  // shutdown or a lost lease the next owner re-sends it and picks up its outcome (P1.2).
38
- if (!model && route.useVaultModel && runAbortKind(signal) === "cancel" && route.modelGate.cancel)
38
+ if (atGate && runAbortKind(signal) === "cancel" && route.modelGate.cancel)
39
39
  await route.modelGate.cancel({
40
40
  tenantId: route.tenantId,
41
41
  sessionId: request.sessionId,
@@ -89,7 +89,9 @@ export function isRemoteMcpCall(routing, request) {
89
89
  return false;
90
90
  return findServer(routing.rootManifest, tool.agentId, tool.capabilityId, tool.serverName) !== undefined;
91
91
  }
92
- export async function callMcpTool(mcp, routing, request, signal) {
92
+ export async function callMcpTool(mcp, routing, request, signal,
93
+ /** The call's span context (R7.4), sent as `traceparent` where this process holds the connection. */
94
+ traceparent) {
93
95
  const tool = mcpToolOf(routing.mcpSnapshot, request);
94
96
  if (!tool)
95
97
  throw new Error(`MCP tool '${request.toolName ?? ""}' is not in the session snapshot`);
@@ -102,8 +104,7 @@ export async function callMcpTool(mcp, routing, request, signal) {
102
104
  args: request.input,
103
105
  manifest: routing.rootManifest,
104
106
  effectId: request.effectId,
105
- // A read-only or idempotent tool's lost answer is the model's to retry (R2b C7, Q15).
106
- retrySafe: tool.annotations?.readOnlyHint === true || tool.annotations?.idempotentHint === true,
107
+ ...(traceparent ? { traceparent } : {}),
107
108
  signal,
108
109
  });
109
110
  }
@@ -112,10 +113,18 @@ export async function callMcpTool(mcp, routing, request, signal) {
112
113
  * effect's agent used as a tool, or the root), or a flow's HTTP stage or HTTP verifier. Its
113
114
  * outcome is the effect's value; a throw leaves the effect `uncertain`.
114
115
  */
115
- export async function callHttpTool(gate, request, signal) {
116
+ export async function callHttpTool(gate, request, signal,
117
+ /** The call's span context (R7.4); the gates service reads its own from the journal. */
118
+ traceparent) {
116
119
  if (!gate.callHttp)
117
120
  throw new Error("This Runtime has no Tool Gate for HTTP tools");
118
- return gate.callHttp({ tool: httpToolRefOf(request), effectId: request.effectId, turnId: request.turnId, input: request.input }, signal);
121
+ return gate.callHttp({
122
+ tool: httpToolRefOf(request),
123
+ effectId: request.effectId,
124
+ turnId: request.turnId,
125
+ input: request.input,
126
+ ...(traceparent ? { traceparent } : {}),
127
+ }, signal);
119
128
  }
120
129
  export async function callSandboxTool(sandbox, routing, request, signal) {
121
130
  const capability = sandboxCapabilityOf(manifestFor(routing.rootManifest, request.agent), request.capabilityId, request.toolName);
@@ -126,7 +135,7 @@ export async function callSandboxTool(sandbox, routing, request, signal) {
126
135
  id: routing.sandboxOwnerId,
127
136
  activeTurnId: routing.activeTurnId,
128
137
  manifest: routing.rootManifest,
129
- ...(routing.sandboxId === undefined ? {} : { sandboxId: routing.sandboxId }),
138
+ ...(routing.environmentId === undefined ? {} : { environmentId: routing.environmentId }),
130
139
  ...(routing.environment === undefined ? {} : { environment: routing.environment }),
131
140
  ...(routing.claim === undefined ? {} : { claim: routing.claim }),
132
141
  }, capability, request.toolName, request.input, signal);
@@ -8,13 +8,11 @@
8
8
  * (`--service harness`, `main.ts`) its own, whose sandbox events it claims from core.
9
9
  */
10
10
  import { hashManifest } from "@nylorun/core/define";
11
- import { toolFixtureModel } from "../core/provider.js";
11
+ import { GateCallLost } from "../gates/tool-client.js";
12
12
  import { serversOf } from "../mcp/pool.js";
13
13
  import { mcpToolOf } from "../mcp/snapshot.js";
14
14
  import { isHttpToolCall } from "../gates/http-tool.js";
15
15
  import { abortOn, callHttpTool, callMcpTool, callSandboxTool, invokeModel, isRemoteMcpCall, isSaveArtifactCall, } from "./calls.js";
16
- /** The Tenant's model of the fixture-model setting. Stateless. */
17
- const fixture = toolFixtureModel();
18
16
  export function harnessExecutors(options) {
19
17
  const recoversTools = options.toolGate.recovers === true;
20
18
  const atGate = (effect, run) => {
@@ -23,16 +21,19 @@ export function harnessExecutors(options) {
23
21
  };
24
22
  const recovered = (effect, run) => recoversTools && atGate(effect, run);
25
23
  return {
26
- model: (effect, signal, run) => invokeModel(options.model, effect, signal, run.start.options.fixtureModel ? fixture : undefined),
27
- tool(effect, signal, run) {
24
+ // With the Tenant's fixture model on, the Model Gate answers it (R4.3), metered like any call.
25
+ model: (effect, signal, run) => invokeModel(options.model, effect, signal, { atGate: run.start.options.fixtureModel }),
26
+ tool(effect, signal, run, call) {
28
27
  const routing = routingOf(run);
28
+ const traceparent = call?.traceparent;
29
29
  // A call at the Tool Gate outlives a shutdown there; a local one finishes and is recorded.
30
30
  const kinds = recovered(effect, run) ? ["cancel", "shutdown"] : ["cancel"];
31
31
  if (isHttpToolCall(routing.rootManifest, effect))
32
- return callHttpTool(options.toolGate, effect, abortOn(signal, kinds));
32
+ return callHttpTool(options.toolGate, effect, abortOn(signal, kinds), traceparent);
33
+ // A sandbox command joins its call's span (R7.4): a CLI that reads TRACEPARENT traces under it.
33
34
  if (!mcpToolOf(routing.mcpSnapshot, effect))
34
- return callSandboxTool(options.sandbox(), routing, effect, signal);
35
- return callMcpTool(options.mcp, routing, effect, abortOn(signal, kinds));
35
+ return callSandboxTool(options.sandbox(), traceparent ? { ...routing, environment: { ...routing.environment, TRACEPARENT: traceparent } } : routing, effect, signal);
36
+ return callMcpTool(options.mcp, routing, effect, abortOn(signal, kinds), traceparent);
36
37
  },
37
38
  recovers: {
38
39
  get model() {
@@ -40,6 +41,8 @@ export function harnessExecutors(options) {
40
41
  },
41
42
  tool: recovered,
42
43
  },
44
+ // A gate hop that lost the answer: the call is sent again with its key (R5.3).
45
+ resends: (effect, error, run) => error instanceof GateCallLost && atGate(effect, run),
43
46
  async cancelAtGate(effect) {
44
47
  await options.toolGate.cancel?.({
45
48
  tenantId: options.model.tenantId,
@@ -94,7 +97,7 @@ export function routingOf(run) {
94
97
  rootManifest: routing.rootManifest,
95
98
  ...(routing.mcpSnapshot ? { mcpSnapshot: routing.mcpSnapshot } : {}),
96
99
  sandboxOwnerId: routing.sandbox?.ownerId ?? run.grant.sessionId,
97
- ...(routing.sandbox?.sandboxId === undefined ? {} : { sandboxId: routing.sandbox.sandboxId }),
100
+ ...(routing.sandbox?.environmentId === undefined ? {} : { environmentId: routing.sandbox.environmentId }),
98
101
  ...(routing.sandbox?.environment === undefined ? {} : { environment: routing.sandbox.environment }),
99
102
  activeTurnId: run.grant.turnId,
100
103
  claim: run.runId,
@@ -10,8 +10,9 @@
10
10
  *
11
11
  * In a pod sandbox (`NYLORUN_SANDBOX_KIND=pod`, F7.2) it is that sandbox's engine: it first waits
12
12
  * until the pod's NetworkPolicy is in force (`awaitNetworkPolicy`), then joins with the
13
- * pod's join token instead of a harness token (`pod.ts`), and runs its sandbox's tools in the
14
- * pod itself (the `local` backend), with egress through egress-gate. SIGTERM (tini forwards
13
+ * pod's join token instead of a harness token (`pod.ts`), runs its disk's setup on a new volume
14
+ * (`setup.ts`), and runs its sandbox's tools in the pod itself (the `local` backend), with egress
15
+ * through egress-gate. SIGTERM (tini forwards
15
16
  * it; the pod's grace is 10 s by default) gives its runs back and exits.
16
17
  */
17
18
  import { mkdirSync } from "node:fs";
@@ -21,9 +22,10 @@ import { httpModelGate } from "../gates/http-client.js";
21
22
  import { httpToolGate } from "../gates/tool-client.js";
22
23
  import { createHostLogger } from "../host/logger.js";
23
24
  import { RUNTIME_VERSION } from "../version.js";
24
- import { localBackend } from "../adapters/sandbox/local.js";
25
+ import { localBackend, localExec } from "../adapters/sandbox/local.js";
25
26
  import { awaitNetworkPolicy } from "../sandbox/pods/network-gate.js";
26
27
  import { podHost } from "./pod.js";
28
+ import { prepareDisk } from "./setup.js";
27
29
  import { harnessRunTokens } from "./run-tokens.js";
28
30
  import { startHarnessService } from "./service.js";
29
31
  /**
@@ -48,6 +50,16 @@ export async function runHarness(stack, baseline) {
48
50
  volumeFile: join(config.root, "volume-id"),
49
51
  caBundleFile: join(config.root, "egress-ca-bundle.pem"),
50
52
  });
53
+ // Join, then make the disk ready (R2d step 2) before taking any turn.
54
+ await pod.token();
55
+ const host = pod;
56
+ await prepareDisk({
57
+ host,
58
+ exec: localExec({ env: baseline, proxyEnv: () => host.proxyEnv() }),
59
+ volumeFile: join(config.root, "volume-id"),
60
+ markerFile: join(config.root, "setup.json"),
61
+ logger,
62
+ });
51
63
  }
52
64
  const token = pod ? () => pod.token() : config.token;
53
65
  if (token === undefined)
@@ -56,13 +68,14 @@ export async function runHarness(stack, baseline) {
56
68
  const service = startHarnessService({
57
69
  url: config.url,
58
70
  token,
59
- ...(pod ? { sandboxBackends: [localBackend({ env: baseline, proxyEnv: () => pod.proxyEnv() })] } : {}),
71
+ ...(pod
72
+ ? { sandboxBackends: [localBackend({ env: baseline, proxyEnv: () => pod.proxyEnv(), harnessRoot: config.root })] }
73
+ : {}),
60
74
  paths,
61
75
  modelGate: httpModelGate({ url: config.gatesUrl, runTokens }),
62
76
  useVaultModel: true,
63
77
  toolGate: httpToolGate({ url: config.gatesUrl, runTokens }),
64
78
  logger,
65
- name: pod ? `sandbox ${config.pod.sandboxId}` : "harness",
66
79
  version: RUNTIME_VERSION,
67
80
  onGrant: (grant) => runTokens.grant(grant),
68
81
  });
@@ -1,6 +1,27 @@
1
1
  import type { PodHostConfig } from "../host/stack-config.js";
2
2
  /** How often the host and egress tokens are renewed. */
3
3
  export declare const RENEW_MS: number;
4
+ /** What to run and copy on a new disk (`sandbox/setup.ts` in core), from the join answer. */
5
+ export interface DiskSetup {
6
+ readonly recipeVersion: number;
7
+ readonly commands: readonly {
8
+ readonly command: string;
9
+ readonly cwd: string;
10
+ readonly timeoutMs: number;
11
+ }[];
12
+ readonly files: readonly {
13
+ readonly index: number;
14
+ readonly path: string;
15
+ }[];
16
+ readonly environment: Readonly<Record<string, string>>;
17
+ }
18
+ /** How a setup ended, as the engine reports it. */
19
+ export interface DiskSetupReport {
20
+ readonly status: "ok" | "failed";
21
+ readonly index?: number;
22
+ readonly exitCode?: number;
23
+ readonly output?: string;
24
+ }
4
25
  export interface PodHost {
5
26
  /** A current host token, for the next connection. Throws when neither renewal nor join works. */
6
27
  token(): Promise<string>;
@@ -8,6 +29,19 @@ export interface PodHost {
8
29
  proxyEnv(): Record<string, string>;
9
30
  /** The host epoch of the tokens held. */
10
31
  readonly epoch: number | undefined;
32
+ /** The disk's setup from the last join, if its recipe has one. */
33
+ readonly setup: DiskSetup | undefined;
34
+ /** The last join's volume starts from a disk version core writes: no setup runs on it. */
35
+ readonly seeded: boolean;
36
+ /** Whether to run the setup again after a failure, with the plan to run now. */
37
+ setupState(): Promise<{
38
+ retry: boolean;
39
+ setup?: DiskSetup;
40
+ }>;
41
+ /** Reports how the setup ended. */
42
+ reportSetup(report: DiskSetupReport): Promise<void>;
43
+ /** Input file `index` (a file's bytes or a folder's listing), or one file of a folder. */
44
+ setupFile(index: number, path?: string): Promise<Response>;
11
45
  stop(): void;
12
46
  }
13
47
  export declare function podHost(config: PodHostConfig, logger: {
@@ -31,6 +31,8 @@ class Refused extends Error {
31
31
  export function podHost(config, logger, options = {}) {
32
32
  const call = options.fetch ?? fetch;
33
33
  let held;
34
+ let setup;
35
+ let seeded = false;
34
36
  /** The CA in the bundle on disk, once one is written. */
35
37
  let bundled;
36
38
  const post = async (path, init) => {
@@ -51,6 +53,10 @@ export function podHost(config, logger, options = {}) {
51
53
  const body = JSON.parse(text);
52
54
  if (typeof body.caCertificate === "string")
53
55
  await writeBundle(body.caCertificate);
56
+ if (path === "join") {
57
+ setup = body.setup;
58
+ seeded = body.seeded === true;
59
+ }
54
60
  return {
55
61
  hostToken: body.hostToken,
56
62
  egressToken: body.egressToken,
@@ -99,6 +105,15 @@ export function podHost(config, logger, options = {}) {
99
105
  logger.info("sandbox_host_joined", { sandboxId: config.sandboxId, epoch: held.epoch });
100
106
  return held;
101
107
  };
108
+ /** A request with a current host token: the held one, renewed near expiry, or a new join. */
109
+ const authorized = async (path, init) => {
110
+ const current = held && held.expiresAt - Date.now() > 60_000 ? held : ((await renew()) ?? (await join()));
111
+ return call(`${config.httpUrl}/nylorun/harness/v1/host/${path}`, {
112
+ ...init,
113
+ headers: { ...init.headers, authorization: `Bearer ${current.hostToken}` },
114
+ signal: AbortSignal.timeout(5 * 60_000),
115
+ });
116
+ };
102
117
  const renew = async () => {
103
118
  if (!held)
104
119
  return undefined;
@@ -147,6 +162,32 @@ export function podHost(config, logger, options = {}) {
147
162
  get epoch() {
148
163
  return held?.epoch;
149
164
  },
165
+ get setup() {
166
+ return setup;
167
+ },
168
+ get seeded() {
169
+ return seeded;
170
+ },
171
+ async setupState() {
172
+ const response = await authorized("setup", { method: "GET" });
173
+ if (!response.ok)
174
+ throw new Error(`host/setup answered ${response.status}`);
175
+ return (await response.json());
176
+ },
177
+ async reportSetup(report) {
178
+ const response = await authorized("setup", {
179
+ method: "POST",
180
+ headers: { "content-type": "application/json" },
181
+ body: JSON.stringify(report),
182
+ });
183
+ await response.body?.cancel();
184
+ if (!response.ok)
185
+ throw new Error(`host/setup answered ${response.status}`);
186
+ },
187
+ setupFile(index, path) {
188
+ const suffix = path === undefined ? "" : `/${path.split("/").map(encodeURIComponent).join("/")}`;
189
+ return authorized(`files/${index}${suffix}`, { method: "GET" });
190
+ },
150
191
  stop() {
151
192
  clearInterval(timer);
152
193
  },
@@ -31,7 +31,6 @@ export interface HarnessServiceOptions {
31
31
  /** Delete the workspaces on stop (an ephemeral Runtime's loopback harness). */
32
32
  readonly ephemeral?: boolean;
33
33
  readonly logger: HarnessClientOptions["logger"];
34
- readonly name?: string;
35
34
  readonly version?: string;
36
35
  readonly onGrant?: HarnessOptions["onGrant"];
37
36
  readonly backoff?: HarnessClientOptions["backoff"];
@@ -9,8 +9,9 @@
9
9
  * keeps its compute records in `<sandboxes>/records.json` and claims its events from core
10
10
  * (`event`): for the run that made the call, or the workspace request core sent.
11
11
  */
12
+ import { fromWireOp, toWireAnswer } from "../sandbox/disk-fs.js";
12
13
  import { join } from "node:path";
13
- import { HarnessApiError, } from "@nylorun/core/harness-api";
14
+ import { DEFAULT_HARNESS, HarnessApiError, } from "@nylorun/core/harness-api";
14
15
  import { McpPool } from "../mcp/pool.js";
15
16
  import { SandboxManager } from "../sandbox/manager.js";
16
17
  import { fileSandboxRecords, workspacePrefix } from "../sandbox/records.js";
@@ -33,7 +34,7 @@ function refOf(session) {
33
34
  return {
34
35
  id: session.ownerId,
35
36
  activeTurnId: session.activeTurnId,
36
- ...(session.sandboxId === undefined ? {} : { sandboxId: session.sandboxId }),
37
+ ...(session.environmentId === undefined ? {} : { environmentId: session.environmentId }),
37
38
  };
38
39
  }
39
40
  const noModel = async () => {
@@ -78,6 +79,12 @@ export function startHarnessService(options) {
78
79
  case "workspace.read":
79
80
  case "workspace.write":
80
81
  case "workspace.exec": {
82
+ if ("disk" in params) {
83
+ // A disk version's read or write (R2d step 3).
84
+ const call = params;
85
+ const answer = await sandbox().disk(refOf(call.session), { id: "sandbox", type: "agent", sandbox: call.spec, tools: [] }, fromWireOp(call.disk), signal);
86
+ return "code" in answer ? answer : toWireAnswer(answer);
87
+ }
81
88
  if (method === "workspace.read" && "list" in params) {
82
89
  const call = params;
83
90
  return sandbox().listFiles(refOf(call.session), { id: "sandbox", type: "agent", sandbox: call.spec, tools: [] }, call.list.dir, call.list.maxEntries, signal);
@@ -102,7 +109,7 @@ export function startHarnessService(options) {
102
109
  return { workspaces: await sandbox().workspaces() };
103
110
  }
104
111
  case "workspace.remove": {
105
- const { keys, sandboxIds, all } = params;
112
+ const { keys, environmentIds, all } = params;
106
113
  const workspaces = sandbox();
107
114
  if (all) {
108
115
  await workspaces.reconcile(() => false, () => false);
@@ -114,7 +121,7 @@ export function startHarnessService(options) {
114
121
  throw new HarnessApiError("invalid", `${key} is not a workspace of this Tenant`);
115
122
  await workspaces.remove(key);
116
123
  }
117
- for (const id of sandboxIds ?? [])
124
+ for (const id of environmentIds ?? [])
118
125
  await workspaces.removeSandbox(id);
119
126
  return {};
120
127
  }
@@ -128,7 +135,8 @@ export function startHarnessService(options) {
128
135
  logger,
129
136
  executors,
130
137
  serve,
131
- name: options.name ?? "harness",
138
+ // Our harness: its hello names our harness id (R4.1).
139
+ name: DEFAULT_HARNESS,
132
140
  ...(options.version ? { version: options.version } : {}),
133
141
  capabilities: { workspace: { backends: backends.map((backend) => backend.name) } },
134
142
  onHello(answer) {
@@ -0,0 +1,27 @@
1
+ import type { ExecRequest, ExecResult } from "../sandbox/types.js";
2
+ import type { PodHost } from "./pod.js";
3
+ /** How often a failed setup asks whether to run again. */
4
+ export declare const SETUP_RETRY_POLL_MS = 5000;
5
+ export interface DiskSetupOptions {
6
+ readonly host: Pick<PodHost, "setup" | "setupState" | "reportSetup" | "setupFile"> & {
7
+ /** The volume starts from a disk version (R2d step 3): it holds what setup made. */
8
+ readonly seeded?: boolean;
9
+ };
10
+ /** Runs one command as the sandbox tools do (`localExec`). */
11
+ readonly exec: (request: ExecRequest, signal: AbortSignal) => Promise<ExecResult>;
12
+ /** The volume's id file (`/harness/volume-id`). */
13
+ readonly volumeFile: string;
14
+ /** The marker (`/harness/setup.json`). */
15
+ readonly markerFile: string;
16
+ readonly logger: {
17
+ info(message: string, fields?: Record<string, unknown>): void;
18
+ warn(message: string, fields?: Record<string, unknown>): void;
19
+ };
20
+ readonly pollMs?: number;
21
+ readonly signal?: AbortSignal;
22
+ }
23
+ /**
24
+ * Runs the disk's setup when this volume has not had it, reports how it ended, and after a
25
+ * failure waits for core to ask for it again. Resolves once the setup is done (or there is none).
26
+ */
27
+ export declare function prepareDisk(options: DiskSetupOptions): Promise<void>;
@@ -0,0 +1,150 @@
1
+ /**
2
+ * The disk's setup in a pod Environment's engine (R2d step 2): after it joins and before it takes
3
+ * any turn, the engine copies the recipe's input files and runs its setup commands, once per
4
+ * volume. A marker on the volume (`/harness/setup.json`, `{ volumeId, recipeVersion, status }`)
5
+ * says it was done: a resume finds it and skips, a reset (a new volume, a new volume id) runs it
6
+ * again. Commands run as the sandbox tools' do (`localExec`): `/bin/sh -c` in `cwd`, with the
7
+ * Environment's variables and secrets (as the sentinel) and egress through egress-gate.
8
+ *
9
+ * A failure is reported to core, which fails the pod and the turn waiting for it; the engine then
10
+ * polls until a turn start asks the pod to run again, and runs the setup again from the start.
11
+ */
12
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
13
+ import { randomUUID } from "node:crypto";
14
+ import { dirname, posix } from "node:path";
15
+ /** How often a failed setup asks whether to run again. */
16
+ export const SETUP_RETRY_POLL_MS = 5_000;
17
+ /** The end of a failed command's output kept in its report. */
18
+ const REPORT_TAIL = 4 * 1024;
19
+ async function readText(path) {
20
+ try {
21
+ return (await readFile(path, "utf8")).trim();
22
+ }
23
+ catch {
24
+ return undefined;
25
+ }
26
+ }
27
+ /** Writes `bytes` at `path`, by way of a temporary name, creating its directory. */
28
+ async function writeAtomic(path, bytes) {
29
+ await mkdir(dirname(path), { recursive: true });
30
+ const next = `${path}.${randomUUID()}.tmp`;
31
+ await writeFile(next, bytes);
32
+ await rename(next, path);
33
+ }
34
+ /** A folder entry's path joined under `base`, refused when it would leave it. */
35
+ function under(base, relative) {
36
+ const joined = posix.normalize(posix.join(base, relative));
37
+ if (joined !== base && !joined.startsWith(`${base.replace(/\/$/, "")}/`))
38
+ throw new Error(`${relative} would leave ${base}`);
39
+ return joined;
40
+ }
41
+ /** Copies input file `file` into place: a file artifact at its path, a folder under it. */
42
+ async function copyFile(host, file) {
43
+ const first = await host.setupFile(file.index);
44
+ if (!first.ok)
45
+ throw new Error(`input file ${file.index + 1} answered ${first.status}`);
46
+ if (first.headers.get("x-nylorun-artifact-kind") !== "folder") {
47
+ await writeAtomic(file.path, new Uint8Array(await first.arrayBuffer()));
48
+ return;
49
+ }
50
+ const { entries } = (await first.json());
51
+ for (const entry of entries) {
52
+ const answer = await host.setupFile(file.index, entry.path);
53
+ if (!answer.ok)
54
+ throw new Error(`input file ${file.index + 1}, ${entry.path}, answered ${answer.status}`);
55
+ await writeAtomic(under(file.path, entry.path), new Uint8Array(await answer.arrayBuffer()));
56
+ }
57
+ }
58
+ /** Runs `setup` once: files first, then commands in order; the report of how it ended. */
59
+ async function runOnce(options, setup) {
60
+ for (const file of setup.files) {
61
+ try {
62
+ await copyFile(options.host, file);
63
+ }
64
+ catch (error) {
65
+ return { status: "failed", output: error instanceof Error ? error.message : String(error) };
66
+ }
67
+ }
68
+ for (const [index, command] of setup.commands.entries()) {
69
+ options.logger.info("environment_setup_command", { index, cwd: command.cwd });
70
+ await mkdir(command.cwd, { recursive: true }).catch(() => undefined);
71
+ let result;
72
+ try {
73
+ result = await options.exec({ command: command.command, cwd: command.cwd, timeoutMs: command.timeoutMs, env: setup.environment }, options.signal ?? new AbortController().signal);
74
+ }
75
+ catch (error) {
76
+ return { status: "failed", index, output: error instanceof Error ? error.message : String(error) };
77
+ }
78
+ if (result.exitCode !== 0) {
79
+ const output = `${result.stdout}${result.stderr ? `\n${result.stderr}` : ""}${result.timedOut ? "\n[timed out]" : ""}`;
80
+ return { status: "failed", index, exitCode: result.exitCode, output: output.slice(-REPORT_TAIL) };
81
+ }
82
+ }
83
+ return { status: "ok" };
84
+ }
85
+ /**
86
+ * Runs the disk's setup when this volume has not had it, reports how it ended, and after a
87
+ * failure waits for core to ask for it again. Resolves once the setup is done (or there is none).
88
+ */
89
+ export async function prepareDisk(options) {
90
+ let setup = options.host.setup;
91
+ const volumeId = await readText(options.volumeFile);
92
+ if (options.host.seeded) {
93
+ // A volume seeded from a disk version: the version holds what setup made, so mark it done.
94
+ if (volumeId) {
95
+ const done = { volumeId, recipeVersion: setup?.recipeVersion ?? 0, status: "ok" };
96
+ await writeAtomic(options.markerFile, new TextEncoder().encode(`${JSON.stringify(done)}\n`));
97
+ }
98
+ options.logger.info("environment_setup_skipped", { reason: "seeded" });
99
+ return;
100
+ }
101
+ if (!setup)
102
+ return;
103
+ const marker = await readText(options.markerFile);
104
+ if (marker && volumeId) {
105
+ try {
106
+ const done = JSON.parse(marker);
107
+ if (done.volumeId === volumeId && done.status === "ok")
108
+ return;
109
+ }
110
+ catch {
111
+ /* a broken marker: run the setup again */
112
+ }
113
+ }
114
+ for (;;) {
115
+ if (options.signal?.aborted)
116
+ return;
117
+ options.logger.info("environment_setup_started", { recipeVersion: setup.recipeVersion });
118
+ const report = await runOnce(options, setup);
119
+ if (report.status === "ok") {
120
+ if (volumeId) {
121
+ const done = { volumeId, recipeVersion: setup.recipeVersion, status: "ok" };
122
+ await writeAtomic(options.markerFile, new TextEncoder().encode(`${JSON.stringify(done)}\n`));
123
+ }
124
+ await options.host.reportSetup(report).catch((error) => options.logger.warn("environment_setup_report_failed", {
125
+ message: error instanceof Error ? error.message : String(error),
126
+ }));
127
+ options.logger.info("environment_setup_done", { recipeVersion: setup.recipeVersion });
128
+ return;
129
+ }
130
+ options.logger.warn("environment_setup_failed", { index: report.index, exitCode: report.exitCode });
131
+ let reported = false;
132
+ // Report it, then wait until a turn start asks the pod to run again, and run it again.
133
+ for (;;) {
134
+ if (!reported)
135
+ reported = await options.host.reportSetup(report).then(() => true, () => false);
136
+ await new Promise((resolve) => setTimeout(resolve, options.pollMs ?? SETUP_RETRY_POLL_MS));
137
+ if (options.signal?.aborted)
138
+ return;
139
+ if (!reported)
140
+ continue;
141
+ const state = await options.host.setupState().catch(() => undefined);
142
+ if (!state?.retry)
143
+ continue;
144
+ if (!state.setup)
145
+ return;
146
+ setup = state.setup;
147
+ break;
148
+ }
149
+ }
150
+ }
@@ -9,7 +9,8 @@
9
9
  import { createChannel } from "@nylorun/core/harness-api";
10
10
  import { createHarness } from "@nylorun/harness/api";
11
11
  import { WebSocket } from "ws";
12
- import { HARNESS_API_HEADER, HARNESS_MAX_PAYLOAD, wsPort } from "./ws-port.js";
12
+ import { HARNESS_API_HEADER, HARNESS_API_TRANSPORT, HARNESS_MAX_PAYLOAD } from "@nylorun/core/harness-api";
13
+ import { wsPort } from "./ws-port.js";
13
14
  export function connectHarness(options) {
14
15
  const { logger } = options;
15
16
  const backoff = options.backoff ?? { minMs: 500, maxMs: 10_000 };
@@ -23,7 +24,7 @@ export function connectHarness(options) {
23
24
  const token = typeof options.token === "string" ? options.token : await options.token();
24
25
  return new Promise((resolve, reject) => {
25
26
  const socket = new WebSocket(options.url, {
26
- headers: { authorization: `Bearer ${token}`, [HARNESS_API_HEADER]: "1" },
27
+ headers: { authorization: `Bearer ${token}`, [HARNESS_API_HEADER]: HARNESS_API_TRANSPORT },
27
28
  maxPayload: HARNESS_MAX_PAYLOAD,
28
29
  handshakeTimeout: 10_000,
29
30
  });
@@ -6,11 +6,7 @@
6
6
  */
7
7
  import type { Port } from "@nylorun/core/harness-api";
8
8
  import { WebSocket } from "ws";
9
- /** The URL path of the Harness API, and the header naming its version. */
10
- export declare const HARNESS_API_PATH = "/nylorun/harness/v1";
11
- export declare const HARNESS_API_HEADER = "nylorun-harness-api";
12
- /** Largest frame either end accepts. */
13
- export declare const HARNESS_MAX_PAYLOAD: number;
9
+ export { HARNESS_API_HEADER, HARNESS_API_PATH, HARNESS_API_TRANSPORT, HARNESS_MAX_PAYLOAD, } from "@nylorun/core/harness-api";
14
10
  export declare const DEFAULT_PING_MS = 30000;
15
11
  export declare const DEFAULT_SILENCE_MS = 90000;
16
12
  export interface WsPortOptions {