@nylorun/runtime 0.15.2-beta → 0.17.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 (291) hide show
  1. package/CHANGELOG.md +134 -0
  2. package/README.md +68 -29
  3. package/dist/adapters/execution/restate.d.ts +9 -1
  4. package/dist/adapters/execution/restate.js +64 -5
  5. package/dist/adapters/sandbox/local.d.ts +10 -0
  6. package/dist/adapters/sandbox/local.js +195 -0
  7. package/dist/adapters/sandbox/virtual.js +36 -1
  8. package/dist/admin-openapi.json +341 -7
  9. package/dist/api/a2a/tasks.js +6 -1
  10. package/dist/api/ag-ui/endpoint.js +1 -2
  11. package/dist/api/ag-ui/history.js +6 -1
  12. package/dist/api/ag-ui/routes.d.ts +1 -1
  13. package/dist/api/ag-ui/routes.js +15 -13
  14. package/dist/api/components.d.ts +2667 -711
  15. package/dist/api/components.js +24 -11
  16. package/dist/api/gate/keys.d.ts +4 -3
  17. package/dist/api/gate/routes.d.ts +19 -3
  18. package/dist/api/gate/routes.js +162 -53
  19. package/dist/api/http/app.js +6 -0
  20. package/dist/api/http/define.d.ts +19 -12
  21. package/dist/api/http/define.js +13 -27
  22. package/dist/api/http/routes/access.d.ts +3 -19
  23. package/dist/api/http/routes/access.js +12 -172
  24. package/dist/api/http/routes/actions.js +0 -1
  25. package/dist/api/http/routes/artifacts.d.ts +11 -0
  26. package/dist/api/http/routes/artifacts.js +516 -0
  27. package/dist/api/http/routes/me.d.ts +13 -0
  28. package/dist/api/http/routes/me.js +45 -0
  29. package/dist/api/http/routes/sandboxes.d.ts +9 -0
  30. package/dist/api/http/routes/sandboxes.js +150 -0
  31. package/dist/api/http/routes/sessions.js +9 -13
  32. package/dist/api/http/routes/tenant.js +36 -2
  33. package/dist/api/http/routes/vaults.d.ts +12 -3
  34. package/dist/api/http/routes/vaults.js +122 -54
  35. package/dist/api/openapi.js +4 -10
  36. package/dist/artifacts/config.d.ts +23 -0
  37. package/dist/artifacts/config.js +27 -0
  38. package/dist/artifacts/export.d.ts +8 -0
  39. package/dist/artifacts/export.js +205 -0
  40. package/dist/artifacts/files.d.ts +36 -0
  41. package/dist/artifacts/files.js +45 -0
  42. package/dist/artifacts/folders.d.ts +60 -0
  43. package/dist/artifacts/folders.js +209 -0
  44. package/dist/artifacts/links.d.ts +28 -0
  45. package/dist/artifacts/links.js +92 -0
  46. package/dist/artifacts/media-types.d.ts +13 -0
  47. package/dist/artifacts/media-types.js +80 -0
  48. package/dist/artifacts/parts.d.ts +34 -0
  49. package/dist/artifacts/parts.js +38 -0
  50. package/dist/artifacts/service.d.ts +74 -0
  51. package/dist/artifacts/service.js +301 -0
  52. package/dist/artifacts/workspace.d.ts +60 -0
  53. package/dist/artifacts/workspace.js +57 -0
  54. package/dist/artifacts/zip.d.ts +10 -0
  55. package/dist/artifacts/zip.js +126 -0
  56. package/dist/blob/fs.d.ts +13 -0
  57. package/dist/blob/fs.js +194 -0
  58. package/dist/blob/index.d.ts +9 -0
  59. package/dist/blob/index.js +9 -0
  60. package/dist/blob/s3.d.ts +42 -0
  61. package/dist/blob/s3.js +366 -0
  62. package/dist/blob/types.d.ts +108 -0
  63. package/dist/blob/types.js +85 -0
  64. package/dist/core/sandbox-routes.d.ts +2 -2
  65. package/dist/core/sandbox-routes.js +4 -3
  66. package/dist/execution/memory.d.ts +11 -1
  67. package/dist/execution/memory.js +77 -3
  68. package/dist/execution/types.d.ts +54 -1
  69. package/dist/execution/types.js +31 -0
  70. package/dist/gates/contract.d.ts +14 -13
  71. package/dist/gates/contract.js +13 -12
  72. package/dist/gates/egress.d.ts +72 -0
  73. package/dist/gates/egress.js +319 -0
  74. package/dist/gates/handler.d.ts +3 -0
  75. package/dist/gates/handler.js +3 -2
  76. package/dist/gates/http-client.d.ts +3 -2
  77. package/dist/gates/http-client.js +17 -6
  78. package/dist/gates/in-process.d.ts +13 -1
  79. package/dist/gates/in-process.js +11 -0
  80. package/dist/gates/inflight.d.ts +23 -4
  81. package/dist/gates/inflight.js +18 -4
  82. package/dist/gates/mcp-handler.js +5 -1
  83. package/dist/gates/model-gate.d.ts +6 -2
  84. package/dist/gates/tenant-vaults.d.ts +18 -9
  85. package/dist/gates/tenant-vaults.js +17 -12
  86. package/dist/gates/tool-calls.d.ts +9 -5
  87. package/dist/gates/tool-calls.js +6 -6
  88. package/dist/gates/tool-client.d.ts +9 -2
  89. package/dist/gates/tool-client.js +56 -22
  90. package/dist/gates/tool-contract.d.ts +12 -7
  91. package/dist/gates/tool-contract.js +10 -4
  92. package/dist/gates/tool-gate.d.ts +1 -0
  93. package/dist/harness/calls.d.ts +52 -0
  94. package/dist/harness/calls.js +113 -0
  95. package/dist/harness/executors.d.ts +22 -0
  96. package/dist/harness/executors.js +96 -0
  97. package/dist/harness/main.d.ts +6 -0
  98. package/dist/harness/main.js +103 -0
  99. package/dist/harness/pod.d.ts +21 -0
  100. package/dist/harness/pod.js +119 -0
  101. package/dist/harness/run-tokens.d.ts +18 -0
  102. package/dist/harness/run-tokens.js +15 -0
  103. package/dist/harness/service.d.ts +46 -0
  104. package/dist/harness/service.js +179 -0
  105. package/dist/harness/ws-client.d.ts +43 -0
  106. package/dist/harness/ws-client.js +122 -0
  107. package/dist/harness/ws-port.d.ts +22 -0
  108. package/dist/harness/ws-port.js +78 -0
  109. package/dist/harness-api/in-process.d.ts +24 -0
  110. package/dist/harness-api/in-process.js +119 -0
  111. package/dist/harness-api/record.d.ts +44 -0
  112. package/dist/harness-api/record.js +250 -0
  113. package/dist/harness-api/renew.d.ts +37 -0
  114. package/dist/harness-api/renew.js +77 -0
  115. package/dist/harness-api/server.d.ts +104 -0
  116. package/dist/harness-api/server.js +408 -0
  117. package/dist/harness-api/start.d.ts +36 -0
  118. package/dist/harness-api/start.js +118 -0
  119. package/dist/harness-api/workspace.d.ts +53 -0
  120. package/dist/harness-api/workspace.js +201 -0
  121. package/dist/harness-api/ws-server.d.ts +34 -0
  122. package/dist/harness-api/ws-server.js +227 -0
  123. package/dist/host/admin-api.d.ts +12 -5
  124. package/dist/host/admin-api.js +72 -6
  125. package/dist/host/app.d.ts +0 -1
  126. package/dist/host/app.js +35 -24
  127. package/dist/host/config-for.d.ts +4 -0
  128. package/dist/host/config-for.js +2 -0
  129. package/dist/host/config.d.ts +0 -6
  130. package/dist/host/create-host.d.ts +3 -10
  131. package/dist/host/create-host.js +4 -1
  132. package/dist/host/gates.d.ts +27 -2
  133. package/dist/host/gates.js +52 -2
  134. package/dist/host/http.d.ts +4 -1
  135. package/dist/host/http.js +6 -1
  136. package/dist/host/main.js +167 -37
  137. package/dist/host/stack-config.d.ts +112 -9
  138. package/dist/host/stack-config.js +222 -20
  139. package/dist/index.d.ts +3 -0
  140. package/dist/index.js +2 -0
  141. package/dist/keys/client.js +9 -1
  142. package/dist/keys/keys.d.ts +12 -2
  143. package/dist/keys/keys.js +8 -0
  144. package/dist/mcp/connect.js +2 -1
  145. package/dist/mcp/pool.d.ts +1 -0
  146. package/dist/mcp/pool.js +1 -0
  147. package/dist/mcp/snapshot.d.ts +8 -1
  148. package/dist/mcp/snapshot.js +28 -0
  149. package/dist/media.d.ts +5 -24
  150. package/dist/media.js +5 -0
  151. package/dist/model/pi-model.d.ts +6 -2
  152. package/dist/model/pi-model.js +39 -22
  153. package/dist/node/index.d.ts +1 -1
  154. package/dist/node/index.js +1 -1
  155. package/dist/openapi.json +14084 -9426
  156. package/dist/record/index.d.ts +1 -0
  157. package/dist/record/index.js +1 -0
  158. package/dist/record/sandbox.d.ts +25 -0
  159. package/dist/record/sandbox.js +43 -0
  160. package/dist/sandbox/capability.d.ts +3 -0
  161. package/dist/sandbox/capability.js +10 -0
  162. package/dist/sandbox/egress-token.d.ts +26 -0
  163. package/dist/sandbox/egress-token.js +8 -0
  164. package/dist/sandbox/join.d.ts +37 -0
  165. package/dist/sandbox/join.js +165 -0
  166. package/dist/sandbox/manager.d.ts +99 -34
  167. package/dist/sandbox/manager.js +216 -84
  168. package/dist/sandbox/placement.d.ts +27 -0
  169. package/dist/sandbox/placement.js +44 -0
  170. package/dist/sandbox/pods/client.d.ts +90 -0
  171. package/dist/sandbox/pods/client.js +79 -0
  172. package/dist/sandbox/pods/lifecycle.d.ts +91 -0
  173. package/dist/sandbox/pods/lifecycle.js +178 -0
  174. package/dist/sandbox/pods/name.d.ts +1 -0
  175. package/dist/sandbox/pods/name.js +28 -0
  176. package/dist/sandbox/pods/network-gate.d.ts +25 -0
  177. package/dist/sandbox/pods/network-gate.js +71 -0
  178. package/dist/sandbox/pods/reconcile.d.ts +11 -0
  179. package/dist/sandbox/pods/reconcile.js +152 -0
  180. package/dist/sandbox/pods/spec.d.ts +45 -0
  181. package/dist/sandbox/pods/spec.js +62 -0
  182. package/dist/sandbox/records.d.ts +29 -0
  183. package/dist/sandbox/records.js +79 -0
  184. package/dist/sandbox/resolve.d.ts +5 -0
  185. package/dist/sandbox/resolve.js +3 -2
  186. package/dist/sandbox/session-sandbox.js +7 -5
  187. package/dist/sandbox/share.d.ts +11 -0
  188. package/dist/sandbox/share.js +10 -0
  189. package/dist/sandbox/tenant-config.d.ts +16 -1
  190. package/dist/sandbox/tenant-config.js +9 -0
  191. package/dist/sandbox/types.d.ts +29 -1
  192. package/dist/sandbox/types.js +13 -1
  193. package/dist/store/postgres/drizzle/0004_sandbox_resources.sql +20 -0
  194. package/dist/store/postgres/drizzle/0005_artifacts.sql +27 -0
  195. package/dist/store/postgres/drizzle/0006_folder_artifacts.sql +12 -0
  196. package/dist/store/postgres/drizzle/0007_sandbox_pods.sql +14 -0
  197. package/dist/store/postgres/drizzle/0008_installation_vaults.sql +2 -0
  198. package/dist/store/postgres/drizzle/0009_oauth_pending.sql +19 -0
  199. package/dist/store/postgres/drizzle/0010_oss_auth_removals.sql +3 -0
  200. package/dist/store/postgres/drizzle/meta/_journal.json +49 -0
  201. package/dist/store/postgres/record-writer.d.ts +6 -1
  202. package/dist/store/postgres/record-writer.js +20 -2
  203. package/dist/store/postgres/schema.d.ts +1168 -250
  204. package/dist/store/postgres/schema.js +174 -28
  205. package/dist/store/postgres/store.js +339 -103
  206. package/dist/store/postgres/tenant.d.ts +1 -1
  207. package/dist/store/postgres/tenant.js +2 -2
  208. package/dist/store/types.d.ts +163 -32
  209. package/dist/streams/control.d.ts +2 -5
  210. package/dist/streams/control.js +5 -8
  211. package/dist/streams/types.d.ts +8 -8
  212. package/dist/streams/types.js +1 -1
  213. package/dist/tenant/advance.js +218 -160
  214. package/dist/tenant/artifact-tool.d.ts +5 -0
  215. package/dist/tenant/artifact-tool.js +119 -0
  216. package/dist/tenant/auth.d.ts +13 -9
  217. package/dist/tenant/auth.js +25 -32
  218. package/dist/tenant/commands.js +31 -10
  219. package/dist/tenant/context.d.ts +83 -26
  220. package/dist/tenant/delivery-token.js +4 -4
  221. package/dist/tenant/effects.d.ts +24 -14
  222. package/dist/tenant/effects.js +10 -398
  223. package/dist/tenant/ephemeral.d.ts +14 -2
  224. package/dist/tenant/ephemeral.js +8 -1
  225. package/dist/tenant/history.d.ts +13 -23
  226. package/dist/tenant/history.js +14 -62
  227. package/dist/tenant/host-token.d.ts +92 -0
  228. package/dist/tenant/host-token.js +179 -0
  229. package/dist/tenant/identity-file.d.ts +75 -0
  230. package/dist/tenant/identity-file.js +202 -0
  231. package/dist/tenant/issuers.d.ts +48 -0
  232. package/dist/tenant/issuers.js +311 -0
  233. package/dist/tenant/jwt.js +3 -3
  234. package/dist/tenant/module.js +1 -0
  235. package/dist/tenant/operator-keys.d.ts +18 -0
  236. package/dist/tenant/operator-keys.js +55 -0
  237. package/dist/tenant/outbound.d.ts +30 -0
  238. package/dist/tenant/outbound.js +142 -10
  239. package/dist/tenant/paths.d.ts +2 -1
  240. package/dist/tenant/paths.js +3 -1
  241. package/dist/tenant/principals.d.ts +2 -11
  242. package/dist/tenant/principals.js +10 -30
  243. package/dist/tenant/reset.d.ts +5 -2
  244. package/dist/tenant/reset.js +22 -2
  245. package/dist/tenant/run-grants.d.ts +49 -0
  246. package/dist/tenant/run-grants.js +62 -0
  247. package/dist/tenant/run-token.d.ts +70 -0
  248. package/dist/tenant/run-token.js +138 -0
  249. package/dist/tenant/runtime.d.ts +42 -0
  250. package/dist/tenant/runtime.js +134 -33
  251. package/dist/tenant/sandboxes.d.ts +119 -0
  252. package/dist/tenant/sandboxes.js +551 -0
  253. package/dist/tenant/session-streams.d.ts +6 -10
  254. package/dist/tenant/session-streams.js +9 -27
  255. package/dist/tenant/session.d.ts +2 -13
  256. package/dist/tenant/session.js +4 -28
  257. package/dist/tenant/sessions.d.ts +6 -1
  258. package/dist/tenant/sessions.js +29 -2
  259. package/dist/tenant/signing-keys.d.ts +2 -1
  260. package/dist/tenant/signing-keys.js +2 -2
  261. package/dist/tenant/status.d.ts +8 -3
  262. package/dist/tenant/status.js +5 -0
  263. package/dist/tenant/streams.d.ts +8 -2
  264. package/dist/tenant/streams.js +25 -6
  265. package/dist/tenant/sweep.js +37 -4
  266. package/dist/tenant/types.d.ts +39 -1
  267. package/dist/tenant/worker.d.ts +11 -5
  268. package/dist/tenant/worker.js +12 -6
  269. package/dist/vault/crypto.d.ts +7 -0
  270. package/dist/vault/crypto.js +16 -0
  271. package/dist/vault/oauth.d.ts +55 -0
  272. package/dist/vault/oauth.js +186 -0
  273. package/dist/vault/service.d.ts +54 -7
  274. package/dist/vault/service.js +261 -17
  275. package/dist/vault/sources.d.ts +54 -0
  276. package/dist/vault/sources.js +185 -0
  277. package/dist/version.d.ts +1 -1
  278. package/dist/version.js +1 -1
  279. package/package.json +14 -10
  280. package/dist/adapters/media.d.ts +0 -24
  281. package/dist/adapters/media.js +0 -111
  282. package/dist/host/cors.d.ts +0 -27
  283. package/dist/host/cors.js +0 -64
  284. package/dist/tenant/access-policy.d.ts +0 -30
  285. package/dist/tenant/access-policy.js +0 -52
  286. package/dist/tenant/browser.d.ts +0 -15
  287. package/dist/tenant/browser.js +0 -39
  288. package/dist/tenant/subject-limits.d.ts +0 -16
  289. package/dist/tenant/subject-limits.js +0 -45
  290. package/dist/tenant/tokens.d.ts +0 -14
  291. package/dist/tenant/tokens.js +0 -180
