@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
@@ -12,6 +12,8 @@ import { resolveSandbox } from "../sandbox/resolve.js";
12
12
  import { withSandboxCapability } from "../sandbox/session-sandbox.js";
13
13
  import { declaredSandboxes, sandboxSpecOf, sessionSandboxSpec, } from "../sandbox/share.js";
14
14
  import { effectiveSandboxConfig, readSandboxConfig } from "../sandbox/tenant-config.js";
15
+ import { checkPlacement, requirePods } from "../sandbox/placement.js";
16
+ import { attachSandbox, recordAttachment } from "./sandboxes.js";
15
17
  import { sandboxLookup, } from "./context.js";
16
18
  import { fail } from "./http.js";
17
19
  /** A session's creation identity: everything but the request id and vault attachments. */
@@ -133,7 +135,14 @@ export async function putSession(ctx, id, body, access, options = {}) {
133
135
  }
134
136
  const definition = (await t.get("definitions", body.agentId)) ??
135
137
  fail(404, "Definition not found");
136
- const sandbox = await sessionSandbox(t, body, definition, { opaque });
138
+ const sandbox = await sessionSandbox(t, body, definition, {
139
+ opaque,
140
+ ...(options.sandboxGrants === undefined ? {} : { grants: options.sandboxGrants }),
141
+ });
142
+ // Placement (D38): where this session's harness may run, decided now and kept.
143
+ checkPlacement(await readSandboxConfig(t), sandbox.kind);
144
+ if (sandbox.kind === "pod")
145
+ await requirePods(ctx.pods, `Sandbox ${sandbox.sandboxId}`);
137
146
  const created = {
138
147
  id,
139
148
  agentId: body.agentId,
@@ -151,23 +160,40 @@ export async function putSession(ctx, id, body, access, options = {}) {
151
160
  ...(sandbox.sandboxOwnerId !== undefined
152
161
  ? { sandboxOwnerId: sandbox.sandboxOwnerId }
153
162
  : {}),
163
+ ...(sandbox.sandboxId !== undefined ? { sandboxId: sandbox.sandboxId } : {}),
154
164
  ...(sandbox.spec !== undefined
155
165
  ? { sandbox: sandbox.spec, sandboxSource: sandbox.source }
156
166
  : {}),
157
167
  };
158
168
  await t.put("sessions", id, created);
159
169
  await ctx.vault.recordAttachment(t, id, vaultIds);
170
+ if (sandbox.sandboxId !== undefined)
171
+ await recordAttachment(t, id, sandbox.sandboxId);
160
172
  return created;
161
173
  });
162
174
  }
163
175
  function isShare(value) {
164
176
  return typeof value === "object" && value !== null && "session" in value;
165
177
  }
178
+ function isAttach(value) {
179
+ return typeof value === "object" && value !== null && "id" in value;
180
+ }
166
181
  /** Decide a new session's sandbox and, for an agent, the manifest that carries its tools. */
