@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
@@ -0,0 +1,150 @@
1
+ import { z } from "zod";
2
+ import { isSandboxId, PutSandboxRequestSchema } from "@nylorun/core/contracts";
3
+ import { DeleteSandboxResponse, ListSandboxEventsResponse, ListSandboxesResponse, PutSandboxRequest, SandboxView, } from "../../components.js";
4
+ import { fail } from "../../../tenant/http.js";
5
+ import { deleteSandbox, getSandbox, listSandboxes, putSandbox, resetSandbox, sandboxEventsOf, stopSandbox, } from "../../../tenant/sandboxes.js";
6
+ import { readJson } from "../body.js";
7
+ import { tenantRoute } from "../define.js";
8
+ import { jsonResponse } from "../respond.js";
9
+ /** Reading a sandbox: any caller that may use sessions, or change sandboxes. */
10
+ const READ = {
11
+ credentials: ["application", "subject", "token"],
12
+ scopes: ["sessions:own", "sandboxes:write"],
13
+ };
14
+ /** Creating or deleting one. */
15
+ const WRITE = {
16
+ credentials: ["application", "subject", "token"],
17
+ scopes: ["sandboxes:write"],
18
+ };
19
+ const json = (schema, description) => ({
20
+ description,
21
+ content: { "application/json": { schema } },
22
+ });
23
+ const sandboxId = z.object({
24
+ sandboxId: z
25
+ .string()
26
+ .meta({ description: "The sandbox id, percent-encoded: `team-a%2Fproj-42` for `team-a/proj-42`" }),
27
+ });
28
+ /** The path's sandbox id, or a 400 naming the rule. */
29
+ function idOf(value) {
30
+ if (!isSandboxId(value))
31
+ return fail(400, "A sandbox id is up to 200 characters: /-separated segments of letters, digits, '.', '_' and '-'");
32
+ return value;
33
+ }
34
+ /** `label=k=v`, repeated: every one must match. */
35
+ function labelsOf(values) {
36
+ const labels = {};
37
+ for (const value of values ?? []) {
38
+ const at = value.indexOf("=");
39
+ if (at <= 0)
40
+ fail(400, `label must be key=value, not ${value}`);
41
+ labels[value.slice(0, at)] = value.slice(at + 1);
42
+ }
43
+ return labels;
44
+ }
45
+ export function sandboxRoutes(api) {
46
+ tenantRoute(api, READ, {
47
+ method: "get",
48
+ path: "/v1/sandboxes",
49
+ tags: ["Sandboxes"],
50
+ summary: "List sandboxes",
51
+ description: "Every sandbox with all the labels asked for. A token caller sees only the sandboxes its grants reach.",
52
+ request: {
53
+ query: z.object({
54
+ label: z
55
+ .union([z.string(), z.array(z.string())])
56
+ .optional()
57
+ .meta({ description: "`key=value`; repeat it to require several labels" }),
58
+ }),
59
+ },
60
+ responses: { 200: json(ListSandboxesResponse, "The sandboxes") },
61
+ }, async (c) => jsonResponse(200, await listSandboxes(c.env.tenant, labelsOf(c.req.queries("label")), c.get("scope"))));
62
+ tenantRoute(api, WRITE, {
63
+ method: "put",
64
+ path: "/v1/sandboxes/{sandboxId}",
65
+ tags: ["Sandboxes"],
66
+ summary: "Create or find a sandbox",
67
+ description: "Creates the sandbox within the Tenant's limits, or answers the one with this id, so get-or-create is one call. Its spec is fixed once it exists; labels, when sent, replace its labels. Kind `pod` needs sandbox pods (`nylorun sandbox enable`): it is created at once, and a PUT on an existing pod sandbox starts it again when it was stopped, or revives it with a longer `lifecycle.ttl`.",
68
+ request: {
69
+ params: sandboxId,
70
+ body: { required: true, content: { "application/json": { schema: PutSandboxRequest } } },
71
+ },
72
+ responses: {
73
+ 200: json(SandboxView, "The sandbox"),
74
+ 409: {
75
+ description: "A sandbox with this id has another spec, the Tenant holds as many sandboxes as it allows (`limit_exceeded`), kind pod without sandbox pods (`sandbox_unavailable`), or a lost pod sandbox (`sandbox_lost`)",
76
+ },
77
+ },
78
+ }, async (c) => {
79
+ const id = idOf(c.req.param("sandboxId"));
80
+ const body = PutSandboxRequestSchema.parse(await readJson(c.req.raw));
81
+ return jsonResponse(200, await putSandbox(c.env.tenant, id, body, c.get("scope")));
82
+ });
83
+ tenantRoute(api, READ, {
84
+ method: "get",
85
+ path: "/v1/sandboxes/{sandboxId}",
86
+ tags: ["Sandboxes"],
87
+ summary: "Get a sandbox",
88
+ description: "Its spec, labels, state and the sessions attached to it (acting for a person, only theirs).",
89
+ request: { params: sandboxId },
90
+ responses: { 200: json(SandboxView, "The sandbox") },
91
+ }, async (c) => jsonResponse(200, await getSandbox(c.env.tenant, idOf(c.req.param("sandboxId")), c.get("scope"))));
92
+ tenantRoute(api, READ, {
93
+ method: "get",
94
+ path: "/v1/sandboxes/{sandboxId}/events",
95
+ tags: ["Sandboxes"],
96
+ summary: "Read a sandbox's lifecycle events",
97
+ description: "`sandbox.created`, `sandbox.attached`, `sandbox.detached` and `sandbox.deleted`, in order, numbered from 0.",
98
+ request: {
99
+ params: sandboxId,
100
+ query: z.object({
101
+ from: z.string().optional().meta({ description: "The first `seq` to return" }),
102
+ }),
103
+ },
104
+ responses: { 200: json(ListSandboxEventsResponse, "The events") },
105
+ }, async (c) => {
106
+ const from = c.req.query("from");
107
+ if (from !== undefined && !/^\d+$/.test(from))
108
+ fail(400, "from must be a seq");
109
+ return jsonResponse(200, await sandboxEventsOf(c.env.tenant, idOf(c.req.param("sandboxId")), c.get("scope"), from === undefined ? undefined : Number(from)));
110
+ });
111
+ tenantRoute(api, WRITE, {
112
+ method: "delete",
113
+ path: "/v1/sandboxes/{sandboxId}",
114
+ tags: ["Sandboxes"],
115
+ summary: "Delete a sandbox",
116
+ description: "Deletes the sandbox and its files. Sessions attached to it stay attached by id; their next turn is refused until a sandbox with this id exists again.",
117
+ request: { params: sandboxId },
118
+ responses: {
119
+ 200: json(DeleteSandboxResponse, "Deleted, or there was none"),
120
+ 409: { description: "A session attached to it has a turn running (`sandbox_busy`)" },
121
+ },
122
+ }, async (c) => jsonResponse(200, await deleteSandbox(c.env.tenant, idOf(c.req.param("sandboxId")), c.get("scope"))));
123
+ for (const [action, summary, description, run] of [
124
+ [
125
+ "stop",
126
+ "Stop a pod sandbox",
127
+ "Suspends the pod: its volume is kept, and the next turn of a session attached to it (or a PUT) starts it again. Refused during a turn (`sandbox_busy`).",
128
+ stopSandbox,
129
+ ],
130
+ [
131
+ "reset",
132
+ "Reset a pod sandbox",
133
+ "A new pod on a new, empty volume; the old pod and volume are deleted. The way out of `sandbox_lost`. Refused during a turn (`sandbox_busy`).",
134
+ resetSandbox,
135
+ ],
136
+ ])
137
+ tenantRoute(api, WRITE, {
138
+ method: "post",
139
+ path: `/v1/sandboxes/{sandboxId}/${action}`,
140
+ tags: ["Sandboxes"],
141
+ summary,
142
+ description,
143
+ request: { params: sandboxId },
144
+ responses: {
145
+ 200: json(SandboxView, "The sandbox"),
146
+ 400: { description: "The sandbox is virtual" },
147
+ 409: { description: "A turn holds it (`sandbox_busy`), or no sandbox pods (`sandbox_unavailable`)" },
148
+ },
149
+ }, async (c) => jsonResponse(200, await run(c.env.tenant, idOf(c.req.param("sandboxId")), c.get("scope"))));
150
+ }
@@ -7,7 +7,8 @@ import { sandboxRouteDeps } from "../../../tenant/actions.js";
7
7
  import { accessOf, requireApplication } from "../../../tenant/auth.js";
