@nylorun/runtime 0.23.0-beta → 0.24.0-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (252) hide show
  1. package/CHANGELOG.md +148 -0
  2. package/README.md +79 -28
  3. package/dist/adapters/execution/restate.js +1 -1
  4. package/dist/adapters/replication/pgoutput.d.ts +2 -0
  5. package/dist/adapters/replication/pgoutput.js +6 -3
  6. package/dist/adapters/sandbox/local.d.ts +9 -1
  7. package/dist/adapters/sandbox/local.js +16 -4
  8. package/dist/adapters/sandbox/virtual.js +4 -1
  9. package/dist/api/a2a/endpoint.js +5 -1
  10. package/dist/api/a2a/routes.d.ts +2 -0
  11. package/dist/api/a2a/routes.js +2 -2
  12. package/dist/api/a2a/tasks.js +2 -1
  13. package/dist/api/ag-ui/endpoint.js +4 -2
  14. package/dist/api/ag-ui/history.js +4 -1
  15. package/dist/api/ag-ui/routes.d.ts +4 -1
  16. package/dist/api/ag-ui/routes.js +11 -10
  17. package/dist/api/ag-ui/translate.js +3 -2
  18. package/dist/api/components.d.ts +4953 -1204
  19. package/dist/api/components.js +25 -16
  20. package/dist/api/gate/keys.js +7 -1
  21. package/dist/api/gate/routes.d.ts +6 -0
  22. package/dist/api/gate/routes.js +39 -3
  23. package/dist/api/http/app.js +4 -4
  24. package/dist/api/http/define.d.ts +6 -0
  25. package/dist/api/http/define.js +13 -1
  26. package/dist/api/http/respond.js +1 -1
  27. package/dist/api/http/routes/artifacts.js +35 -8
  28. package/dist/api/http/routes/coverage.d.ts +9 -0
  29. package/dist/api/http/routes/coverage.js +34 -0
  30. package/dist/api/http/routes/environments.d.ts +11 -0
  31. package/dist/api/http/routes/environments.js +283 -0
  32. package/dist/api/http/routes/mcp.d.ts +1 -1
  33. package/dist/api/http/routes/mcp.js +2 -2
  34. package/dist/api/http/routes/me.js +1 -1
  35. package/dist/api/http/routes/reads.d.ts +45 -9
  36. package/dist/api/http/routes/reads.js +54 -5
  37. package/dist/api/http/routes/sessions.js +11 -6
  38. package/dist/api/http/routes/tenant.js +32 -10
  39. package/dist/api/openapi.js +31 -26
  40. package/dist/artifacts/disk.d.ts +59 -0
  41. package/dist/artifacts/disk.js +563 -0
  42. package/dist/artifacts/export.d.ts +1 -1
  43. package/dist/artifacts/export.js +2 -3
  44. package/dist/artifacts/folders.d.ts +6 -3
  45. package/dist/artifacts/folders.js +17 -12
  46. package/dist/artifacts/service.d.ts +2 -1
  47. package/dist/artifacts/service.js +6 -1
  48. package/dist/artifacts/workspace.d.ts +1 -1
  49. package/dist/artifacts/workspace.js +1 -1
  50. package/dist/core/flow-host.d.ts +2 -3
  51. package/dist/core/flow-host.js +3 -0
  52. package/dist/core/provider.d.ts +3 -2
  53. package/dist/core/provider.js +5 -37
  54. package/dist/core/sandbox-routes.d.ts +2 -21
  55. package/dist/core/sandbox-routes.js +10 -43
  56. package/dist/execution/types.d.ts +1 -1
  57. package/dist/gates/contract.d.ts +3 -24
  58. package/dist/gates/contract.js +1 -36
  59. package/dist/gates/egress-credentials.d.ts +9 -0
  60. package/dist/gates/egress-credentials.js +11 -4
  61. package/dist/gates/egress.d.ts +28 -1
  62. package/dist/gates/egress.js +115 -10
  63. package/dist/gates/fixture-model.d.ts +28 -0
  64. package/dist/gates/fixture-model.js +92 -0
  65. package/dist/gates/handler.js +4 -4
  66. package/dist/gates/http-tool.d.ts +6 -2
  67. package/dist/gates/http-tool.js +3 -5
  68. package/dist/gates/in-process.d.ts +5 -0
  69. package/dist/gates/in-process.js +12 -1
  70. package/dist/gates/journal.d.ts +30 -0
  71. package/dist/gates/journal.js +52 -0
  72. package/dist/gates/mcp-handler.d.ts +4 -0
  73. package/dist/gates/mcp-handler.js +5 -1
  74. package/dist/gates/meter.d.ts +10 -2
  75. package/dist/gates/meter.js +20 -5
  76. package/dist/gates/policy.d.ts +49 -0
  77. package/dist/gates/policy.js +134 -0
  78. package/dist/gates/tenant-vaults.d.ts +7 -7
  79. package/dist/gates/tenant-vaults.js +5 -5
  80. package/dist/gates/tool-calls.d.ts +10 -5
  81. package/dist/gates/tool-calls.js +28 -10
  82. package/dist/gates/tool-client.d.ts +8 -0
  83. package/dist/gates/tool-client.js +27 -11
  84. package/dist/gates/tool-contract.d.ts +10 -68
  85. package/dist/gates/tool-contract.js +1 -85
  86. package/dist/gates/tool-gate.d.ts +1 -1
  87. package/dist/harness/calls.d.ts +12 -5
  88. package/dist/harness/calls.js +21 -12
  89. package/dist/harness/executors.js +12 -9
  90. package/dist/harness/main.js +18 -5
  91. package/dist/harness/pod.d.ts +34 -0
  92. package/dist/harness/pod.js +41 -0
  93. package/dist/harness/service.d.ts +0 -1
  94. package/dist/harness/service.js +13 -5
  95. package/dist/harness/setup.d.ts +27 -0
  96. package/dist/harness/setup.js +150 -0
  97. package/dist/harness/ws-client.js +3 -2
  98. package/dist/harness/ws-port.d.ts +1 -5
  99. package/dist/harness/ws-port.js +1 -5
  100. package/dist/harness-api/in-process.js +3 -3
  101. package/dist/harness-api/record.d.ts +15 -1
  102. package/dist/harness-api/record.js +174 -44
  103. package/dist/harness-api/redact.d.ts +3 -0
  104. package/dist/harness-api/redact.js +45 -0
  105. package/dist/harness-api/server.d.ts +8 -0
  106. package/dist/harness-api/server.js +93 -13
  107. package/dist/harness-api/start.d.ts +10 -3
  108. package/dist/harness-api/start.js +40 -10
  109. package/dist/harness-api/tool-journal.d.ts +96 -0
  110. package/dist/harness-api/tool-journal.js +102 -0
  111. package/dist/harness-api/tool-status.d.ts +13 -0
  112. package/dist/harness-api/tool-status.js +26 -0
  113. package/dist/harness-api/workspace.d.ts +6 -3
  114. package/dist/harness-api/workspace.js +27 -11
  115. package/dist/harness-api/ws-server.d.ts +11 -3
  116. package/dist/harness-api/ws-server.js +94 -15
  117. package/dist/host/gates.d.ts +2 -1
  118. package/dist/host/gates.js +10 -2
  119. package/dist/host/main.js +54 -5
  120. package/dist/host/operate.js +67 -5
  121. package/dist/host/stack-config.d.ts +64 -0
  122. package/dist/host/stack-config.js +61 -1
  123. package/dist/keys/client.js +4 -1
  124. package/dist/keys/keys.d.ts +5 -5
  125. package/dist/keys/keys.js +5 -7
  126. package/dist/management-openapi.json +999 -2211
  127. package/dist/mcp/connect.d.ts +25 -20
  128. package/dist/mcp/connect.js +29 -30
  129. package/dist/mcp/pool.d.ts +2 -2
  130. package/dist/mcp/pool.js +1 -1
  131. package/dist/mcp/preview.d.ts +3 -3
  132. package/dist/mcp/preview.js +10 -20
  133. package/dist/mcp/snapshot.d.ts +5 -11
  134. package/dist/mcp/snapshot.js +6 -7
  135. package/dist/model/pi-model.js +8 -0
  136. package/dist/openapi.json +6955 -2526
  137. package/dist/reads/types.d.ts +16 -3
  138. package/dist/record/append.d.ts +3 -1
  139. package/dist/record/append.js +1 -0
  140. package/dist/record/envelope.d.ts +5 -1
  141. package/dist/record/envelope.js +7 -3
  142. package/dist/record/environment.d.ts +25 -0
  143. package/dist/record/environment.js +44 -0
  144. package/dist/record/index.d.ts +1 -1
  145. package/dist/record/index.js +1 -1
  146. package/dist/sandbox/disk-fs.d.ts +95 -0
  147. package/dist/sandbox/disk-fs.js +206 -0
  148. package/dist/sandbox/join.d.ts +17 -18
  149. package/dist/sandbox/join.js +42 -15
  150. package/dist/sandbox/manager.d.ts +16 -8
  151. package/dist/sandbox/manager.js +33 -17
  152. package/dist/sandbox/placement.d.ts +13 -12
  153. package/dist/sandbox/placement.js +29 -12
  154. package/dist/sandbox/pods/client.d.ts +6 -1
  155. package/dist/sandbox/pods/lifecycle.d.ts +28 -27
  156. package/dist/sandbox/pods/lifecycle.js +14 -26
  157. package/dist/sandbox/pods/reconcile.js +18 -12
  158. package/dist/sandbox/pods/spec.d.ts +15 -13
  159. package/dist/sandbox/pods/spec.js +16 -20
  160. package/dist/sandbox/records.d.ts +2 -2
  161. package/dist/sandbox/records.js +3 -3
  162. package/dist/sandbox/resolve.d.ts +35 -27
  163. package/dist/sandbox/resolve.js +66 -41
  164. package/dist/sandbox/session-sandbox.js +1 -1
  165. package/dist/sandbox/setup.d.ts +61 -0
  166. package/dist/sandbox/setup.js +123 -0
  167. package/dist/sandbox/share.d.ts +11 -21
  168. package/dist/sandbox/share.js +12 -41
  169. package/dist/sandbox/tenant-config.d.ts +12 -14
  170. package/dist/sandbox/tenant-config.js +12 -17
  171. package/dist/sandbox/types.d.ts +6 -0
  172. package/dist/store/postgres/drizzle/0020_environments.sql +63 -0
  173. package/dist/store/postgres/drizzle/0021_disk_versions.sql +7 -0
  174. package/dist/store/postgres/drizzle/0022_model_usage_timing.sql +5 -0
  175. package/dist/store/postgres/drizzle/0023_artifact_pages.sql +1 -0
  176. package/dist/store/postgres/drizzle/meta/_journal.json +28 -0
  177. package/dist/store/postgres/reads.js +117 -17
  178. package/dist/store/postgres/record-writer.d.ts +4 -4
  179. package/dist/store/postgres/record-writer.js +9 -9
  180. package/dist/store/postgres/record.d.ts +7 -0
  181. package/dist/store/postgres/record.js +23 -2
  182. package/dist/store/postgres/schema.d.ts +934 -761
  183. package/dist/store/postgres/schema.js +102 -73
  184. package/dist/store/postgres/store.js +119 -66
  185. package/dist/store/types.d.ts +109 -48
  186. package/dist/tenant/advance.d.ts +7 -6
  187. package/dist/tenant/advance.js +115 -42
  188. package/dist/tenant/artifact-tool.js +3 -4
  189. package/dist/tenant/commands.d.ts +15 -2
  190. package/dist/tenant/commands.js +93 -12
  191. package/dist/tenant/context.d.ts +48 -22
  192. package/dist/tenant/context.js +4 -15
  193. package/dist/tenant/effects.d.ts +1 -1
  194. package/dist/tenant/effects.js +37 -20
  195. package/dist/tenant/environments.d.ts +167 -0
  196. package/dist/tenant/environments.js +823 -0
  197. package/dist/tenant/harness-tokens.d.ts +21 -0
  198. package/dist/tenant/harness-tokens.js +61 -0
  199. package/dist/tenant/host-token.d.ts +1 -1
  200. package/dist/tenant/host-token.js +2 -4
  201. package/dist/tenant/identity-file.d.ts +4 -4
  202. package/dist/tenant/identity-file.js +17 -8
  203. package/dist/tenant/issuers.js +3 -3
  204. package/dist/tenant/principals.js +1 -1
  205. package/dist/tenant/reset.js +3 -3
  206. package/dist/tenant/runtime.d.ts +6 -1
  207. package/dist/tenant/runtime.js +7 -1
  208. package/dist/tenant/session.d.ts +3 -3
  209. package/dist/tenant/session.js +0 -14
  210. package/dist/tenant/sessions.d.ts +13 -13
  211. package/dist/tenant/sessions.js +61 -138
  212. package/dist/tenant/signing-keys.d.ts +5 -1
  213. package/dist/tenant/status.js +3 -3
  214. package/dist/tenant/sweep.js +12 -6
  215. package/dist/tenant/transcript.d.ts +210 -20
  216. package/dist/tenant/transcript.js +62 -11
  217. package/dist/tenant/turn-timing.d.ts +43 -0
  218. package/dist/tenant/turn-timing.js +39 -0
  219. package/dist/tenant/types.d.ts +2 -0
  220. package/dist/traces/ids.d.ts +42 -0
  221. package/dist/traces/ids.js +85 -0
  222. package/dist/traces/key.d.ts +6 -0
  223. package/dist/traces/key.js +37 -0
  224. package/dist/traces/map.d.ts +113 -0
  225. package/dist/traces/map.js +317 -0
  226. package/dist/traces/options.d.ts +7 -0
  227. package/dist/traces/options.js +1 -0
  228. package/dist/traces/otlp.d.ts +42 -0
  229. package/dist/traces/otlp.js +84 -0
  230. package/dist/traces/projector.d.ts +58 -0
  231. package/dist/traces/projector.js +200 -0
  232. package/dist/traces/read.d.ts +23 -0
  233. package/dist/traces/read.js +16 -0
  234. package/dist/vault/coverage.d.ts +26 -0
  235. package/dist/vault/coverage.js +64 -0
  236. package/dist/vault/error.d.ts +11 -1
  237. package/dist/vault/error.js +5 -1
  238. package/dist/vault/service.d.ts +47 -48
  239. package/dist/vault/service.js +321 -382
  240. package/dist/vault/sources.d.ts +5 -16
  241. package/dist/vault/sources.js +6 -8
  242. package/dist/version.d.ts +1 -1
  243. package/dist/version.js +2 -2
  244. package/package.json +13 -13
  245. package/dist/api/http/routes/sandboxes.d.ts +0 -9
  246. package/dist/api/http/routes/sandboxes.js +0 -153
  247. package/dist/api/http/routes/vaults.d.ts +0 -10
  248. package/dist/api/http/routes/vaults.js +0 -139
  249. package/dist/record/sandbox.d.ts +0 -25
  250. package/dist/record/sandbox.js +0 -43
  251. package/dist/tenant/sandboxes.d.ts +0 -123
  252. package/dist/tenant/sandboxes.js +0 -566
