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,1043 @@
1
+ // api.js — OMA REST adapter for the static Managed Agents Console.
2
+ // ES module (plan 0120 §3.3): loaded with type="module" in index.html so the
3
+ // pure header logic below is importable by Node tests without a browser.
4
+ import { followEventStream } from "./sse.js";
5
+
6
+ const MANAGED_AGENTS_BETA = "managed-agents-2026-04-01";
7
+ const FILES_API_BETA = "files-api-2025-04-14";
8
+ const SKILLS_API_BETA = "skills-2025-10-02";
9
+ const BETA_HEADER = `${MANAGED_AGENTS_BETA}, ${FILES_API_BETA}, ${SKILLS_API_BETA}`;
10
+ const PAGE_LIMIT = 100;
11
+ const EVENT_PAGE_LIMIT = 1000;
12
+ const MAX_AUTO_PAGES = 100;
13
+ const VALIDATE_CAPABILITY = Symbol("validate-mcp-oauth-credential");
14
+ const VALIDATE_ENVIRONMENT_NETWORKING_CAPABILITY = Symbol("validate-environment-networking");
15
+ const CREATE_AGENT_CAPABILITY = Symbol("create-agent");
16
+ const CREATE_ENVIRONMENT_CAPABILITY = Symbol("create-environment");
17
+ const ARCHIVE_ENVIRONMENT_CAPABILITY = Symbol("archive-environment");
18
+ const DELETE_ENVIRONMENT_CAPABILITY = Symbol("delete-environment");
19
+ const CREATE_SESSION_CAPABILITY = Symbol("create-session");
20
+ const SEND_SESSION_EVENTS_CAPABILITY = Symbol("send-session-events");
21
+ const ARCHIVE_AGENT_CAPABILITY = Symbol("archive-agent");
22
+ const UPDATE_AGENT_CAPABILITY = Symbol("update-agent");
23
+ const ARCHIVE_SESSION_CAPABILITY = Symbol("archive-session");
24
+ const DELETE_SESSION_CAPABILITY = Symbol("delete-session");
25
+ const CREATE_VAULT_CAPABILITY = Symbol("create-vault");
26
+ const CREATE_VAULT_CREDENTIAL_CAPABILITY = Symbol("create-vault-credential");
27
+ const CREATE_SKILL_CAPABILITY = Symbol("create-skill");
28
+ const CMA_BUILTIN_TOOL_NAMES = [
29
+ "bash",
30
+ "edit",
31
+ "glob",
32
+ "grep",
33
+ "read",
34
+ "web_fetch",
35
+ "web_search",
36
+ "write",
37
+ ];
38
+
39
+ // Legacy direct-header credentials remain available for interactive API tests,
40
+ // but the console UI never sets them. Browser logins exchange a pasted key for
41
+ // an opaque HttpOnly session cookie, which JavaScript cannot read or persist.
42
+ const credentials = {
43
+ adminKey: null,
44
+ workspaceKey: null,
45
+ };
46
+
47
+ export function setAdminKey(key) {
48
+ credentials.adminKey = key || null;
49
+ }
50
+
51
+ export function setWorkspaceKey(key) {
52
+ credentials.workspaceKey = key || null;
53
+ }
54
+
55
+ export function clearCredentials() {
56
+ credentials.adminKey = null;
57
+ credentials.workspaceKey = null;
58
+ }
59
+
60
+ export function hasWorkspaceKey() {
61
+ return credentials.workspaceKey !== null;
62
+ }
63
+
64
+ export function canSubmitToolConfirmation({ status, readOnly, actionBusy }) {
65
+ return status !== "archived" && !readOnly && !actionBusy;
66
+ }
67
+
68
+ // Pure: (path, creds) -> headers. Exported for unit tests — this routing is
69
+ // the guarantee that the admin key never rides a /v1 request and the
70
+ // workspace key never rides an /admin one.
71
+ export function buildRequestHeaders(path, creds) {
72
+ const headers = { accept: "application/json" };
73
+ if (path === "/admin" || path.startsWith("/admin/")) {
74
+ if (creds.adminKey) headers["x-admin-key"] = creds.adminKey;
75
+ } else if (path.startsWith("/v1/")) {
76
+ headers["anthropic-beta"] = BETA_HEADER;
77
+ if (creds.workspaceKey) headers["x-api-key"] = creds.workspaceKey;
78
+ }
79
+ return headers;
80
+ }
81
+
82
+ // A 401 means the key for that tier is wrong or revoked; keeping it would
83
+ // just replay the failure, so drop it and let the UI re-prompt.
84
+ export function clearKeyForPath(path, creds) {
85
+ if (path === "/admin" || path.startsWith("/admin/")) {
86
+ creds.adminKey = null;
87
+ } else if (path.startsWith("/v1/")) {
88
+ creds.workspaceKey = null;
89
+ }
90
+ }
91
+
92
+ function isExactValidatePath(path) {
93
+ const pathname = new URL(path, "http://oma.local").pathname;
94
+ return /^\/v1\/vaults\/[^/]+\/credentials\/[^/]+\/mcp_oauth_validate$/.test(pathname);
95
+ }
96
+
97
+ function isExactEnvironmentNetworkingValidatePath(path) {
98
+ const pathname = new URL(path, "http://oma.local").pathname;
99
+ return pathname === "/v1/environments/networking-presets/validate";
100
+ }
101
+
102
+ async function request(path, { method = "GET", body, form, capability, headers: extraHeaders } = {}) {
103
+ // Normalize the verb once so the guard and fetch see the same value. The
104
+ // guard is already fail-closed for any casing (a lowercase "post" is
105
+ // non-GET, so it is denied unless it exactly matches the capability clause);
106
+ // normalizing just keeps the capability path from rejecting a well-intentioned
107
+ // lowercase caller and avoids case being load-bearing here.
108
+ const normalizedMethod = String(method).toUpperCase();
109
+ // Keep /v1 writes deny-by-default. New workspace-key writes need a narrowly
110
+ // named capability rather than silently gaining access through this generic
111
+ // transport helper.
112
+ if (normalizedMethod !== "GET" && path.startsWith("/v1/") &&
113
+ !isAllowedWorkspaceWrite(path, normalizedMethod, capability)) {
114
+ throw new Error("Console /v1 writes are not permitted");
115
+ }
116
+ const headers = buildRequestHeaders(path, credentials);
117
+ if (extraHeaders) Object.assign(headers, extraHeaders);
118
+ const init = { method: normalizedMethod, headers, credentials: "same-origin" };
119
+ if (body !== undefined) {
120
+ headers["content-type"] = "application/json";
121
+ init.body = JSON.stringify(body);
122
+ }
123
+ if (form !== undefined) init.body = form;
124
+ const response = await fetch(path, init);
125
+ const text = await response.text();
126
+ // A fronting proxy (TLS terminator, LB) can answer 502/504 with HTML; that
127
+ // must surface as "Request failed (5xx)", not a JSON.parse SyntaxError.
128
+ let parsed = null;
129
+ if (text) {
130
+ try {
131
+ parsed = JSON.parse(text);
132
+ } catch {
133
+ parsed = null;
134
+ }
135
+ }
136
+ if (!response.ok) {
137
+ if (response.status === 401) clearKeyForPath(path, credentials);
138
+ const message = parsed?.error?.message ?? `Request failed (${response.status})`;
139
+ const error = new Error(message);
140
+ error.response = parsed;
141
+ error.status = response.status;
142
+ throw error;
143
+ }
144
+ return parsed;
145
+ }
146
+
147
+ function isAllowedWorkspaceWrite(path, method, capability) {
148
+ return (
149
+ (capability === VALIDATE_CAPABILITY && method === "POST" && isExactValidatePath(path)) ||
150
+ (capability === VALIDATE_ENVIRONMENT_NETWORKING_CAPABILITY && method === "POST" && isExactEnvironmentNetworkingValidatePath(path)) ||
151
+ (capability === CREATE_AGENT_CAPABILITY && method === "POST" && path === "/v1/agents") ||
152
+ (capability === CREATE_ENVIRONMENT_CAPABILITY && method === "POST" && path === "/v1/environments") ||
153
+ (
154
+ capability === ARCHIVE_ENVIRONMENT_CAPABILITY &&
155
+ method === "POST" &&
156
+ /^\/v1\/environments\/[^/]+\/archive$/.test(new URL(path, "http://oma.local").pathname)
157
+ ) ||
158
+ (
159
+ capability === DELETE_ENVIRONMENT_CAPABILITY &&
160
+ method === "DELETE" &&
161
+ /^\/v1\/environments\/[^/]+$/.test(new URL(path, "http://oma.local").pathname)
162
+ ) ||
163
+ (capability === CREATE_SESSION_CAPABILITY && method === "POST" && path === "/v1/sessions") ||
164
+ (capability === CREATE_VAULT_CAPABILITY && method === "POST" && path === "/v1/vaults") ||
165
+ (
166
+ capability === CREATE_VAULT_CREDENTIAL_CAPABILITY &&
167
+ method === "POST" &&
168
+ /^\/v1\/vaults\/[^/]+\/credentials$/.test(new URL(path, "http://oma.local").pathname)
169
+ ) ||
170
+ (capability === CREATE_SKILL_CAPABILITY && method === "POST" && path === "/v1/skills") ||
171
+ (
172
+ capability === ARCHIVE_AGENT_CAPABILITY &&
173
+ method === "POST" &&
174
+ /^\/v1\/agents\/[^/]+\/archive$/.test(new URL(path, "http://oma.local").pathname)
175
+ ) ||
176
+ (
177
+ capability === UPDATE_AGENT_CAPABILITY &&
178
+ method === "POST" &&
179
+ /^\/v1\/agents\/[^/]+$/.test(new URL(path, "http://oma.local").pathname)
180
+ ) ||
181
+ (
182
+ capability === ARCHIVE_SESSION_CAPABILITY &&
183
+ method === "POST" &&
184
+ /^\/v1\/sessions\/[^/]+\/archive$/.test(new URL(path, "http://oma.local").pathname)
185
+ ) ||
186
+ (
187
+ capability === DELETE_SESSION_CAPABILITY &&
188
+ method === "DELETE" &&
189
+ /^\/v1\/sessions\/[^/]+$/.test(new URL(path, "http://oma.local").pathname)
190
+ ) ||
191
+ (
192
+ capability === SEND_SESSION_EVENTS_CAPABILITY &&
193
+ method === "POST" &&
194
+ /^\/v1\/sessions\/[^/]+\/events$/.test(new URL(path, "http://oma.local").pathname)
195
+ )
196
+ );
197
+ }
198
+
199
+ // Deliberately not added to window.OmaConsoleApi: this exists solely for the
200
+ // Node contract tests that prove forbidden /v1 writes fail before fetch.
201
+ export const __testRequest = request;
202
+
203
+ const fetchJson = request;
204
+
205
+ // --- Admin API (0119 §4). Lists return bare arrays; mint returns the
206
+ // plaintext exactly once — render it, never store or log it.
207
+
208
+ export function createWorkspace(name) {
209
+ return request("/admin/workspaces", { method: "POST", body: { name } });
210
+ }
211
+
212
+ export function listWorkspaces() {
213
+ return request("/admin/workspaces");
214
+ }
215
+
216
+ // Minting is deduplicated while in flight: a double-click or nervous retry
217
+ // must not create a second active credential whose plaintext instantly
218
+ // replaces the first in the UI — the orphaned key would stay active with
219
+ // nobody holding its plaintext. Both clicks resolve to the same mint.
220
+ const mintsInFlight = new Map();
221
+
222
+ export function mintKey(workspaceId, label) {
223
+ const pending = mintsInFlight.get(workspaceId);
224
+ if (pending) return pending;
225
+ const mint = request(`/admin/workspaces/${encodeURIComponent(workspaceId)}/keys`, {
226
+ method: "POST",
227
+ ...(label ? { body: { label } } : {}),
228
+ }).finally(() => mintsInFlight.delete(workspaceId));
229
+ mintsInFlight.set(workspaceId, mint);
230
+ return mint;
231
+ }
232
+
233
+ export function listKeys(workspaceId) {
234
+ return request(`/admin/workspaces/${encodeURIComponent(workspaceId)}/keys`);
235
+ }
236
+
237
+ export function revokeKey(keySha256) {
238
+ return request(`/admin/keys/${encodeURIComponent(keySha256)}`, {
239
+ method: "DELETE",
240
+ });
241
+ }
242
+
243
+ export function listVaults() {
244
+ return fetchCursorPages("/v1/vaults?include_archived=true");
245
+ }
246
+
247
+ export function listVaultCredentials(vaultId) {
248
+ return fetchCursorPages(
249
+ `/v1/vaults/${encodeURIComponent(vaultId)}/credentials?include_archived=true`,
250
+ );
251
+ }
252
+
253
+ export function createVault(body) {
254
+ return request("/v1/vaults", {
255
+ method: "POST",
256
+ body,
257
+ capability: CREATE_VAULT_CAPABILITY,
258
+ });
259
+ }
260
+
261
+ export function createVaultCredential(vaultId, body) {
262
+ return request(`/v1/vaults/${encodeURIComponent(vaultId)}/credentials`, {
263
+ method: "POST",
264
+ body,
265
+ capability: CREATE_VAULT_CREDENTIAL_CAPABILITY,
266
+ });
267
+ }
268
+
269
+ export function listSkills() {
270
+ return fetchCursorPages("/v1/skills");
271
+ }
272
+
273
+ export function createSkill(displayTitle, files) {
274
+ const form = new FormData();
275
+ if (displayTitle) form.set("display_title", displayTitle);
276
+ for (const file of files) form.append("files[]", file, file.name);
277
+ return request("/v1/skills", {
278
+ method: "POST",
279
+ form,
280
+ capability: CREATE_SKILL_CAPABILITY,
281
+ });
282
+ }
283
+
284
+ export function listWorkspaceCredentialHealth(workspaceId, page) {
285
+ const url = new URL(
286
+ `/admin/workspaces/${encodeURIComponent(workspaceId)}/mcp-credentials`,
287
+ window.location.origin,
288
+ );
289
+ url.searchParams.set("limit", String(PAGE_LIMIT));
290
+ if (page) url.searchParams.set("page", page);
291
+ return request(url.pathname + url.search);
292
+ }
293
+
294
+ export function getConsoleAuthStatus() {
295
+ return request("/console/auth/status");
296
+ }
297
+
298
+ export function loginConsoleWorkspace(apiKey) {
299
+ return request("/console/auth/workspace", { method: "POST", body: { api_key: apiKey } });
300
+ }
301
+
302
+ export function loginConsoleAdmin(adminKey) {
303
+ return request("/console/auth/admin", { method: "POST", body: { admin_key: adminKey } });
304
+ }
305
+
306
+ export function selectConsoleWorkspace(workspaceId) {
307
+ return request("/console/auth/select-workspace", { method: "POST", body: { workspace_id: workspaceId } });
308
+ }
309
+
310
+ export function logoutConsole() {
311
+ return request("/console/auth/logout", { method: "POST" });
312
+ }
313
+
314
+ export function validateMcpOauthCredential(vaultId, credentialId, mode) {
315
+ if (mode !== "api") return Promise.reject(new Error("Validate is only available in live API mode"));
316
+ const path = `/v1/vaults/${encodeURIComponent(vaultId)}/credentials/${encodeURIComponent(credentialId)}/mcp_oauth_validate`;
317
+ return request(path, { method: "POST", capability: VALIDATE_CAPABILITY });
318
+ }
319
+
320
+ export function createIdempotencyIntent() {
321
+ return { key: null, fingerprint: null };
322
+ }
323
+
324
+ export function createAgent(body) {
325
+ return request("/v1/agents", {
326
+ method: "POST",
327
+ body,
328
+ capability: CREATE_AGENT_CAPABILITY,
329
+ }).then(toUiAgent);
330
+ }
331
+
332
+ export function archiveAgent(agentId) {
333
+ return request(`/v1/agents/${encodeURIComponent(agentId)}/archive`, {
334
+ method: "POST",
335
+ capability: ARCHIVE_AGENT_CAPABILITY,
336
+ }).then(toUiAgent);
337
+ }
338
+
339
+ export function updateAgentToolPermission(agent, policy) {
340
+ if (policy !== "always_allow" && policy !== "always_ask") {
341
+ return Promise.reject(new Error("Unsupported tool permission policy"));
342
+ }
343
+ if (!Number.isSafeInteger(agent?.apiVersion) || !Array.isArray(agent?.rawTools)) {
344
+ return Promise.reject(new Error("Agent configuration is incomplete; refresh before updating"));
345
+ }
346
+ let hasEnabledBuiltinTool = false;
347
+ const tools = agent.rawTools.map((toolset) => {
348
+ if (toolset?.type !== "agent_toolset_20260401") return toolset;
349
+ const defaultEnabled = toolset.default_config?.enabled !== false;
350
+ const configs = Array.isArray(toolset.configs) ? toolset.configs : [];
351
+ hasEnabledBuiltinTool ||= effectiveBuiltinTools(toolset).some((tool) => tool.enabled);
352
+ return {
353
+ ...toolset,
354
+ default_config: {
355
+ ...(toolset.default_config ?? {}),
356
+ permission_policy: { type:policy },
357
+ },
358
+ ...(Array.isArray(toolset.configs) ? {
359
+ configs: configs.map((config) =>
360
+ (config.enabled ?? defaultEnabled) === false
361
+ ? config
362
+ : { ...config, permission_policy:{ type:policy } }),
363
+ } : {}),
364
+ };
365
+ });
366
+ if (!hasEnabledBuiltinTool) {
367
+ return Promise.reject(new Error("This agent has no enabled built-in tools to update"));
368
+ }
369
+ return request(`/v1/agents/${encodeURIComponent(agent.id)}`, {
370
+ method: "POST",
371
+ body: { version:agent.apiVersion, tools },
372
+ capability: UPDATE_AGENT_CAPABILITY,
373
+ }).then(toUiAgent);
374
+ }
375
+
376
+ export function modelInputForSelection(model) {
377
+ if (!model || typeof model.provider !== "string" || typeof model.id !== "string") {
378
+ throw new Error("A deployment model must be selected");
379
+ }
380
+ return model.default && model.provider === "anthropic"
381
+ ? model.id
382
+ : { provider: model.provider, id: model.id };
383
+ }
384
+
385
+ export function listModelCatalog() {
386
+ return fetchCursorPages("/v1/model-catalog");
387
+ }
388
+
389
+ export function createEnvironment(body) {
390
+ return request("/v1/environments", {
391
+ method: "POST",
392
+ body,
393
+ capability: CREATE_ENVIRONMENT_CAPABILITY,
394
+ }).then(toUiEnvironment);
395
+ }
396
+
397
+ export function archiveEnvironment(environmentId) {
398
+ return request(`/v1/environments/${encodeURIComponent(environmentId)}/archive`, {
399
+ method: "POST",
400
+ capability: ARCHIVE_ENVIRONMENT_CAPABILITY,
401
+ }).then(toUiEnvironment);
402
+ }
403
+
404
+ export function deleteEnvironment(environmentId) {
405
+ return request(`/v1/environments/${encodeURIComponent(environmentId)}`, {
406
+ method: "DELETE",
407
+ capability: DELETE_ENVIRONMENT_CAPABILITY,
408
+ });
409
+ }
410
+
411
+ export function listEnvironmentNetworkingPresets() {
412
+ return request("/v1/environments/networking-presets")
413
+ .then((response) => {
414
+ const rows = Array.isArray(response?.presets) ? response.presets : [];
415
+ return {
416
+ deployment: response?.deployment ?? {
417
+ provider:null,
418
+ egress_supported:false,
419
+ reason:"Networking capability was not reported by this deployment.",
420
+ },
421
+ presets: rows.map(toUiNetworkingPreset),
422
+ custom: response?.custom ?? {
423
+ https_only:true,
424
+ wildcard_matches_bare_domain:false,
425
+ },
426
+ };
427
+ });
428
+ }
429
+
430
+ export function validateEnvironmentNetworkingHosts(allowedHosts) {
431
+ return request("/v1/environments/networking-presets/validate", {
432
+ method: "POST",
433
+ body: { allowed_hosts: allowedHosts },
434
+ capability: VALIDATE_ENVIRONMENT_NETWORKING_CAPABILITY,
435
+ }).then((response) => {
436
+ const hosts = response?.allowed_hosts ?? response?.normalized_allowed_hosts ?? response?.data?.allowed_hosts;
437
+ return Array.isArray(hosts) ? hosts : [];
438
+ });
439
+ }
440
+
441
+ export function createSession(body, { intent, agentNames } = {}) {
442
+ return request("/v1/sessions", {
443
+ method: "POST",
444
+ body,
445
+ capability: CREATE_SESSION_CAPABILITY,
446
+ headers: { "idempotency-key": keyForIntent(intent, body) },
447
+ }).then((session) => toUiSession(session, agentNames ?? new Map()));
448
+ }
449
+
450
+ export function archiveSession(sessionId) {
451
+ return request(`/v1/sessions/${encodeURIComponent(sessionId)}/archive`, {
452
+ method: "POST",
453
+ capability: ARCHIVE_SESSION_CAPABILITY,
454
+ });
455
+ }
456
+
457
+ export function deleteSession(sessionId) {
458
+ return request(`/v1/sessions/${encodeURIComponent(sessionId)}`, {
459
+ method: "DELETE",
460
+ capability: DELETE_SESSION_CAPABILITY,
461
+ });
462
+ }
463
+
464
+ export function sendSessionEvents(sessionId, events, { intent } = {}) {
465
+ const body = { events };
466
+ return request(`/v1/sessions/${encodeURIComponent(sessionId)}/events`, {
467
+ method: "POST",
468
+ body,
469
+ capability: SEND_SESSION_EVENTS_CAPABILITY,
470
+ headers: { "idempotency-key": keyForIntent(intent, body) },
471
+ });
472
+ }
473
+
474
+ export function followSessionEvents(sessionId, {
475
+ signal,
476
+ onEvent,
477
+ onState,
478
+ lastEventId,
479
+ fetchImpl,
480
+ maxReconnects,
481
+ reconnectDelayMs,
482
+ maxFrameBytes,
483
+ } = {}) {
484
+ const path = `/v1/sessions/${encodeURIComponent(sessionId)}/events/stream`;
485
+ return followEventStream({
486
+ url: path,
487
+ headers: { ...buildRequestHeaders(path, credentials), accept:"text/event-stream" },
488
+ signal,
489
+ onEvent,
490
+ onState,
491
+ lastEventId,
492
+ fetchImpl,
493
+ maxReconnects,
494
+ reconnectDelayMs,
495
+ maxFrameBytes,
496
+ }).catch((error) => {
497
+ if (error?.status === 401) clearKeyForPath(path, credentials);
498
+ throw error;
499
+ });
500
+ }
501
+
502
+ function keyForIntent(intent, payload) {
503
+ const target = intent ?? createIdempotencyIntent();
504
+ const fingerprint = JSON.stringify(payload);
505
+ if (target.key === null || target.fingerprint !== fingerprint) {
506
+ target.key = newIdempotencyKey();
507
+ target.fingerprint = fingerprint;
508
+ }
509
+ return target.key;
510
+ }
511
+
512
+ function newIdempotencyKey() {
513
+ const cryptoApi = globalThis.crypto;
514
+ if (cryptoApi?.randomUUID) return cryptoApi.randomUUID();
515
+ const bytes = new Uint8Array(16);
516
+ if (cryptoApi?.getRandomValues) {
517
+ cryptoApi.getRandomValues(bytes);
518
+ } else {
519
+ for (let i = 0; i < bytes.length; i += 1) {
520
+ bytes[i] = Math.floor(Math.random() * 256);
521
+ }
522
+ }
523
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
524
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
525
+ const hex = [...bytes].map((byte) => byte.toString(16).padStart(2, "0"));
526
+ return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10).join("")}`;
527
+ }
528
+
529
+ // Browser navigation on an <a href> cannot attach x-api-key, so authenticated
530
+ // file content downloads go through fetch -> Blob -> object URL instead
531
+ // (0120 review, Codex finding).
532
+ export async function downloadFile(href, filename) {
533
+ const response = await fetch(href, {
534
+ headers: buildRequestHeaders(href, credentials),
535
+ });
536
+ if (!response.ok) {
537
+ if (response.status === 401) clearKeyForPath(href, credentials);
538
+ const error = new Error(`Download failed (${response.status})`);
539
+ error.status = response.status;
540
+ throw error;
541
+ }
542
+ const blob = await response.blob();
543
+ const url = URL.createObjectURL(blob);
544
+ const anchor = document.createElement("a");
545
+ anchor.href = url;
546
+ anchor.download = filename;
547
+ document.body.appendChild(anchor);
548
+ anchor.click();
549
+ anchor.remove();
550
+ // Deferred a tick: revoking synchronously races the browser's grab of the
551
+ // blob in some engines.
552
+ setTimeout(() => URL.revokeObjectURL(url), 0);
553
+ }
554
+
555
+ async function fetchCursorPages(path, { limit = PAGE_LIMIT, cursorParam = "page" } = {}) {
556
+ const data = [];
557
+ let cursor = null;
558
+ for (let pageCount = 0; pageCount < MAX_AUTO_PAGES; pageCount += 1) {
559
+ const url = new URL(path, window.location.origin);
560
+ url.searchParams.set("limit", String(limit));
561
+ if (cursor) url.searchParams.set(cursorParam, cursor);
562
+ const page = await fetchJson(url.pathname + url.search);
563
+ data.push(...(Array.isArray(page?.data) ? page.data : []));
564
+ cursor = page.next_page;
565
+ if (!page?.has_more && !cursor) return { data, truncated: false };
566
+ if (!cursor) return { data, truncated: true };
567
+ }
568
+ return { data, truncated: true };
569
+ }
570
+
571
+ async function fetchFilePages(path, { limit = PAGE_LIMIT } = {}) {
572
+ const data = [];
573
+ let afterId = null;
574
+ for (let pageCount = 0; pageCount < MAX_AUTO_PAGES; pageCount += 1) {
575
+ const url = new URL(path, window.location.origin);
576
+ url.searchParams.set("limit", String(limit));
577
+ if (afterId) url.searchParams.set("after_id", afterId);
578
+ const page = await fetchJson(url.pathname + url.search);
579
+ data.push(...(Array.isArray(page?.data) ? page.data : []));
580
+ if (!page?.has_more) return { data, truncated: false };
581
+ afterId = page.last_id;
582
+ if (!afterId) return { data, truncated: true };
583
+ }
584
+ return { data, truncated: true };
585
+ }
586
+
587
+ export async function loadConsoleData() {
588
+ const [agentsPage, sessionsPage, environmentsPage, filesPage, modelsPage, networkingCatalog, vaultsPage, skillsPage] =
589
+ await Promise.all([
590
+ fetchCursorPages("/v1/agents?include_archived=true"),
591
+ fetchCursorPages("/v1/sessions?include_archived=true&order=desc"),
592
+ fetchCursorPages("/v1/environments?include_archived=true"),
593
+ fetchFilePages("/v1/files"),
594
+ listModelCatalog(),
595
+ listEnvironmentNetworkingPresets(),
596
+ listVaults(),
597
+ listSkills(),
598
+ ]);
599
+
600
+ const agents = agentsPage.data.map(toUiAgent);
601
+ const agentNames = new Map(agents.map((agent) => [agent.id, agent.name]));
602
+ const sessions = sessionsPage.data.map((session) =>
603
+ toUiSession(session, agentNames));
604
+ const environments = environmentsPage.data.map(toUiEnvironment);
605
+ const files = filesPage.data.map(toUiFile);
606
+ const warnings = paginationWarnings([
607
+ ["agents", agentsPage],
608
+ ["sessions", sessionsPage],
609
+ ["environments", environmentsPage],
610
+ ["files", filesPage],
611
+ ["models", modelsPage],
612
+ ]);
613
+
614
+ return { agents, sessions, environments, files, models: modelsPage.data, networkingCatalog, vaults: vaultsPage.data, skills: skillsPage.data, warnings };
615
+ }
616
+
617
+ async function hydrateSession(session) {
618
+ const [eventsPage, filesPage] = await Promise.all([
619
+ fetchCursorPages(`/v1/sessions/${encodeURIComponent(session.id)}/events?order=asc`, { limit: EVENT_PAGE_LIMIT }),
620
+ fetchFilePages(`/v1/files?scope_id=${encodeURIComponent(session.id)}`),
621
+ ]);
622
+ const sessionSignals = sessionSignalsFromEvents(eventsPage.data);
623
+ const events = toUiEvents(eventsPage.data);
624
+ return {
625
+ ...session,
626
+ ...sessionSignals,
627
+ events,
628
+ files: filesPage.data.map(toUiFile),
629
+ spans: toUiSpans(events),
630
+ warnings: paginationWarnings([
631
+ ["events", eventsPage],
632
+ ["files", filesPage],
633
+ ]),
634
+ };
635
+ }
636
+
637
+ function paginationWarnings(pages) {
638
+ return pages
639
+ .filter(([, page]) => page.truncated)
640
+ .map(([name]) =>
641
+ `${name} reached the ${MAX_AUTO_PAGES}-page safety cap; this view may be partial.`);
642
+ }
643
+
644
+ function toUiAgent(agent) {
645
+ const modelId = agent.model?.id ?? String(agent.model ?? "unknown");
646
+ const modelProvider = agent.model?.provider ?? "anthropic";
647
+ return {
648
+ id: agent.id,
649
+ short: shortId(agent.id),
650
+ name: agent.name || agent.id,
651
+ model: `${modelProvider}/${modelId}`,
652
+ modelId,
653
+ modelProvider,
654
+ status: agent.archived_at ? "archived" : "active",
655
+ created: shortDate(agent.created_at),
656
+ updated: shortDate(agent.updated_at),
657
+ version: `v${agent.version ?? 1}`,
658
+ apiVersion: agent.version ?? 1,
659
+ rawTools: Array.isArray(agent.tools) ? agent.tools : [],
660
+ mcpServers: Array.isArray(agent.mcp_servers) ? agent.mcp_servers : [],
661
+ skills: Array.isArray(agent.skills) ? agent.skills : [],
662
+ metadata: agent.metadata && typeof agent.metadata === "object" ? agent.metadata : {},
663
+ description: agent.description || null,
664
+ tools: Array.isArray(agent.tools) ? agent.tools.length : 0,
665
+ system: agent.system || "No system prompt set.",
666
+ toolset: summarizeToolset(agent.tools),
667
+ toolPermission: summarizeToolPermission(agent.tools),
668
+ sessions: [],
669
+ };
670
+ }
671
+
672
+ function toUiSession(session, agentNames) {
673
+ const agentId = session.agent?.id ?? "unknown-agent";
674
+ return {
675
+ id: session.id,
676
+ short: shortId(session.id),
677
+ title: session.title || session.id,
678
+ status: toUiStatus(session),
679
+ agent: agentNames.get(agentId) ?? agentId,
680
+ agentId,
681
+ env: session.environment_id,
682
+ created: shortDate(session.created_at),
683
+ updated: shortDate(session.updated_at),
684
+ dur: "—",
685
+ tokens: usageLabel(session.usage),
686
+ resources: Array.isArray(session.resources) ? session.resources.length : 0,
687
+ requiresAction: stopReasonType(session.stop_reason) === "requires_action",
688
+ };
689
+ }
690
+
691
+ function toUiEnvironment(environment) {
692
+ const networking = environment.config?.networking;
693
+ const allowedHosts = Array.isArray(networking?.allowed_hosts)
694
+ ? networking.allowed_hosts.length
695
+ : 0;
696
+ const provider = environment.config?.sandbox_provider ?? "deployment provider";
697
+ const networkingSummary = networking?.type === "limited"
698
+ ? (allowedHosts ? `${allowedHosts} allowed host${allowedHosts === 1 ? "" : "s"}` : "offline")
699
+ : "deployment policy";
700
+ return {
701
+ id: environment.id,
702
+ label: environment.name || environment.id,
703
+ image: networking?.type === "limited"
704
+ ? `${provider} · ${networkingSummary}`
705
+ : provider,
706
+ networkingSummary,
707
+ allowedHosts,
708
+ created: shortDate(environment.created_at),
709
+ config: environment.config ?? {},
710
+ archived: Boolean(environment.archived_at),
711
+ };
712
+ }
713
+
714
+ function toUiNetworkingPreset(preset) {
715
+ const rawHosts = preset.networking?.allowed_hosts ?? [];
716
+ const allowedHosts = Array.isArray(rawHosts) ? rawHosts : [];
717
+ const id = String(preset.id ?? (allowedHosts.length ? "custom" : "offline-v1"));
718
+ return {
719
+ id,
720
+ version: preset.version ?? 1,
721
+ label: preset.name ?? titleForPresetId(id),
722
+ description: preset.description ?? "",
723
+ allowed_hosts: allowedHosts,
724
+ config: { networking: { type: "limited", allowed_hosts: allowedHosts } },
725
+ };
726
+ }
727
+
728
+ function titleForPresetId(id) {
729
+ if (id === "offline-v1") return "Offline";
730
+ if (id === "npm-pypi-v1") return "npm + PyPI";
731
+ if (id === "github-packages-v1") return "GitHub + package registries";
732
+ return id.replace(/[-_]+/g, " ");
733
+ }
734
+
735
+ function toUiFile(file) {
736
+ const extension = file.filename.includes(".")
737
+ ? file.filename.split(".").pop().slice(0, 4)
738
+ : "file";
739
+ return {
740
+ id: file.id,
741
+ name: file.filename,
742
+ ext: extension,
743
+ type: file.mime_type,
744
+ size: formatBytes(file.size_bytes),
745
+ created: shortDate(file.created_at),
746
+ dl: Boolean(file.downloadable),
747
+ href: `/v1/files/${encodeURIComponent(file.id)}/content`,
748
+ scope: file.scope,
749
+ };
750
+ }
751
+
752
+ function toUiEvents(events) {
753
+ const firstTime = firstProcessedAt(events);
754
+ return events.map((event) => toUiEvent(event, firstTime));
755
+ }
756
+
757
+ export function toUiSessionEvent(event, firstTime = null) {
758
+ return toUiEvent(event, firstTime);
759
+ }
760
+
761
+ function toUiEvent(event, firstTime) {
762
+ const role = eventRole(event.type);
763
+ const content = contentText(event);
764
+ const usage = event.type === "span.model_request_end"
765
+ ? modelUsage(event.model_usage)
766
+ : null;
767
+ return {
768
+ id: event.id,
769
+ role,
770
+ type: event.type,
771
+ tag: eventTag(event),
772
+ transcript: isTranscriptEvent(event),
773
+ time: relativeTime(event.processed_at, firstTime),
774
+ text: eventSummary(event, content),
775
+ content,
776
+ raw: JSON.stringify(event, null, 2),
777
+ ok: event.type !== "session.error" && event.is_error !== true,
778
+ open: false,
779
+ tokens: usage ? `${usage.input} / ${usage.output}` : undefined,
780
+ dur: undefined,
781
+ pairedStart: event.model_request_start_id,
782
+ usage,
783
+ source: event,
784
+ confirm: (event.type === "agent.tool_use" || event.type === "agent.mcp_tool_use") && event.evaluated_permission === "ask",
785
+ tool: event.name,
786
+ cmd: event.input?.command ?? event.input?.cmd ?? JSON.stringify(event.input ?? {}),
787
+ };
788
+ }
789
+
790
+ function toUiSpans(events) {
791
+ const modelStarts = new Map();
792
+ const spans = [];
793
+ for (const event of events) {
794
+ if (event.type === "span.model_request_start") {
795
+ modelStarts.set(event.id, event);
796
+ continue;
797
+ }
798
+ if (event.type === "span.model_request_end") {
799
+ const start = modelStarts.get(event.pairedStart);
800
+ spans.push({
801
+ role: "span",
802
+ kind: event.ok ? "model" : "error",
803
+ label: "model_request",
804
+ left: 4 + spans.length * 18,
805
+ width: 14,
806
+ info: event.tokens ?? "model",
807
+ eventId: event.id,
808
+ });
809
+ if (start) modelStarts.delete(start.id);
810
+ }
811
+ }
812
+ for (const start of modelStarts.values()) {
813
+ spans.push({
814
+ role: "span",
815
+ kind: "open",
816
+ label: "model_request",
817
+ left: 4 + spans.length * 18,
818
+ width: 12,
819
+ info: "open — no end",
820
+ eventId: start.id,
821
+ });
822
+ }
823
+ return spans;
824
+ }
825
+
826
+ function summarizeToolset(tools) {
827
+ if (!Array.isArray(tools) || tools.length === 0) return "No tools configured";
828
+ const first = tools[0];
829
+ if (first?.type === "agent_toolset_20260401") return "agent_toolset_20260401";
830
+ return `${tools.length} tool${tools.length === 1 ? "" : "s"}`;
831
+ }
832
+
833
+ function summarizeToolPermission(tools) {
834
+ const policies = new Set();
835
+ for (const toolset of Array.isArray(tools) ? tools : []) {
836
+ if (toolset?.type !== "agent_toolset_20260401") continue;
837
+ for (const tool of effectiveBuiltinTools(toolset)) {
838
+ if (!tool.enabled) continue;
839
+ policies.add(tool.policy);
840
+ }
841
+ }
842
+ if (policies.size === 0) return "No enabled tools";
843
+ if (policies.size > 1) return "Mixed permissions";
844
+ const [policy] = policies;
845
+ if (policy === "always_ask") return "Ask before use";
846
+ if (policy === "deny") return "Denied";
847
+ return "Always allow";
848
+ }
849
+
850
+ function effectiveBuiltinTools(toolset) {
851
+ const defaultEnabled = toolset?.default_config?.enabled !== false;
852
+ const defaultPolicy = toolset?.default_config?.permission_policy?.type ?? "always_allow";
853
+ const configs = new Map(
854
+ (Array.isArray(toolset?.configs) ? toolset.configs : [])
855
+ .map((config) => [config.name, config]),
856
+ );
857
+ return CMA_BUILTIN_TOOL_NAMES.map((name) => {
858
+ const config = configs.get(name);
859
+ return {
860
+ name,
861
+ enabled: config?.enabled ?? defaultEnabled,
862
+ policy: config?.permission_policy?.type ?? defaultPolicy,
863
+ };
864
+ });
865
+ }
866
+
867
+ function eventRole(type) {
868
+ if (type.startsWith("user.")) return "user";
869
+ if (type.startsWith("agent.tool") || type.includes("tool_")) return "tool";
870
+ if (type.startsWith("agent.")) return "agent";
871
+ if (type.startsWith("span.")) return "span";
872
+ return "sys";
873
+ }
874
+
875
+ function eventTag(event) {
876
+ if (event.type === "agent.tool_use") return event.name ?? "tool";
877
+ if (event.type === "agent.tool_result") return event.is_error ? "error" : "exit 0";
878
+ if (event.type === "agent.mcp_tool_use") return event.name ?? "mcp tool";
879
+ if (event.type === "agent.mcp_tool_result") return event.is_error ? "error" : "mcp result";
880
+ if (event.type === "span.model_request_end") return event.is_error ? "error" : "model";
881
+ if (event.type === "session.error") return "error";
882
+ if (event.type.startsWith("session.status_")) return event.type.slice("session.status_".length);
883
+ return event.type.split(".").pop();
884
+ }
885
+
886
+ function eventSummary(event, content) {
887
+ if (content) return content.split("\n")[0];
888
+ if (event.type === "session.error") return event.error?.message ?? event.message ?? "session.error";
889
+ if (event.type === "agent.tool_use") return `agent.tool_use · ${event.name ?? "tool"}`;
890
+ if (event.type === "agent.tool_result") return `agent.tool_result · ${event.is_error ? "error" : "ok"}`;
891
+ if (event.type === "agent.mcp_tool_use") return `agent.mcp_tool_use · ${event.name ?? "tool"}`;
892
+ if (event.type === "agent.mcp_tool_result") return `agent.mcp_tool_result · ${event.is_error ? "error" : "ok"}`;
893
+ if (event.type === "span.model_request_start") return "model_request_start";
894
+ if (event.type === "span.model_request_end") return "model_request_end";
895
+ return event.type;
896
+ }
897
+
898
+ function isTranscriptEvent(event) {
899
+ return [
900
+ "user.message",
901
+ "user.interrupt",
902
+ "user.custom_tool_result",
903
+ "user.tool_confirmation",
904
+ "agent.message",
905
+ "agent.tool_use",
906
+ "agent.tool_result",
907
+ "agent.mcp_tool_use",
908
+ "agent.mcp_tool_result",
909
+ "agent.custom_tool_use",
910
+ ].includes(event.type);
911
+ }
912
+
913
+ function contentText(event) {
914
+ if (Array.isArray(event.content)) {
915
+ return event.content
916
+ .map((block) => block?.text ?? block?.content ?? JSON.stringify(block))
917
+ .join("\n");
918
+ }
919
+ if (typeof event.content === "string") return event.content;
920
+ if (event.input) return JSON.stringify(event.input, null, 2);
921
+ return "";
922
+ }
923
+
924
+ function modelUsage(usage) {
925
+ if (!usage) return null;
926
+ return {
927
+ input: numberLabel(usage.input_tokens ?? 0),
928
+ output: numberLabel(usage.output_tokens ?? 0),
929
+ cacheRead: numberLabel(usage.cache_read_input_tokens ?? 0),
930
+ cacheWrite: numberLabel(usage.cache_creation_input_tokens ?? 0),
931
+ };
932
+ }
933
+
934
+ function usageLabel(usage) {
935
+ if (!usage) return "—";
936
+ return `${usage.input_tokens ?? 0} / ${usage.output_tokens ?? 0}`;
937
+ }
938
+
939
+ function toUiStatus(session) {
940
+ if (session.archived_at) return "archived";
941
+ return session.status;
942
+ }
943
+
944
+ function sessionSignalsFromEvents(events) {
945
+ const latestIdle = [...events].reverse()
946
+ .find((event) => event.type === "session.status_idle");
947
+ const sessionError = [...events].reverse()
948
+ .find((event) => event.type === "session.error");
949
+ return {
950
+ requiresAction: stopReasonType(latestIdle?.stop_reason) === "requires_action",
951
+ sessionError: sessionError ? {
952
+ id: sessionError.id,
953
+ type: sessionError.error?.type ?? sessionError.type,
954
+ message: sessionError.error?.message ?? sessionError.message ?? "The session emitted an error.",
955
+ } : null,
956
+ };
957
+ }
958
+
959
+ function stopReasonType(stopReason) {
960
+ return stopReason && typeof stopReason === "object" ? stopReason.type : undefined;
961
+ }
962
+
963
+ function firstProcessedAt(events) {
964
+ const event = events.find((item) => item.processed_at);
965
+ return event?.processed_at ? Date.parse(event.processed_at) : null;
966
+ }
967
+
968
+ function relativeTime(value, firstTime) {
969
+ if (!value || !firstTime) return "queued";
970
+ const seconds = Math.max(0, Math.round((Date.parse(value) - firstTime) / 1000));
971
+ return `0:${String(seconds).padStart(2, "0")}`;
972
+ }
973
+
974
+ function shortId(id) {
975
+ if (!id || id.length <= 14) return id;
976
+ return `${id.slice(0, 5)}…${id.slice(-6)}`;
977
+ }
978
+
979
+ function shortDate(value) {
980
+ if (!value) return "—";
981
+ return new Intl.DateTimeFormat("en", { month: "short", day: "numeric" })
982
+ .format(new Date(value));
983
+ }
984
+
985
+ function numberLabel(value) {
986
+ return new Intl.NumberFormat("en").format(value);
987
+ }
988
+
989
+ function formatBytes(value) {
990
+ if (value < 1024) return `${value} B`;
991
+ if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} KB`;
992
+ return `${(value / 1024 / 1024).toFixed(1)} MB`;
993
+ }
994
+
995
+ // The JSX (compiled by in-browser Babel, not module-scoped) reaches the API
996
+ // through this global; Node tests import the module exports directly.
997
+ if (typeof window !== "undefined") {
998
+ window.OmaConsoleApi = {
999
+ loadConsoleData,
1000
+ hydrateSession,
1001
+ setAdminKey,
1002
+ setWorkspaceKey,
1003
+ clearCredentials,
1004
+ hasWorkspaceKey,
1005
+ canSubmitToolConfirmation,
1006
+ createWorkspace,
1007
+ listWorkspaces,
1008
+ mintKey,
1009
+ listKeys,
1010
+ revokeKey,
1011
+ getConsoleAuthStatus,
1012
+ loginConsoleWorkspace,
1013
+ loginConsoleAdmin,
1014
+ selectConsoleWorkspace,
1015
+ logoutConsole,
1016
+ listVaults,
1017
+ listVaultCredentials,
1018
+ createVault,
1019
+ createVaultCredential,
1020
+ listSkills,
1021
+ createSkill,
1022
+ listWorkspaceCredentialHealth,
1023
+ validateMcpOauthCredential,
1024
+ createIdempotencyIntent,
1025
+ createAgent,
1026
+ archiveAgent,
1027
+ updateAgentToolPermission,
1028
+ modelInputForSelection,
1029
+ listModelCatalog,
1030
+ createEnvironment,
1031
+ archiveEnvironment,
1032
+ deleteEnvironment,
1033
+ listEnvironmentNetworkingPresets,
1034
+ validateEnvironmentNetworkingHosts,
1035
+ createSession,
1036
+ archiveSession,
1037
+ deleteSession,
1038
+ sendSessionEvents,
1039
+ followSessionEvents,
1040
+ toUiSessionEvent,
1041
+ downloadFile,
1042
+ };
1043
+ }