@nylorun/runtime 0.17.0-beta → 0.19.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 (223) hide show
  1. package/CHANGELOG.md +149 -0
  2. package/README.md +88 -58
  3. package/dist/adapters/execution/restate.d.ts +1 -3
  4. package/dist/adapters/execution/restate.js +4 -31
  5. package/dist/adapters/replication/pgoutput.js +1 -1
  6. package/dist/adapters/sandbox/local.js +1 -1
  7. package/dist/adapters/sandbox/virtual.js +1 -1
  8. package/dist/api/ag-ui/routes.js +1 -1
  9. package/dist/api/ag-ui/translate.d.ts +1 -3
  10. package/dist/api/ag-ui/translate.js +2 -17
  11. package/dist/api/components.d.ts +4871 -2614
  12. package/dist/api/components.js +12 -11
  13. package/dist/api/gate/routes.d.ts +7 -8
  14. package/dist/api/gate/routes.js +58 -52
  15. package/dist/api/http/app.d.ts +6 -0
  16. package/dist/api/http/app.js +18 -14
  17. package/dist/api/http/define.d.ts +14 -2
  18. package/dist/api/http/define.js +34 -15
  19. package/dist/api/http/routes/access.d.ts +3 -3
  20. package/dist/api/http/routes/access.js +18 -17
  21. package/dist/api/http/routes/artifacts.js +2 -4
  22. package/dist/api/http/routes/files.d.ts +8 -0
  23. package/dist/api/http/routes/files.js +77 -0
  24. package/dist/api/http/routes/keys.d.ts +10 -0
  25. package/dist/api/http/routes/keys.js +64 -0
  26. package/dist/api/http/routes/me.js +5 -2
  27. package/dist/api/http/routes/reads.d.ts +66 -0
  28. package/dist/api/http/routes/reads.js +127 -0
  29. package/dist/api/http/routes/sandboxes.js +7 -4
  30. package/dist/api/http/routes/sessions.js +25 -8
  31. package/dist/api/http/routes/tenant.js +15 -18
  32. package/dist/api/http/routes/vaults.d.ts +10 -9
  33. package/dist/api/http/routes/vaults.js +27 -23
  34. package/dist/api/openapi.d.ts +10 -11
  35. package/dist/api/openapi.js +291 -61
  36. package/dist/artifacts/links.js +1 -1
  37. package/dist/core/flow-host.d.ts +44 -23
  38. package/dist/core/flow-host.js +88 -106
  39. package/dist/core/provider.d.ts +4 -1
  40. package/dist/core/provider.js +6 -1
  41. package/dist/core/sandbox-routes.d.ts +0 -9
  42. package/dist/core/sandbox-routes.js +2 -21
  43. package/dist/execution/memory.d.ts +4 -5
  44. package/dist/execution/memory.js +11 -25
  45. package/dist/execution/types.d.ts +3 -33
  46. package/dist/execution/types.js +2 -12
  47. package/dist/gates/http-tool.d.ts +86 -0
  48. package/dist/gates/http-tool.js +197 -0
  49. package/dist/gates/mcp-handler.d.ts +3 -0
  50. package/dist/gates/mcp-handler.js +1 -3
  51. package/dist/gates/meter.d.ts +2 -2
  52. package/dist/gates/meter.js +6 -0
  53. package/dist/gates/tenant-vaults.d.ts +5 -9
  54. package/dist/gates/tool-calls.d.ts +7 -15
  55. package/dist/gates/tool-calls.js +4 -4
  56. package/dist/gates/tool-client.d.ts +1 -1
  57. package/dist/gates/tool-client.js +23 -47
  58. package/dist/gates/tool-contract.d.ts +27 -39
  59. package/dist/gates/tool-contract.js +30 -21
  60. package/dist/gates/tool-gate.d.ts +25 -24
  61. package/dist/gates/tool-gate.js +11 -7
  62. package/dist/harness/calls.d.ts +10 -4
  63. package/dist/harness/calls.js +15 -6
  64. package/dist/harness/executors.d.ts +1 -1
  65. package/dist/harness/executors.js +17 -12
  66. package/dist/harness/main.d.ts +2 -2
  67. package/dist/harness/main.js +5 -13
  68. package/dist/harness/service.d.ts +4 -5
  69. package/dist/harness/service.js +9 -2
  70. package/dist/harness-api/in-process.js +2 -2
  71. package/dist/harness-api/record.d.ts +6 -3
  72. package/dist/harness-api/record.js +65 -72
  73. package/dist/harness-api/server.d.ts +1 -9
  74. package/dist/harness-api/server.js +19 -17
  75. package/dist/harness-api/start.js +1 -5
  76. package/dist/harness-api/workspace.js +2 -3
  77. package/dist/harness-api/ws-server.js +1 -1
  78. package/dist/host/app.d.ts +2 -9
  79. package/dist/host/app.js +15 -25
  80. package/dist/host/config-for.d.ts +1 -5
  81. package/dist/host/config-for.js +0 -3
  82. package/dist/host/config.d.ts +0 -5
  83. package/dist/host/create-host.d.ts +3 -33
  84. package/dist/host/create-host.js +18 -94
  85. package/dist/host/environment.d.ts +0 -3
  86. package/dist/host/environment.js +0 -9
  87. package/dist/host/gates.d.ts +4 -1
  88. package/dist/host/gates.js +3 -2
  89. package/dist/host/http.d.ts +1 -2
  90. package/dist/host/http.js +1 -7
  91. package/dist/host/index.d.ts +2 -2
  92. package/dist/host/index.js +2 -2
  93. package/dist/host/main.js +27 -34
  94. package/dist/host/operate.d.ts +29 -0
  95. package/dist/host/operate.js +223 -0
  96. package/dist/host/stack-config.d.ts +12 -12
  97. package/dist/host/stack-config.js +5 -32
  98. package/dist/index.d.ts +1 -1
  99. package/dist/index.js +1 -1
  100. package/dist/infra/workers.d.ts +1 -1
  101. package/dist/keys/keys.js +2 -2
  102. package/dist/management-openapi.json +6491 -0
  103. package/dist/mcp/connect.d.ts +8 -4
  104. package/dist/mcp/connect.js +6 -26
  105. package/dist/mcp/pool.d.ts +5 -9
  106. package/dist/mcp/pool.js +8 -19
  107. package/dist/mcp/snapshot.d.ts +8 -2
  108. package/dist/mcp/snapshot.js +29 -2
  109. package/dist/media.d.ts +1 -1
  110. package/dist/media.js +1 -1
  111. package/dist/model/pi-model.js +3 -0
  112. package/dist/openapi.json +4700 -12350
  113. package/dist/reads/access.d.ts +4 -0
  114. package/dist/reads/access.js +12 -0
  115. package/dist/reads/cursor.d.ts +11 -0
  116. package/dist/reads/cursor.js +80 -0
  117. package/dist/reads/types.d.ts +25 -0
  118. package/dist/reads/types.js +1 -0
  119. package/dist/sandbox/manager.d.ts +16 -0
  120. package/dist/sandbox/manager.js +40 -0
  121. package/dist/sandbox/session-sandbox.js +13 -3
  122. package/dist/sandbox/skills.d.ts +13 -0
  123. package/dist/sandbox/skills.js +46 -0
  124. package/dist/sandbox/types.d.ts +2 -2
  125. package/dist/store/ownership.d.ts +9 -0
  126. package/dist/store/ownership.js +13 -0
  127. package/dist/store/postgres/connect.d.ts +2 -0
  128. package/dist/store/postgres/connect.js +29 -0
  129. package/dist/store/postgres/drizzle/0011_key_roles.sql +3 -0
  130. package/dist/store/postgres/drizzle/0012_session_reads.sql +8 -0
  131. package/dist/store/postgres/drizzle/0013_definition_files.sql +16 -0
  132. package/dist/store/postgres/drizzle/0014_action_endpoints_removed.sql +2 -0
  133. package/dist/store/postgres/drizzle/meta/_journal.json +28 -0
  134. package/dist/store/postgres/reads.d.ts +3 -0
  135. package/dist/store/postgres/reads.js +272 -0
  136. package/dist/store/postgres/schema.d.ts +294 -489
  137. package/dist/store/postgres/schema.js +41 -76
  138. package/dist/store/postgres/store.js +51 -154
  139. package/dist/store/postgres/tenant.d.ts +34 -2
  140. package/dist/store/postgres/tenant.js +16 -32
  141. package/dist/store/types.d.ts +27 -96
  142. package/dist/store/types.js +0 -1
  143. package/dist/streams/control.d.ts +0 -2
  144. package/dist/streams/control.js +0 -5
  145. package/dist/streams/types.d.ts +1 -10
  146. package/dist/tenant/advance.d.ts +3 -3
  147. package/dist/tenant/advance.js +10 -7
  148. package/dist/tenant/auth.d.ts +14 -9
  149. package/dist/tenant/auth.js +24 -28
  150. package/dist/tenant/cause.d.ts +3 -3
  151. package/dist/tenant/cause.js +1 -1
  152. package/dist/tenant/commands.d.ts +2 -18
  153. package/dist/tenant/commands.js +35 -172
  154. package/dist/tenant/context.d.ts +14 -24
  155. package/dist/tenant/definition-files.d.ts +24 -0
  156. package/dist/tenant/definition-files.js +103 -0
  157. package/dist/tenant/effects.d.ts +18 -20
  158. package/dist/tenant/effects.js +56 -119
  159. package/dist/tenant/ephemeral.d.ts +7 -6
  160. package/dist/tenant/ephemeral.js +3 -11
  161. package/dist/tenant/host-token.js +1 -1
  162. package/dist/tenant/jwt.js +1 -1
  163. package/dist/tenant/module.d.ts +1 -4
  164. package/dist/tenant/module.js +3 -12
  165. package/dist/tenant/operator-keys.d.ts +34 -8
  166. package/dist/tenant/operator-keys.js +50 -18
  167. package/dist/tenant/outbound.d.ts +10 -1
  168. package/dist/tenant/outbound.js +44 -27
  169. package/dist/tenant/paths.d.ts +1 -1
  170. package/dist/tenant/paths.js +1 -3
  171. package/dist/tenant/principals.d.ts +11 -2
  172. package/dist/tenant/principals.js +27 -8
  173. package/dist/tenant/run-token.js +3 -3
  174. package/dist/tenant/runtime.d.ts +4 -6
  175. package/dist/tenant/runtime.js +19 -18
  176. package/dist/tenant/sandboxes.d.ts +4 -1
  177. package/dist/tenant/sandboxes.js +11 -0
  178. package/dist/tenant/scheduler.d.ts +1 -5
  179. package/dist/tenant/scheduler.js +2 -5
  180. package/dist/tenant/session-streams.d.ts +2 -1
  181. package/dist/tenant/session-streams.js +39 -5
  182. package/dist/tenant/session.d.ts +8 -41
  183. package/dist/tenant/session.js +0 -37
  184. package/dist/tenant/sessions.js +16 -8
  185. package/dist/tenant/signing-keys.d.ts +3 -3
  186. package/dist/tenant/signing-keys.js +5 -5
  187. package/dist/tenant/skill-tool.d.ts +20 -0
  188. package/dist/tenant/skill-tool.js +72 -0
  189. package/dist/tenant/slim.d.ts +1 -1
  190. package/dist/tenant/slim.js +1 -1
  191. package/dist/tenant/status.js +7 -12
  192. package/dist/tenant/store-pg.d.ts +15 -0
  193. package/dist/tenant/store-pg.js +7 -3
  194. package/dist/tenant/streams.d.ts +3 -10
  195. package/dist/tenant/streams.js +1 -24
  196. package/dist/tenant/sweep.js +0 -5
  197. package/dist/tenant/transcript.d.ts +2 -2
  198. package/dist/tenant/transcript.js +2 -2
  199. package/dist/tenant/types.d.ts +15 -22
  200. package/dist/tenant/worker.d.ts +1 -3
  201. package/dist/tenant/worker.js +3 -8
  202. package/dist/vault/sources.d.ts +7 -1
  203. package/dist/vault/sources.js +22 -11
  204. package/dist/version.d.ts +1 -1
  205. package/dist/version.js +1 -1
  206. package/package.json +4 -4
  207. package/dist/admin-openapi.json +0 -904
  208. package/dist/api/http/routes/actions.d.ts +0 -8
  209. package/dist/api/http/routes/actions.js +0 -75
  210. package/dist/api/http/routes/endpoints.d.ts +0 -7
  211. package/dist/api/http/routes/endpoints.js +0 -100
  212. package/dist/host/admin-api.d.ts +0 -30
  213. package/dist/host/admin-api.js +0 -140
  214. package/dist/mcp/stdio.d.ts +0 -17
  215. package/dist/mcp/stdio.js +0 -82
  216. package/dist/tenant/actions.d.ts +0 -14
  217. package/dist/tenant/actions.js +0 -29
  218. package/dist/tenant/delivery-token.d.ts +0 -41
  219. package/dist/tenant/delivery-token.js +0 -104
  220. package/dist/tenant/delivery.d.ts +0 -62
  221. package/dist/tenant/delivery.js +0 -481
  222. package/dist/tenant/endpoints.d.ts +0 -67
  223. package/dist/tenant/endpoints.js +0 -90