package/CHANGELOG.md CHANGED
@@ -1,5 +1,153 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.24.0-beta
4
+
5
+ ### Minor Changes
6
+
7
+ - e917bd6: **Bring your own harness: harness ids, harness-owned state, and who wrote each event (R4.1, R4.2, R4.4, R4.9).**
8
+
9
+ - **Harness ids and routing.** An Environment's `harness` takes any harness id (`^[a-z][a-z0-9-]{1,62}$`). It is no longer only `nylorun`.
10
+ - **The pin.** A session pins its harness when it opens: its Environment's harness, else `nylorun`. A flow always runs `nylorun`. `GET /v1/sessions/{id}` returns the pin as `harness`.
11
+ - **Routing.** Only a connection whose `hello.name` is that id takes the session's runs.
12
+ - **Our harness's name.** It now says `nylorun` in `hello`. A harness that says `@nylorun/harness` is refused with a message naming the new id, so an older harness image needs this Runtime's.
13
+ - **Placement.** It is keyed by the real harness id, and `"*": {hosts: []}` refuses any harness the map does not name.
14
+ - **Drift.** A turn on an Environment that now runs another harness is `409 harness_mismatch`; open a new session. For a pod Environment, a changed `harness` applies at the pod's next wake, like its image.
15
+ - **Per-harness tokens.** `nylorun-operate harness token put <id>` makes a custom harness container's own Harness API credential, bound to its id; `list` and `rm` manage them. `NYLORUN_HARNESS_TOKEN` stays ours, bound to `nylorun`.
16
+ - **A custom harness as a pod's image.** A pod Environment whose harness is not ours runs its `image` as the harness: the image's own entrypoint (`entrypoint: "image"` to the sandboxes service), with nothing copied in. It needs `image`.
17
+ - **Harness-owned state.** Core stores a harness's turn state as sent and never reads it. Our engine now aligns its state with the turn's manifest itself.
18
+ - **Limits for a custom harness.** Its state is at most 256 KiB, or its turn fails. It cannot pause for approvals in Harness API 2.
19
+ - **Native tools.** It may declare the tools it runs itself in `hello` (`capabilities.tools`). Their calls (capability `harness`) are journaled like built-ins, and the log records a real `tool.completed` instead of `tool.unavailable`.
20
+ - **Who wrote each event.** The writer sets an event's `source` and `evidence`; the event's type no longer decides them. Events written before this release still say `loop` or `api`.
21
+ - **`client`**: a command, with a new `principal` taken from its credential.
22
+ - **`harness`**: what a harness reported, with its harness id and `runId`. Evidence `claimed`.
23
+ - **`core`**: core's own logic. Evidence `observed`.
24
+ - **`effectId` on model calls.** `GET /v1/sessions/{id}/calls/model` returns each call's `effectId`, so a metered call joins the effect journal.
25
+ - **The published contract grows.** It adds `HarnessCheckpointSchema` (the checkpoint fields a harness reads), `NativeToolSchema`, the harness id helpers, `HARNESS_TOOLS_CAPABILITY`, `MAX_HARNESS_STATE_BYTES`, and the gate error codes `key_required` and `effect_not_journaled`. It also documents the effect context keys, the model request hash rule and how a turn's status maps to its output method.
26
+
27
+ - 5165451: **A pod's disk can start prepared, and every Environment's disk has versions (R2d steps 2 and 3).** Before, a new pod volume was empty and a disk could only be kept or reset. Now:
28
+
29
+ - **Setup and input files** (pods, Host feature `environment-setup`): `PUT /v1/environments/{id}` takes `files` (versions of Tenant-wide artifacts, copied in: a file at its `path`, a folder under it) and `setup` (commands run in order with `/bin/sh -c`, `cwd` default `/workspace`, `timeout` default 10m and at most 30m, at most 16 of each). They run once per new disk, after the pod's engine joins and before its first turn, with the Environment's variables, secrets (as the sentinel) and egress. A failed command fails the pod (`observed: failed`, reason `setup: command N …`, an `environment.failed` event with the exit code and the end of the output) and the turn waiting for it; the next turn start runs the setup again. A virtual Environment refuses both with a `400`.
30
+ - **Disk versions** (Host feature `environment-disk-versions`): `POST /v1/environments/{id}/snapshot` keeps a version of the disk between turns (`/workspace`, and a pod's `/harness` but the engine's own files) as a version of a folder artifact the Environment owns, with modes, modification times, links and empty directories (folder manifest `nylorun.folder.v2`); each file's bytes are stored once, and an unchanged disk makes no version. `POST .../restore` puts a version back: a virtual Environment at once, a pod on a new volume seeded before its next turn (`disk.operation` in the view meanwhile; turns wait). `PUT` with `from: { environment, version }` creates an Environment from another's version. `GET .../versions` and `DELETE .../versions/{n}` list and delete them; their trees, files, diffs and zips read through `/v1/artifacts`. A lost pod volume with a version is recovered from the latest one instead of refusing turns until a reset. Snapshot and restore during a turn are `409 environment_busy`. `EnvironmentView.disk` gives the latest version and the copy in progress. Migration `0021_disk_versions`.
31
+ - `@nylorun/agents`: `client.environments.snapshot`, `restore`, `versions` and `deleteVersion`; `put` takes `setup`, `files` and `from`.
32
+
33
+ - 35e29b7: **Environments replace sandboxes and vaults (protocol 11). This is a fresh start: upgrading deletes your sandboxes, their disks and every vault except the model's, and closes every open session (its history stays readable).** List what you have before upgrading; see "Environments replace sandboxes and vaults" in MIGRATION.md.
34
+
35
+ An Environment is where sessions run: one sandbox (kind `virtual`, `pod` or `none`) with a recipe that can change, and one vault of secrets for its shell, MCP servers and HTTP tools. The app decides who uses which Environment; the Runtime keeps it by id and holds its lifecycle.
36
+
37
+ - `@nylorun/core`: protocol 11 (the Host accepts 11 only; older clients get `426`), required feature `environments`. `PutEnvironmentRequestSchema`, `EnvironmentViewSchema` (`recipe`, `version`, `appliedVersion`, `ownerUserId`, `lastUsedAt`), `PutEnvironmentSecretRequestSchema`, `EnvironmentSecretViewSchema`, `ENVIRONMENT_EVENT_TYPES`, `TenantEnvironmentsConfigSchema`, `environmentGranted`. `PutSessionRequest.environment` replaces `sandbox`, `vaultIds` and `credentialSelections`; `SessionView.environment` replaces the session's sandbox and vault fields. Error codes `environment_busy`, `environment_unavailable`, `environment_lost`, `environment_not_granted`, `environment_owner_mismatch` and `session_closed` (the `sandbox_*` codes and `sandbox_expired` are gone); session events `environment.state`, `environment.exec` and `environment.attached`; scope `environments:write`. Credential coverage takes `environmentId` and answers `covered` or `missing`; the MCP preview takes `environmentId`. The vault Management schemas are removed.
38
+ - `@nylorun/runtime`: `/v1/environments/{id}` replaces `/v1/sandboxes/{id}`: `PUT` creates, finds or changes one (`kind` and `storage` fixed; a recipe change bumps `version`; a pod gets a new image or resources at its next start from stopped, a new allowlist within seconds), with list by label, events (`nylorun.environment-event/1`), stop, reset and delete. Each Environment owns one vault: `PUT|GET|DELETE /v1/environments/{id}/secrets/{name}` on the Runtime API, write-only, with a second credential for a host or URL refused at the write (`409 credential_conflict`). An optional owner guard (`ownerUserId`) is checked when a session opens and at every turn start. `lifecycle.deleteAfter` deletes an Environment unused that long; TTL and `onExpiry` are gone. Kind `none` holds secrets for MCP-only agents. `/v1/tenant/vaults`, user and installation vaults, the Tenant's default sandbox, inline sandboxes and `{ session }` sharing are removed. The Tenant setting is `environments.config` (`limits.environments`). Identity files grant `environments:` (an old `sandboxes:` key, or `sandboxes:write`, stops the Runtime with a message naming the new key). Migration `0020_environments`.
39
+ - `@nylorun/harness`: the Harness API claims `environment.state` and `environment.exec`, and its workspace fields name `environmentId`.
40
+ - `@nylorun/agents`: `client.environments` (`put`, `get`, `list`, `page`, `stop`, `reset`, `delete`, `events`, `secrets.put|list|get|delete`, `forSession`) replaces `client.sandboxes`; `createSession({ environment })`; `sessions.page({ environmentId })` filters by Environment. The AG-UI handler's session options take `environment`.
41
+ - `@nylorun/admin`: `admin.coverage({ agentId, environmentId })` replaces `admin.vaults`; `admin.mcp.preview({ url, environmentId })`.
42
+ - `@nylorun/studio`: Environments replace Sandboxes, with a create form, stop, reset, delete and their secrets; the Credentials forms write an Environment's secrets; New session picks an Environment and shows its coverage.
43
+ - `nylorun`: `nylorun sandbox ls|rm` list and delete Environments; `nylorun mcp inspect --environment <id>` replaces `--vault`; `nylo reset --environments` replaces `--sandboxes`; `nylo doctor sandbox` reports the Environment limits.
44
+
45
+ - e917bd6: **The Harness API is a published contract (R4.5).** Everything a harness sends or reads now has a schema in `@nylorun/core/harness-api`, and the Runtime validates with those same schemas:
46
+ - the transport and the pod join (`HARNESS_API_PATH`, `HARNESS_API_HEADER`, `HOST_JOIN_PATH`, `HostJoinRequestSchema`, `HostJoinAnswerSchema`);
47
+ - the Model and Tool Gate bodies (`ModelCallBodySchema`, `ToolCallBodySchema`, `HttpCallBodySchema`, the MCP bodies, `GATE_ERROR_CODES`);
48
+ - the MCP snapshot (`McpSnapshotSchema`);
49
+ - transcript entries (`TranscriptEntrySchema`);
50
+ - the effect id rule (`effectIdOf`, `isEffectIdOf`).
51
+
52
+ The build also writes them as one JSON Schema document, `@nylorun/core/harness-api/schema.json`, for harnesses written in other languages.
53
+
54
+ Other changes:
55
+ - `TurnStart` gains an optional `trace` (W3C `traceparent`) for the turn's trace context. Nothing fills it yet.
56
+ - Core refuses a `hello` whose `api` is not `2`.
57
+ - **The fixture model answers at the Model Gate (R4.3).** With a Tenant's `fixtureModel` seeded, the gate answers model calls itself and meters each one, like any model: a `model_usage` row with provider `fixture` and cost 0, readable at `GET /v1/sessions/{id}/calls/model`. Our harness no longer answers it locally.
58
+ - The fixture model also answers scripted markers: `fixture:text`, `fixture:tool <name> <json>`, `fixture:budget` and `fixture:slow <seconds>`.
59
+ - **The sandboxes service can run a harness image (R4.4, Go).** `PUT /v1/pods/{name}` accepts `entrypoint: "image"`: the workload image's own `ENTRYPOINT` and `CMD` run, with no engine copied in and no `harnessImage`. The default, `entrypoint: "engine"`, is unchanged. The Runtime starts sending it with the harness pin in a later change.
60
+
61
+ - 3013501: **Tool calls are delivered at least once, and every one is on the record (R5 step 1).** A gateway restart used to park the sessions it interrupted as `uncertain` until someone cancelled them, a harness could call tools with no record, and nothing listed a session's tool calls. Now every tool call ends in the log with its outcome, a crash sends it again with the same key, and the gateway runs only calls the effect journal recorded.
62
+
63
+ - **Every outcome in the log.** Each tool call ends in one `tool.completed` with `status` (`completed`: the tool answered; `failed`: the Runtime ended it, `tool.disabled`, `tool.unavailable` or `tool.interrupted`; `cancelled`), `kind` (`mcp`, `http`, `sandbox`, `platform`), `server`, `attempts` and `durationMs`. Refused and cancelled calls are in the log for the first time.
64
+ - **`GET /v1/sessions/{id}/calls/tool`** pages a session's tool calls from the effect journal, in the order they were first sent: tool, input, status, sends, error and timing. Same credentials and paging as `/calls/model`; `@nylorun/agents` adds `session.toolCalls()`. Host feature `tool-calls`.
65
+ - **At least once, with the same key.** An MCP or HTTP tool call that a crash, a shutdown or a lost gateway hop interrupted is sent again with its effect id as the key, up to 3 sends; then it fails to the model as `tool.interrupted` ("may have run") and the session continues. A gateway that restarts mid call runs the call again under its key instead of answering `uncertain`. HTTP tools get the key as `Idempotency-Key`, as before; MCP servers now get it in `_meta["nylorun/idempotency-key"]` on `tools/call`. A tool that honors the key acts once; `attempts` above 1 marks where one may have acted twice.
66
+ - **Sandbox commands are never run twice.** A sandbox or core-served call a crash interrupted has no key to dedupe on, so it fails to the model as `tool.interrupted` at once, and the model decides whether to run it again.
67
+ - **No tool call parks a session.** The `uncertain` state for tool calls, its cancel-only exit and R2b's read-only/idempotent `mcp.lost` exception are gone: a lost MCP answer is `mcp.lost` to the model for every tool. Model calls keep their own rules.
68
+ - **The gateway runs only journaled calls.** Under a run token, `/nylorun/v1/tool-calls` and `/nylorun/v1/http-calls` need the call's effect id as `Idempotency-Key` (`400 key_required`), and the effect journal must hold it running, for the run's turn, the same tool and the same input (`409 effect_not_journaled`). A harness that skips the journal gets nothing run.
69
+ - `@nylorun/harness`: `HarnessExecutors.resends(effect, error, run)` says a gate hop lost a call's answer; the harness then asks core again with the same intent and sends the call again.
70
+ - **Approval** stays in the manifest and our harness still asks for it. Only the Runtime's own harness asks: one built from the Harness API can skip it until tool policy moves to the gateway (R5 step 2).
71
+
72
+ - 6061a3a: **Tool approvals are the Tenant's tool policy (R5 step 2).** An agent's manifest used to say which tool calls wait for a person, and only the Runtime's own harness asked; a harness built from the Harness API could skip it. Now the operator sets one tool policy for every agent and every harness, and the Runtime enforces it before anything is sent.
73
+
74
+ - **`GET`/`PUT /v1/tenant/tool-policy`** (the Management API; `admin.toolPolicy.get()`/`.put({ rules })`): rules `{ effect: "allow" | "ask" | "deny", tool?, server?, kind?, agent?, when?, reason? }`. A rule matches a call when every selector it sets matches (`tool` and `server` take `*` globs; `when` tests input fields by dotted path with `equals`, `in`, `gt`, `gte`, `lt`, `lte` or `matches`). The strictest matching rule decides; a call no rule matches runs.
75
+ - **Enforced for every harness.** Core decides an MCP or HTTP tool call as it journals it. `deny` fails it to the model as `tool.denied`, unsent. `ask` holds the call (`held` at `/calls/tool`) and pauses the turn with one `approval` interaction per held call (`turn.paused`, with the tool, its input and the reason). `approve` sends that exact call, or fails it as `tool.denied`. The Tool Gate runs only calls the journal holds running, so a held or refused call cannot run. Nothing holds a process while it waits: an approval survives a restart. A flow's HTTP stages are decided the same way.
76
+ - **`approval` leaves the manifest.** `PUT /v1/agents/:id` and `saveAgent()` refuse a definition that sets `approval` on an HTTP tool, an MCP server or an MCP tool's settings; `http({ approval })` and `.mcp({ … approval })` throw; a turn manifest can no longer add one. The engine never asks for an HTTP or MCP tool's approval itself. A session pinned to a manifest saved with `approval: "always"` keeps it, as an ask.
77
+ - `@nylorun/studio`: a paused session shows an approval card for each held call, with its tool, why it is held and its input, and **Approve** / **Deny**.
78
+ - Migration: move each `approval: "always"` to an `ask` rule (see MIGRATION.md).
79
+
80
+ - 7d496e4: **Every connection a pod Environment makes to the internet is on the record (R7, egress events).** egress-gate used to keep only log lines, so a session's log could not say which hosts its turn reached or what the gate refused. Now each connection through egress-gate is one event, written by the gate itself.
81
+
82
+ - **`egress.connection`** in the session log of the turn that held the Environment when the connection opened, with that turn's id even if the turn has ended by the time it closes. Source `egress-gate`, evidence `observed`.
83
+ - **`environment.egress`**, the same payload, on the Environment's own stream (`GET /v1/environments/{id}/events`) for traffic while no turn holds the Environment.
84
+ - **The payload:** `environmentId`, `host`, `port`, `outcome` (`allowed` or `refused`), `reason` and `status` for a refusal, `mode` (`tunnel` or `credential`), `credentialBound`, `bytesUp` and `bytesDown`, `startedAt` (the gate's clock at CONNECT) and `durationMs`. A host a shell credential is bound to adds `requests` and the `credentialIds` the gate set. It never carries a header value, a URL path or a body.
85
+ - **One event per connection:** written when it closes, or at once when the gate refuses it (a host not in `network.allowedHosts`, a port other than 443 or 80, a private address, too many tunnels, a name that does not resolve). A refusal before the Environment is known (no token, an expired or stale one) stays a log line only.
86
+ - **Never in the way:** a write happens after the connection closes or is answered, and a failed write is logged as `egress_record_failed` without touching the tunnel. The gateway's `egress_refused` and `egress_terminated` log lines stay.
87
+
88
+ - 81c783e: **Every row that ends a call or a turn carries its own timing (R7.1).** The session log said what happened but not how long it took: a model call's outcome had no time at all, a tool call's only core's window, a flow's HTTP stage had no outcome row, and a turn's end had neither its duration nor its cost. Now each one is a span on its own, ready for the traces that follow, with no protocol change.
89
+
90
+ - **Model calls.** `message.assistant`, `model.failed` and `context.compacted` carry the call's `effectId` and `durationMs` (from the intent to the outcome). When the Model Gate metered the call they also carry the gate's own `gate: { startedAt, endedAt }`, `firstTokenMs` (to the provider's first stream event) and `meteredUsage` (`inputTokens`, `outputTokens`, `totalTokens`, and `cachedTokens` and `costUsd` when not zero), beside the `usage` the harness reported. The usage ledger keeps the gate's times (migration 0022: `model_usage.started_at`, `ended_at`, `first_token_ms`).
91
+ - **One row per model call.** A step that said nothing (no text, no tool calls, as a structured answer) now writes `message.assistant` with `text: ""`, and each partial summary of a long compaction writes `context.compacted` with `partial: true`. The AG-UI and A2A histories skip both; `isEmptyAssistantMessage` in `@nylorun/core/contracts` tells your own UI which steps said nothing.
92
+ - **Tool calls.** `tool.completed` carries the call's `effectId`, the Tool Gate's own `gate: { startedAt, endedAt }` for an MCP or HTTP call it ran (absent for sandbox and core-served tools), and `policy: { decision: "ask" | "deny", reason, approvedBy? }` when the Tenant's tool policy held or refused the call.
93
+ - **Flow HTTP stages.** A new event type, `node.completed` (`path`, `kind`, `key`, `iterations`, `effectId`, `status`, `durationMs`, `gate`), ends each HTTP stage and HTTP verifier that `node.started` began, and a tool stage the Runtime refused to run. Clients ignore event types they do not know.
94
+ - **Turns.** `turn.completed`, `turn.failed` and `turn.cancelled` carry `durationMs` (from the turn's start to its end, waits for a person included), `pausedMs` (how much of that it waited paused), `agentId` and `totals: { steps, tokens, costUsd }`, the same totals `GET /v1/tenant/usage?scope=turn` returns. A turn started before this release has no `durationMs`.
95
+
96
+ - 7800358: **Each turn is one OpenTelemetry trace, projected from the session log (R7).** An operator could not see what a turn did or where its time went without grepping container logs. Now every turn is a trace with a span for each model call, tool call and egress tunnel, exported over OTLP to any collector, readable per turn from the Runtime, and joined to the trace of the app that started it. No protocol change.
97
+
98
+ - **Traces from the log.** A turn's end is its root span, `invoke_agent <agent>`; each model call is a `chat <model>` span, each tool call and flow HTTP stage an `execute_tool <tool>` span, each egress-gate tunnel a `CONNECT <host>` span, and each pause and answer a marker. A model or gate tool span is the gate's own start and end; usage and cost are the gate's (`nylorun.usage.mismatch` when the harness reported other numbers). Attribute names follow the OTel GenAI conventions at one pinned commit, recorded as `nylorun.semconv.genai` on the resource. Ids are derived from the log under a key the Tenant keeps (setting `trace.key`), so every export and read of a turn gives the same ids.
99
+ - **OTLP export, off until you set an endpoint.** With `OTEL_EXPORTER_OTLP_ENDPOINT` (or `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`) the Runtime posts OTLP/HTTP JSON from a replication slot of its own, `nylorun_trace_export`, in batches of up to 512 spans or every 2 s; `OTEL_EXPORTER_OTLP_HEADERS`, `OTEL_SERVICE_NAME` and `OTEL_RESOURCE_ATTRIBUTES` work as usual. No turn waits for it. A collector that is down costs only its spans: after three retries a batch is dropped and logged as `trace_export_dropped`, and the slot moves on, so Postgres keeps no WAL for it. `nylorun start` passes these variables to the runtime container.
100
+ - **References by default.** Spans point at log events (`nylorun.event.cursor`) and carry no content unless you opt in per kind with `NYLORUN_TRACE_CONTENT` (`user`, `outputs`, `tool-results`, `errors`), or all of them with `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true`, each cut at 8 KiB. A span never carries a credential value, a prompt, a person's id, an identity header or an idempotency key.
101
+ - **`GET /v1/sessions/{id}/turns/{turnId}/trace`** answers a turn's spans as OTLP JSON, the same spans the exporter posts: look at a turn without a collector, or post it to one to backfill. Application keys, as the `/calls` reads. Host feature `traces`; `@nylorun/agents` adds `session.trace(turnId)`, and `@nylorun/core/contracts` `TurnTraceSchema`.
102
+ - **Trace context in and out.** A `traceparent` (and `tracestate`) on `POST /v1/sessions/{id}/commands`, an AG-UI run or an A2A message puts the turn in the caller's trace; `command.message` records it as `trace`. The turn hands its context on: to the harness in `TurnStart.trace`, to HTTP tools as `traceparent` beside `Idempotency-Key`, to MCP servers as `traceparent` on the call's requests, and to sandbox commands as `TRACEPARENT`.
103
+ - **Harness API** (additive): the `effect.intent` answer `execute` may carry the tool call's `traceparent`. `@nylorun/harness`: `HarnessExecutors.tool` takes it as a fourth argument (`ToolCallContext`).
104
+
105
+ ### Patch Changes
106
+
107
+ - 17a97e9: **An artifact names the Environment whose disk it holds.** `ArtifactView` (and so `ArtifactListItem` in artifact pages) gains optional `environmentId`: set on the folder artifact that holds an Environment's disk versions (R2d step 3), absent on a session's or the Tenant's own artifacts. Before, such an artifact read as a Tenant-owned one with no way to tell whose disk it was.
108
+ - b6214d9: Add opt-in artifact metadata pagination to `GET /v1/artifacts?limit=…`, shared
109
+ `ArtifactListItem`/`ArtifactPage` contracts and SDK `artifacts.page()` (default 50,
110
+ maximum 200). Hosts advertise `artifact-reads`. Pages order by creation time and
111
+ ID descending, filter by session, kind and labels, bind cursors to the Tenant and
112
+ filters, and reapply ownership and agent grants on each read. Legacy unpaged
113
+ responses remain unchanged. A Drizzle migration adds the creation-order index;
114
+ pages reuse the existing bounded read-only pool. Studio and every other Runtime
115
+ client can consume this public API without database access.
116
+ - a708b27: **The example harness, on harness ids (R4.8).**
117
+
118
+ - **A new fixture marker.** `fixture:unoffered <name> <json>` makes the fixture model call a tool the turn does not offer, so a test can see that such a call runs nothing and the turn goes on. The other markers are unchanged.
119
+ - **The example harness.** `examples/harness-minimal` says `minimal` in `hello`, connects with its own token from `nylorun-operate harness token put minimal`, and runs beside ours: sessions on an Environment whose `harness` is `minimal` go to it. As a pod Environment's `image` it is the pod's harness.
120
+ - **A session's workspace is its own harness's.** Off a pod, a session's sandbox routes, `save_artifact` and turn-end export go only to a connection of the session's harness that serves workspaces. Before, a custom harness's session was served from our harness's workspace, which is not that harness's files. Without such a connection the request is refused. Sweeps and removes still go to ours.
121
+ - **The Harness API reference and guide.** `guides/HARNESS_API.md` covers every section a harness implements, with a walkthrough for building one.
122
+
123
+ - dec813c: **A gateway shutdown no longer leaves an unhandled rejection.** When a keyed MCP or HTTP tool call was re-sent and the re-send's client went away before the gateway joined the running call, closing the gateway mid call rejected that join with nobody listening ("The gateway is shutting down"), which Node reports as an unhandled rejection. The gateway now handles it: the call still stops on shutdown and the next gateway sends it again under its key, as before.
124
+ - 11c3b2a: **The Runtime's own tokens never reach a session's record from what a harness reports.** A harness's commands hold the egress token in their proxy variables, and a command that prints its environment, or a model that repeats it, would put that token into a tool result. Core now replaces every Nylorun-signed token before recording it:
125
+ - where it looks: tool and model outcomes, error messages, claimed sandbox events, and a turn's output, state and transcript edits;
126
+ - what a token is: a compact JWT whose `typ` starts with `nylorun-`, such as egress, run or host tokens;
127
+ - what replaces it: `[nylorun token redacted]`.
128
+
129
+ This applies to every harness, ours included. The leak showed when the example harness ran as a sandbox pod's image. Other values are unchanged.
130
+
131
+ - 02c2d24: **The runtime image keeps the repository's new layout.** The Runtime's code moved to `runtime/server/` and the harness to `runtime/harness/` in the repository, and the image follows it: the entry point is `/app/runtime/server/dist/host/main.js` (was `/app/runtime/dist/host/main.js`). The image's `ENTRYPOINT`, `nylorun-operate` and the sandboxes service (`ghcr.io/nylorun/sandboxes`, released with the Runtime) use the new path; anything that runs a file under `/app/runtime/dist/` directly needs the new one.
132
+ - a0cbca1: **Clearer wording when a definition runs your code.** The refusal for a code tool or a flow's tool stage, the `hooks were removed` error and `nylo endpoints` now say that the Runtime runs agents from their manifests alone, instead of that it runs no code of yours during a session. Only the message text changed; anything matching the old wording (for example `runs no code of yours during a session`) needs the new text.
133
+ - Pin core to the tested release.
134
+ - Pin harness to the tested release.
135
+ - Updated dependencies [17a97e9]
136
+ - Updated dependencies [b6214d9]
137
+ - Updated dependencies [e917bd6]
138
+ - Updated dependencies [5165451]
139
+ - Updated dependencies [35e29b7]
140
+ - Updated dependencies [e917bd6]
141
+ - Updated dependencies [a0cbca1]
142
+ - Updated dependencies [3013501]
143
+ - Updated dependencies [6061a3a]
144
+ - Updated dependencies [7d496e4]
145
+ - Updated dependencies [81c783e]
146
+ - Updated dependencies [7800358]
147
+ - Updated dependencies
148
+ - @nylorun/core@0.20.0-beta
149
+ - @nylorun/harness@0.26.0-beta
150
+
3
151
  ## 0.23.0-beta
4
152
 
5
153
  ### Minor Changes
package/README.md CHANGED
@@ -4,7 +4,7 @@ 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. Every Tenant serves two APIs on one URL: the
6
6
  **Runtime API** for developers (agents, sessions with AG-UI and
7
- A2A, sandboxes, artifacts), whose client is `@nylorun/agents`, and the
7
+ A2A, Environments and their secrets, artifacts), whose client is `@nylorun/agents`, and the
8
8
  **Management API** for operators (`/v1/tenant/*`), whose client is `@nylorun/admin`.
9
9
  Vocabulary: [src/CONTEXT.md](./src/CONTEXT.md).
10
10
 
@@ -24,7 +24,7 @@ Restate and S2: `nylorun start` runs them, as a local Tenant, on a developer mac
24
24
  package is a library with no bin; its Host entry is `@nylorun/runtime/server`
25
25
  (`dist/host/main.js`), which requires `NYLORUN_DATABASE_URL`. For tests and
26
26
  ephemeral embeds, use `startEphemeralRuntime()` from `@nylorun/runtime/core`.
27
- See [MIGRATION.md](../MIGRATION.md).
27
+ See [MIGRATION.md](../../guides/MIGRATION.md).
28
28
 
29
29
  A Runtime serves one **Tenant**: an installation is one Runtime with its own
30
30
  Postgres database, Restate and S2 basin, and two Tenants are two installations. No
@@ -45,7 +45,7 @@ database (`tenant_<id>` schemas), or by a pre-release build of one Tenant per da
45
45
  (`schema_version` tables), is refused: this release starts fresh on a new database. Restate runs one advance of a session at a time, delivers its wakes and holds
46
46
  its timers and the Tenant's sweep (Durable Session Execution), and journals no effect:
47
47
  checkpoints, effects, events and the outbox are the Session Store's, and a wake or a pod
48
- sandbox's reconcile is written to the outbox in the transaction that causes it, so a commit
48
+ Environment's reconcile is written to the outbox in the transaction that causes it, so a commit
49
49
  never loses it ([src/CONTEXT.md](src/CONTEXT.md)); every session's events are relayed from the record to
50
50
  its own S2 stream, which history and SSE read (Durable Streams). One image runs every **service**,
51
51
  and `--service` picks what a process runs: `core` serves the Runtime API, the
@@ -66,7 +66,7 @@ core's process. A `harness` process needs `NYLORUN_HARNESS_URL`,
66
66
  `/harness`), answers `/health` on `127.0.0.1:4300`, and refuses to start with a
67
67
  database, Restate, keys or gates credential in its environment. A local Tenant
68
68
  runs it in the `harness` container, on a network with only the runtime and the
69
- gateway ([DEPLOYMENT.md](../DEPLOYMENT.md#the-harness-agent-turns-mcp-servers-and-workspaces)).
69
+ gateway ([DEPLOYMENT.md](../../guides/DEPLOYMENT.md#the-harness-agent-turns-mcp-servers-and-workspaces)).
70
70
 
71
71
  The container is configured by its environment, which a local Tenant's Compose file
72
72
  sets: `NYLORUN_DATABASE_URL` (required), `NYLORUN_RESTATE_INGRESS_URL`,
@@ -91,7 +91,7 @@ Host) the registration replaces the deployment at that URL.
91
91
  `NYLORUN_IDENTITY_FILE` names the identity
92
92
  file, a YAML list of the trusted issuers whose JWTs the Runtime API accepts
93
93
  (Host feature `trusted-issuers`), read once at boot; a malformed file stops the
94
- boot ([DEPLOYMENT.md](../DEPLOYMENT.md#trusted-issuers)). A process that runs `loop` sends its model
94
+ boot ([DEPLOYMENT.md](../../guides/DEPLOYMENT.md#trusted-issuers)). A process that runs `loop` sends its model
95
95
  calls to the gate at `NYLORUN_GATES_URL`; in a container it refuses to start
96
96
  without it and `NYLORUN_GATES_TOKEN`. That token is core's credential: the gate
97
97
  accepts only it for vault writes and token signing. Model, HTTP tool and
@@ -103,16 +103,16 @@ is refused with `409 run_stale`. A `gates` process needs only
103
103
  (`NYLORUN_GATES_LISTEN_HOST`, `NYLORUN_GATES_LISTEN_PORT`, default 4100, and
104
104
  `NYLORUN_GATES_ALLOWED_HOSTS`) and the Host's `tenant/` directory, which it
105
105
  never writes; it serves the database's one Tenant. A session's MCP servers and
106
- HTTP tools get their credentials from its attached vaults only: a `bearer` token
106
+ HTTP tools get their credentials from the vault of its Environment only: a `bearer` token
107
107
  or a `headers` map per URL, sent to an MCP gateway instead when it has `via`,
108
108
  with the session owner in its identity header
109
- ([DEPLOYMENT.md](../DEPLOYMENT.md#mcp-servers-and-http-tools)); the credential resolver is gone
109
+ ([DEPLOYMENT.md](../../guides/DEPLOYMENT.md#mcp-servers-and-http-tools)); the credential resolver is gone
110
110
  (protocol 10), and a process that still sets a `NYLORUN_RESOLVER_*` variable logs
111
111
  `resolver_removed` and ignores it. With `egress`
112
112
  (`--service gates,keys,egress`, for pod sandboxes) the same process runs
113
113
  egress-gate on `NYLORUN_EGRESS_LISTEN_HOST`:`NYLORUN_EGRESS_LISTEN_PORT`
114
114
  (default `0.0.0.0:4200`): a CONNECT proxy that admits a pod's egress token and
115
- tunnels only to the hosts its sandbox spec allows, on 443 or 80, never to a
115
+ tunnels only to the hosts its Environment allows (`network.allowedHosts`), on 443 or 80, never to a
116
116
  private address. `NYLORUN_PACKING` (`combined` or `split`) is logged at startup.
117
117
 
118
118
  The Host has one listener, which serves both APIs. Host work runs on the machine
@@ -169,9 +169,12 @@ with `415 unsupported_media_type`, except an artifact upload (`POST /v1/artifact
169
169
  presents a trusted issuer's token; keys (application and management)
170
170
  are refused from browsers before they are looked up. The Runtime sends no CORS headers, and answers
171
171
  `OPTIONS` with `204` and `Allow` only: the operator's reverse proxy answers preflights
172
- ([DEPLOYMENT.md](../DEPLOYMENT.md#calling-the-runtime-from-browsers-and-apps)). Missing or
172
+ ([DEPLOYMENT.md](../../guides/DEPLOYMENT.md#calling-the-runtime-from-browsers-and-apps)). Missing or
173
173
  unsupported protocol → `426` before authentication, unless the request sends neither
174
- `Nylorun-Protocol` nor `Authorization` (the Host serves protocols 4 to 10; protocol 10 removes
174
+ `Nylorun-Protocol` nor `Authorization` (the Host serves protocol 11 only; protocol 11 replaces
175
+ sandbox resources, the Tenant's default sandbox, inline sandboxes and user and installation
176
+ vaults with Environments, each one sandbox and one vault, under `/v1/environments`, and
177
+ removes the Management API's vault routes, so a protocol 10 client gets `426`; protocol 10 removes
175
178
  the MCP OAuth connect, the vault's `oauth` credentials and the credential resolver, and adds
176
179
  `headers` credentials, `via` and the identity header, manifest v6 and
177
180
  `POST /v1/tenant/mcp/preview`; protocol 9 makes the Runtime API an OAuth 2.1 resource server: a missing or rejected credential is `401
@@ -182,8 +185,7 @@ the Admin API, and those old paths answer `404` with no alias; protocol 7 remove
182
185
  derived principals, whose routes answer `404`; protocol 6 adds file and folder artifacts and message `parts`; at each turn's end the
183
186
  Runtime exports `/workspace/outputs` of the session's sandbox as a version of its
184
187
  `outputs` folder, read at `/v1/artifacts/{id}/versions/{n|latest}/tree`, `/files/{path}`,
185
- `/diff?from=` and `/zip`). Protocol 5 and later clients name no Tenant. A protocol 4 client's `Nylorun-Tenant` naming
186
- another Tenant (or malformed) and a Tenant that
188
+ `/diff?from=` and `/zip`). Clients name no Tenant (since protocol 5). A Tenant that
187
189
  could not be opened → opaque `404` with identical body. A path or method no route serves is `404 Route not found`
188
190
  once the caller is known.
189
191
 
@@ -207,13 +209,13 @@ https://cdn.jsdelivr.net/npm/@nylorun/runtime@<version>/dist/management-openapi.
207
209
 
208
210
  Each document has described tags, in the order a developer uses them. The Runtime API's are
209
211
  grouped (`x-tagGroups`): Agents (Agents, Definition files), Sessions (Sessions API, AG-UI,
210
- A2A), Sandboxes, Artifacts and Service (health, readiness, `/v1/me`, the JWKS and the
212
+ A2A), Environments, Artifacts and Service (health, readiness, `/v1/me`, the JWKS and the
211
213
  document; the `/openapi.json` alias is served but not listed). The Management API's are
212
- Tenant, Application keys, Models, Vaults, Signing keys and Settings. Each operation's
214
+ Tenant, Application keys, Models, Credential coverage, MCP servers, Signing keys and Settings. Each operation's
213
215
  `security` says which credentials it takes (application key and trusted issuer's token in
214
216
  the Runtime API; management key in the Management API), and its
215
217
  `x-nylorun-credentials` and `x-nylorun-scopes` (the subject scopes that reach it) fields say
216
- 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
218
+ who may call it. Event streams are `text/event-stream` with an `itemSchema`. `runtime/server/openapi/` holds the committed snapshots: a change to a route changes
217
219
  them (`node scripts/build-openapi.mjs --write`), and `check-package` fails until they are
218
220
  updated.
219
221
 
@@ -251,18 +253,22 @@ const runtime = await startEphemeralRuntime({
251
253
 
252
254
  The package's own tests run every Session Store on Postgres: `npm test` gives each
253
255
  test file a database on the Docker test stack (`test/stack/compose.yaml`) and starts
254
- the stack when it is down; see [CONTRIBUTING.md](../CONTRIBUTING.md).
256
+ the stack when it is down; see [CONTRIBUTING.md](../../CONTRIBUTING.md).
255
257
 
256
258
  Save agents with `PUT /v1/agents/{agentId}` using the application principal (`saveAgent`
257
259
  in `@nylorun/agents`). The Runtime runs the agent from its manifest and never calls your
258
260
  code during a session: a definition with a tool that would (`tool({ run })`, a flow's tool
259
261
  stage) is refused. Your services are reached as tools: HTTP tools, POSTed from the Runtime,
260
- so their URLs must be reachable from it, and remote MCP servers. The operator gives
261
- each URL a vault credential, previews an MCP server's tools (`nylorun mcp inspect`),
262
- and the manifest says which tools are enabled, need approval or are deferred
263
- ([DEPLOYMENT.md](../DEPLOYMENT.md#mcp-servers-and-http-tools)). Model gateway
264
- and sandbox backend are Tenant configuration (vault / seed), not Host process
265
- env.
262
+ so their URLs must be reachable from it, and remote MCP servers. Each URL gets a
263
+ credential in the vault of the Environment a session names (`PUT
264
+ /v1/environments/{id}/secrets/{name}`), the operator previews an MCP server's tools
265
+ (`nylorun mcp inspect --environment <id>`),
266
+ and the manifest says which tools are enabled or deferred
267
+ ([DEPLOYMENT.md](../../guides/DEPLOYMENT.md#mcp-servers-and-http-tools)). Which calls wait for a
268
+ person, or are refused, is the Tenant's tool policy
269
+ ([DEPLOYMENT.md](../../guides/DEPLOYMENT.md#tool-policy)). Model gateway
270
+ and sandbox backend are Tenant configuration (the Tenant's own vault, Tenant
271
+ settings and seed), not Host process env.
266
272
 
267
273
  ## Session behaviour
268
274
 
@@ -273,11 +279,12 @@ definition/session APIs.
273
279
  Postgres transactions persist session checkpoints, command receipts, individual
274
280
  effects, waits and the record of session events — in the Tenant's database; history is
275
281
  read from Durable Streams (S2). An advance owns its session through a lease with
276
- an epoch; a Worker that takes over after a crash marks in-flight effects
277
- `uncertain`. Vault ciphertext needs that Tenant's own KEK.
282
+ an epoch; a Worker that takes over after a crash marks in-flight model effects
283
+ `uncertain`, and sends an interrupted MCP or HTTP tool call again with the same key (R5). Vault ciphertext needs that Tenant's own KEK.
278
284
 
279
- Agents that declare `.use(sandbox())` get Runtime-executed sandbox tools. The
280
- Tenant owns each sandbox; backend names are prefixed `nylorun-<tenant-id>-`.
285
+ A session that names an Environment of kind `virtual` or `pod` gets Runtime-executed
286
+ sandbox tools; the agent declares nothing. The Tenant owns each Environment; backend
287
+ names are prefixed `nylorun-<tenant-id>-`.
281
288
 
282
289
  ## Local Project workflow
283
290
 
@@ -307,9 +314,53 @@ Definitions have no `agent.run()`; applications use `@nylorun/agents`.
307
314
  ## Session reads
308
315
 
309
316
  Hosts advertising `session-reads` serve pinned session manifests, recorded usage, model-call
310
- pages and opt-in session/history/sandbox pagination. `calls-export` adds the Management API's
317
+ pages and opt-in session/history/Environment pagination. `calls-export` adds the Management API's
311
318
  unfiltered, resumable model ledger export (`GET /v1/tenant/calls/model`, a management key) in
312
- safe transaction order. Existing list/history
319
+ safe transaction order. `tool-calls` adds `GET /v1/sessions/{id}/calls/tool`: each tool call of
320
+ the session from the effect journal, with its input, status and sends. Existing list/history
313
321
  requests without `limit` retain their response shapes. These are public APIs for Studio,
314
322
  CLI and custom clients; clients never need database access. See the
315
323
  [API contracts, authorization, SDK examples and Studio handoff](https://github.com/nylorun/agents/blob/main/runtime/docs/session-reads.md).
324
+
325
+ ## Artifact pages
326
+
327
+ Hosts advertising `artifact-reads` accept `GET /v1/artifacts?limit=50`, returning
328
+ `{ artifacts: ArtifactListItem[], nextCursor: string | null }`. An explicit `limit`
329
+ of 1–200 opts into paging; without it the existing `{ artifacts: ArtifactView[] }`
330
+ list remains oldest first. Pages order by `(createdAt DESC, artifactId DESC)` and
331
+ contain metadata only: id, kind, name, current content type, latest version number,
332
+ optional session and labels, and creation/update timestamps. Use the existing
333
+ artifact detail, numbered version and download APIs for history and bytes.
334
+
335
+ Paged reads accept equality filters `sessionId`, `kind=file|folder` and repeated
336
+ `label=key=value` (all labels must match). `sessionId` includes only that session's
337
+ artifacts, excluding Tenant-owned artifacts. An application key acting as itself
338
+ sees every artifact; subjects and issuer tokens see only artifacts of their owned
339
+ sessions and permitted agents. Missing or inaccessible session filters return 404.
340
+ Management keys cannot use this Runtime API route.
341
+
342
+ Send `nextCursor` as `cursor` with the same normalized filters until it is null.
343
+ Cursors are bound to the Tenant, route and filters; invalid or mismatched cursors
344
+ return 400 (`cursor_invalid` or `cursor_mismatch`). Each page reapplies current
345
+ authorization and is an independent read: it is not a frozen snapshot. Newer
346
+ inserts require a fresh traversal; deleted rows disappear and versions/metadata
347
+ may change between pages. Creation time and ID remain the paging position.
348
+
349
+ These pages reuse the separate bounded, read-only Drizzle pool (four connections,
350
+ two-second statement timeout). Migration `0023_artifact_pages` adds only the
351
+ creation-order index; the existing session index also serves session pages in
352
+ reverse order. No data backfill, protocol bump or direct client database access is
353
+ required. Studio can discover `artifact-reads` through `/health` before enabling
354
+ a tenant-wide table; `@nylorun/agents` exposes the same API as `artifacts.page()`.
355
+
356
+ [Recorded query plans](docs/artifact-pages-evidence.json) use a disposable local
357
+ Postgres 17 fixture: the public SDK drained 10,006 artifacts in 201 pages of at
358
+ most 50 rows, without gaps or duplicates, in 796 ms. First/deep page queries used
359
+ backward index scans with a tuple index condition on the deep page (0.014/0.028 ms).
360
+ These are fixture timings, not production throughput estimates. Reproduce after
361
+ building dependencies with:
362
+
363
+ ```sh
364
+ ARTIFACT_READS_EVIDENCE="$PWD/runtime/server/docs/artifact-pages-evidence.json" \
365
+ npm test --workspace @nylorun/runtime -- test/http/artifact-pages.test.ts
366
+ ```
@@ -314,7 +314,7 @@ export class RestateExecution {
314
314
  if ((await ctx.get(`at:${input.timer}`)) !== input.at)
315
315
  return;
316
316
  ctx.clear(`at:${input.timer}`);
317
- await runSandbox(ctx, input.timer === "retry" ? "reconcile" : input.timer);
317
+ await runSandbox(ctx, input.timer === "idle" ? "idle" : "reconcile");
318
318
  },
319
319
  },
320
320
  options: serviceOptions,
@@ -13,6 +13,8 @@ export interface PgoutputSourceOptions {
13
13
  publication?: string;
14
14
  /** The schema of the record tables. Default `nylorun_streams`. */
15
15
  schema?: string;
16
+ /** The connections' `application_name`. Default `nylorun-stream-relay`. */
17
+ applicationName?: string;
16
18
  /** Wait between attempts to take the slot. Default 5 s. */
17
19
  retryMs?: number;
18
20
  log?: (message: string, fields?: Record<string, unknown>) => void;
@@ -1,7 +1,9 @@
1
1
  /**
2
2
  * The stream relay's change source on Postgres logical replication (Durable Streams §7.1,
3
3
  * §7.4, §7.6): a persistent `pgoutput` slot on the `nylorun_stream_relay` publication, read
4
- * with `pg-logical-replication`. The only file importing it (with `pg`, which it needs).
4
+ * with `pg-logical-replication`. The only file importing it (with `pg`, which it needs). The
5
+ * trace projector (R7.2) reads the same publication from a slot of its own
6
+ * (`traces/projector.ts`); for it a fresh slot means nothing to reconcile.
5
7
  *
6
8
  * - **One reader per slot.** Postgres lets one connection hold a slot, so every Runtime
7
9
  * process can run a source and exactly one is active; the others retry every few seconds
@@ -25,12 +27,13 @@ export function createPgoutputSource(options) {
25
27
  const publication = options.publication ?? "nylorun_stream_relay";
26
28
  const schema = options.schema ?? "nylorun_streams";
27
29
  const retryMs = options.retryMs ?? 5000;
30
+ const applicationName = options.applicationName ?? "nylorun-stream-relay";
28
31
  const { tenantId } = options;
29
32
  const log = options.log ?? (() => { });
30
33
  const pool = new pg.Pool({
31
34
  connectionString: options.connectionString,
32
35
  max: 2,
33
- application_name: "nylorun-stream-relay",
36
+ application_name: applicationName,
34
37
  });
35
38
  let service;
36
39
  let stopped = false;
@@ -72,7 +75,7 @@ export function createPgoutputSource(options) {
72
75
  // `stop()` during `prepareSlot` had no service to stop: do not start one it cannot.
73
76
  if (stopped)
74
77
  return;
75
- const current = new LogicalReplicationService({ connectionString: options.connectionString, application_name: "nylorun-stream-relay" }, { acknowledge: { auto: false, timeoutSeconds: 0 }, flowControl: { enabled: false } });
78
+ const current = new LogicalReplicationService({ connectionString: options.connectionString, application_name: applicationName }, { acknowledge: { auto: false, timeoutSeconds: 0 }, flowControl: { enabled: false } });
76
79
  service = current;
77
80
  let decoding;
78
81
  current.on("start", () => handlers.onActive({ fresh }));
@@ -1,4 +1,4 @@
1
- import type { SandboxBackend } from "../../sandbox/types.js";
1
+ import type { ExecRequest, ExecResult, SandboxBackend } from "../../sandbox/types.js";
2
2
  export interface LocalBackendOptions {
3
3
  /** Default `/workspace`. */
4
4
  readonly workspace?: string;
@@ -6,5 +6,13 @@ export interface LocalBackendOptions {
6
6
  readonly proxyEnv?: () => Readonly<Record<string, string>>;
7
7
  /** The base environment of commands (the engine's allowlisted one); `NYLORUN_*` is dropped. */
8
8
  readonly env: Readonly<Record<string, string | undefined>>;
9
+ /** The engine's `/harness` on the same volume, a disk root of its versions (R2d step 3). */
10
+ readonly harnessRoot?: string;
9
11
  }
12
+ /**
13
+ * How the pod runs a command: `/bin/sh -c` in its own process group, with the base environment,
14
+ * the request's variables and the proxy of the moment. The sandbox tools and the disk's setup
15
+ * (`harness/setup.ts`) both run commands this way.
16
+ */
17
+ export declare function localExec(options: LocalBackendOptions): (request: ExecRequest, signal: AbortSignal) => Promise<ExecResult>;
10
18
  export declare function localBackend(options: LocalBackendOptions): SandboxBackend;
@@ -49,10 +49,14 @@ function collector() {
49
49
  },
50
50
  };
51
51
  }
52
- export function localBackend(options) {
53
- const workspace = options.workspace ?? SANDBOX_WORKSPACE;
54
- const env = baseEnv(options.env, workspace);
55
- const exec = (request, signal) => new Promise((resolve, reject) => {
52
+ /**
53
+ * How the pod runs a command: `/bin/sh -c` in its own process group, with the base environment,
54
+ * the request's variables and the proxy of the moment. The sandbox tools and the disk's setup
55
+ * (`harness/setup.ts`) both run commands this way.
56
+ */
57
+ export function localExec(options) {
58
+ const env = baseEnv(options.env, options.workspace ?? SANDBOX_WORKSPACE);
59
+ return (request, signal) => new Promise((resolve, reject) => {
56
60
  if (signal.aborted)
57
61
  return reject(signal.reason ?? new Error("Turn cancelled"));
58
62
  const child = spawn("/bin/sh", ["-c", request.command], {
@@ -102,10 +106,18 @@ export function localBackend(options) {
102
106
  });
103
107
  });
104
108
  });
109
+ }
110
+ export function localBackend(options) {
111
+ const workspace = options.workspace ?? SANDBOX_WORKSPACE;
112
+ const exec = localExec(options);
105
113
  const missing = (error) => error?.code === "ENOENT" || error?.code === "ENOTDIR";
106
114
  const handle = {
107
115
  workspace,
108
116
  exec,
117
+ diskRoots: {
118
+ workspace,
119
+ ...(options.harnessRoot === undefined ? {} : { harness: options.harnessRoot }),
120
+ },
109
121
  async readFile(path) {
110
122
  try {
111
123
  return await readFile(path, "utf8");
@@ -1,5 +1,6 @@
1
1
  /**
2
- * Virtual backend: an emulated bash with a virtual filesystem, running in the Runtime process.
2
+ * Virtual backend: an emulated bash with a virtual filesystem, running in the harness's process
3
+ * (the harness container, or the Runtime process with NYLORUN_HARNESS=in-process).
3
4
  * `/workspace` is backed by a host directory so files survive stop and Runtime restarts.
4
5
  * It is not a VM boundary; it exists so the first run and CI work on any machine.
5
6
  */
@@ -67,6 +68,8 @@ export function virtualBackend(options) {
67
68
  ...(network ? { network } : {}),
68
69
  });
69
70
  return {
71
+ // A virtual workspace's disk is its host directory: `/workspace` only.
72
+ diskRoots: { workspace: root },
70
73
  async exec(request, signal) {
71
74
  const controller = new AbortController();
72
75
  let timedOut = false;
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { readText } from "../http/body.js";
3
- import { tenantRoute } from "../http/define.js";
3
+ import { TraceHeaders, tenantRoute } from "../http/define.js";
4
+ import { traceContextOf } from "../../traces/ids.js";
4
5
  import { jsonResponse } from "../http/respond.js";
5
6
  import { a2aCall, a2aCard } from "./routes.js";
6
7
  const A2A_SPECIFICATION = {
@@ -63,6 +64,7 @@ export function a2aRoutes(api) {
63
64
  "A2A-Version": z.string().optional().meta({ description: "Or the `A2A-Version` header: `1.0`" }),
64
65
  }),
65
66
  body: { required: true, content: { "application/json": { schema: JsonRpcRequest } } },
67
+ headers: TraceHeaders,
66
68
  },
67
69
  responses: {
68
70
  200: {
@@ -73,9 +75,11 @@ export function a2aRoutes(api) {
73
75
  }, async (c) => {
74
76
  const scope = c.get("scope");
75
77
  const version = c.env.incoming.headers["a2a-version"];
78
+ const trace = traceContextOf(c.req.raw.headers);
76
79
  return jsonResponse(200, await a2aCall(c.env.tenant, scope, c.req.param("agentId"), {
77
80
  body: () => readText(c.req.raw),
78
81
  version: typeof version === "string" ? version : (c.req.query("A2A-Version") ?? null),
82
+ ...(trace ? { trace } : {}),
79
83
  }, c.env.outgoing));
80
84
  });
81
85
  }
@@ -1,5 +1,6 @@
1
1
  import type { ServerResponse } from "node:http";
2
2
  import { type AuthScope, type TenantContext } from "../../tenant/context.js";
3
+ import type { TraceContext } from "../../traces/ids.js";
3
4
  /**
4
5
  * How long a blocking `SendMessage` waits for the task to end or ask for input. After it, the
5
6
  * task is returned `WORKING` and keeps running; the caller polls `GetTask`.
@@ -16,4 +17,5 @@ export declare function a2aCard(ctx: TenantContext, scope: AuthScope, agentId: s
16
17
  export declare function a2aCall(ctx: TenantContext, scope: AuthScope, agentId: string, input: {
17
18
  body: () => Promise<string>;
18
19
  version: string | null;
20
+ trace?: TraceContext;
19
21
  }, response: ServerResponse): Promise<unknown>;
@@ -50,7 +50,7 @@ export async function a2aCall(ctx, scope, agentId, input, response) {
50
50
  // A blocking call stops waiting when the caller hangs up; the task keeps running.
51
51
  const hangup = new AbortController();
52
52
  response.on("close", () => hangup.abort());
53
- const caller = { ctx, scope, access, agentId };
53
+ const caller = { ctx, scope, access, agentId, ...(input.trace ? { trace: input.trace } : {}) };
54
54
  const { id } = envelope.request;
55
55
  try {
56
56
  checkVersion(input.version);
@@ -137,7 +137,7 @@ async function run(caller, sessionId, input,
137
137
  /** The error for a `409` conflict: the session is busy or no longer waiting. */
138
138
  conflict) {
139
139
  try {
140
- const accepted = (await command(caller.ctx, sessionId, SessionCommandSchema.parse(input), caller.scope));
140
+ const accepted = (await command(caller.ctx, sessionId, SessionCommandSchema.parse(input), caller.scope, caller.trace ? { trace: caller.trace } : {}));
141
141
  return { turnId: accepted.turnId, cursor: accepted.cursor };
142
142
  }
143
143
  catch (error) {
@@ -97,7 +97,8 @@ export function historyOf(events, contextId, taskId) {
97
97
  add(clientMessageId(event), "ROLE_USER", [valuePart(payload.value)]);
98
98
  break;
99
99
  case "message.assistant":
100
- // Agents used as tools speak inside the turn; only the agent's own words are history.
100
+ // Agents used as tools speak inside the turn; only the agent's own words are history. A
101
+ // step that said nothing (`text: ""`, R7.1) is none.
101
102
  if (!payload.agent && typeof payload.text === "string" && payload.text.trim() !== "")
102
103
  add(event.eventId, "ROLE_AGENT", [{ text: payload.text }]);
103
104
  break;