@nylorun/runtime 0.23.0-beta → 0.25.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 (301) hide show
  1. package/CHANGELOG.md +252 -0
  2. package/README.md +87 -28
  3. package/dist/adapters/execution/restate.js +1 -1
  4. package/dist/adapters/replication/pgoutput.d.ts +2 -0
  5. package/dist/adapters/replication/pgoutput.js +6 -3
  6. package/dist/adapters/sandbox/local.d.ts +9 -1
  7. package/dist/adapters/sandbox/local.js +16 -4
  8. package/dist/adapters/sandbox/virtual.js +4 -1
  9. package/dist/api/a2a/card.d.ts +11 -1
  10. package/dist/api/a2a/card.js +26 -2
  11. package/dist/api/a2a/endpoint.js +9 -3
  12. package/dist/api/a2a/protocol.d.ts +25 -2
  13. package/dist/api/a2a/protocol.js +20 -3
  14. package/dist/api/a2a/routes.d.ts +6 -1
  15. package/dist/api/a2a/routes.js +67 -9
  16. package/dist/api/a2a/tasks.js +2 -1
  17. package/dist/api/ag-ui/endpoint.js +8 -3
  18. package/dist/api/ag-ui/history.d.ts +6 -1
  19. package/dist/api/ag-ui/history.js +39 -3
  20. package/dist/api/ag-ui/routes.d.ts +4 -1
  21. package/dist/api/ag-ui/routes.js +63 -20
  22. package/dist/api/ag-ui/translate.js +3 -2
  23. package/dist/api/components.d.ts +5327 -1091
  24. package/dist/api/components.js +40 -16
  25. package/dist/api/gate/keys.d.ts +2 -1
  26. package/dist/api/gate/keys.js +14 -1
  27. package/dist/api/gate/routes.d.ts +6 -0
  28. package/dist/api/gate/routes.js +52 -3
  29. package/dist/api/http/app.js +17 -6
  30. package/dist/api/http/body.d.ts +3 -3
  31. package/dist/api/http/body.js +7 -7
  32. package/dist/api/http/define.d.ts +6 -0
  33. package/dist/api/http/define.js +13 -1
  34. package/dist/api/http/respond.js +1 -1
  35. package/dist/api/http/routes/artifacts.js +35 -8
  36. package/dist/api/http/routes/coverage.d.ts +9 -0
  37. package/dist/api/http/routes/coverage.js +34 -0
  38. package/dist/api/http/routes/environment-manifests.d.ts +9 -0
  39. package/dist/api/http/routes/environment-manifests.js +118 -0
  40. package/dist/api/http/routes/environments.d.ts +11 -0
  41. package/dist/api/http/routes/environments.js +332 -0
  42. package/dist/api/http/routes/mcp.d.ts +1 -1
  43. package/dist/api/http/routes/mcp.js +2 -2
  44. package/dist/api/http/routes/me.js +1 -1
  45. package/dist/api/http/routes/reads.d.ts +94 -9
  46. package/dist/api/http/routes/reads.js +54 -5
  47. package/dist/api/http/routes/sessions.js +24 -9
  48. package/dist/api/http/routes/tenant.js +65 -15
  49. package/dist/api/inbound-files.d.ts +65 -0
  50. package/dist/api/inbound-files.js +115 -0
  51. package/dist/api/openapi.js +48 -28
  52. package/dist/artifacts/disk.d.ts +59 -0
  53. package/dist/artifacts/disk.js +569 -0
  54. package/dist/artifacts/export.d.ts +1 -1
  55. package/dist/artifacts/export.js +2 -3
  56. package/dist/artifacts/folders.d.ts +6 -3
  57. package/dist/artifacts/folders.js +17 -12
  58. package/dist/artifacts/media-types.d.ts +15 -0
  59. package/dist/artifacts/media-types.js +56 -4
  60. package/dist/artifacts/parts.d.ts +12 -3
  61. package/dist/artifacts/parts.js +15 -4
  62. package/dist/artifacts/service.d.ts +2 -1
  63. package/dist/artifacts/service.js +6 -1
  64. package/dist/artifacts/workspace.d.ts +5 -5
  65. package/dist/artifacts/workspace.js +7 -19
  66. package/dist/core/flow-host.d.ts +3 -4
  67. package/dist/core/flow-host.js +5 -2
  68. package/dist/core/provider.d.ts +3 -2
  69. package/dist/core/provider.js +5 -37
  70. package/dist/core/sandbox-routes.d.ts +2 -21
  71. package/dist/core/sandbox-routes.js +11 -44
  72. package/dist/core/turn-manifest.d.ts +3 -2
  73. package/dist/core/turn-manifest.js +29 -5
  74. package/dist/execution/types.d.ts +1 -1
  75. package/dist/gates/contract.d.ts +3 -24
  76. package/dist/gates/contract.js +1 -36
  77. package/dist/gates/egress-credentials.d.ts +9 -0
  78. package/dist/gates/egress-credentials.js +11 -4
  79. package/dist/gates/egress.d.ts +22 -1
  80. package/dist/gates/egress.js +116 -10
  81. package/dist/gates/fixture-model.d.ts +28 -0
  82. package/dist/gates/fixture-model.js +92 -0
  83. package/dist/gates/handler.js +4 -4
  84. package/dist/gates/http-tool.d.ts +9 -6
  85. package/dist/gates/http-tool.js +11 -17
  86. package/dist/gates/in-process.d.ts +5 -0
  87. package/dist/gates/in-process.js +12 -1
  88. package/dist/gates/journal.d.ts +30 -0
  89. package/dist/gates/journal.js +51 -0
  90. package/dist/gates/mcp-handler.d.ts +4 -0
  91. package/dist/gates/mcp-handler.js +7 -4
  92. package/dist/gates/meter.d.ts +10 -2
  93. package/dist/gates/meter.js +20 -5
  94. package/dist/gates/policy.d.ts +49 -0
  95. package/dist/gates/policy.js +132 -0
  96. package/dist/gates/tenant-vaults.d.ts +7 -7
  97. package/dist/gates/tenant-vaults.js +5 -5
  98. package/dist/gates/tool-calls.d.ts +10 -5
  99. package/dist/gates/tool-calls.js +28 -10
  100. package/dist/gates/tool-client.d.ts +8 -0
  101. package/dist/gates/tool-client.js +27 -11
  102. package/dist/gates/tool-contract.d.ts +11 -69
  103. package/dist/gates/tool-contract.js +1 -85
  104. package/dist/gates/tool-gate.d.ts +1 -1
  105. package/dist/harness/calls.d.ts +14 -7
  106. package/dist/harness/calls.js +30 -24
  107. package/dist/harness/executors.js +12 -9
  108. package/dist/harness/main.js +24 -5
  109. package/dist/harness/pod.d.ts +39 -0
  110. package/dist/harness/pod.js +46 -0
  111. package/dist/harness/service.d.ts +0 -1
  112. package/dist/harness/service.js +17 -9
  113. package/dist/harness/setup.d.ts +29 -0
  114. package/dist/harness/setup.js +174 -0
  115. package/dist/harness/ws-client.js +3 -2
  116. package/dist/harness/ws-port.d.ts +1 -5
  117. package/dist/harness/ws-port.js +1 -5
  118. package/dist/harness-api/in-process.js +3 -3
  119. package/dist/harness-api/record.d.ts +15 -1
  120. package/dist/harness-api/record.js +180 -50
  121. package/dist/harness-api/redact.d.ts +3 -0
  122. package/dist/harness-api/redact.js +45 -0
  123. package/dist/harness-api/server.d.ts +35 -4
  124. package/dist/harness-api/server.js +130 -17
  125. package/dist/harness-api/start.d.ts +10 -3
  126. package/dist/harness-api/start.js +40 -10
  127. package/dist/harness-api/tool-journal.d.ts +105 -0
  128. package/dist/harness-api/tool-journal.js +132 -0
  129. package/dist/harness-api/tool-status.d.ts +13 -0
  130. package/dist/harness-api/tool-status.js +26 -0
  131. package/dist/harness-api/workspace.d.ts +12 -7
  132. package/dist/harness-api/workspace.js +47 -25
  133. package/dist/harness-api/ws-server.d.ts +11 -3
  134. package/dist/harness-api/ws-server.js +94 -15
  135. package/dist/host/gates.d.ts +2 -1
  136. package/dist/host/gates.js +10 -2
  137. package/dist/host/main.js +54 -5
  138. package/dist/host/operate.js +67 -5
  139. package/dist/host/stack-config.d.ts +64 -0
  140. package/dist/host/stack-config.js +62 -2
  141. package/dist/keys/client.js +4 -1
  142. package/dist/keys/keys.d.ts +13 -6
  143. package/dist/keys/keys.js +9 -8
  144. package/dist/management-openapi.json +1264 -2171
  145. package/dist/mcp/connect.d.ts +26 -22
  146. package/dist/mcp/connect.js +30 -34
  147. package/dist/mcp/pool.d.ts +3 -6
  148. package/dist/mcp/pool.js +14 -28
  149. package/dist/mcp/preview.d.ts +3 -3
  150. package/dist/mcp/preview.js +10 -21
  151. package/dist/mcp/snapshot.d.ts +16 -24
  152. package/dist/mcp/snapshot.js +33 -45
  153. package/dist/model/capabilities.d.ts +26 -0
  154. package/dist/model/capabilities.js +44 -0
  155. package/dist/model/catalog.d.ts +34 -3
  156. package/dist/model/catalog.js +38 -2
  157. package/dist/model/models.d.ts +2 -0
  158. package/dist/model/models.js +1 -1
  159. package/dist/model/native-blocks.d.ts +35 -0
  160. package/dist/model/native-blocks.js +170 -0
  161. package/dist/model/pi-model.js +114 -94
  162. package/dist/openapi.json +12822 -3943
  163. package/dist/reads/types.d.ts +16 -3
  164. package/dist/record/append.d.ts +3 -1
  165. package/dist/record/append.js +1 -0
  166. package/dist/record/envelope.d.ts +5 -1
  167. package/dist/record/envelope.js +7 -3
  168. package/dist/record/environment.d.ts +25 -0
  169. package/dist/record/environment.js +44 -0
  170. package/dist/record/index.d.ts +1 -1
  171. package/dist/record/index.js +1 -1
  172. package/dist/sandbox/disk-fs.d.ts +95 -0
  173. package/dist/sandbox/disk-fs.js +206 -0
  174. package/dist/sandbox/join.d.ts +17 -18
  175. package/dist/sandbox/join.js +46 -15
  176. package/dist/sandbox/manager.d.ts +47 -21
  177. package/dist/sandbox/manager.js +162 -107
  178. package/dist/sandbox/placement.d.ts +14 -13
  179. package/dist/sandbox/placement.js +31 -14
  180. package/dist/sandbox/pods/client.d.ts +9 -2
  181. package/dist/sandbox/pods/lifecycle.d.ts +36 -27
  182. package/dist/sandbox/pods/lifecycle.js +29 -26
  183. package/dist/sandbox/pods/reconcile.js +18 -12
  184. package/dist/sandbox/pods/spec.d.ts +15 -13
  185. package/dist/sandbox/pods/spec.js +17 -20
  186. package/dist/sandbox/records.d.ts +2 -2
  187. package/dist/sandbox/records.js +3 -3
  188. package/dist/sandbox/resolve.d.ts +37 -27
  189. package/dist/sandbox/resolve.js +73 -41
  190. package/dist/sandbox/runtime-tools.d.ts +9 -0
  191. package/dist/sandbox/runtime-tools.js +16 -0
  192. package/dist/sandbox/session-sandbox.d.ts +4 -6
  193. package/dist/sandbox/session-sandbox.js +47 -94
  194. package/dist/sandbox/setup.d.ts +64 -0
  195. package/dist/sandbox/setup.js +160 -0
  196. package/dist/sandbox/share.d.ts +21 -26
  197. package/dist/sandbox/share.js +36 -63
  198. package/dist/sandbox/skills.d.ts +8 -6
  199. package/dist/sandbox/skills.js +24 -23
  200. package/dist/sandbox/tenant-config.d.ts +12 -14
  201. package/dist/sandbox/tenant-config.js +12 -17
  202. package/dist/sandbox/types.d.ts +6 -0
  203. package/dist/store/postgres/drizzle/0020_environments.sql +63 -0
  204. package/dist/store/postgres/drizzle/0021_disk_versions.sql +7 -0
  205. package/dist/store/postgres/drizzle/0022_model_usage_timing.sql +5 -0
  206. package/dist/store/postgres/drizzle/0023_artifact_pages.sql +1 -0
  207. package/dist/store/postgres/drizzle/0024_flat_manifest.sql +7 -0
  208. package/dist/store/postgres/drizzle/0025_environment_manifests.sql +14 -0
  209. package/dist/store/postgres/drizzle/0026_environment_forks.sql +1 -0
  210. package/dist/store/postgres/drizzle/0027_environment_image_digest.sql +1 -0
  211. package/dist/store/postgres/drizzle/meta/_journal.json +56 -0
  212. package/dist/store/postgres/reads.js +123 -17
  213. package/dist/store/postgres/record-writer.d.ts +4 -4
  214. package/dist/store/postgres/record-writer.js +9 -9
  215. package/dist/store/postgres/record.d.ts +7 -0
  216. package/dist/store/postgres/record.js +23 -2
  217. package/dist/store/postgres/schema.d.ts +1126 -736
  218. package/dist/store/postgres/schema.js +136 -73
  219. package/dist/store/postgres/store.js +195 -67
  220. package/dist/store/types.d.ts +165 -50
  221. package/dist/tenant/advance.d.ts +7 -6
  222. package/dist/tenant/advance.js +118 -42
  223. package/dist/tenant/artifact-tool.d.ts +1 -1
  224. package/dist/tenant/artifact-tool.js +46 -26
  225. package/dist/tenant/commands.d.ts +15 -2
  226. package/dist/tenant/commands.js +115 -14
  227. package/dist/tenant/context.d.ts +56 -24
  228. package/dist/tenant/context.js +4 -15
  229. package/dist/tenant/effects.d.ts +1 -1
  230. package/dist/tenant/effects.js +44 -29
  231. package/dist/tenant/environment-manifests.d.ts +57 -0
  232. package/dist/tenant/environment-manifests.js +200 -0
  233. package/dist/tenant/environments.d.ts +174 -0
  234. package/dist/tenant/environments.js +1044 -0
  235. package/dist/tenant/harness-tokens.d.ts +21 -0
  236. package/dist/tenant/harness-tokens.js +61 -0
  237. package/dist/tenant/host-token.d.ts +1 -1
  238. package/dist/tenant/host-token.js +2 -4
  239. package/dist/tenant/identity-file.d.ts +4 -4
  240. package/dist/tenant/identity-file.js +17 -8
  241. package/dist/tenant/issuers.js +3 -3
  242. package/dist/tenant/model-setting.d.ts +14 -0
  243. package/dist/tenant/model-setting.js +51 -0
  244. package/dist/tenant/principals.js +1 -1
  245. package/dist/tenant/reset.js +3 -3
  246. package/dist/tenant/runtime.d.ts +6 -1
  247. package/dist/tenant/runtime.js +7 -1
  248. package/dist/tenant/session.d.ts +3 -3
  249. package/dist/tenant/session.js +0 -14
  250. package/dist/tenant/sessions.d.ts +18 -14
  251. package/dist/tenant/sessions.js +99 -143
  252. package/dist/tenant/signing-keys.d.ts +5 -1
  253. package/dist/tenant/skill-tool.d.ts +3 -3
  254. package/dist/tenant/skill-tool.js +6 -11
  255. package/dist/tenant/status.js +3 -3
  256. package/dist/tenant/sweep.js +13 -7
  257. package/dist/tenant/tool-results.js +1 -1
  258. package/dist/tenant/tool-search.d.ts +7 -11
  259. package/dist/tenant/tool-search.js +8 -5
  260. package/dist/tenant/transcript.d.ts +245 -36
  261. package/dist/tenant/transcript.js +64 -12
  262. package/dist/tenant/turn-timing.d.ts +43 -0
  263. package/dist/tenant/turn-timing.js +39 -0
  264. package/dist/tenant/types.d.ts +2 -0
  265. package/dist/traces/ids.d.ts +41 -0
  266. package/dist/traces/ids.js +81 -0
  267. package/dist/traces/key.d.ts +6 -0
  268. package/dist/traces/key.js +37 -0
  269. package/dist/traces/map.d.ts +113 -0
  270. package/dist/traces/map.js +289 -0
  271. package/dist/traces/options.d.ts +7 -0
  272. package/dist/traces/options.js +1 -0
  273. package/dist/traces/otlp.d.ts +42 -0
  274. package/dist/traces/otlp.js +84 -0
  275. package/dist/traces/projector.d.ts +58 -0
  276. package/dist/traces/projector.js +200 -0
  277. package/dist/traces/read.d.ts +23 -0
  278. package/dist/traces/read.js +16 -0
  279. package/dist/vault/coverage.d.ts +26 -0
  280. package/dist/vault/coverage.js +62 -0
  281. package/dist/vault/error.d.ts +11 -1
  282. package/dist/vault/error.js +5 -1
  283. package/dist/vault/host-model.d.ts +4 -1
  284. package/dist/vault/host-model.js +10 -2
  285. package/dist/vault/service.d.ts +92 -48
  286. package/dist/vault/service.js +462 -376
  287. package/dist/vault/sources.d.ts +5 -16
  288. package/dist/vault/sources.js +6 -8
  289. package/dist/version.d.ts +1 -1
  290. package/dist/version.js +2 -2
  291. package/package.json +13 -13
  292. package/dist/api/http/routes/sandboxes.d.ts +0 -9
  293. package/dist/api/http/routes/sandboxes.js +0 -153
  294. package/dist/api/http/routes/vaults.d.ts +0 -10
  295. package/dist/api/http/routes/vaults.js +0 -139
  296. package/dist/record/sandbox.d.ts +0 -25
  297. package/dist/record/sandbox.js +0 -43
  298. package/dist/sandbox/capability.d.ts +0 -3
  299. package/dist/sandbox/capability.js +0 -10
  300. package/dist/tenant/sandboxes.d.ts +0 -123
  301. package/dist/tenant/sandboxes.js +0 -566
