@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
@@ -12,9 +12,8 @@
12
12
  * // Administration: each opens its own transaction and writes its audit rows
13
13
  * // in that transaction. Creates and rotations replay by idempotency key.
14
14
  * createVault(body: CreateVaultRequest): Promise<VaultInfo>
15
- * listVaults(ownerUserId: string): Promise<VaultInfo[]>
15
+ * listVaults(ownerUserId: string | undefined, options?: { installation?: boolean }): Promise<VaultInfo[]>
16
16
  * getVault(id: string): Promise<VaultInfo>
17
- * assertOwner(id: string, ownerUserId: string): Promise<void>
18
17
  * deleteVault(id: string): Promise<{ id: string }>
19
18
  * createCredential(vaultId: string, body: CreateCredentialRequest): Promise<CredentialInfo>
20
19
  * listCredentials(vaultId: string): Promise<CredentialInfo[]>
@@ -26,9 +25,14 @@
26
25
  * assertAttachment(t: Tx, ownerUserId: string, vaultIds: readonly string[], selections: readonly CredentialSelection[], options?: { opaque?: boolean }): Promise<void>
27
26
  * recordAttachment(t: Tx, sessionId: string, vaultIds: readonly string[]): Promise<void>
28
27
  *
29
- * // Use: opens its own transactions; never call it inside one.
28
+ * // Use: opens its own transactions; never call it inside one. `CredentialSources`
29
+ * // (`vault/sources.ts`) wraps it with the operator's credential resolver.
30
30
  * authorize(input: { sessionId; vaultIds; credentialSelections; url; serverName? }): Promise<AuthorizeResult>
31
31
  *
32
+ * // MCP OAuth connect (F9 C2, installation vaults only): network calls outside any transaction.
33
+ * startOAuth(input: { vaultId; server; url; clientId?; redirectUri }): Promise<{ authorizeUrl; expiresAt }>
34
+ * finishOAuth(input: { state; code?; error? }): Promise<{ vaultId; credentialId }>
35
+ *
32
36
  * // Host model credential: each opens its own transaction.
33
37
  * getHostModel(): Promise<HostModelView>
34
38
  * listHostProviders(): Promise<{ providers: HostModelProviderInfo[] }>
@@ -40,6 +44,13 @@
40
44
  * (`vault/host-model.ts`), which only the Model Gate uses: this service seals the host model's
41
45
  * credential but never reads it back.
42
46
  *
47
+ * ## Scopes
48
+ *
49
+ * - `user`: one person's vault (owner `ownerUserId`), attachable only to that person's sessions.
50
+ * - `installation` (`INSTALLATION_OWNER`): the installation's own, created by application keys
51
+ * only, attachable to any session. Subject and token callers never see one.
52
+ * - `host`: the model vault (`HOST_VAULT_ID`), never listed or attached.
53
+ *
43
54
  * ## Transactions and I/O
44
55
  *
45
56
  * - No network I/O runs inside a transaction. The KEK getter may touch the
@@ -59,14 +70,19 @@
59
70
  * has an audit row. Unreadable ciphertext and refresh failures are audited
60
71
  * in their own transaction before the refusal is returned.
61
72
  */
62
- import { createHash, randomUUID } from "node:crypto";
73
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
74
+ import { INSTALLATION_OWNER } from "@nylorun/core/contracts";
63
75
  import { canonical } from "../store/canonical.js";
64
- import { decryptSecret, encryptSecret, VaultCryptoError, } from "./crypto.js";
76
+ import { decryptSecret, encryptSecret, kekId as kekIdOf, openBytes, sealBytes, VaultCryptoError, } from "./crypto.js";
77
+ import { HttpError } from "../tenant/http.js";
78
+ import { authorizationUrl, discoverOAuthServer, exchangeCode, oauthClient, } from "./oauth.js";
65
79
  import { VaultError } from "./error.js";
66
80
  import { hostModelCatalog } from "../model/catalog.js";
67
81
  import { normalizeVaultUrl } from "./url.js";
68
82
  const REFRESH_SKEW_MS = 60_000;
