@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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,139 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.17.0-beta
4
+
5
+ ### Major Changes
6
+
7
+ - bd478ee: **Subject tokens, browser keys and derived principals leave open source (F9 I3, protocol 7).** Upgrade these with the Runtime: they speak protocol 7, which this Runtime serves beside 4, 5 and 6; protocol 6 clients keep working on every route that remains. Browsers and apps present a trusted issuer's token (F9 I2), servers an operator key (F9 I1), and a person's own credentials come from the operator's credential resolver (F9 C1). See MIGRATION.md.
8
+
9
+ - **Breaking (`@nylorun/runtime`, `@nylorun/core`): subject tokens are gone.** `POST /v1/tokens`, `POST /v1/access/revocations`, subject-token verification, revocation epochs and the `subject.revoked` stream signal are removed; the routes answer `404`, and a JWT no trusted issuer signed is the opaque `404`. A stream opened with a token now ends only at its expiry (`event: nylorun.closed`, reason `token_expired`; `StreamClosedFrame` loses `revoked`). Migration `0010_oss_auth_removals` drops the `subject_epochs`, `subject_usage` and `publishable_keys` tables. The `token` caller is a trusted issuer's token only: it loses `role`, `limits` and `epoch`, and `issuer` is always set; `GET /v1/me` no longer reports `via: token`. Core drops `SUBJECT_TOKEN_TYPE`, `SUBJECT_TOKEN_AUDIENCE`, `subjectTokenIssuer` (the Runtime's own tokens use `tenantTokenIssuer`, same value), `CreateTokenRequest`/`Response`, `RevokeSubject*`, `SubjectTokenClaims`, `TOKEN_TTL_MIN_SECONDS` and `TOKEN_TTL_DEFAULT_SECONDS`; `TOKEN_SCOPES` is `agents:read`, `sessions:own`, `sandboxes:write`.
10
+ - **Breaking (`@nylorun/runtime`, `@nylorun/core`): the access policy and role limits are gone.** `GET`/`PUT /v1/access/policy` answer `404`, a turn is never refused with `429 limit_exceeded` for a role (limit requests at your proxy), and an existing `access.policy` setting is ignored. Core drops `AccessPolicy*`, `AccessRole*`, `RoleLimits*`, `DEFAULT_ACCESS_POLICY`, `PutAccessPolicyRequest` and `ROLE_NAME_PATTERN`.
11
+ - **Breaking (`@nylorun/runtime`, `@nylorun/core`): publishable keys and the Runtime's CORS are gone.** `/v1/access/publishable-keys*` answer `404`, `Nylorun-Key` is ignored, `NYLORUN_BROWSER_ACCESS` and `browserAccess` (host.json, `createHost`, `startEphemeralRuntime`) are removed, and the Runtime sends no CORS headers. A request with `Origin` reaches the Tenant routes of the public listener with no toggle: a trusted issuer's token is served, an application key or delivery token is `403 origin_rejected`; admin routes, `/health`, `/ready` and the operator listener still refuse `Origin`. Any `OPTIONS` request is `204` with `Allow` and no CORS header: the operator's proxy answers preflights. Route declarations lose `browser` (`x-nylorun-browser` leaves the OpenAPI document) and the `publishable` credential; the token security scheme is `issuerToken`. Core drops `PUBLISHABLE_KEY_HEADER`, `PUBLISHABLE_KEY_PATTERN`, `PUBLISHABLE_KEY_ID_PATTERN`, `tenantOfPublishableKey`, `newPublishableKey`, `newPublishableKeyId`, `PublishableKey*`, `OriginEntrySchema`, `originAllowed` and `LOOPBACK_ORIGIN_WILDCARDS`.
12
+ - **Breaking (`@nylorun/runtime`, `@nylorun/core`, `@nylorun/agents`): vault routes are application-only, and `vaults:own` is retired.** Every `/v1/vaults` route takes an application key acting for no one (operator keys, Studio); a request acting for a subject or a trusted issuer's token gets `403 scope_required`. `vaults:own` leaves `SUBJECT_SCOPES`, `TOKEN_SCOPES` and the issuer scopes; `Nylorun-Scopes` may still name it and it grants nothing. Existing user vaults stay attachable to their own owner's sessions. `client.createVault` takes `{ scope: "installation" }` or `ownerUserId`, and `client.listVaults()` takes an optional owner.
13
+ - **Breaking (`@nylorun/runtime`, `@nylorun/core`, `@nylorun/admin`, `nylorun`): derived principals are gone.** Only Studio's key is derived from the admin key. `NYLORUN_DERIVED_PRINCIPALS` is ignored and no longer written, `hostPrincipals` takes no `derived`, `startEphemeralRuntime` no `derivedPrincipals`, and `@nylorun/admin` drops `deriveTenantKey`, `PROJECT_PRINCIPAL_ID` and `admin.deriveTenantKey`. Keys an earlier Host derived stay in the database and keep working as ordinary keys; replace them with operator keys. Core renames `DERIVED_PRINCIPAL_ID_PATTERN` to `APPLICATION_KEY_ID_PATTERN`.
14
+ - **Breaking (`@nylorun/agents`):** `createTokenEndpoint`, `createBrowserClient` and the `@nylorun/agents/browser` entry, `client.tokens`, `TokensClient`, `PublishableKeysClient`, `client.access.getPolicy`/`putPolicy`/`revokeSubject`/`publishableKeys` and `Destination.publishableKey` are removed. `client.access.signingKeys` and `client.access.jwks()` stay, and `Destination.token` takes a trusted issuer's tokens.
15
+ - **Breaking (`@nylorun/cli`):** `nylo access policy|keys|revoke|token` are removed and name what replaces them; `nylo access signing-keys list|rotate|revoke` stays.
16
+ - **Breaking (`@nylorun/core`, `@nylorun/agents`, `@nylorun/studio`): the Studio embed message `open.babai` is now `open.session`** (`{ sessionId }`): it asks the embedding app to open that session in its own UI. Studio sends no such message itself.
17
+ - **Breaking (`nylorun`): Studio embedding is opt-in.** No origin may frame Studio by default; list your app's origins with `nylorun start --studio-embed-origin <origin>`. A Tenant's `.env` from an earlier nylorun keeps the origins it had until `--studio-embed-origin-reset`.
18
+ - `PROTOCOL_VERSION` is 7 and `HOST_PROTOCOL` 4–7; the Host features `subject-tokens`, `browser-access` and `derived-principals` are removed. `@nylorun/studio` and `@nylorun/create-agent` speak protocol 7.
19
+
20
+ ### Minor Changes
21
+
22
+ - 8773586: **Installation vaults and a credential resolver (F9 C1).** A session's MCP credential now comes from its attached vaults, then from the operator's own credential resolver; nothing changes for existing vaults, and the protocol stays at 6.
23
+
24
+ - Installation vaults: `POST /v1/vaults` takes `scope: "installation"` (no `ownerUserId`) from an application key acting for no one; a request acting for a subject gets `403`. The vault is owned by `installation`, now a reserved subject like `host`. Any session may attach one and select its credentials. `GET /v1/vaults` from an application key lists them after the named person's vaults, and lists only them without `ownerUserId`; a request acting for a subject never sees one (the opaque `404`). The host model vault stays hidden and unattachable. Migration 0008 adds the scope.
25
+ - The credential resolver: `NYLORUN_RESOLVER_URL` and `NYLORUN_RESOLVER_TOKEN` on the gateway (`TenantConfig.resolver` and `startEphemeralRuntime({ resolver })` in process). When the session's vaults hold nothing for a remote MCP server's URL, the Runtime POSTs `{ owner, session, turn, target: { kind: "mcp", server, agent, url } }` with the resolver's bearer: `200 { headers, expiresAt? }` is used, `404` goes without a credential, and anything else or no answer within 5 s refuses the server with `credential_unavailable`. Owner and turn come from the session row. Answers are cached per owner and URL until `expiresAt`, at most 5 minutes (60 s without one), and concurrent misses share one request. See DEPLOYMENT.md, Credentials.
26
+ - `nylorun`: the gateway's Compose service passes `NYLORUN_RESOLVER_URL` and `NYLORUN_RESOLVER_TOKEN` from the shell that runs `nylorun start` (unset by default).
27
+ - Studio: the Vault page is now **Connections**, and creates installation vaults.
28
+
29
+ - e1bfb4b: **MCP OAuth connect for installation vaults (F9 C2).** The installation can sign in once to a remote MCP server that uses OAuth, and every session that attaches the vault uses the credential. Additive; the protocol stays at 6.
30
+
31
+ - `POST /v1/vaults/{vaultId}/oauth/start` (application keys acting for no one, installation vaults only) takes `{ url, server, clientId? }` and answers `{ authorizeUrl, expiresAt }`. The Runtime discovers the server's authorization server (RFC 9728, then RFC 8414), registers itself (RFC 7591) unless `clientId` names a registered client, and starts an S256 PKCE sign-in whose `state` works once, for ten minutes. A server without registration and no `clientId` is `400 oauth_client_required`.
32
+ - `GET /v1/oauth/callback` takes the browser back: anonymous and unversioned like an artifact link, it exchanges the code and stores an `oauth` credential bound to the URL, named after `server` (connecting again rotates it), and answers a small HTML page. An unknown, used or expired `state` is `oauth_state_invalid`; the authorization server's refusal is `oauth_failed`. The callback's base is `NYLORUN_PUBLIC_URL` (`TenantConfig.publicUrl`), else the start request's origin.
33
+ - Every OAuth step runs in the gateway's keys module (F9-D14): the runtime container never sees a token, the PKCE verifier or a client secret, and makes no outbound call. Migration 0009 adds `oauth_pending`, with the verifier and secret sealed under the vault key. `Keys` gains `startOAuth` and `finishOAuth`.
34
+ - OAuth requests, including refresh of every OAuth vault credential, now go through `guardedFetch` (`tenant/outbound.ts`): the `NYLORUN_ENDPOINT_*` address policy checked on the address connected to, no redirects, a bounded answer. With `NYLORUN_ENDPOINT_PRIVATE=refuse` a token endpoint on a private address is refused, where refresh used to call it.
35
+ - `@nylorun/core`: `ERROR_CODES` adds `oauth_client_required`, `oauth_state_invalid` and `oauth_failed`; `StartOAuthRequestSchema` and `StartOAuthResponseSchema`.
36
+ - `nylorun mcp connect <url> --server <name> [--vault <id>] [--client-id <id>]`: creates the installation vault `mcp` if needed, opens the sign-in page and waits up to 10 minutes for the credential. See DEPLOYMENT.md, "Connecting a remote MCP server with OAuth".
37
+
38
+ - 5cfaed9: **Operator keys, and project links on them (F9 I1).** The Admin API manages the Tenant's application keys by name: `PUT /v1/admin/keys/{id}` creates a key or rotates it and returns it once, `GET /v1/admin/keys` lists every key's id, role and issue time (never the keys), and `DELETE /v1/admin/keys/{id}` removes one. A rotated or deleted key stops authenticating on its next request. Ids follow `^[a-z][a-z0-9-]{0,31}$`; `studio` (derived from the admin key) is refused. Keys keep today's format (64 hex) and only their SHA-256 is stored, in the existing principals table (no migration). Host feature `operator-keys` (additive, protocol unchanged); core adds the `OperatorKey`, `ListOperatorKeysResponse`, `PutOperatorKeyResponse` and `DeleteOperatorKeyResponse` schemas.
39
+
40
+ - `@nylorun/admin`: `admin.keys.put(id)`, `admin.keys.list()` and `admin.keys.delete(id)`; they refuse with `incompatible_host` when the Host lacks `operator-keys`.
41
+ - `nylorun key put|list|rm <id>` manages a running local Tenant's keys; `put` prints the key once on stdout.
42
+ - `nylorun start` no longer derives the project's key. It keeps `.nylorun/credentials.json` while its key still reaches the Tenant (one authenticated read); otherwise it gives the project the operator key `project`, which the Host root keeps in `project-credentials.json` (0600) so every checkout linked to the Tenant shares it. An existing derived project key keeps working and is adopted.
43
+ - `nylorun sandbox` uses the linked project's key, or the operator key `cli` it puts once and keeps in `<Host root>/cli-credentials.json` (0600).
44
+ - The ephemeral Runtime (`startEphemeralRuntime`) no longer registers the derived `project` principal; pass `derivedPrincipals: ["project"]` to keep it. Derived principals (`NYLORUN_DERIVED_PRINCIPALS`, `deriveTenantKey`) still work on a Host.
45
+
46
+ - c0b604e: **Trusted issuers (F9 I2).** The Tenant API accepts JWTs from the operator's own identity provider as bearers, configured in an identity file: `NYLORUN_IDENTITY_FILE` names a YAML file listing each issuer's `name`, `issuer`, `audience`, `jwks` URL or static PEM `keys`, a `subject` template over scalar claims (`u:{sub}`), `scopes` from a claim or a fixed list, `allowedScopes`, an optional `agents` allowlist, optional `sandboxes` grant templates (`{org_id}/*`) and `maxLifetime`. A malformed file stops the boot, naming the issuer and the field; a subject template without a claim is malformed. A bearer whose unverified `iss` names an issuer is verified with RS256, ES256 or EdDSA only, up to 16 KiB, `aud` matching, `exp` and `iat` required and `exp − iat` within `maxLifetime`, and becomes a token caller (`role: issuer:<name>`) with the issuer's scopes, agents and rendered sandbox grants; subject revocation does not reach it. JWKS keys are fetched only from the configured URL (no redirects), cached by `kid`, refetched at most once a minute for an unknown `kid`; while a JWKS is unreachable cached keys keep working and a new `kid` is `401 issuer_unavailable` (new error code). A browser request with an issuer token needs no publishable key (CORS comes from the operator's proxy). New: `GET /v1/me` reports the subject, scopes, agents, sandbox grants and `via` (`application:<id>`, `subject`, `token` or `issuer:<name>`) of any credential; the issuer-only scope `studio`; `ISSUER_SCOPES`, `CALLER_SCOPES` and `MeResponseSchema` in `@nylorun/core/contracts`; `parseIdentityFile` and `createTrustedIssuers` in `@nylorun/runtime`, and an `issuers` option on `startEphemeralRuntime`; Host feature `trusted-issuers` (additive, protocol unchanged). `nylorun start` sets `NYLORUN_IDENTITY_FILE=/nylorun/identity.yaml` on the runtime container when `<Host root>/identity.yaml` exists.
47
+
48
+ ### Patch Changes
49
+
50
+ - Pin core to the tested release.
51
+ - Pin harness to the tested release.
52
+ - Updated dependencies [8773586]
53
+ - Updated dependencies [e1bfb4b]
54
+ - Updated dependencies [5cfaed9]
55
+ - Updated dependencies [bd478ee]
56
+ - Updated dependencies [c0b604e]
57
+ - Updated dependencies
58
+ - @nylorun/core@0.13.0-beta
59
+ - @nylorun/harness@0.22.1-beta
60
+
61
+ ## 0.16.0-beta
62
+
63
+ ### Major Changes
64
+
65
+ - b352feb: **File artifacts, message parts and capability links (protocol 6).** Upgrade these with the Runtime: they speak protocol 6, which this Runtime serves beside 4 and 5. See MIGRATION.md.
66
+
67
+ - **`@nylorun/runtime`: file artifacts.** A file is an artifact: an `af_` id, a name and numbered immutable versions, with rows in the new `artifacts` and `artifact_versions` tables (migration `0004_artifacts`) and bytes in the Object store (`BlobStore`). `POST /v1/artifacts?name=&sessionId=` uploads a file in one streamed request and `POST /v1/artifacts/{id}/versions` adds a version, within the Tenant's limits (`GET`/`PUT /v1/tenant/artifacts`: 100 MiB per file and 10 GiB in all by default); a body past either is `413 limit_exceeded`, refused mid-stream with nothing stored. `GET /v1/artifacts` lists them (by session), `GET /v1/artifacts/{id}` reads one with its versions, `GET /v1/artifacts/{id}/versions/{n|latest}/content` downloads through the Runtime with HTTP Range (`206`, `Content-Range`, `416`), and `DELETE /v1/artifacts/{id}` deletes it with its bytes. A session's artifacts go with it on a sessions reset, and appear in its history as `artifact.created`, `artifact.version.created` and `artifact.deleted` (new in the event catalog). A subject reaches only the artifacts of their own sessions.
68
+ - **`@nylorun/runtime`: capability links.** `POST /v1/artifacts/{id}/links` mints a short-lived path, `/v1/artifact-links/<token>`, that downloads one version (with Range) with no credential and no `Nylorun-Protocol`: an ES256 JWT (`typ: nylorun-artifact+jwt`) signed with the Tenant's signing key, at most 15 minutes, and dead once the artifact is deleted. The Host logs its path without the token.
69
+ - **Breaking (`@nylorun/core`, `@nylorun/runtime`): message `parts`.** A user message may carry `parts`: `text`, and `file` by `artifactId` (and `version`, else the latest, pinned when the message is accepted). Model-gate reads the file from the Object store: an image goes to the model as image input, a text file as text, and any other file fails the call (`invalid_request`). The record and the transcript hold only the reference. The gateway builds its `BlobStore` from `NYLORUN_OBJECT_STORE_*` (the Tenant's `fs` store without it). `PROTOCOL_VERSION` is 6; `HOST_PROTOCOL` is 4–6 and protocol 6 clients require the feature `artifacts`.
70
+ - **`@nylorun/runtime`: `save_artifact`.** A session with a sandbox gets the `nylorun.artifacts` capability, whose `save_artifact` tool saves a sandbox file (`path`) or text (`content`) as an artifact of the session, carrying the turn and tool call on its event.
71
+ - **Breaking (`@nylorun/runtime`): `MediaStore` is removed.** `MediaStore` and `localMedia` leave `@nylorun/runtime/node`, and `piModel` takes `files` (a resolver from an artifact reference to its bytes) instead of `media`; images are file artifacts. The image checks (`decodeImageBase64`, `validateImageBytes`, `IMAGE_MEDIA_TYPES`, `MAX_IMAGE_BYTES`) stay.
72
+ - **`@nylorun/agents`:** `client.artifacts` (`upload`, `uploadVersion`, `list`, `get`, `download` with a range, `link`, `delete`), also on `BrowserClient`, and `session.inputParts(parts)`. A request with a body of its own type keeps it.
73
+ - `@nylorun/admin`, `@nylorun/cli`, `nylorun`, `@nylorun/studio`: speak protocol 6.
74
+
75
+ ### Minor Changes
76
+
77
+ - e58e6ec: **egress-gate: pod sandboxes reach only the hosts their spec allows (F7.2).** A new `egress` service runs in the gateway's process (`--service gates,keys,egress`) on `NYLORUN_EGRESS_LISTEN_PORT` (default 4200). It is a CONNECT proxy: each tunnel needs an egress token (`Proxy-Authorization`, Basic with the token as password or Bearer) for a live sandbox at its current host epoch, a host name (no IP literal) that the sandbox's `network.allow` names exactly or by `*.suffix`, port 443 or 80, and a name that resolves to a public address; the gate connects to that checked address and pipes bytes, with no TLS interception and no credential injection. Plain HTTP is answered `405`; a sandbox holds at most 64 tunnels; idle tunnels close after 5 minutes; refusals are logged, never emitted as events. Egress tokens (`typ: nylorun-egress+jwt`), minted with each host token at a pod's join, are accepted nowhere else; egress-gate also refuses a token of a sandbox that is deleted, lost or expired, or of a pod other than the one that joined. Sandbox spec resolution lets `pod` sandboxes allow `*.suffix` hosts within the Tenant's ceiling (virtual ones keep exact names). With sandboxes enabled, `nylorun` runs the gateway with `egress` and publishes it on `NYLORUN_SANDBOX_BIND` at `NYLORUN_SANDBOX_EGRESS_PORT`, next to the gates.
78
+ - 186ec3f: **A local Tenant has an Object store: RustFS, behind the Runtime's new `BlobStore` seam.** `nylorun start` adds a `rustfs` container (RustFS 1.0.1, single node and single drive, pinned by digest) on the `nylorun-<tenant>-rustfs` volume, unpublished and without its console. Its secret key is generated once into `docker/.env` (`NYLORUN_OBJECT_STORE_SECRET_KEY`), and only the `runtime` and `gateway` containers receive the credential (`NYLORUN_OBJECT_STORE_ENDPOINT`, `_ACCESS_KEY`, `_SECRET_KEY`); the runtime creates the bucket at boot. The Runtime reaches the store through `BlobStore` (put with a streamed body and a size cap, get with a byte range, head, delete, list by prefix) with an `s3` adapter over the plain S3 API and an `fs` adapter, which a Runtime without `NYLORUN_OBJECT_STORE_ENDPOINT` (embedded, ephemeral, tests) uses under the Tenant directory's `blobs/`. Nothing stores files there yet; file artifacts build on it.
79
+ - 6077272: **Folder artifacts and the turn-end outputs export (F8.2).** Part of protocol 6, with file artifacts.
80
+
81
+ - **`@nylorun/runtime`: the turn-end export.** When an agent's turn completes, the Runtime reads `/workspace/outputs` of the session's sandbox and keeps it as a version of the session's folder artifact `outputs`: the first export creates it, and each later turn whose outputs changed adds a version (`artifact.created` / `artifact.version.created` with `kind: "folder"`, `source: "export"`, `fileCount` and `claimed: true`, since the listing and bytes are what the sandbox supplied). Nothing is exported without a sandbox or without outputs. An export past 10,000 files, 1 GiB, the per-file limit or the Tenant total stores nothing and records `artifact.export.skipped` with its reason; a failure records `artifact.export.failed`. Neither fails the turn. Core reads the workspace through one seam, `WorkspaceReader`, which the Harness API and pod sandboxes will implement later.
82
+ - **`@nylorun/runtime`: folder artifacts.** A folder version is a manifest of paths to content-addressed files: each file's bytes are stored once at `blobs/sha256/<hex>`, so an unchanged file is never stored again, and the Tenant total (`GET /v1/tenant/artifacts` `usedBytes`) counts it once. New routes: `GET /v1/artifacts/{id}/versions/{n|latest}/tree` (the manifest), `…/files/{path}` (one file by its percent-encoded path, with Range), `…/diff?from=n` (files added, removed and changed) and `…/zip` (a streamed zip). `POST /v1/artifacts/{id}/links` takes `file` to link one file of a folder; a folder's link without it opens the zip. A folder's `/content`, a new version uploaded to a folder, and a message part naming a folder are `400`. Deleting a folder removes the files no other version names. Migration `0006_folder_artifacts` allows the `folder` kind and adds the `artifact_content` table.
83
+ - **`@nylorun/core`:** `ArtifactKindSchema` (`file`, `folder`), the `export` source, `FolderEntrySchema`, `FolderManifestSchema`, `ArtifactTreeSchema`, `ArtifactDiffSchema`, the `artifact.export.skipped` and `artifact.export.failed` events, and `fileCount` and `claimed` on `artifact.created` / `artifact.version.created`.
84
+ - **`@nylorun/agents`:** `client.artifacts.tree()`, `file()`, `diff()` and `zip()`, and `link(id, { file })`.
85
+
86
+ - a5f9cf5: **Gate trust (F5): run tokens and two gate credentials.** A model or MCP call through the gates service reaches only its own run, and the gate takes the call's scope from a token instead of believing the request body.
87
+
88
+ - **Run tokens.** Each advance that takes a session's lease mints a run token (`tenant/run-token.ts`): an ES256 JWT of `typ` `nylorun-run+jwt` and `aud` `nylorun-gates`, signed by the keys service with the Tenant's current signing key, naming the session (`sub`), its active turn (`trn`), its root agent (`agt`) and the lease epoch (`epc`), for 15 minutes. The lease heartbeat re-mints it when less than 5 minutes remain, and it is dropped when the advance ends or loses the lease. The tokens are internal: not in `openapi.json`, and the JWKS is unchanged.
89
+ - **Two credentials at the gateway.** Model calls and their cancels accept only a run token. Tool calls, their cancels and the MCP connect, list and close routes accept a run token, or `NYLORUN_GATES_TOKEN` (now core's credential) for requests made outside a run. Keys, `/deliveries` and endpoint pings accept only core's credential.
90
+ - **Scope from the token.** The model-call body no longer carries `sessionId`, `turnId` or `agentId` (a body naming them is refused with `400`), and MCP requests under a run token leave out `server.sessionId`: ledger rows, turn and agent caps, and MCP manifest and vault-grant lookup use the token's claims.
91
+ - **Stale tokens die.** Every call under a run token checks that the session's epoch and active turn are still the token's and that it is not cancelled, and answers `409 run_stale` otherwise. A keyed call joins a call already running at the gate only for the same session at the same or a newer epoch, so the new owner's re-send after a takeover keeps working (P1.2, F4.1) and the old owner's is refused. A cancel stops only its own session's call (`403 gate_forbidden` otherwise).
92
+ - In process nothing changes: embedding, `startEphemeralRuntime` and Runtimes without the gates service mint no token. No protocol bump: the public API, protocol 5, the Action endpoint wire and `docker/.env` are unchanged. See `DEPLOYMENT.md`.
93
+
94
+ - b6bf1f5: **Harness API v1, in process (F6.1).** Every segment now runs in a harness: the advance takes the session's lease and offers the segment as a run, and the Tenant's own harness, in the same process, runs the engine and reports how it ended. Core settles it exactly as before. Nothing changes on the wire: protocol 5, durable checkpoint 1, engine `hosted-3` and the Action endpoint wire are the same.
95
+
96
+ - `@nylorun/core/harness-api`: the protocol (messages, Zod schemas, the effect request hash, transcript edits, an RPC channel with an in-process memory transport).
97
+ - `@nylorun/harness/api`: `createHarness({ channel, executors })`, a harness that leases runs, renews their leases, replays a run's recorded outcomes without asking, keeps transcripts by record cursor, and runs model, MCP and sandbox calls through the executors it is given.
98
+ - `@nylorun/runtime`: the Harness API server per Tenant (`TenantHandle.attachHarness`), the in-process harness, and the journal as the Record seam. A model call's journal row now stores the request's hash without its prompt, so a replay never sends a prompt twice. `NYLORUN_HARNESS_API=0` runs the engine in the advance as before, until F6.2 removes it. A Runtime older than this one may fail a turn that was in flight across a downgrade with drift.
99
+
100
+ - 8ed4ea6: **Harness service over WebSocket, with the workspace capability (F6.2).** A Runtime started with `NYLORUN_HARNESS=remote` runs no harness of its own: it opens the Harness API listener (`NYLORUN_HARNESS_LISTEN_HOST`/`_PORT`, default port 4200, `NYLORUN_HARNESS_ALLOWED_HOSTS`), which accepts only the harness credential (`NYLORUN_HARNESS_TOKEN`) on `/nylorun/harness/v1`. The runtime image's `--service harness` connects to it (`NYLORUN_HARNESS_URL`, `NYLORUN_HARNESS_TOKEN`, `NYLORUN_GATES_URL`, `NYLORUN_HARNESS_ROOT`) and runs the Tenant's segments, MCP servers and sandboxes with no store; it refuses to start with a database, the gates' or keys' credential, or Restate settings, and presents only run tokens at the gates. The in-process harness stays the default.
101
+
102
+ - `@nylorun/core/harness-api`: the `workspace.*` requests core sends to a harness that serves workspaces, `tenantId` in the `hello` answer, a workspace record on `sandbox.state` claims, and `TurnStart.options.holdMs`. Tenant and admin status report the Tenant's harnesses (`harness`).
103
+ - `@nylorun/harness/api`: `createHarness` declares capabilities, reports grants (`onGrant`) and the `hello` answer, readies MCP through `executors.prepare` (`session.mcp`), and holds a run while its Action is pending until core sends the outcome (`effect.resolved`).
104
+ - `@nylorun/runtime`: the WebSocket listener and client, `--service harness`, the workspace capability (`ctx.sandbox` is a `WorkspacePort`; sandbox tool routes, `save_artifact`, sweep and reset reach the harness's workspaces), the SandboxManager's records port, and held runs (`actionHoldMs`, default 5 minutes). `save_artifact` runs in core. `NYLORUN_HARNESS_API` and the engine run in the advance are removed; tests run with `NYLORUN_TEST_HARNESS=memory|json|ws`.
105
+
106
+ - 678e085: **Pod sandboxes (F7.2, second part).** With `nylorun sandbox enable`, a sandbox resource of kind `pod` (`PUT /v1/sandboxes/{id}` with `kind: "pod"`, an `image`, `*.suffix` hosts, `storage` and `lifecycle.ttl`) is an agent-sandbox pod on the Tenant's cluster, created at once. The turns of the sessions attached to it run in the pod: the engine is copied from the Runtime image into the pod (the Runtime image now carries tini for it), waits until the pod's NetworkPolicy is in force, exchanges its join token for a host token at the Harness API listener (`POST /nylorun/harness/v1/host/join`, published for pods on the Docker host's address, with the gates), and serves its sandbox alone. New: `POST /v1/sandboxes/{id}/stop` and `/reset`; the Tenant's `limits.ttl`, `lifecycle.onExpiry`, `lifecycle.stopGrace` and `placement`; idle stop; lifecycle events `sandbox.running`, `.suspended`, `.expired`, `.relaunched`, `.lost`, `.reset` and `.failed`; error codes `placement_refused`, `sandbox_lost` and `sandbox_expired`; `cluster` in `GET /v1/tenant/sandbox`; Host feature `sandbox-pods` (additive, protocol unchanged). Without a cluster, kind `pod` is `409 sandbox_unavailable` (it was 400). Migration `0007_sandbox_pods` adds the pod lifecycle columns to `sandbox_resources`.
107
+ - 926711b: **Sandboxes are a resource (F7.1, blueprint D39; Host feature `sandboxes`).** A sandbox has its own id, a kind, a spec and labels, and outlives the sessions attached to it. Additive: protocol 5 is unchanged.
108
+
109
+ - `PUT /v1/sandboxes/{id}` creates a sandbox or finds the one with that id (get-or-create in one call); `GET /v1/sandboxes/{id}`, `GET /v1/sandboxes?label=key=value` (repeatable), `GET /v1/sandboxes/{id}/events` and `DELETE /v1/sandboxes/{id}`. Ids are `/`-separated segments (`team-a/proj-42`), sent percent-encoded as one path segment. Only kind `virtual` runs; `pod` is refused with `sandbox_unavailable`. The spec is resolved against the Tenant's limits and fixed once the sandbox exists; labels can change.
110
+ - A session attaches with `sandbox: { id }` and shares the sandbox's `/workspace` with every other session attached to it. Turns are serial per sandbox: a second session's turn is refused with `409 sandbox_busy` while another runs. Deleting a session (a sessions reset) only detaches it. Deleting a sandbox is refused while a turn runs in it; afterwards an attached session's next turn is refused with `sandbox_unavailable` until a sandbox with that id exists again.
111
+ - Subject tokens carry an `sbx` claim: `POST /v1/tokens` takes `sandboxes`, exact ids or prefixes ending in `/*` (at most 16). A token reaches only the sandboxes they match, checked when a session attaches and at every turn start (`403 sandbox_not_granted`); any other sandbox is the 404 of a missing one. The new scope `sandboxes:write` lets a role create and delete the sandboxes its grants reach. Application keys reach every sandbox.
112
+ - The Tenant holds at most `limits.sandboxes` sandboxes (`PUT /v1/tenant/sandbox`, default 100); one more is `409 limit_exceeded`.
113
+ - Lifecycle events (`sandbox.created`, `sandbox.attached`, `sandbox.detached`, `sandbox.deleted`) go to the sandbox's own stream in the record, through the record module; the session's log records `sandbox.attached`. The sandbox stream is not relayed to S2.
114
+ - New error codes `sandbox_not_granted`, `sandbox_busy` and `sandbox_unavailable`; the session view gains `sandboxId` and `sandboxSource: "sandbox"`. Migration `0004_sandbox_resources` adds `sandbox_resources`, `nylorun_streams.sandbox_events` and the sessions' `sandbox_id` column.
115
+ - `@nylorun/agents`: `client.sandboxes` with `ensure(id, spec)`, `get`, `list({ labels })`, `delete`, `events`, and `forSession({ session, spec })`, which creates a sandbox for one session, opens the session on it, and deletes it with `release()`. It replaces sharing through another session (`sandbox: { session }` and the view's `sandboxOwnerId`, now deprecated). `client.tokens.create` takes `sandboxes`.
116
+ - `nylorun sandbox ls [--label key=value]... [--json]` and `nylorun sandbox rm <id>` list and delete the running local Tenant's sandboxes.
117
+
118
+ - db045cd: **The sandboxes service and `nylorun sandbox enable` (F7.2, first part).** A new image, `ghcr.io/nylorun/sandboxes`, versioned with the Runtime, drives agent-sandbox v1.0.5 Sandboxes in one namespace per Tenant: `PUT`, `GET ?wait=` and `DELETE /v1/pods/{name}` with an operation id, `/ready` and `/v1/info`, behind a bearer token only the runtime container holds. `nylorun sandbox enable --context <name>` installs into that kubeconfig context only (the pinned controller when absent, the namespace `nylorun-sbx-<tenant>`, a ServiceAccount whose Role covers Sandbox lifecycle and join Secrets, no NetworkPolicy or exec rights), refuses a cluster whose NetworkPolicy it cannot prove enforced, records `<Host root>/sandboxes/cluster.json` and the token, and adds the `sandboxes` service to the Tenant; `nylorun sandbox disable` and `nylorun sandbox status` remove and report it. Sessions do not run in pods yet: that comes with the Harness API and egress-gate.
119
+
120
+ ### Patch Changes
121
+
122
+ - 3aaf640: **Closing a Tenant no longer hangs on work that ignores its abort.** `MemoryExecution.stop()` and `RestateExecution.stop()` now abort running handlers and wait for them only up to a stop grace (`stopGraceMs`, default `DEFAULT_STOP_GRACE_MS`, 30 s), then abandon the ones still running. Before, `stop()` waited without a limit, so `TenantRuntime.close()` hung in its first step (`detach`) before its bounded `idle` step could run. Shutdown semantics are unchanged: work is aborted as a `shutdown`, and an abandoned advance's lease lapses so the next advance takes the session over. A Tenant that owns its execution uses its advance grace period as the stop grace; the Restate Worker logs a warning when it abandons handlers and drops their connections, so Restate retries them.
123
+ - 167d01a: **A run core stops while it asks about a pending Action is no longer held.** When core cancelled a run, or stopped it for a shutdown, while the harness waited for core's answer to an Action's `effect.intent`, the harness then held the run for the whole `holdMs` (5 minutes by default): it listened for an abort that had already happened. The run kept its lease, and closing the Tenant waited for it. The harness now gives such a run back at once.
124
+ - Pin core to the tested release.
125
+ - Pin harness to the tested release.
126
+ - Updated dependencies [b352feb]
127
+ - Updated dependencies [6077272]
128
+ - Updated dependencies [b6bf1f5]
129
+ - Updated dependencies [8ed4ea6]
130
+ - Updated dependencies [167d01a]
131
+ - Updated dependencies [678e085]
132
+ - Updated dependencies [926711b]
133
+ - Updated dependencies
134
+ - @nylorun/core@0.12.0-beta
135
+ - @nylorun/harness@0.22.0-beta
136
+
3
137
  ## 0.15.2-beta
4
138
 
5
139
  ### Patch Changes
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  The independent OSS **Runtime Host** consumes `@nylorun/harness/run` and
4
4
  `@nylorun/core/contracts`. Cloud installs published `@nylorun/harness` from npm
5
5
  and does not import this package. Client authoring and sessions belong to
6
- `@nylorun/agents`; the Host's status and derived keys belong to `@nylorun/admin`.
6
+ `@nylorun/agents`; the Host's status and operator keys belong to `@nylorun/admin`.
7
7
  Vocabulary: [src/CONTEXT.md](./src/CONTEXT.md).
8
8
 
9
9
  Requires Node 24+. Build from the repository root:
@@ -34,9 +34,9 @@ database, in the fixed schemas `nylorun` (its state: the Session Store) and
34
34
  transaction under an advisory lock, records them in `nylorun.__drizzle_migrations`, and
35
35
  refuses a database holding a migration it does not ship (`schema-too-new`); then, on
36
36
  first start, it creates the Tenant there: its id (`NYLORUN_TENANT_ID`, default a
37
- new one), its name (`NYLORUN_TENANT_NAME`, default `default`), its Studio principal and
38
- the derived principals (`NYLORUN_DERIVED_PRINCIPALS`, default `project`) whose keys the
39
- admin key derives. A database written by a Runtime that kept several Tenants in one
37
+ new one), its name (`NYLORUN_TENANT_NAME`, default `default`) and its Studio principal,
38
+ whose key the admin key derives (the only derived key). Operator keys (`/v1/admin/keys`) add, rotate and delete the Tenant's
39
+ application keys after that; a rotated or deleted key stops authenticating at once. A database written by a Runtime that kept several Tenants in one
40
40
  database (`tenant_<id>` schemas), or by a pre-release build of one Tenant per database
41
41
  (`schema_version` tables), is refused: this release starts fresh on a new database. Restate runs one advance of a session at a time and holds the Tenant's sweep
42
42
  timer (Durable Session Execution); every session's events are relayed from the record to
@@ -50,25 +50,54 @@ them. A local Tenant packs `core,loop` into the `runtime` container and
50
50
  api|worker|all` is the deprecated name of `--service core`, `loop` and
51
51
  `core,loop`.
52
52
 
53
+ `harness` runs alone: agent turns, stdio MCP servers and workspaces, apart from
54
+ core. With `NYLORUN_HARNESS=remote`, core runs no turn itself and opens the
55
+ Harness API listener (`NYLORUN_HARNESS_LISTEN_HOST`, `NYLORUN_HARNESS_LISTEN_PORT`,
56
+ default 4200, `NYLORUN_HARNESS_ALLOWED_HOSTS`), which accepts only
57
+ `NYLORUN_HARNESS_TOKEN`; `in-process` (the default outside Compose) runs turns in
58
+ core's process. A `harness` process needs `NYLORUN_HARNESS_URL`,
59
+ `NYLORUN_HARNESS_TOKEN`, `NYLORUN_GATES_URL` and `NYLORUN_HARNESS_ROOT` (default
60
+ `/harness`), answers `/health` on `127.0.0.1:4300`, and refuses to start with a
61
+ database, Restate, keys or gates credential in its environment. A local Tenant
62
+ runs it in the `harness` container, on a network with only the runtime and the
63
+ gateway ([DEPLOYMENT.md](../DEPLOYMENT.md#the-harness-agent-turns-mcp-servers-and-workspaces)).
64
+
53
65
  The container is configured by its environment, which a local Tenant's Compose file
54
66
  sets: `NYLORUN_DATABASE_URL` (required), `NYLORUN_RESTATE_INGRESS_URL`,
55
67
  `NYLORUN_RESTATE_ADMIN_URL`, `NYLORUN_WORKER_URL` and
56
68
  `NYLORUN_RESTATE_IDENTITY_KEY` (Restate), `NYLORUN_S2_ENDPOINT` and
57
69
  `NYLORUN_S2_TOKEN`, and in container mode `NYLORUN_LISTEN_HOST`,
58
70
  `NYLORUN_LISTEN_PORT`, `NYLORUN_ALLOWED_HOSTS` and `NYLORUN_PUBLIC_URL`, plus
59
- `NYLORUN_BROWSER_ACCESS` (`on` or `off`) and the operator listener
71
+ the operator listener
60
72
  (`NYLORUN_ADMIN_LISTEN_PORT`, `NYLORUN_ADMIN_LISTEN_HOST`,
61
- `NYLORUN_ADMIN_ALLOWED_HOSTS`). A process that runs `loop` sends its model
62
- calls to the gate at `NYLORUN_GATES_URL` with `NYLORUN_GATES_TOKEN`; in a
63
- container it refuses to start without them. A `gates` process needs only
73
+ `NYLORUN_ADMIN_ALLOWED_HOSTS`). `NYLORUN_IDENTITY_FILE` names the identity
74
+ file, a YAML list of the trusted issuers whose JWTs the Tenant API accepts
75
+ (Host feature `trusted-issuers`), read once at boot; a malformed file stops the
76
+ boot ([DEPLOYMENT.md](../DEPLOYMENT.md#trusted-issuers)). A process that runs `loop` sends its model
77
+ calls to the gate at `NYLORUN_GATES_URL`; in a container it refuses to start
78
+ without it and `NYLORUN_GATES_TOKEN`. That token is core's credential: the gate
79
+ accepts only it for vault writes, token signing and Action deliveries. Model and
80
+ remote MCP calls carry a run token instead, which the loop mints for each
81
+ session it advances, and the gate takes the call's session, turn and agent from
82
+ it; a token whose turn was cancelled or whose session another process took over
83
+ is refused with `409 run_stale`. A `gates` process needs only
64
84
  `NYLORUN_DATABASE_URL`, `NYLORUN_GATES_TOKEN`, its listener
65
85
  (`NYLORUN_GATES_LISTEN_HOST`, `NYLORUN_GATES_LISTEN_PORT`, default 4100, and
66
86
  `NYLORUN_GATES_ALLOWED_HOSTS`) and the Host's `tenant/` directory, which it
67
- never writes; it serves the database's one Tenant. `NYLORUN_PACKING` (`combined` or `split`) is logged at startup.
87
+ never writes; it serves the database's one Tenant. `NYLORUN_RESOLVER_URL` and
88
+ `NYLORUN_RESOLVER_TOKEN` (both or neither, gates only) name the operator's
89
+ credential resolver, which the gate asks for a person's MCP credential when the
90
+ session's vaults hold none ([DEPLOYMENT.md](../DEPLOYMENT.md#credentials)); an
91
+ embedding or `startEphemeralRuntime` takes `resolver: { url, token }` instead. With `egress`
92
+ (`--service gates,keys,egress`, for pod sandboxes) the same process runs
93
+ egress-gate on `NYLORUN_EGRESS_LISTEN_HOST`:`NYLORUN_EGRESS_LISTEN_PORT`
94
+ (default `0.0.0.0:4200`): a CONNECT proxy that admits a pod's egress token and
95
+ tunnels only to the hosts its sandbox spec allows, on 443 or 80, never to a
96
+ private address. `NYLORUN_PACKING` (`combined` or `split`) is logged at startup.
68
97
 
69
98
  With an operator listener (a local Tenant's default: container port 4001), the Host
70
- serves two ports. The public one serves the Tenant API, to browsers too when
71
- browser access is on, and answers admin routes with the opaque `404`. The
99
+ serves two ports. The public one serves the Tenant API, to browsers too (with a
100
+ trusted issuer's token), and answers admin routes with the opaque `404`. The
72
101
  operator one serves the Admin API, Host shutdown and the Tenant API, never to
73
102
  browsers; keep it on loopback or a private network and never proxy it. Outside
74
103
  a container, `adminPort` in `host.json` does the same on loopback.
@@ -82,6 +111,7 @@ The Host root is `NYLORUN_HOME` or `~/.nylorun` (for a local Tenant,
82
111
  <host root>/
83
112
  host.json # format 1: hostId, host, port, adminPort?, runtimeVersion, …
84
113
  host-credentials.json # adminKey (0600)
114
+ identity.yaml # optional: trusted issuers (NYLORUN_IDENTITY_FILE)
85
115
  docker/ # compose.yaml, .env (0600), Restate identity key
86
116
  tenant/ # vault-kek, plugin-data, logs, home, tmp, sandboxes
87
117
  ```
@@ -95,20 +125,30 @@ The Host root is `NYLORUN_HOME` or `~/.nylorun` (for a local Tenant,
95
125
  | `GET /openapi.json` | none | The Tenant API's OpenAPI 3.2 document (below); refuses an `Origin` |
96
126
  | `GET /v1/admin/status` | admin key | `AdminStatusSchema`: the Host, its protocol and its Tenant (`tenant`: id, name, `open` or `unavailable`, and the cause when it could not be opened); alias `GET /v1/admin/host`. On the operator listener when there is one |
97
127
  | `POST /v1/admin/host/shutdown` | admin key | Host-private; not in `@nylorun/admin` |
128
+ | `PUT /v1/admin/keys/{id}` | admin key | Operator keys (Host feature `operator-keys`): creates key `id` or rotates it, and returns it once (`PutOperatorKeyResponse`); `studio` is refused |
129
+ | `GET /v1/admin/keys` | admin key | Every key of the Tenant: id, role, when issued (`ListOperatorKeysResponse`), never the keys |
130
+ | `DELETE /v1/admin/keys/{id}` | admin key | The key stops authenticating at once; `404` when there is none |
98
131
  | `GET /v1/admin/openapi.json` | admin key | The Admin API's OpenAPI 3.2 document |
99
- | `/v1/*` Tenant routes | application key, subject token or delivery token | Require `Nylorun-Protocol`; nothing names the Tenant |
132
+ | `/v1/*` Tenant routes | application key, trusted issuer's token or delivery token | Require `Nylorun-Protocol`; nothing names the Tenant |
133
+ | `POST /v1/vaults/{vaultId}/oauth/start` | application key | MCP OAuth connect into an installation vault (F9 C2): `{url, server, clientId?}` → `{authorizeUrl, expiresAt}`; the gateway's keys module does discovery, registration and the exchange. `oauth_client_required` without DCR or a `clientId` |
134
+ | `GET /v1/oauth/callback` | the `state` itself | Where the authorization server sends the browser back: no credential, no `Nylorun-Protocol`; finishes the connect once (`oauth_state_invalid` after) and answers a small HTML page. Its base is `NYLORUN_PUBLIC_URL`, else the start request's origin |
135
+ | `GET /v1/artifact-links/{token}` | the link itself | A capability link to one artifact version (protocol 6): no credential, no `Nylorun-Protocol`, Range supported; a folder's link opens its zip, or one of its files |
100
136
 
101
137
  Every route checks `Host` first (`421 host_rejected`) and rejects non-JSON bodies
102
- with `415 unsupported_media_type`. An `Origin` is `403 origin_rejected` on
103
- `/health`, `/ready`, admin routes, and everywhere when browser access is off.
104
- With browser access on (feature `browser-access`: a local Tenant's default, or
105
- `browserAccess` in `host.json`), the Host answers preflights for browser routes
106
- from the route alone, and the Tenant admits an `Origin` only with a publishable
107
- key (`Nylorun-Key`) that lists it, adding CORS headers only then; Tenant keys
108
- and delivery tokens are refused from browsers before they are looked up. Missing or
109
- unsupported protocol → `426` before authentication (the Host serves protocols 4 and 5).
110
- Protocol 5 clients name no Tenant. A protocol 4 client's `Nylorun-Tenant` naming
111
- another Tenant (or malformed), a publishable key of another Tenant, a Tenant that
138
+ with `415 unsupported_media_type`, except an artifact upload (`POST /v1/artifacts`,
139
+ `POST /v1/artifacts/{id}/versions`), whose body is the file in any media type. An `Origin` is `403 origin_rejected` on
140
+ `/health`, `/ready`, admin routes and the operator listener. On Tenant routes a browser
141
+ presents a trusted issuer's token; application keys and delivery tokens are refused
142
+ from browsers before they are looked up. The Runtime sends no CORS headers, and answers
143
+ `OPTIONS` with `204` and `Allow` only: the operator's reverse proxy answers preflights
144
+ ([DEPLOYMENT.md](../DEPLOYMENT.md#calling-the-runtime-from-browsers-and-apps)). Missing or
145
+ unsupported protocol → `426` before authentication (the Host serves protocols 4 to 7;
146
+ protocol 7 removes subject tokens, the access policy, revocations, browser keys and
147
+ derived principals, whose routes answer `404`; protocol 6 adds file and folder artifacts and message `parts`; at each turn's end the
148
+ Runtime exports `/workspace/outputs` of the session's sandbox as a version of its
149
+ `outputs` folder, read at `/v1/artifacts/{id}/versions/{n|latest}/tree`, `/files/{path}`,
150
+ `/diff?from=` and `/zip`). Protocol 5 and later clients name no Tenant. A protocol 4 client's `Nylorun-Tenant` naming
151
+ another Tenant (or malformed), a Tenant that
112
152
  could not be opened and rejected credentials → opaque `404` with identical body. A path or method no route serves is `404 Route not found`
113
153
  once the caller is known.
114
154
 
@@ -129,18 +169,17 @@ tool at one of them; for [Scalar](https://scalar.com), the package file of a ver
129
169
  https://cdn.jsdelivr.net/npm/@nylorun/runtime@<version>/dist/openapi.json
130
170
  ```
131
171
 
132
- Each operation's `security` says which credentials it takes (application key, subject token,
133
- publishable key, delivery token, admin key), and its `x-nylorun-credentials`,
134
- `x-nylorun-scopes` (the subject scopes that reach it) and `x-nylorun-browser` fields say who
135
- may call it. Event streams are `text/event-stream` with an `itemSchema`. `runtime/openapi/` holds the committed snapshots: a change to a route changes
172
+ Each operation's `security` says which credentials it takes (application key, trusted
173
+ issuer's token, delivery token, admin key), and its `x-nylorun-credentials` and
174
+ `x-nylorun-scopes` (the subject scopes that reach it) fields say who may call it. Event streams are `text/event-stream` with an `itemSchema`. `runtime/openapi/` holds the committed snapshots: a change to a route changes
136
175
  them (`node scripts/build-openapi.mjs --write`), and `check-package` fails until they are
137
176
  updated.
138
177
 
139
178
  ## HTTP layer
140
179
 
141
180
  Routes are Hono routes (`@hono/zod-openapi`), each declared once with who may call it
142
- (`api/http/define.ts`): that declaration serves the route, checks subject scopes, answers
143
- browser preflights and makes the OpenAPI document. `host/` holds the listeners, the `Host`
181
+ (`api/http/define.ts`): that declaration serves the route, checks subject scopes and makes
182
+ the OpenAPI document. `host/` holds the listeners, the `Host`
144
183
  check (in Node, before Hono) and the Host pipeline (`host/app.ts`); `api/` the Tenant and
145
184
  Admin routes (`api/http/routes/`, `api/ag-ui/`, `api/a2a/`, `host/admin-api.ts`). Only `host/`
146
185
  and `api/` import Hono.
@@ -212,7 +251,7 @@ the Project link (outside a project it runs the Tenant `default`); the project's
212
251
  | `schema-too-new` | Run a Runtime at least as new as the one that migrated the database |
213
252
  | `database-layout-old` | The database holds `tenant_<id>` schemas of an older Runtime, or the `schema_version` tables of a pre-release one: point the Runtime at a new database (locally, a new Tenant: `nylorun start --tenant <new name>`); the old one is left as it is |
214
253
  | `426 protocol_unsupported` | Upgrade clients or Host to a compatible set |
215
- | `421 host_rejected` / `403 origin_rejected` | In a container, list the `Host` in `NYLORUN_ALLOWED_HOSTS`. From a browser, use a subject token and a publishable key that lists the page's origin, never a Tenant key |
254
+ | `421 host_rejected` / `403 origin_rejected` | In a container, list the `Host` in `NYLORUN_ALLOWED_HOSTS`. From a browser, send a trusted issuer's token, never a Tenant key |
216
255
  | `503` for a Tenant | Postgres or Restate is unreachable; `GET /ready` names which |
217
256
  | Port in use | Change `NYLORUN_PORT` in `~/.nylorun/tenants/<name>/docker/.env` and run `nylorun start` |
218
257
  | Logs | `nylorun logs runtime` |
@@ -1,4 +1,4 @@
1
- import { type DurableExecution, type StuckInvocation, type Wake, type WorkerHandlers } from "../../execution/types.js";
1
+ import { type DurableExecution, type SandboxSignal, type StuckInvocation, type Wake, type WorkerHandlers } from "../../execution/types.js";
2
2
  export interface RestateTimeouts {
3
3
  /**
4
4
  * How long an invocation may run without journal progress before Restate
@@ -66,6 +66,12 @@ export interface RestateExecutionOptions {
66
66
  forceRegistration?: boolean;
67
67
  /** How long `start` keeps retrying registration while Restate comes up. Default 60000. */
68
68
  registrationTimeoutMs?: number;
69
+ /**
70
+ * How long `stop` waits for running handlers after aborting them; the ones still running
71
+ * then are abandoned and their attempts end with the dropped connections, so Restate
72
+ * retries them. Default `DEFAULT_STOP_GRACE_MS`.
73
+ */
74
+ stopGraceMs?: number;
69
75
  /** Replaces the SDK's console logging. */
70
76
  logger?: (level: string, message: string) => void;
71
77
  }
@@ -91,9 +97,11 @@ export declare class RestateExecution implements DurableExecution {
91
97
  tenant: string;
92
98
  timer: string;
93
99
  action: string;
100
+ sandbox: string;
94
101
  };
95
102
  wake(tenantId: string, sessionId: string, wake: Wake): Promise<void>;
96
103
  deliver(tenantId: string, actionId: string): Promise<void>;
104
+ sandbox(tenantId: string, sandboxId: string, signal: SandboxSignal): Promise<void>;
97
105
  timer(tenantId: string, key: string, at: Date): Promise<void>;
98
106
  armSweep(tenantId: string): Promise<void>;
99
107
  disarmSweep(tenantId: string): Promise<void>;
@@ -22,13 +22,19 @@
22
22
  * no state and makes no `ctx.run`: its only journal entry is the delayed
23
23
  * self-send after a `retry` result, so an endpoint that is down never spends
24
24
  * the retry budget, which is for infrastructure errors.
25
+ * - `<prefix>NylorunSandbox`, key `<tenantId>:<sandboxId>` (F7.2). Its
26
+ * exclusive handlers serialize a pod sandbox's reconciles and timers:
27
+ * `reconcile` calls `WorkerHandlers.sandbox`; `arm` records a timer's latest
28
+ * time (`idle`, `ttl`, or the reconcile's own `retry`) and sends a delayed
29
+ * `fire`; a `fire` for an older time is a no-op, as the timer object's.
30
+ * What a run answers arms its timers and its retry.
25
31
  *
26
32
  * Wakes, deliveries, timers and sweep arming go through the ingress as one-way
27
33
  * sends, so an API node can call them without ever calling `start`.
28
34
  */
29
35
  import { createServer } from "node:http2";
30
36
  import * as restate from "@restatedev/restate-sdk";
31
- import { WAKE_REASONS, parseSessionKey, sessionKey, } from "../../execution/types.js";
37
+ import { DEFAULT_STOP_GRACE_MS, WAKE_REASONS, parseSessionKey, sessionKey, settleWithin, } from "../../execution/types.js";
32
38
  const HOUR_MS = 60 * 60 * 1000;
33
39
  export function createRestateExecution(options) {
34
40
  return new RestateExecution(options);
@@ -55,6 +61,7 @@ export class RestateExecution {
55
61
  tenant: `${prefix}NylorunTenant`,
56
62
  timer: `${prefix}NylorunTimer`,
57
63
  action: `${prefix}NylorunAction`,
64
+ sandbox: `${prefix}NylorunSandbox`,
58
65
  };
59
66
  this.ingressUrl = options.ingressUrl.replace(/\/+$/, "");
60
67
  this.adminUrl = options.adminUrl.replace(/\/+$/, "");
@@ -72,6 +79,16 @@ export class RestateExecution {
72
79
  async deliver(tenantId, actionId) {
73
80
  await this.send(this.names.action, sessionKey(tenantId, actionId), "deliver", {});
74
81
  }
82
+ async sandbox(tenantId, sandboxId, signal) {
83
+ const key = sessionKey(tenantId, sandboxId);
84
+ if (signal.kind === "reconcile")
85
+ await this.send(this.names.sandbox, key, "reconcile", {});
86
+ else
87
+ await this.send(this.names.sandbox, key, "arm", {
88
+ timer: signal.timer,
89
+ at: signal.at,
90
+ });
91
+ }
75
92
  async timer(tenantId, key, at) {
76
93
  await this.send(this.names.timer, sessionKey(tenantId, key), "set", {
77
94
  at: at.getTime(),
@@ -143,8 +160,9 @@ export class RestateExecution {
143
160
  this.stopping = true;
144
161
  for (const controller of this.controllers)
145
162
  controller.abort(new Error("Worker stopping"));
146
- while (this.inflight.size > 0)
147
- await Promise.allSettled(this.inflight);
163
+ const abandoned = await settleWithin(this.inflight, this.options.stopGraceMs ?? DEFAULT_STOP_GRACE_MS);
164
+ if (abandoned > 0)
165
+ this.options.logger?.("warn", `Worker stopped with ${abandoned} handler(s) still running; abandoned`);
148
166
  this.handlers = undefined;
149
167
  const server = this.server;
150
168
  this.server = undefined;
@@ -259,7 +277,48 @@ export class RestateExecution {
259
277
  },
260
278
  options: serviceOptions,
261
279
  });
262
- return [session, tenant, timer, action];
280
+ /** Sets a sandbox timer: the latest time wins; `fire` checks it. */
281
+ const armSandbox = async (ctx, input) => {
282
+ ctx.set(`at:${input.timer}`, input.at);
283
+ const now = await ctx.date.now();
284
+ ctx
285
+ .objectSendClient({ name: names.sandbox }, ctx.key)
286
+ .fire(input, restate.rpc.sendOpts({ delay: Math.max(0, input.at - now) }));
287
+ };
288
+ const runSandbox = async (ctx, trigger) => {
289
+ const { tenantId, sessionId: sandboxId } = parseSessionKey(ctx.key);
290
+ const result = await this.runAborted(ctx.request().attemptCompletedSignal, (handlers, signal) => {
291
+ if (!handlers.sandbox)
292
+ throw new Error("WorkerHandlers.sandbox is required for sandbox reconciles");
293
+ return handlers.sandbox(tenantId, sandboxId, trigger, signal);
294
+ });
295
+ for (const item of result.arm ?? [])
296
+ await armSandbox(ctx, item);
297
+ if (result.retryAfterMs !== undefined)
298
+ await armSandbox(ctx, {
299
+ timer: "retry",
300
+ at: (await ctx.date.now()) + Math.max(0, result.retryAfterMs),
301
+ });
302
+ };
303
+ const sandbox = restate.object({
304
+ name: names.sandbox,
305
+ handlers: {
306
+ reconcile: async (ctx) => {
307
+ await runSandbox(ctx, "reconcile");
308
+ },
309
+ arm: async (ctx, input) => {
310
+ await armSandbox(ctx, input);
311
+ },
312
+ fire: async (ctx, input) => {
313
+ if ((await ctx.get(`at:${input.timer}`)) !== input.at)
314
+ return;
315
+ ctx.clear(`at:${input.timer}`);
316
+ await runSandbox(ctx, input.timer === "retry" ? "reconcile" : input.timer);
317
+ },
318
+ },
319
+ options: serviceOptions,
320
+ });
321
+ return [session, tenant, timer, action, sandbox];
263
322
  }
264
323
  /**
265
324
  * Runs a Worker handler with a signal aborted by `stop` or by the end of the Restate
@@ -365,7 +424,7 @@ export async function listStuckInvocations(options) {
365
424
  const prefix = options.servicePrefix ?? "";
366
425
  if (!/^[A-Za-z0-9_]*$/.test(prefix))
367
426
  throw new Error(`Invalid Restate service prefix: ${prefix}`);
368
- const services = ["NylorunSession", "NylorunTenant", "NylorunTimer", "NylorunAction"].map((name) => `'${prefix}${name}'`);
427
+ const services = ["NylorunSession", "NylorunTenant", "NylorunTimer", "NylorunAction", "NylorunSandbox"].map((name) => `'${prefix}${name}'`);
369
428
  const clauses = [
370
429
  `target_service_name IN (${services.join(", ")})`,
371
430
  `status IN ('paused', 'backing-off')`,
@@ -0,0 +1,10 @@
1
+ import type { SandboxBackend } from "../../sandbox/types.js";
2
+ export interface LocalBackendOptions {
3
+ /** Default `/workspace`. */
4
+ readonly workspace?: string;
5
+ /** The proxy variables of the moment (the egress token rotates); default none. */
6
+ readonly proxyEnv?: () => Readonly<Record<string, string>>;
7
+ /** The base environment of commands (the engine's allowlisted one); `NYLORUN_*` is dropped. */
8
+ readonly env: Readonly<Record<string, string | undefined>>;
9
+ }
10
+ export declare function localBackend(options: LocalBackendOptions): SandboxBackend;