@@ -1,34 +1,42 @@
1
1
  /**
2
- * The Runtime vault (docs/design/runtime/vault.md, architecture §12.9):
3
- * administer vaults and credentials, attach them to sessions, and authorize
4
- * outbound MCP calls. Storage is the async Session Store (`store/types.ts`);
5
- * this module holds no SQL.
2
+ * The Runtime vault (docs/design/runtime/vault.md, D56): one vault per Environment, created and
3
+ * deleted with it, holding the Environment's secrets; and the host vault, which holds the model
4
+ * credential. Storage is the async Session Store (`store/types.ts`); this module holds no SQL.
6
5
  *
7
6
  * ## Public surface
8
7
  *
9
8
  * ```ts
10
9
  * new VaultService({ store: SessionStore, kek: () => Buffer })
11
10
  *
12
- * // Administration: each opens its own transaction and writes its audit rows
13
- * // in that transaction. Creates and rotations replay by idempotency key.
14
- * createVault(body: CreateVaultRequest): Promise<VaultInfo>
15
- * listVaults(ownerUserId: string | undefined, options?: { installation?: boolean }): Promise<VaultInfo[]>
16
- * getVault(id: string): Promise<VaultInfo>
17
- * deleteVault(id: string): Promise<{ id: string }>
18
- * createCredential(vaultId: string, body: CreateCredentialRequest): Promise<CredentialInfo>
19
- * listCredentials(vaultId: string): Promise<CredentialInfo[]>
20
- * getCredential(vaultId: string, id: string): Promise<CredentialInfo>
21
- * rotateCredential(vaultId: string, id: string, body: RotateCredentialRequest): Promise<CredentialInfo>
22
- * deleteCredential(vaultId: string, id: string): Promise<{ id: string }>
11
+ * // An Environment's vault, in the caller's transaction (`tenant/environments.ts`).
12
+ * createEnvironmentVault(t: Tx, environmentId: string, actor: string, id?: string): Promise<string>
13
+ * deleteEnvironmentVault(t: Tx, environmentId: string, actor: string): Promise<void>
23
14
  *
24
- * // Attachment: run inside the caller's session PUT transaction.
25
- * assertAttachment(t: Tx, ownerUserId: string, vaultIds: readonly string[], selections: readonly CredentialSelection[], options?: { opaque?: boolean }): Promise<void>
26
- * recordAttachment(t: Tx, sessionId: string, vaultIds: readonly string[]): Promise<void>
15
+ * // A fork's secrets (Q15): `Keys` seals the copies with the vault key in its own transaction,
16
+ * // before the fork's; the fork's transaction inserts them, so the process that forks never
17
+ * // holds the key.
18
+ * sealEnvironmentSecretCopies(from, vaultId, slots | null): Promise<SealedSecretCopies>
19
+ * insertEnvironmentSecretCopies(t: Tx, vaultId, from, actor, sealed): Promise<{ copied; dropped }>
20
+ *
21
+ * // Its secrets (`/v1/environments/{id}/secrets`): each opens its own transaction and writes its
22
+ * // audit rows in it. A put needs the vault key, so callers reach it through `Keys`.
23
+ * putEnvironmentSecret(environmentId, name, body: PutEnvironmentSecretRequest, actor): Promise<EnvironmentSecretView>
24
+ * listEnvironmentSecrets(environmentId): Promise<EnvironmentSecretView[]>
25
+ * getEnvironmentSecret(environmentId, name): Promise<EnvironmentSecretView>
26
+ * deleteEnvironmentSecret(environmentId, name, actor): Promise<{ id: string }>
27
+ *
28
+ * // The shell's variables of a session's sandbox, in the caller's transaction (R2c).
29
+ * environmentVariables(t: Tx, environmentId: string | undefined): Promise<Record<string, string>>
27
30
  *
28
31
  * // Use: opens its own transactions; never call it inside one. `sessionCredentials`
29
32
  * // (`vault/sources.ts`) calls it for a session's MCP servers and HTTP tools; a tool preview
30
- * // (R2b C12, `mcp/preview.ts`) for no session.
31
- * authorize(input: { sessionId?; vaultIds; credentialSelections; url; serverName? }): Promise<AuthorizeResult>
33
+ * // (R2b C12, `mcp/preview.ts`) for no session; egress-gate releases shell secrets.
34
+ * authorize(input: { sessionId?; environmentId?; url }): Promise<AuthorizeResult>
35
+ * releaseEnvironmentSecret(input: { environmentId; host }): Promise<…>
36
+ *
37
+ * // Coverage: what `authorize` would decide for each URL a manifest names, in one transaction,
38
+ * // with no secret read and no audit row.
39
+ * coverage(manifest: AgentManifest | WorkflowManifest, request: CredentialCoverageRequest): Promise<CredentialCoverage>
32
40
  *
33
41
  * // Host model credential: each opens its own transaction.
34
42
  * getHostModel(): Promise<HostModelView>
@@ -41,12 +49,12 @@
41
49
  * (`vault/host-model.ts`), which only the Model Gate uses: this service seals the host model's
42
50
  * credential but never reads it back.
43
51
  *
44
- * ## Scopes
52
+ * ## One credential per host and per URL
45
53
  *
46
- * - `user`: one person's vault (owner `ownerUserId`), attachable only to that person's sessions.
47
- * - `installation` (`INSTALLATION_OWNER`): the installation's own, created by application keys
48
- * only, attachable to any session. Subject and token callers never see one.
49
- * - `host`: the model vault (`HOST_VAULT_ID`), never listed or attached.
54
+ * A secret write is refused (`409 credential_conflict`) when another `environment_secret` of the
55
+ * vault is bound to one of its hosts, or another `bearer` or `headers` credential to its URL, so
56
+ * a release or a tool call never has two to choose from. Names are unique per vault: for the
57
+ * shell kinds the name is the variable, so no variable is set twice.
50
58
  *
51
59
  * ## Transactions and I/O
52
60
  *
@@ -54,32 +62,31 @@
54
62
  * filesystem (it creates the key file on first use), so methods that
55
63
  * encrypt or decrypt resolve the KEK before opening their transaction and
56
64
  * do only in-memory crypto inside it.
57
- * - `authorize` reads the attached vaults and the matching credential rows
58
- * (ciphertext only) in one transaction and decides there. A refusal decided
59
- * from those rows (missing vault, ambiguous, selection mismatch) writes its
60
- * audit row in that same transaction. Decryption happens after it commits,
61
- * so the plaintext never enters a transaction. The `use`/`approved` audit
62
- * row is written in a final transaction before the header is returned: if
63
- * that write fails, `authorize` rejects and no header leaves the vault, so
64
- * every approved use has an audit row. Unreadable ciphertext is audited in
65
- * its own transaction before the refusal is returned.
65
+ * - `authorize` reads the Environment's vault and the credential bound to the URL (ciphertext
66
+ * only) in one transaction. Decryption happens after it commits, so the plaintext never
67
+ * enters a transaction. The `use`/`approved` audit row is written in a final transaction
68
+ * before the header is returned: if that write fails, `authorize` rejects and no header
69
+ * leaves the vault, so every approved use has an audit row. Unreadable ciphertext is audited
70
+ * in its own transaction before the refusal is returned.
66
71
  *
67
72
  * ## Credentials (R2b C2)
68
73
  *
69
- * A credential is bound to the URL a manifest names and sends a `bearer` token or a `headers`
70
- * map, both sealed. Its `via` (where requests go, such as a gateway) and `identity` (the header
71
- * that names the session owner) are not secret: they live unsealed in the binding, beside the
72
- * URL and the header names, and a rotation keeps them unless it changes them. The seal's AAD
73
- * binds the type and URL only, and matching is by URL only, so `via` never decides which
74
- * credential is chosen.
74
+ * A tool credential is bound to the URL a manifest names and sends a `bearer` token or a
75
+ * `headers` map, both sealed. Its `via` (where requests go, such as a gateway) and `identity`
76
+ * (the header that names the session owner) are not secret: they live unsealed in the binding,
77
+ * beside the URL and the header names. The seal's AAD binds the vault, credential id, type and
78
+ * URL only, and matching is by URL only, so `via` never decides which credential is chosen.
75
79
  */
