@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
@@ -9,3 +9,4 @@
9
9
  export { buildEvent, InvalidEventError, type EventInput } from "./envelope.js";
10
10
  export { decodeCursor, encodeCursor } from "./cursor.js";
11
11
  export { appendEvent, type AppendInput, type RecordWriter } from "./append.js";
12
+ export { appendSandboxEvent, buildSandboxEvent, type SandboxAppendInput, type SandboxRecordWriter, } from "./sandbox.js";
@@ -9,3 +9,4 @@
9
9
  export { buildEvent, InvalidEventError } from "./envelope.js";
10
10
  export { decodeCursor, encodeCursor } from "./cursor.js";
11
11
  export { appendEvent } from "./append.js";
12
+ export { appendSandboxEvent, buildSandboxEvent, } from "./sandbox.js";
@@ -0,0 +1,25 @@
1
+ import { type SandboxEvent, type SandboxEventPayload, type SandboxEventType } from "@nylorun/core/contracts";
2
+ /** The sandbox stream's two statements, in the caller's transaction. */
3
+ export interface SandboxRecordWriter {
4
+ /** The seq the sandbox's next event takes: one past its last, or 0. */
5
+ nextSeq(sandboxId: string): Promise<number>;
6
+ insert(row: {
7
+ sandboxId: string;
8
+ seq: number;
9
+ type: string;
10
+ body: SandboxEvent;
11
+ }): Promise<void>;
12
+ }
13
+ export interface SandboxAppendInput<T extends SandboxEventType> {
14
+ tenantId: string;
15
+ sandboxId: string;
16
+ time: Date;
17
+ type: T;
18
+ payload: SandboxEventPayload<T>;
19
+ }
20
+ /** Builds the event and checks it against the sandbox event catalog. */
21
+ export declare function buildSandboxEvent<T extends SandboxEventType>(input: SandboxAppendInput<T> & {
22
+ seq: number;
23
+ }): SandboxEvent;
24
+ /** Appends one event to the sandbox's stream through `writer`. */
25
+ export declare function appendSandboxEvent<T extends SandboxEventType>(writer: SandboxRecordWriter, input: SandboxAppendInput<T>): Promise<SandboxEvent>;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * A sandbox's lifecycle stream (blueprint D39, F7.1): `sandbox.created`, `sandbox.attached`,
3
+ * `sandbox.detached` and `sandbox.deleted`, on the `nylorun.sandbox-event/1` envelope, numbered
4
+ * from 0 per sandbox id in `nylorun_streams.sandbox_events`. The record module is its only
5
+ * writer, as for session events; the store runs the two statements through
6
+ * `SandboxRecordWriter` in the caller's transaction, under the sandbox row's lock (or, for a
7
+ * sandbox being created, its new row).
8
+ *
9
+ * The stream lives in the record only: it is not published to the stream relay, so no S2
10
+ * stream follows it yet. Clients read it with `GET /v1/sandboxes/{id}/events`.
11
+ */
12
+ import { randomUUID } from "node:crypto";
13
+ import { z } from "zod";
14
+ import { SANDBOX_EVENT_PAYLOADS, SANDBOX_EVENT_SCHEMA, SandboxEventSchema, } from "@nylorun/core/contracts";
15
+ import { InvalidEventError } from "./envelope.js";
16
+ /** Builds the event and checks it against the sandbox event catalog. */
17
+ export function buildSandboxEvent(input) {
18
+ const payload = SANDBOX_EVENT_PAYLOADS[input.type];
19
+ const checkedPayload = payload.safeParse(input.payload);
20
+ if (!checkedPayload.success)
21
+ throw new InvalidEventError(input.type, z.prettifyError(checkedPayload.error));
22
+ const event = JSON.parse(JSON.stringify({
23
+ schema: SANDBOX_EVENT_SCHEMA,
24
+ eventId: randomUUID(),
25
+ tenantId: input.tenantId,
26
+ sandboxId: input.sandboxId,
27
+ seq: input.seq,
28
+ time: input.time.toISOString(),
29
+ type: input.type,
30
+ payload: input.payload,
31
+ }));
32
+ const checked = SandboxEventSchema.safeParse(event);
33
+ if (!checked.success)
34
+ throw new InvalidEventError(input.type, z.prettifyError(checked.error));
35
+ return event;
36
+ }
37
+ /** Appends one event to the sandbox's stream through `writer`. */
38
+ export async function appendSandboxEvent(writer, input) {
39
+ const seq = await writer.nextSeq(input.sandboxId);
40
+ const event = buildSandboxEvent({ ...input, seq });
41
+ await writer.insert({ sandboxId: input.sandboxId, seq, type: input.type, body: event });
42
+ return event;
43
+ }
@@ -0,0 +1,3 @@
1
+ import { type AgentManifest, type CapabilityManifest } from "@nylorun/core/define";
2
+ /** The sandbox capability that owns this tool call, when the call is a built-in sandbox tool. */
3
+ export declare function sandboxCapabilityOf(manifest: AgentManifest | undefined, capabilityId: string | undefined, toolName: string | undefined): CapabilityManifest | undefined;
@@ -0,0 +1,10 @@
1
+ import { isSandboxToolName, } from "@nylorun/core/define";
2
+ /** The sandbox capability that owns this tool call, when the call is a built-in sandbox tool. */
3
+ export function sandboxCapabilityOf(manifest, capabilityId, toolName) {
4
+ if (!manifest || !capabilityId || !toolName || !isSandboxToolName(toolName))
5
+ return undefined;
6
+ const capability = manifest.capabilities.find((item) => item.id === capabilityId);
7
+ return capability?.sandbox && capability.tools?.some((tool) => tool.name === toolName)
8
+ ? capability
9
+ : undefined;
10
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Egress tokens (F7.2, D42), as egress-gate sees them: the proxy credential a pod's engine
3
+ * receives at join with its host token (`sandbox/join.ts`) and presents in `Proxy-Authorization`.
4
+ * Minted and verified by `tenant/host-token.ts`, the one place host and egress tokens are made:
5
+ *
6
+ * ```text
7
+ * header { alg: ES256, typ: nylorun-egress+jwt, kid }
8
+ * claims { iss: urn:nylorun:tenant:<id>, aud: nylorun-egress, sbx: <sandboxId>,
9
+ * epc: <host epoch>, pod: <pod UID>, iat, exp, jti }
10
+ * ```
11
+ *
12
+ * Verification checks shape and header before any key is read, then the signature, `iss`,
13
+ * `aud`, `typ`, lifetime and claims; whether the epoch is still the sandbox's is egress-gate's
14
+ * check (`EgressSandboxes.live`).
15
+ */
16
+ import type { SessionStore } from "../store/types.js";
17
+ import { EGRESS_TOKEN_AUD, EGRESS_TOKEN_TYP, mintEgressToken, type EgressTokenRequest, type HostClaims, type HostTokenKeyCache, type HostTokenVerdict } from "../tenant/host-token.js";
18
+ export { EGRESS_TOKEN_AUD, EGRESS_TOKEN_TYP, mintEgressToken, type EgressTokenRequest };
19
+ /** The longest lifetime an egress token has: a host token's. */
20
+ export declare const EGRESS_TOKEN_MAX_TTL_SECONDS: number;
21
+ export type EgressClaims = HostClaims;
22
+ export type EgressTokenVerdict = HostTokenVerdict;
23
+ /** Public keys imported by key id; the key row is still read on every verification. */
24
+ export type EgressTokenKeyCache = HostTokenKeyCache;
25
+ /** Verifies an egress token of Tenant `tenantId`; refusal reasons are `egress_token_*`. */
26
+ export declare function verifyEgressToken(store: SessionStore, tenantId: string, raw: string, cache?: EgressTokenKeyCache): Promise<EgressTokenVerdict>;
@@ -0,0 +1,8 @@
1
+ import { EGRESS_TOKEN_AUD, EGRESS_TOKEN_TYP, HOST_TOKEN_TTL_SECONDS, mintEgressToken, verifyHostToken, } from "../tenant/host-token.js";
2
+ export { EGRESS_TOKEN_AUD, EGRESS_TOKEN_TYP, mintEgressToken };
3
+ /** The longest lifetime an egress token has: a host token's. */
4
+ export const EGRESS_TOKEN_MAX_TTL_SECONDS = HOST_TOKEN_TTL_SECONDS;
5
+ /** Verifies an egress token of Tenant `tenantId`; refusal reasons are `egress_token_*`. */
6
+ export function verifyEgressToken(store, tenantId, raw, cache = new Map()) {
7
+ return verifyHostToken(store, tenantId, raw, "egress", cache);
8
+ }
@@ -0,0 +1,37 @@
1
+ import type { TenantContext } from "../tenant/context.js";
2
+ import { type HostClaims } from "../tenant/host-token.js";
3
+ export interface HostJoinRequest {
4
+ readonly sandboxId: string;
5
+ readonly podUid: string;
6
+ readonly joinToken: string;
7
+ /** The id the engine keeps on the volume (`/harness/volume-id`). */
8
+ readonly volumeId?: string;
9
+ }
10
+ /** What `join` and `renew` answer. */
11
+ export interface HostJoinAnswer {
12
+ readonly hostToken: string;
13
+ readonly egressToken: string;
14
+ readonly sandboxId: string;
15
+ readonly epoch: number;
16
+ /** ISO time the tokens expire. */
17
+ readonly expiresAt: string;
18
+ }
19
+ /** A refused join, renewal or host connection. */
20
+ export declare class HostAuthError extends Error {
21
+ readonly status: 400 | 401 | 503;
22
+ /** Logged, never answered. */
23
+ readonly reason?: string | undefined;
24
+ readonly name = "HostAuthError";
25
+ constructor(status: 400 | 401 | 503, message: string,
26
+ /** Logged, never answered. */
27
+ reason?: string | undefined);
28
+ }
29
+ /** The Tenant's side of the exchange, as the Harness API listener calls it. */
30
+ export interface HostAuthority {
31
+ join(request: HostJoinRequest): Promise<HostJoinAnswer>;
32
+ renew(hostToken: string): Promise<HostJoinAnswer>;
33
+ /** The claims of a current host's token, for a connection; refuses a stale one. */
34
+ verify(hostToken: string): Promise<HostClaims>;
35
+ }
36
+ export declare function sha256Hex(value: string): string;
37
+ export declare function hostAuthority(ctx: TenantContext): HostAuthority;
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Join token → host token (F7.2, D42): how a pod sandbox's engine becomes the sandbox's host.
3
+ *
4
+ * Core mints a join token (32 random bytes) per incarnation of a pod sandbox (a create, a
5
+ * reset), keeps only its sha256 (`sandbox_resources.join_token_hash`), and hands the plaintext
6
+ * once to the sandboxes service, which writes it to the Sandbox's join Secret, mounted in the
7
+ * pod. The engine exchanges it at the Harness API listener (`POST
8
+ * /nylorun/harness/v1/host/join`, `ws-server.ts`) with its pod's UID:
9
+ *
10
+ * 1. the hash matches, the sandbox is a pod sandbox that is neither deleted, lost nor expired;
11
+ * 2. the UID is the pod the sandboxes service sees in the Sandbox now (a token copied to
12
+ * another pod is refused);
13
+ * 3. the volume is the sandbox's: the engine keeps an id on the volume (`volumeId`), recorded
14
+ * at the first join of an incarnation. agent-sandbox recreates a claim that was deleted, empty;
15
+ * a join from such a volume finds the sandbox `lost` (`sandbox.lost`) and is refused;
16
+ * 4. the host epoch is bumped: tokens of the previous host stop working, and its connection is
17
+ * closed (`HarnessApiServer.revokeHost`), so its runs are given to the next advance. A pod
18
+ * other than the one that joined last records `sandbox.relaunched` (same volume, new pod);
19
+ * a first pod after a create, resume or reset records `sandbox.running`.
20
+ *
21
+ * The answer is a host token (`nylorun-host+jwt`, accepted only by the Harness API listener,
22
+ * for a connection that serves this sandbox alone) and an egress token
23
+ * (`nylorun-egress+jwt`, accepted only by egress-gate), both bound to the sandbox, the epoch
24
+ * and the pod (`tenant/host-token.ts`). `renew` mints a new pair at the same epoch while the
25
+ * host is current; it is refused once the epoch moved.
26
+ *
27
+ * Every refusal answers the same 401 (the reason is logged), so a caller learns nothing about
28
+ * which check failed.
29
+ */
30
+ import { createHash, timingSafeEqual } from "node:crypto";
31
+ import { isSandboxId } from "@nylorun/core/contracts";
32
+ import { mintHostTokens, staleHost, verifyHostToken, } from "../tenant/host-token.js";
33
+ /** A refused join, renewal or host connection. */
34
+ export class HostAuthError extends Error {
35
+ status;
36
+ reason;
37
+ name = "HostAuthError";
38
+ constructor(status, message,
39
+ /** Logged, never answered. */
40
+ reason) {
41
+ super(message);
42
+ this.status = status;
43
+ this.reason = reason;
44
+ }
45
+ }
46
+ const POD_UID = /^[A-Za-z0-9-]{1,64}$/;
47
+ const REFUSED = "The join was refused";
48
+ export function sha256Hex(value) {
49
+ return createHash("sha256").update(value).digest("hex");
50
+ }
51
+ function sameHash(a, b) {
52
+ const left = Buffer.from(a, "hex");
53
+ const right = Buffer.from(b, "hex");
54
+ return left.length === right.length && left.length > 0 && timingSafeEqual(left, right);
55
+ }
56
+ function answerOf(grant) {
57
+ return {
58
+ hostToken: grant.hostToken,
59
+ egressToken: grant.egressToken,
60
+ sandboxId: grant.claims.sandboxId,
61
+ epoch: grant.claims.epoch,
62
+ expiresAt: new Date(grant.claims.expiresAt).toISOString(),
63
+ };
64
+ }
65
+ export function hostAuthority(ctx) {
66
+ const cache = new Map();
67
+ const refuse = (reason) => {
68
+ throw new HostAuthError(401, REFUSED, reason);
69
+ };
70
+ const verified = async (raw) => {
71
+ const verdict = await verifyHostToken(ctx.store, ctx.config.tenantId, raw, "host", cache);
72
+ if (!verdict.ok)
73
+ return refuse(verdict.reason);
74
+ const stale = await staleHost(ctx.store, verdict.claims);
75
+ if (stale)
76
+ return refuse(stale);
77
+ return verdict.claims;
78
+ };
79
+ return {
80
+ async join(request) {
81
+ const { sandboxId, podUid, joinToken, volumeId } = request;
82
+ if (!isSandboxId(sandboxId) ||
83
+ typeof podUid !== "string" ||
84
+ !POD_UID.test(podUid) ||
85
+ typeof joinToken !== "string" ||
86
+ joinToken.length === 0 ||
87
+ joinToken.length > 512 ||
88
+ (volumeId !== undefined && (typeof volumeId !== "string" || !POD_UID.test(volumeId))))
89
+ throw new HostAuthError(400, "A join is { sandboxId, podUid, joinToken }");
90
+ const pods = ctx.pods;
91
+ if (!pods)
92
+ return refuse("this Runtime has no sandbox pods");
93
+ const hash = sha256Hex(joinToken);
94
+ const usable = (row) => {
95
+ const pod = row?.pod;
96
+ if (!row || row.kind !== "pod" || !pod)
97
+ return refuse("no such pod sandbox");
98
+ if (!pod.joinTokenHash || !sameHash(pod.joinTokenHash, hash))
99
+ return refuse("join token mismatch");
100
+ if (pod.desired === "deleted")
101
+ return refuse("the sandbox was deleted");
102
+ // A pod being stopped joins nothing: its sandbox runs again only when asked to.
103
+ if (pod.desired !== "running")
104
+ return refuse("the sandbox is stopped");
105
+ if (pod.observed === "lost" || pod.observed === "expired")
106
+ return refuse(`the sandbox is ${pod.observed}`);
107
+ return pod;
108
+ };
109
+ const readRow = () => ctx.store.tx((t) => t.sandboxResource(sandboxId));
110
+ const before = usable(await readRow());
111
+ let current;
112
+ try {
113
+ current = (await pods.client.status(before.k8sName)).podUID;
114
+ }
115
+ catch (error) {
116
+ throw new HostAuthError(503, "The sandboxes service did not answer; try again", error instanceof Error ? error.message : String(error));
117
+ }
118
+ if (current !== podUid)
119
+ refuse("the pod is not the sandbox's current pod");
120
+ const joined = await ctx.store.tx(async (t) => {
121
+ const row = await t.sandboxResource(sandboxId, { lock: true });
122
+ const pod = usable(row);
123
+ if (pod.k8sName !== before.k8sName)
124
+ refuse("the sandbox was reset during the join");
125
+ const epoch = pod.hostEpoch + 1;
126
+ const now = new Date().toISOString();
127
+ if (pod.volumeId !== undefined && volumeId !== pod.volumeId) {
128
+ // A new, empty volume under the same Sandbox: the sandbox's files are gone.
129
+ const reason = "The sandbox's volume was replaced: its files are gone";
130
+ await t.updateSandboxPod(sandboxId, { observed: "lost", reason, hostEpoch: epoch }, now);
131
+ await t.sandboxEvent(sandboxId, "sandbox.lost", { reason });
132
+ return { lost: epoch };
133
+ }
134
+ await t.updateSandboxPod(sandboxId, {
135
+ hostEpoch: epoch,
136
+ podUid,
137
+ ...(pod.volumeId === undefined && volumeId !== undefined ? { volumeId } : {}),
138
+ ...(pod.desired === "running" ? { observed: "running", reason: null } : {}),
139
+ }, now);
140
+ if (pod.podUid !== podUid) {
141
+ if (pod.podUid !== undefined)
142
+ await t.sandboxEvent(sandboxId, "sandbox.relaunched", { hostEpoch: epoch });
143
+ else
144
+ await t.sandboxEvent(sandboxId, "sandbox.running", { hostEpoch: epoch });
145
+ }
146
+ return epoch;
147
+ });
148
+ if (typeof joined === "object") {
149
+ ctx.harness.revokeHost(sandboxId, joined.lost);
150
+ // Its Sandbox is deleted; only a reset brings the sandbox back.
151
+ await ctx.sandboxSignal(sandboxId, { kind: "reconcile" });
152
+ return refuse("the volume was replaced: the sandbox is lost");
153
+ }
154
+ // The previous host's connection (an older epoch) serves nothing more.
155
+ ctx.harness.revokeHost(sandboxId, joined);
156
+ ctx.config.logger.info("sandbox host joined", { sandboxId, epoch: joined });
157
+ return answerOf(await mintHostTokens(ctx, { sandboxId, epoch: joined, podUid }));
158
+ },
159
+ async renew(hostToken) {
160
+ const claims = await verified(hostToken);
161
+ return answerOf(await mintHostTokens(ctx, claims));
162
+ },
163
+ verify: verified,
164
+ };
165
+ }
@@ -1,55 +1,71 @@
1
1
  /**
2
- * Sandbox Manager: owns sandbox lifecycle for the Runtime. One sandbox per session, created on
3
- * the first sandbox tool call, stopped after its idle timeout, reattached on the next call.
4
- * Commands against one sandbox run one at a time.
2
+ * Sandbox Manager: owns sandbox lifecycle for the Runtime. One workspace per sandbox resource
3
+ * (`sandbox: { id }`, keyed by the sandbox id), or per owning session for a sandbox chosen
4
+ * inline or by the Tenant default (keyed by the session id); created on the first sandbox tool
5
+ * call, stopped after its idle timeout, reattached on the next call. Commands against one
6
+ * workspace run one at a time, whichever session sends them.
5
7
  *
6
- * The manager keeps no timers. The Tenant sweep calls `sweep`, which stops sandboxes idle
7
- * past their timeout, marks records of compute this process no longer holds as stopped
8
- * (after a restart), and, once per process, removes sandboxes whose session is gone.
8
+ * The manager keeps no timers and reaches no store: it keeps its compute records through a
9
+ * `SandboxRecords` port (the Tenant's `sandboxes` table in core's process, a file in a harness's)
10
+ * and writes its events through `emit`. Its owner drives it: `stopIdle` stops sandboxes idle
11
+ * past their timeout and marks records of compute this process no longer holds as stopped
12
+ * (after a restart); `reconcile` removes workspaces whose owner is gone. `sweep` does both, the
13
+ * reconcile once per process, as the Tenant sweep does in core's process.
9
14
  */
10
15
  import type { EventPayload } from "@nylorun/core/contracts";
11
16
  import { type AgentManifest, type CapabilityManifest, type SandboxToolName } from "@nylorun/core/define";
12
- import type { SessionStore } from "../store/types.js";
17
+ import { type SandboxRecord, type SandboxRecords } from "./records.js";
13
18
  import { type SandboxSelection, type SandboxSelectionReport } from "./select.js";
14
19
  import { type SandboxToolOutcome } from "./tools.js";
15
- import type { SandboxBackend } from "./types.js";
20
+ import { type SandboxBackend, type SandboxListing } from "./types.js";
21
+ export { sandboxCapabilityOf } from "./capability.js";
22
+ export type { SandboxRecord, SandboxRecords, SandboxState } from "./records.js";
16
23
  export interface SandboxSessionRef {
24
+ /** The session whose log records the workspace's events: the owning session. */
17
25
  readonly id: string;
18
26
  readonly activeTurnId: string | null;
19
- readonly manifest: AgentManifest;
27
+ readonly manifest?: AgentManifest;
28
+ /**
29
+ * The sandbox resource the owning session is attached to. Its workspace is keyed by this id,
30
+ * so every session attached to it shares the files; absent, the workspace is the session's.
31
+ */
32
+ readonly sandboxId?: string;
33
+ /**
34
+ * Who the call's events are claimed for, passed back to `emit`: in a harness, the run that
35
+ * made the call (its run id).
36
+ */
37
+ readonly claim?: string;
20
38
  }
21
- export type SandboxState = "creating" | "running" | "stopped";
22
- interface SandboxRecord {
23
- key: string;
24
- sessionId: string;
25
- backend: string;
26
- image: string;
27
- state: SandboxState;
28
- createdAt: string;
29
- updatedAt: string;
39
+ /** What `emit` gets with an event besides its payload. */
40
+ export interface SandboxEventMeta {
41
+ /** With `sandbox.state`: the workspace's record as it is now. */
42
+ readonly record?: SandboxRecord;
43
+ /** The `claim` of the call that caused the event; absent for an idle stop. */
44
+ readonly claim?: string;
30
45
  }
31
46
  export interface SandboxSweepOptions {
32
47
  /** The sweep's clock, in ms since the epoch. Defaults to `Date.now()`. */
33
48
  readonly now?: number;
34
49
  /** Whether a session still exists; sandboxes of deleted sessions are removed. */
35
50
  readonly sessionExists: (sessionId: string) => boolean | Promise<boolean>;
51
+ /** Whether a sandbox resource still exists. Default: every one does. */
52
+ readonly sandboxExists?: (sandboxId: string) => boolean | Promise<boolean>;
36
53
  }
37
54
  export interface SandboxManagerOptions {
38
- /** Distinguishes this Runtime's sandboxes from other Runtimes on the same machine. */
55
+ /** Distinguishes this Runtime's sandboxes from other Runtimes on the same machine: the Tenant id. */
39
56
  readonly scope: string;
40
- readonly store: SessionStore;
57
+ /** Where the workspaces' compute records are kept. */
58
+ readonly records: SandboxRecords;
41
59
  readonly backends: readonly SandboxBackend[];
42
60
  /** `auto` or `virtual`. Undefined means an invalid NYLORUN_SANDBOX value. */
43
61
  readonly preference: string | undefined;
44
62
  /** Delete sandboxes on close (the Runtime's store does not outlive the process). */
45
63
  readonly ephemeral: boolean;
46
- /** Writes a session event in its own transaction; never called inside one. */
47
- readonly emit: <T extends SandboxEventType>(sessionId: string, turnId: string | null, type: T, payload: EventPayload<T>) => void | Promise<void>;
64
+ /** Writes a session event (in core, in its own transaction; in a harness, as a claim). */
65
+ readonly emit: <T extends SandboxEventType>(sessionId: string, turnId: string | null, type: T, payload: EventPayload<T>, meta: SandboxEventMeta) => void | Promise<void>;
48
66
  }
49
67
  /** The session events a sandbox writes. */
50
68
  export type SandboxEventType = "sandbox.state" | "sandbox.exec";
51
- /** The sandbox capability that owns this tool call, when the call is a built-in sandbox tool. */
52
- export declare function sandboxCapabilityOf(manifest: AgentManifest | undefined, capabilityId: string | undefined, toolName: string | undefined): CapabilityManifest | undefined;
53
69
  export declare class SandboxManager {
54
70
  private readonly options;
55
71
  private selection?;
@@ -59,24 +75,64 @@ export declare class SandboxManager {
59
75
  private reconciled;
60
76
  constructor(options: SandboxManagerOptions);
61
77
  /**
62
- * One Tenant sweep pass:
63
- * - stops sandboxes idle for at least their idle timeout (their files remain; the next
64
- * call reattaches);
65
- * - marks records whose compute this process does not hold as stopped (compute is gone
66
- * after a Runtime restart);
67
- * - on the first pass of this process, removes sandboxes whose session no longer exists
68
- * (skipped for ephemeral Runtimes, which delete theirs on close).
78
+ * One sweep pass of core's process: `stopIdle`, then, on the first pass of this process,
79
+ * `reconcile` (skipped for ephemeral Runtimes, which delete theirs on close).
69
80
  */
70
81
  sweep(options: SandboxSweepOptions): Promise<void>;
82
+ /**
83
+ * Stops sandboxes idle for at least their idle timeout (their files remain; the next call
84
+ * reattaches), and marks records whose compute this process does not hold as stopped
85
+ * (compute is gone after a restart).
86
+ */
87
+ stopIdle(at?: number): Promise<void>;
88
+ /** Every workspace record this manager keeps. */
89
+ workspaces(): Promise<SandboxRecord[]>;
71
90
  /** Backends are probed once, on first use, and the choice holds for the life of the Runtime. */
72
91
  get ready(): Promise<SandboxSelection>;
73
92
  /** Whether any sandbox was ever recorded, so startup can skip probing for agents without one. */
74
93
  hasRecords(): Promise<boolean>;
75
94
  keyOf(sessionId: string): string;
95
+ /** The workspace key of sandbox resource `sandboxId` (`sandboxWorkspaceKey`). */
96
+ sandboxKeyOf(sandboxId: string): string;
97
+ private keyFor;
76
98
  report(): Promise<SandboxSelectionReport & {
77
99
  readonly defaultImage: string;
78
100
  }>;
79
101
  run(session: SandboxSessionRef, capability: CapabilityManifest, toolName: SandboxToolName, input: unknown, signal: AbortSignal): Promise<SandboxToolOutcome>;
102
+ /**
103
+ * The bytes of one file in the session's sandbox, for `save_artifact` (F8.1), in the same queue
104
+ * as its tool calls: `missing` when there is no such file, a failed outcome when the sandbox
105
+ * cannot open or the file is larger than `maxBytes`. Relative paths resolve in the workspace.
106
+ */
107
+ readBytes(session: SandboxSessionRef, capability: CapabilityManifest, path: string, maxBytes: number, signal: AbortSignal): Promise<{
108
+ kind: "read";
109
+ path: string;
110
+ bytes: Uint8Array;
111
+ } | {
112
+ kind: "missing";
113
+ path: string;
114
+ } | Extract<SandboxToolOutcome, {
115
+ kind: "failed";
116
+ }>>;
117
+ /**
118
+ * The files under directory `dir` of the session's sandbox, for the turn-end export (F8.2), in
119
+ * the same queue as its tool calls. `missing` when the sandbox was never created (nothing is
120
+ * started to look) or `dir` is not a directory. Relative paths resolve in the workspace.
121
+ */
122
+ listFiles(session: SandboxSessionRef, capability: CapabilityManifest, dir: string, maxEntries: number, signal: AbortSignal): Promise<{
123
+ kind: "listed";
124
+ path: string;
125
+ listing: SandboxListing;
126
+ } | {
127
+ kind: "missing";
128
+ path: string;
129
+ } | Extract<SandboxToolOutcome, {
130
+ kind: "failed";
131
+ }>>;
132
+ /** Runs `use` on the session's sandbox in its queue, starting it first when it is stopped. */
133
+ private withHandle;
134
+ /** The session's live sandbox entry and spec, or why it cannot run here. */
135
+ private prepare;
80
136
  private execute;
81
137
  private executeOpen;
82
138
  private open;
@@ -84,8 +140,17 @@ export declare class SandboxManager {
84
140
  private record;
85
141
  /** The sandbox record for a session, if one was ever created. */
86
142
  status(sessionId: string): Promise<SandboxRecord | undefined>;
87
- /** Delete sandboxes this Runtime owns whose session no longer exists. */
88
- reconcile(sessionExists: (sessionId: string) => boolean | Promise<boolean>): Promise<void>;
143
+ /**
144
+ * Deletes sandbox resource `sandboxId`'s workspace, its files and its compute record, after
145
+ * any command queued on it. Called once the resource is deleted.
146
+ */
147
+ removeSandbox(sandboxId: string): Promise<void>;
148
+ /** Deletes the workspace `key`, its files and its record, after any command queued on it. */
149
+ remove(key: string): Promise<void>;
150
+ /**
151
+ * Delete workspaces this Runtime owns whose owner is gone: a session's when `sessionExists`
152
+ * says the session is gone, a sandbox resource's when `sandboxExists` says the resource is.
153
+ */
154
+ reconcile(sessionExists: (sessionId: string) => boolean | Promise<boolean>, sandboxExists: (sandboxId: string) => boolean | Promise<boolean>): Promise<void>;
89
155
  close(): Promise<void>;
90
156
  }
91
- export {};