@nylorun/runtime 0.15.1-beta → 0.16.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 (220) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/README.md +33 -6
  3. package/dist/adapters/execution/restate.d.ts +9 -1
  4. package/dist/adapters/execution/restate.js +64 -5
  5. package/dist/adapters/replication/pgoutput.js +3 -1
  6. package/dist/adapters/sandbox/local.d.ts +10 -0
  7. package/dist/adapters/sandbox/local.js +195 -0
  8. package/dist/adapters/sandbox/virtual.js +36 -1
  9. package/dist/admin-openapi.json +37 -3
  10. package/dist/api/a2a/tasks.js +6 -1
  11. package/dist/api/ag-ui/history.js +6 -1
  12. package/dist/api/ag-ui/routes.js +5 -1
  13. package/dist/api/components.d.ts +2673 -655
  14. package/dist/api/components.js +17 -1
  15. package/dist/api/gate/keys.d.ts +4 -3
  16. package/dist/api/gate/routes.d.ts +19 -3
  17. package/dist/api/gate/routes.js +162 -53
  18. package/dist/api/http/app.js +4 -0
  19. package/dist/api/http/define.d.ts +10 -0
  20. package/dist/api/http/define.js +2 -1
  21. package/dist/api/http/routes/artifacts.d.ts +11 -0
  22. package/dist/api/http/routes/artifacts.js +518 -0
  23. package/dist/api/http/routes/sandboxes.d.ts +9 -0
  24. package/dist/api/http/routes/sandboxes.js +152 -0
  25. package/dist/api/http/routes/sessions.js +3 -1
  26. package/dist/api/http/routes/tenant.js +36 -2
  27. package/dist/artifacts/config.d.ts +23 -0
  28. package/dist/artifacts/config.js +27 -0
  29. package/dist/artifacts/export.d.ts +8 -0
  30. package/dist/artifacts/export.js +205 -0
  31. package/dist/artifacts/files.d.ts +36 -0
  32. package/dist/artifacts/files.js +45 -0
  33. package/dist/artifacts/folders.d.ts +60 -0
  34. package/dist/artifacts/folders.js +209 -0
  35. package/dist/artifacts/links.d.ts +28 -0
  36. package/dist/artifacts/links.js +92 -0
  37. package/dist/artifacts/media-types.d.ts +13 -0
  38. package/dist/artifacts/media-types.js +80 -0
  39. package/dist/artifacts/parts.d.ts +34 -0
  40. package/dist/artifacts/parts.js +38 -0
  41. package/dist/artifacts/service.d.ts +74 -0
  42. package/dist/artifacts/service.js +301 -0
  43. package/dist/artifacts/workspace.d.ts +60 -0
  44. package/dist/artifacts/workspace.js +57 -0
  45. package/dist/artifacts/zip.d.ts +10 -0
  46. package/dist/artifacts/zip.js +126 -0
  47. package/dist/blob/fs.d.ts +13 -0
  48. package/dist/blob/fs.js +194 -0
  49. package/dist/blob/index.d.ts +9 -0
  50. package/dist/blob/index.js +9 -0
  51. package/dist/blob/s3.d.ts +42 -0
  52. package/dist/blob/s3.js +366 -0
  53. package/dist/blob/types.d.ts +108 -0
  54. package/dist/blob/types.js +85 -0
  55. package/dist/core/sandbox-routes.d.ts +2 -2
  56. package/dist/core/sandbox-routes.js +4 -3
  57. package/dist/execution/memory.d.ts +11 -1
  58. package/dist/execution/memory.js +77 -3
  59. package/dist/execution/types.d.ts +54 -1
  60. package/dist/execution/types.js +31 -0
  61. package/dist/gates/contract.d.ts +14 -13
  62. package/dist/gates/contract.js +13 -12
  63. package/dist/gates/egress.d.ts +72 -0
  64. package/dist/gates/egress.js +319 -0
  65. package/dist/gates/handler.d.ts +3 -0
  66. package/dist/gates/handler.js +3 -2
  67. package/dist/gates/http-client.d.ts +3 -2
  68. package/dist/gates/http-client.js +17 -6
  69. package/dist/gates/in-process.d.ts +13 -1
  70. package/dist/gates/in-process.js +11 -0
  71. package/dist/gates/inflight.d.ts +23 -4
  72. package/dist/gates/inflight.js +18 -4
  73. package/dist/gates/model-gate.d.ts +6 -2
  74. package/dist/gates/tool-calls.d.ts +9 -5
  75. package/dist/gates/tool-calls.js +6 -6
  76. package/dist/gates/tool-client.d.ts +9 -2
  77. package/dist/gates/tool-client.js +56 -22
  78. package/dist/gates/tool-contract.d.ts +12 -7
  79. package/dist/gates/tool-contract.js +10 -4
  80. package/dist/gates/tool-gate.d.ts +1 -0
  81. package/dist/harness/calls.d.ts +52 -0
  82. package/dist/harness/calls.js +113 -0
  83. package/dist/harness/executors.d.ts +22 -0
  84. package/dist/harness/executors.js +96 -0
  85. package/dist/harness/main.d.ts +6 -0
  86. package/dist/harness/main.js +103 -0
  87. package/dist/harness/pod.d.ts +21 -0
  88. package/dist/harness/pod.js +119 -0
  89. package/dist/harness/run-tokens.d.ts +18 -0
  90. package/dist/harness/run-tokens.js +15 -0
  91. package/dist/harness/service.d.ts +46 -0
  92. package/dist/harness/service.js +179 -0
  93. package/dist/harness/ws-client.d.ts +43 -0
  94. package/dist/harness/ws-client.js +122 -0
  95. package/dist/harness/ws-port.d.ts +22 -0
  96. package/dist/harness/ws-port.js +78 -0
  97. package/dist/harness-api/in-process.d.ts +24 -0
  98. package/dist/harness-api/in-process.js +119 -0
  99. package/dist/harness-api/record.d.ts +44 -0
  100. package/dist/harness-api/record.js +250 -0
  101. package/dist/harness-api/renew.d.ts +37 -0
  102. package/dist/harness-api/renew.js +77 -0
  103. package/dist/harness-api/server.d.ts +104 -0
  104. package/dist/harness-api/server.js +408 -0
  105. package/dist/harness-api/start.d.ts +36 -0
  106. package/dist/harness-api/start.js +118 -0
  107. package/dist/harness-api/workspace.d.ts +53 -0
  108. package/dist/harness-api/workspace.js +201 -0
  109. package/dist/harness-api/ws-server.d.ts +34 -0
  110. package/dist/harness-api/ws-server.js +227 -0
  111. package/dist/host/app.js +23 -2
  112. package/dist/host/gates.d.ts +27 -2
  113. package/dist/host/gates.js +46 -1
  114. package/dist/host/http.d.ts +4 -1
  115. package/dist/host/http.js +6 -1
  116. package/dist/host/main.js +135 -27
  117. package/dist/host/stack-config.d.ts +101 -1
  118. package/dist/host/stack-config.js +199 -8
  119. package/dist/mcp/snapshot.d.ts +8 -1
  120. package/dist/mcp/snapshot.js +28 -0
  121. package/dist/media.d.ts +5 -24
  122. package/dist/media.js +5 -0
  123. package/dist/model/pi-model.d.ts +6 -2
  124. package/dist/model/pi-model.js +39 -22
  125. package/dist/node/index.d.ts +1 -1
  126. package/dist/node/index.js +1 -1
  127. package/dist/openapi.json +15245 -8850
  128. package/dist/record/index.d.ts +1 -0
  129. package/dist/record/index.js +1 -0
  130. package/dist/record/sandbox.d.ts +25 -0
  131. package/dist/record/sandbox.js +43 -0
  132. package/dist/sandbox/capability.d.ts +3 -0
  133. package/dist/sandbox/capability.js +10 -0
  134. package/dist/sandbox/egress-token.d.ts +26 -0
  135. package/dist/sandbox/egress-token.js +8 -0
  136. package/dist/sandbox/join.d.ts +37 -0
  137. package/dist/sandbox/join.js +165 -0
  138. package/dist/sandbox/manager.d.ts +99 -34
  139. package/dist/sandbox/manager.js +216 -84
  140. package/dist/sandbox/placement.d.ts +27 -0
  141. package/dist/sandbox/placement.js +44 -0
  142. package/dist/sandbox/pods/client.d.ts +90 -0
  143. package/dist/sandbox/pods/client.js +79 -0
  144. package/dist/sandbox/pods/lifecycle.d.ts +91 -0
  145. package/dist/sandbox/pods/lifecycle.js +178 -0
  146. package/dist/sandbox/pods/name.d.ts +1 -0
  147. package/dist/sandbox/pods/name.js +28 -0
  148. package/dist/sandbox/pods/network-gate.d.ts +25 -0
  149. package/dist/sandbox/pods/network-gate.js +71 -0
  150. package/dist/sandbox/pods/reconcile.d.ts +11 -0
  151. package/dist/sandbox/pods/reconcile.js +152 -0
  152. package/dist/sandbox/pods/spec.d.ts +45 -0
  153. package/dist/sandbox/pods/spec.js +62 -0
  154. package/dist/sandbox/records.d.ts +29 -0
  155. package/dist/sandbox/records.js +79 -0
  156. package/dist/sandbox/resolve.d.ts +5 -0
  157. package/dist/sandbox/resolve.js +3 -2
  158. package/dist/sandbox/session-sandbox.js +7 -5
  159. package/dist/sandbox/share.d.ts +11 -0
  160. package/dist/sandbox/share.js +10 -0
  161. package/dist/sandbox/tenant-config.d.ts +16 -1
  162. package/dist/sandbox/tenant-config.js +9 -0
  163. package/dist/sandbox/types.d.ts +29 -1
  164. package/dist/sandbox/types.js +13 -1
  165. package/dist/store/postgres/drizzle/0004_sandbox_resources.sql +20 -0
  166. package/dist/store/postgres/drizzle/0005_artifacts.sql +27 -0
  167. package/dist/store/postgres/drizzle/0006_folder_artifacts.sql +12 -0
  168. package/dist/store/postgres/drizzle/0007_sandbox_pods.sql +14 -0
  169. package/dist/store/postgres/drizzle/meta/_journal.json +28 -0
  170. package/dist/store/postgres/record-writer.d.ts +6 -1
  171. package/dist/store/postgres/record-writer.js +20 -2
  172. package/dist/store/postgres/schema.d.ts +894 -0
  173. package/dist/store/postgres/schema.js +130 -2
  174. package/dist/store/postgres/store.js +289 -4
  175. package/dist/store/types.d.ts +143 -6
  176. package/dist/streams/control.d.ts +2 -0
  177. package/dist/streams/control.js +5 -0
  178. package/dist/streams/types.d.ts +10 -1
  179. package/dist/tenant/advance.js +218 -160
  180. package/dist/tenant/artifact-tool.d.ts +5 -0
  181. package/dist/tenant/artifact-tool.js +119 -0
  182. package/dist/tenant/commands.js +30 -5
  183. package/dist/tenant/context.d.ts +64 -5
  184. package/dist/tenant/effects.d.ts +19 -13
  185. package/dist/tenant/effects.js +5 -382
  186. package/dist/tenant/history.d.ts +13 -23
  187. package/dist/tenant/history.js +14 -62
  188. package/dist/tenant/host-token.d.ts +92 -0
  189. package/dist/tenant/host-token.js +179 -0
  190. package/dist/tenant/module.js +1 -0
  191. package/dist/tenant/paths.d.ts +2 -1
  192. package/dist/tenant/paths.js +3 -1
  193. package/dist/tenant/reset.d.ts +5 -2
  194. package/dist/tenant/reset.js +22 -2
  195. package/dist/tenant/run-grants.d.ts +49 -0
  196. package/dist/tenant/run-grants.js +62 -0
  197. package/dist/tenant/run-token.d.ts +70 -0
  198. package/dist/tenant/run-token.js +138 -0
  199. package/dist/tenant/runtime.d.ts +38 -0
  200. package/dist/tenant/runtime.js +120 -32
  201. package/dist/tenant/sandboxes.d.ts +119 -0
  202. package/dist/tenant/sandboxes.js +551 -0
  203. package/dist/tenant/session.d.ts +2 -13
  204. package/dist/tenant/session.js +4 -28
  205. package/dist/tenant/sessions.d.ts +5 -0
  206. package/dist/tenant/sessions.js +28 -1
  207. package/dist/tenant/status.d.ts +8 -3
  208. package/dist/tenant/status.js +5 -0
  209. package/dist/tenant/streams.d.ts +7 -0
  210. package/dist/tenant/streams.js +24 -1
  211. package/dist/tenant/sweep.js +37 -4
  212. package/dist/tenant/tokens.js +9 -2
  213. package/dist/tenant/types.d.ts +18 -1
  214. package/dist/tenant/worker.d.ts +11 -5
  215. package/dist/tenant/worker.js +12 -6
  216. package/dist/version.d.ts +1 -1
  217. package/dist/version.js +1 -1
  218. package/package.json +13 -10
  219. package/dist/adapters/media.d.ts +0 -24
  220. package/dist/adapters/media.js +0 -111