76
80
  import { createHash, randomUUID } from "node:crypto";
77
- import { ENVIRONMENT_SECRET_DEFAULT_INJECT, ENVIRONMENT_SECRET_SENTINEL, INSTALLATION_OWNER, renderInjectFormat, } from "@nylorun/core/contracts";
81
+ import { ENVIRONMENT_SECRET_DEFAULT_INJECT, ENVIRONMENT_SECRET_SENTINEL, environmentSecretNameError, renderInjectFormat, } from "@nylorun/core/contracts";
78
82
  import { canonical } from "../store/canonical.js";
79
83
  import { decryptSecret, encryptSecret, VaultCryptoError } from "./crypto.js";
80
84
  import { VaultError } from "./error.js";
85
+ import { credentialNeeds } from "./coverage.js";
81
86
  import { checkCredentialHeaders } from "./headers.js";
82
- import { hostModelCatalog } from "../model/catalog.js";
87
+ import { hostModelCatalog, inputsOfHostModel } from "../model/catalog.js";
88
+ import { inputsError } from "../model/capabilities.js";
89
+ import { readAddedInputs } from "../tenant/model-setting.js";
83
90
  import { normalizeVaultUrl } from "./url.js";
84
91
  export const HOST_VAULT_ID = "host";
85
92
  const HOST_MODEL_ID = "host-model";
