@nylorun/runtime 0.22.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 (273) hide show
  1. package/CHANGELOG.md +161 -0
  2. package/README.md +95 -30
  3. package/dist/adapters/execution/restate.d.ts +15 -3
  4. package/dist/adapters/execution/restate.js +78 -84
  5. package/dist/adapters/replication/pgoutput.d.ts +2 -0
  6. package/dist/adapters/replication/pgoutput.js +6 -3
  7. package/dist/adapters/sandbox/local.d.ts +9 -1
  8. package/dist/adapters/sandbox/local.js +16 -4
  9. package/dist/adapters/sandbox/virtual.js +4 -1
  10. package/dist/adapters/streams/s2.js +1 -13
  11. package/dist/api/a2a/endpoint.js +5 -1
  12. package/dist/api/a2a/routes.d.ts +2 -0
  13. package/dist/api/a2a/routes.js +2 -2
  14. package/dist/api/a2a/tasks.js +2 -1
  15. package/dist/api/ag-ui/endpoint.js +4 -2
  16. package/dist/api/ag-ui/history.js +4 -1
  17. package/dist/api/ag-ui/routes.d.ts +4 -1
  18. package/dist/api/ag-ui/routes.js +11 -10
  19. package/dist/api/ag-ui/translate.js +3 -2
  20. package/dist/api/components.d.ts +4953 -1204
  21. package/dist/api/components.js +25 -16
  22. package/dist/api/gate/keys.js +7 -1
  23. package/dist/api/gate/routes.d.ts +6 -0
  24. package/dist/api/gate/routes.js +39 -3
  25. package/dist/api/http/app.js +4 -4
  26. package/dist/api/http/define.d.ts +6 -0
  27. package/dist/api/http/define.js +13 -1
  28. package/dist/api/http/respond.js +1 -1
  29. package/dist/api/http/routes/artifacts.js +35 -8
  30. package/dist/api/http/routes/coverage.d.ts +9 -0
  31. package/dist/api/http/routes/coverage.js +34 -0
  32. package/dist/api/http/routes/environments.d.ts +11 -0
  33. package/dist/api/http/routes/environments.js +283 -0
  34. package/dist/api/http/routes/mcp.d.ts +1 -1
  35. package/dist/api/http/routes/mcp.js +2 -2
  36. package/dist/api/http/routes/me.js +1 -1
  37. package/dist/api/http/routes/reads.d.ts +45 -9
  38. package/dist/api/http/routes/reads.js +54 -5
  39. package/dist/api/http/routes/sessions.js +11 -6
  40. package/dist/api/http/routes/tenant.js +32 -10
  41. package/dist/api/openapi.js +31 -26
  42. package/dist/artifacts/disk.d.ts +59 -0
  43. package/dist/artifacts/disk.js +563 -0
  44. package/dist/artifacts/export.d.ts +1 -1
  45. package/dist/artifacts/export.js +2 -3
  46. package/dist/artifacts/folders.d.ts +6 -3
  47. package/dist/artifacts/folders.js +17 -12
  48. package/dist/artifacts/service.d.ts +2 -1
  49. package/dist/artifacts/service.js +6 -1
  50. package/dist/artifacts/workspace.d.ts +1 -1
  51. package/dist/artifacts/workspace.js +1 -1
  52. package/dist/core/flow-host.d.ts +3 -22
  53. package/dist/core/flow-host.js +11 -37
  54. package/dist/core/provider.d.ts +3 -2
  55. package/dist/core/provider.js +5 -37
  56. package/dist/core/sandbox-routes.d.ts +2 -21
  57. package/dist/core/sandbox-routes.js +10 -43
  58. package/dist/execution/types.d.ts +11 -9
  59. package/dist/execution/types.js +5 -7
  60. package/dist/gates/contract.d.ts +3 -24
  61. package/dist/gates/contract.js +1 -36
  62. package/dist/gates/egress-credentials.d.ts +9 -0
  63. package/dist/gates/egress-credentials.js +11 -4
  64. package/dist/gates/egress.d.ts +28 -1
  65. package/dist/gates/egress.js +115 -10
  66. package/dist/gates/fixture-model.d.ts +28 -0
  67. package/dist/gates/fixture-model.js +92 -0
  68. package/dist/gates/handler.js +4 -4
  69. package/dist/gates/http-tool.d.ts +6 -2
  70. package/dist/gates/http-tool.js +3 -5
  71. package/dist/gates/in-process.d.ts +5 -0
  72. package/dist/gates/in-process.js +12 -1
  73. package/dist/gates/journal.d.ts +30 -0
  74. package/dist/gates/journal.js +52 -0
  75. package/dist/gates/mcp-handler.d.ts +4 -0
  76. package/dist/gates/mcp-handler.js +5 -1
  77. package/dist/gates/meter.d.ts +10 -2
  78. package/dist/gates/meter.js +20 -5
  79. package/dist/gates/policy.d.ts +49 -0
  80. package/dist/gates/policy.js +134 -0
  81. package/dist/gates/tenant-vaults.d.ts +7 -7
  82. package/dist/gates/tenant-vaults.js +5 -5
  83. package/dist/gates/tool-calls.d.ts +10 -5
  84. package/dist/gates/tool-calls.js +28 -10
  85. package/dist/gates/tool-client.d.ts +8 -0
  86. package/dist/gates/tool-client.js +27 -11
  87. package/dist/gates/tool-contract.d.ts +10 -68
  88. package/dist/gates/tool-contract.js +1 -85
  89. package/dist/gates/tool-gate.d.ts +1 -1
  90. package/dist/harness/calls.d.ts +12 -5
  91. package/dist/harness/calls.js +21 -12
  92. package/dist/harness/executors.js +12 -9
  93. package/dist/harness/main.js +18 -5
  94. package/dist/harness/pod.d.ts +34 -0
  95. package/dist/harness/pod.js +41 -0
  96. package/dist/harness/service.d.ts +0 -1
  97. package/dist/harness/service.js +13 -5
  98. package/dist/harness/setup.d.ts +27 -0
  99. package/dist/harness/setup.js +150 -0
  100. package/dist/harness/ws-client.js +3 -2
  101. package/dist/harness/ws-port.d.ts +1 -5
  102. package/dist/harness/ws-port.js +1 -5
  103. package/dist/harness-api/in-process.js +3 -3
  104. package/dist/harness-api/record.d.ts +15 -1
  105. package/dist/harness-api/record.js +174 -44
  106. package/dist/harness-api/redact.d.ts +3 -0
  107. package/dist/harness-api/redact.js +45 -0
  108. package/dist/harness-api/server.d.ts +8 -0
  109. package/dist/harness-api/server.js +93 -13
  110. package/dist/harness-api/start.d.ts +10 -3
  111. package/dist/harness-api/start.js +40 -10
  112. package/dist/harness-api/tool-journal.d.ts +96 -0
  113. package/dist/harness-api/tool-journal.js +102 -0
  114. package/dist/harness-api/tool-status.d.ts +13 -0
  115. package/dist/harness-api/tool-status.js +26 -0
  116. package/dist/harness-api/workspace.d.ts +6 -3
  117. package/dist/harness-api/workspace.js +27 -11
  118. package/dist/harness-api/ws-server.d.ts +11 -3
  119. package/dist/harness-api/ws-server.js +94 -15
  120. package/dist/host/execution.d.ts +8 -6
  121. package/dist/host/gates.d.ts +2 -1
  122. package/dist/host/gates.js +10 -2
  123. package/dist/host/main.js +64 -21
  124. package/dist/host/operate.js +67 -5
  125. package/dist/host/stack-config.d.ts +70 -0
  126. package/dist/host/stack-config.js +67 -1
  127. package/dist/infra/execution.d.ts +18 -1
  128. package/dist/infra/execution.js +28 -4
  129. package/dist/infra/index.d.ts +1 -1
  130. package/dist/keys/client.js +4 -1
  131. package/dist/keys/keys.d.ts +5 -5
  132. package/dist/keys/keys.js +5 -7
  133. package/dist/management-openapi.json +999 -2211
  134. package/dist/mcp/connect.d.ts +25 -20
  135. package/dist/mcp/connect.js +29 -30
  136. package/dist/mcp/pool.d.ts +2 -2
  137. package/dist/mcp/pool.js +1 -1
  138. package/dist/mcp/preview.d.ts +3 -3
  139. package/dist/mcp/preview.js +10 -20
  140. package/dist/mcp/snapshot.d.ts +5 -11
  141. package/dist/mcp/snapshot.js +6 -7
  142. package/dist/model/pi-model.js +8 -0
  143. package/dist/openapi.json +6955 -2526
  144. package/dist/reads/types.d.ts +16 -3
  145. package/dist/record/append.d.ts +3 -1
  146. package/dist/record/append.js +1 -0
  147. package/dist/record/envelope.d.ts +5 -1
  148. package/dist/record/envelope.js +7 -3
  149. package/dist/record/environment.d.ts +25 -0
  150. package/dist/record/environment.js +44 -0
  151. package/dist/record/index.d.ts +1 -1
  152. package/dist/record/index.js +1 -1
  153. package/dist/retry.d.ts +21 -0
  154. package/dist/retry.js +34 -0
  155. package/dist/sandbox/disk-fs.d.ts +95 -0
  156. package/dist/sandbox/disk-fs.js +206 -0
  157. package/dist/sandbox/join.d.ts +17 -18
  158. package/dist/sandbox/join.js +46 -20
  159. package/dist/sandbox/manager.d.ts +16 -8
  160. package/dist/sandbox/manager.js +33 -17
  161. package/dist/sandbox/placement.d.ts +13 -12
  162. package/dist/sandbox/placement.js +29 -12
  163. package/dist/sandbox/pods/client.d.ts +6 -1
  164. package/dist/sandbox/pods/lifecycle.d.ts +28 -27
  165. package/dist/sandbox/pods/lifecycle.js +14 -26
  166. package/dist/sandbox/pods/reconcile.js +21 -15
  167. package/dist/sandbox/pods/spec.d.ts +15 -13
  168. package/dist/sandbox/pods/spec.js +16 -20
  169. package/dist/sandbox/records.d.ts +2 -2
  170. package/dist/sandbox/records.js +3 -3
  171. package/dist/sandbox/resolve.d.ts +35 -27
  172. package/dist/sandbox/resolve.js +66 -41
  173. package/dist/sandbox/session-sandbox.js +1 -1
  174. package/dist/sandbox/setup.d.ts +61 -0
  175. package/dist/sandbox/setup.js +123 -0
  176. package/dist/sandbox/share.d.ts +11 -21
  177. package/dist/sandbox/share.js +12 -41
  178. package/dist/sandbox/tenant-config.d.ts +12 -14
  179. package/dist/sandbox/tenant-config.js +12 -17
  180. package/dist/sandbox/types.d.ts +6 -0
  181. package/dist/store/postgres/control.js +8 -1
  182. package/dist/store/postgres/drizzle/0018_wake_outbox_retries.sql +3 -0
  183. package/dist/store/postgres/drizzle/0019_sandbox_signal_outbox.sql +18 -0
  184. package/dist/store/postgres/drizzle/0020_environments.sql +63 -0
  185. package/dist/store/postgres/drizzle/0021_disk_versions.sql +7 -0
  186. package/dist/store/postgres/drizzle/0022_model_usage_timing.sql +5 -0
  187. package/dist/store/postgres/drizzle/0023_artifact_pages.sql +1 -0
  188. package/dist/store/postgres/drizzle/meta/_journal.json +42 -0
  189. package/dist/store/postgres/reads.js +117 -17
  190. package/dist/store/postgres/record-writer.d.ts +4 -4
  191. package/dist/store/postgres/record-writer.js +9 -9
  192. package/dist/store/postgres/record.d.ts +7 -0
  193. package/dist/store/postgres/record.js +23 -2
  194. package/dist/store/postgres/schema.d.ts +1213 -779
  195. package/dist/store/postgres/schema.js +136 -76
  196. package/dist/store/postgres/store.js +283 -117
  197. package/dist/store/types.d.ts +206 -88
  198. package/dist/streams/relay/core.js +1 -13
  199. package/dist/tenant/advance.d.ts +7 -6
  200. package/dist/tenant/advance.js +121 -49
  201. package/dist/tenant/artifact-tool.js +3 -4
  202. package/dist/tenant/commands.d.ts +15 -2
  203. package/dist/tenant/commands.js +94 -14
  204. package/dist/tenant/context.d.ts +67 -33
  205. package/dist/tenant/context.js +4 -15
  206. package/dist/tenant/control.d.ts +8 -1
  207. package/dist/tenant/control.js +2 -0
  208. package/dist/tenant/effects.d.ts +1 -1
  209. package/dist/tenant/effects.js +37 -20
  210. package/dist/tenant/environments.d.ts +167 -0
  211. package/dist/tenant/environments.js +823 -0
  212. package/dist/tenant/harness-tokens.d.ts +21 -0
  213. package/dist/tenant/harness-tokens.js +61 -0
  214. package/dist/tenant/host-token.d.ts +1 -1
  215. package/dist/tenant/host-token.js +2 -4
  216. package/dist/tenant/identity-file.d.ts +4 -4
  217. package/dist/tenant/identity-file.js +17 -8
  218. package/dist/tenant/issuers.js +3 -3
  219. package/dist/tenant/principals.js +1 -1
  220. package/dist/tenant/reset.js +3 -3
  221. package/dist/tenant/runtime.d.ts +6 -1
  222. package/dist/tenant/runtime.js +30 -4
  223. package/dist/tenant/scheduler.d.ts +7 -0
  224. package/dist/tenant/scheduler.js +38 -7
  225. package/dist/tenant/session-streams.d.ts +0 -1
  226. package/dist/tenant/session-streams.js +1 -13
  227. package/dist/tenant/session.d.ts +3 -3
  228. package/dist/tenant/session.js +0 -14
  229. package/dist/tenant/sessions.d.ts +13 -13
  230. package/dist/tenant/sessions.js +61 -138
  231. package/dist/tenant/signing-keys.d.ts +5 -1
  232. package/dist/tenant/status.js +3 -3
  233. package/dist/tenant/streams.js +14 -29
  234. package/dist/tenant/sweep.d.ts +39 -12
  235. package/dist/tenant/sweep.js +168 -63
  236. package/dist/tenant/transcript.d.ts +210 -20
  237. package/dist/tenant/transcript.js +62 -11
  238. package/dist/tenant/turn-timing.d.ts +43 -0
  239. package/dist/tenant/turn-timing.js +39 -0
  240. package/dist/tenant/types.d.ts +2 -0
  241. package/dist/traces/ids.d.ts +42 -0
  242. package/dist/traces/ids.js +85 -0
  243. package/dist/traces/key.d.ts +6 -0
  244. package/dist/traces/key.js +37 -0
  245. package/dist/traces/map.d.ts +113 -0
  246. package/dist/traces/map.js +317 -0
  247. package/dist/traces/options.d.ts +7 -0
  248. package/dist/traces/options.js +1 -0
  249. package/dist/traces/otlp.d.ts +42 -0
  250. package/dist/traces/otlp.js +84 -0
  251. package/dist/traces/projector.d.ts +58 -0
  252. package/dist/traces/projector.js +200 -0
  253. package/dist/traces/read.d.ts +23 -0
  254. package/dist/traces/read.js +16 -0
  255. package/dist/vault/coverage.d.ts +26 -0
  256. package/dist/vault/coverage.js +64 -0
  257. package/dist/vault/error.d.ts +11 -1
  258. package/dist/vault/error.js +5 -1
  259. package/dist/vault/service.d.ts +47 -48
  260. package/dist/vault/service.js +321 -382
  261. package/dist/vault/sources.d.ts +5 -16
  262. package/dist/vault/sources.js +6 -8
  263. package/dist/version.d.ts +1 -1
  264. package/dist/version.js +2 -2
  265. package/package.json +14 -13
  266. package/dist/api/http/routes/sandboxes.d.ts +0 -9
  267. package/dist/api/http/routes/sandboxes.js +0 -153
  268. package/dist/api/http/routes/vaults.d.ts +0 -10
  269. package/dist/api/http/routes/vaults.js +0 -139
  270. package/dist/record/sandbox.d.ts +0 -25
  271. package/dist/record/sandbox.js +0 -43
  272. package/dist/tenant/sandboxes.d.ts +0 -122
  273. package/dist/tenant/sandboxes.js +0 -562
