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,93 @@
1
+ import { homedir } from "node:os";
2
+ import { join, resolve } from "node:path";
3
+ export const DEFAULT_MODEL_PROVIDER = "anthropic";
4
+ export const DEFAULT_MODEL_ID = "claude-sonnet-5";
5
+ export const DEFAULT_MODEL_PROVIDERS = ["anthropic", "openai", "openrouter"];
6
+ export const MODEL_DEPLOYMENT_ENV_KEYS = [
7
+ "OMA_HOME",
8
+ "OMA_MODEL_PROVIDERS",
9
+ "OMA_DEFAULT_MODEL_PROVIDER",
10
+ "OMA_DEFAULT_MODEL",
11
+ "OMA_PI_AUTH_FILE",
12
+ "OMA_PI_MODELS_FILE",
13
+ "OMA_ALLOW_MODEL_AUTH_COMMANDS",
14
+ ];
15
+ export function parseModelDeploymentConfigFromEnv(env, opts = {}) {
16
+ const allowedProviders = parseAllowedProviders(env.OMA_MODEL_PROVIDERS);
17
+ const defaultModel = parseDefaultModel(env, allowedProviders);
18
+ const home = optionalEnvString(env.OMA_HOME, "OMA_HOME") ?? opts.home ?? join(homedir(), ".oma");
19
+ const authPath = optionalEnvString(env.OMA_PI_AUTH_FILE, "OMA_PI_AUTH_FILE") ??
20
+ join(home, "pi", "auth.json");
21
+ const modelsPath = optionalEnvString(env.OMA_PI_MODELS_FILE, "OMA_PI_MODELS_FILE") ??
22
+ join(home, "pi", "models.json");
23
+ return {
24
+ allowedProviders,
25
+ defaultModel,
26
+ authPath: resolve(authPath),
27
+ modelsPath: resolve(modelsPath),
28
+ allowModelAuthCommands: parseBoolean(env.OMA_ALLOW_MODEL_AUTH_COMMANDS, {
29
+ defaultValue: false,
30
+ name: "OMA_ALLOW_MODEL_AUTH_COMMANDS",
31
+ }),
32
+ };
33
+ }
34
+ export function validateModelDeploymentConfigAgainstCatalog(config, catalog) {
35
+ for (const provider of config.allowedProviders) {
36
+ if (!catalog.hasProvider(provider)) {
37
+ throw new Error(`Model provider ${provider} is not available on this deployment`);
38
+ }
39
+ }
40
+ if (!catalog.hasModel(config.defaultModel)) {
41
+ throw new Error(`Default model ${config.defaultModel.provider}/${config.defaultModel.id} is not available on this deployment`);
42
+ }
43
+ return config;
44
+ }
45
+ function parseAllowedProviders(raw) {
46
+ const value = optionalEnvString(raw, "OMA_MODEL_PROVIDERS");
47
+ if (value === undefined)
48
+ return [...DEFAULT_MODEL_PROVIDERS];
49
+ const providers = value.split(",").map((entry) => entry.trim());
50
+ if (providers.some((provider) => provider.length === 0)) {
51
+ throw new Error("OMA_MODEL_PROVIDERS must be comma-separated provider names");
52
+ }
53
+ const seen = new Set();
54
+ for (const provider of providers) {
55
+ if (seen.has(provider)) {
56
+ throw new Error(`Duplicate model provider in OMA_MODEL_PROVIDERS: ${provider}`);
57
+ }
58
+ seen.add(provider);
59
+ }
60
+ return providers;
61
+ }
62
+ function parseDefaultModel(env, allowedProviders) {
63
+ const provider = optionalEnvString(env.OMA_DEFAULT_MODEL_PROVIDER, "OMA_DEFAULT_MODEL_PROVIDER");
64
+ const id = optionalEnvString(env.OMA_DEFAULT_MODEL, "OMA_DEFAULT_MODEL");
65
+ if (provider === undefined && id === undefined) {
66
+ return { provider: DEFAULT_MODEL_PROVIDER, id: DEFAULT_MODEL_ID };
67
+ }
68
+ if (provider === undefined || id === undefined) {
69
+ throw new Error("OMA_DEFAULT_MODEL_PROVIDER and OMA_DEFAULT_MODEL must be set together");
70
+ }
71
+ if (!allowedProviders.includes(provider)) {
72
+ throw new Error(`OMA_DEFAULT_MODEL_PROVIDER ${provider} must be listed in OMA_MODEL_PROVIDERS`);
73
+ }
74
+ return { provider, id };
75
+ }
76
+ function optionalEnvString(value, name) {
77
+ if (value === undefined)
78
+ return undefined;
79
+ const trimmed = value.trim();
80
+ if (trimmed === "")
81
+ throw new Error(`${name} must not be empty`);
82
+ return trimmed;
83
+ }
84
+ function parseBoolean(value, opts) {
85
+ const parsed = optionalEnvString(value, opts.name);
86
+ if (parsed === undefined)
87
+ return opts.defaultValue;
88
+ if (parsed === "true")
89
+ return true;
90
+ if (parsed === "false")
91
+ return false;
92
+ throw new Error(`${opts.name} must be "true" or "false"`);
93
+ }
@@ -0,0 +1,32 @@
1
+ import { Hono } from "hono";
2
+ import { invalidRequest } from "../errors.js";
3
+ import { parseLimit } from "../http.js";
4
+ import { workspaceIdFrom } from "../workspace.js";
5
+ export function modelCatalogRoutes(service) {
6
+ const app = new Hono();
7
+ app.get("/", (c) => {
8
+ return c.json(service.list(workspaceIdFrom(c), {
9
+ provider: nonEmptyQuery(c.req.query("provider"), "provider"),
10
+ available: parseAvailable(c.req.query("available")),
11
+ limit: parseLimit(c.req.query("limit")),
12
+ page: nonEmptyQuery(c.req.query("page"), "page"),
13
+ }), 200);
14
+ });
15
+ return app;
16
+ }
17
+ function nonEmptyQuery(value, name) {
18
+ if (value === undefined)
19
+ return undefined;
20
+ if (value.length === 0)
21
+ throw invalidRequest(`\`${name}\` must not be empty`);
22
+ return value;
23
+ }
24
+ function parseAvailable(value) {
25
+ if (value === undefined)
26
+ return undefined;
27
+ if (value === "true")
28
+ return true;
29
+ if (value === "false")
30
+ return false;
31
+ throw invalidRequest("`available` must be `true` or `false`");
32
+ }
@@ -0,0 +1,138 @@
1
+ import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
2
+ import { invalidRequest } from "../errors.js";
3
+ const DEFAULT_LIMIT = 50;
4
+ const MAX_LIMIT = 100;
5
+ export class DefaultModelCatalogService {
6
+ catalog;
7
+ cursorSigningKey = randomBytes(32);
8
+ constructor(catalog) {
9
+ this.catalog = catalog;
10
+ }
11
+ list(workspaceId, opts = {}) {
12
+ const context = {
13
+ provider: opts.provider ?? null,
14
+ available: opts.available ?? false,
15
+ };
16
+ const limit = normalizeLimit(opts.limit);
17
+ const anchor = opts.page === undefined
18
+ ? undefined
19
+ : decodeCursor(opts.page, workspaceId, context, this.cursorSigningKey).anchor;
20
+ const models = this.catalog
21
+ .list({
22
+ ...(context.provider === null ? {} : { provider: context.provider }),
23
+ availableOnly: context.available,
24
+ })
25
+ .slice()
26
+ .sort(compareModelRefs);
27
+ const startIndex = anchor === undefined
28
+ ? 0
29
+ : models.findIndex((model) => compareModelRefParts(model.provider, model.id, anchor.provider, anchor.id) > 0);
30
+ const pageModels = models.slice(startIndex < 0 ? models.length : startIndex, (startIndex < 0 ? models.length : startIndex) + limit + 1);
31
+ const dataModels = pageModels.slice(0, limit);
32
+ const next = pageModels.length > limit ? dataModels.at(-1) : undefined;
33
+ return {
34
+ data: dataModels.map((model) => this.toEntry(model)),
35
+ next_page: next === undefined
36
+ ? null
37
+ : encodeCursor({
38
+ v: 1,
39
+ provider: context.provider,
40
+ available: context.available,
41
+ anchor: { provider: next.provider, id: next.id },
42
+ }, workspaceId, this.cursorSigningKey),
43
+ };
44
+ }
45
+ toEntry(model) {
46
+ return {
47
+ type: "model",
48
+ provider: model.provider,
49
+ id: model.id,
50
+ name: model.name,
51
+ provider_name: this.catalog.modelRegistry.getProviderDisplayName(model.provider),
52
+ reasoning: model.reasoning,
53
+ input: [...model.input],
54
+ context_window: model.contextWindow,
55
+ max_output_tokens: model.maxTokens,
56
+ credentials_configured: this.catalog.hasConfiguredAuth(model),
57
+ default: model.provider === this.catalog.defaultModel.provider &&
58
+ model.id === this.catalog.defaultModel.id,
59
+ };
60
+ }
61
+ }
62
+ function normalizeLimit(limit) {
63
+ if (limit === undefined)
64
+ return DEFAULT_LIMIT;
65
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_LIMIT) {
66
+ throw invalidRequest("`limit` must be an integer from 1 through 100");
67
+ }
68
+ return limit;
69
+ }
70
+ function compareModelRefs(left, right) {
71
+ return compareModelRefParts(left.provider, left.id, right.provider, right.id);
72
+ }
73
+ function compareModelRefParts(leftProvider, leftId, rightProvider, rightId) {
74
+ if (leftProvider !== rightProvider)
75
+ return leftProvider < rightProvider ? -1 : 1;
76
+ if (leftId === rightId)
77
+ return 0;
78
+ return leftId < rightId ? -1 : 1;
79
+ }
80
+ function encodeCursor(payload, workspaceId, signingKey) {
81
+ const encodedPayload = Buffer.from(JSON.stringify(payload), "utf8").toString("base64url");
82
+ const signature = signCursor(encodedPayload, workspaceId, signingKey);
83
+ return `${encodedPayload}.${signature.toString("base64url")}`;
84
+ }
85
+ function decodeCursor(value, workspaceId, context, signingKey) {
86
+ let payload;
87
+ try {
88
+ const parts = value.split(".");
89
+ if (parts.length !== 2)
90
+ throw new Error("invalid cursor");
91
+ const [encodedPayload, encodedSignature] = parts;
92
+ const decoded = decodeCanonicalBase64url(encodedPayload);
93
+ const signature = decodeCanonicalBase64url(encodedSignature);
94
+ const expected = signCursor(encodedPayload, workspaceId, signingKey);
95
+ if (signature.length !== expected.length || !timingSafeEqual(signature, expected)) {
96
+ throw new Error("invalid signature");
97
+ }
98
+ payload = JSON.parse(decoded.toString("utf8"));
99
+ }
100
+ catch {
101
+ throw invalidRequest("invalid page cursor");
102
+ }
103
+ if (!isCursorPayload(payload))
104
+ throw invalidRequest("invalid page cursor");
105
+ if (payload.provider !== context.provider || payload.available !== context.available) {
106
+ throw invalidRequest("page token filters do not match request");
107
+ }
108
+ return payload;
109
+ }
110
+ function signCursor(encodedPayload, workspaceId, signingKey) {
111
+ return createHmac("sha256", signingKey)
112
+ .update("oma-model-catalog-page-v1\0", "utf8")
113
+ .update(workspaceId, "utf8")
114
+ .update("\0", "utf8")
115
+ .update(encodedPayload, "utf8")
116
+ .digest();
117
+ }
118
+ function decodeCanonicalBase64url(value) {
119
+ if (!/^[A-Za-z0-9_-]+$/.test(value))
120
+ throw new Error("invalid base64url");
121
+ const decoded = Buffer.from(value, "base64url");
122
+ if (decoded.toString("base64url") !== value)
123
+ throw new Error("invalid base64url");
124
+ return decoded;
125
+ }
126
+ function isCursorPayload(value) {
127
+ if (typeof value !== "object" || value === null)
128
+ return false;
129
+ const record = value;
130
+ const anchor = record.anchor;
131
+ return (record.v === 1 &&
132
+ (record.provider === null || typeof record.provider === "string") &&
133
+ typeof record.available === "boolean" &&
134
+ typeof anchor === "object" &&
135
+ anchor !== null &&
136
+ typeof anchor.provider === "string" &&
137
+ typeof anchor.id === "string");
138
+ }
@@ -0,0 +1,65 @@
1
+ // The control plane's metric inventory (plan 0121 §3.2). Every label is a
2
+ // closed enum — adding a label with tenant cardinality (workspace, session)
3
+ // requires a plan amendment; per-tenant breakdowns are Arc D.
4
+ import { monitorEventLoopDelay } from "node:perf_hooks";
5
+ import { MetricsRegistry } from "./metrics.js";
6
+ export const ROUTE_CLASSES = ["v1", "admin", "console", "health", "metrics", "other"];
7
+ const METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS", "other"];
8
+ // The statuses OMA actually emits; anything novel buckets to other.
9
+ const STATUSES = [
10
+ "200", "201", "204", "301", "304",
11
+ "400", "401", "403", "404", "405", "409", "410", "413", "415", "422", "429",
12
+ "500", "501", "503", "529",
13
+ "other",
14
+ ];
15
+ const TURN_OUTCOMES = ["completed", "interrupted", "abandoned", "other"];
16
+ const ADMISSION_LIMITS = ["sessions", "turns", "uploads", "streams", "other"];
17
+ const ADMISSION_STATUSES = ["429", "529", "other"];
18
+ const SANDBOX_EVENTS = ["created", "disposed", "reaped", "other"];
19
+ export const SANDBOX_PROVIDERS = [
20
+ "docker-local", "microsandbox-local", "host-passthrough", "none", "other",
21
+ ];
22
+ const LOG_LEVELS = ["debug", "info", "warn", "error", "audit", "other"];
23
+ const MODEL_ADMISSION_REASONS = ["provider_disabled", "model_unavailable", "credentials_missing", "other"];
24
+ const MODEL_PROVIDER_LABELS = ["anthropic", "openai", "google", "openrouter", "custom", "other"];
25
+ const NAMED_MODEL_PROVIDER_LABELS = new Set(["anthropic", "openai", "google", "openrouter"]);
26
+ const HTTP_BUCKETS = [0.005, 0.02, 0.1, 0.5, 2, 10];
27
+ // Extends past the 15-min idle TTL so TTL-adjacent turns don't vanish into
28
+ // +Inf (plan §9 Opus).
29
+ const TURN_BUCKETS = [0.25, 1, 5, 15, 60, 300, 900, 1800, 3600];
30
+ export function createControlPlaneMetrics() {
31
+ const registry = new MetricsRegistry();
32
+ return {
33
+ registry,
34
+ httpRequests: registry.counter("oma_http_requests_total", "HTTP requests handled, by route class, method, and status.", { route_class: ROUTE_CLASSES, method: METHODS, status: STATUSES }),
35
+ httpDuration: registry.histogram("oma_http_request_duration_seconds", "HTTP request duration in seconds.", HTTP_BUCKETS, { route_class: ROUTE_CLASSES }),
36
+ turnsTotal: registry.counter("oma_runtime_turns_total", "Runtime turns closed, by outcome.", { outcome: TURN_OUTCOMES }),
37
+ turnDuration: registry.histogram("oma_runtime_turn_duration_seconds", "Completed runtime turn duration in seconds (accept to close).", TURN_BUCKETS),
38
+ admissionRejections: registry.counter("oma_admission_rejections_total", "Admission-limit rejections, by limit and response status.", { limit: ADMISSION_LIMITS, status: ADMISSION_STATUSES }),
39
+ sandboxes: registry.counter("oma_sandboxes_total", "Sandbox lifecycle events, by event and provider.", { event: SANDBOX_EVENTS, provider: SANDBOX_PROVIDERS }),
40
+ sandboxProviderErrors: registry.counter("oma_sandbox_provider_errors_total", "Sandbox provider failures during session materialization.", { provider: SANDBOX_PROVIDERS }),
41
+ logEvents: registry.counter("oma_log_events_total", "Structured log lines emitted, by level.", { level: LOG_LEVELS }),
42
+ mcpToolCalls: registry.counter("oma_mcp_tool_calls_total", "MCP tool calls, by outcome (plan 0122).", { outcome: MCP_TOOL_CALL_OUTCOMES }),
43
+ mcpConnections: registry.counter("oma_mcp_connections_total", "MCP server connection attempts, by outcome (plan 0122).", { event: MCP_CONNECTION_EVENTS }),
44
+ modelAdmissionFailures: registry.counter("oma_model_admission_failures_total", "Model admission failures, by bounded reason and provider class.", { reason: MODEL_ADMISSION_REASONS, provider: MODEL_PROVIDER_LABELS }),
45
+ };
46
+ }
47
+ export function modelProviderMetricLabel(provider) {
48
+ if (NAMED_MODEL_PROVIDER_LABELS.has(provider))
49
+ return provider;
50
+ return provider.length > 0 ? "custom" : "other";
51
+ }
52
+ const MCP_TOOL_CALL_OUTCOMES = ["ok", "error", "denied", "timeout", "aborted"];
53
+ const MCP_CONNECTION_EVENTS = ["connected", "connect_failed", "auth_failed"];
54
+ export function registerProcessGauges(registry) {
55
+ const loopDelay = monitorEventLoopDelay({ resolution: 20 });
56
+ loopDelay.enable();
57
+ registry.gauge("oma_process_resident_memory_bytes", "Resident set size in bytes.", () => process.memoryUsage().rss);
58
+ registry.gauge("oma_process_heap_used_bytes", "V8 heap used in bytes.", () => process.memoryUsage().heapUsed);
59
+ registry.gauge("oma_process_event_loop_delay_seconds", "Mean event-loop delay in seconds since the last scrape.", () => {
60
+ const mean = loopDelay.mean / 1e9; // ns → s
61
+ loopDelay.reset();
62
+ return mean;
63
+ });
64
+ registry.gauge("oma_process_uptime_seconds", "Process uptime in seconds.", () => process.uptime());
65
+ }
@@ -0,0 +1,125 @@
1
+ // Hand-rolled Prometheus registry (plan 0121 §3.2). Deliberately not
2
+ // prom-client (dissent recorded in the plan): zero dependencies, and the
3
+ // label-safety property that makes exposition escaping unnecessary by
4
+ // construction — every label value is a CLOSED ENUM declared at metric
5
+ // registration. A value outside the declared set is bucketed to "other" at
6
+ // record time; dynamic strings can never reach the exposition. Record
7
+ // paths never throw. If this file outgrows ~150 lines of real logic or
8
+ // needs a fourth metric shape, switch to prom-client (plan §9).
9
+ export const EXPOSITION_CONTENT_TYPE = "text/plain; version=0.0.4; charset=utf-8";
10
+ const OTHER = "other";
11
+ // Validated, ordered label values → stable series key.
12
+ function seriesKey(values) {
13
+ return values.join("\u0000");
14
+ }
15
+ function validateLabels(schema, names, labels) {
16
+ return names.map((name) => {
17
+ const value = labels?.[name];
18
+ return value !== undefined && schema[name].includes(value) ? value : OTHER;
19
+ });
20
+ }
21
+ function renderLabels(names, values) {
22
+ if (names.length === 0)
23
+ return "";
24
+ const parts = names.map((name, i) => `${name}="${values[i]}"`);
25
+ return `{${parts.join(",")}}`;
26
+ }
27
+ export class MetricsRegistry {
28
+ names = new Set();
29
+ renderers = [];
30
+ claim(name) {
31
+ if (this.names.has(name)) {
32
+ throw new Error(`Metric ${name} is already registered`);
33
+ }
34
+ this.names.add(name);
35
+ }
36
+ counter(name, help, schema = {}) {
37
+ this.claim(name);
38
+ const labelNames = Object.keys(schema);
39
+ const series = new Map();
40
+ this.renderers.push(() => {
41
+ const lines = [`# HELP ${name} ${help}`, `# TYPE ${name} counter`];
42
+ for (const { values, total } of series.values()) {
43
+ lines.push(`${name}${renderLabels(labelNames, values)} ${total}`);
44
+ }
45
+ return lines.join("\n");
46
+ });
47
+ return {
48
+ inc: (labels, delta = 1) => {
49
+ const values = validateLabels(schema, labelNames, labels);
50
+ const key = seriesKey(values);
51
+ const entry = series.get(key);
52
+ if (entry)
53
+ entry.total += delta;
54
+ else
55
+ series.set(key, { values, total: delta });
56
+ },
57
+ };
58
+ }
59
+ // All gauges are scrape-time collectors: counters drift across crash and
60
+ // recovery paths, so gauge truth lives wherever the state lives (DB,
61
+ // InFlightGauge, process) and is read at exposition time. A collector
62
+ // that throws reports NaN rather than breaking the whole scrape.
63
+ gauge(name, help, collect) {
64
+ this.claim(name);
65
+ this.renderers.push(() => {
66
+ let value;
67
+ try {
68
+ value = collect();
69
+ }
70
+ catch {
71
+ value = Number.NaN;
72
+ }
73
+ return [
74
+ `# HELP ${name} ${help}`,
75
+ `# TYPE ${name} gauge`,
76
+ `${name} ${value}`,
77
+ ].join("\n");
78
+ });
79
+ }
80
+ histogram(name, help, buckets, schema = {}) {
81
+ this.claim(name);
82
+ const labelNames = Object.keys(schema);
83
+ const sorted = [...buckets].sort((a, b) => a - b);
84
+ const series = new Map();
85
+ this.renderers.push(() => {
86
+ const lines = [`# HELP ${name} ${help}`, `# TYPE ${name} histogram`];
87
+ for (const s of series.values()) {
88
+ let cumulative = 0;
89
+ for (let i = 0; i < sorted.length; i++) {
90
+ cumulative += s.counts[i];
91
+ const values = [...s.values, String(sorted[i])];
92
+ lines.push(`${name}_bucket${renderLabels([...labelNames, "le"], values)} ${cumulative}`);
93
+ }
94
+ lines.push(`${name}_bucket${renderLabels([...labelNames, "le"], [...s.values, "+Inf"])} ${s.count}`);
95
+ lines.push(`${name}_sum${renderLabels(labelNames, s.values)} ${s.sum}`);
96
+ lines.push(`${name}_count${renderLabels(labelNames, s.values)} ${s.count}`);
97
+ }
98
+ return lines.join("\n");
99
+ });
100
+ return {
101
+ observe: (seconds, labels) => {
102
+ if (!Number.isFinite(seconds))
103
+ return;
104
+ const values = validateLabels(schema, labelNames, labels);
105
+ const key = seriesKey(values);
106
+ let s = series.get(key);
107
+ if (!s) {
108
+ s = { values, counts: sorted.map(() => 0), sum: 0, count: 0 };
109
+ series.set(key, s);
110
+ }
111
+ for (let i = 0; i < sorted.length; i++) {
112
+ if (seconds <= sorted[i]) {
113
+ s.counts[i] += 1;
114
+ break;
115
+ }
116
+ }
117
+ s.sum += seconds;
118
+ s.count += 1;
119
+ },
120
+ };
121
+ }
122
+ exposition() {
123
+ return this.renderers.map((render) => render()).join("\n") + "\n";
124
+ }
125
+ }
@@ -0,0 +1,95 @@
1
+ // /health + /metrics (plan 0121 §3.1–3.2).
2
+ //
3
+ // /health is deliberately unauthenticated (LB and compose probes can't send
4
+ // keys; the body carries no tenant data). /metrics is FAIL-CLOSED: the
5
+ // deployment assembly resolves the exposure mode from the configured bind
6
+ // host — never from request headers or socket data — and this module only
7
+ // ever sees the resolved decision.
8
+ import { createHash, timingSafeEqual } from "node:crypto";
9
+ import { readFileSync, statfsSync } from "node:fs";
10
+ import { fileURLToPath } from "node:url";
11
+ import { EXPOSITION_CONTENT_TYPE } from "./metrics.js";
12
+ export const METRICS_TOKEN_ENV = "OMA_METRICS_TOKEN";
13
+ export const METRICS_TOKEN_FILE_ENV = "OMA_METRICS_TOKEN_FILE";
14
+ export function loadMetricsToken(env) {
15
+ const direct = env[METRICS_TOKEN_ENV];
16
+ const file = env[METRICS_TOKEN_FILE_ENV];
17
+ if (direct !== undefined && file !== undefined) {
18
+ throw new Error(`set exactly one of ${METRICS_TOKEN_ENV} or ${METRICS_TOKEN_FILE_ENV}, not both`);
19
+ }
20
+ if (direct === undefined && file === undefined)
21
+ return undefined;
22
+ const value = direct === undefined ? readFileSync(file, "utf8").trim() : direct.trim();
23
+ if (value.length === 0) {
24
+ throw new Error(`${direct === undefined ? METRICS_TOKEN_FILE_ENV : METRICS_TOKEN_ENV} must not be empty`);
25
+ }
26
+ return value;
27
+ }
28
+ export function sha256Token(token) {
29
+ return createHash("sha256").update(token).digest();
30
+ }
31
+ // package.json is app-root-relative so the same derivation works from a
32
+ // checkout and inside the image (same pattern as bundledConsoleRoot).
33
+ export function packageVersion() {
34
+ try {
35
+ const raw = readFileSync(fileURLToPath(new URL("../../../package.json", import.meta.url)), "utf8");
36
+ return String(JSON.parse(raw).version ?? "unknown");
37
+ }
38
+ catch {
39
+ return "unknown";
40
+ }
41
+ }
42
+ // Reported, never gating: a disk-threshold 503 would flap the single node
43
+ // and drive compose restart loops — alert on it instead (plan §3.1).
44
+ export function storageFreeBytes(root) {
45
+ try {
46
+ const stat = statfsSync(root);
47
+ return stat.bavail * stat.bsize;
48
+ }
49
+ catch {
50
+ return undefined;
51
+ }
52
+ }
53
+ export function registerObservabilityRoutes(app, config) {
54
+ const version = packageVersion();
55
+ app.get("/health", (c) => {
56
+ const storage = runCheck(config.health.storage);
57
+ const runtime = runCheck(config.health.runtime);
58
+ const ok = storage.status === "ok" && runtime.status === "ok";
59
+ c.header("cache-control", "no-store");
60
+ return c.json({
61
+ status: ok ? "ok" : "degraded",
62
+ version,
63
+ uptime_seconds: Math.round(process.uptime()),
64
+ checks: { storage, runtime },
65
+ }, ok ? 200 : 503);
66
+ });
67
+ const metrics = config.metrics;
68
+ if (metrics === undefined)
69
+ return;
70
+ app.get("/metrics", (c) => {
71
+ if (metrics.tokenSha256 !== undefined) {
72
+ const authorization = c.req.header("authorization");
73
+ const presented = authorization?.match(/^Bearer\s+(\S+)$/i)?.[1];
74
+ if (presented === undefined ||
75
+ !timingSafeEqual(sha256Token(presented), metrics.tokenSha256)) {
76
+ // No body detail: an unauthenticated caller learns nothing about
77
+ // the endpoint beyond its existence.
78
+ return c.body(null, 401, { "cache-control": "no-store" });
79
+ }
80
+ }
81
+ return c.body(metrics.registry.exposition(), 200, {
82
+ "content-type": EXPOSITION_CONTENT_TYPE,
83
+ "cache-control": "no-store",
84
+ });
85
+ });
86
+ }
87
+ function runCheck(check) {
88
+ try {
89
+ return check();
90
+ }
91
+ catch {
92
+ // Coarse on purpose: no stacks, no paths on an unauthenticated endpoint.
93
+ return { status: "failed" };
94
+ }
95
+ }