@nylorun/runtime 0.15.1-beta → 0.16.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 (220) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/README.md +33 -6
  3. package/dist/adapters/execution/restate.d.ts +9 -1
  4. package/dist/adapters/execution/restate.js +64 -5
  5. package/dist/adapters/replication/pgoutput.js +3 -1
  6. package/dist/adapters/sandbox/local.d.ts +10 -0
  7. package/dist/adapters/sandbox/local.js +195 -0
  8. package/dist/adapters/sandbox/virtual.js +36 -1
  9. package/dist/admin-openapi.json +37 -3
  10. package/dist/api/a2a/tasks.js +6 -1
  11. package/dist/api/ag-ui/history.js +6 -1
  12. package/dist/api/ag-ui/routes.js +5 -1
  13. package/dist/api/components.d.ts +2673 -655
  14. package/dist/api/components.js +17 -1
  15. package/dist/api/gate/keys.d.ts +4 -3
  16. package/dist/api/gate/routes.d.ts +19 -3
  17. package/dist/api/gate/routes.js +162 -53
  18. package/dist/api/http/app.js +4 -0
  19. package/dist/api/http/define.d.ts +10 -0
  20. package/dist/api/http/define.js +2 -1
  21. package/dist/api/http/routes/artifacts.d.ts +11 -0
  22. package/dist/api/http/routes/artifacts.js +518 -0
  23. package/dist/api/http/routes/sandboxes.d.ts +9 -0
  24. package/dist/api/http/routes/sandboxes.js +152 -0
  25. package/dist/api/http/routes/sessions.js +3 -1
  26. package/dist/api/http/routes/tenant.js +36 -2
  27. package/dist/artifacts/config.d.ts +23 -0
  28. package/dist/artifacts/config.js +27 -0
  29. package/dist/artifacts/export.d.ts +8 -0
  30. package/dist/artifacts/export.js +205 -0
  31. package/dist/artifacts/files.d.ts +36 -0
  32. package/dist/artifacts/files.js +45 -0
  33. package/dist/artifacts/folders.d.ts +60 -0
  34. package/dist/artifacts/folders.js +209 -0
  35. package/dist/artifacts/links.d.ts +28 -0
  36. package/dist/artifacts/links.js +92 -0
  37. package/dist/artifacts/media-types.d.ts +13 -0
  38. package/dist/artifacts/media-types.js +80 -0
  39. package/dist/artifacts/parts.d.ts +34 -0
  40. package/dist/artifacts/parts.js +38 -0
  41. package/dist/artifacts/service.d.ts +74 -0
  42. package/dist/artifacts/service.js +301 -0
  43. package/dist/artifacts/workspace.d.ts +60 -0
  44. package/dist/artifacts/workspace.js +57 -0
  45. package/dist/artifacts/zip.d.ts +10 -0
  46. package/dist/artifacts/zip.js +126 -0
  47. package/dist/blob/fs.d.ts +13 -0
  48. package/dist/blob/fs.js +194 -0
  49. package/dist/blob/index.d.ts +9 -0
  50. package/dist/blob/index.js +9 -0
  51. package/dist/blob/s3.d.ts +42 -0
  52. package/dist/blob/s3.js +366 -0
  53. package/dist/blob/types.d.ts +108 -0
  54. package/dist/blob/types.js +85 -0
  55. package/dist/core/sandbox-routes.d.ts +2 -2
  56. package/dist/core/sandbox-routes.js +4 -3
  57. package/dist/execution/memory.d.ts +11 -1
  58. package/dist/execution/memory.js +77 -3
  59. package/dist/execution/types.d.ts +54 -1
  60. package/dist/execution/types.js +31 -0
  61. package/dist/gates/contract.d.ts +14 -13
  62. package/dist/gates/contract.js +13 -12
  63. package/dist/gates/egress.d.ts +72 -0
  64. package/dist/gates/egress.js +319 -0
  65. package/dist/gates/handler.d.ts +3 -0
  66. package/dist/gates/handler.js +3 -2
  67. package/dist/gates/http-client.d.ts +3 -2
  68. package/dist/gates/http-client.js +17 -6
  69. package/dist/gates/in-process.d.ts +13 -1
  70. package/dist/gates/in-process.js +11 -0
  71. package/dist/gates/inflight.d.ts +23 -4
  72. package/dist/gates/inflight.js +18 -4
  73. package/dist/gates/model-gate.d.ts +6 -2
  74. package/dist/gates/tool-calls.d.ts +9 -5
  75. package/dist/gates/tool-calls.js +6 -6
  76. package/dist/gates/tool-client.d.ts +9 -2
  77. package/dist/gates/tool-client.js +56 -22
  78. package/dist/gates/tool-contract.d.ts +12 -7
  79. package/dist/gates/tool-contract.js +10 -4
  80. package/dist/gates/tool-gate.d.ts +1 -0
  81. package/dist/harness/calls.d.ts +52 -0
  82. package/dist/harness/calls.js +113 -0
  83. package/dist/harness/executors.d.ts +22 -0
  84. package/dist/harness/executors.js +96 -0
  85. package/dist/harness/main.d.ts +6 -0
  86. package/dist/harness/main.js +103 -0
  87. package/dist/harness/pod.d.ts +21 -0
  88. package/dist/harness/pod.js +119 -0
  89. package/dist/harness/run-tokens.d.ts +18 -0
  90. package/dist/harness/run-tokens.js +15 -0
  91. package/dist/harness/service.d.ts +46 -0
  92. package/dist/harness/service.js +179 -0
  93. package/dist/harness/ws-client.d.ts +43 -0
  94. package/dist/harness/ws-client.js +122 -0
  95. package/dist/harness/ws-port.d.ts +22 -0
  96. package/dist/harness/ws-port.js +78 -0
  97. package/dist/harness-api/in-process.d.ts +24 -0
  98. package/dist/harness-api/in-process.js +119 -0
  99. package/dist/harness-api/record.d.ts +44 -0
  100. package/dist/harness-api/record.js +250 -0
  101. package/dist/harness-api/renew.d.ts +37 -0
  102. package/dist/harness-api/renew.js +77 -0
  103. package/dist/harness-api/server.d.ts +104 -0
  104. package/dist/harness-api/server.js +408 -0
  105. package/dist/harness-api/start.d.ts +36 -0
  106. package/dist/harness-api/start.js +118 -0
  107. package/dist/harness-api/workspace.d.ts +53 -0
  108. package/dist/harness-api/workspace.js +201 -0
  109. package/dist/harness-api/ws-server.d.ts +34 -0
  110. package/dist/harness-api/ws-server.js +227 -0
  111. package/dist/host/app.js +23 -2
  112. package/dist/host/gates.d.ts +27 -2
  113. package/dist/host/gates.js +46 -1
  114. package/dist/host/http.d.ts +4 -1
  115. package/dist/host/http.js +6 -1
  116. package/dist/host/main.js +135 -27
  117. package/dist/host/stack-config.d.ts +101 -1
  118. package/dist/host/stack-config.js +199 -8
  119. package/dist/mcp/snapshot.d.ts +8 -1
  120. package/dist/mcp/snapshot.js +28 -0
  121. package/dist/media.d.ts +5 -24
  122. package/dist/media.js +5 -0
  123. package/dist/model/pi-model.d.ts +6 -2
  124. package/dist/model/pi-model.js +39 -22
  125. package/dist/node/index.d.ts +1 -1
  126. package/dist/node/index.js +1 -1
  127. package/dist/openapi.json +15245 -8850
  128. package/dist/record/index.d.ts +1 -0
  129. package/dist/record/index.js +1 -0
  130. package/dist/record/sandbox.d.ts +25 -0
  131. package/dist/record/sandbox.js +43 -0
  132. package/dist/sandbox/capability.d.ts +3 -0
  133. package/dist/sandbox/capability.js +10 -0
  134. package/dist/sandbox/egress-token.d.ts +26 -0
  135. package/dist/sandbox/egress-token.js +8 -0
  136. package/dist/sandbox/join.d.ts +37 -0
  137. package/dist/sandbox/join.js +165 -0
  138. package/dist/sandbox/manager.d.ts +99 -34
  139. package/dist/sandbox/manager.js +216 -84
  140. package/dist/sandbox/placement.d.ts +27 -0
  141. package/dist/sandbox/placement.js +44 -0
  142. package/dist/sandbox/pods/client.d.ts +90 -0
  143. package/dist/sandbox/pods/client.js +79 -0
  144. package/dist/sandbox/pods/lifecycle.d.ts +91 -0
  145. package/dist/sandbox/pods/lifecycle.js +178 -0
  146. package/dist/sandbox/pods/name.d.ts +1 -0
  147. package/dist/sandbox/pods/name.js +28 -0
  148. package/dist/sandbox/pods/network-gate.d.ts +25 -0
  149. package/dist/sandbox/pods/network-gate.js +71 -0
  150. package/dist/sandbox/pods/reconcile.d.ts +11 -0
  151. package/dist/sandbox/pods/reconcile.js +152 -0
  152. package/dist/sandbox/pods/spec.d.ts +45 -0
  153. package/dist/sandbox/pods/spec.js +62 -0
  154. package/dist/sandbox/records.d.ts +29 -0
  155. package/dist/sandbox/records.js +79 -0
  156. package/dist/sandbox/resolve.d.ts +5 -0
  157. package/dist/sandbox/resolve.js +3 -2
  158. package/dist/sandbox/session-sandbox.js +7 -5
  159. package/dist/sandbox/share.d.ts +11 -0
  160. package/dist/sandbox/share.js +10 -0
  161. package/dist/sandbox/tenant-config.d.ts +16 -1
  162. package/dist/sandbox/tenant-config.js +9 -0
  163. package/dist/sandbox/types.d.ts +29 -1
  164. package/dist/sandbox/types.js +13 -1
  165. package/dist/store/postgres/drizzle/0004_sandbox_resources.sql +20 -0
  166. package/dist/store/postgres/drizzle/0005_artifacts.sql +27 -0
  167. package/dist/store/postgres/drizzle/0006_folder_artifacts.sql +12 -0
  168. package/dist/store/postgres/drizzle/0007_sandbox_pods.sql +14 -0
  169. package/dist/store/postgres/drizzle/meta/_journal.json +28 -0
  170. package/dist/store/postgres/record-writer.d.ts +6 -1
  171. package/dist/store/postgres/record-writer.js +20 -2
  172. package/dist/store/postgres/schema.d.ts +894 -0
  173. package/dist/store/postgres/schema.js +130 -2
  174. package/dist/store/postgres/store.js +289 -4
  175. package/dist/store/types.d.ts +143 -6
  176. package/dist/streams/control.d.ts +2 -0
  177. package/dist/streams/control.js +5 -0
  178. package/dist/streams/types.d.ts +10 -1
  179. package/dist/tenant/advance.js +218 -160
  180. package/dist/tenant/artifact-tool.d.ts +5 -0
  181. package/dist/tenant/artifact-tool.js +119 -0
  182. package/dist/tenant/commands.js +30 -5
  183. package/dist/tenant/context.d.ts +64 -5
  184. package/dist/tenant/effects.d.ts +19 -13
  185. package/dist/tenant/effects.js +5 -382
  186. package/dist/tenant/history.d.ts +13 -23
  187. package/dist/tenant/history.js +14 -62
  188. package/dist/tenant/host-token.d.ts +92 -0
  189. package/dist/tenant/host-token.js +179 -0
  190. package/dist/tenant/module.js +1 -0
  191. package/dist/tenant/paths.d.ts +2 -1
  192. package/dist/tenant/paths.js +3 -1
  193. package/dist/tenant/reset.d.ts +5 -2
  194. package/dist/tenant/reset.js +22 -2
  195. package/dist/tenant/run-grants.d.ts +49 -0
  196. package/dist/tenant/run-grants.js +62 -0
  197. package/dist/tenant/run-token.d.ts +70 -0
  198. package/dist/tenant/run-token.js +138 -0
  199. package/dist/tenant/runtime.d.ts +38 -0
  200. package/dist/tenant/runtime.js +120 -32
  201. package/dist/tenant/sandboxes.d.ts +119 -0
  202. package/dist/tenant/sandboxes.js +551 -0
  203. package/dist/tenant/session.d.ts +2 -13
  204. package/dist/tenant/session.js +4 -28
  205. package/dist/tenant/sessions.d.ts +5 -0
  206. package/dist/tenant/sessions.js +28 -1
  207. package/dist/tenant/status.d.ts +8 -3
  208. package/dist/tenant/status.js +5 -0
  209. package/dist/tenant/streams.d.ts +7 -0
  210. package/dist/tenant/streams.js +24 -1
  211. package/dist/tenant/sweep.js +37 -4
  212. package/dist/tenant/tokens.js +9 -2
  213. package/dist/tenant/types.d.ts +18 -1
  214. package/dist/tenant/worker.d.ts +11 -5
  215. package/dist/tenant/worker.js +12 -6
  216. package/dist/version.d.ts +1 -1
  217. package/dist/version.js +1 -1
  218. package/package.json +13 -10
  219. package/dist/adapters/media.d.ts +0 -24
  220. package/dist/adapters/media.js +0 -111
