@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,29 @@
1
+ import type { WorkspaceRecord } from "@nylorun/core/harness-api";
2
+ import type { SessionStore } from "../store/types.js";
3
+ export type SandboxState = WorkspaceRecord["state"];
4
+ /** A workspace's compute record. */
5
+ export type SandboxRecord = WorkspaceRecord;
6
+ /** The SandboxManager's records port. */
7
+ export interface SandboxRecords {
8
+ get(key: string): Promise<SandboxRecord | undefined>;
9
+ put(record: SandboxRecord): Promise<void>;
10
+ delete(key: string): Promise<void>;
11
+ list(): Promise<SandboxRecord[]>;
12
+ count(): Promise<number>;
13
+ }
14
+ /** The workspace key prefix of the Tenant `scope`. */
15
+ export declare function workspacePrefix(scope: string): string;
16
+ /** The key of session `sessionId`'s own workspace. */
17
+ export declare function sessionWorkspaceKey(scope: string, sessionId: string): string;
18
+ /**
19
+ * The key of sandbox resource `sandboxId`'s workspace: apart from every session key (16 hex
20
+ * characters after the prefix). The virtual backend keeps it in `<sandboxes>/<key>/workspace`.
21
+ */
22
+ export declare function sandboxWorkspaceKey(scope: string, sandboxId: string): string;
23
+ /** The records in the Tenant's `sandboxes` table. */
24
+ export declare function storeSandboxRecords(store: SessionStore): SandboxRecords;
25
+ /**
26
+ * The records in one JSON file, rewritten whole (to a temporary file, then renamed) after each
27
+ * change. Writes are serialized; a file that cannot be read starts empty.
28
+ */
29
+ export declare function fileSandboxRecords(path: string): SandboxRecords;
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Where the SandboxManager keeps its workspaces' compute records (`SandboxRecords`). In core's
3
+ * process the records are the Tenant's `sandboxes` table (`storeSandboxRecords`). A harness
4
+ * process reaches no store (F6.2): it keeps them in a file on its volume
5
+ * (`fileSandboxRecords`), and core keeps its table in step from the harness's `sandbox.state`
6
+ * claims and `workspace.sweep` answers.
7
+ *
8
+ * A workspace's key is `<prefix><16 hex>` for a session's own workspace and
9
+ * `<prefix>sbx-<16 hex>` for a sandbox resource's, where the prefix names the Tenant: core and
10
+ * a harness compute the same keys from the Tenant id.
11
+ */
12
+ import { createHash } from "node:crypto";
13
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
14
+ import { dirname } from "node:path";
15
+ /** The workspace key prefix of the Tenant `scope`. */
16
+ export function workspacePrefix(scope) {
17
+ return `nylorun-${scope}-`;
18
+ }
19
+ /** The key of session `sessionId`'s own workspace. */
20
+ export function sessionWorkspaceKey(scope, sessionId) {
21
+ return workspacePrefix(scope) + createHash("sha256").update(sessionId).digest("hex").slice(0, 16);
22
+ }
23
+ /**
24
+ * The key of sandbox resource `sandboxId`'s workspace: apart from every session key (16 hex
25
+ * characters after the prefix). The virtual backend keeps it in `<sandboxes>/<key>/workspace`.
26
+ */
27
+ export function sandboxWorkspaceKey(scope, sandboxId) {
28
+ return `${workspacePrefix(scope)}sbx-${createHash("sha256").update(sandboxId).digest("hex").slice(0, 16)}`;
29
+ }
30
+ /** The records in the Tenant's `sandboxes` table. */
31
+ export function storeSandboxRecords(store) {
32
+ return {
33
+ get: (key) => store.tx((t) => t.get("sandboxes", key)),
34
+ put: (record) => store.tx((t) => t.put("sandboxes", record.key, record)),
35
+ delete: (key) => store.tx((t) => t.delete("sandboxes", key)),
36
+ list: () => store.tx((t) => t.listSandboxes()),
37
+ count: async () => (await store.tx((t) => t.counts())).sandboxes,
38
+ };
39
+ }
40
+ /**
41
+ * The records in one JSON file, rewritten whole (to a temporary file, then renamed) after each
42
+ * change. Writes are serialized; a file that cannot be read starts empty.
43
+ */
44
+ export function fileSandboxRecords(path) {
45
+ const records = new Map();
46
+ try {
47
+ const stored = JSON.parse(readFileSync(path, "utf8"));
48
+ for (const record of stored.records ?? [])
49
+ records.set(record.key, record);
50
+ }
51
+ catch {
52
+ /* no file yet, or unreadable: start empty */
53
+ }
54
+ let writing = Promise.resolve();
55
+ const save = () => {
56
+ const body = JSON.stringify({ records: [...records.values()] });
57
+ // A failed write does not stop the next one, which writes everything again.
58
+ writing = writing.catch(() => undefined).then(() => {
59
+ mkdirSync(dirname(path), { recursive: true });
60
+ const temporary = `${path}.${process.pid}.tmp`;
61
+ writeFileSync(temporary, body, { mode: 0o600 });
62
+ renameSync(temporary, path);
63
+ });
64
+ return writing;
65
+ };
66
+ return {
67
+ get: async (key) => records.get(key),
68
+ async put(record) {
69
+ records.set(record.key, record);
70
+ await save();
71
+ },
72
+ async delete(key) {
73
+ if (records.delete(key))
74
+ await save();
75
+ },
76
+ list: async () => [...records.values()],
77
+ count: async () => records.size,
78
+ };
79
+ }
@@ -24,6 +24,11 @@ export interface ResolveSandboxInput {
24
24
  readonly config: EffectiveSandboxConfig;
25
25
  /** The request acts for a subject (`Nylorun-Subject`) rather than as the application. */
26
26
  readonly actingForSubject: boolean;
27
+ /**
28
+ * The sandbox's kind. Default `virtual`, which runs no images and allows exact host names
29
+ * only; a `pod` (F7.2) takes an image and `*.suffix` hosts.
30
+ */
31
+ readonly kind?: "virtual" | "pod";
27
32
  }
