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