@@ -0,0 +1,92 @@
1
+ import { importJWK } from "jose";
2
+ import type { Keys } from "../keys/keys.js";
3
+ import type { SessionStore } from "../store/types.js";
4
+ export declare const HOST_TOKEN_TYP = "nylorun-host+jwt";
5
+ export declare const HOST_TOKEN_AUD = "nylorun-harness-api";
6
+ export declare const EGRESS_TOKEN_TYP = "nylorun-egress+jwt";
7
+ export declare const EGRESS_TOKEN_AUD = "nylorun-egress";
8
+ /** How long a host or egress token lives. The engine renews them before. */
9
+ export declare const HOST_TOKEN_TTL_SECONDS: number;
10
+ /** What a verified host or egress token says. */
11
+ export interface HostClaims {
12
+ readonly tenantId: string;
13
+ readonly sandboxId: string;
14
+ /** The host epoch: the fence. */
15
+ readonly epoch: number;
16
+ /** The UID of the pod the token was minted for. */
17
+ readonly podUid: string;
18
+ /** Milliseconds since the epoch. */
19
+ readonly expiresAt: number;
20
+ }
21
+ /** A host token and the egress token minted with it. */
22
+ export interface HostGrant {
23
+ readonly hostToken: string;
24
+ readonly egressToken: string;
25
+ readonly claims: HostClaims;
26
+ }
27
+ export interface HostTokenSigner {
28
+ readonly keys: Keys;
29
+ readonly config: {
30
+ readonly tenantId: string;
31
+ };
32
+ }
33
+ /** One egress token to mint: one pod of one sandbox at one host epoch. */
34
+ export interface EgressTokenRequest {
35
+ readonly tenantId: string;
36
+ readonly sandboxId: string;
37
+ /** The host epoch the join set. */
38
+ readonly epoch: number;
39
+ readonly podUid: string;
40
+ /** Seconds. Default and maximum `HOST_TOKEN_TTL_SECONDS`. */
41
+ readonly ttl?: number;
42
+ }
43
+ /**
44
+ * Mints an egress token (the only path that does): the proxy credential egress-gate accepts,
45
+ * with egress-gate's claims (no `sub`). `mintHostTokens` mints one with every host token.
46
+ */
47
+ export declare function mintEgressToken(signer: {
48
+ readonly keys: Keys;
49
+ }, request: EgressTokenRequest): Promise<{
50
+ readonly token: string;
51
+ readonly claims: HostClaims;
52
+ }>;
53
+ /** Mints the host and egress tokens of `sandboxId`'s pod `podUid` at host epoch `epoch`. */
54
+ export declare function mintHostTokens(signer: HostTokenSigner, host: {
55
+ readonly sandboxId: string;
56
+ readonly epoch: number;
57
+ readonly podUid: string;
58
+ }): Promise<HostGrant>;
59
+ /** A verified token's claims, or why it was refused (logged, never answered). */
60
+ export type HostTokenVerdict = {
61
+ readonly ok: true;
62
+ readonly claims: HostClaims;
63
+ } | {
64
+ readonly ok: false;
65
+ readonly reason: string;
66
+ };
67
+ type PublicKey = Awaited<ReturnType<typeof importJWK>>;
68
+ /** Public keys imported by key id; the key row is still read on every verification. */
69
+ export type HostTokenKeyCache = Map<string, PublicKey>;
70
+ declare const KINDS: {
71
+ readonly host: {
72
+ readonly typ: "nylorun-host+jwt";
73
+ readonly aud: "nylorun-harness-api";
74
+ };
75
+ readonly egress: {
76
+ readonly typ: "nylorun-egress+jwt";
77
+ readonly aud: "nylorun-egress";
78
+ };
79
+ };
80
+ /**
81
+ * Verifies a host (`kind: "host"`) or egress (`kind: "egress"`) token of Tenant `tenantId`:
82
+ * shape and header before any key is read, then the signature, `iss`, `aud`, `typ`, lifetime
83
+ * and claims. It does not read the sandbox: `currentHost` does.
84
+ */
85
+ export declare function verifyHostToken(store: SessionStore, tenantId: string, raw: string, kind: keyof typeof KINDS, cache?: HostTokenKeyCache): Promise<HostTokenVerdict>;
86
+ /**
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
89
+ * the token's. One indexed read of the sandbox's row.
90
+ */
91
+ export declare function staleHost(store: SessionStore, claims: HostClaims): Promise<string | undefined>;
92
+ export {};
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Host and egress tokens (F7.2, D42): the ES256 JWTs a pod sandbox's engine holds once its
3
+ * join token is exchanged (`sandbox/join.ts`). Both name one sandbox and one host epoch; a
4
+ * join, a relaunch, a loss, an expiry, a stop or a reset moves the epoch, and a token of an
5
+ * older one is refused. Signed by the keys service with the Tenant's current signing key,
6
+ * like run tokens, and told apart from them and from each other by `typ` and `aud`:
7
+ *
8
+ * ```text
9
+ * host { alg: ES256, typ: nylorun-host+jwt, kid }
10
+ * { iss: urn:nylorun:tenant:<id>, aud: nylorun-harness-api, sub: <sandboxId>,
11
+ * sbx: <sandboxId>, epc: <host epoch>, pod: <pod UID>, iat, exp, jti }
12
+ * egress { alg: ES256, typ: nylorun-egress+jwt, kid }
13
+ * { iss: urn:nylorun:tenant:<id>, aud: nylorun-egress,
14
+ * sbx: <sandboxId>, epc: <host epoch>, pod: <pod UID>, iat, exp, jti }
15
+ * ```
16
+ *
17
+ * The host token is accepted only by the Harness API listener (a connection serving that one
18
+ * sandbox, and `host.renew`); the egress token only by egress-gate, as the proxy credential
19
+ * (`Proxy-Authorization`). Neither is a bearer anywhere else: the Tenant API's bearer check
20
+ * accepts subject tokens only, and the gates run tokens only.
21
+ *
22
+ * Verification reads the key row on every call (a revoked key refuses at once) and, with
23
+ * `currentHost`, the sandbox's row (one indexed read): the epoch must be the sandbox's, the
24
+ * pod its pod, and the sandbox neither lost, expired nor deleted.
25
+ */
26
+ import { randomUUID } from "node:crypto";
27
+ import { errors, importJWK, jwtVerify } from "jose";
28
+ import { subjectTokenIssuer } from "@nylorun/core/contracts";
29
+ import { CLOCK_TOLERANCE_SECONDS, tokenKeyId } from "./jwt.js";
30
+ export const HOST_TOKEN_TYP = "nylorun-host+jwt";
31
+ export const HOST_TOKEN_AUD = "nylorun-harness-api";
32
+ export const EGRESS_TOKEN_TYP = "nylorun-egress+jwt";
33
+ export const EGRESS_TOKEN_AUD = "nylorun-egress";
34
+ /** How long a host or egress token lives. The engine renews them before. */
35
+ export const HOST_TOKEN_TTL_SECONDS = 15 * 60;
36
+ const LIFETIME_SLACK_SECONDS = 60;
37
+ /**
38
+ * Mints an egress token (the only path that does): the proxy credential egress-gate accepts,
39
+ * with egress-gate's claims (no `sub`). `mintHostTokens` mints one with every host token.
40
+ */
41
+ export async function mintEgressToken(signer, request) {
42
+ const { tenantId, sandboxId, epoch, podUid } = request;
43
+ const ttl = request.ttl ?? HOST_TOKEN_TTL_SECONDS;
44
+ if (!Number.isSafeInteger(ttl) || ttl < 1 || ttl > HOST_TOKEN_TTL_SECONDS)
45
+ throw new Error(`An egress token lives 1 to ${HOST_TOKEN_TTL_SECONDS} s, not ${ttl}`);
46
+ if (!Number.isSafeInteger(epoch) || epoch < 1)
47
+ throw new Error(`Host epoch ${epoch} is not a positive integer`);
48
+ if (sandboxId === "" || podUid === "")
49
+ throw new Error("An egress token names a sandbox and a pod");
50
+ const iat = Math.floor(Date.now() / 1000);
51
+ const exp = iat + ttl;
52
+ const { token } = await signer.keys.sign({
53
+ typ: EGRESS_TOKEN_TYP,
54
+ claims: {
55
+ iss: subjectTokenIssuer(tenantId),
56
+ aud: EGRESS_TOKEN_AUD,
57
+ sbx: sandboxId,
58
+ epc: epoch,
59
+ pod: podUid,
60
+ iat,
61
+ exp,
62
+ jti: randomUUID(),
63
+ },
64
+ });
65
+ return { token, claims: { tenantId, sandboxId, epoch, podUid, expiresAt: exp * 1000 } };
66
+ }
67
+ /** Mints the host and egress tokens of `sandboxId`'s pod `podUid` at host epoch `epoch`. */
68
+ export async function mintHostTokens(signer, host) {
69
+ const tenantId = signer.config.tenantId;
70
+ const iat = Math.floor(Date.now() / 1000);
71
+ const exp = iat + HOST_TOKEN_TTL_SECONDS;
72
+ const [hostToken, egress] = await Promise.all([
73
+ signer.keys.sign({
74
+ typ: HOST_TOKEN_TYP,
75
+ claims: {
76
+ iss: subjectTokenIssuer(tenantId),
77
+ aud: HOST_TOKEN_AUD,
78
+ sub: host.sandboxId,
79
+ sbx: host.sandboxId,
80
+ epc: host.epoch,
81
+ pod: host.podUid,
82
+ iat,
83
+ exp,
84
+ jti: randomUUID(),
85
+ },
86
+ }),
87
+ mintEgressToken(signer, { tenantId, ...host }),
88
+ ]);
89
+ return {
90
+ hostToken: hostToken.token,
91
+ egressToken: egress.token,
92
+ claims: {
93
+ tenantId,
94
+ sandboxId: host.sandboxId,
95
+ epoch: host.epoch,
96
+ podUid: host.podUid,
97
+ expiresAt: exp * 1000,
98
+ },
99
+ };
100
+ }
101
+ const KINDS = {
102
+ host: { typ: HOST_TOKEN_TYP, aud: HOST_TOKEN_AUD },
103
+ egress: { typ: EGRESS_TOKEN_TYP, aud: EGRESS_TOKEN_AUD },
104
+ };
105
+ /**
106
+ * Verifies a host (`kind: "host"`) or egress (`kind: "egress"`) token of Tenant `tenantId`:
107
+ * shape and header before any key is read, then the signature, `iss`, `aud`, `typ`, lifetime
108
+ * and claims. It does not read the sandbox: `currentHost` does.
109
+ */
110
+ export async function verifyHostToken(store, tenantId, raw, kind, cache = new Map()) {
111
+ const refused = (reason) => ({ ok: false, reason: `${kind}_token_${reason}` });
112
+ const { typ, aud } = KINDS[kind];
113
+ const checked = tokenKeyId(raw, typ);
114
+ if ("refused" in checked)
115
+ return refused(checked.refused);
116
+ const { kid } = checked;
117
+ const row = await store.tx((t) => t.signingKey(kid));
118
+ if (!row)
119
+ return refused("key_unknown");
120
+ let key = cache.get(kid);
121
+ if (!key) {
122
+ key = await importJWK({ ...JSON.parse(row.publicJwk), alg: "ES256" }, "ES256");
123
+ cache.set(kid, key);
124
+ }
125
+ let payload;
126
+ try {
127
+ const verified = await jwtVerify(raw, key, {
128
+ algorithms: ["ES256"],
129
+ issuer: subjectTokenIssuer(tenantId),
130
+ audience: aud,
131
+ typ,
132
+ clockTolerance: CLOCK_TOLERANCE_SECONDS,
133
+ requiredClaims: ["iat", "exp", "jti"],
134
+ });
135
+ payload = verified.payload;
136
+ }
137
+ catch (error) {
138
+ return refused(error instanceof errors.JWTExpired ? "expired" : "invalid");
139
+ }
140
+ if (row.state === "revoked")
141
+ return refused("key_revoked");
142
+ const { sub, sbx, epc, pod, iat, exp } = payload;
143
+ if (typeof sbx !== "string" ||
144
+ sbx === "" ||
145
+ (kind === "host" && sub !== sbx) ||
146
+ typeof pod !== "string" ||
147
+ pod === "" ||
148
+ typeof epc !== "number" ||
149
+ !Number.isSafeInteger(epc) ||
150
+ epc < 1 ||
151
+ exp - iat > HOST_TOKEN_TTL_SECONDS + LIFETIME_SLACK_SECONDS)
152
+ return refused("claims");
153
+ return {
154
+ ok: true,
155
+ claims: { tenantId, sandboxId: sbx, epoch: epc, podUid: pod, expiresAt: exp * 1000 },
156
+ };
157
+ }
158
+ /**
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
161
+ * the token's. One indexed read of the sandbox's row.
162
+ */
163
+ export async function staleHost(store, claims) {
164
+ const row = await store.tx((t) => t.sandboxResource(claims.sandboxId));
165
+ const pod = row?.pod;
166
+ if (!row || row.kind !== "pod" || !pod)
167
+ return "the sandbox is gone";
168
+ if (pod.desired === "deleted")
169
+ return "the sandbox was deleted";
170
+ if (pod.observed === "lost")
171
+ return "the sandbox was lost";
172
+ if (pod.observed === "expired")
173
+ return "the sandbox expired";
174
+ if (pod.hostEpoch !== claims.epoch)
175
+ return "another host holds the sandbox now";
176
+ if (pod.podUid !== claims.podUid)
177
+ return "the token is another pod's";
178
+ return undefined;
179
+ }
@@ -158,6 +158,7 @@ export function createTenantModule(options) {
158
158
  pendingActions: summary?.pendingActions ?? 0,
159
159
  uncertainEffects: summary?.uncertainEffects ?? 0,
160
160
  ...(relay ? { relay } : {}),
161
+ ...(summary?.harness ? { harness: summary.harness } : {}),
161
162
  };
