@nylorun/runtime 0.15.2-beta → 0.17.0-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (291) hide show
  1. package/CHANGELOG.md +134 -0
  2. package/README.md +68 -29
  3. package/dist/adapters/execution/restate.d.ts +9 -1
  4. package/dist/adapters/execution/restate.js +64 -5
  5. package/dist/adapters/sandbox/local.d.ts +10 -0
  6. package/dist/adapters/sandbox/local.js +195 -0
  7. package/dist/adapters/sandbox/virtual.js +36 -1
  8. package/dist/admin-openapi.json +341 -7
  9. package/dist/api/a2a/tasks.js +6 -1
  10. package/dist/api/ag-ui/endpoint.js +1 -2
  11. package/dist/api/ag-ui/history.js +6 -1
  12. package/dist/api/ag-ui/routes.d.ts +1 -1
  13. package/dist/api/ag-ui/routes.js +15 -13
  14. package/dist/api/components.d.ts +2667 -711
  15. package/dist/api/components.js +24 -11
  16. package/dist/api/gate/keys.d.ts +4 -3
  17. package/dist/api/gate/routes.d.ts +19 -3
  18. package/dist/api/gate/routes.js +162 -53
  19. package/dist/api/http/app.js +6 -0
  20. package/dist/api/http/define.d.ts +19 -12
  21. package/dist/api/http/define.js +13 -27
  22. package/dist/api/http/routes/access.d.ts +3 -19
  23. package/dist/api/http/routes/access.js +12 -172
  24. package/dist/api/http/routes/actions.js +0 -1
  25. package/dist/api/http/routes/artifacts.d.ts +11 -0
  26. package/dist/api/http/routes/artifacts.js +516 -0
  27. package/dist/api/http/routes/me.d.ts +13 -0
  28. package/dist/api/http/routes/me.js +45 -0
  29. package/dist/api/http/routes/sandboxes.d.ts +9 -0
  30. package/dist/api/http/routes/sandboxes.js +150 -0
  31. package/dist/api/http/routes/sessions.js +9 -13
  32. package/dist/api/http/routes/tenant.js +36 -2
  33. package/dist/api/http/routes/vaults.d.ts +12 -3
  34. package/dist/api/http/routes/vaults.js +122 -54
  35. package/dist/api/openapi.js +4 -10
  36. package/dist/artifacts/config.d.ts +23 -0
  37. package/dist/artifacts/config.js +27 -0
  38. package/dist/artifacts/export.d.ts +8 -0
  39. package/dist/artifacts/export.js +205 -0
  40. package/dist/artifacts/files.d.ts +36 -0
  41. package/dist/artifacts/files.js +45 -0
  42. package/dist/artifacts/folders.d.ts +60 -0
  43. package/dist/artifacts/folders.js +209 -0
  44. package/dist/artifacts/links.d.ts +28 -0
  45. package/dist/artifacts/links.js +92 -0
  46. package/dist/artifacts/media-types.d.ts +13 -0
  47. package/dist/artifacts/media-types.js +80 -0
  48. package/dist/artifacts/parts.d.ts +34 -0
  49. package/dist/artifacts/parts.js +38 -0
  50. package/dist/artifacts/service.d.ts +74 -0
  51. package/dist/artifacts/service.js +301 -0
  52. package/dist/artifacts/workspace.d.ts +60 -0
  53. package/dist/artifacts/workspace.js +57 -0
  54. package/dist/artifacts/zip.d.ts +10 -0
  55. package/dist/artifacts/zip.js +126 -0
  56. package/dist/blob/fs.d.ts +13 -0
  57. package/dist/blob/fs.js +194 -0
  58. package/dist/blob/index.d.ts +9 -0
  59. package/dist/blob/index.js +9 -0
  60. package/dist/blob/s3.d.ts +42 -0
  61. package/dist/blob/s3.js +366 -0
  62. package/dist/blob/types.d.ts +108 -0
  63. package/dist/blob/types.js +85 -0
  64. package/dist/core/sandbox-routes.d.ts +2 -2
  65. package/dist/core/sandbox-routes.js +4 -3
  66. package/dist/execution/memory.d.ts +11 -1
  67. package/dist/execution/memory.js +77 -3
  68. package/dist/execution/types.d.ts +54 -1
  69. package/dist/execution/types.js +31 -0
  70. package/dist/gates/contract.d.ts +14 -13
  71. package/dist/gates/contract.js +13 -12
  72. package/dist/gates/egress.d.ts +72 -0
  73. package/dist/gates/egress.js +319 -0
  74. package/dist/gates/handler.d.ts +3 -0
  75. package/dist/gates/handler.js +3 -2
  76. package/dist/gates/http-client.d.ts +3 -2
  77. package/dist/gates/http-client.js +17 -6
  78. package/dist/gates/in-process.d.ts +13 -1
  79. package/dist/gates/in-process.js +11 -0
  80. package/dist/gates/inflight.d.ts +23 -4
  81. package/dist/gates/inflight.js +18 -4
  82. package/dist/gates/mcp-handler.js +5 -1
  83. package/dist/gates/model-gate.d.ts +6 -2
  84. package/dist/gates/tenant-vaults.d.ts +18 -9
  85. package/dist/gates/tenant-vaults.js +17 -12
  86. package/dist/gates/tool-calls.d.ts +9 -5
  87. package/dist/gates/tool-calls.js +6 -6
  88. package/dist/gates/tool-client.d.ts +9 -2
  89. package/dist/gates/tool-client.js +56 -22
  90. package/dist/gates/tool-contract.d.ts +12 -7
  91. package/dist/gates/tool-contract.js +10 -4
  92. package/dist/gates/tool-gate.d.ts +1 -0
  93. package/dist/harness/calls.d.ts +52 -0
  94. package/dist/harness/calls.js +113 -0
  95. package/dist/harness/executors.d.ts +22 -0
  96. package/dist/harness/executors.js +96 -0
  97. package/dist/harness/main.d.ts +6 -0
  98. package/dist/harness/main.js +103 -0
  99. package/dist/harness/pod.d.ts +21 -0
  100. package/dist/harness/pod.js +119 -0
  101. package/dist/harness/run-tokens.d.ts +18 -0
  102. package/dist/harness/run-tokens.js +15 -0
  103. package/dist/harness/service.d.ts +46 -0
  104. package/dist/harness/service.js +179 -0
  105. package/dist/harness/ws-client.d.ts +43 -0
  106. package/dist/harness/ws-client.js +122 -0
  107. package/dist/harness/ws-port.d.ts +22 -0
  108. package/dist/harness/ws-port.js +78 -0
  109. package/dist/harness-api/in-process.d.ts +24 -0
  110. package/dist/harness-api/in-process.js +119 -0
  111. package/dist/harness-api/record.d.ts +44 -0
  112. package/dist/harness-api/record.js +250 -0
  113. package/dist/harness-api/renew.d.ts +37 -0
  114. package/dist/harness-api/renew.js +77 -0
  115. package/dist/harness-api/server.d.ts +104 -0
  116. package/dist/harness-api/server.js +408 -0
  117. package/dist/harness-api/start.d.ts +36 -0
  118. package/dist/harness-api/start.js +118 -0
  119. package/dist/harness-api/workspace.d.ts +53 -0
  120. package/dist/harness-api/workspace.js +201 -0
  121. package/dist/harness-api/ws-server.d.ts +34 -0
  122. package/dist/harness-api/ws-server.js +227 -0
  123. package/dist/host/admin-api.d.ts +12 -5
  124. package/dist/host/admin-api.js +72 -6
  125. package/dist/host/app.d.ts +0 -1
  126. package/dist/host/app.js +35 -24
  127. package/dist/host/config-for.d.ts +4 -0
  128. package/dist/host/config-for.js +2 -0
  129. package/dist/host/config.d.ts +0 -6
  130. package/dist/host/create-host.d.ts +3 -10
  131. package/dist/host/create-host.js +4 -1
  132. package/dist/host/gates.d.ts +27 -2
  133. package/dist/host/gates.js +52 -2
  134. package/dist/host/http.d.ts +4 -1
  135. package/dist/host/http.js +6 -1
  136. package/dist/host/main.js +167 -37
  137. package/dist/host/stack-config.d.ts +112 -9
  138. package/dist/host/stack-config.js +222 -20
  139. package/dist/index.d.ts +3 -0
  140. package/dist/index.js +2 -0
  141. package/dist/keys/client.js +9 -1
  142. package/dist/keys/keys.d.ts +12 -2
  143. package/dist/keys/keys.js +8 -0
  144. package/dist/mcp/connect.js +2 -1
  145. package/dist/mcp/pool.d.ts +1 -0
  146. package/dist/mcp/pool.js +1 -0
  147. package/dist/mcp/snapshot.d.ts +8 -1
  148. package/dist/mcp/snapshot.js +28 -0
  149. package/dist/media.d.ts +5 -24
  150. package/dist/media.js +5 -0
  151. package/dist/model/pi-model.d.ts +6 -2
  152. package/dist/model/pi-model.js +39 -22
  153. package/dist/node/index.d.ts +1 -1
  154. package/dist/node/index.js +1 -1
  155. package/dist/openapi.json +14084 -9426
  156. package/dist/record/index.d.ts +1 -0
  157. package/dist/record/index.js +1 -0
  158. package/dist/record/sandbox.d.ts +25 -0
  159. package/dist/record/sandbox.js +43 -0
  160. package/dist/sandbox/capability.d.ts +3 -0
  161. package/dist/sandbox/capability.js +10 -0
  162. package/dist/sandbox/egress-token.d.ts +26 -0
  163. package/dist/sandbox/egress-token.js +8 -0
  164. package/dist/sandbox/join.d.ts +37 -0
  165. package/dist/sandbox/join.js +165 -0
  166. package/dist/sandbox/manager.d.ts +99 -34
  167. package/dist/sandbox/manager.js +216 -84
  168. package/dist/sandbox/placement.d.ts +27 -0
  169. package/dist/sandbox/placement.js +44 -0
  170. package/dist/sandbox/pods/client.d.ts +90 -0
  171. package/dist/sandbox/pods/client.js +79 -0
  172. package/dist/sandbox/pods/lifecycle.d.ts +91 -0
  173. package/dist/sandbox/pods/lifecycle.js +178 -0
  174. package/dist/sandbox/pods/name.d.ts +1 -0
  175. package/dist/sandbox/pods/name.js +28 -0
  176. package/dist/sandbox/pods/network-gate.d.ts +25 -0
  177. package/dist/sandbox/pods/network-gate.js +71 -0
  178. package/dist/sandbox/pods/reconcile.d.ts +11 -0
  179. package/dist/sandbox/pods/reconcile.js +152 -0
  180. package/dist/sandbox/pods/spec.d.ts +45 -0
  181. package/dist/sandbox/pods/spec.js +62 -0
  182. package/dist/sandbox/records.d.ts +29 -0
  183. package/dist/sandbox/records.js +79 -0
  184. package/dist/sandbox/resolve.d.ts +5 -0
  185. package/dist/sandbox/resolve.js +3 -2
  186. package/dist/sandbox/session-sandbox.js +7 -5
  187. package/dist/sandbox/share.d.ts +11 -0
  188. package/dist/sandbox/share.js +10 -0
  189. package/dist/sandbox/tenant-config.d.ts +16 -1
  190. package/dist/sandbox/tenant-config.js +9 -0
  191. package/dist/sandbox/types.d.ts +29 -1
  192. package/dist/sandbox/types.js +13 -1
  193. package/dist/store/postgres/drizzle/0004_sandbox_resources.sql +20 -0
  194. package/dist/store/postgres/drizzle/0005_artifacts.sql +27 -0
  195. package/dist/store/postgres/drizzle/0006_folder_artifacts.sql +12 -0
  196. package/dist/store/postgres/drizzle/0007_sandbox_pods.sql +14 -0
  197. package/dist/store/postgres/drizzle/0008_installation_vaults.sql +2 -0
  198. package/dist/store/postgres/drizzle/0009_oauth_pending.sql +19 -0
  199. package/dist/store/postgres/drizzle/0010_oss_auth_removals.sql +3 -0
  200. package/dist/store/postgres/drizzle/meta/_journal.json +49 -0
  201. package/dist/store/postgres/record-writer.d.ts +6 -1
  202. package/dist/store/postgres/record-writer.js +20 -2
  203. package/dist/store/postgres/schema.d.ts +1168 -250
  204. package/dist/store/postgres/schema.js +174 -28
  205. package/dist/store/postgres/store.js +339 -103
  206. package/dist/store/postgres/tenant.d.ts +1 -1
  207. package/dist/store/postgres/tenant.js +2 -2
  208. package/dist/store/types.d.ts +163 -32
  209. package/dist/streams/control.d.ts +2 -5
  210. package/dist/streams/control.js +5 -8
  211. package/dist/streams/types.d.ts +8 -8
  212. package/dist/streams/types.js +1 -1
  213. package/dist/tenant/advance.js +218 -160
  214. package/dist/tenant/artifact-tool.d.ts +5 -0
  215. package/dist/tenant/artifact-tool.js +119 -0
  216. package/dist/tenant/auth.d.ts +13 -9
  217. package/dist/tenant/auth.js +25 -32
  218. package/dist/tenant/commands.js +31 -10
  219. package/dist/tenant/context.d.ts +83 -26
  220. package/dist/tenant/delivery-token.js +4 -4
  221. package/dist/tenant/effects.d.ts +24 -14
  222. package/dist/tenant/effects.js +10 -398
  223. package/dist/tenant/ephemeral.d.ts +14 -2
  224. package/dist/tenant/ephemeral.js +8 -1
  225. package/dist/tenant/history.d.ts +13 -23
  226. package/dist/tenant/history.js +14 -62
  227. package/dist/tenant/host-token.d.ts +92 -0
  228. package/dist/tenant/host-token.js +179 -0
  229. package/dist/tenant/identity-file.d.ts +75 -0
  230. package/dist/tenant/identity-file.js +202 -0
  231. package/dist/tenant/issuers.d.ts +48 -0
  232. package/dist/tenant/issuers.js +311 -0
  233. package/dist/tenant/jwt.js +3 -3
  234. package/dist/tenant/module.js +1 -0
  235. package/dist/tenant/operator-keys.d.ts +18 -0
  236. package/dist/tenant/operator-keys.js +55 -0
  237. package/dist/tenant/outbound.d.ts +30 -0
  238. package/dist/tenant/outbound.js +142 -10
  239. package/dist/tenant/paths.d.ts +2 -1
  240. package/dist/tenant/paths.js +3 -1
  241. package/dist/tenant/principals.d.ts +2 -11
  242. package/dist/tenant/principals.js +10 -30
  243. package/dist/tenant/reset.d.ts +5 -2
  244. package/dist/tenant/reset.js +22 -2
  245. package/dist/tenant/run-grants.d.ts +49 -0
  246. package/dist/tenant/run-grants.js +62 -0
  247. package/dist/tenant/run-token.d.ts +70 -0
  248. package/dist/tenant/run-token.js +138 -0
  249. package/dist/tenant/runtime.d.ts +42 -0
  250. package/dist/tenant/runtime.js +134 -33
  251. package/dist/tenant/sandboxes.d.ts +119 -0
  252. package/dist/tenant/sandboxes.js +551 -0
  253. package/dist/tenant/session-streams.d.ts +6 -10
  254. package/dist/tenant/session-streams.js +9 -27
  255. package/dist/tenant/session.d.ts +2 -13
  256. package/dist/tenant/session.js +4 -28
  257. package/dist/tenant/sessions.d.ts +6 -1
  258. package/dist/tenant/sessions.js +29 -2
  259. package/dist/tenant/signing-keys.d.ts +2 -1
  260. package/dist/tenant/signing-keys.js +2 -2
  261. package/dist/tenant/status.d.ts +8 -3
  262. package/dist/tenant/status.js +5 -0
  263. package/dist/tenant/streams.d.ts +8 -2
  264. package/dist/tenant/streams.js +25 -6
  265. package/dist/tenant/sweep.js +37 -4
  266. package/dist/tenant/types.d.ts +39 -1
  267. package/dist/tenant/worker.d.ts +11 -5
  268. package/dist/tenant/worker.js +12 -6
  269. package/dist/vault/crypto.d.ts +7 -0
  270. package/dist/vault/crypto.js +16 -0
  271. package/dist/vault/oauth.d.ts +55 -0
  272. package/dist/vault/oauth.js +186 -0
  273. package/dist/vault/service.d.ts +54 -7
  274. package/dist/vault/service.js +261 -17
  275. package/dist/vault/sources.d.ts +54 -0
  276. package/dist/vault/sources.js +185 -0
  277. package/dist/version.d.ts +1 -1
  278. package/dist/version.js +1 -1
  279. package/package.json +14 -10
  280. package/dist/adapters/media.d.ts +0 -24
  281. package/dist/adapters/media.js +0 -111
  282. package/dist/host/cors.d.ts +0 -27
  283. package/dist/host/cors.js +0 -64
  284. package/dist/tenant/access-policy.d.ts +0 -30
  285. package/dist/tenant/access-policy.js +0 -52
  286. package/dist/tenant/browser.d.ts +0 -15
  287. package/dist/tenant/browser.js +0 -39
  288. package/dist/tenant/subject-limits.d.ts +0 -16
  289. package/dist/tenant/subject-limits.js +0 -45
  290. package/dist/tenant/tokens.d.ts +0 -14
  291. package/dist/tenant/tokens.js +0 -180