@@ -1,14 +1,14 @@
1
- import { embeddedAgent, flowDelegateManifest, hashManifest, isWorkflowManifestV2, } from "@nylorun/core/define";
2
- import { countActiveFlowWork, deriveAgentEffectSessionId, isFlowToolEffect, isWorkflowManifest, linkedMessageKey, linkedTurnEnd, } from "../core/flow-host.js";
1
+ import { embeddedAgent, flowDelegateManifest, hashManifest, } from "@nylorun/core/define";
2
+ import { countActiveFlowWork, deriveAgentEffectSessionId, isFlowToolEffect, isWorkflowManifest, linkedMessageInput, linkedMessageKey, linkedTurnEnd, settleAgentEffect, } from "../core/flow-host.js";
3
3
  import { mayDispatchMore } from "../core/limits.js";
4
4
  import { isRemoteMcpCall } from "../harness/calls.js";
5
+ import { isHttpToolCall } from "../gates/http-tool.js";
5
6
  import { owningSandboxSessionId, sandboxSpecOf, sessionSandboxSpec, } from "../sandbox/share.js";
6
7
  import { withSandboxCapability } from "../sandbox/session-sandbox.js";
7
8
  import { loadSession, ownedSession, sandboxLookup, sessionOf, } from "./context.js";