167
182
  async function sessionSandbox(t, body, definition, options) {
168
183
  const request = body.sandbox;
169
184
  const declared = sandboxSpecOf(definition.manifest) !== undefined;
185
+ if (isAttach(request)) {
186
+ if (declared)
187
+ fail(400, `'${body.agentId}' declares its own sandbox with .sandbox(). Remove it from the agent to attach a sandbox.`);
188
+ const sandbox = await attachSandbox(t, request.id, options.grants);
189
+ // A pod's storage and lifecycle are the resource's, not the agent's sandbox manifest.
190
+ const { storage: _storage, lifecycle: _lifecycle, ...spec } = sandbox.spec;
191
+ return { ...(await pin(definition, spec, "sandbox")), sandboxId: sandbox.id, kind: sandbox.kind };
192
+ }
170
193
  if (isShare(request)) {
194
+ const owner = await t.get("sessions", request.session);
195
+ if (owner?.sandboxId !== undefined && (!options.opaque || owner.ownerUserId === body.ownerUserId))
196
+ fail(400, `Session ${request.session} is attached to sandbox ${owner.sandboxId}: open this session with sandbox: { id: "${owner.sandboxId}" }.`);
171
197
  const lookup = await sandboxLookup(t, request.session);
172
198
  const sandboxOwnerId = validateSandboxAttach(body, definition.manifest, lookup, {
173
199
  opaque: options.opaque,
@@ -240,6 +266,7 @@ export async function sessionView(t, s) {
240
266
  vaultIds: s.vaultIds ?? [],
241
267
  credentialSelections: s.credentialSelections ?? [],
242
268
  sandboxOwnerId: s.sandboxOwnerId ?? null,
269
+ ...(s.sandboxId === undefined ? {} : { sandboxId: s.sandboxId }),
243
270
  sandbox: s.sandbox ?? null,
244
271
  ...(s.sandboxSource ? { sandboxSource: s.sandboxSource } : {}),
245
272
  mcpSnapshot: s.mcpSnapshot ?? null,
@@ -1,8 +1,9 @@
1
- import type { SeedTenantConfigRequest, SeedTenantConfigResponse, TenantStatus } from "@nylorun/core/contracts";
1
+ import type { HarnessStatus, SeedTenantConfigRequest, SeedTenantConfigResponse, TenantStatus } from "@nylorun/core/contracts";
2
2
  import type { SessionStore } from "../store/types.js";
3
3
  import type { VaultService } from "../vault/service.js";
4
- import type { SandboxManager } from "../sandbox/manager.js";
4
+ import type { WorkspacePort } from "../harness-api/workspace.js";
5
5
  import type { TenantConfig } from "./types.js";
6
+ import type { TenantContext } from "./context.js";
6
7
  import type { TenantEnvelope } from "@nylorun/core/contracts";
7
8
  import type { StuckInvocation } from "../execution/types.js";
8
9
  import type { StreamsStatus } from "./streams.js";
@@ -12,7 +13,7 @@ export interface TenantStatusContext {
12
13
  config: TenantConfig;
13
14
  store: SessionStore;
14
15
  vault: VaultService;
15
- sandbox: SandboxManager;
16
+ sandbox: Pick<WorkspacePort, "report">;
16
17
  closing: boolean;
17
18
  modelConfigured: boolean;
18
19
  /**
@@ -22,7 +23,11 @@ export interface TenantStatusContext {
22
23
  stuckInvocations?: () => Promise<StuckInvocation[]>;
23
24
  /** This Tenant's Durable Streams status (`streamsStatus` in `streams.ts`). */
24
25
  streamsStatus?: () => Promise<StreamsStatus>;
26
+ /** The Tenant's harnesses (F6.2). */
27
+ harness?: HarnessStatus;
25
28
  }
29
+ /** The Tenant's harnesses, for its status and the Host's. */
30
+ export declare function harnessStatusOf(ctx: Pick<TenantContext, "mcp" | "harness">): HarnessStatus;
26
31
  /** Build `TenantStatusSchema` with secrets redacted (A14). */
27
32
  export declare function buildTenantStatus(ctx: TenantStatusContext): Promise<TenantStatus>;
28
33
  /**
@@ -1,5 +1,9 @@
1
1
  import { FIXTURE_MODEL_SETTING, seedFixtureModel } from "./model-setting.js";
2
2
  import { SANDBOX_CONFIG_SETTING, writeSandboxConfig } from "../sandbox/tenant-config.js";
3
+ /** The Tenant's harnesses, for its status and the Host's. */
4
+ export function harnessStatusOf(ctx) {
5
+ return { mode: ctx.mcp ? "in-process" : "remote", ...ctx.harness.status() };
6
+ }
3
7
  /** How long status waits for the execution to list stuck invocations. */
4
8
  const STUCK_TIMEOUT_MS = 5000;
5
9
  /** `TenantStatus.execution`: paused and backing-off invocations, or why they are unknown. */
@@ -84,6 +88,7 @@ export async function buildTenantStatus(ctx) {
84
88
  backend: sandboxReport.backend,
85
89
  retained: counts.sandboxes,
86
90
  },
91
+ ...(ctx.harness ? { harness: ctx.harness } : {}),
87
92
  ...(execution ? { execution } : {}),
88
93
  ...(streams ? { streams } : {}),
89
94
  };
@@ -17,6 +17,7 @@
17
17
  * - **Readers.** History and session SSE read the session's stream
18
18
  * (`tenant/session-streams.ts`). This module runs one `tenant/control` reader per Tenant per
19
19
  * process, on the current basin, which calls `ctx.abortLocal` for each `session.cancel`,
20
+ * passes an Action's outcome to a run held here for each `action.resolved` (F6.2),
20
21
  * checks the session streams for each `sessions.reset`, and ends a subject's streams for
21
22
  * each `subject.revoked`.
22
23
  *
@@ -78,6 +79,12 @@ export declare function wireStreams(ctx: TenantContext, options: WireStreamsOpti
78
79
  * effect), so failures are logged, never thrown.
79
80
  */
80
81
  export declare function signalSessionCancel(ctx: TenantContext, sessionId: string, turnId: string | null): void;
82
+ /**
83
+ * Appends `action.resolved` for an Action whose outcome just committed, so a run held for it
84
+ * by a harness of another process goes on (F6.2). Call it from `t.afterCommit`. A lost signal
85
+ * costs the hold's time only: the segment then ends as waiting and resumes by replay.
86
+ */
87
+ export declare function signalActionOutcome(ctx: TenantContext, sessionId: string, actionId: string): void;
81
88
  /**
82
89
  * After a reset deleted the Tenant's sessions and moved it to a new basin generation: tells
83
90
  * every process with the Tenant open (`sessions.reset` on the old basin's control stream),
@@ -1,5 +1,5 @@
1
1
  import { basinOf } from "../streams/basin.js";
2
- import { signalCancel, signalSessionsReset } from "../streams/control.js";
2
+ import { signalActionResolved, signalCancel, signalSessionsReset } from "../streams/control.js";
3
3
  import { createStreamRelay, } from "../streams/relay/core.js";
4
4
  import { CONTROL_STREAM, } from "../streams/types.js";
5
5
  import { checkSessionStreams, currentBasin, endSubjectStreams, sleep, } from "./session-streams.js";
@@ -59,6 +59,10 @@ export async function wireStreams(ctx, options) {
59
59
  const signal = body;
60
60
  if (signal?.type === "session.cancel" && typeof signal.sessionId === "string")
61
61
  ctx.abortLocal(signal.sessionId, typeof signal.turnId === "string" ? signal.turnId : undefined);
62
+ else if (signal?.type === "action.resolved" &&
63
+ typeof signal.sessionId === "string" &&
64
+ typeof signal.actionId === "string")
65
+ void resolveHeld(ctx, signal.sessionId, signal.actionId).catch(report("held run resolution failed"));
62
66
  else if (signal?.type === "sessions.reset")
63
67
  void checkSessionStreams(ctx).catch(report("session stream check failed"));
64
68
  else if (signal?.type === "subject.revoked" &&
@@ -171,6 +175,25 @@ export function signalSessionCancel(ctx, sessionId, turnId) {
171
175
  message: messageOf(error),
172
176
  }));
173
177
  }
178
+ /**
179
+ * Appends `action.resolved` for an Action whose outcome just committed, so a run held for it
180
+ * by a harness of another process goes on (F6.2). Call it from `t.afterCommit`. A lost signal
181
+ * costs the hold's time only: the segment then ends as waiting and resumes by replay.
182
+ */
183
+ export function signalActionOutcome(ctx, sessionId, actionId) {
184
+ const streams = ctx.sessionStreams.wiring?.streams;
185
+ if (!streams)
186
+ return;
187
+ void signalActionResolved(streams, currentBasin(ctx), sessionId, actionId).catch((error) => ctx.config.logger.warn("action resolved signal failed", { sessionId, message: messageOf(error) }));
188
+ }
189
+ /** Passes an Action's recorded outcome to the run of `sessionId` held here, if any. */
190
+ async function resolveHeld(ctx, sessionId, actionId) {
191
+ if (!ctx.harness.holds(sessionId))
192
+ return;
193
+ const effect = await ctx.store.tx((t) => t.get("effects", actionId));
194
+ if (effect?.status === "completed" && effect.outcome)
195
+ ctx.harness.resolved(sessionId, actionId, effect.outcome);
196
+ }
174
197
  /**
175
198
  * After a reset deleted the Tenant's sessions and moved it to a new basin generation: tells
176
199
  * every process with the Tenant open (`sessions.reset` on the old basin's control stream),
@@ -9,9 +9,10 @@
9
9
  * 2. **Orphaned sessions.** `running` or `runnable` sessions with no owner, or an owner whose
10
10
  * lease expired, are woken with reason `recover`: a wake lost between commit and send, or
11
11
  * a Worker that died mid-advance (the advance then takes over, §11.4).
12
- * 3. **Sandboxes.** Idle sandboxes are stopped, records of compute this process no longer
13
- * holds are marked stopped, and sandboxes whose session is gone are removed. Idle MCP
14
- * connections are closed.
12
+ * 3. **Sandboxes.** Idle sandboxes are stopped, records of compute no longer held are marked
13
+ * stopped, and sandboxes whose session or sandbox resource is gone are removed, by the
14
+ * workspace capability (`harness-api/workspace.ts`: here, or in the harness that serves
15
+ * workspaces). Idle MCP connections of the in-process harness are closed.
15
16
  * 4. **Hooks.** Callbacks registered with `ctx.onSweep`.
16
17
  * 5. **Deliveries.** Deliveries to Action endpoints whose deadline passed without an answer are
17
18
  * lost (`delivery.ts` `loseAction`), and pending Actions of agents with an endpoint are sent
@@ -38,9 +39,12 @@ export async function sweep(ctx) {
38
39
  () => ctx.sandbox.sweep({
39
40
  now: now.getTime(),
40
41
  sessionExists: async (id) => !!(await ctx.store.tx((t) => t.get("sessions", id))),
42
+ sandboxExists: async (id) => (await ctx.store.tx((t) => t.sandboxResource(id))) !== undefined,
41
43
  }),
42
44
  ],
43
- ["mcp", () => ctx.mcp.sweep()],
45
+ ["pods", () => reconcileStalePods(ctx, now)],
46
+ // A harness elsewhere closes its own idle MCP connections.
47
+ ["mcp", async () => ctx.mcp?.sweep()],
44
48
  ...[...ctx.sweepHooks].map((hook) => ["hook", hook]),
45
49
  ];
46
50
  let failure;
@@ -80,3 +84,32 @@ export async function wakeOrphanedSessions(ctx, now = new Date()) {
80
84
  await ctx.wake(id, { reason: "recover" });
81
85
  return ids;
82
86
  }
87
+ /** A pod sandbox in a change this long without a reconcile gets one from the sweep. */
88
+ const STALE_POD_MS = 60_000;
89
+ /**
90
+ * Pod sandboxes (F7.2) whose change has not finished (creating, deleting, an old volume still
91
+ * to delete, an expiry not yet recorded), or that should run but whose engine is not connected,
92
+ * and whose row has not moved for a minute: a reconcile the `Sandbox` object lost (a send
93
+ * dropped while the Tenant closed) is sent again, and a pod or volume deleted outside the
94
+ * Runtime is found.
95
+ */
96
+ async function reconcileStalePods(ctx, now) {
97
+ if (!ctx.pods)
98
+ return;
99
+ const stale = await ctx.store.tx(async (t) => (await t.listSandboxResources()).filter((row) => {
100
+ const pod = row.pod;
101
+ if (!pod || now.getTime() - Date.parse(row.updatedAt) < STALE_POD_MS)
102
+ return false;
103
+ const expiring = pod.expiresAt !== undefined && Date.parse(pod.expiresAt) <= now.getTime() && pod.observed !== "expired";
104
+ return (pod.desired === "deleted" ||
105
+ pod.retiring !== undefined ||
106
+ pod.observed === "creating" ||
107
+ pod.observed === "deleting" ||
108
+ expiring ||
109
+ (pod.desired === "suspended" && pod.observed !== "suspended") ||
110
+ // Running, but its engine is not connected: the pod may be gone, or its volume.
111
+ (pod.desired === "running" && pod.observed === "running" && !ctx.harness.hosting(row.id)));
112
+ }));
113
+ for (const row of stale)
114
+ await ctx.sandboxSignal(row.id, { kind: "reconcile" });
115
+ }
@@ -9,7 +9,7 @@
9
9
  */
10
10
  import { randomUUID } from "node:crypto";
11
11
  import { errors, jwtVerify } from "jose";
12
- import { isSubject, SUBJECT_TOKEN_AUDIENCE, SUBJECT_TOKEN_TYPE, subjectTokenIssuer, TOKEN_SCOPES, TOKEN_TTL_MIN_SECONDS, } from "@nylorun/core/contracts";
12
+ import { isSandboxGrant, isSubject, SUBJECT_TOKEN_AUDIENCE, SUBJECT_TOKEN_TYPE, subjectTokenIssuer, TOKEN_SANDBOX_GRANTS_MAX, TOKEN_SCOPES, TOKEN_TTL_MIN_SECONDS, } from "@nylorun/core/contracts";
13
13
  import { agentAllowed, readPolicy, resolveRole } from "./access-policy.js";
14
14
  import { fail, failOpaque, HttpError } from "./http.js";
15
15
  import { CLOCK_TOLERANCE_SECONDS, tokenKeyId } from "./jwt.js";
@@ -71,6 +71,7 @@ export async function mintToken(ctx, body) {
71
71
  role: body.role,
72
72
  scp: scopes.join(" "),
73
73
  ...(body.agents ? { agt: [...body.agents] } : {}),
74
+ ...(body.sandboxes?.length ? { sbx: [...new Set(body.sandboxes)] } : {}),
74
75
  epc: epoch,
75
76
  };
76
77
  // Signed by the keys service (F4.2): this process never holds the private key.
@@ -99,6 +100,7 @@ export async function mintToken(ctx, body) {
99
100
  role: body.role,
100
101
  scopes,
101
102
  agents: access.agents === "*" ? "*" : [...access.agents],
103
+ ...(claims.sbx ? { sandboxes: [...claims.sbx] } : {}),
102
104
  keyId: signer.id,
103
105
  };
104
106
  }
@@ -151,7 +153,11 @@ export async function verifySubjectToken(ctx, raw) {
151
153
  scopeNames.some((name) => !TOKEN_SCOPES.includes(name)) ||
152
154
  (claims.agt !== undefined &&
153
155
  (!Array.isArray(claims.agt) ||
154
- claims.agt.some((agent) => typeof agent !== "string"))))
156
+ claims.agt.some((agent) => typeof agent !== "string"))) ||
157
+ (claims.sbx !== undefined &&
158
+ (!Array.isArray(claims.sbx) ||
159
+ claims.sbx.length > TOKEN_SANDBOX_GRANTS_MAX ||
160
+ claims.sbx.some((grant) => !isSandboxGrant(grant)))))
155
161
  return rejected(ctx, "token_claims");
