@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,108 @@
1
+ /**
2
+ * The `BlobStore` seam (blueprint D35, F8.1): the only way the Runtime reaches the Object
3
+ * store. Bytes live here; what they mean lives in Postgres, which is the commit point: an
4
+ * object counts only once a committed row references its key, so an orphaned upload is
5
+ * garbage, never data. Keys are chosen by the Runtime (fresh random ids), so the seam needs no
6
+ * conditional writes.
7
+ *
8
+ * Two adapters, held to one conformance suite (`test/blob/conformance.ts`):
9
+ * - `s3` (`./s3.ts`): the plain S3 API, against RustFS in the local stack and S3 or any S3
10
+ * server elsewhere;
11
+ * - `fs` (`./fs.ts`): files under a directory, for embedding and tests.
12
+ */
13
+ /** A body to store: bytes, a web stream, or any async iterable of chunks (a Node stream). */
14
+ export type BlobBody = Uint8Array | ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>;
15
+ export interface BlobPutOptions {
16
+ /** Stored with the object and returned by `head` and `get`. Default `application/octet-stream`. */
17
+ contentType?: string;
18
+ /**
19
+ * Refuse a body longer than this many bytes with `BlobTooLargeError`; nothing is stored. The
20
+ * body is counted as it streams, so a caller need not know its length up front.
21
+ */
22
+ maxBytes?: number;
23
+ signal?: AbortSignal;
24
+ }
25
+ export interface BlobPutResult {
26
+ key: string;
27
+ /** Bytes stored. */
28
+ size: number;
29
+ /** SHA-256 of the bytes stored, lowercase hex. */
30
+ sha256: string;
31
+ }
32
+ export interface BlobMeta {
33
+ key: string;
34
+ /** The whole object's size in bytes. */
35
+ size: number;
36
+ contentType: string;
37
+ lastModified: Date;
38
+ }
39
+ /** Bytes `start` to `end`, both inclusive, as in an HTTP `Range` header. */
40
+ export interface BlobRange {
41
+ start: number;
42
+ /** Inclusive; past the end of the object means "to the end". Default: to the end. */
43
+ end?: number;
44
+ }
45
+ export interface BlobGetOptions {
46
+ /** Only these bytes. A range that starts at or past the end throws `BlobRangeError`. */
47
+ range?: BlobRange;
48
+ signal?: AbortSignal;
49
+ }
50
+ export interface BlobGetResult extends BlobMeta {
51
+ /** The bytes asked for: the whole object, or the range. Read it or cancel it. */
52
+ body: ReadableStream<Uint8Array>;
53
+ /** Present when a range was asked for: the bytes `body` holds, both inclusive. */
54
+ range?: {
55
+ start: number;
56
+ end: number;
57
+ };
58
+ }
59
+ /** An entry of `list`: the object's key, size and when it was last written. */
60
+ export interface BlobListEntry {
61
+ key: string;
62
+ size: number;
63
+ lastModified: Date;
64
+ }
65
+ export interface BlobStore {
66
+ /** Which adapter this is, for logs and `/ready`. */
67
+ readonly kind: "fs" | "s3";
68
+ /** Store `body` at `key`, replacing what is there. Streams; counts and hashes as it goes. */
69
+ put(key: string, body: BlobBody, options?: BlobPutOptions): Promise<BlobPutResult>;
70
+ /** The object's bytes and metadata, or `undefined` when there is none at `key`. */
71
+ get(key: string, options?: BlobGetOptions): Promise<BlobGetResult | undefined>;
72
+ /** The object's metadata, or `undefined` when there is none at `key`. */
73
+ head(key: string): Promise<BlobMeta | undefined>;
74
+ /** Remove the object; removing one that is not there succeeds. */
75
+ delete(key: string): Promise<void>;
76
+ /** Every object whose key starts with `prefix`, in key order. */
77
+ list(prefix: string): AsyncIterable<BlobListEntry>;
78
+ }
79
+ /** The Object store failed or refused a request; `status` is the HTTP status when it has one. */
80
+ export declare class BlobStoreError extends Error {
81
+ readonly status?: number | undefined;
82
+ readonly code?: string | undefined;
83
+ constructor(message: string, status?: number | undefined, code?: string | undefined);
84
+ }
85
+ /** A `put` body grew past `maxBytes`. Nothing was stored. */
86
+ export declare class BlobTooLargeError extends Error {
87
+ readonly maxBytes: number;
88
+ constructor(maxBytes: number);
89
+ }
90
+ /** A `get` range starts at or past the end of the object (HTTP 416). */
91
+ export declare class BlobRangeError extends Error {
92
+ readonly range: BlobRange;
93
+ readonly size: number;
94
+ constructor(range: BlobRange, size: number);
95
+ }
96
+ export declare const DEFAULT_CONTENT_TYPE = "application/octet-stream";
97
+ export declare const MAX_KEY_BYTES = 1024;
98
+ /**
99
+ * Keys are `/`-separated segments of `A-Z a-z 0-9 . _ -`, neither `.` nor `..`, at most 1024
100
+ * bytes: safe as S3 keys without encoding surprises and as relative paths for `fs`.
101
+ */
102
+ export declare function assertBlobKey(key: string): void;
103
+ /** A `list` prefix: empty, a key, or a key followed by `/` (or the start of a segment). */
104
+ export declare function assertBlobPrefix(prefix: string): void;
105
+ export declare function assertRange(range: BlobRange): void;
106
+ export declare function assertMaxBytes(maxBytes: number | undefined): void;
107
+ /** The chunks of a `BlobBody`, as `Uint8Array`s. */
108
+ export declare function chunksOf(body: BlobBody): AsyncGenerator<Uint8Array>;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * The `BlobStore` seam (blueprint D35, F8.1): the only way the Runtime reaches the Object
3
+ * store. Bytes live here; what they mean lives in Postgres, which is the commit point: an
4
+ * object counts only once a committed row references its key, so an orphaned upload is
5
+ * garbage, never data. Keys are chosen by the Runtime (fresh random ids), so the seam needs no
6
+ * conditional writes.
7
+ *
8
+ * Two adapters, held to one conformance suite (`test/blob/conformance.ts`):
9
+ * - `s3` (`./s3.ts`): the plain S3 API, against RustFS in the local stack and S3 or any S3
10
+ * server elsewhere;
11
+ * - `fs` (`./fs.ts`): files under a directory, for embedding and tests.
12
+ */
13
+ /** The Object store failed or refused a request; `status` is the HTTP status when it has one. */
14
+ export class BlobStoreError extends Error {
15
+ status;
16
+ code;
17
+ constructor(message, status, code) {
18
+ super(message);
19
+ this.status = status;
20
+ this.code = code;
21
+ this.name = "BlobStoreError";
22
+ }
23
+ }
24
+ /** A `put` body grew past `maxBytes`. Nothing was stored. */
25
+ export class BlobTooLargeError extends Error {
26
+ maxBytes;
27
+ constructor(maxBytes) {
28
+ super(`The body is larger than ${maxBytes} bytes`);
29
+ this.maxBytes = maxBytes;
30
+ this.name = "BlobTooLargeError";
31
+ }
32
+ }
33
+ /** A `get` range starts at or past the end of the object (HTTP 416). */
34
+ export class BlobRangeError extends Error {
35
+ range;
36
+ size;
37
+ constructor(range, size) {
38
+ super(`Range ${range.start}-${range.end ?? ""} is outside an object of ${size} bytes`);
39
+ this.range = range;
40
+ this.size = size;
41
+ this.name = "BlobRangeError";
42
+ }
43
+ }
44
+ export const DEFAULT_CONTENT_TYPE = "application/octet-stream";
45
+ export const MAX_KEY_BYTES = 1024;
46
+ const SEGMENT = /^[A-Za-z0-9._-]+$/;
47
+ /**
48
+ * Keys are `/`-separated segments of `A-Z a-z 0-9 . _ -`, neither `.` nor `..`, at most 1024
49
+ * bytes: safe as S3 keys without encoding surprises and as relative paths for `fs`.
50
+ */
51
+ export function assertBlobKey(key) {
52
+ if (typeof key !== "string" || key === "" || key.length > MAX_KEY_BYTES)
53
+ throw new TypeError(`Blob key must be 1 to ${MAX_KEY_BYTES} characters`);
54
+ for (const segment of key.split("/"))
55
+ if (!SEGMENT.test(segment) || segment === "." || segment === "..")
56
+ throw new TypeError(`Blob key ${JSON.stringify(key)} must be /-separated segments of A-Z a-z 0-9 . _ -, none . or ..`);
57
+ }
58
+ /** A `list` prefix: empty, a key, or a key followed by `/` (or the start of a segment). */
59
+ export function assertBlobPrefix(prefix) {
60
+ if (prefix === "")
61
+ return;
62
+ assertBlobKey(prefix.endsWith("/") ? prefix.slice(0, -1) : prefix);
63
+ }
64
+ export function assertRange(range) {
65
+ if (!Number.isSafeInteger(range.start) || range.start < 0)
66
+ throw new TypeError("Range start must be a non-negative integer");
67
+ if (range.end !== undefined && (!Number.isSafeInteger(range.end) || range.end < range.start))
68
+ throw new TypeError("Range end must be an integer no smaller than start");
69
+ }
70
+ export function assertMaxBytes(maxBytes) {
71
+ if (maxBytes !== undefined && (!Number.isSafeInteger(maxBytes) || maxBytes < 0))
72
+ throw new TypeError("maxBytes must be a non-negative integer");
73
+ }
74
+ /** The chunks of a `BlobBody`, as `Uint8Array`s. */
75
+ export async function* chunksOf(body) {
76
+ if (body instanceof Uint8Array) {
77
+ yield body;
78
+ return;
79
+ }
80
+ for await (const chunk of body) {
81
+ if (!(chunk instanceof Uint8Array))
82
+ throw new TypeError("A blob body must yield Uint8Array chunks");
83
+ yield chunk;
84
+ }
85
+ }
@@ -1,5 +1,5 @@
1
1
  import type { Action } from "@nylorun/core/contracts";
