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,43 @@
1
+ import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
2
+ import { readFileSync } from "node:fs";
3
+ export const ADMIN_KEY_ENV = "OMA_ADMIN_KEY";
4
+ export const ADMIN_KEY_FILE_ENV = "OMA_ADMIN_KEY_FILE";
5
+ const ADMIN_KEY_BYTES = 32;
6
+ export function generateAdminKey() {
7
+ return randomBytes(ADMIN_KEY_BYTES).toString("base64");
8
+ }
9
+ export function loadAdminKey(env = process.env) {
10
+ const direct = env[ADMIN_KEY_ENV];
11
+ const file = env[ADMIN_KEY_FILE_ENV];
12
+ if (direct !== undefined && file !== undefined) {
13
+ throw new Error(`set exactly one of ${ADMIN_KEY_ENV} or ${ADMIN_KEY_FILE_ENV}, not both`);
14
+ }
15
+ if (direct === undefined && file === undefined)
16
+ return undefined;
17
+ const value = direct === undefined ? readFileSync(file, "utf8").trim() : direct.trim();
18
+ parseAdminKey(value, direct === undefined ? ADMIN_KEY_FILE_ENV : ADMIN_KEY_ENV);
19
+ return value;
20
+ }
21
+ function parseAdminKey(value, source) {
22
+ const decoded = Buffer.from(value, "base64");
23
+ if (decoded.length !== ADMIN_KEY_BYTES ||
24
+ decoded.toString("base64") !== value) {
25
+ throw new Error(`${source} must be exactly ${ADMIN_KEY_BYTES} random bytes, ` +
26
+ `base64-encoded (generate one with: node -e "console.log(` +
27
+ `require('crypto').randomBytes(32).toString('base64'))")`);
28
+ }
29
+ }
30
+ export function createAdminAuth(adminKey) {
31
+ const expectedDigest = sha256(adminKey);
32
+ return {
33
+ verify(presented) {
34
+ return timingSafeEqual(sha256(presented), expectedDigest);
35
+ },
36
+ fingerprint() {
37
+ return expectedDigest.toString("hex");
38
+ },
39
+ };
40
+ }
41
+ function sha256(value) {
42
+ return createHash("sha256").update(value).digest();
43
+ }
@@ -0,0 +1,76 @@
1
+ import { Hono } from "hono";
2
+ import { invalidRequest } from "../errors.js";
3
+ import { parseJsonBody, parseLimit } from "../http.js";
4
+ import { log } from "../logging.js";
5
+ export function adminRoutes(service) {
6
+ const app = new Hono();
7
+ app.post("/workspaces", async (c) => {
8
+ const body = await parseJsonBody(c.req);
9
+ const workspace = service.createWorkspace(body);
10
+ emitAdminAudit(c.get("requestId"), {
11
+ action: "create_workspace",
12
+ workspace_id: workspace.id,
13
+ });
14
+ return c.json(workspace, 201);
15
+ });
16
+ app.get("/workspaces", (c) => {
17
+ return c.json(service.listWorkspaces(), 200);
18
+ });
19
+ app.get("/workspaces/:id", (c) => {
20
+ return c.json(service.getWorkspace(c.req.param("id")), 200);
21
+ });
22
+ app.post("/workspaces/:id/keys", async (c) => {
23
+ const body = await parseOptionalJsonBody(c.req);
24
+ const minted = service.mintKey(c.req.param("id"), body);
25
+ emitAdminAudit(c.get("requestId"), {
26
+ action: "mint_key",
27
+ workspace_id: minted.workspace_id,
28
+ key_sha256: minted.key_sha256,
29
+ });
30
+ return c.json(minted, 201);
31
+ });
32
+ app.get("/workspaces/:id/keys", (c) => {
33
+ return c.json(service.listKeys(c.req.param("id")), 200);
34
+ });
35
+ app.get("/workspaces/:id/mcp-credentials", (c) => {
36
+ const limit = parseLimit(c.req.query("limit"));
37
+ const page = c.req.query("page") || undefined;
38
+ return c.json(service.listWorkspaceCredentialMetadata(c.req.param("id"), {
39
+ ...(limit === undefined ? {} : { limit }),
40
+ ...(page === undefined ? {} : { page }),
41
+ }), 200);
42
+ });
43
+ app.delete("/keys/:sha256", (c) => {
44
+ const revoked = service.revokeKey(c.req.param("sha256"));
45
+ emitAdminAudit(c.get("requestId"), {
46
+ action: "revoke_key",
47
+ workspace_id: revoked.workspace_id,
48
+ key_sha256: revoked.key_sha256,
49
+ revoked_at: revoked.revoked_at,
50
+ });
51
+ return c.json(revoked, 200);
52
+ });
53
+ return app;
54
+ }
55
+ function emitAdminAudit(requestId, event) {
56
+ // Wire shape predates the structured logger; `type: "admin_audit"` is the
57
+ // grep contract and stays (0121 C1 — the logger adds ts/level/event around
58
+ // it). Emitted at the audit level: OMA_LOG_LEVEL must never be able to
59
+ // silence the admin audit trail.
60
+ log.audit("admin_audit", {
61
+ type: "admin_audit",
62
+ request_id: requestId,
63
+ ...event,
64
+ });
65
+ }
66
+ async function parseOptionalJsonBody(req) {
67
+ const text = await req.text();
68
+ if (text.trim() === "")
69
+ return undefined;
70
+ try {
71
+ return JSON.parse(text);
72
+ }
73
+ catch (error) {
74
+ throw invalidRequest("Request body must be valid JSON", String(error));
75
+ }
76
+ }
@@ -0,0 +1,102 @@
1
+ import { invalidRequest, notFound } from "../errors.js";
2
+ export class DefaultAdminService {
3
+ workspaces;
4
+ vaults;
5
+ constructor(workspaces, vaults) {
6
+ this.workspaces = workspaces;
7
+ this.vaults = vaults;
8
+ }
9
+ createWorkspace(input) {
10
+ const { name } = parseCreateWorkspace(input);
11
+ return toAdminWorkspace(this.workspaces.createWorkspace(name));
12
+ }
13
+ listWorkspaces() {
14
+ return this.workspaces.listWorkspaces().map(toAdminWorkspace);
15
+ }
16
+ getWorkspace(workspaceId) {
17
+ const row = this.workspaces.getWorkspace(workspaceId);
18
+ if (!row)
19
+ throw notFound(`Workspace not found: ${workspaceId}`);
20
+ return toAdminWorkspace(row);
21
+ }
22
+ mintKey(workspaceId, input) {
23
+ const { label } = parseMintKey(input);
24
+ try {
25
+ return toAdminMintedKey(this.workspaces.mintKey(workspaceId, label));
26
+ }
27
+ catch (error) {
28
+ if (error instanceof Error && error.message.startsWith("Workspace not found:")) {
29
+ throw notFound(error.message);
30
+ }
31
+ throw error;
32
+ }
33
+ }
34
+ listKeys(workspaceId) {
35
+ if (!this.workspaces.getWorkspace(workspaceId)) {
36
+ throw notFound(`Workspace not found: ${workspaceId}`);
37
+ }
38
+ return this.workspaces.listKeys(workspaceId).map(toAdminKeyMetadata);
39
+ }
40
+ listWorkspaceCredentialMetadata(workspaceId, opts) {
41
+ if (!this.workspaces.getWorkspace(workspaceId)) {
42
+ throw notFound(`Workspace not found: ${workspaceId}`);
43
+ }
44
+ return this.vaults?.listWorkspaceCredentialAdminMetadata(workspaceId, opts)
45
+ ?? { data: [], has_more: false, next_page: null };
46
+ }
47
+ revokeKey(keySha256) {
48
+ const existing = this.workspaces.getKey(keySha256);
49
+ if (!existing)
50
+ throw notFound(`Key not found: ${keySha256}`);
51
+ if (existing.revoked_at !== null)
52
+ return toAdminKeyMetadata(existing);
53
+ this.workspaces.revokeKey(keySha256);
54
+ return toAdminKeyMetadata(this.workspaces.getKey(keySha256) ?? existing);
55
+ }
56
+ }
57
+ function parseCreateWorkspace(input) {
58
+ if (typeof input !== "object" || input === null || Array.isArray(input)) {
59
+ throw invalidRequest("Request body must be a JSON object");
60
+ }
61
+ const { name } = input;
62
+ if (typeof name !== "string" || name.trim().length === 0) {
63
+ throw invalidRequest("`name` must be a non-empty string");
64
+ }
65
+ return { name: name.trim() };
66
+ }
67
+ function parseMintKey(input) {
68
+ if (input === undefined)
69
+ return { label: "default" };
70
+ if (typeof input !== "object" || input === null || Array.isArray(input)) {
71
+ throw invalidRequest("Request body must be a JSON object");
72
+ }
73
+ const { label = "default" } = input;
74
+ if (typeof label !== "string" || label.trim().length === 0) {
75
+ throw invalidRequest("`label` must be a non-empty string");
76
+ }
77
+ return { label: label.trim() };
78
+ }
79
+ function toAdminWorkspace(row) {
80
+ return {
81
+ id: row.workspace_id,
82
+ name: row.name,
83
+ created_at: row.created_at,
84
+ };
85
+ }
86
+ function toAdminMintedKey(row) {
87
+ return {
88
+ workspace_id: row.workspaceId,
89
+ label: row.label,
90
+ key_sha256: row.keySha256,
91
+ api_key: row.plaintextKey,
92
+ };
93
+ }
94
+ function toAdminKeyMetadata(row) {
95
+ return {
96
+ key_sha256: row.key_sha256,
97
+ workspace_id: row.workspace_id,
98
+ label: row.label,
99
+ created_at: row.created_at,
100
+ revoked_at: row.revoked_at,
101
+ };
102
+ }
@@ -0,0 +1,90 @@
1
+ import { overloaded, rateLimited } from "./errors.js";
2
+ export function parseAdmissionLimitsFromEnv(env) {
3
+ return {
4
+ ...limitFromEnv("maxActiveSessionsPerWorkspace", "OMA_MAX_ACTIVE_SESSIONS_PER_WORKSPACE", env.OMA_MAX_ACTIVE_SESSIONS_PER_WORKSPACE),
5
+ ...limitFromEnv("maxPendingRuntimeTurnsPerWorkspace", "OMA_MAX_PENDING_RUNTIME_TURNS_PER_WORKSPACE", env.OMA_MAX_PENDING_RUNTIME_TURNS_PER_WORKSPACE),
6
+ ...limitFromEnv("maxConcurrentUploadsPerWorkspace", "OMA_MAX_CONCURRENT_UPLOADS_PER_WORKSPACE", env.OMA_MAX_CONCURRENT_UPLOADS_PER_WORKSPACE),
7
+ ...limitFromEnv("maxConcurrentUploads", "OMA_MAX_CONCURRENT_UPLOADS", env.OMA_MAX_CONCURRENT_UPLOADS),
8
+ ...limitFromEnv("maxConcurrentSseStreamsPerWorkspace", "OMA_MAX_CONCURRENT_SSE_STREAMS_PER_WORKSPACE", env.OMA_MAX_CONCURRENT_SSE_STREAMS_PER_WORKSPACE),
9
+ ...limitFromEnv("maxConcurrentSseStreams", "OMA_MAX_CONCURRENT_SSE_STREAMS", env.OMA_MAX_CONCURRENT_SSE_STREAMS),
10
+ };
11
+ }
12
+ function limitFromEnv(key, name, raw) {
13
+ if (raw === undefined)
14
+ return {};
15
+ const value = Number(raw.trim());
16
+ if (!Number.isInteger(value) || value <= 0) {
17
+ throw new Error(`${name} must be a positive integer, got ${JSON.stringify(raw)}`);
18
+ }
19
+ return { [key]: value };
20
+ }
21
+ // Tracks in-flight operations whose cost is their lifetime (uploads buffering
22
+ // in RAM, open SSE streams). acquire() throws the wire-shaped rejection;
23
+ // the returned release is idempotent so close/cancel/error paths can all
24
+ // call it safely.
25
+ export class InFlightGauge {
26
+ resource;
27
+ maxPerWorkspace;
28
+ maxTotal;
29
+ onReject;
30
+ perWorkspaceCounts = new Map();
31
+ total = 0;
32
+ constructor(resource, maxPerWorkspace, maxTotal, onReject) {
33
+ this.resource = resource;
34
+ this.maxPerWorkspace = maxPerWorkspace;
35
+ this.maxTotal = maxTotal;
36
+ this.onReject = onReject;
37
+ }
38
+ acquire(workspaceId) {
39
+ const current = this.perWorkspaceCounts.get(workspaceId) ?? 0;
40
+ if (this.maxPerWorkspace !== undefined && current >= this.maxPerWorkspace) {
41
+ this.onReject?.("429");
42
+ throw rateLimited(`Concurrent ${this.resource} limit reached for this workspace; retry later`);
43
+ }
44
+ if (this.maxTotal !== undefined && this.total >= this.maxTotal) {
45
+ this.onReject?.("529");
46
+ throw overloaded();
47
+ }
48
+ this.perWorkspaceCounts.set(workspaceId, current + 1);
49
+ this.total += 1;
50
+ let released = false;
51
+ return () => {
52
+ if (released)
53
+ return;
54
+ released = true;
55
+ const count = this.perWorkspaceCounts.get(workspaceId) ?? 1;
56
+ if (count <= 1)
57
+ this.perWorkspaceCounts.delete(workspaceId);
58
+ else
59
+ this.perWorkspaceCounts.set(workspaceId, count - 1);
60
+ this.total -= 1;
61
+ };
62
+ }
63
+ inFlight(workspaceId) {
64
+ return this.perWorkspaceCounts.get(workspaceId) ?? 0;
65
+ }
66
+ // For the /metrics SSE-streams gauge (0121 C2).
67
+ get totalInFlight() {
68
+ return this.total;
69
+ }
70
+ }
71
+ export function createAdmissionLimits(config,
72
+ // 0121 C2: telemetry-only; absent hooks change nothing.
73
+ onRejection) {
74
+ return {
75
+ ...(config.maxActiveSessionsPerWorkspace === undefined
76
+ ? {}
77
+ : { maxActiveSessionsPerWorkspace: config.maxActiveSessionsPerWorkspace }),
78
+ ...(config.maxPendingRuntimeTurnsPerWorkspace === undefined
79
+ ? {}
80
+ : {
81
+ maxPendingRuntimeTurnsPerWorkspace: config.maxPendingRuntimeTurnsPerWorkspace,
82
+ }),
83
+ uploads: new InFlightGauge("file upload", config.maxConcurrentUploadsPerWorkspace, config.maxConcurrentUploads, onRejection === undefined
84
+ ? undefined
85
+ : (status) => onRejection("uploads", status)),
86
+ sseStreams: new InFlightGauge("SSE stream", config.maxConcurrentSseStreamsPerWorkspace, config.maxConcurrentSseStreams, onRejection === undefined
87
+ ? undefined
88
+ : (status) => onRejection("streams", status)),
89
+ };
90
+ }
@@ -0,0 +1,63 @@
1
+ import { Hono } from "hono";
2
+ import { invalidRequest } from "../errors.js";
3
+ import { parseJsonBody, parseLimit } from "../http.js";
4
+ import { workspaceIdFrom } from "../workspace.js";
5
+ export function agentsRoutes(service) {
6
+ const app = new Hono();
7
+ app.post("/", async (c) => {
8
+ const body = await parseJsonBody(c.req);
9
+ const agent = service.create(workspaceIdFrom(c), body);
10
+ return c.json(agent, 200);
11
+ });
12
+ app.get("/", (c) => {
13
+ const limit = parseLimit(c.req.query("limit"));
14
+ const page = c.req.query("page") || undefined;
15
+ const includeArchived = parseBoolean(c.req.query("include_archived"));
16
+ return c.json(service.list(workspaceIdFrom(c), {
17
+ ...(limit === undefined ? {} : { limit }),
18
+ ...(page === undefined ? {} : { page }),
19
+ ...(includeArchived === undefined ? {} : { includeArchived }),
20
+ }), 200);
21
+ });
22
+ app.get("/:id/versions", (c) => {
23
+ const limit = parseLimit(c.req.query("limit"));
24
+ const page = c.req.query("page") || undefined;
25
+ return c.json(service.listVersions(workspaceIdFrom(c), c.req.param("id"), {
26
+ ...(limit === undefined ? {} : { limit }),
27
+ ...(page === undefined ? {} : { page }),
28
+ }), 200);
29
+ });
30
+ app.get("/:id", (c) => {
31
+ const version = parsePositiveInteger(c.req.query("version"), "version");
32
+ return c.json(service.retrieve(workspaceIdFrom(c), c.req.param("id"), version), 200);
33
+ });
34
+ app.post("/:id", async (c) => {
35
+ const body = await parseJsonBody(c.req);
36
+ return c.json(service.update(workspaceIdFrom(c), c.req.param("id"), body), 200);
37
+ });
38
+ app.post("/:id/archive", (c) => {
39
+ return c.json(service.archive(workspaceIdFrom(c), c.req.param("id")), 200);
40
+ });
41
+ return app;
42
+ }
43
+ function parsePositiveInteger(value, field) {
44
+ if (value === undefined)
45
+ return undefined;
46
+ if (!/^[1-9][0-9]*$/.test(value)) {
47
+ throw invalidRequest(`\`${field}\` must be a positive integer`);
48
+ }
49
+ const parsed = Number(value);
50
+ if (!Number.isSafeInteger(parsed)) {
51
+ throw invalidRequest(`\`${field}\` must be a positive integer`);
52
+ }
53
+ return parsed;
54
+ }
55
+ function parseBoolean(value) {
56
+ if (value === undefined)
57
+ return undefined;
58
+ if (value === "true")
59
+ return true;
60
+ if (value === "false")
61
+ return false;
62
+ throw invalidRequest("`include_archived` must be `true` or `false`");
63
+ }