@nylorun/runtime 0.16.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 (256) hide show
  1. package/CHANGELOG.md +207 -0
  2. package/README.md +105 -63
  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/endpoint.js +1 -2
  9. package/dist/api/ag-ui/routes.d.ts +1 -1
  10. package/dist/api/ag-ui/routes.js +11 -13
  11. package/dist/api/ag-ui/translate.d.ts +1 -3
  12. package/dist/api/ag-ui/translate.js +2 -17
  13. package/dist/api/components.d.ts +4783 -2588
  14. package/dist/api/components.js +19 -21
  15. package/dist/api/gate/routes.d.ts +7 -8
  16. package/dist/api/gate/routes.js +58 -52
  17. package/dist/api/http/app.d.ts +6 -0
  18. package/dist/api/http/app.js +20 -14
  19. package/dist/api/http/define.d.ts +22 -13
  20. package/dist/api/http/define.js +43 -39
  21. package/dist/api/http/routes/access.d.ts +3 -19
  22. package/dist/api/http/routes/access.js +25 -184
  23. package/dist/api/http/routes/artifacts.js +3 -7
  24. package/dist/api/http/routes/files.d.ts +8 -0
  25. package/dist/api/http/routes/files.js +77 -0
  26. package/dist/api/http/routes/keys.d.ts +10 -0
  27. package/dist/api/http/routes/keys.js +64 -0
  28. package/dist/api/http/routes/me.d.ts +13 -0
  29. package/dist/api/http/routes/me.js +48 -0
  30. package/dist/api/http/routes/reads.d.ts +66 -0
  31. package/dist/api/http/routes/reads.js +127 -0
  32. package/dist/api/http/routes/sandboxes.d.ts +2 -2
  33. package/dist/api/http/routes/sandboxes.js +8 -7
  34. package/dist/api/http/routes/sessions.js +31 -20
  35. package/dist/api/http/routes/tenant.js +15 -18
  36. package/dist/api/http/routes/vaults.d.ts +13 -3
  37. package/dist/api/http/routes/vaults.js +136 -64
  38. package/dist/api/openapi.d.ts +10 -11
  39. package/dist/api/openapi.js +292 -68
  40. package/dist/artifacts/links.js +6 -6
  41. package/dist/artifacts/service.js +1 -1
  42. package/dist/core/flow-host.d.ts +44 -23
  43. package/dist/core/flow-host.js +88 -106
  44. package/dist/core/provider.d.ts +4 -1
  45. package/dist/core/provider.js +6 -1
  46. package/dist/core/sandbox-routes.d.ts +0 -9
  47. package/dist/core/sandbox-routes.js +2 -21
  48. package/dist/execution/memory.d.ts +4 -5
  49. package/dist/execution/memory.js +11 -25
  50. package/dist/execution/types.d.ts +3 -33
  51. package/dist/execution/types.js +2 -12
  52. package/dist/gates/http-tool.d.ts +86 -0
  53. package/dist/gates/http-tool.js +197 -0
  54. package/dist/gates/mcp-handler.d.ts +3 -0
  55. package/dist/gates/mcp-handler.js +6 -4
  56. package/dist/gates/meter.d.ts +2 -2
  57. package/dist/gates/meter.js +6 -0
  58. package/dist/gates/tenant-vaults.d.ts +19 -14
  59. package/dist/gates/tenant-vaults.js +17 -12
  60. package/dist/gates/tool-calls.d.ts +7 -15
  61. package/dist/gates/tool-calls.js +4 -4
  62. package/dist/gates/tool-client.d.ts +1 -1
  63. package/dist/gates/tool-client.js +23 -47
  64. package/dist/gates/tool-contract.d.ts +27 -39
  65. package/dist/gates/tool-contract.js +30 -21
  66. package/dist/gates/tool-gate.d.ts +25 -24
  67. package/dist/gates/tool-gate.js +11 -7
  68. package/dist/harness/calls.d.ts +10 -4
  69. package/dist/harness/calls.js +15 -6
  70. package/dist/harness/executors.d.ts +1 -1
  71. package/dist/harness/executors.js +17 -12
  72. package/dist/harness/main.d.ts +2 -2
  73. package/dist/harness/main.js +5 -13
  74. package/dist/harness/service.d.ts +4 -5
  75. package/dist/harness/service.js +9 -2
  76. package/dist/harness-api/in-process.js +2 -2
  77. package/dist/harness-api/record.d.ts +6 -3
  78. package/dist/harness-api/record.js +65 -72
  79. package/dist/harness-api/server.d.ts +1 -9
  80. package/dist/harness-api/server.js +19 -17
  81. package/dist/harness-api/start.js +1 -5
  82. package/dist/harness-api/workspace.js +2 -3
  83. package/dist/harness-api/ws-server.js +1 -1
  84. package/dist/host/app.d.ts +2 -10
  85. package/dist/host/app.js +24 -44
  86. package/dist/host/config-for.d.ts +5 -5
  87. package/dist/host/config-for.js +2 -3
  88. package/dist/host/config.d.ts +0 -11
  89. package/dist/host/create-host.d.ts +3 -40
  90. package/dist/host/create-host.js +18 -91
  91. package/dist/host/environment.d.ts +0 -3
  92. package/dist/host/environment.js +0 -9
  93. package/dist/host/gates.d.ts +4 -1
  94. package/dist/host/gates.js +9 -3
  95. package/dist/host/http.d.ts +1 -2
  96. package/dist/host/http.js +1 -7
  97. package/dist/host/index.d.ts +2 -2
  98. package/dist/host/index.js +2 -2
  99. package/dist/host/main.js +55 -40
  100. package/dist/host/operate.d.ts +29 -0
  101. package/dist/host/operate.js +223 -0
  102. package/dist/host/stack-config.d.ts +23 -20
  103. package/dist/host/stack-config.js +28 -44
  104. package/dist/index.d.ts +4 -1
  105. package/dist/index.js +3 -1
  106. package/dist/infra/workers.d.ts +1 -1
  107. package/dist/keys/client.js +9 -1
  108. package/dist/keys/keys.d.ts +12 -2
  109. package/dist/keys/keys.js +10 -2
  110. package/dist/management-openapi.json +6491 -0
  111. package/dist/mcp/connect.d.ts +8 -4
  112. package/dist/mcp/connect.js +8 -27
  113. package/dist/mcp/pool.d.ts +6 -9
  114. package/dist/mcp/pool.js +8 -18
  115. package/dist/mcp/snapshot.d.ts +8 -2
  116. package/dist/mcp/snapshot.js +29 -2
  117. package/dist/media.d.ts +1 -1
  118. package/dist/media.js +1 -1
  119. package/dist/model/pi-model.js +3 -0
  120. package/dist/openapi.json +6228 -15615
  121. package/dist/reads/access.d.ts +4 -0
  122. package/dist/reads/access.js +12 -0
  123. package/dist/reads/cursor.d.ts +11 -0
  124. package/dist/reads/cursor.js +80 -0
  125. package/dist/reads/types.d.ts +25 -0
  126. package/dist/reads/types.js +1 -0
  127. package/dist/sandbox/manager.d.ts +16 -0
  128. package/dist/sandbox/manager.js +40 -0
  129. package/dist/sandbox/session-sandbox.js +13 -3
  130. package/dist/sandbox/skills.d.ts +13 -0
  131. package/dist/sandbox/skills.js +46 -0
  132. package/dist/sandbox/types.d.ts +2 -2
  133. package/dist/store/ownership.d.ts +9 -0
  134. package/dist/store/ownership.js +13 -0
  135. package/dist/store/postgres/connect.d.ts +2 -0
  136. package/dist/store/postgres/connect.js +29 -0
  137. package/dist/store/postgres/drizzle/0008_installation_vaults.sql +2 -0
  138. package/dist/store/postgres/drizzle/0009_oauth_pending.sql +19 -0
  139. package/dist/store/postgres/drizzle/0010_oss_auth_removals.sql +3 -0
  140. package/dist/store/postgres/drizzle/0011_key_roles.sql +3 -0
  141. package/dist/store/postgres/drizzle/0012_session_reads.sql +8 -0
  142. package/dist/store/postgres/drizzle/0013_definition_files.sql +16 -0
  143. package/dist/store/postgres/drizzle/0014_action_endpoints_removed.sql +2 -0
  144. package/dist/store/postgres/drizzle/meta/_journal.json +49 -0
  145. package/dist/store/postgres/reads.d.ts +3 -0
  146. package/dist/store/postgres/reads.js +272 -0
  147. package/dist/store/postgres/schema.d.ts +423 -594
  148. package/dist/store/postgres/schema.js +84 -101
  149. package/dist/store/postgres/store.js +96 -248
  150. package/dist/store/postgres/tenant.d.ts +34 -2
  151. package/dist/store/postgres/tenant.js +16 -32
  152. package/dist/store/types.d.ts +44 -119
  153. package/dist/store/types.js +0 -1
  154. package/dist/streams/control.d.ts +0 -7
  155. package/dist/streams/control.js +0 -13
  156. package/dist/streams/types.d.ts +2 -20
  157. package/dist/streams/types.js +1 -1
  158. package/dist/tenant/advance.d.ts +3 -3
  159. package/dist/tenant/advance.js +10 -7
  160. package/dist/tenant/auth.d.ts +24 -15
  161. package/dist/tenant/auth.js +44 -55
  162. package/dist/tenant/cause.d.ts +3 -3
  163. package/dist/tenant/cause.js +1 -1
  164. package/dist/tenant/commands.d.ts +2 -18
  165. package/dist/tenant/commands.js +38 -179
  166. package/dist/tenant/context.d.ts +33 -45
  167. package/dist/tenant/definition-files.d.ts +24 -0
  168. package/dist/tenant/definition-files.js +103 -0
  169. package/dist/tenant/effects.d.ts +20 -18
  170. package/dist/tenant/effects.js +59 -133
  171. package/dist/tenant/ephemeral.d.ts +21 -8
  172. package/dist/tenant/ephemeral.js +11 -12
  173. package/dist/tenant/host-token.js +5 -5
  174. package/dist/tenant/identity-file.d.ts +75 -0
  175. package/dist/tenant/identity-file.js +202 -0
  176. package/dist/tenant/issuers.d.ts +48 -0
  177. package/dist/tenant/issuers.js +311 -0
  178. package/dist/tenant/jwt.js +3 -3
  179. package/dist/tenant/module.d.ts +1 -4
  180. package/dist/tenant/module.js +3 -12
  181. package/dist/tenant/operator-keys.d.ts +44 -0
  182. package/dist/tenant/operator-keys.js +87 -0
  183. package/dist/tenant/outbound.d.ts +40 -1
  184. package/dist/tenant/outbound.js +167 -18
  185. package/dist/tenant/paths.d.ts +1 -1
  186. package/dist/tenant/paths.js +1 -3
  187. package/dist/tenant/principals.d.ts +13 -13
  188. package/dist/tenant/principals.js +35 -36
  189. package/dist/tenant/run-token.js +6 -6
  190. package/dist/tenant/runtime.d.ts +5 -3
  191. package/dist/tenant/runtime.js +29 -15
  192. package/dist/tenant/sandboxes.d.ts +9 -6
  193. package/dist/tenant/sandboxes.js +16 -5
  194. package/dist/tenant/scheduler.d.ts +1 -5
  195. package/dist/tenant/scheduler.js +2 -5
  196. package/dist/tenant/session-streams.d.ts +8 -11
  197. package/dist/tenant/session-streams.js +48 -32
  198. package/dist/tenant/session.d.ts +8 -41
  199. package/dist/tenant/session.js +0 -37
  200. package/dist/tenant/sessions.d.ts +2 -2
  201. package/dist/tenant/sessions.js +17 -9
  202. package/dist/tenant/signing-keys.d.ts +3 -2
  203. package/dist/tenant/signing-keys.js +5 -5
  204. package/dist/tenant/skill-tool.d.ts +20 -0
  205. package/dist/tenant/skill-tool.js +72 -0
  206. package/dist/tenant/slim.d.ts +1 -1
  207. package/dist/tenant/slim.js +1 -1
  208. package/dist/tenant/status.js +7 -12
  209. package/dist/tenant/store-pg.d.ts +15 -0
  210. package/dist/tenant/store-pg.js +7 -3
  211. package/dist/tenant/streams.d.ts +3 -11
  212. package/dist/tenant/streams.js +2 -29
  213. package/dist/tenant/sweep.js +0 -5
  214. package/dist/tenant/transcript.d.ts +2 -2
  215. package/dist/tenant/transcript.js +2 -2
  216. package/dist/tenant/types.d.ts +33 -19
  217. package/dist/tenant/worker.d.ts +1 -3
  218. package/dist/tenant/worker.js +3 -8
  219. package/dist/vault/crypto.d.ts +7 -0
  220. package/dist/vault/crypto.js +16 -0
  221. package/dist/vault/oauth.d.ts +55 -0
  222. package/dist/vault/oauth.js +186 -0
  223. package/dist/vault/service.d.ts +54 -7
  224. package/dist/vault/service.js +261 -17
  225. package/dist/vault/sources.d.ts +60 -0
  226. package/dist/vault/sources.js +196 -0
  227. package/dist/version.d.ts +1 -1
  228. package/dist/version.js +1 -1
  229. package/package.json +5 -4
  230. package/dist/admin-openapi.json +0 -604
  231. package/dist/api/http/routes/actions.d.ts +0 -8
  232. package/dist/api/http/routes/actions.js +0 -76
  233. package/dist/api/http/routes/endpoints.d.ts +0 -7
  234. package/dist/api/http/routes/endpoints.js +0 -100
  235. package/dist/host/admin-api.d.ts +0 -23
  236. package/dist/host/admin-api.js +0 -74
  237. package/dist/host/cors.d.ts +0 -27
  238. package/dist/host/cors.js +0 -64
  239. package/dist/mcp/stdio.d.ts +0 -17
  240. package/dist/mcp/stdio.js +0 -82
  241. package/dist/tenant/access-policy.d.ts +0 -30
  242. package/dist/tenant/access-policy.js +0 -52
  243. package/dist/tenant/actions.d.ts +0 -14
  244. package/dist/tenant/actions.js +0 -29
  245. package/dist/tenant/browser.d.ts +0 -15
  246. package/dist/tenant/browser.js +0 -39
  247. package/dist/tenant/delivery-token.d.ts +0 -41
  248. package/dist/tenant/delivery-token.js +0 -104
  249. package/dist/tenant/delivery.d.ts +0 -62
  250. package/dist/tenant/delivery.js +0 -481
  251. package/dist/tenant/endpoints.d.ts +0 -67
  252. package/dist/tenant/endpoints.js +0 -90
  253. package/dist/tenant/subject-limits.d.ts +0 -16
  254. package/dist/tenant/subject-limits.js +0 -45
  255. package/dist/tenant/tokens.d.ts +0 -14
  256. package/dist/tenant/tokens.js +0 -187
