@nylorun/runtime 0.25.0-beta → 0.27.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 (326) hide show
  1. package/CHANGELOG.md +169 -0
  2. package/README.md +26 -16
  3. package/dist/adapters/execution/restate.d.ts +15 -18
  4. package/dist/adapters/execution/restate.js +31 -27
  5. package/dist/adapters/replication/pgoutput.d.ts +1 -1
  6. package/dist/adapters/replication/pgoutput.js +3 -2
  7. package/dist/adapters/sandbox/local.js +8 -5
  8. package/dist/adapters/sandbox/virtual.js +2 -2
  9. package/dist/adapters/streams/s2.d.ts +1 -1
  10. package/dist/adapters/streams/s2.js +18 -14
  11. package/dist/api/a2a/card.js +1 -1
  12. package/dist/api/a2a/endpoint.js +3 -4
  13. package/dist/api/a2a/protocol.js +4 -0
  14. package/dist/api/a2a/routes.d.ts +2 -6
  15. package/dist/api/a2a/routes.js +12 -18
  16. package/dist/api/ag-ui/endpoint.js +2 -3
  17. package/dist/api/ag-ui/routes.js +4 -4
  18. package/dist/api/components.d.ts +326 -45
  19. package/dist/api/components.js +5 -1
  20. package/dist/api/gate/keys.js +7 -4
  21. package/dist/api/gate/routes.d.ts +6 -2
  22. package/dist/api/gate/routes.js +15 -11
  23. package/dist/api/http/app.d.ts +2 -2
  24. package/dist/api/http/app.js +2 -2
  25. package/dist/api/http/body.d.ts +15 -3
  26. package/dist/api/http/body.js +15 -7
  27. package/dist/api/http/define.d.ts +1 -1
  28. package/dist/api/http/respond.js +7 -3
  29. package/dist/api/http/routes/access.js +5 -4
  30. package/dist/api/http/routes/artifacts.js +19 -7
  31. package/dist/api/http/routes/coverage.js +1 -1
  32. package/dist/api/http/routes/environment-manifests.js +1 -1
  33. package/dist/api/http/routes/environments.js +6 -6
  34. package/dist/api/http/routes/files.d.ts +2 -1
  35. package/dist/api/http/routes/files.js +31 -5
  36. package/dist/api/http/routes/mcp.js +1 -1
  37. package/dist/api/http/routes/reads.d.ts +35 -0
  38. package/dist/api/http/routes/reads.js +50 -22
  39. package/dist/api/http/routes/sessions.js +12 -11
  40. package/dist/api/http/routes/tenant.d.ts +2 -2
  41. package/dist/api/http/routes/tenant.js +69 -16
  42. package/dist/api/inbound-files.d.ts +3 -9
  43. package/dist/api/inbound-files.js +18 -24
  44. package/dist/api/openapi.js +6 -2
  45. package/dist/artifacts/disk.d.ts +0 -4
  46. package/dist/artifacts/disk.js +77 -73
  47. package/dist/artifacts/files.d.ts +13 -4
  48. package/dist/artifacts/files.js +27 -9
  49. package/dist/artifacts/folder-upload.d.ts +75 -0
  50. package/dist/artifacts/folder-upload.js +263 -0
  51. package/dist/artifacts/folders.d.ts +14 -8
  52. package/dist/artifacts/folders.js +19 -11
  53. package/dist/artifacts/links.js +7 -6
  54. package/dist/artifacts/parts.d.ts +9 -14
  55. package/dist/artifacts/parts.js +11 -9
  56. package/dist/artifacts/pipe.d.ts +15 -0
  57. package/dist/artifacts/pipe.js +71 -0
  58. package/dist/artifacts/quota.d.ts +36 -0
  59. package/dist/artifacts/quota.js +44 -0
  60. package/dist/artifacts/service.d.ts +25 -1
  61. package/dist/artifacts/service.js +70 -25
  62. package/dist/blob/fs.js +2 -1
  63. package/dist/blob/s3.d.ts +6 -2
  64. package/dist/blob/s3.js +9 -7
  65. package/dist/blob/types.d.ts +1 -1
  66. package/dist/blob/types.js +1 -1
  67. package/dist/core/limits.d.ts +11 -35
  68. package/dist/core/limits.js +5 -73
  69. package/dist/core/sandbox-routes.js +0 -1
  70. package/dist/core/turn-manifest.d.ts +1 -2
  71. package/dist/core/turn-manifest.js +4 -28
  72. package/dist/execution/memory.d.ts +8 -5
  73. package/dist/execution/memory.js +8 -6
  74. package/dist/execution/types.d.ts +1 -6
  75. package/dist/execution/types.js +0 -5
  76. package/dist/gates/contract.d.ts +3 -2
  77. package/dist/gates/contract.js +1 -1
  78. package/dist/gates/egress-credentials.js +4 -3
  79. package/dist/gates/egress.d.ts +9 -9
  80. package/dist/gates/egress.js +19 -19
  81. package/dist/gates/fixture-model.js +13 -2
  82. package/dist/gates/handler.d.ts +1 -3
  83. package/dist/gates/handler.js +4 -3
  84. package/dist/gates/http-client.d.ts +4 -3
  85. package/dist/gates/http-client.js +5 -7
  86. package/dist/gates/http-tool.d.ts +24 -4
  87. package/dist/gates/http-tool.js +53 -17
  88. package/dist/gates/in-process.d.ts +2 -1
  89. package/dist/gates/in-process.js +17 -9
  90. package/dist/gates/inflight.d.ts +2 -2
  91. package/dist/gates/inflight.js +3 -2
  92. package/dist/gates/journal.js +5 -7
  93. package/dist/gates/mcp-handler.d.ts +4 -3
  94. package/dist/gates/mcp-handler.js +15 -5
  95. package/dist/gates/model-gate.d.ts +11 -2
  96. package/dist/gates/policy.d.ts +28 -5
  97. package/dist/gates/policy.js +60 -17
  98. package/dist/gates/provider-gate.d.ts +22 -0
  99. package/dist/gates/provider-gate.js +30 -0
  100. package/dist/gates/tool-calls.d.ts +1 -3
  101. package/dist/gates/tool-calls.js +2 -3
  102. package/dist/gates/tool-client.d.ts +1 -1
  103. package/dist/gates/tool-client.js +5 -6
  104. package/dist/gates/tool-contract.d.ts +8 -5
  105. package/dist/gates/tool-contract.js +1 -1
  106. package/dist/gates/tool-gate.d.ts +18 -7
  107. package/dist/gates/tool-gate.js +49 -7
  108. package/dist/harness/calls.d.ts +27 -25
  109. package/dist/harness/calls.js +40 -46
  110. package/dist/harness/executors.d.ts +10 -3
  111. package/dist/harness/executors.js +53 -23
  112. package/dist/harness/main.js +2 -2
  113. package/dist/harness/pod.d.ts +0 -2
  114. package/dist/harness/pod.js +7 -6
  115. package/dist/harness/run-tokens.d.ts +4 -1
  116. package/dist/harness/run-tokens.js +6 -2
  117. package/dist/harness/service.d.ts +17 -14
  118. package/dist/harness/service.js +21 -35
  119. package/dist/harness/setup.d.ts +0 -2
  120. package/dist/harness/setup.js +4 -9
  121. package/dist/harness/ws-client.d.ts +2 -2
  122. package/dist/harness/ws-client.js +7 -5
  123. package/dist/harness/ws-port.d.ts +3 -4
  124. package/dist/harness/ws-port.js +3 -4
  125. package/dist/harness-api/artifacts.d.ts +20 -0
  126. package/dist/harness-api/artifacts.js +478 -0
  127. package/dist/harness-api/in-process.d.ts +2 -2
  128. package/dist/harness-api/in-process.js +18 -16
  129. package/dist/harness-api/record.d.ts +10 -9
  130. package/dist/harness-api/record.js +102 -157
  131. package/dist/harness-api/renew.d.ts +1 -1
  132. package/dist/harness-api/renew.js +2 -2
  133. package/dist/harness-api/server.d.ts +1 -16
  134. package/dist/harness-api/server.js +82 -73
  135. package/dist/harness-api/start.d.ts +20 -2
  136. package/dist/harness-api/start.js +57 -18
  137. package/dist/harness-api/tool-journal.d.ts +15 -10
  138. package/dist/harness-api/tool-journal.js +26 -24
  139. package/dist/harness-api/workspace.d.ts +0 -4
  140. package/dist/harness-api/workspace.js +2 -45
  141. package/dist/harness-api/ws-server.js +10 -10
  142. package/dist/host/create-host.js +2 -3
  143. package/dist/host/execution.js +2 -3
  144. package/dist/host/gates.d.ts +2 -9
  145. package/dist/host/gates.js +15 -12
  146. package/dist/host/http.d.ts +0 -2
  147. package/dist/host/http.js +0 -27
  148. package/dist/host/main.js +10 -3
  149. package/dist/host/operate.js +12 -3
  150. package/dist/host/stack-config.d.ts +11 -0
  151. package/dist/host/stack-config.js +21 -1
  152. package/dist/index.d.ts +0 -1
  153. package/dist/index.js +0 -1
  154. package/dist/infra/index.js +2 -1
  155. package/dist/infra/readiness.d.ts +0 -2
  156. package/dist/infra/readiness.js +4 -3
  157. package/dist/keys/client.d.ts +1 -0
  158. package/dist/keys/client.js +2 -3
  159. package/dist/keys/contract.d.ts +1 -3
  160. package/dist/keys/contract.js +1 -3
  161. package/dist/keys/egress-ca.d.ts +0 -2
  162. package/dist/keys/egress-ca.js +2 -3
  163. package/dist/keys/keys.js +6 -1
  164. package/dist/keys/x509.d.ts +8 -4
  165. package/dist/keys/x509.js +14 -5
  166. package/dist/management-openapi.json +1961 -994
  167. package/dist/mcp/connect.d.ts +35 -26
  168. package/dist/mcp/connect.js +112 -141
  169. package/dist/mcp/pool.d.ts +27 -22
  170. package/dist/mcp/pool.js +34 -48
  171. package/dist/mcp/preview.d.ts +13 -6
  172. package/dist/mcp/preview.js +37 -20
  173. package/dist/mcp/snapshot.d.ts +35 -86
  174. package/dist/mcp/snapshot.js +93 -135
  175. package/dist/model/auth-store.js +5 -3
  176. package/dist/model/capabilities.d.ts +1 -1
  177. package/dist/model/catalog.d.ts +9 -36
  178. package/dist/model/catalog.js +6 -33
  179. package/dist/model/descriptor.d.ts +51 -0
  180. package/dist/model/descriptor.js +83 -0
  181. package/dist/model/models.d.ts +0 -3
  182. package/dist/model/models.js +3 -5
  183. package/dist/model/native-blocks.js +1 -1
  184. package/dist/model/pi-model.d.ts +16 -5
  185. package/dist/model/pi-model.js +86 -105
  186. package/dist/openapi.json +866 -1218
  187. package/dist/reads/cursor.js +3 -1
  188. package/dist/record/append.d.ts +34 -2
  189. package/dist/record/append.js +50 -1
  190. package/dist/record/environment.d.ts +2 -0
  191. package/dist/record/environment.js +2 -0
  192. package/dist/record/index.d.ts +1 -1
  193. package/dist/record/index.js +1 -1
  194. package/dist/redact.d.ts +1 -1
  195. package/dist/redact.js +3 -2
  196. package/dist/sandbox/egress-token.d.ts +3 -3
  197. package/dist/sandbox/egress-token.js +3 -3
  198. package/dist/sandbox/join.js +3 -2
  199. package/dist/sandbox/manager.d.ts +41 -41
  200. package/dist/sandbox/manager.js +112 -44
  201. package/dist/sandbox/placement.d.ts +1 -1
  202. package/dist/sandbox/placement.js +5 -8
  203. package/dist/sandbox/pods/client.d.ts +1 -10
  204. package/dist/sandbox/pods/client.js +8 -4
  205. package/dist/sandbox/pods/lifecycle.d.ts +19 -11
  206. package/dist/sandbox/pods/lifecycle.js +45 -23
  207. package/dist/sandbox/pods/network-gate.d.ts +4 -5
  208. package/dist/sandbox/pods/network-gate.js +7 -7
  209. package/dist/sandbox/pods/reconcile.js +4 -1
  210. package/dist/sandbox/pods/spec.js +8 -5
  211. package/dist/sandbox/policy.d.ts +7 -1
  212. package/dist/sandbox/policy.js +10 -3
  213. package/dist/sandbox/resolve.d.ts +8 -3
  214. package/dist/sandbox/resolve.js +12 -2
  215. package/dist/sandbox/setup.d.ts +3 -1
  216. package/dist/sandbox/setup.js +34 -11
  217. package/dist/sandbox/share.d.ts +5 -14
  218. package/dist/sandbox/share.js +5 -34
  219. package/dist/sandbox/skills.d.ts +7 -13
  220. package/dist/sandbox/skills.js +16 -31
  221. package/dist/sandbox/tenant-config.d.ts +2 -2
  222. package/dist/sandbox/tenant-config.js +4 -3
  223. package/dist/sandbox/tools.d.ts +7 -1
  224. package/dist/sandbox/tools.js +26 -26
  225. package/dist/sandbox/types.d.ts +10 -3
  226. package/dist/sandbox/types.js +9 -0
  227. package/dist/store/postgres/connect.d.ts +6 -5
  228. package/dist/store/postgres/connect.js +14 -11
  229. package/dist/store/postgres/control.js +13 -13
  230. package/dist/store/postgres/drizzle/0028_artifact_write_keys.sql +2 -0
  231. package/dist/store/postgres/drizzle/0029_protocol_15.sql +11 -0
  232. package/dist/store/postgres/drizzle/0030_protocol_15_runtime_block.sql +11 -0
  233. package/dist/store/postgres/drizzle/0031_protocol_15_mcp_snapshot.sql +8 -0
  234. package/dist/store/postgres/drizzle/meta/_journal.json +28 -0
  235. package/dist/store/postgres/migrate.js +1 -1
  236. package/dist/store/postgres/reads.js +10 -2
  237. package/dist/store/postgres/schema.d.ts +19 -0
  238. package/dist/store/postgres/schema.js +7 -0
  239. package/dist/store/postgres/store.js +55 -26
  240. package/dist/store/types.d.ts +17 -7
  241. package/dist/streams/relay/core.d.ts +3 -3
  242. package/dist/streams/relay/core.js +9 -10
  243. package/dist/tenant/advance.js +21 -31
  244. package/dist/tenant/commands.js +9 -16
  245. package/dist/tenant/context.d.ts +14 -17
  246. package/dist/tenant/definition-files.d.ts +25 -4
  247. package/dist/tenant/definition-files.js +85 -47
  248. package/dist/tenant/effects.js +9 -36
  249. package/dist/tenant/environment-manifests.js +9 -1
  250. package/dist/tenant/environments.d.ts +0 -2
  251. package/dist/tenant/environments.js +54 -30
  252. package/dist/tenant/ephemeral.d.ts +1 -1
  253. package/dist/tenant/ephemeral.js +2 -1
  254. package/dist/tenant/history.d.ts +1 -1
  255. package/dist/tenant/history.js +1 -1
  256. package/dist/tenant/host-token.d.ts +3 -3
  257. package/dist/tenant/host-token.js +12 -11
  258. package/dist/tenant/http.d.ts +5 -8
  259. package/dist/tenant/http.js +9 -0
  260. package/dist/tenant/identity-file.d.ts +24 -1
  261. package/dist/tenant/identity-file.js +35 -7
  262. package/dist/tenant/issuers.d.ts +5 -5
  263. package/dist/tenant/issuers.js +31 -23
  264. package/dist/tenant/jwt.d.ts +8 -2
  265. package/dist/tenant/jwt.js +10 -2
  266. package/dist/tenant/limits.d.ts +90 -0
  267. package/dist/tenant/limits.js +175 -0
  268. package/dist/tenant/module.d.ts +4 -1
  269. package/dist/tenant/module.js +9 -8
  270. package/dist/tenant/outbound.d.ts +17 -19
  271. package/dist/tenant/outbound.js +18 -21
  272. package/dist/tenant/run-grants.js +3 -2
  273. package/dist/tenant/run-token.d.ts +0 -4
  274. package/dist/tenant/run-token.js +6 -11
  275. package/dist/tenant/runtime-limits.d.ts +24 -0
  276. package/dist/tenant/runtime-limits.js +27 -0
  277. package/dist/tenant/runtime.js +30 -43
  278. package/dist/tenant/session-streams.js +7 -6
  279. package/dist/tenant/session.d.ts +1 -1
  280. package/dist/tenant/session.js +1 -1
  281. package/dist/tenant/sessions.js +32 -29
  282. package/dist/tenant/signing-keys.d.ts +1 -1
  283. package/dist/tenant/signing-keys.js +5 -6
  284. package/dist/tenant/status.js +4 -3
  285. package/dist/tenant/streams.js +18 -20
  286. package/dist/tenant/sweep.d.ts +1 -21
  287. package/dist/tenant/sweep.js +39 -48
  288. package/dist/tenant/transcript.d.ts +70 -23
  289. package/dist/tenant/transcript.js +28 -22
  290. package/dist/tenant/types.d.ts +11 -16
  291. package/dist/tenant/worker.d.ts +2 -10
  292. package/dist/tenant/worker.js +5 -14
  293. package/dist/traces/ids.js +5 -1
  294. package/dist/traces/map.d.ts +9 -6
  295. package/dist/traces/map.js +9 -11
  296. package/dist/traces/otlp.d.ts +3 -3
  297. package/dist/traces/otlp.js +5 -3
  298. package/dist/traces/projector.d.ts +9 -4
  299. package/dist/traces/projector.js +13 -7
  300. package/dist/traces/read.js +6 -1
  301. package/dist/tuning.d.ts +383 -0
  302. package/dist/tuning.js +658 -0
  303. package/dist/vault/headers.js +4 -2
  304. package/dist/version.d.ts +1 -1
  305. package/dist/version.js +1 -1
  306. package/package.json +3 -3
  307. package/dist/artifacts/export.d.ts +0 -8
  308. package/dist/artifacts/export.js +0 -204
  309. package/dist/artifacts/media-types.d.ts +0 -30
  310. package/dist/artifacts/media-types.js +0 -137
  311. package/dist/artifacts/workspace.d.ts +0 -60
  312. package/dist/artifacts/workspace.js +0 -45
  313. package/dist/media.d.ts +0 -10
  314. package/dist/media.js +0 -58
  315. package/dist/sandbox/runtime-tools.d.ts +0 -9
  316. package/dist/sandbox/runtime-tools.js +0 -16
  317. package/dist/sandbox/session-sandbox.d.ts +0 -16
  318. package/dist/sandbox/session-sandbox.js +0 -77
  319. package/dist/tenant/artifact-tool.d.ts +0 -13
  320. package/dist/tenant/artifact-tool.js +0 -243
  321. package/dist/tenant/skill-tool.d.ts +0 -20
  322. package/dist/tenant/skill-tool.js +0 -67
  323. package/dist/tenant/tool-results.d.ts +0 -57
  324. package/dist/tenant/tool-results.js +0 -298
  325. package/dist/tenant/tool-search.d.ts +0 -39
  326. package/dist/tenant/tool-search.js +0 -103