156
162
  const epoch = await ctx.store.tx((t) => t.subjectEpoch(claims.sub));
157
163
  if (claims.epc !== epoch)
@@ -168,6 +174,7 @@ export async function verifySubjectToken(ctx, raw) {
168
174
  agents: access.agents,
169
175
  role: access.role,
170
176
  ...(access.limits ? { limits: access.limits } : {}),
177
+ ...(claims.sbx ? { sandboxes: claims.sbx } : {}),
171
178
  epoch,
172
179
  expiresAt: claims.exp * 1000,
173
180
  tokenId: claims.jti,
@@ -1,6 +1,9 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
- import type { HostAggregate, HostTenant, TenantEnvelope } from "@nylorun/core/contracts";
2
+ import type { HarnessStatus, HostAggregate, HostTenant, TenantEnvelope } from "@nylorun/core/contracts";
3
+ import type { HarnessChannel } from "@nylorun/core/harness-api";
3
4
  import type { FlowLimits } from "../core/limits.js";
5
+ import type { HarnessPeer } from "../harness-api/server.js";
6
+ import type { HostAuthority } from "../sandbox/join.js";
4
7
  import type { SessionStore } from "../store/types.js";
5
8
  import type { SandboxBackend } from "../sandbox/types.js";
6
9
  import type { TenantCause } from "./cause.js";
@@ -19,6 +22,8 @@ export interface TenantPaths {
19
22
  tmp: string;
20
23
  sandboxes: string;
21
24
  pluginData: string;
25
+ /** The `fs` BlobStore's directory, used when the Host has no Object store. */
26
+ blobs: string;
22
27
  logs: string;
23
28
  log: string;
24
29
  }
@@ -48,6 +53,12 @@ export interface TenantConfig {
48
53
  steps?: number;
49
54
  ms?: number;
50
55
  };
56
+ /**
57
+ * How long a run waits in its lease for a pending Action's outcome (F6.2): the turn goes on
58
+ * at once when the Action endpoint answers in time, and ends its segment as waiting (resumed
59
+ * by replay) otherwise. Default 5 minutes; 0 never waits.
60
+ */
61
+ actionHoldMs?: number;
51
62
  /** Retries and timeouts for model calls (Model Calls §5, §6). Defaults in `piModel`. */
52
63
  modelCall?: import("../gates/model-gate.js").ModelCallSettings;
53
64
  childEnv: Readonly<Record<string, string>>;
@@ -84,6 +95,8 @@ export interface TenantSummary {
84
95
  inFlightDeliveries: number;
85
96
  pendingActions: number;
86
97
  uncertainEffects: number;
98
+ /** The Tenant's harnesses (F6.2). */
99
+ harness?: HarnessStatus;
87
100
  }
88
101
  /**
89
102
  * The Node request and response behind a `Request`, for what still writes to Node directly
@@ -107,6 +120,10 @@ export interface TenantHandle {
107
120
  /** Stop scheduling; wait for or cancel active turns. */
108
121
  drain(activeWork: "drain" | "cancel", timeoutMs?: number): Promise<void>;
109
122
  close(): Promise<void>;
123
+ /** Serves a harness's Harness API channel until it closes. Returns a function that detaches it. */
124
+ attachHarness?(channel: HarnessChannel, peer: HarnessPeer): () => void;
125
+ /** The Tenant's side of a sandbox pod's join (F7.2); absent without sandbox pods. */
126
+ hostAuthority?(): HostAuthority | undefined;
110
127
  }
