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,270 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { invalidRequest } from "../errors.js";
3
+ import { newFileId } from "../ids.js";
4
+ import { MAX_SESSION_OUTPUT_BYTES, MAX_SESSION_OUTPUT_FILENAME_BYTES, MAX_SESSION_OUTPUT_FILE_BYTES, MAX_SESSION_OUTPUT_FILES, MAX_UPLOADED_FILE_BYTES, MAX_WORKSPACE_FILE_BYTES, } from "./types.js";
5
+ import { compareId, compareRecords, consumeUploadBody, limitLabel, normalizeLimit, reusableOutput, toRecord, validateOutputFilename, } from "./store-common.js";
6
+ export class InMemoryFileStorage {
7
+ files = new Map();
8
+ workspaceBytes = new Map();
9
+ maxUploadedFileBytes;
10
+ maxWorkspaceFileBytes;
11
+ maxSessionOutputFiles;
12
+ maxSessionOutputFileBytes;
13
+ maxSessionOutputBytes;
14
+ maxSessionOutputFilenameBytes;
15
+ constructor(opts = {}) {
16
+ this.maxUploadedFileBytes =
17
+ opts.maxUploadedFileBytes ?? MAX_UPLOADED_FILE_BYTES;
18
+ this.maxWorkspaceFileBytes =
19
+ opts.maxWorkspaceFileBytes ?? MAX_WORKSPACE_FILE_BYTES;
20
+ this.maxSessionOutputFiles =
21
+ opts.maxSessionOutputFiles ?? MAX_SESSION_OUTPUT_FILES;
22
+ this.maxSessionOutputFileBytes =
23
+ opts.maxSessionOutputFileBytes ?? MAX_SESSION_OUTPUT_FILE_BYTES;
24
+ this.maxSessionOutputBytes =
25
+ opts.maxSessionOutputBytes ?? MAX_SESSION_OUTPUT_BYTES;
26
+ this.maxSessionOutputFilenameBytes =
27
+ opts.maxSessionOutputFilenameBytes ?? MAX_SESSION_OUTPUT_FILENAME_BYTES;
28
+ }
29
+ async create(workspaceId, input) {
30
+ return this.createStored(workspaceId, input, {
31
+ visibility: "public",
32
+ kind: "upload",
33
+ scope: null,
34
+ scopeId: null,
35
+ storageKeySegment: "",
36
+ fileId: undefined,
37
+ downloadable: false,
38
+ relativePath: undefined,
39
+ maxBytes: this.maxUploadedFileBytes,
40
+ });
41
+ }
42
+ async createInternalSnapshot(workspaceId, input) {
43
+ return this.createStored(workspaceId, input, {
44
+ visibility: "internal",
45
+ kind: "internal_snapshot",
46
+ scope: { type: "session", id: input.scopeId },
47
+ scopeId: input.scopeId,
48
+ storageKeySegment: "internal/",
49
+ fileId: input.fileId,
50
+ downloadable: false,
51
+ relativePath: undefined,
52
+ maxBytes: this.maxUploadedFileBytes,
53
+ });
54
+ }
55
+ async createStored(workspaceId, input, opts) {
56
+ const { bytes, sizeBytes, sha256 } = await consumeUploadBody(input.body, opts.maxBytes, "Uploaded file");
57
+ const currentWorkspaceBytes = this.workspaceBytes.get(workspaceId) ?? 0;
58
+ if (currentWorkspaceBytes + sizeBytes > this.maxWorkspaceFileBytes) {
59
+ throw invalidRequest(`Workspace file storage exceeds the ${limitLabel(this.maxWorkspaceFileBytes)} in-memory limit`);
60
+ }
61
+ const id = opts.fileId ?? newFileId();
62
+ if (this.files.has(id)) {
63
+ throw invalidRequest(`File ${id} already exists`);
64
+ }
65
+ const now = new Date().toISOString();
66
+ const stored = {
67
+ visibility: opts.visibility,
68
+ kind: opts.kind,
69
+ workspace_id: workspaceId,
70
+ storage_key: `memory://${workspaceId}/${opts.storageKeySegment}${id}/${randomUUID()}`,
71
+ sha256,
72
+ bytes,
73
+ scope_id: opts.scopeId,
74
+ relative_path: opts.relativePath,
75
+ metadata: {
76
+ id,
77
+ type: "file",
78
+ filename: input.filename,
79
+ mime_type: input.mimeType,
80
+ size_bytes: sizeBytes,
81
+ created_at: now,
82
+ downloadable: opts.downloadable,
83
+ scope: opts.scope,
84
+ },
85
+ };
86
+ this.files.set(id, stored);
87
+ this.workspaceBytes.set(workspaceId, currentWorkspaceBytes + sizeBytes);
88
+ return toRecord(stored);
89
+ }
90
+ async retrieveMetadata(workspaceId, fileId) {
91
+ const stored = this.files.get(fileId);
92
+ if (!stored ||
93
+ stored.workspace_id !== workspaceId ||
94
+ stored.visibility !== "public") {
95
+ return undefined;
96
+ }
97
+ return toRecord(stored);
98
+ }
99
+ async openBytes(workspaceId, fileId) {
100
+ const stored = this.files.get(fileId);
101
+ if (!stored ||
102
+ stored.workspace_id !== workspaceId ||
103
+ stored.visibility !== "public") {
104
+ return undefined;
105
+ }
106
+ return singleChunk(stored.bytes);
107
+ }
108
+ async openInternalSnapshotBytes(workspaceId, fileId) {
109
+ const stored = this.files.get(fileId);
110
+ if (!stored ||
111
+ stored.workspace_id !== workspaceId ||
112
+ stored.visibility !== "internal") {
113
+ return undefined;
114
+ }
115
+ return singleChunk(stored.bytes);
116
+ }
117
+ async delete(workspaceId, fileId) {
118
+ const stored = this.files.get(fileId);
119
+ if (!stored ||
120
+ stored.workspace_id !== workspaceId ||
121
+ stored.visibility !== "public") {
122
+ return false;
123
+ }
124
+ this.deleteStored(workspaceId, fileId, stored);
125
+ return true;
126
+ }
127
+ async deleteInternalSnapshot(workspaceId, fileId) {
128
+ const stored = this.files.get(fileId);
129
+ if (!stored ||
130
+ stored.workspace_id !== workspaceId ||
131
+ stored.visibility !== "internal") {
132
+ return false;
133
+ }
134
+ this.deleteStored(workspaceId, fileId, stored);
135
+ return true;
136
+ }
137
+ async replaceSessionOutputs(workspaceId, sessionId, files) {
138
+ if (files.length > this.maxSessionOutputFiles) {
139
+ throw invalidRequest(`Session output collection exceeds the ${this.maxSessionOutputFiles} file limit`);
140
+ }
141
+ const oldOutputs = this.sessionOutputFiles(workspaceId, sessionId);
142
+ const reusableOutputByRelativePath = new Map(oldOutputs
143
+ .filter((stored) => stored.relative_path !== undefined)
144
+ .map((stored) => [stored.relative_path, stored]));
145
+ const basenameOwners = new Map();
146
+ const prepared = [];
147
+ let totalOutputBytes = 0;
148
+ for (const file of files) {
149
+ validateOutputFilename(file.filename, this.maxSessionOutputFilenameBytes);
150
+ const existingRelativePath = basenameOwners.get(file.filename);
151
+ if (existingRelativePath !== undefined &&
152
+ existingRelativePath !== file.relativePath) {
153
+ throw invalidRequest(`Session output filename collision for '${file.filename}' from '${existingRelativePath}' and '${file.relativePath}'`);
154
+ }
155
+ basenameOwners.set(file.filename, file.relativePath);
156
+ if (file.sizeBytes !== undefined &&
157
+ file.sizeBytes > this.maxSessionOutputFileBytes) {
158
+ throw invalidRequest(`Session output file exceeds the ${limitLabel(this.maxSessionOutputFileBytes)} per-file limit`);
159
+ }
160
+ if (file.sizeBytes !== undefined &&
161
+ totalOutputBytes + file.sizeBytes > this.maxSessionOutputBytes) {
162
+ throw invalidRequest(`Session outputs exceed the ${limitLabel(this.maxSessionOutputBytes)} aggregate limit`);
163
+ }
164
+ const { bytes, sizeBytes, sha256 } = await consumeUploadBody(file.body, this.maxSessionOutputFileBytes, "Session output file");
165
+ if (file.sizeBytes !== undefined && file.sizeBytes !== sizeBytes) {
166
+ throw invalidRequest(`Session output '${file.relativePath}' size changed during collection`);
167
+ }
168
+ if (file.sha256 !== undefined && file.sha256 !== sha256) {
169
+ throw invalidRequest(`Session output '${file.relativePath}' checksum changed during collection`);
170
+ }
171
+ totalOutputBytes += sizeBytes;
172
+ if (totalOutputBytes > this.maxSessionOutputBytes) {
173
+ throw invalidRequest(`Session outputs exceed the ${limitLabel(this.maxSessionOutputBytes)} aggregate limit`);
174
+ }
175
+ const reusable = reusableOutput(reusableOutputByRelativePath.get(file.relativePath), file, sizeBytes, sha256);
176
+ const id = reusable?.metadata.id ?? newFileId();
177
+ const createdAt = reusable?.metadata.created_at ?? new Date().toISOString();
178
+ prepared.push({
179
+ visibility: "public",
180
+ kind: "session_output",
181
+ workspace_id: workspaceId,
182
+ storage_key: reusable?.storage_key ??
183
+ `memory://${workspaceId}/outputs/${sessionId}/${id}/${randomUUID()}`,
184
+ sha256,
185
+ bytes,
186
+ scope_id: sessionId,
187
+ relative_path: file.relativePath,
188
+ metadata: {
189
+ id,
190
+ type: "file",
191
+ filename: file.filename,
192
+ mime_type: file.mimeType,
193
+ size_bytes: sizeBytes,
194
+ created_at: createdAt,
195
+ downloadable: true,
196
+ scope: { type: "session", id: sessionId },
197
+ },
198
+ });
199
+ }
200
+ const oldBytes = oldOutputs.reduce((sum, stored) => sum + stored.metadata.size_bytes, 0);
201
+ const currentWorkspaceBytes = this.workspaceBytes.get(workspaceId) ?? 0;
202
+ if (currentWorkspaceBytes - oldBytes + totalOutputBytes > this.maxWorkspaceFileBytes) {
203
+ throw invalidRequest(`Workspace file storage exceeds the ${limitLabel(this.maxWorkspaceFileBytes)} in-memory limit`);
204
+ }
205
+ for (const stored of oldOutputs) {
206
+ this.files.delete(stored.metadata.id);
207
+ }
208
+ for (const stored of prepared) {
209
+ this.files.set(stored.metadata.id, stored);
210
+ }
211
+ this.workspaceBytes.set(workspaceId, currentWorkspaceBytes - oldBytes + totalOutputBytes);
212
+ return prepared.map(toRecord).sort(compareRecords);
213
+ }
214
+ async deleteSessionOutputs(workspaceId, sessionId) {
215
+ for (const stored of this.sessionOutputFiles(workspaceId, sessionId)) {
216
+ this.deleteStored(workspaceId, stored.metadata.id, stored);
217
+ }
218
+ }
219
+ deleteStored(workspaceId, fileId, stored) {
220
+ this.files.delete(fileId);
221
+ this.workspaceBytes.set(workspaceId, Math.max(0, (this.workspaceBytes.get(workspaceId) ?? 0) -
222
+ stored.metadata.size_bytes));
223
+ }
224
+ async list(workspaceId, opts = {}) {
225
+ const limit = normalizeLimit(opts.limit);
226
+ const rows = [...this.files.values()]
227
+ .filter((file) => file.workspace_id === workspaceId)
228
+ .filter((file) => file.visibility === "public")
229
+ .filter((file) => opts.scopeId === undefined
230
+ ? file.scope_id === null
231
+ : file.kind === "session_output" && file.scope_id === opts.scopeId)
232
+ .map(toRecord)
233
+ .sort(compareRecords);
234
+ const cursorRows = opts.beforeId !== undefined
235
+ ? rows.filter((row) => compareId(row.metadata.id, opts.beforeId) < 0)
236
+ : opts.afterId !== undefined
237
+ ? rows.filter((row) => compareId(row.metadata.id, opts.afterId) > 0)
238
+ : rows;
239
+ const pageRows = opts.beforeId === undefined
240
+ ? cursorRows.slice(0, limit)
241
+ : cursorRows.slice(-limit);
242
+ return {
243
+ data: pageRows,
244
+ has_more: cursorRows.length > limit,
245
+ first_id: pageRows[0]?.metadata.id ?? null,
246
+ last_id: pageRows[pageRows.length - 1]?.metadata.id ?? null,
247
+ };
248
+ }
249
+ getInternalRecordForTest(fileId) {
250
+ const stored = this.files.get(fileId);
251
+ return stored ? toRecord(stored) : undefined;
252
+ }
253
+ getWorkspaceBytesForTest(workspaceId) {
254
+ return this.workspaceBytes.get(workspaceId) ?? 0;
255
+ }
256
+ getSessionOutputRecordsForTest(workspaceId, sessionId) {
257
+ return this.sessionOutputFiles(workspaceId, sessionId)
258
+ .map(toRecord)
259
+ .sort(compareRecords);
260
+ }
261
+ sessionOutputFiles(workspaceId, sessionId) {
262
+ return [...this.files.values()].filter((file) => file.workspace_id === workspaceId &&
263
+ file.visibility === "public" &&
264
+ file.kind === "session_output" &&
265
+ file.scope_id === sessionId);
266
+ }
267
+ }
268
+ async function* singleChunk(bytes) {
269
+ yield bytes;
270
+ }
@@ -0,0 +1,2 @@
1
+ export { InMemoryFileStorage } from "./store-memory.js";
2
+ export { LocalObjectFileStorage } from "./store-local.js";
@@ -0,0 +1,7 @@
1
+ export { DEFAULT_WORKSPACE_ID } from "../workspace.js";
2
+ export const MAX_UPLOADED_FILE_BYTES = 20 * 1024 * 1024;
3
+ export const MAX_WORKSPACE_FILE_BYTES = 100 * 1024 * 1024;
4
+ export const MAX_SESSION_OUTPUT_FILES = 100;
5
+ export const MAX_SESSION_OUTPUT_FILE_BYTES = 25 * 1024 * 1024;
6
+ export const MAX_SESSION_OUTPUT_BYTES = 100 * 1024 * 1024;
7
+ export const MAX_SESSION_OUTPUT_FILENAME_BYTES = 255;
@@ -0,0 +1,25 @@
1
+ import { invalidRequest } from "./errors.js";
2
+ export async function parseJsonBody(req) {
3
+ try {
4
+ return await req.json();
5
+ }
6
+ catch (error) {
7
+ throw invalidRequest("Request body must be valid JSON", String(error));
8
+ }
9
+ }
10
+ export function parseLimit(value) {
11
+ if (value === undefined)
12
+ return undefined;
13
+ const limit = Number(value);
14
+ if (!Number.isSafeInteger(limit) || limit <= 0) {
15
+ throw invalidRequest("`limit` must be a positive integer");
16
+ }
17
+ return limit;
18
+ }
19
+ export function parseOrder(value) {
20
+ if (value === undefined)
21
+ return undefined;
22
+ if (value === "asc" || value === "desc")
23
+ return value;
24
+ throw invalidRequest("`order` must be `asc` or `desc`");
25
+ }
@@ -0,0 +1,43 @@
1
+ import { uuidv7 } from "../types/events.js";
2
+ export function newAgentId() {
3
+ return `agent_${uuidv7()}`;
4
+ }
5
+ export function newEnvironmentId() {
6
+ return `env_${uuidv7()}`;
7
+ }
8
+ export function newFileId() {
9
+ return `file_${uuidv7()}`;
10
+ }
11
+ export function newRequestId() {
12
+ return `req_${uuidv7()}`;
13
+ }
14
+ export function newRuntimeTurnId() {
15
+ return `rtun_${uuidv7()}`;
16
+ }
17
+ export function newSessionResourceId() {
18
+ return `sesrsc_${uuidv7()}`;
19
+ }
20
+ export function newSessionSkillSnapshotId() {
21
+ return `sesskill_${uuidv7()}`;
22
+ }
23
+ export function newSessionId() {
24
+ return `sesn_${uuidv7()}`;
25
+ }
26
+ export function newSecretId() {
27
+ return `sec_${uuidv7()}`;
28
+ }
29
+ export function newSkillId() {
30
+ return `skill_${uuidv7()}`;
31
+ }
32
+ export function newSkillVersionId() {
33
+ return `skill_version_${uuidv7()}`;
34
+ }
35
+ export function newSkillContentObjectId() {
36
+ return `skobj_${uuidv7()}`;
37
+ }
38
+ export function newVaultId() {
39
+ return `vlt_${uuidv7()}`;
40
+ }
41
+ export function newVaultCredentialId() {
42
+ return `vcrd_${uuidv7()}`;
43
+ }
@@ -0,0 +1,263 @@
1
+ // Structured logging with enforced redaction (plan 0121 C1, threat-model §5).
2
+ //
3
+ // Every control-plane log line goes through this module. Redaction is
4
+ // programmatic, not a review convention: a field denylist replaces
5
+ // content-bearing keys, and a secret scrubber runs over every string value
6
+ // (not just error messages — a pasted key in a label must not survive).
7
+ // The logger must never throw, and must import cleanly in the egress
8
+ // sidecar: no dependencies beyond the runtime.
9
+ //
10
+ // Wire contract: one JSON line per event via console.debug/info/warn/error
11
+ // (looked up at emit time so test spies and stream semantics both work):
12
+ // {"ts":"…","level":"…","event":"snake_case_name", ...fields}
13
+ // audit outranks every configurable threshold — always emitted.
14
+ const LEVEL_RANK = {
15
+ debug: 10,
16
+ info: 20,
17
+ warn: 30,
18
+ error: 40,
19
+ audit: 50,
20
+ };
21
+ // Keys that carry content or credentials by construction (threat-model §5
22
+ // R1). Content names match per segment (snake/kebab/camel), so compound
23
+ // keys like tool_output or errorMessage are caught, while `context` (which
24
+ // merely contains "text") is not. Identifiers pass; anything named like key
25
+ // material does not — except digest suffixes (key_sha256 is a reference,
26
+ // not a secret; the exception is suffix-anchored so password_hash-style
27
+ // names stay denied).
28
+ const DENY_CONTENT_SEGMENTS = new Set([
29
+ "message",
30
+ "text",
31
+ "prompt",
32
+ "content",
33
+ "input",
34
+ "output",
35
+ "body",
36
+ "authorization",
37
+ ]);
38
+ const DENY_HEURISTIC = /(key|secret|token|password)/i;
39
+ const DENY_DIGEST_EXCEPTION = /(sha256|sha512|digest|fingerprint)$/i;
40
+ function keySegments(key) {
41
+ return key
42
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
43
+ .split(/[\s_-]+/)
44
+ .map((segment) => segment.toLowerCase());
45
+ }
46
+ function isDeniedKey(key) {
47
+ if (keySegments(key).some((segment) => DENY_CONTENT_SEGMENTS.has(segment))) {
48
+ return true;
49
+ }
50
+ return DENY_HEURISTIC.test(key) && !DENY_DIGEST_EXCEPTION.test(key);
51
+ }
52
+ // Credential header/kv assignments: keep the name, mask the whole value —
53
+ // including the scheme+credential form (`Authorization: Bearer <token>`),
54
+ // where a bare \S+ would stop at the scheme word and leak the token
55
+ // (C1 review, Codex P2 / Opus M1).
56
+ const HEADER_ASSIGNMENT = /(x-api-key|x-admin-key|proxy-authorization|authorization)(\s*[:=]\s*)(?:(?:Bearer|Basic|Digest)\s+)?\S+/gi;
57
+ const JSON_HEADER_ASSIGNMENT = /(["'])(x-api-key|x-admin-key|proxy-authorization|authorization)\1(\s*:\s*)(["'])(?:(?:Bearer|Basic|Digest)\s+)?[^"']+\4/gi;
58
+ // Workspace API keys: oma_ + base64url(32 bytes) (workspaces/store.ts).
59
+ const OMA_KEY = /oma_[A-Za-z0-9_-]+/g;
60
+ // Admin/master keys: base64(32 bytes) = 43 chars + optional pad, standalone.
61
+ // STANDARD base64 alphabet only (+/) — base64url (-_) would false-positive on
62
+ // prefixed UUIDs (`sesrsc_<uuid>` is exactly 43 base64url chars; found when
63
+ // the scrubber ate a resource id), and the only base64url secrets OMA mints
64
+ // are oma_-prefixed, caught above. `=` is deliberately absent from the
65
+ // lookbehind so `SOME_VAR=<key>` assignments still match; the boundary
66
+ // assertions keep hex digests and long identifier runs intact.
67
+ const BASE64_32_BYTES = /(?<![A-Za-z0-9+/])[A-Za-z0-9+/]{43}=?(?![A-Za-z0-9+/=])/g;
68
+ // Foreign credential shapes that can transit OMA via provider/tool errors
69
+ // and egress-vendor debug strings (C1 review, Opus M3 / Codex-adv). This is
70
+ // a floor, not an enumeration of the world: Anthropic/OpenAI-style sk-,
71
+ // GitHub gh?_ tokens, JWTs, AWS access key ids.
72
+ const FOREIGN_KEY_SHAPES = [
73
+ /\bsk-[A-Za-z0-9_-]{8,}/g,
74
+ /\bgh[pousr]_[A-Za-z0-9]{20,}/g,
75
+ /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*/g,
76
+ /\bAKIA[0-9A-Z]{16}\b/g,
77
+ ];
78
+ // Credential-bearing query params in logged URLs (egress debug under
79
+ // SRT_DEBUG passes raw request URLs): mask the value, keep the param name.
80
+ const QUERY_CREDENTIAL = /([?&](?:key|apikey|api_key|token|access_token|secret|password|sig|signature|credential)=)[^&\s"']+/gi;
81
+ // Exact-value scrubbing for KNOWN live secrets in server-controlled text
82
+ // (plan 0122 §7B.9 slice 0). The pattern scrubber below catches credential
83
+ // *shapes*; this catches the specific values a chokepoint already holds
84
+ // (e.g. the bearer token injected into an MCP dial, echoed back by a
85
+ // hostile server in a tool result). Common whole-value encodings are covered
86
+ // because event/log serialization can transform an otherwise verbatim echo;
87
+ // arbitrarily split or encrypted reflections remain outside this boundary.
88
+ // Values under 8 chars are ignored so a pathological 1-char "secret" cannot
89
+ // shred output.
90
+ const MIN_KNOWN_SECRET_LENGTH = 8;
91
+ export function scrubKnownSecrets(text, values) {
92
+ let out = text;
93
+ // Longest first: if one value contains another (full header vs bare
94
+ // token), the containing value must not be broken by an inner
95
+ // replacement leaving its prefix behind.
96
+ const eligible = [...new Set(values)]
97
+ .filter((value) => value.length >= MIN_KNOWN_SECRET_LENGTH)
98
+ .flatMap(knownSecretRepresentations)
99
+ .filter((value) => value.length >= MIN_KNOWN_SECRET_LENGTH)
100
+ .filter((value, index, all) => all.indexOf(value) === index)
101
+ .sort((a, b) => b.length - a.length);
102
+ for (const value of eligible) {
103
+ out = out.split(value).join("[redacted]");
104
+ }
105
+ return out;
106
+ }
107
+ export function knownSecretRepresentations(value) {
108
+ return [
109
+ value,
110
+ encodeURIComponent(value),
111
+ JSON.stringify(value).slice(1, -1),
112
+ Buffer.from(value).toString("base64"),
113
+ ];
114
+ }
115
+ export function scrubSecrets(text) {
116
+ let out = text
117
+ .replace(JSON_HEADER_ASSIGNMENT, (_m, keyQuote, name, sep, valueQuote) => `${keyQuote}${name}${keyQuote}${sep}${valueQuote}[redacted]${valueQuote}`)
118
+ .replace(HEADER_ASSIGNMENT, (_m, name, sep) => `${name}${sep}[redacted]`)
119
+ .replace(OMA_KEY, "[redacted]")
120
+ .replace(BASE64_32_BYTES, "[redacted]");
121
+ for (const shape of FOREIGN_KEY_SHAPES) {
122
+ out = out.replace(shape, "[redacted]");
123
+ }
124
+ return out.replace(QUERY_CREDENTIAL, "$1[redacted]");
125
+ }
126
+ const STRING_CAP = 1024;
127
+ const STACK_CAP = 4096;
128
+ const MAX_DEPTH = 6;
129
+ function cap(text, limit) {
130
+ return text.length <= limit ? text : `${text.slice(0, limit)}…[truncated]`;
131
+ }
132
+ function scrubAndCap(text, limit = STRING_CAP) {
133
+ return cap(scrubSecrets(text), limit);
134
+ }
135
+ function serializeError(error, stacks) {
136
+ const out = {
137
+ name: error.name,
138
+ message: scrubAndCap(error.message),
139
+ };
140
+ if (stacks && typeof error.stack === "string") {
141
+ out.stack = scrubAndCap(error.stack, STACK_CAP);
142
+ }
143
+ // One level of cause — enough to diagnose wrapped errors without
144
+ // reintroducing an unbounded structure (deeper causes are dropped).
145
+ if (error.cause instanceof Error) {
146
+ out.cause = { name: error.cause.name, message: scrubAndCap(error.cause.message) };
147
+ }
148
+ else if (typeof error.cause === "string") {
149
+ out.cause = scrubAndCap(error.cause);
150
+ }
151
+ return out;
152
+ }
153
+ function redactValue(value, stacks, depth, seen) {
154
+ if (value === null || typeof value === "boolean" || typeof value === "number") {
155
+ return value;
156
+ }
157
+ if (typeof value === "string")
158
+ return scrubAndCap(value);
159
+ if (typeof value === "bigint")
160
+ return String(value);
161
+ if (value === undefined)
162
+ return undefined;
163
+ if (value instanceof Error)
164
+ return serializeError(value, stacks);
165
+ if (value instanceof Date)
166
+ return value.toISOString();
167
+ if (typeof value !== "object")
168
+ return "[unserializable]";
169
+ if (depth >= MAX_DEPTH)
170
+ return "[truncated]";
171
+ if (seen.has(value))
172
+ return "[circular]";
173
+ seen.add(value);
174
+ if (Array.isArray(value)) {
175
+ return value.map((item) => redactValue(item, stacks, depth + 1, seen));
176
+ }
177
+ const out = {};
178
+ for (const [key, entry] of Object.entries(value)) {
179
+ out[key] = isDeniedKey(key)
180
+ ? "[redacted]"
181
+ : redactValue(entry, stacks, depth + 1, seen);
182
+ }
183
+ return out;
184
+ }
185
+ export function redactFields(fields, stacks) {
186
+ return redactValue(fields, stacks, 0, new WeakSet());
187
+ }
188
+ // House rule: unknown config values refuse to start, never silently coerce.
189
+ // (Moved from app.ts so the one parser serves both; app.ts imports it.)
190
+ export function parseBooleanFlag(raw, name) {
191
+ if (raw === undefined || raw === "0")
192
+ return false;
193
+ if (raw === "1")
194
+ return true;
195
+ throw new Error(`Unsupported ${name}: ${JSON.stringify(raw)} (expected "1" or "0")`);
196
+ }
197
+ export function parseLogConfig(env) {
198
+ const raw = env.OMA_LOG_LEVEL ?? "info";
199
+ if (raw !== "debug" && raw !== "info" && raw !== "warn" && raw !== "error") {
200
+ throw new Error(`Unsupported OMA_LOG_LEVEL: ${JSON.stringify(raw)} (expected "debug", "info", "warn", or "error")`);
201
+ }
202
+ return {
203
+ level: raw,
204
+ stacks: parseBooleanFlag(env.OMA_LOG_STACKS, "OMA_LOG_STACKS"),
205
+ };
206
+ }
207
+ // 0121 C2: oma_log_events_total feed. Module-level because `log` is a
208
+ // module singleton; exactly one deployment assembly sets it per process
209
+ // (matching the in-process admission-counter scope). Must not throw —
210
+ // defensively wrapped at the call site anyway.
211
+ let logEventHook;
212
+ export function setLogEventHook(hook) {
213
+ logEventHook = hook;
214
+ }
215
+ // Looked up per call, not captured: test spies on console.* must intercept,
216
+ // and console keeps the debug/info→stdout, warn/error→stderr split. Audit
217
+ // lines ride console.info (stdout) — existing admin_audit consumers grep
218
+ // stdout, and the audit distinction lives in the JSON `level` field.
219
+ const defaultWriter = (level, line) => {
220
+ console[level === "audit" ? "info" : level](line);
221
+ };
222
+ export function createLogger(config, write = defaultWriter) {
223
+ const threshold = LEVEL_RANK[config.level];
224
+ const emit = (level, event, fields) => {
225
+ if (LEVEL_RANK[level] < threshold)
226
+ return;
227
+ try {
228
+ logEventHook?.(level);
229
+ }
230
+ catch {
231
+ // Telemetry must never fail a log emission.
232
+ }
233
+ try {
234
+ const record = {
235
+ ts: new Date().toISOString(),
236
+ level,
237
+ event,
238
+ };
239
+ for (const [key, value] of Object.entries(redactFields(fields ?? {}, config.stacks))) {
240
+ if (!(key in record))
241
+ record[key] = value;
242
+ }
243
+ write(level, JSON.stringify(record));
244
+ }
245
+ catch {
246
+ // A logger that throws is worse than the event it was reporting.
247
+ try {
248
+ write(level, JSON.stringify({ ts: new Date().toISOString(), level, event, logger_error: "emit_failed" }));
249
+ }
250
+ catch {
251
+ // Writer itself failed; nothing sane left to do.
252
+ }
253
+ }
254
+ };
255
+ return {
256
+ debug: (event, fields) => emit("debug", event, fields),
257
+ info: (event, fields) => emit("info", event, fields),
258
+ warn: (event, fields) => emit("warn", event, fields),
259
+ error: (event, fields) => emit("error", event, fields),
260
+ audit: (event, fields) => emit("audit", event, fields),
261
+ };
262
+ }
263
+ export const log = createLogger(parseLogConfig(process.env));