@@ -0,0 +1,152 @@
1
+ import { z } from "zod";
2
+ import { isSandboxId, PutSandboxRequestSchema } from "@nylorun/core/contracts";
3
+ import { DeleteSandboxResponse, ListSandboxEventsResponse, ListSandboxesResponse, PutSandboxRequest, SandboxView, } from "../../components.js";
4
+ import { fail } from "../../../tenant/http.js";
5
+ import { deleteSandbox, getSandbox, listSandboxes, putSandbox, resetSandbox, sandboxEventsOf, stopSandbox, } from "../../../tenant/sandboxes.js";
6
+ import { readJson } from "../body.js";
7
+ import { tenantRoute } from "../define.js";
8
+ import { jsonResponse } from "../respond.js";
9
+ /** Reading a sandbox: any caller that may use sessions, or change sandboxes. */
10
+ const READ = {
11
+ credentials: ["application", "subject", "token"],
12
+ scopes: ["sessions:own", "sandboxes:write"],
13
+ browser: true,
14
+ };
15
+ /** Creating or deleting one. */
16
+ const WRITE = {
17
+ credentials: ["application", "subject", "token"],
18
+ scopes: ["sandboxes:write"],
19
+ browser: true,
20
+ };
21
+ const json = (schema, description) => ({
22
+ description,
23
+ content: { "application/json": { schema } },
24
+ });
25
+ const sandboxId = z.object({
26
+ sandboxId: z
27
+ .string()
28
+ .meta({ description: "The sandbox id, percent-encoded: `team-a%2Fproj-42` for `team-a/proj-42`" }),
29
+ });
30
+ /** The path's sandbox id, or a 400 naming the rule. */
31
+ function idOf(value) {
32
+ if (!isSandboxId(value))
33
+ return fail(400, "A sandbox id is up to 200 characters: /-separated segments of letters, digits, '.', '_' and '-'");
34
+ return value;
35
+ }
36
+ /** `label=k=v`, repeated: every one must match. */
37
+ function labelsOf(values) {
38
+ const labels = {};
39
+ for (const value of values ?? []) {
40
+ const at = value.indexOf("=");
41
+ if (at <= 0)
42
+ fail(400, `label must be key=value, not ${value}`);
43
+ labels[value.slice(0, at)] = value.slice(at + 1);
44
+ }
45
+ return labels;
46
+ }
47
+ export function sandboxRoutes(api) {
48
+ tenantRoute(api, READ, {
49
+ method: "get",
50
+ path: "/v1/sandboxes",
51
+ tags: ["Sandboxes"],
52
+ summary: "List sandboxes",
53
+ description: "Every sandbox with all the labels asked for. A subject token sees only the sandboxes its grants reach.",
54
+ request: {
55
+ query: z.object({
56
+ label: z
57
+ .union([z.string(), z.array(z.string())])
58
+ .optional()
59
+ .meta({ description: "`key=value`; repeat it to require several labels" }),
60
+ }),
61
+ },
62
+ responses: { 200: json(ListSandboxesResponse, "The sandboxes") },
63
+ }, async (c) => jsonResponse(200, await listSandboxes(c.env.tenant, labelsOf(c.req.queries("label")), c.get("scope"))));
64
+ tenantRoute(api, WRITE, {
65
+ method: "put",
66
+ path: "/v1/sandboxes/{sandboxId}",
67
+ tags: ["Sandboxes"],
68
+ summary: "Create or find a sandbox",
69
+ description: "Creates the sandbox within the Tenant's limits, or answers the one with this id, so get-or-create is one call. Its spec is fixed once it exists; labels, when sent, replace its labels. Kind `pod` needs sandbox pods (`nylorun sandbox enable`): it is created at once, and a PUT on an existing pod sandbox starts it again when it was stopped, or revives it with a longer `lifecycle.ttl`.",
70
+ request: {
71
+ params: sandboxId,
72
+ body: { required: true, content: { "application/json": { schema: PutSandboxRequest } } },
73
+ },
74
+ responses: {
75
+ 200: json(SandboxView, "The sandbox"),
76
+ 409: {
77
+ description: "A sandbox with this id has another spec, the Tenant holds as many sandboxes as it allows (`limit_exceeded`), kind pod without sandbox pods (`sandbox_unavailable`), or a lost pod sandbox (`sandbox_lost`)",
78
+ },
79
+ },
80
+ }, async (c) => {
81
+ const id = idOf(c.req.param("sandboxId"));
82
+ const body = PutSandboxRequestSchema.parse(await readJson(c.req.raw));
83
+ return jsonResponse(200, await putSandbox(c.env.tenant, id, body, c.get("scope")));
84
+ });
85
+ tenantRoute(api, READ, {
86
+ method: "get",
87
+ path: "/v1/sandboxes/{sandboxId}",
88
+ tags: ["Sandboxes"],
89
+ summary: "Get a sandbox",
90
+ description: "Its spec, labels, state and the sessions attached to it (acting for a person, only theirs).",
91
+ request: { params: sandboxId },
92
+ responses: { 200: json(SandboxView, "The sandbox") },
93
+ }, async (c) => jsonResponse(200, await getSandbox(c.env.tenant, idOf(c.req.param("sandboxId")), c.get("scope"))));
94
+ tenantRoute(api, READ, {
95
+ method: "get",
96
+ path: "/v1/sandboxes/{sandboxId}/events",
97
+ tags: ["Sandboxes"],
98
+ summary: "Read a sandbox's lifecycle events",
99
+ description: "`sandbox.created`, `sandbox.attached`, `sandbox.detached` and `sandbox.deleted`, in order, numbered from 0.",
100
+ request: {
101
+ params: sandboxId,
102
+ query: z.object({
103
+ from: z.string().optional().meta({ description: "The first `seq` to return" }),
104
+ }),
105
+ },
106
+ responses: { 200: json(ListSandboxEventsResponse, "The events") },
107
+ }, async (c) => {
108
+ const from = c.req.query("from");
109
+ if (from !== undefined && !/^\d+$/.test(from))
110
+ fail(400, "from must be a seq");
111
+ return jsonResponse(200, await sandboxEventsOf(c.env.tenant, idOf(c.req.param("sandboxId")), c.get("scope"), from === undefined ? undefined : Number(from)));
112
+ });
113
+ tenantRoute(api, WRITE, {
114
+ method: "delete",
115
+ path: "/v1/sandboxes/{sandboxId}",
116
+ tags: ["Sandboxes"],
117
+ summary: "Delete a sandbox",
118
+ description: "Deletes the sandbox and its files. Sessions attached to it stay attached by id; their next turn is refused until a sandbox with this id exists again.",
119
+ request: { params: sandboxId },
120
+ responses: {
121
+ 200: json(DeleteSandboxResponse, "Deleted, or there was none"),
122
+ 409: { description: "A session attached to it has a turn running (`sandbox_busy`)" },
123
+ },
124
+ }, async (c) => jsonResponse(200, await deleteSandbox(c.env.tenant, idOf(c.req.param("sandboxId")), c.get("scope"))));
125
+ for (const [action, summary, description, run] of [
126
+ [
127
+ "stop",
128
+ "Stop a pod sandbox",
129
+ "Suspends the pod: its volume is kept, and the next turn of a session attached to it (or a PUT) starts it again. Refused during a turn (`sandbox_busy`).",
130
+ stopSandbox,
131
+ ],
132
+ [
133
+ "reset",
134
+ "Reset a pod sandbox",
135
+ "A new pod on a new, empty volume; the old pod and volume are deleted. The way out of `sandbox_lost`. Refused during a turn (`sandbox_busy`).",
136
+ resetSandbox,
137
+ ],
138
+ ])
139
+ tenantRoute(api, WRITE, {
140
+ method: "post",
141
+ path: `/v1/sandboxes/{sandboxId}/${action}`,
142
+ tags: ["Sandboxes"],
143
+ summary,
144
+ description,
145
+ request: { params: sandboxId },
146
+ responses: {
147
+ 200: json(SandboxView, "The sandbox"),
148
+ 400: { description: "The sandbox is virtual" },
149
+ 409: { description: "A turn holds it (`sandbox_busy`), or no sandbox pods (`sandbox_unavailable`)" },
150
+ },
151
+ }, async (c) => jsonResponse(200, await run(c.env.tenant, idOf(c.req.param("sandboxId")), c.get("scope"))));
152
+ }
@@ -7,6 +7,7 @@ import { sandboxRouteDeps } from "../../../tenant/actions.js";
7
7
  import { accessOf, requireApplication } from "../../../tenant/auth.js";
