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,907 @@
1
+ import { Hono } from "hono";
2
+ import { bodyLimit } from "hono/body-limit";
3
+ import { deleteCookie, getCookie, setCookie } from "hono/cookie";
4
+ import { existsSync } from "node:fs";
5
+ import { join } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { createAdminAuth, loadAdminKey } from "./admin/auth.js";
8
+ import { log, parseBooleanFlag, setLogEventHook } from "./logging.js";
9
+ import { loadMetricsToken, registerObservabilityRoutes, sha256Token, storageFreeBytes, } from "./observability/routes.js";
10
+ import { createControlPlaneMetrics, modelProviderMetricLabel, registerProcessGauges, } from "./observability/instruments.js";
11
+ import { CONSOLE_MOUNT, registerConsoleRoutes, } from "./console/static.js";
12
+ import { CONSOLE_ADMIN_COOKIE, CONSOLE_WORKSPACE_COOKIE, createConsoleSessionAuth, } from "./console/auth.js";
13
+ import { registerOpenApiRoutes, } from "./openapi/routes.js";
14
+ import { adminRoutes } from "./admin/routes.js";
15
+ import { DefaultAdminService } from "./admin/service.js";
16
+ import { agentsRoutes } from "./agents/routes.js";
17
+ import { DefaultAgentService } from "./agents/service.js";
18
+ import { SqliteAgentStore } from "./agents/store.js";
19
+ import { environmentsRoutes } from "./environments/routes.js";
20
+ import { DefaultEnvironmentService } from "./environments/service.js";
21
+ import { SqliteEnvironmentStore } from "./environments/store.js";
22
+ import { sessionEventsRoutes } from "./events/routes.js";
23
+ import { DefaultSessionEventsService } from "./events/service.js";
24
+ import { SessionEventBroadcaster } from "./events/broadcaster.js";
25
+ import { EventStore } from "./events/store.js";
26
+ import { filesRoutes } from "./files/routes.js";
27
+ import { DefaultFileService } from "./files/service.js";
28
+ import { InMemoryFileStorage } from "./files/store.js";
29
+ import { skillsRoutes } from "./skills/routes.js";
30
+ import { DefaultSkillsService } from "./skills/service.js";
31
+ import { InMemorySkillsStore } from "./skills/store.js";
32
+ import { createBestEffortSessionOutputCoordinator } from "./deployment-session-output-coordinator.js";
33
+ import { createBestEffortRuntimeEventCoordinator } from "./deployment-runtime-event-coordinator.js";
34
+ import { createDeploymentPiSessionRunner, parseDeploymentRuntimeConfigFromEnv, } from "./deployment-runtime-config.js";
35
+ import { createDeploymentStoresFromEnv, } from "./deployment-storage.js";
36
+ import { ApiError, authenticationFailed, ensureApiError, requestTooLarge, requestId, toApiErrorBody, } from "./errors.js";
37
+ import { parseJsonBody } from "./http.js";
38
+ import { DEFAULT_WORKSPACE_ID } from "./workspace.js";
39
+ import { createAdmissionLimits, parseAdmissionLimitsFromEnv, } from "./admission.js";
40
+ import { secretsRoutes } from "./secrets/routes.js";
41
+ import { DefaultSecretsService } from "./secrets/service.js";
42
+ import { sessionsRoutes } from "./sessions/routes.js";
43
+ import { DefaultSessionService } from "./sessions/service.js";
44
+ import { SqliteSessionStore } from "./sessions/store.js";
45
+ import { vaultsRoutes } from "./vaults/routes.js";
46
+ import { DefaultVaultService } from "./vaults/service.js";
47
+ import { SqliteVaultStore } from "./vaults/store.js";
48
+ import { createFileMountResolver, createSkillSnapshotsProvider, createSessionEgressBundleResolver, createStoreBackedAgentRevisionProvider, createStoreBackedCustomToolsProvider, } from "./wiring.js";
49
+ import { createStoreBackedBuiltinToolAccessResolver } from "./sessions/pi/tool-permissions.js";
50
+ import { createStoreBackedMcpCredentialResolver, createStoreBackedMcpServersProvider, createStoreBackedMcpToolAccessResolver, } from "./sessions/pi/mcp/bridge.js";
51
+ import { createDefaultMcpRuntime } from "./sessions/pi/mcp/runtime.js";
52
+ import { PINNED_PI_MODEL_RUNTIME_VERSION, createPiModelCatalog, } from "./models/catalog.js";
53
+ import { modelCatalogRoutes } from "./models/routes.js";
54
+ import { DefaultModelCatalogService, } from "./models/service.js";
55
+ import { parseModelDeploymentConfigFromEnv, } from "./models/deployment-config.js";
56
+ import { createOmaAuthStorageBackend } from "./models/auth-storage-backend.js";
57
+ import { DEFAULT_MCP_OPERATION_TIMEOUT_MS } from "./sessions/pi/mcp/client.js";
58
+ import { createOauthRefreshTicker } from "./vaults/oauth-refresh-ticker.js";
59
+ import { translatePiEvent } from "./sessions/pi/translator.js";
60
+ import { FILES_API_BETA, MANAGED_AGENTS_BETA, SKILLS_API_BETA, } from "./api-constants.js";
61
+ export { FILES_API_BETA, MANAGED_AGENTS_BETA, SKILLS_API_BETA } from "./api-constants.js";
62
+ export const MAX_REQUEST_BODY_BYTES = 1_048_576;
63
+ // 0113 D5: exactly two values; unset stays disabled for the currently allowed
64
+ // rollout tiers but warns loudly; anything else fails construction.
65
+ export function parseDeploymentAuthMode(env, opts = {}) {
66
+ const raw = env.OMA_AUTH_MODE;
67
+ if (raw === undefined) {
68
+ (opts.warn ?? ((message) => log.warn("auth_mode_disabled", { detail: message })))("OMA_AUTH_MODE is unset; workspace authentication is DISABLED and all requests resolve to wrk_default. Set OMA_AUTH_MODE=api-key for any deployment beyond trusted single-node.");
69
+ return "disabled";
70
+ }
71
+ const mode = raw.trim();
72
+ if (mode === "api-key" || mode === "disabled") {
73
+ return mode;
74
+ }
75
+ throw new Error(`Unsupported OMA_AUTH_MODE: ${JSON.stringify(raw)} (expected "api-key" or "disabled")`);
76
+ }
77
+ export function createControlPlaneApp(services) {
78
+ if (services.admin && !services.auth) {
79
+ throw new Error("Admin API requires workspace authentication to be enabled");
80
+ }
81
+ const app = new Hono();
82
+ const defaultBodyLimit = bodyLimit({
83
+ maxSize: MAX_REQUEST_BODY_BYTES,
84
+ onError: (c) => {
85
+ const err = requestTooLarge();
86
+ return withAdminNoStore(c.req.path, jsonError(toApiErrorBody(err, c.get("requestId")), err.status));
87
+ },
88
+ });
89
+ // Metrics middleware registers FIRST: a first-registered middleware that
90
+ // awaits next() sees the FINAL response status — including onError-,
91
+ // notFound-, and bodyLimit-produced responses (verified against Hono's
92
+ // compose(); the context is mutated in place). Plan 0121 §2.
93
+ const observedMetrics = services.observability?.metrics;
94
+ if (observedMetrics !== undefined) {
95
+ app.use("*", async (c, next) => {
96
+ const startedAt = performance.now();
97
+ await next();
98
+ const route_class = routeClassForPath(c.req.path);
99
+ observedMetrics.httpRequests.inc({
100
+ route_class,
101
+ method: c.req.method,
102
+ status: String(c.res.status),
103
+ });
104
+ observedMetrics.httpDuration.observe((performance.now() - startedAt) / 1000, { route_class });
105
+ });
106
+ }
107
+ app.use("*", async (c, next) => {
108
+ const reqId = requestId();
109
+ c.header("request-id", reqId);
110
+ c.set("requestId", reqId);
111
+ await next();
112
+ });
113
+ // Auth before the beta gate, scoped to known Managed Agents prefixes, per
114
+ // the 0113 hosted probe: unknown paths 404 without touching auth; known
115
+ // paths 401 before beta/version are examined.
116
+ if (services.auth) {
117
+ const auth = services.auth;
118
+ app.use("*", async (c, next) => {
119
+ if (!isManagedAgentsRoute(c.req.path)) {
120
+ await next();
121
+ return;
122
+ }
123
+ const key = c.req.header("x-api-key");
124
+ const workspaceCookie = getCookie(c, CONSOLE_WORKSPACE_COOKIE);
125
+ const usedConsoleCookie = key === undefined && workspaceCookie !== undefined;
126
+ const workspaceId = key !== undefined
127
+ ? auth.authenticate(key)
128
+ : workspaceCookie === undefined
129
+ ? undefined
130
+ : services.consoleSessionAuth?.service.authenticateWorkspace(workspaceCookie);
131
+ if (workspaceId === undefined) {
132
+ const err = authenticationFailed();
133
+ return jsonError(toApiErrorBody(err, c.get("requestId")), err.status);
134
+ }
135
+ if (usedConsoleCookie && isUnsafeMethod(c.req.method) && !sameOriginForCookieWrite(c)) {
136
+ return c.text("Forbidden\n", 403);
137
+ }
138
+ c.set("workspaceId", workspaceId);
139
+ await next();
140
+ });
141
+ }
142
+ if (services.admin) {
143
+ const adminAuth = services.admin.auth;
144
+ app.use("*", async (c, next) => {
145
+ if (!isAdminRoute(c.req.path)) {
146
+ await next();
147
+ return;
148
+ }
149
+ c.header("cache-control", "no-store");
150
+ const key = c.req.header("x-admin-key");
151
+ const adminCookie = getCookie(c, CONSOLE_ADMIN_COOKIE);
152
+ const usedConsoleCookie = key === undefined && adminCookie !== undefined;
153
+ const authenticated = key !== undefined
154
+ ? adminAuth.verify(key)
155
+ : adminCookie === undefined
156
+ ? false
157
+ : services.consoleSessionAuth?.service.authenticateAdmin(adminCookie) === true;
158
+ if (!authenticated) {
159
+ const err = authenticationFailed();
160
+ const response = jsonError(toApiErrorBody(err, c.get("requestId")), err.status);
161
+ response.headers.set("cache-control", "no-store");
162
+ return response;
163
+ }
164
+ if (usedConsoleCookie && isUnsafeMethod(c.req.method) && !sameOriginForCookieWrite(c)) {
165
+ return withAdminNoStore(c.req.path, c.text("Forbidden\n", 403));
166
+ }
167
+ await next();
168
+ });
169
+ }
170
+ app.use("*", async (c, next) => {
171
+ const betaFeatures = parseBetaFeatures(c.req.header("anthropic-beta"));
172
+ if (isManagedAgentsRoute(c.req.path) && !hasRequiredBeta(c.req.path, betaFeatures)) {
173
+ const err = new ApiError(404, "not_found_error", "not found");
174
+ return jsonError(toApiErrorBody(err, c.get("requestId")), err.status);
175
+ }
176
+ await next();
177
+ });
178
+ app.use("*", async (c, next) => {
179
+ if (c.req.method === "POST" && (c.req.path === "/v1/files" || c.req.path === "/v1/skills" || /^\/v1\/skills\/[^/]+\/versions$/.test(c.req.path))) {
180
+ await next();
181
+ return;
182
+ }
183
+ return defaultBodyLimit(c, next);
184
+ });
185
+ if (services.consoleSessionAuth) {
186
+ registerConsoleSessionRoutes(app, services);
187
+ }
188
+ if (services.openapi) {
189
+ registerOpenApiRoutes(app, services.openapi);
190
+ }
191
+ if (services.models) {
192
+ app.route("/v1/model-catalog", modelCatalogRoutes(services.models));
193
+ }
194
+ app.route("/v1/agents", agentsRoutes(services.agents));
195
+ app.route("/v1/environments", environmentsRoutes(services.environments, services.environmentNetworking));
196
+ app.route("/v1/files", filesRoutes(services.files ?? new DefaultFileService(new InMemoryFileStorage()), services.admission));
197
+ app.route("/v1/skills", skillsRoutes(services.skills ?? new DefaultSkillsService(new InMemorySkillsStore()), services.admission));
198
+ app.route("/v1/secrets", secretsRoutes(services.secrets ?? new DefaultSecretsService(undefined)));
199
+ if (services.vaults) {
200
+ app.route("/v1/vaults", vaultsRoutes(services.vaults, services.mcp));
201
+ }
202
+ app.route("/v1/sessions", sessionsRoutes(services.sessions, services.sessionEvents));
203
+ app.route("/v1/sessions/:sessionId/events", sessionEventsRoutes(services.sessionEvents, services.admission));
204
+ if (services.admin) {
205
+ app.route("/admin", adminRoutes(services.admin.service));
206
+ }
207
+ if (services.console) {
208
+ app.get("/", (c) => c.redirect("/console/", 302));
209
+ registerConsoleRoutes(app, services.console);
210
+ }
211
+ if (services.observability) {
212
+ registerObservabilityRoutes(app, {
213
+ health: services.observability.health,
214
+ ...(services.observability.metrics === undefined
215
+ ? {}
216
+ : {
217
+ metrics: {
218
+ registry: services.observability.metrics.registry,
219
+ ...(services.observability.metricsTokenSha256 === undefined
220
+ ? {}
221
+ : { tokenSha256: services.observability.metricsTokenSha256 }),
222
+ },
223
+ }),
224
+ });
225
+ }
226
+ app.notFound((c) => {
227
+ const err = new ApiError(404, "not_found_error", "Route not found");
228
+ return withAdminNoStore(c.req.path, jsonError(toApiErrorBody(err, c.get("requestId")), err.status));
229
+ });
230
+ app.onError((error, c) => {
231
+ const err = ensureApiError(error);
232
+ if (err.status >= 500) {
233
+ // 0121 C1: 5xx responses were invisible in logs before this line.
234
+ log.error("request_failed", {
235
+ requestId: c.get("requestId"),
236
+ routeClass: routeClassForPath(c.req.path),
237
+ status: err.status,
238
+ error,
239
+ });
240
+ }
241
+ return withAdminNoStore(c.req.path, jsonError(toApiErrorBody(err, c.get("requestId")), err.status, err.retryAfterSeconds));
242
+ });
243
+ return app;
244
+ }
245
+ function registerConsoleSessionRoutes(app, services) {
246
+ const consoleAuth = services.consoleSessionAuth;
247
+ app.get("/console/auth/status", (c) => {
248
+ c.header("cache-control", "no-store");
249
+ const workspaceToken = getCookie(c, CONSOLE_WORKSPACE_COOKIE);
250
+ const adminToken = getCookie(c, CONSOLE_ADMIN_COOKIE);
251
+ const workspaceId = workspaceToken === undefined
252
+ ? undefined
253
+ : consoleAuth.service.authenticateWorkspace(workspaceToken);
254
+ return c.json({
255
+ auth_required: services.auth !== undefined,
256
+ workspace: workspaceId === undefined && services.auth === undefined
257
+ ? consoleWorkspace(DEFAULT_WORKSPACE_ID, consoleAuth.service)
258
+ : workspaceId === undefined
259
+ ? null
260
+ : {
261
+ id: workspaceId,
262
+ name: consoleAuth.service.workspaceName(workspaceId) ?? workspaceId,
263
+ },
264
+ admin: adminToken !== undefined && consoleAuth.service.authenticateAdmin(adminToken),
265
+ });
266
+ });
267
+ app.post("/console/auth/workspace", async (c) => {
268
+ if (!sameOriginForCookieWrite(c) || services.auth === undefined)
269
+ return c.text("Not found\n", 404);
270
+ const session = consoleAuth.service.createWorkspaceSession(stringField(await parseJsonBody(c.req), "api_key"));
271
+ if (session === undefined)
272
+ return consoleAuthenticationFailed(c);
273
+ setConsoleCookie(c, CONSOLE_WORKSPACE_COOKIE, session.token, consoleAuth.secureCookies, 30 * 24 * 60 * 60);
274
+ c.header("cache-control", "no-store");
275
+ return c.json({ workspace: consoleWorkspace(session.workspaceId, consoleAuth.service) });
276
+ });
277
+ app.post("/console/auth/admin", async (c) => {
278
+ if (!sameOriginForCookieWrite(c) || services.admin === undefined)
279
+ return c.text("Not found\n", 404);
280
+ const token = consoleAuth.service.createAdminSession(stringField(await parseJsonBody(c.req), "admin_key"));
281
+ if (token === undefined)
282
+ return consoleAuthenticationFailed(c);
283
+ setConsoleCookie(c, CONSOLE_ADMIN_COOKIE, token, consoleAuth.secureCookies, 8 * 60 * 60);
284
+ c.header("cache-control", "no-store");
285
+ return c.json({ admin: true });
286
+ });
287
+ app.post("/console/auth/select-workspace", async (c) => {
288
+ if (!sameOriginForCookieWrite(c))
289
+ return c.text("Forbidden\n", 403);
290
+ const adminToken = getCookie(c, CONSOLE_ADMIN_COOKIE);
291
+ if (adminToken === undefined)
292
+ return consoleAuthenticationFailed(c);
293
+ const workspaceId = stringField(await parseJsonBody(c.req), "workspace_id");
294
+ const token = consoleAuth.service.selectWorkspace(adminToken, workspaceId);
295
+ if (token === undefined)
296
+ return consoleAuthenticationFailed(c);
297
+ setConsoleCookie(c, CONSOLE_WORKSPACE_COOKIE, token, consoleAuth.secureCookies, 30 * 24 * 60 * 60);
298
+ c.header("cache-control", "no-store");
299
+ return c.json({ workspace: consoleWorkspace(workspaceId, consoleAuth.service) });
300
+ });
301
+ app.post("/console/auth/logout", (c) => {
302
+ if (!sameOriginForCookieWrite(c))
303
+ return c.text("Forbidden\n", 403);
304
+ const workspaceToken = getCookie(c, CONSOLE_WORKSPACE_COOKIE);
305
+ const adminToken = getCookie(c, CONSOLE_ADMIN_COOKIE);
306
+ if (workspaceToken !== undefined)
307
+ consoleAuth.service.revokeWorkspaceSession(workspaceToken);
308
+ if (adminToken !== undefined)
309
+ consoleAuth.service.revokeAdminSession(adminToken);
310
+ deleteConsoleCookie(c, CONSOLE_WORKSPACE_COOKIE, consoleAuth.secureCookies);
311
+ deleteConsoleCookie(c, CONSOLE_ADMIN_COOKIE, consoleAuth.secureCookies);
312
+ c.header("cache-control", "no-store");
313
+ return c.body(null, 204);
314
+ });
315
+ }
316
+ function consoleWorkspace(workspaceId, auth) {
317
+ return { id: workspaceId, name: auth.workspaceName(workspaceId) ?? workspaceId };
318
+ }
319
+ function stringField(body, name) {
320
+ if (typeof body !== "object" || body === null ||
321
+ typeof body[name] !== "string" ||
322
+ body[name].trim().length === 0) {
323
+ throw new ApiError(400, "invalid_request_error", `\`${name}\` must be a non-empty string`);
324
+ }
325
+ return body[name].trim();
326
+ }
327
+ function consoleAuthenticationFailed(c) {
328
+ c.header("cache-control", "no-store");
329
+ const err = authenticationFailed();
330
+ const response = jsonError(toApiErrorBody(err, c.get("requestId")), err.status);
331
+ response.headers.set("cache-control", "no-store");
332
+ return response;
333
+ }
334
+ function setConsoleCookie(c, name, token, secure, maxAge) {
335
+ setCookie(c, name, token, { httpOnly: true, maxAge, path: "/", sameSite: "Strict", secure });
336
+ }
337
+ function deleteConsoleCookie(c, name, secure) {
338
+ deleteCookie(c, name, { httpOnly: true, path: "/", sameSite: "Strict", secure });
339
+ }
340
+ function isUnsafeMethod(method) {
341
+ return method !== "GET" && method !== "HEAD" && method !== "OPTIONS";
342
+ }
343
+ function sameOriginForCookieWrite(c) {
344
+ const origin = c.req.header("origin");
345
+ if (origin === undefined)
346
+ return false;
347
+ try {
348
+ return new URL(origin).origin === new URL(c.req.url).origin;
349
+ }
350
+ catch {
351
+ return false;
352
+ }
353
+ }
354
+ export function createDeploymentControlPlaneApp(env = process.env, opts = {}) {
355
+ return createDeploymentControlPlane(env, opts, { backgroundWorkers: false }).app;
356
+ }
357
+ // Same wiring as createDeploymentControlPlaneApp, but hands back the stores
358
+ // so a caller that owns the process lifecycle (the appliance entrypoint,
359
+ // tests that boot twice against one OMA_HOME) can close them and release the
360
+ // .oma.lock instead of leaking them until process exit.
361
+ export function createDeploymentControlPlane(env = process.env, opts = {}, internal = {}) {
362
+ const runtimeConfig = parseDeploymentRuntimeConfigFromEnv(env);
363
+ const modelConfig = parseModelDeploymentConfigFromEnv(env);
364
+ const authMode = parseDeploymentAuthMode(env);
365
+ const adminKey = loadAdminKey(env);
366
+ // Parsed before any store opens so a malformed flag can't leak a store.
367
+ const tlsTerminated = parseBooleanFlag(env.OMA_TLS_TERMINATED, "OMA_TLS_TERMINATED");
368
+ const allowInsecureTransport = parseBooleanFlag(env.OMA_ALLOW_INSECURE_TRANSPORT, "OMA_ALLOW_INSECURE_TRANSPORT");
369
+ // 0121 §3.2 exposure matrix, resolved HERE from the configured bind host —
370
+ // the same source of truth as the transport gate below — and passed down
371
+ // as data. Never inferred per-request from headers or socket info. The
372
+ // `?? "1"` is load-bearing: a bare parse would default the flag OFF.
373
+ const metricsEnabled = parseBooleanFlag(env.OMA_METRICS ?? "1", "OMA_METRICS");
374
+ // Token is loaded only when the kill switch is on: OMA_METRICS=0 must be
375
+ // able to recover a deployment whose metrics-secret config is broken
376
+ // (missing token file, both variants set) — C2 review, Codex P2.
377
+ const metricsToken = metricsEnabled ? loadMetricsToken(env) : undefined;
378
+ const metricsServed = metricsEnabled && (metricsToken !== undefined || isLoopbackHost(env.OMA_HOST));
379
+ const metrics = metricsServed ? createControlPlaneMetrics() : undefined;
380
+ const admission = createAdmissionLimits(parseAdmissionLimitsFromEnv(env), metrics === undefined
381
+ ? undefined
382
+ : (limit, status) => metrics.admissionRejections.inc({ limit, status }));
383
+ const stores = createDeploymentStoresFromEnv(env);
384
+ if (authMode === "api-key" && stores.mode !== "durable") {
385
+ stores.close();
386
+ throw new Error("OMA_AUTH_MODE=api-key requires durable deployment storage: set OMA_SQLITE_PATH and OMA_FILE_STORAGE_ROOT. " +
387
+ "In-memory stores are per-process, so no API key could ever be provisioned and every request would fail with 401.");
388
+ }
389
+ // A configured master key means this deployment handles real credentials
390
+ // (secret values + credentialed egress). Without api-key auth every route —
391
+ // including POST/DELETE /v1/secrets — resolves to wrk_default unauthenticated,
392
+ // so anyone who can reach the port could overwrite the credentials the egress
393
+ // proxy injects. Refuse to boot that combination rather than warn (mirrors
394
+ // the api-key-requires-durable guard above); an operator who genuinely wants
395
+ // keyless secrets must not get there by forgetting OMA_AUTH_MODE.
396
+ if (stores.secrets !== undefined && authMode !== "api-key") {
397
+ stores.close();
398
+ throw new Error("A secrets master key (OMA_MASTER_KEY/OMA_MASTER_KEY_FILE) requires OMA_AUTH_MODE=api-key: " +
399
+ "without it the /v1/secrets API is unauthenticated and resolves to wrk_default, so anyone " +
400
+ "reaching the server could read metadata and overwrite the credentials used for egress injection.");
401
+ }
402
+ if (adminKey !== undefined && stores.mode !== "durable") {
403
+ stores.close();
404
+ throw new Error("OMA_ADMIN_KEY/OMA_ADMIN_KEY_FILE requires durable deployment storage: set OMA_SQLITE_PATH and OMA_FILE_STORAGE_ROOT. " +
405
+ "In-memory admin management would not survive restart.");
406
+ }
407
+ if (adminKey !== undefined && authMode !== "api-key") {
408
+ stores.close();
409
+ throw new Error("OMA_ADMIN_KEY/OMA_ADMIN_KEY_FILE requires OMA_AUTH_MODE=api-key: " +
410
+ "the admin API mints workspace keys for /v1 routes, so workspace authentication must be enabled.");
411
+ }
412
+ // 0120 §3.2 (extended after implementation review): every credentialed
413
+ // request — the admin key and each workspace x-api-key — travels in the
414
+ // clear over a plaintext non-loopback bind. Refuse that combination at
415
+ // boot unless the operator asserts a TLS front (OMA_TLS_TERMINATED=1 — an
416
+ // explicit assertion; X-Forwarded-Proto from the request is
417
+ // attacker-suppliable and deliberately not trusted) or consciously opts
418
+ // into plaintext (OMA_ALLOW_INSECURE_TRANSPORT=1, e.g. a Docker bind
419
+ // published only on the host's loopback). Auth-disabled deployments carry
420
+ // no credentials, so they are not gated.
421
+ if (authMode === "api-key" &&
422
+ !isLoopbackHost(env.OMA_HOST) &&
423
+ !tlsTerminated &&
424
+ !allowInsecureTransport) {
425
+ stores.close();
426
+ throw new Error(`API-key authentication on a non-loopback bind (OMA_HOST=${JSON.stringify(env.OMA_HOST)}) without TLS would send ` +
427
+ "every API key — and the admin key, if set — in cleartext. Either front the appliance with TLS and set " +
428
+ "OMA_TLS_TERMINATED=1, bind to loopback (unset OMA_HOST), or set OMA_ALLOW_INSECURE_TRANSPORT=1 if the " +
429
+ "plaintext exposure is intentional (e.g. a container port published only on the host's loopback).");
430
+ }
431
+ const broadcaster = new SessionEventBroadcaster(stores.events);
432
+ let wakeOauthTicker = () => undefined;
433
+ const vaultService = new DefaultVaultService(stores.vaults, {
434
+ onSchedulingChanged: () => wakeOauthTicker(),
435
+ ...(opts.testMcp?.allowInsecureTokenEndpoint === undefined
436
+ ? {}
437
+ : {
438
+ allowInsecureTokenEndpoint: opts.testMcp.allowInsecureTokenEndpoint,
439
+ }),
440
+ });
441
+ const sandboxProviderName = runtimeConfig.sandboxProviderSelection?.type ?? "none";
442
+ if (metrics !== undefined) {
443
+ // Turn outcomes/durations at the single post-commit chokepoint (plan
444
+ // 0121 §2): the store observer fires after the outermost events-store
445
+ // transaction releases, covering every apply path. archived/deleted are
446
+ // session lifecycle, not turn outcomes — their map entries are dropped
447
+ // without observation, so the map cannot grow unbounded.
448
+ const turnAcceptedAtMs = new Map();
449
+ stores.events.setRuntimeChangesObserver((changes) => {
450
+ for (const turn of changes.acceptedTurns ?? []) {
451
+ turnAcceptedAtMs.set(turn.turnId, performance.now());
452
+ }
453
+ for (const closure of changes.closedTurns ?? []) {
454
+ const acceptedAt = turnAcceptedAtMs.get(closure.turnId);
455
+ turnAcceptedAtMs.delete(closure.turnId);
456
+ const outcome = TURN_OUTCOME_BY_REASON[closure.reason];
457
+ if (outcome === undefined)
458
+ continue;
459
+ metrics.turnsTotal.inc({ outcome });
460
+ if (outcome === "completed" && acceptedAt !== undefined) {
461
+ metrics.turnDuration.observe((performance.now() - acceptedAt) / 1000);
462
+ }
463
+ }
464
+ });
465
+ registerProcessGauges(metrics.registry);
466
+ metrics.registry.gauge("oma_sessions_active", "Unarchived sessions (scrape-time count; served by idx_sessions_live).", () => stores.sessions.countAllActive());
467
+ metrics.registry.gauge("oma_runtime_turns_pending", "Pending runtime turns awaiting completion (scrape-time count).", () => stores.events.countAllPendingRuntimeTurns());
468
+ metrics.registry.gauge("oma_sse_streams_active", "Open SSE event streams.", () => admission.sseStreams.totalInFlight);
469
+ setLogEventHook((level) => metrics.logEvents.inc({ level }));
470
+ }
471
+ const mcpRuntime = runtimeConfig.mcp !== undefined || opts.runner?.mcp === undefined
472
+ ? createDefaultMcpRuntime(stores.vaults, opts.testMcp?.fetch, {
473
+ onScheduled: () => wakeOauthTicker(),
474
+ })
475
+ : undefined;
476
+ let modelCatalog;
477
+ try {
478
+ modelCatalog = opts.runner?.modelCatalog ?? createPiModelCatalog({
479
+ allowedProviders: modelConfig.allowedProviders,
480
+ defaultModel: modelConfig.defaultModel,
481
+ authBackend: createOmaAuthStorageBackend(modelConfig.authPath),
482
+ authPath: modelConfig.authPath,
483
+ modelsPath: modelConfig.modelsPath,
484
+ allowModelAuthCommands: modelConfig.allowModelAuthCommands,
485
+ });
486
+ }
487
+ catch (error) {
488
+ stores.close();
489
+ throw error;
490
+ }
491
+ const registeredModels = modelCatalog.list();
492
+ const readyModelCount = registeredModels.filter((model) => modelCatalog.hasConfiguredAuth(model)).length;
493
+ log.info("model_catalog_loaded", {
494
+ piVersion: PINNED_PI_MODEL_RUNTIME_VERSION,
495
+ providers: [...modelCatalog.allowedProviders],
496
+ defaultProvider: modelCatalog.defaultModel.provider,
497
+ defaultModel: modelCatalog.defaultModel.id,
498
+ registeredModelCount: registeredModels.length,
499
+ readyModelCount,
500
+ missingCredentialModelCount: registeredModels.length - readyModelCount,
501
+ });
502
+ for (const detail of modelCatalog.securityReport.warnings) {
503
+ log.warn("model_config_warning", { detail });
504
+ }
505
+ const modelAvailability = {
506
+ resolve(model) {
507
+ if (!modelCatalog.allowedProviders.has(model.provider)) {
508
+ metrics?.modelAdmissionFailures.inc({
509
+ reason: "provider_disabled",
510
+ provider: modelProviderMetricLabel(model.provider),
511
+ });
512
+ throw new ApiError(400, "invalid_request_error", `Model provider ${model.provider} is not enabled on this deployment`);
513
+ }
514
+ const resolved = modelCatalog.resolve(model);
515
+ if (!resolved) {
516
+ metrics?.modelAdmissionFailures.inc({
517
+ reason: "model_unavailable",
518
+ provider: modelProviderMetricLabel(model.provider),
519
+ });
520
+ throw new ApiError(400, "invalid_request_error", `Model ${model.provider}/${model.id} is not available on this deployment`);
521
+ }
522
+ return resolved;
523
+ },
524
+ assertAvailable(model) {
525
+ this.resolve(model);
526
+ },
527
+ assertReady(model) {
528
+ const resolved = this.resolve(model);
529
+ if (!modelCatalog.hasConfiguredAuth(resolved)) {
530
+ metrics?.modelAdmissionFailures.inc({
531
+ reason: "credentials_missing",
532
+ provider: modelProviderMetricLabel(model.provider),
533
+ });
534
+ throw new ApiError(400, "invalid_request_error", `Credentials for model provider ${model.provider} are not configured on this deployment`);
535
+ }
536
+ },
537
+ };
538
+ const runner = createDeploymentPiSessionRunner(runtimeConfig, {
539
+ ...opts.runner,
540
+ modelCatalog,
541
+ agentRevision: createStoreBackedAgentRevisionProvider({
542
+ sessions: stores.sessions,
543
+ agents: stores.agents,
544
+ }),
545
+ ...(metrics === undefined
546
+ ? {}
547
+ : {
548
+ onSandboxEvent: (event) => {
549
+ if (event === "error") {
550
+ metrics.sandboxProviderErrors.inc({ provider: sandboxProviderName });
551
+ }
552
+ else {
553
+ metrics.sandboxes.inc({ event, provider: sandboxProviderName });
554
+ }
555
+ },
556
+ onSandboxesReaped: (count) => metrics.sandboxes.inc({ event: "reaped", provider: sandboxProviderName }, count),
557
+ }),
558
+ resolveEgressBundle: opts.runner?.resolveEgressBundle ??
559
+ createSessionEgressBundleResolver({
560
+ sessions: stores.sessions,
561
+ environments: stores.environments,
562
+ ...(stores.secrets === undefined ? {} : { secrets: stores.secrets }),
563
+ }),
564
+ fileMountResolver: createFileMountResolver(stores.sessions, stores.files),
565
+ skills: createSkillSnapshotsProvider(stores.sessions),
566
+ customTools: opts.runner?.customTools ??
567
+ createStoreBackedCustomToolsProvider({
568
+ sessions: stores.sessions,
569
+ agents: stores.agents,
570
+ }),
571
+ builtinToolAccess: createStoreBackedBuiltinToolAccessResolver({
572
+ sessions: stores.sessions,
573
+ agents: stores.agents,
574
+ }),
575
+ // Plan 0122 §4.6: always wired (disabled agents still get their
576
+ // exhausted session.error), dialing gated by OMA_ENABLE_MCP.
577
+ mcp: opts.runner?.mcp ?? {
578
+ enabled: runtimeConfig.mcp !== undefined,
579
+ fetch: mcpRuntime.fetch,
580
+ servers: createStoreBackedMcpServersProvider({
581
+ sessions: stores.sessions,
582
+ agents: stores.agents,
583
+ }),
584
+ credentials: createStoreBackedMcpCredentialResolver({
585
+ sessions: stores.sessions,
586
+ vaults: vaultService,
587
+ refresh: mcpRuntime.refreshCoordinator,
588
+ }),
589
+ access: createStoreBackedMcpToolAccessResolver({
590
+ sessions: stores.sessions,
591
+ agents: stores.agents,
592
+ }),
593
+ ...(runtimeConfig.mcp?.operationTimeoutMs === undefined
594
+ ? {}
595
+ : { operationTimeoutMs: runtimeConfig.mcp.operationTimeoutMs }),
596
+ ...(metrics === undefined
597
+ ? {}
598
+ : {
599
+ onToolCall: (outcome) => metrics.mcpToolCalls.inc({ outcome }),
600
+ onConnection: (event) => metrics.mcpConnections.inc({ event }),
601
+ }),
602
+ },
603
+ });
604
+ const runtime = { runner, translate: translatePiEvent };
605
+ const sessionEvents = new DefaultSessionEventsService(stores.events, stores.sessions, broadcaster, {
606
+ ...runtime,
607
+ sessionOutputCoordinator: stores.sessionOutputCoordinator,
608
+ runtimeEventCoordinator: stores.runtimeEventCoordinator,
609
+ }, {
610
+ ...(admission.maxPendingRuntimeTurnsPerWorkspace === undefined
611
+ ? {}
612
+ : {
613
+ maxPendingRuntimeTurnsPerWorkspace: admission.maxPendingRuntimeTurnsPerWorkspace,
614
+ }),
615
+ ...(metrics === undefined
616
+ ? {}
617
+ : {
618
+ onAdmissionRejected: () => metrics.admissionRejections.inc({ limit: "turns", status: "429" }),
619
+ }),
620
+ });
621
+ sessionEvents.recoverAllAbandonedRuntimeTurns();
622
+ const consoleRoot = bundledConsoleRoot();
623
+ const openapiRoot = bundledOpenApiDocsRoot();
624
+ const adminAuth = adminKey === undefined ? undefined : createAdminAuth(adminKey);
625
+ const consoleSessionAuth = createConsoleSessionAuth({
626
+ workspaces: stores.workspaces,
627
+ ...(adminAuth === undefined ? {} : { admin: adminAuth }),
628
+ });
629
+ const app = createControlPlaneApp({
630
+ ...(adminKey === undefined
631
+ ? {}
632
+ : {
633
+ admin: {
634
+ service: new DefaultAdminService(stores.workspaces, stores.vaults),
635
+ auth: adminAuth,
636
+ },
637
+ }),
638
+ // Secret-free static content, so it serves whenever the dir shipped —
639
+ // deliberately not coupled to admin being enabled (0120 §3.1): a
640
+ // read-only /v1 browser is useful without an admin key.
641
+ ...(consoleRoot === undefined ? {} : { console: { root: consoleRoot } }),
642
+ consoleSessionAuth: {
643
+ service: consoleSessionAuth,
644
+ secureCookies: tlsTerminated,
645
+ },
646
+ ...(openapiRoot === undefined ? {} : { openapi: { root: openapiRoot } }),
647
+ ...(authMode === "api-key"
648
+ ? {
649
+ auth: {
650
+ authenticate: (key) => stores.workspaces.authenticate(key),
651
+ },
652
+ }
653
+ : {}),
654
+ agents: new DefaultAgentService(stores.agents, stores.skills, modelAvailability),
655
+ environments: new DefaultEnvironmentService(stores.environments, stores.sessions),
656
+ environmentNetworking: environmentNetworkingCapability(runtimeConfig),
657
+ files: new DefaultFileService(stores.files),
658
+ skills: new DefaultSkillsService(stores.skills),
659
+ secrets: new DefaultSecretsService(stores.secrets),
660
+ vaults: vaultService,
661
+ ...(runtimeConfig.mcp === undefined
662
+ ? {}
663
+ : {
664
+ mcp: {
665
+ fetch: mcpRuntime.fetch,
666
+ refresh: mcpRuntime.refreshCoordinator,
667
+ operationTimeoutMs: runtimeConfig.mcp.operationTimeoutMs ??
668
+ DEFAULT_MCP_OPERATION_TIMEOUT_MS,
669
+ },
670
+ }),
671
+ sessions: new DefaultSessionService(stores.sessions, stores.agents, stores.environments, stores.files, {
672
+ assertDeletable: sessionEvents.assertSessionDeletable.bind(sessionEvents),
673
+ runtime: runner,
674
+ modelAvailability,
675
+ skills: stores.skills,
676
+ egressCapability: {
677
+ canHonorNetworking: runtimeConfig.egress !== undefined,
678
+ hasSecretsStore: stores.secrets !== undefined,
679
+ },
680
+ vaults: vaultService,
681
+ deleteSessionRows: stores.sessionCoordinator.deleteSessionRows,
682
+ idempotencyLedger: stores.events,
683
+ createSessionRowsWithIdempotency: stores.sessions.createAndCompleteIdempotency.bind(stores.sessions),
684
+ ...(admission.maxActiveSessionsPerWorkspace === undefined
685
+ ? {}
686
+ : {
687
+ maxActiveSessionsPerWorkspace: admission.maxActiveSessionsPerWorkspace,
688
+ }),
689
+ ...(metrics === undefined
690
+ ? {}
691
+ : {
692
+ onAdmissionRejected: () => metrics.admissionRejections.inc({
693
+ limit: "sessions",
694
+ status: "429",
695
+ }),
696
+ }),
697
+ }),
698
+ sessionEvents,
699
+ models: new DefaultModelCatalogService(modelCatalog),
700
+ admission,
701
+ observability: {
702
+ health: {
703
+ storage: stores.mode === "durable"
704
+ ? () => {
705
+ stores.sessions.countAllActive();
706
+ // A configured object root that cannot be statfs'd (deleted,
707
+ // unmounted, permission-broken) is a FAILED check, not a
708
+ // silently-omitted field — C2 review, Codex-adv MEDIUM.
709
+ const free = env.OMA_FILE_STORAGE_ROOT === undefined
710
+ ? undefined
711
+ : storageFreeBytes(env.OMA_FILE_STORAGE_ROOT);
712
+ if (env.OMA_FILE_STORAGE_ROOT !== undefined && free === undefined) {
713
+ return { status: "failed" };
714
+ }
715
+ return {
716
+ status: "ok",
717
+ ...(free === undefined ? {} : { free_bytes: free }),
718
+ };
719
+ }
720
+ : // In-memory reports its mode rather than lying about durability.
721
+ () => ({ status: "ok", mode: "in-memory" }),
722
+ runtime: () => {
723
+ stores.events.countAllPendingRuntimeTurns();
724
+ return { status: "ok" };
725
+ },
726
+ },
727
+ ...(metrics === undefined ? {} : { metrics }),
728
+ ...(metricsToken === undefined
729
+ ? {}
730
+ : { metricsTokenSha256: sha256Token(metricsToken) }),
731
+ },
732
+ });
733
+ const oauthTicker = runtimeConfig.mcp !== undefined && internal.backgroundWorkers !== false
734
+ ? createOauthRefreshTicker({
735
+ store: stores.vaults,
736
+ refresh: mcpRuntime.refreshCoordinator,
737
+ onError: (error) => log.error("oauth_refresh_ticker_error", { error }),
738
+ })
739
+ : undefined;
740
+ wakeOauthTicker = () => oauthTicker?.wake();
741
+ let closePromise;
742
+ return {
743
+ app,
744
+ stores,
745
+ authMode,
746
+ close() {
747
+ closePromise ??= (async () => {
748
+ await oauthTicker?.close();
749
+ stores.close();
750
+ })();
751
+ return closePromise;
752
+ },
753
+ };
754
+ }
755
+ // undefined = the appliance's 127.0.0.1 default. Everything not provably
756
+ // loopback (0.0.0.0, ::, LAN addresses, hostnames) counts as non-loopback —
757
+ // the safe direction for a check that gates a root credential.
758
+ export function isLoopbackHost(host) {
759
+ if (host === undefined)
760
+ return true;
761
+ const h = host.trim().toLowerCase();
762
+ if (h === "localhost" || h === "::1" || h === "[::1]")
763
+ return true;
764
+ return /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(h);
765
+ }
766
+ // App-root-relative so the same derivation works from a checkout
767
+ // (<repo>/ui/…) and inside the Docker image (/app/ui/…, COPY ui ./ui).
768
+ function bundledConsoleRoot() {
769
+ const root = fileURLToPath(new URL("../../ui/managed-agents-console", import.meta.url));
770
+ return existsSync(join(root, "index.html")) ? root : undefined;
771
+ }
772
+ function bundledOpenApiDocsRoot() {
773
+ const root = fileURLToPath(new URL("../../ui/openapi-docs", import.meta.url));
774
+ return existsSync(join(root, "index.html")) ? root : undefined;
775
+ }
776
+ export function createInMemoryControlPlaneApp(opts = {}) {
777
+ const agentStore = SqliteAgentStore.open(":memory:");
778
+ const environmentStore = SqliteEnvironmentStore.open(":memory:");
779
+ const sessionStore = SqliteSessionStore.open(":memory:");
780
+ const vaultStore = SqliteVaultStore.open(":memory:");
781
+ const eventStore = EventStore.open(":memory:");
782
+ const fileStorage = new InMemoryFileStorage();
783
+ const skillsStore = new InMemorySkillsStore();
784
+ const sessionOutputCoordinator = createBestEffortSessionOutputCoordinator({
785
+ sessions: sessionStore,
786
+ events: eventStore,
787
+ files: fileStorage,
788
+ });
789
+ const runtimeEventCoordinator = createBestEffortRuntimeEventCoordinator({
790
+ sessions: sessionStore,
791
+ events: eventStore,
792
+ });
793
+ const broadcaster = new SessionEventBroadcaster(eventStore);
794
+ const vaultService = new DefaultVaultService(vaultStore);
795
+ const sessionEvents = new DefaultSessionEventsService(eventStore, sessionStore, broadcaster, opts.runtime
796
+ ? { ...opts.runtime, sessionOutputCoordinator, runtimeEventCoordinator }
797
+ : undefined);
798
+ const openapiRoot = bundledOpenApiDocsRoot();
799
+ return createControlPlaneApp({
800
+ ...(openapiRoot === undefined ? {} : { openapi: { root: openapiRoot } }),
801
+ agents: new DefaultAgentService(agentStore, skillsStore),
802
+ environments: new DefaultEnvironmentService(environmentStore, sessionStore),
803
+ ...(opts.environmentNetworking === undefined
804
+ ? {}
805
+ : { environmentNetworking: opts.environmentNetworking }),
806
+ files: new DefaultFileService(fileStorage),
807
+ skills: new DefaultSkillsService(skillsStore),
808
+ vaults: vaultService,
809
+ sessions: new DefaultSessionService(sessionStore, agentStore, environmentStore, fileStorage, {
810
+ assertDeletable: sessionEvents.assertSessionDeletable.bind(sessionEvents),
811
+ ...(opts.runtime?.runner ? { runtime: opts.runtime.runner } : {}),
812
+ skills: skillsStore,
813
+ vaults: vaultService,
814
+ idempotencyLedger: eventStore,
815
+ createSessionRowsWithIdempotency: sessionStore.createAndCompleteIdempotency.bind(sessionStore),
816
+ }),
817
+ sessionEvents,
818
+ });
819
+ }
820
+ export function parseBetaFeatures(header) {
821
+ return new Set((header ?? "")
822
+ .split(",")
823
+ .map((value) => value.trim())
824
+ .filter((value) => value.length > 0));
825
+ }
826
+ function environmentNetworkingCapability(runtimeConfig) {
827
+ const provider = runtimeConfig.sandboxProviderSelection?.type ?? null;
828
+ if (runtimeConfig.egress !== undefined) {
829
+ return { provider, egress_supported: true, reason: null };
830
+ }
831
+ const reason = provider === "docker-local"
832
+ ? "Docker-local is configured without the OMA egress sidecar. Restart through `oma up` to use networked presets."
833
+ : provider === null || provider === "none"
834
+ ? "No sandbox provider with egress support is configured."
835
+ : `Environment allowlists are not supported by ${provider}.`;
836
+ return { provider, egress_supported: false, reason };
837
+ }
838
+ function isManagedAgentsRoute(path) {
839
+ // Keep this in sync with the Managed Agents route registrations above.
840
+ return [
841
+ "/v1/agents",
842
+ "/v1/environments",
843
+ "/v1/files",
844
+ "/v1/skills",
845
+ // Secrets MUST be auth-gated: leaving it off this list would skip the
846
+ // auth middleware and fall back to wrk_default (plan 0117e-2).
847
+ "/v1/secrets",
848
+ // Vault credentials also carry secret metadata and must not silently bind
849
+ // to wrk_default if someone forgets the auth prefix registration.
850
+ "/v1/vaults",
851
+ "/v1/sessions",
852
+ "/v1/model-catalog",
853
+ ].some((prefix) => path === prefix || path.startsWith(`${prefix}/`));
854
+ }
855
+ function isAdminRoute(path) {
856
+ return path === "/admin" || path.startsWith("/admin/");
857
+ }
858
+ // Turn close reasons → metric outcomes (plan 0121 §3.2): archived/deleted
859
+ // are session lifecycle, not turn outcomes, and are deliberately unmapped.
860
+ const TURN_OUTCOME_BY_REASON = {
861
+ completed: "completed",
862
+ interrupted: "interrupted",
863
+ terminalized: "abandoned",
864
+ };
865
+ export function routeClassForPath(path) {
866
+ if (isManagedAgentsRoute(path))
867
+ return "v1";
868
+ if (isAdminRoute(path))
869
+ return "admin";
870
+ if (path === CONSOLE_MOUNT || path.startsWith(`${CONSOLE_MOUNT}/`))
871
+ return "console";
872
+ if (path === "/health")
873
+ return "health";
874
+ if (path === "/metrics")
875
+ return "metrics";
876
+ return "other";
877
+ }
878
+ function withAdminNoStore(path, response) {
879
+ if (isAdminRoute(path)) {
880
+ response.headers.set("cache-control", "no-store");
881
+ }
882
+ return response;
883
+ }
884
+ function hasRequiredBeta(path, betaFeatures) {
885
+ if (betaFeatures.has(MANAGED_AGENTS_BETA)) {
886
+ return true;
887
+ }
888
+ if (path === "/v1/files" || path.startsWith("/v1/files/")) {
889
+ return betaFeatures.has(FILES_API_BETA);
890
+ }
891
+ if (path === "/v1/skills" || path.startsWith("/v1/skills/")) {
892
+ return betaFeatures.has(SKILLS_API_BETA);
893
+ }
894
+ return false;
895
+ }
896
+ function jsonError(body, status, retryAfterSeconds) {
897
+ return new Response(JSON.stringify(body), {
898
+ status,
899
+ headers: {
900
+ "content-type": "application/json; charset=UTF-8",
901
+ "request-id": body.request_id,
902
+ ...(retryAfterSeconds === undefined
903
+ ? {}
904
+ : { "retry-after": String(retryAfterSeconds) }),
905
+ },
906
+ });
907
+ }