28
33
  export declare const SUBJECT_INLINE_MESSAGE = "Defining a sandbox needs an application key. Use the Tenant default, or open the session from the application backend.";
29
34
  export declare function resolveSandbox(input: ResolveSandboxInput): SandboxResolution;
@@ -17,13 +17,14 @@ export function resolveSandbox(input) {
17
17
  function build(request, source, input) {
18
18
  const { limits } = input.config;
19
19
  const errors = [];
20
- if (request.image !== undefined)
20
+ const pod = input.kind === "pod";
21
+ if (request.image !== undefined && !pod)
21
22
  errors.push("sandbox.image is not supported: the virtual sandbox has no images.");
22
23
  const allow = (request.network?.allow ?? []).map((host) => host.toLowerCase());
23
24
  for (const host of allow) {
24
25
  if (!withinCeiling(host, limits.network))
25
26
  errors.push(`sandbox.network.allow includes ${host}, which this Tenant does not allow.`);
26
- else if (host.startsWith("*."))
27
+ else if (host.startsWith("*.") && !pod)
27
28
  errors.push(`sandbox.network.allow includes ${host}; the virtual sandbox allows exact host names only, such as ${host.slice(2)}.`);
28
29
  }
29
30
  const cpus = request.resources?.cpus ?? limits.defaultResources.cpus;
@@ -1,11 +1,12 @@
1
1
  /**
2
2
  * Gives a session the sandbox chosen when it was opened. An agent session's pinned manifest is
3
3
  * its definition plus the `nylorun.sandbox` capability (the six built-in tools, the sandbox
4
- * instructions and the resolved spec), also added to each agent it uses as a tool, so the tree
5
- * shares one spec. The definition in the registry never changes.
4
+ * instructions and the resolved spec) and the `nylorun.artifacts` capability (`save_artifact`),
5
+ * also added to each agent it uses as a tool, so the tree shares one spec. The definition in the
6
+ * registry never changes.
6
7
  */
7
8
  import { AgentManifestSchema } from "@nylorun/core/contracts";
8
- import { hashManifest, sandboxCapabilityManifest, } from "@nylorun/core/define";
9
+ import { artifactsCapabilityManifest, hashManifest, sandboxCapabilityManifest, } from "@nylorun/core/define";
9
10
  import { sandboxSpecOf } from "./share.js";
10
11
  function isAgentManifest(value) {
11
12
  return (typeof value === "object" &&
@@ -30,7 +31,8 @@ function addCapability(manifest, spec) {
30
31
  });
31
32
  return {
32
33
  ...manifest,
33
- capabilities: [...capabilities, sandboxCapabilityManifest(spec)],
34
+ // With a sandbox comes `save_artifact`, so files the agent makes reach the user (F8.1).
35
+ capabilities: [...capabilities, sandboxCapabilityManifest(spec), artifactsCapabilityManifest()],
34
36
  };
35
37
  }
36
38
  /** The agent's manifest with the sandbox capability added, or why it cannot take one. */
@@ -43,7 +45,7 @@ export function withSandboxCapability(manifest, spec) {
43
45
  return {
44
46
  ok: false,
45
47
  message: clash
46
- ? `'${manifest.id}' declares a tool named like a sandbox tool (bash, read, write, edit, grep, glob). Rename it, or open the session with sandbox: false.`
48
+ ? `'${manifest.id}' declares a tool named like a sandbox tool (bash, read, write, edit, grep, glob, save_artifact). Rename it, or open the session with sandbox: false.`
47
49
  : `'${manifest.id}' cannot take this sandbox: ${[...new Set(issues)].join("; ")}`,
48
50
  };
49
51
  }
@@ -11,6 +11,8 @@ export type SessionSandboxRef = {
11
11
  readonly manifest: AgentManifest | WorkflowManifest | Record<string, unknown>;
12
12
  /** Session whose id keys the sandbox. Absent means this session owns its own. */
13
13
  readonly sandboxOwnerId?: string | null;
14
+ /** The sandbox resource the session is attached to; its id keys the workspace. */
15
+ readonly sandboxId?: string;
14
16
  /** The sandbox chosen when the session was opened (or inherited from its owner), resolved. */
15
17
  readonly sandbox?: SandboxManifest;
16
18
  };
@@ -32,4 +34,13 @@ export declare function capabilityForSandbox(sandbox: SandboxManifest): Capabili
32
34
  * cycles fall back to the starting session.
33
35
  */
34
36
  export declare function owningSandboxSessionId(session: SessionSandboxRef, lookup: (id: string) => SessionSandboxRef | undefined): string;
37
+ /**
38
+ * The workspace a session's sandbox tools run in: the owning session (one hop through
39
+ * `sandboxOwnerId`, whose log records the workspace's events) and, when that session is
40
+ * attached to a sandbox resource, the resource's id, which keys the workspace.
41
+ */
42
+ export declare function sandboxWorkspaceOf(session: SessionSandboxRef, lookup: (id: string) => SessionSandboxRef | undefined): {
43
+ readonly ownerId: string;
44
+ readonly sandboxId?: string;
45
+ };
35
46
  export declare function sessionHasSandbox(session: SessionSandboxRef, lookup: (id: string) => SessionSandboxRef | undefined): boolean;
@@ -81,6 +81,16 @@ export function owningSandboxSessionId(session, lookup) {
81
81
  }
82
82
  return session.id;
83
83
  }
84
+ /**
85
+ * The workspace a session's sandbox tools run in: the owning session (one hop through
86
+ * `sandboxOwnerId`, whose log records the workspace's events) and, when that session is
87
+ * attached to a sandbox resource, the resource's id, which keys the workspace.
88
+ */
89
+ export function sandboxWorkspaceOf(session, lookup) {
90
+ const ownerId = owningSandboxSessionId(session, lookup);
91
+ const owner = ownerId === session.id ? session : lookup(ownerId) ?? session;
92
+ return owner.sandboxId === undefined ? { ownerId } : { ownerId, sandboxId: owner.sandboxId };
93
+ }
84
94
  export function sessionHasSandbox(session, lookup) {
85
95
  const ownerId = owningSandboxSessionId(session, lookup);
86
96
  const owner = ownerId === session.id ? session : lookup(ownerId) ?? session;
@@ -3,12 +3,14 @@
3
3
  * session gets when it names none, and the limits every session's sandbox must fit. Unset
4
4
  * fields take the defaults below.
5
5
  */
6
- import { type SandboxInlineRequest, type TenantSandboxConfig } from "@nylorun/core/contracts";
6
+ import { type SandboxInlineRequest, type SandboxPlacementHost, type TenantSandboxConfig } from "@nylorun/core/contracts";
7
7
  export declare const SANDBOX_CONFIG_SETTING = "sandbox.config";
8
8
  /** The most a session may ask for when the Tenant sets no maximum. */
9
9
  export declare const DEFAULT_MAX_CPUS = 4;
10
10
  export declare const DEFAULT_MAX_MEMORY_MIB = 8192;
11
11
  export declare const DEFAULT_SANDBOX_IDLE = "15m";
12
+ /** The most sandbox resources a Tenant holds when it sets no limit. */
13
+ export declare const DEFAULT_MAX_SANDBOXES = 100;
12
14
  export interface SandboxResourcesLimit {
13
15
  readonly cpus: number;
14
16
  readonly memoryMiB: number;
@@ -21,7 +23,20 @@ export interface EffectiveSandboxConfig {
21
23
  readonly resources: SandboxResourcesLimit;
22
24
  readonly defaultResources: SandboxResourcesLimit;
23
25
  readonly idle: string;
26
+ /** The most sandbox resources (`PUT /v1/sandboxes/{id}`) the Tenant may hold. */
27
+ readonly sandboxes: number;
28
+ /** Pods: the longest `lifecycle.ttl`. */
29
+ readonly ttl?: string;
24
30
  };
31
+ /** Pods (F7.2, D36). */
32
+ readonly lifecycle: {
33
+ readonly onExpiry: "retain" | "delete";
34
+ readonly stopGrace: string;
35
+ };
36
+ /** Where each harness may run (D38). */
37
+ readonly placement: Readonly<Record<string, {
38
+ readonly hosts: readonly SandboxPlacementHost[];
39
+ }>>;
25
40
  }
26
41
  interface SettingsReader {
27
42
  getSetting(key: string): Promise<string | undefined>;
@@ -11,6 +11,8 @@ export const SANDBOX_CONFIG_SETTING = "sandbox.config";
11
11
  export const DEFAULT_MAX_CPUS = 4;
12
12
  export const DEFAULT_MAX_MEMORY_MIB = 8192;
13
13
  export const DEFAULT_SANDBOX_IDLE = "15m";
14
+ /** The most sandbox resources a Tenant holds when it sets no limit. */
15
+ export const DEFAULT_MAX_SANDBOXES = 100;
14
16
  export function memoryMiB(value) {
15
17
  const bytes = parseSandboxSize(value);
16
18
  return bytes === undefined ? 0 : Math.max(128, Math.ceil(bytes / 1024 ** 2));
@@ -36,7 +38,14 @@ export function effectiveSandboxConfig(config) {
36
38
  memoryMiB: DEFAULT_SANDBOX_MEMORY_MIB,
37
39
  }),
38
40
  idle: limits.idle ?? DEFAULT_SANDBOX_IDLE,
41
+ sandboxes: limits.sandboxes ?? DEFAULT_MAX_SANDBOXES,
42
+ ...(limits.ttl === undefined ? {} : { ttl: limits.ttl }),
39
43
  },
44
+ lifecycle: {
45
+ onExpiry: config.lifecycle?.onExpiry ?? "retain",
46
+ stopGrace: config.lifecycle?.stopGrace ?? "10s",
47
+ },
48
+ placement: config.placement ?? { "*": { hosts: ["harness-container", "sandbox"] } },
40
49
  };
41
50
  }
