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,65 @@
1
+ export function createWakeLoop(opts) {
2
+ const now = opts.now ?? Date.now;
3
+ let timer;
4
+ let running;
5
+ let closed = false;
6
+ const schedule = (delayMs) => {
7
+ if (closed)
8
+ return;
9
+ if (timer !== undefined)
10
+ clearTimeout(timer);
11
+ timer = setTimeout(() => {
12
+ timer = undefined;
13
+ void cycle();
14
+ }, delayMs);
15
+ };
16
+ const cycle = async () => {
17
+ if (closed || running !== undefined)
18
+ return;
19
+ const work = (async () => {
20
+ try {
21
+ const dueAt = await opts.nextWakeAt();
22
+ if (closed)
23
+ return;
24
+ if (dueAt !== null && dueAt.getTime() <= now()) {
25
+ await opts.run();
26
+ if (closed)
27
+ return;
28
+ schedule(opts.minSleepMs);
29
+ return;
30
+ }
31
+ const untilDue = dueAt === null ? opts.maxSleepMs : dueAt.getTime() - now();
32
+ schedule(Math.min(opts.maxSleepMs, Math.max(opts.minSleepMs, untilDue)));
33
+ }
34
+ catch (error) {
35
+ opts.onError(error);
36
+ schedule(opts.minSleepMs);
37
+ }
38
+ })();
39
+ running = work;
40
+ try {
41
+ await work;
42
+ }
43
+ finally {
44
+ running = undefined;
45
+ }
46
+ };
47
+ schedule(0);
48
+ return {
49
+ wake() {
50
+ if (closed || running !== undefined)
51
+ return;
52
+ schedule(0);
53
+ },
54
+ async close() {
55
+ if (closed)
56
+ return;
57
+ closed = true;
58
+ if (timer !== undefined) {
59
+ clearTimeout(timer);
60
+ timer = undefined;
61
+ }
62
+ await running;
63
+ },
64
+ };
65
+ }
@@ -0,0 +1,92 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { resolveSessionEgressBundle } from "./egress/policy.js";
3
+ import { DEFAULT_SIDECAR_PORT } from "./sessions/pi/sandbox/docker-egress.js";
4
+ /**
5
+ * Per-session egress bundle resolution (plan 0117e-3, Option A): session ->
6
+ * environment -> networking config -> secrets, resolved at sandbox-create
7
+ * time inside the docker factory closure. Returns undefined only when the
8
+ * environment has no networking or a valid hosted empty allowlist. Every
9
+ * other present networking shape is parsed and fails closed. Mints a fresh
10
+ * URL-safe proxy-auth token per session.
11
+ */
12
+ export function createSessionEgressBundleResolver(stores) {
13
+ return async (workspaceId, sessionId, context) => {
14
+ // Normal prompt-time sandbox creation reads the persisted session row. The
15
+ // file-resource create path prepares its sandbox before that row is
16
+ // committed, so DefaultSessionService passes the already-validated
17
+ // environmentId as a creation-time hint.
18
+ const environmentId = context?.environmentId;
19
+ const session = environmentId === undefined
20
+ ? stores.sessions.retrieveAny(workspaceId, sessionId)
21
+ : undefined;
22
+ if (environmentId === undefined && !session)
23
+ return undefined;
24
+ const resolvedEnvironmentId = environmentId ?? session.environment_id;
25
+ const environment = stores.environments.retrieveAny(workspaceId, resolvedEnvironmentId);
26
+ if (!environment)
27
+ return undefined;
28
+ const resolved = resolveSessionEgressBundle({
29
+ environmentConfig: environment.config,
30
+ revealSecret: (name) => stores.secrets?.reveal(workspaceId, name),
31
+ listenPort: DEFAULT_SIDECAR_PORT,
32
+ proxyAuthToken: randomBytes(24).toString("hex"),
33
+ });
34
+ if (resolved === undefined)
35
+ return undefined;
36
+ return { bundle: resolved.bundle, sandboxEnv: resolved.sandboxEnv };
37
+ };
38
+ }
39
+ export function createFileMountResolver(sessionStore, fileStorage) {
40
+ return async (workspaceId, sessionId) => {
41
+ const snapshots = sessionStore.getFileMountSnapshots(workspaceId, sessionId);
42
+ return Promise.all(snapshots.map(async (snapshot) => snapshotToRuntimeMount(workspaceId, fileStorage, snapshot)));
43
+ };
44
+ }
45
+ export function createSkillSnapshotsProvider(sessionStore) {
46
+ return (workspaceId, sessionId) => sessionStore.getSkillSnapshots(workspaceId, sessionId).map((skill) => ({
47
+ name: skill.name,
48
+ description: skill.description,
49
+ }));
50
+ }
51
+ async function snapshotToRuntimeMount(workspaceId, fileStorage, snapshot) {
52
+ const bytes = await fileStorage.openInternalSnapshotBytes(workspaceId, snapshot.snapshot_file_id);
53
+ if (!bytes) {
54
+ throw new Error(`Session file snapshot ${snapshot.snapshot_file_id} not found`);
55
+ }
56
+ return {
57
+ kind: snapshot.kind,
58
+ mountPath: snapshot.mount_path,
59
+ snapshotFileId: snapshot.snapshot_file_id,
60
+ sha256: snapshot.sha256,
61
+ sizeBytes: snapshot.size_bytes,
62
+ bytes,
63
+ };
64
+ }
65
+ export function createStoreBackedAgentRevisionProvider(opts) {
66
+ return (workspaceId, sessionId, context) => {
67
+ const session = opts.sessions.retrieveAny(workspaceId, sessionId);
68
+ const agentId = session?.agent.id ?? context?.agentId;
69
+ const version = session?.agent.version ?? context?.agentVersion;
70
+ if (!agentId || version === undefined)
71
+ return undefined;
72
+ const agent = opts.agents.retrieveVersion(workspaceId, agentId, version);
73
+ if (!agent) {
74
+ throw new Error(`Pinned agent revision not found: ${agentId}@${version}`);
75
+ }
76
+ return { model: agent.model, system: agent.system };
77
+ };
78
+ }
79
+ export function createStoreBackedCustomToolsProvider(opts) {
80
+ return (workspaceId, sessionId, context) => {
81
+ const session = opts.sessions.retrieveAny(workspaceId, sessionId);
82
+ const agentId = session?.agent.id ?? context?.agentId;
83
+ const agentVersion = session?.agent.version ?? context?.agentVersion;
84
+ if (!agentId || agentVersion === undefined)
85
+ return [];
86
+ const agent = opts.agents.retrieveVersion(workspaceId, agentId, agentVersion);
87
+ if (!agent)
88
+ return [];
89
+ const tools = agent.tools.filter((tool) => tool.type === "custom");
90
+ return tools;
91
+ };
92
+ }
@@ -0,0 +1,4 @@
1
+ export const DEFAULT_WORKSPACE_ID = "wrk_default";
2
+ export function workspaceIdFrom(c) {
3
+ return c.get("workspaceId") ?? DEFAULT_WORKSPACE_ID;
4
+ }
@@ -0,0 +1,158 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import { DatabaseSync } from "node:sqlite";
3
+ import { DEFAULT_WORKSPACE_ID } from "../workspace.js";
4
+ import { uuidv7 } from "../../types/events.js";
5
+ // Plan 0113 D2/D3: opaque `oma_`-prefixed 256-bit keys, SHA-256 digests at
6
+ // rest, tombstone revocation, idempotent wrk_default seeding.
7
+ const SCHEMA = `
8
+ PRAGMA foreign_keys = ON;
9
+ CREATE TABLE IF NOT EXISTS workspaces (
10
+ workspace_id TEXT PRIMARY KEY,
11
+ name TEXT NOT NULL,
12
+ created_at TEXT NOT NULL
13
+ );
14
+ CREATE TABLE IF NOT EXISTS workspace_api_keys (
15
+ key_sha256 TEXT PRIMARY KEY,
16
+ workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id),
17
+ label TEXT NOT NULL,
18
+ created_at TEXT NOT NULL,
19
+ revoked_at TEXT
20
+ );
21
+ CREATE INDEX IF NOT EXISTS workspace_api_keys_by_workspace
22
+ ON workspace_api_keys (workspace_id, key_sha256);
23
+ CREATE TABLE IF NOT EXISTS console_sessions (
24
+ token_sha256 TEXT PRIMARY KEY,
25
+ kind TEXT NOT NULL CHECK (kind IN ('workspace_key', 'admin', 'admin_workspace')),
26
+ workspace_id TEXT REFERENCES workspaces(workspace_id),
27
+ credential_sha256 TEXT NOT NULL,
28
+ created_at TEXT NOT NULL,
29
+ expires_at TEXT NOT NULL
30
+ );
31
+ CREATE INDEX IF NOT EXISTS console_sessions_by_expiry
32
+ ON console_sessions (expires_at);
33
+ `;
34
+ export const WORKSPACE_API_KEY_PREFIX = "oma_";
35
+ export function newWorkspaceId() {
36
+ return `wrk_${uuidv7()}`;
37
+ }
38
+ export function hashWorkspaceApiKey(plaintextKey) {
39
+ return createHash("sha256").update(plaintextKey).digest("hex");
40
+ }
41
+ export class SqliteWorkspaceStore {
42
+ db;
43
+ insertWorkspaceStmt;
44
+ getWorkspaceStmt;
45
+ insertKeyStmt;
46
+ authenticateStmt;
47
+ revokeKeyStmt;
48
+ listKeysStmt;
49
+ listWorkspacesStmt;
50
+ getKeyStmt;
51
+ countKeysStmt;
52
+ insertConsoleSessionStmt;
53
+ getConsoleSessionStmt;
54
+ deleteConsoleSessionStmt;
55
+ constructor(db) {
56
+ this.db = db;
57
+ this.db.exec(SCHEMA);
58
+ this.insertWorkspaceStmt = this.db.prepare(`INSERT INTO workspaces (workspace_id, name, created_at) VALUES (?, ?, ?)`);
59
+ this.getWorkspaceStmt = this.db.prepare(`SELECT workspace_id, name, created_at FROM workspaces WHERE workspace_id = ?`);
60
+ this.insertKeyStmt = this.db.prepare(`INSERT INTO workspace_api_keys (key_sha256, workspace_id, label, created_at, revoked_at)
61
+ VALUES (?, ?, ?, ?, NULL)`);
62
+ this.authenticateStmt = this.db.prepare(`SELECT workspace_id FROM workspace_api_keys
63
+ WHERE key_sha256 = ? AND revoked_at IS NULL`);
64
+ this.revokeKeyStmt = this.db.prepare(`UPDATE workspace_api_keys
65
+ SET revoked_at = COALESCE(revoked_at, ?)
66
+ WHERE key_sha256 = ?`);
67
+ this.listKeysStmt = this.db.prepare(`SELECT key_sha256, workspace_id, label, created_at, revoked_at
68
+ FROM workspace_api_keys
69
+ WHERE workspace_id = ?
70
+ ORDER BY key_sha256 ASC`);
71
+ this.listWorkspacesStmt = this.db.prepare(`SELECT workspace_id, name, created_at FROM workspaces
72
+ ORDER BY workspace_id ASC`);
73
+ this.getKeyStmt = this.db.prepare(`SELECT key_sha256, workspace_id, label, created_at, revoked_at
74
+ FROM workspace_api_keys
75
+ WHERE key_sha256 = ?`);
76
+ this.countKeysStmt = this.db.prepare(`SELECT COUNT(*) AS n FROM workspace_api_keys`);
77
+ this.insertConsoleSessionStmt = this.db.prepare(`INSERT INTO console_sessions
78
+ (token_sha256, kind, workspace_id, credential_sha256, created_at, expires_at)
79
+ VALUES (?, ?, ?, ?, ?, ?)`);
80
+ this.getConsoleSessionStmt = this.db.prepare(`SELECT token_sha256, kind, workspace_id, credential_sha256, created_at, expires_at
81
+ FROM console_sessions
82
+ WHERE token_sha256 = ? AND expires_at > ?`);
83
+ this.deleteConsoleSessionStmt = this.db.prepare(`DELETE FROM console_sessions WHERE token_sha256 = ?`);
84
+ this.db.prepare(`INSERT OR IGNORE INTO workspaces (workspace_id, name, created_at)
85
+ VALUES (?, 'Default workspace', ?)`).run(DEFAULT_WORKSPACE_ID, new Date().toISOString());
86
+ }
87
+ static open(path) {
88
+ return new SqliteWorkspaceStore(new DatabaseSync(path));
89
+ }
90
+ createWorkspace(name, workspaceId = newWorkspaceId()) {
91
+ const row = {
92
+ workspace_id: workspaceId,
93
+ name,
94
+ created_at: new Date().toISOString(),
95
+ };
96
+ this.insertWorkspaceStmt.run(row.workspace_id, row.name, row.created_at);
97
+ return row;
98
+ }
99
+ getWorkspace(workspaceId) {
100
+ return this.getWorkspaceStmt.get(workspaceId);
101
+ }
102
+ listWorkspaces() {
103
+ return this.listWorkspacesStmt.all();
104
+ }
105
+ getKey(keySha256) {
106
+ return this.getKeyStmt.get(keySha256);
107
+ }
108
+ mintKey(workspaceId, label) {
109
+ if (!this.getWorkspace(workspaceId)) {
110
+ throw new Error(`Workspace not found: ${workspaceId}`);
111
+ }
112
+ const plaintextKey = `${WORKSPACE_API_KEY_PREFIX}${randomBytes(32).toString("base64url")}`;
113
+ const keySha256 = hashWorkspaceApiKey(plaintextKey);
114
+ this.insertKeyStmt.run(keySha256, workspaceId, label, new Date().toISOString());
115
+ return { plaintextKey, keySha256, workspaceId, label };
116
+ }
117
+ // Counts every key ever minted, revoked included: first boot means "no key
118
+ // was ever issued", not "no key currently works".
119
+ countApiKeys() {
120
+ return this.countKeysStmt.get().n;
121
+ }
122
+ authenticate(plaintextKey) {
123
+ const row = this.authenticateStmt.get(hashWorkspaceApiKey(plaintextKey));
124
+ return row?.workspace_id;
125
+ }
126
+ authenticateKeySha256(keySha256) {
127
+ const row = this.authenticateStmt.get(keySha256);
128
+ return row?.workspace_id;
129
+ }
130
+ mintConsoleSession(input) {
131
+ const plaintextToken = `ocs_${randomBytes(32).toString("base64url")}`;
132
+ const row = {
133
+ token_sha256: hashWorkspaceApiKey(plaintextToken),
134
+ kind: input.kind,
135
+ workspace_id: input.workspaceId ?? null,
136
+ credential_sha256: input.credentialSha256,
137
+ created_at: new Date().toISOString(),
138
+ expires_at: input.expiresAt.toISOString(),
139
+ };
140
+ this.insertConsoleSessionStmt.run(row.token_sha256, row.kind, row.workspace_id, row.credential_sha256, row.created_at, row.expires_at);
141
+ return { plaintextToken, row };
142
+ }
143
+ getConsoleSession(plaintextToken) {
144
+ return this.getConsoleSessionStmt.get(hashWorkspaceApiKey(plaintextToken), new Date().toISOString());
145
+ }
146
+ revokeConsoleSession(plaintextToken) {
147
+ return this.deleteConsoleSessionStmt.run(hashWorkspaceApiKey(plaintextToken)).changes > 0;
148
+ }
149
+ revokeKey(keySha256) {
150
+ return this.revokeKeyStmt.run(new Date().toISOString(), keySha256).changes > 0;
151
+ }
152
+ listKeys(workspaceId) {
153
+ return this.listKeysStmt.all(workspaceId);
154
+ }
155
+ close() {
156
+ this.db.close();
157
+ }
158
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Per-session egress proxy sidecar entrypoint (plan 0117d; ADR 0016 §2/§3).
3
+ *
4
+ * The vendored `createEgressProxy` runs OUT of the long-lived control-plane
5
+ * process, in its own short-lived container, so the control plane never joins
6
+ * the sandbox's network (the sandbox's sole route out is this proxy). It is
7
+ * launched with the SAME appliance image, only a different CMD — no second
8
+ * image to ship.
9
+ *
10
+ * Contract (all via env, since the sidecar takes no CLI args):
11
+ * OMA_EGRESS_BUNDLE_PATH read-only mount of the serialized SessionEgressBundle
12
+ * (policy + grants + resolved secrets + auth token).
13
+ * Holds real secret material — mounted ONLY here.
14
+ * OMA_EGRESS_SHARED_DIR a dir shared with the control plane. The sidecar
15
+ * writes `ca.crt` (its ephemeral MITM CA cert) and,
16
+ * once listening, a `ready` marker. The CA PRIVATE
17
+ * KEY never leaves this container.
18
+ * OMA_EGRESS_BIND_HOST listen address (default 0.0.0.0 — the sidecar is
19
+ * only reachable on the per-session --internal net).
20
+ *
21
+ * The proxy inherits the default SSRF/private-IP deny (we never set the
22
+ * dangerous test flag) and never sets mutateHeadersPlaintext, so secrets are
23
+ * injected only on the TLS-terminated leg.
24
+ */
25
+ import { copyFileSync, readFileSync, writeFileSync } from "node:fs";
26
+ import { join } from "node:path";
27
+ import { pathToFileURL } from "node:url";
28
+ import { createEgressProxy, createMitmCA, disposeMitmCA, } from "./control-plane/egress/proxy.js";
29
+ import { buildHooksFromBundle, } from "./control-plane/egress/policy.js";
30
+ import { log } from "./control-plane/logging.js";
31
+ function requiredEnv(env, key) {
32
+ const value = env[key];
33
+ if (typeof value !== "string" || value === "") {
34
+ throw new Error(`egress proxy sidecar requires ${key}`);
35
+ }
36
+ return value;
37
+ }
38
+ export function loadSessionEgressBundle(path) {
39
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
40
+ if (typeof parsed !== "object" ||
41
+ parsed === null ||
42
+ typeof parsed.listenPort !== "number" ||
43
+ typeof parsed.proxyAuthToken !== "string" ||
44
+ !Array.isArray(parsed.grants) ||
45
+ typeof parsed.policy !== "object") {
46
+ throw new Error(`egress bundle at ${path} is malformed`);
47
+ }
48
+ return parsed;
49
+ }
50
+ /**
51
+ * Start the sidecar proxy. Generates the MITM CA in-process, publishes its
52
+ * cert (not its key) to the shared dir, listens, then drops the `ready`
53
+ * marker the control plane waits on before it launches the sandbox.
54
+ */
55
+ export async function startEgressProxySidecar(env) {
56
+ const bundlePath = requiredEnv(env, "OMA_EGRESS_BUNDLE_PATH");
57
+ const sharedDir = requiredEnv(env, "OMA_EGRESS_SHARED_DIR");
58
+ const bindHost = env.OMA_EGRESS_BIND_HOST ?? "0.0.0.0";
59
+ const bundle = loadSessionEgressBundle(bundlePath);
60
+ // Ephemeral CA minted here; only the trust BUNDLE is published (never the
61
+ // key, which could forge certs for any host — it stays in this container).
62
+ // The bundle is the MITM CA followed by the host's public roots: the sandbox
63
+ // trust env vars (SSL_CERT_FILE, CURL_CA_BUNDLE, ...) REPLACE the client's
64
+ // store, so publishing the CA alone would break verification for any host we
65
+ // do NOT terminate (opaque-tunnel / cert-pinned upstreams talk to the real
66
+ // upstream cert). trustBundlePath already contains CA + roots.
67
+ const ca = createMitmCA({});
68
+ copyFileSync(ca.trustBundlePath, join(sharedDir, "ca.crt"));
69
+ const server = createEgressProxy({
70
+ ...buildHooksFromBundle(bundle),
71
+ mitmCA: ca,
72
+ proxyAuthToken: bundle.proxyAuthToken,
73
+ });
74
+ await new Promise((resolve, reject) => {
75
+ server.once("error", reject);
76
+ server.listen(bundle.listenPort, bindHost, () => {
77
+ server.removeListener("error", reject);
78
+ resolve();
79
+ });
80
+ });
81
+ // Marker is written LAST: its presence means the proxy is accepting
82
+ // connections, so the control plane can start the sandbox without a race.
83
+ writeFileSync(join(sharedDir, "ready"), "", { mode: 0o644 });
84
+ const stop = async () => {
85
+ await new Promise((resolve) => server.close(() => resolve()));
86
+ await disposeMitmCA(ca);
87
+ };
88
+ return { server, port: bundle.listenPort, ca, stop };
89
+ }
90
+ if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
91
+ startEgressProxySidecar(process.env)
92
+ .then((running) => {
93
+ const shutdown = () => {
94
+ void running.stop().then(() => process.exit(0));
95
+ };
96
+ process.on("SIGTERM", shutdown);
97
+ process.on("SIGINT", shutdown);
98
+ })
99
+ .catch((error) => {
100
+ log.error("egress_proxy_sidecar_failed", { error });
101
+ process.exit(1);
102
+ });
103
+ }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Appliance entrypoint (plan 0115, Arc A slice 1 of the 0114 roadmap).
3
+ *
4
+ * One command boots a durable, authenticated OMA server with sensible
5
+ * defaults; the first boot mints the initial workspace API key and prints it
6
+ * exactly once. Every default is an override, not a requirement:
7
+ *
8
+ * OMA_HOME data directory (default ~/.oma)
9
+ * OMA_SQLITE_PATH explicit DB path (overrides OMA_HOME derivation)
10
+ * OMA_FILE_STORAGE_ROOT explicit object root (ditto)
11
+ * OMA_AUTH_MODE default api-key; "disabled" is respected
12
+ * OMA_PORT default 4180 (0 = ephemeral, for tests)
13
+ * OMA_HOST bind address, default 127.0.0.1
14
+ *
15
+ * Run directly (`node --experimental-transform-types src/main.ts`) or via
16
+ * `oma up`, whose CLI launcher adds the Node flags and selects a sandbox.
17
+ */
18
+ import { homedir } from "node:os";
19
+ import { join } from "node:path";
20
+ import { pathToFileURL } from "node:url";
21
+ import { serve } from "@hono/node-server";
22
+ import { createDeploymentControlPlane, } from "./control-plane/app.js";
23
+ import { DEFAULT_WORKSPACE_ID } from "./control-plane/workspace.js";
24
+ export const DEFAULT_APPLIANCE_PORT = 4180;
25
+ // Pure so tests can assert the derivation. OMA_HOME only fills the storage
26
+ // pair when neither is set explicitly; setting exactly one of the pair still
27
+ // hits the existing "must be set together" error from deployment storage.
28
+ export function resolveApplianceEnv(env) {
29
+ const resolved = { ...env };
30
+ if (resolved.OMA_SQLITE_PATH === undefined &&
31
+ resolved.OMA_FILE_STORAGE_ROOT === undefined) {
32
+ const home = resolved.OMA_HOME ?? join(homedir(), ".oma");
33
+ resolved.OMA_SQLITE_PATH = join(home, "oma.sqlite");
34
+ resolved.OMA_FILE_STORAGE_ROOT = join(home, "files");
35
+ }
36
+ resolved.OMA_AUTH_MODE ??= "api-key";
37
+ return resolved;
38
+ }
39
+ export function parseAppliancePort(raw) {
40
+ if (raw === undefined)
41
+ return DEFAULT_APPLIANCE_PORT;
42
+ const port = Number.parseInt(raw, 10);
43
+ if (!Number.isSafeInteger(port) || port < 0 || port > 65535 || String(port) !== raw.trim()) {
44
+ throw new Error(`Invalid OMA_PORT: ${JSON.stringify(raw)} (expected an integer 0-65535)`);
45
+ }
46
+ return port;
47
+ }
48
+ export async function startAppliance(env = process.env, opts = {}) {
49
+ const log = opts.log ?? console.log;
50
+ const resolved = resolveApplianceEnv(env);
51
+ const port = parseAppliancePort(resolved.OMA_PORT);
52
+ const host = resolved.OMA_HOST ?? "127.0.0.1";
53
+ const plane = createDeploymentControlPlane(resolved);
54
+ const { app, stores, authMode } = plane;
55
+ let server;
56
+ try {
57
+ const bound = await new Promise((resolvePromise, reject) => {
58
+ const onError = (error) => reject(error);
59
+ const s = serve({ fetch: app.fetch, hostname: host, port }, (info) => {
60
+ s.off("error", onError);
61
+ resolvePromise({ server: s, boundPort: info.port });
62
+ });
63
+ s.on("error", onError);
64
+ });
65
+ server = bound.server;
66
+ const boundPort = bound.boundPort;
67
+ // Mint only after the server has bound. First boot = no key was ever
68
+ // minted (revoked tombstones count as minted), so persisting a key on a
69
+ // boot that then fails to bind (EADDRINUSE) would make every later boot
70
+ // skip minting — an unprinted key locking the operator out. A key that
71
+ // prints but never serves only costs a retry; the reverse costs the
72
+ // quickstart.
73
+ const minted = authMode === "api-key" && stores.workspaces.countApiKeys() === 0
74
+ ? stores.workspaces.mintKey(DEFAULT_WORKSPACE_ID, "first-boot")
75
+ : undefined;
76
+ const baseUrl = `http://${host}:${boundPort}`;
77
+ log(`open-managed-agents listening on ${baseUrl}`);
78
+ log(` console: ${baseUrl}/console/`);
79
+ log(` api: ${baseUrl}`);
80
+ log(` data: ${resolved.OMA_SQLITE_PATH} | files: ${resolved.OMA_FILE_STORAGE_ROOT}`);
81
+ log(` auth: ${authMode}`);
82
+ if (minted !== undefined) {
83
+ log("");
84
+ log(`First boot: minted the initial API key for ${minted.workspaceId}.`);
85
+ log("It is shown once and stored only as a hash — save it now:");
86
+ log("");
87
+ log(` x-api-key: ${minted.plaintextKey}`);
88
+ log("");
89
+ log(`Connect any Anthropic SDK client with base URL ${baseUrl} and that x-api-key.`);
90
+ // tsx is a devDependency and absent from the Docker image; the node
91
+ // flag works in both the checkout and the container.
92
+ log("Mint more keys/workspaces: node --experimental-transform-types scripts/oma-workspaces.ts --help");
93
+ }
94
+ return {
95
+ port: boundPort,
96
+ close: async () => {
97
+ await closeServer(bound.server);
98
+ await plane.close();
99
+ },
100
+ };
101
+ }
102
+ catch (error) {
103
+ // Failed startups must release everything (notably .oma.lock) so the
104
+ // operator's retry is a clean boot, not a lock error.
105
+ if (server !== undefined) {
106
+ await closeServer(server).catch(() => { });
107
+ }
108
+ await plane.close();
109
+ throw error;
110
+ }
111
+ }
112
+ function closeServer(server) {
113
+ return new Promise((resolvePromise, reject) => {
114
+ server.close((err) => (err ? reject(err) : resolvePromise()));
115
+ });
116
+ }
117
+ if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
118
+ const appliance = await startAppliance(process.env, {});
119
+ const shutdown = () => {
120
+ void appliance.close().then(() => process.exit(0));
121
+ };
122
+ process.once("SIGINT", shutdown);
123
+ process.once("SIGTERM", shutdown);
124
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Managed Agents event types — the wire shapes we publish to API clients.
3
+ *
4
+ * The `type` strings are Tier 1 wire-compatible per ADR 0004 — they must match
5
+ * Anthropic's Managed Agents API verbatim. Per-type fields are deliberately
6
+ * modeled as an open object at this layer; typed elaboration lives in the
7
+ * route/translator layer where the discriminator gives us the variant.
8
+ */
9
+ /**
10
+ * Discriminator strings emitted by the platform. MVP subset; extend as needed.
11
+ *
12
+ * Declared as a runtime `as const` array so we can iterate it in alignment
13
+ * tests (see `src/types/__tests__/events.test.ts`); the TS union is derived
14
+ * via `typeof[number]` so the compile-time and runtime views can never drift.
15
+ */
16
+ export const EVENT_TYPES = [
17
+ // Agent-originated
18
+ "agent.message",
19
+ "agent.tool_use",
20
+ "agent.tool_result",
21
+ "agent.custom_tool_use",
22
+ "agent.mcp_tool_use",
23
+ "agent.mcp_tool_result",
24
+ // Span observability
25
+ "span.model_request_start",
26
+ "span.model_request_end",
27
+ // Session lifecycle
28
+ "session.status_running",
29
+ "session.status_idle",
30
+ "session.status_rescheduled",
31
+ "session.status_terminated",
32
+ "session.deleted",
33
+ "session.error",
34
+ // User-originated (echoed back on the stream after server processes them)
35
+ "user.message",
36
+ "user.interrupt",
37
+ "user.custom_tool_result",
38
+ "user.tool_confirmation",
39
+ ];
40
+ /**
41
+ * Generate a UUIDv7 — time-ordered 128-bit ID with a 48-bit millisecond
42
+ * timestamp prefix. Used so lexical sort = chronological sort, which makes
43
+ * `WHERE id > ? ORDER BY id` a valid cursor scan in SQLite.
44
+ *
45
+ * Strict intra-process monotonicity via **counter-based** scheme (RFC 9562
46
+ * §6.2 Method 1, with random seeding per Method 3): when multiple IDs are
47
+ * minted in the same millisecond, a 12-bit counter in `rand_a` advances
48
+ * deterministically. This preserves real-clock fidelity in the embedded `ms`
49
+ * field — code-review MEDIUM 4 finding fixed (no more timestamp inflation).
50
+ *
51
+ * Layout (per RFC 9562 §5.7):
52
+ * bits 0-47 unix_ts_ms (48-bit ms)
53
+ * bits 48-51 ver (constant 0b0111)
54
+ * bits 52-63 counter (12-bit monotonic within an ms)
55
+ * bits 64-65 var (constant 0b10)
56
+ * bits 66-127 random (62 bits)
57
+ *
58
+ * Counter behavior:
59
+ * - When `ms` advances: counter seeded with random 0..2047 (leaves 2048
60
+ * increments of headroom within the ms; mitigates cross-process collisions)
61
+ * - When `ms` is equal or earlier (clock tied or rewound): counter bumps;
62
+ * if it overflows 0xFFF we borrow 1ms (this is the only path that inflates
63
+ * timestamps, and only by 1ms per 4096-ID burst within a single ms)
64
+ *
65
+ * Spec: https://datatracker.ietf.org/doc/rfc9562/ §5.7 + §6.2
66
+ */
67
+ let lastMs = 0n;
68
+ let lastCounter = 0;
69
+ export function uuidv7() {
70
+ let ms = BigInt(Date.now());
71
+ let counter;
72
+ if (ms > lastMs) {
73
+ lastMs = ms;
74
+ // Seed counter with random 0..2047 (top bit clear → 2048 increments
75
+ // available before counter overflow within the same ms).
76
+ counter = crypto.getRandomValues(new Uint16Array(1))[0] & 0x07ff;
77
+ lastCounter = counter;
78
+ }
79
+ else {
80
+ // Same ms (or rewound clock). Reuse lastMs; bump counter.
81
+ ms = lastMs;
82
+ lastCounter += 1;
83
+ if (lastCounter > 0x0fff) {
84
+ // 12-bit counter overflow. Borrow 1ms (only timestamp-inflation path).
85
+ lastMs += 1n;
86
+ ms = lastMs;
87
+ counter = crypto.getRandomValues(new Uint16Array(1))[0] & 0x07ff;
88
+ lastCounter = counter;
89
+ }
90
+ else {
91
+ counter = lastCounter;
92
+ }
93
+ }
94
+ const random = crypto.getRandomValues(new Uint8Array(8));
95
+ const b0 = Number((ms >> 40n) & 0xffn);
96
+ const b1 = Number((ms >> 32n) & 0xffn);
97
+ const b2 = Number((ms >> 24n) & 0xffn);
98
+ const b3 = Number((ms >> 16n) & 0xffn);
99
+ const b4 = Number((ms >> 8n) & 0xffn);
100
+ const b5 = Number(ms & 0xffn);
101
+ // version 7 (high nibble) | top 4 bits of 12-bit counter (low nibble)
102
+ const b6 = 0x70 | ((counter >> 8) & 0x0f);
103
+ // low 8 bits of 12-bit counter
104
+ const b7 = counter & 0xff;
105
+ // variant 0b10 (high 2 bits) | 6 random bits
106
+ const b8 = 0x80 | (random[0] & 0x3f);
107
+ const b9 = random[1];
108
+ const tail = random.slice(2); // 6 random bytes
109
+ const hex = (n) => n.toString(16).padStart(2, "0");
110
+ return [
111
+ hex(b0) + hex(b1) + hex(b2) + hex(b3),
112
+ hex(b4) + hex(b5),
113
+ hex(b6) + hex(b7),
114
+ hex(b8) + hex(b9),
115
+ [...tail].map(hex).join(""),
116
+ ].join("-");
117
+ }
118
+ /** Convenience: prefixed event ID. */
119
+ export function newEventId() {
120
+ return `sevt_${uuidv7()}`;
121
+ }
@@ -0,0 +1 @@
1
+ export {};