69
83
  const REFRESH_TIMEOUT_MS = 30_000;
84
+ /** How long an MCP OAuth connect may take between its start and its callback (F9 C2). */
85
+ export const OAUTH_PENDING_MS = 10 * 60_000;
70
86
  export const HOST_VAULT_ID = "host";
71
87
  const HOST_MODEL_ID = "host-model";
72
88
  export class VaultService {
@@ -85,13 +101,17 @@ export class VaultService {
85
101
  // --- administration --------------------------------------------------------
86
102
  async createVault(body) {
87
103
  return this.store.tx((t) => this.replay(t, `create-vault:${body.idempotencyKey}`, body, async () => {
104
+ const installation = body.scope === "installation";
105
+ const ownerUserId = installation ? INSTALLATION_OWNER : body.ownerUserId;
106
+ if (!ownerUserId)
107
+ throw new VaultError(400, "ownerUserId is required for a user vault");
88
108
  const row = {
89
109
  id: randomUUID(),
90
110
  name: body.name,
91
- ownerUserId: body.ownerUserId,
111
+ ownerUserId,
92
112
  metadataJson: body.metadata ? JSON.stringify(body.metadata) : null,
93
113
  createdAt: new Date().toISOString(),
94
- scope: "user",
114
+ scope: installation ? "installation" : "user",
95
115
  };
96
116
  await t.insertVault(row);
97
117
  await this.audit(t, {
@@ -103,20 +123,20 @@ export class VaultService {
103
123
  return vaultInfoOf(row);
104
124
  }));
105
125
  }
106
- async listVaults(ownerUserId) {
107
- return this.store.tx(async (t) => (await t.vaultsByOwner(ownerUserId)).map(vaultInfoOf));
126
+ /**
127
+ * One person's vaults, then (with `installation`) the installation vaults. No owner lists
128
+ * the installation vaults only.
129
+ */
130
+ async listVaults(ownerUserId, options = {}) {
131
+ return this.store.tx(async (t) => {
132
+ const own = ownerUserId === undefined ? [] : await t.vaultsByOwner(ownerUserId);
133
+ const shared = options.installation ? await t.installationVaults() : [];
134
+ return [...own, ...shared].map(vaultInfoOf);
135
+ });
108
136
  }
109
137
  async getVault(id) {
110
138
  return this.store.tx((t) => this.vaultInfo(t, id));
111
139
  }
112
- /** Another owner's vault, or the host vault, is the same 404 as a missing one. */
113
- async assertOwner(id, ownerUserId) {
114
- await this.store.tx(async (t) => {
115
- const row = await t.getVault(id);
116
- if (!row || row.scope !== "user" || row.ownerUserId !== ownerUserId)
117
- throw new VaultError(404, "Vault not found");
118
- });
119
- }
120
140
  async deleteVault(id) {
121
141
  return this.store.tx(async (t) => {
122
142
  await this.vaultInfo(t, id);
@@ -238,6 +258,9 @@ export class VaultService {
238
258
  throw new VaultError(404, "Vault not found");
239
259
  if (vault.scope === "host")
240
260
  throw new VaultError(400, "Host vault cannot be attached to a session");
261
+ // Any session may use the installation's own vaults (F9-D13).
262
+ if (vault.scope === "installation")
263
+ continue;
241
264
  if (vault.ownerUserId !== ownerUserId)
242
265
  throw options.opaque
243
266
  ? new VaultError(404, "Vault not found")
@@ -371,6 +394,193 @@ export class VaultService {
371
394
  headers: { authorization: `Bearer ${token}` },
372
395
  };
373
396
  }
397
+ // --- MCP OAuth connect (F9 C2) ---------------------------------------------
398
+ /**
399
+ * Starts signing the installation in to the MCP server at `url`: discovery, a client (the
400
+ * given id, or one registered now), an S256 PKCE verifier and a `state`, kept for ten minutes
401
+ * in a pending row (verifier and secret sealed, state hashed). Returns where to send the
402
+ * browser. Installation vaults only.
403
+ */
404
+ async startOAuth(input) {
405
+ const url = normalizeVaultUrl(input.url);
406
+ const redirectUri = callbackUrl(input.redirectUri);
407
+ await this.store.tx(async (t) => {
408
+ await this.installationVault(t, input.vaultId);
409
+ await t.deleteExpiredOAuthPending(new Date().toISOString());
410
+ });
411
+ const kek = this.kek();
412
+ const server = await discoverOAuthServer(url, this.fetchImpl);
413
+ const client = await oauthClient(server, {
414
+ ...(input.clientId === undefined ? {} : { clientId: input.clientId }),
415
+ redirectUri,
416
+ fetchFn: this.fetchImpl,
417
+ });
418
+ const state = randomBytes(32).toString("base64url");
419
+ const { authorizeUrl, codeVerifier } = await authorizationUrl(server, client, { redirectUri, state });
420
+ const stateHash = hashState(state);
421
+ const now = Date.now();
422
+ const expiresAt = new Date(now + OAUTH_PENDING_MS).toISOString();
423
+ const row = {
424
+ stateHash,
425
+ vaultId: input.vaultId,
426
+ server: input.server,
427
+ url,
428
+ tokenEndpoint: server.metadata.token_endpoint,
429
+ clientId: client.clientId,
430
+ tokenEndpointAuth: client.tokenEndpointAuth,
431
+ resource: server.resource ?? null,
432
+ kekId: "",
433
+ clientSecret: null,
434
+ codeVerifier: new Uint8Array(),
435
+ redirectUri,
436
+ expiresAt,
437
+ createdAt: new Date(now).toISOString(),
438
+ };
439
+ const verifier = sealBytes(kek, pendingAad(row, "codeVerifier"), Buffer.from(codeVerifier, "utf8"));
440
+ const secret = client.clientSecret
441
+ ? sealBytes(kek, pendingAad(row, "clientSecret"), Buffer.from(client.clientSecret, "utf8"))
442
+ : null;
443
+ await this.store.tx(async (t) => {
444
+ await this.installationVault(t, input.vaultId);
445
+ await t.insertOAuthPending({
446
+ ...row,
447
+ kekId: kekIdOf(kek),
448
+ codeVerifier: verifier,
449
+ clientSecret: secret,
450
+ });
451
+ await this.audit(t, {
452
+ actor: "application",
453
+ action: "oauth_start",
454
+ vaultId: input.vaultId,
455
+ target: url,
456
+ outcome: "started",
457
+ });
458
+ });
459
+ return { authorizeUrl, expiresAt };
460
+ }
461
+ /**
462
+ * Finishes a connect from its callback: takes the pending row of `state` (deleted whatever
463
+ * happens next), exchanges the code, and seals an `oauth` credential bound to the server's
464
+ * URL in the installation vault, rotating the vault's OAuth credential for that URL when it
465
+ * has one.
466
+ */
467
+ async finishOAuth(input) {
468
+ const row = await this.store.tx((t) => t.takeOAuthPending(hashState(input.state)));
469
+ if (!row || Date.parse(row.expiresAt) <= Date.now())
470
+ throw new HttpError(400, "This sign-in is unknown, already used or expired: start the connect again", { code: "oauth_state_invalid" });
471
+ const failed = async (error) => {
472
+ await this.store.tx((t) => this.audit(t, {
473
+ actor: "application",
474
+ action: "oauth_connect",
475
+ vaultId: row.vaultId,
476
+ target: row.url,
477
+ outcome: "failed",
478
+ }));
479
+ throw error;
480
+ };
481
+ if (input.error !== undefined || !input.code)
482
+ return failed(new HttpError(400, `The authorization server did not grant access (${oauthErrorName(input.error)})`, { code: "oauth_failed" }));
483
+ const kek = this.kek();
484
+ let client;
485
+ let codeVerifier;
486
+ try {
487
+ codeVerifier = openSealed(kek, row, "codeVerifier", row.codeVerifier);
488
+ client = {
489
+ clientId: row.clientId,
490
+ tokenEndpointAuth: row.tokenEndpointAuth,
491
+ ...(row.clientSecret ? { clientSecret: openSealed(kek, row, "clientSecret", row.clientSecret) } : {}),
492
+ };
493
+ }
494
+ catch (error) {
495
+ if (!(error instanceof VaultCryptoError))
496
+ throw error;
497
+ return failed(new HttpError(500, "The sign-in could not be read with this vault key", { code: "oauth_failed" }));
498
+ }
499
+ let tokens;
500
+ try {
501
+ tokens = await exchangeCode({
502
+ tokenEndpoint: row.tokenEndpoint,
503
+ client,
504
+ code: input.code,
505
+ codeVerifier,
506
+ redirectUri: row.redirectUri,
507
+ ...(row.resource === null ? {} : { resource: row.resource }),
508
+ fetchFn: this.fetchImpl,
509
+ });
510
+ }
511
+ catch (error) {
512
+ return failed(error instanceof HttpError ? error : new HttpError(502, "The OAuth code exchange failed", { code: "oauth_failed" }));
513
+ }
514
+ if (tokens.token_type.toLowerCase() !== "bearer")
515
+ return failed(new HttpError(502, `The authorization server issued a ${tokens.token_type} token; MCP needs a bearer token`, {
516
+ code: "oauth_failed",
517
+ }));
518
+ const payload = {
519
+ accessToken: tokens.access_token,
520
+ ...(tokens.refresh_token
521
+ ? {
522
+ refreshToken: tokens.refresh_token,
523
+ tokenEndpoint: row.tokenEndpoint,
524
+ clientId: client.clientId,
525
+ tokenEndpointAuth: client.tokenEndpointAuth,
526
+ ...(client.clientSecret ? { clientSecret: client.clientSecret } : {}),
527
+ }
528
+ : {}),
529
+ };
530
+ const now = new Date();
531
+ const expiresAt = typeof tokens.expires_in === "number" && Number.isFinite(tokens.expires_in)
532
+ ? new Date(now.getTime() + tokens.expires_in * 1000).toISOString()
533
+ : null;
534
+ return this.store.tx(async (t) => {
535
+ await this.installationVault(t, row.vaultId);
536
+ const existing = (await t.credentialsForVault(row.vaultId, { type: "oauth" })).find((item) => bindingUrl(item) === row.url);
537
+ if (existing) {
538
+ await this.writePayload(t, kek, existing, payload, expiresAt, now.toISOString());
539
+ await this.audit(t, {
540
+ actor: "application",
541
+ action: "oauth_connect",
542
+ vaultId: row.vaultId,
543
+ credentialId: existing.id,
544
+ target: row.url,
545
+ outcome: "rotated",
546
+ });
547
+ return { vaultId: row.vaultId, credentialId: existing.id };
548
+ }
549
+ const id = randomUUID();
550
+ const sealed = encryptSecret(kek, credentialAad(row.vaultId, id, "oauth", row.url), Buffer.from(JSON.stringify(payload), "utf8"));
551
+ await t.insertCredential({
552
+ id,
553
+ vaultId: row.vaultId,
554
+ name: row.server,
555
+ type: "oauth",
556
+ bindingJson: JSON.stringify({ url: row.url }),
557
+ expiresAt,
558
+ createdAt: now.toISOString(),
559
+ rotatedAt: null,
560
+ ...sealed,
561
+ });
562
+ await this.audit(t, {
563
+ actor: "application",
564
+ action: "oauth_connect",
565
+ vaultId: row.vaultId,
566
+ credentialId: id,
567
+ target: row.url,
568
+ outcome: "created",
569
+ });
570
+ return { vaultId: row.vaultId, credentialId: id };
571
+ });
572
+ }
573
+ /** An installation vault, else the 404 of a missing one (a user vault is a 400). */
574
+ async installationVault(t, id) {
575
+ const row = await t.getVault(id);
576
+ if (!row || row.scope === "host")
577
+ throw new VaultError(404, "Vault not found");
578
+ if (row.scope !== "installation")
579
+ throw new HttpError(400, "MCP OAuth connect stores its credential in an installation vault only", {
580
+ code: "request_rejected",
581
+ });
582
+ return row;
583
+ }
374
584
  // --- host model ----------------------------------------------------------------
375
585
  async getHostModel() {
376
586
  return this.store.tx((t) => this.hostModelView(t));
@@ -727,6 +937,40 @@ export class VaultService {
727
937
  });
728
938
  }
729
939
  }
940
+ function hashState(state) {
941
+ return createHash("sha256").update(state, "utf8").digest("hex");
942
+ }
943
+ function pendingAad(row, field) {
944
+ return Buffer.from(canonical({ table: "oauth_pending", stateHash: row.stateHash, vaultId: row.vaultId, field }), "utf8");
945
+ }
946
+ function openSealed(kek, row, field, sealed) {
947
+ const plaintext = openBytes(kek, pendingAad(row, field), Buffer.from(sealed), row.kekId);
948
+ try {
949
+ return plaintext.toString("utf8");
950
+ }
951
+ finally {
952
+ plaintext.fill(0);
953
+ }
954
+ }
955
+ /** The callback URL the authorization server sends the browser back to. */
956
+ function callbackUrl(value) {
957
+ let url;
958
+ try {
959
+ url = new URL(value);
960
+ }
961
+ catch {
962
+ throw new VaultError(400, "The OAuth redirect URI is invalid");
963
+ }
964
+ if (url.protocol !== "https:" && url.protocol !== "http:")
965
+ throw new VaultError(400, "The OAuth redirect URI must be http or https");
966
+ return url.href;
967
+ }
968
+ /** An authorization server's `error`, safe to show: an OAuth error code, or a stand-in. */
969
+ function oauthErrorName(error) {
970
+ if (error === undefined)
971
+ return "no code";
972
+ return /^[A-Za-z0-9_.-]{1,64}$/.test(error) ? error : "error";
973
+ }
730
974
  function isUserCredential(row) {
731
975
  return row.type !== "model";
732
976
  }
@@ -0,0 +1,54 @@
1
+ import type { CredentialSelection } from "@nylorun/core/contracts";
2
+ import type { AuthorizeResult, VaultService } from "./service.js";
3
+ /** The operator's credential resolver (`NYLORUN_RESOLVER_URL`, `NYLORUN_RESOLVER_TOKEN`). */
4
+ export interface ResolverConfig {
5
+ readonly url: string;
6
+ readonly token: string;
7
+ }
8
+ /** What the resolver is asked about: the session's row, as stored. */
9
+ export interface CredentialSession {
10
+ readonly id: string;
11
+ readonly ownerUserId: string;
12
+ readonly activeTurnId: string | null;
13
+ readonly vaultIds?: readonly string[];
14
+ readonly credentialSelections?: readonly CredentialSelection[];
15
+ }
16
+ /** One MCP request: the declared server's URL, its name and the agent that declares it. */
17
+ export interface McpCredentialRequest {
18
+ readonly url: string;
19
+ readonly serverName?: string;
20
+ /** The agent used as a tool that declares the server; absent for the session's root agent. */
21
+ readonly agentId?: string;
22
+ }
23
+ export interface CredentialSourcesOptions {
24
+ readonly vault: Pick<VaultService, "authorize">;
25
+ /** Absent: the vaults are the only source, as before F9. */
26
+ readonly resolver?: ResolverConfig;
27
+ /** Default `globalThis.fetch`. */
28
+ readonly fetch?: typeof fetch;
29
+ /** How long the resolver may take. Default 5 s. */
30
+ readonly timeoutMs?: number;
31
+ readonly now?: () => number;
32
+ }
33
+ export declare const RESOLVER_TIMEOUT_MS = 5000;
34
+ /** The longest an answer is kept, whatever its `expiresAt`. */
35
+ export declare const RESOLVER_MAX_CACHE_MS: number;
36
+ /** How long an answer without `expiresAt` (and a 404) is kept. */
37
+ export declare const RESOLVER_DEFAULT_CACHE_MS = 60000;
38
+ /** The refusal reason when the resolver could not answer. */
39
+ export declare const CREDENTIAL_UNAVAILABLE = "credential_unavailable";
40
+ export declare class CredentialSources {
41
+ private readonly vault;
42
+ private readonly resolver;
43
+ private readonly fetchImpl;
44
+ private readonly timeoutMs;
45
+ private readonly now;
46
+ private readonly cache;
47
+ private readonly pending;
48
+ constructor(options: CredentialSourcesOptions);
49
+ /** The credential of one MCP request made for `session`. */
50
+ authorize(session: CredentialSession, request: McpCredentialRequest): Promise<AuthorizeResult>;
51
+ private answer;
52
+ private until;
53
+ private ask;
54
+ }
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Where a session's MCP credential comes from (F9 C1): the session's attached vaults first,
3
+ * then the operator's credential resolver, an HTTP service on the operator's own network that
4
+ * holds people's own credentials (OSS never stores them, F9-D7).
5
+ *
6
+ * Both authorize sites call this: the gateway (`gates/tenant-vaults.ts`, remote MCP servers in
7
+ * the gates service) and the Tenant (`tenant/effects.ts`, the in-process MCP pool).
8
+ *
9
+ * ## The resolver contract
10
+ *
11
+ * Asked only when the attached vaults hold no credential for the URL (the vault's
12
+ * `unauthenticated`), and only for a person's session (never a reserved owner):
13
+ *
14
+ * ```text
15
+ * POST <resolver url>
16
+ * Authorization: Bearer <resolver token>
17
+ * { "owner": "u:priya", "session": "s_…", "turn": "t_…" | null,
18
+ * "target": { "kind": "mcp", "server": "github", "agent": "support", "url": "https://…/mcp" } }
19
+ *
20
+ * 200 { "headers": { "authorization": "Bearer …" }, "expiresAt"?: "<ISO time>" } → authorized
21
+ * 404 → unauthenticated (the call goes without one)
22
+ * anything else, a bad body, or no answer in 5 s → refused: credential_unavailable
23
+ * ```
24
+ *
25
+ * `owner` and `turn` come from the session row, never from the harness. Because authorize runs
26
+ * on every MCP HTTP request, answers (200 and 404) are cached per (owner, url): until
27
+ * `expiresAt`, at most 5 minutes, and 60 s without one. Concurrent misses for one key share one
28
+ * request. Failures are not cached. The resolver's URL is the operator's, so private addresses
29
+ * are allowed; redirects are refused.
30
+ */
31
+ import { isSubject } from "@nylorun/core/contracts";
32
+ export const RESOLVER_TIMEOUT_MS = 5_000;
33
+ /** The longest an answer is kept, whatever its `expiresAt`. */
34
+ export const RESOLVER_MAX_CACHE_MS = 5 * 60_000;
35
+ /** How long an answer without `expiresAt` (and a 404) is kept. */
36
+ export const RESOLVER_DEFAULT_CACHE_MS = 60_000;
37
+ /** The refusal reason when the resolver could not answer. */
38
+ export const CREDENTIAL_UNAVAILABLE = "credential_unavailable";
39
+ /** RFC 9110 field names. */
40
+ const HEADER_NAME = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
41
+ /** Headers the MCP transport owns; a resolver may not set them. */
42
+ const RESERVED_HEADERS = new Set([
43
+ "host",
44
+ "content-length",
45
+ "content-type",
46
+ "transfer-encoding",
47
+ "connection",
48
+ "accept",
49
+ "mcp-session-id",
50
+ "mcp-protocol-version",
51
+ ]);
52
+ export class CredentialSources {
53
+ vault;
54
+ resolver;
55
+ fetchImpl;
56
+ timeoutMs;
57
+ now;
58
+ cache = new Map();
59
+ pending = new Map();
60
+ constructor(options) {
61
+ this.vault = options.vault;
62
+ this.resolver = options.resolver;
63
+ this.fetchImpl = options.fetch ?? globalThis.fetch;
64
+ this.timeoutMs = options.timeoutMs ?? RESOLVER_TIMEOUT_MS;
65
+ this.now = options.now ?? Date.now;
66
+ }
67
+ /** The credential of one MCP request made for `session`. */
68
+ async authorize(session, request) {
69
+ const result = await this.vault.authorize({
70
+ sessionId: session.id,
71
+ vaultIds: session.vaultIds ?? [],
72
+ credentialSelections: session.credentialSelections ?? [],
73
+ url: request.url,
74
+ ...(request.serverName === undefined ? {} : { serverName: request.serverName }),
75
+ });
76
+ if (result.status !== "unauthenticated" || !this.resolver)
77
+ return result;
78
+ // `host` and `installation` are no one: there is no person to resolve for.
79
+ if (!isSubject(session.ownerUserId))
80
+ return result;
81
+ const answer = await this.answer(session, request, result.url);
82
+ if (answer.kind === "failed")
83
+ return { status: "refused", url: result.url, credentialIds: [], reason: CREDENTIAL_UNAVAILABLE };
84
+ if (answer.kind === "none")
85
+ return result;
86
+ return { status: "authorized", url: result.url, headers: { ...answer.headers } };
87
+ }
88
+ async answer(session, request, url) {
89
+ const key = JSON.stringify([session.ownerUserId, url]);
90
+ const cached = this.cache.get(key);
91
+ if (cached) {
92
+ if (cached.until > this.now())
93
+ return cached.answer;
94
+ this.cache.delete(key);
95
+ }
96
+ let pending = this.pending.get(key);
97
+ if (!pending) {
98
+ pending = this.ask(session, request, url)
99
+ .then((answer) => {
100
+ if (answer.kind !== "failed") {
101
+ const until = this.until(answer);
102
+ if (until > this.now())
103
+ this.cache.set(key, { answer, until });
104
+ }
105
+ return answer;
106
+ })
107
+ .finally(() => this.pending.delete(key));
108
+ this.pending.set(key, pending);
109
+ }
110
+ return pending;
111
+ }
112
+ until(answer) {
113
+ const now = this.now();
114
+ const cap = now + RESOLVER_MAX_CACHE_MS;
115
+ if (answer.kind === "headers" && answer.expiresAt !== undefined)
116
+ return Math.min(answer.expiresAt, cap);
117
+ return now + RESOLVER_DEFAULT_CACHE_MS;
118
+ }
119
+ async ask(session, request, url) {
120
+ const resolver = this.resolver;
121
+ const body = {
122
+ owner: session.ownerUserId,
123
+ session: session.id,
124
+ turn: session.activeTurnId ?? null,
125
+ target: {
126
+ kind: "mcp",
127
+ ...(request.serverName ? { server: request.serverName } : {}),
128
+ ...(request.agentId ? { agent: request.agentId } : {}),
129
+ url,
130
+ },
131
+ };
132
+ try {
133
+ const response = await this.fetchImpl(resolver.url, {
134
+ method: "POST",
135
+ headers: {
136
+ authorization: `Bearer ${resolver.token}`,
137
+ "content-type": "application/json",
138
+ accept: "application/json",
139
+ },
140
+ body: JSON.stringify(body),
141
+ redirect: "error",
142
+ signal: AbortSignal.timeout(this.timeoutMs),
143
+ });
144
+ if (response.status === 404) {
145
+ await response.body?.cancel().catch(() => undefined);
146
+ return { kind: "none" };
147
+ }
148
+ if (response.status !== 200) {
149
+ await response.body?.cancel().catch(() => undefined);
150
+ return { kind: "failed" };
151
+ }
152
+ return parseAnswer(await response.json(), this.now());
153
+ }
154
+ catch {
155
+ return { kind: "failed" };
156
+ }
157
+ }
158
+ }
159
+ function parseAnswer(value, now) {
160
+ if (!value || typeof value !== "object" || Array.isArray(value))
161
+ return { kind: "failed" };
162
+ const { headers, expiresAt } = value;
163
+ if (!headers || typeof headers !== "object" || Array.isArray(headers))
164
+ return { kind: "failed" };
165
+ const entries = Object.entries(headers);
166
+ if (entries.length === 0)
167
+ return { kind: "failed" };
168
+ const out = {};
169
+ for (const [name, item] of entries) {
170
+ if (!HEADER_NAME.test(name) || RESERVED_HEADERS.has(name.toLowerCase()))
171
+ return { kind: "failed" };
172
+ if (typeof item !== "string" || item === "" || /[\r\n\0]/.test(item))
173
+ return { kind: "failed" };
174
+ out[name.toLowerCase()] = item;
175
+ }
176
+ if (expiresAt === undefined || expiresAt === null)
177
+ return { kind: "headers", headers: out };
178
+ if (typeof expiresAt !== "string")
179
+ return { kind: "failed" };
180
+ const at = Date.parse(expiresAt);
181
+ if (Number.isNaN(at))
182
+ return { kind: "failed" };
183
+ // An answer that has already expired is used once and not kept.
184
+ return { kind: "headers", headers: out, expiresAt: Math.max(at, now) };
185
+ }
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const RUNTIME_VERSION = "0.15.2-beta";
1
+ export declare const RUNTIME_VERSION = "0.17.0-beta";
package/dist/version.js CHANGED
@@ -1,3 +1,3 @@
1
1
  // Kept in step with package.json by runtime/scripts/check-package.mjs, which runs in npm run check.
2
2
  // release:prepare also rewrites this when it bumps @nylorun/runtime.
3
- export const RUNTIME_VERSION = "0.15.2-beta";
3
+ export const RUNTIME_VERSION = "0.17.0-beta";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nylorun/runtime",
3
- "version": "0.15.2-beta",
3
+ "version": "0.17.0-beta",
4
4
  "description": "Nylorun Runtime: the agent execution Host on Postgres, Restate and S2, and model providers.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -59,16 +59,17 @@
59
59
  "db:studio": "drizzle-kit studio"
60
60
  },
61
61
  "dependencies": {
62
- "@ag-ui/core": "~1.0.0",
62
+ "@ag-ui/core": "~1.0.1",
63
63
  "@asteasolutions/zod-to-openapi": "^9.1.0",
64
64
  "@earendil-works/pi-ai": "0.99.1",
65
- "@hono/node-server": "^2.1.1",
65
+ "@hono/node-server": "^2.1.3",
66
66
  "@hono/zod-openapi": "^1.6.3",
67
- "@modelcontextprotocol/sdk": "^1.30.0",
68
- "@nylorun/core": "0.11.0-beta",
69
- "@nylorun/harness": "0.21.2-beta",
70
- "@restatedev/restate-sdk": "1.16.9",
71
- "@s2-dev/streamstore": "0.27.0",
67
+ "@modelcontextprotocol/sdk": "^1.31.0",
68
+ "@nylorun/core": "0.13.0-beta",
69
+ "@nylorun/harness": "0.22.1-beta",
70
+ "@restatedev/restate-sdk": "1.17.2",
71
+ "@s2-dev/streamstore": "0.28.0",
72
+ "aws4fetch": "1.0.20",
72
73
  "drizzle-orm": "^0.45.3",
73
74
  "hono": "^4.13.12",
74
75
  "jose": "^6.2.12",
@@ -76,17 +77,20 @@
76
77
  "pg": "8.23.1",
77
78
  "pg-logical-replication": "2.5.0",
78
79
  "postgres": "^3.4.9",
80
+ "ws": "^8.22.0",
81
+ "yaml": "^2.9.1",
79
82
  "zod": "^4.6.5"
80
83
  },
81
84
  "devDependencies": {
82
85
  "@a2a-js/sdk": "~1.3.0",
83
- "@ag-ui/client": "1.0.0",
86
+ "@ag-ui/client": "1.0.1",
84
87
  "@scalar/openapi-parser": "^0.29.9",
85
88
  "@types/node": "^26.5.0",
86
89
  "@types/pg": "^8.23.1",
90
+ "@types/ws": "^8.18.2",
87
91
  "@typescript/native": "npm:typescript@^7.0.2",
88
92
  "drizzle-kit": "^0.31.11",
89
93
  "typescript": "npm:@typescript/typescript6@^6.0.2",
90
- "vitest": "^5.0.1"
94
+ "vitest": "^5.0.2"
91
95
  }
92
96
  }