@@ -0,0 +1,551 @@
1
+ /**
2
+ * Sandboxes as a resource (blueprint D39, F7.1; Host feature `sandboxes`): `PUT`, `GET` and
3
+ * `DELETE /v1/sandboxes/{id}`, the list by label, sessions attaching with `sandbox: { id }`, and
4
+ * the checks every turn start makes on the sandbox its session is attached to.
5
+ *
6
+ * - **No scope.** The Runtime decides nothing about who shares a sandbox. It stores the
7
+ * sandbox's id, kind, spec and labels, and the session body names the sandbox it is attached
8
+ * to (`Session.sandboxId`); deleting a session (a sessions reset) only detaches it.
9
+ * - **Access.** An application key reaches every sandbox, and so does an application key acting
10
+ * for a subject (`Nylorun-Subject`), which needs `sandboxes:write` to change one. A token
11
+ * caller (a trusted issuer's token) reaches only the ids its grants match, and changes them
12
+ * only with `sandboxes:write`; any other id is the 404 of a missing one.
13
+ * - **Serial turns.** One turn at a time per sandbox: a session whose sandbox another attached
14
+ * session's turn holds is refused (`409 sandbox_busy`), as the session sandbox route is
15
+ * refused while its session's turn runs. Commands within the workspace keep queueing in the
16
+ * Sandbox Manager.
17
+ * - **Kinds.** `virtual`: its workspace is the virtual backend's, keyed by the sandbox id
18
+ * (`SandboxManager.sandboxKeyOf`) and created on the first tool call. `pod` (F7.2, Host feature
19
+ * `sandbox-pods`): an agent-sandbox pod on the Tenant's cluster, created at once, whose engine
20
+ * runs the turns of the sessions attached to it. Its lifecycle (`sandbox/pods/lifecycle.ts`)
21
+ * runs in the `Sandbox` object: this module records what is wanted (`desired`, `rev`) and asks
22
+ * for a reconcile after the commit (`ctx.sandboxSignal`). Without sandbox pods kind `pod` is
23
+ * `409 sandbox_unavailable`. `POST .../stop` suspends a pod (the volume is kept; the next turn
24
+ * starts it), `POST .../reset` gives it a new pod on a new volume. An expired pod refuses
25
+ * turns (`sandbox_expired`) until a `PUT` sets a longer `lifecycle.ttl`; a lost one
26
+ * (`sandbox_lost`) until a reset. Deleting a pod sandbox keeps its row (`desired: deleted`,
27
+ * invisible here) until its Sandbox is gone.
28
+ * - **Lifecycle events** go to the sandbox's own stream in the record (`Tx.sandboxEvent`,
29
+ * `record/sandbox.ts`); the session's log records `sandbox.attached` too.
30
+ */
31
+ import { sandboxGranted, } from "@nylorun/core/contracts";
32
+ import { canonical } from "../store/canonical.js";
33
+ import { resolveSandbox } from "../sandbox/resolve.js";
34
+ import { sandboxWorkspaceKey } from "../sandbox/records.js";
35
+ import { effectiveSandboxConfig, memoryMiB, readSandboxConfig, } from "../sandbox/tenant-config.js";
36
+ import { podName } from "../sandbox/pods/name.js";
37
+ import { expiresAtOf, podLifecycleConfig } from "../sandbox/pods/spec.js";
38
+ import { requirePods } from "../sandbox/placement.js";
39
+ import { parseSandboxDuration } from "@nylorun/core/define";
40
+ import { fail } from "./http.js";
41
+ /** The sandbox grants that limit `scope`, or undefined when it reaches every sandbox. */
42
+ export function sandboxGrantsOf(scope) {
43
+ return scope.kind === "token" ? (scope.sandboxes ?? []) : undefined;
44
+ }
45
+ /** The subject whose sessions a sandbox's view lists, when the request acts for one. */
46
+ function ownerOf(scope) {
47
+ return scope.kind === "token" || scope.kind === "subject" ? scope.subject : undefined;
48
+ }
49
+ const notFound = () => fail(404, "Sandbox not found");
50
+ /** The sandbox, when `grants` reach it; otherwise the 404 of a missing one. */
51
+ function reachable(grants, id) {
52
+ if (grants !== undefined && !sandboxGranted(grants, id))
53
+ notFound();
54
+ }
55
+ /** A pod sandbox being deleted is no longer a sandbox, here. */
56
+ function live(row) {
57
+ return row?.pod?.desired === "deleted" ? undefined : row;
58
+ }
59
+ /** A sandbox resource, unless it is gone or being deleted. */
60
+ export async function liveSandbox(t, id, options = {}) {
61
+ return live(await t.sandboxResource(id, options));
62
+ }
63
+ /** The compute state a pod's view reports. */
64
+ function podState(pod) {
65
+ if (pod.observed === "running")
66
+ return "running";
67
+ if (pod.observed === "creating")
68
+ return "creating";
69
+ return "stopped";
70
+ }
71
+ async function viewOf(ctx, t, sandbox, owner) {
72
+ const sessions = await t.sessionsOnSandbox(sandbox.id);
73
+ const record = sandbox.pod
74
+ ? undefined
75
+ : await t.get("sandboxes", sandboxWorkspaceKey(ctx.config.tenantId, sandbox.id));
76
+ const pod = sandbox.pod;
77
+ return {
78
+ id: sandbox.id,
79
+ kind: sandbox.kind,
80
+ labels: sandbox.labels,
81
+ spec: sandbox.spec,
82
+ state: pod ? podState(pod) : (record?.state ?? "ready"),
83
+ ...(pod
84
+ ? {
85
+ pod: {
86
+ desired: pod.desired,
87
+ observed: pod.observed,
88
+ volumeGeneration: pod.volumeGen,
89
+ hostEpoch: pod.hostEpoch,
90
+ ...(pod.expiresAt ? { expiresAt: pod.expiresAt } : {}),
91
+ ...(pod.reason ? { reason: pod.reason } : {}),
92
+ },
93
+ }
94
+ : {}),
95
+ sessions: sessions
96
+ .filter((session) => owner === undefined || session.ownerUserId === owner)
97
+ .map((session) => ({ id: session.id, activeTurnId: session.activeTurnId })),
98
+ createdAt: sandbox.createdAt,
99
+ updatedAt: sandbox.updatedAt,
100
+ };
101
+ }
102
+ /** The spec a `PUT` asks for, resolved against the Tenant's limits. */
103
+ function resolveSpec(body, config, ttlLimit) {
104
+ const pod = body.kind === "pod";
105
+ if (!pod && (body.storage !== undefined || body.lifecycle !== undefined))
106
+ fail(400, "storage and lifecycle are for kind pod only");
107
+ const resolved = resolveSandbox({
108
+ request: {
109
+ ...(body.image === undefined ? {} : { image: body.image }),
110
+ ...(body.network === undefined ? {} : { network: body.network }),
111
+ ...(body.resources === undefined ? {} : { resources: body.resources }),
112
+ },
113
+ config,
114
+ actingForSubject: false,
115
+ kind: pod ? "pod" : "virtual",
116
+ });
117
+ if (resolved.kind === "error")
118
+ return fail(resolved.status, resolved.errors.join(" "));
119
+ if (resolved.kind === "none")
120
+ return fail(400, "The sandbox has no spec");
121
+ if (!pod)
122
+ return resolved.spec;
123
+ checkTtl(body.lifecycle?.ttl, ttlLimit);
124
+ return {
125
+ ...resolved.spec,
126
+ ...(body.storage === undefined ? {} : { storage: body.storage }),
127
+ ...(body.lifecycle?.ttl === undefined ? {} : { lifecycle: { ttl: body.lifecycle.ttl } }),
128
+ };
129
+ }
130
+ /** Refuses a TTL longer than the Tenant's `limits.ttl`. */
131
+ function checkTtl(ttl, limit) {
132
+ if (ttl === undefined || limit === undefined)
133
+ return;
134
+ if ((parseSandboxDuration(ttl) ?? 0) > (parseSandboxDuration(limit) ?? Infinity))
135
+ fail(400, `lifecycle.ttl is ${ttl}; this Tenant allows at most ${limit}.`);
136
+ }
137
+ /**
138
+ * Why an existing sandbox does not match what a `PUT` asks for, comparing only the fields the
139
+ * request sends: an `ensure` that repeats its spec keeps working after the Tenant's defaults or
140
+ * limits change. A pod's `lifecycle.ttl` is not part of the fixed spec.
141
+ */
142
+ function specMismatch(existing, body) {
143
+ if (body.kind !== undefined && body.kind !== existing.kind)
144
+ return "kind";
145
+ if (body.image !== undefined && body.image !== existing.spec.image)
146
+ return "image";
147
+ if (body.network !== undefined) {
148
+ const asked = [...new Set((body.network.allow ?? []).map((host) => host.toLowerCase()))].sort();
149
+ const has = [...(existing.spec.network?.allow ?? [])].sort();
150
+ if (canonical(asked) !== canonical(has))
151
+ return "network";
152
+ }
153
+ const resources = existing.spec.resources;
154
+ if (body.resources?.cpus !== undefined && body.resources.cpus !== resources?.cpus)
155
+ return "resources";
156
+ if (body.resources?.memory !== undefined &&
157
+ (resources?.memory === undefined || memoryMiB(body.resources.memory) !== memoryMiB(resources.memory)))
158
+ return "resources";
159
+ if (body.storage !== undefined && body.storage !== existing.spec.storage)
160
+ return "storage";
161
+ return undefined;
162
+ }
163
+ /** Asks for a pod's reconcile once the transaction commits. */
164
+ function reconcileAfter(ctx, t, id) {
165
+ t.afterCommit(() => ctx.sandboxSignal(id, { kind: "reconcile" }));
166
+ }
167
+ /** Asks the pod to run (again): a new `rev`, so the service applies it. */
168
+ async function wantRunning(t, sandbox, now) {
169
+ await t.updateSandboxPod(sandbox.id, {
170
+ desired: "running",
171
+ rev: sandbox.pod.rev + 1,
172
+ observed: "creating",
173
+ startedAt: now,
174
+ lastActiveAt: now,
175
+ reason: null,
176
+ }, now);
177
+ }
178
+ /**
179
+ * `PUT /v1/sandboxes/{id}`: creates the sandbox (within the Tenant's limit on their number), or
180
+ * finds the one with this id, so "get or create" is one call. The spec is fixed once it exists;
181
+ * labels, when sent, replace its labels. On an existing pod sandbox it also starts a stopped or
182
+ * failed pod again, and a new `lifecycle.ttl` sets a new expiry (reviving an expired one).
183
+ */
184
+ export async function putSandbox(ctx, id, body, scope) {
185
+ const grants = sandboxGrantsOf(scope);
186
+ reachable(grants, id);
187
+ if (body.kind === "pod")
188
+ await requirePods(ctx.pods, "A pod sandbox");
189
+ const conflict = (field) => fail(409, `Sandbox ${id} exists with another ${field}. Its spec is fixed: delete it to create it again.`);
190
+ return ctx.store.tx(async (t) => {
191
+ const now = new Date().toISOString();
192
+ const config = await readSandboxConfig(t);
193
+ const deleting = (row) => {
194
+ if (row && !live(row))
195
+ fail(409, `Sandbox ${id} is being deleted. Try again once it is gone.`, {
196
+ code: "sandbox_busy",
197
+ details: { retryAfterSeconds: 5 },
198
+ }, { "retry-after": "5" });
199
+ };
200
+ const found = async (existing) => {
201
+ const mismatch = specMismatch(existing, body);
202
+ if (mismatch)
203
+ conflict(mismatch);
204
+ let current = existing;
205
+ if (body.labels !== undefined && canonical(body.labels) !== canonical(existing.labels)) {
206
+ await t.updateSandboxLabels(id, body.labels, now);
207
+ current = { ...current, labels: body.labels, updatedAt: now };
208
+ }
209
+ const pod = current.pod;
210
+ if (pod) {
211
+ const spec = current.spec;
212
+ const ttl = body.lifecycle?.ttl;
213
+ let revive = false;
214
+ if (ttl !== undefined && ttl !== spec.lifecycle?.ttl) {
215
+ checkTtl(ttl, config.limits?.ttl);
216
+ const next = { ...spec, lifecycle: { ...spec.lifecycle, ttl } };
217
+ const expiresAt = expiresAtOf(current.createdAt, ttl);
218
+ await t.updateSandboxSpec(id, next, now);
219
+ await t.updateSandboxPod(id, { expiresAt: expiresAt ?? null }, now);
220
+ current = { ...current, spec: next, pod: { ...pod, ...(expiresAt ? { expiresAt } : {}) } };
221
+ revive = pod.observed === "expired" && expiresAt !== undefined && Date.parse(expiresAt) > Date.now();
222
+ }
223
+ if (pod.observed === "lost")
224
+ fail(409, `Sandbox ${id} was lost (${pod.reason ?? "its volume is gone"}). Reset it to continue.`, {
225
+ code: "sandbox_lost",
226
+ });
227
+ if (revive || pod.desired !== "running" || pod.observed === "failed") {
228
+ await wantRunning(t, current, now);
229
+ reconcileAfter(ctx, t, id);
230
+ }
231
+ return viewOf(ctx, t, (await t.sandboxResource(id)) ?? current, ownerOf(scope));
232
+ }
233
+ return viewOf(ctx, t, current, ownerOf(scope));
234
+ };
235
+ const existing = await t.sandboxResource(id, { lock: true });
236
+ deleting(existing);
237
+ if (existing)
238
+ return found(existing);
239
+ const effective = effectiveSandboxConfig(config);
240
+ const spec = resolveSpec(body, effective, config.limits?.ttl);
241
+ const kind = body.kind ?? "virtual";
242
+ const row = {
243
+ id,
244
+ kind,
245
+ spec,
246
+ labels: body.labels ?? {},
247
+ createdAt: now,
248
+ updatedAt: now,
249
+ ...(kind === "pod"
250
+ ? {
251
+ pod: {
252
+ k8sName: podName(ctx.config.tenantId, id, 0),
253
+ volumeGen: 0,
254
+ desired: "running",
255
+ observed: "creating",
256
+ hostEpoch: 0,
257
+ rev: 1,
258
+ startedAt: now,
259
+ lastActiveAt: now,
260
+ ...(body.lifecycle?.ttl ? { expiresAt: expiresAtOf(now, body.lifecycle.ttl) } : {}),
261
+ },
262
+ }
263
+ : {}),
264
+ };
265
+ const created = await t.createSandboxResource(row, effective.limits.sandboxes);
266
+ if (created === "limit")
267
+ fail(409, `This Tenant allows at most ${effective.limits.sandboxes} sandboxes.`, {
268
+ code: "limit_exceeded",
269
+ details: { limit: "sandboxes", max: effective.limits.sandboxes },
270
+ });
271
+ if (created === "exists") {
272
+ // Created by another request since the read above: answer as for an existing one.
273
+ const again = (await t.sandboxResource(id, { lock: true })) ?? notFound();
274
+ deleting(again);
275
+ return found(again);
276
+ }
277
+ await t.sandboxEvent(id, "sandbox.created", { kind, labels: row.labels });
278
+ if (row.pod) {
279
+ // Created at once; idle from now if no turn comes.
280
+ reconcileAfter(ctx, t, id);
281
+ const idleAt = Date.parse(now) + podLifecycleConfig(config).idleMs;
282
+ t.afterCommit(() => ctx.sandboxSignal(id, { kind: "arm", timer: "idle", at: idleAt }));
283
+ }
284
+ return viewOf(ctx, t, row, ownerOf(scope));
285
+ });
286
+ }
287
+ /** `GET /v1/sandboxes/{id}`. */
288
+ export async function getSandbox(ctx, id, scope) {
289
+ reachable(sandboxGrantsOf(scope), id);
290
+ return ctx.store.tx(async (t) => {
291
+ const sandbox = (await liveSandbox(t, id)) ?? notFound();
292
+ return viewOf(ctx, t, sandbox, ownerOf(scope));
293
+ });
294
+ }
295
+ /** `GET /v1/sandboxes?label=k=v`: every sandbox with all the labels, that the caller reaches. */
296
+ export async function listSandboxes(ctx, labels, scope) {
297
+ const grants = sandboxGrantsOf(scope);
298
+ return ctx.store.tx(async (t) => {
299
+ const found = await t.listSandboxResources({ labels });
300
+ const views = [];
301
+ for (const sandbox of found)
302
+ if (live(sandbox) && (grants === undefined || sandboxGranted(grants, sandbox.id)))
303
+ views.push(await viewOf(ctx, t, sandbox, ownerOf(scope)));
304
+ return { sandboxes: views };
305
+ });
306
+ }
307
+ /** Refused while a session attached to sandbox `id` has a turn running. */
308
+ async function refuseBusy(t, id) {
309
+ const busy = (await t.sessionsOnSandbox(id)).some((session) => session.activeTurnId !== null);
310
+ if (busy)
311
+ fail(409, `Sandbox ${id} has a turn running. Cancel it, or wait for it to end.`, {
312
+ code: "sandbox_busy",
313
+ });
314
+ }
315
+ /**
316
+ * `DELETE /v1/sandboxes/{id}`: deletes the sandbox and its workspace. Refused while a session
317
+ * attached to it has an active turn. Sessions stay attached by id: their next turn is refused
318
+ * (`sandbox_unavailable`) unless a sandbox with this id exists again. A pod sandbox's row stays
319
+ * (`desired: deleted`) until its Sandbox is gone.
320
+ */
321
+ export async function deleteSandbox(ctx, id, scope) {
322
+ reachable(sandboxGrantsOf(scope), id);
323
+ const deleted = await ctx.store.tx(async (t) => {
324
+ const sandbox = await liveSandbox(t, id, { lock: true });
325
+ if (!sandbox)
326
+ return false;
327
+ await refuseBusy(t, id);
328
+ await t.sandboxEvent(id, "sandbox.deleted", {});
329
+ if (sandbox.pod) {
330
+ const epoch = sandbox.pod.hostEpoch + 1;
331
+ await t.updateSandboxPod(id, { desired: "deleted", observed: "deleting", rev: sandbox.pod.rev + 1, hostEpoch: epoch }, new Date().toISOString());
332
+ reconcileAfter(ctx, t, id);
333
+ t.afterCommit(() => ctx.harness.revokeHost(id, epoch));
334
+ }
335
+ else
336
+ await t.deleteSandboxResource(id);
337
+ return true;
338
+ });
339
+ // After the commit: a turn that starts now finds no sandbox and is refused.
340
+ if (deleted)
341
+ await ctx.sandbox.removeSandbox(id);
342
+ return { id, deleted };
343
+ }
344
+ /** The pod sandbox `id` under the row lock, or a 404 or 400. */
345
+ async function podSandbox(t, id) {
346
+ const sandbox = (await liveSandbox(t, id, { lock: true })) ?? notFound();
347
+ if (!sandbox.pod)
348
+ return fail(400, `Sandbox ${id} is virtual: only a pod sandbox stops and resets`);
349
+ return sandbox;
350
+ }
351
+ /**
352
+ * `POST /v1/sandboxes/{id}/stop`: suspends a pod sandbox. Its volume is kept, and the next turn
353
+ * of a session attached to it (or a `PUT`) starts it again. Refused during a turn.
354
+ */
355
+ export async function stopSandbox(ctx, id, scope) {
356
+ reachable(sandboxGrantsOf(scope), id);
357
+ return ctx.store.tx(async (t) => {
358
+ const sandbox = await podSandbox(t, id);
359
+ await refuseBusy(t, id);
360
+ const pod = sandbox.pod;
361
+ if (pod.desired === "running" && pod.observed !== "lost" && pod.observed !== "expired") {
362
+ const now = new Date().toISOString();
363
+ const epoch = pod.hostEpoch + 1;
364
+ await t.updateSandboxPod(id, { desired: "suspended", rev: pod.rev + 1, hostEpoch: epoch }, now);
365
+ await t.sandboxEvent(id, "sandbox.suspended", { reason: "stop" });
366
+ reconcileAfter(ctx, t, id);
367
+ t.afterCommit(() => ctx.harness.revokeHost(id, epoch));
368
+ }
369
+ return viewOf(ctx, t, (await t.sandboxResource(id)) ?? sandbox, ownerOf(scope));
370
+ });
371
+ }
372
+ /**
373
+ * `POST /v1/sandboxes/{id}/reset`: a new pod on a new, empty volume (the next volume
374
+ * generation), with a new join token; the old Sandbox and volume are deleted. Leaves `lost` and
375
+ * `failed`; an expired sandbox stays expired. Refused during a turn, and while an earlier
376
+ * reset's Sandbox is still being deleted.
377
+ */
378
+ export async function resetSandbox(ctx, id, scope) {
379
+ reachable(sandboxGrantsOf(scope), id);
380
+ await requirePods(ctx.pods, "Resetting a pod sandbox");
381
+ return ctx.store.tx(async (t) => {
382
+ const sandbox = await podSandbox(t, id);
383
+ await refuseBusy(t, id);
384
+ const pod = sandbox.pod;
385
+ if (pod.retiring !== undefined)
386
+ fail(409, `Sandbox ${id} is still deleting its previous volume. Try again shortly.`, {
387
+ code: "sandbox_busy",
388
+ details: { retryAfterSeconds: 5 },
389
+ }, { "retry-after": "5" });
390
+ const now = new Date().toISOString();
391
+ const volumeGen = pod.volumeGen + 1;
392
+ const epoch = pod.hostEpoch + 1;
393
+ const expired = pod.expiresAt !== undefined && Date.parse(pod.expiresAt) <= Date.now();
394
+ await t.updateSandboxPod(id, {
395
+ k8sName: podName(ctx.config.tenantId, id, volumeGen),
396
+ volumeGen,
397
+ retiring: pod.k8sName,
398
+ desired: "running",
399
+ observed: expired ? "expired" : "creating",
400
+ rev: pod.rev + 1,
401
+ hostEpoch: epoch,
402
+ joinTokenHash: null,
403
+ podUid: null,
404
+ volumeId: null,
405
+ startedAt: now,
406
+ lastActiveAt: now,
407
+ reason: null,
408
+ }, now);
409
+ await t.sandboxEvent(id, "sandbox.reset", { volumeGeneration: volumeGen });
410
+ reconcileAfter(ctx, t, id);
411
+ t.afterCommit(() => ctx.harness.revokeHost(id, epoch));
412
+ return viewOf(ctx, t, (await t.sandboxResource(id)) ?? sandbox, ownerOf(scope));
413
+ });
414
+ }
415
+ /** `GET /v1/sandboxes/{id}/events`: the sandbox's lifecycle stream. */
416
+ export async function sandboxEventsOf(ctx, id, scope, fromSeq) {
417
+ reachable(sandboxGrantsOf(scope), id);
418
+ return ctx.store.tx(async (t) => ({
419
+ events: await t.sandboxEvents(id, fromSeq === undefined ? {} : { fromSeq }),
420
+ }));
421
+ }
422
+ /**
423
+ * Attaches a new session to sandbox `id` (`PutSessionRequest.sandbox = { id }`): the sandbox
424
+ * must exist and be reachable with `grants`. Returns its spec, which the session pins. Runs in
425
+ * the session's transaction, after its session row lock.
426
+ */
427
+ export async function attachSandbox(t, id, grants) {
428
+ reachable(grants, id);
429
+ return (await liveSandbox(t, id, { lock: true })) ?? notFound();
430
+ }
431
+ /** Records the attachment on the session's log and on the sandbox's stream. */
432
+ export async function recordAttachment(t, sessionId, sandboxId) {
433
+ await t.event(sessionId, null, "sandbox.attached", { sandboxId });
434
+ await t.sandboxEvent(sandboxId, "sandbox.attached", { sessionId });
435
+ }
436
+ /**
437
+ * Checks a turn start on a session attached to a sandbox, in the command's transaction after
438
+ * the session row lock: the sandbox still exists, a token caller's grants reach it, and
439
+ * no other session attached to it has a turn running. Holds the sandbox row's lock until the
440
+ * transaction ends, so two turn starts on one sandbox are decided one after the other.
441
+ *
442
+ * A pod sandbox must also be usable: not lost (`sandbox_lost`) or expired (`sandbox_expired`),
443
+ * on a Runtime with sandbox pods (`sandbox_unavailable`). A stopped or failed pod is asked to
444
+ * run again, after the commit.
445
+ */
446
+ export async function checkSandboxTurn(t, session, scope, ctx) {
447
+ const id = session.sandboxId;
448
+ if (id === undefined)
449
+ return;
450
+ const grants = sandboxGrantsOf(scope);
451
+ if (grants !== undefined && !sandboxGranted(grants, id))
452
+ fail(403, `This token does not reach sandbox ${id}.`, { code: "sandbox_not_granted" });
453
+ const sandbox = await liveSandbox(t, id, { lock: true });
454
+ if (!sandbox)
455
+ fail(409, `Sandbox ${id} was deleted. Create it again to continue this session.`, {
456
+ code: "sandbox_unavailable",
457
+ });
458
+ const holder = (await t.sessionsOnSandbox(id)).find((other) => other.id !== session.id && other.activeTurnId !== null);
459
+ if (holder)
460
+ fail(409, `Sandbox ${id} is running another session's turn. Turns are serial per sandbox.`, {
461
+ code: "sandbox_busy",
462
+ details: { retryAfterSeconds: 5 },
463
+ }, { "retry-after": "5" });
464
+ const pod = sandbox?.pod;
465
+ if (!pod)
466
+ return;
467
+ if (!ctx?.pods)
468
+ fail(409, `Sandbox ${id} is a pod sandbox, and this Runtime has no sandbox pods: run \`nylorun sandbox enable --context <name>\`.`, {
469
+ code: "sandbox_unavailable",
470
+ });
471
+ if (pod.observed === "lost")
472
+ fail(409, `Sandbox ${id} was lost (${pod.reason ?? "its volume is gone"}). Reset it to continue.`, {
473
+ code: "sandbox_lost",
474
+ });
475
+ if (pod.observed === "expired")
476
+ fail(409, `Sandbox ${id} expired. PUT it with a longer lifecycle.ttl to revive it.`, {
477
+ code: "sandbox_expired",
478
+ });
479
+ const now = new Date().toISOString();
480
+ if (pod.desired !== "running" || pod.observed === "failed") {
481
+ await wantRunning(t, sandbox, now);
482
+ reconcileAfter(ctx, t, id);
483
+ }
484
+ else
485
+ await t.updateSandboxPod(id, { lastActiveAt: now }, now);
486
+ }
487
+ /** A sessions reset detaches every session from its sandbox: each sandbox's stream says so. */
488
+ export async function detachAllSessions(t) {
489
+ for (const sandbox of await t.listSandboxResources()) {
490
+ const sessions = await t.sessionsOnSandbox(sandbox.id);
491
+ if (sessions.length === 0)
492
+ continue;
493
+ await t.sandboxResource(sandbox.id, { lock: true });
494
+ for (const session of sessions)
495
+ await t.sandboxEvent(sandbox.id, "sandbox.detached", {
496
+ sessionId: session.id,
497
+ reason: "reset",
498
+ });
499
+ }
500
+ }
501
+ /**
502
+ * Marks the end of a turn's segment on pod sandbox `id`: idle counts from now, and the idle
503
+ * timer is armed (D34). Best effort.
504
+ */
505
+ export async function touchPodSandbox(ctx, id) {
506
+ try {
507
+ const at = await ctx.store.tx(async (t) => {
508
+ const sandbox = await t.sandboxResource(id, { lock: true });
509
+ if (!sandbox?.pod || sandbox.pod.desired !== "running")
510
+ return undefined;
511
+ const now = new Date();
512
+ await t.updateSandboxPod(id, { lastActiveAt: now.toISOString() }, now.toISOString());
513
+ return now.getTime() + podLifecycleConfig(await readSandboxConfig(t)).idleMs;
514
+ });
515
+ if (at !== undefined)
516
+ await ctx.sandboxSignal(id, { kind: "arm", timer: "idle", at });
517
+ }
518
+ catch (error) {
519
+ ctx.config.logger.warn("sandbox idle timer not armed", {
520
+ sandboxId: id,
521
+ message: error instanceof Error ? error.message : String(error),
522
+ });
523
+ }
524
+ }
525
+ /**
526
+ * Re-arms the idle timer of every running pod sandbox after the Tenant's settings change, so a
527
+ * shorter `limits.idle` applies to pods already idle (D36: the settings are read whenever a
528
+ * timer is set). Best effort.
529
+ */
530
+ export async function rearmPodTimers(ctx) {
531
+ if (!ctx.pods)
532
+ return;
533
+ try {
534
+ const { rows, idleMs } = await ctx.store.tx(async (t) => ({
535
+ rows: await t.listSandboxResources(),
536
+ idleMs: podLifecycleConfig(await readSandboxConfig(t)).idleMs,
537
+ }));
538
+ for (const row of rows)
539
+ if (row.pod?.desired === "running")
540
+ await ctx.sandboxSignal(row.id, {
541
+ kind: "arm",
542
+ timer: "idle",
543
+ at: Date.parse(row.pod.lastActiveAt ?? row.createdAt) + idleMs,
544
+ });
545
+ }
546
+ catch (error) {
547
+ ctx.config.logger.warn("sandbox idle timers not re-armed", {
548
+ message: error instanceof Error ? error.message : String(error),
549
+ });
550
+ }
551
+ }
@@ -20,23 +20,24 @@
20
20
  */