162
163
  },
163
164
  async close() {
@@ -16,7 +16,8 @@ export declare function hostPaths(hostRoot: string): HostPaths;
16
16
  * root after realpath.
17
17
  *
18
18
  * The directory holds what stays on the Host (the Tenant's data is its database): `home/`,
19
- * `tmp/`, `sandboxes/`, `plugin-data/`, `logs/tenant.log`. The vault key is the one exception:
19
+ * `tmp/`, `sandboxes/`, `plugin-data/`, `blobs/` (the `fs` BlobStore, when the Host has no
20
+ * Object store), `logs/tenant.log`. The vault key is the one exception:
20
21
  * it lives beside the Tenant directory, in `<hostRoot>/keys/vault-kek`, which the local stack
21
22
  * mounts only into the gateway (F4.2), never into the runtime container.
22
23
  */
@@ -24,7 +24,8 @@ export function hostPaths(hostRoot) {
24
24
  * root after realpath.
25
25
  *
26
26
  * The directory holds what stays on the Host (the Tenant's data is its database): `home/`,
27
- * `tmp/`, `sandboxes/`, `plugin-data/`, `logs/tenant.log`. The vault key is the one exception:
27
+ * `tmp/`, `sandboxes/`, `plugin-data/`, `blobs/` (the `fs` BlobStore, when the Host has no
28
+ * Object store), `logs/tenant.log`. The vault key is the one exception:
28
29
  * it lives beside the Tenant directory, in `<hostRoot>/keys/vault-kek`, which the local stack
29
30
  * mounts only into the gateway (F4.2), never into the runtime container.
30
31
  */
@@ -41,6 +42,7 @@ export function tenantPaths(hostRoot) {
41
42
  tmp: join(root, "tmp"),
42
43
  sandboxes: join(root, "sandboxes"),
43
44
  pluginData: join(root, "plugin-data"),
45
+ blobs: join(root, "blobs"),
44
46
  logs: join(root, "logs"),
45
47
  log: join(root, "logs", "tenant.log"),
46
48
  };
@@ -1,11 +1,14 @@
1
1
  import type { ResetScope, SessionStore } from "../store/types.js";
2
- import type { SandboxManager } from "../sandbox/manager.js";
2
+ import type { WorkspacePort } from "../harness-api/workspace.js";
3
3
  import type { TenantPaths } from "./types.js";
4
+ import type { BlobStore } from "../blob/index.js";
4
5
  export type { ResetScope } from "../store/types.js";
5
6
  export interface ResetTenantContext {
6
7
  store: SessionStore;
7
- sandbox: SandboxManager;
8
+ sandbox: WorkspacePort;
8
9
  paths: TenantPaths;
10
+ /** Where the deleted artifacts' bytes are; they are removed after the store wipe. */
11
+ blobs?: BlobStore;
9
12
  /** Clear in-memory session observers after the store wipe. */
10
13
  clearSessionState: () => void;
11
14
  }
@@ -1,5 +1,7 @@
1
1
  import { existsSync, mkdirSync, renameSync, rmSync, writeFileSync, } from "node:fs";
2
2
  import { join } from "node:path";
3
+ import { detachAllSessions } from "./sandboxes.js";
4
+ import { contentKey } from "../artifacts/folders.js";
3
5
  /**
4
6
  * Rename `dir` to a sibling, delete the sibling, recreate an empty `dir`.
5
7
  * Failure before rename leaves the original intact; after rename the empty
@@ -39,11 +41,29 @@ export async function resetTenant(ctx, scope) {
39
41
  const clearSandboxes = scope === "sandboxes" || scope === "all";
40
42
  const clearAll = scope === "all";
41
43
  if (clearSandboxes) {
42
- await ctx.sandbox.reconcile(() => false);
44
+ await ctx.sandbox.removeAll();
43
45
  }
44
- await ctx.store.tx((t) => t.reset(scope));
46
+ // The artifacts the reset deletes: their rows go in its transaction, their bytes after it.
47
+ const blobKeys = await ctx.store.tx(async (t) => {
48
+ // Sandbox resources outlive their sessions: a sessions reset only detaches them.
49
+ if (clearSessions && !clearSandboxes)
50
+ await detachAllSessions(t);
51
+ const scoped = clearAll ? "all" : "sessions";
52
+ const keys = clearSessions ? await t.artifactBlobKeys(scoped) : [];
53
+ // Folders' content-addressed files: those no remaining folder names (F8.2).
54
+ const content = clearSessions ? await t.artifactContentShas(scoped) : [];
55
+ await t.reset(scope);
56
+ const kept = await t.referencedArtifactContent(content);
57
+ for (const sha of content)
58
+ if (!kept.has(sha))
59
+ keys.push(contentKey(sha));
60
+ return keys;
61
+ });
45
62
  if (clearSessions)
46
63
  ctx.clearSessionState();
64
+ if (ctx.blobs)
65
+ for (const key of blobKeys)
66
+ await ctx.blobs.delete(key).catch(() => undefined);
47
67
  if (clearSandboxes)
48
68
  replaceDirectory(ctx.paths.sandboxes);
49
69
  if (clearAll) {
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Run grants (F5, gate trust): the run token each advance of this process holds while it owns
3
+ * a session (`run-token.ts`). The advance mints one when it takes the lease (`grantRun`), the
4
+ * lease heartbeat re-mints it before it expires (`renewRunGrant`), and it goes when the
5
+ * advance ends or loses the lease (`dropRunGrant`). The HTTP gate clients read the session's
6
+ * token on every request (`RunTokens`), so a re-mint applies to the next call.
7
+ *
8
+ * Only a Runtime whose gates are a service has grants (`TenantContext.runGrants`). With the
9
+ * gates in this process nothing is minted (G6): the trust boundary is the gate's HTTP route.
10
+ */
11
+ import type { Lease, TenantContext } from "./context.js";
12
+ import { type RunGrant } from "./run-token.js";
13
+ /** What a gate client reads: the current run token of a session, if an advance holds one. */
14
+ export interface RunTokens {
15
+ token(sessionId: string): string | undefined;
16
+ }
17
+ /** The run grants of this process's advances, by session id. */
18
+ export interface RunGrants extends RunTokens {
19
+ get(sessionId: string): RunGrant | undefined;
20
+ set(grant: RunGrant): void;
21
+ /** Forgets the grant of `sessionId` if it is the one of `epoch`: a newer advance's stays. */
22
+ drop(sessionId: string, epoch: number): void;
23
+ }
24
+ export declare function createRunGrants(): RunGrants;
25
+ type GrantContext = Pick<TenantContext, "keys" | "config" | "runGrants">;
26
+ /** What a run token names of the session: its root agent and the turn the advance runs. */
27
+ export interface RunOf {
28
+ readonly agentId: string;
29
+ readonly activeTurnId: string | null;
30
+ }
31
+ /**
32
+ * Mints and registers the run token of the advance that just took `lease`. Nothing when the
33
+ * gates run in this process, or the session has no active turn (it makes no gate call).
34
+ *
35
+ * Never throws: an advance may make no gate call at all (a model the Tenant serves itself, a
36
+ * workflow step), so a keys service that does not answer must not stop it. Without a grant
37
+ * the session's model calls fail as when the gateway is down, its MCP requests use core's
38
+ * credential, and the heartbeat tries again (`renewRunGrant`).
39
+ */
40
+ export declare function grantRun(ctx: GrantContext, lease: Lease, run: RunOf): Promise<void>;
41
+ /**
42
+ * Re-mints the grant of `lease` when less than `RUN_TOKEN_RENEW_SECONDS` remain, or mints it
43
+ * when the first mint failed. `active` is false once the advance ended: a token minted after
44
+ * that is not registered.
45
+ */
46
+ export declare function renewRunGrant(ctx: GrantContext, lease: Lease, run: RunOf, active?: () => boolean): Promise<void>;
47
+ /** Forgets the grant of `lease` (the advance ended, or lost the lease). */
48
+ export declare function dropRunGrant(ctx: Pick<TenantContext, "runGrants">, lease: Lease): void;
49
+ export {};
@@ -0,0 +1,62 @@
1
+ import { mintRunToken, RUN_TOKEN_RENEW_SECONDS } from "./run-token.js";
2
+ export function createRunGrants() {
3
+ const grants = new Map();
4
+ return {
5
+ token: (sessionId) => grants.get(sessionId)?.token,
6
+ get: (sessionId) => grants.get(sessionId),
7
+ set(grant) {
8
+ const held = grants.get(grant.claims.sessionId);
9
+ // An older advance of the same session (a lease lost while it still ran) never replaces it.
10
+ if (held && held.claims.epoch > grant.claims.epoch)
11
+ return;
12
+ grants.set(grant.claims.sessionId, grant);
13
+ },
14
+ drop(sessionId, epoch) {
15
+ if (grants.get(sessionId)?.claims.epoch === epoch)
16
+ grants.delete(sessionId);
17
+ },
18
+ };
19
+ }
20
+ /**
21
+ * Mints and registers the run token of the advance that just took `lease`. Nothing when the
22
+ * gates run in this process, or the session has no active turn (it makes no gate call).
23
+ *
24
+ * Never throws: an advance may make no gate call at all (a model the Tenant serves itself, a
25
+ * workflow step), so a keys service that does not answer must not stop it. Without a grant
26
+ * the session's model calls fail as when the gateway is down, its MCP requests use core's
27
+ * credential, and the heartbeat tries again (`renewRunGrant`).
28
+ */
29
+ export async function grantRun(ctx, lease, run) {
30
+ if (!ctx.runGrants || !run.activeTurnId)
31
+ return;
32
+ try {
33
+ ctx.runGrants.set(await mintRunToken(ctx, lease, run));
34
+ }
35
+ catch (error) {
36
+ ctx.config.logger.warn("advance could not mint its run token", {
37
+ sessionId: lease.sessionId,
38
+ message: error instanceof Error ? error.message : String(error),
39
+ });
40
+ }
41
+ }
42
+ /**
43
+ * Re-mints the grant of `lease` when less than `RUN_TOKEN_RENEW_SECONDS` remain, or mints it
44
+ * when the first mint failed. `active` is false once the advance ended: a token minted after
45
+ * that is not registered.
46
+ */
47
+ export async function renewRunGrant(ctx, lease, run, active = () => true) {
48
+ if (!ctx.runGrants || !run.activeTurnId)
49
+ return;
50
+ const held = ctx.runGrants.get(lease.sessionId);
51
+ if (held && held.claims.epoch !== lease.epoch)
52
+ return;
53
+ if (held && held.claims.expiresAt - Date.now() > RUN_TOKEN_RENEW_SECONDS * 1000)
54
+ return;
55
+ const grant = await mintRunToken(ctx, lease, held ? { agentId: held.claims.agentId, activeTurnId: held.claims.turnId } : run);
56
+ if (active())
57
+ ctx.runGrants.set(grant);
58
+ }
59
+ /** Forgets the grant of `lease` (the advance ended, or lost the lease). */
60
+ export function dropRunGrant(ctx, lease) {
61
+ ctx.runGrants?.drop(lease.sessionId, lease.epoch);
62
+ }
@@ -0,0 +1,70 @@
1
+ import { importJWK } from "jose";
2
+ import type { Keys } from "../keys/keys.js";
3
+ import type { SessionStore } from "../store/types.js";
4
+ import type { Lease } from "./context.js";
5
+ export declare const RUN_TOKEN_TYP = "nylorun-run+jwt";
6
+ export declare const RUN_TOKEN_AUD = "nylorun-gates";
7
+ /** How long a run token lives. */
8
+ export declare const RUN_TOKEN_TTL_SECONDS: number;
9
+ /** The lease heartbeat re-mints a token with less than this left. */
10
+ export declare const RUN_TOKEN_RENEW_SECONDS: number;
11
+ /** What a verified run token says: one lease of one session's turn. */
12
+ export interface RunClaims {
13
+ readonly tenantId: string;
14
+ readonly sessionId: string;
15
+ readonly turnId: string;
16
+ /** The session's root agent: the ledger's and the agent budget's scope. */
17
+ readonly agentId: string;
18
+ /** The lease epoch: the fence. */
19
+ readonly epoch: number;
20
+ /** Milliseconds since the epoch. */
21
+ readonly expiresAt: number;
22
+ }
23
+ /** A minted run token and what it says. */
24
+ export interface RunGrant {
25
+ readonly token: string;
26
+ readonly claims: RunClaims;
27
+ }
28
+ /** Who mints: the Tenant's keys and id (a `TenantContext` is one). */
29
+ export interface RunTokenSigner {
30
+ readonly keys: Keys;
31
+ readonly config: {
32
+ readonly tenantId: string;
33
+ };
34
+ }
35
+ /**
36
+ * Mints the run token of `lease` over `session`'s active turn. Throws when the session has
37
+ * no active turn: there is no run to name.
38
+ */
39
+ export declare function mintRunToken(signer: RunTokenSigner, lease: Lease, session: {
40
+ readonly agentId: string;
41
+ readonly activeTurnId: string | null;
42
+ }): Promise<RunGrant>;
43
+ /** A verified token's claims, or why it was refused (logged, never answered). */
44
+ export type RunTokenVerdict = {
45
+ readonly ok: true;
46
+ readonly claims: RunClaims;
47
+ } | {
48
+ readonly ok: false;
49
+ readonly reason: string;
50
+ };
51
+ type PublicKey = Awaited<ReturnType<typeof importJWK>>;
52
+ /**
53
+ * Public keys imported by key id. The key row is read on every verification, so a new key
54
+ * (a rotation) and a revoked one apply at once; only the imported key object is kept.
55
+ */
56
+ export type RunTokenKeyCache = Map<string, PublicKey>;
57
+ /**
58
+ * Verifies a run token of Tenant `tenantId` against the public keys in `store`: everything a
59
+ * forger controls (size, shape, header) is checked before any key is read, then the
60
+ * signature, `iss`, `aud`, `typ`, lifetime and claims. A revoked key refuses even a token it
61
+ * signed while it was live.
62
+ */
63
+ export declare function verifyRunToken(store: SessionStore, tenantId: string, raw: string, cache?: RunTokenKeyCache): Promise<RunTokenVerdict>;
64
+ /**
65
+ * Why calls under `claims` are stale, or undefined while its lease is still the session's
66
+ * (G4): the session's epoch is the token's, its active turn is the token's, and it is not
67
+ * cancelled. One indexed read of the session row.
68
+ */
69
+ export declare function staleRun(store: SessionStore, claims: RunClaims): Promise<string | undefined>;
70
+ export {};