8
8
  import { loadSession, sessionOf } from "../../../tenant/context.js";
9
9
  import { fail } from "../../../tenant/http.js";
10
+ import { sandboxGrantsOf } from "../../../tenant/sandboxes.js";
10
11
  import { readHistory, requestCursor, streamSessionEvents } from "../../../tenant/session-streams.js";
11
12
  import { listAgentsPublic, listDefinitions, listSessions, putDefinition, putSession, sessionView, } from "../../../tenant/sessions.js";
12
13
  import { readJson } from "../body.js";
@@ -107,7 +108,8 @@ export function sessionRoutes(api) {
107
108
  // Agent code may trust `info`: only an app server sets it.
108
109
  if (scope.kind === "token" && request.info !== undefined)
109
110
  fail(403, "A subject token cannot set session info", { code: "scope_required" });
110
- const session = await putSession(ctx, c.req.param("sessionId"), request, accessOf(scope));
111
+ const grants = sandboxGrantsOf(scope);
112
+ const session = await putSession(ctx, c.req.param("sessionId"), request, accessOf(scope), grants === undefined ? {} : { sandboxGrants: grants });
111
113
  return jsonResponse(200, await ctx.store.tx((t) => sessionView(t, session)));
112
114
  });
113
115
  tenantRoute(api, OWN_SESSIONS, {
@@ -10,11 +10,12 @@ import { clearObservers } from "../../../tenant/session-streams.js";
10
10
  import { usesFixtureModel } from "../../../tenant/model-setting.js";
11
11
  import { resetTenant } from "../../../tenant/reset.js";
12
12
  import { clearWork, drain } from "../../../tenant/scheduler.js";
13
- import { buildTenantStatus, seedTenantConfig } from "../../../tenant/status.js";
13
+ import { buildTenantStatus, harnessStatusOf, seedTenantConfig } from "../../../tenant/status.js";
14
14
  import { tenantReset, streamsStatus, } from "../../../tenant/streams.js";
15
15
  import { readJson } from "../body.js";
16
16
  import { pathSegments, tenantRoute } from "../define.js";
17
17
  import { jsonResponse } from "../respond.js";
18
+ import { rearmPodTimers } from "../../../tenant/sandboxes.js";
18
19
  const settings = (scopes) => ({
19
20
  credentials: scopes === "never" ? ["application"] : ["application", "subject"],
20
21
  scopes,
@@ -46,7 +47,36 @@ const budgetView = (row) => ({
46
47
  /** `GET /v1/tenant/sandbox`: the backend report and the configuration with defaults applied. */
47
48
  async function sandboxView(ctx) {
48
49
  const config = await ctx.store.tx((t) => readSandboxConfig(t));
49
- return { ...(await ctx.sandbox.report()), config: effectiveSandboxConfig(config) };
50
+ return {
51
+ ...(await ctx.sandbox.report()),
52
+ config: effectiveSandboxConfig(config),
53
+ cluster: await clusterOf(ctx),
54
+ };
55
+ }
56
+ /** The Tenant's cluster for pod sandboxes (F7.2), from the sandboxes service; null without one. */
57
+ async function clusterOf(ctx) {
58
+ if (!ctx.pods)
59
+ return null;
60
+ try {
61
+ const [info, ready] = await Promise.all([ctx.pods.client.info(), ctx.pods.client.ready()]);
62
+ return {
63
+ namespace: info.namespace,
64
+ context: info.context,
65
+ ready,
66
+ ...(info.controllerVersion ? { controllerVersion: info.controllerVersion } : {}),
67
+ ...(info.networkPolicy
68
+ ? {
69
+ networkPolicy: {
70
+ enforced: info.networkPolicy.enforced === true,
71
+ ...(info.networkPolicy.probedAt ? { probedAt: info.networkPolicy.probedAt } : {}),
72
+ },
73
+ }
74
+ : {}),
75
+ };
76
+ }
77
+ catch {
78
+ return null;
79
+ }
50
80
  }
51
81
  export function tenantRoutes(api) {
52
82
  tenantRoute(api, SETTINGS, {
@@ -70,6 +100,7 @@ export function tenantRoutes(api) {
70
100
  (await vault.getHostModel()).configured,
71
101
  ...(ctx.stuckInvocations ? { stuckInvocations: ctx.stuckInvocations } : {}),
72
102
  streamsStatus: () => streamsStatus(ctx),
103
+ harness: harnessStatusOf(ctx),
73
104
  }));
74
105
  });
75
106
  tenantRoute(api, settings("never"), {
@@ -91,6 +122,7 @@ export function tenantRoutes(api) {
91
122
  store: ctx.store,
92
123
  sandbox: ctx.sandbox,
93
124
  paths: ctx.config.paths,
125
+ blobs: ctx.blobs,
94
126
  clearSessionState: () => {
95
127
  clearWork(ctx);
96
128
  clearObservers(ctx.sessionStreams);
@@ -147,6 +179,8 @@ export function tenantRoutes(api) {
147
179
  if (errors.length > 0)
148
180
  fail(400, errors.join(" "));
149
181
  await ctx.store.tx((t) => writeSandboxConfig(t, config));
182
+ // Pods' idle timers follow the new limits.idle (D36).
183
+ await rearmPodTimers(ctx);
150
184
  return jsonResponse(200, await sandboxView(ctx));
151
185
  });
152
186
  tenantRoute(api, SETTINGS, {
@@ -0,0 +1,23 @@
1
+ /**
2
+ * A Tenant's artifact configuration, stored as Tenant setting `artifacts.config`: the largest
3
+ * file one upload may store and the most bytes every artifact version together may hold.
4
+ * Unset limits take the defaults (100 MiB and 10 GiB).
5
+ */
6
+ import { type TenantArtifactsConfig } from "@nylorun/core/contracts";
7
+ export declare const ARTIFACTS_CONFIG_SETTING = "artifacts.config";
8
+ export interface ArtifactLimits {
9
+ readonly fileBytes: number;
10
+ readonly totalBytes: number;
11
+ }
12
+ interface SettingsReader {
13
+ getSetting(key: string): Promise<string | undefined>;
14
+ }
15
+ interface SettingsWriter extends SettingsReader {
16
+ putSetting(key: string, value: string): Promise<void>;
17
+ }
18
+ /** The limits in force: the Tenant's, with the defaults for what it leaves unset. */
19
+ export declare function effectiveArtifactLimits(config: TenantArtifactsConfig): ArtifactLimits;
20
+ export declare function readArtifactsConfig(t: SettingsReader): Promise<TenantArtifactsConfig>;
21
+ export declare function readArtifactLimits(t: SettingsReader): Promise<ArtifactLimits>;
22
+ export declare function writeArtifactsConfig(t: SettingsWriter, config: TenantArtifactsConfig): Promise<void>;
23
+ export {};
@@ -0,0 +1,27 @@
1
+ /**
2
+ * A Tenant's artifact configuration, stored as Tenant setting `artifacts.config`: the largest
3
+ * file one upload may store and the most bytes every artifact version together may hold.
4
+ * Unset limits take the defaults (100 MiB and 10 GiB).
5
+ */
6
+ import { ARTIFACT_FILE_BYTES_DEFAULT, ARTIFACT_TOTAL_BYTES_DEFAULT, TenantArtifactsConfigSchema, } from "@nylorun/core/contracts";
7
+ export const ARTIFACTS_CONFIG_SETTING = "artifacts.config";
8
+ /** The limits in force: the Tenant's, with the defaults for what it leaves unset. */
9
+ export function effectiveArtifactLimits(config) {
10
+ return {
11
+ fileBytes: config.limits?.fileBytes ?? ARTIFACT_FILE_BYTES_DEFAULT,
12
+ totalBytes: config.limits?.totalBytes ?? ARTIFACT_TOTAL_BYTES_DEFAULT,
13
+ };
14
+ }
15
+ export async function readArtifactsConfig(t) {
16
+ const raw = await t.getSetting(ARTIFACTS_CONFIG_SETTING);
17
+ if (raw === undefined)
18
+ return {};
19
+ const parsed = TenantArtifactsConfigSchema.safeParse(JSON.parse(raw));
20
+ return parsed.success ? parsed.data : {};
21
+ }
22
+ export async function readArtifactLimits(t) {
23
+ return effectiveArtifactLimits(await readArtifactsConfig(t));
24
+ }
25
+ export async function writeArtifactsConfig(t, config) {
26
+ await t.putSetting(ARTIFACTS_CONFIG_SETTING, JSON.stringify(TenantArtifactsConfigSchema.parse(config)));
27
+ }
@@ -0,0 +1,8 @@
1
+ import { type TenantContext } from "../tenant/context.js";
2
+ /** The directory every sandbox's outputs are exported from. */
3
+ export declare const OUTPUTS_DIR = "/workspace/outputs";
4
+ /**
5
+ * Exports the outputs of turn `turnId` of session `sessionId`, if it completed and its sandbox
6
+ * has any. Never throws.
7
+ */
8
+ export declare function exportOutputs(ctx: TenantContext, sessionId: string, turnId: string | null, signal: AbortSignal): Promise<void>;
@@ -0,0 +1,205 @@
1
+ /**
2
+ * The turn-end export (F8.2): when an agent's turn completes, core reads `/workspace/outputs` of
3
+ * the session's sandbox through the `WorkspaceReader` seam and keeps it as a version of the
4
+ * session's folder artifact `outputs` (the first export creates it; later turns add versions,
5
+ * and a turn whose outputs did not change adds none).
6
+ *
7
+ * - **After the turn.** The advance calls `exportOutputs` once `settle` has committed the turn,
8
+ * outside its transaction and while it still holds the session's lease, so the next turn
9
+ * cannot change the workspace under it. Nothing here fails the turn: a sandbox or store
10
+ * failure is logged and recorded as `artifact.export.failed`.
11
+ * - **Claimed.** The listing and the bytes are what the holder of the workspace supplied: the
12
+ * version's source is `export` and its event says `claimed: true`.
13
+ * - **Bounded.** At most `EXPORT_MAX_FILES` files and `EXPORT_MAX_BYTES` bytes per export, each
14
+ * file within the Tenant's per-file limit, and the new bytes within its total; past any of
15
+ * them nothing is stored and `artifact.export.skipped` says why.
16
+ * - **Deduplicated.** Each file is stored content-addressed (`folders.ts`); a file the store
17
+ * already holds is not stored again.
18
+ *
19
+ * No sandbox, a sandbox never created, or an empty or missing `/workspace/outputs` exports
20
+ * nothing and records nothing. The package's declared outputs are left for later: the manifest
21
+ * has no `outputs` field yet.
22
+ */
23
+ import { posix } from "node:path";
24
+ import { EXPORT_MAX_BYTES, EXPORT_MAX_FILES, OUTPUTS_ARTIFACT_NAME, } from "@nylorun/core/contracts";
25
+ import { SANDBOX_WORKSPACE } from "@nylorun/core/define";
26
+ import { isWorkflowManifest } from "../core/flow-host.js";
27
+ import { sandboxWorkspaceOf } from "../sandbox/share.js";
28
+ import { SandboxFileTooLargeError } from "../sandbox/types.js";
29
+ import { sandboxLookup } from "../tenant/context.js";
30
+ import { readArtifactLimits } from "./config.js";
31
+ import { collectContent, commitFolderVersion, contentEpoch, contentKey, encodeManifest, FolderCommitRefused, sha256Hex, } from "./folders.js";
32
+ import { contentTypeFor } from "./media-types.js";
33
+ /** The directory every sandbox's outputs are exported from. */
34
+ export const OUTPUTS_DIR = `${SANDBOX_WORKSPACE}/outputs`;
35
+ /** Stops the export with `artifact.export.skipped`. */
36
+ class Skip extends Error {
37
+ reason;
38
+ limit;
39
+ path;
40
+ constructor(reason, message, limit, path) {
41
+ super(message);
42
+ this.reason = reason;
43
+ this.limit = limit;
44
+ this.path = path;
45
+ }
46
+ }
47
+ /**
48
+ * Exports the outputs of turn `turnId` of session `sessionId`, if it completed and its sandbox
49
+ * has any. Never throws.
50
+ */
51
+ export async function exportOutputs(ctx, sessionId, turnId, signal) {
52
+ try {
53
+ await runExport(ctx, sessionId, turnId, signal);
54
+ }
55
+ catch (error) {
56
+ const message = error instanceof Error ? error.message : String(error);
57
+ if (signal.aborted) {
58
+ ctx.config.logger.info("outputs export stopped", { sessionId, message });
59
+ return;
60
+ }
61
+ const skip = error instanceof Skip
62
+ ? error
63
+ : error instanceof FolderCommitRefused && error.reason === "tenant_total"
64
+ ? new Skip("tenant_total", error.message, error.limit)
65
+ : undefined;
66
+ if (!skip)
67
+ ctx.config.logger.warn("outputs export failed", { sessionId, message });
68
+ try {
69
+ await ctx.store.tx(async (t) => {
70
+ if (skip)
71
+ await t.event(sessionId, turnId, "artifact.export.skipped", {
72
+ name: OUTPUTS_ARTIFACT_NAME,
73
+ reason: skip.reason,
74
+ message: skip.message,
75
+ ...(skip.limit === undefined ? {} : { limit: skip.limit }),
76
+ ...(skip.path === undefined ? {} : { path: skip.path }),
77
+ });
78
+ else
79
+ await t.event(sessionId, turnId, "artifact.export.failed", {
80
+ name: OUTPUTS_ARTIFACT_NAME,
81
+ message,
82
+ });
83
+ });
84
+ }
85
+ catch {
86
+ /* the session is gone (reset) or the store closed */
87
+ }
88
+ }
89
+ }
90
+ async function runExport(ctx, sessionId, turnId, signal) {
91
+ const found = await ctx.store.tx(async (t) => {
92
+ const s = await t.get("sessions", sessionId);
93
+ // Only a turn that completed, and only an agent's (a flow agent's sandbox is its agents').
94
+ if (!s || s.status !== "completed" || s.lastTurnId !== turnId || isWorkflowManifest(s.manifest))
95
+ return undefined;
96
+ const workspace = sandboxWorkspaceOf(s, await sandboxLookup(t, s.id));
97
+ const prior = (await t.listArtifacts({ sessionId })).find((row) => row.kind === "folder" && row.name === OUTPUTS_ARTIFACT_NAME);
98
+ const latest = prior ? await t.artifactVersion(prior.id, prior.latestVersion) : undefined;
99
+ return {
100
+ session: {
101
+ sessionId,
102
+ turnId,
103
+ ownerId: workspace.ownerId,
104
+ ...(workspace.sandboxId === undefined ? {} : { sandboxId: workspace.sandboxId }),
105
+ manifest: s.manifest,
106
+ },
107
+ limits: await readArtifactLimits(t),
108
+ priorId: prior?.id,
109
+ latestSha: latest?.sha256,
110
+ epoch: await contentEpoch(t),
111
+ };
112
+ });
113
+ if (!found)
114
+ return;
115
+ const { session, limits } = found;
116
+ const listing = await ctx.workspaces.list(session, OUTPUTS_DIR, {
117
+ maxEntries: EXPORT_MAX_FILES,
118
+ signal,
119
+ });
120
+ if (!listing || listing.entries.length === 0)
121
+ return;
122
+ if (listing.truncated)
123
+ throw new Skip("too_many_files", `${OUTPUTS_DIR} holds more than ${EXPORT_MAX_FILES} files`, EXPORT_MAX_FILES);
124
+ const large = listing.entries.find((entry) => entry.size > limits.fileBytes);
125
+ if (large)
126
+ throw tooLargeFile(large.path, large.size, limits.fileBytes);
127
+ const maxBytes = Math.min(EXPORT_MAX_BYTES, limits.totalBytes);
128
+ const listed = listing.entries.reduce((sum, entry) => sum + entry.size, 0);
129
+ if (listed > maxBytes)
130
+ throw tooLarge(listed, maxBytes);
131
+ // The files this export stored: removed again if it commits nothing, unless another version
132
+ // names them by then.
133
+ const put = [];
134
+ try {
135
+ await storeAndCommit(ctx, found, listing.entries, maxBytes, put, signal);
136
+ }
137
+ catch (error) {
138
+ if (put.length > 0)
139
+ await ctx.store
140
+ .tx(async (t) => {
141
+ await t.lockArtifactQuota();
142
+ await collectContent(ctx.blobs, t, put);
143
+ })
144
+ .catch(() => undefined);
145
+ throw error;
146
+ }
147
+ }
148
+ async function storeAndCommit(ctx, found, listed, maxBytes, put, signal) {
149
+ const { session, limits } = found;
150
+ const { sessionId, turnId } = session;
151
+ const entries = [];
152
+ const stored = new Set();
153
+ let read = 0;
154
+ for (const entry of listed) {
155
+ let bytes;
156
+ try {
157
+ bytes = await ctx.workspaces.readBytes(session, posix.join(OUTPUTS_DIR, entry.path), {
158
+ maxBytes: limits.fileBytes,
159
+ signal,
160
+ });
161
+ }
162
+ catch (error) {
163
+ if (error instanceof SandboxFileTooLargeError)
164
+ throw tooLargeFile(entry.path, error.size, limits.fileBytes);
165
+ throw error;
166
+ }
167
+ // Gone since the listing: the folder is what is there now.
168
+ if (bytes === undefined)
169
+ continue;
170
+ read += bytes.length;
171
+ if (read > maxBytes)
172
+ throw tooLarge(read, maxBytes);
173
+ const sha256 = sha256Hex(bytes);
174
+ const contentType = contentTypeFor(posix.basename(entry.path));
175
+ if (!stored.has(sha256)) {
176
+ stored.add(sha256);
177
+ const key = contentKey(sha256);
178
+ if (!(await ctx.blobs.head(key))) {
179
+ put.push(sha256);
180
+ await ctx.blobs.put(key, bytes, { contentType, signal });
181
+ }
182
+ }
183
+ entries.push({ path: entry.path, size: bytes.length, sha256, contentType });
184
+ }
185
+ // Every file vanished, or nothing changed since the last export: no new version.
186
+ if (entries.length === 0)
187
+ return;
188
+ if (found.latestSha !== undefined && sha256Hex(encodeManifest(entries)) === found.latestSha)
189
+ return;
190
+ await commitFolderVersion(ctx, {
191
+ sessionId,
192
+ turnId,
193
+ ...(found.priorId === undefined ? {} : { artifactId: found.priorId }),
194
+ name: OUTPUTS_ARTIFACT_NAME,
195
+ entries,
196
+ epoch: found.epoch,
197
+ totalBytes: limits.totalBytes,
198
+ });
199
+ }
200
+ function tooLargeFile(path, size, limit) {
201
+ return new Skip("file_too_large", `${OUTPUTS_DIR}/${path} is ${size} bytes; a file may hold at most ${limit}`, limit, path);
202
+ }
203
+ function tooLarge(bytes, limit) {
204
+ return new Skip("too_large", `${OUTPUTS_DIR} holds ${bytes} bytes; an export stores at most ${limit}`, limit);
205
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * How model-gate reads the files a prompt names (protocol 6): a media part's reference is an
3
+ * artifact version (`./parts.ts`), resolved here to its bytes from the Object store. The gate
4
+ * holds the store credential; the loop and the transcript only ever hold the reference.
5
+ *
6
+ * It replaces `MediaStore`: images are file artifacts, and their bytes reach the provider only in
7
+ * the request model-gate builds.
8
+ */
9
+ import type { JsonValue } from "@nylorun/core/define";
10
+ import type { BlobStore } from "../blob/index.js";
11
+ import type { SessionStore } from "../store/types.js";
12
+ /** A file a prompt names, with its bytes. */
13
+ export interface ResolvedFile {
14
+ readonly name: string;
15
+ /** The version's media type, as stored. */
16
+ readonly mediaType: string;
17
+ readonly bytes: Uint8Array;
18
+ }
19
+ /** Why a file cannot go to the model: the message is the outcome's, so it names no secret. */
20
+ export declare class FileUnavailableError extends Error {
21
+ constructor(message: string);
22
+ }
23
+ /** Resolves a media part's reference to the file's bytes, or throws `FileUnavailableError`. */
24
+ export type FileResolver = (reference: JsonValue) => Promise<ResolvedFile>;
25
+ /**
26
+ * Files from the Tenant's artifacts: `store` for the version's row, `blobs` for its bytes, for
27
+ * the call of session `sessionId`. An artifact of another session is refused (a Tenant-wide one
28
+ * is not), and without a session every file is: the check is never skipped.
29
+ */
30
+ export declare function artifactFiles(options: {
31
+ readonly store: SessionStore;
32
+ readonly blobs: BlobStore;
33
+ /** The session the call is for: the run token's, over HTTP. */
34
+ readonly sessionId: string | undefined;
35
+ readonly maxBytes?: number;
36
+ }): FileResolver;
@@ -0,0 +1,45 @@
1
+ import { MODEL_FILE_MAX_BYTES } from "./media-types.js";
2
+ import { fileReferenceOf } from "./parts.js";
3
+ /** Why a file cannot go to the model: the message is the outcome's, so it names no secret. */
4
+ export class FileUnavailableError extends Error {
5
+ constructor(message) {
6
+ super(message);
7
+ this.name = "FileUnavailableError";
8
+ }
9
+ }
10
+ /**
11
+ * Files from the Tenant's artifacts: `store` for the version's row, `blobs` for its bytes, for
12
+ * the call of session `sessionId`. An artifact of another session is refused (a Tenant-wide one
13
+ * is not), and without a session every file is: the check is never skipped.
14
+ */
15
+ export function artifactFiles(options) {
16
+ const maxBytes = options.maxBytes ?? MODEL_FILE_MAX_BYTES;
17
+ return async (value) => {
18
+ const reference = fileReferenceOf(value);
19
+ if (!reference)
20
+ throw new FileUnavailableError("Expected an artifact reference.");
21
+ const sessionId = options.sessionId;
22
+ if (typeof sessionId !== "string" || sessionId === "")
23
+ throw new FileUnavailableError("A model call reads files only for its own session.");
24
+ const found = await options.store.tx(async (t) => {
25
+ const artifact = await t.artifact(reference.artifactId);
26
+ if (!artifact)
27
+ return undefined;
28
+ const version = await t.artifactVersion(artifact.id, reference.version);
29
+ return version ? { artifact, version } : undefined;
30
+ });
31
+ if (!found ||
32
+ (found.artifact.sessionId !== null && found.artifact.sessionId !== sessionId))
33
+ throw new FileUnavailableError(`Artifact ${reference.artifactId} version ${reference.version} is not available.`);
34
+ const { artifact, version } = found;
35
+ if (artifact.kind !== "file")
36
+ throw new FileUnavailableError(`${artifact.name} is a folder; a model reads files only.`);
37
+ if (version.size > maxBytes)
38
+ throw new FileUnavailableError(`${artifact.name} is ${version.size} bytes; a model reads files of at most ${maxBytes} bytes.`);
39
+ const got = await options.blobs.get(version.blobKey);
40
+ if (!got)
41
+ throw new FileUnavailableError(`The bytes of ${artifact.name} are missing from the Object store.`);
42
+ const bytes = new Uint8Array(await new Response(got.body).arrayBuffer());
43
+ return { name: artifact.name, mediaType: version.contentType, bytes };
44
+ };
45
+ }
@@ -0,0 +1,60 @@
1
+ import type { ArtifactDiff, FolderEntry, FolderManifest } from "@nylorun/core/contracts";
2
+ import type { BlobStore } from "../blob/index.js";
3
+ import type { ArtifactRow, ArtifactVersionRow, Tx } from "../store/types.js";
4
+ import { type TenantContext } from "../tenant/context.js";
5
+ /** Where a folder file's bytes are: content-addressed by their SHA-256. */
6
+ export declare function contentKey(sha256: string): string;
7
+ export declare function sha256Hex(bytes: Uint8Array): string;
8
+ /** Entries in manifest order: by path, comparing UTF-16 code units. */
9
+ export declare function sortEntries(entries: readonly FolderEntry[]): FolderEntry[];
10
+ /** The manifest's bytes, as stored: deterministic for the same entries. */
11
+ export declare function encodeManifest(entries: readonly FolderEntry[]): Uint8Array;
12
+ /** A folder version's manifest, read from the Object store. */
13
+ export declare function readManifest(blobs: BlobStore, version: ArtifactVersionRow): Promise<FolderManifest>;
14
+ /** The entry at `path`, or the 404 of a missing file. */
15
+ export declare function entryAt(manifest: FolderManifest, path: string): FolderEntry;
16
+ /** What changed from `from` (absent: nothing) to `to`. */
17
+ export declare function diffManifests(artifactId: string, from: {
18
+ version: number;
19
+ manifest: FolderManifest;
20
+ } | undefined, to: {
21
+ version: number;
22
+ manifest: FolderManifest;
23
+ }): ArtifactDiff;
24
+ /** The Tenant's content epoch: moves whenever content-addressed files are deleted. */
25
+ export declare function contentEpoch(t: Tx): Promise<number>;
26
+ /**
27
+ * Deletes those of `shas` no committed folder version references any more, and moves the content
28
+ * epoch when it deletes any. Call it in a transaction holding the quota lock, after the rows that
29
+ * named them are gone: the deletes happen before the transaction commits, under the lock.
30
+ */
31
+ export declare function collectContent(blobs: BlobStore, t: Tx, shas: readonly string[]): Promise<number>;
32
+ /** The commit could not go ahead: a limit, or content removed while the export ran. */
33
+ export declare class FolderCommitRefused extends Error {
34
+ readonly reason: "tenant_total" | "content_gone" | "artifact_gone";
35
+ readonly limit?: number | undefined;
36
+ constructor(message: string, reason: "tenant_total" | "content_gone" | "artifact_gone", limit?: number | undefined);
37
+ }
38
+ export interface FolderVersionInput {
39
+ readonly sessionId: string;
40
+ readonly turnId: string | null;
41
+ /** A new version of this folder of the session; otherwise a new folder named `name`. */
42
+ readonly artifactId?: string;
43
+ readonly name: string;
44
+ /** Every file, each one's bytes already at `contentKey(sha256)`. */
45
+ readonly entries: readonly FolderEntry[];
46
+ /** The content epoch read before the first `head` of those bytes. */
47
+ readonly epoch: number;
48
+ /** The Tenant total in force. */
49
+ readonly totalBytes: number;
50
+ }
51
+ /**
52
+ * Commits a folder version whose files are already stored: its manifest to the Object store,
53
+ * then, in one transaction under the session and quota locks, the version row, its content rows
54
+ * and `artifact.created` or `artifact.version.created` (`claimed: true`, source `export`).
55
+ * Throws `FolderCommitRefused` when the Tenant total would be passed or a file is gone.
56
+ */
57
+ export declare function commitFolderVersion(ctx: TenantContext, input: FolderVersionInput): Promise<{
58
+ artifact: ArtifactRow;
59
+ version: ArtifactVersionRow;
60
+ }>;