@@ -90,242 +97,262 @@ export class VaultService {
90
97
  this.store = options.store;
91
98
  this.kek = options.kek;
92
99
  }
93
- // --- administration --------------------------------------------------------
94
- async createVault(body) {
95
- return this.store.tx((t) => this.replay(t, `create-vault:${body.idempotencyKey}`, body, async () => {
96
- const installation = body.scope === "installation";
97
- const ownerUserId = installation ? INSTALLATION_OWNER : body.ownerUserId;
98
- if (!ownerUserId)
99
- throw new VaultError(400, "ownerUserId is required for a user vault");
100
- const row = {
101
- id: randomUUID(),
102
- name: body.name,
103
- ownerUserId,
104
- metadataJson: body.metadata ? JSON.stringify(body.metadata) : null,
105
- createdAt: new Date().toISOString(),
106
- scope: installation ? "installation" : "user",
107
- };
108
- await t.insertVault(row);
109
- await this.audit(t, {
110
- actor: "application",
111
- action: "create",
112
- vaultId: row.id,
113
- outcome: "created",
114
- });
115
- return vaultInfoOf(row);
116
- }));
117
- }
100
+ // --- an Environment's vault (caller's transaction) ------------------------------
118
101
  /**
119
- * One person's vaults, then (with `installation`) the installation vaults. No owner lists
120
- * the installation vaults only.
102
+ * Creates Environment `environmentId`'s vault (D56), in the transaction that creates it; answers
103
+ * its id, `id` when given (a fork's, whose secrets were sealed for it first).
121
104
  */