8
8
  import { loadSession, sessionOf } from "../../../tenant/context.js";
9
9
  import { fail } from "../../../tenant/http.js";
10
- import { readHistory, requestCursor, streamSessionEvents } from "../../../tenant/session-streams.js";
10
+ import { sandboxGrantsOf } from "../../../tenant/sandboxes.js";
11
+ import { readHistory, requestCursor, streamHolderOf, streamSessionEvents } from "../../../tenant/session-streams.js";
11
12
  import { listAgentsPublic, listDefinitions, listSessions, putDefinition, putSession, sessionView, } from "../../../tenant/sessions.js";
12
13
  import { readJson } from "../body.js";
13
14
  import { tenantRoute } from "../define.js";
@@ -15,7 +16,6 @@ import { jsonResponse } from "../respond.js";
15
16
  const OWN_SESSIONS = {
16
17
  credentials: ["application", "subject", "token"],
17
18
  scopes: ["sessions:own"],
18
- browser: true,
19
19
  };
20
20
  const json = (schema, description) => ({
21
21
  description,
@@ -46,23 +46,20 @@ async function sessionBelow(ctx, incoming, id, access) {
46
46
  }
47
47
  export function sessionRoutes(api) {
48
48
  tenantRoute(api, {
49
- credentials: ["application", "subject", "token", "publishable"],
49
+ credentials: ["application", "subject", "token"],
50
50
  scopes: ["agents:read", "agents:write"],
51
- browser: true,
52
51
  }, {
53
52
  method: "get",
54
53
  path: "/v1/agents",
55
54
  tags: ["Agents"],
56
55
  summary: "List agents",
57
- description: "With an application key, every definition, manifest included. With a subject token or a publishable key, only the agents it may use, by name.",
56
+ description: "With an application key, every definition, manifest included. With a trusted issuer's token, only the agents it may use, by name.",
58
57
  responses: {
59
58
  200: json(z.union([ListAgentsResponse, ListPublicAgentsResponse]), "The agents"),
60
59
  },
61
60
  }, async (c) => {
62
61
  const scope = c.get("scope");
63
62
  const ctx = c.env.tenant;
64
- if (scope.kind === "publishable")
65
- return jsonResponse(200, await listAgentsPublic(ctx, scope.agents));
66
63
  return jsonResponse(200, scope.kind === "token"
67
64
  ? await listAgentsPublic(ctx, scope.agents)
68
65
  : await listDefinitions(ctx));
@@ -106,8 +103,9 @@ export function sessionRoutes(api) {
106
103
  const request = PutSessionRequestSchema.parse(await readJson(c.req.raw));
107
104
  // Agent code may trust `info`: only an app server sets it.
108
105
  if (scope.kind === "token" && request.info !== undefined)
109
- fail(403, "A subject token cannot set session info", { code: "scope_required" });
110
- const session = await putSession(ctx, c.req.param("sessionId"), request, accessOf(scope));
106
+ fail(403, "A token caller cannot set session info", { code: "scope_required" });
107
+ const grants = sandboxGrantsOf(scope);
108
+ const session = await putSession(ctx, c.req.param("sessionId"), request, accessOf(scope), grants === undefined ? {} : { sandboxGrants: grants });
111
109
  return jsonResponse(200, await ctx.store.tx((t) => sessionView(t, session)));
112
110
  });
113
111
  tenantRoute(api, OWN_SESSIONS, {
@@ -150,7 +148,7 @@ export function sessionRoutes(api) {
150
148
  path: "/v1/sessions/{sessionId}/events",
151
149
  tags: ["Sessions"],
152
150
  summary: "Follow a session's events",
153
- description: "Server-sent events, each with its type as `event` and its cursor as `id`, from the cursor on. Events are on the `nylorun.event/2` envelope, typed by `SessionEvent`; a client ignores a type it does not know. A subject token's stream ends with `event: nylorun.closed` when the token expires or the subject is revoked. A `: keepalive` comment every 15 seconds.",
151
+ description: "Server-sent events, each with its type as `event` and its cursor as `id`, from the cursor on. Events are on the `nylorun.event/2` envelope, typed by `SessionEvent`; a client ignores a type it does not know. A trusted issuer's token's stream ends with `event: nylorun.closed` when the token expires. A `: keepalive` comment every 15 seconds.",
154
152
  request: { params: sessionId, query: resume },
155
153
  responses: {
156
154
  200: {
@@ -168,9 +166,7 @@ export function sessionRoutes(api) {
168
166
  const { incoming, outgoing } = c.env;
169
167
  const id = c.req.param("sessionId");
170
168
  const cursor = await sessionBelow(ctx, incoming, id, accessOf(scope));
171
- await streamSessionEvents(ctx, incoming, outgoing, id, cursor, scope.kind === "token"
172
- ? { subject: scope.subject, epoch: scope.epoch, expiresAt: scope.expiresAt }
173
- : undefined);
169
+ await streamSessionEvents(ctx, incoming, outgoing, id, cursor, streamHolderOf(scope));
174
170
  return RESPONSE_ALREADY_SENT;
175
171
  });
176
172
  tenantRoute(api, { credentials: ["application"], scopes: "never" }, {
@@ -10,11 +10,12 @@ import { clearObservers } from "../../../tenant/session-streams.js";
10
10
  import { usesFixtureModel } from "../../../tenant/model-setting.js";
11
11
  import { resetTenant } from "../../../tenant/reset.js";
12
12
  import { clearWork, drain } from "../../../tenant/scheduler.js";
13
- import { buildTenantStatus, seedTenantConfig } from "../../../tenant/status.js";
13
+ import { buildTenantStatus, harnessStatusOf, seedTenantConfig } from "../../../tenant/status.js";
14
14
  import { tenantReset, streamsStatus, } from "../../../tenant/streams.js";
15
15
  import { readJson } from "../body.js";
16
16
  import { pathSegments, tenantRoute } from "../define.js";
17
17
  import { jsonResponse } from "../respond.js";
18
+ import { rearmPodTimers } from "../../../tenant/sandboxes.js";
18
19
  const settings = (scopes) => ({
19
20
  credentials: scopes === "never" ? ["application"] : ["application", "subject"],
20
21
  scopes,
@@ -46,7 +47,36 @@ const budgetView = (row) => ({
46
47
  /** `GET /v1/tenant/sandbox`: the backend report and the configuration with defaults applied. */
47
48
  async function sandboxView(ctx) {
48
49
  const config = await ctx.store.tx((t) => readSandboxConfig(t));
49
- return { ...(await ctx.sandbox.report()), config: effectiveSandboxConfig(config) };
50
+ return {
51
+ ...(await ctx.sandbox.report()),
52
+ config: effectiveSandboxConfig(config),
53
+ cluster: await clusterOf(ctx),
54
+ };
55
+ }
56
+ /** The Tenant's cluster for pod sandboxes (F7.2), from the sandboxes service; null without one. */
57
+ async function clusterOf(ctx) {
58
+ if (!ctx.pods)
59
+ return null;
60
+ try {
61
+ const [info, ready] = await Promise.all([ctx.pods.client.info(), ctx.pods.client.ready()]);
62
+ return {
63
+ namespace: info.namespace,
64
+ context: info.context,
65
+ ready,
66
+ ...(info.controllerVersion ? { controllerVersion: info.controllerVersion } : {}),
67
+ ...(info.networkPolicy
68
+ ? {
69
+ networkPolicy: {
70
+ enforced: info.networkPolicy.enforced === true,
71
+ ...(info.networkPolicy.probedAt ? { probedAt: info.networkPolicy.probedAt } : {}),
72
+ },
73
+ }
74
+ : {}),
75
+ };
76
+ }
77
+ catch {
78
+ return null;
79
+ }
50
80
  }
51
81
  export function tenantRoutes(api) {
52
82
  tenantRoute(api, SETTINGS, {
@@ -70,6 +100,7 @@ export function tenantRoutes(api) {
70
100
  (await vault.getHostModel()).configured,
71
101
  ...(ctx.stuckInvocations ? { stuckInvocations: ctx.stuckInvocations } : {}),
72
102
  streamsStatus: () => streamsStatus(ctx),
103
+ harness: harnessStatusOf(ctx),
73
104
  }));
74
105
  });
75
106
  tenantRoute(api, settings("never"), {
@@ -91,6 +122,7 @@ export function tenantRoutes(api) {
91
122
  store: ctx.store,
92
123
  sandbox: ctx.sandbox,
93
124
  paths: ctx.config.paths,
125
+ blobs: ctx.blobs,
94
126
  clearSessionState: () => {
95
127
  clearWork(ctx);
96
128
  clearObservers(ctx.sessionStreams);
@@ -147,6 +179,8 @@ export function tenantRoutes(api) {
147
179
  if (errors.length > 0)
148
180
  fail(400, errors.join(" "));
149
181
  await ctx.store.tx((t) => writeSandboxConfig(t, config));
182
+ // Pods' idle timers follow the new limits.idle (D36).
183
+ await rearmPodTimers(ctx);
150
184
  return jsonResponse(200, await sandboxView(ctx));
151
185
  });
152
186
  tenantRoute(api, SETTINGS, {
@@ -1,8 +1,17 @@
1
1
  /**
2
- * Vaults and their credentials (`/v1/vaults/**`): secrets a session's tools use, owned by one
3
- * person. Acting for a subject reaches only the subject's own vaults, and never on another's
4
- * behalf.
2
+ * Vaults and their credentials (`/v1/vaults/**`): secrets a session's tools use. Only an
3
+ * application key acting for no one (an operator key, Studio) reaches these routes (protocol 7):
4
+ * a request acting for a subject, or a trusted issuer's token, is `403 scope_required`. The
5
+ * installation's own vaults (`scope: "installation"`) attach to any session; a vault of one
6
+ * person (`ownerUserId`) attaches only to that person's sessions. A person's own credentials
7
+ * come from the operator's credential resolver (`vault/sources.ts`), not from these routes.
8
+ *
9
+ * MCP OAuth connect (F9 C2): an application key starts one into an installation vault
10
+ * (`/oauth/start`); the authorization server sends the browser back to the anonymous, unversioned
11
+ * `GET /v1/oauth/callback`. Both only route: the gateway's keys module does the OAuth (F9-D14).
5
12
  */
6
13
  import type { OpenAPIHono } from "@hono/zod-openapi";
7
14
  import type { TenantEnv } from "../app.js";
15
+ /** Where the authorization server sends the browser back. */
16
+ export declare const OAUTH_CALLBACK_PATH = "/v1/oauth/callback";
8
17
  export declare function vaultRoutes(api: OpenAPIHono<TenantEnv>): void;
@@ -1,16 +1,25 @@
1
1
  import { z } from "zod";
2
- import { CreateCredentialRequestSchema, CreateVaultRequestSchema, RotateCredentialRequestSchema, } from "@nylorun/core/contracts";
3
- import { CreateCredentialRequest, CreateVaultRequest, CredentialInfo, DeletedResponse, ListCredentialsResponse, ListVaultsResponse, RotateCredentialRequest, VaultInfo, } from "../../components.js";
4
- import { ownerOf } from "../../../tenant/auth.js";
5
- import { fail } from "../../../tenant/http.js";
2
+ import { CreateCredentialRequestSchema, CreateVaultRequestSchema, RotateCredentialRequestSchema, StartOAuthRequestSchema, } from "@nylorun/core/contracts";
3
+ import { CreateCredentialRequest, CreateVaultRequest, CredentialInfo, DeletedResponse, ListCredentialsResponse, ListVaultsResponse, RotateCredentialRequest, StartOAuthRequest, StartOAuthResponse, VaultInfo, } from "../../components.js";
4
+ import { HttpError } from "../../../tenant/http.js";
5
+ import { VaultError } from "../../../vault/error.js";
6
6
  import { readJson } from "../body.js";
7
- import { pathSegments, tenantRoute } from "../define.js";
7
+ import { tenantRoute } from "../define.js";
8
8
  import { jsonResponse } from "../respond.js";
9
- const OWN_VAULTS = {
9
+ /** Every vault route, and starting an MCP OAuth connect: an application key acting for no one. */
10
+ const APPLICATION = { credentials: ["application"], scopes: "never" };
11
+ /**
12
+ * The OAuth callback: a browser sent back by the authorization server, with no credential and no
13
+ * protocol header. The `state` it carries is the grant.
14
+ */
15
+ const CALLBACK = {
10
16
  credentials: ["application", "subject", "token"],
11
- scopes: ["vaults:own"],
12
- browser: true,
17
+ scopes: "any",
18
+ anonymous: true,
19
+ unversioned: true,
13
20
  };
21
+ /** Where the authorization server sends the browser back. */
22
+ export const OAUTH_CALLBACK_PATH = "/v1/oauth/callback";
14
23
  const json = (schema, description) => ({
15
24
  description,
16
25
  content: { "application/json": { schema } },
@@ -21,58 +30,45 @@ const body = (schema) => ({
21
30
  });
22
31
  const vaultId = z.object({ vaultId: z.string() });
23
32
  const credentialId = vaultId.extend({ credentialId: z.string() });
24
- /**
25
- * Who a vault route acts for: the subject, or no one in particular (an application key). Below
26
- * a vault, the vault must be the subject's before anything else.
27
- */
28
- async function ownerFor(c) {
29
- const { vault } = c.env.tenant;
30
- const owner = ownerOf(c.get("scope"));
31
- const id = c.req.param("vaultId");
32
- if (id !== undefined && owner !== undefined)
33
- await vault.assertOwner(id, owner);
34
- return owner;
35
- }
36
33
  export function vaultRoutes(api) {
37
- tenantRoute(api, OWN_VAULTS, {
34
+ tenantRoute(api, APPLICATION, {
38
35
  method: "post",
39
36
  path: "/v1/vaults",
40
37
  tags: ["Vaults"],
41
38
  summary: "Create a vault",
39
+ description: "With `scope: \"installation\"` the installation's own vault, owned by `installation`, which any session may attach; or a vault of one person (`ownerUserId`), which only that person's sessions attach.",
42
40
  request: { body: body(CreateVaultRequest) },
43
41
  responses: {
44
42
  200: json(VaultInfo, "The vault"),
45
43
  409: { description: "The idempotency key was used for another vault" },
46
44
  },
47
45
  }, async (c) => {
48
- const owner = await ownerFor(c);
49
46
  const request = CreateVaultRequestSchema.parse(await readJson(c.req.raw));
50
- if (owner !== undefined && request.ownerUserId !== owner)
51
- fail(403, "ownerUserId must be the subject");
52
47
  return jsonResponse(200, await c.env.tenant.vault.createVault(request));
53
48
  });
54
- tenantRoute(api, OWN_VAULTS, {
49
+ tenantRoute(api, APPLICATION, {
55
50
  method: "get",
56
51
  path: "/v1/vaults",
57
52
  tags: ["Vaults"],
58
- summary: "List a person's vaults",
53
+ summary: "List vaults",
54
+ description: "The installation vaults, after the vaults of `ownerUserId` when it names a person.",
59
55
  request: {
60
56
  query: z.object({
61
57
  ownerUserId: z
62
58
  .string()
63
59
  .optional()
64
- .meta({ description: "Whose vaults; required with an application key acting for no one" }),
60
+ .meta({ description: "Also list this person's vaults" }),
65
61
  }),
66
62
  },
67
63
  responses: { 200: json(ListVaultsResponse, "The vaults") },
68
64
  }, async (c) => {
69
- const owner = await ownerFor(c);
70
- const ownerUserId = c.req.query("ownerUserId") ?? owner ?? fail(400, "ownerUserId is required");
71
- if (owner !== undefined && ownerUserId !== owner)
72
- fail(403, "ownerUserId must be the subject");
73
- return jsonResponse(200, { vaults: await c.env.tenant.vault.listVaults(ownerUserId) });
65
+ return jsonResponse(200, {
66
+ vaults: await c.env.tenant.vault.listVaults(c.req.query("ownerUserId"), {
67
+ installation: true,
68
+ }),
69
+ });
74
70
  });
75
- tenantRoute(api, OWN_VAULTS, {
71
+ tenantRoute(api, APPLICATION, {
76
72
  method: "get",
77
73
  path: "/v1/vaults/{vaultId}",
78
74
  tags: ["Vaults"],
@@ -80,10 +76,9 @@ export function vaultRoutes(api) {
80
76
  request: { params: vaultId },
81
77
  responses: { 200: json(VaultInfo, "The vault") },
82
78
  }, async (c) => {
83
- await ownerFor(c);
84
79
  return jsonResponse(200, await c.env.tenant.vault.getVault(c.req.param("vaultId")));
85
80
  });
86
- tenantRoute(api, OWN_VAULTS, {
81
+ tenantRoute(api, APPLICATION, {
87
82
  method: "delete",
88
83
  path: "/v1/vaults/{vaultId}",
89
84
  tags: ["Vaults"],
@@ -91,30 +86,21 @@ export function vaultRoutes(api) {
91
86
  request: { params: vaultId },
92
87
  responses: { 200: json(DeletedResponse, "Deleted") },
93
88
  }, async (c) => {
94
- await ownerFor(c);
95
89
  return jsonResponse(200, await c.env.tenant.vault.deleteVault(c.req.param("vaultId")));
96
90
  });
97
- tenantRoute(api, OWN_VAULTS, {
91
+ tenantRoute(api, APPLICATION, {
98
92
  method: "post",
99
93
  path: "/v1/vaults/{vaultId}/credentials",
100
94
  tags: ["Vaults"],
101
95
  summary: "Add a credential",
102
- description: "A bearer token or OAuth tokens, bound to the URLs a tool may send them to. A subject token cannot store OAuth refresh credentials.",
96
+ description: "A bearer token or OAuth tokens, bound to the URLs a tool may send them to.",
103
97
  request: { params: vaultId, body: body(CreateCredentialRequest) },
104
98
  responses: { 200: json(CredentialInfo, "The credential, without its secret") },
105
99
  }, async (c) => {
106
- await ownerFor(c);
107
100
  const request = CreateCredentialRequestSchema.parse(await readJson(c.req.raw));
108
- // The Runtime calls a refresh credential's token endpoint itself: never for a browser.
109
- if (c.get("scope").kind === "token" &&
110
- request.auth.type === "oauth" &&
111
- request.auth.refresh)
112
- fail(403, "A subject token cannot store OAuth refresh credentials", {
113
- code: "scope_required",
114
- });
115
101
  return jsonResponse(200, await c.env.tenant.keys.createCredential(c.req.param("vaultId"), request));
116
102
  });
117
- tenantRoute(api, OWN_VAULTS, {
103
+ tenantRoute(api, APPLICATION, {
118
104
  method: "get",
119
105
  path: "/v1/vaults/{vaultId}/credentials",
120
106
  tags: ["Vaults"],
@@ -122,12 +108,11 @@ export function vaultRoutes(api) {
122
108
  request: { params: vaultId },
123
109
  responses: { 200: json(ListCredentialsResponse, "The credentials, without their secrets") },
124
110
  }, async (c) => {
125
- await ownerFor(c);
126
111
  return jsonResponse(200, {
127
112
  credentials: await c.env.tenant.vault.listCredentials(c.req.param("vaultId")),
128
113
  });
129
114
  });
130
- tenantRoute(api, OWN_VAULTS, {
115
+ tenantRoute(api, APPLICATION, {
131
116
  method: "get",
132
117
  path: "/v1/vaults/{vaultId}/credentials/{credentialId}",
133
118
  tags: ["Vaults"],
@@ -135,10 +120,9 @@ export function vaultRoutes(api) {
135
120
  request: { params: credentialId },
136
121
  responses: { 200: json(CredentialInfo, "The credential, without its secret") },
137
122
  }, async (c) => {
138
- await ownerFor(c);
139
123
  return jsonResponse(200, await c.env.tenant.vault.getCredential(c.req.param("vaultId"), c.req.param("credentialId")));
140
124
  });
141
- tenantRoute(api, OWN_VAULTS, {
125
+ tenantRoute(api, APPLICATION, {
142
126
  method: "post",
143
127
  path: "/v1/vaults/{vaultId}/credentials/{credentialId}",
144
128
  tags: ["Vaults"],
@@ -149,11 +133,10 @@ export function vaultRoutes(api) {
149
133
  409: { description: "A credential's type cannot change" },
150
134
  },
151
135
  }, async (c) => {
152
- await ownerFor(c);
153
136
  const request = RotateCredentialRequestSchema.parse(await readJson(c.req.raw));
154
137
  return jsonResponse(200, await c.env.tenant.keys.rotateCredential(c.req.param("vaultId"), c.req.param("credentialId"), request));
155
138
  });
156
- tenantRoute(api, OWN_VAULTS, {
139
+ tenantRoute(api, APPLICATION, {
157
140
  method: "delete",
158
141
  path: "/v1/vaults/{vaultId}/credentials/{credentialId}",
159
142
  tags: ["Vaults"],
@@ -161,7 +144,92 @@ export function vaultRoutes(api) {
161
144
  request: { params: credentialId },
162
145
  responses: { 200: json(DeletedResponse, "Deleted") },
163
146
  }, async (c) => {
164
- await ownerFor(c);
165
147
  return jsonResponse(200, await c.env.tenant.vault.deleteCredential(c.req.param("vaultId"), c.req.param("credentialId")));
166
148
  });
149
+ tenantRoute(api, APPLICATION, {
150
+ method: "post",
151
+ path: "/v1/vaults/{vaultId}/oauth/start",
152
+ tags: ["Vaults"],
153
+ summary: "Start an MCP OAuth connect",
154
+ description: "Signs the installation in to the remote MCP server at `url` (declared as `server`) and stores its OAuth credential, bound to `url`, in this installation vault. The Runtime discovers the server's authorization server (RFC 9728, RFC 8414), registers itself (RFC 7591) unless `clientId` names a registered client, and answers the URL to open in a browser; the sign-in returns to `GET /v1/oauth/callback` within `expiresAt`. The callback URL is `NYLORUN_PUBLIC_URL` + `/v1/oauth/callback`, or this request's own origin when the Host has no public URL. Application keys acting for no one only. `oauth_client_required` when the server offers no registration and no `clientId` was given.",
155
+ request: { params: vaultId, body: body(StartOAuthRequest) },
156
+ responses: {
157
+ 200: json(StartOAuthResponse, "Where to send the browser"),
158
+ 502: {
159
+ description: "The authorization server, or its discovery, failed or refused (`oauth_failed`)",
160
+ },
161
+ },
162
+ }, async (c) => {
163
+ const request = StartOAuthRequestSchema.parse(await readJson(c.req.raw));
164
+ const tenant = c.env.tenant;
165
+ const base = tenant.config.publicUrl ?? new URL(c.req.url).origin;
166
+ return jsonResponse(200, await tenant.keys.startOAuth({
167
+ vaultId: c.req.param("vaultId"),
168
+ server: request.server,
169
+ url: request.url,
170
+ ...(request.clientId === undefined ? {} : { clientId: request.clientId }),
171
+ redirectUri: `${base}${OAUTH_CALLBACK_PATH}`,
172
+ }));
173
+ });
174
+ tenantRoute(api, CALLBACK, {
175
+ method: "get",
176
+ path: OAUTH_CALLBACK_PATH,
177
+ tags: ["Vaults"],
178
+ summary: "Finish an MCP OAuth connect",
179
+ description: "Where the authorization server sends the browser back with `code` and `state` (or `error`). Exchanges the code once and stores the credential; answers a small HTML page. Needs no credential and no `Nylorun-Protocol`. A `state` is used once, for ten minutes (`oauth_state_invalid`).",
180
+ request: {
181
+ query: z.object({
182
+ code: z.string().optional(),
183
+ state: z.string().optional(),
184
+ error: z.string().optional(),
185
+ }),
186
+ },
187
+ responses: {
188
+ 200: { description: "Connected", content: { "text/html": { schema: z.string() } } },
189
+ 502: { description: "The authorization server refused the code (`oauth_failed`)" },
190
+ },
191
+ }, async (c) => {
192
+ const state = c.req.query("state");
193
+ const code = c.req.query("code");
194
+ const error = c.req.query("error");
195
+ if (!state)
196
+ return page(400, "Sign-in failed", "The authorization server sent no state. Start the connect again.");
197
+ try {
198
+ await c.env.tenant.keys.finishOAuth({
199
+ state,
200
+ ...(code === undefined ? {} : { code }),
201
+ ...(error === undefined ? {} : { error }),
202
+ });
203
+ }
204
+ catch (failure) {
205
+ if (failure instanceof HttpError || failure instanceof VaultError)
206
+ return page(failure.status, "Sign-in failed", failure.message);
207
+ c.env.tenant.config.logger.error("oauth_callback_failed", {
208
+ error: failure instanceof Error ? failure.name : "unknown",
209
+ });
210
+ return page(500, "Sign-in failed", "The connect failed. Start it again.");
211
+ }
212
+ return page(200, "Connected", "Connected. You can close this tab.");
213
+ });
214
+ }
215
+ const ESCAPES = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" };
216
+ const escape = (text) => text.replace(/[&<>"']/g, (char) => ESCAPES[char]);
217
+ /** The callback's answer: a small page that loads nothing, is never cached and leaks no referrer. */
218
+ function page(status, title, message) {
219
+ const html = `<!doctype html>
220
+ <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
221
+ <title>${escape(title)} · Nylorun</title>
222
+ <style>body{font:16px/1.5 system-ui,sans-serif;max-width:32rem;margin:15vh auto;padding:0 1rem;color:#1a1a1a;background:#fff}@media (prefers-color-scheme:dark){body{color:#eee;background:#111}}h1{font-size:1.25rem}</style>
223
+ </head><body><h1>${escape(title)}</h1><p>${escape(message)}</p></body></html>
224
+ `;
225
+ return new Response(html, {
226
+ status,
227
+ headers: {
228
+ "content-type": "text/html; charset=utf-8",
229
+ "cache-control": "no-store",
230
+ "referrer-policy": "no-referrer",
231
+ "content-security-policy": "default-src 'none'; style-src 'unsafe-inline'",
232
+ "x-content-type-options": "nosniff",
233
+ },
234
+ });
167
235
  }
@@ -68,15 +68,9 @@ function hostRoutes() {
68
68
  return registry;
69
69
  }
70
70
  function tenantSchemes(registry) {
71
- registry.registerComponent("securitySchemes", "applicationKey", bearer("An application key of the Tenant, or a key derived from the admin key. With `Nylorun-Subject` and `Nylorun-Scopes`, it acts for that person, narrowed to those scopes."));
72
- registry.registerComponent("securitySchemes", "subjectToken", bearer("A subject token (`POST /v1/tokens`): one person and one role of the access policy, for at most 15 minutes.", "JWT"));
71
+ registry.registerComponent("securitySchemes", "applicationKey", bearer("An application key of the Tenant: an operator key (`PUT /v1/admin/keys/{id}`), or Studio's key, derived from the admin key. With `Nylorun-Subject` and `Nylorun-Scopes`, it acts for that person, narrowed to those scopes. Never accepted from a browser (`Origin`)."));
72
+ registry.registerComponent("securitySchemes", "issuerToken", bearer("A JWT from a trusted issuer of the Host's identity file (Host feature `trusted-issuers`): one person, with the issuer's scopes, agents and sandbox grants, until it expires. Accepted from servers and browsers alike; CORS is the operator's proxy's.", "JWT"));
73
73
  registry.registerComponent("securitySchemes", "deliveryToken", bearer("The delivery token the Runtime signs each Action delivery with (`Nylorun-Signature`), sent back by the Action endpoint. It reaches only that Action's heartbeat, result and sandbox routes, while that delivery is current, for at most 15 minutes.", "JWT"));
74
- registry.registerComponent("securitySchemes", "publishableKey", {
75
- type: "apiKey",
76
- in: "header",
77
- name: "Nylorun-Key",
78
- description: "A publishable key, sent by a browser page on one of its origins. Public by design: it grants the access policy's anonymous scopes.",
79
- });
80
74
  }
81
75
  let tenant;
82
76
  let admin;
@@ -94,7 +88,7 @@ export function tenantDocument() {
94
88
  info: {
95
89
  title: "Nylorun Runtime: Tenant API",
96
90
  version: RUNTIME_VERSION,
97
- description: "Agents, sessions and their events, vaults, access and the Tenant's settings. A Runtime serves one Tenant: its URL is the Tenant's, and no request names it. Every request sends `Nylorun-Protocol`. Who may call each operation is its `security` and its `x-nylorun-credentials`, `x-nylorun-scopes` (the subject scopes that reach it) and `x-nylorun-browser` fields.",
91
+ description: "Agents, sessions and their events, vaults, access and the Tenant's settings. A Runtime serves one Tenant: its URL is the Tenant's, and no request names it. Every request sends `Nylorun-Protocol`. Who may call each operation is its `security` and its `x-nylorun-credentials` and `x-nylorun-scopes` (the subject scopes that reach it) fields.",
98
92
  "x-nylorun-protocol": PROTOCOL,
99
93
  },
100
94
  servers: [
@@ -122,7 +116,7 @@ export function adminDocument() {
122
116
  info: {
123
117
  title: "Nylorun Runtime: Admin API",
124
118
  version: RUNTIME_VERSION,
125
- description: "The Host's status, its Tenant's state and shutdown, with the admin key. Served on the operator listener when the Host has one; the public listener answers these routes with the opaque 404.",
119
+ description: "The Host's status, its Tenant's state, shutdown and the Tenant's operator keys, with the admin key. Served on the operator listener when the Host has one; the public listener answers these routes with the opaque 404.",
126
120
  "x-nylorun-protocol": PROTOCOL,
127
121
  },
128
122
  servers: [