@@ -1,11 +1,9 @@
1
1
  import { SCOPES_HEADER, SUBJECT_HEADER, } from "@nylorun/core/compatibility";
2
- import { DELIVERY_TOKEN_TYPE, parseSubjectHeaders, } from "@nylorun/core/contracts";
2
+ import { parseSubjectHeaders } from "@nylorun/core/contracts";
3
3
  import { hashToken } from "../core/bearer.js";
4
4
  import { fail, failOpaque } from "./http.js";
5
- import { looksLikeToken, verifySubjectToken } from "./tokens.js";
6
- import { verifyDeliveryToken } from "./delivery-token.js";
7
- import { tokenType } from "./jwt.js";
8
- import { readPolicy } from "./access-policy.js";
5
+ import { verifyIssuerToken } from "./issuers.js";
6
+ import { looksLikeToken } from "./jwt.js";
9
7
  const SUBJECT_INVALID = { code: "subject_invalid" };
10
8
  /** One header's value, or `undefined`; a header sent twice is refused. */
11
9
  function singleHeader(request, name) {
@@ -18,42 +16,31 @@ function singleHeader(request, name) {
18
16
  return fail(400, `${name} must be sent once`, SUBJECT_INVALID);
19
17
  return values[0];
20
18
  }
21
- export async function authenticate(ctx, request, client) {
19
+ /** Every route that does not say otherwise is a Runtime API route. */
20
+ export const RUNTIME_KEYS = { application: true, management: false };
21
+ export async function authenticate(ctx, request, keys = RUNTIME_KEYS) {
22
22
  const header = request.headers.authorization;
23
23
  const token = header?.startsWith("Bearer ") ? header.slice(7) : "";
24
24
  if (!token || !header?.startsWith("Bearer ")) {
25
- // A publishable key alone: what the policy grants `anon` (nothing by default).
26
- if (client && header === undefined) {
27
- const { anon } = await ctx.store.tx((t) => readPolicy(t));
28
- return {
29
- kind: "publishable",
30
- keyId: client.keyId,
31
- scopes: new Set(anon.scopes),
32
- agents: anon.agents === "*" ? "*" : new Set(anon.agents),
33
- };
34
- }
35
25
  ctx.config.logger.warn("credential rejected", {
36
26
  reason: "missing_bearer",
37
27
  });
38
28
  return failOpaque();
39
29
  }
40
- if (looksLikeToken(token) && tokenType(token) === DELIVERY_TOKEN_TYPE) {
41
- // Delivery tokens come back from the application's server, never from a browser.
42
- if (request.headers.origin !== undefined)
43
- fail(403, "Delivery tokens are not accepted from browsers", { code: "origin_rejected" });
44
- const scope = await verifyDeliveryToken(ctx, token);
30
+ // A trusted issuer's token (Host feature `trusted-issuers`), from a server or a browser: its
31
+ // unverified `iss` names an issuer of the identity file, and only that issuer verifies it.
32
+ const issuer = ctx.config.issuers?.claimed(token);
33
+ if (issuer) {
34
+ const scope = await verifyIssuerToken(ctx, issuer, token);
45
35
  if (singleHeader(request, SUBJECT_HEADER) !== undefined ||
46
36
  singleHeader(request, SCOPES_HEADER) !== undefined)
47
- fail(403, "A delivery token cannot act for a subject");
37
+ fail(403, "An issuer token cannot act for another subject");
48
38
  return scope;
49
39
  }
40
+ // Any other JWT: no issuer of this Host signed it (subject tokens are gone, protocol 7).
50
41
  if (looksLikeToken(token)) {
51
- const scope = await verifySubjectToken(ctx, token);
52
- // Read only after verification, so an unknown token still sees the opaque 404.
53
- if (singleHeader(request, SUBJECT_HEADER) !== undefined ||
54
- singleHeader(request, SCOPES_HEADER) !== undefined)
55
- fail(403, "A subject token cannot act for another subject");
56
- return scope;
42
+ ctx.config.logger.warn("credential rejected", { reason: "token_unknown_issuer" });
43
+ return failOpaque();
57
44
  }
58
45
  // Application keys never come from a browser or a shipped app: refused before they are even
59
46
  // looked up.
@@ -61,10 +48,6 @@ export async function authenticate(ctx, request, client) {
61
48
  fail(403, "Application keys are not accepted from browsers", {
62
49
  code: "origin_rejected",
63
50
  });
64
- if (client)
65
- fail(400, "Nylorun-Key is for browser and mobile clients, not with a Tenant key", {
66
- code: "invalid_request",
67
- });
68
51
  const tokenHash = hashToken(token);
69
52
  const principal = await ctx.store.tx((t) => t.principalByTokenHash(tokenHash));
70
53
  if (!principal) {
@@ -76,6 +59,22 @@ export async function authenticate(ctx, request, client) {
76
59
  // Read only after authentication, so an unknown caller sees the opaque 404 either way.
77
60
  const subject = singleHeader(request, SUBJECT_HEADER);
78
61
  const scopes = singleHeader(request, SCOPES_HEADER);
62
+ // A management key acts as itself on the Management API; Studio's key does too on a route
63
+ // that takes no application key. Neither acts for a subject there.
64
+ const asManagement = principal.role === "management" || (principal.role === "studio" && !keys.application);
65
+ if (asManagement) {
66
+ if (!keys.management)
67
+ fail(403, "A management key reaches only the Management API (/v1/tenant/*)", {
68
+ code: "key_role_mismatch",
69
+ });
70
+ if (subject !== undefined || scopes !== undefined)
71
+ fail(403, "A management key acts as itself, never for a subject", SUBJECT_INVALID);
72
+ return { kind: "management", principalId: principal.id };
73
+ }
74
+ if (!keys.application)
75
+ fail(403, "This is the Management API: it takes a management key, not an application key", {
76
+ code: "key_role_mismatch",
77
+ });
79
78
  if (subject === undefined) {
80
79
  if (scopes !== undefined)
81
80
  fail(400, `${SCOPES_HEADER} requires ${SUBJECT_HEADER}`, SUBJECT_INVALID);
@@ -92,13 +91,11 @@ export async function authenticate(ctx, request, client) {
92
91
  };
93
92
  }
94
93
  /**
95
- * A subject, subject token or publishable key reaches a route only with one of the scopes it
96
- * declares: `403 scope_required`, decided from the route alone before anything is read.
94
+ * A subject or a token caller reaches a route only with one of the scopes it declares:
95
+ * `403 scope_required`, decided from the route alone before anything is read.
97
96
  */
98
97
  export function requireScopes(scope, access) {
99
- if (scope.kind !== "subject" &&
100
- scope.kind !== "token" &&
101
- scope.kind !== "publishable")
98
+ if (scope.kind !== "subject" && scope.kind !== "token")
102
99
  return;
103
100
  if (access === "any")
104
101
  return;
@@ -107,7 +104,8 @@ export function requireScopes(scope, access) {
107
104
  code: "scope_required",
108
105
  details: { scopes: [] },
109
106
  });
110
- if (!access.some((name) => scope.scopes.has(name)))
107
+ const held = scope.scopes;
108
+ if (!access.some((name) => held.has(name)))
111
109
  fail(403, `Scope ${access.join(" or ")} required`, {
112
110
  code: "scope_required",
113
111
  details: { scopes: access },
@@ -117,6 +115,9 @@ export function accessOf(scope) {
117
115
  switch (scope.kind) {
118
116
  case "application":
119
117
  return undefined;
118
+ // A management key reaches the Management API only: no session is its.
119
+ case "management":
120
+ return fail(404, "Not found");
120
121
  case "subject":
121
122
  return { owner: scope.subject };
122
123
  case "token":
@@ -124,21 +125,16 @@ export function accessOf(scope) {
124
125
  owner: scope.subject,
125
126
  ...(scope.agents === "*" ? {} : { agents: scope.agents }),
126
127
  };
127
- // A publishable key alone owns nothing: no session or vault is ever reachable.
128
- case "publishable":
129
- // Nor does a request with no credential, on a route that serves public data.
128
+ // A request with no credential, on a route that serves public data, owns nothing.
130
129
  case "anonymous":
131
130
  return fail(404, "Not found");
132
- // A delivery token reaches its Action's callbacks, never a session or vault.
133
- case "delivery":
134
- return fail(404, "Not found");
135
131
  default: {
136
132
  const unknown = scope;
137
133
  return fail(404, `Unknown credential ${String(unknown.kind)}`);
138
134
  }
139
135
  }
140
136
  }
141
- /** The subject whose sessions and vaults the request is limited to, if it acts for one. */
137
+ /** The subject whose sessions the request is limited to, if it acts for one. */
142
138
  export function ownerOf(scope) {
143
139
  return accessOf(scope)?.owner;
144
140
  }
@@ -148,16 +144,9 @@ export function requirePrincipal(scope) {
148
144
  return scope.principalId;
149
145
  return fail(403, "Application credential required");
150
146
  }
151
- /**
152
- * The caller may act on `action`: only the delivery token minted for it. Whether its generation
153
- * is still the Action's is checked by each callback.
154
- */
155
- export function scoped(scope, action) {
156
- if (scope.kind === "delivery" &&
157
- scope.actionId === action.actionId &&
158
- scope.agentId === action.agentId)
159
- return;
160
- fail(403, "This delivery token is for another Action");
147
+ /** True when a token caller may reach `agentId` (its issuer's allowlist). Other callers always may. */
148
+ export function mayUseAgent(scope, agentId) {
149
+ return scope.kind !== "token" || scope.agents === "*" || scope.agents.has(agentId);
161
150
  }
162
151
  export function requireApplication(scope) {
163
152
  if (scope.kind === "application")
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Why the Host's Tenant could not be opened (tenancy.md §5). A cause is about the Tenant's
3
3
  * own data or files (its database is newer than this Runtime, its vault key is missing, the
4
- * database has an old layout): the Host fails readiness, names the cause in
5
- * `/v1/admin/status` and the logs, and answers Tenant requests with the opaque 404 until it is
4
+ * database has an old layout): the Host fails readiness, names the cause in the logs (and
5
+ * `nylorun-operate status` reads it from the database), and answers Tenant requests with the opaque 404 until it is
6
6
  * repaired and the Host restarted. A failure outside the Tenant (Postgres unreachable) is not
7
7
  * a cause: it is `TenantUnavailableError`, and the next request tries again.
8
8
  */
@@ -12,7 +12,7 @@ export type { TenantCause };
12
12
  export declare class TenantOpenError extends Error {
13
13
  readonly code: TenantCause["code"];
14
14
  readonly repair: string;
15
- /** The Tenant, when it was read before opening failed (`/v1/admin/status` names it). */
15
+ /** The Tenant, when it was read before opening failed. */
16
16
  envelope?: TenantEnvelope;
17
17
  constructor(fields: TenantCause);
18
18
  toCause(): TenantCause;
@@ -2,7 +2,7 @@
2
2
  export class TenantOpenError extends Error {
3
3
  code;
4
4
  repair;
5
- /** The Tenant, when it was read before opening failed (`/v1/admin/status` names it). */
5
+ /** The Tenant, when it was read before opening failed. */
6
6
  envelope;
7
7
  constructor(fields) {
8
8
  super(fields.message);
@@ -1,19 +1,3 @@
1
- import type { Action, ActionOutcome, LiveEvent, SessionCommand } from "@nylorun/core/contracts";
2
- import type { Tx } from "../store/types.js";
3
- import { type AuthScope, type Session, type TenantContext } from "./context.js";
4
- /** `outcome`, or a failed one when a tool's output does not match its stored output schema. */
5
- export declare function acceptedOutcome(action: Action, outcome: ActionOutcome): ActionOutcome;
1
+ import type { SessionCommand } from "@nylorun/core/contracts";
2
+ import { type AuthScope, type TenantContext } from "./context.js";
6
3
  export declare function command(ctx: TenantContext, id: string, input: SessionCommand, scope: AuthScope): Promise<unknown>;
7
- /**
8
- * Completes a delivered Action with `outcome`, in the caller's transaction, which
9
- * holds the lock of the Action's session `s`: the Action and its effect, the
10
- * `action.completed` event (and `loop.verified` or `loop.decided`), the wake that resumes the
11
- * turn, and queued workflow effects. The Action deliverer and the background-result callback
12
- * both record outcomes here. It sets `s.status`; the caller writes `s`.
13
- */
14
- export declare function recordActionOutcome(t: Tx, ctx: TenantContext, s: Session, action: Action, received: ActionOutcome, options?: {
15
- requestId?: string;
16
- }): Promise<{
17
- event: LiveEvent;
18
- receipt: Record<string, unknown>;
19
- }>;
@@ -7,81 +7,26 @@
7
7
  *
8
8
  * Wakes (`ctx.wake`, architecture §12.3) carry the command type as the reason and a dedupe
9
9
  * key naming the cause: `<type>:<turnId>:<segment>` for `message`, `approve` and `respond`
10
- * (every accepted one writes a new checkpoint segment). An Action's outcome
11
- * (`recordActionOutcome`, from the deliverer) wakes with
12
- * `action_result:<turnId>:<actionId>:<generation>`.
10
+ * (every accepted one writes a new checkpoint segment), except that a workflow's `approve` and
11
+ * `respond` name `<type>:<turnId>:<interactionId>`: a flow resumes in the same segment.
13
12
  *
14
13
  * Cancel commits `cancelled` first; the engine host sees it before its next effect and before
15
14
  * settlement on any Worker. It then aborts an advance running on this process
16
15
  * (`ctx.abortLocal`); reaching an advance on another process is the control stream's job.
17
16
  */
18
17
  import { randomUUID } from "node:crypto";
19
- import { createDurableCheckpoint, createFlowCheckpoint, } from "@nylorun/harness/run";
20
- import { schemaFromJSON, } from "@nylorun/core/define";
21
- import { commandKey, fenceWorkflowActions, foreignInteractionConflict, isWorkflowManifest, planCancelCascade, wakeForQueuedEffects, wakeLinkedWorkflow, } from "../core/flow-host.js";
18
+ import { createDurableCheckpoint, createFlowCheckpoint, resumeFlowCheckpoint, } from "@nylorun/harness/run";
19
+ import { commandKey, cancelQueuedEffects, flowInteractionOf, foreignInteractionConflict, isWorkflowManifest, planCancelCascade, wakeLinkedWorkflow, } from "../core/flow-host.js";
22
20
  import { resolveMessageManifest } from "../core/turn-manifest.js";
23
21
  import { canonical } from "../store/canonical.js";
24
22
  import { lockedSession, } from "./context.js";
25
23
  import { fail } from "./http.js";
26
24
  import { accessOf } from "./auth.js";
27
- import { chargeTurn } from "./subject-limits.js";
28
25
  import { checkSandboxTurn } from "./sandboxes.js";
29
- import { actionTarget, rebaseSessionState, turnManifestOf, variantStore, } from "./session.js";
30
- import { signalActionOutcome, signalSessionCancel } from "./streams.js";
31
- import { toolIds } from "./transcript.js";
26
+ import { rebaseSessionState, turnManifestOf, variantStore } from "./session.js";
27
+ import { signalSessionCancel } from "./streams.js";
32
28
  import { slimModelEffects } from "./slim.js";
33
29
  import { resolveMessageParts } from "../artifacts/parts.js";
34
- /** `outcome`, or a failed one when a tool's output does not match its stored output schema. */
35
- export function acceptedOutcome(action, outcome) {
36
- if (action.kind !== "tool" || !action.outputSchema)
37
- return outcome;
38
- const value = outcome.value;
39
- // Only a result carries output: failures, denials, interactions and deferrals pass as sent.
40
- if (!isResultToolValue(value))
41
- return outcome;
42
- // Endpoints wrap successful tool output as `{ kind: "completed", output }`.
43
- // Validate the tool payload, not the outcome envelope.
44
- const candidate = completedToolOutput(value);
45
- let matches = false;
46
- try {
47
- matches = schemaFromJSON(action.outputSchema).validate(candidate).ok;
48
- }
49
- catch {
50
- matches = false;
51
- }
52
- if (matches)
53
- return outcome;
54
- return {
55
- ...outcome,
56
- value: {
57
- kind: "failed",
58
- code: "tool.invalid-output",
59
- message: "Tool result does not match the output schema stored on the action",
60
- },
61
- };
62
- }
63
- function completedToolOutput(value) {
64
- if (value &&
65
- typeof value === "object" &&
66
- !Array.isArray(value) &&
67
- value.kind === "completed" &&
68
- "output" in value) {
69
- return value.output;
70
- }
71
- return value;
72
- }
73
- const NON_RESULT_KINDS = new Set([
74
- "failed",
75
- "denied",
76
- "interaction-required",
77
- "deferred",
78
- ]);
79
- function isResultToolValue(value) {
80
- return !(!!value &&
81
- typeof value === "object" &&
82
- !Array.isArray(value) &&
83
- NON_RESULT_KINDS.has(value.kind));
84
- }
85
30
  /** A command's identity for idempotency: everything but the request id. */
86
31
  const semantic = (value) => {
87
32
  const { requestId: _, ...body } = value;
@@ -97,7 +42,7 @@ export async function command(ctx, id, input, scope) {
97
42
  if (scope.kind === "token" &&
98
43
  command.type === "message" &&
99
44
  command.manifest !== undefined)
100
- fail(403, "A subject token cannot send message.manifest", {
45
+ fail(403, "A token caller cannot send message.manifest", {
101
46
  code: "scope_required",
102
47
  });
103
48
  const key = commandKey(id, command.idempotencyKey);
@@ -121,28 +66,12 @@ export async function command(ctx, id, input, scope) {
121
66
  turnId: cancelledTurnId,
122
67
  });
123
68
  s.status = "cancelled";
124
- if (workflowCancel) {
125
- await fenceWorkflowActions({
69
+ if (workflowCancel)
70
+ await cancelQueuedEffects({
126
71
  t,
127
72
  workflowSessionId: id,
128
73
  turnId: cancelledTurnId,
129
74
  });
130
- }
131
- else if (cancelledTurnId !== null) {
132
- for (const a of await t.actionsForSession(id, {
133
- turnId: cancelledTurnId,
134
- statuses: ["pending", "delivering"],
135
- })) {
136
- // Delivered work may already have an external effect. Preserve it for reconciliation.
137
- a.status = a.status === "pending" ? "cancelled" : "uncertain";
138
- await t.put("actions", a.actionId, a);
139
- const effect = await t.get("effects", a.actionId);
140
- if (effect) {
141
- effect.status = a.status;
142
- await t.put("effects", a.actionId, effect);
143
- }
144
- }
145
- }
146
75
  if (cancelledTurnId !== null)
147
76
  for (const effect of await t.effectsForTurn(id, cancelledTurnId, ["invoking"])) {
148
77
  effect.status = "uncertain";
@@ -180,12 +109,9 @@ export async function command(ctx, id, input, scope) {
180
109
  if (command.type === "message") {
181
110
  if (!["idle", "completed", "failed", "cancelled"].includes(s.status))
182
111
  fail(409, "Session has active or unresolved work");
183
- // Every turn start on a sandbox: it exists, the token's `sbx` reaches it, and no other
184
- // session's turn holds it. Before the charge, so a refused turn costs nothing.
112
+ // Every turn start on a sandbox: it exists, the token's grants reach it, and no other
113
+ // session's turn holds it.
185
114
  await checkSandboxTurn(t, s, scope, ctx);
186
- // After the replay check above, so a retried message is never charged twice.
187
- if (scope.kind === "token" && scope.limits)
188
- await chargeTurn(t, scope.subject, scope.limits);
189
115
  s.turnStartState = s.state;
190
116
  // A new turn folds from the latest snapshot: a cancel or failure only reverts this turn.
191
117
  if (s.history?.snapshot !== undefined)
@@ -239,6 +165,28 @@ export async function command(ctx, id, input, scope) {
239
165
  }
240
166
  }
241
167
  }
168
+ else if (isWorkflowManifest(s.manifest)) {
169
+ // Approvals owned by linked agent sessions must be answered there (WF-R52).
170
+ const conflict = await foreignInteractionConflict({
171
+ t,
172
+ workflowSessionId: id,
173
+ interactionId: command.interactionId,
174
+ });
175
+ if (conflict)
176
+ fail(409, conflict.message);
177
+ if (s.status !== "paused" || !s.checkpoint || !s.activeTurnId)
178
+ fail(409, "Session is not awaiting a response");
179
+ // The flow's own interactions: a tool node that asked (`ctx.approve`, `ctx.ask`).
180
+ const asked = flowInteractionOf(s.waits, id, command.interactionId);
181
+ if (!asked)
182
+ fail(409, "Unknown interaction");
183
+ else if ((command.type === "approve") !== (asked.kind === "approval"))
184
+ fail(409, "Interaction response kind does not match the saved request");
185
+ // Same segment: the flow replays its journal and runs the tool again with the answer.
186
+ s.checkpoint = resumeFlowCheckpoint(s.checkpoint, command.interactionId, command.type === "approve"
187
+ ? { kind: "approval", approved: command.approved }
188
+ : { kind: "response", value: command.value });
189
+ }
242
190
  else {
243
191
  if (s.status !== "paused" || !s.state || !s.activeTurnId)
244
192
  fail(409, "Session is not awaiting a response");
@@ -259,19 +207,6 @@ export async function command(ctx, id, input, scope) {
259
207
  interactionId: command.interactionId,
260
208
  value: command.value,
261
209
  };
262
- if (isWorkflowManifest(s.manifest)) {
263
- // Approvals owned by linked agent sessions must be answered there (WF-R52).
264
- const conflict = await foreignInteractionConflict({
265
- t,
266
- workflowSessionId: id,
267
- interactionId: command.interactionId,
268
- });
269
- if (conflict)
270
- fail(409, conflict.message);
271
- // Workflow-owned interactions (tool-node / verify) resume on this session once
272
- // the flow engine supports pause segments; tracer root Loop has none yet.
273
- fail(409, "Workflow sessions do not accept approve/respond on the root without a pending interaction");
274
- }
275
210
  s.checkpoint = createDurableCheckpoint({
276
211
  manifest: turnManifestOf(s),
277
212
  sessionId: id,
@@ -284,9 +219,13 @@ export async function command(ctx, id, input, scope) {
284
219
  }
285
220
  s.status = "runnable";
286
221
  s.waits = undefined;
222
+ // A flow answers each interaction in the same segment: the interaction names the cause.
223
+ const cause = command.type !== "message" && isWorkflowManifest(s.manifest)
224
+ ? command.interactionId
225
+ : s.checkpoint.segment;
287
226
  const commandWake = {
288
227
  reason: command.type,
289
- dedupeKey: `${command.type}:${s.activeTurnId}:${s.checkpoint.segment}`,
228
+ dedupeKey: `${command.type}:${s.activeTurnId}:${cause}`,
290
229
  };
291
230
  t.afterCommit(() => ctx.wake(id, commandWake));
292
231
  event = await t.event(id, s.activeTurnId, `command.${command.type}`, command);
@@ -330,83 +269,3 @@ export async function command(ctx, id, input, scope) {
330
269
  }
331
270
  return response;
332
271
  }
333
- /**
334
- * Completes a delivered Action with `outcome`, in the caller's transaction, which
335
- * holds the lock of the Action's session `s`: the Action and its effect, the
336
- * `action.completed` event (and `loop.verified` or `loop.decided`), the wake that resumes the
337
- * turn, and queued workflow effects. The Action deliverer and the background-result callback
338
- * both record outcomes here. It sets `s.status`; the caller writes `s`.
339
- */
340
- export async function recordActionOutcome(t, ctx, s, action, received, options = {}) {
341
- const outcome = acceptedOutcome(action, received);
342
- const prior = await t.get("effects", action.actionId);
343
- action.status = "completed";
344
- await t.put("actions", action.actionId, action);
345
- prior.status = "completed";
346
- prior.outcome = outcome;
347
- s.status = "runnable";
348
- const resultWake = {
349
- reason: "action_result",
350
- dedupeKey: `action_result:${action.turnId}:${action.actionId}:${action.generation}`,
351
- };
352
- t.afterCommit(() => ctx.wake(s.id, resultWake));
353
- // A run held while the Action was pending goes on with it in the same lease (F6.2): here, or
354
- // on the process whose harness holds it. The wake finds the session settled, or resumes it
355
- // by replay when the run had already ended.
356
- t.afterCommit(() => {
357
- if (ctx.harness.holds(s.id))
358
- ctx.harness.resolved(s.id, action.actionId, outcome);
359
- else
360
- signalActionOutcome(ctx, s.id, action.actionId);
361
- });
362
- const event = await t.event(s.id, s.activeTurnId, "action.completed", {
363
- actionId: action.actionId,
364
- ...actionTarget(action),
365
- kind: action.kind,
366
- ...(action.kind === "tool" ? toolIds(action.context) : {}),
367
- result: outcome.value,
368
- });
369
- if (action.kind === "verify") {
370
- const verdict = outcome.value;
371
- if (verdict?.kind !== "failed") {
372
- await t.event(s.id, s.activeTurnId, "loop.verified", {
373
- path: String(action.context?.loopPath ?? action.path ?? ""),
374
- n: Number(action.context?.n ?? 1),
375
- pass: Boolean(verdict?.pass),
376
- ...(verdict?.feedback !== undefined
377
- ? { feedback: verdict.feedback }
378
- : {}),
379
- ...(verdict?.data !== undefined ? { data: verdict.data } : {}),
380
- });
381
- }
382
- }
383
- else if (action.kind === "fn" && action.context?.role === "decide") {
384
- const decision = outcome.value;
385
- await t.event(s.id, s.activeTurnId, "loop.decided", {
386
- path: String(action.context?.loopPath ?? action.path ?? ""),
387
- n: Number(action.context?.n ?? 1),
388
- next: "output" in decision && !("input" in decision)
389
- ? "output"
390
- : "input",
391
- patched: Boolean(decision.agent),
392
- });
393
- }
394
- const receipt = {
395
- status: "accepted",
396
- turnId: s.activeTurnId,
397
- cursor: event.cursor,
398
- ...(options.requestId === undefined ? {} : { requestId: options.requestId }),
399
- };
400
- prior.receipt = receipt;
401
- await t.put("effects", action.actionId, prior);
402
- if (isWorkflowManifest(s.manifest) && s.activeTurnId) {
403
- await wakeForQueuedEffects({
404
- t,
405
- workflowSessionId: s.id,
406
- turnId: s.activeTurnId,
407
- limits: ctx.flowLimits,
408
- schedule: ctx.wake,
409
- });
410
- }
411
- return { event, receipt };
412
- }