42
51
  /** The stored configuration, or `{}` when the Tenant has none. */
@@ -3,7 +3,8 @@
3
3
  * and import a substrate SDK; everything else in the Runtime talks to this interface.
4
4
  */
5
5
  import type { SandboxNetworkPreset } from "@nylorun/core/define";
6
- export type SandboxBackendName = "virtual";
6
+ /** `virtual` (just-bash, in process); `local` (the pod sandbox the engine runs in, F7.2). */
7
+ export type SandboxBackendName = "virtual" | "local";
7
8
  /** What separates a sandbox from its host: the Runtime process, or a container boundary. */
8
9
  export type SandboxIsolation = "process" | "container";
9
10
  export interface SandboxProbe {
@@ -52,11 +53,38 @@ export interface SandboxHandle {
52
53
  exec(request: ExecRequest, signal: AbortSignal): Promise<ExecResult>;
53
54
  /** Returns undefined when the file does not exist. */
54
55
  readFile(path: string): Promise<string | undefined>;
56
+ /**
57
+ * The file's bytes, or undefined when it does not exist; refuses one larger than `maxBytes`
58
+ * (`SandboxFileTooLargeError`). For `save_artifact`, which keeps binary files intact.
59
+ */
60
+ readBytes?(path: string, maxBytes: number): Promise<Uint8Array | undefined>;
61
+ /**
62
+ * The regular files under directory `dir`, recursively, with paths relative to it (`/`-
63
+ * separated) in path order; symbolic links are skipped. Undefined when `dir` is not a
64
+ * directory. Stops after `maxEntries + 1` files (`truncated`). For the turn-end export (F8.2).
65
+ */
66
+ listFiles?(dir: string, maxEntries: number): Promise<SandboxListing | undefined>;
55
67
  /** Parent directories must already exist. */
56
68
  writeFile(path: string, content: string): Promise<void>;
57
69
  /** Release compute; files persist. */
58
70
  stop(): Promise<void>;
59
71
  }
72
+ /** The files `listFiles` found: paths relative to the directory listed. */
73
+ export interface SandboxListing {
74
+ readonly entries: readonly {
75
+ readonly path: string;
76
+ readonly size: number;
77
+ }[];
78
+ /** More files than `maxEntries` were found; `entries` holds the first `maxEntries + 1`. */
79
+ readonly truncated: boolean;
80
+ }
81
+ /** A sandbox file read with `readBytes` is larger than the caller allows. */
82
+ export declare class SandboxFileTooLargeError extends Error {
83
+ readonly path: string;
84
+ readonly size: number;
85
+ readonly maxBytes: number;
86
+ constructor(path: string, size: number, maxBytes: number);
87
+ }
60
88
  export interface SandboxBackend {
61
89
  readonly name: SandboxBackendName;
62
90
  readonly isolation: SandboxIsolation;
@@ -1 +1,13 @@
1
- export {};
1
+ /** A sandbox file read with `readBytes` is larger than the caller allows. */
2
+ export class SandboxFileTooLargeError extends Error {
3
+ path;
4
+ size;
5
+ maxBytes;
6
+ constructor(path, size, maxBytes) {
7
+ super(`${path} is ${size} bytes, more than ${maxBytes}`);
8
+ this.path = path;
9
+ this.size = size;
10
+ this.maxBytes = maxBytes;
11
+ this.name = "SandboxFileTooLargeError";
12
+ }
13
+ }
@@ -0,0 +1,20 @@
1
+ CREATE TABLE "nylorun_streams"."sandbox_events" (
2
+ "sandbox_id" text COLLATE "C" NOT NULL,
3
+ "seq" bigint NOT NULL,
4
+ "type" text NOT NULL,
5
+ "body" json NOT NULL,
6
+ "committed_at" timestamp with time zone DEFAULT now() NOT NULL,
7
+ CONSTRAINT "sandbox_events_pkey" PRIMARY KEY("sandbox_id","seq")
8
+ );
9
+ --> statement-breakpoint
10
+ CREATE TABLE "nylorun"."sandbox_resources" (
11
+ "id" text COLLATE "C" PRIMARY KEY NOT NULL,
12
+ "kind" text NOT NULL,
13
+ "spec" json NOT NULL,
14
+ "labels" json NOT NULL,
15
+ "created_at" text COLLATE "C" NOT NULL,
16
+ "updated_at" text COLLATE "C" NOT NULL
17
+ );
18
+ --> statement-breakpoint
19
+ ALTER TABLE "nylorun"."sessions" ADD COLUMN "sandbox_id" text GENERATED ALWAYS AS (nylorun.doc(body)->>'sandboxId') STORED;--> statement-breakpoint
20
+ CREATE INDEX "sessions_sandbox" ON "nylorun"."sessions" USING btree ("sandbox_id");
@@ -0,0 +1,27 @@
1
+ CREATE TABLE "nylorun"."artifact_versions" (
2
+ "artifact_id" text COLLATE "C" NOT NULL,
3
+ "version" integer NOT NULL,
4
+ "blob_key" text NOT NULL,
5
+ "size" bigint NOT NULL,
6
+ "sha256" text NOT NULL,
7
+ "content_type" text NOT NULL,
8
+ "source" text NOT NULL,
9
+ "created_at" text COLLATE "C" NOT NULL,
10
+ CONSTRAINT "artifact_versions_pkey" PRIMARY KEY("artifact_id","version")
11
+ );
12
+ --> statement-breakpoint
13
+ CREATE TABLE "nylorun"."artifacts" (
14
+ "id" text COLLATE "C" PRIMARY KEY NOT NULL,
15
+ "kind" text NOT NULL,
16
+ "name" text NOT NULL,
17
+ "content_type" text NOT NULL,
18
+ "session_id" text COLLATE "C",
19
+ "latest_version" integer NOT NULL,
20
+ "labels_json" text,
21
+ "created_at" text COLLATE "C" NOT NULL,
22
+ "updated_at" text NOT NULL,
23
+ CONSTRAINT "artifacts_kind_check" CHECK (kind = 'file')
24
+ );
25
+ --> statement-breakpoint
26
+ ALTER TABLE "nylorun"."artifact_versions" ADD CONSTRAINT "artifact_versions_artifact_id_fkey" FOREIGN KEY ("artifact_id") REFERENCES "nylorun"."artifacts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
27
+ CREATE INDEX "artifacts_session" ON "nylorun"."artifacts" USING btree ("session_id","created_at","id");
@@ -0,0 +1,12 @@
1
+ CREATE TABLE "nylorun"."artifact_content" (
2
+ "artifact_id" text COLLATE "C" NOT NULL,
3
+ "version" integer NOT NULL,
4
+ "sha256" text COLLATE "C" NOT NULL,
5
+ "size" bigint NOT NULL,
6
+ CONSTRAINT "artifact_content_pkey" PRIMARY KEY("artifact_id","version","sha256")
7
+ );
8
+ --> statement-breakpoint
9
+ ALTER TABLE "nylorun"."artifacts" DROP CONSTRAINT "artifacts_kind_check";--> statement-breakpoint
10
+ ALTER TABLE "nylorun"."artifact_content" ADD CONSTRAINT "artifact_content_version_fkey" FOREIGN KEY ("artifact_id","version") REFERENCES "nylorun"."artifact_versions"("artifact_id","version") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
11
+ CREATE INDEX "artifact_content_sha256" ON "nylorun"."artifact_content" USING btree ("sha256");--> statement-breakpoint
12
+ ALTER TABLE "nylorun"."artifacts" ADD CONSTRAINT "artifacts_kind_check" CHECK (kind IN ('file', 'folder'));
@@ -0,0 +1,14 @@
1
+ ALTER TABLE "nylorun"."sandbox_resources" ADD COLUMN "k8s_name" text COLLATE "C";--> statement-breakpoint
2
+ ALTER TABLE "nylorun"."sandbox_resources" ADD COLUMN "volume_gen" integer DEFAULT 0 NOT NULL;--> statement-breakpoint
3
+ ALTER TABLE "nylorun"."sandbox_resources" ADD COLUMN "desired" text;--> statement-breakpoint
4
+ ALTER TABLE "nylorun"."sandbox_resources" ADD COLUMN "observed" text;--> statement-breakpoint
5
+ ALTER TABLE "nylorun"."sandbox_resources" ADD COLUMN "pod_uid" text;--> statement-breakpoint
6
+ ALTER TABLE "nylorun"."sandbox_resources" ADD COLUMN "host_epoch" integer DEFAULT 0 NOT NULL;--> statement-breakpoint
7
+ ALTER TABLE "nylorun"."sandbox_resources" ADD COLUMN "join_token_hash" text;--> statement-breakpoint
8
+ ALTER TABLE "nylorun"."sandbox_resources" ADD COLUMN "rev" integer DEFAULT 0 NOT NULL;--> statement-breakpoint
9
+ ALTER TABLE "nylorun"."sandbox_resources" ADD COLUMN "last_active_at" timestamp with time zone;--> statement-breakpoint
10
+ ALTER TABLE "nylorun"."sandbox_resources" ADD COLUMN "expires_at" timestamp with time zone;--> statement-breakpoint
11
+ ALTER TABLE "nylorun"."sandbox_resources" ADD COLUMN "started_at" timestamp with time zone;--> statement-breakpoint
12
+ ALTER TABLE "nylorun"."sandbox_resources" ADD COLUMN "reason" text;--> statement-breakpoint
13
+ ALTER TABLE "nylorun"."sandbox_resources" ADD COLUMN "retiring" text COLLATE "C";--> statement-breakpoint
14
+ ALTER TABLE "nylorun"."sandbox_resources" ADD COLUMN "volume_id" text;
@@ -29,6 +29,34 @@
29
29
  "when": 1790956444115,
30
30
  "tag": "0003_tool_crossings",
31
31
  "breakpoints": true
32
+ },
33
+ {
34
+ "idx": 4,
35
+ "version": "7",
36
+ "when": 1791033032750,
37
+ "tag": "0004_sandbox_resources",
38
+ "breakpoints": true
39
+ },
40
+ {
41
+ "idx": 5,
42
+ "version": "7",
43
+ "when": 1791038725360,
44
+ "tag": "0005_artifacts",
45
+ "breakpoints": true
46
+ },
47
+ {
48
+ "idx": 6,
49
+ "version": "7",
50
+ "when": 1791040679989,
51
+ "tag": "0006_folder_artifacts",
52
+ "breakpoints": true
53
+ },
54
+ {
55
+ "idx": 7,
56
+ "version": "7",
57
+ "when": 1791048873567,
58
+ "tag": "0007_sandbox_pods",
59
+ "breakpoints": true
32
60
  }
33
61
  ]
