@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,92 @@
1
+ /**
2
+ * Capability links (protocol 6): a short-lived URL that opens one artifact version with no
3
+ * other credential, for UIs, `<img>` tags and sharing. The token in the path is an ES256 JWT
4
+ * (`typ: nylorun-artifact+jwt`) signed with the Tenant's current signing key by the keys
5
+ * service, like delivery tokens: `sub` is the artifact, `ver` the version, `aud`
6
+ * `nylorun-artifact`, and it lives at most `ARTIFACT_LINK_MAX_TTL_SECONDS` (`TOKEN_TTL_MAX_SECONDS`,
7
+ * so a key rotation never revokes a live link). A link opens nothing once its artifact
8
+ * is deleted. A folder's link (F8.2) opens its zip, or with a `path` claim one of its files.
9
+ */
10
+ import { randomUUID } from "node:crypto";
11
+ import { errors, jwtVerify } from "jose";
12
+ import { ARTIFACT_LINK_DEFAULT_TTL_SECONDS, ARTIFACT_LINK_MAX_TTL_SECONDS, ARTIFACT_LINK_TOKEN_TYPE, tenantTokenIssuer, } from "@nylorun/core/contracts";
13
+ import { isArtifactId } from "@nylorun/core/compatibility";
14
+ import { failOpaque, HttpError } from "../tenant/http.js";
15
+ import { CLOCK_TOLERANCE_SECONDS, tokenKeyId } from "../tenant/jwt.js";
16
+ /** The audience of every capability link. */
17
+ export const ARTIFACT_LINK_AUDIENCE = "nylorun-artifact";
18
+ /** Where a capability link is served: `GET /v1/artifact-links/<token>`. */
19
+ export const ARTIFACT_LINK_PATH = "/v1/artifact-links";
20
+ export async function mintArtifactLink(ctx, link) {
21
+ const ttl = Math.max(1, Math.min(Math.floor(link.expiresIn ?? ARTIFACT_LINK_DEFAULT_TTL_SECONDS), ARTIFACT_LINK_MAX_TTL_SECONDS));
22
+ const iat = Math.floor(Date.now() / 1000);
23
+ // Signed by the keys service (F4.2): this process never holds the private key.
24
+ const { token } = await ctx.keys.sign({
25
+ typ: ARTIFACT_LINK_TOKEN_TYPE,
26
+ claims: {
27
+ iss: tenantTokenIssuer(ctx.config.tenantId),
28
+ aud: ARTIFACT_LINK_AUDIENCE,
29
+ sub: link.artifactId,
30
+ ver: link.version,
31
+ ...(link.file === undefined ? {} : { path: link.file }),
32
+ iat,
33
+ exp: iat + ttl,
34
+ jti: randomUUID(),
35
+ },
36
+ });
37
+ return {
38
+ path: `${ARTIFACT_LINK_PATH}/${token}`,
39
+ artifactId: link.artifactId,
40
+ version: link.version,
41
+ ...(link.file === undefined ? {} : { file: link.file }),
42
+ expiresAt: new Date((iat + ttl) * 1000).toISOString(),
43
+ };
44
+ }
45
+ /**
46
+ * The artifact version a capability link opens. An expired link, or one whose key was revoked,
47
+ * is `401 token_expired`; anything else that does not verify is the opaque 404.
48
+ */
49
+ export async function verifyArtifactLink(ctx, raw) {
50
+ const checked = tokenKeyId(raw, ARTIFACT_LINK_TOKEN_TYPE);
51
+ if ("refused" in checked)
52
+ return refused(ctx, `artifact_link_${checked.refused}`);
53
+ const row = await ctx.store.tx((t) => t.signingKey(checked.kid));
54
+ if (!row)
55
+ return refused(ctx, "artifact_link_key_unknown");
56
+ let claims;
57
+ try {
58
+ const verified = await jwtVerify(raw, await ctx.signingKeys.publicKey(row), {
59
+ algorithms: ["ES256"],
60
+ issuer: tenantTokenIssuer(ctx.config.tenantId),
61
+ audience: ARTIFACT_LINK_AUDIENCE,
62
+ typ: ARTIFACT_LINK_TOKEN_TYPE,
63
+ clockTolerance: CLOCK_TOLERANCE_SECONDS,
64
+ requiredClaims: ["sub", "aud", "iat", "exp", "jti"],
65
+ });
66
+ claims = verified.payload;
67
+ }
68
+ catch (error) {
69
+ if (error instanceof errors.JWTExpired)
70
+ expired(ctx, "artifact_link_expired");
71
+ return refused(ctx, "artifact_link_invalid");
72
+ }
73
+ if (row.state === "revoked")
74
+ expired(ctx, "artifact_link_key_revoked");
75
+ const { sub, ver, path, exp, iat } = claims;
76
+ if (!isArtifactId(sub) ||
77
+ typeof ver !== "number" ||
78
+ !Number.isInteger(ver) ||
79
+ ver < 1 ||
80
+ (path !== undefined && (typeof path !== "string" || path === "")) ||
81
+ exp - iat > ARTIFACT_LINK_MAX_TTL_SECONDS)
82
+ return refused(ctx, "artifact_link_claims");
83
+ return { artifactId: sub, version: ver, ...(typeof path === "string" ? { file: path } : {}) };
84
+ }
85
+ function refused(ctx, reason) {
86
+ ctx.config.logger.warn("credential rejected", { reason });
87
+ return failOpaque();
88
+ }
89
+ function expired(ctx, reason) {
90
+ ctx.config.logger.warn("artifact link refused", { reason });
91
+ throw new HttpError(401, "The link has expired", { code: "token_expired" });
92
+ }
@@ -0,0 +1,13 @@
1
+ /** Images the model reads as images. */
2
+ export declare const MODEL_IMAGE_TYPES: readonly string[];
3
+ /** The largest image or text file a model call carries inline. */
4
+ export declare const MODEL_FILE_MAX_BYTES: number;
5
+ /** The type part of a `Content-Type` value, lowercased, without parameters. */
6
+ export declare function essence(contentType: string): string;
7
+ /** A declared `Content-Type` as stored, or undefined when it is not a media type. */
8
+ export declare function normalizeContentType(value: string | undefined): string | undefined;
9
+ /** The media type a file name implies, or `application/octet-stream`. */
10
+ export declare function contentTypeFor(name: string): string;
11
+ export declare function isModelImage(contentType: string): boolean;
12
+ /** Files the model reads as text: `text/*`, JSON, XML, YAML and their `+json`/`+xml` kin. */
13
+ export declare function isText(contentType: string): boolean;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Media types of file artifacts: what an upload may declare, what a name's extension implies,
3
+ * and how the model reads a file (an image as an image, text as text, anything else refused).
4
+ */
5
+ import { DEFAULT_CONTENT_TYPE } from "../blob/index.js";
6
+ const MEDIA_TYPE = /^[a-z0-9][a-z0-9!#$&^_.+-]{0,126}\/[a-z0-9][a-z0-9!#$&^_.+-]{0,126}$/;
7
+ const BY_EXTENSION = {
8
+ png: "image/png",
9
+ jpg: "image/jpeg",
10
+ jpeg: "image/jpeg",
11
+ gif: "image/gif",
12
+ webp: "image/webp",
13
+ svg: "image/svg+xml",
14
+ pdf: "application/pdf",
15
+ txt: "text/plain",
16
+ md: "text/markdown",
17
+ csv: "text/csv",
18
+ html: "text/html",
19
+ htm: "text/html",
20
+ css: "text/css",
21
+ js: "text/javascript",
22
+ mjs: "text/javascript",
23
+ ts: "text/x-typescript",
24
+ py: "text/x-python",
25
+ sh: "text/x-sh",
26
+ json: "application/json",
27
+ xml: "application/xml",
28
+ yaml: "application/yaml",
29
+ yml: "application/yaml",
30
+ zip: "application/zip",
31
+ tar: "application/x-tar",
32
+ gz: "application/gzip",
33
+ mp3: "audio/mpeg",
34
+ wav: "audio/wav",
35
+ mp4: "video/mp4",
36
+ };
37
+ /** Images the model reads as images. */
38
+ export const MODEL_IMAGE_TYPES = Object.freeze([
39
+ "image/png",
40
+ "image/jpeg",
41
+ "image/webp",
42
+ "image/gif",
43
+ ]);
44
+ /** The largest image or text file a model call carries inline. */
45
+ export const MODEL_FILE_MAX_BYTES = 8 * 1024 * 1024;
46
+ /** The type part of a `Content-Type` value, lowercased, without parameters. */
47
+ export function essence(contentType) {
48
+ return contentType.split(";")[0].trim().toLowerCase();
49
+ }
50
+ /** A declared `Content-Type` as stored, or undefined when it is not a media type. */
51
+ export function normalizeContentType(value) {
52
+ if (value === undefined)
53
+ return undefined;
54
+ const trimmed = value.trim();
55
+ if (trimmed === "" || trimmed.length > 255)
56
+ return undefined;
57
+ return MEDIA_TYPE.test(essence(trimmed)) ? trimmed : undefined;
58
+ }
59
+ /** The media type a file name implies, or `application/octet-stream`. */
60
+ export function contentTypeFor(name) {
61
+ const dot = name.lastIndexOf(".");
62
+ if (dot < 0)
63
+ return DEFAULT_CONTENT_TYPE;
64
+ return BY_EXTENSION[name.slice(dot + 1).toLowerCase()] ?? DEFAULT_CONTENT_TYPE;
65
+ }
66
+ export function isModelImage(contentType) {
67
+ return MODEL_IMAGE_TYPES.includes(essence(contentType));
68
+ }
69
+ /** Files the model reads as text: `text/*`, JSON, XML, YAML and their `+json`/`+xml` kin. */
70
+ export function isText(contentType) {
71
+ const type = essence(contentType);
72
+ return (type.startsWith("text/") ||
73
+ type === "application/json" ||
74
+ type === "application/xml" ||
75
+ type === "application/yaml" ||
76
+ type === "application/x-yaml" ||
77
+ type === "application/javascript" ||
78
+ type.endsWith("+json") ||
79
+ type.endsWith("+xml"));
80
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Message `parts` (protocol 6): what a user message's text and file parts become for the
3
+ * engine. A file part names an artifact the caller may read, of this session or Tenant-wide;
4
+ * its version is pinned when the message is accepted (the latest when the part names none).
5
+ * The engine gets an opaque media part whose `reference` is `{ artifactId, version }`: model-gate
6
+ * resolves it to bytes (`./files.ts`), and the transcript and the record never hold them.
7
+ */
8
+ import type { MessagePart } from "@nylorun/core/contracts";
9
+ import type { JsonObject, JsonValue } from "@nylorun/core/define";
10
+ import type { Tx } from "../store/types.js";
11
+ import { type ArtifactAccess } from "./service.js";
12
+ /** A user-message content part, as the engine's `MessageInput.content` takes it. */
13
+ export type EnginePart = {
14
+ readonly type: "text";
15
+ readonly text: string;
16
+ } | {
17
+ readonly type: "media";
18
+ readonly mediaType: string;
19
+ readonly reference: JsonObject;
20
+ };
21
+ /** The reference a media part carries: the artifact version, by id. */
22
+ export interface FileReference {
23
+ readonly artifactId: string;
24
+ readonly version: number;
25
+ }
26
+ export declare function fileReferenceOf(value: JsonValue | undefined): FileReference | undefined;
27
+ /**
28
+ * The parts with each file pinned to a version: for the engine (`engine`) and for a workflow's
29
+ * JSON input (`pinned`). Reads in the caller's transaction.
30
+ */
31
+ export declare function resolveMessageParts(t: Tx, parts: readonly MessagePart[], sessionId: string, access: ArtifactAccess): Promise<{
32
+ engine: EnginePart[];
33
+ pinned: MessagePart[];
34
+ }>;
@@ -0,0 +1,38 @@
1
+ import { fail } from "../tenant/http.js";
2
+ import { readableArtifact, versionOf } from "./service.js";
3
+ export function fileReferenceOf(value) {
4
+ if (!value || typeof value !== "object" || Array.isArray(value))
5
+ return undefined;
6
+ const { artifactId, version } = value;
7
+ if (typeof artifactId !== "string" || typeof version !== "number" || !Number.isInteger(version))
8
+ return undefined;
9
+ return { artifactId, version };
10
+ }
11
+ /**
12
+ * The parts with each file pinned to a version: for the engine (`engine`) and for a workflow's
13
+ * JSON input (`pinned`). Reads in the caller's transaction.
14
+ */
15
+ export async function resolveMessageParts(t, parts, sessionId, access) {
16
+ const engine = [];
17
+ const pinned = [];
18
+ for (const part of parts) {
19
+ if (part.type === "text") {
20
+ engine.push({ type: "text", text: part.text });
21
+ pinned.push(part);
22
+ continue;
23
+ }
24
+ const artifact = await readableArtifact(t, part.artifactId, access);
25
+ if (artifact.sessionId !== null && artifact.sessionId !== sessionId)
26
+ fail(400, `Artifact ${artifact.id} belongs to another session`);
27
+ if (artifact.kind !== "file")
28
+ fail(400, `Artifact ${artifact.id} is a folder; a file part names a file artifact`);
29
+ const version = await versionOf(t, artifact, part.version);
30
+ engine.push({
31
+ type: "media",
32
+ mediaType: version.contentType,
33
+ reference: { artifactId: artifact.id, version: version.version, name: artifact.name },
34
+ });
35
+ pinned.push({ type: "file", artifactId: artifact.id, version: version.version });
36
+ }
37
+ return { engine, pinned };
38
+ }
@@ -0,0 +1,74 @@
1
+ import type { ArtifactSource, ArtifactVersionView, ArtifactView, FolderManifest } from "@nylorun/core/contracts";
2
+ import { type BlobBody } from "../blob/index.js";
3
+ import type { ArtifactRow, ArtifactVersionRow, Tx } from "../store/types.js";
4
+ import { type SessionAccess, type TenantContext } from "../tenant/context.js";
5
+ /** What the caller may reach: undefined for the whole Tenant (an application principal). */
6
+ export type ArtifactAccess = SessionAccess | undefined;
7
+ export interface UploadInput {
8
+ /** A new version of this artifact; otherwise a new artifact. */
9
+ readonly artifactId?: string;
10
+ /** The new artifact's name; for a new version, it stays the artifact's. */
11
+ readonly name?: string;
12
+ /** The declared media type; default: what the name's extension implies. */
13
+ readonly contentType?: string;
14
+ /** The session a new artifact belongs to. Required when acting for a person. */
15
+ readonly sessionId?: string;
16
+ readonly labels?: Readonly<Record<string, string>>;
17
+ /** The body's declared length (`Content-Length`): one past the limits is refused unread. */
18
+ readonly declaredBytes?: number;
19
+ readonly source: ArtifactSource;
20
+ /** The turn that saved it (`save_artifact`), for the event. */
21
+ readonly turnId?: string | null;
22
+ /** The tool call that saved it, for the event. */
23
+ readonly callId?: string;
24
+ }
25
+ export interface Uploaded {
26
+ readonly artifact: ArtifactView;
27
+ readonly version: ArtifactVersionView;
28
+ }
29
+ export declare function versionView(row: ArtifactVersionRow): ArtifactVersionView;
30
+ export declare function artifactView(row: ArtifactRow, versions?: ArtifactVersionRow[]): ArtifactView;
31
+ /** An artifact name: 1–255 characters, no path separators or control characters. */
32
+ export declare function checkArtifactName(name: string | undefined): string;
33
+ /**
34
+ * The artifact if the caller may reach it, else the 404 of a missing one. Reads its session
35
+ * (without locking) to check whose it is.
36
+ */
37
+ export declare function readableArtifact(t: Tx, id: string, access: ArtifactAccess, options?: {
38
+ lock?: boolean;
39
+ }): Promise<ArtifactRow>;
40
+ /** `version`, or the latest when it is undefined; a missing one is a 404. */
41
+ export declare function versionOf(t: Tx, row: ArtifactRow, version: number | undefined): Promise<ArtifactVersionRow>;
42
+ /**
43
+ * Stores `body` as a new artifact, or a new version of one, within the Tenant's limits. See the
44
+ * module comment for how it streams and commits.
45
+ */
46
+ export declare function uploadArtifact(ctx: TenantContext, input: UploadInput, body: BlobBody, access: ArtifactAccess, signal?: AbortSignal): Promise<Uploaded>;
47
+ /** The caller's artifacts: of one session, or of every session it reaches. */
48
+ export declare function listArtifacts(ctx: TenantContext, filter: {
49
+ sessionId?: string;
50
+ }, access: ArtifactAccess): Promise<{
51
+ artifacts: ArtifactView[];
52
+ }>;
53
+ /** One artifact with every version. */
54
+ export declare function getArtifact(ctx: TenantContext, id: string, access: ArtifactAccess): Promise<ArtifactView>;
55
+ /** One version's row, for a download. */
56
+ export declare function artifactContent(ctx: TenantContext, id: string, version: number | undefined, access: ArtifactAccess): Promise<{
57
+ artifact: ArtifactRow;
58
+ version: ArtifactVersionRow;
59
+ }>;
60
+ /** One version's row, its artifact and, for a folder, its manifest; a file is a 400. */
61
+ export declare function folderContent(ctx: TenantContext, id: string, version: number | undefined, access: ArtifactAccess): Promise<{
62
+ artifact: ArtifactRow;
63
+ version: ArtifactVersionRow;
64
+ manifest: FolderManifest;
65
+ }>;
66
+ /**
67
+ * Deletes the artifact and every version: its rows and, for a session's artifact, its
68
+ * `artifact.deleted` event in one transaction, then its bytes. A folder's content-addressed
69
+ * files go too, those no other version names, under the quota lock (`folders.ts`).
70
+ */
71
+ export declare function deleteArtifact(ctx: TenantContext, id: string, access: ArtifactAccess): Promise<{
72
+ artifactId: string;
73
+ deleted: true;
74
+ }>;
@@ -0,0 +1,301 @@
1
+ /**
2
+ * File artifacts (blueprint D35, F8.1): an opaque id, a name, numbered immutable versions,
3
+ * metadata in Postgres and bytes in the Object store behind the `BlobStore` seam. Folders
4
+ * (F8.2) are written by the turn-end export (`./export.ts`, `./folders.ts`) and read here.
5
+ *
6
+ * - **Uploads stream.** The body goes to the store as it arrives, at a fresh random key, counted
7
+ * against the per-file cap and what is left of the Tenant total (`maxBytes`): nothing is
8
+ * buffered whole, and a body past either limit stores nothing (`413 limit_exceeded`).
9
+ * - **Postgres is the commit point.** One transaction writes the version row and, for a session's
10
+ * artifact, its `artifact.created` or `artifact.version.created` event through the record
11
+ * module, after re-checking the Tenant total under the quota lock. Until it commits the object
12
+ * is garbage, and a failed commit deletes it.
13
+ * - **Access mirrors sessions.** An application principal reaches every artifact. A request acting
14
+ * for a person (a trusted issuer's token or subject headers) reaches only artifacts of that person's
15
+ * sessions; another person's, or a Tenant-wide one, is the same 404 as a missing artifact.
16
+ *
17
+ * Lock order, the same in every write: the session, then the quota lock, then the artifact row.
18
+ */
19
+ import { randomUUID } from "node:crypto";
20
+ import { ARTIFACT_NAME_MAX } from "@nylorun/core/contracts";
21
+ import { isArtifactId, newArtifactId } from "@nylorun/core/compatibility";
22
+ import { BlobTooLargeError } from "../blob/index.js";
23
+ import { lockedSession, sessionOf, } from "../tenant/context.js";
24
+ import { fail } from "../tenant/http.js";
25
+ import { readArtifactLimits } from "./config.js";
26
+ import { collectContent, readManifest } from "./folders.js";
27
+ import { contentTypeFor } from "./media-types.js";
28
+ const notFound = () => fail(404, "Artifact not found");
29
+ function tooLarge(message, limit) {
30
+ return fail(413, message, { code: "limit_exceeded", details: { limitBytes: limit } });
31
+ }
32
+ /** A body past `maxBytes`: past the per-file cap, or past what is left of the Tenant total. */
33
+ function tooLargeFor(maxBytes, limits) {
34
+ return maxBytes === limits.fileBytes
35
+ ? tooLarge(`A file may hold at most ${limits.fileBytes} bytes`, limits.fileBytes)
36
+ : tooLarge(`The Tenant's artifacts may hold at most ${limits.totalBytes} bytes; this file does not fit`, limits.totalBytes);
37
+ }
38
+ export function versionView(row) {
39
+ return {
40
+ version: row.version,
41
+ size: row.size,
42
+ sha256: row.sha256,
43
+ contentType: row.contentType,
44
+ source: row.source,
45
+ createdAt: row.createdAt,
46
+ };
47
+ }
48
+ export function artifactView(row, versions) {
49
+ return {
50
+ artifactId: row.id,
51
+ kind: row.kind,
52
+ name: row.name,
53
+ contentType: row.contentType,
54
+ ...(row.sessionId === null ? {} : { sessionId: row.sessionId }),
55
+ latestVersion: row.latestVersion,
56
+ ...(row.labelsJson === null ? {} : { labels: JSON.parse(row.labelsJson) }),
57
+ createdAt: row.createdAt,
58
+ updatedAt: row.updatedAt,
59
+ ...(versions === undefined ? {} : { versions: versions.map(versionView) }),
60
+ };
61
+ }
62
+ /** An artifact name: 1–255 characters, no path separators or control characters. */
63
+ export function checkArtifactName(name) {
64
+ if (name === undefined ||
65
+ name.trim() === "" ||
66
+ name.length > ARTIFACT_NAME_MAX ||
67
+ /[/\\\u0000-\u001f\u007f]/u.test(name) ||
68
+ name === "." ||
69
+ name === "..")
70
+ return fail(400, `An artifact name is 1 to ${ARTIFACT_NAME_MAX} characters, without '/', '\\' or control characters`);
71
+ return name;
72
+ }
73
+ /**
74
+ * The artifact if the caller may reach it, else the 404 of a missing one. Reads its session
75
+ * (without locking) to check whose it is.
76
+ */
77
+ export async function readableArtifact(t, id, access, options = {}) {
78
+ if (!isArtifactId(id))
79
+ return notFound();
80
+ const row = await t.artifact(id, options);
81
+ if (!row)
82
+ return notFound();
83
+ await checkReach(t, row, access);
84
+ return row;
85
+ }
86
+ async function checkReach(t, row, access) {
87
+ if (access === undefined)
88
+ return;
89
+ if (row.sessionId === null)
90
+ notFound();
91
+ try {
92
+ await sessionOf(t, row.sessionId, access);
93
+ }
94
+ catch {
95
+ notFound();
96
+ }
97
+ }
98
+ /** `version`, or the latest when it is undefined; a missing one is a 404. */
99
+ export async function versionOf(t, row, version) {
100
+ const found = await t.artifactVersion(row.id, version ?? row.latestVersion);
101
+ return found ?? fail(404, "Artifact version not found");
102
+ }
103
+ /**
104
+ * Stores `body` as a new artifact, or a new version of one, within the Tenant's limits. See the
105
+ * module comment for how it streams and commits.
106
+ */
107
+ export async function uploadArtifact(ctx, input, body, access, signal) {
108
+ const name = input.artifactId === undefined ? checkArtifactName(input.name) : undefined;
109
+ // Where it goes and how much room is left, before a byte is stored.
110
+ const { limits, used, sessionId, fileName } = await ctx.store.tx(async (t) => {
111
+ let sessionId;
112
+ let fileName = name;
113
+ if (input.artifactId !== undefined) {
114
+ const prior = await readableArtifact(t, input.artifactId, access);
115
+ if (prior.kind !== "file")
116
+ fail(400, `Artifact ${prior.id} is a folder; its versions come from turn-end exports`);
117
+ sessionId = prior.sessionId;
118
+ fileName = prior.name;
119
+ }
120
+ else {
121
+ if (input.sessionId === undefined && access !== undefined)
122
+ fail(400, "Acting for a person, an artifact belongs to one of their sessions: name sessionId");
123
+ if (input.sessionId !== undefined)
124
+ await sessionOf(t, input.sessionId, access);
125
+ sessionId = input.sessionId ?? null;
126
+ }
127
+ return { limits: await readArtifactLimits(t), used: await t.artifactBytes(), sessionId, fileName };
128
+ });
129
+ const contentType = input.contentType ?? contentTypeFor(fileName);
130
+ const room = limits.totalBytes - used;
131
+ if (room <= 0)
132
+ tooLarge(`The Tenant's artifacts hold ${used} of ${limits.totalBytes} bytes; delete some first`, limits.totalBytes);
133
+ const maxBytes = Math.min(limits.fileBytes, room);
134
+ if (input.declaredBytes !== undefined && input.declaredBytes > maxBytes)
135
+ tooLargeFor(maxBytes, limits);
136
+ const artifactId = input.artifactId ?? newArtifactId();
137
+ const key = `artifacts/${artifactId}/${randomUUID()}`;
138
+ let stored;
139
+ try {
140
+ stored = await ctx.blobs.put(key, body, {
141
+ contentType,
142
+ maxBytes,
143
+ ...(signal ? { signal } : {}),
144
+ });
145
+ }
146
+ catch (error) {
147
+ if (error instanceof BlobTooLargeError)
148
+ tooLargeFor(maxBytes, limits);
149
+ throw error;
150
+ }
151
+ const createdAt = new Date().toISOString();
152
+ try {
153
+ return await ctx.store.tx(async (t) => {
154
+ // The session first, then the quota, then the artifact (the module's lock order).
155
+ let session;
156
+ if (sessionId !== null)
157
+ session = await lockedSession(t, sessionId, access);
158
+ await t.lockArtifactQuota();
159
+ const total = await t.artifactBytes();
160
+ if (total + stored.size > limits.totalBytes)
161
+ tooLarge(`The Tenant's artifacts may hold at most ${limits.totalBytes} bytes; this file does not fit`, limits.totalBytes);
162
+ let row;
163
+ let version;
164
+ if (input.artifactId !== undefined) {
165
+ const prior = await readableArtifact(t, input.artifactId, access, { lock: true });
166
+ version = {
167
+ artifactId: prior.id,
168
+ version: prior.latestVersion + 1,
169
+ blobKey: key,
170
+ size: stored.size,
171
+ sha256: stored.sha256,
172
+ contentType,
173
+ source: input.source,
174
+ createdAt,
175
+ };
176
+ await t.insertArtifactVersion(version);
177
+ row = {
178
+ ...prior,
179
+ latestVersion: version.version,
180
+ contentType: version.contentType,
181
+ updatedAt: createdAt,
182
+ };
183
+ }
184
+ else {
185
+ row = {
186
+ id: artifactId,
187
+ kind: "file",
188
+ name: name,
189
+ contentType,
190
+ sessionId,
191
+ latestVersion: 1,
192
+ labelsJson: input.labels === undefined || Object.keys(input.labels).length === 0
193
+ ? null
194
+ : JSON.stringify(input.labels),
195
+ createdAt,
196
+ updatedAt: createdAt,
197
+ };
198
+ version = {
199
+ artifactId,
200
+ version: 1,
201
+ blobKey: key,
202
+ size: stored.size,
203
+ sha256: stored.sha256,
204
+ contentType,
205
+ source: input.source,
206
+ createdAt,
207
+ };
208
+ await t.insertArtifact(row, version);
209
+ }
210
+ if (session)
211
+ await t.event(session.id, input.turnId ?? null, version.version === 1 ? "artifact.created" : "artifact.version.created", {
212
+ artifactId: row.id,
213
+ kind: "file",
214
+ name: row.name,
215
+ contentType: version.contentType,
216
+ version: version.version,
217
+ size: version.size,
218
+ sha256: version.sha256,
219
+ source: version.source,
220
+ ...(input.callId === undefined ? {} : { callId: input.callId }),
221
+ });
222
+ return { artifact: artifactView(row), version: versionView(version) };
223
+ });
224
+ }
225
+ catch (error) {
226
+ // Nothing references the object: it was garbage from the start.
227
+ await ctx.blobs.delete(key).catch(() => undefined);
228
+ throw error;
229
+ }
230
+ }
231
+ /** The caller's artifacts: of one session, or of every session it reaches. */
232
+ export async function listArtifacts(ctx, filter, access) {
233
+ return ctx.store.tx(async (t) => {
234
+ if (filter.sessionId !== undefined) {
235
+ await sessionOf(t, filter.sessionId, access);
236
+ return { artifacts: (await t.listArtifacts({ sessionId: filter.sessionId })).map((row) => artifactView(row)) };
237
+ }
238
+ const rows = await t.listArtifacts();
239
+ if (access === undefined)
240
+ return { artifacts: rows.map((row) => artifactView(row)) };
241
+ const owned = new Map();
242
+ const reachable = [];
243
+ for (const row of rows) {
244
+ if (row.sessionId === null)
245
+ continue;
246
+ let ok = owned.get(row.sessionId);
247
+ if (ok === undefined) {
248
+ ok = await sessionOf(t, row.sessionId, access).then(() => true, () => false);
249
+ owned.set(row.sessionId, ok);
250
+ }
251
+ if (ok)
252
+ reachable.push(row);
253
+ }
254
+ return { artifacts: reachable.map((row) => artifactView(row)) };
255
+ });
256
+ }
257
+ /** One artifact with every version. */
258
+ export async function getArtifact(ctx, id, access) {
259
+ return ctx.store.tx(async (t) => {
260
+ const row = await readableArtifact(t, id, access);
261
+ return artifactView(row, await t.artifactVersions(row.id));
262
+ });
263
+ }
264
+ /** One version's row, for a download. */
265
+ export async function artifactContent(ctx, id, version, access) {
266
+ return ctx.store.tx(async (t) => {
267
+ const artifact = await readableArtifact(t, id, access);
268
+ return { artifact, version: await versionOf(t, artifact, version) };
269
+ });
270
+ }
271
+ /** One version's row, its artifact and, for a folder, its manifest; a file is a 400. */
272
+ export async function folderContent(ctx, id, version, access) {
273
+ const found = await artifactContent(ctx, id, version, access);
274
+ if (found.artifact.kind !== "folder")
275
+ fail(400, `Artifact ${found.artifact.id} is a file; download its content`);
276
+ return { ...found, manifest: await readManifest(ctx.blobs, found.version) };
277
+ }
278
+ /**
279
+ * Deletes the artifact and every version: its rows and, for a session's artifact, its
280
+ * `artifact.deleted` event in one transaction, then its bytes. A folder's content-addressed
281
+ * files go too, those no other version names, under the quota lock (`folders.ts`).
282
+ */
283
+ export async function deleteArtifact(ctx, id, access) {
284
+ const keys = await ctx.store.tx(async (t) => {
285
+ const found = await readableArtifact(t, id, access);
286
+ if (found.sessionId !== null)
287
+ await lockedSession(t, found.sessionId, access);
288
+ if (found.kind === "folder")
289
+ await t.lockArtifactQuota();
290
+ const row = await readableArtifact(t, id, access, { lock: true });
291
+ const content = row.kind === "folder" ? await t.artifactContentShas({ artifactId: row.id }) : [];
292
+ const keys = await t.deleteArtifact(row.id);
293
+ await collectContent(ctx.blobs, t, content);
294
+ if (row.sessionId !== null)
295
+ await t.event(row.sessionId, null, "artifact.deleted", { artifactId: row.id, name: row.name });
296
+ return keys;
297
+ });
298
+ for (const key of keys)
299
+ await ctx.blobs.delete(key).catch(() => undefined);
300
+ return { artifactId: id, deleted: true };
301
+ }