open-managed-agents 0.1.0

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 (197) hide show
  1. package/LICENSE +93 -0
  2. package/README.md +248 -0
  3. package/bin/oma.mjs +499 -0
  4. package/dist/scripts/alpha-openai-compatible-fixture.mjs +134 -0
  5. package/dist/scripts/alpha-smoke-models.mjs +92 -0
  6. package/dist/scripts/alpha-smoke.mjs +581 -0
  7. package/dist/scripts/oma-doctor.js +230 -0
  8. package/dist/scripts/oma-models.js +299 -0
  9. package/dist/scripts/oma-workspaces.js +133 -0
  10. package/dist/src/control-plane/admin/auth.js +43 -0
  11. package/dist/src/control-plane/admin/routes.js +76 -0
  12. package/dist/src/control-plane/admin/service.js +102 -0
  13. package/dist/src/control-plane/admission.js +90 -0
  14. package/dist/src/control-plane/agents/routes.js +63 -0
  15. package/dist/src/control-plane/agents/service.js +687 -0
  16. package/dist/src/control-plane/agents/store.js +391 -0
  17. package/dist/src/control-plane/agents/types.js +1 -0
  18. package/dist/src/control-plane/api-constants.js +3 -0
  19. package/dist/src/control-plane/app.js +907 -0
  20. package/dist/src/control-plane/console/auth.js +68 -0
  21. package/dist/src/control-plane/console/static.js +112 -0
  22. package/dist/src/control-plane/deployment-runtime-config.js +328 -0
  23. package/dist/src/control-plane/deployment-runtime-event-coordinator.js +25 -0
  24. package/dist/src/control-plane/deployment-runtime-turn-guard.js +14 -0
  25. package/dist/src/control-plane/deployment-session-coordinator.js +25 -0
  26. package/dist/src/control-plane/deployment-session-output-coordinator.js +27 -0
  27. package/dist/src/control-plane/deployment-storage.js +354 -0
  28. package/dist/src/control-plane/egress/image.js +48 -0
  29. package/dist/src/control-plane/egress/policy.js +571 -0
  30. package/dist/src/control-plane/egress/presets.js +77 -0
  31. package/dist/src/control-plane/egress/proxy.js +72 -0
  32. package/dist/src/control-plane/egress/ssrf.js +106 -0
  33. package/dist/src/control-plane/egress/utils/debug.js +14 -0
  34. package/dist/src/control-plane/egress/vendor/http-proxy.js +335 -0
  35. package/dist/src/control-plane/egress/vendor/mitm-ca.js +226 -0
  36. package/dist/src/control-plane/egress/vendor/mitm-leaf.js +124 -0
  37. package/dist/src/control-plane/egress/vendor/parent-proxy.js +438 -0
  38. package/dist/src/control-plane/egress/vendor/request-filter.js +105 -0
  39. package/dist/src/control-plane/egress/vendor/sandbox-config.js +1 -0
  40. package/dist/src/control-plane/egress/vendor/tls-terminate-proxy.js +269 -0
  41. package/dist/src/control-plane/environments/routes.js +54 -0
  42. package/dist/src/control-plane/environments/service.js +112 -0
  43. package/dist/src/control-plane/environments/store.js +132 -0
  44. package/dist/src/control-plane/environments/types.js +1 -0
  45. package/dist/src/control-plane/errors.js +81 -0
  46. package/dist/src/control-plane/events/broadcaster.js +217 -0
  47. package/dist/src/control-plane/events/constants.js +2 -0
  48. package/dist/src/control-plane/events/persist.js +39 -0
  49. package/dist/src/control-plane/events/request.js +190 -0
  50. package/dist/src/control-plane/events/routes.js +159 -0
  51. package/dist/src/control-plane/events/runtime-helpers.js +134 -0
  52. package/dist/src/control-plane/events/service.js +1985 -0
  53. package/dist/src/control-plane/events/session-guards.js +29 -0
  54. package/dist/src/control-plane/events/sse.js +6 -0
  55. package/dist/src/control-plane/events/store.js +772 -0
  56. package/dist/src/control-plane/events/tool-persistence.js +102 -0
  57. package/dist/src/control-plane/events/types.js +21 -0
  58. package/dist/src/control-plane/files/routes.js +121 -0
  59. package/dist/src/control-plane/files/service.js +65 -0
  60. package/dist/src/control-plane/files/store-common.js +87 -0
  61. package/dist/src/control-plane/files/store-local.js +488 -0
  62. package/dist/src/control-plane/files/store-memory.js +270 -0
  63. package/dist/src/control-plane/files/store.js +2 -0
  64. package/dist/src/control-plane/files/types.js +7 -0
  65. package/dist/src/control-plane/http.js +25 -0
  66. package/dist/src/control-plane/ids.js +43 -0
  67. package/dist/src/control-plane/logging.js +263 -0
  68. package/dist/src/control-plane/models/auth-storage-backend.js +238 -0
  69. package/dist/src/control-plane/models/catalog.js +78 -0
  70. package/dist/src/control-plane/models/config-security.js +376 -0
  71. package/dist/src/control-plane/models/deployment-config.js +93 -0
  72. package/dist/src/control-plane/models/routes.js +32 -0
  73. package/dist/src/control-plane/models/service.js +138 -0
  74. package/dist/src/control-plane/observability/instruments.js +65 -0
  75. package/dist/src/control-plane/observability/metrics.js +125 -0
  76. package/dist/src/control-plane/observability/routes.js +95 -0
  77. package/dist/src/control-plane/openapi/document.js +282 -0
  78. package/dist/src/control-plane/openapi/routes.js +78 -0
  79. package/dist/src/control-plane/request-idempotency.js +85 -0
  80. package/dist/src/control-plane/secrets/envelope.js +132 -0
  81. package/dist/src/control-plane/secrets/master-key.js +44 -0
  82. package/dist/src/control-plane/secrets/routes.js +20 -0
  83. package/dist/src/control-plane/secrets/service.js +71 -0
  84. package/dist/src/control-plane/secrets/store.js +212 -0
  85. package/dist/src/control-plane/secrets/types.js +3 -0
  86. package/dist/src/control-plane/sessions/pi/custom-tools.js +146 -0
  87. package/dist/src/control-plane/sessions/pi/mcp/bridge.js +527 -0
  88. package/dist/src/control-plane/sessions/pi/mcp/client.js +157 -0
  89. package/dist/src/control-plane/sessions/pi/mcp/credential.js +28 -0
  90. package/dist/src/control-plane/sessions/pi/mcp/fetch.js +69 -0
  91. package/dist/src/control-plane/sessions/pi/mcp/probe.js +202 -0
  92. package/dist/src/control-plane/sessions/pi/mcp/runtime.js +13 -0
  93. package/dist/src/control-plane/sessions/pi/runner.js +1104 -0
  94. package/dist/src/control-plane/sessions/pi/sandbox/cma-glob.js +388 -0
  95. package/dist/src/control-plane/sessions/pi/sandbox/cma-grep.js +124 -0
  96. package/dist/src/control-plane/sessions/pi/sandbox/docker-egress.js +347 -0
  97. package/dist/src/control-plane/sessions/pi/sandbox/docker.js +1589 -0
  98. package/dist/src/control-plane/sessions/pi/sandbox/glob.js +48 -0
  99. package/dist/src/control-plane/sessions/pi/sandbox/image.js +7 -0
  100. package/dist/src/control-plane/sessions/pi/sandbox/microsandbox.js +1677 -0
  101. package/dist/src/control-plane/sessions/pi/sandbox/provider.js +561 -0
  102. package/dist/src/control-plane/sessions/pi/sandbox/selection.js +168 -0
  103. package/dist/src/control-plane/sessions/pi/span-normalizer.js +82 -0
  104. package/dist/src/control-plane/sessions/pi/tool-permissions.js +392 -0
  105. package/dist/src/control-plane/sessions/pi/translator.js +100 -0
  106. package/dist/src/control-plane/sessions/request.js +164 -0
  107. package/dist/src/control-plane/sessions/resources.js +63 -0
  108. package/dist/src/control-plane/sessions/routes.js +88 -0
  109. package/dist/src/control-plane/sessions/serialize.js +24 -0
  110. package/dist/src/control-plane/sessions/service.js +733 -0
  111. package/dist/src/control-plane/sessions/store.js +538 -0
  112. package/dist/src/control-plane/sessions/types.js +1 -0
  113. package/dist/src/control-plane/skills/archive.js +169 -0
  114. package/dist/src/control-plane/skills/routes.js +41 -0
  115. package/dist/src/control-plane/skills/service.js +29 -0
  116. package/dist/src/control-plane/skills/store.js +233 -0
  117. package/dist/src/control-plane/skills/types.js +5 -0
  118. package/dist/src/control-plane/sqlite-transaction.js +15 -0
  119. package/dist/src/control-plane/vaults/mcp-oauth-validate.js +165 -0
  120. package/dist/src/control-plane/vaults/oauth-refresh-ticker.js +43 -0
  121. package/dist/src/control-plane/vaults/oauth-refresh.js +448 -0
  122. package/dist/src/control-plane/vaults/routes.js +66 -0
  123. package/dist/src/control-plane/vaults/service.js +553 -0
  124. package/dist/src/control-plane/vaults/store.js +691 -0
  125. package/dist/src/control-plane/vaults/types.js +1 -0
  126. package/dist/src/control-plane/wake-loop.js +65 -0
  127. package/dist/src/control-plane/wiring.js +92 -0
  128. package/dist/src/control-plane/workspace.js +4 -0
  129. package/dist/src/control-plane/workspaces/store.js +158 -0
  130. package/dist/src/egress-proxy-main.js +103 -0
  131. package/dist/src/main.js +124 -0
  132. package/dist/src/types/agents.js +1 -0
  133. package/dist/src/types/common.js +1 -0
  134. package/dist/src/types/environments.js +1 -0
  135. package/dist/src/types/events.js +121 -0
  136. package/dist/src/types/files.js +1 -0
  137. package/dist/src/types/json.js +19 -0
  138. package/dist/src/types/sessions.js +1 -0
  139. package/dist/ui/managed-agents-console/README.md +52 -0
  140. package/dist/ui/managed-agents-console/docs/agents.md +23 -0
  141. package/dist/ui/managed-agents-console/docs/console.md +23 -0
  142. package/dist/ui/managed-agents-console/docs/dreams.md +5 -0
  143. package/dist/ui/managed-agents-console/docs/environments.md +27 -0
  144. package/dist/ui/managed-agents-console/docs/events.md +19 -0
  145. package/dist/ui/managed-agents-console/docs/files.md +17 -0
  146. package/dist/ui/managed-agents-console/docs/github.md +11 -0
  147. package/dist/ui/managed-agents-console/docs/integrations.md +19 -0
  148. package/dist/ui/managed-agents-console/docs/memory.md +11 -0
  149. package/dist/ui/managed-agents-console/docs/migration.md +17 -0
  150. package/dist/ui/managed-agents-console/docs/multiagent.md +11 -0
  151. package/dist/ui/managed-agents-console/docs/outcomes.md +11 -0
  152. package/dist/ui/managed-agents-console/docs/overview.md +41 -0
  153. package/dist/ui/managed-agents-console/docs/permissions.md +24 -0
  154. package/dist/ui/managed-agents-console/docs/quickstart.md +51 -0
  155. package/dist/ui/managed-agents-console/docs/reference.md +27 -0
  156. package/dist/ui/managed-agents-console/docs/sandbox-reference.md +19 -0
  157. package/dist/ui/managed-agents-console/docs/sandbox-security.md +15 -0
  158. package/dist/ui/managed-agents-console/docs/scheduled-deployments.md +5 -0
  159. package/dist/ui/managed-agents-console/docs/self-hosted-sandboxes.md +15 -0
  160. package/dist/ui/managed-agents-console/docs/session-operations.md +21 -0
  161. package/dist/ui/managed-agents-console/docs/sessions.md +21 -0
  162. package/dist/ui/managed-agents-console/docs/skills.md +15 -0
  163. package/dist/ui/managed-agents-console/docs/tools.md +23 -0
  164. package/dist/ui/managed-agents-console/docs/vaults.md +19 -0
  165. package/dist/ui/managed-agents-console/docs/webhooks.md +11 -0
  166. package/dist/ui/managed-agents-console/index.html +47 -0
  167. package/dist/ui/managed-agents-console/serve.mjs +116 -0
  168. package/dist/ui/managed-agents-console/src/agents-files.jsx +273 -0
  169. package/dist/ui/managed-agents-console/src/api.js +1043 -0
  170. package/dist/ui/managed-agents-console/src/app.jsx +633 -0
  171. package/dist/ui/managed-agents-console/src/auth.jsx +215 -0
  172. package/dist/ui/managed-agents-console/src/console.css +733 -0
  173. package/dist/ui/managed-agents-console/src/data.js +188 -0
  174. package/dist/ui/managed-agents-console/src/detail.jsx +719 -0
  175. package/dist/ui/managed-agents-console/src/docs.jsx +200 -0
  176. package/dist/ui/managed-agents-console/src/environments.jsx +365 -0
  177. package/dist/ui/managed-agents-console/src/forms.jsx +494 -0
  178. package/dist/ui/managed-agents-console/src/icons.jsx +48 -0
  179. package/dist/ui/managed-agents-console/src/skills.jsx +21 -0
  180. package/dist/ui/managed-agents-console/src/sse.js +185 -0
  181. package/dist/ui/managed-agents-console/src/states.jsx +90 -0
  182. package/dist/ui/managed-agents-console/src/tweaks-panel.jsx +541 -0
  183. package/dist/ui/managed-agents-console/src/ui.jsx +190 -0
  184. package/dist/ui/managed-agents-console/src/vaults-data.js +89 -0
  185. package/dist/ui/managed-agents-console/src/vaults.jsx +171 -0
  186. package/dist/ui/managed-agents-console/vendor/babel.min.js +4 -0
  187. package/dist/ui/managed-agents-console/vendor/react-dom.production.min.js +267 -0
  188. package/dist/ui/managed-agents-console/vendor/react.production.min.js +31 -0
  189. package/dist/ui/openapi-docs/VENDOR.md +27 -0
  190. package/dist/ui/openapi-docs/index.html +16 -0
  191. package/dist/ui/openapi-docs/swagger-initializer.js +14 -0
  192. package/dist/ui/openapi-docs/vendor/LICENSE +202 -0
  193. package/dist/ui/openapi-docs/vendor/NOTICE +2 -0
  194. package/dist/ui/openapi-docs/vendor/swagger-ui-bundle.js +2 -0
  195. package/dist/ui/openapi-docs/vendor/swagger-ui-standalone-preset.js +2 -0
  196. package/dist/ui/openapi-docs/vendor/swagger-ui.css +3 -0
  197. package/package.json +53 -0