@@ -1,12 +1,35 @@
1
- import { SANDBOX_WORKSPACE, } from "@nylorun/core/define";
1
+ /**
2
+ * Sandbox Manager: owns sandbox lifecycle for the Runtime. One workspace per sandbox resource
3
+ * (`sandbox: { id }`, keyed by the sandbox id), or per owning session for a sandbox chosen
4
+ * inline or by the Tenant default (keyed by the session id); created on the first sandbox tool
5
+ * call, stopped after its idle timeout, reattached on the next call. Each session's calls on a
6
+ * workspace run in the order it sends them, and different sessions' calls run at the same time
7
+ * (D66): the sessions of one Environment run their turns at once. Opening, stopping and removing
8
+ * a workspace run alone: a stop or removal waits for the calls running, and calls that come after
9
+ * it wait for it.
10
+ *
11
+ * The manager keeps no timers and reaches no store: it keeps its compute records through a
12
+ * `SandboxRecords` port (the Tenant's `sandboxes` table in core's process, a file in a harness's)
13
+ * and writes its events through `emit`. Its owner drives it: `stopIdle` stops sandboxes idle
14
+ * past their timeout and marks records of compute this process no longer holds as stopped
15
+ * (after a restart); `reconcile` removes workspaces whose owner is gone. `sweep` does both, the
16
+ * reconcile once per process, as the Tenant sweep does in core's process.
17
+ *
18
+ * A folder the harness asks for (`mountFolder`: our engine's skill folders, `/skills/<name>@<hash>/`,
19
+ * D66, protocol 15) is written read-only, once per open handle: the harness decides which folders
20
+ * and when, and gives their bytes; the manager only writes them (`skills.ts`).
21
+ */
22
+ import { posix } from "node:path";
23
+ import { SANDBOX_WORKSPACE } from "@nylorun/core/define";
2
24
  import { sandboxWorkspaceKey, sessionWorkspaceKey, workspacePrefix, } from "./records.js";