21
21
  import type { IncomingMessage, ServerResponse } from "node:http";
22
22
  import type { LiveEvent } from "@nylorun/core/contracts";
23
- import type { TenantContext } from "./context.js";
23
+ import type { AuthScope, TenantContext } from "./context.js";
24
24
  import type { StreamsWiring } from "./streams.js";
25
- /** Why the Runtime ended a stream a subject token opened. */
26
- export type StreamEndReason = "token_expired" | "revoked";
25
+ /** Why the Runtime ended a stream a token opened. */
26
+ export type StreamEndReason = "token_expired";
27
27
  /** Where an observer's events go: an SSE response, or an in-process reader. */
28
28
  export interface ObserverSink {
29
29
  write(event: LiveEvent): void;
30
30
  /** Ends the stream; with a reason, the client is told why before it ends. */
31
31
  end(reason?: StreamEndReason): void;
32
32
  }
33
- /** The subject token a stream was opened with: it ends at expiry or revocation. */
33
+ /** The token a stream was opened with (a trusted issuer's): the stream ends at its expiry. */
34
34
  export interface StreamHolder {
35
35
  readonly subject: string;
36
- readonly epoch: number;
37
36
  /** Epoch ms when the token expires. */
38
37
  readonly expiresAt: number;
39
38
  }