2
- import type { SandboxManager } from "../sandbox/manager.js";
2
+ import type { WorkspacePort } from "../harness-api/workspace.js";
3
3
  import type { SandboxToolOutcome } from "../sandbox/tools.js";
4
4
  import { type SessionSandboxRef } from "../sandbox/share.js";
5
5
  export declare class SandboxRouteError extends Error {
@@ -16,7 +16,7 @@ export type SandboxRouteRead = {
16
16
  readonly lookup: (id: string) => SessionSandboxRef | undefined;
17
17
  };
18
18
  export type SandboxRouteDeps = {
19
- readonly sandbox: SandboxManager;
19
+ readonly sandbox: Pick<WorkspacePort, "run">;
20
20
  /** The session (rejects when missing) and its owner chain, read in one transaction. */
21
21
  readonly session: (id: string) => Promise<SandboxRouteRead>;
22
22
  readonly getAction: (id: string) => Promise<Action | undefined>;
@@ -3,7 +3,7 @@
3
3
  * Wired from TenantRuntime; keeps route bodies out of the hot runtime file.
4
4
  */
5
5
  import { isSandboxToolName, } from "@nylorun/core/define";
6
- import { capabilityForSandbox, owningSandboxSessionId, sandboxSpecOf, sandboxSpecsEqual, sessionSandboxSpec, } from "../sandbox/share.js";
6
+ import { capabilityForSandbox, owningSandboxSessionId, sandboxSpecOf, sandboxWorkspaceOf, sandboxSpecsEqual, sessionSandboxSpec, } from "../sandbox/share.js";
7
7
  export class SandboxRouteError extends Error {
8
8
  status;
9
9
  constructor(status, message) {
@@ -36,11 +36,12 @@ function toolInput(body) {
36
36
  }
37
37
  async function runTool(deps, { session, lookup }, tool, input, signal) {
38
38
  const spec = resolveSpec(session, lookup);
39
- const ownerId = owningSandboxSessionId(session, lookup);
39
+ const workspace = sandboxWorkspaceOf(session, lookup);
40
40
  const ref = {
41
- id: ownerId,
41
+ id: workspace.ownerId,
42
42
  activeTurnId: session.activeTurnId,
43
43
  manifest: session.manifest,
44
+ ...(workspace.sandboxId === undefined ? {} : { sandboxId: workspace.sandboxId }),
44
45
  };
45
46
  return deps.sandbox.run(ref, capabilityForSandbox(spec), tool, input, signal);
46
47
  }
@@ -1,4 +1,4 @@
1
- import { type DurableExecution, type Wake, type WorkerHandlers } from "./types.js";
1
+ import { type DurableExecution, type SandboxSignal, type Wake, type WorkerHandlers } from "./types.js";
2
2
  export interface MemoryExecutionOptions {
3
3
  /** Delay between sweep passes of one Tenant. Default 5000. */
4
4
  sweepIntervalMs?: number;
@@ -8,6 +8,11 @@ export interface MemoryExecutionOptions {
8
8
  maxAttempts?: number;
9
9
  /** How long a dedupe key merges repeated wakes. Default 24 hours. */
10
10
  dedupeRetentionMs?: number;
11
+ /**
12
+ * How long `stop` waits for running handlers after aborting them; the ones still running
13
+ * then are abandoned. Default `DEFAULT_STOP_GRACE_MS`.
14
+ */
15
+ stopGraceMs?: number;
11
16
  /** Receives handler failures that are not retried. */
12
17
  onError?: (error: unknown) => void;
13
18
  }
@@ -23,16 +28,21 @@ export declare class MemoryExecution implements DurableExecution {
23
28
  private readonly seen;
24
29
  private readonly timers;
25
30
  private readonly sweeps;
31
+ private readonly sandboxes;
26
32
  private readonly delayed;
27
33
  private readonly inflight;
28
34
  private readonly sweepIntervalMs;
29
35
  private readonly retryDelayMs;
30
36
  private readonly maxAttempts;
31
37
  private readonly dedupeRetentionMs;
38
+ private readonly stopGraceMs;
32
39
  private readonly onError;
33
40
  constructor(options?: MemoryExecutionOptions);
34
41
  wake(tenantId: string, sessionId: string, wake: Wake): Promise<void>;
35
42
  deliver(tenantId: string, actionId: string): Promise<void>;
43
+ sandbox(tenantId: string, sandboxId: string, signal: SandboxSignal): Promise<void>;
44
+ private armSandbox;
45
+ private runSandbox;
36
46
  timer(tenantId: string, key: string, at: Date): Promise<void>;
37
47
  armSweep(tenantId: string): Promise<void>;
38
48
  disarmSweep(tenantId: string): Promise<void>;
@@ -1,4 +1,4 @@
1
- import { WAKE_REASONS, sessionKey, } from "./types.js";
1
+ import { DEFAULT_STOP_GRACE_MS, WAKE_REASONS, sessionKey, settleWithin, } from "./types.js";
2
2
  /**
3
3
  * In-process `DurableExecution`. Not durable: wakes, timers and sweeps live in
4
4
  * memory. It keeps the seam's ordering guarantees within one process, which is
@@ -11,18 +11,21 @@ export class MemoryExecution {
11
11
  seen = new Map();
12
12
  timers = new Map();
13
13
  sweeps = new Map();
14
+ sandboxes = new Map();
14
15
  delayed = new Set();
15
16
  inflight = new Set();
16
17
  sweepIntervalMs;
17
18
  retryDelayMs;
18
19
  maxAttempts;
19
20
  dedupeRetentionMs;
21
+ stopGraceMs;
20
22
  onError;
21
23
  constructor(options = {}) {
22
24
  this.sweepIntervalMs = options.sweepIntervalMs ?? 5000;
23
25
  this.retryDelayMs = options.retryDelayMs ?? 50;
24
26
  this.maxAttempts = options.maxAttempts ?? 5;
25
27
  this.dedupeRetentionMs = options.dedupeRetentionMs ?? 24 * 60 * 60_000;
28
+ this.stopGraceMs = options.stopGraceMs ?? DEFAULT_STOP_GRACE_MS;
26
29
  this.onError =
27
30
  options.onError ??
28
31
  ((error) => queueMicrotask(() => {
@@ -46,6 +49,66 @@ export class MemoryExecution {
46
49
  async deliver(tenantId, actionId) {
47
50
  this.enqueue("deliver", tenantId, actionId);
48
51
  }
52
+ async sandbox(tenantId, sandboxId, signal) {
53
+ const key = `${tenantId}\u0000${sandboxId}`;
54
+ let state = this.sandboxes.get(key);
55
+ if (!state)
56
+ this.sandboxes.set(key, (state = { tenantId, sandboxId, tail: Promise.resolve(), pending: [], attempts: 0, timers: new Map() }));
57
+ if (signal.kind === "arm")
58
+ this.armSandbox(state, signal.timer, signal.at);
59
+ else
60
+ this.runSandbox(state, "reconcile");
61
+ }
62
+ armSandbox(state, timer, at) {
63
+ const existing = state.timers.get(timer);
64
+ if (existing?.handle)
65
+ clearTimeout(existing.handle);
66
+ const entry = { at };
67
+ state.timers.set(timer, entry);
68
+ if (!this.handlers)
69
+ return;
70
+ entry.handle = setTimeout(() => {
71
+ if (state.timers.get(timer) !== entry)
72
+ return;
73
+ state.timers.delete(timer);
74
+ this.runSandbox(state, timer === "retry" ? "reconcile" : timer);
75
+ }, Math.max(0, at - Date.now()));
76
+ }
77
+ runSandbox(state, trigger) {
78
+ const handlers = this.handlers;
79
+ if (!handlers) {
80
+ state.pending.push(trigger);
81
+ return;
82
+ }
83
+ const run = state.tail.then(async () => {
84
+ if (!this.handlers)
85
+ return;
86
+ if (!handlers.sandbox) {
87
+ this.onError(new Error("WorkerHandlers.sandbox is required for sandbox reconciles"));
88
+ return;
89
+ }
90
+ const controller = new AbortController();
91
+ try {
92
+ const result = await handlers.sandbox(state.tenantId, state.sandboxId, trigger, controller.signal);
93
+ state.attempts = 0;
94
+ for (const item of result.arm ?? [])
95
+ this.armSandbox(state, item.timer, item.at);
96
+ if (result.retryAfterMs !== undefined)
97
+ this.armSandbox(state, "retry", Date.now() + Math.max(0, result.retryAfterMs));
98
+ }
99
+ catch (error) {
100
+ state.attempts += 1;
101
+ if (state.attempts >= this.maxAttempts) {
102
+ state.attempts = 0;
103
+ this.onError(error);
104
+ }
105
+ else
106
+ this.armSandbox(state, "retry", Date.now() + this.retryDelayMs * 2 ** (state.attempts - 1));
107
+ }
108
+ });
109
+ state.tail = run.catch(() => undefined);
110
+ this.track(state.tail);
111
+ }
49
112
  async timer(tenantId, key, at) {
50
113
  const id = `${tenantId}\u0000${key}`;
51
114
  const existing = this.timers.get(id);
@@ -81,6 +144,12 @@ export class MemoryExecution {
81
144
  this.armTimer(id, timer);
82
145
  for (const [tenantId, sweep] of this.sweeps)
83
146
  this.scheduleSweep(tenantId, sweep, 0);
147
+ for (const state of this.sandboxes.values()) {
148
+ for (const [timer, entry] of state.timers)
149
+ this.armSandbox(state, timer, entry.at);
150
+ for (const trigger of state.pending.splice(0))
151
+ this.runSandbox(state, trigger);
152
+ }
84
153
  }
85
154
  async stop() {
86
155
  this.handlers = undefined;
@@ -93,10 +162,15 @@ export class MemoryExecution {
93
162
  for (const sweep of this.sweeps.values())
94
163
  if (sweep.handle)
95
164
  clearTimeout(sweep.handle);
165
+ for (const state of this.sandboxes.values())
166
+ for (const entry of state.timers.values())
167
+ if (entry.handle)
168
+ clearTimeout(entry.handle);
96
169
  for (const state of this.keys.values())
97
170
  state.controller?.abort();
98
- while (this.inflight.size > 0)
99
- await Promise.allSettled(this.inflight);
171
+ // Bounded: a handler that ignores its abort is abandoned and runs on unwaited. An
172
+ // abandoned advance's lease lapses and the next advance takes the session over (§11.4).
173
+ await settleWithin(this.inflight, this.stopGraceMs);
100
174
  }
101
175
  /**
102
176
  * Resolves once no advance or delivery is running or queued and no busy or
@@ -37,6 +37,13 @@
37
37
  * - **Retry after `retry`.** A delivery that returns `retry` is run again for
38
38
  * the same key after `retryAfterMs`. This is how an Action endpoint that is
39
39
  * down or busy is retried; the handler throws only on infrastructure errors.
40
+ * - **One reconcile per sandbox at a time** (F7.2). `sandbox` runs
41
+ * `WorkerHandlers.sandbox` for the key `<tenantId>:<sandboxId>` at least once
42
+ * after a `reconcile` resolves, never overlapping another run for the key. A
43
+ * run that answers `retryAfterMs` runs again after it (one pending retry per
44
+ * key: a later one replaces it), and each timer it answers is armed. `arm`
45
+ * sets one of the sandbox's timers (`idle`, `ttl`): setting it again replaces
46
+ * the earlier time, and the run it causes is told which timer fired.
40
47
  */
41
48
  /** Why a session is woken (§12.3, "Where wakes come from"). */
42
49
  export type WakeReason =
@@ -69,6 +76,28 @@ export type AdvanceResult = {
69
76
  status: "busy";
70
77
  retryAfterMs: number;
71
78
  };
79
+ /** A pod sandbox's timers. */
80
+ export type SandboxTimer = "idle" | "ttl";
81
+ /** What `DurableExecution.sandbox` asks for: a reconcile, or a timer. */
82
+ export type SandboxSignal = {
83
+ kind: "reconcile";
84
+ } | {
85
+ kind: "arm";
86
+ timer: SandboxTimer;
87
+ at: number;
88
+ };
89
+ /** Why `WorkerHandlers.sandbox` runs. */
90
+ export type SandboxTrigger = "reconcile" | SandboxTimer;
91
+ /** What a sandbox reconcile asks for next. */
92
+ export interface SandboxResult {
93
+ /** Run again this soon. */
94
+ retryAfterMs?: number;
95
+ /** Timers to set, at ms since the epoch. */
96
+ arm?: readonly {
97
+ timer: SandboxTimer;
98
+ at: number;
99
+ }[];
100
+ }
72
101
  export type DeliverResult = {
73
102
  status: "done";
74
103
  }
@@ -94,6 +123,11 @@ export interface WorkerHandlers {
94
123
  * stops. Throw only on infrastructure errors. Required when `deliver` is used.
95
124
  */
96
125
  deliver?(tenantId: string, actionId: string, signal: AbortSignal): Promise<DeliverResult>;
126
+ /**
127
+ * Reconciles one pod sandbox (F7.2), serialized per sandbox. Throw only on infrastructure
128
+ * errors. Required when `sandbox` is used.
129
+ */
130
+ sandbox?(tenantId: string, sandboxId: string, trigger: SandboxTrigger, signal: AbortSignal): Promise<SandboxResult>;
97
131
  }
98
132
  export interface DurableExecution {
99
133
  /** At-least-once. `dedupeKey` merges repeated wakes for the same cause. */
@@ -110,13 +144,22 @@ export interface DurableExecution {
110
144
  * after the transaction that made the Action pending commits.
111
145
  */
112
146
  deliver(tenantId: string, actionId: string): Promise<void>;
147
+ /**
148
+ * A pod sandbox's reconcile, or one of its timers (F7.2): see "One reconcile per sandbox
149
+ * at a time". Absent where pods are not supported.
150
+ */
151
+ sandbox?(tenantId: string, sandboxId: string, signal: SandboxSignal): Promise<void>;
113
152
  /** Arms the Tenant's self-re-arming sweep. Idempotent. */
114
153
  armSweep(tenantId: string): Promise<void>;
115
154
  /** Stops re-arming the Tenant's sweep (Tenant deleted). A pass already running finishes. */
116
155
  disarmSweep(tenantId: string): Promise<void>;
117
156
  /** Starts delivering to `handlers` (serving the Worker endpoint, for Restate). */
118
157
  start(handlers: WorkerHandlers): Promise<void>;
119
- /** Stops delivering, aborts running advances' signals and waits for them. */
158
+ /**
159
+ * Stops delivering, aborts running advances' signals and waits for them, up to the
160
+ * implementation's stop grace (`DEFAULT_STOP_GRACE_MS`). Handlers still running then are
161
+ * abandoned: their sessions' leases lapse and the next advance takes over (§11.4).
162
+ */
120
163
  stop(): Promise<void>;
121
164
  /**
122
165
  * Resolves when the backing service answers, rejects otherwise (readiness,
@@ -155,3 +198,13 @@ export declare function parseSessionKey(key: string): {
155
198
  tenantId: string;
156
199
  sessionId: string;
157
200
  };
201
+ /**
202
+ * Default time `DurableExecution.stop` waits for running handlers after aborting them: the
203
+ * advance grace period (`DEFAULT_ADVANCE_GRACE_MS` in `tenant/worker.ts`).
204
+ */
205
+ export declare const DEFAULT_STOP_GRACE_MS = 30000;
206
+ /**
207
+ * Waits until `inflight` is empty (it shrinks as its promises settle and may grow meanwhile),
208
+ * for at most `graceMs`. Resolves to how many were still running when the grace ran out.
209
+ */
210
+ export declare function settleWithin(inflight: ReadonlySet<Promise<unknown>>, graceMs: number): Promise<number>;
@@ -37,6 +37,13 @@
37
37
  * - **Retry after `retry`.** A delivery that returns `retry` is run again for
38
38
  * the same key after `retryAfterMs`. This is how an Action endpoint that is
39
39
  * down or busy is retried; the handler throws only on infrastructure errors.
40
+ * - **One reconcile per sandbox at a time** (F7.2). `sandbox` runs
41
+ * `WorkerHandlers.sandbox` for the key `<tenantId>:<sandboxId>` at least once
42
+ * after a `reconcile` resolves, never overlapping another run for the key. A
43
+ * run that answers `retryAfterMs` runs again after it (one pending retry per
44
+ * key: a later one replaces it), and each timer it answers is armed. `arm`
45
+ * sets one of the sandbox's timers (`idle`, `ttl`): setting it again replaces
46
+ * the earlier time, and the run it causes is told which timer fired.
40
47
  */
41
48
  export const WAKE_REASONS = [
42
49
  "message",
@@ -59,3 +66,27 @@ export function parseSessionKey(key) {
59
66
  throw new Error(`Invalid session key: ${key}`);
60
67
  return { tenantId: key.slice(0, at), sessionId: key.slice(at + 1) };
61
68
  }
69
+ /**
70
+ * Default time `DurableExecution.stop` waits for running handlers after aborting them: the
71
+ * advance grace period (`DEFAULT_ADVANCE_GRACE_MS` in `tenant/worker.ts`).
72
+ */
73
+ export const DEFAULT_STOP_GRACE_MS = 30_000;
74
+ /**
75
+ * Waits until `inflight` is empty (it shrinks as its promises settle and may grow meanwhile),
76
+ * for at most `graceMs`. Resolves to how many were still running when the grace ran out.
77
+ */
78
+ export async function settleWithin(inflight, graceMs) {
79
+ let timer;
80
+ const expired = new Promise((resolve) => {
81
+ timer = setTimeout(() => resolve("expired"), Math.max(0, graceMs));
82
+ });
83
+ try {
84
+ while (inflight.size > 0)
85
+ if ((await Promise.race([Promise.allSettled(inflight), expired])) === "expired")
86
+ break;
87
+ }
88
+ finally {
89
+ clearTimeout(timer);
90
+ }
91
+ return inflight.size;
92
+ }
@@ -3,20 +3,24 @@
3
3
  * it is not in the published OpenAPI documents.
4
4
  *
5
5
  * `POST /nylorun/v1/model-calls`
6
- * - Headers: `Authorization: Bearer <NYLORUN_GATES_TOKEN>`, `Idempotency-Key: <effect id>`,
7
- * and optionally `Nylorun-Tenant: <tenant id>`, which must then be the database's one
8
- * Tenant (a call naming another is refused).
9
- * - Body: `ModelCallBody`, at most `MAX_MODEL_CALL_BYTES`.
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.
10
11
  * - `200 {outcome}` once the call has finished, whether the outcome is a candidate or a
11
- * failure; nothing is sent while it runs. `401 gate_unauthorized` for a missing or wrong
12
- * token, `400 invalid_request` for a malformed Tenant header or a bad body, `409
13
- * gate_conflict` when a different request already runs under the same key.
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.
14
17
  * - With an `Idempotency-Key` the call outlives its client (P1.2): a re-send with the same key
15
18
  * and body joins it, or gets its outcome for 30 minutes. Without one, a client that goes
16
19
  * away aborts the call.
17
20
  *
18
21
  * `POST /nylorun/v1/model-calls/{key}/cancel` (same headers): aborts the keyed call; `204`
19
- * whether or not it exists.
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.
20
24
  */
21
25
  import { z } from "zod";
22
26
  import type { ModelGateOutcome } from "./model-gate.js";
@@ -26,9 +30,6 @@ export declare const TENANT_HEADER = "nylorun-tenant";
26
30
  /** Largest request body the gate reads: one `ModelCall` with its whole projected history. */
27
31
  export declare const MAX_MODEL_CALL_BYTES: number;
28
32
  export declare const ModelCallBodySchema: z.ZodObject<{
29
- sessionId: z.ZodString;
30
- turnId: z.ZodString;
31
- agentId: z.ZodString;
32
33
  effectId: z.ZodString;
33
34
  invocationId: z.ZodString;
34
35
  call: z.ZodObject<{
@@ -37,7 +38,7 @@ export declare const ModelCallBodySchema: z.ZodObject<{
37
38
  name: z.ZodString;
38
39
  }, z.core.$loose>>;
39
40
  }, z.core.$loose>;
40
- }, z.core.$strip>;
41
+ }, z.core.$strict>;
41
42
  export type ModelCallBody = z.infer<typeof ModelCallBodySchema>;
42
43
  export interface ModelCallResponse {
43
44
  readonly outcome: ModelGateOutcome;
@@ -45,7 +46,7 @@ export interface ModelCallResponse {
45
46
  /** An error answer: `{error: {code, message}}`. */
46
47
  export interface GateErrorBody {
47
48
  readonly error: {
48
- readonly code: "gate_unauthorized" | "invalid_request" | "gate_conflict";
49
+ readonly code: "gate_unauthorized" | "gate_forbidden" | "run_stale" | "invalid_request" | "gate_conflict";
49
50
  readonly message: string;
50
51
  };
51
52
  }
@@ -3,20 +3,24 @@
3
3
  * it is not in the published OpenAPI documents.
4
4
  *
5
5
  * `POST /nylorun/v1/model-calls`
6
- * - Headers: `Authorization: Bearer <NYLORUN_GATES_TOKEN>`, `Idempotency-Key: <effect id>`,
7
- * and optionally `Nylorun-Tenant: <tenant id>`, which must then be the database's one
8
- * Tenant (a call naming another is refused).
9
- * - Body: `ModelCallBody`, at most `MAX_MODEL_CALL_BYTES`.
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.
10
11
  * - `200 {outcome}` once the call has finished, whether the outcome is a candidate or a
11
- * failure; nothing is sent while it runs. `401 gate_unauthorized` for a missing or wrong
12
- * token, `400 invalid_request` for a malformed Tenant header or a bad body, `409
13
- * gate_conflict` when a different request already runs under the same key.
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.
14
17
  * - With an `Idempotency-Key` the call outlives its client (P1.2): a re-send with the same key
15
18
  * and body joins it, or gets its outcome for 30 minutes. Without one, a client that goes
16
19
  * away aborts the call.
17
20
  *
18
21
  * `POST /nylorun/v1/model-calls/{key}/cancel` (same headers): aborts the keyed call; `204`
19
- * whether or not it exists.
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.
20
24
  */
21
25
  import { z } from "zod";
22
26
  export const MODEL_CALLS_PATH = "/nylorun/v1/model-calls";
@@ -24,10 +28,7 @@ export const MODEL_CALLS_PATH = "/nylorun/v1/model-calls";
24
28
  export const TENANT_HEADER = "nylorun-tenant";
25
29
  /** Largest request body the gate reads: one `ModelCall` with its whole projected history. */
26
30
  export const MAX_MODEL_CALL_BYTES = 32 * 1024 * 1024;
27
- export const ModelCallBodySchema = z.object({
28
- sessionId: z.string().min(1),
29
- turnId: z.string().min(1),
30
- agentId: z.string().min(1),
31
+ export const ModelCallBodySchema = z.strictObject({
31
32
  effectId: z.string().min(1),
32
33
  invocationId: z.string().min(1),
33
34
  call: z.looseObject({