3
25
  import { DEFAULT_SANDBOX_CPUS, DEFAULT_SANDBOX_IMAGE, describeNetwork, idleMsOf, memoryMiBOf, resolveNetwork, } from "./policy.js";
4
26
  import { parseSandboxPreference, reportSelection, selectSandboxBackend, } from "./select.js";
5
- import { mountSkill, sandboxSkillsOf } from "./skills.js";
27
+ import { mountFolderFiles } from "./skills.js";
28
+ import { DEFAULT_SANDBOX_TOOL_LIMITS } from "@nylorun/harness";
6
29
  import { runDiskOp } from "./disk-fs.js";
7
- import { resolveSandboxPath, runSandboxTool, } from "./tools.js";
30
+ import { helperCommandTimeoutMs, quote, resolveSandboxPath, runSandboxTool, } from "./tools.js";
8
31
  import { SandboxFileTooLargeError, } from "./types.js";
9
- export { sandboxToolSpec } from "./runtime-tools.js";
32
+ import { hostTuning } from "../tuning.js";
10
33
  const READ_TOOLS = new Set(["read", "grep", "glob"]);
11
34
  export class SandboxManager {
12
35
  options;
@@ -99,12 +122,16 @@ export class SandboxManager {
99
122
  async report() {
100
123
  return { ...reportSelection(await this.ready), defaultImage: DEFAULT_SANDBOX_IMAGE };
101
124
  }
102
- async run(session, sandbox, toolName, input, signal) {
125
+ /**
126
+ * Runs sandbox operation `toolName` in the session's sandbox, held to `limits`: the harness's
127
+ * sandbox tool limits (`DEFAULT_SANDBOX_TOOL_LIMITS` when it names none).
128
+ */
129
+ async run(session, sandbox, toolName, input, signal, limits = DEFAULT_SANDBOX_TOOL_LIMITS) {
103
130
  const prepared = await this.prepare(session, sandbox);
104
131
  if ("kind" in prepared)
105
132
  return prepared;
106
133
  const { live: entry, spec } = prepared;
107
- return this.inLane(entry, session.id, () => this.execute(entry, session, spec, toolName, input, signal));
134
+ return this.inLane(entry, session.id, () => this.execute(entry, session, spec, toolName, input, signal, limits));
108
135
  }
109
136
  /**
110
137
  * Runs `call` in session `sessionId`'s queue on `live`: after its earlier calls, and after any
@@ -159,8 +186,8 @@ export class SandboxManager {
159
186
  return handle;
160
187
  }
161
188
  /**
162
- * The bytes of one file in the session's sandbox, for `save_artifact` (F8.1), in the same queue
163
- * as its tool calls: `missing` when there is no such file, a failed outcome when the sandbox
189
+ * The bytes of one file in the session's sandbox, for our engine's `save_artifact` and its
190
+ * outputs folder (`files.read`), in the same queue as its tool calls: `missing` when there is no such file, a failed outcome when the sandbox
164
191
  * cannot open or the file is larger than `maxBytes`. Relative paths resolve in the workspace.
165
192
  */
166
193
  async readBytes(session, sandbox, path, maxBytes, signal) {
@@ -190,8 +217,37 @@ export class SandboxManager {
190
217
  });
191
218
  }
192
219
  /**
193
- * The files under directory `dir` of the session's sandbox, for the turn-end export (F8.2), in
194
- * the same queue as its tool calls. `missing` when the sandbox was never created (nothing is
220
+ * Writes `body` to one file of the session's sandbox, as it comes, for our engine's
221
+ * `read_artifact` with `path` (protocol 15), in the same queue as its tool calls: its folders
222
+ * are made first, and a file there is replaced. Relative paths resolve in the workspace.
223
+ */
224
+ async writeBytes(session, sandbox, path, body, signal) {
225
+ return this.withHandle(session, sandbox, signal, async (handle) => {
226
+ const resolved = resolveSandboxPath(path, handle.workspace ?? SANDBOX_WORKSPACE);
227
+ const parent = posix.dirname(resolved);
228
+ const made = await handle.exec({ command: `mkdir -p ${quote(parent)}`, cwd: "/", timeoutMs: helperCommandTimeoutMs() }, signal);
229
+ if (made.exitCode !== 0)
230
+ return {
231
+ kind: "failed",
232
+ code: "sandbox.write_failed",
233
+ message: made.stderr.trim() || `Could not create ${parent}`,
234
+ };
235
+ let size = 0;
236
+ const counted = body instanceof Uint8Array
237
+ ? ((size = body.byteLength), body)
238
+ : (async function* () {
239
+ for await (const chunk of body) {
240
+ size += chunk.byteLength;
241
+ yield chunk;
242
+ }
243
+ })();
244
+ await handle.writeFile(resolved, counted);
245
+ return { kind: "written", path: resolved, size };
246
+ });
247
+ }
248
+ /**
249
+ * The files under directory `dir` of the session's sandbox, for our engine's outputs folder
250
+ * (`files.list`, protocol 15), in the same queue as its tool calls. `missing` when the sandbox was never created (nothing is
195
251
  * started to look) or `dir` is not a directory. Relative paths resolve in the workspace.
196
252
  */
197
253
  async listFiles(session, sandbox, dir, maxEntries, signal) {
@@ -252,9 +308,6 @@ export class SandboxManager {
252
308
  message: `The sandbox could not start: ${error instanceof Error ? error.message : String(error)}`,
253
309
  };
254
310
  }
255
- const unmounted = await this.mountSkills(live, handle, session, signal);
256
- if (unmounted)
257
- return unmounted;
258
311
  return await use(handle, live);
259
312
  });