@@ -5,8 +5,10 @@ import { tenantModelGate } from "../gates/in-process.js";
5
5
  import { createKekFile, readVaultKek } from "../vault/kek.js";
6
6
  import { SigningKeys } from "./signing-keys.js";
7
7
  import { VaultService } from "../vault/service.js";
8
+ import { CredentialSources } from "../vault/sources.js";
8
9
  import { McpPool } from "../mcp/pool.js";
9
10
  import { SandboxManager } from "../sandbox/manager.js";
11
+ import { storeSandboxRecords } from "../sandbox/records.js";
10
12
  import { defaultSandboxBackends } from "../sandbox/select.js";
11
13
  import { MemoryExecution } from "../execution/memory.js";
12
14
  import { openError } from "./cause.js";
@@ -20,8 +22,18 @@ import { deliverAction } from "./delivery.js";
20
22
  import { TenantWorkers, WORKER_ID, } from "./worker.js";
21
23
  import { authorize } from "./effects.js";
22
24
  import { inProcessToolGate } from "../gates/tool-gate.js";
25
+ import { guardedFetch } from "./outbound.js";
23
26
  import { inProcessKeys } from "../keys/keys.js";
24
27
  import { tenantApi } from "../api/http/app.js";
28
+ import { createFsBlobStore } from "../blob/index.js";
29
+ import { createHarnessApiServer, } from "../harness-api/server.js";
30
+ import { harnessStatusOf } from "./status.js";
31
+ import { startInProcessHarness, startLoopbackHarness, } from "../harness-api/in-process.js";
32
+ import { localWorkspace, remoteWorkspace, withPodWorkspaces, } from "../harness-api/workspace.js";
33
+ import { hostAuthority } from "../sandbox/join.js";
34
+ import { reconcileSandbox } from "../sandbox/pods/reconcile.js";
35
+ import { sandboxWorkspaceReader } from "../artifacts/workspace.js";
36
+ import { operatorKeys } from "./operator-keys.js";
25
37
  /** Default ownership lease of an advance; the heartbeat renews it every third. */
