@nylorun/runtime 0.22.0-beta → 0.24.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 (273) hide show
  1. package/CHANGELOG.md +161 -0
  2. package/README.md +95 -30
  3. package/dist/adapters/execution/restate.d.ts +15 -3
  4. package/dist/adapters/execution/restate.js +78 -84
  5. package/dist/adapters/replication/pgoutput.d.ts +2 -0
  6. package/dist/adapters/replication/pgoutput.js +6 -3
  7. package/dist/adapters/sandbox/local.d.ts +9 -1
  8. package/dist/adapters/sandbox/local.js +16 -4
  9. package/dist/adapters/sandbox/virtual.js +4 -1
  10. package/dist/adapters/streams/s2.js +1 -13
  11. package/dist/api/a2a/endpoint.js +5 -1
  12. package/dist/api/a2a/routes.d.ts +2 -0
  13. package/dist/api/a2a/routes.js +2 -2
  14. package/dist/api/a2a/tasks.js +2 -1
  15. package/dist/api/ag-ui/endpoint.js +4 -2
  16. package/dist/api/ag-ui/history.js +4 -1
  17. package/dist/api/ag-ui/routes.d.ts +4 -1
  18. package/dist/api/ag-ui/routes.js +11 -10
  19. package/dist/api/ag-ui/translate.js +3 -2
  20. package/dist/api/components.d.ts +4953 -1204
  21. package/dist/api/components.js +25 -16
  22. package/dist/api/gate/keys.js +7 -1
  23. package/dist/api/gate/routes.d.ts +6 -0
  24. package/dist/api/gate/routes.js +39 -3
  25. package/dist/api/http/app.js +4 -4
  26. package/dist/api/http/define.d.ts +6 -0
  27. package/dist/api/http/define.js +13 -1
  28. package/dist/api/http/respond.js +1 -1
  29. package/dist/api/http/routes/artifacts.js +35 -8
  30. package/dist/api/http/routes/coverage.d.ts +9 -0
  31. package/dist/api/http/routes/coverage.js +34 -0
  32. package/dist/api/http/routes/environments.d.ts +11 -0
  33. package/dist/api/http/routes/environments.js +283 -0
  34. package/dist/api/http/routes/mcp.d.ts +1 -1
  35. package/dist/api/http/routes/mcp.js +2 -2
  36. package/dist/api/http/routes/me.js +1 -1
  37. package/dist/api/http/routes/reads.d.ts +45 -9
  38. package/dist/api/http/routes/reads.js +54 -5
  39. package/dist/api/http/routes/sessions.js +11 -6
  40. package/dist/api/http/routes/tenant.js +32 -10
  41. package/dist/api/openapi.js +31 -26
  42. package/dist/artifacts/disk.d.ts +59 -0
  43. package/dist/artifacts/disk.js +563 -0
  44. package/dist/artifacts/export.d.ts +1 -1
  45. package/dist/artifacts/export.js +2 -3
  46. package/dist/artifacts/folders.d.ts +6 -3
  47. package/dist/artifacts/folders.js +17 -12
  48. package/dist/artifacts/service.d.ts +2 -1
  49. package/dist/artifacts/service.js +6 -1
  50. package/dist/artifacts/workspace.d.ts +1 -1
  51. package/dist/artifacts/workspace.js +1 -1
  52. package/dist/core/flow-host.d.ts +3 -22
  53. package/dist/core/flow-host.js +11 -37
  54. package/dist/core/provider.d.ts +3 -2
  55. package/dist/core/provider.js +5 -37
  56. package/dist/core/sandbox-routes.d.ts +2 -21
  57. package/dist/core/sandbox-routes.js +10 -43
  58. package/dist/execution/types.d.ts +11 -9
  59. package/dist/execution/types.js +5 -7
  60. package/dist/gates/contract.d.ts +3 -24
  61. package/dist/gates/contract.js +1 -36
  62. package/dist/gates/egress-credentials.d.ts +9 -0
  63. package/dist/gates/egress-credentials.js +11 -4
  64. package/dist/gates/egress.d.ts +28 -1
  65. package/dist/gates/egress.js +115 -10
  66. package/dist/gates/fixture-model.d.ts +28 -0
  67. package/dist/gates/fixture-model.js +92 -0
  68. package/dist/gates/handler.js +4 -4
  69. package/dist/gates/http-tool.d.ts +6 -2
  70. package/dist/gates/http-tool.js +3 -5
  71. package/dist/gates/in-process.d.ts +5 -0
  72. package/dist/gates/in-process.js +12 -1
  73. package/dist/gates/journal.d.ts +30 -0
  74. package/dist/gates/journal.js +52 -0
  75. package/dist/gates/mcp-handler.d.ts +4 -0
  76. package/dist/gates/mcp-handler.js +5 -1
  77. package/dist/gates/meter.d.ts +10 -2
  78. package/dist/gates/meter.js +20 -5
  79. package/dist/gates/policy.d.ts +49 -0
  80. package/dist/gates/policy.js +134 -0
  81. package/dist/gates/tenant-vaults.d.ts +7 -7
  82. package/dist/gates/tenant-vaults.js +5 -5
  83. package/dist/gates/tool-calls.d.ts +10 -5
  84. package/dist/gates/tool-calls.js +28 -10
  85. package/dist/gates/tool-client.d.ts +8 -0
  86. package/dist/gates/tool-client.js +27 -11
  87. package/dist/gates/tool-contract.d.ts +10 -68
  88. package/dist/gates/tool-contract.js +1 -85
  89. package/dist/gates/tool-gate.d.ts +1 -1
  90. package/dist/harness/calls.d.ts +12 -5
  91. package/dist/harness/calls.js +21 -12
  92. package/dist/harness/executors.js +12 -9
  93. package/dist/harness/main.js +18 -5
  94. package/dist/harness/pod.d.ts +34 -0
  95. package/dist/harness/pod.js +41 -0
  96. package/dist/harness/service.d.ts +0 -1
  97. package/dist/harness/service.js +13 -5
  98. package/dist/harness/setup.d.ts +27 -0
  99. package/dist/harness/setup.js +150 -0
  100. package/dist/harness/ws-client.js +3 -2
  101. package/dist/harness/ws-port.d.ts +1 -5
  102. package/dist/harness/ws-port.js +1 -5
  103. package/dist/harness-api/in-process.js +3 -3
  104. package/dist/harness-api/record.d.ts +15 -1
  105. package/dist/harness-api/record.js +174 -44
  106. package/dist/harness-api/redact.d.ts +3 -0
  107. package/dist/harness-api/redact.js +45 -0
  108. package/dist/harness-api/server.d.ts +8 -0
  109. package/dist/harness-api/server.js +93 -13
  110. package/dist/harness-api/start.d.ts +10 -3
  111. package/dist/harness-api/start.js +40 -10
  112. package/dist/harness-api/tool-journal.d.ts +96 -0
  113. package/dist/harness-api/tool-journal.js +102 -0
  114. package/dist/harness-api/tool-status.d.ts +13 -0
  115. package/dist/harness-api/tool-status.js +26 -0
  116. package/dist/harness-api/workspace.d.ts +6 -3
  117. package/dist/harness-api/workspace.js +27 -11
  118. package/dist/harness-api/ws-server.d.ts +11 -3
  119. package/dist/harness-api/ws-server.js +94 -15
  120. package/dist/host/execution.d.ts +8 -6
  121. package/dist/host/gates.d.ts +2 -1
  122. package/dist/host/gates.js +10 -2
  123. package/dist/host/main.js +64 -21
  124. package/dist/host/operate.js +67 -5
  125. package/dist/host/stack-config.d.ts +70 -0
  126. package/dist/host/stack-config.js +67 -1
  127. package/dist/infra/execution.d.ts +18 -1
  128. package/dist/infra/execution.js +28 -4
  129. package/dist/infra/index.d.ts +1 -1
  130. package/dist/keys/client.js +4 -1
  131. package/dist/keys/keys.d.ts +5 -5
  132. package/dist/keys/keys.js +5 -7
  133. package/dist/management-openapi.json +999 -2211
  134. package/dist/mcp/connect.d.ts +25 -20
  135. package/dist/mcp/connect.js +29 -30
  136. package/dist/mcp/pool.d.ts +2 -2
  137. package/dist/mcp/pool.js +1 -1
  138. package/dist/mcp/preview.d.ts +3 -3
  139. package/dist/mcp/preview.js +10 -20
  140. package/dist/mcp/snapshot.d.ts +5 -11
  141. package/dist/mcp/snapshot.js +6 -7
  142. package/dist/model/pi-model.js +8 -0
  143. package/dist/openapi.json +6955 -2526
  144. package/dist/reads/types.d.ts +16 -3
  145. package/dist/record/append.d.ts +3 -1
  146. package/dist/record/append.js +1 -0
  147. package/dist/record/envelope.d.ts +5 -1
  148. package/dist/record/envelope.js +7 -3
  149. package/dist/record/environment.d.ts +25 -0
  150. package/dist/record/environment.js +44 -0
  151. package/dist/record/index.d.ts +1 -1
  152. package/dist/record/index.js +1 -1
  153. package/dist/retry.d.ts +21 -0
  154. package/dist/retry.js +34 -0
  155. package/dist/sandbox/disk-fs.d.ts +95 -0
  156. package/dist/sandbox/disk-fs.js +206 -0
  157. package/dist/sandbox/join.d.ts +17 -18
  158. package/dist/sandbox/join.js +46 -20
  159. package/dist/sandbox/manager.d.ts +16 -8
  160. package/dist/sandbox/manager.js +33 -17
  161. package/dist/sandbox/placement.d.ts +13 -12
  162. package/dist/sandbox/placement.js +29 -12
  163. package/dist/sandbox/pods/client.d.ts +6 -1
  164. package/dist/sandbox/pods/lifecycle.d.ts +28 -27
  165. package/dist/sandbox/pods/lifecycle.js +14 -26
  166. package/dist/sandbox/pods/reconcile.js +21 -15
  167. package/dist/sandbox/pods/spec.d.ts +15 -13
  168. package/dist/sandbox/pods/spec.js +16 -20
  169. package/dist/sandbox/records.d.ts +2 -2
  170. package/dist/sandbox/records.js +3 -3
  171. package/dist/sandbox/resolve.d.ts +35 -27
  172. package/dist/sandbox/resolve.js +66 -41
  173. package/dist/sandbox/session-sandbox.js +1 -1
  174. package/dist/sandbox/setup.d.ts +61 -0
  175. package/dist/sandbox/setup.js +123 -0
  176. package/dist/sandbox/share.d.ts +11 -21
  177. package/dist/sandbox/share.js +12 -41
  178. package/dist/sandbox/tenant-config.d.ts +12 -14
  179. package/dist/sandbox/tenant-config.js +12 -17
  180. package/dist/sandbox/types.d.ts +6 -0
  181. package/dist/store/postgres/control.js +8 -1
  182. package/dist/store/postgres/drizzle/0018_wake_outbox_retries.sql +3 -0
  183. package/dist/store/postgres/drizzle/0019_sandbox_signal_outbox.sql +18 -0
  184. package/dist/store/postgres/drizzle/0020_environments.sql +63 -0
  185. package/dist/store/postgres/drizzle/0021_disk_versions.sql +7 -0
  186. package/dist/store/postgres/drizzle/0022_model_usage_timing.sql +5 -0
  187. package/dist/store/postgres/drizzle/0023_artifact_pages.sql +1 -0
  188. package/dist/store/postgres/drizzle/meta/_journal.json +42 -0
  189. package/dist/store/postgres/reads.js +117 -17
  190. package/dist/store/postgres/record-writer.d.ts +4 -4
  191. package/dist/store/postgres/record-writer.js +9 -9
  192. package/dist/store/postgres/record.d.ts +7 -0
  193. package/dist/store/postgres/record.js +23 -2
  194. package/dist/store/postgres/schema.d.ts +1213 -779
  195. package/dist/store/postgres/schema.js +136 -76
  196. package/dist/store/postgres/store.js +283 -117
  197. package/dist/store/types.d.ts +206 -88
  198. package/dist/streams/relay/core.js +1 -13
  199. package/dist/tenant/advance.d.ts +7 -6
  200. package/dist/tenant/advance.js +121 -49
  201. package/dist/tenant/artifact-tool.js +3 -4
  202. package/dist/tenant/commands.d.ts +15 -2
  203. package/dist/tenant/commands.js +94 -14
  204. package/dist/tenant/context.d.ts +67 -33
  205. package/dist/tenant/context.js +4 -15
  206. package/dist/tenant/control.d.ts +8 -1
  207. package/dist/tenant/control.js +2 -0
  208. package/dist/tenant/effects.d.ts +1 -1
  209. package/dist/tenant/effects.js +37 -20
  210. package/dist/tenant/environments.d.ts +167 -0
  211. package/dist/tenant/environments.js +823 -0
  212. package/dist/tenant/harness-tokens.d.ts +21 -0
  213. package/dist/tenant/harness-tokens.js +61 -0
  214. package/dist/tenant/host-token.d.ts +1 -1
  215. package/dist/tenant/host-token.js +2 -4
  216. package/dist/tenant/identity-file.d.ts +4 -4
  217. package/dist/tenant/identity-file.js +17 -8
  218. package/dist/tenant/issuers.js +3 -3
  219. package/dist/tenant/principals.js +1 -1
  220. package/dist/tenant/reset.js +3 -3
  221. package/dist/tenant/runtime.d.ts +6 -1
  222. package/dist/tenant/runtime.js +30 -4
  223. package/dist/tenant/scheduler.d.ts +7 -0
  224. package/dist/tenant/scheduler.js +38 -7
  225. package/dist/tenant/session-streams.d.ts +0 -1
  226. package/dist/tenant/session-streams.js +1 -13
  227. package/dist/tenant/session.d.ts +3 -3
  228. package/dist/tenant/session.js +0 -14
  229. package/dist/tenant/sessions.d.ts +13 -13
  230. package/dist/tenant/sessions.js +61 -138
  231. package/dist/tenant/signing-keys.d.ts +5 -1
  232. package/dist/tenant/status.js +3 -3
  233. package/dist/tenant/streams.js +14 -29
  234. package/dist/tenant/sweep.d.ts +39 -12
  235. package/dist/tenant/sweep.js +168 -63
  236. package/dist/tenant/transcript.d.ts +210 -20
  237. package/dist/tenant/transcript.js +62 -11
  238. package/dist/tenant/turn-timing.d.ts +43 -0
  239. package/dist/tenant/turn-timing.js +39 -0
  240. package/dist/tenant/types.d.ts +2 -0
  241. package/dist/traces/ids.d.ts +42 -0
  242. package/dist/traces/ids.js +85 -0
  243. package/dist/traces/key.d.ts +6 -0
  244. package/dist/traces/key.js +37 -0
  245. package/dist/traces/map.d.ts +113 -0
  246. package/dist/traces/map.js +317 -0
  247. package/dist/traces/options.d.ts +7 -0
  248. package/dist/traces/options.js +1 -0
  249. package/dist/traces/otlp.d.ts +42 -0
  250. package/dist/traces/otlp.js +84 -0
  251. package/dist/traces/projector.d.ts +58 -0
  252. package/dist/traces/projector.js +200 -0
  253. package/dist/traces/read.d.ts +23 -0
  254. package/dist/traces/read.js +16 -0
  255. package/dist/vault/coverage.d.ts +26 -0
  256. package/dist/vault/coverage.js +64 -0
  257. package/dist/vault/error.d.ts +11 -1
  258. package/dist/vault/error.js +5 -1
  259. package/dist/vault/service.d.ts +47 -48
  260. package/dist/vault/service.js +321 -382
  261. package/dist/vault/sources.d.ts +5 -16
  262. package/dist/vault/sources.js +6 -8
  263. package/dist/version.d.ts +1 -1
  264. package/dist/version.js +2 -2
  265. package/package.json +14 -13
  266. package/dist/api/http/routes/sandboxes.d.ts +0 -9
  267. package/dist/api/http/routes/sandboxes.js +0 -153
  268. package/dist/api/http/routes/vaults.d.ts +0 -10
  269. package/dist/api/http/routes/vaults.js +0 -139
  270. package/dist/record/sandbox.d.ts +0 -25
  271. package/dist/record/sandbox.js +0 -43
  272. package/dist/tenant/sandboxes.d.ts +0 -122
  273. package/dist/tenant/sandboxes.js +0 -562