111
128
  /**
112
129
  * The Host's Tenant for a request: open, or unavailable. `cause` says why when opening it
@@ -1,4 +1,6 @@
1
- import type { AdvanceResult, DeliverResult, DurableExecution, StuckInvocation, WorkerHandlers } from "../execution/types.js";
1
+ import type { AbortReason } from "@nylorun/core/harness-api";
2
+ import { RunAbort } from "@nylorun/harness/api";
3
+ import type { AdvanceResult, DeliverResult, SandboxResult, SandboxTrigger, DurableExecution, StuckInvocation, WorkerHandlers } from "../execution/types.js";
2
4
  import type { Logger } from "./types.js";
3
5
  /** This process's Worker id: the `owner` it writes on the sessions it advances (§10.6). */
4
6
  export declare const WORKER_ID: string;
@@ -11,10 +13,12 @@ export declare const DEFAULT_ADVANCE_DEADLINE_MS: number;
11
13
  /** Default time an aborted advance has to settle before it is abandoned. */
12
14
  export declare const DEFAULT_ADVANCE_GRACE_MS = 30000;
13
15
  /** Why an advance's signal aborted (see "Abort reasons" above). */
14
- export type AdvanceAbortKind = "cancel" | "shutdown" | "deadline" | "ownership.lost";
15
- /** The abort reason of an advance. */
16
- export declare class AdvanceAbort extends Error {
17
- readonly kind: AdvanceAbortKind;
16
+ export type AdvanceAbortKind = AbortReason;
17
+ /**
18
+ * The abort reason of an advance. A harness run's reason (`RunAbort`) has the same kinds, so the
19
+ * executors read either one.
20
+ */
21
+ export declare class AdvanceAbort extends RunAbort {
18
22
  readonly name: string;
19
23
  constructor(kind: AdvanceAbortKind, message: string, options?: {
20
24
  cause?: unknown;
@@ -40,6 +44,8 @@ export interface TenantWorker {
40
44
  sweep(): Promise<void>;
41
45
  /** Delivers one Action to its endpoint. Only infrastructure errors throw. */
42
46
  deliver?(actionId: string, signal: AbortSignal): Promise<DeliverResult>;
47
+ /** Reconciles one pod sandbox (F7.2). Only infrastructure errors throw. */
48
+ sandbox?(sandboxId: string, trigger: SandboxTrigger, signal: AbortSignal): Promise<SandboxResult>;
43
49
  }
44
50
  export interface TenantWorkersOptions {
45
51
  /** Finds or opens a Tenant that is not registered on this process. */
@@ -51,6 +51,7 @@
51
51
  */
52
52
  import { hostname } from "node:os";
53
53
  import { randomBytes } from "node:crypto";
54
+ import { RunAbort } from "@nylorun/harness/api";
54
55
  /** This process's Worker id: the `owner` it writes on the sessions it advances (§10.6). */
55
56
  export const WORKER_ID = `worker-${hostname()}-${process.pid}-${randomBytes(4).toString("hex")}`;
56
57
  /**
@@ -61,13 +62,14 @@ export const WORKER_ID = `worker-${hostname()}-${process.pid}-${randomBytes(4).t
61
62
  export const DEFAULT_ADVANCE_DEADLINE_MS = 50 * 60_000;
62
63
  /** Default time an aborted advance has to settle before it is abandoned. */
63
64
  export const DEFAULT_ADVANCE_GRACE_MS = 30_000;
64
- /** The abort reason of an advance. */
65
- export class AdvanceAbort extends Error {
66
- kind;
65
+ /**
66
+ * The abort reason of an advance. A harness run's reason (`RunAbort`) has the same kinds, so the
67
+ * executors read either one.
68
+ */
69
+ export class AdvanceAbort extends RunAbort {
67
70
  name = "AdvanceAbort";
68
71
  constructor(kind, message, options) {
69
- super(message, options);
70
- this.kind = kind;
72
+ super(kind, message, options);
71
73
  }
72
74
  }
73
75
  /** The abort reason of an advance that ran past its deadline. */
@@ -87,7 +89,7 @@ export class AdvanceDeadlineError extends AdvanceAbort {
87
89
  export function abortKind(signal) {
88
90
  if (!signal.aborted)
89
91
  return undefined;
90
- return signal.reason instanceof AdvanceAbort ? signal.reason.kind : "shutdown";
92
+ return signal.reason instanceof RunAbort ? signal.reason.kind : "shutdown";
91
93
  }
92
94
  /** The execution's abort reason as a `shutdown`, keeping its message. */
93
95
  function shutdownOf(reason) {
@@ -138,6 +140,10 @@ export class TenantWorkers {
138
140
  const worker = await this.find(tenantId);
139
141
  return worker?.deliver ? worker.deliver(actionId, signal) : DONE;
140
142
  },
143
+ sandbox: async (tenantId, sandboxId, trigger, signal) => {
144
+ const worker = await this.find(tenantId);
145
+ return worker?.sandbox ? worker.sandbox(sandboxId, trigger, signal) : {};
146
+ },
141
147
  };
142
148
  /** Runs one advance under the deadline and the grace period (see the module comment). */
143
149
  async bounded(tenantId, sessionId, worker, signal) {
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const RUNTIME_VERSION = "0.15.1-beta";
1
+ export declare const RUNTIME_VERSION = "0.16.0-beta";
package/dist/version.js CHANGED
@@ -1,3 +1,3 @@
1
1
  // Kept in step with package.json by runtime/scripts/check-package.mjs, which runs in npm run check.
2
2
  // release:prepare also rewrites this when it bumps @nylorun/runtime.
3
- export const RUNTIME_VERSION = "0.15.1-beta";
3
+ export const RUNTIME_VERSION = "0.16.0-beta";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nylorun/runtime",
3
- "version": "0.15.1-beta",
3
+ "version": "0.16.0-beta",
4
4
  "description": "Nylorun Runtime: the agent execution Host on Postgres, Restate and S2, and model providers.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -59,16 +59,17 @@
59
59
  "db:studio": "drizzle-kit studio"
60
60
  },
61
61
  "dependencies": {
62
- "@ag-ui/core": "~1.0.0",
62
+ "@ag-ui/core": "~1.0.1",
63
63
  "@asteasolutions/zod-to-openapi": "^9.1.0",
64
64
  "@earendil-works/pi-ai": "0.99.1",
65
- "@hono/node-server": "^2.1.1",
65
+ "@hono/node-server": "^2.1.3",
66
66
  "@hono/zod-openapi": "^1.6.3",
67
- "@modelcontextprotocol/sdk": "^1.30.0",
68
- "@nylorun/core": "0.11.0-beta",
69
- "@nylorun/harness": "0.21.2-beta",
70
- "@restatedev/restate-sdk": "1.16.9",
71
- "@s2-dev/streamstore": "0.27.0",
67
+ "@modelcontextprotocol/sdk": "^1.31.0",
68
+ "@nylorun/core": "0.12.0-beta",
69
+ "@nylorun/harness": "0.22.0-beta",
70
+ "@restatedev/restate-sdk": "1.17.2",
71
+ "@s2-dev/streamstore": "0.28.0",
72
+ "aws4fetch": "1.0.20",
72
73
  "drizzle-orm": "^0.45.3",
73
74
  "hono": "^4.13.12",
74
75
  "jose": "^6.2.12",
@@ -76,17 +77,19 @@
76
77
  "pg": "8.23.1",
77
78
  "pg-logical-replication": "2.5.0",
78
79
  "postgres": "^3.4.9",
80
+ "ws": "^8.22.0",
79
81
  "zod": "^4.6.5"
80
82
  },
81
83
  "devDependencies": {
82
84
  "@a2a-js/sdk": "~1.3.0",
83
- "@ag-ui/client": "1.0.0",
85
+ "@ag-ui/client": "1.0.1",
84
86
  "@scalar/openapi-parser": "^0.29.9",
85
87
  "@types/node": "^26.5.0",
86
88
  "@types/pg": "^8.23.1",
89
+ "@types/ws": "^8.18.2",
87
90
  "@typescript/native": "npm:typescript@^7.0.2",
88
91
  "drizzle-kit": "^0.31.11",
89
92
  "typescript": "npm:@typescript/typescript6@^6.0.2",
90
- "vitest": "^5.0.1"
93
+ "vitest": "^5.0.2"
91
94
  }
92
95
  }
@@ -1,24 +0,0 @@
1
- import { type MediaAsset, type MediaReference } from "../media.js";
2
- export * from "../media.js";
3
- /** Explicit local asset store; agents receive only opaque references. */
4
- export declare class MediaStore {
5
- private readonly root;
6
- constructor(root: string);
7
- saveInput(agentId: string, sessionId: string, mediaType: string, base64: string): Promise<MediaAsset>;
8
- saveGenerated(agentId: string, sessionId: string, mediaType: string, bytes: Uint8Array): Promise<MediaAsset>;
9
- dataUrl(reference: MediaReference, sessionId: string): Promise<{
10
- readonly asset: MediaAsset;
11
- readonly url: string;
12
- } | undefined>;
13
- latestInput(agentId: string, sessionId: string): Promise<MediaAsset | undefined>;
14
- read(agentId: string, sessionId: string, assetId: string): Promise<{
15
- asset: MediaAsset;
16
- bytes: Uint8Array;
17
- } | undefined>;
18
- private save;
19
- private readMetadata;
20
- private directory;
21
- }
22
- export declare function localMedia(options: {
23
- root: string;
24
- }): MediaStore;
@@ -1,111 +0,0 @@
1
- import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
2
- import { join } from "node:path";
3
- import { randomUUID } from "node:crypto";
4
- import { IMAGE_MEDIA_TYPES, MAX_IMAGE_BYTES, decodeImageBase64, validateImageBytes, } from "../media.js";
5
- export * from "../media.js";
6
- /** Explicit local asset store; agents receive only opaque references. */
7
- export class MediaStore {
8
- root;
9
- constructor(root) {
10
- this.root = root;
11
- }
12
- async saveInput(agentId, sessionId, mediaType, base64) {
13
- const bytes = decodeImageBase64(mediaType, base64);
14
- return this.save(agentId, sessionId, "input", mediaType, bytes);
15
- }
16
- async saveGenerated(agentId, sessionId, mediaType, bytes) {
17
- validateImageBytes(mediaType, bytes);
18
- return this.save(agentId, sessionId, "generated", mediaType, bytes);
19
- }
20
- async dataUrl(reference, sessionId) {
21
- const stored = await this.read(reference.agentId, sessionId, reference.assetId);
22
- if (!stored)
23
- return undefined;
24
- return Object.freeze({
25
- asset: stored.asset,
26
- url: `data:${stored.asset.mediaType};base64,${Buffer.from(stored.bytes).toString("base64")}`,
27
- });
28
- }
29
- async latestInput(agentId, sessionId) {
30
- const directory = this.directory(agentId, sessionId);
31
- try {
32
- const metadata = await Promise.all((await readdir(directory))
33
- .filter((name) => name.endsWith(".json"))
34
- .map(async (name) => this.readMetadata(directory, name)));
35
- return metadata
36
- .filter((entry) => entry?.kind === "input")
37
- .sort((left, right) => right.createdAt - left.createdAt)[0];
38
- }
39
- catch {
40
- return undefined;
41
- }
42
- }
43
- async read(agentId, sessionId, assetId) {
44
- if (!safe(agentId) || !safe(sessionId) || !safe(assetId))
45
- return undefined;
46
- const directory = this.directory(agentId, sessionId);
47
- const metadata = await this.readMetadata(directory, `${assetId}.json`);
48
- if (!metadata)
49
- return undefined;
50
- try {
51
- return {
52
- asset: metadata,
53
- bytes: await readFile(join(directory, `${assetId}.bin`)),
54
- };
55
- }
56
- catch {
57
- return undefined;
58
- }
59
- }
60
- async save(agentId, sessionId, kind, mediaType, bytes) {
61
- if (!safe(agentId) || !safe(sessionId))
62
- throw new Error("Invalid media asset scope.");
63
- const directory = this.directory(agentId, sessionId);
64
- const asset = {
65
- id: randomUUID(),
66
- mediaType,
67
- bytes: bytes.byteLength,
68
- kind,
69
- createdAt: Date.now(),
70
- };
71
- await mkdir(directory, { recursive: true });
72
- await writeFile(join(directory, `${asset.id}.bin`), bytes);
73
- await writeFile(join(directory, `${asset.id}.json`), JSON.stringify(asset));
74
- return omitCreatedAt(asset);
75
- }
76
- async readMetadata(directory, name) {
77
- try {
78
- const value = JSON.parse(await readFile(join(directory, name), "utf8"));
79
- return validAsset(value) ? value : undefined;
80
- }
81
- catch {
82
- return undefined;
83
- }
84
- }
85
- directory(agentId, sessionId) {
86
- return join(this.root, agentId, sessionId);
87
- }
88
- }
89
- /** Decode one canonical base64 image before it is persisted or sent to a provider. */
90
- function validAsset(value) {
91
- if (!value || typeof value !== "object")
92
- return false;
93
- const asset = value;
94
- return (typeof asset.id === "string" &&
95
- safe(asset.id) &&
96
- typeof asset.mediaType === "string" &&
97
- IMAGE_MEDIA_TYPES.includes(asset.mediaType) &&
98
- typeof asset.bytes === "number" &&
99
- (asset.kind === "input" || asset.kind === "generated") &&
100
- typeof asset.createdAt === "number");
101
- }
102
- function omitCreatedAt(asset) {
103
- const { createdAt: _createdAt, ...result } = asset;
104
- return Object.freeze(result);
105
- }
106
- function safe(value) {
107
- return value !== "." && value !== ".." && /^[a-zA-Z0-9._-]+$/u.test(value);
108
- }
109
- export function localMedia(options) {
110
- return new MediaStore(options.root);
111
- }