26
38
  const DEFAULT_OWNER_LEASE_MS = 30_000;
27
39
  function validateConfig(config) {
@@ -45,16 +57,24 @@ export class TenantRuntime {
45
57
  worker;
46
58
  detach;
47
59
  closeGraceMs;
60
+ harness;
61
+ hosts;
48
62
  constructor(ctx, envelope,
49
63
  /** This Tenant's Worker handlers, as registered with the execution. */
50
64
  worker, detach,
51
65
  /** How long close waits for running advances: the execution's advance grace period. */
52
- closeGraceMs) {
66
+ closeGraceMs, harness, hosts) {
53
67
  this.ctx = ctx;
54
68
  this.envelope = envelope;
55
69
  this.worker = worker;
56
70
  this.detach = detach;
57
71
  this.closeGraceMs = closeGraceMs;
72
+ this.harness = harness;
73
+ this.hosts = hosts;
74
+ }
75
+ /** The Tenant's side of a sandbox pod's join (F7.2). */
76
+ hostAuthority() {
77
+ return this.ctx.closing || this.ctx.closed ? undefined : this.hosts;
58
78
  }
59
79
  static async open(config, hooks) {
60
80
  const envelope = hooks.envelope;
@@ -69,6 +89,7 @@ export class TenantRuntime {
69
89
  const store = hooks.store;
70
90
  let wired;
71
91
  let detach;
92
+ let harness;
72
93
  try {
73
94
  // With the keys service (F4.2) the key lives in the gateway: this process never reads,
74
95
  // creates or holds it, and the gateway reports a missing key on its readiness.
@@ -101,38 +122,75 @@ export class TenantRuntime {
101
122
  const vault = new VaultService({
102
123
  store: opened,
103
124
  kek: ensureKek,
104
- fetch: config.vaultFetch ?? globalThis.fetch,
125
+ // OAuth refresh and connect follow the Host's address policy (F9 C2).
126
+ fetch: config.vaultFetch ?? guardedFetch(config.delivery ?? {}),
127
+ });
128
+ const credentials = new CredentialSources({
129
+ vault,
130
+ ...(config.resolver ? { resolver: config.resolver } : {}),
105
131
  });
106
132
  // `ctx` is assigned below; these callbacks only run once the Tenant is open.
107
133
  let ctx;
108
134
  const toolGate = hooks.toolGate ?? inProcessToolGate(config.delivery ?? {});
109
135
  const signingKeys = new SigningKeys({ tenantId: config.tenantId, kek: ensureKek });
110
136
  const keys = hooks.keys ?? inProcessKeys({ store: opened, vault, signingKeys, kek: ensureKek });
111
- const mcp = new McpPool({
112
- pluginData: paths.pluginData,
113
- childEnv: config.childEnv,
114
- authorize: (sessionId, request) => authorize(ctx, sessionId, request),
115
- ...(toolGate.openMcp ? { openRemote: (server) => toolGate.openMcp(server) } : {}),
116
- });
137
+ const harnessMode = hooks.harness ?? "memory";
138
+ const inProcess = harnessMode === "memory" || harnessMode === "json";
139
+ const preference = seededBackend ?? config.sandbox.backend;
140
+ // With a harness in this process, the Tenant's MCP pool and SandboxManager are its
141
+ // executors; otherwise the harness holds both (F6.2).
142
+ const mcp = inProcess
143
+ ? new McpPool({
144
+ pluginData: paths.pluginData,
145
+ childEnv: config.childEnv,
146
+ authorize: (sessionId, request) => authorize(ctx, sessionId, request),
147
+ ...(toolGate.openMcp ? { openRemote: (server) => toolGate.openMcp(server) } : {}),
148
+ })
149
+ : undefined;
117
150
  const ephemeral = config.mode === "ephemeral";
118
- const sandbox = new SandboxManager({
119
- scope: config.tenantId,
120
- store: opened,
121
- backends: config.sandbox.backends ??
122
- defaultSandboxBackends({ root: paths.sandboxes }),
123
- preference: seededBackend ?? config.sandbox.backend,
124
- ephemeral,
125
- emit: async (sessionId, turnId, type, payload) => {
126
- if (ctx.closed)
127
- return;
128
- try {
129
- await opened.tx((t) => t.event(sessionId, turnId, type, payload));
130
- }
131
- catch {
132
- /* the session is gone (reset) or the store closed */
133
- }
134
- },
135
- });
151
+ const manager = inProcess
152
+ ? new SandboxManager({
153
+ scope: config.tenantId,
154
+ records: storeSandboxRecords(opened),
155
+ backends: config.sandbox.backends ??
156
+ defaultSandboxBackends({ root: paths.sandboxes }),
157
+ preference,
158
+ ephemeral,
159
+ emit: async (sessionId, turnId, type, payload) => {
160
+ if (ctx.closed)
161
+ return;
162
+ try {
163
+ await opened.tx((t) => t.event(sessionId, turnId, type, payload));
164
+ }
165
+ catch {
166
+ /* the session is gone (reset) or the store closed */
167
+ }
168
+ },
169
+ })
170
+ : undefined;
171
+ let harnessServer;
172
+ const workspaces = manager
173
+ ? localWorkspace(manager)
174
+ : remoteWorkspace({
175
+ get server() {
176
+ return harnessServer;
177
+ },
178
+ store: opened,
179
+ logger: config.logger,
180
+ preference,
181
+ });
182
+ // A pod sandbox's workspace is in its pod, served by its engine (F7.2).
183
+ const sandbox = hooks.pods
184
+ ? withPodWorkspaces(workspaces, {
185
+ get server() {
186
+ return harnessServer;
187
+ },
188
+ store: opened,
189
+ logger: config.logger,
190
+ preference,
191
+ isPod: async (sandboxId) => (await opened.tx((t) => t.sandboxResource(sandboxId)))?.kind === "pod",
192
+ })
193
+ : workspaces;
136
194
  const useVaultModel = config.model.kind === "vault";
137
195
  let modelProvider;
138
196
  if (hooks.modelProvider)
@@ -143,6 +201,7 @@ export class TenantRuntime {
143
201
  modelProvider = toolFixtureModel();
144
202
  else
145
203
  modelProvider = scriptedModel();
204
+ const blobs = hooks.blobs ?? createFsBlobStore({ root: paths.blobs });
146
205
  // With the gates service the loop never reads a model credential: only a Runtime
147
206
  // without one (embedding, the ephemeral Runtime, tests) reads it here.
148
207
  const modelGate = hooks.modelGate ??
@@ -151,40 +210,48 @@ export class TenantRuntime {
151
210
  kek: ensureKek,
152
211
  root: paths.home,
153
212
  logger: config.logger,
213
+ blobs,
154
214
  ...(config.modelCall ? { settings: config.modelCall } : {}),
155
215
  });
156
216
  const sessionStreams = createSessionStreams();
217
+ const workers = hooks.execution?.workers ?? new TenantWorkers();
157
218
  const local = hooks.execution
158
219
  ? undefined
159
220
  : new MemoryExecution({
160
221
  sweepIntervalMs: config.sweepIntervalMs ?? Math.min(config.leaseMs ?? 30_000, 5000),
222
+ // Close's `detach` step waits no longer for running work than for its advances.
223
+ stopGraceMs: workers.graceMs,
161
224
  onError: (error) => config.logger.error("tenant execution failed", {
162
225
  message: error instanceof Error ? error.message : String(error),
163
226
  }),
164
227
  });
165
- const { execution, workers } = hooks.execution ?? {
166
- execution: local,
167
- workers: new TenantWorkers(),
168
- };
228
+ const execution = hooks.execution?.execution ?? local;
169
229
  const sweepHooks = new Set();
170
230
  ctx = {
171
231
  config,
172
232
  envelope,
173
233
  store: opened,
174
234
  vault,
175
- mcp,
235
+ credentials,
236
+ ...(mcp ? { mcp } : {}),
176
237
  sandbox,
177
238
  flowLimits,
178
239
  modelProvider,
179
240
  useVaultModel,
180
241
  modelGate,
181
242
  toolGate,
243
+ get harness() {
244
+ return harnessServer;
245
+ },
182
246
  closing: false,
183
247
  closed: false,
184
248
  work: createWorkState(),
185
249
  sessionStreams,
186
250
  signingKeys,
187
251
  keys,
252
+ blobs,
253
+ workspaces: sandboxWorkspaceReader(sandbox),
254
+ ...(hooks.runGrants ? { runGrants: hooks.runGrants } : {}),
188
255
  workerId: hooks.workerId ?? WORKER_ID,
189
256
  ownerLeaseMs: config.ownerLeaseMs ?? DEFAULT_OWNER_LEASE_MS,
190
257
  wake: async (sessionId, wake) => {
@@ -198,6 +265,12 @@ export class TenantRuntime {
198
265
  return;
199
266
  await execution.deliver(config.tenantId, actionId);
200
267
  },
268
+ ...(hooks.pods ? { pods: hooks.pods } : {}),
269
+ sandboxSignal: async (sandboxId, signal) => {
270
+ if (ctx.closing || ctx.closed || !execution.sandbox)
271
+ return;
272
+ await execution.sandbox(config.tenantId, sandboxId, signal);
273
+ },
201
274
  ...(hooks.execution?.stuckInvocations
202
275
  ? {
203
276
  stuckInvocations: () => hooks.execution.stuckInvocations(config.tenantId),
@@ -209,6 +282,16 @@ export class TenantRuntime {
209
282
  return () => sweepHooks.delete(hook);
210
283
  },
211
284
  };
285
+ harnessServer = createHarnessApiServer(ctx, { sandboxPreference: preference });
286
+ if (mcp && manager)
287
+ harness = await startInProcessHarness(ctx, { mcp, sandbox: manager }, {
288
+ json: harnessMode === "json",
289
+ ...(hooks.harnessTap ? { tap: hooks.harnessTap } : {}),
290
+ });
291
+ else if (harnessMode === "ws")
292
+ harness = await startLoopbackHarness(ctx, {
293
+ ...(config.sandbox.backends ? { sandboxBackends: config.sandbox.backends } : {}),
294
+ });
212
295
  wired = await wireStreams(ctx, {
213
296
  store: opened,
214
297
  streams: hooks.streams ?? new MemoryStreams(),
@@ -223,6 +306,7 @@ export class TenantRuntime {
223
306
  advance: (sessionId, signal) => advance(ctx, sessionId, signal),
224
307
  deliver: (actionId, signal) => deliverAction(ctx, actionId, signal),
225
308
  sweep: () => sweep(ctx),
309
+ sandbox: (sandboxId, trigger) => reconcileSandbox(ctx, sandboxId, trigger),
226
310
  };
227
311
  const unregister = workers.register(config.tenantId, worker);
228
312
  detach = async () => {
@@ -232,10 +316,11 @@ export class TenantRuntime {
232
316
  if (local)
233
317
  await local.start(workers.handlers);
234
318
  await execution.armSweep(config.tenantId);
235
- return new TenantRuntime(ctx, envelope, worker, detach, workers.graceMs);
319
+ return new TenantRuntime(ctx, envelope, worker, detach, workers.graceMs, harness, hooks.pods ? hostAuthority(ctx) : undefined);
236
320
  }
237
321
  catch (error) {
238
322
  await detach?.().catch(() => undefined);
323
+ await harness?.stop().catch(() => undefined);
239
324
  await wired?.close().catch(() => undefined);
240
325
  await store.close().catch(() => undefined);
241
326
  throw error;
@@ -256,6 +341,7 @@ export class TenantRuntime {
256
341
  inFlightDeliveries: [...this.ctx.work.deliveries.values()].reduce((n, set) => n + set.size, 0),
257
342
  pendingActions: counts.pendingActions,
258
343
  uncertainEffects: counts.uncertainEffects,
344
+ harness: harnessStatusOf(this.ctx),
259
345
  };
260
346
  }
261
347
  drain(activeWork, timeoutMs = 30_000) {
@@ -279,6 +365,13 @@ export class TenantRuntime {
279
365
  onSweep(hook) {
280
366
  return this.ctx.onSweep(hook);
281
367
  }
368
+ attachHarness(channel, peer) {
369
+ return this.ctx.harness.attach(channel, peer);
370
+ }
371
+ /** The Tenant's operator keys (F9 I1), over its store. */
372
+ operatorKeys() {
373
+ return operatorKeys(this.ctx.store);
374
+ }
282
375
  async close() {
283
376
  const ctx = this.ctx;
284
377
  ctx.closing = true;
@@ -289,10 +382,18 @@ export class TenantRuntime {
289
382
  // error is rethrown at the end.
290
383
  const steps = [
291
384
  ["detach", () => this.detach()],
292
- ["mcp", () => ctx.mcp.close()],
385
+ ["mcp", () => ctx.mcp?.close()],
293
386
  ["session streams", () => endAllStreams(ctx.sessionStreams)],
294
387
  // Bounded: an advance that ignores its abort is abandoned; its lease lapses (§11.4).
295
388
  ["idle", () => waitForIdle(ctx, Math.max(0, idleBy - Date.now()))],
389
+ [
390
+ "harness",
391
+ async () => {
392
+ ctx.harness.close();
393
+ // Advances still running were abandoned at "idle": their runs are left behind too.
394
+ await this.harness?.stop(Math.max(0, idleBy - Date.now()));
395
+ },
396
+ ],
296
397
  ["sandbox", () => ctx.sandbox.close()],
297
398
  [
298
399
  "streams",
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Sandboxes as a resource (blueprint D39, F7.1; Host feature `sandboxes`): `PUT`, `GET` and
3
+ * `DELETE /v1/sandboxes/{id}`, the list by label, sessions attaching with `sandbox: { id }`, and
4
+ * the checks every turn start makes on the sandbox its session is attached to.
5
+ *
6
+ * - **No scope.** The Runtime decides nothing about who shares a sandbox. It stores the
7
+ * sandbox's id, kind, spec and labels, and the session body names the sandbox it is attached
8
+ * to (`Session.sandboxId`); deleting a session (a sessions reset) only detaches it.
9
+ * - **Access.** An application key reaches every sandbox, and so does an application key acting
10
+ * for a subject (`Nylorun-Subject`), which needs `sandboxes:write` to change one. A token
11
+ * caller (a trusted issuer's token) reaches only the ids its grants match, and changes them
12
+ * only with `sandboxes:write`; any other id is the 404 of a missing one.
13
+ * - **Serial turns.** One turn at a time per sandbox: a session whose sandbox another attached
14
+ * session's turn holds is refused (`409 sandbox_busy`), as the session sandbox route is
15
+ * refused while its session's turn runs. Commands within the workspace keep queueing in the
16
+ * Sandbox Manager.
17
+ * - **Kinds.** `virtual`: its workspace is the virtual backend's, keyed by the sandbox id
18
+ * (`SandboxManager.sandboxKeyOf`) and created on the first tool call. `pod` (F7.2, Host feature
19
+ * `sandbox-pods`): an agent-sandbox pod on the Tenant's cluster, created at once, whose engine
20
+ * runs the turns of the sessions attached to it. Its lifecycle (`sandbox/pods/lifecycle.ts`)
21
+ * runs in the `Sandbox` object: this module records what is wanted (`desired`, `rev`) and asks
22
+ * for a reconcile after the commit (`ctx.sandboxSignal`). Without sandbox pods kind `pod` is
23
+ * `409 sandbox_unavailable`. `POST .../stop` suspends a pod (the volume is kept; the next turn
24
+ * starts it), `POST .../reset` gives it a new pod on a new volume. An expired pod refuses
25
+ * turns (`sandbox_expired`) until a `PUT` sets a longer `lifecycle.ttl`; a lost one
26
+ * (`sandbox_lost`) until a reset. Deleting a pod sandbox keeps its row (`desired: deleted`,
27
+ * invisible here) until its Sandbox is gone.
28
+ * - **Lifecycle events** go to the sandbox's own stream in the record (`Tx.sandboxEvent`,
29
+ * `record/sandbox.ts`); the session's log records `sandbox.attached` too.
30
+ */
31
+ import { type PutSandboxRequest, type SandboxView } from "@nylorun/core/contracts";
32
+ import type { SandboxResource, Tx } from "../store/types.js";
33
+ import type { AuthScope, Session, TenantContext } from "./context.js";
34
+ /** The sandbox grants that limit `scope`, or undefined when it reaches every sandbox. */
35
+ export declare function sandboxGrantsOf(scope: AuthScope): readonly string[] | undefined;
36
+ /** A sandbox resource, unless it is gone or being deleted. */
37
+ export declare function liveSandbox(t: Tx, id: string, options?: {
38
+ lock?: boolean;
39
+ }): Promise<SandboxResource | undefined>;
40
+ /**
41
+ * `PUT /v1/sandboxes/{id}`: creates the sandbox (within the Tenant's limit on their number), or
42
+ * finds the one with this id, so "get or create" is one call. The spec is fixed once it exists;
43
+ * labels, when sent, replace its labels. On an existing pod sandbox it also starts a stopped or
44
+ * failed pod again, and a new `lifecycle.ttl` sets a new expiry (reviving an expired one).
45
+ */
46
+ export declare function putSandbox(ctx: TenantContext, id: string, body: PutSandboxRequest, scope: AuthScope): Promise<SandboxView>;
47
+ /** `GET /v1/sandboxes/{id}`. */
48
+ export declare function getSandbox(ctx: TenantContext, id: string, scope: AuthScope): Promise<SandboxView>;
49
+ /** `GET /v1/sandboxes?label=k=v`: every sandbox with all the labels, that the caller reaches. */
50
+ export declare function listSandboxes(ctx: TenantContext, labels: Record<string, string>, scope: AuthScope): Promise<{
51
+ sandboxes: SandboxView[];
52
+ }>;
53
+ /**
54
+ * `DELETE /v1/sandboxes/{id}`: deletes the sandbox and its workspace. Refused while a session
55
+ * attached to it has an active turn. Sessions stay attached by id: their next turn is refused
56
+ * (`sandbox_unavailable`) unless a sandbox with this id exists again. A pod sandbox's row stays
57
+ * (`desired: deleted`) until its Sandbox is gone.
58
+ */
59
+ export declare function deleteSandbox(ctx: TenantContext, id: string, scope: AuthScope): Promise<{
60
+ id: string;
61
+ deleted: boolean;
62
+ }>;
63
+ /**
64
+ * `POST /v1/sandboxes/{id}/stop`: suspends a pod sandbox. Its volume is kept, and the next turn
65
+ * of a session attached to it (or a `PUT`) starts it again. Refused during a turn.
66
+ */
67
+ export declare function stopSandbox(ctx: TenantContext, id: string, scope: AuthScope): Promise<SandboxView>;
68
+ /**
69
+ * `POST /v1/sandboxes/{id}/reset`: a new pod on a new, empty volume (the next volume
70
+ * generation), with a new join token; the old Sandbox and volume are deleted. Leaves `lost` and
71
+ * `failed`; an expired sandbox stays expired. Refused during a turn, and while an earlier
72
+ * reset's Sandbox is still being deleted.
73
+ */
74
+ export declare function resetSandbox(ctx: TenantContext, id: string, scope: AuthScope): Promise<SandboxView>;
75
+ /** `GET /v1/sandboxes/{id}/events`: the sandbox's lifecycle stream. */
76
+ export declare function sandboxEventsOf(ctx: TenantContext, id: string, scope: AuthScope, fromSeq: number | undefined): Promise<{
77
+ events: {
78
+ schema: "nylorun.sandbox-event/1";
79
+ eventId: string;
80
+ tenantId: string;
81
+ sandboxId: string;
82
+ seq: number;
83
+ time: string;
84
+ type: "sandbox.attached" | "sandbox.created" | "sandbox.deleted" | "sandbox.detached" | "sandbox.expired" | "sandbox.failed" | "sandbox.lost" | "sandbox.relaunched" | "sandbox.reset" | "sandbox.running" | "sandbox.suspended";
85
+ payload: Record<string, unknown>;
86
+ }[];
87
+ }>;
88
+ /**
89
+ * Attaches a new session to sandbox `id` (`PutSessionRequest.sandbox = { id }`): the sandbox
90
+ * must exist and be reachable with `grants`. Returns its spec, which the session pins. Runs in
91
+ * the session's transaction, after its session row lock.
92
+ */
93
+ export declare function attachSandbox(t: Tx, id: string, grants: readonly string[] | undefined): Promise<SandboxResource>;
94
+ /** Records the attachment on the session's log and on the sandbox's stream. */
95
+ export declare function recordAttachment(t: Tx, sessionId: string, sandboxId: string): Promise<void>;
96
+ /**
97
+ * Checks a turn start on a session attached to a sandbox, in the command's transaction after
98
+ * the session row lock: the sandbox still exists, a token caller's grants reach it, and
99
+ * no other session attached to it has a turn running. Holds the sandbox row's lock until the
100
+ * transaction ends, so two turn starts on one sandbox are decided one after the other.
101
+ *
102
+ * A pod sandbox must also be usable: not lost (`sandbox_lost`) or expired (`sandbox_expired`),
103
+ * on a Runtime with sandbox pods (`sandbox_unavailable`). A stopped or failed pod is asked to
104
+ * run again, after the commit.
105
+ */
106
+ export declare function checkSandboxTurn(t: Tx, session: Session, scope: AuthScope, ctx?: TenantContext): Promise<void>;
107
+ /** A sessions reset detaches every session from its sandbox: each sandbox's stream says so. */
108
+ export declare function detachAllSessions(t: Tx): Promise<void>;
109
+ /**
110
+ * Marks the end of a turn's segment on pod sandbox `id`: idle counts from now, and the idle
111
+ * timer is armed (D34). Best effort.
112
+ */
113
+ export declare function touchPodSandbox(ctx: TenantContext, id: string): Promise<void>;
114
+ /**
115
+ * Re-arms the idle timer of every running pod sandbox after the Tenant's settings change, so a
116
+ * shorter `limits.idle` applies to pods already idle (D36: the settings are read whenever a
117
+ * timer is set). Best effort.
118
+ */
119
+ export declare function rearmPodTimers(ctx: TenantContext): Promise<void>;