122
- async listVaults(ownerUserId, options = {}) {
123
- return this.store.tx(async (t) => {
124
- const own = ownerUserId === undefined ? [] : await t.vaultsByOwner(ownerUserId);
125
- const shared = options.installation ? await t.installationVaults() : [];
126
- return [...own, ...shared].map(vaultInfoOf);
127
- });
128
- }
129
- async getVault(id) {
130
- return this.store.tx((t) => this.vaultInfo(t, id));
105
+ async createEnvironmentVault(t, environmentId, actor, id = randomUUID()) {
106
+ const row = {
107
+ id,
108
+ name: environmentId,
109
+ metadataJson: null,
110
+ createdAt: new Date().toISOString(),
111
+ environmentId,
112
+ };
113
+ await t.insertVault(row);
114
+ await this.audit(t, { actor, action: "create", vaultId: row.id, target: environmentId, outcome: "created" });
115
+ return row.id;
131
116
  }
132
- async deleteVault(id) {
133
- return this.store.tx(async (t) => {
134
- await this.vaultInfo(t, id);
135
- for (const credential of await t.credentialsForVault(id)) {
136
- await this.audit(t, {
137
- actor: "application",
138
- action: "delete",
139
- vaultId: id,
140
- credentialId: credential.id,
141
- outcome: "deleted",
142
- });
143
- }
144
- await t.deleteVault(id);
117
+ /**
118
+ * Deletes Environment `environmentId`'s vault and its credentials, with their audit rows, in
119
+ * the transaction that deletes the Environment: no release or tool call finds them after it
120
+ * commits.
121
+ */
122
+ async deleteEnvironmentVault(t, environmentId, actor) {
123
+ const vault = await t.vaultOfEnvironment(environmentId);
124
+ if (!vault)
125
+ return;
126
+ for (const credential of await t.credentialsForVault(vault.id))
145
127
  await this.audit(t, {
146
- actor: "application",
128
+ actor,
147
129
  action: "delete",
148
- vaultId: id,
130
+ vaultId: vault.id,
131
+ credentialId: credential.id,
132
+ target: environmentId,
149
133
  outcome: "deleted",
150
134
  });
151
- return { id };
152
- });
135
+ await t.deleteVault(vault.id);
136
+ await this.audit(t, { actor, action: "delete", vaultId: vault.id, target: environmentId, outcome: "deleted" });
153
137
  }
154
- async createCredential(vaultId, body) {
138
+ /**
139
+ * Seals Environment `from`'s secrets again for vault `vaultId`, a fork's new vault (Q15): each
140
+ * value is unsealed and sealed under the new vault's and credential's ids, which its AAD binds.
141
+ * Needs the vault key, so callers reach it through `Keys`; it reads `from` in its own
142
+ * transaction and writes nothing. With `slots` (what the fork runs declares secret slots, D64),
143
+ * only the names it declares are sealed, each with its slot's binding; a value the slot's type
144
+ * cannot take is left behind. The fork's transaction inserts the copies
145
+ * (`insertEnvironmentSecretCopies`).
146
+ */
147
+ async sealEnvironmentSecretCopies(from, vaultId, slots) {
155
148
  const kek = this.kek();
156
- return this.store.tx((t) => this.replay(t, `create-credential:${vaultId}:${body.idempotencyKey}`, body, async () => {
157
- await this.vaultInfo(t, vaultId);
158
- const id = randomUUID();
159
- const type = body.auth.type;
160
- let payload;
161
- let binding;
162
- if (body.auth.type === "environment_secret" || body.auth.type === "environment_variable") {
163
- ({ payload, binding } = environmentCredential(body.auth));
164
- }
165
- else {
166
- const url = normalizeVaultUrl(body.auth.url);
167
- payload = payloadOf(body.auth);
168
- binding = bindingOf(url, payload, body.auth);
149
+ const rows = await this.store.tx(async (t) => {
150
+ const source = await t.vaultOfEnvironment(from);
151
+ return source ? (await t.credentialsForVault(source.id)).filter(isUserCredential) : [];
152
+ });
153
+ const sealed = [];
154
+ const dropped = [];
155
+ const now = new Date().toISOString();
156
+ for (const row of rows) {
157
+ const payload = readPayload(kek, row);
158
+ let type = row.type;
159
+ let binding = credentialBinding(row);
160
+ let value = payload;
161
+ if (slots) {
162
+ const slot = slots[row.name];
163
+ const rebound = slot === undefined ? undefined : rebind(row.name, slot, row, payload, binding);
164
+ if (rebound === undefined) {
165
+ dropped.push(row.name);
166
+ continue;
167
+ }
168
+ ({ type, binding, payload: value } = rebound);
169
+ try {
170
+ conflictOf(type, binding, sealed);
171
+ }
172
+ catch (error) {
173
+ if (!(error instanceof VaultError))
174
+ throw error;
175
+ dropped.push(row.name);
176
+ continue;
177
+ }
169
178
  }
170
- const sealed = encryptSecret(kek, credentialAad(vaultId, id, type, binding), Buffer.from(JSON.stringify(payload), "utf8"));
171
- const row = {
179
+ const id = randomUUID();
180
+ sealed.push({
172
181
  id,
173
182
  vaultId,
174
- name: body.name,
183
+ name: row.name,
175
184
  type,
176
185
  bindingJson: JSON.stringify(binding),
177
- expiresAt: null,
178
- createdAt: new Date().toISOString(),
186
+ expiresAt: row.expiresAt,
187
+ createdAt: now,
179
188
  rotatedAt: null,
180
- ...sealed,
181
- };
182
- await t.insertCredential(row);
183
- await this.audit(t, {
184
- actor: "application",
185
- action: "create",
189
+ ...encryptSecret(kek, credentialAad(vaultId, id, type, binding), Buffer.from(JSON.stringify(value), "utf8")),
190
+ });
191
+ }
192
+ return {
193
+ copies: sealed.map((row) => ({
194
+ id: row.id,
195
+ name: row.name,
196
+ type: row.type,
197
+ bindingJson: row.bindingJson,
198
+ expiresAt: row.expiresAt,
199
+ kekId: row.kekId,
200
+ nonce: Buffer.from(row.nonce).toString("base64"),
201
+ ciphertext: Buffer.from(row.ciphertext).toString("base64"),
202
+ wrappedDek: Buffer.from(row.wrappedDek).toString("base64"),
203
+ })),
204
+ dropped: dropped.sort(),
205
+ };
206
+ }
207
+ /**
208
+ * Inserts a fork's sealed copies (`sealEnvironmentSecretCopies`) into vault `vaultId`, in the
209
+ * transaction that creates the fork, with an audit row each naming `from`. Reads no secret.
210
+ */
211
+ async insertEnvironmentSecretCopies(t, vaultId, from, actor, sealed) {
212
+ const now = new Date().toISOString();
213
+ for (const copy of sealed.copies) {
214
+ await t.insertCredential({
215
+ id: copy.id,
186
216
  vaultId,
187
- credentialId: id,
188
- outcome: "created",
217
+ name: copy.name,
218
+ type: copy.type,
219
+ bindingJson: copy.bindingJson,
220
+ expiresAt: copy.expiresAt,
221
+ createdAt: now,
222
+ rotatedAt: null,
223
+ kekId: copy.kekId,
224
+ nonce: Buffer.from(copy.nonce, "base64"),
225
+ ciphertext: Buffer.from(copy.ciphertext, "base64"),
226
+ wrappedDek: Buffer.from(copy.wrappedDek, "base64"),
189
227
  });
190
- return credentialInfoOf(row);
191
- }));
228
+ await this.audit(t, { actor, action: "create", vaultId, credentialId: copy.id, target: from, outcome: "copied" });
229
+ }
230
+ return { copied: sealed.copies.map((copy) => copy.name).sort(), dropped: [...sealed.dropped] };
192
231
  }
193
- async listCredentials(vaultId) {
232
+ // --- an Environment's secrets ----------------------------------------------------
233
+ /**
234
+ * `PUT /v1/environments/{id}/secrets/{name}`: creates or replaces one credential. The same
235
+ * name and type keeps the credential's id and re-seals its value and binding; another type
236
+ * replaces it. Refused (`409 credential_conflict`) when another credential of the vault is
237
+ * bound to one of its hosts or to its URL. Runs under the Environment row's lock, which also
238
+ * orders it with session opens and deletes, and counts as the Environment's use.
239
+ */
240
+ async putEnvironmentSecret(environmentId, name, body, actor) {
241
+ const invalid = environmentSecretNameError(body.type, name);
242
+ if (invalid)
243
+ throw new VaultError(400, invalid);
244
+ let payload;
245
+ let binding;
246
+ if (body.type === "environment_secret" || body.type === "environment_variable")
247
+ ({ payload, binding } = environmentCredential(name, body));
248
+ else {
249
+ const url = normalizeVaultUrl(body.url);
250
+ payload = payloadOf(body);
251
+ binding = bindingOf(url, payload, body);
252
+ }
253
+ const kek = this.kek();
194
254
  return this.store.tx(async (t) => {
195
- await this.vaultInfo(t, vaultId);
196
- return (await t.credentialsForVault(vaultId))
255
+ const vault = await this.environmentVault(t, environmentId, { lock: true });
256
+ const now = new Date().toISOString();
257
+ const others = (await t.credentialsForVault(vault.id))
197
258
  .filter(isUserCredential)
198
- .map(credentialInfoOf);
199
- });
200
- }
201
- async getCredential(vaultId, id) {
202
- return this.store.tx(async (t) => credentialInfoOf(await this.credentialRow(t, vaultId, id)));
203
- }
204
- async rotateCredential(vaultId, id, body) {
205
- const kek = this.kek();
206
- return this.store.tx((t) => this.replay(t, `rotate:${vaultId}:${id}:${body.idempotencyKey}`, body, async () => {
207
- const row = await this.credentialRow(t, vaultId, id);
208
- if (row.type !== body.auth.type)
209
- throw new VaultError(409, "Credential type cannot change");
210
- const current = credentialBinding(row);
211
- let next;
212
- let binding;
213
- if (body.auth.type === "environment_secret") {
214
- next = { value: body.auth.secretValue };
215
- binding = current;
216
- }
217
- else if (body.auth.type === "environment_variable") {
218
- next = {};
219
- binding = { ...current, variableValue: body.auth.variableValue };
259
+ .filter((row) => row.name !== name);
260
+ conflictOf(body.type, binding, others);
261
+ const existing = await t.credentialByName(vault.id, name);
262
+ let row;
263
+ if (existing && isUserCredential(existing) && existing.type === body.type) {
264
+ row = await this.writePayload(t, kek, existing, payload, binding, now);
265
+ await this.audit(t, { actor, action: "rotate", vaultId: vault.id, credentialId: row.id, outcome: "rotated" });
220
266
  }
221
267
  else {
222
- next = payloadOf(body.auth);
223
- binding = bindingOf(current.url, next, {
224
- via: body.auth.via === undefined ? current.via : body.auth.via,
225
- identity: body.auth.identity === undefined ? current.identity : body.auth.identity,
226
- });
268
+ if (existing) {
269
+ await t.deleteCredential(vault.id, existing.id);
270
+ await this.audit(t, { actor, action: "delete", vaultId: vault.id, credentialId: existing.id, outcome: "deleted" });
271
+ }
272
+ const id = randomUUID();
273
+ const sealed = encryptSecret(kek, credentialAad(vault.id, id, body.type, binding), Buffer.from(JSON.stringify(payload), "utf8"));
274
+ row = {
275
+ id,
276
+ vaultId: vault.id,
277
+ name,
278
+ type: body.type,
279
+ bindingJson: JSON.stringify(binding),
280
+ expiresAt: null,
281
+ createdAt: now,
282
+ rotatedAt: null,
283
+ ...sealed,
284
+ };
285
+ await t.insertCredential(row);
286
+ await this.audit(t, { actor, action: "create", vaultId: vault.id, credentialId: id, outcome: "created" });
227
287
  }
228
- const updated = await this.writePayload(t, kek, row, next, binding, new Date().toISOString());
229
- await this.audit(t, {
230
- actor: "application",
231
- action: "rotate",
232
- vaultId,
233
- credentialId: id,
234
- outcome: "rotated",
235
- });
236
- return credentialInfoOf(updated);
237
- }));
288
+ await t.updateEnvironment(environmentId, { lastUsedAt: now }, now);
289
+ return secretViewOf(row);
290
+ });
238
291
  }
239
- async deleteCredential(vaultId, id) {
292
+ /** The Environment's secrets: names, types and bindings, never values. */
293
+ async listEnvironmentSecrets(environmentId) {
240
294
  return this.store.tx(async (t) => {
241
- await this.credentialRow(t, vaultId, id);
242
- await t.deleteCredential(vaultId, id);
243
- await this.audit(t, {
244
- actor: "application",
245
- action: "delete",
246
- vaultId,
247
- credentialId: id,
248
- outcome: "deleted",
249
- });
250
- return { id };
295
+ const vault = await this.environmentVault(t, environmentId);
296
+ return (await t.credentialsForVault(vault.id))
297
+ .filter(isUserCredential)
298
+ .sort((a, b) => a.name.localeCompare(b.name))
299
+ .map(secretViewOf);
251
300
  });
252
301
  }
253
- // --- attachment (caller's transaction) --------------------------------------
254
- /**
255
- * Checks a session's vaults and selections. Run in the session PUT transaction. `opaque`
256
- * (a request acting for a subject) reports another owner's vault as the 404 of a missing one.
257
- */
258
- async assertAttachment(t, ownerUserId, vaultIds, selections, options = {}) {
259
- if (new Set(vaultIds).size !== vaultIds.length)
260
- throw new VaultError(400, "Duplicate vault id");
261
- if (new Set(selections.map((item) => item.serverName)).size !== selections.length)
262
- throw new VaultError(400, "Duplicate credential selection");
263
- for (const id of vaultIds) {
264
- const vault = await t.getVault(id);
265
- if (!vault)
266
- throw new VaultError(404, "Vault not found");
267
- if (vault.scope === "host")
268
- throw new VaultError(400, "Host vault cannot be attached to a session");
269
- // Any session may use the installation's own vaults (F9-D13).
270
- if (vault.scope === "installation")
271
- continue;
272
- if (vault.ownerUserId !== ownerUserId)
273
- throw options.opaque
274
- ? new VaultError(404, "Vault not found")
275
- : new VaultError(403, "Vault belongs to another user");
276
- }
277
- for (const selection of selections) {
278
- const row = await t.getCredential(selection.credentialId);
279
- if (!row || !vaultIds.includes(row.vaultId))
280
- throw new VaultError(400, "Credential is not in an attached vault");
281
- }
302
+ async getEnvironmentSecret(environmentId, name) {
303
+ return this.store.tx(async (t) => {
304
+ const vault = await this.environmentVault(t, environmentId);
305
+ const row = await t.credentialByName(vault.id, name);
306
+ if (!row || !isUserCredential(row))
307
+ throw new VaultError(404, "Secret not found");
308
+ return secretViewOf(row);
309
+ });
282
310
  }
283
- /** Writes the `attach` audit row. Run in the session PUT transaction. */
284
- async recordAttachment(t, sessionId, vaultIds) {
285
- await this.audit(t, {
286
- actor: "application",
287
- action: "attach",
288
- sessionId,
289
- target: vaultIds.join(","),
290
- outcome: "attached",
311
+ async deleteEnvironmentSecret(environmentId, name, actor) {
312
+ return this.store.tx(async (t) => {
313
+ const vault = await this.environmentVault(t, environmentId, { lock: true });
314
+ const row = await t.credentialByName(vault.id, name);
315
+ if (!row || !isUserCredential(row))
316
+ throw new VaultError(404, "Secret not found");
317
+ await t.deleteCredential(vault.id, row.id);
318
+ await this.audit(t, { actor, action: "delete", vaultId: vault.id, credentialId: row.id, outcome: "deleted" });
319
+ const now = new Date().toISOString();
320
+ await t.updateEnvironment(environmentId, { lastUsedAt: now }, now);
321
+ return { id: name };
291
322
  });
292
323
  }
293
324
  /**
294
325
  * The variables a session's sandbox commands get (R2c, D50): each `environment_secret`'s name
295
- * set to the sentinel, each `environment_variable`'s value, and each host a secret is bound to
296
- * with that secret's credential id. `conflict` says why they are ambiguous: two credentials with one variable name, or two
297
- * secrets bound to one host. Reads rows only; no plaintext. Run in the caller's transaction.
326
+ * set to the sentinel, and each `environment_variable`'s value, from the Environment's vault.
327
+ * Reads rows only; no plaintext. Run in the caller's transaction.
298
328
  */
299
- async sessionEnvironment(t, vaultIds) {
329
+ async environmentVariables(t, environmentId) {
330
+ if (environmentId === undefined)
331
+ return {};
332
+ const vault = await t.vaultOfEnvironment(environmentId);
333
+ if (!vault)
334
+ return {};
300
335
  const environment = {};
301
- const hosts = new Map();
302
- let conflict;
303
- for (const vaultId of vaultIds)
304
- for (const row of await t.credentialsForVault(vaultId)) {
305
- if (row.type !== "environment_secret" && row.type !== "environment_variable")
306
- continue;
307
- const binding = JSON.parse(row.bindingJson);
308
- const name = row.type === "environment_secret" ? binding.secretName : binding.variableName;
309
- if (name in environment)
310
- conflict ??= `Two attached credentials set the variable ${name}`;
311
- environment[name] = row.type === "environment_secret" ? ENVIRONMENT_SECRET_SENTINEL : binding.variableValue;
312
- for (const host of row.type === "environment_secret" ? binding.allowedHosts ?? [] : []) {
313
- if (hosts.has(host) && hosts.get(host) !== row.id)
314
- conflict ??= `Two attached environment_secret credentials are bound to ${host}`;
315
- hosts.set(host, row.id);
316
- }
317
- }
318
- return { environment, secretHosts: Object.fromEntries([...hosts].sort()), ...(conflict ? { conflict } : {}) };
336
+ for (const row of await t.credentialsForVault(vault.id)) {
337
+ if (row.type !== "environment_secret" && row.type !== "environment_variable")
338
+ continue;
339
+ const binding = JSON.parse(row.bindingJson);
340
+ if (row.type === "environment_secret")
341
+ environment[binding.secretName] = ENVIRONMENT_SECRET_SENTINEL;
342
+ else
343
+ environment[binding.variableName] = binding.variableValue;
344
+ }
345
+ return environment;
319
346
  }
320
347
  /**
321
- * The header egress-gate sets on a request from `sandboxId` to `host` (R2c, D50): the
322
- * `environment_secret` bound to `host` among the vaults of the sessions using the sandbox, its
323
- * value unsealed and rendered into its format. `none` when no secret is bound to the host;
324
- * `refused` when two are, or the sealed value cannot be read. One query and one unseal per
325
- * call; nothing is cached (E3). Plaintext exists only in the returned header.
348
+ * The header egress-gate sets on a request from Environment `environmentId`'s sandbox to
349
+ * `host` (R2c, D50): the `environment_secret` of its vault bound to `host`, its value unsealed
350
+ * and rendered into its format. `none` when no secret is bound to the host; `refused` when the
351
+ * sealed value cannot be read. One query and one unseal per call; nothing is cached (E3).
352
+ * Plaintext exists only in the returned header.
326
353
  */
327
354
  async releaseEnvironmentSecret(input) {
328
- const found = await this.store.tx((t) => sandboxSecretsFor(t, input.sandboxId, input.host));
355
+ const found = await this.store.tx((t) => environmentSecretsFor(t, input.environmentId, input.host));
329
356
  if (found.length === 0)
330
357
  return { status: "none" };
331
358
  if (found.length > 1)
@@ -345,21 +372,23 @@ export class VaultService {
345
372
  const inject = credentialBinding(row).inject ?? ENVIRONMENT_SECRET_DEFAULT_INJECT;
346
373
  return { status: "released", header: inject.header, value: renderInjectFormat(inject.format, payload.value), credentialId: row.id };
347
374
  }
348
- /** Whether any `environment_secret` of the sessions using `sandboxId` is bound to `host`. */
349
- async environmentSecretBound(sandboxId, host) {
350
- return (await this.store.tx((t) => sandboxSecretsFor(t, sandboxId, host))).length > 0;
375
+ /** Whether an `environment_secret` of Environment `environmentId` is bound to `host`. */
376
+ async environmentSecretBound(environmentId, host) {
377
+ return (await this.store.tx((t) => environmentSecretsFor(t, environmentId, host))).length > 0;
351
378
  }
352
379
  // --- use ---------------------------------------------------------------------
380
+ /**
381
+ * The headers a request to `url` carries: the credential of the Environment's vault bound to
382
+ * it, or none (`unauthenticated`) without an Environment or such a credential.
383
+ */
353
384
  async authorize(input) {
354
385
  const url = normalizeVaultUrl(input.url);
386
+ const environmentId = input.environmentId;
387
+ if (environmentId === undefined)
388
+ return { status: "unauthenticated", url, headers: {} };
355
389
  const decided = await this.store.tx(async (t) => {
356
- const scopes = new Map();
357
- for (const id of input.vaultIds) {
358
- const found = await t.getVault(id);
359
- if (found) {
360
- scopes.set(id, found.scope);
361
- continue;
362
- }
390
+ const vault = await t.vaultOfEnvironment(environmentId);
391
+ if (!vault) {
363
392
  await this.audit(t, {
364
393
  actor: "host",
365
394
  action: "use",
@@ -367,48 +396,29 @@ export class VaultService {
367
396
  target: url,
368
397
  outcome: "refused",
369
398
  });
370
- return {
371
- result: {
372
- status: "refused",
373
- url,
374
- credentialIds: [],
375
- reason: "vault_missing",
376
- },
377
- };
399
+ return { result: { status: "refused", url, credentialIds: [], reason: "vault_missing" } };
378
400
  }
379
- const matches = [];
380
- for (const vaultId of input.vaultIds)
381
- for (const row of await t.credentialsForVault(vaultId))
382
- if (bindingUrl(row) === url)
383
- matches.push(row);
384
- matches.sort((a, b) => a.id.localeCompare(b.id));
401
+ const matches = (await t.credentialsForVault(vault.id))
402
+ .filter(isUserCredential)
403
+ .filter((row) => bindingUrl(row) === url)
404
+ .sort((a, b) => a.id.localeCompare(b.id));
385
405
  if (matches.length === 0)
386
406
  return { result: { status: "unauthenticated", url, headers: {} } };
387
- const selection = input.serverName
388
- ? input.credentialSelections.find((item) => item.serverName === input.serverName)
389
- : undefined;
390
- const chosen = choose(matches, selection);
391
- if (chosen.kind === "refused") {
407
+ if (matches.length > 1) {
408
+ // Refused at write (D56); kept as a guard.
392
409
  await this.audit(t, {
393
410
  actor: "host",
394
411
  action: "use",
412
+ vaultId: vault.id,
395
413
  sessionId: input.sessionId,
396
414
  target: url,
397
415
  outcome: "refused",
398
416
  });
399
417
  return {
400
- result: {
401
- status: "refused",
402
- url,
403
- credentialIds: chosen.credentialIds,
404
- reason: chosen.reason,
405
- },
418
+ result: { status: "refused", url, credentialIds: matches.map((row) => row.id), reason: "ambiguous" },
406
419
  };
407
420
  }
408
- return {
409
- row: chosen.row,
410
- vault: scopes.get(chosen.row.vaultId) === "installation" ? "installation" : "user",
411
- };
421
+ return { row: matches[0] };
412
422
  });
413
423
  if ("result" in decided)
414
424
  return decided.result;
@@ -431,22 +441,11 @@ export class VaultService {
431
441
  target: url,
432
442
  outcome: "refused",
433
443
  }));
434
- return {
435
- status: "refused",
436
- url,
437
- credentialIds: [row.id],
438
- reason: "unreadable",
439
- };
444
+ return { status: "refused", url, credentialIds: [row.id], reason: "unreadable" };
440
445
  }
441
446
  const headers = sendHeaders(row, payload);
442
- if (!headers) {
443
- return {
444
- status: "refused",
445
- url,
446
- credentialIds: [row.id],
447
- reason: "unreadable",
448
- };
449
- }
447
+ if (!headers)
448
+ return { status: "refused", url, credentialIds: [row.id], reason: "unreadable" };
450
449
  await this.store.tx((t) => this.audit(t, {
451
450
  actor: "host",
452
451
  action: "use",
@@ -463,9 +462,45 @@ export class VaultService {
463
462
  headers,
464
463
  ...(binding.via === undefined ? {} : { via: binding.via }),
465
464
  ...(binding.identity === undefined ? {} : { identity: binding.identity }),
466
- vault: decided.vault,
467
465
  };
468
466
  }
467
+ // --- coverage --------------------------------------------------------------------
468
+ /**
469
+ * What a session that names `request.environmentId` would send for each URL `manifest` names
470
+ * (`credentialNeeds`), decided as `authorize` decides a call: the credential of the
471
+ * Environment's vault bound to the URL. Reads no secret, writes no audit row.
472
+ */
473
+ async coverage(manifest, request) {
474
+ const environmentId = request.environmentId;
475
+ return this.store.tx(async (t) => {
476
+ const credentials = [];
477
+ if (environmentId !== undefined) {
478
+ const vault = await this.environmentVault(t, environmentId);
479
+ for (const row of await t.credentialsForVault(vault.id))
480
+ if (isUserCredential(row))
481
+ credentials.push({ row, url: bindingUrl(row) });
482
+ }
483
+ const entries = credentialNeeds(manifest).map((need) => {
484
+ let url;
485
+ try {
486
+ url = normalizeVaultUrl(need.url);
487
+ }
488
+ catch (error) {
489
+ if (!(error instanceof VaultError))
490
+ throw error;
491
+ return coverageEntry(need, need.url, undefined, environmentId, error.message);
492
+ }
493
+ const bound = credentials.find((item) => item.url === url);
494
+ return coverageEntry(need, url, bound && coverageCredential(environmentId, bound.row), environmentId);
495
+ });
496
+ return {
497
+ agentId: request.agentId,
498
+ environmentId: environmentId ?? null,
499
+ complete: entries.every((entry) => entry.status === "covered"),
500
+ entries,
501
+ };
502
+ });
503
+ }
469
504
  // --- host model ----------------------------------------------------------------
470
505
  async getHostModel() {
471
506
  return this.store.tx((t) => this.hostModelView(t));
@@ -473,9 +508,11 @@ export class VaultService {
473
508
  async listHostProviders() {
474
509
  return this.store.tx(async (t) => {
475
510
  const active = await activeProviderId(t);
511
+ const added = await readAddedInputs(t);
476
512
  const catalog = new Map(hostModelCatalog().providers.map((provider) => [provider.id, provider.name]));
477
513
  const providers = (await hostModelRows(t)).map((row) => {
478
514
  const binding = modelBinding(row);
515
+ const inputs = inputsOfHostModel({ configured: true, ...binding }, added);
479
516
  return {
480
517
  id: binding.provider,
481
518
  name: catalog.get(binding.provider) ??
@@ -486,6 +523,7 @@ export class VaultService {
486
523
  authType: binding.authType,
487
524
  ...(binding.baseUrl ? { baseUrl: binding.baseUrl } : {}),
488
525
  ...(binding.settings ? { settings: binding.settings } : {}),
526
+ ...(inputs ? { inputs } : {}),
489
527
  lastUpdated: row.rotatedAt ?? row.createdAt,
490
528
  active: binding.provider === active,
491
529
  };
@@ -586,31 +624,39 @@ export class VaultService {
586
624
  throw new VaultError(404, "Credential not found");
587
625
  return { ...row, ...patch };
588
626
  }
589
- async vaultInfo(t, id) {
590
- const row = await t.getVault(id);
591
- if (!row || row.scope === "host")
592
- throw new VaultError(404, "Vault not found");
593
- return vaultInfoOf(row);
627
+ /**
628
+ * The vault of a live Environment (not being deleted), or the 404 of a missing one. With
629
+ * `lock`, the Environment's row is locked until the transaction ends.
630
+ */
631
+ async environmentVault(t, environmentId, options = {}) {
632
+ const environment = await t.environment(environmentId, options);
633
+ const vault = environment && environment.pod?.desired !== "deleted"
634
+ ? await t.vaultOfEnvironment(environmentId)
635
+ : undefined;
636
+ if (!vault)
637
+ throw new VaultError(404, "Environment not found");
638
+ return vault;
594
639
  }
595
640
  async ensureHostVault(t) {
596
- const existing = await t.getVault(HOST_VAULT_ID);
597
- if (existing?.scope === "host")
641
+ if (await t.getVault(HOST_VAULT_ID))
598
642
  return;
599
- if (existing)
600
- throw new VaultError(409, "Host vault id is already used");
601
643
  await t.insertVault({
602
644
  id: HOST_VAULT_ID,
603
645
  name: "Host",
604
- ownerUserId: "host",
605
646
  metadataJson: null,
606
647
  createdAt: new Date().toISOString(),
607
- scope: "host",
648
+ environmentId: null,
608
649
  });
609
650
  }
610
651
  validateHostModel(body) {
611
652
  if (body.settings && body.provider !== "custom")
612
- throw new VaultError(400, "Model settings (context window, output limit, reasoning, compat) apply only to a custom provider.");
653
+ throw new VaultError(400, "Model settings (context window, output limit, reasoning, inputs, compat) apply only to a custom provider.");
613
654
  if (body.provider === "custom") {
655
+ // A custom endpoint is OpenAI-compatible: it reads only what that API carries (protocol 12).
656
+ const inputsRefused = body.settings?.inputs &&
657
+ inputsError({ api: "openai-completions", input: ["text", "image"] }, body.settings.inputs);
658
+ if (inputsRefused)
659
+ throw new VaultError(400, inputsRefused);
614
660
  if (!body.baseUrl)
615
661
  throw new VaultError(400, "A base URL is required for a custom provider.");
616
662
  let url;
@@ -680,12 +726,6 @@ export class VaultService {
680
726
  for (const id of new Set(ids))
681
727
  await t.deleteCredential(HOST_VAULT_ID, id);
682
728
  }
683
- async credentialRow(t, vaultId, id) {
684
- const row = await t.getCredential(id);
685
- if (!row || row.vaultId !== vaultId || !isUserCredential(row))
686
- throw new VaultError(404, "Credential not found");
687
- return row;
688
- }
689
729
  async audit(t, entry) {
690
730
  await t.insertVaultAudit({
691
731
  id: randomUUID(),
@@ -706,35 +746,80 @@ function isUserCredential(row) {
706
746
  function bindingUrl(row) {
707
747
  return JSON.parse(row.bindingJson).url;
708
748
  }
709
- function vaultInfoOf(row) {
749
+ /** Where a credential is, for coverage: never its value. */
750
+ function coverageCredential(environmentId, row) {
751
+ const via = JSON.parse(row.bindingJson).via;
752
+ return { environmentId, name: row.name, ...(via === undefined ? {} : { via }) };
753
+ }
754
+ /** One coverage entry, with the sentence that says what the session would do. */
755
+ function coverageEntry(need, url, credential, environmentId, invalid) {
756
+ const required = need.kind === "http";
757
+ const where = environmentId === undefined
758
+ ? "Without an Environment, no credential is sent"
759
+ : `Environment ${environmentId} holds no credential for this URL`;
760
+ const message = invalid !== undefined
761
+ ? `No credential can be bound to this URL (${invalid}).`
762
+ : credential !== undefined
763
+ ? `Sends '${credential.name}' from Environment ${credential.environmentId}${credential.via ? ` through ${credential.via}` : ""}.`
764
+ : required
765
+ ? `${where}: each call fails with http.credential.`
766
+ : `${where}: the server is called without one, which only a public server accepts.`;
710
767
  return {
711
- id: row.id,
712
- name: row.name,
713
- ownerUserId: row.ownerUserId,
714
- ...(row.metadataJson
715
- ? { metadata: JSON.parse(row.metadataJson) }
716
- : {}),
717
- createdAt: row.createdAt,
768
+ kind: need.kind,
769
+ ...(need.agentId === undefined ? {} : { agentId: need.agentId }),
770
+ ...(need.stage === undefined ? {} : { stage: need.stage }),
771
+ name: need.name,
772
+ serverName: need.serverName,
773
+ url,
774
+ required,
775
+ status: credential === undefined ? "missing" : "covered",
776
+ ...(credential === undefined ? {} : { credential }),
777
+ message,
718
778
  };
719
779
  }
780
+ /**
781
+ * Refuses a write that would bind a second credential to a host or a URL (D56): `others` are
782
+ * the vault's other credentials.
783
+ */
784
+ function conflictOf(type, binding, others) {
785
+ if (type === "environment_secret") {
786
+ for (const row of others) {
787
+ if (row.type !== "environment_secret")
788
+ continue;
789
+ const hosts = credentialBinding(row).allowedHosts ?? [];
790
+ const host = (binding.allowedHosts ?? []).find((item) => hosts.includes(item));
791
+ if (host !== undefined)
792
+ throw new VaultError(409, `${row.name} is already bound to ${host}`, {
793
+ code: "credential_conflict",
794
+ details: { host, existing: row.name },
795
+ });
796
+ }
797
+ }
798
+ else if (type === "bearer" || type === "headers") {
799
+ const existing = others.find((row) => (row.type === "bearer" || row.type === "headers") && credentialBinding(row).url === binding.url);
800
+ if (existing)
801
+ throw new VaultError(409, `${existing.name} is already bound to ${binding.url}`, {
802
+ code: "credential_conflict",
803
+ details: { url: binding.url, existing: existing.name },
804
+ });
805
+ }
806
+ }
720
807
  function credentialBinding(row) {
721
808
  return JSON.parse(row.bindingJson);
722
809
  }
723
- function credentialInfoOf(row) {
810
+ /** A secret as `GET .../secrets/{name}` shows it: never its value. */
811
+ function secretViewOf(row) {
724
812
  const binding = credentialBinding(row);
725
813
  return {
726
- id: row.id,
727
- vaultId: row.vaultId,
728
814
  name: row.name,
729
815
  type: row.type,
730
816
  binding: row.type === "environment_secret"
731
817
  ? {
732
- secretName: binding.secretName,
733
818
  allowedHosts: binding.allowedHosts,
734
819
  inject: binding.inject ?? { ...ENVIRONMENT_SECRET_DEFAULT_INJECT },
735
820
  }
736
821
  : row.type === "environment_variable"
737
- ? { variableName: binding.variableName, variableValue: binding.variableValue }
822
+ ? {}
738
823
  : { url: binding.url },
739
824
  ...(binding.headerNames === undefined ? {} : { headerNames: binding.headerNames }),
740
825
  ...(binding.via === undefined ? {} : { via: binding.via }),
@@ -819,33 +904,10 @@ export function readHostPayload(kek, row, binding) {
819
904
  plaintext.fill(0);
820
905
  }
821
906
  }
822
- function choose(matches, selection) {
823
- const ids = matches.map((row) => row.id);
824
- if (matches.length === 1) {
825
- const only = matches[0];
826
- if (selection && selection.credentialId !== only.id)
827
- return {
828
- kind: "refused",
829
- credentialIds: ids,
830
- reason: "selection_mismatch",
831
- };
832
- return { kind: "use", row: only };
833
- }
834
- if (!selection)
835
- return { kind: "refused", credentialIds: ids, reason: "ambiguous" };
836
- const chosen = matches.find((row) => row.id === selection.credentialId);
837
- if (!chosen)
838
- return {
839
- kind: "refused",
840
- credentialIds: ids,
841
- reason: "selection_mismatch",
842
- };
843
- return { kind: "use", row: chosen };
844
- }
845
- /** The sealed part of a create or rotate: a token, or a header map with lower-cased names. */
907
+ /** The sealed part of a tool credential: a token, or a header map with lower-cased names. */
846
908
  function payloadOf(auth) {
847
909
  if (auth.type === "bearer")
848
- return { token: auth.token };
910
+ return { token: auth.value };
849
911
  return {
850
912
  headers: Object.fromEntries(Object.entries(auth.headers).map(([name, value]) => [name.toLowerCase(), value])),
851
913
  };
@@ -907,18 +969,51 @@ function credentialAad(vaultId, credentialId, type, binding) {
907
969
  }
908
970
  : { vaultId, credentialId, type, url: binding.url }), "utf8");
909
971
  }
910
- /** The sealed payload and unsealed binding of an environment credential (R2c). */
911
- function environmentCredential(auth) {
912
- if (auth.type === "environment_variable")
913
- return { payload: {}, binding: { variableName: auth.variableName, variableValue: auth.variableValue } };
914
- if (auth.inject)
915
- checkCredentialHeaders([auth.inject.header.toLowerCase()], undefined);
972
+ /**
973
+ * A copied credential's type, binding and payload under a slot of the fork (D64): its value with
974
+ * the slot's binding, or undefined when the slot's type cannot take it.
975
+ */
976
+ function rebind(name, slot, row, payload, binding) {
977
+ const type = slot.type;
978
+ const value = row.type === "environment_variable" ? binding.variableValue : row.type === "bearer" ? payload.token : payload.value;
979
+ try {
980
+ if (type === "environment_secret" || type === "environment_variable") {
981
+ if (value === undefined)
982
+ return undefined;
983
+ const made = environmentCredential(name, { ...slot, type, value });
984
+ return { type, ...made };
985
+ }
986
+ const auth = type === "headers"
987
+ ? payload.headers === undefined
988
+ ? undefined
989
+ : { type, headers: payload.headers }
990
+ : value === undefined
991
+ ? undefined
992
+ : { type: "bearer", value };
993
+ if (auth === undefined)
994
+ return undefined;
995
+ const sealed = payloadOf(auth);
996
+ const routing = slot;
997
+ return { type, payload: sealed, binding: bindingOf(normalizeVaultUrl(routing.url), sealed, routing) };
998
+ }
999
+ catch (error) {
1000
+ if (error instanceof VaultError)
1001
+ return undefined;
1002
+ throw error;
1003
+ }
1004
+ }
1005
+ /** The sealed payload and unsealed binding of a shell credential named `name` (R2c). */
1006
+ function environmentCredential(name, body) {
1007
+ if (body.type === "environment_variable")
1008
+ return { payload: {}, binding: { variableName: name, variableValue: body.value } };
1009
+ if (body.inject)
1010
+ checkCredentialHeaders([body.inject.header.toLowerCase()], undefined);
916
1011
  return {
917
- payload: { value: auth.secretValue },
1012
+ payload: { value: body.value },
918
1013
  binding: {
919
- secretName: auth.secretName,
920
- allowedHosts: auth.allowedHosts.map((host) => host.toLowerCase()),
921
- ...(auth.inject ? { inject: { header: auth.inject.header, format: auth.inject.format } } : {}),
1014
+ secretName: name,
1015
+ allowedHosts: body.allowedHosts.map((host) => host.toLowerCase()),
1016
+ ...(body.inject ? { inject: { header: body.inject.header, format: body.inject.format } } : {}),
922
1017
  },
923
1018
  };
924
1019
  }
@@ -926,21 +1021,12 @@ function requestHash(body) {
926
1021
  const { requestId: _requestId, ...rest } = body;
927
1022
  return canonical(rest);
928
1023
  }
929
- /**
930
- * The `environment_secret` credentials bound to `host` in the vaults of every session on the
931
- * sandbox (R2c). A session between turns counts: it can run again, and what it started may
932
- * still run. Session open keeps them to one owner and one credential per host.
933
- */
934
- async function sandboxSecretsFor(t, sandboxId, host) {
1024
+ /** The `environment_secret` credentials of Environment `environmentId` bound to `host` (R2c). */
1025
+ async function environmentSecretsFor(t, environmentId, host) {
935
1026
  const lower = host.toLowerCase();
936
- const vaultIds = new Set();
937
- for (const session of await t.sessionsOnSandbox(sandboxId))
938
- for (const vaultId of session.vaultIds ?? [])
939
- vaultIds.add(vaultId);
940
- const matches = [];
941
- for (const vaultId of vaultIds)
942
- for (const row of await t.credentialsForVault(vaultId))
943
- if (row.type === "environment_secret" && (credentialBinding(row).allowedHosts ?? []).includes(lower))
944
- matches.push(row);
945
- return matches;
1027
+ const vault = await t.vaultOfEnvironment(environmentId);
1028
+ if (!vault)
1029
+ return [];
1030
+ return (await t.credentialsForVault(vault.id)).filter((row) => row.type === "environment_secret" &&
1031
+ (credentialBinding(row).allowedHosts ?? []).includes(lower));
946
1032
  }