8
9
  import { fail } from "./http.js";
9
- import { linkedAgentOutput, turnManifestOf } from "./session.js";
10
+ import { turnManifestOf } from "./session.js";
10
11
  import { command } from "./commands.js";
11
- import { offerAction } from "./delivery.js";
12
12
  /**
13
13
  * The outcome of a flow `agent` effect once the linked turn it started ended, or undefined
14
14
  * while it has not. The linked session may still show an earlier iteration's turn.
@@ -18,7 +18,7 @@ export async function linkedOutcome(t, effect, agent) {
18
18
  if (!end || !agent)
19
19
  return undefined;
20
20
  if (end.status === "completed")
21
- return { value: linkedAgentOutput(agent, end.output) };
21
+ return { value: end.output };
22
22
  return {
23
23
  value: {
24
24
  kind: "failed",
@@ -31,16 +31,17 @@ export async function linkedOutcome(t, effect, agent) {
31
31
  };
32
32
  }
33
33
  /**
34
- * True when this Tenant's remote MCP calls outlive the process that sent them (the gates
35
- * service, F4.1 G3): after a takeover or a shutdown, the journaled call is re-sent and joins
36
- * the running call or gets its answer, instead of becoming `uncertain`.
34
+ * True when this Tenant's remote MCP and HTTP tool calls outlive the process that sent them
35
+ * (the gates service, F4.1 G3): after a takeover or a shutdown, the journaled call is re-sent
36
+ * and joins the running call or gets its answer, instead of becoming `uncertain`.
37
37
  */