34
62
  }
@@ -1,4 +1,9 @@
1
- import type { RecordWriter } from "../../record/index.js";
1
+ import type { RecordWriter, SandboxRecordWriter } from "../../record/index.js";
2
2
  import type { Transaction } from "./db.js";
3
+ /**
4
+ * The sandbox stream writer of transaction `db` (`record/sandbox.ts`): the only inserts into
5
+ * `sandbox_events`. The caller holds the sandbox row's lock, which orders its events.
6
+ */
7
+ export declare function postgresSandboxRecordWriter(db: Transaction): SandboxRecordWriter;
3
8
  /** The record writer of transaction `db`. */
4
9
  export declare function postgresRecordWriter(db: Transaction): RecordWriter;
@@ -4,8 +4,26 @@
4
4
  * and `session_log_heads`. They sit here, behind the driver boundary (session-store.md §3), and
5
5
  * run in the store's transaction under the caller's session row lock.
6
6
  */
7
- import { sql } from "drizzle-orm";
8
- import { sessionEvents, sessionLogHeads, tenant } from "./schema.js";
7
+ import { eq, sql } from "drizzle-orm";
8
+ import { sandboxEvents, sessionEvents, sessionLogHeads, tenant } from "./schema.js";
9
+ /**
10
+ * The sandbox stream writer of transaction `db` (`record/sandbox.ts`): the only inserts into
11
+ * `sandbox_events`. The caller holds the sandbox row's lock, which orders its events.
12
+ */
13
+ export function postgresSandboxRecordWriter(db) {
14
+ return {
15
+ async nextSeq(sandboxId) {
16
+ const [row] = await db
17
+ .select({ next: sql `coalesce(max(${sandboxEvents.seq}) + 1, 0)`.mapWith(Number) })
18
+ .from(sandboxEvents)
19
+ .where(eq(sandboxEvents.sandboxId, sandboxId));
20
+ return row?.next ?? 0;
21
+ },
22
+ async insert(row) {
23
+ await db.insert(sandboxEvents).values(row);
24
+ },
25
+ };
26
+ }
9
27
  /** The record writer of transaction `db`. */
10
28
  export function postgresRecordWriter(db) {
11
29
  return {