@@ -0,0 +1,21 @@
1
+ import type { SessionStore } from "../store/types.js";
2
+ export declare const HARNESS_TOKENS_SETTING = "harness.tokens";
3
+ export interface HarnessTokenInfo {
4
+ readonly id: string;
5
+ readonly createdAt: string;
6
+ }
7
+ export type HarnessTokenRefusal = {
8
+ readonly reason: "invalid_id";
9
+ readonly message: string;
10
+ };
11
+ /** The harness id `token` is for, or undefined when it is none of the Tenant's harness tokens. */
12
+ export declare function harnessOfToken(store: SessionStore, token: string): Promise<string | undefined>;
13
+ /** Creates or rotates harness `id`'s token; the token is returned this once. */
14
+ export declare function putHarnessToken(store: SessionStore, id: string): Promise<{
15
+ id: string;
16
+ token: string;
17
+ rotated: boolean;
18
+ } | HarnessTokenRefusal>;
19
+ export declare function listHarnessTokens(store: SessionStore): Promise<HarnessTokenInfo[]>;
20
+ /** Deletes harness `id`'s token; whether there was one. */
21
+ export declare function deleteHarnessToken(store: SessionStore, id: string): Promise<boolean>;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Per-harness tokens (R4.1): the credential a custom harness's container connects to the Harness
3
+ * API listener with. Each is bound to one harness id, so a harness can only say that id in
4
+ * `hello` and only take the sessions pinned to it. Our harness keeps the installation's
5
+ * `NYLORUN_HARNESS_TOKEN`, bound to `nylorun`; a pod's harness holds neither (it joins).
6
+ *
7
+ * The Tenant setting `harness.tokens` keeps each token's sha256 and when it was made, never the
8
+ * token: `nylorun-operate harness token put <id>` prints it once. Putting an id again rotates its
9
+ * token; the old one stops working at its next connection.
10
+ */
11
+ import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
12
+ import { DEFAULT_HARNESS, isHarnessId } from "@nylorun/core/contracts";
13
+ export const HARNESS_TOKENS_SETTING = "harness.tokens";
14
+ const sha256 = (token) => createHash("sha256").update(token).digest("hex");
15
+ async function read(store) {
16
+ const raw = await store.tx((t) => t.getSetting(HARNESS_TOKENS_SETTING));
17
+ return raw === undefined ? {} : JSON.parse(raw);
18
+ }
19
+ /** The harness id `token` is for, or undefined when it is none of the Tenant's harness tokens. */
20
+ export async function harnessOfToken(store, token) {
21
+ const hash = Buffer.from(sha256(token), "hex");
22
+ for (const [id, stored] of Object.entries(await read(store))) {
23
+ const candidate = Buffer.from(stored.sha256, "hex");
24
+ if (candidate.length === hash.length && timingSafeEqual(candidate, hash))
25
+ return id;
26
+ }
27
+ return undefined;
28
+ }
29
+ /** Creates or rotates harness `id`'s token; the token is returned this once. */
30
+ export async function putHarnessToken(store, id) {
31
+ if (!isHarnessId(id))
32
+ return { reason: "invalid_id", message: `${id} is not a harness id: lowercase letters, digits and dashes, starting with a letter` };
33
+ if (id === DEFAULT_HARNESS)
34
+ return { reason: "invalid_id", message: `${DEFAULT_HARNESS} is our harness: it connects with NYLORUN_HARNESS_TOKEN` };
35
+ const token = randomBytes(32).toString("hex");
36
+ return store.tx(async (t) => {
37
+ const raw = await t.getSetting(HARNESS_TOKENS_SETTING);
38
+ const tokens = raw === undefined ? {} : JSON.parse(raw);
39
+ const rotated = id in tokens;
40
+ tokens[id] = { sha256: sha256(token), createdAt: new Date().toISOString() };
41
+ await t.putSetting(HARNESS_TOKENS_SETTING, JSON.stringify(tokens));
42
+ return { id, token, rotated };
43
+ });
44
+ }
45
+ export async function listHarnessTokens(store) {
46
+ return Object.entries(await read(store))
47
+ .map(([id, stored]) => ({ id, createdAt: stored.createdAt }))
48
+ .sort((a, b) => a.id.localeCompare(b.id));
49
+ }
50
+ /** Deletes harness `id`'s token; whether there was one. */
51
+ export async function deleteHarnessToken(store, id) {
52
+ return store.tx(async (t) => {
53
+ const raw = await t.getSetting(HARNESS_TOKENS_SETTING);
54
+ const tokens = raw === undefined ? {} : JSON.parse(raw);
55
+ if (!(id in tokens))
56
+ return false;
57
+ delete tokens[id];
58
+ await t.putSetting(HARNESS_TOKENS_SETTING, JSON.stringify(tokens));
59
+ return true;
60
+ });
61
+ }
@@ -85,7 +85,7 @@ declare const KINDS: {
85
85
  export declare function verifyHostToken(store: SessionStore, tenantId: string, raw: string, kind: keyof typeof KINDS, cache?: HostTokenKeyCache): Promise<HostTokenVerdict>;
86
86
  /**
87
87
  * Why a host's claims are stale, or undefined while they are its sandbox's current host: the
88
- * sandbox exists as a pod sandbox, is not deleted, lost or expired, and its epoch and pod are
88
+ * sandbox exists as a pod Environment, is not deleted or lost, and its epoch and pod are
89
89
  * the token's. One indexed read of the sandbox's row.
90
90
  */
91
91
  export declare function staleHost(store: SessionStore, claims: HostClaims): Promise<string | undefined>;
@@ -157,11 +157,11 @@ export async function verifyHostToken(store, tenantId, raw, kind, cache = new Ma
157
157
  }
158
158
  /**
159
159
  * Why a host's claims are stale, or undefined while they are its sandbox's current host: the
160
- * sandbox exists as a pod sandbox, is not deleted, lost or expired, and its epoch and pod are
160
+ * sandbox exists as a pod Environment, is not deleted or lost, and its epoch and pod are
161
161
  * the token's. One indexed read of the sandbox's row.
162
162
  */
163
163
  export async function staleHost(store, claims) {
164
- const row = await store.tx((t) => t.sandboxResource(claims.sandboxId));
164
+ const row = await store.tx((t) => t.environment(claims.sandboxId));
165
165
  const pod = row?.pod;
166
166
  if (!row || row.kind !== "pod" || !pod)
167
167
  return "the sandbox is gone";
@@ -169,8 +169,6 @@ export async function staleHost(store, claims) {
169
169
  return "the sandbox was deleted";
170
170
  if (pod.observed === "lost")
171
171
  return "the sandbox was lost";
172
- if (pod.observed === "expired")
173
- return "the sandbox expired";
174
172
  if (pod.hostEpoch !== claims.epoch)
175
173
  return "another host holds the sandbox now";
176
174
  if (pod.podUid !== claims.podUid)
@@ -15,9 +15,9 @@
15
15
  * # Optional, with their defaults:
16
16
  * subject: "{sub}" # scalar claims only
17
17
  * scopes: { claim: scope } # or { fixed: [sessions:own, agents:read] }
18
- * allowedScopes: [agents:read, sessions:own, sandboxes:write] # add studio to grant it
18
+ * allowedScopes: [agents:read, sessions:own, environments:write] # add studio to grant it
19
19
  * agents: [support] # absent reaches every agent
20
- * sandboxes: ["{org_id}/*"] # absent reaches no sandbox
20
+ * environments: ["{org_id}/*"] # absent reaches no Environment
21
21
  * ```
22
22
  */
23
23
  import { type KeyObject } from "node:crypto";
@@ -55,8 +55,8 @@ export interface TrustedIssuerConfig {
55
55
  readonly allowedScopes: readonly IssuerScope[];
56
56
  /** The agents its tokens reach; absent reaches all. */
57
57
  readonly agents?: readonly string[];
58
- /** Sandbox grant templates, such as `{org_id}/*`; absent reaches no sandbox. */
59
- readonly sandboxes?: readonly string[];
58
+ /** Environment grant templates, such as `{org_id}/*`; absent reaches no Environment. */
59
+ readonly environments?: readonly string[];
60
60
  }
61
61
  /** A malformed identity file: the message names the issuer and the field. */
62
62
  export declare class IdentityFileError extends Error {
@@ -15,15 +15,15 @@
15
15
  * # Optional, with their defaults:
16
16
  * subject: "{sub}" # scalar claims only
17
17
  * scopes: { claim: scope } # or { fixed: [sessions:own, agents:read] }
18
- * allowedScopes: [agents:read, sessions:own, sandboxes:write] # add studio to grant it
18
+ * allowedScopes: [agents:read, sessions:own, environments:write] # add studio to grant it
19
19
  * agents: [support] # absent reaches every agent
20
- * sandboxes: ["{org_id}/*"] # absent reaches no sandbox
20
+ * environments: ["{org_id}/*"] # absent reaches no Environment
21
21
  * ```
22
22
  */
23
23
  import { createPublicKey } from "node:crypto";
24
24
  import { parse as parseYaml } from "yaml";
25
25
  import { z } from "zod";
26
- import { ISSUER_SCOPES, isSandboxGrant, TOKEN_SANDBOX_GRANTS_MAX, TOKEN_SCOPES, } from "@nylorun/core/contracts";
26
+ import { ISSUER_SCOPES, isEnvironmentGrant, TOKEN_ENVIRONMENT_GRANTS_MAX, TOKEN_SCOPES, } from "@nylorun/core/contracts";
27
27
  /** The JWT algorithms an issuer may sign with. */
28
28
  export const ISSUER_ALGORITHMS = ["RS256", "ES256", "EdDSA"];
29
29
  /** An entry's defaults: the `sub` claim, the RFC 9068 `scope` claim, every token scope but `studio`. */
@@ -119,11 +119,11 @@ const IssuerEntrySchema = z
119
119
  .default(DEFAULT_SCOPES),
120
120
  allowedScopes: z.array(IssuerScopeSchema).min(1).default([...DEFAULT_ALLOWED_SCOPES]),
121
121
  agents: z.array(z.string().min(1)).optional(),
122
- sandboxes: z
123
- .array(template("a sandbox grant").refine(
122
+ environments: z
123
+ .array(template("an Environment grant").refine(
124
124
  // A rendered grant must be a grant: checked here with a sample value per claim.
125
- (value) => isSandboxGrant(renderTemplate(value, () => "x")), "must render to a sandbox id or a prefix ending in /*, as {org_id}/*"))
126
- .max(TOKEN_SANDBOX_GRANTS_MAX)
125
+ (value) => isEnvironmentGrant(renderTemplate(value, () => "x")), "must render to an Environment id or a prefix ending in /*, as {org_id}/*"))
126
+ .max(TOKEN_ENVIRONMENT_GRANTS_MAX)
127
127
  .optional(),
128
128
  });
129
129
  const IdentityFileSchema = z.object({ issuers: z.array(z.unknown()) });
@@ -173,6 +173,15 @@ export function parseIdentityFile(text, source = "identity file", options = {})
173
173
  ? entry.name
174
174
  : `#${index + 1}`;
175
175
  const where = `issuer ${named}`;
176
+ // Protocol 11 renamed these (R2d Q11): refused, never read as no grants.
177
+ if (entry && typeof entry === "object" && "sandboxes" in entry)
178
+ return fail(`${where}: sandboxes: renamed environments in protocol 11`);
179
+ const scopesNamed = JSON.stringify([
180
+ entry?.allowedScopes,
181
+ entry?.scopes,
182
+ ]);
183
+ if (scopesNamed.includes('"sandboxes:write"'))
184
+ return fail(`${where}: sandboxes:write: renamed environments:write in protocol 11`);
176
185
  const parsed = IssuerEntrySchema.safeParse(entry);
177
186
  if (!parsed.success)
178
187
  return fail(`${where}: ${issueText(parsed.error.issues)}`);
@@ -207,7 +216,7 @@ export function parseIdentityFile(text, source = "identity file", options = {})
207
216
  scopes: value.scopes,
208
217
  allowedScopes: [...new Set(value.allowedScopes)],
209
218
  ...(value.agents ? { agents: [...new Set(value.agents)] } : {}),
210
- ...(value.sandboxes ? { sandboxes: value.sandboxes } : {}),
219
+ ...(value.environments ? { environments: value.environments } : {}),
211
220
  });
212
221
  });