260
313
  }
@@ -294,48 +347,52 @@ export class SandboxManager {
294
347
  lastUsedAt: Date.now(),
295
348
  lanes: new Map(),
296
349
  gate: Promise.resolve(),
297
- skills: new Map(),
350
+ mounts: new Map(),
298
351
  };
299
352
  this.live.set(key, live);
300
353
  }
301
354
  return { live, spec };
302
355
  }
303
- async execute(live, session, spec, toolName, input, signal) {
356
+ async execute(live, session, spec, toolName, input, signal, limits) {
304
357
  if (signal.aborted)
305
358
  throw new Error("Turn cancelled");
306
359
  if (this.closing)
307
360
  throw new Error("Runtime is shutting down");
308
- return this.executeOpen(live, session, spec, toolName, input, signal);
361
+ return this.executeOpen(live, session, spec, toolName, input, signal, limits);
309
362
  }
310
- async executeOpen(live, session, spec, toolName, input, signal) {
311
- let handle;
363
+ /**
364
+ * The workspace's handle, opened when there is none. A start that fails is recorded as stopped
365
+ * and answered as a failed outcome: nothing ran yet, so it is definite, not uncertain.
366
+ */
367
+ async started(live, session, spec) {
312
368
  try {
313
- handle = await this.handleOf(live, session, spec);
369
+ return await this.handleOf(live, session, spec);
314
370
  }
315
371
  catch (error) {
316
372
  const message = error instanceof Error ? error.message : String(error);
317
373
  const record = await this.record(spec.key, live, session.id, spec.image, "stopped");
318
374
  await this.options.emit(session.id, session.activeTurnId, "environment.state", { state: "stopped", backend: live.backend.name, error: message }, { record, ...claimOf(session) });
319
- // Nothing ran yet, so a failed start is a definite outcome, not an uncertain one.
320
375
  return {
321
376
  kind: "failed",
322
377
  code: "sandbox.start_failed",
323
378
  message: `The sandbox could not start on ${live.backend.name} (image ${spec.image ?? "the backend's default"}): ${message}`,
324
379
  };
325
380
  }
326
- const unmounted = await this.mountSkills(live, handle, session, signal);
327
- if (unmounted)
328
- return unmounted;
381
+ }
382
+ async executeOpen(live, session, spec, toolName, input, signal, limits) {
383
+ const handle = await this.started(live, session, spec);
384
+ if ("kind" in handle)
385
+ return handle;
329
386
  const started = Date.now();
330
387
  let report = {};
331
388
  try {
332
389
  const outcome = await runSandboxTool(handle, toolName, (input ?? {}), signal, (value) => {
333
390
  report = value;
334
- }, session.environment);
391
+ }, limits, session.environment);
335
392
  await this.options.emit(session.id, session.activeTurnId, "environment.exec", {
336
393
  tool: toolName,
337
394
  ...report,
338
- ...(report.command ? { command: report.command.slice(0, 500) } : {}),
395
+ ...(report.command ? { command: report.command.slice(0, hostTuning().harness.execCommandChars) } : {}),
339
396
  outcome: outcome.kind,
340
397
  ...(outcome.kind === "failed" ? { code: outcome.code } : {}),
341
398
  durationMs: Date.now() - started,
@@ -356,23 +413,34 @@ export class SandboxManager {
356
413
  }
357
414
  }
358
415
  /**
359
- * Mounts the skill folders the session needs that the workspace does not hold (D66): on a
360
- * handle just opened all of them, later only versions another session has not mounted. Calls
361
- * that need one folder at once share its mount. A failed outcome when one cannot be mounted;
362
- * the folder is not marked mounted then, so the next call tries again.
416
+ * Writes `files` (path in `folder` to `sha256:<hex>`) to `folder` of the session's sandbox,
417
+ * read-only, each one's bytes from `read`, in the session's queue (protocol 15: the harness
418
+ * decides which folders, and when). A folder the open handle already holds is `present` without
419
+ * an exec; calls that need one folder at once share its mount. A failed outcome when the sandbox
420
+ * cannot start or the folder cannot be written; it is not marked mounted then, so the next call
421
+ * tries again. A reopened handle holds none.
363
422
  */
364
- async mountSkills(live, handle, session, signal) {
365
- const read = this.options.definitionFile;
366
- if (!read)
367
- return undefined;
368
- for (const [folder, files] of sandboxSkillsOf(session.manifest)) {
369
- let mounting = live.skills.get(folder);
423
+ async mountFolder(session, sandbox, folder, files, read, signal) {
424
+ const prepared = await this.prepare(session, sandbox);
425
+ if ("kind" in prepared)
426
+ return prepared;
427
+ const { live, spec } = prepared;
428
+ return this.inLane(live, session.id, async () => {
429
+ if (signal.aborted)
430
+ throw new Error("Turn cancelled");
431
+ if (this.closing)
432
+ throw new Error("Runtime is shutting down");
433
+ const handle = await this.started(live, session, spec);
434
+ if ("kind" in handle)
435
+ return handle;
436
+ let mounting = live.mounts.get(folder);
437
+ const present = mounting !== undefined;
370
438
  if (!mounting) {
371
- const mount = mountSkill(handle, folder, files, (sha256) => read(sha256, session), signal);
372
- live.skills.set(folder, mount);
439
+ const mount = mountFolderFiles(handle, folder, files, async (sha256) => read(sha256), signal);
440
+ live.mounts.set(folder, mount);
373
441
  void mount.catch(() => {
374
- if (live.skills.get(folder) === mount)
375
- live.skills.delete(folder);
442
+ if (live.mounts.get(folder) === mount)
443
+ live.mounts.delete(folder);
376
444
  });
377
445
  mounting = mount;
378
446
  }
@@ -385,11 +453,11 @@ export class SandboxManager {
385
453
  return {
386
454
  kind: "failed",
387
455
  code: "sandbox.skills_unavailable",
388
- message: `The files of skill folder ${folder} could not be put in the sandbox: ${error instanceof Error ? error.message : String(error)}`,
456
+ message: `The files of folder ${folder} could not be put in the sandbox: ${error instanceof Error ? error.message : String(error)}`,
389
457
  };
390
458
  }
391
- }
392
- return undefined;
459
+ return { kind: present ? "present" : "mounted" };
460
+ });
393
461
  }
394
462
  async open(live, session, spec) {
395
463
  const existing = await this.options.records.get(spec.key);
@@ -402,8 +470,8 @@ export class SandboxManager {
402
470
  const creating = await this.record(spec.key, live, session.id, spec.image, "creating", existing);
403
471
  await this.options.emit(session.id, session.activeTurnId, "environment.state", { state: "creating", ...payload, ...(existing ? { reattach: true } : {}) }, { record: creating, ...claimOf(session) });
404
472
  const handle = await live.backend.open(spec);
405
- // A handle opened afresh holds no skills this manager knows of.
406
- live.skills.clear();
473
+ // A handle opened afresh holds no folder this manager knows of.
474
+ live.mounts.clear();
407
475
  const running = await this.record(spec.key, live, session.id, spec.image, "running", existing);
408
476
  await this.options.emit(session.id, session.activeTurnId, "environment.state", {
409
477
  state: "running",
@@ -22,7 +22,7 @@ export declare function placementOf(config: TenantSandboxConfig, harnessId?: str
22
22
  export declare const ENABLE_PODS = "run `nylorun local sandbox enable --context <name>` to give this Tenant a cluster";
23
23
  /** Refuses a session whose harness, on the host its Environment needs, the Tenant does not allow. */
24
24
  export declare function checkPlacement(config: TenantSandboxConfig, kind: EnvironmentKind | undefined, harness?: string): void;
25
- /** Whether the sandboxes service is up, cached for a few seconds. */
25
+ /** Whether the sandboxes service is up, the answer reused for `pods.readyCacheMs` (Host tuning). */
26
26
  export declare function podsReady(pods: TenantPods): Promise<boolean>;
27
27
  /** Refuses a pod Environment without sandbox pods, or with the sandboxes service down. */
28
28
  export declare function requirePods(pods: TenantPods | undefined, what: string): Promise<TenantPods>;
@@ -11,7 +11,8 @@
11
11
  * sandbox enable`), or with the sandboxes service not ready, it is `409 environment_unavailable`.
12
12
  */
13
13
  import { DEFAULT_HARNESS, } from "@nylorun/core/contracts";
14
- import { fail } from "../tenant/http.js";
14
+ import { fail, failRetryLater } from "../tenant/http.js";
15
+ import { hostTuning } from "../tuning.js";
15
16
  /** Our harness's id (blueprint D38): what a session runs unless its Environment names another. */
16
17
  export const HARNESS_ID = DEFAULT_HARNESS;
17
18
  export const DEFAULT_PLACEMENT = ["harness-container", "sandbox"];
@@ -35,12 +36,11 @@ export function checkPlacement(config, kind, harness = HARNESS_ID) {
35
36
  ? `This Tenant does not let the ${harness} harness run in a sandbox pod (${where}).`
36
37
  : `This Tenant runs the ${harness} harness only in sandbox pods (${where}): open the session on a pod Environment.`, { code: "placement_refused", details: { harness, host, allowed: [...allowed] } });
37
38
  }
38
- const READY_CACHE_MS = 5_000;
39
39
  const ready = new WeakMap();
40
- /** Whether the sandboxes service is up, cached for a few seconds. */
40
+ /** Whether the sandboxes service is up, the answer reused for `pods.readyCacheMs` (Host tuning). */
41
41
  export async function podsReady(pods) {
42
42
  const cached = ready.get(pods);
43
- if (cached && Date.now() - cached.at < READY_CACHE_MS)
43
+ if (cached && Date.now() - cached.at < hostTuning().pods.readyCacheMs)
44
44
  return cached.ready;
45
45
  const answer = await pods.client.ready();
46
46
  ready.set(pods, { at: Date.now(), ready: answer });
@@ -53,9 +53,6 @@ export async function requirePods(pods, what) {
53
53
  code: "environment_unavailable",
54
54
  });
55
55
  if (!(await podsReady(pods)))
56
- fail(409, `${what} needs sandbox pods, and the sandboxes service is not ready. Check \`nylorun local sandbox status\`.`, {
57
- code: "environment_unavailable",
58
- details: { retryAfterSeconds: 5 },
59
- }, { "retry-after": "5" });
56
+ failRetryLater(`${what} needs sandbox pods, and the sandboxes service is not ready. Check \`nylorun local sandbox status\`.`, "environment_unavailable");
60
57
  return pods;
61
58
  }
@@ -1,12 +1,3 @@
1
- /**
2
- * The sandboxes service's client (F7.2, D32): the Go service (`sandboxes/`) that alone holds
3
- * the cluster credentials and drives agent-sandbox Sandboxes in the Tenant's namespace. Core
4
- * reaches it at `NYLORUN_SANDBOXES_URL` with `NYLORUN_SANDBOXES_TOKEN`; nothing else does.
5
- *
6
- * Pull, not push: the service never calls core. Core reads a Sandbox's status when it
7
- * reconciles (`GET /v1/pods/{name}`, optionally blocking with `?wait=`), and every change it
8
- * asks for carries an operation id, so a retried `PUT` is a no-op.
9
- */
10
1
  /** `PUT /v1/pods/{name}`: the Sandbox the service applies (`driver.Spec` in Go). */
11
2
  export interface PodSpec {
12
3
  readonly opId: string;
@@ -92,6 +83,6 @@ export declare function httpSandboxesClient(options: {
92
83
  readonly url: string;
93
84
  readonly token: string;
94
85
  readonly fetch?: typeof fetch;
95
- /** Per request, unless the request waits longer. Default 20 s. */
86
+ /** Per request, unless the request waits longer. Default `pods.requestTimeoutMs` (Host tuning, 20 s). */
96
87
  readonly timeoutMs?: number;
97
88
  }): SandboxesClient;
@@ -7,6 +7,7 @@
7
7
  * reconciles (`GET /v1/pods/{name}`, optionally blocking with `?wait=`), and every change it
8
8
  * asks for carries an operation id, so a retried `PUT` is a no-op.
9
9
  */
10
+ import { hostTuning, SANDBOXES_MAX_WAIT_MS } from "../../tuning.js";
10
11
  /** The service refused a request, or did not answer. */
11
12
  export class SandboxesError extends Error {
12
13
  status;
@@ -23,7 +24,7 @@ export function httpSandboxesClient(options) {
23
24
  const base = options.url.replace(/\/+$/, "");
24
25
  const call = options.fetch ?? fetch;
25
26
  const request = async (method, path, init = {}) => {
26
- const timeout = AbortSignal.timeout(init.timeoutMs ?? options.timeoutMs ?? 20_000);
27
+ const timeout = AbortSignal.timeout(init.timeoutMs ?? options.timeoutMs ?? hostTuning().pods.requestTimeoutMs);
27
28
  const signal = init.signal ? AbortSignal.any([init.signal, timeout]) : timeout;
28
29
  let response;
29
30
  try {
@@ -58,7 +59,7 @@ export function httpSandboxesClient(options) {
58
59
  return {
59
60
  async ready(signal) {
60
61
  try {
61
- await request("GET", "/ready", { auth: false, timeoutMs: 3_000, ...(signal ? { signal } : {}) });
62
+ await request("GET", "/ready", { auth: false, timeoutMs: hostTuning().pods.readyProbeTimeoutMs, ...(signal ? { signal } : {}) });
62
63
  return true;
63
64
  }
64
65
  catch {
@@ -68,10 +69,13 @@ export function httpSandboxesClient(options) {
68
69
  info: (signal) => request("GET", "/v1/info", signal ? { signal } : {}),
69
70
  put: (name, spec, signal) => request("PUT", pod(name), { body: spec, ...(signal ? { signal } : {}) }),
70
71
  status(name, { wait, timeoutMs, signal } = {}) {
71
- const query = wait ? `?wait=${wait}&timeoutMs=${Math.min(timeoutMs ?? 30_000, 30_000)}` : "";
72
+ const tuning = hostTuning().pods;
73
+ // The service waits at most `SANDBOXES_MAX_WAIT_MS`; the answer has `waitSlackMs` to arrive.
74
+ const waitMs = Math.min(timeoutMs ?? tuning.waitMs, SANDBOXES_MAX_WAIT_MS);
75
+ const query = wait ? `?wait=${wait}&timeoutMs=${waitMs}` : "";
72
76
  return request("GET", `${pod(name)}${query}`, {
73
77
  ...(signal ? { signal } : {}),
74
- ...(wait ? { timeoutMs: Math.min(timeoutMs ?? 30_000, 30_000) + 10_000 } : {}),
78
+ ...(wait ? { timeoutMs: waitMs + tuning.waitSlackMs } : {}),
75
79
  });
76
80
  },
77
81
  delete: (name, opId, signal) => request("DELETE", `${pod(name)}?opId=${encodeURIComponent(opId)}`, signal ? { signal } : {}),
@@ -7,7 +7,7 @@
7
7
  * per sandbox and keeps its timers.
8
8
  *
9
9
  * - **Create** is eager: a `PUT` of a new pod sandbox asks for `Running` at once. One that is
10
- * not ready within `READY_DEADLINE_MS` is `failed`, with the pod's reason; the next turn
10
+ * not ready within `pods.readyDeadlineMs` (Host tuning) is `failed`, with the pod's reason; the next turn
11
11
  * start tries again.
12
12
  * - **Every change** the Runtime asks for bumps `rev`; the `PUT` carries `opId =
13
13
  * <name>.r<rev>`, so a repeated apply is a no-op at the service, and a Sandbox whose `opId`
@@ -29,7 +29,9 @@
29
29
  * reason that starts `setup:`, even while the pod is ready, until a turn start asks it to run.
30
30
  * - **Image** (Q14): the first ready pod's workload image is recorded by digest (`imageDigest`),
31
31
  * and every later apply runs that digest, so a stop, relaunch or reset never pulls another
32
- * image under the same tag. An image id with no repository (a locally loaded image) pins nothing.
32
+ * image under the same tag. Only a digest of the repository the Environment asked for is
33
+ * pinned: an image id with no repository, or one a node gave a name of its own (kind names an
34
+ * image it loads `import-<date>`), pins nothing, since no registry serves it under that name.
33
35
  *
34
36
  * An unused Environment is deleted by the Tenant sweep (`lifecycle.deleteAfter`), not here.
35
37
  */
@@ -37,18 +39,19 @@ import type { EnvironmentEventPayload, EnvironmentEventType } from "@nylorun/cor
37
39
  import type { Environment, SandboxPodPatch, SandboxPodState } from "../../store/types.js";
38
40
  import type { PodStatus } from "./client.js";
39
41
  /**
40
- * The pullable reference of the image id a container status names (`repo@sha256:…`), or
41
- * undefined for one without a repository, such as an image loaded onto the node.
42
+ * The repository an image reference names, as a container runtime writes it: the tag and digest
43
+ * dropped, Docker Hub's host and `library/` filled in (`python:3.13-slim` is
44
+ * `docker.io/library/python`).
42
45
  */
43
- export declare function imageDigestOf(imageID: string | undefined): string | undefined;
46
+ export declare function repositoryOf(reference: string): string;
47
+ /**
48
+ * The pullable reference of the image id a container status names (`repo@sha256:…`), when it is
49
+ * a digest of `requested`'s repository; undefined for an id without a repository, or with one
50
+ * the node made up for an image loaded onto it, which no registry serves.
51
+ */
52
+ export declare function imageDigestOf(imageID: string | undefined, requested: string): string | undefined;
44
53
  /** A setup failure's `reason` starts with this (`sandbox/setup.ts`); it is sticky. */
45
54
  export declare const SETUP_FAILED = "setup:";
46
- /** A pod not ready this long after it was asked to run has failed. */
47
- export declare const READY_DEADLINE_MS: number;
48
- /** How soon to look again while the pod changes. */
49
- export declare const POLL_MS = 2000;
50
- /** How soon to look again when the sandboxes service does not answer. */
51
- export declare const UNREACHABLE_RETRY_MS = 5000;
52
55
  /** The lifecycle settings (D36), read each time a decision is made. */
53
56
  export interface PodLifecycleConfig {
54
57
  readonly idleMs: number;
@@ -61,6 +64,11 @@ export type PodRow = Environment & {
61
64
  };
62
65
  export interface PodFacts {
63
66
  readonly row: PodRow;
67
+ /**
68
+ * The workload image the pod is asked to run: its applied recipe's, else the sandboxes
69
+ * service's default (`DEFAULT_SANDBOX_IMAGE`). Only a digest of its repository is pinned.
70
+ */
71
+ readonly image: string;
64
72
  readonly config: PodLifecycleConfig;
65
73
  /** The Sandbox's status; undefined when the sandboxes service did not answer. */
66
74
  readonly status: PodStatus | undefined;
@@ -1,22 +1,41 @@
1
+ import { hostTuning } from "../../tuning.js";
2
+ import { DEFAULT_SANDBOX_IMAGE } from "../policy.js";
1
3
  /**
2
- * The pullable reference of the image id a container status names (`repo@sha256:…`), or
3
- * undefined for one without a repository, such as an image loaded onto the node.
4
+ * The repository an image reference names, as a container runtime writes it: the tag and digest
5
+ * dropped, Docker Hub's host and `library/` filled in (`python:3.13-slim` is
6
+ * `docker.io/library/python`).
4
7
  */
5
- export function imageDigestOf(imageID) {
8
+ export function repositoryOf(reference) {
9
+ let name = reference.replace(/^docker-pullable:\/\//, "");
10
+ const at = name.indexOf("@");
11
+ if (at >= 0)
12
+ name = name.slice(0, at);
13
+ const colon = name.lastIndexOf(":");
14
+ if (colon > name.lastIndexOf("/"))
15
+ name = name.slice(0, colon);
16
+ const parts = name.split("/");
17
+ const first = parts[0] ?? "";
18
+ const hasHost = parts.length > 1 && (first.includes(".") || first.includes(":") || first === "localhost");
19
+ if (hasHost)
20
+ return name;
21
+ return parts.length === 1 ? `docker.io/library/${name}` : `docker.io/${name}`;
22
+ }
23
+ /**
24
+ * The pullable reference of the image id a container status names (`repo@sha256:…`), when it is
25
+ * a digest of `requested`'s repository; undefined for an id without a repository, or with one
26
+ * the node made up for an image loaded onto it, which no registry serves.
27
+ */
28
+ export function imageDigestOf(imageID, requested) {
6
29
  const id = imageID?.replace(/^docker-pullable:\/\//, "");
7
30
  if (!id)
8
31
  return undefined;
9
32
  const at = id.indexOf("@sha256:");
10
- return at > 0 && /^[0-9a-f]{64}$/.test(id.slice(at + 8)) ? id : undefined;
33
+ if (at <= 0 || !/^[0-9a-f]{64}$/.test(id.slice(at + 8)))
34
+ return undefined;
35
+ return repositoryOf(id) === repositoryOf(requested) ? id : undefined;
11
36
  }
12
37
  /** A setup failure's `reason` starts with this (`sandbox/setup.ts`); it is sticky. */
13
38
  export const SETUP_FAILED = "setup:";
14
- /** A pod not ready this long after it was asked to run has failed. */
15
- export const READY_DEADLINE_MS = 5 * 60_000;
16
- /** How soon to look again while the pod changes. */
17
- export const POLL_MS = 2_000;
18
- /** How soon to look again when the sandboxes service does not answer. */
19
- export const UNREACHABLE_RETRY_MS = 5_000;
20
39
  /** The service's operation id of the row's current spec. */
21
40
  export function opIdOf(pod) {
22
41
  return `${pod.k8sName}.r${pod.rev}`;
@@ -28,6 +47,9 @@ function gone(status) {
28
47
  const event = (type, payload) => ({ type, payload });
29
48
  export function decide(facts) {
30
49
  const { row, status, now } = facts;
50
+ // A pod not ready `readyDeadlineMs` after it was asked to run has failed; look again after
51
+ // `pollMs` while it changes, or `unreachableRetryMs` when the sandboxes service does not answer.
52
+ const { readyDeadlineMs, pollMs, unreachableRetryMs } = hostTuning().pods;
31
53
  const pod = row.pod;
32
54
  const patch = {};
33
55
  const events = [];
@@ -49,17 +71,17 @@ export function decide(facts) {
49
71
  if (pod.retiring !== undefined) {
50
72
  const retiring = facts.retiring;
51
73
  if (retiring === undefined)
52
- again(UNREACHABLE_RETRY_MS);
74
+ again(unreachableRetryMs);
53
75
  else if (gone(retiring))
54
76
  patch.retiring = null;
55
77
  else {
56
78
  if (retiring.exists && !retiring.deleting)
57
79
  deletes.push(pod.retiring);
58
- again(POLL_MS);
80
+ again(pollMs);
59
81
  }
60
82
  }
61
83
  if (status === undefined) {
62
- again(UNREACHABLE_RETRY_MS);
84
+ again(unreachableRetryMs);
63
85
  return done();
64
86
  }
65
87
  if (pod.desired === "deleted") {
@@ -69,7 +91,7 @@ export function decide(facts) {
69
91
  deletes.push(pod.k8sName);
70
92
  if (pod.observed !== "deleting")
71
93
  patch.observed = "deleting";
72
- again(POLL_MS);
94
+ again(pollMs);
73
95
  return done();
74
96
  }
75
97
  // Lost is terminal: only a reset (a new name) leaves it.
@@ -77,7 +99,7 @@ export function decide(facts) {
77
99
  if (status.exists && !status.deleting)
78
100
  deletes.push(pod.k8sName);
79
101
  if (status.exists)
80
- again(POLL_MS);
102
+ again(pollMs);
81
103
  return done();
82
104
  }
83
105
  let desired = pod.desired;
@@ -110,12 +132,12 @@ export function decide(facts) {
110
132
  if (status.exists && !status.deleting)
111
133
  deletes.push(pod.k8sName);
112
134
  if (status.exists)
113
- again(POLL_MS);
135
+ again(pollMs);
114
136
  return done();
115
137
  }
116
138
  if (status.deleting) {
117
139
  // The previous incarnation of this name is still going (an Environment deleted and created again).
118
- again(POLL_MS);
140
+ again(pollMs);
119
141
  return done();
120
142
  }
121
143
  const opId = opIdOf({ k8sName: pod.k8sName, rev });
@@ -133,7 +155,7 @@ export function decide(facts) {
133
155
  // A pod started by this apply (a create, a resume) is the Environment's first, not a relaunch.
134
156
  if (desired === "running" && pod.podUid !== undefined)
135
157
  patch.podUid = null;
136
- again(POLL_MS);
158
+ again(pollMs);
137
159
  return done({
138
160
  apply: { mode: desired === "running" ? "Running" : "Suspended", rotateJoin: !status.exists },
139
161
  });
@@ -149,7 +171,7 @@ export function decide(facts) {
149
171
  patch.reason = null;
150
172
  }
151
173
  // The image it runs, pinned by digest from now on (Q14).
152
- const digest = pod.imageDigest === undefined ? imageDigestOf(status.imageID) : undefined;
174
+ const digest = pod.imageDigest === undefined ? imageDigestOf(status.imageID, facts.image) : undefined;
153
175
  if (digest !== undefined)
154
176
  patch.imageDigest = digest;
155
177
  return done();
@@ -157,9 +179,9 @@ export function decide(facts) {
157
179
  const startedAt = Date.parse(pod.startedAt ?? row.updatedAt);
158
180
  if (pod.observed === "failed")
159
181
  return done();
160
- if (now - startedAt >= READY_DEADLINE_MS) {
182
+ if (now - startedAt >= readyDeadlineMs) {
161
183
  const reason = [status.reason, status.message].filter(Boolean).join(": ") ||
162
- `The pod was not ready within ${READY_DEADLINE_MS / 60_000} minutes`;
184
+ `The pod was not ready within ${Math.round(readyDeadlineMs / 1000)} s`;
163
185
  patch.observed = "failed";
164
186
  patch.reason = reason;
165
187
  events.push(event("environment.failed", { reason }));
@@ -167,7 +189,7 @@ export function decide(facts) {
167
189
  }
168
190
  if (pod.observed !== "creating")
169
191
  patch.observed = "creating";
170
- again(POLL_MS);
192
+ again(pollMs);
171
193
  return done();
172
194
  }
173
195
  // Suspended, applied: suspended once its pod is gone.
@@ -176,6 +198,6 @@ export function decide(facts) {
176
198
  patch.observed = "suspended";
177
199
  return done();
178
200
  }
179
- again(POLL_MS);
201
+ again(pollMs);
180
202
  return done();
181
203
  }
@@ -2,20 +2,19 @@ export interface BlockedAddress {
2
2
  readonly host: string;
3
3
  readonly port: number;
4
4
  }
5
- /** Consecutive failed connections that prove an address blocked. */
6
- export declare const CONFIRMATIONS = 3;
7
5
  /** The addresses a pod's NetworkPolicy must block: the API server's service address. */
8
6
  export declare function blockedAddresses(env: Readonly<Record<string, string | undefined>>): BlockedAddress[];
9
7
  /** Whether a TCP connection to `address` opens within `timeoutMs`. */
10
8
  export declare function opens(address: BlockedAddress, timeoutMs: number): Promise<boolean>;
11
9
  export interface NetworkGateOptions {
12
10
  readonly addresses: readonly BlockedAddress[];
13
- /** Give up after this long. Default 120 s. */
11
+ /** Give up after this long. Default `harness.networkPolicyTimeoutMs` (Host tuning, 120 s). */
14
12
  readonly timeoutMs?: number;
15
- /** One connection attempt. Default 1 s. */
13
+ /** One connection attempt. Default `harness.networkProbeMs` (1 s). */
16
14
  readonly attemptMs?: number;
17
- /** Between attempts after a connection opened. Default 250 ms. */
15
+ /** Between attempts after a connection opened. Default `harness.networkPauseMs` (250 ms). */
18
16
  readonly pauseMs?: number;
17
+ /** Consecutive failed connections that prove an address blocked. Default `harness.networkConfirmations` (3). */
19
18
  readonly confirmations?: number;
20
19
  readonly log?: (message: string, fields: Record<string, unknown>) => void;
21
20
  }