@@ -1,3 +1,5 @@
1
+ /** A setup failure's `reason` starts with this (`sandbox/setup.ts`); it is sticky. */
2
+ export const SETUP_FAILED = "setup:";
1
3
  /** A pod not ready this long after it was asked to run has failed. */
2
4
  export const READY_DEADLINE_MS = 5 * 60_000;
3
5
  /** How soon to look again while the pod changes. */
@@ -67,24 +69,6 @@ export function decide(facts) {
67
69
  again(POLL_MS);
68
70
  return done();
69
71
  }
70
- const expiresAt = pod.expiresAt === undefined ? undefined : Date.parse(pod.expiresAt);
71
- if (expiresAt !== undefined && now >= expiresAt) {
72
- if (pod.observed !== "expired") {
73
- patch.observed = "expired";
74
- patch.hostEpoch = pod.hostEpoch + 1;
75
- events.push(event("sandbox.expired", { onExpiry: facts.config.onExpiry }));
76
- }
77
- if (facts.config.onExpiry === "delete") {
78
- patch.desired = "deleted";
79
- patch.rev = pod.rev + 1;
80
- if (status.exists && !status.deleting)
81
- deletes.push(pod.k8sName);
82
- again(POLL_MS);
83
- }
84
- return done();
85
- }
86
- if (expiresAt !== undefined)
87
- arm.push({ timer: "ttl", at: expiresAt });
88
72
  let desired = pod.desired;