213
222
  return issuers;
@@ -37,7 +37,7 @@ const JWKS_MAX_BYTES = 256 * 1024;
37
37
  const JWKS_MAX_KEYS = 64;
38
38
  const DEFAULT_REFETCH_MS = 60_000;
39
39
  const DEFAULT_MAX_AGE_MS = 10 * 60_000;
40
- /** A grant's claim value: one sandbox id segment, so a claim never widens the template. */
40
+ /** A grant's claim value: one Environment id segment, so a claim never widens the template. */
41
41
  const GRANT_SEGMENT = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
42
42
  /** The issuer's public keys: static, or its JWKS's as last fetched. */
43
43
  class IssuerKeys {
@@ -292,7 +292,7 @@ export async function verifyIssuerToken(ctx, issuer, raw, resourceMetadata) {
292
292
  const subject = renderTemplate(config.subject, (claim) => scalarClaim(payload, claim));
293
293
  if (subject === undefined || !isSubject(subject) || RESERVED.has(subject))
294
294
  return rejected("issuer_token_subject");
295
- const sandboxes = config.sandboxes?.flatMap((grant) => {
295
+ const environments = config.environments?.flatMap((grant) => {
296
296
  const rendered = renderTemplate(grant, (claim) => {
297
297
  const value = scalarClaim(payload, claim);
298
298
  return value !== undefined && GRANT_SEGMENT.test(value) ? value : undefined;
@@ -309,7 +309,7 @@ export async function verifyIssuerToken(ctx, issuer, raw, resourceMetadata) {
309
309
  subject,
310
310
  scopes: scopesOf(config, payload),
311
311
  agents: config.agents ? new Set(config.agents) : "*",
312
- sandboxes: [...new Set(sandboxes ?? [])],
312
+ environments: [...new Set(environments ?? [])],
313
313
  expiresAt: payload.exp * 1000,
314
314
  tokenId: jti,
315
315
  ...(verdict.kid !== undefined ? { keyId: verdict.kid } : {}),
@@ -6,7 +6,7 @@
6
6
  * `nylorun-operate keys`, `operator-keys.ts`); no other key is derived.
7
7
  *
8
8
  * The derivation is the one `@nylorun/admin` computes on the client side
9
- * (`admin/src/derived-credentials.ts`: `deriveStudioToken`); both sides must stay byte for byte
9
+ * (`sdks/admin/src/derived-credentials.ts`: `deriveStudioToken`); both sides must stay byte for byte
10
10
  * the same.
11
11
  */
12
12
  import { createHmac } from "node:crypto";
@@ -1,6 +1,6 @@
1
1
  import { existsSync, mkdirSync, renameSync, rmSync, writeFileSync, } from "node:fs";
2
2
  import { join } from "node:path";
3
- import { detachAllSessions } from "./sandboxes.js";
3
+ import { detachAllSessions } from "./environments.js";
4
4
  import { contentKey } from "../artifacts/folders.js";
5
5
  /**
6
6
  * Rename `dir` to a sibling, delete the sibling, recreate an empty `dir`.
@@ -38,14 +38,14 @@ function replaceDirectory(dir) {
38
38
  */
39
39
  export async function resetTenant(ctx, scope) {
40
40
  const clearSessions = scope === "sessions" || scope === "all";
41
- const clearSandboxes = scope === "sandboxes" || scope === "all";
41
+ const clearSandboxes = scope === "environments" || scope === "all";
42
42
  const clearAll = scope === "all";
43
43
  if (clearSandboxes) {
44
44
  await ctx.sandbox.removeAll();
45
45
  }
46
46
  // The artifacts the reset deletes: their rows go in its transaction, their bytes after it.
47
47
  const blobKeys = await ctx.store.tx(async (t) => {
48
- // Sandbox resources outlive their sessions: a sessions reset only detaches them.
48
+ // Environments outlive their sessions: a sessions reset only detaches them.
49
49
  if (clearSessions && !clearSandboxes)
50
50
  await detachAllSessions(t);
51
51
  const scoped = clearAll ? "all" : "sessions";
@@ -14,6 +14,7 @@ import type { HarnessChannel, MemoryPortsOptions } from "@nylorun/core/harness-a
14
14
  import { type HarnessPeer } from "../harness-api/server.js";
15
15
  import { type HostAuthority } from "../sandbox/join.js";
16
16
  import type { TenantPods } from "./context.js";
17
+ import type { TraceOptions } from "../traces/options.js";
17
18
  /** TENANTS-CCR: test/injection hooks until TenantConfig gains them. */
18
19
  export type TenantOpenHooks = {
19
20
  modelProvider?: ModelProvider;
@@ -80,9 +81,11 @@ export type TenantOpenHooks = {
80
81
  harnessTap?: MemoryPortsOptions["tap"];
81
82
  /**
82
83
  * Sandbox pods (F7.2): the sandboxes service (`NYLORUN_SANDBOXES_URL`) and the Runtime image
83
- * pods copy the engine from. Without it, kind `pod` is `sandbox_unavailable`.
84
+ * pods copy the engine from. Without it, kind `pod` is `environment_unavailable`.
84
85
  */
85
86
  pods?: TenantPods;
87
+ /** How the trace route makes spans (R7.5): the Host's `OTEL_*` resource and content opt-ins. */
88
+ traces?: TraceOptions;
86
89
  } & OpenedTenant;
87
90
  export declare class TenantRuntime implements TenantHandle {
88
91
  private readonly ctx;
@@ -97,6 +100,8 @@ export declare class TenantRuntime implements TenantHandle {
97
100
  private constructor();
98
101
  /** The Tenant's side of a sandbox pod's join (F7.2). */
99
102
  hostAuthority(): HostAuthority | undefined;
103
+ /** The custom harness a per-harness token is for (R4.1), or undefined. */
104
+ harnessOfToken(token: string): Promise<string | undefined>;
100
105
  static open(config: TenantConfig, hooks: TenantOpenHooks): Promise<TenantRuntime>;
101
106
  fetch(request: Request, node: NodeBindings): Promise<Response>;
102
107
  authorize(sessionId: string, request: {
@@ -29,6 +29,7 @@ import { harnessStatusOf } from "./status.js";
29
29
  import { startInProcessHarness, startLoopbackHarness, } from "../harness-api/in-process.js";
30
30
  import { localWorkspace, remoteWorkspace, withPodWorkspaces, } from "../harness-api/workspace.js";
31
31
  import { hostAuthority } from "../sandbox/join.js";
32
+ import { harnessOfToken } from "./harness-tokens.js";
32
33
  import { reconcileSandbox } from "../sandbox/pods/reconcile.js";
33
34
  import { sandboxWorkspaceReader } from "../artifacts/workspace.js";
34
35
  import { readDefinitionFile } from "./definition-files.js";
@@ -74,6 +75,10 @@ export class TenantRuntime {
74
75
  hostAuthority() {
75
76
  return this.ctx.closing || this.ctx.closed ? undefined : this.hosts;
76
77
  }
78
+ /** The custom harness a per-harness token is for (R4.1), or undefined. */
79
+ harnessOfToken(token) {
80
+ return harnessOfToken(this.ctx.store, token);
81
+ }
77
82
  static async open(config, hooks) {
78
83
  const envelope = hooks.envelope;
79
84
  const flowLimits = validateConfig(config);
@@ -193,7 +198,7 @@ export class TenantRuntime {
193
198
  store: opened,
194
199
  logger: config.logger,
195
200
  preference,
196
- isPod: async (sandboxId) => (await opened.tx((t) => t.sandboxResource(sandboxId)))?.kind === "pod",
201
+ isPod: async (sandboxId) => (await opened.tx((t) => t.environment(sandboxId)))?.kind === "pod",
197
202
  })
198
203
  : workspaces;
199
204
  const useVaultModel = config.model.kind === "vault";
@@ -256,6 +261,7 @@ export class TenantRuntime {
256
261
  keys,
257
262
  blobs,
258
263
  workspaces: sandboxWorkspaceReader(sandbox),
264
+ ...(hooks.traces ? { traces: hooks.traces } : {}),
259
265
  ...(hooks.runGrants ? { runGrants: hooks.runGrants } : {}),
260
266
  workerId: hooks.workerId ?? WORKER_ID,
261
267
  ownerLeaseMs: config.ownerLeaseMs ?? DEFAULT_OWNER_LEASE_MS,
@@ -268,10 +274,14 @@ export class TenantRuntime {
268
274
  abortLocal: (sessionId, turnId) => abortLocal(ctx, sessionId, "cancel", turnId),
269
275
  ...(hooks.pods ? { pods: hooks.pods } : {}),
270
276
  sandboxSignal: async (sandboxId, signal) => {
271
- if (ctx.closing || ctx.closed || !execution.sandbox)
272
- return;
273
- await execution.sandbox(config.tenantId, sandboxId, signal);
277
+ if (ctx.closing || ctx.closed)
278
+ return false;
279
+ await execution.sandbox?.(config.tenantId, sandboxId, signal);
280
+ return true;
274
281
  },
282
+ send: (request) => request.kind === "wake"
283
+ ? ctx.wake(request.sessionId, request.wake)
284
+ : ctx.sandboxSignal(request.sandboxId, request.signal),
275
285
  ...(hooks.execution?.stuckInvocations
276
286
  ? {
277
287
  stuckInvocations: () => hooks.execution.stuckInvocations(config.tenantId),
@@ -283,6 +293,22 @@ export class TenantRuntime {
283
293
  return () => sweepHooks.delete(hook);
284
294
  },
285
295
  };
296
+ // What a commit asks of the execution goes without the request waiting; a send that
297
+ // fails is the sweep's to send again, so it is only a warning here.
298
+ opened.deliverTo(async (request) => {
299
+ try {
300
+ return await ctx.send(request);
301
+ }
302
+ catch (error) {
303
+ config.logger.warn("outbox delivery after commit failed; the sweep sends it again", {
304
+ ...(request.kind === "wake"
305
+ ? { sessionId: request.sessionId, reason: request.wake.reason }
306
+ : { sandboxId: request.sandboxId, signal: request.signal.kind }),
307
+ message: error instanceof Error ? error.message : String(error),
308
+ });
309
+ return false;
310
+ }
311
+ });
286
312
  harnessServer = createHarnessApiServer(ctx, { sandboxPreference: preference });
287
313
  if (mcp && manager)
288
314
  harness = await startInProcessHarness(ctx, { mcp, sandbox: manager }, {
@@ -5,8 +5,15 @@ export interface WorkState {
5
5
  readonly running: Map<string, AbortController>;
6
6
  /** The turn each running advance belongs to, by session id (`null`: none). */
7
7
  readonly runningTurns: Map<string, string | null>;
8
+ /** Called once when the last running advance ends (`endAdvance`), then forgotten. */
9
+ readonly idleWaiters: Set<() => void>;
8
10
  }
9
11
  export declare function createWorkState(): WorkState;
12
+ /**
13
+ * Advance `id`'s end on this process: forgets its controller (unless a later advance of the
14
+ * session replaced it) and, when it was the last, settles every `idle` wait.
15
+ */
16
+ export declare function endAdvance(work: WorkState, id: string, controller: AbortController): void;
10
17
  /**
11
18
  * Abort the advance of `id` if it runs on this process.
12
19
  * Cancel calls it after committing `cancelled`, and so does the control bus for cancels made
@@ -10,10 +10,45 @@
10
10
  * advance instead of settling it.
11
11
  */
12
12
  import { randomUUID } from "node:crypto";
13
+ import { sleep } from "../retry.js";
13
14
  import { command } from "./commands.js";
14
15
  import { AdvanceAbort } from "./worker.js";
15
16
  export function createWorkState() {
16
- return { running: new Map(), runningTurns: new Map() };
17
+ return { running: new Map(), runningTurns: new Map(), idleWaiters: new Set() };
18
+ }
19
+ /**
20
+ * Advance `id`'s end on this process: forgets its controller (unless a later advance of the
21
+ * session replaced it) and, when it was the last, settles every `idle` wait.
22
+ */
23
+ export function endAdvance(work, id, controller) {
24
+ if (work.running.get(id) !== controller)
25
+ return;
26
+ work.running.delete(id);
27
+ work.runningTurns.delete(id);
28
+ if (work.running.size > 0)
29
+ return;
30
+ for (const waiter of work.idleWaiters)
31
+ waiter();
32
+ work.idleWaiters.clear();
33
+ }
34
+ /** Resolves once no advance runs on this process, or after `timeoutMs`, whichever is first. */
35
+ async function idle(work, timeoutMs) {
36
+ if (work.running.size === 0)
37
+ return;
38
+ const timer = new AbortController();
39
+ let settle;
40
+ const ended = new Promise((resolve) => (settle = resolve));
41
+ work.idleWaiters.add(settle);
42
+ try {
43
+ await Promise.race([
44
+ ended,
45
+ sleep(Math.max(0, timeoutMs), timer.signal),
46
+ ]);
47
+ }
48
+ finally {
49
+ timer.abort();
50
+ work.idleWaiters.delete(settle);
51
+ }
17
52
  }
18
53
  const MESSAGES = {
19
54
  cancel: "Turn cancelled",
@@ -56,9 +91,7 @@ export async function drain(ctx, activeWork, timeoutMs) {
56
91
  principalId: "drain",
57
92
  });
58
93
  }
59
- const deadline = Date.now() + timeoutMs;
60
- while (ctx.work.running.size > 0 && Date.now() < deadline)
61
- await new Promise((resolve) => setTimeout(resolve, 10));
94
+ await idle(ctx.work, timeoutMs);
62
95
  }
63
96
  /** Abort every advance running on this process; closing the Tenant is a `shutdown`. */
64
97
  export function abortAll(ctx, kind = "shutdown") {
@@ -72,9 +105,7 @@ export function abortAll(ctx, kind = "shutdown") {
72
105
  * (§11.4); the epoch fences anything they still try to write. Returns their session ids.
73
106
  */
74
107
  export async function waitForIdle(ctx, timeoutMs) {
75
- const deadline = Date.now() + timeoutMs;
76
- while (ctx.work.running.size > 0 && Date.now() < deadline)
77
- await new Promise((resolve) => setTimeout(resolve, 10));
108
+ await idle(ctx.work, timeoutMs);
78
109
  const abandoned = [...ctx.work.running.keys()];
79
110
  if (abandoned.length > 0)
80
111
  ctx.config.logger.warn("tenant closed with advances still running", {
@@ -120,5 +120,4 @@ export declare function checkSessionStreams(ctx: TenantContext): Promise<void>;
120
120
  export declare function clearObservers(hub: SessionStreams): void;
121
121
  /** Close: end every session observer. */
122
122
  export declare function endAllStreams(hub: SessionStreams): void;
123
- export declare function sleep(ms: number, signal: AbortSignal): Promise<void>;
124
123
  export {};
@@ -20,6 +20,7 @@
20
20
  */
21
21
  import { historyCursor, historyResume } from "../reads/cursor.js";
22
22
  import { decodeCursor, encodeCursor } from "../record/index.js";
23
+ import { sleep } from "../retry.js";
23
24
  import { basinOf } from "../streams/basin.js";
24
25
  import { sessionStream, } from "../streams/types.js";
25
26
  import { fail } from "./http.js";
@@ -405,16 +406,3 @@ export function clearObservers(hub) {
405
406
  export function endAllStreams(hub) {
406
407
  clearObservers(hub);
407
408
  }
408
- export function sleep(ms, signal) {
409
- return new Promise((resolve) => {
410
- if (signal.aborted)
411
- return resolve();
412
- const done = () => {
413
- clearTimeout(timer);
414
- signal.removeEventListener("abort", done);
415
- resolve();
416
- };
417
- const timer = setTimeout(done, ms);
418
- signal.addEventListener("abort", done, { once: true });
419
- });
420
- }
@@ -1,16 +1,16 @@
1
1
  /**
2
- * Pure helpers over the session document: turn-manifest pins and variants, and state rebasing.
2
+ * Pure helpers over the session document: turn-manifest pins and variants. The engine's state
3
+ * is the harness's (R4.2): nothing here reads it.
3
4
  * No store access and no I/O. (The tools of an MCP snapshot
4
5
  * live in `mcp/snapshot.ts`.)
5
6
  *
6
7
  * Later waves: stable; the async store conversion (Wave 1 / A) does not change these.
7
8
  */
8
9
  import type { AgentManifest } from "@nylorun/core/define";
9
- import { type TurnManifestStore } from "../core/turn-manifest.js";
10
+ import type { TurnManifestStore } from "../core/turn-manifest.js";
10
11
  import type { Session } from "./context.js";
11
12
  export { manifestFor, sessionToolsOf } from "../mcp/snapshot.js";
12
13
  /** Turn-manifest store backed by the session's `variants` map (no schema change). */
13
14
  export declare function variantStore(session: Session): TurnManifestStore;
14
15
  /** Manifest this turn's checkpoint is pinned to (session pin or a stored variant). */
15
16
  export declare function turnManifestOf(session: Session): AgentManifest;
16
- export declare function rebaseSessionState(session: Session, turnHash: string): void;
@@ -1,4 +1,3 @@
1
- import { allowedManifestHashes, rebaseTurnState, } from "../core/turn-manifest.js";
2
1
  export { manifestFor, sessionToolsOf } from "../mcp/snapshot.js";
3
2
  /** Turn-manifest store backed by the session's `variants` map (no schema change). */
4
3
  export function variantStore(session) {
@@ -18,16 +17,3 @@ export function turnManifestOf(session) {
18
17
  return session.manifest;
19
18
  return session.variants?.[hash] ?? session.manifest;
20
19
  }
21
- export function rebaseSessionState(session, turnHash) {
22
- const allowed = allowedManifestHashes({
23
- pinnedHash: session.manifestHash,
24
- variantHashes: Object.keys(session.variants ?? {}),
25
- });
26
- const next = rebaseTurnState({
27
- state: session.state,
28
- turnManifestHash: turnHash,
29
- isAllowedHash: (hash) => allowed.has(hash),
30
- });
31
- if (next !== session.state)
32
- session.state = next;
33
- }
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Session and definition resources: `PUT/GET /v1/agents`, `PUT /v1/sessions/:id` (create or
3
- * re-attach vaults), the session list and the session view. Reads use the store's typed
4
- * queries; `PUT /v1/sessions/:id` runs in one transaction with the vault attachment checks.
2
+ * Session and definition resources: `PUT/GET /v1/agents`, `PUT /v1/sessions/:id` (create, or
3
+ * find an identical one), the session list and the session view. Reads use the store's typed
4
+ * queries; `PUT /v1/sessions/:id` runs in one transaction with the Environment's checks.
5
5
  */
6
6
  import { type PutAgentRequest, type PutSessionRequest } from "@nylorun/core/contracts";
7
7
  import type { Tx } from "../store/types.js";
@@ -44,14 +44,14 @@ export declare function listSessions(ctx: TenantContext, agentId: string | null,
44
44
  }[];
45
45
  }>;
46
46
  /**
47
- * Create a session from its definition, or re-attach vaults to an identical one. `owner`, when
48
- * the request acts for a subject, must be the session's owner: another owner's session id is
49
- * the 404 of a missing one (not the 409 of a mismatch), and so are another owner's vaults and
50
- * sandboxes.
47
+ * Create a session from its definition, or find an identical one. `access`, when the request
48
+ * acts for a subject, must be the session's owner: another owner's session id is the 404 of a
49
+ * missing one (not the 409 of a mismatch), and so is an Environment whose owner guard names
50
+ * another owner.
51
51
  *
52
- * The session's sandbox is decided here and fixed for its life (§5 of Sandboxes v3): shared
53
- * with `{ session }`, none with `false`, inline, or the Tenant default when omitted. A definition
54
- * that declares its own sandbox (`.sandbox()`) keeps it and takes no sandbox request.
52
+ * The session's Environment is fixed for its life: its sandbox (kinds `virtual` and `pod`) is
53
+ * the session's, with the spec of the Environment's applied recipe pinned now, and its vault
54
+ * holds the session's credentials. Without one the session has no sandbox and no credentials.
55
55
  */
56
56
  export declare function putSession(ctx: TenantContext, id: string, body: PutSessionRequest, access?: SessionAccess, options?: {
57
57
  /**
@@ -60,10 +60,10 @@ export declare function putSession(ctx: TenantContext, id: string, body: PutSess
60
60
  */
61
61
  createOnly?: boolean;
62
62
  /**
63
- * The sandboxes the caller reaches (a token caller's grants, `sandboxGrantsOf`); undefined
64
- * reaches every one. `sandbox: { id }` must name one of them.
63
+ * The Environments the caller reaches (a token caller's grants, `environmentGrantsOf`);
64
+ * undefined reaches every one. `environment` must name one of them.
65
65
  */
66
- sandboxGrants?: readonly string[];
66
+ environmentGrants?: readonly string[];
67
67
  }): Promise<Session>;
68
68
  /** The session resource returned by `GET`/`PUT /v1/sessions/:id`. */
69
69
  export declare function sessionView(t: Tx, s: Session): Promise<unknown>;