39
+ /** The holder a stream opened with `scope` records: token callers only. */
40
+ export declare function streamHolderOf(scope: AuthScope): StreamHolder | undefined;
40
41
  /** One client of a session: the next sequence it needs. */
41
42
  interface Observer {
42
43
  readonly sink: ObserverSink;
@@ -114,11 +115,6 @@ export declare function served(event: LiveEvent): boolean;
114
115
  * the Tenant end too.
115
116
  */
116
117
  export declare function checkSessionStreams(ctx: TenantContext): Promise<void>;
117
- /**
118
- * Ends the streams of `subject` opened with a token older than `epoch` (a revocation), on
119
- * this process. Other processes do the same on the `subject.revoked` signal.
120
- */
121
- export declare function endSubjectStreams(hub: SessionStreams, subject: string, epoch: number): void;
122
118
  /** Reset or close: end every session observer and stop every shared read. */
123
119
  export declare function clearObservers(hub: SessionStreams): void;
124
120
  /** Close: end every session observer. */
@@ -2,6 +2,12 @@ import { decodeCursor, encodeCursor } from "../record/index.js";
2
2
  import { basinOf } from "../streams/basin.js";
3
3
  import { sessionStream, } from "../streams/types.js";
4
4
  import { fail } from "./http.js";
5
+ /** The holder a stream opened with `scope` records: token callers only. */
6
+ export function streamHolderOf(scope) {
7
+ if (scope.kind !== "token")
8
+ return undefined;
9
+ return { subject: scope.subject, expiresAt: scope.expiresAt };
10
+ }
5
11
  export function createSessionStreams() {
6
12
  return { wiring: undefined, generation: 0, sessions: new Map() };
7
13
  }
@@ -233,7 +239,7 @@ async function join(ctx, sessionId, observer) {
233
239
  feed.observers.add(observer);
234
240
  return feed;
235
241
  }
236
- /** Ends one observer early (its token expired or was revoked). */
242
+ /** Ends one observer early (its token expired). */
237
243
  function endObserver(hub, feed, observer, reason) {
238
244
  if (!feed.observers.has(observer))
239
245
  return;
@@ -328,24 +334,12 @@ async function stillCurrent(ctx, feed) {
328
334
  */
329
335
  export async function checkSessionStreams(ctx) {
330
336
  const feeds = [...ctx.sessionStreams.sessions.values()];
331
- // Also a backstop for lost `subject.revoked` signals: the holders' current epochs.
332
- const subjects = new Set();
333
- for (const feed of feeds)
334
- for (const observer of feed.observers)
335
- if (observer.holder)
336
- subjects.add(observer.holder.subject);
337
- const { exists, epochs, generation } = await ctx.store.tx(async (t) => {
337
+ const { exists, generation } = await ctx.store.tx(async (t) => {
338
338
  const exists = new Set();
339
339
  for (const feed of feeds)
340
340
  if (await t.get("sessions", feed.sessionId))
341
341
  exists.add(feed.sessionId);
342
- return {
343
- exists,
344
- generation: (await t.basinGenerations()).current,
345
- epochs: subjects.size === 0
346
- ? new Map()
347
- : await t.subjectEpochs([...subjects]),
348
- };
342
+ return { exists, generation: (await t.basinGenerations()).current };
349
343
  });
350
344
  if (generation !== ctx.sessionStreams.generation)
351
345
  ctx.sessionStreams.wiring?.moveTo(generation);
@@ -355,18 +349,6 @@ export async function checkSessionStreams(ctx) {
355
349
  ? { basin: currentBasin(ctx), stream: sessionStream(feed.sessionId) }
356
350
  : undefined))
357
351
  endFeed(ctx.sessionStreams, feed);
358
- for (const [subject, epoch] of epochs)
359
- endSubjectStreams(ctx.sessionStreams, subject, epoch);
360
- }
361
- /**
362
- * Ends the streams of `subject` opened with a token older than `epoch` (a revocation), on
363
- * this process. Other processes do the same on the `subject.revoked` signal.
364
- */
365
- export function endSubjectStreams(hub, subject, epoch) {
366
- for (const feed of [...hub.sessions.values()])
367
- for (const observer of [...feed.observers])
368
- if (observer.holder?.subject === subject && observer.holder.epoch < epoch)
369
- endObserver(hub, feed, observer, "revoked");
370
352
  }
371
353
  function endFeed(hub, feed) {
372
354
  feed.read.abort();