89
73
  let rev = pod.rev;
90
74
  if (facts.trigger === "idle" && desired === "running" && !facts.busy) {
@@ -96,7 +80,7 @@ export function decide(facts) {
96
80
  patch.desired = desired;
97
81
  patch.rev = rev;
98
82
  patch.hostEpoch = pod.hostEpoch + 1;
99
- events.push(event("sandbox.suspended", { reason: "idle" }));
83
+ events.push(event("environment.suspended", { reason: "idle" }));
100
84
  }
101
85
  else
102
86
  arm.push({ timer: "idle", at: due });
@@ -104,14 +88,14 @@ export function decide(facts) {
104
88
  // An incarnation a pod has joined, whose Sandbox or volume is gone: lost.
105
89
  if (pod.podUid !== undefined && (!status.exists || status.deleting || status.volume === "missing")) {
106
90
  const reason = !status.exists
107
- ? "The sandbox's pod and volume were deleted outside the Runtime"
91
+ ? "The Environment's pod and volume were deleted outside the Runtime"
108
92
  : status.deleting
109
- ? "The sandbox is being deleted outside the Runtime"
110
- : "The sandbox's volume is gone";
93
+ ? "The Environment's pod is being deleted outside the Runtime"
94
+ : "The Environment's volume is gone";
111
95
  patch.observed = "lost";
112
96
  patch.reason = reason;
113
97
  patch.hostEpoch = (patch.hostEpoch ?? pod.hostEpoch) + 1;
114
- events.push(event("sandbox.lost", { reason }));
98
+ events.push(event("environment.lost", { reason }));
115
99
  if (status.exists && !status.deleting)
116
100
  deletes.push(pod.k8sName);
117
101
  if (status.exists)
@@ -119,7 +103,7 @@ export function decide(facts) {
119
103
  return done();
120
104
  }
121
105
  if (status.deleting) {
122
- // The previous incarnation of this name is still going (a sandbox deleted and created again).
106
+ // The previous incarnation of this name is still going (an Environment deleted and created again).
123
107
  again(POLL_MS);
124
108
  return done();
125
109
  }
@@ -135,7 +119,7 @@ export function decide(facts) {
135
119
  patch.startedAt = new Date(now).toISOString();
136
120
  patch.reason = null;
137
121
  }
138
- // A pod started by this apply (a create, a resume) is the sandbox's first, not a relaunch.
122
+ // A pod started by this apply (a create, a resume) is the Environment's first, not a relaunch.
139
123
  if (desired === "running" && pod.podUid !== undefined)
140
124
  patch.podUid = null;
141
125
  again(POLL_MS);
@@ -144,6 +128,10 @@ export function decide(facts) {
144
128
  });
145
129
  }
146
130
  if (desired === "running") {
131
+ // A failed setup (R2d step 2) stays failed until a turn start asks the pod to run again,
132
+ // though the pod itself is ready: its engine takes no turn meanwhile.
133
+ if (pod.observed === "failed" && pod.reason?.startsWith(SETUP_FAILED))
134
+ return done();
147
135
  if (status.ready) {
148
136
  if (pod.observed !== "running") {
149
137
  patch.observed = "running";
@@ -159,7 +147,7 @@ export function decide(facts) {
159
147
  `The pod was not ready within ${READY_DEADLINE_MS / 60_000} minutes`;
160
148
  patch.observed = "failed";
161
149
  patch.reason = reason;
162
- events.push(event("sandbox.failed", { reason }));
150
+ events.push(event("environment.failed", { reason }));
163
151
  return done();
164
152
  }
165
153
  if (pod.observed !== "creating")
@@ -15,8 +15,10 @@
15
15
  * infrastructure errors throw; Restate retries those.
16
16
  */
17
17
  import { randomBytes } from "node:crypto";
18
- import { readSandboxConfig } from "../tenant-config.js";
18
+ import { effectiveSandboxConfig, readSandboxConfig } from "../tenant-config.js";
19
+ import { podSandboxSpecOf } from "../resolve.js";
19
20
  import { sha256Hex } from "../join.js";
21
+ import { recoverLostVolume } from "../../artifacts/disk.js";
20
22
  import { decide } from "./lifecycle.js";
21
23
  import { podLifecycleConfig, podSpecOf } from "./spec.js";
22
24
  export async function reconcileSandbox(ctx, sandboxId, trigger) {
@@ -24,11 +26,11 @@ export async function reconcileSandbox(ctx, sandboxId, trigger) {
24
26
  if (!pods || ctx.closed)
25
27
  return {};
26
28
  const read = await ctx.store.tx(async (t) => {
27
- const row = await t.sandboxResource(sandboxId);
29
+ const row = await t.environment(sandboxId);
28
30
  if (!row || row.kind !== "pod" || !row.pod)
29
31
  return undefined;
30
32
  const config = await readSandboxConfig(t);
31
- const busy = (await t.sessionsOnSandbox(sandboxId)).some((session) => session.activeTurnId != null);
33
+ const busy = (await t.sessionsOnEnvironment(sandboxId)).some((session) => session.activeTurnId != null);
32
34
  return { row: row, config, busy };
33
35
  });
34
36
  if (!read)
@@ -47,7 +49,7 @@ export async function reconcileSandbox(ctx, sandboxId, trigger) {
47
49
  return undefined;
48
50
  }
49
51
  };
50
- const lifecycle = podLifecycleConfig(config);
52
+ const lifecycle = podLifecycleConfig(config, row.recipe);
51
53
  const decision = decide({
52
54
  row,
53
55
  config: lifecycle,
@@ -69,10 +71,10 @@ export async function reconcileSandbox(ctx, sandboxId, trigger) {
69
71
  joinToken = randomBytes(32).toString("base64url");
70
72
  rev += 1;
71
73
  const recorded = await ctx.store.tx(async (t) => {
72
- const current = await t.sandboxResource(sandboxId, { lock: true });
74
+ const current = await t.environment(sandboxId, { lock: true });
73
75
  if (!unchanged(current?.pod))
74
76
  return false;
75
- await t.updateSandboxPod(sandboxId, { joinTokenHash: sha256Hex(joinToken), rev }, new Date().toISOString());
77
+ await t.updateEnvironmentPod(sandboxId, { joinTokenHash: sha256Hex(joinToken), rev }, new Date().toISOString());
76
78
  return true;
77
79
  });
78
80
  if (!recorded)
@@ -95,12 +97,13 @@ export async function reconcileSandbox(ctx, sandboxId, trigger) {
95
97
  }
96
98
  if (decision.apply) {
97
99
  const spec = podSpecOf({
98
- sandboxId,
99
- spec: row.spec,
100
+ environmentId: sandboxId,
101
+ spec: podSandboxSpecOf(row.applied, effectiveSandboxConfig(config).limits.idle),
100
102
  pod: { ...row.pod, rev },
101
103
  config,
102
104
  mode: decision.apply.mode,
103
105
  harnessImage: pods.harnessImage,
106
+ harness: row.applied.harness,
104
107
  ...(joinToken ? { joinToken } : {}),
105
108
  });
106
109
  try {
@@ -117,13 +120,13 @@ export async function reconcileSandbox(ctx, sandboxId, trigger) {
117
120
  }
118
121
  }
119
122
  const written = await ctx.store.tx(async (t) => {
120
- const current = await t.sandboxResource(sandboxId, { lock: true });
123
+ const current = await t.environment(sandboxId, { lock: true });
121
124
  if (!current?.pod)
122
125
  return { kind: "gone" };
123
126
  if (!unchanged(current.pod))
124
127
  return { kind: "changed" };
125
128
  if (decision.removeRow) {
126
- await t.deleteSandboxResource(sandboxId);
129
+ await t.deleteEnvironment(sandboxId);
127
130
  return { kind: "removed" };
128
131
  }
129
132
  const epochMoved = patch.hostEpoch !== undefined && patch.hostEpoch !== null;
@@ -132,10 +135,12 @@ export async function reconcileSandbox(ctx, sandboxId, trigger) {
132
135
  ...(epochMoved ? { hostEpoch: current.pod.hostEpoch + 1 } : {}),
133
136
  };
134
137
  if (Object.keys(finalPatch).length > 0)
135
- await t.updateSandboxPod(sandboxId, finalPatch, new Date().toISOString());
138
+ await t.updateEnvironmentPod(sandboxId, finalPatch, new Date().toISOString());
136
139
  for (const item of decision.events)
137
- await t.sandboxEvent(sandboxId, item.type, item.payload);
138
- return { kind: "written", epoch: epochMoved ? current.pod.hostEpoch + 1 : undefined };
140
+ await t.environmentEvent(sandboxId, item.type, item.payload);
141
+ if (epochMoved)
142
+ await t.signal({ type: "host.revoked", sandboxId, epoch: finalPatch.hostEpoch });
143
+ return { kind: "written" };
139
144
  });
140
145
  if (written.kind === "changed")
141
146
  return { retryAfterMs: 0 };
@@ -143,8 +148,9 @@ export async function reconcileSandbox(ctx, sandboxId, trigger) {
143
148
  await ctx.sandbox.removeSandbox(sandboxId).catch(() => undefined);
144
149
  return {};
145
150
  }
146
- if (written.kind === "written" && written.epoch !== undefined)
147
- ctx.harness.revokeHost(sandboxId, written.epoch);
151
+ // A lost volume with a disk version starts again from the latest one (R2d step 3).
152
+ if (patch.observed === "lost" && (await recoverLostVolume(ctx, sandboxId).catch(() => false)))
153
+ return { retryAfterMs: 0 };
148
154
  return {
149
155
  ...(decision.retryAfterMs === undefined ? {} : { retryAfterMs: decision.retryAfterMs }),
150
156
  ...(decision.arm.length > 0 ? { arm: decision.arm } : {}),
@@ -1,10 +1,11 @@
1
1
  /**
2
- * The `PUT /v1/pods/{name}` body of a pod sandbox: its resolved spec, the Tenant's lifecycle
2
+ * The `PUT /v1/pods/{name}` body of a pod Environment: its applied recipe, the Tenant's lifecycle
3
3
  * settings (D36, read at every apply) and the incarnation's fences. The pod shape itself is
4
4
  * rendered only by the sandboxes service.
5
5
  */
6
6
  import { type SandboxManifest } from "@nylorun/core/define";
7
- import type { TenantSandboxConfig } from "@nylorun/core/contracts";
7
+ import { type EnvironmentRecipe } from "@nylorun/core/contracts";
8
+ import type { TenantSandboxConfig } from "../tenant-config.js";
8
9
  import type { SandboxPodState } from "../../store/types.js";
9
10
  import type { PodSpec } from "./client.js";
10
11
  import { type PodLifecycleConfig } from "./lifecycle.js";
@@ -14,32 +15,33 @@ export declare const DEFAULT_STOP_GRACE_SECONDS = 10;
14
15
  export declare const DEFAULT_STORAGE_GIB = 5;
15
16
  /** Where the engine in the pod keeps its records, plugin data and home (on the volume). */
16
17
  export declare const POD_HARNESS_ROOT = "/harness";
17
- /** What a pod sandbox's spec holds beyond a virtual one's. */
18
+ /** What a pod's spec holds beyond a virtual sandbox's. */
18
19
  export type PodSandboxSpec = SandboxManifest & {
19
20
  readonly storage?: string;
20
- readonly lifecycle?: {
21
- readonly ttl?: string;
22
- };
23
21
  };
24
- /** The Tenant's lifecycle settings with their defaults. */
25
- export declare function podLifecycleConfig(config: TenantSandboxConfig): PodLifecycleConfig & {
22
+ /**
23
+ * The lifecycle settings with their defaults: the Tenant's (D36), and the Environment's
24
+ * `lifecycle.idle` within the Tenant's `limits.idle` when `recipe` is given.
25
+ */
26
+ export declare function podLifecycleConfig(config: TenantSandboxConfig, recipe?: Pick<EnvironmentRecipe, "lifecycle">): PodLifecycleConfig & {
26
27
  readonly stopGraceSeconds: number;
27
28
  };
28
29
  /** The volume's size in GiB. */
29
30
  export declare function storageGiBOf(spec: PodSandboxSpec): number;
30
- /** The Tenant's default image, when its default sandbox names one. */
31
- export declare function tenantDefaultImage(config: TenantSandboxConfig): string | undefined;
32
31
  export interface PodSpecInput {
33
- readonly sandboxId: string;
32
+ readonly environmentId: string;
34
33
  readonly spec: PodSandboxSpec;
35
34
  readonly pod: SandboxPodState;
36
35
  readonly config: TenantSandboxConfig;
37
36
  readonly mode: "Running" | "Suspended";
38
37
  /** The Runtime image the engine is copied from. */
39
38
  readonly harnessImage: string;
39
+ /**
40
+ * The harness the pod runs (R4.4): ours, copied in from `harnessImage`, or a custom one, which
41
+ * is the workload image itself and runs as its entrypoint.
42
+ */
43
+ readonly harness?: string;
40
44
  /** The join token, when this apply rotates it. */
41
45
  readonly joinToken?: string;
42
46
  }
43
47
  export declare function podSpecOf(input: PodSpecInput): PodSpec;
44
- /** When a sandbox with this TTL, created at `createdAt`, expires. */
45
- export declare function expiresAtOf(createdAt: string, ttl: string | undefined): string | undefined;
@@ -1,9 +1,10 @@
1
1
  /**
2
- * The `PUT /v1/pods/{name}` body of a pod sandbox: its resolved spec, the Tenant's lifecycle
2
+ * The `PUT /v1/pods/{name}` body of a pod Environment: its applied recipe, the Tenant's lifecycle
3
3
  * settings (D36, read at every apply) and the incarnation's fences. The pod shape itself is
4
4
  * rendered only by the sandboxes service.
5
5
  */
6
6
  import { parseSandboxDuration, parseSandboxSize } from "@nylorun/core/define";
7
+ import { DEFAULT_HARNESS } from "@nylorun/core/contracts";
7
8
  import { memoryMiBOf } from "../policy.js";
8
9
  import { opIdOf } from "./lifecycle.js";
9
10
  /** The pod's grace period to stop when the Tenant sets none (D34). */
@@ -12,12 +13,16 @@ export const DEFAULT_STOP_GRACE_SECONDS = 10;
12
13
  export const DEFAULT_STORAGE_GIB = 5;
13
14
  /** Where the engine in the pod keeps its records, plugin data and home (on the volume). */
14
15
  export const POD_HARNESS_ROOT = "/harness";
15
- /** The Tenant's lifecycle settings with their defaults. */
16
- export function podLifecycleConfig(config) {
16
+ /**
17
+ * The lifecycle settings with their defaults: the Tenant's (D36), and the Environment's
18
+ * `lifecycle.idle` within the Tenant's `limits.idle` when `recipe` is given.
19
+ */
20
+ export function podLifecycleConfig(config, recipe) {
17
21
  const stopGrace = config.lifecycle?.stopGrace;
22
+ const tenantIdle = parseSandboxDuration(config.limits?.idle ?? "15m") ?? 15 * 60_000;
23
+ const own = recipe?.lifecycle.idle === undefined ? undefined : parseSandboxDuration(recipe.lifecycle.idle);
18
24
  return {
19
- onExpiry: config.lifecycle?.onExpiry ?? "retain",
20
- idleMs: parseSandboxDuration(config.limits?.idle ?? "15m") ?? 15 * 60_000,
25
+ idleMs: own === undefined ? tenantIdle : Math.min(own, tenantIdle),
21
26
  stopGraceSeconds: stopGrace === undefined
22
27
  ? DEFAULT_STOP_GRACE_SECONDS
23
28
  : Math.max(1, Math.min(30, Math.round((parseSandboxDuration(stopGrace) ?? 10_000) / 1000))),
@@ -28,35 +33,26 @@ export function storageGiBOf(spec) {
28
33
  const bytes = spec.storage === undefined ? undefined : parseSandboxSize(spec.storage);
29
34
  return bytes === undefined ? DEFAULT_STORAGE_GIB : Math.max(1, Math.ceil(bytes / 1024 ** 3));
30
35
  }
31
- /** The Tenant's default image, when its default sandbox names one. */
32
- export function tenantDefaultImage(config) {
33
- const fallback = config.default;
34
- return typeof fallback === "object" ? fallback.image : undefined;
35
- }
36
36
  export function podSpecOf(input) {
37
37
  const { spec, pod, config } = input;
38
38
  const lifecycle = podLifecycleConfig(config);
39
- const image = spec.image ?? tenantDefaultImage(config);
39
+ const image = spec.image;
40
+ const ours = (input.harness ?? DEFAULT_HARNESS) === DEFAULT_HARNESS;
40
41
  return {
41
42
  opId: opIdOf(pod),
42
43
  mode: input.mode,
43
44
  ...(image ? { image } : {}),
44
- harnessImage: input.harnessImage,
45
+ // A custom harness is its image: the pod runs the image's entrypoint, nothing copied in.
46
+ ...(ours ? { harnessImage: input.harnessImage } : { entrypoint: "image" }),
45
47
  cpus: spec.resources?.cpus ?? 1,
46
48
  memoryMiB: memoryMiBOf(spec),
47
49
  storageGiB: storageGiBOf(spec),
48
50
  stopGraceSeconds: lifecycle.stopGraceSeconds,
49
- ...(pod.expiresAt ? { shutdownTime: new Date(pod.expiresAt).toISOString().replace(/\.\d{3}Z$/, "Z") } : {}),
50
- shutdownPolicy: lifecycle.onExpiry === "delete" ? "Delete" : "Retain",
51
+ shutdownPolicy: "Retain",
51
52
  env: {
52
- NYLORUN_SANDBOX_ID: input.sandboxId,
53
+ NYLORUN_SANDBOX_ID: input.environmentId,
53
54
  NYLORUN_HARNESS_ROOT: POD_HARNESS_ROOT,
54
55
  },
55
56
  ...(input.joinToken ? { joinToken: input.joinToken } : {}),
56
57
  };
57
58
  }
58
- /** When a sandbox with this TTL, created at `createdAt`, expires. */
59
- export function expiresAtOf(createdAt, ttl) {
60
- const ms = ttl === undefined ? undefined : parseSandboxDuration(ttl);
61
- return ms === undefined ? undefined : new Date(Date.parse(createdAt) + ms).toISOString();
62
- }
@@ -16,10 +16,10 @@ export declare function workspacePrefix(scope: string): string;
16
16
  /** The key of session `sessionId`'s own workspace. */
17
17
  export declare function sessionWorkspaceKey(scope: string, sessionId: string): string;
18
18
  /**
19
- * The key of sandbox resource `sandboxId`'s workspace: apart from every session key (16 hex
19
+ * The key of sandbox resource `environmentId`'s workspace: apart from every session key (16 hex
20
20
  * characters after the prefix). The virtual backend keeps it in `<sandboxes>/<key>/workspace`.
21
21
  */
22
- export declare function sandboxWorkspaceKey(scope: string, sandboxId: string): string;
22
+ export declare function sandboxWorkspaceKey(scope: string, environmentId: string): string;
23
23
  /** The records in the Tenant's `sandboxes` table. */
24
24
  export declare function storeSandboxRecords(store: SessionStore): SandboxRecords;
25
25
  /**
@@ -21,11 +21,11 @@ export function sessionWorkspaceKey(scope, sessionId) {
21
21
  return workspacePrefix(scope) + createHash("sha256").update(sessionId).digest("hex").slice(0, 16);
22
22
  }
23
23
  /**
24
- * The key of sandbox resource `sandboxId`'s workspace: apart from every session key (16 hex
24
+ * The key of sandbox resource `environmentId`'s workspace: apart from every session key (16 hex
25
25
  * characters after the prefix). The virtual backend keeps it in `<sandboxes>/<key>/workspace`.
26
26
  */
27
- export function sandboxWorkspaceKey(scope, sandboxId) {
28
- return `${workspacePrefix(scope)}sbx-${createHash("sha256").update(sandboxId).digest("hex").slice(0, 16)}`;
27
+ export function sandboxWorkspaceKey(scope, environmentId) {
28
+ return `${workspacePrefix(scope)}sbx-${createHash("sha256").update(environmentId).digest("hex").slice(0, 16)}`;
29
29
  }
30
30
  /** The records in the Tenant's `sandboxes` table. */
31
31
  export function storeSandboxRecords(store) {
@@ -1,37 +1,45 @@
1
1
  /**
2
- * Resolves the sandbox a session asks for when it is opened against its Tenant's configuration.
3
- * Pure: the caller supplies the configuration and who is asking. Every problem is
4
- * reported, not only the first, so one response lists all of them.
2
+ * Resolves an Environment's recipe (`PUT /v1/environments/{id}`) against its Tenant's limits,
3
+ * and the sandbox spec a recipe gives the session's sandbox capability and the pod. Pure: the
4
+ * caller supplies the configuration. Every problem is reported, not only the first, so one
5
+ * response lists all of them.
5
6
  */
6
- import type { SandboxInlineRequest } from "@nylorun/core/contracts";
7
- import type { SandboxManifest } from "@nylorun/core/define";
7
+ import type { EnvironmentFile, EnvironmentKind, EnvironmentLifecycle, EnvironmentRecipe, EnvironmentSetupCommand } from "@nylorun/core/contracts";
8
+ import { type SandboxManifest } from "@nylorun/core/define";
9
+ import type { PodSandboxSpec } from "./pods/spec.js";
8
10
  import { type EffectiveSandboxConfig } from "./tenant-config.js";
9
- export type SandboxSource = "default" | "inline";
10
- export type SandboxResolution = {
11
- readonly kind: "none";
12
- } | {
13
- readonly kind: "sandbox";
14
- readonly spec: SandboxManifest;
15
- readonly source: SandboxSource;
11
+ /** What a `PUT` asks of the recipe: the request's fields, over the existing recipe's. */
12
+ export interface RecipeRequest {
13
+ readonly image?: string;
14
+ readonly resources?: {
15
+ readonly cpus?: number;
16
+ readonly memory?: string;
17
+ };
18
+ readonly storage?: string;
19
+ readonly network?: {
20
+ readonly allowedHosts?: readonly string[];
21
+ };
22
+ readonly harness?: string;
23
+ readonly lifecycle?: EnvironmentLifecycle;
24
+ readonly setup?: readonly EnvironmentSetupCommand[];
25
+ readonly files?: readonly EnvironmentFile[];
26
+ }
27
+ export type RecipeResolution = {
28
+ readonly kind: "recipe";
29
+ readonly recipe: EnvironmentRecipe;
16
30
  } | {
17
31
  readonly kind: "error";
18
- readonly status: 400 | 403;
19
32
  readonly errors: readonly string[];
20
33
  };
21
- export interface ResolveSandboxInput {
22
- /** `PutSessionRequest.sandbox` other than `{ session }`, which the caller handles. */
23
- readonly request: false | SandboxInlineRequest | undefined;
24
- readonly config: EffectiveSandboxConfig;
25
- /** The request acts for a subject (`Nylorun-Subject`) rather than as the application. */
26
- readonly actingForSubject: boolean;
27
- /**
28
- * The sandbox's kind. Default `virtual`, which runs no images and allows exact host names
29
- * only; a `pod` (F7.2) takes an image and `*.suffix` hosts.
30
- */
31
- readonly kind?: "virtual" | "pod";
32
- }
33
- export declare const SUBJECT_INLINE_MESSAGE = "Defining a sandbox needs an application key. Use the Tenant default, or open the session from the application backend.";
34
- export declare function resolveSandbox(input: ResolveSandboxInput): SandboxResolution;
34
+ /** The recipe `request` asks for an Environment of `kind`, or every reason it cannot have it. */
35
+ export declare function resolveRecipe(kind: EnvironmentKind, request: RecipeRequest, config: EffectiveSandboxConfig): RecipeResolution;
36
+ /**
37
+ * The sandbox spec of a recipe: what the session's sandbox capability pins and the virtual
38
+ * workspace runs. Idle is the Environment's `lifecycle.idle`, else the Tenant's.
39
+ */
40
+ export declare function sandboxSpecOf(recipe: EnvironmentRecipe, tenantIdle: string): SandboxManifest;
41
+ /** The pod's spec of a recipe: its sandbox spec and its volume's size. */
42
+ export declare function podSandboxSpecOf(recipe: EnvironmentRecipe, tenantIdle: string): PodSandboxSpec;
35
43
  /** Whether `host` (a name or `*.suffix` pattern) is inside one of the ceiling's patterns. */
36
44
  export declare function withinCeiling(host: string, ceiling: readonly string[]): boolean;
37
45
  /** Problems with a Tenant configuration, checked when an operator saves it. */
@@ -1,50 +1,84 @@
1
+ import { DEFAULT_HARNESS } from "@nylorun/core/contracts";
2
+ import { parseSandboxDuration } from "@nylorun/core/define";
1
3
  import { memoryMiB } from "./tenant-config.js";
2
- export const SUBJECT_INLINE_MESSAGE = "Defining a sandbox needs an application key. Use the Tenant default, or open the session from the application backend.";
3
- export function resolveSandbox(input) {
4
- const { request, config } = input;
5
- if (request === false)
6
- return { kind: "none" };
7
- if (request === undefined) {
8
- const fallback = config.default;
9
- if (fallback === "none")
10
- return { kind: "none" };
11
- return build(fallback === "virtual" ? {} : fallback, "default", input);
12
- }
13
- if (input.actingForSubject)
14
- return { kind: "error", status: 403, errors: [SUBJECT_INLINE_MESSAGE] };
15
- return build(request, "inline", input);
16
- }
17
- function build(request, source, input) {
18
- const { limits } = input.config;
4
+ /** The recipe `request` asks for an Environment of `kind`, or every reason it cannot have it. */
5
+ export function resolveRecipe(kind, request, config) {
6
+ const { limits } = config;
19
7
  const errors = [];
20
- const pod = input.kind === "pod";
8
+ const pod = kind === "pod";
9
+ // Any harness id (R4.1): placement decides where it may run, when a session opens.
10
+ const harness = request.harness ?? DEFAULT_HARNESS;
11
+ if (pod && harness !== DEFAULT_HARNESS && request.image === undefined)
12
+ errors.push(`harness ${harness} needs image: a pod Environment runs its image as the harness.`);
13
+ const lifecycle = request.lifecycle ?? {};
14
+ const idle = lifecycle.idle === undefined ? undefined : parseSandboxDuration(lifecycle.idle);
15
+ if (idle !== undefined && idle > (parseSandboxDuration(limits.idle) ?? Infinity))
16
+ errors.push(`lifecycle.idle is ${lifecycle.idle}; this Tenant allows at most ${limits.idle}.`);
17
+ if (kind === "none") {
18
+ for (const field of ["image", "resources", "storage", "network", "setup", "files"])
19
+ if (request[field] !== undefined)
20
+ errors.push(`${field} is for an Environment with a sandbox: kind none has none.`);
21
+ if (errors.length > 0)
22
+ return { kind: "error", errors };
23
+ return { kind: "recipe", recipe: { harness, lifecycle: { ...lifecycle } } };
24
+ }
21
25
  if (request.image !== undefined && !pod)
22
- errors.push("sandbox.image is not supported: the virtual sandbox has no images.");
23
- const allow = (request.network?.allow ?? []).map((host) => host.toLowerCase());
26
+ errors.push("image is for kind pod: a virtual Environment runs no images.");
27
+ if (request.storage !== undefined && !pod)
28
+ errors.push("storage is for kind pod: a virtual Environment has no volume.");
29
+ for (const field of ["setup", "files"])
30
+ if (request[field] !== undefined && request[field].length > 0 && !pod)
31
+ errors.push(`${field} is for kind pod: it runs when a pod's disk is made or reset.`);
32
+ const allow = (request.network?.allowedHosts ?? []).map((host) => host.toLowerCase());
24
33
  for (const host of allow) {
25
34
  if (!withinCeiling(host, limits.network))
26
- errors.push(`sandbox.network.allow includes ${host}, which this Tenant does not allow.`);
35
+ errors.push(`network.allowedHosts includes ${host}, which this Tenant does not allow.`);
27
36
  else if (host.startsWith("*.") && !pod)
28
- errors.push(`sandbox.network.allow includes ${host}; the virtual sandbox allows exact host names only, such as ${host.slice(2)}.`);
37
+ errors.push(`network.allowedHosts includes ${host}; a virtual Environment allows exact host names only, such as ${host.slice(2)}.`);
29
38
  }
30
39
  const cpus = request.resources?.cpus ?? limits.defaultResources.cpus;
31
40
  const memory = request.resources?.memory === undefined
32
41
  ? limits.defaultResources.memoryMiB
33
42
  : memoryMiB(request.resources.memory);
34
43
  if (cpus > limits.resources.cpus)
35
- errors.push(`sandbox.resources.cpus asks for ${cpus}; this Tenant allows at most ${limits.resources.cpus}.`);
44
+ errors.push(`resources.cpus asks for ${cpus}; this Tenant allows at most ${limits.resources.cpus}.`);
36
45
  if (memory > limits.resources.memoryMiB)
37
- errors.push(`sandbox.resources.memory asks for ${memory}MiB; this Tenant allows at most ${limits.resources.memoryMiB}MiB.`);
46
+ errors.push(`resources.memory asks for ${memory}MiB; this Tenant allows at most ${limits.resources.memoryMiB}MiB.`);
38
47
  if (errors.length > 0)
39
- return { kind: "error", status: 400, errors };
40
- const unique = [...new Set(allow)].sort();
41
- const spec = {
42
- ...(request.image === undefined ? {} : { image: request.image }),
43
- network: { preset: "none", ...(unique.length > 0 ? { allow: unique } : {}) },
44
- resources: { cpus, memory: `${memory}MiB` },
45
- idle: limits.idle,
48
+ return { kind: "error", errors };
49
+ return {
50
+ kind: "recipe",
51
+ recipe: {
52
+ ...(request.image === undefined ? {} : { image: request.image }),
53
+ resources: { cpus, memory: `${memory}MiB` },
54
+ ...(request.storage === undefined ? {} : { storage: request.storage }),
55
+ network: { allowedHosts: [...new Set(allow)].sort() },
56
+ harness,
57
+ lifecycle: { ...lifecycle },
58
+ ...(pod && request.setup?.length ? { setup: request.setup.map((command) => ({ ...command })) } : {}),
59
+ ...(pod && request.files?.length ? { files: request.files.map((file) => ({ ...file })) } : {}),
60
+ },
61
+ };
62
+ }
63
+ /**
64
+ * The sandbox spec of a recipe: what the session's sandbox capability pins and the virtual
65
+ * workspace runs. Idle is the Environment's `lifecycle.idle`, else the Tenant's.
66
+ */
67
+ export function sandboxSpecOf(recipe, tenantIdle) {
68
+ const allow = recipe.network?.allowedHosts ?? [];
69
+ return {
70
+ ...(recipe.image === undefined ? {} : { image: recipe.image }),
71
+ network: { preset: "none", ...(allow.length > 0 ? { allow: [...allow] } : {}) },
72
+ ...(recipe.resources === undefined ? {} : { resources: { ...recipe.resources } }),
73
+ idle: recipe.lifecycle.idle ?? tenantIdle,
74
+ };
75
+ }
76
+ /** The pod's spec of a recipe: its sandbox spec and its volume's size. */
77
+ export function podSandboxSpecOf(recipe, tenantIdle) {
78
+ return {
79
+ ...sandboxSpecOf(recipe, tenantIdle),
80
+ ...(recipe.storage === undefined ? {} : { storage: recipe.storage }),
46
81
  };
47
- return { kind: "sandbox", spec, source };
48
82
  }
49
83
  /** Whether `host` (a name or `*.suffix` pattern) is inside one of the ceiling's patterns. */
50
84
  export function withinCeiling(host, ceiling) {
@@ -69,14 +103,5 @@ export function sandboxConfigErrors(config) {
69
103
  errors.push("limits.defaultResources.cpus is above limits.resources.cpus.");
70
104
  if (limits.defaultResources.memoryMiB > limits.resources.memoryMiB)
71
105
  errors.push("limits.defaultResources.memory is above limits.resources.memory.");
72
- if (config.default !== "none" && config.default !== "virtual") {
73
- const resolved = resolveSandbox({
74
- request: undefined,
75
- config,
76
- actingForSubject: false,
77
- });
78
- if (resolved.kind === "error")
79
- errors.push(...resolved.errors.map((message) => `default: ${message}`));
80
- }
81
106
  return errors;
82
107
  }
@@ -102,7 +102,7 @@ function checked(original, sandboxed) {
102
102
  return {
103
103
  ok: false,
104
104
  message: clash
105
- ? `'${original.id}' declares a tool named like a sandbox tool (bash, read, write, edit, grep, glob, save_artifact). Rename it, or open the session with sandbox: false.`
105
+ ? `'${original.id}' declares a tool named like a sandbox tool (bash, read, write, edit, grep, glob, save_artifact). Rename it, or open the session on no Environment or one of kind none.`
106
106
  : `'${original.id}' cannot take this sandbox: ${[...new Set(issues)].join("; ")}`,
107
107
  };
108
108
  }
@@ -0,0 +1,61 @@
1
+ import type { Environment, Tx } from "../store/types.js";
2
+ import type { TenantContext } from "../tenant/context.js";
3
+ import { SETUP_FAILED } from "./pods/lifecycle.js";
4
+ export { SETUP_FAILED };
5
+ /** What the engine runs on a new disk. */
6
+ export interface SetupPlan {
7
+ /** The applied recipe's version: written into the volume's marker. */
8
+ readonly recipeVersion: number;
9
+ readonly commands: readonly {
10
+ readonly command: string;
11
+ readonly cwd: string;
12
+ readonly timeoutMs: number;
13
+ }[];
14
+ /** Each input file by its index into the recipe's `files`, and where it goes. */
15
+ readonly files: readonly {
16
+ readonly index: number;
17
+ readonly path: string;
18
+ }[];
19
+ /** Variables every command gets: secrets as the sentinel, plain values (R2c). */
20
+ readonly environment: Readonly<Record<string, string>>;
21
+ }
22
+ /** The setup of `recipe`, or undefined when it has neither commands nor files. */
23
+ export declare function setupPlanOf(ctx: TenantContext, t: Tx, environment: Environment): Promise<SetupPlan | undefined>;
24
+ /** What the engine reports after running a setup. */
25
+ export interface SetupReport {
26
+ readonly status: "ok" | "failed";
27
+ /** The failed command's index into the plan's commands; absent for an input file. */
28
+ readonly index?: number;
29
+ readonly exitCode?: number;
30
+ /** The end of the failed command's output, or why an input file could not be copied. */
31
+ readonly output?: string;
32
+ }
33
+ export declare function parseSetupReport(value: unknown): SetupReport | undefined;
34
+ /**
35
+ * Records a failed setup on Environment `id` held at host epoch `epoch` (the reporting engine's):
36
+ * the pod is `failed` with a reason that starts `setup:`, and `environment.failed` says which
37
+ * command failed and how. A report from an older host, or a success, changes nothing.
38
+ */
39
+ export declare function recordSetup(ctx: TenantContext, id: string, epoch: number, report: SetupReport): Promise<void>;
40
+ /** Whether the engine of `id` should run its setup again: the pod is no longer `failed`. */
41
+ export declare function setupState(ctx: TenantContext, id: string): Promise<{
42
+ retry: boolean;
43
+ setup?: SetupPlan;
44
+ }>;
45
+ /** One input file's bytes, or a folder's listing (`path` absent). */
46
+ export type SetupFile = {
47
+ readonly kind: "file";
48
+ readonly contentType: string;
49
+ readonly body: ReadableStream<Uint8Array>;
50
+ } | {
51
+ readonly kind: "folder";
52
+ readonly entries: readonly {
53
+ readonly path: string;
54
+ readonly size: number;
55
+ }[];
56
+ };
57
+ /**
58
+ * Input file `index` of Environment `id`'s applied recipe: a file artifact's bytes, a folder's
59
+ * listing, or (with `path`) one of the folder's files. Undefined when there is no such file.
60
+ */
61
+ export declare function setupFile(ctx: TenantContext, id: string, index: number, path: string | undefined): Promise<SetupFile | undefined>;