@@ -0,0 +1,102 @@
1
+ import { materializePersistedEvents } from "./persist.js";
2
+ export function materializeToolPermissionUseRows(input) {
3
+ const useRows = materializePersistedEvents(input.workspaceId, input.sessionId, [
4
+ {
5
+ type: "agent.tool_use",
6
+ payload: {
7
+ name: input.event.name,
8
+ input: input.event.input,
9
+ evaluated_permission: input.event.evaluatedPermission,
10
+ },
11
+ },
12
+ ], input.now);
13
+ input.event.bindToolUseId(useRows[0].id, (reason) => {
14
+ input.onReleased(useRows[0].id, reason);
15
+ });
16
+ return useRows;
17
+ }
18
+ export function materializeMcpToolUseRows(input) {
19
+ const useRows = materializePersistedEvents(input.workspaceId, input.sessionId, [
20
+ {
21
+ type: "agent.mcp_tool_use",
22
+ payload: {
23
+ mcp_server_name: input.event.mcpServerName,
24
+ name: input.event.name,
25
+ input: input.event.input,
26
+ evaluated_permission: input.event.evaluatedPermission,
27
+ // Probe 47 wire parity: hosted emits null outside subagent threads.
28
+ session_thread_id: null,
29
+ },
30
+ },
31
+ ], input.now);
32
+ input.event.bindToolUseId(useRows[0].id, (reason) => {
33
+ input.onReleased(useRows[0].id, reason);
34
+ });
35
+ return useRows;
36
+ }
37
+ export function toolPermissionRuntimeChanges(input) {
38
+ return {
39
+ openedActions: input.evaluatedPermission === "ask"
40
+ ? [
41
+ {
42
+ workspaceId: input.workspaceId,
43
+ sessionId: input.sessionId,
44
+ turnId: input.turnId,
45
+ actionId: input.toolUseId,
46
+ actionType: "tool_confirmation",
47
+ now: input.now,
48
+ },
49
+ ]
50
+ : [],
51
+ turnStates: input.evaluatedPermission === "ask"
52
+ ? [
53
+ {
54
+ workspaceId: input.workspaceId,
55
+ sessionId: input.sessionId,
56
+ turnId: input.turnId,
57
+ ownerId: input.ownerId,
58
+ ownerGeneration: input.ownerGeneration,
59
+ state: "paused",
60
+ now: input.now,
61
+ },
62
+ ]
63
+ : [
64
+ {
65
+ workspaceId: input.workspaceId,
66
+ sessionId: input.sessionId,
67
+ turnId: input.turnId,
68
+ ownerId: input.ownerId,
69
+ ownerGeneration: input.ownerGeneration,
70
+ state: "running",
71
+ now: input.now,
72
+ },
73
+ ],
74
+ };
75
+ }
76
+ export function materializeMcpToolResultRows(input) {
77
+ return materializePersistedEvents(input.workspaceId, input.sessionId, [
78
+ {
79
+ type: "agent.mcp_tool_result",
80
+ payload: {
81
+ mcp_tool_use_id: input.event.mcpToolUseId,
82
+ content: input.event.content,
83
+ is_error: input.event.isError,
84
+ },
85
+ },
86
+ ], input.now);
87
+ }
88
+ export function materializeMcpConnectionFailedRows(input) {
89
+ return materializePersistedEvents(input.workspaceId, input.sessionId, [
90
+ {
91
+ type: "session.error",
92
+ payload: {
93
+ error: {
94
+ type: input.event.errorType ?? "mcp_connection_failed_error",
95
+ mcp_server_name: input.event.mcpServerName,
96
+ message: input.event.message,
97
+ retry_status: { type: input.event.retryStatus },
98
+ },
99
+ },
100
+ },
101
+ ], input.now);
102
+ }
@@ -0,0 +1,21 @@
1
+ export class RuntimeTurnOwnershipLostError extends Error {
2
+ turnId;
3
+ constructor(turnId) {
4
+ super(`Runtime turn ownership lost: ${turnId}`);
5
+ this.turnId = turnId;
6
+ this.name = "RuntimeTurnOwnershipLostError";
7
+ }
8
+ }
9
+ export class RuntimeUnsupportedSessionFileResourcesError extends Error {
10
+ constructor() {
11
+ super("Configured runtime does not support session file resources");
12
+ }
13
+ }
14
+ export function toManagedAgentsEvent(event) {
15
+ return {
16
+ id: event.id,
17
+ type: event.type,
18
+ processed_at: event.processed_at,
19
+ ...event.payload,
20
+ };
21
+ }
@@ -0,0 +1,121 @@
1
+ import { Hono } from "hono";
2
+ import { bodyLimit } from "hono/body-limit";
3
+ import { File as NodeFile } from "node:buffer";
4
+ import { invalidRequest, requestTooLarge, toApiErrorBody, } from "../errors.js";
5
+ import { parseLimit } from "../http.js";
6
+ import { workspaceIdFrom } from "../workspace.js";
7
+ export const MAX_FILE_UPLOAD_REQUEST_BYTES = 24 * 1024 * 1024;
8
+ export function filesRoutes(service, admission) {
9
+ const app = new Hono();
10
+ // 0113 D9: the upload slot must be reserved BEFORE bodyLimit. For requests
11
+ // without a content-length, Hono's bodyLimit eagerly reads the raw body
12
+ // into a wrapped stream, so a gate placed after it (or in the handler)
13
+ // admits up to 24 MiB of buffering per rejected request.
14
+ app.use("/", async (c, next) => {
15
+ if (c.req.method !== "POST" || admission === undefined) {
16
+ await next();
17
+ return;
18
+ }
19
+ const releaseUpload = admission.uploads.acquire(workspaceIdFrom(c));
20
+ try {
21
+ await next();
22
+ }
23
+ finally {
24
+ releaseUpload();
25
+ }
26
+ });
27
+ app.use("/", bodyLimit({
28
+ maxSize: MAX_FILE_UPLOAD_REQUEST_BYTES,
29
+ onError: (c) => {
30
+ const err = requestTooLarge();
31
+ return jsonError(toApiErrorBody(err, c.get("requestId")), err.status);
32
+ },
33
+ }));
34
+ app.post("/", async (c) => {
35
+ const body = await parseMultipartBody(c.req);
36
+ const file = body.file;
37
+ if (!isUploadedFile(file)) {
38
+ throw invalidRequest("`file` is required");
39
+ }
40
+ const uploaded = await service.upload(workspaceIdFrom(c), {
41
+ filename: file.name,
42
+ mimeType: file.type,
43
+ body: new Uint8Array(await file.arrayBuffer()),
44
+ });
45
+ return c.json(uploaded, 200);
46
+ });
47
+ app.get("/", async (c) => {
48
+ const limit = parseLimit(c.req.query("limit"));
49
+ const afterId = c.req.query("after_id") || undefined;
50
+ const beforeId = c.req.query("before_id") || undefined;
51
+ const scopeId = c.req.query("scope_id") || undefined;
52
+ return c.json(await service.list(workspaceIdFrom(c), {
53
+ limit,
54
+ afterId,
55
+ beforeId,
56
+ scopeId,
57
+ }), 200);
58
+ });
59
+ app.get("/:id", async (c) => {
60
+ return c.json(await service.retrieveMetadata(workspaceIdFrom(c), c.req.param("id")), 200);
61
+ });
62
+ app.get("/:id/content", async (c) => {
63
+ const download = await service.download(workspaceIdFrom(c), c.req.param("id"));
64
+ return new Response(asyncIterableToReadableStream(download.body), {
65
+ status: 200,
66
+ headers: {
67
+ "content-type": download.mimeType,
68
+ "content-length": String(download.sizeBytes),
69
+ "request-id": c.get("requestId"),
70
+ },
71
+ });
72
+ });
73
+ app.delete("/:id", async (c) => {
74
+ return c.json(await service.delete(workspaceIdFrom(c), c.req.param("id")), 200);
75
+ });
76
+ return app;
77
+ }
78
+ function isUploadedFile(value) {
79
+ return (value instanceof NodeFile ||
80
+ (typeof value === "object" &&
81
+ value !== null &&
82
+ "arrayBuffer" in value &&
83
+ typeof value.arrayBuffer === "function" &&
84
+ "name" in value &&
85
+ typeof value.name === "string" &&
86
+ "type" in value &&
87
+ typeof value.type === "string"));
88
+ }
89
+ async function parseMultipartBody(req) {
90
+ try {
91
+ return await req.parseBody();
92
+ }
93
+ catch (error) {
94
+ throw invalidRequest("Request body must be valid multipart/form-data", String(error));
95
+ }
96
+ }
97
+ function jsonError(body, status) {
98
+ return new Response(JSON.stringify(body), {
99
+ status,
100
+ headers: {
101
+ "content-type": "application/json; charset=UTF-8",
102
+ "request-id": body.request_id,
103
+ },
104
+ });
105
+ }
106
+ function asyncIterableToReadableStream(body) {
107
+ const iterator = body[Symbol.asyncIterator]();
108
+ return new ReadableStream({
109
+ async pull(controller) {
110
+ const next = await iterator.next();
111
+ if (next.done) {
112
+ controller.close();
113
+ return;
114
+ }
115
+ controller.enqueue(next.value);
116
+ },
117
+ async cancel(reason) {
118
+ await iterator.throw?.(reason);
119
+ },
120
+ });
121
+ }
@@ -0,0 +1,65 @@
1
+ import { invalidRequest, notFound } from "../errors.js";
2
+ export class DefaultFileService {
3
+ storage;
4
+ constructor(storage) {
5
+ this.storage = storage;
6
+ }
7
+ async upload(workspaceId, input) {
8
+ const record = await this.storage.create(workspaceId, {
9
+ filename: normalizeFilename(input.filename),
10
+ mimeType: normalizeMimeType(input.mimeType),
11
+ body: input.body,
12
+ });
13
+ return record.metadata;
14
+ }
15
+ async retrieveMetadata(workspaceId, fileId) {
16
+ const record = await this.storage.retrieveMetadata(workspaceId, fileId);
17
+ if (!record) {
18
+ throw notFound(`File ${fileId} not found`);
19
+ }
20
+ return record.metadata;
21
+ }
22
+ async download(workspaceId, fileId) {
23
+ const metadata = await this.retrieveMetadata(workspaceId, fileId);
24
+ if (!metadata.downloadable) {
25
+ throw invalidRequest(`File '${fileId}' is not downloadable`);
26
+ }
27
+ const body = await this.storage.openBytes(workspaceId, fileId);
28
+ if (!body) {
29
+ throw notFound(`File ${fileId} not found`);
30
+ }
31
+ return {
32
+ filename: metadata.filename,
33
+ mimeType: metadata.mime_type,
34
+ sizeBytes: metadata.size_bytes,
35
+ body,
36
+ };
37
+ }
38
+ async delete(workspaceId, fileId) {
39
+ const deleted = await this.storage.delete(workspaceId, fileId);
40
+ if (!deleted) {
41
+ throw notFound(`File ${fileId} not found`);
42
+ }
43
+ return { id: fileId, type: "file_deleted" };
44
+ }
45
+ async list(workspaceId, opts = {}) {
46
+ const page = await this.storage.list(workspaceId, opts);
47
+ return {
48
+ data: page.data.map((record) => record.metadata),
49
+ has_more: page.has_more,
50
+ first_id: page.first_id,
51
+ last_id: page.last_id,
52
+ };
53
+ }
54
+ }
55
+ function normalizeFilename(filename) {
56
+ const trimmed = filename.trim();
57
+ if (trimmed.length === 0) {
58
+ throw invalidRequest("Uploaded file must include a filename");
59
+ }
60
+ return trimmed;
61
+ }
62
+ function normalizeMimeType(mimeType) {
63
+ const trimmed = mimeType.trim();
64
+ return trimmed.length === 0 ? "application/octet-stream" : trimmed;
65
+ }
@@ -0,0 +1,87 @@
1
+ import { createHash } from "node:crypto";
2
+ import { invalidRequest } from "../errors.js";
3
+ export function reusableOutput(stored, file, sizeBytes, sha256) {
4
+ if (!stored)
5
+ return undefined;
6
+ if (stored.sha256 !== sha256)
7
+ return undefined;
8
+ if (stored.metadata.size_bytes !== sizeBytes)
9
+ return undefined;
10
+ if (stored.metadata.filename !== file.filename)
11
+ return undefined;
12
+ return stored;
13
+ }
14
+ export async function consumeUploadBody(body, maxBytes, label) {
15
+ const hash = createHash("sha256");
16
+ const chunks = [];
17
+ let sizeBytes = 0;
18
+ for await (const chunk of chunksOf(body)) {
19
+ sizeBytes += chunk.byteLength;
20
+ if (sizeBytes > maxBytes) {
21
+ throw invalidRequest(`${label} exceeds the ${limitLabel(maxBytes)} per-file limit`);
22
+ }
23
+ hash.update(chunk);
24
+ chunks.push(chunk);
25
+ }
26
+ return {
27
+ bytes: concat(chunks, sizeBytes),
28
+ sizeBytes,
29
+ sha256: hash.digest("hex"),
30
+ };
31
+ }
32
+ export function validateOutputFilename(filename, maxBytes) {
33
+ const bytes = new TextEncoder().encode(filename).byteLength;
34
+ if (bytes === 0) {
35
+ throw invalidRequest("Session output filename must not be empty");
36
+ }
37
+ if (filename.includes("/") || filename.includes("\\")) {
38
+ throw invalidRequest(`Session output filename must be a basename: ${filename}`);
39
+ }
40
+ if (bytes > maxBytes) {
41
+ throw invalidRequest(`Session output filename exceeds the ${maxBytes} byte limit`);
42
+ }
43
+ }
44
+ async function* chunksOf(body) {
45
+ if (body instanceof Uint8Array) {
46
+ yield body;
47
+ return;
48
+ }
49
+ for await (const chunk of body) {
50
+ yield chunk;
51
+ }
52
+ }
53
+ function concat(chunks, totalLength) {
54
+ const out = new Uint8Array(totalLength);
55
+ let offset = 0;
56
+ for (const chunk of chunks) {
57
+ out.set(chunk, offset);
58
+ offset += chunk.byteLength;
59
+ }
60
+ return out;
61
+ }
62
+ export function normalizeLimit(limit) {
63
+ if (limit === undefined)
64
+ return 20;
65
+ if (!Number.isSafeInteger(limit) || limit <= 0)
66
+ return 20;
67
+ return Math.min(limit, 1000);
68
+ }
69
+ export function limitLabel(bytes) {
70
+ if (bytes % (1024 * 1024) === 0)
71
+ return `${bytes / (1024 * 1024)} MiB`;
72
+ return `${bytes} bytes`;
73
+ }
74
+ export function toRecord(stored) {
75
+ return {
76
+ workspace_id: stored.workspace_id,
77
+ storage_key: stored.storage_key,
78
+ sha256: stored.sha256,
79
+ metadata: { ...stored.metadata },
80
+ };
81
+ }
82
+ export function compareRecords(a, b) {
83
+ return compareId(a.metadata.id, b.metadata.id);
84
+ }
85
+ export function compareId(a, b) {
86
+ return a.localeCompare(b);
87
+ }