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,230 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { existsSync, lstatSync, readFileSync, statSync } from "node:fs";
3
+ import { createServer } from "node:net";
4
+ import { homedir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { pathToFileURL } from "node:url";
7
+ import { parseModelDeploymentConfigFromEnv, validateModelDeploymentConfigAgainstCatalog, } from "../src/control-plane/models/deployment-config.js";
8
+ import { createReadOnlyPiModelCatalog } from "../src/control-plane/models/catalog.js";
9
+ import { DEFAULT_OMA_SANDBOX_IMAGE } from "../src/control-plane/sessions/pi/sandbox/image.js";
10
+ import { DEFAULT_OMA_EGRESS_SIDECAR_IMAGE, resolveOmaUpEgressEnvironment, } from "../src/control-plane/egress/image.js";
11
+ export async function inspectOma(options, runtime = { env: process.env }) {
12
+ const checks = [];
13
+ const env = runtime.env;
14
+ const command = runtime.command ?? spawnSync;
15
+ const nodeVersion = runtime.nodeVersion ?? process.versions.node;
16
+ const [major = 0, minor = 0] = nodeVersion.split(".").map(Number);
17
+ checks.push(check("node.version", major > 22 || (major === 22 && minor >= 19) ? "pass" : "fail", major > 22 || (major === 22 && minor >= 19)
18
+ ? `Node ${nodeVersion} satisfies >=22.19.0.`
19
+ : `Node ${nodeVersion} is too old; install Node >=22.19.0.`));
20
+ const home = env.OMA_HOME?.trim() || join(homedir(), ".oma");
21
+ const homePathCheck = inspectPath("paths.oma_home", home, "directory", 0o700);
22
+ checks.push(homePathCheck);
23
+ let deployment;
24
+ try {
25
+ if (homePathCheck.status === "fail") {
26
+ throw new Error("OMA_HOME is not safe to inspect");
27
+ }
28
+ deployment = parseModelDeploymentConfigFromEnv(env, { home });
29
+ const authPathCheck = inspectPath("paths.pi_auth", deployment.authPath, "file", 0o600);
30
+ const modelsPathCheck = inspectPath("paths.pi_models", deployment.modelsPath, "file", 0o600);
31
+ checks.push(authPathCheck, modelsPathCheck);
32
+ if (authPathCheck.status === "fail" || modelsPathCheck.status === "fail") {
33
+ throw new Error("Pi configuration paths are not safe to read");
34
+ }
35
+ const authData = readAuthSnapshot(deployment.authPath);
36
+ const catalog = createReadOnlyPiModelCatalog({
37
+ allowedProviders: deployment.allowedProviders,
38
+ defaultModel: deployment.defaultModel,
39
+ authPath: deployment.authPath,
40
+ modelsPath: deployment.modelsPath,
41
+ allowModelAuthCommands: deployment.allowModelAuthCommands,
42
+ }, authData);
43
+ validateModelDeploymentConfigAgainstCatalog(deployment, {
44
+ hasProvider: (provider) => catalog.allowedProviders.has(provider),
45
+ hasModel: (model) => catalog.resolve(model) !== undefined,
46
+ });
47
+ checks.push(check("models.catalog", "pass", `${catalog.list().length} registered model(s) across ${catalog.allowedProviders.size} enabled provider(s); default ${catalog.defaultModel.provider}/${catalog.defaultModel.id}.`));
48
+ const defaultModel = catalog.resolve(catalog.defaultModel);
49
+ const configured = defaultModel !== undefined && catalog.hasConfiguredAuth(defaultModel);
50
+ checks.push(check("models.credentials", configured ? "pass" : "warn", configured
51
+ ? `Credentials are configured for default model ${catalog.defaultModel.provider}/${catalog.defaultModel.id}.`
52
+ : `Credentials are not configured for default model ${catalog.defaultModel.provider}/${catalog.defaultModel.id}; the local-compatible smoke still works, or run oma auth set ${catalog.defaultModel.provider} before a credentialed session.`));
53
+ checks.push(check("models.config", catalog.securityReport.warnings.length === 0 ? "pass" : "warn", catalog.securityReport.warnings.length === 0
54
+ ? "Model configuration passed read-only validation."
55
+ : `Model configuration is valid with ${catalog.securityReport.warnings.length} warning(s); run oma models validate for details.`));
56
+ }
57
+ catch (error) {
58
+ checks.push(check("models.catalog", "fail", safeError("Model configuration is invalid", error)));
59
+ }
60
+ if (options.sandbox === "docker-local") {
61
+ const info = command("docker", ["info"], { encoding: "utf8" });
62
+ checks.push(check("sandbox.runtime", info.status === 0 ? "pass" : "fail", info.status === 0 ? "Docker daemon is reachable." : "Docker daemon is unavailable; start Docker and retry."));
63
+ const image = command("docker", ["image", "inspect", DEFAULT_OMA_SANDBOX_IMAGE], { encoding: "utf8" });
64
+ checks.push(check("sandbox.image", image.status === 0 ? "pass" : "warn", image.status === 0
65
+ ? "The pinned OMA sandbox image is already present locally."
66
+ : "The pinned OMA sandbox image is not present locally; oma doctor did not download it."));
67
+ try {
68
+ const effective = resolveOmaUpEgressEnvironment(env, "docker-local");
69
+ if (effective.OMA_ENABLE_EGRESS === "false") {
70
+ checks.push(check("egress.sidecar", "warn", "Approved HTTPS egress is explicitly disabled; only offline environments can run."));
71
+ }
72
+ else {
73
+ const sidecarImage = effective.OMA_EGRESS_SIDECAR_IMAGE ?? DEFAULT_OMA_EGRESS_SIDECAR_IMAGE;
74
+ const sidecar = command("docker", ["image", "inspect", sidecarImage], { encoding: "utf8" });
75
+ checks.push(check("egress.sidecar", sidecar.status === 0 ? "pass" : "warn", sidecar.status === 0
76
+ ? "The effective digest-pinned OMA egress sidecar is present locally."
77
+ : "The effective digest-pinned OMA egress sidecar is not present locally; oma doctor did not download it."));
78
+ }
79
+ }
80
+ catch (error) {
81
+ checks.push(check("egress.sidecar", "fail", safeError("Egress configuration is invalid", error)));
82
+ }
83
+ }
84
+ else {
85
+ const executable = env.OMA_MICROSANDBOX_COMMAND?.trim() || "msb";
86
+ const version = command(executable, ["--version"], { encoding: "utf8" });
87
+ checks.push(check("sandbox.runtime", version.status === 0 ? "pass" : "fail", version.status === 0 ? "Microsandbox CLI is reachable." : `Microsandbox CLI ${executable} is unavailable.`));
88
+ checks.push(check("sandbox.image", "warn", "Microsandbox image presence is not inspected in read-only mode; no image was downloaded."));
89
+ checks.push(check("egress.sidecar", "warn", "Approved HTTPS egress presets are not supported by microsandbox-local in this alpha."));
90
+ }
91
+ checks.push(await inspectPort(env, runtime.fetch ?? globalThis.fetch));
92
+ return { schema_version: 1, ok: !checks.some((item) => item.status === "fail"), checks };
93
+ }
94
+ function readAuthSnapshot(path) {
95
+ if (!existsSync(path))
96
+ return {};
97
+ let parsed;
98
+ try {
99
+ parsed = JSON.parse(readFileSync(path, "utf8"));
100
+ }
101
+ catch {
102
+ throw new Error("auth.json is not valid JSON");
103
+ }
104
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
105
+ throw new Error("auth.json must contain an object");
106
+ }
107
+ for (const [provider, value] of Object.entries(parsed)) {
108
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
109
+ throw new Error(`auth.json.${provider} must contain a credential object`);
110
+ }
111
+ const credential = value;
112
+ if (credential.type !== "api_key" && credential.type !== "oauth") {
113
+ throw new Error(`auth.json.${provider}.type must be api_key or oauth`);
114
+ }
115
+ if (credential.type === "api_key" && (typeof credential.key !== "string" || credential.key.length === 0)) {
116
+ throw new Error(`auth.json.${provider}.key must be a non-empty string`);
117
+ }
118
+ }
119
+ return parsed;
120
+ }
121
+ function inspectPath(id, path, kind, expectedMode) {
122
+ if (!existsSync(path))
123
+ return check(id, "warn", `${path} does not exist; oma doctor left it unchanged.`);
124
+ const lst = lstatSync(path);
125
+ if (lst.isSymbolicLink())
126
+ return check(id, "fail", `${path} is a symlink; use an owner-controlled ${kind}.`);
127
+ const st = statSync(path);
128
+ const correctKind = kind === "directory" ? st.isDirectory() : st.isFile();
129
+ if (!correctKind)
130
+ return check(id, "fail", `${path} is not a ${kind}.`);
131
+ const mode = st.mode & 0o777;
132
+ if ((mode & 0o077) !== 0) {
133
+ return check(id, "fail", `${path} permissions are 0${mode.toString(8)}; expected owner-only access such as 0${expectedMode.toString(8)}.`);
134
+ }
135
+ return check(id, "pass", `${path} is owner-controlled.`);
136
+ }
137
+ async function inspectPort(env, fetchImpl) {
138
+ const host = env.OMA_HOST?.trim() || "127.0.0.1";
139
+ const port = Number(env.OMA_PORT?.trim() || "4180");
140
+ if (!Number.isSafeInteger(port) || port < 1 || port > 65535) {
141
+ return check("server.port", "fail", "OMA_PORT must be an integer from 1 through 65535.");
142
+ }
143
+ if (host === "127.0.0.1" || host === "localhost" || host === "::1") {
144
+ try {
145
+ const response = await fetchImpl(`http://${host}:${port}/health`, { signal: AbortSignal.timeout(750) });
146
+ if (response.ok)
147
+ return check("server.port", "pass", `OMA is already responding on ${host}:${port}.`);
148
+ }
149
+ catch {
150
+ // A failed health request is followed by a bind-only availability test.
151
+ }
152
+ }
153
+ return await new Promise((resolve) => {
154
+ const server = createServer();
155
+ server.once("error", () => resolve(check("server.port", "fail", `${host}:${port} is occupied by a process that did not answer as OMA.`)));
156
+ server.listen({ host, port, exclusive: true }, () => {
157
+ server.close(() => resolve(check("server.port", "pass", `${host}:${port} is available for oma up.`)));
158
+ });
159
+ });
160
+ }
161
+ function check(id, status, summary) {
162
+ return { id, status, summary };
163
+ }
164
+ function safeError(prefix, error) {
165
+ const message = error instanceof Error ? error.message : String(error);
166
+ return `${prefix}: ${message.replace(/https?:\/\/[^\s]+/g, "[redacted-url]")}`;
167
+ }
168
+ function parseArgs(argv) {
169
+ let json = false;
170
+ let sandbox = "docker-local";
171
+ for (let index = 0; index < argv.length; index += 1) {
172
+ const arg = argv[index];
173
+ if (arg === "--json")
174
+ json = true;
175
+ else if (arg === "--sandbox") {
176
+ const value = argv[++index];
177
+ if (value === "docker" || value === "docker-local")
178
+ sandbox = "docker-local";
179
+ else if (value === "microsandbox" || value === "microsandbox-local")
180
+ sandbox = "microsandbox-local";
181
+ else
182
+ throw new Error("--sandbox requires docker or microsandbox");
183
+ }
184
+ else {
185
+ throw new Error(`Unknown option for oma doctor: ${arg}`);
186
+ }
187
+ }
188
+ return { json, sandbox };
189
+ }
190
+ async function main() {
191
+ let options;
192
+ try {
193
+ options = parseArgs(process.argv.slice(2));
194
+ }
195
+ catch (error) {
196
+ console.error(`oma: ${error instanceof Error ? error.message : String(error)}\nRun \`oma doctor --help\` for usage.`);
197
+ process.exit(2);
198
+ }
199
+ let report;
200
+ try {
201
+ report = await inspectOma(options);
202
+ }
203
+ catch (error) {
204
+ const summary = safeError("Doctor could not complete its read-only inspection", error);
205
+ if (options.json) {
206
+ console.log(JSON.stringify({
207
+ schema_version: 1,
208
+ ok: false,
209
+ checks: [check("doctor.internal", "fail", summary)],
210
+ }, null, 2));
211
+ }
212
+ else {
213
+ console.error(`oma: ${summary}`);
214
+ }
215
+ process.exit(2);
216
+ }
217
+ if (options.json) {
218
+ console.log(JSON.stringify(report, null, 2));
219
+ }
220
+ else {
221
+ console.log("OMA doctor (read-only)\n");
222
+ for (const item of report.checks)
223
+ console.log(`${item.status.toUpperCase().padEnd(4)} ${item.id} ${item.summary}`);
224
+ console.log(`\n${report.ok ? "Ready" : "Not ready"}. No files were created and no images were downloaded.`);
225
+ }
226
+ process.exit(report.ok ? 0 : 1);
227
+ }
228
+ if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
229
+ await main();
230
+ }
@@ -0,0 +1,299 @@
1
+ // Operator CLI for Pi model/provider discovery and model credential storage
2
+ // (plan 0139 D6). This intentionally reuses the same deployment parser,
3
+ // catalog factory, and atomic AuthStorage backend as the control plane.
4
+ import { stdin as input, stderr as errorOut } from "node:process";
5
+ import { pathToFileURL } from "node:url";
6
+ import { parseModelDeploymentConfigFromEnv, validateModelDeploymentConfigAgainstCatalog, } from "../src/control-plane/models/deployment-config.js";
7
+ import { createOmaAuthStorageBackend } from "../src/control-plane/models/auth-storage-backend.js";
8
+ import { createPiModelCatalog } from "../src/control-plane/models/catalog.js";
9
+ const USAGE = `Usage: npx tsx scripts/oma-models.ts <command> [args]
10
+
11
+ Commands:
12
+ providers-status Show enabled model providers
13
+ models-list [--provider name] [--available]
14
+ List enabled models
15
+ models-validate [--file path] Validate Pi models.json and deployment policy
16
+ auth-set <provider> [--stdin] Store an API key for an enabled provider
17
+ auth-remove <provider> Remove a stored provider API key (idempotent)
18
+ auth-status [provider] Show credential readiness without secrets
19
+ `;
20
+ export async function runModelsCli(argv, runtime) {
21
+ const [command, ...args] = argv;
22
+ if (command === undefined || command === "help" || command === "--help") {
23
+ return { code: command === undefined ? 1 : 0, stdout: USAGE, stderr: "" };
24
+ }
25
+ try {
26
+ switch (command) {
27
+ case "providers-status":
28
+ return ok(providersStatus(loadCatalog(runtime.env)));
29
+ case "models-list": {
30
+ const options = parseModelsListArgs(args);
31
+ return ok(modelsList(loadCatalog(runtime.env), options));
32
+ }
33
+ case "models-validate":
34
+ return ok(modelsValidate(runtime.env, parseModelsValidateArgs(args)));
35
+ case "auth-set":
36
+ return ok(await authSet(runtime, args));
37
+ case "auth-remove":
38
+ return ok(authRemove(runtime.env, args));
39
+ case "auth-status":
40
+ assertAuthStatusArgs(args);
41
+ return ok(authStatus(loadCatalog(runtime.env), args));
42
+ default:
43
+ return fail(`Unknown command: ${command}\n\n${USAGE}`);
44
+ }
45
+ }
46
+ catch (err) {
47
+ return fail(err instanceof Error ? err.message : String(err));
48
+ }
49
+ }
50
+ function loadCatalog(env, overrides = {}) {
51
+ const deployment = parseModelDeploymentConfigFromEnv(env);
52
+ const config = {
53
+ ...deployment,
54
+ ...(overrides.modelsPath === undefined ? {} : { modelsPath: overrides.modelsPath }),
55
+ };
56
+ const catalog = createPiModelCatalog({
57
+ allowedProviders: config.allowedProviders,
58
+ defaultModel: config.defaultModel,
59
+ authPath: config.authPath,
60
+ modelsPath: config.modelsPath,
61
+ allowModelAuthCommands: config.allowModelAuthCommands,
62
+ authBackend: createOmaAuthStorageBackend(config.authPath),
63
+ });
64
+ validateModelDeploymentConfigAgainstCatalog(config, {
65
+ hasProvider: (provider) => catalog.allowedProviders.has(provider),
66
+ hasModel: (model) => catalog.resolve(model) !== undefined,
67
+ });
68
+ return catalog;
69
+ }
70
+ function providersStatus(catalog) {
71
+ const lines = ["provider\tname\tready_models\ttotal_models\tdefault"];
72
+ for (const provider of [...catalog.allowedProviders].sort()) {
73
+ const models = catalog.list({ provider });
74
+ const available = models.filter((model) => catalog.hasConfiguredAuth(model));
75
+ const isDefault = provider === catalog.defaultModel.provider;
76
+ const displayName = catalog.modelRegistry.getProviderDisplayName(provider);
77
+ lines.push([
78
+ provider,
79
+ displayName,
80
+ String(available.length),
81
+ String(models.length),
82
+ isDefault ? "true" : "false",
83
+ ].join("\t"));
84
+ }
85
+ return `${lines.join("\n")}\n`;
86
+ }
87
+ function modelsList(catalog, options) {
88
+ if (options.provider !== undefined && !catalog.allowedProviders.has(options.provider)) {
89
+ throw new Error(`Model provider ${options.provider} is not enabled on this deployment`);
90
+ }
91
+ const models = catalog.list({
92
+ provider: options.provider,
93
+ availableOnly: options.availableOnly,
94
+ }).sort((a, b) => a.provider.localeCompare(b.provider) ||
95
+ a.id.localeCompare(b.id));
96
+ const lines = ["provider\tid\tname\tapi\tcredentials_configured\tdefault"];
97
+ for (const model of models) {
98
+ const isDefault = model.provider === catalog.defaultModel.provider &&
99
+ model.id === catalog.defaultModel.id;
100
+ lines.push([
101
+ model.provider,
102
+ model.id,
103
+ model.name,
104
+ model.api,
105
+ catalog.hasConfiguredAuth(model) ? "true" : "false",
106
+ isDefault ? "true" : "false",
107
+ ].join("\t"));
108
+ }
109
+ return `${lines.join("\n")}\n`;
110
+ }
111
+ function modelsValidate(env, options) {
112
+ const catalog = loadCatalog(env, options.file === undefined ? {} : { modelsPath: options.file });
113
+ const warnings = catalog.securityReport.warnings.map((warning) => `Warning: ${warning}`);
114
+ return [
115
+ ...warnings,
116
+ `Model configuration is valid.`,
117
+ `Enabled providers: ${[...catalog.allowedProviders].sort().join(", ")}`,
118
+ `Default model: ${catalog.defaultModel.provider}/${catalog.defaultModel.id}`,
119
+ "",
120
+ ].join("\n");
121
+ }
122
+ async function authSet(runtime, args) {
123
+ const parsed = parseAuthSetArgs(args);
124
+ const catalog = loadCatalog(runtime.env);
125
+ assertEnabledProvider(catalog, parsed.provider);
126
+ const key = parsed.stdin
127
+ ? await (runtime.readStdin ?? readAllStdin)()
128
+ : await (runtime.promptSecret ?? promptSecret)(`API key for ${parsed.provider}: `);
129
+ const trimmed = key.trim();
130
+ if (trimmed === "")
131
+ throw new Error("API key must not be empty");
132
+ catalog.authStorage.set(parsed.provider, { type: "api_key", key: trimmed });
133
+ return (`Stored API key for ${parsed.provider}.\n` +
134
+ `The key was not printed. Restart \`oma up\` for a running appliance to observe this change.\n`);
135
+ }
136
+ function authRemove(env, args) {
137
+ if (args.length !== 1)
138
+ throw new Error("Usage: oma auth remove <provider>");
139
+ const [provider] = args;
140
+ assertProviderName(provider);
141
+ const catalog = loadCatalog(env);
142
+ catalog.authStorage.remove(provider);
143
+ return `Removed stored API key for ${provider} if one existed. Restart \`oma up\` for a running appliance to observe this change.\n`;
144
+ }
145
+ function authStatus(catalog, args) {
146
+ const providers = args.length === 1 ? [args[0]] : [...catalog.allowedProviders].sort();
147
+ const lines = ["provider\tstored\tready_models\ttotal_models"];
148
+ for (const provider of providers) {
149
+ assertProviderName(provider);
150
+ assertEnabledProvider(catalog, provider);
151
+ const models = catalog.list({ provider });
152
+ const ready = models.filter((model) => catalog.hasConfiguredAuth(model));
153
+ lines.push([
154
+ provider,
155
+ catalog.authStorage.has(provider) ? "true" : "false",
156
+ String(ready.length),
157
+ String(models.length),
158
+ ].join("\t"));
159
+ }
160
+ return `${lines.join("\n")}\n`;
161
+ }
162
+ function assertAuthStatusArgs(args) {
163
+ if (args.length > 1 || args[0]?.startsWith("--")) {
164
+ throw new Error("Usage: oma auth status [provider]");
165
+ }
166
+ }
167
+ function parseModelsListArgs(args) {
168
+ const options = { availableOnly: false };
169
+ for (let index = 0; index < args.length; index += 1) {
170
+ const arg = args[index];
171
+ if (arg === "--provider") {
172
+ const provider = requiredOption(args, ++index, arg);
173
+ assertProviderName(provider);
174
+ options.provider = provider;
175
+ }
176
+ else if (arg === "--available") {
177
+ options.availableOnly = true;
178
+ }
179
+ else {
180
+ throw new Error(`Unknown option for oma models list: ${arg}\nRun \`oma help models list\` for usage.`);
181
+ }
182
+ }
183
+ return options;
184
+ }
185
+ function parseModelsValidateArgs(args) {
186
+ const options = {};
187
+ for (let index = 0; index < args.length; index += 1) {
188
+ const arg = args[index];
189
+ if (arg === "--file") {
190
+ options.file = requiredOption(args, ++index, arg);
191
+ }
192
+ else {
193
+ throw new Error(`Unknown option for oma models validate: ${arg}\nRun \`oma help models validate\` for usage.`);
194
+ }
195
+ }
196
+ return options;
197
+ }
198
+ function parseAuthSetArgs(args) {
199
+ const [provider, ...rest] = args;
200
+ if (!provider)
201
+ throw new Error("Usage: oma auth set <provider> [--stdin]");
202
+ assertProviderName(provider);
203
+ let stdin = false;
204
+ for (const arg of rest) {
205
+ if (arg === "--stdin") {
206
+ stdin = true;
207
+ }
208
+ else if (arg.startsWith("--")) {
209
+ throw new Error(`Unknown option for oma auth set: ${arg}\nRun \`oma help auth set\` for usage.`);
210
+ }
211
+ else {
212
+ throw new Error("oma auth set does not accept API keys as command-line arguments. Use --stdin or the hidden prompt.");
213
+ }
214
+ }
215
+ return { provider, stdin };
216
+ }
217
+ function assertEnabledProvider(catalog, provider) {
218
+ if (!catalog.allowedProviders.has(provider)) {
219
+ throw new Error(`Model provider ${provider} is not enabled on this deployment`);
220
+ }
221
+ }
222
+ function assertProviderName(provider) {
223
+ if (provider === "" || provider.trim() !== provider) {
224
+ throw new Error(`Invalid provider name: ${provider}`);
225
+ }
226
+ }
227
+ function requiredOption(args, index, option) {
228
+ const value = args[index];
229
+ if (value === undefined || value.startsWith("--"))
230
+ throw new Error(`${option} requires a value`);
231
+ return value;
232
+ }
233
+ function ok(stdout) {
234
+ return { code: 0, stdout, stderr: "" };
235
+ }
236
+ function fail(message) {
237
+ return { code: 2, stdout: "", stderr: `${message}\n` };
238
+ }
239
+ async function readAllStdin() {
240
+ let out = "";
241
+ input.setEncoding("utf8");
242
+ for await (const chunk of input)
243
+ out += chunk;
244
+ if (out.endsWith("\n"))
245
+ out = out.slice(0, -1);
246
+ if (out.endsWith("\r"))
247
+ out = out.slice(0, -1);
248
+ if (out.includes("\n") || out.includes("\r")) {
249
+ throw new Error("--stdin accepts exactly one API key value");
250
+ }
251
+ return out;
252
+ }
253
+ async function promptSecret(message) {
254
+ if (!input.isTTY)
255
+ throw new Error("Cannot prompt for an API key on non-interactive stdin; use --stdin");
256
+ errorOut.write(message);
257
+ return await new Promise((resolve, reject) => {
258
+ const chunks = [];
259
+ const wasRaw = input.isRaw;
260
+ const onData = (chunk) => {
261
+ const text = chunk.toString("utf8");
262
+ for (const char of text) {
263
+ if (char === "\n" || char === "\r") {
264
+ cleanup();
265
+ errorOut.write("\n");
266
+ resolve(chunks.join(""));
267
+ return;
268
+ }
269
+ if (char === "\u0003") {
270
+ cleanup();
271
+ errorOut.write("\n");
272
+ reject(new Error("Cancelled"));
273
+ return;
274
+ }
275
+ if (char === "\u007f" || char === "\b") {
276
+ chunks.pop();
277
+ continue;
278
+ }
279
+ chunks.push(char);
280
+ }
281
+ };
282
+ const cleanup = () => {
283
+ input.off("data", onData);
284
+ input.setRawMode(wasRaw);
285
+ input.pause();
286
+ };
287
+ input.resume();
288
+ input.setRawMode(true);
289
+ input.on("data", onData);
290
+ });
291
+ }
292
+ if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
293
+ const result = await runModelsCli(process.argv.slice(2), { env: process.env });
294
+ if (result.stdout)
295
+ process.stdout.write(result.stdout);
296
+ if (result.stderr)
297
+ process.stderr.write(result.stderr);
298
+ process.exit(result.code);
299
+ }
@@ -0,0 +1,133 @@
1
+ // Operator CLI for workspace and API-key provisioning (plan 0113 D8).
2
+ //
3
+ // Runs against the live server's SQLite database through
4
+ // openWorkspaceStoreForProvisioning: same WAL/busy_timeout pragmas, no
5
+ // .oma.lock, so keys take effect without a server restart.
6
+ //
7
+ // OMA_SQLITE_PATH=/path/to/oma.db npx tsx scripts/oma-workspaces.ts <command>
8
+ import { pathToFileURL } from "node:url";
9
+ import { openWorkspaceStoreForProvisioning, } from "../src/control-plane/deployment-storage.js";
10
+ const USAGE = `Usage: npx tsx scripts/oma-workspaces.ts <command> [args]
11
+
12
+ The database path comes from OMA_SQLITE_PATH or --db <path> (the same file
13
+ the OMA server uses). Commands run safely while the server is up; minted and
14
+ revoked keys take effect without a restart.
15
+
16
+ Commands:
17
+ create-workspace <name> Create a workspace and print its id
18
+ list-workspaces List workspace ids and names
19
+ mint-key <workspace_id> [label] Mint an API key. The plaintext is printed
20
+ ONCE and never stored; only its SHA-256
21
+ digest exists at rest.
22
+ list-keys <workspace_id> List key digests, labels, revocation state
23
+ revoke-key <key_sha256> Revoke a key (tombstone; audit row kept)
24
+ `;
25
+ export function runWorkspacesCli(argv, env) {
26
+ const args = [...argv];
27
+ let sqlitePath = env.OMA_SQLITE_PATH;
28
+ const dbFlag = args.indexOf("--db");
29
+ if (dbFlag !== -1) {
30
+ sqlitePath = args[dbFlag + 1];
31
+ if (sqlitePath === undefined)
32
+ return fail("--db requires a path");
33
+ args.splice(dbFlag, 2);
34
+ }
35
+ const [command, ...rest] = args;
36
+ if (command === undefined || command === "help" || command === "--help") {
37
+ return { code: command === undefined ? 1 : 0, stdout: USAGE, stderr: "" };
38
+ }
39
+ if (sqlitePath === undefined || sqlitePath.trim() === "") {
40
+ return fail("Set OMA_SQLITE_PATH or pass --db <path>");
41
+ }
42
+ let store;
43
+ try {
44
+ store = openWorkspaceStoreForProvisioning(sqlitePath);
45
+ }
46
+ catch (error) {
47
+ return fail(error instanceof Error ? error.message : String(error));
48
+ }
49
+ try {
50
+ return dispatch(store, command, rest);
51
+ }
52
+ finally {
53
+ store.close();
54
+ }
55
+ }
56
+ function dispatch(store, command, args) {
57
+ switch (command) {
58
+ case "create-workspace": {
59
+ const [name] = args;
60
+ if (!name)
61
+ return fail("create-workspace requires a name");
62
+ const workspace = store.createWorkspace(name);
63
+ return ok(`Created workspace ${workspace.workspace_id} (${workspace.name})\n` +
64
+ `Next: mint-key ${workspace.workspace_id}\n`);
65
+ }
66
+ case "list-workspaces": {
67
+ const rows = store.listWorkspaces();
68
+ return ok(rows.map((row) => `${row.workspace_id}\t${row.name}`).join("\n") + "\n");
69
+ }
70
+ case "mint-key": {
71
+ const [workspaceId, label = "default"] = args;
72
+ if (!workspaceId)
73
+ return fail("mint-key requires a workspace_id");
74
+ let minted;
75
+ try {
76
+ minted = store.mintKey(workspaceId, label);
77
+ }
78
+ catch (error) {
79
+ return fail(error instanceof Error ? error.message : String(error));
80
+ }
81
+ return ok(`Workspace: ${minted.workspaceId}\n` +
82
+ `Label: ${minted.label}\n` +
83
+ `Key sha256: ${minted.keySha256}\n` +
84
+ `API key: ${minted.plaintextKey}\n` +
85
+ `\n` +
86
+ `This is the ONLY time the key is shown. Store it now.\n` +
87
+ `Clients send it as the x-api-key header.\n`);
88
+ }
89
+ case "list-keys": {
90
+ const [workspaceId] = args;
91
+ if (!workspaceId)
92
+ return fail("list-keys requires a workspace_id");
93
+ if (!store.getWorkspace(workspaceId)) {
94
+ return fail(`Workspace not found: ${workspaceId}`);
95
+ }
96
+ const rows = store.listKeys(workspaceId);
97
+ const lines = rows.map((row) => `${row.key_sha256}\t${row.label}\t` +
98
+ (row.revoked_at === null ? "active" : `revoked ${row.revoked_at}`));
99
+ return ok(lines.length === 0 ? "(no keys)\n" : lines.join("\n") + "\n");
100
+ }
101
+ case "revoke-key": {
102
+ const [keySha256] = args;
103
+ if (!keySha256)
104
+ return fail("revoke-key requires a key_sha256");
105
+ const existing = store.getKey(keySha256);
106
+ if (existing === undefined)
107
+ return fail(`Key not found: ${keySha256}`);
108
+ if (existing.revoked_at !== null) {
109
+ return ok(`Key was already revoked at ${existing.revoked_at}\n`);
110
+ }
111
+ store.revokeKey(keySha256);
112
+ return ok(`Revoked ${keySha256} (workspace ${existing.workspace_id}).\n` +
113
+ `New requests with this key now fail with 401. Already-open SSE\n` +
114
+ `streams run until disconnect; restart the server to sever them.\n`);
115
+ }
116
+ default:
117
+ return fail(`Unknown command: ${command}\n\n${USAGE}`);
118
+ }
119
+ }
120
+ function ok(stdout) {
121
+ return { code: 0, stdout, stderr: "" };
122
+ }
123
+ function fail(message) {
124
+ return { code: 1, stdout: "", stderr: `${message}\n` };
125
+ }
126
+ if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
127
+ const result = runWorkspacesCli(process.argv.slice(2), process.env);
128
+ if (result.stdout)
129
+ process.stdout.write(result.stdout);
130
+ if (result.stderr)
131
+ process.stderr.write(result.stderr);
132
+ process.exit(result.code);
133
+ }