@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
@@ -66,7 +66,8 @@ export declare function folderContent(ctx: TenantContext, id: string, version: n
66
66
  /**
67
67
  * Deletes the artifact and every version: its rows and, for a session's artifact, its
68
68
  * `artifact.deleted` event in one transaction, then its bytes. A folder's content-addressed
69
- * files go too, those no other version names, under the quota lock (`folders.ts`).
69
+ * files go too, those no other version names, under the quota lock (`folders.ts`). An
70
+ * Environment's disk artifact is refused: its own routes delete its versions.
70
71
  */
71
72
  export declare function deleteArtifact(ctx: TenantContext, id: string, access: ArtifactAccess): Promise<{
72
73
  artifactId: string;
@@ -52,6 +52,7 @@ export function artifactView(row, versions) {
52
52
  name: row.name,
53
53
  contentType: row.contentType,
54
54
  ...(row.sessionId === null ? {} : { sessionId: row.sessionId }),
55
+ ...(row.environmentId === null ? {} : { environmentId: row.environmentId }),
55
56
  latestVersion: row.latestVersion,
56
57
  ...(row.labelsJson === null ? {} : { labels: JSON.parse(row.labelsJson) }),
57
58
  createdAt: row.createdAt,
@@ -194,6 +195,7 @@ export async function uploadArtifact(ctx, input, body, access, signal) {
194
195
  : JSON.stringify(input.labels),
195
196
  createdAt,
196
197
  updatedAt: createdAt,
198
+ environmentId: null,
197
199
  };
198
200
  version = {
199
201
  artifactId,
@@ -278,11 +280,14 @@ export async function folderContent(ctx, id, version, access) {
278
280
  /**
279
281
  * Deletes the artifact and every version: its rows and, for a session's artifact, its
280
282
  * `artifact.deleted` event in one transaction, then its bytes. A folder's content-addressed
281
- * files go too, those no other version names, under the quota lock (`folders.ts`).
283
+ * files go too, those no other version names, under the quota lock (`folders.ts`). An
284
+ * Environment's disk artifact is refused: its own routes delete its versions.
282
285
  */
283
286
  export async function deleteArtifact(ctx, id, access) {
284
287
  const keys = await ctx.store.tx(async (t) => {
285
288
  const found = await readableArtifact(t, id, access);
289
+ if (found.environmentId !== null)
290
+ fail(409, `Artifact ${id} holds Environment ${found.environmentId}'s disk versions: delete them with DELETE /v1/environments/{id}/versions/{version}, or delete the Environment.`);
286
291
  if (found.sessionId !== null)
287
292
  await lockedSession(t, found.sessionId, access);
288
293
  if (found.kind === "folder")
@@ -22,7 +22,7 @@ export interface WorkspaceSession {
22
22
  /** The session that owns the workspace (one hop through `sandboxOwnerId`). */
23
23
  readonly ownerId: string;
24
24
  /** The sandbox resource the workspace belongs to, when the owner is attached to one. */
25
- readonly sandboxId?: string;
25
+ readonly environmentId?: string;
26
26
  /** The session's pinned manifest: its sandbox capability carries the sandbox's spec. */
27
27
  readonly manifest: AgentManifest;
28
28
  }
@@ -24,7 +24,7 @@ export function sandboxWorkspaceReader(sandbox) {
24
24
  id: session.ownerId,
25
25
  activeTurnId: session.turnId,
26
26
  manifest: session.manifest,
27
- ...(session.sandboxId === undefined ? {} : { sandboxId: session.sandboxId }),
27
+ ...(session.environmentId === undefined ? {} : { environmentId: session.environmentId }),
28
28
  },
29
29
  };
30
30
  };
@@ -2,6 +2,7 @@ import type { EffectOutcome } from "@nylorun/core/contracts";
2
2
  import { type JsonValue, type WorkflowManifest } from "@nylorun/core/define";
3
3
  import type { HostEffect } from "@nylorun/harness/run";
4
4
  import type { EffectDoc, Tx } from "../store/types.js";
5
+ import { type TurnTiming } from "../tenant/turn-timing.js";
5
6
  /** Deterministic agent session id: derive(workflowSessionId, path, …parts). */
6
7
  export declare function deriveSessionId(workflowSessionId: string, path: string, ...parts: string[]): string;
7
8
  /**
@@ -59,15 +60,13 @@ export type FlowHostSession = {
59
60
  status: string;
60
61
  activeTurnId: string | null;
61
62
  info?: any;
62
- vaultIds?: readonly string[];
63
- credentialSelections?: readonly unknown[];
64
63
  creation?: unknown;
65
64
  waits?: unknown;
66
65
  lastOutput?: JsonValue;
67
66
  /** The turn `status`, `lastOutput` and `error` describe, once it ended. */
68
67
  lastTurnId?: string;
69
68
  error?: string;
70
- };
69
+ } & TurnTiming;
71
70
  /** How a linked agent turn ended. */
72
71
  export type LinkedTurnEnd = {
73
72
  readonly turnId: string;
@@ -50,6 +50,7 @@
50
50
  */
51
51
  import { createHash } from "node:crypto";
52
52
  import { isVerdict } from "@nylorun/core/define";
53
+ import { resumeTurn } from "../tenant/turn-timing.js";
53
54
  /** Deterministic agent session id: derive(workflowSessionId, path, …parts). */
54
55
  export function deriveSessionId(workflowSessionId, path, ...parts) {
55
56
  const digest = createHash("sha256")
@@ -237,6 +238,8 @@ export async function wakeLinkedWorkflow(input) {
237
238
  }
238
239
  : { value: input.output ?? null };
239
240
  await settleAgentEffect({ t, effect, outcome });
241
+ // A workflow paused on its agents' interactions goes on (R7.1, `tenant/turn-timing.ts`).
242
+ resumeTurn(workflow);
240
243
  workflow.status = "runnable";
241
244
  await t.put("sessions", workflow.id, workflow);
242
245
  await t.wake(workflow.id, {
@@ -9,7 +9,8 @@ export declare function gatewayModel(options: {
9
9
  model: string;
10
10
  }): ModelProvider;
11
11
  /**
12
- * Deterministic release fixture; still uses the real engine. With a `lookup_order` tool it looks
13
- * up the demo order through it once; without one it answers in text.
12
+ * The fixture model as a provider (`model: {kind: "fixture"}` in the Runtime's config). The Tenant
13
+ * setting `model.fixture` is answered by the Model Gate instead (R4.3); both answer the same way
14
+ * (`gates/fixture-model.ts`).
14
15
  */
15
16
  export declare function toolFixtureModel(): ModelProvider;
@@ -1,3 +1,4 @@
1
+ import { fixtureAnswer } from "../gates/fixture-model.js";
1
2
  export function scriptedModel(output = "Scripted development response") {
2
3
  return async () => ({ output: [{ type: "text", text: output }] });
3
4
  }
@@ -23,43 +24,10 @@ export function gatewayModel(options) {
23
24
  };
24
25
  }
25
26
  /**
26
- * Deterministic release fixture; still uses the real engine. With a `lookup_order` tool it looks
27
- * up the demo order through it once; without one it answers in text.
27
+ * The fixture model as a provider (`model: {kind: "fixture"}` in the Runtime's config). The Tenant
28
+ * setting `model.fixture` is answered by the Model Gate instead (R4.3); both answer the same way
29
+ * (`gates/fixture-model.ts`).
28
30
  */
29
31
  export function toolFixtureModel() {
30
- return async (effect) => {
31
- const call = effect.input;
32
- const last = call.prompt.at(-1);
33
- if (last?.kind === "tool-result")
34
- return {
35
- output: [
36
- {
37
- type: "text",
38
- text: `Order lookup complete: ${last.content.map((part) => (part.type === "text" ? part.text : JSON.stringify(part))).join(" ")}`,
39
- },
40
- ],
41
- };
42
- const users = call.prompt.filter((p) => p.kind === "message" && p.role === "user");
43
- if (users.length > 1)
44
- return {
45
- output: [
46
- {
47
- type: "text",
48
- text: `I remember: ${users[0].content.map((part) => (part.type === "text" ? part.text : "")).join(" ")}`,
49
- },
50
- ],
51
- };
52
- if (!call.tools.some((tool) => tool.name === "lookup_order"))
53
- return { output: [{ type: "text", text: "Hello from the fixture model." }] };
54
- return {
55
- output: [
56
- {
57
- type: "tool-call",
58
- id: "fixture_lookup",
59
- name: "lookup_order",
60
- args: { orderId: "demo-123" },
61
- },
62
- ],
63
- };
64
- };
32
+ return async (effect, signal) => fixtureAnswer(effect.input, signal);
65
33
  }
@@ -9,32 +9,13 @@ export type SandboxRouteSession = SessionSandboxRef & {
9
9
  readonly activeTurnId: string | null;
10
10
  readonly status: string;
11
11
  };
12
- /** A session together with a synchronous lookup over its sandbox owner chain. */
13
- export type SandboxRouteRead = {
14
- readonly session: SandboxRouteSession;
15
- readonly lookup: (id: string) => SessionSandboxRef | undefined;
16
- };
17
12
  export type SandboxRouteDeps = {
18
13
  readonly sandbox: Pick<WorkspacePort, "run">;
19
- /** The session (rejects when missing) and its owner chain, read in one transaction. */
20
- readonly session: (id: string) => Promise<SandboxRouteRead>;
14
+ /** The session; rejects when it is missing. */
15
+ readonly session: (id: string) => Promise<SandboxRouteSession>;
21
16
  };
22
17
  /**
23
18
  * `POST /v1/sessions/:id/sandbox/:tool` — application principal.
24
19
  * Refused while the session has an active agent turn.
25
20
  */
26
21
  export declare function handleSessionSandboxTool(deps: SandboxRouteDeps, sessionId: string, toolName: string, body: unknown, signal: AbortSignal): Promise<SandboxToolOutcome>;
27
- /**
28
- * Validate PutSession.sandbox attach: same owner, and a sandbox to share. A session whose
29
- * definition declares a sandbox must declare the owner's spec; one whose definition declares
30
- * none inherits it (`inherit`).
31
- */
32
- export declare function validateSandboxAttach(body: {
33
- ownerUserId: string;
34
- sandbox?: unknown;
35
- }, newManifest: SessionSandboxRef["manifest"], lookup: (id: string) => SessionSandboxRef | undefined,
36
- /** Acting for a subject: another owner's session is the 404 of a missing one. */
37
- options?: {
38
- opaque?: boolean;
39
- inherit?: boolean;
40
- }): string | undefined;
@@ -1,9 +1,9 @@
1
1
  /**
2
- * HTTP handlers for the session-scoped sandbox tool route (`POST /v1/sessions/:id/sandbox/:tool`)
3
- * and the sandbox attach check of `PUT /v1/sessions/:id`.
2
+ * HTTP handlers for the session-scoped sandbox tool route (`POST /v1/sessions/:id/sandbox/:tool`):
3
+ * a tool run in the sandbox of the session's Environment.
4
4
  */
5
5
  import { isSandboxToolName, } from "@nylorun/core/define";
6
- import { capabilityForSandbox, owningSandboxSessionId, sandboxSpecOf, sandboxWorkspaceOf, sandboxSpecsEqual, sessionSandboxSpec, } from "../sandbox/share.js";
6
+ import { capabilityForSandbox, sandboxWorkspaceOf, sessionSandboxSpec, } from "../sandbox/share.js";
7
7
  export class SandboxRouteError extends Error {
8
8
  status;
9
9
  constructor(status, message) {
@@ -12,10 +12,8 @@ export class SandboxRouteError extends Error {
12
12
  this.name = "SandboxRouteError";
13
13
  }
14
14
  }
15
- function resolveSpec(session, lookup) {
16
- const ownerId = owningSandboxSessionId(session, lookup);
17
- const owner = ownerId === session.id ? session : lookup(ownerId) ?? session;
18
- const spec = sessionSandboxSpec(owner) ?? sessionSandboxSpec(session);
15
+ function resolveSpec(session) {
16
+ const spec = sessionSandboxSpec(session);
19
17
  if (!spec)
20
18
  throw new SandboxRouteError(404, "Session has no sandbox");
21
19
  return spec;
@@ -34,14 +32,14 @@ function toolInput(body) {
34
32
  const { requestId: _r, ...input } = record;
35
33
  return input;
36
34
  }
37
- async function runTool(deps, { session, lookup }, tool, input, signal) {
38
- const spec = resolveSpec(session, lookup);
39
- const workspace = sandboxWorkspaceOf(session, lookup);
35
+ async function runTool(deps, session, tool, input, signal) {
36
+ const spec = resolveSpec(session);
37
+ const workspace = sandboxWorkspaceOf(session);
40
38
  const ref = {
41
39
  id: workspace.ownerId,
42
40
  activeTurnId: session.activeTurnId,
43
41
  manifest: session.manifest,
44
- ...(workspace.sandboxId === undefined ? {} : { sandboxId: workspace.sandboxId }),
42
+ ...(workspace.environmentId === undefined ? {} : { environmentId: workspace.environmentId }),
45
43
  };
46
44
  return deps.sandbox.run(ref, capabilityForSandbox(spec), tool, input, signal);
47
45
  }
@@ -51,39 +49,8 @@ async function runTool(deps, { session, lookup }, tool, input, signal) {
51
49
  */
52
50
  export async function handleSessionSandboxTool(deps, sessionId, toolName, body, signal) {
53
51
  const read = await deps.session(sessionId);
54
- if (read.session.activeTurnId)
52
+ if (read.activeTurnId)
55
53
  throw new SandboxRouteError(409, "Sandbox is unavailable while the session has an active agent turn");
56
54
  const tool = parseTool(toolName);
57
55
  return runTool(deps, read, tool, toolInput(body), signal);
58
56
  }
59
- /**
60
- * Validate PutSession.sandbox attach: same owner, and a sandbox to share. A session whose
61
- * definition declares a sandbox must declare the owner's spec; one whose definition declares
62
- * none inherits it (`inherit`).
63
- */
64
- export function validateSandboxAttach(body, newManifest, lookup,
65
- /** Acting for a subject: another owner's session is the 404 of a missing one. */
66
- options = {}) {
67
- const request = body.sandbox;
68
- const share = typeof request === "object" && request !== null && "session" in request
69
- ? String(request.session)
70
- : undefined;
71
- if (!share)
72
- return undefined;
73
- const owner = lookup(share);
74
- if (!owner || (options.opaque && owner.ownerUserId !== body.ownerUserId))
75
- throw new SandboxRouteError(404, "Sandbox session not found");
76
- if (owner.ownerUserId !== body.ownerUserId)
77
- throw new SandboxRouteError(403, "Sandbox session must belong to the same owner");
78
- const ownerSpec = sessionSandboxSpec(owner);
79
- if (!ownerSpec)
80
- throw new SandboxRouteError(400, "Target session has no sandbox to share");
81
- if (options.inherit)
82
- return owningSandboxSessionId(owner, lookup);
83
- const newSpec = sandboxSpecOf(newManifest);
84
- if (!newSpec)
85
- throw new SandboxRouteError(400, "Session must declare a sandbox identical to the shared session");
86
- if (!sandboxSpecsEqual(ownerSpec, newSpec))
87
- throw new SandboxRouteError(409, "Sandbox specs must be identical to share a sandbox");
88
- return owningSandboxSessionId(owner, lookup);
89
- }
@@ -72,7 +72,7 @@ export type AdvanceResult = {
72
72
  retryAfterMs: number;
73
73
  };
74
74
  /** A pod sandbox's timers. */
75
- export type SandboxTimer = "idle" | "ttl";
75
+ export type SandboxTimer = "idle";
76
76
  /**
77
77
  * What `DurableExecution.sandbox` asks for: a reconcile, or a timer. A reconcile's `key` merges
78
78
  * repeated sends of one request (the outbox's `signal:<row id>`); without one, every send runs.
@@ -1,6 +1,6 @@
1
1
  /**
2
- * The gates service's wire format (blueprint §15, P1.1). Internal: only the loop calls it, so
3
- * it is not in the published OpenAPI documents.
2
+ * The gates service's wire format (blueprint §15, P1.1). Not in the published OpenAPI documents:
3
+ * harnesses call it, and its bodies are published in `@nylorun/core/harness-api` (R4.5).
4
4
  *
5
5
  * `POST /nylorun/v1/model-calls`
6
6
  * - Headers: `Authorization: Bearer <run token>` (F5, `tenant/run-token.ts`), `Idempotency-Key:
@@ -22,31 +22,10 @@
22
22
  * whether or not it exists, `403 gate_forbidden` when it is another session's call. The token
23
23
  * need not be live: a user cancel makes it stale, and still stops its call.
24
24
  */
25
- import { z } from "zod";
26
25
  import type { ModelGateOutcome } from "./model-gate.js";
27
- export declare const MODEL_CALLS_PATH = "/nylorun/v1/model-calls";
26
+ export { MAX_MODEL_CALL_BYTES, MODEL_CALLS_PATH, ModelCallBodySchema, type GateErrorBody, type ModelCallBody, } from "@nylorun/core/harness-api";
28
27
  /** Header naming the Tenant a call is for: optional, and checked against the gate's Tenant. */
29
28
  export declare const TENANT_HEADER = "nylorun-tenant";
30
- /** Largest request body the gate reads: one `ModelCall` with its whole projected history. */
31
- export declare const MAX_MODEL_CALL_BYTES: number;
32
- export declare const ModelCallBodySchema: z.ZodObject<{
33
- effectId: z.ZodString;
34
- invocationId: z.ZodString;
35
- call: z.ZodObject<{
36
- prompt: z.ZodArray<z.ZodUnknown>;
37
- tools: z.ZodArray<z.ZodObject<{
38
- name: z.ZodString;
39
- }, z.core.$loose>>;
40
- }, z.core.$loose>;
41
- }, z.core.$strict>;
42
- export type ModelCallBody = z.infer<typeof ModelCallBodySchema>;
43
29
  export interface ModelCallResponse {
44
30
  readonly outcome: ModelGateOutcome;
45
31
  }
46
- /** An error answer: `{error: {code, message}}`. */
47
- export interface GateErrorBody {
48
- readonly error: {
49
- readonly code: "gate_unauthorized" | "gate_forbidden" | "run_stale" | "invalid_request" | "gate_conflict";
50
- readonly message: string;
51
- };
52
- }
@@ -1,38 +1,3 @@
1
- /**
2
- * The gates service's wire format (blueprint §15, P1.1). Internal: only the loop calls it, so
3
- * it is not in the published OpenAPI documents.
4
- *
5
- * `POST /nylorun/v1/model-calls`
6
- * - Headers: `Authorization: Bearer <run token>` (F5, `tenant/run-token.ts`), `Idempotency-Key:
7
- * <effect id>`, and optionally `Nylorun-Tenant: <tenant id>`, which must then be the token's
8
- * Tenant. Core's credential (`NYLORUN_GATES_TOKEN`) is refused: only a run calls the model.
9
- * - Body: `ModelCallBody`, at most `MAX_MODEL_CALL_BYTES`. It names no session, turn or agent:
10
- * the gate takes them from the token, and refuses a body that names them.
11
- * - `200 {outcome}` once the call has finished, whether the outcome is a candidate or a
12
- * failure; nothing is sent while it runs. `401 gate_unauthorized` for a missing, malformed,
13
- * expired or foreign token, `409 run_stale` when the token's lease is no longer the
14
- * session's (a cancel, a new turn, a takeover), `400 invalid_request` for a malformed Tenant
15
- * header or a bad body, `409 gate_conflict` when a different request already runs under the
16
- * same key.
17
- * - With an `Idempotency-Key` the call outlives its client (P1.2): a re-send with the same key
18
- * and body joins it, or gets its outcome for 30 minutes. Without one, a client that goes
19
- * away aborts the call.
20
- *
21
- * `POST /nylorun/v1/model-calls/{key}/cancel` (same headers): aborts the keyed call; `204`
22
- * whether or not it exists, `403 gate_forbidden` when it is another session's call. The token
23
- * need not be live: a user cancel makes it stale, and still stops its call.
24
- */
25
- import { z } from "zod";
26
- export const MODEL_CALLS_PATH = "/nylorun/v1/model-calls";
1
+ export { MAX_MODEL_CALL_BYTES, MODEL_CALLS_PATH, ModelCallBodySchema, } from "@nylorun/core/harness-api";
27
2
  /** Header naming the Tenant a call is for: optional, and checked against the gate's Tenant. */
28
3
  export const TENANT_HEADER = "nylorun-tenant";
29
- /** Largest request body the gate reads: one `ModelCall` with its whole projected history. */
30
- export const MAX_MODEL_CALL_BYTES = 32 * 1024 * 1024;
31
- export const ModelCallBodySchema = z.strictObject({
32
- effectId: z.string().min(1),
33
- invocationId: z.string().min(1),
34
- call: z.looseObject({
35
- prompt: z.array(z.unknown()),
36
- tools: z.array(z.looseObject({ name: z.string() })),
37
- }),
38
- });
@@ -27,6 +27,13 @@ export interface EgressCredentials {
27
27
  /** Tests: the port requests are sent to, instead of 443. */
28
28
  readonly upstreamPort?: number;
29
29
  }
30
+ /** The answer to a CONNECT the gate serves; a tunnel's byte counts leave it out. */
31
+ export declare const CONNECT_ESTABLISHED = "HTTP/1.1 200 Connection Established\r\n\r\n";
32
+ /** What one terminated connection did so far (R7.3): requests read, credentials set (ids). */
33
+ export interface EgressTally {
34
+ requests: number;
35
+ readonly credentialIds: Set<string>;
36
+ }
30
37
  export interface CredentialTerminator {
31
38
  /**
32
39
  * Answers the CONNECT, terminates TLS for `host` and serves its requests, sending each to one
@@ -39,6 +46,8 @@ export interface CredentialTerminator {
39
46
  address: string;
40
47
  family: number;
41
48
  }[];
49
+ /** Counted as requests are served, read when the connection closes. */
50
+ readonly tally: EgressTally;
42
51
  }): Promise<void>;
43
52
  }
44
53
  export declare function credentialTerminator(options: {
@@ -12,10 +12,14 @@
12
12
  * Nothing here knows a tool: the header name and its format are the credential's. A release the
13
13
  * vault refuses (two secrets for one host, an unreadable value) answers 502 and is logged, never
14
14
  * as an event; a value never is. Upgrades (WebSocket) and HTTP/2 are not served on this path.
15
+ * Each connection keeps a tally for its `egress.connection` event (R7.3): how many requests it
16
+ * read and which credentials it set, by id.
15
17
  */
16
18
  import { createServer } from "node:http";
17
19
  import { Agent, request } from "node:https";
18
20
  import { createSecureContext, TLSSocket } from "node:tls";
21
+ /** The answer to a CONNECT the gate serves; a tunnel's byte counts leave it out. */
22
+ export const CONNECT_ESTABLISHED = "HTTP/1.1 200 Connection Established\r\n\r\n";
19
23
  /** Headers that describe one hop and are never forwarded. */
20
24
  const HOP_BY_HOP = new Set([
21
25
  "connection",
@@ -74,6 +78,7 @@ export function credentialTerminator(options) {
74
78
  return;
75
79
  }
76
80
  const { sandboxId, host } = at;
81
+ at.tally.requests += 1;
77
82
  const release = await credentials.release(sandboxId, host);
78
83
  if (release.status === "refused") {
79
84
  logger.warn("egress_credential_refused", { status: 502, reason: release.reason, sandboxId, host });
@@ -82,8 +87,10 @@ export function credentialTerminator(options) {
82
87
  }
83
88
  const header = release.status === "released" ? release.header.toLowerCase() : undefined;
84
89
  const headers = forwardable(req.rawHeaders, header);
85
- if (release.status === "released")
90
+ if (release.status === "released") {
86
91
  headers[header] = [release.value];
92
+ at.tally.credentialIds.add(release.credentialId);
93
+ }
87
94
  const upstream = request({
88
95
  host,
89
96
  port: credentials.upstreamPort ?? 443,
@@ -111,7 +118,7 @@ export function credentialTerminator(options) {
111
118
  }
112
119
  return {
113
120
  async terminate(client, target) {
114
- const { sandboxId, host, addresses } = target;
121
+ const { sandboxId, host, addresses, tally } = target;
115
122
  const leaf = await credentials.leaf(host);
116
123
  if (client.destroyed)
117
124
  return;
@@ -128,13 +135,13 @@ export function credentialTerminator(options) {
128
135
  lookup,
129
136
  ...(credentials.upstreamCa ? { ca: credentials.upstreamCa } : {}),
130
137
  });
131
- client.write("HTTP/1.1 200 Connection Established\r\n\r\n");
138
+ client.write(CONNECT_ESTABLISHED);
132
139
  const tls = new TLSSocket(client, {
133
140
  isServer: true,
134
141
  secureContext: createSecureContext({ key: leaf.key, cert: leaf.cert }),
135
142
  ALPNProtocols: ["http/1.1"],
136
143
  });
137
- terminated.set(tls, { sandboxId, host, agent });
144
+ terminated.set(tls, { sandboxId, host, agent, tally });
138
145
  tls.on("error", () => tls.destroy());
139
146
  tls.on("close", () => {
140
147
  agent.destroy();
@@ -1,5 +1,6 @@
1
1
  import { type Server } from "node:http";
2
2
  import { type Socket } from "node:net";
3
+ import type { EventPayload, EventWriter } from "@nylorun/core/contracts";
3
4
  import type { EgressTokenVerdict } from "../sandbox/egress-token.js";
4
5
  import type { SessionStore } from "../store/types.js";
5
6
  import type { Logger } from "../tenant/types.js";
@@ -15,6 +16,22 @@ export interface EgressSandboxes {
15
16
  readonly allow: readonly string[];
16
17
  } | undefined>;
17
18
  }
19
+ /** The session and turn holding an Environment: turns are serial per Environment, so one at most. */
20
+ export interface EgressHolder {
21
+ readonly sessionId: string;
22
+ readonly turnId: string;
23
+ }
24
+ /** One connection's record (R7.3): `egress.connection`'s payload, `environment.egress`'s too. */
25
+ export type EgressConnection = EventPayload<"egress.connection">;
26
+ /** Where egress-gate records connections (R7.3). */
27
+ export interface EgressRecord {
28
+ /** The session whose turn holds the Environment now; `undefined` between turns. */
29
+ holder(environmentId: string): Promise<EgressHolder | undefined>;
30
+ /** Writes `connection` to `holder`'s session log, or to the Environment's stream without one. */
31
+ write(holder: EgressHolder | undefined, connection: EgressConnection): Promise<void>;
32
+ }
33
+ /** egress-gate as the writer of its events (R4.9): the gate saw the connection itself. */
34
+ export declare const EGRESS_WRITER: EventWriter;
18
35
  /** Ports a tunnel may open. */
19
36
  export declare const EGRESS_PORTS: readonly number[];
20
37
  /** Tunnels one sandbox may hold open at once. */
@@ -49,6 +66,8 @@ export interface StartEgressGateOptions {
49
66
  readonly dial?: (address: string, port: number) => Socket;
50
67
  /** The vault and keys, for hosts an `environment_secret` is bound to (R2c). None: tunnels only. */
51
68
  readonly credentials?: EgressCredentials;
69
+ /** Where connections are recorded (R7.3). None: they are only logged. */
70
+ readonly record?: EgressRecord;
52
71
  }
53
72
  export interface EgressGate {
54
73
  /** Where the gate listens, e.g. `http://0.0.0.0:4200`. */
@@ -70,6 +89,14 @@ export declare function startEgressGate(options: StartEgressGateOptions): Promis
70
89
  /**
71
90
  * `EgressSandboxes` over the Tenant's `sandbox_resources` (one indexed read): a pod sandbox's
72
91
  * host epoch, the pod that joined at it and its spec's allowlist; nothing for a virtual
73
- * sandbox, or one deleted, lost or expired, as `staleHost` judges a host token.
92
+ * Environment, or one deleted or lost, as `staleHost` judges a host token.
74
93
  */
75
94
  export declare function storeEgressSandboxes(store: SessionStore): EgressSandboxes;
95
+ /**
96
+ * `EgressRecord` over the Tenant's record (R7.3). The holder is the session on the Environment
97
+ * whose body has an `activeTurnId` (turns are serial per Environment). With a holder, the write
98
+ * is `egress.connection` on its session log, at that turn, under the session row's lock; without
99
+ * one, `environment.egress` on the Environment's stream, under its row's lock. A deleted
100
+ * Environment gets nothing.
101
+ */
102
+ export declare function storeEgressRecord(store: SessionStore): EgressRecord;