38
- export function recoversMcpCalls(ctx) {
38
+ export function recoversToolCalls(ctx) {
39
39
  return ctx.toolGate.recovers === true;
40
40
  }
41
- /** True when `request` calls a tool of a remote (`streamable-http` or `sse`) MCP server. */
42
- export function isRemoteMcpEffect(s, request) {
43
- return isRemoteMcpCall({ rootManifest: s.manifest, mcpSnapshot: s.mcpSnapshot }, request);
41
+ /** True when `request` calls a tool of a declared MCP server (every one is remote) or an HTTP tool. */
42
+ export function isGateToolEffect(s, request) {
43
+ return (isRemoteMcpCall({ rootManifest: s.manifest, mcpSnapshot: s.mcpSnapshot }, request) ||
44
+ isHttpToolCall(s.manifest, request));
44
45
  }
45
46
  /**
46
47
  * True when this Tenant's vault-backed model calls outlive the process that sent them (the
@@ -50,7 +51,7 @@ export function isRemoteMcpEffect(s, request) {
50
51
  export function recoversModelCalls(ctx) {
51
52
  return ctx.useVaultModel && ctx.modelGate.recovers === true;
52
53
  }
53
- /** Journal and dispatch a new flow effect (agent / tool node / fn / verify). */
54
+ /** Journal and dispatch a new flow effect (agent or tool node). */
54
55
  export async function resolveNewFlowEffect(ctx, request, signal, lease) {
55
56
  const { store } = ctx;
56
57
  signal.throwIfAborted();
@@ -75,9 +76,7 @@ export async function resolveNewFlowEffect(ctx, request, signal, lease) {
75
76
  const outcome = await linkedOutcome(t, existing, agent);
76
77
  if (!outcome)
77
78
  return resolved({ status: "pending" });
78
- existing.status = "completed";
79
- existing.outcome = outcome;
80
- await t.put("effects", request.effectId, existing);
79
+ await settleAgentEffect({ t, effect: existing, outcome });
81
80
  return resolved({ status: "completed", outcome });
82
81
  }
83
82
  else {
@@ -93,64 +92,25 @@ export async function resolveNewFlowEffect(ctx, request, signal, lease) {
93
92
  });
94
93
  return resolved({ status: "pending" });
95
94
  }
96
- if (request.kind === "fn" ||
97
- request.kind === "verify" ||
98
- isFlowToolEffect(request)) {
99
- await t.put("effects", request.effectId, {
100
- request,
101
- status: "pending",
95
+ if (isFlowToolEffect(request)) {
96
+ // A tool stage without `http` would run the developer's code: refused at save, so this
97
+ // is a backstop (HTTP stages are executed by the harness, `harness-api/record.ts`). The
98
+ // stage fails; nothing runs.
99
+ const outcome = {
100
+ value: {
101
+ kind: "failed",
102
+ code: "tool.unavailable",
103
+ message: `The tool stage '${request.key ?? request.path ?? ""}' runs your code, and the Runtime runs no code of yours during a session`,
104
+ },
105
+ };
106
+ await t.put("effects", request.effectId, { request, status: "completed", outcome });
107
+ await t.event(workflow.id, workflow.activeTurnId, "node.started", {
108
+ path: request.path,
109
+ kind: "tool",
110
+ key: request.key,
111
+ ...(request.iterations !== undefined ? { iterations: request.iterations } : {}),
102
112
  });
103
- const action = request.kind === "fn" || request.kind === "verify"
104
- ? {
105
- actionId: request.effectId,
106
- sessionId: request.sessionId,
107
- turnId: request.turnId,
108
- agentId: request.agentId,
109
- manifestHash: request.manifestHash,
110
- implementationVersion: workflow.implementationVersion,
111
- input: request.input,
112
- context: request.context,
113
- status: "pending",
114
- generation: 0,
115
- kind: request.kind,
116
- path: request.path,
117
- key: request.key,
118
- }
119
- : {
120
- actionId: request.effectId,
121
- sessionId: request.sessionId,
122
- turnId: request.turnId,
123
- agentId: request.agentId,
124
- manifestHash: request.manifestHash,
125
- implementationVersion: workflow.implementationVersion,
126
- input: request.input,
127
- context: request.context,
128
- status: "pending",
129
- generation: 0,
130
- kind: "tool",
131
- path: request.path,
132
- key: request.key,
133
- };
134
- await t.put("actions", action.actionId, action);
135
- if (isFlowToolEffect(request)) {
136
- await t.event(workflow.id, workflow.activeTurnId, "node.started", {
137
- path: request.path,
138
- kind: "tool",
139
- key: request.key,
140
- ...(request.iterations !== undefined
141
- ? { iterations: request.iterations }
142
- : {}),
143
- });
144
- }
145
- await t.event(workflow.id, workflow.activeTurnId, "action.pending", {
146
- actionId: action.actionId,
147
- kind: action.kind,
148
- path: action.path,
149
- key: action.key,
150
- input: action.input,
151
- });
152
- await offerAction(t, ctx, action);
153
- return resolved({ status: "pending" });
113
+ return resolved({ status: "completed", outcome });
154
114
  }
155
115
  return { kind: "agent", workflow };
156
116
  });
@@ -183,7 +143,7 @@ export async function resolveNewFlowEffect(ctx, request, signal, lease) {
183
143
  await ownedSession(t, lease, workflow.id);
184
144
  if (exists)
185
145
  return;
186
- const definition = await leafDefinition(t, workflow, body, request);
146
+ const definition = leafDefinition(workflow, body, request);
187
147
  const lookup = await sandboxLookup(t, workflow.sandboxOwnerId);
188
148
  const sandboxOwnerId = sessionSandboxSpec(workflow) || workflow.sandboxOwnerId
189
149
  ? owningSandboxSessionId(workflow, lookup)
@@ -207,7 +167,6 @@ export async function resolveNewFlowEffect(ctx, request, signal, lease) {
207
167
  },
208
168
  vaultIds: workflow.vaultIds,
209
169
  credentialSelections: workflow.credentialSelections,
210
- pluginRoots: definition.pluginRoots ?? {},
211
170
  ...(sandboxOwnerId ? { sandboxOwnerId } : {}),
212
171
  ...(inherited
213
172
  ? { sandbox: inherited.spec, sandboxSource: "shared" }
@@ -217,21 +176,19 @@ export async function resolveNewFlowEffect(ctx, request, signal, lease) {
217
176
  });
218
177
  // Binds this effect to the one linked turn its message opens (`linkedTurnEnd`).
219
178
  const idempotencyKey = linkedMessageKey(request);
220
- const messageInput = body.input;
179
+ const messageInput = linkedMessageInput(body);
221
180
  const messageCommand = typeof messageInput === "string"
222
181
  ? {
223
182
  type: "message",
224
183
  content: messageInput,
225
184
  requestId: `flow-${request.effectId}`,
226
185
  idempotencyKey,
227
- ...(body.manifest ? { manifest: body.manifest } : {}),
228
186
  }
229
187
  : {
230
188
  type: "message",
231
189
  data: messageInput,
232
190
  requestId: `flow-${request.effectId}`,
233
191
  idempotencyKey,
234
- ...(body.manifest ? { manifest: body.manifest } : {}),
235
192
  };
236
193
  const accepted = (await command(ctx, agentSessionId, messageCommand, {
237
194
  kind: "application",
@@ -241,8 +198,8 @@ export async function resolveNewFlowEffect(ctx, request, signal, lease) {
241
198
  return store.tx(async (t) => {
242
199
  const s = await ownedSession(t, lease, request.sessionId);
243
200
  const agent = await sessionOf(t, agentSessionId);
244
- // Only a Loop's own agent turns are iterations; a step elsewhere in the flow is not.
245
- if (typeof request.context.loopPath === "string")
201
+ // Only a Loop body's agent turns are iterations: not its verifier, nor a step elsewhere.
202
+ if (typeof request.context.loopPath === "string" && request.context.role !== "verify-agent")
246
203
  await t.event(s.id, s.activeTurnId, "loop.iteration", {
247
204
  path: request.context.loopPath,
248
205
  n,
@@ -259,11 +216,10 @@ export async function resolveNewFlowEffect(ctx, request, signal, lease) {
259
216
  // May already be settled if the agent was fast / replayed.
260
217
  const outcome = await linkedOutcome(t, { request, agentSessionId }, agent);
261
218
  if (outcome) {
262
- await t.put("effects", request.effectId, {
263
- request,
264
- status: "completed",
219
+ await settleAgentEffect({
220
+ t,
221
+ effect: { request, status: "pending", agentSessionId },
265
222
  outcome,
266
- agentSessionId,
267
223
  });
268
224
  return { status: "completed", outcome };
269
225
  }
@@ -286,19 +242,18 @@ function inheritedSandbox(owner, definition) {
286
242
  return { spec, manifest: sandboxed.manifest, manifestHash: sandboxed.manifestHash };
287
243
  }
288
244
  /**
289
- * The credential of an MCP server request made on behalf of a session: from its attached vaults,
290
- * else the operator's credential resolver (`vault/sources.ts`).
245
+ * The credential of an MCP server or HTTP tool request made on behalf of a session: from its
246
+ * attached vaults, else the operator's credential resolver (`vault/sources.ts`).
291
247
  */
292
248
  export async function authorize(ctx, sessionId, request) {
293
249
  const s = await loadSession(ctx, sessionId);
294
250
  return ctx.credentials.authorize({ ...s, id: sessionId }, request);
295
251
  }
296
252
  /**
297
- * The definition a flow leaf's session runs. A v2 workflow embeds its leaves, so they
298
- * come from the workflow's own manifest and can't drift from it; v1 leaves come from the
299
- * registry by id.
253
+ * The definition a flow leaf's session runs. A flow agent embeds its leaves, so they come
254
+ * from the workflow's own manifest and can't drift from it.
300
255
  */
301
- async function leafDefinition(t, workflow, body, request) {
256
+ function leafDefinition(workflow, body, request) {
302
257
  // A flow agent used as a tool: its manifest is inlined in the parent's pinned tool.
303
258
  if (request.context.role === "delegate" && !isWorkflowManifest(workflow.manifest)) {
304
259
  const flow = flowDelegateManifest(turnManifestOf(workflow), body.agentId);
@@ -308,35 +263,17 @@ async function leafDefinition(t, workflow, body, request) {
308
263
  manifest: flow,
309
264
  manifestHash: hashManifest(flow),
310
265
  implementationVersion: workflow.implementationVersion,
311
- pluginRoots: leafPluginRoots(workflow.pluginRoots, body.agentId),
312
266
  };
313
267
  }
314
- if (isWorkflowManifestV2(workflow.manifest)) {
315
- const leaf = embeddedAgent(workflow.manifest, body.flow ?? [], body.agentId);
316
- if (!leaf || isWorkflowManifestV2(leaf))
317
- fail(404, `Agent '${body.agentId}' is not embedded in workflow '${workflow.manifest.id}'`);
318
- const manifest = leaf;
319
- return {
320
- manifest,
321
- manifestHash: hashManifest(manifest),
322
- implementationVersion: workflow.implementationVersion,
323
- pluginRoots: leafPluginRoots(workflow.pluginRoots, body.agentId),
324
- };
325
- }
326
- const definition = await t.get("definitions", body.agentId);
327
- if (!definition)
328
- fail(404, "Definition not found");
329
- return definition;
330
- }
331
- /**
332
- * A v2 workflow keys its leaves' plugin roots `<agentId>/<capability>`, and an agent keys
333
- * a flow agent it uses as a tool the same way (`<flowId>/<leafId>/<capability>`).
334
- */
335
- function leafPluginRoots(roots, agentId) {
336
- const own = {};
337
- const prefix = `${agentId}/`;
338
- for (const [key, root] of Object.entries(roots ?? {}))
339
- if (key.startsWith(prefix))
340
- own[key.slice(prefix.length)] = root;
341
- return own;
268
+ const leaf = isWorkflowManifest(workflow.manifest)
269
+ ? embeddedAgent(workflow.manifest, body.flow ?? [], body.agentId)
270
+ : undefined;
271
+ if (!leaf || isWorkflowManifest(leaf))
272
+ return fail(404, `Agent '${body.agentId}' is not embedded in workflow '${workflow.manifest.id}'`);
273
+ const manifest = leaf;
274
+ return {
275
+ manifest,
276
+ manifestHash: hashManifest(manifest),
277
+ implementationVersion: workflow.implementationVersion,
278
+ };
342
279
  }
@@ -15,6 +15,11 @@ export interface StartEphemeralRuntimeOptions {
15
15
  name?: string;
16
16
  /** An application key for an application principal of the Tenant. Default: a new key. */
17
17
  applicationKey?: string;
18
+ /**
19
+ * A management key (protocol 8), registered as the key `bootstrap` like
20
+ * `NYLORUN_MANAGEMENT_KEY_FILE`. Default: a new key.
21
+ */
22
+ managementKey?: string;
18
23
  adminKey?: string;
19
24
  principalId?: string;
20
25
  /**
@@ -22,14 +27,10 @@ export interface StartEphemeralRuntimeOptions {
22
27
  * admin key derives (`deriveStudioToken` in `@nylorun/admin`).
23
28
  */
24
29
  studioCredentialHash?: string;
25
- /** Allowlisted baseline for childEnv (e.g. PATH). Never read from ambient here. */
26
- baseline?: Readonly<Record<string, string>>;
27
30
  model?: TenantModelConfig;
28
31
  sandboxBackend?: "auto" | "virtual";
29
32
  /** When true, close() leaves hostRoot on disk. */
30
33
  retainRoot?: boolean;
31
- /** Serve the Admin API on its own loopback listener (`adminUrl`). Default off. */
32
- operatorListener?: boolean;
33
34
  /**
34
35
  * Trusted issuers whose JWTs the Tenant API accepts (Host feature `trusted-issuers`): the
35
36
  * issuers of an identity file (`parseIdentityFile`), or ones already built with
@@ -52,10 +53,10 @@ export interface StartEphemeralRuntimeOptions {
52
53
  }
53
54
  export interface EphemeralRuntime {
54
55
  url: string;
55
- /** Where the Admin API answers: `url`, or the operator listener when requested. */
56
- adminUrl: string;
57
56
  tenantId: string;
58
57
  applicationKey: string;
58
+ /** The Management API's key (`/v1/tenant/*`), principal `bootstrap`. */
59
+ managementKey: string;
59
60
  adminKey: string;
60
61
  principalId: string;
61
62
  hostRoot: string;
@@ -65,7 +65,6 @@ export async function startEphemeralRuntime(options) {
65
65
  warn() { },
66
66
  error() { },
67
67
  };
68
- const baseline = options.baseline ?? {};
69
68
  const model = options.model ?? { kind: "scripted", output: "ok" };
70
69
  const issuers = options.issuers === undefined
71
70
  ? undefined
@@ -84,11 +83,6 @@ export async function startEphemeralRuntime(options) {
84
83
  model,
85
84
  ...(issuers ? { issuers } : {}),
86
85
  ...(options.resolver ? { resolver: options.resolver } : {}),
87
- childEnv: Object.freeze({
88
- ...baseline,
89
- HOME: tenant.home,
90
- TMPDIR: tenant.tmp,
91
- }),
92
86
  logger: createTenantLogger({
93
87
  tenantId,
94
88
  logPath: tenant.log,
@@ -100,6 +94,7 @@ export async function startEphemeralRuntime(options) {
100
94
  ? createPostgresClient(options.database)
101
95
  : undefined;
102
96
  const applicationKey = options.applicationKey ?? mintBearerToken();
97
+ const managementKey = options.managementKey ?? mintBearerToken();
103
98
  const principalId = options.principalId ?? `principal_${randomBytes(8).toString("hex")}`;
104
99
  const module = createTenantModule({
105
100
  open: createPostgresTenantOpener({
@@ -111,6 +106,7 @@ export async function startEphemeralRuntime(options) {
111
106
  principals: hostPrincipals({
112
107
  adminKey,
113
108
  application: { principalId, key: applicationKey },
109
+ bootstrapKey: managementKey,
114
110
  ...(options.studioCredentialHash
115
111
  ? { studioCredentialHash: options.studioCredentialHash }
116
112
  : {}),
@@ -147,12 +143,8 @@ export async function startEphemeralRuntime(options) {
147
143
  hostRoot,
148
144
  module,
149
145
  config: hostConfig,
150
- credentials,
151
146
  logger,
152
147
  coreVersion: coreVersion(),
153
- ...(options.operatorListener
154
- ? { operator: { host: "127.0.0.1", port: 0 } }
155
- : {}),
156
148
  });
157
149
  // Opens the Tenant: creates it in the database first when it holds none.
158
150
  await host.listen();
@@ -173,9 +165,9 @@ export async function startEphemeralRuntime(options) {
173
165
  let closed = false;
174
166
  return {
175
167
  url: started.url,
176
- adminUrl: started.adminUrl,
177
168
  tenantId,
178
169
  applicationKey,
170
+ managementKey,
179
171
  adminKey,
180
172
  principalId,
181
173
  hostRoot,
@@ -17,7 +17,7 @@
17
17
  * The host token is accepted only by the Harness API listener (a connection serving that one
18
18
  * sandbox, and `host.renew`); the egress token only by egress-gate, as the proxy credential
19
19
  * (`Proxy-Authorization`). Neither is a bearer anywhere else: the Tenant API's bearer check
20
- * accepts trusted issuers' and delivery tokens only, and the gates run tokens only.
20
+ * accepts trusted issuers' tokens only, and the gates run tokens only.
21
21
  *
22
22
  * Verification reads the key row on every call (a revoked key refuses at once) and, with
23
23
  * `currentHost`, the sandbox's row (one indexed read): the epoch must be the sandbox's, the
@@ -1,5 +1,5 @@
1
1
  /**
2
- * What the Runtime's own tokens share (delivery tokens, capability links, run and host tokens):
2
+ * What the Runtime's own tokens share (capability links, run and host tokens):
3
3
  * the Tenant's ES256 JWTs, recognised by shape, told apart by their `typ`, and refused before
4
4
  * any key is read when the header is not one the Runtime issues.
5
5
  */
@@ -1,4 +1,3 @@
1
- import type { StreamRelayStatus } from "@nylorun/core/contracts";
2
1
  import { TenantUnavailableError, type Logger, type TenantHandle, type TenantModule, type TenantOpener } from "./types.js";
3
2
  export interface CreateTenantModuleOptions {
4
3
  /** Opens the Host's Tenant (`store-pg.ts`). */
@@ -11,15 +10,13 @@ export interface CreateTenantModuleOptions {
11
10
  * Tenant's id (its stream relay). A failure is logged; the Tenant stays open.
12
11
  */
13
12
  onOpen?: (handle: TenantHandle) => void | Promise<void>;
14
- /** This process's stream relay, for the Host aggregate (a Host with S2 runs one). */
15
- relayStatus?: () => Promise<StreamRelayStatus>;
16
13
  }
17
14
  /**
18
15
  * Deep Tenant module (§8): the Host's one Tenant over an injected opener (tenancy.md §5).
19
16
  *
20
17
  * `start` opens it once, even when requests or Worker calls ask at the same time. An open that
21
18
  * fails for a reason in the Tenant (a `TenantOpenError`, or a timeout) is kept as the Tenant's
22
- * cause: the Host is not ready, `/v1/admin/status` names it, requests get the opaque 404.
19
+ * cause: the Host is not ready, `nylorun-operate status` names it, requests get the opaque 404.
23
20
  * One that fails outside the Tenant (`TenantUnavailableError`) is retried in the background
24
21
  * with backoff, and by the next request, which gets 503 meanwhile.
25
22
  */
@@ -10,7 +10,7 @@ const RETRY_MAX_MS = 30_000;
10
10
  *
11
11
  * `start` opens it once, even when requests or Worker calls ask at the same time. An open that
12
12
  * fails for a reason in the Tenant (a `TenantOpenError`, or a timeout) is kept as the Tenant's
13
- * cause: the Host is not ready, `/v1/admin/status` names it, requests get the opaque 404.
13
+ * cause: the Host is not ready, `nylorun-operate status` names it, requests get the opaque 404.
14
14
  * One that fails outside the Tenant (`TenantUnavailableError`) is retried in the background
15
15
  * with backoff, and by the next request, which gets 503 meanwhile.
16
16
  */
@@ -148,18 +148,9 @@ export function createTenantModule(options) {
148
148
  return { id: null, name: null, state: "unavailable", envelope: null };
149
149
  }
150
150
  },
151
- async summarize() {
151
+ harnessStatus() {
152
152
  const now = current();
153
- const summary = now.kind === "open" ? await now.handle.summary() : undefined;
154
- const relay = await options.relayStatus?.().catch(() => undefined);
155
- return {
156
- runningSessions: summary?.runningSessions ?? 0,
157
- inFlightDeliveries: summary?.inFlightDeliveries ?? 0,
158
- pendingActions: summary?.pendingActions ?? 0,
159
- uncertainEffects: summary?.uncertainEffects ?? 0,
160
- ...(relay ? { relay } : {}),
161
- ...(summary?.harness ? { harness: summary.harness } : {}),
162
- };
153
+ return now.kind === "open" ? now.handle.harnessStatus?.() : undefined;
163
154
  },
164
155
  async close() {
165
156
  const previous = current();
@@ -1,18 +1,44 @@
1
+ /**
2
+ * The Tenant's keys by name (F9 I1), managed through the Management API (`/v1/tenant/keys`,
3
+ * application keys only) and `nylorun-operate keys`. A key is a principal (F9-D11): its id is the key's name
4
+ * (`APPLICATION_KEY_ID_PATTERN`), the key is 64 hex characters (`mintBearerToken`), and the
5
+ * Tenant keeps only its SHA-256. Putting a key creates the principal or rotates its key;
6
+ * deleting it removes the row. Either way the old key stops authenticating on its next request,
7
+ * since authentication reads the principal on every request (`auth.ts`).
8
+ *
9
+ * `studio` is derived from the admin key (F9-D9): it is never put or deleted here. `bootstrap` is
10
+ * the management key a Host registers from `NYLORUN_MANAGEMENT_KEY_FILE`: its file is its only
11
+ * source. A key has a role (protocol 8): `application` reaches the Runtime API, `management` the
12
+ * Management API; a key keeps its role when it is rotated.
13
+ */
14
+ import { type KeyRole } from "@nylorun/core/compatibility";
1
15
  import type { OperatorKey, PutOperatorKeyResponse } from "@nylorun/core/contracts";
2
16
  import type { SessionStore } from "../store/types.js";
3
- /** Why a key id is refused: not a key name, or the reserved `studio`. */
17
+ /**
18
+ * Why a key is refused: not a key name, a reserved id (`studio`, `bootstrap`), or an id that
19
+ * holds the other role (`role`).
20
+ */
4
21
  export type OperatorKeyRefusal = {
5
- reason: "invalid" | "reserved";
22
+ reason: "invalid" | "reserved" | "role";
6
23
  message: string;
7
24
  };
8
- /** The refusal for `id`, or undefined when operator keys may use it. */
9
- export declare function refuseOperatorKeyId(id: string): OperatorKeyRefusal | undefined;
25
+ /**
26
+ * The refusal for `id`, or undefined when it may be `put` or deleted. `bootstrap` is never put
27
+ * here (its file is its only source) but may be deleted on the machine (AP18).
28
+ */
29
+ export declare function refuseOperatorKeyId(id: string, purpose?: "put" | "delete"): OperatorKeyRefusal | undefined;
10
30
  export interface OperatorKeys {
11
31
  /** Every principal of the Tenant, by id (Studio's and derived ones included). */
12
32
  list(): Promise<OperatorKey[]>;
13
- /** Creates key `id` or rotates it; the key is returned this once. */
14
- put(id: string): Promise<PutOperatorKeyResponse | OperatorKeyRefusal>;
15
- /** Deletes key `id`: false when there is none. */
16
- delete(id: string): Promise<boolean | OperatorKeyRefusal>;
33
+ /**
34
+ * Creates key `id` with `role` (default `application`) or rotates it; the key is returned
35
+ * this once. An id that holds the other role is refused.
36
+ */
37
+ put(id: string, role?: Exclude<KeyRole, "studio">): Promise<PutOperatorKeyResponse | OperatorKeyRefusal>;
38
+ /**
39
+ * Deletes key `id`: false when there is none. With `role`, a key of another role is refused
40
+ * (the Management API deletes application keys only).
41
+ */
42
+ delete(id: string, role?: Exclude<KeyRole, "studio">): Promise<boolean | OperatorKeyRefusal>;
17
43
  }
18
44
  export declare function operatorKeys(store: SessionStore, now?: () => string): OperatorKeys;
@@ -1,18 +1,25 @@
1
1
  /**
2
- * Operator keys (F9 I1, Host feature `operator-keys`): the Tenant's application keys, managed
3
- * by name through the Admin API. A key is a principal (F9-D11): its id is the key's name
2
+ * The Tenant's keys by name (F9 I1), managed through the Management API (`/v1/tenant/keys`,
3
+ * application keys only) and `nylorun-operate keys`. A key is a principal (F9-D11): its id is the key's name
4
4
  * (`APPLICATION_KEY_ID_PATTERN`), the key is 64 hex characters (`mintBearerToken`), and the
5
5
  * Tenant keeps only its SHA-256. Putting a key creates the principal or rotates its key;
6
6
  * deleting it removes the row. Either way the old key stops authenticating on its next request,
7
7
  * since authentication reads the principal on every request (`auth.ts`).
8
8
  *
9
- * `studio` is derived from the admin key (F9-D9): it is never put or deleted here.
9
+ * `studio` is derived from the admin key (F9-D9): it is never put or deleted here. `bootstrap` is
10
+ * the management key a Host registers from `NYLORUN_MANAGEMENT_KEY_FILE`: its file is its only
11
+ * source. A key has a role (protocol 8): `application` reaches the Runtime API, `management` the
12
+ * Management API; a key keeps its role when it is rotated.
10
13
  */
11
- import { APPLICATION_KEY_ID_PATTERN } from "@nylorun/core/compatibility";
14
+ import { APPLICATION_KEY_ID_PATTERN, BOOTSTRAP_KEY_ID, } from "@nylorun/core/compatibility";
12
15
  import { hashToken, mintBearerToken } from "../core/bearer.js";
16
+ import { PrincipalRoleConflict } from "../store/ownership.js";
13
17
  import { STUDIO_PRINCIPAL_ID } from "./principals.js";
14
- /** The refusal for `id`, or undefined when operator keys may use it. */
15
- export function refuseOperatorKeyId(id) {
18
+ /**
19
+ * The refusal for `id`, or undefined when it may be `put` or deleted. `bootstrap` is never put
20
+ * here (its file is its only source) but may be deleted on the machine (AP18).
21
+ */
22
+ export function refuseOperatorKeyId(id, purpose = "put") {
16
23
  if (!APPLICATION_KEY_ID_PATTERN.test(id))
17
24
  return {
18
25
  reason: "invalid",
@@ -23,6 +30,11 @@ export function refuseOperatorKeyId(id) {
23
30
  reason: "reserved",
24
31
  message: "The studio key is derived from the admin key: it is not put or deleted here",
25
32
  };
33
+ if (id === BOOTSTRAP_KEY_ID && purpose === "put")
34
+ return {
35
+ reason: "reserved",
36
+ message: "The bootstrap key comes from NYLORUN_MANAGEMENT_KEY_FILE: change that file to rotate it",
37
+ };
26
38
  return undefined;
27
39
  }
28
40
  export function operatorKeys(store, now = () => new Date().toISOString()) {
@@ -31,25 +43,45 @@ export function operatorKeys(store, now = () => new Date().toISOString()) {
31
43
  const rows = await store.tx((t) => t.listPrincipals());
32
44
  return rows.map(({ id, role, createdAt }) => ({ id, role, createdAt }));
33
45
  },
34
- async put(id) {
46
+ async put(id, role = "application") {
35
47
  const refused = refuseOperatorKeyId(id);
36
48
  if (refused)
37
49
  return refused;
38
50
  const key = mintBearerToken();
39
- const { row, rotated } = await store.tx(async (t) => {
40
- const existing = await t.principalById(id);
41
- return {
42
- row: await t.putPrincipal(id, hashToken(key), now()),
43
- rotated: existing !== undefined,
44
- };
45
- });
46
- return { id: row.id, role: row.role, createdAt: row.createdAt, key, rotated };
51
+ try {
52
+ const { row, rotated } = await store.tx(async (t) => {
53
+ const existing = await t.principalById(id);
54
+ return {
55
+ row: await t.putPrincipal(id, hashToken(key), now(), role),
56
+ rotated: existing !== undefined,
57
+ };
58
+ });
59
+ return { id: row.id, role: row.role, createdAt: row.createdAt, key, rotated };
60
+ }
61
+ catch (error) {
62
+ if (error instanceof PrincipalRoleConflict)
63
+ return {
64
+ reason: "role",
65
+ message: `Key ${id} is not a${role === "application" ? "n application" : " management"} key: delete it first, or choose another name`,
66
+ };
67
+ throw error;
68
+ }
47
69
  },
48
- async delete(id) {
49
- const refused = refuseOperatorKeyId(id);
70
+ async delete(id, role) {
71
+ const refused = refuseOperatorKeyId(id, "delete");
50
72
  if (refused)
51
73
  return refused;
52
- return await store.tx((t) => t.deletePrincipal(id));
74
+ return await store.tx(async (t) => {
75
+ if (role !== undefined) {
76
+ const existing = await t.principalById(id);
77
+ if (existing && existing.role !== role)
78
+ return {
79
+ reason: "role",
80
+ message: `Key ${id} is a ${existing.role} key: only the Tenant's machine manages it (nylorun-operate)`,
81
+ };
82
+ }
83
+ return await t.deletePrincipal(id);
84
+ });
53
85
  },
54
86
  };
55
87
  }
@@ -36,11 +36,15 @@ export declare const MAX_RESPONSE_BYTES: number;
36
36
  export declare function isPrivateAddress(address: string): boolean;
37
37
  /** Why the Host refuses `url`, or `undefined` when it may be called. */
38
38
  export declare function refusal(url: URL, policy: OutboundPolicy): string | undefined;
39
- /** POSTs `body` to `url` under `policy`. Never throws; `signal` aborts the request. */
39
+ /**
40
+ * POSTs (or, with `method`, PUTs or PATCHes) the JSON `body` to `url` under `policy`. Never
41
+ * throws; `signal` aborts the request.
42
+ */
40
43
  export declare function post(target: string, body: string, headers: Record<string, string>, options: {
41
44
  policy: OutboundPolicy;
42
45
  signal: AbortSignal;
43
46
  maxResponseBytes?: number;
47
+ method?: "POST" | "PUT" | "PATCH";
44
48
  }): Promise<OutboundResult>;
45
49
  /**
46
50
  * A refusal of `guardedFetch`: the Host's policy forbids the URL or every address it resolves
@@ -67,8 +71,13 @@ export declare const FETCH_TIMEOUT_MS = 30000;
67
71
  * unless the caller passes a signal. The gateway calls OAuth discovery, registration, the code
68
72
  * exchange and refresh with it. A refused URL or address rejects with `OutboundRefused`, any
69
73
  * other failure with `OutboundFailed`.
74
+ *
75
+ * `stream`: the Response resolves once the headers arrive and its body streams, unbounded and
76
+ * with no timeout but the caller's signal: remote MCP servers, whose answers and event streams
77
+ * stay open (`mcp/connect.ts`).
70
78
  */
71
79
  export declare function guardedFetch(policy: OutboundPolicy, options?: {
72
80
  maxResponseBytes?: number;
73
81
  timeoutMs?: number;
82
+ stream?: boolean;
74
83
  }): typeof fetch;